summaryrefslogtreecommitdiffstats
path: root/gfx/wr
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
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')
-rw-r--r--gfx/wr/.gitignore27
-rw-r--r--gfx/wr/.taskcluster.yml180
-rw-r--r--gfx/wr/Cargo.lock3334
-rw-r--r--gfx/wr/Cargo.toml27
-rw-r--r--gfx/wr/LICENSE374
-rw-r--r--gfx/wr/README.md53
-rw-r--r--gfx/wr/ci-scripts/docker-image/Dockerfile9
-rwxr-xr-xgfx/wr/ci-scripts/docker-image/setup.sh56
-rw-r--r--gfx/wr/ci-scripts/etc/wr-darwin.meson8
-rwxr-xr-xgfx/wr/ci-scripts/install-meson.sh22
-rwxr-xr-xgfx/wr/ci-scripts/linux-debug-tests.sh36
-rwxr-xr-xgfx/wr/ci-scripts/linux-release-tests.sh29
-rwxr-xr-xgfx/wr/ci-scripts/macos-debug-tests.sh42
-rwxr-xr-xgfx/wr/ci-scripts/macos-release-tests.sh36
-rw-r--r--gfx/wr/ci-scripts/set-screenresolution.ps1124
-rwxr-xr-xgfx/wr/ci-scripts/windows-tests.cmd41
-rw-r--r--gfx/wr/example-compositor/compositor-wayland/Cargo.toml11
-rw-r--r--gfx/wr/example-compositor/compositor-wayland/build.rs63
-rw-r--r--gfx/wr/example-compositor/compositor-wayland/src/lib.cpp772
-rw-r--r--gfx/wr/example-compositor/compositor-wayland/src/lib.rs269
-rw-r--r--gfx/wr/example-compositor/compositor-windows/Cargo.toml9
-rw-r--r--gfx/wr/example-compositor/compositor-windows/build.rs25
-rw-r--r--gfx/wr/example-compositor/compositor-windows/src/lib.cpp694
-rw-r--r--gfx/wr/example-compositor/compositor-windows/src/lib.rs265
-rw-r--r--gfx/wr/example-compositor/compositor/Cargo.toml16
-rw-r--r--gfx/wr/example-compositor/compositor/src/main.rs549
-rw-r--r--gfx/wr/examples/Cargo.toml67
-rw-r--r--gfx/wr/examples/README.md8
-rw-r--r--gfx/wr/examples/alpha_perf.rs95
-rw-r--r--gfx/wr/examples/animation.rs258
-rw-r--r--gfx/wr/examples/basic.rs130
-rw-r--r--gfx/wr/examples/blob.rs290
-rw-r--r--gfx/wr/examples/common/boilerplate.rs323
-rw-r--r--gfx/wr/examples/common/image_helper.rs19
-rw-r--r--gfx/wr/examples/document.rs136
-rw-r--r--gfx/wr/examples/iframe.rs99
-rw-r--r--gfx/wr/examples/image_resize.rs128
-rw-r--r--gfx/wr/examples/multiwindow.rs321
-rw-r--r--gfx/wr/examples/scrolling.rs298
-rw-r--r--gfx/wr/examples/texture_cache_stress.rs322
-rw-r--r--gfx/wr/examples/yuv.rs225
-rw-r--r--gfx/wr/fog/Cargo.toml9
-rw-r--r--gfx/wr/fog/src/main.rs7
-rw-r--r--gfx/wr/glsl-to-cxx/Cargo.toml11
-rw-r--r--gfx/wr/glsl-to-cxx/README.md21
-rw-r--r--gfx/wr/glsl-to-cxx/src/hir.rs4545
-rw-r--r--gfx/wr/glsl-to-cxx/src/lib.rs3681
-rw-r--r--gfx/wr/glsl-to-cxx/src/main.rs8
-rw-r--r--gfx/wr/peek-poke/Cargo.toml17
-rw-r--r--gfx/wr/peek-poke/LICENSE-APACHE201
-rw-r--r--gfx/wr/peek-poke/LICENSE-MIT44
-rw-r--r--gfx/wr/peek-poke/README.md54
-rw-r--r--gfx/wr/peek-poke/peek-poke-derive/Cargo.toml19
-rw-r--r--gfx/wr/peek-poke/peek-poke-derive/LICENSE-APACHE201
-rw-r--r--gfx/wr/peek-poke/peek-poke-derive/LICENSE-MIT44
-rw-r--r--gfx/wr/peek-poke/peek-poke-derive/README.md54
-rw-r--r--gfx/wr/peek-poke/peek-poke-derive/src/lib.rs263
-rw-r--r--gfx/wr/peek-poke/src/euclid.rs191
-rw-r--r--gfx/wr/peek-poke/src/lib.rs440
-rw-r--r--gfx/wr/peek-poke/src/slice_ext.rs19
-rw-r--r--gfx/wr/peek-poke/src/vec_ext.rs26
-rw-r--r--gfx/wr/peek-poke/tests/max_size.rs117
-rw-r--r--gfx/wr/peek-poke/tests/round_trip.rs275
-rw-r--r--gfx/wr/rustfmt.toml6
-rw-r--r--gfx/wr/servo-tidy.toml39
-rw-r--r--gfx/wr/swgl/Cargo.toml15
-rw-r--r--gfx/wr/swgl/README.md220
-rw-r--r--gfx/wr/swgl/build.rs205
-rw-r--r--gfx/wr/swgl/src/blend.h864
-rw-r--r--gfx/wr/swgl/src/composite.h1386
-rw-r--r--gfx/wr/swgl/src/gl.cc2851
-rw-r--r--gfx/wr/swgl/src/gl_defs.h220
-rw-r--r--gfx/wr/swgl/src/glsl.h3117
-rw-r--r--gfx/wr/swgl/src/lib.rs12
-rw-r--r--gfx/wr/swgl/src/program.h186
-rw-r--r--gfx/wr/swgl/src/rasterize.h1680
-rw-r--r--gfx/wr/swgl/src/swgl_ext.h1906
-rw-r--r--gfx/wr/swgl/src/swgl_fns.rs2489
-rw-r--r--gfx/wr/swgl/src/texture.h1299
-rw-r--r--gfx/wr/swgl/src/vector_type.h563
-rw-r--r--gfx/wr/webrender/Cargo.toml63
-rw-r--r--gfx/wr/webrender/build.rs352
-rw-r--r--gfx/wr/webrender/doc/CLIPPING_AND_POSITIONING.md150
-rw-r--r--gfx/wr/webrender/doc/blob.md43
-rw-r--r--gfx/wr/webrender/doc/swizzling.md31
-rw-r--r--gfx/wr/webrender/doc/text-rendering.md720
-rw-r--r--gfx/wr/webrender/res/Proggy.ttfbin0 -> 5284 bytes
-rw-r--r--gfx/wr/webrender/res/area-lut.tgabin0 -> 65580 bytes
-rw-r--r--gfx/wr/webrender/res/base.glsl70
-rw-r--r--gfx/wr/webrender/res/blend.glsl238
-rw-r--r--gfx/wr/webrender/res/brush.glsl256
-rw-r--r--gfx/wr/webrender/res/brush_blend.glsl121
-rw-r--r--gfx/wr/webrender/res/brush_image.glsl393
-rw-r--r--gfx/wr/webrender/res/brush_linear_gradient.glsl95
-rw-r--r--gfx/wr/webrender/res/brush_mix_blend.glsl332
-rw-r--r--gfx/wr/webrender/res/brush_opacity.glsl83
-rw-r--r--gfx/wr/webrender/res/brush_solid.glsl60
-rw-r--r--gfx/wr/webrender/res/brush_yuv_image.glsl140
-rw-r--r--gfx/wr/webrender/res/clip_shared.glsl80
-rw-r--r--gfx/wr/webrender/res/composite.glsl242
-rw-r--r--gfx/wr/webrender/res/cs_blur.glsl196
-rw-r--r--gfx/wr/webrender/res/cs_border_segment.glsl450
-rw-r--r--gfx/wr/webrender/res/cs_border_solid.glsl178
-rw-r--r--gfx/wr/webrender/res/cs_clip_box_shadow.glsl327
-rw-r--r--gfx/wr/webrender/res/cs_clip_image.glsl117
-rw-r--r--gfx/wr/webrender/res/cs_clip_rectangle.glsl498
-rw-r--r--gfx/wr/webrender/res/cs_conic_gradient.glsl67
-rw-r--r--gfx/wr/webrender/res/cs_fast_linear_gradient.glsl32
-rw-r--r--gfx/wr/webrender/res/cs_line_decoration.glsl165
-rw-r--r--gfx/wr/webrender/res/cs_linear_gradient.glsl68
-rw-r--r--gfx/wr/webrender/res/cs_radial_gradient.glsl71
-rw-r--r--gfx/wr/webrender/res/cs_scale.glsl62
-rw-r--r--gfx/wr/webrender/res/cs_svg_filter.glsl594
-rw-r--r--gfx/wr/webrender/res/debug_color.glsl24
-rw-r--r--gfx/wr/webrender/res/debug_font.glsl30
-rw-r--r--gfx/wr/webrender/res/ellipse.glsl93
-rw-r--r--gfx/wr/webrender/res/gpu_buffer.glsl42
-rw-r--r--gfx/wr/webrender/res/gpu_cache.glsl137
-rw-r--r--gfx/wr/webrender/res/gpu_cache_update.glsl27
-rw-r--r--gfx/wr/webrender/res/gradient.glsl63
-rw-r--r--gfx/wr/webrender/res/gradient_shared.glsl78
-rw-r--r--gfx/wr/webrender/res/prim_shared.glsl250
-rw-r--r--gfx/wr/webrender/res/ps_clear.glsl25
-rw-r--r--gfx/wr/webrender/res/ps_copy.glsl41
-rw-r--r--gfx/wr/webrender/res/ps_quad.glsl287
-rw-r--r--gfx/wr/webrender/res/ps_quad_mask.glsl165
-rw-r--r--gfx/wr/webrender/res/ps_quad_textured.glsl63
-rw-r--r--gfx/wr/webrender/res/ps_split_composite.glsl134
-rw-r--r--gfx/wr/webrender/res/ps_text_run.glsl354
-rw-r--r--gfx/wr/webrender/res/rect.glsl40
-rw-r--r--gfx/wr/webrender/res/render_task.glsl102
-rw-r--r--gfx/wr/webrender/res/shared.glsl230
-rw-r--r--gfx/wr/webrender/res/shared_other.glsl33
-rw-r--r--gfx/wr/webrender/res/transform.glsl140
-rw-r--r--gfx/wr/webrender/res/yuv.glsl237
-rw-r--r--gfx/wr/webrender/src/api_resources.rs305
-rw-r--r--gfx/wr/webrender/src/batch.rs3971
-rw-r--r--gfx/wr/webrender/src/border.rs1473
-rw-r--r--gfx/wr/webrender/src/box_shadow.rs280
-rw-r--r--gfx/wr/webrender/src/capture.rs290
-rw-r--r--gfx/wr/webrender/src/clip.rs2310
-rw-r--r--gfx/wr/webrender/src/command_buffer.rs436
-rw-r--r--gfx/wr/webrender/src/composite.rs1484
-rw-r--r--gfx/wr/webrender/src/compositor/mod.rs6
-rw-r--r--gfx/wr/webrender/src/compositor/sw_compositor.rs1538
-rw-r--r--gfx/wr/webrender/src/debug_colors.rs159
-rw-r--r--gfx/wr/webrender/src/debug_font_data.rs1914
-rw-r--r--gfx/wr/webrender/src/debug_item.rs26
-rw-r--r--gfx/wr/webrender/src/device/gl.rs4814
-rw-r--r--gfx/wr/webrender/src/device/mod.rs9
-rw-r--r--gfx/wr/webrender/src/device/query_gl.rs318
-rw-r--r--gfx/wr/webrender/src/ellipse.rs187
-rw-r--r--gfx/wr/webrender/src/filterdata.rs219
-rw-r--r--gfx/wr/webrender/src/frame_builder.rs1014
-rw-r--r--gfx/wr/webrender/src/freelist.rs264
-rw-r--r--gfx/wr/webrender/src/glyph_cache.rs208
-rw-r--r--gfx/wr/webrender/src/gpu_cache.rs937
-rw-r--r--gfx/wr/webrender/src/gpu_types.rs955
-rw-r--r--gfx/wr/webrender/src/hit_test.rs420
-rw-r--r--gfx/wr/webrender/src/image_source.rs93
-rw-r--r--gfx/wr/webrender/src/image_tiling.rs823
-rw-r--r--gfx/wr/webrender/src/intern.rs470
-rw-r--r--gfx/wr/webrender/src/internal_types.rs761
-rw-r--r--gfx/wr/webrender/src/lib.rs194
-rw-r--r--gfx/wr/webrender/src/lru_cache.rs675
-rw-r--r--gfx/wr/webrender/src/picture.rs7199
-rw-r--r--gfx/wr/webrender/src/picture_graph.rs212
-rw-r--r--gfx/wr/webrender/src/picture_textures.rs382
-rw-r--r--gfx/wr/webrender/src/prepare.rs2035
-rw-r--r--gfx/wr/webrender/src/prim_store/backdrop.rs175
-rw-r--r--gfx/wr/webrender/src/prim_store/borders.rs387
-rw-r--r--gfx/wr/webrender/src/prim_store/gradient/conic.rs399
-rw-r--r--gfx/wr/webrender/src/prim_store/gradient/linear.rs750
-rw-r--r--gfx/wr/webrender/src/prim_store/gradient/mod.rs392
-rw-r--r--gfx/wr/webrender/src/prim_store/gradient/radial.rs531
-rw-r--r--gfx/wr/webrender/src/prim_store/image.rs682
-rw-r--r--gfx/wr/webrender/src/prim_store/interned.rs14
-rw-r--r--gfx/wr/webrender/src/prim_store/line_dec.rs257
-rw-r--r--gfx/wr/webrender/src/prim_store/mod.rs1455
-rw-r--r--gfx/wr/webrender/src/prim_store/picture.rs328
-rw-r--r--gfx/wr/webrender/src/prim_store/storage.rs156
-rw-r--r--gfx/wr/webrender/src/prim_store/text_run.rs505
-rw-r--r--gfx/wr/webrender/src/print_tree.rs122
-rw-r--r--gfx/wr/webrender/src/profiler.rs1856
-rw-r--r--gfx/wr/webrender/src/rectangle_occlusion.rs208
-rw-r--r--gfx/wr/webrender/src/render_api.rs1416
-rw-r--r--gfx/wr/webrender/src/render_backend.rs1946
-rw-r--r--gfx/wr/webrender/src/render_target.rs1035
-rw-r--r--gfx/wr/webrender/src/render_task.rs1560
-rw-r--r--gfx/wr/webrender/src/render_task_cache.rs379
-rw-r--r--gfx/wr/webrender/src/render_task_graph.rs1234
-rw-r--r--gfx/wr/webrender/src/renderer/debug.rs415
-rw-r--r--gfx/wr/webrender/src/renderer/gpu_buffer.rs326
-rw-r--r--gfx/wr/webrender/src/renderer/gpu_cache.rs525
-rw-r--r--gfx/wr/webrender/src/renderer/init.rs788
-rw-r--r--gfx/wr/webrender/src/renderer/mod.rs5923
-rw-r--r--gfx/wr/webrender/src/renderer/shade.rs1496
-rw-r--r--gfx/wr/webrender/src/renderer/upload.rs847
-rw-r--r--gfx/wr/webrender/src/renderer/vertex.rs1154
-rw-r--r--gfx/wr/webrender/src/resource_cache.rs2308
-rw-r--r--gfx/wr/webrender/src/scene.rs364
-rw-r--r--gfx/wr/webrender/src/scene_builder_thread.rs794
-rw-r--r--gfx/wr/webrender/src/scene_building.rs4123
-rw-r--r--gfx/wr/webrender/src/screen_capture.rs495
-rw-r--r--gfx/wr/webrender/src/segment.rs1374
-rw-r--r--gfx/wr/webrender/src/space.rs269
-rw-r--r--gfx/wr/webrender/src/spatial_node.rs995
-rw-r--r--gfx/wr/webrender/src/spatial_tree.rs1998
-rw-r--r--gfx/wr/webrender/src/surface.rs612
-rw-r--r--gfx/wr/webrender/src/telemetry.rs41
-rw-r--r--gfx/wr/webrender/src/texture_cache.rs1707
-rw-r--r--gfx/wr/webrender/src/texture_pack/guillotine.rs284
-rw-r--r--gfx/wr/webrender/src/texture_pack/mod.rs441
-rw-r--r--gfx/wr/webrender/src/tile_cache.rs745
-rw-r--r--gfx/wr/webrender/src/util.rs1631
-rw-r--r--gfx/wr/webrender/src/visibility.rs382
-rw-r--r--gfx/wr/webrender/tests/angle_shader_validation.rs75
-rw-r--r--gfx/wr/webrender_api/Cargo.toml28
-rw-r--r--gfx/wr/webrender_api/src/channel.rs180
-rw-r--r--gfx/wr/webrender_api/src/color.rs161
-rw-r--r--gfx/wr/webrender_api/src/display_item.rs1801
-rw-r--r--gfx/wr/webrender_api/src/display_item_cache.rs115
-rw-r--r--gfx/wr/webrender_api/src/display_list.rs2338
-rw-r--r--gfx/wr/webrender_api/src/font.rs435
-rw-r--r--gfx/wr/webrender_api/src/gradient_builder.rs180
-rw-r--r--gfx/wr/webrender_api/src/image.rs580
-rw-r--r--gfx/wr/webrender_api/src/lib.rs768
-rw-r--r--gfx/wr/webrender_api/src/units.rs346
-rw-r--r--gfx/wr/webrender_build/Cargo.toml16
-rw-r--r--gfx/wr/webrender_build/src/lib.rs19
-rw-r--r--gfx/wr/webrender_build/src/shader.rs240
-rw-r--r--gfx/wr/webrender_build/src/shader_features.rs240
-rw-r--r--gfx/wr/wr_glyph_rasterizer/Cargo.toml51
-rw-r--r--gfx/wr/wr_glyph_rasterizer/README.md3
-rw-r--r--gfx/wr/wr_glyph_rasterizer/examples/basic.rs177
-rw-r--r--gfx/wr/wr_glyph_rasterizer/examples/common/boilerplate.rs298
-rw-r--r--gfx/wr/wr_glyph_rasterizer/src/gamma_lut.rs413
-rw-r--r--gfx/wr/wr_glyph_rasterizer/src/lib.rs60
-rw-r--r--gfx/wr/wr_glyph_rasterizer/src/platform/macos/font.rs899
-rw-r--r--gfx/wr/wr_glyph_rasterizer/src/platform/unix/font.rs1099
-rw-r--r--gfx/wr/wr_glyph_rasterizer/src/platform/windows/font.rs639
-rw-r--r--gfx/wr/wr_glyph_rasterizer/src/profiler.rs12
-rw-r--r--gfx/wr/wr_glyph_rasterizer/src/rasterizer.rs1879
-rw-r--r--gfx/wr/wr_glyph_rasterizer/src/telemetry.rs28
-rw-r--r--gfx/wr/wr_glyph_rasterizer/src/types.rs10
-rw-r--r--gfx/wr/wr_malloc_size_of/Cargo.toml14
-rw-r--r--gfx/wr/wr_malloc_size_of/LICENSE-APACHE201
-rw-r--r--gfx/wr/wr_malloc_size_of/LICENSE-MIT23
-rw-r--r--gfx/wr/wr_malloc_size_of/lib.rs451
-rw-r--r--gfx/wr/wrench/.gitignore7
-rw-r--r--gfx/wr/wrench/Cargo.toml76
-rw-r--r--gfx/wr/wrench/README.md26
-rw-r--r--gfx/wr/wrench/android.txt100
-rw-r--r--gfx/wr/wrench/benchmarks/aligned-gradient.yaml62
-rw-r--r--gfx/wr/wrench/benchmarks/benchmarks.list11
-rw-r--r--gfx/wr/wrench/benchmarks/box-shadow-large.yaml17
-rw-r--r--gfx/wr/wrench/benchmarks/clip-clear.yaml48
-rw-r--r--gfx/wr/wrench/benchmarks/large-blur-radius.yaml10
-rw-r--r--gfx/wr/wrench/benchmarks/large-boxshadow-ellipse-2.yaml15
-rw-r--r--gfx/wr/wrench/benchmarks/large-boxshadow-ellipse.yaml15
-rw-r--r--gfx/wr/wrench/benchmarks/large-clip-rect.yaml33
-rw-r--r--gfx/wr/wrench/benchmarks/many-box-shadows.yaml115
-rw-r--r--gfx/wr/wrench/benchmarks/many-images.yaml16386
-rw-r--r--gfx/wr/wrench/benchmarks/overlapping-text-shadows.yaml807
-rw-r--r--gfx/wr/wrench/benchmarks/radial-gradient.yaml52
-rw-r--r--gfx/wr/wrench/benchmarks/simple-batching.yaml45
-rw-r--r--gfx/wr/wrench/benchmarks/text-rendering.yaml260
-rw-r--r--gfx/wr/wrench/benchmarks/transforms-simple.yaml44
-rw-r--r--gfx/wr/wrench/benchmarks/unaligned-gradient.yaml62
-rw-r--r--gfx/wr/wrench/build.rs28
-rw-r--r--gfx/wr/wrench/examples/animated.anim13
-rw-r--r--gfx/wr/wrench/examples/animated.yaml13
-rw-r--r--gfx/wr/wrench/invalidation/basic.yaml6
-rw-r--r--gfx/wr/wrench/invalidation/composite_nop_1.yaml6
-rw-r--r--gfx/wr/wrench/invalidation/composite_nop_2.yaml6
-rw-r--r--gfx/wr/wrench/reftests/aa/aa-dist-bug-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/aa/aa-dist-bug.yaml48
-rw-r--r--gfx/wr/wrench/reftests/aa/fractional-radii-ref.yaml18
-rw-r--r--gfx/wr/wrench/reftests/aa/fractional-radii.yaml54
-rw-r--r--gfx/wr/wrench/reftests/aa/reftest.list3
-rw-r--r--gfx/wr/wrench/reftests/aa/rounded-rects-ref.pngbin0 -> 10886 bytes
-rw-r--r--gfx/wr/wrench/reftests/aa/rounded-rects.yaml56
-rw-r--r--gfx/wr/wrench/reftests/backface/backface-3d-leaf.yaml18
-rw-r--r--gfx/wr/wrench/reftests/backface/backface-both-sides-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/backface/backface-both-sides.yaml25
-rw-r--r--gfx/wr/wrench/reftests/backface/backface-double-flip.yaml21
-rw-r--r--gfx/wr/wrench/reftests/backface/backface-flatten.yaml31
-rw-r--r--gfx/wr/wrench/reftests/backface/backface-hidden.yaml18
-rw-r--r--gfx/wr/wrench/reftests/backface/backface-leaf-ref.yaml12
-rw-r--r--gfx/wr/wrench/reftests/backface/backface-leaf.yaml16
-rw-r--r--gfx/wr/wrench/reftests/backface/backface-perspective-ref.yaml16
-rw-r--r--gfx/wr/wrench/reftests/backface/backface-perspective.yaml20
-rw-r--r--gfx/wr/wrench/reftests/backface/backface-picture-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/backface/backface-picture.yaml21
-rw-r--r--gfx/wr/wrench/reftests/backface/backface-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/backface/backface-sc.yaml17
-rw-r--r--gfx/wr/wrench/reftests/backface/backface-vis-3d.yaml16
-rw-r--r--gfx/wr/wrench/reftests/backface/blank.yaml2
-rw-r--r--gfx/wr/wrench/reftests/backface/reftest.list10
-rw-r--r--gfx/wr/wrench/reftests/blend/backdrop-filter-blend-container-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/blend/backdrop-filter-blend-container.yaml21
-rw-r--r--gfx/wr/wrench/reftests/blend/backdrop-filter-preceding-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/blend/backdrop-filter-preceding.yaml24
-rw-r--r--gfx/wr/wrench/reftests/blend/blank.yaml2
-rw-r--r--gfx/wr/wrench/reftests/blend/blend-overflow-ref.yaml14
-rw-r--r--gfx/wr/wrench/reftests/blend/blend-overflow.yaml18
-rw-r--r--gfx/wr/wrench/reftests/blend/child-surface-ref.yaml17
-rw-r--r--gfx/wr/wrench/reftests/blend/child-surface.yaml22
-rw-r--r--gfx/wr/wrench/reftests/blend/darken-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/blend/darken.yaml16
-rw-r--r--gfx/wr/wrench/reftests/blend/difference-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/blend/difference-transparent-ref.yaml23
-rw-r--r--gfx/wr/wrench/reftests/blend/difference-transparent.yaml22
-rw-r--r--gfx/wr/wrench/reftests/blend/difference.yaml16
-rw-r--r--gfx/wr/wrench/reftests/blend/isolated-2-ref.yaml21
-rw-r--r--gfx/wr/wrench/reftests/blend/isolated-2.yaml25
-rw-r--r--gfx/wr/wrench/reftests/blend/isolated-premultiplied-2-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/blend/isolated-premultiplied-2.yaml14
-rw-r--r--gfx/wr/wrench/reftests/blend/isolated-premultiplied.yaml24
-rw-r--r--gfx/wr/wrench/reftests/blend/isolated-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/blend/isolated-with-filter.yaml19
-rw-r--r--gfx/wr/wrench/reftests/blend/isolated.yaml19
-rw-r--r--gfx/wr/wrench/reftests/blend/large-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/blend/large.yaml14
-rw-r--r--gfx/wr/wrench/reftests/blend/lighten-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/blend/lighten.yaml16
-rw-r--r--gfx/wr/wrench/reftests/blend/mix-blend-complex-transform.pngbin0 -> 34481 bytes
-rw-r--r--gfx/wr/wrench/reftests/blend/mix-blend-complex-transform.yaml56
-rw-r--r--gfx/wr/wrench/reftests/blend/mix-blend-invalid-backdrop-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/blend/mix-blend-invalid-backdrop.yaml33
-rw-r--r--gfx/wr/wrench/reftests/blend/multi-mix-blend-mode-ref.yaml14
-rw-r--r--gfx/wr/wrench/reftests/blend/multi-mix-blend-mode.yaml25
-rw-r--r--gfx/wr/wrench/reftests/blend/multiply-2-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/blend/multiply-2.yaml16
-rw-r--r--gfx/wr/wrench/reftests/blend/multiply-3.yaml20
-rw-r--r--gfx/wr/wrench/reftests/blend/multiply-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/blend/multiply.yaml16
-rw-r--r--gfx/wr/wrench/reftests/blend/raster-roots-1-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/blend/raster-roots-1.yaml32
-rw-r--r--gfx/wr/wrench/reftests/blend/reftest.list32
-rw-r--r--gfx/wr/wrench/reftests/blend/repeated-difference-ref.yaml7
-rw-r--r--gfx/wr/wrench/reftests/blend/repeated-difference.yaml31
-rw-r--r--gfx/wr/wrench/reftests/blend/transparent-composite-1-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/blend/transparent-composite-1.yaml12
-rw-r--r--gfx/wr/wrench/reftests/blend/transparent-composite-2-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/blend/transparent-composite-2.yaml16
-rw-r--r--gfx/wr/wrench/reftests/blend/transparent-white.pngbin0 -> 303 bytes
-rw-r--r--gfx/wr/wrench/reftests/border/blank.yaml3
-rw-r--r--gfx/wr/wrench/reftests/border/border-clamp-corner-radius.pngbin0 -> 12265 bytes
-rw-r--r--gfx/wr/wrench/reftests/border/border-clamp-corner-radius.yaml24
-rw-r--r--gfx/wr/wrench/reftests/border/border-dashed-dotted-caching.pngbin0 -> 6793 bytes
-rw-r--r--gfx/wr/wrench/reftests/border/border-dashed-dotted-caching.yaml76
-rw-r--r--gfx/wr/wrench/reftests/border/border-double-1px-ref.yaml17
-rw-r--r--gfx/wr/wrench/reftests/border/border-double-1px.yaml11
-rw-r--r--gfx/wr/wrench/reftests/border/border-double-simple-2-ref.yaml12
-rw-r--r--gfx/wr/wrench/reftests/border/border-double-simple-2.yaml12
-rw-r--r--gfx/wr/wrench/reftests/border/border-double-simple-ref.yaml21
-rw-r--r--gfx/wr/wrench/reftests/border/border-double-simple.yaml12
-rw-r--r--gfx/wr/wrench/reftests/border/border-gradient-nine-patch.pngbin0 -> 1283 bytes
-rw-r--r--gfx/wr/wrench/reftests/border/border-gradient-nine-patch.yaml11
-rw-r--r--gfx/wr/wrench/reftests/border/border-gradient-simple-ref.yaml55
-rw-r--r--gfx/wr/wrench/reftests/border/border-gradient-simple.yaml13
-rw-r--r--gfx/wr/wrench/reftests/border/border-groove-simple-ref.yaml21
-rw-r--r--gfx/wr/wrench/reftests/border/border-groove-simple.yaml12
-rw-r--r--gfx/wr/wrench/reftests/border/border-image-crash-ref.yaml20
-rw-r--r--gfx/wr/wrench/reftests/border/border-image-crash.yaml20
-rw-r--r--gfx/wr/wrench/reftests/border/border-image-empty-slice-ref.pngbin0 -> 9635 bytes
-rw-r--r--gfx/wr/wrench/reftests/border/border-image-empty-slice.yaml46
-rw-r--r--gfx/wr/wrench/reftests/border/border-image-fill-2-ref.pngbin0 -> 25443 bytes
-rw-r--r--gfx/wr/wrench/reftests/border/border-image-fill-2.yaml51
-rw-r--r--gfx/wr/wrench/reftests/border/border-image-fill-ref.pngbin0 -> 20384 bytes
-rw-r--r--gfx/wr/wrench/reftests/border/border-image-fill.yaml50
-rw-r--r--gfx/wr/wrench/reftests/border/border-image-ref.pngbin0 -> 5519 bytes
-rw-r--r--gfx/wr/wrench/reftests/border/border-image-round-ref.pngbin0 -> 14815 bytes
-rw-r--r--gfx/wr/wrench/reftests/border/border-image-round.yaml39
-rw-r--r--gfx/wr/wrench/reftests/border/border-image-src-2.pngbin0 -> 2215 bytes
-rw-r--r--gfx/wr/wrench/reftests/border/border-image-src.pngbin0 -> 1247 bytes
-rw-r--r--gfx/wr/wrench/reftests/border/border-image.yaml16
-rw-r--r--gfx/wr/wrench/reftests/border/border-invisible-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/border/border-invisible.yaml17
-rw-r--r--gfx/wr/wrench/reftests/border/border-no-bogus-line-ref.pngbin0 -> 2294 bytes
-rw-r--r--gfx/wr/wrench/reftests/border/border-no-bogus-line.yaml13
-rw-r--r--gfx/wr/wrench/reftests/border/border-none-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/border/border-none.yaml12
-rw-r--r--gfx/wr/wrench/reftests/border/border-overlapping-corner-ref.yaml22
-rw-r--r--gfx/wr/wrench/reftests/border/border-overlapping-corner.yaml17
-rw-r--r--gfx/wr/wrench/reftests/border/border-overlapping-edge-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/border/border-overlapping-edge.yaml12
-rw-r--r--gfx/wr/wrench/reftests/border/border-radial-gradient-nine-patch.pngbin0 -> 2394 bytes
-rw-r--r--gfx/wr/wrench/reftests/border/border-radial-gradient-nine-patch.yaml11
-rw-r--r--gfx/wr/wrench/reftests/border/border-radial-gradient-simple-ref.yaml54
-rw-r--r--gfx/wr/wrench/reftests/border/border-radial-gradient-simple.yaml13
-rw-r--r--gfx/wr/wrench/reftests/border/border-radii.pngbin0 -> 1035 bytes
-rw-r--r--gfx/wr/wrench/reftests/border/border-radii.yaml17
-rw-r--r--gfx/wr/wrench/reftests/border/border-ridge-simple-ref.yaml21
-rw-r--r--gfx/wr/wrench/reftests/border/border-ridge-simple.yaml12
-rw-r--r--gfx/wr/wrench/reftests/border/border-suite-2.pngbin0 -> 48233 bytes
-rw-r--r--gfx/wr/wrench/reftests/border/border-suite-2.yaml171
-rw-r--r--gfx/wr/wrench/reftests/border/border-suite-3.pngbin0 -> 25873 bytes
-rw-r--r--gfx/wr/wrench/reftests/border/border-suite-3.yaml57
-rw-r--r--gfx/wr/wrench/reftests/border/border-suite.pngbin0 -> 64319 bytes
-rw-r--r--gfx/wr/wrench/reftests/border/border-suite.yaml351
-rw-r--r--gfx/wr/wrench/reftests/border/degenerate-curve.pngbin0 -> 20687 bytes
-rw-r--r--gfx/wr/wrench/reftests/border/degenerate-curve.yaml293
-rw-r--r--gfx/wr/wrench/reftests/border/discontinued-dash.pngbin0 -> 1915 bytes
-rw-r--r--gfx/wr/wrench/reftests/border/discontinued-dash.yaml12
-rw-r--r--gfx/wr/wrench/reftests/border/dotted-corner-small-radius.pngbin0 -> 8741 bytes
-rw-r--r--gfx/wr/wrench/reftests/border/dotted-corner-small-radius.yaml11
-rw-r--r--gfx/wr/wrench/reftests/border/green-square.yaml9
-rw-r--r--gfx/wr/wrench/reftests/border/max-scale-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/border/max-scale.yaml12
-rw-r--r--gfx/wr/wrench/reftests/border/no-aa.yaml20
-rw-r--r--gfx/wr/wrench/reftests/border/overlapping.pngbin0 -> 4074 bytes
-rw-r--r--gfx/wr/wrench/reftests/border/overlapping.yaml22
-rw-r--r--gfx/wr/wrench/reftests/border/reftest.list35
-rw-r--r--gfx/wr/wrench/reftests/border/small-dotted-border.pngbin0 -> 1551 bytes
-rw-r--r--gfx/wr/wrench/reftests/border/small-dotted-border.yaml19
-rw-r--r--gfx/wr/wrench/reftests/border/small-inset-outset-notref.yaml12
-rw-r--r--gfx/wr/wrench/reftests/border/small-inset-outset.yaml12
-rw-r--r--gfx/wr/wrench/reftests/border/zero-width.yaml12
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/blank.yaml2
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-blurred-overlapping-radii-ref.yaml20
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-blurred-overlapping-radii.yaml19
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-border-radii.pngbin0 -> 11567 bytes
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-border-radii.yaml36
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-cache.pngbin0 -> 33603 bytes
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-cache.yaml98
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-clip-ref.yaml59
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-clip.yaml32
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-empty.yaml13
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-huge-radius.pngbin0 -> 15792 bytes
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-huge-radius.yaml120
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius-2.pngbin0 -> 125598 bytes
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius-2.yaml14
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius-3.pngbin0 -> 56821 bytes
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius-3.yaml14
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius-ref.yaml13
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius.yaml13
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-non-uniform-corner.pngbin0 -> 2018 bytes
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-non-uniform-corner.yaml12
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-spread-radii-ref.yaml21
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-spread-radii.yaml18
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-spread.pngbin0 -> 7573 bytes
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-spread.yaml68
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-stretch-mode-x.pngbin0 -> 13529 bytes
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-stretch-mode-x.yaml8
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-stretch-mode-y.pngbin0 -> 5612 bytes
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-stretch-mode-y.yaml9
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-suite-blur.pngbin0 -> 86873 bytes
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-suite-blur.yaml154
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-suite-no-blur.pngbin0 -> 21209 bytes
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/box-shadow-suite-no-blur.yaml134
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/boxshadow-spread-only-ref.pngbin0 -> 19562 bytes
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/boxshadow-spread-only.yaml13
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-alpha.pngbin0 -> 1619 bytes
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-alpha.yaml14
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-border-radius-ref.yaml16
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-border-radius.pngbin0 -> 3016 bytes
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-border-radius.yaml11
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-downscale.pngbin0 -> 12980 bytes
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-downscale.yaml10
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-empty.yaml13
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-large-offset-ref.pngbin0 -> 4086 bytes
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-large-offset.yaml13
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-mask-region.pngbin0 -> 24233 bytes
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-mask-region.yaml11
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-neg-offset.pngbin0 -> 3048 bytes
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-neg-offset.yaml18
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-no-blur-radius-ref.pngbin0 -> 672 bytes
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-no-blur-radius.yaml13
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-offset.pngbin0 -> 3114 bytes
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-offset.yaml18
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-simple-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-simple.yaml13
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-spread-large-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-spread-large.yaml12
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-spread-ref.yaml10
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-spread.yaml12
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-subpx.pngbin0 -> 1809 bytes
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/inset-subpx.yaml12
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/invalid-ref.yaml5
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/invalid.yaml12
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/no-stretch.pngbin0 -> 4020 bytes
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/no-stretch.yaml21
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/overlap1.pngbin0 -> 10907 bytes
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/overlap1.yaml18
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/overlap2.pngbin0 -> 8491 bytes
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/overlap2.yaml19
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/reftest.list38
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/rounding-ref.yaml18
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/rounding.yaml15
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/scale.pngbin0 -> 13662 bytes
-rw-r--r--gfx/wr/wrench/reftests/boxshadow/scale.yaml183
-rw-r--r--gfx/wr/wrench/reftests/clip/blank.yaml2
-rw-r--r--gfx/wr/wrench/reftests/clip/blend-container-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/clip/blend-container.yaml19
-rw-r--r--gfx/wr/wrench/reftests/clip/border-with-rounded-clip.pngbin0 -> 1846 bytes
-rw-r--r--gfx/wr/wrench/reftests/clip/border-with-rounded-clip.yaml23
-rw-r--r--gfx/wr/wrench/reftests/clip/clip-3d-transform-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/clip/clip-3d-transform.yaml33
-rw-r--r--gfx/wr/wrench/reftests/clip/clip-45-degree-rotation-ref.pngbin0 -> 13211 bytes
-rw-r--r--gfx/wr/wrench/reftests/clip/clip-45-degree-rotation.yaml35
-rw-r--r--gfx/wr/wrench/reftests/clip/clip-and-filter-with-rotation-ref.yaml18
-rw-r--r--gfx/wr/wrench/reftests/clip/clip-and-filter-with-rotation.yaml36
-rw-r--r--gfx/wr/wrench/reftests/clip/clip-corner-overlap-ref.yaml117
-rw-r--r--gfx/wr/wrench/reftests/clip/clip-corner-overlap.yaml61
-rw-r--r--gfx/wr/wrench/reftests/clip/clip-ellipse.pngbin0 -> 9542 bytes
-rw-r--r--gfx/wr/wrench/reftests/clip/clip-ellipse.yaml110
-rw-r--r--gfx/wr/wrench/reftests/clip/clip-empty-inner-rect-ref.yaml20
-rw-r--r--gfx/wr/wrench/reftests/clip/clip-empty-inner-rect.yaml29
-rw-r--r--gfx/wr/wrench/reftests/clip/clip-filter-raster-root-ref.yaml16
-rw-r--r--gfx/wr/wrench/reftests/clip/clip-filter-raster-root.yaml27
-rw-r--r--gfx/wr/wrench/reftests/clip/clip-mode.pngbin0 -> 2783 bytes
-rw-r--r--gfx/wr/wrench/reftests/clip/clip-mode.yaml29
-rw-r--r--gfx/wr/wrench/reftests/clip/clip-rectangle-redundant-sc-ref.yaml7
-rw-r--r--gfx/wr/wrench/reftests/clip/clip-rectangle-redundant-sc.yaml20
-rw-r--r--gfx/wr/wrench/reftests/clip/clip-thin-rotated-ref.yaml13
-rw-r--r--gfx/wr/wrench/reftests/clip/clip-thin-rotated.yaml41
-rw-r--r--gfx/wr/wrench/reftests/clip/clipped-occlusion-ref.yaml7
-rw-r--r--gfx/wr/wrench/reftests/clip/clipped-occlusion.yaml25
-rw-r--r--gfx/wr/wrench/reftests/clip/custom-clip-chain-node-ancestors-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/clip/custom-clip-chain-node-ancestors.yaml31
-rw-r--r--gfx/wr/wrench/reftests/clip/fixed-position-clipping-ref.yaml15
-rw-r--r--gfx/wr/wrench/reftests/clip/fixed-position-clipping.yaml45
-rw-r--r--gfx/wr/wrench/reftests/clip/iframe-nested-in-stacking-context-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/clip/iframe-nested-in-stacking-context.yaml27
-rw-r--r--gfx/wr/wrench/reftests/clip/raster-roots-tiled-mask-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/clip/raster-roots-tiled-mask.yaml22
-rw-r--r--gfx/wr/wrench/reftests/clip/reftest.list20
-rw-r--r--gfx/wr/wrench/reftests/clip/segmentation-across-rotation-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/clip/segmentation-across-rotation.yaml31
-rw-r--r--gfx/wr/wrench/reftests/clip/segmentation-with-other-coordinate-system-clip-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/clip/segmentation-with-other-coordinate-system-clip.pngbin0 -> 763 bytes
-rw-r--r--gfx/wr/wrench/reftests/clip/segmentation-with-other-coordinate-system-clip.yaml48
-rw-r--r--gfx/wr/wrench/reftests/clip/snapping-ref.yaml17
-rw-r--r--gfx/wr/wrench/reftests/clip/snapping.yaml17
-rw-r--r--gfx/wr/wrench/reftests/clip/stacking-context-clip-ref.yaml37
-rw-r--r--gfx/wr/wrench/reftests/clip/stacking-context-clip.yaml43
-rw-r--r--gfx/wr/wrench/reftests/compositor-surface/basic-ref.yaml18
-rw-r--r--gfx/wr/wrench/reftests/compositor-surface/basic.yaml22
-rw-r--r--gfx/wr/wrench/reftests/compositor-surface/blank.yaml2
-rw-r--r--gfx/wr/wrench/reftests/compositor-surface/coord-systems.yaml17
-rw-r--r--gfx/wr/wrench/reftests/compositor-surface/filter-overlay-ref.yaml11
-rw-r--r--gfx/wr/wrench/reftests/compositor-surface/filter-overlay.yaml15
-rw-r--r--gfx/wr/wrench/reftests/compositor-surface/mix-blend-ref.yaml17
-rw-r--r--gfx/wr/wrench/reftests/compositor-surface/mix-blend.yaml20
-rw-r--r--gfx/wr/wrench/reftests/compositor-surface/picture-passthrough.yaml15
-rw-r--r--gfx/wr/wrench/reftests/compositor-surface/reftest.list6
-rw-r--r--gfx/wr/wrench/reftests/compositor-surface/too-many-surfaces-ref.yaml31
-rw-r--r--gfx/wr/wrench/reftests/compositor-surface/too-many-surfaces.yaml39
-rw-r--r--gfx/wr/wrench/reftests/crash/blank.yaml2
-rw-r--r--gfx/wr/wrench/reftests/crash/iframe-dup.yaml21
-rw-r--r--gfx/wr/wrench/reftests/crash/many-segments.yaml112
-rw-r--r--gfx/wr/wrench/reftests/crash/reftest.list2
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-across-tiles.pngbin0 -> 9964 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-across-tiles.yaml12
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-basic-ref.yaml11
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-basic.yaml21
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-blur-across-tiles.pngbin0 -> 111826 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-blur-across-tiles.yaml19
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-blur-edge-mode-ref.yaml10
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-blur-edge-mode.yaml17
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-chain.pngbin0 -> 53393 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-chain.yaml12
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-clip-mask-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-clip-mask.yaml28
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-culled-filter-ref.yaml5
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-culled-filter.yaml25
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-drop-shadow.pngbin0 -> 2866 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-drop-shadow.yaml13
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-half-opacity-ref.yaml5
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-half-opacity.yaml13
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-isolation-ref.yaml20
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-isolation.yaml20
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-nested.yaml31
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-on-child-surface-ref.yaml7
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-on-child-surface.yaml14
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-overlap.pngbin0 -> 1250 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-overlap.yaml15
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-perspective.pngbin0 -> 60136 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-perspective.yaml32
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-transformed-filter.pngbin0 -> 7827 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/backdrop-filter-transformed-filter.yaml17
-rw-r--r--gfx/wr/wrench/reftests/filters/blank.yaml2
-rw-r--r--gfx/wr/wrench/reftests/filters/blend-clipped-raster-root.pngbin0 -> 17505 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/blend-clipped-raster-root.yaml26
-rw-r--r--gfx/wr/wrench/reftests/filters/blend-clipped.pngbin0 -> 2623 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/blend-clipped.yaml103
-rw-r--r--gfx/wr/wrench/reftests/filters/drop-shadow-inverse-scale.yaml14
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-blur-clamping-ref.yaml18
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-blur-clamping.yaml30
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-blur-downscale-fractional.pngbin0 -> 43832 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-blur-downscale-fractional.yaml11
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-blur-huge.yaml10
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-blur-scaled-ref.yaml11
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-blur-scaled-xonly.pngbin0 -> 3364 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-blur-scaled-xonly.yaml16
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-blur-scaled.yaml15
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-blur.pngbin0 -> 55138 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-blur.yaml9
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-brightness-2-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-brightness-2.yaml13
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-brightness-3-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-brightness-3.yaml13
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-brightness-4-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-brightness-4.yaml13
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-brightness-ref.yaml13
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-brightness.yaml17
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-color-matrix-ref.yaml21
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-color-matrix.yaml53
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-component-transfer-ref.yaml51
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-component-transfer.yaml352
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-contrast-gray-alpha-1-ref.yaml13
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-contrast-gray-alpha-1.yaml13
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-drop-shadow-blur-clamping-ref.yaml18
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-drop-shadow-blur-clamping.yaml30
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-drop-shadow-clip-2.pngbin0 -> 3131 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-drop-shadow-clip-2.yaml21
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-drop-shadow-clip-3.pngbin0 -> 46591 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-drop-shadow-clip-3.yaml37
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-drop-shadow-clip.pngbin0 -> 12165 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-drop-shadow-clip.yaml27
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-drop-shadow-fractional.pngbin0 -> 1649 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-drop-shadow-fractional.yaml11
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-drop-shadow-huge.yaml10
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-drop-shadow-on-viewport-edge.pngbin0 -> 11337 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-drop-shadow-on-viewport-edge.yaml10
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-drop-shadow-scaled-ref.yaml11
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-drop-shadow-scaled.yaml15
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-drop-shadow-transform-huge.yaml17
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-drop-shadow.pngbin0 -> 78605 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-drop-shadow.yaml9
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-grayscale-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-grayscale.yaml10
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-hue-rotate-1-ref.yaml21
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-hue-rotate-1.yaml37
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-hue-rotate-alpha-1-ref.yaml21
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-hue-rotate-alpha-1.yaml37
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-invert-2-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-invert-2.yaml17
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-invert-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-invert.yaml13
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-large-blur-radius.pngbin0 -> 108646 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-large-blur-radius.yaml10
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-long-chain.pngbin0 -> 23150 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-long-chain.yaml19
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-mix-blend-mode-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-mix-blend-mode.yaml17
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-mix-blend-scaling-ref.yaml11
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-mix-blend-scaling.yaml23
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-saturate-blue-1-ref.yaml13
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-saturate-blue-1.yaml17
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-saturate-blue-2-ref.yaml13
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-saturate-blue-2.yaml17
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-saturate-blue-3-ref.yaml13
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-saturate-blue-3.yaml17
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-saturate-blue-alpha-1-ref.yaml13
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-saturate-blue-alpha-1.yaml17
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-saturate-green-1-ref.yaml13
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-saturate-green-1.yaml17
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-saturate-green-2-ref.yaml13
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-saturate-green-2.yaml17
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-saturate-green-3-ref.yaml13
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-saturate-green-3.yaml17
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-saturate-green-alpha-1-ref.yaml12
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-saturate-green-alpha-1.yaml17
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-saturate-red-1-ref.yaml13
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-saturate-red-1.yaml17
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-saturate-red-2-ref.yaml13
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-saturate-red-2.yaml17
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-saturate-red-3-ref.yaml13
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-saturate-red-3.yaml17
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-saturate-red-alpha-1-ref.yaml12
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-saturate-red-alpha-1.yaml17
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-segments-ref.yaml18
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-segments.yaml24
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-small-blur-radius.pngbin0 -> 17400 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/filter-small-blur-radius.yaml10
-rw-r--r--gfx/wr/wrench/reftests/filters/firefox.pngbin0 -> 45376 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/iframe-dropshadow-ref.yaml22
-rw-r--r--gfx/wr/wrench/reftests/filters/iframe-dropshadow.yaml34
-rw-r--r--gfx/wr/wrench/reftests/filters/invisible-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/filters/invisible.yaml27
-rw-r--r--gfx/wr/wrench/reftests/filters/isolated-ref.yaml13
-rw-r--r--gfx/wr/wrench/reftests/filters/isolated.yaml17
-rw-r--r--gfx/wr/wrench/reftests/filters/opacity-combined-ref.yaml10
-rw-r--r--gfx/wr/wrench/reftests/filters/opacity-combined.yaml10
-rw-r--r--gfx/wr/wrench/reftests/filters/opacity-overlap-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/filters/opacity-overlap.yaml16
-rw-r--r--gfx/wr/wrench/reftests/filters/opacity-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/filters/opacity.yaml10
-rw-r--r--gfx/wr/wrench/reftests/filters/reftest.list85
-rw-r--r--gfx/wr/wrench/reftests/filters/srgb-to-linear-2.yaml16
-rw-r--r--gfx/wr/wrench/reftests/filters/srgb-to-linear-ref.yaml15
-rw-r--r--gfx/wr/wrench/reftests/filters/srgb-to-linear.yaml16
-rw-r--r--gfx/wr/wrench/reftests/filters/svg-filter-blend-ref.yaml21
-rw-r--r--gfx/wr/wrench/reftests/filters/svg-filter-blend.yaml83
-rw-r--r--gfx/wr/wrench/reftests/filters/svg-filter-blur-transforms.pngbin0 -> 5728 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/svg-filter-blur-transforms.yaml16
-rw-r--r--gfx/wr/wrench/reftests/filters/svg-filter-blur.yaml14
-rw-r--r--gfx/wr/wrench/reftests/filters/svg-filter-color-matrix.yaml69
-rw-r--r--gfx/wr/wrench/reftests/filters/svg-filter-component-transfer.yaml382
-rw-r--r--gfx/wr/wrench/reftests/filters/svg-filter-composite-ref.yaml73
-rw-r--r--gfx/wr/wrench/reftests/filters/svg-filter-composite.yaml124
-rw-r--r--gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow-on-viewport-edge.pngbin0 -> 11337 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow-on-viewport-edge.yaml16
-rw-r--r--gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow-perspective.pngbin0 -> 12691 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow-perspective.yaml22
-rw-r--r--gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow-rotate-ref.yaml11
-rw-r--r--gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow-rotate.yaml17
-rw-r--r--gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow.pngbin0 -> 79321 bytes
-rw-r--r--gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow.yaml16
-rw-r--r--gfx/wr/wrench/reftests/filters/svg-filter-flood-ref.yaml10
-rw-r--r--gfx/wr/wrench/reftests/filters/svg-filter-flood.yaml10
-rw-r--r--gfx/wr/wrench/reftests/filters/svg-filter-offset-ref.yaml11
-rw-r--r--gfx/wr/wrench/reftests/filters/svg-filter-offset.yaml15
-rw-r--r--gfx/wr/wrench/reftests/filters/svg-srgb-to-linear.yaml20
-rw-r--r--gfx/wr/wrench/reftests/gradient/conic-angle-wraparound-negative.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/conic-angle-wraparound.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/conic-angle.pngbin0 -> 24700 bytes
-rw-r--r--gfx/wr/wrench/reftests/gradient/conic-angle.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/conic-backdrop-ref.yaml11
-rw-r--r--gfx/wr/wrench/reftests/gradient/conic-backdrop-with-spacing-ref.yaml13
-rw-r--r--gfx/wr/wrench/reftests/gradient/conic-backdrop-with-spacing.yaml18
-rw-r--r--gfx/wr/wrench/reftests/gradient/conic-backdrop.yaml16
-rw-r--r--gfx/wr/wrench/reftests/gradient/conic-center.pngbin0 -> 1695 bytes
-rw-r--r--gfx/wr/wrench/reftests/gradient/conic-center.yaml11
-rw-r--r--gfx/wr/wrench/reftests/gradient/conic-color-wheel.pngbin0 -> 35963 bytes
-rw-r--r--gfx/wr/wrench/reftests/gradient/conic-color-wheel.yaml14
-rw-r--r--gfx/wr/wrench/reftests/gradient/conic-large-hard-stop-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/conic-large-hard-stop.yaml14
-rw-r--r--gfx/wr/wrench/reftests/gradient/conic-large-ref.yaml16
-rw-r--r--gfx/wr/wrench/reftests/gradient/conic-large.yaml12
-rw-r--r--gfx/wr/wrench/reftests/gradient/conic-nan.yaml41
-rw-r--r--gfx/wr/wrench/reftests/gradient/conic-ref.yaml18
-rw-r--r--gfx/wr/wrench/reftests/gradient/conic-simple.pngbin0 -> 24158 bytes
-rw-r--r--gfx/wr/wrench/reftests/gradient/conic-simple.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/conic.yaml11
-rw-r--r--gfx/wr/wrench/reftests/gradient/gradient_cache_5stops.yaml13
-rw-r--r--gfx/wr/wrench/reftests/gradient/gradient_cache_5stops_ref.yaml18
-rw-r--r--gfx/wr/wrench/reftests/gradient/gradient_cache_5stops_vertical.yaml13
-rw-r--r--gfx/wr/wrench/reftests/gradient/gradient_cache_5stops_vertical_ref.yaml18
-rw-r--r--gfx/wr/wrench/reftests/gradient/gradient_cache_clamp.yaml20
-rw-r--r--gfx/wr/wrench/reftests/gradient/gradient_cache_clamp_ref.yaml30
-rw-r--r--gfx/wr/wrench/reftests/gradient/gradient_cache_hardstop.yaml19
-rw-r--r--gfx/wr/wrench/reftests/gradient/gradient_cache_hardstop_clip.yaml27
-rw-r--r--gfx/wr/wrench/reftests/gradient/gradient_cache_hardstop_clip_ref.yaml41
-rw-r--r--gfx/wr/wrench/reftests/gradient/gradient_cache_hardstop_ref.yaml24
-rw-r--r--gfx/wr/wrench/reftests/gradient/gradient_cache_repeat.yaml119
-rw-r--r--gfx/wr/wrench/reftests/gradient/gradient_cache_repeat_ref.yaml119
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-adjust-tile-size-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-adjust-tile-size.yaml10
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-aligned-border-radius.pngbin0 -> 6851 bytes
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-aligned-border-radius.yaml46
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-aligned-clip-ref.yaml16
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-aligned-clip.yaml9
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-backdrop-ref.yaml11
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-backdrop-with-spacing-ref.yaml14
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-backdrop-with-spacing.yaml18
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-backdrop.yaml16
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-bug-1703141.yaml12
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-clamp-1-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-clamp-1a.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-clamp-1b.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-clamp-2-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-clamp-2.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-double.yaml13
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-far-endpoints.yaml10
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-hard-stop-ref.pngbin0 -> 2031 bytes
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-hard-stop-repeat-large-ref.yaml11
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-hard-stop-repeat-large.yaml14
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-hard-stop.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-large-ref.yaml12
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-large.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-nan.yaml213
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-ref.pngbin0 -> 3227 bytes
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-ref.yaml18
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-repeat-clip-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-repeat-clip.yaml13
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-reverse-2-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-reverse-2.yaml9
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-reverse-3-ref.yaml13
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-reverse-3.yaml13
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-reverse.yaml11
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-stops-ref.pngbin0 -> 7360 bytes
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear-stops.yaml7
-rw-r--r--gfx/wr/wrench/reftests/gradient/linear.yaml11
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-conic-1-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-conic-1.yaml9
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-conic-2-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-conic-2.yaml9
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-conic-3-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-conic-3.yaml9
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-conic-4-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-conic-4.yaml9
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-conic-degenerate-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-conic-degenerate.yaml14
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-linear-1-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-linear-1.yaml9
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-linear-2-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-linear-2.yaml9
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-linear-3-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-linear-3.yaml9
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-linear-4-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-linear-4.yaml9
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-linear-degenerate-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-linear-degenerate.yaml14
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-radial-1-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-radial-1.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-radial-2-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-radial-2.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-radial-3-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-radial-3.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-radial-degenerate-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/norm-radial-degenerate.yaml14
-rw-r--r--gfx/wr/wrench/reftests/gradient/premultiplied-aligned-2.pngbin0 -> 10340 bytes
-rw-r--r--gfx/wr/wrench/reftests/gradient/premultiplied-aligned-2.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/premultiplied-aligned.pngbin0 -> 11635 bytes
-rw-r--r--gfx/wr/wrench/reftests/gradient/premultiplied-aligned.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/premultiplied-angle-2.pngbin0 -> 19390 bytes
-rw-r--r--gfx/wr/wrench/reftests/gradient/premultiplied-angle-2.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/premultiplied-angle.pngbin0 -> 18487 bytes
-rw-r--r--gfx/wr/wrench/reftests/gradient/premultiplied-angle.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/premultiplied-conic-2.pngbin0 -> 14475 bytes
-rw-r--r--gfx/wr/wrench/reftests/gradient/premultiplied-conic-2.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/premultiplied-conic.pngbin0 -> 16782 bytes
-rw-r--r--gfx/wr/wrench/reftests/gradient/premultiplied-conic.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/premultiplied-radial-2.pngbin0 -> 14593 bytes
-rw-r--r--gfx/wr/wrench/reftests/gradient/premultiplied-radial-2.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/premultiplied-radial.pngbin0 -> 18857 bytes
-rw-r--r--gfx/wr/wrench/reftests/gradient/premultiplied-radial.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/radial-backdrop-ref.yaml11
-rw-r--r--gfx/wr/wrench/reftests/gradient/radial-backdrop-with-spacing-ref.yaml13
-rw-r--r--gfx/wr/wrench/reftests/gradient/radial-backdrop-with-spacing.yaml18
-rw-r--r--gfx/wr/wrench/reftests/gradient/radial-backdrop.yaml16
-rw-r--r--gfx/wr/wrench/reftests/gradient/radial-circle-ref.pngbin0 -> 27523 bytes
-rw-r--r--gfx/wr/wrench/reftests/gradient/radial-circle.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/radial-ellipse-ref.pngbin0 -> 22812 bytes
-rw-r--r--gfx/wr/wrench/reftests/gradient/radial-ellipse.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/radial-large-ref.pngbin0 -> 36904 bytes
-rw-r--r--gfx/wr/wrench/reftests/gradient/radial-large.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/radial-nan.yaml35
-rw-r--r--gfx/wr/wrench/reftests/gradient/radial-optimized-2-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/radial-optimized-2.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/radial-optimized-ref.yaml33
-rw-r--r--gfx/wr/wrench/reftests/gradient/radial-optimized.yaml12
-rw-r--r--gfx/wr/wrench/reftests/gradient/radial-tiling-optimized-ref.yaml24
-rw-r--r--gfx/wr/wrench/reftests/gradient/radial-tiling-optimized.yaml13
-rw-r--r--gfx/wr/wrench/reftests/gradient/radial-zero-size-1.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/radial-zero-size-2.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/radial-zero-size-3.yaml6
-rw-r--r--gfx/wr/wrench/reftests/gradient/radial-zero-size-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/gradient/reftest.list128
-rw-r--r--gfx/wr/wrench/reftests/gradient/repeat-border-radius.pngbin0 -> 31999 bytes
-rw-r--r--gfx/wr/wrench/reftests/gradient/repeat-border-radius.yaml136
-rw-r--r--gfx/wr/wrench/reftests/gradient/repeat-conic-negative.yaml9
-rw-r--r--gfx/wr/wrench/reftests/gradient/repeat-conic-ref.yaml27
-rw-r--r--gfx/wr/wrench/reftests/gradient/repeat-conic.yaml9
-rw-r--r--gfx/wr/wrench/reftests/gradient/repeat-linear-ref.yaml27
-rw-r--r--gfx/wr/wrench/reftests/gradient/repeat-linear-reverse.yaml9
-rw-r--r--gfx/wr/wrench/reftests/gradient/repeat-linear.yaml9
-rw-r--r--gfx/wr/wrench/reftests/gradient/repeat-radial-negative.yaml9
-rw-r--r--gfx/wr/wrench/reftests/gradient/repeat-radial-ref.yaml38
-rw-r--r--gfx/wr/wrench/reftests/gradient/repeat-radial.yaml9
-rw-r--r--gfx/wr/wrench/reftests/gradient/tiling-conic-1-ref.yaml27
-rw-r--r--gfx/wr/wrench/reftests/gradient/tiling-conic-1.yaml11
-rw-r--r--gfx/wr/wrench/reftests/gradient/tiling-conic-2-ref.yaml27
-rw-r--r--gfx/wr/wrench/reftests/gradient/tiling-conic-2.yaml11
-rw-r--r--gfx/wr/wrench/reftests/gradient/tiling-conic-3-ref.yaml27
-rw-r--r--gfx/wr/wrench/reftests/gradient/tiling-conic-3.yaml11
-rw-r--r--gfx/wr/wrench/reftests/gradient/tiling-linear-1-ref.yaml27
-rw-r--r--gfx/wr/wrench/reftests/gradient/tiling-linear-1.yaml11
-rw-r--r--gfx/wr/wrench/reftests/gradient/tiling-linear-2-ref.yaml27
-rw-r--r--gfx/wr/wrench/reftests/gradient/tiling-linear-2.yaml11
-rw-r--r--gfx/wr/wrench/reftests/gradient/tiling-linear-3-ref.yaml15
-rw-r--r--gfx/wr/wrench/reftests/gradient/tiling-linear-3.yaml11
-rw-r--r--gfx/wr/wrench/reftests/gradient/tiling-radial-1-ref.yaml27
-rw-r--r--gfx/wr/wrench/reftests/gradient/tiling-radial-1.yaml11
-rw-r--r--gfx/wr/wrench/reftests/gradient/tiling-radial-2-ref.yaml27
-rw-r--r--gfx/wr/wrench/reftests/gradient/tiling-radial-2.yaml11
-rw-r--r--gfx/wr/wrench/reftests/gradient/tiling-radial-3-ref.yaml27
-rw-r--r--gfx/wr/wrench/reftests/gradient/tiling-radial-3.yaml11
-rw-r--r--gfx/wr/wrench/reftests/gradient/tiling-radial-4-ref.yaml27
-rw-r--r--gfx/wr/wrench/reftests/gradient/tiling-radial-4.yaml11
-rw-r--r--gfx/wr/wrench/reftests/image/colorrect.pngbin0 -> 256 bytes
-rw-r--r--gfx/wr/wrench/reftests/image/downscale.pngbin0 -> 10613 bytes
-rw-r--r--gfx/wr/wrench/reftests/image/downscale.yaml18
-rw-r--r--gfx/wr/wrench/reftests/image/firefox.pngbin0 -> 25927 bytes
-rw-r--r--gfx/wr/wrench/reftests/image/green-alpha-ref.yaml4
-rw-r--r--gfx/wr/wrench/reftests/image/image-alpha-stretch-tile.yaml8
-rw-r--r--gfx/wr/wrench/reftests/image/image-alpha-stretch.yaml6
-rw-r--r--gfx/wr/wrench/reftests/image/image-filter-stretch-tile.yaml10
-rw-r--r--gfx/wr/wrench/reftests/image/image-filter-stretch.yaml8
-rw-r--r--gfx/wr/wrench/reftests/image/occlusion.pngbin0 -> 57660 bytes
-rw-r--r--gfx/wr/wrench/reftests/image/occlusion.yaml15
-rw-r--r--gfx/wr/wrench/reftests/image/reftest.list19
-rw-r--r--gfx/wr/wrench/reftests/image/rgb_composite.yaml10
-rw-r--r--gfx/wr/wrench/reftests/image/rgb_composite_ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/image/segments.pngbin0 -> 7200 bytes
-rw-r--r--gfx/wr/wrench/reftests/image/segments.yaml12
-rw-r--r--gfx/wr/wrench/reftests/image/spacex-u.pngbin0 -> 81302 bytes
-rw-r--r--gfx/wr/wrench/reftests/image/spacex-uv.pngbin0 -> 202309 bytes
-rw-r--r--gfx/wr/wrench/reftests/image/spacex-v.pngbin0 -> 97851 bytes
-rw-r--r--gfx/wr/wrench/reftests/image/spacex-y.pngbin0 -> 128761 bytes
-rw-r--r--gfx/wr/wrench/reftests/image/spacex-yuv.pngbin0 -> 310131 bytes
-rw-r--r--gfx/wr/wrench/reftests/image/texture-rect-ref.yaml15
-rw-r--r--gfx/wr/wrench/reftests/image/texture-rect.yaml7
-rw-r--r--gfx/wr/wrench/reftests/image/tile-repeat-prim-or-decompose-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/image/tile-repeat-prim-or-decompose.yaml17
-rw-r--r--gfx/wr/wrench/reftests/image/tile-size-ref.yaml14
-rw-r--r--gfx/wr/wrench/reftests/image/tile-size.yaml19
-rw-r--r--gfx/wr/wrench/reftests/image/tile-with-spacing-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/image/tile-with-spacing.yaml12
-rw-r--r--gfx/wr/wrench/reftests/image/tiled-clip-chain-ref.yaml10
-rw-r--r--gfx/wr/wrench/reftests/image/tiled-clip-chain.yaml11
-rw-r--r--gfx/wr/wrench/reftests/image/tiled-complex-clip-ref.yaml10
-rw-r--r--gfx/wr/wrench/reftests/image/tiled-complex-clip.yaml13
-rw-r--r--gfx/wr/wrench/reftests/image/very-big-ref.yaml5
-rw-r--r--gfx/wr/wrench/reftests/image/very-big-tile-size-ref.yaml5
-rw-r--r--gfx/wr/wrench/reftests/image/very-big-tile-size.yaml9
-rw-r--r--gfx/wr/wrench/reftests/image/very-big.yaml5
-rw-r--r--gfx/wr/wrench/reftests/image/yuv-clip.yaml26
-rw-r--r--gfx/wr/wrench/reftests/image/yuv.pngbin0 -> 620802 bytes
-rw-r--r--gfx/wr/wrench/reftests/image/yuv.yaml19
-rw-r--r--gfx/wr/wrench/reftests/mask/aligned-layer-rect-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/mask/aligned-layer-rect.yaml17
-rw-r--r--gfx/wr/wrench/reftests/mask/checkerboard-tiling.yaml18
-rw-r--r--gfx/wr/wrench/reftests/mask/checkerboard.pngbin0 -> 4958 bytes
-rw-r--r--gfx/wr/wrench/reftests/mask/checkerboard.yaml15
-rw-r--r--gfx/wr/wrench/reftests/mask/circle.pngbin0 -> 4928 bytes
-rw-r--r--gfx/wr/wrench/reftests/mask/green.yaml6
-rw-r--r--gfx/wr/wrench/reftests/mask/mask-atomicity-ref.yaml17
-rw-r--r--gfx/wr/wrench/reftests/mask/mask-atomicity-tiling.yaml28
-rw-r--r--gfx/wr/wrench/reftests/mask/mask-atomicity.yaml27
-rw-r--r--gfx/wr/wrench/reftests/mask/mask-perspective-tiling.yaml25
-rw-r--r--gfx/wr/wrench/reftests/mask/mask-perspective.pngbin0 -> 279 bytes
-rw-r--r--gfx/wr/wrench/reftests/mask/mask-perspective.yaml24
-rw-r--r--gfx/wr/wrench/reftests/mask/mask-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/mask/mask-tiling.yaml16
-rw-r--r--gfx/wr/wrench/reftests/mask/mask-transformed-to-empty-rect-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/mask/mask-transformed-to-empty-rect.yaml25
-rw-r--r--gfx/wr/wrench/reftests/mask/mask.pngbin0 -> 187 bytes
-rw-r--r--gfx/wr/wrench/reftests/mask/mask.yaml15
-rw-r--r--gfx/wr/wrench/reftests/mask/missing-mask-ref.yaml7
-rw-r--r--gfx/wr/wrench/reftests/mask/missing-mask.yaml14
-rw-r--r--gfx/wr/wrench/reftests/mask/nested-mask-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/mask/nested-mask-tiling.yaml23
-rw-r--r--gfx/wr/wrench/reftests/mask/nested-mask.yaml21
-rw-r--r--gfx/wr/wrench/reftests/mask/out-of-bounds.yaml16
-rw-r--r--gfx/wr/wrench/reftests/mask/reftest.list17
-rw-r--r--gfx/wr/wrench/reftests/mask/rounded-corners.pngbin0 -> 1697 bytes
-rw-r--r--gfx/wr/wrench/reftests/mask/rounded-corners.yaml24
-rw-r--r--gfx/wr/wrench/reftests/mask/scaled-filter-raster-root.pngbin0 -> 5314 bytes
-rw-r--r--gfx/wr/wrench/reftests/mask/scaled-filter-raster-root.yaml30
-rw-r--r--gfx/wr/wrench/reftests/mask/tiny-check-mask.pngbin0 -> 138 bytes
-rw-r--r--gfx/wr/wrench/reftests/performance/compositor-surface-opaque-slice-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/performance/compositor-surface-opaque-slice.yaml10
-rw-r--r--gfx/wr/wrench/reftests/performance/no-clip-mask.pngbin0 -> 3201 bytes
-rw-r--r--gfx/wr/wrench/reftests/performance/no-clip-mask.yaml24
-rw-r--r--gfx/wr/wrench/reftests/performance/reftest.list2
-rw-r--r--gfx/wr/wrench/reftests/reftest.list19
-rw-r--r--gfx/wr/wrench/reftests/scrolling/ancestor-scroll-frames.yaml37
-rw-r--r--gfx/wr/wrench/reftests/scrolling/blank.yaml2
-rw-r--r--gfx/wr/wrench/reftests/scrolling/clip-and-scroll-property-ref.yaml5
-rw-r--r--gfx/wr/wrench/reftests/scrolling/clip-and-scroll-property.yaml33
-rw-r--r--gfx/wr/wrench/reftests/scrolling/empty-mask-ref.yaml5
-rw-r--r--gfx/wr/wrench/reftests/scrolling/empty-mask.yaml20
-rw-r--r--gfx/wr/wrench/reftests/scrolling/ext-scroll-offset-1-ref.yaml10
-rw-r--r--gfx/wr/wrench/reftests/scrolling/ext-scroll-offset-1.yaml14
-rw-r--r--gfx/wr/wrench/reftests/scrolling/fixed-position-ref.yaml14
-rw-r--r--gfx/wr/wrench/reftests/scrolling/fixed-position-scrolling-clip-ref.yaml5
-rw-r--r--gfx/wr/wrench/reftests/scrolling/fixed-position-scrolling-clip.yaml33
-rw-r--r--gfx/wr/wrench/reftests/scrolling/fixed-position.yaml52
-rw-r--r--gfx/wr/wrench/reftests/scrolling/mask.pngbin0 -> 282 bytes
-rw-r--r--gfx/wr/wrench/reftests/scrolling/nested-scroll-offset-ref.yaml5
-rw-r--r--gfx/wr/wrench/reftests/scrolling/nested-scroll-offset.yaml16
-rw-r--r--gfx/wr/wrench/reftests/scrolling/nested-stickys-ref.yaml11
-rw-r--r--gfx/wr/wrench/reftests/scrolling/nested-stickys.yaml20
-rw-r--r--gfx/wr/wrench/reftests/scrolling/out-of-bounds-scroll-ref.yaml5
-rw-r--r--gfx/wr/wrench/reftests/scrolling/out-of-bounds-scroll.yaml10
-rw-r--r--gfx/wr/wrench/reftests/scrolling/reftest.list25
-rw-r--r--gfx/wr/wrench/reftests/scrolling/root-scroll-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/scrolling/root-scroll.yaml7
-rw-r--r--gfx/wr/wrench/reftests/scrolling/scale-offsets-ref.yaml5
-rw-r--r--gfx/wr/wrench/reftests/scrolling/scale-offsets.yaml18
-rw-r--r--gfx/wr/wrench/reftests/scrolling/scroll-frame-order-ref.yaml34
-rw-r--r--gfx/wr/wrench/reftests/scrolling/scroll-frame-order.yaml35
-rw-r--r--gfx/wr/wrench/reftests/scrolling/scroll-generation-1.yaml18
-rw-r--r--gfx/wr/wrench/reftests/scrolling/scroll-generation-2.yaml19
-rw-r--r--gfx/wr/wrench/reftests/scrolling/scroll-generation-3.yaml20
-rw-r--r--gfx/wr/wrench/reftests/scrolling/scroll-generation-4.yaml13
-rw-r--r--gfx/wr/wrench/reftests/scrolling/scroll-generation-ref.yaml10
-rw-r--r--gfx/wr/wrench/reftests/scrolling/scroll-layer-ref.yaml5
-rw-r--r--gfx/wr/wrench/reftests/scrolling/scroll-layer.yaml10
-rw-r--r--gfx/wr/wrench/reftests/scrolling/sibling-hidden-clip-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/scrolling/sibling-hidden-clip.yaml23
-rw-r--r--gfx/wr/wrench/reftests/scrolling/simple-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/scrolling/simple.yaml22
-rw-r--r--gfx/wr/wrench/reftests/scrolling/sticky-applied-ref.yaml70
-rw-r--r--gfx/wr/wrench/reftests/scrolling/sticky-applied.yaml367
-rw-r--r--gfx/wr/wrench/reftests/scrolling/sticky-nested.yaml257
-rw-r--r--gfx/wr/wrench/reftests/scrolling/sticky-ref.yaml40
-rw-r--r--gfx/wr/wrench/reftests/scrolling/sticky-transformed-ref.yaml5
-rw-r--r--gfx/wr/wrench/reftests/scrolling/sticky-transformed.yaml21
-rw-r--r--gfx/wr/wrench/reftests/scrolling/sticky.yaml203
-rw-r--r--gfx/wr/wrench/reftests/scrolling/translate-nested-ref.yaml14
-rw-r--r--gfx/wr/wrench/reftests/scrolling/translate-nested.yaml29
-rw-r--r--gfx/wr/wrench/reftests/scrolling/viewport-offset-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/scrolling/viewport-offset.yaml23
-rw-r--r--gfx/wr/wrench/reftests/snap/1761299-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/snap/1761299.yaml12
-rw-r--r--gfx/wr/wrench/reftests/snap/fractional-filter-ref.yaml13
-rw-r--r--gfx/wr/wrench/reftests/snap/fractional-filter.yaml14
-rw-r--r--gfx/wr/wrench/reftests/snap/preserve-3d.pngbin0 -> 6837 bytes
-rw-r--r--gfx/wr/wrench/reftests/snap/preserve-3d.yaml55
-rw-r--r--gfx/wr/wrench/reftests/snap/reftest.list6
-rw-r--r--gfx/wr/wrench/reftests/snap/snap.pngbin0 -> 164 bytes
-rw-r--r--gfx/wr/wrench/reftests/snap/snap.yaml15
-rw-r--r--gfx/wr/wrench/reftests/snap/subpixel-raster-root-ref.yaml7
-rw-r--r--gfx/wr/wrench/reftests/snap/subpixel-raster-root.yaml13
-rw-r--r--gfx/wr/wrench/reftests/snap/transform.pngbin0 -> 412 bytes
-rw-r--r--gfx/wr/wrench/reftests/snap/transform.yaml19
-rw-r--r--gfx/wr/wrench/reftests/split/cross-ref.yaml36
-rw-r--r--gfx/wr/wrench/reftests/split/cross.yaml24
-rw-r--r--gfx/wr/wrench/reftests/split/filter-ref.yaml10
-rw-r--r--gfx/wr/wrench/reftests/split/filter.yaml21
-rw-r--r--gfx/wr/wrench/reftests/split/gradient-ref.yaml39
-rw-r--r--gfx/wr/wrench/reftests/split/gradient.yaml42
-rw-r--r--gfx/wr/wrench/reftests/split/intermediate-1-ref.yaml12
-rw-r--r--gfx/wr/wrench/reftests/split/intermediate-1.yaml38
-rw-r--r--gfx/wr/wrench/reftests/split/intermediate-2.yaml34
-rw-r--r--gfx/wr/wrench/reftests/split/mixed-order-ref.yaml22
-rw-r--r--gfx/wr/wrench/reftests/split/mixed-order.yaml25
-rw-r--r--gfx/wr/wrench/reftests/split/near-plane.pngbin0 -> 3210 bytes
-rw-r--r--gfx/wr/wrench/reftests/split/near-plane.yaml16
-rw-r--r--gfx/wr/wrench/reftests/split/nested-coord-systems-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/split/nested-coord-systems.yaml28
-rw-r--r--gfx/wr/wrench/reftests/split/nested-preserve3d-crash.yaml37
-rw-r--r--gfx/wr/wrench/reftests/split/nested-ref.yaml12
-rw-r--r--gfx/wr/wrench/reftests/split/nested.yaml25
-rw-r--r--gfx/wr/wrench/reftests/split/order-1-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/split/order-1.yaml20
-rw-r--r--gfx/wr/wrench/reftests/split/order-2-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/split/order-2.yaml27
-rw-r--r--gfx/wr/wrench/reftests/split/order-3-ref.yaml15
-rw-r--r--gfx/wr/wrench/reftests/split/order-3.yaml31
-rw-r--r--gfx/wr/wrench/reftests/split/ordering-ref.yaml15
-rw-r--r--gfx/wr/wrench/reftests/split/ordering.yaml25
-rw-r--r--gfx/wr/wrench/reftests/split/perspective-clipping-ref.yaml9
-rw-r--r--gfx/wr/wrench/reftests/split/perspective-clipping.yaml23
-rw-r--r--gfx/wr/wrench/reftests/split/reftest.list22
-rw-r--r--gfx/wr/wrench/reftests/split/same-plane.pngbin0 -> 4279 bytes
-rw-r--r--gfx/wr/wrench/reftests/split/same-plane.yaml33
-rw-r--r--gfx/wr/wrench/reftests/split/simple-ref.yaml12
-rw-r--r--gfx/wr/wrench/reftests/split/simple.yaml24
-rw-r--r--gfx/wr/wrench/reftests/split/split-intersect1-ref.yaml15
-rw-r--r--gfx/wr/wrench/reftests/split/split-intersect1.yaml18
-rw-r--r--gfx/wr/wrench/reftests/text/1658-ref.yaml26
-rw-r--r--gfx/wr/wrench/reftests/text/1658.yaml19
-rw-r--r--gfx/wr/wrench/reftests/text/Ahem.ttfbin0 -> 21768 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/FreeSans.ttfbin0 -> 714456 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/Proggy-License.txt10
-rw-r--r--gfx/wr/wrench/reftests/text/Proggy.ttfbin0 -> 5284 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/VeraBd.ttfbin0 -> 58716 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/ahem-ref.yaml18
-rw-r--r--gfx/wr/wrench/reftests/text/ahem.yaml219
-rw-r--r--gfx/wr/wrench/reftests/text/allow-subpixel-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/text/allow-subpixel.yaml19
-rw-r--r--gfx/wr/wrench/reftests/text/alpha-transform.pngbin0 -> 23933 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/alpha-transform.yaml10
-rw-r--r--gfx/wr/wrench/reftests/text/bg-color-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/text/bg-color.yaml15
-rw-r--r--gfx/wr/wrench/reftests/text/blank.yaml2
-rw-r--r--gfx/wr/wrench/reftests/text/blurred-shadow-local-clip-rect-ref.pngbin0 -> 19462 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/blurred-shadow-local-clip-rect.yaml48
-rw-r--r--gfx/wr/wrench/reftests/text/border-radius-alpha.pngbin0 -> 14077 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/border-radius-subpx.pngbin0 -> 14413 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/border-radius.yaml18
-rw-r--r--gfx/wr/wrench/reftests/text/clipped-transform.pngbin0 -> 1790 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/clipped-transform.yaml12
-rw-r--r--gfx/wr/wrench/reftests/text/color-bitmap-shadow-ref.yaml30
-rw-r--r--gfx/wr/wrench/reftests/text/color-bitmap-shadow.yaml34
-rw-r--r--gfx/wr/wrench/reftests/text/colors-alpha.pngbin0 -> 101403 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/colors-subpx.pngbin0 -> 133545 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/colors.yaml211
-rw-r--r--gfx/wr/wrench/reftests/text/decorations-ref.yaml19
-rw-r--r--gfx/wr/wrench/reftests/text/decorations-suite.pngbin0 -> 23790 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/decorations-suite.yaml348
-rw-r--r--gfx/wr/wrench/reftests/text/decorations.yaml36
-rw-r--r--gfx/wr/wrench/reftests/text/diacritics-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/text/diacritics.yaml6
-rw-r--r--gfx/wr/wrench/reftests/text/embedded-bitmaps.pngbin0 -> 2288 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/embedded-bitmaps.yaml9
-rw-r--r--gfx/wr/wrench/reftests/text/intermediate-transform.yaml54
-rw-r--r--gfx/wr/wrench/reftests/text/isolated-text.pngbin0 -> 7159 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/isolated-text.yaml14
-rw-r--r--gfx/wr/wrench/reftests/text/large-glyphs.yaml8
-rw-r--r--gfx/wr/wrench/reftests/text/large-line-decoration.yaml43
-rw-r--r--gfx/wr/wrench/reftests/text/long-text.yaml610
-rw-r--r--gfx/wr/wrench/reftests/text/mix-blend-layers-ref.yaml50
-rw-r--r--gfx/wr/wrench/reftests/text/mix-blend-layers.yaml53
-rw-r--r--gfx/wr/wrench/reftests/text/negative-pos.yaml10
-rw-r--r--gfx/wr/wrench/reftests/text/non-opaque-notref.yaml10
-rw-r--r--gfx/wr/wrench/reftests/text/non-opaque.yaml10
-rw-r--r--gfx/wr/wrench/reftests/text/perspective-clip.pngbin0 -> 1755 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/perspective-clip.yaml28
-rw-r--r--gfx/wr/wrench/reftests/text/raster-space-snap-ref.yaml10
-rw-r--r--gfx/wr/wrench/reftests/text/raster-space-snap.yaml10
-rw-r--r--gfx/wr/wrench/reftests/text/raster-space.pngbin0 -> 62107 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/raster-space.yaml29
-rw-r--r--gfx/wr/wrench/reftests/text/raster_root_C_8192.yaml391
-rw-r--r--gfx/wr/wrench/reftests/text/raster_root_C_ref.yaml391
-rw-r--r--gfx/wr/wrench/reftests/text/reftest.list85
-rw-r--r--gfx/wr/wrench/reftests/text/rotate-snap-clip-ref.yaml26
-rw-r--r--gfx/wr/wrench/reftests/text/rotate-snap-clip.yaml29
-rw-r--r--gfx/wr/wrench/reftests/text/rotate-snap-filter-ref.yaml14
-rw-r--r--gfx/wr/wrench/reftests/text/rotate-snap-filter.yaml16
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-atomic-ref.yaml81
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-atomic.yaml42
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-border.yaml19
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-clip-rect.yaml45
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-clip-ref.yaml11
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-clip.yaml26
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-clipped-text.yaml24
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-complex.yaml46
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-cover-1.yaml18
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-cover-2.yaml26
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-fast-clip-ref.yaml23
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-fast-clip.yaml24
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-grey-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-grey-transparent.yaml19
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-grey.yaml19
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-huge-ref.yaml18
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-huge.yaml18
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-image.yaml14
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-many.yaml30
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-ordering-ref.yaml131
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-ordering.yaml48
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-partial-glyph-ref.yaml50
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-partial-glyph.yaml43
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-red-ref.yaml5
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-red.yaml19
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-ref.yaml25
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-rotate.yaml18
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-single.yaml18
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-solid-ref.yaml16
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-transforms.pngbin0 -> 94776 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/shadow-transforms.yaml58
-rw-r--r--gfx/wr/wrench/reftests/text/shadow.yaml33
-rw-r--r--gfx/wr/wrench/reftests/text/snap-clip-ref.yaml7
-rw-r--r--gfx/wr/wrench/reftests/text/snap-clip.yaml8
-rw-r--r--gfx/wr/wrench/reftests/text/snap-text-offset-ref.yaml11
-rw-r--r--gfx/wr/wrench/reftests/text/snap-text-offset.yaml10
-rw-r--r--gfx/wr/wrench/reftests/text/split-batch-ref.yaml18
-rw-r--r--gfx/wr/wrench/reftests/text/split-batch.yaml18
-rw-r--r--gfx/wr/wrench/reftests/text/subpixel-rotate.pngbin0 -> 19021 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/subpixel-rotate.yaml10
-rw-r--r--gfx/wr/wrench/reftests/text/subpixel-scale.pngbin0 -> 14069 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/subpixel-scale.yaml10
-rw-r--r--gfx/wr/wrench/reftests/text/subpixel-skew.pngbin0 -> 15332 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/subpixel-skew.yaml10
-rw-r--r--gfx/wr/wrench/reftests/text/subpixel-translate-ref.yaml12
-rw-r--r--gfx/wr/wrench/reftests/text/subpixel-translate.yaml13
-rw-r--r--gfx/wr/wrench/reftests/text/subpx-bg-mask-ref.yaml8
-rw-r--r--gfx/wr/wrench/reftests/text/subpx-bg-mask.yaml28
-rw-r--r--gfx/wr/wrench/reftests/text/subtle-shadow-ref.yaml27
-rw-r--r--gfx/wr/wrench/reftests/text/subtle-shadow.yaml21
-rw-r--r--gfx/wr/wrench/reftests/text/synthetic-bold-not-ref.yaml5
-rw-r--r--gfx/wr/wrench/reftests/text/synthetic-bold-transparent-ref.yaml10
-rw-r--r--gfx/wr/wrench/reftests/text/synthetic-bold-transparent.yaml7
-rw-r--r--gfx/wr/wrench/reftests/text/synthetic-bold.yaml6
-rw-r--r--gfx/wr/wrench/reftests/text/synthetic-italics-custom.yaml7
-rw-r--r--gfx/wr/wrench/reftests/text/synthetic-italics-ref.yaml5
-rw-r--r--gfx/wr/wrench/reftests/text/synthetic-italics.yaml7
-rw-r--r--gfx/wr/wrench/reftests/text/text-fixed-slice-fast.pngbin0 -> 5353 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/text-fixed-slice-slow.pngbin0 -> 7081 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/text-fixed-slice.yaml27
-rw-r--r--gfx/wr/wrench/reftests/text/text-masking-alpha.pngbin0 -> 19118 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/text-masking-mask.pngbin0 -> 129 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/text-masking-subpx.pngbin0 -> 29047 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/text-masking.yaml26
-rw-r--r--gfx/wr/wrench/reftests/text/text.yaml10
-rw-r--r--gfx/wr/wrench/reftests/text/transparent-no-aa-ref.yaml17
-rw-r--r--gfx/wr/wrench/reftests/text/transparent-no-aa.yaml9
-rw-r--r--gfx/wr/wrench/reftests/text/two-shadows.pngbin0 -> 11313 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/two-shadows.yaml24
-rw-r--r--gfx/wr/wrench/reftests/text/white-opacity.pngbin0 -> 8266 bytes
-rw-r--r--gfx/wr/wrench/reftests/text/white-opacity.yaml17
-rw-r--r--gfx/wr/wrench/reftests/text/writing-modes-ref.yaml19
-rw-r--r--gfx/wr/wrench/reftests/text/writing-modes.yaml15
-rw-r--r--gfx/wr/wrench/reftests/tiles/complex-shared-clip-root-ref.yaml13
-rw-r--r--gfx/wr/wrench/reftests/tiles/complex-shared-clip-root.yaml25
-rw-r--r--gfx/wr/wrench/reftests/tiles/mix-blend-clip-ref.yaml14
-rw-r--r--gfx/wr/wrench/reftests/tiles/mix-blend-clip.yaml31
-rw-r--r--gfx/wr/wrench/reftests/tiles/prim-suite.yaml45
-rw-r--r--gfx/wr/wrench/reftests/tiles/rect.yaml6
-rw-r--r--gfx/wr/wrench/reftests/tiles/reftest.list7
-rw-r--r--gfx/wr/wrench/reftests/tiles/simple-gradient.yaml9
-rw-r--r--gfx/wr/wrench/reftests/tiles/tile-cache-raster-root.pngbin0 -> 11246 bytes
-rw-r--r--gfx/wr/wrench/reftests/tiles/tile-cache-raster-root.yaml43
-rw-r--r--gfx/wr/wrench/reftests/transforms/big-axis-aligned-scale-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/transforms/big-axis-aligned-scale.yaml10
-rw-r--r--gfx/wr/wrench/reftests/transforms/blank.yaml2
-rw-r--r--gfx/wr/wrench/reftests/transforms/border-scale-2.pngbin0 -> 4234 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/border-scale-2.yaml19
-rw-r--r--gfx/wr/wrench/reftests/transforms/border-scale-3.pngbin0 -> 4058 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/border-scale-3.yaml19
-rw-r--r--gfx/wr/wrench/reftests/transforms/border-scale-4.pngbin0 -> 4064 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/border-scale-4.yaml19
-rw-r--r--gfx/wr/wrench/reftests/transforms/border-scale.pngbin0 -> 4879 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/border-scale.yaml19
-rw-r--r--gfx/wr/wrench/reftests/transforms/border-zoom.pngbin0 -> 26360 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/border-zoom.yaml22
-rw-r--r--gfx/wr/wrench/reftests/transforms/clip-translate-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/transforms/clip-translate.yaml23
-rw-r--r--gfx/wr/wrench/reftests/transforms/complex-preserve-3d.yaml24
-rw-r--r--gfx/wr/wrench/reftests/transforms/computed-rotation-yflip.pngbin0 -> 36363 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/computed-rotation-yflip.yaml17
-rw-r--r--gfx/wr/wrench/reftests/transforms/content-offset.pngbin0 -> 2121 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/content-offset.yaml18
-rw-r--r--gfx/wr/wrench/reftests/transforms/coord-system.pngbin0 -> 3400 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/coord-system.yaml24
-rw-r--r--gfx/wr/wrench/reftests/transforms/firefox.pngbin0 -> 45376 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/flatten-all-flat-ref.yaml18
-rw-r--r--gfx/wr/wrench/reftests/transforms/flatten-all-flat.yaml20
-rw-r--r--gfx/wr/wrench/reftests/transforms/flatten-preserve-3d-root-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/transforms/flatten-preserve-3d-root.yaml23
-rw-r--r--gfx/wr/wrench/reftests/transforms/flatten-twice-ref.yaml6
-rw-r--r--gfx/wr/wrench/reftests/transforms/flatten-twice.yaml21
-rw-r--r--gfx/wr/wrench/reftests/transforms/image-rotated-clip.pngbin0 -> 3816 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/image-rotated-clip.yaml19
-rw-r--r--gfx/wr/wrench/reftests/transforms/image.pngbin0 -> 528 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/large-raster-root.yaml14
-rw-r--r--gfx/wr/wrench/reftests/transforms/local-clip.pngbin0 -> 2167 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/local-clip.yaml26
-rw-r--r--gfx/wr/wrench/reftests/transforms/near-plane-clip.pngbin0 -> 70226 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/near-plane-clip.yaml18
-rw-r--r--gfx/wr/wrench/reftests/transforms/nested-local-scale-ref.yaml23
-rw-r--r--gfx/wr/wrench/reftests/transforms/nested-local-scale.yaml24
-rw-r--r--gfx/wr/wrench/reftests/transforms/nested-preserve-3d.pngbin0 -> 3793 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/nested-preserve-3d.yaml28
-rw-r--r--gfx/wr/wrench/reftests/transforms/nested-rotate-x-flat.pngbin0 -> 4539 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/nested-rotate-x-flat.yaml27
-rw-r--r--gfx/wr/wrench/reftests/transforms/nested-rotate-x.pngbin0 -> 3789 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/nested-rotate-x.yaml27
-rw-r--r--gfx/wr/wrench/reftests/transforms/non-inversible-world-rect.yaml23
-rw-r--r--gfx/wr/wrench/reftests/transforms/perspective-border-radius.pngbin0 -> 11471 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/perspective-border-radius.yaml26
-rw-r--r--gfx/wr/wrench/reftests/transforms/perspective-box-shadow-ref.yaml24
-rw-r--r--gfx/wr/wrench/reftests/transforms/perspective-box-shadow.yaml23
-rw-r--r--gfx/wr/wrench/reftests/transforms/perspective-clip-1.pngbin0 -> 2004 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/perspective-clip-1.yaml30
-rw-r--r--gfx/wr/wrench/reftests/transforms/perspective-clip.pngbin0 -> 17216 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/perspective-clip.yaml27
-rw-r--r--gfx/wr/wrench/reftests/transforms/perspective-mask.pngbin0 -> 2157 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/perspective-mask.yaml24
-rw-r--r--gfx/wr/wrench/reftests/transforms/perspective-origin.pngbin0 -> 46673 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/perspective-origin.yaml16
-rw-r--r--gfx/wr/wrench/reftests/transforms/perspective-shadow.pngbin0 -> 9496 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/perspective-shadow.yaml27
-rw-r--r--gfx/wr/wrench/reftests/transforms/perspective-surface-scale.pngbin0 -> 40725 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/perspective-surface-scale.yaml14
-rw-r--r--gfx/wr/wrench/reftests/transforms/perspective.pngbin0 -> 127232 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/perspective.yaml54
-rw-r--r--gfx/wr/wrench/reftests/transforms/prim-suite.pngbin0 -> 51496 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/prim-suite.yaml45
-rw-r--r--gfx/wr/wrench/reftests/transforms/raster-root-huge-scale.yaml35
-rw-r--r--gfx/wr/wrench/reftests/transforms/raster-root-large-mask.yaml28
-rw-r--r--gfx/wr/wrench/reftests/transforms/raster-root-scaling-2-ref.yaml11
-rw-r--r--gfx/wr/wrench/reftests/transforms/raster-root-scaling-2.yaml20
-rw-r--r--gfx/wr/wrench/reftests/transforms/raster-root-scaling-ref.yaml10
-rw-r--r--gfx/wr/wrench/reftests/transforms/raster-root-scaling.yaml14
-rw-r--r--gfx/wr/wrench/reftests/transforms/raster_root_A_8192.yaml26
-rw-r--r--gfx/wr/wrench/reftests/transforms/raster_root_A_ref.yaml26
-rw-r--r--gfx/wr/wrench/reftests/transforms/raster_root_B_8192.yaml14
-rw-r--r--gfx/wr/wrench/reftests/transforms/raster_root_B_ref.yaml14
-rw-r--r--gfx/wr/wrench/reftests/transforms/reftest.list59
-rw-r--r--gfx/wr/wrench/reftests/transforms/rotate-clip-ref.yaml7
-rw-r--r--gfx/wr/wrench/reftests/transforms/rotate-clip.yaml19
-rw-r--r--gfx/wr/wrench/reftests/transforms/rotated-clip-large.pngbin0 -> 7183 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/rotated-clip-large.yaml17
-rw-r--r--gfx/wr/wrench/reftests/transforms/rotated-clip.pngbin0 -> 3767 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/rotated-clip.yaml17
-rw-r--r--gfx/wr/wrench/reftests/transforms/rotated-image.pngbin0 -> 7396 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/rotated-image.yaml72
-rw-r--r--gfx/wr/wrench/reftests/transforms/screen-space-blit-trivial.pngbin0 -> 74583 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/screen-space-blit-trivial.yaml22
-rw-r--r--gfx/wr/wrench/reftests/transforms/screen-space-blit.pngbin0 -> 73794 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/screen-space-blit.yaml22
-rw-r--r--gfx/wr/wrench/reftests/transforms/screen-space-blur.pngbin0 -> 230748 bytes
-rw-r--r--gfx/wr/wrench/reftests/transforms/screen-space-blur.yaml20
-rw-r--r--gfx/wr/wrench/reftests/transforms/segments-bug-ref.yaml22
-rw-r--r--gfx/wr/wrench/reftests/transforms/segments-bug.yaml26
-rw-r--r--gfx/wr/wrench/reftests/transforms/singular-ref.yaml28
-rw-r--r--gfx/wr/wrench/reftests/transforms/singular.yaml39
-rw-r--r--gfx/wr/wrench/reftests/transforms/snapped-preserve-3d-ref.yaml19
-rw-r--r--gfx/wr/wrench/reftests/transforms/snapped-preserve-3d.yaml21
-rw-r--r--gfx/wr/wrench/reftests/transforms/strange-w-ref.yaml12
-rw-r--r--gfx/wr/wrench/reftests/transforms/strange-w.yaml15
-rw-r--r--gfx/wr/wrench/res/wrench.exe.manifest24
-rw-r--r--gfx/wr/wrench/script/benchmark_server.py59
-rw-r--r--gfx/wr/wrench/script/gen-many-images.py15
-rwxr-xr-xgfx/wr/wrench/script/headless.py152
-rw-r--r--gfx/wr/wrench/script/reftest-analyzer.xhtml857
-rwxr-xr-xgfx/wr/wrench/script/reftest-debugger.py15
-rwxr-xr-xgfx/wr/wrench/script/wrench_with_renderer.py52
-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
1322 files changed, 191921 insertions, 0 deletions
diff --git a/gfx/wr/.gitignore b/gfx/wr/.gitignore
new file mode 100644
index 0000000000..849001b76c
--- /dev/null
+++ b/gfx/wr/.gitignore
@@ -0,0 +1,27 @@
+target/
+*~
+*#
+
+# WR internals
+captures
+wrench/json_frames
+wrench/ron_frames
+
+# Editors
+*.swp
+*.swo
+
+# IntelliJ
+.idea
+*.iws
+*.iml
+
+# Gradle
+.gradle
+
+# VSCode
+.vscode
+.vs
+
+# System
+.fuse_hidden*
diff --git a/gfx/wr/.taskcluster.yml b/gfx/wr/.taskcluster.yml
new file mode 100644
index 0000000000..0c0b268dd7
--- /dev/null
+++ b/gfx/wr/.taskcluster.yml
@@ -0,0 +1,180 @@
+# This file specifies the configuration needed to test WebRender using the
+# Taskcluster infrastructure. Most of this should be relatively
+# self-explanatory; this file was originally generated by using the
+# Taskcluster-GitHub integration quickstart tool which no longer exists.
+#
+# See https://community-tc.services.mozilla.com/docs
+version: 1
+policy:
+ pullRequests: public
+
+# This file triggers a set of tasks; the ones targeting Linux are run in a docker
+# container using docker-worker (which is a worker type provided by TaskCluster).
+# The OS X ones are run in a custom worker type, for which we have worker
+# instances configured and running.
+tasks:
+ $if: 'tasks_for in ["github-push", "github-pull-request"]'
+ then:
+ $let:
+ should_run:
+ $if: 'tasks_for == "github-push"'
+ # for pushes, run on any branch but master
+ then: {$eval: 'event.ref != "refs/heads/master"'}
+ # for PRs, run for opened and synchronized events
+ else: {$eval: 'event.action in ["opened", "synchronize"]'}
+ repo_url:
+ $if: 'tasks_for == "github-push"'
+ then: ${event.repository.clone_url}
+ else: ${event.pull_request.head.repo.clone_url}
+ sha:
+ $if: 'tasks_for == "github-push"'
+ then: ${event.after}
+ else: ${event.pull_request.head.sha}
+ login: ${event.sender.login}
+ branch:
+ $if: 'tasks_for == "github-push"'
+ then:
+ $if: 'event.ref[:11] == "refs/heads/"'
+ then: ${event.ref[11:]}
+ else: ${event.ref}
+ else: ${event.pull_request.head.ref}
+ in:
+ $if: should_run
+ then:
+ # For the docker-worker tasks, the Docker image used
+ # (staktrace/webrender-test:debian-v10) was created using the Dockerfile in
+ # ci-scripts/docker-image.
+ #
+ # The docker image may need to be updated over time if the set of required
+ # packages increases. Note in particular that rust/cargo are not part of the
+ # docker image, and are re-installed using rustup on each CI run. This ensures
+ # the latest stable rust compiler is always used.
+ # CI runs will be triggered on opening PRs, updating PRs, and pushes to the
+ # repository.
+ - metadata:
+ name: Linux release tests
+ description: Runs release-mode WebRender CI stuff on a Linux TC worker
+ owner: noreply@mozilla.com
+ source: ${repo_url}
+ provisionerId: proj-webrender
+ workerType: ci-linux
+ deadline: {$fromNow: '1 day'}
+ payload:
+ maxRunTime: 7200
+ image: 'staktrace/webrender-test:debian-v10'
+ env:
+ RUST_BACKTRACE: 'full'
+ RUSTFLAGS: '--deny warnings'
+ command:
+ - /bin/bash
+ - '--login'
+ - '-c'
+ - >-
+ curl https://sh.rustup.rs -sSf | sh -s -- -y &&
+ source $HOME/.cargo/env &&
+ git clone ${repo_url} webrender && cd webrender &&
+ git checkout ${sha} &&
+ servo-tidy &&
+ ci-scripts/install-meson.sh &&
+ export PATH=$PATH:$PWD/meson &&
+ ci-scripts/linux-release-tests.sh
+ routes:
+ - "index.project.webrender.ci.${login}.${branch}.linux-release"
+ - metadata:
+ name: Linux debug tests
+ description: Runs debug-mode WebRender CI stuff on a Linux TC worker
+ owner: noreply@mozilla.com
+ source: ${repo_url}
+ provisionerId: proj-webrender
+ workerType: ci-linux
+ deadline: {$fromNow: '1 day'}
+ payload:
+ maxRunTime: 7200
+ image: 'staktrace/webrender-test:debian-v10'
+ env:
+ RUST_BACKTRACE: 'full'
+ RUSTFLAGS: '--deny warnings'
+ command:
+ - /bin/bash
+ - '--login'
+ - '-c'
+ - >-
+ curl https://sh.rustup.rs -sSf | sh -s -- -y &&
+ source $HOME/.cargo/env &&
+ git clone ${repo_url} webrender && cd webrender &&
+ git checkout ${sha} &&
+ servo-tidy &&
+ ci-scripts/install-meson.sh &&
+ export PATH=$PATH:$PWD/meson &&
+ ci-scripts/linux-debug-tests.sh
+ routes:
+ - "index.project.webrender.ci.${login}.${branch}.linux-debug"
+ # For the OS X jobs we use a pool of machines that we are managing, because
+ # Mozilla releng doesn't have any spare OS X machines for us at this time.
+ # Talk to :kats or :jrmuizel if you need more details about this. The machines
+ # are hooked up to taskcluster using taskcluster-worker; they use a workerpool
+ # of proj-webrender/ci-macos. They are set up with all the dependencies needed
+ # to build and test webrender, including Rust (currently 1.41.1), servo-tidy,
+ # mako, zlib, etc. Note that unlike the docker-worker used for Linux, these
+ # machines WILL persist state from one run to the next, so any cleanup needs
+ # to be handled explicitly.
+
+ # macOS tasks currently disabled, see bug 1639217.
+ # - metadata:
+ # name: OS X release tests
+ # description: Runs release-mode WebRender CI stuff on a OS X TC worker
+ # owner: noreply@mozilla.com
+ # source: ${repo_url}
+ # provisionerId: 'proj-webrender'
+ # workerType: 'ci-macos'
+ # deadline: {$fromNow: '1 day'}
+ # payload:
+ # maxRunTime: 3600
+ # command:
+ # - - /bin/bash
+ # - '--login'
+ # - '-vec'
+ # - |
+ # git clone ${repo_url} webrender
+ # cd webrender
+ # git checkout ${sha}
+ # source $HOME/servotidy-venv/bin/activate
+ # servo-tidy
+ # export RUST_BACKTRACE=full
+ # export RUSTFLAGS='--deny warnings'
+ # export PKG_CONFIG_PATH="/usr/local/opt/zlib/lib/pkgconfig:$PKG_CONFIG_PATH"
+ # echo 'exec make -j1 "$@"' > $HOME/make # See #2638
+ # chmod +x $HOME/make
+ # export MAKE="$HOME/make"
+ # ci-scripts/macos-release-tests.sh
+ # routes:
+ # - "index.project.webrender.ci.${login}.${branch}.osx-release"
+ # - metadata:
+ # name: OS X debug tests
+ # description: Runs debug-mode WebRender CI stuff on a OS X TC worker
+ # owner: noreply@mozilla.com
+ # source: ${repo_url}
+ # provisionerId: 'proj-webrender'
+ # workerType: 'ci-macos'
+ # deadline: {$fromNow: '1 day'}
+ # payload:
+ # maxRunTime: 3600
+ # command:
+ # - - /bin/bash
+ # - '--login'
+ # - '-vec'
+ # - |
+ # git clone ${repo_url} webrender
+ # cd webrender
+ # git checkout ${sha}
+ # source $HOME/servotidy-venv/bin/activate
+ # servo-tidy
+ # export RUST_BACKTRACE=full
+ # export RUSTFLAGS='--deny warnings'
+ # export PKG_CONFIG_PATH="/usr/local/opt/zlib/lib/pkgconfig:$PKG_CONFIG_PATH"
+ # echo 'exec make -j1 "$@"' > $HOME/make # See #2638
+ # chmod +x $HOME/make
+ # export MAKE="$HOME/make"
+ # ci-scripts/macos-debug-tests.sh
+ # routes:
+ # - "index.project.webrender.ci.${login}.${branch}.osx-debug"
diff --git a/gfx/wr/Cargo.lock b/gfx/wr/Cargo.lock
new file mode 100644
index 0000000000..3b3a0acb08
--- /dev/null
+++ b/gfx/wr/Cargo.lock
@@ -0,0 +1,3334 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "adler32"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2"
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android_log-sys"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85965b6739a430150bdd138e2374a98af0c3ee0d030b3bb7fc3bddff58d0102e"
+
+[[package]]
+name = "android_logger"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037f3e1da32ddba7770530e69258b742c15ad67bdf90e5f6b35f4b6db9a60eb7"
+dependencies = [
+ "android_log-sys",
+ "env_logger",
+ "log",
+ "once_cell",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
+
+[[package]]
+name = "app_units"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc3ec9d4c47b25a5a9e5c848e053640331c7cedb1637434d75db68b79fee8a7f"
+dependencies = [
+ "num-traits",
+ "serde",
+]
+
+[[package]]
+name = "arrayref"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
+
+[[package]]
+name = "ascii-canvas"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6"
+dependencies = [
+ "term",
+]
+
+[[package]]
+name = "askama"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb98f10f371286b177db5eeb9a6e5396609555686a35e1d4f7b9a9c6d8af0139"
+dependencies = [
+ "askama_derive",
+ "askama_escape",
+ "askama_shared",
+]
+
+[[package]]
+name = "askama_derive"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87bf87e6e8b47264efa9bde63d6225c6276a52e05e91bf37eaa8afd0032d6b71"
+dependencies = [
+ "askama_shared",
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "askama_escape"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
+
+[[package]]
+name = "askama_shared"
+version = "0.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf722b94118a07fcbc6640190f247334027685d4e218b794dbfe17c32bf38ed0"
+dependencies = [
+ "askama_escape",
+ "mime",
+ "mime_guess",
+ "nom 7.1.1",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn",
+ "toml",
+]
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
+name = "bimap"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7"
+
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bit-set"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitvec"
+version = "0.19.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55f93d0ef3363c364d5976646a38f04cf67cfe1d4c8d160cdea02cab2c116b33"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
+[[package]]
+name = "block"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
+
+[[package]]
+name = "build-parallel"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e3ff9db740167616e528c509b3618046fc05d337f8f3182d300f4aa977d2bb"
+dependencies = [
+ "crossbeam-utils",
+ "jobserver",
+ "num_cpus",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
+
+[[package]]
+name = "bytemuck"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37fa13df2292ecb479ec23aa06f4507928bef07839be9ef15281411076629431"
+
+[[package]]
+name = "byteorder"
+version = "1.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
+
+[[package]]
+name = "bytes"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
+
+[[package]]
+name = "calloop"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf2eec61efe56aa1e813f5126959296933cf0700030e4314786c48779a66ab82"
+dependencies = [
+ "log",
+ "nix",
+]
+
+[[package]]
+name = "camino"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "869119e97797867fd90f5e22af7d0bd274bd4635ebb9eb68c04f3f513ae6c412"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cargo-platform"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cargo_metadata"
+version = "0.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "982a0cf6a99c350d7246035613882e376d58cebe571785abc5da4f648d53ac0a"
+dependencies = [
+ "camino",
+ "cargo-platform",
+ "semver",
+ "serde",
+ "serde_json",
+ "thiserror",
+]
+
+[[package]]
+name = "cbitset"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29b6ad25ae296159fb0da12b970b2fe179b234584d7cd294c891e2bbb284466b"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+dependencies = [
+ "jobserver",
+]
+
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "cgl"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "chrono"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
+dependencies = [
+ "libc",
+ "num-integer",
+ "num-traits",
+ "serde",
+ "time",
+ "winapi",
+]
+
+[[package]]
+name = "clap"
+version = "3.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b"
+dependencies = [
+ "atty",
+ "bitflags",
+ "clap_lex",
+ "indexmap",
+ "strsim",
+ "termcolor",
+ "textwrap",
+ "yaml-rust",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213"
+dependencies = [
+ "os_str_bytes",
+]
+
+[[package]]
+name = "cmake"
+version = "0.1.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81fb25b677f8bf1eb325017cb6bb8452f87969db0fedb4f757b297bee78a7c62"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "cocoa"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f63902e9223530efb4e26ccd0cf55ec30d592d3b42e21a28defc42a9586e832"
+dependencies = [
+ "bitflags",
+ "block",
+ "cocoa-foundation",
+ "core-foundation 0.9.2",
+ "core-graphics 0.22.3",
+ "foreign-types",
+ "libc",
+ "objc",
+]
+
+[[package]]
+name = "cocoa-foundation"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ade49b65d560ca58c403a479bb396592b155c0185eada742ee323d1d68d6318"
+dependencies = [
+ "bitflags",
+ "block",
+ "core-foundation 0.9.2",
+ "core-graphics-types",
+ "foreign-types",
+ "libc",
+ "objc",
+]
+
+[[package]]
+name = "compositor"
+version = "0.1.0"
+dependencies = [
+ "compositor-wayland",
+ "compositor-windows",
+ "gleam",
+ "webrender",
+]
+
+[[package]]
+name = "compositor-wayland"
+version = "0.1.0"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
+[[package]]
+name = "compositor-windows"
+version = "0.1.0"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "const_fn"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd51eab21ab4fd6a3bf889e2d0958c0a6e3a61ad04260325e919e652a2a62826"
+
+[[package]]
+name = "core-foundation"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171"
+dependencies = [
+ "core-foundation-sys 0.7.0",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3"
+dependencies = [
+ "core-foundation-sys 0.8.3",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac"
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[package]]
+name = "core-graphics"
+version = "0.19.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923"
+dependencies = [
+ "bitflags",
+ "core-foundation 0.7.0",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics"
+version = "0.22.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb"
+dependencies = [
+ "bitflags",
+ "core-foundation 0.9.2",
+ "core-graphics-types",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics-types"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e92f5d519093a4178296707dbaa3880eae85a5ef5386675f361a1cf25376e93c"
+dependencies = [
+ "bitflags",
+ "core-foundation 0.9.2",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-text"
+version = "19.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d74ada66e07c1cefa18f8abfba765b486f250de2e4a999e5727fc0dd4b4a25"
+dependencies = [
+ "core-foundation 0.9.2",
+ "core-graphics 0.22.3",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-video-sys"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34ecad23610ad9757664d644e369246edde1803fcb43ed72876565098a5d3828"
+dependencies = [
+ "cfg-if 0.1.10",
+ "core-foundation-sys 0.7.0",
+ "core-graphics 0.19.2",
+ "libc",
+ "objc",
+]
+
+[[package]]
+name = "countme"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
+
+[[package]]
+name = "crc32fast"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1"
+dependencies = [
+ "cfg-if 0.1.10",
+]
+
+[[package]]
+name = "crossbeam"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd66663db5a988098a89599d4857919b3acf7f61402e61365acfd3919857b9be"
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775"
+dependencies = [
+ "cfg-if 1.0.0",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9"
+dependencies = [
+ "cfg-if 1.0.0",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1aaa739f95311c2c7887a76863f500026092fb1dce0161dab577e559ef3569d"
+dependencies = [
+ "cfg-if 1.0.0",
+ "const_fn",
+ "crossbeam-utils",
+ "lazy_static",
+ "memoffset",
+ "scopeguard",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d"
+dependencies = [
+ "autocfg",
+ "cfg-if 1.0.0",
+ "lazy_static",
+]
+
+[[package]]
+name = "crunchy"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
+
+[[package]]
+name = "cty"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35"
+
+[[package]]
+name = "darling"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "dashmap"
+version = "4.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c"
+dependencies = [
+ "cfg-if 1.0.0",
+ "num_cpus",
+]
+
+[[package]]
+name = "deflate"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7e5d2a2273fed52a7f947ee55b092c4057025d7a3e04e5ecdbd25d6c3fb1bd7"
+dependencies = [
+ "adler32",
+ "byteorder",
+]
+
+[[package]]
+name = "derive_more"
+version = "0.99.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2323f3f47db9a0e77ce7a300605d8d2098597fc451ed1a97bb1f6411bb550a7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "diff"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
+
+[[package]]
+name = "dirs-next"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
+dependencies = [
+ "cfg-if 1.0.0",
+ "dirs-sys-next",
+]
+
+[[package]]
+name = "dirs-sys-next"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "dispatch"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
+
+[[package]]
+name = "dlib"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794"
+dependencies = [
+ "libloading",
+]
+
+[[package]]
+name = "downcast-rs"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ba6eb47c2131e784a38b726eb54c1e1484904f013e576a25354d0124161af6"
+
+[[package]]
+name = "dwrote"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439a1c2ba5611ad3ed731280541d36d2e9c4ac5e7fb818a27b604bdc5a6aa65b"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "serde",
+ "serde_derive",
+ "winapi",
+ "wio",
+]
+
+[[package]]
+name = "either"
+version = "1.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
+
+[[package]]
+name = "ena"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c533630cf40e9caa44bd91aadc88a75d75a4c3a12b4cfde353cbed41daa1e1f1"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "etagere"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eb66dc3d6bb6b2ab4a12454db6988079311d6443e627bc7e6065f907f556272"
+dependencies = [
+ "euclid",
+ "serde",
+ "svg_fmt",
+]
+
+[[package]]
+name = "euclid"
+version = "0.22.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da96828553a086d7b18dcebfc579bd9628b016f86590d7453c115e490fa74b80"
+dependencies = [
+ "num-traits",
+ "serde",
+]
+
+[[package]]
+name = "expat-sys"
+version = "2.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "658f19728920138342f68408b7cf7644d90d4784353d8ebc32e7e8663dbe45fa"
+dependencies = [
+ "cmake",
+ "pkg-config",
+]
+
+[[package]]
+name = "firefox-on-glean"
+version = "0.1.0"
+
+[[package]]
+name = "fixedbitset"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
+
+[[package]]
+name = "flate2"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39522e96686d38f4bc984b9198e3a0613264abaebaff2c5c918bfa6b6da09af"
+dependencies = [
+ "cfg-if 1.0.0",
+ "crc32fast",
+ "libc",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "font-loader"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c49d6b4c11dca1a1dd931a34a9f397e2da91abe3de4110505f3530a80e560b52"
+dependencies = [
+ "core-foundation 0.9.2",
+ "core-text",
+ "libc",
+ "servo-fontconfig",
+ "winapi",
+]
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
+dependencies = [
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "freetype"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee38378a9e3db1cc693b4f88d166ae375338a0ff75cb8263e1c601d51f35dc6"
+dependencies = [
+ "freetype-sys",
+ "libc",
+]
+
+[[package]]
+name = "freetype-sys"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a37d4011c0cc628dfa766fcc195454f4b068d7afdc2adfd28861191d866e731a"
+dependencies = [
+ "cmake",
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "fs-err"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5bd79fa345a495d3ae89fb7165fec01c0e72f41821d642dda363a1e97975652e"
+
+[[package]]
+name = "fuchsia-cprng"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
+
+[[package]]
+name = "funty"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
+
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "wasi 0.10.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "gl_generator"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca98bbde17256e02d17336a6bdb5a50f7d0ccacee502e191d3e3d0ec2f96f84a"
+dependencies = [
+ "khronos_api",
+ "log",
+ "xml-rs",
+]
+
+[[package]]
+name = "gl_generator"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d"
+dependencies = [
+ "khronos_api",
+ "log",
+ "xml-rs",
+]
+
+[[package]]
+name = "gleam"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0173481f2bb6e809bf4985de2e86c83876d84d2805830e3301cd37355e897f0f"
+dependencies = [
+ "gl_generator 0.14.0",
+]
+
+[[package]]
+name = "glean"
+version = "52.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb5fc2dc8615ab49bfa879d64a02565b459881b72023ff39aca75e5581825695"
+dependencies = [
+ "chrono",
+ "crossbeam-channel",
+ "glean-core",
+ "inherent",
+ "log",
+ "once_cell",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "time",
+ "uuid",
+ "whatsys",
+]
+
+[[package]]
+name = "glean-core"
+version = "52.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3493e4f4df45199762f43a6d2298fa9d885fa5ddc9efdc118a41b38c69c7ad59"
+dependencies = [
+ "android_logger",
+ "bincode",
+ "chrono",
+ "crossbeam-channel",
+ "flate2",
+ "log",
+ "once_cell",
+ "oslog",
+ "rkv",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "time",
+ "uniffi",
+ "uuid",
+ "zeitstempel",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+
+[[package]]
+name = "glsl"
+version = "6.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd49bbe5e12dd5aed9d66a84899af422f3d4fcfdd20b2294c8b4ade11500b05"
+dependencies = [
+ "nom 6.2.1",
+]
+
+[[package]]
+name = "glsl-lang"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63b28f6f12b2053aca7502f82b34b1de2bbad4bf89238ca1609e5a8424edda3e"
+dependencies = [
+ "glsl-lang-lexer",
+ "glsl-lang-types",
+ "lalrpop",
+ "lalrpop-util",
+ "lang-util",
+ "lazy_static",
+ "thiserror",
+]
+
+[[package]]
+name = "glsl-lang-lexer"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75208cb3c9a4b8651281e533cede1f49c4938adc8c31d2228f8be9b961d26ccf"
+dependencies = [
+ "glsl-lang-pp",
+ "glsl-lang-types",
+ "lalrpop-util",
+ "lang-util",
+ "thiserror",
+]
+
+[[package]]
+name = "glsl-lang-pp"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2c847306dfc1aded12394263868b69eb2c0f60916b3714f9691270025111ec2"
+dependencies = [
+ "arrayvec",
+ "bimap",
+ "cbitset",
+ "derive_more",
+ "itertools",
+ "lang-util",
+ "lazy_static",
+ "lexical",
+ "rowan",
+ "static_assertions",
+ "string_cache",
+ "string_cache_codegen",
+ "thiserror",
+]
+
+[[package]]
+name = "glsl-lang-types"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2db6674176dc42051c1b9d2f1b994d91cdd96176b2be067d9f245873db20d8b7"
+dependencies = [
+ "lang-util",
+ "thiserror",
+]
+
+[[package]]
+name = "glsl-to-cxx"
+version = "0.1.0"
+dependencies = [
+ "glsl",
+]
+
+[[package]]
+name = "glslopt"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74a3f5c04450dfdadb4b08f6e5ee6f5110f674de1acbd6199bfec68392a8cbaf"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "glutin"
+version = "0.28.0"
+source = "git+https://github.com/jamienicol/glutin?branch=wr#03285da9c14ec56296c2400c781d2c32b80d745a"
+dependencies = [
+ "cgl",
+ "cocoa",
+ "core-foundation 0.9.2",
+ "glutin_egl_sys",
+ "glutin_emscripten_sys",
+ "glutin_gles2_sys",
+ "glutin_glx_sys",
+ "glutin_wgl_sys",
+ "lazy_static",
+ "libloading",
+ "log",
+ "ndk-glue",
+ "objc",
+ "osmesa-sys",
+ "parking_lot 0.11.2",
+ "wayland-client",
+ "wayland-egl",
+ "winapi",
+ "winit",
+]
+
+[[package]]
+name = "glutin_egl_sys"
+version = "0.1.5"
+source = "git+https://github.com/jamienicol/glutin?branch=wr#03285da9c14ec56296c2400c781d2c32b80d745a"
+dependencies = [
+ "gl_generator 0.14.0",
+ "winapi",
+]
+
+[[package]]
+name = "glutin_emscripten_sys"
+version = "0.1.1"
+source = "git+https://github.com/jamienicol/glutin?branch=wr#03285da9c14ec56296c2400c781d2c32b80d745a"
+
+[[package]]
+name = "glutin_gles2_sys"
+version = "0.1.5"
+source = "git+https://github.com/jamienicol/glutin?branch=wr#03285da9c14ec56296c2400c781d2c32b80d745a"
+dependencies = [
+ "gl_generator 0.14.0",
+ "objc",
+]
+
+[[package]]
+name = "glutin_glx_sys"
+version = "0.1.7"
+source = "git+https://github.com/jamienicol/glutin?branch=wr#03285da9c14ec56296c2400c781d2c32b80d745a"
+dependencies = [
+ "gl_generator 0.14.0",
+ "x11-dl",
+]
+
+[[package]]
+name = "glutin_wgl_sys"
+version = "0.1.5"
+source = "git+https://github.com/jamienicol/glutin?branch=wr#03285da9c14ec56296c2400c781d2c32b80d745a"
+dependencies = [
+ "gl_generator 0.14.0",
+]
+
+[[package]]
+name = "goblin"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "572564d6cba7d09775202c8e7eebc4d534d5ae36578ab402fb21e182a0ac9505"
+dependencies = [
+ "log",
+ "plain",
+ "scroll",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "heck"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "725cf19794cf90aa94e65050cb4191ff5d8fa87a498383774c47b332e3af952e"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "id-arena"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
+dependencies = [
+ "matches",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "image"
+version = "0.23.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfc5483f8d5afd3653b38a196c52294dcb239c3e1a5bade1990353ea13bcf387"
+dependencies = [
+ "bytemuck",
+ "byteorder",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+ "png",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.11.2",
+]
+
+[[package]]
+name = "inflate"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff"
+dependencies = [
+ "adler32",
+]
+
+[[package]]
+name = "inherent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daa5135cb6aa90ee17b4f0a0fb2908059b0830f90fda333f12816f275b21820c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if 1.0.0",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
+
+[[package]]
+name = "jni-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
+
+[[package]]
+name = "jobserver"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "khronos_api"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
+
+[[package]]
+name = "lalrpop"
+version = "0.19.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b30455341b0e18f276fa64540aff54deafb54c589de6aca68659c63dd2d5d823"
+dependencies = [
+ "ascii-canvas",
+ "atty",
+ "bit-set",
+ "diff",
+ "ena",
+ "itertools",
+ "lalrpop-util",
+ "petgraph",
+ "pico-args",
+ "regex",
+ "regex-syntax",
+ "string_cache",
+ "term",
+ "tiny-keccak",
+ "unicode-xid",
+]
+
+[[package]]
+name = "lalrpop-util"
+version = "0.19.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcf796c978e9b4d983414f4caedc9273aa33ee214c5b887bd55fde84c85d2dc4"
+dependencies = [
+ "regex",
+]
+
+[[package]]
+name = "lang-util"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "152d714c385fbd7dce6e198d5bcbdb1ff28a82c02657e7b939e51fe7b6ba9a10"
+dependencies = [
+ "derive_more",
+ "lalrpop-util",
+ "lang-util-derive",
+ "line-span",
+ "smol_str",
+ "text-size",
+]
+
+[[package]]
+name = "lang-util-derive"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1c46a8ccac3c9e409871080f7f31b275ea2b6528e79d92c0336fad0bdf46719"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "lexical"
+version = "6.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7aefb36fd43fef7003334742cbf77b243fcd36418a1d1bdd480d613a67968f6"
+dependencies = [
+ "lexical-core",
+]
+
+[[package]]
+name = "lexical-core"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2cde5de06e8d4c2faabc400238f9ae1c74d5412d03a7bd067645ccbc47070e46"
+dependencies = [
+ "lexical-parse-float",
+ "lexical-parse-integer",
+ "lexical-util",
+ "lexical-write-float",
+ "lexical-write-integer",
+]
+
+[[package]]
+name = "lexical-parse-float"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f"
+dependencies = [
+ "lexical-parse-integer",
+ "lexical-util",
+ "static_assertions",
+]
+
+[[package]]
+name = "lexical-parse-integer"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9"
+dependencies = [
+ "lexical-util",
+ "static_assertions",
+]
+
+[[package]]
+name = "lexical-util"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc"
+dependencies = [
+ "static_assertions",
+]
+
+[[package]]
+name = "lexical-write-float"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accabaa1c4581f05a3923d1b4cfd124c329352288b7b9da09e766b0668116862"
+dependencies = [
+ "lexical-util",
+ "lexical-write-integer",
+ "static_assertions",
+]
+
+[[package]]
+name = "lexical-write-integer"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1b6f3d1f4422866b68192d62f77bc5c700bee84f3069f2469d7bc8c77852446"
+dependencies = [
+ "lexical-util",
+ "static_assertions",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.139"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
+
+[[package]]
+name = "libloading"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd"
+dependencies = [
+ "cfg-if 1.0.0",
+ "winapi",
+]
+
+[[package]]
+name = "line-span"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "806de604a37f6d73a83c850af7b3ba33a44f330d12fd5e4ac216645b54da912b"
+
+[[package]]
+name = "linked-hash-map"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a"
+
+[[package]]
+name = "lmdb-rkv"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "447a296f7aca299cfbb50f4e4f3d49451549af655fb7215d7f8c0c3d64bad42b"
+dependencies = [
+ "bitflags",
+ "byteorder",
+ "libc",
+ "lmdb-rkv-sys",
+]
+
+[[package]]
+name = "lmdb-rkv-sys"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61b9ce6b3be08acefa3003c57b7565377432a89ec24476bbe72e11d101f852fe"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "lock_api"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "malloc_buf"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "malloc_size_of_derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e37c5d4cd9473c5f4c9c111f033f15d4df9bd378fdf615944e360a4f55a05f0b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
+
+[[package]]
+name = "maybe-uninit"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
+
+[[package]]
+name = "memchr"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
+
+[[package]]
+name = "memmap2"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b6c2ebff6180198788f5db08d7ce3bc1d0b617176678831a7510825973e357"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "memoffset"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "minidl"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "647da2489d438eb707e9bcffe0e47fb6f3f355ed288120740fc1e614cfadb9e9"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9"
+dependencies = [
+ "libc",
+ "log",
+ "miow",
+ "ntapi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+ "winapi",
+]
+
+[[package]]
+name = "miow"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "mozangle"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef98797da14500fb5eaabc90dc91cf7c42710c11330351521d72f4aa268d689c"
+dependencies = [
+ "cc",
+ "gl_generator 0.13.1",
+ "lazy_static",
+ "walkdir",
+]
+
+[[package]]
+name = "ndk"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96d868f654c72e75f8687572699cdabe755f03effbb62542768e995d5b8d699d"
+dependencies = [
+ "bitflags",
+ "jni-sys",
+ "ndk-sys",
+ "num_enum",
+ "thiserror",
+]
+
+[[package]]
+name = "ndk-context"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
+
+[[package]]
+name = "ndk-glue"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c71bee8ea72d685477e28bd004cfe1bf99c754d688cd78cad139eae4089484d4"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "ndk",
+ "ndk-context",
+ "ndk-macro",
+ "ndk-sys",
+]
+
+[[package]]
+name = "ndk-macro"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c"
+dependencies = [
+ "darling",
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "ndk-sys"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121"
+
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
+
+[[package]]
+name = "nix"
+version = "0.22.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4916f159ed8e5de0082076562152a76b7a1f64a01fd9d1e0fea002c37624faf"
+dependencies = [
+ "bitflags",
+ "cc",
+ "cfg-if 1.0.0",
+ "libc",
+ "memoffset",
+]
+
+[[package]]
+name = "nom"
+version = "6.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c5c51b9083a3c620fa67a2a635d1ce7d95b897e957d6b28ff9a5da960a103a6"
+dependencies = [
+ "bitvec",
+ "funty",
+ "memchr",
+ "version_check",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "ntapi"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfb0800a0291891dd9f4fe7bd9c19384f98f7fbe0cd0f39a2c6b88b9868bbc00"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "num_enum"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9"
+dependencies = [
+ "num_enum_derive",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "objc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
+dependencies = [
+ "malloc_buf",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e"
+
+[[package]]
+name = "ordered-float"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d84eb1409416d254e4a9c8fa56cc24701755025b458f0fcd8e59e1f5f40c23bf"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "os_str_bytes"
+version = "6.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "029d8d0b2f198229de29dca79676f2738ff952edf3fde542eb8bf94d8c21b435"
+
+[[package]]
+name = "oslog"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8343ce955f18e7e68c0207dd0ea776ec453035685395ababd2ea651c569728b3"
+dependencies = [
+ "cc",
+ "dashmap",
+ "log",
+]
+
+[[package]]
+name = "osmesa-src"
+version = "0.2.0"
+source = "git+https://github.com/servo/osmesa-src#51679b6a34c2fd98b5a4be1fa225c24ef289d8d7"
+
+[[package]]
+name = "osmesa-sys"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88cfece6e95d2e717e0872a7f53a8684712ad13822a7979bc760b9c77ec0013b"
+dependencies = [
+ "shared_library",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+dependencies = [
+ "instant",
+ "lock_api",
+ "parking_lot_core 0.8.5",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api",
+ "parking_lot_core 0.9.7",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
+dependencies = [
+ "cfg-if 1.0.0",
+ "instant",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "winapi",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-sys",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc"
+
+[[package]]
+name = "peek-poke"
+version = "0.3.0"
+dependencies = [
+ "euclid",
+ "peek-poke-derive",
+]
+
+[[package]]
+name = "peek-poke-derive"
+version = "0.3.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+ "unicode-xid",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+
+[[package]]
+name = "petgraph"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4"
+dependencies = [
+ "fixedbitset",
+ "indexmap",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
+dependencies = [
+ "phf_shared",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "pico-args"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677"
+
+[[package]]
+name = "plain"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
+
+[[package]]
+name = "plane-split"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c1f7d82649829ecdef8e258790b0587acf0a8403f0ce963473d8e918acc1643"
+dependencies = [
+ "euclid",
+ "log",
+ "smallvec",
+]
+
+[[package]]
+name = "png"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "910f09135b1ed14bb16be445a8c23ddf0777eca485fbfc7cee00d81fecab158a"
+dependencies = [
+ "bitflags",
+ "crc32fast",
+ "deflate",
+ "inflate",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a"
+dependencies = [
+ "thiserror",
+ "toml",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "radium"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
+
+[[package]]
+name = "rand"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
+dependencies = [
+ "fuchsia-cprng",
+ "libc",
+ "rand_core 0.3.1",
+ "rdrand",
+ "winapi",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
+dependencies = [
+ "rand_core 0.4.2",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "raw-window-handle"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b800beb9b6e7d2df1fe337c9e3d04e3af22a124460fb4c30fcc22c9117cefb41"
+dependencies = [
+ "cty",
+]
+
+[[package]]
+name = "rayon"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b0d8e0819fadc20c74ea8373106ead0600e3a67ef1fe8da56e39b9ae7275674"
+dependencies = [
+ "autocfg",
+ "crossbeam-deque",
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ab346ac5921dc62ffa9f89b7a773907511cdfa5490c572ae9be1be33e8afa4a"
+dependencies = [
+ "crossbeam-channel",
+ "crossbeam-deque",
+ "crossbeam-utils",
+ "lazy_static",
+ "num_cpus",
+]
+
+[[package]]
+name = "rdrand"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
+dependencies = [
+ "getrandom",
+ "redox_syscall",
+ "thiserror",
+]
+
+[[package]]
+name = "regex"
+version = "1.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
+
+[[package]]
+name = "rkv"
+version = "0.18.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f0ea3af1393b22f8fe25615b6fa5d13072b7b622e66acffc8b12b2baa0342b1"
+dependencies = [
+ "arrayref",
+ "bincode",
+ "bitflags",
+ "byteorder",
+ "id-arena",
+ "lazy_static",
+ "lmdb-rkv",
+ "log",
+ "ordered-float",
+ "paste",
+ "serde",
+ "serde_derive",
+ "thiserror",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "ron"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "300a51053b1cb55c80b7a9fde4120726ddf25ca241a1cbb926626f62fb136bff"
+dependencies = [
+ "base64",
+ "bitflags",
+ "serde",
+]
+
+[[package]]
+name = "rowan"
+version = "0.15.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5811547e7ba31e903fe48c8ceab10d40d70a101f3d15523c847cce91aa71f332"
+dependencies = [
+ "countme",
+ "hashbrown 0.12.3",
+ "memoffset",
+ "rustc-hash",
+ "text-size",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "rustversion"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06"
+
+[[package]]
+name = "ryu"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "535622e6be132bccd223f4bb2b8ac8d53cda3c7a6394944d3b2b33fb974f9d76"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "scoped-tls"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "scroll"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da"
+dependencies = [
+ "scroll_derive",
+]
+
+[[package]]
+name = "scroll_derive"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdbda6ac5cd1321e724fa9cee216f3a61885889b896f073b8f82322789c5250e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_bytes"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "325a073952621257820e7a3469f55ba4726d8b28657e7e36653d1c36dc2c84ae"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "servo-fontconfig"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7e3e22fe5fd73d04ebf0daa049d3efe3eae55369ce38ab16d07ddd9ac5c217c"
+dependencies = [
+ "libc",
+ "servo-fontconfig-sys",
+]
+
+[[package]]
+name = "servo-fontconfig-sys"
+version = "5.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e36b879db9892dfa40f95da1c38a835d41634b825fbd8c4c418093d53c24b388"
+dependencies = [
+ "expat-sys",
+ "freetype-sys",
+ "pkg-config",
+]
+
+[[package]]
+name = "shared_library"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11"
+dependencies = [
+ "lazy_static",
+ "libc",
+]
+
+[[package]]
+name = "siphasher"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
+
+[[package]]
+name = "smallvec"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "smithay-client-toolkit"
+version = "0.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a28f16a97fa0e8ce563b2774d1e732dd5d4025d2772c5dba0a41a0f90a29da3"
+dependencies = [
+ "bitflags",
+ "calloop",
+ "dlib",
+ "lazy_static",
+ "log",
+ "memmap2",
+ "nix",
+ "pkg-config",
+ "wayland-client",
+ "wayland-cursor",
+ "wayland-protocols",
+]
+
+[[package]]
+name = "smol_str"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fad6c857cbab2627dcf01ec85a623ca4e7dcb5691cbaa3d7fb7653671f0d09c9"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "string_cache"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b"
+dependencies = [
+ "new_debug_unreachable",
+ "once_cell",
+ "parking_lot 0.12.1",
+ "phf_shared",
+ "precomputed-hash",
+ "serde",
+]
+
+[[package]]
+name = "string_cache_codegen"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "svg_fmt"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fb1df15f412ee2e9dfc1c504260fa695c1c3f10fe9f4a6ee2d2184d7d6450e2"
+
+[[package]]
+name = "swgl"
+version = "0.1.0"
+dependencies = [
+ "cc",
+ "gleam",
+ "glsl-to-cxx",
+ "webrender_build",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "unicode-xid",
+]
+
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
+[[package]]
+name = "term"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f"
+dependencies = [
+ "dirs-next",
+ "rustversion",
+ "winapi",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "text-size"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "288cb548dbe72b652243ea797201f3d481a0609a967980fcc5b2315ea811560a"
+
+[[package]]
+name = "textwrap"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
+
+[[package]]
+name = "thiserror"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "time"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
+dependencies = [
+ "libc",
+ "wasi 0.10.0+wasi-snapshot-preview1",
+ "winapi",
+]
+
+[[package]]
+name = "tiny-keccak"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
+dependencies = [
+ "crunchy",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
+[[package]]
+name = "toml"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "topological-sort"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa7c7f42dea4b1b99439786f5633aeb9c14c1b53f75e282803c2ec2ad545873c"
+
+[[package]]
+name = "tracy-rs"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce607aae8ab0ab3abf3a2723a9ab6f09bb8639ed83fdd888d857b8e556c868d8"
+dependencies = [
+ "minidl",
+]
+
+[[package]]
+name = "unicase"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
+
+[[package]]
+name = "uniffi"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f71cc01459bc34cfe43fabf32b39f1228709bc6db1b3a664a92940af3d062376"
+dependencies = [
+ "anyhow",
+ "uniffi_build",
+ "uniffi_core",
+ "uniffi_macros",
+]
+
+[[package]]
+name = "uniffi_bindgen"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbbba5103051c18f10b22f80a74439ddf7100273f217a547005d2735b2498994"
+dependencies = [
+ "anyhow",
+ "askama",
+ "bincode",
+ "camino",
+ "fs-err",
+ "glob",
+ "goblin",
+ "heck",
+ "once_cell",
+ "paste",
+ "serde",
+ "serde_json",
+ "toml",
+ "uniffi_meta",
+ "uniffi_testing",
+ "weedle2",
+]
+
+[[package]]
+name = "uniffi_build"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ee1a28368ff3d83717e3d3e2e15a66269c43488c3f036914131bb68892f29fb"
+dependencies = [
+ "anyhow",
+ "camino",
+ "uniffi_bindgen",
+]
+
+[[package]]
+name = "uniffi_checksum_derive"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03de61393a42b4ad4984a3763c0600594ac3e57e5aaa1d05cede933958987c03"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "uniffi_core"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2b4852d638d74ca2d70e450475efb6d91fe6d54a7cd8d6bd80ad2ee6cd7daa"
+dependencies = [
+ "anyhow",
+ "bytes",
+ "camino",
+ "cargo_metadata",
+ "log",
+ "once_cell",
+ "paste",
+ "static_assertions",
+]
+
+[[package]]
+name = "uniffi_macros"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa03394de21e759e0022f1ea8d992d2e39290d735b9ed52b1f74b20a684f794e"
+dependencies = [
+ "bincode",
+ "camino",
+ "fs-err",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn",
+ "toml",
+ "uniffi_build",
+ "uniffi_meta",
+]
+
+[[package]]
+name = "uniffi_meta"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66fdab2c436aed7a6391bec64204ec33948bfed9b11b303235740771f85c4ea6"
+dependencies = [
+ "serde",
+ "siphasher",
+ "uniffi_checksum_derive",
+]
+
+[[package]]
+name = "uniffi_testing"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92b0570953ec41d97ce23e3b92161ac18231670a1f97523258a6d2ab76d7f76c"
+dependencies = [
+ "anyhow",
+ "camino",
+ "cargo_metadata",
+ "fs-err",
+ "once_cell",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "url"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "uuid"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "version_check"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce"
+
+[[package]]
+name = "walkdir"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d"
+dependencies = [
+ "same-file",
+ "winapi",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.10.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad"
+dependencies = [
+ "cfg-if 1.0.0",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4"
+dependencies = [
+ "bumpalo",
+ "lazy_static",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744"
+
+[[package]]
+name = "wayland-client"
+version = "0.29.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91223460e73257f697d9e23d401279123d36039a3f7a449e983f123292d4458f"
+dependencies = [
+ "bitflags",
+ "downcast-rs",
+ "libc",
+ "nix",
+ "scoped-tls",
+ "wayland-commons",
+ "wayland-scanner",
+ "wayland-sys",
+]
+
+[[package]]
+name = "wayland-commons"
+version = "0.29.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94f6e5e340d7c13490eca867898c4cec5af56c27a5ffe5c80c6fc4708e22d33e"
+dependencies = [
+ "nix",
+ "once_cell",
+ "smallvec",
+ "wayland-sys",
+]
+
+[[package]]
+name = "wayland-cursor"
+version = "0.29.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c52758f13d5e7861fc83d942d3d99bf270c83269575e52ac29e5b73cb956a6bd"
+dependencies = [
+ "nix",
+ "wayland-client",
+ "xcursor",
+]
+
+[[package]]
+name = "wayland-egl"
+version = "0.29.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83281d69ee162b59031c666385e93bde4039ec553b90c4191cdb128ceea29a3a"
+dependencies = [
+ "wayland-client",
+ "wayland-sys",
+]
+
+[[package]]
+name = "wayland-protocols"
+version = "0.29.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60147ae23303402e41fe034f74fb2c35ad0780ee88a1c40ac09a3be1e7465741"
+dependencies = [
+ "bitflags",
+ "wayland-client",
+ "wayland-commons",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-scanner"
+version = "0.29.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39a1ed3143f7a143187156a2ab52742e89dac33245ba505c17224df48939f9e0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "xml-rs",
+]
+
+[[package]]
+name = "wayland-sys"
+version = "0.29.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9341df79a8975679188e37dab3889bfa57c44ac2cb6da166f519a81cbe452d4"
+dependencies = [
+ "dlib",
+ "lazy_static",
+ "pkg-config",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webrender"
+version = "0.62.0"
+dependencies = [
+ "bincode",
+ "bitflags",
+ "build-parallel",
+ "byteorder",
+ "derive_more",
+ "etagere",
+ "euclid",
+ "firefox-on-glean",
+ "fxhash",
+ "gleam",
+ "glean",
+ "glslopt",
+ "lazy_static",
+ "log",
+ "malloc_size_of_derive",
+ "mozangle",
+ "num-traits",
+ "peek-poke",
+ "plane-split",
+ "png",
+ "rand 0.4.6",
+ "rayon",
+ "ron",
+ "serde",
+ "smallvec",
+ "svg_fmt",
+ "swgl",
+ "time",
+ "topological-sort",
+ "tracy-rs",
+ "webrender_api",
+ "webrender_build",
+ "wr_glyph_rasterizer",
+ "wr_malloc_size_of",
+]
+
+[[package]]
+name = "webrender-examples"
+version = "0.1.0"
+dependencies = [
+ "app_units",
+ "core-foundation 0.7.0",
+ "env_logger",
+ "euclid",
+ "gleam",
+ "glutin",
+ "rayon",
+ "webrender",
+ "winit",
+]
+
+[[package]]
+name = "webrender_api"
+version = "0.62.0"
+dependencies = [
+ "app_units",
+ "bitflags",
+ "byteorder",
+ "crossbeam-channel",
+ "euclid",
+ "malloc_size_of_derive",
+ "peek-poke",
+ "serde",
+ "serde_bytes",
+ "serde_derive",
+ "time",
+ "wr_malloc_size_of",
+]
+
+[[package]]
+name = "webrender_build"
+version = "0.0.2"
+dependencies = [
+ "bitflags",
+ "lazy_static",
+ "serde",
+]
+
+[[package]]
+name = "weedle2"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e79c5206e1f43a2306fd64bdb95025ee4228960f2e6c5a8b173f3caaf807741"
+dependencies = [
+ "nom 7.1.1",
+]
+
+[[package]]
+name = "whatsys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a48baa5192a81e347fbaae5f5e50bc27955ef084fc0fda5ccb1f8f9aecd73c9"
+dependencies = [
+ "cc",
+ "cfg-if 1.0.0",
+ "libc",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa515c5163a99cc82bab70fd3bfdd36d827be85de63737b40fcef2ce084a436e"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "winit"
+version = "0.26.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b43cc931d58b99461188607efd7acb2a093e65fc621f54cad78517a6063e73a"
+dependencies = [
+ "bitflags",
+ "cocoa",
+ "core-foundation 0.9.2",
+ "core-graphics 0.22.3",
+ "core-video-sys",
+ "dispatch",
+ "instant",
+ "lazy_static",
+ "libc",
+ "log",
+ "mio",
+ "ndk",
+ "ndk-glue",
+ "ndk-sys",
+ "objc",
+ "parking_lot 0.11.2",
+ "percent-encoding",
+ "raw-window-handle",
+ "smithay-client-toolkit",
+ "wasm-bindgen",
+ "wayland-client",
+ "wayland-protocols",
+ "web-sys",
+ "winapi",
+ "x11-dl",
+]
+
+[[package]]
+name = "wio"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "wr_glyph_rasterizer"
+version = "0.1.0"
+dependencies = [
+ "core-foundation 0.9.2",
+ "core-graphics 0.22.3",
+ "core-text",
+ "dwrote",
+ "env_logger",
+ "euclid",
+ "firefox-on-glean",
+ "freetype",
+ "fxhash",
+ "gleam",
+ "glean",
+ "glutin",
+ "lazy_static",
+ "libc",
+ "log",
+ "malloc_size_of_derive",
+ "objc",
+ "rayon",
+ "serde",
+ "smallvec",
+ "tracy-rs",
+ "webrender_api",
+ "winit",
+ "wr_malloc_size_of",
+]
+
+[[package]]
+name = "wr_malloc_size_of"
+version = "0.0.2"
+dependencies = [
+ "app_units",
+ "euclid",
+]
+
+[[package]]
+name = "wrench"
+version = "0.3.0"
+dependencies = [
+ "base64",
+ "chrono",
+ "clap",
+ "core-foundation 0.9.2",
+ "core-graphics 0.22.3",
+ "crossbeam",
+ "dwrote",
+ "env_logger",
+ "font-loader",
+ "gleam",
+ "glsl-lang",
+ "glutin",
+ "image",
+ "libc",
+ "log",
+ "mozangle",
+ "ndk-glue",
+ "osmesa-src",
+ "osmesa-sys",
+ "semver",
+ "serde",
+ "serde_json",
+ "swgl",
+ "time",
+ "tracy-rs",
+ "webrender",
+ "webrender_build",
+ "winit",
+ "yaml-rust",
+]
+
+[[package]]
+name = "wyz"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
+
+[[package]]
+name = "x11-dl"
+version = "2.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bf981e3a5b3301209754218f962052d4d9ee97e478f4d26d4a6eced34c1fef8"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "maybe-uninit",
+ "pkg-config",
+]
+
+[[package]]
+name = "xcursor"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "463705a63313cd4301184381c5e8042f0a7e9b4bb63653f216311d4ae74690b7"
+dependencies = [
+ "nom 7.1.1",
+]
+
+[[package]]
+name = "xml-rs"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bb76e5c421bbbeb8924c60c030331b345555024d56261dae8f3e786ed817c23"
+
+[[package]]
+name = "yaml-rust"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65923dd1784f44da1d2c3dbbc5e822045628c590ba72123e1c73d3c230c4434d"
+dependencies = [
+ "linked-hash-map",
+]
+
+[[package]]
+name = "zeitstempel"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eeea3eb6a30ed24e374f59368d3917c5180a845fdd4ed6f1b2278811a9e826f8"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "once_cell",
+]
diff --git a/gfx/wr/Cargo.toml b/gfx/wr/Cargo.toml
new file mode 100644
index 0000000000..6be188c09c
--- /dev/null
+++ b/gfx/wr/Cargo.toml
@@ -0,0 +1,27 @@
+[workspace]
+members = [
+ "examples",
+ "webrender",
+ "webrender_api",
+ "wrench",
+ "example-compositor/compositor",
+]
+
+[profile.release]
+debug = true
+panic = "abort"
+
+[profile.dev]
+panic = "abort"
+
+# optimizing glsl more makes a big difference in swgl build times
+[profile.dev.package.glsl]
+opt-level = 2
+
+[profile.release.package.glsl]
+opt-level = 2
+
+[patch.crates-io]
+firefox-on-glean = { path = "fog" }
+# use a patched version of glutin that works on android
+glutin = { version = "0.28", git = "https://github.com/jamienicol/glutin", branch="wr" }
diff --git a/gfx/wr/LICENSE b/gfx/wr/LICENSE
new file mode 100644
index 0000000000..398385c9fb
--- /dev/null
+++ b/gfx/wr/LICENSE
@@ -0,0 +1,374 @@
+ Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+means each individual or legal entity that creates, contributes to
+the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+means the combination of the Contributions of others (if any) used
+by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+means Source Code Form to which the initial Contributor has attached
+the notice in Exhibit A, the Executable Form of such Source Code
+Form, and Modifications of such Source Code Form, in each case
+including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+means
+
+(a) that the initial Contributor has attached the notice described
+in Exhibit B to the Covered Software; or
+
+(b) that the Covered Software was made available under the terms of
+version 1.1 or earlier of the License, but not also under the
+terms of a Secondary License.
+
+1.6. "Executable Form"
+means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+means a work that combines Covered Software with other material, in
+a separate file or files, that is not Covered Software.
+
+1.8. "License"
+means this document.
+
+1.9. "Licensable"
+means having the right to grant, to the maximum extent possible,
+whether at the time of the initial grant or subsequently, any and
+all of the rights conveyed by this License.
+
+1.10. "Modifications"
+means any of the following:
+
+(a) any file in Source Code Form that results from an addition to,
+deletion from, or modification of the contents of Covered
+Software; or
+
+(b) any new file in Source Code Form that contains any Covered
+Software.
+
+1.11. "Patent Claims" of a Contributor
+means any patent claim(s), including without limitation, method,
+process, and apparatus claims, in any patent Licensable by such
+Contributor that would be infringed, but for the grant of the
+License, by the making, using, selling, offering for sale, having
+made, import, or transfer of either its Contributions or its
+Contributor Version.
+
+1.12. "Secondary License"
+means either the GNU General Public License, Version 2.0, the GNU
+Lesser General Public License, Version 2.1, the GNU Affero General
+Public License, Version 3.0, or any later versions of those
+licenses.
+
+1.13. "Source Code Form"
+means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+means an individual or a legal entity exercising rights under this
+License. For legal entities, "You" includes any entity that
+controls, is controlled by, or is under common control with You. For
+purposes of this definition, "control" means (a) the power, direct
+or indirect, to cause the direction or management of such entity,
+whether by contract or otherwise, or (b) ownership of more than
+fifty percent (50%) of the outstanding shares or beneficial
+ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+Licensable by such Contributor to use, reproduce, make available,
+modify, display, perform, distribute, and otherwise exploit its
+Contributions, either on an unmodified basis, with Modifications, or
+as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+for sale, have made, import, and otherwise transfer either its
+Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+or
+
+(b) for infringements caused by: (i) Your and any other third party's
+modifications of Covered Software, or (ii) the combination of its
+Contributions with other software (except as part of its Contributor
+Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+Form, as described in Section 3.1, and You must inform recipients of
+the Executable Form how they can obtain a copy of such Source Code
+Form by reasonable means in a timely manner, at a charge no more
+than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+License, or sublicense it under different terms, provided that the
+license for the Executable Form does not attempt to limit or alter
+the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+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/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+This Source Code Form is "Incompatible With Secondary Licenses", as
+defined by the Mozilla Public License, v. 2.0.
+
diff --git a/gfx/wr/README.md b/gfx/wr/README.md
new file mode 100644
index 0000000000..6162a5f86b
--- /dev/null
+++ b/gfx/wr/README.md
@@ -0,0 +1,53 @@
+# WebRender
+
+[![Version](https://img.shields.io/crates/v/webrender.svg)](https://crates.io/crates/webrender)
+
+WebRender is a GPU-based 2D rendering engine written in [Rust](https://www.rust-lang.org/). [Firefox](https://www.mozilla.org/firefox), the research web browser [Servo](https://github.com/servo/servo), and other GUI frameworks draw with it. It currently uses the OpenGL API internally.
+
+Note that the canonical home for this code is in gfx/wr folder of the
+mozilla-central repository at https://hg.mozilla.org/mozilla-central. The
+Github repository at https://github.com/servo/webrender should be considered
+a downstream mirror, although it contains additional metadata (such as Github
+wiki pages) that do not exist in mozilla-central. Pull requests against the
+Github repository are still being accepted, although once reviewed, they will
+be landed on mozilla-central first and then mirrored back. If you are familiar
+with the mozilla-central contribution workflow, filing bugs in
+[Bugzilla](https://bugzilla.mozilla.org/enter_bug.cgi?product=Core&component=Graphics%3A%20WebRender)
+and submitting patches there would be preferred.
+
+## Update as a Dependency
+After updating shaders in WebRender, go to servo and:
+
+ * Go to the servo directory and do ./mach update-cargo -p webrender
+ * Create a pull request to servo
+
+
+## Use WebRender with Servo
+To use a local copy of WebRender with servo, go to your servo build directory and:
+
+ * Edit Cargo.toml
+ * Add at the end of the file:
+
+```
+[patch."https://github.com/servo/webrender"]
+"webrender" = { path = "<path>/webrender" }
+"webrender_api" = { path = "<path>/webrender_api" }
+```
+
+where `<path>` is the path to your local copy of WebRender.
+
+ * Build as normal
+
+## Documentation
+
+The Wiki has a [few pages](https://github.com/servo/webrender/wiki/) describing the internals and conventions of WebRender.
+
+## Testing
+
+Tests run using OSMesa to get consistent rendering across platforms.
+
+Still there may be differences depending on font libraries on your system, for
+example.
+
+See [this gist](https://gist.github.com/finalfantasia/129cae811e02bf4551ac) for
+how to make the text tests useful in Fedora, for example.
diff --git a/gfx/wr/ci-scripts/docker-image/Dockerfile b/gfx/wr/ci-scripts/docker-image/Dockerfile
new file mode 100644
index 0000000000..3b7805195d
--- /dev/null
+++ b/gfx/wr/ci-scripts/docker-image/Dockerfile
@@ -0,0 +1,9 @@
+FROM debian:bullseye-20221205
+
+COPY setup.sh /root
+RUN cd /root && ./setup.sh
+
+RUN useradd -d /home/worker -s /bin/bash -m worker
+USER worker
+WORKDIR /home/worker
+CMD /bin/bash
diff --git a/gfx/wr/ci-scripts/docker-image/setup.sh b/gfx/wr/ci-scripts/docker-image/setup.sh
new file mode 100755
index 0000000000..ccf36d1be1
--- /dev/null
+++ b/gfx/wr/ci-scripts/docker-image/setup.sh
@@ -0,0 +1,56 @@
+#!/usr/bin/env bash
+
+# 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/. */
+
+set -o errexit
+set -o nounset
+set -o pipefail
+set -o xtrace
+
+test "$(whoami)" == 'root'
+
+# Install stuff we need
+apt-get -y update
+apt-get install -y \
+ bison \
+ bzip2 \
+ cmake \
+ curl \
+ flex \
+ gcc \
+ git \
+ g++ \
+ libfontconfig1-dev \
+ libgl1-mesa-dev \
+ libx11-dev \
+ llvm-dev \
+ ninja-build \
+ pkg-config \
+ python3-pip \
+ python3-six \
+ python3-setuptools \
+ python3-mako \
+ software-properties-common \
+ clang
+
+# some reftests fail with freetype >= 2.10, so downgrade to the version in
+# Debian buster. See bug 1804782.
+apt-get -y remove libfreetype-dev
+curl -LO http://snapshot.debian.org/archive/debian/20220718T031307Z/pool/main/f/freetype/libfreetype6_2.9.1-3%2Bdeb10u3_amd64.deb
+curl -LO http://snapshot.debian.org/archive/debian/20220718T031307Z/pool/main/f/freetype/libfreetype6-dev_2.9.1-3%2Bdeb10u3_amd64.deb
+
+dpkg -i libfreetype6*.deb
+rm libfreetype6*.deb
+
+# Other stuff we need
+
+# Normally, we'd
+# pip3 install servo-tidy==0.3.0
+# but the last version of servo-tidy published on pypi doesn't have the
+# python3 fixes that are in the servo repo.
+git clone -n https://github.com/servo/servo/
+git -C servo checkout 65a4d1646da46c37fe748add6dcf24b62ebb602a
+pip3 install servo/python/tidy
+rm -rf servo
diff --git a/gfx/wr/ci-scripts/etc/wr-darwin.meson b/gfx/wr/ci-scripts/etc/wr-darwin.meson
new file mode 100644
index 0000000000..e6efc7deb1
--- /dev/null
+++ b/gfx/wr/ci-scripts/etc/wr-darwin.meson
@@ -0,0 +1,8 @@
+[binaries]
+llvm-config = '/builds/worker/fetches/clang-mac/clang/bin/llvm-config'
+
+[host_machine]
+system = 'darwin'
+cpu_family = 'x86_64'
+cpu = 'i686'
+endian = 'little'
diff --git a/gfx/wr/ci-scripts/install-meson.sh b/gfx/wr/ci-scripts/install-meson.sh
new file mode 100755
index 0000000000..35fb3970e8
--- /dev/null
+++ b/gfx/wr/ci-scripts/install-meson.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+
+# 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/. */
+
+# This file downloads and installs meson which is required for building
+# osmesa-src, a dependency of wrench.
+
+set -o errexit
+set -o nounset
+set -o pipefail
+set -o xtrace
+
+MESON_VER=1.1.0
+MESON_BASE_URL="https://github.com/mesonbuild/meson/releases/download"
+
+curl -L ${MESON_BASE_URL}/${MESON_VER}/meson-${MESON_VER}.tar.gz -o meson.tar.gz
+tar -xf meson.tar.gz
+mv meson-${MESON_VER} meson
+cd meson
+ln -s meson.py meson
diff --git a/gfx/wr/ci-scripts/linux-debug-tests.sh b/gfx/wr/ci-scripts/linux-debug-tests.sh
new file mode 100755
index 0000000000..38d974bd4b
--- /dev/null
+++ b/gfx/wr/ci-scripts/linux-debug-tests.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+
+# 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/. */
+
+# This must be run from the root webrender directory!
+# Users may set the CARGOFLAGS environment variable to pass
+# additional flags to cargo if desired.
+
+set -o errexit
+set -o nounset
+set -o pipefail
+set -o xtrace
+
+CARGOFLAGS=${CARGOFLAGS:-"--verbose"} # default to --verbose if not set
+
+pushd webrender
+cargo build ${CARGOFLAGS} --no-default-features
+cargo build ${CARGOFLAGS} --no-default-features --features capture
+cargo build ${CARGOFLAGS} --features capture,profiler
+cargo build ${CARGOFLAGS} --features replay
+popd
+
+pushd wrench
+cargo build ${CARGOFLAGS} --features env_logger
+OPTIMIZED=0 python3 script/headless.py reftest
+popd
+
+pushd examples
+cargo build ${CARGOFLAGS}
+popd
+
+cargo test ${CARGOFLAGS} \
+ --all --exclude compositor --exclude compositor-wayland \
+ --exclude compositor-windows --exclude glsl-to-cxx --exclude swgl
diff --git a/gfx/wr/ci-scripts/linux-release-tests.sh b/gfx/wr/ci-scripts/linux-release-tests.sh
new file mode 100755
index 0000000000..6f3dc72d33
--- /dev/null
+++ b/gfx/wr/ci-scripts/linux-release-tests.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+
+# 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/. */
+
+# This must be run from the root webrender directory!
+# Users may set the CARGOFLAGS environment variable to pass
+# additional flags to cargo if desired.
+
+set -o errexit
+set -o nounset
+set -o pipefail
+set -o xtrace
+
+CARGOFLAGS=${CARGOFLAGS:-""} # default to empty if not set
+
+pushd wrench
+# Test that all shaders compile successfully and pass tests.
+python3 script/headless.py --precache test_init
+python3 script/headless.py --precache --use-unoptimized-shaders test_init
+python3 script/headless.py test_shaders
+
+python3 script/headless.py reftest
+python3 script/headless.py rawtest
+python3 script/headless.py test_invalidation
+CXX=clang++ cargo run ${CARGOFLAGS} --release --features=software -- \
+ --software --headless reftest
+popd
diff --git a/gfx/wr/ci-scripts/macos-debug-tests.sh b/gfx/wr/ci-scripts/macos-debug-tests.sh
new file mode 100755
index 0000000000..6dbf1f1231
--- /dev/null
+++ b/gfx/wr/ci-scripts/macos-debug-tests.sh
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+
+# 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/. */
+
+# This must be run from the root webrender directory!
+# Users may set the CARGOFLAGS environment variable to pass
+# additional flags to cargo if desired.
+
+# Note that this script is run in a special cross-compiling configuration,
+# where CARGOTESTFLAGS includes `--no-run`, and the binaries produced by
+# `cargo test` are run on a different machine. When making changes to this
+# file, please ensure any such binaries produced by `cargo test` are not
+# deleted, or they may not get run as expected.
+
+set -o errexit
+set -o nounset
+set -o pipefail
+set -o xtrace
+
+CARGOFLAGS=${CARGOFLAGS:-"--verbose"} # default to --verbose if not set
+CARGOTESTFLAGS=${CARGOTESTFLAGS:-""}
+
+pushd webrender
+cargo check ${CARGOFLAGS} --no-default-features
+cargo check ${CARGOFLAGS} --no-default-features --features capture
+cargo check ${CARGOFLAGS} --features capture,profiler
+cargo check ${CARGOFLAGS} --features replay
+popd
+
+pushd wrench
+cargo check ${CARGOFLAGS} --features env_logger
+popd
+
+pushd examples
+cargo check ${CARGOFLAGS}
+popd
+
+cargo test ${CARGOFLAGS} ${CARGOTESTFLAGS} \
+ --all --exclude compositor --exclude compositor-wayland \
+ --exclude compositor-windows --exclude glsl-to-cxx --exclude swgl
diff --git a/gfx/wr/ci-scripts/macos-release-tests.sh b/gfx/wr/ci-scripts/macos-release-tests.sh
new file mode 100755
index 0000000000..1c7f97ae67
--- /dev/null
+++ b/gfx/wr/ci-scripts/macos-release-tests.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+
+# 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/. */
+
+# This must be run from the root webrender directory!
+# Users may set the CARGOFLAGS environment variable to pass
+# additional flags to cargo if desired.
+# The WRENCH_BINARY environment variable, if set, is used to run
+# the precached reftest.
+
+set -o errexit
+set -o nounset
+set -o pipefail
+set -o xtrace
+
+CARGOFLAGS=${CARGOFLAGS:-""} # default to empty if not set
+WRENCH_BINARY=${WRENCH_BINARY:-""}
+
+pushd wrench
+
+# Test that all shaders compile successfully and pass tests.
+python3 script/headless.py --precache test_init
+python3 script/headless.py --precache --use-unoptimized-shaders test_init
+python3 script/headless.py test_shaders
+
+python3 script/headless.py reftest
+python3 script/headless.py test_invalidation
+if [[ -z "${WRENCH_BINARY}" ]]; then
+ cargo build ${CARGOFLAGS} --release
+ WRENCH_BINARY="../target/release/wrench"
+fi
+"${WRENCH_BINARY}" --precache \
+ reftest reftests/clip/fixed-position-clipping.yaml
+popd
diff --git a/gfx/wr/ci-scripts/set-screenresolution.ps1 b/gfx/wr/ci-scripts/set-screenresolution.ps1
new file mode 100644
index 0000000000..2f58443417
--- /dev/null
+++ b/gfx/wr/ci-scripts/set-screenresolution.ps1
@@ -0,0 +1,124 @@
+# http://blogs.technet.com/b/heyscriptingguy/archive/2010/07/07/hey-scripting-guy-how-can-i-change-my-desktop-monitor-resolution-via-windows-powershell.aspx
+
+Function Set-ScreenResolution {
+param (
+[Parameter(Mandatory=$true,
+ Position = 0)]
+[int]
+$Width,
+[Parameter(Mandatory=$true,
+ Position = 1)]
+[int]
+$Height
+)
+$pinvokeCode = @"
+using System;
+using System.Runtime.InteropServices;
+namespace Resolution
+{
+ [StructLayout(LayoutKind.Sequential)]
+ public struct DEVMODE1
+ {
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
+ public string dmDeviceName;
+ public short dmSpecVersion;
+ public short dmDriverVersion;
+ public short dmSize;
+ public short dmDriverExtra;
+ public int dmFields;
+ public short dmOrientation;
+ public short dmPaperSize;
+ public short dmPaperLength;
+ public short dmPaperWidth;
+ public short dmScale;
+ public short dmCopies;
+ public short dmDefaultSource;
+ public short dmPrintQuality;
+ public short dmColor;
+ public short dmDuplex;
+ public short dmYResolution;
+ public short dmTTOption;
+ public short dmCollate;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
+ public string dmFormName;
+ public short dmLogPixels;
+ public short dmBitsPerPel;
+ public int dmPelsWidth;
+ public int dmPelsHeight;
+ public int dmDisplayFlags;
+ public int dmDisplayFrequency;
+ public int dmICMMethod;
+ public int dmICMIntent;
+ public int dmMediaType;
+ public int dmDitherType;
+ public int dmReserved1;
+ public int dmReserved2;
+ public int dmPanningWidth;
+ public int dmPanningHeight;
+ };
+ class User_32
+ {
+ [DllImport("user32.dll")]
+ public static extern int EnumDisplaySettings(string deviceName, int modeNum, ref DEVMODE1 devMode);
+ [DllImport("user32.dll")]
+ public static extern int ChangeDisplaySettings(ref DEVMODE1 devMode, int flags);
+ public const int ENUM_CURRENT_SETTINGS = -1;
+ public const int CDS_UPDATEREGISTRY = 0x01;
+ public const int CDS_TEST = 0x02;
+ public const int DISP_CHANGE_SUCCESSFUL = 0;
+ public const int DISP_CHANGE_RESTART = 1;
+ public const int DISP_CHANGE_FAILED = -1;
+ }
+ public class PrmaryScreenResolution
+ {
+ static public string ChangeResolution(int width, int height)
+ {
+ DEVMODE1 dm = GetDevMode1();
+ if (0 != User_32.EnumDisplaySettings(null, User_32.ENUM_CURRENT_SETTINGS, ref dm))
+ {
+ dm.dmPelsWidth = width;
+ dm.dmPelsHeight = height;
+ int iRet = User_32.ChangeDisplaySettings(ref dm, User_32.CDS_TEST);
+ if (iRet == User_32.DISP_CHANGE_FAILED)
+ {
+ return "Unable To Process Your Request. Sorry For This Inconvenience.";
+ }
+ else
+ {
+ iRet = User_32.ChangeDisplaySettings(ref dm, User_32.CDS_UPDATEREGISTRY);
+ switch (iRet)
+ {
+ case User_32.DISP_CHANGE_SUCCESSFUL:
+ {
+ return "Success";
+ }
+ case User_32.DISP_CHANGE_RESTART:
+ {
+ return "You Need To Reboot For The Change To Happen.\n If You Feel Any Problem After Rebooting Your Machine\nThen Try To Change Resolution In Safe Mode.";
+ }
+ default:
+ {
+ return "Failed To Change The Resolution";
+ }
+ }
+ }
+ }
+ else
+ {
+ return "Failed To Change The Resolution.";
+ }
+ }
+ private static DEVMODE1 GetDevMode1()
+ {
+ DEVMODE1 dm = new DEVMODE1();
+ dm.dmDeviceName = new String(new char[32]);
+ dm.dmFormName = new String(new char[32]);
+ dm.dmSize = (short)Marshal.SizeOf(dm);
+ return dm;
+ }
+ }
+}
+"@
+Add-Type $pinvokeCode -ErrorAction SilentlyContinue
+[Resolution.PrmaryScreenResolution]::ChangeResolution($width,$height)
+}
diff --git a/gfx/wr/ci-scripts/windows-tests.cmd b/gfx/wr/ci-scripts/windows-tests.cmd
new file mode 100755
index 0000000000..fcafcf2b90
--- /dev/null
+++ b/gfx/wr/ci-scripts/windows-tests.cmd
@@ -0,0 +1,41 @@
+:: 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/. */
+
+:: This must be run from the root webrender directory!
+:: Users may set the CARGOFLAGS environment variable to pass
+:: additional flags to cargo if desired.
+
+if NOT DEFINED CARGOFLAGS SET CARGOFLAGS=--verbose
+
+pushd webrender_api
+cargo test %CARGOFLAGS%
+if %ERRORLEVEL% NEQ 0 EXIT /b %ERRORLEVEL%
+popd
+
+pushd webrender
+cargo test %CARGOFLAGS%
+if %ERRORLEVEL% NEQ 0 EXIT /b %ERRORLEVEL%
+popd
+
+pushd wrench
+cargo test --verbose
+if %ERRORLEVEL% NEQ 0 EXIT /b %ERRORLEVEL%
+:: Test that all shaders compile successfully and pass tests.
+:: --precache compiles all shaders during initialization, therefore if init
+:: is successful then the shaders compile.
+cargo run --release -- --angle --precache test_init
+if %ERRORLEVEL% NEQ 0 EXIT /b %ERRORLEVEL%
+cargo run --release -- --angle --precache --use-unoptimized-shaders test_init
+if %ERRORLEVEL% NEQ 0 EXIT /b %ERRORLEVEL%
+cargo run --release -- --angle test_shaders
+if %ERRORLEVEL% NEQ 0 EXIT /b %ERRORLEVEL%
+
+cargo run --release -- --angle reftest
+if %ERRORLEVEL% NEQ 0 EXIT /b %ERRORLEVEL%
+popd
+
+pushd examples
+cargo check --verbose
+if %ERRORLEVEL% NEQ 0 EXIT /b %ERRORLEVEL%
+popd
diff --git a/gfx/wr/example-compositor/compositor-wayland/Cargo.toml b/gfx/wr/example-compositor/compositor-wayland/Cargo.toml
new file mode 100644
index 0000000000..0f5bba73b5
--- /dev/null
+++ b/gfx/wr/example-compositor/compositor-wayland/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "compositor-wayland"
+version = "0.1.0"
+authors = ["Glenn Watson <gw@intuitionlibrary.com>",
+ "Robert Mader <robert.mader@posteo.de>"]
+edition = "2018"
+license = "MPL-2.0"
+
+[build-dependencies]
+cc = "1.0"
+pkg-config = "^0.3.17"
diff --git a/gfx/wr/example-compositor/compositor-wayland/build.rs b/gfx/wr/example-compositor/compositor-wayland/build.rs
new file mode 100644
index 0000000000..ab418b94c7
--- /dev/null
+++ b/gfx/wr/example-compositor/compositor-wayland/build.rs
@@ -0,0 +1,63 @@
+/* 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::process::Command;
+use std::env;
+use std::fs;
+
+extern crate pkg_config;
+
+fn main() {
+ let out_dir = env::var("OUT_DIR").unwrap();
+
+ fs::create_dir_all(&format!("{}/include", out_dir)).unwrap();
+ Command::new("wayland-scanner")
+ .args(&["client-header", "/usr/share/wayland-protocols/stable/viewporter/viewporter.xml"])
+ .arg(&format!("{}/include/viewporter-client-protocol.h", out_dir))
+ .status().unwrap();
+
+ Command::new("wayland-scanner")
+ .args(&["public-code", "/usr/share/wayland-protocols/stable/viewporter/viewporter.xml"])
+ .arg(&format!("{}/viewporter-protocol.c", out_dir))
+ .status().unwrap();
+
+ Command::new("wayland-scanner")
+ .args(&["client-header", "/usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml"])
+ .arg(&format!("{}/include/xdg-shell-client-protocol.h", out_dir))
+ .status().unwrap();
+
+ Command::new("wayland-scanner")
+ .args(&["public-code", "/usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml"])
+ .arg(&format!("{}/xdg-shell-protocol.c", out_dir))
+ .status().unwrap();
+
+ cc::Build::new()
+ .include(&format!("{}/include", out_dir))
+ .file("src/lib.cpp")
+ .file(&format!("{}/viewporter-protocol.c", out_dir))
+ .file(&format!("{}/xdg-shell-protocol.c", out_dir))
+ .compile("wayland");
+
+ println!("cargo:rustc-link-lib=dylib=stdc++");
+
+ pkg_config::Config::new()
+ .atleast_version("1")
+ .probe("egl")
+ .unwrap();
+ pkg_config::Config::new()
+ .atleast_version("1")
+ .probe("gl")
+ .unwrap();
+ pkg_config::Config::new()
+ .atleast_version("1")
+ .probe("wayland-client")
+ .unwrap();
+ pkg_config::Config::new()
+ .atleast_version("1")
+ .probe("wayland-egl")
+ .unwrap();
+
+ println!("cargo:rerun-if-changed=src/lib.rs");
+ println!("cargo:rerun-if-changed=src/lib.cpp");
+}
diff --git a/gfx/wr/example-compositor/compositor-wayland/src/lib.cpp b/gfx/wr/example-compositor/compositor-wayland/src/lib.cpp
new file mode 100644
index 0000000000..5529c987a9
--- /dev/null
+++ b/gfx/wr/example-compositor/compositor-wayland/src/lib.cpp
@@ -0,0 +1,772 @@
+/* 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/. */
+
+#define UNICODE
+
+#include <algorithm>
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <map>
+#include <math.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <unistd.h>
+#include <unordered_map>
+#include <vector>
+
+#include <wayland-client.h>
+#include <wayland-egl.h>
+
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+#include <GL/gl.h>
+#include <GLES2/gl2.h>
+
+#include "viewporter-client-protocol.h"
+#include "xdg-shell-client-protocol.h"
+
+#define UNUSED(x) (void)(x)
+
+#define MIN(x, y) (((x) < (y)) ? (x) : (y))
+#define MAX(x, y) (((x) > (y)) ? (x) : (y))
+
+#define NUM_QUERIES 2
+
+#define VIRTUAL_OFFSET 512 * 1024
+
+enum SyncMode {
+ None_ = 0,
+ Swap = 1,
+ Commit = 2,
+ Flush = 3,
+ Query = 4,
+};
+
+// The OS compositor representation of a picture cache tile.
+struct Tile {
+ uint64_t surface_id;
+ int x;
+ int y;
+
+ struct wl_surface* surface;
+ struct wl_subsurface* subsurface;
+ struct wp_viewport* viewport;
+ struct wl_egl_window* egl_window;
+ EGLSurface egl_surface;
+ bool is_visible;
+
+ std::vector<EGLint> damage_rects;
+};
+
+struct TileKey {
+ int x;
+ int y;
+
+ TileKey(int ax, int ay) : x(ax), y(ay) {}
+};
+
+bool operator==(const TileKey& k0, const TileKey& k1) {
+ return k0.x == k1.x && k0.y == k1.y;
+}
+
+struct TileKeyHasher {
+ size_t operator()(const TileKey& key) const { return key.x ^ key.y; }
+};
+
+struct Surface {
+ uint64_t id;
+ int tile_width;
+ int tile_height;
+ bool is_opaque;
+ std::unordered_map<TileKey, Tile*, TileKeyHasher> tiles;
+};
+
+struct WLDisplay {
+ struct wl_display* display;
+ struct wl_registry* registry;
+ struct wl_compositor* compositor;
+ struct wl_subcompositor* subcompositor;
+ struct xdg_wm_base* wm_base;
+ struct wl_seat* seat;
+ struct wl_pointer* pointer;
+ struct wl_touch* touch;
+ struct wl_keyboard* keyboard;
+ struct wl_shm* shm;
+ struct wl_cursor_theme* cursor_theme;
+ struct wl_cursor* default_cursor;
+ struct wl_surface* cursor_surface;
+ struct wp_viewporter* viewporter;
+
+ PFNEGLSWAPBUFFERSWITHDAMAGEEXTPROC swap_buffers_with_damage;
+};
+
+struct WLGeometry {
+ int width, height;
+};
+
+struct WLWindow {
+ WLGeometry geometry;
+ bool enable_compositor;
+ SyncMode sync_mode;
+ bool closed;
+
+ WLDisplay* display;
+ struct wl_surface* surface;
+ struct xdg_surface* xdg_surface;
+ struct xdg_toplevel* xdg_toplevel;
+ struct wl_callback* callback;
+ struct wp_viewport* viewport;
+ bool wait_for_configure;
+
+ struct wl_egl_window* egl_window;
+ EGLSurface egl_surface;
+
+ EGLDeviceEXT eglDevice;
+ EGLDisplay eglDisplay;
+ EGLContext eglContext;
+ EGLConfig config;
+
+ // Maintain list of layer state between frames to avoid visual tree rebuild.
+ std::vector<uint64_t> currentLayers;
+ std::vector<uint64_t> prevLayers;
+
+ // Maps WR surface IDs to each OS surface
+ std::unordered_map<uint64_t, Surface> surfaces;
+ std::vector<Tile*> destroyedTiles;
+ std::vector<Tile*> hiddenTiles;
+};
+
+extern "C" {
+
+static void init_wl_registry(WLWindow* window);
+static void init_xdg_window(WLWindow* window);
+
+WLWindow* com_wl_create_window(int width, int height, bool enable_compositor,
+ SyncMode sync_mode) {
+ WLDisplay* display = new WLDisplay;
+ WLWindow* window = new WLWindow;
+
+ window->display = display;
+ window->geometry.width = width;
+ window->geometry.height = height;
+ window->enable_compositor = enable_compositor;
+ window->sync_mode = sync_mode;
+ window->closed = false;
+
+ display->display = wl_display_connect(NULL);
+ assert(display->display);
+
+ init_wl_registry(window);
+ if (enable_compositor && !display->viewporter) {
+ fprintf(stderr, "Native compositor mode requires wp_viewporter support\n");
+ window->closed = true;
+ }
+
+ window->eglDisplay =
+ eglGetPlatformDisplay(EGL_PLATFORM_WAYLAND_KHR, display->display, NULL);
+
+ eglInitialize(window->eglDisplay, nullptr, nullptr);
+ eglBindAPI(EGL_OPENGL_API);
+
+ EGLint num_configs = 0;
+ EGLint cfg_attribs[] = {EGL_SURFACE_TYPE,
+ EGL_WINDOW_BIT,
+ EGL_RENDERABLE_TYPE,
+ EGL_OPENGL_BIT,
+ EGL_RED_SIZE,
+ 8,
+ EGL_GREEN_SIZE,
+ 8,
+ EGL_BLUE_SIZE,
+ 8,
+ EGL_ALPHA_SIZE,
+ 8,
+ EGL_DEPTH_SIZE,
+ 24,
+ EGL_NONE};
+ EGLConfig configs[32];
+
+ eglChooseConfig(window->eglDisplay, cfg_attribs, configs,
+ sizeof(configs) / sizeof(EGLConfig), &num_configs);
+ assert(num_configs > 0);
+ window->config = configs[0];
+
+ EGLint ctx_attribs[] = {EGL_CONTEXT_OPENGL_PROFILE_MASK,
+ EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
+ EGL_CONTEXT_MAJOR_VERSION,
+ 3,
+ EGL_CONTEXT_MINOR_VERSION,
+ 2,
+ EGL_NONE};
+
+ // Create an EGL context that can be used for drawing
+ window->eglContext = eglCreateContext(window->eglDisplay, window->config,
+ EGL_NO_CONTEXT, ctx_attribs);
+
+ window->surface = wl_compositor_create_surface(display->compositor);
+ init_xdg_window(window);
+
+ struct wl_region* region =
+ wl_compositor_create_region(window->display->compositor);
+ wl_region_add(region, 0, 0, INT32_MAX, INT32_MAX);
+ wl_surface_set_opaque_region(window->surface, region);
+ wl_region_destroy(region);
+
+ if (enable_compositor) {
+ xdg_toplevel_set_title(window->xdg_toplevel,
+ "example-compositor (Wayland)");
+ } else {
+ xdg_toplevel_set_title(window->xdg_toplevel, "example-compositor (Simple)");
+ }
+
+ window->wait_for_configure = true;
+ wl_surface_commit(window->surface);
+
+ EGLBoolean ok = eglMakeCurrent(window->eglDisplay, EGL_NO_SURFACE,
+ EGL_NO_SURFACE, window->eglContext);
+ assert(ok);
+
+ display->swap_buffers_with_damage =
+ (PFNEGLSWAPBUFFERSWITHDAMAGEEXTPROC)eglGetProcAddress(
+ "eglSwapBuffersWithDamageKHR");
+
+ return window;
+}
+
+bool com_wl_tick(WLWindow* window) {
+ if (window->wait_for_configure) {
+ int ret = 0;
+ while (window->wait_for_configure && !window->closed && ret != -1) {
+ wl_display_dispatch(window->display->display);
+ }
+ } else {
+ wl_display_dispatch_pending(window->display->display);
+ }
+
+ return !window->closed;
+}
+
+static void unmap_hidden_tiles(WLWindow* window) {
+ for (Tile* tile : window->hiddenTiles) {
+ if (tile->subsurface) {
+ wl_subsurface_destroy(tile->subsurface);
+ tile->subsurface = nullptr;
+ }
+ }
+ window->hiddenTiles.clear();
+}
+
+static void clean_up_tiles(WLWindow* window) {
+ for (Tile* tile : window->destroyedTiles) {
+ eglDestroySurface(window->eglDisplay, tile->egl_surface);
+ wl_egl_window_destroy(tile->egl_window);
+ wp_viewport_destroy(tile->viewport);
+ wl_surface_destroy(tile->surface);
+ delete tile;
+ }
+ window->destroyedTiles.clear();
+}
+
+static void handle_callback(void* data, struct wl_callback* callback,
+ uint32_t time) {
+ WLWindow* window = (WLWindow*)data;
+ UNUSED(time);
+
+ assert(window->callback == callback);
+
+ wl_callback_destroy(callback);
+ window->callback = nullptr;
+}
+
+static const struct wl_callback_listener frame_listener = {handle_callback};
+
+void com_wl_swap_buffers(WLWindow* window) {
+ if (window->enable_compositor) {
+ for (auto surface_it = window->surfaces.begin();
+ surface_it != window->surfaces.end(); ++surface_it) {
+ Surface* surface = &surface_it->second;
+
+ for (auto tile_it = surface->tiles.begin();
+ tile_it != surface->tiles.end(); ++tile_it) {
+ Tile* tile = tile_it->second;
+
+ if (!tile->damage_rects.empty() && tile->is_visible) {
+ eglMakeCurrent(window->eglDisplay, tile->egl_surface,
+ tile->egl_surface, window->eglContext);
+ eglSwapInterval(window->eglDisplay, 0);
+
+ /* if (window->display->swap_buffers_with_damage) {
+ window->display->swap_buffers_with_damage(
+ window->eglDisplay, tile->egl_surface,
+ tile->damage_rects.data(), tile->damage_rects.size() / 4);
+ } else */
+ eglSwapBuffers(window->eglDisplay, tile->egl_surface);
+ tile->damage_rects.clear();
+
+ eglMakeCurrent(window->eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE,
+ window->eglContext);
+ } else {
+ wl_surface_commit(tile->surface);
+ }
+ }
+ }
+ wl_surface_commit(window->surface);
+ unmap_hidden_tiles(window);
+ clean_up_tiles(window);
+
+ int ret = 0;
+ switch (window->sync_mode) {
+ case SyncMode::None_:
+ wl_display_roundtrip(window->display->display);
+ break;
+ case SyncMode::Swap:
+ window->callback = wl_surface_frame(window->surface);
+ wl_callback_add_listener(window->callback, &frame_listener, window);
+ wl_surface_commit(window->surface);
+
+ while (window->callback && !window->closed && ret != -1) {
+ ret = wl_display_dispatch(window->display->display);
+ }
+ break;
+ default:
+ assert(false);
+ break;
+ }
+ } else {
+ // If not using native mode, then do a normal EGL swap buffers.
+ switch (window->sync_mode) {
+ case SyncMode::None_:
+ eglSwapInterval(window->eglDisplay, 0);
+ break;
+ case SyncMode::Swap:
+ eglSwapInterval(window->eglDisplay, 1);
+ break;
+ default:
+ assert(false);
+ break;
+ }
+ eglSwapBuffers(window->eglDisplay, window->egl_surface);
+ }
+}
+
+// Create a new native surface
+void com_wl_create_surface(WLWindow* window, uint64_t surface_id,
+ int tile_width, int tile_height, bool is_opaque) {
+ assert(window->surfaces.count(surface_id) == 0);
+
+ Surface surface;
+ surface.id = surface_id;
+ surface.tile_width = tile_width;
+ surface.tile_height = tile_height;
+ surface.is_opaque = is_opaque;
+
+ window->surfaces.emplace(surface_id, surface);
+}
+
+void com_wl_create_tile(WLWindow* window, uint64_t surface_id, int x, int y) {
+ WLDisplay* display = window->display;
+
+ assert(window->surfaces.count(surface_id) == 1);
+ Surface* surface = &window->surfaces.at(surface_id);
+
+ TileKey key(x, y);
+ assert(surface->tiles.count(key) == 0);
+
+ Tile* tile = new Tile;
+ tile->surface_id = surface_id;
+ tile->x = x;
+ tile->y = y;
+ tile->is_visible = false;
+
+ tile->surface = wl_compositor_create_surface(display->compositor);
+ tile->viewport =
+ wp_viewporter_get_viewport(display->viewporter, tile->surface);
+
+ if (surface->is_opaque) {
+ struct wl_region* region =
+ wl_compositor_create_region(window->display->compositor);
+ wl_region_add(region, 0, 0, INT32_MAX, INT32_MAX);
+ wl_surface_set_opaque_region(tile->surface, region);
+ wl_region_destroy(region);
+ }
+
+ tile->egl_window = wl_egl_window_create(tile->surface, surface->tile_width,
+ surface->tile_height);
+ tile->egl_surface = eglCreateWindowSurface(window->eglDisplay, window->config,
+ tile->egl_window, NULL);
+ assert(tile->egl_surface != EGL_NO_SURFACE);
+
+ surface->tiles.emplace(key, tile);
+}
+
+static void show_tile(WLWindow* window, Tile* tile) {
+ if (tile->is_visible) {
+ assert(tile->subsurface);
+ return;
+ }
+
+ tile->subsurface = wl_subcompositor_get_subsurface(
+ window->display->subcompositor, tile->surface, window->surface);
+
+ /* This is not comprehensive yet, see hide_tile() */
+ Surface* surface = &window->surfaces.at(tile->surface_id);
+ for (auto tile_it = surface->tiles.begin(); tile_it != surface->tiles.end();
+ ++tile_it) {
+ Tile* other_tile = tile_it->second;
+
+ if (other_tile->is_visible) {
+ wl_subsurface_place_above(tile->subsurface, other_tile->surface);
+ }
+ }
+
+ tile->is_visible = true;
+}
+
+static void hide_tile(WLWindow* window, Tile* tile) {
+ if (!tile->is_visible) {
+ return;
+ }
+
+ /*
+ * This is a workaround for missing API on the egl-wayland platform. We
+ * likely want to replace it a solution that detaches the buffer from
+ * the surface, which would require us to manage buffers manually.
+ */
+ wl_subsurface_set_position(tile->subsurface, window->geometry.width / 2,
+ window->geometry.height / 2);
+ wp_viewport_set_source(tile->viewport, wl_fixed_from_int(0),
+ wl_fixed_from_int(0), wl_fixed_from_int(1),
+ wl_fixed_from_int(1));
+ wl_subsurface_place_below(tile->subsurface, window->surface);
+ tile->is_visible = false;
+ window->hiddenTiles.push_back(tile);
+}
+
+void com_wl_destroy_tile(WLWindow* window, uint64_t surface_id, int x, int y) {
+ assert(window->surfaces.count(surface_id) == 1);
+
+ Surface* surface = &window->surfaces.at(surface_id);
+ TileKey key(x, y);
+ assert(surface->tiles.count(key) == 1);
+ Tile* tile = surface->tiles[key];
+
+ hide_tile(window, tile);
+ wl_surface_commit(tile->surface);
+
+ window->destroyedTiles.push_back(tile);
+ surface->tiles.erase(key);
+}
+
+void com_wl_destroy_surface(WLWindow* window, uint64_t surface_id) {
+ assert(window->surfaces.count(surface_id) == 1);
+
+ Surface* surface = &window->surfaces.at(surface_id);
+ for (auto tile_it = surface->tiles.begin(); tile_it != surface->tiles.end();
+ tile_it = surface->tiles.begin()) {
+ Tile* tile = tile_it->second;
+
+ com_wl_destroy_tile(window, surface_id, tile->x, tile->y);
+ }
+
+ window->surfaces.erase(surface_id);
+}
+
+void com_wl_destroy_window(WLWindow* window) {
+ for (auto surface_it = window->surfaces.begin();
+ surface_it != window->surfaces.end(); ++surface_it) {
+ Surface& surface = surface_it->second;
+
+ com_wl_destroy_surface(window, surface.id);
+ }
+
+ if (window->egl_surface != EGL_NO_SURFACE) {
+ eglDestroySurface(window->eglDisplay, window->egl_surface);
+ }
+ eglDestroyContext(window->eglDisplay, window->eglContext);
+ eglTerminate(window->eglDisplay);
+
+ delete window;
+}
+
+// Bind a native surface to allow issuing GL commands to it
+GLuint com_wl_bind_surface(WLWindow* window, uint64_t surface_id, int tile_x,
+ int tile_y, int* x_offset, int* y_offset,
+ int dirty_x0, int dirty_y0, int dirty_width,
+ int dirty_height) {
+ *x_offset = 0;
+ *y_offset = 0;
+
+ assert(window->surfaces.count(surface_id) == 1);
+ Surface* surface = &window->surfaces[surface_id];
+
+ TileKey key(tile_x, tile_y);
+ assert(surface->tiles.count(key) == 1);
+ Tile* tile = surface->tiles[key];
+
+ tile->damage_rects.push_back(dirty_x0);
+ tile->damage_rects.push_back(dirty_y0);
+ tile->damage_rects.push_back(dirty_width);
+ tile->damage_rects.push_back(dirty_height);
+
+ EGLBoolean ok = eglMakeCurrent(window->eglDisplay, tile->egl_surface,
+ tile->egl_surface, window->eglContext);
+ assert(ok);
+
+ return 0;
+}
+
+// Unbind a currently bound native surface
+void com_wl_unbind_surface(WLWindow* window) {
+ eglMakeCurrent(window->eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE,
+ window->eglContext);
+}
+
+void com_wl_begin_transaction(WLWindow*) {}
+
+// Add a native surface to the visual tree. Called per-frame to build the
+// composition.
+void com_wl_add_surface(WLWindow* window, uint64_t surface_id, int offset_x,
+ int offset_y, int clip_x, int clip_y, int clip_w,
+ int clip_h) {
+ Surface* surface = &window->surfaces[surface_id];
+ window->currentLayers.push_back(surface_id);
+
+ for (auto tile_it = surface->tiles.begin(); tile_it != surface->tiles.end();
+ ++tile_it) {
+ Tile* tile = tile_it->second;
+
+ int pos_x = MAX((tile->x * surface->tile_width) + offset_x, clip_x);
+ int pos_y = MAX((tile->y * surface->tile_height) + offset_y, clip_y);
+
+ float view_x = MAX((clip_x - offset_x) - tile->x * surface->tile_width, 0);
+ float view_y = MAX((clip_y - offset_y) - tile->y * surface->tile_height, 0);
+
+ float view_w = MIN(surface->tile_width - view_x, (clip_x + clip_w) - pos_x);
+ float view_h =
+ MIN(surface->tile_height - view_y, (clip_y + clip_h) - pos_y);
+ view_w = MIN(window->geometry.width - pos_x, view_w);
+ view_h = MIN(window->geometry.height - pos_y, view_h);
+
+ if (view_w > 0 && view_h > 0) {
+ show_tile(window, tile);
+
+ wl_surface_set_buffer_transform(tile->surface,
+ WL_OUTPUT_TRANSFORM_FLIPPED_180);
+ wl_subsurface_set_position(tile->subsurface, pos_x, pos_y);
+ wp_viewport_set_source(tile->viewport, wl_fixed_from_double(view_x),
+ wl_fixed_from_double(view_y),
+ wl_fixed_from_double(view_w),
+ wl_fixed_from_double(view_h));
+ } else {
+ hide_tile(window, tile);
+ }
+ }
+}
+
+void com_wl_end_transaction(WLWindow* window) {
+ bool same = window->prevLayers == window->currentLayers;
+ if (!same) {
+ struct wl_surface* prev_surface = window->surface;
+
+ for (auto it = window->currentLayers.begin();
+ it != window->currentLayers.end(); ++it) {
+ Surface* surface = &window->surfaces[*it];
+
+ struct wl_surface* next_surface = nullptr;
+ for (auto tile_it = surface->tiles.begin();
+ tile_it != surface->tiles.end(); ++tile_it) {
+ Tile* tile = tile_it->second;
+
+ if (tile->is_visible) {
+ wl_subsurface_place_above(tile->subsurface, prev_surface);
+
+ if (!next_surface) {
+ next_surface = tile->surface;
+ }
+ }
+ }
+ prev_surface = next_surface;
+ }
+ }
+
+ window->prevLayers.swap(window->currentLayers);
+ window->currentLayers.clear();
+}
+
+void glInvalidateFramebuffer(GLenum target, GLsizei numAttachments,
+ const GLenum* attachments) {
+ UNUSED(target);
+ UNUSED(numAttachments);
+ UNUSED(attachments);
+}
+
+// Get a pointer to an EGL symbol
+void* com_wl_get_proc_address(const char* name) {
+ /* Disable glInvalidateFramebuffer for now as it triggers errors.
+ * This is likely due to the egl-wayland platform, which we may want to
+ * replace with a custom implementation in order to have more control
+ * over the low-lever bits.
+ */
+ if (strcmp(name, "glInvalidateFramebuffer") == 0) {
+ return (void*)glInvalidateFramebuffer;
+ }
+
+ return (void*)eglGetProcAddress(name);
+}
+
+void com_wl_deinit(WLWindow* window) { UNUSED(window); }
+
+static void handle_xdg_surface_configure(void* data,
+ struct xdg_surface* surface,
+ uint32_t serial) {
+ WLWindow* window = (WLWindow*)data;
+
+ xdg_surface_ack_configure(surface, serial);
+
+ if (window->wait_for_configure) {
+ if (window->enable_compositor) {
+ int width = window->geometry.width;
+ int height = window->geometry.height;
+
+ window->egl_window = wl_egl_window_create(window->surface, 1, 1);
+ window->egl_surface = eglCreateWindowSurface(
+ window->eglDisplay, window->config, window->egl_window, NULL);
+ assert(window->egl_surface != EGL_NO_SURFACE);
+
+ EGLBoolean ok = eglMakeCurrent(window->eglDisplay, window->egl_surface,
+ window->egl_surface, window->eglContext);
+ assert(ok);
+
+ glClearColor(1.0, 1.0, 1.0, 1.0);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ window->viewport = wp_viewporter_get_viewport(window->display->viewporter,
+ window->surface);
+ wp_viewport_set_destination(window->viewport, width, height);
+
+ eglSwapBuffers(window->eglDisplay, window->egl_surface);
+ } else {
+ window->egl_window = wl_egl_window_create(
+ window->surface, window->geometry.width, window->geometry.height);
+ window->egl_surface = eglCreateWindowSurface(
+ window->eglDisplay, window->config, window->egl_window, NULL);
+ assert(window->egl_surface != EGL_NO_SURFACE);
+
+ EGLBoolean ok = eglMakeCurrent(window->eglDisplay, window->egl_surface,
+ window->egl_surface, window->eglContext);
+ assert(ok);
+ }
+ }
+
+ window->wait_for_configure = false;
+}
+
+static const struct xdg_surface_listener xdg_surface_listener = {
+ handle_xdg_surface_configure};
+
+static void handle_xdg_toplevel_configure(void* data,
+ struct xdg_toplevel* toplevel,
+ int32_t width, int32_t height,
+ struct wl_array* states) {
+ WLWindow* window = (WLWindow*)data;
+ UNUSED(toplevel);
+ UNUSED(states);
+
+ if (width > 0 && height > 0) {
+ window->geometry.width = width;
+ window->geometry.height = height;
+
+ if (!window->wait_for_configure) {
+ if (window->enable_compositor) {
+ wp_viewport_set_destination(window->viewport, window->geometry.width,
+ window->geometry.height);
+ } else {
+ wl_egl_window_resize(window->egl_window, window->geometry.width,
+ window->geometry.height, 0, 0);
+ }
+ }
+ }
+}
+
+static void handle_xdg_toplevel_close(void* data,
+ struct xdg_toplevel* toplevel) {
+ UNUSED(toplevel);
+ WLWindow* window = (WLWindow*)data;
+ window->closed = true;
+}
+
+static const struct xdg_toplevel_listener xdg_toplevel_listener = {
+ handle_xdg_toplevel_configure,
+ handle_xdg_toplevel_close,
+};
+
+static void xdg_wm_base_ping(void* data, struct xdg_wm_base* shell,
+ uint32_t serial) {
+ UNUSED(data);
+ xdg_wm_base_pong(shell, serial);
+}
+
+static const struct xdg_wm_base_listener wm_base_listener = {
+ xdg_wm_base_ping,
+};
+
+static void registry_handle_global(void* data, struct wl_registry* registry,
+ uint32_t name, const char* interface,
+ uint32_t version) {
+ WLDisplay* d = (WLDisplay*)data;
+
+ if (strcmp(interface, "wl_compositor") == 0) {
+ d->compositor = (struct wl_compositor*)wl_registry_bind(
+ registry, name, &wl_compositor_interface, MIN(version, 4));
+ } else if (strcmp(interface, "wp_viewporter") == 0) {
+ d->viewporter = (struct wp_viewporter*)wl_registry_bind(
+ registry, name, &wp_viewporter_interface, 1);
+ } else if (strcmp(interface, "xdg_wm_base") == 0) {
+ d->wm_base = (struct xdg_wm_base*)wl_registry_bind(
+ registry, name, &xdg_wm_base_interface, 1);
+ xdg_wm_base_add_listener(d->wm_base, &wm_base_listener, NULL);
+ } else if (strcmp(interface, "wl_subcompositor") == 0) {
+ d->subcompositor = (struct wl_subcompositor*)wl_registry_bind(
+ registry, name, &wl_subcompositor_interface, 1);
+ }
+}
+
+static void registry_handle_global_remove(void* data,
+ struct wl_registry* registry,
+ uint32_t name) {
+ UNUSED(data);
+ UNUSED(registry);
+ UNUSED(name);
+}
+
+static const struct wl_registry_listener registry_listener = {
+ registry_handle_global, registry_handle_global_remove};
+
+static void init_wl_registry(WLWindow* window) {
+ WLDisplay* display = window->display;
+
+ display->registry = wl_display_get_registry(display->display);
+ wl_registry_add_listener(display->registry, &registry_listener, display);
+
+ wl_display_roundtrip(display->display);
+
+ assert(display->compositor);
+ assert(display->wm_base);
+ assert(display->subcompositor);
+}
+
+static void init_xdg_window(WLWindow* window) {
+ window->xdg_surface =
+ xdg_wm_base_get_xdg_surface(window->display->wm_base, window->surface);
+ assert(window->xdg_surface);
+ xdg_surface_add_listener(window->xdg_surface, &xdg_surface_listener, window);
+
+ window->xdg_toplevel = xdg_surface_get_toplevel(window->xdg_surface);
+ xdg_toplevel_add_listener(window->xdg_toplevel, &xdg_toplevel_listener,
+ window);
+ assert(window->xdg_toplevel);
+}
+}
diff --git a/gfx/wr/example-compositor/compositor-wayland/src/lib.rs b/gfx/wr/example-compositor/compositor-wayland/src/lib.rs
new file mode 100644
index 0000000000..daddbb6495
--- /dev/null
+++ b/gfx/wr/example-compositor/compositor-wayland/src/lib.rs
@@ -0,0 +1,269 @@
+/* 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::os::raw::{c_void, c_char};
+
+/*
+
+ This is a very simple (and unsafe!) rust wrapper for the Wayland / EGL
+ implementation in lib.cpp.
+
+ It just proxies the calls from the Compositor impl to the C99 code. This is very
+ hacky and not suitable for production!
+
+ */
+
+// Opaque wrapper for the Window type in lib.cpp
+#[repr(C)]
+pub struct Window {
+ _unused: [u8; 0]
+}
+
+// C99 functions that do the compositor work
+extern {
+ fn com_wl_create_window(
+ width: i32,
+ height: i32,
+ enable_compositor: bool,
+ sync_mode: i32,
+ ) -> *mut Window;
+ fn com_wl_destroy_window(window: *mut Window);
+ fn com_wl_tick(window: *mut Window) -> bool;
+ fn com_wl_get_proc_address(name: *const c_char) -> *const c_void;
+ fn com_wl_swap_buffers(window: *mut Window);
+
+ fn com_wl_create_surface(
+ window: *mut Window,
+ id: u64,
+ tile_width: i32,
+ tile_height: i32,
+ is_opaque: bool,
+ );
+
+ fn com_wl_create_tile(
+ window: *mut Window,
+ id: u64,
+ x: i32,
+ y: i32,
+ );
+
+ fn com_wl_destroy_tile(
+ window: *mut Window,
+ id: u64,
+ x: i32,
+ y: i32,
+ );
+
+ fn com_wl_destroy_surface(
+ window: *mut Window,
+ id: u64,
+ );
+
+ fn com_wl_bind_surface(
+ window: *mut Window,
+ surface_id: u64,
+ tile_x: i32,
+ tile_y: i32,
+ x_offset: &mut i32,
+ y_offset: &mut i32,
+ dirty_x0: i32,
+ dirty_y0: i32,
+ dirty_width: i32,
+ dirty_height: i32,
+ ) -> u32;
+ fn com_wl_unbind_surface(window: *mut Window);
+
+ fn com_wl_begin_transaction(window: *mut Window);
+
+ fn com_wl_add_surface(
+ window: *mut Window,
+ id: u64,
+ x: i32,
+ y: i32,
+ clip_x: i32,
+ clip_y: i32,
+ clip_w: i32,
+ clip_h: i32,
+ );
+
+ fn com_wl_end_transaction(window: *mut Window);
+
+ fn com_wl_deinit(window: *mut Window);
+}
+
+pub fn create_window(
+ width: i32,
+ height: i32,
+ enable_compositor: bool,
+ sync_mode: i32,
+) -> *mut Window {
+ unsafe {
+ com_wl_create_window(width, height, enable_compositor, sync_mode)
+ }
+}
+
+pub fn destroy_window(window: *mut Window) {
+ unsafe {
+ com_wl_destroy_window(window);
+ }
+}
+
+pub fn tick(window: *mut Window) -> bool {
+ unsafe {
+ com_wl_tick(window)
+ }
+}
+
+pub fn get_proc_address(name: *const c_char) -> *const c_void {
+ unsafe {
+ com_wl_get_proc_address(name)
+ }
+}
+
+pub fn create_surface(
+ window: *mut Window,
+ id: u64,
+ tile_width: i32,
+ tile_height: i32,
+ is_opaque: bool,
+) {
+ unsafe {
+ com_wl_create_surface(
+ window,
+ id,
+ tile_width,
+ tile_height,
+ is_opaque,
+ )
+ }
+}
+
+pub fn create_tile(
+ window: *mut Window,
+ id: u64,
+ x: i32,
+ y: i32,
+) {
+ unsafe {
+ com_wl_create_tile(
+ window,
+ id,
+ x,
+ y,
+ )
+ }
+}
+
+pub fn destroy_tile(
+ window: *mut Window,
+ id: u64,
+ x: i32,
+ y: i32,
+) {
+ unsafe {
+ com_wl_destroy_tile(
+ window,
+ id,
+ x,
+ y,
+ )
+ }
+}
+
+pub fn destroy_surface(
+ window: *mut Window,
+ id: u64,
+) {
+ unsafe {
+ com_wl_destroy_surface(
+ window,
+ id,
+ )
+ }
+}
+
+pub fn bind_surface(
+ window: *mut Window,
+ surface_id: u64,
+ tile_x: i32,
+ tile_y: i32,
+ dirty_x0: i32,
+ dirty_y0: i32,
+ dirty_width: i32,
+ dirty_height: i32,
+) -> (u32, i32, i32) {
+ unsafe {
+ let mut x_offset = 0;
+ let mut y_offset = 0;
+
+ let fbo_id = com_wl_bind_surface(
+ window,
+ surface_id,
+ tile_x,
+ tile_y,
+ &mut x_offset,
+ &mut y_offset,
+ dirty_x0,
+ dirty_y0,
+ dirty_width,
+ dirty_height,
+ );
+
+ (fbo_id, x_offset, y_offset)
+ }
+}
+
+pub fn add_surface(
+ window: *mut Window,
+ id: u64,
+ x: i32,
+ y: i32,
+ clip_x: i32,
+ clip_y: i32,
+ clip_w: i32,
+ clip_h: i32,
+) {
+ unsafe {
+ com_wl_add_surface(
+ window,
+ id,
+ x,
+ y,
+ clip_x,
+ clip_y,
+ clip_w,
+ clip_h,
+ )
+ }
+}
+
+pub fn begin_transaction(window: *mut Window) {
+ unsafe {
+ com_wl_begin_transaction(window)
+ }
+}
+
+pub fn unbind_surface(window: *mut Window) {
+ unsafe {
+ com_wl_unbind_surface(window)
+ }
+}
+
+pub fn end_transaction(window: *mut Window) {
+ unsafe {
+ com_wl_end_transaction(window)
+ }
+}
+
+pub fn swap_buffers(window: *mut Window) {
+ unsafe {
+ com_wl_swap_buffers(window);
+ }
+}
+
+pub fn deinit(window: *mut Window) {
+ unsafe {
+ com_wl_deinit(window);
+ }
+}
diff --git a/gfx/wr/example-compositor/compositor-windows/Cargo.toml b/gfx/wr/example-compositor/compositor-windows/Cargo.toml
new file mode 100644
index 0000000000..b191626a69
--- /dev/null
+++ b/gfx/wr/example-compositor/compositor-windows/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "compositor-windows"
+version = "0.1.0"
+authors = ["Glenn Watson <gw@intuitionlibrary.com>"]
+edition = "2018"
+license = "MPL-2.0"
+
+[build-dependencies]
+cc = "1.0"
diff --git a/gfx/wr/example-compositor/compositor-windows/build.rs b/gfx/wr/example-compositor/compositor-windows/build.rs
new file mode 100644
index 0000000000..dc7358aa6a
--- /dev/null
+++ b/gfx/wr/example-compositor/compositor-windows/build.rs
@@ -0,0 +1,25 @@
+/* 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/. */
+
+fn main() {
+ // HACK - This build script relies on Gecko having been built, so that the ANGLE libraries
+ // have already been compiled. It also assumes they are being built with an in-tree
+ // x86_64 object directory.
+
+ cc::Build::new()
+ .file("src/lib.cpp")
+ .include("../../../angle/checkout/include")
+ .compile("windows");
+
+ // Set up linker paths for ANGLE that is built by Gecko
+ println!("cargo:rustc-link-search=../../obj-x86_64-pc-mingw32/gfx/angle/targets/libEGL");
+ println!("cargo:rustc-link-search=../../obj-x86_64-pc-mingw32/gfx/angle/targets/libGLESv2");
+
+ // Link to libEGL and libGLESv2 (ANGLE) and D3D11 + DirectComposition
+ println!("cargo:rustc-link-lib=libEGL");
+ println!("cargo:rustc-link-lib=libGLESv2");
+ println!("cargo:rustc-link-lib=dcomp");
+ println!("cargo:rustc-link-lib=d3d11");
+ println!("cargo:rustc-link-lib=dwmapi");
+}
diff --git a/gfx/wr/example-compositor/compositor-windows/src/lib.cpp b/gfx/wr/example-compositor/compositor-windows/src/lib.cpp
new file mode 100644
index 0000000000..d39726ea0a
--- /dev/null
+++ b/gfx/wr/example-compositor/compositor-windows/src/lib.cpp
@@ -0,0 +1,694 @@
+/* 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/. */
+
+#define UNICODE
+
+#include <windows.h>
+#include <math.h>
+#include <dcomp.h>
+#include <d3d11.h>
+#include <assert.h>
+#include <map>
+#include <vector>
+#include <dwmapi.h>
+#include <unordered_map>
+
+#define EGL_EGL_PROTOTYPES 1
+#define EGL_EGLEXT_PROTOTYPES 1
+#define GL_GLEXT_PROTOTYPES 1
+#include "EGL/egl.h"
+#include "EGL/eglext.h"
+#include "EGL/eglext_angle.h"
+#include "GL/gl.h"
+#include "GLES/gl.h"
+#include "GLES/glext.h"
+#include "GLES3/gl3.h"
+
+#define NUM_QUERIES 2
+
+#define USE_VIRTUAL_SURFACES
+#define VIRTUAL_OFFSET 512 * 1024
+
+enum SyncMode {
+ None = 0,
+ Swap = 1,
+ Commit = 2,
+ Flush = 3,
+ Query = 4,
+};
+
+// The OS compositor representation of a picture cache tile.
+struct Tile {
+#ifndef USE_VIRTUAL_SURFACES
+ // Represents the underlying DirectComposition surface texture that gets drawn
+ // into.
+ IDCompositionSurface* pSurface;
+ // Represents the node in the visual tree that defines the properties of this
+ // tile (clip, position etc).
+ IDCompositionVisual2* pVisual;
+#endif
+};
+
+struct TileKey {
+ int x;
+ int y;
+
+ TileKey(int ax, int ay) : x(ax), y(ay) {}
+};
+
+bool operator==(const TileKey& k0, const TileKey& k1) {
+ return k0.x == k1.x && k0.y == k1.y;
+}
+
+struct TileKeyHasher {
+ size_t operator()(const TileKey& key) const { return key.x ^ key.y; }
+};
+
+struct Surface {
+ int tile_width;
+ int tile_height;
+ bool is_opaque;
+ std::unordered_map<TileKey, Tile, TileKeyHasher> tiles;
+ IDCompositionVisual2* pVisual;
+#ifdef USE_VIRTUAL_SURFACES
+ IDCompositionVirtualSurface* pVirtualSurface;
+#endif
+};
+
+struct CachedFrameBuffer {
+ int width;
+ int height;
+ GLuint fboId;
+ GLuint depthRboId;
+};
+
+struct Window {
+ // Win32 window details
+ HWND hWnd;
+ HINSTANCE hInstance;
+ bool enable_compositor;
+ RECT client_rect;
+ SyncMode sync_mode;
+
+ // Main interfaces to D3D11 and DirectComposition
+ ID3D11Device* pD3D11Device;
+ IDCompositionDesktopDevice* pDCompDevice;
+ IDCompositionTarget* pDCompTarget;
+ IDXGIDevice* pDXGIDevice;
+ ID3D11Query* pQueries[NUM_QUERIES];
+ int current_query;
+
+ // ANGLE interfaces that wrap the D3D device
+ EGLDeviceEXT EGLDevice;
+ EGLDisplay EGLDisplay;
+ EGLContext EGLContext;
+ EGLConfig config;
+ // Framebuffer surface for debug mode when we are not using DC
+ EGLSurface fb_surface;
+
+ // The currently bound surface, valid during bind() and unbind()
+ IDCompositionSurface* pCurrentSurface;
+ EGLImage mEGLImage;
+ GLuint mColorRBO;
+
+ // The root of the DC visual tree. Nothing is drawn on this, but
+ // all child tiles are parented to here.
+ IDCompositionVisual2* pRoot;
+ IDCompositionVisualDebug* pVisualDebug;
+ std::vector<CachedFrameBuffer> mFrameBuffers;
+
+ // Maintain list of layer state between frames to avoid visual tree rebuild.
+ std::vector<uint64_t> mCurrentLayers;
+ std::vector<uint64_t> mPrevLayers;
+
+ // Maps WR surface IDs to each OS surface
+ std::unordered_map<uint64_t, Surface> surfaces;
+};
+
+static const wchar_t* CLASS_NAME = L"WR DirectComposite";
+
+static GLuint GetOrCreateFbo(Window* window, int aWidth, int aHeight) {
+ GLuint fboId = 0;
+
+ // Check if we have a cached FBO with matching dimensions
+ for (auto it = window->mFrameBuffers.begin();
+ it != window->mFrameBuffers.end(); ++it) {
+ if (it->width == aWidth && it->height == aHeight) {
+ fboId = it->fboId;
+ break;
+ }
+ }
+
+ // If not, create a new FBO with attached depth buffer
+ if (fboId == 0) {
+ // Create the depth buffer
+ GLuint depthRboId;
+ glGenRenderbuffers(1, &depthRboId);
+ glBindRenderbuffer(GL_RENDERBUFFER, depthRboId);
+ glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, aWidth,
+ aHeight);
+
+ // Create the framebuffer and attach the depth buffer to it
+ glGenFramebuffers(1, &fboId);
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fboId);
+ glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
+ GL_RENDERBUFFER, depthRboId);
+
+ // Store this in the cache for future calls.
+ CachedFrameBuffer frame_buffer_info;
+ frame_buffer_info.width = aWidth;
+ frame_buffer_info.height = aHeight;
+ frame_buffer_info.fboId = fboId;
+ frame_buffer_info.depthRboId = depthRboId;
+ window->mFrameBuffers.push_back(frame_buffer_info);
+ }
+
+ return fboId;
+}
+
+static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam,
+ LPARAM lParam) {
+ switch (message) {
+ case WM_DESTROY:
+ PostQuitMessage(0);
+ return 1;
+ }
+
+ return DefWindowProc(hwnd, message, wParam, lParam);
+}
+
+extern "C" {
+Window* com_dc_create_window(int width, int height, bool enable_compositor,
+ SyncMode sync_mode) {
+ // Create a simple Win32 window
+ Window* window = new Window;
+ window->hInstance = GetModuleHandle(NULL);
+ window->enable_compositor = enable_compositor;
+ window->mEGLImage = EGL_NO_IMAGE;
+ window->sync_mode = sync_mode;
+
+ WNDCLASSEX wcex = {sizeof(WNDCLASSEX)};
+ wcex.style = CS_HREDRAW | CS_VREDRAW;
+ wcex.lpfnWndProc = WndProc;
+ wcex.cbClsExtra = 0;
+ wcex.cbWndExtra = 0;
+ wcex.hInstance = window->hInstance;
+ wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
+ ;
+ wcex.lpszMenuName = nullptr;
+ wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
+ wcex.lpszClassName = CLASS_NAME;
+ RegisterClassEx(&wcex);
+
+ int dpiX = 0;
+ int dpiY = 0;
+ HDC hdc = GetDC(NULL);
+ if (hdc) {
+ dpiX = GetDeviceCaps(hdc, LOGPIXELSX);
+ dpiY = GetDeviceCaps(hdc, LOGPIXELSY);
+ ReleaseDC(NULL, hdc);
+ }
+
+ RECT window_rect = {0, 0, width, height};
+ AdjustWindowRect(&window_rect, WS_OVERLAPPEDWINDOW, FALSE);
+ UINT window_width = static_cast<UINT>(
+ ceil(float(window_rect.right - window_rect.left) * dpiX / 96.f));
+ UINT window_height = static_cast<UINT>(
+ ceil(float(window_rect.bottom - window_rect.top) * dpiY / 96.f));
+
+ LPCWSTR name;
+ DWORD style;
+ if (enable_compositor) {
+ name = L"example-compositor (DirectComposition)";
+ style = WS_EX_NOREDIRECTIONBITMAP;
+ } else {
+ name = L"example-compositor (Simple)";
+ style = 0;
+ }
+
+ window->hWnd =
+ CreateWindowEx(style, CLASS_NAME, name, WS_OVERLAPPEDWINDOW,
+ CW_USEDEFAULT, CW_USEDEFAULT, window_width, window_height,
+ NULL, NULL, window->hInstance, NULL);
+
+ ShowWindow(window->hWnd, SW_SHOWNORMAL);
+ UpdateWindow(window->hWnd);
+ GetClientRect(window->hWnd, &window->client_rect);
+
+ // Create a D3D11 device
+ D3D_FEATURE_LEVEL featureLevelSupported;
+ HRESULT hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, NULL,
+ D3D11_CREATE_DEVICE_BGRA_SUPPORT, NULL, 0,
+ D3D11_SDK_VERSION, &window->pD3D11Device,
+ &featureLevelSupported, nullptr);
+ assert(SUCCEEDED(hr));
+
+ D3D11_QUERY_DESC query_desc;
+ memset(&query_desc, 0, sizeof(query_desc));
+ query_desc.Query = D3D11_QUERY_EVENT;
+ for (int i = 0; i < NUM_QUERIES; ++i) {
+ hr = window->pD3D11Device->CreateQuery(&query_desc, &window->pQueries[i]);
+ assert(SUCCEEDED(hr));
+ }
+ window->current_query = 0;
+
+ hr = window->pD3D11Device->QueryInterface(&window->pDXGIDevice);
+ assert(SUCCEEDED(hr));
+
+ // Create a DirectComposition device
+ hr = DCompositionCreateDevice2(window->pDXGIDevice,
+ __uuidof(IDCompositionDesktopDevice),
+ (void**)&window->pDCompDevice);
+ assert(SUCCEEDED(hr));
+
+ // Create a DirectComposition target for a Win32 window handle
+ hr = window->pDCompDevice->CreateTargetForHwnd(window->hWnd, TRUE,
+ &window->pDCompTarget);
+ assert(SUCCEEDED(hr));
+
+ // Create an ANGLE EGL device that wraps D3D11
+ window->EGLDevice = eglCreateDeviceANGLE(EGL_D3D11_DEVICE_ANGLE,
+ window->pD3D11Device, nullptr);
+
+ EGLint display_attribs[] = {EGL_NONE};
+
+ window->EGLDisplay = eglGetPlatformDisplayEXT(
+ EGL_PLATFORM_DEVICE_EXT, window->EGLDevice, display_attribs);
+
+ eglInitialize(window->EGLDisplay, nullptr, nullptr);
+
+ EGLint num_configs = 0;
+ EGLint cfg_attribs[] = {EGL_SURFACE_TYPE,
+ EGL_WINDOW_BIT,
+ EGL_RENDERABLE_TYPE,
+ EGL_OPENGL_ES2_BIT,
+ EGL_RED_SIZE,
+ 8,
+ EGL_GREEN_SIZE,
+ 8,
+ EGL_BLUE_SIZE,
+ 8,
+ EGL_ALPHA_SIZE,
+ 8,
+ EGL_DEPTH_SIZE,
+ 24,
+ EGL_NONE};
+ EGLConfig configs[32];
+
+ eglChooseConfig(window->EGLDisplay, cfg_attribs, configs,
+ sizeof(configs) / sizeof(EGLConfig), &num_configs);
+ assert(num_configs > 0);
+ window->config = configs[0];
+
+ if (window->enable_compositor) {
+ window->fb_surface = EGL_NO_SURFACE;
+ } else {
+ window->fb_surface = eglCreateWindowSurface(
+ window->EGLDisplay, window->config, window->hWnd, NULL);
+ assert(window->fb_surface != EGL_NO_SURFACE);
+ }
+
+ EGLint ctx_attribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
+
+ // Create an EGL context that can be used for drawing
+ window->EGLContext = eglCreateContext(window->EGLDisplay, window->config,
+ EGL_NO_CONTEXT, ctx_attribs);
+
+ // Create the root of the DirectComposition visual tree
+ hr = window->pDCompDevice->CreateVisual(&window->pRoot);
+ assert(SUCCEEDED(hr));
+ hr = window->pDCompTarget->SetRoot(window->pRoot);
+ assert(SUCCEEDED(hr));
+
+ hr = window->pRoot->QueryInterface(__uuidof(IDCompositionVisualDebug),
+ (void**)&window->pVisualDebug);
+ assert(SUCCEEDED(hr));
+
+ // Uncomment this to see redraw regions during composite
+ // window->pVisualDebug->EnableRedrawRegions();
+
+ EGLBoolean ok = eglMakeCurrent(window->EGLDisplay, window->fb_surface,
+ window->fb_surface, window->EGLContext);
+ assert(ok);
+
+ return window;
+}
+
+void com_dc_destroy_window(Window* window) {
+ for (auto surface_it = window->surfaces.begin();
+ surface_it != window->surfaces.end(); ++surface_it) {
+ Surface& surface = surface_it->second;
+
+#ifndef USE_VIRTUAL_SURFACES
+ for (auto tile_it = surface.tiles.begin(); tile_it != surface.tiles.end();
+ ++tile_it) {
+ tile_it->second.pSurface->Release();
+ tile_it->second.pVisual->Release();
+ }
+#endif
+
+ surface.pVisual->Release();
+ }
+
+ if (window->fb_surface != EGL_NO_SURFACE) {
+ eglDestroySurface(window->EGLDisplay, window->fb_surface);
+ }
+ eglDestroyContext(window->EGLDisplay, window->EGLContext);
+ eglTerminate(window->EGLDisplay);
+ eglReleaseDeviceANGLE(window->EGLDevice);
+
+ for (int i = 0; i < NUM_QUERIES; ++i) {
+ window->pQueries[i]->Release();
+ }
+ window->pRoot->Release();
+ window->pVisualDebug->Release();
+ window->pD3D11Device->Release();
+ window->pDXGIDevice->Release();
+ window->pDCompDevice->Release();
+ window->pDCompTarget->Release();
+
+ CloseWindow(window->hWnd);
+ UnregisterClass(CLASS_NAME, window->hInstance);
+
+ delete window;
+}
+
+bool com_dc_tick(Window*) {
+ // Check and dispatch the windows event loop
+ MSG msg;
+ while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
+ if (msg.message == WM_QUIT) {
+ return false;
+ }
+
+ TranslateMessage(&msg);
+ DispatchMessage(&msg);
+ }
+
+ return true;
+}
+
+void com_dc_swap_buffers(Window* window) {
+ // If not using DC mode, then do a normal EGL swap buffers.
+ if (window->fb_surface != EGL_NO_SURFACE) {
+ switch (window->sync_mode) {
+ case SyncMode::None:
+ eglSwapInterval(window->EGLDisplay, 0);
+ break;
+ case SyncMode::Swap:
+ eglSwapInterval(window->EGLDisplay, 1);
+ break;
+ default:
+ assert(false); // unexpected vsync mode for simple compositor.
+ break;
+ }
+
+ eglSwapBuffers(window->EGLDisplay, window->fb_surface);
+ } else {
+ switch (window->sync_mode) {
+ case SyncMode::None:
+ break;
+ case SyncMode::Commit:
+ window->pDCompDevice->WaitForCommitCompletion();
+ break;
+ case SyncMode::Flush:
+ DwmFlush();
+ break;
+ case SyncMode::Query:
+ // todo!!!!
+ break;
+ default:
+ assert(false); // unexpected vsync mode for native compositor
+ break;
+ }
+ }
+}
+
+// Create a new DC surface
+void com_dc_create_surface(Window* window, uint64_t id, int tile_width,
+ int tile_height, bool is_opaque) {
+ assert(window->surfaces.count(id) == 0);
+
+ Surface surface;
+ surface.tile_width = tile_width;
+ surface.tile_height = tile_height;
+ surface.is_opaque = is_opaque;
+
+ // Create the visual node in the DC tree that stores properties
+ HRESULT hr = window->pDCompDevice->CreateVisual(&surface.pVisual);
+ assert(SUCCEEDED(hr));
+
+#ifdef USE_VIRTUAL_SURFACES
+ DXGI_ALPHA_MODE alpha_mode = surface.is_opaque
+ ? DXGI_ALPHA_MODE_IGNORE
+ : DXGI_ALPHA_MODE_PREMULTIPLIED;
+
+ hr = window->pDCompDevice->CreateVirtualSurface(
+ VIRTUAL_OFFSET * 2, VIRTUAL_OFFSET * 2, DXGI_FORMAT_B8G8R8A8_UNORM,
+ alpha_mode, &surface.pVirtualSurface);
+ assert(SUCCEEDED(hr));
+
+ // Bind the surface memory to this visual
+ hr = surface.pVisual->SetContent(surface.pVirtualSurface);
+ assert(SUCCEEDED(hr));
+#endif
+
+ window->surfaces[id] = surface;
+}
+
+void com_dc_create_tile(Window* window, uint64_t id, int x, int y) {
+ assert(window->surfaces.count(id) == 1);
+ Surface& surface = window->surfaces[id];
+
+ TileKey key(x, y);
+ assert(surface.tiles.count(key) == 0);
+
+ Tile tile;
+
+#ifndef USE_VIRTUAL_SURFACES
+ // Create the video memory surface.
+ DXGI_ALPHA_MODE alpha_mode = surface.is_opaque
+ ? DXGI_ALPHA_MODE_IGNORE
+ : DXGI_ALPHA_MODE_PREMULTIPLIED;
+ HRESULT hr = window->pDCompDevice->CreateSurface(
+ surface.tile_width, surface.tile_height, DXGI_FORMAT_B8G8R8A8_UNORM,
+ alpha_mode, &tile.pSurface);
+ assert(SUCCEEDED(hr));
+
+ // Create the visual node in the DC tree that stores properties
+ hr = window->pDCompDevice->CreateVisual(&tile.pVisual);
+ assert(SUCCEEDED(hr));
+
+ // Bind the surface memory to this visual
+ hr = tile.pVisual->SetContent(tile.pSurface);
+ assert(SUCCEEDED(hr));
+
+ // Place the visual in local-space of this surface
+ float offset_x = (float)(x * surface.tile_width);
+ float offset_y = (float)(y * surface.tile_height);
+ tile.pVisual->SetOffsetX(offset_x);
+ tile.pVisual->SetOffsetY(offset_y);
+
+ surface.pVisual->AddVisual(tile.pVisual, FALSE, NULL);
+#endif
+
+ surface.tiles[key] = tile;
+}
+
+void com_dc_destroy_tile(Window* window, uint64_t id, int x, int y) {
+ assert(window->surfaces.count(id) == 1);
+ Surface& surface = window->surfaces[id];
+
+ TileKey key(x, y);
+ assert(surface.tiles.count(key) == 1);
+ Tile& tile = surface.tiles[key];
+
+#ifndef USE_VIRTUAL_SURFACES
+ surface.pVisual->RemoveVisual(tile.pVisual);
+
+ tile.pVisual->Release();
+ tile.pSurface->Release();
+#endif
+
+ surface.tiles.erase(key);
+}
+
+void com_dc_destroy_surface(Window* window, uint64_t id) {
+ assert(window->surfaces.count(id) == 1);
+ Surface& surface = window->surfaces[id];
+
+ window->pRoot->RemoveVisual(surface.pVisual);
+
+#ifdef USE_VIRTUAL_SURFACES
+ surface.pVirtualSurface->Release();
+#else
+ // Release the video memory and visual in the tree
+ for (auto tile_it = surface.tiles.begin(); tile_it != surface.tiles.end();
+ ++tile_it) {
+ tile_it->second.pSurface->Release();
+ tile_it->second.pVisual->Release();
+ }
+#endif
+
+ surface.pVisual->Release();
+ window->surfaces.erase(id);
+}
+
+// Bind a DC surface to allow issuing GL commands to it
+GLuint com_dc_bind_surface(Window* window, uint64_t surface_id, int tile_x,
+ int tile_y, int* x_offset, int* y_offset,
+ int dirty_x0, int dirty_y0, int dirty_width,
+ int dirty_height) {
+ assert(window->surfaces.count(surface_id) == 1);
+ Surface& surface = window->surfaces[surface_id];
+
+ TileKey key(tile_x, tile_y);
+ assert(surface.tiles.count(key) == 1);
+ Tile& tile = surface.tiles[key];
+
+ // Inform DC that we want to draw on this surface. DC uses texture
+ // atlases when the tiles are small. It returns an offset where the
+ // client code must draw into this surface when this happens.
+ RECT update_rect;
+ update_rect.left = dirty_x0;
+ update_rect.top = dirty_y0;
+ update_rect.right = dirty_x0 + dirty_width;
+ update_rect.bottom = dirty_y0 + dirty_height;
+ POINT offset;
+ D3D11_TEXTURE2D_DESC desc;
+ ID3D11Texture2D* pTexture;
+ HRESULT hr;
+
+ // Store the current surface for unbinding later
+#ifdef USE_VIRTUAL_SURFACES
+ LONG tile_offset_x = VIRTUAL_OFFSET + tile_x * surface.tile_width;
+ LONG tile_offset_y = VIRTUAL_OFFSET + tile_y * surface.tile_height;
+
+ update_rect.left += tile_offset_x;
+ update_rect.top += tile_offset_y;
+ update_rect.right += tile_offset_x;
+ update_rect.bottom += tile_offset_y;
+
+ hr = surface.pVirtualSurface->BeginDraw(
+ &update_rect, __uuidof(ID3D11Texture2D), (void**)&pTexture, &offset);
+ window->pCurrentSurface = surface.pVirtualSurface;
+#else
+ hr = tile.pSurface->BeginDraw(&update_rect, __uuidof(ID3D11Texture2D),
+ (void**)&pTexture, &offset);
+ window->pCurrentSurface = tile.pSurface;
+#endif
+
+ // DC includes the origin of the dirty / update rect in the draw offset,
+ // undo that here since WR expects it to be an absolute offset.
+ assert(SUCCEEDED(hr));
+ offset.x -= dirty_x0;
+ offset.y -= dirty_y0;
+ pTexture->GetDesc(&desc);
+ *x_offset = offset.x;
+ *y_offset = offset.y;
+
+ // Construct an EGLImage wrapper around the D3D texture for ANGLE.
+ const EGLAttrib attribs[] = {EGL_NONE};
+ window->mEGLImage = eglCreateImage(
+ window->EGLDisplay, EGL_NO_CONTEXT, EGL_D3D11_TEXTURE_ANGLE,
+ static_cast<EGLClientBuffer>(pTexture), attribs);
+
+ // Get the current FBO and RBO id, so we can restore them later
+ GLint currentFboId, currentRboId;
+ glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &currentFboId);
+ glGetIntegerv(GL_RENDERBUFFER_BINDING, &currentRboId);
+
+ // Create a render buffer object that is backed by the EGL image.
+ glGenRenderbuffers(1, &window->mColorRBO);
+ glBindRenderbuffer(GL_RENDERBUFFER, window->mColorRBO);
+ glEGLImageTargetRenderbufferStorageOES(GL_RENDERBUFFER, window->mEGLImage);
+
+ // Get or create an FBO for the specified dimensions
+ GLuint fboId = GetOrCreateFbo(window, desc.Width, desc.Height);
+
+ // Attach the new renderbuffer to the FBO
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fboId);
+ glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+ GL_RENDERBUFFER, window->mColorRBO);
+
+ // Restore previous FBO and RBO bindings
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, currentFboId);
+ glBindRenderbuffer(GL_RENDERBUFFER, currentRboId);
+
+ return fboId;
+}
+
+// Unbind a currently bound DC surface
+void com_dc_unbind_surface(Window* window) {
+ HRESULT hr = window->pCurrentSurface->EndDraw();
+ assert(SUCCEEDED(hr));
+
+ glDeleteRenderbuffers(1, &window->mColorRBO);
+ window->mColorRBO = 0;
+
+ eglDestroyImage(window->EGLDisplay, window->mEGLImage);
+ window->mEGLImage = EGL_NO_IMAGE;
+}
+
+void com_dc_begin_transaction(Window*) {}
+
+// Add a DC surface to the visual tree. Called per-frame to build the
+// composition.
+void com_dc_add_surface(Window* window, uint64_t id, int x, int y, int clip_x,
+ int clip_y, int clip_w, int clip_h) {
+ Surface surface = window->surfaces[id];
+ window->mCurrentLayers.push_back(id);
+
+ // Place the visual - this changes frame to frame based on scroll position
+ // of the slice.
+ float offset_x = (float)(x + window->client_rect.left);
+ float offset_y = (float)(y + window->client_rect.top);
+#ifdef USE_VIRTUAL_SURFACES
+ offset_x -= VIRTUAL_OFFSET;
+ offset_y -= VIRTUAL_OFFSET;
+#endif
+ surface.pVisual->SetOffsetX(offset_x);
+ surface.pVisual->SetOffsetY(offset_y);
+
+ // Set the clip rect - converting from world space to the pre-offset space
+ // that DC requires for rectangle clips.
+ D2D_RECT_F clip_rect;
+ clip_rect.left = clip_x - offset_x;
+ clip_rect.top = clip_y - offset_y;
+ clip_rect.right = clip_rect.left + clip_w;
+ clip_rect.bottom = clip_rect.top + clip_h;
+ surface.pVisual->SetClip(clip_rect);
+}
+
+// Finish the composition transaction, telling DC to composite
+void com_dc_end_transaction(Window* window) {
+ bool same = window->mPrevLayers == window->mCurrentLayers;
+
+ if (!same) {
+ HRESULT hr = window->pRoot->RemoveAllVisuals();
+ assert(SUCCEEDED(hr));
+
+ for (auto it = window->mCurrentLayers.begin();
+ it != window->mCurrentLayers.end(); ++it) {
+ Surface& surface = window->surfaces[*it];
+
+ // Add this visual as the last element in the visual tree (z-order is
+ // implicit, based on the order tiles are added).
+ hr = window->pRoot->AddVisual(surface.pVisual, FALSE, NULL);
+ assert(SUCCEEDED(hr));
+ }
+ }
+
+ window->mPrevLayers.swap(window->mCurrentLayers);
+ window->mCurrentLayers.clear();
+
+ HRESULT hr = window->pDCompDevice->Commit();
+ assert(SUCCEEDED(hr));
+}
+
+// Get a pointer to an EGL symbol
+void* com_dc_get_proc_address(const char* name) {
+ return eglGetProcAddress(name);
+}
+}
diff --git a/gfx/wr/example-compositor/compositor-windows/src/lib.rs b/gfx/wr/example-compositor/compositor-windows/src/lib.rs
new file mode 100644
index 0000000000..f0e5fa5b9c
--- /dev/null
+++ b/gfx/wr/example-compositor/compositor-windows/src/lib.rs
@@ -0,0 +1,265 @@
+/* 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::os::raw::{c_void, c_char};
+
+/*
+
+ This is a very simple (and unsafe!) rust wrapper for the DirectComposite / D3D11 / ANGLE
+ implementation in lib.cpp.
+
+ It just proxies the calls from the Compositor impl to the C99 code. This is very
+ hacky and not suitable for production!
+
+ */
+
+// Opaque wrapper for the Window type in lib.cpp
+#[repr(C)]
+pub struct Window {
+ _unused: [u8; 0]
+}
+
+// C99 functions that do the compositor work
+extern {
+ fn com_dc_create_window(
+ width: i32,
+ height: i32,
+ enable_compositor: bool,
+ sync_mode: i32,
+ ) -> *mut Window;
+ fn com_dc_destroy_window(window: *mut Window);
+ fn com_dc_tick(window: *mut Window) -> bool;
+ fn com_dc_get_proc_address(name: *const c_char) -> *const c_void;
+ fn com_dc_swap_buffers(window: *mut Window);
+
+ fn com_dc_create_surface(
+ window: *mut Window,
+ id: u64,
+ tile_width: i32,
+ tile_height: i32,
+ is_opaque: bool,
+ );
+
+ fn com_dc_create_tile(
+ window: *mut Window,
+ id: u64,
+ x: i32,
+ y: i32,
+ );
+
+ fn com_dc_destroy_tile(
+ window: *mut Window,
+ id: u64,
+ x: i32,
+ y: i32,
+ );
+
+ fn com_dc_destroy_surface(
+ window: *mut Window,
+ id: u64,
+ );
+
+ fn com_dc_bind_surface(
+ window: *mut Window,
+ surface_id: u64,
+ tile_x: i32,
+ tile_y: i32,
+ x_offset: &mut i32,
+ y_offset: &mut i32,
+ dirty_x0: i32,
+ dirty_y0: i32,
+ dirty_width: i32,
+ dirty_height: i32,
+ ) -> u32;
+ fn com_dc_unbind_surface(window: *mut Window);
+
+ fn com_dc_begin_transaction(window: *mut Window);
+
+ fn com_dc_add_surface(
+ window: *mut Window,
+ id: u64,
+ x: i32,
+ y: i32,
+ clip_x: i32,
+ clip_y: i32,
+ clip_w: i32,
+ clip_h: i32,
+ );
+
+ fn com_dc_end_transaction(window: *mut Window);
+}
+
+pub fn create_window(
+ width: i32,
+ height: i32,
+ enable_compositor: bool,
+ sync_mode: i32,
+) -> *mut Window {
+ unsafe {
+ com_dc_create_window(width, height, enable_compositor, sync_mode)
+ }
+}
+
+pub fn destroy_window(window: *mut Window) {
+ unsafe {
+ com_dc_destroy_window(window);
+ }
+}
+
+pub fn tick(window: *mut Window) -> bool {
+ unsafe {
+ com_dc_tick(window)
+ }
+}
+
+pub fn get_proc_address(name: *const c_char) -> *const c_void {
+ unsafe {
+ com_dc_get_proc_address(name)
+ }
+}
+
+pub fn create_surface(
+ window: *mut Window,
+ id: u64,
+ tile_width: i32,
+ tile_height: i32,
+ is_opaque: bool,
+) {
+ unsafe {
+ com_dc_create_surface(
+ window,
+ id,
+ tile_width,
+ tile_height,
+ is_opaque,
+ )
+ }
+}
+
+pub fn create_tile(
+ window: *mut Window,
+ id: u64,
+ x: i32,
+ y: i32,
+) {
+ unsafe {
+ com_dc_create_tile(
+ window,
+ id,
+ x,
+ y,
+ )
+ }
+}
+
+pub fn destroy_tile(
+ window: *mut Window,
+ id: u64,
+ x: i32,
+ y: i32,
+) {
+ unsafe {
+ com_dc_destroy_tile(
+ window,
+ id,
+ x,
+ y,
+ )
+ }
+}
+
+pub fn destroy_surface(
+ window: *mut Window,
+ id: u64,
+) {
+ unsafe {
+ com_dc_destroy_surface(
+ window,
+ id,
+ )
+ }
+}
+
+pub fn bind_surface(
+ window: *mut Window,
+ surface_id: u64,
+ tile_x: i32,
+ tile_y: i32,
+ dirty_x0: i32,
+ dirty_y0: i32,
+ dirty_width: i32,
+ dirty_height: i32,
+) -> (u32, i32, i32) {
+ unsafe {
+ let mut x_offset = 0;
+ let mut y_offset = 0;
+
+ let fbo_id = com_dc_bind_surface(
+ window,
+ surface_id,
+ tile_x,
+ tile_y,
+ &mut x_offset,
+ &mut y_offset,
+ dirty_x0,
+ dirty_y0,
+ dirty_width,
+ dirty_height,
+ );
+
+ (fbo_id, x_offset, y_offset)
+ }
+}
+
+pub fn add_surface(
+ window: *mut Window,
+ id: u64,
+ x: i32,
+ y: i32,
+ clip_x: i32,
+ clip_y: i32,
+ clip_w: i32,
+ clip_h: i32,
+) {
+ unsafe {
+ com_dc_add_surface(
+ window,
+ id,
+ x,
+ y,
+ clip_x,
+ clip_y,
+ clip_w,
+ clip_h,
+ )
+ }
+}
+
+pub fn begin_transaction(window: *mut Window) {
+ unsafe {
+ com_dc_begin_transaction(window)
+ }
+}
+
+pub fn unbind_surface(window: *mut Window) {
+ unsafe {
+ com_dc_unbind_surface(window)
+ }
+}
+
+pub fn end_transaction(window: *mut Window) {
+ unsafe {
+ com_dc_end_transaction(window)
+ }
+}
+
+pub fn swap_buffers(window: *mut Window) {
+ unsafe {
+ com_dc_swap_buffers(window);
+ }
+}
+
+pub fn deinit(_window: *mut Window) {
+ todo!()
+}
diff --git a/gfx/wr/example-compositor/compositor/Cargo.toml b/gfx/wr/example-compositor/compositor/Cargo.toml
new file mode 100644
index 0000000000..02c6ebe0ce
--- /dev/null
+++ b/gfx/wr/example-compositor/compositor/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "compositor"
+version = "0.1.0"
+authors = ["Glenn Watson <gw@intuitionlibrary.com>"]
+edition = "2018"
+license = "MPL-2.0"
+
+[dependencies]
+webrender = { path = "../../webrender" }
+gleam = "0.15"
+
+[target.'cfg(windows)'.dependencies]
+compositor-windows = { path = "../compositor-windows" }
+
+[target.'cfg(target_os = "linux")'.dependencies]
+compositor-wayland = { path = "../compositor-wayland" }
diff --git a/gfx/wr/example-compositor/compositor/src/main.rs b/gfx/wr/example-compositor/compositor/src/main.rs
new file mode 100644
index 0000000000..0a0275fe3f
--- /dev/null
+++ b/gfx/wr/example-compositor/compositor/src/main.rs
@@ -0,0 +1,549 @@
+/* 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/. */
+
+/*
+
+ An example of how to implement the Compositor trait that
+ allows picture caching surfaces to be composited by the operating
+ system.
+
+ The current example supports DirectComposite on Windows only.
+
+*/
+
+use euclid::Angle;
+use gleam::gl;
+use std::ffi::CString;
+use std::sync::mpsc;
+use webrender::{CompositorSurfaceTransform, Transaction, api::*};
+use webrender::api::units::*;
+use webrender::Device;
+#[cfg(target_os = "windows")]
+use compositor_windows as compositor;
+#[cfg(target_os = "linux")]
+use compositor_wayland as compositor;
+use std::{env, f32, process};
+
+// A very hacky integration with DirectComposite. It proxies calls from the compositor
+// interface to a simple C99 library which does the DirectComposition / D3D11 / ANGLE
+// interfacing. This is a very unsafe impl due to the way the window pointer is passed
+// around!
+struct DirectCompositeInterface {
+ window: *mut compositor::Window,
+}
+
+impl DirectCompositeInterface {
+ fn new(window: *mut compositor::Window) -> Self {
+ DirectCompositeInterface { window }
+ }
+}
+
+impl webrender::Compositor for DirectCompositeInterface {
+ fn create_surface(
+ &mut self,
+ _device: &mut Device,
+ id: webrender::NativeSurfaceId,
+ _virtual_offset: DeviceIntPoint,
+ tile_size: DeviceIntSize,
+ is_opaque: bool,
+ ) {
+ compositor::create_surface(
+ self.window,
+ id.0,
+ tile_size.width,
+ tile_size.height,
+ is_opaque,
+ );
+ }
+
+ fn destroy_surface(&mut self, _device: &mut Device, id: webrender::NativeSurfaceId) {
+ compositor::destroy_surface(self.window, id.0);
+ }
+
+ fn create_tile(&mut self, _device: &mut Device, id: webrender::NativeTileId) {
+ compositor::create_tile(self.window, id.surface_id.0, id.x, id.y);
+ }
+
+ fn destroy_tile(&mut self, _device: &mut Device, id: webrender::NativeTileId) {
+ compositor::destroy_tile(self.window, id.surface_id.0, id.x, id.y);
+ }
+
+ fn bind(
+ &mut self,
+ _device: &mut Device,
+ id: webrender::NativeTileId,
+ dirty_rect: DeviceIntRect,
+ _valid_rect: DeviceIntRect,
+ ) -> webrender::NativeSurfaceInfo {
+ let (fbo_id, x, y) = compositor::bind_surface(
+ self.window,
+ id.surface_id.0,
+ id.x,
+ id.y,
+ dirty_rect.min.x,
+ dirty_rect.min.y,
+ dirty_rect.width(),
+ dirty_rect.height(),
+ );
+
+ webrender::NativeSurfaceInfo {
+ origin: DeviceIntPoint::new(x, y),
+ fbo_id,
+ }
+ }
+
+ fn unbind(&mut self, _device: &mut Device) {
+ compositor::unbind_surface(self.window);
+ }
+
+ fn begin_frame(&mut self, _device: &mut Device) {
+ compositor::begin_transaction(self.window);
+ }
+
+ fn add_surface(
+ &mut self,
+ _device: &mut Device,
+ id: webrender::NativeSurfaceId,
+ transform: CompositorSurfaceTransform,
+ clip_rect: DeviceIntRect,
+ _image_rendering: ImageRendering,
+ ) {
+ compositor::add_surface(
+ self.window,
+ id.0,
+ transform.offset.x as i32,
+ transform.offset.y as i32,
+ clip_rect.min.x,
+ clip_rect.min.y,
+ clip_rect.width(),
+ clip_rect.height(),
+ );
+ }
+
+ fn end_frame(&mut self, _device: &mut Device) {
+ compositor::end_transaction(self.window);
+ }
+ fn create_external_surface(
+ &mut self,
+ _device: &mut Device,
+ _id: webrender::NativeSurfaceId,
+ _: bool,
+ ) {
+ todo!()
+ }
+
+ fn attach_external_image(
+ &mut self,
+ _device: &mut Device,
+ _id: webrender::NativeSurfaceId,
+ _external_image: ExternalImageId,
+ ) {
+ todo!()
+ }
+
+ fn enable_native_compositor(&mut self, _device: &mut Device, _enable: bool) {
+ todo!()
+ }
+
+ fn deinit(&mut self, _device: &mut Device) {
+ compositor::deinit(self.window);
+ }
+
+ fn get_capabilities(&self, _device: &mut Device) -> webrender::CompositorCapabilities {
+ webrender::CompositorCapabilities {
+ virtual_surface_size: 1024 * 1024,
+ ..Default::default()
+ }
+ }
+
+ fn invalidate_tile(
+ &mut self,
+ _device: &mut Device,
+ _id: webrender::NativeTileId,
+ _valid_rect: DeviceIntRect,
+ ) {
+ }
+
+ fn start_compositing(
+ &mut self,
+ _device: &mut Device,
+ _color: webrender::webrender_api::ColorF,
+ _dirty_rects: &[DeviceIntRect],
+ _opaque_rects: &[DeviceIntRect],
+ ) {
+ }
+
+ fn create_backdrop_surface(
+ &mut self,
+ _device: &mut Device,
+ _id: webrender::NativeSurfaceId,
+ _color: webrender::webrender_api::ColorF,
+ ) {
+ todo!()
+ }
+
+ fn get_window_visibility(&self, _device: &mut Device) -> webrender::WindowVisibility {
+ todo!()
+ }
+}
+
+// Simplisitic implementation of the WR notifier interface to know when a frame
+// has been prepared and can be rendered.
+struct Notifier {
+ tx: mpsc::Sender<()>,
+}
+
+impl Notifier {
+ fn new(tx: mpsc::Sender<()>) -> Self {
+ Notifier { tx }
+ }
+}
+
+impl RenderNotifier for Notifier {
+ fn clone(&self) -> Box<dyn RenderNotifier> {
+ Box::new(Notifier {
+ tx: self.tx.clone(),
+ })
+ }
+
+ fn wake_up(&self, _composite_needed: bool) {}
+
+ fn new_frame_ready(&self, _: DocumentId, _scrolled: bool, _composite_needed: bool, _: FramePublishId) {
+ self.tx.send(()).ok();
+ }
+}
+
+fn push_rotated_rect(
+ builder: &mut DisplayListBuilder,
+ rect: LayoutRect,
+ color: ColorF,
+ spatial_id: SpatialId,
+ _root_pipeline_id: PipelineId,
+ angle: f32,
+ time: f32,
+ item_key: SpatialTreeItemKey,
+) {
+ let color = color.scale_rgb(time);
+ let rotation = LayoutTransform::rotation(
+ 0.0,
+ 0.0,
+ 1.0,
+ Angle::radians(2.0 * std::f32::consts::PI * angle),
+ );
+ let center = rect.center();
+ let transform_origin = LayoutVector3D::new(center.x, center.y, 0.0);
+ let transform = rotation
+ .pre_translate(-transform_origin)
+ .then_translate(transform_origin);
+ let spatial_id = builder.push_reference_frame(
+ LayoutPoint::zero(),
+ spatial_id,
+ TransformStyle::Flat,
+ PropertyBinding::Value(transform),
+ ReferenceFrameKind::Transform {
+ is_2d_scale_translation: false,
+ should_snap: false,
+ paired_with_perspective: false,
+ },
+ item_key,
+ );
+ builder.push_rect(
+ &CommonItemProperties::new(
+ rect,
+ SpaceAndClipInfo {
+ spatial_id,
+ clip_chain_id: ClipChainId::INVALID,
+ },
+ ),
+ rect,
+ color,
+ );
+}
+
+fn build_display_list(
+ builder: &mut DisplayListBuilder,
+ scroll_id: ExternalScrollId,
+ root_pipeline_id: PipelineId,
+ layout_size: LayoutSize,
+ time: f32,
+ invalidations: Invalidations,
+) {
+ let size_factor = match invalidations {
+ Invalidations::Small => 0.1,
+ Invalidations::Large | Invalidations::Scrolling => 1.0,
+ };
+
+ let fixed_space_info = SpaceAndClipInfo {
+ spatial_id: SpatialId::root_scroll_node(root_pipeline_id),
+ clip_chain_id: ClipChainId::INVALID,
+ };
+
+ let scroll_spatial_id = builder.define_scroll_frame(
+ fixed_space_info.spatial_id,
+ scroll_id,
+ LayoutRect::from_size(layout_size),
+ LayoutRect::from_size(layout_size),
+ LayoutVector2D::zero(),
+ APZScrollGeneration::default(),
+ HasScrollLinkedEffect::No,
+ SpatialTreeItemKey::new(1, 0),
+ );
+
+ builder.push_rect(
+ &CommonItemProperties::new(
+ LayoutRect::from_size(layout_size).inflate(-10.0, -10.0),
+ fixed_space_info,
+ ),
+ LayoutRect::from_size(layout_size).inflate(-10.0, -10.0),
+ ColorF::new(0.8, 0.8, 0.8, 1.0),
+ );
+
+ push_rotated_rect(
+ builder,
+ LayoutRect::from_origin_and_size(
+ LayoutPoint::new(100.0, 100.0),
+ LayoutSize::new(size_factor * 400.0, size_factor * 400.0),
+ ),
+ ColorF::new(1.0, 0.0, 0.0, 1.0),
+ scroll_spatial_id,
+ root_pipeline_id,
+ time,
+ time,
+ SpatialTreeItemKey::new(1, 1),
+ );
+
+ push_rotated_rect(
+ builder,
+ LayoutRect::from_origin_and_size(
+ LayoutPoint::new(800.0, 100.0),
+ LayoutSize::new(size_factor * 100.0, size_factor * 600.0),
+ ),
+ ColorF::new(0.0, 1.0, 0.0, 1.0),
+ fixed_space_info.spatial_id,
+ root_pipeline_id,
+ 0.2,
+ time,
+ SpatialTreeItemKey::new(1, 2),
+ );
+
+ push_rotated_rect(
+ builder,
+ LayoutRect::from_origin_and_size(
+ LayoutPoint::new(700.0, 200.0),
+ LayoutSize::new(size_factor * 300.0, size_factor * 300.0),
+ ),
+ ColorF::new(0.0, 0.0, 1.0, 1.0),
+ scroll_spatial_id,
+ root_pipeline_id,
+ 0.1,
+ time,
+ SpatialTreeItemKey::new(1, 3),
+ );
+
+ push_rotated_rect(
+ builder,
+ LayoutRect::from_origin_and_size(
+ LayoutPoint::new(100.0, 600.0),
+ LayoutSize::new(size_factor * 400.0, size_factor * 400.0),
+ ),
+ ColorF::new(1.0, 1.0, 0.0, 1.0),
+ scroll_spatial_id,
+ root_pipeline_id,
+ time,
+ time,
+ SpatialTreeItemKey::new(1, 4),
+ );
+
+ push_rotated_rect(
+ builder,
+ LayoutRect::from_origin_and_size(
+ LayoutPoint::new(700.0, 600.0),
+ LayoutSize::new(size_factor * 400.0, size_factor * 400.0),
+ ),
+ ColorF::new(0.0, 1.0, 1.0, 1.0),
+ scroll_spatial_id,
+ root_pipeline_id,
+ time,
+ time,
+ SpatialTreeItemKey::new(1, 5),
+ );
+}
+
+#[derive(Debug, Copy, Clone)]
+enum Invalidations {
+ Large,
+ Small,
+ Scrolling,
+}
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+enum Sync {
+ None = 0,
+ Swap = 1,
+ Commit = 2,
+ Flush = 3,
+ Query = 4,
+}
+
+fn main() {
+ let args: Vec<String> = env::args().collect();
+
+ if args.len() != 6 {
+ println!("USAGE: compositor [native|none] [small|large|scroll] [none|swap|commit|flush|query] width height");
+ process::exit(0);
+ }
+
+ let enable_compositor = match args[1].parse::<String>().unwrap().as_str() {
+ "native" => true,
+ "none" => false,
+ _ => panic!("invalid compositor [native, none]"),
+ };
+
+ let inv_mode = match args[2].parse::<String>().unwrap().as_str() {
+ "small" => Invalidations::Small,
+ "large" => Invalidations::Large,
+ "scroll" => Invalidations::Scrolling,
+ _ => panic!("invalid invalidations [small, large, scroll]"),
+ };
+
+ let sync_mode = match args[3].parse::<String>().unwrap().as_str() {
+ "none" => Sync::None,
+ "swap" => Sync::Swap,
+ "commit" => Sync::Commit,
+ "flush" => Sync::Flush,
+ "query" => Sync::Query,
+ _ => panic!("invalid sync mode [none, swap, commit, flush, query]"),
+ };
+
+ let width = args[4].parse().unwrap();
+ let height = args[5].parse().unwrap();
+ let device_size = DeviceIntSize::new(width, height);
+
+ // Load GL, construct WR and the native compositor interface.
+ let window = compositor::create_window(
+ device_size.width,
+ device_size.height,
+ enable_compositor,
+ sync_mode as i32,
+ );
+ let debug_flags = DebugFlags::empty();
+ let compositor_config = if enable_compositor {
+ webrender::CompositorConfig::Native {
+ compositor: Box::new(DirectCompositeInterface::new(window)),
+ }
+ } else {
+ webrender::CompositorConfig::Draw {
+ max_partial_present_rects: 0,
+ draw_previous_partial_present_regions: false,
+ partial_present: None,
+ }
+ };
+ let opts = webrender::WebRenderOptions {
+ clear_color: ColorF::new(1.0, 1.0, 1.0, 1.0),
+ debug_flags,
+ compositor_config,
+ surface_origin_is_top_left: false,
+ ..webrender::WebRenderOptions::default()
+ };
+ let (tx, rx) = mpsc::channel();
+ let notifier = Box::new(Notifier::new(tx));
+ let gl = unsafe {
+ gl::GlesFns::load_with(|symbol| {
+ let symbol = CString::new(symbol).unwrap();
+ let ptr = compositor::get_proc_address(symbol.as_ptr());
+ ptr
+ })
+ };
+ let (mut renderer, sender) =
+ webrender::create_webrender_instance(gl.clone(), notifier, opts, None).unwrap();
+ let mut api = sender.create_api();
+ let document_id = api.add_document(device_size);
+ let device_pixel_ratio = 1.0;
+ let mut current_epoch = Epoch(0);
+ let root_pipeline_id = PipelineId(0, 0);
+ let layout_size = device_size.to_f32() / euclid::Scale::new(device_pixel_ratio);
+ let mut time = 0.0;
+ let scroll_id = ExternalScrollId(3, root_pipeline_id);
+
+ // Kick off first transaction which will mean we get a notify below to build the DL and render.
+ let mut txn = Transaction::new();
+ txn.set_root_pipeline(root_pipeline_id);
+
+ if let Invalidations::Scrolling = inv_mode {
+ let mut root_builder = DisplayListBuilder::new(root_pipeline_id);
+ root_builder.begin();
+
+ build_display_list(
+ &mut root_builder,
+ scroll_id,
+ root_pipeline_id,
+ layout_size,
+ 1.0,
+ inv_mode,
+ );
+
+ txn.set_display_list(current_epoch, root_builder.end());
+ }
+
+ txn.generate_frame(0, RenderReasons::empty());
+ api.send_transaction(document_id, txn);
+
+ // Tick the compositor (in this sample, we don't block on UI events)
+ while compositor::tick(window) {
+ // If there is a new frame ready to draw
+ if let Ok(..) = rx.try_recv() {
+ // Update and render. This will invoke the native compositor interface implemented above
+ // as required.
+ renderer.update();
+ renderer.render(device_size, 0).unwrap();
+ let _ = renderer.flush_pipeline_info();
+
+ // Construct a simple display list that can be drawn and composited by DC.
+ let mut txn = Transaction::new();
+
+ match inv_mode {
+ Invalidations::Small | Invalidations::Large => {
+ let mut root_builder = DisplayListBuilder::new(root_pipeline_id);
+ root_builder.begin();
+
+ build_display_list(
+ &mut root_builder,
+ scroll_id,
+ root_pipeline_id,
+ layout_size,
+ time,
+ inv_mode,
+ );
+
+ txn.set_display_list(current_epoch, root_builder.end());
+ }
+ Invalidations::Scrolling => {
+ let d = 0.5 - 0.5 * (2.0 * f32::consts::PI * 5.0 * time).cos();
+ txn.set_scroll_offsets(
+ scroll_id,
+ vec![SampledScrollOffset {
+ offset: LayoutVector2D::new(0.0, (d * 100.0).round()),
+ generation: APZScrollGeneration::default(),
+ }],
+ );
+ }
+ }
+
+ txn.generate_frame(0, RenderReasons::empty());
+ api.send_transaction(document_id, txn);
+ current_epoch.0 += 1;
+ time += 0.001;
+ if time > 1.0 {
+ time = 0.0;
+ }
+
+ // This does nothing when native compositor is enabled
+ compositor::swap_buffers(window);
+ }
+ }
+
+ renderer.deinit();
+ compositor::destroy_window(window);
+}
diff --git a/gfx/wr/examples/Cargo.toml b/gfx/wr/examples/Cargo.toml
new file mode 100644
index 0000000000..9065707538
--- /dev/null
+++ b/gfx/wr/examples/Cargo.toml
@@ -0,0 +1,67 @@
+[package]
+name = "webrender-examples"
+version = "0.1.0"
+authors = ["Glenn Watson <gw@intuitionlibrary.com>"]
+license = "MPL-2.0"
+repository = "https://github.com/servo/webrender"
+edition = "2018"
+
+[[bin]]
+name = "alpha_perf"
+path = "alpha_perf.rs"
+
+[[bin]]
+name = "animation"
+path = "animation.rs"
+
+[[bin]]
+name = "basic"
+path = "basic.rs"
+
+[[bin]]
+name = "blob"
+path = "blob.rs"
+
+[[bin]]
+name = "document"
+path = "document.rs"
+
+[[bin]]
+name = "iframe"
+path = "iframe.rs"
+
+[[bin]]
+name = "image_resize"
+path = "image_resize.rs"
+
+[[bin]]
+name = "multiwindow"
+path = "multiwindow.rs"
+
+[[bin]]
+name = "scrolling"
+path = "scrolling.rs"
+
+[[bin]]
+name = "texture_cache_stress"
+path = "texture_cache_stress.rs"
+
+[[bin]]
+name = "yuv"
+path = "yuv.rs"
+
+[features]
+debug = ["webrender/capture", "webrender/profiler"]
+
+[dependencies]
+app_units = "0.7"
+env_logger = { version = "0.10", default_features = false }
+euclid = "0.22"
+gleam = "0.15"
+glutin = "0.28"
+rayon = "1"
+webrender = { path = "../webrender" }
+winit = "0.26"
+
+[target.'cfg(target_os = "macos")'.dependencies]
+core-foundation = "0.7"
diff --git a/gfx/wr/examples/README.md b/gfx/wr/examples/README.md
new file mode 100644
index 0000000000..68efd33a89
--- /dev/null
+++ b/gfx/wr/examples/README.md
@@ -0,0 +1,8 @@
+# Examples
+
+This directory contains a collection of examples which uses the WebRender API.
+
+To run an example e.g. `basic`, try:
+```
+cargo run --bin basic
+```
diff --git a/gfx/wr/examples/alpha_perf.rs b/gfx/wr/examples/alpha_perf.rs
new file mode 100644
index 0000000000..76cfa40c13
--- /dev/null
+++ b/gfx/wr/examples/alpha_perf.rs
@@ -0,0 +1,95 @@
+/* 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/. */
+
+extern crate euclid;
+extern crate gleam;
+extern crate glutin;
+extern crate webrender;
+extern crate winit;
+
+#[path = "common/boilerplate.rs"]
+mod boilerplate;
+
+use crate::boilerplate::{Example, HandyDandyRectBuilder};
+use std::cmp;
+use webrender::api::*;
+use webrender::render_api::*;
+use webrender::api::units::DeviceIntSize;
+
+
+struct App {
+ rect_count: usize,
+}
+
+impl Example for App {
+ fn render(
+ &mut self,
+ _api: &mut RenderApi,
+ builder: &mut DisplayListBuilder,
+ _txn: &mut Transaction,
+ _device_size: DeviceIntSize,
+ pipeline_id: PipelineId,
+ _document_id: DocumentId,
+ ) {
+ let bounds = (0, 0).to(1920, 1080);
+ let space_and_clip = SpaceAndClipInfo::root_scroll(pipeline_id);
+
+ builder.push_simple_stacking_context(
+ bounds.min,
+ space_and_clip.spatial_id,
+ PrimitiveFlags::IS_BACKFACE_VISIBLE,
+ );
+
+ for _ in 0 .. self.rect_count {
+ builder.push_rect(
+ &CommonItemProperties::new(bounds, space_and_clip),
+ bounds,
+ ColorF::new(1.0, 1.0, 1.0, 0.05)
+ );
+ }
+
+ builder.pop_stacking_context();
+ }
+
+ fn on_event(
+ &mut self,
+ event: winit::event::WindowEvent,
+ _window: &winit::window::Window,
+ _api: &mut RenderApi,
+ _document_id: DocumentId,
+ ) -> bool {
+ match event {
+ winit::event::WindowEvent::KeyboardInput {
+ input: winit::event::KeyboardInput {
+ state: winit::event::ElementState::Pressed,
+ virtual_keycode: Some(key),
+ ..
+ },
+ ..
+ } => {
+ match key {
+ winit::event::VirtualKeyCode::Right => {
+ self.rect_count += 1;
+ println!("rects = {}", self.rect_count);
+ }
+ winit::event::VirtualKeyCode::Left => {
+ self.rect_count = cmp::max(self.rect_count, 1) - 1;
+ println!("rects = {}", self.rect_count);
+ }
+ _ => {}
+ };
+ }
+ _ => (),
+ }
+
+ true
+ }
+}
+
+fn main() {
+ let mut app = App {
+ rect_count: 1,
+ };
+ boilerplate::main_wrapper(&mut app, None);
+}
diff --git a/gfx/wr/examples/animation.rs b/gfx/wr/examples/animation.rs
new file mode 100644
index 0000000000..7d24b6542e
--- /dev/null
+++ b/gfx/wr/examples/animation.rs
@@ -0,0 +1,258 @@
+/* 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/. */
+
+//! This example creates a 200x200 white rect and allows the user to move it
+//! around by using the arrow keys and rotate with '<'/'>'.
+//! It does this by using the animation API.
+
+//! The example also features seamless opaque/transparent split of a
+//! rounded cornered rectangle, which is done automatically during the
+//! scene building for render optimization.
+
+extern crate euclid;
+extern crate gleam;
+extern crate glutin;
+extern crate webrender;
+extern crate winit;
+
+#[path = "common/boilerplate.rs"]
+mod boilerplate;
+
+use crate::boilerplate::{Example, HandyDandyRectBuilder};
+use euclid::Angle;
+use webrender::api::*;
+use webrender::render_api::*;
+use webrender::api::units::*;
+
+
+struct App {
+ property_key0: PropertyBindingKey<LayoutTransform>,
+ property_key1: PropertyBindingKey<LayoutTransform>,
+ property_key2: PropertyBindingKey<LayoutTransform>,
+ opacity_key: PropertyBindingKey<f32>,
+ opacity: f32,
+ angle0: f32,
+ angle1: f32,
+ angle2: f32,
+}
+
+impl App {
+ fn add_rounded_rect(
+ &mut self,
+ bounds: LayoutRect,
+ color: ColorF,
+ builder: &mut DisplayListBuilder,
+ pipeline_id: PipelineId,
+ property_key: PropertyBindingKey<LayoutTransform>,
+ opacity_key: Option<PropertyBindingKey<f32>>,
+ spatial_tree_item_key: SpatialTreeItemKey,
+ ) {
+ let filters = match opacity_key {
+ Some(opacity_key) => {
+ vec![
+ FilterOp::Opacity(PropertyBinding::Binding(opacity_key, self.opacity), self.opacity),
+ ]
+ }
+ None => {
+ vec![]
+ }
+ };
+
+ let spatial_id = builder.push_reference_frame(
+ bounds.min,
+ SpatialId::root_scroll_node(pipeline_id),
+ TransformStyle::Flat,
+ PropertyBinding::Binding(property_key, LayoutTransform::identity()),
+ ReferenceFrameKind::Transform {
+ is_2d_scale_translation: false,
+ should_snap: false,
+ paired_with_perspective: false,
+ },
+ spatial_tree_item_key,
+ );
+
+ builder.push_simple_stacking_context_with_filters(
+ LayoutPoint::zero(),
+ spatial_id,
+ PrimitiveFlags::IS_BACKFACE_VISIBLE,
+ &filters,
+ &[],
+ &[]
+ );
+
+ let space_and_clip = SpaceAndClipInfo {
+ spatial_id,
+ clip_chain_id: ClipChainId::INVALID,
+ };
+ let clip_bounds = LayoutRect::from_size(bounds.size());
+ let complex_clip = ComplexClipRegion {
+ rect: clip_bounds,
+ radii: BorderRadius::uniform(30.0),
+ mode: ClipMode::Clip,
+ };
+ let clip_id = builder.define_clip_rounded_rect(
+ space_and_clip.spatial_id,
+ complex_clip,
+ );
+ let clip_chain_id = builder.define_clip_chain(None, [clip_id]);
+
+ // Fill it with a white rect
+ builder.push_rect(
+ &CommonItemProperties::new(
+ LayoutRect::from_size(bounds.size()),
+ SpaceAndClipInfo {
+ spatial_id,
+ clip_chain_id,
+ }
+ ),
+ LayoutRect::from_size(bounds.size()),
+ color,
+ );
+
+ builder.pop_stacking_context();
+ builder.pop_reference_frame();
+ }
+}
+
+impl Example for App {
+ const WIDTH: u32 = 2048;
+ const HEIGHT: u32 = 1536;
+
+ fn render(
+ &mut self,
+ _api: &mut RenderApi,
+ builder: &mut DisplayListBuilder,
+ _txn: &mut Transaction,
+ _device_size: DeviceIntSize,
+ pipeline_id: PipelineId,
+ _document_id: DocumentId,
+ ) {
+ let opacity_key = self.opacity_key;
+
+ let bounds = (150, 150).to(250, 250);
+ let key0 = self.property_key0;
+ self.add_rounded_rect(
+ bounds,
+ ColorF::new(1.0, 0.0, 0.0, 0.5),
+ builder,
+ pipeline_id,
+ key0,
+ Some(opacity_key),
+ SpatialTreeItemKey::new(0, 0)
+ );
+
+ let bounds = (400, 400).to(600, 600);
+ let key1 = self.property_key1;
+ self.add_rounded_rect(
+ bounds,
+ ColorF::new(0.0, 1.0, 0.0, 0.5),
+ builder,
+ pipeline_id,
+ key1,
+ None,
+ SpatialTreeItemKey::new(0, 1)
+ );
+
+ let bounds = (200, 500).to(350, 580);
+ let key2 = self.property_key2;
+ self.add_rounded_rect(
+ bounds,
+ ColorF::new(0.0, 0.0, 1.0, 0.5),
+ builder,
+ pipeline_id,
+ key2,
+ None,
+ SpatialTreeItemKey::new(0, 2)
+ );
+ }
+
+ fn on_event(
+ &mut self,
+ win_event: winit::event::WindowEvent,
+ _window: &winit::window::Window,
+ api: &mut RenderApi,
+ document_id: DocumentId
+ ) -> bool {
+ let mut rebuild_display_list = false;
+
+ match win_event {
+ winit::event::WindowEvent::KeyboardInput {
+ input: winit::event::KeyboardInput {
+ state: winit::event::ElementState::Pressed,
+ virtual_keycode: Some(key),
+ ..
+ },
+ ..
+ } => {
+ let (delta_angle, delta_opacity) = match key {
+ winit::event::VirtualKeyCode::Down => (0.0, -0.1),
+ winit::event::VirtualKeyCode::Up => (0.0, 0.1),
+ winit::event::VirtualKeyCode::Right => (1.0, 0.0),
+ winit::event::VirtualKeyCode::Left => (-1.0, 0.0),
+ winit::event::VirtualKeyCode::R => {
+ rebuild_display_list = true;
+ (0.0, 0.0)
+ }
+ _ => return false,
+ };
+ // Update the transform based on the keyboard input and push it to
+ // webrender using the generate_frame API. This will recomposite with
+ // the updated transform.
+ self.opacity += delta_opacity;
+ self.angle0 += delta_angle * 0.1;
+ self.angle1 += delta_angle * 0.2;
+ self.angle2 -= delta_angle * 0.15;
+ let xf0 = LayoutTransform::rotation(0.0, 0.0, 1.0, Angle::radians(self.angle0));
+ let xf1 = LayoutTransform::rotation(0.0, 0.0, 1.0, Angle::radians(self.angle1));
+ let xf2 = LayoutTransform::rotation(0.0, 0.0, 1.0, Angle::radians(self.angle2));
+ let mut txn = Transaction::new();
+ txn.reset_dynamic_properties();
+ txn.append_dynamic_properties(
+ DynamicProperties {
+ transforms: vec![
+ PropertyValue {
+ key: self.property_key0,
+ value: xf0,
+ },
+ PropertyValue {
+ key: self.property_key1,
+ value: xf1,
+ },
+ PropertyValue {
+ key: self.property_key2,
+ value: xf2,
+ },
+ ],
+ floats: vec![
+ PropertyValue {
+ key: self.opacity_key,
+ value: self.opacity,
+ }
+ ],
+ colors: vec![],
+ },
+ );
+ txn.generate_frame(0, RenderReasons::empty());
+ api.send_transaction(document_id, txn);
+ }
+ _ => (),
+ }
+
+ rebuild_display_list
+ }
+}
+
+fn main() {
+ let mut app = App {
+ property_key0: PropertyBindingKey::new(42), // arbitrary magic number
+ property_key1: PropertyBindingKey::new(44), // arbitrary magic number
+ property_key2: PropertyBindingKey::new(45), // arbitrary magic number
+ opacity_key: PropertyBindingKey::new(43),
+ opacity: 0.5,
+ angle0: 0.0,
+ angle1: 0.0,
+ angle2: 0.0,
+ };
+ boilerplate::main_wrapper(&mut app, None);
+}
diff --git a/gfx/wr/examples/basic.rs b/gfx/wr/examples/basic.rs
new file mode 100644
index 0000000000..53f3d9443d
--- /dev/null
+++ b/gfx/wr/examples/basic.rs
@@ -0,0 +1,130 @@
+/* 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/. */
+
+extern crate euclid;
+extern crate gleam;
+extern crate glutin;
+extern crate webrender;
+extern crate winit;
+
+#[path = "common/boilerplate.rs"]
+mod boilerplate;
+
+use crate::boilerplate::{Example, HandyDandyRectBuilder};
+use euclid::vec2;
+use webrender::ShaderPrecacheFlags;
+use webrender::api::*;
+use webrender::render_api::*;
+use webrender::api::units::*;
+
+fn main() {
+ let mut app = App {
+ };
+ boilerplate::main_wrapper(&mut app, None);
+}
+
+struct App {
+}
+
+impl Example for App {
+ // Make this the only example to test all shaders for compile errors.
+ const PRECACHE_SHADER_FLAGS: ShaderPrecacheFlags = ShaderPrecacheFlags::FULL_COMPILE;
+
+ fn render(
+ &mut self,
+ _api: &mut RenderApi,
+ builder: &mut DisplayListBuilder,
+ _txn: &mut Transaction,
+ _: DeviceIntSize,
+ pipeline_id: PipelineId,
+ _document_id: DocumentId,
+ ) {
+ let content_bounds = LayoutRect::from_size(LayoutSize::new(800.0, 600.0));
+ let root_space_and_clip = SpaceAndClipInfo::root_scroll(pipeline_id);
+ let spatial_id = root_space_and_clip.spatial_id;
+
+ builder.push_simple_stacking_context(
+ content_bounds.min,
+ spatial_id,
+ PrimitiveFlags::IS_BACKFACE_VISIBLE,
+ );
+
+ let complex = ComplexClipRegion::new(
+ (50, 50).to(150, 150),
+ BorderRadius::uniform(20.0),
+ ClipMode::Clip
+ );
+ let clip_id = builder.define_clip_rounded_rect(
+ root_space_and_clip.spatial_id,
+ complex,
+ );
+ let clip_chain_id = builder.define_clip_chain(None, [clip_id]);
+
+ builder.push_rect(
+ &CommonItemProperties::new(
+ (100, 100).to(200, 200),
+ SpaceAndClipInfo { spatial_id, clip_chain_id },
+ ),
+ (100, 100).to(200, 200),
+ ColorF::new(0.0, 1.0, 0.0, 1.0),
+ );
+
+ builder.push_rect(
+ &CommonItemProperties::new(
+ (250, 100).to(350, 200),
+ SpaceAndClipInfo { spatial_id, clip_chain_id },
+ ),
+ (250, 100).to(350, 200),
+ ColorF::new(0.0, 1.0, 0.0, 1.0),
+ );
+ let border_side = BorderSide {
+ color: ColorF::new(0.0, 0.0, 1.0, 1.0),
+ style: BorderStyle::Groove,
+ };
+ let border_widths = LayoutSideOffsets::new_all_same(10.0);
+ let border_details = BorderDetails::Normal(NormalBorder {
+ top: border_side,
+ right: border_side,
+ bottom: border_side,
+ left: border_side,
+ radius: BorderRadius::uniform(20.0),
+ do_aa: true,
+ });
+
+ let bounds = (100, 100).to(200, 200);
+ builder.push_border(
+ &CommonItemProperties::new(
+ bounds,
+ SpaceAndClipInfo { spatial_id, clip_chain_id },
+ ),
+ bounds,
+ border_widths,
+ border_details,
+ );
+
+ if false {
+ // draw box shadow?
+ let simple_box_bounds = (20, 200).by(50, 50);
+ let offset = vec2(10.0, 10.0);
+ let color = ColorF::new(1.0, 1.0, 1.0, 1.0);
+ let blur_radius = 0.0;
+ let spread_radius = 0.0;
+ let simple_border_radius = 8.0;
+ let box_shadow_type = BoxShadowClipMode::Inset;
+
+ builder.push_box_shadow(
+ &CommonItemProperties::new(content_bounds, root_space_and_clip),
+ simple_box_bounds,
+ offset,
+ color,
+ blur_radius,
+ spread_radius,
+ BorderRadius::uniform(simple_border_radius),
+ box_shadow_type,
+ );
+ }
+
+ builder.pop_stacking_context();
+ }
+}
diff --git a/gfx/wr/examples/blob.rs b/gfx/wr/examples/blob.rs
new file mode 100644
index 0000000000..206944c1fb
--- /dev/null
+++ b/gfx/wr/examples/blob.rs
@@ -0,0 +1,290 @@
+/* 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/. */
+
+extern crate gleam;
+extern crate glutin;
+extern crate rayon;
+extern crate webrender;
+extern crate winit;
+
+#[path = "common/boilerplate.rs"]
+mod boilerplate;
+
+use crate::boilerplate::{Example, HandyDandyRectBuilder};
+use rayon::{ThreadPool, ThreadPoolBuilder};
+use rayon::prelude::*;
+use std::collections::HashMap;
+use std::sync::Arc;
+use webrender::api::{self, DisplayListBuilder, DocumentId, PipelineId, PrimitiveFlags};
+use webrender::api::{ColorF, CommonItemProperties, SpaceAndClipInfo, ImageDescriptorFlags};
+use webrender::api::units::*;
+use webrender::render_api::*;
+use webrender::euclid::size2;
+
+// This example shows how to implement a very basic BlobImageHandler that can only render
+// a checkerboard pattern.
+
+// The deserialized command list internally used by this example is just a color.
+type ImageRenderingCommands = api::ColorU;
+
+// Serialize/deserialize the blob.
+// For real usecases you should probably use serde rather than doing it by hand.
+
+fn serialize_blob(color: api::ColorU) -> Arc<Vec<u8>> {
+ Arc::new(vec![color.r, color.g, color.b, color.a])
+}
+
+fn deserialize_blob(blob: &[u8]) -> Result<ImageRenderingCommands, ()> {
+ let mut iter = blob.iter();
+ return match (iter.next(), iter.next(), iter.next(), iter.next()) {
+ (Some(&r), Some(&g), Some(&b), Some(&a)) => Ok(api::ColorU::new(r, g, b, a)),
+ (Some(&a), None, None, None) => Ok(api::ColorU::new(a, a, a, a)),
+ _ => Err(()),
+ };
+}
+
+// This is the function that applies the deserialized drawing commands and generates
+// actual image data.
+fn render_blob(
+ commands: Arc<ImageRenderingCommands>,
+ descriptor: &api::BlobImageDescriptor,
+ tile: TileOffset,
+) -> api::BlobImageResult {
+ let color = *commands;
+
+ // Note: This implementation ignores the dirty rect which isn't incorrect
+ // but is a missed optimization.
+
+ // Allocate storage for the result. Right now the resource cache expects the
+ // tiles to have have no stride or offset.
+ let bpp = 4;
+ let mut texels = Vec::with_capacity((descriptor.rect.area() * bpp) 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 [w, h] = descriptor.rect.size().to_array();
+ let offset = descriptor.rect.min;
+
+ for y in 0..h {
+ for x in 0..w {
+ // 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 + offset.x;
+ let y2 = y + offset.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 {
+ api::ImageFormat::BGRA8 => {
+ texels.push(color.b * checker + tc);
+ texels.push(color.g * checker + tc);
+ texels.push(color.r * checker + tc);
+ texels.push(color.a * checker + tc);
+ }
+ api::ImageFormat::R8 => {
+ texels.push(color.a * checker + tc);
+ }
+ _ => {
+ return Err(api::BlobImageError::Other(
+ format!("Unsupported image format"),
+ ));
+ }
+ }
+ }
+ }
+
+ Ok(api::RasterizedBlobImage {
+ data: Arc::new(texels),
+ rasterized_rect: size2(w, h).into(),
+ })
+}
+
+struct CheckerboardRenderer {
+ // We are going to defer the rendering work to worker threads.
+ // Using a pre-built Arc<ThreadPool> rather than creating our own threads
+ // makes it possible to share the same thread pool as the glyph renderer (if we
+ // want to).
+ workers: Arc<ThreadPool>,
+
+ // The deserialized drawing commands.
+ // In this example we store them in Arcs. This isn't necessary since in this simplified
+ // case the command list is a simple 32 bits value and would be cheap to clone before sending
+ // to the workers. But in a more realistic scenario the commands would typically be bigger
+ // and more expensive to clone, so let's pretend it is also the case here.
+ image_cmds: HashMap<api::BlobImageKey, Arc<ImageRenderingCommands>>,
+}
+
+impl CheckerboardRenderer {
+ fn new(workers: Arc<ThreadPool>) -> Self {
+ CheckerboardRenderer {
+ image_cmds: HashMap::new(),
+ workers,
+ }
+ }
+}
+
+impl api::BlobImageHandler for CheckerboardRenderer {
+ fn create_similar(&self) -> Box<dyn api::BlobImageHandler> {
+ Box::new(CheckerboardRenderer::new(Arc::clone(&self.workers)))
+ }
+
+ fn add(&mut self, key: api::BlobImageKey, cmds: Arc<api::BlobImageData>,
+ _visible_rect: &DeviceIntRect, _: api::TileSize) {
+ self.image_cmds
+ .insert(key, Arc::new(deserialize_blob(&cmds[..]).unwrap()));
+ }
+
+ fn update(&mut self, key: api::BlobImageKey, cmds: Arc<api::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
+ .insert(key, Arc::new(deserialize_blob(&cmds[..]).unwrap()));
+ }
+
+ fn delete(&mut self, key: api::BlobImageKey) {
+ self.image_cmds.remove(&key);
+ }
+
+ fn prepare_resources(
+ &mut self,
+ _services: &dyn api::BlobImageResources,
+ _requests: &[api::BlobImageParams],
+ ) {}
+
+ fn enable_multithreading(&mut self, _: bool) {}
+ fn delete_font(&mut self, _font: api::FontKey) {}
+ fn delete_font_instance(&mut self, _instance: api::FontInstanceKey) {}
+ fn clear_namespace(&mut self, _namespace: api::IdNamespace) {}
+ fn create_blob_rasterizer(&mut self) -> Box<dyn api::AsyncBlobImageRasterizer> {
+ Box::new(Rasterizer {
+ workers: Arc::clone(&self.workers),
+ image_cmds: self.image_cmds.clone(),
+ })
+ }
+}
+
+struct Rasterizer {
+ workers: Arc<ThreadPool>,
+ image_cmds: HashMap<api::BlobImageKey, Arc<ImageRenderingCommands>>,
+}
+
+impl api::AsyncBlobImageRasterizer for Rasterizer {
+ fn rasterize(
+ &mut self,
+ requests: &[api::BlobImageParams],
+ _low_priority: bool
+ ) -> Vec<(api::BlobImageRequest, api::BlobImageResult)> {
+ let requests: Vec<(&api::BlobImageParams, Arc<ImageRenderingCommands>)> = requests.into_iter().map(|params| {
+ (params, Arc::clone(&self.image_cmds[&params.request.key]))
+ }).collect();
+
+ self.workers.install(|| {
+ requests.into_par_iter().map(|(params, commands)| {
+ (params.request, render_blob(commands, &params.descriptor, params.request.tile))
+ }).collect()
+ })
+ }
+}
+
+struct App {}
+
+impl Example for App {
+ fn render(
+ &mut self,
+ api: &mut RenderApi,
+ builder: &mut DisplayListBuilder,
+ txn: &mut Transaction,
+ _device_size: DeviceIntSize,
+ pipeline_id: PipelineId,
+ _document_id: DocumentId,
+ ) {
+ let space_and_clip = SpaceAndClipInfo::root_scroll(pipeline_id);
+
+ builder.push_simple_stacking_context(
+ LayoutPoint::zero(),
+ space_and_clip.spatial_id,
+ PrimitiveFlags::IS_BACKFACE_VISIBLE,
+ );
+
+ let size1 = DeviceIntSize::new(500, 500);
+ let blob_img1 = api.generate_blob_image_key();
+ txn.add_blob_image(
+ blob_img1,
+ api::ImageDescriptor::new(
+ size1.width,
+ size1.height,
+ api::ImageFormat::BGRA8,
+ ImageDescriptorFlags::IS_OPAQUE,
+ ),
+ serialize_blob(api::ColorU::new(50, 50, 150, 255)),
+ size1.into(),
+ Some(128),
+ );
+ let bounds = (30, 30).by(size1.width, size1.height);
+ builder.push_image(
+ &CommonItemProperties::new(bounds, space_and_clip),
+ bounds,
+ api::ImageRendering::Auto,
+ api::AlphaType::PremultipliedAlpha,
+ blob_img1.as_image(),
+ ColorF::WHITE,
+ );
+
+ let size2 = DeviceIntSize::new(256, 256);
+ let blob_img2 = api.generate_blob_image_key();
+ txn.add_blob_image(
+ blob_img2,
+ api::ImageDescriptor::new(
+ size2.width,
+ size2.height,
+ api::ImageFormat::BGRA8,
+ ImageDescriptorFlags::IS_OPAQUE,
+ ),
+ serialize_blob(api::ColorU::new(50, 150, 50, 255)),
+ size2.into(),
+ None,
+ );
+ let bounds = (600, 600).by(size2.width, size2.height);
+ builder.push_image(
+ &CommonItemProperties::new(bounds, space_and_clip),
+ bounds,
+ api::ImageRendering::Auto,
+ api::AlphaType::PremultipliedAlpha,
+ blob_img2.as_image(),
+ ColorF::WHITE,
+ );
+
+ builder.pop_stacking_context();
+ }
+}
+
+fn main() {
+ let workers =
+ ThreadPoolBuilder::new().thread_name(|idx| format!("WebRender:Worker#{}", idx))
+ .build();
+
+ let workers = Arc::new(workers.unwrap());
+
+ let opts = webrender::WebRenderOptions {
+ workers: Some(Arc::clone(&workers)),
+ // Register our blob renderer, so that WebRender integrates it in the resource cache..
+ // Share the same pool of worker threads between WebRender and our blob renderer.
+ blob_image_handler: Some(Box::new(CheckerboardRenderer::new(Arc::clone(&workers)))),
+ ..Default::default()
+ };
+
+ let mut app = App {};
+
+ boilerplate::main_wrapper(&mut app, Some(opts));
+}
diff --git a/gfx/wr/examples/common/boilerplate.rs b/gfx/wr/examples/common/boilerplate.rs
new file mode 100644
index 0000000000..11f153f6e8
--- /dev/null
+++ b/gfx/wr/examples/common/boilerplate.rs
@@ -0,0 +1,323 @@
+/* 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 gleam::gl;
+use glutin;
+use std::env;
+use std::path::PathBuf;
+use webrender;
+use winit;
+use winit::platform::run_return::EventLoopExtRunReturn;
+use webrender::{DebugFlags, ShaderPrecacheFlags};
+use webrender::api::*;
+use webrender::render_api::*;
+use webrender::api::units::*;
+
+struct Notifier {
+ events_proxy: winit::event_loop::EventLoopProxy<()>,
+}
+
+impl Notifier {
+ fn new(events_proxy: winit::event_loop::EventLoopProxy<()>) -> Notifier {
+ Notifier { events_proxy }
+ }
+}
+
+impl RenderNotifier for Notifier {
+ fn clone(&self) -> Box<dyn RenderNotifier> {
+ Box::new(Notifier {
+ events_proxy: self.events_proxy.clone(),
+ })
+ }
+
+ fn wake_up(&self, _composite_needed: bool) {
+ #[cfg(not(target_os = "android"))]
+ let _ = self.events_proxy.send_event(());
+ }
+
+ fn new_frame_ready(&self,
+ _: DocumentId,
+ _scrolled: bool,
+ composite_needed: bool,
+ _: FramePublishId) {
+ self.wake_up(composite_needed);
+ }
+}
+
+pub trait HandyDandyRectBuilder {
+ fn to(&self, x2: i32, y2: i32) -> LayoutRect;
+ fn by(&self, w: i32, h: i32) -> LayoutRect;
+}
+// Allows doing `(x, y).to(x2, y2)` or `(x, y).by(width, height)` with i32
+// values to build a f32 LayoutRect
+impl HandyDandyRectBuilder for (i32, i32) {
+ fn to(&self, x2: i32, y2: i32) -> LayoutRect {
+ LayoutRect::from_origin_and_size(
+ LayoutPoint::new(self.0 as f32, self.1 as f32),
+ LayoutSize::new((x2 - self.0) as f32, (y2 - self.1) as f32),
+ )
+ }
+
+ fn by(&self, w: i32, h: i32) -> LayoutRect {
+ LayoutRect::from_origin_and_size(
+ LayoutPoint::new(self.0 as f32, self.1 as f32),
+ LayoutSize::new(w as f32, h as f32),
+ )
+ }
+}
+
+pub trait Example {
+ const TITLE: &'static str = "WebRender Sample App";
+ const PRECACHE_SHADER_FLAGS: ShaderPrecacheFlags = ShaderPrecacheFlags::EMPTY;
+ const WIDTH: u32 = 1920;
+ const HEIGHT: u32 = 1080;
+
+ fn render(
+ &mut self,
+ api: &mut RenderApi,
+ builder: &mut DisplayListBuilder,
+ txn: &mut Transaction,
+ device_size: DeviceIntSize,
+ pipeline_id: PipelineId,
+ document_id: DocumentId,
+ );
+ fn on_event(
+ &mut self,
+ _: winit::event::WindowEvent,
+ _: &winit::window::Window,
+ _: &mut RenderApi,
+ _: DocumentId,
+ ) -> bool {
+ false
+ }
+ fn get_image_handler(
+ &mut self,
+ _gl: &dyn gl::Gl,
+ ) -> Option<Box<dyn ExternalImageHandler>> {
+ None
+ }
+ fn draw_custom(&mut self, _gl: &dyn gl::Gl) {
+ }
+}
+
+pub fn main_wrapper<E: Example>(
+ example: &mut E,
+ options: Option<webrender::WebRenderOptions>,
+) {
+ env_logger::init();
+
+ #[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(),
+ );
+ }
+
+ let args: Vec<String> = env::args().collect();
+ let res_path = if args.len() > 1 {
+ Some(PathBuf::from(&args[1]))
+ } else {
+ None
+ };
+
+ let mut events_loop = winit::event_loop::EventLoop::new();
+ let window_builder = winit::window::WindowBuilder::new()
+ .with_title(E::TITLE)
+ .with_inner_size(winit::dpi::LogicalSize::new(E::WIDTH as f64, E::HEIGHT as f64));
+ let windowed_context = glutin::ContextBuilder::new()
+ .with_gl(glutin::GlRequest::GlThenGles {
+ opengl_version: (3, 2),
+ opengles_version: (3, 0),
+ })
+ .build_windowed(window_builder, &events_loop)
+ .unwrap();
+
+ let windowed_context = unsafe { windowed_context.make_current().unwrap() };
+
+ 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!(),
+ };
+
+ println!("OpenGL version {}", gl.get_string(gl::VERSION));
+ println!("Shader resource path: {:?}", res_path);
+ let device_pixel_ratio = windowed_context.window().scale_factor() as f32;
+ println!("Device pixel ratio: {}", device_pixel_ratio);
+
+ println!("Loading shaders...");
+ let mut debug_flags = DebugFlags::ECHO_DRIVER_MESSAGES | DebugFlags::TEXTURE_CACHE_DBG;
+ let opts = webrender::WebRenderOptions {
+ resource_override_path: res_path,
+ precache_flags: E::PRECACHE_SHADER_FLAGS,
+ clear_color: ColorF::new(0.3, 0.0, 0.0, 1.0),
+ debug_flags,
+ //allow_texture_swizzling: false,
+ ..options.unwrap_or(webrender::WebRenderOptions::default())
+ };
+
+ let device_size = {
+ let size = windowed_context
+ .window()
+ .inner_size();
+ DeviceIntSize::new(size.width as i32, size.height as i32)
+ };
+ let notifier = Box::new(Notifier::new(events_loop.create_proxy()));
+ let (mut renderer, sender) = webrender::create_webrender_instance(
+ gl.clone(),
+ notifier,
+ opts,
+ None,
+ ).unwrap();
+ let mut api = sender.create_api();
+ let document_id = api.add_document(device_size);
+
+ let external = example.get_image_handler(&*gl);
+
+ if let Some(external_image_handler) = external {
+ renderer.set_external_image_handler(external_image_handler);
+ }
+
+ let epoch = Epoch(0);
+ let pipeline_id = PipelineId(0, 0);
+ let mut builder = DisplayListBuilder::new(pipeline_id);
+ let mut txn = Transaction::new();
+ builder.begin();
+
+ example.render(
+ &mut api,
+ &mut builder,
+ &mut txn,
+ device_size,
+ pipeline_id,
+ document_id,
+ );
+ txn.set_display_list(
+ epoch,
+ builder.end(),
+ );
+ txn.set_root_pipeline(pipeline_id);
+ txn.generate_frame(0, RenderReasons::empty());
+ api.send_transaction(document_id, txn);
+
+ println!("Entering event loop");
+ events_loop.run_return(|global_event, _elwt, control_flow| {
+ let mut txn = Transaction::new();
+ let mut custom_event = true;
+
+ let old_flags = debug_flags;
+ let win_event = match global_event {
+ winit::event::Event::WindowEvent { event, .. } => event,
+ _ => return,
+ };
+ match win_event {
+ winit::event::WindowEvent::CloseRequested => {
+ *control_flow = winit::event_loop::ControlFlow::Exit;
+ return;
+ }
+ winit::event::WindowEvent::AxisMotion { .. } |
+ winit::event::WindowEvent::CursorMoved { .. } => {
+ custom_event = example.on_event(
+ win_event,
+ windowed_context.window(),
+ &mut api,
+ document_id,
+ );
+ // skip high-frequency events from triggering a frame draw.
+ if !custom_event {
+ return;
+ }
+ },
+ winit::event::WindowEvent::KeyboardInput {
+ input: winit::event::KeyboardInput {
+ state: winit::event::ElementState::Pressed,
+ virtual_keycode: Some(key),
+ ..
+ },
+ ..
+ } => match key {
+ winit::event::VirtualKeyCode::Escape => {
+ *control_flow = winit::event_loop::ControlFlow::Exit;
+ return;
+ }
+ winit::event::VirtualKeyCode::P => debug_flags.toggle(DebugFlags::PROFILER_DBG),
+ winit::event::VirtualKeyCode::O => debug_flags.toggle(DebugFlags::RENDER_TARGET_DBG),
+ winit::event::VirtualKeyCode::I => debug_flags.toggle(DebugFlags::TEXTURE_CACHE_DBG),
+ winit::event::VirtualKeyCode::T => debug_flags.toggle(DebugFlags::PICTURE_CACHING_DBG),
+ winit::event::VirtualKeyCode::Q => debug_flags.toggle(
+ DebugFlags::GPU_TIME_QUERIES | DebugFlags::GPU_SAMPLE_QUERIES
+ ),
+ winit::event::VirtualKeyCode::G => debug_flags.toggle(DebugFlags::GPU_CACHE_DBG),
+ winit::event::VirtualKeyCode::M => api.notify_memory_pressure(),
+ winit::event::VirtualKeyCode::C => {
+ let path: PathBuf = "../captures/example".into();
+ //TODO: switch between SCENE/FRAME capture types
+ // based on "shift" modifier, when `glutin` is updated.
+ let bits = CaptureBits::all();
+ api.save_capture(path, bits);
+ },
+ _ => {
+ custom_event = example.on_event(
+ win_event,
+ windowed_context.window(),
+ &mut api,
+ document_id,
+ )
+ },
+ },
+ other => custom_event = example.on_event(
+ other,
+ windowed_context.window(),
+ &mut api,
+ document_id,
+ ),
+ };
+
+ if debug_flags != old_flags {
+ api.send_debug_cmd(DebugCommand::SetFlags(debug_flags));
+ }
+
+ if custom_event {
+ let mut builder = DisplayListBuilder::new(pipeline_id);
+ builder.begin();
+
+ example.render(
+ &mut api,
+ &mut builder,
+ &mut txn,
+ device_size,
+ pipeline_id,
+ document_id,
+ );
+ txn.set_display_list(
+ epoch,
+ builder.end(),
+ );
+ txn.generate_frame(0, RenderReasons::empty());
+ }
+ api.send_transaction(document_id, txn);
+
+ renderer.update();
+ renderer.render(device_size, 0).unwrap();
+ let _ = renderer.flush_pipeline_info();
+ example.draw_custom(&*gl);
+ windowed_context.swap_buffers().ok();
+
+ *control_flow = winit::event_loop::ControlFlow::Wait;
+ });
+
+ renderer.deinit();
+}
diff --git a/gfx/wr/examples/common/image_helper.rs b/gfx/wr/examples/common/image_helper.rs
new file mode 100644
index 0000000000..368674c0e1
--- /dev/null
+++ b/gfx/wr/examples/common/image_helper.rs
@@ -0,0 +1,19 @@
+/* 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::api::{ImageData, ImageDescriptor, ImageFormat, ImageDescriptorFlags};
+
+pub fn make_checkerboard(width: u32, height: u32) -> (ImageDescriptor, ImageData) {
+ let mut image_data = Vec::new();
+ for y in 0 .. height {
+ for x in 0 .. width {
+ let lum = 255 * (((x & 8) == 0) ^ ((y & 8) == 0)) as u8;
+ image_data.extend_from_slice(&[lum, lum, lum, 0xff]);
+ }
+ }
+ (
+ ImageDescriptor::new(width as i32, height as i32, ImageFormat::BGRA8, ImageDescriptorFlags::IS_OPAQUE),
+ ImageData::new(image_data)
+ )
+}
diff --git a/gfx/wr/examples/document.rs b/gfx/wr/examples/document.rs
new file mode 100644
index 0000000000..24b357f528
--- /dev/null
+++ b/gfx/wr/examples/document.rs
@@ -0,0 +1,136 @@
+/* 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/. */
+
+extern crate euclid;
+extern crate gleam;
+extern crate glutin;
+extern crate webrender;
+extern crate winit;
+
+#[path = "common/boilerplate.rs"]
+mod boilerplate;
+
+use crate::boilerplate::Example;
+use euclid::Scale;
+use webrender::api::*;
+use webrender::render_api::*;
+use webrender::api::units::*;
+
+// This example creates multiple documents overlapping each other with
+// specified layer indices.
+
+struct Document {
+ id: DocumentId,
+ pipeline_id: PipelineId,
+ content_rect: LayoutRect,
+ color: ColorF,
+}
+
+struct App {
+ documents: Vec<Document>,
+}
+
+impl App {
+ fn init(
+ &mut self,
+ api: &mut RenderApi,
+ device_pixel_ratio: f32,
+ ) {
+ let init_data = vec![
+ (
+ PipelineId(1, 0),
+ ColorF::new(0.0, 1.0, 0.0, 1.0),
+ DeviceIntPoint::new(0, 0),
+ ),
+ (
+ PipelineId(2, 0),
+ ColorF::new(1.0, 1.0, 0.0, 1.0),
+ DeviceIntPoint::new(200, 0),
+ ),
+ (
+ PipelineId(3, 0),
+ ColorF::new(1.0, 0.0, 0.0, 1.0),
+ DeviceIntPoint::new(200, 200),
+ ),
+ (
+ PipelineId(4, 0),
+ ColorF::new(1.0, 0.0, 1.0, 1.0),
+ DeviceIntPoint::new(0, 200),
+ ),
+ ];
+
+ for (pipeline_id, color, offset) in init_data {
+ let size = DeviceIntSize::new(250, 250);
+ let bounds = DeviceIntRect::from_origin_and_size(offset, size);
+
+ let document_id = api.add_document(size);
+ let mut txn = Transaction::new();
+ txn.set_root_pipeline(pipeline_id);
+ api.send_transaction(document_id, txn);
+
+ self.documents.push(Document {
+ id: document_id,
+ pipeline_id,
+ content_rect: LayoutRect::from_size(
+ bounds.size().to_f32() / Scale::new(device_pixel_ratio),
+ ),
+ color,
+ });
+ }
+ }
+}
+
+impl Example for App {
+ fn render(
+ &mut self,
+ api: &mut RenderApi,
+ _base_builder: &mut DisplayListBuilder,
+ _txn: &mut Transaction,
+ _device_size: DeviceIntSize,
+ _pipeline_id: PipelineId,
+ _: DocumentId,
+ ) {
+ if self.documents.is_empty() {
+ // this is the first run, hack around the boilerplate,
+ // which assumes an example only needs one document
+ self.init(api, 1.0);
+ }
+
+ for doc in &self.documents {
+ let space_and_clip = SpaceAndClipInfo::root_scroll(doc.pipeline_id);
+ let mut builder = DisplayListBuilder::new(
+ doc.pipeline_id,
+ );
+ builder.begin();
+ let local_rect = LayoutRect::from_size(doc.content_rect.size());
+
+ builder.push_simple_stacking_context(
+ doc.content_rect.min,
+ space_and_clip.spatial_id,
+ PrimitiveFlags::IS_BACKFACE_VISIBLE,
+ );
+ builder.push_rect(
+ &CommonItemProperties::new(local_rect, space_and_clip),
+ local_rect,
+ doc.color,
+ );
+ builder.pop_stacking_context();
+
+ let mut txn = Transaction::new();
+ txn.set_display_list(
+ Epoch(0),
+ builder.end(),
+ );
+ txn.generate_frame(0, RenderReasons::empty());
+ api.send_transaction(doc.id, txn);
+ }
+ }
+}
+
+fn main() {
+ let mut app = App {
+ documents: Vec::new(),
+ };
+ boilerplate::main_wrapper(&mut app, None);
+}
diff --git a/gfx/wr/examples/iframe.rs b/gfx/wr/examples/iframe.rs
new file mode 100644
index 0000000000..8c808ad979
--- /dev/null
+++ b/gfx/wr/examples/iframe.rs
@@ -0,0 +1,99 @@
+/* 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/. */
+
+extern crate gleam;
+extern crate glutin;
+extern crate webrender;
+extern crate winit;
+
+#[path = "common/boilerplate.rs"]
+mod boilerplate;
+
+use crate::boilerplate::{Example, HandyDandyRectBuilder};
+use webrender::api::*;
+use webrender::render_api::*;
+use webrender::api::units::*;
+
+// This example uses the push_iframe API to nest a second pipeline's displaylist
+// inside the root pipeline's display list. When it works, a green square is
+// shown. If it fails, a red square is shown.
+
+struct App {}
+
+impl Example for App {
+ fn render(
+ &mut self,
+ api: &mut RenderApi,
+ builder: &mut DisplayListBuilder,
+ _txn: &mut Transaction,
+ _device_size: DeviceIntSize,
+ pipeline_id: PipelineId,
+ document_id: DocumentId,
+ ) {
+ // All the sub_* things are for the nested pipeline
+ let sub_size = DeviceIntSize::new(100, 100);
+ let sub_bounds = (0, 0).to(sub_size.width as i32, sub_size.height as i32);
+
+ let sub_pipeline_id = PipelineId(pipeline_id.0, 42);
+ let mut sub_builder = DisplayListBuilder::new(sub_pipeline_id);
+ let mut space_and_clip = SpaceAndClipInfo::root_scroll(pipeline_id);
+ sub_builder.begin();
+
+ sub_builder.push_simple_stacking_context(
+ sub_bounds.min,
+ space_and_clip.spatial_id,
+ PrimitiveFlags::IS_BACKFACE_VISIBLE,
+ );
+
+ // green rect visible == success
+ sub_builder.push_rect(
+ &CommonItemProperties::new(sub_bounds, space_and_clip),
+ sub_bounds,
+ ColorF::new(0.0, 1.0, 0.0, 1.0)
+ );
+ sub_builder.pop_stacking_context();
+
+ let mut txn = Transaction::new();
+ txn.set_display_list(
+ Epoch(0),
+ sub_builder.end(),
+ );
+ api.send_transaction(document_id, txn);
+
+ space_and_clip.spatial_id = builder.push_reference_frame(
+ sub_bounds.min,
+ space_and_clip.spatial_id,
+ TransformStyle::Flat,
+ PropertyBinding::Binding(PropertyBindingKey::new(42), LayoutTransform::identity()),
+ ReferenceFrameKind::Transform {
+ is_2d_scale_translation: false,
+ should_snap: false,
+ paired_with_perspective: false,
+ },
+ SpatialTreeItemKey::new(0, 0),
+ );
+
+ // And this is for the root pipeline
+ builder.push_simple_stacking_context(
+ sub_bounds.min,
+ space_and_clip.spatial_id,
+ PrimitiveFlags::IS_BACKFACE_VISIBLE,
+ );
+
+ // red rect under the iframe: if this is visible, things have gone wrong
+ builder.push_rect(
+ &CommonItemProperties::new(sub_bounds, space_and_clip),
+ sub_bounds,
+ ColorF::new(1.0, 0.0, 0.0, 1.0)
+ );
+ builder.push_iframe(sub_bounds, sub_bounds, &space_and_clip, sub_pipeline_id, false);
+ builder.pop_stacking_context();
+ builder.pop_reference_frame();
+ }
+}
+
+fn main() {
+ let mut app = App {};
+ boilerplate::main_wrapper(&mut app, None);
+}
diff --git a/gfx/wr/examples/image_resize.rs b/gfx/wr/examples/image_resize.rs
new file mode 100644
index 0000000000..4456b00caf
--- /dev/null
+++ b/gfx/wr/examples/image_resize.rs
@@ -0,0 +1,128 @@
+/* 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/. */
+
+extern crate gleam;
+extern crate glutin;
+extern crate webrender;
+extern crate winit;
+
+#[path = "common/boilerplate.rs"]
+mod boilerplate;
+#[path = "common/image_helper.rs"]
+mod image_helper;
+
+use crate::boilerplate::{Example, HandyDandyRectBuilder};
+use webrender::api::*;
+use webrender::render_api::*;
+use webrender::api::units::*;
+
+struct App {
+ image_key: ImageKey,
+}
+
+impl Example for App {
+ fn render(
+ &mut self,
+ _api: &mut RenderApi,
+ builder: &mut DisplayListBuilder,
+ txn: &mut Transaction,
+ _device_size: DeviceIntSize,
+ pipeline_id: PipelineId,
+ _document_id: DocumentId,
+ ) {
+ let (image_descriptor, image_data) = image_helper::make_checkerboard(32, 32);
+ txn.add_image(
+ self.image_key,
+ image_descriptor,
+ image_data,
+ None,
+ );
+
+ let bounds = (0, 0).to(512, 512);
+ let space_and_clip = SpaceAndClipInfo::root_scroll(pipeline_id);
+
+ builder.push_simple_stacking_context(
+ bounds.min,
+ space_and_clip.spatial_id,
+ PrimitiveFlags::IS_BACKFACE_VISIBLE,
+ );
+
+ let image_size = LayoutSize::new(100.0, 100.0);
+
+ builder.push_image(
+ &CommonItemProperties::new(
+ LayoutRect::from_origin_and_size(LayoutPoint::new(100.0, 100.0), image_size),
+ space_and_clip,
+ ),
+ bounds,
+ ImageRendering::Auto,
+ AlphaType::PremultipliedAlpha,
+ self.image_key,
+ ColorF::WHITE,
+ );
+
+ builder.push_image(
+ &CommonItemProperties::new(
+ LayoutRect::from_origin_and_size(LayoutPoint::new(250.0, 100.0), image_size),
+ space_and_clip,
+ ),
+ bounds,
+ ImageRendering::Pixelated,
+ AlphaType::PremultipliedAlpha,
+ self.image_key,
+ ColorF::WHITE,
+ );
+
+ builder.pop_stacking_context();
+ }
+
+ fn on_event(
+ &mut self,
+ event: winit::event::WindowEvent,
+ _window: &winit::window::Window,
+ api: &mut RenderApi,
+ document_id: DocumentId,
+ ) -> bool {
+ match event {
+ winit::event::WindowEvent::KeyboardInput {
+ input: winit::event::KeyboardInput {
+ state: winit::event::ElementState::Pressed,
+ virtual_keycode: Some(winit::event::VirtualKeyCode::Space),
+ ..
+ },
+ ..
+ } => {
+ let mut image_data = Vec::new();
+ for y in 0 .. 64 {
+ for x in 0 .. 64 {
+ let r = 255 * ((y & 32) == 0) as u8;
+ let g = 255 * ((x & 32) == 0) as u8;
+ image_data.extend_from_slice(&[0, g, r, 0xff]);
+ }
+ }
+
+ let mut txn = Transaction::new();
+ txn.update_image(
+ self.image_key,
+ ImageDescriptor::new(64, 64, ImageFormat::BGRA8, ImageDescriptorFlags::IS_OPAQUE),
+ ImageData::new(image_data),
+ &DirtyRect::All,
+ );
+ let mut txn = Transaction::new();
+ txn.generate_frame(0, RenderReasons::empty());
+ api.send_transaction(document_id, txn);
+ }
+ _ => {}
+ }
+
+ false
+ }
+}
+
+fn main() {
+ let mut app = App {
+ image_key: ImageKey(IdNamespace(0), 0),
+ };
+ boilerplate::main_wrapper(&mut app, None);
+}
diff --git a/gfx/wr/examples/multiwindow.rs b/gfx/wr/examples/multiwindow.rs
new file mode 100644
index 0000000000..2f2ff02b46
--- /dev/null
+++ b/gfx/wr/examples/multiwindow.rs
@@ -0,0 +1,321 @@
+/* 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/. */
+
+extern crate euclid;
+extern crate gleam;
+extern crate glutin;
+extern crate webrender;
+extern crate winit;
+
+use gleam::gl;
+use glutin::NotCurrent;
+use std::fs::File;
+use std::io::Read;
+use webrender::api::*;
+use webrender::api::units::*;
+use webrender::render_api::*;
+use webrender::DebugFlags;
+use winit::dpi::LogicalSize;
+use winit::platform::run_return::EventLoopExtRunReturn;
+
+struct Notifier {
+ events_proxy: winit::event_loop::EventLoopProxy<()>,
+}
+
+impl Notifier {
+ fn new(events_proxy: winit::event_loop::EventLoopProxy<()>) -> Notifier {
+ Notifier { events_proxy }
+ }
+}
+
+impl RenderNotifier for Notifier {
+ fn clone(&self) -> Box<dyn RenderNotifier> {
+ Box::new(Notifier {
+ events_proxy: self.events_proxy.clone(),
+ })
+ }
+
+ fn wake_up(&self, _composite_needed: bool) {
+ #[cfg(not(target_os = "android"))]
+ let _ = self.events_proxy.send_event(());
+ }
+
+ fn new_frame_ready(&self,
+ _: DocumentId,
+ _scrolled: bool,
+ composite_needed: bool,
+ _: FramePublishId) {
+ self.wake_up(composite_needed);
+ }
+}
+
+struct Window {
+ events_loop: winit::event_loop::EventLoop<()>, //TODO: share events loop?
+ context: Option<glutin::WindowedContext<NotCurrent>>,
+ renderer: webrender::Renderer,
+ name: &'static str,
+ pipeline_id: PipelineId,
+ document_id: DocumentId,
+ epoch: Epoch,
+ api: RenderApi,
+ font_instance_key: FontInstanceKey,
+}
+
+impl Window {
+ fn new(name: &'static str, clear_color: ColorF) -> Self {
+ let events_loop = winit::event_loop::EventLoop::new();
+ let window_builder = winit::window::WindowBuilder::new()
+ .with_title(name)
+ .with_inner_size(LogicalSize::new(800. as f64, 600. as f64));
+ let context = glutin::ContextBuilder::new()
+ .with_gl(glutin::GlRequest::GlThenGles {
+ opengl_version: (3, 2),
+ opengles_version: (3, 0),
+ })
+ .build_windowed(window_builder, &events_loop)
+ .unwrap();
+
+ let context = unsafe { context.make_current().unwrap() };
+
+ 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!(),
+ };
+
+ let opts = webrender::WebRenderOptions {
+ clear_color,
+ ..webrender::WebRenderOptions::default()
+ };
+
+ let device_size = {
+ let size = context
+ .window()
+ .inner_size();
+ DeviceIntSize::new(size.width as i32, size.height as i32)
+ };
+ let notifier = Box::new(Notifier::new(events_loop.create_proxy()));
+ let (renderer, sender) = webrender::create_webrender_instance(gl.clone(), notifier, opts, None).unwrap();
+ let mut api = sender.create_api();
+ let document_id = api.add_document(device_size);
+
+ let epoch = Epoch(0);
+ let pipeline_id = PipelineId(0, 0);
+ let mut txn = Transaction::new();
+
+ let font_key = api.generate_font_key();
+ let font_bytes = load_file("../wrench/reftests/text/FreeSans.ttf");
+ txn.add_raw_font(font_key, font_bytes, 0);
+
+ let font_instance_key = api.generate_font_instance_key();
+ txn.add_font_instance(font_instance_key, font_key, 32.0, None, None, Vec::new());
+
+ api.send_transaction(document_id, txn);
+
+ Window {
+ events_loop,
+ context: Some(unsafe { context.make_not_current().unwrap() }),
+ renderer,
+ name,
+ epoch,
+ pipeline_id,
+ document_id,
+ api,
+ font_instance_key,
+ }
+ }
+
+ fn tick(&mut self) -> bool {
+ let mut do_exit = false;
+ let my_name = &self.name;
+ let renderer = &mut self.renderer;
+ let api = &mut self.api;
+
+ self.events_loop.run_return(|global_event, _elwt, control_flow| {
+ *control_flow = winit::event_loop::ControlFlow::Exit;
+ match global_event {
+ winit::event::Event::WindowEvent { event, .. } => match event {
+ winit::event::WindowEvent::CloseRequested |
+ winit::event::WindowEvent::KeyboardInput {
+ input: winit::event::KeyboardInput {
+ virtual_keycode: Some(winit::event::VirtualKeyCode::Escape),
+ ..
+ },
+ ..
+ } => {
+ do_exit = true
+ }
+ winit::event::WindowEvent::KeyboardInput {
+ input: winit::event::KeyboardInput {
+ state: winit::event::ElementState::Pressed,
+ virtual_keycode: Some(winit::event::VirtualKeyCode::P),
+ ..
+ },
+ ..
+ } => {
+ println!("set flags {}", my_name);
+ api.send_debug_cmd(DebugCommand::SetFlags(DebugFlags::PROFILER_DBG))
+ }
+ _ => {}
+ }
+ _ => {}
+ }
+ });
+ if do_exit {
+ return true
+ }
+
+ let context = unsafe { self.context.take().unwrap().make_current().unwrap() };
+ let device_pixel_ratio = context.window().scale_factor() as f32;
+ let device_size = {
+ let size = context
+ .window()
+ .inner_size();
+ DeviceIntSize::new(size.width as i32, size.height as i32)
+ };
+ let layout_size = device_size.to_f32() / euclid::Scale::new(device_pixel_ratio);
+ let mut txn = Transaction::new();
+ let mut builder = DisplayListBuilder::new(self.pipeline_id);
+ let space_and_clip = SpaceAndClipInfo::root_scroll(self.pipeline_id);
+ builder.begin();
+
+ let bounds = LayoutRect::from_size(layout_size);
+ builder.push_simple_stacking_context(
+ bounds.min,
+ space_and_clip.spatial_id,
+ PrimitiveFlags::IS_BACKFACE_VISIBLE,
+ );
+
+ builder.push_rect(
+ &CommonItemProperties::new(
+ LayoutRect::from_origin_and_size(
+ LayoutPoint::new(100.0, 200.0),
+ LayoutSize::new(100.0, 200.0),
+ ),
+ space_and_clip,
+ ),
+ LayoutRect::from_origin_and_size(
+ LayoutPoint::new(100.0, 200.0),
+ LayoutSize::new(100.0, 200.0),
+ ),
+ ColorF::new(0.0, 1.0, 0.0, 1.0));
+
+ let text_bounds = LayoutRect::from_origin_and_size(
+ LayoutPoint::new(100.0, 50.0),
+ LayoutSize::new(700.0, 200.0)
+ );
+ let glyphs = vec![
+ GlyphInstance {
+ index: 48,
+ point: LayoutPoint::new(100.0, 100.0),
+ },
+ GlyphInstance {
+ index: 68,
+ point: LayoutPoint::new(150.0, 100.0),
+ },
+ GlyphInstance {
+ index: 80,
+ point: LayoutPoint::new(200.0, 100.0),
+ },
+ GlyphInstance {
+ index: 82,
+ point: LayoutPoint::new(250.0, 100.0),
+ },
+ GlyphInstance {
+ index: 81,
+ point: LayoutPoint::new(300.0, 100.0),
+ },
+ GlyphInstance {
+ index: 3,
+ point: LayoutPoint::new(350.0, 100.0),
+ },
+ GlyphInstance {
+ index: 86,
+ point: LayoutPoint::new(400.0, 100.0),
+ },
+ GlyphInstance {
+ index: 79,
+ point: LayoutPoint::new(450.0, 100.0),
+ },
+ GlyphInstance {
+ index: 72,
+ point: LayoutPoint::new(500.0, 100.0),
+ },
+ GlyphInstance {
+ index: 83,
+ point: LayoutPoint::new(550.0, 100.0),
+ },
+ GlyphInstance {
+ index: 87,
+ point: LayoutPoint::new(600.0, 100.0),
+ },
+ GlyphInstance {
+ index: 17,
+ point: LayoutPoint::new(650.0, 100.0),
+ },
+ ];
+
+ builder.push_text(
+ &CommonItemProperties::new(
+ text_bounds,
+ space_and_clip,
+ ),
+ text_bounds,
+ &glyphs,
+ self.font_instance_key,
+ ColorF::new(1.0, 1.0, 0.0, 1.0),
+ None,
+ );
+
+ builder.pop_stacking_context();
+
+ txn.set_display_list(
+ self.epoch,
+ builder.end(),
+ );
+ txn.set_root_pipeline(self.pipeline_id);
+ txn.generate_frame(0, RenderReasons::empty());
+ api.send_transaction(self.document_id, txn);
+
+ renderer.update();
+ renderer.render(device_size, 0).unwrap();
+ context.swap_buffers().ok();
+
+ self.context = Some(unsafe { context.make_not_current().unwrap() });
+
+ false
+ }
+
+ fn deinit(self) {
+ self.renderer.deinit();
+ }
+}
+
+fn main() {
+ let mut win1 = Window::new("window1", ColorF::new(0.3, 0.0, 0.0, 1.0));
+ let mut win2 = Window::new("window2", ColorF::new(0.0, 0.3, 0.0, 1.0));
+
+ loop {
+ if win1.tick() {
+ break;
+ }
+ if win2.tick() {
+ break;
+ }
+ }
+
+ win1.deinit();
+ win2.deinit();
+}
+
+fn load_file(name: &str) -> Vec<u8> {
+ let mut file = File::open(name).unwrap();
+ let mut buffer = vec![];
+ file.read_to_end(&mut buffer).unwrap();
+ buffer
+}
diff --git a/gfx/wr/examples/scrolling.rs b/gfx/wr/examples/scrolling.rs
new file mode 100644
index 0000000000..315b945d20
--- /dev/null
+++ b/gfx/wr/examples/scrolling.rs
@@ -0,0 +1,298 @@
+/* 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/. */
+
+extern crate euclid;
+extern crate gleam;
+extern crate glutin;
+extern crate webrender;
+extern crate winit;
+
+#[path = "common/boilerplate.rs"]
+mod boilerplate;
+
+use crate::boilerplate::{Example, HandyDandyRectBuilder};
+use euclid::SideOffsets2D;
+use webrender::api::*;
+use webrender::render_api::*;
+use webrender::api::units::*;
+use winit::dpi::LogicalPosition;
+
+
+const EXT_SCROLL_ID_ROOT: u64 = 1;
+const EXT_SCROLL_ID_CONTENT: u64 = 2;
+
+struct App {
+ cursor_position: WorldPoint,
+ scroll_offset: LayoutVector2D,
+}
+
+impl Example for App {
+ fn render(
+ &mut self,
+ _api: &mut RenderApi,
+ builder: &mut DisplayListBuilder,
+ _txn: &mut Transaction,
+ _device_size: DeviceIntSize,
+ pipeline_id: PipelineId,
+ _document_id: DocumentId,
+ ) {
+ let root_space_and_clip = SpaceAndClipInfo::root_scroll(pipeline_id);
+ builder.push_simple_stacking_context(
+ LayoutPoint::zero(),
+ root_space_and_clip.spatial_id,
+ PrimitiveFlags::IS_BACKFACE_VISIBLE,
+ );
+
+ if true {
+ // scrolling and clips stuff
+ // let's make a scrollbox
+ let scrollbox = (0, 0).to(300, 400);
+ builder.push_simple_stacking_context(
+ LayoutPoint::new(10., 10.),
+ root_space_and_clip.spatial_id,
+ PrimitiveFlags::IS_BACKFACE_VISIBLE,
+ );
+ // set the scrolling clip
+ let space1 = builder.define_scroll_frame(
+ root_space_and_clip.spatial_id,
+ ExternalScrollId(EXT_SCROLL_ID_ROOT, PipelineId::dummy()),
+ (0, 0).by(1000, 1000),
+ scrollbox,
+ LayoutVector2D::zero(),
+ APZScrollGeneration::default(),
+ HasScrollLinkedEffect::No,
+ SpatialTreeItemKey::new(0, 0),
+ );
+ let space_and_clip1 = SpaceAndClipInfo {
+ spatial_id: space1,
+ clip_chain_id: root_space_and_clip.clip_chain_id,
+ };
+
+ // now put some content into it.
+ // start with a white background
+ let info = CommonItemProperties::new((0, 0).to(1000, 1000), space_and_clip1);
+ builder.push_hit_test(
+ info.clip_rect,
+ ClipChainId::INVALID,
+ info.spatial_id,
+ info.flags,
+ (0, 1)
+ );
+ builder.push_rect(&info, info.clip_rect, ColorF::new(1.0, 1.0, 1.0, 1.0));
+
+ // let's make a 50x50 blue square as a visual reference
+ let info = CommonItemProperties::new((0, 0).to(50, 50), space_and_clip1);
+ builder.push_hit_test(
+ info.clip_rect,
+ ClipChainId::INVALID,
+ info.spatial_id,
+ info.flags,
+ (0, 2)
+ );
+ builder.push_rect(&info, info.clip_rect, ColorF::new(0.0, 0.0, 1.0, 1.0));
+
+ // and a 50x50 green square next to it with an offset clip
+ // to see what that looks like
+ let info = CommonItemProperties::new(
+ (50, 0).to(100, 50).intersection(&(60, 10).to(110, 60)).unwrap(),
+ space_and_clip1,
+ );
+ builder.push_hit_test(
+ info.clip_rect,
+ ClipChainId::INVALID,
+ info.spatial_id,
+ info.flags,
+ (0, 3)
+ );
+ builder.push_rect(&info, info.clip_rect, ColorF::new(0.0, 1.0, 0.0, 1.0));
+
+ // Below the above rectangles, set up a nested scrollbox. It's still in
+ // the same stacking context, so note that the rects passed in need to
+ // be relative to the stacking context.
+ let space2 = builder.define_scroll_frame(
+ space1,
+ ExternalScrollId(EXT_SCROLL_ID_CONTENT, PipelineId::dummy()),
+ (0, 100).to(300, 1000),
+ (0, 100).to(200, 300),
+ LayoutVector2D::zero(),
+ APZScrollGeneration::default(),
+ HasScrollLinkedEffect::No,
+ SpatialTreeItemKey::new(0, 1),
+ );
+ let space_and_clip2 = SpaceAndClipInfo {
+ spatial_id: space2,
+ clip_chain_id: root_space_and_clip.clip_chain_id,
+ };
+
+ // give it a giant gray background just to distinguish it and to easily
+ // visually identify the nested scrollbox
+ let info = CommonItemProperties::new(
+ (-1000, -1000).to(5000, 5000),
+ space_and_clip2,
+ );
+ builder.push_hit_test(
+ info.clip_rect,
+ ClipChainId::INVALID,
+ info.spatial_id,
+ info.flags,
+ (0, 4)
+ );
+ builder.push_rect(&info, info.clip_rect, ColorF::new(0.5, 0.5, 0.5, 1.0));
+
+ // add a teal square to visualize the scrolling/clipping behaviour
+ // as you scroll the nested scrollbox
+ let info = CommonItemProperties::new((0, 200).to(50, 250), space_and_clip2);
+ builder.push_hit_test(
+ info.clip_rect,
+ ClipChainId::INVALID,
+ info.spatial_id,
+ info.flags,
+ (0, 5)
+ );
+ builder.push_rect(&info, info.clip_rect, ColorF::new(0.0, 1.0, 1.0, 1.0));
+
+ // Add a sticky frame. It will "stick" twice while scrolling, once
+ // at a margin of 10px from the bottom, for 40 pixels of scrolling,
+ // and once at a margin of 10px from the top, for 60 pixels of
+ // scrolling.
+ let sticky_id = builder.define_sticky_frame(
+ space_and_clip2.spatial_id,
+ (50, 350).by(50, 50),
+ SideOffsets2D::new(Some(10.0), None, Some(10.0), None),
+ StickyOffsetBounds::new(-40.0, 60.0),
+ StickyOffsetBounds::new(0.0, 0.0),
+ LayoutVector2D::new(0.0, 0.0),
+ SpatialTreeItemKey::new(0, 2),
+ );
+
+ let info = CommonItemProperties::new(
+ (50, 350).by(50, 50),
+ SpaceAndClipInfo {
+ spatial_id: sticky_id,
+ clip_chain_id: space_and_clip2.clip_chain_id,
+ },
+ );
+ builder.push_hit_test(
+ info.clip_rect,
+ ClipChainId::INVALID,
+ info.spatial_id,
+ info.flags,
+ (0, 6)
+ );
+ builder.push_rect(
+ &info,
+ info.clip_rect,
+ ColorF::new(0.5, 0.5, 1.0, 1.0),
+ );
+
+ // just for good measure add another teal square further down and to
+ // the right, which can be scrolled into view by the user
+ let info = CommonItemProperties::new(
+ (250, 350).to(300, 400),
+ space_and_clip2,
+ );
+ builder.push_hit_test(
+ info.clip_rect,
+ ClipChainId::INVALID,
+ info.spatial_id,
+ info.flags,
+ (0, 7)
+ );
+ builder.push_rect(&info, info.clip_rect, ColorF::new(0.0, 1.0, 1.0, 1.0));
+
+ builder.pop_stacking_context();
+ }
+
+ builder.pop_stacking_context();
+ }
+
+ fn on_event(
+ &mut self,
+ event: winit::event::WindowEvent,
+ window: &winit::window::Window,
+ api: &mut RenderApi,
+ document_id: DocumentId,
+ ) -> bool {
+ let mut txn = Transaction::new();
+ match event {
+ winit::event::WindowEvent::KeyboardInput {
+ input: winit::event::KeyboardInput {
+ state: winit::event::ElementState::Pressed,
+ virtual_keycode: Some(key),
+ ..
+ },
+ ..
+ } => {
+ let offset = match key {
+ winit::event::VirtualKeyCode::Down => Some(LayoutVector2D::new(0.0, -10.0)),
+ winit::event::VirtualKeyCode::Up => Some(LayoutVector2D::new(0.0, 10.0)),
+ winit::event::VirtualKeyCode::Right => Some(LayoutVector2D::new(-10.0, 0.0)),
+ winit::event::VirtualKeyCode::Left => Some(LayoutVector2D::new(10.0, 0.0)),
+ _ => None,
+ };
+
+ if let Some(offset) = offset {
+ self.scroll_offset += offset;
+
+ txn.set_scroll_offsets(
+ ExternalScrollId(EXT_SCROLL_ID_CONTENT, PipelineId::dummy()),
+ vec![SampledScrollOffset {
+ offset: self.scroll_offset,
+ generation: APZScrollGeneration::default(),
+ }],
+ );
+ txn.generate_frame(0, RenderReasons::empty());
+ }
+ }
+ winit::event::WindowEvent::CursorMoved { position, .. } => {
+ let pos: LogicalPosition<f32> = position.to_logical(window.scale_factor());
+ self.cursor_position = WorldPoint::new(pos.x, pos.y);
+ }
+ winit::event::WindowEvent::MouseWheel { delta, .. } => {
+ const LINE_HEIGHT: f32 = 38.0;
+ let (dx, dy) = match delta {
+ winit::event::MouseScrollDelta::LineDelta(dx, dy) => (dx, dy * LINE_HEIGHT),
+ winit::event::MouseScrollDelta::PixelDelta(pos) => (pos.x as f32, pos.y as f32),
+ };
+
+ self.scroll_offset += LayoutVector2D::new(dx, dy);
+
+ txn.set_scroll_offsets(
+ ExternalScrollId(EXT_SCROLL_ID_CONTENT, PipelineId::dummy()),
+ vec![SampledScrollOffset {
+ offset: self.scroll_offset,
+ generation: APZScrollGeneration::default(),
+ }],
+ );
+
+ txn.generate_frame(0, RenderReasons::empty());
+ }
+ winit::event::WindowEvent::MouseInput { .. } => {
+ let results = api.hit_test(
+ document_id,
+ self.cursor_position,
+ );
+
+ println!("Hit test results:");
+ for item in &results.items {
+ println!(" • {:?}", item);
+ }
+ println!("");
+ }
+ _ => (),
+ }
+
+ api.send_transaction(document_id, txn);
+
+ false
+ }
+}
+
+fn main() {
+ let mut app = App {
+ cursor_position: WorldPoint::zero(),
+ scroll_offset: LayoutVector2D::zero(),
+ };
+ boilerplate::main_wrapper(&mut app, None);
+}
diff --git a/gfx/wr/examples/texture_cache_stress.rs b/gfx/wr/examples/texture_cache_stress.rs
new file mode 100644
index 0000000000..710a5bc0d5
--- /dev/null
+++ b/gfx/wr/examples/texture_cache_stress.rs
@@ -0,0 +1,322 @@
+/* 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/. */
+
+extern crate gleam;
+extern crate glutin;
+extern crate webrender;
+extern crate winit;
+
+#[path = "common/boilerplate.rs"]
+mod boilerplate;
+
+use crate::boilerplate::{Example, HandyDandyRectBuilder};
+use gleam::gl;
+use std::mem;
+use webrender::api::*;
+use webrender::render_api::*;
+use webrender::api::units::*;
+
+
+struct ImageGenerator {
+ patterns: [[u8; 3]; 6],
+ next_pattern: usize,
+ current_image: Vec<u8>,
+}
+
+impl ImageGenerator {
+ fn new() -> Self {
+ ImageGenerator {
+ next_pattern: 0,
+ patterns: [
+ [1, 0, 0],
+ [0, 1, 0],
+ [0, 0, 1],
+ [1, 1, 0],
+ [0, 1, 1],
+ [1, 0, 1],
+ ],
+ current_image: Vec::new(),
+ }
+ }
+
+ fn generate_image(&mut self, size: i32) {
+ let pattern = &self.patterns[self.next_pattern];
+ self.current_image.clear();
+ for y in 0 .. size {
+ for x in 0 .. size {
+ let lum = 255 * (1 - (((x & 8) == 0) ^ ((y & 8) == 0)) as u8);
+ self.current_image.extend_from_slice(&[
+ lum * pattern[0],
+ lum * pattern[1],
+ lum * pattern[2],
+ 0xff,
+ ]);
+ }
+ }
+
+ self.next_pattern = (self.next_pattern + 1) % self.patterns.len();
+ }
+
+ fn take(&mut self) -> Vec<u8> {
+ mem::replace(&mut self.current_image, Vec::new())
+ }
+}
+
+impl ExternalImageHandler for ImageGenerator {
+ fn lock(
+ &mut self,
+ _key: ExternalImageId,
+ channel_index: u8,
+ ) -> ExternalImage {
+ self.generate_image(channel_index as i32);
+ ExternalImage {
+ uv: TexelRect::new(0.0, 0.0, 1.0, 1.0),
+ source: ExternalImageSource::RawData(&self.current_image),
+ }
+ }
+ fn unlock(&mut self, _key: ExternalImageId, _channel_index: u8) {}
+}
+
+struct App {
+ stress_keys: Vec<ImageKey>,
+ image_key: Option<ImageKey>,
+ image_generator: ImageGenerator,
+ swap_keys: Vec<ImageKey>,
+ swap_index: usize,
+}
+
+impl Example for App {
+ fn render(
+ &mut self,
+ api: &mut RenderApi,
+ builder: &mut DisplayListBuilder,
+ txn: &mut Transaction,
+ _device_size: DeviceIntSize,
+ pipeline_id: PipelineId,
+ _document_id: DocumentId,
+ ) {
+ let bounds = (0, 0).to(512, 512);
+ let space_and_clip = SpaceAndClipInfo::root_scroll(pipeline_id);
+
+ builder.push_simple_stacking_context(
+ bounds.min,
+ space_and_clip.spatial_id,
+ PrimitiveFlags::IS_BACKFACE_VISIBLE,
+ );
+
+ let x0 = 50.0;
+ let y0 = 50.0;
+ let image_size = LayoutSize::new(4.0, 4.0);
+
+ if self.swap_keys.is_empty() {
+ let key0 = api.generate_image_key();
+ let key1 = api.generate_image_key();
+
+ self.image_generator.generate_image(128);
+ txn.add_image(
+ key0,
+ ImageDescriptor::new(128, 128, ImageFormat::BGRA8, ImageDescriptorFlags::IS_OPAQUE),
+ ImageData::new(self.image_generator.take()),
+ None,
+ );
+
+ self.image_generator.generate_image(128);
+ txn.add_image(
+ key1,
+ ImageDescriptor::new(128, 128, ImageFormat::BGRA8, ImageDescriptorFlags::IS_OPAQUE),
+ ImageData::new(self.image_generator.take()),
+ None,
+ );
+
+ self.swap_keys.push(key0);
+ self.swap_keys.push(key1);
+ }
+
+ for (i, key) in self.stress_keys.iter().enumerate() {
+ let x = (i % 128) as f32;
+ let y = (i / 128) as f32;
+ let info = CommonItemProperties::new(
+ LayoutRect::from_origin_and_size(
+ LayoutPoint::new(x0 + image_size.width * x, y0 + image_size.height * y),
+ image_size,
+ ),
+ space_and_clip,
+ );
+
+ builder.push_image(
+ &info,
+ bounds,
+ ImageRendering::Auto,
+ AlphaType::PremultipliedAlpha,
+ *key,
+ ColorF::WHITE,
+ );
+ }
+
+ if let Some(image_key) = self.image_key {
+ let image_size = LayoutSize::new(100.0, 100.0);
+ let info = CommonItemProperties::new(
+ LayoutRect::from_origin_and_size(LayoutPoint::new(100.0, 100.0), image_size),
+ space_and_clip,
+ );
+ builder.push_image(
+ &info,
+ bounds,
+ ImageRendering::Auto,
+ AlphaType::PremultipliedAlpha,
+ image_key,
+ ColorF::WHITE,
+ );
+ }
+
+ let swap_key = self.swap_keys[self.swap_index];
+ let image_size = LayoutSize::new(64.0, 64.0);
+ let info = CommonItemProperties::new(
+ LayoutRect::from_origin_and_size(LayoutPoint::new(100.0, 400.0), image_size),
+ space_and_clip,
+ );
+ builder.push_image(
+ &info,
+ bounds,
+ ImageRendering::Auto,
+ AlphaType::PremultipliedAlpha,
+ swap_key,
+ ColorF::WHITE,
+ );
+ self.swap_index = 1 - self.swap_index;
+
+ builder.pop_stacking_context();
+ }
+
+ fn on_event(
+ &mut self,
+ event: winit::event::WindowEvent,
+ _window: &winit::window::Window,
+ api: &mut RenderApi,
+ document_id: DocumentId,
+ ) -> bool {
+ match event {
+ winit::event::WindowEvent::KeyboardInput {
+ input: winit::event::KeyboardInput {
+ state: winit::event::ElementState::Pressed,
+ virtual_keycode: Some(key),
+ ..
+ },
+ ..
+ } => {
+ let mut txn = Transaction::new();
+
+ match key {
+ winit::event::VirtualKeyCode::S => {
+ self.stress_keys.clear();
+
+ for _ in 0 .. 16 {
+ for _ in 0 .. 16 {
+ let size = 4;
+
+ let image_key = api.generate_image_key();
+
+ self.image_generator.generate_image(size);
+
+ txn.add_image(
+ image_key,
+ ImageDescriptor::new(
+ size,
+ size,
+ ImageFormat::BGRA8,
+ ImageDescriptorFlags::IS_OPAQUE,
+ ),
+ ImageData::new(self.image_generator.take()),
+ None,
+ );
+
+ self.stress_keys.push(image_key);
+ }
+ }
+ }
+ winit::event::VirtualKeyCode::D => if let Some(image_key) = self.image_key.take() {
+ txn.delete_image(image_key);
+ },
+ winit::event::VirtualKeyCode::U => if let Some(image_key) = self.image_key {
+ let size = 128;
+ self.image_generator.generate_image(size);
+
+ txn.update_image(
+ image_key,
+ ImageDescriptor::new(size, size, ImageFormat::BGRA8, ImageDescriptorFlags::IS_OPAQUE),
+ ImageData::new(self.image_generator.take()),
+ &DirtyRect::All,
+ );
+ },
+ winit::event::VirtualKeyCode::E => {
+ if let Some(image_key) = self.image_key.take() {
+ txn.delete_image(image_key);
+ }
+
+ let size = 32;
+ let image_key = api.generate_image_key();
+
+ let image_data = ExternalImageData {
+ id: ExternalImageId(0),
+ channel_index: size as u8,
+ image_type: ExternalImageType::Buffer,
+ };
+
+ txn.add_image(
+ image_key,
+ ImageDescriptor::new(size, size, ImageFormat::BGRA8, ImageDescriptorFlags::IS_OPAQUE),
+ ImageData::External(image_data),
+ None,
+ );
+
+ self.image_key = Some(image_key);
+ }
+ winit::event::VirtualKeyCode::R => {
+ if let Some(image_key) = self.image_key.take() {
+ txn.delete_image(image_key);
+ }
+
+ let image_key = api.generate_image_key();
+ let size = 32;
+ self.image_generator.generate_image(size);
+
+ txn.add_image(
+ image_key,
+ ImageDescriptor::new(size, size, ImageFormat::BGRA8, ImageDescriptorFlags::IS_OPAQUE),
+ ImageData::new(self.image_generator.take()),
+ None,
+ );
+
+ self.image_key = Some(image_key);
+ }
+ _ => {}
+ }
+
+ api.send_transaction(document_id, txn);
+ return true;
+ }
+ _ => {}
+ }
+
+ false
+ }
+
+ fn get_image_handler(
+ &mut self,
+ _gl: &dyn gl::Gl,
+ ) -> Option<Box<dyn ExternalImageHandler>> {
+ Some(Box::new(ImageGenerator::new()))
+ }
+}
+
+fn main() {
+ let mut app = App {
+ image_key: None,
+ stress_keys: Vec::new(),
+ image_generator: ImageGenerator::new(),
+ swap_keys: Vec::new(),
+ swap_index: 0,
+ };
+ boilerplate::main_wrapper(&mut app, None);
+}
diff --git a/gfx/wr/examples/yuv.rs b/gfx/wr/examples/yuv.rs
new file mode 100644
index 0000000000..ecbf6f428a
--- /dev/null
+++ b/gfx/wr/examples/yuv.rs
@@ -0,0 +1,225 @@
+/* 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/. */
+
+extern crate gleam;
+extern crate glutin;
+extern crate webrender;
+extern crate winit;
+
+#[path = "common/boilerplate.rs"]
+mod boilerplate;
+
+use crate::boilerplate::Example;
+use gleam::gl;
+use webrender::api::*;
+use webrender::render_api::*;
+use webrender::api::units::*;
+
+
+fn init_gl_texture(
+ id: gl::GLuint,
+ internal: gl::GLenum,
+ external: gl::GLenum,
+ bytes: &[u8],
+ gl: &dyn gl::Gl,
+) {
+ gl.bind_texture(gl::TEXTURE_2D, id);
+ gl.tex_parameter_i(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::LINEAR as gl::GLint);
+ gl.tex_parameter_i(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::LINEAR as gl::GLint);
+ gl.tex_parameter_i(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as gl::GLint);
+ gl.tex_parameter_i(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as gl::GLint);
+ gl.tex_image_2d(
+ gl::TEXTURE_2D,
+ 0,
+ internal as gl::GLint,
+ 100,
+ 100,
+ 0,
+ external,
+ gl::UNSIGNED_BYTE,
+ Some(bytes),
+ );
+ gl.bind_texture(gl::TEXTURE_2D, 0);
+}
+
+struct YuvImageProvider {
+ texture_ids: Vec<gl::GLuint>,
+}
+
+impl YuvImageProvider {
+ fn new(gl: &dyn gl::Gl) -> Self {
+ let texture_ids = gl.gen_textures(4);
+
+ init_gl_texture(texture_ids[0], gl::RED, gl::RED, &[127; 100 * 100], gl);
+ init_gl_texture(texture_ids[1], gl::RG8, gl::RG, &[0; 100 * 100 * 2], gl);
+ init_gl_texture(texture_ids[2], gl::RED, gl::RED, &[127; 100 * 100], gl);
+ init_gl_texture(texture_ids[3], gl::RED, gl::RED, &[127; 100 * 100], gl);
+
+ YuvImageProvider {
+ texture_ids
+ }
+ }
+}
+
+impl ExternalImageHandler for YuvImageProvider {
+ fn lock(
+ &mut self,
+ key: ExternalImageId,
+ _channel_index: u8,
+ ) -> ExternalImage {
+ let id = self.texture_ids[key.0 as usize];
+ ExternalImage {
+ uv: TexelRect::new(0.0, 0.0, 1.0, 1.0),
+ source: ExternalImageSource::NativeTexture(id),
+ }
+ }
+ fn unlock(&mut self, _key: ExternalImageId, _channel_index: u8) {
+ }
+}
+
+struct App {
+ texture_id: gl::GLuint,
+ current_value: u8,
+}
+
+impl Example for App {
+ fn render(
+ &mut self,
+ api: &mut RenderApi,
+ builder: &mut DisplayListBuilder,
+ txn: &mut Transaction,
+ _device_size: DeviceIntSize,
+ pipeline_id: PipelineId,
+ _document_id: DocumentId,
+ ) {
+ let bounds = LayoutRect::from_size(LayoutSize::new(500.0, 500.0));
+ let space_and_clip = SpaceAndClipInfo::root_scroll(pipeline_id);
+
+ builder.push_simple_stacking_context(
+ bounds.min,
+ space_and_clip.spatial_id,
+ PrimitiveFlags::IS_BACKFACE_VISIBLE,
+ );
+
+ let yuv_chanel1 = api.generate_image_key();
+ let yuv_chanel2 = api.generate_image_key();
+ let yuv_chanel2_1 = api.generate_image_key();
+ let yuv_chanel3 = api.generate_image_key();
+ txn.add_image(
+ yuv_chanel1,
+ ImageDescriptor::new(100, 100, ImageFormat::R8, ImageDescriptorFlags::IS_OPAQUE),
+ ImageData::External(ExternalImageData {
+ id: ExternalImageId(0),
+ channel_index: 0,
+ image_type: ExternalImageType::TextureHandle(
+ ImageBufferKind::Texture2D,
+ ),
+ }),
+ None,
+ );
+ txn.add_image(
+ yuv_chanel2,
+ ImageDescriptor::new(100, 100, ImageFormat::RG8, ImageDescriptorFlags::IS_OPAQUE),
+ ImageData::External(ExternalImageData {
+ id: ExternalImageId(1),
+ channel_index: 0,
+ image_type: ExternalImageType::TextureHandle(
+ ImageBufferKind::Texture2D,
+ ),
+ }),
+ None,
+ );
+ txn.add_image(
+ yuv_chanel2_1,
+ ImageDescriptor::new(100, 100, ImageFormat::R8, ImageDescriptorFlags::IS_OPAQUE),
+ ImageData::External(ExternalImageData {
+ id: ExternalImageId(2),
+ channel_index: 0,
+ image_type: ExternalImageType::TextureHandle(
+ ImageBufferKind::Texture2D,
+ ),
+ }),
+ None,
+ );
+ txn.add_image(
+ yuv_chanel3,
+ ImageDescriptor::new(100, 100, ImageFormat::R8, ImageDescriptorFlags::IS_OPAQUE),
+ ImageData::External(ExternalImageData {
+ id: ExternalImageId(3),
+ channel_index: 0,
+ image_type: ExternalImageType::TextureHandle(
+ ImageBufferKind::Texture2D,
+ ),
+ }),
+ None,
+ );
+
+ let info = CommonItemProperties::new(
+ LayoutRect::from_origin_and_size(LayoutPoint::new(100.0, 0.0), LayoutSize::new(100.0, 100.0)),
+ space_and_clip,
+ );
+ builder.push_yuv_image(
+ &info,
+ bounds,
+ YuvData::NV12(yuv_chanel1, yuv_chanel2),
+ ColorDepth::Color8,
+ YuvColorSpace::Rec601,
+ ColorRange::Limited,
+ ImageRendering::Auto,
+ );
+
+ let info = CommonItemProperties::new(
+ LayoutRect::from_origin_and_size(LayoutPoint::new(300.0, 0.0), LayoutSize::new(100.0, 100.0)),
+ space_and_clip,
+ );
+ builder.push_yuv_image(
+ &info,
+ bounds,
+ YuvData::PlanarYCbCr(yuv_chanel1, yuv_chanel2_1, yuv_chanel3),
+ ColorDepth::Color8,
+ YuvColorSpace::Rec601,
+ ColorRange::Limited,
+ ImageRendering::Auto,
+ );
+
+ builder.pop_stacking_context();
+ }
+
+ fn on_event(
+ &mut self,
+ _event: winit::event::WindowEvent,
+ _window: &winit::window::Window,
+ _api: &mut RenderApi,
+ _document_id: DocumentId,
+ ) -> bool {
+ false
+ }
+
+ fn get_image_handler(
+ &mut self,
+ gl: &dyn gl::Gl,
+ ) -> Option<Box<dyn ExternalImageHandler>> {
+ let provider = YuvImageProvider::new(gl);
+ self.texture_id = provider.texture_ids[0];
+ Some(Box::new(provider))
+ }
+
+ fn draw_custom(&mut self, gl: &dyn gl::Gl) {
+ init_gl_texture(self.texture_id, gl::RED, gl::RED, &[self.current_value; 100 * 100], gl);
+ self.current_value = self.current_value.wrapping_add(1);
+ }
+}
+
+fn main() {
+ let mut app = App {
+ texture_id: 0,
+ current_value: 0,
+ };
+
+ let opts = webrender::WebRenderOptions {
+ ..Default::default()
+ };
+
+ boilerplate::main_wrapper(&mut app, Some(opts));
+}
diff --git a/gfx/wr/fog/Cargo.toml b/gfx/wr/fog/Cargo.toml
new file mode 100644
index 0000000000..a45cc52277
--- /dev/null
+++ b/gfx/wr/fog/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "firefox-on-glean"
+version = "0.1.0"
+edition = "2018"
+license = "MPL-2.0"
+
+# This is a dummy crate for non-gecko builds.
+
+[dependencies]
diff --git a/gfx/wr/fog/src/main.rs b/gfx/wr/fog/src/main.rs
new file mode 100644
index 0000000000..a58657b036
--- /dev/null
+++ b/gfx/wr/fog/src/main.rs
@@ -0,0 +1,7 @@
+/* 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/. */
+
+fn main() {
+ println!("Hello, world!");
+}
diff --git a/gfx/wr/glsl-to-cxx/Cargo.toml b/gfx/wr/glsl-to-cxx/Cargo.toml
new file mode 100644
index 0000000000..8161060c91
--- /dev/null
+++ b/gfx/wr/glsl-to-cxx/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "glsl-to-cxx"
+version = "0.1.0"
+license = "MPL-2.0"
+authors = ["The Mozilla Project Developers", "Dimitri Sabadie"]
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+glsl = "6.0"
diff --git a/gfx/wr/glsl-to-cxx/README.md b/gfx/wr/glsl-to-cxx/README.md
new file mode 100644
index 0000000000..54dcbc7d61
--- /dev/null
+++ b/gfx/wr/glsl-to-cxx/README.md
@@ -0,0 +1,21 @@
+A GLSL to C++ translator.
+
+Translates GLSL to vectorized C++. Intended for use with WebRender software backend.
+
+Architecture
+------------
+GLSL code is parsed by the glsl crate. In hir.rs we traverse the resulting AST
+and build a higher level representation by doing type checking and name
+resolution. The resulting hir tree is traversed by lib.rs to output C++ code.
+
+The generated C++ code is 4x wider then the original glsl. i.e. a glsl 'float'
+becomes a C++ 'Float' which is represented by a xmm register (a vector of 4 floats).
+Likewise, a vec4 becomes a struct of 4 'Float's for a total of 4 xmm registers and
+16 floating point values.
+
+Vector branching is flattened to non-branching code that unconditionally runs both
+sides of the branch and combines the results with a mask based on the condition.
+
+The compiler also supports scalarization. Values that are known to be the same
+across all vector lanes are translated to scalars instead of vectors. Branches on
+scalars are translated as actual branches.
diff --git a/gfx/wr/glsl-to-cxx/src/hir.rs b/gfx/wr/glsl-to-cxx/src/hir.rs
new file mode 100644
index 0000000000..ad48c11df8
--- /dev/null
+++ b/gfx/wr/glsl-to-cxx/src/hir.rs
@@ -0,0 +1,4545 @@
+/* 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/.
+ *
+ * Large chunks of this file are derived from the glsl crate which is:
+ * Copyright (c) 2018, Dimitri Sabadie <dimitri.sabadie@gmail.com>
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * * Neither the name of Dimitri Sabadie <dimitri.sabadie@gmail.com> nor the names of other
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+use glsl::syntax;
+use glsl::syntax::{ArrayedIdentifier, ArraySpecifier, ArraySpecifierDimension, AssignmentOp, BinaryOp, Identifier};
+use glsl::syntax::{NonEmpty, PrecisionQualifier, StructFieldSpecifier, StructSpecifier};
+use glsl::syntax::{TypeSpecifier, TypeSpecifierNonArray, UnaryOp};
+use std::cell::{Cell, Ref, RefCell};
+use std::collections::HashMap;
+use std::iter::FromIterator;
+use std::mem;
+use std::ops::{Deref, DerefMut};
+use std::rc::Rc;
+
+trait LiftFrom<S> {
+ fn lift(state: &mut State, s: S) -> Self;
+}
+
+fn lift<S, T: LiftFrom<S>>(state: &mut State, s: S) -> T {
+ LiftFrom::lift(state, s)
+}
+
+#[derive(Debug)]
+pub struct Symbol {
+ pub name: String,
+ pub decl: SymDecl,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct FunctionSignature {
+ ret: Type,
+ params: Vec<Type>,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct FunctionType {
+ signatures: NonEmpty<FunctionSignature>,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum SamplerFormat {
+ Unknown,
+ RGBA8,
+ RGBA32F,
+ RGBA32I,
+ R8,
+ RG8,
+}
+
+impl SamplerFormat {
+ pub fn type_suffix(self) -> Option<&'static str> {
+ match self {
+ SamplerFormat::Unknown => None,
+ SamplerFormat::RGBA8 => Some("RGBA8"),
+ SamplerFormat::RGBA32F => Some("RGBA32F"),
+ SamplerFormat::RGBA32I => Some("RGBA32I"),
+ SamplerFormat::R8 => Some("R8"),
+ SamplerFormat::RG8 => Some("RG8"),
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum StorageClass {
+ None,
+ Const,
+ In,
+ Out,
+ Uniform,
+ Sampler(SamplerFormat),
+ FragColor(i32),
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct ArraySizes {
+ pub sizes: Vec<Expr>,
+}
+
+impl LiftFrom<&ArraySpecifier> for ArraySizes {
+ fn lift(state: &mut State, a: &ArraySpecifier) -> Self {
+ ArraySizes {
+ sizes: a.dimensions.0.iter().map(|a| match a {
+ ArraySpecifierDimension::Unsized => panic!(),
+ ArraySpecifierDimension::ExplicitlySized(expr) => translate_expression(state, expr),
+ }).collect(),
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+pub enum TypeKind {
+ Void,
+ Bool,
+ Int,
+ UInt,
+ Float,
+ Double,
+ Vec2,
+ Vec3,
+ Vec4,
+ DVec2,
+ DVec3,
+ DVec4,
+ BVec2,
+ BVec3,
+ BVec4,
+ IVec2,
+ IVec3,
+ IVec4,
+ UVec2,
+ UVec3,
+ UVec4,
+ Mat2,
+ Mat3,
+ Mat4,
+ Mat23,
+ Mat24,
+ Mat32,
+ Mat34,
+ Mat42,
+ Mat43,
+ DMat2,
+ DMat3,
+ DMat4,
+ DMat23,
+ DMat24,
+ DMat32,
+ DMat34,
+ DMat42,
+ DMat43,
+ // floating point opaque types
+ Sampler1D,
+ Image1D,
+ Sampler2D,
+ Image2D,
+ Sampler3D,
+ Image3D,
+ SamplerCube,
+ ImageCube,
+ Sampler2DRect,
+ Image2DRect,
+ Sampler1DArray,
+ Image1DArray,
+ Sampler2DArray,
+ Image2DArray,
+ SamplerBuffer,
+ ImageBuffer,
+ Sampler2DMS,
+ Image2DMS,
+ Sampler2DMSArray,
+ Image2DMSArray,
+ SamplerCubeArray,
+ ImageCubeArray,
+ Sampler1DShadow,
+ Sampler2DShadow,
+ Sampler2DRectShadow,
+ Sampler1DArrayShadow,
+ Sampler2DArrayShadow,
+ SamplerCubeShadow,
+ SamplerCubeArrayShadow,
+ // signed integer opaque types
+ ISampler1D,
+ IImage1D,
+ ISampler2D,
+ IImage2D,
+ ISampler3D,
+ IImage3D,
+ ISamplerCube,
+ IImageCube,
+ ISampler2DRect,
+ IImage2DRect,
+ ISampler1DArray,
+ IImage1DArray,
+ ISampler2DArray,
+ IImage2DArray,
+ ISamplerBuffer,
+ IImageBuffer,
+ ISampler2DMS,
+ IImage2DMS,
+ ISampler2DMSArray,
+ IImage2DMSArray,
+ ISamplerCubeArray,
+ IImageCubeArray,
+ // unsigned integer opaque types
+ AtomicUInt,
+ USampler1D,
+ UImage1D,
+ USampler2D,
+ UImage2D,
+ USampler3D,
+ UImage3D,
+ USamplerCube,
+ UImageCube,
+ USampler2DRect,
+ UImage2DRect,
+ USampler1DArray,
+ UImage1DArray,
+ USampler2DArray,
+ UImage2DArray,
+ USamplerBuffer,
+ UImageBuffer,
+ USampler2DMS,
+ UImage2DMS,
+ USampler2DMSArray,
+ UImage2DMSArray,
+ USamplerCubeArray,
+ UImageCubeArray,
+ Struct(SymRef),
+}
+
+impl TypeKind {
+ pub fn is_sampler(&self) -> bool {
+ use TypeKind::*;
+ match self {
+ Sampler1D
+ | Image1D
+ | Sampler2D
+ | Image2D
+ | Sampler3D
+ | Image3D
+ | SamplerCube
+ | ImageCube
+ | Sampler2DRect
+ | Image2DRect
+ | Sampler1DArray
+ | Image1DArray
+ | Sampler2DArray
+ | Image2DArray
+ | SamplerBuffer
+ | ImageBuffer
+ | Sampler2DMS
+ | Image2DMS
+ | Sampler2DMSArray
+ | Image2DMSArray
+ | SamplerCubeArray
+ | ImageCubeArray
+ | Sampler1DShadow
+ | Sampler2DShadow
+ | Sampler2DRectShadow
+ | Sampler1DArrayShadow
+ | Sampler2DArrayShadow
+ | SamplerCubeShadow
+ | SamplerCubeArrayShadow
+ | ISampler1D
+ | IImage1D
+ | ISampler2D
+ | IImage2D
+ | ISampler3D
+ | IImage3D
+ | ISamplerCube
+ | IImageCube
+ | ISampler2DRect
+ | IImage2DRect
+ | ISampler1DArray
+ | IImage1DArray
+ | ISampler2DArray
+ | IImage2DArray
+ | ISamplerBuffer
+ | IImageBuffer
+ | ISampler2DMS
+ | IImage2DMS
+ | ISampler2DMSArray
+ | IImage2DMSArray
+ | ISamplerCubeArray
+ | IImageCubeArray
+ | USampler1D
+ | UImage1D
+ | USampler2D
+ | UImage2D
+ | USampler3D
+ | UImage3D
+ | USamplerCube
+ | UImageCube
+ | USampler2DRect
+ | UImage2DRect
+ | USampler1DArray
+ | UImage1DArray
+ | USampler2DArray
+ | UImage2DArray
+ | USamplerBuffer
+ | UImageBuffer
+ | USampler2DMS
+ | UImage2DMS
+ | USampler2DMSArray
+ | UImage2DMSArray
+ | USamplerCubeArray
+ | UImageCubeArray => true,
+ _ => false,
+ }
+ }
+
+ pub fn is_bool(&self) -> bool {
+ use TypeKind::*;
+ match self {
+ Bool | BVec2 | BVec3 | BVec4 => true,
+ _ => false,
+ }
+ }
+
+ pub fn to_bool(&self) -> Self {
+ use TypeKind::*;
+ match self {
+ Int | UInt | Float | Double => Bool,
+ IVec2 | UVec2 | Vec2 | DVec2 => BVec2,
+ IVec3 | UVec3 | Vec3 | DVec3 => BVec3,
+ IVec4 | UVec4 | Vec4 | DVec4 => BVec4,
+ _ => *self,
+ }
+ }
+
+ pub fn to_int(&self) -> Self {
+ use TypeKind::*;
+ match self {
+ Bool | UInt | Float | Double => Int,
+ BVec2 | UVec2 | Vec2 | DVec2 => IVec2,
+ BVec3 | UVec3 | Vec3 | DVec3 => IVec3,
+ BVec4 | UVec4 | Vec4 | DVec4 => IVec4,
+ _ => *self,
+ }
+ }
+
+ pub fn to_scalar(&self) -> Self {
+ use TypeKind::*;
+ match self {
+ IVec2 | IVec3 | IVec4 => Int,
+ UVec2 | UVec3 | UVec4 => UInt,
+ Vec2 | Vec3 | Vec4 => Float,
+ DVec2 | DVec3 | DVec4 => Double,
+ BVec2 | BVec3 | BVec4 => Bool,
+ _ => *self,
+ }
+ }
+
+ pub fn glsl_primitive_type_name(&self) -> Option<&'static str> {
+ use TypeKind::*;
+ Some(match self {
+ Void => "void",
+ Bool => "bool",
+ Int => "int",
+ UInt => "uint",
+ Float => "float",
+ Double => "double",
+ Vec2 => "vec2",
+ Vec3 => "vec3",
+ Vec4 => "vec4",
+ DVec2 => "dvec2",
+ DVec3 => "dvec3",
+ DVec4 => "dvec4",
+ BVec2 => "bvec2",
+ BVec3 => "bvec3",
+ BVec4 => "bvec4",
+ IVec2 => "ivec2",
+ IVec3 => "ivec3",
+ IVec4 => "ivec4",
+ UVec2 => "uvec2",
+ UVec3 => "uvec3",
+ UVec4 => "uvec4",
+ Mat2 => "mat2",
+ Mat3 => "mat3",
+ Mat4 => "mat4",
+ Mat23 => "mat23",
+ Mat24 => "mat24",
+ Mat32 => "mat32",
+ Mat34 => "mat34",
+ Mat42 => "mat42",
+ Mat43 => "mat43",
+ DMat2 => "dmat2",
+ DMat3 => "dmat3",
+ DMat4 => "dmat4",
+ DMat23 => "dmat23",
+ DMat24 => "dmat24",
+ DMat32 => "dmat32",
+ DMat34 => "dmat34",
+ DMat42 => "dmat42",
+ DMat43 => "dmat43",
+ Sampler1D => "sampler1D",
+ Image1D => "image1D",
+ Sampler2D => "sampler2D",
+ Image2D => "image2D",
+ Sampler3D => "sampler3D",
+ Image3D => "image3D",
+ SamplerCube => "samplerCube",
+ ImageCube => "imageCube",
+ Sampler2DRect => "sampler2DRect",
+ Image2DRect => "image2DRect",
+ Sampler1DArray => "sampler1DArray",
+ Image1DArray => "image1DArray",
+ Sampler2DArray => "sampler2DArray",
+ Image2DArray => "image2DArray",
+ SamplerBuffer => "samplerBuffer",
+ ImageBuffer => "imageBuffer",
+ Sampler2DMS => "sampler2DMS",
+ Image2DMS => "image2DMS",
+ Sampler2DMSArray => "sampler2DMSArray",
+ Image2DMSArray => "image2DMSArray",
+ SamplerCubeArray => "samplerCubeArray",
+ ImageCubeArray => "imageCubeArray",
+ Sampler1DShadow => "sampler1DShadow",
+ Sampler2DShadow => "sampler2DShadow",
+ Sampler2DRectShadow => "sampler2DRectShadow",
+ Sampler1DArrayShadow => "sampler1DArrayShadow",
+ Sampler2DArrayShadow => "sampler2DArrayShadow",
+ SamplerCubeShadow => "samplerCubeShadow",
+ SamplerCubeArrayShadow => "samplerCubeArrayShadow",
+ ISampler1D => "isampler1D",
+ IImage1D => "iimage1D",
+ ISampler2D => "isampler2D",
+ IImage2D => "iimage2D",
+ ISampler3D => "isampler3D",
+ IImage3D => "iimage3D",
+ ISamplerCube => "isamplerCube",
+ IImageCube => "iimageCube",
+ ISampler2DRect => "isampler2DRect",
+ IImage2DRect => "iimage2DRect",
+ ISampler1DArray => "isampler1DArray",
+ IImage1DArray => "iimage1DArray",
+ ISampler2DArray => "isampler2DArray",
+ IImage2DArray => "iimage2DArray",
+ ISamplerBuffer => "isamplerBuffer",
+ IImageBuffer => "iimageBuffer",
+ ISampler2DMS => "isampler2MS",
+ IImage2DMS => "iimage2DMS",
+ ISampler2DMSArray => "isampler2DMSArray",
+ IImage2DMSArray => "iimage2DMSArray",
+ ISamplerCubeArray => "isamplerCubeArray",
+ IImageCubeArray => "iimageCubeArray",
+ AtomicUInt => "atomic_uint",
+ USampler1D => "usampler1D",
+ UImage1D => "uimage1D",
+ USampler2D => "usampler2D",
+ UImage2D => "uimage2D",
+ USampler3D => "usampler3D",
+ UImage3D => "uimage3D",
+ USamplerCube => "usamplerCube",
+ UImageCube => "uimageCube",
+ USampler2DRect => "usampler2DRect",
+ UImage2DRect => "uimage2DRect",
+ USampler1DArray => "usampler1DArray",
+ UImage1DArray => "uimage1DArray",
+ USampler2DArray => "usampler2DArray",
+ UImage2DArray => "uimage2DArray",
+ USamplerBuffer => "usamplerBuffer",
+ UImageBuffer => "uimageBuffer",
+ USampler2DMS => "usampler2DMS",
+ UImage2DMS => "uimage2DMS",
+ USampler2DMSArray => "usamplerDMSArray",
+ UImage2DMSArray => "uimage2DMSArray",
+ USamplerCubeArray => "usamplerCubeArray",
+ UImageCubeArray => "uimageCubeArray",
+ Struct(..) => return None,
+ })
+ }
+
+ pub fn cxx_primitive_type_name(&self) -> Option<&'static str> {
+ use TypeKind::*;
+ match self {
+ Bool => Some("Bool"),
+ Int => Some("I32"),
+ UInt => Some("U32"),
+ Float => Some("Float"),
+ Double => Some("Double"),
+ _ => self.glsl_primitive_type_name(),
+ }
+ }
+
+ pub fn cxx_primitive_scalar_type_name(&self) -> Option<&'static str> {
+ use TypeKind::*;
+ match self {
+ Void => Some("void"),
+ Bool => Some("bool"),
+ Int => Some("int32_t"),
+ UInt => Some("uint32_t"),
+ Float => Some("float"),
+ Double => Some("double"),
+ _ => {
+ if self.is_sampler() {
+ self.cxx_primitive_type_name()
+ } else {
+ None
+ }
+ }
+ }
+ }
+
+ pub fn from_glsl_primitive_type_name(name: &str) -> Option<TypeKind> {
+ use TypeKind::*;
+ Some(match name {
+ "void" => Void,
+ "bool" => Bool,
+ "int" => Int,
+ "uint" => UInt,
+ "float" => Float,
+ "double" => Double,
+ "vec2" => Vec2,
+ "vec3" => Vec3,
+ "vec4" => Vec4,
+ "dvec2" => DVec2,
+ "dvec3" => DVec3,
+ "dvec4" => DVec4,
+ "bvec2" => BVec2,
+ "bvec3" => BVec3,
+ "bvec4" => BVec4,
+ "ivec2" => IVec2,
+ "ivec3" => IVec3,
+ "ivec4" => IVec4,
+ "uvec2" => UVec2,
+ "uvec3" => UVec3,
+ "uvec4" => UVec4,
+ "mat2" => Mat2,
+ "mat3" => Mat3,
+ "mat4" => Mat4,
+ "mat23" => Mat23,
+ "mat24" => Mat24,
+ "mat32" => Mat32,
+ "mat34" => Mat34,
+ "mat42" => Mat42,
+ "mat43" => Mat43,
+ "dmat2" => DMat2,
+ "dmat3" => DMat3,
+ "dmat4" => DMat4,
+ "dmat23" => DMat23,
+ "dmat24" => DMat24,
+ "dmat32" => DMat32,
+ "dmat34" => DMat34,
+ "dmat42" => DMat42,
+ "dmat43" => DMat43,
+ "sampler1D" => Sampler1D,
+ "image1D" => Image1D,
+ "sampler2D" => Sampler2D,
+ "image2D" => Image2D,
+ "sampler3D" => Sampler3D,
+ "image3D" => Image3D,
+ "samplerCube" => SamplerCube,
+ "imageCube" => ImageCube,
+ "sampler2DRect" => Sampler2DRect,
+ "image2DRect" => Image2DRect,
+ "sampler1DArray" => Sampler1DArray,
+ "image1DArray" => Image1DArray,
+ "sampler2DArray" => Sampler2DArray,
+ "image2DArray" => Image2DArray,
+ "samplerBuffer" => SamplerBuffer,
+ "imageBuffer" => ImageBuffer,
+ "sampler2DMS" => Sampler2DMS,
+ "image2DMS" => Image2DMS,
+ "sampler2DMSArray" => Sampler2DMSArray,
+ "image2DMSArray" => Image2DMSArray,
+ "samplerCubeArray" => SamplerCubeArray,
+ "imageCubeArray" => ImageCubeArray,
+ "sampler1DShadow" => Sampler1DShadow,
+ "sampler2DShadow" => Sampler2DShadow,
+ "sampler2DRectShadow" => Sampler2DRectShadow,
+ "sampler1DArrayShadow" => Sampler1DArrayShadow,
+ "sampler2DArrayShadow" => Sampler2DArrayShadow,
+ "samplerCubeShadow" => SamplerCubeShadow,
+ "samplerCubeArrayShadow" => SamplerCubeArrayShadow,
+ "isampler1D" => ISampler1D,
+ "iimage1D" => IImage1D,
+ "isampler2D" => ISampler2D,
+ "iimage2D" => IImage2D,
+ "isampler3D" => ISampler3D,
+ "iimage3D" => IImage3D,
+ "isamplerCube" => ISamplerCube,
+ "iimageCube" => IImageCube,
+ "isampler2DRect" => ISampler2DRect,
+ "iimage2DRect" => IImage2DRect,
+ "isampler1DArray" => ISampler1DArray,
+ "iimage1DArray" => IImage1DArray,
+ "isampler2DArray" => ISampler2DArray,
+ "iimage2DArray" => IImage2DArray,
+ "isamplerBuffer" => ISamplerBuffer,
+ "iimageBuffer" => IImageBuffer,
+ "isampler2MS" => ISampler2DMS,
+ "iimage2DMS" => IImage2DMS,
+ "isampler2DMSArray" => ISampler2DMSArray,
+ "iimage2DMSArray" => IImage2DMSArray,
+ "isamplerCubeArray" => ISamplerCubeArray,
+ "iimageCubeArray" => IImageCubeArray,
+ "atomic_uint" => AtomicUInt,
+ "usampler1D" => USampler1D,
+ "uimage1D" => UImage1D,
+ "usampler2D" => USampler2D,
+ "uimage2D" => UImage2D,
+ "usampler3D" => USampler3D,
+ "uimage3D" => UImage3D,
+ "usamplerCube" => USamplerCube,
+ "uimageCube" => UImageCube,
+ "usampler2DRect" => USampler2DRect,
+ "uimage2DRect" => UImage2DRect,
+ "usampler1DArray" => USampler1DArray,
+ "uimage1DArray" => UImage1DArray,
+ "usampler2DArray" => USampler2DArray,
+ "uimage2DArray" => UImage2DArray,
+ "usamplerBuffer" => USamplerBuffer,
+ "uimageBuffer" => UImageBuffer,
+ "usampler2DMS" => USampler2DMS,
+ "uimage2DMS" => UImage2DMS,
+ "usamplerDMSArray" => USampler2DMSArray,
+ "uimage2DMSArray" => UImage2DMSArray,
+ "usamplerCubeArray" => USamplerCubeArray,
+ "uimageCubeArray" => UImageCubeArray,
+ _ => return None,
+ })
+ }
+
+ pub fn from_primitive_type_specifier(spec: &syntax::TypeSpecifierNonArray) -> Option<TypeKind> {
+ use TypeKind::*;
+ Some(match spec {
+ TypeSpecifierNonArray::Void => Void,
+ TypeSpecifierNonArray::Bool => Bool,
+ TypeSpecifierNonArray::Int => Int,
+ TypeSpecifierNonArray::UInt => UInt,
+ TypeSpecifierNonArray::Float => Float,
+ TypeSpecifierNonArray::Double => Double,
+ TypeSpecifierNonArray::Vec2 => Vec2,
+ TypeSpecifierNonArray::Vec3 => Vec3,
+ TypeSpecifierNonArray::Vec4 => Vec4,
+ TypeSpecifierNonArray::DVec2 => DVec2,
+ TypeSpecifierNonArray::DVec3 => DVec3,
+ TypeSpecifierNonArray::DVec4 => DVec4,
+ TypeSpecifierNonArray::BVec2 => BVec2,
+ TypeSpecifierNonArray::BVec3 => BVec3,
+ TypeSpecifierNonArray::BVec4 => BVec4,
+ TypeSpecifierNonArray::IVec2 => IVec2,
+ TypeSpecifierNonArray::IVec3 => IVec3,
+ TypeSpecifierNonArray::IVec4 => IVec4,
+ TypeSpecifierNonArray::UVec2 => UVec2,
+ TypeSpecifierNonArray::UVec3 => UVec3,
+ TypeSpecifierNonArray::UVec4 => UVec4,
+ TypeSpecifierNonArray::Mat2 => Mat2,
+ TypeSpecifierNonArray::Mat3 => Mat3,
+ TypeSpecifierNonArray::Mat4 => Mat4,
+ TypeSpecifierNonArray::Mat23 => Mat23,
+ TypeSpecifierNonArray::Mat24 => Mat24,
+ TypeSpecifierNonArray::Mat32 => Mat32,
+ TypeSpecifierNonArray::Mat34 => Mat34,
+ TypeSpecifierNonArray::Mat42 => Mat42,
+ TypeSpecifierNonArray::Mat43 => Mat43,
+ TypeSpecifierNonArray::DMat2 => DMat2,
+ TypeSpecifierNonArray::DMat3 => DMat3,
+ TypeSpecifierNonArray::DMat4 => DMat4,
+ TypeSpecifierNonArray::DMat23 => DMat23,
+ TypeSpecifierNonArray::DMat24 => DMat24,
+ TypeSpecifierNonArray::DMat32 => DMat32,
+ TypeSpecifierNonArray::DMat34 => DMat34,
+ TypeSpecifierNonArray::DMat42 => DMat42,
+ TypeSpecifierNonArray::DMat43 => DMat43,
+ TypeSpecifierNonArray::Sampler1D => Sampler1D,
+ TypeSpecifierNonArray::Image1D => Image1D,
+ TypeSpecifierNonArray::Sampler2D => Sampler2D,
+ TypeSpecifierNonArray::Image2D => Image2D,
+ TypeSpecifierNonArray::Sampler3D => Sampler3D,
+ TypeSpecifierNonArray::Image3D => Image3D,
+ TypeSpecifierNonArray::SamplerCube => SamplerCube,
+ TypeSpecifierNonArray::ImageCube => ImageCube,
+ TypeSpecifierNonArray::Sampler2DRect => Sampler2DRect,
+ TypeSpecifierNonArray::Image2DRect => Image2DRect,
+ TypeSpecifierNonArray::Sampler1DArray => Sampler1DArray,
+ TypeSpecifierNonArray::Image1DArray => Image1DArray,
+ TypeSpecifierNonArray::Sampler2DArray => Sampler2DArray,
+ TypeSpecifierNonArray::Image2DArray => Image2DArray,
+ TypeSpecifierNonArray::SamplerBuffer => SamplerBuffer,
+ TypeSpecifierNonArray::ImageBuffer => ImageBuffer,
+ TypeSpecifierNonArray::Sampler2DMS => Sampler2DMS,
+ TypeSpecifierNonArray::Image2DMS => Image2DMS,
+ TypeSpecifierNonArray::Sampler2DMSArray => Sampler2DMSArray,
+ TypeSpecifierNonArray::Image2DMSArray => Image2DMSArray,
+ TypeSpecifierNonArray::SamplerCubeArray => SamplerCubeArray,
+ TypeSpecifierNonArray::ImageCubeArray => ImageCubeArray,
+ TypeSpecifierNonArray::Sampler1DShadow => Sampler1DShadow,
+ TypeSpecifierNonArray::Sampler2DShadow => Sampler2DShadow,
+ TypeSpecifierNonArray::Sampler2DRectShadow => Sampler2DRectShadow,
+ TypeSpecifierNonArray::Sampler1DArrayShadow => Sampler1DArrayShadow,
+ TypeSpecifierNonArray::Sampler2DArrayShadow => Sampler2DArrayShadow,
+ TypeSpecifierNonArray::SamplerCubeShadow => SamplerCubeShadow,
+ TypeSpecifierNonArray::SamplerCubeArrayShadow => SamplerCubeArrayShadow,
+ TypeSpecifierNonArray::ISampler1D => ISampler1D,
+ TypeSpecifierNonArray::IImage1D => IImage1D,
+ TypeSpecifierNonArray::ISampler2D => ISampler2D,
+ TypeSpecifierNonArray::IImage2D => IImage2D,
+ TypeSpecifierNonArray::ISampler3D => ISampler3D,
+ TypeSpecifierNonArray::IImage3D => IImage3D,
+ TypeSpecifierNonArray::ISamplerCube => ISamplerCube,
+ TypeSpecifierNonArray::IImageCube => IImageCube,
+ TypeSpecifierNonArray::ISampler2DRect => ISampler2DRect,
+ TypeSpecifierNonArray::IImage2DRect => IImage2DRect,
+ TypeSpecifierNonArray::ISampler1DArray => ISampler1DArray,
+ TypeSpecifierNonArray::IImage1DArray => IImage1DArray,
+ TypeSpecifierNonArray::ISampler2DArray => ISampler2DArray,
+ TypeSpecifierNonArray::IImage2DArray => IImage2DArray,
+ TypeSpecifierNonArray::ISamplerBuffer => ISamplerBuffer,
+ TypeSpecifierNonArray::IImageBuffer => IImageBuffer,
+ TypeSpecifierNonArray::ISampler2DMS => ISampler2DMS,
+ TypeSpecifierNonArray::IImage2DMS => IImage2DMS,
+ TypeSpecifierNonArray::ISampler2DMSArray => ISampler2DMSArray,
+ TypeSpecifierNonArray::IImage2DMSArray => IImage2DMSArray,
+ TypeSpecifierNonArray::ISamplerCubeArray => ISamplerCubeArray,
+ TypeSpecifierNonArray::IImageCubeArray => IImageCubeArray,
+ TypeSpecifierNonArray::AtomicUInt => AtomicUInt,
+ TypeSpecifierNonArray::USampler1D => USampler1D,
+ TypeSpecifierNonArray::UImage1D => UImage1D,
+ TypeSpecifierNonArray::USampler2D => USampler2D,
+ TypeSpecifierNonArray::UImage2D => UImage2D,
+ TypeSpecifierNonArray::USampler3D => USampler3D,
+ TypeSpecifierNonArray::UImage3D => UImage3D,
+ TypeSpecifierNonArray::USamplerCube => USamplerCube,
+ TypeSpecifierNonArray::UImageCube => UImageCube,
+ TypeSpecifierNonArray::USampler2DRect => USampler2DRect,
+ TypeSpecifierNonArray::UImage2DRect => UImage2DRect,
+ TypeSpecifierNonArray::USampler1DArray => USampler1DArray,
+ TypeSpecifierNonArray::UImage1DArray => UImage1DArray,
+ TypeSpecifierNonArray::USampler2DArray => USampler2DArray,
+ TypeSpecifierNonArray::UImage2DArray => UImage2DArray,
+ TypeSpecifierNonArray::USamplerBuffer => USamplerBuffer,
+ TypeSpecifierNonArray::UImageBuffer => UImageBuffer,
+ TypeSpecifierNonArray::USampler2DMS => USampler2DMS,
+ TypeSpecifierNonArray::UImage2DMS => UImage2DMS,
+ TypeSpecifierNonArray::USampler2DMSArray => USampler2DMSArray,
+ TypeSpecifierNonArray::UImage2DMSArray => UImage2DMSArray,
+ TypeSpecifierNonArray::USamplerCubeArray => USamplerCubeArray,
+ TypeSpecifierNonArray::UImageCubeArray => UImageCubeArray,
+ TypeSpecifierNonArray::Struct(..) | TypeSpecifierNonArray::TypeName(..) => return None,
+ })
+ }
+}
+
+impl LiftFrom<&syntax::TypeSpecifierNonArray> for TypeKind {
+ fn lift(state: &mut State, spec: &syntax::TypeSpecifierNonArray) -> Self {
+ use TypeKind::*;
+ if let Some(kind) = TypeKind::from_primitive_type_specifier(spec) {
+ kind
+ } else {
+ match spec {
+ TypeSpecifierNonArray::Struct(s) => {
+ Struct(state.lookup(s.name.as_ref().unwrap().as_str()).unwrap())
+ }
+ TypeSpecifierNonArray::TypeName(s) => Struct(state.lookup(&s.0).unwrap()),
+ _ => unreachable!(),
+ }
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct Type {
+ pub kind: TypeKind,
+ pub precision: Option<PrecisionQualifier>,
+ pub array_sizes: Option<Box<ArraySizes>>,
+}
+
+impl Type {
+ pub fn new(kind: TypeKind) -> Self {
+ Type {
+ kind,
+ precision: None,
+ array_sizes: None,
+ }
+ }
+
+ pub fn new_array(kind: TypeKind, size: i32) -> Self {
+ Type {
+ kind,
+ precision: None,
+ array_sizes: Some(Box::new(ArraySizes { sizes: vec![make_const(TypeKind::Int, size)] })),
+ }
+ }
+}
+
+impl LiftFrom<&syntax::FullySpecifiedType> for Type {
+ fn lift(state: &mut State, ty: &syntax::FullySpecifiedType) -> Self {
+ let kind = lift(state, &ty.ty.ty);
+ let array_sizes = match ty.ty.array_specifier.as_ref() {
+ Some(x) => Some(Box::new(lift(state, x))),
+ None => None,
+ };
+ let precision = get_precision(&ty.qualifier);
+ Type {
+ kind,
+ precision,
+ array_sizes,
+ }
+ }
+}
+
+impl LiftFrom<&syntax::TypeSpecifier> for Type {
+ fn lift(state: &mut State, ty: &syntax::TypeSpecifier) -> Self {
+ let kind = lift(state, &ty.ty);
+ let array_sizes = ty
+ .array_specifier
+ .as_ref()
+ .map(|x| Box::new(lift(state, x)));
+ Type {
+ kind,
+ precision: None,
+ array_sizes,
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct StructField {
+ pub ty: Type,
+ pub name: syntax::Identifier,
+}
+
+fn get_precision(qualifiers: &Option<syntax::TypeQualifier>) -> Option<PrecisionQualifier> {
+ let mut precision = None;
+ for qual in qualifiers.iter().flat_map(|x| x.qualifiers.0.iter()) {
+ match qual {
+ syntax::TypeQualifierSpec::Precision(p) => {
+ if precision.is_some() {
+ panic!("Multiple precisions");
+ }
+ precision = Some(p.clone());
+ }
+ _ => {}
+ }
+ }
+ precision
+}
+
+impl LiftFrom<&StructFieldSpecifier> for StructField {
+ fn lift(state: &mut State, f: &StructFieldSpecifier) -> Self {
+ let mut ty: Type = lift(state, &f.ty);
+ match &f.identifiers.0[..] {
+ [ident] => {
+ if let Some(a) = &ident.array_spec {
+ ty.array_sizes = Some(Box::new(lift(state, a)));
+ }
+ StructField {
+ ty,
+ name: ident.ident.clone(),
+ }
+ }
+ _ => panic!("bad number of identifiers"),
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct StructFields {
+ pub fields: Vec<StructField>,
+}
+
+impl LiftFrom<&StructSpecifier> for StructFields {
+ fn lift(state: &mut State, s: &StructSpecifier) -> Self {
+ let fields = s.fields.0.iter().map(|field| lift(state, field)).collect();
+ Self { fields }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum RunClass {
+ Unknown,
+ Scalar,
+ Vector,
+ Dependent(u32),
+}
+
+impl RunClass {
+ pub fn merge(self, run_class: RunClass) -> RunClass {
+ match (self, run_class) {
+ (RunClass::Vector, _) | (_, RunClass::Vector) => RunClass::Vector,
+ (RunClass::Dependent(x), RunClass::Dependent(y)) => RunClass::Dependent(x | y),
+ (RunClass::Unknown, _) | (_, RunClass::Dependent(..)) => run_class,
+ _ => self,
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum SymDecl {
+ NativeFunction(FunctionType, Option<&'static str>, RunClass),
+ UserFunction(Rc<FunctionDefinition>, RunClass),
+ Local(StorageClass, Type, RunClass),
+ Global(
+ StorageClass,
+ Option<syntax::InterpolationQualifier>,
+ Type,
+ RunClass,
+ ),
+ Struct(StructFields),
+}
+
+#[derive(Clone, Debug, PartialEq, Copy, Eq, Hash)]
+pub struct SymRef(u32);
+
+#[derive(Debug)]
+struct Scope {
+ // The field is not actively used but useful for `Debug`.
+ #[allow(dead_code)]
+ name: String,
+ names: HashMap<String, SymRef>,
+}
+impl Scope {
+ fn new(name: String) -> Self {
+ Scope {
+ name,
+ names: HashMap::new(),
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct TexelFetchOffsets {
+ pub min_x: i32,
+ pub max_x: i32,
+ pub min_y: i32,
+ pub max_y: i32,
+}
+
+impl TexelFetchOffsets {
+ fn new(x: i32, y: i32) -> Self {
+ TexelFetchOffsets {
+ min_x: x,
+ max_x: x,
+ min_y: y,
+ max_y: y,
+ }
+ }
+
+ fn add_offset(&mut self, x: i32, y: i32) {
+ self.min_x = self.min_x.min(x);
+ self.max_x = self.max_x.max(x);
+ self.min_y = self.min_y.min(y);
+ self.max_y = self.max_y.max(y);
+ }
+}
+
+#[derive(Debug)]
+pub struct State {
+ scopes: Vec<Scope>,
+ syms: Vec<RefCell<Symbol>>,
+ in_function: Option<SymRef>,
+ run_class_changed: Cell<bool>,
+ last_declaration: SymRef,
+ branch_run_class: RunClass,
+ branch_declaration: SymRef,
+ modified_globals: RefCell<Vec<SymRef>>,
+ pub used_globals: RefCell<Vec<SymRef>>,
+ pub texel_fetches: HashMap<(SymRef, SymRef), TexelFetchOffsets>,
+ clip_dist_sym: SymRef,
+ pub used_clip_dist: u32,
+}
+
+impl State {
+ pub fn new() -> Self {
+ State {
+ scopes: Vec::new(),
+ syms: Vec::new(),
+ in_function: None,
+ run_class_changed: Cell::new(false),
+ last_declaration: SymRef(0),
+ branch_run_class: RunClass::Unknown,
+ branch_declaration: SymRef(0),
+ modified_globals: RefCell::new(Vec::new()),
+ used_globals: RefCell::new(Vec::new()),
+ texel_fetches: HashMap::new(),
+ clip_dist_sym: SymRef(0),
+ used_clip_dist: 0,
+ }
+ }
+
+ pub fn lookup(&self, name: &str) -> Option<SymRef> {
+ for s in self.scopes.iter().rev() {
+ if let Some(sym) = s.names.get(name) {
+ return Some(*sym);
+ }
+ }
+ return None;
+ }
+
+ fn declare(&mut self, name: &str, decl: SymDecl) -> SymRef {
+ let s = SymRef(self.syms.len() as u32);
+ self.syms.push(RefCell::new(Symbol {
+ name: name.into(),
+ decl,
+ }));
+ self.scopes.last_mut().unwrap().names.insert(name.into(), s);
+ s
+ }
+
+ pub fn sym(&self, sym: SymRef) -> Ref<Symbol> {
+ self.syms[sym.0 as usize].borrow()
+ }
+
+ pub fn sym_mut(&mut self, sym: SymRef) -> &mut Symbol {
+ self.syms[sym.0 as usize].get_mut()
+ }
+
+ pub fn lookup_sym_mut(&mut self, name: &str) -> Option<&mut Symbol> {
+ self.lookup(name)
+ .map(move |x| self.syms[x.0 as usize].get_mut())
+ }
+
+ fn push_scope(&mut self, name: String) {
+ self.scopes.push(Scope::new(name));
+ }
+ fn pop_scope(&mut self) {
+ self.scopes.pop();
+ }
+
+ fn return_run_class(&self, mut new_run_class: RunClass) {
+ new_run_class = self.branch_run_class.merge(new_run_class);
+ if let Some(sym) = self.in_function {
+ let mut b = self.syms[sym.0 as usize].borrow_mut();
+ if let SymDecl::UserFunction(_, ref mut run_class) = b.decl {
+ *run_class = run_class.merge(new_run_class);
+ }
+ }
+ }
+
+ pub fn function_definition(&self, name: SymRef) -> Option<(Rc<FunctionDefinition>, RunClass)> {
+ if let SymDecl::UserFunction(ref fd, ref run_class) = &self.sym(name).decl {
+ Some((fd.clone(), *run_class))
+ } else {
+ None
+ }
+ }
+
+ fn merge_run_class(&self, sym: SymRef, mut new_run_class: RunClass) -> RunClass {
+ if sym.0 <= self.branch_declaration.0 {
+ new_run_class = self.branch_run_class.merge(new_run_class);
+ }
+ let mut b = self.syms[sym.0 as usize].borrow_mut();
+ let mut old_run_class = new_run_class;
+ if let SymDecl::Local(_, _, ref mut run_class) = b.decl {
+ old_run_class = *run_class;
+ new_run_class = old_run_class.merge(new_run_class);
+ *run_class = new_run_class;
+ }
+ if old_run_class != RunClass::Unknown && old_run_class != new_run_class {
+ self.run_class_changed.set(true);
+ }
+ new_run_class
+ }
+}
+
+/// A declaration.
+#[derive(Clone, Debug, PartialEq)]
+pub enum Declaration {
+ FunctionPrototype(FunctionPrototype),
+ StructDefinition(SymRef),
+ InitDeclaratorList(InitDeclaratorList),
+ Precision(PrecisionQualifier, TypeSpecifier),
+ Block(Block),
+ Global(TypeQualifier, Vec<Identifier>),
+}
+
+/// A general purpose block, containing fields and possibly a list of declared identifiers. Semantic
+/// is given with the storage qualifier.
+#[derive(Clone, Debug, PartialEq)]
+pub struct Block {
+ pub qualifier: TypeQualifier,
+ pub name: Identifier,
+ pub fields: Vec<StructFieldSpecifier>,
+ pub identifier: Option<ArrayedIdentifier>,
+}
+
+/// Function identifier.
+#[derive(Clone, Debug, PartialEq)]
+pub enum FunIdentifier {
+ Identifier(SymRef),
+ Constructor(Type),
+}
+
+/// Function prototype.
+#[derive(Clone, Debug, PartialEq)]
+pub struct FunctionPrototype {
+ pub ty: Type,
+ pub name: Identifier,
+ pub parameters: Vec<FunctionParameterDeclaration>,
+}
+
+impl FunctionPrototype {
+ pub fn has_parameter(&self, sym: SymRef) -> bool {
+ for param in &self.parameters {
+ match param {
+ FunctionParameterDeclaration::Named(_, ref d) => {
+ if d.sym == sym {
+ return true;
+ }
+ }
+ _ => {}
+ }
+ }
+ false
+ }
+}
+
+/// Function parameter declaration.
+#[derive(Clone, Debug, PartialEq)]
+pub enum FunctionParameterDeclaration {
+ Named(Option<ParameterQualifier>, FunctionParameterDeclarator),
+ Unnamed(Option<ParameterQualifier>, TypeSpecifier),
+}
+
+/// Function parameter declarator.
+#[derive(Clone, Debug, PartialEq)]
+pub struct FunctionParameterDeclarator {
+ pub ty: Type,
+ pub name: Identifier,
+ pub sym: SymRef,
+}
+
+/// Init declarator list.
+#[derive(Clone, Debug, PartialEq)]
+pub struct InitDeclaratorList {
+ // XXX it feels like separating out the type and the names is better than
+ // head and tail
+ // Also, it might be nice to separate out type definitions from name definitions
+ pub head: SingleDeclaration,
+ pub tail: Vec<SingleDeclarationNoType>,
+}
+
+/// Type qualifier.
+#[derive(Clone, Debug, PartialEq)]
+pub struct TypeQualifier {
+ pub qualifiers: NonEmpty<TypeQualifierSpec>,
+}
+
+fn lift_type_qualifier_for_declaration(
+ _state: &mut State,
+ q: &Option<syntax::TypeQualifier>,
+) -> Option<TypeQualifier> {
+ q.as_ref().and_then(|x| {
+ NonEmpty::from_non_empty_iter(x.qualifiers.0.iter().flat_map(|x| match x {
+ syntax::TypeQualifierSpec::Precision(_) => None,
+ syntax::TypeQualifierSpec::Interpolation(_) => None,
+ syntax::TypeQualifierSpec::Invariant => Some(TypeQualifierSpec::Invariant),
+ syntax::TypeQualifierSpec::Layout(l) => Some(TypeQualifierSpec::Layout(l.clone())),
+ syntax::TypeQualifierSpec::Precise => Some(TypeQualifierSpec::Precise),
+ syntax::TypeQualifierSpec::Storage(_) => None,
+ }))
+ .map(|x| TypeQualifier { qualifiers: x })
+ })
+}
+
+fn lift_type_qualifier_for_parameter(
+ _state: &mut State,
+ q: &Option<syntax::TypeQualifier>,
+) -> Option<ParameterQualifier> {
+ let mut qp: Option<ParameterQualifier> = None;
+ if let Some(q) = q {
+ for x in &q.qualifiers.0 {
+ match (&qp, x) {
+ (None, syntax::TypeQualifierSpec::Storage(s)) => match s {
+ syntax::StorageQualifier::Const => qp = Some(ParameterQualifier::Const),
+ syntax::StorageQualifier::In => qp = Some(ParameterQualifier::In),
+ syntax::StorageQualifier::Out => qp = Some(ParameterQualifier::Out),
+ syntax::StorageQualifier::InOut => qp = Some(ParameterQualifier::InOut),
+ _ => panic!("Bad storage qualifier for parameter"),
+ },
+ (_, syntax::TypeQualifierSpec::Precision(_)) => {}
+ _ => panic!("Bad parameter qualifier {:?}", x),
+ }
+ }
+ }
+ qp
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum ParameterQualifier {
+ Const,
+ In,
+ InOut,
+ Out,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum MemoryQualifier {
+ Coherent,
+ Volatile,
+ Restrict,
+ ReadOnly,
+ WriteOnly,
+}
+
+/// Type qualifier spec.
+#[derive(Clone, Debug, PartialEq)]
+pub enum TypeQualifierSpec {
+ Layout(syntax::LayoutQualifier),
+ Invariant,
+ Parameter(ParameterQualifier),
+ Memory(MemoryQualifier),
+ Precise,
+}
+
+/// Single declaration.
+#[derive(Clone, Debug, PartialEq)]
+pub struct SingleDeclaration {
+ pub ty: Type,
+ pub ty_def: Option<SymRef>,
+ pub qualifier: Option<TypeQualifier>,
+ pub name: SymRef,
+ pub initializer: Option<Initializer>,
+}
+
+/// A single declaration with implicit, already-defined type.
+#[derive(Clone, Debug, PartialEq)]
+pub struct SingleDeclarationNoType {
+ pub ident: ArrayedIdentifier,
+ pub initializer: Option<Initializer>,
+}
+
+/// Initializer.
+#[derive(Clone, Debug, PartialEq)]
+pub enum Initializer {
+ Simple(Box<Expr>),
+ List(NonEmpty<Initializer>),
+}
+
+impl From<Expr> for Initializer {
+ fn from(e: Expr) -> Self {
+ Initializer::Simple(Box::new(e))
+ }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct Expr {
+ pub kind: ExprKind,
+ pub ty: Type,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+pub enum FieldSet {
+ Rgba,
+ Xyzw,
+ Stpq,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct SwizzleSelector {
+ pub field_set: FieldSet,
+ pub components: Vec<i8>,
+}
+
+impl SwizzleSelector {
+ fn parse(s: &str) -> Self {
+ let mut components = Vec::new();
+ let mut field_set = Vec::new();
+
+ for c in s.chars() {
+ match c {
+ 'r' => {
+ components.push(0);
+ field_set.push(FieldSet::Rgba);
+ }
+ 'x' => {
+ components.push(0);
+ field_set.push(FieldSet::Xyzw);
+ }
+ 's' => {
+ components.push(0);
+ field_set.push(FieldSet::Stpq);
+ }
+
+ 'g' => {
+ components.push(1);
+ field_set.push(FieldSet::Rgba);
+ }
+ 'y' => {
+ components.push(1);
+ field_set.push(FieldSet::Xyzw);
+ }
+ 't' => {
+ components.push(1);
+ field_set.push(FieldSet::Stpq);
+ }
+
+ 'b' => {
+ components.push(2);
+ field_set.push(FieldSet::Rgba);
+ }
+ 'z' => {
+ components.push(2);
+ field_set.push(FieldSet::Xyzw);
+ }
+ 'p' => {
+ components.push(2);
+ field_set.push(FieldSet::Stpq);
+ }
+
+ 'a' => {
+ components.push(3);
+ field_set.push(FieldSet::Rgba);
+ }
+ 'w' => {
+ components.push(3);
+ field_set.push(FieldSet::Xyzw);
+ }
+ 'q' => {
+ components.push(3);
+ field_set.push(FieldSet::Stpq);
+ }
+ _ => panic!("bad selector"),
+ }
+ }
+
+ let first = &field_set[0];
+ assert!(field_set.iter().all(|item| item == first));
+ assert!(components.len() <= 4);
+ SwizzleSelector {
+ field_set: first.clone(),
+ components,
+ }
+ }
+
+ pub fn to_field_set(&self, field_set: FieldSet) -> String {
+ let mut s = String::new();
+ let fs = match field_set {
+ FieldSet::Rgba => ['r', 'g', 'b', 'a'],
+ FieldSet::Xyzw => ['x', 'y', 'z', 'w'],
+ FieldSet::Stpq => ['s', 't', 'p', 'q'],
+ };
+ for i in &self.components {
+ s.push(fs[*i as usize])
+ }
+ s
+ }
+
+ pub fn to_string(&self) -> String {
+ self.to_field_set(self.field_set)
+ }
+}
+
+/// The most general form of an expression. As you can see if you read the variant list, in GLSL, an
+/// assignment is an expression. This is a bit silly but think of an assignment as a statement first
+/// then an expression which evaluates to what the statement “returns”.
+///
+/// An expression is either an assignment or a list (comma) of assignments.
+#[derive(Clone, Debug, PartialEq)]
+pub enum ExprKind {
+ /// A variable expression, using an identifier.
+ Variable(SymRef),
+ /// Integral constant expression.
+ IntConst(i32),
+ /// Unsigned integral constant expression.
+ UIntConst(u32),
+ /// Boolean constant expression.
+ BoolConst(bool),
+ /// Single precision floating expression.
+ FloatConst(f32),
+ /// Double precision floating expression.
+ DoubleConst(f64),
+ /// A unary expression, gathering a single expression and a unary operator.
+ Unary(UnaryOp, Box<Expr>),
+ /// A binary expression, gathering two expressions and a binary operator.
+ Binary(BinaryOp, Box<Expr>, Box<Expr>),
+ /// A ternary conditional expression, gathering three expressions.
+ Ternary(Box<Expr>, Box<Expr>, Box<Expr>),
+ /// An assignment is also an expression. Gathers an expression that defines what to assign to, an
+ /// assignment operator and the value to associate with.
+ Assignment(Box<Expr>, AssignmentOp, Box<Expr>),
+ /// Add an array specifier to an expression.
+ Bracket(Box<Expr>, Vec<Expr>),
+ /// A functional call. It has a function identifier and a list of expressions (arguments).
+ FunCall(FunIdentifier, Vec<Expr>),
+ /// An expression associated with a field selection (struct).
+ Dot(Box<Expr>, Identifier),
+ /// An expression associated with a component selection
+ SwizzleSelector(Box<Expr>, SwizzleSelector),
+ /// Post-incrementation of an expression.
+ PostInc(Box<Expr>),
+ /// Post-decrementation of an expression.
+ PostDec(Box<Expr>),
+ /// An expression that contains several, separated with comma.
+ Comma(Box<Expr>, Box<Expr>),
+ /// A temporary condition variable
+ Cond(usize, Box<Expr>),
+ CondMask,
+}
+
+/*
+impl From<i32> for Expr {
+ fn from(x: i32) -> Expr {
+ ExprKind::IntConst(x)
+ }
+}
+
+impl From<u32> for Expr {
+ fn from(x: u32) -> Expr {
+ Expr::UIntConst(x)
+ }
+}
+
+impl From<bool> for Expr {
+ fn from(x: bool) -> Expr {
+ Expr::BoolConst(x)
+ }
+}
+
+impl From<f32> for Expr {
+ fn from(x: f32) -> Expr {
+ Expr::FloatConst(x)
+ }
+}
+
+impl From<f64> for Expr {
+ fn from(x: f64) -> Expr {
+ Expr::DoubleConst(x)
+ }
+}
+*/
+/// Starting rule.
+#[derive(Clone, Debug, PartialEq)]
+pub struct TranslationUnit(pub NonEmpty<ExternalDeclaration>);
+
+impl TranslationUnit {
+ /// Construct a translation unit from an iterator.
+ ///
+ /// # Errors
+ ///
+ /// `None` if the iterator yields no value.
+ pub fn from_iter<I>(iter: I) -> Option<Self>
+ where
+ I: IntoIterator<Item = ExternalDeclaration>,
+ {
+ NonEmpty::from_non_empty_iter(iter).map(TranslationUnit)
+ }
+}
+
+impl Deref for TranslationUnit {
+ type Target = NonEmpty<ExternalDeclaration>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl DerefMut for TranslationUnit {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+impl IntoIterator for TranslationUnit {
+ type IntoIter = <NonEmpty<ExternalDeclaration> as IntoIterator>::IntoIter;
+ type Item = ExternalDeclaration;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.0.into_iter()
+ }
+}
+
+impl<'a> IntoIterator for &'a TranslationUnit {
+ type IntoIter = <&'a NonEmpty<ExternalDeclaration> as IntoIterator>::IntoIter;
+ type Item = &'a ExternalDeclaration;
+
+ fn into_iter(self) -> Self::IntoIter {
+ (&self.0).into_iter()
+ }
+}
+
+impl<'a> IntoIterator for &'a mut TranslationUnit {
+ type IntoIter = <&'a mut NonEmpty<ExternalDeclaration> as IntoIterator>::IntoIter;
+ type Item = &'a mut ExternalDeclaration;
+
+ fn into_iter(self) -> Self::IntoIter {
+ (&mut self.0).into_iter()
+ }
+}
+
+/// External declaration.
+#[derive(Clone, Debug, PartialEq)]
+pub enum ExternalDeclaration {
+ Preprocessor(syntax::Preprocessor),
+ FunctionDefinition(Rc<FunctionDefinition>),
+ Declaration(Declaration),
+}
+
+/// Function definition.
+#[derive(Clone, Debug, PartialEq)]
+pub struct FunctionDefinition {
+ pub prototype: FunctionPrototype,
+ pub body: CompoundStatement,
+ pub globals: Vec<SymRef>,
+ pub texel_fetches: HashMap<(SymRef, SymRef), TexelFetchOffsets>,
+}
+
+/// Compound statement (with no new scope).
+#[derive(Clone, Debug, PartialEq)]
+pub struct CompoundStatement {
+ pub statement_list: Vec<Statement>,
+}
+
+impl CompoundStatement {
+ pub fn new() -> Self {
+ CompoundStatement {
+ statement_list: Vec::new(),
+ }
+ }
+}
+
+impl FromIterator<Statement> for CompoundStatement {
+ fn from_iter<T>(iter: T) -> Self
+ where
+ T: IntoIterator<Item = Statement>,
+ {
+ CompoundStatement {
+ statement_list: iter.into_iter().collect(),
+ }
+ }
+}
+
+/// Statement.
+#[derive(Clone, Debug, PartialEq)]
+pub enum Statement {
+ Compound(Box<CompoundStatement>),
+ Simple(Box<SimpleStatement>),
+}
+
+/// Simple statement.
+#[derive(Clone, Debug, PartialEq)]
+pub enum SimpleStatement {
+ Declaration(Declaration),
+ Expression(ExprStatement),
+ Selection(SelectionStatement),
+ Switch(SwitchStatement),
+ Iteration(IterationStatement),
+ Jump(JumpStatement),
+}
+
+impl SimpleStatement {
+ /// Create a new expression statement.
+ pub fn new_expr<E>(expr: E) -> Self
+ where
+ E: Into<Expr>,
+ {
+ SimpleStatement::Expression(Some(expr.into()))
+ }
+
+ /// Create a new selection statement (if / else).
+ pub fn new_if_else<If, True, False>(ife: If, truee: True, falsee: False) -> Self
+ where
+ If: Into<Expr>,
+ True: Into<Statement>,
+ False: Into<Statement>,
+ {
+ SimpleStatement::Selection(SelectionStatement {
+ cond: Box::new(ife.into()),
+ body: Box::new(truee.into()),
+ else_stmt: Some(Box::new(falsee.into())),
+ })
+ }
+
+ /// Create a new while statement.
+ pub fn new_while<C, S>(cond: C, body: S) -> Self
+ where
+ C: Into<Condition>,
+ S: Into<Statement>,
+ {
+ SimpleStatement::Iteration(IterationStatement::While(
+ cond.into(),
+ Box::new(body.into()),
+ ))
+ }
+
+ /// Create a new do-while statement.
+ pub fn new_do_while<C, S>(body: S, cond: C) -> Self
+ where
+ S: Into<Statement>,
+ C: Into<Expr>,
+ {
+ SimpleStatement::Iteration(IterationStatement::DoWhile(
+ Box::new(body.into()),
+ Box::new(cond.into()),
+ ))
+ }
+}
+
+/// Expression statement.
+pub type ExprStatement = Option<Expr>;
+
+/// Selection statement.
+#[derive(Clone, Debug, PartialEq)]
+pub struct SelectionStatement {
+ pub cond: Box<Expr>,
+ pub body: Box<Statement>,
+ // the else branch
+ pub else_stmt: Option<Box<Statement>>,
+}
+
+/// Condition.
+#[derive(Clone, Debug, PartialEq)]
+pub enum Condition {
+ Expr(Box<Expr>),
+}
+
+impl From<Expr> for Condition {
+ fn from(expr: Expr) -> Self {
+ Condition::Expr(Box::new(expr))
+ }
+}
+
+/// Switch statement.
+#[derive(Clone, Debug, PartialEq)]
+pub struct SwitchStatement {
+ pub head: Box<Expr>,
+ pub cases: Vec<Case>,
+}
+
+/// Case label statement.
+#[derive(Clone, Debug, PartialEq)]
+pub enum CaseLabel {
+ Case(Box<Expr>),
+ Def,
+}
+
+/// An individual case
+#[derive(Clone, Debug, PartialEq)]
+pub struct Case {
+ pub label: CaseLabel,
+ pub stmts: Vec<Statement>,
+}
+
+/// Iteration statement.
+#[derive(Clone, Debug, PartialEq)]
+pub enum IterationStatement {
+ While(Condition, Box<Statement>),
+ DoWhile(Box<Statement>, Box<Expr>),
+ For(ForInitStatement, ForRestStatement, Box<Statement>),
+}
+
+/// For init statement.
+#[derive(Clone, Debug, PartialEq)]
+pub enum ForInitStatement {
+ Expression(Option<Expr>),
+ Declaration(Box<Declaration>),
+}
+
+/// For init statement.
+#[derive(Clone, Debug, PartialEq)]
+pub struct ForRestStatement {
+ pub condition: Option<Condition>,
+ pub post_expr: Option<Box<Expr>>,
+}
+
+/// Jump statement.
+#[derive(Clone, Debug, PartialEq)]
+pub enum JumpStatement {
+ Continue,
+ Break,
+ Return(Option<Box<Expr>>),
+ Discard,
+}
+
+trait NonEmptyExt<T> {
+ fn map<U, F: FnMut(&mut State, &T) -> U>(&self, s: &mut State, f: F) -> NonEmpty<U>;
+ fn new(x: T) -> NonEmpty<T>;
+}
+
+impl<T> NonEmptyExt<T> for NonEmpty<T> {
+ fn map<U, F: FnMut(&mut State, &T) -> U>(&self, s: &mut State, mut f: F) -> NonEmpty<U> {
+ NonEmpty::from_non_empty_iter(self.into_iter().map(|x| f(s, &x))).unwrap()
+ }
+ fn new(x: T) -> NonEmpty<T> {
+ NonEmpty::from_non_empty_iter(vec![x].into_iter()).unwrap()
+ }
+}
+
+fn translate_initializater(state: &mut State, i: &syntax::Initializer) -> Initializer {
+ match i {
+ syntax::Initializer::Simple(i) => {
+ Initializer::Simple(Box::new(translate_expression(state, i)))
+ }
+ _ => panic!(),
+ }
+}
+
+fn translate_struct_declaration(state: &mut State, d: &syntax::SingleDeclaration) -> Declaration {
+ let ty = d.ty.clone();
+ let ty_def = match &ty.ty.ty {
+ TypeSpecifierNonArray::Struct(s) => {
+ let decl = SymDecl::Struct(lift(state, s));
+ Some(state.declare(s.name.as_ref().unwrap().as_str(), decl))
+ }
+ _ => None,
+ };
+
+ let ty_def = ty_def.expect("Must be type definition");
+
+ Declaration::StructDefinition(ty_def)
+}
+
+fn get_expr_index(e: &syntax::Expr) -> i32 {
+ match e {
+ syntax::Expr::IntConst(i) => *i,
+ syntax::Expr::UIntConst(u) => *u as i32,
+ syntax::Expr::FloatConst(f) => *f as i32,
+ syntax::Expr::DoubleConst(f) => *f as i32,
+ _ => panic!(),
+ }
+}
+
+fn translate_variable_declaration(
+ state: &mut State,
+ d: &syntax::InitDeclaratorList,
+ default_run_class: RunClass,
+) -> Declaration {
+ let mut ty = d.head.ty.clone();
+ ty.ty.array_specifier = d.head.array_specifier.clone();
+ let ty_def = match &ty.ty.ty {
+ TypeSpecifierNonArray::Struct(s) => {
+ let decl = SymDecl::Struct(lift(state, s));
+ Some(state.declare(s.name.as_ref().unwrap().as_str(), decl))
+ }
+ _ => None,
+ };
+
+ let mut ty: Type = lift(state, &d.head.ty);
+ if let Some(array) = &d.head.array_specifier {
+ ty.array_sizes = Some(Box::new(lift(state, array)))
+ }
+
+ let (sym, decl) = match d.head.name.as_ref() {
+ Some(name) => {
+ let mut storage = StorageClass::None;
+ let mut interpolation = None;
+ for qual in d
+ .head
+ .ty
+ .qualifier
+ .iter()
+ .flat_map(|x| x.qualifiers.0.iter())
+ {
+ match qual {
+ syntax::TypeQualifierSpec::Storage(s) => match (&storage, s) {
+ (StorageClass::FragColor(..), syntax::StorageQualifier::Out) => {}
+ (StorageClass::Sampler(..), syntax::StorageQualifier::Uniform) => {}
+ (StorageClass::None, syntax::StorageQualifier::Out) => {
+ storage = StorageClass::Out;
+ }
+ (StorageClass::None, syntax::StorageQualifier::In) => {
+ storage = StorageClass::In;
+ }
+ (StorageClass::None, syntax::StorageQualifier::Uniform) => {
+ if ty.kind.is_sampler() {
+ storage = StorageClass::Sampler(SamplerFormat::Unknown);
+ } else {
+ storage = StorageClass::Uniform;
+ }
+ }
+ (StorageClass::None, syntax::StorageQualifier::Const) => {
+ storage = StorageClass::Const;
+ }
+ _ => panic!("bad storage {:?}", (storage, s)),
+ },
+ syntax::TypeQualifierSpec::Interpolation(i) => match (&interpolation, i) {
+ (None, i) => interpolation = Some(i.clone()),
+ _ => panic!("multiple interpolation"),
+ },
+ syntax::TypeQualifierSpec::Layout(l) => {
+ let mut loc = -1;
+ let mut index = -1;
+ for id in &l.ids {
+ match id {
+ syntax::LayoutQualifierSpec::Identifier(ref key, None) => {
+ match key.as_str() {
+ "rgba8" => {
+ storage = StorageClass::Sampler(SamplerFormat::RGBA8);
+ }
+ "rgba32f" => {
+ storage = StorageClass::Sampler(SamplerFormat::RGBA32F);
+ }
+ "rgba32i" => {
+ storage = StorageClass::Sampler(SamplerFormat::RGBA32I);
+ }
+ "r8" => {
+ storage = StorageClass::Sampler(SamplerFormat::R8);
+ }
+ "rg8" => {
+ storage = StorageClass::Sampler(SamplerFormat::RG8);
+ }
+ _ => {}
+ }
+ }
+ syntax::LayoutQualifierSpec::Identifier(ref key, Some(ref e)) => {
+ match key.as_str() {
+ "location" => {
+ loc = get_expr_index(e);
+ }
+ "index" => {
+ index = get_expr_index(e);
+ }
+ _ => {}
+ }
+ }
+ _ => {}
+ }
+ }
+ if index >= 0 {
+ assert!(loc == 0);
+ assert!(index <= 1);
+ assert!(storage == StorageClass::None);
+ storage = StorageClass::FragColor(index);
+ }
+ }
+ _ => {}
+ }
+ }
+ let decl = if state.in_function.is_some() {
+ let run_class = match storage {
+ StorageClass::Const => RunClass::Scalar,
+ StorageClass::None => default_run_class,
+ _ => panic!("bad local storage {:?}", storage),
+ };
+ SymDecl::Local(storage, ty.clone(), run_class)
+ } else {
+ let run_class = match storage {
+ StorageClass::Const | StorageClass::Uniform | StorageClass::Sampler(..) => {
+ RunClass::Scalar
+ }
+ StorageClass::In | StorageClass::Out | StorageClass::FragColor(..)
+ if interpolation == Some(syntax::InterpolationQualifier::Flat) =>
+ {
+ RunClass::Scalar
+ }
+ _ => RunClass::Vector,
+ };
+ SymDecl::Global(storage, interpolation, ty.clone(), run_class)
+ };
+ (state.declare(name.as_str(), decl.clone()), decl)
+ }
+ None => panic!(),
+ };
+
+ let head = SingleDeclaration {
+ qualifier: lift_type_qualifier_for_declaration(state, &d.head.ty.qualifier),
+ name: sym,
+ ty,
+ ty_def,
+ initializer: d
+ .head
+ .initializer
+ .as_ref()
+ .map(|x| translate_initializater(state, x)),
+ };
+
+ let tail = d
+ .tail
+ .iter()
+ .map(|d| {
+ if let Some(_array) = &d.ident.array_spec {
+ panic!("unhandled array")
+ }
+ state.declare(d.ident.ident.as_str(), decl.clone());
+ SingleDeclarationNoType {
+ ident: d.ident.clone(),
+ initializer: d
+ .initializer
+ .as_ref()
+ .map(|x| translate_initializater(state, x)),
+ }
+ })
+ .collect();
+ Declaration::InitDeclaratorList(InitDeclaratorList { head, tail })
+}
+
+fn translate_init_declarator_list(
+ state: &mut State,
+ l: &syntax::InitDeclaratorList,
+ default_run_class: RunClass,
+) -> Declaration {
+ match &l.head.name {
+ Some(_name) => translate_variable_declaration(state, l, default_run_class),
+ None => translate_struct_declaration(state, &l.head),
+ }
+}
+
+fn translate_declaration(
+ state: &mut State,
+ d: &syntax::Declaration,
+ default_run_class: RunClass,
+) -> Declaration {
+ match d {
+ syntax::Declaration::Block(_) => panic!(), //Declaration::Block(..),
+ syntax::Declaration::FunctionPrototype(p) => {
+ Declaration::FunctionPrototype(translate_function_prototype(state, p))
+ }
+ syntax::Declaration::Global(ty, ids) => {
+ // glsl non-es supports requalifying variables, but we don't yet.
+ // However, we still want to allow global layout qualifiers for
+ // KHR_advanced_blend_equation.
+ if !ids.is_empty() {
+ panic!();
+ }
+ let _ = for qual in &ty.qualifiers {
+ match qual {
+ syntax::TypeQualifierSpec::Layout(l) => {
+ for id in &l.ids {
+ match id {
+ syntax::LayoutQualifierSpec::Identifier(key, _) => {
+ match key.as_str() {
+ "blend_support_all_equations" => (),
+ _ => panic!(),
+ }
+ }
+ _ => panic!(),
+ }
+ }
+ }
+ syntax::TypeQualifierSpec::Storage(syntax::StorageQualifier::Out) => (),
+ _ => panic!(),
+ }
+ };
+ Declaration::Global(lift_type_qualifier_for_declaration(state, &Some(ty.clone())).unwrap(), ids.clone())
+ }
+ syntax::Declaration::InitDeclaratorList(dl) => {
+ translate_init_declarator_list(state, dl, default_run_class)
+ }
+ syntax::Declaration::Precision(p, ts) => Declaration::Precision(p.clone(), ts.clone()),
+ }
+}
+
+fn is_vector(ty: &Type) -> bool {
+ match ty.kind {
+ TypeKind::Vec2
+ | TypeKind::Vec3
+ | TypeKind::Vec4
+ | TypeKind::BVec2
+ | TypeKind::BVec3
+ | TypeKind::BVec4
+ | TypeKind::IVec2
+ | TypeKind::IVec3
+ | TypeKind::IVec4 => ty.array_sizes == None,
+ _ => false,
+ }
+}
+
+fn index_vector(ty: &Type) -> Option<TypeKind> {
+ use TypeKind::*;
+ if ty.array_sizes != None {
+ return None;
+ }
+ Some(match ty.kind {
+ Vec2 => Float,
+ Vec3 => Float,
+ Vec4 => Float,
+ DVec2 => Double,
+ DVec3 => Double,
+ DVec4 => Double,
+ BVec2 => Bool,
+ BVec3 => Bool,
+ BVec4 => Bool,
+ IVec2 => Int,
+ IVec3 => Int,
+ IVec4 => Int,
+ UVec2 => UInt,
+ UVec3 => UInt,
+ UVec4 => UInt,
+ _ => return None,
+ })
+
+}
+
+fn index_matrix(ty: &Type) -> Option<TypeKind> {
+ use TypeKind::*;
+ if ty.array_sizes != None {
+ return None;
+ }
+ Some(match ty.kind {
+ Mat2 => Vec2,
+ Mat3 => Vec3,
+ Mat4 => Vec4,
+ Mat23 => Vec3,
+ Mat24 => Vec4,
+ Mat32 => Vec2,
+ Mat34 => Vec4,
+ Mat42 => Vec2,
+ Mat43 => Vec3,
+ DMat2 => DVec2,
+ DMat3 => DVec3,
+ DMat4 => DVec4,
+ DMat23 => DVec3,
+ DMat24 => DVec4,
+ DMat32 => DVec2,
+ DMat34 => DVec4,
+ DMat42 => DVec2,
+ DMat43 => DVec3,
+ _ => return None,
+ })
+}
+
+fn is_ivec(ty: &Type) -> bool {
+ match ty.kind {
+ TypeKind::IVec2 | TypeKind::IVec3 | TypeKind::IVec4 => ty.array_sizes == None,
+ _ => false,
+ }
+}
+
+fn can_implicitly_convert_to(src: &Type, dst: &Type) -> bool {
+ // XXX: use an underlying type helper
+ if src == &Type::new(TypeKind::Double) && dst == &Type::new(TypeKind::Float) {
+ // We're not supposed to implicitly convert from double to float but glsl 4 has a bug
+ // where it parses unannotated float constants as double.
+ true
+ } else if dst == &Type::new(TypeKind::Double) && src == &Type::new(TypeKind::Float) {
+ true
+ } else if (dst == &Type::new(TypeKind::Float) || dst == &Type::new(TypeKind::Double)) &&
+ src == &Type::new(TypeKind::Int)
+ {
+ true
+ } else if (dst == &Type::new(TypeKind::Vec2) || dst == &Type::new(TypeKind::DVec2)) &&
+ src == &Type::new(TypeKind::IVec2)
+ {
+ true
+ } else if dst == &Type::new(TypeKind::IVec2) &&
+ (src == &Type::new(TypeKind::Vec2) || src == &Type::new(TypeKind::DVec2))
+ {
+ true
+ } else {
+ src.kind == dst.kind && src.array_sizes == dst.array_sizes
+ }
+}
+
+fn promoted_type(lhs: &Type, rhs: &Type) -> Type {
+ if lhs == &Type::new(TypeKind::Double) && rhs == &Type::new(TypeKind::Float) {
+ Type::new(TypeKind::Double)
+ } else if lhs == &Type::new(TypeKind::Float) && rhs == &Type::new(TypeKind::Double) {
+ Type::new(TypeKind::Double)
+ } else if lhs == &Type::new(TypeKind::Int) && rhs == &Type::new(TypeKind::Double) {
+ Type::new(TypeKind::Double)
+ } else if is_vector(&lhs) &&
+ (rhs == &Type::new(TypeKind::Float) ||
+ rhs == &Type::new(TypeKind::Double) ||
+ rhs == &Type::new(TypeKind::Int))
+ {
+ // scalars promote to vectors
+ lhs.clone()
+ } else if is_vector(&rhs) &&
+ (lhs == &Type::new(TypeKind::Float) ||
+ lhs == &Type::new(TypeKind::Double) ||
+ lhs == &Type::new(TypeKind::Int))
+ {
+ // scalars promote to vectors
+ rhs.clone()
+ } else if lhs == rhs {
+ lhs.clone()
+ } else if lhs.kind == rhs.kind {
+ if lhs.array_sizes == rhs.array_sizes {
+ // XXX: we need to be able to query the default precision here
+ match (&lhs.precision, &rhs.precision) {
+ (Some(PrecisionQualifier::High), _) => lhs.clone(),
+ (_, Some(PrecisionQualifier::High)) => rhs.clone(),
+ (None, _) => lhs.clone(),
+ (_, None) => rhs.clone(),
+ _ => panic!("precision mismatch {:?} {:?}", lhs.precision, rhs.precision),
+ }
+ } else {
+ panic!("array size mismatch")
+ }
+ } else {
+ assert_eq!(lhs, rhs);
+ lhs.clone()
+ }
+}
+
+pub fn is_output(expr: &Expr, state: &State) -> Option<SymRef> {
+ match &expr.kind {
+ ExprKind::Variable(i) => match state.sym(*i).decl {
+ SymDecl::Global(storage, ..) => match storage {
+ StorageClass::In | StorageClass::Out => return Some(*i),
+ _ => {}
+ },
+ SymDecl::Local(..) => {}
+ _ => panic!("should be variable"),
+ },
+ ExprKind::SwizzleSelector(e, ..) => {
+ return is_output(e, state);
+ }
+ ExprKind::Bracket(e, ..) => {
+ return is_output(e, state);
+ }
+ ExprKind::Dot(e, ..) => {
+ return is_output(e, state);
+ }
+ _ => {}
+ };
+ None
+}
+
+pub fn get_texel_fetch_offset(
+ state: &State,
+ sampler_expr: &Expr,
+ uv_expr: &Expr,
+ offset_expr: &Expr,
+) -> Option<(SymRef, SymRef, i32, i32)> {
+ if let ExprKind::Variable(ref sampler) = &sampler_expr.kind {
+ //if let ExprKind::Binary(BinaryOp::Add, ref lhs, ref rhs) = &uv_expr.kind {
+ if let ExprKind::Variable(ref base) = &uv_expr.kind {
+ if let ExprKind::FunCall(ref fun, ref args) = &offset_expr.kind {
+ if let FunIdentifier::Identifier(ref offset) = fun {
+ if state.sym(*offset).name == "ivec2" {
+ if let ExprKind::IntConst(ref x) = &args[0].kind {
+ if let ExprKind::IntConst(ref y) = &args[1].kind {
+ return Some((*sampler, *base, *x, *y));
+ }
+ }
+ }
+ }
+ }
+ }
+ //}
+ }
+ None
+}
+
+fn make_const(t: TypeKind, v: i32) -> Expr {
+ Expr {
+ kind: match t {
+ TypeKind::Int => ExprKind::IntConst(v as _),
+ TypeKind::UInt => ExprKind::UIntConst(v as _),
+ TypeKind::Bool => ExprKind::BoolConst(v != 0),
+ TypeKind::Float => ExprKind::FloatConst(v as _),
+ TypeKind::Double => ExprKind::DoubleConst(v as _),
+ _ => panic!("bad constant type"),
+ },
+ ty: Type::new(t),
+ }
+}
+
+// Any parameters needing to convert to bool should just compare via != 0.
+// This ensures they get the proper all-1s pattern for C++ OpenCL vectors.
+fn force_params_to_bool(_state: &mut State, params: &mut Vec<Expr>) {
+ for e in params {
+ if !e.ty.kind.is_bool() {
+ let k = e.ty.kind;
+ *e = Expr {
+ kind: ExprKind::Binary(
+ BinaryOp::NonEqual,
+ Box::new(e.clone()),
+ Box::new(make_const(k.to_scalar(), 0)),
+ ),
+ ty: Type::new(k.to_bool()),
+ };
+ }
+ }
+}
+
+// Transform bool params to int, then mask off the low bit so they become 0 or 1.
+// C++ OpenCL vectors represent bool as all-1s patterns, which will erroneously
+// convert to -1 otherwise.
+fn force_params_from_bool(state: &mut State, params: &mut Vec<Expr>) {
+ for e in params {
+ if e.ty.kind.is_bool() {
+ let k = e.ty.kind.to_int();
+ let sym = state.lookup(k.glsl_primitive_type_name().unwrap()).unwrap();
+ *e = Expr {
+ kind: ExprKind::Binary(
+ BinaryOp::BitAnd,
+ Box::new(Expr {
+ kind: ExprKind::FunCall(
+ FunIdentifier::Identifier(sym),
+ vec![e.clone()],
+ ),
+ ty: Type::new(k),
+ }),
+ Box::new(make_const(TypeKind::Int, 1)),
+ ),
+ ty: Type::new(k),
+ };
+ }
+ }
+}
+
+fn translate_expression(state: &mut State, e: &syntax::Expr) -> Expr {
+ match e {
+ syntax::Expr::Variable(i) => {
+ let sym = match state.lookup(i.as_str()) {
+ Some(sym) => sym,
+ None => panic!("missing declaration {}", i.as_str()),
+ };
+ let ty = match &state.sym(sym).decl {
+ SymDecl::Global(_, _, ty, _) => {
+ let mut globals = state.used_globals.borrow_mut();
+ if !globals.contains(&sym) {
+ globals.push(sym);
+ }
+ ty.clone()
+ }
+ SymDecl::Local(_, ty, _) => ty.clone(),
+ _ => panic!("bad variable type"),
+ };
+ Expr {
+ kind: ExprKind::Variable(sym),
+ ty,
+ }
+ }
+ syntax::Expr::Assignment(lhs, op, rhs) => {
+ let lhs = Box::new(translate_expression(state, lhs));
+ let rhs = Box::new(translate_expression(state, rhs));
+ let ty = if op == &AssignmentOp::Mult {
+ if lhs.ty.kind == TypeKind::Vec4 && rhs.ty.kind == TypeKind::Float {
+ lhs.ty.clone()
+ } else {
+ promoted_type(&lhs.ty, &rhs.ty)
+ }
+ } else {
+ promoted_type(&lhs.ty, &rhs.ty)
+ };
+ if let Some(global) = is_output(&lhs, state) {
+ let mut globals = state.modified_globals.borrow_mut();
+ if !globals.contains(&global) {
+ globals.push(global);
+ }
+ if global == state.clip_dist_sym {
+ if let ExprKind::Bracket(_, idx) = &lhs.kind {
+ // Get the constant array index used for gl_ClipDistance and add it to the used mask.
+ for dimension in idx {
+ let idx = match dimension.kind {
+ ExprKind::IntConst(idx) => idx,
+ ExprKind::UIntConst(idx) => idx as i32,
+ _ => panic!("bad index for gl_ClipDistance"),
+ };
+ assert!(idx >= 0 && idx < 4);
+ state.used_clip_dist |= 1 << idx;
+ }
+ }
+ }
+ }
+ Expr {
+ kind: ExprKind::Assignment(lhs, op.clone(), rhs),
+ ty,
+ }
+ }
+ syntax::Expr::Binary(op, lhs, rhs) => {
+ let lhs = Box::new(translate_expression(state, lhs));
+ let rhs = Box::new(translate_expression(state, rhs));
+ let ty = match op {
+ BinaryOp::Equal | BinaryOp::NonEqual | BinaryOp::GT | BinaryOp::GTE | BinaryOp::LT | BinaryOp::LTE => {
+ // comparison operators have a bool result
+ Type::new(TypeKind::Bool)
+ }
+ BinaryOp::Mult => {
+ match (lhs.ty.kind, rhs.ty.kind) {
+ (TypeKind::Mat2, TypeKind::Vec2) |
+ (TypeKind::Mat3, TypeKind::Vec3) |
+ (TypeKind::Mat3, TypeKind::Mat3) |
+ (TypeKind::Mat3, TypeKind::Mat43) |
+ (TypeKind::Mat4, TypeKind::Vec4) => rhs.ty.clone(),
+ (TypeKind::Mat43, TypeKind::Vec4) => Type::new(TypeKind::Vec3),
+ (TypeKind::Mat2, TypeKind::Float) |
+ (TypeKind::Mat3, TypeKind::Float) |
+ (TypeKind::Mat4, TypeKind::Float) => lhs.ty.clone(),
+ _ => promoted_type(&lhs.ty, &rhs.ty),
+ }
+ }
+ _ => promoted_type(&lhs.ty, &rhs.ty),
+ };
+
+ Expr {
+ kind: ExprKind::Binary(op.clone(), lhs, rhs),
+ ty,
+ }
+ }
+ syntax::Expr::Unary(op, e) => {
+ let e = Box::new(translate_expression(state, e));
+ let ty = e.ty.clone();
+ Expr {
+ kind: ExprKind::Unary(op.clone(), e),
+ ty,
+ }
+ }
+ syntax::Expr::BoolConst(b) => Expr {
+ kind: ExprKind::BoolConst(*b),
+ ty: Type::new(TypeKind::Bool),
+ },
+ syntax::Expr::Comma(lhs, rhs) => {
+ let lhs = Box::new(translate_expression(state, lhs));
+ let rhs = Box::new(translate_expression(state, rhs));
+ assert_eq!(lhs.ty, rhs.ty);
+ let ty = lhs.ty.clone();
+ Expr {
+ kind: ExprKind::Comma(lhs, rhs),
+ ty,
+ }
+ }
+ syntax::Expr::DoubleConst(d) => Expr {
+ kind: ExprKind::DoubleConst(*d),
+ ty: Type::new(TypeKind::Double),
+ },
+ syntax::Expr::FloatConst(f) => Expr {
+ kind: ExprKind::FloatConst(*f),
+ ty: Type::new(TypeKind::Float),
+ },
+ syntax::Expr::FunCall(fun, params) => {
+ let ret_ty: Type;
+ let mut params: Vec<Expr> = params
+ .iter()
+ .map(|x| translate_expression(state, x))
+ .collect();
+ Expr {
+ kind: ExprKind::FunCall(
+ match fun {
+ syntax::FunIdentifier::Identifier(i) => {
+ let name = i.as_str();
+ if name == "texelFetchOffset" && params.len() >= 4 {
+ if let Some((sampler, base, x, y)) = get_texel_fetch_offset(
+ state, &params[0], &params[1], &params[3],
+ ) {
+ if let Some(offsets) =
+ state.texel_fetches.get_mut(&(sampler, base))
+ {
+ offsets.add_offset(x, y);
+ } else {
+ state
+ .texel_fetches
+ .insert((sampler, base), TexelFetchOffsets::new(x, y));
+ }
+ }
+ } else if name == "swgl_stepInterp" {
+ let mut globals = state.modified_globals.borrow_mut();
+ for (i, sym) in state.syms.iter().enumerate() {
+ match &sym.borrow().decl {
+ SymDecl::Global(StorageClass::In, _, _, RunClass::Vector) => {
+ let symref = SymRef(i as u32);
+ if !globals.contains(&symref) {
+ globals.push(symref);
+ }
+ }
+ _ => {}
+ }
+ }
+ }
+ let sym = match state.lookup(name) {
+ Some(s) => s,
+ None => panic!("missing symbol {}", name),
+ };
+ // Force any boolean basic type constructors to generate correct
+ // bit patterns.
+ if let Some(t) = TypeKind::from_glsl_primitive_type_name(name) {
+ if t.is_bool() {
+ force_params_to_bool(state, &mut params);
+ } else {
+ force_params_from_bool(state, &mut params);
+ }
+ }
+ match &state.sym(sym).decl {
+ SymDecl::NativeFunction(fn_ty, _, _) => {
+ // Search for a signature where all parameter types are
+ // compatible. If there are many compatible signatures,
+ // then choose the one with the most exact matches.
+ // This is an approximation of the algorith described in
+ // the "Function Definitions" section of the spec.
+ let mut ret = None;
+ let mut best_score = 0;
+ 'next_sig: for sig in &fn_ty.signatures {
+ let mut score = 0;
+ for (e, p) in params.iter().zip(sig.params.iter()) {
+ if e.ty == *p {
+ score += 1;
+ } else if !can_implicitly_convert_to(&e.ty, p) {
+ continue 'next_sig;
+ }
+ }
+ if score >= best_score {
+ ret = Some(sig.ret.clone());
+ best_score = score;
+ // If all parameters match exactly, then there
+ // is no need to search for other matches.
+ if best_score >= params.len() {
+ break;
+ }
+ }
+ }
+ ret_ty = match ret {
+ Some(t) => t,
+ None => {
+ dbg!(&fn_ty.signatures);
+ dbg!(params.iter().map(|p| p).collect::<Vec<_>>());
+ panic!("no matching func {}", i.as_str())
+ }
+ };
+ }
+ SymDecl::UserFunction(fd, _) => {
+ let mut globals = state.modified_globals.borrow_mut();
+ for global in &fd.globals {
+ if !globals.contains(global) {
+ globals.push(*global);
+ }
+ }
+ let mut matching = true;
+ for (e, p) in params.iter().zip(fd.prototype.parameters.iter())
+ {
+ matching &= match p {
+ FunctionParameterDeclaration::Named(q, d) => {
+ match q {
+ Some(ParameterQualifier::InOut)
+ | Some(ParameterQualifier::Out) => {
+ if let Some(global) = is_output(e, state) {
+ if !globals.contains(&global) {
+ globals.push(global);
+ }
+ }
+ }
+ _ => {}
+ }
+ can_implicitly_convert_to(&e.ty, &d.ty)
+ }
+ FunctionParameterDeclaration::Unnamed(..) => panic!(),
+ };
+ }
+ assert!(matching);
+ ret_ty = fd.prototype.ty.clone();
+ }
+ SymDecl::Struct(_) => ret_ty = Type::new(TypeKind::Struct(sym)),
+ _ => panic!("can only call functions"),
+ };
+ FunIdentifier::Identifier(sym)
+ }
+ // array constructor
+ syntax::FunIdentifier::Expr(e) => {
+ let ty = match &**e {
+ syntax::Expr::Bracket(i, array) => {
+ let kind = match &**i {
+ syntax::Expr::Variable(i) => match i.as_str() {
+ "vec4" => TypeKind::Vec4,
+ "vec2" => TypeKind::Vec2,
+ "int" => TypeKind::Int,
+ _ => panic!("unexpected type constructor {:?}", i),
+ },
+ _ => panic!(),
+ };
+
+ Type {
+ kind,
+ precision: None,
+ array_sizes: Some(Box::new(lift(state, array))),
+ }
+ }
+ _ => panic!(),
+ };
+ ret_ty = ty.clone();
+
+ FunIdentifier::Constructor(ty)
+ }
+ },
+ params,
+ ),
+ ty: ret_ty,
+ }
+ }
+ syntax::Expr::IntConst(i) => Expr {
+ kind: ExprKind::IntConst(*i),
+ ty: Type::new(TypeKind::Int),
+ },
+ syntax::Expr::UIntConst(u) => Expr {
+ kind: ExprKind::UIntConst(*u),
+ ty: Type::new(TypeKind::UInt),
+ },
+ syntax::Expr::PostDec(e) => {
+ let e = Box::new(translate_expression(state, e));
+ let ty = e.ty.clone();
+ Expr {
+ kind: ExprKind::PostDec(e),
+ ty,
+ }
+ }
+ syntax::Expr::PostInc(e) => {
+ let e = Box::new(translate_expression(state, e));
+ let ty = e.ty.clone();
+ Expr {
+ kind: ExprKind::PostInc(e),
+ ty,
+ }
+ }
+ syntax::Expr::Ternary(cond, lhs, rhs) => {
+ let cond = Box::new(translate_expression(state, cond));
+ let lhs = Box::new(translate_expression(state, lhs));
+ let rhs = Box::new(translate_expression(state, rhs));
+ let ty = promoted_type(&lhs.ty, &rhs.ty);
+ Expr {
+ kind: ExprKind::Ternary(cond, lhs, rhs),
+ ty,
+ }
+ }
+ syntax::Expr::Dot(e, i) => {
+ let e = Box::new(translate_expression(state, e));
+ let ty = e.ty.clone();
+ let ivec = is_ivec(&ty);
+ if is_vector(&ty) {
+ let ty = Type::new(match i.as_str().len() {
+ 1 => {
+ if ivec {
+ TypeKind::Int
+ } else {
+ TypeKind::Float
+ }
+ }
+ 2 => {
+ if ivec {
+ TypeKind::IVec2
+ } else {
+ TypeKind::Vec2
+ }
+ }
+ 3 => {
+ if ivec {
+ TypeKind::IVec3
+ } else {
+ TypeKind::Vec3
+ }
+ }
+ 4 => {
+ if ivec {
+ TypeKind::IVec4
+ } else {
+ TypeKind::Vec4
+ }
+ }
+ _ => panic!(),
+ });
+
+ let sel = SwizzleSelector::parse(i.as_str());
+
+ Expr {
+ kind: ExprKind::SwizzleSelector(e, sel),
+ ty,
+ }
+ } else {
+ match ty.kind {
+ TypeKind::Struct(s) => {
+ let sym = state.sym(s);
+ let fields = match &sym.decl {
+ SymDecl::Struct(fields) => fields,
+ _ => panic!("expected struct"),
+ };
+ let field = fields
+ .fields
+ .iter()
+ .find(|x| &x.name == i)
+ .expect(&format!("missing field `{}` in `{}`", i, sym.name));
+ Expr {
+ kind: ExprKind::Dot(e, i.clone()),
+ ty: field.ty.clone(),
+ }
+ }
+ _ => panic!("expected struct found {:#?} {:#?}", e, ty),
+ }
+ }
+ }
+ syntax::Expr::Bracket(e, specifier) => {
+ let e = Box::new(translate_expression(state, e));
+ let ty = if let Some(ty) = index_vector(&e.ty) {
+ Type::new(ty)
+ } else if let Some(ty) = index_matrix(&e.ty) {
+ Type::new(ty)
+ } else {
+ let a = match &e.ty.array_sizes {
+ Some(a) => {
+ let mut a = *a.clone();
+ a.sizes.pop();
+ if a.sizes.len() == 0 {
+ None
+ } else {
+ Some(Box::new(a))
+ }
+ }
+ _ => panic!("{:#?}", e),
+ };
+ Type {
+ kind: e.ty.kind.clone(),
+ precision: e.ty.precision.clone(),
+ array_sizes: a,
+ }
+ };
+ let indx = specifier.dimensions.0.iter().map(|a| match a {
+ ArraySpecifierDimension::Unsized => panic!("need expression"),
+ ArraySpecifierDimension::ExplicitlySized(e) => translate_expression(state, e),
+ }).collect();
+ Expr {
+ kind: ExprKind::Bracket(e, indx),
+ ty,
+ }
+ }
+ }
+}
+
+fn translate_switch(state: &mut State, s: &syntax::SwitchStatement) -> SwitchStatement {
+ let mut cases = Vec::new();
+
+ let mut case = None;
+ for stmt in &s.body {
+ match stmt {
+ syntax::Statement::Simple(s) => match &**s {
+ syntax::SimpleStatement::CaseLabel(label) => {
+ match case.take() {
+ Some(case) => cases.push(case),
+ _ => {}
+ }
+ case = Some(Case {
+ label: translate_case(state, &label),
+ stmts: Vec::new(),
+ })
+ }
+ _ => match case {
+ Some(ref mut case) => case.stmts.push(translate_statement(state, stmt)),
+ _ => panic!("switch must start with case"),
+ },
+ },
+ _ => match case {
+ Some(ref mut case) => case.stmts.push(translate_statement(state, stmt)),
+ _ => panic!("switch must start with case"),
+ },
+ }
+ }
+ match case.take() {
+ Some(case) => cases.push(case),
+ _ => {}
+ }
+ SwitchStatement {
+ head: Box::new(translate_expression(state, &s.head)),
+ cases,
+ }
+}
+
+fn translate_jump(state: &mut State, s: &syntax::JumpStatement) -> JumpStatement {
+ match s {
+ syntax::JumpStatement::Break => JumpStatement::Break,
+ syntax::JumpStatement::Continue => JumpStatement::Continue,
+ syntax::JumpStatement::Discard => JumpStatement::Discard,
+ syntax::JumpStatement::Return(e) => {
+ JumpStatement::Return(e.as_ref().map(|e| Box::new(translate_expression(state, e))))
+ }
+ }
+}
+
+fn translate_condition(state: &mut State, c: &syntax::Condition) -> Condition {
+ match c {
+ syntax::Condition::Expr(e) => Condition::Expr(Box::new(translate_expression(state, e))),
+ _ => panic!(),
+ }
+}
+
+fn translate_for_init(state: &mut State, s: &syntax::ForInitStatement) -> ForInitStatement {
+ match s {
+ syntax::ForInitStatement::Expression(e) => {
+ ForInitStatement::Expression(e.as_ref().map(|e| translate_expression(state, e)))
+ }
+ syntax::ForInitStatement::Declaration(d) => ForInitStatement::Declaration(Box::new(
+ translate_declaration(state, d, RunClass::Scalar),
+ )),
+ }
+}
+
+fn translate_for_rest(state: &mut State, s: &syntax::ForRestStatement) -> ForRestStatement {
+ ForRestStatement {
+ condition: s.condition.as_ref().map(|c| translate_condition(state, c)),
+ post_expr: s
+ .post_expr
+ .as_ref()
+ .map(|e| Box::new(translate_expression(state, e))),
+ }
+}
+
+fn translate_iteration(state: &mut State, s: &syntax::IterationStatement) -> IterationStatement {
+ match s {
+ syntax::IterationStatement::While(cond, s) => IterationStatement::While(
+ translate_condition(state, cond),
+ Box::new(translate_statement(state, s)),
+ ),
+ syntax::IterationStatement::For(init, rest, s) => IterationStatement::For(
+ translate_for_init(state, init),
+ translate_for_rest(state, rest),
+ Box::new(translate_statement(state, s)),
+ ),
+ syntax::IterationStatement::DoWhile(s, e) => IterationStatement::DoWhile(
+ Box::new(translate_statement(state, s)),
+ Box::new(translate_expression(state, e)),
+ ),
+ }
+}
+
+fn translate_case(state: &mut State, c: &syntax::CaseLabel) -> CaseLabel {
+ match c {
+ syntax::CaseLabel::Def => CaseLabel::Def,
+ syntax::CaseLabel::Case(e) => CaseLabel::Case(Box::new(translate_expression(state, e))),
+ }
+}
+
+fn translate_selection_rest(
+ state: &mut State,
+ s: &syntax::SelectionRestStatement,
+) -> (Box<Statement>, Option<Box<Statement>>) {
+ match s {
+ syntax::SelectionRestStatement::Statement(s) => {
+ (Box::new(translate_statement(state, s)), None)
+ }
+ syntax::SelectionRestStatement::Else(if_body, rest) => (
+ Box::new(translate_statement(state, if_body)),
+ Some(Box::new(translate_statement(state, rest))),
+ ),
+ }
+}
+
+fn translate_selection(state: &mut State, s: &syntax::SelectionStatement) -> SelectionStatement {
+ let cond = Box::new(translate_expression(state, &s.cond));
+ let (body, else_stmt) = translate_selection_rest(state, &s.rest);
+ SelectionStatement {
+ cond,
+ body,
+ else_stmt,
+ }
+}
+
+fn translate_simple_statement(state: &mut State, s: &syntax::SimpleStatement) -> SimpleStatement {
+ match s {
+ syntax::SimpleStatement::Declaration(d) => {
+ SimpleStatement::Declaration(translate_declaration(state, d, RunClass::Unknown))
+ }
+ syntax::SimpleStatement::Expression(e) => {
+ SimpleStatement::Expression(e.as_ref().map(|e| translate_expression(state, e)))
+ }
+ syntax::SimpleStatement::Iteration(i) => {
+ SimpleStatement::Iteration(translate_iteration(state, i))
+ }
+ syntax::SimpleStatement::Selection(s) => {
+ SimpleStatement::Selection(translate_selection(state, s))
+ }
+ syntax::SimpleStatement::Jump(j) => SimpleStatement::Jump(translate_jump(state, j)),
+ syntax::SimpleStatement::Switch(s) => SimpleStatement::Switch(translate_switch(state, s)),
+ syntax::SimpleStatement::CaseLabel(_) => panic!("should be handled by translate_switch"),
+ }
+}
+
+fn translate_statement(state: &mut State, s: &syntax::Statement) -> Statement {
+ match s {
+ syntax::Statement::Compound(s) => {
+ Statement::Compound(Box::new(translate_compound_statement(state, s)))
+ }
+ syntax::Statement::Simple(s) => {
+ Statement::Simple(Box::new(translate_simple_statement(state, s)))
+ }
+ }
+}
+
+fn translate_compound_statement(
+ state: &mut State,
+ cs: &syntax::CompoundStatement,
+) -> CompoundStatement {
+ CompoundStatement {
+ statement_list: cs
+ .statement_list
+ .iter()
+ .map(|x| translate_statement(state, x))
+ .collect(),
+ }
+}
+
+fn translate_function_parameter_declaration(
+ state: &mut State,
+ p: &syntax::FunctionParameterDeclaration,
+ index: usize,
+) -> FunctionParameterDeclaration {
+ match p {
+ syntax::FunctionParameterDeclaration::Named(qual, p) => {
+ let mut ty: Type = lift(state, &p.ty);
+ if let Some(a) = &p.ident.array_spec {
+ ty.array_sizes = Some(Box::new(lift(state, a)));
+ }
+
+ ty.precision = get_precision(qual);
+
+ let decl = SymDecl::Local(
+ StorageClass::None,
+ ty.clone(),
+ RunClass::Dependent(1 << index),
+ );
+ let d = FunctionParameterDeclarator {
+ ty,
+ name: p.ident.ident.clone(),
+ sym: state.declare(p.ident.ident.as_str(), decl),
+ };
+ FunctionParameterDeclaration::Named(lift_type_qualifier_for_parameter(state, qual), d)
+ }
+ syntax::FunctionParameterDeclaration::Unnamed(qual, p) => {
+ FunctionParameterDeclaration::Unnamed(
+ lift_type_qualifier_for_parameter(state, qual),
+ p.clone(),
+ )
+ }
+ }
+}
+
+fn translate_prototype(
+ state: &mut State,
+ cs: &syntax::FunctionPrototype,
+) -> (FunctionPrototype, SymRef) {
+ let prototype = FunctionPrototype {
+ ty: lift(state, &cs.ty),
+ name: cs.name.clone(),
+ parameters: cs
+ .parameters
+ .iter()
+ .enumerate()
+ .map(|(i, x)| translate_function_parameter_declaration(state, x, i))
+ .collect(),
+ };
+ let sym = if let Some(sym) = state.lookup(prototype.name.as_str()) {
+ match &state.sym(sym).decl {
+ SymDecl::UserFunction(..) => {}
+ _ => panic!(
+ "prototype conflicts with existing symbol: {}",
+ prototype.name.as_str()
+ ),
+ }
+ sym
+ } else {
+ let pfd = Rc::new(FunctionDefinition {
+ prototype: prototype.clone(),
+ body: CompoundStatement::new(),
+ globals: Vec::new(),
+ texel_fetches: HashMap::new(),
+ });
+ state.declare(
+ prototype.name.as_str(),
+ SymDecl::UserFunction(pfd, RunClass::Unknown),
+ )
+ };
+ (prototype, sym)
+}
+
+fn translate_function_prototype(
+ state: &mut State,
+ prototype: &syntax::FunctionPrototype,
+) -> FunctionPrototype {
+ let (prototype, _) = translate_prototype(state, prototype);
+ prototype
+}
+
+fn translate_function_definition(
+ state: &mut State,
+ sfd: &syntax::FunctionDefinition,
+) -> Rc<FunctionDefinition> {
+ let (prototype, sym) = translate_prototype(state, &sfd.prototype);
+
+ state.push_scope(prototype.name.as_str().into());
+ state.in_function = Some(sym);
+ state.modified_globals.get_mut().clear();
+ state.texel_fetches.clear();
+ let body = translate_compound_statement(state, &sfd.statement);
+ let mut globals = Vec::new();
+ mem::swap(&mut globals, state.modified_globals.get_mut());
+ let mut texel_fetches = HashMap::new();
+ mem::swap(&mut texel_fetches, &mut state.texel_fetches);
+ state.in_function = None;
+ state.pop_scope();
+
+ let fd = Rc::new(FunctionDefinition {
+ prototype,
+ body,
+ globals,
+ texel_fetches,
+ });
+ state.sym_mut(sym).decl = SymDecl::UserFunction(fd.clone(), RunClass::Unknown);
+ fd
+}
+
+fn translate_external_declaration(
+ state: &mut State,
+ ed: &syntax::ExternalDeclaration,
+) -> ExternalDeclaration {
+ match ed {
+ syntax::ExternalDeclaration::Declaration(d) => {
+ ExternalDeclaration::Declaration(translate_declaration(state, d, RunClass::Unknown))
+ }
+ syntax::ExternalDeclaration::FunctionDefinition(fd) => {
+ ExternalDeclaration::FunctionDefinition(translate_function_definition(state, fd))
+ }
+ syntax::ExternalDeclaration::Preprocessor(p) => {
+ ExternalDeclaration::Preprocessor(p.clone())
+ }
+ }
+}
+
+fn declare_function_ext(
+ state: &mut State,
+ name: &str,
+ cxx_name: Option<&'static str>,
+ ret: Type,
+ params: Vec<Type>,
+ run_class: RunClass,
+) {
+ let sig = FunctionSignature { ret, params };
+ match state.lookup_sym_mut(name) {
+ Some(Symbol {
+ decl: SymDecl::NativeFunction(f, ..),
+ ..
+ }) => f.signatures.push(sig),
+ None => {
+ state.declare(
+ name,
+ SymDecl::NativeFunction(
+ FunctionType {
+ signatures: NonEmpty::new(sig),
+ },
+ cxx_name,
+ run_class,
+ ),
+ );
+ }
+ _ => panic!("overloaded function name {}", name),
+ }
+ //state.declare(name, Type::Function(FunctionType{ v}))
+}
+
+fn declare_function(
+ state: &mut State,
+ name: &str,
+ cxx_name: Option<&'static str>,
+ ret: Type,
+ params: Vec<Type>,
+) {
+ declare_function_ext(state, name, cxx_name, ret, params, RunClass::Unknown)
+}
+
+pub fn ast_to_hir(state: &mut State, tu: &syntax::TranslationUnit) -> TranslationUnit {
+ // global scope
+ state.push_scope("global".into());
+ use TypeKind::*;
+ declare_function(
+ state,
+ "vec2",
+ Some("make_vec2"),
+ Type::new(Vec2),
+ vec![Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "vec2",
+ Some("make_vec2"),
+ Type::new(Vec2),
+ vec![Type::new(Float), Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "vec2",
+ Some("make_vec2"),
+ Type::new(Vec2),
+ vec![Type::new(IVec2)],
+ );
+ declare_function(
+ state,
+ "vec2",
+ Some("make_vec2"),
+ Type::new(Vec2),
+ vec![Type::new(IVec3)],
+ );
+ declare_function(
+ state,
+ "vec3",
+ Some("make_vec3"),
+ Type::new(Vec3),
+ vec![Type::new(Float), Type::new(Float), Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "vec3",
+ Some("make_vec3"),
+ Type::new(Vec3),
+ vec![Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "vec3",
+ Some("make_vec3"),
+ Type::new(Vec3),
+ vec![Type::new(Vec2), Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "vec4",
+ Some("make_vec4"),
+ Type::new(Vec4),
+ vec![Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "vec4",
+ Some("make_vec4"),
+ Type::new(Vec4),
+ vec![Type::new(Vec3), Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "vec4",
+ Some("make_vec4"),
+ Type::new(Vec4),
+ vec![
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ ],
+ );
+ declare_function(
+ state,
+ "vec4",
+ Some("make_vec4"),
+ Type::new(Vec4),
+ vec![Type::new(Vec2), Type::new(Float), Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "vec4",
+ Some("make_vec4"),
+ Type::new(Vec4),
+ vec![Type::new(Vec2), Type::new(Vec2)],
+ );
+ declare_function(
+ state,
+ "vec4",
+ Some("make_vec4"),
+ Type::new(Vec4),
+ vec![Type::new(Float), Type::new(Float), Type::new(Vec2)],
+ );
+ declare_function(
+ state,
+ "vec4",
+ Some("make_vec4"),
+ Type::new(Vec4),
+ vec![Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "vec4",
+ Some("make_vec4"),
+ Type::new(Vec4),
+ vec![Type::new(IVec4)],
+ );
+
+ declare_function(
+ state,
+ "bvec2",
+ Some("make_bvec2"),
+ Type::new(BVec2),
+ vec![Type::new(Bool)],
+ );
+ declare_function(
+ state,
+ "bvec3",
+ Some("make_bvec3"),
+ Type::new(BVec3),
+ vec![Type::new(Bool)],
+ );
+ declare_function(
+ state,
+ "bvec4",
+ Some("make_bvec4"),
+ Type::new(BVec4),
+ vec![Type::new(Bool)],
+ );
+ declare_function(
+ state,
+ "bvec4",
+ Some("make_bvec4"),
+ Type::new(BVec4),
+ vec![Type::new(BVec2), Type::new(BVec2)],
+ );
+ declare_function(
+ state,
+ "bvec4",
+ Some("make_bvec4"),
+ Type::new(BVec4),
+ vec![Type::new(Bool), Type::new(Bool), Type::new(Bool), Type::new(Bool)],
+ );
+ declare_function(
+ state,
+ "int",
+ Some("make_int"),
+ Type::new(Int),
+ vec![Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "float",
+ Some("make_float"),
+ Type::new(Float),
+ vec![Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "float",
+ Some("make_float"),
+ Type::new(Float),
+ vec![Type::new(Int)],
+ );
+ declare_function(
+ state,
+ "int",
+ Some("make_int"),
+ Type::new(Int),
+ vec![Type::new(UInt)],
+ );
+ declare_function(
+ state,
+ "uint",
+ Some("make_uint"),
+ Type::new(UInt),
+ vec![Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "uint",
+ Some("make_uint"),
+ Type::new(UInt),
+ vec![Type::new(Int)],
+ );
+ declare_function(
+ state,
+ "ivec2",
+ Some("make_ivec2"),
+ Type::new(IVec2),
+ vec![Type::new(UInt), Type::new(UInt)],
+ );
+ declare_function(
+ state,
+ "ivec2",
+ Some("make_ivec2"),
+ Type::new(IVec2),
+ vec![Type::new(Int), Type::new(Int)],
+ );
+ declare_function(
+ state,
+ "ivec2",
+ Some("make_ivec2"),
+ Type::new(IVec2),
+ vec![Type::new(Vec2)],
+ );
+ declare_function(
+ state,
+ "ivec3",
+ Some("make_ivec3"),
+ Type::new(IVec3),
+ vec![Type::new(IVec2), Type::new(Int)],
+ );
+ declare_function(
+ state,
+ "ivec4",
+ Some("make_ivec4"),
+ Type::new(IVec4),
+ vec![
+ Type::new(Int),
+ Type::new(Int),
+ Type::new(Int),
+ Type::new(Int),
+ ],
+ );
+ declare_function(
+ state,
+ "ivec4",
+ Some("make_ivec4"),
+ Type::new(IVec4),
+ vec![Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "ivec4",
+ Some("make_ivec4"),
+ Type::new(IVec4),
+ vec![Type::new(IVec2), Type::new(Int), Type::new(Int)],
+ );
+
+ declare_function(
+ state,
+ "mat2",
+ Some("make_mat2"),
+ Type::new(Mat2),
+ vec![Type::new(Vec2), Type::new(Vec2)],
+ );
+ declare_function(
+ state,
+ "mat2",
+ Some("make_mat2"),
+ Type::new(Mat2),
+ vec![Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "mat2",
+ Some("make_mat2"),
+ Type::new(Mat2),
+ vec![Type::new(Mat4)],
+ );
+ declare_function(
+ state,
+ "mat3",
+ Some("make_mat3"),
+ Type::new(Mat3),
+ vec![Type::new(Vec3), Type::new(Vec3), Type::new(Vec3)],
+ );
+ declare_function(
+ state,
+ "mat3",
+ Some("make_mat3"),
+ Type::new(Mat3),
+ vec![Type::new(Mat4)],
+ );
+ declare_function(
+ state,
+ "mat3",
+ Some("make_mat3"),
+ Type::new(Mat3),
+ vec![
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ ],
+ );
+ declare_function(
+ state,
+ "mat3x4",
+ Some("make_mat3x4"),
+ Type::new(Mat34),
+ vec![
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ ],
+ );
+ declare_function(
+ state,
+ "transpose",
+ None,
+ Type::new(Mat43),
+ vec![Type::new(Mat34)],
+ );
+ declare_function(
+ state,
+ "mat4",
+ Some("make_mat4"),
+ Type::new(Mat4),
+ vec![
+ Type::new(Vec4),
+ Type::new(Vec4),
+ Type::new(Vec4),
+ Type::new(Vec4),
+ ],
+ );
+ declare_function(
+ state,
+ "mat4",
+ Some("make_mat4"),
+ Type::new(Mat4),
+ vec![
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ Type::new(Float),
+ ],
+ );
+ declare_function(state, "abs", None, Type::new(Vec2), vec![Type::new(Vec2)]);
+ declare_function(state, "abs", None, Type::new(Vec3), vec![Type::new(Vec3)]);
+ declare_function(state, "abs", None, Type::new(Float), vec![Type::new(Float)]);
+ declare_function(state, "sign", None, Type::new(Vec2), vec![Type::new(Vec2)]);
+ declare_function(state, "sign", None, Type::new(Vec3), vec![Type::new(Vec3)]);
+ declare_function(state, "sign", None, Type::new(Float), vec![Type::new(Float)]);
+ declare_function(
+ state,
+ "dot",
+ None,
+ Type::new(Float),
+ vec![Type::new(Vec3), Type::new(Vec3)],
+ );
+ declare_function(
+ state,
+ "dot",
+ None,
+ Type::new(Float),
+ vec![Type::new(Vec2), Type::new(Vec2)],
+ );
+ for t in &[Vec2, Vec3, Vec4] {
+ declare_function(
+ state,
+ "min",
+ None,
+ Type::new(*t),
+ vec![Type::new(*t), Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "max",
+ None,
+ Type::new(*t),
+ vec![Type::new(*t), Type::new(Float)],
+ );
+ }
+ for t in &[Int, Float, Vec2, Vec3, Vec4] {
+ declare_function(
+ state,
+ "min",
+ None,
+ Type::new(*t),
+ vec![Type::new(*t), Type::new(*t)],
+ );
+ declare_function(
+ state,
+ "max",
+ None,
+ Type::new(*t),
+ vec![Type::new(*t), Type::new(*t)],
+ );
+ }
+
+ declare_function(
+ state,
+ "mix",
+ None,
+ Type::new(Vec2),
+ vec![Type::new(Vec2), Type::new(Vec2), Type::new(Vec2)],
+ );
+ declare_function(
+ state,
+ "mix",
+ None,
+ Type::new(Vec2),
+ vec![Type::new(Vec2), Type::new(Vec2), Type::new(BVec2)],
+ );
+ declare_function(
+ state,
+ "mix",
+ None,
+ Type::new(Vec2),
+ vec![Type::new(Vec2), Type::new(Vec2), Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "mix",
+ None,
+ Type::new(Vec3),
+ vec![Type::new(Vec3), Type::new(Vec3), Type::new(Vec3)],
+ );
+ declare_function(
+ state,
+ "mix",
+ None,
+ Type::new(Vec4),
+ vec![Type::new(Vec4), Type::new(Vec4), Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "mix",
+ None,
+ Type::new(Vec4),
+ vec![Type::new(Vec4), Type::new(Vec4), Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "mix",
+ None,
+ Type::new(Vec3),
+ vec![Type::new(Vec3), Type::new(Vec3), Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "mix",
+ None,
+ Type::new(Vec3),
+ vec![Type::new(Vec3), Type::new(Vec3), Type::new(BVec3)],
+ );
+ declare_function(
+ state,
+ "mix",
+ None,
+ Type::new(Float),
+ vec![Type::new(Float), Type::new(Float), Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "mix",
+ None,
+ Type::new(Vec4),
+ vec![Type::new(Vec4), Type::new(Vec4), Type::new(BVec4)],
+ );
+ declare_function(
+ state,
+ "step",
+ None,
+ Type::new(Float),
+ vec![Type::new(Float), Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "step",
+ None,
+ Type::new(Vec2),
+ vec![Type::new(Vec2), Type::new(Vec2)],
+ );
+ declare_function(
+ state,
+ "step",
+ None,
+ Type::new(Vec2),
+ vec![Type::new(Float), Type::new(Vec2)],
+ );
+ declare_function(
+ state,
+ "step",
+ None,
+ Type::new(Vec3),
+ vec![Type::new(Vec3), Type::new(Vec3)],
+ );
+ declare_function(
+ state,
+ "step",
+ None,
+ Type::new(Vec4),
+ vec![Type::new(Float), Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "notEqual",
+ None,
+ Type::new(BVec4),
+ vec![Type::new(IVec4), Type::new(IVec4)],
+ );
+
+ declare_function_ext(
+ state,
+ "fwidth",
+ None,
+ Type::new(Vec2),
+ vec![Type::new(Vec2)],
+ RunClass::Scalar,
+ );
+ declare_function_ext(
+ state,
+ "dFdx",
+ None,
+ Type::new(Float),
+ vec![Type::new(Float)],
+ RunClass::Scalar,
+ );
+ declare_function_ext(
+ state,
+ "dFdx",
+ None,
+ Type::new(Vec2),
+ vec![Type::new(Vec2)],
+ RunClass::Scalar,
+ );
+
+ declare_function(state, "cos", None, Type::new(Float), vec![Type::new(Float)]);
+ declare_function(state, "sin", None, Type::new(Float), vec![Type::new(Float)]);
+ declare_function(state, "tan", None, Type::new(Float), vec![Type::new(Float)]);
+ declare_function(state, "atan", None, Type::new(Float), vec![Type::new(Float)]);
+ declare_function(state, "atan", None, Type::new(Float), vec![Type::new(Float), Type::new(Float)]);
+ for t in &[Vec2, Vec3, Vec4] {
+ declare_function(
+ state,
+ "clamp",
+ None,
+ Type::new(*t),
+ vec![Type::new(*t), Type::new(Float), Type::new(Float)],
+ );
+ }
+ for t in &[Float, Vec2, Vec3, Vec4] {
+ declare_function(
+ state,
+ "clamp",
+ None,
+ Type::new(*t),
+ vec![Type::new(*t), Type::new(*t), Type::new(*t)],
+ );
+ }
+ declare_function(
+ state,
+ "length",
+ None,
+ Type::new(Float),
+ vec![Type::new(Vec2)],
+ );
+ declare_function(state, "pow", None, Type::new(Vec3), vec![Type::new(Vec3)]);
+ declare_function(state, "pow", None, Type::new(Float), vec![Type::new(Float)]);
+ declare_function(state, "exp", None, Type::new(Float), vec![Type::new(Float)]);
+ declare_function(state, "exp2", None, Type::new(Float), vec![Type::new(Float)]);
+ declare_function(state, "log", None, Type::new(Float), vec![Type::new(Float)]);
+ declare_function(state, "log2", None, Type::new(Float), vec![Type::new(Float)]);
+ for t in &[Float, Vec2] {
+ // recip is non-standard
+ declare_function(
+ state,
+ "recip",
+ None,
+ Type::new(*t),
+ vec![Type::new(*t)],
+ );
+ declare_function(
+ state,
+ "inversesqrt",
+ None,
+ Type::new(*t),
+ vec![Type::new(*t)],
+ );
+ declare_function(
+ state,
+ "sqrt",
+ None,
+ Type::new(*t),
+ vec![Type::new(*t)],
+ );
+ }
+ declare_function(
+ state,
+ "distance",
+ None,
+ Type::new(Float),
+ vec![Type::new(Vec2), Type::new(Vec2)],
+ );
+
+ declare_function(
+ state,
+ "equal",
+ None,
+ Type::new(BVec2),
+ vec![Type::new(Vec2), Type::new(Vec2)],
+ );
+ declare_function(
+ state,
+ "equal",
+ None,
+ Type::new(BVec4),
+ vec![Type::new(Vec4), Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "notEqual",
+ None,
+ Type::new(BVec2),
+ vec![Type::new(Vec2), Type::new(Vec2)],
+ );
+ declare_function(
+ state,
+ "notEqual",
+ None,
+ Type::new(BVec4),
+ vec![Type::new(Vec4), Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "lessThanEqual",
+ None,
+ Type::new(BVec2),
+ vec![Type::new(Vec2), Type::new(Vec2)],
+ );
+ declare_function(
+ state,
+ "lessThanEqual",
+ None,
+ Type::new(BVec3),
+ vec![Type::new(Vec3), Type::new(Vec3)],
+ );
+ declare_function(
+ state,
+ "lessThanEqual",
+ None,
+ Type::new(BVec4),
+ vec![Type::new(Vec4), Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "lessThan",
+ None,
+ Type::new(BVec2),
+ vec![Type::new(Vec2), Type::new(Vec2)],
+ );
+ declare_function(
+ state,
+ "lessThan",
+ None,
+ Type::new(BVec4),
+ vec![Type::new(Vec4), Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "greaterThan",
+ None,
+ Type::new(BVec2),
+ vec![Type::new(Vec2), Type::new(Vec2)],
+ );
+ declare_function(
+ state,
+ "greaterThan",
+ None,
+ Type::new(BVec4),
+ vec![Type::new(Vec4), Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "greaterThanEqual",
+ None,
+ Type::new(BVec2),
+ vec![Type::new(Vec2), Type::new(Vec2)],
+ );
+ declare_function(
+ state,
+ "greaterThanEqual",
+ None,
+ Type::new(BVec4),
+ vec![Type::new(Vec4), Type::new(Vec4)],
+ );
+ declare_function(state, "any", None, Type::new(Bool), vec![Type::new(BVec2)]);
+ declare_function(state, "all", None, Type::new(Bool), vec![Type::new(BVec2)]);
+ declare_function(state, "all", None, Type::new(Bool), vec![Type::new(BVec4)]);
+
+ declare_function(
+ state,
+ "if_then_else",
+ None,
+ Type::new(Vec3),
+ vec![Type::new(BVec3), Type::new(Vec3), Type::new(Vec3)],
+ );
+ declare_function(state, "floor", None, Type::new(Vec4), vec![Type::new(Vec4)]);
+ declare_function(state, "floor", None, Type::new(Vec2), vec![Type::new(Vec2)]);
+ declare_function(
+ state,
+ "floor",
+ None,
+ Type::new(Float),
+ vec![Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "ceil",
+ None,
+ Type::new(Float),
+ vec![Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "round",
+ None,
+ Type::new(Float),
+ vec![Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "fract",
+ None,
+ Type::new(Float),
+ vec![Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "fract",
+ None,
+ Type::new(Vec2),
+ vec![Type::new(Vec2)],
+ );
+ declare_function(state, "mod", None, Type::new(Vec2), vec![Type::new(Vec2)]);
+ declare_function(state, "mod", None, Type::new(Float), vec![Type::new(Float)]);
+
+ declare_function(
+ state,
+ "texelFetch",
+ None,
+ Type::new(Vec4),
+ vec![Type::new(Sampler2D), Type::new(IVec2), Type::new(Int)],
+ );
+ declare_function(
+ state,
+ "texelFetch",
+ None,
+ Type::new(IVec4),
+ vec![Type::new(ISampler2D), Type::new(IVec2), Type::new(Int)],
+ );
+ declare_function(
+ state,
+ "texelFetchOffset",
+ None,
+ Type::new(Vec4),
+ vec![
+ Type::new(Sampler2D),
+ Type::new(IVec2),
+ Type::new(Int),
+ Type::new(IVec2),
+ ],
+ );
+ declare_function(
+ state,
+ "texelFetchOffset",
+ None,
+ Type::new(IVec4),
+ vec![
+ Type::new(ISampler2D),
+ Type::new(IVec2),
+ Type::new(Int),
+ Type::new(IVec2),
+ ],
+ );
+ declare_function(
+ state,
+ "texture",
+ None,
+ Type::new(Vec4),
+ vec![Type::new(Sampler2D), Type::new(Vec2)],
+ );
+ declare_function(
+ state,
+ "texture",
+ None,
+ Type::new(Vec4),
+ vec![Type::new(Sampler2DRect), Type::new(Vec2)],
+ );
+ declare_function(
+ state,
+ "textureSize",
+ None,
+ Type::new(IVec2),
+ vec![Type::new(Sampler2D), Type::new(Int)],
+ );
+ declare_function(
+ state,
+ "textureSize",
+ None,
+ Type::new(IVec2),
+ vec![Type::new(Sampler2DRect), Type::new(Int)],
+ );
+
+ declare_function(
+ state,
+ "inverse",
+ None,
+ Type::new(Mat2),
+ vec![Type::new(Mat2)],
+ );
+ declare_function(
+ state,
+ "transpose",
+ None,
+ Type::new(Mat3),
+ vec![Type::new(Mat3)],
+ );
+ declare_function(
+ state,
+ "normalize",
+ None,
+ Type::new(Vec2),
+ vec![Type::new(Vec2)],
+ );
+ state.declare(
+ "gl_FragCoord",
+ SymDecl::Global(StorageClass::In, None, Type::new(Vec4), RunClass::Vector),
+ );
+ state.declare(
+ "gl_FragColor",
+ SymDecl::Global(StorageClass::Out, None, Type::new(Vec4), RunClass::Vector),
+ );
+ state.declare(
+ "gl_Position",
+ SymDecl::Global(StorageClass::Out, None, Type::new(Vec4), RunClass::Vector),
+ );
+ state.clip_dist_sym = state.declare(
+ "gl_ClipDistance",
+ SymDecl::Global(StorageClass::Out, None, Type::new_array(Float, 4), RunClass::Vector),
+ );
+
+ state.declare(
+ "swgl_SpanLength",
+ SymDecl::Global(StorageClass::In, None, Type::new(Int), RunClass::Scalar),
+ );
+ state.declare(
+ "swgl_StepSize",
+ SymDecl::Global(StorageClass::Const, None, Type::new(Int), RunClass::Scalar),
+ );
+
+ for t in &[Float, Vec2, Vec3, Vec4, Int, IVec2, IVec3, IVec4, Mat3, Mat4] {
+ declare_function_ext(
+ state,
+ "swgl_forceScalar",
+ None,
+ Type::new(*t),
+ vec![Type::new(*t)],
+ RunClass::Scalar,
+ );
+ }
+
+ // GL_ARB_shader_group_vote
+ for (name, cxx_name) in &[("anyInvocations", "test_any"),
+ ("allInvocations", "test_all"),
+ ("allInvocationsEqual", "test_equal")] {
+ declare_function_ext(
+ state,
+ name,
+ Some(cxx_name),
+ Type::new(Bool),
+ vec![Type::new(Bool)],
+ RunClass::Scalar,
+ );
+ }
+
+ declare_function(
+ state,
+ "swgl_stepInterp",
+ None,
+ Type::new(Void),
+ vec![],
+ );
+
+ for t in &[Float, Vec2, Vec3, Vec4] {
+ declare_function_ext(
+ state,
+ "swgl_interpStep",
+ None,
+ Type::new(*t),
+ vec![Type::new(*t)],
+ RunClass::Scalar,
+ );
+ }
+
+ declare_function(
+ state,
+ "swgl_commitPartialSolidRGBA8",
+ None,
+ Type::new(Void),
+ vec![Type::new(Int), Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "swgl_commitPartialSolidR8",
+ None,
+ Type::new(Void),
+ vec![Type::new(Int), Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "swgl_commitSolidRGBA8",
+ None,
+ Type::new(Void),
+ vec![Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "swgl_commitSolidR8",
+ None,
+ Type::new(Void),
+ vec![Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "swgl_commitColorRGBA8",
+ None,
+ Type::new(Void),
+ vec![Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "swgl_commitColorR8",
+ None,
+ Type::new(Void),
+ vec![Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "swgl_blendDropShadow",
+ None,
+ Type::new(Void),
+ vec![Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "swgl_blendSubpixelText",
+ None,
+ Type::new(Void),
+ vec![Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "swgl_clipMask",
+ None,
+ Type::new(Void),
+ vec![Type::new(Sampler2D), Type::new(Vec2), Type::new(Vec2), Type::new(Vec2)],
+ );
+ declare_function(
+ state,
+ "swgl_antiAlias",
+ None,
+ Type::new(Void),
+ vec![Type::new(Int)],
+ );
+ declare_function(
+ state,
+ "swgl_antiAlias",
+ None,
+ Type::new(Void),
+ vec![Type::new(BVec4)],
+ );
+ declare_function_ext(
+ state,
+ "swgl_validateGradient",
+ None,
+ Type::new(Int),
+ vec![Type::new(Sampler2D), Type::new(IVec2), Type::new(Int)],
+ RunClass::Scalar,
+ );
+ declare_function(
+ state,
+ "swgl_commitLinearGradientRGBA8",
+ None,
+ Type::new(Void),
+ vec![Type::new(Sampler2D), Type::new(Int), Type::new(Float), Type::new(Bool), Type::new(Bool),
+ Type::new(Vec2), Type::new(Vec2), Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "swgl_commitRadialGradientRGBA8",
+ None,
+ Type::new(Void),
+ vec![Type::new(Sampler2D), Type::new(Int), Type::new(Float), Type::new(Bool), Type::new(Vec2),
+ Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "swgl_commitGradientRGBA8",
+ None,
+ Type::new(Void),
+ vec![Type::new(Sampler2D), Type::new(Int), Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "swgl_commitGradientColorRGBA8",
+ None,
+ Type::new(Void),
+ vec![Type::new(Sampler2D), Type::new(Int), Type::new(Float), Type::new(Float)],
+ );
+ for s in &[Sampler2D, Sampler2DRect] {
+ declare_function_ext(
+ state,
+ "swgl_isTextureLinear",
+ None,
+ Type::new(Bool),
+ vec![Type::new(*s)],
+ RunClass::Scalar,
+ );
+ declare_function_ext(
+ state,
+ "swgl_isTextureRGBA8",
+ None,
+ Type::new(Bool),
+ vec![Type::new(*s)],
+ RunClass::Scalar,
+ );
+ declare_function_ext(
+ state,
+ "swgl_isTextureR8",
+ None,
+ Type::new(Bool),
+ vec![Type::new(*s)],
+ RunClass::Scalar,
+ );
+ declare_function(
+ state,
+ "swgl_commitTextureLinearRGBA8",
+ None,
+ Type::new(Void),
+ vec![Type::new(*s), Type::new(Vec2), Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "swgl_commitTextureLinearR8",
+ None,
+ Type::new(Void),
+ vec![Type::new(*s), Type::new(Vec2), Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "swgl_commitTextureLinearR8ToRGBA8",
+ None,
+ Type::new(Void),
+ vec![Type::new(*s), Type::new(Vec2), Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "swgl_commitPartialTextureLinearR8",
+ None,
+ Type::new(Void),
+ vec![Type::new(Int), Type::new(*s), Type::new(Vec2), Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "swgl_commitPartialTextureLinearInvertR8",
+ None,
+ Type::new(Void),
+ vec![Type::new(Int), Type::new(*s), Type::new(Vec2), Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "swgl_commitTextureLinearColorRGBA8",
+ None,
+ Type::new(Void),
+ vec![Type::new(*s), Type::new(Vec2), Type::new(Vec4), Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "swgl_commitTextureLinearColorRGBA8",
+ None,
+ Type::new(Void),
+ vec![Type::new(*s), Type::new(Vec2), Type::new(Vec4), Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "swgl_commitTextureLinearColorR8",
+ None,
+ Type::new(Void),
+ vec![Type::new(*s), Type::new(Vec2), Type::new(Vec4), Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "swgl_commitTextureLinearColorR8ToRGBA8",
+ None,
+ Type::new(Void),
+ vec![Type::new(*s), Type::new(Vec2), Type::new(Vec4), Type::new(Vec4)],
+ );
+
+ declare_function(
+ state,
+ "swgl_commitTextureLinearRepeatRGBA8",
+ None,
+ Type::new(Void),
+ vec![Type::new(*s), Type::new(Vec2), Type::new(Vec2),
+ Type::new(Vec4), Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "swgl_commitTextureLinearRepeatColorRGBA8",
+ None,
+ Type::new(Void),
+ vec![Type::new(*s), Type::new(Vec2), Type::new(Vec2),
+ Type::new(Vec4), Type::new(Vec4), Type::new(Vec4)],
+ );
+
+ declare_function(
+ state,
+ "swgl_commitTextureNearestRGBA8",
+ None,
+ Type::new(Void),
+ vec![Type::new(*s), Type::new(Vec2), Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "swgl_commitTextureNearestColorRGBA8",
+ None,
+ Type::new(Void),
+ vec![Type::new(*s), Type::new(Vec2), Type::new(Vec4), Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "swgl_commitTextureNearestRepeatRGBA8",
+ None,
+ Type::new(Void),
+ vec![Type::new(*s), Type::new(Vec2), Type::new(Vec2),
+ Type::new(Vec4), Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "swgl_commitTextureNearestRepeatColorRGBA8",
+ None,
+ Type::new(Void),
+ vec![Type::new(*s), Type::new(Vec2), Type::new(Vec2),
+ Type::new(Vec4), Type::new(Vec4), Type::new(Vec4)],
+ );
+
+ declare_function(
+ state,
+ "swgl_commitTextureRGBA8",
+ None,
+ Type::new(Void),
+ vec![Type::new(*s), Type::new(Vec2), Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "swgl_commitTextureColorRGBA8",
+ None,
+ Type::new(Void),
+ vec![Type::new(*s), Type::new(Vec2), Type::new(Vec4), Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "swgl_commitTextureRepeatRGBA8",
+ None,
+ Type::new(Void),
+ vec![Type::new(*s), Type::new(Vec2), Type::new(Vec2),
+ Type::new(Vec4), Type::new(Vec4)],
+ );
+ declare_function(
+ state,
+ "swgl_commitTextureRepeatColorRGBA8",
+ None,
+ Type::new(Void),
+ vec![Type::new(*s), Type::new(Vec2), Type::new(Vec2),
+ Type::new(Vec4), Type::new(Vec4), Type::new(Vec4)],
+ );
+
+ declare_function(
+ state,
+ "swgl_commitGaussianBlurRGBA8",
+ None,
+ Type::new(Void),
+ vec![Type::new(*s), Type::new(Vec2), Type::new(Vec4), Type::new(Bool),
+ Type::new(Int), Type::new(Vec2)],
+ );
+ declare_function(
+ state,
+ "swgl_commitGaussianBlurR8",
+ None,
+ Type::new(Void),
+ vec![Type::new(*s), Type::new(Vec2), Type::new(Vec4), Type::new(Bool),
+ Type::new(Int), Type::new(Vec2)],
+ );
+ declare_function(
+ state,
+ "swgl_commitTextureLinearYUV",
+ None,
+ Type::new(Void),
+ vec![Type::new(*s), Type::new(Vec2), Type::new(Vec4),
+ Type::new(Vec3), Type::new(Mat3), Type::new(Int)],
+ );
+ declare_function(
+ state,
+ "swgl_commitTextureLinearYUV",
+ None,
+ Type::new(Void),
+ vec![Type::new(*s), Type::new(Vec2), Type::new(Vec4),
+ Type::new(*s), Type::new(Vec2), Type::new(Vec4),
+ Type::new(Vec3), Type::new(Mat3), Type::new(Int)],
+ );
+ declare_function(
+ state,
+ "swgl_commitTextureLinearYUV",
+ None,
+ Type::new(Void),
+ vec![Type::new(*s), Type::new(Vec2), Type::new(Vec4),
+ Type::new(*s), Type::new(Vec2), Type::new(Vec4),
+ Type::new(*s), Type::new(Vec2), Type::new(Vec4),
+ Type::new(Vec3), Type::new(Mat3), Type::new(Int)],
+ );
+ declare_function(
+ state,
+ "swgl_commitTextureLinearColorYUV",
+ None,
+ Type::new(Void),
+ vec![Type::new(*s), Type::new(Vec2), Type::new(Vec4),
+ Type::new(Vec3), Type::new(Mat3), Type::new(Int),
+ Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "swgl_commitTextureLinearColorYUV",
+ None,
+ Type::new(Void),
+ vec![Type::new(*s), Type::new(Vec2), Type::new(Vec4),
+ Type::new(*s), Type::new(Vec2), Type::new(Vec4),
+ Type::new(Vec3), Type::new(Mat3), Type::new(Int),
+ Type::new(Float)],
+ );
+ declare_function(
+ state,
+ "swgl_commitTextureLinearColorYUV",
+ None,
+ Type::new(Void),
+ vec![Type::new(*s), Type::new(Vec2), Type::new(Vec4),
+ Type::new(*s), Type::new(Vec2), Type::new(Vec4),
+ Type::new(*s), Type::new(Vec2), Type::new(Vec4),
+ Type::new(Vec3), Type::new(Mat3), Type::new(Int),
+ Type::new(Float)],
+ );
+ }
+
+ TranslationUnit(tu.0.map(state, translate_external_declaration))
+}
+
+fn infer_expr_inner(state: &mut State, expr: &Expr, assign: &mut SymRef) -> RunClass {
+ match expr.kind {
+ ExprKind::Variable(ref i) => {
+ *assign = *i;
+ match &state.sym(*i).decl {
+ SymDecl::Local(_, _, ref run_class) => *run_class,
+ SymDecl::Global(_, _, _, ref run_class) => *run_class,
+ _ => panic!(),
+ }
+ }
+ ExprKind::IntConst(_)
+ | ExprKind::UIntConst(_)
+ | ExprKind::BoolConst(_)
+ | ExprKind::FloatConst(_)
+ | ExprKind::DoubleConst(_) => RunClass::Scalar,
+ ExprKind::Unary(_, ref e) => infer_expr(state, e),
+ ExprKind::Binary(_, ref l, ref r) => infer_expr(state, l).merge(infer_expr(state, r)),
+ ExprKind::Ternary(ref c, ref s, ref e) => infer_expr(state, c)
+ .merge(infer_expr(state, s))
+ .merge(infer_expr(state, e)),
+ ExprKind::Assignment(ref v, _, ref e) => {
+ let mut sym = SymRef(!0);
+ let run_class = infer_expr_inner(state, v, &mut sym).merge(infer_expr(state, e));
+ assert!(sym != SymRef(!0));
+ state.merge_run_class(sym, run_class)
+ }
+ ExprKind::Bracket(ref e, ref indx) => {
+ indx.iter().fold(
+ infer_expr_inner(state, e, assign),
+ |run_class, indx| run_class.merge(infer_expr(state, indx)),
+ )
+ }
+ ExprKind::FunCall(ref fun, ref args) => {
+ let arg_classes: Vec<(RunClass, SymRef)> = args
+ .iter()
+ .map(|e| {
+ let mut assign = SymRef(!0);
+ let run_class = infer_expr_inner(state, e, &mut assign);
+ (run_class, assign)
+ })
+ .collect();
+ let run_class = if args.is_empty() {
+ RunClass::Scalar
+ } else {
+ arg_classes
+ .iter()
+ .fold(RunClass::Unknown, |x, &(y, _)| x.merge(y))
+ };
+ match fun {
+ FunIdentifier::Identifier(ref sym) => match &state.sym(*sym).decl {
+ SymDecl::NativeFunction(_, _, ref ret_class) => {
+ if *ret_class != RunClass::Unknown {
+ *ret_class
+ } else {
+ run_class
+ }
+ }
+ SymDecl::UserFunction(ref fd, ref run_class) => {
+ for (&(mut arg_class, assign), param) in
+ arg_classes.iter().zip(fd.prototype.parameters.iter())
+ {
+ if let FunctionParameterDeclaration::Named(Some(qual), p) = param {
+ match qual {
+ ParameterQualifier::InOut | ParameterQualifier::Out => {
+ if let SymDecl::Local(_, _, param_class) =
+ &state.sym(p.sym).decl
+ {
+ match param_class {
+ RunClass::Unknown | RunClass::Vector => {
+ arg_class = RunClass::Vector;
+ }
+ RunClass::Dependent(mask) => {
+ for i in 0 .. 31 {
+ if (mask & (1 << i)) != 0 {
+ arg_class =
+ arg_class.merge(arg_classes[i].0);
+ }
+ }
+ }
+ RunClass::Scalar => {}
+ }
+ }
+ assert!(assign != SymRef(!0));
+ state.merge_run_class(assign, arg_class);
+ }
+ _ => {}
+ }
+ }
+ }
+ if fd.prototype.ty.kind == TypeKind::Void {
+ RunClass::Scalar
+ } else {
+ match *run_class {
+ RunClass::Unknown | RunClass::Vector => RunClass::Vector,
+ RunClass::Dependent(mask) => {
+ let mut ret_class = RunClass::Unknown;
+ for i in 0 .. 31 {
+ if (mask & (1 << i)) != 0 {
+ ret_class = ret_class.merge(arg_classes[i].0);
+ }
+ }
+ ret_class
+ }
+ RunClass::Scalar => RunClass::Scalar,
+ }
+ }
+ }
+ SymDecl::Struct(..) => run_class,
+ _ => panic!(),
+ },
+ FunIdentifier::Constructor(..) => run_class,
+ }
+ }
+ ExprKind::Dot(ref e, _) => infer_expr_inner(state, e, assign),
+ ExprKind::SwizzleSelector(ref e, _) => infer_expr_inner(state, e, assign),
+ ExprKind::PostInc(ref e) => infer_expr_inner(state, e, assign),
+ ExprKind::PostDec(ref e) => infer_expr_inner(state, e, assign),
+ ExprKind::Comma(ref a, ref b) => {
+ infer_expr(state, a);
+ infer_expr(state, b)
+ }
+ ExprKind::Cond(_, ref e) => infer_expr(state, e),
+ ExprKind::CondMask => RunClass::Vector,
+ }
+}
+
+fn infer_expr(state: &mut State, expr: &Expr) -> RunClass {
+ infer_expr_inner(state, expr, &mut SymRef(!0))
+}
+
+fn infer_condition(state: &mut State, c: &Condition) {
+ match *c {
+ Condition::Expr(ref e) => {
+ infer_expr(state, e);
+ }
+ }
+}
+
+fn infer_iteration_statement(state: &mut State, ist: &IterationStatement) {
+ let changed = state.run_class_changed.replace(true);
+ match *ist {
+ IterationStatement::While(ref cond, ref body) => {
+ while state.run_class_changed.replace(false) {
+ infer_condition(state, cond);
+ infer_statement(state, body);
+ }
+ }
+ IterationStatement::DoWhile(ref body, ref cond) => {
+ while state.run_class_changed.replace(false) {
+ infer_statement(state, body);
+ infer_expr(state, cond);
+ }
+ }
+ IterationStatement::For(ref init, ref rest, ref body) => {
+ match *init {
+ ForInitStatement::Expression(ref expr) => {
+ if let Some(ref e) = *expr {
+ infer_expr(state, e);
+ }
+ }
+ ForInitStatement::Declaration(ref d) => {
+ infer_declaration(state, d);
+ }
+ }
+ while state.run_class_changed.replace(false) {
+ if let Some(ref cond) = rest.condition {
+ infer_condition(state, cond);
+ }
+ if let Some(ref e) = rest.post_expr {
+ infer_expr(state, e);
+ }
+ infer_statement(state, body);
+ }
+ }
+ }
+ state.run_class_changed.set(changed);
+}
+
+fn infer_selection_statement(state: &mut State, sst: &SelectionStatement) {
+ let mut branch_run_class = state.branch_run_class.merge(infer_expr(state, &sst.cond));
+ mem::swap(&mut state.branch_run_class, &mut branch_run_class);
+ let branch_declaration = state.branch_declaration;
+ state.branch_declaration = state.last_declaration;
+ infer_statement(state, &sst.body);
+ if let Some(ref else_st) = sst.else_stmt {
+ infer_statement(state, else_st);
+ }
+ state.branch_run_class = branch_run_class;
+ state.branch_declaration = branch_declaration;
+}
+
+fn infer_expression_statement(state: &mut State, est: &ExprStatement) {
+ if let Some(ref e) = *est {
+ infer_expr(state, e);
+ }
+}
+
+fn infer_switch_statement(state: &mut State, sst: &SwitchStatement) {
+ let mut branch_run_class = state.branch_run_class.merge(infer_expr(state, &sst.head));
+ mem::swap(&mut state.branch_run_class, &mut branch_run_class);
+ let branch_declaration = state.branch_declaration;
+ state.branch_declaration = state.last_declaration;
+ for case in &sst.cases {
+ for st in &case.stmts {
+ infer_statement(state, st);
+ }
+ }
+ state.branch_run_class = branch_run_class;
+ state.branch_declaration = branch_declaration;
+}
+
+fn infer_jump_statement(state: &mut State, j: &JumpStatement) {
+ match *j {
+ JumpStatement::Continue => {}
+ JumpStatement::Break => {}
+ JumpStatement::Discard => {}
+ JumpStatement::Return(ref e) => {
+ if let Some(e) = e {
+ let run_class = infer_expr(state, e);
+ state.return_run_class(run_class);
+ }
+ }
+ }
+}
+
+fn infer_initializer(state: &mut State, i: &Initializer) -> RunClass {
+ match *i {
+ Initializer::Simple(ref e) => infer_expr(state, e),
+ Initializer::List(ref list) => {
+ let mut run_class = RunClass::Unknown;
+ for ini in list.0.iter() {
+ run_class = run_class.merge(infer_initializer(state, ini));
+ }
+ run_class
+ }
+ }
+}
+
+fn infer_declaration(state: &mut State, d: &Declaration) {
+ match *d {
+ Declaration::FunctionPrototype(..) => {}
+ Declaration::InitDeclaratorList(ref list) => {
+ state.last_declaration = list.head.name;
+
+ let mut run_class = RunClass::Unknown;
+ for decl in &list.tail {
+ if let Some(ref initializer) = decl.initializer {
+ run_class = run_class.merge(infer_initializer(state, initializer));
+ }
+ }
+ if let Some(ref initializer) = list.head.initializer {
+ run_class = run_class.merge(infer_initializer(state, initializer));
+ state.merge_run_class(list.head.name, run_class);
+ }
+ }
+ Declaration::Precision(..) => {}
+ Declaration::Block(..) => {}
+ Declaration::Global(..) => {}
+ Declaration::StructDefinition(..) => {}
+ }
+}
+
+fn infer_simple_statement(state: &mut State, sst: &SimpleStatement) {
+ match *sst {
+ SimpleStatement::Declaration(ref d) => infer_declaration(state, d),
+ SimpleStatement::Expression(ref e) => infer_expression_statement(state, e),
+ SimpleStatement::Selection(ref s) => infer_selection_statement(state, s),
+ SimpleStatement::Switch(ref s) => infer_switch_statement(state, s),
+ SimpleStatement::Iteration(ref i) => infer_iteration_statement(state, i),
+ SimpleStatement::Jump(ref j) => infer_jump_statement(state, j),
+ }
+}
+
+fn infer_compound_statement(state: &mut State, cst: &CompoundStatement) {
+ for st in &cst.statement_list {
+ infer_statement(state, st);
+ }
+}
+
+fn infer_statement(state: &mut State, st: &Statement) {
+ match *st {
+ Statement::Compound(ref cst) => infer_compound_statement(state, cst),
+ Statement::Simple(ref sst) => infer_simple_statement(state, sst),
+ }
+}
+
+fn infer_function_definition(state: &mut State, fd: &FunctionDefinition) {
+ state.in_function = Some(state.lookup(fd.prototype.name.as_str()).unwrap());
+
+ state.run_class_changed.set(true);
+ while state.run_class_changed.replace(false) {
+ for st in &fd.body.statement_list {
+ infer_statement(state, st);
+ }
+ }
+
+ state.in_function = None;
+}
+
+fn infer_external_declaration(state: &mut State, ed: &ExternalDeclaration) {
+ match *ed {
+ ExternalDeclaration::Preprocessor(_) => {}
+ ExternalDeclaration::FunctionDefinition(ref fd) => infer_function_definition(state, fd),
+ ExternalDeclaration::Declaration(_) => {}
+ }
+}
+
+pub fn infer_run_class(state: &mut State, tu: &TranslationUnit) {
+ for ed in &(tu.0).0 {
+ infer_external_declaration(state, ed);
+ }
+}
diff --git a/gfx/wr/glsl-to-cxx/src/lib.rs b/gfx/wr/glsl-to-cxx/src/lib.rs
new file mode 100644
index 0000000000..0e8273a987
--- /dev/null
+++ b/gfx/wr/glsl-to-cxx/src/lib.rs
@@ -0,0 +1,3681 @@
+/* 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/. */
+
+extern crate glsl;
+
+mod hir;
+
+use glsl::parser::Parse;
+use glsl::syntax;
+use glsl::syntax::{TranslationUnit, UnaryOp};
+use hir::{Statement, Type};
+use std::cell::{Cell, RefCell};
+use std::collections::{BTreeMap, HashMap};
+use std::io::Read;
+use std::mem;
+
+#[derive(PartialEq, Eq)]
+enum ShaderKind {
+ Fragment,
+ Vertex,
+}
+
+type UniformIndices = BTreeMap<String, (i32, hir::TypeKind, hir::StorageClass)>;
+
+fn build_uniform_indices(indices: &mut UniformIndices, state: &hir::State) {
+ for u in state.used_globals.borrow().iter() {
+ let sym = state.sym(*u);
+ match &sym.decl {
+ hir::SymDecl::Global(storage, _, ty, _) => match storage {
+ hir::StorageClass::Uniform | hir::StorageClass::Sampler(..) => {
+ let next_index = indices.len() as i32 + 1;
+ indices.entry(sym.name.clone()).or_insert((
+ next_index,
+ ty.kind.clone(),
+ *storage,
+ ));
+ }
+ _ => {}
+ },
+ _ => {}
+ }
+ }
+}
+
+pub fn translate(args: &mut dyn Iterator<Item = String>) -> String {
+ let _cmd_name = args.next();
+ let vertex_file = args.next().unwrap();
+
+ let vs_name = std::path::Path::new(&vertex_file)
+ .file_stem()
+ .unwrap()
+ .to_string_lossy()
+ .to_string();
+
+ let frag_file = args.next().unwrap();
+
+ let fs_name = std::path::Path::new(&frag_file)
+ .file_stem()
+ .unwrap()
+ .to_string_lossy()
+ .to_string();
+
+ let (vs_state, vs_hir, vs_is_frag) = parse_shader(vertex_file);
+ let (fs_state, fs_hir, fs_is_frag) = parse_shader(frag_file);
+
+ // we use a BTree so that iteration is stable
+ let mut uniform_indices = BTreeMap::new();
+ build_uniform_indices(&mut uniform_indices, &vs_state);
+ build_uniform_indices(&mut uniform_indices, &fs_state);
+
+ assert_eq!(fs_name, vs_name);
+
+ let mut result = translate_shader(
+ vs_name,
+ vs_state,
+ vs_hir,
+ vs_is_frag,
+ &uniform_indices,
+ );
+ result += "\n";
+ result += &translate_shader(
+ fs_name,
+ fs_state,
+ fs_hir,
+ fs_is_frag,
+ &uniform_indices,
+ );
+ result
+}
+
+fn parse_shader(file: String) -> (hir::State, hir::TranslationUnit, bool) {
+ let mut contents = String::new();
+ let is_frag = file.contains("frag");
+ std::fs::File::open(&file)
+ .unwrap()
+ .read_to_string(&mut contents)
+ .unwrap();
+ let r = TranslationUnit::parse(contents);
+
+ //println!("{:#?}", r);
+ let mut ast_glsl = String::new();
+ let r = r.unwrap();
+ glsl::transpiler::glsl::show_translation_unit(&mut ast_glsl, &r);
+ //let mut fast = std::fs::File::create("ast").unwrap();
+ //fast.write(ast_glsl.as_bytes());
+
+ let mut state = hir::State::new();
+ let hir = hir::ast_to_hir(&mut state, &r);
+ (state, hir, is_frag)
+}
+
+fn translate_shader(
+ name: String,
+ mut state: hir::State,
+ hir: hir::TranslationUnit,
+ is_frag: bool,
+ uniform_indices: &UniformIndices,
+) -> String {
+ //println!("{:#?}", state);
+
+ hir::infer_run_class(&mut state, &hir);
+
+ let mut uniforms = Vec::new();
+ let mut inputs = Vec::new();
+ let mut outputs = Vec::new();
+
+ for i in &hir {
+ match i {
+ hir::ExternalDeclaration::Declaration(hir::Declaration::InitDeclaratorList(ref d)) => {
+ match &state.sym(d.head.name).decl {
+ hir::SymDecl::Global(storage, ..)
+ if state.used_globals.borrow().contains(&d.head.name) =>
+ {
+ match storage {
+ hir::StorageClass::Uniform | hir::StorageClass::Sampler(..) => {
+ uniforms.push(d.head.name);
+ }
+ hir::StorageClass::In => {
+ inputs.push(d.head.name);
+ }
+ hir::StorageClass::Out | hir::StorageClass::FragColor(_) => {
+ outputs.push(d.head.name);
+ }
+ _ => {}
+ }
+ }
+ _ => {}
+ }
+ }
+ _ => {}
+ }
+ }
+
+ //println!("{:#?}", hir);
+
+ let mut state = OutputState {
+ hir: state,
+ output: String::new(),
+ buffer: RefCell::new(String::new()),
+ indent: 0,
+ should_indent: false,
+ output_cxx: false,
+ mask: None,
+ cond_index: 0,
+ return_type: None,
+ return_declared: false,
+ return_vector: false,
+ is_scalar: Cell::new(false),
+ is_lval: Cell::new(false),
+ name: name.clone(),
+ kind: if is_frag {
+ ShaderKind::Fragment
+ } else {
+ ShaderKind::Vertex
+ },
+ functions: HashMap::new(),
+ deps: RefCell::new(Vec::new()),
+ vector_mask: 0,
+ uses_discard: false,
+ used_fragcoord: Cell::new(0),
+ use_perspective: false,
+ used_globals: RefCell::new(Vec::new()),
+ texel_fetches: RefCell::new(Vec::new()),
+ };
+
+ show_translation_unit(&mut state, &hir);
+ let _output_glsl = state.finish_output();
+
+ state.should_indent = true;
+ state.output_cxx = true;
+
+ if state.output_cxx {
+ let part_name = name.to_owned()
+ + match state.kind {
+ ShaderKind::Vertex => "_vert",
+ ShaderKind::Fragment => "_frag",
+ };
+
+ if state.kind == ShaderKind::Vertex {
+ write_common_globals(&mut state, &inputs, &outputs, uniform_indices);
+ write!(state, "struct {0}_vert : VertexShaderImpl, {0}_common {{\nprivate:\n", name);
+ } else {
+ write!(state, "struct {0}_frag : FragmentShaderImpl, {0}_vert {{\nprivate:\n", name);
+ }
+
+ write!(state, "typedef {} Self;\n", part_name);
+
+ show_translation_unit(&mut state, &hir);
+
+ let pruned_inputs: Vec<_> = inputs
+ .iter()
+ .filter(|i| state.used_globals.borrow().contains(i))
+ .cloned()
+ .collect();
+
+ if state.kind == ShaderKind::Vertex {
+ write_set_uniform_1i(&mut state, uniform_indices);
+ write_set_uniform_4fv(&mut state, uniform_indices);
+ write_set_uniform_matrix4fv(&mut state, uniform_indices);
+ write_load_attribs(&mut state, &pruned_inputs);
+ write_store_outputs(&mut state, &outputs);
+ } else {
+ write_read_inputs(&mut state, &pruned_inputs);
+ }
+
+ write_abi(&mut state);
+ write!(state, "}};\n\n");
+
+ if state.kind == ShaderKind::Fragment {
+ write!(state, "struct {0}_program : ProgramImpl, {0}_frag {{\n", name);
+ write_get_uniform_index(&mut state, uniform_indices);
+ write!(state, "void bind_attrib(const char* name, int index) override {{\n");
+ write!(state, " attrib_locations.bind_loc(name, index);\n}}\n");
+ write!(state, "int get_attrib(const char* name) const override {{\n");
+ write!(state, " return attrib_locations.get_loc(name);\n}}\n");
+ write!(state, "size_t interpolants_size() const override {{ return sizeof(InterpOutputs); }}\n");
+ write!(state, "VertexShaderImpl* get_vertex_shader() override {{\n");
+ write!(state, " return this;\n}}\n");
+ write!(state, "FragmentShaderImpl* get_fragment_shader() override {{\n");
+ write!(state, " return this;\n}}\n");
+ write!(state, "const char* get_name() const override {{ return \"{}\"; }}\n", name);
+ write!(state, "static ProgramImpl* loader() {{ return new {}_program; }}\n", name);
+ write!(state, "}};\n\n");
+ }
+
+ define_global_consts(&mut state, &hir, &part_name);
+ } else {
+ show_translation_unit(&mut state, &hir);
+ }
+ let output_cxx = state.finish_output();
+
+ //let mut hir = std::fs::File::create("hir").unwrap();
+ //hir.write(output_glsl.as_bytes());
+
+ output_cxx
+}
+
+fn write_get_uniform_index(state: &mut OutputState, uniform_indices: &UniformIndices) {
+ write!(
+ state,
+ "int get_uniform(const char *name) const override {{\n"
+ );
+ for (uniform_name, (index, _, _)) in uniform_indices.iter() {
+ write!(
+ state,
+ " if (strcmp(\"{}\", name) == 0) {{ return {}; }}\n",
+ uniform_name, index
+ );
+ }
+ write!(state, " return -1;\n");
+ write!(state, "}}\n");
+}
+
+fn float4_compatible(ty: hir::TypeKind) -> bool {
+ match ty {
+ hir::TypeKind::Vec4 => true,
+ _ => false,
+ }
+}
+
+fn matrix4_compatible(ty: hir::TypeKind) -> bool {
+ match ty {
+ hir::TypeKind::Mat4 => true,
+ _ => false,
+ }
+}
+
+fn write_program_samplers(state: &mut OutputState, uniform_indices: &UniformIndices) {
+ write!(state, "struct Samplers {{\n");
+ for (name, (_, tk, storage)) in uniform_indices.iter() {
+ match tk {
+ hir::TypeKind::Sampler2D
+ | hir::TypeKind::Sampler2DRect
+ | hir::TypeKind::ISampler2D => {
+ write!(state, " ");
+ show_type_kind(state, &tk);
+ let suffix = if let hir::StorageClass::Sampler(format) = storage {
+ format.type_suffix()
+ } else {
+ None
+ };
+ write!(state, "{}_impl {}_impl;\n", suffix.unwrap_or(""), name);
+ write!(state, " int {}_slot;\n", name);
+ }
+ _ => {}
+ }
+ }
+ write!(
+ state,
+ " bool set_slot(int index, int value) {{\n"
+ );
+ write!(state, " switch (index) {{\n");
+ for (name, (index, tk, _)) in uniform_indices.iter() {
+ match tk {
+ hir::TypeKind::Sampler2D
+ | hir::TypeKind::Sampler2DRect
+ | hir::TypeKind::ISampler2D => {
+ write!(state, " case {}:\n", index);
+ write!(state, " {}_slot = value;\n", name);
+ write!(state, " return true;\n");
+ }
+ _ => {}
+ }
+ }
+ write!(state, " }}\n");
+ write!(state, " return false;\n");
+ write!(state, " }}\n");
+ write!(state, "}} samplers;\n");
+
+}
+
+fn write_bind_textures(state: &mut OutputState, uniforms: &UniformIndices) {
+ write!(state, "void bind_textures() {{\n");
+ for (name, (_, tk, storage)) in uniforms {
+ match storage {
+ hir::StorageClass::Sampler(_format) => {
+ match tk {
+ hir::TypeKind::Sampler2D
+ | hir::TypeKind::Sampler2DRect => write!(state,
+ " {0} = lookup_sampler(&samplers.{0}_impl, samplers.{0}_slot);\n",
+ name),
+ hir::TypeKind::ISampler2D => write!(state,
+ " {0} = lookup_isampler(&samplers.{0}_impl, samplers.{0}_slot);\n",
+ name),
+ _ => {}
+ };
+ }
+ _ => {}
+ }
+ }
+ write!(state, "}}\n");
+}
+
+fn write_set_uniform_1i(
+ state: &mut OutputState,
+ uniforms: &UniformIndices,
+) {
+ write!(
+ state,
+ "static void set_uniform_1i(VertexShaderImpl* impl, int index, int value) {{\n"
+ );
+ write!(state, " Self* self = (Self*)impl;\n");
+ write!(state, " if (self->samplers.set_slot(index, value)) return;\n");
+ write!(state, " switch (index) {{\n");
+ for (name, (index, tk, _)) in uniforms {
+ write!(state, " case {}:\n", index);
+ match tk {
+ hir::TypeKind::Int => write!(
+ state,
+ " self->{} = {}(value);\n",
+ name,
+ tk.cxx_primitive_scalar_type_name().unwrap(),
+ ),
+ _ => write!(state, " assert(0); // {}\n", name),
+ };
+ write!(state, " break;\n");
+ }
+ write!(state, " }}\n");
+ write!(state, "}}\n");
+}
+
+fn write_set_uniform_4fv(
+ state: &mut OutputState,
+ uniforms: &UniformIndices,
+) {
+ write!(
+ state,
+ "static void set_uniform_4fv(VertexShaderImpl* impl, int index, const float *value) {{\n"
+ );
+ write!(state, " Self* self = (Self*)impl;\n");
+ write!(state, " switch (index) {{\n");
+ for (name, (index, tk, _)) in uniforms {
+ write!(state, " case {}:\n", index);
+ if float4_compatible(tk.clone()) {
+ write!(
+ state,
+ " self->{} = vec4_scalar::load_from_ptr(value);\n",
+ name
+ );
+ } else {
+ write!(state, " assert(0); // {}\n", name);
+ }
+ write!(state, " break;\n");
+ }
+ write!(state, " }}\n");
+ write!(state, "}}\n");
+}
+
+fn write_set_uniform_matrix4fv(
+ state: &mut OutputState,
+ uniforms: &UniformIndices,
+) {
+ write!(
+ state,
+ "static void set_uniform_matrix4fv(VertexShaderImpl* impl, int index, const float *value) {{\n"
+ );
+ write!(state, " Self* self = (Self*)impl;\n");
+ write!(state, " switch (index) {{\n");
+ for (name, (index, tk, _)) in uniforms {
+ write!(state, " case {}:\n", index);
+ if matrix4_compatible(tk.clone()) {
+ write!(
+ state,
+ " self->{} = mat4_scalar::load_from_ptr(value);\n",
+ name
+ );
+ } else {
+ write!(state, " assert(0); // {}\n", name);
+ }
+ write!(state, " break;\n");
+ }
+ write!(state, " }}\n");
+ write!(state, "}}\n");
+}
+
+fn write_bind_attrib_location(state: &mut OutputState, attribs: &[hir::SymRef]) {
+ write!(state, "struct AttribLocations {{\n");
+ for i in attribs {
+ let sym = state.hir.sym(*i);
+ write!(state, " int {} = NULL_ATTRIB;\n", sym.name.as_str());
+ }
+ write!(state, " void bind_loc(const char* name, int index) {{\n");
+ for i in attribs {
+ let sym = state.hir.sym(*i);
+ write!(
+ state,
+ " if (strcmp(\"{0}\", name) == 0) {{ {0} = index; return; }}\n",
+ sym.name.as_str()
+ );
+ }
+ write!(state, " }}\n");
+ write!(state, " int get_loc(const char* name) const {{\n");
+ for i in attribs {
+ let sym = state.hir.sym(*i);
+ write!(state,
+ " if (strcmp(\"{0}\", name) == 0) {{ \
+ return {0} != NULL_ATTRIB ? {0} : -1; \
+ }}\n",
+ sym.name.as_str());
+ }
+ write!(state, " return -1;\n");
+ write!(state, " }}\n");
+ write!(state, "}} attrib_locations;\n");
+}
+
+fn write_common_globals(state: &mut OutputState, attribs: &[hir::SymRef],
+ outputs: &[hir::SymRef], uniforms: &UniformIndices) {
+ write!(state, "struct {}_common {{\n", state.name);
+
+ write_program_samplers(state, uniforms);
+ write_bind_attrib_location(state, attribs);
+
+ let is_scalar = state.is_scalar.replace(true);
+ for i in outputs {
+ let sym = state.hir.sym(*i);
+ match &sym.decl {
+ hir::SymDecl::Global(hir::StorageClass::Out, _, ty, hir::RunClass::Scalar) => {
+ show_type(state, ty);
+ write!(state, " {};\n", sym.name.as_str());
+ }
+ _ => {}
+ }
+ }
+ for (name, (_, tk, storage)) in uniforms {
+ match storage {
+ hir::StorageClass::Sampler(format) => {
+ write!(state,
+ "{}{} {};\n",
+ tk.cxx_primitive_type_name().unwrap(),
+ format.type_suffix().unwrap_or(""),
+ name,
+ );
+ }
+ _ => {
+ show_type_kind(state, tk);
+ write!(state, " {};\n", name);
+ }
+ }
+ }
+ state.is_scalar.set(is_scalar);
+
+ write_bind_textures(state, uniforms);
+
+ write!(state, "}};\n");
+}
+
+//fn type_name(state: &OutputState, ty: &Type) -> String {
+// let buffer = state.push_buffer();
+// show_type(state, ty);
+// state.pop_buffer(buffer)
+//}
+
+fn write_load_attribs(state: &mut OutputState, attribs: &[hir::SymRef]) {
+ write!(state, "static void load_attribs(\
+ VertexShaderImpl* impl, VertexAttrib *attribs, \
+ uint32_t start, int instance, int count) {{\
+ Self* self = (Self*)impl;\n");
+ for i in attribs {
+ let sym = state.hir.sym(*i);
+ match &sym.decl {
+ hir::SymDecl::Global(_, _interpolation, _ty, run_class) => {
+ let name = sym.name.as_str();
+ let func = if *run_class == hir::RunClass::Scalar {
+ "load_flat_attrib"
+ } else {
+ "load_attrib"
+ };
+ write!(state,
+ " {0}(self->{1}, attribs[self->attrib_locations.{1}], start, instance, count);\n",
+ func, name);
+ }
+ _ => panic!(),
+ }
+ }
+ write!(state, "}}\n");
+}
+
+fn write_store_outputs(state: &mut OutputState, outputs: &[hir::SymRef]) {
+ let is_scalar = state.is_scalar.replace(true);
+ write!(state, "public:\nstruct InterpOutputs {{\n");
+ if state.hir.used_clip_dist != 0 {
+ state.write(" Float swgl_ClipDistance;\n");
+ }
+ for i in outputs {
+ let sym = state.hir.sym(*i);
+ match &sym.decl {
+ hir::SymDecl::Global(_, _, ty, run_class) => {
+ if *run_class != hir::RunClass::Scalar {
+ show_type(state, ty);
+ write!(state, " {};\n", sym.name.as_str());
+ }
+ }
+ _ => panic!(),
+ }
+ }
+
+ write!(state, "}};\nprivate:\n");
+ state.is_scalar.set(is_scalar);
+
+ write!(
+ state,
+ "ALWAYS_INLINE void store_interp_outputs(char* dest_ptr, size_t stride) {{\n"
+ );
+ write!(state, " for(int n = 0; n < 4; n++) {{\n");
+ write!(
+ state,
+ " auto* dest = reinterpret_cast<InterpOutputs*>(dest_ptr);\n"
+ );
+ if state.hir.used_clip_dist != 0 {
+ for (i, comp) in "xyzw".chars().enumerate() {
+ if (state.hir.used_clip_dist & (1 << i)) != 0 {
+ write!(state, " dest->swgl_ClipDistance.{} = get_nth(gl_ClipDistance[{}], n);\n", comp, i);
+ } else {
+ write!(state, " dest->swgl_ClipDistance.{} = 0.0f;\n", comp);
+ }
+ }
+ }
+ for i in outputs {
+ let sym = state.hir.sym(*i);
+ match &sym.decl {
+ hir::SymDecl::Global(_, _, _, run_class) => {
+ if *run_class != hir::RunClass::Scalar {
+ let name = sym.name.as_str();
+ write!(state, " dest->{} = get_nth({}, n);\n", name, name);
+ }
+ }
+ _ => panic!(),
+ }
+ }
+ write!(state, " dest_ptr += stride;\n");
+ write!(state, " }}\n");
+ write!(state, "}}\n");
+}
+
+fn write_read_inputs(state: &mut OutputState, inputs: &[hir::SymRef]) {
+ write!(
+ state,
+ "typedef {}_vert::InterpOutputs InterpInputs;\n",
+ state.name
+ );
+
+ write!(state, "InterpInputs interp_step;\n");
+
+ let mut has_varying = false;
+ for i in inputs {
+ let sym = state.hir.sym(*i);
+ match &sym.decl {
+ hir::SymDecl::Global(_, _, ty, run_class) => {
+ if *run_class != hir::RunClass::Scalar {
+ if !has_varying {
+ has_varying = true;
+ write!(state, "struct InterpPerspective {{\n");
+ }
+ show_type(state, ty);
+ write!(state, " {};\n", sym.name.as_str());
+ }
+ }
+ _ => panic!(),
+ }
+ }
+ if has_varying {
+ write!(state, "}};\n");
+ write!(state, "InterpPerspective interp_perspective;\n");
+ }
+
+ write!(state,
+ "static void read_interp_inputs(\
+ FragmentShaderImpl* impl, const void* init_, const void* step_) {{\
+ Self* self = (Self*)impl;\
+ const InterpInputs* init = (const InterpInputs*)init_;\
+ const InterpInputs* step = (const InterpInputs*)step_;\n");
+ for i in inputs {
+ let sym = state.hir.sym(*i);
+ match &sym.decl {
+ hir::SymDecl::Global(_, _, _, run_class) => {
+ if *run_class != hir::RunClass::Scalar {
+ let name = sym.name.as_str();
+ write!(
+ state,
+ " self->{0} = init_interp(init->{0}, step->{0});\n",
+ name
+ );
+ write!(
+ state,
+ " self->interp_step.{0} = step->{0} * 4.0f;\n",
+ name
+ );
+ }
+ }
+ _ => panic!(),
+ }
+ }
+ write!(state, "}}\n");
+
+ let used_fragcoord = state.used_fragcoord.get();
+ if has_varying || (used_fragcoord & (4 | 8)) != 0 {
+ state.use_perspective = true;
+ }
+ if state.use_perspective {
+ write!(state,
+ "static void read_perspective_inputs(\
+ FragmentShaderImpl* impl, const void* init_, const void* step_) {{\
+ Self* self = (Self*)impl;\
+ const InterpInputs* init = (const InterpInputs*)init_;\
+ const InterpInputs* step = (const InterpInputs*)step_;\n");
+ if has_varying {
+ write!(state, " Float w = 1.0f / self->gl_FragCoord.w;\n");
+ }
+ for i in inputs {
+ let sym = state.hir.sym(*i);
+ match &sym.decl {
+ hir::SymDecl::Global(_, _, _, run_class) => {
+ if *run_class != hir::RunClass::Scalar {
+ let name = sym.name.as_str();
+ write!(
+ state,
+ " self->interp_perspective.{0} = init_interp(init->{0}, step->{0});\n",
+ name
+ );
+ write!(state, " self->{0} = self->interp_perspective.{0} * w;\n", name);
+ write!(
+ state,
+ " self->interp_step.{0} = step->{0} * 4.0f;\n",
+ name
+ );
+ }
+ }
+ _ => panic!(),
+ }
+ }
+ write!(state, "}}\n");
+ }
+
+ write!(state, "ALWAYS_INLINE void step_interp_inputs(int steps = 4) {{\n");
+ if (used_fragcoord & 1) != 0 {
+ write!(state, " step_fragcoord(steps);\n");
+ }
+ if !inputs.is_empty() {
+ write!(state, " float chunks = steps * 0.25f;\n");
+ }
+ for i in inputs {
+ let sym = state.hir.sym(*i);
+ match &sym.decl {
+ hir::SymDecl::Global(_, _, _, run_class) => {
+ if *run_class != hir::RunClass::Scalar {
+ let name = sym.name.as_str();
+ write!(state, " {0} += interp_step.{0} * chunks;\n", name);
+ }
+ }
+ _ => panic!(),
+ }
+ }
+ write!(state, "}}\n");
+
+ if state.use_perspective {
+ write!(state, "ALWAYS_INLINE void step_perspective_inputs(int steps = 4) {{\n");
+ if (used_fragcoord & 1) != 0 {
+ write!(state, " step_fragcoord(steps);\n");
+ }
+ write!(state, " step_perspective(steps);\n");
+ if !inputs.is_empty() {
+ write!(state, " float chunks = steps * 0.25f;\n");
+ }
+ if has_varying {
+ write!(state, " Float w = 1.0f / gl_FragCoord.w;\n");
+ }
+ for i in inputs {
+ let sym = state.hir.sym(*i);
+ match &sym.decl {
+ hir::SymDecl::Global(_, _, _, run_class) => {
+ if *run_class != hir::RunClass::Scalar {
+ let name = sym.name.as_str();
+ write!(state, " interp_perspective.{0} += interp_step.{0} * chunks;\n", name);
+ write!(state, " {0} = w * interp_perspective.{0};\n", name);
+ }
+ }
+ _ => panic!(),
+ }
+ }
+ write!(state, "}}\n");
+ }
+}
+
+pub struct OutputState {
+ hir: hir::State,
+ output: String,
+ buffer: RefCell<String>,
+ should_indent: bool,
+ output_cxx: bool,
+ indent: i32,
+ mask: Option<Box<hir::Expr>>,
+ cond_index: usize,
+ return_type: Option<Box<hir::Type>>,
+ return_declared: bool,
+ return_vector: bool,
+ is_scalar: Cell<bool>,
+ is_lval: Cell<bool>,
+ name: String,
+ kind: ShaderKind,
+ functions: HashMap<(hir::SymRef, u32), bool>,
+ deps: RefCell<Vec<(hir::SymRef, u32)>>,
+ vector_mask: u32,
+ uses_discard: bool,
+ used_fragcoord: Cell<i32>,
+ use_perspective: bool,
+ used_globals: RefCell<Vec<hir::SymRef>>,
+ texel_fetches: RefCell<Vec<(hir::SymRef, hir::SymRef, hir::TexelFetchOffsets)>>,
+}
+
+use std::fmt::{Arguments, Write};
+
+impl OutputState {
+ fn indent(&mut self) {
+ if self.should_indent {
+ self.indent += 1
+ }
+ }
+ fn outdent(&mut self) {
+ if self.should_indent {
+ self.indent -= 1
+ }
+ }
+
+ fn write(&self, s: &str) {
+ self.buffer.borrow_mut().push_str(s);
+ }
+
+ fn flush_buffer(&mut self) {
+ self.output.push_str(&self.buffer.borrow());
+ self.buffer.borrow_mut().clear();
+ }
+
+ fn finish_output(&mut self) -> String {
+ self.flush_buffer();
+
+ let mut s = String::new();
+ mem::swap(&mut self.output, &mut s);
+ s
+ }
+
+ fn push_buffer(&self) -> String {
+ self.buffer.replace(String::new())
+ }
+
+ fn pop_buffer(&self, s: String) -> String {
+ self.buffer.replace(s)
+ }
+
+ fn write_fmt(&self, args: Arguments) {
+ let _ = self.buffer.borrow_mut().write_fmt(args);
+ }
+}
+
+pub fn show_identifier(state: &OutputState, i: &syntax::Identifier) {
+ state.write(&i.0);
+}
+
+fn glsl_primitive_type_name_to_cxx(glsl_name: &str) -> &str {
+ hir::TypeKind::from_glsl_primitive_type_name(glsl_name)
+ .and_then(|kind| kind.cxx_primitive_type_name())
+ .unwrap_or(glsl_name)
+}
+
+fn add_used_global(state: &OutputState, i: &hir::SymRef) {
+ let mut globals = state.used_globals.borrow_mut();
+ if !globals.contains(i) {
+ globals.push(*i);
+ }
+}
+
+pub fn show_sym(state: &OutputState, i: &hir::SymRef) {
+ let sym = state.hir.sym(*i);
+ match &sym.decl {
+ hir::SymDecl::NativeFunction(_, ref cxx_name, _) => {
+ let mut name = sym.name.as_str();
+ if state.output_cxx {
+ name = cxx_name.unwrap_or(name);
+ }
+ state.write(name);
+ }
+ hir::SymDecl::Global(..) => {
+ if state.output_cxx {
+ add_used_global(state, i);
+ }
+ let mut name = sym.name.as_str();
+ if state.output_cxx {
+ name = glsl_primitive_type_name_to_cxx(name);
+ }
+ state.write(name);
+ }
+ hir::SymDecl::UserFunction(..) | hir::SymDecl::Local(..) | hir::SymDecl::Struct(..) => {
+ let mut name = sym.name.as_str();
+ // we want to replace constructor names
+ if state.output_cxx {
+ name = glsl_primitive_type_name_to_cxx(name);
+ }
+ state.write(name);
+ }
+ }
+}
+
+pub fn show_variable(state: &OutputState, i: &hir::SymRef) {
+ let sym = state.hir.sym(*i);
+ match &sym.decl {
+ hir::SymDecl::Global(_, _, ty, _) => {
+ show_type(state, ty);
+ state.write(" ");
+ let mut name = sym.name.as_str();
+ if state.output_cxx {
+ name = glsl_primitive_type_name_to_cxx(name);
+ }
+ state.write(name);
+ }
+ _ => panic!(),
+ }
+}
+
+pub fn write_default_constructor(state: &OutputState, name: &str) {
+ // write default constructor
+ let _ = write!(state, "{}() = default;\n", name);
+}
+
+pub fn write_constructor(state: &OutputState, name: &str, s: &hir::StructFields) {
+ if s.fields.len() == 1 {
+ state.write("explicit ");
+ }
+ let _ = write!(state, "{}(", name);
+ let mut first_field = true;
+ for field in &s.fields {
+ if !first_field {
+ state.write(", ");
+ }
+ show_type(state, &field.ty);
+ state.write(" ");
+ show_identifier_and_type(state, &field.name, &field.ty);
+ first_field = false;
+ }
+ state.write(") : ");
+
+ let mut first_field = true;
+ for field in &s.fields {
+ if !first_field {
+ state.write(", ");
+ }
+ let _ = write!(state, "{}({})", field.name, field.name);
+ first_field = false;
+ }
+ state.write("{}\n");
+}
+
+pub fn write_convert_constructor(state: &OutputState, name: &str, s: &hir::StructFields) {
+ if s.fields.len() == 1 {
+ state.write("explicit ");
+ }
+ let _ = write!(state, "{}(", name);
+ let mut first_field = true;
+ for field in &s.fields {
+ if !first_field {
+ state.write(", ");
+ }
+
+ let is_scalar = state.is_scalar.replace(true);
+ show_type(state, &field.ty);
+ state.is_scalar.set(is_scalar);
+
+ state.write(" ");
+
+ show_identifier_and_type(state, &field.name, &field.ty);
+ first_field = false;
+ }
+ state.write(")");
+
+ let mut first_field = true;
+ for hir::StructField { ty, name } in &s.fields {
+ if ty.array_sizes.is_none() {
+ if first_field {
+ state.write(":");
+ } else {
+ state.write(",");
+ }
+ let _ = write!(state, "{}({})", name, name);
+ first_field = false;
+ }
+ }
+ state.write("{\n");
+ for hir::StructField { ty, name } in &s.fields {
+ if ty.array_sizes.is_some() {
+ let _ = write!(state, "this->{}.convert({});\n", name, name);
+ }
+ }
+ state.write("}\n");
+
+ let _ = write!(state, "IMPLICIT {}({}_scalar s)", name, name);
+ let mut first_field = true;
+ for hir::StructField { ty, name } in &s.fields {
+ if ty.array_sizes.is_none() {
+ if first_field {
+ state.write(":");
+ } else {
+ state.write(",");
+ }
+ let _ = write!(state, "{}(s.{})", name, name);
+ first_field = false;
+ }
+ }
+ state.write("{\n");
+ for hir::StructField { ty, name } in &s.fields {
+ if ty.array_sizes.is_some() {
+ let _ = write!(state, "{}.convert(s.{});\n", name, name);
+ }
+ }
+ state.write("}\n");
+}
+
+pub fn write_if_then_else(state: &OutputState, name: &str, s: &hir::StructFields) {
+ let _ = write!(
+ state,
+ "friend {} if_then_else(I32 c, {} t, {} e) {{ return {}(\n",
+ name, name, name, name
+ );
+ let mut first_field = true;
+ for field in &s.fields {
+ if !first_field {
+ state.write(", ");
+ }
+ let _ = write!(state, "if_then_else(c, t.{}, e.{})", field.name, field.name);
+ first_field = false;
+ }
+ state.write(");\n}");
+}
+
+pub fn show_storage_class(state: &OutputState, q: &hir::StorageClass) {
+ match *q {
+ hir::StorageClass::None => {}
+ hir::StorageClass::Const => {
+ state.write("const ");
+ }
+ hir::StorageClass::In => {
+ state.write("in ");
+ }
+ hir::StorageClass::Out => {
+ state.write("out ");
+ }
+ hir::StorageClass::FragColor(index) => {
+ write!(state, "layout(location = 0, index = {}) out ", index);
+ }
+ hir::StorageClass::Uniform | hir::StorageClass::Sampler(..) => {
+ state.write("uniform ");
+ }
+ }
+}
+
+pub fn show_sym_decl(state: &OutputState, i: &hir::SymRef) {
+ let sym = state.hir.sym(*i);
+ match &sym.decl {
+ hir::SymDecl::Global(storage, ..) => {
+ if !state.output_cxx {
+ show_storage_class(state, storage)
+ }
+ if storage == &hir::StorageClass::Const {
+ state.write("static constexpr ");
+ }
+ let mut name = sym.name.as_str();
+ if state.output_cxx {
+ name = glsl_primitive_type_name_to_cxx(name);
+ }
+ state.write(name);
+ }
+ hir::SymDecl::Local(storage, ..) => {
+ if !state.output_cxx {
+ show_storage_class(state, storage)
+ }
+ if storage == &hir::StorageClass::Const {
+ state.write("const ");
+ }
+ let mut name = sym.name.as_str();
+ if state.output_cxx {
+ name = glsl_primitive_type_name_to_cxx(name);
+ }
+ state.write(name);
+ }
+ hir::SymDecl::Struct(s) => {
+ let name = sym.name.as_str();
+
+ if state.output_cxx {
+ let name_scalar = format!("{}_scalar", name);
+ write!(state, "struct {} {{\n", name_scalar);
+ let is_scalar = state.is_scalar.replace(true);
+ for field in &s.fields {
+ show_struct_field(state, field);
+ }
+ write_default_constructor(state, &name_scalar);
+ write_constructor(state, &name_scalar, s);
+ state.is_scalar.set(is_scalar);
+ state.write("};\n");
+ }
+
+ write!(state, "struct {} {{\n", name);
+ for field in &s.fields {
+ show_struct_field(state, field);
+ }
+
+ // write if_then_else
+ if state.output_cxx {
+ write_default_constructor(state, name);
+ write_constructor(state, name, s);
+ write_convert_constructor(state, name, s);
+ write_if_then_else(state, name, s);
+ }
+ state.write("}");
+ }
+ _ => panic!(),
+ }
+}
+
+pub fn show_type_name(state: &OutputState, t: &syntax::TypeName) {
+ state.write(&t.0);
+}
+
+pub fn show_type_specifier_non_array(state: &mut OutputState, t: &syntax::TypeSpecifierNonArray) {
+ if let Some(kind) = hir::TypeKind::from_primitive_type_specifier(t) {
+ show_type_kind(state, &kind);
+ } else {
+ match t {
+ syntax::TypeSpecifierNonArray::Struct(ref _s) => panic!(), //show_struct_non_declaration(state, s),
+ syntax::TypeSpecifierNonArray::TypeName(ref tn) => show_type_name(state, tn),
+ _ => unreachable!(),
+ }
+ }
+}
+
+pub fn show_type_kind(state: &OutputState, t: &hir::TypeKind) {
+ if state.output_cxx {
+ if state.is_scalar.get() {
+ if let Some(name) = t.cxx_primitive_scalar_type_name() {
+ state.write(name);
+ } else if let Some(name) = t.cxx_primitive_type_name() {
+ let mut scalar_name = String::from(name);
+ scalar_name.push_str("_scalar");
+ state.write(scalar_name.as_str());
+ } else {
+ match t {
+ hir::TypeKind::Struct(ref s) => {
+ let mut scalar_name = String::from(state.hir.sym(*s).name.as_str());
+ scalar_name.push_str("_scalar");
+ state.write(scalar_name.as_str());
+ }
+ _ => unreachable!(),
+ }
+ }
+ } else if let Some(name) = t.cxx_primitive_type_name() {
+ state.write(name);
+ } else {
+ match t {
+ hir::TypeKind::Struct(ref s) => {
+ state.write(state.hir.sym(*s).name.as_str());
+ }
+ _ => unreachable!(),
+ }
+ }
+ } else if let Some(name) = t.glsl_primitive_type_name() {
+ state.write(name);
+ } else {
+ match t {
+ hir::TypeKind::Struct(ref s) => {
+ state.write(state.hir.sym(*s).name.as_str());
+ }
+ _ => unreachable!(),
+ }
+ }
+}
+
+pub fn show_type_specifier(state: &mut OutputState, t: &syntax::TypeSpecifier) {
+ show_type_specifier_non_array(state, &t.ty);
+
+ if let Some(ref arr_spec) = t.array_specifier {
+ show_array_spec(state, arr_spec);
+ }
+}
+
+pub fn show_type(state: &OutputState, t: &Type) {
+ if !state.output_cxx {
+ if let Some(ref precision) = t.precision {
+ show_precision_qualifier(state, precision);
+ state.write(" ");
+ }
+ }
+
+ if state.output_cxx {
+ if let Some(ref array) = t.array_sizes {
+ state.write("Array<");
+ show_type_kind(state, &t.kind);
+ let size = match &array.sizes[..] {
+ [size] => size,
+ _ => panic!(),
+ };
+ state.write(",");
+ show_hir_expr(state, size);
+ state.write(">");
+ } else {
+ show_type_kind(state, &t.kind);
+ }
+ } else {
+ show_type_kind(state, &t.kind);
+ }
+
+ /*if let Some(ref arr_spec) = t.array_sizes {
+ panic!();
+ }*/
+}
+
+/*pub fn show_fully_specified_type(state: &mut OutputState, t: &FullySpecifiedType) {
+ state.flat = false;
+ if let Some(ref qual) = t.qualifier {
+ if !state.output_cxx {
+ show_type_qualifier(state, &qual);
+ } else {
+ state.flat =
+ qual.qualifiers.0.iter()
+ .flat_map(|q| match q { syntax::TypeQualifierSpec::Interpolation(Flat) => Some(()), _ => None})
+ .next().is_some();
+ }
+ state.write(" ");
+ }
+
+ show_type_specifier(state, &t.ty);
+}*/
+
+/*pub fn show_struct_non_declaration(state: &mut OutputState, s: &syntax::StructSpecifier) {
+ state.write("struct ");
+
+ if let Some(ref name) = s.name {
+ let _ = write!(state, "{} ", name);
+ }
+
+ state.write("{\n");
+
+ for field in &s.fields.0 {
+ show_struct_field(state, field);
+ }
+
+ state.write("}");
+}*/
+
+pub fn show_struct(_state: &OutputState, _s: &syntax::StructSpecifier) {
+ panic!();
+ //show_struct_non_declaration(state, s);
+ //state.write(";\n");
+}
+
+pub fn show_struct_field(state: &OutputState, field: &hir::StructField) {
+ show_type(state, &field.ty);
+ state.write(" ");
+
+ show_identifier_and_type(state, &field.name, &field.ty);
+
+ state.write(";\n");
+}
+
+pub fn show_array_spec(state: &OutputState, a: &syntax::ArraySpecifier) {
+ for dimension in &a.dimensions {
+ match dimension {
+ syntax::ArraySpecifierDimension::Unsized => {
+ state.write("[]");
+ }
+ syntax::ArraySpecifierDimension::ExplicitlySized(ref e) => {
+ state.write("[");
+ show_expr(state, &e);
+ state.write("]");
+ }
+ }
+ }
+}
+
+pub fn show_identifier_and_type(state: &OutputState, ident: &syntax::Identifier, ty: &hir::Type) {
+ let _ = write!(state, "{}", ident);
+
+ if !state.output_cxx {
+ if let Some(ref arr_spec) = ty.array_sizes {
+ show_array_sizes(state, &arr_spec);
+ }
+ }
+}
+
+pub fn show_arrayed_identifier(state: &OutputState, ident: &syntax::ArrayedIdentifier) {
+ let _ = write!(state, "{}", ident.ident);
+
+ if let Some(ref arr_spec) = ident.array_spec {
+ show_array_spec(state, &arr_spec);
+ }
+}
+
+pub fn show_array_sizes(state: &OutputState, a: &hir::ArraySizes) {
+ state.write("[");
+ match &a.sizes[..] {
+ [a] => show_hir_expr(state, a),
+ _ => panic!(),
+ }
+
+ state.write("]");
+ /*
+ match *a {
+ syntax::ArraySpecifier::Unsized => { state.write("[]"); }
+ syntax::ArraySpecifier::ExplicitlySized(ref e) => {
+ state.write("[");
+ show_expr(state, &e);
+ state.write("]");
+ }
+ }*/
+}
+
+pub fn show_type_qualifier(state: &OutputState, q: &hir::TypeQualifier) {
+ let mut qualifiers = q.qualifiers.0.iter();
+ let first = qualifiers.next().unwrap();
+
+ show_type_qualifier_spec(state, first);
+
+ for qual_spec in qualifiers {
+ state.write(" ");
+ show_type_qualifier_spec(state, qual_spec)
+ }
+}
+
+pub fn show_type_qualifier_spec(state: &OutputState, q: &hir::TypeQualifierSpec) {
+ match *q {
+ hir::TypeQualifierSpec::Layout(ref l) => show_layout_qualifier(state, &l),
+ hir::TypeQualifierSpec::Parameter(ref _p) => panic!(),
+ hir::TypeQualifierSpec::Memory(ref _m) => panic!(),
+ hir::TypeQualifierSpec::Invariant => {
+ state.write("invariant");
+ }
+ hir::TypeQualifierSpec::Precise => {
+ state.write("precise");
+ }
+ }
+}
+
+pub fn show_syntax_storage_qualifier(state: &OutputState, q: &syntax::StorageQualifier) {
+ match *q {
+ syntax::StorageQualifier::Const => {
+ state.write("const");
+ }
+ syntax::StorageQualifier::InOut => {
+ state.write("inout");
+ }
+ syntax::StorageQualifier::In => {
+ state.write("in");
+ }
+ syntax::StorageQualifier::Out => {
+ state.write("out");
+ }
+ syntax::StorageQualifier::Centroid => {
+ state.write("centroid");
+ }
+ syntax::StorageQualifier::Patch => {
+ state.write("patch");
+ }
+ syntax::StorageQualifier::Sample => {
+ state.write("sample");
+ }
+ syntax::StorageQualifier::Uniform => {
+ state.write("uniform");
+ }
+ syntax::StorageQualifier::Attribute => {
+ state.write("attribute");
+ }
+ syntax::StorageQualifier::Varying => {
+ state.write("varying");
+ }
+ syntax::StorageQualifier::Buffer => {
+ state.write("buffer");
+ }
+ syntax::StorageQualifier::Shared => {
+ state.write("shared");
+ }
+ syntax::StorageQualifier::Coherent => {
+ state.write("coherent");
+ }
+ syntax::StorageQualifier::Volatile => {
+ state.write("volatile");
+ }
+ syntax::StorageQualifier::Restrict => {
+ state.write("restrict");
+ }
+ syntax::StorageQualifier::ReadOnly => {
+ state.write("readonly");
+ }
+ syntax::StorageQualifier::WriteOnly => {
+ state.write("writeonly");
+ }
+ syntax::StorageQualifier::Subroutine(ref n) => show_subroutine(state, &n),
+ }
+}
+
+pub fn show_subroutine(state: &OutputState, types: &[syntax::TypeName]) {
+ state.write("subroutine");
+
+ if !types.is_empty() {
+ state.write("(");
+
+ let mut types_iter = types.iter();
+ let first = types_iter.next().unwrap();
+
+ show_type_name(state, first);
+
+ for type_name in types_iter {
+ state.write(", ");
+ show_type_name(state, type_name);
+ }
+
+ state.write(")");
+ }
+}
+
+pub fn show_layout_qualifier(state: &OutputState, l: &syntax::LayoutQualifier) {
+ let mut qualifiers = l.ids.0.iter();
+ let first = qualifiers.next().unwrap();
+
+ state.write("layout (");
+ show_layout_qualifier_spec(state, first);
+
+ for qual_spec in qualifiers {
+ state.write(", ");
+ show_layout_qualifier_spec(state, qual_spec);
+ }
+
+ state.write(")");
+}
+
+pub fn show_layout_qualifier_spec(state: &OutputState, l: &syntax::LayoutQualifierSpec) {
+ match *l {
+ syntax::LayoutQualifierSpec::Identifier(ref i, Some(ref e)) => {
+ let _ = write!(state, "{} = ", i);
+ show_expr(state, &e);
+ }
+ syntax::LayoutQualifierSpec::Identifier(ref i, None) => show_identifier(state, &i),
+ syntax::LayoutQualifierSpec::Shared => {
+ state.write("shared");
+ }
+ }
+}
+
+pub fn show_precision_qualifier(state: &OutputState, p: &syntax::PrecisionQualifier) {
+ match *p {
+ syntax::PrecisionQualifier::High => {
+ state.write("highp");
+ }
+ syntax::PrecisionQualifier::Medium => {
+ state.write("mediump");
+ }
+ syntax::PrecisionQualifier::Low => {
+ state.write("low");
+ }
+ }
+}
+
+pub fn show_interpolation_qualifier(state: &OutputState, i: &syntax::InterpolationQualifier) {
+ match *i {
+ syntax::InterpolationQualifier::Smooth => {
+ state.write("smooth");
+ }
+ syntax::InterpolationQualifier::Flat => {
+ state.write("flat");
+ }
+ syntax::InterpolationQualifier::NoPerspective => {
+ state.write("noperspective");
+ }
+ }
+}
+
+pub fn show_parameter_qualifier(state: &mut OutputState, i: &Option<hir::ParameterQualifier>) {
+ if let Some(i) = i {
+ if state.output_cxx {
+ match *i {
+ hir::ParameterQualifier::Out => {
+ state.write("&");
+ }
+ hir::ParameterQualifier::InOut => {
+ state.write("&");
+ }
+ _ => {}
+ }
+ } else {
+ match *i {
+ hir::ParameterQualifier::Const => {
+ state.write("const");
+ }
+ hir::ParameterQualifier::In => {
+ state.write("in");
+ }
+ hir::ParameterQualifier::Out => {
+ state.write("out");
+ }
+ hir::ParameterQualifier::InOut => {
+ state.write("inout");
+ }
+ }
+ }
+ }
+}
+
+pub fn show_float(state: &OutputState, x: f32) {
+ if x.fract() == 0. {
+ write!(state, "{}.f", x);
+ } else {
+ write!(state, "{}f", x);
+ }
+}
+
+pub fn show_double(state: &OutputState, x: f64) {
+ // force doubles to print as floats
+ if x.fract() == 0. {
+ write!(state, "{}.f", x);
+ } else {
+ write!(state, "{}f", x);
+ }
+}
+
+fn expr_run_class(state: &OutputState, expr: &hir::Expr) -> hir::RunClass {
+ match &expr.kind {
+ hir::ExprKind::Variable(i) => symbol_run_class(&state.hir.sym(*i).decl, state.vector_mask),
+ hir::ExprKind::IntConst(_)
+ | hir::ExprKind::UIntConst(_)
+ | hir::ExprKind::BoolConst(_)
+ | hir::ExprKind::FloatConst(_)
+ | hir::ExprKind::DoubleConst(_) => hir::RunClass::Scalar,
+ hir::ExprKind::Unary(_, ref e) => expr_run_class(state, e),
+ hir::ExprKind::Binary(_, ref l, ref r) => {
+ expr_run_class(state, l).merge(expr_run_class(state, r))
+ }
+ hir::ExprKind::Ternary(ref c, ref s, ref e) => expr_run_class(state, c)
+ .merge(expr_run_class(state, s))
+ .merge(expr_run_class(state, e)),
+ hir::ExprKind::Assignment(ref v, _, ref e) => {
+ expr_run_class(state, v).merge(expr_run_class(state, e))
+ }
+ hir::ExprKind::Bracket(ref e, ref indx) => {
+ indx.iter().fold(
+ expr_run_class(state, e),
+ |run_class, indx| run_class.merge(expr_run_class(state, indx)),
+ )
+ }
+ hir::ExprKind::FunCall(ref fun, ref args) => {
+ let arg_mask: u32 = args.iter().enumerate().fold(0, |mask, (idx, e)| {
+ if expr_run_class(state, e) == hir::RunClass::Vector {
+ mask | (1 << idx)
+ } else {
+ mask
+ }
+ });
+ match fun {
+ hir::FunIdentifier::Identifier(ref sym) => match &state.hir.sym(*sym).decl {
+ hir::SymDecl::NativeFunction(_, _, ref ret_class) => {
+ if *ret_class != hir::RunClass::Unknown {
+ *ret_class
+ } else if arg_mask != 0 {
+ hir::RunClass::Vector
+ } else {
+ hir::RunClass::Scalar
+ }
+ }
+ hir::SymDecl::UserFunction(ref fd, ref run_class) => {
+ let param_mask: u32 = fd.prototype.parameters.iter().enumerate().fold(
+ arg_mask,
+ |mask, (idx, param)| {
+ if let hir::FunctionParameterDeclaration::Named(Some(qual), p) =
+ param
+ {
+ match qual {
+ hir::ParameterQualifier::InOut
+ | hir::ParameterQualifier::Out => {
+ if symbol_run_class(
+ &state.hir.sym(p.sym).decl,
+ arg_mask,
+ ) == hir::RunClass::Vector
+ {
+ mask | (1 << idx)
+ } else {
+ mask
+ }
+ }
+ _ => mask,
+ }
+ } else {
+ mask
+ }
+ },
+ );
+ match *run_class {
+ hir::RunClass::Scalar => hir::RunClass::Scalar,
+ hir::RunClass::Dependent(mask) => {
+ if (mask & param_mask) != 0 {
+ hir::RunClass::Vector
+ } else {
+ hir::RunClass::Scalar
+ }
+ }
+ _ => hir::RunClass::Vector,
+ }
+ }
+ hir::SymDecl::Struct(..) => {
+ if arg_mask != 0 {
+ hir::RunClass::Vector
+ } else {
+ hir::RunClass::Scalar
+ }
+ }
+ _ => panic!(),
+ },
+ hir::FunIdentifier::Constructor(..) => {
+ if arg_mask != 0 {
+ hir::RunClass::Vector
+ } else {
+ hir::RunClass::Scalar
+ }
+ }
+ }
+ }
+ hir::ExprKind::Dot(ref e, _) => expr_run_class(state, e),
+ hir::ExprKind::SwizzleSelector(ref e, _) => expr_run_class(state, e),
+ hir::ExprKind::PostInc(ref e) => expr_run_class(state, e),
+ hir::ExprKind::PostDec(ref e) => expr_run_class(state, e),
+ hir::ExprKind::Comma(_, ref e) => expr_run_class(state, e),
+ hir::ExprKind::Cond(_, ref e) => expr_run_class(state, e),
+ hir::ExprKind::CondMask => hir::RunClass::Vector,
+ }
+}
+
+pub fn show_hir_expr(state: &OutputState, expr: &hir::Expr) {
+ show_hir_expr_inner(state, expr, false);
+}
+
+pub fn show_hir_expr_inner(state: &OutputState, expr: &hir::Expr, top_level: bool) {
+ match expr.kind {
+ hir::ExprKind::Variable(ref i) => show_sym(state, i),
+ hir::ExprKind::IntConst(ref x) => {
+ let _ = write!(state, "{}", x);
+ }
+ hir::ExprKind::UIntConst(ref x) => {
+ let _ = write!(state, "{}u", x);
+ }
+ hir::ExprKind::BoolConst(ref x) => {
+ let _ = write!(state, "{}", x);
+ }
+ hir::ExprKind::FloatConst(ref x) => show_float(state, *x),
+ hir::ExprKind::DoubleConst(ref x) => show_double(state, *x),
+ hir::ExprKind::Unary(ref op, ref e) => {
+ show_unary_op(state, &op);
+ state.write("(");
+ show_hir_expr(state, &e);
+ state.write(")");
+ }
+ hir::ExprKind::Binary(ref op, ref l, ref r) => {
+ state.write("(");
+ show_hir_expr(state, &l);
+ state.write(")");
+ show_binary_op(state, &op);
+ state.write("(");
+ show_hir_expr(state, &r);
+ state.write(")");
+ }
+ hir::ExprKind::Ternary(ref c, ref s, ref e) => {
+ if state.output_cxx && expr_run_class(state, c) != hir::RunClass::Scalar {
+ state.write("if_then_else(");
+ show_hir_expr(state, &c);
+ state.write(", ");
+ show_hir_expr(state, &s);
+ state.write(", ");
+ show_hir_expr(state, &e);
+ state.write(")");
+ } else {
+ show_hir_expr(state, &c);
+ state.write(" ? ");
+ show_hir_expr(state, &s);
+ state.write(" : ");
+ show_hir_expr(state, &e);
+ }
+ }
+ hir::ExprKind::Assignment(ref v, ref op, ref e) => {
+ let is_output = hir::is_output(v, &state.hir).is_some();
+ let is_scalar_var = expr_run_class(state, v) == hir::RunClass::Scalar;
+ let is_scalar_expr = expr_run_class(state, e) == hir::RunClass::Scalar;
+ let force_scalar = is_scalar_var && !is_scalar_expr;
+
+ if let Some(mask) = &state.mask {
+ let is_scalar_mask = expr_run_class(state, mask) == hir::RunClass::Scalar;
+ let force_scalar_mask = is_scalar_var && is_scalar_expr && !is_scalar_mask;
+
+ if force_scalar || force_scalar_mask {
+ if top_level {
+ state.write("if (");
+ } else {
+ state.write("(");
+ }
+ } else {
+ state.is_lval.set(true);
+ show_hir_expr(state, &v);
+ state.is_lval.set(false);
+ state.write(" = if_then_else(");
+ }
+
+ if is_output && state.return_declared {
+ state.write("((");
+ show_hir_expr(state, mask);
+ state.write(")&ret_mask)");
+ } else {
+ show_hir_expr(state, mask);
+ }
+ if force_scalar || force_scalar_mask {
+ if top_level {
+ state.write("[0]) { ");
+ } else {
+ state.write("[0] ? ");
+ }
+ state.is_lval.set(true);
+ show_hir_expr(state, &v);
+ state.is_lval.set(false);
+ state.write(" = ");
+ } else {
+ state.write(",");
+ }
+
+ if op != &syntax::AssignmentOp::Equal {
+ show_hir_expr(state, &v);
+ }
+
+ match *op {
+ syntax::AssignmentOp::Equal => {}
+ syntax::AssignmentOp::Mult => {
+ state.write("*");
+ }
+ syntax::AssignmentOp::Div => {
+ state.write("/");
+ }
+ syntax::AssignmentOp::Mod => {
+ state.write("%");
+ }
+ syntax::AssignmentOp::Add => {
+ state.write("+");
+ }
+ syntax::AssignmentOp::Sub => {
+ state.write("-");
+ }
+ syntax::AssignmentOp::LShift => {
+ state.write("<<");
+ }
+ syntax::AssignmentOp::RShift => {
+ state.write(">>");
+ }
+ syntax::AssignmentOp::And => {
+ state.write("&");
+ }
+ syntax::AssignmentOp::Xor => {
+ state.write("^");
+ }
+ syntax::AssignmentOp::Or => {
+ state.write("|");
+ }
+ }
+ if force_scalar {
+ state.write("force_scalar(");
+ }
+ show_hir_expr(state, &e);
+ if force_scalar {
+ state.write(")");
+ }
+ if force_scalar || force_scalar_mask {
+ if top_level {
+ state.write("; }");
+ } else {
+ state.write(" : ");
+ show_hir_expr(state, &v);
+ state.write(")");
+ }
+ } else {
+ state.write(",");
+ show_hir_expr(state, &v);
+ state.write(")");
+ }
+ } else {
+ state.is_lval.set(true);
+ show_hir_expr(state, &v);
+ state.is_lval.set(false);
+ state.write(" ");
+
+ if is_output && state.return_declared {
+ state.write("= ");
+ if force_scalar {
+ state.write("force_scalar(");
+ }
+ state.write("if_then_else(ret_mask,");
+
+ if op != &syntax::AssignmentOp::Equal {
+ show_hir_expr(state, &v);
+ }
+
+ match *op {
+ syntax::AssignmentOp::Equal => {}
+ syntax::AssignmentOp::Mult => {
+ state.write("*");
+ }
+ syntax::AssignmentOp::Div => {
+ state.write("/");
+ }
+ syntax::AssignmentOp::Mod => {
+ state.write("%");
+ }
+ syntax::AssignmentOp::Add => {
+ state.write("+");
+ }
+ syntax::AssignmentOp::Sub => {
+ state.write("-");
+ }
+ syntax::AssignmentOp::LShift => {
+ state.write("<<");
+ }
+ syntax::AssignmentOp::RShift => {
+ state.write(">>");
+ }
+ syntax::AssignmentOp::And => {
+ state.write("&");
+ }
+ syntax::AssignmentOp::Xor => {
+ state.write("^");
+ }
+ syntax::AssignmentOp::Or => {
+ state.write("|");
+ }
+ }
+ show_hir_expr(state, &e);
+ state.write(",");
+ show_hir_expr(state, &v);
+ state.write(")");
+ } else {
+ show_assignment_op(state, &op);
+ state.write(" ");
+ if force_scalar {
+ state.write("force_scalar(");
+ }
+ show_hir_expr(state, &e);
+ }
+
+ if force_scalar {
+ state.write(")");
+ }
+ }
+ }
+ hir::ExprKind::Bracket(ref e, ref indx) => {
+ show_hir_expr(state, &e);
+ state.write("[");
+ for dimension in indx {
+ show_hir_expr(state, dimension);
+ }
+ state.write("]");
+ }
+ hir::ExprKind::FunCall(ref fun, ref args) => {
+ let mut cond_mask: u32 = 0;
+ let mut adapt_mask: u32 = 0;
+ let mut has_ret = false;
+ let mut array_constructor = false;
+
+ let mut arg_mask: u32 = 0;
+ for (idx, e) in args.iter().enumerate() {
+ if expr_run_class(state, e) == hir::RunClass::Vector {
+ arg_mask |= 1 << idx;
+ }
+ }
+
+ match fun {
+ hir::FunIdentifier::Constructor(t) => {
+ let is_scalar = state.is_scalar.replace(arg_mask == 0);
+ show_type(state, t);
+ state.is_scalar.set(is_scalar);
+ array_constructor = t.array_sizes.is_some();
+ }
+ hir::FunIdentifier::Identifier(name) => {
+ if state.output_cxx {
+ let sym = state.hir.sym(*name);
+ match &sym.decl {
+ hir::SymDecl::NativeFunction(..) => {
+ if sym.name == "texelFetchOffset" && args.len() >= 4 {
+ if let Some((sampler, base, x, y)) = hir::get_texel_fetch_offset(
+ &state.hir, &args[0], &args[1], &args[3],
+ ) {
+ let base_sym = state.hir.sym(base);
+ let sampler_sym = state.hir.sym(sampler);
+ add_used_global(state, &sampler);
+ if let hir::SymDecl::Global(..) = &base_sym.decl {
+ add_used_global(state, &base);
+ }
+ write!(
+ state,
+ "texelFetchUnchecked({}, {}_{}_fetch, {}, {})",
+ sampler_sym.name,
+ sampler_sym.name,
+ base_sym.name,
+ x,
+ y,
+ );
+ return;
+ }
+ }
+ show_sym(state, name)
+ }
+ hir::SymDecl::UserFunction(ref fd, ref _run_class) => {
+ if (state.mask.is_some() || state.return_declared) &&
+ !fd.globals.is_empty()
+ {
+ cond_mask |= 1 << 31;
+ }
+ let mut param_mask: u32 = 0;
+ for (idx, (param, e)) in
+ fd.prototype.parameters.iter().zip(args.iter()).enumerate()
+ {
+ if let hir::FunctionParameterDeclaration::Named(qual, p) = param
+ {
+ if symbol_run_class(&state.hir.sym(p.sym).decl, arg_mask)
+ == hir::RunClass::Vector
+ {
+ param_mask |= 1 << idx;
+ }
+ match qual {
+ Some(hir::ParameterQualifier::InOut)
+ | Some(hir::ParameterQualifier::Out) => {
+ if state.mask.is_some() || state.return_declared {
+ cond_mask |= 1 << idx;
+ }
+ if (!arg_mask & param_mask & (1 << idx)) != 0 {
+ if adapt_mask == 0 {
+ state.write(if top_level {
+ "{ "
+ } else {
+ "({ "
+ });
+ }
+ show_type(state, &p.ty);
+ write!(state, " _arg{}_ = ", idx);
+ show_hir_expr(state, e);
+ state.write("; ");
+ adapt_mask |= 1 << idx;
+ }
+ }
+ _ => {}
+ }
+ }
+ }
+ if adapt_mask != 0 &&
+ fd.prototype.ty.kind != hir::TypeKind::Void &&
+ !top_level
+ {
+ state.write("auto _ret_ = ");
+ has_ret = true;
+ }
+ show_sym(state, name);
+ let mut deps = state.deps.borrow_mut();
+ let dep_key = (
+ *name,
+ if cond_mask != 0 {
+ param_mask | (1 << 31)
+ } else {
+ param_mask
+ },
+ );
+ if !deps.contains(&dep_key) {
+ deps.push(dep_key);
+ }
+ }
+ hir::SymDecl::Struct(..) => {
+ show_sym(state, name);
+ if arg_mask == 0 {
+ state.write("_scalar");
+ }
+ }
+ _ => panic!("bad identifier to function call"),
+ }
+ }
+ }
+ }
+
+ if array_constructor {
+ state.write("{{");
+ } else {
+ state.write("(");
+ }
+
+ for (idx, e) in args.iter().enumerate() {
+ if idx != 0 {
+ state.write(", ");
+ }
+ if (adapt_mask & (1 << idx)) != 0 {
+ write!(state, "_arg{}_", idx);
+ } else {
+ show_hir_expr(state, e);
+ }
+ }
+
+ if cond_mask != 0 {
+ if !args.is_empty() {
+ state.write(", ");
+ }
+ if let Some(mask) = &state.mask {
+ if state.return_declared {
+ state.write("(");
+ show_hir_expr(state, mask);
+ state.write(")&ret_mask");
+ } else {
+ show_hir_expr(state, mask);
+ }
+ } else if state.return_declared {
+ state.write("ret_mask");
+ } else {
+ state.write("~0");
+ }
+ }
+
+ if array_constructor {
+ state.write("}}");
+ } else {
+ state.write(")");
+ }
+
+ if adapt_mask != 0 {
+ state.write("; ");
+ for (idx, e) in args.iter().enumerate() {
+ if (adapt_mask & (1 << idx)) != 0 {
+ state.is_lval.set(true);
+ show_hir_expr(state, e);
+ state.is_lval.set(false);
+ write!(state, " = force_scalar(_arg{}_); ", idx);
+ }
+ }
+ if has_ret {
+ state.write("_ret_; })");
+ } else {
+ state.write(if top_level { "}" } else { "})" });
+ }
+ }
+ }
+ hir::ExprKind::Dot(ref e, ref i) => {
+ state.write("(");
+ show_hir_expr(state, &e);
+ state.write(")");
+ state.write(".");
+ show_identifier(state, i);
+ }
+ hir::ExprKind::SwizzleSelector(ref e, ref s) => {
+ if state.output_cxx {
+ if let hir::ExprKind::Variable(ref sym) = &e.kind {
+ if state.hir.sym(*sym).name == "gl_FragCoord" {
+ state.used_fragcoord.set(
+ s.components.iter().fold(
+ state.used_fragcoord.get(),
+ |used, c| used | (1 << c)));
+ }
+ }
+ state.write("(");
+ show_hir_expr(state, &e);
+ state.write(").");
+ if s.components.len() == 1 {
+ // For single component swizzles, output a field access to
+ // avoid stressing inlining of sel().
+ state.write(&s.to_field_set(hir::FieldSet::Xyzw));
+ } else {
+ if state.is_lval.get() && s.components.len() > 1 {
+ state.write("lsel(");
+ } else {
+ state.write("sel(");
+ }
+ for (i, c) in s.to_string().chars().enumerate() {
+ if i > 0 {
+ state.write(",");
+ }
+ write!(state, "{}", c.to_uppercase());
+ }
+ state.write(")");
+ }
+ } else {
+ state.write("(");
+ show_hir_expr(state, &e);
+ state.write(")");
+ state.write(".");
+ state.write(&s.to_string());
+ }
+ }
+ hir::ExprKind::PostInc(ref e) => {
+ show_hir_expr(state, &e);
+ state.write("++");
+ }
+ hir::ExprKind::PostDec(ref e) => {
+ show_hir_expr(state, &e);
+ state.write("--");
+ }
+ hir::ExprKind::Comma(ref a, ref b) => {
+ show_hir_expr(state, &a);
+ state.write(", ");
+ show_hir_expr(state, &b);
+ }
+ hir::ExprKind::Cond(index, _) => {
+ write!(state, "_c{}_", index);
+ }
+ hir::ExprKind::CondMask => {
+ state.write("_cond_mask_");
+ }
+ }
+}
+
+pub fn show_expr(state: &OutputState, expr: &syntax::Expr) {
+ match *expr {
+ syntax::Expr::Variable(ref i) => show_identifier(state, &i),
+ syntax::Expr::IntConst(ref x) => {
+ let _ = write!(state, "{}", x);
+ }
+ syntax::Expr::UIntConst(ref x) => {
+ let _ = write!(state, "{}u", x);
+ }
+ syntax::Expr::BoolConst(ref x) => {
+ let _ = write!(state, "{}", x);
+ }
+ syntax::Expr::FloatConst(ref x) => show_float(state, *x),
+ syntax::Expr::DoubleConst(ref x) => show_double(state, *x),
+ syntax::Expr::Unary(ref op, ref e) => {
+ show_unary_op(state, &op);
+ state.write("(");
+ show_expr(state, &e);
+ state.write(")");
+ }
+ syntax::Expr::Binary(ref op, ref l, ref r) => {
+ state.write("(");
+ show_expr(state, &l);
+ state.write(")");
+ show_binary_op(state, &op);
+ state.write("(");
+ show_expr(state, &r);
+ state.write(")");
+ }
+ syntax::Expr::Ternary(ref c, ref s, ref e) => {
+ show_expr(state, &c);
+ state.write(" ? ");
+ show_expr(state, &s);
+ state.write(" : ");
+ show_expr(state, &e);
+ }
+ syntax::Expr::Assignment(ref v, ref op, ref e) => {
+ show_expr(state, &v);
+ state.write(" ");
+ show_assignment_op(state, &op);
+ state.write(" ");
+ show_expr(state, &e);
+ }
+ syntax::Expr::Bracket(ref e, ref a) => {
+ show_expr(state, &e);
+ show_array_spec(state, &a);
+ }
+ syntax::Expr::FunCall(ref fun, ref args) => {
+ show_function_identifier(state, &fun);
+ state.write("(");
+
+ if !args.is_empty() {
+ let mut args_iter = args.iter();
+ let first = args_iter.next().unwrap();
+ show_expr(state, first);
+
+ for e in args_iter {
+ state.write(", ");
+ show_expr(state, e);
+ }
+ }
+
+ state.write(")");
+ }
+ syntax::Expr::Dot(ref e, ref i) => {
+ state.write("(");
+ show_expr(state, &e);
+ state.write(")");
+ state.write(".");
+ show_identifier(state, &i);
+ }
+ syntax::Expr::PostInc(ref e) => {
+ show_expr(state, &e);
+ state.write("++");
+ }
+ syntax::Expr::PostDec(ref e) => {
+ show_expr(state, &e);
+ state.write("--");
+ }
+ syntax::Expr::Comma(ref a, ref b) => {
+ show_expr(state, &a);
+ state.write(", ");
+ show_expr(state, &b);
+ }
+ }
+}
+
+pub fn show_unary_op(state: &OutputState, op: &syntax::UnaryOp) {
+ match *op {
+ syntax::UnaryOp::Inc => {
+ state.write("++");
+ }
+ syntax::UnaryOp::Dec => {
+ state.write("--");
+ }
+ syntax::UnaryOp::Add => {
+ state.write("+");
+ }
+ syntax::UnaryOp::Minus => {
+ state.write("-");
+ }
+ syntax::UnaryOp::Not => {
+ state.write("!");
+ }
+ syntax::UnaryOp::Complement => {
+ state.write("~");
+ }
+ }
+}
+
+pub fn show_binary_op(state: &OutputState, op: &syntax::BinaryOp) {
+ match *op {
+ syntax::BinaryOp::Or => {
+ state.write("||");
+ }
+ syntax::BinaryOp::Xor => {
+ state.write("^^");
+ }
+ syntax::BinaryOp::And => {
+ state.write("&&");
+ }
+ syntax::BinaryOp::BitOr => {
+ state.write("|");
+ }
+ syntax::BinaryOp::BitXor => {
+ state.write("^");
+ }
+ syntax::BinaryOp::BitAnd => {
+ state.write("&");
+ }
+ syntax::BinaryOp::Equal => {
+ state.write("==");
+ }
+ syntax::BinaryOp::NonEqual => {
+ state.write("!=");
+ }
+ syntax::BinaryOp::LT => {
+ state.write("<");
+ }
+ syntax::BinaryOp::GT => {
+ state.write(">");
+ }
+ syntax::BinaryOp::LTE => {
+ state.write("<=");
+ }
+ syntax::BinaryOp::GTE => {
+ state.write(">=");
+ }
+ syntax::BinaryOp::LShift => {
+ state.write("<<");
+ }
+ syntax::BinaryOp::RShift => {
+ state.write(">>");
+ }
+ syntax::BinaryOp::Add => {
+ state.write("+");
+ }
+ syntax::BinaryOp::Sub => {
+ state.write("-");
+ }
+ syntax::BinaryOp::Mult => {
+ state.write("*");
+ }
+ syntax::BinaryOp::Div => {
+ state.write("/");
+ }
+ syntax::BinaryOp::Mod => {
+ state.write("%");
+ }
+ }
+}
+
+pub fn show_assignment_op(state: &OutputState, op: &syntax::AssignmentOp) {
+ match *op {
+ syntax::AssignmentOp::Equal => {
+ state.write("=");
+ }
+ syntax::AssignmentOp::Mult => {
+ state.write("*=");
+ }
+ syntax::AssignmentOp::Div => {
+ state.write("/=");
+ }
+ syntax::AssignmentOp::Mod => {
+ state.write("%=");
+ }
+ syntax::AssignmentOp::Add => {
+ state.write("+=");
+ }
+ syntax::AssignmentOp::Sub => {
+ state.write("-=");
+ }
+ syntax::AssignmentOp::LShift => {
+ state.write("<<=");
+ }
+ syntax::AssignmentOp::RShift => {
+ state.write(">>=");
+ }
+ syntax::AssignmentOp::And => {
+ state.write("&=");
+ }
+ syntax::AssignmentOp::Xor => {
+ state.write("^=");
+ }
+ syntax::AssignmentOp::Or => {
+ state.write("|=");
+ }
+ }
+}
+
+pub fn show_function_identifier(state: &OutputState, i: &syntax::FunIdentifier) {
+ match *i {
+ syntax::FunIdentifier::Identifier(ref n) => show_identifier(state, &n),
+ syntax::FunIdentifier::Expr(ref e) => show_expr(state, &*e),
+ }
+}
+
+pub fn show_hir_function_identifier(state: &OutputState, i: &hir::FunIdentifier) {
+ match *i {
+ hir::FunIdentifier::Identifier(ref n) => show_sym(state, n),
+ hir::FunIdentifier::Constructor(ref t) => show_type(state, &*t),
+ }
+}
+
+pub fn show_declaration(state: &mut OutputState, d: &hir::Declaration) {
+ show_indent(state);
+ match *d {
+ hir::Declaration::FunctionPrototype(ref proto) => {
+ if !state.output_cxx {
+ show_function_prototype(state, &proto);
+ state.write(";\n");
+ }
+ }
+ hir::Declaration::InitDeclaratorList(ref list) => {
+ show_init_declarator_list(state, &list);
+ state.write(";\n");
+
+ if state.output_cxx {
+ let base = list.head.name;
+ let base_sym = state.hir.sym(base);
+ if let hir::SymDecl::Local(..) = &base_sym.decl {
+ let mut texel_fetches = state.texel_fetches.borrow_mut();
+ while let Some(idx) = texel_fetches.iter().position(|&(_, b, _)| b == base)
+ {
+ let (sampler, _, offsets) = texel_fetches.remove(idx);
+ let sampler_sym = state.hir.sym(sampler);
+ define_texel_fetch_ptr(state, &base_sym, &sampler_sym, &offsets);
+ }
+ }
+ }
+ }
+ hir::Declaration::Precision(ref qual, ref ty) => {
+ if !state.output_cxx {
+ show_precision_qualifier(state, &qual);
+ show_type_specifier(state, &ty);
+ state.write(";\n");
+ }
+ }
+ hir::Declaration::Block(ref _block) => {
+ panic!();
+ //show_block(state, &block);
+ //state.write(";\n");
+ }
+ hir::Declaration::Global(ref qual, ref identifiers) => {
+ // We only want to output GLSL layout qualifiers if not C++
+ if !state.output_cxx {
+ show_type_qualifier(state, &qual);
+
+ if !identifiers.is_empty() {
+ let mut iter = identifiers.iter();
+ let first = iter.next().unwrap();
+ show_identifier(state, first);
+
+ for identifier in iter {
+ let _ = write!(state, ", {}", identifier);
+ }
+ }
+
+ state.write(";\n");
+ }
+ }
+ hir::Declaration::StructDefinition(ref sym) => {
+ show_sym_decl(state, sym);
+
+ state.write(";\n");
+ }
+ }
+}
+
+pub fn show_function_prototype(state: &mut OutputState, fp: &hir::FunctionPrototype) {
+ let is_scalar = state.is_scalar.replace(!state.return_vector);
+ show_type(state, &fp.ty);
+ state.is_scalar.set(is_scalar);
+
+ state.write(" ");
+ show_identifier(state, &fp.name);
+
+ state.write("(");
+
+ if !fp.parameters.is_empty() {
+ let mut iter = fp.parameters.iter();
+ let first = iter.next().unwrap();
+ show_function_parameter_declaration(state, first);
+
+ for param in iter {
+ state.write(", ");
+ show_function_parameter_declaration(state, param);
+ }
+ }
+
+ if state.output_cxx && (state.vector_mask & (1 << 31)) != 0 {
+ if !fp.parameters.is_empty() {
+ state.write(", ");
+ }
+ state.write("I32 _cond_mask_");
+ }
+
+ state.write(")");
+}
+
+pub fn show_function_parameter_declaration(
+ state: &mut OutputState,
+ p: &hir::FunctionParameterDeclaration,
+) {
+ match *p {
+ hir::FunctionParameterDeclaration::Named(ref qual, ref fpd) => {
+ if state.output_cxx {
+ let is_scalar = state.is_scalar.replace(
+ symbol_run_class(&state.hir.sym(fpd.sym).decl, state.vector_mask)
+ == hir::RunClass::Scalar,
+ );
+ show_type(state, &fpd.ty);
+ state.is_scalar.set(is_scalar);
+ show_parameter_qualifier(state, qual);
+ } else {
+ show_parameter_qualifier(state, qual);
+ state.write(" ");
+ show_type(state, &fpd.ty);
+ }
+ state.write(" ");
+ show_identifier_and_type(state, &fpd.name, &fpd.ty);
+ }
+ hir::FunctionParameterDeclaration::Unnamed(ref qual, ref ty) => {
+ if state.output_cxx {
+ show_type_specifier(state, ty);
+ show_parameter_qualifier(state, qual);
+ } else {
+ show_parameter_qualifier(state, qual);
+ state.write(" ");
+ show_type_specifier(state, ty);
+ }
+ }
+ }
+}
+
+pub fn show_init_declarator_list(state: &mut OutputState, i: &hir::InitDeclaratorList) {
+ show_single_declaration(state, &i.head);
+
+ for decl in &i.tail {
+ state.write(", ");
+ show_single_declaration_no_type(state, decl);
+ }
+}
+
+pub fn show_single_declaration(state: &mut OutputState, d: &hir::SingleDeclaration) {
+ if state.output_cxx {
+ show_single_declaration_cxx(state, d)
+ } else {
+ show_single_declaration_glsl(state, d)
+ }
+}
+
+pub fn show_single_declaration_glsl(state: &mut OutputState, d: &hir::SingleDeclaration) {
+ if let Some(ref qual) = d.qualifier {
+ show_type_qualifier(state, &qual);
+ state.write(" ");
+ }
+
+ let sym = state.hir.sym(d.name);
+ match &sym.decl {
+ hir::SymDecl::Global(storage, interpolation, ..) => {
+ show_storage_class(state, storage);
+ if let Some(i) = interpolation {
+ show_interpolation_qualifier(state, i);
+ }
+ }
+ hir::SymDecl::Local(storage, ..) => show_storage_class(state, storage),
+ _ => panic!("should be variable"),
+ }
+
+ if let Some(ty_def) = d.ty_def {
+ show_sym_decl(state, &ty_def);
+ } else {
+ show_type(state, &d.ty);
+ }
+
+ state.write(" ");
+ state.write(sym.name.as_str());
+
+ if let Some(ref arr_spec) = d.ty.array_sizes {
+ show_array_sizes(state, &arr_spec);
+ }
+
+ if let Some(ref initializer) = d.initializer {
+ state.write(" = ");
+ show_initializer(state, initializer);
+ }
+}
+
+fn symbol_run_class(decl: &hir::SymDecl, vector_mask: u32) -> hir::RunClass {
+ let run_class = match decl {
+ hir::SymDecl::Global(_, _, _, run_class) => *run_class,
+ hir::SymDecl::Local(_, _, run_class) => *run_class,
+ _ => hir::RunClass::Vector,
+ };
+ match run_class {
+ hir::RunClass::Scalar => hir::RunClass::Scalar,
+ hir::RunClass::Dependent(mask) => {
+ if (mask & vector_mask) != 0 {
+ hir::RunClass::Vector
+ } else {
+ hir::RunClass::Scalar
+ }
+ }
+ _ => hir::RunClass::Vector,
+ }
+}
+
+pub fn show_single_declaration_cxx(state: &mut OutputState, d: &hir::SingleDeclaration) {
+ let sym = state.hir.sym(d.name);
+ if state.kind == ShaderKind::Vertex {
+ match &sym.decl {
+ hir::SymDecl::Global(hir::StorageClass::Uniform, ..) |
+ hir::SymDecl::Global(hir::StorageClass::Sampler(_), ..) |
+ hir::SymDecl::Global(hir::StorageClass::Out, _, _, hir::RunClass::Scalar) => {
+ state.write("// ");
+ }
+ _ => {}
+ }
+ } else {
+ match &sym.decl {
+ hir::SymDecl::Global(hir::StorageClass::FragColor(index), ..) => {
+ let fragcolor = match index {
+ 0 => "gl_FragColor",
+ 1 => "gl_SecondaryFragColor",
+ _ => panic!(),
+ };
+ write!(state, "#define {} {}\n", sym.name, fragcolor);
+ show_indent(state);
+ state.write("// ");
+ }
+ hir::SymDecl::Global(hir::StorageClass::Out, ..) => {
+ write!(state, "#define {} gl_FragColor\n", sym.name);
+ show_indent(state);
+ state.write("// ");
+ }
+ hir::SymDecl::Global(hir::StorageClass::Uniform, ..) |
+ hir::SymDecl::Global(hir::StorageClass::Sampler(_), ..) |
+ hir::SymDecl::Global(hir::StorageClass::In, _, _, hir::RunClass::Scalar) => {
+ state.write("// ");
+ }
+ _ => {}
+ }
+ }
+ let is_scalar = state
+ .is_scalar
+ .replace(symbol_run_class(&sym.decl, state.vector_mask) == hir::RunClass::Scalar);
+
+ if let Some(ref _array) = d.ty.array_sizes {
+ show_type(state, &d.ty);
+ } else {
+ if let Some(ty_def) = d.ty_def {
+ show_sym_decl(state, &ty_def);
+ } else {
+ show_type(state, &d.ty);
+ }
+ }
+
+ // XXX: this is pretty grotty
+ state.write(" ");
+ show_sym_decl(state, &d.name);
+
+ state.is_scalar.set(is_scalar);
+
+ if let Some(ref initializer) = d.initializer {
+ state.write(" = ");
+ show_initializer(state, initializer);
+ }
+}
+
+pub fn show_single_declaration_no_type(state: &OutputState, d: &hir::SingleDeclarationNoType) {
+ show_arrayed_identifier(state, &d.ident);
+
+ if let Some(ref initializer) = d.initializer {
+ state.write(" = ");
+ show_initializer(state, initializer);
+ }
+}
+
+pub fn show_initializer(state: &OutputState, i: &hir::Initializer) {
+ match *i {
+ hir::Initializer::Simple(ref e) => show_hir_expr(state, e),
+ hir::Initializer::List(ref list) => {
+ let mut iter = list.0.iter();
+ let first = iter.next().unwrap();
+
+ state.write("{ ");
+ show_initializer(state, first);
+
+ for ini in iter {
+ state.write(", ");
+ show_initializer(state, ini);
+ }
+
+ state.write(" }");
+ }
+ }
+}
+
+/*
+pub fn show_block(state: &mut OutputState, b: &hir::Block) {
+ show_type_qualifier(state, &b.qualifier);
+ state.write(" ");
+ show_identifier(state, &b.name);
+ state.write(" {");
+
+ for field in &b.fields {
+ show_struct_field(state, field);
+ state.write("\n");
+ }
+ state.write("}");
+
+ if let Some(ref ident) = b.identifier {
+ show_arrayed_identifier(state, ident);
+ }
+}
+*/
+
+// This is a hack to run through the first time with an empty writter to find if 'return' is declared.
+pub fn has_conditional_return(state: &mut OutputState, cst: &hir::CompoundStatement) -> bool {
+ let buffer = state.push_buffer();
+ show_compound_statement(state, cst);
+ state.pop_buffer(buffer);
+ let result = state.return_declared;
+ state.return_declared = false;
+ result
+}
+
+fn define_texel_fetch_ptr(
+ state: &OutputState,
+ base_sym: &hir::Symbol,
+ sampler_sym: &hir::Symbol,
+ offsets: &hir::TexelFetchOffsets,
+) {
+ show_indent(state);
+ write!(
+ state,
+ "auto {}_{}_fetch = texelFetchPtr({}, {}, {}, {}, {}, {});\n",
+ sampler_sym.name,
+ base_sym.name,
+ sampler_sym.name,
+ base_sym.name,
+ offsets.min_x,
+ offsets.max_x,
+ offsets.min_y,
+ offsets.max_y,
+ );
+}
+
+pub fn show_function_definition(
+ state: &mut OutputState,
+ fd: &hir::FunctionDefinition,
+ vector_mask: u32,
+) {
+ // println!("start {:?} {:?}", fd.prototype.name, vector_mask);
+ if state.output_cxx && fd.prototype.name.as_str() == "main" {
+ state.write("ALWAYS_INLINE ");
+ }
+ show_function_prototype(state, &fd.prototype);
+ state.write(" ");
+ state.return_type = Some(Box::new(fd.prototype.ty.clone()));
+
+ if state.output_cxx && (vector_mask & (1 << 31)) != 0 {
+ state.mask = Some(Box::new(hir::Expr {
+ kind: hir::ExprKind::CondMask,
+ ty: hir::Type::new(hir::TypeKind::Bool),
+ }));
+ }
+
+ show_indent(state);
+ state.write("{\n");
+
+ state.indent();
+ if has_conditional_return(state, &fd.body) {
+ show_indent(state);
+ state.write(if state.return_vector {
+ "I32"
+ } else {
+ "int32_t"
+ });
+ state.write(" ret_mask = ");
+ if let Some(mask) = &state.mask {
+ show_hir_expr(state, mask);
+ } else {
+ state.write("~0");
+ }
+ state.write(";\n");
+ // XXX: the cloning here is bad
+ show_indent(state);
+ if fd.prototype.ty != Type::new(hir::TypeKind::Void) {
+ let is_scalar = state.is_scalar.replace(!state.return_vector);
+ show_type(state, &state.return_type.clone().unwrap());
+ state.write(" ret;\n");
+ state.is_scalar.set(is_scalar);
+ }
+ }
+
+ if state.output_cxx {
+ match fd.prototype.name.as_str() {
+ "swgl_drawSpanRGBA8" |
+ "swgl_drawSpanR8" => {
+ // Partial spans are not drawn using span shaders, but rather drawn with a fragment shader
+ // where the span shader left off. We need to undo any changes to the interpolants made by
+ // the span shaders so that we can reset the interpolants to where the fragment shader
+ // expects them. We do this by saving them in an _Undo_ struct on entry to the span shader,
+ // and then restore them in the _Undo_ struct destructor.
+ let mut needs_undo = vec![];
+ for global in &fd.globals {
+ let sym = state.hir.sym(*global);
+ match &sym.decl {
+ hir::SymDecl::Global(hir::StorageClass::In, _, ty, hir::RunClass::Vector) => {
+ if needs_undo.is_empty() {
+ state.write("struct _Undo_ {\nSelf* self;\n");
+ }
+ show_type(state, ty);
+ write!(state, " {};\n", sym.name);
+ needs_undo.push(sym.name.clone());
+ }
+ _ => {}
+ }
+ }
+ if !needs_undo.is_empty() {
+ state.write("explicit _Undo_(Self* self) : self(self)");
+ for name in &needs_undo {
+ write!(state, ", {0}(self->{0})", name);
+ }
+ state.write(" {}\n");
+ state.write("~_Undo_() {\n");
+ for name in &needs_undo {
+ write!(state, "self->{0} = {0};\n", name);
+ }
+ state.write("}} _undo_(this);\n");
+ }
+ }
+ _ => {}
+ }
+
+ let mut texel_fetches = state.texel_fetches.borrow_mut();
+ texel_fetches.clear();
+ for ((sampler, base), offsets) in fd.texel_fetches.iter() {
+ add_used_global(state, sampler);
+ let sampler_sym = state.hir.sym(*sampler);
+ let base_sym = state.hir.sym(*base);
+ match &base_sym.decl {
+ hir::SymDecl::Global(..) => {
+ add_used_global(state, base);
+ define_texel_fetch_ptr(state, &base_sym, &sampler_sym, &offsets);
+ }
+ hir::SymDecl::Local(..) => {
+ if fd.prototype.has_parameter(*base) {
+ define_texel_fetch_ptr(state, &base_sym, &sampler_sym, &offsets);
+ } else {
+ texel_fetches.push((*sampler, *base, offsets.clone()));
+ }
+ }
+ _ => panic!(),
+ }
+ }
+ }
+
+ for st in &fd.body.statement_list {
+ show_statement(state, st);
+ }
+
+ if state.return_declared {
+ show_indent(state);
+ if fd.prototype.ty == Type::new(hir::TypeKind::Void) {
+ state.write("return;\n");
+ } else {
+ state.write("return ret;\n");
+ }
+ }
+ state.outdent();
+
+ show_indent(state);
+ state.write("}\n");
+ // println!("end {:?}", fd.prototype.name);
+
+ state.return_type = None;
+ state.return_declared = false;
+ state.mask = None;
+}
+
+pub fn show_compound_statement(state: &mut OutputState, cst: &hir::CompoundStatement) {
+ show_indent(state);
+ state.write("{\n");
+
+ state.indent();
+ for st in &cst.statement_list {
+ show_statement(state, st);
+ }
+ state.outdent();
+
+ show_indent(state);
+ state.write("}\n");
+}
+
+pub fn show_statement(state: &mut OutputState, st: &hir::Statement) {
+ match *st {
+ hir::Statement::Compound(ref cst) => show_compound_statement(state, cst),
+ hir::Statement::Simple(ref sst) => show_simple_statement(state, sst),
+ }
+}
+
+pub fn show_simple_statement(state: &mut OutputState, sst: &hir::SimpleStatement) {
+ match *sst {
+ hir::SimpleStatement::Declaration(ref d) => show_declaration(state, d),
+ hir::SimpleStatement::Expression(ref e) => show_expression_statement(state, e),
+ hir::SimpleStatement::Selection(ref s) => show_selection_statement(state, s),
+ hir::SimpleStatement::Switch(ref s) => show_switch_statement(state, s),
+ hir::SimpleStatement::Iteration(ref i) => show_iteration_statement(state, i),
+ hir::SimpleStatement::Jump(ref j) => show_jump_statement(state, j),
+ }
+}
+
+pub fn show_indent(state: &OutputState) {
+ for _ in 0 .. state.indent {
+ state.write(" ");
+ }
+}
+
+pub fn show_expression_statement(state: &mut OutputState, est: &hir::ExprStatement) {
+ show_indent(state);
+
+ if let Some(ref e) = *est {
+ show_hir_expr_inner(state, e, true);
+ }
+
+ state.write(";\n");
+}
+
+pub fn show_selection_statement(state: &mut OutputState, sst: &hir::SelectionStatement) {
+ show_indent(state);
+
+ if state.output_cxx &&
+ (state.return_declared || expr_run_class(state, &sst.cond) != hir::RunClass::Scalar)
+ {
+ let (cond_index, mask) = if state.mask.is_none() || sst.else_stmt.is_some() {
+ let cond = sst.cond.clone();
+ state.cond_index += 1;
+ let cond_index = state.cond_index;
+ write!(state, "auto _c{}_ = ", cond_index);
+ show_hir_expr(state, &cond);
+ state.write(";\n");
+ (
+ cond_index,
+ Box::new(hir::Expr {
+ kind: hir::ExprKind::Cond(cond_index, cond),
+ ty: hir::Type::new(hir::TypeKind::Bool),
+ }),
+ )
+ } else {
+ (0, sst.cond.clone())
+ };
+
+ let previous = mem::replace(&mut state.mask, None);
+ state.mask = Some(match previous.clone() {
+ Some(e) => {
+ let cond = Box::new(hir::Expr {
+ kind: hir::ExprKind::Binary(syntax::BinaryOp::BitAnd, e, mask.clone()),
+ ty: hir::Type::new(hir::TypeKind::Bool),
+ });
+ state.cond_index += 1;
+ let nested_cond_index = state.cond_index;
+ show_indent(state);
+ write!(state, "auto _c{}_ = ", nested_cond_index);
+ show_hir_expr(state, &cond);
+ state.write(";\n");
+ Box::new(hir::Expr {
+ kind: hir::ExprKind::Cond(nested_cond_index, cond),
+ ty: hir::Type::new(hir::TypeKind::Bool),
+ })
+ }
+ None => mask.clone(),
+ });
+
+ show_statement(state, &sst.body);
+ state.mask = previous;
+
+ if let Some(rest) = &sst.else_stmt {
+ // invert the condition
+ let inverted_cond = Box::new(hir::Expr {
+ kind: hir::ExprKind::Unary(UnaryOp::Complement, mask),
+ ty: hir::Type::new(hir::TypeKind::Bool),
+ });
+ let previous = mem::replace(&mut state.mask, None);
+ state.mask = Some(match previous.clone() {
+ Some(e) => {
+ let cond = Box::new(hir::Expr {
+ kind: hir::ExprKind::Binary(syntax::BinaryOp::BitAnd, e, inverted_cond),
+ ty: hir::Type::new(hir::TypeKind::Bool),
+ });
+ show_indent(state);
+ write!(state, "_c{}_ = ", cond_index);
+ show_hir_expr(state, &cond);
+ state.write(";\n");
+ Box::new(hir::Expr {
+ kind: hir::ExprKind::Cond(cond_index, cond),
+ ty: hir::Type::new(hir::TypeKind::Bool),
+ })
+ }
+ None => inverted_cond,
+ });
+
+ show_statement(state, rest);
+ state.mask = previous;
+ }
+ } else {
+ state.write("if (");
+ show_hir_expr(state, &sst.cond);
+ state.write(") {\n");
+
+ state.indent();
+ show_statement(state, &sst.body);
+ state.outdent();
+
+ show_indent(state);
+ if let Some(rest) = &sst.else_stmt {
+ state.write("} else ");
+ show_statement(state, rest);
+ } else {
+ state.write("}\n");
+ }
+ }
+}
+
+fn case_stmts_to_if_stmts(stmts: &[Statement], last: bool) -> (Option<Box<Statement>>, bool) {
+ // Look for jump statements and remove them
+ // We currently are pretty strict on the form that the statement
+ // list needs to be in. This can be loosened as needed.
+ let mut fallthrough = false;
+ let cstmt = match &stmts[..] {
+ [hir::Statement::Compound(c)] => match c.statement_list.split_last() {
+ Some((hir::Statement::Simple(s), rest)) => match **s {
+ hir::SimpleStatement::Jump(hir::JumpStatement::Break) => hir::CompoundStatement {
+ statement_list: rest.to_owned(),
+ },
+ _ => panic!("fall through not supported"),
+ },
+ _ => panic!("empty compound"),
+ },
+ [hir::Statement::Simple(s)] => {
+ match **s {
+ hir::SimpleStatement::Jump(hir::JumpStatement::Break) => hir::CompoundStatement {
+ statement_list: Vec::new(),
+ },
+ _ => {
+ if last {
+ // we don't need a break at the end
+ hir::CompoundStatement {
+ statement_list: vec![hir::Statement::Simple(s.clone())],
+ }
+ } else {
+ panic!("fall through not supported {:?}", s)
+ }
+ }
+ }
+ }
+ [] => return (None, true),
+ stmts => match stmts.split_last() {
+ Some((hir::Statement::Simple(s), rest)) => match **s {
+ hir::SimpleStatement::Jump(hir::JumpStatement::Break) => hir::CompoundStatement {
+ statement_list: rest.to_owned(),
+ },
+ _ => {
+ if !last {
+ fallthrough = true;
+ }
+ hir::CompoundStatement {
+ statement_list: stmts.to_owned(),
+ }
+ }
+ },
+ _ => panic!("unexpected empty"),
+ },
+ };
+ let stmts = Box::new(hir::Statement::Compound(Box::new(cstmt)));
+ (Some(stmts), fallthrough)
+}
+
+fn build_selection<'a, I: Iterator<Item = &'a hir::Case>>(
+ head: &Box<hir::Expr>,
+ case: &hir::Case,
+ mut cases: I,
+ default: Option<&hir::Case>,
+ previous_condition: Option<Box<hir::Expr>>,
+ previous_stmts: Option<Box<hir::Statement>>,
+) -> hir::SelectionStatement {
+ let cond = match &case.label {
+ hir::CaseLabel::Case(e) => Some(Box::new(hir::Expr {
+ kind: hir::ExprKind::Binary(syntax::BinaryOp::Equal, head.clone(), e.clone()),
+ ty: hir::Type::new(hir::TypeKind::Bool),
+ })),
+ hir::CaseLabel::Def => None,
+ };
+
+ // if we have two conditions join them
+ let cond = match (&previous_condition, &cond) {
+ (Some(prev), Some(cond)) => Some(Box::new(hir::Expr {
+ kind: hir::ExprKind::Binary(syntax::BinaryOp::Or, prev.clone(), cond.clone()),
+ ty: hir::Type::new(hir::TypeKind::Bool),
+ })),
+ (_, cond) => cond.clone(),
+ };
+
+ /*
+
+ // find the next case that's not a default
+ let next_case = loop {
+ match cases.next() {
+ Some(hir::Case { label: hir::CaseLabel::Def, ..}) => { },
+ case => break case,
+ }
+ };*/
+
+ let (cond, body, else_stmt) = match (cond, cases.next()) {
+ (None, Some(next_case)) => {
+ assert!(previous_stmts.is_none());
+ // default so just move on to the next
+ return build_selection(head, next_case, cases, default, None, None);
+ }
+ (Some(cond), Some(next_case)) => {
+ assert!(previous_stmts.is_none());
+ let (stmts, fallthrough) = case_stmts_to_if_stmts(&case.stmts, false);
+ if !fallthrough && stmts.is_some() {
+ (
+ cond,
+ stmts.unwrap(),
+ Some(Box::new(hir::Statement::Simple(Box::new(
+ hir::SimpleStatement::Selection(build_selection(
+ head, next_case, cases, default, None, None,
+ )),
+ )))),
+ )
+ } else {
+ // empty so fall through to the next
+ return build_selection(head, next_case, cases, default, Some(cond), stmts);
+ }
+ }
+ (Some(cond), None) => {
+ // non-default last
+ assert!(previous_stmts.is_none());
+ let (stmts, _) = case_stmts_to_if_stmts(&case.stmts, default.is_none());
+ let stmts = stmts.expect("empty case labels unsupported at the end");
+ // add the default case at the end if we have one
+ (
+ cond,
+ stmts,
+ match default {
+ Some(default) => {
+ let (default_stmts, fallthrough) =
+ case_stmts_to_if_stmts(&default.stmts, true);
+ assert!(!fallthrough);
+ Some(default_stmts.expect("empty default unsupported"))
+ }
+ None => None,
+ },
+ )
+ }
+ (None, None) => {
+ // default, last
+
+ assert!(default.is_some());
+
+ let (stmts, fallthrough) = case_stmts_to_if_stmts(&case.stmts, true);
+ let stmts = stmts.expect("empty default unsupported");
+ assert!(!fallthrough);
+
+ match previous_stmts {
+ Some(previous_stmts) => {
+ let cond = previous_condition.expect("must have previous condition");
+ (cond, previous_stmts, Some(stmts))
+ }
+ None => {
+ let cond = Box::new(hir::Expr {
+ kind: hir::ExprKind::BoolConst(true),
+ ty: hir::Type::new(hir::TypeKind::Bool),
+ });
+ (cond, stmts, None)
+ }
+ }
+ }
+ };
+
+ hir::SelectionStatement {
+ cond,
+ body,
+ else_stmt,
+ }
+}
+
+pub fn lower_switch_to_ifs(sst: &hir::SwitchStatement) -> hir::SelectionStatement {
+ let default = sst.cases.iter().find(|x| x.label == hir::CaseLabel::Def);
+ let mut cases = sst.cases.iter();
+ let r = build_selection(&sst.head, cases.next().unwrap(), cases, default, None, None);
+ r
+}
+
+fn is_declaration(stmt: &hir::Statement) -> bool {
+ if let hir::Statement::Simple(s) = stmt {
+ if let hir::SimpleStatement::Declaration(..) = **s {
+ return true;
+ }
+ }
+ return false;
+}
+
+pub fn show_switch_statement(state: &mut OutputState, sst: &hir::SwitchStatement) {
+ if state.output_cxx && expr_run_class(state, &sst.head) != hir::RunClass::Scalar {
+ // XXX: when lowering switches we end up with a mask that has
+ // a bunch of mutually exclusive conditions.
+ // It would be nice if we could fold them together.
+ let ifs = lower_switch_to_ifs(sst);
+ return show_selection_statement(state, &ifs);
+ }
+
+ show_indent(state);
+ state.write("switch (");
+ show_hir_expr(state, &sst.head);
+ state.write(") {\n");
+ state.indent();
+
+ for case in &sst.cases {
+ show_case_label(state, &case.label);
+ state.indent();
+
+ let has_declaration = case.stmts.iter().any(|x| is_declaration(x));
+ // glsl allows declarations in switch statements while C requires them to be
+ // in a compound statement. If we have a declaration wrap the statements in an block.
+ // This will break some glsl shaders but keeps the saner ones working
+ if has_declaration {
+ show_indent(state);
+ state.write("{\n");
+ state.indent();
+ }
+ for st in &case.stmts {
+ show_statement(state, st);
+ }
+
+ if has_declaration {
+ show_indent(state);
+ state.write("}\n");
+ state.outdent();
+ }
+
+ state.outdent();
+ }
+ state.outdent();
+ show_indent(state);
+ state.write("}\n");
+}
+
+pub fn show_case_label(state: &mut OutputState, cl: &hir::CaseLabel) {
+ show_indent(state);
+ match *cl {
+ hir::CaseLabel::Case(ref e) => {
+ state.write("case ");
+ show_hir_expr(state, e);
+ state.write(":\n");
+ }
+ hir::CaseLabel::Def => {
+ state.write("default:\n");
+ }
+ }
+}
+
+pub fn show_iteration_statement(state: &mut OutputState, ist: &hir::IterationStatement) {
+ show_indent(state);
+ match *ist {
+ hir::IterationStatement::While(ref cond, ref body) => {
+ state.write("while (");
+ show_condition(state, cond);
+ state.write(") ");
+ show_statement(state, body);
+ }
+ hir::IterationStatement::DoWhile(ref body, ref cond) => {
+ state.write("do ");
+ show_statement(state, body);
+ state.write(" while (");
+ show_hir_expr(state, cond);
+ state.write(");\n");
+ }
+ hir::IterationStatement::For(ref init, ref rest, ref body) => {
+ state.write("for (");
+ show_for_init_statement(state, init);
+ show_for_rest_statement(state, rest);
+ state.write(") ");
+ show_statement(state, body);
+ }
+ }
+}
+
+pub fn show_condition(state: &mut OutputState, c: &hir::Condition) {
+ match *c {
+ hir::Condition::Expr(ref e) => show_hir_expr(state, e),
+ /*hir::Condition::Assignment(ref ty, ref name, ref initializer) => {
+ show_type(state, ty);
+ state.write(" ");
+ show_identifier(f, name);
+ state.write(" = ");
+ show_initializer(state, initializer);
+ }*/
+ }
+}
+
+pub fn show_for_init_statement(state: &mut OutputState, i: &hir::ForInitStatement) {
+ match *i {
+ hir::ForInitStatement::Expression(ref expr) => {
+ if let Some(ref e) = *expr {
+ show_hir_expr(state, e);
+ }
+ }
+ hir::ForInitStatement::Declaration(ref d) => {
+ show_declaration(state, d);
+ }
+ }
+}
+
+pub fn show_for_rest_statement(state: &mut OutputState, r: &hir::ForRestStatement) {
+ if let Some(ref cond) = r.condition {
+ show_condition(state, cond);
+ }
+
+ state.write("; ");
+
+ if let Some(ref e) = r.post_expr {
+ show_hir_expr(state, e);
+ }
+}
+
+fn use_return_mask(state: &OutputState) -> bool {
+ if let Some(mask) = &state.mask {
+ mask.kind != hir::ExprKind::CondMask
+ } else {
+ false
+ }
+}
+
+pub fn show_jump_statement(state: &mut OutputState, j: &hir::JumpStatement) {
+ show_indent(state);
+ match *j {
+ hir::JumpStatement::Continue => {
+ state.write("continue;\n");
+ }
+ hir::JumpStatement::Break => {
+ state.write("break;\n");
+ }
+ hir::JumpStatement::Discard => {
+ if state.output_cxx {
+ state.uses_discard = true;
+ if let Some(mask) = &state.mask {
+ state.write("swgl_IsPixelDiscarded |= (");
+ show_hir_expr(state, mask);
+ state.write(")");
+ if state.return_declared {
+ state.write("&ret_mask");
+ }
+ state.write(";\n");
+ } else {
+ state.write("swgl_IsPixelDiscarded = true;\n");
+ }
+ } else {
+ state.write("discard;\n");
+ }
+ }
+ hir::JumpStatement::Return(ref e) => {
+ if let Some(e) = e {
+ if state.output_cxx {
+ if use_return_mask(state) {
+ // We cast any conditions by `ret_mask_type` so that scalars nicely
+ // convert to -1. i.e. I32 &= bool will give the wrong result. while I32 &= I32(bool) works
+ let ret_mask_type = if state.return_vector {
+ "I32"
+ } else {
+ "int32_t"
+ };
+ if state.return_declared {
+ // XXX: the cloning here is bad
+ write!(state, "ret = if_then_else(ret_mask & {}(", ret_mask_type);
+ show_hir_expr(state, &state.mask.clone().unwrap());
+ state.write("), ");
+ show_hir_expr(state, e);
+ state.write(", ret);\n");
+ } else {
+ state.write("ret = ");
+ show_hir_expr(state, e);
+ state.write(";\n");
+ }
+
+ show_indent(state);
+
+ if state.return_declared {
+ write!(state, "ret_mask &= ~{}(", ret_mask_type);
+ } else {
+ write!(state, "ret_mask = ~{}(", ret_mask_type);
+ }
+ show_hir_expr(state, &state.mask.clone().unwrap());
+ state.write(");\n");
+ state.return_declared = true;
+ } else {
+ if state.return_declared {
+ state.write("ret = if_then_else(ret_mask, ");
+ show_hir_expr(state, e);
+ state.write(", ret);\n");
+ } else {
+ state.write("return ");
+ show_hir_expr(state, e);
+ state.write(";\n");
+ }
+ }
+ } else {
+ state.write("return ");
+ show_hir_expr(state, e);
+ state.write(";\n");
+ }
+ } else {
+ if state.output_cxx {
+ if use_return_mask(state) {
+ show_indent(state);
+ let ret_mask_type = if state.return_vector {
+ "I32"
+ } else {
+ "int32_t"
+ };
+ if state.return_declared {
+ write!(state, "ret_mask &= ~{}(", ret_mask_type);
+ } else {
+ write!(state, "ret_mask = ~{}(", ret_mask_type);
+ }
+ show_hir_expr(state, &state.mask.clone().unwrap());
+ state.write(");\n");
+ state.return_declared = true;
+ } else {
+ state.write("return;\n");
+ }
+ } else {
+ state.write("return;\n");
+ }
+ }
+ }
+ }
+}
+
+pub fn show_path(state: &OutputState, path: &syntax::Path) {
+ match path {
+ syntax::Path::Absolute(s) => {
+ let _ = write!(state, "<{}>", s);
+ }
+ syntax::Path::Relative(s) => {
+ let _ = write!(state, "\"{}\"", s);
+ }
+ }
+}
+
+pub fn show_preprocessor(state: &OutputState, pp: &syntax::Preprocessor) {
+ match *pp {
+ syntax::Preprocessor::Define(ref pd) => show_preprocessor_define(state, pd),
+ syntax::Preprocessor::Else => show_preprocessor_else(state),
+ syntax::Preprocessor::ElseIf(ref pei) => show_preprocessor_elseif(state, pei),
+ syntax::Preprocessor::EndIf => show_preprocessor_endif(state),
+ syntax::Preprocessor::Error(ref pe) => show_preprocessor_error(state, pe),
+ syntax::Preprocessor::If(ref pi) => show_preprocessor_if(state, pi),
+ syntax::Preprocessor::IfDef(ref pid) => show_preprocessor_ifdef(state, pid),
+ syntax::Preprocessor::IfNDef(ref pind) => show_preprocessor_ifndef(state, pind),
+ syntax::Preprocessor::Include(ref pi) => show_preprocessor_include(state, pi),
+ syntax::Preprocessor::Line(ref pl) => show_preprocessor_line(state, pl),
+ syntax::Preprocessor::Pragma(ref pp) => show_preprocessor_pragma(state, pp),
+ syntax::Preprocessor::Undef(ref pu) => show_preprocessor_undef(state, pu),
+ syntax::Preprocessor::Version(ref pv) => show_preprocessor_version(state, pv),
+ syntax::Preprocessor::Extension(ref pe) => show_preprocessor_extension(state, pe),
+ }
+}
+
+pub fn show_preprocessor_define(state: &OutputState, pd: &syntax::PreprocessorDefine) {
+ match *pd {
+ syntax::PreprocessorDefine::ObjectLike {
+ ref ident,
+ ref value,
+ } => {
+ let _ = write!(state, "#define {} {}\n", ident, value);
+ }
+
+ syntax::PreprocessorDefine::FunctionLike {
+ ref ident,
+ ref args,
+ ref value,
+ } => {
+ let _ = write!(state, "#define {}(", ident);
+
+ if !args.is_empty() {
+ let _ = write!(state, "{}", &args[0]);
+
+ for arg in &args[1 .. args.len()] {
+ let _ = write!(state, ", {}", arg);
+ }
+ }
+
+ let _ = write!(state, ") {}\n", value);
+ }
+ }
+}
+
+pub fn show_preprocessor_else(state: &OutputState) {
+ state.write("#else\n");
+}
+
+pub fn show_preprocessor_elseif(state: &OutputState, pei: &syntax::PreprocessorElseIf) {
+ let _ = write!(state, "#elseif {}\n", pei.condition);
+}
+
+pub fn show_preprocessor_error(state: &OutputState, pe: &syntax::PreprocessorError) {
+ let _ = writeln!(state, "#error {}", pe.message);
+}
+
+pub fn show_preprocessor_endif(state: &OutputState) {
+ state.write("#endif\n");
+}
+
+pub fn show_preprocessor_if(state: &OutputState, pi: &syntax::PreprocessorIf) {
+ let _ = write!(state, "#if {}\n", pi.condition);
+}
+
+pub fn show_preprocessor_ifdef(state: &OutputState, pid: &syntax::PreprocessorIfDef) {
+ state.write("#ifdef ");
+ show_identifier(state, &pid.ident);
+ state.write("\n");
+}
+
+pub fn show_preprocessor_ifndef(state: &OutputState, pind: &syntax::PreprocessorIfNDef) {
+ state.write("#ifndef ");
+ show_identifier(state, &pind.ident);
+ state.write("\n");
+}
+
+pub fn show_preprocessor_include(state: &OutputState, pi: &syntax::PreprocessorInclude) {
+ state.write("#include ");
+ show_path(state, &pi.path);
+ state.write("\n");
+}
+
+pub fn show_preprocessor_line(state: &OutputState, pl: &syntax::PreprocessorLine) {
+ let _ = write!(state, "#line {}", pl.line);
+ if let Some(source_string_number) = pl.source_string_number {
+ let _ = write!(state, " {}", source_string_number);
+ }
+ state.write("\n");
+}
+
+pub fn show_preprocessor_pragma(state: &OutputState, pp: &syntax::PreprocessorPragma) {
+ let _ = writeln!(state, "#pragma {}", pp.command);
+}
+
+pub fn show_preprocessor_undef(state: &OutputState, pud: &syntax::PreprocessorUndef) {
+ state.write("#undef ");
+ show_identifier(state, &pud.name);
+ state.write("\n");
+}
+
+pub fn show_preprocessor_version(state: &OutputState, pv: &syntax::PreprocessorVersion) {
+ let _ = write!(state, "#version {}", pv.version);
+
+ if let Some(ref profile) = pv.profile {
+ match *profile {
+ syntax::PreprocessorVersionProfile::Core => {
+ state.write(" core");
+ }
+ syntax::PreprocessorVersionProfile::Compatibility => {
+ state.write(" compatibility");
+ }
+ syntax::PreprocessorVersionProfile::ES => {
+ state.write(" es");
+ }
+ }
+ }
+
+ state.write("\n");
+}
+
+pub fn show_preprocessor_extension(state: &OutputState, pe: &syntax::PreprocessorExtension) {
+ state.write("#extension ");
+
+ match pe.name {
+ syntax::PreprocessorExtensionName::All => {
+ state.write("all");
+ }
+ syntax::PreprocessorExtensionName::Specific(ref n) => {
+ state.write(n);
+ }
+ }
+
+ if let Some(ref behavior) = pe.behavior {
+ match *behavior {
+ syntax::PreprocessorExtensionBehavior::Require => {
+ state.write(" : require");
+ }
+ syntax::PreprocessorExtensionBehavior::Enable => {
+ state.write(" : enable");
+ }
+ syntax::PreprocessorExtensionBehavior::Warn => {
+ state.write(" : warn");
+ }
+ syntax::PreprocessorExtensionBehavior::Disable => {
+ state.write(" : disable");
+ }
+ }
+ }
+
+ state.write("\n");
+}
+
+pub fn show_external_declaration(state: &mut OutputState, ed: &hir::ExternalDeclaration) {
+ match *ed {
+ hir::ExternalDeclaration::Preprocessor(ref pp) => {
+ if !state.output_cxx {
+ show_preprocessor(state, pp)
+ }
+ }
+ hir::ExternalDeclaration::FunctionDefinition(ref fd) => {
+ if !state.output_cxx {
+ show_function_definition(state, fd, !0)
+ }
+ }
+ hir::ExternalDeclaration::Declaration(ref d) => show_declaration(state, d),
+ }
+}
+
+pub fn show_cxx_function_definition(state: &mut OutputState, name: hir::SymRef, vector_mask: u32) {
+ if let Some((ref fd, run_class)) = state.hir.function_definition(name) {
+ state.vector_mask = vector_mask;
+ state.return_vector = (vector_mask & (1 << 31)) != 0
+ || match run_class {
+ hir::RunClass::Scalar => false,
+ hir::RunClass::Dependent(mask) => (mask & vector_mask) != 0,
+ _ => true,
+ };
+ match state.functions.get(&(name, vector_mask)) {
+ Some(true) => {}
+ Some(false) => {
+ show_function_prototype(state, &fd.prototype);
+ state.functions.insert((name, vector_mask), true);
+ }
+ None => {
+ state.functions.insert((name, vector_mask), false);
+ let buffer = state.push_buffer();
+ show_function_definition(state, fd, vector_mask);
+ for (name, vector_mask) in state.deps.replace(Vec::new()) {
+ show_cxx_function_definition(state, name, vector_mask);
+ }
+ state.flush_buffer();
+ state.pop_buffer(buffer);
+ state.functions.insert((name, vector_mask), true);
+ }
+ }
+ }
+}
+
+pub fn show_translation_unit(state: &mut OutputState, tu: &hir::TranslationUnit) {
+ state.flush_buffer();
+
+ for ed in &(tu.0).0 {
+ show_external_declaration(state, ed);
+ state.flush_buffer();
+ }
+ if state.output_cxx {
+ for name in &["main", "swgl_drawSpanRGBA8", "swgl_drawSpanR8"] {
+ if let Some(sym) = state.hir.lookup(name) {
+ show_cxx_function_definition(state, sym, 0);
+ state.flush_buffer();
+ }
+ }
+ }
+}
+
+fn write_abi(state: &mut OutputState) {
+ match state.kind {
+ ShaderKind::Fragment => {
+ state.write("static void run(FragmentShaderImpl* impl) {\n");
+ state.write(" Self* self = (Self*)impl;\n");
+ if state.uses_discard {
+ state.write(" self->swgl_IsPixelDiscarded = false;\n");
+ }
+ state.write(" self->main();\n");
+ state.write(" self->step_interp_inputs();\n");
+ state.write("}\n");
+ state.write("static void skip(FragmentShaderImpl* impl, int steps) {\n");
+ state.write(" Self* self = (Self*)impl;\n");
+ state.write(" self->step_interp_inputs(steps);\n");
+ state.write("}\n");
+ if state.use_perspective {
+ state.write("static void run_perspective(FragmentShaderImpl* impl) {\n");
+ state.write(" Self* self = (Self*)impl;\n");
+ if state.uses_discard {
+ state.write(" self->swgl_IsPixelDiscarded = false;\n");
+ }
+ state.write(" self->main();\n");
+ state.write(" self->step_perspective_inputs();\n");
+ state.write("}\n");
+ state.write("static void skip_perspective(FragmentShaderImpl* impl, int steps) {\n");
+ state.write(" Self* self = (Self*)impl;\n");
+ state.write(" self->step_perspective_inputs(steps);\n");
+ state.write("}\n");
+ }
+ if state.hir.lookup("swgl_drawSpanRGBA8").is_some() {
+ state.write("static int draw_span_RGBA8(FragmentShaderImpl* impl) {\n");
+ state.write(" Self* self = (Self*)impl; DISPATCH_DRAW_SPAN(self, RGBA8); }\n");
+ }
+ if state.hir.lookup("swgl_drawSpanR8").is_some() {
+ state.write("static int draw_span_R8(FragmentShaderImpl* impl) {\n");
+ state.write(" Self* self = (Self*)impl; DISPATCH_DRAW_SPAN(self, R8); }\n");
+ }
+
+ write!(state, "public:\n{}_frag() {{\n", state.name);
+ }
+ ShaderKind::Vertex => {
+ state.write("static void run(VertexShaderImpl* impl, char* interps, size_t interp_stride) {\n");
+ state.write(" Self* self = (Self*)impl;\n");
+ state.write(" self->main();\n");
+ state.write(" self->store_interp_outputs(interps, interp_stride);\n");
+ state.write("}\n");
+ state.write("static void init_batch(VertexShaderImpl* impl) {\n");
+ state.write(" Self* self = (Self*)impl; self->bind_textures(); }\n");
+
+ write!(state, "public:\n{}_vert() {{\n", state.name);
+ }
+ }
+ match state.kind {
+ ShaderKind::Fragment => {
+ state.write(" init_span_func = &read_interp_inputs;\n");
+ state.write(" run_func = &run;\n");
+ state.write(" skip_func = &skip;\n");
+ if state.hir.lookup("swgl_drawSpanRGBA8").is_some() {
+ state.write(" draw_span_RGBA8_func = &draw_span_RGBA8;\n");
+ }
+ if state.hir.lookup("swgl_drawSpanR8").is_some() {
+ state.write(" draw_span_R8_func = &draw_span_R8;\n");
+ }
+ if state.uses_discard {
+ state.write(" enable_discard();\n");
+ }
+ if state.use_perspective {
+ state.write(" enable_perspective();\n");
+ state.write(" init_span_w_func = &read_perspective_inputs;\n");
+ state.write(" run_w_func = &run_perspective;\n");
+ state.write(" skip_w_func = &skip_perspective;\n");
+ } else {
+ state.write(" init_span_w_func = &read_interp_inputs;\n");
+ state.write(" run_w_func = &run;\n");
+ state.write(" skip_w_func = &skip;\n");
+ }
+ }
+ ShaderKind::Vertex => {
+ state.write(" set_uniform_1i_func = &set_uniform_1i;\n");
+ state.write(" set_uniform_4fv_func = &set_uniform_4fv;\n");
+ state.write(" set_uniform_matrix4fv_func = &set_uniform_matrix4fv;\n");
+ state.write(" init_batch_func = &init_batch;\n");
+ state.write(" load_attribs_func = &load_attribs;\n");
+ state.write(" run_primitive_func = &run;\n");
+ if state.hir.used_clip_dist != 0 {
+ state.write(" enable_clip_distance();\n");
+ }
+ }
+ }
+ state.write("}\n");
+}
+
+pub fn define_global_consts(state: &mut OutputState, tu: &hir::TranslationUnit, part_name: &str) {
+ for i in tu {
+ match i {
+ hir::ExternalDeclaration::Declaration(hir::Declaration::InitDeclaratorList(ref d)) => {
+ let sym = state.hir.sym(d.head.name);
+ match &sym.decl {
+ hir::SymDecl::Global(hir::StorageClass::Const, ..) => {
+ let is_scalar = state.is_scalar.replace(
+ symbol_run_class(&sym.decl, state.vector_mask) == hir::RunClass::Scalar,
+ );
+ if let Some(ref _array) = d.head.ty.array_sizes {
+ show_type(state, &d.head.ty);
+ } else {
+ if let Some(ty_def) = d.head.ty_def {
+ show_sym_decl(state, &ty_def);
+ } else {
+ show_type(state, &d.head.ty);
+ }
+ }
+ write!(state, " constexpr {}::{};\n", part_name, sym.name);
+ state.is_scalar.set(is_scalar);
+ }
+ _ => {}
+ }
+ }
+ _ => {}
+ }
+ }
+}
diff --git a/gfx/wr/glsl-to-cxx/src/main.rs b/gfx/wr/glsl-to-cxx/src/main.rs
new file mode 100644
index 0000000000..e40262c84e
--- /dev/null
+++ b/gfx/wr/glsl-to-cxx/src/main.rs
@@ -0,0 +1,8 @@
+/* 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 glsl_to_cxx::translate;
+fn main() {
+ println!("{}", translate(&mut std::env::args()));
+}
diff --git a/gfx/wr/peek-poke/Cargo.toml b/gfx/wr/peek-poke/Cargo.toml
new file mode 100644
index 0000000000..a2dc99065b
--- /dev/null
+++ b/gfx/wr/peek-poke/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "peek-poke"
+version = "0.3.0"
+authors = ["Dan Glastonbury <dan.glastonbury@gmail.com>"]
+repository = "https://github.com/servo/webrender"
+description = "A mechanism for serializing and deserializing data into/from byte buffers, for use in WebRender."
+license = "MIT/Apache-2.0"
+edition = "2018"
+
+[dependencies]
+euclid = { version = "0.22.0", optional = true }
+peek-poke-derive = { version = "0.3", path = "./peek-poke-derive", optional = true }
+
+[features]
+default = ["derive"]
+derive = ["peek-poke-derive"]
+extras = ["derive", "euclid"]
diff --git a/gfx/wr/peek-poke/LICENSE-APACHE b/gfx/wr/peek-poke/LICENSE-APACHE
new file mode 100644
index 0000000000..16fe87b06e
--- /dev/null
+++ b/gfx/wr/peek-poke/LICENSE-APACHE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/gfx/wr/peek-poke/LICENSE-MIT b/gfx/wr/peek-poke/LICENSE-MIT
new file mode 100644
index 0000000000..110bb34712
--- /dev/null
+++ b/gfx/wr/peek-poke/LICENSE-MIT
@@ -0,0 +1,44 @@
+MIT License
+
+Copyright (c) 2019 Daniel Glastonbury
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+This work incorporates work covered by the following copyright and permission
+notice:
+
+Copyright (c) 2019 Devashish Dixit
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/gfx/wr/peek-poke/README.md b/gfx/wr/peek-poke/README.md
new file mode 100644
index 0000000000..1c379aa121
--- /dev/null
+++ b/gfx/wr/peek-poke/README.md
@@ -0,0 +1,54 @@
+# Peeks, Pokes, and Pointers
+
+Peek from and poke structures into byte slices.
+
+## Benchmark
+
+Below are the benchmark results of comparison between `peek-poke` and `bincode` serializing and deserializing same `struct`:
+```
+struct MyPeekPokeStruct {
+ a: u8,
+ b: u16,
+ c: MyPeekPokeEnum,
+ d: Option<usize>,
+}
+
+enum MyPeekPokeEnum {
+ Variant1,
+ Variant2(u16),
+}
+```
+
+```
+Benchmarking struct::serialize/peek_poke::poke_into: Collecting 100 samples in struct::serialize/peek_poke::poke_into
+ time: [2.7267 ns 2.7321 ns 2.7380 ns]
+
+Benchmarking struct::serialize/bincode::serialize: Collecting 100 samples in est struct::serialize/bincode::serialize
+ time: [31.264 ns 31.326 ns 31.389 ns]
+
+Benchmarking struct::deserialize/peek_poke::peek_from: Collecting 100 samples struct::deserialize/peek_poke::peek_from
+ time: [5.3544 ns 5.3672 ns 5.3817 ns]
+
+Benchmarking struct::deserialize/bincode::deserialize: Collecting 100 samples in struct::deserialize/bincode::deserialize
+ time: [25.155 ns 26.439 ns 27.661 ns]
+```
+
+You can run benchmarks by running following command:
+```
+cargo bench
+```
+
+## License
+[license]: #license
+
+Licensed under either of
+- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+- MIT license (http://opensource.org/licenses/MIT)
+
+at your option.
+
+see [LICENSE-APACHE](LICENSE-APACHE), [LICENSE-MIT](LICENSE-MIT) for details.
+
+## Contribution
+Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as
+defined in the Apache-2.0 license, shall be dual licensed as about, without any additional terms or conditions.
diff --git a/gfx/wr/peek-poke/peek-poke-derive/Cargo.toml b/gfx/wr/peek-poke/peek-poke-derive/Cargo.toml
new file mode 100644
index 0000000000..491e0d95ae
--- /dev/null
+++ b/gfx/wr/peek-poke/peek-poke-derive/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "peek-poke-derive"
+version = "0.3.0"
+authors = ["Dan Glastonbury <dan.glastonbury@gmail.com>"]
+repository = "https://github.com/servo/webrender"
+description = "Derive macro for peek-poke."
+license = "MIT/Apache-2.0"
+edition = "2018"
+
+[lib]
+doctest = false
+proc-macro = true
+
+[dependencies]
+proc-macro2 = "1"
+quote = "1"
+syn = "1"
+synstructure = "0.12"
+unicode-xid = "0.2"
diff --git a/gfx/wr/peek-poke/peek-poke-derive/LICENSE-APACHE b/gfx/wr/peek-poke/peek-poke-derive/LICENSE-APACHE
new file mode 100644
index 0000000000..16fe87b06e
--- /dev/null
+++ b/gfx/wr/peek-poke/peek-poke-derive/LICENSE-APACHE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/gfx/wr/peek-poke/peek-poke-derive/LICENSE-MIT b/gfx/wr/peek-poke/peek-poke-derive/LICENSE-MIT
new file mode 100644
index 0000000000..110bb34712
--- /dev/null
+++ b/gfx/wr/peek-poke/peek-poke-derive/LICENSE-MIT
@@ -0,0 +1,44 @@
+MIT License
+
+Copyright (c) 2019 Daniel Glastonbury
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+This work incorporates work covered by the following copyright and permission
+notice:
+
+Copyright (c) 2019 Devashish Dixit
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/gfx/wr/peek-poke/peek-poke-derive/README.md b/gfx/wr/peek-poke/peek-poke-derive/README.md
new file mode 100644
index 0000000000..1c379aa121
--- /dev/null
+++ b/gfx/wr/peek-poke/peek-poke-derive/README.md
@@ -0,0 +1,54 @@
+# Peeks, Pokes, and Pointers
+
+Peek from and poke structures into byte slices.
+
+## Benchmark
+
+Below are the benchmark results of comparison between `peek-poke` and `bincode` serializing and deserializing same `struct`:
+```
+struct MyPeekPokeStruct {
+ a: u8,
+ b: u16,
+ c: MyPeekPokeEnum,
+ d: Option<usize>,
+}
+
+enum MyPeekPokeEnum {
+ Variant1,
+ Variant2(u16),
+}
+```
+
+```
+Benchmarking struct::serialize/peek_poke::poke_into: Collecting 100 samples in struct::serialize/peek_poke::poke_into
+ time: [2.7267 ns 2.7321 ns 2.7380 ns]
+
+Benchmarking struct::serialize/bincode::serialize: Collecting 100 samples in est struct::serialize/bincode::serialize
+ time: [31.264 ns 31.326 ns 31.389 ns]
+
+Benchmarking struct::deserialize/peek_poke::peek_from: Collecting 100 samples struct::deserialize/peek_poke::peek_from
+ time: [5.3544 ns 5.3672 ns 5.3817 ns]
+
+Benchmarking struct::deserialize/bincode::deserialize: Collecting 100 samples in struct::deserialize/bincode::deserialize
+ time: [25.155 ns 26.439 ns 27.661 ns]
+```
+
+You can run benchmarks by running following command:
+```
+cargo bench
+```
+
+## License
+[license]: #license
+
+Licensed under either of
+- Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+- MIT license (http://opensource.org/licenses/MIT)
+
+at your option.
+
+see [LICENSE-APACHE](LICENSE-APACHE), [LICENSE-MIT](LICENSE-MIT) for details.
+
+## Contribution
+Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as
+defined in the Apache-2.0 license, shall be dual licensed as about, without any additional terms or conditions.
diff --git a/gfx/wr/peek-poke/peek-poke-derive/src/lib.rs b/gfx/wr/peek-poke/peek-poke-derive/src/lib.rs
new file mode 100644
index 0000000000..ac20b2b922
--- /dev/null
+++ b/gfx/wr/peek-poke/peek-poke-derive/src/lib.rs
@@ -0,0 +1,263 @@
+// Copyright 2019 The Servo Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+use proc_macro2::{Span, TokenStream};
+use quote::quote;
+use syn::{Ident, Index, TraitBound};
+use synstructure::{decl_derive, Structure, BindStyle, AddBounds};
+use unicode_xid::UnicodeXID;
+
+// Internal method for sanitizing an identifier for hygiene purposes.
+fn sanitize_ident(s: &str) -> Ident {
+ let mut res = String::with_capacity(s.len());
+ for mut c in s.chars() {
+ if !UnicodeXID::is_xid_continue(c) {
+ c = '_'
+ }
+ // Deduplicate consecutive _ characters.
+ if res.ends_with('_') && c == '_' {
+ continue;
+ }
+ res.push(c);
+ }
+ Ident::new(&res, Span::call_site())
+}
+
+/// Calculates size type for number of variants (used for enums)
+fn get_discriminant_size_type(len: usize) -> TokenStream {
+ if len <= <u8>::max_value() as usize {
+ quote! { u8 }
+ } else if len <= <u16>::max_value() as usize {
+ quote! { u16 }
+ } else {
+ quote! { u32 }
+ }
+}
+
+fn is_struct(s: &Structure) -> bool {
+ // a single variant with no prefix is 'struct'
+ matches!(&s.variants()[..], [v] if v.prefix.is_none())
+}
+
+fn derive_max_size(s: &Structure) -> TokenStream {
+ let max_size = s.variants().iter().fold(quote!(0), |acc, vi| {
+ let variant_size = vi.bindings().iter().fold(quote!(0), |acc, bi| {
+ // compute size of each variant by summing the sizes of its bindings
+ let ty = &bi.ast().ty;
+ quote!(#acc + <#ty>::max_size())
+ });
+
+ // find the maximum of each variant
+ quote! {
+ max(#acc, #variant_size)
+ }
+ });
+
+ let body = if is_struct(s) {
+ max_size
+ } else {
+ let discriminant_size_type = get_discriminant_size_type(s.variants().len());
+ quote! {
+ #discriminant_size_type ::max_size() + #max_size
+ }
+ };
+
+ quote! {
+ #[inline(always)]
+ fn max_size() -> usize {
+ use std::cmp::max;
+ #body
+ }
+ }
+}
+
+fn derive_peek_from_for_enum(s: &mut Structure) -> TokenStream {
+ assert!(!is_struct(s));
+ s.bind_with(|_| BindStyle::Move);
+
+ let num_variants = s.variants().len();
+ let discriminant_size_type = get_discriminant_size_type(num_variants);
+ let body = s
+ .variants()
+ .iter()
+ .enumerate()
+ .fold(quote!(), |acc, (i, vi)| {
+ let bindings = vi
+ .bindings()
+ .iter()
+ .map(|bi| quote!(#bi))
+ .collect::<Vec<_>>();
+
+ let variant_pat = Index::from(i);
+ let poke_exprs = bindings.iter().fold(quote!(), |acc, bi| {
+ quote! {
+ #acc
+ let (#bi, bytes) = peek_poke::peek_from_default(bytes);
+ }
+ });
+ let construct = vi.construct(|_, i| {
+ let bi = &bindings[i];
+ quote!(#bi)
+ });
+
+ quote! {
+ #acc
+ #variant_pat => {
+ #poke_exprs
+ *output = #construct;
+ bytes
+ }
+ }
+ });
+
+ let type_name = s.ast().ident.to_string();
+ let max_tag_value = num_variants - 1;
+
+ quote! {
+ #[inline(always)]
+ unsafe fn peek_from(bytes: *const u8, output: *mut Self) -> *const u8 {
+ let (variant, bytes) = peek_poke::peek_from_default::<#discriminant_size_type>(bytes);
+ match variant {
+ #body
+ out_of_range_tag => {
+ panic!("WRDL: memory corruption detected while parsing {} - enum tag should be <= {}, but was {}",
+ #type_name, #max_tag_value, out_of_range_tag);
+ }
+ }
+ }
+ }
+}
+
+fn derive_peek_from_for_struct(s: &mut Structure) -> TokenStream {
+ assert!(is_struct(&s));
+
+ s.variants_mut()[0].bind_with(|_| BindStyle::RefMut);
+ let pat = s.variants()[0].pat();
+ let peek_exprs = s.variants()[0].bindings().iter().fold(quote!(), |acc, bi| {
+ let ty = &bi.ast().ty;
+ quote! {
+ #acc
+ let bytes = <#ty>::peek_from(bytes, #bi);
+ }
+ });
+
+ let body = quote! {
+ #pat => {
+ #peek_exprs
+ bytes
+ }
+ };
+
+ quote! {
+ #[inline(always)]
+ unsafe fn peek_from(bytes: *const u8, output: *mut Self) -> *const u8 {
+ match &mut (*output) {
+ #body
+ }
+ }
+ }
+}
+
+fn derive_poke_into(s: &Structure) -> TokenStream {
+ let is_struct = is_struct(&s);
+ let discriminant_size_type = get_discriminant_size_type(s.variants().len());
+ let body = s
+ .variants()
+ .iter()
+ .enumerate()
+ .fold(quote!(), |acc, (i, vi)| {
+ let init = if !is_struct {
+ let index = Index::from(i);
+ quote! {
+ let bytes = #discriminant_size_type::poke_into(&#index, bytes);
+ }
+ } else {
+ quote!()
+ };
+ let variant_pat = vi.pat();
+ let poke_exprs = vi.bindings().iter().fold(init, |acc, bi| {
+ quote! {
+ #acc
+ let bytes = #bi.poke_into(bytes);
+ }
+ });
+
+ quote! {
+ #acc
+ #variant_pat => {
+ #poke_exprs
+ bytes
+ }
+ }
+ });
+
+ quote! {
+ #[inline(always)]
+ unsafe fn poke_into(&self, bytes: *mut u8) -> *mut u8 {
+ match &*self {
+ #body
+ }
+ }
+ }
+}
+
+fn peek_poke_derive(mut s: Structure) -> TokenStream {
+ s.binding_name(|_, i| Ident::new(&format!("__self_{}", i), Span::call_site()));
+
+ let max_size_fn = derive_max_size(&s);
+ let poke_into_fn = derive_poke_into(&s);
+ let peek_from_fn = if is_struct(&s) {
+ derive_peek_from_for_struct(&mut s)
+ } else {
+ derive_peek_from_for_enum(&mut s)
+ };
+
+ let poke_impl = s.gen_impl(quote! {
+ extern crate peek_poke;
+
+ gen unsafe impl peek_poke::Poke for @Self {
+ #max_size_fn
+ #poke_into_fn
+ }
+ });
+
+ // To implement `fn peek_from` we require that types implement `Default`
+ // trait to create temporary values. This code does the addition all
+ // manually until https://github.com/mystor/synstructure/issues/24 is fixed.
+ let default_trait = syn::parse_str::<TraitBound>("::std::default::Default").unwrap();
+ let peek_trait = syn::parse_str::<TraitBound>("peek_poke::Peek").unwrap();
+
+ let ast = s.ast();
+ let name = &ast.ident;
+ let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
+ let mut where_clause = where_clause.cloned();
+ s.add_trait_bounds(&default_trait, &mut where_clause, AddBounds::Generics);
+ s.add_trait_bounds(&peek_trait, &mut where_clause, AddBounds::Generics);
+
+ let dummy_const: Ident = sanitize_ident(&format!("_DERIVE_peek_poke_Peek_FOR_{}", name));
+
+ let peek_impl = quote! {
+ #[allow(non_upper_case_globals)]
+ const #dummy_const: () = {
+ extern crate peek_poke;
+
+ impl #impl_generics peek_poke::Peek for #name #ty_generics #where_clause {
+ #peek_from_fn
+ }
+ };
+ };
+
+ quote! {
+ #poke_impl
+ #peek_impl
+ }
+}
+
+decl_derive!([PeekPoke] => peek_poke_derive);
diff --git a/gfx/wr/peek-poke/src/euclid.rs b/gfx/wr/peek-poke/src/euclid.rs
new file mode 100644
index 0000000000..dc4dd7774b
--- /dev/null
+++ b/gfx/wr/peek-poke/src/euclid.rs
@@ -0,0 +1,191 @@
+// Copyright 2019 The Servo Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+use crate::{Peek, Poke};
+use euclid::{Point2D, Rect, Box2D, SideOffsets2D, Size2D, Transform3D, Vector2D};
+
+unsafe impl<T: Poke, U> Poke for Point2D<T, U> {
+ #[inline(always)]
+ fn max_size() -> usize {
+ 2 * T::max_size()
+ }
+ #[inline(always)]
+ unsafe fn poke_into(&self, bytes: *mut u8) -> *mut u8 {
+ let bytes = self.x.poke_into(bytes);
+ let bytes = self.y.poke_into(bytes);
+ bytes
+ }
+}
+impl<T: Peek, U> Peek for Point2D<T, U> {
+ #[inline(always)]
+ unsafe fn peek_from(bytes: *const u8, output: *mut Self) -> *const u8 {
+ let bytes = T::peek_from(bytes, &mut (*output).x);
+ let bytes = T::peek_from(bytes, &mut (*output).y);
+ bytes
+ }
+}
+
+unsafe impl<T: Poke, U> Poke for Rect<T, U> {
+ #[inline(always)]
+ fn max_size() -> usize {
+ Point2D::<T, U>::max_size() + Size2D::<T, U>::max_size()
+ }
+ #[inline(always)]
+ unsafe fn poke_into(&self, bytes: *mut u8) -> *mut u8 {
+ let bytes = self.origin.poke_into(bytes);
+ let bytes = self.size.poke_into(bytes);
+ bytes
+ }
+}
+impl<T: Peek, U> Peek for Rect<T, U> {
+ #[inline(always)]
+ unsafe fn peek_from(bytes: *const u8, output: *mut Self) -> *const u8 {
+ let bytes = Point2D::<T, U>::peek_from(bytes, &mut (*output).origin);
+ let bytes = Size2D::<T, U>::peek_from(bytes, &mut (*output).size);
+ bytes
+ }
+}
+
+unsafe impl<T: Poke, U> Poke for Box2D<T, U> {
+ #[inline(always)]
+ fn max_size() -> usize {
+ Point2D::<T, U>::max_size() * 2
+ }
+ #[inline(always)]
+ unsafe fn poke_into(&self, bytes: *mut u8) -> *mut u8 {
+ let bytes = self.min.poke_into(bytes);
+ let bytes = self.max.poke_into(bytes);
+ bytes
+ }
+}
+impl<T: Peek, U> Peek for Box2D<T, U> {
+ #[inline(always)]
+ unsafe fn peek_from(bytes: *const u8, output: *mut Self) -> *const u8 {
+ let bytes = Point2D::<T, U>::peek_from(bytes, &mut (*output).min);
+ let bytes = Point2D::<T, U>::peek_from(bytes, &mut (*output).max);
+ bytes
+ }
+}
+
+unsafe impl<T: Poke, U> Poke for SideOffsets2D<T, U> {
+ #[inline(always)]
+ fn max_size() -> usize {
+ 4 * T::max_size()
+ }
+ #[inline(always)]
+ unsafe fn poke_into(&self, bytes: *mut u8) -> *mut u8 {
+ let bytes = self.top.poke_into(bytes);
+ let bytes = self.right.poke_into(bytes);
+ let bytes = self.bottom.poke_into(bytes);
+ let bytes = self.left.poke_into(bytes);
+ bytes
+ }
+}
+impl<T: Peek, U> Peek for SideOffsets2D<T, U> {
+ #[inline(always)]
+ unsafe fn peek_from(bytes: *const u8, output: *mut Self) -> *const u8 {
+ let bytes = T::peek_from(bytes, &mut (*output).top);
+ let bytes = T::peek_from(bytes, &mut (*output).right);
+ let bytes = T::peek_from(bytes, &mut (*output).bottom);
+ let bytes = T::peek_from(bytes, &mut (*output).left);
+ bytes
+ }
+}
+
+unsafe impl<T: Poke, U> Poke for Size2D<T, U> {
+ #[inline(always)]
+ fn max_size() -> usize {
+ 2 * T::max_size()
+ }
+ #[inline(always)]
+ unsafe fn poke_into(&self, bytes: *mut u8) -> *mut u8 {
+ let bytes = self.width.poke_into(bytes);
+ let bytes = self.height.poke_into(bytes);
+ bytes
+ }
+}
+impl<T: Peek, U> Peek for Size2D<T, U> {
+ #[inline(always)]
+ unsafe fn peek_from(bytes: *const u8, output: *mut Self) -> *const u8 {
+ let bytes = T::peek_from(bytes, &mut (*output).width);
+ let bytes = T::peek_from(bytes, &mut (*output).height);
+ bytes
+ }
+}
+
+unsafe impl<T: Poke, S, D> Poke for Transform3D<T, S, D> {
+ #[inline(always)]
+ fn max_size() -> usize {
+ 16 * T::max_size()
+ }
+ #[inline(always)]
+ unsafe fn poke_into(&self, bytes: *mut u8) -> *mut u8 {
+ let bytes = self.m11.poke_into(bytes);
+ let bytes = self.m12.poke_into(bytes);
+ let bytes = self.m13.poke_into(bytes);
+ let bytes = self.m14.poke_into(bytes);
+ let bytes = self.m21.poke_into(bytes);
+ let bytes = self.m22.poke_into(bytes);
+ let bytes = self.m23.poke_into(bytes);
+ let bytes = self.m24.poke_into(bytes);
+ let bytes = self.m31.poke_into(bytes);
+ let bytes = self.m32.poke_into(bytes);
+ let bytes = self.m33.poke_into(bytes);
+ let bytes = self.m34.poke_into(bytes);
+ let bytes = self.m41.poke_into(bytes);
+ let bytes = self.m42.poke_into(bytes);
+ let bytes = self.m43.poke_into(bytes);
+ let bytes = self.m44.poke_into(bytes);
+ bytes
+ }
+}
+impl<T: Peek, S, D> Peek for Transform3D<T, S, D> {
+ #[inline(always)]
+ unsafe fn peek_from(bytes: *const u8, output: *mut Self) -> *const u8 {
+ let bytes = T::peek_from(bytes, &mut (*output).m11);
+ let bytes = T::peek_from(bytes, &mut (*output).m12);
+ let bytes = T::peek_from(bytes, &mut (*output).m13);
+ let bytes = T::peek_from(bytes, &mut (*output).m14);
+ let bytes = T::peek_from(bytes, &mut (*output).m21);
+ let bytes = T::peek_from(bytes, &mut (*output).m22);
+ let bytes = T::peek_from(bytes, &mut (*output).m23);
+ let bytes = T::peek_from(bytes, &mut (*output).m24);
+ let bytes = T::peek_from(bytes, &mut (*output).m31);
+ let bytes = T::peek_from(bytes, &mut (*output).m32);
+ let bytes = T::peek_from(bytes, &mut (*output).m33);
+ let bytes = T::peek_from(bytes, &mut (*output).m34);
+ let bytes = T::peek_from(bytes, &mut (*output).m41);
+ let bytes = T::peek_from(bytes, &mut (*output).m42);
+ let bytes = T::peek_from(bytes, &mut (*output).m43);
+ let bytes = T::peek_from(bytes, &mut (*output).m44);
+ bytes
+ }
+}
+
+unsafe impl<T: Poke, U> Poke for Vector2D<T, U> {
+ #[inline(always)]
+ fn max_size() -> usize {
+ 2 * T::max_size()
+ }
+ #[inline(always)]
+ unsafe fn poke_into(&self, bytes: *mut u8) -> *mut u8 {
+ let bytes = self.x.poke_into(bytes);
+ let bytes = self.y.poke_into(bytes);
+ bytes
+ }
+}
+impl<T: Peek, U> Peek for Vector2D<T, U> {
+ #[inline(always)]
+ unsafe fn peek_from(bytes: *const u8, output: *mut Self) -> *const u8 {
+ let bytes = T::peek_from(bytes, &mut (*output).x);
+ let bytes = T::peek_from(bytes, &mut (*output).y);
+ bytes
+ }
+}
diff --git a/gfx/wr/peek-poke/src/lib.rs b/gfx/wr/peek-poke/src/lib.rs
new file mode 100644
index 0000000000..18f6a9acd8
--- /dev/null
+++ b/gfx/wr/peek-poke/src/lib.rs
@@ -0,0 +1,440 @@
+// Copyright 2019 The Servo Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+//! Fast binary serialization and deserialization for types with a known maximum size.
+//!
+//! ## Binary Encoding Scheme
+//!
+//! ## Usage
+//!
+//! ## Comparison to bincode
+
+#[cfg(feature = "derive")]
+pub use peek_poke_derive::*;
+
+use core::{marker::PhantomData, mem::size_of, slice};
+use crate::{slice_ext::*, vec_ext::*};
+
+mod slice_ext;
+mod vec_ext;
+
+union MaybeUninitShim<T: Copy> {
+ uninit: (),
+ init: T,
+}
+
+/// Peek helper for constructing a `T` by `Copy`ing into an uninitialized stack
+/// allocation.
+pub unsafe fn peek_from_uninit<T: Copy + Peek>(bytes: *const u8) -> (T, *const u8) {
+ let mut val = MaybeUninitShim { uninit: () };
+ let bytes = <T>::peek_from(bytes, &mut val.init);
+ (val.init, bytes)
+}
+
+/// Peek helper for constructing a `T` by `Default` initialized stack
+/// allocation.
+pub unsafe fn peek_from_default<T: Default + Peek>(bytes: *const u8) -> (T, *const u8) {
+ let mut val = T::default();
+ let bytes = <T>::peek_from(bytes, &mut val);
+ (val, bytes)
+}
+
+/// Peek inplace a `T` from a slice of bytes, returning a slice of the remaining
+/// bytes. `src` must contain at least `T::max_size()` bytes.
+///
+/// [`ensure_red_zone`] can be used to add required padding.
+pub fn peek_from_slice<'a, T: Peek>(src: &'a [u8], dst: &mut T) -> &'a [u8] {
+ unsafe {
+ // If src.len() == T::max_size() then src is at the start of the red-zone.
+ assert!(T::max_size() < src.len(), "WRDL: unexpected end of display list");
+ let end_ptr = T::peek_from(src.as_ptr(), dst);
+ let len = end_ptr as usize - src.as_ptr() as usize;
+ // Did someone break the T::peek_from() can't read more than T::max_size()
+ // bytes contract?
+ assert!(len <= src.len(), "WRDL: Peek::max_size was wrong");
+ slice::from_raw_parts(end_ptr, src.len() - len)
+ }
+}
+
+/// Poke helper to insert a serialized version of `src` at the beginning for `dst`.
+pub fn poke_inplace_slice<T: Poke>(src: &T, dst: &mut [u8]) {
+ assert!(T::max_size() <= dst.len(), "WRDL: buffer too small to write into");
+ unsafe {
+ src.poke_into(dst.as_mut_ptr());
+ }
+}
+
+/// Poke helper to append a serialized version of `src` to the end of `dst`.
+pub fn poke_into_vec<T: Poke>(src: &T, dst: &mut Vec<u8>) {
+ dst.reserve(T::max_size());
+ unsafe {
+ let ptr = dst.as_end_mut_ptr();
+ let end_ptr = src.poke_into(ptr);
+ dst.set_end_ptr(end_ptr);
+ }
+}
+
+// TODO: Is returning the len of the iterator of any practical use?
+pub fn poke_extend_vec<I>(src: I, dst: &mut Vec<u8>) -> usize
+where
+ I: ExactSizeIterator,
+ I::Item: Poke,
+{
+ let len = src.len();
+ let max_size = len * I::Item::max_size();
+ dst.reserve(max_size);
+ unsafe {
+ let ptr = dst.as_end_mut_ptr();
+ // Guard against the possibility of a misbehaved implementation of
+ // ExactSizeIterator by writing at most `len` items.
+ let end_ptr = src.take(len).fold(ptr, |ptr, item| item.poke_into(ptr));
+ dst.set_end_ptr(end_ptr);
+ }
+
+ len
+}
+
+/// Add `T::max_size()` "red zone" (padding of zeroes) to the end of the vec of
+/// `bytes`. This allows deserialization to assert that at least `T::max_size()`
+/// bytes exist at all times.
+pub fn ensure_red_zone<T: Poke>(bytes: &mut Vec<u8>) {
+ bytes.reserve(T::max_size());
+ unsafe {
+ let end_ptr = bytes.as_end_mut_ptr();
+ end_ptr.write_bytes(0, T::max_size());
+ bytes.set_end_ptr(end_ptr.add(T::max_size()));
+ }
+}
+
+/// Remove the "red zone" (padding of zeroes) from the end of the vec of `bytes`.
+/// This is effectively the inverse of `ensure_red_zone`, with the caveat that
+/// space reserved for the red zone is not un-reserved. Callers are repsonsible
+/// for making sure the vec actually has a red zone, otherwise data bytes can
+/// get stripped instead.
+pub fn strip_red_zone<T: Poke>(bytes: &mut Vec<u8>) {
+ assert!(bytes.len() >= T::max_size());
+ unsafe {
+ let end_ptr = bytes.as_end_mut_ptr();
+ bytes.set_end_ptr(end_ptr.sub(T::max_size()));
+ }
+}
+
+#[inline]
+unsafe fn read_verbatim<T>(src: *const u8, dst: *mut T) -> *const u8 {
+ *dst = (src as *const T).read_unaligned();
+ src.add(size_of::<T>())
+}
+
+#[inline]
+unsafe fn write_verbatim<T>(src: T, dst: *mut u8) -> *mut u8 {
+ (dst as *mut T).write_unaligned(src);
+ dst.add(size_of::<T>())
+}
+
+#[cfg(feature = "extras")]
+mod euclid;
+
+/// A trait for values that provide serialization into buffers of bytes.
+///
+/// # Example
+///
+/// ```no_run
+/// use peek_poke::Poke;
+///
+/// struct Bar {
+/// a: u32,
+/// b: u8,
+/// c: i16,
+/// }
+///
+/// unsafe impl Poke for Bar {
+/// fn max_size() -> usize {
+/// <u32>::max_size() + <u8>::max_size() + <i16>::max_size()
+/// }
+/// unsafe fn poke_into(&self, bytes: *mut u8) -> *mut u8 {
+/// let bytes = self.a.poke_into(bytes);
+/// let bytes = self.b.poke_into(bytes);
+/// self.c.poke_into(bytes)
+/// }
+/// }
+/// ```
+///
+/// # Safety
+///
+/// The `Poke` trait is an `unsafe` trait for the reasons, and implementors must
+/// ensure that they adhere to these contracts:
+///
+/// * `max_size()` query and calculations in general must be correct. Callers
+/// of this trait are expected to rely on the contract defined on each
+/// method, and implementors must ensure such contracts remain true.
+pub unsafe trait Poke {
+ /// Return the maximum number of bytes that the serialized version of `Self`
+ /// will occupy.
+ ///
+ /// # Safety
+ ///
+ /// Implementors of `Poke` guarantee to not write more than the result of
+ /// calling `max_size()` into the buffer pointed to by `bytes` when
+ /// `poke_into()` is called.
+ fn max_size() -> usize;
+ /// Serialize into the buffer pointed to by `bytes`.
+ ///
+ /// Returns a pointer to the next byte after the serialized representation of `Self`.
+ ///
+ /// # Safety
+ ///
+ /// This function is unsafe because undefined behavior can result if the
+ /// caller does not ensure all of the following:
+ ///
+ /// * `bytes` must denote a valid pointer to a block of memory.
+ ///
+ /// * `bytes` must pointer to at least the number of bytes returned by
+ /// `max_size()`.
+ unsafe fn poke_into(&self, bytes: *mut u8) -> *mut u8;
+}
+
+/// A trait for values that provide deserialization from buffers of bytes.
+///
+/// # Example
+///
+/// ```ignore
+/// use peek_poke::Peek;
+///
+/// struct Bar {
+/// a: u32,
+/// b: u8,
+/// c: i16,
+/// }
+///
+/// ...
+///
+/// impl Peek for Bar {
+/// unsafe fn peek_from(&mut self, bytes: *const u8) -> *const u8 {
+/// let bytes = self.a.peek_from(bytes);
+/// let bytes = self.b.peek_from(bytes);
+/// self.c.peek_from(bytes)
+/// }
+/// }
+/// ```
+///
+/// # Safety
+///
+/// The `Peek` trait contains unsafe methods for the following reasons, and
+/// implementors must ensure that they adhere to these contracts:
+///
+/// * Callers of this trait are expected to rely on the contract defined on each
+/// method, and implementors must ensure that `peek_from()` doesn't read more
+/// bytes from `bytes` than is returned by `Peek::max_size()`.
+pub trait Peek: Poke {
+ /// Deserialize from the buffer pointed to by `bytes`.
+ ///
+ /// Returns a pointer to the next byte after the unconsumed bytes not used
+ /// to deserialize the representation of `Self`.
+ ///
+ /// # Safety
+ ///
+ /// This function is unsafe because undefined behavior can result if the
+ /// caller does not ensure all of the following:
+ ///
+ /// * `bytes` must denote a valid pointer to a block of memory.
+ ///
+ /// * `bytes` must pointer to at least the number of bytes returned by
+ /// `Poke::max_size()`.
+ unsafe fn peek_from(bytes: *const u8, output: *mut Self) -> *const u8;
+}
+
+macro_rules! impl_poke_for_deref {
+ (<$($desc:tt)+) => {
+ unsafe impl <$($desc)+ {
+ #[inline(always)]
+ fn max_size() -> usize {
+ <T>::max_size()
+ }
+ unsafe fn poke_into(&self, bytes: *mut u8) -> *mut u8 {
+ (**self).poke_into(bytes)
+ }
+ }
+ }
+}
+
+impl_poke_for_deref!(<'a, T: Poke> Poke for &'a T);
+impl_poke_for_deref!(<'a, T: Poke> Poke for &'a mut T);
+
+macro_rules! impl_for_primitive {
+ ($($ty:ty)+) => {
+ $(unsafe impl Poke for $ty {
+ #[inline(always)]
+ fn max_size() -> usize {
+ size_of::<Self>()
+ }
+ #[inline(always)]
+ unsafe fn poke_into(&self, bytes: *mut u8) -> *mut u8 {
+ write_verbatim(*self, bytes)
+ }
+ }
+ impl Peek for $ty {
+ #[inline(always)]
+ unsafe fn peek_from(bytes: *const u8, output: *mut Self) -> *const u8 {
+ read_verbatim(bytes, output)
+ }
+ })+
+ };
+}
+
+impl_for_primitive! {
+ i8 i16 i32 i64 isize
+ u8 u16 u32 u64 usize
+ f32 f64
+}
+
+unsafe impl Poke for bool {
+ #[inline(always)]
+ fn max_size() -> usize {
+ u8::max_size()
+ }
+ #[inline]
+ unsafe fn poke_into(&self, bytes: *mut u8) -> *mut u8 {
+ (*self as u8).poke_into(bytes)
+ }
+}
+
+impl Peek for bool {
+ #[inline]
+ unsafe fn peek_from(bytes: *const u8, output: *mut Self) -> *const u8 {
+ let mut int_bool = 0u8;
+ let ptr = <u8>::peek_from(bytes, &mut int_bool);
+ *output = int_bool != 0;
+ ptr
+ }
+}
+
+unsafe impl<T> Poke for PhantomData<T> {
+ #[inline(always)]
+ fn max_size() -> usize {
+ 0
+ }
+ #[inline(always)]
+ unsafe fn poke_into(&self, bytes: *mut u8) -> *mut u8 {
+ bytes
+ }
+}
+
+impl<T> Peek for PhantomData<T> {
+ #[inline(always)]
+ unsafe fn peek_from(bytes: *const u8, output: *mut Self) -> *const u8 {
+ *output = PhantomData;
+ bytes
+ }
+}
+
+unsafe impl<T: Poke> Poke for Option<T> {
+ #[inline(always)]
+ fn max_size() -> usize {
+ u8::max_size() + T::max_size()
+ }
+
+ #[inline]
+ unsafe fn poke_into(&self, bytes: *mut u8) -> *mut u8 {
+ match self {
+ None => 0u8.poke_into(bytes),
+ Some(ref v) => {
+ let bytes = 1u8.poke_into(bytes);
+ let bytes = v.poke_into(bytes);
+ bytes
+ }
+ }
+ }
+}
+
+impl<T: Default + Peek> Peek for Option<T> {
+ #[inline]
+ unsafe fn peek_from(bytes: *const u8, output: *mut Self) -> *const u8 {
+ let (variant, bytes) = peek_from_default::<u8>(bytes);
+ match variant {
+ 0 => {
+ *output = None;
+ bytes
+ }
+ 1 => {
+ let (val, bytes) = peek_from_default(bytes);
+ *output = Some(val);
+ bytes
+ }
+ _ => unreachable!(),
+ }
+ }
+}
+
+macro_rules! impl_for_arrays {
+ ($($len:tt)+) => {
+ $(unsafe impl<T: Poke> Poke for [T; $len] {
+ fn max_size() -> usize {
+ $len * T::max_size()
+ }
+ unsafe fn poke_into(&self, bytes: *mut u8) -> *mut u8 {
+ self.iter().fold(bytes, |bytes, e| e.poke_into(bytes))
+ }
+ }
+ impl<T: Peek> Peek for [T; $len] {
+ unsafe fn peek_from(bytes: *const u8, output: *mut Self) -> *const u8 {
+ (&mut *output).iter_mut().fold(bytes, |bytes, e| <T>::peek_from(bytes, e))
+ }
+ })+
+ }
+}
+
+impl_for_arrays! {
+ 01 02 03 04 05 06 07 08 09 10
+ 11 12 13 14 15 16 17 18 19 20
+ 21 22 23 24 25 26 27 28 29 30
+ 31 32
+}
+
+unsafe impl Poke for () {
+ fn max_size() -> usize {
+ 0
+ }
+ unsafe fn poke_into(&self, bytes: *mut u8) -> *mut u8 {
+ bytes
+ }
+}
+impl Peek for () {
+ unsafe fn peek_from(bytes: *const u8, output: *mut Self) -> *const u8 {
+ *output = ();
+ bytes
+ }
+}
+
+macro_rules! impl_for_tuple {
+ ($($n:tt: $ty:ident),+) => {
+ unsafe impl<$($ty: Poke),+> Poke for ($($ty,)+) {
+ #[inline(always)]
+ fn max_size() -> usize {
+ 0 $(+ <$ty>::max_size())+
+ }
+ unsafe fn poke_into(&self, bytes: *mut u8) -> *mut u8 {
+ $(let bytes = self.$n.poke_into(bytes);)+
+ bytes
+ }
+ }
+ impl<$($ty: Peek),+> Peek for ($($ty,)+) {
+ unsafe fn peek_from(bytes: *const u8, output: *mut Self) -> *const u8 {
+ $(let bytes = $ty::peek_from(bytes, &mut (*output).$n);)+
+ bytes
+ }
+ }
+ }
+}
+
+impl_for_tuple!(0: A);
+impl_for_tuple!(0: A, 1: B);
+impl_for_tuple!(0: A, 1: B, 2: C);
+impl_for_tuple!(0: A, 1: B, 2: C, 3: D);
+impl_for_tuple!(0: A, 1: B, 2: C, 3: D, 4: E);
diff --git a/gfx/wr/peek-poke/src/slice_ext.rs b/gfx/wr/peek-poke/src/slice_ext.rs
new file mode 100644
index 0000000000..f309d2f741
--- /dev/null
+++ b/gfx/wr/peek-poke/src/slice_ext.rs
@@ -0,0 +1,19 @@
+// Copyright 2019 The Servo Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+pub trait AsEndMutPtr<T> {
+ fn as_end_mut_ptr(self) -> *mut T;
+}
+
+impl<'a> AsEndMutPtr<u8> for &'a mut [u8] {
+ fn as_end_mut_ptr(self) -> *mut u8 {
+ unsafe { self.as_mut_ptr().add(self.len()) }
+ }
+}
diff --git a/gfx/wr/peek-poke/src/vec_ext.rs b/gfx/wr/peek-poke/src/vec_ext.rs
new file mode 100644
index 0000000000..42e26032e5
--- /dev/null
+++ b/gfx/wr/peek-poke/src/vec_ext.rs
@@ -0,0 +1,26 @@
+// Copyright 2019 The Servo Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+use std::vec::Vec;
+
+pub trait VecExt {
+ type Item;
+ unsafe fn set_end_ptr(&mut self, end: *const Self::Item);
+}
+
+impl<T> VecExt for Vec<T> {
+ type Item = T;
+ unsafe fn set_end_ptr(&mut self, end: *const T) {
+ assert!(end as usize >= self.as_ptr() as usize);
+ let new_len = end as usize - self.as_ptr() as usize;
+ assert!(new_len <= self.capacity());
+ self.set_len(new_len);
+ }
+}
diff --git a/gfx/wr/peek-poke/tests/max_size.rs b/gfx/wr/peek-poke/tests/max_size.rs
new file mode 100644
index 0000000000..67cd1ca4a6
--- /dev/null
+++ b/gfx/wr/peek-poke/tests/max_size.rs
@@ -0,0 +1,117 @@
+// Copyright 2019 The Servo Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+#![allow(dead_code)]
+
+use peek_poke::{PeekPoke, Poke};
+use std::{marker::PhantomData, mem::size_of};
+
+#[test]
+fn test_numbers() {
+ assert_eq!(u8::max_size(), size_of::<u8>());
+ assert_eq!(u16::max_size(), size_of::<u16>());
+ assert_eq!(u32::max_size(), size_of::<u32>());
+ assert_eq!(u64::max_size(), size_of::<u64>());
+ assert_eq!(usize::max_size(), size_of::<usize>());
+ assert_eq!(i8::max_size(), size_of::<i8>());
+ assert_eq!(i16::max_size(), size_of::<i16>());
+ assert_eq!(i32::max_size(), size_of::<i32>());
+ assert_eq!(i64::max_size(), size_of::<i64>());
+ assert_eq!(isize::max_size(), size_of::<isize>());
+ // floating
+ assert_eq!(f32::max_size(), size_of::<f32>());
+ assert_eq!(f64::max_size(), size_of::<f64>());
+}
+
+#[test]
+fn test_bool() {
+ assert_eq!(bool::max_size(), size_of::<u8>());
+}
+
+#[test]
+fn test_option() {
+ assert_eq!(
+ Option::<usize>::max_size(),
+ <u8>::max_size() + <usize>::max_size()
+ );
+}
+
+#[test]
+fn test_fixed_size_array() {
+ assert_eq!(<[u32; 32]>::max_size(), 32 * size_of::<u32>());
+ assert_eq!(<[u64; 8]>::max_size(), 8 * size_of::<u64>());
+ assert_eq!(<[u8; 19]>::max_size(), 19 * size_of::<u8>());
+}
+
+#[test]
+fn test_tuple() {
+ assert_eq!(<(isize, )>::max_size(), size_of::<isize>());
+ assert_eq!(<(isize, isize, isize)>::max_size(), 3 * size_of::<isize>());
+ assert_eq!(<(isize, ())>::max_size(), size_of::<isize>());
+}
+
+#[test]
+fn test_basic_struct() {
+ #[derive(Debug, PeekPoke)]
+ struct Bar {
+ a: u32,
+ b: u32,
+ c: u32,
+ }
+
+ assert_eq!(<Bar>::max_size(), 3 * <u32>::max_size());
+}
+
+#[test]
+fn test_enum() {
+ #[derive(Clone, Copy, PeekPoke)]
+ enum TestEnum {
+ NoArg,
+ OneArg(usize),
+ Args(usize, usize),
+ AnotherNoArg,
+ StructLike { x: usize, y: f32 },
+ }
+ assert_eq!(
+ TestEnum::max_size(),
+ <u8>::max_size() + 2 * <usize>::max_size()
+ );
+}
+
+#[test]
+fn test_enum_cstyle() {
+ #[repr(u32)]
+ #[derive(Clone, Copy, PeekPoke)]
+ enum BorderStyle {
+ None = 0,
+ Solid = 1,
+ Double = 2,
+ Dotted = 3,
+ Dashed = 4,
+ Hidden = 5,
+ Groove = 6,
+ Ridge = 7,
+ Inset = 8,
+ Outset = 9,
+ }
+ assert_eq!(BorderStyle::max_size(), <u8>::max_size());
+}
+
+#[test]
+fn test_phantom_data() {
+ struct Bar;
+ #[derive(PeekPoke)]
+ struct Foo {
+ x: u32,
+ y: u32,
+ _marker: PhantomData<Bar>,
+ }
+ assert_eq!(Foo::max_size(), 2 * size_of::<u32>())
+}
diff --git a/gfx/wr/peek-poke/tests/round_trip.rs b/gfx/wr/peek-poke/tests/round_trip.rs
new file mode 100644
index 0000000000..3134c207ec
--- /dev/null
+++ b/gfx/wr/peek-poke/tests/round_trip.rs
@@ -0,0 +1,275 @@
+// Copyright 2019 The Servo Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+use peek_poke::{Peek, PeekPoke, Poke};
+use std::{fmt::Debug, marker::PhantomData};
+
+fn poke_into<V: Peek + Poke>(a: &V) -> Vec<u8> {
+ let mut v = <Vec<u8>>::with_capacity(<V>::max_size());
+ let end_ptr = unsafe { a.poke_into(v.as_mut_ptr()) };
+ let new_size = end_ptr as usize - v.as_ptr() as usize;
+ assert!(new_size <= v.capacity());
+ unsafe {
+ v.set_len(new_size);
+ }
+ v
+}
+
+#[cfg(not(feature = "option_copy"))]
+fn the_same<V>(a: V)
+where
+ V: Debug + Default + PartialEq + Peek + Poke,
+{
+ let v = poke_into(&a);
+ let (b, end_ptr) = unsafe { peek_poke::peek_from_default(v.as_ptr()) };
+ let size = end_ptr as usize - v.as_ptr() as usize;
+ assert_eq!(size, v.len());
+ assert_eq!(a, b);
+}
+
+#[cfg(feature = "option_copy")]
+fn the_same<V>(a: V)
+where
+ V: Copy + Debug + PartialEq + Peek + Poke,
+{
+ let v = poke_into(&a);
+ let mut b = a;
+ let end_ptr = unsafe { b.peek_from(v.as_ptr()) };
+ let size = end_ptr as usize - v.as_ptr() as usize;
+ assert_eq!(size, v.len());
+ assert_eq!(a, b);
+}
+
+#[test]
+fn test_numbers() {
+ // unsigned positive
+ the_same(5u8);
+ the_same(5u16);
+ the_same(5u32);
+ the_same(5u64);
+ the_same(5usize);
+ // signed positive
+ the_same(5i8);
+ the_same(5i16);
+ the_same(5i32);
+ the_same(5i64);
+ the_same(5isize);
+ // signed negative
+ the_same(-5i8);
+ the_same(-5i16);
+ the_same(-5i32);
+ the_same(-5i64);
+ the_same(-5isize);
+ // floating
+ the_same(-100f32);
+ the_same(0f32);
+ the_same(5f32);
+ the_same(-100f64);
+ the_same(5f64);
+}
+
+#[test]
+fn test_bool() {
+ the_same(true);
+ the_same(false);
+}
+
+#[cfg(any(feature = "option_copy", feature = "option_default"))]
+#[test]
+fn test_option() {
+ the_same(Some(5usize));
+ //the_same(Some("foo bar".to_string()));
+ the_same(None::<usize>);
+}
+
+#[test]
+fn test_fixed_size_array() {
+ the_same([24u32; 32]);
+ the_same([1u64, 2, 3, 4, 5, 6, 7, 8]);
+ the_same([0u8; 19]);
+}
+
+#[test]
+fn test_tuple() {
+ the_same((1isize, ));
+ the_same((1isize, 2isize, 3isize));
+ the_same((1isize, ()));
+}
+
+#[test]
+fn test_basic_struct() {
+ #[derive(Copy, Clone, Debug, Default, PartialEq, PeekPoke)]
+ struct Bar {
+ a: u32,
+ b: u32,
+ c: u32,
+ #[cfg(any(feature = "option_copy", feature = "option_default"))]
+ d: Option<u32>,
+ }
+
+ the_same(Bar {
+ a: 2,
+ b: 4,
+ c: 42,
+ #[cfg(any(feature = "option_copy", feature = "option_default"))]
+ d: None,
+ });
+}
+
+#[test]
+fn test_enum() {
+ #[derive(Clone, Copy, Debug, PartialEq, PeekPoke)]
+ enum TestEnum {
+ NoArg,
+ OneArg(usize),
+ Args(usize, usize),
+ AnotherNoArg,
+ StructLike { x: usize, y: f32 },
+ }
+
+ impl Default for TestEnum {
+ fn default() -> Self {
+ TestEnum::NoArg
+ }
+ }
+
+ the_same(TestEnum::NoArg);
+ the_same(TestEnum::OneArg(4));
+ the_same(TestEnum::Args(4, 5));
+ the_same(TestEnum::AnotherNoArg);
+ the_same(TestEnum::StructLike { x: 4, y: 3.14159 });
+}
+
+#[test]
+fn test_enum_cstyle() {
+ #[repr(u32)]
+ #[derive(Clone, Copy, Debug, PartialEq, Eq, PeekPoke)]
+ enum BorderStyle {
+ None = 0,
+ Solid = 1,
+ Double = 2,
+ Dotted = 3,
+ Dashed = 4,
+ Hidden = 5,
+ Groove = 6,
+ Ridge = 7,
+ Inset = 8,
+ Outset = 9,
+ }
+
+ impl Default for BorderStyle {
+ fn default() -> Self {
+ BorderStyle::None
+ }
+ }
+
+ the_same(BorderStyle::None);
+ the_same(BorderStyle::Solid);
+ the_same(BorderStyle::Double);
+ the_same(BorderStyle::Dotted);
+ the_same(BorderStyle::Dashed);
+ the_same(BorderStyle::Hidden);
+ the_same(BorderStyle::Groove);
+ the_same(BorderStyle::Ridge);
+ the_same(BorderStyle::Inset);
+ the_same(BorderStyle::Outset);
+}
+
+#[test]
+fn test_phantom_data() {
+ struct Bar;
+ #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PeekPoke)]
+ struct Foo {
+ x: u32,
+ y: u32,
+ _marker: PhantomData<Bar>,
+ }
+ the_same(Foo {
+ x: 19,
+ y: 42,
+ _marker: PhantomData,
+ });
+}
+
+#[test]
+fn test_generic() {
+ #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PeekPoke)]
+ struct Foo<T> {
+ x: T,
+ y: T,
+ }
+ the_same(Foo { x: 19.0, y: 42.0 });
+}
+
+#[test]
+fn test_generic_enum() {
+ #[derive(Clone, Copy, Debug, Default, PartialEq, PeekPoke)]
+ pub struct PropertyBindingKey<T> {
+ pub id: usize,
+ _phantom: PhantomData<T>,
+ }
+
+ #[derive(Clone, Copy, Debug, PartialEq, PeekPoke)]
+ pub enum PropertyBinding<T> {
+ Value(T),
+ Binding(PropertyBindingKey<T>, T),
+ }
+
+ impl<T: Default> Default for PropertyBinding<T> {
+ fn default() -> Self {
+ PropertyBinding::Value(Default::default())
+ }
+ }
+}
+
+#[cfg(all(feature = "extras", feature = "option_copy"))]
+mod extra_tests {
+ use super::*;
+ use euclid::{Point2D, Rect, SideOffsets2D, Size2D, Transform3D, Vector2D};
+ use std::mem::size_of;
+
+ #[test]
+ fn euclid_types() {
+ the_same(Point2D::<f32>::new(1.0, 2.0));
+ assert_eq!(Point2D::<f32>::max_size(), 2 * size_of::<f32>());
+
+ the_same(Rect::<f32>::new(
+ Point2D::<f32>::new(0.0, 0.0),
+ Size2D::<f32>::new(100.0, 80.0),
+ ));
+ assert_eq!(Rect::<f32>::max_size(), 4 * size_of::<f32>());
+
+ the_same(SideOffsets2D::<f32>::new(0.0, 10.0, -1.0, -10.0));
+ assert_eq!(SideOffsets2D::<f32>::max_size(), 4 * size_of::<f32>());
+
+ the_same(Transform3D::<f32>::identity());
+ assert_eq!(Transform3D::<f32>::max_size(), 16 * size_of::<f32>());
+
+ the_same(Vector2D::<f32>::new(1.0, 2.0));
+ assert_eq!(Vector2D::<f32>::max_size(), 2 * size_of::<f32>());
+ }
+
+ #[test]
+ fn webrender_api_types() {
+ type PipelineSourceId = i32;
+ #[derive(Clone, Copy, Debug, PartialEq, PeekPoke)]
+ struct PipelineId(pub PipelineSourceId, pub u32);
+
+ #[derive(Clone, Copy, Debug, PartialEq, PeekPoke)]
+ struct ClipChainId(pub u64, pub PipelineId);
+
+ #[derive(Clone, Copy, Debug, PartialEq, PeekPoke)]
+ struct SpatialId(pub usize, pub PipelineId);
+
+ the_same(PipelineId(42, 2));
+ the_same(ClipChainId(19u64, PipelineId(42, 2)));
+ the_same(SpatialId(19usize, PipelineId(42, 2)));
+ }
+}
diff --git a/gfx/wr/rustfmt.toml b/gfx/wr/rustfmt.toml
new file mode 100644
index 0000000000..d43a44fcbd
--- /dev/null
+++ b/gfx/wr/rustfmt.toml
@@ -0,0 +1,6 @@
+reorder_imports = false
+reorder_imports_in_group = true
+reorder_imported_names = true
+error_on_line_overflow_comments = false
+max_width = 100
+spaces_around_ranges = true
diff --git a/gfx/wr/servo-tidy.toml b/gfx/wr/servo-tidy.toml
new file mode 100644
index 0000000000..05227a952c
--- /dev/null
+++ b/gfx/wr/servo-tidy.toml
@@ -0,0 +1,39 @@
+[configs]
+skip-check-length = false
+skip-check-licenses = false
+check-alphabetical-order = false
+
+[ignore]
+# Ignored packages with duplicated versions
+packages = [
+ "cfg-if",
+ "core-foundation",
+ "core-foundation-sys",
+ "core-graphics",
+ "gl_generator",
+ # Internal dependencies of glsl-lang (only used in wrench) require different versions.
+ "hashbrown",
+ # glsl requires 5.1, and xcursor (required by winit) requires 7.1.
+ # when a version of glsl depending on 7.1 is published we can update.
+ "nom",
+ # glsl-lang requires 0.12.1, and glutin requires 0.11.2
+ "parking_lot",
+ "parking_lot_core",
+ "rand",
+ "rand_core",
+ # Can be fixed by removing time dependency - see bug 1765324
+ "wasi",
+]
+
+# Files that are ignored for all tidy and lint checks.
+files = [
+ "./wrench/src/egl.rs", # Copied from glutin
+]
+
+# Many directories are currently ignored while we tidy things up
+# gradually.
+directories = [
+ # Generated and upstream code combined with our own. Could use cleanup
+ "./target",
+ "./webrender/src",
+]
diff --git a/gfx/wr/swgl/Cargo.toml b/gfx/wr/swgl/Cargo.toml
new file mode 100644
index 0000000000..2e84e2267c
--- /dev/null
+++ b/gfx/wr/swgl/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "swgl"
+version = "0.1.0"
+license = "MPL-2.0"
+authors = ["The Mozilla Project Developers"]
+build = "build.rs"
+description = "Software OpenGL implementation for WebRender."
+
+[build-dependencies]
+cc = "1.0.46"
+glsl-to-cxx = { path = "../glsl-to-cxx" }
+webrender_build = { path = "../webrender_build" }
+
+[dependencies]
+gleam = "0.15"
diff --git a/gfx/wr/swgl/README.md b/gfx/wr/swgl/README.md
new file mode 100644
index 0000000000..2c43ed8819
--- /dev/null
+++ b/gfx/wr/swgl/README.md
@@ -0,0 +1,220 @@
+# swgl
+
+Software OpenGL implementation for WebRender
+
+## Overview
+This is a relatively simple single threaded software rasterizer designed
+for use by WebRender. It will shade one quad at a time using a 4xf32 vector
+with one vertex per lane. It rasterizes quads usings spans and shades that
+span 4 pixels at a time.
+
+## Building
+clang-cl is required to build on Windows. This can be done by installing
+the llvm binaries from https://releases.llvm.org/ and adding the installation
+to the path with something like `set PATH=%PATH%;C:\Program Files\LLVM\bin`.
+Then `set CC=clang-cl` and `set CXX=clang-cl`. That should be sufficient
+for `cc-rs` to use `clang-cl` instead of `cl`.
+
+## Extensions
+SWGL contains a number of OpenGL and GLSL extensions designed to both ease
+integration with WebRender and to help accelerate span rasterization.
+
+GLSL extension intrinsics are generally prefixed with `swgl_` to distinguish
+them from other items in the GLSL namespace.
+
+Inside GLSL, the `SWGL` preprocessor token is defined so that usage of SWGL
+extensions may be conditionally compiled.
+
+```
+void swgl_drawSpanRGBA8();
+void swgl_drawSpanR8();
+
+int swgl_SpanLength;
+int swgl_StepSize;
+
+mixed swgl_interpStep(mixed varying_input);
+void swgl_stepInterp();
+```
+
+SWGL's default fragment processing calls a fragment shader's `main` function
+on groups of fragments in units of `swgl_StepSize`. On return, the value of
+gl_FragColor is read, packed to an appropriate pixel format, and sent to the
+blend stage for output to the destination framebuffer. This can be inefficient
+for some types of fragment shaders, such as those that must lookup from a
+texture and immediately output it, unpacking the texels only to subsequently
+repack them at cost. Also, various per-fragment conditions in the shader might
+need to be repeatedly checked, even though they are actually constant over
+the entire primitive.
+
+To work around this inefficiency, SWGL allows fragments to optionally be
+processed over entire spans. This can both side-step the packing inefficiency
+as well as more efficiently deal with conditions that remain constant over an
+entire span. SWGL also introduces various specialized intrinsics for more
+efficiently dealing with certain types of primitive spans with optimal
+fixed-function processing.
+
+Inside a fragment shader, a `swgl_drawSpan` function may be defined to override
+the normal fragment processing for that fragment shader. The function must then
+call some form of `swgl_commit` intrinsic to actually output to the destination
+framebuffer via the blend stage, as normal fragment processing does not take
+place otherwise as would have happened in `main`. This function is used by the
+rasterizer to process an entire span of fragments that have passed the depth
+test (if applicable) and clipping, but have not yet been output to the blend
+stage.
+
+The amount of fragments within the span to be processed is designated by
+`swgl_SpanLength` and is always aligned to units of `swgl_StepSize`.
+The size of a group of fragments in terms of which `swgl_commit` intrinsics
+process and output fragments is designated by `swgl_StepSize`. The
+`swgl_commit` intrinsics will deduct accordingly from `swgl_SpanLength` in
+units of `swgl_StepSize` to reflect the fragments actually processed, which
+may be less than the entire span or up to the entire span.
+
+Fragments should be output until `swgl_SpanLength` becomes zero to process the
+entire span. If `swgl_drawSpan` returns while leaving any fragments unprocessed,
+the remaining fragments will be processed as normal by the fragment shader's
+`main` function. This can be used to conditionally handle certain fast-paths
+in a fragment shader by otherwise defaulting to the `main` function if
+`swgl_drawSpan` can't appropriately process some or all of the fragments.
+
+The values of any varying inputs to the fragment shader will be set to their
+values for the start of the span, but do not automatically update over the
+the course of a span within a given call to `swgl_drawSpan`. The
+`swgl_interpStep` intrinsic may be used to get the derivative per `swgl_StepSize`
+group of fragments of a varying input so that the caller may update such
+variables manually if desired or otherwise use that information for processing.
+The `swgl_stepInterp` intrinsic forces all such varying inputs to advance by
+a single step.
+
+The RGBA8 version will be used when the destination framebuffer is RGBA8 format,
+and the R8 version will be used when the destination framebuffer is R8. Various
+other intrinsics described below may have restrictions on whether they can be
+used only with a certain destination framebuffer format and are noted as such if
+so.
+
+```
+void swgl_clipMask(sampler2D mask, vec2 offset, vec2 bb_origin, vec2 bb_size);
+```
+
+When called from the the vertex shader, this specifies a clip mask texture to
+be used to mask the currently drawn primitive while blending is enabled. This
+mask will only apply to the current primitive.
+
+The mask must be an R8 texture that will be interpreted as alpha weighting
+applied to the source pixel prior to the blend stage. It is sampled 1:1 with
+nearest filtering without any applied transform. The given offset specifies
+the positioning of the clip mask relative to the framebuffer's viewport.
+
+The supplied bounding box constrains sampling of the clip mask to only fall
+within the given rectangle, specified relative to the clip mask offset.
+Anything falling outside this rectangle will be clipped entirely. If the
+rectangle is empty, then the clip mask will be ignored.
+
+```
+void swgl_antiAlias(int edgeMask);
+```
+
+When called from the vertex shader, this enables anti-aliasing for the
+currently drawn primitive while blending is enabled. This setting will only
+apply to the current primitive. Anti-aliasing will be applied only to the
+edges corresponding to bits supplied in the mask. For simple use-cases,
+the edge mask can be set to all 1 bits to enable AA for the entire quad.
+
+The order of the bits in the edge mask must match the winding order in which
+the vertices are output in the vertex shader if processed as a quad, so that
+the edge ends on that vertex. The easiest way to understand this ordering
+is that for a rectangle (x0,y0,x1,y1) then the edge Nth edge bit corresponds
+to the edge where Nth coordinate in the rectangle is constant.
+
+SWGL tries to use an anti-aliasing method that is reasonably close to WR's
+signed-distance field approximation. WR would normally try to discern the
+2D local-space coordinates of a given destination pixel relative to the
+2D local-space bounding rectangle of a primitive. It then uses the screen-
+space derivative to try to determine the how many local-space units equate
+to a distance of around one screen-space pixel. A distance approximation
+of coverage is then used based on the distance in local-space from the
+the current pixel's center, roughly at half-intensity at pixel center
+and ranging to zero or full intensity within a radius of half a pixel
+away from the center. To account for AAing going outside the normal geometry
+boundaries of the primitive, WR has to extrude the primitive by a local-space
+estimate to allow some AA to happen within the extruded region.
+
+SWGL can ultimately do this approximation more simply and get around the
+extrusion limitations by just ensuring spans encompass any pixel that is
+partially covered when computing span boundaries. Further, since SWGL already
+knows the slope of an edge and the coordinate of the span relative to the span
+boundaries, finding the partial coverage of a given span becomes easy to do
+without requiring any extra interpolants to track against local-space bounds.
+Essentially, SWGL just performs anti-aliasing on the actual geometry bounds,
+but when the pixels on a span's edge are determined to be partially covered
+during span rasterization, it uses the same distance field method as WR on
+those span boundary pixels to estimate the coverage based on edge slope.
+
+```
+void swgl_commitTextureLinearRGBA8(sampler, vec2 uv, vec4 uv_bounds);
+void swgl_commitTextureLinearR8(sampler, vec2 uv, vec4 uv_bounds);
+void swgl_commitTextureLinearR8ToRGBA8(sampler, vec2 uv, vec4 uv_bounds);
+
+void swgl_commitTextureLinearColorRGBA8(sampler, vec2 uv, vec4 uv_bounds, vec4|float color);
+void swgl_commitTextureLinearColorR8(sampler, vec2 uv, vec4 uv_bounds, vec4|float color);
+void swgl_commitTextureLinearColorR8ToRGBA8(sampler, vec2 uv, vec4 uv_bounds, vec4|float color);
+
+void swgl_commitTextureLinearRepeatRGBA8(sampler, vec2 uv, vec2 tile_repeat, vec4 uv_repeat, vec4 uv_bounds);
+void swgl_commitTextureLinearRepeatColorRGBA8(sampler, vec2 uv, vec2 tile_repeat, vec4 uv_repeat, vec4 uv_bounds, vec4|float color);
+
+void swgl_commitTextureNearestRGBA8(sampler, vec2 uv, vec4 uv_bounds);
+void swgl_commitTextureNearestColorRGBA8(sampler, vec2 uv, vec4 uv_bounds, vec4|float color);
+
+void swgl_commitTextureNearestRepeatRGBA8(sampler, vec2 uv, vec2 tile_repeat, vec4 uv_repeat, vec4 uv_bounds);
+void swgl_commitTextureNearestRepeatColorRGBA8(sampler, vec2 uv, vec2 tile_repeat, vec4 uv_repeat, vec4 uv_bounds, vec4|float color);
+
+void swgl_commitTextureRGBA8(sampler, vec2 uv, vec4 uv_bounds);
+void swgl_commitTextureColorRGBA8(sampler, vec2 uv, vec4 uv_bounds, vec4|float color);
+
+void swgl_commitTextureRepeatRGBA8(sampler, vec2 uv, vec2 tile_repeat, vec4 uv_repeat, vec4 uv_bounds);
+void swgl_commitTextureRepeatColorRGBA8(sampler, vec2 uv, vec2 tile_repeat, vec4 uv_repeat, vec4 uv_bounds, vec4|float color);
+
+void swgl_commitPartialTextureLinearR8(int len, sampler, vec2 uv, vec4 uv_bounds);
+void swgl_commitPartialTextureLinearInvertR8(int len, sampler, vec2 uv, vec4 uv_bounds);
+```
+
+Samples and commits an entire span of texture starting at the given uv and
+within the supplied uv bounds. The color variations also accept a supplied color
+that modulates the result.
+
+The RGBA8 versions may only be used to commit within `swgl_drawSpanRGBA8`, and
+the R8 versions may only be used to commit within `swgl_drawSpanR8`. The R8ToRGBA8
+versions may be used to sample from an R8 source while committing to an RGBA8
+framebuffer.
+
+The Linear variations use a linear filter that bilinearly interpolates between
+the four samples near the pixel. The Nearest variations use a nearest filter
+that chooses the closest aliased sample to the center of the pixel. If neither
+Linear nor Nearest is specified in the `swgl_commitTexture` variation name, then
+it will automatically select either the Linear or Nearest variation depending
+on the sampler's specified filter.
+
+The Repeat variations require an optional repeat rect that specifies how to
+scale and offset the UVs, assuming the UVs are normalized to repeat in the
+range 0 to 1. For NearestRepeat variations, it is assumed the repeat rect is
+always within the bounds. The tile repeat limit, if non-zero, specifies the
+maximum number of repetitions allowed.
+
+The Partial variations allow committing only a sub-span rather the entire
+remaining span. These are currently only implemented in linear R8 variants
+for optimizing clip shaders in WebRender. The Invert variant of these is
+useful for implementing clip-out modes by inverting the source texture value.
+
+```
+// Premultiplied alpha over blend, but with source color set to source alpha modulated with a constant color.
+void swgl_blendDropShadow(vec4 color);
+// Premultiplied alpha over blend, but treats the source as a subpixel mask modulated with a constant color.
+void swgl_blendSubpixelText(vec4 color);
+```
+
+SWGL allows overriding the blend mode per-primitive by calling `swgl_blend`
+intrinsics in the vertex shader. The existing blend mode set by the GL is
+replaced with the one specified by the intrinsic for the current primitive.
+The blend mode will be reset to the blend mode set by the GL for the next
+primitive after the current one, even within the same draw call.
+
diff --git a/gfx/wr/swgl/build.rs b/gfx/wr/swgl/build.rs
new file mode 100644
index 0000000000..d02797d437
--- /dev/null
+++ b/gfx/wr/swgl/build.rs
@@ -0,0 +1,205 @@
+/* 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/. */
+
+extern crate cc;
+extern crate glsl_to_cxx;
+extern crate webrender_build;
+
+use std::collections::HashSet;
+use std::fmt::Write;
+use webrender_build::shader::{ShaderFeatureFlags, get_shader_features};
+
+// Shader key is in "name feature,feature" format.
+// File name needs to be formatted as "name_feature_feature".
+fn shader_file(shader_key: &str) -> String {
+ shader_key.replace(' ', "_").replace(',', "_")
+}
+
+fn write_load_shader(shader_keys: &[String]) {
+ let mut load_shader = String::new();
+ for s in shader_keys {
+ let _ = write!(load_shader, "#include \"{}.h\"\n", shader_file(s));
+ }
+ load_shader.push_str("ProgramLoader load_shader(const char* name) {\n");
+ for s in shader_keys {
+ let _ = write!(load_shader, " if (!strcmp(name, \"{}\")) {{ return {}_program::loader; }}\n",
+ s, shader_file(s));
+ }
+ load_shader.push_str(" return nullptr;\n}\n");
+ std::fs::write(std::env::var("OUT_DIR").unwrap() + "/load_shader.h", load_shader).unwrap();
+}
+
+fn process_imports(shader_dir: &str, shader: &str, included: &mut HashSet<String>, output: &mut String) {
+ if !included.insert(shader.into()) {
+ return;
+ }
+ println!("cargo:rerun-if-changed={}/{}.glsl", shader_dir, shader);
+ let source = std::fs::read_to_string(format!("{}/{}.glsl", shader_dir, shader)).unwrap();
+ for line in source.lines() {
+ if line.starts_with("#include ") {
+ let imports = line["#include ".len() ..].split(',');
+ for import in imports {
+ process_imports(shader_dir, import, included, output);
+ }
+ } else if line.starts_with("#version ") || line.starts_with("#extension ") {
+ // ignore
+ } else {
+ output.push_str(line);
+ output.push('\n');
+ }
+ }
+}
+
+fn translate_shader(shader_key: &str, shader_dir: &str) {
+ let mut imported = String::from("#define SWGL 1\n#define __VERSION__ 150\n");
+ let _ = write!(imported, "#define WR_MAX_VERTEX_TEXTURE_WIDTH {}U\n",
+ webrender_build::MAX_VERTEX_TEXTURE_WIDTH);
+
+ let (basename, features) =
+ shader_key.split_at(shader_key.find(' ').unwrap_or(shader_key.len()));
+ if !features.is_empty() {
+ for feature in features.trim().split(',') {
+ let _ = write!(imported, "#define WR_FEATURE_{}\n", feature);
+ }
+ }
+
+ process_imports(shader_dir, basename, &mut HashSet::new(), &mut imported);
+
+ let shader = shader_file(shader_key);
+
+ let out_dir = std::env::var("OUT_DIR").unwrap();
+ let imp_name = format!("{}/{}.c", out_dir, shader);
+ std::fs::write(&imp_name, imported).unwrap();
+
+ let mut build = cc::Build::new();
+ build.no_default_flags(true);
+ if let Ok(tool) = build.try_get_compiler() {
+ if tool.is_like_msvc() {
+ build.flag("/EP");
+ if tool.path().to_str().map_or(false, |p| p.contains("clang")) {
+ build.flag("/clang:-undef");
+ } else {
+ build.flag("/u");
+ }
+ } else {
+ build.flag("-xc").flag("-P").flag("-undef");
+ }
+ }
+ // Use SWGLPP target to avoid pulling CFLAGS/CXXFLAGS.
+ build.target("SWGLPP");
+ build.file(&imp_name);
+ let vs = build.clone()
+ .define("WR_VERTEX_SHADER", Some("1"))
+ .expand();
+ let fs = build.clone()
+ .define("WR_FRAGMENT_SHADER", Some("1"))
+ .expand();
+ let vs_name = format!("{}/{}.vert", out_dir, shader);
+ let fs_name = format!("{}/{}.frag", out_dir, shader);
+ std::fs::write(&vs_name, vs).unwrap();
+ std::fs::write(&fs_name, fs).unwrap();
+
+ let args = vec![
+ "glsl_to_cxx".to_string(),
+ vs_name,
+ fs_name,
+ ];
+ let result = glsl_to_cxx::translate(&mut args.into_iter());
+ std::fs::write(format!("{}/{}.h", out_dir, shader), result).unwrap();
+}
+
+fn main() {
+ let shader_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap() + "/../webrender/res";
+
+ let shader_flags =
+ ShaderFeatureFlags::GL |
+ ShaderFeatureFlags::DUAL_SOURCE_BLENDING |
+ ShaderFeatureFlags::ADVANCED_BLEND_EQUATION |
+ ShaderFeatureFlags::DEBUG;
+ let mut shaders: Vec<String> = Vec::new();
+ for (name, features) in get_shader_features(shader_flags) {
+ shaders.extend(features.iter().map(|f| {
+ if f.is_empty() { name.to_owned() } else { format!("{} {}", name, f) }
+ }));
+ }
+
+ shaders.sort();
+
+ // We need to ensure that the C preprocessor does not pull compiler flags from
+ // the host or target environment. Set up a SWGLPP target with empty flags to
+ // work around this.
+ if let Ok(target) = std::env::var("TARGET") {
+ if let Ok(cc) = std::env::var(format!("CC_{}", target))
+ .or(std::env::var(format!("CC_{}", target.replace("-", "_")))) {
+ std::env::set_var("CC_SWGLPP", cc);
+ }
+ }
+ std::env::set_var("CFLAGS_SWGLPP", "");
+
+ for shader in &shaders {
+ translate_shader(shader, &shader_dir);
+ }
+
+ write_load_shader(&shaders);
+
+ println!("cargo:rerun-if-changed=src/blend.h");
+ println!("cargo:rerun-if-changed=src/composite.h");
+ println!("cargo:rerun-if-changed=src/gl_defs.h");
+ println!("cargo:rerun-if-changed=src/glsl.h");
+ println!("cargo:rerun-if-changed=src/program.h");
+ println!("cargo:rerun-if-changed=src/rasterize.h");
+ println!("cargo:rerun-if-changed=src/swgl_ext.h");
+ println!("cargo:rerun-if-changed=src/texture.h");
+ println!("cargo:rerun-if-changed=src/vector_type.h");
+ println!("cargo:rerun-if-changed=src/gl.cc");
+ let mut build = cc::Build::new();
+ build.cpp(true);
+
+ if let Ok(tool) = build.try_get_compiler() {
+ if tool.is_like_msvc() {
+ build.flag("/std:c++17")
+ .flag("/EHs-")
+ .flag("/GR-")
+ .flag("/UMOZILLA_CONFIG_H");
+ } else {
+ build.flag("-std=c++17")
+ .flag("-fno-exceptions")
+ .flag("-fno-rtti")
+ .flag("-fno-math-errno")
+ .flag("-UMOZILLA_CONFIG_H");
+ }
+ // SWGL relies heavily on inlining for performance so override -Oz with -O2
+ if tool.args().contains(&"-Oz".into()) {
+ build.flag("-O2");
+ }
+
+ // Most GLSL compilers assume something like fast-math so we turn it on.
+ // However, reciprocal division makes it so 1/1 = 0.999994 which can produce a lot of fuzz
+ // in reftests and the use of reciprocal instructions usually involves a refinement step
+ // which bloats our already bloated code. Further, our shader code is sufficiently parallel
+ // that we're more likely to be throughput bound vs latency bound. Having fewer
+ // instructions makes things easier on the processor and in places where it matters we can
+ // probably explicitly use reciprocal instructions and avoid the refinement step.
+ // Also, allow checks for non-finite values which fast-math may disable.
+ if tool.is_like_msvc() {
+ build.flag("/fp:fast")
+ .flag("-Xclang")
+ .flag("-mrecip=none")
+ .flag("/clang:-fno-finite-math-only");
+ } else if tool.is_like_clang() {
+ // gcc only supports -mrecip=none on some targets so to keep
+ // things simple we don't use -ffast-math with gcc at all
+ build.flag("-ffast-math")
+ .flag("-mrecip=none")
+ .flag("-fno-finite-math-only");
+ }
+ }
+
+ build.file("src/gl.cc")
+ .define("_GLIBCXX_USE_CXX11_ABI", Some("0"))
+ .include(shader_dir)
+ .include("src")
+ .include(std::env::var("OUT_DIR").unwrap())
+ .compile("gl_cc");
+}
diff --git a/gfx/wr/swgl/src/blend.h b/gfx/wr/swgl/src/blend.h
new file mode 100644
index 0000000000..8bc1c93994
--- /dev/null
+++ b/gfx/wr/swgl/src/blend.h
@@ -0,0 +1,864 @@
+/* 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/. */
+
+static ALWAYS_INLINE HalfRGBA8 packRGBA8(I32 a, I32 b) {
+#if USE_SSE2
+ return _mm_packs_epi32(a, b);
+#elif USE_NEON
+ return vcombine_u16(vqmovun_s32(a), vqmovun_s32(b));
+#else
+ return CONVERT(combine(a, b), HalfRGBA8);
+#endif
+}
+
+static ALWAYS_INLINE WideRGBA8 pack_pixels_RGBA8(const vec4& v,
+ float scale = 255.0f) {
+ ivec4 i = round_pixel(v, scale);
+ HalfRGBA8 xz = packRGBA8(i.z, i.x);
+ HalfRGBA8 yw = packRGBA8(i.y, i.w);
+ HalfRGBA8 xyzwl = zipLow(xz, yw);
+ HalfRGBA8 xyzwh = zipHigh(xz, yw);
+ HalfRGBA8 lo = zip2Low(xyzwl, xyzwh);
+ HalfRGBA8 hi = zip2High(xyzwl, xyzwh);
+ return combine(lo, hi);
+}
+
+static ALWAYS_INLINE WideRGBA8 pack_pixels_RGBA8(Float alpha,
+ float scale = 255.0f) {
+ I32 i = round_pixel(alpha, scale);
+ HalfRGBA8 c = packRGBA8(i, i);
+ c = zipLow(c, c);
+ return zip(c, c);
+}
+
+static ALWAYS_INLINE WideRGBA8 pack_pixels_RGBA8(float alpha,
+ float scale = 255.0f) {
+ I32 i = round_pixel(alpha, scale);
+ return repeat2(packRGBA8(i, i));
+}
+
+UNUSED static ALWAYS_INLINE WideRGBA8 pack_pixels_RGBA8(const vec4_scalar& v,
+ float scale = 255.0f) {
+ I32 i = round_pixel((Float){v.z, v.y, v.x, v.w}, scale);
+ return repeat2(packRGBA8(i, i));
+}
+
+static ALWAYS_INLINE WideRGBA8 pack_pixels_RGBA8() {
+ return pack_pixels_RGBA8(fragment_shader->gl_FragColor);
+}
+
+static ALWAYS_INLINE WideRGBA8 pack_pixels_RGBA8(WideRGBA32F v,
+ float scale = 255.0f) {
+ ivec4 i = round_pixel(bit_cast<vec4>(v), scale);
+ return combine(packRGBA8(i.x, i.y), packRGBA8(i.z, i.w));
+}
+
+static ALWAYS_INLINE WideR8 packR8(I32 a) {
+#if USE_SSE2
+ return lowHalf(bit_cast<V8<uint16_t>>(_mm_packs_epi32(a, a)));
+#elif USE_NEON
+ return vqmovun_s32(a);
+#else
+ return CONVERT(a, WideR8);
+#endif
+}
+
+static ALWAYS_INLINE WideR8 pack_pixels_R8(Float c, float scale = 255.0f) {
+ return packR8(round_pixel(c, scale));
+}
+
+static ALWAYS_INLINE WideR8 pack_pixels_R8() {
+ return pack_pixels_R8(fragment_shader->gl_FragColor.x);
+}
+
+// Load a partial span > 0 and < 4 pixels.
+template <typename V, typename P>
+static ALWAYS_INLINE V partial_load_span(const P* src, int span) {
+ return bit_cast<V>(
+ (span >= 2
+ ? combine(unaligned_load<V2<P>>(src),
+ V2<P>{span > 2 ? unaligned_load<P>(src + 2) : P(0), 0})
+ : V4<P>{unaligned_load<P>(src), 0, 0, 0}));
+}
+
+// Store a partial span > 0 and < 4 pixels.
+template <typename V, typename P>
+static ALWAYS_INLINE void partial_store_span(P* dst, V src, int span) {
+ auto pixels = bit_cast<V4<P>>(src);
+ if (span >= 2) {
+ unaligned_store(dst, lowHalf(pixels));
+ if (span > 2) {
+ unaligned_store(dst + 2, pixels.z);
+ }
+ } else {
+ unaligned_store(dst, pixels.x);
+ }
+}
+
+// Dispatcher that chooses when to load a full or partial span
+template <typename V, typename P>
+static ALWAYS_INLINE V load_span(const P* src, int span) {
+ if (span >= 4) {
+ return unaligned_load<V, P>(src);
+ } else {
+ return partial_load_span<V, P>(src, span);
+ }
+}
+
+// Dispatcher that chooses when to store a full or partial span
+template <typename V, typename P>
+static ALWAYS_INLINE void store_span(P* dst, V src, int span) {
+ if (span >= 4) {
+ unaligned_store<V, P>(dst, src);
+ } else {
+ partial_store_span<V, P>(dst, src, span);
+ }
+}
+
+template <typename T>
+static ALWAYS_INLINE T muldiv256(T x, T y) {
+ return (x * y) >> 8;
+}
+
+// (x*y + x) >> 8, cheap approximation of (x*y) / 255
+template <typename T>
+static ALWAYS_INLINE T muldiv255(T x, T y) {
+ return (x * y + x) >> 8;
+}
+
+template <typename V>
+static ALWAYS_INLINE WideRGBA8 pack_span(uint32_t*, const V& v,
+ float scale = 255.0f) {
+ return pack_pixels_RGBA8(v, scale);
+}
+
+template <typename C>
+static ALWAYS_INLINE WideR8 pack_span(uint8_t*, C c, float scale = 255.0f) {
+ return pack_pixels_R8(c, scale);
+}
+
+// Helper functions to apply a color modulus when available.
+struct NoColor {};
+
+template <typename P>
+static ALWAYS_INLINE P applyColor(P src, NoColor) {
+ return src;
+}
+
+struct InvertColor {};
+
+template <typename P>
+static ALWAYS_INLINE P applyColor(P src, InvertColor) {
+ return 255 - src;
+}
+
+template <typename P>
+static ALWAYS_INLINE P applyColor(P src, P color) {
+ return muldiv255(color, src);
+}
+
+static ALWAYS_INLINE WideRGBA8 applyColor(PackedRGBA8 src, WideRGBA8 color) {
+ return applyColor(unpack(src), color);
+}
+
+template <typename P, typename C>
+static ALWAYS_INLINE auto packColor(P* buf, C color) {
+ return pack_span(buf, color, 255.0f);
+}
+
+template <typename P>
+static ALWAYS_INLINE NoColor packColor(UNUSED P* buf, NoColor noColor) {
+ return noColor;
+}
+
+template <typename P>
+static ALWAYS_INLINE InvertColor packColor(UNUSED P* buf,
+ InvertColor invertColor) {
+ return invertColor;
+}
+
+// Single argument variation that takes an explicit destination buffer type.
+template <typename P, typename C>
+static ALWAYS_INLINE auto packColor(C color) {
+ // Just pass in a typed null pointer, as the pack routines never use the
+ // pointer's value, just its type.
+ return packColor((P*)0, color);
+}
+
+// Byte-wise addition for when x or y is a signed 8-bit value stored in the
+// low byte of a larger type T only with zeroed-out high bits, where T is
+// greater than 8 bits, i.e. uint16_t. This can result when muldiv255 is used
+// upon signed operands, using up all the precision in a 16 bit integer, and
+// potentially losing the sign bit in the last >> 8 shift. Due to the
+// properties of two's complement arithmetic, even though we've discarded the
+// sign bit, we can still represent a negative number under addition (without
+// requiring any extra sign bits), just that any negative number will behave
+// like a large unsigned number under addition, generating a single carry bit
+// on overflow that we need to discard. Thus, just doing a byte-wise add will
+// overflow without the troublesome carry, giving us only the remaining 8 low
+// bits we actually need while keeping the high bits at zero.
+template <typename T>
+static ALWAYS_INLINE T addlow(T x, T y) {
+ typedef VectorType<uint8_t, sizeof(T)> bytes;
+ return bit_cast<T>(bit_cast<bytes>(x) + bit_cast<bytes>(y));
+}
+
+// Replace color components of each pixel with the pixel's alpha values.
+template <typename T>
+static ALWAYS_INLINE T alphas(T c) {
+ return SHUFFLE(c, c, 3, 3, 3, 3, 7, 7, 7, 7, 11, 11, 11, 11, 15, 15, 15, 15);
+}
+
+// Replace the alpha values of the first vector with alpha values from the
+// second, while leaving the color components unmodified.
+template <typename T>
+static ALWAYS_INLINE T set_alphas(T c, T a) {
+ return SHUFFLE(c, a, 0, 1, 2, 19, 4, 5, 6, 23, 8, 9, 10, 27, 12, 13, 14, 31);
+}
+
+// Miscellaneous helper functions for working with packed RGBA8 data.
+static ALWAYS_INLINE HalfRGBA8 if_then_else(V8<int16_t> c, HalfRGBA8 t,
+ HalfRGBA8 e) {
+ return bit_cast<HalfRGBA8>((c & t) | (~c & e));
+}
+
+template <typename T, typename C, int N>
+static ALWAYS_INLINE VectorType<T, N> if_then_else(VectorType<C, N> c,
+ VectorType<T, N> t,
+ VectorType<T, N> e) {
+ return combine(if_then_else(lowHalf(c), lowHalf(t), lowHalf(e)),
+ if_then_else(highHalf(c), highHalf(t), highHalf(e)));
+}
+
+static ALWAYS_INLINE HalfRGBA8 min(HalfRGBA8 x, HalfRGBA8 y) {
+#if USE_SSE2
+ return bit_cast<HalfRGBA8>(
+ _mm_min_epi16(bit_cast<V8<int16_t>>(x), bit_cast<V8<int16_t>>(y)));
+#elif USE_NEON
+ return vminq_u16(x, y);
+#else
+ return if_then_else(x < y, x, y);
+#endif
+}
+
+template <typename T, int N>
+static ALWAYS_INLINE VectorType<T, N> min(VectorType<T, N> x,
+ VectorType<T, N> y) {
+ return combine(min(lowHalf(x), lowHalf(y)), min(highHalf(x), highHalf(y)));
+}
+
+static ALWAYS_INLINE HalfRGBA8 max(HalfRGBA8 x, HalfRGBA8 y) {
+#if USE_SSE2
+ return bit_cast<HalfRGBA8>(
+ _mm_max_epi16(bit_cast<V8<int16_t>>(x), bit_cast<V8<int16_t>>(y)));
+#elif USE_NEON
+ return vmaxq_u16(x, y);
+#else
+ return if_then_else(x > y, x, y);
+#endif
+}
+
+template <typename T, int N>
+static ALWAYS_INLINE VectorType<T, N> max(VectorType<T, N> x,
+ VectorType<T, N> y) {
+ return combine(max(lowHalf(x), lowHalf(y)), max(highHalf(x), highHalf(y)));
+}
+
+template <typename T, int N>
+static ALWAYS_INLINE VectorType<T, N> recip(VectorType<T, N> v) {
+ return combine(recip(lowHalf(v)), recip(highHalf(v)));
+}
+
+// Helper to get the reciprocal if the value is non-zero, or otherwise default
+// to the supplied fallback value.
+template <typename V>
+static ALWAYS_INLINE V recip_or(V v, float f) {
+ return if_then_else(v != V(0.0f), recip(v), V(f));
+}
+
+template <typename T, int N>
+static ALWAYS_INLINE VectorType<T, N> inversesqrt(VectorType<T, N> v) {
+ return combine(inversesqrt(lowHalf(v)), inversesqrt(highHalf(v)));
+}
+
+// Extract the alpha components so that we can cheaply calculate the reciprocal
+// on a single SIMD register. Then multiply the duplicated alpha reciprocal with
+// the pixel data. 0 alpha is treated as transparent black.
+static ALWAYS_INLINE WideRGBA32F unpremultiply(WideRGBA32F v) {
+ Float a = recip_or((Float){v[3], v[7], v[11], v[15]}, 0.0f);
+ return v * a.xxxxyyyyzzzzwwww;
+}
+
+// Packed RGBA32F data is AoS in BGRA order. Transpose it to SoA and swizzle to
+// RGBA to unpack.
+static ALWAYS_INLINE vec4 unpack(PackedRGBA32F c) {
+ return bit_cast<vec4>(
+ SHUFFLE(c, c, 2, 6, 10, 14, 1, 5, 9, 13, 0, 4, 8, 12, 3, 7, 11, 15));
+}
+
+// The following lum/sat functions mostly follow the KHR_blend_equation_advanced
+// specification but are rearranged to work on premultiplied data.
+static ALWAYS_INLINE Float lumv3(vec3 v) {
+ return v.x * 0.30f + v.y * 0.59f + v.z * 0.11f;
+}
+
+static ALWAYS_INLINE Float minv3(vec3 v) { return min(min(v.x, v.y), v.z); }
+
+static ALWAYS_INLINE Float maxv3(vec3 v) { return max(max(v.x, v.y), v.z); }
+
+static inline vec3 clip_color(vec3 v, Float lum, Float alpha) {
+ Float mincol = max(-minv3(v), lum);
+ Float maxcol = max(maxv3(v), alpha - lum);
+ return lum + v * (lum * (alpha - lum) * recip_or(mincol * maxcol, 0.0f));
+}
+
+static inline vec3 set_lum(vec3 base, vec3 ref, Float alpha) {
+ return clip_color(base - lumv3(base), lumv3(ref), alpha);
+}
+
+static inline vec3 set_lum_sat(vec3 base, vec3 sref, vec3 lref, Float alpha) {
+ vec3 diff = base - minv3(base);
+ Float sbase = maxv3(diff);
+ Float ssat = maxv3(sref) - minv3(sref);
+ // The sbase range is rescaled to ssat. If sbase has 0 extent, then rescale
+ // to black, as per specification.
+ return set_lum(diff * ssat * recip_or(sbase, 0.0f), lref, alpha);
+}
+
+// Flags the reflect the current blend-stage clipping to be applied.
+enum SWGLClipFlag {
+ SWGL_CLIP_FLAG_MASK = 1 << 0,
+ SWGL_CLIP_FLAG_AA = 1 << 1,
+ SWGL_CLIP_FLAG_BLEND_OVERRIDE = 1 << 2,
+};
+static int swgl_ClipFlags = 0;
+static BlendKey swgl_BlendOverride = BLEND_KEY_NONE;
+static WideRGBA8 swgl_BlendColorRGBA8 = {0};
+static WideRGBA8 swgl_BlendAlphaRGBA8 = {0};
+
+// A pointer into the color buffer for the start of the span.
+static void* swgl_SpanBuf = nullptr;
+// A pointer into the clip mask for the start of the span.
+static uint8_t* swgl_ClipMaskBuf = nullptr;
+
+static ALWAYS_INLINE WideR8 expand_mask(UNUSED uint8_t* buf, WideR8 mask) {
+ return mask;
+}
+static ALWAYS_INLINE WideRGBA8 expand_mask(UNUSED uint32_t* buf, WideR8 mask) {
+ WideRG8 maskRG = zip(mask, mask);
+ return zip(maskRG, maskRG);
+}
+
+// Loads a chunk of clip masks. The current pointer into the color buffer is
+// used to reconstruct the relative position within the span. From there, the
+// pointer into the clip mask can be generated from the start of the clip mask
+// span.
+template <typename P>
+static ALWAYS_INLINE uint8_t* get_clip_mask(P* buf) {
+ return &swgl_ClipMaskBuf[buf - (P*)swgl_SpanBuf];
+}
+
+template <typename P>
+static ALWAYS_INLINE auto load_clip_mask(P* buf, int span)
+ -> decltype(expand_mask(buf, 0)) {
+ return expand_mask(buf,
+ unpack(load_span<PackedR8>(get_clip_mask(buf), span)));
+}
+
+// Temporarily removes masking from the blend stage, assuming the caller will
+// handle it.
+static ALWAYS_INLINE void override_clip_mask() {
+ blend_key = BlendKey(blend_key - MASK_BLEND_KEY_NONE);
+}
+
+// Restores masking to the blend stage, assuming it was previously overridden.
+static ALWAYS_INLINE void restore_clip_mask() {
+ blend_key = BlendKey(MASK_BLEND_KEY_NONE + blend_key);
+}
+
+// A pointer to the start of the opaque destination region of the span for AA.
+static const uint8_t* swgl_OpaqueStart = nullptr;
+// The size, in bytes, of the opaque region.
+static uint32_t swgl_OpaqueSize = 0;
+// AA coverage distance offsets for the left and right edges.
+static Float swgl_LeftAADist = 0.0f;
+static Float swgl_RightAADist = 0.0f;
+// AA coverage slope values used for accumulating coverage for each step.
+static Float swgl_AASlope = 0.0f;
+
+// Get the amount of pixels we need to process before the start of the opaque
+// region.
+template <typename P>
+static ALWAYS_INLINE int get_aa_opaque_start(P* buf) {
+ return max(int((P*)swgl_OpaqueStart - buf), 0);
+}
+
+// Assuming we are already in the opaque part of the span, return the remaining
+// size of the opaque part.
+template <typename P>
+static ALWAYS_INLINE int get_aa_opaque_size(P* buf) {
+ return max(int((P*)&swgl_OpaqueStart[swgl_OpaqueSize] - buf), 0);
+}
+
+// Temporarily removes anti-aliasing from the blend stage, assuming the caller
+// will handle it.
+static ALWAYS_INLINE void override_aa() {
+ blend_key = BlendKey(blend_key - AA_BLEND_KEY_NONE);
+}
+
+// Restores anti-aliasing to the blend stage, assuming it was previously
+// overridden.
+static ALWAYS_INLINE void restore_aa() {
+ blend_key = BlendKey(AA_BLEND_KEY_NONE + blend_key);
+}
+
+static PREFER_INLINE WideRGBA8 blend_pixels(uint32_t* buf, PackedRGBA8 pdst,
+ WideRGBA8 src, int span = 4) {
+ WideRGBA8 dst = unpack(pdst);
+ const WideRGBA8 RGB_MASK = {0xFFFF, 0xFFFF, 0xFFFF, 0, 0xFFFF, 0xFFFF,
+ 0xFFFF, 0, 0xFFFF, 0xFFFF, 0xFFFF, 0,
+ 0xFFFF, 0xFFFF, 0xFFFF, 0};
+ const WideRGBA8 ALPHA_MASK = {0, 0, 0, 0xFFFF, 0, 0, 0, 0xFFFF,
+ 0, 0, 0, 0xFFFF, 0, 0, 0, 0xFFFF};
+ const WideRGBA8 ALPHA_OPAQUE = {0, 0, 0, 255, 0, 0, 0, 255,
+ 0, 0, 0, 255, 0, 0, 0, 255};
+
+// clang-format off
+ // Computes AA for the given pixel based on the offset of the pixel within
+ // destination row. Given the initial coverage offsets for the left and right
+ // edges, the offset is scaled by the slope and accumulated to find the
+ // minimum coverage value for the pixel. A final weight is generated that
+ // can be used to scale the source pixel.
+#define DO_AA(format, body) \
+ do { \
+ int offset = int((const uint8_t*)buf - swgl_OpaqueStart); \
+ if (uint32_t(offset) >= swgl_OpaqueSize) { \
+ Float delta = swgl_AASlope * float(offset); \
+ Float dist = clamp(min(swgl_LeftAADist + delta.x, \
+ swgl_RightAADist + delta.y), \
+ 0.0f, 256.0f); \
+ auto aa = pack_pixels_##format(dist, 1.0f); \
+ body; \
+ } \
+ } while (0)
+
+ // Each blend case is preceded by the MASK_ variant. The MASK_ case first
+ // loads the mask values and multiplies the source value by them. After, it
+ // falls through to the normal blending case using the masked source. The
+ // AA_ variations may further precede the blend cases, in which case the
+ // source value is further modified before use.
+#define BLEND_CASE_KEY(key) \
+ case AA_##key: \
+ DO_AA(RGBA8, src = muldiv256(src, aa)); \
+ goto key; \
+ case AA_MASK_##key: \
+ DO_AA(RGBA8, src = muldiv256(src, aa)); \
+ FALLTHROUGH; \
+ case MASK_##key: \
+ src = muldiv255(src, load_clip_mask(buf, span)); \
+ FALLTHROUGH; \
+ case key: key
+
+#define BLEND_CASE(...) BLEND_CASE_KEY(BLEND_KEY(__VA_ARGS__))
+
+ switch (blend_key) {
+ BLEND_CASE(GL_ONE, GL_ZERO):
+ return src;
+ BLEND_CASE(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE,
+ GL_ONE_MINUS_SRC_ALPHA):
+ // dst + src.a*(src.rgb1 - dst)
+ // use addlow for signed overflow
+ return addlow(dst, muldiv255(alphas(src), (src | ALPHA_OPAQUE) - dst));
+ BLEND_CASE(GL_ONE, GL_ONE_MINUS_SRC_ALPHA):
+ return src + dst - muldiv255(dst, alphas(src));
+ BLEND_CASE(GL_ZERO, GL_ONE_MINUS_SRC_COLOR):
+ return dst - muldiv255(dst, src);
+ BLEND_CASE(GL_ZERO, GL_ONE_MINUS_SRC_COLOR, GL_ZERO, GL_ONE):
+ return dst - (muldiv255(dst, src) & RGB_MASK);
+ BLEND_CASE(GL_ZERO, GL_ONE_MINUS_SRC_ALPHA):
+ return dst - muldiv255(dst, alphas(src));
+ BLEND_CASE(GL_ZERO, GL_SRC_COLOR):
+ return muldiv255(src, dst);
+ BLEND_CASE(GL_ONE, GL_ONE):
+ return src + dst;
+ BLEND_CASE(GL_ONE, GL_ONE, GL_ONE, GL_ONE_MINUS_SRC_ALPHA):
+ return src + dst - (muldiv255(dst, src) & ALPHA_MASK);
+ BLEND_CASE(GL_ONE_MINUS_DST_ALPHA, GL_ONE, GL_ZERO, GL_ONE):
+ // src*(1-dst.a) + dst*1 = src - src*dst.a + dst
+ return dst + ((src - muldiv255(src, alphas(dst))) & RGB_MASK);
+ BLEND_CASE(GL_CONSTANT_COLOR, GL_ONE_MINUS_SRC_COLOR):
+ // src*k + (1-src)*dst = src*k + dst -
+ // src*dst = dst + src*(k - dst) use addlow
+ // for signed overflow
+ return addlow(
+ dst, muldiv255(src, repeat2(ctx->blendcolor) - dst));
+
+ // We must explicitly handle the masked/anti-aliased secondary blend case.
+ // The secondary color as well as the source must be multiplied by the
+ // weights.
+ case BLEND_KEY(GL_ONE, GL_ONE_MINUS_SRC1_COLOR): {
+ WideRGBA8 secondary =
+ applyColor(dst,
+ packColor<uint32_t>(fragment_shader->gl_SecondaryFragColor));
+ return src + dst - secondary;
+ }
+ case MASK_BLEND_KEY(GL_ONE, GL_ONE_MINUS_SRC1_COLOR): {
+ WideRGBA8 secondary =
+ applyColor(dst,
+ packColor<uint32_t>(fragment_shader->gl_SecondaryFragColor));
+ WideRGBA8 mask = load_clip_mask(buf, span);
+ return muldiv255(src, mask) + dst - muldiv255(secondary, mask);
+ }
+ case AA_BLEND_KEY(GL_ONE, GL_ONE_MINUS_SRC1_COLOR): {
+ WideRGBA8 secondary =
+ applyColor(dst,
+ packColor<uint32_t>(fragment_shader->gl_SecondaryFragColor));
+ DO_AA(RGBA8, {
+ src = muldiv256(src, aa);
+ secondary = muldiv256(secondary, aa);
+ });
+ return src + dst - secondary;
+ }
+ case AA_MASK_BLEND_KEY(GL_ONE, GL_ONE_MINUS_SRC1_COLOR): {
+ WideRGBA8 secondary =
+ applyColor(dst,
+ packColor<uint32_t>(fragment_shader->gl_SecondaryFragColor));
+ WideRGBA8 mask = load_clip_mask(buf, span);
+ DO_AA(RGBA8, mask = muldiv256(mask, aa));
+ return muldiv255(src, mask) + dst - muldiv255(secondary, mask);
+ }
+
+ BLEND_CASE(GL_MIN):
+ return min(src, dst);
+ BLEND_CASE(GL_MAX):
+ return max(src, dst);
+
+ // The KHR_blend_equation_advanced spec describes the blend equations such
+ // that the unpremultiplied values Cs, Cd, As, Ad and function f combine to
+ // the result:
+ // Cr = f(Cs,Cd)*As*Ad + Cs*As*(1-Ad) + Cd*AD*(1-As)
+ // Ar = As*Ad + As*(1-Ad) + Ad*(1-As)
+ // However, working with unpremultiplied values requires expensive math to
+ // unpremultiply and premultiply again during blending. We can use the fact
+ // that premultiplied value P = C*A and simplify the equations such that no
+ // unpremultiplied colors are necessary, allowing us to stay with integer
+ // math that avoids floating-point conversions in the common case. Some of
+ // the blend modes require division or sqrt, in which case we do convert
+ // to (possibly transposed/unpacked) floating-point to implement the mode.
+ // However, most common modes can still use cheaper premultiplied integer
+ // math. As an example, the multiply mode f(Cs,Cd) = Cs*Cd is simplified
+ // to:
+ // Cr = Cs*Cd*As*Ad + Cs*As*(1-Ad) + Cd*Ad*(1-As)
+ // .. Pr = Ps*Pd + Ps - Ps*Ad + Pd - Pd*As
+ // Ar = As*Ad + As - As*Ad + Ad - Ad*As
+ // .. Ar = As + Ad - As*Ad
+ // Note that the alpha equation is the same for all blend equations, such
+ // that so long as the implementation results in As + Ad - As*Ad, we can
+ // avoid using separate instructions to compute the alpha result, which is
+ // dependent on the math used to implement each blend mode. The exact
+ // reductions used to get the final math for every blend mode are too
+ // involved to show here in comments, but mostly follows from replacing
+ // Cs*As and Cd*Ad with Ps and Ps while factoring out as many common terms
+ // as possible.
+
+ BLEND_CASE(GL_MULTIPLY_KHR): {
+ WideRGBA8 diff = muldiv255(alphas(src) - (src & RGB_MASK),
+ alphas(dst) - (dst & RGB_MASK));
+ return src + dst + (diff & RGB_MASK) - alphas(diff);
+ }
+ BLEND_CASE(GL_SCREEN_KHR):
+ return src + dst - muldiv255(src, dst);
+ BLEND_CASE(GL_OVERLAY_KHR): {
+ WideRGBA8 srcA = alphas(src);
+ WideRGBA8 dstA = alphas(dst);
+ WideRGBA8 diff = muldiv255(src, dst) + muldiv255(srcA - src, dstA - dst);
+ return src + dst +
+ if_then_else(dst * 2 <= dstA, (diff & RGB_MASK) - alphas(diff),
+ -diff);
+ }
+ BLEND_CASE(GL_DARKEN_KHR):
+ return src + dst -
+ max(muldiv255(src, alphas(dst)), muldiv255(dst, alphas(src)));
+ BLEND_CASE(GL_LIGHTEN_KHR):
+ return src + dst -
+ min(muldiv255(src, alphas(dst)), muldiv255(dst, alphas(src)));
+
+ BLEND_CASE(GL_COLORDODGE_KHR): {
+ // Color-dodge and color-burn require division, so we convert to FP math
+ // here, but avoid transposing to a vec4.
+ WideRGBA32F srcF = CONVERT(src, WideRGBA32F);
+ WideRGBA32F srcA = alphas(srcF);
+ WideRGBA32F dstF = CONVERT(dst, WideRGBA32F);
+ WideRGBA32F dstA = alphas(dstF);
+ return pack_pixels_RGBA8(
+ srcA * set_alphas(
+ min(dstA, dstF * srcA * recip_or(srcA - srcF, 255.0f)),
+ dstF) +
+ srcF * (255.0f - dstA) + dstF * (255.0f - srcA),
+ 1.0f / 255.0f);
+ }
+ BLEND_CASE(GL_COLORBURN_KHR): {
+ WideRGBA32F srcF = CONVERT(src, WideRGBA32F);
+ WideRGBA32F srcA = alphas(srcF);
+ WideRGBA32F dstF = CONVERT(dst, WideRGBA32F);
+ WideRGBA32F dstA = alphas(dstF);
+ return pack_pixels_RGBA8(
+ srcA * set_alphas((dstA - min(dstA, (dstA - dstF) * srcA *
+ recip_or(srcF, 255.0f))),
+ dstF) +
+ srcF * (255.0f - dstA) + dstF * (255.0f - srcA),
+ 1.0f / 255.0f);
+ }
+ BLEND_CASE(GL_HARDLIGHT_KHR): {
+ WideRGBA8 srcA = alphas(src);
+ WideRGBA8 dstA = alphas(dst);
+ WideRGBA8 diff = muldiv255(src, dst) + muldiv255(srcA - src, dstA - dst);
+ return src + dst +
+ if_then_else(src * 2 <= srcA, (diff & RGB_MASK) - alphas(diff),
+ -diff);
+ }
+
+ BLEND_CASE(GL_SOFTLIGHT_KHR): {
+ // Soft-light requires an unpremultiply that can't be factored out as
+ // well as a sqrt, so we convert to FP math here, but avoid transposing
+ // to a vec4.
+ WideRGBA32F srcF = CONVERT(src, WideRGBA32F);
+ WideRGBA32F srcA = alphas(srcF);
+ WideRGBA32F dstF = CONVERT(dst, WideRGBA32F);
+ WideRGBA32F dstA = alphas(dstF);
+ WideRGBA32F dstU = unpremultiply(dstF);
+ WideRGBA32F scale = srcF + srcF - srcA;
+ return pack_pixels_RGBA8(
+ dstF * (255.0f +
+ set_alphas(
+ scale *
+ if_then_else(scale < 0.0f, 1.0f - dstU,
+ min((16.0f * dstU - 12.0f) * dstU + 3.0f,
+ inversesqrt(dstU) - 1.0f)),
+ WideRGBA32F(0.0f))) +
+ srcF * (255.0f - dstA),
+ 1.0f / 255.0f);
+ }
+ BLEND_CASE(GL_DIFFERENCE_KHR): {
+ WideRGBA8 diff =
+ min(muldiv255(dst, alphas(src)), muldiv255(src, alphas(dst)));
+ return src + dst - diff - (diff & RGB_MASK);
+ }
+ BLEND_CASE(GL_EXCLUSION_KHR): {
+ WideRGBA8 diff = muldiv255(src, dst);
+ return src + dst - diff - (diff & RGB_MASK);
+ }
+
+ // The HSL blend modes are non-separable and require complicated use of
+ // division. It is advantageous to convert to FP and transpose to vec4
+ // math to more easily manipulate the individual color components.
+#define DO_HSL(rgb) \
+ do { \
+ vec4 srcV = unpack(CONVERT(src, PackedRGBA32F)); \
+ vec4 dstV = unpack(CONVERT(dst, PackedRGBA32F)); \
+ Float srcA = srcV.w * (1.0f / 255.0f); \
+ Float dstA = dstV.w * (1.0f / 255.0f); \
+ Float srcDstA = srcV.w * dstA; \
+ vec3 srcC = vec3(srcV) * dstA; \
+ vec3 dstC = vec3(dstV) * srcA; \
+ return pack_pixels_RGBA8(vec4(rgb + vec3(srcV) - srcC + vec3(dstV) - dstC, \
+ srcV.w + dstV.w - srcDstA), \
+ 1.0f); \
+ } while (0)
+
+ BLEND_CASE(GL_HSL_HUE_KHR):
+ DO_HSL(set_lum_sat(srcC, dstC, dstC, srcDstA));
+ BLEND_CASE(GL_HSL_SATURATION_KHR):
+ DO_HSL(set_lum_sat(dstC, srcC, dstC, srcDstA));
+ BLEND_CASE(GL_HSL_COLOR_KHR):
+ DO_HSL(set_lum(srcC, dstC, srcDstA));
+ BLEND_CASE(GL_HSL_LUMINOSITY_KHR):
+ DO_HSL(set_lum(dstC, srcC, srcDstA));
+
+ // SWGL-specific extended blend modes.
+ BLEND_CASE(SWGL_BLEND_DROP_SHADOW): {
+ // Premultiplied alpha over blend, but with source color set to source alpha
+ // modulated with a constant color.
+ WideRGBA8 color = applyColor(alphas(src), swgl_BlendColorRGBA8);
+ return color + dst - muldiv255(dst, alphas(color));
+ }
+
+ BLEND_CASE(SWGL_BLEND_SUBPIXEL_TEXT):
+ // Premultiplied alpha over blend, but treats the source as a subpixel mask
+ // modulated with a constant color.
+ return applyColor(src, swgl_BlendColorRGBA8) + dst -
+ muldiv255(dst, applyColor(src, swgl_BlendAlphaRGBA8));
+
+ default:
+ UNREACHABLE;
+ // return src;
+ }
+
+#undef BLEND_CASE
+#undef BLEND_CASE_KEY
+ // clang-format on
+}
+
+static PREFER_INLINE WideR8 blend_pixels(uint8_t* buf, WideR8 dst, WideR8 src,
+ int span = 4) {
+// clang-format off
+#define BLEND_CASE_KEY(key) \
+ case AA_##key: \
+ DO_AA(R8, src = muldiv256(src, aa)); \
+ goto key; \
+ case AA_MASK_##key: \
+ DO_AA(R8, src = muldiv256(src, aa)); \
+ FALLTHROUGH; \
+ case MASK_##key: \
+ src = muldiv255(src, load_clip_mask(buf, span)); \
+ FALLTHROUGH; \
+ case key: key
+
+#define BLEND_CASE(...) BLEND_CASE_KEY(BLEND_KEY(__VA_ARGS__))
+
+ switch (blend_key) {
+ BLEND_CASE(GL_ONE, GL_ZERO):
+ return src;
+ BLEND_CASE(GL_ZERO, GL_SRC_COLOR):
+ return muldiv255(src, dst);
+ BLEND_CASE(GL_ONE, GL_ONE):
+ return src + dst;
+ default:
+ UNREACHABLE;
+ // return src;
+ }
+
+#undef BLEND_CASE
+#undef BLEND_CASE_KEY
+ // clang-format on
+}
+
+static ALWAYS_INLINE void commit_span(uint32_t* buf, WideRGBA8 r) {
+ unaligned_store(buf, pack(r));
+}
+
+static ALWAYS_INLINE void commit_span(uint32_t* buf, WideRGBA8 r, int len) {
+ partial_store_span(buf, pack(r), len);
+}
+
+static ALWAYS_INLINE WideRGBA8 blend_span(uint32_t* buf, WideRGBA8 r) {
+ return blend_pixels(buf, unaligned_load<PackedRGBA8>(buf), r);
+}
+
+static ALWAYS_INLINE WideRGBA8 blend_span(uint32_t* buf, WideRGBA8 r, int len) {
+ return blend_pixels(buf, partial_load_span<PackedRGBA8>(buf, len), r, len);
+}
+
+static ALWAYS_INLINE void commit_span(uint32_t* buf, PackedRGBA8 r) {
+ unaligned_store(buf, r);
+}
+
+static ALWAYS_INLINE void commit_span(uint32_t* buf, PackedRGBA8 r, int len) {
+ partial_store_span(buf, r, len);
+}
+
+static ALWAYS_INLINE PackedRGBA8 blend_span(uint32_t* buf, PackedRGBA8 r) {
+ return pack(blend_span(buf, unpack(r)));
+}
+
+static ALWAYS_INLINE PackedRGBA8 blend_span(uint32_t* buf, PackedRGBA8 r,
+ int len) {
+ return pack(blend_span(buf, unpack(r), len));
+}
+
+static ALWAYS_INLINE void commit_span(uint8_t* buf, WideR8 r) {
+ unaligned_store(buf, pack(r));
+}
+
+static ALWAYS_INLINE void commit_span(uint8_t* buf, WideR8 r, int len) {
+ partial_store_span(buf, pack(r), len);
+}
+
+static ALWAYS_INLINE WideR8 blend_span(uint8_t* buf, WideR8 r) {
+ return blend_pixels(buf, unpack(unaligned_load<PackedR8>(buf)), r);
+}
+
+static ALWAYS_INLINE WideR8 blend_span(uint8_t* buf, WideR8 r, int len) {
+ return blend_pixels(buf, unpack(partial_load_span<PackedR8>(buf, len)), r,
+ len);
+}
+
+static ALWAYS_INLINE void commit_span(uint8_t* buf, PackedR8 r) {
+ unaligned_store(buf, r);
+}
+
+static ALWAYS_INLINE void commit_span(uint8_t* buf, PackedR8 r, int len) {
+ partial_store_span(buf, r, len);
+}
+
+static ALWAYS_INLINE PackedR8 blend_span(uint8_t* buf, PackedR8 r) {
+ return pack(blend_span(buf, unpack(r)));
+}
+
+static ALWAYS_INLINE PackedR8 blend_span(uint8_t* buf, PackedR8 r, int len) {
+ return pack(blend_span(buf, unpack(r), len));
+}
+
+template <bool BLEND, typename P, typename R>
+static ALWAYS_INLINE void commit_blend_span(P* buf, R r) {
+ if (BLEND) {
+ commit_span(buf, blend_span(buf, r));
+ } else {
+ commit_span(buf, r);
+ }
+}
+
+template <bool BLEND, typename P, typename R>
+static ALWAYS_INLINE void commit_blend_span(P* buf, R r, int len) {
+ if (BLEND) {
+ commit_span(buf, blend_span(buf, r, len), len);
+ } else {
+ commit_span(buf, r, len);
+ }
+}
+
+template <typename P, typename R>
+static ALWAYS_INLINE void commit_blend_solid_span(P* buf, R r, int len) {
+ for (P* end = &buf[len & ~3]; buf < end; buf += 4) {
+ commit_span(buf, blend_span(buf, r));
+ }
+ len &= 3;
+ if (len > 0) {
+ partial_store_span(buf, pack(blend_span(buf, r, len)), len);
+ }
+}
+
+template <bool BLEND>
+static void commit_solid_span(uint32_t* buf, WideRGBA8 r, int len) {
+ commit_blend_solid_span(buf, r, len);
+}
+
+template <>
+ALWAYS_INLINE void commit_solid_span<false>(uint32_t* buf, WideRGBA8 r,
+ int len) {
+ fill_n(buf, len, bit_cast<U32>(pack(r)).x);
+}
+
+template <bool BLEND>
+static void commit_solid_span(uint8_t* buf, WideR8 r, int len) {
+ commit_blend_solid_span(buf, r, len);
+}
+
+template <>
+ALWAYS_INLINE void commit_solid_span<false>(uint8_t* buf, WideR8 r, int len) {
+ PackedR8 p = pack(r);
+ if (uintptr_t(buf) & 3) {
+ int align = 4 - (uintptr_t(buf) & 3);
+ align = min(align, len);
+ partial_store_span(buf, p, align);
+ buf += align;
+ len -= align;
+ }
+ fill_n((uint32_t*)buf, len / 4, bit_cast<uint32_t>(p));
+ buf += len & ~3;
+ len &= 3;
+ if (len > 0) {
+ partial_store_span(buf, p, len);
+ }
+}
diff --git a/gfx/wr/swgl/src/composite.h b/gfx/wr/swgl/src/composite.h
new file mode 100644
index 0000000000..70acabeca4
--- /dev/null
+++ b/gfx/wr/swgl/src/composite.h
@@ -0,0 +1,1386 @@
+/* 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/. */
+
+// Converts a pixel from a source format to a destination format. By default,
+// just return the value unchanged as for a simple copy.
+template <typename P, typename U>
+static ALWAYS_INLINE P convert_pixel(U src) {
+ return src;
+}
+
+// R8 format maps to BGRA value 0,0,R,1. The byte order is endian independent,
+// but the shifts unfortunately depend on endianness.
+template <>
+ALWAYS_INLINE uint32_t convert_pixel<uint32_t>(uint8_t src) {
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+ return (uint32_t(src) << 16) | 0xFF000000;
+#else
+ return (uint32_t(src) << 8) | 0x000000FF;
+#endif
+}
+
+// RG8 format maps to BGRA value 0,G,R,1.
+template <>
+ALWAYS_INLINE uint32_t convert_pixel<uint32_t>(uint16_t src) {
+ uint32_t rg = src;
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+ return ((rg & 0x00FF) << 16) | (rg & 0xFF00) | 0xFF000000;
+#else
+ return (rg & 0xFF00) | ((rg & 0x00FF) << 16) | 0x000000FF;
+#endif
+}
+
+// RGBA8 format maps to R.
+template <>
+ALWAYS_INLINE uint8_t convert_pixel<uint8_t>(uint32_t src) {
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+ return (src >> 16) & 0xFF;
+#else
+ return (src >> 8) & 0xFF;
+#endif
+}
+
+// RGBA8 formats maps to R,G.
+template <>
+ALWAYS_INLINE uint16_t convert_pixel<uint16_t>(uint32_t src) {
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+ return ((src >> 16) & 0x00FF) | (src & 0xFF00);
+#else
+ return (src & 0xFF00) | ((src >> 16) & 0x00FF);
+#endif
+}
+
+// R8 format maps to R,0.
+template <>
+ALWAYS_INLINE uint16_t convert_pixel<uint16_t>(uint8_t src) {
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+ return src;
+#else
+ return uint16_t(src) << 8;
+#endif
+}
+
+// RG8 format maps to R.
+template <>
+ALWAYS_INLINE uint8_t convert_pixel<uint8_t>(uint16_t src) {
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+ return src & 0xFF;
+#else
+ return src >> 8;
+#endif
+}
+
+template <bool COMPOSITE, typename P>
+static inline void copy_row(P* dst, const P* src, int span) {
+ // No scaling, so just do a fast copy.
+ memcpy(dst, src, span * sizeof(P));
+}
+
+template <>
+void copy_row<true, uint32_t>(uint32_t* dst, const uint32_t* src, int span) {
+ // No scaling, so just do a fast composite.
+ auto* end = dst + span;
+ while (dst + 4 <= end) {
+ WideRGBA8 srcpx = unpack(unaligned_load<PackedRGBA8>(src));
+ WideRGBA8 dstpx = unpack(unaligned_load<PackedRGBA8>(dst));
+ PackedRGBA8 r = pack(srcpx + dstpx - muldiv255(dstpx, alphas(srcpx)));
+ unaligned_store(dst, r);
+ src += 4;
+ dst += 4;
+ }
+ if (dst < end) {
+ WideRGBA8 srcpx = unpack(partial_load_span<PackedRGBA8>(src, end - dst));
+ WideRGBA8 dstpx = unpack(partial_load_span<PackedRGBA8>(dst, end - dst));
+ auto r = pack(srcpx + dstpx - muldiv255(dstpx, alphas(srcpx)));
+ partial_store_span(dst, r, end - dst);
+ }
+}
+
+template <bool COMPOSITE, typename P, typename U>
+static inline void scale_row(P* dst, int dstWidth, const U* src, int srcWidth,
+ int span, int frac) {
+ // Do scaling with different source and dest widths.
+ for (P* end = dst + span; dst < end; dst++) {
+ *dst = convert_pixel<P>(*src);
+ // Step source according to width ratio.
+ for (frac += srcWidth; frac >= dstWidth; frac -= dstWidth) {
+ src++;
+ }
+ }
+}
+
+template <>
+void scale_row<true, uint32_t, uint32_t>(uint32_t* dst, int dstWidth,
+ const uint32_t* src, int srcWidth,
+ int span, int frac) {
+ // Do scaling with different source and dest widths.
+ // Gather source pixels four at a time for better packing.
+ auto* end = dst + span;
+ for (; dst + 4 <= end; dst += 4) {
+ U32 srcn;
+ srcn.x = *src;
+ for (frac += srcWidth; frac >= dstWidth; frac -= dstWidth) {
+ src++;
+ }
+ srcn.y = *src;
+ for (frac += srcWidth; frac >= dstWidth; frac -= dstWidth) {
+ src++;
+ }
+ srcn.z = *src;
+ for (frac += srcWidth; frac >= dstWidth; frac -= dstWidth) {
+ src++;
+ }
+ srcn.w = *src;
+ for (frac += srcWidth; frac >= dstWidth; frac -= dstWidth) {
+ src++;
+ }
+ WideRGBA8 srcpx = unpack(bit_cast<PackedRGBA8>(srcn));
+ WideRGBA8 dstpx = unpack(unaligned_load<PackedRGBA8>(dst));
+ PackedRGBA8 r = pack(srcpx + dstpx - muldiv255(dstpx, alphas(srcpx)));
+ unaligned_store(dst, r);
+ }
+ if (dst < end) {
+ // Process any remaining pixels. Try to gather as many pixels as possible
+ // into a single source chunk for compositing.
+ U32 srcn = {*src, 0, 0, 0};
+ if (end - dst > 1) {
+ for (frac += srcWidth; frac >= dstWidth; frac -= dstWidth) {
+ src++;
+ }
+ srcn.y = *src;
+ if (end - dst > 2) {
+ for (frac += srcWidth; frac >= dstWidth; frac -= dstWidth) {
+ src++;
+ }
+ srcn.z = *src;
+ }
+ }
+ WideRGBA8 srcpx = unpack(bit_cast<PackedRGBA8>(srcn));
+ WideRGBA8 dstpx = unpack(partial_load_span<PackedRGBA8>(dst, end - dst));
+ auto r = pack(srcpx + dstpx - muldiv255(dstpx, alphas(srcpx)));
+ partial_store_span(dst, r, end - dst);
+ }
+}
+
+template <bool COMPOSITE = false>
+static NO_INLINE void scale_blit(Texture& srctex, const IntRect& srcReq,
+ Texture& dsttex, const IntRect& dstReq,
+ bool invertY, const IntRect& clipRect) {
+ assert(!COMPOSITE || (srctex.internal_format == GL_RGBA8 &&
+ dsttex.internal_format == GL_RGBA8));
+ // Cache scaling ratios
+ int srcWidth = srcReq.width();
+ int srcHeight = srcReq.height();
+ int dstWidth = dstReq.width();
+ int dstHeight = dstReq.height();
+ // Compute valid dest bounds
+ IntRect dstBounds = dsttex.sample_bounds(dstReq).intersect(clipRect);
+ // Compute valid source bounds
+ IntRect srcBounds = srctex.sample_bounds(srcReq, invertY);
+ // If srcReq is outside the source texture, we need to clip the sampling
+ // bounds so that we never sample outside valid source bounds. Get texture
+ // bounds relative to srcReq and scale to dest-space rounding inward, using
+ // this rect to limit the dest bounds further.
+ IntRect srcClip = srctex.bounds() - srcReq.origin();
+ if (invertY) {
+ srcClip.invert_y(srcReq.height());
+ }
+ srcClip.scale(srcWidth, srcHeight, dstWidth, dstHeight, true);
+ dstBounds.intersect(srcClip);
+ // Check if clipped sampling bounds are empty
+ if (dstBounds.is_empty()) {
+ return;
+ }
+
+ // Calculate source and dest pointers from clamped offsets
+ int srcStride = srctex.stride();
+ int destStride = dsttex.stride();
+ char* dest = dsttex.sample_ptr(dstReq, dstBounds);
+ // Clip the source bounds by the destination offset.
+ int fracX = srcWidth * dstBounds.x0;
+ int fracY = srcHeight * dstBounds.y0;
+ srcBounds.x0 = max(fracX / dstWidth, srcBounds.x0);
+ srcBounds.y0 = max(fracY / dstHeight, srcBounds.y0);
+ fracX %= dstWidth;
+ fracY %= dstHeight;
+ char* src = srctex.sample_ptr(srcReq, srcBounds, invertY);
+ // Inverted Y must step downward along source rows
+ if (invertY) {
+ srcStride = -srcStride;
+ }
+ int span = dstBounds.width();
+ for (int rows = dstBounds.height(); rows > 0; rows--) {
+ switch (srctex.bpp()) {
+ case 1:
+ switch (dsttex.bpp()) {
+ case 2:
+ scale_row<COMPOSITE>((uint16_t*)dest, dstWidth, (uint8_t*)src,
+ srcWidth, span, fracX);
+ break;
+ case 4:
+ scale_row<COMPOSITE>((uint32_t*)dest, dstWidth, (uint8_t*)src,
+ srcWidth, span, fracX);
+ break;
+ default:
+ if (srcWidth == dstWidth)
+ copy_row<COMPOSITE>((uint8_t*)dest, (uint8_t*)src, span);
+ else
+ scale_row<COMPOSITE>((uint8_t*)dest, dstWidth, (uint8_t*)src,
+ srcWidth, span, fracX);
+ break;
+ }
+ break;
+ case 2:
+ switch (dsttex.bpp()) {
+ case 1:
+ scale_row<COMPOSITE>((uint8_t*)dest, dstWidth, (uint16_t*)src,
+ srcWidth, span, fracX);
+ break;
+ case 4:
+ scale_row<COMPOSITE>((uint32_t*)dest, dstWidth, (uint16_t*)src,
+ srcWidth, span, fracX);
+ break;
+ default:
+ if (srcWidth == dstWidth)
+ copy_row<COMPOSITE>((uint16_t*)dest, (uint16_t*)src, span);
+ else
+ scale_row<COMPOSITE>((uint16_t*)dest, dstWidth, (uint16_t*)src,
+ srcWidth, span, fracX);
+ break;
+ }
+ break;
+ case 4:
+ switch (dsttex.bpp()) {
+ case 1:
+ scale_row<COMPOSITE>((uint8_t*)dest, dstWidth, (uint32_t*)src,
+ srcWidth, span, fracX);
+ break;
+ case 2:
+ scale_row<COMPOSITE>((uint16_t*)dest, dstWidth, (uint32_t*)src,
+ srcWidth, span, fracX);
+ break;
+ default:
+ if (srcWidth == dstWidth)
+ copy_row<COMPOSITE>((uint32_t*)dest, (uint32_t*)src, span);
+ else
+ scale_row<COMPOSITE>((uint32_t*)dest, dstWidth, (uint32_t*)src,
+ srcWidth, span, fracX);
+ break;
+ }
+ break;
+ default:
+ assert(false);
+ break;
+ }
+ dest += destStride;
+ // Step source according to height ratio.
+ for (fracY += srcHeight; fracY >= dstHeight; fracY -= dstHeight) {
+ src += srcStride;
+ }
+ }
+}
+
+template <bool COMPOSITE>
+static void linear_row_blit(uint32_t* dest, int span, const vec2_scalar& srcUV,
+ float srcDU, sampler2D sampler) {
+ vec2 uv = init_interp(srcUV, vec2_scalar(srcDU, 0.0f));
+ for (; span >= 4; span -= 4) {
+ auto srcpx = textureLinearPackedRGBA8(sampler, ivec2(uv));
+ unaligned_store(dest, srcpx);
+ dest += 4;
+ uv.x += 4 * srcDU;
+ }
+ if (span > 0) {
+ auto srcpx = textureLinearPackedRGBA8(sampler, ivec2(uv));
+ partial_store_span(dest, srcpx, span);
+ }
+}
+
+template <>
+void linear_row_blit<true>(uint32_t* dest, int span, const vec2_scalar& srcUV,
+ float srcDU, sampler2D sampler) {
+ vec2 uv = init_interp(srcUV, vec2_scalar(srcDU, 0.0f));
+ for (; span >= 4; span -= 4) {
+ WideRGBA8 srcpx = textureLinearUnpackedRGBA8(sampler, ivec2(uv));
+ WideRGBA8 dstpx = unpack(unaligned_load<PackedRGBA8>(dest));
+ PackedRGBA8 r = pack(srcpx + dstpx - muldiv255(dstpx, alphas(srcpx)));
+ unaligned_store(dest, r);
+
+ dest += 4;
+ uv.x += 4 * srcDU;
+ }
+ if (span > 0) {
+ WideRGBA8 srcpx = textureLinearUnpackedRGBA8(sampler, ivec2(uv));
+ WideRGBA8 dstpx = unpack(partial_load_span<PackedRGBA8>(dest, span));
+ PackedRGBA8 r = pack(srcpx + dstpx - muldiv255(dstpx, alphas(srcpx)));
+ partial_store_span(dest, r, span);
+ }
+}
+
+template <bool COMPOSITE>
+static void linear_row_blit(uint8_t* dest, int span, const vec2_scalar& srcUV,
+ float srcDU, sampler2D sampler) {
+ vec2 uv = init_interp(srcUV, vec2_scalar(srcDU, 0.0f));
+ for (; span >= 4; span -= 4) {
+ auto srcpx = textureLinearPackedR8(sampler, ivec2(uv));
+ unaligned_store(dest, srcpx);
+ dest += 4;
+ uv.x += 4 * srcDU;
+ }
+ if (span > 0) {
+ auto srcpx = textureLinearPackedR8(sampler, ivec2(uv));
+ partial_store_span(dest, srcpx, span);
+ }
+}
+
+template <bool COMPOSITE>
+static void linear_row_blit(uint16_t* dest, int span, const vec2_scalar& srcUV,
+ float srcDU, sampler2D sampler) {
+ vec2 uv = init_interp(srcUV, vec2_scalar(srcDU, 0.0f));
+ for (; span >= 4; span -= 4) {
+ auto srcpx = textureLinearPackedRG8(sampler, ivec2(uv));
+ unaligned_store(dest, srcpx);
+ dest += 4;
+ uv.x += 4 * srcDU;
+ }
+ if (span > 0) {
+ auto srcpx = textureLinearPackedRG8(sampler, ivec2(uv));
+ partial_store_span(dest, srcpx, span);
+ }
+}
+
+template <bool COMPOSITE = false>
+static NO_INLINE void linear_blit(Texture& srctex, const IntRect& srcReq,
+ Texture& dsttex, const IntRect& dstReq,
+ bool invertX, bool invertY,
+ const IntRect& clipRect) {
+ assert(srctex.internal_format == GL_RGBA8 ||
+ srctex.internal_format == GL_R8 || srctex.internal_format == GL_RG8);
+ assert(!COMPOSITE || (srctex.internal_format == GL_RGBA8 &&
+ dsttex.internal_format == GL_RGBA8));
+ // Compute valid dest bounds
+ IntRect dstBounds = dsttex.sample_bounds(dstReq);
+ dstBounds.intersect(clipRect);
+ // Check if sampling bounds are empty
+ if (dstBounds.is_empty()) {
+ return;
+ }
+ // Initialize sampler for source texture
+ sampler2D_impl sampler;
+ init_sampler(&sampler, srctex);
+ sampler.filter = TextureFilter::LINEAR;
+ // Compute source UVs
+ vec2_scalar srcUV(srcReq.x0, srcReq.y0);
+ vec2_scalar srcDUV(float(srcReq.width()) / dstReq.width(),
+ float(srcReq.height()) / dstReq.height());
+ if (invertX) {
+ // Advance to the end of the row and flip the step.
+ srcUV.x += srcReq.width();
+ srcDUV.x = -srcDUV.x;
+ }
+ // Inverted Y must step downward along source rows
+ if (invertY) {
+ srcUV.y += srcReq.height();
+ srcDUV.y = -srcDUV.y;
+ }
+ // Skip to clamped source start
+ srcUV += srcDUV * (vec2_scalar(dstBounds.x0, dstBounds.y0) + 0.5f);
+ // Scale UVs by lerp precision
+ srcUV = linearQuantize(srcUV, 128);
+ srcDUV *= 128.0f;
+ // Calculate dest pointer from clamped offsets
+ int bpp = dsttex.bpp();
+ int destStride = dsttex.stride();
+ char* dest = dsttex.sample_ptr(dstReq, dstBounds);
+ int span = dstBounds.width();
+ for (int rows = dstBounds.height(); rows > 0; rows--) {
+ switch (bpp) {
+ case 1:
+ linear_row_blit<COMPOSITE>((uint8_t*)dest, span, srcUV, srcDUV.x,
+ &sampler);
+ break;
+ case 2:
+ linear_row_blit<COMPOSITE>((uint16_t*)dest, span, srcUV, srcDUV.x,
+ &sampler);
+ break;
+ case 4:
+ linear_row_blit<COMPOSITE>((uint32_t*)dest, span, srcUV, srcDUV.x,
+ &sampler);
+ break;
+ default:
+ assert(false);
+ break;
+ }
+ dest += destStride;
+ srcUV.y += srcDUV.y;
+ }
+}
+
+// Whether the blit format is renderable.
+static inline bool is_renderable_format(GLenum format) {
+ switch (format) {
+ case GL_R8:
+ case GL_RG8:
+ case GL_RGBA8:
+ return true;
+ default:
+ return false;
+ }
+}
+
+extern "C" {
+
+void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
+ GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
+ GLbitfield mask, GLenum filter) {
+ assert(mask == GL_COLOR_BUFFER_BIT);
+ Framebuffer* srcfb = get_framebuffer(GL_READ_FRAMEBUFFER);
+ if (!srcfb) return;
+ Framebuffer* dstfb = get_framebuffer(GL_DRAW_FRAMEBUFFER);
+ if (!dstfb) return;
+ Texture& srctex = ctx->textures[srcfb->color_attachment];
+ if (!srctex.buf) return;
+ Texture& dsttex = ctx->textures[dstfb->color_attachment];
+ if (!dsttex.buf) return;
+ assert(!dsttex.locked);
+ if (srctex.internal_format != dsttex.internal_format &&
+ (!is_renderable_format(srctex.internal_format) ||
+ !is_renderable_format(dsttex.internal_format))) {
+ // If the internal formats don't match, then we may have to convert. Require
+ // that the format is a simple renderable format to limit combinatoric
+ // explosion for now.
+ assert(false);
+ return;
+ }
+ // Force flipped Y onto dest coordinates
+ if (srcY1 < srcY0) {
+ swap(srcY0, srcY1);
+ swap(dstY0, dstY1);
+ }
+ bool invertY = dstY1 < dstY0;
+ if (invertY) {
+ swap(dstY0, dstY1);
+ }
+ IntRect srcReq = IntRect{srcX0, srcY0, srcX1, srcY1} - srctex.offset;
+ IntRect dstReq = IntRect{dstX0, dstY0, dstX1, dstY1} - dsttex.offset;
+ if (srcReq.is_empty() || dstReq.is_empty()) {
+ return;
+ }
+ IntRect clipRect = {0, 0, dstReq.width(), dstReq.height()};
+ prepare_texture(srctex);
+ prepare_texture(dsttex, &dstReq);
+ if (!srcReq.same_size(dstReq) && srctex.width >= 2 && filter == GL_LINEAR &&
+ srctex.internal_format == dsttex.internal_format &&
+ is_renderable_format(srctex.internal_format)) {
+ linear_blit(srctex, srcReq, dsttex, dstReq, false, invertY, dstReq);
+ } else {
+ scale_blit(srctex, srcReq, dsttex, dstReq, invertY, clipRect);
+ }
+}
+
+typedef Texture LockedTexture;
+
+// Lock the given texture to prevent modification.
+LockedTexture* LockTexture(GLuint texId) {
+ Texture& tex = ctx->textures[texId];
+ if (!tex.buf) {
+ assert(tex.buf != nullptr);
+ return nullptr;
+ }
+ if (__sync_fetch_and_add(&tex.locked, 1) == 0) {
+ // If this is the first time locking the texture, flush any delayed clears.
+ prepare_texture(tex);
+ }
+ return (LockedTexture*)&tex;
+}
+
+// Lock the given framebuffer's color attachment to prevent modification.
+LockedTexture* LockFramebuffer(GLuint fboId) {
+ Framebuffer& fb = ctx->framebuffers[fboId];
+ // Only allow locking a framebuffer if it has a valid color attachment.
+ if (!fb.color_attachment) {
+ assert(fb.color_attachment != 0);
+ return nullptr;
+ }
+ return LockTexture(fb.color_attachment);
+}
+
+// Reference an already locked resource
+void LockResource(LockedTexture* resource) {
+ if (!resource) {
+ return;
+ }
+ __sync_fetch_and_add(&resource->locked, 1);
+}
+
+// Remove a lock on a texture that has been previously locked
+void UnlockResource(LockedTexture* resource) {
+ if (!resource) {
+ return;
+ }
+ if (__sync_fetch_and_add(&resource->locked, -1) <= 0) {
+ // The lock should always be non-zero before unlocking.
+ assert(0);
+ }
+}
+
+// Get the underlying buffer for a locked resource
+void* GetResourceBuffer(LockedTexture* resource, int32_t* width,
+ int32_t* height, int32_t* stride) {
+ *width = resource->width;
+ *height = resource->height;
+ *stride = resource->stride();
+ return resource->buf;
+}
+
+// Extension for optimized compositing of textures or framebuffers that may be
+// safely used across threads. The source and destination must be locked to
+// ensure that they can be safely accessed while the SWGL context might be used
+// by another thread. Band extents along the Y axis may be used to clip the
+// destination rectangle without effecting the integer scaling ratios.
+void Composite(LockedTexture* lockedDst, LockedTexture* lockedSrc, GLint srcX,
+ GLint srcY, GLsizei srcWidth, GLsizei srcHeight, GLint dstX,
+ GLint dstY, GLsizei dstWidth, GLsizei dstHeight,
+ GLboolean opaque, GLboolean flipX, GLboolean flipY,
+ GLenum filter, GLint clipX, GLint clipY, GLsizei clipWidth,
+ GLsizei clipHeight) {
+ if (!lockedDst || !lockedSrc) {
+ return;
+ }
+ Texture& srctex = *lockedSrc;
+ Texture& dsttex = *lockedDst;
+ assert(srctex.bpp() == 4);
+ assert(dsttex.bpp() == 4);
+
+ IntRect srcReq =
+ IntRect{srcX, srcY, srcX + srcWidth, srcY + srcHeight} - srctex.offset;
+ IntRect dstReq =
+ IntRect{dstX, dstY, dstX + dstWidth, dstY + dstHeight} - dsttex.offset;
+ if (srcReq.is_empty() || dstReq.is_empty()) {
+ return;
+ }
+
+ // Compute clip rect as relative to the dstReq, as that's the same coords
+ // as used for the sampling bounds.
+ IntRect clipRect = {clipX - dstX, clipY - dstY, clipX - dstX + clipWidth,
+ clipY - dstY + clipHeight};
+ // Ensure we have rows of at least 2 pixels when using the linear filter to
+ // avoid overreading the row. Force X flips onto the linear filter for now
+ // until scale_blit supports it.
+ bool useLinear =
+ srctex.width >= 2 &&
+ (flipX || (!srcReq.same_size(dstReq) && filter == GL_LINEAR));
+
+ if (opaque) {
+ if (useLinear) {
+ linear_blit<false>(srctex, srcReq, dsttex, dstReq, flipX, flipY,
+ clipRect);
+ } else {
+ scale_blit<false>(srctex, srcReq, dsttex, dstReq, flipY, clipRect);
+ }
+ } else {
+ if (useLinear) {
+ linear_blit<true>(srctex, srcReq, dsttex, dstReq, flipX, flipY, clipRect);
+ } else {
+ scale_blit<true>(srctex, srcReq, dsttex, dstReq, flipY, clipRect);
+ }
+ }
+}
+
+} // extern "C"
+
+// Saturated add helper for YUV conversion. Supported platforms have intrinsics
+// to do this natively, but support a slower generic fallback just in case.
+static inline V8<int16_t> addsat(V8<int16_t> x, V8<int16_t> y) {
+#if USE_SSE2
+ return _mm_adds_epi16(x, y);
+#elif USE_NEON
+ return vqaddq_s16(x, y);
+#else
+ auto r = x + y;
+ // An overflow occurred if the signs of both inputs x and y did not differ
+ // but yet the sign of the result did differ.
+ auto overflow = (~(x ^ y) & (r ^ x)) >> 15;
+ // If there was an overflow, we need to choose the appropriate limit to clamp
+ // to depending on whether or not the inputs are negative.
+ auto limit = (x >> 15) ^ 0x7FFF;
+ // If we didn't overflow, just use the result, and otherwise, use the limit.
+ return (~overflow & r) | (overflow & limit);
+#endif
+}
+
+// Interleave and packing helper for YUV conversion. During transform by the
+// color matrix, the color components are de-interleaved as this format is
+// usually what comes out of the planar YUV textures. The components thus need
+// to be interleaved before finally getting packed to BGRA format. Alpha is
+// forced to be opaque.
+static inline PackedRGBA8 packYUV(V8<int16_t> gg, V8<int16_t> br) {
+ return pack(bit_cast<WideRGBA8>(zip(br, gg))) |
+ PackedRGBA8{0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255};
+}
+
+// clang-format off
+// Supports YUV color matrixes of the form:
+// [R] [1.1643835616438356, 0.0, rv ] [Y - 16]
+// [G] = [1.1643835616438358, -gu, -gv ] x [U - 128]
+// [B] [1.1643835616438356, bu, 0.0 ] [V - 128]
+// We must be able to multiply a YUV input by a matrix coefficient ranging as
+// high as ~2.2 in the U/V cases, where U/V can be signed values between -128
+// and 127. The largest fixed-point representation we can thus support without
+// overflowing 16 bit integers leaves us 6 bits of fractional precision while
+// also supporting a sign bit. The closest representation of the Y coefficient
+// ~1.164 in this precision is 74.5/2^6 which is common to all color spaces
+// we support. Conversions can still sometimes overflow the precision and
+// require clamping back into range, so we use saturated additions to do this
+// efficiently at no extra cost.
+// clang-format on
+struct YUVMatrix {
+ // These constants are loaded off the "this" pointer via relative addressing
+ // modes and should be about as quick to load as directly addressed SIMD
+ // constant memory.
+
+ V8<int16_t> br_uvCoeffs; // biased by 6 bits [b_from_u, r_from_v, repeats]
+ V8<int16_t> gg_uvCoeffs; // biased by 6 bits [g_from_u, g_from_v, repeats]
+ V8<uint16_t> yCoeffs; // biased by 7 bits
+ V8<int16_t> yBias; // 0 or 16
+ V8<int16_t> uvBias; // 128
+ V8<int16_t> br_yMask;
+
+ // E.g. rec709-narrow:
+ // [ 1.16, 0, 1.79, -0.97 ]
+ // [ 1.16, -0.21, -0.53, 0.30 ]
+ // [ 1.16, 2.11, 0, -1.13 ]
+ // =
+ // [ yScale, 0, r_from_v ] ([Y ] )
+ // [ yScale, g_from_u, g_from_v ] x ([cb] - ycbcr_bias )
+ // [ yScale, b_from_u, 0 ] ([cr] )
+ static YUVMatrix From(const vec3_scalar& ycbcr_bias,
+ const mat3_scalar& rgb_from_debiased_ycbcr,
+ int rescale_factor = 0) {
+ assert(ycbcr_bias.z == ycbcr_bias.y);
+
+ const auto rgb_from_y = rgb_from_debiased_ycbcr[0].y;
+ assert(rgb_from_debiased_ycbcr[0].x == rgb_from_debiased_ycbcr[0].z);
+
+ int16_t br_from_y_mask = -1;
+ if (rgb_from_debiased_ycbcr[0].x == 0.0) {
+ // gbr-identity matrix?
+ assert(rgb_from_debiased_ycbcr[0].x == 0);
+ assert(rgb_from_debiased_ycbcr[0].y >= 1);
+ assert(rgb_from_debiased_ycbcr[0].z == 0);
+
+ assert(rgb_from_debiased_ycbcr[1].x == 0);
+ assert(rgb_from_debiased_ycbcr[1].y == 0);
+ assert(rgb_from_debiased_ycbcr[1].z >= 1);
+
+ assert(rgb_from_debiased_ycbcr[2].x >= 1);
+ assert(rgb_from_debiased_ycbcr[2].y == 0);
+ assert(rgb_from_debiased_ycbcr[2].z == 0);
+
+ assert(ycbcr_bias.x == 0);
+ assert(ycbcr_bias.y == 0);
+ assert(ycbcr_bias.z == 0);
+
+ br_from_y_mask = 0;
+ } else {
+ assert(rgb_from_debiased_ycbcr[0].x == rgb_from_y);
+ }
+
+ assert(rgb_from_debiased_ycbcr[1].x == 0.0);
+ const auto g_from_u = rgb_from_debiased_ycbcr[1].y;
+ const auto b_from_u = rgb_from_debiased_ycbcr[1].z;
+
+ const auto r_from_v = rgb_from_debiased_ycbcr[2].x;
+ const auto g_from_v = rgb_from_debiased_ycbcr[2].y;
+ assert(rgb_from_debiased_ycbcr[2].z == 0.0);
+
+ return YUVMatrix({ycbcr_bias.x, ycbcr_bias.y}, rgb_from_y, br_from_y_mask,
+ r_from_v, g_from_u, g_from_v, b_from_u, rescale_factor);
+ }
+
+ // Convert matrix coefficients to fixed-point representation. If the matrix
+ // has a rescaling applied to it, then we need to take care to undo the
+ // scaling so that we can convert the coefficients to fixed-point range. The
+ // bias still requires shifting to apply the rescaling. The rescaling will be
+ // applied to the actual YCbCr sample data later by manually shifting it
+ // before applying this matrix.
+ YUVMatrix(vec2_scalar yuv_bias, double yCoeff, int16_t br_yMask_, double rv,
+ double gu, double gv, double bu, int rescale_factor = 0)
+ : br_uvCoeffs(zip(I16(int16_t(bu * (1 << (6 - rescale_factor)) + 0.5)),
+ I16(int16_t(rv * (1 << (6 - rescale_factor)) + 0.5)))),
+ gg_uvCoeffs(
+ zip(I16(-int16_t(-gu * (1 << (6 - rescale_factor)) +
+ 0.5)), // These are negative coeffs, so
+ // round them away from zero
+ I16(-int16_t(-gv * (1 << (6 - rescale_factor)) + 0.5)))),
+ yCoeffs(uint16_t(yCoeff * (1 << (6 + 1 - rescale_factor)) + 0.5)),
+ // We have a +0.5 fudge-factor for -ybias.
+ // Without this, we get white=254 not 255.
+ // This approximates rounding rather than truncation during `gg >>= 6`.
+ yBias(int16_t(((yuv_bias.x * 255 * yCoeff) - 0.5) * (1 << 6))),
+ uvBias(int16_t(yuv_bias.y * (255 << rescale_factor) + 0.5)),
+ br_yMask(br_yMask_) {
+ assert(yuv_bias.x >= 0);
+ assert(yuv_bias.y >= 0);
+ assert(yCoeff > 0);
+ assert(br_yMask_ == 0 || br_yMask_ == -1);
+ assert(bu > 0);
+ assert(rv > 0);
+ assert(gu <= 0);
+ assert(gv <= 0);
+ assert(rescale_factor <= 6);
+ }
+
+ ALWAYS_INLINE PackedRGBA8 convert(V8<int16_t> yy, V8<int16_t> uv) const {
+ // We gave ourselves an extra bit (7 instead of 6) of bias to give us some
+ // extra precision for the more-sensitive y scaling.
+ // Note that we have to use an unsigned multiply with a 2x scale to
+ // represent a fractional scale and to avoid shifting with the sign bit.
+
+ // Note: if you subtract the bias before multiplication, we see more
+ // underflows. This could be fixed by an unsigned subsat.
+ yy = bit_cast<V8<int16_t>>((bit_cast<V8<uint16_t>>(yy) * yCoeffs) >> 1);
+ yy -= yBias;
+
+ // Compute [B] = [yCoeff*Y + bu*U + 0*V]
+ // [R] [yCoeff*Y + 0*U + rv*V]
+ uv -= uvBias;
+ auto br = br_uvCoeffs * uv;
+ br = addsat(yy & br_yMask, br);
+ br >>= 6;
+
+ // Compute G = yCoeff*Y + gu*U + gv*V
+ // First calc [gu*U, gv*V, ...]:
+ auto gg = gg_uvCoeffs * uv;
+ // Then cross the streams to get `gu*U + gv*V`:
+ gg = addsat(gg, bit_cast<V8<int16_t>>(bit_cast<V4<uint32_t>>(gg) >> 16));
+ // Add the other parts:
+ gg = addsat(yy, gg); // This is the part that needs the most headroom
+ // usually. In particular, ycbcr(255,255,255) hugely
+ // saturates.
+ gg >>= 6;
+
+ // Interleave B/R and G values. Force alpha (high-gg half) to opaque.
+ return packYUV(gg, br);
+ }
+};
+
+// Helper function for textureLinearRowR8 that samples horizontal taps and
+// combines them based on Y fraction with next row.
+template <typename S>
+static ALWAYS_INLINE V8<int16_t> linearRowTapsR8(S sampler, I32 ix,
+ int32_t offsety,
+ int32_t stridey,
+ int16_t fracy) {
+ uint8_t* buf = (uint8_t*)sampler->buf + offsety;
+ auto a0 = unaligned_load<V2<uint8_t>>(&buf[ix.x]);
+ auto b0 = unaligned_load<V2<uint8_t>>(&buf[ix.y]);
+ auto c0 = unaligned_load<V2<uint8_t>>(&buf[ix.z]);
+ auto d0 = unaligned_load<V2<uint8_t>>(&buf[ix.w]);
+ auto abcd0 = CONVERT(combine(a0, b0, c0, d0), V8<int16_t>);
+ buf += stridey;
+ auto a1 = unaligned_load<V2<uint8_t>>(&buf[ix.x]);
+ auto b1 = unaligned_load<V2<uint8_t>>(&buf[ix.y]);
+ auto c1 = unaligned_load<V2<uint8_t>>(&buf[ix.z]);
+ auto d1 = unaligned_load<V2<uint8_t>>(&buf[ix.w]);
+ auto abcd1 = CONVERT(combine(a1, b1, c1, d1), V8<int16_t>);
+ abcd0 += ((abcd1 - abcd0) * fracy) >> 7;
+ return abcd0;
+}
+
+// Optimized version of textureLinearPackedR8 for Y R8 texture. This assumes
+// constant Y and returns a duplicate of the result interleaved with itself
+// to aid in later YUV transformation.
+template <typename S>
+static inline V8<int16_t> textureLinearRowR8(S sampler, I32 ix, int32_t offsety,
+ int32_t stridey, int16_t fracy) {
+ assert(sampler->format == TextureFormat::R8);
+
+ // Calculate X fraction and clamp X offset into range.
+ I32 fracx = ix;
+ ix >>= 7;
+ fracx = ((fracx & (ix >= 0)) | (ix > int32_t(sampler->width) - 2)) & 0x7F;
+ ix = clampCoord(ix, sampler->width - 1);
+
+ // Load the sample taps and combine rows.
+ auto abcd = linearRowTapsR8(sampler, ix, offsety, stridey, fracy);
+
+ // Unzip the result and do final horizontal multiply-add base on X fraction.
+ auto abcdl = SHUFFLE(abcd, abcd, 0, 0, 2, 2, 4, 4, 6, 6);
+ auto abcdh = SHUFFLE(abcd, abcd, 1, 1, 3, 3, 5, 5, 7, 7);
+ abcdl += ((abcdh - abcdl) * CONVERT(fracx, I16).xxyyzzww) >> 7;
+
+ // The final result is the packed values interleaved with a duplicate of
+ // themselves.
+ return abcdl;
+}
+
+// Optimized version of textureLinearPackedR8 for paired U/V R8 textures.
+// Since the two textures have the same dimensions and stride, the addressing
+// math can be shared between both samplers. This also allows a coalesced
+// multiply in the final stage by packing both U/V results into a single
+// operation.
+template <typename S>
+static inline V8<int16_t> textureLinearRowPairedR8(S sampler, S sampler2,
+ I32 ix, int32_t offsety,
+ int32_t stridey,
+ int16_t fracy) {
+ assert(sampler->format == TextureFormat::R8 &&
+ sampler2->format == TextureFormat::R8);
+ assert(sampler->width == sampler2->width &&
+ sampler->height == sampler2->height);
+ assert(sampler->stride == sampler2->stride);
+
+ // Calculate X fraction and clamp X offset into range.
+ I32 fracx = ix;
+ ix >>= 7;
+ fracx = ((fracx & (ix >= 0)) | (ix > int32_t(sampler->width) - 2)) & 0x7F;
+ ix = clampCoord(ix, sampler->width - 1);
+
+ // Load the sample taps for the first sampler and combine rows.
+ auto abcd = linearRowTapsR8(sampler, ix, offsety, stridey, fracy);
+
+ // Load the sample taps for the second sampler and combine rows.
+ auto xyzw = linearRowTapsR8(sampler2, ix, offsety, stridey, fracy);
+
+ // We are left with a result vector for each sampler with values for adjacent
+ // pixels interleaved together in each. We need to unzip these values so that
+ // we can do the final horizontal multiply-add based on the X fraction.
+ auto abcdxyzwl = SHUFFLE(abcd, xyzw, 0, 8, 2, 10, 4, 12, 6, 14);
+ auto abcdxyzwh = SHUFFLE(abcd, xyzw, 1, 9, 3, 11, 5, 13, 7, 15);
+ abcdxyzwl += ((abcdxyzwh - abcdxyzwl) * CONVERT(fracx, I16).xxyyzzww) >> 7;
+
+ // The final result is the packed values for the first sampler interleaved
+ // with the packed values for the second sampler.
+ return abcdxyzwl;
+}
+
+// Casting to int loses some precision while stepping that can offset the
+// image, so shift the values by some extra bits of precision to minimize
+// this. We support up to 16 bits of image size, 7 bits of quantization,
+// and 1 bit for sign, which leaves 8 bits left for extra precision.
+const int STEP_BITS = 8;
+
+// Optimized version of textureLinearPackedR8 for Y R8 texture with
+// half-resolution paired U/V R8 textures. This allows us to more efficiently
+// pack YUV samples into vectors to substantially reduce math operations even
+// further.
+template <bool BLEND>
+static inline void upscaleYUV42R8(uint32_t* dest, int span, uint8_t* yRow,
+ I32 yU, int32_t yDU, int32_t yStrideV,
+ int16_t yFracV, uint8_t* cRow1,
+ uint8_t* cRow2, I32 cU, int32_t cDU,
+ int32_t cStrideV, int16_t cFracV,
+ const YUVMatrix& colorSpace) {
+ // As much as possible try to utilize the fact that we're only using half
+ // the UV samples to combine Y and UV samples into single vectors. Here we
+ // need to initialize several useful vector quantities for stepping fractional
+ // offsets. For the UV samples, we take the average of the first+second and
+ // third+fourth samples in a chunk which conceptually correspond to offsets
+ // 0.5 and 1.5 (in 0..2 range). This allows us to reconstruct intermediate
+ // samples 0.25, 0.75, 1.25, and 1.75 later. X fraction is shifted over into
+ // the top 7 bits of an unsigned short so that we can mask off the exact
+ // fractional bits we need to blend merely by right shifting them into
+ // position.
+ cU = (cU.xzxz + cU.ywyw) >> 1;
+ auto ycFracX = CONVERT(combine(yU, cU), V8<uint16_t>)
+ << (16 - (STEP_BITS + 7));
+ auto ycFracDX = combine(I16(yDU), I16(cDU)) << (16 - (STEP_BITS + 7));
+ auto ycFracV = combine(I16(yFracV), I16(cFracV));
+ I32 yI = yU >> (STEP_BITS + 7);
+ I32 cI = cU >> (STEP_BITS + 7);
+ // Load initial combined YUV samples for each row and blend them.
+ auto ycSrc0 =
+ CONVERT(combine(unaligned_load<V4<uint8_t>>(&yRow[yI.x]),
+ combine(unaligned_load<V2<uint8_t>>(&cRow1[cI.x]),
+ unaligned_load<V2<uint8_t>>(&cRow2[cI.x]))),
+ V8<int16_t>);
+ auto ycSrc1 = CONVERT(
+ combine(unaligned_load<V4<uint8_t>>(&yRow[yI.x + yStrideV]),
+ combine(unaligned_load<V2<uint8_t>>(&cRow1[cI.x + cStrideV]),
+ unaligned_load<V2<uint8_t>>(&cRow2[cI.x + cStrideV]))),
+ V8<int16_t>);
+ auto ycSrc = ycSrc0 + (((ycSrc1 - ycSrc0) * ycFracV) >> 7);
+
+ // Here we shift in results from the next sample while caching results from
+ // the previous sample. This allows us to reduce the multiplications in the
+ // inner loop down to only two since we just need to blend the new samples
+ // horizontally and then vertically once each.
+ for (uint32_t* end = dest + span; dest < end; dest += 4) {
+ yU += yDU;
+ I32 yIn = yU >> (STEP_BITS + 7);
+ cU += cDU;
+ I32 cIn = cU >> (STEP_BITS + 7);
+ // Load combined YUV samples for the next chunk on each row and blend them.
+ auto ycSrc0n =
+ CONVERT(combine(unaligned_load<V4<uint8_t>>(&yRow[yIn.x]),
+ combine(unaligned_load<V2<uint8_t>>(&cRow1[cIn.x]),
+ unaligned_load<V2<uint8_t>>(&cRow2[cIn.x]))),
+ V8<int16_t>);
+ auto ycSrc1n = CONVERT(
+ combine(unaligned_load<V4<uint8_t>>(&yRow[yIn.x + yStrideV]),
+ combine(unaligned_load<V2<uint8_t>>(&cRow1[cIn.x + cStrideV]),
+ unaligned_load<V2<uint8_t>>(&cRow2[cIn.x + cStrideV]))),
+ V8<int16_t>);
+ auto ycSrcn = ycSrc0n + (((ycSrc1n - ycSrc0n) * ycFracV) >> 7);
+
+ // The source samples for the chunk may not match the actual tap offsets.
+ // Since we're upscaling, we know the tap offsets fall within all the
+ // samples in a 4-wide chunk. Since we can't rely on PSHUFB or similar,
+ // instead we do laborious shuffling here for the Y samples and then the UV
+ // samples.
+ auto yshuf = lowHalf(ycSrc);
+ auto yshufn =
+ SHUFFLE(yshuf, yIn.x == yI.w ? lowHalf(ycSrcn).yyyy : lowHalf(ycSrcn),
+ 1, 2, 3, 4);
+ if (yI.y == yI.x) {
+ yshuf = yshuf.xxyz;
+ yshufn = yshufn.xxyz;
+ }
+ if (yI.z == yI.y) {
+ yshuf = yshuf.xyyz;
+ yshufn = yshufn.xyyz;
+ }
+ if (yI.w == yI.z) {
+ yshuf = yshuf.xyzz;
+ yshufn = yshufn.xyzz;
+ }
+
+ auto cshuf = highHalf(ycSrc);
+ auto cshufn =
+ SHUFFLE(cshuf, cIn.x == cI.y ? highHalf(ycSrcn).yyww : highHalf(ycSrcn),
+ 1, 4, 3, 6);
+ if (cI.y == cI.x) {
+ cshuf = cshuf.xxzz;
+ cshufn = cshufn.xxzz;
+ }
+
+ // After shuffling, combine the Y and UV samples back into a single vector
+ // for blending. Shift X fraction into position as unsigned to mask off top
+ // bits and get rid of low bits to avoid multiplication overflow.
+ auto yuvPx = combine(yshuf, cshuf);
+ yuvPx += ((combine(yshufn, cshufn) - yuvPx) *
+ bit_cast<V8<int16_t>>(ycFracX >> (16 - 7))) >>
+ 7;
+
+ // Cache the new samples as the current samples on the next iteration.
+ ycSrc = ycSrcn;
+ ycFracX += ycFracDX;
+ yI = yIn;
+ cI = cIn;
+
+ // De-interleave the Y and UV results. We need to average the UV results
+ // to produce values for intermediate samples. Taps for UV were collected at
+ // offsets 0.5 and 1.5, such that if we take a quarter of the difference
+ // (1.5-0.5)/4, subtract it from even samples, and add it to odd samples,
+ // we can estimate samples 0.25, 0.75, 1.25, and 1.75.
+ auto yPx = SHUFFLE(yuvPx, yuvPx, 0, 0, 1, 1, 2, 2, 3, 3);
+ auto uvPx = SHUFFLE(yuvPx, yuvPx, 4, 6, 4, 6, 5, 7, 5, 7) +
+ ((SHUFFLE(yuvPx, yuvPx, 4, 6, 5, 7, 4, 6, 5, 7) -
+ SHUFFLE(yuvPx, yuvPx, 5, 7, 4, 6, 5, 7, 4, 6)) >>
+ 2);
+
+ commit_blend_span<BLEND>(dest, colorSpace.convert(yPx, uvPx));
+ }
+}
+
+// This is the inner loop driver of CompositeYUV that processes an axis-aligned
+// YUV span, dispatching based on appropriate format and scaling. This is also
+// reused by blendYUV to accelerate some cases of texture sampling in the
+// shader.
+template <bool BLEND = false>
+static void linear_row_yuv(uint32_t* dest, int span, sampler2DRect samplerY,
+ const vec2_scalar& srcUV, float srcDU,
+ sampler2DRect samplerU, sampler2DRect samplerV,
+ const vec2_scalar& chromaUV, float chromaDU,
+ int colorDepth, const YUVMatrix& colorSpace) {
+ // Calculate varying and constant interp data for Y plane.
+ I32 yU = cast(init_interp(srcUV.x, srcDU) * (1 << STEP_BITS));
+ int32_t yV = int32_t(srcUV.y);
+
+ // Calculate varying and constant interp data for chroma planes.
+ I32 cU = cast(init_interp(chromaUV.x, chromaDU) * (1 << STEP_BITS));
+ int32_t cV = int32_t(chromaUV.y);
+
+ // We need to skip 4 pixels per chunk.
+ int32_t yDU = int32_t((4 << STEP_BITS) * srcDU);
+ int32_t cDU = int32_t((4 << STEP_BITS) * chromaDU);
+
+ if (samplerY->width < 2 || samplerU->width < 2) {
+ // If the source row has less than 2 pixels, it's not safe to use a linear
+ // filter because it may overread the row. Just convert the single pixel
+ // with nearest filtering and fill the row with it.
+ Float yuvF = {texelFetch(samplerY, ivec2(srcUV)).x.x,
+ texelFetch(samplerU, ivec2(chromaUV)).x.x,
+ texelFetch(samplerV, ivec2(chromaUV)).x.x, 1.0f};
+ // If this is an HDR LSB format, we need to renormalize the result.
+ if (colorDepth > 8) {
+ int rescaleFactor = 16 - colorDepth;
+ yuvF *= float(1 << rescaleFactor);
+ }
+ I16 yuv = CONVERT(round_pixel(yuvF), I16);
+ commit_solid_span<BLEND>(
+ dest,
+ unpack(colorSpace.convert(V8<int16_t>(yuv.x),
+ zip(I16(yuv.y), I16(yuv.z)))),
+ span);
+ } else if (samplerY->format == TextureFormat::R16) {
+ // Sample each YUV plane, rescale it to fit in low 8 bits of word, and
+ // then transform them by the appropriate color space.
+ assert(colorDepth > 8);
+ // Need to right shift the sample by the amount of bits over 8 it
+ // occupies. On output from textureLinearUnpackedR16, we have lost 1 bit
+ // of precision at the low end already, hence 1 is subtracted from the
+ // color depth.
+ int rescaleBits = (colorDepth - 1) - 8;
+ for (; span >= 4; span -= 4) {
+ auto yPx =
+ textureLinearUnpackedR16(samplerY, ivec2(yU >> STEP_BITS, yV)) >>
+ rescaleBits;
+ auto uPx =
+ textureLinearUnpackedR16(samplerU, ivec2(cU >> STEP_BITS, cV)) >>
+ rescaleBits;
+ auto vPx =
+ textureLinearUnpackedR16(samplerV, ivec2(cU >> STEP_BITS, cV)) >>
+ rescaleBits;
+ commit_blend_span<BLEND>(
+ dest, colorSpace.convert(zip(yPx, yPx), zip(uPx, vPx)));
+ dest += 4;
+ yU += yDU;
+ cU += cDU;
+ }
+ if (span > 0) {
+ // Handle any remaining pixels...
+ auto yPx =
+ textureLinearUnpackedR16(samplerY, ivec2(yU >> STEP_BITS, yV)) >>
+ rescaleBits;
+ auto uPx =
+ textureLinearUnpackedR16(samplerU, ivec2(cU >> STEP_BITS, cV)) >>
+ rescaleBits;
+ auto vPx =
+ textureLinearUnpackedR16(samplerV, ivec2(cU >> STEP_BITS, cV)) >>
+ rescaleBits;
+ commit_blend_span<BLEND>(
+ dest, colorSpace.convert(zip(yPx, yPx), zip(uPx, vPx)), span);
+ }
+ } else {
+ assert(samplerY->format == TextureFormat::R8);
+ assert(colorDepth == 8);
+
+ // Calculate varying and constant interp data for Y plane.
+ int16_t yFracV = yV & 0x7F;
+ yV >>= 7;
+ int32_t yOffsetV = clampCoord(yV, samplerY->height) * samplerY->stride;
+ int32_t yStrideV =
+ yV >= 0 && yV < int32_t(samplerY->height) - 1 ? samplerY->stride : 0;
+
+ // Calculate varying and constant interp data for chroma planes.
+ int16_t cFracV = cV & 0x7F;
+ cV >>= 7;
+ int32_t cOffsetV = clampCoord(cV, samplerU->height) * samplerU->stride;
+ int32_t cStrideV =
+ cV >= 0 && cV < int32_t(samplerU->height) - 1 ? samplerU->stride : 0;
+
+ // If we're sampling the UV planes at half the resolution of the Y plane,
+ // then try to use half resolution fast-path.
+ if (yDU >= cDU && cDU > 0 && yDU <= (4 << (STEP_BITS + 7)) &&
+ cDU <= (2 << (STEP_BITS + 7))) {
+ // Ensure that samples don't fall outside of the valid bounds of each
+ // planar texture. Step until the initial X coordinates are positive.
+ for (; (yU.x < 0 || cU.x < 0) && span >= 4; span -= 4) {
+ auto yPx = textureLinearRowR8(samplerY, yU >> STEP_BITS, yOffsetV,
+ yStrideV, yFracV);
+ auto uvPx = textureLinearRowPairedR8(
+ samplerU, samplerV, cU >> STEP_BITS, cOffsetV, cStrideV, cFracV);
+ commit_blend_span<BLEND>(dest, colorSpace.convert(yPx, uvPx));
+ dest += 4;
+ yU += yDU;
+ cU += cDU;
+ }
+ // Calculate the number of aligned chunks that we can step inside the
+ // bounds of each planar texture without overreading.
+ int inside = min(
+ min((((int(samplerY->width) - 4) << (STEP_BITS + 7)) - yU.x) / yDU,
+ (((int(samplerU->width) - 4) << (STEP_BITS + 7)) - cU.x) / cDU) *
+ 4,
+ span & ~3);
+ if (inside > 0) {
+ uint8_t* yRow = (uint8_t*)samplerY->buf + yOffsetV;
+ uint8_t* cRow1 = (uint8_t*)samplerU->buf + cOffsetV;
+ uint8_t* cRow2 = (uint8_t*)samplerV->buf + cOffsetV;
+ upscaleYUV42R8<BLEND>(dest, inside, yRow, yU, yDU, yStrideV, yFracV,
+ cRow1, cRow2, cU, cDU, cStrideV, cFracV,
+ colorSpace);
+ span -= inside;
+ dest += inside;
+ yU += (inside / 4) * yDU;
+ cU += (inside / 4) * cDU;
+ }
+ // If there are any remaining chunks that weren't inside, handle them
+ // below.
+ }
+ for (; span >= 4; span -= 4) {
+ // Sample each YUV plane and then transform them by the appropriate
+ // color space.
+ auto yPx = textureLinearRowR8(samplerY, yU >> STEP_BITS, yOffsetV,
+ yStrideV, yFracV);
+ auto uvPx = textureLinearRowPairedR8(samplerU, samplerV, cU >> STEP_BITS,
+ cOffsetV, cStrideV, cFracV);
+ commit_blend_span<BLEND>(dest, colorSpace.convert(yPx, uvPx));
+ dest += 4;
+ yU += yDU;
+ cU += cDU;
+ }
+ if (span > 0) {
+ // Handle any remaining pixels...
+ auto yPx = textureLinearRowR8(samplerY, yU >> STEP_BITS, yOffsetV,
+ yStrideV, yFracV);
+ auto uvPx = textureLinearRowPairedR8(samplerU, samplerV, cU >> STEP_BITS,
+ cOffsetV, cStrideV, cFracV);
+ commit_blend_span<BLEND>(dest, colorSpace.convert(yPx, uvPx), span);
+ }
+ }
+}
+
+static void linear_convert_yuv(Texture& ytex, Texture& utex, Texture& vtex,
+ const YUVMatrix& rgbFromYcbcr, int colorDepth,
+ const IntRect& srcReq, Texture& dsttex,
+ const IntRect& dstReq, bool invertX,
+ bool invertY, const IntRect& clipRect) {
+ // Compute valid dest bounds
+ IntRect dstBounds = dsttex.sample_bounds(dstReq);
+ dstBounds.intersect(clipRect);
+ // Check if sampling bounds are empty
+ if (dstBounds.is_empty()) {
+ return;
+ }
+ // Initialize samplers for source textures
+ sampler2DRect_impl sampler[3];
+ init_sampler(&sampler[0], ytex);
+ init_sampler(&sampler[1], utex);
+ init_sampler(&sampler[2], vtex);
+
+ // Compute source UVs
+ vec2_scalar srcUV(srcReq.x0, srcReq.y0);
+ vec2_scalar srcDUV(float(srcReq.width()) / dstReq.width(),
+ float(srcReq.height()) / dstReq.height());
+ if (invertX) {
+ // Advance to the end of the row and flip the step.
+ srcUV.x += srcReq.width();
+ srcDUV.x = -srcDUV.x;
+ }
+ // Inverted Y must step downward along source rows
+ if (invertY) {
+ srcUV.y += srcReq.height();
+ srcDUV.y = -srcDUV.y;
+ }
+ // Skip to clamped source start
+ srcUV += srcDUV * (vec2_scalar(dstBounds.x0, dstBounds.y0) + 0.5f);
+ // Calculate separate chroma UVs for chroma planes with different scale
+ vec2_scalar chromaScale(float(utex.width) / ytex.width,
+ float(utex.height) / ytex.height);
+ vec2_scalar chromaUV = srcUV * chromaScale;
+ vec2_scalar chromaDUV = srcDUV * chromaScale;
+ // Scale UVs by lerp precision. If the row has only 1 pixel, then don't
+ // quantize so that we can use nearest filtering instead to avoid overreads.
+ if (ytex.width >= 2 && utex.width >= 2) {
+ srcUV = linearQuantize(srcUV, 128);
+ srcDUV *= 128.0f;
+ chromaUV = linearQuantize(chromaUV, 128);
+ chromaDUV *= 128.0f;
+ }
+ // Calculate dest pointer from clamped offsets
+ int destStride = dsttex.stride();
+ char* dest = dsttex.sample_ptr(dstReq, dstBounds);
+ int span = dstBounds.width();
+ for (int rows = dstBounds.height(); rows > 0; rows--) {
+ linear_row_yuv((uint32_t*)dest, span, &sampler[0], srcUV, srcDUV.x,
+ &sampler[1], &sampler[2], chromaUV, chromaDUV.x, colorDepth,
+ rgbFromYcbcr);
+ dest += destStride;
+ srcUV.y += srcDUV.y;
+ chromaUV.y += chromaDUV.y;
+ }
+}
+
+// -
+// This section must match gfx/2d/Types.h
+
+enum class YUVRangedColorSpace : uint8_t {
+ BT601_Narrow = 0,
+ BT601_Full,
+ BT709_Narrow,
+ BT709_Full,
+ BT2020_Narrow,
+ BT2020_Full,
+ GbrIdentity,
+};
+
+// -
+// This section must match yuv.glsl
+
+vec4_scalar get_ycbcr_zeros_ones(const YUVRangedColorSpace color_space,
+ const GLuint color_depth) {
+ // For SWGL's 8bpc-only pipeline, our extra care here probably doesn't matter.
+ // However, technically e.g. 10-bit achromatic zero for cb and cr is
+ // (128 << 2) / ((1 << 10) - 1) = 512 / 1023, which != 128 / 255, and affects
+ // our matrix values subtly. Maybe not enough to matter? But it's the most
+ // correct thing to do.
+ // Unlike the glsl version, our texture samples are u8([0,255]) not
+ // u16([0,1023]) though.
+ switch (color_space) {
+ case YUVRangedColorSpace::BT601_Narrow:
+ case YUVRangedColorSpace::BT709_Narrow:
+ case YUVRangedColorSpace::BT2020_Narrow: {
+ auto extra_bit_count = color_depth - 8;
+ vec4_scalar zo = {
+ float(16 << extra_bit_count),
+ float(128 << extra_bit_count),
+ float(235 << extra_bit_count),
+ float(240 << extra_bit_count),
+ };
+ float all_bits = (1 << color_depth) - 1;
+ zo /= all_bits;
+ return zo;
+ }
+
+ case YUVRangedColorSpace::BT601_Full:
+ case YUVRangedColorSpace::BT709_Full:
+ case YUVRangedColorSpace::BT2020_Full: {
+ const auto narrow =
+ get_ycbcr_zeros_ones(YUVRangedColorSpace::BT601_Narrow, color_depth);
+ return {0.0, narrow.y, 1.0, 1.0};
+ }
+
+ case YUVRangedColorSpace::GbrIdentity:
+ break;
+ }
+ return {0.0, 0.0, 1.0, 1.0};
+}
+
+constexpr mat3_scalar RgbFromYuv_Rec601 = {
+ {1.00000, 1.00000, 1.00000},
+ {0.00000, -0.17207, 0.88600},
+ {0.70100, -0.35707, 0.00000},
+};
+constexpr mat3_scalar RgbFromYuv_Rec709 = {
+ {1.00000, 1.00000, 1.00000},
+ {0.00000, -0.09366, 0.92780},
+ {0.78740, -0.23406, 0.00000},
+};
+constexpr mat3_scalar RgbFromYuv_Rec2020 = {
+ {1.00000, 1.00000, 1.00000},
+ {0.00000, -0.08228, 0.94070},
+ {0.73730, -0.28568, 0.00000},
+};
+constexpr mat3_scalar RgbFromYuv_GbrIdentity = {
+ {0, 1, 0},
+ {0, 0, 1},
+ {1, 0, 0},
+};
+
+inline mat3_scalar get_rgb_from_yuv(const YUVRangedColorSpace color_space) {
+ switch (color_space) {
+ case YUVRangedColorSpace::BT601_Narrow:
+ case YUVRangedColorSpace::BT601_Full:
+ return RgbFromYuv_Rec601;
+ case YUVRangedColorSpace::BT709_Narrow:
+ case YUVRangedColorSpace::BT709_Full:
+ return RgbFromYuv_Rec709;
+ case YUVRangedColorSpace::BT2020_Narrow:
+ case YUVRangedColorSpace::BT2020_Full:
+ return RgbFromYuv_Rec2020;
+ case YUVRangedColorSpace::GbrIdentity:
+ break;
+ }
+ return RgbFromYuv_GbrIdentity;
+}
+
+struct YcbcrInfo final {
+ vec3_scalar ycbcr_bias;
+ mat3_scalar rgb_from_debiased_ycbcr;
+};
+
+inline YcbcrInfo get_ycbcr_info(const YUVRangedColorSpace color_space,
+ GLuint color_depth) {
+ // SWGL always does 8bpc math, so don't scale the matrix for 10bpc!
+ color_depth = 8;
+
+ const auto zeros_ones = get_ycbcr_zeros_ones(color_space, color_depth);
+ const auto zeros = vec2_scalar{zeros_ones.x, zeros_ones.y};
+ const auto ones = vec2_scalar{zeros_ones.z, zeros_ones.w};
+ const auto scale = 1.0f / (ones - zeros);
+
+ const auto rgb_from_yuv = get_rgb_from_yuv(color_space);
+ const mat3_scalar yuv_from_debiased_ycbcr = {
+ {scale.x, 0, 0},
+ {0, scale.y, 0},
+ {0, 0, scale.y},
+ };
+
+ YcbcrInfo ret;
+ ret.ycbcr_bias = {zeros.x, zeros.y, zeros.y};
+ ret.rgb_from_debiased_ycbcr = rgb_from_yuv * yuv_from_debiased_ycbcr;
+ return ret;
+}
+
+// -
+
+extern "C" {
+
+// Extension for compositing a YUV surface represented by separate YUV planes
+// to a BGRA destination. The supplied color space is used to determine the
+// transform from YUV to BGRA after sampling.
+void CompositeYUV(LockedTexture* lockedDst, LockedTexture* lockedY,
+ LockedTexture* lockedU, LockedTexture* lockedV,
+ YUVRangedColorSpace colorSpace, GLuint colorDepth, GLint srcX,
+ GLint srcY, GLsizei srcWidth, GLsizei srcHeight, GLint dstX,
+ GLint dstY, GLsizei dstWidth, GLsizei dstHeight,
+ GLboolean flipX, GLboolean flipY, GLint clipX, GLint clipY,
+ GLsizei clipWidth, GLsizei clipHeight) {
+ if (!lockedDst || !lockedY || !lockedU || !lockedV) {
+ return;
+ }
+ if (colorSpace > YUVRangedColorSpace::GbrIdentity) {
+ assert(false);
+ return;
+ }
+ const auto ycbcrInfo = get_ycbcr_info(colorSpace, colorDepth);
+ const auto rgbFromYcbcr =
+ YUVMatrix::From(ycbcrInfo.ycbcr_bias, ycbcrInfo.rgb_from_debiased_ycbcr);
+
+ Texture& ytex = *lockedY;
+ Texture& utex = *lockedU;
+ Texture& vtex = *lockedV;
+ Texture& dsttex = *lockedDst;
+ // All YUV planes must currently be represented by R8 or R16 textures.
+ // The chroma (U/V) planes must have matching dimensions.
+ assert(ytex.bpp() == utex.bpp() && ytex.bpp() == vtex.bpp());
+ assert((ytex.bpp() == 1 && colorDepth == 8) ||
+ (ytex.bpp() == 2 && colorDepth > 8));
+ // assert(ytex.width == utex.width && ytex.height == utex.height);
+ assert(utex.width == vtex.width && utex.height == vtex.height);
+ assert(ytex.offset == utex.offset && ytex.offset == vtex.offset);
+ assert(dsttex.bpp() == 4);
+
+ IntRect srcReq =
+ IntRect{srcX, srcY, srcX + srcWidth, srcY + srcHeight} - ytex.offset;
+ IntRect dstReq =
+ IntRect{dstX, dstY, dstX + dstWidth, dstY + dstHeight} - dsttex.offset;
+ if (srcReq.is_empty() || dstReq.is_empty()) {
+ return;
+ }
+
+ // Compute clip rect as relative to the dstReq, as that's the same coords
+ // as used for the sampling bounds.
+ IntRect clipRect = {clipX - dstX, clipY - dstY, clipX - dstX + clipWidth,
+ clipY - dstY + clipHeight};
+ // For now, always use a linear filter path that would be required for
+ // scaling. Further fast-paths for non-scaled video might be desirable in the
+ // future.
+ linear_convert_yuv(ytex, utex, vtex, rgbFromYcbcr, colorDepth, srcReq, dsttex,
+ dstReq, flipX, flipY, clipRect);
+}
+
+} // extern "C"
diff --git a/gfx/wr/swgl/src/gl.cc b/gfx/wr/swgl/src/gl.cc
new file mode 100644
index 0000000000..0b09e29008
--- /dev/null
+++ b/gfx/wr/swgl/src/gl.cc
@@ -0,0 +1,2851 @@
+/* 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/. */
+
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <assert.h>
+#include <stdio.h>
+#include <math.h>
+
+#ifdef __MACH__
+# include <mach/mach.h>
+# include <mach/mach_time.h>
+#else
+# include <time.h>
+#endif
+
+#ifdef NDEBUG
+# define debugf(...)
+#else
+# define debugf(...) printf(__VA_ARGS__)
+#endif
+
+// #define PRINT_TIMINGS
+
+#ifdef _WIN32
+# define ALWAYS_INLINE __forceinline
+# define NO_INLINE __declspec(noinline)
+
+// Including Windows.h brings a huge amount of namespace polution so just
+// define a couple of things manually
+typedef int BOOL;
+# define WINAPI __stdcall
+# define DECLSPEC_IMPORT __declspec(dllimport)
+# define WINBASEAPI DECLSPEC_IMPORT
+typedef unsigned long DWORD;
+typedef long LONG;
+typedef __int64 LONGLONG;
+# define DUMMYSTRUCTNAME
+
+typedef union _LARGE_INTEGER {
+ struct {
+ DWORD LowPart;
+ LONG HighPart;
+ } DUMMYSTRUCTNAME;
+ struct {
+ DWORD LowPart;
+ LONG HighPart;
+ } u;
+ LONGLONG QuadPart;
+} LARGE_INTEGER;
+extern "C" {
+WINBASEAPI BOOL WINAPI
+QueryPerformanceCounter(LARGE_INTEGER* lpPerformanceCount);
+
+WINBASEAPI BOOL WINAPI QueryPerformanceFrequency(LARGE_INTEGER* lpFrequency);
+}
+
+#else
+// GCC is slower when dealing with always_inline, especially in debug builds.
+// When using Clang, use always_inline more aggressively.
+# if defined(__clang__) || defined(NDEBUG)
+# define ALWAYS_INLINE __attribute__((always_inline)) inline
+# else
+# define ALWAYS_INLINE inline
+# endif
+# define NO_INLINE __attribute__((noinline))
+#endif
+
+// Some functions may cause excessive binary bloat if inlined in debug or with
+// GCC builds, so use PREFER_INLINE on these instead of ALWAYS_INLINE.
+#if defined(__clang__) && defined(NDEBUG)
+# define PREFER_INLINE ALWAYS_INLINE
+#else
+# define PREFER_INLINE inline
+#endif
+
+#define UNREACHABLE __builtin_unreachable()
+
+#define UNUSED [[maybe_unused]]
+
+#define FALLTHROUGH [[fallthrough]]
+
+#if defined(MOZILLA_CLIENT) && defined(MOZ_CLANG_PLUGIN)
+# define IMPLICIT __attribute__((annotate("moz_implicit")))
+#else
+# define IMPLICIT
+#endif
+
+#include "gl_defs.h"
+#include "glsl.h"
+#include "program.h"
+#include "texture.h"
+
+using namespace glsl;
+
+typedef ivec2_scalar IntPoint;
+
+struct IntRect {
+ int x0;
+ int y0;
+ int x1;
+ int y1;
+
+ IntRect() : x0(0), y0(0), x1(0), y1(0) {}
+ IntRect(int x0, int y0, int x1, int y1) : x0(x0), y0(y0), x1(x1), y1(y1) {}
+ IntRect(IntPoint origin, IntPoint size)
+ : x0(origin.x),
+ y0(origin.y),
+ x1(origin.x + size.x),
+ y1(origin.y + size.y) {}
+
+ int width() const { return x1 - x0; }
+ int height() const { return y1 - y0; }
+ bool is_empty() const { return width() <= 0 || height() <= 0; }
+
+ IntPoint origin() const { return IntPoint(x0, y0); }
+
+ bool same_size(const IntRect& o) const {
+ return width() == o.width() && height() == o.height();
+ }
+
+ bool contains(const IntRect& o) const {
+ return o.x0 >= x0 && o.y0 >= y0 && o.x1 <= x1 && o.y1 <= y1;
+ }
+
+ IntRect& intersect(const IntRect& o) {
+ x0 = max(x0, o.x0);
+ y0 = max(y0, o.y0);
+ x1 = min(x1, o.x1);
+ y1 = min(y1, o.y1);
+ return *this;
+ }
+
+ IntRect intersection(const IntRect& o) {
+ IntRect result = *this;
+ result.intersect(o);
+ return result;
+ }
+
+ // Scale from source-space to dest-space, optionally rounding inward
+ IntRect& scale(int srcWidth, int srcHeight, int dstWidth, int dstHeight,
+ bool roundIn = false) {
+ x0 = (x0 * dstWidth + (roundIn ? srcWidth - 1 : 0)) / srcWidth;
+ y0 = (y0 * dstHeight + (roundIn ? srcHeight - 1 : 0)) / srcHeight;
+ x1 = (x1 * dstWidth) / srcWidth;
+ y1 = (y1 * dstHeight) / srcHeight;
+ return *this;
+ }
+
+ // Flip the rect's Y coords around inflection point at Y=offset
+ void invert_y(int offset) {
+ y0 = offset - y0;
+ y1 = offset - y1;
+ swap(y0, y1);
+ }
+
+ IntRect& offset(const IntPoint& o) {
+ x0 += o.x;
+ y0 += o.y;
+ x1 += o.x;
+ y1 += o.y;
+ return *this;
+ }
+
+ IntRect operator+(const IntPoint& o) const {
+ return IntRect(*this).offset(o);
+ }
+ IntRect operator-(const IntPoint& o) const {
+ return IntRect(*this).offset(-o);
+ }
+};
+
+typedef vec2_scalar Point2D;
+typedef vec4_scalar Point3D;
+
+struct IntRange {
+ int start;
+ int end;
+
+ int len() const { return end - start; }
+
+ IntRange intersect(IntRange r) const {
+ return {max(start, r.start), min(end, r.end)};
+ }
+};
+
+struct FloatRange {
+ float start;
+ float end;
+
+ float clip(float x) const { return clamp(x, start, end); }
+
+ FloatRange clip(FloatRange r) const { return {clip(r.start), clip(r.end)}; }
+
+ FloatRange merge(FloatRange r) const {
+ return {min(start, r.start), max(end, r.end)};
+ }
+
+ IntRange round() const {
+ return {int(floor(start + 0.5f)), int(floor(end + 0.5f))};
+ }
+
+ IntRange round_out() const { return {int(floor(start)), int(ceil(end))}; }
+};
+
+template <typename P>
+static inline FloatRange x_range(P p0, P p1) {
+ return {min(p0.x, p1.x), max(p0.x, p1.x)};
+}
+
+struct VertexAttrib {
+ size_t size = 0; // in bytes
+ GLenum type = 0;
+ bool normalized = false;
+ GLsizei stride = 0;
+ GLuint offset = 0;
+ bool enabled = false;
+ GLuint divisor = 0;
+ int vertex_array = 0;
+ int vertex_buffer = 0;
+ char* buf = nullptr; // XXX: this can easily dangle
+ size_t buf_size = 0; // this will let us bounds check
+
+ // Mark the buffer as invalid so we don't accidentally use stale data.
+ void disable() {
+ enabled = false;
+ buf = nullptr;
+ buf_size = 0;
+ }
+};
+
+static int bytes_for_internal_format(GLenum internal_format) {
+ switch (internal_format) {
+ case GL_RGBA32F:
+ return 4 * 4;
+ case GL_RGBA32I:
+ return 4 * 4;
+ case GL_RGBA8:
+ case GL_BGRA8:
+ case GL_RGBA:
+ return 4;
+ case GL_R8:
+ case GL_RED:
+ return 1;
+ case GL_RG8:
+ case GL_RG:
+ return 2;
+ case GL_DEPTH_COMPONENT:
+ case GL_DEPTH_COMPONENT16:
+ case GL_DEPTH_COMPONENT24:
+ case GL_DEPTH_COMPONENT32:
+ return 4;
+ case GL_RGB_RAW_422_APPLE:
+ return 2;
+ case GL_R16:
+ return 2;
+ case GL_RG16:
+ return 4;
+ default:
+ debugf("internal format: %x\n", internal_format);
+ assert(0);
+ return 0;
+ }
+}
+
+static inline int aligned_stride(int row_bytes) { return (row_bytes + 3) & ~3; }
+
+static TextureFormat gl_format_to_texture_format(int type) {
+ switch (type) {
+ case GL_RGBA32F:
+ return TextureFormat::RGBA32F;
+ case GL_RGBA32I:
+ return TextureFormat::RGBA32I;
+ case GL_RGBA8:
+ return TextureFormat::RGBA8;
+ case GL_R8:
+ return TextureFormat::R8;
+ case GL_RG8:
+ return TextureFormat::RG8;
+ case GL_R16:
+ return TextureFormat::R16;
+ case GL_RG16:
+ return TextureFormat::RG16;
+ case GL_RGB_RAW_422_APPLE:
+ return TextureFormat::YUV422;
+ default:
+ assert(0);
+ return TextureFormat::RGBA8;
+ }
+}
+
+struct Query {
+ uint64_t value = 0;
+};
+
+struct Buffer {
+ char* buf = nullptr;
+ size_t size = 0;
+ size_t capacity = 0;
+
+ // Returns true if re-allocation succeeded, false otherwise...
+ bool allocate(size_t new_size) {
+ // If the size remains unchanged, don't allocate anything.
+ if (new_size == size) {
+ return true;
+ }
+ // If the new size is within the existing capacity of the buffer, just
+ // reuse the existing buffer.
+ if (new_size <= capacity) {
+ size = new_size;
+ return true;
+ }
+ // Otherwise we need to reallocate the buffer to hold up to the requested
+ // larger size.
+ char* new_buf = (char*)realloc(buf, new_size);
+ assert(new_buf);
+ if (!new_buf) {
+ // If we fail, null out the buffer rather than leave around the old
+ // allocation state.
+ cleanup();
+ return false;
+ }
+ // The reallocation succeeded, so install the buffer.
+ buf = new_buf;
+ size = new_size;
+ capacity = new_size;
+ return true;
+ }
+
+ void cleanup() {
+ if (buf) {
+ free(buf);
+ buf = nullptr;
+ size = 0;
+ capacity = 0;
+ }
+ }
+
+ ~Buffer() { cleanup(); }
+};
+
+struct Framebuffer {
+ GLuint color_attachment = 0;
+ GLuint depth_attachment = 0;
+};
+
+struct Renderbuffer {
+ GLuint texture = 0;
+
+ void on_erase();
+};
+
+TextureFilter gl_filter_to_texture_filter(int type) {
+ switch (type) {
+ case GL_NEAREST:
+ return TextureFilter::NEAREST;
+ case GL_NEAREST_MIPMAP_LINEAR:
+ return TextureFilter::NEAREST;
+ case GL_NEAREST_MIPMAP_NEAREST:
+ return TextureFilter::NEAREST;
+ case GL_LINEAR:
+ return TextureFilter::LINEAR;
+ case GL_LINEAR_MIPMAP_LINEAR:
+ return TextureFilter::LINEAR;
+ case GL_LINEAR_MIPMAP_NEAREST:
+ return TextureFilter::LINEAR;
+ default:
+ assert(0);
+ return TextureFilter::NEAREST;
+ }
+}
+
+struct Texture {
+ GLenum internal_format = 0;
+ int width = 0;
+ int height = 0;
+ char* buf = nullptr;
+ size_t buf_size = 0;
+ uint32_t buf_stride = 0;
+ uint8_t buf_bpp = 0;
+ GLenum min_filter = GL_NEAREST;
+ GLenum mag_filter = GL_LINEAR;
+ // The number of active locks on this texture. If this texture has any active
+ // locks, we need to disallow modifying or destroying the texture as it may
+ // be accessed by other threads where modifications could lead to races.
+ int32_t locked = 0;
+ // When used as an attachment of a framebuffer, rendering to the texture
+ // behaves as if it is located at the given offset such that the offset is
+ // subtracted from all transformed vertexes after the viewport is applied.
+ IntPoint offset;
+
+ enum FLAGS {
+ // If the buffer is internally-allocated by SWGL
+ SHOULD_FREE = 1 << 1,
+ // If the buffer has been cleared to initialize it. Currently this is only
+ // utilized by depth buffers which need to know when depth runs have reset
+ // to a valid row state. When unset, the depth runs may contain garbage.
+ CLEARED = 1 << 2,
+ };
+ int flags = SHOULD_FREE;
+ bool should_free() const { return bool(flags & SHOULD_FREE); }
+ bool cleared() const { return bool(flags & CLEARED); }
+
+ void set_flag(int flag, bool val) {
+ if (val) {
+ flags |= flag;
+ } else {
+ flags &= ~flag;
+ }
+ }
+ void set_should_free(bool val) {
+ // buf must be null before SHOULD_FREE can be safely toggled. Otherwise, we
+ // might accidentally mistakenly realloc an externally allocated buffer as
+ // if it were an internally allocated one.
+ assert(!buf);
+ set_flag(SHOULD_FREE, val);
+ }
+ void set_cleared(bool val) { set_flag(CLEARED, val); }
+
+ // Delayed-clearing state. When a clear of an FB is requested, we don't
+ // immediately clear each row, as the rows may be subsequently overwritten
+ // by draw calls, allowing us to skip the work of clearing the affected rows
+ // either fully or partially. Instead, we keep a bit vector of rows that need
+ // to be cleared later and save the value they need to be cleared with so
+ // that we can clear these rows individually when they are touched by draws.
+ // This currently only works for 2D textures, but not on texture arrays.
+ int delay_clear = 0;
+ uint32_t clear_val = 0;
+ uint32_t* cleared_rows = nullptr;
+
+ void init_depth_runs(uint32_t z);
+ void fill_depth_runs(uint32_t z, const IntRect& scissor);
+
+ void enable_delayed_clear(uint32_t val) {
+ delay_clear = height;
+ clear_val = val;
+ if (!cleared_rows) {
+ cleared_rows = new uint32_t[(height + 31) / 32];
+ }
+ memset(cleared_rows, 0, ((height + 31) / 32) * sizeof(uint32_t));
+ if (height & 31) {
+ cleared_rows[height / 32] = ~0U << (height & 31);
+ }
+ }
+
+ void disable_delayed_clear() {
+ if (cleared_rows) {
+ delete[] cleared_rows;
+ cleared_rows = nullptr;
+ delay_clear = 0;
+ }
+ }
+
+ int bpp() const { return buf_bpp; }
+ int compute_bpp() const { return bytes_for_internal_format(internal_format); }
+
+ size_t stride() const { return buf_stride; }
+ size_t compute_stride(int bpp, int width) const {
+ return aligned_stride(bpp * width);
+ }
+
+ // Set an external backing buffer of this texture.
+ void set_buffer(void* new_buf, size_t new_stride) {
+ assert(!should_free());
+ // Ensure that the supplied stride is at least as big as the row data and
+ // is aligned to the smaller of either the BPP or word-size. We need to at
+ // least be able to sample data from within a row and sample whole pixels
+ // of smaller formats without risking unaligned access.
+ int new_bpp = compute_bpp();
+ assert(new_stride >= size_t(new_bpp * width) &&
+ new_stride % min(new_bpp, sizeof(uint32_t)) == 0);
+
+ buf = (char*)new_buf;
+ buf_size = 0;
+ buf_bpp = new_bpp;
+ buf_stride = new_stride;
+ }
+
+ // Returns true if re-allocation succeeded, false otherwise...
+ bool allocate(bool force = false, int min_width = 0, int min_height = 0) {
+ assert(!locked); // Locked textures shouldn't be reallocated
+ // If we get here, some GL API call that invalidates the texture was used.
+ // Mark the buffer as not-cleared to signal this.
+ set_cleared(false);
+ // Check if there is either no buffer currently or if we forced validation
+ // of the buffer size because some dimension might have changed.
+ if ((!buf || force) && should_free()) {
+ // Compute the buffer's BPP and stride, since they may have changed.
+ int new_bpp = compute_bpp();
+ size_t new_stride = compute_stride(new_bpp, width);
+ // Compute new size based on the maximum potential stride, rather than
+ // the current stride, to hopefully avoid reallocations when size would
+ // otherwise change too much...
+ size_t max_stride = compute_stride(new_bpp, max(width, min_width));
+ size_t size = max_stride * max(height, min_height);
+ if ((!buf && size > 0) || size > buf_size) {
+ // Allocate with a SIMD register-sized tail of padding at the end so we
+ // can safely read or write past the end of the texture with SIMD ops.
+ // Currently only the flat Z-buffer texture needs this padding due to
+ // full-register loads and stores in check_depth and discard_depth. In
+ // case some code in the future accidentally uses a linear filter on a
+ // texture with less than 2 pixels per row, we also add this padding
+ // just to be safe. All other texture types and use-cases should be
+ // safe to omit padding.
+ size_t padding =
+ internal_format == GL_DEPTH_COMPONENT24 || max(width, min_width) < 2
+ ? sizeof(Float)
+ : 0;
+ char* new_buf = (char*)realloc(buf, size + padding);
+ assert(new_buf);
+ if (!new_buf) {
+ // Allocation failed, so ensure we don't leave stale buffer state.
+ cleanup();
+ return false;
+ }
+ // Successfully reallocated the buffer, so go ahead and set it.
+ buf = new_buf;
+ buf_size = size;
+ }
+ // Set the BPP and stride in case they changed.
+ buf_bpp = new_bpp;
+ buf_stride = new_stride;
+ }
+ // Allocation succeeded or nothing changed...
+ return true;
+ }
+
+ void cleanup() {
+ assert(!locked); // Locked textures shouldn't be destroyed
+ if (buf) {
+ // If we need to toggle SHOULD_FREE state, ensure that buf is nulled out,
+ // regardless of whether we internally allocated it. This will prevent us
+ // from wrongly treating buf as having been internally allocated for when
+ // we go to realloc if it actually was externally allocted.
+ if (should_free()) {
+ free(buf);
+ }
+ buf = nullptr;
+ buf_size = 0;
+ buf_bpp = 0;
+ buf_stride = 0;
+ }
+ disable_delayed_clear();
+ }
+
+ ~Texture() { cleanup(); }
+
+ IntRect bounds() const { return IntRect{0, 0, width, height}; }
+ IntRect offset_bounds() const { return bounds() + offset; }
+
+ // Find the valid sampling bounds relative to the requested region
+ IntRect sample_bounds(const IntRect& req, bool invertY = false) const {
+ IntRect bb = bounds().intersect(req) - req.origin();
+ if (invertY) bb.invert_y(req.height());
+ return bb;
+ }
+
+ // Get a pointer for sampling at the given offset
+ char* sample_ptr(int x, int y) const {
+ return buf + y * stride() + x * bpp();
+ }
+
+ // Get a pointer for sampling the requested region and limit to the provided
+ // sampling bounds
+ char* sample_ptr(const IntRect& req, const IntRect& bounds,
+ bool invertY = false) const {
+ // Offset the sample pointer by the clamped bounds
+ int x = req.x0 + bounds.x0;
+ // Invert the Y offset if necessary
+ int y = invertY ? req.y1 - 1 - bounds.y0 : req.y0 + bounds.y0;
+ return sample_ptr(x, y);
+ }
+};
+
+// The last vertex attribute is reserved as a null attribute in case a vertex
+// attribute is used without being set.
+#define MAX_ATTRIBS 17
+#define NULL_ATTRIB 16
+struct VertexArray {
+ VertexAttrib attribs[MAX_ATTRIBS];
+ int max_attrib = -1;
+ // The GL spec defines element array buffer binding to be part of VAO state.
+ GLuint element_array_buffer_binding = 0;
+
+ void validate();
+};
+
+struct Shader {
+ GLenum type = 0;
+ ProgramLoader loader = nullptr;
+};
+
+struct Program {
+ ProgramImpl* impl = nullptr;
+ VertexShaderImpl* vert_impl = nullptr;
+ FragmentShaderImpl* frag_impl = nullptr;
+ bool deleted = false;
+
+ ~Program() { delete impl; }
+};
+
+// clang-format off
+// Fully-expand GL defines while ignoring more than 4 suffixes
+#define CONCAT_KEY(prefix, x, y, z, w, ...) prefix##x##y##z##w
+// Generate a blend key enum symbol
+#define BLEND_KEY(...) CONCAT_KEY(BLEND_, __VA_ARGS__, 0, 0, 0)
+#define MASK_BLEND_KEY(...) CONCAT_KEY(MASK_BLEND_, __VA_ARGS__, 0, 0, 0)
+#define AA_BLEND_KEY(...) CONCAT_KEY(AA_BLEND_, __VA_ARGS__, 0, 0, 0)
+#define AA_MASK_BLEND_KEY(...) CONCAT_KEY(AA_MASK_BLEND_, __VA_ARGS__, 0, 0, 0)
+
+// Utility macro to easily generate similar code for all implemented blend modes
+#define FOR_EACH_BLEND_KEY(macro) \
+ macro(GL_ONE, GL_ZERO, 0, 0) \
+ macro(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA) \
+ macro(GL_ONE, GL_ONE_MINUS_SRC_ALPHA, 0, 0) \
+ macro(GL_ZERO, GL_ONE_MINUS_SRC_COLOR, 0, 0) \
+ macro(GL_ZERO, GL_ONE_MINUS_SRC_COLOR, GL_ZERO, GL_ONE) \
+ macro(GL_ZERO, GL_ONE_MINUS_SRC_ALPHA, 0, 0) \
+ macro(GL_ZERO, GL_SRC_COLOR, 0, 0) \
+ macro(GL_ONE, GL_ONE, 0, 0) \
+ macro(GL_ONE, GL_ONE, GL_ONE, GL_ONE_MINUS_SRC_ALPHA) \
+ macro(GL_ONE_MINUS_DST_ALPHA, GL_ONE, GL_ZERO, GL_ONE) \
+ macro(GL_CONSTANT_COLOR, GL_ONE_MINUS_SRC_COLOR, 0, 0) \
+ macro(GL_ONE, GL_ONE_MINUS_SRC1_COLOR, 0, 0) \
+ macro(GL_MIN, 0, 0, 0) \
+ macro(GL_MAX, 0, 0, 0) \
+ macro(GL_MULTIPLY_KHR, 0, 0, 0) \
+ macro(GL_SCREEN_KHR, 0, 0, 0) \
+ macro(GL_OVERLAY_KHR, 0, 0, 0) \
+ macro(GL_DARKEN_KHR, 0, 0, 0) \
+ macro(GL_LIGHTEN_KHR, 0, 0, 0) \
+ macro(GL_COLORDODGE_KHR, 0, 0, 0) \
+ macro(GL_COLORBURN_KHR, 0, 0, 0) \
+ macro(GL_HARDLIGHT_KHR, 0, 0, 0) \
+ macro(GL_SOFTLIGHT_KHR, 0, 0, 0) \
+ macro(GL_DIFFERENCE_KHR, 0, 0, 0) \
+ macro(GL_EXCLUSION_KHR, 0, 0, 0) \
+ macro(GL_HSL_HUE_KHR, 0, 0, 0) \
+ macro(GL_HSL_SATURATION_KHR, 0, 0, 0) \
+ macro(GL_HSL_COLOR_KHR, 0, 0, 0) \
+ macro(GL_HSL_LUMINOSITY_KHR, 0, 0, 0) \
+ macro(SWGL_BLEND_DROP_SHADOW, 0, 0, 0) \
+ macro(SWGL_BLEND_SUBPIXEL_TEXT, 0, 0, 0)
+
+#define DEFINE_BLEND_KEY(...) BLEND_KEY(__VA_ARGS__),
+#define DEFINE_MASK_BLEND_KEY(...) MASK_BLEND_KEY(__VA_ARGS__),
+#define DEFINE_AA_BLEND_KEY(...) AA_BLEND_KEY(__VA_ARGS__),
+#define DEFINE_AA_MASK_BLEND_KEY(...) AA_MASK_BLEND_KEY(__VA_ARGS__),
+enum BlendKey : uint8_t {
+ FOR_EACH_BLEND_KEY(DEFINE_BLEND_KEY)
+ FOR_EACH_BLEND_KEY(DEFINE_MASK_BLEND_KEY)
+ FOR_EACH_BLEND_KEY(DEFINE_AA_BLEND_KEY)
+ FOR_EACH_BLEND_KEY(DEFINE_AA_MASK_BLEND_KEY)
+ BLEND_KEY_NONE = BLEND_KEY(GL_ONE, GL_ZERO),
+ MASK_BLEND_KEY_NONE = MASK_BLEND_KEY(GL_ONE, GL_ZERO),
+ AA_BLEND_KEY_NONE = AA_BLEND_KEY(GL_ONE, GL_ZERO),
+ AA_MASK_BLEND_KEY_NONE = AA_MASK_BLEND_KEY(GL_ONE, GL_ZERO),
+};
+// clang-format on
+
+const size_t MAX_TEXTURE_UNITS = 16;
+
+template <typename T>
+static inline bool unlink(T& binding, T n) {
+ if (binding == n) {
+ binding = 0;
+ return true;
+ }
+ return false;
+}
+
+template <typename O>
+struct ObjectStore {
+ O** objects = nullptr;
+ size_t size = 0;
+ // reserve object 0 as null
+ size_t first_free = 1;
+ O invalid;
+
+ ~ObjectStore() {
+ if (objects) {
+ for (size_t i = 0; i < size; i++) delete objects[i];
+ free(objects);
+ }
+ }
+
+ bool grow(size_t i) {
+ size_t new_size = size ? size : 8;
+ while (new_size <= i) new_size += new_size / 2;
+ O** new_objects = (O**)realloc(objects, new_size * sizeof(O*));
+ assert(new_objects);
+ if (!new_objects) return false;
+ while (size < new_size) new_objects[size++] = nullptr;
+ objects = new_objects;
+ return true;
+ }
+
+ void insert(size_t i, const O& o) {
+ if (i >= size && !grow(i)) return;
+ if (!objects[i]) objects[i] = new O(o);
+ }
+
+ size_t next_free() {
+ size_t i = first_free;
+ while (i < size && objects[i]) i++;
+ first_free = i;
+ return i;
+ }
+
+ size_t insert(const O& o = O()) {
+ size_t i = next_free();
+ insert(i, o);
+ return i;
+ }
+
+ O& operator[](size_t i) {
+ insert(i, O());
+ return i < size ? *objects[i] : invalid;
+ }
+
+ O* find(size_t i) const { return i < size ? objects[i] : nullptr; }
+
+ template <typename T>
+ void on_erase(T*, ...) {}
+ template <typename T>
+ void on_erase(T* o, decltype(&T::on_erase)) {
+ o->on_erase();
+ }
+
+ bool erase(size_t i) {
+ if (i < size && objects[i]) {
+ on_erase(objects[i], nullptr);
+ delete objects[i];
+ objects[i] = nullptr;
+ if (i < first_free) first_free = i;
+ return true;
+ }
+ return false;
+ }
+
+ O** begin() const { return objects; }
+ O** end() const { return &objects[size]; }
+};
+
+struct Context {
+ int32_t references = 1;
+
+ ObjectStore<Query> queries;
+ ObjectStore<Buffer> buffers;
+ ObjectStore<Texture> textures;
+ ObjectStore<VertexArray> vertex_arrays;
+ ObjectStore<Framebuffer> framebuffers;
+ ObjectStore<Renderbuffer> renderbuffers;
+ ObjectStore<Shader> shaders;
+ ObjectStore<Program> programs;
+
+ GLenum last_error = GL_NO_ERROR;
+
+ IntRect viewport = {0, 0, 0, 0};
+
+ bool blend = false;
+ GLenum blendfunc_srgb = GL_ONE;
+ GLenum blendfunc_drgb = GL_ZERO;
+ GLenum blendfunc_sa = GL_ONE;
+ GLenum blendfunc_da = GL_ZERO;
+ GLenum blend_equation = GL_FUNC_ADD;
+ V8<uint16_t> blendcolor = 0;
+ BlendKey blend_key = BLEND_KEY_NONE;
+
+ bool depthtest = false;
+ bool depthmask = true;
+ GLenum depthfunc = GL_LESS;
+
+ bool scissortest = false;
+ IntRect scissor = {0, 0, 0, 0};
+
+ GLfloat clearcolor[4] = {0, 0, 0, 0};
+ GLdouble cleardepth = 1;
+
+ int unpack_row_length = 0;
+
+ int shaded_rows = 0;
+ int shaded_pixels = 0;
+
+ struct TextureUnit {
+ GLuint texture_2d_binding = 0;
+ GLuint texture_rectangle_binding = 0;
+
+ void unlink(GLuint n) {
+ ::unlink(texture_2d_binding, n);
+ ::unlink(texture_rectangle_binding, n);
+ }
+ };
+ TextureUnit texture_units[MAX_TEXTURE_UNITS];
+ int active_texture_unit = 0;
+
+ GLuint current_program = 0;
+
+ GLuint current_vertex_array = 0;
+ bool validate_vertex_array = true;
+
+ GLuint pixel_pack_buffer_binding = 0;
+ GLuint pixel_unpack_buffer_binding = 0;
+ GLuint array_buffer_binding = 0;
+ GLuint time_elapsed_query = 0;
+ GLuint samples_passed_query = 0;
+ GLuint renderbuffer_binding = 0;
+ GLuint draw_framebuffer_binding = 0;
+ GLuint read_framebuffer_binding = 0;
+ GLuint unknown_binding = 0;
+
+ GLuint& get_binding(GLenum name) {
+ switch (name) {
+ case GL_PIXEL_PACK_BUFFER:
+ return pixel_pack_buffer_binding;
+ case GL_PIXEL_UNPACK_BUFFER:
+ return pixel_unpack_buffer_binding;
+ case GL_ARRAY_BUFFER:
+ return array_buffer_binding;
+ case GL_ELEMENT_ARRAY_BUFFER:
+ return vertex_arrays[current_vertex_array].element_array_buffer_binding;
+ case GL_TEXTURE_2D:
+ return texture_units[active_texture_unit].texture_2d_binding;
+ case GL_TEXTURE_RECTANGLE:
+ return texture_units[active_texture_unit].texture_rectangle_binding;
+ case GL_TIME_ELAPSED:
+ return time_elapsed_query;
+ case GL_SAMPLES_PASSED:
+ return samples_passed_query;
+ case GL_RENDERBUFFER:
+ return renderbuffer_binding;
+ case GL_DRAW_FRAMEBUFFER:
+ return draw_framebuffer_binding;
+ case GL_READ_FRAMEBUFFER:
+ return read_framebuffer_binding;
+ default:
+ debugf("unknown binding %x\n", name);
+ assert(false);
+ return unknown_binding;
+ }
+ }
+
+ Texture& get_texture(sampler2D, int unit) {
+ return textures[texture_units[unit].texture_2d_binding];
+ }
+
+ Texture& get_texture(isampler2D, int unit) {
+ return textures[texture_units[unit].texture_2d_binding];
+ }
+
+ Texture& get_texture(sampler2DRect, int unit) {
+ return textures[texture_units[unit].texture_rectangle_binding];
+ }
+
+ IntRect apply_scissor(IntRect bb,
+ const IntPoint& origin = IntPoint(0, 0)) const {
+ return scissortest ? bb.intersect(scissor - origin) : bb;
+ }
+
+ IntRect apply_scissor(const Texture& t) const {
+ return apply_scissor(t.bounds(), t.offset);
+ }
+};
+static Context* ctx = nullptr;
+static VertexShaderImpl* vertex_shader = nullptr;
+static FragmentShaderImpl* fragment_shader = nullptr;
+static BlendKey blend_key = BLEND_KEY_NONE;
+
+static void prepare_texture(Texture& t, const IntRect* skip = nullptr);
+
+template <typename S>
+static inline void init_filter(S* s, Texture& t) {
+ // If the width is not at least 2 pixels, then we can't safely sample the end
+ // of the row with a linear filter. In that case, just punt to using nearest
+ // filtering instead.
+ s->filter = t.width >= 2 ? gl_filter_to_texture_filter(t.mag_filter)
+ : TextureFilter::NEAREST;
+}
+
+template <typename S>
+static inline void init_sampler(S* s, Texture& t) {
+ prepare_texture(t);
+ s->width = t.width;
+ s->height = t.height;
+ s->stride = t.stride();
+ int bpp = t.bpp();
+ if (bpp >= 4)
+ s->stride /= 4;
+ else if (bpp == 2)
+ s->stride /= 2;
+ else
+ assert(bpp == 1);
+ // Use uint32_t* for easier sampling, but need to cast to uint8_t* or
+ // uint16_t* for formats with bpp < 4.
+ s->buf = (uint32_t*)t.buf;
+ s->format = gl_format_to_texture_format(t.internal_format);
+}
+
+template <typename S>
+static inline void null_sampler(S* s) {
+ // For null texture data, just make the sampler provide a 1x1 buffer that is
+ // transparent black. Ensure buffer holds at least a SIMD vector of zero data
+ // for SIMD padding of unaligned loads.
+ static const uint32_t zeroBuf[sizeof(Float) / sizeof(uint32_t)] = {0};
+ s->width = 1;
+ s->height = 1;
+ s->stride = s->width;
+ s->buf = (uint32_t*)zeroBuf;
+ s->format = TextureFormat::RGBA8;
+}
+
+template <typename S>
+static inline void null_filter(S* s) {
+ s->filter = TextureFilter::NEAREST;
+}
+
+template <typename S>
+S* lookup_sampler(S* s, int texture) {
+ Texture& t = ctx->get_texture(s, texture);
+ if (!t.buf) {
+ null_sampler(s);
+ null_filter(s);
+ } else {
+ init_sampler(s, t);
+ init_filter(s, t);
+ }
+ return s;
+}
+
+template <typename S>
+S* lookup_isampler(S* s, int texture) {
+ Texture& t = ctx->get_texture(s, texture);
+ if (!t.buf) {
+ null_sampler(s);
+ } else {
+ init_sampler(s, t);
+ }
+ return s;
+}
+
+int bytes_per_type(GLenum type) {
+ switch (type) {
+ case GL_INT:
+ return 4;
+ case GL_FLOAT:
+ return 4;
+ case GL_UNSIGNED_SHORT:
+ return 2;
+ case GL_UNSIGNED_BYTE:
+ return 1;
+ default:
+ assert(0);
+ return 0;
+ }
+}
+
+template <typename S, typename C>
+static inline S expand_attrib(const char* buf, size_t size, bool normalized) {
+ typedef typename ElementType<S>::ty elem_type;
+ S scalar = {0};
+ const C* src = reinterpret_cast<const C*>(buf);
+ if (normalized) {
+ const float scale = 1.0f / ((1 << (8 * sizeof(C))) - 1);
+ for (size_t i = 0; i < size / sizeof(C); i++) {
+ put_nth_component(scalar, i, elem_type(src[i]) * scale);
+ }
+ } else {
+ for (size_t i = 0; i < size / sizeof(C); i++) {
+ put_nth_component(scalar, i, elem_type(src[i]));
+ }
+ }
+ return scalar;
+}
+
+template <typename S>
+static inline S load_attrib_scalar(VertexAttrib& va, const char* src) {
+ if (sizeof(S) <= va.size) {
+ return *reinterpret_cast<const S*>(src);
+ }
+ if (va.type == GL_UNSIGNED_SHORT) {
+ return expand_attrib<S, uint16_t>(src, va.size, va.normalized);
+ }
+ if (va.type == GL_UNSIGNED_BYTE) {
+ return expand_attrib<S, uint8_t>(src, va.size, va.normalized);
+ }
+ assert(sizeof(typename ElementType<S>::ty) == bytes_per_type(va.type));
+ S scalar = {0};
+ memcpy(&scalar, src, va.size);
+ return scalar;
+}
+
+template <typename T>
+void load_attrib(T& attrib, VertexAttrib& va, uint32_t start, int instance,
+ int count) {
+ typedef decltype(force_scalar(attrib)) scalar_type;
+ // If no buffer is available, just use a zero default.
+ if (!va.buf_size) {
+ attrib = T(scalar_type{0});
+ } else if (va.divisor != 0) {
+ char* src = (char*)va.buf + va.stride * instance + va.offset;
+ assert(src + va.size <= va.buf + va.buf_size);
+ attrib = T(load_attrib_scalar<scalar_type>(va, src));
+ } else {
+ // Specialized for WR's primitive vertex order/winding.
+ if (!count) return;
+ assert(count >= 2 && count <= 4);
+ char* src = (char*)va.buf + va.stride * start + va.offset;
+ switch (count) {
+ case 2: {
+ // Lines must be indexed at offsets 0, 1.
+ // Line vertexes fill vertex shader SIMD lanes as 0, 1, 1, 0.
+ scalar_type lanes[2] = {
+ load_attrib_scalar<scalar_type>(va, src),
+ load_attrib_scalar<scalar_type>(va, src + va.stride)};
+ attrib = (T){lanes[0], lanes[1], lanes[1], lanes[0]};
+ break;
+ }
+ case 3: {
+ // Triangles must be indexed at offsets 0, 1, 2.
+ // Triangle vertexes fill vertex shader SIMD lanes as 0, 1, 2, 2.
+ scalar_type lanes[3] = {
+ load_attrib_scalar<scalar_type>(va, src),
+ load_attrib_scalar<scalar_type>(va, src + va.stride),
+ load_attrib_scalar<scalar_type>(va, src + va.stride * 2)};
+ attrib = (T){lanes[0], lanes[1], lanes[2], lanes[2]};
+ break;
+ }
+ default:
+ // Quads must be successive triangles indexed at offsets 0, 1, 2, 2,
+ // 1, 3. Quad vertexes fill vertex shader SIMD lanes as 0, 1, 3, 2, so
+ // that the points form a convex path that can be traversed by the
+ // rasterizer.
+ attrib = (T){load_attrib_scalar<scalar_type>(va, src),
+ load_attrib_scalar<scalar_type>(va, src + va.stride),
+ load_attrib_scalar<scalar_type>(va, src + va.stride * 3),
+ load_attrib_scalar<scalar_type>(va, src + va.stride * 2)};
+ break;
+ }
+ }
+}
+
+template <typename T>
+void load_flat_attrib(T& attrib, VertexAttrib& va, uint32_t start, int instance,
+ int count) {
+ typedef decltype(force_scalar(attrib)) scalar_type;
+ // If no buffer is available, just use a zero default.
+ if (!va.buf_size) {
+ attrib = T{0};
+ return;
+ }
+ char* src = nullptr;
+ if (va.divisor != 0) {
+ src = (char*)va.buf + va.stride * instance + va.offset;
+ } else {
+ if (!count) return;
+ src = (char*)va.buf + va.stride * start + va.offset;
+ }
+ assert(src + va.size <= va.buf + va.buf_size);
+ attrib = T(load_attrib_scalar<scalar_type>(va, src));
+}
+
+void setup_program(GLuint program) {
+ if (!program) {
+ vertex_shader = nullptr;
+ fragment_shader = nullptr;
+ return;
+ }
+ Program& p = ctx->programs[program];
+ assert(p.impl);
+ assert(p.vert_impl);
+ assert(p.frag_impl);
+ vertex_shader = p.vert_impl;
+ fragment_shader = p.frag_impl;
+}
+
+extern ProgramLoader load_shader(const char* name);
+
+extern "C" {
+
+void UseProgram(GLuint program) {
+ if (ctx->current_program && program != ctx->current_program) {
+ auto* p = ctx->programs.find(ctx->current_program);
+ if (p && p->deleted) {
+ ctx->programs.erase(ctx->current_program);
+ }
+ }
+ ctx->current_program = program;
+ setup_program(program);
+}
+
+void SetViewport(GLint x, GLint y, GLsizei width, GLsizei height) {
+ ctx->viewport = IntRect{x, y, x + width, y + height};
+}
+
+void Enable(GLenum cap) {
+ switch (cap) {
+ case GL_BLEND:
+ ctx->blend = true;
+ break;
+ case GL_DEPTH_TEST:
+ ctx->depthtest = true;
+ break;
+ case GL_SCISSOR_TEST:
+ ctx->scissortest = true;
+ break;
+ }
+}
+
+void Disable(GLenum cap) {
+ switch (cap) {
+ case GL_BLEND:
+ ctx->blend = false;
+ break;
+ case GL_DEPTH_TEST:
+ ctx->depthtest = false;
+ break;
+ case GL_SCISSOR_TEST:
+ ctx->scissortest = false;
+ break;
+ }
+}
+
+// Report the last error generated and clear the error status.
+GLenum GetError() {
+ GLenum error = ctx->last_error;
+ ctx->last_error = GL_NO_ERROR;
+ return error;
+}
+
+// Sets the error status to out-of-memory to indicate that a buffer
+// or texture re-allocation failed.
+static void out_of_memory() { ctx->last_error = GL_OUT_OF_MEMORY; }
+
+static const char* const extensions[] = {
+ "GL_ARB_blend_func_extended",
+ "GL_ARB_clear_texture",
+ "GL_ARB_copy_image",
+ "GL_ARB_draw_instanced",
+ "GL_ARB_explicit_attrib_location",
+ "GL_ARB_instanced_arrays",
+ "GL_ARB_invalidate_subdata",
+ "GL_ARB_texture_storage",
+ "GL_EXT_timer_query",
+ "GL_KHR_blend_equation_advanced",
+ "GL_KHR_blend_equation_advanced_coherent",
+ "GL_APPLE_rgb_422",
+};
+
+void GetIntegerv(GLenum pname, GLint* params) {
+ assert(params);
+ switch (pname) {
+ case GL_MAX_TEXTURE_UNITS:
+ case GL_MAX_TEXTURE_IMAGE_UNITS:
+ params[0] = MAX_TEXTURE_UNITS;
+ break;
+ case GL_MAX_TEXTURE_SIZE:
+ params[0] = 1 << 15;
+ break;
+ case GL_MAX_ARRAY_TEXTURE_LAYERS:
+ params[0] = 0;
+ break;
+ case GL_READ_FRAMEBUFFER_BINDING:
+ params[0] = ctx->read_framebuffer_binding;
+ break;
+ case GL_DRAW_FRAMEBUFFER_BINDING:
+ params[0] = ctx->draw_framebuffer_binding;
+ break;
+ case GL_PIXEL_PACK_BUFFER_BINDING:
+ params[0] = ctx->pixel_pack_buffer_binding;
+ break;
+ case GL_PIXEL_UNPACK_BUFFER_BINDING:
+ params[0] = ctx->pixel_unpack_buffer_binding;
+ break;
+ case GL_NUM_EXTENSIONS:
+ params[0] = sizeof(extensions) / sizeof(extensions[0]);
+ break;
+ case GL_MAJOR_VERSION:
+ params[0] = 3;
+ break;
+ case GL_MINOR_VERSION:
+ params[0] = 2;
+ break;
+ case GL_MIN_PROGRAM_TEXEL_OFFSET:
+ params[0] = 0;
+ break;
+ case GL_MAX_PROGRAM_TEXEL_OFFSET:
+ params[0] = MAX_TEXEL_OFFSET;
+ break;
+ default:
+ debugf("unhandled glGetIntegerv parameter %x\n", pname);
+ assert(false);
+ }
+}
+
+void GetBooleanv(GLenum pname, GLboolean* params) {
+ assert(params);
+ switch (pname) {
+ case GL_DEPTH_WRITEMASK:
+ params[0] = ctx->depthmask;
+ break;
+ default:
+ debugf("unhandled glGetBooleanv parameter %x\n", pname);
+ assert(false);
+ }
+}
+
+const char* GetString(GLenum name) {
+ switch (name) {
+ case GL_VENDOR:
+ return "Mozilla Gfx";
+ case GL_RENDERER:
+ return "Software WebRender";
+ case GL_VERSION:
+ return "3.2";
+ case GL_SHADING_LANGUAGE_VERSION:
+ return "1.50";
+ default:
+ debugf("unhandled glGetString parameter %x\n", name);
+ assert(false);
+ return nullptr;
+ }
+}
+
+const char* GetStringi(GLenum name, GLuint index) {
+ switch (name) {
+ case GL_EXTENSIONS:
+ if (index >= sizeof(extensions) / sizeof(extensions[0])) {
+ return nullptr;
+ }
+ return extensions[index];
+ default:
+ debugf("unhandled glGetStringi parameter %x\n", name);
+ assert(false);
+ return nullptr;
+ }
+}
+
+GLenum remap_blendfunc(GLenum rgb, GLenum a) {
+ switch (a) {
+ case GL_SRC_ALPHA:
+ if (rgb == GL_SRC_COLOR) a = GL_SRC_COLOR;
+ break;
+ case GL_ONE_MINUS_SRC_ALPHA:
+ if (rgb == GL_ONE_MINUS_SRC_COLOR) a = GL_ONE_MINUS_SRC_COLOR;
+ break;
+ case GL_DST_ALPHA:
+ if (rgb == GL_DST_COLOR) a = GL_DST_COLOR;
+ break;
+ case GL_ONE_MINUS_DST_ALPHA:
+ if (rgb == GL_ONE_MINUS_DST_COLOR) a = GL_ONE_MINUS_DST_COLOR;
+ break;
+ case GL_CONSTANT_ALPHA:
+ if (rgb == GL_CONSTANT_COLOR) a = GL_CONSTANT_COLOR;
+ break;
+ case GL_ONE_MINUS_CONSTANT_ALPHA:
+ if (rgb == GL_ONE_MINUS_CONSTANT_COLOR) a = GL_ONE_MINUS_CONSTANT_COLOR;
+ break;
+ case GL_SRC_COLOR:
+ if (rgb == GL_SRC_ALPHA) a = GL_SRC_ALPHA;
+ break;
+ case GL_ONE_MINUS_SRC_COLOR:
+ if (rgb == GL_ONE_MINUS_SRC_ALPHA) a = GL_ONE_MINUS_SRC_ALPHA;
+ break;
+ case GL_DST_COLOR:
+ if (rgb == GL_DST_ALPHA) a = GL_DST_ALPHA;
+ break;
+ case GL_ONE_MINUS_DST_COLOR:
+ if (rgb == GL_ONE_MINUS_DST_ALPHA) a = GL_ONE_MINUS_DST_ALPHA;
+ break;
+ case GL_CONSTANT_COLOR:
+ if (rgb == GL_CONSTANT_ALPHA) a = GL_CONSTANT_ALPHA;
+ break;
+ case GL_ONE_MINUS_CONSTANT_COLOR:
+ if (rgb == GL_ONE_MINUS_CONSTANT_ALPHA) a = GL_ONE_MINUS_CONSTANT_ALPHA;
+ break;
+ case GL_SRC1_ALPHA:
+ if (rgb == GL_SRC1_COLOR) a = GL_SRC1_COLOR;
+ break;
+ case GL_ONE_MINUS_SRC1_ALPHA:
+ if (rgb == GL_ONE_MINUS_SRC1_COLOR) a = GL_ONE_MINUS_SRC1_COLOR;
+ break;
+ case GL_SRC1_COLOR:
+ if (rgb == GL_SRC1_ALPHA) a = GL_SRC1_ALPHA;
+ break;
+ case GL_ONE_MINUS_SRC1_COLOR:
+ if (rgb == GL_ONE_MINUS_SRC1_ALPHA) a = GL_ONE_MINUS_SRC1_ALPHA;
+ break;
+ }
+ return a;
+}
+
+// Generate a hashed blend key based on blend func and equation state. This
+// allows all the blend state to be processed down to a blend key that can be
+// dealt with inside a single switch statement.
+static void hash_blend_key() {
+ GLenum srgb = ctx->blendfunc_srgb;
+ GLenum drgb = ctx->blendfunc_drgb;
+ GLenum sa = ctx->blendfunc_sa;
+ GLenum da = ctx->blendfunc_da;
+ GLenum equation = ctx->blend_equation;
+#define HASH_BLEND_KEY(x, y, z, w) ((x << 4) | (y) | (z << 24) | (w << 20))
+ // Basic non-separate blend funcs used the two argument form
+ int hash = HASH_BLEND_KEY(srgb, drgb, 0, 0);
+ // Separate alpha blend funcs use the 4 argument hash
+ if (srgb != sa || drgb != da) hash |= HASH_BLEND_KEY(0, 0, sa, da);
+ // Any other blend equation than the default func_add ignores the func and
+ // instead generates a one-argument hash based on the equation
+ if (equation != GL_FUNC_ADD) hash = HASH_BLEND_KEY(equation, 0, 0, 0);
+ switch (hash) {
+#define MAP_BLEND_KEY(...) \
+ case HASH_BLEND_KEY(__VA_ARGS__): \
+ ctx->blend_key = BLEND_KEY(__VA_ARGS__); \
+ break;
+ FOR_EACH_BLEND_KEY(MAP_BLEND_KEY)
+ default:
+ debugf("blendfunc: %x, %x, separate: %x, %x, equation: %x\n", srgb, drgb,
+ sa, da, equation);
+ assert(false);
+ break;
+ }
+}
+
+void BlendFunc(GLenum srgb, GLenum drgb, GLenum sa, GLenum da) {
+ ctx->blendfunc_srgb = srgb;
+ ctx->blendfunc_drgb = drgb;
+ sa = remap_blendfunc(srgb, sa);
+ da = remap_blendfunc(drgb, da);
+ ctx->blendfunc_sa = sa;
+ ctx->blendfunc_da = da;
+
+ hash_blend_key();
+}
+
+void BlendColor(GLfloat r, GLfloat g, GLfloat b, GLfloat a) {
+ I32 c = round_pixel((Float){b, g, r, a});
+ ctx->blendcolor = CONVERT(c, U16).xyzwxyzw;
+}
+
+void BlendEquation(GLenum mode) {
+ assert(mode == GL_FUNC_ADD || mode == GL_MIN || mode == GL_MAX ||
+ (mode >= GL_MULTIPLY_KHR && mode <= GL_HSL_LUMINOSITY_KHR));
+ if (mode != ctx->blend_equation) {
+ ctx->blend_equation = mode;
+ hash_blend_key();
+ }
+}
+
+void DepthMask(GLboolean flag) { ctx->depthmask = flag; }
+
+void DepthFunc(GLenum func) {
+ switch (func) {
+ case GL_LESS:
+ case GL_LEQUAL:
+ break;
+ default:
+ assert(false);
+ }
+ ctx->depthfunc = func;
+}
+
+void SetScissor(GLint x, GLint y, GLsizei width, GLsizei height) {
+ ctx->scissor = IntRect{x, y, x + width, y + height};
+}
+
+void ClearColor(GLfloat r, GLfloat g, GLfloat b, GLfloat a) {
+ ctx->clearcolor[0] = r;
+ ctx->clearcolor[1] = g;
+ ctx->clearcolor[2] = b;
+ ctx->clearcolor[3] = a;
+}
+
+void ClearDepth(GLdouble depth) { ctx->cleardepth = depth; }
+
+void ActiveTexture(GLenum texture) {
+ assert(texture >= GL_TEXTURE0);
+ assert(texture < GL_TEXTURE0 + MAX_TEXTURE_UNITS);
+ ctx->active_texture_unit =
+ clamp(int(texture - GL_TEXTURE0), 0, int(MAX_TEXTURE_UNITS - 1));
+}
+
+void GenQueries(GLsizei n, GLuint* result) {
+ for (int i = 0; i < n; i++) {
+ Query q;
+ result[i] = ctx->queries.insert(q);
+ }
+}
+
+void DeleteQuery(GLuint n) {
+ if (n && ctx->queries.erase(n)) {
+ unlink(ctx->time_elapsed_query, n);
+ unlink(ctx->samples_passed_query, n);
+ }
+}
+
+void GenBuffers(int n, GLuint* result) {
+ for (int i = 0; i < n; i++) {
+ Buffer b;
+ result[i] = ctx->buffers.insert(b);
+ }
+}
+
+void DeleteBuffer(GLuint n) {
+ if (n && ctx->buffers.erase(n)) {
+ unlink(ctx->pixel_pack_buffer_binding, n);
+ unlink(ctx->pixel_unpack_buffer_binding, n);
+ unlink(ctx->array_buffer_binding, n);
+ }
+}
+
+void GenVertexArrays(int n, GLuint* result) {
+ for (int i = 0; i < n; i++) {
+ VertexArray v;
+ result[i] = ctx->vertex_arrays.insert(v);
+ }
+}
+
+void DeleteVertexArray(GLuint n) {
+ if (n && ctx->vertex_arrays.erase(n)) {
+ unlink(ctx->current_vertex_array, n);
+ }
+}
+
+GLuint CreateShader(GLenum type) {
+ Shader s;
+ s.type = type;
+ return ctx->shaders.insert(s);
+}
+
+void ShaderSourceByName(GLuint shader, char* name) {
+ Shader& s = ctx->shaders[shader];
+ s.loader = load_shader(name);
+ if (!s.loader) {
+ debugf("unknown shader %s\n", name);
+ }
+}
+
+void AttachShader(GLuint program, GLuint shader) {
+ Program& p = ctx->programs[program];
+ Shader& s = ctx->shaders[shader];
+ if (s.type == GL_VERTEX_SHADER) {
+ if (!p.impl && s.loader) p.impl = s.loader();
+ } else if (s.type == GL_FRAGMENT_SHADER) {
+ if (!p.impl && s.loader) p.impl = s.loader();
+ } else {
+ assert(0);
+ }
+}
+
+void DeleteShader(GLuint n) {
+ if (n) ctx->shaders.erase(n);
+}
+
+GLuint CreateProgram() {
+ Program p;
+ return ctx->programs.insert(p);
+}
+
+void DeleteProgram(GLuint n) {
+ if (!n) return;
+ if (ctx->current_program == n) {
+ if (auto* p = ctx->programs.find(n)) {
+ p->deleted = true;
+ }
+ } else {
+ ctx->programs.erase(n);
+ }
+}
+
+void LinkProgram(GLuint program) {
+ Program& p = ctx->programs[program];
+ assert(p.impl);
+ if (!p.impl) {
+ return;
+ }
+ assert(p.impl->interpolants_size() <= sizeof(Interpolants));
+ if (!p.vert_impl) p.vert_impl = p.impl->get_vertex_shader();
+ if (!p.frag_impl) p.frag_impl = p.impl->get_fragment_shader();
+}
+
+GLint GetLinkStatus(GLuint program) {
+ if (auto* p = ctx->programs.find(program)) {
+ return p->impl ? 1 : 0;
+ }
+ return 0;
+}
+
+void BindAttribLocation(GLuint program, GLuint index, char* name) {
+ Program& p = ctx->programs[program];
+ assert(p.impl);
+ if (!p.impl) {
+ return;
+ }
+ p.impl->bind_attrib(name, index);
+}
+
+GLint GetAttribLocation(GLuint program, char* name) {
+ Program& p = ctx->programs[program];
+ assert(p.impl);
+ if (!p.impl) {
+ return -1;
+ }
+ return p.impl->get_attrib(name);
+}
+
+GLint GetUniformLocation(GLuint program, char* name) {
+ Program& p = ctx->programs[program];
+ assert(p.impl);
+ if (!p.impl) {
+ return -1;
+ }
+ GLint loc = p.impl->get_uniform(name);
+ // debugf("location: %d\n", loc);
+ return loc;
+}
+
+static uint64_t get_time_value() {
+#ifdef __MACH__
+ return mach_absolute_time();
+#elif defined(_WIN32)
+ LARGE_INTEGER time;
+ static bool have_frequency = false;
+ static LARGE_INTEGER frequency;
+ if (!have_frequency) {
+ QueryPerformanceFrequency(&frequency);
+ have_frequency = true;
+ }
+ QueryPerformanceCounter(&time);
+ return time.QuadPart * 1000000000ULL / frequency.QuadPart;
+#else
+ return ({
+ struct timespec tp;
+ clock_gettime(CLOCK_MONOTONIC, &tp);
+ tp.tv_sec * 1000000000ULL + tp.tv_nsec;
+ });
+#endif
+}
+
+void BeginQuery(GLenum target, GLuint id) {
+ ctx->get_binding(target) = id;
+ Query& q = ctx->queries[id];
+ switch (target) {
+ case GL_SAMPLES_PASSED:
+ q.value = 0;
+ break;
+ case GL_TIME_ELAPSED:
+ q.value = get_time_value();
+ break;
+ default:
+ debugf("unknown query target %x for query %d\n", target, id);
+ assert(false);
+ }
+}
+
+void EndQuery(GLenum target) {
+ Query& q = ctx->queries[ctx->get_binding(target)];
+ switch (target) {
+ case GL_SAMPLES_PASSED:
+ break;
+ case GL_TIME_ELAPSED:
+ q.value = get_time_value() - q.value;
+ break;
+ default:
+ debugf("unknown query target %x\n", target);
+ assert(false);
+ }
+ ctx->get_binding(target) = 0;
+}
+
+void GetQueryObjectui64v(GLuint id, GLenum pname, GLuint64* params) {
+ Query& q = ctx->queries[id];
+ switch (pname) {
+ case GL_QUERY_RESULT:
+ assert(params);
+ params[0] = q.value;
+ break;
+ default:
+ assert(false);
+ }
+}
+
+void BindVertexArray(GLuint vertex_array) {
+ if (vertex_array != ctx->current_vertex_array) {
+ ctx->validate_vertex_array = true;
+ }
+ ctx->current_vertex_array = vertex_array;
+}
+
+void BindTexture(GLenum target, GLuint texture) {
+ ctx->get_binding(target) = texture;
+}
+
+void BindBuffer(GLenum target, GLuint buffer) {
+ ctx->get_binding(target) = buffer;
+}
+
+void BindFramebuffer(GLenum target, GLuint fb) {
+ if (target == GL_FRAMEBUFFER) {
+ ctx->read_framebuffer_binding = fb;
+ ctx->draw_framebuffer_binding = fb;
+ } else {
+ assert(target == GL_READ_FRAMEBUFFER || target == GL_DRAW_FRAMEBUFFER);
+ ctx->get_binding(target) = fb;
+ }
+}
+
+void BindRenderbuffer(GLenum target, GLuint rb) {
+ ctx->get_binding(target) = rb;
+}
+
+void PixelStorei(GLenum name, GLint param) {
+ if (name == GL_UNPACK_ALIGNMENT) {
+ assert(param == 1);
+ } else if (name == GL_UNPACK_ROW_LENGTH) {
+ ctx->unpack_row_length = param;
+ }
+}
+
+static GLenum remap_internal_format(GLenum format) {
+ switch (format) {
+ case GL_DEPTH_COMPONENT:
+ return GL_DEPTH_COMPONENT24;
+ case GL_RGBA:
+ return GL_RGBA8;
+ case GL_RED:
+ return GL_R8;
+ case GL_RG:
+ return GL_RG8;
+ case GL_RGB_422_APPLE:
+ return GL_RGB_RAW_422_APPLE;
+ default:
+ return format;
+ }
+}
+
+} // extern "C"
+
+static bool format_requires_conversion(GLenum external_format,
+ GLenum internal_format) {
+ switch (external_format) {
+ case GL_RGBA:
+ return internal_format == GL_RGBA8;
+ default:
+ return false;
+ }
+}
+
+static inline void copy_bgra8_to_rgba8(uint32_t* dest, const uint32_t* src,
+ int width) {
+ for (; width >= 4; width -= 4, dest += 4, src += 4) {
+ U32 p = unaligned_load<U32>(src);
+ U32 rb = p & 0x00FF00FF;
+ unaligned_store(dest, (p & 0xFF00FF00) | (rb << 16) | (rb >> 16));
+ }
+ for (; width > 0; width--, dest++, src++) {
+ uint32_t p = *src;
+ uint32_t rb = p & 0x00FF00FF;
+ *dest = (p & 0xFF00FF00) | (rb << 16) | (rb >> 16);
+ }
+}
+
+static void convert_copy(GLenum external_format, GLenum internal_format,
+ uint8_t* dst_buf, size_t dst_stride,
+ const uint8_t* src_buf, size_t src_stride,
+ size_t width, size_t height) {
+ switch (external_format) {
+ case GL_RGBA:
+ if (internal_format == GL_RGBA8) {
+ for (; height; height--) {
+ copy_bgra8_to_rgba8((uint32_t*)dst_buf, (const uint32_t*)src_buf,
+ width);
+ dst_buf += dst_stride;
+ src_buf += src_stride;
+ }
+ return;
+ }
+ break;
+ default:
+ break;
+ }
+ size_t row_bytes = width * bytes_for_internal_format(internal_format);
+ for (; height; height--) {
+ memcpy(dst_buf, src_buf, row_bytes);
+ dst_buf += dst_stride;
+ src_buf += src_stride;
+ }
+}
+
+static void set_tex_storage(Texture& t, GLenum external_format, GLsizei width,
+ GLsizei height, void* buf = nullptr,
+ GLsizei stride = 0, GLsizei min_width = 0,
+ GLsizei min_height = 0) {
+ GLenum internal_format = remap_internal_format(external_format);
+ bool changed = false;
+ if (t.width != width || t.height != height ||
+ t.internal_format != internal_format) {
+ changed = true;
+ t.internal_format = internal_format;
+ t.width = width;
+ t.height = height;
+ }
+ // If we are changed from an internally managed buffer to an externally
+ // supplied one or vice versa, ensure that we clean up old buffer state.
+ // However, if we have to convert the data from a non-native format, then
+ // always treat it as internally managed since we will need to copy to an
+ // internally managed native format buffer.
+ bool should_free = buf == nullptr || format_requires_conversion(
+ external_format, internal_format);
+ if (t.should_free() != should_free) {
+ changed = true;
+ t.cleanup();
+ t.set_should_free(should_free);
+ }
+ // If now an external buffer, explicitly set it...
+ if (!should_free) {
+ t.set_buffer(buf, stride);
+ }
+ t.disable_delayed_clear();
+ if (!t.allocate(changed, min_width, min_height)) {
+ out_of_memory();
+ }
+ // If we have a buffer that needs format conversion, then do that now.
+ if (buf && should_free) {
+ convert_copy(external_format, internal_format, (uint8_t*)t.buf, t.stride(),
+ (const uint8_t*)buf, stride, width, height);
+ }
+}
+
+extern "C" {
+
+void TexStorage2D(GLenum target, GLint levels, GLenum internal_format,
+ GLsizei width, GLsizei height) {
+ assert(levels == 1);
+ Texture& t = ctx->textures[ctx->get_binding(target)];
+ set_tex_storage(t, internal_format, width, height);
+}
+
+GLenum internal_format_for_data(GLenum format, GLenum ty) {
+ if (format == GL_RED && ty == GL_UNSIGNED_BYTE) {
+ return GL_R8;
+ } else if ((format == GL_RGBA || format == GL_BGRA) &&
+ (ty == GL_UNSIGNED_BYTE || ty == GL_UNSIGNED_INT_8_8_8_8_REV)) {
+ return GL_RGBA8;
+ } else if (format == GL_RGBA && ty == GL_FLOAT) {
+ return GL_RGBA32F;
+ } else if (format == GL_RGBA_INTEGER && ty == GL_INT) {
+ return GL_RGBA32I;
+ } else if (format == GL_RG && ty == GL_UNSIGNED_BYTE) {
+ return GL_RG8;
+ } else if (format == GL_RGB_422_APPLE &&
+ ty == GL_UNSIGNED_SHORT_8_8_REV_APPLE) {
+ return GL_RGB_RAW_422_APPLE;
+ } else if (format == GL_RED && ty == GL_UNSIGNED_SHORT) {
+ return GL_R16;
+ } else if (format == GL_RG && ty == GL_UNSIGNED_SHORT) {
+ return GL_RG16;
+ } else {
+ debugf("unknown internal format for format %x, type %x\n", format, ty);
+ assert(false);
+ return 0;
+ }
+}
+
+static Buffer* get_pixel_pack_buffer() {
+ return ctx->pixel_pack_buffer_binding
+ ? &ctx->buffers[ctx->pixel_pack_buffer_binding]
+ : nullptr;
+}
+
+static void* get_pixel_pack_buffer_data(void* data) {
+ if (Buffer* b = get_pixel_pack_buffer()) {
+ return b->buf ? b->buf + (size_t)data : nullptr;
+ }
+ return data;
+}
+
+static Buffer* get_pixel_unpack_buffer() {
+ return ctx->pixel_unpack_buffer_binding
+ ? &ctx->buffers[ctx->pixel_unpack_buffer_binding]
+ : nullptr;
+}
+
+static void* get_pixel_unpack_buffer_data(void* data) {
+ if (Buffer* b = get_pixel_unpack_buffer()) {
+ return b->buf ? b->buf + (size_t)data : nullptr;
+ }
+ return data;
+}
+
+void TexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
+ GLsizei width, GLsizei height, GLenum format, GLenum ty,
+ void* data) {
+ if (level != 0) {
+ assert(false);
+ return;
+ }
+ data = get_pixel_unpack_buffer_data(data);
+ if (!data) return;
+ Texture& t = ctx->textures[ctx->get_binding(target)];
+ IntRect skip = {xoffset, yoffset, xoffset + width, yoffset + height};
+ prepare_texture(t, &skip);
+ assert(xoffset + width <= t.width);
+ assert(yoffset + height <= t.height);
+ assert(ctx->unpack_row_length == 0 || ctx->unpack_row_length >= width);
+ GLsizei row_length =
+ ctx->unpack_row_length != 0 ? ctx->unpack_row_length : width;
+ assert(t.internal_format == internal_format_for_data(format, ty));
+ int src_bpp = format_requires_conversion(format, t.internal_format)
+ ? bytes_for_internal_format(format)
+ : t.bpp();
+ if (!src_bpp || !t.buf) return;
+ convert_copy(format, t.internal_format,
+ (uint8_t*)t.sample_ptr(xoffset, yoffset), t.stride(),
+ (const uint8_t*)data, row_length * src_bpp, width, height);
+}
+
+void TexImage2D(GLenum target, GLint level, GLint internal_format,
+ GLsizei width, GLsizei height, GLint border, GLenum format,
+ GLenum ty, void* data) {
+ if (level != 0) {
+ assert(false);
+ return;
+ }
+ assert(border == 0);
+ TexStorage2D(target, 1, internal_format, width, height);
+ TexSubImage2D(target, 0, 0, 0, width, height, format, ty, data);
+}
+
+void GenerateMipmap(UNUSED GLenum target) {
+ // TODO: support mipmaps
+}
+
+void SetTextureParameter(GLuint texid, GLenum pname, GLint param) {
+ Texture& t = ctx->textures[texid];
+ switch (pname) {
+ case GL_TEXTURE_WRAP_S:
+ assert(param == GL_CLAMP_TO_EDGE);
+ break;
+ case GL_TEXTURE_WRAP_T:
+ assert(param == GL_CLAMP_TO_EDGE);
+ break;
+ case GL_TEXTURE_MIN_FILTER:
+ t.min_filter = param;
+ break;
+ case GL_TEXTURE_MAG_FILTER:
+ t.mag_filter = param;
+ break;
+ default:
+ break;
+ }
+}
+
+void TexParameteri(GLenum target, GLenum pname, GLint param) {
+ SetTextureParameter(ctx->get_binding(target), pname, param);
+}
+
+void GenTextures(int n, GLuint* result) {
+ for (int i = 0; i < n; i++) {
+ Texture t;
+ result[i] = ctx->textures.insert(t);
+ }
+}
+
+void DeleteTexture(GLuint n) {
+ if (n && ctx->textures.erase(n)) {
+ for (size_t i = 0; i < MAX_TEXTURE_UNITS; i++) {
+ ctx->texture_units[i].unlink(n);
+ }
+ }
+}
+
+void GenRenderbuffers(int n, GLuint* result) {
+ for (int i = 0; i < n; i++) {
+ Renderbuffer r;
+ result[i] = ctx->renderbuffers.insert(r);
+ }
+}
+
+void Renderbuffer::on_erase() {
+ for (auto* fb : ctx->framebuffers) {
+ if (fb) {
+ unlink(fb->color_attachment, texture);
+ unlink(fb->depth_attachment, texture);
+ }
+ }
+ DeleteTexture(texture);
+}
+
+void DeleteRenderbuffer(GLuint n) {
+ if (n && ctx->renderbuffers.erase(n)) {
+ unlink(ctx->renderbuffer_binding, n);
+ }
+}
+
+void GenFramebuffers(int n, GLuint* result) {
+ for (int i = 0; i < n; i++) {
+ Framebuffer f;
+ result[i] = ctx->framebuffers.insert(f);
+ }
+}
+
+void DeleteFramebuffer(GLuint n) {
+ if (n && ctx->framebuffers.erase(n)) {
+ unlink(ctx->read_framebuffer_binding, n);
+ unlink(ctx->draw_framebuffer_binding, n);
+ }
+}
+
+void RenderbufferStorage(GLenum target, GLenum internal_format, GLsizei width,
+ GLsizei height) {
+ // Just refer a renderbuffer to a texture to simplify things for now...
+ Renderbuffer& r = ctx->renderbuffers[ctx->get_binding(target)];
+ if (!r.texture) {
+ GenTextures(1, &r.texture);
+ }
+ switch (internal_format) {
+ case GL_DEPTH_COMPONENT:
+ case GL_DEPTH_COMPONENT16:
+ case GL_DEPTH_COMPONENT24:
+ case GL_DEPTH_COMPONENT32:
+ // Force depth format to 24 bits...
+ internal_format = GL_DEPTH_COMPONENT24;
+ break;
+ }
+ set_tex_storage(ctx->textures[r.texture], internal_format, width, height);
+}
+
+void VertexAttribPointer(GLuint index, GLint size, GLenum type, bool normalized,
+ GLsizei stride, GLuint offset) {
+ // debugf("cva: %d\n", ctx->current_vertex_array);
+ VertexArray& v = ctx->vertex_arrays[ctx->current_vertex_array];
+ if (index >= NULL_ATTRIB) {
+ assert(0);
+ return;
+ }
+ VertexAttrib& va = v.attribs[index];
+ va.size = size * bytes_per_type(type);
+ va.type = type;
+ va.normalized = normalized;
+ va.stride = stride;
+ va.offset = offset;
+ // Buffer &vertex_buf = ctx->buffers[ctx->array_buffer_binding];
+ va.vertex_buffer = ctx->array_buffer_binding;
+ va.vertex_array = ctx->current_vertex_array;
+ ctx->validate_vertex_array = true;
+}
+
+void VertexAttribIPointer(GLuint index, GLint size, GLenum type, GLsizei stride,
+ GLuint offset) {
+ // debugf("cva: %d\n", ctx->current_vertex_array);
+ VertexArray& v = ctx->vertex_arrays[ctx->current_vertex_array];
+ if (index >= NULL_ATTRIB) {
+ assert(0);
+ return;
+ }
+ VertexAttrib& va = v.attribs[index];
+ va.size = size * bytes_per_type(type);
+ va.type = type;
+ va.normalized = false;
+ va.stride = stride;
+ va.offset = offset;
+ // Buffer &vertex_buf = ctx->buffers[ctx->array_buffer_binding];
+ va.vertex_buffer = ctx->array_buffer_binding;
+ va.vertex_array = ctx->current_vertex_array;
+ ctx->validate_vertex_array = true;
+}
+
+void EnableVertexAttribArray(GLuint index) {
+ VertexArray& v = ctx->vertex_arrays[ctx->current_vertex_array];
+ if (index >= NULL_ATTRIB) {
+ assert(0);
+ return;
+ }
+ VertexAttrib& va = v.attribs[index];
+ if (!va.enabled) {
+ ctx->validate_vertex_array = true;
+ }
+ va.enabled = true;
+ v.max_attrib = max(v.max_attrib, (int)index);
+}
+
+void DisableVertexAttribArray(GLuint index) {
+ VertexArray& v = ctx->vertex_arrays[ctx->current_vertex_array];
+ if (index >= NULL_ATTRIB) {
+ assert(0);
+ return;
+ }
+ VertexAttrib& va = v.attribs[index];
+ if (va.enabled) {
+ ctx->validate_vertex_array = true;
+ }
+ va.disable();
+}
+
+void VertexAttribDivisor(GLuint index, GLuint divisor) {
+ VertexArray& v = ctx->vertex_arrays[ctx->current_vertex_array];
+ // Only support divisor being 0 (per-vertex) or 1 (per-instance).
+ if (index >= NULL_ATTRIB || divisor > 1) {
+ assert(0);
+ return;
+ }
+ VertexAttrib& va = v.attribs[index];
+ va.divisor = divisor;
+}
+
+void BufferData(GLenum target, GLsizeiptr size, void* data,
+ UNUSED GLenum usage) {
+ Buffer& b = ctx->buffers[ctx->get_binding(target)];
+ if (size != b.size) {
+ if (!b.allocate(size)) {
+ out_of_memory();
+ }
+ ctx->validate_vertex_array = true;
+ }
+ if (data && b.buf && size <= b.size) {
+ memcpy(b.buf, data, size);
+ }
+}
+
+void BufferSubData(GLenum target, GLintptr offset, GLsizeiptr size,
+ void* data) {
+ Buffer& b = ctx->buffers[ctx->get_binding(target)];
+ assert(offset + size <= b.size);
+ if (data && b.buf && offset + size <= b.size) {
+ memcpy(&b.buf[offset], data, size);
+ }
+}
+
+void* MapBuffer(GLenum target, UNUSED GLbitfield access) {
+ Buffer& b = ctx->buffers[ctx->get_binding(target)];
+ return b.buf;
+}
+
+void* MapBufferRange(GLenum target, GLintptr offset, GLsizeiptr length,
+ UNUSED GLbitfield access) {
+ Buffer& b = ctx->buffers[ctx->get_binding(target)];
+ if (b.buf && offset >= 0 && length > 0 && offset + length <= b.size) {
+ return b.buf + offset;
+ }
+ return nullptr;
+}
+
+GLboolean UnmapBuffer(GLenum target) {
+ Buffer& b = ctx->buffers[ctx->get_binding(target)];
+ return b.buf != nullptr;
+}
+
+void Uniform1i(GLint location, GLint V0) {
+ // debugf("tex: %d\n", (int)ctx->textures.size);
+ if (vertex_shader) {
+ vertex_shader->set_uniform_1i(location, V0);
+ }
+}
+void Uniform4fv(GLint location, GLsizei count, const GLfloat* v) {
+ assert(count == 1);
+ if (vertex_shader) {
+ vertex_shader->set_uniform_4fv(location, v);
+ }
+}
+void UniformMatrix4fv(GLint location, GLsizei count, GLboolean transpose,
+ const GLfloat* value) {
+ assert(count == 1);
+ assert(!transpose);
+ if (vertex_shader) {
+ vertex_shader->set_uniform_matrix4fv(location, value);
+ }
+}
+
+void FramebufferTexture2D(GLenum target, GLenum attachment, GLenum textarget,
+ GLuint texture, GLint level) {
+ assert(target == GL_READ_FRAMEBUFFER || target == GL_DRAW_FRAMEBUFFER);
+ assert(textarget == GL_TEXTURE_2D || textarget == GL_TEXTURE_RECTANGLE);
+ assert(level == 0);
+ Framebuffer& fb = ctx->framebuffers[ctx->get_binding(target)];
+ if (attachment == GL_COLOR_ATTACHMENT0) {
+ fb.color_attachment = texture;
+ } else if (attachment == GL_DEPTH_ATTACHMENT) {
+ fb.depth_attachment = texture;
+ } else {
+ assert(0);
+ }
+}
+
+void FramebufferRenderbuffer(GLenum target, GLenum attachment,
+ GLenum renderbuffertarget, GLuint renderbuffer) {
+ assert(target == GL_READ_FRAMEBUFFER || target == GL_DRAW_FRAMEBUFFER);
+ assert(renderbuffertarget == GL_RENDERBUFFER);
+ Framebuffer& fb = ctx->framebuffers[ctx->get_binding(target)];
+ Renderbuffer& rb = ctx->renderbuffers[renderbuffer];
+ if (attachment == GL_COLOR_ATTACHMENT0) {
+ fb.color_attachment = rb.texture;
+ } else if (attachment == GL_DEPTH_ATTACHMENT) {
+ fb.depth_attachment = rb.texture;
+ } else {
+ assert(0);
+ }
+}
+
+} // extern "C"
+
+static inline Framebuffer* get_framebuffer(GLenum target,
+ bool fallback = false) {
+ if (target == GL_FRAMEBUFFER) {
+ target = GL_DRAW_FRAMEBUFFER;
+ }
+ Framebuffer* fb = ctx->framebuffers.find(ctx->get_binding(target));
+ if (fallback && !fb) {
+ // If the specified framebuffer isn't found and a fallback is requested,
+ // use the default framebuffer.
+ fb = &ctx->framebuffers[0];
+ }
+ return fb;
+}
+
+template <typename T>
+static inline void fill_n(T* dst, size_t n, T val) {
+ for (T* end = &dst[n]; dst < end; dst++) *dst = val;
+}
+
+#if USE_SSE2
+template <>
+inline void fill_n<uint32_t>(uint32_t* dst, size_t n, uint32_t val) {
+ __asm__ __volatile__("rep stosl\n"
+ : "+D"(dst), "+c"(n)
+ : "a"(val)
+ : "memory", "cc");
+}
+#endif
+
+static inline uint32_t clear_chunk(uint8_t value) {
+ return uint32_t(value) * 0x01010101U;
+}
+
+static inline uint32_t clear_chunk(uint16_t value) {
+ return uint32_t(value) | (uint32_t(value) << 16);
+}
+
+static inline uint32_t clear_chunk(uint32_t value) { return value; }
+
+template <typename T>
+static inline void clear_row(T* buf, size_t len, T value, uint32_t chunk) {
+ const size_t N = sizeof(uint32_t) / sizeof(T);
+ // fill any leading unaligned values
+ if (N > 1) {
+ size_t align = (-(intptr_t)buf & (sizeof(uint32_t) - 1)) / sizeof(T);
+ if (align <= len) {
+ fill_n(buf, align, value);
+ len -= align;
+ buf += align;
+ }
+ }
+ // fill as many aligned chunks as possible
+ fill_n((uint32_t*)buf, len / N, chunk);
+ // fill any remaining values
+ if (N > 1) {
+ fill_n(buf + (len & ~(N - 1)), len & (N - 1), value);
+ }
+}
+
+template <typename T>
+static void clear_buffer(Texture& t, T value, IntRect bb, int skip_start = 0,
+ int skip_end = 0) {
+ if (!t.buf) return;
+ skip_start = max(skip_start, bb.x0);
+ skip_end = max(skip_end, skip_start);
+ assert(sizeof(T) == t.bpp());
+ size_t stride = t.stride();
+ // When clearing multiple full-width rows, collapse them into a single large
+ // "row" to avoid redundant setup from clearing each row individually. Note
+ // that we can only safely do this if the stride is tightly packed.
+ if (bb.width() == t.width && bb.height() > 1 && skip_start >= skip_end &&
+ (t.should_free() || stride == t.width * sizeof(T))) {
+ bb.x1 += (stride / sizeof(T)) * (bb.height() - 1);
+ bb.y1 = bb.y0 + 1;
+ }
+ T* buf = (T*)t.sample_ptr(bb.x0, bb.y0);
+ uint32_t chunk = clear_chunk(value);
+ for (int rows = bb.height(); rows > 0; rows--) {
+ if (bb.x0 < skip_start) {
+ clear_row(buf, skip_start - bb.x0, value, chunk);
+ }
+ if (skip_end < bb.x1) {
+ clear_row(buf + (skip_end - bb.x0), bb.x1 - skip_end, value, chunk);
+ }
+ buf += stride / sizeof(T);
+ }
+}
+
+template <typename T>
+static inline void force_clear_row(Texture& t, int y, int skip_start = 0,
+ int skip_end = 0) {
+ assert(t.buf != nullptr);
+ assert(sizeof(T) == t.bpp());
+ assert(skip_start <= skip_end);
+ T* buf = (T*)t.sample_ptr(0, y);
+ uint32_t chunk = clear_chunk((T)t.clear_val);
+ if (skip_start > 0) {
+ clear_row<T>(buf, skip_start, t.clear_val, chunk);
+ }
+ if (skip_end < t.width) {
+ clear_row<T>(buf + skip_end, t.width - skip_end, t.clear_val, chunk);
+ }
+}
+
+template <typename T>
+static void force_clear(Texture& t, const IntRect* skip = nullptr) {
+ if (!t.delay_clear || !t.cleared_rows) {
+ return;
+ }
+ int y0 = 0;
+ int y1 = t.height;
+ int skip_start = 0;
+ int skip_end = 0;
+ if (skip) {
+ y0 = clamp(skip->y0, 0, t.height);
+ y1 = clamp(skip->y1, y0, t.height);
+ skip_start = clamp(skip->x0, 0, t.width);
+ skip_end = clamp(skip->x1, skip_start, t.width);
+ if (skip_start <= 0 && skip_end >= t.width && y0 <= 0 && y1 >= t.height) {
+ t.disable_delayed_clear();
+ return;
+ }
+ }
+ int num_masks = (y1 + 31) / 32;
+ uint32_t* rows = t.cleared_rows;
+ for (int i = y0 / 32; i < num_masks; i++) {
+ uint32_t mask = rows[i];
+ if (mask != ~0U) {
+ rows[i] = ~0U;
+ int start = i * 32;
+ while (mask) {
+ int count = __builtin_ctz(mask);
+ if (count > 0) {
+ clear_buffer<T>(t, t.clear_val,
+ IntRect{0, start, t.width, start + count}, skip_start,
+ skip_end);
+ t.delay_clear -= count;
+ start += count;
+ mask >>= count;
+ }
+ count = __builtin_ctz(mask + 1);
+ start += count;
+ mask >>= count;
+ }
+ int count = (i + 1) * 32 - start;
+ if (count > 0) {
+ clear_buffer<T>(t, t.clear_val,
+ IntRect{0, start, t.width, start + count}, skip_start,
+ skip_end);
+ t.delay_clear -= count;
+ }
+ }
+ }
+ if (t.delay_clear <= 0) t.disable_delayed_clear();
+}
+
+static void prepare_texture(Texture& t, const IntRect* skip) {
+ if (t.delay_clear) {
+ switch (t.internal_format) {
+ case GL_RGBA8:
+ force_clear<uint32_t>(t, skip);
+ break;
+ case GL_R8:
+ force_clear<uint8_t>(t, skip);
+ break;
+ case GL_RG8:
+ force_clear<uint16_t>(t, skip);
+ break;
+ default:
+ assert(false);
+ break;
+ }
+ }
+}
+
+// Setup a clear on a texture. This may either force an immediate clear or
+// potentially punt to a delayed clear, if applicable.
+template <typename T>
+static void request_clear(Texture& t, T value, const IntRect& scissor) {
+ // If the clear would require a scissor, force clear anything outside
+ // the scissor, and then immediately clear anything inside the scissor.
+ if (!scissor.contains(t.offset_bounds())) {
+ IntRect skip = scissor - t.offset;
+ force_clear<T>(t, &skip);
+ clear_buffer<T>(t, value, skip.intersection(t.bounds()));
+ } else {
+ // Do delayed clear for 2D texture without scissor.
+ t.enable_delayed_clear(value);
+ }
+}
+
+template <typename T>
+static inline void request_clear(Texture& t, T value) {
+ // If scissoring is enabled, use the scissor rect. Otherwise, just scissor to
+ // the entire texture bounds.
+ request_clear(t, value, ctx->scissortest ? ctx->scissor : t.offset_bounds());
+}
+
+extern "C" {
+
+void InitDefaultFramebuffer(int x, int y, int width, int height, int stride,
+ void* buf) {
+ Framebuffer& fb = ctx->framebuffers[0];
+ if (!fb.color_attachment) {
+ GenTextures(1, &fb.color_attachment);
+ }
+ // If the dimensions or buffer properties changed, we need to reallocate
+ // the underlying storage for the color buffer texture.
+ Texture& colortex = ctx->textures[fb.color_attachment];
+ set_tex_storage(colortex, GL_RGBA8, width, height, buf, stride);
+ colortex.offset = IntPoint(x, y);
+ if (!fb.depth_attachment) {
+ GenTextures(1, &fb.depth_attachment);
+ }
+ // Ensure dimensions of the depth buffer match the color buffer.
+ Texture& depthtex = ctx->textures[fb.depth_attachment];
+ set_tex_storage(depthtex, GL_DEPTH_COMPONENT24, width, height);
+ depthtex.offset = IntPoint(x, y);
+}
+
+void* GetColorBuffer(GLuint fbo, GLboolean flush, int32_t* width,
+ int32_t* height, int32_t* stride) {
+ Framebuffer* fb = ctx->framebuffers.find(fbo);
+ if (!fb || !fb->color_attachment) {
+ return nullptr;
+ }
+ Texture& colortex = ctx->textures[fb->color_attachment];
+ if (flush) {
+ prepare_texture(colortex);
+ }
+ assert(colortex.offset == IntPoint(0, 0));
+ if (width) {
+ *width = colortex.width;
+ }
+ if (height) {
+ *height = colortex.height;
+ }
+ if (stride) {
+ *stride = colortex.stride();
+ }
+ return colortex.buf ? colortex.sample_ptr(0, 0) : nullptr;
+}
+
+void ResolveFramebuffer(GLuint fbo) {
+ Framebuffer* fb = ctx->framebuffers.find(fbo);
+ if (!fb || !fb->color_attachment) {
+ return;
+ }
+ Texture& colortex = ctx->textures[fb->color_attachment];
+ prepare_texture(colortex);
+}
+
+void SetTextureBuffer(GLuint texid, GLenum internal_format, GLsizei width,
+ GLsizei height, GLsizei stride, void* buf,
+ GLsizei min_width, GLsizei min_height) {
+ Texture& t = ctx->textures[texid];
+ set_tex_storage(t, internal_format, width, height, buf, stride, min_width,
+ min_height);
+}
+
+GLenum CheckFramebufferStatus(GLenum target) {
+ Framebuffer* fb = get_framebuffer(target);
+ if (!fb || !fb->color_attachment) {
+ return GL_FRAMEBUFFER_UNSUPPORTED;
+ }
+ return GL_FRAMEBUFFER_COMPLETE;
+}
+
+void ClearTexSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset,
+ GLint zoffset, GLsizei width, GLsizei height,
+ GLsizei depth, GLenum format, GLenum type,
+ const void* data) {
+ if (level != 0) {
+ assert(false);
+ return;
+ }
+ Texture& t = ctx->textures[texture];
+ assert(!t.locked);
+ if (width <= 0 || height <= 0 || depth <= 0) {
+ return;
+ }
+ assert(zoffset == 0 && depth == 1);
+ IntRect scissor = {xoffset, yoffset, xoffset + width, yoffset + height};
+ if (t.internal_format == GL_DEPTH_COMPONENT24) {
+ uint32_t value = 0xFFFFFF;
+ switch (format) {
+ case GL_DEPTH_COMPONENT:
+ switch (type) {
+ case GL_DOUBLE:
+ value = uint32_t(*(const GLdouble*)data * 0xFFFFFF);
+ break;
+ case GL_FLOAT:
+ value = uint32_t(*(const GLfloat*)data * 0xFFFFFF);
+ break;
+ default:
+ assert(false);
+ break;
+ }
+ break;
+ default:
+ assert(false);
+ break;
+ }
+ if (t.cleared() && !scissor.contains(t.offset_bounds())) {
+ // If we need to scissor the clear and the depth buffer was already
+ // initialized, then just fill runs for that scissor area.
+ t.fill_depth_runs(value, scissor);
+ } else {
+ // Otherwise, the buffer is either uninitialized or the clear would
+ // encompass the entire buffer. If uninitialized, we can safely fill
+ // the entire buffer with any value and thus ignore any scissoring.
+ t.init_depth_runs(value);
+ }
+ return;
+ }
+
+ uint32_t color = 0xFF000000;
+ switch (type) {
+ case GL_FLOAT: {
+ const GLfloat* f = (const GLfloat*)data;
+ Float v = {0.0f, 0.0f, 0.0f, 1.0f};
+ switch (format) {
+ case GL_RGBA:
+ v.w = f[3]; // alpha
+ FALLTHROUGH;
+ case GL_RGB:
+ v.z = f[2]; // blue
+ FALLTHROUGH;
+ case GL_RG:
+ v.y = f[1]; // green
+ FALLTHROUGH;
+ case GL_RED:
+ v.x = f[0]; // red
+ break;
+ default:
+ assert(false);
+ break;
+ }
+ color = bit_cast<uint32_t>(CONVERT(round_pixel(v), U8));
+ break;
+ }
+ case GL_UNSIGNED_BYTE: {
+ const GLubyte* b = (const GLubyte*)data;
+ switch (format) {
+ case GL_RGBA:
+ color = (color & ~0xFF000000) | (uint32_t(b[3]) << 24); // alpha
+ FALLTHROUGH;
+ case GL_RGB:
+ color = (color & ~0x00FF0000) | (uint32_t(b[2]) << 16); // blue
+ FALLTHROUGH;
+ case GL_RG:
+ color = (color & ~0x0000FF00) | (uint32_t(b[1]) << 8); // green
+ FALLTHROUGH;
+ case GL_RED:
+ color = (color & ~0x000000FF) | uint32_t(b[0]); // red
+ break;
+ default:
+ assert(false);
+ break;
+ }
+ break;
+ }
+ default:
+ assert(false);
+ break;
+ }
+
+ switch (t.internal_format) {
+ case GL_RGBA8:
+ // Clear color needs to swizzle to BGRA.
+ request_clear<uint32_t>(t,
+ (color & 0xFF00FF00) |
+ ((color << 16) & 0xFF0000) |
+ ((color >> 16) & 0xFF),
+ scissor);
+ break;
+ case GL_R8:
+ request_clear<uint8_t>(t, uint8_t(color & 0xFF), scissor);
+ break;
+ case GL_RG8:
+ request_clear<uint16_t>(t, uint16_t(color & 0xFFFF), scissor);
+ break;
+ default:
+ assert(false);
+ break;
+ }
+}
+
+void ClearTexImage(GLuint texture, GLint level, GLenum format, GLenum type,
+ const void* data) {
+ Texture& t = ctx->textures[texture];
+ IntRect scissor = t.offset_bounds();
+ ClearTexSubImage(texture, level, scissor.x0, scissor.y0, 0, scissor.width(),
+ scissor.height(), 1, format, type, data);
+}
+
+void Clear(GLbitfield mask) {
+ Framebuffer& fb = *get_framebuffer(GL_DRAW_FRAMEBUFFER, true);
+ if ((mask & GL_COLOR_BUFFER_BIT) && fb.color_attachment) {
+ Texture& t = ctx->textures[fb.color_attachment];
+ IntRect scissor = ctx->scissortest
+ ? ctx->scissor.intersection(t.offset_bounds())
+ : t.offset_bounds();
+ ClearTexSubImage(fb.color_attachment, 0, scissor.x0, scissor.y0, 0,
+ scissor.width(), scissor.height(), 1, GL_RGBA, GL_FLOAT,
+ ctx->clearcolor);
+ }
+ if ((mask & GL_DEPTH_BUFFER_BIT) && fb.depth_attachment) {
+ Texture& t = ctx->textures[fb.depth_attachment];
+ IntRect scissor = ctx->scissortest
+ ? ctx->scissor.intersection(t.offset_bounds())
+ : t.offset_bounds();
+ ClearTexSubImage(fb.depth_attachment, 0, scissor.x0, scissor.y0, 0,
+ scissor.width(), scissor.height(), 1, GL_DEPTH_COMPONENT,
+ GL_DOUBLE, &ctx->cleardepth);
+ }
+}
+
+void ClearColorRect(GLuint fbo, GLint xoffset, GLint yoffset, GLsizei width,
+ GLsizei height, GLfloat r, GLfloat g, GLfloat b,
+ GLfloat a) {
+ GLfloat color[] = {r, g, b, a};
+ Framebuffer& fb = ctx->framebuffers[fbo];
+ Texture& t = ctx->textures[fb.color_attachment];
+ IntRect scissor =
+ IntRect{xoffset, yoffset, xoffset + width, yoffset + height}.intersection(
+ t.offset_bounds());
+ ClearTexSubImage(fb.color_attachment, 0, scissor.x0, scissor.y0, 0,
+ scissor.width(), scissor.height(), 1, GL_RGBA, GL_FLOAT,
+ color);
+}
+
+void InvalidateFramebuffer(GLenum target, GLsizei num_attachments,
+ const GLenum* attachments) {
+ Framebuffer* fb = get_framebuffer(target);
+ if (!fb || num_attachments <= 0 || !attachments) {
+ return;
+ }
+ for (GLsizei i = 0; i < num_attachments; i++) {
+ switch (attachments[i]) {
+ case GL_DEPTH_ATTACHMENT: {
+ Texture& t = ctx->textures[fb->depth_attachment];
+ t.set_cleared(false);
+ break;
+ }
+ case GL_COLOR_ATTACHMENT0: {
+ Texture& t = ctx->textures[fb->color_attachment];
+ t.disable_delayed_clear();
+ break;
+ }
+ }
+ }
+}
+
+void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format,
+ GLenum type, void* data) {
+ data = get_pixel_pack_buffer_data(data);
+ if (!data) return;
+ Framebuffer* fb = get_framebuffer(GL_READ_FRAMEBUFFER);
+ if (!fb) return;
+ assert(format == GL_RED || format == GL_RGBA || format == GL_RGBA_INTEGER ||
+ format == GL_BGRA || format == GL_RG);
+ Texture& t = ctx->textures[fb->color_attachment];
+ if (!t.buf) return;
+ prepare_texture(t);
+ // debugf("read pixels %d, %d, %d, %d from fb %d with format %x\n", x, y,
+ // width, height, ctx->read_framebuffer_binding, t.internal_format);
+ x -= t.offset.x;
+ y -= t.offset.y;
+ assert(x >= 0 && y >= 0);
+ assert(x + width <= t.width);
+ assert(y + height <= t.height);
+ if (internal_format_for_data(format, type) != t.internal_format) {
+ debugf("mismatched format for read pixels: %x vs %x\n", t.internal_format,
+ internal_format_for_data(format, type));
+ assert(false);
+ return;
+ }
+ // Only support readback conversions that are reversible
+ assert(!format_requires_conversion(format, t.internal_format) ||
+ bytes_for_internal_format(format) == t.bpp());
+ uint8_t* dest = (uint8_t*)data;
+ size_t destStride = width * t.bpp();
+ if (y < 0) {
+ dest += -y * destStride;
+ height += y;
+ y = 0;
+ }
+ if (y + height > t.height) {
+ height = t.height - y;
+ }
+ if (x < 0) {
+ dest += -x * t.bpp();
+ width += x;
+ x = 0;
+ }
+ if (x + width > t.width) {
+ width = t.width - x;
+ }
+ if (width <= 0 || height <= 0) {
+ return;
+ }
+ convert_copy(format, t.internal_format, dest, destStride,
+ (const uint8_t*)t.sample_ptr(x, y), t.stride(), width, height);
+}
+
+void CopyImageSubData(GLuint srcName, GLenum srcTarget, UNUSED GLint srcLevel,
+ GLint srcX, GLint srcY, GLint srcZ, GLuint dstName,
+ GLenum dstTarget, UNUSED GLint dstLevel, GLint dstX,
+ GLint dstY, GLint dstZ, GLsizei srcWidth,
+ GLsizei srcHeight, GLsizei srcDepth) {
+ assert(srcLevel == 0 && dstLevel == 0);
+ assert(srcZ == 0 && srcDepth == 1 && dstZ == 0);
+ if (srcTarget == GL_RENDERBUFFER) {
+ Renderbuffer& rb = ctx->renderbuffers[srcName];
+ srcName = rb.texture;
+ }
+ if (dstTarget == GL_RENDERBUFFER) {
+ Renderbuffer& rb = ctx->renderbuffers[dstName];
+ dstName = rb.texture;
+ }
+ Texture& srctex = ctx->textures[srcName];
+ if (!srctex.buf) return;
+ prepare_texture(srctex);
+ Texture& dsttex = ctx->textures[dstName];
+ if (!dsttex.buf) return;
+ assert(!dsttex.locked);
+ IntRect skip = {dstX, dstY, dstX + srcWidth, dstY + srcHeight};
+ prepare_texture(dsttex, &skip);
+ assert(srctex.internal_format == dsttex.internal_format);
+ assert(srcWidth >= 0);
+ assert(srcHeight >= 0);
+ assert(srcX + srcWidth <= srctex.width);
+ assert(srcY + srcHeight <= srctex.height);
+ assert(dstX + srcWidth <= dsttex.width);
+ assert(dstY + srcHeight <= dsttex.height);
+ int bpp = srctex.bpp();
+ int src_stride = srctex.stride();
+ int dest_stride = dsttex.stride();
+ char* dest = dsttex.sample_ptr(dstX, dstY);
+ char* src = srctex.sample_ptr(srcX, srcY);
+ for (int y = 0; y < srcHeight; y++) {
+ memcpy(dest, src, srcWidth * bpp);
+ dest += dest_stride;
+ src += src_stride;
+ }
+}
+
+void CopyTexSubImage2D(GLenum target, UNUSED GLint level, GLint xoffset,
+ GLint yoffset, GLint x, GLint y, GLsizei width,
+ GLsizei height) {
+ assert(level == 0);
+ Framebuffer* fb = get_framebuffer(GL_READ_FRAMEBUFFER);
+ if (!fb) return;
+ CopyImageSubData(fb->color_attachment, GL_TEXTURE_2D, 0, x, y, 0,
+ ctx->get_binding(target), GL_TEXTURE_2D, 0, xoffset, yoffset,
+ 0, width, height, 1);
+}
+
+} // extern "C"
+
+#include "blend.h"
+#include "composite.h"
+#include "swgl_ext.h"
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wuninitialized"
+#pragma GCC diagnostic ignored "-Wunused-function"
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-variable"
+#pragma GCC diagnostic ignored "-Wimplicit-fallthrough"
+#ifdef __clang__
+# pragma GCC diagnostic ignored "-Wunused-private-field"
+#else
+# pragma GCC diagnostic ignored "-Wunused-but-set-variable"
+#endif
+#include "load_shader.h"
+#pragma GCC diagnostic pop
+
+#include "rasterize.h"
+
+void VertexArray::validate() {
+ int last_enabled = -1;
+ for (int i = 0; i <= max_attrib; i++) {
+ VertexAttrib& attr = attribs[i];
+ if (attr.enabled) {
+ // VertexArray &v = ctx->vertex_arrays[attr.vertex_array];
+ Buffer& vertex_buf = ctx->buffers[attr.vertex_buffer];
+ attr.buf = vertex_buf.buf;
+ attr.buf_size = vertex_buf.size;
+ // debugf("%d %x %d %d %d %d\n", i, attr.type, attr.size, attr.stride,
+ // attr.offset, attr.divisor);
+ last_enabled = i;
+ }
+ }
+ max_attrib = last_enabled;
+}
+
+extern "C" {
+
+void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type,
+ GLintptr offset, GLsizei instancecount) {
+ if (offset < 0 || count <= 0 || instancecount <= 0 || !vertex_shader ||
+ !fragment_shader) {
+ return;
+ }
+
+ Framebuffer& fb = *get_framebuffer(GL_DRAW_FRAMEBUFFER, true);
+ if (!fb.color_attachment) {
+ return;
+ }
+ Texture& colortex = ctx->textures[fb.color_attachment];
+ if (!colortex.buf) {
+ return;
+ }
+ assert(!colortex.locked);
+ assert(colortex.internal_format == GL_RGBA8 ||
+ colortex.internal_format == GL_R8);
+ Texture& depthtex = ctx->textures[ctx->depthtest ? fb.depth_attachment : 0];
+ if (depthtex.buf) {
+ assert(depthtex.internal_format == GL_DEPTH_COMPONENT24);
+ assert(colortex.width == depthtex.width &&
+ colortex.height == depthtex.height);
+ assert(colortex.offset == depthtex.offset);
+ }
+
+ // debugf("current_vertex_array %d\n", ctx->current_vertex_array);
+ // debugf("indices size: %d\n", indices_buf.size);
+ VertexArray& v = ctx->vertex_arrays[ctx->current_vertex_array];
+ if (ctx->validate_vertex_array) {
+ ctx->validate_vertex_array = false;
+ v.validate();
+ }
+
+#ifdef PRINT_TIMINGS
+ uint64_t start = get_time_value();
+#endif
+
+ ctx->shaded_rows = 0;
+ ctx->shaded_pixels = 0;
+
+ vertex_shader->init_batch();
+
+ switch (type) {
+ case GL_UNSIGNED_SHORT:
+ assert(mode == GL_TRIANGLES);
+ draw_elements<uint16_t>(count, instancecount, offset, v, colortex,
+ depthtex);
+ break;
+ case GL_UNSIGNED_INT:
+ assert(mode == GL_TRIANGLES);
+ draw_elements<uint32_t>(count, instancecount, offset, v, colortex,
+ depthtex);
+ break;
+ case GL_NONE:
+ // Non-standard GL extension - if element type is GL_NONE, then we don't
+ // use any element buffer and behave as if DrawArrays was called instead.
+ for (GLsizei instance = 0; instance < instancecount; instance++) {
+ switch (mode) {
+ case GL_LINES:
+ for (GLsizei i = 0; i + 2 <= count; i += 2) {
+ vertex_shader->load_attribs(v.attribs, offset + i, instance, 2);
+ draw_quad(2, colortex, depthtex);
+ }
+ break;
+ case GL_TRIANGLES:
+ for (GLsizei i = 0; i + 3 <= count; i += 3) {
+ vertex_shader->load_attribs(v.attribs, offset + i, instance, 3);
+ draw_quad(3, colortex, depthtex);
+ }
+ break;
+ default:
+ assert(false);
+ break;
+ }
+ }
+ break;
+ default:
+ assert(false);
+ break;
+ }
+
+ if (ctx->samples_passed_query) {
+ Query& q = ctx->queries[ctx->samples_passed_query];
+ q.value += ctx->shaded_pixels;
+ }
+
+#ifdef PRINT_TIMINGS
+ uint64_t end = get_time_value();
+ printf(
+ "%7.3fms draw(%s, %d): %d pixels in %d rows (avg %f pixels/row, "
+ "%fns/pixel)\n",
+ double(end - start) / (1000. * 1000.),
+ ctx->programs[ctx->current_program].impl->get_name(), instancecount,
+ ctx->shaded_pixels, ctx->shaded_rows,
+ double(ctx->shaded_pixels) / ctx->shaded_rows,
+ double(end - start) / max(ctx->shaded_pixels, 1));
+#endif
+}
+
+void Finish() {
+#ifdef PRINT_TIMINGS
+ printf("Finish\n");
+#endif
+}
+
+void MakeCurrent(Context* c) {
+ if (ctx == c) {
+ return;
+ }
+ ctx = c;
+ setup_program(ctx ? ctx->current_program : 0);
+}
+
+Context* CreateContext() { return new Context; }
+
+void ReferenceContext(Context* c) {
+ if (!c) {
+ return;
+ }
+ ++c->references;
+}
+
+void DestroyContext(Context* c) {
+ if (!c) {
+ return;
+ }
+ assert(c->references > 0);
+ --c->references;
+ if (c->references > 0) {
+ return;
+ }
+ if (ctx == c) {
+ MakeCurrent(nullptr);
+ }
+ delete c;
+}
+
+size_t ReportMemory(Context* ctx, size_t (*size_of_op)(const void*)) {
+ size_t size = 0;
+ if (ctx) {
+ for (auto& t : ctx->textures) {
+ if (t && t->should_free()) {
+ size += size_of_op(t->buf);
+ }
+ }
+ }
+ return size;
+}
+} // extern "C"
diff --git a/gfx/wr/swgl/src/gl_defs.h b/gfx/wr/swgl/src/gl_defs.h
new file mode 100644
index 0000000000..b60eaad0ec
--- /dev/null
+++ b/gfx/wr/swgl/src/gl_defs.h
@@ -0,0 +1,220 @@
+/* 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/. */
+
+typedef int8_t GLbyte;
+typedef uint8_t GLubyte;
+typedef int16_t GLshort;
+typedef uint16_t GLushort;
+typedef int32_t GLint;
+typedef uint32_t GLuint;
+typedef int64_t GLint64;
+typedef uint64_t GLuint64;
+
+typedef float GLfloat;
+typedef double GLdouble;
+
+typedef uint32_t GLenum;
+typedef uint8_t GLboolean;
+typedef uint32_t GLbitfield;
+
+typedef int32_t GLsizei;
+typedef size_t GLsizeiptr;
+typedef intptr_t GLintptr;
+
+#define GL_FALSE 0
+#define GL_TRUE 1
+
+#define GL_NONE 0
+
+#define GL_NO_ERROR 0
+#define GL_OUT_OF_MEMORY 0x0505
+
+#define GL_RGBA32F 0x8814
+#define GL_RGBA8 0x8058
+#define GL_R8 0x8229
+#define GL_R16 0x822A
+#define GL_RG16 0x822C
+#define GL_RGBA32I 0x8D82
+#define GL_BGRA8 0x93A1
+#define GL_RG8 0x822B
+
+#define GL_BYTE 0x1400
+#define GL_UNSIGNED_BYTE 0x1401
+#define GL_SHORT 0x1402
+#define GL_UNSIGNED_SHORT 0x1403
+#define GL_INT 0x1404
+#define GL_UNSIGNED_INT 0x1405
+#define GL_FLOAT 0x1406
+#define GL_DOUBLE 0x1408
+
+#define GL_RED 0x1903
+#define GL_GREEN 0x1904
+#define GL_BLUE 0x1905
+#define GL_ALPHA 0x1906
+#define GL_RGB 0x1907
+#define GL_RGBA 0x1908
+#define GL_RGBA_INTEGER 0x8D99
+#define GL_BGRA 0x80E1
+#define GL_RG 0x8227
+
+#define GL_DEPTH_COMPONENT 0x1902
+#define GL_DEPTH_COMPONENT16 0x81A5
+#define GL_DEPTH_COMPONENT24 0x81A6
+#define GL_DEPTH_COMPONENT32 0x81A7
+
+#define GL_ARRAY_BUFFER 0x8892
+#define GL_ELEMENT_ARRAY_BUFFER 0x8893
+
+#define GL_READ_FRAMEBUFFER 0x8CA8
+#define GL_DRAW_FRAMEBUFFER 0x8CA9
+#define GL_FRAMEBUFFER 0x8D40
+#define GL_DRAW_FRAMEBUFFER_BINDING 0x8CA6
+#define GL_READ_FRAMEBUFFER_BINDING 0x8CAA
+#define GL_RENDERBUFFER 0x8D41
+#define GL_COLOR_ATTACHMENT0 0x8CE0
+#define GL_DEPTH_ATTACHMENT 0x8D00
+#define GL_STENCIL_ATTACHMENT 0x8D20
+#define GL_FRAMEBUFFER_COMPLETE 0x8CD5
+#define GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT 0x8CD6
+#define GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT 0x8CD7
+#define GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER 0x8CDB
+#define GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER 0x8CDC
+#define GL_FRAMEBUFFER_UNSUPPORTED 0x8CDD
+#define GL_COLOR_BUFFER_BIT 0x00004000
+#define GL_DEPTH_BUFFER_BIT 0x00000100
+#define GL_STENCIL_BUFFER_BIT 0x00000400
+
+#define GL_PIXEL_PACK_BUFFER 0x88EB
+#define GL_PIXEL_UNPACK_BUFFER 0x88EC
+#define GL_PIXEL_PACK_BUFFER_BINDING 0x88ED
+#define GL_PIXEL_UNPACK_BUFFER_BINDING 0x88EF
+#define GL_UNPACK_ROW_LENGTH 0x0CF2
+#define GL_UNPACK_ALIGNMENT 0x0CF5
+
+#define GL_QUERY_RESULT 0x8866
+#define GL_QUERY_RESULT_AVAILABLE 0x8867
+#define GL_TIME_ELAPSED 0x88BF
+#define GL_SAMPLES_PASSED 0x8914
+
+#define GL_NEAREST 0x2600
+#define GL_LINEAR 0x2601
+#define GL_NEAREST_MIPMAP_NEAREST 0x2700
+#define GL_NEAREST_MIPMAP_LINEAR 0x2702
+#define GL_LINEAR_MIPMAP_NEAREST 0x2701
+#define GL_LINEAR_MIPMAP_LINEAR 0x2703
+#define GL_TEXTURE_WRAP_S 0x2802
+#define GL_TEXTURE_WRAP_T 0x2803
+#define GL_TEXTURE_MAG_FILTER 0x2800
+#define GL_TEXTURE_MIN_FILTER 0x2801
+#define GL_CLAMP_TO_EDGE 0x812F
+#define GL_TEXTURE_2D 0x0DE1
+#define GL_TEXTURE_3D 0x806F
+#define GL_TEXTURE_2D_ARRAY 0x8C1A
+#define GL_TEXTURE_RECTANGLE 0x84F5
+#define GL_TEXTURE0 0x84C0
+#define GL_TEXTURE1 0x84C1
+#define GL_TEXTURE2 0x84C2
+#define GL_TEXTURE3 0x84C3
+#define GL_TEXTURE4 0x84C4
+#define GL_TEXTURE5 0x84C5
+#define GL_TEXTURE6 0x84C6
+#define GL_TEXTURE7 0x84C7
+#define GL_TEXTURE8 0x84C8
+#define GL_TEXTURE9 0x84C9
+#define GL_TEXTURE10 0x84CA
+#define GL_TEXTURE11 0x84CB
+#define GL_TEXTURE12 0x84CC
+#define GL_TEXTURE13 0x84CD
+#define GL_TEXTURE14 0x84CE
+#define GL_TEXTURE15 0x84CF
+#define GL_MAX_TEXTURE_UNITS 0x84E2
+#define GL_MAX_TEXTURE_IMAGE_UNITS 0x8872
+#define GL_MAX_TEXTURE_SIZE 0x0D33
+#define GL_MAX_ARRAY_TEXTURE_LAYERS 0x88FF
+#define GL_MIN_PROGRAM_TEXEL_OFFSET 0x8904
+#define GL_MAX_PROGRAM_TEXEL_OFFSET 0x8905
+
+#define GL_VERTEX_SHADER 0x8B31
+#define GL_FRAGMENT_SHADER 0x8B30
+
+#define GL_BLEND 0x0BE2
+#define GL_ZERO 0
+#define GL_ONE 1
+#define GL_SRC_COLOR 0x0300
+#define GL_ONE_MINUS_SRC_COLOR 0x0301
+#define GL_SRC_ALPHA 0x0302
+#define GL_ONE_MINUS_SRC_ALPHA 0x0303
+#define GL_DST_ALPHA 0x0304
+#define GL_ONE_MINUS_DST_ALPHA 0x0305
+#define GL_DST_COLOR 0x0306
+#define GL_ONE_MINUS_DST_COLOR 0x0307
+#define GL_CONSTANT_COLOR 0x8001
+#define GL_ONE_MINUS_CONSTANT_COLOR 0x8002
+#define GL_CONSTANT_ALPHA 0x8003
+#define GL_ONE_MINUS_CONSTANT_ALPHA 0x8004
+#define GL_SRC1_ALPHA 0x8589
+#define GL_SRC1_COLOR 0x88F9
+#define GL_ONE_MINUS_SRC1_COLOR 0x88FA
+#define GL_ONE_MINUS_SRC1_ALPHA 0x88FB
+
+#define GL_FUNC_ADD 0x8006
+#define GL_MIN 0x8007
+#define GL_MAX 0x8008
+
+#define GL_NEVER 0x0200
+#define GL_LESS 0x0201
+#define GL_EQUAL 0x0202
+#define GL_LEQUAL 0x0203
+#define GL_GREATER 0x0204
+#define GL_NOTEQUAL 0x0205
+#define GL_GEQUAL 0x0206
+#define GL_ALWAYS 0x0207
+#define GL_DEPTH_TEST 0x0B71
+#define GL_DEPTH_WRITEMASK 0x0B72
+
+#define GL_SCISSOR_TEST 0x0C11
+
+#define GL_VENDOR 0x1F00
+#define GL_RENDERER 0x1F01
+#define GL_VERSION 0x1F02
+#define GL_EXTENSIONS 0x1F03
+#define GL_NUM_EXTENSIONS 0x821D
+#define GL_MINOR_VERSION 0x821C
+#define GL_MAJOR_VERSION 0x821B
+#define GL_SHADING_LANGUAGE_VERSION 0x8B8C
+
+#define GL_POINTS 0x0000
+#define GL_LINES 0x0001
+#define GL_LINE_LOOP 0x0002
+#define GL_LINE_STRIP 0x0003
+#define GL_TRIANGLES 0x0004
+#define GL_TRIANGLE_STRIP 0x0005
+#define GL_TRIANGLE_FAN 0x0006
+#define GL_QUADS 0x0007
+
+#define GL_UNSIGNED_INT_8_8_8_8_REV 0x8367
+
+#define GL_RGB_422_APPLE 0x8A1F
+#define GL_UNSIGNED_SHORT_8_8_APPLE 0x85BA
+#define GL_UNSIGNED_SHORT_8_8_REV_APPLE 0x85BB
+#define GL_RGB_RAW_422_APPLE 0x8A51
+
+#define GL_MULTIPLY_KHR 0x9294
+#define GL_SCREEN_KHR 0x9295
+#define GL_OVERLAY_KHR 0x9296
+#define GL_DARKEN_KHR 0x9297
+#define GL_LIGHTEN_KHR 0x9298
+#define GL_COLORDODGE_KHR 0x9299
+#define GL_COLORBURN_KHR 0x929A
+#define GL_HARDLIGHT_KHR 0x929B
+#define GL_SOFTLIGHT_KHR 0x929C
+#define GL_DIFFERENCE_KHR 0x929E
+#define GL_EXCLUSION_KHR 0x92A0
+#define GL_HSL_HUE_KHR 0x92AD
+#define GL_HSL_SATURATION_KHR 0x92AE
+#define GL_HSL_COLOR_KHR 0x92AF
+#define GL_HSL_LUMINOSITY_KHR 0x92B0
+
+#define SWGL_BLEND_DROP_SHADOW 0xB001
+#define SWGL_BLEND_SUBPIXEL_TEXT 0xB002
diff --git a/gfx/wr/swgl/src/glsl.h b/gfx/wr/swgl/src/glsl.h
new file mode 100644
index 0000000000..64743b78c8
--- /dev/null
+++ b/gfx/wr/swgl/src/glsl.h
@@ -0,0 +1,3117 @@
+/* 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/. */
+
+#define SI ALWAYS_INLINE static
+
+#include "vector_type.h"
+
+namespace glsl {
+
+enum TextureFormat { RGBA32F, RGBA32I, RGBA8, R8, RG8, R16, RG16, YUV422 };
+
+enum TextureFilter { NEAREST, LINEAR };
+
+struct samplerCommon {
+ uint32_t* buf = nullptr;
+ uint32_t stride = 0; // in units of BPP if < 4, or dwords if BPP >= 4
+ uint32_t height = 0;
+ uint32_t width = 0;
+ TextureFormat format = TextureFormat::RGBA8;
+};
+
+struct samplerFilter {
+ TextureFilter filter = TextureFilter::NEAREST;
+};
+
+struct sampler2D_impl : samplerCommon, samplerFilter {};
+typedef sampler2D_impl* sampler2D;
+
+typedef struct sampler2DR8_impl : sampler2D_impl{} * sampler2DR8;
+typedef struct sampler2DRG8_impl : sampler2D_impl{} * sampler2DRG8;
+typedef struct sampler2DRGBA8_impl : sampler2D_impl{} * sampler2DRGBA8;
+typedef struct sampler2DRGBA32F_impl : sampler2D_impl{} * sampler2DRGBA32F;
+
+struct isampler2D_impl : samplerCommon {};
+typedef isampler2D_impl* isampler2D;
+
+struct isampler2DRGBA32I_impl : isampler2D_impl {};
+typedef isampler2DRGBA32I_impl* isampler2DRGBA32I;
+
+struct sampler2DRect_impl : samplerCommon, samplerFilter {};
+typedef sampler2DRect_impl* sampler2DRect;
+
+#if USE_SSE2
+SI bool test_all(Bool cond) { return _mm_movemask_ps(cond) == 0xF; }
+SI bool test_any(Bool cond) { return _mm_movemask_ps(cond) != 0; }
+SI bool test_none(Bool cond) { return _mm_movemask_ps(cond) == 0; }
+#else
+SI bool test_all(Bool cond) {
+ return bit_cast<uint32_t>(CONVERT(cond, U8)) == 0xFFFFFFFFU;
+}
+SI bool test_any(Bool cond) {
+ return bit_cast<uint32_t>(CONVERT(cond, U8)) != 0;
+}
+SI bool test_none(Bool cond) {
+ return bit_cast<uint32_t>(CONVERT(cond, U8)) == 0;
+}
+#endif
+SI bool test_equal(Bool cond) { return test_none(cond != cond.x); }
+
+float make_float(float n) { return n; }
+
+float make_float(int32_t n) { return float(n); }
+
+float make_float(uint32_t n) { return float(n); }
+
+float make_float(bool n) { return float(n); }
+
+template <typename T>
+Float make_float(T v) {
+ return CONVERT(v, Float);
+}
+
+int32_t make_int(uint32_t n) { return n; }
+
+int32_t make_int(int32_t n) { return n; }
+
+int32_t make_int(float n) { return int32_t(n); }
+
+int32_t make_int(bool n) { return int32_t(n); }
+
+template <typename T>
+I32 make_int(T v) {
+ return CONVERT(v, I32);
+}
+
+uint32_t make_uint(uint32_t n) { return n; }
+
+uint32_t make_uint(int32_t n) { return n; }
+
+uint32_t make_uint(float n) { return uint32_t(n); }
+
+uint32_t make_uint(bool n) { return uint32_t(n); }
+
+template <typename T>
+U32 make_uint(T v) {
+ return CONVERT(v, U32);
+}
+
+template <typename T>
+T force_scalar(T n) {
+ return n;
+}
+
+float force_scalar(Float f) { return f[0]; }
+
+int32_t force_scalar(I32 i) { return i[0]; }
+
+struct vec4;
+struct ivec2;
+
+SI int32_t if_then_else(int32_t c, int32_t t, int32_t e) { return c ? t : e; }
+SI int32_t if_then_else(bool c, int32_t t, int32_t e) { return c ? t : e; }
+
+SI float if_then_else(int32_t c, float t, float e) { return c ? t : e; }
+
+SI Float if_then_else(I32 c, float t, float e) {
+ return bit_cast<Float>((c & bit_cast<I32>(Float(t))) |
+ (~c & bit_cast<I32>(Float(e))));
+}
+
+SI I32 if_then_else(I32 c, int32_t t, int32_t e) {
+ return (c & I32(t)) | (~c & I32(e));
+}
+
+SI U32 if_then_else(I32 c, U32 t, U32 e) {
+ return bit_cast<U32>((c & bit_cast<I32>(t)) | (~c & bit_cast<I32>(e)));
+}
+
+// Cheaper version of if_then_else that returns Float(0) if condition is false.
+SI Float if_then(I32 c, Float t) {
+ return bit_cast<Float>(c & bit_cast<I32>(t));
+}
+
+SI Float if_then_else(I32 c, Float t, Float e) {
+ return bit_cast<Float>((c & bit_cast<I32>(t)) | (~c & bit_cast<I32>(e)));
+}
+
+SI Float if_then_else(int32_t c, Float t, Float e) { return c ? t : e; }
+
+SI Bool if_then_else(I32 c, Bool t, Bool e) { return (c & t) | (~c & e); }
+
+SI Bool if_then_else(int32_t c, Bool t, Bool e) { return c ? t : e; }
+
+SI I16 if_then_else(I16 c, I16 t, I16 e) { return (c & t) | (~c & e); }
+
+template <typename T>
+SI void swap(T& a, T& b) {
+ T t(a);
+ a = b;
+ b = t;
+}
+
+SI int32_t min(int32_t a, int32_t b) { return a < b ? a : b; }
+SI int32_t max(int32_t a, int32_t b) { return a > b ? a : b; }
+
+SI int32_t clamp(int32_t a, int32_t minVal, int32_t maxVal) {
+ return min(max(a, minVal), maxVal);
+}
+
+SI float min(float a, float b) { return a < b ? a : b; }
+SI float max(float a, float b) { return a > b ? a : b; }
+
+SI float clamp(float a, float minVal, float maxVal) {
+ return min(max(a, minVal), maxVal);
+}
+
+SI Float min(Float a, Float b) {
+#if USE_SSE2
+ return _mm_min_ps(a, b);
+#elif USE_NEON
+ return vminq_f32(a, b);
+#else
+ return if_then_else(a < b, a, b);
+#endif
+}
+
+SI Float max(Float a, Float b) {
+#if USE_SSE2
+ return _mm_max_ps(a, b);
+#elif USE_NEON
+ return vmaxq_f32(a, b);
+#else
+ return if_then_else(a > b, a, b);
+#endif
+}
+
+SI Float clamp(Float a, Float minVal, Float maxVal) {
+ return min(max(a, minVal), maxVal);
+}
+
+#define sqrt __glsl_sqrt
+
+SI float sqrt(float x) { return sqrtf(x); }
+
+SI Float sqrt(Float v) {
+#if USE_SSE2
+ return _mm_sqrt_ps(v);
+#elif USE_NEON
+ Float e = vrsqrteq_f32(v);
+ e *= vrsqrtsq_f32(v, e * e);
+ e *= vrsqrtsq_f32(v, e * e);
+ return if_then(v != Float(0.0f), v * e);
+#else
+ return (Float){sqrtf(v.x), sqrtf(v.y), sqrtf(v.z), sqrtf(v.w)};
+#endif
+}
+
+SI float recip(float x) {
+#if USE_SSE2
+ return _mm_cvtss_f32(_mm_rcp_ss(_mm_set_ss(x)));
+#else
+ return 1.0f / x;
+#endif
+}
+
+// Use a fast vector reciprocal approximation when available. This should only
+// be used in cases where it is okay that the approximation is imprecise -
+// essentially visually correct but numerically wrong. Otherwise just rely on
+// however the compiler would implement slower division if the platform doesn't
+// provide a convenient intrinsic.
+SI Float recip(Float v) {
+#if USE_SSE2
+ return _mm_rcp_ps(v);
+#elif USE_NEON
+ Float e = vrecpeq_f32(v);
+ return vrecpsq_f32(v, e) * e;
+#else
+ return 1.0f / v;
+#endif
+}
+
+SI float inversesqrt(float x) {
+#if USE_SSE2
+ return _mm_cvtss_f32(_mm_rsqrt_ss(_mm_set_ss(x)));
+#else
+ return 1.0f / sqrtf(x);
+#endif
+}
+
+SI Float inversesqrt(Float v) {
+#if USE_SSE2
+ return _mm_rsqrt_ps(v);
+#elif USE_NEON
+ Float e = vrsqrteq_f32(v);
+ return vrsqrtsq_f32(v, e * e) * e;
+#else
+ return 1.0f / sqrt(v);
+#endif
+}
+
+SI float step(float edge, float x) { return float(x >= edge); }
+
+SI Float step(Float edge, Float x) { return if_then(x >= edge, Float(1)); }
+
+/*
+enum RGBA {
+ R,
+ G,
+ B,
+ A
+};*/
+
+enum XYZW {
+ X = 0,
+ Y = 1,
+ Z = 2,
+ W = 3,
+ R = 0,
+ G = 1,
+ B = 2,
+ A = 3,
+};
+
+struct bvec4_scalar;
+
+struct bvec2_scalar {
+ bool x;
+ bool y;
+
+ bvec2_scalar() : bvec2_scalar(false) {}
+ IMPLICIT constexpr bvec2_scalar(bool a) : x(a), y(a) {}
+ constexpr bvec2_scalar(bool x, bool y) : x(x), y(y) {}
+
+ bool& select(XYZW c) {
+ switch (c) {
+ case X:
+ return x;
+ case Y:
+ return y;
+ default:
+ UNREACHABLE;
+ }
+ }
+ bool sel(XYZW c1) { return select(c1); }
+
+ bvec2_scalar sel(XYZW c1, XYZW c2) {
+ return bvec2_scalar(select(c1), select(c2));
+ }
+ bvec4_scalar sel(XYZW c1, XYZW c2, XYZW c3, XYZW c4);
+};
+
+struct bvec2_scalar1 {
+ bool x;
+
+ IMPLICIT constexpr bvec2_scalar1(bool a) : x(a) {}
+
+ operator bvec2_scalar() const { return bvec2_scalar(x); }
+};
+
+struct bvec2 {
+ bvec2() : bvec2(0) {}
+ IMPLICIT bvec2(Bool a) : x(a), y(a) {}
+ bvec2(Bool x, Bool y) : x(x), y(y) {}
+ Bool& select(XYZW c) {
+ switch (c) {
+ case X:
+ return x;
+ case Y:
+ return y;
+ default:
+ UNREACHABLE;
+ }
+ }
+ Bool sel(XYZW c1) { return select(c1); }
+
+ bvec2 sel(XYZW c1, XYZW c2) { return bvec2(select(c1), select(c2)); }
+
+ bvec2 operator~() { return bvec2(~x, ~y); }
+
+ Bool x;
+ Bool y;
+};
+
+bvec2_scalar1 make_bvec2(bool n) { return bvec2_scalar1(n); }
+
+bvec2_scalar make_bvec2(bool x, bool y) { return bvec2_scalar{x, y}; }
+
+template <typename N>
+bvec2 make_bvec2(const N& n) {
+ return bvec2(n);
+}
+
+template <typename X, typename Y>
+bvec2 make_bvec2(const X& x, const Y& y) {
+ return bvec2(x, y);
+}
+
+struct vec3_scalar;
+struct vec4_scalar;
+
+struct vec2_scalar {
+ typedef struct vec2 vector_type;
+ typedef float element_type;
+
+ float x;
+ float y;
+
+ constexpr vec2_scalar() : vec2_scalar(0.0f) {}
+ IMPLICIT constexpr vec2_scalar(float a) : x(a), y(a) {}
+ IMPLICIT constexpr vec2_scalar(int a) : x(a), y(a) {}
+ constexpr vec2_scalar(float x, float y) : x(x), y(y) {}
+
+ float& select(XYZW c) {
+ switch (c) {
+ case X:
+ return x;
+ case Y:
+ return y;
+ default:
+ UNREACHABLE;
+ }
+ }
+ float& sel(XYZW c1) { return select(c1); }
+ vec2_scalar sel(XYZW c1, XYZW c2) {
+ return vec2_scalar(select(c1), select(c2));
+ }
+ vec3_scalar sel(XYZW c1, XYZW c2, XYZW c3);
+ vec4_scalar sel(XYZW c1, XYZW c2, XYZW c3, XYZW c4);
+
+ friend bool operator==(const vec2_scalar& l, const vec2_scalar& r) {
+ return l.x == r.x && l.y == r.y;
+ }
+
+ friend bool operator!=(const vec2_scalar& l, const vec2_scalar& r) {
+ return l.x != r.x || l.y != r.y;
+ }
+
+ friend vec2_scalar operator*(float a, vec2_scalar b) {
+ return vec2_scalar(a * b.x, a * b.y);
+ }
+ friend vec2_scalar operator*(vec2_scalar a, float b) {
+ return vec2_scalar(a.x * b, a.y * b);
+ }
+ friend vec2_scalar operator*(vec2_scalar a, vec2_scalar b) {
+ return vec2_scalar(a.x * b.x, a.y * b.y);
+ }
+ friend vec2_scalar operator/(vec2_scalar a, float b) {
+ return vec2_scalar(a.x / b, a.y / b);
+ }
+ friend vec2_scalar operator/(vec2_scalar a, vec2_scalar b) {
+ return vec2_scalar(a.x / b.x, a.y / b.y);
+ }
+
+ friend vec2_scalar operator-(vec2_scalar a, vec2_scalar b) {
+ return vec2_scalar(a.x - b.x, a.y - b.y);
+ }
+ friend vec2_scalar operator-(vec2_scalar a, float b) {
+ return vec2_scalar(a.x - b, a.y - b);
+ }
+ friend vec2_scalar operator-(float a, vec2_scalar b) {
+ return vec2_scalar(a - b.x, a - b.y);
+ }
+ friend vec2_scalar operator+(vec2_scalar a, vec2_scalar b) {
+ return vec2_scalar(a.x + b.x, a.y + b.y);
+ }
+ friend vec2_scalar operator+(vec2_scalar a, float b) {
+ return vec2_scalar(a.x + b, a.y + b);
+ }
+
+ vec2_scalar operator-() { return vec2_scalar(-x, -y); }
+
+ vec2_scalar operator*=(vec2_scalar a) {
+ x *= a.x;
+ y *= a.y;
+ return *this;
+ }
+
+ vec2_scalar operator/=(vec2_scalar a) {
+ x /= a.x;
+ y /= a.y;
+ return *this;
+ }
+
+ vec2_scalar operator+=(vec2_scalar a) {
+ x += a.x;
+ y += a.y;
+ return *this;
+ }
+
+ vec2_scalar operator-=(vec2_scalar a) {
+ x -= a.x;
+ y -= a.y;
+ return *this;
+ }
+};
+
+struct vec2_scalar_ref {
+ vec2_scalar_ref(float& x, float& y) : x(x), y(y) {}
+ float& x;
+ float& y;
+
+ float& select(XYZW c) {
+ switch (c) {
+ case X:
+ return x;
+ case Y:
+ return y;
+ default:
+ UNREACHABLE;
+ }
+ }
+ float& sel(XYZW c1) { return select(c1); }
+
+ vec2_scalar_ref& operator=(const vec2_scalar& a) {
+ x = a.x;
+ y = a.y;
+ return *this;
+ }
+ vec2_scalar_ref& operator*=(vec2_scalar a) {
+ x *= a.x;
+ y *= a.y;
+ return *this;
+ }
+ operator vec2_scalar() const { return vec2_scalar{x, y}; }
+};
+
+struct vec2 {
+ typedef struct vec2 vector_type;
+ typedef float element_type;
+
+ constexpr vec2() : vec2(Float(0.0f)) {}
+ IMPLICIT constexpr vec2(Float a) : x(a), y(a) {}
+ vec2(Float x, Float y) : x(x), y(y) {}
+ IMPLICIT constexpr vec2(vec2_scalar s) : x(s.x), y(s.y) {}
+ constexpr vec2(vec2_scalar s0, vec2_scalar s1, vec2_scalar s2, vec2_scalar s3)
+ : x(Float{s0.x, s1.x, s2.x, s3.x}), y(Float{s0.y, s1.y, s2.y, s3.y}) {}
+ explicit vec2(ivec2 a);
+ Float x;
+ Float y;
+
+ Float& select(XYZW c) {
+ switch (c) {
+ case X:
+ return x;
+ case Y:
+ return y;
+ default:
+ UNREACHABLE;
+ }
+ }
+ Float& sel(XYZW c1) { return select(c1); }
+ vec2 sel(XYZW c1, XYZW c2) { return vec2(select(c1), select(c2)); }
+
+ vec4 sel(XYZW c1, XYZW c2, XYZW c3, XYZW c4);
+
+ vec2 operator*=(Float a) {
+ x *= a;
+ y *= a;
+ return *this;
+ }
+ vec2 operator*=(vec2 a) {
+ x *= a.x;
+ y *= a.y;
+ return *this;
+ }
+
+ vec2 operator/=(Float a) {
+ x /= a;
+ y /= a;
+ return *this;
+ }
+ vec2 operator/=(vec2 a) {
+ x /= a.x;
+ y /= a.y;
+ return *this;
+ }
+
+ vec2 operator+=(vec2 a) {
+ x += a.x;
+ y += a.y;
+ return *this;
+ }
+ vec2 operator-=(vec2 a) {
+ x -= a.x;
+ y -= a.y;
+ return *this;
+ }
+ vec2 operator-=(Float a) {
+ x -= a;
+ y -= a;
+ return *this;
+ }
+
+ vec2 operator-() { return vec2(-x, -y); }
+
+ friend I32 operator==(const vec2& l, const vec2& r) {
+ return l.x == r.x && l.y == r.y;
+ }
+
+ friend I32 operator!=(const vec2& l, const vec2& r) {
+ return l.x != r.x || l.y != r.y;
+ }
+
+ friend vec2 operator*(vec2 a, Float b) { return vec2(a.x * b, a.y * b); }
+ friend vec2 operator*(vec2 a, vec2 b) { return vec2(a.x * b.x, a.y * b.y); }
+ friend vec2 operator*(Float a, vec2 b) { return vec2(a * b.x, a * b.y); }
+
+ friend vec2 operator/(vec2 a, vec2 b) { return vec2(a.x / b.x, a.y / b.y); }
+ friend vec2 operator/(vec2 a, Float b) { return vec2(a.x / b, a.y / b); }
+
+ friend vec2 operator-(vec2 a, vec2 b) { return vec2(a.x - b.x, a.y - b.y); }
+ friend vec2 operator-(vec2 a, Float b) { return vec2(a.x - b, a.y - b); }
+ friend vec2 operator-(Float a, vec2 b) { return vec2(a - b.x, a - b.y); }
+ friend vec2 operator+(vec2 a, vec2 b) { return vec2(a.x + b.x, a.y + b.y); }
+ friend vec2 operator+(vec2 a, Float b) { return vec2(a.x + b, a.y + b); }
+ friend vec2 operator+(Float a, vec2 b) { return vec2(a + b.x, a + b.y); }
+};
+
+vec2_scalar force_scalar(const vec2& v) {
+ return vec2_scalar{force_scalar(v.x), force_scalar(v.y)};
+}
+
+vec2_scalar make_vec2(float n) { return vec2_scalar{n, n}; }
+
+vec2_scalar make_vec2(float x, float y) { return vec2_scalar{x, y}; }
+
+vec2_scalar make_vec2(int32_t x, int32_t y) {
+ return vec2_scalar{float(x), float(y)};
+}
+
+template <typename N>
+vec2 make_vec2(const N& n) {
+ return vec2(n);
+}
+
+template <typename X, typename Y>
+vec2 make_vec2(const X& x, const Y& y) {
+ return vec2(x, y);
+}
+
+vec2 operator*(vec2_scalar a, Float b) { return vec2(a.x * b, a.y * b); }
+
+vec2 operator*(Float a, vec2_scalar b) { return vec2(a * b.x, a * b.y); }
+
+SI vec2 min(vec2 a, vec2 b) { return vec2(min(a.x, b.x), min(a.y, b.y)); }
+SI vec2 min(vec2 a, Float b) { return vec2(min(a.x, b), min(a.y, b)); }
+
+SI vec2_scalar min(vec2_scalar a, vec2_scalar b) {
+ return vec2_scalar{min(a.x, b.x), min(a.y, b.y)};
+}
+
+SI vec2 if_then_else(I32 c, vec2 t, vec2 e) {
+ return vec2(if_then_else(c, t.x, e.x), if_then_else(c, t.y, e.y));
+}
+
+SI vec2 if_then_else(int32_t c, vec2 t, vec2 e) { return c ? t : e; }
+
+vec2 step(vec2 edge, vec2 x) {
+ return vec2(step(edge.x, x.x), step(edge.y, x.y));
+}
+
+vec2_scalar step(vec2_scalar edge, vec2_scalar x) {
+ return vec2_scalar(step(edge.x, x.x), step(edge.y, x.y));
+}
+
+SI vec2 max(vec2 a, vec2 b) { return vec2(max(a.x, b.x), max(a.y, b.y)); }
+SI vec2 max(vec2 a, Float b) { return vec2(max(a.x, b), max(a.y, b)); }
+
+SI vec2_scalar max(vec2_scalar a, vec2_scalar b) {
+ return vec2_scalar{max(a.x, b.x), max(a.y, b.y)};
+}
+SI vec2_scalar max(vec2_scalar a, float b) {
+ return vec2_scalar{max(a.x, b), max(a.y, b)};
+}
+
+Float length(vec2 a) { return sqrt(a.x * a.x + a.y * a.y); }
+
+float length(vec2_scalar a) { return hypotf(a.x, a.y); }
+
+template <typename A, typename B>
+SI auto distance(A a, B b) {
+ return length(a - b);
+}
+
+template <typename T>
+SI T normalize(T a) {
+ return a / length(a);
+}
+
+SI vec2 sqrt(vec2 a) { return vec2(sqrt(a.x), sqrt(a.y)); }
+
+SI vec2_scalar sqrt(vec2_scalar a) { return vec2_scalar(sqrt(a.x), sqrt(a.y)); }
+
+SI vec2 recip(vec2 a) { return vec2(recip(a.x), recip(a.y)); }
+
+SI vec2_scalar recip(vec2_scalar a) {
+ return vec2_scalar(recip(a.x), recip(a.y));
+}
+
+SI vec2 inversesqrt(vec2 a) { return vec2(inversesqrt(a.x), inversesqrt(a.y)); }
+
+SI vec2_scalar inversesqrt(vec2_scalar a) {
+ return vec2_scalar(inversesqrt(a.x), inversesqrt(a.y));
+}
+
+#define abs __glsl_abs
+
+int32_t abs(int32_t a) { return a < 0 ? -a : a; }
+
+float abs(float a) { return fabsf(a); }
+
+Float abs(Float v) {
+#if USE_NEON
+ return vabsq_f32(v);
+#else
+ return bit_cast<Float>(bit_cast<I32>(v) & bit_cast<I32>(0.0f - v));
+#endif
+}
+
+float sign(float a) { return copysignf(1.0f, a); }
+
+Float sign(Float v) {
+ return bit_cast<Float>((bit_cast<I32>(v) & 0x80000000) |
+ bit_cast<I32>(Float(1.0f)));
+}
+
+Float cast(U32 v) { return CONVERT((I32)v, Float); }
+Float cast(I32 v) { return CONVERT((I32)v, Float); }
+I32 cast(Float v) { return CONVERT(v, I32); }
+
+#define floor __glsl_floor
+
+float floor(float a) { return floorf(a); }
+
+Float floor(Float v) {
+ Float roundtrip = cast(cast(v));
+ return roundtrip - if_then(roundtrip > v, Float(1));
+}
+
+vec2 floor(vec2 v) { return vec2(floor(v.x), floor(v.y)); }
+
+vec2_scalar floor(vec2_scalar v) {
+ return vec2_scalar{floorf(v.x), floorf(v.y)};
+}
+
+#define ceil __glsl_ceil
+
+float ceil(float a) { return ceilf(a); }
+
+Float ceil(Float v) {
+ Float roundtrip = cast(cast(v));
+ return roundtrip + if_then(roundtrip < v, Float(1));
+}
+
+// Round to nearest even
+SI int32_t roundeven(float v, float scale) {
+#if USE_SSE2
+ return _mm_cvtss_si32(_mm_set_ss(v * scale));
+#else
+ return bit_cast<int32_t>(v * scale + float(0xC00000)) - 0x4B400000;
+#endif
+}
+
+SI I32 roundeven(Float v, Float scale) {
+#if USE_SSE2
+ return _mm_cvtps_epi32(v * scale);
+#else
+ // Magic number implementation of round-to-nearest-even
+ // see http://stereopsis.com/sree/fpu2006.html
+ return bit_cast<I32>(v * scale + Float(0xC00000)) - 0x4B400000;
+#endif
+}
+
+// Round towards zero
+SI int32_t roundzero(float v, float scale) { return int32_t(v * scale); }
+
+SI I32 roundzero(Float v, Float scale) { return cast(v * scale); }
+
+// Round whichever direction is fastest for positive numbers
+SI I32 roundfast(Float v, Float scale) {
+#if USE_SSE2
+ return _mm_cvtps_epi32(v * scale);
+#else
+ return cast(v * scale + 0.5f);
+#endif
+}
+
+template <typename T>
+SI auto round_pixel(T v, float scale = 255.0f) {
+ return roundfast(v, scale);
+}
+
+#define round __glsl_round
+
+float round(float a) { return roundf(a); }
+
+Float round(Float v) { return floor(v + 0.5f); }
+
+float fract(float a) { return a - floor(a); }
+
+Float fract(Float v) { return v - floor(v); }
+
+vec2 fract(vec2 v) { return vec2(fract(v.x), fract(v.y)); }
+
+// X derivatives can be approximated by dFdx(x) = x[1] - x[0].
+// Y derivatives are not easily available since we operate in terms of X spans
+// only. To work around, assume dFdy(p.x) = dFdx(p.y), which only holds for
+// uniform scaling, and thus abs(dFdx(p.x)) + abs(dFdy(p.x)) = abs(dFdx(p.x)) +
+// abs(dFdx(p.y)) which mirrors abs(dFdx(p.y)) + abs(dFdy(p.y)) = abs(dFdx(p.y))
+// + abs(dFdx(p.x)).
+vec2_scalar fwidth(vec2 p) {
+ Float d = abs(SHUFFLE(p.x, p.y, 1, 1, 5, 5) - SHUFFLE(p.x, p.y, 0, 0, 4, 4));
+ return vec2_scalar(d.x + d.z);
+}
+
+float dFdx(Float x) { return x.y - x.x; }
+
+vec2_scalar dFdx(vec2 p) { return vec2_scalar(dFdx(p.x), dFdx(p.y)); }
+
+// See
+// http://www.machinedlearnings.com/2011/06/fast-approximate-logarithm-exponential.html.
+Float approx_log2(Float x) {
+ // e - 127 is a fair approximation of log2(x) in its own right...
+ Float e = cast(bit_cast<U32>(x)) * (1.0f / (1 << 23));
+
+ // ... but using the mantissa to refine its error is _much_ better.
+ Float m = bit_cast<Float>((bit_cast<U32>(x) & 0x007fffff) | 0x3f000000);
+ return e - 124.225514990f - 1.498030302f * m -
+ 1.725879990f / (0.3520887068f + m);
+}
+
+Float approx_pow2(Float x) {
+ Float f = fract(x);
+ return bit_cast<Float>(
+ roundfast(1.0f * (1 << 23), x + 121.274057500f - 1.490129070f * f +
+ 27.728023300f / (4.84252568f - f)));
+}
+
+#define pow __glsl_pow
+
+SI float pow(float x, float y) { return powf(x, y); }
+
+Float pow(Float x, Float y) {
+ return if_then_else((x == 0) | (x == 1), x, approx_pow2(approx_log2(x) * y));
+}
+
+#define exp __glsl_exp
+
+SI float exp(float x) { return expf(x); }
+
+Float exp(Float y) {
+ float l2e = 1.4426950408889634074f;
+ return approx_pow2(l2e * y);
+}
+
+#define exp2 __glsl_exp2
+
+SI float exp2(float x) { return exp2f(x); }
+
+Float exp2(Float x) { return approx_pow2(x); }
+
+#define log __glsl_log
+
+SI float log(float x) { return logf(x); }
+
+Float log(Float x) { return approx_log2(x) * 0.69314718f; }
+
+#define log2 __glsl_log2
+
+SI float log2(float x) { return log2f(x); }
+
+Float log2(Float x) { return approx_log2(x); }
+
+struct ivec4;
+
+struct ivec2_scalar {
+ typedef int32_t element_type;
+
+ int32_t x;
+ int32_t y;
+
+ ivec2_scalar() : ivec2_scalar(0) {}
+ IMPLICIT constexpr ivec2_scalar(int32_t a) : x(a), y(a) {}
+ constexpr ivec2_scalar(int32_t x, int32_t y) : x(x), y(y) {}
+
+ int32_t& select(XYZW c) {
+ switch (c) {
+ case X:
+ return x;
+ case Y:
+ return y;
+ default:
+ UNREACHABLE;
+ }
+ }
+ int32_t& sel(XYZW c1) { return select(c1); }
+ ivec2_scalar sel(XYZW c1, XYZW c2) {
+ return ivec2_scalar{select(c1), select(c2)};
+ }
+
+ ivec2_scalar operator-() const { return ivec2_scalar{-x, -y}; }
+
+ ivec2_scalar& operator+=(ivec2_scalar a) {
+ x += a.x;
+ y += a.y;
+ return *this;
+ }
+ ivec2_scalar& operator+=(int n) {
+ x += n;
+ y += n;
+ return *this;
+ }
+
+ ivec2_scalar& operator>>=(int shift) {
+ x >>= shift;
+ y >>= shift;
+ return *this;
+ }
+
+ friend ivec2_scalar operator&(ivec2_scalar a, int b) {
+ return ivec2_scalar{a.x & b, a.y & b};
+ }
+
+ friend ivec2_scalar operator+(ivec2_scalar a, ivec2_scalar b) {
+ return ivec2_scalar{a.x + b.x, a.y + b.y};
+ }
+ friend ivec2_scalar operator+(ivec2_scalar a, int b) {
+ return ivec2_scalar{a.x + b, a.y + b};
+ }
+
+ friend ivec2_scalar operator-(ivec2_scalar a, ivec2_scalar b) {
+ return ivec2_scalar{a.x - b.x, a.y - b.y};
+ }
+ friend ivec2_scalar operator-(ivec2_scalar a, int b) {
+ return ivec2_scalar{a.x - b, a.y - b};
+ }
+
+ friend bool operator==(const ivec2_scalar& l, const ivec2_scalar& r) {
+ return l.x == r.x && l.y == r.y;
+ }
+};
+
+struct ivec2 {
+ typedef int32_t element_type;
+
+ ivec2() : ivec2(I32(0)) {}
+ IMPLICIT ivec2(I32 a) : x(a), y(a) {}
+ ivec2(I32 x, I32 y) : x(x), y(y) {}
+ IMPLICIT ivec2(vec2 a) : x(cast(a.x)), y(cast(a.y)) {}
+ ivec2(U32 x, U32 y) : x(CONVERT(x, I32)), y(CONVERT(y, I32)) {}
+ IMPLICIT constexpr ivec2(ivec2_scalar s) : x(s.x), y(s.y) {}
+ constexpr ivec2(ivec2_scalar s0, ivec2_scalar s1, ivec2_scalar s2,
+ ivec2_scalar s3)
+ : x(I32{s0.x, s1.x, s2.x, s3.x}), y(I32{s0.y, s1.y, s2.y, s3.y}) {}
+ I32 x;
+ I32 y;
+
+ I32& select(XYZW c) {
+ switch (c) {
+ case X:
+ return x;
+ case Y:
+ return y;
+ default:
+ UNREACHABLE;
+ }
+ }
+ I32& sel(XYZW c1) { return select(c1); }
+
+ ivec2 sel(XYZW c1, XYZW c2) { return ivec2(select(c1), select(c2)); }
+
+ ivec4 sel(XYZW c1, XYZW c2, XYZW c3, XYZW c4);
+
+ ivec2& operator*=(I32 a) {
+ x *= a;
+ y *= a;
+ return *this;
+ }
+ ivec2& operator+=(ivec2 a) {
+ x += a.x;
+ y += a.y;
+ return *this;
+ }
+ ivec2& operator>>=(int shift) {
+ x >>= shift;
+ y >>= shift;
+ return *this;
+ }
+
+ friend ivec2 operator*(ivec2 a, I32 b) { return ivec2(a.x * b, a.y * b); }
+ friend ivec2 operator&(ivec2 a, ivec2 b) {
+ return ivec2(a.x & b.x, a.y & b.y);
+ }
+ friend ivec2 operator&(ivec2 a, I32 b) { return ivec2(a.x & b, a.y & b); }
+ friend ivec2 operator+(ivec2 a, ivec2 b) {
+ return ivec2(a.x + b.x, a.y + b.y);
+ }
+};
+
+vec2::vec2(ivec2 a) : x(cast(a.x)), y(cast(a.y)) {}
+
+ivec2_scalar make_ivec2(int32_t n) { return ivec2_scalar{n, n}; }
+
+ivec2_scalar make_ivec2(uint32_t n) {
+ return ivec2_scalar{int32_t(n), int32_t(n)};
+}
+
+ivec2_scalar make_ivec2(int32_t x, int32_t y) { return ivec2_scalar{x, y}; }
+
+ivec2_scalar make_ivec2(uint32_t x, uint32_t y) {
+ return ivec2_scalar{int32_t(x), int32_t(y)};
+}
+
+vec2_scalar make_vec2(const ivec2_scalar& v) {
+ return vec2_scalar{float(v.x), float(v.y)};
+}
+
+ivec2_scalar make_ivec2(const vec2_scalar& v) {
+ return ivec2_scalar{int32_t(v.x), int32_t(v.y)};
+}
+
+template <typename N>
+ivec2 make_ivec2(const N& n) {
+ return ivec2(n);
+}
+
+template <typename X, typename Y>
+ivec2 make_ivec2(const X& x, const Y& y) {
+ return ivec2(x, y);
+}
+
+ivec2_scalar force_scalar(const ivec2& v) {
+ return ivec2_scalar{force_scalar(v.x), force_scalar(v.y)};
+}
+
+struct ivec3_scalar {
+ int32_t x;
+ int32_t y;
+ int32_t z;
+
+ ivec3_scalar() : ivec3_scalar(0) {}
+ IMPLICIT constexpr ivec3_scalar(int32_t a) : x(a), y(a), z(a) {}
+ constexpr ivec3_scalar(int32_t x, int32_t y, int32_t z) : x(x), y(y), z(z) {}
+
+ int32_t& select(XYZW c) {
+ switch (c) {
+ case X:
+ return x;
+ case Y:
+ return y;
+ case Z:
+ return z;
+ default:
+ UNREACHABLE;
+ }
+ }
+ int32_t& sel(XYZW c1) { return select(c1); }
+ ivec2_scalar sel(XYZW c1, XYZW c2) {
+ return ivec2_scalar{select(c1), select(c2)};
+ }
+};
+
+struct ivec3 {
+ ivec3() : ivec3(0) {}
+ IMPLICIT ivec3(I32 a) : x(a), y(a), z(a) {}
+ ivec3(I32 x, I32 y, I32 z) : x(x), y(y), z(z) {}
+ ivec3(ivec2 a, I32 b) : x(a.x), y(a.y), z(b) {}
+ ivec3(vec2 a, Float b) : x(cast(a.x)), y(cast(a.y)), z(cast(b)) {}
+ I32 x;
+ I32 y;
+ I32 z;
+
+ friend ivec3 operator+(ivec3 a, ivec3 b) {
+ return ivec3(a.x + b.x, a.y + b.y, a.z + b.z);
+ }
+};
+
+vec2_scalar make_vec2(ivec3_scalar s) {
+ return vec2_scalar{float(s.x), float(s.y)};
+}
+
+ivec3_scalar make_ivec3(int32_t n) { return ivec3_scalar{n, n, n}; }
+
+ivec3_scalar make_ivec3(const ivec2_scalar& v, int32_t z) {
+ return ivec3_scalar{v.x, v.y, z};
+}
+
+ivec3_scalar make_ivec3(int32_t x, int32_t y, int32_t z) {
+ return ivec3_scalar{x, y, z};
+}
+
+template <typename N>
+ivec3 make_ivec3(const N& n) {
+ return ivec3(n);
+}
+
+template <typename X, typename Y>
+ivec3 make_ivec3(const X& x, const Y& y) {
+ return ivec3(x, y);
+}
+
+template <typename X, typename Y, typename Z>
+ivec3 make_ivec3(const X& x, const Y& y, const Z& z) {
+ return ivec3(x, y, z);
+}
+
+struct ivec4_scalar {
+ typedef int32_t element_type;
+
+ int32_t x;
+ int32_t y;
+ int32_t z;
+ int32_t w;
+
+ ivec4_scalar() : ivec4_scalar(0) {}
+ IMPLICIT constexpr ivec4_scalar(int32_t a) : x(a), y(a), z(a), w(a) {}
+ constexpr ivec4_scalar(int32_t x, int32_t y, int32_t z, int32_t w)
+ : x(x), y(y), z(z), w(w) {}
+
+ int32_t& select(XYZW c) {
+ switch (c) {
+ case X:
+ return x;
+ case Y:
+ return y;
+ case Z:
+ return z;
+ case W:
+ return w;
+ default:
+ UNREACHABLE;
+ }
+ }
+ int32_t& sel(XYZW c1) { return select(c1); }
+ ivec2_scalar sel(XYZW c1, XYZW c2) {
+ return ivec2_scalar{select(c1), select(c2)};
+ }
+
+ friend ivec4_scalar operator&(int32_t a, ivec4_scalar b) {
+ return ivec4_scalar{a & b.x, a & b.y, a & b.z, a & b.w};
+ }
+ friend ivec4_scalar operator<<(ivec4_scalar a, int32_t b) {
+ return ivec4_scalar{a.x << b, a.y << b, a.z << b, a.w << b};
+ }
+
+ int32_t& operator[](int index) {
+ switch (index) {
+ case 0:
+ return x;
+ case 1:
+ return y;
+ case 2:
+ return z;
+ case 3:
+ return w;
+ default:
+ UNREACHABLE;
+ }
+ }
+};
+
+struct ivec4 {
+ typedef int32_t element_type;
+
+ ivec4() : ivec4(I32(0)) {}
+ IMPLICIT ivec4(I32 a) : x(a), y(a), z(a), w(a) {}
+ ivec4(I32 x, I32 y, I32 z, I32 w) : x(x), y(y), z(z), w(w) {}
+ ivec4(ivec2 a, I32 b, I32 c) : x(a.x), y(a.y), z(b), w(c) {}
+ IMPLICIT constexpr ivec4(ivec4_scalar s) : x(s.x), y(s.y), z(s.z), w(s.w) {}
+ constexpr ivec4(ivec4_scalar s0, ivec4_scalar s1, ivec4_scalar s2,
+ ivec4_scalar s3)
+ : x(I32{s0.x, s1.x, s2.x, s3.x}),
+ y(I32{s0.y, s1.y, s2.y, s3.y}),
+ z(I32{s0.z, s1.z, s2.z, s3.z}),
+ w(I32{s0.w, s1.w, s2.w, s3.w}) {}
+
+ I32& select(XYZW c) {
+ switch (c) {
+ case X:
+ return x;
+ case Y:
+ return y;
+ case Z:
+ return z;
+ case W:
+ return w;
+ default:
+ UNREACHABLE;
+ }
+ }
+ I32 sel(XYZW c1) { return select(c1); }
+
+ ivec2 sel(XYZW c1, XYZW c2) { return ivec2(select(c1), select(c2)); }
+
+ ivec3 sel(XYZW c1, XYZW c2, XYZW c3) {
+ return ivec3(select(c1), select(c2), select(c3));
+ }
+
+ friend ivec4 operator&(I32 a, ivec4 b) {
+ return ivec4(a & b.x, a & b.y, a & b.z, a & b.w);
+ }
+
+ I32 x;
+ I32 y;
+ I32 z;
+ I32 w;
+};
+
+ivec4_scalar force_scalar(const ivec4& v) {
+ return ivec4_scalar{force_scalar(v.x), force_scalar(v.y), force_scalar(v.z),
+ force_scalar(v.w)};
+}
+
+ivec4_scalar make_ivec4(int32_t n) { return ivec4_scalar{n, n, n, n}; }
+
+ivec4_scalar make_ivec4(const ivec2_scalar& xy, int32_t z, int32_t w) {
+ return ivec4_scalar{xy.x, xy.y, z, w};
+}
+
+ivec4_scalar make_ivec4(int32_t x, int32_t y, int32_t z, int32_t w) {
+ return ivec4_scalar{x, y, z, w};
+}
+
+template <typename N>
+ivec4 make_ivec4(const N& n) {
+ return ivec4(n);
+}
+
+template <typename X, typename Y, typename Z>
+ivec4 make_ivec4(const X& x, const Y& y, const Z& z) {
+ return ivec4(x, y, z);
+}
+
+template <typename X, typename Y, typename Z, typename W>
+ivec4 make_ivec4(const X& x, const Y& y, const Z& z, const W& w) {
+ return ivec4(x, y, z, w);
+}
+
+SI ivec2 if_then_else(I32 c, ivec2 t, ivec2 e) {
+ return ivec2(if_then_else(c, t.x, e.x), if_then_else(c, t.y, e.y));
+}
+
+SI ivec2 if_then_else(int32_t c, ivec2 t, ivec2 e) { return c ? t : e; }
+
+SI ivec4 if_then_else(I32 c, ivec4 t, ivec4 e) {
+ return ivec4(if_then_else(c, t.x, e.x), if_then_else(c, t.y, e.y),
+ if_then_else(c, t.z, e.z), if_then_else(c, t.w, e.w));
+}
+
+SI ivec4 if_then_else(int32_t c, ivec4 t, ivec4 e) { return c ? t : e; }
+
+ivec4 operator&(I32 a, ivec4_scalar b) {
+ return ivec4(a & b.x, a & b.y, a & b.z, a & b.w);
+}
+
+struct bvec3_scalar {
+ bool x;
+ bool y;
+ bool z;
+
+ bvec3_scalar() : bvec3_scalar(false) {}
+ IMPLICIT constexpr bvec3_scalar(bool a) : x(a), y(a), z(a) {}
+ constexpr bvec3_scalar(bool x, bool y, bool z) : x(x), y(y), z(z) {}
+};
+
+struct bvec3_scalar1 {
+ bool x;
+
+ IMPLICIT constexpr bvec3_scalar1(bool a) : x(a) {}
+
+ operator bvec3_scalar() const { return bvec3_scalar(x); }
+};
+
+struct bvec3 {
+ bvec3() : bvec3(0) {}
+ IMPLICIT bvec3(Bool a) : x(a), y(a), z(a) {}
+ bvec3(Bool x, Bool y, Bool z) : x(x), y(y), z(z) {}
+ Bool& select(XYZW c) {
+ switch (c) {
+ case X:
+ return x;
+ case Y:
+ return y;
+ case Z:
+ return z;
+ default:
+ UNREACHABLE;
+ }
+ }
+ Bool sel(XYZW c1) { return select(c1); }
+
+ Bool x;
+ Bool y;
+ Bool z;
+};
+
+bvec3_scalar1 make_bvec3(bool n) { return bvec3_scalar1(n); }
+
+struct bvec4_scalar {
+ bool x;
+ bool y;
+ bool z;
+ bool w;
+
+ bvec4_scalar() : bvec4_scalar(false) {}
+ IMPLICIT constexpr bvec4_scalar(bool a) : x(a), y(a), z(a), w(a) {}
+ constexpr bvec4_scalar(bool x, bool y, bool z, bool w)
+ : x(x), y(y), z(z), w(w) {}
+
+ bool& select(XYZW c) {
+ switch (c) {
+ case X:
+ return x;
+ case Y:
+ return y;
+ case Z:
+ return z;
+ case W:
+ return w;
+ default:
+ UNREACHABLE;
+ }
+ }
+ bool sel(XYZW c1) { return select(c1); }
+ bvec2_scalar sel(XYZW c1, XYZW c2) {
+ return bvec2_scalar(select(c1), select(c2));
+ }
+};
+
+bvec4_scalar bvec2_scalar::sel(XYZW c1, XYZW c2, XYZW c3, XYZW c4) {
+ return bvec4_scalar{select(c1), select(c2), select(c3), select(c4)};
+}
+
+struct bvec4_scalar1 {
+ bool x;
+
+ IMPLICIT constexpr bvec4_scalar1(bool a) : x(a) {}
+
+ operator bvec4_scalar() const { return bvec4_scalar(x); }
+};
+
+struct bvec4 {
+ bvec4() : bvec4(0) {}
+ IMPLICIT bvec4(Bool a) : x(a), y(a), z(a), w(a) {}
+ bvec4(Bool x, Bool y, Bool z, Bool w) : x(x), y(y), z(z), w(w) {}
+ bvec4(bvec2 x, bvec2 y) : x(x.x), y(x.y), z(y.x), w(y.y) {}
+ Bool& select(XYZW c) {
+ switch (c) {
+ case X:
+ return x;
+ case Y:
+ return y;
+ case Z:
+ return z;
+ case W:
+ return w;
+ default:
+ UNREACHABLE;
+ }
+ }
+ Bool sel(XYZW c1) { return select(c1); }
+
+ Bool x;
+ Bool y;
+ Bool z;
+ Bool w;
+};
+
+bvec4_scalar1 make_bvec4(bool n) { return bvec4_scalar1(n); }
+
+bvec4_scalar make_bvec4(bool x, bool y, bool z, bool w) {
+ return bvec4_scalar{x, y, z, w};
+}
+
+bvec4_scalar make_bvec4(bvec2_scalar a, bvec2_scalar b) {
+ return bvec4_scalar{a.x, a.y, b.x, b.y};
+}
+
+template <typename N>
+bvec4 make_bvec4(const N& n) {
+ return bvec4(n);
+}
+
+template <typename X, typename Y>
+bvec4 make_bvec4(const X& x, const Y& y) {
+ return bvec4(x, y);
+}
+
+template <typename X, typename Y, typename Z, typename W>
+bvec4 make_bvec4(const X& x, const Y& y, const Z& z, const W& w) {
+ return bvec4(x, y, z, w);
+}
+
+struct vec2_ref {
+ vec2_ref(Float& x, Float& y) : x(x), y(y) {}
+ Float& x;
+ Float& y;
+
+ Float& select(XYZW c) {
+ switch (c) {
+ case X:
+ return x;
+ case Y:
+ return y;
+ default:
+ UNREACHABLE;
+ }
+ }
+ Float& sel(XYZW c1) { return select(c1); }
+
+ vec2_ref& operator=(const vec2& a) {
+ x = a.x;
+ y = a.y;
+ return *this;
+ }
+
+ vec2_ref& operator/=(Float a) {
+ x /= a;
+ y /= a;
+ return *this;
+ }
+
+ vec2_ref& operator/=(vec2 a) {
+ x /= a.x;
+ y /= a.y;
+ return *this;
+ }
+
+ vec2_ref& operator+=(vec2 a) {
+ x += a.x;
+ y += a.y;
+ return *this;
+ }
+ vec2_ref& operator-=(vec2 a) {
+ x -= a.x;
+ y -= a.y;
+ return *this;
+ }
+ vec2_ref& operator*=(vec2 a) {
+ x *= a.x;
+ y *= a.y;
+ return *this;
+ }
+};
+
+struct vec3_scalar {
+ typedef struct vec3 vector_type;
+ typedef float element_type;
+
+ float x;
+ float y;
+ float z;
+
+ constexpr vec3_scalar() : vec3_scalar(0.0f) {}
+ IMPLICIT constexpr vec3_scalar(float a) : x(a), y(a), z(a) {}
+ constexpr vec3_scalar(float x, float y, float z) : x(x), y(y), z(z) {}
+
+ float& select(XYZW c) {
+ switch (c) {
+ case X:
+ return x;
+ case Y:
+ return y;
+ case Z:
+ return z;
+ default:
+ UNREACHABLE;
+ }
+ }
+ float& sel(XYZW c1) { return select(c1); }
+ vec2_scalar sel(XYZW c1, XYZW c2) {
+ return vec2_scalar(select(c1), select(c2));
+ }
+ vec3_scalar sel(XYZW c1, XYZW c2, XYZW c3) {
+ return vec3_scalar(select(c1), select(c2), select(c3));
+ }
+ vec2_scalar_ref lsel(XYZW c1, XYZW c2) {
+ return vec2_scalar_ref(select(c1), select(c2));
+ }
+
+ friend vec3_scalar operator*(vec3_scalar a, vec3_scalar b) {
+ return vec3_scalar{a.x * b.x, a.y * b.y, a.z * b.z};
+ }
+ friend vec3_scalar operator*(vec3_scalar a, float b) {
+ return vec3_scalar{a.x * b, a.y * b, a.z * b};
+ }
+
+ friend vec3_scalar operator-(vec3_scalar a, vec3_scalar b) {
+ return vec3_scalar{a.x - b.x, a.y - b.y, a.z - b.z};
+ }
+ friend vec3_scalar operator-(vec3_scalar a, float b) {
+ return vec3_scalar{a.x - b, a.y - b, a.z - b};
+ }
+ friend vec3_scalar operator+(vec3_scalar a, vec3_scalar b) {
+ return vec3_scalar{a.x + b.x, a.y + b.y, a.z + b.z};
+ }
+ friend vec3_scalar operator+(vec3_scalar a, float b) {
+ return vec3_scalar{a.x + b, a.y + b, a.z + b};
+ }
+
+ friend vec3_scalar operator/(vec3_scalar a, vec3_scalar b) {
+ return vec3_scalar{a.x / b.x, a.y / b.y, a.z / b.z};
+ }
+ friend vec3_scalar operator/(vec3_scalar a, float b) {
+ return vec3_scalar{a.x / b, a.y / b, a.z / b};
+ }
+
+ vec3_scalar operator+=(vec3_scalar a) {
+ x += a.x;
+ y += a.y;
+ z += a.z;
+ return *this;
+ }
+
+ friend bool operator==(const vec3_scalar& l, const vec3_scalar& r) {
+ return l.x == r.x && l.y == r.y && l.z == r.z;
+ }
+};
+
+struct vec3_scalar_ref {
+ vec3_scalar_ref(float& x, float& y, float& z) : x(x), y(y), z(z) {}
+ float& x;
+ float& y;
+ float& z;
+
+ float& select(XYZW c) {
+ switch (c) {
+ case X:
+ return x;
+ case Y:
+ return y;
+ case Z:
+ return z;
+ default:
+ UNREACHABLE;
+ }
+ }
+ float& sel(XYZW c1) { return select(c1); }
+
+ vec3_scalar_ref& operator=(const vec3_scalar& a) {
+ x = a.x;
+ y = a.y;
+ z = a.z;
+ return *this;
+ }
+
+ operator vec3_scalar() const { return vec3_scalar{x, y, z}; }
+};
+
+struct vec3 {
+ typedef struct vec3 vector_type;
+ typedef float element_type;
+
+ constexpr vec3() : vec3(Float(0.0f)) {}
+ IMPLICIT constexpr vec3(Float a) : x(a), y(a), z(a) {}
+ constexpr vec3(Float x, Float y, Float z) : x(x), y(y), z(z) {}
+ vec3(vec2 a, Float z) : x(a.x), y(a.y), z(z) {}
+ explicit vec3(vec4);
+ IMPLICIT constexpr vec3(vec3_scalar s) : x(s.x), y(s.y), z(s.z) {}
+ constexpr vec3(vec3_scalar s0, vec3_scalar s1, vec3_scalar s2, vec3_scalar s3)
+ : x(Float{s0.x, s1.x, s2.x, s3.x}),
+ y(Float{s0.y, s1.y, s2.y, s3.y}),
+ z(Float{s0.z, s1.z, s2.z, s3.z}) {}
+ Float x;
+ Float y;
+ Float z;
+
+ Float& select(XYZW c) {
+ switch (c) {
+ case X:
+ return x;
+ case Y:
+ return y;
+ case Z:
+ return z;
+ default:
+ UNREACHABLE;
+ }
+ }
+ Float& sel(XYZW c1) { return select(c1); }
+
+ vec2 sel(XYZW c1, XYZW c2) { return vec2(select(c1), select(c2)); }
+
+ vec3 sel(XYZW c1, XYZW c2, XYZW c3) {
+ return vec3(select(c1), select(c2), select(c3));
+ }
+
+ vec4 sel(XYZW c1, XYZW c2, XYZW c3, XYZW c4);
+
+ vec2_ref lsel(XYZW c1, XYZW c2) { return vec2_ref(select(c1), select(c2)); }
+
+ friend vec3 operator*(vec3 a, Float b) {
+ return vec3(a.x * b, a.y * b, a.z * b);
+ }
+ friend vec3 operator*(vec3 a, vec3 b) {
+ return vec3(a.x * b.x, a.y * b.y, a.z * b.z);
+ }
+ friend vec3 operator*(Float a, vec3 b) {
+ return vec3(a * b.x, a * b.y, a * b.z);
+ }
+
+ friend vec3 operator/(vec3 a, Float b) {
+ return vec3(a.x / b, a.y / b, a.z / b);
+ }
+ friend vec3 operator/(vec3 a, vec3 b) {
+ return vec3(a.x / b.x, a.y / b.y, a.z / b.z);
+ }
+
+ friend I32 operator==(const vec3& l, const vec3& r) {
+ return l.x == r.x && l.y == r.y && l.z == r.z;
+ }
+
+ friend vec3 operator-(vec3 a, Float b) {
+ return vec3(a.x - b, a.y - b, a.z - b);
+ }
+ friend vec3 operator-(vec3 a, vec3 b) {
+ return vec3(a.x - b.x, a.y - b.y, a.z - b.z);
+ }
+ friend vec3 operator+(vec3 a, Float b) {
+ return vec3(a.x + b, a.y + b, a.z + b);
+ }
+ friend vec3 operator+(vec3 a, vec3 b) {
+ return vec3(a.x + b.x, a.y + b.y, a.z + b.z);
+ }
+
+ vec3 operator+=(vec3_scalar a) {
+ x += a.x;
+ y += a.y;
+ z += a.z;
+ return *this;
+ }
+ vec3& operator+=(vec3 a) {
+ x += a.x;
+ y += a.y;
+ z += a.z;
+ return *this;
+ }
+};
+
+vec3_scalar force_scalar(const vec3& v) {
+ return vec3_scalar{force_scalar(v.x), force_scalar(v.y), force_scalar(v.z)};
+}
+
+vec3_scalar make_vec3(float n) { return vec3_scalar{n, n, n}; }
+
+vec3_scalar make_vec3(const vec2_scalar& v, float z) {
+ return vec3_scalar{v.x, v.y, z};
+}
+
+vec3_scalar make_vec3(float x, float y, float z) {
+ return vec3_scalar{x, y, z};
+}
+
+vec3_scalar make_vec3(int32_t x, int32_t y, float z) {
+ return vec3_scalar{float(x), float(y), z};
+}
+
+template <typename N>
+vec3 make_vec3(const N& n) {
+ return vec3(n);
+}
+
+template <typename X, typename Y>
+vec3 make_vec3(const X& x, const Y& y) {
+ return vec3(x, y);
+}
+
+template <typename X, typename Y, typename Z>
+vec3 make_vec3(const X& x, const Y& y, const Z& z) {
+ return vec3(x, y, z);
+}
+
+SI vec3 if_then_else(I32 c, vec3 t, vec3 e) {
+ return vec3(if_then_else(c, t.x, e.x), if_then_else(c, t.y, e.y),
+ if_then_else(c, t.z, e.z));
+}
+
+SI vec3 if_then_else(int32_t c, vec3 t, vec3 e) { return c ? t : e; }
+
+SI vec3 if_then_else(ivec3 c, vec3 t, vec3 e) {
+ return vec3(if_then_else(c.x, t.x, e.x), if_then_else(c.y, t.y, e.y),
+ if_then_else(c.z, t.z, e.z));
+}
+
+vec3 step(vec3 edge, vec3 x) {
+ return vec3(step(edge.x, x.x), step(edge.y, x.y), step(edge.z, x.z));
+}
+
+vec3_scalar step(vec3_scalar edge, vec3_scalar x) {
+ return vec3_scalar(step(edge.x, x.x), step(edge.y, x.y), step(edge.z, x.z));
+}
+
+SI vec3 min(vec3 a, vec3 b) {
+ return vec3(min(a.x, b.x), min(a.y, b.y), min(a.z, b.z));
+}
+SI vec3 min(vec3 a, Float b) {
+ return vec3(min(a.x, b), min(a.y, b), min(a.z, b));
+}
+SI vec3_scalar min(vec3_scalar a, vec3_scalar b) {
+ return vec3_scalar{min(a.x, b.x), min(a.y, b.y), min(a.z, b.z)};
+}
+
+SI vec3 max(vec3 a, vec3 b) {
+ return vec3(max(a.x, b.x), max(a.y, b.y), max(a.z, b.z));
+}
+SI vec3 max(vec3 a, Float b) {
+ return vec3(max(a.x, b), max(a.y, b), max(a.z, b));
+}
+SI vec3_scalar max(vec3_scalar a, vec3_scalar b) {
+ return vec3_scalar{max(a.x, b.x), max(a.y, b.y), max(a.z, b.z)};
+}
+
+vec3 pow(vec3 x, vec3 y) {
+ return vec3(pow(x.x, y.x), pow(x.y, y.y), pow(x.z, y.z));
+}
+
+struct vec3_ref {
+ vec3_ref(Float& x, Float& y, Float& z) : x(x), y(y), z(z) {}
+ Float& x;
+ Float& y;
+ Float& z;
+ vec3_ref& operator=(const vec3& a) {
+ x = a.x;
+ y = a.y;
+ z = a.z;
+ return *this;
+ }
+
+ vec3_ref& operator/=(Float a) {
+ x /= a;
+ y /= a;
+ z /= a;
+ return *this;
+ }
+
+ vec3_ref& operator*=(Float a) {
+ x *= a;
+ y *= a;
+ z *= a;
+ return *this;
+ }
+};
+
+struct vec4_scalar {
+ typedef struct vec4 vector_type;
+ typedef float element_type;
+
+ float x;
+ float y;
+ float z;
+ float w;
+
+ constexpr vec4_scalar() : vec4_scalar(0.0f) {}
+ IMPLICIT constexpr vec4_scalar(float a) : x(a), y(a), z(a), w(a) {}
+ constexpr vec4_scalar(float x, float y, float z, float w)
+ : x(x), y(y), z(z), w(w) {}
+ vec4_scalar(vec3_scalar xyz, float w) : x(xyz.x), y(xyz.y), z(xyz.z), w(w) {}
+
+ static vec4_scalar load_from_ptr(const float* f) {
+ return vec4_scalar(f[0], f[1], f[2], f[3]);
+ }
+
+ ALWAYS_INLINE float& select(XYZW c) {
+ switch (c) {
+ case X:
+ return x;
+ case Y:
+ return y;
+ case Z:
+ return z;
+ case W:
+ return w;
+ default:
+ UNREACHABLE;
+ }
+ }
+ float& sel(XYZW c1) { return select(c1); }
+ vec2_scalar sel(XYZW c1, XYZW c2) {
+ return vec2_scalar{select(c1), select(c2)};
+ }
+ vec3_scalar sel(XYZW c1, XYZW c2, XYZW c3) {
+ return vec3_scalar{select(c1), select(c2), select(c3)};
+ }
+ vec4_scalar sel(XYZW c1, XYZW c2, XYZW c3, XYZW c4) {
+ return vec4_scalar{select(c1), select(c2), select(c3), select(c4)};
+ }
+ vec2_scalar_ref lsel(XYZW c1, XYZW c2) {
+ return vec2_scalar_ref(select(c1), select(c2));
+ }
+ vec3_scalar_ref lsel(XYZW c1, XYZW c2, XYZW c3) {
+ return vec3_scalar_ref(select(c1), select(c2), select(c3));
+ }
+
+ friend vec4_scalar operator*(vec4_scalar a, vec4_scalar b) {
+ return vec4_scalar{a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w};
+ }
+ friend vec4_scalar operator*(vec4_scalar a, float b) {
+ return vec4_scalar{a.x * b, a.y * b, a.z * b, a.w * b};
+ }
+ friend vec4_scalar operator*(float a, vec4_scalar b) {
+ return vec4_scalar{a * b.x, a * b.y, a * b.z, a * b.w};
+ }
+ vec4_scalar& operator*=(float a) {
+ x *= a;
+ y *= a;
+ z *= a;
+ w *= a;
+ return *this;
+ }
+
+ friend vec4_scalar operator-(vec4_scalar a, vec4_scalar b) {
+ return vec4_scalar{a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w};
+ }
+ friend vec4_scalar operator-(vec4_scalar a, float b) {
+ return vec4_scalar{a.x - b, a.y - b, a.z - b, a.w - b};
+ }
+ friend vec4_scalar operator+(vec4_scalar a, vec4_scalar b) {
+ return vec4_scalar{a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w};
+ }
+ friend vec4_scalar operator+(vec4_scalar a, float b) {
+ return vec4_scalar{a.x + b, a.y + b, a.z + b, a.w + b};
+ }
+
+ friend vec4_scalar operator/(vec4_scalar a, vec4_scalar b) {
+ return vec4_scalar{a.x / b.x, a.y / b.y, a.z / b.z, a.w / b.w};
+ }
+ friend vec4_scalar operator/(vec4_scalar a, float b) {
+ return vec4_scalar{a.x / b, a.y / b, a.z / b, a.w / b};
+ }
+
+ vec4_scalar& operator+=(vec4_scalar a) {
+ x += a.x;
+ y += a.y;
+ z += a.z;
+ w += a.w;
+ return *this;
+ }
+
+ vec4_scalar& operator/=(vec4_scalar a) {
+ x /= a.x;
+ y /= a.y;
+ z /= a.z;
+ w /= a.w;
+ return *this;
+ }
+
+ vec4_scalar& operator*=(vec4_scalar a) {
+ x *= a.x;
+ y *= a.y;
+ z *= a.z;
+ w *= a.w;
+ return *this;
+ }
+
+ friend bool operator==(const vec4_scalar& l, const vec4_scalar& r) {
+ return l.x == r.x && l.y == r.y && l.z == r.z && l.w == r.w;
+ }
+
+ friend bool operator!=(const vec4_scalar& l, const vec4_scalar& r) {
+ return l.x != r.x || l.y != r.y || l.z != r.z || l.w != r.w;
+ }
+};
+
+vec3_scalar vec2_scalar::sel(XYZW c1, XYZW c2, XYZW c3) {
+ return {select(c1), select(c2), select(c3)};
+}
+vec4_scalar vec2_scalar::sel(XYZW c1, XYZW c2, XYZW c3, XYZW c4) {
+ return vec4_scalar{select(c1), select(c2), select(c3), select(c4)};
+}
+
+struct vec4_ref {
+ vec4_ref(Float& x, Float& y, Float& z, Float& w) : x(x), y(y), z(z), w(w) {}
+ Float& x;
+ Float& y;
+ Float& z;
+ Float& w;
+
+ vec4_ref& operator=(const vec4& a);
+};
+
+struct vec4 {
+ typedef struct vec4 vector_type;
+ typedef float element_type;
+
+ constexpr vec4() : vec4(Float(0.0f)) {}
+ IMPLICIT constexpr vec4(Float a) : x(a), y(a), z(a), w(a) {}
+ vec4(Float x, Float y, Float z, Float w) : x(x), y(y), z(z), w(w) {}
+ vec4(vec3 xyz, Float w) : x(xyz.x), y(xyz.y), z(xyz.z), w(w) {}
+ vec4(vec2 xy, vec2 zw) : x(xy.x), y(xy.y), z(zw.x), w(zw.y) {}
+ vec4(vec2 xy, Float z, Float w) : x(xy.x), y(xy.y), z(z), w(w) {}
+ vec4(Float x, Float y, vec2 zw) : x(x), y(y), z(zw.x), w(zw.y) {}
+ IMPLICIT constexpr vec4(vec4_scalar s) : x(s.x), y(s.y), z(s.z), w(s.w) {}
+ constexpr vec4(vec4_scalar s0, vec4_scalar s1, vec4_scalar s2, vec4_scalar s3)
+ : x(Float{s0.x, s1.x, s2.x, s3.x}),
+ y(Float{s0.y, s1.y, s2.y, s3.y}),
+ z(Float{s0.z, s1.z, s2.z, s3.z}),
+ w(Float{s0.w, s1.w, s2.w, s3.w}) {}
+ ALWAYS_INLINE Float& select(XYZW c) {
+ switch (c) {
+ case X:
+ return x;
+ case Y:
+ return y;
+ case Z:
+ return z;
+ case W:
+ return w;
+ default:
+ UNREACHABLE;
+ }
+ }
+ ALWAYS_INLINE Float& sel(XYZW c1) { return select(c1); }
+
+ ALWAYS_INLINE vec2 sel(XYZW c1, XYZW c2) {
+ return vec2(select(c1), select(c2));
+ }
+
+ ALWAYS_INLINE vec3 sel(XYZW c1, XYZW c2, XYZW c3) {
+ return vec3(select(c1), select(c2), select(c3));
+ }
+ ALWAYS_INLINE vec3_ref lsel(XYZW c1, XYZW c2, XYZW c3) {
+ return vec3_ref(select(c1), select(c2), select(c3));
+ }
+
+ ALWAYS_INLINE vec2_ref lsel(XYZW c1, XYZW c2) {
+ return vec2_ref(select(c1), select(c2));
+ }
+
+ ALWAYS_INLINE vec4 sel(XYZW c1, XYZW c2, XYZW c3, XYZW c4) {
+ return vec4(select(c1), select(c2), select(c3), select(c4));
+ }
+ ALWAYS_INLINE vec4_ref lsel(XYZW c1, XYZW c2, XYZW c3, XYZW c4) {
+ return vec4_ref(select(c1), select(c2), select(c3), select(c4));
+ }
+
+ Float& operator[](int index) {
+ switch (index) {
+ case 0:
+ return x;
+ case 1:
+ return y;
+ case 2:
+ return z;
+ case 3:
+ return w;
+ default:
+ UNREACHABLE;
+ }
+ }
+
+ // glsl supports non-const indexing of vecs.
+ // hlsl doesn't. The code it generates is probably not wonderful.
+ Float operator[](I32 index) {
+ float sel_x = 0;
+ switch (index.x) {
+ case 0:
+ sel_x = x.x;
+ break;
+ case 1:
+ sel_x = y.x;
+ break;
+ case 2:
+ sel_x = z.x;
+ break;
+ case 3:
+ sel_x = w.x;
+ break;
+ }
+ float sel_y = 0;
+ switch (index.y) {
+ case 0:
+ sel_y = x.y;
+ break;
+ case 1:
+ sel_y = y.y;
+ break;
+ case 2:
+ sel_y = z.y;
+ break;
+ case 3:
+ sel_y = w.y;
+ break;
+ }
+ float sel_z = 0;
+ switch (index.z) {
+ case 0:
+ sel_z = x.z;
+ break;
+ case 1:
+ sel_z = y.z;
+ break;
+ case 2:
+ sel_z = z.z;
+ break;
+ case 3:
+ sel_z = w.z;
+ break;
+ }
+ float sel_w = 0;
+ switch (index.w) {
+ case 0:
+ sel_w = x.w;
+ break;
+ case 1:
+ sel_w = y.w;
+ break;
+ case 2:
+ sel_w = z.w;
+ break;
+ case 3:
+ sel_w = w.w;
+ break;
+ }
+ Float ret = {sel_x, sel_y, sel_z, sel_w};
+ return ret;
+ }
+
+ friend vec4 operator/(vec4 a, Float b) {
+ return vec4(a.x / b, a.y / b, a.z / b, a.w / b);
+ }
+ friend vec4 operator/(vec4 a, vec4 b) {
+ return vec4(a.x / b.x, a.y / b.y, a.z / b.z, a.w / b.w);
+ }
+
+ friend vec4 operator*(vec4 a, Float b) {
+ return vec4(a.x * b, a.y * b, a.z * b, a.w * b);
+ }
+
+ friend vec4 operator*(Float b, vec4 a) {
+ return vec4(a.x * b, a.y * b, a.z * b, a.w * b);
+ }
+ friend vec4 operator*(vec4 a, vec4 b) {
+ return vec4(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w);
+ }
+
+ friend vec4 operator-(vec4 a, vec4 b) {
+ return vec4(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w);
+ }
+ friend vec4 operator+(vec4 a, vec4 b) {
+ return vec4(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w);
+ }
+ vec4& operator+=(vec4 a) {
+ x += a.x;
+ y += a.y;
+ z += a.z;
+ w += a.w;
+ return *this;
+ }
+ vec4& operator/=(vec4 a) {
+ x /= a.x;
+ y /= a.y;
+ z /= a.z;
+ w /= a.w;
+ return *this;
+ }
+ vec4& operator*=(vec4 a) {
+ x *= a.x;
+ y *= a.y;
+ z *= a.z;
+ w *= a.w;
+ return *this;
+ }
+ vec4& operator*=(Float a) {
+ x *= a;
+ y *= a;
+ z *= a;
+ w *= a;
+ return *this;
+ }
+
+ Float x;
+ Float y;
+ Float z;
+ Float w;
+};
+
+inline vec4_ref& vec4_ref::operator=(const vec4& a) {
+ x = a.x;
+ y = a.y;
+ z = a.z;
+ w = a.w;
+ return *this;
+}
+
+inline vec4 vec3::sel(XYZW c1, XYZW c2, XYZW c3, XYZW c4) {
+ return vec4(select(c1), select(c2), select(c3), select(c4));
+}
+
+vec4_scalar force_scalar(const vec4& v) {
+ return vec4_scalar{force_scalar(v.x), force_scalar(v.y), force_scalar(v.z),
+ force_scalar(v.w)};
+}
+
+vec4_scalar make_vec4(float n) { return vec4_scalar{n, n, n, n}; }
+
+vec4_scalar make_vec4(const vec2_scalar& v, float z, float w) {
+ return vec4_scalar{v.x, v.y, z, w};
+}
+
+vec4_scalar make_vec4(const vec2_scalar& a, const vec2_scalar& b) {
+ return vec4_scalar{a.x, a.y, b.x, b.y};
+}
+
+vec4_scalar make_vec4(const vec3_scalar& v, float w) {
+ return vec4_scalar{v.x, v.y, v.z, w};
+}
+
+vec4_scalar make_vec4(float x, float y, float z, float w) {
+ return vec4_scalar{x, y, z, w};
+}
+
+vec4_scalar make_vec4(float x, float y, const vec2_scalar& v) {
+ return vec4_scalar{x, y, v.x, v.y};
+}
+
+ivec4_scalar make_ivec4(const vec4_scalar& v) {
+ return ivec4_scalar{int32_t(v.x), int32_t(v.y), int32_t(v.z), int32_t(v.w)};
+}
+
+template <typename N>
+vec4 make_vec4(const N& n) {
+ return vec4(n);
+}
+
+template <typename X, typename Y>
+vec4 make_vec4(const X& x, const Y& y) {
+ return vec4(x, y);
+}
+
+template <typename X, typename Y, typename Z>
+vec4 make_vec4(const X& x, const Y& y, const Z& z) {
+ return vec4(x, y, z);
+}
+
+template <typename X, typename Y, typename Z, typename W>
+vec4 make_vec4(const X& x, const Y& y, const Z& z, const W& w) {
+ return vec4(x, y, z, w);
+}
+
+vec4_scalar make_vec4(const ivec4_scalar& v) {
+ return vec4_scalar{float(v.x), float(v.y), float(v.z), float(v.w)};
+}
+
+ALWAYS_INLINE vec3::vec3(vec4 v) : x(v.x), y(v.y), z(v.z) {}
+
+SI ivec4 roundfast(vec4 v, Float scale) {
+ return ivec4(roundfast(v.x, scale), roundfast(v.y, scale),
+ roundfast(v.z, scale), roundfast(v.w, scale));
+}
+
+vec4 operator*(vec4_scalar a, Float b) {
+ return vec4(a.x * b, a.y * b, a.z * b, a.w * b);
+}
+
+SI vec4 if_then_else(I32 c, vec4 t, vec4 e) {
+ return vec4(if_then_else(c, t.x, e.x), if_then_else(c, t.y, e.y),
+ if_then_else(c, t.z, e.z), if_then_else(c, t.w, e.w));
+}
+
+SI vec4 if_then_else(int32_t c, vec4 t, vec4 e) { return c ? t : e; }
+
+SI vec4_scalar if_then_else(int32_t c, vec4_scalar t, vec4_scalar e) {
+ return c ? t : e;
+}
+
+SI vec2 clamp(vec2 a, Float minVal, Float maxVal) {
+ return vec2(clamp(a.x, minVal, maxVal), clamp(a.y, minVal, maxVal));
+}
+
+SI vec2 clamp(vec2 a, vec2 minVal, vec2 maxVal) {
+ return vec2(clamp(a.x, minVal.x, maxVal.x), clamp(a.y, minVal.y, maxVal.y));
+}
+
+SI vec2_scalar clamp(vec2_scalar a, vec2_scalar minVal, vec2_scalar maxVal) {
+ return vec2_scalar{clamp(a.x, minVal.x, maxVal.x),
+ clamp(a.y, minVal.y, maxVal.y)};
+}
+
+SI vec2_scalar clamp(vec2_scalar a, float minVal, float maxVal) {
+ return vec2_scalar{clamp(a.x, minVal, maxVal), clamp(a.y, minVal, maxVal)};
+}
+
+SI I32 clamp(I32 a, I32 minVal, I32 maxVal) {
+ a = if_then_else(a < minVal, minVal, a);
+ return if_then_else(a > maxVal, maxVal, a);
+}
+
+SI vec3 clamp(vec3 a, Float minVal, Float maxVal) {
+ return vec3(clamp(a.x, minVal, maxVal), clamp(a.y, minVal, maxVal),
+ clamp(a.z, minVal, maxVal));
+}
+
+SI vec3 clamp(vec3 a, vec3 minVal, vec3 maxVal) {
+ return vec3(clamp(a.x, minVal.x, maxVal.x), clamp(a.y, minVal.y, maxVal.y),
+ clamp(a.z, minVal.z, maxVal.z));
+}
+
+SI vec4 clamp(vec4 a, Float minVal, Float maxVal) {
+ return vec4(clamp(a.x, minVal, maxVal), clamp(a.y, minVal, maxVal),
+ clamp(a.z, minVal, maxVal), clamp(a.w, minVal, maxVal));
+}
+
+SI vec4 clamp(vec4 a, vec4 minVal, vec4 maxVal) {
+ return vec4(clamp(a.x, minVal.x, maxVal.x), clamp(a.y, minVal.y, maxVal.y),
+ clamp(a.z, minVal.z, maxVal.z), clamp(a.w, minVal.w, maxVal.w));
+}
+
+SI vec4_scalar clamp(vec4_scalar a, vec4_scalar minVal, vec4_scalar maxVal) {
+ return vec4_scalar{
+ clamp(a.x, minVal.x, maxVal.x), clamp(a.y, minVal.y, maxVal.y),
+ clamp(a.z, minVal.z, maxVal.z), clamp(a.w, minVal.w, maxVal.w)};
+}
+
+SI vec4_scalar clamp(vec4_scalar a, float minVal, float maxVal) {
+ return vec4_scalar{clamp(a.x, minVal, maxVal), clamp(a.y, minVal, maxVal),
+ clamp(a.z, minVal, maxVal), clamp(a.w, minVal, maxVal)};
+}
+
+vec4 step(vec4 edge, vec4 x) {
+ return vec4(step(edge.x, x.x), step(edge.y, x.y), step(edge.z, x.z),
+ step(edge.w, x.w));
+}
+
+vec4_scalar step(vec4_scalar edge, vec4_scalar x) {
+ return vec4_scalar(step(edge.x, x.x), step(edge.y, x.y), step(edge.z, x.z),
+ step(edge.w, x.w));
+}
+
+template <typename T>
+auto lessThanEqual(T x, T y) -> decltype(x <= y) {
+ return x <= y;
+}
+
+template <typename T>
+auto lessThan(T x, T y) -> decltype(x < y) {
+ return x < y;
+}
+
+SI bvec3 lessThanEqual(vec3 x, vec3 y) {
+ return bvec3(lessThanEqual(x.x, y.x), lessThanEqual(x.y, y.y),
+ lessThanEqual(x.z, y.z));
+}
+
+SI bvec2 lessThanEqual(vec2 x, vec2 y) {
+ return bvec2(lessThanEqual(x.x, y.x), lessThanEqual(x.y, y.y));
+}
+
+SI bvec2_scalar lessThanEqual(vec2_scalar x, vec2_scalar y) {
+ return bvec2_scalar{lessThanEqual(x.x, y.x), lessThanEqual(x.y, y.y)};
+}
+
+SI bvec4 lessThanEqual(vec4 x, vec4 y) {
+ return bvec4(lessThanEqual(x.x, y.x), lessThanEqual(x.y, y.y),
+ lessThanEqual(x.z, y.z), lessThanEqual(x.w, y.w));
+}
+
+SI bvec4_scalar lessThanEqual(vec4_scalar x, vec4_scalar y) {
+ return bvec4_scalar{lessThanEqual(x.x, y.x), lessThanEqual(x.y, y.y),
+ lessThanEqual(x.z, y.z), lessThanEqual(x.w, y.w)};
+}
+
+SI bvec2 lessThan(vec2 x, vec2 y) {
+ return bvec2(lessThan(x.x, y.x), lessThan(x.y, y.y));
+}
+
+SI bvec2_scalar lessThan(vec2_scalar x, vec2_scalar y) {
+ return bvec2_scalar(lessThan(x.x, y.x), lessThan(x.y, y.y));
+}
+
+SI bvec4 lessThan(vec4 x, vec4 y) {
+ return bvec4(lessThan(x.x, y.x), lessThan(x.y, y.y), lessThan(x.z, y.z),
+ lessThan(x.w, y.w));
+}
+
+SI bvec4_scalar lessThan(vec4_scalar x, vec4_scalar y) {
+ return bvec4_scalar{lessThan(x.x, y.x), lessThan(x.y, y.y),
+ lessThan(x.z, y.z), lessThan(x.w, y.w)};
+}
+
+template <typename T>
+auto greaterThan(T x, T y) -> decltype(x > y) {
+ return x > y;
+}
+
+bvec2 greaterThan(vec2 x, vec2 y) {
+ return bvec2(greaterThan(x.x, y.x), greaterThan(x.y, y.y));
+}
+
+bvec2_scalar greaterThan(vec2_scalar x, vec2_scalar y) {
+ return bvec2_scalar(greaterThan(x.x, y.x), greaterThan(x.y, y.y));
+}
+
+SI bvec4 greaterThan(vec4 x, vec4 y) {
+ return bvec4(greaterThan(x.x, y.x), greaterThan(x.y, y.y),
+ greaterThan(x.z, y.z), greaterThan(x.w, y.w));
+}
+
+SI bvec4_scalar greaterThan(vec4_scalar x, vec4_scalar y) {
+ return bvec4_scalar{greaterThan(x.x, y.x), greaterThan(x.y, y.y),
+ greaterThan(x.z, y.z), greaterThan(x.w, y.w)};
+}
+
+template <typename T>
+auto greaterThanEqual(T x, T y) -> decltype(x >= y) {
+ return x >= y;
+}
+
+bvec4 greaterThanEqual(vec4 x, vec4 y) {
+ return bvec4(greaterThanEqual(x.x, y.x), greaterThanEqual(x.y, y.y),
+ greaterThanEqual(x.z, y.z), greaterThanEqual(x.w, y.w));
+}
+
+template <typename T>
+auto equal(T x, T y) -> decltype(x > y) {
+ return x == y;
+}
+
+bvec2 equal(vec2 x, vec2 y) { return bvec2(equal(x.x, y.x), equal(x.y, y.y)); }
+
+bvec2_scalar equal(vec2_scalar x, vec2_scalar y) {
+ return bvec2_scalar(equal(x.x, y.x), equal(x.y, y.y));
+}
+
+template <typename T>
+auto notEqual(T x, T y) -> decltype(x > y) {
+ return x != y;
+}
+
+bvec2 notEqual(vec2 x, vec2 y) {
+ return bvec2(notEqual(x.x, y.x), notEqual(x.y, y.y));
+}
+
+bvec2_scalar notEqual(vec2_scalar x, vec2_scalar y) {
+ return bvec2_scalar(notEqual(x.x, y.x), notEqual(x.y, y.y));
+}
+
+struct mat4_scalar;
+
+struct mat2_scalar {
+ vec2_scalar data[2];
+
+ mat2_scalar() = default;
+ IMPLICIT constexpr mat2_scalar(float a)
+ : data{vec2_scalar(a), vec2_scalar(a)} {}
+ constexpr mat2_scalar(vec2_scalar a, vec2_scalar b) : data{a, b} {}
+ IMPLICIT mat2_scalar(const mat4_scalar& mat);
+
+ vec2_scalar& operator[](int index) { return data[index]; }
+ const vec2_scalar& operator[](int index) const { return data[index]; }
+
+ friend vec2_scalar operator*(mat2_scalar m, vec2_scalar v) {
+ vec2_scalar u;
+ u.x = m[0].x * v.x + m[1].x * v.y;
+ u.y = m[0].y * v.x + m[1].y * v.y;
+ return u;
+ }
+
+ friend vec2 operator*(mat2_scalar m, vec2 v) {
+ vec2 u;
+ u.x = m[0].x * v.x + m[1].x * v.y;
+ u.y = m[0].y * v.x + m[1].y * v.y;
+ return u;
+ }
+
+ friend mat2_scalar operator*(mat2_scalar m, float f) {
+ mat2_scalar u = m;
+ u[0].x *= f;
+ u[0].y *= f;
+ u[1].x *= f;
+ u[1].y *= f;
+ return u;
+ }
+};
+
+struct mat4;
+
+struct mat2 {
+ vec2 data[2];
+
+ vec2& operator[](int index) { return data[index]; }
+ const vec2& operator[](int index) const { return data[index]; }
+ mat2() = default;
+
+ IMPLICIT constexpr mat2(Float a) : data{vec2(a), vec2(a)} {}
+
+ constexpr mat2(vec2 a, vec2 b) : data{a, b} {}
+ IMPLICIT mat2(const mat4& mat);
+ IMPLICIT constexpr mat2(mat2_scalar s)
+ : data{vec2(s.data[0]), vec2(s.data[1])} {}
+
+ friend vec2 operator*(mat2 m, vec2 v) {
+ vec2 u;
+ u.x = m[0].x * v.x + m[1].x * v.y;
+ u.y = m[0].y * v.x + m[1].y * v.y;
+ return u;
+ }
+ friend mat2 operator*(mat2 m, Float f) {
+ mat2 u = m;
+ u[0].x *= f;
+ u[0].y *= f;
+ u[1].x *= f;
+ u[1].y *= f;
+ return u;
+ }
+};
+
+mat2_scalar make_mat2(float n) { return mat2_scalar{{n, n}, {n, n}}; }
+
+mat2_scalar make_mat2(const mat2_scalar& m) { return m; }
+
+mat2_scalar make_mat2(const vec2_scalar& x, const vec2_scalar& y) {
+ return mat2_scalar{x, y};
+}
+
+template <typename N>
+mat2 make_mat2(const N& n) {
+ return mat2(n);
+}
+
+template <typename X, typename Y>
+mat2 make_mat2(const X& x, const Y& y) {
+ return mat2(x, y);
+}
+
+SI mat2 if_then_else(I32 c, mat2 t, mat2 e) {
+ return mat2(if_then_else(c, t[0], e[0]), if_then_else(c, t[0], e[1]));
+}
+
+SI mat2 if_then_else(int32_t c, mat2 t, mat2 e) { return c ? t : e; }
+
+struct mat3_scalar {
+ vec3_scalar data[3];
+
+ mat3_scalar() = default;
+ constexpr mat3_scalar(vec3_scalar a, vec3_scalar b, vec3_scalar c)
+ : data{a, b, c} {}
+ IMPLICIT mat3_scalar(const mat4_scalar& mat);
+
+ vec3_scalar& operator[](int index) { return data[index]; }
+ const vec3_scalar& operator[](int index) const { return data[index]; }
+
+ friend vec3_scalar operator*(mat3_scalar m, vec3_scalar v) {
+ vec3_scalar u;
+ u.x = m[0].x * v.x + m[1].x * v.y + m[2].x * v.z;
+ u.y = m[0].y * v.x + m[1].y * v.y + m[2].y * v.z;
+ u.z = m[0].z * v.x + m[1].z * v.y + m[2].z * v.z;
+ return u;
+ }
+
+ friend vec3 operator*(mat3_scalar m, vec3 v) {
+ vec3 u;
+ u.x = m[0].x * v.x + m[1].x * v.y + m[2].x * v.z;
+ u.y = m[0].y * v.x + m[1].y * v.y + m[2].y * v.z;
+ u.z = m[0].z * v.x + m[1].z * v.y + m[2].z * v.z;
+ return u;
+ }
+
+ friend auto operator*(mat3_scalar a, mat3_scalar b) {
+ mat3_scalar r;
+ for (int c = 0; c < 3; c++) {
+ const auto& v = b[c];
+ r[c].x = a[0].x * v.x + a[1].x * v.y + a[2].x * v.z;
+ r[c].y = a[0].y * v.x + a[1].y * v.y + a[2].y * v.z;
+ r[c].z = a[0].z * v.x + a[1].z * v.y + a[2].z * v.z;
+ }
+ return r;
+ }
+};
+
+struct mat3 {
+ vec3 data[3];
+
+ vec3& operator[](int index) { return data[index]; }
+ const vec3& operator[](int index) const { return data[index]; }
+ mat3() = default;
+ constexpr mat3(vec3 a, vec3 b, vec3 c) : data{a, b, c} {}
+
+ IMPLICIT constexpr mat3(mat3_scalar s)
+ : data{vec3(s.data[0]), vec3(s.data[1]), vec3(s.data[2])} {}
+
+ constexpr mat3(mat3_scalar s0, mat3_scalar s1, mat3_scalar s2, mat3_scalar s3)
+ : data{vec3(s0.data[0], s1.data[0], s2.data[0], s3.data[0]),
+ vec3(s0.data[1], s1.data[1], s2.data[1], s3.data[1]),
+ vec3(s0.data[2], s1.data[2], s2.data[2], s3.data[2])} {}
+
+ constexpr mat3(Float d1, Float d2, Float d3, Float d4, Float d5, Float d6,
+ Float d7, Float d8, Float d9)
+ : data{vec3(d1, d2, d3), vec3(d4, d5, d6), vec3(d7, d8, d9)} {}
+
+ IMPLICIT mat3(const mat4& mat);
+
+ friend vec3 operator*(mat3 m, vec3 v) {
+ vec3 u;
+ u.x = m[0].x * v.x + m[1].x * v.y + m[2].x * v.z;
+ u.y = m[0].y * v.x + m[1].y * v.y + m[2].y * v.z;
+ u.z = m[0].z * v.x + m[1].z * v.y + m[2].z * v.z;
+ return u;
+ }
+};
+
+mat3_scalar force_scalar(const mat3& v) {
+ return mat3_scalar{force_scalar(v[0]), force_scalar(v[1]),
+ force_scalar(v[2])};
+}
+
+mat3_scalar make_mat3(const mat3_scalar& m) { return m; }
+
+mat3_scalar make_mat3(const vec3_scalar& x, const vec3_scalar& y,
+ const vec3_scalar& z) {
+ return mat3_scalar{x, y, z};
+}
+
+constexpr mat3_scalar make_mat3(float m0, float m1, float m2, float m3,
+ float m4, float m5, float m6, float m7,
+ float m8) {
+ return mat3_scalar{{m0, m1, m2}, {m3, m4, m5}, {m6, m7, m8}};
+}
+
+template <typename N>
+mat3 make_mat3(const N& n) {
+ return mat3(n);
+}
+
+template <typename X, typename Y, typename Z>
+mat3 make_mat3(const X& x, const Y& y, const Z& z) {
+ return mat3(x, y, z);
+}
+
+struct mat3x4_scalar {
+ vec4_scalar data[3];
+
+ mat3x4_scalar() = default;
+ constexpr mat3x4_scalar(vec4_scalar a, vec4_scalar b, vec4_scalar c)
+ : data{a, b, c} {}
+
+ auto& operator[](int index) { return data[index]; }
+ constexpr auto operator[](int index) const { return data[index]; }
+
+ friend auto operator*(mat3x4_scalar m, vec3_scalar v) {
+ vec4_scalar u;
+ u.x = m[0].x * v.x + m[1].x * v.y + m[2].x * v.z;
+ u.y = m[0].y * v.x + m[1].y * v.y + m[2].y * v.z;
+ u.z = m[0].z * v.x + m[1].z * v.y + m[2].z * v.z;
+ u.w = m[0].w * v.x + m[1].w * v.y + m[2].w * v.z;
+ return u;
+ }
+
+ friend auto operator*(mat3x4_scalar m, vec3 v) {
+ vec4 u;
+ u.x = m[0].x * v.x + m[1].x * v.y + m[2].x * v.z;
+ u.y = m[0].y * v.x + m[1].y * v.y + m[2].y * v.z;
+ u.z = m[0].z * v.x + m[1].z * v.y + m[2].z * v.z;
+ u.w = m[0].w * v.x + m[1].w * v.y + m[2].w * v.z;
+ return u;
+ }
+};
+
+constexpr mat3x4_scalar make_mat3x4(float m0, float m1, float m2, float m3,
+ float m4, float m5, float m6, float m7,
+ float m8, float m9, float m10, float m11) {
+ return mat3x4_scalar{
+ {m0, m1, m2, m3},
+ {m4, m5, m6, m7},
+ {m8, m9, m10, m11},
+ };
+}
+
+struct mat4x3_scalar {
+ vec3_scalar data[4];
+
+ mat4x3_scalar() = default;
+ constexpr mat4x3_scalar(vec3_scalar a, vec3_scalar b, vec3_scalar c,
+ vec3_scalar d)
+ : data{a, b, c, d} {}
+
+ auto& operator[](int index) { return data[index]; }
+ constexpr auto operator[](int index) const { return data[index]; }
+
+ friend auto operator*(mat4x3_scalar m, vec4_scalar v) {
+ vec3_scalar u;
+ u.x = m[0].x * v.x + m[1].x * v.y + m[2].x * v.z + m[3].x * v.w;
+ u.y = m[0].y * v.x + m[1].y * v.y + m[2].y * v.z + m[3].y * v.w;
+ u.z = m[0].z * v.x + m[1].z * v.y + m[2].z * v.z + m[3].z * v.w;
+ return u;
+ }
+
+ friend auto operator*(mat4x3_scalar m, vec4 v) {
+ vec3 u;
+ u.x = m[0].x * v.x + m[1].x * v.y + m[2].x * v.z + m[3].x * v.w;
+ u.y = m[0].y * v.x + m[1].y * v.y + m[2].y * v.z + m[3].y * v.w;
+ u.z = m[0].z * v.x + m[1].z * v.y + m[2].z * v.z + m[3].z * v.w;
+ return u;
+ }
+};
+
+constexpr mat4x3_scalar transpose(const mat3x4_scalar m) {
+ return {{m[0].x, m[1].x, m[2].x},
+ {m[0].y, m[1].y, m[2].y},
+ {m[0].z, m[1].z, m[2].z},
+ {m[0].w, m[1].w, m[2].w}};
+}
+
+struct mat4_scalar {
+ vec4_scalar data[4];
+
+ mat4_scalar() = default;
+ constexpr mat4_scalar(vec4_scalar a, vec4_scalar b, vec4_scalar c,
+ vec4_scalar d)
+ : data{a, b, c, d} {}
+
+ vec4_scalar& operator[](int index) { return data[index]; }
+ const vec4_scalar& operator[](int index) const { return data[index]; }
+
+ static mat4_scalar load_from_ptr(const float* f) {
+ return mat4_scalar(
+ vec4_scalar::load_from_ptr(&f[0]), vec4_scalar::load_from_ptr(&f[4]),
+ vec4_scalar::load_from_ptr(&f[8]), vec4_scalar::load_from_ptr(&f[12]));
+ }
+
+ friend vec4_scalar operator*(mat4_scalar m, vec4_scalar v) {
+ vec4_scalar u;
+ u.x = m[0].x * v.x + m[1].x * v.y + m[2].x * v.z + m[3].x * v.w;
+ u.y = m[0].y * v.x + m[1].y * v.y + m[2].y * v.z + m[3].y * v.w;
+ u.z = m[0].z * v.x + m[1].z * v.y + m[2].z * v.z + m[3].z * v.w;
+ u.w = m[0].w * v.x + m[1].w * v.y + m[2].w * v.z + m[3].w * v.w;
+ return u;
+ }
+
+ friend vec4 operator*(mat4_scalar m, vec4 v) {
+ vec4 u;
+ u.x = m[0].x * v.x + m[1].x * v.y + m[2].x * v.z + m[3].x * v.w;
+ u.y = m[0].y * v.x + m[1].y * v.y + m[2].y * v.z + m[3].y * v.w;
+ u.z = m[0].z * v.x + m[1].z * v.y + m[2].z * v.z + m[3].z * v.w;
+ u.w = m[0].w * v.x + m[1].w * v.y + m[2].w * v.z + m[3].w * v.w;
+ return u;
+ }
+};
+
+struct mat4 {
+ vec4 data[4];
+
+ mat4() = default;
+ IMPLICIT constexpr mat4(mat4_scalar s)
+ : data{vec4(s.data[0]), vec4(s.data[1]), vec4(s.data[2]),
+ vec4(s.data[3])} {}
+
+ constexpr mat4(vec4 a, vec4 b, vec4 c, vec4 d) : data{a, b, c, d} {}
+
+ vec4& operator[](int index) { return data[index]; }
+ const vec4& operator[](int index) const { return data[index]; }
+
+ friend vec4 operator*(mat4 m, vec4 v) {
+ vec4 u;
+ u.x = m[0].x * v.x + m[1].x * v.y + m[2].x * v.z + m[3].x * v.w;
+ u.y = m[0].y * v.x + m[1].y * v.y + m[2].y * v.z + m[3].y * v.w;
+ u.z = m[0].z * v.x + m[1].z * v.y + m[2].z * v.z + m[3].z * v.w;
+ u.w = m[0].w * v.x + m[1].w * v.y + m[2].w * v.z + m[3].w * v.w;
+ return u;
+ }
+};
+
+mat3::mat3(const mat4& mat)
+ : mat3(vec3(mat[0].x, mat[0].y, mat[0].z),
+ vec3(mat[1].x, mat[1].y, mat[1].z),
+ vec3(mat[2].x, mat[2].y, mat[2].z)) {}
+
+IMPLICIT mat3_scalar::mat3_scalar(const mat4_scalar& mat)
+ : mat3_scalar(vec3_scalar(mat[0].x, mat[0].y, mat[0].z),
+ vec3_scalar(mat[1].x, mat[1].y, mat[1].z),
+ vec3_scalar(mat[2].x, mat[2].y, mat[2].z)) {}
+
+IMPLICIT mat2::mat2(const mat4& mat)
+ : mat2(vec2(mat[0].x, mat[0].y), vec2(mat[1].x, mat[1].y)) {}
+
+IMPLICIT mat2_scalar::mat2_scalar(const mat4_scalar& mat)
+ : mat2_scalar(vec2_scalar(mat[0].x, mat[0].y),
+ vec2_scalar(mat[1].x, mat[1].y)) {}
+
+mat2_scalar make_mat2(const mat4_scalar& m) { return mat2_scalar(m); }
+
+mat3_scalar make_mat3(const mat4_scalar& m) { return mat3_scalar(m); }
+
+mat4_scalar force_scalar(const mat4& v) {
+ return mat4_scalar(force_scalar(v[0]), force_scalar(v[1]), force_scalar(v[2]),
+ force_scalar(v[3]));
+}
+
+mat4_scalar make_mat4(const mat4_scalar& m) { return m; }
+
+mat4_scalar make_mat4(const vec4_scalar& x, const vec4_scalar& y,
+ const vec4_scalar& z, const vec4_scalar& w) {
+ return mat4_scalar{x, y, z, w};
+}
+
+constexpr mat4_scalar make_mat4(float m0, float m1, float m2, float m3,
+ float m4, float m5, float m6, float m7,
+ float m8, float m9, float m10, float m11,
+ float m12, float m13, float m14, float m15) {
+ return mat4_scalar{{m0, m1, m2, m3},
+ {m4, m5, m6, m7},
+ {m8, m9, m10, m11},
+ {m12, m13, m14, m15}};
+}
+
+template <typename N>
+mat4 make_mat4(const N& n) {
+ return mat4(n);
+}
+
+template <typename X, typename Y, typename Z, typename W>
+mat4 make_mat4(const X& x, const Y& y, const Z& z, const W& w) {
+ return mat4(x, y, z, w);
+}
+
+SI mat3 if_then_else(I32 c, mat3 t, mat3 e) {
+ return mat3{if_then_else(c, t[0], e[0]), if_then_else(c, t[1], e[1]),
+ if_then_else(c, t[2], e[2])};
+}
+
+SI mat3 if_then_else(int32_t c, mat3 t, mat3 e) { return c ? t : e; }
+
+SI mat4 if_then_else(I32 c, mat4 t, mat4 e) {
+ return mat4{if_then_else(c, t[0], e[0]), if_then_else(c, t[1], e[1]),
+ if_then_else(c, t[2], e[2]), if_then_else(c, t[3], e[3])};
+}
+
+SI mat4 if_then_else(int32_t c, mat4 t, mat4 e) { return c ? t : e; }
+
+template <typename T, typename U, typename A,
+ typename R = typename T::vector_type>
+SI R mix(T x, U y, A a) {
+ return (y - x) * a + x;
+}
+
+SI Float mix(Float x, Float y, Float a) { return (y - x) * a + x; }
+
+template <typename T>
+SI T mix(T x, T y, float a) {
+ return (y - x) * a + x;
+}
+
+template <typename T>
+SI T mix(T x, T y, vec2_scalar a) {
+ return T{mix(x.x, y.x, a.x), mix(x.y, y.y, a.y)};
+}
+
+template <typename T>
+SI T mix(T x, T y, vec3_scalar a) {
+ return T{mix(x.x, y.x, a.x), mix(x.y, y.y, a.y), mix(x.z, y.z, a.z)};
+}
+
+template <typename T>
+SI T mix(T x, T y, vec4_scalar a) {
+ return T{mix(x.x, y.x, a.x), mix(x.y, y.y, a.y), mix(x.z, y.z, a.z),
+ mix(x.w, y.w, a.w)};
+}
+
+ivec4 ivec2::sel(XYZW c1, XYZW c2, XYZW c3, XYZW c4) {
+ return ivec4(select(c1), select(c2), select(c3), select(c4));
+}
+
+vec4 vec2::sel(XYZW c1, XYZW c2, XYZW c3, XYZW c4) {
+ return vec4(select(c1), select(c2), select(c3), select(c4));
+}
+
+bool any(bool x) { return x; }
+
+Bool any(bvec4 x) { return x.x | x.y | x.z | x.w; }
+
+bool any(bvec4_scalar x) { return x.x | x.y | x.z | x.w; }
+
+Bool any(bvec2 x) { return x.x | x.y; }
+
+bool any(bvec2_scalar x) { return x.x | x.y; }
+
+bool all(bool x) { return x; }
+
+Bool all(bvec2 x) { return x.x & x.y; }
+
+bool all(bvec2_scalar x) { return x.x & x.y; }
+
+Bool all(bvec4 x) { return x.x & x.y & x.z & x.w; }
+
+bool all(bvec4_scalar x) { return x.x & x.y & x.z & x.w; }
+
+SI vec4 if_then_else(bvec4 c, vec4 t, vec4 e) {
+ return vec4(if_then_else(c.x, t.x, e.x), if_then_else(c.y, t.y, e.y),
+ if_then_else(c.z, t.z, e.z), if_then_else(c.w, t.w, e.w));
+}
+SI vec3 if_then_else(bvec3 c, vec3 t, vec3 e) {
+ return vec3(if_then_else(c.x, t.x, e.x), if_then_else(c.y, t.y, e.y),
+ if_then_else(c.z, t.z, e.z));
+}
+
+SI vec2 if_then_else(bvec2 c, vec2 t, vec2 e) {
+ return vec2(if_then_else(c.x, t.x, e.x), if_then_else(c.y, t.y, e.y));
+}
+
+template <typename T, typename R = typename T::vector_type>
+SI R mix(T x, T y, bvec4 a) {
+ return if_then_else(a, y, x);
+}
+
+template <typename T, typename R = typename T::vector_type>
+SI R mix(T x, T y, bvec3 a) {
+ return if_then_else(a, y, x);
+}
+
+template <typename T, typename R = typename T::vector_type>
+SI R mix(T x, T y, bvec2 a) {
+ return if_then_else(a, y, x);
+}
+
+template <typename T>
+SI T mix(T x, T y, bvec4_scalar a) {
+ return T{a.x ? y.x : x.x, a.y ? y.y : x.y, a.z ? y.z : x.z, a.w ? y.w : x.w};
+}
+
+template <typename T>
+SI T mix(T x, T y, bvec4_scalar1 a) {
+ return a.x ? y : x;
+}
+
+template <typename T>
+SI T mix(T x, T y, bvec3_scalar a) {
+ return T{a.x ? y.x : x.x, a.y ? y.y : x.y, a.z ? y.z : x.z};
+}
+
+template <typename T>
+SI T mix(T x, T y, bvec3_scalar1 a) {
+ return a.x ? y : x;
+}
+
+template <typename T>
+SI T mix(T x, T y, bvec2_scalar a) {
+ return T{a.x ? y.x : x.x, a.y ? y.y : x.y};
+}
+
+template <typename T>
+SI T mix(T x, T y, bvec2_scalar1 a) {
+ return a.x ? y : x;
+}
+
+float dot(vec3_scalar a, vec3_scalar b) {
+ return a.x * b.x + a.y * b.y + a.z * b.z;
+}
+
+Float dot(vec3 a, vec3 b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
+
+float dot(vec2_scalar a, vec2_scalar b) { return a.x * b.x + a.y * b.y; }
+
+Float dot(vec2 a, vec2 b) { return a.x * b.x + a.y * b.y; }
+
+#define sin __glsl_sin
+
+float sin(float x) { return sinf(x); }
+
+Float sin(Float v) { return {sinf(v.x), sinf(v.y), sinf(v.z), sinf(v.w)}; }
+
+#define cos __glsl_cos
+
+float cos(float x) { return cosf(x); }
+
+Float cos(Float v) { return {cosf(v.x), cosf(v.y), cosf(v.z), cosf(v.w)}; }
+
+#define tan __glsl_tan
+
+float tan(float x) { return tanf(x); }
+
+Float tan(Float v) { return {tanf(v.x), tanf(v.y), tanf(v.z), tanf(v.w)}; }
+
+#define atan __glsl_atan
+
+float atan(float x) { return atanf(x); }
+
+Float atan(Float v) { return {atanf(v.x), atanf(v.y), atanf(v.z), atanf(v.w)}; }
+
+float atan(float a, float b) { return atan2f(a, b); }
+
+Float atan(Float a, Float b) {
+ return {atan2f(a.x, b.x), atan2f(a.y, b.y), atan2f(a.z, b.z),
+ atan2f(a.w, b.w)};
+}
+
+bvec4 equal(vec4 x, vec4 y) {
+ return bvec4(equal(x.x, y.x), equal(x.y, y.y), equal(x.z, y.z),
+ equal(x.w, y.w));
+}
+
+bvec4_scalar equal(vec4_scalar x, vec4_scalar y) {
+ return bvec4_scalar(equal(x.x, y.x), equal(x.y, y.y), equal(x.z, y.z),
+ equal(x.w, y.w));
+}
+
+bvec4 notEqual(vec4 x, vec4 y) {
+ return bvec4(notEqual(x.x, y.x), notEqual(x.y, y.y), notEqual(x.z, y.z),
+ notEqual(x.w, y.w));
+}
+
+bvec4_scalar notEqual(vec4_scalar x, vec4_scalar y) {
+ return bvec4_scalar(notEqual(x.x, y.x), notEqual(x.y, y.y),
+ notEqual(x.z, y.z), notEqual(x.w, y.w));
+}
+
+bvec4 notEqual(ivec4 a, ivec4 b) {
+ return bvec4(a.x != b.x, a.y != b.y, a.z != b.z, a.w != b.w);
+}
+
+bvec4_scalar notEqual(ivec4_scalar a, ivec4_scalar b) {
+ return bvec4_scalar{a.x != b.x, a.y != b.y, a.z != b.z, a.w != b.w};
+}
+
+mat3 transpose(mat3 m) {
+ return mat3(vec3(m[0].x, m[1].x, m[2].x), vec3(m[0].y, m[1].y, m[2].y),
+ vec3(m[0].z, m[1].z, m[2].z));
+}
+
+mat3_scalar transpose(mat3_scalar m) {
+ return mat3_scalar{vec3_scalar(m[0].x, m[1].x, m[2].x),
+ vec3_scalar(m[0].y, m[1].y, m[2].y),
+ vec3_scalar(m[0].z, m[1].z, m[2].z)};
+}
+
+vec2 abs(vec2 v) { return vec2(abs(v.x), abs(v.y)); }
+
+vec2_scalar abs(vec2_scalar v) { return vec2_scalar{fabsf(v.x), fabsf(v.y)}; }
+
+vec2 sign(vec2 v) { return vec2(sign(v.x), sign(v.y)); }
+
+vec2_scalar sign(vec2_scalar v) { return vec2_scalar{sign(v.x), sign(v.y)}; }
+
+Float mod(Float a, Float b) { return a - b * floor(a / b); }
+
+vec2 mod(vec2 a, vec2 b) { return vec2(mod(a.x, b.x), mod(a.y, b.y)); }
+
+vec3 abs(vec3 v) { return vec3(abs(v.x), abs(v.y), abs(v.z)); }
+
+vec3 sign(vec3 v) { return vec3(sign(v.x), sign(v.y), sign(v.z)); }
+
+mat2 inverse(mat2 v) {
+ Float det = v[0].x * v[1].y - v[0].y * v[1].x;
+ return mat2(vec2(v[1].y, -v[0].y), vec2(-v[1].x, v[0].x)) * (1. / det);
+}
+
+mat2_scalar inverse(mat2_scalar v) {
+ float det = v[0].x * v[1].y - v[0].y * v[1].x;
+ return mat2_scalar{{v[1].y, -v[0].y}, {-v[1].x, v[0].x}} * (1. / det);
+}
+
+int32_t get_nth(I32 a, int n) { return a[n]; }
+
+float get_nth(Float a, int n) { return a[n]; }
+
+float get_nth(float a, int) { return a; }
+
+ivec2_scalar get_nth(ivec2 a, int n) { return ivec2_scalar{a.x[n], a.y[n]}; }
+
+vec2_scalar get_nth(vec2 a, int n) { return vec2_scalar{a.x[n], a.y[n]}; }
+
+vec3_scalar get_nth(vec3 a, int n) {
+ return vec3_scalar{a.x[n], a.y[n], a.z[n]};
+}
+
+vec4_scalar get_nth(vec4 a, int n) {
+ return vec4_scalar{a.x[n], a.y[n], a.z[n], a.w[n]};
+}
+
+ivec4_scalar get_nth(ivec4 a, int n) {
+ return ivec4_scalar{a.x[n], a.y[n], a.z[n], a.w[n]};
+}
+
+mat3_scalar get_nth(mat3 a, int n) {
+ return make_mat3(get_nth(a[0], n), get_nth(a[1], n), get_nth(a[2], n));
+}
+
+void put_nth(Float& dst, int n, float src) { dst[n] = src; }
+
+void put_nth(I32& dst, int n, int32_t src) { dst[n] = src; }
+
+void put_nth(ivec2& dst, int n, ivec2_scalar src) {
+ dst.x[n] = src.x;
+ dst.y[n] = src.y;
+}
+
+void put_nth(vec2& dst, int n, vec2_scalar src) {
+ dst.x[n] = src.x;
+ dst.y[n] = src.y;
+}
+
+void put_nth(vec3& dst, int n, vec3_scalar src) {
+ dst.x[n] = src.x;
+ dst.y[n] = src.y;
+ dst.z[n] = src.z;
+}
+
+void put_nth(ivec4& dst, int n, ivec4_scalar src) {
+ dst.x[n] = src.x;
+ dst.y[n] = src.y;
+ dst.z[n] = src.z;
+ dst.w[n] = src.w;
+}
+
+void put_nth(vec4& dst, int n, vec4_scalar src) {
+ dst.x[n] = src.x;
+ dst.y[n] = src.y;
+ dst.z[n] = src.z;
+ dst.w[n] = src.w;
+}
+
+// Use an ElementType type constructor
+// so that we can implement element_type for
+// Int and Float
+template <typename V>
+struct ElementType {
+ typedef typename V::element_type ty;
+};
+
+template <>
+struct ElementType<float> {
+ typedef float ty;
+};
+
+template <>
+struct ElementType<int> {
+ typedef float ty;
+};
+
+template <>
+struct ElementType<Float> {
+ typedef float ty;
+};
+
+template <>
+struct ElementType<I32> {
+ typedef int32_t ty;
+};
+
+void put_nth_component(ivec2_scalar& dst, int n, int32_t src) {
+ switch (n) {
+ case 0:
+ dst.x = src;
+ break;
+ case 1:
+ dst.y = src;
+ break;
+ }
+}
+
+void put_nth_component(ivec4_scalar& dst, int n, int32_t src) {
+ switch (n) {
+ case 0:
+ dst.x = src;
+ break;
+ case 1:
+ dst.y = src;
+ break;
+ case 2:
+ dst.z = src;
+ break;
+ case 3:
+ dst.w = src;
+ break;
+ }
+}
+
+void put_nth_component(int& dst, int n, int src) {
+ switch (n) {
+ case 0:
+ dst = src;
+ break;
+ }
+}
+
+void put_nth_component(float& dst, int n, float src) {
+ switch (n) {
+ case 0:
+ dst = src;
+ break;
+ }
+}
+
+void put_nth_component(vec2_scalar& dst, int n, float src) {
+ switch (n) {
+ case 0:
+ dst.x = src;
+ break;
+ case 1:
+ dst.y = src;
+ break;
+ }
+}
+
+void put_nth_component(vec3_scalar& dst, int n, float src) {
+ switch (n) {
+ case 0:
+ dst.x = src;
+ break;
+ case 1:
+ dst.y = src;
+ break;
+ case 2:
+ dst.z = src;
+ break;
+ }
+}
+
+void put_nth_component(vec4_scalar& dst, int n, float src) {
+ switch (n) {
+ case 0:
+ dst.x = src;
+ break;
+ case 1:
+ dst.y = src;
+ break;
+ case 2:
+ dst.z = src;
+ break;
+ case 3:
+ dst.w = src;
+ break;
+ }
+}
+
+Float init_interp(float init0, float step) {
+ float init1 = init0 + step;
+ float init2 = init1 + step;
+ float init3 = init2 + step;
+ return {init0, init1, init2, init3};
+}
+
+vec2 init_interp(vec2_scalar init, vec2_scalar step) {
+ return vec2(init_interp(init.x, step.x), init_interp(init.y, step.y));
+}
+
+vec3 init_interp(vec3_scalar init, vec3_scalar step) {
+ return vec3(init_interp(init.x, step.x), init_interp(init.y, step.y),
+ init_interp(init.z, step.z));
+}
+
+vec4 init_interp(vec4_scalar init, vec4_scalar step) {
+ return vec4(init_interp(init.x, step.x), init_interp(init.y, step.y),
+ init_interp(init.z, step.z), init_interp(init.w, step.w));
+}
+
+template <typename T, size_t N>
+struct Array {
+ T elements[N];
+ T& operator[](size_t i) { return elements[i]; }
+ const T& operator[](size_t i) const { return elements[i]; }
+ template <typename S>
+ void convert(const Array<S, N>& s) {
+ for (size_t i = 0; i < N; ++i) elements[i] = T(s[i]);
+ }
+};
+
+template <size_t SIZE>
+Array<vec2, SIZE> if_then_else(I32 c, Array<vec2, SIZE> t,
+ Array<vec2, SIZE> e) {
+ Array<vec2, SIZE> r;
+ for (size_t i = 0; i < SIZE; i++) {
+ r[i] = if_then_else(c, t[i], e[i]);
+ }
+ return r;
+}
+
+} // namespace glsl
diff --git a/gfx/wr/swgl/src/lib.rs b/gfx/wr/swgl/src/lib.rs
new file mode 100644
index 0000000000..e8fc030e0c
--- /dev/null
+++ b/gfx/wr/swgl/src/lib.rs
@@ -0,0 +1,12 @@
+/* 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/. */
+
+#![crate_name = "swgl"]
+#![crate_type = "lib"]
+
+extern crate gleam;
+
+mod swgl_fns;
+
+pub use crate::swgl_fns::*;
diff --git a/gfx/wr/swgl/src/program.h b/gfx/wr/swgl/src/program.h
new file mode 100644
index 0000000000..9ea7c6dd6e
--- /dev/null
+++ b/gfx/wr/swgl/src/program.h
@@ -0,0 +1,186 @@
+/* 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/. */
+
+struct VertexAttrib;
+
+namespace glsl {
+
+// Type holding group of scalars interpolated across rasterized rows and spans,
+// shuttling values between vertex shaders and fragment shaders.
+// GCC requires power-of-two vector sizes, so must use glsl type as workaround
+// to operate in Float-sized chunks.
+typedef vec3 Interpolants;
+
+// Clip distances, if enabled, are always stored in the first SIMD chunk of the
+// interpolants.
+static ALWAYS_INLINE Float get_clip_distances(const Interpolants& interp) {
+ return interp.x;
+}
+
+struct VertexShaderImpl;
+struct FragmentShaderImpl;
+
+struct ProgramImpl {
+ virtual ~ProgramImpl() {}
+ virtual int get_uniform(const char* name) const = 0;
+ virtual void bind_attrib(const char* name, int index) = 0;
+ virtual int get_attrib(const char* name) const = 0;
+ virtual size_t interpolants_size() const = 0;
+ virtual VertexShaderImpl* get_vertex_shader() = 0;
+ virtual FragmentShaderImpl* get_fragment_shader() = 0;
+ virtual const char* get_name() const = 0;
+};
+
+typedef ProgramImpl* (*ProgramLoader)();
+
+// The maximum size of the gl_ClipDistance array.
+constexpr int32_t gl_MaxClipDistances = 4;
+
+struct VertexShaderImpl {
+ typedef void (*SetUniform1iFunc)(VertexShaderImpl*, int index, int value);
+ typedef void (*SetUniform4fvFunc)(VertexShaderImpl*, int index,
+ const float* value);
+ typedef void (*SetUniformMatrix4fvFunc)(VertexShaderImpl*, int index,
+ const float* value);
+ typedef void (*InitBatchFunc)(VertexShaderImpl*);
+ typedef void (*LoadAttribsFunc)(VertexShaderImpl*, VertexAttrib* attribs,
+ uint32_t start, int instance, int count);
+ typedef void (*RunPrimitiveFunc)(VertexShaderImpl*, char* interps,
+ size_t interp_stride);
+
+ SetUniform1iFunc set_uniform_1i_func = nullptr;
+ SetUniform4fvFunc set_uniform_4fv_func = nullptr;
+ SetUniformMatrix4fvFunc set_uniform_matrix4fv_func = nullptr;
+ InitBatchFunc init_batch_func = nullptr;
+ LoadAttribsFunc load_attribs_func = nullptr;
+ RunPrimitiveFunc run_primitive_func = nullptr;
+
+ enum FLAGS {
+ CLIP_DISTANCE = 1 << 0,
+ };
+ int flags = 0;
+ void enable_clip_distance() { flags |= CLIP_DISTANCE; }
+ ALWAYS_INLINE bool use_clip_distance() const {
+ return (flags & CLIP_DISTANCE) != 0;
+ }
+
+ vec4 gl_Position;
+ Float gl_ClipDistance[gl_MaxClipDistances];
+
+ void set_uniform_1i(int index, int value) {
+ (*set_uniform_1i_func)(this, index, value);
+ }
+
+ void set_uniform_4fv(int index, const float* value) {
+ (*set_uniform_4fv_func)(this, index, value);
+ }
+
+ void set_uniform_matrix4fv(int index, const float* value) {
+ (*set_uniform_matrix4fv_func)(this, index, value);
+ }
+
+ void init_batch() { (*init_batch_func)(this); }
+
+ ALWAYS_INLINE void load_attribs(VertexAttrib* attribs, uint32_t start,
+ int instance, int count) {
+ (*load_attribs_func)(this, attribs, start, instance, count);
+ }
+
+ ALWAYS_INLINE void run_primitive(char* interps, size_t interp_stride) {
+ (*run_primitive_func)(this, interps, interp_stride);
+ }
+};
+
+// The number of pixels in a step.
+constexpr int32_t swgl_StepSize = 4;
+
+struct FragmentShaderImpl {
+ typedef void (*InitSpanFunc)(FragmentShaderImpl*, const void* interps,
+ const void* step);
+ typedef void (*RunFunc)(FragmentShaderImpl*);
+ typedef void (*SkipFunc)(FragmentShaderImpl*, int steps);
+ typedef void (*InitSpanWFunc)(FragmentShaderImpl*, const void* interps,
+ const void* step);
+ typedef void (*RunWFunc)(FragmentShaderImpl*);
+ typedef void (*SkipWFunc)(FragmentShaderImpl*, int steps);
+ typedef int (*DrawSpanRGBA8Func)(FragmentShaderImpl*);
+ typedef int (*DrawSpanR8Func)(FragmentShaderImpl*);
+
+ InitSpanFunc init_span_func = nullptr;
+ RunFunc run_func = nullptr;
+ SkipFunc skip_func = nullptr;
+ InitSpanWFunc init_span_w_func = nullptr;
+ RunWFunc run_w_func = nullptr;
+ SkipWFunc skip_w_func = nullptr;
+ DrawSpanRGBA8Func draw_span_RGBA8_func = nullptr;
+ DrawSpanR8Func draw_span_R8_func = nullptr;
+
+ enum FLAGS {
+ DISCARD = 1 << 0,
+ PERSPECTIVE = 1 << 1,
+ };
+ int flags = 0;
+ void enable_discard() { flags |= DISCARD; }
+ void enable_perspective() { flags |= PERSPECTIVE; }
+ ALWAYS_INLINE bool use_discard() const { return (flags & DISCARD) != 0; }
+ ALWAYS_INLINE bool use_perspective() const {
+ return (flags & PERSPECTIVE) != 0;
+ }
+
+ vec4 gl_FragCoord;
+ vec4 gl_FragColor;
+ vec4 gl_SecondaryFragColor;
+
+ vec2_scalar swgl_StepZW;
+ Bool swgl_IsPixelDiscarded = false;
+ // The current buffer position for committing span output.
+ uint32_t* swgl_OutRGBA8 = nullptr;
+ uint8_t* swgl_OutR8 = nullptr;
+ // The remaining number of pixels in the span.
+ int32_t swgl_SpanLength = 0;
+
+ ALWAYS_INLINE void step_fragcoord(int steps = 4) { gl_FragCoord.x += steps; }
+
+ ALWAYS_INLINE void step_perspective(int steps = 4) {
+ gl_FragCoord.z += swgl_StepZW.x * steps;
+ gl_FragCoord.w += swgl_StepZW.y * steps;
+ }
+
+ template <bool W = false>
+ ALWAYS_INLINE void init_span(const void* interps, const void* step) {
+ (*(W ? init_span_w_func : init_span_func))(this, interps, step);
+ }
+
+ template <bool W = false>
+ ALWAYS_INLINE void run() {
+ (*(W ? run_w_func : run_func))(this);
+ }
+
+ template <bool W = false>
+ ALWAYS_INLINE void skip(int steps = 4) {
+ (*(W ? skip_w_func : skip_func))(this, steps);
+ }
+
+ ALWAYS_INLINE int draw_span(uint32_t* buf, int len) {
+ swgl_OutRGBA8 = buf;
+ swgl_SpanLength = len;
+ return (*draw_span_RGBA8_func)(this);
+ }
+
+ ALWAYS_INLINE bool has_draw_span(uint32_t*) {
+ return draw_span_RGBA8_func != nullptr;
+ }
+
+ ALWAYS_INLINE int draw_span(uint8_t* buf, int len) {
+ swgl_OutR8 = buf;
+ swgl_SpanLength = len;
+ return (*draw_span_R8_func)(this);
+ }
+
+ ALWAYS_INLINE bool has_draw_span(uint8_t*) {
+ return draw_span_R8_func != nullptr;
+ }
+};
+
+} // namespace glsl
diff --git a/gfx/wr/swgl/src/rasterize.h b/gfx/wr/swgl/src/rasterize.h
new file mode 100644
index 0000000000..9b49f25315
--- /dev/null
+++ b/gfx/wr/swgl/src/rasterize.h
@@ -0,0 +1,1680 @@
+/* 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/. */
+
+// The SWGL depth buffer is roughly organized as a span buffer where each row
+// of the depth buffer is a list of spans, and each span has a constant depth
+// and a run length (represented by DepthRun). The span from start..start+count
+// is placed directly at that start index in the row's array of runs, so that
+// there is no need to explicitly record the start index at all. This also
+// avoids the need to move items around in the run array to manage insertions
+// since space is implicitly always available for a run between any two
+// pre-existing runs. Linkage from one run to the next is implicitly defined by
+// the count, so if a run exists from start..start+count, the next run will
+// implicitly pick up right at index start+count where that preceding run left
+// off. All of the DepthRun items that are after the head of the run can remain
+// uninitialized until the run needs to be split and a new run needs to start
+// somewhere in between.
+// For uses like perspective-correct rasterization or with a discard mask, a
+// run is not an efficient representation, and it is more beneficial to have
+// a flattened array of individual depth samples that can be masked off easily.
+// To support this case, the first run in a given row's run array may have a
+// zero count, signaling that this entire row is flattened. Critically, the
+// depth and count fields in DepthRun are ordered (endian-dependently) so that
+// the DepthRun struct can be interpreted as a sign-extended int32_t depth. It
+// is then possible to just treat the entire row as an array of int32_t depth
+// samples that can be processed with SIMD comparisons, since the count field
+// behaves as just the sign-extension of the depth field. The count field is
+// limited to 8 bits so that we can support depth values up to 24 bits.
+// When a depth buffer is cleared, each row is initialized to a maximal runs
+// spanning the entire row. In the normal case, the depth buffer will continue
+// to manage itself as a list of runs. If perspective or discard is used for
+// a given row, the row will be converted to the flattened representation to
+// support it, after which it will only ever revert back to runs if the depth
+// buffer is cleared.
+
+// The largest 24-bit depth value supported.
+constexpr uint32_t MAX_DEPTH_VALUE = 0xFFFFFF;
+// The longest 8-bit depth run that is supported, aligned to SIMD chunk size.
+constexpr uint32_t MAX_DEPTH_RUN = 255 & ~3;
+
+struct DepthRun {
+ // Ensure that depth always occupies the LSB and count the MSB so that we
+ // can sign-extend depth just by setting count to zero, marking it flat.
+ // When count is non-zero, then this is interpreted as an actual run and
+ // depth is read in isolation.
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+ uint32_t depth : 24;
+ uint32_t count : 8;
+#else
+ uint32_t count : 8;
+ uint32_t depth : 24;
+#endif
+
+ DepthRun() = default;
+ DepthRun(uint32_t depth, uint8_t count) : depth(depth), count(count) {}
+
+ // If count is zero, this is actually a flat depth sample rather than a run.
+ bool is_flat() const { return !count; }
+
+ // Compare a source depth from rasterization with a stored depth value.
+ template <int FUNC>
+ ALWAYS_INLINE bool compare(uint32_t src) const {
+ switch (FUNC) {
+ case GL_LEQUAL:
+ return src <= depth;
+ case GL_LESS:
+ return src < depth;
+ case GL_ALWAYS:
+ return true;
+ default:
+ assert(false);
+ return false;
+ }
+ }
+};
+
+// Fills runs at the given position with the given depth up to the span width.
+static ALWAYS_INLINE void set_depth_runs(DepthRun* runs, uint32_t depth,
+ uint32_t width) {
+ // If the width exceeds the maximum run size, then we need to output clamped
+ // runs first.
+ for (; width >= MAX_DEPTH_RUN;
+ runs += MAX_DEPTH_RUN, width -= MAX_DEPTH_RUN) {
+ *runs = DepthRun(depth, MAX_DEPTH_RUN);
+ }
+ // If there are still any left over samples to fill under the maximum run
+ // size, then output one last run for them.
+ if (width > 0) {
+ *runs = DepthRun(depth, width);
+ }
+}
+
+// A cursor for reading and modifying a row's depth run array. It locates
+// and iterates through a desired span within all the runs, testing if
+// the depth of this span passes or fails the depth test against existing
+// runs. If desired, new runs may be inserted to represent depth occlusion
+// from this span in the run array.
+struct DepthCursor {
+ // Current position of run the cursor has advanced to.
+ DepthRun* cur = nullptr;
+ // The start of the remaining potential samples in the desired span.
+ DepthRun* start = nullptr;
+ // The end of the potential samples in the desired span.
+ DepthRun* end = nullptr;
+
+ DepthCursor() = default;
+
+ // Construct a cursor with runs for a given row's run array and the bounds
+ // of the span we wish to iterate within it.
+ DepthCursor(DepthRun* runs, int num_runs, int span_offset, int span_count)
+ : cur(runs), start(&runs[span_offset]), end(start + span_count) {
+ // This cursor should never iterate over flat runs
+ assert(!runs->is_flat());
+ DepthRun* end_runs = &runs[num_runs];
+ // Clamp end of span to end of row
+ if (end > end_runs) {
+ end = end_runs;
+ }
+ // If the span starts past the end of the row, just advance immediately
+ // to it to signal that we're done.
+ if (start >= end_runs) {
+ cur = end_runs;
+ start = end_runs;
+ return;
+ }
+ // Otherwise, find the first depth run that contains the start of the span.
+ // If the span starts after the given run, then we need to keep searching
+ // through the row to find an appropriate run. The check above already
+ // guaranteed that the span starts within the row's runs, and the search
+ // won't fall off the end.
+ for (;;) {
+ assert(cur < end);
+ DepthRun* next = cur + cur->count;
+ if (start < next) {
+ break;
+ }
+ cur = next;
+ }
+ }
+
+ // The cursor is valid if the current position is at the end or if the run
+ // contains the start position.
+ bool valid() const {
+ return cur >= end || (cur <= start && start < cur + cur->count);
+ }
+
+ // Skip past any initial runs that fail the depth test. If we find a run that
+ // would pass, then return the accumulated length between where we started
+ // and that position. Otherwise, if we fall off the end, return -1 to signal
+ // that there are no more passed runs at the end of this failed region and
+ // so it is safe for the caller to stop processing any more regions in this
+ // row.
+ template <int FUNC>
+ int skip_failed(uint32_t val) {
+ assert(valid());
+ DepthRun* prev = start;
+ while (cur < end) {
+ if (cur->compare<FUNC>(val)) {
+ return start - prev;
+ }
+ cur += cur->count;
+ start = cur;
+ }
+ return -1;
+ }
+
+ // Helper to convert function parameters into template parameters to hoist
+ // some checks out of inner loops.
+ ALWAYS_INLINE int skip_failed(uint32_t val, GLenum func) {
+ switch (func) {
+ case GL_LEQUAL:
+ return skip_failed<GL_LEQUAL>(val);
+ case GL_LESS:
+ return skip_failed<GL_LESS>(val);
+ default:
+ assert(false);
+ return -1;
+ }
+ }
+
+ // Find a region of runs that passes the depth test. It is assumed the caller
+ // has called skip_failed first to skip past any runs that failed the depth
+ // test. This stops when it finds a run that fails the depth test or we fall
+ // off the end of the row. If the write mask is enabled, this will insert runs
+ // to represent this new region that passed the depth test. The length of the
+ // region is returned.
+ template <int FUNC, bool MASK>
+ int check_passed(uint32_t val) {
+ assert(valid());
+ DepthRun* prev = cur;
+ while (cur < end) {
+ if (!cur->compare<FUNC>(val)) {
+ break;
+ }
+ DepthRun* next = cur + cur->count;
+ if (next > end) {
+ if (MASK) {
+ // Chop the current run where the end of the span falls, making a new
+ // run from the end of the span till the next run. The beginning of
+ // the current run will be folded into the run from the start of the
+ // passed region before returning below.
+ *end = DepthRun(cur->depth, next - end);
+ }
+ // If the next run starts past the end, then just advance the current
+ // run to the end to signal that we're now at the end of the row.
+ next = end;
+ }
+ cur = next;
+ }
+ // If we haven't advanced past the start of the span region, then we found
+ // nothing that passed.
+ if (cur <= start) {
+ return 0;
+ }
+ // If 'end' fell within the middle of a passing run, then 'cur' will end up
+ // pointing at the new partial run created at 'end' where the passing run
+ // was split to accommodate starting in the middle. The preceding runs will
+ // be fixed below to properly join with this new split.
+ int passed = cur - start;
+ if (MASK) {
+ // If the search started from a run before the start of the span, then
+ // edit that run to meet up with the start.
+ if (prev < start) {
+ prev->count = start - prev;
+ }
+ // Create a new run for the entirety of the passed samples.
+ set_depth_runs(start, val, passed);
+ }
+ start = cur;
+ return passed;
+ }
+
+ // Helper to convert function parameters into template parameters to hoist
+ // some checks out of inner loops.
+ template <bool MASK>
+ ALWAYS_INLINE int check_passed(uint32_t val, GLenum func) {
+ switch (func) {
+ case GL_LEQUAL:
+ return check_passed<GL_LEQUAL, MASK>(val);
+ case GL_LESS:
+ return check_passed<GL_LESS, MASK>(val);
+ default:
+ assert(false);
+ return 0;
+ }
+ }
+
+ ALWAYS_INLINE int check_passed(uint32_t val, GLenum func, bool mask) {
+ return mask ? check_passed<true>(val, func)
+ : check_passed<false>(val, func);
+ }
+
+ // Fill a region of runs with a given depth value, bypassing any depth test.
+ ALWAYS_INLINE void fill(uint32_t depth) {
+ check_passed<GL_ALWAYS, true>(depth);
+ }
+};
+
+// Initialize a depth texture by setting the first run in each row to encompass
+// the entire row.
+void Texture::init_depth_runs(uint32_t depth) {
+ if (!buf) return;
+ DepthRun* runs = (DepthRun*)buf;
+ for (int y = 0; y < height; y++) {
+ set_depth_runs(runs, depth, width);
+ runs += stride() / sizeof(DepthRun);
+ }
+ set_cleared(true);
+}
+
+// Fill a portion of the run array with flattened depth samples.
+static ALWAYS_INLINE void fill_flat_depth(DepthRun* dst, size_t n,
+ uint32_t depth) {
+ fill_n((uint32_t*)dst, n, depth);
+}
+
+// Fills a scissored region of a depth texture with a given depth.
+void Texture::fill_depth_runs(uint32_t depth, const IntRect& scissor) {
+ if (!buf) return;
+ assert(cleared());
+ IntRect bb = bounds().intersection(scissor - offset);
+ DepthRun* runs = (DepthRun*)sample_ptr(0, bb.y0);
+ for (int rows = bb.height(); rows > 0; rows--) {
+ if (bb.width() >= width) {
+ // If the scissor region encompasses the entire row, reset the row to a
+ // single run encompassing the entire row.
+ set_depth_runs(runs, depth, width);
+ } else if (runs->is_flat()) {
+ // If the row is flattened, just directly fill the portion of the row.
+ fill_flat_depth(&runs[bb.x0], bb.width(), depth);
+ } else {
+ // Otherwise, if we are still using runs, then set up a cursor to fill
+ // it with depth runs.
+ DepthCursor(runs, width, bb.x0, bb.width()).fill(depth);
+ }
+ runs += stride() / sizeof(DepthRun);
+ }
+}
+
+using ZMask = I32;
+
+#if USE_SSE2
+# define ZMASK_NONE_PASSED 0xFFFF
+# define ZMASK_ALL_PASSED 0
+static inline uint32_t zmask_code(ZMask mask) {
+ return _mm_movemask_epi8(mask);
+}
+#else
+# define ZMASK_NONE_PASSED 0xFFFFFFFFU
+# define ZMASK_ALL_PASSED 0
+static inline uint32_t zmask_code(ZMask mask) {
+ return bit_cast<uint32_t>(CONVERT(mask, U8));
+}
+#endif
+
+// Interprets items in the depth buffer as sign-extended 32-bit depth values
+// instead of as runs. Returns a mask that signals which samples in the given
+// chunk passed or failed the depth test with given Z value.
+template <bool DISCARD>
+static ALWAYS_INLINE bool check_depth(I32 src, DepthRun* zbuf, ZMask& outmask,
+ int span = 4) {
+ // SSE2 does not support unsigned comparison. So ensure Z value is
+ // sign-extended to int32_t.
+ I32 dest = unaligned_load<I32>(zbuf);
+ // Invert the depth test to check which pixels failed and should be discarded.
+ ZMask mask = ctx->depthfunc == GL_LEQUAL
+ ?
+ // GL_LEQUAL: Not(LessEqual) = Greater
+ ZMask(src > dest)
+ :
+ // GL_LESS: Not(Less) = GreaterEqual
+ ZMask(src >= dest);
+ // Mask off any unused lanes in the span.
+ mask |= ZMask(span) < ZMask{1, 2, 3, 4};
+ if (zmask_code(mask) == ZMASK_NONE_PASSED) {
+ return false;
+ }
+ if (!DISCARD && ctx->depthmask) {
+ unaligned_store(zbuf, (mask & dest) | (~mask & src));
+ }
+ outmask = mask;
+ return true;
+}
+
+static ALWAYS_INLINE I32 packDepth() {
+ return cast(fragment_shader->gl_FragCoord.z * MAX_DEPTH_VALUE);
+}
+
+static ALWAYS_INLINE void discard_depth(I32 src, DepthRun* zbuf, I32 mask) {
+ if (ctx->depthmask) {
+ I32 dest = unaligned_load<I32>(zbuf);
+ mask |= fragment_shader->swgl_IsPixelDiscarded;
+ unaligned_store(zbuf, (mask & dest) | (~mask & src));
+ }
+}
+
+static ALWAYS_INLINE void mask_output(uint32_t* buf, ZMask zmask,
+ int span = 4) {
+ WideRGBA8 r = pack_pixels_RGBA8();
+ PackedRGBA8 dst = load_span<PackedRGBA8>(buf, span);
+ if (blend_key) r = blend_pixels(buf, dst, r, span);
+ PackedRGBA8 mask = bit_cast<PackedRGBA8>(zmask);
+ store_span(buf, (mask & dst) | (~mask & pack(r)), span);
+}
+
+template <bool DISCARD>
+static ALWAYS_INLINE void discard_output(uint32_t* buf, int span = 4) {
+ mask_output(buf, fragment_shader->swgl_IsPixelDiscarded, span);
+}
+
+template <>
+ALWAYS_INLINE void discard_output<false>(uint32_t* buf, int span) {
+ WideRGBA8 r = pack_pixels_RGBA8();
+ if (blend_key)
+ r = blend_pixels(buf, load_span<PackedRGBA8>(buf, span), r, span);
+ store_span(buf, pack(r), span);
+}
+
+static ALWAYS_INLINE void mask_output(uint8_t* buf, ZMask zmask, int span = 4) {
+ WideR8 r = pack_pixels_R8();
+ WideR8 dst = unpack(load_span<PackedR8>(buf, span));
+ if (blend_key) r = blend_pixels(buf, dst, r, span);
+ WideR8 mask = packR8(zmask);
+ store_span(buf, pack((mask & dst) | (~mask & r)), span);
+}
+
+template <bool DISCARD>
+static ALWAYS_INLINE void discard_output(uint8_t* buf, int span = 4) {
+ mask_output(buf, fragment_shader->swgl_IsPixelDiscarded, span);
+}
+
+template <>
+ALWAYS_INLINE void discard_output<false>(uint8_t* buf, int span) {
+ WideR8 r = pack_pixels_R8();
+ if (blend_key)
+ r = blend_pixels(buf, unpack(load_span<PackedR8>(buf, span)), r, span);
+ store_span(buf, pack(r), span);
+}
+
+struct ClipRect {
+ float x0;
+ float y0;
+ float x1;
+ float y1;
+
+ explicit ClipRect(const IntRect& i)
+ : x0(i.x0), y0(i.y0), x1(i.x1), y1(i.y1) {}
+ explicit ClipRect(const Texture& t) : ClipRect(ctx->apply_scissor(t)) {
+ // If blending is enabled, set blend_key to reflect the resolved blend
+ // state for the currently drawn primitive.
+ if (ctx->blend) {
+ blend_key = ctx->blend_key;
+ if (swgl_ClipFlags) {
+ // If there is a blend override set, replace the blend key with it.
+ if (swgl_ClipFlags & SWGL_CLIP_FLAG_BLEND_OVERRIDE) {
+ blend_key = swgl_BlendOverride;
+ }
+ // If a clip mask is available, set up blending state to use the clip
+ // mask.
+ if (swgl_ClipFlags & SWGL_CLIP_FLAG_MASK) {
+ assert(swgl_ClipMask->format == TextureFormat::R8);
+ // Constrain the clip mask bounds to always fall within the clip mask.
+ swgl_ClipMaskBounds.intersect(IntRect{0, 0, int(swgl_ClipMask->width),
+ int(swgl_ClipMask->height)});
+ // The clip mask offset is relative to the viewport.
+ swgl_ClipMaskOffset += ctx->viewport.origin() - t.offset;
+ // The clip mask bounds are relative to the clip mask offset.
+ swgl_ClipMaskBounds.offset(swgl_ClipMaskOffset);
+ // Finally, constrain the clip rectangle by the clip mask bounds.
+ intersect(swgl_ClipMaskBounds);
+ // Modify the blend key so that it will use the clip mask while
+ // blending.
+ restore_clip_mask();
+ }
+ if (swgl_ClipFlags & SWGL_CLIP_FLAG_AA) {
+ // Modify the blend key so that it will use AA while blending.
+ restore_aa();
+ }
+ }
+ } else {
+ blend_key = BLEND_KEY_NONE;
+ swgl_ClipFlags = 0;
+ }
+ }
+
+ FloatRange x_range() const { return {x0, x1}; }
+
+ void intersect(const IntRect& c) {
+ x0 = max(x0, float(c.x0));
+ y0 = max(y0, float(c.y0));
+ x1 = min(x1, float(c.x1));
+ y1 = min(y1, float(c.y1));
+ }
+
+ template <typename P>
+ void set_clip_mask(int x, int y, P* buf) const {
+ if (swgl_ClipFlags & SWGL_CLIP_FLAG_MASK) {
+ swgl_SpanBuf = buf;
+ swgl_ClipMaskBuf = (uint8_t*)swgl_ClipMask->buf +
+ (y - swgl_ClipMaskOffset.y) * swgl_ClipMask->stride +
+ (x - swgl_ClipMaskOffset.x);
+ }
+ }
+
+ template <typename P>
+ bool overlaps(int nump, const P* p) const {
+ // Generate a mask of which side of the clip rect all of a polygon's points
+ // fall inside of. This is a cheap conservative estimate of whether the
+ // bounding box of the polygon might overlap the clip rect, rather than an
+ // exact test that would require multiple slower line intersections.
+ int sides = 0;
+ for (int i = 0; i < nump; i++) {
+ sides |= p[i].x < x1 ? (p[i].x > x0 ? 1 | 2 : 1) : 2;
+ sides |= p[i].y < y1 ? (p[i].y > y0 ? 4 | 8 : 4) : 8;
+ }
+ return sides == 0xF;
+ }
+};
+
+// Given a current X position at the center Y position of a row, return the X
+// position of the left and right intercepts of the row top and bottom.
+template <typename E>
+static ALWAYS_INLINE FloatRange x_intercepts(const E& e) {
+ float rad = 0.5f * abs(e.x_slope());
+ return {e.cur_x() - rad, e.cur_x() + rad};
+}
+
+// Return the AA sub-span corresponding to a given edge. If AA is requested,
+// then this finds the X intercepts with the row clipped into range of the
+// edge and finally conservatively rounds them out. If there is no AA, then
+// it just returns the current rounded X position clipped within bounds.
+template <typename E>
+static ALWAYS_INLINE IntRange aa_edge(const E& e, const FloatRange& bounds) {
+ return e.edgeMask ? bounds.clip(x_intercepts(e)).round_out()
+ : bounds.clip({e.cur_x(), e.cur_x()}).round();
+}
+
+// Calculate the initial AA coverage as an approximation of the distance from
+// the center of the pixel in the direction of the edge slope. Given an edge
+// (x,y)..(x+dx,y+dy), then the normalized tangent vector along the edge is
+// (dx,dy)/sqrt(dx^2+dy^2). We know that for dy=1 then dx=e.x_slope. We rotate
+// the tangent vector either -90 or +90 degrees to get the edge normal vector,
+// where 'dx=-dy and 'dy=dx. Once normalized by 1/sqrt(dx^2+dy^2), scale into
+// the range of 0..256 so that we can cheaply convert to a fixed-point scale
+// factor. It is assumed that at exactly the pixel center the opacity is half
+// (128) and linearly decreases along the normal vector at 1:1 scale with the
+// slope. While not entirely accurate, this gives a reasonably agreeable looking
+// approximation of AA. For edges on which there is no AA, just force the
+// opacity to maximum (256) with no slope, relying on the span clipping to trim
+// pixels outside the span.
+template <typename E>
+static ALWAYS_INLINE FloatRange aa_dist(const E& e, float dir) {
+ if (e.edgeMask) {
+ float dx = (dir * 256.0f) * inversesqrt(1.0f + e.x_slope() * e.x_slope());
+ return {128.0f + dx * (e.cur_x() - 0.5f), -dx};
+ } else {
+ return {256.0f, 0.0f};
+ }
+}
+
+template <typename P, typename E>
+static ALWAYS_INLINE IntRange aa_span(P* buf, const E& left, const E& right,
+ const FloatRange& bounds) {
+ // If there is no AA, just return the span from the rounded left edge X
+ // position to the rounded right edge X position. Clip the span to be within
+ // the valid bounds.
+ if (!(swgl_ClipFlags & SWGL_CLIP_FLAG_AA)) {
+ return bounds.clip({left.cur_x(), right.cur_x()}).round();
+ }
+
+ // Calculate the left and right AA spans along with the coverage distances
+ // and slopes necessary to do blending.
+ IntRange leftAA = aa_edge(left, bounds);
+ FloatRange leftDist = aa_dist(left, -1.0f);
+ IntRange rightAA = aa_edge(right, bounds);
+ FloatRange rightDist = aa_dist(right, 1.0f);
+
+ // Use the pointer into the destination buffer as a status indicator of the
+ // coverage offset. The pointer is calculated so that subtracting it with
+ // the current destination pointer will yield a negative value if the span
+ // is outside the opaque area and otherwise will yield a positive value
+ // above the opaque size. This pointer is stored as a uint8 pointer so that
+ // there are no hidden multiplication instructions and will just return a
+ // 1:1 linear memory address. Thus the size of the opaque region must also
+ // be scaled by the pixel size in bytes.
+ swgl_OpaqueStart = (const uint8_t*)(buf + leftAA.end);
+ swgl_OpaqueSize = max(rightAA.start - leftAA.end - 3, 0) * sizeof(P);
+
+ // Offset the coverage distances by the end of the left AA span, which
+ // corresponds to the opaque start pointer, so that pixels become opaque
+ // immediately after. The distances are also offset for each lane in the
+ // chunk.
+ Float offset = cast(leftAA.end + (I32){0, 1, 2, 3});
+ swgl_LeftAADist = leftDist.start + offset * leftDist.end;
+ swgl_RightAADist = rightDist.start + offset * rightDist.end;
+ swgl_AASlope =
+ (Float){leftDist.end, rightDist.end, 0.0f, 0.0f} / float(sizeof(P));
+
+ // Return the full span width from the start of the left span to the end of
+ // the right span.
+ return {leftAA.start, rightAA.end};
+}
+
+// Calculate the span the user clip distances occupy from the left and right
+// edges at the current row.
+template <typename E>
+static ALWAYS_INLINE IntRange clip_distance_range(const E& left,
+ const E& right) {
+ Float leftClip = get_clip_distances(left.interp);
+ Float rightClip = get_clip_distances(right.interp);
+ // Get the change in clip dist per X step.
+ Float clipStep = (rightClip - leftClip) / (right.cur_x() - left.cur_x());
+ // Find the zero intercepts starting from the left edge.
+ Float clipDist =
+ clamp(left.cur_x() - leftClip * recip(clipStep), 0.0f, 1.0e6f);
+ // Find the distance to the start of the span for any clip distances that
+ // are increasing in value. If the clip distance is constant or decreasing
+ // in value, then check if it starts outside the clip volume.
+ Float start = if_then_else(clipStep > 0.0f, clipDist,
+ if_then_else(leftClip < 0.0f, 1.0e6f, 0.0f));
+ // Find the distance to the end of the span for any clip distances that are
+ // decreasing in value. If the clip distance is constant or increasing in
+ // value, then check if it ends inside the clip volume.
+ Float end = if_then_else(clipStep < 0.0f, clipDist,
+ if_then_else(rightClip >= 0.0f, 1.0e6f, 0.0f));
+ // Find the furthest start offset.
+ start = max(start, start.zwxy);
+ // Find the closest end offset.
+ end = min(end, end.zwxy);
+ // Finally, round the offsets to an integer span that can be used to bound
+ // the current span.
+ return FloatRange{max(start.x, start.y), min(end.x, end.y)}.round();
+}
+
+// Converts a run array into a flattened array of depth samples. This just
+// walks through every run and fills the samples with the depth value from
+// the run.
+static void flatten_depth_runs(DepthRun* runs, size_t width) {
+ if (runs->is_flat()) {
+ return;
+ }
+ while (width > 0) {
+ size_t n = runs->count;
+ fill_flat_depth(runs, n, runs->depth);
+ runs += n;
+ width -= n;
+ }
+}
+
+// Helper function for drawing passed depth runs within the depth buffer.
+// Flattened depth (perspective or discard) is not supported.
+template <typename P>
+static ALWAYS_INLINE void draw_depth_span(uint32_t z, P* buf,
+ DepthCursor& cursor) {
+ for (;;) {
+ // Get the span that passes the depth test. Assume on entry that
+ // any failed runs have already been skipped.
+ int span = cursor.check_passed(z, ctx->depthfunc, ctx->depthmask);
+ // If nothing passed, since we already skipped passed failed runs
+ // previously, we must have hit the end of the row. Bail out.
+ if (span <= 0) {
+ break;
+ }
+ if (span >= 4) {
+ // If we have a draw specialization, try to process as many 4-pixel
+ // chunks as possible using it.
+ if (fragment_shader->has_draw_span(buf)) {
+ int drawn = fragment_shader->draw_span(buf, span & ~3);
+ buf += drawn;
+ span -= drawn;
+ }
+ // Otherwise, just process each chunk individually.
+ while (span >= 4) {
+ fragment_shader->run();
+ discard_output<false>(buf);
+ buf += 4;
+ span -= 4;
+ }
+ }
+ // If we have a partial chunk left over, we still have to process it as if
+ // it were a full chunk. Mask off only the part of the chunk we want to
+ // use.
+ if (span > 0) {
+ fragment_shader->run();
+ discard_output<false>(buf, span);
+ buf += span;
+ }
+ // Skip past any runs that fail the depth test.
+ int skip = cursor.skip_failed(z, ctx->depthfunc);
+ // If there aren't any, that means we won't encounter any more passing runs
+ // and so it's safe to bail out.
+ if (skip <= 0) {
+ break;
+ }
+ // Advance interpolants for the fragment shader past the skipped region.
+ // If we processed a partial chunk above, we actually advanced the
+ // interpolants a full chunk in the fragment shader's run function. Thus,
+ // we need to first subtract off that 4-pixel chunk and only partially
+ // advance them to that partial chunk before we can add on the rest of the
+ // skips. This is combined with the skip here for efficiency's sake.
+ fragment_shader->skip(skip - (span > 0 ? 4 - span : 0));
+ buf += skip;
+ }
+}
+
+// Draw a simple span in 4-pixel wide chunks, optionally using depth.
+template <bool DISCARD, bool W, typename P, typename Z>
+static ALWAYS_INLINE void draw_span(P* buf, DepthRun* depth, int span, Z z) {
+ if (depth) {
+ // Depth testing is enabled. If perspective is used, Z values will vary
+ // across the span, we use packDepth to generate packed Z values suitable
+ // for depth testing based on current values from gl_FragCoord.z.
+ // Otherwise, for the no-perspective case, we just use the provided Z.
+ // Process 4-pixel chunks first.
+ for (; span >= 4; span -= 4, buf += 4, depth += 4) {
+ I32 zsrc = z();
+ ZMask zmask;
+ if (check_depth<DISCARD>(zsrc, depth, zmask)) {
+ fragment_shader->run<W>();
+ mask_output(buf, zmask);
+ if (DISCARD) discard_depth(zsrc, depth, zmask);
+ } else {
+ fragment_shader->skip<W>();
+ }
+ }
+ // If there are any remaining pixels, do a partial chunk.
+ if (span > 0) {
+ I32 zsrc = z();
+ ZMask zmask;
+ if (check_depth<DISCARD>(zsrc, depth, zmask, span)) {
+ fragment_shader->run<W>();
+ mask_output(buf, zmask, span);
+ if (DISCARD) discard_depth(zsrc, depth, zmask);
+ }
+ }
+ } else {
+ // Process 4-pixel chunks first.
+ for (; span >= 4; span -= 4, buf += 4) {
+ fragment_shader->run<W>();
+ discard_output<DISCARD>(buf);
+ }
+ // If there are any remaining pixels, do a partial chunk.
+ if (span > 0) {
+ fragment_shader->run<W>();
+ discard_output<DISCARD>(buf, span);
+ }
+ }
+}
+
+// Called during rasterization to forcefully clear a row on which delayed clear
+// has been enabled. If we know that we are going to completely overwrite a part
+// of the row, then we only need to clear the row outside of that part. However,
+// if blending or discard is enabled, the values of that underlying part of the
+// row may be used regardless to produce the final rasterization result, so we
+// have to then clear the entire underlying row to prepare it.
+template <typename P>
+static inline void prepare_row(Texture& colortex, int y, int startx, int endx,
+ bool use_discard, DepthRun* depth,
+ uint32_t z = 0, DepthCursor* cursor = nullptr) {
+ assert(colortex.delay_clear > 0);
+ // Delayed clear is enabled for the color buffer. Check if needs clear.
+ uint32_t& mask = colortex.cleared_rows[y / 32];
+ if ((mask & (1 << (y & 31))) == 0) {
+ mask |= 1 << (y & 31);
+ colortex.delay_clear--;
+ if (blend_key || use_discard) {
+ // If depth test, blending, or discard is used, old color values
+ // might be sampled, so we need to clear the entire row to fill it.
+ force_clear_row<P>(colortex, y);
+ } else if (depth) {
+ if (depth->is_flat() || !cursor) {
+ // If flat depth is used, we can't cheaply predict if which samples will
+ // pass.
+ force_clear_row<P>(colortex, y);
+ } else {
+ // Otherwise if depth runs are used, see how many samples initially pass
+ // the depth test and only fill the row outside those. The fragment
+ // shader will fill the row within the passed samples.
+ int passed =
+ DepthCursor(*cursor).check_passed<false>(z, ctx->depthfunc);
+ if (startx > 0 || startx + passed < colortex.width) {
+ force_clear_row<P>(colortex, y, startx, startx + passed);
+ }
+ }
+ } else if (startx > 0 || endx < colortex.width) {
+ // Otherwise, we only need to clear the row outside of the span.
+ // The fragment shader will fill the row within the span itself.
+ force_clear_row<P>(colortex, y, startx, endx);
+ }
+ }
+}
+
+// Perpendicular dot-product is the dot-product of a vector with the
+// perpendicular vector of the other, i.e. dot(a, {-b.y, b.x})
+template <typename T>
+static ALWAYS_INLINE auto perpDot(T a, T b) {
+ return a.x * b.y - a.y * b.x;
+}
+
+// Check if the winding of the initial edges is flipped, requiring us to swap
+// the edges to avoid spans having negative lengths. Assume that l0.y == r0.y
+// due to the initial edge scan in draw_quad/perspective_spans.
+template <typename T>
+static ALWAYS_INLINE bool checkIfEdgesFlipped(T l0, T l1, T r0, T r1) {
+ // If the starting point of the left edge is to the right of the starting
+ // point of the right edge, then just assume the edges are flipped. If the
+ // left and right starting points are the same, then check the sign of the
+ // cross-product of the edges to see if the edges are flipped. Otherwise,
+ // if the left starting point is actually just to the left of the right
+ // starting point, then assume no edge flip.
+ return l0.x > r0.x || (l0.x == r0.x && perpDot(l1 - l0, r1 - r0) > 0.0f);
+}
+
+// Draw spans for each row of a given quad (or triangle) with a constant Z
+// value. The quad is assumed convex. It is clipped to fall within the given
+// clip rect. In short, this function rasterizes a quad by first finding a
+// top most starting point and then from there tracing down the left and right
+// sides of this quad until it hits the bottom, outputting a span between the
+// current left and right positions at each row along the way. Points are
+// assumed to be ordered in either CW or CCW to support this, but currently
+// both orders (CW and CCW) are supported and equivalent.
+template <typename P>
+static inline void draw_quad_spans(int nump, Point2D p[4], uint32_t z,
+ Interpolants interp_outs[4],
+ Texture& colortex, Texture& depthtex,
+ const ClipRect& clipRect) {
+ // Only triangles and convex quads supported.
+ assert(nump == 3 || nump == 4);
+
+ Point2D l0, r0, l1, r1;
+ int l0i, r0i, l1i, r1i;
+ {
+ // Find the index of the top-most (smallest Y) point from which
+ // rasterization can start.
+ int top = nump > 3 && p[3].y < p[2].y
+ ? (p[0].y < p[1].y ? (p[0].y < p[3].y ? 0 : 3)
+ : (p[1].y < p[3].y ? 1 : 3))
+ : (p[0].y < p[1].y ? (p[0].y < p[2].y ? 0 : 2)
+ : (p[1].y < p[2].y ? 1 : 2));
+ // Helper to find next index in the points array, walking forward.
+#define NEXT_POINT(idx) \
+ ({ \
+ int cur = (idx) + 1; \
+ cur < nump ? cur : 0; \
+ })
+ // Helper to find the previous index in the points array, walking backward.
+#define PREV_POINT(idx) \
+ ({ \
+ int cur = (idx)-1; \
+ cur >= 0 ? cur : nump - 1; \
+ })
+ // Start looking for "left"-side and "right"-side descending edges starting
+ // from the determined top point.
+ int next = NEXT_POINT(top);
+ int prev = PREV_POINT(top);
+ if (p[top].y == p[next].y) {
+ // If the next point is on the same row as the top, then advance one more
+ // time to the next point and use that as the "left" descending edge.
+ l0i = next;
+ l1i = NEXT_POINT(next);
+ // Assume top and prev form a descending "right" edge, as otherwise this
+ // will be a collapsed polygon and harmlessly bail out down below.
+ r0i = top;
+ r1i = prev;
+ } else if (p[top].y == p[prev].y) {
+ // If the prev point is on the same row as the top, then advance to the
+ // prev again and use that as the "right" descending edge.
+ // Assume top and next form a non-empty descending "left" edge.
+ l0i = top;
+ l1i = next;
+ r0i = prev;
+ r1i = PREV_POINT(prev);
+ } else {
+ // Both next and prev are on distinct rows from top, so both "left" and
+ // "right" edges are non-empty/descending.
+ l0i = r0i = top;
+ l1i = next;
+ r1i = prev;
+ }
+ // Load the points from the indices.
+ l0 = p[l0i]; // Start of left edge
+ r0 = p[r0i]; // End of left edge
+ l1 = p[l1i]; // Start of right edge
+ r1 = p[r1i]; // End of right edge
+ // debugf("l0: %d(%f,%f), r0: %d(%f,%f) -> l1: %d(%f,%f), r1:
+ // %d(%f,%f)\n", l0i, l0.x, l0.y, r0i, r0.x, r0.y, l1i, l1.x, l1.y, r1i,
+ // r1.x, r1.y);
+ }
+
+ struct Edge {
+ float yScale;
+ float xSlope;
+ float x;
+ Interpolants interpSlope;
+ Interpolants interp;
+ bool edgeMask;
+
+ Edge(float y, const Point2D& p0, const Point2D& p1, const Interpolants& i0,
+ const Interpolants& i1, int edgeIndex)
+ : // Inverse Y scale for slope calculations. Avoid divide on 0-length
+ // edge. Later checks below ensure that Y <= p1.y, or otherwise we
+ // don't use this edge. We just need to guard against Y == p1.y ==
+ // p0.y. In that case, Y - p0.y == 0 and will cancel out the slopes
+ // below, except if yScale is Inf for some reason (or worse, NaN),
+ // which 1/(p1.y-p0.y) might produce if we don't bound it.
+ yScale(1.0f / max(p1.y - p0.y, 1.0f / 256)),
+ // Calculate dX/dY slope
+ xSlope((p1.x - p0.x) * yScale),
+ // Initialize current X based on Y and slope
+ x(p0.x + (y - p0.y) * xSlope),
+ // Calculate change in interpolants per change in Y
+ interpSlope((i1 - i0) * yScale),
+ // Initialize current interpolants based on Y and slope
+ interp(i0 + (y - p0.y) * interpSlope),
+ // Extract the edge mask status for this edge
+ edgeMask((swgl_AAEdgeMask >> edgeIndex) & 1) {}
+
+ void nextRow() {
+ // step current X and interpolants to next row from slope
+ x += xSlope;
+ interp += interpSlope;
+ }
+
+ float cur_x() const { return x; }
+ float x_slope() const { return xSlope; }
+ };
+
+ // Vertex selection above should result in equal left and right start rows
+ assert(l0.y == r0.y);
+ // Find the start y, clip to within the clip rect, and round to row center.
+ // If AA is enabled, round out conservatively rather than round to nearest.
+ float aaRound = swgl_ClipFlags & SWGL_CLIP_FLAG_AA ? 0.0f : 0.5f;
+ float y = floor(max(min(l0.y, clipRect.y1), clipRect.y0) + aaRound) + 0.5f;
+ // Initialize left and right edges from end points and start Y
+ Edge left(y, l0, l1, interp_outs[l0i], interp_outs[l1i], l1i);
+ Edge right(y, r0, r1, interp_outs[r0i], interp_outs[r1i], r0i);
+ // WR does not use backface culling, so check if edges are flipped.
+ bool flipped = checkIfEdgesFlipped(l0, l1, r0, r1);
+ if (flipped) swap(left, right);
+ // Get pointer to color buffer and depth buffer at current Y
+ P* fbuf = (P*)colortex.sample_ptr(0, int(y));
+ DepthRun* fdepth = depthtex.buf != nullptr
+ ? (DepthRun*)depthtex.sample_ptr(0, int(y))
+ : nullptr;
+ // Loop along advancing Ys, rasterizing spans at each row
+ float checkY = min(min(l1.y, r1.y), clipRect.y1);
+ // Ensure we don't rasterize out edge bounds
+ FloatRange clipSpan =
+ clipRect.x_range().clip(x_range(l0, l1).merge(x_range(r0, r1)));
+ for (;;) {
+ // Check if we maybe passed edge ends or outside clip rect...
+ if (y > checkY) {
+ // If we're outside the clip rect, we're done.
+ if (y > clipRect.y1) break;
+ // Helper to find the next non-duplicate vertex that doesn't loop back.
+#define STEP_EDGE(y, e0i, e0, e1i, e1, STEP_POINT, end) \
+ do { \
+ /* Set new start of edge to be end of old edge */ \
+ e0i = e1i; \
+ e0 = e1; \
+ /* Set new end of edge to next point */ \
+ e1i = STEP_POINT(e1i); \
+ e1 = p[e1i]; \
+ /* If the edge crossed the end, we're done. */ \
+ if (e0i == end) return; \
+ /* Otherwise, it doesn't advance, so keep searching. */ \
+ } while (y > e1.y)
+ // Check if Y advanced past the end of the left edge
+ if (y > l1.y) {
+ // Step to next left edge past Y and reset edge interpolants.
+ STEP_EDGE(y, l0i, l0, l1i, l1, NEXT_POINT, r1i);
+ (flipped ? right : left) =
+ Edge(y, l0, l1, interp_outs[l0i], interp_outs[l1i], l1i);
+ }
+ // Check if Y advanced past the end of the right edge
+ if (y > r1.y) {
+ // Step to next right edge past Y and reset edge interpolants.
+ STEP_EDGE(y, r0i, r0, r1i, r1, PREV_POINT, l1i);
+ (flipped ? left : right) =
+ Edge(y, r0, r1, interp_outs[r0i], interp_outs[r1i], r0i);
+ }
+ // Reset the clip bounds for the new edges
+ clipSpan =
+ clipRect.x_range().clip(x_range(l0, l1).merge(x_range(r0, r1)));
+ // Reset check condition for next time around.
+ checkY = min(ceil(min(l1.y, r1.y) - aaRound), clipRect.y1);
+ }
+
+ // Calculate a potentially AA'd span and check if it is non-empty.
+ IntRange span = aa_span(fbuf, left, right, clipSpan);
+ if (span.len() > 0) {
+ // If user clip planes are enabled, use them to bound the current span.
+ if (vertex_shader->use_clip_distance()) {
+ span = span.intersect(clip_distance_range(left, right));
+ if (span.len() <= 0) goto next_span;
+ }
+ ctx->shaded_rows++;
+ ctx->shaded_pixels += span.len();
+ // Advance color/depth buffer pointers to the start of the span.
+ P* buf = fbuf + span.start;
+ // Check if we will need to use depth-buffer or discard on this span.
+ DepthRun* depth =
+ depthtex.buf != nullptr && depthtex.cleared() ? fdepth : nullptr;
+ DepthCursor cursor;
+ bool use_discard = fragment_shader->use_discard();
+ if (use_discard) {
+ if (depth) {
+ // If we're using discard, we may have to unpredictably drop out some
+ // samples. Flatten the depth run array here to allow this.
+ if (!depth->is_flat()) {
+ flatten_depth_runs(depth, depthtex.width);
+ }
+ // Advance to the depth sample at the start of the span.
+ depth += span.start;
+ }
+ } else if (depth) {
+ if (!depth->is_flat()) {
+ // We're not using discard and the depth row is still organized into
+ // runs. Skip past any runs that would fail the depth test so we
+ // don't have to do any extra work to process them with the rest of
+ // the span.
+ cursor = DepthCursor(depth, depthtex.width, span.start, span.len());
+ int skipped = cursor.skip_failed(z, ctx->depthfunc);
+ // If we fell off the row, that means we couldn't find any passing
+ // runs. We can just skip the entire span.
+ if (skipped < 0) {
+ goto next_span;
+ }
+ buf += skipped;
+ span.start += skipped;
+ } else {
+ // The row is already flattened, so just advance to the span start.
+ depth += span.start;
+ }
+ }
+
+ if (colortex.delay_clear) {
+ // Delayed clear is enabled for the color buffer. Check if needs clear.
+ prepare_row<P>(colortex, int(y), span.start, span.end, use_discard,
+ depth, z, &cursor);
+ }
+
+ // Initialize fragment shader interpolants to current span position.
+ fragment_shader->gl_FragCoord.x = init_interp(span.start + 0.5f, 1);
+ fragment_shader->gl_FragCoord.y = y;
+ {
+ // Change in interpolants is difference between current right and left
+ // edges per the change in right and left X. If the left and right X
+ // positions are extremely close together, then avoid stepping the
+ // interpolants.
+ float stepScale = 1.0f / (right.x - left.x);
+ if (!isfinite(stepScale)) stepScale = 0.0f;
+ Interpolants step = (right.interp - left.interp) * stepScale;
+ // Advance current interpolants to X at start of span.
+ Interpolants o = left.interp + step * (span.start + 0.5f - left.x);
+ fragment_shader->init_span(&o, &step);
+ }
+ clipRect.set_clip_mask(span.start, y, buf);
+ if (!use_discard) {
+ // Fast paths for the case where fragment discard is not used.
+ if (depth) {
+ // If depth is used, we want to process entire depth runs if depth is
+ // not flattened.
+ if (!depth->is_flat()) {
+ draw_depth_span(z, buf, cursor);
+ goto next_span;
+ }
+ // Otherwise, flattened depth must fall back to the slightly slower
+ // per-chunk depth test path in draw_span below.
+ } else {
+ // Check if the fragment shader has an optimized draw specialization.
+ if (span.len() >= 4 && fragment_shader->has_draw_span(buf)) {
+ // Draw specialization expects 4-pixel chunks.
+ int drawn = fragment_shader->draw_span(buf, span.len() & ~3);
+ buf += drawn;
+ span.start += drawn;
+ }
+ }
+ draw_span<false, false>(buf, depth, span.len(), [=] { return z; });
+ } else {
+ // If discard is used, then use slower fallbacks. This should be rare.
+ // Just needs to work, doesn't need to be too fast yet...
+ draw_span<true, false>(buf, depth, span.len(), [=] { return z; });
+ }
+ }
+ next_span:
+ // Advance Y and edge interpolants to next row.
+ y++;
+ left.nextRow();
+ right.nextRow();
+ // Advance buffers to next row.
+ fbuf += colortex.stride() / sizeof(P);
+ fdepth += depthtex.stride() / sizeof(DepthRun);
+ }
+}
+
+// Draw perspective-correct spans for a convex quad that has been clipped to
+// the near and far Z planes, possibly producing a clipped convex polygon with
+// more than 4 sides. This assumes the Z value will vary across the spans and
+// requires interpolants to factor in W values. This tends to be slower than
+// the simpler 2D draw_quad_spans above, especially since we can't optimize the
+// depth test easily when Z values, and should be used only rarely if possible.
+template <typename P>
+static inline void draw_perspective_spans(int nump, Point3D* p,
+ Interpolants* interp_outs,
+ Texture& colortex, Texture& depthtex,
+ const ClipRect& clipRect) {
+ Point3D l0, r0, l1, r1;
+ int l0i, r0i, l1i, r1i;
+ {
+ // Find the index of the top-most point (smallest Y) from which
+ // rasterization can start.
+ int top = 0;
+ for (int i = 1; i < nump; i++) {
+ if (p[i].y < p[top].y) {
+ top = i;
+ }
+ }
+ // Find left-most top point, the start of the left descending edge.
+ // Advance forward in the points array, searching at most nump points
+ // in case the polygon is flat.
+ l0i = top;
+ for (int i = top + 1; i < nump && p[i].y == p[top].y; i++) {
+ l0i = i;
+ }
+ if (l0i == nump - 1) {
+ for (int i = 0; i <= top && p[i].y == p[top].y; i++) {
+ l0i = i;
+ }
+ }
+ // Find right-most top point, the start of the right descending edge.
+ // Advance backward in the points array, searching at most nump points.
+ r0i = top;
+ for (int i = top - 1; i >= 0 && p[i].y == p[top].y; i--) {
+ r0i = i;
+ }
+ if (r0i == 0) {
+ for (int i = nump - 1; i >= top && p[i].y == p[top].y; i--) {
+ r0i = i;
+ }
+ }
+ // End of left edge is next point after left edge start.
+ l1i = NEXT_POINT(l0i);
+ // End of right edge is prev point after right edge start.
+ r1i = PREV_POINT(r0i);
+ l0 = p[l0i]; // Start of left edge
+ r0 = p[r0i]; // End of left edge
+ l1 = p[l1i]; // Start of right edge
+ r1 = p[r1i]; // End of right edge
+ }
+
+ struct Edge {
+ float yScale;
+ // Current coordinates for edge. Where in the 2D case of draw_quad_spans,
+ // it is enough to just track the X coordinate as we advance along the rows,
+ // for the perspective case we also need to keep track of Z and W. For
+ // simplicity, we just use the full 3D point to track all these coordinates.
+ Point3D pSlope;
+ Point3D p;
+ Interpolants interpSlope;
+ Interpolants interp;
+ bool edgeMask;
+
+ Edge(float y, const Point3D& p0, const Point3D& p1, const Interpolants& i0,
+ const Interpolants& i1, int edgeIndex)
+ : // Inverse Y scale for slope calculations. Avoid divide on 0-length
+ // edge.
+ yScale(1.0f / max(p1.y - p0.y, 1.0f / 256)),
+ // Calculate dX/dY slope
+ pSlope((p1 - p0) * yScale),
+ // Initialize current coords based on Y and slope
+ p(p0 + (y - p0.y) * pSlope),
+ // Crucially, these interpolants must be scaled by the point's 1/w
+ // value, which allows linear interpolation in a perspective-correct
+ // manner. This will be canceled out inside the fragment shader later.
+ // Calculate change in interpolants per change in Y
+ interpSlope((i1 * p1.w - i0 * p0.w) * yScale),
+ // Initialize current interpolants based on Y and slope
+ interp(i0 * p0.w + (y - p0.y) * interpSlope),
+ // Extract the edge mask status for this edge
+ edgeMask((swgl_AAEdgeMask >> edgeIndex) & 1) {}
+
+ float x() const { return p.x; }
+ vec2_scalar zw() const { return {p.z, p.w}; }
+
+ void nextRow() {
+ // step current coords and interpolants to next row from slope
+ p += pSlope;
+ interp += interpSlope;
+ }
+
+ float cur_x() const { return p.x; }
+ float x_slope() const { return pSlope.x; }
+ };
+
+ // Vertex selection above should result in equal left and right start rows
+ assert(l0.y == r0.y);
+ // Find the start y, clip to within the clip rect, and round to row center.
+ // If AA is enabled, round out conservatively rather than round to nearest.
+ float aaRound = swgl_ClipFlags & SWGL_CLIP_FLAG_AA ? 0.0f : 0.5f;
+ float y = floor(max(min(l0.y, clipRect.y1), clipRect.y0) + aaRound) + 0.5f;
+ // Initialize left and right edges from end points and start Y
+ Edge left(y, l0, l1, interp_outs[l0i], interp_outs[l1i], l1i);
+ Edge right(y, r0, r1, interp_outs[r0i], interp_outs[r1i], r0i);
+ // WR does not use backface culling, so check if edges are flipped.
+ bool flipped = checkIfEdgesFlipped(l0, l1, r0, r1);
+ if (flipped) swap(left, right);
+ // Get pointer to color buffer and depth buffer at current Y
+ P* fbuf = (P*)colortex.sample_ptr(0, int(y));
+ DepthRun* fdepth = depthtex.buf != nullptr
+ ? (DepthRun*)depthtex.sample_ptr(0, int(y))
+ : nullptr;
+ // Loop along advancing Ys, rasterizing spans at each row
+ float checkY = min(min(l1.y, r1.y), clipRect.y1);
+ // Ensure we don't rasterize out edge bounds
+ FloatRange clipSpan =
+ clipRect.x_range().clip(x_range(l0, l1).merge(x_range(r0, r1)));
+ for (;;) {
+ // Check if we maybe passed edge ends or outside clip rect...
+ if (y > checkY) {
+ // If we're outside the clip rect, we're done.
+ if (y > clipRect.y1) break;
+ // Check if Y advanced past the end of the left edge
+ if (y > l1.y) {
+ // Step to next left edge past Y and reset edge interpolants.
+ STEP_EDGE(y, l0i, l0, l1i, l1, NEXT_POINT, r1i);
+ (flipped ? right : left) =
+ Edge(y, l0, l1, interp_outs[l0i], interp_outs[l1i], l1i);
+ }
+ // Check if Y advanced past the end of the right edge
+ if (y > r1.y) {
+ // Step to next right edge past Y and reset edge interpolants.
+ STEP_EDGE(y, r0i, r0, r1i, r1, PREV_POINT, l1i);
+ (flipped ? left : right) =
+ Edge(y, r0, r1, interp_outs[r0i], interp_outs[r1i], r0i);
+ }
+ // Reset the clip bounds for the new edges
+ clipSpan =
+ clipRect.x_range().clip(x_range(l0, l1).merge(x_range(r0, r1)));
+ // Reset check condition for next time around.
+ checkY = min(ceil(min(l1.y, r1.y) - aaRound), clipRect.y1);
+ }
+
+ // Calculate a potentially AA'd span and check if it is non-empty.
+ IntRange span = aa_span(fbuf, left, right, clipSpan);
+ if (span.len() > 0) {
+ // If user clip planes are enabled, use them to bound the current span.
+ if (vertex_shader->use_clip_distance()) {
+ span = span.intersect(clip_distance_range(left, right));
+ if (span.len() <= 0) goto next_span;
+ }
+ ctx->shaded_rows++;
+ ctx->shaded_pixels += span.len();
+ // Advance color/depth buffer pointers to the start of the span.
+ P* buf = fbuf + span.start;
+ // Check if the we will need to use depth-buffer or discard on this span.
+ DepthRun* depth =
+ depthtex.buf != nullptr && depthtex.cleared() ? fdepth : nullptr;
+ bool use_discard = fragment_shader->use_discard();
+ if (depth) {
+ // Perspective may cause the depth value to vary on a per sample basis.
+ // Ensure the depth row is flattened to allow testing of individual
+ // samples
+ if (!depth->is_flat()) {
+ flatten_depth_runs(depth, depthtex.width);
+ }
+ // Advance to the depth sample at the start of the span.
+ depth += span.start;
+ }
+ if (colortex.delay_clear) {
+ // Delayed clear is enabled for the color buffer. Check if needs clear.
+ prepare_row<P>(colortex, int(y), span.start, span.end, use_discard,
+ depth);
+ }
+ // Initialize fragment shader interpolants to current span position.
+ fragment_shader->gl_FragCoord.x = init_interp(span.start + 0.5f, 1);
+ fragment_shader->gl_FragCoord.y = y;
+ {
+ // Calculate the fragment Z and W change per change in fragment X step.
+ // If the left and right X positions are extremely close together, then
+ // avoid stepping.
+ float stepScale = 1.0f / (right.x() - left.x());
+ if (!isfinite(stepScale)) stepScale = 0.0f;
+ vec2_scalar stepZW = (right.zw() - left.zw()) * stepScale;
+ // Calculate initial Z and W values for span start.
+ vec2_scalar zw = left.zw() + stepZW * (span.start + 0.5f - left.x());
+ // Set fragment shader's Z and W values so that it can use them to
+ // cancel out the 1/w baked into the interpolants.
+ fragment_shader->gl_FragCoord.z = init_interp(zw.x, stepZW.x);
+ fragment_shader->gl_FragCoord.w = init_interp(zw.y, stepZW.y);
+ fragment_shader->swgl_StepZW = stepZW;
+ // Change in interpolants is difference between current right and left
+ // edges per the change in right and left X. The left and right
+ // interpolant values were previously multipled by 1/w, so the step and
+ // initial span values take this into account.
+ Interpolants step = (right.interp - left.interp) * stepScale;
+ // Advance current interpolants to X at start of span.
+ Interpolants o = left.interp + step * (span.start + 0.5f - left.x());
+ fragment_shader->init_span<true>(&o, &step);
+ }
+ clipRect.set_clip_mask(span.start, y, buf);
+ if (!use_discard) {
+ // No discard is used. Common case.
+ draw_span<false, true>(buf, depth, span.len(), packDepth);
+ } else {
+ // Discard is used. Rare.
+ draw_span<true, true>(buf, depth, span.len(), packDepth);
+ }
+ }
+ next_span:
+ // Advance Y and edge interpolants to next row.
+ y++;
+ left.nextRow();
+ right.nextRow();
+ // Advance buffers to next row.
+ fbuf += colortex.stride() / sizeof(P);
+ fdepth += depthtex.stride() / sizeof(DepthRun);
+ }
+}
+
+// Clip a primitive against both sides of a view-frustum axis, producing
+// intermediate vertexes with interpolated attributes that will no longer
+// intersect the selected axis planes. This assumes the primitive is convex
+// and should produce at most N+2 vertexes for each invocation (only in the
+// worst case where one point falls outside on each of the opposite sides
+// with the rest of the points inside). The supplied AA edge mask will be
+// modified such that it corresponds to the clipped polygon edges.
+template <XYZW AXIS>
+static int clip_side(int nump, Point3D* p, Interpolants* interp, Point3D* outP,
+ Interpolants* outInterp, int& outEdgeMask) {
+ // Potential mask bits of which side of a plane a coordinate falls on.
+ enum SIDE { POSITIVE = 1, NEGATIVE = 2 };
+ int numClip = 0;
+ int edgeMask = outEdgeMask;
+ Point3D prev = p[nump - 1];
+ Interpolants prevInterp = interp[nump - 1];
+ float prevCoord = prev.select(AXIS);
+ // Coordinate must satisfy -W <= C <= W. Determine if it is outside, and
+ // if so, remember which side it is outside of. In the special case that W is
+ // negative and |C| < |W|, both -W <= C and C <= W will be false, such that
+ // we must consider the coordinate as falling outside of both plane sides
+ // simultaneously. We test each condition separately and combine them to form
+ // a mask of which plane sides we exceeded. If we neglect to consider both
+ // sides simultaneously, points can erroneously oscillate from one plane side
+ // to the other and exceed the supported maximum number of clip outputs.
+ int prevMask = (prevCoord < -prev.w ? NEGATIVE : 0) |
+ (prevCoord > prev.w ? POSITIVE : 0);
+ // Loop through points, finding edges that cross the planes by evaluating
+ // the side at each point.
+ outEdgeMask = 0;
+ for (int i = 0; i < nump; i++, edgeMask >>= 1) {
+ Point3D cur = p[i];
+ Interpolants curInterp = interp[i];
+ float curCoord = cur.select(AXIS);
+ int curMask =
+ (curCoord < -cur.w ? NEGATIVE : 0) | (curCoord > cur.w ? POSITIVE : 0);
+ // Check if the previous and current end points are on different sides. If
+ // the masks of sides intersect, then we consider them to be on the same
+ // side. So in the case the masks do not intersect, we then consider them
+ // to fall on different sides.
+ if (!(curMask & prevMask)) {
+ // One of the edge's end points is outside the plane with the other
+ // inside the plane. Find the offset where it crosses the plane and
+ // adjust the point and interpolants to there.
+ if (prevMask) {
+ // Edge that was previously outside crosses inside.
+ // Evaluate plane equation for previous and current end-point
+ // based on previous side and calculate relative offset.
+ if (numClip >= nump + 2) {
+ // If for some reason we produced more vertexes than we support, just
+ // bail out.
+ assert(false);
+ return 0;
+ }
+ // The positive plane is assigned the sign 1, and the negative plane is
+ // assigned -1. If the point falls outside both planes, that means W is
+ // negative. To compensate for this, we must interpolate the coordinate
+ // till W=0, at which point we can choose a single plane side for the
+ // coordinate to fall on since W will no longer be negative. To compute
+ // the coordinate where W=0, we compute K = prev.w / (prev.w-cur.w) and
+ // interpolate C = prev.C + K*(cur.C - prev.C). The sign of C will be
+ // the side of the plane we need to consider. Substituting K into the
+ // comparison C < 0, we can then avoid the division in K with a
+ // cross-multiplication.
+ float prevSide =
+ (prevMask & NEGATIVE) && (!(prevMask & POSITIVE) ||
+ prevCoord * (cur.w - prev.w) <
+ prev.w * (curCoord - prevCoord))
+ ? -1
+ : 1;
+ float prevDist = prevCoord - prevSide * prev.w;
+ float curDist = curCoord - prevSide * cur.w;
+ // It may happen that after we interpolate by the weight k that due to
+ // floating point rounding we've underestimated the value necessary to
+ // push it over the clipping boundary. Just in case, nudge the mantissa
+ // by a single increment so that we essentially round it up and move it
+ // further inside the clipping boundary. We use nextafter to do this in
+ // a portable fashion.
+ float k = prevDist / (prevDist - curDist);
+ Point3D clipped = prev + (cur - prev) * k;
+ if (prevSide * clipped.select(AXIS) > clipped.w) {
+ k = nextafterf(k, 1.0f);
+ clipped = prev + (cur - prev) * k;
+ }
+ outP[numClip] = clipped;
+ outInterp[numClip] = prevInterp + (curInterp - prevInterp) * k;
+ // Don't output the current edge mask since start point was outside.
+ numClip++;
+ }
+ if (curMask) {
+ // Edge that was previously inside crosses outside.
+ // Evaluate plane equation for previous and current end-point
+ // based on current side and calculate relative offset.
+ if (numClip >= nump + 2) {
+ assert(false);
+ return 0;
+ }
+ // In the case the coordinate falls on both plane sides, the computation
+ // here is much the same as for prevSide, but since we are going from a
+ // previous W that is positive to current W that is negative, then the
+ // sign of cur.w - prev.w will flip in the equation. The resulting sign
+ // is negated to compensate for this.
+ float curSide =
+ (curMask & POSITIVE) && (!(curMask & NEGATIVE) ||
+ prevCoord * (cur.w - prev.w) <
+ prev.w * (curCoord - prevCoord))
+ ? 1
+ : -1;
+ float prevDist = prevCoord - curSide * prev.w;
+ float curDist = curCoord - curSide * cur.w;
+ // Calculate interpolation weight k and the nudge it inside clipping
+ // boundary with nextafter. Note that since we were previously inside
+ // and now crossing outside, we have to flip the nudge direction for
+ // the weight towards 0 instead of 1.
+ float k = prevDist / (prevDist - curDist);
+ Point3D clipped = prev + (cur - prev) * k;
+ if (curSide * clipped.select(AXIS) > clipped.w) {
+ k = nextafterf(k, 0.0f);
+ clipped = prev + (cur - prev) * k;
+ }
+ outP[numClip] = clipped;
+ outInterp[numClip] = prevInterp + (curInterp - prevInterp) * k;
+ // Output the current edge mask since the end point is inside.
+ outEdgeMask |= (edgeMask & 1) << numClip;
+ numClip++;
+ }
+ }
+ if (!curMask) {
+ // The current end point is inside the plane, so output point unmodified.
+ if (numClip >= nump + 2) {
+ assert(false);
+ return 0;
+ }
+ outP[numClip] = cur;
+ outInterp[numClip] = curInterp;
+ // Output the current edge mask since the end point is inside.
+ outEdgeMask |= (edgeMask & 1) << numClip;
+ numClip++;
+ }
+ prev = cur;
+ prevInterp = curInterp;
+ prevCoord = curCoord;
+ prevMask = curMask;
+ }
+ return numClip;
+}
+
+// Helper function to dispatch to perspective span drawing with points that
+// have already been transformed and clipped.
+static inline void draw_perspective_clipped(int nump, Point3D* p_clip,
+ Interpolants* interp_clip,
+ Texture& colortex,
+ Texture& depthtex) {
+ // If polygon is ouside clip rect, nothing to draw.
+ ClipRect clipRect(colortex);
+ if (!clipRect.overlaps(nump, p_clip)) {
+ return;
+ }
+
+ // Finally draw perspective-correct spans for the polygon.
+ if (colortex.internal_format == GL_RGBA8) {
+ draw_perspective_spans<uint32_t>(nump, p_clip, interp_clip, colortex,
+ depthtex, clipRect);
+ } else if (colortex.internal_format == GL_R8) {
+ draw_perspective_spans<uint8_t>(nump, p_clip, interp_clip, colortex,
+ depthtex, clipRect);
+ } else {
+ assert(false);
+ }
+}
+
+// Draws a perspective-correct 3D primitive with varying Z value, as opposed
+// to a simple 2D planar primitive with a constant Z value that could be
+// trivially Z rejected. This requires clipping the primitive against the near
+// and far planes to ensure it stays within the valid Z-buffer range. The Z
+// and W of each fragment of the primitives are interpolated across the
+// generated spans and then depth-tested as appropriate.
+// Additionally, vertex attributes must be interpolated with perspective-
+// correction by dividing by W before interpolation, and then later multiplied
+// by W again to produce the final correct attribute value for each fragment.
+// This process is expensive and should be avoided if possible for primitive
+// batches that are known ahead of time to not need perspective-correction.
+static void draw_perspective(int nump, Interpolants interp_outs[4],
+ Texture& colortex, Texture& depthtex) {
+ // Lines are not supported with perspective.
+ assert(nump >= 3);
+ // Convert output of vertex shader to screen space.
+ vec4 pos = vertex_shader->gl_Position;
+ vec3_scalar scale =
+ vec3_scalar(ctx->viewport.width(), ctx->viewport.height(), 1) * 0.5f;
+ vec3_scalar offset =
+ make_vec3(make_vec2(ctx->viewport.origin() - colortex.offset), 0.0f) +
+ scale;
+ // Verify if point is between near and far planes, rejecting NaN.
+ if (test_all(pos.z > -pos.w && pos.z < pos.w)) {
+ // No points cross the near or far planes, so no clipping required.
+ // Just divide coords by W and convert to viewport. We assume the W
+ // coordinate is non-zero and the reciprocal is finite since it would
+ // otherwise fail the test_none condition.
+ Float w = 1.0f / pos.w;
+ vec3 screen = pos.sel(X, Y, Z) * w * scale + offset;
+ Point3D p[4] = {{screen.x.x, screen.y.x, screen.z.x, w.x},
+ {screen.x.y, screen.y.y, screen.z.y, w.y},
+ {screen.x.z, screen.y.z, screen.z.z, w.z},
+ {screen.x.w, screen.y.w, screen.z.w, w.w}};
+ draw_perspective_clipped(nump, p, interp_outs, colortex, depthtex);
+ } else {
+ // Points cross the near or far planes, so we need to clip.
+ // Start with the original 3 or 4 points...
+ Point3D p[4] = {{pos.x.x, pos.y.x, pos.z.x, pos.w.x},
+ {pos.x.y, pos.y.y, pos.z.y, pos.w.y},
+ {pos.x.z, pos.y.z, pos.z.z, pos.w.z},
+ {pos.x.w, pos.y.w, pos.z.w, pos.w.w}};
+ // Clipping can expand the points by 1 for each of 6 view frustum planes.
+ Point3D p_clip[4 + 6];
+ Interpolants interp_clip[4 + 6];
+ // Clip against near and far Z planes.
+ nump = clip_side<Z>(nump, p, interp_outs, p_clip, interp_clip,
+ swgl_AAEdgeMask);
+ // If no points are left inside the view frustum, there's nothing to draw.
+ if (nump < 3) {
+ return;
+ }
+ // After clipping against only the near and far planes, we might still
+ // produce points where W = 0, exactly at the camera plane. OpenGL specifies
+ // that for clip coordinates, points must satisfy:
+ // -W <= X <= W
+ // -W <= Y <= W
+ // -W <= Z <= W
+ // When Z = W = 0, this is trivially satisfied, but when we transform and
+ // divide by W below it will produce a divide by 0. Usually we want to only
+ // clip Z to avoid the extra work of clipping X and Y. We can still project
+ // points that fall outside the view frustum X and Y so long as Z is valid.
+ // The span drawing code will then ensure X and Y are clamped to viewport
+ // boundaries. However, in the Z = W = 0 case, sometimes clipping X and Y,
+ // will push W further inside the view frustum so that it is no longer 0,
+ // allowing us to finally proceed to projecting the points to the screen.
+ for (int i = 0; i < nump; i++) {
+ // Found an invalid W, so need to clip against X and Y...
+ if (p_clip[i].w <= 0.0f) {
+ // Ping-pong p_clip -> p_tmp -> p_clip.
+ Point3D p_tmp[4 + 6];
+ Interpolants interp_tmp[4 + 6];
+ nump = clip_side<X>(nump, p_clip, interp_clip, p_tmp, interp_tmp,
+ swgl_AAEdgeMask);
+ if (nump < 3) return;
+ nump = clip_side<Y>(nump, p_tmp, interp_tmp, p_clip, interp_clip,
+ swgl_AAEdgeMask);
+ if (nump < 3) return;
+ // After clipping against X and Y planes, there's still points left
+ // to draw, so proceed to trying projection now...
+ break;
+ }
+ }
+ // Divide coords by W and convert to viewport.
+ for (int i = 0; i < nump; i++) {
+ float w = 1.0f / p_clip[i].w;
+ // If the W coord is essentially zero, small enough that division would
+ // result in Inf/NaN, then just set the reciprocal itself to zero so that
+ // the coordinates becomes zeroed out, as the only valid point that
+ // satisfies -W <= X/Y/Z <= W is all zeroes.
+ if (!isfinite(w)) w = 0.0f;
+ p_clip[i] = Point3D(p_clip[i].sel(X, Y, Z) * w * scale + offset, w);
+ }
+ draw_perspective_clipped(nump, p_clip, interp_clip, colortex, depthtex);
+ }
+}
+
+static void draw_quad(int nump, Texture& colortex, Texture& depthtex) {
+ // Run vertex shader once for the primitive's vertices.
+ // Reserve space for 6 sets of interpolants, in case we need to clip against
+ // near and far planes in the perspective case.
+ Interpolants interp_outs[4];
+ swgl_ClipFlags = 0;
+ vertex_shader->run_primitive((char*)interp_outs, sizeof(Interpolants));
+ vec4 pos = vertex_shader->gl_Position;
+ // Check if any vertex W is different from another. If so, use perspective.
+ if (test_any(pos.w != pos.w.x)) {
+ draw_perspective(nump, interp_outs, colortex, depthtex);
+ return;
+ }
+
+ // Convert output of vertex shader to screen space.
+ // Divide coords by W and convert to viewport.
+ float w = 1.0f / pos.w.x;
+ // If the W coord is essentially zero, small enough that division would
+ // result in Inf/NaN, then just set the reciprocal itself to zero so that
+ // the coordinates becomes zeroed out, as the only valid point that
+ // satisfies -W <= X/Y/Z <= W is all zeroes.
+ if (!isfinite(w)) w = 0.0f;
+ vec2 screen = (pos.sel(X, Y) * w + 1) * 0.5f *
+ vec2_scalar(ctx->viewport.width(), ctx->viewport.height()) +
+ make_vec2(ctx->viewport.origin() - colortex.offset);
+ Point2D p[4] = {{screen.x.x, screen.y.x},
+ {screen.x.y, screen.y.y},
+ {screen.x.z, screen.y.z},
+ {screen.x.w, screen.y.w}};
+
+ // If quad is ouside clip rect, nothing to draw.
+ ClipRect clipRect(colortex);
+ if (!clipRect.overlaps(nump, p)) {
+ return;
+ }
+
+ // Since the quad is assumed 2D, Z is constant across the quad.
+ float screenZ = (pos.z.x * w + 1) * 0.5f;
+ if (screenZ < 0 || screenZ > 1) {
+ // Z values would cross the near or far plane, so just bail.
+ return;
+ }
+ // Since Z doesn't need to be interpolated, just set the fragment shader's
+ // Z and W values here, once and for all fragment shader invocations.
+ uint32_t z = uint32_t(MAX_DEPTH_VALUE * screenZ);
+ fragment_shader->gl_FragCoord.z = screenZ;
+ fragment_shader->gl_FragCoord.w = w;
+
+ // If supplied a line, adjust it so that it is a quad at least 1 pixel thick.
+ // Assume that for a line that all 4 SIMD lanes were actually filled with
+ // vertexes 0, 1, 1, 0.
+ if (nump == 2) {
+ // Nudge Y height to span at least 1 pixel by advancing to next pixel
+ // boundary so that we step at least 1 row when drawing spans.
+ if (int(p[0].y + 0.5f) == int(p[1].y + 0.5f)) {
+ p[2].y = 1 + int(p[1].y + 0.5f);
+ p[3].y = p[2].y;
+ // Nudge X width to span at least 1 pixel so that rounded coords fall on
+ // separate pixels.
+ if (int(p[0].x + 0.5f) == int(p[1].x + 0.5f)) {
+ p[1].x += 1.0f;
+ p[2].x += 1.0f;
+ }
+ } else {
+ // If the line already spans at least 1 row, then assume line is vertical
+ // or diagonal and just needs to be dilated horizontally.
+ p[2].x += 1.0f;
+ p[3].x += 1.0f;
+ }
+ // Pretend that it's a quad now...
+ nump = 4;
+ }
+
+ // Finally draw 2D spans for the quad. Currently only supports drawing to
+ // RGBA8 and R8 color buffers.
+ if (colortex.internal_format == GL_RGBA8) {
+ draw_quad_spans<uint32_t>(nump, p, z, interp_outs, colortex, depthtex,
+ clipRect);
+ } else if (colortex.internal_format == GL_R8) {
+ draw_quad_spans<uint8_t>(nump, p, z, interp_outs, colortex, depthtex,
+ clipRect);
+ } else {
+ assert(false);
+ }
+}
+
+template <typename INDEX>
+static inline void draw_elements(GLsizei count, GLsizei instancecount,
+ size_t offset, VertexArray& v,
+ Texture& colortex, Texture& depthtex) {
+ Buffer& indices_buf = ctx->buffers[v.element_array_buffer_binding];
+ if (!indices_buf.buf || offset >= indices_buf.size) {
+ return;
+ }
+ assert((offset & (sizeof(INDEX) - 1)) == 0);
+ INDEX* indices = (INDEX*)(indices_buf.buf + offset);
+ count = min(count, (GLsizei)((indices_buf.size - offset) / sizeof(INDEX)));
+ // Triangles must be indexed at offsets 0, 1, 2.
+ // Quads must be successive triangles indexed at offsets 0, 1, 2, 2, 1, 3.
+ if (count == 6 && indices[1] == indices[0] + 1 &&
+ indices[2] == indices[0] + 2 && indices[5] == indices[0] + 3) {
+ assert(indices[3] == indices[0] + 2 && indices[4] == indices[0] + 1);
+ // Fast path - since there is only a single quad, we only load per-vertex
+ // attribs once for all instances, as they won't change across instances
+ // or within an instance.
+ vertex_shader->load_attribs(v.attribs, indices[0], 0, 4);
+ draw_quad(4, colortex, depthtex);
+ for (GLsizei instance = 1; instance < instancecount; instance++) {
+ vertex_shader->load_attribs(v.attribs, indices[0], instance, 0);
+ draw_quad(4, colortex, depthtex);
+ }
+ } else {
+ for (GLsizei instance = 0; instance < instancecount; instance++) {
+ for (GLsizei i = 0; i + 3 <= count; i += 3) {
+ if (indices[i + 1] != indices[i] + 1 ||
+ indices[i + 2] != indices[i] + 2) {
+ continue;
+ }
+ if (i + 6 <= count && indices[i + 5] == indices[i] + 3) {
+ assert(indices[i + 3] == indices[i] + 2 &&
+ indices[i + 4] == indices[i] + 1);
+ vertex_shader->load_attribs(v.attribs, indices[i], instance, 4);
+ draw_quad(4, colortex, depthtex);
+ i += 3;
+ } else {
+ vertex_shader->load_attribs(v.attribs, indices[i], instance, 3);
+ draw_quad(3, colortex, depthtex);
+ }
+ }
+ }
+ }
+}
diff --git a/gfx/wr/swgl/src/swgl_ext.h b/gfx/wr/swgl/src/swgl_ext.h
new file mode 100644
index 0000000000..164ca8d7b1
--- /dev/null
+++ b/gfx/wr/swgl/src/swgl_ext.h
@@ -0,0 +1,1906 @@
+/* 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/. */
+
+// When using a solid color with clip masking, the cost of loading the clip mask
+// in the blend stage exceeds the cost of processing the color. Here we handle
+// the entire span of clip mask texture before the blend stage to more
+// efficiently process it and modulate it with color without incurring blend
+// stage overheads.
+template <typename P, typename C>
+static void commit_masked_solid_span(P* buf, C color, int len) {
+ override_clip_mask();
+ uint8_t* mask = get_clip_mask(buf);
+ for (P* end = &buf[len]; buf < end; buf += 4, mask += 4) {
+ commit_span(
+ buf,
+ blend_span(
+ buf,
+ applyColor(expand_mask(buf, unpack(unaligned_load<PackedR8>(mask))),
+ color)));
+ }
+ restore_clip_mask();
+}
+
+// When using a solid color with anti-aliasing, most of the solid span will not
+// benefit from anti-aliasing in the opaque region. We only want to apply the AA
+// blend stage in the non-opaque start and end of the span where AA is needed.
+template <typename P, typename R>
+static ALWAYS_INLINE void commit_aa_solid_span(P* buf, R r, int len) {
+ if (int start = min((get_aa_opaque_start(buf) + 3) & ~3, len)) {
+ commit_solid_span<true>(buf, r, start);
+ buf += start;
+ len -= start;
+ }
+ if (int opaque = min((get_aa_opaque_size(buf) + 3) & ~3, len)) {
+ override_aa();
+ commit_solid_span<true>(buf, r, opaque);
+ restore_aa();
+ buf += opaque;
+ len -= opaque;
+ }
+ if (len > 0) {
+ commit_solid_span<true>(buf, r, len);
+ }
+}
+
+// Forces a value with vector run-class to have scalar run-class.
+template <typename T>
+static ALWAYS_INLINE auto swgl_forceScalar(T v) -> decltype(force_scalar(v)) {
+ return force_scalar(v);
+}
+
+// Advance all varying inperpolants by a single chunk
+#define swgl_stepInterp() step_interp_inputs()
+
+// Pseudo-intrinsic that accesses the interpolation step for a given varying
+#define swgl_interpStep(v) (interp_step.v)
+
+// Commit an entire span of a solid color. This dispatches to clip-masked and
+// anti-aliased fast-paths as appropriate.
+#define swgl_commitSolid(format, v, n) \
+ do { \
+ int len = (n); \
+ if (blend_key) { \
+ if (swgl_ClipFlags & SWGL_CLIP_FLAG_MASK) { \
+ commit_masked_solid_span(swgl_Out##format, \
+ packColor(swgl_Out##format, (v)), len); \
+ } else if (swgl_ClipFlags & SWGL_CLIP_FLAG_AA) { \
+ commit_aa_solid_span(swgl_Out##format, \
+ pack_span(swgl_Out##format, (v)), len); \
+ } else { \
+ commit_solid_span<true>(swgl_Out##format, \
+ pack_span(swgl_Out##format, (v)), len); \
+ } \
+ } else { \
+ commit_solid_span<false>(swgl_Out##format, \
+ pack_span(swgl_Out##format, (v)), len); \
+ } \
+ swgl_Out##format += len; \
+ swgl_SpanLength -= len; \
+ } while (0)
+#define swgl_commitSolidRGBA8(v) swgl_commitSolid(RGBA8, v, swgl_SpanLength)
+#define swgl_commitSolidR8(v) swgl_commitSolid(R8, v, swgl_SpanLength)
+#define swgl_commitPartialSolidRGBA8(len, v) \
+ swgl_commitSolid(RGBA8, v, min(int(len), swgl_SpanLength))
+#define swgl_commitPartialSolidR8(len, v) \
+ swgl_commitSolid(R8, v, min(int(len), swgl_SpanLength))
+
+#define swgl_commitChunk(format, chunk) \
+ do { \
+ auto r = chunk; \
+ if (blend_key) r = blend_span(swgl_Out##format, r); \
+ commit_span(swgl_Out##format, r); \
+ swgl_Out##format += swgl_StepSize; \
+ swgl_SpanLength -= swgl_StepSize; \
+ } while (0)
+
+// Commit a single chunk of a color
+#define swgl_commitColor(format, color) \
+ swgl_commitChunk(format, pack_pixels_##format(color))
+#define swgl_commitColorRGBA8(color) swgl_commitColor(RGBA8, color)
+#define swgl_commitColorR8(color) swgl_commitColor(R8, color)
+
+template <typename S>
+static ALWAYS_INLINE bool swgl_isTextureLinear(S s) {
+ return s->filter == TextureFilter::LINEAR;
+}
+
+template <typename S>
+static ALWAYS_INLINE bool swgl_isTextureRGBA8(S s) {
+ return s->format == TextureFormat::RGBA8;
+}
+
+template <typename S>
+static ALWAYS_INLINE bool swgl_isTextureR8(S s) {
+ return s->format == TextureFormat::R8;
+}
+
+// Use the default linear quantization scale of 128. This gives 7 bits of
+// fractional precision, which when multiplied with a signed 9 bit value
+// still fits in a 16 bit integer.
+const int swgl_LinearQuantizeScale = 128;
+
+// Quantizes UVs for access into a linear texture.
+template <typename S, typename T>
+static ALWAYS_INLINE T swgl_linearQuantize(S s, T p) {
+ return linearQuantize(p, swgl_LinearQuantizeScale, s);
+}
+
+// Quantizes an interpolation step for UVs for access into a linear texture.
+template <typename S, typename T>
+static ALWAYS_INLINE T swgl_linearQuantizeStep(S s, T p) {
+ return samplerScale(s, p) * swgl_LinearQuantizeScale;
+}
+
+template <typename S>
+static ALWAYS_INLINE WideRGBA8 textureLinearUnpacked(UNUSED uint32_t* buf,
+ S sampler, ivec2 i) {
+ return textureLinearUnpackedRGBA8(sampler, i);
+}
+
+template <typename S>
+static ALWAYS_INLINE WideR8 textureLinearUnpacked(UNUSED uint8_t* buf,
+ S sampler, ivec2 i) {
+ return textureLinearUnpackedR8(sampler, i);
+}
+
+template <typename S>
+static ALWAYS_INLINE bool matchTextureFormat(S s, UNUSED uint32_t* buf) {
+ return swgl_isTextureRGBA8(s);
+}
+
+template <typename S>
+static ALWAYS_INLINE bool matchTextureFormat(S s, UNUSED uint8_t* buf) {
+ return swgl_isTextureR8(s);
+}
+
+// Quantizes the UVs to the 2^7 scale needed for calculating fractional offsets
+// for linear sampling.
+#define LINEAR_QUANTIZE_UV(sampler, uv, uv_step, uv_rect, min_uv, max_uv) \
+ uv = swgl_linearQuantize(sampler, uv); \
+ vec2_scalar uv_step = \
+ float(swgl_StepSize) * vec2_scalar{uv.x.y - uv.x.x, uv.y.y - uv.y.x}; \
+ vec2_scalar min_uv = max( \
+ swgl_linearQuantize(sampler, vec2_scalar{uv_rect.x, uv_rect.y}), 0.0f); \
+ vec2_scalar max_uv = \
+ max(swgl_linearQuantize(sampler, vec2_scalar{uv_rect.z, uv_rect.w}), \
+ min_uv);
+
+// Implements the fallback linear filter that can deal with clamping and
+// arbitrary scales.
+template <bool BLEND, typename S, typename C, typename P>
+static P* blendTextureLinearFallback(S sampler, vec2 uv, int span,
+ vec2_scalar uv_step, vec2_scalar min_uv,
+ vec2_scalar max_uv, C color, P* buf) {
+ for (P* end = buf + span; buf < end; buf += swgl_StepSize, uv += uv_step) {
+ commit_blend_span<BLEND>(
+ buf, applyColor(textureLinearUnpacked(buf, sampler,
+ ivec2(clamp(uv, min_uv, max_uv))),
+ color));
+ }
+ return buf;
+}
+
+static ALWAYS_INLINE U64 castForShuffle(V16<int16_t> r) {
+ return bit_cast<U64>(r);
+}
+static ALWAYS_INLINE U16 castForShuffle(V4<int16_t> r) {
+ return bit_cast<U16>(r);
+}
+
+static ALWAYS_INLINE V16<int16_t> applyFracX(V16<int16_t> r, I16 fracx) {
+ return r * fracx.xxxxyyyyzzzzwwww;
+}
+static ALWAYS_INLINE V4<int16_t> applyFracX(V4<int16_t> r, I16 fracx) {
+ return r * fracx;
+}
+
+// Implements a faster linear filter that works with axis-aligned constant Y but
+// scales less than 1, i.e. upscaling. In this case we can optimize for the
+// constant Y fraction as well as load all chunks from memory in a single tap
+// for each row.
+template <bool BLEND, typename S, typename C, typename P>
+static void blendTextureLinearUpscale(S sampler, vec2 uv, int span,
+ vec2_scalar uv_step, vec2_scalar min_uv,
+ vec2_scalar max_uv, C color, P* buf) {
+ typedef VectorType<uint8_t, 4 * sizeof(P)> packed_type;
+ typedef VectorType<uint16_t, 4 * sizeof(P)> unpacked_type;
+ typedef VectorType<int16_t, 4 * sizeof(P)> signed_unpacked_type;
+
+ ivec2 i(clamp(uv, min_uv, max_uv));
+ ivec2 frac = i;
+ i >>= 7;
+ P* row0 = (P*)sampler->buf + computeRow(sampler, ivec2_scalar(0, i.y.x));
+ P* row1 = row0 + computeNextRowOffset(sampler, ivec2_scalar(0, i.y.x));
+ I16 fracx = computeFracX(sampler, i, frac);
+ int16_t fracy = computeFracY(frac).x;
+ auto src0 =
+ CONVERT(unaligned_load<packed_type>(&row0[i.x.x]), signed_unpacked_type);
+ auto src1 =
+ CONVERT(unaligned_load<packed_type>(&row1[i.x.x]), signed_unpacked_type);
+ auto src = castForShuffle(src0 + (((src1 - src0) * fracy) >> 7));
+
+ // We attempt to sample ahead by one chunk and interpolate it with the current
+ // one. However, due to the complication of upscaling, we may not necessarily
+ // shift in all the next set of samples.
+ for (P* end = buf + span; buf < end; buf += 4) {
+ uv.x += uv_step.x;
+ I32 ixn = cast(uv.x);
+ I16 fracn = computeFracNoClamp(ixn);
+ ixn >>= 7;
+ auto src0n = CONVERT(unaligned_load<packed_type>(&row0[ixn.x]),
+ signed_unpacked_type);
+ auto src1n = CONVERT(unaligned_load<packed_type>(&row1[ixn.x]),
+ signed_unpacked_type);
+ auto srcn = castForShuffle(src0n + (((src1n - src0n) * fracy) >> 7));
+
+ // Since we're upscaling, we know that a source pixel has a larger footprint
+ // than the destination pixel, and thus all the source pixels needed for
+ // this chunk will fall within a single chunk of texture data. However,
+ // since the source pixels don't map 1:1 with destination pixels, we need to
+ // shift the source pixels over based on their offset from the start of the
+ // chunk. This could conceivably be optimized better with usage of PSHUFB or
+ // VTBL instructions However, since PSHUFB requires SSSE3, instead we resort
+ // to masking in the correct pixels to avoid having to index into memory.
+ // For the last sample to interpolate with, we need to potentially shift in
+ // a sample from the next chunk over in the case the samples fill out an
+ // entire chunk.
+ auto shuf = src;
+ auto shufn = SHUFFLE(src, ixn.x == i.x.w ? srcn.yyyy : srcn, 1, 2, 3, 4);
+ if (i.x.y == i.x.x) {
+ shuf = shuf.xxyz;
+ shufn = shufn.xxyz;
+ }
+ if (i.x.z == i.x.y) {
+ shuf = shuf.xyyz;
+ shufn = shufn.xyyz;
+ }
+ if (i.x.w == i.x.z) {
+ shuf = shuf.xyzz;
+ shufn = shufn.xyzz;
+ }
+
+ // Convert back to a signed unpacked type so that we can interpolate the
+ // final result.
+ auto interp = bit_cast<signed_unpacked_type>(shuf);
+ auto interpn = bit_cast<signed_unpacked_type>(shufn);
+ interp += applyFracX(interpn - interp, fracx) >> 7;
+
+ commit_blend_span<BLEND>(
+ buf, applyColor(bit_cast<unpacked_type>(interp), color));
+
+ i.x = ixn;
+ fracx = fracn;
+ src = srcn;
+ }
+}
+
+// This is the fastest variant of the linear filter that still provides
+// filtering. In cases where there is no scaling required, but we have a
+// subpixel offset that forces us to blend in neighboring pixels, we can
+// optimize away most of the memory loads and shuffling that is required by the
+// fallback filter.
+template <bool BLEND, typename S, typename C, typename P>
+static void blendTextureLinearFast(S sampler, vec2 uv, int span,
+ vec2_scalar min_uv, vec2_scalar max_uv,
+ C color, P* buf) {
+ typedef VectorType<uint8_t, 4 * sizeof(P)> packed_type;
+ typedef VectorType<uint16_t, 4 * sizeof(P)> unpacked_type;
+ typedef VectorType<int16_t, 4 * sizeof(P)> signed_unpacked_type;
+
+ ivec2 i(clamp(uv, min_uv, max_uv));
+ ivec2 frac = i;
+ i >>= 7;
+ P* row0 = (P*)sampler->buf + computeRow(sampler, force_scalar(i));
+ P* row1 = row0 + computeNextRowOffset(sampler, force_scalar(i));
+ int16_t fracx = computeFracX(sampler, i, frac).x;
+ int16_t fracy = computeFracY(frac).x;
+ auto src0 = CONVERT(unaligned_load<packed_type>(row0), signed_unpacked_type);
+ auto src1 = CONVERT(unaligned_load<packed_type>(row1), signed_unpacked_type);
+ auto src = castForShuffle(src0 + (((src1 - src0) * fracy) >> 7));
+
+ // Since there is no scaling, we sample ahead by one chunk and interpolate it
+ // with the current one. We can then reuse this value on the next iteration.
+ for (P* end = buf + span; buf < end; buf += 4) {
+ row0 += 4;
+ row1 += 4;
+ auto src0n =
+ CONVERT(unaligned_load<packed_type>(row0), signed_unpacked_type);
+ auto src1n =
+ CONVERT(unaligned_load<packed_type>(row1), signed_unpacked_type);
+ auto srcn = castForShuffle(src0n + (((src1n - src0n) * fracy) >> 7));
+
+ // For the last sample to interpolate with, we need to potentially shift in
+ // a sample from the next chunk over since the samples fill out an entire
+ // chunk.
+ auto interp = bit_cast<signed_unpacked_type>(src);
+ auto interpn =
+ bit_cast<signed_unpacked_type>(SHUFFLE(src, srcn, 1, 2, 3, 4));
+ interp += ((interpn - interp) * fracx) >> 7;
+
+ commit_blend_span<BLEND>(
+ buf, applyColor(bit_cast<unpacked_type>(interp), color));
+
+ src = srcn;
+ }
+}
+
+// Implements a faster linear filter that works with axis-aligned constant Y but
+// downscaling the texture by half. In this case we can optimize for the
+// constant X/Y fractions and reduction factor while minimizing shuffling.
+template <bool BLEND, typename S, typename C, typename P>
+static NO_INLINE void blendTextureLinearDownscale(S sampler, vec2 uv, int span,
+ vec2_scalar min_uv,
+ vec2_scalar max_uv, C color,
+ P* buf) {
+ typedef VectorType<uint8_t, 4 * sizeof(P)> packed_type;
+ typedef VectorType<uint16_t, 4 * sizeof(P)> unpacked_type;
+ typedef VectorType<int16_t, 4 * sizeof(P)> signed_unpacked_type;
+
+ ivec2 i(clamp(uv, min_uv, max_uv));
+ ivec2 frac = i;
+ i >>= 7;
+ P* row0 = (P*)sampler->buf + computeRow(sampler, force_scalar(i));
+ P* row1 = row0 + computeNextRowOffset(sampler, force_scalar(i));
+ int16_t fracx = computeFracX(sampler, i, frac).x;
+ int16_t fracy = computeFracY(frac).x;
+
+ for (P* end = buf + span; buf < end; buf += 4) {
+ auto src0 =
+ CONVERT(unaligned_load<packed_type>(row0), signed_unpacked_type);
+ auto src1 =
+ CONVERT(unaligned_load<packed_type>(row1), signed_unpacked_type);
+ auto src = castForShuffle(src0 + (((src1 - src0) * fracy) >> 7));
+ row0 += 4;
+ row1 += 4;
+ auto src0n =
+ CONVERT(unaligned_load<packed_type>(row0), signed_unpacked_type);
+ auto src1n =
+ CONVERT(unaligned_load<packed_type>(row1), signed_unpacked_type);
+ auto srcn = castForShuffle(src0n + (((src1n - src0n) * fracy) >> 7));
+ row0 += 4;
+ row1 += 4;
+
+ auto interp =
+ bit_cast<signed_unpacked_type>(SHUFFLE(src, srcn, 0, 2, 4, 6));
+ auto interpn =
+ bit_cast<signed_unpacked_type>(SHUFFLE(src, srcn, 1, 3, 5, 7));
+ interp += ((interpn - interp) * fracx) >> 7;
+
+ commit_blend_span<BLEND>(
+ buf, applyColor(bit_cast<unpacked_type>(interp), color));
+ }
+}
+
+enum LinearFilter {
+ // No linear filter is needed.
+ LINEAR_FILTER_NEAREST = 0,
+ // The most general linear filter that handles clamping and varying scales.
+ LINEAR_FILTER_FALLBACK,
+ // A linear filter optimized for axis-aligned upscaling.
+ LINEAR_FILTER_UPSCALE,
+ // A linear filter with no scaling but with subpixel offset.
+ LINEAR_FILTER_FAST,
+ // A linear filter optimized for 2x axis-aligned downscaling.
+ LINEAR_FILTER_DOWNSCALE
+};
+
+// Dispatches to an appropriate linear filter depending on the selected filter.
+template <bool BLEND, typename S, typename C, typename P>
+static P* blendTextureLinearDispatch(S sampler, vec2 uv, int span,
+ vec2_scalar uv_step, vec2_scalar min_uv,
+ vec2_scalar max_uv, C color, P* buf,
+ LinearFilter filter) {
+ P* end = buf + span;
+ if (filter != LINEAR_FILTER_FALLBACK) {
+ // If we're not using the fallback, then Y is constant across the entire
+ // row. We just need to ensure that we handle any samples that might pull
+ // data from before the start of the row and require clamping.
+ float beforeDist = max(0.0f, min_uv.x) - uv.x.x;
+ if (beforeDist > 0) {
+ int before = clamp(int(ceil(beforeDist / uv_step.x)) * swgl_StepSize, 0,
+ int(end - buf));
+ buf = blendTextureLinearFallback<BLEND>(sampler, uv, before, uv_step,
+ min_uv, max_uv, color, buf);
+ uv.x += (before / swgl_StepSize) * uv_step.x;
+ }
+ // We need to check how many samples we can take from inside the row without
+ // requiring clamping. In case the filter oversamples the row by a step, we
+ // subtract off a step from the width to leave some room.
+ float insideDist =
+ min(max_uv.x, float((int(sampler->width) - swgl_StepSize) *
+ swgl_LinearQuantizeScale)) -
+ uv.x.x;
+ if (uv_step.x > 0.0f && insideDist >= uv_step.x) {
+ int32_t inside = int(end - buf);
+ if (filter == LINEAR_FILTER_DOWNSCALE) {
+ inside = min(int(insideDist * (0.5f / swgl_LinearQuantizeScale)) &
+ ~(swgl_StepSize - 1),
+ inside);
+ if (inside > 0) {
+ blendTextureLinearDownscale<BLEND>(sampler, uv, inside, min_uv,
+ max_uv, color, buf);
+ buf += inside;
+ uv.x += (inside / swgl_StepSize) * uv_step.x;
+ }
+ } else if (filter == LINEAR_FILTER_UPSCALE) {
+ inside = min(int(insideDist / uv_step.x) * swgl_StepSize, inside);
+ if (inside > 0) {
+ blendTextureLinearUpscale<BLEND>(sampler, uv, inside, uv_step, min_uv,
+ max_uv, color, buf);
+ buf += inside;
+ uv.x += (inside / swgl_StepSize) * uv_step.x;
+ }
+ } else {
+ inside = min(int(insideDist * (1.0f / swgl_LinearQuantizeScale)) &
+ ~(swgl_StepSize - 1),
+ inside);
+ if (inside > 0) {
+ blendTextureLinearFast<BLEND>(sampler, uv, inside, min_uv, max_uv,
+ color, buf);
+ buf += inside;
+ uv.x += (inside / swgl_StepSize) * uv_step.x;
+ }
+ }
+ }
+ }
+ // If the fallback filter was requested, or if there are any samples left that
+ // may be outside the row and require clamping, then handle that with here.
+ if (buf < end) {
+ buf = blendTextureLinearFallback<BLEND>(
+ sampler, uv, int(end - buf), uv_step, min_uv, max_uv, color, buf);
+ }
+ return buf;
+}
+
+// Helper function to quantize UVs for linear filtering before dispatch
+template <bool BLEND, typename S, typename C, typename P>
+static inline int blendTextureLinear(S sampler, vec2 uv, int span,
+ const vec4_scalar& uv_rect, C color,
+ P* buf, LinearFilter filter) {
+ if (!matchTextureFormat(sampler, buf)) {
+ return 0;
+ }
+ LINEAR_QUANTIZE_UV(sampler, uv, uv_step, uv_rect, min_uv, max_uv);
+ blendTextureLinearDispatch<BLEND>(sampler, uv, span, uv_step, min_uv, max_uv,
+ color, buf, filter);
+ return span;
+}
+
+// Samples an axis-aligned span of on a single row of a texture using 1:1
+// nearest filtering. Sampling is constrained to only fall within the given UV
+// bounds. This requires a pointer to the destination buffer. An optional color
+// modulus can be supplied.
+template <bool BLEND, typename S, typename C, typename P>
+static int blendTextureNearestFast(S sampler, vec2 uv, int span,
+ const vec4_scalar& uv_rect, C color,
+ P* buf) {
+ if (!matchTextureFormat(sampler, buf)) {
+ return 0;
+ }
+
+ typedef VectorType<uint8_t, 4 * sizeof(P)> packed_type;
+
+ ivec2_scalar i = make_ivec2(samplerScale(sampler, force_scalar(uv)));
+ ivec2_scalar minUV =
+ make_ivec2(samplerScale(sampler, vec2_scalar{uv_rect.x, uv_rect.y}));
+ ivec2_scalar maxUV =
+ make_ivec2(samplerScale(sampler, vec2_scalar{uv_rect.z, uv_rect.w}));
+
+ // Calculate the row pointer within the buffer, clamping to within valid row
+ // bounds.
+ P* row =
+ &((P*)sampler
+ ->buf)[clampCoord(clamp(i.y, minUV.y, maxUV.y), sampler->height) *
+ sampler->stride];
+ // Find clamped X bounds within the row.
+ int minX = clamp(minUV.x, 0, sampler->width - 1);
+ int maxX = clamp(maxUV.x, minX, sampler->width - 1);
+ int curX = i.x;
+ int endX = i.x + span;
+ // If we need to start sampling below the valid sample bounds, then we need to
+ // fill this section with a constant clamped sample.
+ if (curX < minX) {
+ int n = min(minX, endX) - curX;
+ auto src =
+ applyColor(unpack(bit_cast<packed_type>(V4<P>(row[minX]))), color);
+ commit_solid_span<BLEND>(buf, src, n);
+ buf += n;
+ curX += n;
+ }
+ // Here we only deal with valid samples within the sample bounds. No clamping
+ // should occur here within these inner loops.
+ int n = max(min(maxX + 1, endX) - curX, 0);
+ // Try to process as many chunks as possible with full loads and stores.
+ for (int end = curX + (n & ~3); curX < end; curX += 4, buf += 4) {
+ auto src = applyColor(unaligned_load<packed_type>(&row[curX]), color);
+ commit_blend_span<BLEND>(buf, src);
+ }
+ n &= 3;
+ // If we have any leftover samples after processing chunks, use partial loads
+ // and stores.
+ if (n > 0) {
+ auto src = applyColor(partial_load_span<packed_type>(&row[curX], n), color);
+ commit_blend_span<BLEND>(buf, src, n);
+ buf += n;
+ curX += n;
+ }
+ // If we still have samples left above the valid sample bounds, then we again
+ // need to fill this section with a constant clamped sample.
+ if (curX < endX) {
+ auto src =
+ applyColor(unpack(bit_cast<packed_type>(V4<P>(row[maxX]))), color);
+ commit_solid_span<BLEND>(buf, src, endX - curX);
+ }
+ return span;
+}
+
+// We need to verify that the pixel step reasonably approximates stepping by a
+// single texel for every pixel we need to reproduce. Try to ensure that the
+// margin of error is no more than approximately 2^-7. Also, we check here if
+// the scaling can be quantized for acceleration.
+template <typename T>
+static ALWAYS_INLINE int spanNeedsScale(int span, T P) {
+ span &= ~(128 - 1);
+ span += 128;
+ int scaled = round((P.x.y - P.x.x) * span);
+ return scaled != span ? (scaled == span * 2 ? 2 : 1) : 0;
+}
+
+// Helper function to decide whether we can safely apply 1:1 nearest filtering
+// without diverging too much from the linear filter.
+template <typename S, typename T>
+static inline LinearFilter needsTextureLinear(S sampler, T P, int span) {
+ // If each row is not wide enough for linear filtering, then just use nearest
+ // filtering.
+ if (sampler->width < 2) {
+ return LINEAR_FILTER_NEAREST;
+ }
+ // First verify if the row Y doesn't change across samples
+ if (P.y.x != P.y.y) {
+ return LINEAR_FILTER_FALLBACK;
+ }
+ P = samplerScale(sampler, P);
+ if (int scale = spanNeedsScale(span, P)) {
+ // If the source region is not flipped and smaller than the destination,
+ // then we can use the upscaling filter since row Y is constant.
+ return P.x.x < P.x.y && P.x.y - P.x.x <= 1
+ ? LINEAR_FILTER_UPSCALE
+ : (scale == 2 ? LINEAR_FILTER_DOWNSCALE
+ : LINEAR_FILTER_FALLBACK);
+ }
+ // Also verify that we're reasonably close to the center of a texel
+ // so that it doesn't look that much different than if a linear filter
+ // was used.
+ if ((int(P.x.x * 4.0f + 0.5f) & 3) != 2 ||
+ (int(P.y.x * 4.0f + 0.5f) & 3) != 2) {
+ // The source and destination regions are the same, but there is a
+ // significant subpixel offset. We can use a faster linear filter to deal
+ // with the offset in this case.
+ return LINEAR_FILTER_FAST;
+ }
+ // Otherwise, we have a constant 1:1 step and we're stepping reasonably close
+ // to the center of each pixel, so it's safe to disable the linear filter and
+ // use nearest.
+ return LINEAR_FILTER_NEAREST;
+}
+
+// Commit an entire span with linear filtering
+#define swgl_commitTextureLinear(format, s, p, uv_rect, color, n) \
+ do { \
+ auto packed_color = packColor(swgl_Out##format, color); \
+ int len = (n); \
+ int drawn = 0; \
+ if (LinearFilter filter = needsTextureLinear(s, p, len)) { \
+ if (blend_key) { \
+ drawn = blendTextureLinear<true>(s, p, len, uv_rect, packed_color, \
+ swgl_Out##format, filter); \
+ } else { \
+ drawn = blendTextureLinear<false>(s, p, len, uv_rect, packed_color, \
+ swgl_Out##format, filter); \
+ } \
+ } else if (blend_key) { \
+ drawn = blendTextureNearestFast<true>(s, p, len, uv_rect, packed_color, \
+ swgl_Out##format); \
+ } else { \
+ drawn = blendTextureNearestFast<false>(s, p, len, uv_rect, packed_color, \
+ swgl_Out##format); \
+ } \
+ swgl_Out##format += drawn; \
+ swgl_SpanLength -= drawn; \
+ } while (0)
+#define swgl_commitTextureLinearRGBA8(s, p, uv_rect) \
+ swgl_commitTextureLinear(RGBA8, s, p, uv_rect, NoColor(), swgl_SpanLength)
+#define swgl_commitTextureLinearR8(s, p, uv_rect) \
+ swgl_commitTextureLinear(R8, s, p, uv_rect, NoColor(), swgl_SpanLength)
+
+// Commit a partial span with linear filtering, optionally inverting the color
+#define swgl_commitPartialTextureLinearR8(len, s, p, uv_rect) \
+ swgl_commitTextureLinear(R8, s, p, uv_rect, NoColor(), \
+ min(int(len), swgl_SpanLength))
+#define swgl_commitPartialTextureLinearInvertR8(len, s, p, uv_rect) \
+ swgl_commitTextureLinear(R8, s, p, uv_rect, InvertColor(), \
+ min(int(len), swgl_SpanLength))
+
+// Commit an entire span with linear filtering that is scaled by a color
+#define swgl_commitTextureLinearColorRGBA8(s, p, uv_rect, color) \
+ swgl_commitTextureLinear(RGBA8, s, p, uv_rect, color, swgl_SpanLength)
+#define swgl_commitTextureLinearColorR8(s, p, uv_rect, color) \
+ swgl_commitTextureLinear(R8, s, p, uv_rect, color, swgl_SpanLength)
+
+// Helper function that samples from an R8 texture while expanding it to support
+// a differing framebuffer format.
+template <bool BLEND, typename S, typename C, typename P>
+static inline int blendTextureLinearR8(S sampler, vec2 uv, int span,
+ const vec4_scalar& uv_rect, C color,
+ P* buf) {
+ if (!swgl_isTextureR8(sampler) || sampler->width < 2) {
+ return 0;
+ }
+ LINEAR_QUANTIZE_UV(sampler, uv, uv_step, uv_rect, min_uv, max_uv);
+ for (P* end = buf + span; buf < end; buf += swgl_StepSize, uv += uv_step) {
+ commit_blend_span<BLEND>(
+ buf, applyColor(expand_mask(buf, textureLinearUnpackedR8(
+ sampler,
+ ivec2(clamp(uv, min_uv, max_uv)))),
+ color));
+ }
+ return span;
+}
+
+// Commit an entire span with linear filtering while expanding from R8 to RGBA8
+#define swgl_commitTextureLinearColorR8ToRGBA8(s, p, uv_rect, color) \
+ do { \
+ auto packed_color = packColor(swgl_OutRGBA8, color); \
+ int drawn = 0; \
+ if (blend_key) { \
+ drawn = blendTextureLinearR8<true>(s, p, swgl_SpanLength, uv_rect, \
+ packed_color, swgl_OutRGBA8); \
+ } else { \
+ drawn = blendTextureLinearR8<false>(s, p, swgl_SpanLength, uv_rect, \
+ packed_color, swgl_OutRGBA8); \
+ } \
+ swgl_OutRGBA8 += drawn; \
+ swgl_SpanLength -= drawn; \
+ } while (0)
+#define swgl_commitTextureLinearR8ToRGBA8(s, p, uv_rect) \
+ swgl_commitTextureLinearColorR8ToRGBA8(s, p, uv_rect, NoColor())
+
+// Compute repeating UVs, possibly constrained by tile repeat limits
+static inline vec2 tileRepeatUV(vec2 uv, const vec2_scalar& tile_repeat) {
+ if (tile_repeat.x > 0.0f) {
+ // Clamp to a number slightly less than the tile repeat limit so that
+ // it results in a number close to but not equal to 1 after fract().
+ // This avoids fract() yielding 0 if the limit was left as whole integer.
+ uv = clamp(uv, vec2_scalar(0.0f), tile_repeat - 1.0e-6f);
+ }
+ return fract(uv);
+}
+
+// Compute the number of non-repeating steps before we need to potentially
+// repeat the UVs.
+static inline int computeNoRepeatSteps(Float uv, float uv_step,
+ float tile_repeat, int steps) {
+ if (uv.w < uv.x) {
+ // Ensure the UV taps are ordered low to high.
+ uv = uv.wzyx;
+ }
+ // Check if the samples cross the boundary of the next whole integer or the
+ // tile repeat limit, whichever is lower.
+ float limit = floor(uv.x) + 1.0f;
+ if (tile_repeat > 0.0f) {
+ limit = min(limit, tile_repeat);
+ }
+ return uv.x >= 0.0f && uv.w < limit
+ ? (uv_step != 0.0f
+ ? int(clamp((limit - uv.x) / uv_step, 0.0f, float(steps)))
+ : steps)
+ : 0;
+}
+
+// Blends an entire span of texture with linear filtering and repeating UVs.
+template <bool BLEND, typename S, typename C, typename P>
+static int blendTextureLinearRepeat(S sampler, vec2 uv, int span,
+ const vec2_scalar& tile_repeat,
+ const vec4_scalar& uv_repeat,
+ const vec4_scalar& uv_rect, C color,
+ P* buf) {
+ if (!matchTextureFormat(sampler, buf)) {
+ return 0;
+ }
+ vec2_scalar uv_scale = {uv_repeat.z - uv_repeat.x, uv_repeat.w - uv_repeat.y};
+ vec2_scalar uv_offset = {uv_repeat.x, uv_repeat.y};
+ // Choose a linear filter to use for no-repeat sub-spans
+ LinearFilter filter =
+ needsTextureLinear(sampler, uv * uv_scale + uv_offset, span);
+ // We need to step UVs unscaled and unquantized so that we can modulo them
+ // with fract. We use uv_scale and uv_offset to map them into the correct
+ // range.
+ vec2_scalar uv_step =
+ float(swgl_StepSize) * vec2_scalar{uv.x.y - uv.x.x, uv.y.y - uv.y.x};
+ uv_scale = swgl_linearQuantizeStep(sampler, uv_scale);
+ uv_offset = swgl_linearQuantize(sampler, uv_offset);
+ vec2_scalar min_uv = max(
+ swgl_linearQuantize(sampler, vec2_scalar{uv_rect.x, uv_rect.y}), 0.0f);
+ vec2_scalar max_uv = max(
+ swgl_linearQuantize(sampler, vec2_scalar{uv_rect.z, uv_rect.w}), min_uv);
+ for (P* end = buf + span; buf < end; buf += swgl_StepSize, uv += uv_step) {
+ int steps = int(end - buf) / swgl_StepSize;
+ // Find the sub-span before UVs repeat to avoid expensive repeat math
+ steps = computeNoRepeatSteps(uv.x, uv_step.x, tile_repeat.x, steps);
+ if (steps > 0) {
+ steps = computeNoRepeatSteps(uv.y, uv_step.y, tile_repeat.y, steps);
+ if (steps > 0) {
+ buf = blendTextureLinearDispatch<BLEND>(
+ sampler, fract(uv) * uv_scale + uv_offset, steps * swgl_StepSize,
+ uv_step * uv_scale, min_uv, max_uv, color, buf, filter);
+ if (buf >= end) {
+ break;
+ }
+ uv += steps * uv_step;
+ }
+ }
+ // UVs might repeat within this step, so explicitly compute repeated UVs
+ vec2 repeated_uv = clamp(
+ tileRepeatUV(uv, tile_repeat) * uv_scale + uv_offset, min_uv, max_uv);
+ commit_blend_span<BLEND>(
+ buf, applyColor(textureLinearUnpacked(buf, sampler, ivec2(repeated_uv)),
+ color));
+ }
+ return span;
+}
+
+// Commit an entire span with linear filtering and repeating UVs
+#define swgl_commitTextureLinearRepeat(format, s, p, tile_repeat, uv_repeat, \
+ uv_rect, color) \
+ do { \
+ auto packed_color = packColor(swgl_Out##format, color); \
+ int drawn = 0; \
+ if (blend_key) { \
+ drawn = blendTextureLinearRepeat<true>(s, p, swgl_SpanLength, \
+ tile_repeat, uv_repeat, uv_rect, \
+ packed_color, swgl_Out##format); \
+ } else { \
+ drawn = blendTextureLinearRepeat<false>(s, p, swgl_SpanLength, \
+ tile_repeat, uv_repeat, uv_rect, \
+ packed_color, swgl_Out##format); \
+ } \
+ swgl_Out##format += drawn; \
+ swgl_SpanLength -= drawn; \
+ } while (0)
+#define swgl_commitTextureLinearRepeatRGBA8(s, p, tile_repeat, uv_repeat, \
+ uv_rect) \
+ swgl_commitTextureLinearRepeat(RGBA8, s, p, tile_repeat, uv_repeat, uv_rect, \
+ NoColor())
+#define swgl_commitTextureLinearRepeatColorRGBA8(s, p, tile_repeat, uv_repeat, \
+ uv_rect, color) \
+ swgl_commitTextureLinearRepeat(RGBA8, s, p, tile_repeat, uv_repeat, uv_rect, \
+ color)
+
+template <typename S>
+static ALWAYS_INLINE PackedRGBA8 textureNearestPacked(UNUSED uint32_t* buf,
+ S sampler, ivec2 i) {
+ return textureNearestPackedRGBA8(sampler, i);
+}
+
+// Blends an entire span of texture with nearest filtering and either
+// repeated or clamped UVs.
+template <bool BLEND, bool REPEAT, typename S, typename C, typename P>
+static int blendTextureNearestRepeat(S sampler, vec2 uv, int span,
+ const vec2_scalar& tile_repeat,
+ const vec4_scalar& uv_rect, C color,
+ P* buf) {
+ if (!matchTextureFormat(sampler, buf)) {
+ return 0;
+ }
+ if (!REPEAT) {
+ // If clamping, then we step pre-scaled to the sampler. For repeat modes,
+ // this will be accomplished via uv_scale instead.
+ uv = samplerScale(sampler, uv);
+ }
+ vec2_scalar uv_step =
+ float(swgl_StepSize) * vec2_scalar{uv.x.y - uv.x.x, uv.y.y - uv.y.x};
+ vec2_scalar min_uv = samplerScale(sampler, vec2_scalar{uv_rect.x, uv_rect.y});
+ vec2_scalar max_uv = samplerScale(sampler, vec2_scalar{uv_rect.z, uv_rect.w});
+ vec2_scalar uv_scale = max_uv - min_uv;
+ // If the effective sampling area of this texture is only a single pixel, then
+ // treat it as a solid span. For repeat modes, the bounds are specified on
+ // pixel boundaries, whereas for clamp modes, bounds are on pixel centers, so
+ // the test varies depending on which. If the sample range on an axis is
+ // greater than one pixel, we can still check if we don't move far enough from
+ // the pixel center on that axis to hit the next pixel.
+ if ((int(min_uv.x) + (REPEAT ? 1 : 0) >= int(max_uv.x) ||
+ (abs(uv_step.x) * span * (REPEAT ? uv_scale.x : 1.0f) < 0.5f)) &&
+ (int(min_uv.y) + (REPEAT ? 1 : 0) >= int(max_uv.y) ||
+ (abs(uv_step.y) * span * (REPEAT ? uv_scale.y : 1.0f) < 0.5f))) {
+ vec2 repeated_uv = REPEAT
+ ? tileRepeatUV(uv, tile_repeat) * uv_scale + min_uv
+ : clamp(uv, min_uv, max_uv);
+ commit_solid_span<BLEND>(buf,
+ applyColor(unpack(textureNearestPacked(
+ buf, sampler, ivec2(repeated_uv))),
+ color),
+ span);
+ } else {
+ for (P* end = buf + span; buf < end; buf += swgl_StepSize, uv += uv_step) {
+ if (REPEAT) {
+ int steps = int(end - buf) / swgl_StepSize;
+ // Find the sub-span before UVs repeat to avoid expensive repeat math
+ steps = computeNoRepeatSteps(uv.x, uv_step.x, tile_repeat.x, steps);
+ if (steps > 0) {
+ steps = computeNoRepeatSteps(uv.y, uv_step.y, tile_repeat.y, steps);
+ if (steps > 0) {
+ vec2 inside_uv = fract(uv) * uv_scale + min_uv;
+ vec2 inside_step = uv_step * uv_scale;
+ for (P* outside = &buf[steps * swgl_StepSize]; buf < outside;
+ buf += swgl_StepSize, inside_uv += inside_step) {
+ commit_blend_span<BLEND>(
+ buf, applyColor(
+ textureNearestPacked(buf, sampler, ivec2(inside_uv)),
+ color));
+ }
+ if (buf >= end) {
+ break;
+ }
+ uv += steps * uv_step;
+ }
+ }
+ }
+
+ // UVs might repeat within this step, so explicitly compute repeated UVs
+ vec2 repeated_uv = REPEAT
+ ? tileRepeatUV(uv, tile_repeat) * uv_scale + min_uv
+ : clamp(uv, min_uv, max_uv);
+ commit_blend_span<BLEND>(
+ buf,
+ applyColor(textureNearestPacked(buf, sampler, ivec2(repeated_uv)),
+ color));
+ }
+ }
+ return span;
+}
+
+// Determine if we can use the fast nearest filter for the given nearest mode.
+// If the Y coordinate varies more than half a pixel over
+// the span (which might cause the texel to alias to the next one), or the span
+// needs X scaling, then we have to use the fallback.
+template <typename S, typename T>
+static ALWAYS_INLINE bool needsNearestFallback(S sampler, T P, int span) {
+ P = samplerScale(sampler, P);
+ return (P.y.y - P.y.x) * span >= 0.5f || spanNeedsScale(span, P);
+}
+
+// Commit an entire span with nearest filtering and either clamped or repeating
+// UVs
+#define swgl_commitTextureNearest(format, s, p, uv_rect, color) \
+ do { \
+ auto packed_color = packColor(swgl_Out##format, color); \
+ int drawn = 0; \
+ if (needsNearestFallback(s, p, swgl_SpanLength)) { \
+ if (blend_key) { \
+ drawn = blendTextureNearestRepeat<true, false>( \
+ s, p, swgl_SpanLength, 0.0f, uv_rect, packed_color, \
+ swgl_Out##format); \
+ } else { \
+ drawn = blendTextureNearestRepeat<false, false>( \
+ s, p, swgl_SpanLength, 0.0f, uv_rect, packed_color, \
+ swgl_Out##format); \
+ } \
+ } else if (blend_key) { \
+ drawn = blendTextureNearestFast<true>(s, p, swgl_SpanLength, uv_rect, \
+ packed_color, swgl_Out##format); \
+ } else { \
+ drawn = blendTextureNearestFast<false>(s, p, swgl_SpanLength, uv_rect, \
+ packed_color, swgl_Out##format); \
+ } \
+ swgl_Out##format += drawn; \
+ swgl_SpanLength -= drawn; \
+ } while (0)
+#define swgl_commitTextureNearestRGBA8(s, p, uv_rect) \
+ swgl_commitTextureNearest(RGBA8, s, p, uv_rect, NoColor())
+#define swgl_commitTextureNearestColorRGBA8(s, p, uv_rect, color) \
+ swgl_commitTextureNearest(RGBA8, s, p, uv_rect, color)
+
+#define swgl_commitTextureNearestRepeat(format, s, p, tile_repeat, uv_rect, \
+ color) \
+ do { \
+ auto packed_color = packColor(swgl_Out##format, color); \
+ int drawn = 0; \
+ if (blend_key) { \
+ drawn = blendTextureNearestRepeat<true, true>( \
+ s, p, swgl_SpanLength, tile_repeat, uv_rect, packed_color, \
+ swgl_Out##format); \
+ } else { \
+ drawn = blendTextureNearestRepeat<false, true>( \
+ s, p, swgl_SpanLength, tile_repeat, uv_rect, packed_color, \
+ swgl_Out##format); \
+ } \
+ swgl_Out##format += drawn; \
+ swgl_SpanLength -= drawn; \
+ } while (0)
+#define swgl_commitTextureNearestRepeatRGBA8(s, p, tile_repeat, uv_repeat, \
+ uv_rect) \
+ swgl_commitTextureNearestRepeat(RGBA8, s, p, tile_repeat, uv_repeat, \
+ NoColor())
+#define swgl_commitTextureNearestRepeatColorRGBA8(s, p, tile_repeat, \
+ uv_repeat, uv_rect, color) \
+ swgl_commitTextureNearestRepeat(RGBA8, s, p, tile_repeat, uv_repeat, color)
+
+// Commit an entire span of texture with filtering determined by sampler state.
+#define swgl_commitTexture(format, s, ...) \
+ do { \
+ if (s->filter == TextureFilter::LINEAR) { \
+ swgl_commitTextureLinear##format(s, __VA_ARGS__); \
+ } else { \
+ swgl_commitTextureNearest##format(s, __VA_ARGS__); \
+ } \
+ } while (0)
+#define swgl_commitTextureRGBA8(...) swgl_commitTexture(RGBA8, __VA_ARGS__)
+#define swgl_commitTextureColorRGBA8(...) \
+ swgl_commitTexture(ColorRGBA8, __VA_ARGS__)
+#define swgl_commitTextureRepeatRGBA8(...) \
+ swgl_commitTexture(RepeatRGBA8, __VA_ARGS__)
+#define swgl_commitTextureRepeatColorRGBA8(...) \
+ swgl_commitTexture(RepeatColorRGBA8, __VA_ARGS__)
+
+// Commit an entire span of a separable pass of a Gaussian blur that falls
+// within the given radius scaled by supplied coefficients, clamped to uv_rect
+// bounds.
+template <bool BLEND, typename S, typename P>
+static int blendGaussianBlur(S sampler, vec2 uv, const vec4_scalar& uv_rect,
+ P* buf, int span, bool hori, int radius,
+ vec2_scalar coeffs) {
+ if (!matchTextureFormat(sampler, buf)) {
+ return 0;
+ }
+ vec2_scalar size = {float(sampler->width), float(sampler->height)};
+ ivec2_scalar curUV = make_ivec2(force_scalar(uv) * size);
+ ivec4_scalar bounds = make_ivec4(uv_rect * make_vec4(size, size));
+ int startX = curUV.x;
+ int endX = min(min(bounds.z, curUV.x + span), int(size.x));
+ if (hori) {
+ for (; curUV.x + swgl_StepSize <= endX;
+ buf += swgl_StepSize, curUV.x += swgl_StepSize) {
+ commit_blend_span<BLEND>(
+ buf, gaussianBlurHorizontal<P>(sampler, curUV, bounds.x, bounds.z,
+ radius, coeffs.x, coeffs.y));
+ }
+ } else {
+ for (; curUV.x + swgl_StepSize <= endX;
+ buf += swgl_StepSize, curUV.x += swgl_StepSize) {
+ commit_blend_span<BLEND>(
+ buf, gaussianBlurVertical<P>(sampler, curUV, bounds.y, bounds.w,
+ radius, coeffs.x, coeffs.y));
+ }
+ }
+ return curUV.x - startX;
+}
+
+#define swgl_commitGaussianBlur(format, s, p, uv_rect, hori, radius, coeffs) \
+ do { \
+ int drawn = 0; \
+ if (blend_key) { \
+ drawn = blendGaussianBlur<true>(s, p, uv_rect, swgl_Out##format, \
+ swgl_SpanLength, hori, radius, coeffs); \
+ } else { \
+ drawn = blendGaussianBlur<false>(s, p, uv_rect, swgl_Out##format, \
+ swgl_SpanLength, hori, radius, coeffs); \
+ } \
+ swgl_Out##format += drawn; \
+ swgl_SpanLength -= drawn; \
+ } while (0)
+#define swgl_commitGaussianBlurRGBA8(s, p, uv_rect, hori, radius, coeffs) \
+ swgl_commitGaussianBlur(RGBA8, s, p, uv_rect, hori, radius, coeffs)
+#define swgl_commitGaussianBlurR8(s, p, uv_rect, hori, radius, coeffs) \
+ swgl_commitGaussianBlur(R8, s, p, uv_rect, hori, radius, coeffs)
+
+// Convert and pack planar YUV samples to RGB output using a color space
+static ALWAYS_INLINE PackedRGBA8 convertYUV(const YUVMatrix& rgb_from_ycbcr,
+ U16 y, U16 u, U16 v) {
+ auto yy = V8<int16_t>(zip(y, y));
+ auto uv = V8<int16_t>(zip(u, v));
+ return rgb_from_ycbcr.convert(yy, uv);
+}
+
+// Helper functions to sample from planar YUV textures before converting to RGB
+template <typename S0>
+static ALWAYS_INLINE PackedRGBA8 sampleYUV(S0 sampler0, ivec2 uv0,
+ const YUVMatrix& rgb_from_ycbcr,
+ UNUSED int rescaleFactor) {
+ switch (sampler0->format) {
+ case TextureFormat::RGBA8: {
+ auto planar = textureLinearPlanarRGBA8(sampler0, uv0);
+ return convertYUV(rgb_from_ycbcr, highHalf(planar.rg), lowHalf(planar.rg),
+ lowHalf(planar.ba));
+ }
+ case TextureFormat::YUV422: {
+ auto planar = textureLinearPlanarYUV422(sampler0, uv0);
+ return convertYUV(rgb_from_ycbcr, planar.y, planar.u, planar.v);
+ }
+ default:
+ assert(false);
+ return PackedRGBA8(0);
+ }
+}
+
+template <bool BLEND, typename S0, typename P, typename C = NoColor>
+static int blendYUV(P* buf, int span, S0 sampler0, vec2 uv0,
+ const vec4_scalar& uv_rect0, const vec3_scalar& ycbcr_bias,
+ const mat3_scalar& rgb_from_debiased_ycbcr,
+ int rescaleFactor, C color = C()) {
+ if (!swgl_isTextureLinear(sampler0)) {
+ return 0;
+ }
+ LINEAR_QUANTIZE_UV(sampler0, uv0, uv_step0, uv_rect0, min_uv0, max_uv0);
+ const auto rgb_from_ycbcr =
+ YUVMatrix::From(ycbcr_bias, rgb_from_debiased_ycbcr, rescaleFactor);
+ auto c = packColor(buf, color);
+ auto* end = buf + span;
+ for (; buf < end; buf += swgl_StepSize, uv0 += uv_step0) {
+ commit_blend_span<BLEND>(
+ buf, applyColor(sampleYUV(sampler0, ivec2(clamp(uv0, min_uv0, max_uv0)),
+ rgb_from_ycbcr, rescaleFactor),
+ c));
+ }
+ return span;
+}
+
+template <typename S0, typename S1>
+static ALWAYS_INLINE PackedRGBA8 sampleYUV(S0 sampler0, ivec2 uv0, S1 sampler1,
+ ivec2 uv1,
+ const YUVMatrix& rgb_from_ycbcr,
+ int rescaleFactor) {
+ switch (sampler1->format) {
+ case TextureFormat::RG8: {
+ assert(sampler0->format == TextureFormat::R8);
+ auto y = textureLinearUnpackedR8(sampler0, uv0);
+ auto planar = textureLinearPlanarRG8(sampler1, uv1);
+ return convertYUV(rgb_from_ycbcr, y, lowHalf(planar.rg),
+ highHalf(planar.rg));
+ }
+ case TextureFormat::RGBA8: {
+ assert(sampler0->format == TextureFormat::R8);
+ auto y = textureLinearUnpackedR8(sampler0, uv0);
+ auto planar = textureLinearPlanarRGBA8(sampler1, uv1);
+ return convertYUV(rgb_from_ycbcr, y, lowHalf(planar.ba),
+ highHalf(planar.rg));
+ }
+ case TextureFormat::RG16: {
+ assert(sampler0->format == TextureFormat::R16);
+ // The rescaling factor represents how many bits to add to renormalize the
+ // texture to 16 bits, and so the color depth is actually 16 minus the
+ // rescaling factor.
+ // Need to right shift the sample by the amount of bits over 8 it
+ // occupies. On output from textureLinearUnpackedR16, we have lost 1 bit
+ // of precision at the low end already, hence 1 is subtracted from the
+ // color depth.
+ int colorDepth = 16 - rescaleFactor;
+ int rescaleBits = (colorDepth - 1) - 8;
+ auto y = textureLinearUnpackedR16(sampler0, uv0) >> rescaleBits;
+ auto uv = textureLinearUnpackedRG16(sampler1, uv1) >> rescaleBits;
+ return rgb_from_ycbcr.convert(zip(y, y), uv);
+ }
+ default:
+ assert(false);
+ return PackedRGBA8(0);
+ }
+}
+
+template <bool BLEND, typename S0, typename S1, typename P,
+ typename C = NoColor>
+static int blendYUV(P* buf, int span, S0 sampler0, vec2 uv0,
+ const vec4_scalar& uv_rect0, S1 sampler1, vec2 uv1,
+ const vec4_scalar& uv_rect1, const vec3_scalar& ycbcr_bias,
+ const mat3_scalar& rgb_from_debiased_ycbcr,
+ int rescaleFactor, C color = C()) {
+ if (!swgl_isTextureLinear(sampler0) || !swgl_isTextureLinear(sampler1)) {
+ return 0;
+ }
+ LINEAR_QUANTIZE_UV(sampler0, uv0, uv_step0, uv_rect0, min_uv0, max_uv0);
+ LINEAR_QUANTIZE_UV(sampler1, uv1, uv_step1, uv_rect1, min_uv1, max_uv1);
+ const auto rgb_from_ycbcr =
+ YUVMatrix::From(ycbcr_bias, rgb_from_debiased_ycbcr, rescaleFactor);
+ auto c = packColor(buf, color);
+ auto* end = buf + span;
+ for (; buf < end; buf += swgl_StepSize, uv0 += uv_step0, uv1 += uv_step1) {
+ commit_blend_span<BLEND>(
+ buf, applyColor(sampleYUV(sampler0, ivec2(clamp(uv0, min_uv0, max_uv0)),
+ sampler1, ivec2(clamp(uv1, min_uv1, max_uv1)),
+ rgb_from_ycbcr, rescaleFactor),
+ c));
+ }
+ return span;
+}
+
+template <typename S0, typename S1, typename S2>
+static ALWAYS_INLINE PackedRGBA8 sampleYUV(S0 sampler0, ivec2 uv0, S1 sampler1,
+ ivec2 uv1, S2 sampler2, ivec2 uv2,
+ const YUVMatrix& rgb_from_ycbcr,
+ int rescaleFactor) {
+ assert(sampler0->format == sampler1->format &&
+ sampler0->format == sampler2->format);
+ switch (sampler0->format) {
+ case TextureFormat::R8: {
+ auto y = textureLinearUnpackedR8(sampler0, uv0);
+ auto u = textureLinearUnpackedR8(sampler1, uv1);
+ auto v = textureLinearUnpackedR8(sampler2, uv2);
+ return convertYUV(rgb_from_ycbcr, y, u, v);
+ }
+ case TextureFormat::R16: {
+ // The rescaling factor represents how many bits to add to renormalize the
+ // texture to 16 bits, and so the color depth is actually 16 minus the
+ // rescaling factor.
+ // Need to right shift the sample by the amount of bits over 8 it
+ // occupies. On output from textureLinearUnpackedR16, we have lost 1 bit
+ // of precision at the low end already, hence 1 is subtracted from the
+ // color depth.
+ int colorDepth = 16 - rescaleFactor;
+ int rescaleBits = (colorDepth - 1) - 8;
+ auto y = textureLinearUnpackedR16(sampler0, uv0) >> rescaleBits;
+ auto u = textureLinearUnpackedR16(sampler1, uv1) >> rescaleBits;
+ auto v = textureLinearUnpackedR16(sampler2, uv2) >> rescaleBits;
+ return convertYUV(rgb_from_ycbcr, U16(y), U16(u), U16(v));
+ }
+ default:
+ assert(false);
+ return PackedRGBA8(0);
+ }
+}
+
+// Fallback helper for when we can't specifically accelerate YUV with
+// composition.
+template <bool BLEND, typename S0, typename S1, typename S2, typename P,
+ typename C>
+static void blendYUVFallback(P* buf, int span, S0 sampler0, vec2 uv0,
+ vec2_scalar uv_step0, vec2_scalar min_uv0,
+ vec2_scalar max_uv0, S1 sampler1, vec2 uv1,
+ vec2_scalar uv_step1, vec2_scalar min_uv1,
+ vec2_scalar max_uv1, S2 sampler2, vec2 uv2,
+ vec2_scalar uv_step2, vec2_scalar min_uv2,
+ vec2_scalar max_uv2, const vec3_scalar& ycbcr_bias,
+ const mat3_scalar& rgb_from_debiased_ycbcr,
+ int rescaleFactor, C color) {
+ const auto rgb_from_ycbcr =
+ YUVMatrix::From(ycbcr_bias, rgb_from_debiased_ycbcr, rescaleFactor);
+ for (auto* end = buf + span; buf < end; buf += swgl_StepSize, uv0 += uv_step0,
+ uv1 += uv_step1, uv2 += uv_step2) {
+ commit_blend_span<BLEND>(
+ buf, applyColor(sampleYUV(sampler0, ivec2(clamp(uv0, min_uv0, max_uv0)),
+ sampler1, ivec2(clamp(uv1, min_uv1, max_uv1)),
+ sampler2, ivec2(clamp(uv2, min_uv2, max_uv2)),
+ rgb_from_ycbcr, rescaleFactor),
+ color));
+ }
+}
+
+template <bool BLEND, typename S0, typename S1, typename S2, typename P,
+ typename C = NoColor>
+static int blendYUV(P* buf, int span, S0 sampler0, vec2 uv0,
+ const vec4_scalar& uv_rect0, S1 sampler1, vec2 uv1,
+ const vec4_scalar& uv_rect1, S2 sampler2, vec2 uv2,
+ const vec4_scalar& uv_rect2, const vec3_scalar& ycbcr_bias,
+ const mat3_scalar& rgb_from_debiased_ycbcr,
+ int rescaleFactor, C color = C()) {
+ if (!swgl_isTextureLinear(sampler0) || !swgl_isTextureLinear(sampler1) ||
+ !swgl_isTextureLinear(sampler2)) {
+ return 0;
+ }
+ LINEAR_QUANTIZE_UV(sampler0, uv0, uv_step0, uv_rect0, min_uv0, max_uv0);
+ LINEAR_QUANTIZE_UV(sampler1, uv1, uv_step1, uv_rect1, min_uv1, max_uv1);
+ LINEAR_QUANTIZE_UV(sampler2, uv2, uv_step2, uv_rect2, min_uv2, max_uv2);
+ auto c = packColor(buf, color);
+ blendYUVFallback<BLEND>(buf, span, sampler0, uv0, uv_step0, min_uv0, max_uv0,
+ sampler1, uv1, uv_step1, min_uv1, max_uv1, sampler2,
+ uv2, uv_step2, min_uv2, max_uv2, ycbcr_bias,
+ rgb_from_debiased_ycbcr, rescaleFactor, c);
+ return span;
+}
+
+// A variant of the blendYUV that attempts to reuse the inner loops from the
+// CompositeYUV infrastructure. CompositeYUV imposes stricter requirements on
+// the source data, which in turn allows it to be much faster than blendYUV.
+// At a minimum, we need to ensure that we are outputting to a BGRA8 framebuffer
+// and that no color scaling is applied, which we can accomplish via template
+// specialization. We need to further validate inside that texture formats
+// and dimensions are sane for video and that the video is axis-aligned before
+// acceleration can proceed.
+template <bool BLEND>
+static int blendYUV(uint32_t* buf, int span, sampler2DRect sampler0, vec2 uv0,
+ const vec4_scalar& uv_rect0, sampler2DRect sampler1,
+ vec2 uv1, const vec4_scalar& uv_rect1,
+ sampler2DRect sampler2, vec2 uv2,
+ const vec4_scalar& uv_rect2, const vec3_scalar& ycbcr_bias,
+ const mat3_scalar& rgb_from_debiased_ycbcr,
+ int rescaleFactor, NoColor noColor = NoColor()) {
+ if (!swgl_isTextureLinear(sampler0) || !swgl_isTextureLinear(sampler1) ||
+ !swgl_isTextureLinear(sampler2)) {
+ return 0;
+ }
+ LINEAR_QUANTIZE_UV(sampler0, uv0, uv_step0, uv_rect0, min_uv0, max_uv0);
+ LINEAR_QUANTIZE_UV(sampler1, uv1, uv_step1, uv_rect1, min_uv1, max_uv1);
+ LINEAR_QUANTIZE_UV(sampler2, uv2, uv_step2, uv_rect2, min_uv2, max_uv2);
+ auto* end = buf + span;
+ // CompositeYUV imposes further restrictions on the source textures, such that
+ // the the Y/U/V samplers must all have a matching format, the U/V samplers
+ // must have matching sizes and sample coordinates, and there must be no
+ // change in row across the entire span.
+ if (sampler0->format == sampler1->format &&
+ sampler1->format == sampler2->format &&
+ sampler1->width == sampler2->width &&
+ sampler1->height == sampler2->height && uv_step0.y == 0 &&
+ uv_step0.x > 0 && uv_step1.y == 0 && uv_step1.x > 0 &&
+ uv_step1 == uv_step2 && uv1.x.x == uv2.x.x && uv1.y.x == uv2.y.x) {
+ // CompositeYUV does not support a clamp rect, so we must take care to
+ // advance till we're inside the bounds of the clamp rect.
+ int outside = min(int(ceil(max((min_uv0.x - uv0.x.x) / uv_step0.x,
+ (min_uv1.x - uv1.x.x) / uv_step1.x))),
+ (end - buf) / swgl_StepSize);
+ if (outside > 0) {
+ blendYUVFallback<BLEND>(buf, outside * swgl_StepSize, sampler0, uv0,
+ uv_step0, min_uv0, max_uv0, sampler1, uv1,
+ uv_step1, min_uv1, max_uv1, sampler2, uv2,
+ uv_step2, min_uv2, max_uv2, ycbcr_bias,
+ rgb_from_debiased_ycbcr, rescaleFactor, noColor);
+ buf += outside * swgl_StepSize;
+ uv0.x += outside * uv_step0.x;
+ uv1.x += outside * uv_step1.x;
+ uv2.x += outside * uv_step2.x;
+ }
+ // Find the amount of chunks inside the clamp rect before we hit the
+ // maximum. If there are any chunks inside, we can finally dispatch to
+ // CompositeYUV.
+ int inside = min(int(min((max_uv0.x - uv0.x.x) / uv_step0.x,
+ (max_uv1.x - uv1.x.x) / uv_step1.x)),
+ (end - buf) / swgl_StepSize);
+ if (inside > 0) {
+ // We need the color depth, which is relative to the texture format and
+ // rescale factor.
+ int colorDepth =
+ (sampler0->format == TextureFormat::R16 ? 16 : 8) - rescaleFactor;
+ // Finally, call the inner loop of CompositeYUV.
+ const auto rgb_from_ycbcr =
+ YUVMatrix::From(ycbcr_bias, rgb_from_debiased_ycbcr, rescaleFactor);
+ linear_row_yuv<BLEND>(
+ buf, inside * swgl_StepSize, sampler0, force_scalar(uv0),
+ uv_step0.x / swgl_StepSize, sampler1, sampler2, force_scalar(uv1),
+ uv_step1.x / swgl_StepSize, colorDepth, rgb_from_ycbcr);
+ // Now that we're done, advance past the processed inside portion.
+ buf += inside * swgl_StepSize;
+ uv0.x += inside * uv_step0.x;
+ uv1.x += inside * uv_step1.x;
+ uv2.x += inside * uv_step2.x;
+ }
+ }
+ // We either got here because we have some samples outside the clamp rect, or
+ // because some of the preconditions were not satisfied. Process whatever is
+ // left of the span.
+ blendYUVFallback<BLEND>(buf, end - buf, sampler0, uv0, uv_step0, min_uv0,
+ max_uv0, sampler1, uv1, uv_step1, min_uv1, max_uv1,
+ sampler2, uv2, uv_step2, min_uv2, max_uv2, ycbcr_bias,
+ rgb_from_debiased_ycbcr, rescaleFactor, noColor);
+ return span;
+}
+
+// Commit a single chunk of a YUV surface represented by multiple planar
+// textures. This requires a color space specifier selecting how to convert
+// from YUV to RGB output. In the case of HDR formats, a rescaling factor
+// selects how many bits of precision must be utilized on conversion. See the
+// sampleYUV dispatcher functions for the various supported plane
+// configurations this intrinsic accepts.
+#define swgl_commitTextureLinearYUV(...) \
+ do { \
+ int drawn = 0; \
+ if (blend_key) { \
+ drawn = blendYUV<true>(swgl_OutRGBA8, swgl_SpanLength, __VA_ARGS__); \
+ } else { \
+ drawn = blendYUV<false>(swgl_OutRGBA8, swgl_SpanLength, __VA_ARGS__); \
+ } \
+ swgl_OutRGBA8 += drawn; \
+ swgl_SpanLength -= drawn; \
+ } while (0)
+
+// Commit a single chunk of a YUV surface scaled by a color.
+#define swgl_commitTextureLinearColorYUV(...) \
+ swgl_commitTextureLinearYUV(__VA_ARGS__)
+
+// Each gradient stops entry is a pair of RGBA32F start color and end step.
+struct GradientStops {
+ Float startColor;
+ union {
+ Float stepColor;
+ vec4_scalar stepData;
+ };
+
+ // Whether this gradient entry can be merged with an adjacent entry. The
+ // step will be equal with the adjacent step if and only if they can be
+ // merged, or rather, that the stops are actually part of a single larger
+ // gradient.
+ bool can_merge(const GradientStops& next) const {
+ return stepData == next.stepData;
+ }
+
+ // Get the interpolated color within the entry based on the offset from its
+ // start.
+ Float interpolate(float offset) const {
+ return startColor + stepColor * offset;
+ }
+
+ // Get the end color of the entry where interpolation stops.
+ Float end_color() const { return startColor + stepColor; }
+};
+
+// Checks if a gradient table of the specified size exists at the UV coords of
+// the address within an RGBA32F texture. If so, a linear address within the
+// texture is returned that may be used to sample the gradient table later. If
+// the address doesn't describe a valid gradient, then a negative value is
+// returned.
+static inline int swgl_validateGradient(sampler2D sampler, ivec2_scalar address,
+ int entries) {
+ return sampler->format == TextureFormat::RGBA32F && address.y >= 0 &&
+ address.y < int(sampler->height) && address.x >= 0 &&
+ address.x < int(sampler->width) && entries > 0 &&
+ address.x +
+ int(sizeof(GradientStops) / sizeof(Float)) * entries <=
+ int(sampler->width)
+ ? address.y * sampler->stride + address.x * 4
+ : -1;
+}
+
+static inline WideRGBA8 sampleGradient(sampler2D sampler, int address,
+ Float entry) {
+ assert(sampler->format == TextureFormat::RGBA32F);
+ assert(address >= 0 && address < int(sampler->height * sampler->stride));
+ // Get the integer portion of the entry index to find the entry colors.
+ I32 index = cast(entry);
+ // Use the fractional portion of the entry index to control blending between
+ // entry colors.
+ Float offset = entry - cast(index);
+ // Every entry is a pair of colors blended by the fractional offset.
+ assert(test_all(index >= 0 &&
+ index * int(sizeof(GradientStops) / sizeof(Float)) <
+ int(sampler->width)));
+ GradientStops* stops = (GradientStops*)&sampler->buf[address];
+ // Blend between the colors for each SIMD lane, then pack them to RGBA8
+ // result. Since the layout of the RGBA8 framebuffer is actually BGRA while
+ // the gradient table has RGBA colors, swizzling is required.
+ return combine(
+ packRGBA8(round_pixel(stops[index.x].interpolate(offset.x).zyxw),
+ round_pixel(stops[index.y].interpolate(offset.y).zyxw)),
+ packRGBA8(round_pixel(stops[index.z].interpolate(offset.z).zyxw),
+ round_pixel(stops[index.w].interpolate(offset.w).zyxw)));
+}
+
+// Samples a gradient entry from the gradient at the provided linearized
+// address. The integer portion of the entry index is used to find the entry
+// within the table whereas the fractional portion is used to blend between
+// adjacent table entries.
+#define swgl_commitGradientRGBA8(sampler, address, entry) \
+ swgl_commitChunk(RGBA8, sampleGradient(sampler, address, entry))
+
+// Variant that allows specifying a color multiplier of the gradient result.
+#define swgl_commitGradientColorRGBA8(sampler, address, entry, color) \
+ swgl_commitChunk(RGBA8, applyColor(sampleGradient(sampler, address, entry), \
+ packColor(swgl_OutRGBA, color)))
+
+// Samples an entire span of a linear gradient by crawling the gradient table
+// and looking for consecutive stops that can be merged into a single larger
+// gradient, then interpolating between those larger gradients within the span.
+template <bool BLEND>
+static bool commitLinearGradient(sampler2D sampler, int address, float size,
+ bool tileRepeat, bool gradientRepeat, vec2 pos,
+ const vec2_scalar& scaleDir, float startOffset,
+ uint32_t* buf, int span) {
+ assert(sampler->format == TextureFormat::RGBA32F);
+ assert(address >= 0 && address < int(sampler->height * sampler->stride));
+ GradientStops* stops = (GradientStops*)&sampler->buf[address];
+ // Get the chunk delta from the difference in offset steps. This represents
+ // how far within the gradient table we advance for every step in output,
+ // normalized to gradient table size.
+ vec2_scalar posStep = dFdx(pos) * 4.0f;
+ float delta = dot(posStep, scaleDir);
+ if (!isfinite(delta)) {
+ return false;
+ }
+ // If we have a repeating brush, then the position will be modulo the [0,1)
+ // interval. Compute coefficients that can be used to quickly evaluate the
+ // distance to the interval boundary where the offset will wrap.
+ vec2_scalar distCoeffsX = {0.25f * span, 0.0f};
+ vec2_scalar distCoeffsY = distCoeffsX;
+ if (tileRepeat) {
+ if (posStep.x != 0.0f) {
+ distCoeffsX = vec2_scalar{step(0.0f, posStep.x), 1.0f} * recip(posStep.x);
+ }
+ if (posStep.y != 0.0f) {
+ distCoeffsY = vec2_scalar{step(0.0f, posStep.y), 1.0f} * recip(posStep.y);
+ }
+ }
+ for (; span > 0;) {
+ // Try to process as many chunks as are within the span if possible.
+ float chunks = 0.25f * span;
+ vec2 repeatPos = pos;
+ if (tileRepeat) {
+ // If this is a repeating brush, then limit the chunks to not cross the
+ // interval boundaries.
+ repeatPos = fract(pos);
+ chunks = min(chunks, distCoeffsX.x - repeatPos.x.x * distCoeffsX.y);
+ chunks = min(chunks, distCoeffsY.x - repeatPos.y.x * distCoeffsY.y);
+ }
+ // Compute the gradient offset from the position.
+ Float offset =
+ repeatPos.x * scaleDir.x + repeatPos.y * scaleDir.y - startOffset;
+ // If repeat is desired, we need to limit the offset to a fractional value.
+ if (gradientRepeat) {
+ offset = fract(offset);
+ }
+ // To properly handle both clamping and repeating of the table offset, we
+ // need to ensure we don't run past the 0 and 1 points. Here we compute the
+ // intercept points depending on whether advancing forwards or backwards in
+ // the gradient table to ensure the chunk count is limited by the amount
+ // before intersection. If there is no delta, then we compute no intercept.
+ float startEntry;
+ int minIndex, maxIndex;
+ if (offset.x < 0) {
+ // If we're below the gradient table, use the first color stop. We can
+ // only intercept the table if walking forward.
+ startEntry = 0;
+ minIndex = int(startEntry);
+ maxIndex = minIndex;
+ if (delta > 0) {
+ chunks = min(chunks, -offset.x / delta);
+ }
+ } else if (offset.x < 1) {
+ // Otherwise, we're inside the gradient table. Depending on the direction
+ // we're walking the the table, we may intersect either the 0 or 1 offset.
+ // Compute the start entry based on our initial offset, and compute the
+ // end entry based on the available chunks limited by intercepts. Clamp
+ // them into the valid range of the table.
+ startEntry = 1.0f + offset.x * size;
+ if (delta < 0) {
+ chunks = min(chunks, -offset.x / delta);
+ } else if (delta > 0) {
+ chunks = min(chunks, (1 - offset.x) / delta);
+ }
+ float endEntry = clamp(1.0f + (offset.x + delta * int(chunks)) * size,
+ 0.0f, 1.0f + size);
+ // Now that we know the range of entries we need to sample, we want to
+ // find the largest possible merged gradient within that range. Depending
+ // on which direction we are advancing in the table, we either walk up or
+ // down the table trying to merge the current entry with the adjacent
+ // entry. We finally limit the chunks to only sample from this merged
+ // gradient.
+ minIndex = int(startEntry);
+ maxIndex = minIndex;
+ if (delta > 0) {
+ while (maxIndex + 1 < endEntry &&
+ stops[maxIndex].can_merge(stops[maxIndex + 1])) {
+ maxIndex++;
+ }
+ chunks = min(chunks, (maxIndex + 1 - startEntry) / (delta * size));
+ } else if (delta < 0) {
+ while (minIndex - 1 > endEntry &&
+ stops[minIndex - 1].can_merge(stops[minIndex])) {
+ minIndex--;
+ }
+ chunks = min(chunks, (minIndex - startEntry) / (delta * size));
+ }
+ } else {
+ // If we're above the gradient table, use the last color stop. We can
+ // only intercept the table if walking backward.
+ startEntry = 1.0f + size;
+ minIndex = int(startEntry);
+ maxIndex = minIndex;
+ if (delta < 0) {
+ chunks = min(chunks, (1 - offset.x) / delta);
+ }
+ }
+ // If there are any amount of whole chunks of a merged gradient found,
+ // then we want to process that as a single gradient span with the start
+ // and end colors from the min and max entries.
+ if (chunks >= 1.0f) {
+ int inside = int(chunks);
+ // Sample the start color from the min entry and the end color from the
+ // max entry of the merged gradient. These are scaled to a range of
+ // 0..0xFF00, as that is the largest shifted value that can fit in a U16.
+ // Since we are only doing addition with the step value, we can still
+ // represent negative step values without having to use an explicit sign
+ // bit, as the result will still come out the same, allowing us to gain an
+ // extra bit of precision. We will later shift these into 8 bit output
+ // range while committing the span, but stepping with higher precision to
+ // avoid banding. We convert from RGBA to BGRA here to avoid doing this in
+ // the inner loop.
+ auto minColorF = stops[minIndex].startColor.zyxw * float(0xFF00);
+ auto maxColorF = stops[maxIndex].end_color().zyxw * float(0xFF00);
+ // Get the color range of the merged gradient, normalized to its size.
+ auto colorRangeF =
+ (maxColorF - minColorF) * (1.0f / (maxIndex + 1 - minIndex));
+ // Compute the actual starting color of the current start offset within
+ // the merged gradient. The value 0.5 is added to the low bits (0x80) so
+ // that the color will effective round to the nearest increment below.
+ auto colorF =
+ minColorF + colorRangeF * (startEntry - minIndex) + float(0x80);
+ // Compute the portion of the color range that we advance on each chunk.
+ Float deltaColorF = colorRangeF * (delta * size);
+ // Quantize the color delta and current color. These have already been
+ // scaled to the 0..0xFF00 range, so we just need to round them to U16.
+ auto deltaColor = repeat4(CONVERT(round_pixel(deltaColorF, 1), U16));
+ auto color =
+ combine(CONVERT(round_pixel(colorF, 1), U16),
+ CONVERT(round_pixel(colorF + deltaColorF * 0.25f, 1), U16),
+ CONVERT(round_pixel(colorF + deltaColorF * 0.5f, 1), U16),
+ CONVERT(round_pixel(colorF + deltaColorF * 0.75f, 1), U16));
+ // Finally, step the current color through the output chunks, shifting
+ // it into 8 bit range and outputting as we go.
+ for (auto* end = buf + inside * 4; buf < end; buf += 4) {
+ commit_blend_span<BLEND>(buf, bit_cast<WideRGBA8>(color >> 8));
+ color += deltaColor;
+ }
+ // Deduct the number of chunks inside the gradient from the remaining
+ // overall span. If we exhausted the span, bail out.
+ span -= inside * 4;
+ if (span <= 0) {
+ break;
+ }
+ // Otherwise, assume we're in a transitional section of the gradient that
+ // will probably require per-sample table lookups, so fall through below.
+ // We need to re-evaluate the position and offset first, though.
+ pos += posStep * float(inside);
+ repeatPos = tileRepeat ? fract(pos) : pos;
+ offset =
+ repeatPos.x * scaleDir.x + repeatPos.y * scaleDir.y - startOffset;
+ if (gradientRepeat) {
+ offset = fract(offset);
+ }
+ }
+ // If we get here, there were no whole chunks of a merged gradient found
+ // that we could process, but we still have a non-zero amount of span left.
+ // That means we have segments of gradient that begin or end at the current
+ // entry we're on. For this case, we just fall back to sampleGradient which
+ // will calculate a table entry for each sample, assuming the samples may
+ // have different table entries.
+ Float entry = clamp(offset * size + 1.0f, 0.0f, 1.0f + size);
+ commit_blend_span<BLEND>(buf, sampleGradient(sampler, address, entry));
+ span -= 4;
+ buf += 4;
+ pos += posStep;
+ }
+ return true;
+}
+
+// Commits an entire span of a linear gradient, given the address of a table
+// previously resolved with swgl_validateGradient. The size of the inner portion
+// of the table is given, assuming the table start and ends with a single entry
+// each to deal with clamping. Repeating will be handled if necessary. The
+// initial offset within the table is used to designate where to start the span
+// and how to step through the gradient table.
+#define swgl_commitLinearGradientRGBA8(sampler, address, size, tileRepeat, \
+ gradientRepeat, pos, scaleDir, \
+ startOffset) \
+ do { \
+ bool drawn = false; \
+ if (blend_key) { \
+ drawn = commitLinearGradient<true>( \
+ sampler, address, size, tileRepeat, gradientRepeat, pos, scaleDir, \
+ startOffset, swgl_OutRGBA8, swgl_SpanLength); \
+ } else { \
+ drawn = commitLinearGradient<false>( \
+ sampler, address, size, tileRepeat, gradientRepeat, pos, scaleDir, \
+ startOffset, swgl_OutRGBA8, swgl_SpanLength); \
+ } \
+ if (drawn) { \
+ swgl_OutRGBA8 += swgl_SpanLength; \
+ swgl_SpanLength = 0; \
+ } \
+ } while (0)
+
+template <bool CLAMP, typename V>
+static ALWAYS_INLINE V fastSqrt(V v) {
+#if USE_SSE2 || USE_NEON
+ // Clamp to avoid zero in inversesqrt.
+ return v * inversesqrt(CLAMP ? max(v, V(1.0e-10f)) : v);
+#else
+ return sqrt(v);
+#endif
+}
+
+template <bool CLAMP, typename V>
+static ALWAYS_INLINE auto fastLength(V v) {
+ return fastSqrt<CLAMP>(dot(v, v));
+}
+
+// Samples an entire span of a radial gradient by crawling the gradient table
+// and looking for consecutive stops that can be merged into a single larger
+// gradient, then interpolating between those larger gradients within the span
+// based on the computed position relative to a radius.
+template <bool BLEND>
+static bool commitRadialGradient(sampler2D sampler, int address, float size,
+ bool repeat, vec2 pos, float radius,
+ uint32_t* buf, int span) {
+ assert(sampler->format == TextureFormat::RGBA32F);
+ assert(address >= 0 && address < int(sampler->height * sampler->stride));
+ GradientStops* stops = (GradientStops*)&sampler->buf[address];
+ // clang-format off
+ // Given position p, delta d, and radius r, we need to repeatedly solve the
+ // following quadratic for the pixel offset t:
+ // length(p + t*d) = r
+ // (px + t*dx)^2 + (py + t*dy)^2 = r^2
+ // Rearranged into quadratic equation form (t^2*a + t*b + c = 0) this is:
+ // t^2*(dx^2+dy^2) + t*2*(dx*px+dy*py) + (px^2+py^2-r^2) = 0
+ // t^2*d.d + t*2*d.p + (p.p-r^2) = 0
+ // The solution of the quadratic formula t=(-b+-sqrt(b^2-4ac))/2a reduces to:
+ // t = -d.p/d.d +- sqrt((d.p/d.d)^2 - (p.p-r^2)/d.d)
+ // Note that d.p, d.d, p.p, and r^2 are constant across the gradient, and so
+ // we cache them below for faster computation.
+ //
+ // The quadratic has two solutions, representing the span intersecting the
+ // given radius of gradient, which can occur at two offsets. If there is only
+ // one solution (where b^2-4ac = 0), this represents the point at which the
+ // span runs tangent to the radius. This middle point is significant in that
+ // before it, we walk down the gradient ramp, and after it, we walk up the
+ // ramp.
+ // clang-format on
+ vec2_scalar pos0 = {pos.x.x, pos.y.x};
+ vec2_scalar delta = {pos.x.y - pos.x.x, pos.y.y - pos.y.x};
+ float deltaDelta = dot(delta, delta);
+ if (!isfinite(deltaDelta) || !isfinite(radius)) {
+ return false;
+ }
+ float invDelta, middleT, middleB;
+ if (deltaDelta > 0) {
+ invDelta = 1.0f / deltaDelta;
+ middleT = -dot(delta, pos0) * invDelta;
+ middleB = middleT * middleT - dot(pos0, pos0) * invDelta;
+ } else {
+ // If position is invariant, just set the coefficients so the quadratic
+ // always reduces to the end of the span.
+ invDelta = 0.0f;
+ middleT = float(span);
+ middleB = 0.0f;
+ }
+ // We only want search for merged gradients up to the minimum of either the
+ // mid-point or the span length. Cache those offsets here as they don't vary
+ // in the inner loop.
+ Float middleEndRadius = fastLength<true>(
+ pos0 + delta * (Float){middleT, float(span), 0.0f, 0.0f});
+ float middleRadius = span < middleT ? middleEndRadius.y : middleEndRadius.x;
+ float endRadius = middleEndRadius.y;
+ // Convert delta to change in position per chunk.
+ delta *= 4;
+ deltaDelta *= 4 * 4;
+ // clang-format off
+ // Given current position p and delta d, we reduce:
+ // length(p) = sqrt(dot(p,p)) = dot(p,p) * invsqrt(dot(p,p))
+ // where dot(p+d,p+d) can be accumulated as:
+ // (x+dx)^2+(y+dy)^2 = (x^2+y^2) + 2(x*dx+y*dy) + (dx^2+dy^2)
+ // = p.p + 2p.d + d.d
+ // Since p increases by d every loop iteration, p.d increases by d.d, and thus
+ // we can accumulate d.d to calculate 2p.d, then allowing us to get the next
+ // dot-product by adding it to dot-product p.p of the prior iteration. This
+ // saves us some multiplications and an expensive sqrt inside the inner loop.
+ // clang-format on
+ Float dotPos = dot(pos, pos);
+ Float dotPosDelta = 2.0f * dot(pos, delta) + deltaDelta;
+ float deltaDelta2 = 2.0f * deltaDelta;
+ for (int t = 0; t < span;) {
+ // Compute the gradient table offset from the current position.
+ Float offset = fastSqrt<true>(dotPos) - radius;
+ float startRadius = radius;
+ // If repeat is desired, we need to limit the offset to a fractional value.
+ if (repeat) {
+ // The non-repeating radius at which the gradient table actually starts,
+ // radius + floor(offset) = radius + (offset - fract(offset)).
+ startRadius += offset.x;
+ offset = fract(offset);
+ startRadius -= offset.x;
+ }
+ // We need to find the min/max index in the table of the gradient we want to
+ // use as well as the intercept point where we leave this gradient.
+ float intercept = -1;
+ int minIndex = 0;
+ int maxIndex = int(1.0f + size);
+ if (offset.x < 0) {
+ // If inside the inner radius of the gradient table, then use the first
+ // stop. Set the intercept to advance forward to the start of the gradient
+ // table.
+ maxIndex = minIndex;
+ if (t >= middleT) {
+ intercept = radius;
+ }
+ } else if (offset.x < 1) {
+ // Otherwise, we're inside the valid part of the gradient table.
+ minIndex = int(1.0f + offset.x * size);
+ maxIndex = minIndex;
+ // Find the offset in the gradient that corresponds to the search limit.
+ // We only search up to the minimum of either the mid-point or the span
+ // length. Get the table index that corresponds to this offset, clamped so
+ // that we avoid hitting the beginning (0) or end (1 + size) of the table.
+ float searchOffset =
+ (t >= middleT ? endRadius : middleRadius) - startRadius;
+ int searchIndex = int(clamp(1.0f + size * searchOffset, 1.0f, size));
+ // If we are past the mid-point, walk up the gradient table trying to
+ // merge stops. If we're below the mid-point, we need to walk down the
+ // table. We note the table index at which we need to look for an
+ // intercept to determine a valid span.
+ if (t >= middleT) {
+ while (maxIndex + 1 <= searchIndex &&
+ stops[maxIndex].can_merge(stops[maxIndex + 1])) {
+ maxIndex++;
+ }
+ intercept = maxIndex + 1;
+ } else {
+ while (minIndex - 1 >= searchIndex &&
+ stops[minIndex - 1].can_merge(stops[minIndex])) {
+ minIndex--;
+ }
+ intercept = minIndex;
+ }
+ // Convert from a table index into units of radius from the center of the
+ // gradient.
+ intercept = clamp((intercept - 1.0f) / size, 0.0f, 1.0f) + startRadius;
+ } else {
+ // If outside the outer radius of the gradient table, then use the last
+ // stop. Set the intercept to advance toward the valid part of the
+ // gradient table if going in, or just run to the end of the span if going
+ // away from the gradient.
+ minIndex = maxIndex;
+ if (t < middleT) {
+ intercept = radius + 1;
+ }
+ }
+ // Solve the quadratic for t to find where the merged gradient ends. If no
+ // intercept is found, just go to the middle or end of the span.
+ float endT = t >= middleT ? span : min(span, int(middleT));
+ if (intercept >= 0) {
+ float b = middleB + intercept * intercept * invDelta;
+ if (b > 0) {
+ b = fastSqrt<false>(b);
+ endT = min(endT, t >= middleT ? middleT + b : middleT - b);
+ }
+ }
+ // Figure out how many chunks are actually inside the merged gradient.
+ if (t + 4.0f <= endT) {
+ int inside = int(endT - t) & ~3;
+ // Convert start and end colors to BGRA and scale to 0..255 range later.
+ auto minColorF = stops[minIndex].startColor.zyxw * 255.0f;
+ auto maxColorF = stops[maxIndex].end_color().zyxw * 255.0f;
+ // Compute the change in color per change in gradient offset.
+ auto deltaColorF =
+ (maxColorF - minColorF) * (size / (maxIndex + 1 - minIndex));
+ // Subtract off the color difference of the beginning of the current span
+ // from the beginning of the gradient.
+ Float colorF =
+ minColorF - deltaColorF * (startRadius + (minIndex - 1) / size);
+ // Finally, walk over the span accumulating the position dot product and
+ // getting its sqrt as an offset into the color ramp. Since we're already
+ // in BGRA format and scaled to 255, we just need to round to an integer
+ // and pack down to pixel format.
+ for (auto* end = buf + inside; buf < end; buf += 4) {
+ Float offsetG = fastSqrt<false>(dotPos);
+ commit_blend_span<BLEND>(
+ buf,
+ combine(
+ packRGBA8(round_pixel(colorF + deltaColorF * offsetG.x, 1),
+ round_pixel(colorF + deltaColorF * offsetG.y, 1)),
+ packRGBA8(round_pixel(colorF + deltaColorF * offsetG.z, 1),
+ round_pixel(colorF + deltaColorF * offsetG.w, 1))));
+ dotPos += dotPosDelta;
+ dotPosDelta += deltaDelta2;
+ }
+ // Advance past the portion of gradient we just processed.
+ t += inside;
+ // If we hit the end of the span, exit out now.
+ if (t >= span) {
+ break;
+ }
+ // Otherwise, we are most likely in a transitional section of the gradient
+ // between stops that will likely require doing per-sample table lookups.
+ // Rather than having to redo all the searching above to figure that out,
+ // just assume that to be the case and fall through below to doing the
+ // table lookups to hopefully avoid an iteration.
+ offset = fastSqrt<true>(dotPos) - radius;
+ if (repeat) {
+ offset = fract(offset);
+ }
+ }
+ // If we got here, that means we still have span left to process but did not
+ // have any whole chunks that fell within a merged gradient. Just fall back
+ // to doing a table lookup for each sample.
+ Float entry = clamp(offset * size + 1.0f, 0.0f, 1.0f + size);
+ commit_blend_span<BLEND>(buf, sampleGradient(sampler, address, entry));
+ buf += 4;
+ t += 4;
+ dotPos += dotPosDelta;
+ dotPosDelta += deltaDelta2;
+ }
+ return true;
+}
+
+// Commits an entire span of a radial gradient similar to
+// swglcommitLinearGradient, but given a varying 2D position scaled to
+// gradient-space and a radius at which the distance from the origin maps to the
+// start of the gradient table.
+#define swgl_commitRadialGradientRGBA8(sampler, address, size, repeat, pos, \
+ radius) \
+ do { \
+ bool drawn = false; \
+ if (blend_key) { \
+ drawn = \
+ commitRadialGradient<true>(sampler, address, size, repeat, pos, \
+ radius, swgl_OutRGBA8, swgl_SpanLength); \
+ } else { \
+ drawn = \
+ commitRadialGradient<false>(sampler, address, size, repeat, pos, \
+ radius, swgl_OutRGBA8, swgl_SpanLength); \
+ } \
+ if (drawn) { \
+ swgl_OutRGBA8 += swgl_SpanLength; \
+ swgl_SpanLength = 0; \
+ } \
+ } while (0)
+
+// Extension to set a clip mask image to be sampled during blending. The offset
+// specifies the positioning of the clip mask image relative to the viewport
+// origin. The bounding box specifies the rectangle relative to the clip mask's
+// origin that constrains sampling within the clip mask. Blending must be
+// enabled for this to work.
+static sampler2D swgl_ClipMask = nullptr;
+static IntPoint swgl_ClipMaskOffset = {0, 0};
+static IntRect swgl_ClipMaskBounds = {0, 0, 0, 0};
+#define swgl_clipMask(mask, offset, bb_origin, bb_size) \
+ do { \
+ if (bb_size != vec2_scalar(0.0f, 0.0f)) { \
+ swgl_ClipFlags |= SWGL_CLIP_FLAG_MASK; \
+ swgl_ClipMask = mask; \
+ swgl_ClipMaskOffset = make_ivec2(offset); \
+ swgl_ClipMaskBounds = \
+ IntRect(make_ivec2(bb_origin), make_ivec2(bb_size)); \
+ } \
+ } while (0)
+
+// Extension to enable anti-aliasing for the given edges of a quad.
+// Blending must be enable for this to work.
+static int swgl_AAEdgeMask = 0;
+
+static ALWAYS_INLINE int calcAAEdgeMask(bool on) { return on ? 0xF : 0; }
+static ALWAYS_INLINE int calcAAEdgeMask(int mask) { return mask; }
+static ALWAYS_INLINE int calcAAEdgeMask(bvec4_scalar mask) {
+ return (mask.x ? 1 : 0) | (mask.y ? 2 : 0) | (mask.z ? 4 : 0) |
+ (mask.w ? 8 : 0);
+}
+
+#define swgl_antiAlias(edges) \
+ do { \
+ swgl_AAEdgeMask = calcAAEdgeMask(edges); \
+ if (swgl_AAEdgeMask) { \
+ swgl_ClipFlags |= SWGL_CLIP_FLAG_AA; \
+ } \
+ } while (0)
+
+#define swgl_blendDropShadow(color) \
+ do { \
+ swgl_ClipFlags |= SWGL_CLIP_FLAG_BLEND_OVERRIDE; \
+ swgl_BlendOverride = BLEND_KEY(SWGL_BLEND_DROP_SHADOW); \
+ swgl_BlendColorRGBA8 = packColor<uint32_t>(color); \
+ } while (0)
+
+#define swgl_blendSubpixelText(color) \
+ do { \
+ swgl_ClipFlags |= SWGL_CLIP_FLAG_BLEND_OVERRIDE; \
+ swgl_BlendOverride = BLEND_KEY(SWGL_BLEND_SUBPIXEL_TEXT); \
+ swgl_BlendColorRGBA8 = packColor<uint32_t>(color); \
+ swgl_BlendAlphaRGBA8 = alphas(swgl_BlendColorRGBA8); \
+ } while (0)
+
+// Dispatch helper used by the GLSL translator to swgl_drawSpan functions.
+// The number of pixels committed is tracked by checking for the difference in
+// swgl_SpanLength. Any varying interpolants used will be advanced past the
+// committed part of the span in case the fragment shader must be executed for
+// any remaining pixels that were not committed by the span shader.
+#define DISPATCH_DRAW_SPAN(self, format) \
+ do { \
+ int total = self->swgl_SpanLength; \
+ self->swgl_drawSpan##format(); \
+ int drawn = total - self->swgl_SpanLength; \
+ if (drawn) self->step_interp_inputs(drawn); \
+ return drawn; \
+ } while (0)
diff --git a/gfx/wr/swgl/src/swgl_fns.rs b/gfx/wr/swgl/src/swgl_fns.rs
new file mode 100644
index 0000000000..79669bd205
--- /dev/null
+++ b/gfx/wr/swgl/src/swgl_fns.rs
@@ -0,0 +1,2489 @@
+/* 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/. */
+#![allow(unused_variables)]
+
+use gleam::gl::*;
+use std::ffi::{CStr, CString};
+use std::os::raw::{c_char, c_int, c_void};
+use std::ptr;
+use std::str;
+
+#[allow(unused)]
+macro_rules! debug {
+ ($($x:tt)*) => {};
+}
+
+#[repr(C)]
+struct LockedTexture {
+ _private: [u8; 0],
+}
+
+#[allow(dead_code)]
+extern "C" {
+ fn ActiveTexture(texture: GLenum);
+ fn BindTexture(target: GLenum, texture: GLuint);
+ fn BindBuffer(target: GLenum, buffer: GLuint);
+ fn BindVertexArray(vao: GLuint);
+ fn BindFramebuffer(target: GLenum, fb: GLuint);
+ fn BindRenderbuffer(target: GLenum, rb: GLuint);
+ fn BlendFunc(srgb: GLenum, drgb: GLenum, sa: GLenum, da: GLenum);
+ fn BlendColor(r: GLfloat, g: GLfloat, b: GLfloat, a: GLfloat);
+ fn BlendEquation(mode: GLenum);
+ fn Enable(cap: GLenum);
+ fn Disable(cap: GLenum);
+ fn GenQueries(n: GLsizei, result: *mut GLuint);
+ fn BeginQuery(target: GLenum, id: GLuint);
+ fn EndQuery(target: GLenum);
+ fn GetQueryObjectui64v(id: GLuint, pname: GLenum, params: *mut GLuint64);
+ fn GenBuffers(n: i32, result: *mut GLuint);
+ fn GenTextures(n: i32, result: *mut GLuint);
+ fn GenFramebuffers(n: i32, result: *mut GLuint);
+ fn GenRenderbuffers(n: i32, result: *mut GLuint);
+ fn BufferData(target: GLenum, size: GLsizeiptr, data: *const GLvoid, usage: GLenum);
+ fn BufferSubData(target: GLenum, offset: GLintptr, size: GLsizeiptr, data: *const GLvoid);
+ fn MapBuffer(target: GLenum, access: GLbitfield) -> *mut c_void;
+ fn MapBufferRange(
+ target: GLenum,
+ offset: GLintptr,
+ length: GLsizeiptr,
+ access: GLbitfield,
+ ) -> *mut c_void;
+ fn UnmapBuffer(target: GLenum) -> GLboolean;
+ fn TexStorage2D(
+ target: GLenum,
+ levels: GLint,
+ internal_format: GLenum,
+ width: GLsizei,
+ height: GLsizei,
+ );
+ fn FramebufferTexture2D(
+ target: GLenum,
+ attachment: GLenum,
+ textarget: GLenum,
+ texture: GLuint,
+ level: GLint,
+ );
+ fn CheckFramebufferStatus(target: GLenum) -> GLenum;
+ fn InvalidateFramebuffer(target: GLenum, num_attachments: GLsizei, attachments: *const GLenum);
+ fn TexImage2D(
+ target: GLenum,
+ level: GLint,
+ internal_format: GLint,
+ width: GLsizei,
+ height: GLsizei,
+ border: GLint,
+ format: GLenum,
+ ty: GLenum,
+ data: *const c_void,
+ );
+ fn TexSubImage2D(
+ target: GLenum,
+ level: GLint,
+ xoffset: GLint,
+ yoffset: GLint,
+ width: GLsizei,
+ height: GLsizei,
+ format: GLenum,
+ ty: GLenum,
+ data: *const c_void,
+ );
+ fn GenerateMipmap(target: GLenum);
+ fn GetUniformLocation(program: GLuint, name: *const GLchar) -> GLint;
+ fn BindAttribLocation(program: GLuint, index: GLuint, name: *const GLchar);
+ fn GetAttribLocation(program: GLuint, name: *const GLchar) -> GLint;
+ fn GenVertexArrays(n: i32, result: *mut GLuint);
+ fn VertexAttribPointer(
+ index: GLuint,
+ size: GLint,
+ type_: GLenum,
+ normalized: GLboolean,
+ stride: GLsizei,
+ offset: *const GLvoid,
+ );
+ fn VertexAttribIPointer(
+ index: GLuint,
+ size: GLint,
+ type_: GLenum,
+ stride: GLsizei,
+ offset: *const GLvoid,
+ );
+ fn CreateShader(shader_type: GLenum) -> GLuint;
+ fn AttachShader(program: GLuint, shader: GLuint);
+ fn CreateProgram() -> GLuint;
+ fn Uniform1i(location: GLint, v0: GLint);
+ fn Uniform4fv(location: GLint, count: GLsizei, value: *const GLfloat);
+ fn UniformMatrix4fv(
+ location: GLint,
+ count: GLsizei,
+ transpose: GLboolean,
+ value: *const GLfloat,
+ );
+ fn DrawElementsInstanced(
+ mode: GLenum,
+ count: GLsizei,
+ type_: GLenum,
+ indices: GLintptr,
+ instancecount: GLsizei,
+ );
+ fn EnableVertexAttribArray(index: GLuint);
+ fn VertexAttribDivisor(index: GLuint, divisor: GLuint);
+ fn LinkProgram(program: GLuint);
+ fn GetLinkStatus(program: GLuint) -> GLint;
+ fn UseProgram(program: GLuint);
+ fn SetViewport(x: GLint, y: GLint, width: GLsizei, height: GLsizei);
+ fn FramebufferRenderbuffer(
+ target: GLenum,
+ attachment: GLenum,
+ renderbuffertarget: GLenum,
+ renderbuffer: GLuint,
+ );
+ fn RenderbufferStorage(target: GLenum, internalformat: GLenum, width: GLsizei, height: GLsizei);
+ fn DepthMask(flag: GLboolean);
+ fn DepthFunc(func: GLenum);
+ fn SetScissor(x: GLint, y: GLint, width: GLsizei, height: GLsizei);
+ fn ClearColor(r: GLfloat, g: GLfloat, b: GLfloat, a: GLfloat);
+ fn ClearDepth(depth: GLdouble);
+ fn Clear(mask: GLbitfield);
+ fn ClearTexSubImage(
+ target: GLenum,
+ level: GLint,
+ xoffset: GLint,
+ yoffset: GLint,
+ zoffset: GLint,
+ width: GLsizei,
+ height: GLsizei,
+ depth: GLsizei,
+ format: GLenum,
+ ty: GLenum,
+ data: *const c_void,
+ );
+ fn ClearTexImage(target: GLenum, level: GLint, format: GLenum, ty: GLenum, data: *const c_void);
+ fn ClearColorRect(
+ fbo: GLuint,
+ xoffset: GLint,
+ yoffset: GLint,
+ width: GLsizei,
+ height: GLsizei,
+ r: GLfloat,
+ g: GLfloat,
+ b: GLfloat,
+ a: GLfloat,
+ );
+ fn PixelStorei(name: GLenum, param: GLint);
+ fn ReadPixels(
+ x: GLint,
+ y: GLint,
+ width: GLsizei,
+ height: GLsizei,
+ format: GLenum,
+ ty: GLenum,
+ data: *mut c_void,
+ );
+ fn Finish();
+ fn ShaderSourceByName(shader: GLuint, name: *const GLchar);
+ fn TexParameteri(target: GLenum, pname: GLenum, param: GLint);
+ fn CopyImageSubData(
+ src_name: GLuint,
+ src_target: GLenum,
+ src_level: GLint,
+ src_x: GLint,
+ src_y: GLint,
+ src_z: GLint,
+ dst_name: GLuint,
+ dst_target: GLenum,
+ dst_level: GLint,
+ dst_x: GLint,
+ dst_y: GLint,
+ dst_z: GLint,
+ src_width: GLsizei,
+ src_height: GLsizei,
+ src_depth: GLsizei,
+ );
+ fn CopyTexSubImage2D(
+ target: GLenum,
+ level: GLint,
+ xoffset: GLint,
+ yoffset: GLint,
+ x: GLint,
+ y: GLint,
+ width: GLsizei,
+ height: GLsizei,
+ );
+ fn BlitFramebuffer(
+ src_x0: GLint,
+ src_y0: GLint,
+ src_x1: GLint,
+ src_y1: GLint,
+ dst_x0: GLint,
+ dst_y0: GLint,
+ dst_x1: GLint,
+ dst_y1: GLint,
+ mask: GLbitfield,
+ filter: GLenum,
+ );
+ fn GetIntegerv(pname: GLenum, params: *mut GLint);
+ fn GetBooleanv(pname: GLenum, params: *mut GLboolean);
+ fn GetString(name: GLenum) -> *const c_char;
+ fn GetStringi(name: GLenum, index: GLuint) -> *const c_char;
+ fn GetError() -> GLenum;
+ fn InitDefaultFramebuffer(
+ x: i32,
+ y: i32,
+ width: i32,
+ height: i32,
+ stride: i32,
+ buf: *mut c_void,
+ );
+ fn GetColorBuffer(
+ fbo: GLuint,
+ flush: GLboolean,
+ width: *mut i32,
+ height: *mut i32,
+ stride: *mut i32,
+ ) -> *mut c_void;
+ fn ResolveFramebuffer(fbo: GLuint);
+ fn SetTextureBuffer(
+ tex: GLuint,
+ internal_format: GLenum,
+ width: GLsizei,
+ height: GLsizei,
+ stride: GLsizei,
+ buf: *mut c_void,
+ min_width: GLsizei,
+ min_height: GLsizei,
+ );
+ fn SetTextureParameter(tex: GLuint, pname: GLenum, param: GLint);
+ fn DeleteTexture(n: GLuint);
+ fn DeleteRenderbuffer(n: GLuint);
+ fn DeleteFramebuffer(n: GLuint);
+ fn DeleteBuffer(n: GLuint);
+ fn DeleteVertexArray(n: GLuint);
+ fn DeleteQuery(n: GLuint);
+ fn DeleteShader(shader: GLuint);
+ fn DeleteProgram(program: GLuint);
+ fn LockFramebuffer(fbo: GLuint) -> *mut LockedTexture;
+ fn LockTexture(tex: GLuint) -> *mut LockedTexture;
+ fn LockResource(resource: *mut LockedTexture);
+ fn UnlockResource(resource: *mut LockedTexture);
+ fn GetResourceBuffer(
+ resource: *mut LockedTexture,
+ width: *mut i32,
+ height: *mut i32,
+ stride: *mut i32,
+ ) -> *mut c_void;
+ fn Composite(
+ locked_dst: *mut LockedTexture,
+ locked_src: *mut LockedTexture,
+ src_x: GLint,
+ src_y: GLint,
+ src_width: GLsizei,
+ src_height: GLsizei,
+ dst_x: GLint,
+ dst_y: GLint,
+ dst_width: GLsizei,
+ dst_height: GLsizei,
+ opaque: GLboolean,
+ flip_x: GLboolean,
+ flip_y: GLboolean,
+ filter: GLenum,
+ clip_x: GLint,
+ clip_y: GLint,
+ clip_width: GLsizei,
+ clip_height: GLsizei,
+ );
+ fn CompositeYUV(
+ locked_dst: *mut LockedTexture,
+ locked_y: *mut LockedTexture,
+ locked_u: *mut LockedTexture,
+ locked_v: *mut LockedTexture,
+ color_space: YuvRangedColorSpace,
+ color_depth: GLuint,
+ src_x: GLint,
+ src_y: GLint,
+ src_width: GLsizei,
+ src_height: GLsizei,
+ dst_x: GLint,
+ dst_y: GLint,
+ dst_width: GLsizei,
+ dst_height: GLsizei,
+ flip_x: GLboolean,
+ flip_y: GLboolean,
+ clip_x: GLint,
+ clip_y: GLint,
+ clip_width: GLsizei,
+ clip_height: GLsizei,
+ );
+ fn CreateContext() -> *mut c_void;
+ fn ReferenceContext(ctx: *mut c_void);
+ fn DestroyContext(ctx: *mut c_void);
+ fn MakeCurrent(ctx: *mut c_void);
+ fn ReportMemory(ctx: *mut c_void, size_of_op: unsafe extern "C" fn(ptr: *const c_void) -> usize) -> usize;
+}
+
+#[derive(Clone, Copy)]
+pub struct Context(*mut c_void);
+
+impl Context {
+ pub fn create() -> Self {
+ Context(unsafe { CreateContext() })
+ }
+
+ pub fn reference(&self) {
+ unsafe {
+ ReferenceContext(self.0);
+ }
+ }
+
+ pub fn destroy(&self) {
+ unsafe {
+ DestroyContext(self.0);
+ }
+ }
+
+ pub fn make_current(&self) {
+ unsafe {
+ MakeCurrent(self.0);
+ }
+ }
+
+ pub fn init_default_framebuffer(
+ &self,
+ x: i32,
+ y: i32,
+ width: i32,
+ height: i32,
+ stride: i32,
+ buf: *mut c_void,
+ ) {
+ unsafe {
+ InitDefaultFramebuffer(x, y, width, height, stride, buf);
+ }
+ }
+
+ pub fn get_color_buffer(&self, fbo: GLuint, flush: bool) -> (*mut c_void, i32, i32, i32) {
+ unsafe {
+ let mut width: i32 = 0;
+ let mut height: i32 = 0;
+ let mut stride: i32 = 0;
+ let data_ptr = GetColorBuffer(
+ fbo,
+ flush as GLboolean,
+ &mut width,
+ &mut height,
+ &mut stride,
+ );
+ (data_ptr, width, height, stride)
+ }
+ }
+
+ pub fn resolve_framebuffer(&self, fbo: GLuint) {
+ unsafe {
+ ResolveFramebuffer(fbo);
+ }
+ }
+
+ pub fn clear_color_rect(
+ &self,
+ fbo: GLuint,
+ xoffset: GLint,
+ yoffset: GLint,
+ width: GLsizei,
+ height: GLsizei,
+ r: f32,
+ g: f32,
+ b: f32,
+ a: f32,
+ ) {
+ unsafe {
+ ClearColorRect(fbo, xoffset, yoffset, width, height, r, g, b, a);
+ }
+ }
+
+ pub fn set_texture_buffer(
+ &self,
+ tex: GLuint,
+ internal_format: GLenum,
+ width: GLsizei,
+ height: GLsizei,
+ stride: GLsizei,
+ buf: *mut c_void,
+ min_width: GLsizei,
+ min_height: GLsizei,
+ ) {
+ unsafe {
+ SetTextureBuffer(
+ tex,
+ internal_format,
+ width,
+ height,
+ stride,
+ buf,
+ min_width,
+ min_height,
+ );
+ }
+ }
+
+ pub fn set_texture_parameter(&self, tex: GLuint, pname: GLenum, param: GLint) {
+ unsafe {
+ SetTextureParameter(tex, pname, param);
+ }
+ }
+
+ pub fn lock_framebuffer(&self, fbo: GLuint) -> Option<LockedResource> {
+ unsafe {
+ let resource = LockFramebuffer(fbo);
+ if resource != ptr::null_mut() {
+ Some(LockedResource(resource))
+ } else {
+ None
+ }
+ }
+ }
+
+ pub fn lock_texture(&self, tex: GLuint) -> Option<LockedResource> {
+ unsafe {
+ let resource = LockTexture(tex);
+ if resource != ptr::null_mut() {
+ Some(LockedResource(resource))
+ } else {
+ None
+ }
+ }
+ }
+
+ pub fn report_memory(&self, size_of_op: unsafe extern "C" fn(ptr: *const c_void) -> usize) -> usize {
+ unsafe { ReportMemory(self.0, size_of_op) }
+ }
+}
+
+impl From<*mut c_void> for Context {
+ fn from(ptr: *mut c_void) -> Self {
+ Context(ptr)
+ }
+}
+
+impl From<Context> for *mut c_void {
+ fn from(ctx: Context) -> Self {
+ ctx.0
+ }
+}
+
+fn calculate_length(width: GLsizei, height: GLsizei, format: GLenum, pixel_type: GLenum) -> usize {
+ let colors = match format {
+ RED => 1,
+ RGB => 3,
+ BGR => 3,
+
+ RGBA => 4,
+ BGRA => 4,
+
+ ALPHA => 1,
+ R16 => 1,
+ LUMINANCE => 1,
+ DEPTH_COMPONENT => 1,
+ _ => panic!("unsupported format for read_pixels: {:?}", format),
+ };
+ let depth = match pixel_type {
+ UNSIGNED_BYTE => 1,
+ UNSIGNED_SHORT => 2,
+ SHORT => 2,
+ FLOAT => 4,
+ UNSIGNED_INT_8_8_8_8_REV => 1,
+ _ => panic!("unsupported pixel_type for read_pixels: {:?}", pixel_type),
+ };
+
+ return (width * height * colors * depth) as usize;
+}
+
+impl Gl for Context {
+ fn get_type(&self) -> GlType {
+ GlType::Gl
+ }
+
+ fn buffer_data_untyped(
+ &self,
+ target: GLenum,
+ size: GLsizeiptr,
+ data: *const GLvoid,
+ usage: GLenum,
+ ) {
+ debug!(
+ "buffer_data_untyped {} {} {:?} {}",
+ target, size, data, usage
+ );
+ //panic!();
+ unsafe {
+ BufferData(target, size, data, usage);
+ }
+ }
+
+ fn buffer_sub_data_untyped(
+ &self,
+ target: GLenum,
+ offset: isize,
+ size: GLsizeiptr,
+ data: *const GLvoid,
+ ) {
+ debug!(
+ "buffer_sub_data_untyped {} {} {} {:?}",
+ target, offset, size, data
+ );
+ //panic!();
+ unsafe {
+ BufferSubData(target, offset, size, data);
+ }
+ }
+
+ fn map_buffer(&self, target: GLenum, access: GLbitfield) -> *mut c_void {
+ unsafe { MapBuffer(target, access) }
+ }
+
+ fn map_buffer_range(
+ &self,
+ target: GLenum,
+ offset: GLintptr,
+ length: GLsizeiptr,
+ access: GLbitfield,
+ ) -> *mut c_void {
+ unsafe { MapBufferRange(target, offset, length, access) }
+ }
+
+ fn unmap_buffer(&self, target: GLenum) -> GLboolean {
+ unsafe { UnmapBuffer(target) }
+ }
+
+ fn shader_source(&self, shader: GLuint, strings: &[&[u8]]) {
+ //panic!();
+ debug!("shader_source {}", shader);
+ //for s in strings {
+ // debug!("{}", str::from_utf8(s).unwrap());
+ //}
+ //panic!();
+ for s in strings {
+ let u = str::from_utf8(s).unwrap();
+ const PREFIX: &'static str = "// shader: ";
+ if let Some(start) = u.find(PREFIX) {
+ if let Some(end) = u[start..].find('\n') {
+ let name = u[start + PREFIX.len()..start + end].trim();
+ debug!("shader name: {}", name);
+ unsafe {
+ let c_string = CString::new(name).unwrap();
+ ShaderSourceByName(shader, c_string.as_ptr());
+ return;
+ }
+ }
+ }
+ }
+ panic!("unknown shader");
+ }
+
+ fn tex_buffer(&self, target: GLenum, internal_format: GLenum, buffer: GLuint) {
+ panic!();
+ }
+
+ fn read_buffer(&self, mode: GLenum) {
+ panic!();
+ }
+
+ fn read_pixels_into_buffer(
+ &self,
+ x: GLint,
+ y: GLint,
+ width: GLsizei,
+ height: GLsizei,
+ format: GLenum,
+ pixel_type: GLenum,
+ dst_buffer: &mut [u8],
+ ) {
+ // Assumes that the user properly allocated the size for dst_buffer.
+ assert!(calculate_length(width, height, format, pixel_type) == dst_buffer.len());
+
+ unsafe {
+ ReadPixels(
+ x,
+ y,
+ width,
+ height,
+ format,
+ pixel_type,
+ dst_buffer.as_mut_ptr() as *mut c_void,
+ );
+ }
+ }
+
+ fn read_pixels(
+ &self,
+ x: GLint,
+ y: GLint,
+ width: GLsizei,
+ height: GLsizei,
+ format: GLenum,
+ pixel_type: GLenum,
+ ) -> Vec<u8> {
+ let len = calculate_length(width, height, format, pixel_type);
+ let mut pixels: Vec<u8> = Vec::new();
+ pixels.reserve(len);
+ unsafe {
+ pixels.set_len(len);
+ }
+
+ self.read_pixels_into_buffer(
+ x,
+ y,
+ width,
+ height,
+ format,
+ pixel_type,
+ pixels.as_mut_slice(),
+ );
+
+ pixels
+ }
+
+ unsafe fn read_pixels_into_pbo(
+ &self,
+ x: GLint,
+ y: GLint,
+ width: GLsizei,
+ height: GLsizei,
+ format: GLenum,
+ pixel_type: GLenum,
+ ) {
+ ReadPixels(x, y, width, height, format, pixel_type, ptr::null_mut());
+ }
+
+ fn sample_coverage(&self, value: GLclampf, invert: bool) {
+ panic!();
+ }
+
+ fn polygon_offset(&self, factor: GLfloat, units: GLfloat) {
+ panic!();
+ }
+
+ fn pixel_store_i(&self, name: GLenum, param: GLint) {
+ //panic!();
+ debug!("pixel_store_i {:x} {}", name, param);
+ unsafe {
+ PixelStorei(name, param);
+ }
+ }
+
+ fn gen_buffers(&self, n: GLsizei) -> Vec<GLuint> {
+ //panic!();
+ let mut result = vec![0 as GLuint; n as usize];
+ unsafe {
+ GenBuffers(n, result.as_mut_ptr());
+ }
+ result
+ }
+
+ fn gen_renderbuffers(&self, n: GLsizei) -> Vec<GLuint> {
+ debug!("gen_renderbuffers {}", n);
+ //panic!();
+ let mut result = vec![0 as GLuint; n as usize];
+ unsafe {
+ GenRenderbuffers(n, result.as_mut_ptr());
+ }
+ result
+ }
+
+ fn gen_framebuffers(&self, n: GLsizei) -> Vec<GLuint> {
+ //panic!();
+ debug!("gen_framebuffers {}", n);
+ let mut result = vec![0 as GLuint; n as usize];
+ unsafe {
+ GenFramebuffers(n, result.as_mut_ptr());
+ }
+ result
+ }
+
+ fn gen_textures(&self, n: GLsizei) -> Vec<GLuint> {
+ //panic!();
+ let mut result = vec![0 as GLuint; n as usize];
+ unsafe {
+ GenTextures(n, result.as_mut_ptr());
+ }
+ result
+ }
+
+ fn gen_vertex_arrays(&self, n: GLsizei) -> Vec<GLuint> {
+ //panic!();
+ let mut result = vec![0 as GLuint; n as usize];
+ unsafe {
+ GenVertexArrays(n, result.as_mut_ptr());
+ }
+ result
+ }
+
+ fn gen_vertex_arrays_apple(&self, n: GLsizei) -> Vec<GLuint> {
+ self.gen_vertex_arrays(n)
+ }
+
+ fn gen_queries(&self, n: GLsizei) -> Vec<GLuint> {
+ let mut result = vec![0 as GLuint; n as usize];
+ unsafe {
+ GenQueries(n, result.as_mut_ptr());
+ }
+ result
+ }
+
+ fn begin_query(&self, target: GLenum, id: GLuint) {
+ unsafe {
+ BeginQuery(target, id);
+ }
+ }
+
+ fn end_query(&self, target: GLenum) {
+ unsafe {
+ EndQuery(target);
+ }
+ }
+
+ fn query_counter(&self, id: GLuint, target: GLenum) {
+ panic!();
+ }
+
+ fn get_query_object_iv(&self, id: GLuint, pname: GLenum) -> i32 {
+ panic!();
+ //0
+ }
+
+ fn get_query_object_uiv(&self, id: GLuint, pname: GLenum) -> u32 {
+ panic!();
+ //0
+ }
+
+ fn get_query_object_i64v(&self, id: GLuint, pname: GLenum) -> i64 {
+ panic!();
+ //0
+ }
+
+ fn get_query_object_ui64v(&self, id: GLuint, pname: GLenum) -> u64 {
+ let mut result = 0;
+ unsafe {
+ GetQueryObjectui64v(id, pname, &mut result);
+ }
+ result
+ }
+
+ fn delete_queries(&self, queries: &[GLuint]) {
+ unsafe {
+ for q in queries {
+ DeleteQuery(*q);
+ }
+ }
+ }
+
+ fn delete_vertex_arrays(&self, vertex_arrays: &[GLuint]) {
+ unsafe {
+ for v in vertex_arrays {
+ DeleteVertexArray(*v);
+ }
+ }
+ }
+
+ fn delete_vertex_arrays_apple(&self, vertex_arrays: &[GLuint]) {
+ self.delete_vertex_arrays(vertex_arrays)
+ }
+
+ fn delete_buffers(&self, buffers: &[GLuint]) {
+ unsafe {
+ for b in buffers {
+ DeleteBuffer(*b);
+ }
+ }
+ }
+
+ fn delete_renderbuffers(&self, renderbuffers: &[GLuint]) {
+ unsafe {
+ for r in renderbuffers {
+ DeleteRenderbuffer(*r);
+ }
+ }
+ }
+
+ fn delete_framebuffers(&self, framebuffers: &[GLuint]) {
+ unsafe {
+ for f in framebuffers {
+ DeleteFramebuffer(*f);
+ }
+ }
+ }
+
+ fn delete_textures(&self, textures: &[GLuint]) {
+ unsafe {
+ for t in textures {
+ DeleteTexture(*t);
+ }
+ }
+ }
+
+ fn framebuffer_renderbuffer(
+ &self,
+ target: GLenum,
+ attachment: GLenum,
+ renderbuffertarget: GLenum,
+ renderbuffer: GLuint,
+ ) {
+ debug!(
+ "framebufer_renderbuffer {} {} {} {}",
+ target, attachment, renderbuffertarget, renderbuffer
+ );
+ //panic!();
+ unsafe {
+ FramebufferRenderbuffer(target, attachment, renderbuffertarget, renderbuffer);
+ }
+ }
+
+ fn renderbuffer_storage(
+ &self,
+ target: GLenum,
+ internalformat: GLenum,
+ width: GLsizei,
+ height: GLsizei,
+ ) {
+ debug!(
+ "renderbuffer_storage {} {} {} {}",
+ target, internalformat, width, height
+ );
+ //panic!();
+ unsafe {
+ RenderbufferStorage(target, internalformat, width, height);
+ }
+ }
+
+ fn depth_func(&self, func: GLenum) {
+ debug!("depth_func {}", func);
+ //panic!();
+ unsafe {
+ DepthFunc(func);
+ }
+ }
+
+ fn active_texture(&self, texture: GLenum) {
+ //panic!();
+ unsafe {
+ ActiveTexture(texture);
+ }
+ }
+
+ fn attach_shader(&self, program: GLuint, shader: GLuint) {
+ debug!("attach shader {} {}", program, shader);
+ //panic!();
+ unsafe {
+ AttachShader(program, shader);
+ }
+ }
+
+ fn bind_attrib_location(&self, program: GLuint, index: GLuint, name: &str) {
+ debug!("bind_attrib_location {} {} {}", program, index, name);
+ //panic!();
+ let c_string = CString::new(name).unwrap();
+ unsafe { BindAttribLocation(program, index, c_string.as_ptr()) }
+ }
+
+ // https://www.khronos.org/registry/OpenGL-Refpages/es2.0/xhtml/glGetUniform.xml
+ unsafe fn get_uniform_iv(&self, program: GLuint, location: GLint, result: &mut [GLint]) {
+ panic!();
+ //assert!(!result.is_empty());
+ }
+
+ // https://www.khronos.org/registry/OpenGL-Refpages/es2.0/xhtml/glGetUniform.xml
+ unsafe fn get_uniform_fv(&self, program: GLuint, location: GLint, result: &mut [GLfloat]) {
+ panic!();
+ //assert!(!result.is_empty());
+ }
+
+ fn get_uniform_block_index(&self, program: GLuint, name: &str) -> GLuint {
+ panic!();
+ //0
+ }
+
+ fn get_uniform_indices(&self, program: GLuint, names: &[&str]) -> Vec<GLuint> {
+ panic!();
+ //Vec::new()
+ }
+
+ fn bind_buffer_base(&self, target: GLenum, index: GLuint, buffer: GLuint) {
+ panic!();
+ }
+
+ fn bind_buffer_range(
+ &self,
+ target: GLenum,
+ index: GLuint,
+ buffer: GLuint,
+ offset: GLintptr,
+ size: GLsizeiptr,
+ ) {
+ panic!();
+ }
+
+ fn uniform_block_binding(
+ &self,
+ program: GLuint,
+ uniform_block_index: GLuint,
+ uniform_block_binding: GLuint,
+ ) {
+ panic!();
+ }
+
+ fn bind_buffer(&self, target: GLenum, buffer: GLuint) {
+ //panic!();
+ unsafe {
+ BindBuffer(target, buffer);
+ }
+ }
+
+ fn bind_vertex_array(&self, vao: GLuint) {
+ //panic!();
+ unsafe {
+ BindVertexArray(vao);
+ }
+ }
+
+ fn bind_vertex_array_apple(&self, vao: GLuint) {
+ self.bind_vertex_array(vao)
+ }
+
+ fn bind_renderbuffer(&self, target: GLenum, renderbuffer: GLuint) {
+ debug!("bind_renderbuffer {} {}", target, renderbuffer);
+ //panic!();
+ unsafe {
+ BindRenderbuffer(target, renderbuffer);
+ }
+ }
+
+ fn bind_framebuffer(&self, target: GLenum, framebuffer: GLuint) {
+ debug!("bind_framebuffer {} {}", target, framebuffer);
+ //panic!();
+ unsafe {
+ BindFramebuffer(target, framebuffer);
+ }
+ }
+
+ fn bind_vertex_buffer(
+ &self,
+ binding_index: GLuint,
+ buffer: GLuint,
+ offset: GLintptr,
+ stride: GLint,
+ ) {
+ unimplemented!("Not supported by SWGL");
+ }
+
+ fn bind_texture(&self, target: GLenum, texture: GLuint) {
+ //panic!();
+ unsafe {
+ BindTexture(target, texture);
+ }
+ }
+
+ fn draw_buffers(&self, bufs: &[GLenum]) {
+ panic!();
+ //unsafe {}
+ }
+
+ // FIXME: Does not verify buffer size -- unsafe!
+ fn tex_image_2d(
+ &self,
+ target: GLenum,
+ level: GLint,
+ internal_format: GLint,
+ width: GLsizei,
+ height: GLsizei,
+ border: GLint,
+ format: GLenum,
+ ty: GLenum,
+ opt_data: Option<&[u8]>,
+ ) {
+ unsafe {
+ let pdata = match opt_data {
+ Some(data) => data.as_ptr() as *const GLvoid,
+ None => ptr::null(),
+ };
+ TexImage2D(
+ target,
+ level,
+ internal_format,
+ width,
+ height,
+ border,
+ format,
+ ty,
+ pdata,
+ );
+ }
+ }
+
+ fn compressed_tex_image_2d(
+ &self,
+ target: GLenum,
+ level: GLint,
+ internal_format: GLenum,
+ width: GLsizei,
+ height: GLsizei,
+ border: GLint,
+ data: &[u8],
+ ) {
+ panic!();
+ }
+
+ fn compressed_tex_sub_image_2d(
+ &self,
+ target: GLenum,
+ level: GLint,
+ xoffset: GLint,
+ yoffset: GLint,
+ width: GLsizei,
+ height: GLsizei,
+ format: GLenum,
+ data: &[u8],
+ ) {
+ panic!();
+ }
+
+ fn tex_image_3d(
+ &self,
+ target: GLenum,
+ level: GLint,
+ internal_format: GLint,
+ width: GLsizei,
+ height: GLsizei,
+ depth: GLsizei,
+ border: GLint,
+ format: GLenum,
+ ty: GLenum,
+ opt_data: Option<&[u8]>,
+ ) {
+ panic!();
+ }
+
+ fn copy_tex_image_2d(
+ &self,
+ target: GLenum,
+ level: GLint,
+ internal_format: GLenum,
+ x: GLint,
+ y: GLint,
+ width: GLsizei,
+ height: GLsizei,
+ border: GLint,
+ ) {
+ panic!();
+ }
+
+ fn copy_tex_sub_image_2d(
+ &self,
+ target: GLenum,
+ level: GLint,
+ xoffset: GLint,
+ yoffset: GLint,
+ x: GLint,
+ y: GLint,
+ width: GLsizei,
+ height: GLsizei,
+ ) {
+ unsafe {
+ CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
+ }
+ }
+
+ fn copy_tex_sub_image_3d(
+ &self,
+ target: GLenum,
+ level: GLint,
+ xoffset: GLint,
+ yoffset: GLint,
+ zoffset: GLint,
+ x: GLint,
+ y: GLint,
+ width: GLsizei,
+ height: GLsizei,
+ ) {
+ panic!();
+ }
+
+ fn tex_sub_image_2d(
+ &self,
+ target: GLenum,
+ level: GLint,
+ xoffset: GLint,
+ yoffset: GLint,
+ width: GLsizei,
+ height: GLsizei,
+ format: GLenum,
+ ty: GLenum,
+ data: &[u8],
+ ) {
+ debug!(
+ "tex_sub_image_2d {} {} {} {} {} {} {} {}",
+ target, level, xoffset, yoffset, width, height, format, ty
+ );
+ //panic!();
+ unsafe {
+ TexSubImage2D(
+ target,
+ level,
+ xoffset,
+ yoffset,
+ width,
+ height,
+ format,
+ ty,
+ data.as_ptr() as *const c_void,
+ );
+ }
+ }
+
+ fn tex_sub_image_2d_pbo(
+ &self,
+ target: GLenum,
+ level: GLint,
+ xoffset: GLint,
+ yoffset: GLint,
+ width: GLsizei,
+ height: GLsizei,
+ format: GLenum,
+ ty: GLenum,
+ offset: usize,
+ ) {
+ debug!(
+ "tex_sub_image_2d_pbo {} {} {} {} {} {} {} {} {}",
+ target, level, xoffset, yoffset, width, height, format, ty, offset
+ );
+ //panic!();
+ unsafe {
+ TexSubImage2D(
+ target,
+ level,
+ xoffset,
+ yoffset,
+ width,
+ height,
+ format,
+ ty,
+ offset as *const c_void,
+ );
+ }
+ }
+
+ fn tex_sub_image_3d(
+ &self,
+ target: GLenum,
+ level: GLint,
+ xoffset: GLint,
+ yoffset: GLint,
+ zoffset: GLint,
+ width: GLsizei,
+ height: GLsizei,
+ depth: GLsizei,
+ format: GLenum,
+ ty: GLenum,
+ data: &[u8],
+ ) {
+ debug!("tex_sub_image_3d");
+ panic!();
+ }
+
+ fn tex_sub_image_3d_pbo(
+ &self,
+ target: GLenum,
+ level: GLint,
+ xoffset: GLint,
+ yoffset: GLint,
+ zoffset: GLint,
+ width: GLsizei,
+ height: GLsizei,
+ depth: GLsizei,
+ format: GLenum,
+ ty: GLenum,
+ offset: usize,
+ ) {
+ panic!();
+ }
+
+ fn tex_storage_2d(
+ &self,
+ target: GLenum,
+ levels: GLint,
+ internal_format: GLenum,
+ width: GLsizei,
+ height: GLsizei,
+ ) {
+ //panic!();
+ unsafe {
+ TexStorage2D(target, levels, internal_format, width, height);
+ }
+ }
+
+ fn tex_storage_3d(
+ &self,
+ target: GLenum,
+ levels: GLint,
+ internal_format: GLenum,
+ width: GLsizei,
+ height: GLsizei,
+ depth: GLsizei,
+ ) {
+ panic!();
+ }
+
+ fn get_tex_image_into_buffer(
+ &self,
+ target: GLenum,
+ level: GLint,
+ format: GLenum,
+ ty: GLenum,
+ output: &mut [u8],
+ ) {
+ panic!();
+ }
+
+ unsafe fn copy_image_sub_data(
+ &self,
+ src_name: GLuint,
+ src_target: GLenum,
+ src_level: GLint,
+ src_x: GLint,
+ src_y: GLint,
+ src_z: GLint,
+ dst_name: GLuint,
+ dst_target: GLenum,
+ dst_level: GLint,
+ dst_x: GLint,
+ dst_y: GLint,
+ dst_z: GLint,
+ src_width: GLsizei,
+ src_height: GLsizei,
+ src_depth: GLsizei,
+ ) {
+ CopyImageSubData(
+ src_name, src_target, src_level, src_x, src_y, src_z, dst_name, dst_target, dst_level,
+ dst_x, dst_y, dst_z, src_width, src_height, src_depth,
+ );
+ }
+
+ fn invalidate_framebuffer(&self, target: GLenum, attachments: &[GLenum]) {
+ unsafe {
+ InvalidateFramebuffer(target, attachments.len() as GLsizei, attachments.as_ptr());
+ }
+ }
+
+ fn invalidate_sub_framebuffer(
+ &self,
+ target: GLenum,
+ attachments: &[GLenum],
+ xoffset: GLint,
+ yoffset: GLint,
+ width: GLsizei,
+ height: GLsizei,
+ ) {
+ }
+
+ #[inline]
+ unsafe fn get_integer_v(&self, name: GLenum, result: &mut [GLint]) {
+ //panic!();
+ assert!(!result.is_empty());
+ GetIntegerv(name, result.as_mut_ptr());
+ }
+
+ #[inline]
+ unsafe fn get_integer_64v(&self, name: GLenum, result: &mut [GLint64]) {
+ panic!();
+ //assert!(!result.is_empty());
+ }
+
+ #[inline]
+ unsafe fn get_integer_iv(&self, name: GLenum, index: GLuint, result: &mut [GLint]) {
+ panic!();
+ //assert!(!result.is_empty());
+ }
+
+ #[inline]
+ unsafe fn get_integer_64iv(&self, name: GLenum, index: GLuint, result: &mut [GLint64]) {
+ panic!();
+ //assert!(!result.is_empty());
+ }
+
+ #[inline]
+ unsafe fn get_boolean_v(&self, name: GLenum, result: &mut [GLboolean]) {
+ debug!("get_boolean_v {}", name);
+ //panic!();
+ assert!(!result.is_empty());
+ GetBooleanv(name, result.as_mut_ptr());
+ }
+
+ #[inline]
+ unsafe fn get_float_v(&self, name: GLenum, result: &mut [GLfloat]) {
+ panic!();
+ //assert!(!result.is_empty());
+ }
+
+ fn get_framebuffer_attachment_parameter_iv(
+ &self,
+ target: GLenum,
+ attachment: GLenum,
+ pname: GLenum,
+ ) -> GLint {
+ panic!();
+ //0
+ }
+
+ fn get_renderbuffer_parameter_iv(&self, target: GLenum, pname: GLenum) -> GLint {
+ panic!();
+ //0
+ }
+
+ fn get_tex_parameter_iv(&self, target: GLenum, pname: GLenum) -> GLint {
+ panic!();
+ //0
+ }
+
+ fn get_tex_parameter_fv(&self, target: GLenum, pname: GLenum) -> GLfloat {
+ panic!();
+ //0.0
+ }
+
+ fn tex_parameter_i(&self, target: GLenum, pname: GLenum, param: GLint) {
+ //panic!();
+ unsafe {
+ TexParameteri(target, pname, param);
+ }
+ }
+
+ fn tex_parameter_f(&self, target: GLenum, pname: GLenum, param: GLfloat) {
+ panic!();
+ }
+
+ fn framebuffer_texture_2d(
+ &self,
+ target: GLenum,
+ attachment: GLenum,
+ textarget: GLenum,
+ texture: GLuint,
+ level: GLint,
+ ) {
+ debug!(
+ "framebuffer_texture_2d {} {} {} {} {}",
+ target, attachment, textarget, texture, level
+ );
+ //panic!();
+ unsafe {
+ FramebufferTexture2D(target, attachment, textarget, texture, level);
+ }
+ }
+
+ fn framebuffer_texture_layer(
+ &self,
+ target: GLenum,
+ attachment: GLenum,
+ texture: GLuint,
+ level: GLint,
+ layer: GLint,
+ ) {
+ debug!(
+ "framebuffer_texture_layer {} {} {} {} {}",
+ target, attachment, texture, level, layer
+ );
+ panic!();
+ }
+
+ fn blit_framebuffer(
+ &self,
+ src_x0: GLint,
+ src_y0: GLint,
+ src_x1: GLint,
+ src_y1: GLint,
+ dst_x0: GLint,
+ dst_y0: GLint,
+ dst_x1: GLint,
+ dst_y1: GLint,
+ mask: GLbitfield,
+ filter: GLenum,
+ ) {
+ unsafe {
+ BlitFramebuffer(
+ src_x0, src_y0, src_x1, src_y1, dst_x0, dst_y0, dst_x1, dst_y1, mask, filter,
+ );
+ }
+ }
+
+ fn vertex_attrib_4f(&self, index: GLuint, x: GLfloat, y: GLfloat, z: GLfloat, w: GLfloat) {
+ panic!();
+ }
+
+ fn vertex_attrib_binding(&self, attrib_index: GLuint, binding_index: GLuint) {
+ unimplemented!("Not supported by SWGL");
+ }
+
+ fn vertex_attrib_pointer_f32(
+ &self,
+ index: GLuint,
+ size: GLint,
+ normalized: bool,
+ stride: GLsizei,
+ offset: GLuint,
+ ) {
+ panic!();
+ }
+
+ fn vertex_attrib_pointer(
+ &self,
+ index: GLuint,
+ size: GLint,
+ type_: GLenum,
+ normalized: bool,
+ stride: GLsizei,
+ offset: GLuint,
+ ) {
+ debug!(
+ "vertex_attrib_pointer {} {} {} {} {} {}",
+ index, size, type_, normalized, stride, offset
+ );
+ //panic!();
+ unsafe {
+ VertexAttribPointer(
+ index,
+ size,
+ type_,
+ normalized as GLboolean,
+ stride,
+ offset as *const GLvoid,
+ );
+ }
+ }
+
+ fn vertex_attrib_i_pointer(
+ &self,
+ index: GLuint,
+ size: GLint,
+ type_: GLenum,
+ stride: GLsizei,
+ offset: GLuint,
+ ) {
+ debug!(
+ "vertex_attrib_i_pointer {} {} {} {} {}",
+ index, size, type_, stride, offset
+ );
+ //panic!();
+ unsafe {
+ VertexAttribIPointer(index, size, type_, stride, offset as *const GLvoid);
+ }
+ }
+
+ fn vertex_attrib_divisor(&self, index: GLuint, divisor: GLuint) {
+ debug!("vertex_attrib_divisor {} {}", index, divisor);
+ //assert!(index == 0 && divisor == 0);
+ //panic!();
+ unsafe {
+ VertexAttribDivisor(index, divisor);
+ }
+ }
+
+ fn vertex_attrib_format(
+ &self,
+ attrib_index: GLuint,
+ size: GLint,
+ type_: GLenum,
+ normalized: bool,
+ relative_offset: GLuint,
+ ) {
+ unimplemented!("Not supported by SWGL");
+ }
+
+ fn vertex_attrib_i_format(
+ &self,
+ attrib_index: GLuint,
+ size: GLint,
+ type_: GLenum,
+ relative_offset: GLuint,
+ ) {
+ unimplemented!("Not supported by SWGL");
+ }
+
+ fn vertex_binding_divisor(&self, binding_index: GLuint, divisor: GLuint) {
+ unimplemented!("Not supported by SWGL");
+ }
+
+ fn viewport(&self, x: GLint, y: GLint, width: GLsizei, height: GLsizei) {
+ debug!("viewport {} {} {} {}", x, y, width, height);
+ //panic!();
+ unsafe {
+ SetViewport(x, y, width, height);
+ }
+ }
+
+ fn scissor(&self, x: GLint, y: GLint, width: GLsizei, height: GLsizei) {
+ //panic!();
+ unsafe {
+ SetScissor(x, y, width, height);
+ }
+ }
+
+ fn line_width(&self, width: GLfloat) {
+ panic!();
+ }
+
+ fn use_program(&self, program: GLuint) {
+ //panic!();
+ unsafe {
+ UseProgram(program);
+ }
+ }
+
+ fn validate_program(&self, program: GLuint) {
+ panic!();
+ }
+
+ fn draw_arrays(&self, mode: GLenum, first: GLint, count: GLsizei) {
+ unsafe {
+ DrawElementsInstanced(mode, count, NONE, first as GLintptr, 1);
+ }
+ }
+
+ fn draw_arrays_instanced(
+ &self,
+ mode: GLenum,
+ first: GLint,
+ count: GLsizei,
+ primcount: GLsizei,
+ ) {
+ unsafe {
+ DrawElementsInstanced(mode, count, NONE, first as GLintptr, primcount);
+ }
+ }
+
+ fn draw_elements(
+ &self,
+ mode: GLenum,
+ count: GLsizei,
+ element_type: GLenum,
+ indices_offset: GLuint,
+ ) {
+ debug!(
+ "draw_elements {} {} {} {} {}",
+ mode, count, element_type, indices_offset
+ );
+ //panic!();
+ unsafe {
+ DrawElementsInstanced(mode, count, element_type, indices_offset as GLintptr, 1);
+ }
+ }
+
+ fn draw_elements_instanced(
+ &self,
+ mode: GLenum,
+ count: GLsizei,
+ element_type: GLenum,
+ indices_offset: GLuint,
+ primcount: GLsizei,
+ ) {
+ debug!(
+ "draw_elements_instanced {} {} {} {} {}",
+ mode, count, element_type, indices_offset, primcount
+ );
+ //panic!();
+ unsafe {
+ DrawElementsInstanced(
+ mode,
+ count,
+ element_type,
+ indices_offset as GLintptr,
+ primcount,
+ );
+ }
+ }
+
+ fn blend_color(&self, r: f32, g: f32, b: f32, a: f32) {
+ unsafe {
+ BlendColor(r, g, b, a);
+ }
+ }
+
+ fn blend_func(&self, sfactor: GLenum, dfactor: GLenum) {
+ unsafe {
+ BlendFunc(sfactor, dfactor, sfactor, dfactor);
+ }
+ }
+
+ fn blend_func_separate(
+ &self,
+ src_rgb: GLenum,
+ dest_rgb: GLenum,
+ src_alpha: GLenum,
+ dest_alpha: GLenum,
+ ) {
+ unsafe {
+ BlendFunc(src_rgb, dest_rgb, src_alpha, dest_alpha);
+ }
+ }
+
+ fn blend_equation(&self, mode: GLenum) {
+ unsafe {
+ BlendEquation(mode);
+ }
+ }
+
+ fn blend_equation_separate(&self, mode_rgb: GLenum, mode_alpha: GLenum) {
+ panic!();
+ }
+
+ fn color_mask(&self, r: bool, g: bool, b: bool, a: bool) {
+ panic!();
+ }
+
+ fn cull_face(&self, mode: GLenum) {
+ panic!();
+ }
+
+ fn front_face(&self, mode: GLenum) {
+ panic!();
+ }
+
+ fn enable(&self, cap: GLenum) {
+ debug!("enable {}", cap);
+ //panic!();
+ unsafe {
+ Enable(cap);
+ }
+ }
+
+ fn disable(&self, cap: GLenum) {
+ debug!("disable {}", cap);
+ //panic!();
+ unsafe {
+ Disable(cap);
+ }
+ }
+
+ fn hint(&self, param_name: GLenum, param_val: GLenum) {
+ panic!();
+ }
+
+ fn is_enabled(&self, cap: GLenum) -> GLboolean {
+ panic!();
+ //0
+ }
+
+ fn is_shader(&self, shader: GLuint) -> GLboolean {
+ panic!();
+ //0
+ }
+
+ fn is_texture(&self, texture: GLenum) -> GLboolean {
+ panic!();
+ //0
+ }
+
+ fn is_framebuffer(&self, framebuffer: GLenum) -> GLboolean {
+ panic!();
+ //0
+ }
+
+ fn is_renderbuffer(&self, renderbuffer: GLenum) -> GLboolean {
+ panic!();
+ //0
+ }
+
+ fn check_frame_buffer_status(&self, target: GLenum) -> GLenum {
+ debug!("check_frame_buffer_status {}", target);
+ //panic!();
+ unsafe { CheckFramebufferStatus(target) }
+ }
+
+ fn enable_vertex_attrib_array(&self, index: GLuint) {
+ //panic!();
+ debug!("enable_vertex_attrib_array {}", index);
+ unsafe {
+ EnableVertexAttribArray(index);
+ //assert_eq!(index, 0);
+ }
+ }
+
+ fn disable_vertex_attrib_array(&self, index: GLuint) {
+ panic!();
+ }
+
+ fn uniform_1f(&self, location: GLint, v0: GLfloat) {
+ panic!();
+ }
+
+ fn uniform_1fv(&self, location: GLint, values: &[f32]) {
+ panic!();
+ }
+
+ fn uniform_1i(&self, location: GLint, v0: GLint) {
+ debug!("uniform_1i {} {}", location, v0);
+ //panic!();
+ unsafe {
+ Uniform1i(location, v0);
+ }
+ }
+
+ fn uniform_1iv(&self, location: GLint, values: &[i32]) {
+ panic!();
+ }
+
+ fn uniform_1ui(&self, location: GLint, v0: GLuint) {
+ panic!();
+ }
+
+ fn uniform_2f(&self, location: GLint, v0: GLfloat, v1: GLfloat) {
+ panic!();
+ }
+
+ fn uniform_2fv(&self, location: GLint, values: &[f32]) {
+ panic!();
+ }
+
+ fn uniform_2i(&self, location: GLint, v0: GLint, v1: GLint) {
+ panic!();
+ }
+
+ fn uniform_2iv(&self, location: GLint, values: &[i32]) {
+ panic!();
+ }
+
+ fn uniform_2ui(&self, location: GLint, v0: GLuint, v1: GLuint) {
+ panic!();
+ }
+
+ fn uniform_3f(&self, location: GLint, v0: GLfloat, v1: GLfloat, v2: GLfloat) {
+ panic!();
+ }
+
+ fn uniform_3fv(&self, location: GLint, values: &[f32]) {
+ panic!();
+ }
+
+ fn uniform_3i(&self, location: GLint, v0: GLint, v1: GLint, v2: GLint) {
+ panic!();
+ }
+
+ fn uniform_3iv(&self, location: GLint, values: &[i32]) {
+ panic!();
+ }
+
+ fn uniform_3ui(&self, location: GLint, v0: GLuint, v1: GLuint, v2: GLuint) {
+ panic!();
+ }
+
+ fn uniform_4f(&self, location: GLint, x: GLfloat, y: GLfloat, z: GLfloat, w: GLfloat) {
+ panic!();
+ }
+
+ fn uniform_4i(&self, location: GLint, x: GLint, y: GLint, z: GLint, w: GLint) {
+ panic!();
+ }
+
+ fn uniform_4iv(&self, location: GLint, values: &[i32]) {
+ panic!();
+ }
+
+ fn uniform_4ui(&self, location: GLint, x: GLuint, y: GLuint, z: GLuint, w: GLuint) {
+ panic!();
+ }
+
+ fn uniform_4fv(&self, location: GLint, values: &[f32]) {
+ unsafe {
+ Uniform4fv(location, (values.len() / 4) as GLsizei, values.as_ptr());
+ }
+ }
+
+ fn uniform_matrix_2fv(&self, location: GLint, transpose: bool, value: &[f32]) {
+ panic!();
+ }
+
+ fn uniform_matrix_3fv(&self, location: GLint, transpose: bool, value: &[f32]) {
+ panic!();
+ }
+
+ fn uniform_matrix_4fv(&self, location: GLint, transpose: bool, value: &[f32]) {
+ debug!("uniform_matrix_4fv {} {} {:?}", location, transpose, value);
+ //panic!();
+ unsafe {
+ UniformMatrix4fv(
+ location,
+ (value.len() / 16) as GLsizei,
+ transpose as GLboolean,
+ value.as_ptr(),
+ );
+ }
+ }
+
+ fn depth_mask(&self, flag: bool) {
+ debug!("depth_mask {}", flag);
+ //panic!();
+ unsafe {
+ DepthMask(flag as GLboolean);
+ }
+ }
+
+ fn depth_range(&self, near: f64, far: f64) {
+ panic!();
+ }
+
+ fn get_active_attrib(&self, program: GLuint, index: GLuint) -> (i32, u32, String) {
+ panic!();
+ //(0, 0, String::new())
+ }
+
+ fn get_active_uniform(&self, program: GLuint, index: GLuint) -> (i32, u32, String) {
+ panic!();
+ //(0, 0, String::new())
+ }
+
+ fn get_active_uniforms_iv(
+ &self,
+ program: GLuint,
+ indices: Vec<GLuint>,
+ pname: GLenum,
+ ) -> Vec<GLint> {
+ panic!();
+ //Vec::new()
+ }
+
+ fn get_active_uniform_block_i(&self, program: GLuint, index: GLuint, pname: GLenum) -> GLint {
+ panic!();
+ //0
+ }
+
+ fn get_active_uniform_block_iv(
+ &self,
+ program: GLuint,
+ index: GLuint,
+ pname: GLenum,
+ ) -> Vec<GLint> {
+ panic!();
+ //Vec::new()
+ }
+
+ fn get_active_uniform_block_name(&self, program: GLuint, index: GLuint) -> String {
+ panic!();
+ //String::new()
+ }
+
+ fn get_attrib_location(&self, program: GLuint, name: &str) -> c_int {
+ let name = CString::new(name).unwrap();
+ unsafe { GetAttribLocation(program, name.as_ptr()) }
+ }
+
+ fn get_frag_data_location(&self, program: GLuint, name: &str) -> c_int {
+ panic!();
+ //0
+ }
+
+ fn get_uniform_location(&self, program: GLuint, name: &str) -> c_int {
+ debug!("get_uniform_location {} {}", program, name);
+ //panic!();
+ let name = CString::new(name).unwrap();
+ unsafe { GetUniformLocation(program, name.as_ptr()) }
+ }
+
+ fn get_program_info_log(&self, program: GLuint) -> String {
+ debug!("get_program_info_log {}", program);
+ String::new()
+ }
+
+ #[inline]
+ unsafe fn get_program_iv(&self, program: GLuint, pname: GLenum, result: &mut [GLint]) {
+ debug!("get_program_iv {}", pname);
+ //panic!();
+ assert!(!result.is_empty());
+ //#define GL_LINK_STATUS 0x8B82
+ if pname == 0x8b82 {
+ result[0] = GetLinkStatus(program);
+ }
+ }
+
+ fn get_program_binary(&self, program: GLuint) -> (Vec<u8>, GLenum) {
+ panic!();
+ //(Vec::new(), NONE)
+ }
+
+ fn program_binary(&self, program: GLuint, format: GLenum, binary: &[u8]) {
+ panic!();
+ }
+
+ fn program_parameter_i(&self, program: GLuint, pname: GLenum, value: GLint) {
+ panic!();
+ }
+
+ #[inline]
+ unsafe fn get_vertex_attrib_iv(&self, index: GLuint, pname: GLenum, result: &mut [GLint]) {
+ panic!();
+ //assert!(!result.is_empty());
+ }
+
+ #[inline]
+ unsafe fn get_vertex_attrib_fv(&self, index: GLuint, pname: GLenum, result: &mut [GLfloat]) {
+ panic!();
+ //assert!(!result.is_empty());
+ }
+
+ fn get_vertex_attrib_pointer_v(&self, index: GLuint, pname: GLenum) -> GLsizeiptr {
+ panic!();
+ //0
+ }
+
+ fn get_buffer_parameter_iv(&self, target: GLuint, pname: GLenum) -> GLint {
+ panic!();
+ //0
+ }
+
+ fn get_shader_info_log(&self, shader: GLuint) -> String {
+ debug!("get_shader_info_log {}", shader);
+ //panic!();
+ String::new()
+ }
+
+ fn get_string(&self, which: GLenum) -> String {
+ // panic!();
+ unsafe {
+ let llstr = GetString(which);
+ if !llstr.is_null() {
+ return str::from_utf8_unchecked(CStr::from_ptr(llstr).to_bytes()).to_string();
+ } else {
+ return "".to_string();
+ }
+ }
+ }
+
+ fn get_string_i(&self, which: GLenum, index: GLuint) -> String {
+ //panic!();
+ unsafe {
+ let llstr = GetStringi(which, index);
+ if !llstr.is_null() {
+ str::from_utf8_unchecked(CStr::from_ptr(llstr).to_bytes()).to_string()
+ } else {
+ "".to_string()
+ }
+ }
+ }
+
+ unsafe fn get_shader_iv(&self, shader: GLuint, pname: GLenum, result: &mut [GLint]) {
+ debug!("get_shader_iv");
+ //panic!();
+ assert!(!result.is_empty());
+ if pname == 0x8B81
+ /*gl::COMPILE_STATUS*/
+ {
+ result[0] = 1;
+ }
+ }
+
+ fn get_shader_precision_format(
+ &self,
+ _shader_type: GLuint,
+ precision_type: GLuint,
+ ) -> (GLint, GLint, GLint) {
+ // gl.GetShaderPrecisionFormat is not available until OpenGL 4.1.
+ // Fallback to OpenGL standard precissions that most desktop hardware support.
+ match precision_type {
+ LOW_FLOAT | MEDIUM_FLOAT | HIGH_FLOAT => {
+ // Fallback to IEEE 754 single precision
+ // Range: from -2^127 to 2^127
+ // Significand precision: 23 bits
+ (127, 127, 23)
+ }
+ LOW_INT | MEDIUM_INT | HIGH_INT => {
+ // Fallback to single precision integer
+ // Range: from -2^24 to 2^24
+ // Precision: For integer formats this value is always 0
+ (24, 24, 0)
+ }
+ _ => (0, 0, 0),
+ }
+ }
+
+ fn compile_shader(&self, shader: GLuint) {
+ debug!("compile_shader {}", shader);
+ //panic!();
+ }
+
+ fn create_program(&self) -> GLuint {
+ debug!("create_program");
+ //panic!();
+ unsafe { CreateProgram() }
+ }
+
+ fn delete_program(&self, program: GLuint) {
+ unsafe {
+ DeleteProgram(program);
+ }
+ }
+
+ fn create_shader(&self, shader_type: GLenum) -> GLuint {
+ debug!("create_shader {}", shader_type);
+ //panic!();
+ unsafe { CreateShader(shader_type) }
+ }
+
+ fn delete_shader(&self, shader: GLuint) {
+ debug!("delete_shader {}", shader);
+ //panic!();
+ unsafe {
+ DeleteShader(shader);
+ }
+ }
+
+ fn detach_shader(&self, program: GLuint, shader: GLuint) {
+ debug!("detach_shader {} {}", program, shader);
+ //panic!();
+ }
+
+ fn link_program(&self, program: GLuint) {
+ debug!("link_program {}", program);
+ //panic!();
+ unsafe {
+ LinkProgram(program);
+ }
+ }
+
+ fn clear_color(&self, r: f32, g: f32, b: f32, a: f32) {
+ //panic!();
+ unsafe {
+ ClearColor(r, g, b, a);
+ }
+ }
+
+ fn clear(&self, buffer_mask: GLbitfield) {
+ debug!("clear {}", buffer_mask);
+ //panic!();
+ unsafe {
+ Clear(buffer_mask);
+ }
+ }
+
+ fn clear_depth(&self, depth: f64) {
+ debug!("clear_depth {}", depth);
+ //panic!();
+ unsafe {
+ ClearDepth(depth as GLclampd);
+ }
+ }
+
+ fn clear_stencil(&self, s: GLint) {
+ panic!();
+ }
+
+ fn flush(&self) {}
+
+ fn finish(&self) {
+ unsafe {
+ Finish();
+ }
+ }
+
+ fn get_error(&self) -> GLenum {
+ //panic!();
+ unsafe { GetError() }
+ }
+
+ fn stencil_mask(&self, mask: GLuint) {
+ panic!();
+ }
+
+ fn stencil_mask_separate(&self, face: GLenum, mask: GLuint) {
+ panic!();
+ }
+
+ fn stencil_func(&self, func: GLenum, ref_: GLint, mask: GLuint) {
+ panic!();
+ }
+
+ fn stencil_func_separate(&self, face: GLenum, func: GLenum, ref_: GLint, mask: GLuint) {
+ panic!();
+ }
+
+ fn stencil_op(&self, sfail: GLenum, dpfail: GLenum, dppass: GLenum) {
+ panic!();
+ }
+
+ fn stencil_op_separate(&self, face: GLenum, sfail: GLenum, dpfail: GLenum, dppass: GLenum) {
+ panic!();
+ }
+
+ fn egl_image_target_texture2d_oes(&self, target: GLenum, image: GLeglImageOES) {
+ panic!("not supported")
+ }
+
+ fn egl_image_target_renderbuffer_storage_oes(&self, target: GLenum, image: GLeglImageOES) {
+ panic!("not supported")
+ }
+
+ fn generate_mipmap(&self, target: GLenum) {
+ unsafe {
+ GenerateMipmap(target);
+ }
+ }
+
+ fn insert_event_marker_ext(&self, message: &str) {
+ panic!();
+ }
+
+ fn push_group_marker_ext(&self, message: &str) {
+ debug!("push group {}", message);
+ panic!();
+ }
+
+ fn pop_group_marker_ext(&self) {
+ debug!("pop group");
+ panic!();
+ }
+
+ fn debug_message_insert_khr(
+ &self,
+ source: GLenum,
+ type_: GLenum,
+ id: GLuint,
+ severity: GLenum,
+ message: &str,
+ ) {
+ panic!();
+ }
+
+ fn push_debug_group_khr(&self, source: GLenum, id: GLuint, message: &str) {
+ panic!();
+ }
+
+ fn pop_debug_group_khr(&self) {
+ panic!();
+ }
+
+ fn fence_sync(&self, condition: GLenum, flags: GLbitfield) -> GLsync {
+ panic!();
+ //ptr::null()
+ }
+
+ fn client_wait_sync(&self, sync: GLsync, flags: GLbitfield, timeout: GLuint64) -> GLenum {
+ panic!();
+ }
+
+ fn wait_sync(&self, sync: GLsync, flags: GLbitfield, timeout: GLuint64) {
+ panic!();
+ }
+
+ fn texture_range_apple(&self, target: GLenum, data: &[u8]) {
+ panic!();
+ }
+
+ fn delete_sync(&self, sync: GLsync) {
+ panic!();
+ }
+
+ fn gen_fences_apple(&self, n: GLsizei) -> Vec<GLuint> {
+ panic!();
+ //Vec::new()
+ }
+
+ fn delete_fences_apple(&self, fences: &[GLuint]) {
+ panic!();
+ }
+
+ fn set_fence_apple(&self, fence: GLuint) {
+ panic!();
+ }
+
+ fn finish_fence_apple(&self, fence: GLuint) {
+ panic!();
+ }
+
+ fn test_fence_apple(&self, fence: GLuint) {
+ panic!();
+ }
+
+ fn test_object_apple(&self, object: GLenum, name: GLuint) -> GLboolean {
+ panic!();
+ //0
+ }
+
+ fn finish_object_apple(&self, object: GLenum, name: GLuint) {
+ panic!();
+ }
+
+ // GL_ARB_blend_func_extended
+ fn bind_frag_data_location_indexed(
+ &self,
+ program: GLuint,
+ color_number: GLuint,
+ index: GLuint,
+ name: &str,
+ ) {
+ panic!();
+ }
+
+ fn get_frag_data_index(&self, program: GLuint, name: &str) -> GLint {
+ panic!();
+ //-1
+ }
+
+ // GL_KHR_debug
+ fn get_debug_messages(&self) -> Vec<DebugMessage> {
+ Vec::new()
+ }
+
+ fn provoking_vertex_angle(&self, _mode: GLenum) {
+ unimplemented!("This extension is GLES only");
+ }
+
+ // GL_KHR_blend_equation_advanced
+ fn blend_barrier_khr(&self) {
+ // No barrier required, so nothing to do
+ }
+
+ // GL_CHROMIUM_copy_texture
+ fn copy_texture_chromium(
+ &self,
+ _source_id: GLuint,
+ _source_level: GLint,
+ _dest_target: GLenum,
+ _dest_id: GLuint,
+ _dest_level: GLint,
+ _internal_format: GLint,
+ _dest_type: GLenum,
+ _unpack_flip_y: GLboolean,
+ _unpack_premultiply_alpha: GLboolean,
+ _unpack_unmultiply_alpha: GLboolean,
+ ) {
+ unimplemented!("This extension is GLES only");
+ }
+ fn copy_sub_texture_chromium(
+ &self,
+ _source_id: GLuint,
+ _source_level: GLint,
+ _dest_target: GLenum,
+ _dest_id: GLuint,
+ _dest_level: GLint,
+ _x_offset: GLint,
+ _y_offset: GLint,
+ _x: GLint,
+ _y: GLint,
+ _width: GLsizei,
+ _height: GLsizei,
+ _unpack_flip_y: GLboolean,
+ _unpack_premultiply_alpha: GLboolean,
+ _unpack_unmultiply_alpha: GLboolean,
+ ) {
+ unimplemented!("This extension is GLES only");
+ }
+
+ // GL_ANGLE_copy_texture_3d
+ fn copy_texture_3d_angle(
+ &self,
+ _source_id: GLuint,
+ _source_level: GLint,
+ _dest_target: GLenum,
+ _dest_id: GLuint,
+ _dest_level: GLint,
+ _internal_format: GLint,
+ _dest_type: GLenum,
+ _unpack_flip_y: GLboolean,
+ _unpack_premultiply_alpha: GLboolean,
+ _unpack_unmultiply_alpha: GLboolean,
+ ) {
+ unimplemented!("Not supported by SWGL");
+ }
+
+ fn copy_sub_texture_3d_angle(
+ &self,
+ _source_id: GLuint,
+ _source_level: GLint,
+ _dest_target: GLenum,
+ _dest_id: GLuint,
+ _dest_level: GLint,
+ _x_offset: GLint,
+ _y_offset: GLint,
+ _z_offset: GLint,
+ _x: GLint,
+ _y: GLint,
+ _z: GLint,
+ _width: GLsizei,
+ _height: GLsizei,
+ _depth: GLsizei,
+ _unpack_flip_y: GLboolean,
+ _unpack_premultiply_alpha: GLboolean,
+ _unpack_unmultiply_alpha: GLboolean,
+ ) {
+ unimplemented!("Not supported by SWGL");
+ }
+
+ fn buffer_storage(
+ &self,
+ target: GLenum,
+ size: GLsizeiptr,
+ data: *const GLvoid,
+ flags: GLbitfield,
+ ) {
+ unimplemented!("Not supported by SWGL");
+ }
+
+ fn flush_mapped_buffer_range(&self, target: GLenum, offset: GLintptr, length: GLsizeiptr) {
+ unimplemented!("Not supported by SWGL");
+ }
+
+ fn start_tiling_qcom(
+ &self,
+ x: GLuint,
+ y: GLuint,
+ width: GLuint,
+ height: GLuint,
+ preserve_mask: GLbitfield,
+ ) {
+ unimplemented!("Not supported by SWGL");
+ }
+
+ fn end_tiling_qcom(&self, preserve_mask: GLbitfield) {
+ unimplemented!("Not supported by SWGL");
+ }
+}
+
+/// A resource that is intended for sharing between threads.
+/// Locked resources such as textures or framebuffers will
+/// not allow any further modifications while it remains
+/// locked. The resource will be unlocked when LockedResource
+/// is dropped.
+pub struct LockedResource(*mut LockedTexture);
+
+unsafe impl Send for LockedResource {}
+unsafe impl Sync for LockedResource {}
+
+#[repr(u8)]
+pub enum YuvRangedColorSpace {
+ Rec601Narrow = 0,
+ Rec601Full,
+ Rec709Narrow,
+ Rec709Full,
+ Rec2020Narrow,
+ Rec2020Full,
+ GbrIdentity,
+}
+
+impl LockedResource {
+ /// Composites from a locked resource to another locked resource. The band
+ /// offset and height are relative to the destination rectangle and specify
+ /// how to clip the composition into appropriate range for this band.
+ pub fn composite(
+ &self,
+ locked_src: &LockedResource,
+ src_x: GLint,
+ src_y: GLint,
+ src_width: GLsizei,
+ src_height: GLsizei,
+ dst_x: GLint,
+ dst_y: GLint,
+ dst_width: GLsizei,
+ dst_height: GLsizei,
+ opaque: bool,
+ flip_x: bool,
+ flip_y: bool,
+ filter: GLenum,
+ clip_x: GLint,
+ clip_y: GLint,
+ clip_width: GLsizei,
+ clip_height: GLsizei,
+ ) {
+ unsafe {
+ Composite(
+ self.0,
+ locked_src.0,
+ src_x,
+ src_y,
+ src_width,
+ src_height,
+ dst_x,
+ dst_y,
+ dst_width,
+ dst_height,
+ opaque as GLboolean,
+ flip_x as GLboolean,
+ flip_y as GLboolean,
+ filter,
+ clip_x,
+ clip_y,
+ clip_width,
+ clip_height,
+ );
+ }
+ }
+
+ /// Composites from locked resources representing YUV planes
+ pub fn composite_yuv(
+ &self,
+ locked_y: &LockedResource,
+ locked_u: &LockedResource,
+ locked_v: &LockedResource,
+ color_space: YuvRangedColorSpace,
+ color_depth: GLuint,
+ src_x: GLint,
+ src_y: GLint,
+ src_width: GLsizei,
+ src_height: GLsizei,
+ dst_x: GLint,
+ dst_y: GLint,
+ dst_width: GLsizei,
+ dst_height: GLsizei,
+ flip_x: bool,
+ flip_y: bool,
+ clip_x: GLint,
+ clip_y: GLint,
+ clip_width: GLsizei,
+ clip_height: GLsizei,
+ ) {
+ unsafe {
+ CompositeYUV(
+ self.0,
+ locked_y.0,
+ locked_u.0,
+ locked_v.0,
+ color_space,
+ color_depth,
+ src_x,
+ src_y,
+ src_width,
+ src_height,
+ dst_x,
+ dst_y,
+ dst_width,
+ dst_height,
+ flip_x as GLboolean,
+ flip_y as GLboolean,
+ clip_x,
+ clip_y,
+ clip_width,
+ clip_height,
+ );
+ }
+ }
+
+ /// Get the underlying buffer for a locked resource
+ pub fn get_buffer(&self) -> (*mut c_void, i32, i32, i32) {
+ unsafe {
+ let mut width: i32 = 0;
+ let mut height: i32 = 0;
+ let mut stride: i32 = 0;
+ let data_ptr = GetResourceBuffer(self.0, &mut width, &mut height, &mut stride);
+ (data_ptr, width, height, stride)
+ }
+ }
+}
+
+impl Clone for LockedResource {
+ fn clone(&self) -> Self {
+ unsafe {
+ LockResource(self.0);
+ }
+ LockedResource(self.0)
+ }
+}
+
+impl Drop for LockedResource {
+ fn drop(&mut self) {
+ unsafe {
+ UnlockResource(self.0);
+ }
+ }
+}
diff --git a/gfx/wr/swgl/src/texture.h b/gfx/wr/swgl/src/texture.h
new file mode 100644
index 0000000000..57d27ed05c
--- /dev/null
+++ b/gfx/wr/swgl/src/texture.h
@@ -0,0 +1,1299 @@
+/* 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/. */
+
+namespace glsl {
+
+using PackedRGBA8 = V16<uint8_t>;
+using WideRGBA8 = V16<uint16_t>;
+using HalfRGBA8 = V8<uint16_t>;
+
+SI WideRGBA8 unpack(PackedRGBA8 p) { return CONVERT(p, WideRGBA8); }
+
+template <int N>
+UNUSED SI VectorType<uint8_t, N> genericPackWide(VectorType<uint16_t, N> p) {
+ typedef VectorType<uint8_t, N> packed_type;
+ // Generic conversions only mask off the low byte without actually clamping
+ // like a real pack. First force the word to all 1s if it overflows, and then
+ // add on the sign bit to cause it to roll over to 0 if it was negative.
+ p = (p | (p > 255)) + (p >> 15);
+ return CONVERT(p, packed_type);
+}
+
+SI PackedRGBA8 pack(WideRGBA8 p) {
+#if USE_SSE2
+ return _mm_packus_epi16(lowHalf(p), highHalf(p));
+#elif USE_NEON
+ return vcombine_u8(vqmovun_s16(bit_cast<V8<int16_t>>(lowHalf(p))),
+ vqmovun_s16(bit_cast<V8<int16_t>>(highHalf(p))));
+#else
+ return genericPackWide(p);
+#endif
+}
+
+using PackedR8 = V4<uint8_t>;
+using WideR8 = V4<uint16_t>;
+
+SI WideR8 unpack(PackedR8 p) { return CONVERT(p, WideR8); }
+
+SI PackedR8 pack(WideR8 p) {
+#if USE_SSE2
+ auto m = expand(p);
+ auto r = bit_cast<V16<uint8_t>>(_mm_packus_epi16(m, m));
+ return SHUFFLE(r, r, 0, 1, 2, 3);
+#elif USE_NEON
+ return lowHalf(
+ bit_cast<V8<uint8_t>>(vqmovun_s16(bit_cast<V8<int16_t>>(expand(p)))));
+#else
+ return genericPackWide(p);
+#endif
+}
+
+using PackedRG8 = V8<uint8_t>;
+using WideRG8 = V8<uint16_t>;
+
+SI PackedRG8 pack(WideRG8 p) {
+#if USE_SSE2
+ return lowHalf(bit_cast<V16<uint8_t>>(_mm_packus_epi16(p, p)));
+#elif USE_NEON
+ return bit_cast<V8<uint8_t>>(vqmovun_s16(bit_cast<V8<int16_t>>(p)));
+#else
+ return genericPackWide(p);
+#endif
+}
+
+SI I32 clampCoord(I32 coord, int limit, int base = 0) {
+#if USE_SSE2
+ return _mm_min_epi16(_mm_max_epi16(coord, _mm_set1_epi32(base)),
+ _mm_set1_epi32(limit - 1));
+#else
+ return clamp(coord, base, limit - 1);
+#endif
+}
+
+SI int clampCoord(int coord, int limit, int base = 0) {
+ return min(max(coord, base), limit - 1);
+}
+
+template <typename T, typename S>
+SI T clamp2D(T P, S sampler) {
+ return T{clampCoord(P.x, sampler->width), clampCoord(P.y, sampler->height)};
+}
+
+SI float to_float(uint32_t x) { return x * (1.f / 255.f); }
+
+SI vec4 pixel_to_vec4(uint32_t a, uint32_t b, uint32_t c, uint32_t d) {
+ U32 pixels = {a, b, c, d};
+ return vec4(cast((pixels >> 16) & 0xFF), cast((pixels >> 8) & 0xFF),
+ cast(pixels & 0xFF), cast(pixels >> 24)) *
+ (1.0f / 255.0f);
+}
+
+SI vec4 pixel_float_to_vec4(Float a, Float b, Float c, Float d) {
+ return vec4(Float{a.x, b.x, c.x, d.x}, Float{a.y, b.y, c.y, d.y},
+ Float{a.z, b.z, c.z, d.z}, Float{a.w, b.w, c.w, d.w});
+}
+
+SI ivec4 pixel_int_to_ivec4(I32 a, I32 b, I32 c, I32 d) {
+ return ivec4(I32{a.x, b.x, c.x, d.x}, I32{a.y, b.y, c.y, d.y},
+ I32{a.z, b.z, c.z, d.z}, I32{a.w, b.w, c.w, d.w});
+}
+
+SI vec4_scalar pixel_to_vec4(uint32_t p) {
+ U32 i = {(p >> 16) & 0xFF, (p >> 8) & 0xFF, p & 0xFF, p >> 24};
+ Float f = cast(i) * (1.0f / 255.0f);
+ return vec4_scalar(f.x, f.y, f.z, f.w);
+}
+
+template <typename S>
+SI vec4 fetchOffsetsRGBA8(S sampler, I32 offset) {
+ return pixel_to_vec4(sampler->buf[offset.x], sampler->buf[offset.y],
+ sampler->buf[offset.z], sampler->buf[offset.w]);
+}
+
+template <typename S>
+vec4 texelFetchRGBA8(S sampler, ivec2 P) {
+ I32 offset = P.x + P.y * sampler->stride;
+ return fetchOffsetsRGBA8(sampler, offset);
+}
+
+template <typename S>
+SI Float fetchOffsetsR8(S sampler, I32 offset) {
+ U32 i = {
+ ((uint8_t*)sampler->buf)[offset.x], ((uint8_t*)sampler->buf)[offset.y],
+ ((uint8_t*)sampler->buf)[offset.z], ((uint8_t*)sampler->buf)[offset.w]};
+ return cast(i) * (1.0f / 255.0f);
+}
+
+template <typename S>
+vec4 texelFetchR8(S sampler, ivec2 P) {
+ I32 offset = P.x + P.y * sampler->stride;
+ return vec4(fetchOffsetsR8(sampler, offset), 0.0f, 0.0f, 1.0f);
+}
+
+template <typename S>
+SI vec4 fetchOffsetsRG8(S sampler, I32 offset) {
+ uint16_t* buf = (uint16_t*)sampler->buf;
+ U16 pixels = {buf[offset.x], buf[offset.y], buf[offset.z], buf[offset.w]};
+ Float r = CONVERT(pixels & 0xFF, Float) * (1.0f / 255.0f);
+ Float g = CONVERT(pixels >> 8, Float) * (1.0f / 255.0f);
+ return vec4(r, g, 0.0f, 1.0f);
+}
+
+template <typename S>
+vec4 texelFetchRG8(S sampler, ivec2 P) {
+ I32 offset = P.x + P.y * sampler->stride;
+ return fetchOffsetsRG8(sampler, offset);
+}
+
+template <typename S>
+SI Float fetchOffsetsR16(S sampler, I32 offset) {
+ U32 i = {
+ ((uint16_t*)sampler->buf)[offset.x], ((uint16_t*)sampler->buf)[offset.y],
+ ((uint16_t*)sampler->buf)[offset.z], ((uint16_t*)sampler->buf)[offset.w]};
+ return cast(i) * (1.0f / 65535.0f);
+}
+
+template <typename S>
+vec4 texelFetchR16(S sampler, ivec2 P) {
+ I32 offset = P.x + P.y * sampler->stride;
+ return vec4(fetchOffsetsR16(sampler, offset), 0.0f, 0.0f, 1.0f);
+}
+
+template <typename S>
+SI vec4 fetchOffsetsRG16(S sampler, I32 offset) {
+ U32 pixels = {sampler->buf[offset.x], sampler->buf[offset.y],
+ sampler->buf[offset.z], sampler->buf[offset.w]};
+ Float r = cast(pixels & 0xFFFF) * (1.0f / 65535.0f);
+ Float g = cast(pixels >> 16) * (1.0f / 65535.0f);
+ return vec4(r, g, 0.0f, 1.0f);
+}
+
+template <typename S>
+vec4 texelFetchRG16(S sampler, ivec2 P) {
+ I32 offset = P.x + P.y * sampler->stride;
+ return fetchOffsetsRG16(sampler, offset);
+}
+
+SI vec4 fetchOffsetsFloat(const uint32_t* buf, I32 offset) {
+ return pixel_float_to_vec4(*(Float*)&buf[offset.x], *(Float*)&buf[offset.y],
+ *(Float*)&buf[offset.z], *(Float*)&buf[offset.w]);
+}
+
+SI vec4 fetchOffsetsFloat(samplerCommon* sampler, I32 offset) {
+ return fetchOffsetsFloat(sampler->buf, offset);
+}
+
+vec4 texelFetchFloat(sampler2D sampler, ivec2 P) {
+ I32 offset = P.x * 4 + P.y * sampler->stride;
+ return fetchOffsetsFloat(sampler, offset);
+}
+
+template <typename S>
+SI vec4 fetchOffsetsYUV422(S sampler, I32 offset) {
+ // Layout is 2 pixel chunks (occupying 4 bytes) organized as: G0, B, G1, R.
+ // Offset is aligned to a chunk rather than a pixel, and selector specifies
+ // pixel within the chunk.
+ I32 selector = offset & 1;
+ offset &= ~1;
+ uint16_t* buf = (uint16_t*)sampler->buf;
+ U32 pixels = {*(uint32_t*)&buf[offset.x], *(uint32_t*)&buf[offset.y],
+ *(uint32_t*)&buf[offset.z], *(uint32_t*)&buf[offset.w]};
+ Float b = CONVERT((pixels >> 8) & 0xFF, Float) * (1.0f / 255.0f);
+ Float r = CONVERT((pixels >> 24), Float) * (1.0f / 255.0f);
+ Float g =
+ CONVERT(if_then_else(-selector, pixels >> 16, pixels) & 0xFF, Float) *
+ (1.0f / 255.0f);
+ return vec4(r, g, b, 1.0f);
+}
+
+template <typename S>
+vec4 texelFetchYUV422(S sampler, ivec2 P) {
+ I32 offset = P.x + P.y * sampler->stride;
+ return fetchOffsetsYUV422(sampler, offset);
+}
+
+vec4 texelFetch(sampler2D sampler, ivec2 P, int lod) {
+ assert(lod == 0);
+ P = clamp2D(P, sampler);
+ switch (sampler->format) {
+ case TextureFormat::RGBA32F:
+ return texelFetchFloat(sampler, P);
+ case TextureFormat::RGBA8:
+ return texelFetchRGBA8(sampler, P);
+ case TextureFormat::R8:
+ return texelFetchR8(sampler, P);
+ case TextureFormat::RG8:
+ return texelFetchRG8(sampler, P);
+ case TextureFormat::R16:
+ return texelFetchR16(sampler, P);
+ case TextureFormat::RG16:
+ return texelFetchRG16(sampler, P);
+ case TextureFormat::YUV422:
+ return texelFetchYUV422(sampler, P);
+ default:
+ assert(false);
+ return vec4();
+ }
+}
+
+vec4 texelFetch(sampler2DRGBA32F sampler, ivec2 P, int lod) {
+ assert(lod == 0);
+ P = clamp2D(P, sampler);
+ assert(sampler->format == TextureFormat::RGBA32F);
+ return texelFetchFloat(sampler, P);
+}
+
+vec4 texelFetch(sampler2DRGBA8 sampler, ivec2 P, int lod) {
+ assert(lod == 0);
+ P = clamp2D(P, sampler);
+ assert(sampler->format == TextureFormat::RGBA8);
+ return texelFetchRGBA8(sampler, P);
+}
+
+vec4 texelFetch(sampler2DR8 sampler, ivec2 P, int lod) {
+ assert(lod == 0);
+ P = clamp2D(P, sampler);
+ assert(sampler->format == TextureFormat::R8);
+ return texelFetchR8(sampler, P);
+}
+
+vec4 texelFetch(sampler2DRG8 sampler, ivec2 P, int lod) {
+ assert(lod == 0);
+ P = clamp2D(P, sampler);
+ assert(sampler->format == TextureFormat::RG8);
+ return texelFetchRG8(sampler, P);
+}
+
+vec4_scalar texelFetch(sampler2D sampler, ivec2_scalar P, int lod) {
+ assert(lod == 0);
+ P = clamp2D(P, sampler);
+ if (sampler->format == TextureFormat::RGBA32F) {
+ return *(vec4_scalar*)&sampler->buf[P.x * 4 + P.y * sampler->stride];
+ } else {
+ assert(sampler->format == TextureFormat::RGBA8);
+ return pixel_to_vec4(sampler->buf[P.x + P.y * sampler->stride]);
+ }
+}
+
+vec4_scalar texelFetch(sampler2DRGBA32F sampler, ivec2_scalar P, int lod) {
+ assert(lod == 0);
+ P = clamp2D(P, sampler);
+ assert(sampler->format == TextureFormat::RGBA32F);
+ return *(vec4_scalar*)&sampler->buf[P.x * 4 + P.y * sampler->stride];
+}
+
+vec4_scalar texelFetch(sampler2DRGBA8 sampler, ivec2_scalar P, int lod) {
+ assert(lod == 0);
+ P = clamp2D(P, sampler);
+ assert(sampler->format == TextureFormat::RGBA8);
+ return pixel_to_vec4(sampler->buf[P.x + P.y * sampler->stride]);
+}
+
+vec4_scalar texelFetch(sampler2DR8 sampler, ivec2_scalar P, int lod) {
+ assert(lod == 0);
+ P = clamp2D(P, sampler);
+ assert(sampler->format == TextureFormat::R8);
+ return vec4_scalar{
+ to_float(((uint8_t*)sampler->buf)[P.x + P.y * sampler->stride]), 0.0f,
+ 0.0f, 1.0f};
+}
+
+vec4_scalar texelFetch(sampler2DRG8 sampler, ivec2_scalar P, int lod) {
+ assert(lod == 0);
+ P = clamp2D(P, sampler);
+ assert(sampler->format == TextureFormat::RG8);
+ uint16_t pixel = ((uint16_t*)sampler->buf)[P.x + P.y * sampler->stride];
+ return vec4_scalar{to_float(pixel & 0xFF), to_float(pixel >> 8), 0.0f, 1.0f};
+}
+
+vec4 texelFetch(sampler2DRect sampler, ivec2 P) {
+ P = clamp2D(P, sampler);
+ switch (sampler->format) {
+ case TextureFormat::RGBA8:
+ return texelFetchRGBA8(sampler, P);
+ case TextureFormat::R8:
+ return texelFetchR8(sampler, P);
+ case TextureFormat::RG8:
+ return texelFetchRG8(sampler, P);
+ case TextureFormat::R16:
+ return texelFetchR16(sampler, P);
+ case TextureFormat::RG16:
+ return texelFetchRG16(sampler, P);
+ case TextureFormat::YUV422:
+ return texelFetchYUV422(sampler, P);
+ default:
+ assert(false);
+ return vec4();
+ }
+}
+
+SI ivec4 fetchOffsetsInt(const uint32_t* buf, I32 offset) {
+ return pixel_int_to_ivec4(*(I32*)&buf[offset.x], *(I32*)&buf[offset.y],
+ *(I32*)&buf[offset.z], *(I32*)&buf[offset.w]);
+}
+
+SI ivec4 fetchOffsetsInt(samplerCommon* sampler, I32 offset) {
+ return fetchOffsetsInt(sampler->buf, offset);
+}
+
+ivec4 texelFetch(isampler2D sampler, ivec2 P, int lod) {
+ assert(lod == 0);
+ P = clamp2D(P, sampler);
+ assert(sampler->format == TextureFormat::RGBA32I);
+ I32 offset = P.x * 4 + P.y * sampler->stride;
+ return fetchOffsetsInt(sampler, offset);
+}
+
+ivec4_scalar texelFetch(isampler2D sampler, ivec2_scalar P, int lod) {
+ assert(lod == 0);
+ P = clamp2D(P, sampler);
+ assert(sampler->format == TextureFormat::RGBA32I);
+ return *(ivec4_scalar*)&sampler->buf[P.x * 4 + P.y * sampler->stride];
+}
+
+constexpr int MAX_TEXEL_OFFSET = 8;
+
+// Fill texelFetchOffset outside the valid texture bounds with zeroes. The
+// stride will be set to 0 so that only one row of zeroes is needed.
+static const uint32_t
+ zeroFetchBuf[MAX_TEXEL_OFFSET * sizeof(Float) / sizeof(uint32_t)] = {0};
+
+struct FetchScalar {
+ const uint32_t* buf;
+ uint32_t stride;
+};
+
+template <typename S>
+SI FetchScalar texelFetchPtr(S sampler, ivec2_scalar P, int min_x, int max_x,
+ int min_y, int max_y) {
+ assert(max_x < MAX_TEXEL_OFFSET);
+ if (P.x < -min_x || P.x >= int(sampler->width) - max_x || P.y < -min_y ||
+ P.y >= int(sampler->height) - max_y) {
+ return FetchScalar{zeroFetchBuf, 0};
+ }
+ return FetchScalar{&sampler->buf[P.x * 4 + P.y * sampler->stride],
+ sampler->stride};
+}
+
+SI vec4_scalar texelFetchUnchecked(sampler2D sampler, FetchScalar ptr, int x,
+ int y = 0) {
+ assert(sampler->format == TextureFormat::RGBA32F);
+ return *(vec4_scalar*)&ptr.buf[x * 4 + y * ptr.stride];
+}
+
+SI ivec4_scalar texelFetchUnchecked(isampler2D sampler, FetchScalar ptr, int x,
+ int y = 0) {
+ assert(sampler->format == TextureFormat::RGBA32I);
+ return *(ivec4_scalar*)&ptr.buf[x * 4 + y * ptr.stride];
+}
+
+struct FetchVector {
+ const uint32_t* buf;
+ I32 offset;
+ uint32_t stride;
+};
+
+template <typename S>
+SI FetchVector texelFetchPtr(S sampler, ivec2 P, int min_x, int max_x,
+ int min_y, int max_y) {
+ assert(max_x < MAX_TEXEL_OFFSET);
+ if (test_any(P.x < -min_x || P.x >= int(sampler->width) - max_x ||
+ P.y < -min_y || P.y >= int(sampler->height) - max_y)) {
+ return FetchVector{zeroFetchBuf, I32(0), 0};
+ }
+ return FetchVector{sampler->buf, P.x * 4 + P.y * sampler->stride,
+ sampler->stride};
+}
+
+SI vec4 texelFetchUnchecked(sampler2D sampler, FetchVector ptr, int x,
+ int y = 0) {
+ assert(sampler->format == TextureFormat::RGBA32F);
+ return fetchOffsetsFloat(&ptr.buf[x * 4 + y * ptr.stride], ptr.offset);
+}
+
+SI ivec4 texelFetchUnchecked(isampler2D sampler, FetchVector ptr, int x,
+ int y = 0) {
+ assert(sampler->format == TextureFormat::RGBA32I);
+ return fetchOffsetsInt(&ptr.buf[x * 4 + y * ptr.stride], ptr.offset);
+}
+
+#define texelFetchOffset(sampler, P, lod, offset) \
+ texelFetch(sampler, (P) + (offset), lod)
+
+// Scale texture coords for quantization, subtract offset for filtering
+// (assuming coords already offset to texel centers), and round to nearest
+// 1/scale increment
+template <typename T>
+SI T linearQuantize(T P, float scale) {
+ return P * scale + (0.5f - 0.5f * scale);
+}
+
+// Helper version that also scales normalized texture coords for sampler
+template <typename T, typename S>
+SI T samplerScale(S sampler, T P) {
+ P.x *= sampler->width;
+ P.y *= sampler->height;
+ return P;
+}
+
+template <typename T>
+SI T samplerScale(UNUSED sampler2DRect sampler, T P) {
+ return P;
+}
+
+template <typename T, typename S>
+SI T linearQuantize(T P, float scale, S sampler) {
+ return linearQuantize(samplerScale(sampler, P), scale);
+}
+
+// Compute clamped offset of first row for linear interpolation
+template <typename S, typename I>
+SI auto computeRow(S sampler, I i, size_t margin = 1) -> decltype(i.x) {
+ return clampCoord(i.x, sampler->width - margin) +
+ clampCoord(i.y, sampler->height) * sampler->stride;
+}
+
+// Compute clamped offset of second row for linear interpolation from first row
+template <typename S, typename I>
+SI auto computeNextRowOffset(S sampler, I i) -> decltype(i.x) {
+ return if_then_else(i.y >= 0 && i.y < int32_t(sampler->height) - 1,
+ sampler->stride, 0);
+}
+
+// Convert X coordinate to a 2^7 scale fraction for interpolation
+template <typename S>
+SI I16 computeFracX(S sampler, ivec2 i, ivec2 frac) {
+ auto overread = i.x > int32_t(sampler->width) - 2;
+ return CONVERT((((frac.x & (i.x >= 0)) | overread) & 0x7F) - overread, I16);
+}
+
+// Convert Y coordinate to a 2^7 scale fraction for interpolation
+SI I16 computeFracNoClamp(I32 frac) { return CONVERT(frac & 0x7F, I16); }
+SI I16 computeFracY(ivec2 frac) { return computeFracNoClamp(frac.y); }
+
+struct WidePlanarRGBA8 {
+ V8<uint16_t> rg;
+ V8<uint16_t> ba;
+};
+
+template <typename S>
+SI WidePlanarRGBA8 textureLinearPlanarRGBA8(S sampler, ivec2 i) {
+ assert(sampler->format == TextureFormat::RGBA8);
+
+ ivec2 frac = i;
+ i >>= 7;
+
+ I32 row0 = computeRow(sampler, i);
+ I32 row1 = row0 + computeNextRowOffset(sampler, i);
+ I16 fracx = computeFracX(sampler, i, frac);
+ I16 fracy = computeFracY(frac);
+
+ auto a0 =
+ CONVERT(unaligned_load<V8<uint8_t>>(&sampler->buf[row0.x]), V8<int16_t>);
+ auto a1 =
+ CONVERT(unaligned_load<V8<uint8_t>>(&sampler->buf[row1.x]), V8<int16_t>);
+ a0 += ((a1 - a0) * fracy.x) >> 7;
+
+ auto b0 =
+ CONVERT(unaligned_load<V8<uint8_t>>(&sampler->buf[row0.y]), V8<int16_t>);
+ auto b1 =
+ CONVERT(unaligned_load<V8<uint8_t>>(&sampler->buf[row1.y]), V8<int16_t>);
+ b0 += ((b1 - b0) * fracy.y) >> 7;
+
+ auto abl = zipLow(a0, b0);
+ auto abh = zipHigh(a0, b0);
+ abl += ((abh - abl) * fracx.xyxyxyxy) >> 7;
+
+ auto c0 =
+ CONVERT(unaligned_load<V8<uint8_t>>(&sampler->buf[row0.z]), V8<int16_t>);
+ auto c1 =
+ CONVERT(unaligned_load<V8<uint8_t>>(&sampler->buf[row1.z]), V8<int16_t>);
+ c0 += ((c1 - c0) * fracy.z) >> 7;
+
+ auto d0 =
+ CONVERT(unaligned_load<V8<uint8_t>>(&sampler->buf[row0.w]), V8<int16_t>);
+ auto d1 =
+ CONVERT(unaligned_load<V8<uint8_t>>(&sampler->buf[row1.w]), V8<int16_t>);
+ d0 += ((d1 - d0) * fracy.w) >> 7;
+
+ auto cdl = zipLow(c0, d0);
+ auto cdh = zipHigh(c0, d0);
+ cdl += ((cdh - cdl) * fracx.zwzwzwzw) >> 7;
+
+ auto rg = V8<uint16_t>(zip2Low(abl, cdl));
+ auto ba = V8<uint16_t>(zip2High(abl, cdl));
+ return WidePlanarRGBA8{rg, ba};
+}
+
+template <typename S>
+vec4 textureLinearRGBA8(S sampler, vec2 P) {
+ ivec2 i(linearQuantize(P, 128, sampler));
+ auto planar = textureLinearPlanarRGBA8(sampler, i);
+ auto rg = CONVERT(planar.rg, V8<float>);
+ auto ba = CONVERT(planar.ba, V8<float>);
+ auto r = lowHalf(rg);
+ auto g = highHalf(rg);
+ auto b = lowHalf(ba);
+ auto a = highHalf(ba);
+ return vec4(b, g, r, a) * (1.0f / 255.0f);
+}
+
+template <typename S>
+static inline U16 textureLinearUnpackedR8(S sampler, ivec2 i) {
+ assert(sampler->format == TextureFormat::R8);
+ ivec2 frac = i;
+ i >>= 7;
+
+ I32 row0 = computeRow(sampler, i);
+ I32 row1 = row0 + computeNextRowOffset(sampler, i);
+ I16 fracx = computeFracX(sampler, i, frac);
+ I16 fracy = computeFracY(frac);
+
+ uint8_t* buf = (uint8_t*)sampler->buf;
+ auto a0 = unaligned_load<V2<uint8_t>>(&buf[row0.x]);
+ auto b0 = unaligned_load<V2<uint8_t>>(&buf[row0.y]);
+ auto c0 = unaligned_load<V2<uint8_t>>(&buf[row0.z]);
+ auto d0 = unaligned_load<V2<uint8_t>>(&buf[row0.w]);
+ auto abcd0 = CONVERT(combine(a0, b0, c0, d0), V8<int16_t>);
+
+ auto a1 = unaligned_load<V2<uint8_t>>(&buf[row1.x]);
+ auto b1 = unaligned_load<V2<uint8_t>>(&buf[row1.y]);
+ auto c1 = unaligned_load<V2<uint8_t>>(&buf[row1.z]);
+ auto d1 = unaligned_load<V2<uint8_t>>(&buf[row1.w]);
+ auto abcd1 = CONVERT(combine(a1, b1, c1, d1), V8<int16_t>);
+
+ abcd0 += ((abcd1 - abcd0) * fracy.xxyyzzww) >> 7;
+
+ abcd0 = SHUFFLE(abcd0, abcd0, 0, 2, 4, 6, 1, 3, 5, 7);
+ auto abcdl = lowHalf(abcd0);
+ auto abcdh = highHalf(abcd0);
+ abcdl += ((abcdh - abcdl) * fracx) >> 7;
+
+ return U16(abcdl);
+}
+
+template <typename S>
+vec4 textureLinearR8(S sampler, vec2 P) {
+ assert(sampler->format == TextureFormat::R8);
+
+ ivec2 i(linearQuantize(P, 128, sampler));
+ Float r = CONVERT(textureLinearUnpackedR8(sampler, i), Float);
+ return vec4(r * (1.0f / 255.0f), 0.0f, 0.0f, 1.0f);
+}
+
+struct WidePlanarRG8 {
+ V8<uint16_t> rg;
+};
+
+template <typename S>
+SI WidePlanarRG8 textureLinearPlanarRG8(S sampler, ivec2 i) {
+ assert(sampler->format == TextureFormat::RG8);
+
+ ivec2 frac = i;
+ i >>= 7;
+
+ I32 row0 = computeRow(sampler, i);
+ I32 row1 = row0 + computeNextRowOffset(sampler, i);
+ I16 fracx = computeFracX(sampler, i, frac);
+ I16 fracy = computeFracY(frac);
+
+ uint16_t* buf = (uint16_t*)sampler->buf;
+
+ // Load RG bytes for two adjacent pixels - rgRG
+ auto a0 = unaligned_load<V4<uint8_t>>(&buf[row0.x]);
+ auto b0 = unaligned_load<V4<uint8_t>>(&buf[row0.y]);
+ auto ab0 = CONVERT(combine(a0, b0), V8<int16_t>);
+ // Load two pixels for next row
+ auto a1 = unaligned_load<V4<uint8_t>>(&buf[row1.x]);
+ auto b1 = unaligned_load<V4<uint8_t>>(&buf[row1.y]);
+ auto ab1 = CONVERT(combine(a1, b1), V8<int16_t>);
+ // Blend rows
+ ab0 += ((ab1 - ab0) * fracy.xxxxyyyy) >> 7;
+
+ auto c0 = unaligned_load<V4<uint8_t>>(&buf[row0.z]);
+ auto d0 = unaligned_load<V4<uint8_t>>(&buf[row0.w]);
+ auto cd0 = CONVERT(combine(c0, d0), V8<int16_t>);
+ auto c1 = unaligned_load<V4<uint8_t>>(&buf[row1.z]);
+ auto d1 = unaligned_load<V4<uint8_t>>(&buf[row1.w]);
+ auto cd1 = CONVERT(combine(c1, d1), V8<int16_t>);
+ // Blend rows
+ cd0 += ((cd1 - cd0) * fracy.zzzzwwww) >> 7;
+
+ // ab = a.rgRG,b.rgRG
+ // cd = c.rgRG,d.rgRG
+ // ... ac = ar,cr,ag,cg,aR,cR,aG,cG
+ // ... bd = br,dr,bg,dg,bR,dR,bG,dG
+ auto ac = zipLow(ab0, cd0);
+ auto bd = zipHigh(ab0, cd0);
+ // ar,br,cr,dr,ag,bg,cg,dg
+ // aR,bR,cR,dR,aG,bG,cG,dG
+ auto abcdl = zipLow(ac, bd);
+ auto abcdh = zipHigh(ac, bd);
+ // Blend columns
+ abcdl += ((abcdh - abcdl) * fracx.xyzwxyzw) >> 7;
+
+ auto rg = V8<uint16_t>(abcdl);
+ return WidePlanarRG8{rg};
+}
+
+template <typename S>
+vec4 textureLinearRG8(S sampler, vec2 P) {
+ ivec2 i(linearQuantize(P, 128, sampler));
+ auto planar = textureLinearPlanarRG8(sampler, i);
+ auto rg = CONVERT(planar.rg, V8<float>) * (1.0f / 255.0f);
+ auto r = lowHalf(rg);
+ auto g = highHalf(rg);
+ return vec4(r, g, 0.0f, 1.0f);
+}
+
+// Samples R16 texture with linear filtering and returns results packed as
+// signed I16. One bit of precision is shifted away from the bottom end to
+// accommodate the sign bit, so only 15 bits of precision is left.
+template <typename S>
+static inline I16 textureLinearUnpackedR16(S sampler, ivec2 i) {
+ assert(sampler->format == TextureFormat::R16);
+
+ ivec2 frac = i;
+ i >>= 7;
+
+ I32 row0 = computeRow(sampler, i);
+ I32 row1 = row0 + computeNextRowOffset(sampler, i);
+
+ I16 fracx =
+ CONVERT(
+ ((frac.x & (i.x >= 0)) | (i.x > int32_t(sampler->width) - 2)) & 0x7F,
+ I16)
+ << 8;
+ I16 fracy = computeFracY(frac) << 8;
+
+ // Sample the 16 bit data for both rows
+ uint16_t* buf = (uint16_t*)sampler->buf;
+ auto a0 = unaligned_load<V2<uint16_t>>(&buf[row0.x]);
+ auto b0 = unaligned_load<V2<uint16_t>>(&buf[row0.y]);
+ auto c0 = unaligned_load<V2<uint16_t>>(&buf[row0.z]);
+ auto d0 = unaligned_load<V2<uint16_t>>(&buf[row0.w]);
+ auto abcd0 = CONVERT(combine(a0, b0, c0, d0) >> 1, V8<int16_t>);
+
+ auto a1 = unaligned_load<V2<uint16_t>>(&buf[row1.x]);
+ auto b1 = unaligned_load<V2<uint16_t>>(&buf[row1.y]);
+ auto c1 = unaligned_load<V2<uint16_t>>(&buf[row1.z]);
+ auto d1 = unaligned_load<V2<uint16_t>>(&buf[row1.w]);
+ auto abcd1 = CONVERT(combine(a1, b1, c1, d1) >> 1, V8<int16_t>);
+
+ // The samples occupy 15 bits and the fraction occupies 15 bits, so that when
+ // they are multiplied together, the new scaled sample will fit in the high
+ // 14 bits of the result. It is left shifted once to make it 15 bits again
+ // for the final multiply.
+#if USE_SSE2
+ abcd0 += bit_cast<V8<int16_t>>(_mm_mulhi_epi16(abcd1 - abcd0, fracy.xxyyzzww))
+ << 1;
+#elif USE_NEON
+ // NEON has a convenient instruction that does both the multiply and the
+ // doubling, so doesn't need an extra shift.
+ abcd0 += bit_cast<V8<int16_t>>(vqrdmulhq_s16(abcd1 - abcd0, fracy.xxyyzzww));
+#else
+ abcd0 += CONVERT((CONVERT(abcd1 - abcd0, V8<int32_t>) *
+ CONVERT(fracy.xxyyzzww, V8<int32_t>)) >>
+ 16,
+ V8<int16_t>)
+ << 1;
+#endif
+
+ abcd0 = SHUFFLE(abcd0, abcd0, 0, 2, 4, 6, 1, 3, 5, 7);
+ auto abcdl = lowHalf(abcd0);
+ auto abcdh = highHalf(abcd0);
+#if USE_SSE2
+ abcdl += lowHalf(bit_cast<V8<int16_t>>(
+ _mm_mulhi_epi16(expand(abcdh - abcdl), expand(fracx))))
+ << 1;
+#elif USE_NEON
+ abcdl += bit_cast<V4<int16_t>>(vqrdmulh_s16(abcdh - abcdl, fracx));
+#else
+ abcdl += CONVERT((CONVERT(abcdh - abcdl, V4<int32_t>) *
+ CONVERT(fracx, V4<int32_t>)) >>
+ 16,
+ V4<int16_t>)
+ << 1;
+#endif
+
+ return abcdl;
+}
+
+template <typename S>
+vec4 textureLinearR16(S sampler, vec2 P) {
+ assert(sampler->format == TextureFormat::R16);
+
+ ivec2 i(linearQuantize(P, 128, sampler));
+ Float r = CONVERT(textureLinearUnpackedR16(sampler, i), Float);
+ return vec4(r * (1.0f / 32767.0f), 0.0f, 0.0f, 1.0f);
+}
+
+// Samples RG16 texture with linear filtering and returns results packed as
+// signed I16. One bit of precision is shifted away from the bottom end to
+// accommodate the sign bit, so only 15 bits of precision is left.
+template <typename S>
+static inline V8<int16_t> textureLinearUnpackedRG16(S sampler, ivec2 i) {
+ assert(sampler->format == TextureFormat::RG16);
+
+ ivec2 frac = i;
+ i >>= 7;
+
+ I32 row0 = computeRow(sampler, i);
+ I32 row1 = row0 + computeNextRowOffset(sampler, i);
+
+ I16 fracx =
+ CONVERT(
+ ((frac.x & (i.x >= 0)) | (i.x > int32_t(sampler->width) - 2)) & 0x7F,
+ I16)
+ << 8;
+ I16 fracy = computeFracY(frac) << 8;
+
+ // Sample the 2x16 bit data for both rows
+ auto a0 = unaligned_load<V4<uint16_t>>(&sampler->buf[row0.x]);
+ auto b0 = unaligned_load<V4<uint16_t>>(&sampler->buf[row0.y]);
+ auto ab0 = CONVERT(combine(a0, b0) >> 1, V8<int16_t>);
+ auto c0 = unaligned_load<V4<uint16_t>>(&sampler->buf[row0.z]);
+ auto d0 = unaligned_load<V4<uint16_t>>(&sampler->buf[row0.w]);
+ auto cd0 = CONVERT(combine(c0, d0) >> 1, V8<int16_t>);
+
+ auto a1 = unaligned_load<V4<uint16_t>>(&sampler->buf[row1.x]);
+ auto b1 = unaligned_load<V4<uint16_t>>(&sampler->buf[row1.y]);
+ auto ab1 = CONVERT(combine(a1, b1) >> 1, V8<int16_t>);
+ auto c1 = unaligned_load<V4<uint16_t>>(&sampler->buf[row1.z]);
+ auto d1 = unaligned_load<V4<uint16_t>>(&sampler->buf[row1.w]);
+ auto cd1 = CONVERT(combine(c1, d1) >> 1, V8<int16_t>);
+
+ // The samples occupy 15 bits and the fraction occupies 15 bits, so that when
+ // they are multiplied together, the new scaled sample will fit in the high
+ // 14 bits of the result. It is left shifted once to make it 15 bits again
+ // for the final multiply.
+#if USE_SSE2
+ ab0 += bit_cast<V8<int16_t>>(_mm_mulhi_epi16(ab1 - ab0, fracy.xxxxyyyy)) << 1;
+ cd0 += bit_cast<V8<int16_t>>(_mm_mulhi_epi16(cd1 - cd0, fracy.zzzzwwww)) << 1;
+#elif USE_NEON
+ // NEON has a convenient instruction that does both the multiply and the
+ // doubling, so doesn't need an extra shift.
+ ab0 += bit_cast<V8<int16_t>>(vqrdmulhq_s16(ab1 - ab0, fracy.xxxxyyyy));
+ cd0 += bit_cast<V8<int16_t>>(vqrdmulhq_s16(cd1 - cd0, fracy.zzzzwwww));
+#else
+ ab0 += CONVERT((CONVERT(ab1 - ab0, V8<int32_t>) *
+ CONVERT(fracy.xxxxyyyy, V8<int32_t>)) >>
+ 16,
+ V8<int16_t>)
+ << 1;
+ cd0 += CONVERT((CONVERT(cd1 - cd0, V8<int32_t>) *
+ CONVERT(fracy.zzzzwwww, V8<int32_t>)) >>
+ 16,
+ V8<int16_t>)
+ << 1;
+#endif
+
+ // ab = a.rgRG,b.rgRG
+ // cd = c.rgRG,d.rgRG
+ // ... ac = a.rg,c.rg,a.RG,c.RG
+ // ... bd = b.rg,d.rg,b.RG,d.RG
+ auto ac = zip2Low(ab0, cd0);
+ auto bd = zip2High(ab0, cd0);
+ // a.rg,b.rg,c.rg,d.rg
+ // a.RG,b.RG,c.RG,d.RG
+ auto abcdl = zip2Low(ac, bd);
+ auto abcdh = zip2High(ac, bd);
+ // Blend columns
+#if USE_SSE2
+ abcdl += bit_cast<V8<int16_t>>(_mm_mulhi_epi16(abcdh - abcdl, fracx.xxyyzzww))
+ << 1;
+#elif USE_NEON
+ abcdl += bit_cast<V8<int16_t>>(vqrdmulhq_s16(abcdh - abcdl, fracx.xxyyzzww));
+#else
+ abcdl += CONVERT((CONVERT(abcdh - abcdl, V8<int32_t>) *
+ CONVERT(fracx.xxyyzzww, V8<int32_t>)) >>
+ 16,
+ V8<int16_t>)
+ << 1;
+#endif
+
+ return abcdl;
+}
+
+template <typename S>
+vec4 textureLinearRG16(S sampler, vec2 P) {
+ assert(sampler->format == TextureFormat::RG16);
+
+ ivec2 i(linearQuantize(P, 128, sampler));
+ auto rg = bit_cast<V4<int32_t>>(textureLinearUnpackedRG16(sampler, i));
+ auto r = cast(rg & 0xFFFF) * (1.0f / 32767.0f);
+ auto g = cast(rg >> 16) * (1.0f / 32767.0f);
+ return vec4(r, g, 0.0f, 1.0f);
+}
+
+using PackedRGBA32F = V16<float>;
+using WideRGBA32F = V16<float>;
+
+template <typename S>
+vec4 textureLinearRGBA32F(S sampler, vec2 P) {
+ assert(sampler->format == TextureFormat::RGBA32F);
+ P = samplerScale(sampler, P);
+ P -= 0.5f;
+ vec2 f = floor(P);
+ vec2 r = P - f;
+ ivec2 i(f);
+ ivec2 c(clampCoord(i.x, sampler->width - 1),
+ clampCoord(i.y, sampler->height));
+ r.x = if_then_else(i.x >= 0, if_then_else(i.x < sampler->width - 1, r.x, 1.0),
+ 0.0f);
+ I32 offset0 = c.x * 4 + c.y * sampler->stride;
+ I32 offset1 = offset0 + computeNextRowOffset(sampler, i);
+
+ Float c0 = mix(mix(*(Float*)&sampler->buf[offset0.x],
+ *(Float*)&sampler->buf[offset0.x + 4], r.x),
+ mix(*(Float*)&sampler->buf[offset1.x],
+ *(Float*)&sampler->buf[offset1.x + 4], r.x),
+ r.y);
+ Float c1 = mix(mix(*(Float*)&sampler->buf[offset0.y],
+ *(Float*)&sampler->buf[offset0.y + 4], r.x),
+ mix(*(Float*)&sampler->buf[offset1.y],
+ *(Float*)&sampler->buf[offset1.y + 4], r.x),
+ r.y);
+ Float c2 = mix(mix(*(Float*)&sampler->buf[offset0.z],
+ *(Float*)&sampler->buf[offset0.z + 4], r.x),
+ mix(*(Float*)&sampler->buf[offset1.z],
+ *(Float*)&sampler->buf[offset1.z + 4], r.x),
+ r.y);
+ Float c3 = mix(mix(*(Float*)&sampler->buf[offset0.w],
+ *(Float*)&sampler->buf[offset0.w + 4], r.x),
+ mix(*(Float*)&sampler->buf[offset1.w],
+ *(Float*)&sampler->buf[offset1.w + 4], r.x),
+ r.y);
+ return pixel_float_to_vec4(c0, c1, c2, c3);
+}
+
+struct WidePlanarYUV8 {
+ U16 y;
+ U16 u;
+ U16 v;
+};
+
+template <typename S>
+SI WidePlanarYUV8 textureLinearPlanarYUV422(S sampler, ivec2 i) {
+ assert(sampler->format == TextureFormat::YUV422);
+
+ ivec2 frac = i;
+ i >>= 7;
+
+ I32 row0 = computeRow(sampler, i, 2);
+ // Layout is 2 pixel chunks (occupying 4 bytes) organized as: G0, B, G1, R.
+ // Get the selector for the pixel within the chunk.
+ I32 selector = row0 & 1;
+ // Align the row index to the chunk.
+ row0 &= ~1;
+ I32 row1 = row0 + computeNextRowOffset(sampler, i);
+ // G only needs to be clamped to a pixel boundary for safe interpolation,
+ // whereas the BR fraction needs to be clamped 1 extra pixel inside to a chunk
+ // boundary.
+ frac.x &= (i.x >= 0);
+ auto fracx =
+ CONVERT(combine(frac.x | (i.x > int32_t(sampler->width) - 3),
+ (frac.x >> 1) | (i.x > int32_t(sampler->width) - 3)) &
+ 0x7F,
+ V8<int16_t>);
+ I16 fracy = computeFracY(frac);
+
+ uint16_t* buf = (uint16_t*)sampler->buf;
+
+ // Load bytes for two adjacent chunks - g0,b,g1,r,G0,B,G1,R
+ // We always need to interpolate between (b,r) and (B,R).
+ // Depending on selector we need to either interpolate between g0 and g1
+ // or between g1 and G0. So for now we just interpolate both cases for g
+ // and will select the appropriate one on output.
+ auto a0 = CONVERT(unaligned_load<V8<uint8_t>>(&buf[row0.x]), V8<int16_t>);
+ auto a1 = CONVERT(unaligned_load<V8<uint8_t>>(&buf[row1.x]), V8<int16_t>);
+ // Combine with next row.
+ a0 += ((a1 - a0) * fracy.x) >> 7;
+
+ auto b0 = CONVERT(unaligned_load<V8<uint8_t>>(&buf[row0.y]), V8<int16_t>);
+ auto b1 = CONVERT(unaligned_load<V8<uint8_t>>(&buf[row1.y]), V8<int16_t>);
+ b0 += ((b1 - b0) * fracy.y) >> 7;
+
+ auto c0 = CONVERT(unaligned_load<V8<uint8_t>>(&buf[row0.z]), V8<int16_t>);
+ auto c1 = CONVERT(unaligned_load<V8<uint8_t>>(&buf[row1.z]), V8<int16_t>);
+ c0 += ((c1 - c0) * fracy.z) >> 7;
+
+ auto d0 = CONVERT(unaligned_load<V8<uint8_t>>(&buf[row0.w]), V8<int16_t>);
+ auto d1 = CONVERT(unaligned_load<V8<uint8_t>>(&buf[row1.w]), V8<int16_t>);
+ d0 += ((d1 - d0) * fracy.w) >> 7;
+
+ // Shuffle things around so we end up with g0,g0,g0,g0,b,b,b,b and
+ // g1,g1,g1,g1,r,r,r,r.
+ auto abl = zipLow(a0, b0);
+ auto cdl = zipLow(c0, d0);
+ auto g0b = zip2Low(abl, cdl);
+ auto g1r = zip2High(abl, cdl);
+
+ // Need to zip g1,B,G0,R. Instead of using a bunch of complicated masks and
+ // and shifts, just shuffle here instead... We finally end up with
+ // g1,g1,g1,g1,B,B,B,B and G0,G0,G0,G0,R,R,R,R.
+ auto abh = SHUFFLE(a0, b0, 2, 10, 5, 13, 4, 12, 7, 15);
+ auto cdh = SHUFFLE(c0, d0, 2, 10, 5, 13, 4, 12, 7, 15);
+ auto g1B = zip2Low(abh, cdh);
+ auto G0R = zip2High(abh, cdh);
+
+ // Finally interpolate between adjacent columns.
+ g0b += ((g1B - g0b) * fracx) >> 7;
+ g1r += ((G0R - g1r) * fracx) >> 7;
+
+ // Choose either g0 or g1 based on selector.
+ return WidePlanarYUV8{
+ U16(if_then_else(CONVERT(-selector, I16), lowHalf(g1r), lowHalf(g0b))),
+ U16(highHalf(g0b)), U16(highHalf(g1r))};
+}
+
+template <typename S>
+vec4 textureLinearYUV422(S sampler, vec2 P) {
+ ivec2 i(linearQuantize(P, 128, sampler));
+ auto planar = textureLinearPlanarYUV422(sampler, i);
+ auto y = CONVERT(planar.y, Float) * (1.0f / 255.0f);
+ auto u = CONVERT(planar.u, Float) * (1.0f / 255.0f);
+ auto v = CONVERT(planar.v, Float) * (1.0f / 255.0f);
+ return vec4(v, y, u, 1.0f);
+}
+
+SI vec4 texture(sampler2D sampler, vec2 P) {
+ if (sampler->filter == TextureFilter::LINEAR) {
+ switch (sampler->format) {
+ case TextureFormat::RGBA32F:
+ return textureLinearRGBA32F(sampler, P);
+ case TextureFormat::RGBA8:
+ return textureLinearRGBA8(sampler, P);
+ case TextureFormat::R8:
+ return textureLinearR8(sampler, P);
+ case TextureFormat::RG8:
+ return textureLinearRG8(sampler, P);
+ case TextureFormat::R16:
+ return textureLinearR16(sampler, P);
+ case TextureFormat::RG16:
+ return textureLinearRG16(sampler, P);
+ case TextureFormat::YUV422:
+ return textureLinearYUV422(sampler, P);
+ default:
+ assert(false);
+ return vec4();
+ }
+ } else {
+ ivec2 coord(roundzero(P.x, sampler->width),
+ roundzero(P.y, sampler->height));
+ return texelFetch(sampler, coord, 0);
+ }
+}
+
+vec4 texture(sampler2DRect sampler, vec2 P) {
+ if (sampler->filter == TextureFilter::LINEAR) {
+ switch (sampler->format) {
+ case TextureFormat::RGBA8:
+ return textureLinearRGBA8(sampler, P);
+ case TextureFormat::R8:
+ return textureLinearR8(sampler, P);
+ case TextureFormat::RG8:
+ return textureLinearRG8(sampler, P);
+ case TextureFormat::R16:
+ return textureLinearR16(sampler, P);
+ case TextureFormat::RG16:
+ return textureLinearRG16(sampler, P);
+ case TextureFormat::YUV422:
+ return textureLinearYUV422(sampler, P);
+ default:
+ assert(false);
+ return vec4();
+ }
+ } else {
+ ivec2 coord(roundzero(P.x, 1.0f), roundzero(P.y, 1.0f));
+ return texelFetch(sampler, coord);
+ }
+}
+
+template <typename S>
+vec4_scalar texture(S sampler, vec2_scalar P) {
+ return force_scalar(texture(sampler, vec2(P)));
+}
+
+ivec2_scalar textureSize(sampler2D sampler, int) {
+ return ivec2_scalar{int32_t(sampler->width), int32_t(sampler->height)};
+}
+
+ivec2_scalar textureSize(sampler2DRect sampler) {
+ return ivec2_scalar{int32_t(sampler->width), int32_t(sampler->height)};
+}
+
+template <typename S>
+static WideRGBA8 textureLinearUnpackedRGBA8(S sampler, ivec2 i) {
+ assert(sampler->format == TextureFormat::RGBA8);
+ ivec2 frac = i;
+ i >>= 7;
+
+ I32 row0 = computeRow(sampler, i);
+ I32 row1 = row0 + computeNextRowOffset(sampler, i);
+ I16 fracx = computeFracX(sampler, i, frac);
+ I16 fracy = computeFracY(frac);
+
+ auto a0 =
+ CONVERT(unaligned_load<V8<uint8_t>>(&sampler->buf[row0.x]), V8<int16_t>);
+ auto a1 =
+ CONVERT(unaligned_load<V8<uint8_t>>(&sampler->buf[row1.x]), V8<int16_t>);
+ a0 += ((a1 - a0) * fracy.x) >> 7;
+
+ auto b0 =
+ CONVERT(unaligned_load<V8<uint8_t>>(&sampler->buf[row0.y]), V8<int16_t>);
+ auto b1 =
+ CONVERT(unaligned_load<V8<uint8_t>>(&sampler->buf[row1.y]), V8<int16_t>);
+ b0 += ((b1 - b0) * fracy.y) >> 7;
+
+ auto abl = combine(lowHalf(a0), lowHalf(b0));
+ auto abh = combine(highHalf(a0), highHalf(b0));
+ abl += ((abh - abl) * fracx.xxxxyyyy) >> 7;
+
+ auto c0 =
+ CONVERT(unaligned_load<V8<uint8_t>>(&sampler->buf[row0.z]), V8<int16_t>);
+ auto c1 =
+ CONVERT(unaligned_load<V8<uint8_t>>(&sampler->buf[row1.z]), V8<int16_t>);
+ c0 += ((c1 - c0) * fracy.z) >> 7;
+
+ auto d0 =
+ CONVERT(unaligned_load<V8<uint8_t>>(&sampler->buf[row0.w]), V8<int16_t>);
+ auto d1 =
+ CONVERT(unaligned_load<V8<uint8_t>>(&sampler->buf[row1.w]), V8<int16_t>);
+ d0 += ((d1 - d0) * fracy.w) >> 7;
+
+ auto cdl = combine(lowHalf(c0), lowHalf(d0));
+ auto cdh = combine(highHalf(c0), highHalf(d0));
+ cdl += ((cdh - cdl) * fracx.zzzzwwww) >> 7;
+
+ return combine(HalfRGBA8(abl), HalfRGBA8(cdl));
+}
+
+template <typename S>
+static PackedRGBA8 textureLinearPackedRGBA8(S sampler, ivec2 i) {
+ return pack(textureLinearUnpackedRGBA8(sampler, i));
+}
+
+template <typename S>
+static PackedRGBA8 textureNearestPackedRGBA8(S sampler, ivec2 i) {
+ assert(sampler->format == TextureFormat::RGBA8);
+ I32 row = computeRow(sampler, i, 0);
+ return combine(unaligned_load<V4<uint8_t>>(&sampler->buf[row.x]),
+ unaligned_load<V4<uint8_t>>(&sampler->buf[row.y]),
+ unaligned_load<V4<uint8_t>>(&sampler->buf[row.z]),
+ unaligned_load<V4<uint8_t>>(&sampler->buf[row.w]));
+}
+
+template <typename S>
+static PackedR8 textureLinearPackedR8(S sampler, ivec2 i) {
+ return pack(textureLinearUnpackedR8(sampler, i));
+}
+
+template <typename S>
+static WideRG8 textureLinearUnpackedRG8(S sampler, ivec2 i) {
+ assert(sampler->format == TextureFormat::RG8);
+ ivec2 frac = i & 0x7F;
+ i >>= 7;
+
+ I32 row0 = computeRow(sampler, i);
+ I32 row1 = row0 + computeNextRowOffset(sampler, i);
+ I16 fracx = computeFracX(sampler, i, frac);
+ I16 fracy = computeFracY(frac);
+
+ uint16_t* buf = (uint16_t*)sampler->buf;
+
+ // Load RG bytes for two adjacent pixels - rgRG
+ auto a0 = unaligned_load<V4<uint8_t>>(&buf[row0.x]);
+ auto b0 = unaligned_load<V4<uint8_t>>(&buf[row0.y]);
+ auto ab0 = CONVERT(combine(a0, b0), V8<int16_t>);
+ // Load two pixels for next row
+ auto a1 = unaligned_load<V4<uint8_t>>(&buf[row1.x]);
+ auto b1 = unaligned_load<V4<uint8_t>>(&buf[row1.y]);
+ auto ab1 = CONVERT(combine(a1, b1), V8<int16_t>);
+ // Blend rows
+ ab0 += ((ab1 - ab0) * fracy.xxxxyyyy) >> 7;
+
+ auto c0 = unaligned_load<V4<uint8_t>>(&buf[row0.z]);
+ auto d0 = unaligned_load<V4<uint8_t>>(&buf[row0.w]);
+ auto cd0 = CONVERT(combine(c0, d0), V8<int16_t>);
+ auto c1 = unaligned_load<V4<uint8_t>>(&buf[row1.z]);
+ auto d1 = unaligned_load<V4<uint8_t>>(&buf[row1.w]);
+ auto cd1 = CONVERT(combine(c1, d1), V8<int16_t>);
+ // Blend rows
+ cd0 += ((cd1 - cd0) * fracy.zzzzwwww) >> 7;
+
+ // ab = a.rgRG,b.rgRG
+ // cd = c.rgRG,d.rgRG
+ // ... ac = a.rg,c.rg,a.RG,c.RG
+ // ... bd = b.rg,d.rg,b.RG,d.RG
+ auto ac = zip2Low(ab0, cd0);
+ auto bd = zip2High(ab0, cd0);
+ // a.rg,b.rg,c.rg,d.rg
+ // a.RG,b.RG,c.RG,d.RG
+ auto abcdl = zip2Low(ac, bd);
+ auto abcdh = zip2High(ac, bd);
+ // Blend columns
+ abcdl += ((abcdh - abcdl) * fracx.xxyyzzww) >> 7;
+
+ return WideRG8(abcdl);
+}
+
+template <typename S>
+static PackedRG8 textureLinearPackedRG8(S sampler, ivec2 i) {
+ return pack(textureLinearUnpackedRG8(sampler, i));
+}
+
+template <int N>
+static ALWAYS_INLINE VectorType<uint16_t, N> addsat(VectorType<uint16_t, N> x,
+ VectorType<uint16_t, N> y) {
+ auto r = x + y;
+ return r | (r < x);
+}
+
+template <typename P, typename S>
+static VectorType<uint16_t, 4 * sizeof(P)> gaussianBlurHorizontal(
+ S sampler, const ivec2_scalar& i, int minX, int maxX, int radius,
+ float coeff, float coeffStep) {
+ // Packed and unpacked vectors for a chunk of the given pixel type.
+ typedef VectorType<uint8_t, 4 * sizeof(P)> packed_type;
+ typedef VectorType<uint16_t, 4 * sizeof(P)> unpacked_type;
+
+ // Pre-scale the coefficient by 8 bits of fractional precision, so that when
+ // the sample is multiplied by it, it will yield a 16 bit unsigned integer
+ // that will use all 16 bits of precision to accumulate the sum.
+ coeff *= 1 << 8;
+ float coeffStep2 = coeffStep * coeffStep;
+
+ int row = computeRow(sampler, i);
+ P* buf = (P*)sampler->buf;
+ auto pixelsRight = unaligned_load<V4<P>>(&buf[row]);
+ auto pixelsLeft = pixelsRight;
+ auto sum = CONVERT(bit_cast<packed_type>(pixelsRight), unpacked_type) *
+ uint16_t(coeff + 0.5f);
+
+ // Here we use some trickery to reuse the pixels within a chunk, shifted over
+ // by one pixel, to get the next sample for the entire chunk. This allows us
+ // to sample only one pixel for each offset across the entire chunk in both
+ // the left and right directions. To avoid clamping within the loop to the
+ // texture bounds, we compute the valid radius that doesn't require clamping
+ // and fall back to a slower clamping loop outside of that valid radius.
+ int offset = 1;
+ // The left bound is how much we can offset the sample before the start of
+ // the row bounds.
+ int leftBound = i.x - max(minX, 0);
+ // The right bound is how much we can offset the sample before the end of the
+ // row bounds.
+ int rightBound = min(maxX, sampler->width - 1) - i.x;
+ int validRadius = min(radius, min(leftBound, rightBound - (4 - 1)));
+ for (; offset <= validRadius; offset++) {
+ // Overwrite the pixel that needs to be shifted out with the new pixel, and
+ // shift it into the correct location.
+ pixelsRight.x = unaligned_load<P>(&buf[row + offset + 4 - 1]);
+ pixelsRight = pixelsRight.yzwx;
+ pixelsLeft = pixelsLeft.wxyz;
+ pixelsLeft.x = unaligned_load<P>(&buf[row - offset]);
+
+ // Accumulate the Gaussian coefficients step-wise.
+ coeff *= coeffStep;
+ coeffStep *= coeffStep2;
+
+ // Both left and right samples at this offset use the same coefficient.
+ sum = addsat(sum,
+ (CONVERT(bit_cast<packed_type>(pixelsRight), unpacked_type) +
+ CONVERT(bit_cast<packed_type>(pixelsLeft), unpacked_type)) *
+ uint16_t(coeff + 0.5f));
+ }
+
+ for (; offset <= radius; offset++) {
+ pixelsRight.x =
+ unaligned_load<P>(&buf[row + min(offset + 4 - 1, rightBound)]);
+ pixelsRight = pixelsRight.yzwx;
+ pixelsLeft = pixelsLeft.wxyz;
+ pixelsLeft.x = unaligned_load<P>(&buf[row - min(offset, leftBound)]);
+
+ coeff *= coeffStep;
+ coeffStep *= coeffStep2;
+
+ sum = addsat(sum,
+ (CONVERT(bit_cast<packed_type>(pixelsRight), unpacked_type) +
+ CONVERT(bit_cast<packed_type>(pixelsLeft), unpacked_type)) *
+ uint16_t(coeff + 0.5f));
+ }
+
+ // Shift away the intermediate precision.
+ return sum >> 8;
+}
+
+template <typename P, typename S>
+static VectorType<uint16_t, 4 * sizeof(P)> gaussianBlurVertical(
+ S sampler, const ivec2_scalar& i, int minY, int maxY, int radius,
+ float coeff, float coeffStep) {
+ // Packed and unpacked vectors for a chunk of the given pixel type.
+ typedef VectorType<uint8_t, 4 * sizeof(P)> packed_type;
+ typedef VectorType<uint16_t, 4 * sizeof(P)> unpacked_type;
+
+ // Pre-scale the coefficient by 8 bits of fractional precision, so that when
+ // the sample is multiplied by it, it will yield a 16 bit unsigned integer
+ // that will use all 16 bits of precision to accumulate the sum.
+ coeff *= 1 << 8;
+ float coeffStep2 = coeffStep * coeffStep;
+
+ int rowAbove = computeRow(sampler, i);
+ int rowBelow = rowAbove;
+ P* buf = (P*)sampler->buf;
+ auto pixels = unaligned_load<V4<P>>(&buf[rowAbove]);
+ auto sum = CONVERT(bit_cast<packed_type>(pixels), unpacked_type) *
+ uint16_t(coeff + 0.5f);
+
+ // For the vertical loop we can't be quite as creative with reusing old values
+ // as we were in the horizontal loop. We just do the obvious implementation of
+ // loading a chunk from each row in turn and accumulating it into the sum. We
+ // compute a valid radius within which we don't need to clamp the sampled row
+ // and use that to avoid any clamping in the main inner loop. We fall back to
+ // a slower clamping loop outside of that valid radius.
+ int offset = 1;
+ int belowBound = i.y - max(minY, 0);
+ int aboveBound = min(maxY, sampler->height - 1) - i.y;
+ int validRadius = min(radius, min(belowBound, aboveBound));
+ for (; offset <= validRadius; offset++) {
+ rowAbove += sampler->stride;
+ rowBelow -= sampler->stride;
+ auto pixelsAbove = unaligned_load<V4<P>>(&buf[rowAbove]);
+ auto pixelsBelow = unaligned_load<V4<P>>(&buf[rowBelow]);
+
+ // Accumulate the Gaussian coefficients step-wise.
+ coeff *= coeffStep;
+ coeffStep *= coeffStep2;
+
+ // Both above and below samples at this offset use the same coefficient.
+ sum = addsat(sum,
+ (CONVERT(bit_cast<packed_type>(pixelsAbove), unpacked_type) +
+ CONVERT(bit_cast<packed_type>(pixelsBelow), unpacked_type)) *
+ uint16_t(coeff + 0.5f));
+ }
+
+ for (; offset <= radius; offset++) {
+ if (offset <= aboveBound) {
+ rowAbove += sampler->stride;
+ }
+ if (offset <= belowBound) {
+ rowBelow -= sampler->stride;
+ }
+ auto pixelsAbove = unaligned_load<V4<P>>(&buf[rowAbove]);
+ auto pixelsBelow = unaligned_load<V4<P>>(&buf[rowBelow]);
+
+ coeff *= coeffStep;
+ coeffStep *= coeffStep2;
+
+ sum = addsat(sum,
+ (CONVERT(bit_cast<packed_type>(pixelsAbove), unpacked_type) +
+ CONVERT(bit_cast<packed_type>(pixelsBelow), unpacked_type)) *
+ uint16_t(coeff + 0.5f));
+ }
+
+ // Shift away the intermediate precision.
+ return sum >> 8;
+}
+
+} // namespace glsl
diff --git a/gfx/wr/swgl/src/vector_type.h b/gfx/wr/swgl/src/vector_type.h
new file mode 100644
index 0000000000..43364ffcce
--- /dev/null
+++ b/gfx/wr/swgl/src/vector_type.h
@@ -0,0 +1,563 @@
+/* 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/. */
+
+#ifdef __clang__
+# ifdef __SSE2__
+# include <xmmintrin.h>
+# define USE_SSE2 1
+# endif
+# ifdef __ARM_NEON
+# include <arm_neon.h>
+# define USE_NEON 1
+# endif
+#endif
+
+namespace glsl {
+
+#ifdef __clang__
+template <typename T, int N>
+using VectorType = T __attribute__((ext_vector_type(N)));
+
+# define CONVERT(vector, type) __builtin_convertvector(vector, type)
+# define SHUFFLE(a, b, ...) __builtin_shufflevector(a, b, __VA_ARGS__)
+
+template <typename T>
+SI VectorType<T, 4> combine(VectorType<T, 2> a, VectorType<T, 2> b) {
+ return __builtin_shufflevector(a, b, 0, 1, 2, 3);
+}
+
+template <typename T>
+SI VectorType<T, 8> combine(VectorType<T, 4> a, VectorType<T, 4> b) {
+ return __builtin_shufflevector(a, b, 0, 1, 2, 3, 4, 5, 6, 7);
+}
+
+template <typename T>
+SI VectorType<T, 16> combine(VectorType<T, 8> a, VectorType<T, 8> b) {
+ return __builtin_shufflevector(a, b, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
+ 13, 14, 15);
+}
+
+template <typename T>
+SI VectorType<T, 2> lowHalf(VectorType<T, 4> a) {
+ return __builtin_shufflevector(a, a, 0, 1);
+}
+
+template <typename T>
+SI VectorType<T, 2> highHalf(VectorType<T, 4> a) {
+ return __builtin_shufflevector(a, a, 2, 3);
+}
+
+template <typename T>
+SI VectorType<T, 4> lowHalf(VectorType<T, 8> a) {
+ return __builtin_shufflevector(a, a, 0, 1, 2, 3);
+}
+
+template <typename T>
+SI VectorType<T, 4> highHalf(VectorType<T, 8> a) {
+ return __builtin_shufflevector(a, a, 4, 5, 6, 7);
+}
+
+template <typename T>
+SI VectorType<T, 8> lowHalf(VectorType<T, 16> a) {
+ return __builtin_shufflevector(a, a, 0, 1, 2, 3, 4, 5, 6, 7);
+}
+
+template <typename T>
+SI VectorType<T, 8> highHalf(VectorType<T, 16> a) {
+ return __builtin_shufflevector(a, a, 8, 9, 10, 11, 12, 13, 14, 15);
+}
+
+template <typename T>
+SI VectorType<T, 8> expand(VectorType<T, 4> a) {
+ return __builtin_shufflevector(a, a, 0, 1, 2, 3, -1, -1, -1, -1);
+}
+#else
+template <typename T>
+struct VectorMask {
+ typedef T type;
+};
+template <>
+struct VectorMask<uint32_t> {
+ typedef int32_t type;
+};
+template <>
+struct VectorMask<uint16_t> {
+ typedef int16_t type;
+};
+template <>
+struct VectorMask<uint8_t> {
+ typedef int8_t type;
+};
+template <>
+struct VectorMask<float> {
+ typedef int type;
+};
+
+template <typename T, int N>
+struct VectorType {
+ enum { SIZE = N };
+
+ typedef T data_type __attribute__((vector_size(sizeof(T) * N)));
+ typedef typename VectorMask<T>::type mask_index;
+ typedef mask_index mask_type
+ __attribute__((vector_size(sizeof(mask_index) * N)));
+ typedef T half_type __attribute__((vector_size(sizeof(T) * (N / 2))));
+ union {
+ data_type data;
+ struct {
+ T x, y, z, w;
+ };
+ T elements[N];
+ struct {
+ half_type low_half, high_half;
+ };
+ };
+
+ VectorType() : data{0} {}
+
+ constexpr VectorType(const VectorType& rhs) : data(rhs.data) {}
+ // GCC vector extensions only support broadcasting scalars on arithmetic ops,
+ // but not on initializers, hence the following...
+ constexpr VectorType(T n) : data((data_type){0} + n) {}
+ constexpr VectorType(T a, T b, T c, T d) : data{a, b, c, d} {}
+ constexpr VectorType(T a, T b, T c, T d, T e, T f, T g, T h)
+ : data{a, b, c, d, e, f, g, h} {}
+ constexpr VectorType(T a, T b, T c, T d, T e, T f, T g, T h, T i, T j, T k,
+ T l, T m, T n, T o, T p)
+ : data{a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p} {}
+
+ SI VectorType wrap(const data_type& data) {
+ VectorType v;
+ v.data = data;
+ return v;
+ }
+
+ T& operator[](size_t i) { return elements[i]; }
+ T operator[](size_t i) const { return elements[i]; }
+
+ template <typename U>
+ operator VectorType<U, 2>() const {
+ return VectorType<U, 2>::wrap(
+ (typename VectorType<U, N>::data_type){U(x), U(y)});
+ }
+ template <typename U>
+ operator VectorType<U, 4>() const {
+ return VectorType<U, 4>::wrap(
+ (typename VectorType<U, N>::data_type){U(x), U(y), U(z), U(w)});
+ }
+ template <typename U>
+ operator VectorType<U, 8>() const {
+ return VectorType<U, 8>::wrap((typename VectorType<U, N>::data_type){
+ U(elements[0]), U(elements[1]), U(elements[2]), U(elements[3]),
+ U(elements[4]), U(elements[5]), U(elements[6]), U(elements[7])});
+ }
+ template <typename U>
+ operator VectorType<U, 16>() const {
+ return VectorType<U, 16>::wrap((typename VectorType<U, N>::data_type){
+ U(elements[0]),
+ U(elements[1]),
+ U(elements[2]),
+ U(elements[3]),
+ U(elements[4]),
+ U(elements[5]),
+ U(elements[6]),
+ U(elements[7]),
+ U(elements[8]),
+ U(elements[9]),
+ U(elements[10]),
+ U(elements[11]),
+ U(elements[12]),
+ U(elements[13]),
+ U(elements[14]),
+ U(elements[15]),
+ });
+ }
+
+ VectorType operator-() const { return wrap(-data); }
+ VectorType operator~() const { return wrap(~data); }
+
+ VectorType operator&(VectorType x) const { return wrap(data & x.data); }
+ VectorType operator&(T x) const { return wrap(data & x); }
+ VectorType operator|(VectorType x) const { return wrap(data | x.data); }
+ VectorType operator|(T x) const { return wrap(data | x); }
+ VectorType operator^(VectorType x) const { return wrap(data ^ x.data); }
+ VectorType operator^(T x) const { return wrap(data ^ x); }
+ VectorType operator<<(int x) const { return wrap(data << x); }
+ VectorType operator>>(int x) const { return wrap(data >> x); }
+ VectorType operator+(VectorType x) const { return wrap(data + x.data); }
+ VectorType operator+(T x) const { return wrap(data + x); }
+ friend VectorType operator+(T x, VectorType y) { return wrap(x + y.data); }
+ VectorType operator-(VectorType x) const { return wrap(data - x.data); }
+ VectorType operator-(T x) const { return wrap(data - x); }
+ friend VectorType operator-(T x, VectorType y) { return wrap(x - y.data); }
+ VectorType operator*(VectorType x) const { return wrap(data * x.data); }
+ VectorType operator*(T x) const { return wrap(data * x); }
+ friend VectorType operator*(T x, VectorType y) { return wrap(x * y.data); }
+ VectorType operator/(VectorType x) const { return wrap(data / x.data); }
+ VectorType operator/(T x) const { return wrap(data / x); }
+ friend VectorType operator/(T x, VectorType y) { return wrap(x / y.data); }
+ VectorType operator%(int x) const { return wrap(data % x); }
+
+ VectorType& operator&=(VectorType x) {
+ data &= x.data;
+ return *this;
+ }
+ VectorType& operator|=(VectorType x) {
+ data |= x.data;
+ return *this;
+ }
+ VectorType& operator^=(VectorType x) {
+ data ^= x.data;
+ return *this;
+ }
+ VectorType& operator<<=(int x) {
+ data <<= x;
+ return *this;
+ }
+ VectorType& operator>>=(int x) {
+ data >>= x;
+ return *this;
+ }
+ VectorType& operator+=(VectorType x) {
+ data += x.data;
+ return *this;
+ }
+ VectorType& operator-=(VectorType x) {
+ data -= x.data;
+ return *this;
+ }
+ VectorType& operator*=(VectorType x) {
+ data *= x.data;
+ return *this;
+ }
+ VectorType& operator/=(VectorType x) {
+ data /= x.data;
+ return *this;
+ }
+ VectorType& operator%=(int x) {
+ data %= x;
+ return *this;
+ }
+
+ VectorType<mask_type, N> operator==(VectorType x) const {
+ return VectorType<mask_type, N>::wrap(data == x.data);
+ }
+ VectorType<mask_type, N> operator!=(VectorType x) const {
+ return VectorType<mask_type, N>::wrap(data != x.data);
+ }
+ VectorType<mask_type, N> operator<(VectorType x) const {
+ return VectorType<mask_type, N>::wrap(data < x.data);
+ }
+ VectorType<mask_type, N> operator>(VectorType x) const {
+ return VectorType<mask_type, N>::wrap(data > x.data);
+ }
+ VectorType<mask_type, N> operator<=(VectorType x) const {
+ return VectorType<mask_type, N>::wrap(data <= x.data);
+ }
+ VectorType<mask_type, N> operator>=(VectorType x) const {
+ return VectorType<mask_type, N>::wrap(data >= x.data);
+ }
+
+ VectorType operator!() const { return wrap(!data); }
+ VectorType operator&&(VectorType x) const { return wrap(data & x.data); }
+ VectorType operator||(VectorType x) const { return wrap(data | x.data); }
+
+ VectorType& operator=(VectorType x) {
+ data = x.data;
+ return *this;
+ }
+
+ VectorType<T, 4> shuffle(VectorType b, mask_index x, mask_index y,
+ mask_index z, mask_index w) const {
+ return VectorType<T, 4>::wrap(__builtin_shuffle(
+ data, b.data, (typename VectorType<T, 4>::mask_type){x, y, z, w}));
+ }
+ VectorType<T, 8> shuffle(VectorType b, mask_index x, mask_index y,
+ mask_index z, mask_index w, mask_index s,
+ mask_index t, mask_index u, mask_index v) const {
+ return VectorType<T, 8>::wrap(__builtin_shuffle(
+ data, b.data,
+ (typename VectorType<T, 8>::mask_type){x, y, z, w, s, t, u, v}));
+ }
+ VectorType<T, 16> shuffle(VectorType b, mask_index x, mask_index y,
+ mask_index z, mask_index w, mask_index s,
+ mask_index t, mask_index u, mask_index v,
+ mask_index i, mask_index j, mask_index k,
+ mask_index l, mask_index m, mask_index n,
+ mask_index o, mask_index p) const {
+ return VectorType<T, 16>::wrap(
+ __builtin_shuffle(data, b.data,
+ (typename VectorType<T, 16>::mask_type){
+ x, y, z, w, s, t, u, v, i, j, k, l, m, n, o, p}));
+ }
+
+ VectorType<T, 4> swizzle(mask_index x, mask_index y, mask_index z,
+ mask_index w) const {
+ return VectorType<T, 4>::wrap(__builtin_shuffle(
+ data, (typename VectorType<T, 4>::mask_type){x, y, z, w}));
+ }
+ VectorType<T, 8> swizzle(mask_index x, mask_index y, mask_index z,
+ mask_index w, mask_index s, mask_index t,
+ mask_index u, mask_index v) const {
+ return VectorType<T, 8>::wrap(__builtin_shuffle(
+ data, (typename VectorType<T, 8>::mask_type){x, y, z, w, s, t, u, v}));
+ }
+
+ SI VectorType wrap(half_type low, half_type high) {
+ VectorType v;
+ v.low_half = low;
+ v.high_half = high;
+ return v;
+ }
+
+ VectorType<T, N * 2> combine(VectorType high) const {
+ return VectorType<T, N * 2>::wrap(data, high.data);
+ }
+
+# define xxxx swizzle(0, 0, 0, 0)
+# define yyyy swizzle(1, 1, 1, 1)
+# define zzzz swizzle(2, 2, 2, 2)
+# define wwww swizzle(3, 3, 3, 3)
+# define xxyy swizzle(0, 0, 1, 1)
+# define xxzz swizzle(0, 0, 2, 2)
+# define yyww swizzle(1, 1, 3, 3)
+# define zzww swizzle(2, 2, 3, 3)
+# define xyxy swizzle(0, 1, 0, 1)
+# define xzxz swizzle(0, 2, 0, 2)
+# define ywyw swizzle(1, 3, 1, 3)
+# define zwzw swizzle(2, 3, 2, 3)
+# define zwxy swizzle(2, 3, 0, 1)
+# define zyxw swizzle(2, 1, 0, 3)
+# define xxyz swizzle(0, 0, 1, 2)
+# define xyyz swizzle(0, 1, 1, 2)
+# define xyzz swizzle(0, 1, 2, 2)
+# define xzyw swizzle(0, 2, 1, 3)
+# define yzwx swizzle(1, 2, 3, 0)
+# define wxyz swizzle(3, 0, 1, 2)
+# define wzyx swizzle(3, 2, 1, 0)
+# define xxxxyyyy XXXXYYYY()
+ VectorType<T, 8> XXXXYYYY() const {
+ return swizzle(0, 0, 0, 0).combine(swizzle(1, 1, 1, 1));
+ }
+# define zzzzwwww ZZZZWWWW()
+ VectorType<T, 8> ZZZZWWWW() const {
+ return swizzle(2, 2, 2, 2).combine(swizzle(3, 3, 3, 3));
+ }
+# define xyzwxyzw XYZWXYZW()
+ VectorType<T, 8> XYZWXYZW() const { return combine(*this); }
+# define xyxyxyxy XYXYXYXY()
+ VectorType<T, 8> XYXYXYXY() const {
+ return swizzle(0, 1, 0, 1).combine(swizzle(0, 1, 0, 1));
+ }
+# define zwzwzwzw ZWZWZWZW()
+ VectorType<T, 8> ZWZWZWZW() const {
+ return swizzle(2, 3, 2, 3).combine(swizzle(2, 3, 2, 3));
+ }
+# define xxyyzzww XXYYZZWW()
+ VectorType<T, 8> XXYYZZWW() const {
+ return swizzle(0, 0, 1, 1).combine(swizzle(2, 2, 3, 3));
+ }
+# define xxxxyyyyzzzzwwww XXXXYYYYZZZZWWWW()
+ VectorType<T, 16> XXXXYYYYZZZZWWWW() {
+ return XXXXYYYY().combine(ZZZZWWWW());
+ }
+};
+
+template <typename T>
+struct VectorType<T, 2> {
+ typedef T data_type __attribute__((vector_size(sizeof(T) * 2)));
+ union {
+ data_type data;
+ struct {
+ T x, y;
+ };
+ T elements[2];
+ };
+
+ SI VectorType wrap(const data_type& data) {
+ VectorType v;
+ v.data = data;
+ return v;
+ }
+
+ VectorType operator&(VectorType x) const { return wrap(data & x.data); }
+ VectorType operator&(T x) const { return wrap(data & x); }
+ VectorType operator|(VectorType x) const { return wrap(data | x.data); }
+ VectorType operator|(T x) const { return wrap(data | x); }
+};
+
+# define CONVERT(vector, type) ((type)(vector))
+# define SHUFFLE(a, b, ...) a.shuffle(b, __VA_ARGS__)
+
+template <typename T, int N>
+SI VectorType<T, N * 2> combine(VectorType<T, N> a, VectorType<T, N> b) {
+ return VectorType<T, N * 2>::wrap(a.data, b.data);
+}
+
+template <typename T, int N>
+SI VectorType<T, N / 2> lowHalf(VectorType<T, N> a) {
+ return VectorType<T, N / 2>::wrap(a.low_half);
+}
+
+template <typename T, int N>
+SI VectorType<T, N / 2> highHalf(VectorType<T, N> a) {
+ return VectorType<T, N / 2>::wrap(a.high_half);
+}
+
+template <typename T, int N>
+SI VectorType<T, N * 2> expand(VectorType<T, N> a) {
+ return combine(a, a);
+}
+#endif
+
+template <typename T, int N>
+SI VectorType<T, N * 4> combine(VectorType<T, N> a, VectorType<T, N> b,
+ VectorType<T, N> c, VectorType<T, N> d) {
+ return combine(combine(a, b), combine(c, d));
+}
+
+template <typename T, int N>
+SI VectorType<T, N> combineLow(VectorType<T, N> a, VectorType<T, N> b) {
+ return combine(lowHalf(a), lowHalf(b));
+}
+
+template <typename T, int N>
+SI VectorType<T, N> combineHigh(VectorType<T, N> a, VectorType<T, N> b) {
+ return combine(highHalf(a), highHalf(b));
+}
+
+template <typename T, int N>
+SI VectorType<T, N * 2> repeat2(VectorType<T, N> a) {
+ return combine(a, a);
+}
+
+template <typename T, int N>
+SI VectorType<T, N * 4> repeat4(VectorType<T, N> a) {
+ return combine(a, a, a, a);
+}
+
+template <typename T>
+SI VectorType<T, 4> zipLow(VectorType<T, 4> a, VectorType<T, 4> b) {
+ return SHUFFLE(a, b, 0, 4, 1, 5);
+}
+
+template <typename T>
+SI VectorType<T, 4> zipHigh(VectorType<T, 4> a, VectorType<T, 4> b) {
+ return SHUFFLE(a, b, 2, 6, 3, 7);
+}
+
+template <typename T>
+SI VectorType<T, 8> zipLow(VectorType<T, 8> a, VectorType<T, 8> b) {
+ return SHUFFLE(a, b, 0, 8, 1, 9, 2, 10, 3, 11);
+}
+
+template <typename T>
+SI VectorType<T, 8> zipHigh(VectorType<T, 8> a, VectorType<T, 8> b) {
+ return SHUFFLE(a, b, 4, 12, 5, 13, 6, 14, 7, 15);
+}
+
+template <typename T>
+SI VectorType<T, 16> zipLow(VectorType<T, 16> a, VectorType<T, 16> b) {
+ return SHUFFLE(a, b, 0, 1, 2, 3, 4, 5, 6, 7, 16, 17, 18, 19, 20, 21, 22, 23);
+}
+
+template <typename T>
+SI VectorType<T, 16> zipHigh(VectorType<T, 16> a, VectorType<T, 16> b) {
+ return SHUFFLE(a, b, 8, 9, 10, 11, 12, 13, 14, 15, 24, 25, 26, 27, 28, 29, 30,
+ 31);
+}
+
+template <typename T>
+SI VectorType<T, 8> zip2Low(VectorType<T, 8> a, VectorType<T, 8> b) {
+ return SHUFFLE(a, b, 0, 1, 8, 9, 2, 3, 10, 11);
+}
+
+template <typename T>
+SI VectorType<T, 8> zip2High(VectorType<T, 8> a, VectorType<T, 8> b) {
+ return SHUFFLE(a, b, 4, 5, 12, 13, 6, 7, 14, 15);
+}
+
+#ifdef __clang__
+template <typename T>
+SI VectorType<T, 8> zip(VectorType<T, 4> a, VectorType<T, 4> b) {
+ return SHUFFLE(a, b, 0, 4, 1, 5, 2, 6, 3, 7);
+}
+
+template <typename T>
+SI VectorType<T, 16> zip(VectorType<T, 8> a, VectorType<T, 8> b) {
+ return SHUFFLE(a, b, 0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15);
+}
+#else
+template <typename T, int N>
+SI VectorType<T, N * 2> zip(VectorType<T, N> a, VectorType<T, N> b) {
+ return combine(zipLow(a, b), zipHigh(a, b));
+}
+#endif
+
+template <typename T>
+struct Unaligned {
+ template <typename P>
+ SI T load(const P* p) {
+ T v;
+ memcpy(&v, p, sizeof(v));
+ return v;
+ }
+
+ template <typename P>
+ SI void store(P* p, T v) {
+ memcpy(p, &v, sizeof(v));
+ }
+};
+
+#ifndef __clang__
+template <typename T, int N>
+struct Unaligned<VectorType<T, N>> {
+ template <typename P>
+ SI VectorType<T, N> load(const P* p) {
+ VectorType<T, N> v;
+ memcpy(v.elements, p, sizeof(v));
+ return v;
+ }
+
+ template <typename P>
+ SI void store(P* p, VectorType<T, N> v) {
+ memcpy(p, v.elements, sizeof(v));
+ }
+};
+#endif
+
+template <typename T, typename P>
+SI T unaligned_load(const P* p) {
+ return Unaligned<T>::load(p);
+}
+
+template <typename T, typename P>
+SI void unaligned_store(P* p, T v) {
+ Unaligned<T>::store(p, v);
+}
+
+template <typename D, typename S>
+SI D bit_cast(const S& src) {
+ static_assert(sizeof(D) == sizeof(S), "");
+ return unaligned_load<D>(&src);
+}
+
+template <typename T>
+using V2 = VectorType<T, 2>;
+template <typename T>
+using V4 = VectorType<T, 4>;
+using Float = V4<float>;
+using I32 = V4<int32_t>;
+using I16 = V4<int16_t>;
+using U64 = V4<uint64_t>;
+using U32 = V4<uint32_t>;
+using U16 = V4<uint16_t>;
+using U8 = V4<uint8_t>;
+using Bool = V4<int>;
+template <typename T>
+using V8 = VectorType<T, 8>;
+template <typename T>
+using V16 = VectorType<T, 16>;
+
+} // namespace glsl
diff --git a/gfx/wr/webrender/Cargo.toml b/gfx/wr/webrender/Cargo.toml
new file mode 100644
index 0000000000..f747f071d8
--- /dev/null
+++ b/gfx/wr/webrender/Cargo.toml
@@ -0,0 +1,63 @@
+[package]
+name = "webrender"
+version = "0.62.0"
+authors = ["Glenn Watson <gw@intuitionlibrary.com>"]
+license = "MPL-2.0"
+repository = "https://github.com/servo/webrender"
+description = "A GPU accelerated 2D renderer for web content"
+build = "build.rs"
+edition = "2018"
+
+[features]
+default = ["static_freetype"]
+profiler = ["tracy-rs/enable_profiler"]
+capture = ["api/serialize", "ron", "serde", "smallvec/serde", "etagere/serialization", "glyph_rasterizer/capture"]
+replay = ["api/deserialize", "ron", "serde", "smallvec/serde", "etagere/serialization", "glyph_rasterizer/replay"]
+display_list_stats = ["api/display_list_stats"]
+serialize_program = ["serde", "webrender_build/serialize_program"]
+dynamic_freetype = ["glyph_rasterizer/dynamic_freetype"]
+static_freetype = ["glyph_rasterizer/static_freetype"]
+leak_checks = []
+gecko = ["firefox-on-glean", "glyph_rasterizer/gecko"]
+sw_compositor = ["swgl"]
+
+[build-dependencies]
+build-parallel = "0.1.2"
+glslopt = "0.1.9"
+webrender_build = { version = "0.0.2", path = "../webrender_build" }
+
+[dependencies]
+bincode = "1.0"
+bitflags = "1.2"
+byteorder = "1.0"
+euclid = { version = "0.22.0", features = ["serde"] }
+fxhash = "0.2.1"
+gleam = "0.15.0"
+lazy_static = "1"
+log = "0.4"
+malloc_size_of_derive = "0.1"
+num-traits = "0.2"
+plane-split = "0.18"
+png = { optional = true, version = "0.16" }
+rayon = "1"
+ron = { optional = true, version = "0.8" }
+serde = { optional = true, version = "1.0", features = ["serde_derive"] }
+smallvec = "1"
+time = "0.1"
+api = { version = "0.62.0", path = "../webrender_api", package = "webrender_api" }
+webrender_build = { version = "0.0.2", path = "../webrender_build" }
+malloc_size_of = { version = "0.0.2", path = "../wr_malloc_size_of", package = "wr_malloc_size_of" }
+glyph_rasterizer = { version = "0.1.0", path = "../wr_glyph_rasterizer", package = "wr_glyph_rasterizer", default-features = false }
+svg_fmt = "0.4"
+tracy-rs = "0.1.2"
+derive_more = { version = "0.99", default-features = false, features = ["add_assign"] }
+etagere = "0.2.6"
+glean = "52.7.0"
+firefox-on-glean = { version = "0.1.0", optional = true }
+swgl = { path = "../swgl", optional = true }
+topological-sort = "0.1"
+peek-poke = { version = "0.3", path = "../peek-poke" }
+
+[dev-dependencies]
+mozangle = "0.3.3"
+rand = "0.4"
diff --git a/gfx/wr/webrender/build.rs b/gfx/wr/webrender/build.rs
new file mode 100644
index 0000000000..783f8cd5ca
--- /dev/null
+++ b/gfx/wr/webrender/build.rs
@@ -0,0 +1,352 @@
+/* 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/. */
+
+extern crate webrender_build;
+
+use std::borrow::Cow;
+use std::env;
+use std::fs::{canonicalize, read_dir, File};
+use std::io::prelude::*;
+use std::path::{Path, PathBuf};
+use std::collections::hash_map::DefaultHasher;
+use std::hash::Hasher;
+use webrender_build::shader::*;
+use webrender_build::shader_features::{ShaderFeatureFlags, get_shader_features};
+
+// glsopt is known to leak, but we don't particularly care.
+#[no_mangle]
+pub extern "C" fn __lsan_default_options() -> *const u8 {
+ b"detect_leaks=0\0".as_ptr()
+}
+
+/// Compute the shader path for insertion into the include_str!() macro.
+/// This makes for more compact generated code than inserting the literal
+/// shader source into the generated file.
+///
+/// If someone is building on a network share, I'm sorry.
+fn escape_include_path(path: &Path) -> String {
+ let full_path = canonicalize(path).unwrap();
+ let full_name = full_path.as_os_str().to_str().unwrap();
+ let full_name = full_name.replace("\\\\?\\", "");
+ let full_name = full_name.replace("\\", "/");
+
+ full_name
+}
+
+fn write_unoptimized_shaders(
+ mut glsl_files: Vec<PathBuf>,
+ shader_file: &mut File,
+) -> Result<(), std::io::Error> {
+ writeln!(
+ shader_file,
+ " pub static ref UNOPTIMIZED_SHADERS: HashMap<&'static str, SourceWithDigest> = {{"
+ )?;
+ writeln!(shader_file, " let mut shaders = HashMap::new();")?;
+
+ // Sort the file list so that the shaders.rs file is filled
+ // deterministically.
+ glsl_files.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
+
+ for glsl in glsl_files {
+ // Compute the shader name.
+ assert!(glsl.is_file());
+ let shader_name = glsl.file_name().unwrap().to_str().unwrap();
+ let shader_name = shader_name.replace(".glsl", "");
+
+ // Compute a digest of the #include-expanded shader source. We store
+ // this as a literal alongside the source string so that we don't need
+ // to hash large strings at runtime.
+ let mut hasher = DefaultHasher::new();
+ let base = glsl.parent().unwrap();
+ assert!(base.is_dir());
+ ShaderSourceParser::new().parse(
+ Cow::Owned(shader_source_from_file(&glsl)),
+ &|f| Cow::Owned(shader_source_from_file(&base.join(&format!("{}.glsl", f)))),
+ &mut |s| hasher.write(s.as_bytes()),
+ );
+ let digest: ProgramSourceDigest = hasher.into();
+
+ writeln!(
+ shader_file,
+ " shaders.insert(\"{}\", SourceWithDigest {{ source: include_str!(\"{}\"), digest: \"{}\"}});",
+ shader_name,
+ escape_include_path(&glsl),
+ digest,
+ )?;
+ }
+ writeln!(shader_file, " shaders")?;
+ writeln!(shader_file, " }};")?;
+
+ Ok(())
+}
+
+#[derive(Clone, Debug)]
+struct ShaderOptimizationInput {
+ shader_name: &'static str,
+ config: String,
+ gl_version: ShaderVersion,
+}
+
+#[derive(Debug)]
+struct ShaderOptimizationOutput {
+ full_shader_name: String,
+ gl_version: ShaderVersion,
+ vert_file_path: PathBuf,
+ frag_file_path: PathBuf,
+ digest: ProgramSourceDigest,
+}
+
+#[derive(Debug)]
+struct ShaderOptimizationError {
+ shader: ShaderOptimizationInput,
+ message: String,
+}
+
+/// Prepends the line number to each line of a shader source.
+fn enumerate_shader_source_lines(shader_src: &str) -> String {
+ // For some reason the glsl-opt errors are offset by 1 compared
+ // to the provided shader source string.
+ let mut out = format!("0\t|");
+ for (n, line) in shader_src.split('\n').enumerate() {
+ let line_number = n + 1;
+ out.push_str(&format!("{}\t|{}\n", line_number, line));
+ }
+ out
+}
+
+fn write_optimized_shaders(
+ shader_dir: &Path,
+ shader_file: &mut File,
+ out_dir: &str,
+) -> Result<(), std::io::Error> {
+ writeln!(
+ shader_file,
+ " pub static ref OPTIMIZED_SHADERS: HashMap<(ShaderVersion, &'static str), OptimizedSourceWithDigest> = {{"
+ )?;
+ writeln!(shader_file, " let mut shaders = HashMap::new();")?;
+
+ // The full set of optimized shaders can be quite large, so only optimize
+ // for the GL version we expect to be used on the target platform. If a different GL
+ // version is used we will simply fall back to the unoptimized shaders.
+ let shader_versions = match env::var("CARGO_CFG_TARGET_OS").as_ref().map(|s| &**s) {
+ Ok("android") | Ok("windows") => [ShaderVersion::Gles],
+ _ => [ShaderVersion::Gl],
+ };
+
+ let mut shaders = Vec::default();
+ for &gl_version in &shader_versions {
+ let mut flags = ShaderFeatureFlags::all();
+ if gl_version != ShaderVersion::Gl {
+ flags.remove(ShaderFeatureFlags::GL);
+ }
+ if gl_version != ShaderVersion::Gles {
+ flags.remove(ShaderFeatureFlags::GLES);
+ flags.remove(ShaderFeatureFlags::TEXTURE_EXTERNAL);
+ }
+ if !matches!(
+ env::var("CARGO_CFG_TARGET_OS").as_ref().map(|s| &**s),
+ Ok("android")
+ ) {
+ flags.remove(ShaderFeatureFlags::TEXTURE_EXTERNAL_ESSL1);
+ }
+ flags.remove(ShaderFeatureFlags::DITHERING);
+
+ for (shader_name, configs) in get_shader_features(flags) {
+ for config in configs {
+ shaders.push(ShaderOptimizationInput {
+ shader_name,
+ config,
+ gl_version,
+ });
+ }
+ }
+ }
+
+ let outputs = build_parallel::compile_objects::<_, _, ShaderOptimizationError, _>(
+ &|shader: &ShaderOptimizationInput| {
+ println!("Optimizing shader {:?}", shader);
+ let target = match shader.gl_version {
+ ShaderVersion::Gl => glslopt::Target::OpenGl,
+ ShaderVersion::Gles => glslopt::Target::OpenGles30,
+ };
+ let glslopt_ctx = glslopt::Context::new(target);
+
+ let features = shader
+ .config
+ .split(",")
+ .filter(|f| !f.is_empty())
+ .collect::<Vec<_>>();
+
+ let (vert_src, frag_src) =
+ build_shader_strings(shader.gl_version, &features, shader.shader_name, &|f| {
+ Cow::Owned(shader_source_from_file(
+ &shader_dir.join(&format!("{}.glsl", f)),
+ ))
+ });
+
+ let full_shader_name = if shader.config.is_empty() {
+ shader.shader_name.to_string()
+ } else {
+ format!("{}_{}", shader.shader_name, shader.config.replace(",", "_"))
+ };
+
+ // Compute a digest of the optimized shader sources. We store this
+ // as a literal alongside the source string so that we don't need
+ // to hash large strings at runtime.
+ let mut hasher = DefaultHasher::new();
+
+ let [vert_file_path, frag_file_path] = [
+ (glslopt::ShaderType::Vertex, vert_src, "vert"),
+ (glslopt::ShaderType::Fragment, frag_src, "frag"),
+ ]
+ .map(|(shader_type, shader_src, extension)| {
+ let output = glslopt_ctx.optimize(shader_type, shader_src.clone());
+ if !output.get_status() {
+ let source = enumerate_shader_source_lines(&shader_src);
+ return Err(ShaderOptimizationError {
+ shader: shader.clone(),
+ message: format!("{}\n{}", source, output.get_log()),
+ });
+ }
+
+ let shader_path = Path::new(out_dir).join(format!(
+ "{}_{:?}.{}",
+ full_shader_name, shader.gl_version, extension
+ ));
+ write_optimized_shader_file(
+ &shader_path,
+ output.get_output().unwrap(),
+ &shader.shader_name,
+ &features,
+ &mut hasher,
+ );
+ Ok(shader_path)
+ });
+
+ let vert_file_path = vert_file_path?;
+ let frag_file_path = frag_file_path?;
+
+ println!("Finished optimizing shader {:?}", shader);
+
+ Ok(ShaderOptimizationOutput {
+ full_shader_name,
+ gl_version: shader.gl_version,
+ vert_file_path,
+ frag_file_path,
+ digest: hasher.into(),
+ })
+ },
+ &shaders,
+ );
+
+ match outputs {
+ Ok(mut outputs) => {
+ // Sort the shader list so that the shaders.rs file is filled
+ // deterministically.
+ outputs.sort_by(|a, b| {
+ (a.gl_version, a.full_shader_name.clone())
+ .cmp(&(b.gl_version, b.full_shader_name.clone()))
+ });
+
+ for shader in outputs {
+ writeln!(
+ shader_file,
+ " shaders.insert(({}, \"{}\"), OptimizedSourceWithDigest {{",
+ shader.gl_version.variant_name(),
+ shader.full_shader_name,
+ )?;
+ writeln!(
+ shader_file,
+ " vert_source: include_str!(\"{}\"),",
+ escape_include_path(&shader.vert_file_path),
+ )?;
+ writeln!(
+ shader_file,
+ " frag_source: include_str!(\"{}\"),",
+ escape_include_path(&shader.frag_file_path),
+ )?;
+ writeln!(shader_file, " digest: \"{}\",", shader.digest)?;
+ writeln!(shader_file, " }});")?;
+ }
+ }
+ Err(err) => match err {
+ build_parallel::Error::BuildError(err) => {
+ panic!("Error optimizing shader {:?}: {}", err.shader, err.message)
+ }
+ _ => panic!("Error optimizing shaders."),
+ },
+ }
+
+ writeln!(shader_file, " shaders")?;
+ writeln!(shader_file, " }};")?;
+
+ Ok(())
+}
+
+fn write_optimized_shader_file(
+ path: &Path,
+ source: &str,
+ shader_name: &str,
+ features: &[&str],
+ hasher: &mut DefaultHasher,
+) {
+ let mut file = File::create(&path).unwrap();
+ for (line_number, line) in source.lines().enumerate() {
+ // We embed the shader name and features as a comment in the
+ // source to make debugging easier.
+ // The #version directive must be on the first line so we insert
+ // the extra information on the next line.
+ if line_number == 1 {
+ let prelude = format!("// {}\n// features: {:?}\n\n", shader_name, features);
+ file.write_all(prelude.as_bytes()).unwrap();
+ hasher.write(prelude.as_bytes());
+ }
+ file.write_all(line.as_bytes()).unwrap();
+ file.write_all("\n".as_bytes()).unwrap();
+ hasher.write(line.as_bytes());
+ hasher.write("\n".as_bytes());
+ }
+}
+
+fn main() -> Result<(), std::io::Error> {
+ let out_dir = env::var("OUT_DIR").unwrap_or("out".to_owned());
+
+ let shaders_file_path = Path::new(&out_dir).join("shaders.rs");
+ let mut glsl_files = vec![];
+
+ println!("cargo:rerun-if-changed=res");
+ let res_dir = Path::new("res");
+ for entry in read_dir(res_dir)? {
+ let entry = entry?;
+ let path = entry.path();
+
+ if entry.file_name().to_str().unwrap().ends_with(".glsl") {
+ println!("cargo:rerun-if-changed={}", path.display());
+ glsl_files.push(path.to_owned());
+ }
+ }
+
+ let mut shader_file = File::create(shaders_file_path)?;
+
+ writeln!(shader_file, "/// AUTO GENERATED BY build.rs\n")?;
+ writeln!(shader_file, "use std::collections::HashMap;\n")?;
+ writeln!(shader_file, "use webrender_build::shader::ShaderVersion;\n")?;
+ writeln!(shader_file, "pub struct SourceWithDigest {{")?;
+ writeln!(shader_file, " pub source: &'static str,")?;
+ writeln!(shader_file, " pub digest: &'static str,")?;
+ writeln!(shader_file, "}}\n")?;
+ writeln!(shader_file, "pub struct OptimizedSourceWithDigest {{")?;
+ writeln!(shader_file, " pub vert_source: &'static str,")?;
+ writeln!(shader_file, " pub frag_source: &'static str,")?;
+ writeln!(shader_file, " pub digest: &'static str,")?;
+ writeln!(shader_file, "}}\n")?;
+ writeln!(shader_file, "lazy_static! {{")?;
+
+ write_unoptimized_shaders(glsl_files, &mut shader_file)?;
+ writeln!(shader_file, "")?;
+ write_optimized_shaders(&res_dir, &mut shader_file, &out_dir)?;
+ writeln!(shader_file, "}}")?;
+
+ Ok(())
+}
diff --git a/gfx/wr/webrender/doc/CLIPPING_AND_POSITIONING.md b/gfx/wr/webrender/doc/CLIPPING_AND_POSITIONING.md
new file mode 100644
index 0000000000..4aa8d0c684
--- /dev/null
+++ b/gfx/wr/webrender/doc/CLIPPING_AND_POSITIONING.md
@@ -0,0 +1,150 @@
+# Original Design
+
+To understand the current design for clipping and positioning (transformations
+and scrolling) in WebRender it can be useful to have a little background about
+the original design for these features. The most important thing to remember is
+that originally clipping, scrolling regions, and transformations were
+properties of stacking contexts and they were completely _hierarchical_. This
+goes a long way toward representing the majority of CSS content on the web, but
+fails when dealing with important edges cases and features including:
+ 1. Support for sticky positioned content
+ 2. Scrolling areas that include content that is ordered both above and below
+ intersecting content from outside the scroll area.
+ 3. Items in the same scrolling root, clipped by different clips one or more of
+ which are defined outside the scrolling root itself.
+ 4. Completely non-hierarchical clipping situations, such as when items are
+ clipped by some clips in the hierarchy, but not others.
+
+Design changes have been a step by step path from the original design to one
+that can handle all CSS content.
+
+# Current Design
+
+All positioning and clipping is handled by the `SpatialTree`. The name is a
+holdover from when this tree was a tree of `Layers` which handled both
+positioning and clipping. Currently the `SpatialTree` holds:
+ 1. A hierarchical collection of `SpatialNodes`, with the final screen
+ transformation of each node depending on the relative transformation of the
+ node combined with the transformations of all of its ancestors. These nodes
+ are responsible for positioning display list items and clips.
+ 2. A collection of `ClipNodes` which specify a rectangular clip and, optionally,
+ a set of rounded rectangle clips and a masking image.
+ 3. A collection of `ClipChains`. Each `ClipChain` is a list of `ClipNode`
+ elements. Every display list item has an assigned `ClipChain` which
+ specifies what `ClipNodes` are applied to that item.
+
+The `SpatialNode` of each clip applied to an item is completely independent of
+the `SpatialNode` applied to the item itself.
+
+One holdover from the previous design is that both `ClipNode` and `SpatialNodes`
+have a parent node, which is either a `SpatialNode` or a `ClipNode`. From this
+node WebRender can determine both a parent `ClipNode` and a parent `SpatialNode`
+by finding the first ancestor of that type. This is handled by the
+`DisplayListFlattener`.
+
+## `SpatialNode`
+There are three types of `SpatialNodes`:
+ 1. Reference frames which are created when content needs to apply
+ transformation or perspective properties to display list items. Reference
+ frames establish a new coordinate system, so internally all coordinates on
+ display list items are relative to the reference frame origin. Later
+ any non-reference frame positioning nodes that display list items belong
+ to can adjust this position relative to the reference frame origin.
+ 2. Scrolling nodes are used to define scrolling areas. These nodes have scroll
+ offsets which are a 2D translation relative to ancestor nodes and, ultimately,
+ the reference frame origin.
+ 3. Sticky frames are responsible for implementing position:sticky behavior.
+ This is also an 2D translation.
+
+`SpatialNodes` are defined as items in the display list. After scene building
+each node is traversed hierarchically during the `SpatialTree::update()` step.
+Once reference frame transforms and relative offsets are calculated, a to screen
+space transformation can be calculated for each `SpatialNode`. This transformation
+is added the `TransformPalette` and becomes directly available to WebRender shaders.
+
+In addition to screen space transformation calculation, the `SpatialNode` tree
+is divided up into _compatible coordinate systems_. These are coordinate systems
+which differ only by 2D translations from their parent system. These compatible
+coordinate systems may even cross reference frame boundaries. The goal here is
+to allow the application clipping rectangles from different compatible
+coordinate systems without generating mask images.
+
+## `ClipNode`
+
+Each clip node holds a clip rectangle along with an optional collection of
+rounded clip rectangles and a mask image. The fact that `ClipNodes` all have a
+clip rectangle is important because it means that all content clipped by a
+clip node has a bounding rectangle, which can be converted into a bounding
+screen space rectangle. This rectangle is called the _outer rectangle_ of the
+clip. `ClipNodes` may also have an _inner rectangle_, which is an area within
+the boundaries of the _outer rectangle_ that is completely unclipped.
+
+These rectangles are calculated during the `SpatialTree::update()` phase. In
+addition, each `ClipNode` produces a template `ClipChainNode` used to build
+the `ClipChains` which use that node.
+
+## `ClipChains`
+
+There are two ways that `ClipChains` are defined in WebRender. The first is
+through using the API for manually specifying `ClipChains` via a parent
+`ClipChain` and a list of `ClipNodes`. The second is through the hierarchy of a
+`ClipNode` established by its parent node. Every `ClipNode` has a chain of
+ancestor `SpatialNodes` and `ClipNodes`. The creation of a `ClipNode`
+automatically defines a `ClipChain` for this hierarchy. This behavior is a
+compatibility feature with the old completely hierarchical clipping architecture
+and is still how Gecko and Servo create most of their `ClipChains`. These
+hierarchical `ClipChains` are constructed during the `ClipNode::update()` step.
+
+During `ClipChain` construction, WebRender tries to eliminate clips that will
+not affect rendering, by looking at the combined _outer rectangle_ and _inner
+rectangle_ of a `ClipChain` and the _outer rectangle_ and _inner rectangle_ of
+any `ClipNode` appended to the chain. An example of the goal of this process is
+to avoid having to render a mask for a large rounded rectangle when the rest of
+the clip chain constrains the content to an area completely inside that
+rectangle. Avoiding mask rasterization in this case and others has large
+performance impacts on WebRender.
+
+# Clipping and Positioning in the Display List
+
+Each non-structural WebRender display list item has
+ * A `SpatialId` of a `SpatialNode` for positioning
+ * A `ClipId` of a `ClipNode` or a `ClipChain` for clipping
+ * An item-specific rectangular clip rectangle
+
+The positioning node determines how that item is positioned. It's assumed that
+the positioning node and the item are children of the same reference frame. The
+clipping node determines how that item is clipped. This should be fully
+independent of how the node is positioned and items can be clipped by any
+`ClipChain` regardless of the reference frame of their member clips. Finally,
+the item-specific clipping rectangle is applied directly to the item and should
+never result in the creation of a clip mask itself.
+
+## Converting user-exposed `ClipId`/`SpatialId` to internal indices
+
+WebRender must access `ClipNodes` and `SpatialNodes` quite a bit when building
+scenes and frames, so it tries to convert `ClipId`/`SpatialId`, which are already
+per-pipeline indices, to global scene-wide indices. Internally this is a
+conversion from `ClipId` into `ClipNodeIndex` or `ClipChainIndex`, and from
+`SpatialId` into `SpatialNodeIndex`. In order to make this conversion cheaper, the
+`DisplayListFlattner` assigns offsets for each pipeline and node type in the
+scene-wide `SpatialTree`.
+
+Nodes are added to their respective arrays sequentially as the display list is
+processed during scene building. When encountering an iframe, the
+`DisplayListFlattener` must start processing the nodes for that iframe's
+pipeline, meaning that nodes are now being added out of order to the node arrays
+of the `SpatialTree`. In this case, the `SpatialTree` fills in the gaps in
+the node arrays with placeholder nodes.
+
+# Hit Testing
+
+Hit testing is the responsibility of the `HitTester` data structure. This
+structure copies information necessary for hit testing from the
+`SpatialTree`. This is done so that hit testing can still take place while a
+new `SpatialTree` is under construction.
+
+# Ideas for the Future
+1. Expose the difference between `ClipId` and `ClipChainId` in the API.
+2. Prevent having to duplicate the `SpatialTree` for hit testing.
+3. Avoid having to create placeholder nodes in the `SpatialTree` while
+ processing iframes.
diff --git a/gfx/wr/webrender/doc/blob.md b/gfx/wr/webrender/doc/blob.md
new file mode 100644
index 0000000000..b910f6f76a
--- /dev/null
+++ b/gfx/wr/webrender/doc/blob.md
@@ -0,0 +1,43 @@
+# Blob images
+
+Blob image is fallback mechanism for webrender that Gecko uses to render primitives that aren't currently supported by webrender. The main idea is to provide webrender with a custom handler that can take arbitray drawing commands serialized as buffers of bytes (the blobs) and turn them into images that webrender internally will treat as regular images.
+
+At the API level, blob images are treated as other images. They are resources created and associated with image keys, and are used in the display list with regular image display items.
+
+
+## Active area
+
+In order to support scrolling very large content, blob images don't necessarily have a finite size. They can grow in any direction. At any time they do have an "active area", also called "visible area" which defines the portion that has to be rasterized. Typically this active area moves along large blob images depending on the scroll position.
+The coordinate system of active area the *should* be the one of the blob's drawing commands (this is really up to the blob handler implementation to enforce that, Gecko does), and its scale should correspond to device pixels. The active area's coordinates can be negative.
+
+As far as positioning goes, the active area maps to the image display item's bounds. In other words the content at the top-left corner of the active area will be rendered on screen at the position of the top-left corner of the display item's local rect.
+
+In Gecko, the active area corresponds to the intersection of the fallback content's rect and the displayport.
+
+The terms "visible area" and "visible rect" are used a lot in the blobs code, unfortunately they collide with frame building's visibility/culling terminology. They don't correspond to what is visible to the user, but rather what is in the displayport.
+
+
+## Tiling
+
+Blob images can be either tiled or non-tiled. Non-tiled blob images support invalid rects while tiled blob images track only validty at the tile level. In gecko all blobs are tiled with a tile size of 256x256.
+
+Just like regular tiled images, blob image tiles along the border of the image are shrinked to fit the remaining size. The only difference is that the tiling pattern always starts at the top-left corner for regular images (smaller boundary tiles only along the right and bottom edges), while it can be aribtrarily positioned for blob images (smaller boundary tiles potentially on all sides).
+
+The tiling logic is in webrender/src/image.rs.
+
+
+## Async rasterization
+
+Blobs are typically too slow to rasterize on the critical path. We try to avoid blocking frame building on blob image rasterization. In order to do that we rasterize blobs as part of scene building. Rather than rasterize tiles on demand from visibility informating, we rasterize the entire active area during scene building. This means we potentially process a lot more content than will be displayed if the user doesn't scroll through all of the visible area.
+
+When the render backend receives a transaction, it looks for all new and update blob images, and generate blob rasterization requests for all tiles of the blob images that overlap their active area. The requests are bundled with an `AsyncBlobImageRasterizer` object in the transaction that is sent to the scene builder thread. The async rasterizer is created by the `BlobImageHandler` at each transaction. It is a snapshot of the state of the blobs as well as external information such as fonts, and does the actual rasterization.
+
+While tiles are rasterized eagerly during scene building, their content is uploaded lazily to the texture cache depending on the result of the visibility pass during frame building.
+
+
+## Late rasterization
+
+In some case we run into a missing blob image during frame building and have to rasterize it synchronously. This happens when a rasterized tile is uploaded to the texture cache (at which point the CPU side is discarded), the texture cache entry expires and after scrolling back into view the tile is needed again.
+We should really keep the rasterized blobs around just like we keep regular images in the cache. Hopefully this section will become obsolete eventually and we'll be able to remove late blob rasterization.
+
+The information needed for async rasterization corresponds to the state of blobs before scene building while late rasterization needs the state of blobs after the last complete scene build. This means we have to be careful about which version we manipulate in the resource cache.
diff --git a/gfx/wr/webrender/doc/swizzling.md b/gfx/wr/webrender/doc/swizzling.md
new file mode 100644
index 0000000000..4b38791940
--- /dev/null
+++ b/gfx/wr/webrender/doc/swizzling.md
@@ -0,0 +1,31 @@
+> It'd be great to have some (in-tree) docs describing the process you've worked through here, the overall motivation, how it works on different GPUs / platforms etc. Perhaps as a follow up?
+
+# Swizzling in WR
+
+## Problem statement
+
+The goal is to avoid the CPU conversion of data done by the driver on texture data uploads. It's slow and always done synchronously, hurting our "Renderer" thread CPU utilization.
+
+Gecko produces all of the image data in BGRA. Switching "imagelib" to RGBA is possible, but modifying Skia to follow is less trivial.
+OpenGL support for BGRA8 as an internal texture format is a complex story: it's officially supported in Angle and a fair share of Android devices, but it's not available on the desktop GL (and until recently wasn't available in the Android emulator). Unofficially, when textures are initialized with `glTexImage` (as opposed to `glTexStorage`) with RGBA internal format, the desktop GL drivers often prefer to store the data in BGRA8 format, actually.
+
+The only way to avoid the CPU conversion is to provide the data in exactly the same format that the driver is using internally for a texture. In this case, the driver does a straght `memcpy` into its CPU-visible memory, which is the best we can hope for with OpenGL API.
+
+## Solution: swizzling
+
+https://phabricator.services.mozilla.com/D21965 is providing the solution to this problem. The main principles are:
+
+ 1. Use `glTexStorage` whenever it's available. Doing so gives us full control of the internal format, also allows to avoid allocating memory for mipmaps that we don't use.
+ 2. Make the shared texture cache format to be determined at the init time, based on the GL device capabilities. For Angle and OpenGL ES this is BGRA8, for desktop this is RGBA8 (since desktop GL doesn't support BGRA internal formats). WebRender is now able to tell Gecko, which color format it prefers the texture data to use.
+ 3. If the data comes in a format that is different from our best case, we pretend that the data is actually in our best case format, and associate the allocated cache entry with the `Swizzle`. That swizzle configuration changes the way shaders sample from a texture, adjusting for the fact the data was provided in a different format.
+ 4. The lifetime of a "swizzled" texture cache data is starting at the point the data is uploaded and ending at a point where any shader samples from this data. Any other operation on that data (copying or blitting) is not configurable by `Swizzle` and thus would produce incorrect results. To address this, the change enhances `cs_copy` shader to be used in place of blitting from the texture cache, where needed.
+ 5. Swizzling becomes a part of the batch key per texture. Mixing up draw calls with texture data that is differently swizzled then introduces the batch breaks. This is a downside for the swizzling approach in general, but it's not clear to what extent this would affect Gecko.
+
+## Code paths
+
+Windows/Angle and Android:
+ - we use `glTexStorage` with BGRA8 internal format, no swizzling is needed in general case.
+
+Windows (non-Angle), Mac, Linux:
+ - if `glTexStorage` is available, we use it with RGBA8 internal format, swizzling everything on texture sampling.
+ - otherwise, we use RGBA unsized format with `gTexImage` and expect the data to come in BGRA format, no swizzling is involved.
diff --git a/gfx/wr/webrender/doc/text-rendering.md b/gfx/wr/webrender/doc/text-rendering.md
new file mode 100644
index 0000000000..b965562b99
--- /dev/null
+++ b/gfx/wr/webrender/doc/text-rendering.md
@@ -0,0 +1,720 @@
+# Text Rendering
+
+This document describes the details of how WebRender renders text, particularly the blending stage of text rendering.
+We will go into grayscale text blending, subpixel text blending, and "subpixel text with background color" blending.
+
+### Prerequisites
+
+The description below assumes you're familiar with regular rgba compositing, operator over,
+and the concept of premultiplied alpha.
+
+### Not covered in this document
+
+We are going to treat the origin of the text mask as a black box.
+We're also going to assume we can blend text in the device color space and will not go into the gamma correction and linear pre-blending that happens in some of the backends that produce the text masks.
+
+## Grayscale Text Blending
+
+Grayscale text blending is the simplest form of text blending. Our blending function has three inputs:
+
+ - The text color, as a premultiplied rgba color.
+ - The text mask, as a single-channel alpha texture.
+ - The existing contents of the framebuffer that we're rendering to, the "destination". This is also a premultiplied rgba buffer.
+
+Note: The word "grayscale" here does *not* mean that we can only draw gray text.
+It means that the mask only has a single alpha value per pixel, so we can visualize
+the mask in our minds as a grayscale image.
+
+### Deriving the math
+
+We want to mask our text color using the single-channel mask, and composite that to the destination.
+This compositing step uses operator "over", just like regular compositing of rgba images.
+
+I'll be using GLSL syntax to describe the blend equations, but please consider most of the code below pseudocode.
+
+We can express the blending described above as the following blend equation:
+
+```glsl
+vec4 textblend(vec4 text_color, vec4 mask, vec4 dest) {
+ return over(in(text_color, mask), dest);
+}
+```
+
+with `over` being the blend function for (premultiplied) operator "over":
+
+```glsl
+vec4 over(vec4 src, vec4 dest) {
+ return src + (1.0 - src.a) * dest;
+}
+```
+
+and `in` being the blend function for (premultiplied) operator "in", i.e. the masking operator:
+
+```glsl
+vec4 in(vec4 src, vec4 mask) {
+ return src * mask.a;
+}
+```
+
+So the complete blending function is:
+
+```glsl
+result.r = text_color.r * mask.a + (1.0 - text_color.a * mask.a) * dest.r;
+result.g = text_color.g * mask.a + (1.0 - text_color.a * mask.a) * dest.g;
+result.b = text_color.b * mask.a + (1.0 - text_color.a * mask.a) * dest.b;
+result.a = text_color.a * mask.a + (1.0 - text_color.a * mask.a) * dest.a;
+```
+
+### Rendering this with OpenGL
+
+In general, a fragment shader does not have access to the destination.
+So the full blend equation needs to be expressed in a way that the shader only computes values that are independent of the destination,
+and the parts of the equation that use the destination values need to be applied by the OpenGL blend pipeline itself.
+The OpenGL blend pipeline can be tweaked using the functions `glBlendEquation` and `glBlendFunc`.
+
+In our example, the fragment shader can output just `text_color * mask.a`:
+
+```glsl
+ oFragColor = text_color * mask.a;
+```
+
+and the OpenGL blend pipeline can be configured like so:
+
+```rust
+ pub fn set_blend_mode_premultiplied_alpha(&self) {
+ self.gl.blend_func(gl::ONE, gl::ONE_MINUS_SRC_ALPHA);
+ self.gl.blend_equation(gl::FUNC_ADD);
+ }
+```
+
+This results in an overall blend equation of
+
+```
+result.r = 1 * oFragColor.r + (1 - oFragColor.a) * dest.r;
+ ^ ^ ^^^^^^^^^^^^^^^^^
+ | | |
+ +--gl::ONE | +-- gl::ONE_MINUS_SRC_ALPHA
+ |
+ +-- gl::FUNC_ADD
+
+ = 1 * (text_color.r * mask.a) + (1 - (text_color.a * mask.a)) * dest.r
+ = text_color.r * mask.a + (1 - text_color.a * mask.a) * dest.r
+```
+
+which is exactly what we wanted.
+
+### Differences to the actual WebRender code
+
+There are two minor differences between the shader code above and the actual code in the text run shader in WebRender:
+
+```glsl
+oFragColor = text_color * mask.a; // (shown above)
+// vs.
+oFragColor = vColor * mask * alpha; // (actual webrender code)
+```
+
+`vColor` is set to the text color. The differences are:
+
+ - WebRender multiplies with all components of `mask` instead of just with `mask.a`.
+ However, our font rasterization code fills the rgb values of `mask` with the value of `mask.a`,
+ so this is completely equivalent.
+ - WebRender applies another alpha to the text. This is coming from the clip.
+ You can think of this alpha to be a pre-adjustment of the text color for that pixel, or as an
+ additional mask that gets applied to the mask.
+
+## Subpixel Text Blending
+
+Now that we have the blend equation for single-channel text blending, we can look at subpixel text blending.
+
+The main difference between subpixel text blending and grayscale text blending is the fact that,
+for subpixel text, the text mask contains a separate alpha value for each color component.
+
+### Component alpha
+
+Regular painting uses four values per pixel: three color values, and one alpha value. The alpha value applies to all components of the pixel equally.
+
+Imagine for a second a world in which you have *three alpha values per pixel*, one for each color component.
+
+ - Old world: Each pixel has four values: `color.r`, `color.g`, `color.b`, and `color.a`.
+ - New world: Each pixel has *six* values: `color.r`, `color.a_r`, `color.g`, `color.a_g`, `color.b`, and `color.a_b`.
+
+In such a world we can define a component-alpha-aware operator "over":
+
+```glsl
+vec6 over_comp(vec6 src, vec6 dest) {
+ vec6 result;
+ result.r = src.r + (1.0 - src.a_r) * dest.r;
+ result.g = src.g + (1.0 - src.a_g) * dest.g;
+ result.b = src.b + (1.0 - src.a_b) * dest.b;
+ result.a_r = src.a_r + (1.0 - src.a_r) * dest.a_r;
+ result.a_g = src.a_g + (1.0 - src.a_g) * dest.a_g;
+ result.a_b = src.a_b + (1.0 - src.a_b) * dest.a_b;
+ return result;
+}
+```
+
+and a component-alpha-aware operator "in":
+
+```glsl
+vec6 in_comp(vec6 src, vec6 mask) {
+ vec6 result;
+ result.r = src.r * mask.a_r;
+ result.g = src.g * mask.a_g;
+ result.b = src.b * mask.a_b;
+ result.a_r = src.a_r * mask.a_r;
+ result.a_g = src.a_g * mask.a_g;
+ result.a_b = src.a_b * mask.a_b;
+ return result;
+}
+```
+
+and even a component-alpha-aware version of `textblend`:
+
+```glsl
+vec6 textblend_comp(vec6 text_color, vec6 mask, vec6 dest) {
+ return over_comp(in_comp(text_color, mask), dest);
+}
+```
+
+This results in the following set of equations:
+
+```glsl
+result.r = text_color.r * mask.a_r + (1.0 - text_color.a_r * mask.a_r) * dest.r;
+result.g = text_color.g * mask.a_g + (1.0 - text_color.a_g * mask.a_g) * dest.g;
+result.b = text_color.b * mask.a_b + (1.0 - text_color.a_b * mask.a_b) * dest.b;
+result.a_r = text_color.a_r * mask.a_r + (1.0 - text_color.a_r * mask.a_r) * dest.a_r;
+result.a_g = text_color.a_g * mask.a_g + (1.0 - text_color.a_g * mask.a_g) * dest.a_g;
+result.a_b = text_color.a_b * mask.a_b + (1.0 - text_color.a_b * mask.a_b) * dest.a_b;
+```
+
+### Back to the real world
+
+If we want to transfer the component alpha blend equation into the real world, we need to make a few small changes:
+
+ - Our text color only needs one alpha value.
+ So we'll replace all instances of `text_color.a_r/g/b` with `text_color.a`.
+ - We're currently not making use of the mask's `r`, `g` and `b` values, only of the `a_r`, `a_g` and `a_b` values.
+ So in the real world, we can use the rgb channels of `mask` to store those component alphas and
+ replace `mask.a_r/g/b` with `mask.r/g/b`.
+
+These two changes give us:
+
+```glsl
+result.r = text_color.r * mask.r + (1.0 - text_color.a * mask.r) * dest.r;
+result.g = text_color.g * mask.g + (1.0 - text_color.a * mask.g) * dest.g;
+result.b = text_color.b * mask.b + (1.0 - text_color.a * mask.b) * dest.b;
+result.a_r = text_color.a * mask.r + (1.0 - text_color.a * mask.r) * dest.a_r;
+result.a_g = text_color.a * mask.g + (1.0 - text_color.a * mask.g) * dest.a_g;
+result.a_b = text_color.a * mask.b + (1.0 - text_color.a * mask.b) * dest.a_b;
+```
+
+There's a third change we need to make:
+
+ - We're rendering to a destination surface that only has one alpha channel instead of three.
+ So `dest.a_r/g/b` and `result.a_r/g/b` will need to become `dest.a` and `result.a`.
+
+This creates a problem: We're currently assigning different values to `result.a_r`, `result.a_g` and `result.a_b`.
+Which of them should we use to compute `result.a`?
+
+This question does not have an answer. One alpha value per pixel is simply not sufficient
+to express the same information as three alpha values.
+
+However, see what happens if the destination is already opaque:
+
+We have `dest.a_r == 1`, `dest.a_g == 1`, and `dest.a_b == 1`.
+
+```
+result.a_r = text_color.a * mask.r + (1 - text_color.a * mask.r) * dest.a_r
+ = text_color.a * mask.r + (1 - text_color.a * mask.r) * 1
+ = text_color.a * mask.r + 1 - text_color.a * mask.r
+ = 1
+same for result.a_g and result.a_b
+```
+
+In other words, for opaque destinations, it doesn't matter what which channel of the mask we use when computing `result.a`, the result will always be completely opaque anyways. In WebRender we just pick `mask.g` (or rather,
+have font rasterization set `mask.a` to the value of `mask.g`) because it's as good as any.
+
+The takeaway here is: **Subpixel text blending is only supported for opaque destinations.** Attempting to render subpixel
+text into partially transparent destinations will result in bad alpha values. Or rather, it will result in alpha values which
+are not anticipated by the r, g, and b values in the same pixel, so that subsequent blend operations, which will mix r and a values
+from the same pixel, will produce incorrect colors.
+
+Here's the final subpixel blend function:
+
+```glsl
+vec4 subpixeltextblend(vec4 text_color, vec4 mask, vec4 dest) {
+ vec4 result;
+ result.r = text_color.r * mask.r + (1.0 - text_color.a * mask.r) * dest.r;
+ result.g = text_color.g * mask.g + (1.0 - text_color.a * mask.g) * dest.g;
+ result.b = text_color.b * mask.b + (1.0 - text_color.a * mask.b) * dest.b;
+ result.a = text_color.a * mask.a + (1.0 - text_color.a * mask.a) * dest.a;
+ return result;
+}
+```
+
+or for short:
+
+```glsl
+vec4 subpixeltextblend(vec4 text_color, vec4 mask, vec4 dest) {
+ return text_color * mask + (1.0 - text_color.a * mask) * dest;
+}
+```
+
+To recap, here's what we gained and lost by making the transition from the full-component-alpha world to the
+regular rgba world: All colors and textures now only need four values to be represented, we still use a
+component alpha mask, and the results are equivalent to the full-component-alpha result assuming that the
+destination is opaque. We lost the ability to draw to partially transparent destinations.
+
+### Making this work in OpenGL
+
+We have the complete subpixel blend function.
+Now we need to cut it into pieces and mix it with the OpenGL blend pipeline in such a way that
+the fragment shader does not need to know about the destination.
+
+Compare the equation for the red channel and the alpha channel between the two ways of text blending:
+
+```
+ single-channel alpha:
+ result.r = text_color.r * mask.a + (1.0 - text_color.a * mask.a) * dest.r
+ result.a = text_color.a * mask.a + (1.0 - text_color.a * mask.a) * dest.r
+
+ component alpha:
+ result.r = text_color.r * mask.r + (1.0 - text_color.a * mask.r) * dest.r
+ result.a = text_color.a * mask.a + (1.0 - text_color.a * mask.a) * dest.r
+```
+
+Notably, in the single-channel alpha case, all three destination color channels are multiplied with the same thing:
+`(1.0 - text_color.a * mask.a)`. This factor also happens to be "one minus `oFragColor.a`".
+So we were able to take advantage of OpenGL's `ONE_MINUS_SRC_ALPHA` blend func.
+
+In the component alpha case, we're not so lucky: Each destination color channel
+is multiplied with a different factor. We can use `ONE_MINUS_SRC_COLOR` instead,
+and output `text_color.a * mask` from our fragment shader.
+But then there's still the problem that the first summand of the computation for `result.r` uses
+`text_color.r * mask.r` and the second summand uses `text_color.a * mask.r`.
+
+There are multiple ways to deal with this. They are:
+
+ 1. Making use of `glBlendColor` and the `GL_CONSTANT_COLOR` blend func.
+ 2. Using a two-pass method.
+ 3. Using "dual source blending".
+
+Let's look at them in order.
+
+#### 1. Subpixel text blending in OpenGL using `glBlendColor`
+
+In this approach we return `text_color.a * mask` from the shader.
+Then we set the blend color to `text_color / text_color.a` and use `GL_CONSTANT_COLOR` as the source blendfunc.
+This results in the following blend equation:
+
+```
+result.r = (text_color.r / text_color.a) * oFragColor.r + (1 - oFragColor.r) * dest.r;
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^ ^^^^^^^^^^^^^^^^^
+ | | |
+ +--gl::CONSTANT_COLOR | +-- gl::ONE_MINUS_SRC_COLOR
+ |
+ +-- gl::FUNC_ADD
+
+ = (text_color.r / text_color.a) * (text_color.a * mask.r) + (1 - (text_color.a * mask.r)) * dest.r
+ = text_color.r * mask.r + (1 - text_color.a * mask.r) * dest.r
+```
+
+At the very beginning of this document, we defined `text_color` as the *premultiplied* text color.
+So instead of actually doing the calculation `text_color.r / text_color.a` when specifying the blend color,
+we really just want to use the *unpremultiplied* text color in that place.
+That's usually the representation we start with anyway.
+
+#### 2. Two-pass subpixel blending in OpenGL
+
+The `glBlendColor` method has the disadvantage that the text color is part of the OpenGL state.
+So if we want to draw text with different colors, we have two use separate batches / draw calls
+to draw the differently-colored parts of text.
+
+Alternatively, we can use a two-pass method which avoids the need to use the `GL_CONSTANT_COLOR` blend func:
+
+ - The first pass outputs `text_color.a * mask` from the fragment shader and
+ uses `gl::ZERO, gl::ONE_MINUS_SRC_COLOR` as the glBlendFuncs. This achieves:
+
+```
+oFragColor = text_color.a * mask;
+
+result_after_pass0.r = 0 * oFragColor.r + (1 - oFragColor.r) * dest.r
+ = (1 - text_color.a * mask.r) * dest.r
+
+result_after_pass0.g = 0 * oFragColor.g + (1 - oFragColor.g) * dest.r
+ = (1 - text_color.a * mask.r) * dest.r
+
+...
+```
+
+ - The second pass outputs `text_color * mask` from the fragment shader and uses
+ `gl::ONE, gl::ONE` as the glBlendFuncs. This results in the correct overall blend equation.
+
+```
+oFragColor = text_color * mask;
+
+result_after_pass1.r
+ = 1 * oFragColor.r + 1 * result_after_pass0.r
+ = text_color.r * mask.r + result_after_pass0.r
+ = text_color.r * mask.r + (1 - text_color.a * mask.r) * dest.r
+```
+
+#### 3. Dual source subpixel blending in OpenGL
+
+The third approach is similar to the second approach, but makes use of the [`ARB_blend_func_extended`](https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_blend_func_extended.txt) extension
+in order to fold the two passes into one:
+Instead of outputting the two different colors in two separate passes, we output them from the same pass,
+as two separate fragment shader outputs.
+Those outputs can then be treated as two different sources in the blend equation.
+
+## Subpixel Text Rendering to Transparent Destinations with a Background Color Hint
+
+### Motivation
+
+As we've seen in the previous section, subpixel text drawing has the limitation that it only works on opaque destinations.
+
+In other words, if you use the `subpixeltextblend` function to draw something to a transparent surface,
+and then composite that surface onto on opaque background,
+the result will generally be different from drawing the text directly onto the opaque background.
+
+Let's express that inequality in code.
+
+```
+ - vec4 text_color
+ - vec4 mask
+ - vec4 transparency = vec4(0.0, 0.0, 0.0, 0.0)
+ - vec4 background with background.a == 1.0
+
+over(subpixeltextblend(text_color, mask, transparency), background).rgb
+ is, in general, not equal to
+subpixeltextblend(text_color, mask, background).rgb
+```
+
+However, one interesting observation is that if the background is black, the two *are* equal:
+
+```
+vec4 black = vec4(0.0, 0.0, 0.0, 1.0);
+
+over(subpixeltextblend(text_color, mask, transparency), black).r
+ = subpixeltextblend(text_color, mask, transparency).r +
+ (1 - subpixeltextblend(text_color, mask, transparency).a) * black.r
+ = subpixeltextblend(text_color, mask, transparency).r +
+ (1 - subpixeltextblend(text_color, mask, transparency).a) * 0
+ = subpixeltextblend(text_color, mask, transparency).r
+ = text_color.r * mask.r + (1 - text_color.a * mask.r) * transparency.r
+ = text_color.r * mask.r + (1 - text_color.a * mask.r) * 0
+ = text_color.r * mask.r + (1 - text_color.a * mask.r) * black.r
+ = subpixeltextblend(text_color, mask, black).r
+```
+
+So it works out for black backgrounds. The further your *actual* background color gets away from black,
+the more incorrect your result will be.
+
+If it works for black, is there a way to make it work for other colors?
+This is the motivating question for this third way of text blending:
+
+We want to be able to specify an *estimated background color*, and have a blending function
+`vec4 subpixeltextblend_withbgcolor(vec4 text_color, vec4 mask, vec4 bg_color, vec4 dest)`,
+in such a way that the error we get by using an intermediate surface is somehow in relation
+to the error we made when estimating the background color. In particular, if we estimated
+the background color perfectly, we want the intermediate surface to go unnoticed.
+
+Expressed as code:
+
+```
+over(subpixeltextblend_withbgcolor(text_color, mask, bg_color, transparency), bg_color)
+ should always be equal to
+subpixeltextblend(text_color, mask, bg_color)
+```
+
+This is one of three constraints we'd like `subpixeltextblend_withbgcolor` to satisfy.
+
+The next constraint is the following: If `dest` is already opaque, `subpixeltextblend_withbgcolor`
+should have the same results as `subpixeltextblend`, and the background color hint should be ignored.
+
+```
+ If dest.a == 1.0,
+subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest)
+ should always be equal to
+subpixeltextblend(text_color, mask, dest)
+```
+
+And there's a third condition we'd like it to fulfill:
+In places where the mask is zero, the destination should be unaffected.
+
+```
+subpixeltextblend_withbgcolor(text_color, transparency, bg_color, dest)
+ should always be equal to
+dest
+```
+
+### Use cases
+
+The primary use case for such a blend method is text on top of vibrant areas of a window on macOS.
+
+Vibrant backgrounds with behind-window blending are computed by the window server, and they are tinted
+in a color that's based on the chosen vibrancy type.
+
+The window's rgba buffer is transparent in the vibrant areas. Window contents, even text, are drawn onto
+that transparent rgba buffer. Then the window server composites the window onto an opaque backdrop.
+So the results on the screen are computed as follows:
+
+```glsl
+window_buffer_pixel = subpixeltextblend_withbgcolor(text_color, mask, bg_color, transparency);
+screen_pixel = over(window_buffer_pixel, window_backdrop);
+```
+
+### Prior art
+
+Apple has implemented such a method of text blending in CoreGraphics, specifically for rendering text onto vibrant backgrounds.
+It's hidden behind the private API `CGContextSetFontSmoothingBackgroundColor` and is called by AppKit internally before
+calling the `-[NSView drawRect:]` method of your `NSVisualEffectView`, with the appropriate font smoothing background color
+for the vibrancy type of that view.
+
+I'm not aware of any public documentation of this way of text blending.
+It seems to be considered an implementation detail by Apple, and is probably hidden by default because it can be a footgun:
+If the font smoothing background color you specify is very different from the actual background that our surface is placed
+on top of, the text will look glitchy.
+
+### Deriving the blending function from first principles
+
+Before we dive into the math, let's repeat our goal once more.
+
+We want to create a blending function of the form
+`vec4 subpixeltextblend_withbgcolor(vec4 text_color, vec4 mask, vec4 bg_color, vec4 dest)`
+(with `bg_color` being an opaque color)
+which satisfies the following three constraints:
+
+```
+Constraint I:
+ over(subpixeltextblend_withbgcolor(text_color, mask, bg_color, transparency), bg_color)
+ should always be equal to
+ subpixeltextblend(text_color, mask, bg_color)
+
+Constraint II:
+ If dest.a == 1.0,
+ subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest)
+ should always be equal to
+ subpixeltextblend(text_color, mask, dest)
+
+Constraint II:
+ subpixeltextblend_withbgcolor(text_color, transparency, bg_color, dest)
+ should always be equal to
+ dest
+```
+
+Constraint I and constraint II are about what happens depending on the destination's alpha.
+In particular: If the destination is completely transparent, we should blend into the
+estimated background color, and if it's completely opaque, we should blend into the destination color.
+In fact, we really want to blend into `over(dest, bg_color)`: we want `bg_color` to be used
+as a backdrop *behind* the current destination. So let's combine constraints I and II into a new
+constraint IV:
+
+```
+Constraint IV:
+ over(subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest), bg_color)
+ should always be equal to
+ subpixeltextblend(text_color, mask, over(dest, bg_color))
+```
+
+Let's look at just the left side of that equation and rejiggle it a bit:
+
+```
+over(subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest), bg_color).r
+ = subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest).r +
+ (1 - subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest).a) * bg_color.r
+
+<=>
+
+over(subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest), bg_color).r -
+(1 - subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest).a) * bg_color.r
+ = subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest).r
+```
+
+Now insert the right side of constraint IV:
+
+```
+subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest).r
+ = over(subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest), bg_color).r -
+ (1 - subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest).a) * bg_color.r
+ = subpixeltextblend(text_color, mask, over(dest, bg_color)).r -
+ (1 - subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest).a) * bg_color.r
+```
+
+Our blend function is almost finished. We just need select an alpha for our result.
+Constraints I, II and IV don't really care about the alpha value. But constraint III requires that:
+
+```
+ subpixeltextblend_withbgcolor(text_color, transparency, bg_color, dest).a
+ should always be equal to
+ dest.a
+```
+
+so the computation of the alpha value somehow needs to take into account the mask.
+
+Let's say we have an unknown function `make_alpha(text_color.a, mask)` which returns
+a number between 0 and 1 and which is 0 if the mask is entirely zero, and let's defer
+the actual implementation of that function until later.
+
+Now we can define the alpha of our overall function using the `over` function:
+
+```
+subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest).a
+ := make_alpha(text_color.a, mask) + (1 - make_alpha(text_color.a, mask)) * dest.a
+```
+
+We can plug this in to our previous result:
+
+```
+subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest).r
+ = subpixeltextblend(text_color, mask, over(dest, bg_color)).r
+ - (1 - subpixeltextblend_withbgcolor(text_color, mask, bg_color, dest).a) * bg_color.r
+ = subpixeltextblend(text_color, mask, over(dest, bg_color)).r
+ - (1 - (make_alpha(text_color.a, mask) +
+ (1 - make_alpha(text_color.a, mask)) * dest.a)) * bg_color.r
+ = text_color.r * mask.r + (1 - text_color.a * mask.r) * over(dest, bg_color).r
+ - (1 - (make_alpha(text_color.a, mask)
+ + (1 - make_alpha(text_color.a, mask)) * dest.a)) * bg_color.r
+ = text_color.r * mask.r
+ + (1 - text_color.a * mask.r) * (dest.r + (1 - dest.a) * bg_color.r)
+ - (1 - (make_alpha(text_color.a, mask)
+ + (1 - make_alpha(text_color.a, mask)) * dest.a)) * bg_color.r
+ = text_color.r * mask.r
+ + (1 - text_color.a * mask.r) * (dest.r + (1 - dest.a) * bg_color.r)
+ - (1 - (make_alpha(text_color.a, mask)
+ + (1 - make_alpha(text_color.a, mask)) * dest.a)) * bg_color.r
+ = text_color.r * mask.r
+ + (dest.r + (1 - dest.a) * bg_color.r)
+ - (text_color.a * mask.r) * (dest.r + (1 - dest.a) * bg_color.r)
+ - (1 - make_alpha(text_color.a, mask)
+ - (1 - make_alpha(text_color.a, mask)) * dest.a) * bg_color.r
+ = text_color.r * mask.r
+ + dest.r + (1 - dest.a) * bg_color.r
+ - text_color.a * mask.r * dest.r
+ - text_color.a * mask.r * (1 - dest.a) * bg_color.r
+ - (1 - make_alpha(text_color.a, mask)
+ - (1 - make_alpha(text_color.a, mask)) * dest.a) * bg_color.r
+ = text_color.r * mask.r
+ + dest.r + (1 - dest.a) * bg_color.r
+ - text_color.a * mask.r * dest.r
+ - text_color.a * mask.r * (1 - dest.a) * bg_color.r
+ - ((1 - make_alpha(text_color.a, mask)) * 1
+ - (1 - make_alpha(text_color.a, mask)) * dest.a) * bg_color.r
+ = text_color.r * mask.r
+ + dest.r + (1 - dest.a) * bg_color.r
+ - text_color.a * mask.r * dest.r
+ - text_color.a * mask.r * (1 - dest.a) * bg_color.r
+ - ((1 - make_alpha(text_color.a, mask)) * (1 - dest.a)) * bg_color.r
+ = text_color.r * mask.r
+ + dest.r - text_color.a * mask.r * dest.r
+ + (1 - dest.a) * bg_color.r
+ - text_color.a * mask.r * (1 - dest.a) * bg_color.r
+ - (1 - make_alpha(text_color.a, mask)) * (1 - dest.a) * bg_color.r
+ = text_color.r * mask.r
+ + (1 - text_color.a * mask.r) * dest.r
+ + (1 - dest.a) * bg_color.r
+ - text_color.a * mask.r * (1 - dest.a) * bg_color.r
+ - (1 - make_alpha(text_color.a, mask)) * (1 - dest.a) * bg_color.r
+ = text_color.r * mask.r
+ + (1 - text_color.a * mask.r) * dest.r
+ + (1 - text_color.a * mask.r) * (1 - dest.a) * bg_color.r
+ - (1 - make_alpha(text_color.a, mask)) * (1 - dest.a) * bg_color.r
+ = text_color.r * mask.r
+ + (1 - text_color.a * mask.r) * dest.r
+ + ((1 - text_color.a * mask.r)
+ - (1 - make_alpha(text_color.a, mask))) * (1 - dest.a) * bg_color.r
+ = text_color.r * mask.r
+ + (1 - text_color.a * mask.r) * dest.r
+ + (1 - text_color.a * mask.r
+ - 1 + make_alpha(text_color.a, mask)) * (1 - dest.a) * bg_color.r
+ = text_color.r * mask.r
+ + (1 - text_color.a * mask.r) * dest.r
+ + (make_alpha(text_color.a, mask) - text_color.a * mask.r) * (1 - dest.a) * bg_color.r
+```
+
+We now have a term of the form `A + B + C`, with `A` and `B` being guaranteed to
+be between zero and one.
+
+We also want `C` to be between zero and one.
+We can use this restriction to help us decide on an implementation of `make_alpha`.
+
+If we define `make_alpha` as
+
+```glsl
+float make_alpha(text_color_a, mask) {
+ float max_rgb = max(max(mask.r, mask.g), mask.b);
+ return text_color_a * max_rgb;
+}
+```
+
+, then `(make_alpha(text_color.a, mask) - text_color.a * mask.r)` becomes
+`(text_color.a * max(max(mask.r, mask.g), mask.b) - text_color.a * mask.r)`, which is
+`text_color.a * (max(max(mask.r, mask.g), mask.b) - mask.r)`, and the subtraction will
+always yield something that's greater or equal to zero for r, g, and b,
+because we will subtract each channel from the maximum of the channels.
+
+Putting this all together, we have:
+
+```glsl
+vec4 subpixeltextblend_withbgcolor(vec4 text_color, vec4 mask, vec4 bg_color, vec4 dest) {
+ float max_rgb = max(max(mask.r, mask.g), mask.b);
+ vec4 result;
+ result.r = text_color.r * mask.r + (1 - text_color.a * mask.r) * dest.r +
+ text_color.a * bg_color.r * (max_rgb - mask.r) * (1 - dest.a);
+ result.g = text_color.g * mask.g + (1 - text_color.a * mask.g) * dest.g +
+ text_color.a * bg_color.g * (max_rgb - mask.g) * (1 - dest.a);
+ result.b = text_color.b * mask.b + (1 - text_color.a * mask.b) * dest.b +
+ text_color.a * bg_color.b * (max_rgb - mask.b) * (1 - dest.a);
+ result.a = text_color.a * max_rgb + (1 - text_color.a * max_rgb) * dest.a;
+ return result;
+}
+```
+
+This is the final form of this blend function. It satisfies all of the four constraints.
+
+### Implementing it with OpenGL
+
+Our color channel equations consist of three pieces:
+
+ - `text_color.r * mask.r`, which simply gets added to the rest.
+ - `(1 - text_color.a * mask.r) * dest.r`, a factor which gets multiplied with the destination color.
+ - `text_color.a * bg_color.r * (max_rgb - mask.r) * (1 - dest.a)`, a factor which gets multiplied
+ with "one minus destination alpha".
+
+We will need three passes. Each pass modifies the color channels in the destination.
+This means that the part that uses `dest.r` needs to be applied first.
+Then we can apply the part that uses `1 - dest.a`.
+(This means that the first pass needs to leave `dest.a` untouched.)
+And the final pass can apply the `result.a` equation and modify `dest.a`.
+
+```
+pub fn set_blend_mode_subpixel_with_bg_color_pass0(&self) {
+ self.gl.blend_func_separate(gl::ZERO, gl::ONE_MINUS_SRC_COLOR, gl::ZERO, gl::ONE);
+}
+pub fn set_blend_mode_subpixel_with_bg_color_pass1(&self) {
+ self.gl.blend_func_separate(gl::ONE_MINUS_DST_ALPHA, gl::ONE, gl::ZERO, gl::ONE);
+}
+pub fn set_blend_mode_subpixel_with_bg_color_pass2(&self) {
+ self.gl.blend_func_separate(gl::ONE, gl::ONE, gl::ONE, gl::ONE_MINUS_SRC_ALPHA);
+}
+
+Pass0:
+ oFragColor = vec4(text.color.a) * mask;
+Pass1:
+ oFragColor = vec4(text.color.a) * text.bg_color * (vec4(mask.a) - mask);
+Pass2:
+ oFragColor = text.color * mask;
+
+result_after_pass0.r = 0 * (text_color.a * mask.r) + (1 - text_color.a * mask.r) * dest.r
+result_after_pass0.a = 0 * (text_color.a * mask.a) + 1 * dest.a
+
+result_after_pass1.r = (1 - result_after_pass0.a) * (text_color.a * (mask.max_rgb - mask.r) * bg_color.r) + 1 * result_after_pass0.r
+result_after_pass1.a = 0 * (text_color.a * (mask.max_rgb - mask.a) * bg_color.a) + 1 * result_after_pass0.a
+
+result_after_pass2.r = 1 * (text_color.r * mask.r) + 1 * result_after_pass1.r
+result_after_pass2.a = 1 * (text_color.a * mask.max_rgb) + (1 - text_color.a * mask.max_rgb) * result_after_pass1.a
+```
+
+Instead of computing `max_rgb` in the shader, we can just require the font rasterization code to fill
+`mask.a` with the `max_rgb` value.
+
diff --git a/gfx/wr/webrender/res/Proggy.ttf b/gfx/wr/webrender/res/Proggy.ttf
new file mode 100644
index 0000000000..308d3e1ac9
--- /dev/null
+++ b/gfx/wr/webrender/res/Proggy.ttf
Binary files differ
diff --git a/gfx/wr/webrender/res/area-lut.tga b/gfx/wr/webrender/res/area-lut.tga
new file mode 100644
index 0000000000..5edcddc3d1
--- /dev/null
+++ b/gfx/wr/webrender/res/area-lut.tga
Binary files differ
diff --git a/gfx/wr/webrender/res/base.glsl b/gfx/wr/webrender/res/base.glsl
new file mode 100644
index 0000000000..e381ff6ca9
--- /dev/null
+++ b/gfx/wr/webrender/res/base.glsl
@@ -0,0 +1,70 @@
+/* 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/. */
+
+#if defined(GL_ES)
+ #if GL_ES == 1
+ // Sampler default precision is lowp on mobile GPUs.
+ // This causes RGBA32F texture data to be clamped to 16 bit floats on some GPUs (e.g. Mali-T880).
+ // Define highp precision macro to allow lossless FLOAT texture sampling.
+ #define HIGHP_SAMPLER_FLOAT highp
+
+ // Default int precision in GLES 3 is highp (32 bits) in vertex shaders
+ // and mediump (16 bits) in fragment shaders. If an int is being used as
+ // a texel address in a fragment shader it, and therefore requires > 16
+ // bits, it must be qualified with this.
+ #define HIGHP_FS_ADDRESS highp
+
+ // texelFetchOffset is buggy on some Android GPUs (see issue #1694).
+ // Fallback to texelFetch on mobile GPUs.
+ #define TEXEL_FETCH(sampler, position, lod, offset) texelFetch(sampler, position + offset, lod)
+ #else
+ #define HIGHP_SAMPLER_FLOAT
+ #define HIGHP_FS_ADDRESS
+ #define TEXEL_FETCH(sampler, position, lod, offset) texelFetchOffset(sampler, position, lod, offset)
+ #endif
+#else
+ #define HIGHP_SAMPLER_FLOAT
+ #define HIGHP_FS_ADDRESS
+ #if defined(PLATFORM_MACOS) && !defined(SWGL)
+ // texelFetchOffset introduces a variety of shader compilation bugs on macOS Intel so avoid it.
+ #define TEXEL_FETCH(sampler, position, lod, offset) texelFetch(sampler, position + offset, lod)
+ #else
+ #define TEXEL_FETCH(sampler, position, lod, offset) texelFetchOffset(sampler, position, lod, offset)
+ #endif
+#endif
+
+#ifdef SWGL
+ #define SWGL_DRAW_SPAN
+ #define SWGL_CLIP_MASK
+ #define SWGL_ANTIALIAS
+ #define SWGL_BLEND
+ #define SWGL_CLIP_DIST
+#endif
+
+#ifdef WR_VERTEX_SHADER
+ #ifdef SWGL
+ // Annotate a vertex attribute as being flat per each drawn primitive instance.
+ // SWGL can use this information to avoid redundantly loading the attribute in all SIMD lanes.
+ #define PER_INSTANCE flat
+ #else
+ #define PER_INSTANCE
+ #endif
+
+ #if __VERSION__ != 100
+ #define varying out
+ #define attribute in
+ #endif
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+ precision highp float;
+ #if __VERSION__ != 100
+ #define varying in
+ #endif
+#endif
+
+// Flat interpolation is not supported on ESSL 1
+#if __VERSION__ == 100
+ #define flat
+#endif
diff --git a/gfx/wr/webrender/res/blend.glsl b/gfx/wr/webrender/res/blend.glsl
new file mode 100644
index 0000000000..2deed01143
--- /dev/null
+++ b/gfx/wr/webrender/res/blend.glsl
@@ -0,0 +1,238 @@
+/* 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/. */
+
+#define COMPONENT_TRANSFER_IDENTITY 0
+#define COMPONENT_TRANSFER_TABLE 1
+#define COMPONENT_TRANSFER_DISCRETE 2
+#define COMPONENT_TRANSFER_LINEAR 3
+#define COMPONENT_TRANSFER_GAMMA 4
+
+// Must be kept in sync with `Filter::as_int` in internal_types.rs
+// Not all filters are defined here because some filter use different shaders.
+#define FILTER_CONTRAST 0
+#define FILTER_GRAYSCALE 1
+#define FILTER_HUE_ROTATE 2
+#define FILTER_INVERT 3
+#define FILTER_SATURATE 4
+#define FILTER_SEPIA 5
+#define FILTER_BRIGHTNESS 6
+#define FILTER_COLOR_MATRIX 7
+#define FILTER_SRGB_TO_LINEAR 8
+#define FILTER_LINEAR_TO_SRGB 9
+#define FILTER_FLOOD 10
+#define FILTER_COMPONENT_TRANSFER 11
+
+#ifdef WR_VERTEX_SHADER
+void SetupFilterParams(
+ int op,
+ float amount,
+ int gpu_data_address,
+ out vec4 color_offset,
+ out mat4 color_mat,
+ out highp int table_address
+) {
+ float lumR = 0.2126;
+ float lumG = 0.7152;
+ float lumB = 0.0722;
+ float oneMinusLumR = 1.0 - lumR;
+ float oneMinusLumG = 1.0 - lumG;
+ float oneMinusLumB = 1.0 - lumB;
+ float invAmount = 1.0 - amount;
+
+ if (op == FILTER_GRAYSCALE) {
+ color_mat = mat4(
+ vec4(lumR + oneMinusLumR * invAmount, lumR - lumR * invAmount, lumR - lumR * invAmount, 0.0),
+ vec4(lumG - lumG * invAmount, lumG + oneMinusLumG * invAmount, lumG - lumG * invAmount, 0.0),
+ vec4(lumB - lumB * invAmount, lumB - lumB * invAmount, lumB + oneMinusLumB * invAmount, 0.0),
+ vec4(0.0, 0.0, 0.0, 1.0)
+ );
+ color_offset = vec4(0.0);
+ } else if (op == FILTER_HUE_ROTATE) {
+ float c = cos(amount);
+ float s = sin(amount);
+ color_mat = mat4(
+ vec4(lumR + oneMinusLumR * c - lumR * s, lumR - lumR * c + 0.143 * s, lumR - lumR * c - oneMinusLumR * s, 0.0),
+ vec4(lumG - lumG * c - lumG * s, lumG + oneMinusLumG * c + 0.140 * s, lumG - lumG * c + lumG * s, 0.0),
+ vec4(lumB - lumB * c + oneMinusLumB * s, lumB - lumB * c - 0.283 * s, lumB + oneMinusLumB * c + lumB * s, 0.0),
+ vec4(0.0, 0.0, 0.0, 1.0)
+ );
+ color_offset = vec4(0.0);
+ } else if (op == FILTER_SATURATE) {
+ color_mat = mat4(
+ vec4(invAmount * lumR + amount, invAmount * lumR, invAmount * lumR, 0.0),
+ vec4(invAmount * lumG, invAmount * lumG + amount, invAmount * lumG, 0.0),
+ vec4(invAmount * lumB, invAmount * lumB, invAmount * lumB + amount, 0.0),
+ vec4(0.0, 0.0, 0.0, 1.0)
+ );
+ color_offset = vec4(0.0);
+ } else if (op == FILTER_SEPIA) {
+ color_mat = mat4(
+ vec4(0.393 + 0.607 * invAmount, 0.349 - 0.349 * invAmount, 0.272 - 0.272 * invAmount, 0.0),
+ vec4(0.769 - 0.769 * invAmount, 0.686 + 0.314 * invAmount, 0.534 - 0.534 * invAmount, 0.0),
+ vec4(0.189 - 0.189 * invAmount, 0.168 - 0.168 * invAmount, 0.131 + 0.869 * invAmount, 0.0),
+ vec4(0.0, 0.0, 0.0, 1.0)
+ );
+ color_offset = vec4(0.0);
+ } else if (op == FILTER_COLOR_MATRIX) {
+ vec4 mat_data[4] = fetch_from_gpu_cache_4(gpu_data_address);
+ vec4 offset_data = fetch_from_gpu_cache_1(gpu_data_address + 4);
+ color_mat = mat4(mat_data[0], mat_data[1], mat_data[2], mat_data[3]);
+ color_offset = offset_data;
+ } else if (op == FILTER_COMPONENT_TRANSFER) {
+ table_address = gpu_data_address;
+ } else if (op == FILTER_FLOOD) {
+ color_offset = fetch_from_gpu_cache_1(gpu_data_address);
+ }
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+vec3 Contrast(vec3 Cs, float amount) {
+ return clamp(Cs.rgb * amount - 0.5 * amount + 0.5, 0.0, 1.0);
+}
+
+vec3 Invert(vec3 Cs, float amount) {
+ return mix(Cs.rgb, vec3(1.0) - Cs.rgb, amount);
+}
+
+vec3 Brightness(vec3 Cs, float amount) {
+ // Apply the brightness factor.
+ // Resulting color needs to be clamped to output range
+ // since we are pre-multiplying alpha in the shader.
+ return clamp(Cs.rgb * amount, vec3(0.0), vec3(1.0));
+}
+
+// Based on the Gecko's implementation in
+// https://hg.mozilla.org/mozilla-central/file/91b4c3687d75/gfx/src/FilterSupport.cpp#l24
+// These could be made faster by sampling a lookup table stored in a float texture
+// with linear interpolation.
+
+vec3 SrgbToLinear(vec3 color) {
+ vec3 c1 = color / 12.92;
+ vec3 c2 = pow(color / 1.055 + vec3(0.055 / 1.055), vec3(2.4));
+ return if_then_else(lessThanEqual(color, vec3(0.04045)), c1, c2);
+}
+
+vec3 LinearToSrgb(vec3 color) {
+ vec3 c1 = color * 12.92;
+ vec3 c2 = vec3(1.055) * pow(color, vec3(1.0 / 2.4)) - vec3(0.055);
+ return if_then_else(lessThanEqual(color, vec3(0.0031308)), c1, c2);
+}
+
+// This function has to be factored out due to the following issue:
+// https://github.com/servo/webrender/wiki/Driver-issues#bug-1532245---switch-statement-inside-control-flow-inside-switch-statement-fails-to-compile-on-some-android-phones
+// (and now the words "default: default:" so angle_shader_validation.rs passes)
+vec4 ComponentTransfer(vec4 colora, vec4 vfuncs, highp int table_address) {
+ // We push a different amount of data to the gpu cache depending on the
+ // function type.
+ // Identity => 0 blocks
+ // Table/Discrete => 64 blocks (256 values)
+ // Linear => 1 block (2 values)
+ // Gamma => 1 block (3 values)
+ // We loop through the color components and increment the offset (for the
+ // next color component) into the gpu cache based on how many blocks that
+ // function type put into the gpu cache.
+ // Table/Discrete use a 256 entry look up table.
+ // Linear/Gamma are a simple calculation.
+
+ // Both offset and k must be marked as highp due to a Adreno 3xx bug likely
+ // to do with converting between precisions (as they would otherwise be
+ // promoted when adding to table_address).
+ highp int offset = 0;
+ highp int k;
+
+ vec4 texel;
+
+ // Dynamically indexing a vector is buggy on some platforms, so use a temporary array
+ int[4] funcs = int[4](int(vfuncs.r), int(vfuncs.g), int(vfuncs.b), int(vfuncs.a));
+ for (int i = 0; i < 4; i++) {
+ switch (funcs[i]) {
+ case COMPONENT_TRANSFER_IDENTITY:
+ break;
+ case COMPONENT_TRANSFER_TABLE:
+ case COMPONENT_TRANSFER_DISCRETE: {
+ // fetch value from lookup table
+ k = int(floor(colora[i]*255.0 + 0.5));
+ texel = fetch_from_gpu_cache_1(table_address + offset + k/4);
+ colora[i] = clamp(texel[k % 4], 0.0, 1.0);
+ // offset plus 256/4 blocks
+ offset = offset + 64;
+ break;
+ }
+ case COMPONENT_TRANSFER_LINEAR: {
+ // fetch the two values for use in the linear equation
+ texel = fetch_from_gpu_cache_1(table_address + offset);
+ colora[i] = clamp(texel[0] * colora[i] + texel[1], 0.0, 1.0);
+ // offset plus 1 block
+ offset = offset + 1;
+ break;
+ }
+ case COMPONENT_TRANSFER_GAMMA: {
+ // fetch the three values for use in the gamma equation
+ texel = fetch_from_gpu_cache_1(table_address + offset);
+ colora[i] = clamp(texel[0] * pow(colora[i], texel[1]) + texel[2], 0.0, 1.0);
+ // offset plus 1 block
+ offset = offset + 1;
+ break;
+ }
+ default:
+ // shouldn't happen
+ break;
+ }
+ }
+ return colora;
+}
+
+void CalculateFilter(
+ vec4 Cs,
+ int op,
+ float amount,
+ highp int table_address,
+ vec4 color_offset,
+ mat4 color_mat,
+ vec4 v_funcs,
+ out vec3 color,
+ out float alpha
+) {
+ // Un-premultiply the input.
+ alpha = Cs.a;
+ color = alpha != 0.0 ? Cs.rgb / alpha : Cs.rgb;
+
+ switch (op) {
+ case FILTER_CONTRAST:
+ color = Contrast(color, amount);
+ break;
+ case FILTER_INVERT:
+ color = Invert(color, amount);
+ break;
+ case FILTER_BRIGHTNESS:
+ color = Brightness(color, amount);
+ break;
+ case FILTER_SRGB_TO_LINEAR:
+ color = SrgbToLinear(color);
+ break;
+ case FILTER_LINEAR_TO_SRGB:
+ color = LinearToSrgb(color);
+ break;
+ case FILTER_COMPONENT_TRANSFER: {
+ // Get the unpremultiplied color with alpha.
+ vec4 colora = vec4(color, alpha);
+ colora = ComponentTransfer(colora, v_funcs, table_address);
+ color = colora.rgb;
+ alpha = colora.a;
+ break;
+ }
+ case FILTER_FLOOD:
+ color = color_offset.rgb;
+ alpha = color_offset.a;
+ break;
+ default:
+ // Color matrix type filters (sepia, hue-rotate, etc...)
+ vec4 result = color_mat * vec4(color, alpha) + color_offset;
+ result = clamp(result, vec4(0.0), vec4(1.0));
+ color = result.rgb;
+ alpha = result.a;
+ }
+}
+#endif
diff --git a/gfx/wr/webrender/res/brush.glsl b/gfx/wr/webrender/res/brush.glsl
new file mode 100644
index 0000000000..1ba5685781
--- /dev/null
+++ b/gfx/wr/webrender/res/brush.glsl
@@ -0,0 +1,256 @@
+/* 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/. */
+
+/// # Brush vertex shaders memory layout
+///
+/// The overall memory layout is the same for all brush shaders.
+///
+/// The vertex shader receives a minimal amount of data from vertex attributes (packed into a single
+/// ivec4 per instance) and the rest is fetched from various uniform samplers using offsets decoded
+/// from the vertex attributes.
+///
+/// The diagram below shows the the various pieces of data fectched in the vertex shader:
+///
+///```ascii
+/// (sPrimitiveHeadersI)
+/// (VBO) +-----------------------+
+/// +----------------------------+ +----------------------------> | Int header |
+/// | Instance vertex attributes | | (sPrimitiveHeadersF) | |
+/// | | | +---------------------+ | z |
+/// | x: prim_header_address +-------+---> | Float header | | specific_address +-----+
+/// | y: picture_task_address +---------+ | | | transform_address +---+ |
+/// | clip_address +-----+ | | local_rect | | user_data | | |
+/// | z: flags | | | | local_clip_rect | +-----------------------+ | |
+/// | segment_index | | | +---------------------+ | |
+/// | w: resource_address +--+ | | | |
+/// +----------------------------+ | | | (sGpuCache) | |
+/// | | | (sGpuCache) +------------+ | |
+/// | | | +---------------+ | Transform | <--------+ |
+/// (sGpuCache) | | +-> | Picture task | +------------+ |
+/// +-------------+ | | | | |
+/// | Resource | <---+ | | ... | |
+/// | | | +---------------+ +--------------------------------+
+/// | | | |
+/// +-------------+ | (sGpuCache) v (sGpuCache)
+/// | +---------------+ +--------------+---------------+-+-+
+/// +-----> | Clip area | | Brush data | Segment data | | |
+/// | | | | | | |
+/// | ... | | ... | ... | | | ...
+/// +---------------+ +--------------+---------------+-+-+
+///```
+///
+/// - Segment data address is obtained by combining the address stored in the int header and the
+/// segment index decoded from the vertex attributes.
+/// - Resource data is optional, some brush types (such as images) store some extra data there while
+/// other brush types don't use it.
+///
+
+#if (defined(WR_FEATURE_ALPHA_PASS) || defined(WR_FEATURE_ANTIALIASING)) && !defined(SWGL_ANTIALIAS)
+varying highp vec2 v_local_pos;
+#endif
+
+#ifdef WR_VERTEX_SHADER
+
+void brush_vs(
+ VertexInfo vi,
+ int prim_address,
+ RectWithEndpoint local_rect,
+ RectWithEndpoint segment_rect,
+ ivec4 prim_user_data,
+ int specific_resource_address,
+ mat4 transform,
+ PictureTask pic_task,
+ int brush_flags,
+ vec4 segment_data
+);
+
+// Forward-declare the text vertex shader entry point which is currently
+// different from other brushes.
+void text_shader_main(
+ Instance instance,
+ PrimitiveHeader ph,
+ Transform transform,
+ PictureTask task,
+ ClipArea clip_area
+);
+
+#define VECS_PER_SEGMENT 2
+
+#define BRUSH_FLAG_PERSPECTIVE_INTERPOLATION 1
+#define BRUSH_FLAG_SEGMENT_RELATIVE 2
+#define BRUSH_FLAG_SEGMENT_REPEAT_X 4
+#define BRUSH_FLAG_SEGMENT_REPEAT_Y 8
+#define BRUSH_FLAG_SEGMENT_REPEAT_X_ROUND 16
+#define BRUSH_FLAG_SEGMENT_REPEAT_Y_ROUND 32
+#define BRUSH_FLAG_SEGMENT_NINEPATCH_MIDDLE 64
+#define BRUSH_FLAG_TEXEL_RECT 128
+#define BRUSH_FLAG_FORCE_AA 256
+
+#define INVALID_SEGMENT_INDEX 0xffff
+
+void brush_shader_main_vs(
+ Instance instance,
+ PrimitiveHeader ph,
+ Transform transform,
+ PictureTask pic_task,
+ ClipArea clip_area
+) {
+ int edge_flags = (instance.flags >> 12) & 0xf;
+ int brush_flags = instance.flags & 0xfff;
+
+ // Fetch the segment of this brush primitive we are drawing.
+ vec4 segment_data;
+ RectWithEndpoint segment_rect;
+ if (instance.segment_index == INVALID_SEGMENT_INDEX) {
+ segment_rect = ph.local_rect;
+ segment_data = vec4(0.0);
+ } else {
+ int segment_address = ph.specific_prim_address +
+ VECS_PER_SPECIFIC_BRUSH +
+ instance.segment_index * VECS_PER_SEGMENT;
+
+ vec4[2] segment_info = fetch_from_gpu_cache_2(segment_address);
+ segment_rect = RectWithEndpoint(segment_info[0].xy, segment_info[0].zw);
+ segment_rect.p0 += ph.local_rect.p0;
+ segment_rect.p1 += ph.local_rect.p0;
+ segment_data = segment_info[1];
+ }
+
+ // Most of the time this is the segment rect, but when doing the edge AA
+ // it is inflated.
+ RectWithEndpoint adjusted_segment_rect = segment_rect;
+
+ bool antialiased = !transform.is_axis_aligned || ((brush_flags & BRUSH_FLAG_FORCE_AA) != 0);
+
+ // Write the normal vertex information out.
+ if (antialiased) {
+ adjusted_segment_rect = clip_and_init_antialiasing(
+ segment_rect,
+ ph.local_rect,
+ ph.local_clip_rect,
+ edge_flags,
+ ph.z,
+ transform,
+ pic_task
+ );
+
+ // The clip was taken into account in clip_and_init_antialiasing, remove
+ // it so that it doesn't interfere with the aa.
+ ph.local_clip_rect.p0 = vec2(-1.0e16);
+ ph.local_clip_rect.p1 = vec2(1.0e16);
+ } else {
+ // The common case for most CSS content.
+
+ // TODO(gw): transform bounds may be referenced by
+ // the fragment shader when running in
+ // the alpha pass, even on non-transformed
+ // items. For now, just ensure it has no
+ // effect. We can tidy this up as we move
+ // more items to be brush shaders.
+#if defined(WR_FEATURE_ALPHA_PASS) && !defined(SWGL_ANTIALIAS)
+ init_transform_vs(vec4(vec2(-1.0e16), vec2(1.0e16)));
+#endif
+ }
+
+ // Select the corner of the local rect that we are processing.
+ vec2 local_pos = mix(adjusted_segment_rect.p0, adjusted_segment_rect.p1, aPosition.xy);
+
+ VertexInfo vi = write_vertex(
+ local_pos,
+ ph.local_clip_rect,
+ ph.z,
+ transform,
+ pic_task
+ );
+
+ // For brush instances in the alpha pass, always write
+ // out clip information.
+ // TODO(gw): It's possible that we might want alpha
+ // shaders that don't clip in the future,
+ // but it's reasonable to assume that one
+ // implies the other, for now.
+ // SW-WR may decay some requests for alpha-pass shaders to
+ // the opaque version if only the clip-mask is required. In
+ // that case the opaque vertex shader must still write out
+ // the clip information, which is cheap to do for SWGL.
+#if defined(WR_FEATURE_ALPHA_PASS) || defined(SWGL_CLIP_MASK)
+ write_clip(
+ vi.world_pos,
+ clip_area,
+ pic_task
+ );
+#endif
+
+ // Run the specific brush VS code to write interpolators.
+ brush_vs(
+ vi,
+ ph.specific_prim_address,
+ ph.local_rect,
+ segment_rect,
+ ph.user_data,
+ instance.resource_address,
+ transform.m,
+ pic_task,
+ brush_flags,
+ segment_data
+ );
+
+#if (defined(WR_FEATURE_ALPHA_PASS) || defined(WR_FEATURE_ANTIALIASING)) && !defined(SWGL_ANTIALIAS)
+ v_local_pos = vi.local_pos;
+#endif
+}
+
+#ifndef WR_VERTEX_SHADER_MAIN_FUNCTION
+// If the entry-point was not overridden before including the brush shader,
+// use the default one.
+#define WR_VERTEX_SHADER_MAIN_FUNCTION brush_shader_main_vs
+#endif
+
+void main(void) {
+
+ Instance instance = decode_instance_attributes();
+ PrimitiveHeader ph = fetch_prim_header(instance.prim_header_address);
+ Transform transform = fetch_transform(ph.transform_id);
+ PictureTask task = fetch_picture_task(instance.picture_task_address);
+ ClipArea clip_area = fetch_clip_area(instance.clip_address);
+
+ WR_VERTEX_SHADER_MAIN_FUNCTION(instance, ph, transform, task, clip_area);
+}
+
+#endif // WR_VERTEX_SHADER
+
+#ifdef WR_FRAGMENT_SHADER
+
+float antialias_brush() {
+#if (defined(WR_FEATURE_ALPHA_PASS) || defined(WR_FEATURE_ANTIALIASING)) && !defined(SWGL_ANTIALIAS)
+ return init_transform_fs(v_local_pos);
+#else
+ return 1.0;
+#endif
+}
+
+Fragment brush_fs();
+
+void main(void) {
+#ifdef WR_FEATURE_DEBUG_OVERDRAW
+ oFragColor = WR_DEBUG_OVERDRAW_COLOR;
+#else
+
+ Fragment frag = brush_fs();
+
+#ifdef WR_FEATURE_ALPHA_PASS
+ // Apply the clip mask
+ float clip_alpha = do_clip();
+
+ frag.color *= clip_alpha;
+
+ #ifdef WR_FEATURE_DUAL_SOURCE_BLENDING
+ oFragBlend = frag.blend * clip_alpha;
+ #endif
+#endif
+
+ write_output(frag.color);
+#endif
+}
+#endif
diff --git a/gfx/wr/webrender/res/brush_blend.glsl b/gfx/wr/webrender/res/brush_blend.glsl
new file mode 100644
index 0000000000..49f047edbd
--- /dev/null
+++ b/gfx/wr/webrender/res/brush_blend.glsl
@@ -0,0 +1,121 @@
+/* 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/. */
+
+#define VECS_PER_SPECIFIC_BRUSH 3
+#define WR_FEATURE_TEXTURE_2D
+
+#include shared,prim_shared,brush,blend
+
+// Interpolated UV coordinates to sample.
+varying highp vec2 v_uv;
+
+// Normalized bounds of the source image in the texture, adjusted to avoid
+// sampling artifacts.
+flat varying highp vec4 v_uv_sample_bounds;
+
+// x: Flag to allow perspective interpolation of UV.
+// y: Filter-dependent "amount" parameter.
+// Packed in to a vector to work around bug 1630356.
+flat varying mediump vec2 v_perspective_amount;
+#define v_perspective v_perspective_amount.x
+#define v_amount v_perspective_amount.y
+
+// x: Blend op, y: Lookup table GPU cache address.
+// Packed in to a vector to work around bug 1630356.
+// Must be explicitly marked as highp, as the default integer precision in
+// fragment shaders is mediump which may only be 16 bits in ESSL 3, and GPU
+// cache address can exceed that maximum representable value.
+flat varying highp ivec2 v_op_table_address_vec;
+#define v_op v_op_table_address_vec.x
+#define v_table_address v_op_table_address_vec.y
+
+// We must keep this as highp as we encoutered shader compilation crashes on
+// Mali-T devices when mediump.
+flat varying highp mat4 v_color_mat;
+// The function to use for each component of a component transfer filter. Using a int[4]
+// or ivec4 (with each element or component containing the function for each component) has
+// ran in to bugs 1695912 and 1731758, so instead use a vec4 and cast the values to/from floats.
+flat varying mediump vec4 v_funcs;
+flat varying mediump vec4 v_color_offset;
+
+#ifdef WR_VERTEX_SHADER
+void brush_vs(
+ VertexInfo vi,
+ int prim_address,
+ RectWithEndpoint local_rect,
+ RectWithEndpoint segment_rect,
+ ivec4 prim_user_data,
+ int specific_resource_address,
+ mat4 transform,
+ PictureTask pic_task,
+ int brush_flags,
+ vec4 unused
+) {
+ ImageSource res = fetch_image_source(prim_user_data.x);
+ vec2 uv0 = res.uv_rect.p0;
+ vec2 uv1 = res.uv_rect.p1;
+
+ vec2 inv_texture_size = vec2(1.0) / vec2(TEX_SIZE(sColor0).xy);
+ vec2 f = (vi.local_pos - local_rect.p0) / rect_size(local_rect);
+ f = get_image_quad_uv(prim_user_data.x, f);
+ vec2 uv = mix(uv0, uv1, f);
+ float perspective_interpolate = (brush_flags & BRUSH_FLAG_PERSPECTIVE_INTERPOLATION) != 0 ? 1.0 : 0.0;
+
+ v_uv = uv * inv_texture_size * mix(vi.world_pos.w, 1.0, perspective_interpolate);
+ v_perspective = perspective_interpolate;
+
+ v_uv_sample_bounds = vec4(uv0 + vec2(0.5), uv1 - vec2(0.5)) * inv_texture_size.xyxy;
+
+ float amount = float(prim_user_data.z) / 65536.0;
+
+ v_op = prim_user_data.y & 0xffff;
+ v_amount = amount;
+
+ v_funcs.r = float((prim_user_data.y >> 28) & 0xf);
+ v_funcs.g = float((prim_user_data.y >> 24) & 0xf);
+ v_funcs.b = float((prim_user_data.y >> 20) & 0xf);
+ v_funcs.a = float((prim_user_data.y >> 16) & 0xf);
+
+ SetupFilterParams(
+ v_op,
+ amount,
+ prim_user_data.z,
+ v_color_offset,
+ v_color_mat,
+ v_table_address
+ );
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+Fragment brush_fs() {
+ float perspective_divisor = mix(gl_FragCoord.w, 1.0, v_perspective);
+ vec2 uv = v_uv * perspective_divisor;
+ // Clamp the uvs to avoid sampling artifacts.
+ uv = clamp(uv, v_uv_sample_bounds.xy, v_uv_sample_bounds.zw);
+
+ vec4 Cs = texture(sColor0, uv);
+
+ float alpha;
+ vec3 color;
+ CalculateFilter(
+ Cs,
+ v_op,
+ v_amount,
+ v_table_address,
+ v_color_offset,
+ v_color_mat,
+ v_funcs,
+ color,
+ alpha
+ );
+
+ #ifdef WR_FEATURE_ALPHA_PASS
+ alpha *= antialias_brush();
+ #endif
+
+ // Pre-multiply the alpha into the output value.
+ return Fragment(alpha * vec4(color, 1.0));
+}
+#endif
diff --git a/gfx/wr/webrender/res/brush_image.glsl b/gfx/wr/webrender/res/brush_image.glsl
new file mode 100644
index 0000000000..64dfc232e0
--- /dev/null
+++ b/gfx/wr/webrender/res/brush_image.glsl
@@ -0,0 +1,393 @@
+/* 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/. */
+
+#define VECS_PER_SPECIFIC_BRUSH 3
+
+#include shared,prim_shared,brush
+
+// Interpolated UV coordinates to sample.
+varying highp vec2 v_uv;
+
+#ifdef WR_FEATURE_ALPHA_PASS
+flat varying mediump vec4 v_color;
+flat varying mediump vec2 v_mask_swizzle;
+flat varying mediump vec2 v_tile_repeat;
+#endif
+
+// Normalized bounds of the source image in the texture.
+flat varying highp vec4 v_uv_bounds;
+// Normalized bounds of the source image in the texture, adjusted to avoid
+// sampling artifacts.
+flat varying highp vec4 v_uv_sample_bounds;
+
+// Flag to allow perspective interpolation of UV.
+// Packed in to vector to work around bug 1630356.
+flat varying mediump vec2 v_perspective;
+
+#ifdef WR_VERTEX_SHADER
+
+// Must match the AlphaType enum.
+#define BLEND_MODE_ALPHA 0
+#define BLEND_MODE_PREMUL_ALPHA 1
+
+struct ImageBrushData {
+ vec4 color;
+ vec4 background_color;
+ vec2 stretch_size;
+};
+
+ImageBrushData fetch_image_data(int address) {
+ vec4[3] raw_data = fetch_from_gpu_cache_3(address);
+ ImageBrushData data = ImageBrushData(
+ raw_data[0],
+ raw_data[1],
+ raw_data[2].xy
+ );
+ return data;
+}
+
+void brush_vs(
+ VertexInfo vi,
+ int prim_address,
+ RectWithEndpoint prim_rect,
+ RectWithEndpoint segment_rect,
+ ivec4 prim_user_data,
+ int specific_resource_address,
+ mat4 transform,
+ PictureTask pic_task,
+ int brush_flags,
+ vec4 segment_data
+) {
+ ImageBrushData image_data = fetch_image_data(prim_address);
+
+ // If this is in WR_FEATURE_TEXTURE_RECT mode, the rect and size use
+ // non-normalized texture coordinates.
+#ifdef WR_FEATURE_TEXTURE_RECT
+ vec2 texture_size = vec2(1, 1);
+#else
+ vec2 texture_size = vec2(TEX_SIZE(sColor0));
+#endif
+
+ ImageSource res = fetch_image_source(specific_resource_address);
+ vec2 uv0 = res.uv_rect.p0;
+ vec2 uv1 = res.uv_rect.p1;
+
+ RectWithEndpoint local_rect = prim_rect;
+ vec2 stretch_size = image_data.stretch_size;
+ if (stretch_size.x < 0.0) {
+ stretch_size = rect_size(local_rect);
+ }
+
+ // If this segment should interpolate relative to the
+ // segment, modify the parameters for that.
+ if ((brush_flags & BRUSH_FLAG_SEGMENT_RELATIVE) != 0) {
+ local_rect = segment_rect;
+ stretch_size = rect_size(local_rect);
+
+ if ((brush_flags & BRUSH_FLAG_TEXEL_RECT) != 0) {
+ // If the extra data is a texel rect, modify the UVs.
+ vec2 uv_size = res.uv_rect.p1 - res.uv_rect.p0;
+ uv0 = res.uv_rect.p0 + segment_data.xy * uv_size;
+ uv1 = res.uv_rect.p0 + segment_data.zw * uv_size;
+ }
+
+ #ifdef WR_FEATURE_REPETITION
+ // TODO(bug 1609893): Move this logic to the CPU as well as other sources of
+ // branchiness in this shader.
+ if ((brush_flags & BRUSH_FLAG_TEXEL_RECT) != 0) {
+ // Value of the stretch size with repetition. We have to compute it for
+ // both axis even if we only repeat on one axis because the value for
+ // each axis depends on what the repeated value would have been for the
+ // other axis.
+ vec2 repeated_stretch_size = stretch_size;
+ // Size of the uv rect of the segment we are considering when computing
+ // the repetitions. For the fill area it is a tad more complicated as we
+ // have to use the uv size of the top-middle segment to drive horizontal
+ // repetitions, and the size of the left-middle segment to drive vertical
+ // repetitions. So we track the reference sizes for both axis separately
+ // even though in the common case (the border segments) they are the same.
+ vec2 horizontal_uv_size = uv1 - uv0;
+ vec2 vertical_uv_size = uv1 - uv0;
+ // We use top and left sizes by default and fall back to bottom and right
+ // when a size is empty.
+ if ((brush_flags & BRUSH_FLAG_SEGMENT_NINEPATCH_MIDDLE) != 0) {
+ repeated_stretch_size = segment_rect.p0 - prim_rect.p0;
+
+ float epsilon = 0.001;
+
+ // Adjust the the referecne uv size to compute vertical repetitions for
+ // the fill area.
+ vertical_uv_size.x = uv0.x - res.uv_rect.p0.x;
+ if (vertical_uv_size.x < epsilon || repeated_stretch_size.x < epsilon) {
+ vertical_uv_size.x = res.uv_rect.p1.x - uv1.x;
+ repeated_stretch_size.x = prim_rect.p1.x - segment_rect.p1.x;
+ }
+
+ // Adjust the the referecne uv size to compute horizontal repetitions
+ // for the fill area.
+ horizontal_uv_size.y = uv0.y - res.uv_rect.p0.y;
+ if (horizontal_uv_size.y < epsilon || repeated_stretch_size.y < epsilon) {
+ horizontal_uv_size.y = res.uv_rect.p1.y - uv1.y;
+ repeated_stretch_size.y = prim_rect.p1.y - segment_rect.p1.y;
+ }
+ }
+
+ if ((brush_flags & BRUSH_FLAG_SEGMENT_REPEAT_X) != 0) {
+ float uv_ratio = horizontal_uv_size.x / horizontal_uv_size.y;
+ stretch_size.x = repeated_stretch_size.y * uv_ratio;
+ }
+ if ((brush_flags & BRUSH_FLAG_SEGMENT_REPEAT_Y) != 0) {
+ float uv_ratio = vertical_uv_size.y / vertical_uv_size.x;
+ stretch_size.y = repeated_stretch_size.x * uv_ratio;
+ }
+
+ } else {
+ if ((brush_flags & BRUSH_FLAG_SEGMENT_REPEAT_X) != 0) {
+ stretch_size.x = segment_data.z - segment_data.x;
+ }
+ if ((brush_flags & BRUSH_FLAG_SEGMENT_REPEAT_Y) != 0) {
+ stretch_size.y = segment_data.w - segment_data.y;
+ }
+ }
+ if ((brush_flags & BRUSH_FLAG_SEGMENT_REPEAT_X_ROUND) != 0) {
+ float segment_rect_width = segment_rect.p1.x - segment_rect.p0.x;
+ float nx = max(1.0, round(segment_rect_width / stretch_size.x));
+ stretch_size.x = segment_rect_width / nx;
+ }
+ if ((brush_flags & BRUSH_FLAG_SEGMENT_REPEAT_Y_ROUND) != 0) {
+ float segment_rect_height = segment_rect.p1.y - segment_rect.p0.y;
+ float ny = max(1.0, round(segment_rect_height / stretch_size.y));
+ stretch_size.y = segment_rect_height / ny;
+ }
+ #endif
+ }
+
+ float perspective_interpolate = (brush_flags & BRUSH_FLAG_PERSPECTIVE_INTERPOLATION) != 0 ? 1.0 : 0.0;
+ v_perspective.x = perspective_interpolate;
+
+ // Handle case where the UV coords are inverted (e.g. from an
+ // external image).
+ vec2 min_uv = min(uv0, uv1);
+ vec2 max_uv = max(uv0, uv1);
+
+ v_uv_sample_bounds = vec4(
+ min_uv + vec2(0.5),
+ max_uv - vec2(0.5)
+ ) / texture_size.xyxy;
+
+ vec2 f = (vi.local_pos - local_rect.p0) / rect_size(local_rect);
+
+#ifdef WR_FEATURE_ALPHA_PASS
+ int color_mode = prim_user_data.x & 0xffff;
+ int blend_mode = prim_user_data.x >> 16;
+
+ if (color_mode == COLOR_MODE_FROM_PASS) {
+ color_mode = uMode;
+ }
+
+#endif
+
+ // Derive the texture coordinates for this image, based on
+ // whether the source image is a local-space or screen-space
+ // image.
+ int raster_space = prim_user_data.y;
+ if (raster_space == RASTER_SCREEN) {
+ // Since the screen space UVs specify an arbitrary quad, do
+ // a bilinear interpolation to get the correct UV for this
+ // local position.
+ f = get_image_quad_uv(specific_resource_address, f);
+ }
+
+ // Offset and scale v_uv here to avoid doing it in the fragment shader.
+ vec2 repeat = rect_size(local_rect) / stretch_size;
+ v_uv = mix(uv0, uv1, f) - min_uv;
+ v_uv /= texture_size;
+ v_uv *= repeat.xy;
+ if (perspective_interpolate == 0.0) {
+ v_uv *= vi.world_pos.w;
+ }
+
+#ifdef WR_FEATURE_TEXTURE_RECT
+ v_uv_bounds = vec4(0.0, 0.0, vec2(textureSize(sColor0)));
+#else
+ v_uv_bounds = vec4(min_uv, max_uv) / texture_size.xyxy;
+#endif
+
+#ifdef WR_FEATURE_REPETITION
+ // Normalize UV to 0..1 scale only if using repetition. Otherwise, leave
+ // UVs unnormalized since we won't compute a modulus without repetition
+ // enabled.
+ v_uv /= (v_uv_bounds.zw - v_uv_bounds.xy);
+#endif
+
+#ifdef WR_FEATURE_ALPHA_PASS
+ v_tile_repeat = repeat.xy;
+
+ float opacity = float(prim_user_data.z) / 65535.0;
+ switch (blend_mode) {
+ case BLEND_MODE_ALPHA:
+ image_data.color.a *= opacity;
+ break;
+ case BLEND_MODE_PREMUL_ALPHA:
+ default:
+ image_data.color *= opacity;
+ break;
+ }
+
+ switch (color_mode) {
+ case COLOR_MODE_ALPHA:
+ case COLOR_MODE_BITMAP_SHADOW:
+ #ifdef SWGL_BLEND
+ swgl_blendDropShadow(image_data.color);
+ v_mask_swizzle = vec2(1.0, 0.0);
+ v_color = vec4(1.0);
+ #else
+ v_mask_swizzle = vec2(0.0, 1.0);
+ v_color = image_data.color;
+ #endif
+ break;
+ case COLOR_MODE_SUBPX_BG_PASS2:
+ case COLOR_MODE_IMAGE:
+ v_mask_swizzle = vec2(1.0, 0.0);
+ v_color = image_data.color;
+ break;
+ case COLOR_MODE_SUBPX_BG_PASS0:
+ case COLOR_MODE_COLOR_BITMAP:
+ v_mask_swizzle = vec2(1.0, 0.0);
+ v_color = vec4(image_data.color.a);
+ break;
+ case COLOR_MODE_SUBPX_BG_PASS1:
+ v_mask_swizzle = vec2(-1.0, 1.0);
+ v_color = vec4(image_data.color.a) * image_data.background_color;
+ break;
+ case COLOR_MODE_SUBPX_DUAL_SOURCE:
+ v_mask_swizzle = vec2(image_data.color.a, 0.0);
+ v_color = image_data.color;
+ break;
+ case COLOR_MODE_MULTIPLY_DUAL_SOURCE:
+ v_mask_swizzle = vec2(-image_data.color.a, image_data.color.a);
+ v_color = image_data.color;
+ break;
+ default:
+ v_mask_swizzle = vec2(0.0);
+ v_color = vec4(1.0);
+ }
+#endif
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+
+vec2 compute_repeated_uvs(float perspective_divisor) {
+#ifdef WR_FEATURE_REPETITION
+ vec2 uv_size = v_uv_bounds.zw - v_uv_bounds.xy;
+
+ #ifdef WR_FEATURE_ALPHA_PASS
+ // This prevents the uv on the top and left parts of the primitive that was inflated
+ // for anti-aliasing purposes from going beyound the range covered by the regular
+ // (non-inflated) primitive.
+ vec2 local_uv = max(v_uv * perspective_divisor, vec2(0.0));
+
+ // Handle horizontal and vertical repetitions.
+ vec2 repeated_uv = fract(local_uv) * uv_size + v_uv_bounds.xy;
+
+ // This takes care of the bottom and right inflated parts.
+ // We do it after the modulo because the latter wraps around the values exactly on
+ // the right and bottom edges, which we do not want.
+ if (local_uv.x >= v_tile_repeat.x) {
+ repeated_uv.x = v_uv_bounds.z;
+ }
+ if (local_uv.y >= v_tile_repeat.y) {
+ repeated_uv.y = v_uv_bounds.w;
+ }
+ #else
+ vec2 repeated_uv = fract(v_uv * perspective_divisor) * uv_size + v_uv_bounds.xy;
+ #endif
+
+ return repeated_uv;
+#else
+ return v_uv * perspective_divisor + v_uv_bounds.xy;
+#endif
+}
+
+Fragment brush_fs() {
+ float perspective_divisor = mix(gl_FragCoord.w, 1.0, v_perspective.x);
+ vec2 repeated_uv = compute_repeated_uvs(perspective_divisor);
+
+ // Clamp the uvs to avoid sampling artifacts.
+ vec2 uv = clamp(repeated_uv, v_uv_sample_bounds.xy, v_uv_sample_bounds.zw);
+
+ vec4 texel = TEX_SAMPLE(sColor0, uv);
+
+ Fragment frag;
+
+#ifdef WR_FEATURE_ALPHA_PASS
+ #ifdef WR_FEATURE_ANTIALIASING
+ float alpha = antialias_brush();
+ #else
+ float alpha = 1.0;
+ #endif
+ #ifndef WR_FEATURE_DUAL_SOURCE_BLENDING
+ texel.rgb = texel.rgb * v_mask_swizzle.x + texel.aaa * v_mask_swizzle.y;
+ #endif
+
+ vec4 alpha_mask = texel * alpha;
+ frag.color = v_color * alpha_mask;
+
+ #ifdef WR_FEATURE_DUAL_SOURCE_BLENDING
+ frag.blend = alpha_mask * v_mask_swizzle.x + alpha_mask.aaaa * v_mask_swizzle.y;
+ #endif
+#else
+ frag.color = texel;
+#endif
+
+ return frag;
+}
+
+#if defined(SWGL_DRAW_SPAN) && (!defined(WR_FEATURE_ALPHA_PASS) || !defined(WR_FEATURE_DUAL_SOURCE_BLENDING))
+void swgl_drawSpanRGBA8() {
+ if (!swgl_isTextureRGBA8(sColor0)) {
+ return;
+ }
+
+ #ifdef WR_FEATURE_ALPHA_PASS
+ if (v_mask_swizzle != vec2(1.0, 0.0)) {
+ return;
+ }
+ #endif
+
+ float perspective_divisor = mix(swgl_forceScalar(gl_FragCoord.w), 1.0, v_perspective.x);
+
+ #ifdef WR_FEATURE_REPETITION
+ // Get the UVs before any repetition, scaling, or offsetting has occurred...
+ vec2 uv = v_uv * perspective_divisor;
+ #else
+ vec2 uv = compute_repeated_uvs(perspective_divisor);
+ #endif
+
+ #ifdef WR_FEATURE_ALPHA_PASS
+ if (v_color != vec4(1.0)) {
+ #ifdef WR_FEATURE_REPETITION
+ swgl_commitTextureRepeatColorRGBA8(sColor0, uv, v_tile_repeat, v_uv_bounds, v_uv_sample_bounds, v_color);
+ #else
+ swgl_commitTextureColorRGBA8(sColor0, uv, v_uv_sample_bounds, v_color);
+ #endif
+ return;
+ }
+ // No color scaling required, so just fall through to a normal textured span...
+ #endif
+
+ #ifdef WR_FEATURE_REPETITION
+ #ifdef WR_FEATURE_ALPHA_PASS
+ swgl_commitTextureRepeatRGBA8(sColor0, uv, v_tile_repeat, v_uv_bounds, v_uv_sample_bounds);
+ #else
+ swgl_commitTextureRepeatRGBA8(sColor0, uv, vec2(0.0), v_uv_bounds, v_uv_sample_bounds);
+ #endif
+ #else
+ swgl_commitTextureRGBA8(sColor0, uv, v_uv_sample_bounds);
+ #endif
+}
+#endif
+
+#endif
diff --git a/gfx/wr/webrender/res/brush_linear_gradient.glsl b/gfx/wr/webrender/res/brush_linear_gradient.glsl
new file mode 100644
index 0000000000..ceb1b14e5b
--- /dev/null
+++ b/gfx/wr/webrender/res/brush_linear_gradient.glsl
@@ -0,0 +1,95 @@
+/* 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/. */
+
+#define VECS_PER_SPECIFIC_BRUSH 2
+
+#include shared,prim_shared,brush,gpu_buffer,gradient_shared
+
+// Start offset. Packed in to vector to work around bug 1630356.
+flat varying mediump vec2 v_start_offset;
+
+flat varying mediump vec2 v_scale_dir;
+
+#ifdef WR_VERTEX_SHADER
+
+struct Gradient {
+ vec4 start_end_point;
+ int extend_mode;
+ vec2 stretch_size;
+};
+
+Gradient fetch_gradient(int address) {
+ vec4 data[2] = fetch_from_gpu_cache_2(address);
+ return Gradient(
+ data[0],
+ int(data[1].x),
+ data[1].yz
+ );
+}
+
+void brush_vs(
+ VertexInfo vi,
+ int prim_address,
+ RectWithEndpoint local_rect,
+ RectWithEndpoint segment_rect,
+ ivec4 prim_user_data,
+ int specific_resource_address,
+ mat4 transform,
+ PictureTask pic_task,
+ int brush_flags,
+ vec4 texel_rect
+) {
+ Gradient gradient = fetch_gradient(prim_address);
+
+ write_gradient_vertex(
+ vi,
+ local_rect,
+ segment_rect,
+ prim_user_data,
+ brush_flags,
+ texel_rect,
+ gradient.extend_mode,
+ gradient.stretch_size
+ );
+
+ vec2 start_point = gradient.start_end_point.xy;
+ vec2 end_point = gradient.start_end_point.zw;
+ vec2 dir = end_point - start_point;
+
+ // Normalize UV and offsets to 0..1 scale.
+ v_scale_dir = dir / dot(dir, dir);
+ v_start_offset.x = dot(start_point, v_scale_dir);
+ v_scale_dir *= v_repeated_size;
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+float get_gradient_offset(vec2 pos) {
+ // Project position onto a direction vector to compute offset.
+ return dot(pos, v_scale_dir) - v_start_offset.x;
+}
+
+Fragment brush_fs() {
+ vec4 color = sample_gradient(get_gradient_offset(compute_repeated_pos()));
+
+#ifdef WR_FEATURE_ALPHA_PASS
+ color *= antialias_brush();
+#endif
+
+ return Fragment(color);
+}
+
+#ifdef SWGL_DRAW_SPAN
+void swgl_drawSpanRGBA8() {
+ int address = swgl_validateGradient(sGpuBuffer, get_gpu_buffer_uv(v_gradient_address.x), int(GRADIENT_ENTRIES + 2.0));
+ if (address < 0) {
+ return;
+ }
+
+ swgl_commitLinearGradientRGBA8(sGpuBuffer, address, GRADIENT_ENTRIES, true, v_gradient_repeat.x != 0.0,
+ v_pos, v_scale_dir, v_start_offset.x);
+}
+#endif
+
+#endif
diff --git a/gfx/wr/webrender/res/brush_mix_blend.glsl b/gfx/wr/webrender/res/brush_mix_blend.glsl
new file mode 100644
index 0000000000..c18b95161b
--- /dev/null
+++ b/gfx/wr/webrender/res/brush_mix_blend.glsl
@@ -0,0 +1,332 @@
+/* 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/. */
+
+#define VECS_PER_SPECIFIC_BRUSH 3
+#define WR_FEATURE_TEXTURE_2D
+
+#include shared,prim_shared,brush
+
+// UV and bounds for the source image
+varying highp vec2 v_src_uv;
+flat varying highp vec4 v_src_uv_sample_bounds;
+
+// UV and bounds for the backdrop image
+varying highp vec2 v_backdrop_uv;
+flat varying highp vec4 v_backdrop_uv_sample_bounds;
+
+// Flag to allow perspective interpolation of UV.
+// Packed in to vector to work around bug 1630356.
+flat varying mediump vec2 v_perspective;
+// mix-blend op. Packed in to vector to work around bug 1630356.
+flat varying mediump ivec2 v_op;
+
+#ifdef WR_VERTEX_SHADER
+
+void get_uv(
+ int res_address,
+ vec2 f,
+ ivec2 texture_size,
+ float perspective_f,
+ out vec2 out_uv,
+ out vec4 out_uv_sample_bounds
+) {
+ ImageSource res = fetch_image_source(res_address);
+ vec2 uv0 = res.uv_rect.p0;
+ vec2 uv1 = res.uv_rect.p1;
+
+ vec2 inv_texture_size = vec2(1.0) / vec2(texture_size);
+ f = get_image_quad_uv(res_address, f);
+ vec2 uv = mix(uv0, uv1, f);
+
+ out_uv = uv * inv_texture_size * perspective_f;
+ out_uv_sample_bounds = vec4(uv0 + vec2(0.5), uv1 - vec2(0.5)) * inv_texture_size.xyxy;
+}
+
+void brush_vs(
+ VertexInfo vi,
+ int prim_address,
+ RectWithEndpoint local_rect,
+ RectWithEndpoint segment_rect,
+ ivec4 prim_user_data,
+ int specific_resource_address,
+ mat4 transform,
+ PictureTask pic_task,
+ int brush_flags,
+ vec4 unused
+) {
+ vec2 f = (vi.local_pos - local_rect.p0) / rect_size(local_rect);
+ float perspective_interpolate = (brush_flags & BRUSH_FLAG_PERSPECTIVE_INTERPOLATION) != 0 ? 1.0 : 0.0;
+ float perspective_f = mix(vi.world_pos.w, 1.0, perspective_interpolate);
+ v_perspective.x = perspective_interpolate;
+ v_op.x = prim_user_data.x;
+
+ get_uv(
+ prim_user_data.y,
+ f,
+ TEX_SIZE(sColor0).xy,
+ 1.0,
+ v_backdrop_uv,
+ v_backdrop_uv_sample_bounds
+ );
+
+ get_uv(
+ prim_user_data.z,
+ f,
+ TEX_SIZE(sColor1).xy,
+ perspective_f,
+ v_src_uv,
+ v_src_uv_sample_bounds
+ );
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+vec3 Multiply(vec3 Cb, vec3 Cs) {
+ return Cb * Cs;
+}
+
+vec3 Screen(vec3 Cb, vec3 Cs) {
+ return Cb + Cs - (Cb * Cs);
+}
+
+vec3 HardLight(vec3 Cb, vec3 Cs) {
+ vec3 m = Multiply(Cb, 2.0 * Cs);
+ vec3 s = Screen(Cb, 2.0 * Cs - 1.0);
+ vec3 edge = vec3(0.5, 0.5, 0.5);
+ return mix(m, s, step(edge, Cs));
+}
+
+// TODO: Worth doing with mix/step? Check GLSL output.
+float ColorDodge(float Cb, float Cs) {
+ if (Cb == 0.0)
+ return 0.0;
+ else if (Cs == 1.0)
+ return 1.0;
+ else
+ return min(1.0, Cb / (1.0 - Cs));
+}
+
+// TODO: Worth doing with mix/step? Check GLSL output.
+float ColorBurn(float Cb, float Cs) {
+ if (Cb == 1.0)
+ return 1.0;
+ else if (Cs == 0.0)
+ return 0.0;
+ else
+ return 1.0 - min(1.0, (1.0 - Cb) / Cs);
+}
+
+float SoftLight(float Cb, float Cs) {
+ if (Cs <= 0.5) {
+ return Cb - (1.0 - 2.0 * Cs) * Cb * (1.0 - Cb);
+ } else {
+ float D;
+
+ if (Cb <= 0.25)
+ D = ((16.0 * Cb - 12.0) * Cb + 4.0) * Cb;
+ else
+ D = sqrt(Cb);
+
+ return Cb + (2.0 * Cs - 1.0) * (D - Cb);
+ }
+}
+
+vec3 Difference(vec3 Cb, vec3 Cs) {
+ return abs(Cb - Cs);
+}
+
+// These functions below are taken from the spec.
+// There's probably a much quicker way to implement
+// them in GLSL...
+float Sat(vec3 c) {
+ return max(c.r, max(c.g, c.b)) - min(c.r, min(c.g, c.b));
+}
+
+float Lum(vec3 c) {
+ vec3 f = vec3(0.3, 0.59, 0.11);
+ return dot(c, f);
+}
+
+vec3 ClipColor(vec3 C) {
+ float L = Lum(C);
+ float n = min(C.r, min(C.g, C.b));
+ float x = max(C.r, max(C.g, C.b));
+
+ if (n < 0.0)
+ C = L + (((C - L) * L) / (L - n));
+
+ if (x > 1.0)
+ C = L + (((C - L) * (1.0 - L)) / (x - L));
+
+ return C;
+}
+
+vec3 SetLum(vec3 C, float l) {
+ float d = l - Lum(C);
+ return ClipColor(C + d);
+}
+
+void SetSatInner(inout float Cmin, inout float Cmid, inout float Cmax, float s) {
+ if (Cmax > Cmin) {
+ Cmid = (((Cmid - Cmin) * s) / (Cmax - Cmin));
+ Cmax = s;
+ } else {
+ Cmid = 0.0;
+ Cmax = 0.0;
+ }
+ Cmin = 0.0;
+}
+
+vec3 SetSat(vec3 C, float s) {
+ if (C.r <= C.g) {
+ if (C.g <= C.b) {
+ SetSatInner(C.r, C.g, C.b, s);
+ } else {
+ if (C.r <= C.b) {
+ SetSatInner(C.r, C.b, C.g, s);
+ } else {
+ SetSatInner(C.b, C.r, C.g, s);
+ }
+ }
+ } else {
+ if (C.r <= C.b) {
+ SetSatInner(C.g, C.r, C.b, s);
+ } else {
+ if (C.g <= C.b) {
+ SetSatInner(C.g, C.b, C.r, s);
+ } else {
+ SetSatInner(C.b, C.g, C.r, s);
+ }
+ }
+ }
+ return C;
+}
+
+vec3 Hue(vec3 Cb, vec3 Cs) {
+ return SetLum(SetSat(Cs, Sat(Cb)), Lum(Cb));
+}
+
+vec3 Saturation(vec3 Cb, vec3 Cs) {
+ return SetLum(SetSat(Cb, Sat(Cs)), Lum(Cb));
+}
+
+vec3 Color(vec3 Cb, vec3 Cs) {
+ return SetLum(Cs, Lum(Cb));
+}
+
+vec3 Luminosity(vec3 Cb, vec3 Cs) {
+ return SetLum(Cb, Lum(Cs));
+}
+
+const int MixBlendMode_Multiply = 1;
+const int MixBlendMode_Screen = 2;
+const int MixBlendMode_Overlay = 3;
+const int MixBlendMode_Darken = 4;
+const int MixBlendMode_Lighten = 5;
+const int MixBlendMode_ColorDodge = 6;
+const int MixBlendMode_ColorBurn = 7;
+const int MixBlendMode_HardLight = 8;
+const int MixBlendMode_SoftLight = 9;
+const int MixBlendMode_Difference = 10;
+const int MixBlendMode_Exclusion = 11;
+const int MixBlendMode_Hue = 12;
+const int MixBlendMode_Saturation = 13;
+const int MixBlendMode_Color = 14;
+const int MixBlendMode_Luminosity = 15;
+const int MixBlendMode_PlusLighter = 16;
+
+Fragment brush_fs() {
+ float perspective_divisor = mix(gl_FragCoord.w, 1.0, v_perspective.x);
+
+ vec2 src_uv = v_src_uv * perspective_divisor;
+ src_uv = clamp(src_uv, v_src_uv_sample_bounds.xy, v_src_uv_sample_bounds.zw);
+
+ vec2 backdrop_uv = clamp(v_backdrop_uv, v_backdrop_uv_sample_bounds.xy, v_backdrop_uv_sample_bounds.zw);
+
+ vec4 Cb = texture(sColor0, backdrop_uv);
+ vec4 Cs = texture(sColor1, src_uv);
+
+ // The mix-blend-mode functions assume no premultiplied alpha
+ if (Cb.a != 0.0) {
+ Cb.rgb /= Cb.a;
+ }
+
+ if (Cs.a != 0.0) {
+ Cs.rgb /= Cs.a;
+ }
+
+ // Return yellow if none of the branches match (shouldn't happen).
+ vec4 result = vec4(1.0, 1.0, 0.0, 1.0);
+
+ // On Android v_op has been packed in to a vector to avoid a driver bug
+ // on Adreno 3xx. However, this runs in to another Adreno 3xx driver bug
+ // where the switch doesn't match any cases. Unpacking the value from the
+ // vec in to a local variable prior to the switch works around this, but
+ // gets optimized away by glslopt. Adding a bitwise AND prevents that.
+ // See bug 1726755.
+ // default: default: to appease angle_shader_validation
+ switch (v_op.x & 0xFF) {
+ case MixBlendMode_Multiply:
+ result.rgb = Multiply(Cb.rgb, Cs.rgb);
+ break;
+ case MixBlendMode_Overlay:
+ // Overlay is inverse of Hardlight
+ result.rgb = HardLight(Cs.rgb, Cb.rgb);
+ break;
+ case MixBlendMode_Darken:
+ result.rgb = min(Cs.rgb, Cb.rgb);
+ break;
+ case MixBlendMode_Lighten:
+ result.rgb = max(Cs.rgb, Cb.rgb);
+ break;
+ case MixBlendMode_ColorDodge:
+ result.r = ColorDodge(Cb.r, Cs.r);
+ result.g = ColorDodge(Cb.g, Cs.g);
+ result.b = ColorDodge(Cb.b, Cs.b);
+ break;
+ case MixBlendMode_ColorBurn:
+ result.r = ColorBurn(Cb.r, Cs.r);
+ result.g = ColorBurn(Cb.g, Cs.g);
+ result.b = ColorBurn(Cb.b, Cs.b);
+ break;
+ case MixBlendMode_HardLight:
+ result.rgb = HardLight(Cb.rgb, Cs.rgb);
+ break;
+ case MixBlendMode_SoftLight:
+ result.r = SoftLight(Cb.r, Cs.r);
+ result.g = SoftLight(Cb.g, Cs.g);
+ result.b = SoftLight(Cb.b, Cs.b);
+ break;
+ case MixBlendMode_Difference:
+ result.rgb = Difference(Cb.rgb, Cs.rgb);
+ break;
+ case MixBlendMode_Hue:
+ result.rgb = Hue(Cb.rgb, Cs.rgb);
+ break;
+ case MixBlendMode_Saturation:
+ result.rgb = Saturation(Cb.rgb, Cs.rgb);
+ break;
+ case MixBlendMode_Color:
+ result.rgb = Color(Cb.rgb, Cs.rgb);
+ break;
+ case MixBlendMode_Luminosity:
+ result.rgb = Luminosity(Cb.rgb, Cs.rgb);
+ break;
+ case MixBlendMode_Screen:
+ case MixBlendMode_Exclusion:
+ case MixBlendMode_PlusLighter:
+ // This should be unreachable, since we implement
+ // MixBlendMode::Screen, MixBlendMode::Exclusion and
+ // MixBlendMode::PlusLighter using glBlendFuncSeparate.
+ break;
+ default: break;
+ }
+
+ result.rgb = (1.0 - Cb.a) * Cs.rgb + Cb.a * result.rgb;
+ result.a = Cs.a;
+ result.rgb *= result.a;
+
+ return Fragment(result);
+}
+#endif
diff --git a/gfx/wr/webrender/res/brush_opacity.glsl b/gfx/wr/webrender/res/brush_opacity.glsl
new file mode 100644
index 0000000000..caef83304a
--- /dev/null
+++ b/gfx/wr/webrender/res/brush_opacity.glsl
@@ -0,0 +1,83 @@
+/* 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/. */
+
+#define VECS_PER_SPECIFIC_BRUSH 3
+#define WR_FEATURE_TEXTURE_2D
+
+#include shared,prim_shared,brush
+
+// Interpolated UV coordinates to sample.
+varying highp vec2 v_uv;
+
+// Normalized bounds of the source image in the texture, adjusted to avoid
+// sampling artifacts.
+flat varying highp vec4 v_uv_sample_bounds;
+
+flat varying mediump vec2 v_opacity_perspective_vec;
+#define v_opacity v_opacity_perspective_vec.x
+// Flag to allow perspective interpolation of UV.
+#define v_perspective v_opacity_perspective_vec.y
+
+#ifdef WR_VERTEX_SHADER
+void brush_vs(
+ VertexInfo vi,
+ int prim_address,
+ RectWithEndpoint local_rect,
+ RectWithEndpoint segment_rect,
+ ivec4 prim_user_data,
+ int specific_resource_address,
+ mat4 transform,
+ PictureTask pic_task,
+ int brush_flags,
+ vec4 unused
+) {
+ ImageSource res = fetch_image_source(prim_user_data.x);
+ vec2 uv0 = res.uv_rect.p0;
+ vec2 uv1 = res.uv_rect.p1;
+
+ vec2 texture_size = vec2(TEX_SIZE(sColor0).xy);
+ vec2 f = (vi.local_pos - local_rect.p0) / rect_size(local_rect);
+ f = get_image_quad_uv(prim_user_data.x, f);
+ vec2 uv = mix(uv0, uv1, f);
+ float perspective_interpolate = (brush_flags & BRUSH_FLAG_PERSPECTIVE_INTERPOLATION) != 0 ? 1.0 : 0.0;
+
+ v_uv = uv / texture_size * mix(vi.world_pos.w, 1.0, perspective_interpolate);
+ v_perspective = perspective_interpolate;
+
+ v_uv_sample_bounds = vec4(uv0 + vec2(0.5), uv1 - vec2(0.5)) / texture_size.xyxy;
+
+ v_opacity = clamp(float(prim_user_data.y) / 65536.0, 0.0, 1.0);
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+Fragment brush_fs() {
+ float perspective_divisor = mix(gl_FragCoord.w, 1.0, v_perspective);
+ vec2 uv = v_uv * perspective_divisor;
+ // Clamp the uvs to avoid sampling artifacts.
+ uv = clamp(uv, v_uv_sample_bounds.xy, v_uv_sample_bounds.zw);
+
+ // No need to un-premultiply since we'll only apply a factor to the alpha.
+ vec4 color = texture(sColor0, uv);
+
+ float alpha = v_opacity;
+
+ #ifdef WR_FEATURE_ALPHA_PASS
+ alpha *= antialias_brush();
+ #endif
+
+ // Pre-multiply the contribution of the opacity factor.
+ return Fragment(alpha * color);
+}
+
+#if defined(SWGL_DRAW_SPAN) && !defined(WR_FEATURE_DUAL_SOURCE_BLENDING)
+void swgl_drawSpanRGBA8() {
+ float perspective_divisor = mix(swgl_forceScalar(gl_FragCoord.w), 1.0, v_perspective);
+ vec2 uv = v_uv * perspective_divisor;
+
+ swgl_commitTextureLinearColorRGBA8(sColor0, uv, v_uv_sample_bounds, v_opacity);
+}
+#endif
+
+#endif
diff --git a/gfx/wr/webrender/res/brush_solid.glsl b/gfx/wr/webrender/res/brush_solid.glsl
new file mode 100644
index 0000000000..d1028179c4
--- /dev/null
+++ b/gfx/wr/webrender/res/brush_solid.glsl
@@ -0,0 +1,60 @@
+/* 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/. */
+
+#define VECS_PER_SPECIFIC_BRUSH 1
+
+#include shared,prim_shared,brush
+
+flat varying mediump vec4 v_color;
+
+#ifdef WR_VERTEX_SHADER
+
+struct SolidBrush {
+ vec4 color;
+};
+
+SolidBrush fetch_solid_primitive(int address) {
+ vec4 data = fetch_from_gpu_cache_1(address);
+ return SolidBrush(data);
+}
+
+void brush_vs(
+ VertexInfo vi,
+ int prim_address,
+ RectWithEndpoint local_rect,
+ RectWithEndpoint segment_rect,
+ ivec4 prim_user_data,
+ int specific_resource_address,
+ mat4 transform,
+ PictureTask pic_task,
+ int brush_flags,
+ vec4 unused
+) {
+ SolidBrush prim = fetch_solid_primitive(prim_address);
+
+ float opacity = float(prim_user_data.x) / 65535.0;
+ v_color = prim.color * opacity;
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+Fragment brush_fs() {
+ vec4 color = v_color;
+#ifdef WR_FEATURE_ALPHA_PASS
+ color *= antialias_brush();
+#endif
+ return Fragment(color);
+}
+
+#if defined(SWGL_DRAW_SPAN) && (!defined(WR_FEATURE_ALPHA_PASS) || !defined(WR_FEATURE_DUAL_SOURCE_BLENDING))
+void swgl_drawSpanRGBA8() {
+ swgl_commitSolidRGBA8(v_color);
+}
+
+void swgl_drawSpanR8() {
+ swgl_commitSolidR8(v_color.x);
+}
+#endif
+
+#endif
diff --git a/gfx/wr/webrender/res/brush_yuv_image.glsl b/gfx/wr/webrender/res/brush_yuv_image.glsl
new file mode 100644
index 0000000000..4b7e5f1944
--- /dev/null
+++ b/gfx/wr/webrender/res/brush_yuv_image.glsl
@@ -0,0 +1,140 @@
+/* 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/. */
+
+#define VECS_PER_SPECIFIC_BRUSH 1
+
+#include shared,prim_shared,brush,yuv
+
+varying highp vec2 vUv_Y;
+flat varying highp vec4 vUvBounds_Y;
+
+varying highp vec2 vUv_U;
+flat varying highp vec4 vUvBounds_U;
+
+varying highp vec2 vUv_V;
+flat varying highp vec4 vUvBounds_V;
+
+flat varying YUV_PRECISION vec3 vYcbcrBias;
+flat varying YUV_PRECISION mat3 vRgbFromDebiasedYcbcr;
+
+// YUV format. Packed in to vector to work around bug 1630356.
+flat varying mediump ivec2 vFormat;
+
+#ifdef SWGL_DRAW_SPAN
+flat varying mediump int vRescaleFactor;
+#endif
+
+#ifdef WR_VERTEX_SHADER
+
+YuvPrimitive fetch_yuv_primitive(int address) {
+ vec4 data = fetch_from_gpu_cache_1(address);
+ // From YuvImageData.write_prim_gpu_blocks:
+ int channel_bit_depth = int(data.x);
+ int color_space = int(data.y);
+ int yuv_format = int(data.z);
+ return YuvPrimitive(channel_bit_depth, color_space, yuv_format);
+}
+
+void brush_vs(
+ VertexInfo vi,
+ int prim_address,
+ RectWithEndpoint local_rect,
+ RectWithEndpoint segment_rect,
+ ivec4 prim_user_data,
+ int specific_resource_address,
+ mat4 transform,
+ PictureTask pic_task,
+ int brush_flags,
+ vec4 unused
+) {
+ vec2 f = (vi.local_pos - local_rect.p0) / rect_size(local_rect);
+
+ YuvPrimitive prim = fetch_yuv_primitive(prim_address);
+
+#ifdef SWGL_DRAW_SPAN
+ // swgl_commitTextureLinearYUV needs to know the color space specifier and
+ // also needs to know how many bits of scaling are required to normalize
+ // HDR textures. Note that MSB HDR formats don't need renormalization.
+ vRescaleFactor = 0;
+ if (prim.channel_bit_depth > 8 && prim.yuv_format != YUV_FORMAT_P010) {
+ vRescaleFactor = 16 - prim.channel_bit_depth;
+ }
+#endif
+
+ YuvColorMatrixInfo mat_info = get_rgb_from_ycbcr_info(prim);
+ vYcbcrBias = mat_info.ycbcr_bias;
+ vRgbFromDebiasedYcbcr = mat_info.rgb_from_debiased_ycbrc;
+
+ vFormat.x = prim.yuv_format;
+
+ // The additional test for 99 works around a gen6 shader compiler bug: 1708937
+ if (vFormat.x == YUV_FORMAT_PLANAR || vFormat.x == 99) {
+ ImageSource res_y = fetch_image_source(prim_user_data.x);
+ ImageSource res_u = fetch_image_source(prim_user_data.y);
+ ImageSource res_v = fetch_image_source(prim_user_data.z);
+ write_uv_rect(res_y.uv_rect.p0, res_y.uv_rect.p1, f, TEX_SIZE_YUV(sColor0), vUv_Y, vUvBounds_Y);
+ write_uv_rect(res_u.uv_rect.p0, res_u.uv_rect.p1, f, TEX_SIZE_YUV(sColor1), vUv_U, vUvBounds_U);
+ write_uv_rect(res_v.uv_rect.p0, res_v.uv_rect.p1, f, TEX_SIZE_YUV(sColor2), vUv_V, vUvBounds_V);
+ } else if (vFormat.x == YUV_FORMAT_NV12 || vFormat.x == YUV_FORMAT_P010) {
+ ImageSource res_y = fetch_image_source(prim_user_data.x);
+ ImageSource res_u = fetch_image_source(prim_user_data.y);
+ write_uv_rect(res_y.uv_rect.p0, res_y.uv_rect.p1, f, TEX_SIZE_YUV(sColor0), vUv_Y, vUvBounds_Y);
+ write_uv_rect(res_u.uv_rect.p0, res_u.uv_rect.p1, f, TEX_SIZE_YUV(sColor1), vUv_U, vUvBounds_U);
+ } else if (vFormat.x == YUV_FORMAT_INTERLEAVED) {
+ ImageSource res_y = fetch_image_source(prim_user_data.x);
+ write_uv_rect(res_y.uv_rect.p0, res_y.uv_rect.p1, f, TEX_SIZE_YUV(sColor0), vUv_Y, vUvBounds_Y);
+ }
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+
+Fragment brush_fs() {
+ vec4 color = sample_yuv(
+ vFormat.x,
+ vYcbcrBias,
+ vRgbFromDebiasedYcbcr,
+ vUv_Y,
+ vUv_U,
+ vUv_V,
+ vUvBounds_Y,
+ vUvBounds_U,
+ vUvBounds_V
+ );
+
+#ifdef WR_FEATURE_ALPHA_PASS
+ color *= antialias_brush();
+#endif
+
+ //color.r = float(100+vFormat) / 255.0;
+ //color.g = vYcbcrBias.x;
+ //color.b = vYcbcrBias.y;
+ return Fragment(color);
+}
+
+#ifdef SWGL_DRAW_SPAN
+void swgl_drawSpanRGBA8() {
+ if (vFormat.x == YUV_FORMAT_PLANAR) {
+ swgl_commitTextureLinearYUV(sColor0, vUv_Y, vUvBounds_Y,
+ sColor1, vUv_U, vUvBounds_U,
+ sColor2, vUv_V, vUvBounds_V,
+ vYcbcrBias,
+ vRgbFromDebiasedYcbcr,
+ vRescaleFactor);
+ } else if (vFormat.x == YUV_FORMAT_NV12 || vFormat.x == YUV_FORMAT_P010) {
+ swgl_commitTextureLinearYUV(sColor0, vUv_Y, vUvBounds_Y,
+ sColor1, vUv_U, vUvBounds_U,
+ vYcbcrBias,
+ vRgbFromDebiasedYcbcr,
+ vRescaleFactor);
+ } else if (vFormat.x == YUV_FORMAT_INTERLEAVED) {
+ swgl_commitTextureLinearYUV(sColor0, vUv_Y, vUvBounds_Y,
+ vYcbcrBias,
+ vRgbFromDebiasedYcbcr,
+ vRescaleFactor);
+ }
+}
+#endif
+
+#endif
diff --git a/gfx/wr/webrender/res/clip_shared.glsl b/gfx/wr/webrender/res/clip_shared.glsl
new file mode 100644
index 0000000000..ef28bfde22
--- /dev/null
+++ b/gfx/wr/webrender/res/clip_shared.glsl
@@ -0,0 +1,80 @@
+/* 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/. */
+
+#include rect,render_task,gpu_cache,transform
+
+#ifdef WR_VERTEX_SHADER
+
+PER_INSTANCE in vec4 aClipDeviceArea;
+PER_INSTANCE in vec4 aClipOrigins;
+PER_INSTANCE in float aDevicePixelScale;
+PER_INSTANCE in ivec2 aTransformIds;
+
+struct ClipMaskInstanceCommon {
+ RectWithEndpoint sub_rect;
+ vec2 task_origin;
+ vec2 screen_origin;
+ float device_pixel_scale;
+ int clip_transform_id;
+ int prim_transform_id;
+};
+
+ClipMaskInstanceCommon fetch_clip_item_common() {
+ ClipMaskInstanceCommon cmi;
+
+ cmi.sub_rect = RectWithEndpoint(aClipDeviceArea.xy, aClipDeviceArea.zw);
+ cmi.task_origin = aClipOrigins.xy;
+ cmi.screen_origin = aClipOrigins.zw;
+ cmi.device_pixel_scale = aDevicePixelScale;
+ cmi.clip_transform_id = aTransformIds.x;
+ cmi.prim_transform_id = aTransformIds.y;
+
+ return cmi;
+}
+
+struct ClipVertexInfo {
+ vec4 local_pos;
+ RectWithEndpoint clipped_local_rect;
+};
+
+// The transformed vertex function that always covers the whole clip area,
+// which is the intersection of all clip instances of a given primitive
+ClipVertexInfo write_clip_tile_vertex(RectWithEndpoint local_clip_rect,
+ Transform prim_transform,
+ Transform clip_transform,
+ RectWithEndpoint sub_rect,
+ vec2 task_origin,
+ vec2 screen_origin,
+ float device_pixel_scale) {
+ vec2 device_pos = screen_origin + mix(sub_rect.p0, sub_rect.p1, aPosition.xy);
+ vec2 world_pos = device_pos / device_pixel_scale;
+
+ vec4 pos = prim_transform.m * vec4(world_pos, 0.0, 1.0);
+ pos.xyz /= pos.w;
+
+ vec4 p = get_node_pos(pos.xy, clip_transform);
+ vec4 local_pos = p * pos.w;
+
+ //TODO: Interpolate in clip space, where "local_pos.w" contains
+ // the W of the homogeneous transform *from* clip space into the world.
+ // float interpolate_w = 1.0 / local_pos.w;
+ // This is problematic today, because the W<=0 hemisphere is going to be
+ // clipped, while we currently want this shader to fill out the whole rect.
+ // We can therefore simplify this when the clip construction is rewritten
+ // to only affect the areas touched by a clip.
+ vec4 vertex_pos = vec4(
+ task_origin + mix(sub_rect.p0, sub_rect.p1, aPosition.xy),
+ 0.0,
+ 1.0
+ );
+
+ gl_Position = uTransform * vertex_pos;
+
+ init_transform_vs(vec4(local_clip_rect.p0, local_clip_rect.p1));
+
+ ClipVertexInfo vi = ClipVertexInfo(local_pos, local_clip_rect);
+ return vi;
+}
+
+#endif //WR_VERTEX_SHADER
diff --git a/gfx/wr/webrender/res/composite.glsl b/gfx/wr/webrender/res/composite.glsl
new file mode 100644
index 0000000000..4d30685ea1
--- /dev/null
+++ b/gfx/wr/webrender/res/composite.glsl
@@ -0,0 +1,242 @@
+/* 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/. */
+
+// Composite a picture cache tile into the framebuffer.
+
+// This shader must remain compatible with ESSL 1, at least for the
+// WR_FEATURE_TEXTURE_EXTERNAL_ESSL1 feature, so that it can be used to render
+// video on GLES devices without GL_OES_EGL_image_external_essl3 support.
+// This means we cannot use textureSize(), int inputs/outputs, etc.
+
+#include shared
+
+#ifdef WR_FEATURE_YUV
+#include yuv
+#endif
+
+#ifdef WR_FEATURE_YUV
+flat varying YUV_PRECISION vec3 vYcbcrBias;
+flat varying YUV_PRECISION mat3 vRgbFromDebiasedYcbcr;
+// YUV format. Packed in to vector to avoid bug 1630356.
+flat varying mediump ivec2 vYuvFormat;
+
+#ifdef SWGL_DRAW_SPAN
+flat varying mediump int vRescaleFactor;
+#endif
+varying highp vec2 vUV_y;
+varying highp vec2 vUV_u;
+varying highp vec2 vUV_v;
+flat varying highp vec4 vUVBounds_y;
+flat varying highp vec4 vUVBounds_u;
+flat varying highp vec4 vUVBounds_v;
+#else
+varying highp vec2 vUv;
+#ifndef WR_FEATURE_FAST_PATH
+flat varying mediump vec4 vColor;
+flat varying highp vec4 vUVBounds;
+#endif
+#ifdef WR_FEATURE_TEXTURE_EXTERNAL_ESSL1
+uniform mediump vec2 uTextureSize;
+#endif
+#endif
+
+#ifdef WR_VERTEX_SHADER
+// CPU side data is in CompositeInstance (gpu_types.rs) and is
+// converted to GPU data using desc::COMPOSITE (renderer.rs) by
+// filling vaos.composite_vao with VertexArrayKind::Composite.
+PER_INSTANCE attribute vec4 aLocalRect;
+PER_INSTANCE attribute vec4 aDeviceClipRect;
+PER_INSTANCE attribute vec4 aColor;
+PER_INSTANCE attribute vec4 aParams;
+PER_INSTANCE attribute vec4 aTransform;
+
+#ifdef WR_FEATURE_YUV
+// YUV treats these as a UV clip rect (clamp)
+PER_INSTANCE attribute vec4 aUvRect0;
+PER_INSTANCE attribute vec4 aUvRect1;
+PER_INSTANCE attribute vec4 aUvRect2;
+#else
+PER_INSTANCE attribute vec4 aUvRect0;
+#endif
+
+vec2 apply_transform(vec2 p, vec4 transform) {
+ return p * transform.xy + transform.zw;
+}
+
+#ifdef WR_FEATURE_YUV
+YuvPrimitive fetch_yuv_primitive() {
+ // From ExternalSurfaceDependency::Yuv:
+ int color_space = int(aParams.y);
+ int yuv_format = int(aParams.z);
+ int channel_bit_depth = int(aParams.w);
+ return YuvPrimitive(channel_bit_depth, color_space, yuv_format);
+}
+#endif
+
+void main(void) {
+ // Get world position
+ vec2 world_p0 = apply_transform(aLocalRect.xy, aTransform);
+ vec2 world_p1 = apply_transform(aLocalRect.zw, aTransform);
+ vec2 world_pos = mix(world_p0, world_p1, aPosition.xy);
+
+ // Clip the position to the world space clip rect
+ vec2 clipped_world_pos = clamp(world_pos, aDeviceClipRect.xy, aDeviceClipRect.zw);
+
+ // Derive the normalized UV from the clipped vertex position
+ vec2 uv = (clipped_world_pos - world_p0) / (world_p1 - world_p0);
+
+#ifdef WR_FEATURE_YUV
+ YuvPrimitive prim = fetch_yuv_primitive();
+
+#ifdef SWGL_DRAW_SPAN
+ // swgl_commitTextureLinearYUV needs to know the color space specifier and
+ // also needs to know how many bits of scaling are required to normalize
+ // HDR textures. Note that MSB HDR formats don't need renormalization.
+ vRescaleFactor = 0;
+ if (prim.channel_bit_depth > 8 && prim.yuv_format != YUV_FORMAT_P010) {
+ vRescaleFactor = 16 - prim.channel_bit_depth;
+ }
+#endif
+
+ YuvColorMatrixInfo mat_info = get_rgb_from_ycbcr_info(prim);
+ vYcbcrBias = mat_info.ycbcr_bias;
+ vRgbFromDebiasedYcbcr = mat_info.rgb_from_debiased_ycbrc;
+
+ vYuvFormat.x = prim.yuv_format;
+
+ write_uv_rect(
+ aUvRect0.xy,
+ aUvRect0.zw,
+ uv,
+ TEX_SIZE_YUV(sColor0),
+ vUV_y,
+ vUVBounds_y
+ );
+ write_uv_rect(
+ aUvRect1.xy,
+ aUvRect1.zw,
+ uv,
+ TEX_SIZE_YUV(sColor1),
+ vUV_u,
+ vUVBounds_u
+ );
+ write_uv_rect(
+ aUvRect2.xy,
+ aUvRect2.zw,
+ uv,
+ TEX_SIZE_YUV(sColor2),
+ vUV_v,
+ vUVBounds_v
+ );
+#else
+ uv = mix(aUvRect0.xy, aUvRect0.zw, uv);
+ // The uvs may be inverted, so use the min and max for the bounds
+ vec4 uvBounds = vec4(min(aUvRect0.xy, aUvRect0.zw), max(aUvRect0.xy, aUvRect0.zw));
+ int rescale_uv = int(aParams.y);
+ if (rescale_uv == 1)
+ {
+ // using an atlas, so UVs are in pixels, and need to be
+ // normalized and clamped.
+#if defined(WR_FEATURE_TEXTURE_RECT)
+ vec2 texture_size = vec2(1.0, 1.0);
+#elif defined(WR_FEATURE_TEXTURE_EXTERNAL_ESSL1)
+ vec2 texture_size = uTextureSize;
+#else
+ vec2 texture_size = vec2(TEX_SIZE(sColor0));
+#endif
+ uvBounds += vec4(0.5, 0.5, -0.5, -0.5);
+ #ifndef WR_FEATURE_TEXTURE_RECT
+ uv /= texture_size;
+ uvBounds /= texture_size.xyxy;
+ #endif
+ }
+
+ vUv = uv;
+#ifndef WR_FEATURE_FAST_PATH
+ vUVBounds = uvBounds;
+ // Pass through color
+ vColor = aColor;
+#endif
+#endif
+
+ gl_Position = uTransform * vec4(clipped_world_pos, 0.0, 1.0);
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+void main(void) {
+#ifdef WR_FEATURE_YUV
+ vec4 color = sample_yuv(
+ vYuvFormat.x,
+ vYcbcrBias,
+ vRgbFromDebiasedYcbcr,
+ vUV_y,
+ vUV_u,
+ vUV_v,
+ vUVBounds_y,
+ vUVBounds_u,
+ vUVBounds_v
+ );
+#else
+ // The color is just the texture sample modulated by a supplied color.
+ // In the fast path we avoid clamping the UV coordinates and modulating by the color.
+#ifdef WR_FEATURE_FAST_PATH
+ vec2 uv = vUv;
+#else
+ vec2 uv = clamp(vUv, vUVBounds.xy, vUVBounds.zw);
+#endif
+ vec4 texel = TEX_SAMPLE(sColor0, uv);
+#ifdef WR_FEATURE_FAST_PATH
+ vec4 color = texel;
+#else
+ vec4 color = vColor * texel;
+#endif
+#endif
+ write_output(color);
+}
+
+#ifdef SWGL_DRAW_SPAN
+void swgl_drawSpanRGBA8() {
+#ifdef WR_FEATURE_YUV
+ if (vYuvFormat.x == YUV_FORMAT_PLANAR) {
+ swgl_commitTextureLinearYUV(sColor0, vUV_y, vUVBounds_y,
+ sColor1, vUV_u, vUVBounds_u,
+ sColor2, vUV_v, vUVBounds_v,
+ vYcbcrBias,
+ vRgbFromDebiasedYcbcr,
+ vRescaleFactor);
+ } else if (vYuvFormat.x == YUV_FORMAT_NV12 || vYuvFormat.x == YUV_FORMAT_P010) {
+ swgl_commitTextureLinearYUV(sColor0, vUV_y, vUVBounds_y,
+ sColor1, vUV_u, vUVBounds_u,
+ vYcbcrBias,
+ vRgbFromDebiasedYcbcr,
+ vRescaleFactor);
+ } else if (vYuvFormat.x == YUV_FORMAT_INTERLEAVED) {
+ swgl_commitTextureLinearYUV(sColor0, vUV_y, vUVBounds_y,
+ vYcbcrBias,
+ vRgbFromDebiasedYcbcr,
+ vRescaleFactor);
+ }
+#else
+#ifdef WR_FEATURE_FAST_PATH
+ vec4 color = vec4(1.0);
+#ifdef WR_FEATURE_TEXTURE_RECT
+ vec4 uvBounds = vec4(vec2(0.0), vec2(textureSize(sColor0)));
+#else
+ vec4 uvBounds = vec4(0.0, 0.0, 1.0, 1.0);
+#endif
+#else
+ vec4 color = vColor;
+ vec4 uvBounds = vUVBounds;
+#endif
+ if (color != vec4(1.0)) {
+ swgl_commitTextureColorRGBA8(sColor0, vUv, uvBounds, color);
+ } else {
+ swgl_commitTextureRGBA8(sColor0, vUv, uvBounds);
+ }
+#endif
+}
+#endif
+
+#endif
diff --git a/gfx/wr/webrender/res/cs_blur.glsl b/gfx/wr/webrender/res/cs_blur.glsl
new file mode 100644
index 0000000000..51927e1a65
--- /dev/null
+++ b/gfx/wr/webrender/res/cs_blur.glsl
@@ -0,0 +1,196 @@
+/* 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/. */
+
+#define WR_FEATURE_TEXTURE_2D
+
+#include shared,prim_shared
+
+varying highp vec2 vUv;
+flat varying highp vec4 vUvRect;
+flat varying mediump vec2 vOffsetScale;
+// The number of pixels on each end that we apply the blur filter over.
+// Packed in to vector to work around bug 1630356.
+flat varying mediump ivec2 vSupport;
+flat varying mediump vec2 vGaussCoefficients;
+
+#ifdef WR_VERTEX_SHADER
+// Applies a separable gaussian blur in one direction, as specified
+// by the dir field in the blur command.
+
+#define DIR_HORIZONTAL 0
+#define DIR_VERTICAL 1
+
+PER_INSTANCE in int aBlurRenderTaskAddress;
+PER_INSTANCE in int aBlurSourceTaskAddress;
+PER_INSTANCE in int aBlurDirection;
+
+struct BlurTask {
+ RectWithEndpoint task_rect;
+ float blur_radius;
+ vec2 blur_region;
+};
+
+BlurTask fetch_blur_task(int address) {
+ RenderTaskData task_data = fetch_render_task_data(address);
+
+ BlurTask task = BlurTask(
+ task_data.task_rect,
+ task_data.user_data.x,
+ task_data.user_data.yz
+ );
+
+ return task;
+}
+
+void calculate_gauss_coefficients(float sigma) {
+ // Incremental Gaussian Coefficent Calculation (See GPU Gems 3 pp. 877 - 889)
+ vGaussCoefficients = vec2(1.0 / (sqrt(2.0 * 3.14159265) * sigma),
+ exp(-0.5 / (sigma * sigma)));
+
+ // Pre-calculate the coefficient total in the vertex shader so that
+ // we can avoid having to do it per-fragment and also avoid division
+ // by zero in the degenerate case.
+ vec3 gauss_coefficient = vec3(vGaussCoefficients,
+ vGaussCoefficients.y * vGaussCoefficients.y);
+ float gauss_coefficient_total = gauss_coefficient.x;
+ for (int i = 1; i <= vSupport.x; i += 2) {
+ gauss_coefficient.xy *= gauss_coefficient.yz;
+ float gauss_coefficient_subtotal = gauss_coefficient.x;
+ gauss_coefficient.xy *= gauss_coefficient.yz;
+ gauss_coefficient_subtotal += gauss_coefficient.x;
+ gauss_coefficient_total += 2.0 * gauss_coefficient_subtotal;
+ }
+
+ // Scale initial coefficient by total to avoid passing the total separately
+ // to the fragment shader.
+ vGaussCoefficients.x /= gauss_coefficient_total;
+}
+
+void main(void) {
+ BlurTask blur_task = fetch_blur_task(aBlurRenderTaskAddress);
+ RectWithEndpoint src_rect = fetch_render_task_rect(aBlurSourceTaskAddress);
+
+ RectWithEndpoint target_rect = blur_task.task_rect;
+
+ vec2 texture_size = vec2(TEX_SIZE(sColor0).xy);
+
+ // Ensure that the support is an even number of pixels to simplify the
+ // fragment shader logic.
+ //
+ // TODO(pcwalton): Actually make use of this fact and use the texture
+ // hardware for linear filtering.
+ vSupport.x = int(ceil(1.5 * blur_task.blur_radius)) * 2;
+
+ if (vSupport.x > 0) {
+ calculate_gauss_coefficients(blur_task.blur_radius);
+ } else {
+ // The gauss function gets NaNs when blur radius is zero.
+ vGaussCoefficients = vec2(1.0, 1.0);
+ }
+
+ switch (aBlurDirection) {
+ case DIR_HORIZONTAL:
+ vOffsetScale = vec2(1.0 / texture_size.x, 0.0);
+ break;
+ case DIR_VERTICAL:
+ vOffsetScale = vec2(0.0, 1.0 / texture_size.y);
+ break;
+ default:
+ vOffsetScale = vec2(0.0);
+ }
+
+ vUvRect = vec4(src_rect.p0 + vec2(0.5),
+ src_rect.p0 + blur_task.blur_region - vec2(0.5));
+ vUvRect /= texture_size.xyxy;
+
+ vec2 pos = mix(target_rect.p0, target_rect.p1, aPosition.xy);
+
+ vec2 uv0 = src_rect.p0 / texture_size;
+ vec2 uv1 = src_rect.p1 / texture_size;
+ vUv = mix(uv0, uv1, aPosition.xy);
+
+ gl_Position = uTransform * vec4(pos, 0.0, 1.0);
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+
+#if defined WR_FEATURE_COLOR_TARGET
+#define SAMPLE_TYPE vec4
+#define SAMPLE_TEXTURE(uv) texture(sColor0, uv)
+#else
+#define SAMPLE_TYPE float
+#define SAMPLE_TEXTURE(uv) texture(sColor0, uv).r
+#endif
+
+// TODO(gw): Write a fast path blur that handles smaller blur radii
+// with a offset / weight uniform table and a constant
+// loop iteration count!
+
+void main(void) {
+ SAMPLE_TYPE original_color = SAMPLE_TEXTURE(vUv);
+
+ // Incremental Gaussian Coefficent Calculation (See GPU Gems 3 pp. 877 - 889)
+ vec3 gauss_coefficient = vec3(vGaussCoefficients,
+ vGaussCoefficients.y * vGaussCoefficients.y);
+
+ SAMPLE_TYPE avg_color = original_color * gauss_coefficient.x;
+
+ // Evaluate two adjacent texels at a time. We can do this because, if c0
+ // and c1 are colors of adjacent texels and k0 and k1 are arbitrary
+ // factors, this formula:
+ //
+ // k0 * c0 + k1 * c1 (Equation 1)
+ //
+ // is equivalent to:
+ //
+ // k1
+ // (k0 + k1) * lerp(c0, c1, -------)
+ // k0 + k1
+ //
+ // A texture lookup of adjacent texels evaluates this formula:
+ //
+ // lerp(c0, c1, t)
+ //
+ // for some t. So we can let `t = k1/(k0 + k1)` and effectively evaluate
+ // Equation 1 with a single texture lookup.
+ //
+ // Clamp loop condition variable to a statically known value to workaround
+ // driver bug on Adreno 3xx. vSupport should not exceed 300 anyway, due to
+ // the max blur radius being 100. See bug 1720841 for details.
+ int support = min(vSupport.x, 300);
+ for (int i = 1; i <= support; i += 2) {
+ gauss_coefficient.xy *= gauss_coefficient.yz;
+
+ float gauss_coefficient_subtotal = gauss_coefficient.x;
+ gauss_coefficient.xy *= gauss_coefficient.yz;
+ gauss_coefficient_subtotal += gauss_coefficient.x;
+ float gauss_ratio = gauss_coefficient.x / gauss_coefficient_subtotal;
+
+ vec2 offset = vOffsetScale * (float(i) + gauss_ratio);
+
+ vec2 st0 = max(vUv - offset, vUvRect.xy);
+ vec2 st1 = min(vUv + offset, vUvRect.zw);
+ avg_color += (SAMPLE_TEXTURE(st0) + SAMPLE_TEXTURE(st1)) *
+ gauss_coefficient_subtotal;
+ }
+
+ oFragColor = vec4(avg_color);
+}
+
+#ifdef SWGL_DRAW_SPAN
+ #ifdef WR_FEATURE_COLOR_TARGET
+void swgl_drawSpanRGBA8() {
+ swgl_commitGaussianBlurRGBA8(sColor0, vUv, vUvRect, vOffsetScale.x != 0.0,
+ vSupport.x, vGaussCoefficients);
+}
+ #else
+void swgl_drawSpanR8() {
+ swgl_commitGaussianBlurR8(sColor0, vUv, vUvRect, vOffsetScale.x != 0.0,
+ vSupport.x, vGaussCoefficients);
+}
+ #endif
+#endif
+
+#endif
diff --git a/gfx/wr/webrender/res/cs_border_segment.glsl b/gfx/wr/webrender/res/cs_border_segment.glsl
new file mode 100644
index 0000000000..e684bfa6df
--- /dev/null
+++ b/gfx/wr/webrender/res/cs_border_segment.glsl
@@ -0,0 +1,450 @@
+/* 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/. */
+
+#include shared,rect,ellipse
+
+// For edges, the colors are the same. For corners, these
+// are the colors of each edge making up the corner.
+flat varying mediump vec4 vColor00;
+flat varying mediump vec4 vColor01;
+flat varying mediump vec4 vColor10;
+flat varying mediump vec4 vColor11;
+
+// A point + tangent defining the line where the edge
+// transition occurs. Used for corners only.
+flat varying mediump vec4 vColorLine;
+
+// x: segment, y: clip mode
+// We cast these to/from floats rather than using an ivec due to a driver bug
+// on Adreno 3xx. See bug 1730458.
+flat varying mediump vec2 vSegmentClipMode;
+// x, y: styles, z, w: edge axes
+// We cast these to/from floats rather than using an ivec (and bitshifting)
+// due to a driver bug on Adreno 3xx. See bug 1730458.
+flat varying mediump vec4 vStyleEdgeAxis;
+
+// xy = Local space position of the clip center.
+// zw = Scale the rect origin by this to get the outer
+// corner from the segment rectangle.
+flat varying highp vec4 vClipCenter_Sign;
+
+// An outer and inner elliptical radii for border
+// corner clipping.
+flat varying mediump vec4 vClipRadii;
+
+// Reference point for determine edge clip lines.
+flat varying mediump vec4 vEdgeReference;
+
+// Stores widths/2 and widths/3 to save doing this in FS.
+flat varying mediump vec4 vPartialWidths;
+
+// Clipping parameters for dot or dash.
+flat varying mediump vec4 vClipParams1;
+flat varying mediump vec4 vClipParams2;
+
+// Local space position
+varying highp vec2 vPos;
+
+#define SEGMENT_TOP_LEFT 0
+#define SEGMENT_TOP_RIGHT 1
+#define SEGMENT_BOTTOM_RIGHT 2
+#define SEGMENT_BOTTOM_LEFT 3
+#define SEGMENT_LEFT 4
+#define SEGMENT_TOP 5
+#define SEGMENT_RIGHT 6
+#define SEGMENT_BOTTOM 7
+
+// Border styles as defined in webrender_api/types.rs
+#define BORDER_STYLE_NONE 0
+#define BORDER_STYLE_SOLID 1
+#define BORDER_STYLE_DOUBLE 2
+#define BORDER_STYLE_DOTTED 3
+#define BORDER_STYLE_DASHED 4
+#define BORDER_STYLE_HIDDEN 5
+#define BORDER_STYLE_GROOVE 6
+#define BORDER_STYLE_RIDGE 7
+#define BORDER_STYLE_INSET 8
+#define BORDER_STYLE_OUTSET 9
+
+#define CLIP_NONE 0
+#define CLIP_DASH_CORNER 1
+#define CLIP_DASH_EDGE 2
+#define CLIP_DOT 3
+
+#ifdef WR_VERTEX_SHADER
+
+PER_INSTANCE in vec2 aTaskOrigin;
+PER_INSTANCE in vec4 aRect;
+PER_INSTANCE in vec4 aColor0;
+PER_INSTANCE in vec4 aColor1;
+PER_INSTANCE in int aFlags;
+PER_INSTANCE in vec2 aWidths;
+PER_INSTANCE in vec2 aRadii;
+PER_INSTANCE in vec4 aClipParams1;
+PER_INSTANCE in vec4 aClipParams2;
+
+vec2 get_outer_corner_scale(int segment) {
+ vec2 p;
+
+ switch (segment) {
+ case SEGMENT_TOP_LEFT:
+ p = vec2(0.0, 0.0);
+ break;
+ case SEGMENT_TOP_RIGHT:
+ p = vec2(1.0, 0.0);
+ break;
+ case SEGMENT_BOTTOM_RIGHT:
+ p = vec2(1.0, 1.0);
+ break;
+ case SEGMENT_BOTTOM_LEFT:
+ p = vec2(0.0, 1.0);
+ break;
+ default:
+ // The result is only used for non-default segment cases
+ p = vec2(0.0);
+ break;
+ }
+
+ return p;
+}
+
+// NOTE(emilio): If you change this algorithm, do the same change
+// in border.rs
+vec4 mod_color(vec4 color, bool is_black, bool lighter) {
+ const float light_black = 0.7;
+ const float dark_black = 0.3;
+
+ const float dark_scale = 0.66666666;
+ const float light_scale = 1.0;
+
+ if (is_black) {
+ if (lighter) {
+ return vec4(vec3(light_black), color.a);
+ }
+ return vec4(vec3(dark_black), color.a);
+ }
+
+ if (lighter) {
+ return vec4(color.rgb * light_scale, color.a);
+ }
+ return vec4(color.rgb * dark_scale, color.a);
+}
+
+vec4[2] get_colors_for_side(vec4 color, int style) {
+ vec4 result[2];
+
+ bool is_black = color.rgb == vec3(0.0, 0.0, 0.0);
+
+ switch (style) {
+ case BORDER_STYLE_GROOVE:
+ result[0] = mod_color(color, is_black, true);
+ result[1] = mod_color(color, is_black, false);
+ break;
+ case BORDER_STYLE_RIDGE:
+ result[0] = mod_color(color, is_black, false);
+ result[1] = mod_color(color, is_black, true);
+ break;
+ default:
+ result[0] = color;
+ result[1] = color;
+ break;
+ }
+
+ return result;
+}
+
+void main(void) {
+ int segment = aFlags & 0xff;
+ int style0 = (aFlags >> 8) & 0xff;
+ int style1 = (aFlags >> 16) & 0xff;
+ int clip_mode = (aFlags >> 24) & 0x0f;
+
+ vec2 size = aRect.zw - aRect.xy;
+ vec2 outer_scale = get_outer_corner_scale(segment);
+ vec2 outer = outer_scale * size;
+ vec2 clip_sign = 1.0 - 2.0 * outer_scale;
+
+ // Set some flags used by the FS to determine the
+ // orientation of the two edges in this corner.
+ ivec2 edge_axis = ivec2(0, 0);
+ // Derive the positions for the edge clips, which must be handled
+ // differently between corners and edges.
+ vec2 edge_reference = vec2(0.0);
+ switch (segment) {
+ case SEGMENT_TOP_LEFT:
+ edge_axis = ivec2(0, 1);
+ edge_reference = outer;
+ break;
+ case SEGMENT_TOP_RIGHT:
+ edge_axis = ivec2(1, 0);
+ edge_reference = vec2(outer.x - aWidths.x, outer.y);
+ break;
+ case SEGMENT_BOTTOM_RIGHT:
+ edge_axis = ivec2(0, 1);
+ edge_reference = outer - aWidths;
+ break;
+ case SEGMENT_BOTTOM_LEFT:
+ edge_axis = ivec2(1, 0);
+ edge_reference = vec2(outer.x, outer.y - aWidths.y);
+ break;
+ case SEGMENT_TOP:
+ case SEGMENT_BOTTOM:
+ edge_axis = ivec2(1, 1);
+ break;
+ case SEGMENT_LEFT:
+ case SEGMENT_RIGHT:
+ default:
+ break;
+ }
+
+ vSegmentClipMode = vec2(float(segment), float(clip_mode));
+ vStyleEdgeAxis = vec4(float(style0), float(style1), float(edge_axis.x), float(edge_axis.y));
+
+ vPartialWidths = vec4(aWidths / 3.0, aWidths / 2.0);
+ vPos = size * aPosition.xy;
+
+ vec4[2] color0 = get_colors_for_side(aColor0, style0);
+ vColor00 = color0[0];
+ vColor01 = color0[1];
+ vec4[2] color1 = get_colors_for_side(aColor1, style1);
+ vColor10 = color1[0];
+ vColor11 = color1[1];
+ vClipCenter_Sign = vec4(outer + clip_sign * aRadii, clip_sign);
+ vClipRadii = vec4(aRadii, max(aRadii - aWidths, 0.0));
+ vColorLine = vec4(outer, aWidths.y * -clip_sign.y, aWidths.x * clip_sign.x);
+ vEdgeReference = vec4(edge_reference, edge_reference + aWidths);
+ vClipParams1 = aClipParams1;
+ vClipParams2 = aClipParams2;
+
+ // For the case of dot and dash clips, optimize the number of pixels that
+ // are hit to just include the dot itself.
+ if (clip_mode == CLIP_DOT) {
+ float radius = aClipParams1.z;
+
+ // Expand by a small amount to allow room for AA around
+ // the dot if it's big enough.
+ if (radius > 0.5)
+ radius += 2.0;
+
+ vPos = vClipParams1.xy + radius * (2.0 * aPosition.xy - 1.0);
+ vPos = clamp(vPos, vec2(0.0), size);
+ } else if (clip_mode == CLIP_DASH_CORNER) {
+ vec2 center = (aClipParams1.xy + aClipParams2.xy) * 0.5;
+ // This is a gross approximation which works out because dashes don't have
+ // a strong curvature and we will overshoot by inflating the geometry by
+ // this amount on each side (sqrt(2) * length(dash) would be enough and we
+ // compute 2 * approx_length(dash)).
+ float dash_length = length(aClipParams1.xy - aClipParams2.xy);
+ float width = max(aWidths.x, aWidths.y);
+ // expand by a small amout for AA just like we do for dots.
+ vec2 r = vec2(max(dash_length, width)) + 2.0;
+ vPos = clamp(vPos, center - r, center + r);
+ }
+
+ gl_Position = uTransform * vec4(aTaskOrigin + aRect.xy + vPos, 0.0, 1.0);
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+vec4 evaluate_color_for_style_in_corner(
+ vec2 clip_relative_pos,
+ int style,
+ vec4 color0,
+ vec4 color1,
+ vec4 clip_radii,
+ float mix_factor,
+ int segment,
+ float aa_range
+) {
+ switch (style) {
+ case BORDER_STYLE_DOUBLE: {
+ // Get the distances from 0.33 of the radii, and
+ // also 0.67 of the radii. Use these to form a
+ // SDF subtraction which will clip out the inside
+ // third of the rounded edge.
+ float d_radii_a = distance_to_ellipse(
+ clip_relative_pos,
+ clip_radii.xy - vPartialWidths.xy
+ );
+ float d_radii_b = distance_to_ellipse(
+ clip_relative_pos,
+ clip_radii.xy - 2.0 * vPartialWidths.xy
+ );
+ float d = min(-d_radii_a, d_radii_b);
+ color0 *= distance_aa(aa_range, d);
+ break;
+ }
+ case BORDER_STYLE_GROOVE:
+ case BORDER_STYLE_RIDGE: {
+ float d = distance_to_ellipse(
+ clip_relative_pos,
+ clip_radii.xy - vPartialWidths.zw
+ );
+ float alpha = distance_aa(aa_range, d);
+ float swizzled_factor;
+ switch (segment) {
+ case SEGMENT_TOP_LEFT: swizzled_factor = 0.0; break;
+ case SEGMENT_TOP_RIGHT: swizzled_factor = mix_factor; break;
+ case SEGMENT_BOTTOM_RIGHT: swizzled_factor = 1.0; break;
+ case SEGMENT_BOTTOM_LEFT: swizzled_factor = 1.0 - mix_factor; break;
+ default: swizzled_factor = 0.0; break;
+ };
+ vec4 c0 = mix(color1, color0, swizzled_factor);
+ vec4 c1 = mix(color0, color1, swizzled_factor);
+ color0 = mix(c0, c1, alpha);
+ break;
+ }
+ default:
+ break;
+ }
+
+ return color0;
+}
+
+vec4 evaluate_color_for_style_in_edge(
+ vec2 pos_vec,
+ int style,
+ vec4 color0,
+ vec4 color1,
+ float aa_range,
+ int edge_axis_id
+) {
+ vec2 edge_axis = edge_axis_id != 0 ? vec2(0.0, 1.0) : vec2(1.0, 0.0);
+ float pos = dot(pos_vec, edge_axis);
+ switch (style) {
+ case BORDER_STYLE_DOUBLE: {
+ float d = -1.0;
+ float partial_width = dot(vPartialWidths.xy, edge_axis);
+ if (partial_width >= 1.0) {
+ vec2 ref = vec2(
+ dot(vEdgeReference.xy, edge_axis) + partial_width,
+ dot(vEdgeReference.zw, edge_axis) - partial_width
+ );
+ d = min(pos - ref.x, ref.y - pos);
+ }
+ color0 *= distance_aa(aa_range, d);
+ break;
+ }
+ case BORDER_STYLE_GROOVE:
+ case BORDER_STYLE_RIDGE: {
+ float ref = dot(vEdgeReference.xy + vPartialWidths.zw, edge_axis);
+ float d = pos - ref;
+ float alpha = distance_aa(aa_range, d);
+ color0 = mix(color0, color1, alpha);
+ break;
+ }
+ default:
+ break;
+ }
+
+ return color0;
+}
+
+void main(void) {
+ float aa_range = compute_aa_range(vPos);
+ vec4 color0, color1;
+
+ int segment = int(vSegmentClipMode.x);
+ int clip_mode = int(vSegmentClipMode.y);
+ ivec2 style = ivec2(int(vStyleEdgeAxis.x), int(vStyleEdgeAxis.y));
+ ivec2 edge_axis = ivec2(int(vStyleEdgeAxis.z), int(vStyleEdgeAxis.w));
+
+ float mix_factor = 0.0;
+ if (edge_axis.x != edge_axis.y) {
+ float d_line = distance_to_line(vColorLine.xy, vColorLine.zw, vPos);
+ mix_factor = distance_aa(aa_range, -d_line);
+ }
+
+ // Check if inside corner clip-region
+ vec2 clip_relative_pos = vPos - vClipCenter_Sign.xy;
+ bool in_clip_region = all(lessThan(vClipCenter_Sign.zw * clip_relative_pos, vec2(0.0)));
+ float d = -1.0;
+
+ switch (clip_mode) {
+ case CLIP_DOT: {
+ // Set clip distance based or dot position and radius.
+ d = distance(vClipParams1.xy, vPos) - vClipParams1.z;
+ break;
+ }
+ case CLIP_DASH_EDGE: {
+ bool is_vertical = vClipParams1.x == 0.;
+ float half_dash = is_vertical ? vClipParams1.y : vClipParams1.x;
+ // We want to draw something like:
+ // +---+---+---+---+
+ // |xxx| | |xxx|
+ // +---+---+---+---+
+ float pos = is_vertical ? vPos.y : vPos.x;
+ bool in_dash = pos < half_dash || pos > 3.0 * half_dash;
+ if (!in_dash) {
+ d = 1.;
+ }
+ break;
+ }
+ case CLIP_DASH_CORNER: {
+ // Get SDF for the two line/tangent clip lines,
+ // do SDF subtract to get clip distance.
+ float d0 = distance_to_line(vClipParams1.xy,
+ vClipParams1.zw,
+ vPos);
+ float d1 = distance_to_line(vClipParams2.xy,
+ vClipParams2.zw,
+ vPos);
+ d = max(d0, -d1);
+ break;
+ }
+ case CLIP_NONE:
+ default:
+ break;
+ }
+
+ if (in_clip_region) {
+ float d_radii_a = distance_to_ellipse(clip_relative_pos, vClipRadii.xy);
+ float d_radii_b = distance_to_ellipse(clip_relative_pos, vClipRadii.zw);
+ float d_radii = max(d_radii_a, -d_radii_b);
+ d = max(d, d_radii);
+
+ color0 = evaluate_color_for_style_in_corner(
+ clip_relative_pos,
+ style.x,
+ vColor00,
+ vColor01,
+ vClipRadii,
+ mix_factor,
+ segment,
+ aa_range
+ );
+ color1 = evaluate_color_for_style_in_corner(
+ clip_relative_pos,
+ style.y,
+ vColor10,
+ vColor11,
+ vClipRadii,
+ mix_factor,
+ segment,
+ aa_range
+ );
+ } else {
+ color0 = evaluate_color_for_style_in_edge(
+ vPos,
+ style.x,
+ vColor00,
+ vColor01,
+ aa_range,
+ edge_axis.x
+ );
+ color1 = evaluate_color_for_style_in_edge(
+ vPos,
+ style.y,
+ vColor10,
+ vColor11,
+ aa_range,
+ edge_axis.y
+ );
+ }
+
+ float alpha = distance_aa(aa_range, d);
+ vec4 color = mix(color0, color1, mix_factor);
+ oFragColor = color * alpha;
+}
+#endif
diff --git a/gfx/wr/webrender/res/cs_border_solid.glsl b/gfx/wr/webrender/res/cs_border_solid.glsl
new file mode 100644
index 0000000000..460646e21b
--- /dev/null
+++ b/gfx/wr/webrender/res/cs_border_solid.glsl
@@ -0,0 +1,178 @@
+/* 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/. */
+
+#include shared,rect,ellipse
+
+#define DONT_MIX 0
+#define MIX_AA 1
+#define MIX_NO_AA 2
+
+// For edges, the colors are the same. For corners, these
+// are the colors of each edge making up the corner.
+flat varying mediump vec4 vColor0;
+flat varying mediump vec4 vColor1;
+
+// A point + tangent defining the line where the edge
+// transition occurs. Used for corners only.
+flat varying highp vec4 vColorLine;
+
+// A boolean indicating that we should be mixing between edge colors.
+// Packed in to a vector to work around bug 1630356.
+flat varying mediump ivec2 vMixColors;
+
+// xy = Local space position of the clip center.
+// zw = Scale the rect origin by this to get the outer
+// corner from the segment rectangle.
+flat varying highp vec4 vClipCenter_Sign;
+
+// An outer and inner elliptical radii for border
+// corner clipping.
+flat varying highp vec4 vClipRadii;
+
+// Position, scale, and radii of horizontally and vertically adjacent corner clips.
+flat varying highp vec4 vHorizontalClipCenter_Sign;
+flat varying highp vec2 vHorizontalClipRadii;
+flat varying highp vec4 vVerticalClipCenter_Sign;
+flat varying highp vec2 vVerticalClipRadii;
+
+// Local space position
+varying highp vec2 vPos;
+
+#define SEGMENT_TOP_LEFT 0
+#define SEGMENT_TOP_RIGHT 1
+#define SEGMENT_BOTTOM_RIGHT 2
+#define SEGMENT_BOTTOM_LEFT 3
+
+#ifdef WR_VERTEX_SHADER
+
+PER_INSTANCE in vec2 aTaskOrigin;
+PER_INSTANCE in vec4 aRect;
+PER_INSTANCE in vec4 aColor0;
+PER_INSTANCE in vec4 aColor1;
+PER_INSTANCE in int aFlags;
+PER_INSTANCE in vec2 aWidths;
+PER_INSTANCE in vec2 aRadii;
+PER_INSTANCE in vec4 aClipParams1;
+PER_INSTANCE in vec4 aClipParams2;
+
+vec2 get_outer_corner_scale(int segment) {
+ vec2 p;
+
+ switch (segment) {
+ case SEGMENT_TOP_LEFT:
+ p = vec2(0.0, 0.0);
+ break;
+ case SEGMENT_TOP_RIGHT:
+ p = vec2(1.0, 0.0);
+ break;
+ case SEGMENT_BOTTOM_RIGHT:
+ p = vec2(1.0, 1.0);
+ break;
+ case SEGMENT_BOTTOM_LEFT:
+ p = vec2(0.0, 1.0);
+ break;
+ default:
+ // The result is only used for non-default segment cases
+ p = vec2(0.0);
+ break;
+ }
+
+ return p;
+}
+
+void main(void) {
+ int segment = aFlags & 0xff;
+ bool do_aa = ((aFlags >> 24) & 0xf0) != 0;
+
+ vec2 outer_scale = get_outer_corner_scale(segment);
+ vec2 size = aRect.zw - aRect.xy;
+ vec2 outer = outer_scale * size;
+ vec2 clip_sign = 1.0 - 2.0 * outer_scale;
+
+ int mix_colors;
+ switch (segment) {
+ case SEGMENT_TOP_LEFT:
+ case SEGMENT_TOP_RIGHT:
+ case SEGMENT_BOTTOM_RIGHT:
+ case SEGMENT_BOTTOM_LEFT: {
+ mix_colors = do_aa ? MIX_AA : MIX_NO_AA;
+ break;
+ }
+ default:
+ mix_colors = DONT_MIX;
+ break;
+ }
+
+ vMixColors.x = mix_colors;
+ vPos = size * aPosition.xy;
+
+ vColor0 = aColor0;
+ vColor1 = aColor1;
+ vClipCenter_Sign = vec4(outer + clip_sign * aRadii, clip_sign);
+ vClipRadii = vec4(aRadii, max(aRadii - aWidths, 0.0));
+ vColorLine = vec4(outer, aWidths.y * -clip_sign.y, aWidths.x * clip_sign.x);
+
+ vec2 horizontal_clip_sign = vec2(-clip_sign.x, clip_sign.y);
+ vHorizontalClipCenter_Sign = vec4(aClipParams1.xy +
+ horizontal_clip_sign * aClipParams1.zw,
+ horizontal_clip_sign);
+ vHorizontalClipRadii = aClipParams1.zw;
+
+ vec2 vertical_clip_sign = vec2(clip_sign.x, -clip_sign.y);
+ vVerticalClipCenter_Sign = vec4(aClipParams2.xy +
+ vertical_clip_sign * aClipParams2.zw,
+ vertical_clip_sign);
+ vVerticalClipRadii = aClipParams2.zw;
+
+ gl_Position = uTransform * vec4(aTaskOrigin + aRect.xy + vPos, 0.0, 1.0);
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+void main(void) {
+ float aa_range = compute_aa_range(vPos);
+ bool do_aa = vMixColors.x != MIX_NO_AA;
+
+ float mix_factor = 0.0;
+ if (vMixColors.x != DONT_MIX) {
+ float d_line = distance_to_line(vColorLine.xy, vColorLine.zw, vPos);
+ if (do_aa) {
+ mix_factor = distance_aa(aa_range, -d_line);
+ } else {
+ mix_factor = d_line + EPSILON >= 0. ? 1.0 : 0.0;
+ }
+ }
+
+ // Check if inside main corner clip-region
+ vec2 clip_relative_pos = vPos - vClipCenter_Sign.xy;
+ bool in_clip_region = all(lessThan(vClipCenter_Sign.zw * clip_relative_pos, vec2(0.0)));
+
+ float d = -1.0;
+ if (in_clip_region) {
+ float d_radii_a = distance_to_ellipse(clip_relative_pos, vClipRadii.xy);
+ float d_radii_b = distance_to_ellipse(clip_relative_pos, vClipRadii.zw);
+ d = max(d_radii_a, -d_radii_b);
+ }
+
+ // And again for horizontally-adjacent corner
+ clip_relative_pos = vPos - vHorizontalClipCenter_Sign.xy;
+ in_clip_region = all(lessThan(vHorizontalClipCenter_Sign.zw * clip_relative_pos, vec2(0.0)));
+ if (in_clip_region) {
+ float d_radii = distance_to_ellipse(clip_relative_pos, vHorizontalClipRadii.xy);
+ d = max(d_radii, d);
+ }
+
+ // And finally for vertically-adjacent corner
+ clip_relative_pos = vPos - vVerticalClipCenter_Sign.xy;
+ in_clip_region = all(lessThan(vVerticalClipCenter_Sign.zw * clip_relative_pos, vec2(0.0)));
+ if (in_clip_region) {
+ float d_radii = distance_to_ellipse(clip_relative_pos, vVerticalClipRadii.xy);
+ d = max(d_radii, d);
+ }
+
+ float alpha = do_aa ? distance_aa(aa_range, d) : 1.0;
+ vec4 color = mix(vColor0, vColor1, mix_factor);
+ oFragColor = color * alpha;
+}
+#endif
diff --git a/gfx/wr/webrender/res/cs_clip_box_shadow.glsl b/gfx/wr/webrender/res/cs_clip_box_shadow.glsl
new file mode 100644
index 0000000000..37a983c759
--- /dev/null
+++ b/gfx/wr/webrender/res/cs_clip_box_shadow.glsl
@@ -0,0 +1,327 @@
+/* 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/. */
+
+#include shared,clip_shared
+
+varying highp vec4 vLocalPos;
+varying highp vec2 vUv;
+flat varying highp vec4 vUvBounds;
+flat varying mediump vec4 vEdge;
+flat varying highp vec4 vUvBounds_NoClamp;
+// Clip mode. Packed in to a vector to avoid bug 1630356.
+flat varying mediump vec2 vClipMode;
+
+#define MODE_STRETCH 0
+#define MODE_SIMPLE 1
+
+#ifdef WR_VERTEX_SHADER
+
+PER_INSTANCE in ivec2 aClipDataResourceAddress;
+PER_INSTANCE in vec2 aClipSrcRectSize;
+PER_INSTANCE in int aClipMode;
+PER_INSTANCE in ivec2 aStretchMode;
+PER_INSTANCE in vec4 aClipDestRect;
+
+struct ClipMaskInstanceBoxShadow {
+ ClipMaskInstanceCommon base;
+ ivec2 resource_address;
+};
+
+ClipMaskInstanceBoxShadow fetch_clip_item() {
+ ClipMaskInstanceBoxShadow cmi;
+
+ cmi.base = fetch_clip_item_common();
+ cmi.resource_address = aClipDataResourceAddress;
+
+ return cmi;
+}
+
+struct BoxShadowData {
+ vec2 src_rect_size;
+ int clip_mode;
+ int stretch_mode_x;
+ int stretch_mode_y;
+ RectWithEndpoint dest_rect;
+};
+
+BoxShadowData fetch_data() {
+ BoxShadowData bs_data = BoxShadowData(
+ aClipSrcRectSize,
+ aClipMode,
+ aStretchMode.x,
+ aStretchMode.y,
+ RectWithEndpoint(aClipDestRect.xy, aClipDestRect.zw)
+ );
+ return bs_data;
+}
+
+void main(void) {
+ ClipMaskInstanceBoxShadow cmi = fetch_clip_item();
+ Transform clip_transform = fetch_transform(cmi.base.clip_transform_id);
+ Transform prim_transform = fetch_transform(cmi.base.prim_transform_id);
+ BoxShadowData bs_data = fetch_data();
+ ImageSource res = fetch_image_source_direct(cmi.resource_address);
+
+ RectWithEndpoint dest_rect = bs_data.dest_rect;
+
+ ClipVertexInfo vi = write_clip_tile_vertex(
+ dest_rect,
+ prim_transform,
+ clip_transform,
+ cmi.base.sub_rect,
+ cmi.base.task_origin,
+ cmi.base.screen_origin,
+ cmi.base.device_pixel_scale
+ );
+ vClipMode.x = float(bs_data.clip_mode);
+
+ vec2 texture_size = vec2(TEX_SIZE(sColor0));
+ vec2 local_pos = vi.local_pos.xy / vi.local_pos.w;
+ vLocalPos = vi.local_pos;
+ vec2 dest_rect_size = rect_size(dest_rect);
+
+ switch (bs_data.stretch_mode_x) {
+ case MODE_STRETCH: {
+ vEdge.x = 0.5;
+ vEdge.z = (dest_rect_size.x / bs_data.src_rect_size.x) - 0.5;
+ vUv.x = (local_pos.x - dest_rect.p0.x) / bs_data.src_rect_size.x;
+ break;
+ }
+ case MODE_SIMPLE:
+ default: {
+ vEdge.xz = vec2(1.0);
+ vUv.x = (local_pos.x - dest_rect.p0.x) / dest_rect_size.x;
+ break;
+ }
+ }
+
+ switch (bs_data.stretch_mode_y) {
+ case MODE_STRETCH: {
+ vEdge.y = 0.5;
+ vEdge.w = (dest_rect_size.y / bs_data.src_rect_size.y) - 0.5;
+ vUv.y = (local_pos.y - dest_rect.p0.y) / bs_data.src_rect_size.y;
+ break;
+ }
+ case MODE_SIMPLE:
+ default: {
+ vEdge.yw = vec2(1.0);
+ vUv.y = (local_pos.y - dest_rect.p0.y) / dest_rect_size.y;
+ break;
+ }
+ }
+
+ vUv *= vi.local_pos.w;
+ vec2 uv0 = res.uv_rect.p0;
+ vec2 uv1 = res.uv_rect.p1;
+ vUvBounds = vec4(uv0 + vec2(0.5), uv1 - vec2(0.5)) / texture_size.xyxy;
+ vUvBounds_NoClamp = vec4(uv0, uv1) / texture_size.xyxy;
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+void main(void) {
+ vec2 uv_linear = vUv / vLocalPos.w;
+ vec2 uv = clamp(uv_linear, vec2(0.0), vEdge.xy);
+ uv += max(vec2(0.0), uv_linear - vEdge.zw);
+ uv = mix(vUvBounds_NoClamp.xy, vUvBounds_NoClamp.zw, uv);
+ uv = clamp(uv, vUvBounds.xy, vUvBounds.zw);
+
+ float in_shadow_rect = init_transform_rough_fs(vLocalPos.xy / vLocalPos.w);
+
+ float texel = TEX_SAMPLE(sColor0, uv).r;
+
+ float alpha = mix(texel, 1.0 - texel, vClipMode.x);
+ float result = vLocalPos.w > 0.0 ? mix(vClipMode.x, alpha, in_shadow_rect) : 0.0;
+
+ oFragColor = vec4(result);
+}
+
+#ifdef SWGL_DRAW_SPAN
+// As with cs_clip_rectangle, this shader spends a lot of time doing clipping and
+// combining for every fragment, even if outside of the primitive to initialize
+// the clip tile, or inside the inner bounds of the primitive, where the shadow
+// is unnecessary. To alleviate this, the span shader attempts to first intersect
+// the the local clip bounds, outside of which we can just use a solid fill
+// to initialize those clip tile fragments. Once inside the primitive bounds,
+// we further intersect with the inner region where no shadow is necessary either
+// so that we can commit entire spans of texture within this nine-patch region
+// instead of having to do the work of mapping per fragment.
+void swgl_drawSpanR8() {
+ // Perspective is not supported.
+ if (swgl_interpStep(vLocalPos).w != 0.0) {
+ return;
+ }
+
+ // If the span is completely outside the Z-range and clipped out, just
+ // output clear so we don't need to consider invalid W in the rest of the
+ // shader.
+ float w = swgl_forceScalar(vLocalPos.w);
+ if (w <= 0.0) {
+ swgl_commitSolidR8(0.0);
+ return;
+ }
+
+ // To start, we evaluate the box shadow in both UV and local space relative
+ // to the local-space position. This will be interpolated across the span to
+ // track whether we intersect the nine-patch.
+ w = 1.0 / w;
+ vec2 uv_linear = vUv * w;
+ vec2 uv_linear0 = swgl_forceScalar(uv_linear);
+ vec2 uv_linear_step = swgl_interpStep(vUv).xy * w;
+ vec2 local_pos = vLocalPos.xy * w;
+ vec2 local_pos0 = swgl_forceScalar(local_pos);
+ vec2 local_step = swgl_interpStep(vLocalPos).xy * w;
+
+ // We need to compute the local-space distance to the bounding box and then
+ // figure out how many processing steps that maps to. If we are stepping in
+ // a negative direction on an axis, we need to swap the sides of the box
+ // which we consider as the start or end. If there is no local-space step
+ // on an axis (i.e. constant Y), we need to take care to force the steps to
+ // either the start or end of the span depending on if we are inside or
+ // outside of the bounding box.
+ vec4 clip_dist =
+ mix(vTransformBounds, vTransformBounds.zwxy, lessThan(local_step, vec2(0.0)).xyxy)
+ - local_pos0.xyxy;
+ clip_dist =
+ mix(1.0e6 * step(0.0, clip_dist),
+ clip_dist * recip(local_step).xyxy,
+ notEqual(local_step, vec2(0.0)).xyxy);
+
+ // Find the start and end of the shadowed region on this span.
+ float shadow_start = max(clip_dist.x, clip_dist.y);
+ float shadow_end = min(clip_dist.z, clip_dist.w);
+
+ // Flip the offsets from the start of the span so we can compare against the
+ // remaining span length which automatically deducts as we commit fragments.
+ ivec2 shadow_steps = ivec2(clamp(
+ swgl_SpanLength - swgl_StepSize * vec2(floor(shadow_start), ceil(shadow_end)),
+ 0.0, swgl_SpanLength));
+ int shadow_start_len = shadow_steps.x;
+ int shadow_end_len = shadow_steps.y;
+
+ // Likewise, once inside the primitive bounds, we also need to track which
+ // sector of the nine-patch we are in which requires intersecting against
+ // the inner box instead of the outer box.
+ vec4 opaque_dist =
+ mix(vEdge, vEdge.zwxy, lessThan(uv_linear_step, vec2(0.0)).xyxy)
+ - uv_linear0.xyxy;
+ opaque_dist =
+ mix(1.0e6 * step(0.0, opaque_dist),
+ opaque_dist * recip(uv_linear_step).xyxy,
+ notEqual(uv_linear_step, vec2(0.0)).xyxy);
+
+ // Unlike for the shadow clipping bounds, here we need to rather find the floor of all
+ // the offsets so that we don't accidentally process any chunks in the transitional areas
+ // between sectors of the nine-patch.
+ ivec4 opaque_steps = ivec4(clamp(
+ swgl_SpanLength -
+ swgl_StepSize *
+ vec4(floor(opaque_dist.x), floor(opaque_dist.y), floor(opaque_dist.z), floor(opaque_dist.w)),
+ shadow_end_len, swgl_SpanLength));
+
+ // Fill any initial sections of the span that are clipped out based on clip mode.
+ if (swgl_SpanLength > shadow_start_len) {
+ int num_before = swgl_SpanLength - shadow_start_len;
+ swgl_commitPartialSolidR8(num_before, vClipMode.x);
+ float steps_before = float(num_before / swgl_StepSize);
+ uv_linear += steps_before * uv_linear_step;
+ local_pos += steps_before * local_step;
+ }
+
+ // This loop tries to repeatedly process entire spans of the nine-patch that map
+ // to a contiguous spans of texture in the source box shadow. First, we process
+ // a chunk with per-fragment clipping and mapping in case we're starting on a
+ // transitional region between sectors of the nine-patch which may need to map
+ // to different spans of texture per-fragment. After, we find the largest span
+ // within the current sector before we hit the next transitional region, and
+ // attempt to commit an entire span of texture therein.
+ while (swgl_SpanLength > 0) {
+ // Here we might be in a transitional chunk, so do everything per-fragment.
+ {
+ vec2 uv = clamp(uv_linear, vec2(0.0), vEdge.xy);
+ uv += max(vec2(0.0), uv_linear - vEdge.zw);
+ uv = mix(vUvBounds_NoClamp.xy, vUvBounds_NoClamp.zw, uv);
+ uv = clamp(uv, vUvBounds.xy, vUvBounds.zw);
+
+ float in_shadow_rect = init_transform_rough_fs(local_pos);
+
+ float texel = TEX_SAMPLE(sColor0, uv).r;
+
+ float alpha = mix(texel, 1.0 - texel, vClipMode.x);
+ float result = mix(vClipMode.x, alpha, in_shadow_rect);
+ swgl_commitColorR8(result);
+
+ uv_linear += uv_linear_step;
+ local_pos += local_step;
+ }
+ // If we now hit the end of the clip bounds, just bail out since there is
+ // no more shadow to map.
+ if (swgl_SpanLength <= shadow_end_len) {
+ break;
+ }
+ // By here we've determined to be still inside the nine-patch. We need to
+ // compare against the inner rectangle thresholds to see which sector of
+ // the nine-patch to use and thus how to map the box shadow texture. Stop
+ // at least one step before the end of the shadow region to properly clip
+ // on the boundary.
+ int num_inside = swgl_SpanLength - swgl_StepSize - shadow_end_len;
+ vec4 uv_bounds = vUvBounds;
+ if (swgl_SpanLength >= opaque_steps.y) {
+ // We're in the top Y band of the nine-patch.
+ num_inside = min(num_inside, swgl_SpanLength - opaque_steps.y);
+ } else if (swgl_SpanLength >= opaque_steps.w) {
+ // We're in the middle Y band of the nine-patch. Set the UV clamp bounds
+ // to the vertical center texel of the box shadow.
+ num_inside = min(num_inside, swgl_SpanLength - opaque_steps.w);
+ uv_bounds.yw = vec2(clamp(mix(vUvBounds_NoClamp.y, vUvBounds_NoClamp.w, vEdge.y),
+ vUvBounds.y, vUvBounds.w));
+ }
+ if (swgl_SpanLength >= opaque_steps.x) {
+ // We're in the left X column of the nine-patch.
+ num_inside = min(num_inside, swgl_SpanLength - opaque_steps.x);
+ } else if (swgl_SpanLength >= opaque_steps.z) {
+ // We're in the middle X band of the nine-patch. Set the UV clamp bounds
+ // to the horizontal center texel of the box shadow.
+ num_inside = min(num_inside, swgl_SpanLength - opaque_steps.z);
+ uv_bounds.xz = vec2(clamp(mix(vUvBounds_NoClamp.x, vUvBounds_NoClamp.z, vEdge.x),
+ vUvBounds.x, vUvBounds.z));
+ }
+ if (num_inside > 0) {
+ // We have a non-zero span of fragments within the sector. Map to the UV
+ // start offset of the sector and the UV offset within the sector.
+ vec2 uv = clamp(uv_linear, vec2(0.0), vEdge.xy);
+ uv += max(vec2(0.0), uv_linear - vEdge.zw);
+ uv = mix(vUvBounds_NoClamp.xy, vUvBounds_NoClamp.zw, uv);
+ // If we're in the center sector of the nine-patch, then we only need to
+ // sample from a single texel of the box shadow. Just sample that single
+ // texel once and output it for the entire span. Otherwise, we just need
+ // to commit an actual span of texture from the box shadow. Depending on
+ // if we are in clip-out mode, we may need to invert the source texture.
+ if (uv_bounds.xy == uv_bounds.zw) {
+ uv = clamp(uv, uv_bounds.xy, uv_bounds.zw);
+ float texel = TEX_SAMPLE(sColor0, uv).r;
+ float alpha = mix(texel, 1.0 - texel, vClipMode.x);
+ swgl_commitPartialSolidR8(num_inside, alpha);
+ } else if (vClipMode.x != 0.0) {
+ swgl_commitPartialTextureLinearInvertR8(num_inside, sColor0, uv, uv_bounds);
+ } else {
+ swgl_commitPartialTextureLinearR8(num_inside, sColor0, uv, uv_bounds);
+ }
+ float steps_inside = float(num_inside / swgl_StepSize);
+ uv_linear += steps_inside * uv_linear_step;
+ local_pos += steps_inside * local_step;
+ }
+ // By here we're probably in a transitional chunk of the nine-patch that
+ // requires per-fragment processing, so loop around again to the handler
+ // for that case.
+ }
+
+ // Fill any remaining sections of the span that are clipped out.
+ if (swgl_SpanLength > 0) {
+ swgl_commitPartialSolidR8(swgl_SpanLength, vClipMode.x);
+ }
+}
+#endif
+
+#endif
diff --git a/gfx/wr/webrender/res/cs_clip_image.glsl b/gfx/wr/webrender/res/cs_clip_image.glsl
new file mode 100644
index 0000000000..24ba1dab8e
--- /dev/null
+++ b/gfx/wr/webrender/res/cs_clip_image.glsl
@@ -0,0 +1,117 @@
+/* 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/. */
+
+#include shared,clip_shared
+
+varying highp vec2 vLocalPos;
+varying highp vec2 vClipMaskImageUv;
+
+flat varying highp vec4 vClipMaskUvInnerRect;
+
+#ifdef WR_VERTEX_SHADER
+
+PER_INSTANCE in vec4 aClipTileRect;
+PER_INSTANCE in ivec2 aClipDataResourceAddress;
+PER_INSTANCE in vec4 aClipLocalRect;
+
+struct ClipMaskInstanceImage {
+ ClipMaskInstanceCommon base;
+ RectWithEndpoint tile_rect;
+ ivec2 resource_address;
+ RectWithEndpoint local_rect;
+};
+
+ClipMaskInstanceImage fetch_clip_item() {
+ ClipMaskInstanceImage cmi;
+
+ cmi.base = fetch_clip_item_common();
+
+ cmi.tile_rect = RectWithEndpoint(aClipTileRect.xy, aClipTileRect.zw);
+ cmi.resource_address = aClipDataResourceAddress;
+ cmi.local_rect = RectWithEndpoint(aClipLocalRect.xy, aClipLocalRect.zw);
+
+ return cmi;
+}
+
+struct ClipImageVertexInfo {
+ vec2 local_pos;
+ vec4 world_pos;
+};
+
+// This differs from write_clip_tile_vertex in that we forward transform the
+// primitive's local-space tile rect into the target space. We use scissoring
+// to ensure that the primitive does not draw outside the target bounds.
+ClipImageVertexInfo write_clip_image_vertex(RectWithEndpoint tile_rect,
+ RectWithEndpoint local_clip_rect,
+ Transform prim_transform,
+ Transform clip_transform,
+ RectWithEndpoint sub_rect,
+ vec2 task_origin,
+ vec2 screen_origin,
+ float device_pixel_scale) {
+ vec2 local_pos = rect_clamp(local_clip_rect, mix(tile_rect.p0, tile_rect.p1, aPosition.xy));
+ vec4 world_pos = prim_transform.m * vec4(local_pos, 0.0, 1.0);
+ vec4 final_pos = vec4(
+ world_pos.xy * device_pixel_scale + (task_origin - screen_origin) * world_pos.w,
+ 0.0,
+ world_pos.w
+ );
+ gl_Position = uTransform * final_pos;
+
+ init_transform_vs(
+ clip_transform.is_axis_aligned
+ ? vec4(vec2(-1.0e16), vec2(1.0e16))
+ : vec4(local_clip_rect.p0, local_clip_rect.p1));
+
+ ClipImageVertexInfo vi = ClipImageVertexInfo(local_pos, world_pos);
+ return vi;
+}
+
+void main(void) {
+ ClipMaskInstanceImage cmi = fetch_clip_item();
+ Transform clip_transform = fetch_transform(cmi.base.clip_transform_id);
+ Transform prim_transform = fetch_transform(cmi.base.prim_transform_id);
+ ImageSource res = fetch_image_source_direct(cmi.resource_address);
+
+ ClipImageVertexInfo vi = write_clip_image_vertex(
+ cmi.tile_rect,
+ cmi.local_rect,
+ prim_transform,
+ clip_transform,
+ cmi.base.sub_rect,
+ cmi.base.task_origin,
+ cmi.base.screen_origin,
+ cmi.base.device_pixel_scale
+ );
+ vLocalPos = vi.local_pos;
+ vec2 uv = (vi.local_pos - cmi.tile_rect.p0) / rect_size(cmi.tile_rect);
+
+ vec2 texture_size = vec2(TEX_SIZE(sColor0));
+ vec4 uv_rect = vec4(res.uv_rect.p0, res.uv_rect.p1);
+ vClipMaskImageUv = mix(uv_rect.xy, uv_rect.zw, uv) / texture_size;
+
+ // applying a half-texel offset to the UV boundaries to prevent linear samples from the outside
+ vClipMaskUvInnerRect = (uv_rect + vec4(0.5, 0.5, -0.5, -0.5)) / texture_size.xyxy;
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+void main(void) {
+ float alpha = init_transform_rough_fs(vLocalPos);
+ vec2 source_uv = clamp(vClipMaskImageUv, vClipMaskUvInnerRect.xy, vClipMaskUvInnerRect.zw);
+ float clip_alpha = texture(sColor0, source_uv).r; //careful: texture has type A8
+ oFragColor = vec4(mix(1.0, clip_alpha, alpha), 0.0, 0.0, 1.0);
+}
+
+#ifdef SWGL_DRAW_SPAN
+void swgl_drawSpanR8() {
+ if (has_valid_transform_bounds()) {
+ return;
+ }
+
+ swgl_commitTextureLinearR8(sColor0, vClipMaskImageUv, vClipMaskUvInnerRect);
+}
+#endif
+
+#endif
diff --git a/gfx/wr/webrender/res/cs_clip_rectangle.glsl b/gfx/wr/webrender/res/cs_clip_rectangle.glsl
new file mode 100644
index 0000000000..8a6aabc710
--- /dev/null
+++ b/gfx/wr/webrender/res/cs_clip_rectangle.glsl
@@ -0,0 +1,498 @@
+/* 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/. */
+
+#include shared,clip_shared,ellipse
+
+varying highp vec4 vLocalPos;
+#ifdef WR_FEATURE_FAST_PATH
+flat varying mediump vec3 vClipParams; // xy = box size, z = radius
+#else
+flat varying highp vec4 vClipCenter_Radius_TL;
+flat varying highp vec4 vClipCenter_Radius_TR;
+flat varying highp vec4 vClipCenter_Radius_BL;
+flat varying highp vec4 vClipCenter_Radius_BR;
+flat varying highp vec3 vClipPlane_TL;
+flat varying highp vec3 vClipPlane_TR;
+flat varying highp vec3 vClipPlane_BL;
+flat varying highp vec3 vClipPlane_BR;
+#endif
+// Clip mode. Packed in to a vector to work around bug 1630356.
+flat varying mediump vec2 vClipMode;
+
+#ifdef WR_VERTEX_SHADER
+
+PER_INSTANCE in vec2 aClipLocalPos;
+PER_INSTANCE in vec4 aClipLocalRect;
+PER_INSTANCE in float aClipMode;
+PER_INSTANCE in vec4 aClipRect_TL;
+PER_INSTANCE in vec4 aClipRadii_TL;
+PER_INSTANCE in vec4 aClipRect_TR;
+PER_INSTANCE in vec4 aClipRadii_TR;
+PER_INSTANCE in vec4 aClipRect_BL;
+PER_INSTANCE in vec4 aClipRadii_BL;
+PER_INSTANCE in vec4 aClipRect_BR;
+PER_INSTANCE in vec4 aClipRadii_BR;
+
+struct ClipMaskInstanceRect {
+ ClipMaskInstanceCommon base;
+ vec2 local_pos;
+};
+
+ClipMaskInstanceRect fetch_clip_item() {
+ ClipMaskInstanceRect cmi;
+
+ cmi.base = fetch_clip_item_common();
+ cmi.local_pos = aClipLocalPos;
+
+ return cmi;
+}
+
+struct ClipRect {
+ RectWithEndpoint rect;
+ float mode;
+};
+
+struct ClipCorner {
+ RectWithEndpoint rect;
+ vec4 outer_inner_radius;
+};
+
+struct ClipData {
+ ClipRect rect;
+ ClipCorner top_left;
+ ClipCorner top_right;
+ ClipCorner bottom_left;
+ ClipCorner bottom_right;
+};
+
+ClipData fetch_clip() {
+ ClipData clip;
+
+ clip.rect = ClipRect(RectWithEndpoint(aClipLocalRect.xy, aClipLocalRect.zw), aClipMode);
+ clip.top_left = ClipCorner(RectWithEndpoint(aClipRect_TL.xy, aClipRect_TL.zw), aClipRadii_TL);
+ clip.top_right = ClipCorner(RectWithEndpoint(aClipRect_TR.xy, aClipRect_TR.zw), aClipRadii_TR);
+ clip.bottom_left = ClipCorner(RectWithEndpoint(aClipRect_BL.xy, aClipRect_BL.zw), aClipRadii_BL);
+ clip.bottom_right = ClipCorner(RectWithEndpoint(aClipRect_BR.xy, aClipRect_BR.zw), aClipRadii_BR);
+
+ return clip;
+}
+
+void main(void) {
+ ClipMaskInstanceRect cmi = fetch_clip_item();
+ Transform clip_transform = fetch_transform(cmi.base.clip_transform_id);
+ Transform prim_transform = fetch_transform(cmi.base.prim_transform_id);
+ ClipData clip = fetch_clip();
+
+ RectWithEndpoint local_rect = clip.rect.rect;
+ vec2 diff = cmi.local_pos - local_rect.p0;
+ local_rect.p0 = cmi.local_pos;
+ local_rect.p1 += diff;
+
+ ClipVertexInfo vi = write_clip_tile_vertex(
+ local_rect,
+ prim_transform,
+ clip_transform,
+ cmi.base.sub_rect,
+ cmi.base.task_origin,
+ cmi.base.screen_origin,
+ cmi.base.device_pixel_scale
+ );
+
+ vClipMode.x = clip.rect.mode;
+ vLocalPos = vi.local_pos;
+
+#ifdef WR_FEATURE_FAST_PATH
+ // If the radii are all uniform, we can use a much simpler 2d
+ // signed distance function to get a rounded rect clip.
+ vec2 half_size = 0.5 * rect_size(local_rect);
+ float radius = clip.top_left.outer_inner_radius.x;
+ vLocalPos.xy -= (half_size + cmi.local_pos) * vi.local_pos.w;
+ vClipParams = vec3(half_size - vec2(radius), radius);
+#else
+ RectWithEndpoint clip_rect = local_rect;
+
+ vec2 r_tl = clip.top_left.outer_inner_radius.xy;
+ vec2 r_tr = clip.top_right.outer_inner_radius.xy;
+ vec2 r_br = clip.bottom_right.outer_inner_radius.xy;
+ vec2 r_bl = clip.bottom_left.outer_inner_radius.xy;
+
+ vClipCenter_Radius_TL = vec4(clip_rect.p0 + r_tl,
+ inverse_radii_squared(r_tl));
+
+ vClipCenter_Radius_TR = vec4(clip_rect.p1.x - r_tr.x,
+ clip_rect.p0.y + r_tr.y,
+ inverse_radii_squared(r_tr));
+
+ vClipCenter_Radius_BR = vec4(clip_rect.p1 - r_br,
+ inverse_radii_squared(r_br));
+
+ vClipCenter_Radius_BL = vec4(clip_rect.p0.x + r_bl.x,
+ clip_rect.p1.y - r_bl.y,
+ inverse_radii_squared(r_bl));
+
+ // We need to know the half-spaces of the corners separate from the center
+ // and radius. We compute a point that falls on the diagonal (which is just
+ // an inner vertex pushed out along one axis, but not on both) to get the
+ // plane offset of the half-space. We also compute the direction vector of
+ // the half-space, which is a perpendicular vertex (-y,x) of the vector of
+ // the diagonal. We leave the scales of the vectors unchanged.
+ vec2 n_tl = -r_tl.yx;
+ vec2 n_tr = vec2(r_tr.y, -r_tr.x);
+ vec2 n_br = r_br.yx;
+ vec2 n_bl = vec2(-r_bl.y, r_bl.x);
+ vClipPlane_TL = vec3(n_tl,
+ dot(n_tl, vec2(clip_rect.p0.x, clip_rect.p0.y + r_tl.y)));
+ vClipPlane_TR = vec3(n_tr,
+ dot(n_tr, vec2(clip_rect.p1.x - r_tr.x, clip_rect.p0.y)));
+ vClipPlane_BR = vec3(n_br,
+ dot(n_br, vec2(clip_rect.p1.x, clip_rect.p1.y - r_br.y)));
+ vClipPlane_BL = vec3(n_bl,
+ dot(n_bl, vec2(clip_rect.p0.x + r_bl.x, clip_rect.p1.y)));
+#endif
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+
+#ifdef WR_FEATURE_FAST_PATH
+// See http://www.iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm
+float sd_box(in vec2 pos, in vec2 box_size) {
+ vec2 d = abs(pos) - box_size;
+ return length(max(d, vec2(0.0))) + min(max(d.x,d.y), 0.0);
+}
+
+float sd_rounded_box(in vec2 pos, in vec2 box_size, in float radius) {
+ return sd_box(pos, box_size) - radius;
+}
+#endif
+
+void main(void) {
+ vec2 local_pos = vLocalPos.xy / vLocalPos.w;
+ float aa_range = compute_aa_range(local_pos);
+
+#ifdef WR_FEATURE_FAST_PATH
+ float dist = sd_rounded_box(local_pos, vClipParams.xy, vClipParams.z);
+#else
+ float dist = distance_to_rounded_rect(
+ local_pos,
+ vClipPlane_TL,
+ vClipCenter_Radius_TL,
+ vClipPlane_TR,
+ vClipCenter_Radius_TR,
+ vClipPlane_BR,
+ vClipCenter_Radius_BR,
+ vClipPlane_BL,
+ vClipCenter_Radius_BL,
+ vTransformBounds
+ );
+#endif
+
+ // Compute AA for the given dist and range.
+ float alpha = distance_aa(aa_range, dist);
+
+ // Select alpha or inverse alpha depending on clip in/out.
+ float final_alpha = mix(alpha, 1.0 - alpha, vClipMode.x);
+
+ float final_final_alpha = vLocalPos.w > 0.0 ? final_alpha : 0.0;
+ oFragColor = vec4(final_final_alpha, 0.0, 0.0, 1.0);
+}
+
+#ifdef SWGL_DRAW_SPAN
+// Currently the cs_clip_rectangle shader is slow because it always evaluates
+// the corner ellipse segments and the rectangle AA for every fragment the
+// shader is run on. To alleviate this for now with SWGL, this essentially
+// implements a rounded-rectangle span rasterizer inside the span shader. The
+// motivation is that we can separate out the parts of the span which are fully
+// opaque and fully transparent, outputting runs of fixed color in those areas,
+// while only evaluating the ellipse segments and AA in the smaller outlying
+// parts of the span that actually need it.
+// The shader conceptually represents a rounded rectangle as an inner octagon
+// (8 half-spaces) bounding the opaque region and an outer octagon bounding the
+// curve and AA parts. Everything outside is transparent. The line of the span
+// is intersected with half-spaces, looking for interior spans that minimally
+// intersect the half-spaces (start max, end min). In the ideal case we hit a
+// start corner ellipse segment and an end corner ellipse segment, rendering
+// the two curves on the ends with an opaque run in between, outputting clear
+// for any transparent runs before and after the start and end curves.
+// This is slightly complicated by the fact that the results here must agree
+// with the main results of the fragment shader, in case SWGL has to fall back
+// to the main fragment shader for any reason. So, we make an effort to handle
+// both ways of operating - the uniform radius fast-path and the varying radius
+// slow-path.
+void swgl_drawSpanR8() {
+ // Perspective is not supported.
+ if (swgl_interpStep(vLocalPos).w != 0.0) {
+ return;
+ }
+
+ // If the span is completely outside the Z-range and clipped out, just
+ // output clear so we don't need to consider invalid W in the rest of the
+ // shader.
+ float w = swgl_forceScalar(vLocalPos.w);
+ if (w <= 0.0) {
+ swgl_commitSolidR8(0.0);
+ return;
+ }
+
+ // To start, we evaluate the rounded-rectangle in local space relative to
+ // the local-space position. This will be interpolated across the span to
+ // track whether we intersect any half-spaces.
+ w = 1.0 / w;
+ vec2 local_pos = vLocalPos.xy * w;
+ vec2 local_pos0 = swgl_forceScalar(local_pos);
+ vec2 local_step = swgl_interpStep(vLocalPos).xy * w;
+ float step_scale = max(dot(local_step, local_step), 1.0e-6);
+
+ // Get the local-space AA range. This range represents 1/fwidth(local_pos),
+ // essentially the scale of how much local-space maps to an AA pixel. We
+ // need to know the inverse, how much local-space we traverse per AA pixel
+ // pixel step. We then scale this to represent the amount of span steps
+ // traversed per AA pixel step.
+ float aa_range = compute_aa_range(local_pos);
+ float aa_margin = inversesqrt(aa_range * aa_range * step_scale);
+
+ // We need to know the bounds of the aligned rectangle portion of the rrect
+ // in local-space. If we're using the fast-path, this is specified as the
+ // inner bounding-box half-width of the rrect and the uniform outer radius
+ // of the corners in vClipParams, which we map to the outer bounding-box.
+ // For the general case, we have already stored the outer bounding box in
+ // vTransformBounds.
+ #ifdef WR_FEATURE_FAST_PATH
+ vec4 clip_rect = vec4(-vClipParams.xy - vClipParams.z, vClipParams.xy + vClipParams.z);
+ #else
+ vec4 clip_rect = vTransformBounds;
+ #endif
+
+ // We need to compute the local-space distance to the bounding box and then
+ // figure out how many processing steps that maps to. If we are stepping in
+ // a negative direction on an axis, we need to swap the sides of the box
+ // which we consider as the start or end. If there is no local-space step
+ // on an axis (i.e. constant Y), we need to take care to force the steps to
+ // either the start or end of the span depending on if we are inside or
+ // outside of the bounding box.
+ vec4 clip_dist =
+ mix(clip_rect, clip_rect.zwxy, lessThan(local_step, vec2(0.0)).xyxy)
+ - local_pos0.xyxy;
+ clip_dist =
+ mix(1.0e6 * step(0.0, clip_dist),
+ clip_dist * recip(local_step).xyxy,
+ notEqual(local_step, vec2(0.0)).xyxy);
+
+ // Initially, the opaque region is bounded by the further start intersect
+ // with the bounding box and the nearest end intersect with the bounding
+ // box.
+ float opaque_start = max(clip_dist.x, clip_dist.y);
+ float opaque_end = min(clip_dist.z, clip_dist.w);
+ float aa_start = opaque_start;
+ float aa_end = opaque_end;
+
+ // Here we actually intersect with the half-space of the corner. We get the
+ // plane distance of the local-space position from the diagonal bounding
+ // ellipse segment from the opaque region. The half-space is defined by the
+ // direction vector of the plane and an offset point that falls on the
+ // dividing line (which is a vertex on the corner box, which is actually on
+ // the outer radius of the bounding box, but not a corner vertex). This
+ // distance is positive if on the curve side and negative if on the inner
+ // opaque region. If we are on the curve side, we need to verify we are
+ // traveling in direction towards the opaque region so that we will
+ // eventually intersect the diagonal so we can calculate when the start
+ // corner segment will end, otherwise we are going away from the rrect.
+ // If we are inside the opaque interior, we need to verify we are traveling
+ // in direction towards the curve, so that we can calculate when the end
+ // corner segment will start. Further, if we intersect, we calculate the
+ // offset of the outer octagon where AA starts from the inner octagon of
+ // where the opaque region starts using the apex vector (which is transpose
+ // of the half-space's direction).
+ //
+ // We need to intersect the corner ellipse segments. Significantly, we need
+ // to know where the apex of the ellipse segment is and how far to push the
+ // outer diagonal of the octagon from the inner diagonal. The position of
+ // the inner diagonal simply runs diagonal across the corner box and has a
+ // constant offset from vertex on the inner bounding box. The apex also has
+ // a constant offset along the opposite diagonal relative to the diagonal
+ // intersect which is 1/sqrt(2) - 0.5 assuming unit length for the diagonal.
+ // We then need to project the vector to the apex onto the local-space step
+ // scale, but we do this with reference to the normal vector of the diagonal
+ // using dot(normal, apex) / dot(normal, local_step), where the apex vector
+ // is (0.7071 - 0.5) * abs(normal).yx * sign(normal).
+ vec3 start_plane = vec3(1.0e6);
+ vec3 end_plane = vec3(1.0e6);
+
+ // plane is assumed to be a vec3 with normal in (X, Y) and offset in Z.
+ #define CLIP_CORNER(plane, info) do { \
+ float dist = dot(local_pos0, plane.xy) - plane.z; \
+ float scale = -dot(local_step, plane.xy); \
+ if (scale >= 0.0) { \
+ if (dist > opaque_start * scale) { \
+ SET_CORNER(start_corner, info); \
+ start_plane = plane; \
+ float inv_scale = recip(max(scale, 1.0e-6)); \
+ opaque_start = dist * inv_scale; \
+ float apex = (0.7071 - 0.5) * 2.0 * abs(plane.x * plane.y); \
+ aa_start = opaque_start - apex * inv_scale; \
+ } \
+ } else if (dist > opaque_end * scale) { \
+ SET_CORNER(end_corner, info); \
+ end_plane = plane; \
+ float inv_scale = recip(min(scale, -1.0e-6)); \
+ opaque_end = dist * inv_scale; \
+ float apex = (0.7071 - 0.5) * 2.0 * abs(plane.x * plane.y); \
+ aa_end = opaque_end - apex * inv_scale; \
+ } \
+ } while (false)
+
+ #ifdef WR_FEATURE_FAST_PATH
+ // For the fast-path, we only have the half-width of the inner bounding
+ // box. We need to map this to points that fall on the diagonal of the
+ // half-space for each corner. To do this we just need to push out the
+ // vertex in the right direction on a single axis, leaving the other
+ // unchanged.
+ // However, since the corner radii are all the same, and since the local
+ // origin of each ellipse is assumed to be at (0, 0), the plane offset
+ // of the half-space is the same for each case. So given a corner offset
+ // of (x+z, y) and a vector of (z, z), the dot product becomes:
+ // (x+z)*z + y*z == x*z + y*z + z*z
+ // The direction vector of the corner half-space has constant length,
+ // but just needs an appropriate direction set.
+ float offset = (vClipParams.x + vClipParams.y + vClipParams.z) * vClipParams.z;
+ vec3 plane_tl = vec3(-vClipParams.zz, offset);
+ vec3 plane_tr = vec3(vClipParams.z, -vClipParams.z, offset);
+ vec3 plane_br = vec3(vClipParams.zz, offset);
+ vec3 plane_bl = vec3(-vClipParams.z, vClipParams.z, offset);
+
+ #define SET_CORNER(corner, info)
+
+ // Clip against the corner half-spaces.
+ CLIP_CORNER(plane_tl, );
+ CLIP_CORNER(plane_tr, );
+ CLIP_CORNER(plane_br, );
+ CLIP_CORNER(plane_bl, );
+
+ // Later we need to calculate distance AA for both corners and the
+ // outer bounding rect. For the fast-path, this is all done inside
+ // sd_rounded_box.
+ #define AA_RECT(local_pos) \
+ sd_rounded_box(local_pos, vClipParams.xy, vClipParams.z)
+ #else
+ // For the general case, we need to remember which of the actual start
+ // and end corners we intersect, so that we can evaluate the curve AA
+ // against only those corners rather than having to try against all 4
+ // corners for both sides of the span. Initialize these values so that
+ // if no corner is intersected, they will just zero the AA.
+ vec4 start_corner = vec4(vec2(1.0e6), vec2(1.0));
+ vec4 end_corner = vec4(vec2(1.0e6), vec2(1.0));
+
+ #define SET_CORNER(corner, info) corner = info
+
+ // Clip against the corner half-spaces. We have already computed the
+ // corner half-spaces in the vertex shader.
+ CLIP_CORNER(vClipPlane_TL, vClipCenter_Radius_TL);
+ CLIP_CORNER(vClipPlane_TR, vClipCenter_Radius_TR);
+ CLIP_CORNER(vClipPlane_BR, vClipCenter_Radius_BR);
+ CLIP_CORNER(vClipPlane_BL, vClipCenter_Radius_BL);
+
+ // Later we need to calculate distance AA for both corners and the
+ // outer bounding rect. For the general case, we need to explicitly
+ // evaluate either the ellipse segment distance or the rect distance.
+ #define AA_RECT(local_pos) \
+ signed_distance_rect(local_pos, vTransformBounds.xy, vTransformBounds.zw)
+ #define AA_CORNER(local_pos, corner) \
+ distance_to_ellipse_approx(local_pos - corner.xy, corner.zw, 1.0)
+ #endif
+
+ // Pad the AA region by a margin, as the intersections take place assuming
+ // pixel centers, but AA actually starts half a pixel away from the center.
+ // If the AA region narrows to nothing, be careful not to inflate so much
+ // that we start processing AA for fragments that don't need it.
+ aa_margin = max(aa_margin - max(aa_start - aa_end, 0.0), 0.0);
+ aa_start -= aa_margin;
+ aa_end += aa_margin;
+
+ // Compute the thresholds at which we need to transition between various
+ // segments of the span, from fully transparent outside to the start of
+ // the outer octagon where AA starts, from there to where the inner opaque
+ // octagon starts, from there to where the opaque inner octagon ends and
+ // AA starts again, to finally where the outer octagon/AA ends and we're
+ // back to fully transparent. These thresholds are just flipped offsets
+ // from the start of the span so we can compare against the remaining
+ // span length which automatically deducts as we commit fragments.
+ ivec4 steps = ivec4(clamp(
+ swgl_SpanLength -
+ swgl_StepSize *
+ vec4(floor(aa_start), ceil(opaque_start), floor(opaque_end), ceil(aa_end)),
+ 0.0, swgl_SpanLength));
+ int aa_start_len = steps.x;
+ int opaque_start_len = steps.y;
+ int opaque_end_len = steps.z;
+ int aa_end_len = steps.w;
+
+ // Output fully clear while we're outside the AA region.
+ if (swgl_SpanLength > aa_start_len) {
+ int num_aa = swgl_SpanLength - aa_start_len;
+ swgl_commitPartialSolidR8(num_aa, vClipMode.x);
+ local_pos += float(num_aa / swgl_StepSize) * local_step;
+ }
+ #ifdef AA_CORNER
+ if (start_plane.x < 1.0e5) {
+ // We're now in the outer octagon which requires AA. Evaluate the corner
+ // distance of the start corner here and output AA for it. Before we hit
+ // the actual opaque inner octagon, we have a transitional step where the
+ // diagonal might intersect mid-way through the step. We have consider
+ // either the corner or rect distance depending on which side we're on.
+ while (swgl_SpanLength > opaque_start_len) {
+ float alpha = distance_aa(aa_range,
+ dot(local_pos, start_plane.xy) > start_plane.z
+ ? AA_CORNER(local_pos, start_corner)
+ : AA_RECT(local_pos));
+ swgl_commitColorR8(mix(alpha, 1.0 - alpha, vClipMode.x));
+ local_pos += local_step;
+ }
+ }
+ #endif
+ // If there's no start corner, just do rect AA until opaque.
+ while (swgl_SpanLength > opaque_start_len) {
+ float alpha = distance_aa(aa_range, AA_RECT(local_pos));
+ swgl_commitColorR8(mix(alpha, 1.0 - alpha, vClipMode.x));
+ local_pos += local_step;
+ }
+ // Now we're finally in the opaque inner octagon part of the span. Just
+ // output a solid run.
+ if (swgl_SpanLength > opaque_end_len) {
+ int num_opaque = swgl_SpanLength - opaque_end_len;
+ swgl_commitPartialSolidR8(num_opaque, 1.0 - vClipMode.x);
+ local_pos += float(num_opaque / swgl_StepSize) * local_step;
+ }
+ #ifdef AA_CORNER
+ if (end_plane.x < 1.0e5) {
+ // Finally we're in the AA region on the other side, inside the outer
+ // octagon again. Just evaluate the distance to the end corner and
+ // compute AA for it. We're leaving the opaque inner octagon, but like
+ // before, we have to be careful we're not dealing with a step partially
+ // intersected by the end corner's diagonal. Check which side we are on
+ // and use either the corner or rect distance as appropriate.
+ while (swgl_SpanLength > aa_end_len) {
+ float alpha = distance_aa(aa_range,
+ dot(local_pos, end_plane.xy) > end_plane.z
+ ? AA_CORNER(local_pos, end_corner)
+ : AA_RECT(local_pos));
+ swgl_commitColorR8(mix(alpha, 1.0 - alpha, vClipMode.x));
+ local_pos += local_step;
+ }
+ }
+ #endif
+ // If there's no end corner, just do rect AA until clear.
+ while (swgl_SpanLength > aa_end_len) {
+ float alpha = distance_aa(aa_range, AA_RECT(local_pos));
+ swgl_commitColorR8(mix(alpha, 1.0 - alpha, vClipMode.x));
+ local_pos += local_step;
+ }
+ // We're now outside the outer AA octagon on the other side. Just output
+ // fully clear.
+ if (swgl_SpanLength > 0) {
+ swgl_commitPartialSolidR8(swgl_SpanLength, vClipMode.x);
+ }
+}
+#endif
+
+#endif
diff --git a/gfx/wr/webrender/res/cs_conic_gradient.glsl b/gfx/wr/webrender/res/cs_conic_gradient.glsl
new file mode 100644
index 0000000000..551fc8a90b
--- /dev/null
+++ b/gfx/wr/webrender/res/cs_conic_gradient.glsl
@@ -0,0 +1,67 @@
+/* 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/. */
+
+#include shared,rect,render_task,gpu_cache,gpu_buffer,gradient
+
+#define PI 3.141592653589793
+
+varying highp vec2 v_pos;
+
+flat varying highp vec2 v_center;
+
+// x: start offset, y: offset scale, z: angle
+// Packed in to a vector to work around bug 1630356.
+flat varying highp vec3 v_start_offset_offset_scale_angle_vec;
+#define v_start_offset v_start_offset_offset_scale_angle_vec.x
+#define v_offset_scale v_start_offset_offset_scale_angle_vec.y
+#define v_angle v_start_offset_offset_scale_angle_vec.z
+
+#ifdef WR_VERTEX_SHADER
+
+#define EXTEND_MODE_REPEAT 1
+
+PER_INSTANCE in vec4 aTaskRect;
+PER_INSTANCE in vec2 aCenter;
+PER_INSTANCE in vec2 aScale;
+PER_INSTANCE in float aStartOffset;
+PER_INSTANCE in float aEndOffset;
+PER_INSTANCE in float aAngle;
+PER_INSTANCE in int aExtendMode;
+PER_INSTANCE in int aGradientStopsAddress;
+
+void main(void) {
+ // Store 1/d where d = end_offset - start_offset
+ // If d = 0, we can't get its reciprocal. Instead, just use a zero scale.
+ float d = aEndOffset - aStartOffset;
+ v_offset_scale = d != 0.0 ? 1.0 / d : 0.0;
+
+ vec2 pos = mix(aTaskRect.xy, aTaskRect.zw, aPosition.xy);
+ gl_Position = uTransform * vec4(pos, 0.0, 1.0);
+
+ v_angle = PI / 2.0 - aAngle;
+ v_start_offset = aStartOffset * v_offset_scale;
+
+ // v_pos and v_center are in a coordinate space relative to the task rect
+ // (so they are independent of the task origin).
+ v_center = aCenter * v_offset_scale;
+ v_pos = (aTaskRect.zw - aTaskRect.xy) * aPosition.xy * v_offset_scale * aScale;
+
+ v_gradient_repeat.x = float(aExtendMode == EXTEND_MODE_REPEAT);
+ v_gradient_address.x = aGradientStopsAddress;
+}
+#endif
+
+
+#ifdef WR_FRAGMENT_SHADER
+
+void main(void) {
+ // Use inverse trig to find the angle offset from the relative position.
+ vec2 current_dir = v_pos - v_center;
+ float current_angle = atan(current_dir.y, current_dir.x) + v_angle;
+ float offset = fract(current_angle / (2.0 * PI)) * v_offset_scale - v_start_offset;
+
+ oFragColor = sample_gradient(offset);
+}
+
+#endif
diff --git a/gfx/wr/webrender/res/cs_fast_linear_gradient.glsl b/gfx/wr/webrender/res/cs_fast_linear_gradient.glsl
new file mode 100644
index 0000000000..36b3368dfe
--- /dev/null
+++ b/gfx/wr/webrender/res/cs_fast_linear_gradient.glsl
@@ -0,0 +1,32 @@
+/* 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/. */
+
+#include shared
+
+varying highp float vPos;
+flat varying mediump vec4 vColor0;
+flat varying mediump vec4 vColor1;
+
+#ifdef WR_VERTEX_SHADER
+
+PER_INSTANCE in vec4 aTaskRect;
+PER_INSTANCE in vec4 aColor0;
+PER_INSTANCE in vec4 aColor1;
+PER_INSTANCE in float aAxisSelect;
+
+void main(void) {
+ vPos = mix(0.0, 1.0, mix(aPosition.x, aPosition.y, aAxisSelect));
+
+ vColor0 = aColor0;
+ vColor1 = aColor1;
+
+ gl_Position = uTransform * vec4(mix(aTaskRect.xy, aTaskRect.zw, aPosition.xy), 0.0, 1.0);
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+void main(void) {
+ oFragColor = mix(vColor0, vColor1, vPos);
+}
+#endif
diff --git a/gfx/wr/webrender/res/cs_line_decoration.glsl b/gfx/wr/webrender/res/cs_line_decoration.glsl
new file mode 100644
index 0000000000..00ed2e249a
--- /dev/null
+++ b/gfx/wr/webrender/res/cs_line_decoration.glsl
@@ -0,0 +1,165 @@
+/* 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/. */
+
+#include shared
+
+#define LINE_STYLE_SOLID 0
+#define LINE_STYLE_DOTTED 1
+#define LINE_STYLE_DASHED 2
+#define LINE_STYLE_WAVY 3
+
+// Fragment position in the coordinate system used for positioning decorations.
+// To keep the code independent of whether the line is horizontal or vertical,
+// vLocalPos.x is always parallel, and .y always perpendicular, to the line
+// being decorated.
+varying highp vec2 vLocalPos;
+
+// Line style. Packed in to a vector to work around bug 1630356.
+flat varying mediump ivec2 vStyle;
+
+flat varying mediump vec4 vParams;
+
+#ifdef WR_VERTEX_SHADER
+
+// The size of the mask tile we're rendering, in pixels.
+PER_INSTANCE in vec4 aTaskRect;
+
+// The size of the mask tile. aLocalSize.x is always horizontal and .y vertical,
+// regardless of the line's orientation. The size is chosen by
+// prim_store::line_dec::get_line_decoration_sizes.
+PER_INSTANCE in vec2 aLocalSize;
+
+// A LINE_STYLE_* value, indicating what sort of line to draw.
+PER_INSTANCE in int aStyle;
+
+// 0.0 for a horizontal line, 1.0 for a vertical line.
+PER_INSTANCE in float aAxisSelect;
+
+// The thickness of the wavy line itself, not the amplitude of the waves (i.e.,
+// the thickness of the final decorated line).
+PER_INSTANCE in float aWavyLineThickness;
+
+void main(void) {
+ vec2 size = mix(aLocalSize, aLocalSize.yx, aAxisSelect);
+ vStyle.x = aStyle;
+
+ switch (vStyle.x) {
+ case LINE_STYLE_SOLID: {
+ break;
+ }
+ case LINE_STYLE_DASHED: {
+ vParams = vec4(size.x, // period
+ 0.5 * size.x, // dash length
+ 0.0,
+ 0.0);
+ break;
+ }
+ case LINE_STYLE_DOTTED: {
+ float diameter = size.y;
+ float period = diameter * 2.0;
+ float center_line = 0.5 * size.y;
+ vParams = vec4(period,
+ diameter / 2.0, // radius
+ center_line,
+ 0.0);
+ break;
+ }
+ case LINE_STYLE_WAVY: {
+ // This logic copied from gecko to get the same results
+ float line_thickness = max(aWavyLineThickness, 1.0);
+ // Difference in height between peaks and troughs
+ // (and since slopes are 45 degrees, the length of each slope)
+ float slope_length = size.y - line_thickness;
+ // Length of flat runs
+ float flat_length = max((line_thickness - 1.0) * 2.0, 1.0);
+
+ vParams = vec4(line_thickness / 2.0,
+ slope_length,
+ flat_length,
+ size.y);
+ break;
+ }
+ default:
+ vParams = vec4(0.0);
+ }
+
+ vLocalPos = mix(aPosition.xy, aPosition.yx, aAxisSelect) * size;
+
+ gl_Position = uTransform * vec4(mix(aTaskRect.xy, aTaskRect.zw, aPosition.xy), 0.0, 1.0);
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+
+#define MAGIC_WAVY_LINE_AA_SNAP 0.5
+
+void main(void) {
+ // Find the appropriate distance to apply the step over.
+ vec2 pos = vLocalPos;
+ float aa_range = compute_aa_range(pos);
+ float alpha = 1.0;
+
+ switch (vStyle.x) {
+ case LINE_STYLE_SOLID: {
+ break;
+ }
+ case LINE_STYLE_DASHED: {
+ // Calculate dash alpha (on/off) based on dash length
+ alpha = step(floor(pos.x + 0.5), vParams.y);
+ break;
+ }
+ case LINE_STYLE_DOTTED: {
+ // Get the dot alpha
+ vec2 dot_relative_pos = pos - vParams.yz;
+ float dot_distance = length(dot_relative_pos) - vParams.y;
+ alpha = distance_aa(aa_range, dot_distance);
+ break;
+ }
+ case LINE_STYLE_WAVY: {
+ float half_line_thickness = vParams.x;
+ float slope_length = vParams.y;
+ float flat_length = vParams.z;
+ float vertical_bounds = vParams.w;
+ // Our pattern is just two slopes and two flats
+ float half_period = slope_length + flat_length;
+
+ float mid_height = vertical_bounds / 2.0;
+ float peak_offset = mid_height - half_line_thickness;
+ // Flip the wave every half period
+ float flip = -2.0 * (step(mod(pos.x, 2.0 * half_period), half_period) - 0.5);
+ // float flip = -1.0;
+ peak_offset *= flip;
+ float peak_height = mid_height + peak_offset;
+
+ // Convert pos to a local position within one half period
+ pos.x = mod(pos.x, half_period);
+
+ // Compute signed distance to the 3 lines that make up an arc
+ float dist1 = distance_to_line(vec2(0.0, peak_height),
+ vec2(1.0, -flip),
+ pos);
+ float dist2 = distance_to_line(vec2(0.0, peak_height),
+ vec2(0, -flip),
+ pos);
+ float dist3 = distance_to_line(vec2(flat_length, peak_height),
+ vec2(-1.0, -flip),
+ pos);
+ float dist = abs(max(max(dist1, dist2), dist3));
+
+ // Apply AA based on the thickness of the wave
+ alpha = distance_aa(aa_range, dist - half_line_thickness);
+
+ // Disable AA for thin lines
+ if (half_line_thickness <= 1.0) {
+ alpha = 1.0 - step(alpha, MAGIC_WAVY_LINE_AA_SNAP);
+ }
+
+ break;
+ }
+ default: break;
+ }
+
+ oFragColor = vec4(alpha);
+}
+#endif
diff --git a/gfx/wr/webrender/res/cs_linear_gradient.glsl b/gfx/wr/webrender/res/cs_linear_gradient.glsl
new file mode 100644
index 0000000000..b1aff899a6
--- /dev/null
+++ b/gfx/wr/webrender/res/cs_linear_gradient.glsl
@@ -0,0 +1,68 @@
+/* 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/. */
+
+#include shared,rect,render_task,gpu_cache,gpu_buffer,gradient
+
+varying highp vec2 v_pos;
+
+flat varying mediump vec2 v_scale_dir;
+
+// Start offset. Packed in to a vector to work around bug 1630356.
+flat varying highp vec2 v_start_offset;
+
+#ifdef WR_VERTEX_SHADER
+
+#define EXTEND_MODE_REPEAT 1
+
+PER_INSTANCE in vec4 aTaskRect;
+PER_INSTANCE in vec2 aStartPoint;
+PER_INSTANCE in vec2 aEndPoint;
+PER_INSTANCE in vec2 aScale;
+PER_INSTANCE in int aExtendMode;
+PER_INSTANCE in int aGradientStopsAddress;
+
+void main(void) {
+ vec2 pos = mix(aTaskRect.xy, aTaskRect.zw, aPosition.xy);
+ gl_Position = uTransform * vec4(pos, 0.0, 1.0);
+
+ v_pos = aPosition.xy * aScale;
+
+ vec2 dir = aEndPoint - aStartPoint;
+
+ // Normalize UV and offsets to 0..1 scale.
+ v_scale_dir = dir / dot(dir, dir);
+ v_start_offset.x = dot(aStartPoint, v_scale_dir);
+
+ v_scale_dir *= (aTaskRect.zw - aTaskRect.xy);
+
+ v_gradient_repeat.x = float(aExtendMode == EXTEND_MODE_REPEAT);
+ v_gradient_address.x = aGradientStopsAddress;
+}
+#endif
+
+
+#ifdef WR_FRAGMENT_SHADER
+
+void main(void) {
+ // Project position onto a direction vector to compute offset.
+ float offset = dot(v_pos, v_scale_dir) - v_start_offset.x;
+
+ oFragColor = sample_gradient(offset);
+}
+
+
+#ifdef SWGL_DRAW_SPAN
+void swgl_drawSpanRGBA8() {
+ int address = swgl_validateGradient(sGpuBuffer, get_gpu_buffer_uv(v_gradient_address.x), int(GRADIENT_ENTRIES + 2.0));
+ if (address < 0) {
+ return;
+ }
+
+ swgl_commitLinearGradientRGBA8(sGpuBuffer, address, GRADIENT_ENTRIES, false, v_gradient_repeat.x != 0.0,
+ v_pos, v_scale_dir, v_start_offset.x);
+}
+#endif
+
+
+#endif
diff --git a/gfx/wr/webrender/res/cs_radial_gradient.glsl b/gfx/wr/webrender/res/cs_radial_gradient.glsl
new file mode 100644
index 0000000000..16ffe06376
--- /dev/null
+++ b/gfx/wr/webrender/res/cs_radial_gradient.glsl
@@ -0,0 +1,71 @@
+/* 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/. */
+
+#include shared,rect,render_task,gpu_cache,gpu_buffer,gradient
+
+varying highp vec2 v_pos;
+
+// Start radius. Packed in to a vector to work around bug 1630356.
+flat varying highp vec2 v_start_radius;
+
+#ifdef WR_VERTEX_SHADER
+
+#define EXTEND_MODE_REPEAT 1
+
+PER_INSTANCE in vec4 aTaskRect;
+PER_INSTANCE in vec2 aCenter;
+PER_INSTANCE in vec2 aScale;
+PER_INSTANCE in float aStartRadius;
+PER_INSTANCE in float aEndRadius;
+PER_INSTANCE in float aXYRatio;
+PER_INSTANCE in int aExtendMode;
+PER_INSTANCE in int aGradientStopsAddress;
+
+void main(void) {
+ // Store 1/rd where rd = end_radius - start_radius
+ // If rd = 0, we can't get its reciprocal. Instead, just use a zero scale.
+ float rd = aEndRadius - aStartRadius;
+ float radius_scale = rd != 0.0 ? 1.0 / rd : 0.0;
+
+ vec2 pos = mix(aTaskRect.xy, aTaskRect.zw, aPosition.xy);
+ gl_Position = uTransform * vec4(pos, 0.0, 1.0);
+
+ v_start_radius.x = aStartRadius * radius_scale;
+
+ // Transform all coordinates by the y scale so the
+ // fragment shader can work with circles
+
+ // v_pos is in a coordinate space relative to the task rect
+ // (so it is independent of the task origin).
+ v_pos = ((aTaskRect.zw - aTaskRect.xy) * aPosition.xy * aScale - aCenter) * radius_scale;
+ v_pos.y *= aXYRatio;
+
+ v_gradient_repeat.x = float(aExtendMode == EXTEND_MODE_REPEAT);
+ v_gradient_address.x = aGradientStopsAddress;
+}
+#endif
+
+
+#ifdef WR_FRAGMENT_SHADER
+
+void main(void) {
+ // Solve for t in length(pd) = v_start_radius + t * rd
+ float offset = length(v_pos) - v_start_radius.x;
+
+ oFragColor = sample_gradient(offset);
+}
+
+#ifdef SWGL_DRAW_SPAN
+void swgl_drawSpanRGBA8() {
+ int address = swgl_validateGradient(sGpuBuffer, get_gpu_buffer_uv(v_gradient_address.x),
+ int(GRADIENT_ENTRIES + 2.0));
+ if (address < 0) {
+ return;
+ }
+ swgl_commitRadialGradientRGBA8(sGpuBuffer, address, GRADIENT_ENTRIES, v_gradient_repeat.x != 0.0,
+ v_pos, v_start_radius.x);
+}
+#endif
+
+#endif
diff --git a/gfx/wr/webrender/res/cs_scale.glsl b/gfx/wr/webrender/res/cs_scale.glsl
new file mode 100644
index 0000000000..0b4f5d744b
--- /dev/null
+++ b/gfx/wr/webrender/res/cs_scale.glsl
@@ -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/. */
+
+// This shader must remain compatible with ESSL 1, at least for the
+// WR_FEATURE_TEXTURE_EXTERNAL_ESSL1 feature, so that it can be used to render
+// video on GLES devices without GL_OES_EGL_image_external_essl3 support.
+// This means we cannot use textureSize(), int inputs/outputs, etc.
+
+#include shared
+
+varying highp vec2 vUv;
+flat varying highp vec4 vUvRect;
+#ifdef WR_FEATURE_TEXTURE_EXTERNAL_ESSL1
+uniform vec2 uTextureSize;
+#endif
+
+#ifdef WR_VERTEX_SHADER
+
+PER_INSTANCE attribute vec4 aScaleTargetRect;
+PER_INSTANCE attribute vec4 aScaleSourceRect;
+
+void main(void) {
+ vec2 src_offset = aScaleSourceRect.xy;
+ vec2 src_size = aScaleSourceRect.zw - aScaleSourceRect.xy;
+
+ // If this is in WR_FEATURE_TEXTURE_RECT mode, the rect and size use
+ // non-normalized texture coordinates.
+#ifdef WR_FEATURE_TEXTURE_RECT
+ vec2 texture_size = vec2(1, 1);
+#elif defined(WR_FEATURE_TEXTURE_EXTERNAL_ESSL1)
+ vec2 texture_size = uTextureSize;
+#else
+ vec2 texture_size = vec2(TEX_SIZE(sColor0));
+#endif
+
+ // The uvs may be inverted, so use the min and max for the bounds
+ vUvRect = vec4(min(aScaleSourceRect.xy, aScaleSourceRect.zw) + vec2(0.5),
+ max(aScaleSourceRect.xy, aScaleSourceRect.zw) - vec2(0.5)) / texture_size.xyxy;
+
+ vec2 pos = mix(aScaleTargetRect.xy, aScaleTargetRect.zw, aPosition.xy);
+ vUv = (src_offset + src_size * aPosition.xy) / texture_size;
+
+ gl_Position = uTransform * vec4(pos, 0.0, 1.0);
+}
+
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+
+void main(void) {
+ vec2 st = clamp(vUv, vUvRect.xy, vUvRect.zw);
+ oFragColor = TEX_SAMPLE(sColor0, st);
+}
+
+#ifdef SWGL_DRAW_SPAN
+void swgl_drawSpanRGBA8() {
+ swgl_commitTextureLinearRGBA8(sColor0, vUv, vUvRect);
+}
+#endif
+
+#endif
diff --git a/gfx/wr/webrender/res/cs_svg_filter.glsl b/gfx/wr/webrender/res/cs_svg_filter.glsl
new file mode 100644
index 0000000000..6ccdda90d6
--- /dev/null
+++ b/gfx/wr/webrender/res/cs_svg_filter.glsl
@@ -0,0 +1,594 @@
+/* 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/. */
+
+#define WR_FEATURE_TEXTURE_2D
+
+#include shared,prim_shared
+
+varying highp vec2 vInput1Uv;
+varying highp vec2 vInput2Uv;
+flat varying highp vec4 vInput1UvRect;
+flat varying highp vec4 vInput2UvRect;
+flat varying mediump ivec4 vData;
+flat varying mediump vec4 vFilterData0;
+flat varying mediump vec4 vFilterData1;
+
+// x: Filter input count, y: Filter kind.
+// Packed in to a vector to work around bug 1630356.
+flat varying mediump ivec2 vFilterInputCountFilterKindVec;
+#define vFilterInputCount vFilterInputCountFilterKindVec.x
+#define vFilterKind vFilterInputCountFilterKindVec.y
+// Packed in to a vector to work around bug 1630356.
+flat varying mediump vec2 vFloat0;
+
+flat varying mediump mat4 vColorMat;
+flat varying mediump ivec4 vFuncs;
+
+#define FILTER_BLEND 0
+#define FILTER_FLOOD 1
+#define FILTER_LINEAR_TO_SRGB 2
+#define FILTER_SRGB_TO_LINEAR 3
+#define FILTER_OPACITY 4
+#define FILTER_COLOR_MATRIX 5
+#define FILTER_DROP_SHADOW 6
+#define FILTER_OFFSET 7
+#define FILTER_COMPONENT_TRANSFER 8
+#define FILTER_IDENTITY 9
+#define FILTER_COMPOSITE 10
+
+#define COMPOSITE_OVER 0
+#define COMPOSITE_IN 1
+#define COMPOSITE_OUT 2
+#define COMPOSITE_ATOP 3
+#define COMPOSITE_XOR 4
+#define COMPOSITE_LIGHTER 5
+#define COMPOSITE_ARITHMETIC 6
+
+#ifdef WR_VERTEX_SHADER
+
+PER_INSTANCE in int aFilterRenderTaskAddress;
+PER_INSTANCE in int aFilterInput1TaskAddress;
+PER_INSTANCE in int aFilterInput2TaskAddress;
+PER_INSTANCE in int aFilterKind;
+PER_INSTANCE in int aFilterInputCount;
+PER_INSTANCE in int aFilterGenericInt;
+PER_INSTANCE in ivec2 aFilterExtraDataAddress;
+
+struct FilterTask {
+ RectWithEndpoint task_rect;
+ vec3 user_data;
+};
+
+FilterTask fetch_filter_task(int address) {
+ RenderTaskData task_data = fetch_render_task_data(address);
+
+ FilterTask task = FilterTask(
+ task_data.task_rect,
+ task_data.user_data.xyz
+ );
+
+ return task;
+}
+
+vec4 compute_uv_rect(RectWithEndpoint task_rect, vec2 texture_size) {
+ vec4 uvRect = vec4(task_rect.p0 + vec2(0.5),
+ task_rect.p1 - vec2(0.5));
+ uvRect /= texture_size.xyxy;
+ return uvRect;
+}
+
+vec2 compute_uv(RectWithEndpoint task_rect, vec2 texture_size) {
+ vec2 uv0 = task_rect.p0 / texture_size;
+ vec2 uv1 = floor(task_rect.p1) / texture_size;
+ return mix(uv0, uv1, aPosition.xy);
+}
+
+void main(void) {
+ FilterTask filter_task = fetch_filter_task(aFilterRenderTaskAddress);
+ RectWithEndpoint target_rect = filter_task.task_rect;
+
+ vec2 pos = mix(target_rect.p0, target_rect.p1, aPosition.xy);
+
+ RectWithEndpoint input_1_task;
+ if (aFilterInputCount > 0) {
+ vec2 texture_size = vec2(TEX_SIZE(sColor0).xy);
+ input_1_task = fetch_render_task_rect(aFilterInput1TaskAddress);
+ vInput1UvRect = compute_uv_rect(input_1_task, texture_size);
+ vInput1Uv = compute_uv(input_1_task, texture_size);
+ }
+
+ RectWithEndpoint input_2_task;
+ if (aFilterInputCount > 1) {
+ vec2 texture_size = vec2(TEX_SIZE(sColor1).xy);
+ input_2_task = fetch_render_task_rect(aFilterInput2TaskAddress);
+ vInput2UvRect = compute_uv_rect(input_2_task, texture_size);
+ vInput2Uv = compute_uv(input_2_task, texture_size);
+ }
+
+ vFilterInputCount = aFilterInputCount;
+ vFilterKind = aFilterKind;
+
+ // This assignment is only used for component transfer filters but this
+ // assignment has to be done here and not in the component transfer case
+ // below because it doesn't get executed on Windows because of a suspected
+ // miscompile of this shader on Windows. See
+ // https://github.com/servo/webrender/wiki/Driver-issues#bug-1505871---assignment-to-varying-flat-arrays-inside-switch-statement-of-vertex-shader-suspected-miscompile-on-windows
+ // default: just to satisfy angle_shader_validation.rs which needs one
+ // default: for every switch, even in comments.
+ vFuncs.r = (aFilterGenericInt >> 12) & 0xf; // R
+ vFuncs.g = (aFilterGenericInt >> 8) & 0xf; // G
+ vFuncs.b = (aFilterGenericInt >> 4) & 0xf; // B
+ vFuncs.a = (aFilterGenericInt) & 0xf; // A
+
+ switch (aFilterKind) {
+ case FILTER_BLEND:
+ vData = ivec4(aFilterGenericInt, 0, 0, 0);
+ break;
+ case FILTER_FLOOD:
+ vFilterData0 = fetch_from_gpu_cache_1_direct(aFilterExtraDataAddress);
+ break;
+ case FILTER_OPACITY:
+ vFloat0.x = filter_task.user_data.x;
+ break;
+ case FILTER_COLOR_MATRIX:
+ vec4 mat_data[4] = fetch_from_gpu_cache_4_direct(aFilterExtraDataAddress);
+ vColorMat = mat4(mat_data[0], mat_data[1], mat_data[2], mat_data[3]);
+ vFilterData0 = fetch_from_gpu_cache_1_direct(aFilterExtraDataAddress + ivec2(4, 0));
+ break;
+ case FILTER_DROP_SHADOW:
+ vFilterData0 = fetch_from_gpu_cache_1_direct(aFilterExtraDataAddress);
+ break;
+ case FILTER_OFFSET:
+ vec2 texture_size = vec2(TEX_SIZE(sColor0).xy);
+ vFilterData0 = vec4(-filter_task.user_data.xy / texture_size, vec2(0.0));
+
+ RectWithEndpoint task_rect = input_1_task;
+ vec4 clipRect = vec4(task_rect.p0, task_rect.p1);
+ clipRect /= texture_size.xyxy;
+ vFilterData1 = clipRect;
+ break;
+ case FILTER_COMPONENT_TRANSFER:
+ vData = ivec4(aFilterExtraDataAddress, 0, 0);
+ break;
+ case FILTER_COMPOSITE:
+ vData = ivec4(aFilterGenericInt, 0, 0, 0);
+ if (aFilterGenericInt == COMPOSITE_ARITHMETIC) {
+ vFilterData0 = fetch_from_gpu_cache_1_direct(aFilterExtraDataAddress);
+ }
+ break;
+ default:
+ break;
+ }
+
+ gl_Position = uTransform * vec4(pos, 0.0, 1.0);
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+
+#define COMPONENT_TRANSFER_IDENTITY 0
+#define COMPONENT_TRANSFER_TABLE 1
+#define COMPONENT_TRANSFER_DISCRETE 2
+#define COMPONENT_TRANSFER_LINEAR 3
+#define COMPONENT_TRANSFER_GAMMA 4
+
+vec3 Multiply(vec3 Cb, vec3 Cs) {
+ return Cb * Cs;
+}
+
+vec3 Screen(vec3 Cb, vec3 Cs) {
+ return Cb + Cs - (Cb * Cs);
+}
+
+vec3 HardLight(vec3 Cb, vec3 Cs) {
+ vec3 m = Multiply(Cb, 2.0 * Cs);
+ vec3 s = Screen(Cb, 2.0 * Cs - 1.0);
+ vec3 edge = vec3(0.5, 0.5, 0.5);
+ return mix(m, s, step(edge, Cs));
+}
+
+// TODO: Worth doing with mix/step? Check GLSL output.
+float ColorDodge(float Cb, float Cs) {
+ if (Cb == 0.0)
+ return 0.0;
+ else if (Cs == 1.0)
+ return 1.0;
+ else
+ return min(1.0, Cb / (1.0 - Cs));
+}
+
+// TODO: Worth doing with mix/step? Check GLSL output.
+float ColorBurn(float Cb, float Cs) {
+ if (Cb == 1.0)
+ return 1.0;
+ else if (Cs == 0.0)
+ return 0.0;
+ else
+ return 1.0 - min(1.0, (1.0 - Cb) / Cs);
+}
+
+float SoftLight(float Cb, float Cs) {
+ if (Cs <= 0.5) {
+ return Cb - (1.0 - 2.0 * Cs) * Cb * (1.0 - Cb);
+ } else {
+ float D;
+
+ if (Cb <= 0.25)
+ D = ((16.0 * Cb - 12.0) * Cb + 4.0) * Cb;
+ else
+ D = sqrt(Cb);
+
+ return Cb + (2.0 * Cs - 1.0) * (D - Cb);
+ }
+}
+
+vec3 Difference(vec3 Cb, vec3 Cs) {
+ return abs(Cb - Cs);
+}
+
+vec3 Exclusion(vec3 Cb, vec3 Cs) {
+ return Cb + Cs - 2.0 * Cb * Cs;
+}
+
+// These functions below are taken from the spec.
+// There's probably a much quicker way to implement
+// them in GLSL...
+float Sat(vec3 c) {
+ return max(c.r, max(c.g, c.b)) - min(c.r, min(c.g, c.b));
+}
+
+float Lum(vec3 c) {
+ vec3 f = vec3(0.3, 0.59, 0.11);
+ return dot(c, f);
+}
+
+vec3 ClipColor(vec3 C) {
+ float L = Lum(C);
+ float n = min(C.r, min(C.g, C.b));
+ float x = max(C.r, max(C.g, C.b));
+
+ if (n < 0.0)
+ C = L + (((C - L) * L) / (L - n));
+
+ if (x > 1.0)
+ C = L + (((C - L) * (1.0 - L)) / (x - L));
+
+ return C;
+}
+
+vec3 SetLum(vec3 C, float l) {
+ float d = l - Lum(C);
+ return ClipColor(C + d);
+}
+
+void SetSatInner(inout float Cmin, inout float Cmid, inout float Cmax, float s) {
+ if (Cmax > Cmin) {
+ Cmid = (((Cmid - Cmin) * s) / (Cmax - Cmin));
+ Cmax = s;
+ } else {
+ Cmid = 0.0;
+ Cmax = 0.0;
+ }
+ Cmin = 0.0;
+}
+
+vec3 SetSat(vec3 C, float s) {
+ if (C.r <= C.g) {
+ if (C.g <= C.b) {
+ SetSatInner(C.r, C.g, C.b, s);
+ } else {
+ if (C.r <= C.b) {
+ SetSatInner(C.r, C.b, C.g, s);
+ } else {
+ SetSatInner(C.b, C.r, C.g, s);
+ }
+ }
+ } else {
+ if (C.r <= C.b) {
+ SetSatInner(C.g, C.r, C.b, s);
+ } else {
+ if (C.g <= C.b) {
+ SetSatInner(C.g, C.b, C.r, s);
+ } else {
+ SetSatInner(C.b, C.g, C.r, s);
+ }
+ }
+ }
+ return C;
+}
+
+vec3 Hue(vec3 Cb, vec3 Cs) {
+ return SetLum(SetSat(Cs, Sat(Cb)), Lum(Cb));
+}
+
+vec3 Saturation(vec3 Cb, vec3 Cs) {
+ return SetLum(SetSat(Cb, Sat(Cs)), Lum(Cb));
+}
+
+vec3 Color(vec3 Cb, vec3 Cs) {
+ return SetLum(Cs, Lum(Cb));
+}
+
+vec3 Luminosity(vec3 Cb, vec3 Cs) {
+ return SetLum(Cb, Lum(Cs));
+}
+
+const int BlendMode_Normal = 0;
+const int BlendMode_Multiply = 1;
+const int BlendMode_Screen = 2;
+const int BlendMode_Overlay = 3;
+const int BlendMode_Darken = 4;
+const int BlendMode_Lighten = 5;
+const int BlendMode_ColorDodge = 6;
+const int BlendMode_ColorBurn = 7;
+const int BlendMode_HardLight = 8;
+const int BlendMode_SoftLight = 9;
+const int BlendMode_Difference = 10;
+const int BlendMode_Exclusion = 11;
+const int BlendMode_Hue = 12;
+const int BlendMode_Saturation = 13;
+const int BlendMode_Color = 14;
+const int BlendMode_Luminosity = 15;
+
+vec4 blend(vec4 Cs, vec4 Cb, int mode) {
+ vec4 result = vec4(1.0, 0.0, 0.0, 1.0);
+
+ switch (mode) {
+ case BlendMode_Normal:
+ result.rgb = Cs.rgb;
+ break;
+ case BlendMode_Multiply:
+ result.rgb = Multiply(Cb.rgb, Cs.rgb);
+ break;
+ case BlendMode_Screen:
+ result.rgb = Screen(Cb.rgb, Cs.rgb);
+ break;
+ case BlendMode_Overlay:
+ // Overlay is inverse of Hardlight
+ result.rgb = HardLight(Cs.rgb, Cb.rgb);
+ break;
+ case BlendMode_Darken:
+ result.rgb = min(Cs.rgb, Cb.rgb);
+ break;
+ case BlendMode_Lighten:
+ result.rgb = max(Cs.rgb, Cb.rgb);
+ break;
+ case BlendMode_ColorDodge:
+ result.r = ColorDodge(Cb.r, Cs.r);
+ result.g = ColorDodge(Cb.g, Cs.g);
+ result.b = ColorDodge(Cb.b, Cs.b);
+ break;
+ case BlendMode_ColorBurn:
+ result.r = ColorBurn(Cb.r, Cs.r);
+ result.g = ColorBurn(Cb.g, Cs.g);
+ result.b = ColorBurn(Cb.b, Cs.b);
+ break;
+ case BlendMode_HardLight:
+ result.rgb = HardLight(Cb.rgb, Cs.rgb);
+ break;
+ case BlendMode_SoftLight:
+ result.r = SoftLight(Cb.r, Cs.r);
+ result.g = SoftLight(Cb.g, Cs.g);
+ result.b = SoftLight(Cb.b, Cs.b);
+ break;
+ case BlendMode_Difference:
+ result.rgb = Difference(Cb.rgb, Cs.rgb);
+ break;
+ case BlendMode_Exclusion:
+ result.rgb = Exclusion(Cb.rgb, Cs.rgb);
+ break;
+ case BlendMode_Hue:
+ result.rgb = Hue(Cb.rgb, Cs.rgb);
+ break;
+ case BlendMode_Saturation:
+ result.rgb = Saturation(Cb.rgb, Cs.rgb);
+ break;
+ case BlendMode_Color:
+ result.rgb = Color(Cb.rgb, Cs.rgb);
+ break;
+ case BlendMode_Luminosity:
+ result.rgb = Luminosity(Cb.rgb, Cs.rgb);
+ break;
+ default: break;
+ }
+ vec3 rgb = (1.0 - Cb.a) * Cs.rgb + Cb.a * result.rgb;
+ result = mix(vec4(Cb.rgb * Cb.a, Cb.a), vec4(rgb, 1.0), Cs.a);
+ return result;
+}
+
+// Based on the Gecko's implementation in
+// https://hg.mozilla.org/mozilla-central/file/91b4c3687d75/gfx/src/FilterSupport.cpp#l24
+// These could be made faster by sampling a lookup table stored in a float texture
+// with linear interpolation.
+
+vec3 SrgbToLinear(vec3 color) {
+ vec3 c1 = color / 12.92;
+ vec3 c2 = pow(color / 1.055 + vec3(0.055 / 1.055), vec3(2.4));
+ return if_then_else(lessThanEqual(color, vec3(0.04045)), c1, c2);
+}
+
+vec3 LinearToSrgb(vec3 color) {
+ vec3 c1 = color * 12.92;
+ vec3 c2 = vec3(1.055) * pow(color, vec3(1.0 / 2.4)) - vec3(0.055);
+ return if_then_else(lessThanEqual(color, vec3(0.0031308)), c1, c2);
+}
+
+// This function has to be factored out due to the following issue:
+// https://github.com/servo/webrender/wiki/Driver-issues#bug-1532245---switch-statement-inside-control-flow-inside-switch-statement-fails-to-compile-on-some-android-phones
+// (and now the words "default: default:" so angle_shader_validation.rs passes)
+vec4 ComponentTransfer(vec4 colora) {
+ // We push a different amount of data to the gpu cache depending on the
+ // function type.
+ // Identity => 0 blocks
+ // Table/Discrete => 64 blocks (256 values)
+ // Linear => 1 block (2 values)
+ // Gamma => 1 block (3 values)
+ // We loop through the color components and increment the offset (for the
+ // next color component) into the gpu cache based on how many blocks that
+ // function type put into the gpu cache.
+ // Table/Discrete use a 256 entry look up table.
+ // Linear/Gamma are a simple calculation.
+ int offset = 0;
+ vec4 texel;
+ int k;
+
+ // Dynamically indexing a vector is buggy on some devices, so use a temporary array.
+ int[4] funcs = int[4](vFuncs.r, vFuncs.g, vFuncs.b, vFuncs.a);
+ for (int i = 0; i < 4; i++) {
+ switch (funcs[i]) {
+ case COMPONENT_TRANSFER_IDENTITY:
+ break;
+ case COMPONENT_TRANSFER_TABLE:
+ case COMPONENT_TRANSFER_DISCRETE:
+ // fetch value from lookup table
+ k = int(floor(colora[i]*255.0 + 0.5));
+ texel = fetch_from_gpu_cache_1_direct(vData.xy + ivec2(offset + k/4, 0));
+ colora[i] = clamp(texel[k % 4], 0.0, 1.0);
+ // offset plus 256/4 blocks
+ offset = offset + 64;
+ break;
+ case COMPONENT_TRANSFER_LINEAR:
+ // fetch the two values for use in the linear equation
+ texel = fetch_from_gpu_cache_1_direct(vData.xy + ivec2(offset, 0));
+ colora[i] = clamp(texel[0] * colora[i] + texel[1], 0.0, 1.0);
+ // offset plus 1 block
+ offset = offset + 1;
+ break;
+ case COMPONENT_TRANSFER_GAMMA:
+ // fetch the three values for use in the gamma equation
+ texel = fetch_from_gpu_cache_1_direct(vData.xy + ivec2(offset, 0));
+ colora[i] = clamp(texel[0] * pow(colora[i], texel[1]) + texel[2], 0.0, 1.0);
+ // offset plus 1 block
+ offset = offset + 1;
+ break;
+ default:
+ // shouldn't happen
+ break;
+ }
+ }
+ return colora;
+}
+
+// Composite Filter
+
+vec4 composite(vec4 Cs, vec4 Cb, int mode) {
+ vec4 Cr = vec4(0.0, 1.0, 0.0, 1.0);
+ switch (mode) {
+ case COMPOSITE_OVER:
+ Cr.rgb = Cs.a * Cs.rgb + Cb.a * Cb.rgb * (1.0 - Cs.a);
+ Cr.a = Cs.a + Cb.a * (1.0 - Cs.a);
+ break;
+ case COMPOSITE_IN:
+ Cr.rgb = Cs.a * Cs.rgb * Cb.a;
+ Cr.a = Cs.a * Cb.a;
+ break;
+ case COMPOSITE_OUT:
+ Cr.rgb = Cs.a * Cs.rgb * (1.0 - Cb.a);
+ Cr.a = Cs.a * (1.0 - Cb.a);
+ break;
+ case COMPOSITE_ATOP:
+ Cr.rgb = Cs.a * Cs.rgb * Cb.a + Cb.a * Cb.rgb * (1.0 - Cs.a);
+ Cr.a = Cs.a * Cb.a + Cb.a * (1.0 - Cs.a);
+ break;
+ case COMPOSITE_XOR:
+ Cr.rgb = Cs.a * Cs.rgb * (1.0 - Cb.a) + Cb.a * Cb.rgb * (1.0 - Cs.a);
+ Cr.a = Cs.a * (1.0 - Cb.a) + Cb.a * (1.0 - Cs.a);
+ break;
+ case COMPOSITE_LIGHTER:
+ Cr.rgb = Cs.a * Cs.rgb + Cb.a * Cb.rgb;
+ Cr.a = Cs.a + Cb.a;
+ Cr = clamp(Cr, vec4(0.0), vec4(1.0));
+ break;
+ case COMPOSITE_ARITHMETIC:
+ Cr = vec4(vFilterData0.x) * Cs * Cb + vec4(vFilterData0.y) * Cs + vec4(vFilterData0.z) * Cb + vec4(vFilterData0.w);
+ Cr = clamp(Cr, vec4(0.0), vec4(1.0));
+ break;
+ default:
+ break;
+ }
+ return Cr;
+}
+
+vec4 sampleInUvRect(sampler2D sampler, vec2 uv, vec4 uvRect) {
+ vec2 clamped = clamp(uv.xy, uvRect.xy, uvRect.zw);
+ return texture(sampler, clamped);
+}
+
+void main(void) {
+ vec4 Ca = vec4(0.0, 0.0, 0.0, 0.0);
+ vec4 Cb = vec4(0.0, 0.0, 0.0, 0.0);
+ if (vFilterInputCount > 0) {
+ Ca = sampleInUvRect(sColor0, vInput1Uv, vInput1UvRect);
+ if (Ca.a != 0.0) {
+ Ca.rgb /= Ca.a;
+ }
+ }
+ if (vFilterInputCount > 1) {
+ Cb = sampleInUvRect(sColor1, vInput2Uv, vInput2UvRect);
+ if (Cb.a != 0.0) {
+ Cb.rgb /= Cb.a;
+ }
+ }
+
+ vec4 result = vec4(1.0, 0.0, 0.0, 1.0);
+
+ bool needsPremul = true;
+
+ switch (vFilterKind) {
+ case FILTER_BLEND:
+ result = blend(Ca, Cb, vData.x);
+ needsPremul = false;
+ break;
+ case FILTER_FLOOD:
+ result = vFilterData0;
+ needsPremul = false;
+ break;
+ case FILTER_LINEAR_TO_SRGB:
+ result.rgb = LinearToSrgb(Ca.rgb);
+ result.a = Ca.a;
+ break;
+ case FILTER_SRGB_TO_LINEAR:
+ result.rgb = SrgbToLinear(Ca.rgb);
+ result.a = Ca.a;
+ break;
+ case FILTER_OPACITY:
+ result.rgb = Ca.rgb;
+ result.a = Ca.a * vFloat0.x;
+ break;
+ case FILTER_COLOR_MATRIX:
+ result = vColorMat * Ca + vFilterData0;
+ result = clamp(result, vec4(0.0), vec4(1.0));
+ break;
+ case FILTER_DROP_SHADOW:
+ vec4 shadow = vec4(vFilterData0.rgb, Cb.a * vFilterData0.a);
+ // Normal blend + source-over coposite
+ result = blend(Ca, shadow, BlendMode_Normal);
+ needsPremul = false;
+ break;
+ case FILTER_OFFSET:
+ vec2 offsetUv = vInput1Uv + vFilterData0.xy;
+ result = sampleInUvRect(sColor0, offsetUv, vInput1UvRect);
+ result *= point_inside_rect(offsetUv, vFilterData1.xy, vFilterData1.zw);
+ needsPremul = false;
+ break;
+ case FILTER_COMPONENT_TRANSFER:
+ result = ComponentTransfer(Ca);
+ break;
+ case FILTER_IDENTITY:
+ result = Ca;
+ break;
+ case FILTER_COMPOSITE:
+ result = composite(Ca, Cb, vData.x);
+ needsPremul = false;
+ default:
+ break;
+ }
+
+ if (needsPremul) {
+ result.rgb *= result.a;
+ }
+
+ oFragColor = result;
+}
+#endif
diff --git a/gfx/wr/webrender/res/debug_color.glsl b/gfx/wr/webrender/res/debug_color.glsl
new file mode 100644
index 0000000000..12b530cda0
--- /dev/null
+++ b/gfx/wr/webrender/res/debug_color.glsl
@@ -0,0 +1,24 @@
+/* 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/. */
+
+#include shared,shared_other
+
+varying mediump vec4 vColor;
+
+#ifdef WR_VERTEX_SHADER
+in vec4 aColor;
+
+void main(void) {
+ vColor = vec4(aColor.rgb * aColor.a, aColor.a);
+ vec4 pos = vec4(aPosition, 0.0, 1.0);
+ pos.xy = floor(pos.xy + 0.5);
+ gl_Position = uTransform * pos;
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+void main(void) {
+ oFragColor = vColor;
+}
+#endif
diff --git a/gfx/wr/webrender/res/debug_font.glsl b/gfx/wr/webrender/res/debug_font.glsl
new file mode 100644
index 0000000000..3b08f1b2fe
--- /dev/null
+++ b/gfx/wr/webrender/res/debug_font.glsl
@@ -0,0 +1,30 @@
+/* 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/. */
+
+#define WR_FEATURE_TEXTURE_2D
+
+#include shared,shared_other
+
+varying highp vec2 vColorTexCoord;
+varying mediump vec4 vColor;
+
+#ifdef WR_VERTEX_SHADER
+in vec4 aColor;
+in vec2 aColorTexCoord;
+
+void main(void) {
+ vColor = aColor;
+ vColorTexCoord = aColorTexCoord;
+ vec4 pos = vec4(aPosition, 0.0, 1.0);
+ pos.xy = floor(pos.xy + 0.5);
+ gl_Position = uTransform * pos;
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+void main(void) {
+ float alpha = texture(sColor0, vColorTexCoord).r;
+ oFragColor = vColor * alpha;
+}
+#endif
diff --git a/gfx/wr/webrender/res/ellipse.glsl b/gfx/wr/webrender/res/ellipse.glsl
new file mode 100644
index 0000000000..a378c199ef
--- /dev/null
+++ b/gfx/wr/webrender/res/ellipse.glsl
@@ -0,0 +1,93 @@
+/* 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/. */
+
+// Preprocess the radii for computing the distance approximation. This should
+// be used in the vertex shader if possible to avoid doing expensive division
+// in the fragment shader. When dealing with a point (zero radii), approximate
+// it as an ellipse with very small radii so that we don't need to branch.
+vec2 inverse_radii_squared(vec2 radii) {
+ return 1.0 / max(radii * radii, 1.0e-6);
+}
+
+#ifdef WR_FRAGMENT_SHADER
+
+// One iteration of Newton's method on the 2D equation of an ellipse:
+//
+// E(x, y) = x^2/a^2 + y^2/b^2 - 1
+//
+// The Jacobian of this equation is:
+//
+// J(E(x, y)) = [ 2*x/a^2 2*y/b^2 ]
+//
+// We approximate the distance with:
+//
+// E(x, y) / ||J(E(x, y))||
+//
+// See G. Taubin, "Distance Approximations for Rasterizing Implicit
+// Curves", section 3.
+//
+// A scale relative to the unit scale of the ellipse may be passed in to cause
+// the math to degenerate to length(p) when scale is 0, or otherwise give the
+// normal distance approximation if scale is 1.
+float distance_to_ellipse_approx(vec2 p, vec2 inv_radii_sq, float scale) {
+ vec2 p_r = p * inv_radii_sq;
+ float g = dot(p, p_r) - scale;
+ vec2 dG = (1.0 + scale) * p_r;
+ return g * inversesqrt(dot(dG, dG));
+}
+
+// Slower but more accurate version that uses the exact distance when dealing
+// with a 0-radius point distance and otherwise uses the faster approximation
+// when dealing with non-zero radii.
+float distance_to_ellipse(vec2 p, vec2 radii) {
+ return distance_to_ellipse_approx(p, inverse_radii_squared(radii),
+ float(all(greaterThan(radii, vec2(0.0)))));
+}
+
+float distance_to_rounded_rect(
+ vec2 pos,
+ vec3 plane_tl,
+ vec4 center_radius_tl,
+ vec3 plane_tr,
+ vec4 center_radius_tr,
+ vec3 plane_br,
+ vec4 center_radius_br,
+ vec3 plane_bl,
+ vec4 center_radius_bl,
+ vec4 rect_bounds
+) {
+ // Clip against each ellipse. If the fragment is in a corner, one of the
+ // branches below will select it as the corner to calculate the distance
+ // to. We use half-space planes to detect which corner's ellipse the
+ // fragment is inside, where the plane is defined by a normal and offset.
+ // If outside any ellipse, default to a small offset so a negative distance
+ // is returned for it.
+ vec4 corner = vec4(vec2(1.0e-6), vec2(1.0));
+
+ // Calculate the ellipse parameters for each corner.
+ center_radius_tl.xy = center_radius_tl.xy - pos;
+ center_radius_tr.xy = (center_radius_tr.xy - pos) * vec2(-1.0, 1.0);
+ center_radius_br.xy = pos - center_radius_br.xy;
+ center_radius_bl.xy = (center_radius_bl.xy - pos) * vec2(1.0, -1.0);
+
+ // Evaluate each half-space plane in turn to select a corner.
+ if (dot(pos, plane_tl.xy) > plane_tl.z) {
+ corner = center_radius_tl;
+ }
+ if (dot(pos, plane_tr.xy) > plane_tr.z) {
+ corner = center_radius_tr;
+ }
+ if (dot(pos, plane_br.xy) > plane_br.z) {
+ corner = center_radius_br;
+ }
+ if (dot(pos, plane_bl.xy) > plane_bl.z) {
+ corner = center_radius_bl;
+ }
+
+ // Calculate the distance of the selected corner and the rectangle bounds,
+ // whichever is greater.
+ return max(distance_to_ellipse_approx(corner.xy, corner.zw, 1.0),
+ signed_distance_rect(pos, rect_bounds.xy, rect_bounds.zw));
+}
+#endif
diff --git a/gfx/wr/webrender/res/gpu_buffer.glsl b/gfx/wr/webrender/res/gpu_buffer.glsl
new file mode 100644
index 0000000000..25f4622db2
--- /dev/null
+++ b/gfx/wr/webrender/res/gpu_buffer.glsl
@@ -0,0 +1,42 @@
+/* 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/. */
+
+uniform HIGHP_SAMPLER_FLOAT sampler2D sGpuBuffer;
+
+ivec2 get_gpu_buffer_uv(HIGHP_FS_ADDRESS int address) {
+ return ivec2(uint(address) % WR_MAX_VERTEX_TEXTURE_WIDTH,
+ uint(address) / WR_MAX_VERTEX_TEXTURE_WIDTH);
+}
+
+vec4 fetch_from_gpu_buffer_1(HIGHP_FS_ADDRESS int address) {
+ ivec2 uv = get_gpu_buffer_uv(address);
+ return texelFetch(sGpuBuffer, uv, 0);
+}
+
+vec4[2] fetch_from_gpu_buffer_2(HIGHP_FS_ADDRESS int address) {
+ ivec2 uv = get_gpu_buffer_uv(address);
+ return vec4[2](
+ TEXEL_FETCH(sGpuBuffer, uv, 0, ivec2(0, 0)),
+ TEXEL_FETCH(sGpuBuffer, uv, 0, ivec2(1, 0))
+ );
+}
+
+vec4[3] fetch_from_gpu_buffer_3(HIGHP_FS_ADDRESS int address) {
+ ivec2 uv = get_gpu_buffer_uv(address);
+ return vec4[3](
+ TEXEL_FETCH(sGpuBuffer, uv, 0, ivec2(0, 0)),
+ TEXEL_FETCH(sGpuBuffer, uv, 0, ivec2(1, 0)),
+ TEXEL_FETCH(sGpuBuffer, uv, 0, ivec2(2, 0))
+ );
+}
+
+vec4[4] fetch_from_gpu_buffer_4(HIGHP_FS_ADDRESS int address) {
+ ivec2 uv = get_gpu_buffer_uv(address);
+ return vec4[4](
+ TEXEL_FETCH(sGpuBuffer, uv, 0, ivec2(0, 0)),
+ TEXEL_FETCH(sGpuBuffer, uv, 0, ivec2(1, 0)),
+ TEXEL_FETCH(sGpuBuffer, uv, 0, ivec2(2, 0)),
+ TEXEL_FETCH(sGpuBuffer, uv, 0, ivec2(3, 0))
+ );
+}
diff --git a/gfx/wr/webrender/res/gpu_cache.glsl b/gfx/wr/webrender/res/gpu_cache.glsl
new file mode 100644
index 0000000000..cd5e41fec4
--- /dev/null
+++ b/gfx/wr/webrender/res/gpu_cache.glsl
@@ -0,0 +1,137 @@
+/* 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/. */
+
+uniform HIGHP_SAMPLER_FLOAT sampler2D sGpuCache;
+
+#define VECS_PER_IMAGE_RESOURCE 2
+
+// TODO(gw): This is here temporarily while we have
+// both GPU store and cache. When the GPU
+// store code is removed, we can change the
+// PrimitiveInstance instance structure to
+// use 2x unsigned shorts as vertex attributes
+// instead of an int, and encode the UV directly
+// in the vertices.
+ivec2 get_gpu_cache_uv(HIGHP_FS_ADDRESS int address) {
+ return ivec2(uint(address) % WR_MAX_VERTEX_TEXTURE_WIDTH,
+ uint(address) / WR_MAX_VERTEX_TEXTURE_WIDTH);
+}
+
+vec4[2] fetch_from_gpu_cache_2_direct(ivec2 address) {
+ return vec4[2](
+ TEXEL_FETCH(sGpuCache, address, 0, ivec2(0, 0)),
+ TEXEL_FETCH(sGpuCache, address, 0, ivec2(1, 0))
+ );
+}
+
+vec4[2] fetch_from_gpu_cache_2(HIGHP_FS_ADDRESS int address) {
+ ivec2 uv = get_gpu_cache_uv(address);
+ return vec4[2](
+ TEXEL_FETCH(sGpuCache, uv, 0, ivec2(0, 0)),
+ TEXEL_FETCH(sGpuCache, uv, 0, ivec2(1, 0))
+ );
+}
+
+vec4 fetch_from_gpu_cache_1_direct(ivec2 address) {
+ return texelFetch(sGpuCache, address, 0);
+}
+
+vec4 fetch_from_gpu_cache_1(HIGHP_FS_ADDRESS int address) {
+ ivec2 uv = get_gpu_cache_uv(address);
+ return texelFetch(sGpuCache, uv, 0);
+}
+
+#ifdef WR_VERTEX_SHADER
+
+vec4[8] fetch_from_gpu_cache_8(int address) {
+ ivec2 uv = get_gpu_cache_uv(address);
+ return vec4[8](
+ TEXEL_FETCH(sGpuCache, uv, 0, ivec2(0, 0)),
+ TEXEL_FETCH(sGpuCache, uv, 0, ivec2(1, 0)),
+ TEXEL_FETCH(sGpuCache, uv, 0, ivec2(2, 0)),
+ TEXEL_FETCH(sGpuCache, uv, 0, ivec2(3, 0)),
+ TEXEL_FETCH(sGpuCache, uv, 0, ivec2(4, 0)),
+ TEXEL_FETCH(sGpuCache, uv, 0, ivec2(5, 0)),
+ TEXEL_FETCH(sGpuCache, uv, 0, ivec2(6, 0)),
+ TEXEL_FETCH(sGpuCache, uv, 0, ivec2(7, 0))
+ );
+}
+
+vec4[3] fetch_from_gpu_cache_3(int address) {
+ ivec2 uv = get_gpu_cache_uv(address);
+ return vec4[3](
+ TEXEL_FETCH(sGpuCache, uv, 0, ivec2(0, 0)),
+ TEXEL_FETCH(sGpuCache, uv, 0, ivec2(1, 0)),
+ TEXEL_FETCH(sGpuCache, uv, 0, ivec2(2, 0))
+ );
+}
+
+vec4[3] fetch_from_gpu_cache_3_direct(ivec2 address) {
+ return vec4[3](
+ TEXEL_FETCH(sGpuCache, address, 0, ivec2(0, 0)),
+ TEXEL_FETCH(sGpuCache, address, 0, ivec2(1, 0)),
+ TEXEL_FETCH(sGpuCache, address, 0, ivec2(2, 0))
+ );
+}
+
+vec4[4] fetch_from_gpu_cache_4_direct(ivec2 address) {
+ return vec4[4](
+ TEXEL_FETCH(sGpuCache, address, 0, ivec2(0, 0)),
+ TEXEL_FETCH(sGpuCache, address, 0, ivec2(1, 0)),
+ TEXEL_FETCH(sGpuCache, address, 0, ivec2(2, 0)),
+ TEXEL_FETCH(sGpuCache, address, 0, ivec2(3, 0))
+ );
+}
+
+vec4[4] fetch_from_gpu_cache_4(int address) {
+ ivec2 uv = get_gpu_cache_uv(address);
+ return vec4[4](
+ TEXEL_FETCH(sGpuCache, uv, 0, ivec2(0, 0)),
+ TEXEL_FETCH(sGpuCache, uv, 0, ivec2(1, 0)),
+ TEXEL_FETCH(sGpuCache, uv, 0, ivec2(2, 0)),
+ TEXEL_FETCH(sGpuCache, uv, 0, ivec2(3, 0))
+ );
+}
+
+//TODO: image resource is too specific for this module
+
+struct ImageSource {
+ RectWithEndpoint uv_rect;
+ vec4 user_data;
+};
+
+ImageSource fetch_image_source(int address) {
+ //Note: number of blocks has to match `renderer::BLOCKS_PER_UV_RECT`
+ vec4 data[2] = fetch_from_gpu_cache_2(address);
+ RectWithEndpoint uv_rect = RectWithEndpoint(data[0].xy, data[0].zw);
+ return ImageSource(uv_rect, data[1]);
+}
+
+ImageSource fetch_image_source_direct(ivec2 address) {
+ vec4 data[2] = fetch_from_gpu_cache_2_direct(address);
+ RectWithEndpoint uv_rect = RectWithEndpoint(data[0].xy, data[0].zw);
+ return ImageSource(uv_rect, data[1]);
+}
+
+// Fetch optional extra data for a texture cache resource. This can contain
+// a polygon defining a UV rect within the texture cache resource.
+// Note: the polygon coordinates are in homogeneous space.
+struct ImageSourceExtra {
+ vec4 st_tl;
+ vec4 st_tr;
+ vec4 st_bl;
+ vec4 st_br;
+};
+
+ImageSourceExtra fetch_image_source_extra(int address) {
+ vec4 data[4] = fetch_from_gpu_cache_4(address + VECS_PER_IMAGE_RESOURCE);
+ return ImageSourceExtra(
+ data[0],
+ data[1],
+ data[2],
+ data[3]
+ );
+}
+
+#endif //WR_VERTEX_SHADER
diff --git a/gfx/wr/webrender/res/gpu_cache_update.glsl b/gfx/wr/webrender/res/gpu_cache_update.glsl
new file mode 100644
index 0000000000..fcabfacb4f
--- /dev/null
+++ b/gfx/wr/webrender/res/gpu_cache_update.glsl
@@ -0,0 +1,27 @@
+/* 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/. */
+
+#include base
+
+varying highp vec4 vData;
+
+#ifdef WR_VERTEX_SHADER
+in vec4 aValue;
+in vec2 aPosition;
+
+void main() {
+ vData = aValue;
+ gl_Position = vec4(aPosition * 2.0 - 1.0, 0.0, 1.0);
+ gl_PointSize = 1.0;
+}
+
+#endif //WR_VERTEX_SHADER
+
+#ifdef WR_FRAGMENT_SHADER
+out vec4 oValue;
+
+void main() {
+ oValue = vData;
+}
+#endif //WR_FRAGMENT_SHADER
diff --git a/gfx/wr/webrender/res/gradient.glsl b/gfx/wr/webrender/res/gradient.glsl
new file mode 100644
index 0000000000..87c011fefc
--- /dev/null
+++ b/gfx/wr/webrender/res/gradient.glsl
@@ -0,0 +1,63 @@
+/* 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/. */
+
+// Gradient GPU cache address.
+// Packed in to a vector to work around bug 1630356.
+flat varying highp ivec2 v_gradient_address;
+// Repetition along the gradient stops.
+// Packed in to a vector to work around bug 1630356.
+flat varying mediump vec2 v_gradient_repeat;
+
+#ifdef WR_FRAGMENT_SHADER
+
+#ifdef WR_FEATURE_DITHERING
+vec4 dither(vec4 color) {
+ const int matrix_mask = 7;
+
+ ivec2 pos = ivec2(gl_FragCoord.xy) & ivec2(matrix_mask);
+ float noise_normalized = (texelFetch(sDither, pos, 0).r * 255.0 + 0.5) / 64.0;
+ float noise = (noise_normalized - 0.5) / 256.0; // scale down to the unit length
+
+ return color + vec4(noise, noise, noise, 0);
+}
+#else
+vec4 dither(vec4 color) {
+ return color;
+}
+#endif //WR_FEATURE_DITHERING
+
+#define GRADIENT_ENTRIES 128.0
+
+float clamp_gradient_entry(float offset) {
+ // Calculate the color entry index to use for this offset:
+ // offsets < 0 use the first color entry, 0
+ // offsets from [0, 1) use the color entries in the range of [1, N-1)
+ // offsets >= 1 use the last color entry, N-1
+ // so transform the range [0, 1) -> [1, N-1)
+
+ // TODO(gw): In the future we might consider making the size of the
+ // LUT vary based on number / distribution of stops in the gradient.
+ // Ensure we don't fetch outside the valid range of the LUT.
+ return clamp(1.0 + offset * GRADIENT_ENTRIES, 0.0, 1.0 + GRADIENT_ENTRIES);
+}
+
+vec4 sample_gradient(float offset) {
+ // Modulo the offset if the gradient repeats.
+ offset -= floor(offset) * v_gradient_repeat.x;
+
+ // Calculate the texel to index into the gradient color entries:
+ // floor(x) is the gradient color entry index
+ // fract(x) is the linear filtering factor between start and end
+ float x = clamp_gradient_entry(offset);
+ float entry_index = floor(x);
+ float entry_fract = x - entry_index;
+
+ // Fetch the start and end color. There is a [start, end] color per entry.
+ vec4 texels[2] = fetch_from_gpu_buffer_2(v_gradient_address.x + 2 * int(entry_index));
+
+ // Finally interpolate and apply dithering
+ return dither(texels[0] + texels[1] * entry_fract);
+}
+
+#endif //WR_FRAGMENT_SHADER
diff --git a/gfx/wr/webrender/res/gradient_shared.glsl b/gfx/wr/webrender/res/gradient_shared.glsl
new file mode 100644
index 0000000000..a3cc042ca6
--- /dev/null
+++ b/gfx/wr/webrender/res/gradient_shared.glsl
@@ -0,0 +1,78 @@
+/* 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/. */
+
+#include gradient
+
+// Size of the gradient pattern's rectangle, used to compute horizontal and vertical
+// repetitions. Not to be confused with another kind of repetition of the pattern
+// which happens along the gradient stops.
+flat varying highp vec2 v_repeated_size;
+
+varying highp vec2 v_pos;
+
+#ifdef WR_FEATURE_ALPHA_PASS
+flat varying highp vec2 v_tile_repeat;
+#endif
+
+#ifdef WR_VERTEX_SHADER
+void write_gradient_vertex(
+ VertexInfo vi,
+ RectWithEndpoint local_rect,
+ RectWithEndpoint segment_rect,
+ ivec4 prim_user_data,
+ int brush_flags,
+ vec4 texel_rect,
+ int extend_mode,
+ vec2 stretch_size
+) {
+ if ((brush_flags & BRUSH_FLAG_SEGMENT_RELATIVE) != 0) {
+ v_pos = (vi.local_pos - segment_rect.p0) / rect_size(segment_rect);
+ v_pos = v_pos * (texel_rect.zw - texel_rect.xy) + texel_rect.xy;
+ v_pos = v_pos * rect_size(local_rect);
+ } else {
+ v_pos = vi.local_pos - local_rect.p0;
+ }
+
+ vec2 tile_repeat = rect_size(local_rect) / stretch_size;
+ v_repeated_size = stretch_size;
+
+ // Normalize UV to 0..1 scale.
+ v_pos /= v_repeated_size;
+
+ v_gradient_address.x = prim_user_data.x;
+
+ // Whether to repeat the gradient along the line instead of clamping.
+ v_gradient_repeat.x = float(extend_mode == EXTEND_MODE_REPEAT);
+
+#ifdef WR_FEATURE_ALPHA_PASS
+ v_tile_repeat = tile_repeat;
+#endif
+}
+#endif //WR_VERTEX_SHADER
+
+#ifdef WR_FRAGMENT_SHADER
+vec2 compute_repeated_pos() {
+#if defined(WR_FEATURE_ALPHA_PASS) && !defined(SWGL_ANTIALIAS)
+ // Handle top and left inflated edges (see brush_image).
+ vec2 local_pos = max(v_pos, vec2(0.0));
+
+ // Apply potential horizontal and vertical repetitions.
+ vec2 pos = fract(local_pos);
+
+ // Handle bottom and right inflated edges (see brush_image).
+ if (local_pos.x >= v_tile_repeat.x) {
+ pos.x = 1.0;
+ }
+ if (local_pos.y >= v_tile_repeat.y) {
+ pos.y = 1.0;
+ }
+ return pos;
+#else
+ // Apply potential horizontal and vertical repetitions.
+ return fract(v_pos);
+#endif
+}
+
+#endif //WR_FRAGMENT_SHADER
+
diff --git a/gfx/wr/webrender/res/prim_shared.glsl b/gfx/wr/webrender/res/prim_shared.glsl
new file mode 100644
index 0000000000..1b2267da4f
--- /dev/null
+++ b/gfx/wr/webrender/res/prim_shared.glsl
@@ -0,0 +1,250 @@
+/* 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/. */
+
+#include rect,render_task,gpu_cache,transform
+
+#define EXTEND_MODE_CLAMP 0
+#define EXTEND_MODE_REPEAT 1
+
+#define SUBPX_DIR_NONE 0
+#define SUBPX_DIR_HORIZONTAL 1
+#define SUBPX_DIR_VERTICAL 2
+#define SUBPX_DIR_MIXED 3
+
+#define RASTER_LOCAL 0
+#define RASTER_SCREEN 1
+
+uniform sampler2D sClipMask;
+
+#ifndef SWGL_CLIP_MASK
+// TODO: convert back to RectWithEndpoint if driver issues are resolved, if ever.
+flat varying mediump vec4 vClipMaskUvBounds;
+varying highp vec2 vClipMaskUv;
+#endif
+
+#ifdef WR_VERTEX_SHADER
+
+#define COLOR_MODE_FROM_PASS 0
+#define COLOR_MODE_ALPHA 1
+#define COLOR_MODE_SUBPX_BG_PASS0 2
+#define COLOR_MODE_SUBPX_BG_PASS1 3
+#define COLOR_MODE_SUBPX_BG_PASS2 4
+#define COLOR_MODE_SUBPX_DUAL_SOURCE 5
+#define COLOR_MODE_BITMAP_SHADOW 6
+#define COLOR_MODE_COLOR_BITMAP 7
+#define COLOR_MODE_IMAGE 8
+#define COLOR_MODE_MULTIPLY_DUAL_SOURCE 9
+
+uniform HIGHP_SAMPLER_FLOAT sampler2D sPrimitiveHeadersF;
+uniform HIGHP_SAMPLER_FLOAT isampler2D sPrimitiveHeadersI;
+
+// Instanced attributes
+PER_INSTANCE in ivec4 aData;
+
+#define VECS_PER_PRIM_HEADER_F 2U
+#define VECS_PER_PRIM_HEADER_I 2U
+
+struct Instance
+{
+ int prim_header_address;
+ int picture_task_address;
+ int clip_address;
+ int segment_index;
+ int flags;
+ int resource_address;
+ int brush_kind;
+};
+
+Instance decode_instance_attributes() {
+ Instance instance;
+
+ instance.prim_header_address = aData.x;
+ instance.picture_task_address = aData.y >> 16;
+ instance.clip_address = aData.y & 0xffff;
+ instance.segment_index = aData.z & 0xffff;
+ instance.flags = aData.z >> 16;
+ instance.resource_address = aData.w & 0xffffff;
+ instance.brush_kind = aData.w >> 24;
+
+ return instance;
+}
+
+struct PrimitiveHeader {
+ RectWithEndpoint local_rect;
+ RectWithEndpoint local_clip_rect;
+ float z;
+ int specific_prim_address;
+ int transform_id;
+ ivec4 user_data;
+};
+
+PrimitiveHeader fetch_prim_header(int index) {
+ PrimitiveHeader ph;
+
+ ivec2 uv_f = get_fetch_uv(index, VECS_PER_PRIM_HEADER_F);
+ vec4 local_rect = TEXEL_FETCH(sPrimitiveHeadersF, uv_f, 0, ivec2(0, 0));
+ vec4 local_clip_rect = TEXEL_FETCH(sPrimitiveHeadersF, uv_f, 0, ivec2(1, 0));
+ ph.local_rect = RectWithEndpoint(local_rect.xy, local_rect.zw);
+ ph.local_clip_rect = RectWithEndpoint(local_clip_rect.xy, local_clip_rect.zw);
+
+ ivec2 uv_i = get_fetch_uv(index, VECS_PER_PRIM_HEADER_I);
+ ivec4 data0 = TEXEL_FETCH(sPrimitiveHeadersI, uv_i, 0, ivec2(0, 0));
+ ivec4 data1 = TEXEL_FETCH(sPrimitiveHeadersI, uv_i, 0, ivec2(1, 0));
+ ph.z = float(data0.x);
+ ph.specific_prim_address = data0.y;
+ ph.transform_id = data0.z;
+ ph.user_data = data1;
+
+ return ph;
+}
+
+struct VertexInfo {
+ vec2 local_pos;
+ vec4 world_pos;
+};
+
+VertexInfo write_vertex(vec2 local_pos,
+ RectWithEndpoint local_clip_rect,
+ float z,
+ Transform transform,
+ PictureTask task) {
+ // Clamp to the two local clip rects.
+ vec2 clamped_local_pos = rect_clamp(local_clip_rect, local_pos);
+
+ // Transform the current vertex to world space.
+ vec4 world_pos = transform.m * vec4(clamped_local_pos, 0.0, 1.0);
+
+ // Convert the world positions to device pixel space.
+ vec2 device_pos = world_pos.xy * task.device_pixel_scale;
+
+ // Apply offsets for the render task to get correct screen location.
+ vec2 final_offset = -task.content_origin + task.task_rect.p0;
+
+ gl_Position = uTransform * vec4(device_pos + final_offset * world_pos.w, z * world_pos.w, world_pos.w);
+
+ VertexInfo vi = VertexInfo(
+ clamped_local_pos,
+ world_pos
+ );
+
+ return vi;
+}
+
+RectWithEndpoint clip_and_init_antialiasing(RectWithEndpoint segment_rect,
+ RectWithEndpoint prim_rect,
+ RectWithEndpoint clip_rect,
+ int edge_flags,
+ float z,
+ Transform transform,
+ PictureTask task) {
+#ifdef SWGL_ANTIALIAS
+ // Check if the bounds are smaller than the unmodified segment rect. If so,
+ // it is safe to enable AA on those edges.
+ bvec4 clipped = bvec4(greaterThan(clip_rect.p0, segment_rect.p0),
+ lessThan(clip_rect.p1, segment_rect.p1));
+ swgl_antiAlias(edge_flags | (clipped.x ? 1 : 0) | (clipped.y ? 2 : 0) |
+ (clipped.z ? 4 : 0) | (clipped.w ? 8 : 0));
+#endif
+
+ segment_rect.p0 = clamp(segment_rect.p0, clip_rect.p0, clip_rect.p1);
+ segment_rect.p1 = clamp(segment_rect.p1, clip_rect.p0, clip_rect.p1);
+
+#ifndef SWGL_ANTIALIAS
+ prim_rect.p0 = clamp(prim_rect.p0, clip_rect.p0, clip_rect.p1);
+ prim_rect.p1 = clamp(prim_rect.p1, clip_rect.p0, clip_rect.p1);
+
+ // Select between the segment and prim edges based on edge mask.
+ // We must perform the bitwise-and for each component individually, as a
+ // vector bitwise-and followed by conversion to bvec4 causes shader
+ // compilation crashes on some Adreno devices. See bug 1715746.
+ bvec4 clip_edge_mask = bvec4(bool(edge_flags & 1), bool(edge_flags & 2), bool(edge_flags & 4), bool(edge_flags & 8));
+ init_transform_vs(mix(
+ vec4(vec2(-1e16), vec2(1e16)),
+ vec4(segment_rect.p0, segment_rect.p1),
+ clip_edge_mask
+ ));
+
+ // As this is a transform shader, extrude by 2 (local space) pixels
+ // in each direction. This gives enough space around the edge to
+ // apply distance anti-aliasing. Technically, it:
+ // (a) slightly over-estimates the number of required pixels in the simple case.
+ // (b) might not provide enough edge in edge case perspective projections.
+ // However, it's fast and simple. If / when we ever run into issues, we
+ // can do some math on the projection matrix to work out a variable
+ // amount to extrude.
+
+ // Only extrude along edges where we are going to apply AA.
+ float extrude_amount = 2.0;
+ vec4 extrude_distance = mix(vec4(0.0), vec4(extrude_amount), clip_edge_mask);
+ segment_rect.p0 -= extrude_distance.xy;
+ segment_rect.p1 += extrude_distance.zw;
+#endif
+
+ return segment_rect;
+}
+
+void write_clip(vec4 world_pos, ClipArea area, PictureTask task) {
+#ifdef SWGL_CLIP_MASK
+ swgl_clipMask(
+ sClipMask,
+ (task.task_rect.p0 - task.content_origin) - (area.task_rect.p0 - area.screen_origin),
+ area.task_rect.p0,
+ rect_size(area.task_rect)
+ );
+#else
+ vec2 uv = world_pos.xy * area.device_pixel_scale +
+ world_pos.w * (area.task_rect.p0 - area.screen_origin);
+ vClipMaskUvBounds = vec4(
+ area.task_rect.p0,
+ area.task_rect.p1
+ );
+ vClipMaskUv = uv;
+#endif
+}
+
+// Read the exta image data containing the homogeneous screen space coordinates
+// of the corners, interpolate between them, and return real screen space UV.
+vec2 get_image_quad_uv(int address, vec2 f) {
+ ImageSourceExtra extra_data = fetch_image_source_extra(address);
+ vec4 x = mix(extra_data.st_tl, extra_data.st_tr, f.x);
+ vec4 y = mix(extra_data.st_bl, extra_data.st_br, f.x);
+ vec4 z = mix(x, y, f.y);
+ return z.xy / z.w;
+}
+#endif //WR_VERTEX_SHADER
+
+#ifdef WR_FRAGMENT_SHADER
+
+struct Fragment {
+ vec4 color;
+#ifdef WR_FEATURE_DUAL_SOURCE_BLENDING
+ vec4 blend;
+#endif
+};
+
+float do_clip() {
+#ifdef SWGL_CLIP_MASK
+ // SWGL relies on builtin clip-mask support to do this more efficiently,
+ // so no clipping is required here.
+ return 1.0;
+#else
+ // check for the dummy bounds, which are given to the opaque objects
+ if (vClipMaskUvBounds.xy == vClipMaskUvBounds.zw) {
+ return 1.0;
+ }
+ // anything outside of the mask is considered transparent
+ //Note: we assume gl_FragCoord.w == interpolated(1 / vClipMaskUv.w)
+ vec2 mask_uv = vClipMaskUv * gl_FragCoord.w;
+ bvec2 left = lessThanEqual(vClipMaskUvBounds.xy, mask_uv); // inclusive
+ bvec2 right = greaterThan(vClipMaskUvBounds.zw, mask_uv); // non-inclusive
+ // bail out if the pixel is outside the valid bounds
+ if (!all(bvec4(left, right))) {
+ return 0.0;
+ }
+ // finally, the slow path - fetch the mask value from an image
+ return texelFetch(sClipMask, ivec2(mask_uv), 0).r;
+#endif
+}
+
+#endif //WR_FRAGMENT_SHADER
diff --git a/gfx/wr/webrender/res/ps_clear.glsl b/gfx/wr/webrender/res/ps_clear.glsl
new file mode 100644
index 0000000000..567dea978d
--- /dev/null
+++ b/gfx/wr/webrender/res/ps_clear.glsl
@@ -0,0 +1,25 @@
+/* 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/. */
+
+#include shared
+
+varying mediump vec4 vColor;
+
+#ifdef WR_VERTEX_SHADER
+PER_INSTANCE in vec4 aRect;
+PER_INSTANCE in vec4 aColor;
+
+void main(void) {
+ vec2 pos = mix(aRect.xy, aRect.zw, aPosition.xy);
+ gl_Position = uTransform * vec4(pos, 0.0, 1.0);
+ gl_Position.z = gl_Position.w; // force depth clear to 1.0
+ vColor = aColor;
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+void main(void) {
+ oFragColor = vColor;
+}
+#endif
diff --git a/gfx/wr/webrender/res/ps_copy.glsl b/gfx/wr/webrender/res/ps_copy.glsl
new file mode 100644
index 0000000000..b4e43f1556
--- /dev/null
+++ b/gfx/wr/webrender/res/ps_copy.glsl
@@ -0,0 +1,41 @@
+/* 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/. */
+
+#include base
+
+#ifdef WR_VERTEX_SHADER
+
+attribute vec2 aPosition;
+
+// See CopyInstance struct.
+attribute vec4 a_src_rect;
+attribute vec4 a_dst_rect;
+attribute vec2 a_dst_texture_size;
+
+varying highp vec2 v_uv;
+
+void main(void) {
+ // We use texel fetch so v_uv is in unnormalized device space.
+ v_uv = mix(a_src_rect.xy, a_src_rect.zw, aPosition.xy);
+
+ // Transform into framebuffer [-1, 1] space.
+ vec2 pos = mix(a_dst_rect.xy, a_dst_rect.zw, aPosition.xy);
+ gl_Position = vec4(pos / (a_dst_texture_size * 0.5) - vec2(1.0, 1.0), 0.0, 1.0);
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+
+
+out vec4 oFragColor;
+
+varying highp vec2 v_uv;
+
+uniform sampler2D sColor0;
+
+void main(void) {
+ oFragColor = texelFetch(sColor0, ivec2(v_uv), 0);
+}
+
+#endif
diff --git a/gfx/wr/webrender/res/ps_quad.glsl b/gfx/wr/webrender/res/ps_quad.glsl
new file mode 100644
index 0000000000..e419e9cc49
--- /dev/null
+++ b/gfx/wr/webrender/res/ps_quad.glsl
@@ -0,0 +1,287 @@
+/* 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/. */
+
+#define WR_FEATURE_TEXTURE_2D
+
+#include shared,rect,transform,render_task,gpu_buffer
+
+flat varying mediump vec4 v_color;
+flat varying mediump vec4 v_uv_sample_bounds;
+flat varying lowp ivec4 v_flags;
+varying highp vec2 v_uv;
+
+#ifdef WR_VERTEX_SHADER
+
+#define EDGE_AA_LEFT 1
+#define EDGE_AA_TOP 2
+#define EDGE_AA_RIGHT 4
+#define EDGE_AA_BOTTOM 8
+
+#define PART_CENTER 0
+#define PART_LEFT 1
+#define PART_TOP 2
+#define PART_RIGHT 3
+#define PART_BOTTOM 4
+
+#define QF_IS_OPAQUE 1
+#define QF_APPLY_DEVICE_CLIP 2
+#define QF_IGNORE_DEVICE_SCALE 4
+
+#define INVALID_SEGMENT_INDEX 0xff
+
+#define AA_PIXEL_RADIUS 2.0
+
+PER_INSTANCE in ivec4 aData;
+
+struct PrimitiveInfo {
+ vec2 local_pos;
+
+ RectWithEndpoint local_prim_rect;
+ RectWithEndpoint local_clip_rect;
+
+ int edge_flags;
+};
+
+struct QuadSegment {
+ RectWithEndpoint rect;
+ vec4 uv_rect;
+};
+
+struct QuadPrimitive {
+ RectWithEndpoint bounds;
+ RectWithEndpoint clip;
+ vec4 color;
+};
+
+QuadSegment fetch_segment(int base, int index) {
+ QuadSegment seg;
+
+ vec4 texels[2] = fetch_from_gpu_buffer_2(base + 3 + index * 2);
+
+ seg.rect = RectWithEndpoint(texels[0].xy, texels[0].zw);
+ seg.uv_rect = texels[1];
+
+ return seg;
+}
+
+QuadPrimitive fetch_primitive(int index) {
+ QuadPrimitive prim;
+
+ vec4 texels[3] = fetch_from_gpu_buffer_3(index);
+
+ prim.bounds = RectWithEndpoint(texels[0].xy, texels[0].zw);
+ prim.clip = RectWithEndpoint(texels[1].xy, texels[1].zw);
+ prim.color = texels[2];
+
+ return prim;
+}
+
+struct QuadInstance {
+ // x
+ int prim_address;
+
+ // y
+ int quad_flags;
+ int edge_flags;
+ int picture_task_address;
+
+ // z
+ int part_index;
+ int z_id;
+
+ // w
+ int segment_index;
+ int transform_id;
+};
+
+QuadInstance decode_instance() {
+ QuadInstance qi = QuadInstance(
+ aData.x,
+
+ (aData.y >> 24) & 0xff,
+ (aData.y >> 16) & 0xff,
+ aData.y & 0xffff,
+
+ (aData.z >> 24) & 0xff,
+ aData.z & 0xffffff,
+
+ (aData.w >> 24) & 0xff,
+ aData.w & 0xffffff
+ );
+
+ return qi;
+}
+
+struct VertexInfo {
+ vec2 local_pos;
+};
+
+VertexInfo write_vertex(vec2 local_pos,
+ float z,
+ Transform transform,
+ vec2 content_origin,
+ RectWithEndpoint task_rect,
+ float device_pixel_scale,
+ int quad_flags) {
+ VertexInfo vi;
+
+ // Transform the current vertex to world space.
+ vec4 world_pos = transform.m * vec4(local_pos, 0.0, 1.0);
+
+ // Convert the world positions to device pixel space.
+ vec2 device_pos = world_pos.xy * device_pixel_scale;
+
+ if ((quad_flags & QF_APPLY_DEVICE_CLIP) != 0) {
+ RectWithEndpoint device_clip_rect = RectWithEndpoint(
+ content_origin,
+ content_origin + task_rect.p1 - task_rect.p0
+ );
+
+ // Clip to task rect
+ device_pos = rect_clamp(device_clip_rect, device_pos);
+
+ vi.local_pos = (transform.inv_m * vec4(device_pos / device_pixel_scale, 0.0, 1.0)).xy;
+ } else {
+ vi.local_pos = local_pos;
+ }
+
+ // Apply offsets for the render task to get correct screen location.
+ vec2 final_offset = -content_origin + task_rect.p0;
+
+ gl_Position = uTransform * vec4(device_pos + final_offset * world_pos.w, z * world_pos.w, world_pos.w);
+
+ return vi;
+}
+
+float edge_aa_offset(int edge, int flags) {
+ return ((flags & edge) != 0) ? AA_PIXEL_RADIUS : 0.0;
+}
+
+PrimitiveInfo ps_quad_main(void) {
+ QuadInstance qi = decode_instance();
+
+ Transform transform = fetch_transform(qi.transform_id);
+ PictureTask task = fetch_picture_task(qi.picture_task_address);
+ QuadPrimitive prim = fetch_primitive(qi.prim_address);
+ float z = float(qi.z_id);
+
+ QuadSegment seg;
+ if (qi.segment_index == INVALID_SEGMENT_INDEX) {
+ seg.rect = prim.bounds;
+ seg.uv_rect = vec4(0.0);
+ } else {
+ seg = fetch_segment(qi.prim_address, qi.segment_index);
+ }
+
+ // The local space rect that we will draw, which is effectively:
+ // - The tile within the primitive we will draw
+ // - Intersected with any local-space clip rect(s)
+ // - Expanded for AA edges where appropriate
+ RectWithEndpoint local_coverage_rect = seg.rect;
+
+ // Apply local clip rect
+ local_coverage_rect.p0 = max(local_coverage_rect.p0, prim.clip.p0);
+ local_coverage_rect.p1 = min(local_coverage_rect.p1, prim.clip.p1);
+ local_coverage_rect.p1 = max(local_coverage_rect.p0, local_coverage_rect.p1);
+
+ switch (qi.part_index) {
+ case PART_LEFT:
+ local_coverage_rect.p1.x = local_coverage_rect.p0.x + AA_PIXEL_RADIUS;
+#ifdef SWGL_ANTIALIAS
+ swgl_antiAlias(EDGE_AA_LEFT);
+#else
+ local_coverage_rect.p0.x -= AA_PIXEL_RADIUS;
+ local_coverage_rect.p0.y -= AA_PIXEL_RADIUS;
+ local_coverage_rect.p1.y += AA_PIXEL_RADIUS;
+#endif
+ break;
+ case PART_TOP:
+ local_coverage_rect.p0.x = local_coverage_rect.p0.x + AA_PIXEL_RADIUS;
+ local_coverage_rect.p1.x = local_coverage_rect.p1.x - AA_PIXEL_RADIUS;
+ local_coverage_rect.p1.y = local_coverage_rect.p0.y + AA_PIXEL_RADIUS;
+#ifdef SWGL_ANTIALIAS
+ swgl_antiAlias(EDGE_AA_TOP);
+#else
+ local_coverage_rect.p0.y -= AA_PIXEL_RADIUS;
+#endif
+ break;
+ case PART_RIGHT:
+ local_coverage_rect.p0.x = local_coverage_rect.p1.x - AA_PIXEL_RADIUS;
+#ifdef SWGL_ANTIALIAS
+ swgl_antiAlias(EDGE_AA_RIGHT);
+#else
+ local_coverage_rect.p1.x += AA_PIXEL_RADIUS;
+ local_coverage_rect.p0.y -= AA_PIXEL_RADIUS;
+ local_coverage_rect.p1.y += AA_PIXEL_RADIUS;
+#endif
+ break;
+ case PART_BOTTOM:
+ local_coverage_rect.p0.x = local_coverage_rect.p0.x + AA_PIXEL_RADIUS;
+ local_coverage_rect.p1.x = local_coverage_rect.p1.x - AA_PIXEL_RADIUS;
+ local_coverage_rect.p0.y = local_coverage_rect.p1.y - AA_PIXEL_RADIUS;
+#ifdef SWGL_ANTIALIAS
+ swgl_antiAlias(EDGE_AA_BOTTOM);
+#else
+ local_coverage_rect.p1.y += AA_PIXEL_RADIUS;
+#endif
+ break;
+ case PART_CENTER:
+ default:
+ local_coverage_rect.p0.x += edge_aa_offset(EDGE_AA_LEFT, qi.edge_flags);
+ local_coverage_rect.p1.x -= edge_aa_offset(EDGE_AA_RIGHT, qi.edge_flags);
+ local_coverage_rect.p0.y += edge_aa_offset(EDGE_AA_TOP, qi.edge_flags);
+ local_coverage_rect.p1.y -= edge_aa_offset(EDGE_AA_BOTTOM, qi.edge_flags);
+ break;
+ }
+
+ vec2 local_pos = mix(local_coverage_rect.p0, local_coverage_rect.p1, aPosition);
+
+ float device_pixel_scale = task.device_pixel_scale;
+ if ((qi.quad_flags & QF_IGNORE_DEVICE_SCALE) != 0) {
+ device_pixel_scale = 1.0f;
+ }
+
+ VertexInfo vi = write_vertex(
+ local_pos,
+ z,
+ transform,
+ task.content_origin,
+ task.task_rect,
+ device_pixel_scale,
+ qi.quad_flags
+ );
+
+ if (seg.uv_rect.xy == seg.uv_rect.zw) {
+ v_color = prim.color;
+ v_flags.y = 0;
+ } else {
+ v_color = vec4(1.0);
+ v_flags.y = 1;
+
+ vec2 f = (vi.local_pos - seg.rect.p0) / (seg.rect.p1 - seg.rect.p0);
+
+ vec2 uv = mix(
+ seg.uv_rect.xy,
+ seg.uv_rect.zw,
+ f
+ );
+
+ vec2 texture_size = vec2(TEX_SIZE(sColor0));
+
+ v_uv = uv / texture_size;
+
+ v_uv_sample_bounds = vec4(
+ seg.uv_rect.xy + vec2(0.5),
+ seg.uv_rect.zw - vec2(0.5)
+ ) / texture_size.xyxy;
+ }
+
+ return PrimitiveInfo(
+ vi.local_pos,
+ prim.bounds,
+ prim.clip,
+ qi.edge_flags
+ );
+}
+#endif
diff --git a/gfx/wr/webrender/res/ps_quad_mask.glsl b/gfx/wr/webrender/res/ps_quad_mask.glsl
new file mode 100644
index 0000000000..3c6f7f6713
--- /dev/null
+++ b/gfx/wr/webrender/res/ps_quad_mask.glsl
@@ -0,0 +1,165 @@
+/* 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/. */
+
+#include ps_quad,ellipse
+
+varying highp vec2 vClipLocalPos;
+
+#ifdef WR_FEATURE_FAST_PATH
+flat varying highp vec3 v_clip_params; // xy = box size, z = radius
+#else
+flat varying highp vec4 vClipCenter_Radius_TL;
+flat varying highp vec4 vClipCenter_Radius_TR;
+flat varying highp vec4 vClipCenter_Radius_BR;
+flat varying highp vec4 vClipCenter_Radius_BL;
+flat varying highp vec3 vClipPlane_TL;
+flat varying highp vec3 vClipPlane_TR;
+flat varying highp vec3 vClipPlane_BL;
+flat varying highp vec3 vClipPlane_BR;
+#endif
+flat varying highp vec2 vClipMode;
+
+#ifdef WR_VERTEX_SHADER
+
+PER_INSTANCE in ivec4 aClipData;
+
+struct Clip {
+ RectWithEndpoint rect;
+#ifdef WR_FEATURE_FAST_PATH
+ vec4 radii;
+#else
+ vec4 radii_top;
+ vec4 radii_bottom;
+#endif
+ float mode;
+};
+
+Clip fetch_clip(int index) {
+ Clip clip;
+
+#ifdef WR_FEATURE_FAST_PATH
+ vec4 texels[3] = fetch_from_gpu_buffer_3(index);
+ clip.rect = RectWithEndpoint(texels[0].xy, texels[0].zw);
+ clip.radii = texels[1];
+ clip.mode = texels[2].x;
+#else
+ vec4 texels[4] = fetch_from_gpu_buffer_4(index);
+ clip.rect = RectWithEndpoint(texels[0].xy, texels[0].zw);
+ clip.radii_top = texels[1];
+ clip.radii_bottom = texels[2];
+ clip.mode = texels[3].x;
+#endif
+
+ return clip;
+}
+
+void main(void) {
+ PrimitiveInfo prim_info = ps_quad_main();
+
+ Clip clip = fetch_clip(aClipData.y);
+
+ RectWithEndpoint xf_bounds = RectWithEndpoint(
+ max(clip.rect.p0, prim_info.local_clip_rect.p0),
+ min(clip.rect.p1, prim_info.local_clip_rect.p1)
+ );
+ vTransformBounds = vec4(xf_bounds.p0, xf_bounds.p1);
+
+ Transform clip_transform = fetch_transform(aClipData.x);
+
+ vClipLocalPos = (clip_transform.m * vec4(prim_info.local_pos, 0.0, 1.0)).xy;
+ vClipMode.x = clip.mode;
+
+#ifdef WR_FEATURE_FAST_PATH
+ // If the radii are all uniform, we can use a much simpler 2d
+ // signed distance function to get a rounded rect clip.
+ vec2 half_size = 0.5 * (clip.rect.p1 - clip.rect.p0);
+ float radius = clip.radii.x;
+ vClipLocalPos -= (half_size + clip.rect.p0);
+ v_clip_params = vec3(half_size - vec2(radius), radius);
+#else
+ vec2 r_tl = clip.radii_top.xy;
+ vec2 r_tr = clip.radii_top.zw;
+ vec2 r_br = clip.radii_bottom.zw;
+ vec2 r_bl = clip.radii_bottom.xy;
+
+ vClipCenter_Radius_TL = vec4(clip.rect.p0 + r_tl,
+ inverse_radii_squared(r_tl));
+
+ vClipCenter_Radius_TR = vec4(clip.rect.p1.x - r_tr.x,
+ clip.rect.p0.y + r_tr.y,
+ inverse_radii_squared(r_tr));
+
+ vClipCenter_Radius_BR = vec4(clip.rect.p1 - r_br,
+ inverse_radii_squared(r_br));
+
+ vClipCenter_Radius_BL = vec4(clip.rect.p0.x + r_bl.x,
+ clip.rect.p1.y - r_bl.y,
+ inverse_radii_squared(r_bl));
+
+ // We need to know the half-spaces of the corners separate from the center
+ // and radius. We compute a point that falls on the diagonal (which is just
+ // an inner vertex pushed out along one axis, but not on both) to get the
+ // plane offset of the half-space. We also compute the direction vector of
+ // the half-space, which is a perpendicular vertex (-y,x) of the vector of
+ // the diagonal. We leave the scales of the vectors unchanged.
+ vec2 n_tl = -r_tl.yx;
+ vec2 n_tr = vec2(r_tr.y, -r_tr.x);
+ vec2 n_br = r_br.yx;
+ vec2 n_bl = vec2(-r_bl.y, r_bl.x);
+ vClipPlane_TL = vec3(n_tl,
+ dot(n_tl, vec2(clip.rect.p0.x, clip.rect.p0.y + r_tl.y)));
+ vClipPlane_TR = vec3(n_tr,
+ dot(n_tr, vec2(clip.rect.p1.x - r_tr.x, clip.rect.p0.y)));
+ vClipPlane_BR = vec3(n_br,
+ dot(n_br, vec2(clip.rect.p1.x, clip.rect.p1.y - r_br.y)));
+ vClipPlane_BL = vec3(n_bl,
+ dot(n_bl, vec2(clip.rect.p0.x + r_bl.x, clip.rect.p1.y)));
+#endif
+
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+
+#ifdef WR_FEATURE_FAST_PATH
+// See http://www.iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm
+float sd_box(in vec2 pos, in vec2 box_size) {
+ vec2 d = abs(pos) - box_size;
+ return length(max(d, vec2(0.0))) + min(max(d.x,d.y), 0.0);
+}
+
+float sd_rounded_box(in vec2 pos, in vec2 box_size, in float radius) {
+ return sd_box(pos, box_size) - radius;
+}
+#endif
+
+void main(void) {
+ float aa_range = compute_aa_range(vClipLocalPos);
+
+#ifdef WR_FEATURE_FAST_PATH
+ float dist = sd_rounded_box(vClipLocalPos, v_clip_params.xy, v_clip_params.z);
+#else
+ float dist = distance_to_rounded_rect(
+ vClipLocalPos,
+ vClipPlane_TL,
+ vClipCenter_Radius_TL,
+ vClipPlane_TR,
+ vClipCenter_Radius_TR,
+ vClipPlane_BR,
+ vClipCenter_Radius_BR,
+ vClipPlane_BL,
+ vClipCenter_Radius_BL,
+ vTransformBounds
+ );
+#endif
+
+ // Compute AA for the given dist and range.
+ float alpha = distance_aa(aa_range, dist);
+
+ // Select alpha or inverse alpha depending on clip in/out.
+ float final_alpha = mix(alpha, 1.0 - alpha, vClipMode.x);
+
+ oFragColor = vec4(final_alpha);
+}
+#endif
diff --git a/gfx/wr/webrender/res/ps_quad_textured.glsl b/gfx/wr/webrender/res/ps_quad_textured.glsl
new file mode 100644
index 0000000000..6b49850f87
--- /dev/null
+++ b/gfx/wr/webrender/res/ps_quad_textured.glsl
@@ -0,0 +1,63 @@
+/* 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/. */
+
+#include ps_quad
+
+#ifndef SWGL_ANTIALIAS
+varying highp vec2 vLocalPos;
+#endif
+
+#ifdef WR_VERTEX_SHADER
+void main(void) {
+ PrimitiveInfo info = ps_quad_main();
+
+#ifndef SWGL_ANTIALIAS
+ RectWithEndpoint xf_bounds = RectWithEndpoint(
+ max(info.local_prim_rect.p0, info.local_clip_rect.p0),
+ min(info.local_prim_rect.p1, info.local_clip_rect.p1)
+ );
+ vTransformBounds = vec4(xf_bounds.p0, xf_bounds.p1);
+
+ vLocalPos = info.local_pos;
+
+ if (info.edge_flags == 0) {
+ v_flags.x = 0;
+ } else {
+ v_flags.x = 1;
+ }
+#endif
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+void main(void) {
+ vec4 color = v_color;
+
+#ifndef SWGL_ANTIALIAS
+ if (v_flags.x != 0) {
+ float alpha = init_transform_fs(vLocalPos);
+ color *= alpha;
+ }
+#endif
+
+ if (v_flags.y != 0) {
+ vec2 uv = clamp(v_uv, v_uv_sample_bounds.xy, v_uv_sample_bounds.zw);
+ vec4 texel = TEX_SAMPLE(sColor0, uv);
+ color *= texel;
+ }
+
+ oFragColor = color;
+}
+
+#if defined(SWGL_DRAW_SPAN)
+void swgl_drawSpanRGBA8() {
+ if (v_flags.y != 0) {
+ swgl_commitTextureLinearColorRGBA8(sColor0, v_uv, v_uv_sample_bounds, v_color);
+ } else {
+ swgl_commitSolidRGBA8(v_color);
+ }
+}
+#endif
+
+#endif
diff --git a/gfx/wr/webrender/res/ps_split_composite.glsl b/gfx/wr/webrender/res/ps_split_composite.glsl
new file mode 100644
index 0000000000..c07eca1371
--- /dev/null
+++ b/gfx/wr/webrender/res/ps_split_composite.glsl
@@ -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/. */
+
+#define WR_FEATURE_TEXTURE_2D
+
+#include shared,prim_shared
+
+// interpolated UV coordinates to sample.
+varying highp vec2 vUv;
+
+// Flag to allow perspective interpolation of UV.
+// Packed in to a vector to work around bug 1630356.
+flat varying mediump vec2 vPerspective;
+
+flat varying highp vec4 vUvSampleBounds;
+
+#ifdef WR_VERTEX_SHADER
+struct SplitGeometry {
+ vec2 local[4];
+};
+
+SplitGeometry fetch_split_geometry(int address) {
+ ivec2 uv = get_gpu_cache_uv(address);
+
+ vec4 data0 = TEXEL_FETCH(sGpuCache, uv, 0, ivec2(0, 0));
+ vec4 data1 = TEXEL_FETCH(sGpuCache, uv, 0, ivec2(1, 0));
+
+ SplitGeometry geo;
+ geo.local = vec2[4](
+ data0.xy,
+ data0.zw,
+ data1.xy,
+ data1.zw
+ );
+
+ return geo;
+}
+
+vec2 bilerp(vec2 a, vec2 b, vec2 c, vec2 d, float s, float t) {
+ vec2 x = mix(a, b, t);
+ vec2 y = mix(c, d, t);
+ return mix(x, y, s);
+}
+
+struct SplitCompositeInstance {
+ int prim_header_index;
+ int polygons_address;
+ float z;
+ int render_task_index;
+};
+
+SplitCompositeInstance fetch_composite_instance() {
+ SplitCompositeInstance ci;
+
+ ci.prim_header_index = aData.x;
+ ci.polygons_address = aData.y;
+ ci.z = float(aData.z);
+ ci.render_task_index = aData.w;
+
+ return ci;
+}
+
+void main(void) {
+ SplitCompositeInstance ci = fetch_composite_instance();
+ SplitGeometry geometry = fetch_split_geometry(ci.polygons_address);
+ PrimitiveHeader ph = fetch_prim_header(ci.prim_header_index);
+ PictureTask dest_task = fetch_picture_task(ci.render_task_index);
+ Transform transform = fetch_transform(ph.transform_id);
+ ImageSource res = fetch_image_source(ph.user_data.x);
+ ClipArea clip_area = fetch_clip_area(ph.user_data.w);
+
+ vec2 dest_origin = dest_task.task_rect.p0 -
+ dest_task.content_origin;
+
+ vec2 local_pos = bilerp(geometry.local[0], geometry.local[1],
+ geometry.local[3], geometry.local[2],
+ aPosition.y, aPosition.x);
+ vec4 world_pos = transform.m * vec4(local_pos, 0.0, 1.0);
+
+ vec4 final_pos = vec4(
+ dest_origin * world_pos.w + world_pos.xy * dest_task.device_pixel_scale,
+ world_pos.w * ci.z,
+ world_pos.w
+ );
+
+ write_clip(
+ world_pos,
+ clip_area,
+ dest_task
+ );
+
+ gl_Position = uTransform * final_pos;
+
+ vec2 texture_size = vec2(TEX_SIZE(sColor0));
+ vec2 uv0 = res.uv_rect.p0;
+ vec2 uv1 = res.uv_rect.p1;
+
+ vec2 min_uv = min(uv0, uv1);
+ vec2 max_uv = max(uv0, uv1);
+
+ vUvSampleBounds = vec4(
+ min_uv + vec2(0.5),
+ max_uv - vec2(0.5)
+ ) / texture_size.xyxy;
+
+ vec2 f = (local_pos - ph.local_rect.p0) / rect_size(ph.local_rect);
+ f = get_image_quad_uv(ph.user_data.x, f);
+ vec2 uv = mix(uv0, uv1, f);
+ float perspective_interpolate = float(ph.user_data.y);
+
+ vUv = uv / texture_size * mix(gl_Position.w, 1.0, perspective_interpolate);
+ vPerspective.x = perspective_interpolate;
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+void main(void) {
+ float alpha = do_clip();
+ float perspective_divisor = mix(gl_FragCoord.w, 1.0, vPerspective.x);
+ vec2 uv = clamp(vUv * perspective_divisor, vUvSampleBounds.xy, vUvSampleBounds.zw);
+ write_output(alpha * texture(sColor0, uv));
+}
+
+#ifdef SWGL_DRAW_SPAN
+void swgl_drawSpanRGBA8() {
+ float perspective_divisor = mix(swgl_forceScalar(gl_FragCoord.w), 1.0, vPerspective.x);
+ vec2 uv = vUv * perspective_divisor;
+
+ swgl_commitTextureRGBA8(sColor0, uv, vUvSampleBounds);
+}
+#endif
+
+#endif
diff --git a/gfx/wr/webrender/res/ps_text_run.glsl b/gfx/wr/webrender/res/ps_text_run.glsl
new file mode 100644
index 0000000000..9c5f34e3a6
--- /dev/null
+++ b/gfx/wr/webrender/res/ps_text_run.glsl
@@ -0,0 +1,354 @@
+/* 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/. */
+
+#include shared,prim_shared
+
+flat varying mediump vec4 v_color;
+flat varying mediump vec3 v_mask_swizzle;
+// Normalized bounds of the source image in the texture.
+flat varying highp vec4 v_uv_bounds;
+
+// Interpolated UV coordinates to sample.
+varying highp vec2 v_uv;
+
+
+#if defined(WR_FEATURE_GLYPH_TRANSFORM) && !defined(SWGL_CLIP_DIST)
+varying highp vec4 v_uv_clip;
+#endif
+
+#ifdef WR_VERTEX_SHADER
+
+#define VECS_PER_TEXT_RUN 2
+#define GLYPHS_PER_GPU_BLOCK 2U
+
+#ifdef WR_FEATURE_GLYPH_TRANSFORM
+RectWithEndpoint transform_rect(RectWithEndpoint rect, mat2 transform) {
+ vec2 size = rect_size(rect);
+ vec2 center = transform * (rect.p0 + size * 0.5);
+ vec2 radius = mat2(abs(transform[0]), abs(transform[1])) * (size * 0.5);
+ return RectWithEndpoint(center - radius, center + radius);
+}
+
+bool rect_inside_rect(RectWithEndpoint little, RectWithEndpoint big) {
+ return all(lessThanEqual(vec4(big.p0, little.p1), vec4(little.p0, big.p1)));
+}
+#endif //WR_FEATURE_GLYPH_TRANSFORM
+
+struct Glyph {
+ vec2 offset;
+};
+
+Glyph fetch_glyph(int specific_prim_address,
+ int glyph_index) {
+ // Two glyphs are packed in each texel in the GPU cache.
+ int glyph_address = specific_prim_address +
+ VECS_PER_TEXT_RUN +
+ int(uint(glyph_index) / GLYPHS_PER_GPU_BLOCK);
+ vec4 data = fetch_from_gpu_cache_1(glyph_address);
+ // Select XY or ZW based on glyph index.
+ vec2 glyph = mix(data.xy, data.zw,
+ bvec2(uint(glyph_index) % GLYPHS_PER_GPU_BLOCK == 1U));
+
+ return Glyph(glyph);
+}
+
+struct GlyphResource {
+ vec4 uv_rect;
+ vec2 offset;
+ float scale;
+};
+
+GlyphResource fetch_glyph_resource(int address) {
+ vec4 data[2] = fetch_from_gpu_cache_2(address);
+ return GlyphResource(data[0], data[1].xy, data[1].z);
+}
+
+struct TextRun {
+ vec4 color;
+ vec4 bg_color;
+};
+
+TextRun fetch_text_run(int address) {
+ vec4 data[2] = fetch_from_gpu_cache_2(address);
+ return TextRun(data[0], data[1]);
+}
+
+vec2 get_snap_bias(int subpx_dir) {
+ // In subpixel mode, the subpixel offset has already been
+ // accounted for while rasterizing the glyph. However, we
+ // must still round with a subpixel bias rather than rounding
+ // to the nearest whole pixel, depending on subpixel direciton.
+ switch (subpx_dir) {
+ case SUBPX_DIR_NONE:
+ default:
+ return vec2(0.5);
+ case SUBPX_DIR_HORIZONTAL:
+ // Glyphs positioned [-0.125, 0.125] get a
+ // subpx position of zero. So include that
+ // offset in the glyph position to ensure
+ // we round to the correct whole position.
+ return vec2(0.125, 0.5);
+ case SUBPX_DIR_VERTICAL:
+ return vec2(0.5, 0.125);
+ case SUBPX_DIR_MIXED:
+ return vec2(0.125);
+ }
+}
+
+void main() {
+ Instance instance = decode_instance_attributes();
+ PrimitiveHeader ph = fetch_prim_header(instance.prim_header_address);
+ Transform transform = fetch_transform(ph.transform_id);
+ ClipArea clip_area = fetch_clip_area(instance.clip_address);
+ PictureTask task = fetch_picture_task(instance.picture_task_address);
+
+ int glyph_index = instance.segment_index;
+ int subpx_dir = (instance.flags >> 8) & 0xff;
+ int color_mode = instance.flags & 0xff;
+
+ // Note that the reference frame relative offset is stored in the prim local
+ // rect size during batching, instead of the actual size of the primitive.
+ TextRun text = fetch_text_run(ph.specific_prim_address);
+ vec2 text_offset = ph.local_rect.p1;
+
+ if (color_mode == COLOR_MODE_FROM_PASS) {
+ color_mode = uMode;
+ }
+
+ // Note that the unsnapped reference frame relative offset has already
+ // been subtracted from the prim local rect origin during batching.
+ // It was done this way to avoid pushing both the snapped and the
+ // unsnapped offsets to the shader.
+ Glyph glyph = fetch_glyph(ph.specific_prim_address, glyph_index);
+ glyph.offset += ph.local_rect.p0;
+
+ GlyphResource res = fetch_glyph_resource(instance.resource_address);
+
+ vec2 snap_bias = get_snap_bias(subpx_dir);
+
+ // Glyph space refers to the pixel space used by glyph rasterization during frame
+ // building. If a non-identity transform was used, WR_FEATURE_GLYPH_TRANSFORM will
+ // be set. Otherwise, regardless of whether the raster space is LOCAL or SCREEN,
+ // we ignored the transform during glyph rasterization, and need to snap just using
+ // the device pixel scale and the raster scale.
+#ifdef WR_FEATURE_GLYPH_TRANSFORM
+ // Transform from local space to glyph space.
+ mat2 glyph_transform = mat2(transform.m) * task.device_pixel_scale;
+ vec2 glyph_translation = transform.m[3].xy * task.device_pixel_scale;
+
+ // Transform from glyph space back to local space.
+ mat2 glyph_transform_inv = inverse(glyph_transform);
+
+ // Glyph raster pixels include the impact of the transform. This path can only be
+ // entered for 3d transforms that can be coerced into a 2d transform; they have no
+ // perspective, and have a 2d inverse. This is a looser condition than axis aligned
+ // transforms because it also allows 2d rotations.
+ vec2 raster_glyph_offset = floor(glyph_transform * glyph.offset + snap_bias);
+
+ // We want to eliminate any subpixel translation in device space to ensure glyph
+ // snapping is stable for equivalent glyph subpixel positions. Note that we must take
+ // into account the translation from the transform for snapping purposes.
+ vec2 raster_text_offset = floor(glyph_transform * text_offset + glyph_translation + 0.5) - glyph_translation;
+
+ vec2 glyph_origin = res.offset + raster_glyph_offset + raster_text_offset;
+ // Compute the glyph rect in glyph space.
+ RectWithEndpoint glyph_rect = RectWithEndpoint(
+ glyph_origin,
+ glyph_origin + res.uv_rect.zw - res.uv_rect.xy
+ );
+
+ // The glyph rect is in glyph space, so transform it back to local space.
+ RectWithEndpoint local_rect = transform_rect(glyph_rect, glyph_transform_inv);
+
+ // Select the corner of the glyph's local space rect that we are processing.
+ vec2 local_pos = mix(local_rect.p0, local_rect.p1, aPosition.xy);
+
+ // If the glyph's local rect would fit inside the local clip rect, then select a corner from
+ // the device space glyph rect to reduce overdraw of clipped pixels in the fragment shader.
+ // Otherwise, fall back to clamping the glyph's local rect to the local clip rect.
+ if (rect_inside_rect(local_rect, ph.local_clip_rect)) {
+ local_pos = glyph_transform_inv * mix(glyph_rect.p0, glyph_rect.p1, aPosition.xy);
+ }
+#else
+ float raster_scale = float(ph.user_data.x) / 65535.0;
+
+ // Scale in which the glyph is snapped when rasterized.
+ float glyph_raster_scale = raster_scale * task.device_pixel_scale;
+
+ // Scale from glyph space to local space.
+ float glyph_scale_inv = res.scale / glyph_raster_scale;
+
+ // Glyph raster pixels do not include the impact of the transform. Instead it was
+ // replaced with an identity transform during glyph rasterization. As such only the
+ // impact of the raster scale (if in local space) and the device pixel scale (for both
+ // local and screen space) are included.
+ //
+ // This implies one or more of the following conditions:
+ // - The transform is an identity. In that case, setting WR_FEATURE_GLYPH_TRANSFORM
+ // should have the same output result as not. We just distingush which path to use
+ // based on the transform used during glyph rasterization. (Screen space).
+ // - The transform contains an animation. We will imply local raster space in such
+ // cases to avoid constantly rerasterizing the glyphs.
+ // - The transform has perspective or does not have a 2d inverse (Screen or local space).
+ // - The transform's scale will result in result in very large rasterized glyphs and
+ // we clamped the size. This will imply local raster space.
+ vec2 raster_glyph_offset = floor(glyph.offset * glyph_raster_scale + snap_bias) / res.scale;
+
+ // Compute the glyph rect in local space.
+ //
+ // The transform may be animated, so we don't want to do any snapping here for the
+ // text offset to avoid glyphs wiggling. The text offset should have been snapped
+ // already for axis aligned transforms excluding any animations during frame building.
+ vec2 glyph_origin = glyph_scale_inv * (res.offset + raster_glyph_offset) + text_offset;
+ RectWithEndpoint glyph_rect = RectWithEndpoint(
+ glyph_origin,
+ glyph_origin + glyph_scale_inv * (res.uv_rect.zw - res.uv_rect.xy)
+ );
+
+ // Select the corner of the glyph rect that we are processing.
+ vec2 local_pos = mix(glyph_rect.p0, glyph_rect.p1, aPosition.xy);
+#endif
+
+ VertexInfo vi = write_vertex(
+ local_pos,
+ ph.local_clip_rect,
+ ph.z,
+ transform,
+ task
+ );
+
+#ifdef WR_FEATURE_GLYPH_TRANSFORM
+ vec2 f = (glyph_transform * vi.local_pos - glyph_rect.p0) / rect_size(glyph_rect);
+ #ifdef SWGL_CLIP_DIST
+ gl_ClipDistance[0] = f.x;
+ gl_ClipDistance[1] = f.y;
+ gl_ClipDistance[2] = 1.0 - f.x;
+ gl_ClipDistance[3] = 1.0 - f.y;
+ #else
+ v_uv_clip = vec4(f, 1.0 - f);
+ #endif
+#else
+ vec2 f = (vi.local_pos - glyph_rect.p0) / rect_size(glyph_rect);
+#endif
+
+ write_clip(vi.world_pos, clip_area, task);
+
+ switch (color_mode) {
+ case COLOR_MODE_ALPHA:
+ v_mask_swizzle = vec3(0.0, 1.0, 1.0);
+ v_color = text.color;
+ break;
+ case COLOR_MODE_BITMAP_SHADOW:
+ #ifdef SWGL_BLEND
+ swgl_blendDropShadow(text.color);
+ v_mask_swizzle = vec3(1.0, 0.0, 0.0);
+ v_color = vec4(1.0);
+ #else
+ v_mask_swizzle = vec3(0.0, 1.0, 0.0);
+ v_color = text.color;
+ #endif
+ break;
+ case COLOR_MODE_SUBPX_BG_PASS2:
+ v_mask_swizzle = vec3(1.0, 0.0, 0.0);
+ v_color = text.color;
+ break;
+ case COLOR_MODE_SUBPX_BG_PASS0:
+ case COLOR_MODE_COLOR_BITMAP:
+ v_mask_swizzle = vec3(1.0, 0.0, 0.0);
+ v_color = vec4(text.color.a);
+ break;
+ case COLOR_MODE_SUBPX_BG_PASS1:
+ v_mask_swizzle = vec3(-1.0, 1.0, 0.0);
+ v_color = vec4(text.color.a) * text.bg_color;
+ break;
+ case COLOR_MODE_SUBPX_DUAL_SOURCE:
+ #ifdef SWGL_BLEND
+ swgl_blendSubpixelText(text.color);
+ v_mask_swizzle = vec3(1.0, 0.0, 0.0);
+ v_color = vec4(1.0);
+ #else
+ v_mask_swizzle = vec3(text.color.a, 0.0, 0.0);
+ v_color = text.color;
+ #endif
+ break;
+ default:
+ v_mask_swizzle = vec3(0.0, 0.0, 0.0);
+ v_color = vec4(1.0);
+ }
+
+ vec2 texture_size = vec2(TEX_SIZE(sColor0));
+ vec2 st0 = res.uv_rect.xy / texture_size;
+ vec2 st1 = res.uv_rect.zw / texture_size;
+
+ v_uv = mix(st0, st1, f);
+ v_uv_bounds = (res.uv_rect + vec4(0.5, 0.5, -0.5, -0.5)) / texture_size.xyxy;
+}
+
+#endif // WR_VERTEX_SHADER
+
+#ifdef WR_FRAGMENT_SHADER
+
+Fragment text_fs(void) {
+ Fragment frag;
+
+ vec2 tc = clamp(v_uv, v_uv_bounds.xy, v_uv_bounds.zw);
+ vec4 mask = texture(sColor0, tc);
+ // v_mask_swizzle.z != 0 means we are using an R8 texture as alpha,
+ // and therefore must swizzle from the r channel to all channels.
+ mask = mix(mask, mask.rrrr, bvec4(v_mask_swizzle.z != 0.0));
+ #ifndef WR_FEATURE_DUAL_SOURCE_BLENDING
+ mask.rgb = mask.rgb * v_mask_swizzle.x + mask.aaa * v_mask_swizzle.y;
+ #endif
+
+ #if defined(WR_FEATURE_GLYPH_TRANSFORM) && !defined(SWGL_CLIP_DIST)
+ mask *= float(all(greaterThanEqual(v_uv_clip, vec4(0.0))));
+ #endif
+
+ frag.color = v_color * mask;
+
+ #if defined(WR_FEATURE_DUAL_SOURCE_BLENDING) && !defined(SWGL_BLEND)
+ frag.blend = mask * v_mask_swizzle.x + mask.aaaa * v_mask_swizzle.y;
+ #endif
+
+ return frag;
+}
+
+
+void main() {
+ Fragment frag = text_fs();
+
+ float clip_mask = do_clip();
+ frag.color *= clip_mask;
+
+ #if defined(WR_FEATURE_DEBUG_OVERDRAW)
+ oFragColor = WR_DEBUG_OVERDRAW_COLOR;
+ #elif defined(WR_FEATURE_DUAL_SOURCE_BLENDING) && !defined(SWGL_BLEND)
+ oFragColor = frag.color;
+ oFragBlend = frag.blend * clip_mask;
+ #else
+ write_output(frag.color);
+ #endif
+}
+
+#if defined(SWGL_DRAW_SPAN) && defined(SWGL_BLEND) && defined(SWGL_CLIP_DIST)
+void swgl_drawSpanRGBA8() {
+ // Only support simple swizzles for now. More complex swizzles must either
+ // be handled by blend overrides or the slow path.
+ if (v_mask_swizzle.x != 0.0 && v_mask_swizzle.x != 1.0) {
+ return;
+ }
+
+ #ifdef WR_FEATURE_DUAL_SOURCE_BLENDING
+ swgl_commitTextureLinearRGBA8(sColor0, v_uv, v_uv_bounds);
+ #else
+ if (swgl_isTextureR8(sColor0)) {
+ swgl_commitTextureLinearColorR8ToRGBA8(sColor0, v_uv, v_uv_bounds, v_color);
+ } else {
+ swgl_commitTextureLinearColorRGBA8(sColor0, v_uv, v_uv_bounds, v_color);
+ }
+ #endif
+}
+#endif
+
+#endif // WR_FRAGMENT_SHADER
diff --git a/gfx/wr/webrender/res/rect.glsl b/gfx/wr/webrender/res/rect.glsl
new file mode 100644
index 0000000000..2a080ee393
--- /dev/null
+++ b/gfx/wr/webrender/res/rect.glsl
@@ -0,0 +1,40 @@
+/* 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/. */
+
+struct RectWithSize {
+ vec2 p0;
+ vec2 size;
+};
+
+struct RectWithEndpoint {
+ vec2 p0;
+ vec2 p1;
+};
+
+float point_inside_rect(vec2 p, vec2 p0, vec2 p1) {
+ vec2 s = step(p0, p) - step(p1, p);
+ return s.x * s.y;
+}
+
+vec2 signed_distance_rect_xy(vec2 pos, vec2 p0, vec2 p1) {
+ // Instead of using a true signed distance to rect here, we just use the
+ // simpler approximation of the maximum distance on either axis from the
+ // outside of the rectangle. This avoids expensive use of length() and only
+ // causes mostly imperceptible differences at corner pixels.
+ return max(p0 - pos, pos - p1);
+}
+
+float signed_distance_rect(vec2 pos, vec2 p0, vec2 p1) {
+ // Collapse the per-axis distances to edges to a single approximate value.
+ vec2 d = signed_distance_rect_xy(pos, p0, p1);
+ return max(d.x, d.y);
+}
+
+vec2 rect_clamp(RectWithEndpoint rect, vec2 pt) {
+ return clamp(pt, rect.p0, rect.p1);
+}
+
+vec2 rect_size(RectWithEndpoint rect) {
+ return rect.p1 - rect.p0;
+}
diff --git a/gfx/wr/webrender/res/render_task.glsl b/gfx/wr/webrender/res/render_task.glsl
new file mode 100644
index 0000000000..cd9aea402c
--- /dev/null
+++ b/gfx/wr/webrender/res/render_task.glsl
@@ -0,0 +1,102 @@
+/* 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/. */
+
+
+#ifdef WR_VERTEX_SHADER
+#define VECS_PER_RENDER_TASK 2U
+
+uniform HIGHP_SAMPLER_FLOAT sampler2D sRenderTasks;
+
+struct RenderTaskData {
+ RectWithEndpoint task_rect;
+ vec4 user_data;
+};
+
+// See RenderTaskData in render_task.rs
+RenderTaskData fetch_render_task_data(int index) {
+ ivec2 uv = get_fetch_uv(index, VECS_PER_RENDER_TASK);
+
+ vec4 texel0 = TEXEL_FETCH(sRenderTasks, uv, 0, ivec2(0, 0));
+ vec4 texel1 = TEXEL_FETCH(sRenderTasks, uv, 0, ivec2(1, 0));
+
+ RectWithEndpoint task_rect = RectWithEndpoint(
+ texel0.xy,
+ texel0.zw
+ );
+
+ RenderTaskData data = RenderTaskData(
+ task_rect,
+ texel1
+ );
+
+ return data;
+}
+
+RectWithEndpoint fetch_render_task_rect(int index) {
+ ivec2 uv = get_fetch_uv(index, VECS_PER_RENDER_TASK);
+
+ vec4 texel0 = TEXEL_FETCH(sRenderTasks, uv, 0, ivec2(0, 0));
+ vec4 texel1 = TEXEL_FETCH(sRenderTasks, uv, 0, ivec2(1, 0));
+
+ RectWithEndpoint task_rect = RectWithEndpoint(
+ texel0.xy,
+ texel0.zw
+ );
+
+ return task_rect;
+}
+
+#define PIC_TYPE_IMAGE 1
+#define PIC_TYPE_TEXT_SHADOW 2
+
+/*
+ The dynamic picture that this brush exists on. Right now, it
+ contains minimal information. In the future, it will describe
+ the transform mode of primitives on this picture, among other things.
+ */
+struct PictureTask {
+ RectWithEndpoint task_rect;
+ float device_pixel_scale;
+ vec2 content_origin;
+};
+
+PictureTask fetch_picture_task(int address) {
+ RenderTaskData task_data = fetch_render_task_data(address);
+
+ PictureTask task = PictureTask(
+ task_data.task_rect,
+ task_data.user_data.x,
+ task_data.user_data.yz
+ );
+
+ return task;
+}
+
+#define CLIP_TASK_EMPTY 0x7FFF
+
+struct ClipArea {
+ RectWithEndpoint task_rect;
+ float device_pixel_scale;
+ vec2 screen_origin;
+};
+
+ClipArea fetch_clip_area(int index) {
+ ClipArea area;
+
+ if (index >= CLIP_TASK_EMPTY) {
+ area.task_rect = RectWithEndpoint(vec2(0.0), vec2(0.0));
+ area.device_pixel_scale = 0.0;
+ area.screen_origin = vec2(0.0);
+ } else {
+ RenderTaskData task_data = fetch_render_task_data(index);
+
+ area.task_rect = task_data.task_rect;
+ area.device_pixel_scale = task_data.user_data.x;
+ area.screen_origin = task_data.user_data.yz;
+ }
+
+ return area;
+}
+
+#endif //WR_VERTEX_SHADER
diff --git a/gfx/wr/webrender/res/shared.glsl b/gfx/wr/webrender/res/shared.glsl
new file mode 100644
index 0000000000..4f21bd205d
--- /dev/null
+++ b/gfx/wr/webrender/res/shared.glsl
@@ -0,0 +1,230 @@
+/* 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/. */
+
+#ifdef WR_FEATURE_TEXTURE_EXTERNAL
+// Please check https://www.khronos.org/registry/OpenGL/extensions/OES/OES_EGL_image_external_essl3.txt
+// for this extension.
+#extension GL_OES_EGL_image_external_essl3 : require
+#endif
+
+#ifdef WR_FEATURE_TEXTURE_EXTERNAL_ESSL1
+// Some GLES 3 devices do not support GL_OES_EGL_image_external_essl3, so we
+// must use GL_OES_EGL_image_external instead and make the shader ESSL1
+// compatible.
+#extension GL_OES_EGL_image_external : require
+#endif
+
+#ifdef WR_FEATURE_ADVANCED_BLEND
+#extension GL_KHR_blend_equation_advanced : require
+#endif
+
+#ifdef WR_FEATURE_DUAL_SOURCE_BLENDING
+#ifdef GL_ES
+#extension GL_EXT_blend_func_extended : require
+#else
+#extension GL_ARB_explicit_attrib_location : require
+#endif
+#endif
+
+#include base
+
+#if defined(WR_FEATURE_TEXTURE_EXTERNAL_ESSL1)
+#define TEX_SAMPLE(sampler, tex_coord) texture2D(sampler, tex_coord.xy)
+#else
+#define TEX_SAMPLE(sampler, tex_coord) texture(sampler, tex_coord.xy)
+#endif
+
+#if defined(WR_FEATURE_TEXTURE_EXTERNAL) && defined(PLATFORM_ANDROID)
+// On some Mali GPUs we have encountered crashes in glDrawElements when using
+// textureSize(samplerExternalOES) in a vertex shader without potentially
+// sampling from the texture. This tricks the driver in to thinking the texture
+// may be sampled from, avoiding the crash. See bug 1692848.
+uniform bool u_mali_workaround_dummy;
+#define TEX_SIZE(sampler) (u_mali_workaround_dummy ? ivec2(texture(sampler, vec2(0.0, 0.0)).rr) : textureSize(sampler, 0))
+#else
+#define TEX_SIZE(sampler) textureSize(sampler, 0)
+#endif
+
+//======================================================================================
+// Vertex shader attributes and uniforms
+//======================================================================================
+#ifdef WR_VERTEX_SHADER
+ // A generic uniform that shaders can optionally use to configure
+ // an operation mode for this batch.
+ uniform int uMode;
+
+ // Uniform inputs
+ uniform mat4 uTransform; // Orthographic projection
+
+ // Attribute inputs
+ attribute vec2 aPosition;
+
+ // get_fetch_uv is a macro to work around a macOS Intel driver parsing bug.
+ // TODO: convert back to a function once the driver issues are resolved, if ever.
+ // https://github.com/servo/webrender/pull/623
+ // https://github.com/servo/servo/issues/13953
+ // Do the division with unsigned ints because that's more efficient with D3D
+ #define get_fetch_uv(i, vpi) ivec2(int(vpi * (uint(i) % (WR_MAX_VERTEX_TEXTURE_WIDTH/vpi))), int(uint(i) / (WR_MAX_VERTEX_TEXTURE_WIDTH/vpi)))
+#endif
+
+//======================================================================================
+// Fragment shader attributes and uniforms
+//======================================================================================
+#ifdef WR_FRAGMENT_SHADER
+ // Uniform inputs
+
+ // Fragment shader outputs
+ #ifdef WR_FEATURE_ADVANCED_BLEND
+ layout(blend_support_all_equations) out;
+ #endif
+
+ #if __VERSION__ == 100
+ #define oFragColor gl_FragColor
+ #elif defined(WR_FEATURE_DUAL_SOURCE_BLENDING)
+ layout(location = 0, index = 0) out vec4 oFragColor;
+ layout(location = 0, index = 1) out vec4 oFragBlend;
+ #else
+ out vec4 oFragColor;
+ #endif
+
+ // Write an output color in normal shaders.
+ void write_output(vec4 color) {
+ oFragColor = color;
+ }
+
+ #define EPSILON 0.0001
+
+ // "Show Overdraw" color. Premultiplied.
+ #define WR_DEBUG_OVERDRAW_COLOR vec4(0.110, 0.077, 0.027, 0.125)
+
+ float distance_to_line(vec2 p0, vec2 perp_dir, vec2 p) {
+ vec2 dir_to_p0 = p0 - p;
+ return dot(normalize(perp_dir), dir_to_p0);
+ }
+
+// fwidth is not defined in ESSL 1, but that's okay because we don't need
+// it for any ESSL 1 shader variants.
+#if __VERSION__ != 100
+ /// Find the appropriate half range to apply the AA approximation over.
+ /// This range represents a coefficient to go from one CSS pixel to half a device pixel.
+ vec2 compute_aa_range_xy(vec2 position) {
+ return fwidth(position);
+ }
+
+ float compute_aa_range(vec2 position) {
+ // The constant factor is chosen to compensate for the fact that length(fw) is equal
+ // to sqrt(2) times the device pixel ratio in the typical case.
+ //
+ // This coefficient is chosen to ensure that any sample 0.5 pixels or more inside of
+ // the shape has no anti-aliasing applied to it (since pixels are sampled at their center,
+ // such a pixel (axis aligned) is fully inside the border). We need this so that antialiased
+ // curves properly connect with non-antialiased vertical or horizontal lines, among other things.
+ //
+ // Lines over a half-pixel away from the pixel center *can* intersect with the pixel square;
+ // indeed, unless they are horizontal or vertical, they are guaranteed to. However, choosing
+ // a nonzero area for such pixels causes noticeable artifacts at the junction between an anti-
+ // aliased corner and a straight edge.
+ //
+ // We may want to adjust this constant in specific scenarios (for example keep the principled
+ // value for straight edges where we want pixel-perfect equivalence with non antialiased lines
+ // when axis aligned, while selecting a larger and smoother aa range on curves).
+ //
+ // As a further optimization, we compute the reciprocal of this range, such that we
+ // can then use the cheaper inversesqrt() instead of length(). This also elides a
+ // division that would otherwise be necessary inside distance_aa.
+ #ifdef SWGL
+ // SWGL uses an approximation for fwidth() such that it returns equal x and y.
+ // Thus, sqrt(2)/length(w) = sqrt(2)/sqrt(x*x + x*x) = recip(x).
+ return recip(fwidth(position).x);
+ #else
+ // sqrt(2)/length(w) = inversesqrt(0.5 * dot(w, w))
+ vec2 w = fwidth(position);
+ return inversesqrt(0.5 * dot(w, w));
+ #endif
+ }
+#endif
+
+ /// Return the blending coefficient for distance antialiasing.
+ ///
+ /// 0.0 means inside the shape, 1.0 means outside.
+ ///
+ /// This makes the simplifying assumption that the area of a 1x1 pixel square
+ /// under a line is reasonably similar to just the signed Euclidian distance
+ /// from the center of the square to that line. This diverges slightly from
+ /// better approximations of the exact area, but the difference between the
+ /// methods is not perceptibly noticeable, while this approximation is much
+ /// faster to compute.
+ ///
+ /// See the comments in `compute_aa_range()` for more information on the
+ /// cutoff values of -0.5 and 0.5.
+ float distance_aa_xy(vec2 aa_range, vec2 signed_distance) {
+ // The aa_range is the raw per-axis filter width, so we need to divide
+ // the local signed distance by the filter width to get an approximation
+ // of screen distance.
+ #ifdef SWGL
+ // The SWGL fwidth() approximation returns uniform X and Y ranges.
+ vec2 dist = signed_distance * recip(aa_range.x);
+ #else
+ vec2 dist = signed_distance / aa_range;
+ #endif
+ // Choose whichever axis is further outside the rectangle for AA.
+ return clamp(0.5 - max(dist.x, dist.y), 0.0, 1.0);
+ }
+
+ float distance_aa(float aa_range, float signed_distance) {
+ // The aa_range is already stored as a reciprocal with uniform scale,
+ // so just multiply it, then use that for AA.
+ float dist = signed_distance * aa_range;
+ return clamp(0.5 - dist, 0.0, 1.0);
+ }
+
+ /// Component-wise selection.
+ ///
+ /// The idea of using this is to ensure both potential branches are executed before
+ /// selecting the result, to avoid observable timing differences based on the condition.
+ ///
+ /// Example usage: color = if_then_else(LessThanEqual(color, vec3(0.5)), vec3(0.0), vec3(1.0));
+ ///
+ /// The above example sets each component to 0.0 or 1.0 independently depending on whether
+ /// their values are below or above 0.5.
+ ///
+ /// This is written as a macro in order to work with vectors of any dimension.
+ ///
+ /// Note: Some older android devices don't support mix with bvec. If we ever run into them
+ /// the only option we have is to polyfill it with a branch per component.
+ #define if_then_else(cond, then_branch, else_branch) mix(else_branch, then_branch, cond)
+#endif
+
+//======================================================================================
+// Shared shader uniforms
+//======================================================================================
+#ifdef WR_FEATURE_TEXTURE_2D
+uniform sampler2D sColor0;
+uniform sampler2D sColor1;
+uniform sampler2D sColor2;
+#elif defined WR_FEATURE_TEXTURE_RECT
+uniform sampler2DRect sColor0;
+uniform sampler2DRect sColor1;
+uniform sampler2DRect sColor2;
+#elif defined(WR_FEATURE_TEXTURE_EXTERNAL) || defined(WR_FEATURE_TEXTURE_EXTERNAL_ESSL1)
+uniform samplerExternalOES sColor0;
+uniform samplerExternalOES sColor1;
+uniform samplerExternalOES sColor2;
+#endif
+
+#ifdef WR_FEATURE_DITHERING
+uniform sampler2D sDither;
+#endif
+
+//======================================================================================
+// Interpolator definitions
+//======================================================================================
+
+//======================================================================================
+// VS only types and UBOs
+//======================================================================================
+
+//======================================================================================
+// VS only functions
+//======================================================================================
diff --git a/gfx/wr/webrender/res/shared_other.glsl b/gfx/wr/webrender/res/shared_other.glsl
new file mode 100644
index 0000000000..03cad173cd
--- /dev/null
+++ b/gfx/wr/webrender/res/shared_other.glsl
@@ -0,0 +1,33 @@
+/* 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/. */
+
+//======================================================================================
+// Vertex shader attributes and uniforms
+//======================================================================================
+#ifdef WR_VERTEX_SHADER
+#endif
+
+//======================================================================================
+// Fragment shader attributes and uniforms
+//======================================================================================
+#ifdef WR_FRAGMENT_SHADER
+#endif
+
+//======================================================================================
+// Interpolator definitions
+//======================================================================================
+
+//======================================================================================
+// VS only types and UBOs
+//======================================================================================
+
+//======================================================================================
+// VS only functions
+//======================================================================================
+
+//======================================================================================
+// FS only functions
+//======================================================================================
+#ifdef WR_FRAGMENT_SHADER
+#endif
diff --git a/gfx/wr/webrender/res/transform.glsl b/gfx/wr/webrender/res/transform.glsl
new file mode 100644
index 0000000000..d068b26c0e
--- /dev/null
+++ b/gfx/wr/webrender/res/transform.glsl
@@ -0,0 +1,140 @@
+/* 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/. */
+
+flat varying highp vec4 vTransformBounds;
+
+#ifdef WR_VERTEX_SHADER
+
+#define VECS_PER_TRANSFORM 8U
+uniform HIGHP_SAMPLER_FLOAT sampler2D sTransformPalette;
+
+void init_transform_vs(vec4 local_bounds) {
+ vTransformBounds = local_bounds;
+}
+
+struct Transform {
+ mat4 m;
+ mat4 inv_m;
+ bool is_axis_aligned;
+};
+
+Transform fetch_transform(int id) {
+ Transform transform;
+
+ transform.is_axis_aligned = (id >> 23) == 0;
+ int index = id & 0x007fffff;
+
+ // Create a UV base coord for each 8 texels.
+ // This is required because trying to use an offset
+ // of more than 8 texels doesn't work on some versions
+ // of macOS.
+ ivec2 uv = get_fetch_uv(index, VECS_PER_TRANSFORM);
+ ivec2 uv0 = ivec2(uv.x + 0, uv.y);
+
+ transform.m[0] = TEXEL_FETCH(sTransformPalette, uv0, 0, ivec2(0, 0));
+ transform.m[1] = TEXEL_FETCH(sTransformPalette, uv0, 0, ivec2(1, 0));
+ transform.m[2] = TEXEL_FETCH(sTransformPalette, uv0, 0, ivec2(2, 0));
+ transform.m[3] = TEXEL_FETCH(sTransformPalette, uv0, 0, ivec2(3, 0));
+
+ transform.inv_m[0] = TEXEL_FETCH(sTransformPalette, uv0, 0, ivec2(4, 0));
+ transform.inv_m[1] = TEXEL_FETCH(sTransformPalette, uv0, 0, ivec2(5, 0));
+ transform.inv_m[2] = TEXEL_FETCH(sTransformPalette, uv0, 0, ivec2(6, 0));
+ transform.inv_m[3] = TEXEL_FETCH(sTransformPalette, uv0, 0, ivec2(7, 0));
+
+ return transform;
+}
+
+// Return the intersection of the plane (set up by "normal" and "point")
+// with the ray (set up by "ray_origin" and "ray_dir"),
+// writing the resulting scaler into "t".
+bool ray_plane(vec3 normal, vec3 pt, vec3 ray_origin, vec3 ray_dir, out float t)
+{
+ float denom = dot(normal, ray_dir);
+ if (abs(denom) > 1e-6) {
+ vec3 d = pt - ray_origin;
+ t = dot(d, normal) / denom;
+ return t >= 0.0;
+ }
+
+ return false;
+}
+
+// Apply the inverse transform "inv_transform"
+// to the reference point "ref" in CSS space,
+// producing a local point on a Transform plane,
+// set by a base point "a" and a normal "n".
+vec4 untransform(vec2 ref, vec3 n, vec3 a, mat4 inv_transform) {
+ vec3 p = vec3(ref, -10000.0);
+ vec3 d = vec3(0, 0, 1.0);
+
+ float t = 0.0;
+ // get an intersection of the Transform plane with Z axis vector,
+ // originated from the "ref" point
+ ray_plane(n, a, p, d, t);
+ float z = p.z + d.z * t; // Z of the visible point on the Transform
+
+ vec4 r = inv_transform * vec4(ref, z, 1.0);
+ return r;
+}
+
+// Given a CSS space position, transform it back into the Transform space.
+vec4 get_node_pos(vec2 pos, Transform transform) {
+ // get a point on the scroll node plane
+ vec4 ah = transform.m * vec4(0.0, 0.0, 0.0, 1.0);
+ vec3 a = ah.xyz / ah.w;
+
+ // get the normal to the scroll node plane
+ vec3 n = transpose(mat3(transform.inv_m)) * vec3(0.0, 0.0, 1.0);
+ return untransform(pos, n, a, transform.inv_m);
+}
+
+#endif //WR_VERTEX_SHADER
+
+#ifdef WR_FRAGMENT_SHADER
+
+// Assume transform bounds are set to a large scale to signal they are invalid.
+bool has_valid_transform_bounds() {
+ return vTransformBounds.w < 1.0e15;
+}
+
+float init_transform_fs(vec2 local_pos) {
+ // Ideally we want to track distances in screen space after transformation
+ // as signed distance calculations lose context about the direction vector
+ // to exit the geometry, merely remembering the minimum distance to the
+ // exit. However, we can't always sanely track distances in screen space
+ // due to perspective transforms, clipping, and other concerns, so we do
+ // this in local space. However, this causes problems tracking distances
+ // in local space when attempting to scale by a uniform AA range later in
+ // the presence of a transform which actually has non-uniform scaling.
+ //
+ // To work around this, we independently track the distances on the local
+ // space X and Y axes and then scale them by the independent AA ranges (as
+ // computed from fwidth derivatives) for the X and Y axes. This can break
+ // down at certain angles (45 degrees or close to it), but still gives a
+ // better approximation of screen-space distances in the presence of non-
+ // uniform scaling for other rotations.
+ //
+ // Get signed distance from local rect bounds.
+ vec2 d = signed_distance_rect_xy(
+ local_pos,
+ vTransformBounds.xy,
+ vTransformBounds.zw
+ );
+
+ // Find the appropriate distance to apply the AA smoothstep over.
+ vec2 aa_range = compute_aa_range_xy(local_pos);
+
+ // Only apply AA to fragments outside the signed distance field.
+ return distance_aa_xy(aa_range, d);
+}
+
+float init_transform_rough_fs(vec2 local_pos) {
+ return point_inside_rect(
+ local_pos,
+ vTransformBounds.xy,
+ vTransformBounds.zw
+ );
+}
+
+#endif //WR_FRAGMENT_SHADER
diff --git a/gfx/wr/webrender/res/yuv.glsl b/gfx/wr/webrender/res/yuv.glsl
new file mode 100644
index 0000000000..ccbfecd086
--- /dev/null
+++ b/gfx/wr/webrender/res/yuv.glsl
@@ -0,0 +1,237 @@
+/* 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/. */
+
+#include shared
+
+#define YUV_FORMAT_NV12 0
+#define YUV_FORMAT_P010 1
+#define YUV_FORMAT_PLANAR 2
+#define YUV_FORMAT_INTERLEAVED 3
+
+//#define YUV_PRECISION mediump
+#define YUV_PRECISION highp
+
+#ifdef WR_VERTEX_SHADER
+
+#ifdef WR_FEATURE_TEXTURE_RECT
+ #define TEX_SIZE_YUV(sampler) vec2(1.0)
+#else
+ #define TEX_SIZE_YUV(sampler) vec2(TEX_SIZE(sampler).xy)
+#endif
+
+// `YuvRangedColorSpace`
+#define YUV_COLOR_SPACE_REC601_NARROW 0
+#define YUV_COLOR_SPACE_REC601_FULL 1
+#define YUV_COLOR_SPACE_REC709_NARROW 2
+#define YUV_COLOR_SPACE_REC709_FULL 3
+#define YUV_COLOR_SPACE_REC2020_NARROW 4
+#define YUV_COLOR_SPACE_REC2020_FULL 5
+#define YUV_COLOR_SPACE_GBR_IDENTITY 6
+
+// The constants added to the Y, U and V components are applied in the fragment shader.
+
+// `rgbFromYuv` from https://jdashg.github.io/misc/colors/from-coeffs.html
+// The matrix is stored in column-major.
+const mat3 RgbFromYuv_Rec601 = mat3(
+ 1.00000, 1.00000, 1.00000,
+ 0.00000,-0.17207, 0.88600,
+ 0.70100,-0.35707, 0.00000
+);
+const mat3 RgbFromYuv_Rec709 = mat3(
+ 1.00000, 1.00000, 1.00000,
+ 0.00000,-0.09366, 0.92780,
+ 0.78740,-0.23406, 0.00000
+);
+const mat3 RgbFromYuv_Rec2020 = mat3(
+ 1.00000, 1.00000, 1.00000,
+ 0.00000,-0.08228, 0.94070,
+ 0.73730,-0.28568, 0.00000
+);
+
+// The matrix is stored in column-major.
+// Identity is stored as GBR
+const mat3 RgbFromYuv_GbrIdentity = mat3(
+ 0.0 , 1.0, 0.0,
+ 0.0 , 0.0, 1.0,
+ 1.0 , 0.0, 0.0
+);
+
+// -
+
+struct YuvPrimitive {
+ int channel_bit_depth;
+ int color_space;
+ int yuv_format;
+};
+
+struct YuvColorSamplingInfo {
+ mat3 rgb_from_yuv;
+ vec4 packed_zero_one_vals;
+};
+
+struct YuvColorMatrixInfo {
+ vec3 ycbcr_bias;
+ mat3 rgb_from_debiased_ycbrc;
+};
+
+// -
+
+vec4 yuv_channel_zero_one_identity(int bit_depth, float channel_max) {
+ float all_ones_normalized = float((1 << bit_depth) - 1) / channel_max;
+ return vec4(0.0, 0.0, all_ones_normalized, all_ones_normalized);
+}
+
+vec4 yuv_channel_zero_one_narrow_range(int bit_depth, float channel_max) {
+ // Note: 512/1023 != 128/255
+ ivec4 zero_one_ints = ivec4(16, 128, 235, 240) << (bit_depth - 8);
+ return vec4(zero_one_ints) / channel_max;
+}
+
+vec4 yuv_channel_zero_one_full_range(int bit_depth, float channel_max) {
+ vec4 narrow = yuv_channel_zero_one_narrow_range(bit_depth, channel_max);
+ vec4 identity = yuv_channel_zero_one_identity(bit_depth, channel_max);
+ return vec4(0.0, narrow.y, identity.z, identity.w);
+}
+
+YuvColorSamplingInfo get_yuv_color_info(YuvPrimitive prim) {
+ float channel_max = 255.0;
+ if (prim.channel_bit_depth > 8) {
+ if (prim.yuv_format == YUV_FORMAT_P010) {
+ // This is an MSB format.
+ channel_max = float((1 << prim.channel_bit_depth) - 1);
+ } else {
+ // For >8bpc, we get the low bits, not the high bits:
+ // 10bpc(1.0): 0b0000_0011_1111_1111
+ channel_max = 65535.0;
+ }
+ }
+ if (prim.color_space == YUV_COLOR_SPACE_REC601_NARROW) {
+ return YuvColorSamplingInfo(RgbFromYuv_Rec601,
+ yuv_channel_zero_one_narrow_range(prim.channel_bit_depth, channel_max));
+ } else if (prim.color_space == YUV_COLOR_SPACE_REC601_FULL) {
+ return YuvColorSamplingInfo(RgbFromYuv_Rec601,
+ yuv_channel_zero_one_full_range(prim.channel_bit_depth, channel_max));
+
+ } else if (prim.color_space == YUV_COLOR_SPACE_REC709_NARROW) {
+ return YuvColorSamplingInfo(RgbFromYuv_Rec709,
+ yuv_channel_zero_one_narrow_range(prim.channel_bit_depth, channel_max));
+ } else if (prim.color_space == YUV_COLOR_SPACE_REC709_FULL) {
+ return YuvColorSamplingInfo(RgbFromYuv_Rec709,
+ yuv_channel_zero_one_full_range(prim.channel_bit_depth, channel_max));
+
+ } else if (prim.color_space == YUV_COLOR_SPACE_REC2020_NARROW) {
+ return YuvColorSamplingInfo(RgbFromYuv_Rec2020,
+ yuv_channel_zero_one_narrow_range(prim.channel_bit_depth, channel_max));
+ } else if (prim.color_space == YUV_COLOR_SPACE_REC2020_FULL) {
+ return YuvColorSamplingInfo(RgbFromYuv_Rec2020,
+ yuv_channel_zero_one_full_range(prim.channel_bit_depth, channel_max));
+
+ } else {
+ // Identity
+ return YuvColorSamplingInfo(RgbFromYuv_GbrIdentity,
+ yuv_channel_zero_one_identity(prim.channel_bit_depth, channel_max));
+ }
+}
+
+YuvColorMatrixInfo get_rgb_from_ycbcr_info(YuvPrimitive prim) {
+ YuvColorSamplingInfo info = get_yuv_color_info(prim);
+
+ vec2 zero = info.packed_zero_one_vals.xy;
+ vec2 one = info.packed_zero_one_vals.zw;
+ // Such that yuv_value = (ycbcr_sample - zero) / (one - zero)
+ vec2 scale = 1.0 / (one - zero);
+
+ YuvColorMatrixInfo mat_info;
+ mat_info.ycbcr_bias = zero.xyy;
+ mat3 yuv_from_debiased_ycbcr = mat3(scale.x, 0.0, 0.0,
+ 0.0, scale.y, 0.0,
+ 0.0, 0.0, scale.y);
+ mat_info.rgb_from_debiased_ycbrc = info.rgb_from_yuv * yuv_from_debiased_ycbcr;
+ return mat_info;
+}
+
+void write_uv_rect(
+ vec2 uv0,
+ vec2 uv1,
+ vec2 f,
+ vec2 texture_size,
+ out vec2 uv,
+ out vec4 uv_bounds
+) {
+ uv = mix(uv0, uv1, f);
+
+ uv_bounds = vec4(uv0 + vec2(0.5), uv1 - vec2(0.5));
+
+ #ifndef WR_FEATURE_TEXTURE_RECT
+ uv /= texture_size;
+ uv_bounds /= texture_size.xyxy;
+ #endif
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+
+vec4 sample_yuv(
+ int format,
+ YUV_PRECISION vec3 ycbcr_bias,
+ YUV_PRECISION mat3 rgb_from_debiased_ycbrc,
+ vec2 in_uv_y,
+ vec2 in_uv_u,
+ vec2 in_uv_v,
+ vec4 uv_bounds_y,
+ vec4 uv_bounds_u,
+ vec4 uv_bounds_v
+) {
+ YUV_PRECISION vec3 ycbcr_sample;
+
+ switch (format) {
+ case YUV_FORMAT_PLANAR:
+ {
+ // The yuv_planar format should have this third texture coordinate.
+ vec2 uv_y = clamp(in_uv_y, uv_bounds_y.xy, uv_bounds_y.zw);
+ vec2 uv_u = clamp(in_uv_u, uv_bounds_u.xy, uv_bounds_u.zw);
+ vec2 uv_v = clamp(in_uv_v, uv_bounds_v.xy, uv_bounds_v.zw);
+ ycbcr_sample.x = TEX_SAMPLE(sColor0, uv_y).r;
+ ycbcr_sample.y = TEX_SAMPLE(sColor1, uv_u).r;
+ ycbcr_sample.z = TEX_SAMPLE(sColor2, uv_v).r;
+ }
+ break;
+
+ case YUV_FORMAT_NV12:
+ case YUV_FORMAT_P010:
+ {
+ vec2 uv_y = clamp(in_uv_y, uv_bounds_y.xy, uv_bounds_y.zw);
+ vec2 uv_uv = clamp(in_uv_u, uv_bounds_u.xy, uv_bounds_u.zw);
+ ycbcr_sample.x = TEX_SAMPLE(sColor0, uv_y).r;
+ ycbcr_sample.yz = TEX_SAMPLE(sColor1, uv_uv).rg;
+ }
+ break;
+
+ case YUV_FORMAT_INTERLEAVED:
+ {
+ // "The Y, Cb and Cr color channels within the 422 data are mapped into
+ // the existing green, blue and red color channels."
+ // https://www.khronos.org/registry/OpenGL/extensions/APPLE/APPLE_rgb_422.txt
+ vec2 uv_y = clamp(in_uv_y, uv_bounds_y.xy, uv_bounds_y.zw);
+ ycbcr_sample = TEX_SAMPLE(sColor0, uv_y).gbr;
+ }
+ break;
+
+ default:
+ ycbcr_sample = vec3(0.0);
+ break;
+ }
+ //if (true) return vec4(ycbcr_sample, 1.0);
+
+ // See the YuvColorMatrix definition for an explanation of where the constants come from.
+ YUV_PRECISION vec3 rgb = rgb_from_debiased_ycbrc * (ycbcr_sample - ycbcr_bias);
+
+ #if defined(WR_FEATURE_ALPHA_PASS) && defined(SWGL_CLIP_MASK)
+ // Avoid out-of-range RGB values that can mess with blending. These occur due to invalid
+ // YUV values outside the mappable space that never the less can be generated.
+ rgb = clamp(rgb, 0.0, 1.0);
+ #endif
+ return vec4(rgb, 1.0);
+}
+#endif
diff --git a/gfx/wr/webrender/src/api_resources.rs b/gfx/wr/webrender/src/api_resources.rs
new file mode 100644
index 0000000000..d0cece19f5
--- /dev/null
+++ b/gfx/wr/webrender/src/api_resources.rs
@@ -0,0 +1,305 @@
+/* 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::api::{BlobImageKey, ImageDescriptor, DirtyRect, TileSize, DebugFlags};
+use crate::api::{BlobImageHandler, AsyncBlobImageRasterizer, BlobImageData, BlobImageParams};
+use crate::api::{BlobImageRequest, BlobImageDescriptor, FontTemplate};
+use crate::api::units::*;
+use glyph_rasterizer::{SharedFontResources, BaseFontInstance};
+use crate::render_api::{ResourceUpdate, TransactionMsg, AddFont};
+use crate::image_tiling::*;
+use crate::profiler;
+
+use std::collections::HashMap;
+use std::mem;
+use std::sync::Arc;
+
+/// We use this to generate the async blob rendering requests.
+struct BlobImageTemplate {
+ descriptor: ImageDescriptor,
+ tile_size: TileSize,
+ dirty_rect: BlobDirtyRect,
+ /// See ImageResource::visible_rect.
+ visible_rect: DeviceIntRect,
+ // If the active rect of the blob changes, this represents the
+ // range of tiles that remain valid. This must be taken into
+ // account in addition to the valid rect when submitting blob
+ // rasterization requests.
+ // `None` means the bounds have not changed (tiles are still valid).
+ // `Some(TileRange::zero())` means all of the tiles are invalid.
+ valid_tiles_after_bounds_change: Option<TileRange>,
+}
+
+pub struct ApiResources {
+ blob_image_templates: HashMap<BlobImageKey, BlobImageTemplate>,
+ pub blob_image_handler: Option<Box<dyn BlobImageHandler>>,
+ fonts: SharedFontResources,
+ // This should only be true for CI or debugging purposes. If true,
+ // we'll restrict the size of blob images as a result effectively
+ // rendering them incorrectly.
+ debug_restrict_blob_size: bool,
+}
+
+impl ApiResources {
+ pub fn new(
+ blob_image_handler: Option<Box<dyn BlobImageHandler>>,
+ fonts: SharedFontResources,
+ ) -> Self {
+ ApiResources {
+ blob_image_templates: HashMap::new(),
+ blob_image_handler,
+ fonts,
+ debug_restrict_blob_size: false,
+ }
+ }
+
+ pub fn get_fonts(&self) -> SharedFontResources {
+ self.fonts.clone()
+ }
+
+ pub fn set_debug_flags(&mut self, flags: DebugFlags) {
+ self.debug_restrict_blob_size = flags.contains(DebugFlags::RESTRICT_BLOB_SIZE);
+ }
+
+ pub fn adjust_blob_visible_rect(&self, rect: &mut DeviceIntRect, size: Option<&mut DeviceIntSize>) {
+ if self.debug_restrict_blob_size {
+ rect.max.x = rect.max.x.min(rect.min.x + 2048);
+ rect.max.y = rect.max.y.min(rect.min.y + 2048);
+ if let Some(size) = size {
+ size.width = size.width.min(2048);
+ size.height = size.height.min(2048);
+ }
+ }
+ }
+
+ pub fn update(&mut self, transaction: &mut TransactionMsg) {
+ let mut blobs_to_rasterize = Vec::new();
+ for update in &mut transaction.resource_updates {
+ match *update {
+ ResourceUpdate::AddBlobImage(ref mut img) => {
+ self.adjust_blob_visible_rect(&mut img.visible_rect, Some(&mut img.descriptor.size));
+ self.blob_image_handler
+ .as_mut()
+ .expect("no blob image handler")
+ .add(img.key, Arc::clone(&img.data), &img.visible_rect, img.tile_size);
+
+ self.blob_image_templates.insert(
+ img.key,
+ BlobImageTemplate {
+ descriptor: img.descriptor,
+ tile_size: img.tile_size,
+ dirty_rect: DirtyRect::All,
+ valid_tiles_after_bounds_change: None,
+ visible_rect: img.visible_rect,
+ },
+ );
+ blobs_to_rasterize.push(img.key);
+ }
+ ResourceUpdate::UpdateBlobImage(ref mut img) => {
+ debug_assert_eq!(img.visible_rect.size(), img.descriptor.size);
+ self.adjust_blob_visible_rect(&mut img.visible_rect, Some(&mut img.descriptor.size));
+ self.update_blob_image(
+ img.key,
+ Some(&img.descriptor),
+ Some(&img.dirty_rect),
+ Some(Arc::clone(&img.data)),
+ &img.visible_rect,
+ );
+ blobs_to_rasterize.push(img.key);
+ }
+ ResourceUpdate::DeleteBlobImage(key) => {
+ transaction.use_scene_builder_thread = true;
+ self.blob_image_templates.remove(&key);
+ if let Some(ref mut handler) = self.blob_image_handler {
+ handler.delete(key);
+ }
+ }
+ ResourceUpdate::SetBlobImageVisibleArea(ref key, ref mut area) => {
+ self.adjust_blob_visible_rect(area, None);
+ self.update_blob_image(*key, None, None, None, &area);
+ blobs_to_rasterize.push(*key);
+ }
+ ResourceUpdate::AddFont(ref font) => {
+ let (key, template) = match font {
+ AddFont::Raw(key, bytes, index) => {
+ (key, FontTemplate::Raw(Arc::clone(bytes), *index))
+ }
+ AddFont::Native(key, native_font_handle) => {
+ (key, FontTemplate::Native(native_font_handle.clone()))
+ }
+ };
+ if let Some(shared_key) = self.fonts.font_keys.add_key(key, &template) {
+ self.fonts.templates.add_font(shared_key, template);
+ }
+ }
+ ResourceUpdate::AddFontInstance(ref mut instance) => {
+ let shared_font_key = self.fonts.font_keys.map_key(&instance.font_key);
+ assert!(self.fonts.templates.has_font(&shared_font_key));
+ // AddFontInstance will only be processed here, not in the resource cache, so it
+ // is safe to take the options rather than clone them.
+ let base = BaseFontInstance::new(
+ instance.key,
+ shared_font_key,
+ instance.glyph_size,
+ mem::take(&mut instance.options),
+ mem::take(&mut instance.platform_options),
+ mem::take(&mut instance.variations),
+ );
+ if let Some(shared_instance) = self.fonts.instance_keys.add_key(base) {
+ self.fonts.instances.add_font_instance(shared_instance);
+ }
+ }
+ ResourceUpdate::DeleteFont(_key) => {
+ transaction.use_scene_builder_thread = true;
+ }
+ ResourceUpdate::DeleteFontInstance(_key) => {
+ transaction.use_scene_builder_thread = true;
+ // We will delete from the shared font instance map in the resource cache
+ // after scene swap.
+ }
+ ResourceUpdate::DeleteImage(..) => {
+ transaction.use_scene_builder_thread = true;
+ }
+ _ => {}
+ }
+ }
+
+ let (rasterizer, requests) = self.create_blob_scene_builder_requests(&blobs_to_rasterize);
+ transaction.profile.set(profiler::RASTERIZED_BLOBS, blobs_to_rasterize.len());
+ transaction.profile.set(profiler::RASTERIZED_BLOB_TILES, requests.len());
+ transaction.use_scene_builder_thread |= !requests.is_empty();
+ transaction.use_scene_builder_thread |= !transaction.scene_ops.is_empty();
+ transaction.blob_rasterizer = rasterizer;
+ transaction.blob_requests = requests;
+ }
+
+ pub fn enable_multithreading(&mut self, enable: bool) {
+ if let Some(ref mut handler) = self.blob_image_handler {
+ handler.enable_multithreading(enable);
+ }
+ }
+
+ fn update_blob_image(
+ &mut self,
+ key: BlobImageKey,
+ descriptor: Option<&ImageDescriptor>,
+ dirty_rect: Option<&BlobDirtyRect>,
+ data: Option<Arc<BlobImageData>>,
+ visible_rect: &DeviceIntRect,
+ ) {
+ if let Some(data) = data {
+ let dirty_rect = dirty_rect.expect("no dirty rect");
+ self.blob_image_handler
+ .as_mut()
+ .expect("no blob image handler")
+ .update(key, data, visible_rect, dirty_rect);
+ }
+
+ let image = self.blob_image_templates
+ .get_mut(&key)
+ .expect("Attempt to update non-existent blob image");
+
+ let mut valid_tiles_after_bounds_change = compute_valid_tiles_if_bounds_change(
+ &image.visible_rect,
+ visible_rect,
+ image.tile_size,
+ );
+
+ match (image.valid_tiles_after_bounds_change, valid_tiles_after_bounds_change) {
+ (Some(old), Some(ref mut new)) => {
+ *new = new.intersection(&old).unwrap_or_else(TileRange::zero);
+ }
+ (Some(old), None) => {
+ valid_tiles_after_bounds_change = Some(old);
+ }
+ _ => {}
+ }
+
+ let blob_size = visible_rect.size();
+
+ if let Some(descriptor) = descriptor {
+ image.descriptor = *descriptor;
+ } else {
+ // make sure the descriptor size matches the visible rect.
+ // This might not be necessary but let's stay on the safe side.
+ image.descriptor.size = blob_size;
+ }
+
+ if let Some(dirty_rect) = dirty_rect {
+ image.dirty_rect = image.dirty_rect.union(dirty_rect);
+ }
+
+ image.valid_tiles_after_bounds_change = valid_tiles_after_bounds_change;
+ image.visible_rect = *visible_rect;
+ }
+
+ pub fn create_blob_scene_builder_requests(
+ &mut self,
+ keys: &[BlobImageKey]
+ ) -> (Option<Box<dyn AsyncBlobImageRasterizer>>, Vec<BlobImageParams>) {
+ if self.blob_image_handler.is_none() || keys.is_empty() {
+ return (None, Vec::new());
+ }
+
+ let mut blob_request_params = Vec::new();
+ for key in keys {
+ let template = self.blob_image_templates.get_mut(key)
+ .expect("no blob image template");
+
+ // If we know that only a portion of the blob image is in the viewport,
+ // only request these visible tiles since blob images can be huge.
+ let tiles = compute_tile_range(
+ &template.visible_rect,
+ template.tile_size,
+ );
+
+ // Don't request tiles that weren't invalidated.
+ let dirty_tiles = match template.dirty_rect {
+ DirtyRect::Partial(dirty_rect) => {
+ compute_tile_range(
+ &dirty_rect.cast_unit(),
+ template.tile_size,
+ )
+ }
+ DirtyRect::All => tiles,
+ };
+
+ for_each_tile_in_range(&tiles, |tile| {
+ let still_valid = template.valid_tiles_after_bounds_change
+ .map(|valid_tiles| valid_tiles.contains(tile))
+ .unwrap_or(true);
+
+ if still_valid && !dirty_tiles.contains(tile) {
+ return;
+ }
+
+ let descriptor = BlobImageDescriptor {
+ rect: compute_tile_rect(
+ &template.visible_rect,
+ template.tile_size,
+ tile,
+ ).cast_unit(),
+ format: template.descriptor.format,
+ };
+
+ assert!(descriptor.rect.width() > 0 && descriptor.rect.height() > 0);
+ blob_request_params.push(
+ BlobImageParams {
+ request: BlobImageRequest { key: *key, tile },
+ descriptor,
+ dirty_rect: DirtyRect::All,
+ }
+ );
+ });
+
+ template.dirty_rect = DirtyRect::empty();
+ template.valid_tiles_after_bounds_change = None;
+ }
+
+ let handler = self.blob_image_handler.as_mut()
+ .expect("no blob image handler");
+ handler.prepare_resources(&self.fonts, &blob_request_params);
+ (Some(handler.create_blob_rasterizer()), blob_request_params)
+ }
+}
diff --git a/gfx/wr/webrender/src/batch.rs b/gfx/wr/webrender/src/batch.rs
new file mode 100644
index 0000000000..d2be9ed3b8
--- /dev/null
+++ b/gfx/wr/webrender/src/batch.rs
@@ -0,0 +1,3971 @@
+/* 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 api::{AlphaType, ClipMode, ImageRendering, ImageBufferKind};
+use api::{FontInstanceFlags, YuvColorSpace, YuvFormat, ColorDepth, ColorRange, PremultipliedColorF};
+use api::units::*;
+use crate::clip::{ClipNodeFlags, ClipNodeRange, ClipItemKind, ClipStore};
+use crate::command_buffer::{PrimitiveCommand, QuadFlags};
+use crate::spatial_tree::{SpatialTree, SpatialNodeIndex, CoordinateSystemId};
+use glyph_rasterizer::{GlyphFormat, SubpixelDirection};
+use crate::gpu_cache::{GpuBlockData, GpuCache, GpuCacheAddress};
+use crate::gpu_types::{BrushFlags, BrushInstance, PrimitiveHeaders, ZBufferId, ZBufferIdGenerator};
+use crate::gpu_types::{SplitCompositeInstance, QuadInstance};
+use crate::gpu_types::{PrimitiveInstanceData, RasterizationSpace, GlyphInstance};
+use crate::gpu_types::{PrimitiveHeader, PrimitiveHeaderIndex, TransformPaletteId, TransformPalette};
+use crate::gpu_types::{ImageBrushData, get_shader_opacity, BoxShadowData};
+use crate::gpu_types::{ClipMaskInstanceCommon, ClipMaskInstanceImage, ClipMaskInstanceRect, ClipMaskInstanceBoxShadow};
+use crate::internal_types::{FastHashMap, Swizzle, TextureSource, Filter};
+use crate::picture::{Picture3DContext, PictureCompositeMode, calculate_screen_uv};
+use crate::prim_store::{PrimitiveInstanceKind, ClipData};
+use crate::prim_store::{PrimitiveInstance, PrimitiveOpacity, SegmentInstanceIndex};
+use crate::prim_store::{BrushSegment, ClipMaskKind, ClipTaskIndex};
+use crate::prim_store::{VECS_PER_SEGMENT, PrimitiveInstanceIndex};
+use crate::render_target::RenderTargetContext;
+use crate::render_task_graph::{RenderTaskId, RenderTaskGraph};
+use crate::render_task::{RenderTaskAddress, RenderTaskKind};
+use crate::renderer::{BlendMode, ShaderColorMode};
+use crate::renderer::{MAX_VERTEX_TEXTURE_WIDTH, GpuBufferBuilder, GpuBufferAddress};
+use crate::resource_cache::{GlyphFetchResult, ImageProperties, ImageRequest};
+use crate::space::SpaceMapper;
+use crate::visibility::{PrimitiveVisibilityFlags, VisibilityState};
+use smallvec::SmallVec;
+use std::{f32, i32, usize};
+use crate::util::{project_rect, MaxRect, MatrixHelpers, TransformedRectKind, ScaleOffset};
+use crate::segment::EdgeAaSegmentMask;
+
+// Special sentinel value recognized by the shader. It is considered to be
+// a dummy task that doesn't mask out anything.
+const OPAQUE_TASK_ADDRESS: RenderTaskAddress = RenderTaskAddress(0x7fff);
+
+/// Used to signal there are no segments provided with this primitive.
+pub const INVALID_SEGMENT_INDEX: i32 = 0xffff;
+
+/// Size in device pixels for tiles that clip masks are drawn in.
+const CLIP_RECTANGLE_TILE_SIZE: i32 = 128;
+
+/// The minimum size of a clip mask before trying to draw in tiles.
+const CLIP_RECTANGLE_AREA_THRESHOLD: f32 = (CLIP_RECTANGLE_TILE_SIZE * CLIP_RECTANGLE_TILE_SIZE * 4) as f32;
+
+#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum BrushBatchKind {
+ Solid,
+ Image(ImageBufferKind),
+ Blend,
+ MixBlend {
+ task_id: RenderTaskId,
+ backdrop_id: RenderTaskId,
+ },
+ YuvImage(ImageBufferKind, YuvFormat, ColorDepth, YuvColorSpace, ColorRange),
+ LinearGradient,
+ Opacity,
+}
+
+#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum BatchKind {
+ SplitComposite,
+ TextRun(GlyphFormat),
+ Brush(BrushBatchKind),
+ Primitive,
+}
+
+/// Input textures for a primitive, without consideration of clip mask
+#[derive(Copy, Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct TextureSet {
+ pub colors: [TextureSource; 3],
+}
+
+impl TextureSet {
+ const UNTEXTURED: TextureSet = TextureSet {
+ colors: [
+ TextureSource::Invalid,
+ TextureSource::Invalid,
+ TextureSource::Invalid,
+ ],
+ };
+
+ /// A textured primitive
+ fn prim_textured(
+ color: TextureSource,
+ ) -> Self {
+ TextureSet {
+ colors: [
+ color,
+ TextureSource::Invalid,
+ TextureSource::Invalid,
+ ],
+ }
+ }
+
+ fn is_compatible_with(&self, other: &TextureSet) -> bool {
+ self.colors[0].is_compatible(&other.colors[0]) &&
+ self.colors[1].is_compatible(&other.colors[1]) &&
+ self.colors[2].is_compatible(&other.colors[2])
+ }
+}
+
+impl TextureSource {
+ fn combine(&self, other: TextureSource) -> TextureSource {
+ if other == TextureSource::Invalid {
+ *self
+ } else {
+ other
+ }
+ }
+}
+
+/// Optional textures that can be used as a source in the shaders.
+/// Textures that are not used by the batch are equal to TextureId::invalid().
+#[derive(Copy, Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BatchTextures {
+ pub input: TextureSet,
+ pub clip_mask: TextureSource,
+}
+
+impl BatchTextures {
+ /// An empty batch textures (no binding slots set)
+ pub fn empty() -> BatchTextures {
+ BatchTextures {
+ input: TextureSet::UNTEXTURED,
+ clip_mask: TextureSource::Invalid,
+ }
+ }
+
+ /// A textured primitive with optional clip mask
+ pub fn prim_textured(
+ color: TextureSource,
+ clip_mask: TextureSource,
+ ) -> BatchTextures {
+ BatchTextures {
+ input: TextureSet::prim_textured(color),
+ clip_mask,
+ }
+ }
+
+ /// An untextured primitive with optional clip mask
+ pub fn prim_untextured(
+ clip_mask: TextureSource,
+ ) -> BatchTextures {
+ BatchTextures {
+ input: TextureSet::UNTEXTURED,
+ clip_mask,
+ }
+ }
+
+ /// A composite style effect with single input texture
+ pub fn composite_rgb(
+ texture: TextureSource,
+ ) -> BatchTextures {
+ BatchTextures {
+ input: TextureSet {
+ colors: [
+ texture,
+ TextureSource::Invalid,
+ TextureSource::Invalid,
+ ],
+ },
+ clip_mask: TextureSource::Invalid,
+ }
+ }
+
+ /// A composite style effect with up to 3 input textures
+ pub fn composite_yuv(
+ color0: TextureSource,
+ color1: TextureSource,
+ color2: TextureSource,
+ ) -> BatchTextures {
+ BatchTextures {
+ input: TextureSet {
+ colors: [color0, color1, color2],
+ },
+ clip_mask: TextureSource::Invalid,
+ }
+ }
+
+ pub fn is_compatible_with(&self, other: &BatchTextures) -> bool {
+ if !self.clip_mask.is_compatible(&other.clip_mask) {
+ return false;
+ }
+
+ self.input.is_compatible_with(&other.input)
+ }
+
+ pub fn combine_textures(&self, other: BatchTextures) -> Option<BatchTextures> {
+ if !self.is_compatible_with(&other) {
+ return None;
+ }
+
+ let mut new_textures = BatchTextures::empty();
+
+ new_textures.clip_mask = self.clip_mask.combine(other.clip_mask);
+
+ for i in 0 .. 3 {
+ new_textures.input.colors[i] = self.input.colors[i].combine(other.input.colors[i]);
+ }
+
+ Some(new_textures)
+ }
+
+ fn merge(&mut self, other: &BatchTextures) {
+ self.clip_mask = self.clip_mask.combine(other.clip_mask);
+
+ for (s, o) in self.input.colors.iter_mut().zip(other.input.colors.iter()) {
+ *s = s.combine(*o);
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BatchKey {
+ pub kind: BatchKind,
+ pub blend_mode: BlendMode,
+ pub textures: BatchTextures,
+}
+
+impl BatchKey {
+ pub fn new(kind: BatchKind, blend_mode: BlendMode, textures: BatchTextures) -> Self {
+ BatchKey {
+ kind,
+ blend_mode,
+ textures,
+ }
+ }
+
+ pub fn is_compatible_with(&self, other: &BatchKey) -> bool {
+ self.kind == other.kind && self.blend_mode == other.blend_mode && self.textures.is_compatible_with(&other.textures)
+ }
+}
+
+pub struct BatchRects {
+ /// Union of all of the batch's item rects.
+ ///
+ /// Very often we can skip iterating over item rects by testing against
+ /// this one first.
+ batch: PictureRect,
+ /// When the batch rectangle above isn't a good enough approximation, we
+ /// store per item rects.
+ items: Option<Vec<PictureRect>>,
+}
+
+impl BatchRects {
+ fn new() -> Self {
+ BatchRects {
+ batch: PictureRect::zero(),
+ items: None,
+ }
+ }
+
+ #[inline]
+ fn add_rect(&mut self, rect: &PictureRect) {
+ let union = self.batch.union(rect);
+ // If we have already started storing per-item rects, continue doing so.
+ // Otherwise, check whether only storing the batch rect is a good enough
+ // approximation.
+ if let Some(items) = &mut self.items {
+ items.push(*rect);
+ } else if self.batch.area() + rect.area() < union.area() {
+ let mut items = Vec::with_capacity(16);
+ items.push(self.batch);
+ items.push(*rect);
+ self.items = Some(items);
+ }
+
+ self.batch = union;
+ }
+
+ #[inline]
+ fn intersects(&mut self, rect: &PictureRect) -> bool {
+ if !self.batch.intersects(rect) {
+ return false;
+ }
+
+ if let Some(items) = &self.items {
+ items.iter().any(|item| item.intersects(rect))
+ } else {
+ // If we don't have per-item rects it means the batch rect is a good
+ // enough approximation and we didn't bother storing per-rect items.
+ true
+ }
+ }
+}
+
+
+pub struct AlphaBatchList {
+ pub batches: Vec<PrimitiveBatch>,
+ pub batch_rects: Vec<BatchRects>,
+ current_batch_index: usize,
+ current_z_id: ZBufferId,
+ break_advanced_blend_batches: bool,
+}
+
+impl AlphaBatchList {
+ fn new(break_advanced_blend_batches: bool, preallocate: usize) -> Self {
+ AlphaBatchList {
+ batches: Vec::with_capacity(preallocate),
+ batch_rects: Vec::with_capacity(preallocate),
+ current_z_id: ZBufferId::invalid(),
+ current_batch_index: usize::MAX,
+ break_advanced_blend_batches,
+ }
+ }
+
+ /// Clear all current batches in this list. This is typically used
+ /// when a primitive is encountered that occludes all previous
+ /// content in this batch list.
+ fn clear(&mut self) {
+ self.current_batch_index = usize::MAX;
+ self.current_z_id = ZBufferId::invalid();
+ self.batches.clear();
+ self.batch_rects.clear();
+ }
+
+ pub fn set_params_and_get_batch(
+ &mut self,
+ key: BatchKey,
+ features: BatchFeatures,
+ // The bounding box of everything at this Z plane. We expect potentially
+ // multiple primitive segments coming with the same `z_id`.
+ z_bounding_rect: &PictureRect,
+ z_id: ZBufferId,
+ ) -> &mut Vec<PrimitiveInstanceData> {
+ if z_id != self.current_z_id ||
+ self.current_batch_index == usize::MAX ||
+ !self.batches[self.current_batch_index].key.is_compatible_with(&key)
+ {
+ let mut selected_batch_index = None;
+
+ match key.blend_mode {
+ BlendMode::SubpixelWithBgColor => {
+ for (batch_index, batch) in self.batches.iter().enumerate().rev() {
+ // Some subpixel batches are drawn in two passes. Because of this, we need
+ // to check for overlaps with every batch (which is a bit different
+ // than the normal batching below).
+ if self.batch_rects[batch_index].intersects(z_bounding_rect) {
+ break;
+ }
+
+ if batch.key.is_compatible_with(&key) {
+ selected_batch_index = Some(batch_index);
+ break;
+ }
+ }
+ }
+ BlendMode::Advanced(_) if self.break_advanced_blend_batches => {
+ // don't try to find a batch
+ }
+ _ => {
+ for (batch_index, batch) in self.batches.iter().enumerate().rev() {
+ // For normal batches, we only need to check for overlaps for batches
+ // other than the first batch we consider. If the first batch
+ // is compatible, then we know there isn't any potential overlap
+ // issues to worry about.
+ if batch.key.is_compatible_with(&key) {
+ selected_batch_index = Some(batch_index);
+ break;
+ }
+
+ // check for intersections
+ if self.batch_rects[batch_index].intersects(z_bounding_rect) {
+ break;
+ }
+ }
+ }
+ }
+
+ if selected_batch_index.is_none() {
+ // Text runs tend to have a lot of instances per batch, causing a lot of reallocation
+ // churn as items are added one by one, so we give it a head start. Ideally we'd start
+ // with a larger number, closer to 1k but in some bad cases with lots of batch break
+ // we would be wasting a lot of memory.
+ // Generally it is safe to preallocate small-ish values for other batch kinds because
+ // the items are small and there are no zero-sized batches so there will always be
+ // at least one allocation.
+ let prealloc = match key.kind {
+ BatchKind::TextRun(..) => 128,
+ _ => 16,
+ };
+ let mut new_batch = PrimitiveBatch::new(key);
+ new_batch.instances.reserve(prealloc);
+ selected_batch_index = Some(self.batches.len());
+ self.batches.push(new_batch);
+ self.batch_rects.push(BatchRects::new());
+ }
+
+ self.current_batch_index = selected_batch_index.unwrap();
+ self.batch_rects[self.current_batch_index].add_rect(z_bounding_rect);
+ self.current_z_id = z_id;
+ }
+
+ let batch = &mut self.batches[self.current_batch_index];
+ batch.features |= features;
+ batch.key.textures.merge(&key.textures);
+
+ &mut batch.instances
+ }
+}
+
+pub struct OpaqueBatchList {
+ pub pixel_area_threshold_for_new_batch: f32,
+ pub batches: Vec<PrimitiveBatch>,
+ pub current_batch_index: usize,
+ lookback_count: usize,
+}
+
+impl OpaqueBatchList {
+ fn new(pixel_area_threshold_for_new_batch: f32, lookback_count: usize) -> Self {
+ OpaqueBatchList {
+ batches: Vec::new(),
+ pixel_area_threshold_for_new_batch,
+ current_batch_index: usize::MAX,
+ lookback_count,
+ }
+ }
+
+ /// Clear all current batches in this list. This is typically used
+ /// when a primitive is encountered that occludes all previous
+ /// content in this batch list.
+ fn clear(&mut self) {
+ self.current_batch_index = usize::MAX;
+ self.batches.clear();
+ }
+
+ pub fn set_params_and_get_batch(
+ &mut self,
+ key: BatchKey,
+ features: BatchFeatures,
+ // The bounding box of everything at the current Z, whatever it is. We expect potentially
+ // multiple primitive segments produced by a primitive, which we allow to check
+ // `current_batch_index` instead of iterating the batches.
+ z_bounding_rect: &PictureRect,
+ ) -> &mut Vec<PrimitiveInstanceData> {
+ if self.current_batch_index == usize::MAX ||
+ !self.batches[self.current_batch_index].key.is_compatible_with(&key) {
+ let mut selected_batch_index = None;
+ let item_area = z_bounding_rect.area();
+
+ // If the area of this primitive is larger than the given threshold,
+ // then it is large enough to warrant breaking a batch for. In this
+ // case we just see if it can be added to the existing batch or
+ // create a new one.
+ if item_area > self.pixel_area_threshold_for_new_batch {
+ if let Some(batch) = self.batches.last() {
+ if batch.key.is_compatible_with(&key) {
+ selected_batch_index = Some(self.batches.len() - 1);
+ }
+ }
+ } else {
+ // Otherwise, look back through a reasonable number of batches.
+ for (batch_index, batch) in self.batches.iter().enumerate().rev().take(self.lookback_count) {
+ if batch.key.is_compatible_with(&key) {
+ selected_batch_index = Some(batch_index);
+ break;
+ }
+ }
+ }
+
+ if selected_batch_index.is_none() {
+ let new_batch = PrimitiveBatch::new(key);
+ selected_batch_index = Some(self.batches.len());
+ self.batches.push(new_batch);
+ }
+
+ self.current_batch_index = selected_batch_index.unwrap();
+ }
+
+ let batch = &mut self.batches[self.current_batch_index];
+ batch.features |= features;
+ batch.key.textures.merge(&key.textures);
+
+ &mut batch.instances
+ }
+
+ fn finalize(&mut self) {
+ // Reverse the instance arrays in the opaque batches
+ // to get maximum z-buffer efficiency by drawing
+ // front-to-back.
+ // TODO(gw): Maybe we can change the batch code to
+ // build these in reverse and avoid having
+ // to reverse the instance array here.
+ for batch in &mut self.batches {
+ batch.instances.reverse();
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PrimitiveBatch {
+ pub key: BatchKey,
+ pub instances: Vec<PrimitiveInstanceData>,
+ pub features: BatchFeatures,
+}
+
+bitflags! {
+ /// Features of the batch that, if not requested, may allow a fast-path.
+ ///
+ /// Rather than breaking batches when primitives request different features,
+ /// we always request the minimum amount of features to satisfy all items in
+ /// the batch.
+ /// The goal is to let the renderer be optionally select more specialized
+ /// versions of a shader if the batch doesn't require code certain code paths.
+ /// Not all shaders necessarily implement all of these features.
+ #[cfg_attr(feature = "capture", derive(Serialize))]
+ #[cfg_attr(feature = "replay", derive(Deserialize))]
+ pub struct BatchFeatures: u8 {
+ const ALPHA_PASS = 1 << 0;
+ const ANTIALIASING = 1 << 1;
+ const REPETITION = 1 << 2;
+ /// Indicates a primitive in this batch may use a clip mask.
+ const CLIP_MASK = 1 << 3;
+ }
+}
+
+impl PrimitiveBatch {
+ fn new(key: BatchKey) -> PrimitiveBatch {
+ PrimitiveBatch {
+ key,
+ instances: Vec::new(),
+ features: BatchFeatures::empty(),
+ }
+ }
+
+ fn merge(&mut self, other: PrimitiveBatch) {
+ self.instances.extend(other.instances);
+ self.features |= other.features;
+ self.key.textures.merge(&other.key.textures);
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct AlphaBatchContainer {
+ pub opaque_batches: Vec<PrimitiveBatch>,
+ pub alpha_batches: Vec<PrimitiveBatch>,
+ /// The overall scissor rect for this render task, if one
+ /// is required.
+ pub task_scissor_rect: Option<DeviceIntRect>,
+ /// The rectangle of the owning render target that this
+ /// set of batches affects.
+ pub task_rect: DeviceIntRect,
+}
+
+impl AlphaBatchContainer {
+ pub fn new(
+ task_scissor_rect: Option<DeviceIntRect>,
+ ) -> AlphaBatchContainer {
+ AlphaBatchContainer {
+ opaque_batches: Vec::new(),
+ alpha_batches: Vec::new(),
+ task_scissor_rect,
+ task_rect: DeviceIntRect::zero(),
+ }
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.opaque_batches.is_empty() &&
+ self.alpha_batches.is_empty()
+ }
+
+ fn merge(&mut self, builder: AlphaBatchBuilder, task_rect: &DeviceIntRect) {
+ self.task_rect = self.task_rect.union(task_rect);
+
+ for other_batch in builder.opaque_batch_list.batches {
+ let batch_index = self.opaque_batches.iter().position(|batch| {
+ batch.key.is_compatible_with(&other_batch.key)
+ });
+
+ match batch_index {
+ Some(batch_index) => {
+ self.opaque_batches[batch_index].merge(other_batch);
+ }
+ None => {
+ self.opaque_batches.push(other_batch);
+ }
+ }
+ }
+
+ let mut min_batch_index = 0;
+
+ for other_batch in builder.alpha_batch_list.batches {
+ let batch_index = self.alpha_batches.iter().skip(min_batch_index).position(|batch| {
+ batch.key.is_compatible_with(&other_batch.key)
+ });
+
+ match batch_index {
+ Some(batch_index) => {
+ let index = batch_index + min_batch_index;
+ self.alpha_batches[index].merge(other_batch);
+ min_batch_index = index;
+ }
+ None => {
+ self.alpha_batches.push(other_batch);
+ min_batch_index = self.alpha_batches.len();
+ }
+ }
+ }
+ }
+}
+
+/// Each segment can optionally specify a per-segment
+/// texture set and one user data field.
+#[derive(Debug, Copy, Clone)]
+struct SegmentInstanceData {
+ textures: TextureSet,
+ specific_resource_address: i32,
+}
+
+/// Encapsulates the logic of building batches for items that are blended.
+pub struct AlphaBatchBuilder {
+ pub alpha_batch_list: AlphaBatchList,
+ pub opaque_batch_list: OpaqueBatchList,
+ pub render_task_id: RenderTaskId,
+ render_task_address: RenderTaskAddress,
+}
+
+impl AlphaBatchBuilder {
+ pub fn new(
+ screen_size: DeviceIntSize,
+ break_advanced_blend_batches: bool,
+ lookback_count: usize,
+ render_task_id: RenderTaskId,
+ render_task_address: RenderTaskAddress,
+ ) -> Self {
+ // The threshold for creating a new batch is
+ // one quarter the screen size.
+ let batch_area_threshold = (screen_size.width * screen_size.height) as f32 / 4.0;
+
+ AlphaBatchBuilder {
+ alpha_batch_list: AlphaBatchList::new(break_advanced_blend_batches, 128),
+ opaque_batch_list: OpaqueBatchList::new(batch_area_threshold, lookback_count),
+ render_task_id,
+ render_task_address,
+ }
+ }
+
+ /// Clear all current batches in this builder. This is typically used
+ /// when a primitive is encountered that occludes all previous
+ /// content in this batch list.
+ fn clear(&mut self) {
+ self.alpha_batch_list.clear();
+ self.opaque_batch_list.clear();
+ }
+
+ pub fn build(
+ mut self,
+ batch_containers: &mut Vec<AlphaBatchContainer>,
+ merged_batches: &mut AlphaBatchContainer,
+ task_rect: DeviceIntRect,
+ task_scissor_rect: Option<DeviceIntRect>,
+ ) {
+ self.opaque_batch_list.finalize();
+
+ if task_scissor_rect.is_none() {
+ merged_batches.merge(self, &task_rect);
+ } else {
+ batch_containers.push(AlphaBatchContainer {
+ alpha_batches: self.alpha_batch_list.batches,
+ opaque_batches: self.opaque_batch_list.batches,
+ task_scissor_rect,
+ task_rect,
+ });
+ }
+ }
+
+ pub fn push_single_instance(
+ &mut self,
+ key: BatchKey,
+ features: BatchFeatures,
+ bounding_rect: &PictureRect,
+ z_id: ZBufferId,
+ instance: PrimitiveInstanceData,
+ ) {
+ self.set_params_and_get_batch(key, features, bounding_rect, z_id)
+ .push(instance);
+ }
+
+ pub fn set_params_and_get_batch(
+ &mut self,
+ key: BatchKey,
+ features: BatchFeatures,
+ bounding_rect: &PictureRect,
+ z_id: ZBufferId,
+ ) -> &mut Vec<PrimitiveInstanceData> {
+ match key.blend_mode {
+ BlendMode::None => {
+ self.opaque_batch_list
+ .set_params_and_get_batch(key, features, bounding_rect)
+ }
+ BlendMode::Alpha |
+ BlendMode::PremultipliedAlpha |
+ BlendMode::PremultipliedDestOut |
+ BlendMode::SubpixelWithBgColor |
+ BlendMode::SubpixelDualSource |
+ BlendMode::Advanced(_) |
+ BlendMode::MultiplyDualSource |
+ BlendMode::Screen |
+ BlendMode::Exclusion |
+ BlendMode::PlusLighter => {
+ self.alpha_batch_list
+ .set_params_and_get_batch(key, features, bounding_rect, z_id)
+ }
+ }
+ }
+}
+
+/// Supports (recursively) adding a list of primitives and pictures to an alpha batch
+/// builder. In future, it will support multiple dirty regions / slices, allowing the
+/// contents of a picture to be spliced into multiple batch builders.
+pub struct BatchBuilder {
+ /// A temporary buffer that is used during glyph fetching, stored here
+ /// to reduce memory allocations.
+ glyph_fetch_buffer: Vec<GlyphFetchResult>,
+
+ batcher: AlphaBatchBuilder,
+}
+
+impl BatchBuilder {
+ pub fn new(batcher: AlphaBatchBuilder) -> Self {
+ BatchBuilder {
+ glyph_fetch_buffer: Vec::new(),
+ batcher,
+ }
+ }
+
+ pub fn finalize(self) -> AlphaBatchBuilder {
+ self.batcher
+ }
+
+ fn add_brush_instance_to_batches(
+ &mut self,
+ batch_key: BatchKey,
+ features: BatchFeatures,
+ bounding_rect: &PictureRect,
+ z_id: ZBufferId,
+ segment_index: i32,
+ edge_flags: EdgeAaSegmentMask,
+ clip_task_address: RenderTaskAddress,
+ brush_flags: BrushFlags,
+ prim_header_index: PrimitiveHeaderIndex,
+ resource_address: i32,
+ ) {
+ let render_task_address = self.batcher.render_task_address;
+
+ let instance = BrushInstance {
+ segment_index,
+ edge_flags,
+ clip_task_address,
+ render_task_address,
+ brush_flags,
+ prim_header_index,
+ resource_address,
+ };
+
+ self.batcher.push_single_instance(
+ batch_key,
+ features,
+ bounding_rect,
+ z_id,
+ PrimitiveInstanceData::from(instance),
+ );
+ }
+
+ fn add_split_composite_instance_to_batches(
+ &mut self,
+ batch_key: BatchKey,
+ features: BatchFeatures,
+ bounding_rect: &PictureRect,
+ z_id: ZBufferId,
+ prim_header_index: PrimitiveHeaderIndex,
+ polygons_address: i32,
+ ) {
+ let render_task_address = self.batcher.render_task_address;
+
+ self.batcher.push_single_instance(
+ batch_key,
+ features,
+ bounding_rect,
+ z_id,
+ PrimitiveInstanceData::from(SplitCompositeInstance {
+ prim_header_index,
+ render_task_address,
+ polygons_address,
+ z: z_id,
+ }),
+ );
+ }
+
+ /// Clear all current batchers. This is typically used when a primitive
+ /// is encountered that occludes all previous content in this batch list.
+ fn clear_batches(&mut self) {
+ self.batcher.clear();
+ }
+
+ /// Add a quad primitive to the batch list, appllying edge AA and tiling
+ /// segments as required.
+ fn add_quad_to_batch(
+ &mut self,
+ prim_instance_index: PrimitiveInstanceIndex,
+ transform_id: TransformPaletteId,
+ gpu_buffer_address: GpuBufferAddress,
+ quad_flags: QuadFlags,
+ edge_flags: EdgeAaSegmentMask,
+ segment_index: u8,
+ task_id: RenderTaskId,
+ z_generator: &mut ZBufferIdGenerator,
+ prim_instances: &[PrimitiveInstance],
+ render_tasks: &RenderTaskGraph,
+ ) {
+ let prim_instance = &prim_instances[prim_instance_index.0 as usize];
+ let prim_info = &prim_instance.vis;
+ let bounding_rect = &prim_info.clip_chain.pic_coverage_rect;
+ let z_id = z_generator.next();
+
+ add_quad_to_batch(
+ self.batcher.render_task_address,
+ transform_id,
+ gpu_buffer_address,
+ quad_flags,
+ edge_flags,
+ segment_index,
+ task_id,
+ z_id,
+ render_tasks,
+ |key, instance| {
+ let batch = self.batcher.set_params_and_get_batch(
+ key,
+ BatchFeatures::empty(),
+ bounding_rect,
+ z_id,
+ );
+
+ batch.push(instance);
+ }
+ );
+ }
+
+ // Adds a primitive to a batch.
+ // It can recursively call itself in some situations, for
+ // example if it encounters a picture where the items
+ // in that picture are being drawn into the same target.
+ pub fn add_prim_to_batch(
+ &mut self,
+ cmd: &PrimitiveCommand,
+ prim_spatial_node_index: SpatialNodeIndex,
+ ctx: &RenderTargetContext,
+ gpu_cache: &mut GpuCache,
+ render_tasks: &RenderTaskGraph,
+ prim_headers: &mut PrimitiveHeaders,
+ transforms: &mut TransformPalette,
+ root_spatial_node_index: SpatialNodeIndex,
+ surface_spatial_node_index: SpatialNodeIndex,
+ z_generator: &mut ZBufferIdGenerator,
+ prim_instances: &[PrimitiveInstance],
+ _gpu_buffer_builder: &mut GpuBufferBuilder,
+ segments: &[RenderTaskId],
+ ) {
+ let (prim_instance_index, extra_prim_gpu_address) = match cmd {
+ PrimitiveCommand::Simple { prim_instance_index } => {
+ (prim_instance_index, None)
+ }
+ PrimitiveCommand::Complex { prim_instance_index, gpu_address } => {
+ (prim_instance_index, Some(gpu_address.as_int()))
+ }
+ PrimitiveCommand::Instance { prim_instance_index, gpu_buffer_address } => {
+ (prim_instance_index, Some(gpu_buffer_address.as_int()))
+ }
+ PrimitiveCommand::Quad { prim_instance_index, gpu_buffer_address, quad_flags, edge_flags, transform_id } => {
+ if segments.is_empty() {
+ self.add_quad_to_batch(
+ *prim_instance_index,
+ *transform_id,
+ *gpu_buffer_address,
+ *quad_flags,
+ *edge_flags,
+ INVALID_SEGMENT_INDEX as u8,
+ RenderTaskId::INVALID,
+ z_generator,
+ prim_instances,
+ render_tasks,
+ );
+ } else {
+ for (i, task_id) in segments.iter().enumerate() {
+ // TODO(gw): edge_flags should be per-segment, when used for more than composites
+ debug_assert!(edge_flags.is_empty());
+
+ self.add_quad_to_batch(
+ *prim_instance_index,
+ *transform_id,
+ *gpu_buffer_address,
+ *quad_flags,
+ *edge_flags,
+ i as u8,
+ *task_id,
+ z_generator,
+ prim_instances,
+ render_tasks,
+ );
+ }
+ }
+
+ return;
+ }
+ };
+
+ let prim_instance = &prim_instances[prim_instance_index.0 as usize];
+ let is_anti_aliased = ctx.data_stores.prim_has_anti_aliasing(prim_instance);
+
+ let brush_flags = if is_anti_aliased {
+ BrushFlags::FORCE_AA
+ } else {
+ BrushFlags::empty()
+ };
+
+ let vis_flags = match prim_instance.vis.state {
+ VisibilityState::Culled => {
+ return;
+ }
+ VisibilityState::PassThrough |
+ VisibilityState::Unset => {
+ panic!("bug: invalid visibility state");
+ }
+ VisibilityState::Visible { vis_flags, .. } => {
+ vis_flags
+ }
+ };
+
+ // If this primitive is a backdrop, that means that it is known to cover
+ // the entire picture cache background. In that case, the renderer will
+ // use the backdrop color as a clear color, and so we can drop this
+ // primitive and any prior primitives from the batch lists for this
+ // picture cache slice.
+ if vis_flags.contains(PrimitiveVisibilityFlags::IS_BACKDROP) {
+ self.clear_batches();
+ return;
+ }
+
+ let transform_id = transforms
+ .get_id(
+ prim_spatial_node_index,
+ root_spatial_node_index,
+ ctx.spatial_tree,
+ );
+
+ // TODO(gw): Calculating this for every primitive is a bit
+ // wasteful. We should probably cache this in
+ // the scroll node...
+ let transform_kind = transform_id.transform_kind();
+ let prim_info = &prim_instance.vis;
+ let bounding_rect = &prim_info.clip_chain.pic_coverage_rect;
+
+ let z_id = z_generator.next();
+
+ let prim_rect = ctx.data_stores.get_local_prim_rect(
+ prim_instance,
+ &ctx.prim_store.pictures,
+ ctx.surfaces,
+ );
+
+ let mut batch_features = BatchFeatures::empty();
+ if ctx.data_stores.prim_may_need_repetition(prim_instance) {
+ batch_features |= BatchFeatures::REPETITION;
+ }
+
+ if transform_kind != TransformedRectKind::AxisAligned || is_anti_aliased {
+ batch_features |= BatchFeatures::ANTIALIASING;
+ }
+
+ // Check if the primitive might require a clip mask.
+ if prim_info.clip_task_index != ClipTaskIndex::INVALID {
+ batch_features |= BatchFeatures::CLIP_MASK;
+ }
+
+ if !bounding_rect.is_empty() {
+ debug_assert_eq!(prim_info.clip_chain.pic_spatial_node_index, surface_spatial_node_index,
+ "The primitive's bounding box is specified in a different coordinate system from the current batch!");
+ }
+
+ match prim_instance.kind {
+ PrimitiveInstanceKind::Clear { data_handle } => {
+ let prim_data = &ctx.data_stores.prim[data_handle];
+ let prim_cache_address = gpu_cache.get_address(&prim_data.gpu_cache_handle);
+
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ // TODO(gw): We can abstract some of the common code below into
+ // helper methods, as we port more primitives to make
+ // use of interning.
+
+ let prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: prim_info.clip_chain.local_clip_rect,
+ specific_prim_address: prim_cache_address,
+ transform_id,
+ };
+
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ [get_shader_opacity(1.0), 0, 0, 0],
+ );
+
+ let batch_key = BatchKey {
+ blend_mode: BlendMode::PremultipliedDestOut,
+ kind: BatchKind::Brush(BrushBatchKind::Solid),
+ textures: BatchTextures::prim_untextured(clip_mask_texture_id),
+ };
+
+ self.add_brush_instance_to_batches(
+ batch_key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ prim_data.edge_aa_mask,
+ clip_task_address,
+ brush_flags | BrushFlags::PERSPECTIVE_INTERPOLATION,
+ prim_header_index,
+ 0,
+ );
+ }
+ PrimitiveInstanceKind::NormalBorder { data_handle, ref render_task_ids, .. } => {
+ let prim_data = &ctx.data_stores.normal_border[data_handle];
+ let common_data = &prim_data.common;
+ let prim_cache_address = gpu_cache.get_address(&common_data.gpu_cache_handle);
+ let task_ids = &ctx.scratch.border_cache_handles[*render_task_ids];
+ let specified_blend_mode = BlendMode::PremultipliedAlpha;
+ let mut segment_data: SmallVec<[SegmentInstanceData; 8]> = SmallVec::new();
+
+ // Collect the segment instance data from each render
+ // task for each valid edge / corner of the border.
+
+ for task_id in task_ids {
+ if let Some((uv_rect_address, texture)) = render_tasks.resolve_location(*task_id, gpu_cache) {
+ segment_data.push(
+ SegmentInstanceData {
+ textures: TextureSet::prim_textured(texture),
+ specific_resource_address: uv_rect_address.as_int(),
+ }
+ );
+ }
+ }
+
+ // TODO: it would be less error-prone to get this info from the texture cache.
+ let image_buffer_kind = ImageBufferKind::Texture2D;
+
+ let blend_mode = if !common_data.opacity.is_opaque ||
+ prim_info.clip_task_index != ClipTaskIndex::INVALID ||
+ transform_kind == TransformedRectKind::Complex ||
+ is_anti_aliased
+ {
+ specified_blend_mode
+ } else {
+ BlendMode::None
+ };
+
+ let prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: prim_info.clip_chain.local_clip_rect,
+ specific_prim_address: prim_cache_address,
+ transform_id,
+ };
+
+ let batch_params = BrushBatchParameters::instanced(
+ BrushBatchKind::Image(image_buffer_kind),
+ ImageBrushData {
+ color_mode: ShaderColorMode::Image,
+ alpha_type: AlphaType::PremultipliedAlpha,
+ raster_space: RasterizationSpace::Local,
+ opacity: 1.0,
+ }.encode(),
+ segment_data,
+ );
+
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ batch_params.prim_user_data,
+ );
+
+ let border_data = &prim_data.kind;
+ self.add_segmented_prim_to_batch(
+ Some(border_data.brush_segments.as_slice()),
+ common_data.opacity,
+ &batch_params,
+ blend_mode,
+ batch_features,
+ brush_flags,
+ common_data.edge_aa_mask,
+ prim_header_index,
+ bounding_rect,
+ transform_kind,
+ z_id,
+ prim_info.clip_task_index,
+ ctx,
+ render_tasks,
+ );
+ }
+ PrimitiveInstanceKind::TextRun { data_handle, run_index, .. } => {
+ let run = &ctx.prim_store.text_runs[run_index];
+ let subpx_dir = run.used_font.get_subpx_dir();
+
+ // The GPU cache data is stored in the template and reused across
+ // frames and display lists.
+ let prim_data = &ctx.data_stores.text_run[data_handle];
+ let prim_cache_address = gpu_cache.get_address(&prim_data.gpu_cache_handle);
+
+ // The local prim rect is only informative for text primitives, as
+ // thus is not directly necessary for any drawing of the text run.
+ // However the glyph offsets are relative to the prim rect origin
+ // less the unsnapped reference frame offset. We also want the
+ // the snapped reference frame offset, because cannot recalculate
+ // it as it ignores the animated components for the transform. As
+ // such, we adjust the prim rect origin here, and replace the size
+ // with the unsnapped and snapped offsets respectively. This has
+ // the added bonus of avoiding quantization effects when storing
+ // floats in the extra header integers.
+ let prim_header = PrimitiveHeader {
+ local_rect: LayoutRect {
+ min: prim_rect.min - run.reference_frame_relative_offset,
+ max: run.snapped_reference_frame_relative_offset.to_point(),
+ },
+ local_clip_rect: prim_info.clip_chain.local_clip_rect,
+ specific_prim_address: prim_cache_address,
+ transform_id,
+ };
+
+ let glyph_keys = &ctx.scratch.glyph_keys[run.glyph_keys_range];
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ [
+ (run.raster_scale * 65535.0).round() as i32,
+ 0,
+ 0,
+ 0,
+ ],
+ );
+ let base_instance = GlyphInstance::new(
+ prim_header_index,
+ );
+ let batcher = &mut self.batcher;
+
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ // The run.used_font.clone() is here instead of instead of inline in the `fetch_glyph`
+ // function call to work around a miscompilation.
+ // https://github.com/rust-lang/rust/issues/80111
+ let font = run.used_font.clone();
+ ctx.resource_cache.fetch_glyphs(
+ font,
+ &glyph_keys,
+ &mut self.glyph_fetch_buffer,
+ gpu_cache,
+ |texture_id, glyph_format, glyphs| {
+ debug_assert_ne!(texture_id, TextureSource::Invalid);
+
+ let subpx_dir = subpx_dir.limit_by(glyph_format);
+
+ let textures = BatchTextures::prim_textured(
+ texture_id,
+ clip_mask_texture_id,
+ );
+
+ let kind = BatchKind::TextRun(glyph_format);
+
+ let (blend_mode, color_mode) = match glyph_format {
+ GlyphFormat::Subpixel |
+ GlyphFormat::TransformedSubpixel => {
+ if run.used_font.bg_color.a != 0 {
+ (
+ BlendMode::SubpixelWithBgColor,
+ ShaderColorMode::FromRenderPassMode,
+ )
+ } else {
+ debug_assert!(ctx.use_dual_source_blending);
+ (
+ BlendMode::SubpixelDualSource,
+ ShaderColorMode::SubpixelDualSource,
+ )
+ }
+ }
+ GlyphFormat::Alpha |
+ GlyphFormat::TransformedAlpha |
+ GlyphFormat::Bitmap => {
+ (
+ BlendMode::PremultipliedAlpha,
+ ShaderColorMode::Alpha,
+ )
+ }
+ GlyphFormat::ColorBitmap => {
+ (
+ BlendMode::PremultipliedAlpha,
+ if run.shadow {
+ // Ignore color and only sample alpha when shadowing.
+ ShaderColorMode::BitmapShadow
+ } else {
+ ShaderColorMode::ColorBitmap
+ },
+ )
+ }
+ };
+
+ // Calculate a tighter bounding rect of just the glyphs passed to this
+ // callback from request_glyphs(), rather than using the bounds of the
+ // entire text run. This improves batching when glyphs are fragmented
+ // over multiple textures in the texture cache.
+ // This code is taken from the ps_text_run shader.
+ let tight_bounding_rect = {
+ let snap_bias = match subpx_dir {
+ SubpixelDirection::None => DeviceVector2D::new(0.5, 0.5),
+ SubpixelDirection::Horizontal => DeviceVector2D::new(0.125, 0.5),
+ SubpixelDirection::Vertical => DeviceVector2D::new(0.5, 0.125),
+ SubpixelDirection::Mixed => DeviceVector2D::new(0.125, 0.125),
+ };
+ let text_offset = prim_header.local_rect.max.to_vector();
+
+ let pic_bounding_rect = if run.used_font.flags.contains(FontInstanceFlags::TRANSFORM_GLYPHS) {
+ let mut device_bounding_rect = DeviceRect::default();
+
+ let glyph_transform = ctx.spatial_tree.get_relative_transform(
+ prim_spatial_node_index,
+ root_spatial_node_index,
+ ).into_transform()
+ .with_destination::<WorldPixel>()
+ .then(&euclid::Transform3D::from_scale(ctx.global_device_pixel_scale));
+
+ let glyph_translation = DeviceVector2D::new(glyph_transform.m41, glyph_transform.m42);
+
+ let mut use_tight_bounding_rect = true;
+ for glyph in glyphs {
+ let glyph_offset = prim_data.glyphs[glyph.index_in_text_run as usize].point + prim_header.local_rect.min.to_vector();
+
+ let transformed_offset = match glyph_transform.transform_point2d(glyph_offset) {
+ Some(transformed_offset) => transformed_offset,
+ None => {
+ use_tight_bounding_rect = false;
+ break;
+ }
+ };
+ let raster_glyph_offset = (transformed_offset + snap_bias).floor();
+ let raster_text_offset = (
+ glyph_transform.transform_vector2d(text_offset) +
+ glyph_translation +
+ DeviceVector2D::new(0.5, 0.5)
+ ).floor() - glyph_translation;
+
+ let device_glyph_rect = DeviceRect::from_origin_and_size(
+ glyph.offset + raster_glyph_offset.to_vector() + raster_text_offset,
+ glyph.size.to_f32(),
+ );
+
+ device_bounding_rect = device_bounding_rect.union(&device_glyph_rect);
+ }
+
+ if use_tight_bounding_rect {
+ let map_device_to_surface: SpaceMapper<PicturePixel, DevicePixel> = SpaceMapper::new_with_target(
+ root_spatial_node_index,
+ surface_spatial_node_index,
+ device_bounding_rect,
+ ctx.spatial_tree,
+ );
+
+ match map_device_to_surface.unmap(&device_bounding_rect) {
+ Some(r) => r.intersection(bounding_rect),
+ None => Some(*bounding_rect),
+ }
+ } else {
+ Some(*bounding_rect)
+ }
+ } else {
+ let mut local_bounding_rect = LayoutRect::default();
+
+ let glyph_raster_scale = run.raster_scale * ctx.global_device_pixel_scale.get();
+
+ for glyph in glyphs {
+ let glyph_offset = prim_data.glyphs[glyph.index_in_text_run as usize].point + prim_header.local_rect.min.to_vector();
+ let glyph_scale = LayoutToDeviceScale::new(glyph_raster_scale / glyph.scale);
+ let raster_glyph_offset = (glyph_offset * LayoutToDeviceScale::new(glyph_raster_scale) + snap_bias).floor() / glyph.scale;
+ let local_glyph_rect = LayoutRect::from_origin_and_size(
+ (glyph.offset + raster_glyph_offset.to_vector()) / glyph_scale + text_offset,
+ glyph.size.to_f32() / glyph_scale,
+ );
+
+ local_bounding_rect = local_bounding_rect.union(&local_glyph_rect);
+ }
+
+ let map_prim_to_surface: SpaceMapper<LayoutPixel, PicturePixel> = SpaceMapper::new_with_target(
+ surface_spatial_node_index,
+ prim_spatial_node_index,
+ *bounding_rect,
+ ctx.spatial_tree,
+ );
+ map_prim_to_surface.map(&local_bounding_rect)
+ };
+
+ let intersected = match pic_bounding_rect {
+ // The text run may have been clipped, for example if part of it is offscreen.
+ // So intersect our result with the original bounding rect.
+ Some(rect) => rect.intersection(bounding_rect).unwrap_or_else(PictureRect::zero),
+ // If space mapping went off the rails, fall back to the old behavior.
+ //TODO: consider skipping the glyph run completely in this case.
+ None => *bounding_rect,
+ };
+
+ intersected
+ };
+
+ let key = BatchKey::new(kind, blend_mode, textures);
+
+ let render_task_address = batcher.render_task_address;
+ let batch = batcher.alpha_batch_list.set_params_and_get_batch(
+ key,
+ batch_features,
+ &tight_bounding_rect,
+ z_id,
+ );
+
+ batch.reserve(glyphs.len());
+ for glyph in glyphs {
+ batch.push(base_instance.build(
+ render_task_address,
+ clip_task_address,
+ subpx_dir,
+ glyph.index_in_text_run,
+ glyph.uv_rect_address,
+ color_mode,
+ ));
+ }
+ },
+ );
+ }
+ PrimitiveInstanceKind::LineDecoration { data_handle, ref render_task, .. } => {
+ // The GPU cache data is stored in the template and reused across
+ // frames and display lists.
+ let common_data = &ctx.data_stores.line_decoration[data_handle].common;
+ let prim_cache_address = gpu_cache.get_address(&common_data.gpu_cache_handle);
+
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ let (batch_kind, textures, prim_user_data, specific_resource_address) = match render_task {
+ Some(task_id) => {
+ let (uv_rect_address, texture) = render_tasks.resolve_location(*task_id, gpu_cache).unwrap();
+ let textures = BatchTextures::prim_textured(
+ texture,
+ clip_mask_texture_id,
+ );
+ (
+ BrushBatchKind::Image(texture.image_buffer_kind()),
+ textures,
+ ImageBrushData {
+ color_mode: ShaderColorMode::Image,
+ alpha_type: AlphaType::PremultipliedAlpha,
+ raster_space: RasterizationSpace::Local,
+ opacity: 1.0,
+ }.encode(),
+ uv_rect_address.as_int(),
+ )
+ }
+ None => {
+ (
+ BrushBatchKind::Solid,
+ BatchTextures::prim_untextured(clip_mask_texture_id),
+ [get_shader_opacity(1.0), 0, 0, 0],
+ 0,
+ )
+ }
+ };
+
+ // TODO(gw): We can abstract some of the common code below into
+ // helper methods, as we port more primitives to make
+ // use of interning.
+ let blend_mode = if !common_data.opacity.is_opaque ||
+ prim_info.clip_task_index != ClipTaskIndex::INVALID ||
+ transform_kind == TransformedRectKind::Complex ||
+ is_anti_aliased
+ {
+ BlendMode::PremultipliedAlpha
+ } else {
+ BlendMode::None
+ };
+
+ let prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: prim_info.clip_chain.local_clip_rect,
+ specific_prim_address: prim_cache_address,
+ transform_id,
+ };
+
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ prim_user_data,
+ );
+
+ let batch_key = BatchKey {
+ blend_mode,
+ kind: BatchKind::Brush(batch_kind),
+ textures,
+ };
+
+ self.add_brush_instance_to_batches(
+ batch_key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ common_data.edge_aa_mask,
+ clip_task_address,
+ brush_flags | BrushFlags::PERSPECTIVE_INTERPOLATION,
+ prim_header_index,
+ specific_resource_address,
+ );
+ }
+ PrimitiveInstanceKind::Picture { pic_index, segment_instance_index, .. } => {
+ let picture = &ctx.prim_store.pictures[pic_index.0];
+ let blend_mode = BlendMode::PremultipliedAlpha;
+ let prim_cache_address = gpu_cache.get_address(&ctx.globals.default_image_handle);
+
+ match picture.raster_config {
+ Some(ref raster_config) => {
+ // If the child picture was rendered in local space, we can safely
+ // interpolate the UV coordinates with perspective correction.
+ let brush_flags = brush_flags | BrushFlags::PERSPECTIVE_INTERPOLATION;
+
+ let surface = &ctx.surfaces[raster_config.surface_index.0];
+ let mut local_clip_rect = prim_info.clip_chain.local_clip_rect;
+
+ // If we are drawing with snapping enabled, form a simple transform that just applies
+ // the scale / translation from the raster transform. Otherwise, in edge cases where the
+ // intermediate surface has a non-identity but axis-aligned transform (e.g. a 180 degree
+ // rotation) it can be applied twice.
+ let transform_id = if surface.surface_spatial_node_index == surface.raster_spatial_node_index {
+ transform_id
+ } else {
+ let map_local_to_raster = SpaceMapper::new_with_target(
+ root_spatial_node_index,
+ surface.surface_spatial_node_index,
+ LayoutRect::max_rect(),
+ ctx.spatial_tree,
+ );
+
+ let raster_rect = map_local_to_raster
+ .map(&prim_rect)
+ .unwrap();
+
+ let sx = (raster_rect.max.x - raster_rect.min.x) / (prim_rect.max.x - prim_rect.min.x);
+ let sy = (raster_rect.max.y - raster_rect.min.y) / (prim_rect.max.y - prim_rect.min.y);
+
+ let tx = raster_rect.min.x - sx * prim_rect.min.x;
+ let ty = raster_rect.min.y - sy * prim_rect.min.y;
+
+ let transform = ScaleOffset::new(sx, sy, tx, ty);
+
+ let raster_clip_rect = map_local_to_raster
+ .map(&prim_info.clip_chain.local_clip_rect)
+ .unwrap();
+ local_clip_rect = transform.unmap_rect(&raster_clip_rect);
+
+ transforms.get_custom(transform.to_transform())
+ };
+
+ let prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect,
+ specific_prim_address: prim_cache_address,
+ transform_id,
+ };
+
+ let mut is_opaque = prim_info.clip_task_index == ClipTaskIndex::INVALID
+ && surface.is_opaque
+ && transform_kind == TransformedRectKind::AxisAligned
+ && !is_anti_aliased;
+
+ let pic_task_id = picture.primary_render_task_id.unwrap();
+
+ match raster_config.composite_mode {
+ PictureCompositeMode::TileCache { .. } => {
+ // TODO(gw): For now, TileCache is still a composite mode, even though
+ // it will only exist as a top level primitive and never
+ // be encountered during batching. Consider making TileCache
+ // a standalone type, not a picture.
+ }
+ PictureCompositeMode::IntermediateSurface { .. } => {
+ // TODO(gw): As an optimization, support making this a pass-through
+ // and/or drawing directly from here when possible
+ // (e.g. if not wrapped by filters / different spatial node).
+ }
+ PictureCompositeMode::Filter(ref filter) => {
+ assert!(filter.is_visible());
+ match filter {
+ Filter::Blur { .. } => {
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ let kind = BatchKind::Brush(
+ BrushBatchKind::Image(ImageBufferKind::Texture2D)
+ );
+
+ let (uv_rect_address, texture) = render_tasks.resolve_location(
+ pic_task_id,
+ gpu_cache,
+ ).unwrap();
+ let textures = BatchTextures::prim_textured(
+ texture,
+ clip_mask_texture_id,
+ );
+
+ let key = BatchKey::new(
+ kind,
+ blend_mode,
+ textures,
+ );
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ ImageBrushData {
+ color_mode: ShaderColorMode::Image,
+ alpha_type: AlphaType::PremultipliedAlpha,
+ raster_space: RasterizationSpace::Screen,
+ opacity: 1.0,
+ }.encode(),
+ );
+
+ self.add_brush_instance_to_batches(
+ key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ EdgeAaSegmentMask::all(),
+ clip_task_address,
+ brush_flags,
+ prim_header_index,
+ uv_rect_address.as_int(),
+ );
+ }
+ Filter::DropShadows(shadows) => {
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ // Draw an instance per shadow first, following by the content.
+
+ // The shadows and the content get drawn as a brush image.
+ let kind = BatchKind::Brush(
+ BrushBatchKind::Image(ImageBufferKind::Texture2D),
+ );
+
+ // Gets the saved render task ID of the content, which is
+ // deeper in the render task graph than the direct child.
+ let secondary_id = picture.secondary_render_task_id.expect("no secondary!?");
+ let content_source = {
+ let secondary_task = &render_tasks[secondary_id];
+ let texture_id = secondary_task.get_target_texture();
+ TextureSource::TextureCache(
+ texture_id,
+ Swizzle::default(),
+ )
+ };
+
+ // Retrieve the UV rect addresses for shadow/content.
+ let (shadow_uv_rect_address, shadow_texture) = render_tasks.resolve_location(
+ pic_task_id,
+ gpu_cache,
+ ).unwrap();
+ let shadow_textures = BatchTextures::prim_textured(
+ shadow_texture,
+ clip_mask_texture_id,
+ );
+
+ let content_uv_rect_address = render_tasks[secondary_id]
+ .get_texture_address(gpu_cache)
+ .as_int();
+
+ // Build BatchTextures for shadow/content
+ let content_textures = BatchTextures::prim_textured(
+ content_source,
+ clip_mask_texture_id,
+ );
+
+ // Build batch keys for shadow/content
+ let shadow_key = BatchKey::new(kind, blend_mode, shadow_textures);
+ let content_key = BatchKey::new(kind, blend_mode, content_textures);
+
+ for (shadow, shadow_gpu_data) in shadows.iter().zip(picture.extra_gpu_data_handles.iter()) {
+ // Get the GPU cache address of the extra data handle.
+ let shadow_prim_address = gpu_cache.get_address(shadow_gpu_data);
+
+ let shadow_rect = prim_header.local_rect.translate(shadow.offset);
+
+ let shadow_prim_header = PrimitiveHeader {
+ local_rect: shadow_rect,
+ specific_prim_address: shadow_prim_address,
+ ..prim_header
+ };
+
+ let shadow_prim_header_index = prim_headers.push(
+ &shadow_prim_header,
+ z_id,
+ ImageBrushData {
+ color_mode: ShaderColorMode::Alpha,
+ alpha_type: AlphaType::PremultipliedAlpha,
+ raster_space: RasterizationSpace::Screen,
+ opacity: 1.0,
+ }.encode(),
+ );
+
+ self.add_brush_instance_to_batches(
+ shadow_key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ EdgeAaSegmentMask::all(),
+ clip_task_address,
+ brush_flags,
+ shadow_prim_header_index,
+ shadow_uv_rect_address.as_int(),
+ );
+ }
+ let z_id_content = z_generator.next();
+
+ let content_prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id_content,
+ ImageBrushData {
+ color_mode: ShaderColorMode::Image,
+ alpha_type: AlphaType::PremultipliedAlpha,
+ raster_space: RasterizationSpace::Screen,
+ opacity: 1.0,
+ }.encode(),
+ );
+
+ self.add_brush_instance_to_batches(
+ content_key,
+ batch_features,
+ bounding_rect,
+ z_id_content,
+ INVALID_SEGMENT_INDEX,
+ EdgeAaSegmentMask::all(),
+ clip_task_address,
+ brush_flags,
+ content_prim_header_index,
+ content_uv_rect_address,
+ );
+ }
+ Filter::Opacity(_, amount) => {
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ let amount = (amount * 65536.0) as i32;
+
+ let (uv_rect_address, texture) = render_tasks.resolve_location(
+ pic_task_id,
+ gpu_cache,
+ ).unwrap();
+ let textures = BatchTextures::prim_textured(
+ texture,
+ clip_mask_texture_id,
+ );
+
+
+ let key = BatchKey::new(
+ BatchKind::Brush(BrushBatchKind::Opacity),
+ BlendMode::PremultipliedAlpha,
+ textures,
+ );
+
+ let prim_header_index = prim_headers.push(&prim_header, z_id, [
+ uv_rect_address.as_int(),
+ amount,
+ 0,
+ 0,
+ ]);
+
+ self.add_brush_instance_to_batches(
+ key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ EdgeAaSegmentMask::all(),
+ clip_task_address,
+ brush_flags,
+ prim_header_index,
+ 0,
+ );
+ }
+ _ => {
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ // Must be kept in sync with brush_blend.glsl
+ let filter_mode = filter.as_int();
+
+ let user_data = match filter {
+ Filter::Identity => 0x10000i32, // matches `Contrast(1)`
+ Filter::Contrast(amount) |
+ Filter::Grayscale(amount) |
+ Filter::Invert(amount) |
+ Filter::Saturate(amount) |
+ Filter::Sepia(amount) |
+ Filter::Brightness(amount) => {
+ (amount * 65536.0) as i32
+ }
+ Filter::SrgbToLinear | Filter::LinearToSrgb => 0,
+ Filter::HueRotate(angle) => {
+ (0.01745329251 * angle * 65536.0) as i32
+ }
+ Filter::ColorMatrix(_) => {
+ picture.extra_gpu_data_handles[0].as_int(gpu_cache)
+ }
+ Filter::Flood(_) => {
+ picture.extra_gpu_data_handles[0].as_int(gpu_cache)
+ }
+
+ // These filters are handled via different paths.
+ Filter::ComponentTransfer |
+ Filter::Blur { .. } |
+ Filter::DropShadows(..) |
+ Filter::Opacity(..) => unreachable!(),
+ };
+
+ // Other filters that may introduce opacity are handled via different
+ // paths.
+ if let Filter::ColorMatrix(..) = filter {
+ is_opaque = false;
+ }
+
+ let (uv_rect_address, texture) = render_tasks.resolve_location(
+ pic_task_id,
+ gpu_cache,
+ ).unwrap();
+ let textures = BatchTextures::prim_textured(
+ texture,
+ clip_mask_texture_id,
+ );
+
+ let blend_mode = if is_opaque {
+ BlendMode::None
+ } else {
+ BlendMode::PremultipliedAlpha
+ };
+
+ let key = BatchKey::new(
+ BatchKind::Brush(BrushBatchKind::Blend),
+ blend_mode,
+ textures,
+ );
+
+ let prim_header_index = prim_headers.push(&prim_header, z_id, [
+ uv_rect_address.as_int(),
+ filter_mode,
+ user_data,
+ 0,
+ ]);
+
+ self.add_brush_instance_to_batches(
+ key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ EdgeAaSegmentMask::all(),
+ clip_task_address,
+ brush_flags,
+ prim_header_index,
+ 0,
+ );
+ }
+ }
+ }
+ PictureCompositeMode::ComponentTransferFilter(handle) => {
+ // This is basically the same as the general filter case above
+ // except we store a little more data in the filter mode and
+ // a gpu cache handle in the user data.
+ let filter_data = &ctx.data_stores.filter_data[handle];
+ let filter_mode : i32 = Filter::ComponentTransfer.as_int() |
+ ((filter_data.data.r_func.to_int() << 28 |
+ filter_data.data.g_func.to_int() << 24 |
+ filter_data.data.b_func.to_int() << 20 |
+ filter_data.data.a_func.to_int() << 16) as i32);
+
+ let user_data = filter_data.gpu_cache_handle.as_int(gpu_cache);
+
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ let (uv_rect_address, texture) = render_tasks.resolve_location(
+ pic_task_id,
+ gpu_cache,
+ ).unwrap();
+ let textures = BatchTextures::prim_textured(
+ texture,
+ clip_mask_texture_id,
+ );
+
+ let key = BatchKey::new(
+ BatchKind::Brush(BrushBatchKind::Blend),
+ BlendMode::PremultipliedAlpha,
+ textures,
+ );
+
+ let prim_header_index = prim_headers.push(&prim_header, z_id, [
+ uv_rect_address.as_int(),
+ filter_mode,
+ user_data,
+ 0,
+ ]);
+
+ self.add_brush_instance_to_batches(
+ key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ EdgeAaSegmentMask::all(),
+ clip_task_address,
+ brush_flags,
+ prim_header_index,
+ 0,
+ );
+ }
+ PictureCompositeMode::MixBlend(mode) if BlendMode::from_mix_blend_mode(
+ mode,
+ ctx.use_advanced_blending,
+ !ctx.break_advanced_blend_batches,
+ ctx.use_dual_source_blending,
+ ).is_some() => {
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ let (uv_rect_address, texture) = render_tasks.resolve_location(
+ pic_task_id,
+ gpu_cache,
+ ).unwrap();
+ let textures = BatchTextures::prim_textured(
+ texture,
+ clip_mask_texture_id,
+ );
+
+
+ let key = BatchKey::new(
+ BatchKind::Brush(
+ BrushBatchKind::Image(ImageBufferKind::Texture2D),
+ ),
+ BlendMode::from_mix_blend_mode(
+ mode,
+ ctx.use_advanced_blending,
+ !ctx.break_advanced_blend_batches,
+ ctx.use_dual_source_blending,
+ ).unwrap(),
+ textures,
+ );
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ ImageBrushData {
+ color_mode: match key.blend_mode {
+ BlendMode::MultiplyDualSource => ShaderColorMode::MultiplyDualSource,
+ _ => ShaderColorMode::Image,
+ },
+ alpha_type: AlphaType::PremultipliedAlpha,
+ raster_space: RasterizationSpace::Screen,
+ opacity: 1.0,
+ }.encode(),
+ );
+
+ self.add_brush_instance_to_batches(
+ key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ EdgeAaSegmentMask::all(),
+ clip_task_address,
+ brush_flags,
+ prim_header_index,
+ uv_rect_address.as_int(),
+ );
+ }
+ PictureCompositeMode::MixBlend(mode) => {
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+ let backdrop_id = picture.secondary_render_task_id.expect("no backdrop!?");
+
+ let color0 = render_tasks[backdrop_id].get_target_texture();
+ let color1 = render_tasks[pic_task_id].get_target_texture();
+
+ // Create a separate brush instance for each batcher. For most cases,
+ // there is only one batcher. However, in the case of drawing onto
+ // a picture cache, there is one batcher per tile. Although not
+ // currently used, the implementation of mix-blend-mode now supports
+ // doing partial readbacks per-tile. In future, this will be enabled
+ // and allow mix-blends to operate on picture cache surfaces without
+ // a separate isolated intermediate surface.
+
+ let render_task_address = self.batcher.render_task_address;
+
+ let batch_key = BatchKey::new(
+ BatchKind::Brush(
+ BrushBatchKind::MixBlend {
+ task_id: self.batcher.render_task_id,
+ backdrop_id,
+ },
+ ),
+ BlendMode::PremultipliedAlpha,
+ BatchTextures {
+ input: TextureSet {
+ colors: [
+ TextureSource::TextureCache(
+ color0,
+ Swizzle::default(),
+ ),
+ TextureSource::TextureCache(
+ color1,
+ Swizzle::default(),
+ ),
+ TextureSource::Invalid,
+ ],
+ },
+ clip_mask: clip_mask_texture_id,
+ },
+ );
+ let src_uv_address = render_tasks[pic_task_id].get_texture_address(gpu_cache);
+ let readback_uv_address = render_tasks[backdrop_id].get_texture_address(gpu_cache);
+ let prim_header_index = prim_headers.push(&prim_header, z_id, [
+ mode as u32 as i32,
+ readback_uv_address.as_int(),
+ src_uv_address.as_int(),
+ 0,
+ ]);
+
+ let instance = BrushInstance {
+ segment_index: INVALID_SEGMENT_INDEX,
+ edge_flags: EdgeAaSegmentMask::all(),
+ clip_task_address,
+ render_task_address,
+ brush_flags,
+ prim_header_index,
+ resource_address: 0,
+ };
+
+ self.batcher.push_single_instance(
+ batch_key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ PrimitiveInstanceData::from(instance),
+ );
+ }
+ PictureCompositeMode::Blit(_) => {
+ match picture.context_3d {
+ Picture3DContext::In { root_data: Some(_), .. } => {
+ unreachable!("bug: should not have a raster_config");
+ }
+ Picture3DContext::In { root_data: None, .. } => {
+ // TODO(gw): Store this inside the split picture so that we
+ // don't need to pass in extra_prim_gpu_address for
+ // every prim instance.
+ // TODO(gw): Ideally we'd skip adding 3d child prims to batches
+ // without gpu cache address but it's currently
+ // used by the prepare pass. Refactor this!
+ let extra_prim_gpu_address = match extra_prim_gpu_address {
+ Some(prim_address) => prim_address,
+ None => return,
+ };
+
+ // Get clip task, if set, for the picture primitive.
+ let (child_clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ let prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: prim_info.clip_chain.local_clip_rect,
+ specific_prim_address: GpuCacheAddress::INVALID,
+ transform_id: transforms
+ .get_id(
+ prim_spatial_node_index,
+ root_spatial_node_index,
+ ctx.spatial_tree,
+ ),
+ };
+
+ let child_pic_task_id = picture
+ .primary_render_task_id
+ .unwrap();
+
+ let (uv_rect_address, texture) = render_tasks.resolve_location(
+ child_pic_task_id,
+ gpu_cache,
+ ).unwrap();
+ let textures = BatchTextures::prim_textured(
+ texture,
+ clip_mask_texture_id,
+ );
+
+ // Need a new z-id for each child preserve-3d context added
+ // by this inner loop.
+ let z_id = z_generator.next();
+
+ let prim_header_index = prim_headers.push(&prim_header, z_id, [
+ uv_rect_address.as_int(),
+ BrushFlags::PERSPECTIVE_INTERPOLATION.bits() as i32,
+ 0,
+ child_clip_task_address.0 as i32,
+ ]);
+
+ let key = BatchKey::new(
+ BatchKind::SplitComposite,
+ BlendMode::PremultipliedAlpha,
+ textures,
+ );
+
+ self.add_split_composite_instance_to_batches(
+ key,
+ BatchFeatures::CLIP_MASK,
+ &prim_info.clip_chain.pic_coverage_rect,
+ z_id,
+ prim_header_index,
+ extra_prim_gpu_address,
+ );
+ }
+ Picture3DContext::Out { .. } => {
+ let uv_rect_address = render_tasks[pic_task_id]
+ .get_texture_address(gpu_cache)
+ .as_int();
+ let cache_render_task = &render_tasks[pic_task_id];
+ let texture_id = cache_render_task.get_target_texture();
+ let textures = TextureSet {
+ colors: [
+ TextureSource::TextureCache(
+ texture_id,
+ Swizzle::default(),
+ ),
+ TextureSource::Invalid,
+ TextureSource::Invalid,
+ ],
+ };
+ let batch_params = BrushBatchParameters::shared(
+ BrushBatchKind::Image(ImageBufferKind::Texture2D),
+ textures,
+ ImageBrushData {
+ color_mode: ShaderColorMode::Image,
+ alpha_type: AlphaType::PremultipliedAlpha,
+ raster_space: RasterizationSpace::Screen,
+ opacity: 1.0,
+ }.encode(),
+ uv_rect_address,
+ );
+
+ let is_segmented =
+ segment_instance_index != SegmentInstanceIndex::INVALID &&
+ segment_instance_index != SegmentInstanceIndex::UNUSED;
+
+ let (prim_cache_address, segments) = if is_segmented {
+ let segment_instance = &ctx.scratch.segment_instances[segment_instance_index];
+ let segments = Some(&ctx.scratch.segments[segment_instance.segments_range]);
+ (gpu_cache.get_address(&segment_instance.gpu_cache_handle), segments)
+ } else {
+ (prim_cache_address, None)
+ };
+
+ let prim_header = PrimitiveHeader {
+ specific_prim_address: prim_cache_address,
+ ..prim_header
+ };
+
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ batch_params.prim_user_data,
+ );
+
+ let (opacity, blend_mode) = if is_opaque {
+ (PrimitiveOpacity::opaque(), BlendMode::None)
+ } else {
+ (PrimitiveOpacity::translucent(), BlendMode::PremultipliedAlpha)
+ };
+
+ self.add_segmented_prim_to_batch(
+ segments,
+ opacity,
+ &batch_params,
+ blend_mode,
+ batch_features,
+ brush_flags,
+ EdgeAaSegmentMask::all(),
+ prim_header_index,
+ bounding_rect,
+ transform_kind,
+ z_id,
+ prim_info.clip_task_index,
+ ctx,
+ render_tasks,
+ );
+ }
+ }
+ }
+ PictureCompositeMode::SvgFilter(..) => {
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ let kind = BatchKind::Brush(
+ BrushBatchKind::Image(ImageBufferKind::Texture2D)
+ );
+ let (uv_rect_address, texture) = render_tasks.resolve_location(
+ pic_task_id,
+ gpu_cache,
+ ).unwrap();
+ let textures = BatchTextures::prim_textured(
+ texture,
+ clip_mask_texture_id,
+ );
+ let key = BatchKey::new(
+ kind,
+ blend_mode,
+ textures,
+ );
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ ImageBrushData {
+ color_mode: ShaderColorMode::Image,
+ alpha_type: AlphaType::PremultipliedAlpha,
+ raster_space: RasterizationSpace::Screen,
+ opacity: 1.0,
+ }.encode(),
+ );
+
+ self.add_brush_instance_to_batches(
+ key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ EdgeAaSegmentMask::all(),
+ clip_task_address,
+ brush_flags,
+ prim_header_index,
+ uv_rect_address.as_int(),
+ );
+ }
+ }
+ }
+ None => {
+ unreachable!();
+ }
+ }
+ }
+ PrimitiveInstanceKind::ImageBorder { data_handle, .. } => {
+ let prim_data = &ctx.data_stores.image_border[data_handle];
+ let common_data = &prim_data.common;
+ let border_data = &prim_data.kind;
+
+ let (uv_rect_address, texture) = match render_tasks.resolve_location(border_data.src_color, gpu_cache) {
+ Some(src) => src,
+ None => {
+ return;
+ }
+ };
+
+ let textures = TextureSet::prim_textured(texture);
+ let prim_cache_address = gpu_cache.get_address(&common_data.gpu_cache_handle);
+ let blend_mode = if !common_data.opacity.is_opaque ||
+ prim_info.clip_task_index != ClipTaskIndex::INVALID ||
+ transform_kind == TransformedRectKind::Complex ||
+ is_anti_aliased
+ {
+ BlendMode::PremultipliedAlpha
+ } else {
+ BlendMode::None
+ };
+
+ let prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: prim_info.clip_chain.local_clip_rect,
+ specific_prim_address: prim_cache_address,
+ transform_id,
+ };
+
+ let batch_params = BrushBatchParameters::shared(
+ BrushBatchKind::Image(texture.image_buffer_kind()),
+ textures,
+ ImageBrushData {
+ color_mode: ShaderColorMode::Image,
+ alpha_type: AlphaType::PremultipliedAlpha,
+ raster_space: RasterizationSpace::Local,
+ opacity: 1.0,
+ }.encode(),
+ uv_rect_address.as_int(),
+ );
+
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ batch_params.prim_user_data,
+ );
+
+ self.add_segmented_prim_to_batch(
+ Some(border_data.brush_segments.as_slice()),
+ common_data.opacity,
+ &batch_params,
+ blend_mode,
+ batch_features,
+ brush_flags,
+ common_data.edge_aa_mask,
+ prim_header_index,
+ bounding_rect,
+ transform_kind,
+ z_id,
+ prim_info.clip_task_index,
+ ctx,
+ render_tasks,
+ );
+ }
+ PrimitiveInstanceKind::Rectangle { data_handle, segment_instance_index, use_legacy_path, .. } => {
+ debug_assert!(use_legacy_path);
+ let prim_data = &ctx.data_stores.prim[data_handle];
+
+ let blend_mode = if !prim_data.opacity.is_opaque ||
+ prim_info.clip_task_index != ClipTaskIndex::INVALID ||
+ transform_kind == TransformedRectKind::Complex ||
+ is_anti_aliased
+ {
+ BlendMode::PremultipliedAlpha
+ } else {
+ BlendMode::None
+ };
+
+ let batch_params = BrushBatchParameters::shared(
+ BrushBatchKind::Solid,
+ TextureSet::UNTEXTURED,
+ [get_shader_opacity(1.0), 0, 0, 0],
+ 0,
+ );
+
+ let (prim_cache_address, segments) = if segment_instance_index == SegmentInstanceIndex::UNUSED {
+ (gpu_cache.get_address(&prim_data.gpu_cache_handle), None)
+ } else {
+ let segment_instance = &ctx.scratch.segment_instances[segment_instance_index];
+ let segments = Some(&ctx.scratch.segments[segment_instance.segments_range]);
+ (gpu_cache.get_address(&segment_instance.gpu_cache_handle), segments)
+ };
+
+ let prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: prim_info.clip_chain.local_clip_rect,
+ specific_prim_address: prim_cache_address,
+ transform_id,
+ };
+
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ batch_params.prim_user_data,
+ );
+
+ self.add_segmented_prim_to_batch(
+ segments,
+ prim_data.opacity,
+ &batch_params,
+ blend_mode,
+ batch_features,
+ brush_flags,
+ prim_data.edge_aa_mask,
+ prim_header_index,
+ bounding_rect,
+ transform_kind,
+ z_id,
+ prim_info.clip_task_index,
+ ctx,
+ render_tasks,
+ );
+ }
+ PrimitiveInstanceKind::YuvImage { data_handle, segment_instance_index, is_compositor_surface, .. } => {
+ debug_assert!(!is_compositor_surface);
+
+ let yuv_image_data = &ctx.data_stores.yuv_image[data_handle].kind;
+ let mut textures = TextureSet::UNTEXTURED;
+ let mut uv_rect_addresses = [0; 3];
+
+ //yuv channel
+ let channel_count = yuv_image_data.format.get_plane_num();
+ debug_assert!(channel_count <= 3);
+ for channel in 0 .. channel_count {
+
+ let src_channel = render_tasks.resolve_location(yuv_image_data.src_yuv[channel], gpu_cache);
+
+ let (uv_rect_address, texture_source) = match src_channel {
+ Some(src) => src,
+ None => {
+ warn!("Warnings: skip a PrimitiveKind::YuvImage");
+ return;
+ }
+ };
+
+ textures.colors[channel] = texture_source;
+ uv_rect_addresses[channel] = uv_rect_address.as_int();
+ }
+
+ // All yuv textures should be the same type.
+ let buffer_kind = textures.colors[0].image_buffer_kind();
+ assert!(
+ textures.colors[1 .. yuv_image_data.format.get_plane_num()]
+ .iter()
+ .all(|&tid| buffer_kind == tid.image_buffer_kind())
+ );
+
+ let kind = BrushBatchKind::YuvImage(
+ buffer_kind,
+ yuv_image_data.format,
+ yuv_image_data.color_depth,
+ yuv_image_data.color_space,
+ yuv_image_data.color_range,
+ );
+
+ let batch_params = BrushBatchParameters::shared(
+ kind,
+ textures,
+ [
+ uv_rect_addresses[0],
+ uv_rect_addresses[1],
+ uv_rect_addresses[2],
+ 0,
+ ],
+ 0,
+ );
+
+ let prim_common_data = ctx.data_stores.as_common_data(&prim_instance);
+
+ let blend_mode = if !prim_common_data.opacity.is_opaque ||
+ prim_info.clip_task_index != ClipTaskIndex::INVALID ||
+ transform_kind == TransformedRectKind::Complex ||
+ is_anti_aliased
+ {
+ BlendMode::PremultipliedAlpha
+ } else {
+ BlendMode::None
+ };
+
+ debug_assert_ne!(segment_instance_index, SegmentInstanceIndex::INVALID);
+ let (prim_cache_address, segments) = if segment_instance_index == SegmentInstanceIndex::UNUSED {
+ (gpu_cache.get_address(&prim_common_data.gpu_cache_handle), None)
+ } else {
+ let segment_instance = &ctx.scratch.segment_instances[segment_instance_index];
+ let segments = Some(&ctx.scratch.segments[segment_instance.segments_range]);
+ (gpu_cache.get_address(&segment_instance.gpu_cache_handle), segments)
+ };
+
+ let prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: prim_info.clip_chain.local_clip_rect,
+ specific_prim_address: prim_cache_address,
+ transform_id,
+ };
+
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ batch_params.prim_user_data,
+ );
+
+ self.add_segmented_prim_to_batch(
+ segments,
+ prim_common_data.opacity,
+ &batch_params,
+ blend_mode,
+ batch_features,
+ brush_flags,
+ prim_common_data.edge_aa_mask,
+ prim_header_index,
+ bounding_rect,
+ transform_kind,
+ z_id,
+ prim_info.clip_task_index,
+ ctx,
+ render_tasks,
+ );
+ }
+ PrimitiveInstanceKind::Image { data_handle, image_instance_index, is_compositor_surface, .. } => {
+ debug_assert!(!is_compositor_surface);
+
+ let image_data = &ctx.data_stores.image[data_handle].kind;
+ let common_data = &ctx.data_stores.image[data_handle].common;
+ let image_instance = &ctx.prim_store.images[image_instance_index];
+ let prim_user_data = ImageBrushData {
+ color_mode: ShaderColorMode::Image,
+ alpha_type: image_data.alpha_type,
+ raster_space: RasterizationSpace::Local,
+ opacity: 1.0,
+ }.encode();
+
+ let blend_mode = if !common_data.opacity.is_opaque ||
+ prim_info.clip_task_index != ClipTaskIndex::INVALID ||
+ transform_kind == TransformedRectKind::Complex ||
+ is_anti_aliased
+ {
+ match image_data.alpha_type {
+ AlphaType::PremultipliedAlpha => BlendMode::PremultipliedAlpha,
+ AlphaType::Alpha => BlendMode::Alpha,
+ }
+ } else {
+ BlendMode::None
+ };
+
+ if image_instance.visible_tiles.is_empty() {
+ if cfg!(debug_assertions) {
+ match ctx.resource_cache.get_image_properties(image_data.key) {
+ Some(ImageProperties { tiling: None, .. }) | None => (),
+ other => panic!("Non-tiled image with no visible images detected! Properties {:?}", other),
+ }
+ }
+
+ let src_color = render_tasks.resolve_location(image_instance.src_color, gpu_cache);
+
+ let (uv_rect_address, texture_source) = match src_color {
+ Some(src) => src,
+ None => {
+ return;
+ }
+ };
+
+ let batch_params = BrushBatchParameters::shared(
+ BrushBatchKind::Image(texture_source.image_buffer_kind()),
+ TextureSet::prim_textured(texture_source),
+ prim_user_data,
+ uv_rect_address.as_int(),
+ );
+
+ debug_assert_ne!(image_instance.segment_instance_index, SegmentInstanceIndex::INVALID);
+ let (prim_cache_address, segments) = if image_instance.segment_instance_index == SegmentInstanceIndex::UNUSED {
+ (gpu_cache.get_address(&common_data.gpu_cache_handle), None)
+ } else {
+ let segment_instance = &ctx.scratch.segment_instances[image_instance.segment_instance_index];
+ let segments = Some(&ctx.scratch.segments[segment_instance.segments_range]);
+ (gpu_cache.get_address(&segment_instance.gpu_cache_handle), segments)
+ };
+
+ let prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: prim_info.clip_chain.local_clip_rect,
+ specific_prim_address: prim_cache_address,
+ transform_id,
+ };
+
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ batch_params.prim_user_data,
+ );
+
+ self.add_segmented_prim_to_batch(
+ segments,
+ common_data.opacity,
+ &batch_params,
+ blend_mode,
+ batch_features,
+ brush_flags,
+ common_data.edge_aa_mask,
+ prim_header_index,
+ bounding_rect,
+ transform_kind,
+ z_id,
+ prim_info.clip_task_index,
+ ctx,
+ render_tasks,
+ );
+ } else {
+ const VECS_PER_SPECIFIC_BRUSH: usize = 3;
+ let max_tiles_per_header = (MAX_VERTEX_TEXTURE_WIDTH - VECS_PER_SPECIFIC_BRUSH) / VECS_PER_SEGMENT;
+
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ // use temporary block storage since we don't know the number of visible tiles beforehand
+ let mut gpu_blocks = Vec::<GpuBlockData>::with_capacity(3 + max_tiles_per_header * 2);
+ for chunk in image_instance.visible_tiles.chunks(max_tiles_per_header) {
+ gpu_blocks.clear();
+ gpu_blocks.push(image_data.color.premultiplied().into()); //color
+ gpu_blocks.push(PremultipliedColorF::WHITE.into()); //bg color
+ gpu_blocks.push([-1.0, 0.0, 0.0, 0.0].into()); //stretch size
+ // negative first value makes the shader code ignore it and use the local size instead
+ for tile in chunk {
+ let tile_rect = tile.local_rect.translate(-prim_rect.min.to_vector());
+ gpu_blocks.push(tile_rect.into());
+ gpu_blocks.push(GpuBlockData::EMPTY);
+ }
+
+ let gpu_handle = gpu_cache.push_per_frame_blocks(&gpu_blocks);
+ let prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: image_instance.tight_local_clip_rect,
+ specific_prim_address: gpu_cache.get_address(&gpu_handle),
+ transform_id,
+ };
+ let prim_header_index = prim_headers.push(&prim_header, z_id, prim_user_data);
+
+ for (i, tile) in chunk.iter().enumerate() {
+ let (uv_rect_address, texture) = match render_tasks.resolve_location(tile.src_color, gpu_cache) {
+ Some(result) => result,
+ None => {
+ return;
+ }
+ };
+
+ let textures = BatchTextures::prim_textured(
+ texture,
+ clip_mask_texture_id,
+ );
+
+ let batch_key = BatchKey {
+ blend_mode,
+ kind: BatchKind::Brush(BrushBatchKind::Image(texture.image_buffer_kind())),
+ textures,
+ };
+
+ self.add_brush_instance_to_batches(
+ batch_key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ i as i32,
+ tile.edge_flags,
+ clip_task_address,
+ brush_flags | BrushFlags::SEGMENT_RELATIVE | BrushFlags::PERSPECTIVE_INTERPOLATION,
+ prim_header_index,
+ uv_rect_address.as_int(),
+ );
+ }
+ }
+ }
+ }
+ PrimitiveInstanceKind::LinearGradient { data_handle, ref visible_tiles_range, .. } => {
+ let prim_data = &ctx.data_stores.linear_grad[data_handle];
+
+ let mut prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: prim_info.clip_chain.local_clip_rect,
+ specific_prim_address: GpuCacheAddress::INVALID,
+ transform_id,
+ };
+
+ let blend_mode = if !prim_data.opacity.is_opaque ||
+ prim_info.clip_task_index != ClipTaskIndex::INVALID ||
+ transform_kind == TransformedRectKind::Complex ||
+ is_anti_aliased
+ {
+ BlendMode::PremultipliedAlpha
+ } else {
+ BlendMode::None
+ };
+
+ let user_data = [extra_prim_gpu_address.unwrap(), 0, 0, 0];
+
+ if visible_tiles_range.is_empty() {
+ let batch_params = BrushBatchParameters::shared(
+ BrushBatchKind::LinearGradient,
+ TextureSet::UNTEXTURED,
+ user_data,
+ 0,
+ );
+
+ prim_header.specific_prim_address = gpu_cache.get_address(&prim_data.gpu_cache_handle);
+
+ let prim_header_index = prim_headers.push(&prim_header, z_id, user_data);
+
+ let segments = if prim_data.brush_segments.is_empty() {
+ None
+ } else {
+ Some(prim_data.brush_segments.as_slice())
+ };
+ self.add_segmented_prim_to_batch(
+ segments,
+ prim_data.opacity,
+ &batch_params,
+ blend_mode,
+ batch_features,
+ brush_flags,
+ prim_data.edge_aa_mask,
+ prim_header_index,
+ bounding_rect,
+ transform_kind,
+ z_id,
+ prim_info.clip_task_index,
+ ctx,
+ render_tasks,
+ );
+ } else {
+ let visible_tiles = &ctx.scratch.gradient_tiles[*visible_tiles_range];
+
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ let key = BatchKey {
+ blend_mode,
+ kind: BatchKind::Brush(BrushBatchKind::LinearGradient),
+ textures: BatchTextures::prim_untextured(clip_mask_texture_id),
+ };
+
+ for tile in visible_tiles {
+ let tile_prim_header = PrimitiveHeader {
+ specific_prim_address: gpu_cache.get_address(&tile.handle),
+ local_rect: tile.local_rect,
+ local_clip_rect: tile.local_clip_rect,
+ ..prim_header
+ };
+ let prim_header_index = prim_headers.push(&tile_prim_header, z_id, user_data);
+
+ self.add_brush_instance_to_batches(
+ key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ prim_data.edge_aa_mask,
+ clip_task_address,
+ brush_flags | BrushFlags::PERSPECTIVE_INTERPOLATION,
+ prim_header_index,
+ 0,
+ );
+ }
+ }
+ }
+ PrimitiveInstanceKind::CachedLinearGradient { data_handle, ref visible_tiles_range, .. } => {
+ let prim_data = &ctx.data_stores.linear_grad[data_handle];
+ let common_data = &prim_data.common;
+
+ let src_color = render_tasks.resolve_location(prim_data.src_color, gpu_cache);
+
+ let (uv_rect_address, texture_source) = match src_color {
+ Some(src) => src,
+ None => {
+ return;
+ }
+ };
+
+ let textures = TextureSet::prim_textured(texture_source);
+
+ let prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: prim_info.clip_chain.local_clip_rect,
+ specific_prim_address: gpu_cache.get_address(&common_data.gpu_cache_handle),
+ transform_id,
+ };
+
+ let prim_user_data = ImageBrushData {
+ color_mode: ShaderColorMode::Image,
+ alpha_type: AlphaType::PremultipliedAlpha,
+ raster_space: RasterizationSpace::Local,
+ opacity: 1.0,
+ }.encode();
+
+ let blend_mode = if !common_data.opacity.is_opaque ||
+ prim_info.clip_task_index != ClipTaskIndex::INVALID ||
+ transform_kind == TransformedRectKind::Complex ||
+ is_anti_aliased
+ {
+ BlendMode::PremultipliedAlpha
+ } else {
+ BlendMode::None
+ };
+
+ let batch_kind = BrushBatchKind::Image(texture_source.image_buffer_kind());
+
+ if visible_tiles_range.is_empty() {
+ let batch_params = BrushBatchParameters::shared(
+ batch_kind,
+ textures,
+ prim_user_data,
+ uv_rect_address.as_int(),
+ );
+
+ let segments = if prim_data.brush_segments.is_empty() {
+ None
+ } else {
+ Some(&prim_data.brush_segments[..])
+ };
+
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ batch_params.prim_user_data,
+ );
+
+ self.add_segmented_prim_to_batch(
+ segments,
+ common_data.opacity,
+ &batch_params,
+ blend_mode,
+ batch_features,
+ brush_flags,
+ common_data.edge_aa_mask,
+ prim_header_index,
+ bounding_rect,
+ transform_kind,
+ z_id,
+ prim_info.clip_task_index,
+ ctx,
+ render_tasks,
+ );
+ } else {
+ let visible_tiles = &ctx.scratch.gradient_tiles[*visible_tiles_range];
+
+ let (clip_task_address, clip_mask) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ let batch_key = BatchKey {
+ blend_mode,
+ kind: BatchKind::Brush(batch_kind),
+ textures: BatchTextures {
+ input: textures,
+ clip_mask,
+ },
+ };
+
+ for tile in visible_tiles {
+ let tile_prim_header = PrimitiveHeader {
+ local_rect: tile.local_rect,
+ local_clip_rect: tile.local_clip_rect,
+ ..prim_header
+ };
+ let prim_header_index = prim_headers.push(&tile_prim_header, z_id, prim_user_data);
+
+ self.add_brush_instance_to_batches(
+ batch_key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ prim_data.edge_aa_mask,
+ clip_task_address,
+ brush_flags | BrushFlags::PERSPECTIVE_INTERPOLATION,
+ prim_header_index,
+ uv_rect_address.as_int(),
+ );
+ }
+ }
+ }
+ PrimitiveInstanceKind::RadialGradient { data_handle, ref visible_tiles_range, .. } => {
+ let prim_data = &ctx.data_stores.radial_grad[data_handle];
+ let common_data = &prim_data.common;
+
+ let src_color = render_tasks.resolve_location(prim_data.src_color, gpu_cache);
+
+ let (uv_rect_address, texture_source) = match src_color {
+ Some(src) => src,
+ None => {
+ return;
+ }
+ };
+
+ let textures = TextureSet::prim_textured(texture_source);
+
+ let prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: prim_info.clip_chain.local_clip_rect,
+ specific_prim_address: gpu_cache.get_address(&common_data.gpu_cache_handle),
+ transform_id,
+ };
+
+ let prim_user_data = ImageBrushData {
+ color_mode: ShaderColorMode::Image,
+ alpha_type: AlphaType::PremultipliedAlpha,
+ raster_space: RasterizationSpace::Local,
+ opacity: 1.0,
+ }.encode();
+
+
+ let blend_mode = if !common_data.opacity.is_opaque ||
+ prim_info.clip_task_index != ClipTaskIndex::INVALID ||
+ transform_kind == TransformedRectKind::Complex ||
+ is_anti_aliased
+ {
+ BlendMode::PremultipliedAlpha
+ } else {
+ BlendMode::None
+ };
+
+ let batch_kind = BrushBatchKind::Image(texture_source.image_buffer_kind());
+
+ if visible_tiles_range.is_empty() {
+ let batch_params = BrushBatchParameters::shared(
+ batch_kind,
+ textures,
+ prim_user_data,
+ uv_rect_address.as_int(),
+ );
+
+ let segments = if prim_data.brush_segments.is_empty() {
+ None
+ } else {
+ Some(&prim_data.brush_segments[..])
+ };
+
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ batch_params.prim_user_data,
+ );
+
+ self.add_segmented_prim_to_batch(
+ segments,
+ common_data.opacity,
+ &batch_params,
+ blend_mode,
+ batch_features,
+ brush_flags,
+ prim_data.edge_aa_mask,
+ prim_header_index,
+ bounding_rect,
+ transform_kind,
+ z_id,
+ prim_info.clip_task_index,
+ ctx,
+ render_tasks,
+ );
+ } else {
+ let visible_tiles = &ctx.scratch.gradient_tiles[*visible_tiles_range];
+
+ let (clip_task_address, clip_mask) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ let batch_key = BatchKey {
+ blend_mode,
+ kind: BatchKind::Brush(batch_kind),
+ textures: BatchTextures {
+ input: textures,
+ clip_mask,
+ },
+ };
+
+ for tile in visible_tiles {
+ let tile_prim_header = PrimitiveHeader {
+ local_rect: tile.local_rect,
+ local_clip_rect: tile.local_clip_rect,
+ ..prim_header
+ };
+ let prim_header_index = prim_headers.push(&tile_prim_header, z_id, prim_user_data);
+
+ self.add_brush_instance_to_batches(
+ batch_key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ prim_data.edge_aa_mask,
+ clip_task_address,
+ brush_flags | BrushFlags::PERSPECTIVE_INTERPOLATION,
+ prim_header_index,
+ uv_rect_address.as_int(),
+ );
+ }
+ }
+
+ }
+ PrimitiveInstanceKind::ConicGradient { data_handle, ref visible_tiles_range, .. } => {
+ let prim_data = &ctx.data_stores.conic_grad[data_handle];
+ let common_data = &prim_data.common;
+
+ let src_color = render_tasks.resolve_location(prim_data.src_color, gpu_cache);
+
+ let (uv_rect_address, texture_source) = match src_color {
+ Some(src) => src,
+ None => {
+ return;
+ }
+ };
+
+ let textures = TextureSet::prim_textured(texture_source);
+
+ let prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: prim_info.clip_chain.local_clip_rect,
+ specific_prim_address: gpu_cache.get_address(&common_data.gpu_cache_handle),
+ transform_id,
+ };
+
+ let prim_user_data = ImageBrushData {
+ color_mode: ShaderColorMode::Image,
+ alpha_type: AlphaType::PremultipliedAlpha,
+ raster_space: RasterizationSpace::Local,
+ opacity: 1.0,
+ }.encode();
+
+
+ let blend_mode = if !common_data.opacity.is_opaque ||
+ prim_info.clip_task_index != ClipTaskIndex::INVALID ||
+ transform_kind == TransformedRectKind::Complex ||
+ is_anti_aliased
+ {
+ BlendMode::PremultipliedAlpha
+ } else {
+ BlendMode::None
+ };
+
+ let batch_kind = BrushBatchKind::Image(texture_source.image_buffer_kind());
+
+ if visible_tiles_range.is_empty() {
+ let batch_params = BrushBatchParameters::shared(
+ batch_kind,
+ textures,
+ prim_user_data,
+ uv_rect_address.as_int(),
+ );
+
+ let segments = if prim_data.brush_segments.is_empty() {
+ None
+ } else {
+ Some(&prim_data.brush_segments[..])
+ };
+
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ batch_params.prim_user_data,
+ );
+
+ self.add_segmented_prim_to_batch(
+ segments,
+ common_data.opacity,
+ &batch_params,
+ blend_mode,
+ batch_features,
+ brush_flags,
+ prim_data.edge_aa_mask,
+ prim_header_index,
+ bounding_rect,
+ transform_kind,
+ z_id,
+ prim_info.clip_task_index,
+ ctx,
+ render_tasks,
+ );
+ } else {
+ let visible_tiles = &ctx.scratch.gradient_tiles[*visible_tiles_range];
+
+ let (clip_task_address, clip_mask) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ let batch_key = BatchKey {
+ blend_mode,
+ kind: BatchKind::Brush(batch_kind),
+ textures: BatchTextures {
+ input: textures,
+ clip_mask,
+ },
+ };
+
+ for tile in visible_tiles {
+ let tile_prim_header = PrimitiveHeader {
+ local_rect: tile.local_rect,
+ local_clip_rect: tile.local_clip_rect,
+ ..prim_header
+ };
+ let prim_header_index = prim_headers.push(&tile_prim_header, z_id, prim_user_data);
+
+ self.add_brush_instance_to_batches(
+ batch_key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ prim_data.edge_aa_mask,
+ clip_task_address,
+ brush_flags | BrushFlags::PERSPECTIVE_INTERPOLATION,
+ prim_header_index,
+ uv_rect_address.as_int(),
+ );
+ }
+ }
+ }
+ PrimitiveInstanceKind::BackdropCapture { .. } => {}
+ PrimitiveInstanceKind::BackdropRender { pic_index, .. } => {
+ let prim_cache_address = gpu_cache.get_address(&ctx.globals.default_image_handle);
+ let blend_mode = BlendMode::PremultipliedAlpha;
+ let pic_task_id = ctx.prim_store.pictures[pic_index.0].primary_render_task_id;
+
+ let prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: prim_info.clip_chain.local_clip_rect,
+ specific_prim_address: prim_cache_address,
+ transform_id,
+ };
+
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ let kind = BatchKind::Brush(
+ BrushBatchKind::Image(ImageBufferKind::Texture2D)
+ );
+ let (_, texture) = render_tasks.resolve_location(
+ pic_task_id,
+ gpu_cache,
+ ).unwrap();
+ let textures = BatchTextures::prim_textured(
+ texture,
+ clip_mask_texture_id,
+ );
+ let key = BatchKey::new(
+ kind,
+ blend_mode,
+ textures,
+ );
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ ImageBrushData {
+ color_mode: ShaderColorMode::Image,
+ alpha_type: AlphaType::PremultipliedAlpha,
+ raster_space: RasterizationSpace::Screen,
+ opacity: 1.0,
+ }.encode(),
+ );
+
+ let pic_task = &render_tasks[pic_task_id.unwrap()];
+ let pic_info = match pic_task.kind {
+ RenderTaskKind::Picture(ref info) => info,
+ _ => panic!("bug: not a picture"),
+ };
+ let target_rect = pic_task.get_target_rect();
+
+ let backdrop_rect = DeviceRect::from_origin_and_size(
+ pic_info.content_origin,
+ target_rect.size().to_f32(),
+ );
+
+ let map_prim_to_backdrop = SpaceMapper::new_with_target(
+ pic_info.surface_spatial_node_index,
+ prim_spatial_node_index,
+ WorldRect::max_rect(),
+ ctx.spatial_tree,
+ );
+
+ let points = [
+ map_prim_to_backdrop.map_point(prim_rect.top_left()),
+ map_prim_to_backdrop.map_point(prim_rect.top_right()),
+ map_prim_to_backdrop.map_point(prim_rect.bottom_left()),
+ map_prim_to_backdrop.map_point(prim_rect.bottom_right()),
+ ];
+
+ if points.iter().any(|p| p.is_none()) {
+ return;
+ }
+
+ let uvs = [
+ calculate_screen_uv(points[0].unwrap() * pic_info.device_pixel_scale, backdrop_rect),
+ calculate_screen_uv(points[1].unwrap() * pic_info.device_pixel_scale, backdrop_rect),
+ calculate_screen_uv(points[2].unwrap() * pic_info.device_pixel_scale, backdrop_rect),
+ calculate_screen_uv(points[3].unwrap() * pic_info.device_pixel_scale, backdrop_rect),
+ ];
+
+ // TODO (gw): This is a hack that provides the GPU cache blocks for an
+ // ImageSource. We should update the GPU cache interfaces to
+ // allow pushing per-frame blocks via a request interface.
+ let gpu_blocks = &[
+ GpuBlockData::from([
+ target_rect.min.x as f32,
+ target_rect.min.y as f32,
+ target_rect.max.x as f32,
+ target_rect.max.y as f32,
+ ]),
+ GpuBlockData::from([0.0; 4]),
+ GpuBlockData::from(uvs[0]),
+ GpuBlockData::from(uvs[1]),
+ GpuBlockData::from(uvs[2]),
+ GpuBlockData::from(uvs[3]),
+ ];
+ let uv_rect_handle = gpu_cache.push_per_frame_blocks(gpu_blocks);
+
+ self.add_brush_instance_to_batches(
+ key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ EdgeAaSegmentMask::all(),
+ clip_task_address,
+ brush_flags,
+ prim_header_index,
+ uv_rect_handle.as_int(gpu_cache),
+ );
+ }
+ }
+ }
+
+ /// Add a single segment instance to a batch.
+ ///
+ /// `edge_aa_mask` Specifies the edges that are *allowed* to have anti-aliasing, if and only
+ /// if the segments enable it.
+ /// In other words passing EdgeAaSegmentFlags::all() does not necessarily mean all edges will
+ /// be anti-aliased, only that they could be.
+ fn add_segment_to_batch(
+ &mut self,
+ segment: &BrushSegment,
+ segment_data: &SegmentInstanceData,
+ segment_index: i32,
+ batch_kind: BrushBatchKind,
+ prim_header_index: PrimitiveHeaderIndex,
+ alpha_blend_mode: BlendMode,
+ features: BatchFeatures,
+ brush_flags: BrushFlags,
+ edge_aa_mask: EdgeAaSegmentMask,
+ bounding_rect: &PictureRect,
+ transform_kind: TransformedRectKind,
+ z_id: ZBufferId,
+ prim_opacity: PrimitiveOpacity,
+ clip_task_index: ClipTaskIndex,
+ ctx: &RenderTargetContext,
+ render_tasks: &RenderTaskGraph,
+ ) {
+ debug_assert!(clip_task_index != ClipTaskIndex::INVALID);
+
+ // Get GPU address of clip task for this segment, or None if
+ // the entire segment is clipped out.
+ if let Some((clip_task_address, clip_mask)) = ctx.get_clip_task_and_texture(
+ clip_task_index,
+ segment_index,
+ render_tasks,
+ ) {
+ // If a got a valid (or OPAQUE) clip task address, add the segment.
+ let is_inner = segment.edge_flags.is_empty();
+ let needs_blending = !prim_opacity.is_opaque ||
+ clip_task_address != OPAQUE_TASK_ADDRESS ||
+ (!is_inner && transform_kind == TransformedRectKind::Complex) ||
+ brush_flags.contains(BrushFlags::FORCE_AA);
+
+ let textures = BatchTextures {
+ input: segment_data.textures,
+ clip_mask,
+ };
+
+ let batch_key = BatchKey {
+ blend_mode: if needs_blending { alpha_blend_mode } else { BlendMode::None },
+ kind: BatchKind::Brush(batch_kind),
+ textures,
+ };
+
+ self.add_brush_instance_to_batches(
+ batch_key,
+ features,
+ bounding_rect,
+ z_id,
+ segment_index,
+ segment.edge_flags & edge_aa_mask,
+ clip_task_address,
+ brush_flags | BrushFlags::PERSPECTIVE_INTERPOLATION | segment.brush_flags,
+ prim_header_index,
+ segment_data.specific_resource_address,
+ );
+ }
+ }
+
+ /// Add any segment(s) from a brush to batches.
+ ///
+ /// `edge_aa_mask` Specifies the edges that are *allowed* to have anti-aliasing, if and only
+ /// if the segments enable it.
+ /// In other words passing EdgeAaSegmentFlags::all() does not necessarily mean all edges will
+ /// be anti-aliased, only that they could be.
+ fn add_segmented_prim_to_batch(
+ &mut self,
+ brush_segments: Option<&[BrushSegment]>,
+ prim_opacity: PrimitiveOpacity,
+ params: &BrushBatchParameters,
+ blend_mode: BlendMode,
+ features: BatchFeatures,
+ brush_flags: BrushFlags,
+ edge_aa_mask: EdgeAaSegmentMask,
+ prim_header_index: PrimitiveHeaderIndex,
+ bounding_rect: &PictureRect,
+ transform_kind: TransformedRectKind,
+ z_id: ZBufferId,
+ clip_task_index: ClipTaskIndex,
+ ctx: &RenderTargetContext,
+ render_tasks: &RenderTaskGraph,
+ ) {
+ match (brush_segments, &params.segment_data) {
+ (Some(ref brush_segments), SegmentDataKind::Instanced(ref segment_data)) => {
+ // In this case, we have both a list of segments, and a list of
+ // per-segment instance data. Zip them together to build batches.
+ debug_assert_eq!(brush_segments.len(), segment_data.len());
+ for (segment_index, (segment, segment_data)) in brush_segments
+ .iter()
+ .zip(segment_data.iter())
+ .enumerate()
+ {
+ self.add_segment_to_batch(
+ segment,
+ segment_data,
+ segment_index as i32,
+ params.batch_kind,
+ prim_header_index,
+ blend_mode,
+ features,
+ brush_flags,
+ edge_aa_mask,
+ bounding_rect,
+ transform_kind,
+ z_id,
+ prim_opacity,
+ clip_task_index,
+ ctx,
+ render_tasks,
+ );
+ }
+ }
+ (Some(ref brush_segments), SegmentDataKind::Shared(ref segment_data)) => {
+ // A list of segments, but the per-segment data is common
+ // between all segments.
+ for (segment_index, segment) in brush_segments
+ .iter()
+ .enumerate()
+ {
+ self.add_segment_to_batch(
+ segment,
+ segment_data,
+ segment_index as i32,
+ params.batch_kind,
+ prim_header_index,
+ blend_mode,
+ features,
+ brush_flags,
+ edge_aa_mask,
+ bounding_rect,
+ transform_kind,
+ z_id,
+ prim_opacity,
+ clip_task_index,
+ ctx,
+ render_tasks,
+ );
+ }
+ }
+ (None, SegmentDataKind::Shared(ref segment_data)) => {
+ // No segments, and thus no per-segment instance data.
+ // Note: the blend mode already takes opacity into account
+
+ let (clip_task_address, clip_mask) = ctx.get_prim_clip_task_and_texture(
+ clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ let textures = BatchTextures {
+ input: segment_data.textures,
+ clip_mask,
+ };
+
+ let batch_key = BatchKey {
+ blend_mode,
+ kind: BatchKind::Brush(params.batch_kind),
+ textures,
+ };
+
+ self.add_brush_instance_to_batches(
+ batch_key,
+ features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ edge_aa_mask,
+ clip_task_address,
+ brush_flags | BrushFlags::PERSPECTIVE_INTERPOLATION,
+ prim_header_index,
+ segment_data.specific_resource_address,
+ );
+ }
+ (None, SegmentDataKind::Instanced(..)) => {
+ // We should never hit the case where there are no segments,
+ // but a list of segment instance data.
+ unreachable!();
+ }
+ }
+ }
+}
+
+/// Either a single texture / user data for all segments,
+/// or a list of one per segment.
+enum SegmentDataKind {
+ Shared(SegmentInstanceData),
+ Instanced(SmallVec<[SegmentInstanceData; 8]>),
+}
+
+/// The parameters that are specific to a kind of brush,
+/// used by the common method to add a brush to batches.
+struct BrushBatchParameters {
+ batch_kind: BrushBatchKind,
+ prim_user_data: [i32; 4],
+ segment_data: SegmentDataKind,
+}
+
+impl BrushBatchParameters {
+ /// This brush instance has a list of per-segment
+ /// instance data.
+ fn instanced(
+ batch_kind: BrushBatchKind,
+ prim_user_data: [i32; 4],
+ segment_data: SmallVec<[SegmentInstanceData; 8]>,
+ ) -> Self {
+ BrushBatchParameters {
+ batch_kind,
+ prim_user_data,
+ segment_data: SegmentDataKind::Instanced(segment_data),
+ }
+ }
+
+ /// This brush instance shares the per-segment data
+ /// across all segments.
+ fn shared(
+ batch_kind: BrushBatchKind,
+ textures: TextureSet,
+ prim_user_data: [i32; 4],
+ specific_resource_address: i32,
+ ) -> Self {
+ BrushBatchParameters {
+ batch_kind,
+ prim_user_data,
+ segment_data: SegmentDataKind::Shared(
+ SegmentInstanceData {
+ textures,
+ specific_resource_address,
+ }
+ ),
+ }
+ }
+}
+
+/// A list of clip instances to be drawn into a target.
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ClipBatchList {
+ /// Rectangle draws fill up the rectangles with rounded corners.
+ pub slow_rectangles: Vec<ClipMaskInstanceRect>,
+ pub fast_rectangles: Vec<ClipMaskInstanceRect>,
+ /// Image draws apply the image masking.
+ pub images: FastHashMap<(TextureSource, Option<DeviceIntRect>), Vec<ClipMaskInstanceImage>>,
+ pub box_shadows: FastHashMap<TextureSource, Vec<ClipMaskInstanceBoxShadow>>,
+}
+
+impl ClipBatchList {
+ fn new() -> Self {
+ ClipBatchList {
+ slow_rectangles: Vec::new(),
+ fast_rectangles: Vec::new(),
+ images: FastHashMap::default(),
+ box_shadows: FastHashMap::default(),
+ }
+ }
+}
+
+/// Batcher managing draw calls into the clip mask (in the RT cache).
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ClipBatcher {
+ /// The first clip in each clip task. This will overwrite all pixels
+ /// in the clip region, so we can skip doing a clear and write with
+ /// blending disabled, which is a big performance win on Intel GPUs.
+ pub primary_clips: ClipBatchList,
+ /// Any subsequent clip masks (rare) for a clip task get drawn in
+ /// a second pass with multiplicative blending enabled.
+ pub secondary_clips: ClipBatchList,
+
+ gpu_supports_fast_clears: bool,
+}
+
+impl ClipBatcher {
+ pub fn new(
+ gpu_supports_fast_clears: bool,
+ ) -> Self {
+ ClipBatcher {
+ primary_clips: ClipBatchList::new(),
+ secondary_clips: ClipBatchList::new(),
+ gpu_supports_fast_clears,
+ }
+ }
+
+ pub fn add_clip_region(
+ &mut self,
+ local_pos: LayoutPoint,
+ sub_rect: DeviceRect,
+ clip_data: ClipData,
+ task_origin: DevicePoint,
+ screen_origin: DevicePoint,
+ device_pixel_scale: f32,
+ ) {
+ let instance = ClipMaskInstanceRect {
+ common: ClipMaskInstanceCommon {
+ clip_transform_id: TransformPaletteId::IDENTITY,
+ prim_transform_id: TransformPaletteId::IDENTITY,
+ sub_rect,
+ task_origin,
+ screen_origin,
+ device_pixel_scale,
+ },
+ local_pos,
+ clip_data,
+ };
+
+ self.primary_clips.slow_rectangles.push(instance);
+ }
+
+ /// Where appropriate, draw a clip rectangle as a small series of tiles,
+ /// instead of one large rectangle.
+ fn add_tiled_clip_mask(
+ &mut self,
+ mask_screen_rect: DeviceRect,
+ local_clip_rect: LayoutRect,
+ clip_spatial_node_index: SpatialNodeIndex,
+ spatial_tree: &SpatialTree,
+ world_rect: &WorldRect,
+ global_device_pixel_scale: DevicePixelScale,
+ common: &ClipMaskInstanceCommon,
+ is_first_clip: bool,
+ ) -> bool {
+ // Only try to draw in tiles if the clip mark is big enough.
+ if mask_screen_rect.area() < CLIP_RECTANGLE_AREA_THRESHOLD {
+ return false;
+ }
+
+ let mask_screen_rect_size = mask_screen_rect.size().to_i32();
+ let clip_spatial_node = spatial_tree.get_spatial_node(clip_spatial_node_index);
+
+ // Only support clips that are axis-aligned to the root coordinate space,
+ // for now, to simplify the logic below. This handles the vast majority
+ // of real world cases, but could be expanded in future if needed.
+ if clip_spatial_node.coordinate_system_id != CoordinateSystemId::root() {
+ return false;
+ }
+
+ // Get the world rect of the clip rectangle. If we can't transform it due
+ // to the matrix, just fall back to drawing the entire clip mask.
+ let transform = spatial_tree.get_world_transform(
+ clip_spatial_node_index,
+ );
+ let world_clip_rect = match project_rect(
+ &transform.into_transform(),
+ &local_clip_rect,
+ &world_rect,
+ ) {
+ Some(rect) => rect,
+ None => return false,
+ };
+
+ // Work out how many tiles to draw this clip mask in, stretched across the
+ // device rect of the primitive clip mask.
+ let world_device_rect = world_clip_rect * global_device_pixel_scale;
+ let x_tiles = (mask_screen_rect_size.width + CLIP_RECTANGLE_TILE_SIZE-1) / CLIP_RECTANGLE_TILE_SIZE;
+ let y_tiles = (mask_screen_rect_size.height + CLIP_RECTANGLE_TILE_SIZE-1) / CLIP_RECTANGLE_TILE_SIZE;
+
+ // Because we only run this code path for axis-aligned rects (the root coord system check above),
+ // and only for rectangles (not rounded etc), the world_device_rect is not conservative - we know
+ // that there is no inner_rect, and the world_device_rect should be the real, axis-aligned clip rect.
+ let mask_origin = mask_screen_rect.min.to_vector();
+ let clip_list = self.get_batch_list(is_first_clip);
+
+ for y in 0 .. y_tiles {
+ for x in 0 .. x_tiles {
+ let p0 = DeviceIntPoint::new(
+ x * CLIP_RECTANGLE_TILE_SIZE,
+ y * CLIP_RECTANGLE_TILE_SIZE,
+ );
+ let p1 = DeviceIntPoint::new(
+ (p0.x + CLIP_RECTANGLE_TILE_SIZE).min(mask_screen_rect_size.width),
+ (p0.y + CLIP_RECTANGLE_TILE_SIZE).min(mask_screen_rect_size.height),
+ );
+ let normalized_sub_rect = DeviceIntRect {
+ min: p0,
+ max: p1,
+ }.to_f32();
+ let world_sub_rect = normalized_sub_rect.translate(mask_origin);
+
+ // If the clip rect completely contains this tile rect, then drawing
+ // these pixels would be redundant - since this clip can't possibly
+ // affect the pixels in this tile, skip them!
+ if !world_device_rect.contains_box(&world_sub_rect) {
+ clip_list.slow_rectangles.push(ClipMaskInstanceRect {
+ common: ClipMaskInstanceCommon {
+ sub_rect: normalized_sub_rect,
+ ..*common
+ },
+ local_pos: local_clip_rect.min,
+ clip_data: ClipData::uniform(local_clip_rect.size(), 0.0, ClipMode::Clip),
+ });
+ }
+ }
+ }
+
+ true
+ }
+
+ /// Retrieve the correct clip batch list to append to, depending
+ /// on whether this is the first clip mask for a clip task.
+ fn get_batch_list(
+ &mut self,
+ is_first_clip: bool,
+ ) -> &mut ClipBatchList {
+ if is_first_clip && !self.gpu_supports_fast_clears {
+ &mut self.primary_clips
+ } else {
+ &mut self.secondary_clips
+ }
+ }
+
+ pub fn add(
+ &mut self,
+ clip_node_range: ClipNodeRange,
+ root_spatial_node_index: SpatialNodeIndex,
+ render_tasks: &RenderTaskGraph,
+ gpu_cache: &GpuCache,
+ clip_store: &ClipStore,
+ transforms: &mut TransformPalette,
+ actual_rect: DeviceRect,
+ surface_device_pixel_scale: DevicePixelScale,
+ task_origin: DevicePoint,
+ screen_origin: DevicePoint,
+ ctx: &RenderTargetContext,
+ ) -> bool {
+ let mut is_first_clip = true;
+ let mut clear_to_one = false;
+
+ for i in 0 .. clip_node_range.count {
+ let clip_instance = clip_store.get_instance_from_range(&clip_node_range, i);
+ let clip_node = &ctx.data_stores.clip[clip_instance.handle];
+
+ let clip_transform_id = transforms.get_id(
+ clip_node.item.spatial_node_index,
+ ctx.root_spatial_node_index,
+ ctx.spatial_tree,
+ );
+
+ // For clip mask images, we need to map from the primitive's layout space to
+ // the target space, as the cs_clip_image shader needs to forward transform
+ // the local image bounds, rather than backwards transform the target bounds
+ // as in done in write_clip_tile_vertex.
+ let prim_transform_id = match clip_node.item.kind {
+ ClipItemKind::Image { .. } => {
+ transforms.get_id(
+ clip_node.item.spatial_node_index,
+ root_spatial_node_index,
+ ctx.spatial_tree,
+ )
+ }
+ _ => {
+ transforms.get_id(
+ root_spatial_node_index,
+ ctx.root_spatial_node_index,
+ ctx.spatial_tree,
+ )
+ }
+ };
+
+ let common = ClipMaskInstanceCommon {
+ sub_rect: DeviceRect::from_size(actual_rect.size()),
+ task_origin,
+ screen_origin,
+ device_pixel_scale: surface_device_pixel_scale.0,
+ clip_transform_id,
+ prim_transform_id,
+ };
+
+ let added_clip = match clip_node.item.kind {
+ ClipItemKind::Image { image, rect, .. } => {
+ let request = ImageRequest {
+ key: image,
+ rendering: ImageRendering::Auto,
+ tile: None,
+ };
+
+ let map_local_to_raster = SpaceMapper::new_with_target(
+ root_spatial_node_index,
+ clip_node.item.spatial_node_index,
+ WorldRect::max_rect(),
+ ctx.spatial_tree,
+ );
+
+ let mut add_image = |request: ImageRequest, tile_rect: LayoutRect, sub_rect: DeviceRect| {
+ let cache_item = match ctx.resource_cache.get_cached_image(request) {
+ Ok(item) => item,
+ Err(..) => {
+ warn!("Warnings: skip a image mask");
+ debug!("request: {:?}", request);
+ return;
+ }
+ };
+
+ // If the clip transform is axis-aligned, we can skip any need for scissoring
+ // by clipping the local clip rect with the backwards transformed target bounds.
+ // If it is not axis-aligned, then we pass the local clip rect through unmodified
+ // to the shader and also set up a scissor rect for the overall target bounds to
+ // ensure nothing is drawn outside the target. If for some reason we can't map the
+ // rect back to local space, we also fall back to just using a scissor rectangle.
+ let raster_rect =
+ sub_rect.translate(actual_rect.min.to_vector()) / surface_device_pixel_scale;
+ let (clip_transform_id, local_rect, scissor) = match map_local_to_raster.unmap(&raster_rect) {
+ Some(local_rect)
+ if clip_transform_id.transform_kind() == TransformedRectKind::AxisAligned &&
+ !map_local_to_raster.get_transform().has_perspective_component() => {
+ match local_rect.intersection(&rect) {
+ Some(local_rect) => (clip_transform_id, local_rect, None),
+ None => return,
+ }
+ }
+ _ => {
+ // If for some reason inverting the transform failed, then don't consider
+ // the transform to be axis-aligned if it was.
+ (clip_transform_id.override_transform_kind(TransformedRectKind::Complex),
+ rect,
+ Some(common.sub_rect
+ .translate(task_origin.to_vector())
+ .round_out()
+ .to_i32()))
+ }
+ };
+
+ self.get_batch_list(is_first_clip)
+ .images
+ .entry((cache_item.texture_id, scissor))
+ .or_insert_with(Vec::new)
+ .push(ClipMaskInstanceImage {
+ common: ClipMaskInstanceCommon {
+ sub_rect,
+ clip_transform_id,
+ ..common
+ },
+ resource_address: gpu_cache.get_address(&cache_item.uv_rect_handle),
+ tile_rect,
+ local_rect,
+ });
+ };
+
+ let clip_spatial_node = ctx.spatial_tree.get_spatial_node(clip_node.item.spatial_node_index);
+ let clip_is_axis_aligned = clip_spatial_node.coordinate_system_id == CoordinateSystemId::root();
+
+ if clip_instance.has_visible_tiles() {
+ let sub_rect_bounds = actual_rect.size().into();
+
+ for tile in clip_store.visible_mask_tiles(&clip_instance) {
+ let tile_sub_rect = if clip_is_axis_aligned {
+ let tile_raster_rect = map_local_to_raster
+ .map(&tile.tile_rect)
+ .expect("bug: should always map as axis-aligned");
+ let tile_device_rect = tile_raster_rect * surface_device_pixel_scale;
+ tile_device_rect
+ .translate(-actual_rect.min.to_vector())
+ .round_out()
+ .intersection(&sub_rect_bounds)
+ } else {
+ Some(common.sub_rect)
+ };
+
+ if let Some(tile_sub_rect) = tile_sub_rect {
+ assert!(sub_rect_bounds.contains_box(&tile_sub_rect));
+ add_image(
+ request.with_tile(tile.tile_offset),
+ tile.tile_rect,
+ tile_sub_rect,
+ )
+ }
+ }
+ } else {
+ add_image(request, rect, common.sub_rect)
+ }
+
+ // If this is the first clip and either there is a transform or the image rect
+ // doesn't cover the entire task, then request a clear so that pixels outside
+ // the image boundaries will be properly initialized.
+ if is_first_clip &&
+ (!clip_is_axis_aligned ||
+ !(map_local_to_raster.map(&rect).expect("bug: should always map as axis-aligned")
+ * surface_device_pixel_scale).contains_box(&actual_rect)) {
+ clear_to_one = true;
+ }
+
+ true
+ }
+ ClipItemKind::BoxShadow { ref source } => {
+ let task_id = source
+ .render_task
+ .expect("bug: render task handle not allocated");
+ let (uv_rect_address, texture) = render_tasks.resolve_location(task_id, gpu_cache).unwrap();
+
+ self.get_batch_list(is_first_clip)
+ .box_shadows
+ .entry(texture)
+ .or_insert_with(Vec::new)
+ .push(ClipMaskInstanceBoxShadow {
+ common,
+ resource_address: uv_rect_address,
+ shadow_data: BoxShadowData {
+ src_rect_size: source.original_alloc_size,
+ clip_mode: source.clip_mode as i32,
+ stretch_mode_x: source.stretch_mode_x as i32,
+ stretch_mode_y: source.stretch_mode_y as i32,
+ dest_rect: source.prim_shadow_rect,
+ },
+ });
+
+ true
+ }
+ ClipItemKind::Rectangle { rect, mode: ClipMode::ClipOut } => {
+ self.get_batch_list(is_first_clip)
+ .slow_rectangles
+ .push(ClipMaskInstanceRect {
+ common,
+ local_pos: rect.min,
+ clip_data: ClipData::uniform(rect.size(), 0.0, ClipMode::ClipOut),
+ });
+
+ true
+ }
+ ClipItemKind::Rectangle { rect, mode: ClipMode::Clip } => {
+ if clip_instance.flags.contains(ClipNodeFlags::SAME_COORD_SYSTEM) {
+ false
+ } else {
+ if self.add_tiled_clip_mask(
+ actual_rect,
+ rect,
+ clip_node.item.spatial_node_index,
+ ctx.spatial_tree,
+ &ctx.screen_world_rect,
+ ctx.global_device_pixel_scale,
+ &common,
+ is_first_clip,
+ ) {
+ clear_to_one |= is_first_clip;
+ } else {
+ self.get_batch_list(is_first_clip)
+ .slow_rectangles
+ .push(ClipMaskInstanceRect {
+ common,
+ local_pos: rect.min,
+ clip_data: ClipData::uniform(rect.size(), 0.0, ClipMode::Clip),
+ });
+ }
+
+ true
+ }
+ }
+ ClipItemKind::RoundedRectangle { rect, ref radius, mode, .. } => {
+ let batch_list = self.get_batch_list(is_first_clip);
+ let instance = ClipMaskInstanceRect {
+ common,
+ local_pos: rect.min,
+ clip_data: ClipData::rounded_rect(rect.size(), radius, mode),
+ };
+ if clip_instance.flags.contains(ClipNodeFlags::USE_FAST_PATH) {
+ batch_list.fast_rectangles.push(instance);
+ } else {
+ batch_list.slow_rectangles.push(instance);
+ }
+
+ true
+ }
+ };
+
+ is_first_clip &= !added_clip;
+ }
+
+ clear_to_one
+ }
+}
+
+impl<'a, 'rc> RenderTargetContext<'a, 'rc> {
+ /// Retrieve the GPU task address for a given clip task instance.
+ /// Returns None if the segment was completely clipped out.
+ /// Returns Some(OPAQUE_TASK_ADDRESS) if no clip mask is needed.
+ /// Returns Some(task_address) if there was a valid clip mask.
+ fn get_clip_task_and_texture(
+ &self,
+ clip_task_index: ClipTaskIndex,
+ offset: i32,
+ render_tasks: &RenderTaskGraph,
+ ) -> Option<(RenderTaskAddress, TextureSource)> {
+ match self.scratch.clip_mask_instances[clip_task_index.0 as usize + offset as usize] {
+ ClipMaskKind::Mask(task_id) => {
+ Some((
+ task_id.into(),
+ TextureSource::TextureCache(
+ render_tasks[task_id].get_target_texture(),
+ Swizzle::default(),
+ )
+ ))
+ }
+ ClipMaskKind::None => {
+ Some((OPAQUE_TASK_ADDRESS, TextureSource::Invalid))
+ }
+ ClipMaskKind::Clipped => {
+ None
+ }
+ }
+ }
+
+ /// Helper function to get the clip task address for a
+ /// non-segmented primitive.
+ fn get_prim_clip_task_and_texture(
+ &self,
+ clip_task_index: ClipTaskIndex,
+ render_tasks: &RenderTaskGraph,
+ ) -> Option<(RenderTaskAddress, TextureSource)> {
+ self.get_clip_task_and_texture(
+ clip_task_index,
+ 0,
+ render_tasks,
+ )
+ }
+}
+
+pub fn add_quad_to_batch<F>(
+ render_task_address: RenderTaskAddress,
+ transform_id: TransformPaletteId,
+ gpu_buffer_address: GpuBufferAddress,
+ quad_flags: QuadFlags,
+ edge_flags: EdgeAaSegmentMask,
+ segment_index: u8,
+ task_id: RenderTaskId,
+ z_id: ZBufferId,
+ render_tasks: &RenderTaskGraph,
+ mut f: F,
+) where F: FnMut(BatchKey, PrimitiveInstanceData) {
+
+ #[repr(u8)]
+ enum PartIndex {
+ Center = 0,
+ Left = 1,
+ Top = 2,
+ Right = 3,
+ Bottom = 4,
+ }
+
+ let texture = match task_id {
+ RenderTaskId::INVALID => {
+ TextureSource::Invalid
+ }
+ _ => {
+ let texture = render_tasks
+ .resolve_texture(task_id)
+ .expect("bug: valid task id must be resolvable");
+
+ texture
+ }
+ };
+
+ let textures = BatchTextures::prim_textured(
+ texture,
+ TextureSource::Invalid,
+ );
+
+ let default_blend_mode = if quad_flags.contains(QuadFlags::IS_OPAQUE) && task_id == RenderTaskId::INVALID {
+ BlendMode::None
+ } else {
+ BlendMode::PremultipliedAlpha
+ };
+
+ let inner_batch_key = BatchKey {
+ blend_mode: default_blend_mode,
+ kind: BatchKind::Primitive,
+ textures,
+ };
+
+ let edge_batch_key = BatchKey {
+ blend_mode: BlendMode::PremultipliedAlpha,
+ kind: BatchKind::Primitive,
+ textures,
+ };
+
+ let edge_flags_bits = edge_flags.bits();
+
+ let main_instance = QuadInstance {
+ render_task_address,
+ prim_address: gpu_buffer_address,
+ z_id,
+ transform_id,
+ edge_flags: edge_flags_bits,
+ quad_flags: quad_flags.bits(),
+ part_index: PartIndex::Center as u8,
+ segment_index,
+ };
+
+ if edge_flags.contains(EdgeAaSegmentMask::LEFT) {
+ let instance = QuadInstance {
+ part_index: PartIndex::Left as u8,
+ ..main_instance
+ };
+ f(edge_batch_key, instance.into());
+ }
+ if edge_flags.contains(EdgeAaSegmentMask::RIGHT) {
+ let instance = QuadInstance {
+ part_index: PartIndex::Top as u8,
+ ..main_instance
+ };
+ f(edge_batch_key, instance.into());
+ }
+ if edge_flags.contains(EdgeAaSegmentMask::TOP) {
+ let instance = QuadInstance {
+ part_index: PartIndex::Right as u8,
+ ..main_instance
+ };
+ f(edge_batch_key, instance.into());
+ }
+ if edge_flags.contains(EdgeAaSegmentMask::BOTTOM) {
+ let instance = QuadInstance {
+ part_index: PartIndex::Bottom as u8,
+ ..main_instance
+ };
+ f(edge_batch_key, instance.into());
+ }
+
+ let instance = QuadInstance {
+ ..main_instance
+ };
+ f(inner_batch_key, instance.into());
+}
diff --git a/gfx/wr/webrender/src/border.rs b/gfx/wr/webrender/src/border.rs
new file mode 100644
index 0000000000..d811f5edc2
--- /dev/null
+++ b/gfx/wr/webrender/src/border.rs
@@ -0,0 +1,1473 @@
+/* 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 api::{BorderRadius, BorderSide, BorderStyle, ColorF, ColorU};
+use api::{NormalBorder as ApiNormalBorder, RepeatMode};
+use api::units::*;
+use crate::clip::ClipNodeId;
+use crate::ellipse::Ellipse;
+use euclid::vec2;
+use crate::scene_building::SceneBuilder;
+use crate::spatial_tree::SpatialNodeIndex;
+use crate::gpu_types::{BorderInstance, BorderSegment, BrushFlags};
+use crate::prim_store::{BorderSegmentInfo, BrushSegment, NinePatchDescriptor};
+use crate::prim_store::borders::{NormalBorderPrim, NormalBorderData};
+use crate::util::{lerp, RectHelpers};
+use crate::internal_types::LayoutPrimitiveInfo;
+use crate::segment::EdgeAaSegmentMask;
+
+// Using 2048 as the maximum radius in device space before which we
+// start stretching is up for debate.
+// the value must be chosen so that the corners will not use an
+// unreasonable amount of memory but should allow crisp corners in the
+// common cases.
+
+/// Maximum resolution in device pixels at which borders are rasterized.
+pub const MAX_BORDER_RESOLUTION: u32 = 2048;
+/// Maximum number of dots or dashes per segment to avoid freezing and filling up
+/// memory with unreasonable inputs. It would be better to address this by not building
+/// a list of per-dot information in the first place.
+pub const MAX_DASH_COUNT: u32 = 2048;
+
+// TODO(gw): Perhaps there is a better way to store
+// the border cache key than duplicating
+// all the border structs with hashable
+// variants...
+
+#[derive(Copy, Clone, Debug, Hash, MallocSizeOf, PartialEq, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BorderRadiusAu {
+ pub top_left: LayoutSizeAu,
+ pub top_right: LayoutSizeAu,
+ pub bottom_left: LayoutSizeAu,
+ pub bottom_right: LayoutSizeAu,
+}
+
+impl From<BorderRadius> for BorderRadiusAu {
+ fn from(radius: BorderRadius) -> BorderRadiusAu {
+ BorderRadiusAu {
+ top_left: radius.top_left.to_au(),
+ top_right: radius.top_right.to_au(),
+ bottom_right: radius.bottom_right.to_au(),
+ bottom_left: radius.bottom_left.to_au(),
+ }
+ }
+}
+
+impl From<BorderRadiusAu> for BorderRadius {
+ fn from(radius: BorderRadiusAu) -> Self {
+ BorderRadius {
+ top_left: LayoutSize::from_au(radius.top_left),
+ top_right: LayoutSize::from_au(radius.top_right),
+ bottom_right: LayoutSize::from_au(radius.bottom_right),
+ bottom_left: LayoutSize::from_au(radius.bottom_left),
+ }
+ }
+}
+
+#[derive(Clone, Debug, Hash, MallocSizeOf, PartialEq, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BorderSideAu {
+ pub color: ColorU,
+ pub style: BorderStyle,
+}
+
+impl From<BorderSide> for BorderSideAu {
+ fn from(side: BorderSide) -> Self {
+ BorderSideAu {
+ color: side.color.into(),
+ style: side.style,
+ }
+ }
+}
+
+impl From<BorderSideAu> for BorderSide {
+ fn from(side: BorderSideAu) -> Self {
+ BorderSide {
+ color: side.color.into(),
+ style: side.style,
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Hash, Eq, MallocSizeOf, PartialEq)]
+pub struct NormalBorderAu {
+ pub left: BorderSideAu,
+ pub right: BorderSideAu,
+ pub top: BorderSideAu,
+ pub bottom: BorderSideAu,
+ pub radius: BorderRadiusAu,
+ /// Whether to apply anti-aliasing on the border corners.
+ ///
+ /// Note that for this to be `false` and work, this requires the borders to
+ /// be solid, and no border-radius.
+ pub do_aa: bool,
+}
+
+impl NormalBorderAu {
+ // Construct a border based upon self with color
+ pub fn with_color(&self, color: ColorU) -> Self {
+ let mut b = self.clone();
+ b.left.color = color;
+ b.right.color = color;
+ b.top.color = color;
+ b.bottom.color = color;
+ b
+ }
+}
+
+impl From<ApiNormalBorder> for NormalBorderAu {
+ fn from(border: ApiNormalBorder) -> Self {
+ NormalBorderAu {
+ left: border.left.into(),
+ right: border.right.into(),
+ top: border.top.into(),
+ bottom: border.bottom.into(),
+ radius: border.radius.into(),
+ do_aa: border.do_aa,
+ }
+ }
+}
+
+impl From<NormalBorderAu> for ApiNormalBorder {
+ fn from(border: NormalBorderAu) -> Self {
+ ApiNormalBorder {
+ left: border.left.into(),
+ right: border.right.into(),
+ top: border.top.into(),
+ bottom: border.bottom.into(),
+ radius: border.radius.into(),
+ do_aa: border.do_aa,
+ }
+ }
+}
+
+/// Cache key that uniquely identifies a border
+/// segment in the render task cache.
+#[derive(Clone, Debug, Hash, MallocSizeOf, PartialEq, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BorderSegmentCacheKey {
+ pub size: LayoutSizeAu,
+ pub radius: LayoutSizeAu,
+ pub side0: BorderSideAu,
+ pub side1: BorderSideAu,
+ pub segment: BorderSegment,
+ pub do_aa: bool,
+ pub h_adjacent_corner_outer: LayoutPointAu,
+ pub h_adjacent_corner_radius: LayoutSizeAu,
+ pub v_adjacent_corner_outer: LayoutPointAu,
+ pub v_adjacent_corner_radius: LayoutSizeAu,
+}
+
+pub fn ensure_no_corner_overlap(
+ radius: &mut BorderRadius,
+ size: LayoutSize,
+) {
+ let mut ratio = 1.0;
+ let top_left_radius = &mut radius.top_left;
+ let top_right_radius = &mut radius.top_right;
+ let bottom_right_radius = &mut radius.bottom_right;
+ let bottom_left_radius = &mut radius.bottom_left;
+
+ let sum = top_left_radius.width + top_right_radius.width;
+ if size.width < sum {
+ ratio = f32::min(ratio, size.width / sum);
+ }
+
+ let sum = bottom_left_radius.width + bottom_right_radius.width;
+ if size.width < sum {
+ ratio = f32::min(ratio, size.width / sum);
+ }
+
+ let sum = top_left_radius.height + bottom_left_radius.height;
+ if size.height < sum {
+ ratio = f32::min(ratio, size.height / sum);
+ }
+
+ let sum = top_right_radius.height + bottom_right_radius.height;
+ if size.height < sum {
+ ratio = f32::min(ratio, size.height / sum);
+ }
+
+ if ratio < 1. {
+ top_left_radius.width *= ratio;
+ top_left_radius.height *= ratio;
+
+ top_right_radius.width *= ratio;
+ top_right_radius.height *= ratio;
+
+ bottom_left_radius.width *= ratio;
+ bottom_left_radius.height *= ratio;
+
+ bottom_right_radius.width *= ratio;
+ bottom_right_radius.height *= ratio;
+ }
+}
+
+impl<'a> SceneBuilder<'a> {
+ pub fn add_normal_border(
+ &mut self,
+ info: &LayoutPrimitiveInfo,
+ border: &ApiNormalBorder,
+ widths: LayoutSideOffsets,
+ spatial_node_index: SpatialNodeIndex,
+ clip_node_id: ClipNodeId,
+ ) {
+ let mut border = *border;
+ ensure_no_corner_overlap(&mut border.radius, info.rect.size());
+
+ self.add_primitive(
+ spatial_node_index,
+ clip_node_id,
+ info,
+ Vec::new(),
+ NormalBorderPrim {
+ border: border.into(),
+ widths: widths.to_au(),
+ },
+ );
+ }
+}
+
+pub trait BorderSideHelpers {
+ fn border_color(&self, is_inner_border: bool) -> ColorF;
+}
+
+impl BorderSideHelpers for BorderSide {
+ fn border_color(&self, is_inner_border: bool) -> ColorF {
+ let lighter = match self.style {
+ BorderStyle::Inset => is_inner_border,
+ BorderStyle::Outset => !is_inner_border,
+ _ => return self.color,
+ };
+
+ // The modulate colors below are not part of the specification. They are
+ // derived from the Gecko source code and experimentation, and used to
+ // modulate the colors in order to generate colors for the inset/outset
+ // and groove/ridge border styles.
+ //
+ // NOTE(emilio): Gecko at least takes the background color into
+ // account, should we do the same? Looks a bit annoying for this.
+ //
+ // NOTE(emilio): If you change this algorithm, do the same change on
+ // get_colors_for_side in cs_border_segment.glsl.
+ if self.color.r != 0.0 || self.color.g != 0.0 || self.color.b != 0.0 {
+ let scale = if lighter { 1.0 } else { 2.0 / 3.0 };
+ return self.color.scale_rgb(scale)
+ }
+
+ let black = if lighter { 0.7 } else { 0.3 };
+ ColorF::new(black, black, black, self.color.a)
+ }
+}
+
+/// The kind of border corner clip.
+#[repr(C)]
+#[derive(Copy, Debug, Clone, PartialEq)]
+pub enum BorderClipKind {
+ DashCorner = 1,
+ DashEdge = 2,
+ Dot = 3,
+}
+
+fn compute_outer_and_clip_sign(
+ corner_segment: BorderSegment,
+ radius: DeviceSize,
+) -> (DevicePoint, DeviceVector2D) {
+ let outer_scale = match corner_segment {
+ BorderSegment::TopLeft => DeviceVector2D::new(0.0, 0.0),
+ BorderSegment::TopRight => DeviceVector2D::new(1.0, 0.0),
+ BorderSegment::BottomRight => DeviceVector2D::new(1.0, 1.0),
+ BorderSegment::BottomLeft => DeviceVector2D::new(0.0, 1.0),
+ _ => panic!("bug: expected a corner segment"),
+ };
+ let outer = DevicePoint::new(
+ outer_scale.x * radius.width,
+ outer_scale.y * radius.height,
+ );
+
+ let clip_sign = DeviceVector2D::new(
+ 1.0 - 2.0 * outer_scale.x,
+ 1.0 - 2.0 * outer_scale.y,
+ );
+
+ (outer, clip_sign)
+}
+
+fn write_dashed_corner_instances(
+ corner_radius: DeviceSize,
+ widths: DeviceSize,
+ segment: BorderSegment,
+ base_instance: &BorderInstance,
+ instances: &mut Vec<BorderInstance>,
+) -> Result<(), ()> {
+ let ellipse = Ellipse::new(corner_radius);
+
+ let average_border_width = 0.5 * (widths.width + widths.height);
+
+ let (_half_dash, num_half_dashes) =
+ compute_half_dash(average_border_width, ellipse.total_arc_length);
+
+ if num_half_dashes == 0 {
+ return Err(());
+ }
+
+ let num_half_dashes = num_half_dashes.min(MAX_DASH_COUNT);
+
+ let (outer, clip_sign) = compute_outer_and_clip_sign(segment, corner_radius);
+
+ let instance_count = num_half_dashes / 4 + 1;
+ instances.reserve(instance_count as usize);
+
+ let half_dash_arc_length =
+ ellipse.total_arc_length / num_half_dashes as f32;
+ let dash_length = 2. * half_dash_arc_length;
+
+ let mut current_length = 0.;
+ for i in 0..instance_count {
+ let arc_length0 = current_length;
+ current_length += if i == 0 {
+ half_dash_arc_length
+ } else {
+ dash_length
+ };
+
+ let arc_length1 = current_length;
+ current_length += dash_length;
+
+ let alpha = ellipse.find_angle_for_arc_length(arc_length0);
+ let beta = ellipse.find_angle_for_arc_length(arc_length1);
+
+ let (point0, tangent0) = ellipse.get_point_and_tangent(alpha);
+ let (point1, tangent1) = ellipse.get_point_and_tangent(beta);
+
+ let point0 = DevicePoint::new(
+ outer.x + clip_sign.x * (corner_radius.width - point0.x),
+ outer.y + clip_sign.y * (corner_radius.height - point0.y),
+ );
+
+ let tangent0 = DeviceVector2D::new(
+ -tangent0.x * clip_sign.x,
+ -tangent0.y * clip_sign.y,
+ );
+
+ let point1 = DevicePoint::new(
+ outer.x + clip_sign.x * (corner_radius.width - point1.x),
+ outer.y + clip_sign.y * (corner_radius.height - point1.y),
+ );
+
+ let tangent1 = DeviceVector2D::new(
+ -tangent1.x * clip_sign.x,
+ -tangent1.y * clip_sign.y,
+ );
+
+ instances.push(BorderInstance {
+ flags: base_instance.flags | ((BorderClipKind::DashCorner as i32) << 24),
+ clip_params: [
+ point0.x,
+ point0.y,
+ tangent0.x,
+ tangent0.y,
+ point1.x,
+ point1.y,
+ tangent1.x,
+ tangent1.y,
+ ],
+ .. *base_instance
+ });
+ }
+
+ Ok(())
+}
+
+fn write_dotted_corner_instances(
+ corner_radius: DeviceSize,
+ widths: DeviceSize,
+ segment: BorderSegment,
+ base_instance: &BorderInstance,
+ instances: &mut Vec<BorderInstance>,
+) -> Result<(), ()> {
+ let mut corner_radius = corner_radius;
+ if corner_radius.width < (widths.width / 2.0) {
+ corner_radius.width = 0.0;
+ }
+ if corner_radius.height < (widths.height / 2.0) {
+ corner_radius.height = 0.0;
+ }
+
+ let (ellipse, max_dot_count) =
+ if corner_radius.width == 0. && corner_radius.height == 0. {
+ (Ellipse::new(corner_radius), 1)
+ } else {
+ // The centers of dots follow an ellipse along the middle of the
+ // border radius.
+ let inner_radius = (corner_radius - widths * 0.5).abs();
+ let ellipse = Ellipse::new(inner_radius);
+
+ // Allocate a "worst case" number of dot clips. This can be
+ // calculated by taking the minimum edge radius, since that
+ // will result in the maximum number of dots along the path.
+ let min_diameter = widths.width.min(widths.height);
+
+ // Get the number of circles (assuming spacing of one diameter
+ // between dots).
+ let max_dot_count = 0.5 * ellipse.total_arc_length / min_diameter;
+
+ // Add space for one extra dot since they are centered at the
+ // start of the arc.
+ (ellipse, max_dot_count.ceil() as usize)
+ };
+
+ if max_dot_count == 0 {
+ return Err(());
+ }
+
+ if max_dot_count == 1 {
+ let dot_diameter = lerp(widths.width, widths.height, 0.5);
+ instances.push(BorderInstance {
+ flags: base_instance.flags | ((BorderClipKind::Dot as i32) << 24),
+ clip_params: [
+ widths.width / 2.0, widths.height / 2.0, 0.5 * dot_diameter, 0.,
+ 0., 0., 0., 0.,
+ ],
+ .. *base_instance
+ });
+ return Ok(());
+ }
+
+ let max_dot_count = max_dot_count.min(MAX_DASH_COUNT as usize);
+
+ // FIXME(emilio): Should probably use SmallVec.
+ let mut forward_dots = Vec::with_capacity(max_dot_count / 2 + 1);
+ let mut back_dots = Vec::with_capacity(max_dot_count / 2 + 1);
+ let mut leftover_arc_length = 0.0;
+
+ // Alternate between adding dots at the start and end of the
+ // ellipse arc. This ensures that we always end up with an exact
+ // half dot at each end of the arc, to match up with the edges.
+ forward_dots.push(DotInfo::new(widths.width, widths.width));
+ back_dots.push(DotInfo::new(
+ ellipse.total_arc_length - widths.height,
+ widths.height,
+ ));
+
+ let (outer, clip_sign) = compute_outer_and_clip_sign(segment, corner_radius);
+ for dot_index in 0 .. max_dot_count {
+ let prev_forward_pos = *forward_dots.last().unwrap();
+ let prev_back_pos = *back_dots.last().unwrap();
+
+ // Select which end of the arc to place a dot from.
+ // This just alternates between the start and end of
+ // the arc, which ensures that there is always an
+ // exact half-dot at each end of the ellipse.
+ let going_forward = dot_index & 1 == 0;
+
+ let (next_dot_pos, leftover) = if going_forward {
+ let next_dot_pos =
+ prev_forward_pos.arc_pos + 2.0 * prev_forward_pos.diameter;
+ (next_dot_pos, prev_back_pos.arc_pos - next_dot_pos)
+ } else {
+ let next_dot_pos = prev_back_pos.arc_pos - 2.0 * prev_back_pos.diameter;
+ (next_dot_pos, next_dot_pos - prev_forward_pos.arc_pos)
+ };
+
+ // Use a lerp between each edge's dot
+ // diameter, based on the linear distance
+ // along the arc to get the diameter of the
+ // dot at this arc position.
+ let t = next_dot_pos / ellipse.total_arc_length;
+ let dot_diameter = lerp(widths.width, widths.height, t);
+
+ // If we can't fit a dot, bail out.
+ if leftover < dot_diameter {
+ leftover_arc_length = leftover;
+ break;
+ }
+
+ // We can place a dot!
+ let dot = DotInfo::new(next_dot_pos, dot_diameter);
+ if going_forward {
+ forward_dots.push(dot);
+ } else {
+ back_dots.push(dot);
+ }
+ }
+
+ // Now step through the dots, and distribute any extra
+ // leftover space on the arc between them evenly. Once
+ // the final arc position is determined, generate the correct
+ // arc positions and angles that get passed to the clip shader.
+ let number_of_dots = forward_dots.len() + back_dots.len();
+ let extra_space_per_dot = leftover_arc_length / (number_of_dots - 1) as f32;
+
+ let create_dot_data = |arc_length: f32, dot_radius: f32| -> [f32; 8] {
+ // Represents the GPU data for drawing a single dot to a clip mask. The order
+ // these are specified must stay in sync with the way this data is read in the
+ // dot clip shader.
+ let theta = ellipse.find_angle_for_arc_length(arc_length);
+ let (center, _) = ellipse.get_point_and_tangent(theta);
+
+ let center = DevicePoint::new(
+ outer.x + clip_sign.x * (corner_radius.width - center.x),
+ outer.y + clip_sign.y * (corner_radius.height - center.y),
+ );
+
+ [center.x, center.y, dot_radius, 0.0, 0.0, 0.0, 0.0, 0.0]
+ };
+
+ instances.reserve(number_of_dots);
+ for (i, dot) in forward_dots.iter().enumerate() {
+ let extra_dist = i as f32 * extra_space_per_dot;
+ instances.push(BorderInstance {
+ flags: base_instance.flags | ((BorderClipKind::Dot as i32) << 24),
+ clip_params: create_dot_data(dot.arc_pos + extra_dist, 0.5 * dot.diameter),
+ .. *base_instance
+ });
+ }
+
+ for (i, dot) in back_dots.iter().enumerate() {
+ let extra_dist = i as f32 * extra_space_per_dot;
+ instances.push(BorderInstance {
+ flags: base_instance.flags | ((BorderClipKind::Dot as i32) << 24),
+ clip_params: create_dot_data(dot.arc_pos - extra_dist, 0.5 * dot.diameter),
+ .. *base_instance
+ });
+ }
+
+ Ok(())
+}
+
+#[derive(Copy, Clone, Debug)]
+struct DotInfo {
+ arc_pos: f32,
+ diameter: f32,
+}
+
+impl DotInfo {
+ fn new(arc_pos: f32, diameter: f32) -> DotInfo {
+ DotInfo { arc_pos, diameter }
+ }
+}
+
+/// Information needed to place and draw a border edge.
+#[derive(Debug)]
+struct EdgeInfo {
+ /// Offset in local space to place the edge from origin.
+ local_offset: f32,
+ /// Size of the edge in local space.
+ local_size: f32,
+ /// Local stretch size for this edge (repeat past this).
+ stretch_size: f32,
+}
+
+impl EdgeInfo {
+ fn new(
+ local_offset: f32,
+ local_size: f32,
+ stretch_size: f32,
+ ) -> Self {
+ Self {
+ local_offset,
+ local_size,
+ stretch_size,
+ }
+ }
+}
+
+// Given a side width and the available space, compute the half-dash (half of
+// the 'on' segment) and the count of them for a given segment.
+fn compute_half_dash(side_width: f32, total_size: f32) -> (f32, u32) {
+ let half_dash = side_width * 1.5;
+ let num_half_dashes = (total_size / half_dash).ceil() as u32;
+
+ if num_half_dashes == 0 {
+ return (0., 0);
+ }
+
+ // TODO(emilio): Gecko has some other heuristics here to start with a full
+ // dash when the border side is zero, for example. We might consider those
+ // in the future.
+ let num_half_dashes = if num_half_dashes % 4 != 0 {
+ num_half_dashes + 4 - num_half_dashes % 4
+ } else {
+ num_half_dashes
+ };
+
+ let half_dash = total_size / num_half_dashes as f32;
+ (half_dash, num_half_dashes)
+}
+
+
+// Get the needed size in device pixels for an edge,
+// based on the border style of that edge. This is used
+// to determine how big the render task should be.
+fn get_edge_info(
+ style: BorderStyle,
+ side_width: f32,
+ avail_size: f32,
+) -> EdgeInfo {
+ // To avoid division by zero below.
+ if side_width <= 0.0 || avail_size <= 0.0 {
+ return EdgeInfo::new(0.0, 0.0, 0.0);
+ }
+
+ match style {
+ BorderStyle::Dashed => {
+ // Basically, two times the dash size.
+ let (half_dash, _num_half_dashes) =
+ compute_half_dash(side_width, avail_size);
+ let stretch_size = 2.0 * 2.0 * half_dash;
+ EdgeInfo::new(0., avail_size, stretch_size)
+ }
+ BorderStyle::Dotted => {
+ let dot_and_space_size = 2.0 * side_width;
+ if avail_size < dot_and_space_size * 0.75 {
+ return EdgeInfo::new(0.0, 0.0, 0.0);
+ }
+ let approx_dot_count = avail_size / dot_and_space_size;
+ let dot_count = approx_dot_count.floor().max(1.0);
+ let used_size = dot_count * dot_and_space_size;
+ let extra_space = avail_size - used_size;
+ let stretch_size = dot_and_space_size;
+ let offset = (extra_space * 0.5).round();
+ EdgeInfo::new(offset, used_size, stretch_size)
+ }
+ _ => {
+ EdgeInfo::new(0.0, avail_size, 8.0)
+ }
+ }
+}
+
+/// Create the set of border segments and render task
+/// cache keys for a given CSS border.
+pub fn create_border_segments(
+ size: LayoutSize,
+ border: &ApiNormalBorder,
+ widths: &LayoutSideOffsets,
+ border_segments: &mut Vec<BorderSegmentInfo>,
+ brush_segments: &mut Vec<BrushSegment>,
+) {
+ let rect = LayoutRect::from_size(size);
+
+ let overlap = LayoutSize::new(
+ (widths.left + widths.right - size.width).max(0.0),
+ (widths.top + widths.bottom - size.height).max(0.0),
+ );
+ let non_overlapping_widths = LayoutSideOffsets::new(
+ widths.top - overlap.height / 2.0,
+ widths.right - overlap.width / 2.0,
+ widths.bottom - overlap.height / 2.0,
+ widths.left - overlap.width / 2.0,
+ );
+
+ let local_size_tl = LayoutSize::new(
+ border.radius.top_left.width.max(widths.left),
+ border.radius.top_left.height.max(widths.top),
+ );
+ let local_size_tr = LayoutSize::new(
+ border.radius.top_right.width.max(widths.right),
+ border.radius.top_right.height.max(widths.top),
+ );
+ let local_size_br = LayoutSize::new(
+ border.radius.bottom_right.width.max(widths.right),
+ border.radius.bottom_right.height.max(widths.bottom),
+ );
+ let local_size_bl = LayoutSize::new(
+ border.radius.bottom_left.width.max(widths.left),
+ border.radius.bottom_left.height.max(widths.bottom),
+ );
+
+ let top_edge_info = get_edge_info(
+ border.top.style,
+ widths.top,
+ rect.width() - local_size_tl.width - local_size_tr.width,
+ );
+ let bottom_edge_info = get_edge_info(
+ border.bottom.style,
+ widths.bottom,
+ rect.width() - local_size_bl.width - local_size_br.width,
+ );
+
+ let left_edge_info = get_edge_info(
+ border.left.style,
+ widths.left,
+ rect.height() - local_size_tl.height - local_size_bl.height,
+ );
+ let right_edge_info = get_edge_info(
+ border.right.style,
+ widths.right,
+ rect.height() - local_size_tr.height - local_size_br.height,
+ );
+
+ add_edge_segment(
+ LayoutRect::from_floats(
+ rect.min.x,
+ rect.min.y + local_size_tl.height + left_edge_info.local_offset,
+ rect.min.x + non_overlapping_widths.left,
+ rect.min.y + local_size_tl.height + left_edge_info.local_offset + left_edge_info.local_size,
+ ),
+ &left_edge_info,
+ border.left,
+ non_overlapping_widths.left,
+ BorderSegment::Left,
+ EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::RIGHT,
+ brush_segments,
+ border_segments,
+ border.do_aa,
+ );
+ add_edge_segment(
+ LayoutRect::from_floats(
+ rect.min.x + local_size_tl.width + top_edge_info.local_offset,
+ rect.min.y,
+ rect.min.x + local_size_tl.width + top_edge_info.local_offset + top_edge_info.local_size,
+ rect.min.y + non_overlapping_widths.top,
+ ),
+ &top_edge_info,
+ border.top,
+ non_overlapping_widths.top,
+ BorderSegment::Top,
+ EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::BOTTOM,
+ brush_segments,
+ border_segments,
+ border.do_aa,
+ );
+ add_edge_segment(
+ LayoutRect::from_floats(
+ rect.min.x + rect.width() - non_overlapping_widths.right,
+ rect.min.y + local_size_tr.height + right_edge_info.local_offset,
+ rect.min.x + rect.width(),
+ rect.min.y + local_size_tr.height + right_edge_info.local_offset + right_edge_info.local_size,
+ ),
+ &right_edge_info,
+ border.right,
+ non_overlapping_widths.right,
+ BorderSegment::Right,
+ EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::LEFT,
+ brush_segments,
+ border_segments,
+ border.do_aa,
+ );
+ add_edge_segment(
+ LayoutRect::from_floats(
+ rect.min.x + local_size_bl.width + bottom_edge_info.local_offset,
+ rect.min.y + rect.height() - non_overlapping_widths.bottom,
+ rect.min.x + local_size_bl.width + bottom_edge_info.local_offset + bottom_edge_info.local_size,
+ rect.min.y + rect.height(),
+ ),
+ &bottom_edge_info,
+ border.bottom,
+ non_overlapping_widths.bottom,
+ BorderSegment::Bottom,
+ EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::TOP,
+ brush_segments,
+ border_segments,
+ border.do_aa,
+ );
+
+ add_corner_segment(
+ LayoutRect::from_floats(
+ rect.min.x,
+ rect.min.y,
+ rect.min.x + local_size_tl.width,
+ rect.min.y + local_size_tl.height,
+ ),
+ LayoutRect::from_floats(
+ rect.min.x,
+ rect.min.y,
+ rect.max.x - non_overlapping_widths.right,
+ rect.max.y - non_overlapping_widths.bottom
+ ),
+ border.left,
+ border.top,
+ LayoutSize::new(widths.left, widths.top),
+ border.radius.top_left,
+ BorderSegment::TopLeft,
+ EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::LEFT,
+ rect.top_right(),
+ border.radius.top_right,
+ rect.bottom_left(),
+ border.radius.bottom_left,
+ brush_segments,
+ border_segments,
+ border.do_aa,
+ );
+ add_corner_segment(
+ LayoutRect::from_floats(
+ rect.min.x + rect.width() - local_size_tr.width,
+ rect.min.y,
+ rect.min.x + rect.width(),
+ rect.min.y + local_size_tr.height,
+ ),
+ LayoutRect::from_floats(
+ rect.min.x + non_overlapping_widths.left,
+ rect.min.y,
+ rect.max.x,
+ rect.max.y - non_overlapping_widths.bottom,
+ ),
+ border.top,
+ border.right,
+ LayoutSize::new(widths.right, widths.top),
+ border.radius.top_right,
+ BorderSegment::TopRight,
+ EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::RIGHT,
+ rect.min,
+ border.radius.top_left,
+ rect.max,
+ border.radius.bottom_right,
+ brush_segments,
+ border_segments,
+ border.do_aa,
+ );
+ add_corner_segment(
+ LayoutRect::from_floats(
+ rect.min.x + rect.width() - local_size_br.width,
+ rect.min.y + rect.height() - local_size_br.height,
+ rect.min.x + rect.width(),
+ rect.min.y + rect.height(),
+ ),
+ LayoutRect::from_floats(
+ rect.min.x + non_overlapping_widths.left,
+ rect.min.y + non_overlapping_widths.top,
+ rect.max.x,
+ rect.max.y,
+ ),
+ border.right,
+ border.bottom,
+ LayoutSize::new(widths.right, widths.bottom),
+ border.radius.bottom_right,
+ BorderSegment::BottomRight,
+ EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::RIGHT,
+ rect.bottom_left(),
+ border.radius.bottom_left,
+ rect.top_right(),
+ border.radius.top_right,
+ brush_segments,
+ border_segments,
+ border.do_aa,
+ );
+ add_corner_segment(
+ LayoutRect::from_floats(
+ rect.min.x,
+ rect.min.y + rect.height() - local_size_bl.height,
+ rect.min.x + local_size_bl.width,
+ rect.min.y + rect.height(),
+ ),
+ LayoutRect::from_floats(
+ rect.min.x,
+ rect.min.y + non_overlapping_widths.top,
+ rect.max.x - non_overlapping_widths.right,
+ rect.max.y,
+ ),
+ border.bottom,
+ border.left,
+ LayoutSize::new(widths.left, widths.bottom),
+ border.radius.bottom_left,
+ BorderSegment::BottomLeft,
+ EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::LEFT,
+ rect.max,
+ border.radius.bottom_right,
+ rect.min,
+ border.radius.top_left,
+ brush_segments,
+ border_segments,
+ border.do_aa,
+ );
+}
+
+/// Computes the maximum scale that we allow for this set of border parameters.
+/// capping the scale will result in rendering very large corners at a lower
+/// resolution and stretching them, so they will have the right shape, but
+/// blurrier.
+pub fn get_max_scale_for_border(
+ border_data: &NormalBorderData,
+) -> LayoutToDeviceScale {
+ let mut r = 1.0;
+ for segment in &border_data.border_segments {
+ let size = segment.local_task_size;
+ r = size.width.max(size.height.max(r));
+ }
+
+ LayoutToDeviceScale::new(MAX_BORDER_RESOLUTION as f32 / r)
+}
+
+fn add_segment(
+ task_rect: DeviceRect,
+ style0: BorderStyle,
+ style1: BorderStyle,
+ color0: ColorF,
+ color1: ColorF,
+ segment: BorderSegment,
+ instances: &mut Vec<BorderInstance>,
+ widths: DeviceSize,
+ radius: DeviceSize,
+ do_aa: bool,
+ h_adjacent_corner_outer: DevicePoint,
+ h_adjacent_corner_radius: DeviceSize,
+ v_adjacent_corner_outer: DevicePoint,
+ v_adjacent_corner_radius: DeviceSize,
+) {
+ let base_flags = (segment as i32) |
+ ((style0 as i32) << 8) |
+ ((style1 as i32) << 16) |
+ ((do_aa as i32) << 28);
+
+ let base_instance = BorderInstance {
+ task_origin: DevicePoint::zero(),
+ local_rect: task_rect,
+ flags: base_flags,
+ color0: color0.premultiplied(),
+ color1: color1.premultiplied(),
+ widths,
+ radius,
+ clip_params: [0.0; 8],
+ };
+
+ match segment {
+ BorderSegment::TopLeft |
+ BorderSegment::TopRight |
+ BorderSegment::BottomLeft |
+ BorderSegment::BottomRight => {
+ // TODO(gw): Similarly to the old border code, we don't correctly handle a a corner
+ // that is dashed on one edge, and dotted on another. We can handle this
+ // in the future by submitting two instances, each one with one side
+ // color set to have an alpha of 0.
+ if (style0 == BorderStyle::Dotted && style1 == BorderStyle::Dashed) ||
+ (style0 == BorderStyle::Dashed && style0 == BorderStyle::Dotted) {
+ warn!("TODO: Handle a corner with dotted / dashed transition.");
+ }
+
+ let dashed_or_dotted_corner = match style0 {
+ BorderStyle::Dashed => {
+ write_dashed_corner_instances(
+ radius,
+ widths,
+ segment,
+ &base_instance,
+ instances,
+ )
+ }
+ BorderStyle::Dotted => {
+ write_dotted_corner_instances(
+ radius,
+ widths,
+ segment,
+ &base_instance,
+ instances,
+ )
+ }
+ _ => Err(()),
+ };
+
+ if dashed_or_dotted_corner.is_err() {
+ let clip_params = [
+ h_adjacent_corner_outer.x,
+ h_adjacent_corner_outer.y,
+ h_adjacent_corner_radius.width,
+ h_adjacent_corner_radius.height,
+ v_adjacent_corner_outer.x,
+ v_adjacent_corner_outer.y,
+ v_adjacent_corner_radius.width,
+ v_adjacent_corner_radius.height,
+ ];
+
+ instances.push(BorderInstance {
+ clip_params,
+ ..base_instance
+ });
+ }
+ }
+ BorderSegment::Top |
+ BorderSegment::Bottom |
+ BorderSegment::Right |
+ BorderSegment::Left => {
+ let is_vertical = segment == BorderSegment::Left ||
+ segment == BorderSegment::Right;
+
+ match style0 {
+ BorderStyle::Dashed => {
+ let (x, y) = if is_vertical {
+ let half_dash_size = task_rect.height() * 0.25;
+ (0., half_dash_size)
+ } else {
+ let half_dash_size = task_rect.width() * 0.25;
+ (half_dash_size, 0.)
+ };
+
+ instances.push(BorderInstance {
+ flags: base_flags | ((BorderClipKind::DashEdge as i32) << 24),
+ clip_params: [
+ x, y, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+ ],
+ ..base_instance
+ });
+ }
+ BorderStyle::Dotted => {
+ let (x, y, r) = if is_vertical {
+ (widths.width * 0.5,
+ widths.width,
+ widths.width * 0.5)
+ } else {
+ (widths.height,
+ widths.height * 0.5,
+ widths.height * 0.5)
+ };
+
+ instances.push(BorderInstance {
+ flags: base_flags | ((BorderClipKind::Dot as i32) << 24),
+ clip_params: [
+ x, y, r, 0.0, 0.0, 0.0, 0.0, 0.0,
+ ],
+ ..base_instance
+ });
+ }
+ _ => {
+ instances.push(base_instance);
+ }
+ }
+ }
+ }
+}
+
+/// Add a corner segment (if valid) to the list of
+/// border segments for this primitive.
+fn add_corner_segment(
+ image_rect: LayoutRect,
+ non_overlapping_rect: LayoutRect,
+ side0: BorderSide,
+ side1: BorderSide,
+ widths: LayoutSize,
+ radius: LayoutSize,
+ segment: BorderSegment,
+ edge_flags: EdgeAaSegmentMask,
+ h_adjacent_corner_outer: LayoutPoint,
+ h_adjacent_corner_radius: LayoutSize,
+ v_adjacent_corner_outer: LayoutPoint,
+ v_adjacent_corner_radius: LayoutSize,
+ brush_segments: &mut Vec<BrushSegment>,
+ border_segments: &mut Vec<BorderSegmentInfo>,
+ do_aa: bool,
+) {
+ if side0.color.a <= 0.0 && side1.color.a <= 0.0 {
+ return;
+ }
+
+ if widths.width <= 0.0 && widths.height <= 0.0 {
+ return;
+ }
+
+ if side0.style.is_hidden() && side1.style.is_hidden() {
+ return;
+ }
+
+ let segment_rect = match image_rect.intersection(&non_overlapping_rect) {
+ Some(rect) => rect,
+ None => {
+ return;
+ }
+ };
+
+ let texture_rect = segment_rect
+ .translate(-image_rect.min.to_vector())
+ .scale(1.0 / image_rect.width(), 1.0 / image_rect.height());
+
+ brush_segments.push(
+ BrushSegment::new(
+ segment_rect,
+ /* may_need_clip_mask = */ true,
+ edge_flags,
+ [texture_rect.min.x, texture_rect.min.y, texture_rect.max.x, texture_rect.max.y],
+ BrushFlags::SEGMENT_RELATIVE | BrushFlags::SEGMENT_TEXEL_RECT,
+ )
+ );
+
+ // If the radii of the adjacent corners do not overlap with this segment,
+ // then set the outer position to this segment's corner and the radii to zero.
+ // That way the cache key is unaffected by non-overlapping corners, resulting
+ // in fewer misses.
+ let (h_corner_outer, h_corner_radius) = match segment {
+ BorderSegment::TopLeft => {
+ if h_adjacent_corner_outer.x - h_adjacent_corner_radius.width < image_rect.max.x {
+ (h_adjacent_corner_outer, h_adjacent_corner_radius)
+ } else {
+ (LayoutPoint::new(image_rect.max.x, image_rect.min.y), LayoutSize::zero())
+ }
+ }
+ BorderSegment::TopRight => {
+ if h_adjacent_corner_outer.x + h_adjacent_corner_radius.width > image_rect.min.x {
+ (h_adjacent_corner_outer, h_adjacent_corner_radius)
+ } else {
+ (LayoutPoint::new(image_rect.min.x, image_rect.min.y), LayoutSize::zero())
+ }
+ }
+ BorderSegment::BottomRight => {
+ if h_adjacent_corner_outer.x + h_adjacent_corner_radius.width > image_rect.min.x {
+ (h_adjacent_corner_outer, h_adjacent_corner_radius)
+ } else {
+ (LayoutPoint::new(image_rect.min.x, image_rect.max.y), LayoutSize::zero())
+ }
+ }
+ BorderSegment::BottomLeft => {
+ if h_adjacent_corner_outer.x - h_adjacent_corner_radius.width < image_rect.max.x {
+ (h_adjacent_corner_outer, h_adjacent_corner_radius)
+ } else {
+ (image_rect.max, LayoutSize::zero())
+ }
+ }
+ _ => unreachable!()
+ };
+
+ let (v_corner_outer, v_corner_radius) = match segment {
+ BorderSegment::TopLeft => {
+ if v_adjacent_corner_outer.y - v_adjacent_corner_radius.height < image_rect.max.y {
+ (v_adjacent_corner_outer, v_adjacent_corner_radius)
+ } else {
+ (LayoutPoint::new(image_rect.min.x, image_rect.max.y), LayoutSize::zero())
+ }
+ }
+ BorderSegment::TopRight => {
+ if v_adjacent_corner_outer.y - v_adjacent_corner_radius.height < image_rect.max.y {
+ (v_adjacent_corner_outer, v_adjacent_corner_radius)
+ } else {
+ (image_rect.max, LayoutSize::zero())
+ }
+ }
+ BorderSegment::BottomRight => {
+ if v_adjacent_corner_outer.y + v_adjacent_corner_radius.height > image_rect.min.y {
+ (v_adjacent_corner_outer, v_adjacent_corner_radius)
+ } else {
+ (LayoutPoint::new(image_rect.max.x, image_rect.min.y), LayoutSize::zero())
+ }
+ }
+ BorderSegment::BottomLeft => {
+ if v_adjacent_corner_outer.y + v_adjacent_corner_radius.height > image_rect.min.y {
+ (v_adjacent_corner_outer, v_adjacent_corner_radius)
+ } else {
+ (LayoutPoint::new(image_rect.min.x, image_rect.min.y), LayoutSize::zero())
+ }
+ }
+ _ => unreachable!()
+ };
+
+ border_segments.push(BorderSegmentInfo {
+ local_task_size: image_rect.size(),
+ cache_key: BorderSegmentCacheKey {
+ do_aa,
+ side0: side0.into(),
+ side1: side1.into(),
+ segment,
+ radius: radius.to_au(),
+ size: widths.to_au(),
+ h_adjacent_corner_outer: (h_corner_outer - image_rect.min).to_point().to_au(),
+ h_adjacent_corner_radius: h_corner_radius.to_au(),
+ v_adjacent_corner_outer: (v_corner_outer - image_rect.min).to_point().to_au(),
+ v_adjacent_corner_radius: v_corner_radius.to_au(),
+ },
+ });
+}
+
+/// Add an edge segment (if valid) to the list of
+/// border segments for this primitive.
+fn add_edge_segment(
+ image_rect: LayoutRect,
+ edge_info: &EdgeInfo,
+ side: BorderSide,
+ width: f32,
+ segment: BorderSegment,
+ edge_flags: EdgeAaSegmentMask,
+ brush_segments: &mut Vec<BrushSegment>,
+ border_segments: &mut Vec<BorderSegmentInfo>,
+ do_aa: bool,
+) {
+ if side.color.a <= 0.0 {
+ return;
+ }
+
+ if side.style.is_hidden() {
+ return;
+ }
+
+ let (size, brush_flags) = match segment {
+ BorderSegment::Left | BorderSegment::Right => {
+ (LayoutSize::new(width, edge_info.stretch_size), BrushFlags::SEGMENT_REPEAT_Y)
+ }
+ BorderSegment::Top | BorderSegment::Bottom => {
+ (LayoutSize::new(edge_info.stretch_size, width), BrushFlags::SEGMENT_REPEAT_X)
+ }
+ _ => {
+ unreachable!();
+ }
+ };
+
+ if image_rect.width() <= 0. || image_rect.height() <= 0. {
+ return;
+ }
+
+ brush_segments.push(
+ BrushSegment::new(
+ image_rect,
+ /* may_need_clip_mask = */ true,
+ edge_flags,
+ [0.0, 0.0, size.width, size.height],
+ BrushFlags::SEGMENT_RELATIVE | brush_flags,
+ )
+ );
+
+ border_segments.push(BorderSegmentInfo {
+ local_task_size: size,
+ cache_key: BorderSegmentCacheKey {
+ do_aa,
+ side0: side.into(),
+ side1: side.into(),
+ radius: LayoutSizeAu::zero(),
+ size: size.to_au(),
+ segment,
+ h_adjacent_corner_outer: LayoutPointAu::zero(),
+ h_adjacent_corner_radius: LayoutSizeAu::zero(),
+ v_adjacent_corner_outer: LayoutPointAu::zero(),
+ v_adjacent_corner_radius: LayoutSizeAu::zero(),
+ },
+ });
+}
+
+/// Build the set of border instances needed to draw a border
+/// segment into the render task cache.
+pub fn build_border_instances(
+ cache_key: &BorderSegmentCacheKey,
+ cache_size: DeviceIntSize,
+ border: &ApiNormalBorder,
+ scale: LayoutToDeviceScale,
+) -> Vec<BorderInstance> {
+ let mut instances = Vec::new();
+
+ let (side0, side1, flip0, flip1) = match cache_key.segment {
+ BorderSegment::Left => (&border.left, &border.left, false, false),
+ BorderSegment::Top => (&border.top, &border.top, false, false),
+ BorderSegment::Right => (&border.right, &border.right, true, true),
+ BorderSegment::Bottom => (&border.bottom, &border.bottom, true, true),
+ BorderSegment::TopLeft => (&border.left, &border.top, false, false),
+ BorderSegment::TopRight => (&border.top, &border.right, false, true),
+ BorderSegment::BottomRight => (&border.right, &border.bottom, true, true),
+ BorderSegment::BottomLeft => (&border.bottom, &border.left, true, false),
+ };
+
+ let style0 = if side0.style.is_hidden() {
+ side1.style
+ } else {
+ side0.style
+ };
+ let style1 = if side1.style.is_hidden() {
+ side0.style
+ } else {
+ side1.style
+ };
+
+ let color0 = side0.border_color(flip0);
+ let color1 = side1.border_color(flip1);
+
+ let widths = (LayoutSize::from_au(cache_key.size) * scale).ceil();
+ let radius = (LayoutSize::from_au(cache_key.radius) * scale).ceil();
+
+ let h_corner_outer = (LayoutPoint::from_au(cache_key.h_adjacent_corner_outer) * scale).round();
+ let h_corner_radius = (LayoutSize::from_au(cache_key.h_adjacent_corner_radius) * scale).ceil();
+ let v_corner_outer = (LayoutPoint::from_au(cache_key.v_adjacent_corner_outer) * scale).round();
+ let v_corner_radius = (LayoutSize::from_au(cache_key.v_adjacent_corner_radius) * scale).ceil();
+
+ add_segment(
+ DeviceRect::from_size(cache_size.to_f32()),
+ style0,
+ style1,
+ color0,
+ color1,
+ cache_key.segment,
+ &mut instances,
+ widths,
+ radius,
+ border.do_aa,
+ h_corner_outer,
+ h_corner_radius,
+ v_corner_outer,
+ v_corner_radius,
+ );
+
+ instances
+}
+
+impl NinePatchDescriptor {
+ pub fn create_segments(
+ &self,
+ size: LayoutSize,
+ ) -> Vec<BrushSegment> {
+ let rect = LayoutRect::from_size(size);
+
+ // Calculate the local texel coords of the slices.
+ let px0 = 0.0;
+ let px1 = self.slice.left as f32 / self.width as f32;
+ let px2 = (self.width as f32 - self.slice.right as f32) / self.width as f32;
+ let px3 = 1.0;
+
+ let py0 = 0.0;
+ let py1 = self.slice.top as f32 / self.height as f32;
+ let py2 = (self.height as f32 - self.slice.bottom as f32) / self.height as f32;
+ let py3 = 1.0;
+
+ let tl_outer = LayoutPoint::new(rect.min.x, rect.min.y);
+ let tl_inner = tl_outer + vec2(self.widths.left, self.widths.top);
+
+ let tr_outer = LayoutPoint::new(rect.min.x + rect.width(), rect.min.y);
+ let tr_inner = tr_outer + vec2(-self.widths.right, self.widths.top);
+
+ let bl_outer = LayoutPoint::new(rect.min.x, rect.min.y + rect.height());
+ let bl_inner = bl_outer + vec2(self.widths.left, -self.widths.bottom);
+
+ let br_outer = rect.max;
+
+ let br_inner = br_outer - vec2(self.widths.right, self.widths.bottom);
+
+ fn add_segment(
+ segments: &mut Vec<BrushSegment>,
+ rect: LayoutRect,
+ uv_rect: TexelRect,
+ repeat_horizontal: RepeatMode,
+ repeat_vertical: RepeatMode,
+ extra_flags: BrushFlags,
+ ) {
+ if uv_rect.uv1.x <= uv_rect.uv0.x || uv_rect.uv1.y <= uv_rect.uv0.y {
+ return;
+ }
+
+ // Use segment relative interpolation for all
+ // instances in this primitive.
+ let mut brush_flags =
+ BrushFlags::SEGMENT_RELATIVE |
+ BrushFlags::SEGMENT_TEXEL_RECT |
+ extra_flags;
+
+ // Enable repeat modes on the segment.
+ if repeat_horizontal == RepeatMode::Repeat {
+ brush_flags |= BrushFlags::SEGMENT_REPEAT_X;
+ } else if repeat_horizontal == RepeatMode::Round {
+ brush_flags |= BrushFlags::SEGMENT_REPEAT_X | BrushFlags::SEGMENT_REPEAT_X_ROUND;
+ }
+
+ if repeat_vertical == RepeatMode::Repeat {
+ brush_flags |= BrushFlags::SEGMENT_REPEAT_Y;
+ } else if repeat_vertical == RepeatMode::Round {
+ brush_flags |= BrushFlags::SEGMENT_REPEAT_Y | BrushFlags::SEGMENT_REPEAT_Y_ROUND;
+ }
+
+ let segment = BrushSegment::new(
+ rect,
+ true,
+ EdgeAaSegmentMask::empty(),
+ [
+ uv_rect.uv0.x,
+ uv_rect.uv0.y,
+ uv_rect.uv1.x,
+ uv_rect.uv1.y,
+ ],
+ brush_flags,
+ );
+
+ segments.push(segment);
+ }
+
+ // Build the list of image segments
+ let mut segments = Vec::new();
+
+ // Top left
+ add_segment(
+ &mut segments,
+ LayoutRect::from_floats(tl_outer.x, tl_outer.y, tl_inner.x, tl_inner.y),
+ TexelRect::new(px0, py0, px1, py1),
+ RepeatMode::Stretch,
+ RepeatMode::Stretch,
+ BrushFlags::empty(),
+ );
+ // Top right
+ add_segment(
+ &mut segments,
+ LayoutRect::from_floats(tr_inner.x, tr_outer.y, tr_outer.x, tr_inner.y),
+ TexelRect::new(px2, py0, px3, py1),
+ RepeatMode::Stretch,
+ RepeatMode::Stretch,
+ BrushFlags::empty(),
+ );
+ // Bottom right
+ add_segment(
+ &mut segments,
+ LayoutRect::from_floats(br_inner.x, br_inner.y, br_outer.x, br_outer.y),
+ TexelRect::new(px2, py2, px3, py3),
+ RepeatMode::Stretch,
+ RepeatMode::Stretch,
+ BrushFlags::empty(),
+ );
+ // Bottom left
+ add_segment(
+ &mut segments,
+ LayoutRect::from_floats(bl_outer.x, bl_inner.y, bl_inner.x, bl_outer.y),
+ TexelRect::new(px0, py2, px1, py3),
+ RepeatMode::Stretch,
+ RepeatMode::Stretch,
+ BrushFlags::empty(),
+ );
+
+ // Center
+ if self.fill {
+ add_segment(
+ &mut segments,
+ LayoutRect::from_floats(tl_inner.x, tl_inner.y, tr_inner.x, bl_inner.y),
+ TexelRect::new(px1, py1, px2, py2),
+ self.repeat_horizontal,
+ self.repeat_vertical,
+ BrushFlags::SEGMENT_NINEPATCH_MIDDLE,
+ );
+ }
+
+ // Add edge segments.
+
+ // Top
+ add_segment(
+ &mut segments,
+ LayoutRect::from_floats(tl_inner.x, tl_outer.y, tr_inner.x, tl_inner.y),
+ TexelRect::new(px1, py0, px2, py1),
+ self.repeat_horizontal,
+ RepeatMode::Stretch,
+ BrushFlags::empty(),
+ );
+ // Bottom
+ add_segment(
+ &mut segments,
+ LayoutRect::from_floats(bl_inner.x, bl_inner.y, br_inner.x, bl_outer.y),
+ TexelRect::new(px1, py2, px2, py3),
+ self.repeat_horizontal,
+ RepeatMode::Stretch,
+ BrushFlags::empty(),
+ );
+ // Left
+ add_segment(
+ &mut segments,
+ LayoutRect::from_floats(tl_outer.x, tl_inner.y, tl_inner.x, bl_inner.y),
+ TexelRect::new(px0, py1, px1, py2),
+ RepeatMode::Stretch,
+ self.repeat_vertical,
+ BrushFlags::empty(),
+ );
+ // Right
+ add_segment(
+ &mut segments,
+ LayoutRect::from_floats(tr_inner.x, tr_inner.y, br_outer.x, br_inner.y),
+ TexelRect::new(px2, py1, px3, py2),
+ RepeatMode::Stretch,
+ self.repeat_vertical,
+ BrushFlags::empty(),
+ );
+
+ segments
+ }
+}
diff --git a/gfx/wr/webrender/src/box_shadow.rs b/gfx/wr/webrender/src/box_shadow.rs
new file mode 100644
index 0000000000..b24363addb
--- /dev/null
+++ b/gfx/wr/webrender/src/box_shadow.rs
@@ -0,0 +1,280 @@
+/* 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 api::{BorderRadius, BoxShadowClipMode, ClipMode, ColorF, PrimitiveKeyKind};
+use api::PropertyBinding;
+use api::units::*;
+use crate::clip::{ClipItemKey, ClipItemKeyKind, ClipNodeId};
+use crate::scene_building::SceneBuilder;
+use crate::spatial_tree::SpatialNodeIndex;
+use crate::gpu_types::BoxShadowStretchMode;
+use crate::render_task_graph::RenderTaskId;
+use crate::internal_types::LayoutPrimitiveInfo;
+
+#[derive(Debug, Clone, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BoxShadowClipSource {
+ // Parameters that define the shadow and are constant.
+ pub shadow_radius: BorderRadius,
+ pub blur_radius: f32,
+ pub clip_mode: BoxShadowClipMode,
+ pub stretch_mode_x: BoxShadowStretchMode,
+ pub stretch_mode_y: BoxShadowStretchMode,
+
+ // The current cache key (in device-pixels), and handles
+ // to the cached clip region and blurred texture.
+ pub cache_key: Option<(DeviceIntSize, BoxShadowCacheKey)>,
+ pub render_task: Option<RenderTaskId>,
+
+ // Local-space size of the required render task size.
+ pub shadow_rect_alloc_size: LayoutSize,
+
+ // Local-space size of the required render task size without any downscaling
+ // applied. This is needed to stretch the shadow properly.
+ pub original_alloc_size: LayoutSize,
+
+ // The minimal shadow rect for the parameters above,
+ // used when drawing the shadow rect to be blurred.
+ pub minimal_shadow_rect: LayoutRect,
+
+ // Local space rect for the shadow to be drawn or
+ // stretched in the shadow primitive.
+ pub prim_shadow_rect: LayoutRect,
+}
+
+// The blur shader samples BLUR_SAMPLE_SCALE * blur_radius surrounding texels.
+pub const BLUR_SAMPLE_SCALE: f32 = 3.0;
+
+// Maximum blur radius for box-shadows (different than blur filters).
+// Taken from nsCSSRendering.cpp in Gecko.
+pub const MAX_BLUR_RADIUS: f32 = 300.;
+
+// A cache key that uniquely identifies a minimally sized
+// and blurred box-shadow rect that can be stored in the
+// texture cache and applied to clip-masks.
+#[derive(Debug, Clone, Eq, Hash, MallocSizeOf, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BoxShadowCacheKey {
+ pub blur_radius_dp: i32,
+ pub clip_mode: BoxShadowClipMode,
+ // NOTE(emilio): Only the original allocation size needs to be in the cache
+ // key, since the actual size is derived from that.
+ pub original_alloc_size: DeviceIntSize,
+ pub br_top_left: DeviceIntSize,
+ pub br_top_right: DeviceIntSize,
+ pub br_bottom_right: DeviceIntSize,
+ pub br_bottom_left: DeviceIntSize,
+ pub device_pixel_scale: Au,
+}
+
+impl<'a> SceneBuilder<'a> {
+ pub fn add_box_shadow(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ clip_node_id: ClipNodeId,
+ prim_info: &LayoutPrimitiveInfo,
+ box_offset: &LayoutVector2D,
+ color: ColorF,
+ mut blur_radius: f32,
+ spread_radius: f32,
+ border_radius: BorderRadius,
+ clip_mode: BoxShadowClipMode,
+ ) {
+ if color.a == 0.0 {
+ return;
+ }
+
+ // Inset shadows get smaller as spread radius increases.
+ let (spread_amount, prim_clip_mode) = match clip_mode {
+ BoxShadowClipMode::Outset => (spread_radius, ClipMode::ClipOut),
+ BoxShadowClipMode::Inset => (-spread_radius, ClipMode::Clip),
+ };
+
+ // Ensure the blur radius is somewhat sensible.
+ blur_radius = f32::min(blur_radius, MAX_BLUR_RADIUS);
+
+ // Adjust the border radius of the box shadow per CSS-spec.
+ let shadow_radius = adjust_border_radius_for_box_shadow(border_radius, spread_amount);
+
+ // Apply parameters that affect where the shadow rect
+ // exists in the local space of the primitive.
+ let shadow_rect = self.snap_rect(
+ &prim_info
+ .rect
+ .translate(*box_offset)
+ .inflate(spread_amount, spread_amount),
+ spatial_node_index,
+ );
+
+ // If blur radius is zero, we can use a fast path with
+ // no blur applied.
+ if blur_radius == 0.0 {
+ // Trivial reject of box-shadows that are not visible.
+ if box_offset.x == 0.0 && box_offset.y == 0.0 && spread_amount == 0.0 {
+ return;
+ }
+
+ let mut clips = Vec::with_capacity(2);
+ let (final_prim_rect, clip_radius) = match clip_mode {
+ BoxShadowClipMode::Outset => {
+ if shadow_rect.is_empty() {
+ return;
+ }
+
+ // TODO(gw): Add a fast path for ClipOut + zero border radius!
+ clips.push(ClipItemKey {
+ kind: ClipItemKeyKind::rounded_rect(
+ prim_info.rect,
+ border_radius,
+ ClipMode::ClipOut,
+ ),
+ spatial_node_index,
+ });
+
+ (shadow_rect, shadow_radius)
+ }
+ BoxShadowClipMode::Inset => {
+ if !shadow_rect.is_empty() {
+ clips.push(ClipItemKey {
+ kind: ClipItemKeyKind::rounded_rect(
+ shadow_rect,
+ shadow_radius,
+ ClipMode::ClipOut,
+ ),
+ spatial_node_index,
+ });
+ }
+
+ (prim_info.rect, border_radius)
+ }
+ };
+
+ clips.push(ClipItemKey {
+ kind: ClipItemKeyKind::rounded_rect(
+ final_prim_rect,
+ clip_radius,
+ ClipMode::Clip,
+ ),
+ spatial_node_index,
+ });
+
+ self.add_primitive(
+ spatial_node_index,
+ clip_node_id,
+ &LayoutPrimitiveInfo::with_clip_rect(final_prim_rect, prim_info.clip_rect),
+ clips,
+ PrimitiveKeyKind::Rectangle {
+ color: PropertyBinding::Value(color.into()),
+ },
+ );
+ } else {
+ // Normal path for box-shadows with a valid blur radius.
+ let blur_offset = (BLUR_SAMPLE_SCALE * blur_radius).ceil();
+ let mut extra_clips = vec![];
+
+ // Add a normal clip mask to clip out the contents
+ // of the surrounding primitive.
+ extra_clips.push(ClipItemKey {
+ kind: ClipItemKeyKind::rounded_rect(
+ prim_info.rect,
+ border_radius,
+ prim_clip_mode,
+ ),
+ spatial_node_index,
+ });
+
+ // Get the local rect of where the shadow will be drawn,
+ // expanded to include room for the blurred region.
+ let dest_rect = shadow_rect.inflate(blur_offset, blur_offset);
+
+ // Draw the box-shadow as a solid rect, using a box-shadow
+ // clip mask item.
+ let prim = PrimitiveKeyKind::Rectangle {
+ color: PropertyBinding::Value(color.into()),
+ };
+
+ // Create the box-shadow clip item.
+ let shadow_clip_source = ClipItemKey {
+ kind: ClipItemKeyKind::box_shadow(
+ shadow_rect,
+ shadow_radius,
+ dest_rect,
+ blur_radius,
+ clip_mode,
+ ),
+ spatial_node_index,
+ };
+
+ let prim_info = match clip_mode {
+ BoxShadowClipMode::Outset => {
+ // Certain spread-radii make the shadow invalid.
+ if shadow_rect.is_empty() {
+ return;
+ }
+
+ // Add the box-shadow clip source.
+ extra_clips.push(shadow_clip_source);
+
+ // Outset shadows are expanded by the shadow
+ // region from the original primitive.
+ LayoutPrimitiveInfo::with_clip_rect(dest_rect, prim_info.clip_rect)
+ }
+ BoxShadowClipMode::Inset => {
+ // If the inner shadow rect contains the prim
+ // rect, no pixels will be shadowed.
+ if border_radius.is_zero() && shadow_rect
+ .inflate(-blur_radius, -blur_radius)
+ .contains_box(&prim_info.rect)
+ {
+ return;
+ }
+
+ // Inset shadows are still visible, even if the
+ // inset shadow rect becomes invalid (they will
+ // just look like a solid rectangle).
+ if !shadow_rect.is_empty() {
+ extra_clips.push(shadow_clip_source);
+ }
+
+ // Inset shadows draw inside the original primitive.
+ prim_info.clone()
+ }
+ };
+
+ self.add_primitive(
+ spatial_node_index,
+ clip_node_id,
+ &prim_info,
+ extra_clips,
+ prim,
+ );
+ }
+ }
+}
+
+fn adjust_border_radius_for_box_shadow(radius: BorderRadius, spread_amount: f32) -> BorderRadius {
+ BorderRadius {
+ top_left: adjust_corner_for_box_shadow(radius.top_left, spread_amount),
+ top_right: adjust_corner_for_box_shadow(radius.top_right, spread_amount),
+ bottom_right: adjust_corner_for_box_shadow(radius.bottom_right, spread_amount),
+ bottom_left: adjust_corner_for_box_shadow(radius.bottom_left, spread_amount),
+ }
+}
+
+fn adjust_corner_for_box_shadow(corner: LayoutSize, spread_amount: f32) -> LayoutSize {
+ LayoutSize::new(
+ adjust_radius_for_box_shadow(corner.width, spread_amount),
+ adjust_radius_for_box_shadow(corner.height, spread_amount),
+ )
+}
+
+fn adjust_radius_for_box_shadow(border_radius: f32, spread_amount: f32) -> f32 {
+ if border_radius > 0.0 {
+ (border_radius + spread_amount).max(0.0)
+ } else {
+ 0.0
+ }
+}
diff --git a/gfx/wr/webrender/src/capture.rs b/gfx/wr/webrender/src/capture.rs
new file mode 100644
index 0000000000..5cc1f90bab
--- /dev/null
+++ b/gfx/wr/webrender/src/capture.rs
@@ -0,0 +1,290 @@
+/* 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::fs::File;
+use std::path::{Path, PathBuf};
+
+use api::{ExternalImageData, ImageDescriptor};
+#[cfg(feature = "png")]
+use api::ImageFormat;
+use api::units::TexelRect;
+#[cfg(feature = "png")]
+use api::units::DeviceIntSize;
+#[cfg(feature = "capture")]
+use crate::print_tree::{PrintableTree, PrintTree};
+use crate::render_api::CaptureBits;
+use ron;
+use serde;
+
+
+#[derive(Clone)]
+pub struct CaptureConfig {
+ pub root: PathBuf,
+ pub bits: CaptureBits,
+ /// Scene sequence ID when capturing multiple frames. Zero for a single frame capture.
+ pub scene_id: u32,
+ /// Frame sequence ID when capturing multiple frames. Zero for a single frame capture.
+ pub frame_id: u32,
+ /// Resource sequence ID when capturing multiple frames. Zero for a single frame capture.
+ pub resource_id: u32,
+ #[cfg(feature = "capture")]
+ pretty: ron::ser::PrettyConfig,
+}
+
+impl CaptureConfig {
+ #[cfg(any(feature = "capture", feature = "replay"))]
+ pub fn new(root: PathBuf, bits: CaptureBits) -> Self {
+ CaptureConfig {
+ root,
+ bits,
+ scene_id: 0,
+ frame_id: 0,
+ resource_id: 0,
+ #[cfg(feature = "capture")]
+ pretty: ron::ser::PrettyConfig::new()
+ .enumerate_arrays(true)
+ .indentor(" ".to_string()),
+ }
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn prepare_scene(&mut self) {
+ use std::fs::create_dir_all;
+ self.scene_id += 1;
+ let _ = create_dir_all(&self.scene_root());
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn prepare_frame(&mut self) {
+ use std::fs::create_dir_all;
+ self.frame_id += 1;
+ let _ = create_dir_all(&self.frame_root());
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn prepare_resource(&mut self) {
+ use std::fs::create_dir_all;
+ self.resource_id += 1;
+ let _ = create_dir_all(&self.resource_root());
+ }
+
+ #[cfg(any(feature = "capture", feature = "replay"))]
+ pub fn scene_root(&self) -> PathBuf {
+ if self.scene_id > 0 {
+ let path = format!("scenes/{:05}", self.scene_id);
+ self.root.join(path)
+ } else {
+ self.root.clone()
+ }
+ }
+
+ #[cfg(any(feature = "capture", feature = "replay"))]
+ pub fn frame_root(&self) -> PathBuf {
+ if self.frame_id > 0 {
+ let path = format!("frames/{:05}", self.frame_id);
+ self.scene_root().join(path)
+ } else {
+ self.root.clone()
+ }
+ }
+
+ #[cfg(any(feature = "capture", feature = "replay"))]
+ pub fn resource_root(&self) -> PathBuf {
+ if self.resource_id > 0 {
+ let path = format!("resources/{:05}", self.resource_id);
+ self.root.join(path)
+ } else {
+ self.root.clone()
+ }
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn serialize_for_scene<T, P>(&self, data: &T, name: P)
+ where
+ T: serde::Serialize,
+ P: AsRef<Path>,
+ {
+ self.serialize(data, self.scene_root(), name)
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn serialize_for_frame<T, P>(&self, data: &T, name: P)
+ where
+ T: serde::Serialize,
+ P: AsRef<Path>,
+ {
+ self.serialize(data, self.frame_root(), name)
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn serialize_for_resource<T, P>(&self, data: &T, name: P)
+ where
+ T: serde::Serialize,
+ P: AsRef<Path>,
+ {
+ self.serialize(data, self.resource_root(), name)
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn file_path_for_frame<P>(&self, name: P, ext: &str) -> PathBuf
+ where P: AsRef<Path> {
+ self.frame_root().join(name).with_extension(ext)
+ }
+
+ #[cfg(feature = "capture")]
+ fn serialize<T, P>(&self, data: &T, path: PathBuf, name: P)
+ where
+ T: serde::Serialize,
+ P: AsRef<Path>,
+ {
+ use std::io::Write;
+ let ron = ron::ser::to_string_pretty(data, self.pretty.clone())
+ .unwrap();
+ let mut file = File::create(path.join(name).with_extension("ron"))
+ .unwrap();
+ write!(file, "{}\n", ron)
+ .unwrap();
+ }
+
+ #[cfg(feature = "capture")]
+ fn serialize_tree<T, P>(data: &T, root: PathBuf, name: P)
+ where
+ T: PrintableTree,
+ P: AsRef<Path>
+ {
+ let path = root
+ .join(name)
+ .with_extension("tree");
+ let file = File::create(path)
+ .unwrap();
+ let mut pt = PrintTree::new_with_sink("", file);
+ data.print_with(&mut pt);
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn serialize_tree_for_frame<T, P>(&self, data: &T, name: P)
+ where
+ T: PrintableTree,
+ P: AsRef<Path>
+ {
+ Self::serialize_tree(data, self.frame_root(), name)
+ }
+
+ #[cfg(feature = "replay")]
+ fn deserialize<T, P>(root: &PathBuf, name: P) -> Option<T>
+ where
+ T: for<'a> serde::Deserialize<'a>,
+ P: AsRef<Path>,
+ {
+ use std::io::Read;
+
+ let mut string = String::new();
+ let path = root
+ .join(name.as_ref())
+ .with_extension("ron");
+ File::open(path)
+ .ok()?
+ .read_to_string(&mut string)
+ .unwrap();
+ match ron::de::from_str(&string) {
+ Ok(out) => Some(out),
+ Err(e) => panic!("File {:?} deserialization failed: {:?}", name.as_ref(), e),
+ }
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn deserialize_for_scene<T, P>(&self, name: P) -> Option<T>
+ where
+ T: for<'a> serde::Deserialize<'a>,
+ P: AsRef<Path>,
+ {
+ Self::deserialize(&self.scene_root(), name)
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn deserialize_for_frame<T, P>(&self, name: P) -> Option<T>
+ where
+ T: for<'a> serde::Deserialize<'a>,
+ P: AsRef<Path>,
+ {
+ Self::deserialize(&self.frame_root(), name)
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn deserialize_for_resource<T, P>(&self, name: P) -> Option<T>
+ where
+ T: for<'a> serde::Deserialize<'a>,
+ P: AsRef<Path>,
+ {
+ Self::deserialize(&self.resource_root(), name)
+ }
+
+ #[cfg(feature = "png")]
+ pub fn save_png(
+ path: PathBuf, size: DeviceIntSize, format: ImageFormat, stride: Option<i32>, data: &[u8],
+ ) {
+ use png::{BitDepth, ColorType, Encoder};
+ use std::io::BufWriter;
+ use std::borrow::Cow;
+
+ // `png` expects
+ let data = match stride {
+ Some(stride) if stride != format.bytes_per_pixel() * size.width => {
+ let mut unstrided = Vec::new();
+ for y in 0..size.height {
+ let start = (y * stride) as usize;
+ unstrided.extend_from_slice(&data[start..start+(size.width * format.bytes_per_pixel()) as usize]);
+ }
+ Cow::from(unstrided)
+ }
+ _ => Cow::from(data),
+ };
+
+ let color_type = match format {
+ ImageFormat::RGBA8 => ColorType::RGBA,
+ ImageFormat::BGRA8 => {
+ warn!("Unable to swizzle PNG of BGRA8 type");
+ ColorType::RGBA
+ },
+ ImageFormat::R8 => ColorType::Grayscale,
+ ImageFormat::RG8 => ColorType::GrayscaleAlpha,
+ _ => {
+ error!("Unable to save PNG of {:?}", format);
+ return;
+ }
+ };
+ let w = BufWriter::new(File::create(path).unwrap());
+ let mut enc = Encoder::new(w, size.width as u32, size.height as u32);
+ enc.set_color(color_type);
+ enc.set_depth(BitDepth::Eight);
+ enc
+ .write_header()
+ .unwrap()
+ .write_image_data(&*data)
+ .unwrap();
+ }
+}
+
+/// An image that `ResourceCache` is unable to resolve during a capture.
+/// The image has to be transferred to `Renderer` and locked with the
+/// external image handler to get the actual contents and serialize them.
+#[derive(Deserialize, Serialize)]
+pub struct ExternalCaptureImage {
+ pub short_path: String,
+ pub descriptor: ImageDescriptor,
+ pub external: ExternalImageData,
+}
+
+/// A short description of an external image to be saved separately as
+/// "externals/XX.ron", redirecting into a specific texture/blob with
+/// the corresponding UV rectangle.
+#[derive(Deserialize, Serialize)]
+pub struct PlainExternalImage {
+ /// Path to the RON file describing the texel data.
+ pub data: String,
+ /// External image data source.
+ pub external: ExternalImageData,
+ /// UV sub-rectangle of the image.
+ pub uv: TexelRect,
+}
diff --git a/gfx/wr/webrender/src/clip.rs b/gfx/wr/webrender/src/clip.rs
new file mode 100644
index 0000000000..f6bf55e967
--- /dev/null
+++ b/gfx/wr/webrender/src/clip.rs
@@ -0,0 +1,2310 @@
+/* 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/. */
+
+//! Internal representation of clips in WebRender.
+//!
+//! # Data structures
+//!
+//! There are a number of data structures involved in the clip module:
+//!
+//! - ClipStore - Main interface used by other modules.
+//!
+//! - ClipItem - A single clip item (e.g. a rounded rect, or a box shadow).
+//! These are an exposed API type, stored inline in a ClipNode.
+//!
+//! - ClipNode - A ClipItem with an attached GPU handle. The GPU handle is populated
+//! when a ClipNodeInstance is built from this node (which happens while
+//! preparing primitives for render).
+//!
+//! ClipNodeInstance - A ClipNode with attached positioning information (a spatial
+//! node index). This is stored as a contiguous array of nodes
+//! within the ClipStore.
+//!
+//! ```ascii
+//! +-----------------------+-----------------------+-----------------------+
+//! | ClipNodeInstance | ClipNodeInstance | ClipNodeInstance |
+//! +-----------------------+-----------------------+-----------------------+
+//! | ClipItem | ClipItem | ClipItem |
+//! | Spatial Node Index | Spatial Node Index | Spatial Node Index |
+//! | GPU cache handle | GPU cache handle | GPU cache handle |
+//! | ... | ... | ... |
+//! +-----------------------+-----------------------+-----------------------+
+//! 0 1 2
+//! +----------------+ | |
+//! | ClipNodeRange |____| |
+//! | index: 1 | |
+//! | count: 2 |___________________________________________________|
+//! +----------------+
+//! ```
+//!
+//! - ClipNodeRange - A clip item range identifies a range of clip nodes instances.
+//! It is stored as an (index, count).
+//!
+//! - ClipChainNode - A clip chain node contains a handle to an interned clip item,
+//! positioning information (from where the clip was defined), and
+//! an optional parent link to another ClipChainNode. ClipChainId
+//! is an index into an array, or ClipChainId::NONE for no parent.
+//!
+//! ```ascii
+//! +----------------+ ____+----------------+ ____+----------------+ /---> ClipChainId::NONE
+//! | ClipChainNode | | | ClipChainNode | | | ClipChainNode | |
+//! +----------------+ | +----------------+ | +----------------+ |
+//! | ClipDataHandle | | | ClipDataHandle | | | ClipDataHandle | |
+//! | Spatial index | | | Spatial index | | | Spatial index | |
+//! | Parent Id |___| | Parent Id |___| | Parent Id |___|
+//! | ... | | ... | | ... |
+//! +----------------+ +----------------+ +----------------+
+//! ```
+//!
+//! - ClipChainInstance - A ClipChain that has been built for a specific primitive + positioning node.
+//!
+//! When given a clip chain ID, and a local primitive rect and its spatial node, the clip module
+//! creates a clip chain instance. This is a struct with various pieces of useful information
+//! (such as a local clip rect). It also contains a (index, count)
+//! range specifier into an index buffer of the ClipNodeInstance structures that are actually relevant
+//! for this clip chain instance. The index buffer structure allows a single array to be used for
+//! all of the clip-chain instances built in a single frame. Each entry in the index buffer
+//! also stores some flags relevant to the clip node in this positioning context.
+//!
+//! ```ascii
+//! +----------------------+
+//! | ClipChainInstance |
+//! +----------------------+
+//! | ... |
+//! | local_clip_rect |________________________________________________________________________
+//! | clips_range |_______________ |
+//! +----------------------+ | |
+//! | |
+//! +------------------+------------------+------------------+------------------+------------------+
+//! | ClipNodeInstance | ClipNodeInstance | ClipNodeInstance | ClipNodeInstance | ClipNodeInstance |
+//! +------------------+------------------+------------------+------------------+------------------+
+//! | flags | flags | flags | flags | flags |
+//! | ... | ... | ... | ... | ... |
+//! +------------------+------------------+------------------+------------------+------------------+
+//! ```
+//!
+//! # Rendering clipped primitives
+//!
+//! See the [`segment` module documentation][segment.rs].
+//!
+//!
+//! [segment.rs]: ../segment/index.html
+//!
+
+use api::{BorderRadius, ClipMode, ComplexClipRegion, ImageMask, ClipId, ClipChainId};
+use api::{BoxShadowClipMode, FillRule, ImageKey, ImageRendering};
+use api::units::*;
+use crate::image_tiling::{self, Repetition};
+use crate::border::{ensure_no_corner_overlap, BorderRadiusAu};
+use crate::box_shadow::{BLUR_SAMPLE_SCALE, BoxShadowClipSource, BoxShadowCacheKey};
+use crate::spatial_tree::{SpatialTree, SpatialNodeIndex};
+use crate::ellipse::Ellipse;
+use crate::gpu_cache::GpuCache;
+use crate::gpu_types::{BoxShadowStretchMode};
+use crate::intern;
+use crate::internal_types::{FastHashMap, FastHashSet, LayoutPrimitiveInfo};
+use crate::prim_store::{VisibleMaskImageTile};
+use crate::prim_store::{PointKey, SizeKey, RectangleKey, PolygonKey};
+use crate::render_task_cache::to_cache_size;
+use crate::resource_cache::{ImageRequest, ResourceCache};
+use crate::scene_builder_thread::Interners;
+use crate::space::SpaceMapper;
+use crate::util::{clamp_to_scale_factor, MaxRect, extract_inner_rect_safe, project_rect, ScaleOffset};
+use euclid::approxeq::ApproxEq;
+use std::{iter, ops, u32, mem};
+
+/// A (non-leaf) node inside a clip-tree
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct ClipTreeNode {
+ pub handle: ClipDataHandle,
+ pub parent: ClipNodeId,
+
+ children: Vec<ClipNodeId>,
+
+ // TODO(gw): Consider adding a default leaf for cases when the local_clip_rect is not relevant,
+ // that can be shared among primitives (to reduce amount of clip-chain building).
+}
+
+/// A leaf node in a clip-tree. Any primitive that is clipped will have a handle to
+/// a clip-tree leaf.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct ClipTreeLeaf {
+ pub node_id: ClipNodeId,
+
+ // TODO(gw): For now, this preserves the ability to build a culling rect
+ // from the supplied leaf local clip rect on the primitive. In
+ // future, we'll expand this to be more efficient by combining
+ // it will compatible clip rects from the `node_id`.
+ pub local_clip_rect: LayoutRect,
+}
+
+/// ID for a ClipTreeNode
+#[derive(Debug, Copy, Clone, PartialEq, MallocSizeOf, Eq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ClipNodeId(u32);
+
+impl ClipNodeId {
+ pub const NONE: ClipNodeId = ClipNodeId(0);
+}
+
+/// ID for a ClipTreeLeaf
+#[derive(Debug, Copy, Clone, PartialEq, MallocSizeOf, Eq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ClipLeafId(u32);
+
+/// A clip-tree built during scene building and used during frame-building to apply clips to primitives.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ClipTree {
+ nodes: Vec<ClipTreeNode>,
+ leaves: Vec<ClipTreeLeaf>,
+ clip_root_stack: Vec<ClipNodeId>,
+}
+
+impl ClipTree {
+ pub fn new() -> Self {
+ ClipTree {
+ nodes: vec![
+ ClipTreeNode {
+ handle: ClipDataHandle::INVALID,
+ children: Vec::new(),
+ parent: ClipNodeId::NONE,
+ }
+ ],
+ leaves: Vec::new(),
+ clip_root_stack: vec![
+ ClipNodeId::NONE,
+ ],
+ }
+ }
+
+ /// Add a set of clips to the provided tree node id, reusing existing
+ /// nodes in the tree where possible
+ fn add_impl(
+ id: ClipNodeId,
+ clips: &[ClipDataHandle],
+ nodes: &mut Vec<ClipTreeNode>,
+ ) -> ClipNodeId {
+ if clips.is_empty() {
+ return id;
+ }
+
+ let handle = clips[0];
+ let next_clips = &clips[1..];
+
+ let node_index = nodes[id.0 as usize]
+ .children
+ .iter()
+ .find(|n| nodes[n.0 as usize].handle == handle)
+ .cloned();
+
+ let node_index = match node_index {
+ Some(node_index) => node_index,
+ None => {
+ let node_index = ClipNodeId(nodes.len() as u32);
+ nodes[id.0 as usize].children.push(node_index);
+ let node = ClipTreeNode {
+ handle,
+ children: Vec::new(),
+ parent: id,
+ };
+ nodes.push(node);
+ node_index
+ }
+ };
+
+ ClipTree::add_impl(
+ node_index,
+ next_clips,
+ nodes,
+ )
+ }
+
+ /// Add a set of clips to the provided tree node id, reusing existing
+ /// nodes in the tree where possible
+ pub fn add(
+ &mut self,
+ root: ClipNodeId,
+ clips: &[ClipDataHandle],
+ ) -> ClipNodeId {
+ ClipTree::add_impl(
+ root,
+ clips,
+ &mut self.nodes,
+ )
+ }
+
+ /// Get the current clip root (the node in the clip-tree where clips can be
+ /// ignored when building the clip-chain instance for a primitive)
+ pub fn current_clip_root(&self) -> ClipNodeId {
+ self.clip_root_stack.last().cloned().unwrap()
+ }
+
+ /// Push a clip root (e.g. when a surface is encountered) that prevents clips
+ /// from this node and above being applied to primitives within the root.
+ pub fn push_clip_root_leaf(&mut self, clip_leaf_id: ClipLeafId) {
+ let leaf = &self.leaves[clip_leaf_id.0 as usize];
+ self.clip_root_stack.push(leaf.node_id);
+ }
+
+ /// Push a clip root (e.g. when a surface is encountered) that prevents clips
+ /// from this node and above being applied to primitives within the root.
+ pub fn push_clip_root_node(&mut self, clip_node_id: ClipNodeId) {
+ self.clip_root_stack.push(clip_node_id);
+ }
+
+ /// Pop a clip root, when exiting a surface.
+ pub fn pop_clip_root(&mut self) {
+ self.clip_root_stack.pop().unwrap();
+ }
+
+ /// Retrieve a clip tree node by id
+ pub fn get_node(&self, id: ClipNodeId) -> &ClipTreeNode {
+ assert!(id != ClipNodeId::NONE);
+
+ &self.nodes[id.0 as usize]
+ }
+
+ /// Retrieve a clip tree leaf by id
+ pub fn get_leaf(&self, id: ClipLeafId) -> &ClipTreeLeaf {
+ &self.leaves[id.0 as usize]
+ }
+
+ /// Debug print the clip-tree
+ #[allow(unused)]
+ pub fn print(&self) {
+ use crate::print_tree::PrintTree;
+
+ fn print_node<T: crate::print_tree::PrintTreePrinter>(
+ id: ClipNodeId,
+ nodes: &[ClipTreeNode],
+ pt: &mut T,
+ ) {
+ let node = &nodes[id.0 as usize];
+
+ pt.new_level(format!("{:?}", id));
+ pt.add_item(format!("{:?}", node.handle));
+
+ for child_id in &node.children {
+ print_node(*child_id, nodes, pt);
+ }
+
+ pt.end_level();
+ }
+
+ fn print_leaf<T: crate::print_tree::PrintTreePrinter>(
+ id: ClipLeafId,
+ leaves: &[ClipTreeLeaf],
+ pt: &mut T,
+ ) {
+ let leaf = &leaves[id.0 as usize];
+
+ pt.new_level(format!("{:?}", id));
+ pt.add_item(format!("node_id: {:?}", leaf.node_id));
+ pt.add_item(format!("local_clip_rect: {:?}", leaf.local_clip_rect));
+ pt.end_level();
+ }
+
+ let mut pt = PrintTree::new("clip tree");
+ print_node(ClipNodeId::NONE, &self.nodes, &mut pt);
+
+ for i in 0 .. self.leaves.len() {
+ print_leaf(ClipLeafId(i as u32), &self.leaves, &mut pt);
+ }
+ }
+
+ /// Find the lowest common ancestor of two clip tree nodes. This is useful
+ /// to identify shared clips between primitives attached to different clip-leaves.
+ pub fn find_lowest_common_ancestor(
+ &self,
+ mut node1: ClipNodeId,
+ mut node2: ClipNodeId,
+ ) -> ClipNodeId {
+ // TODO(gw): Consider caching / storing the depth in the node?
+ fn get_node_depth(
+ id: ClipNodeId,
+ nodes: &[ClipTreeNode],
+ ) -> usize {
+ let mut depth = 0;
+ let mut current = id;
+
+ while current != ClipNodeId::NONE {
+ let node = &nodes[current.0 as usize];
+ depth += 1;
+ current = node.parent;
+ }
+
+ depth
+ }
+
+ let mut depth1 = get_node_depth(node1, &self.nodes);
+ let mut depth2 = get_node_depth(node2, &self.nodes);
+
+ while depth1 > depth2 {
+ node1 = self.nodes[node1.0 as usize].parent;
+ depth1 -= 1;
+ }
+
+ while depth2 > depth1 {
+ node2 = self.nodes[node2.0 as usize].parent;
+ depth2 -= 1;
+ }
+
+ while node1 != node2 {
+ node1 = self.nodes[node1.0 as usize].parent;
+ node2 = self.nodes[node2.0 as usize].parent;
+ }
+
+ node1
+ }
+}
+
+/// Represents a clip-chain as defined by the public API that we decompose in to
+/// the clip-tree. In future, we would like to remove this and have Gecko directly
+/// build the clip-tree.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ClipChain {
+ parent: Option<usize>,
+ clips: Vec<ClipDataHandle>,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ClipStackEntry {
+ /// Cache the previous clip-chain build, since this is a common case
+ last_clip_chain_cache: Option<(ClipChainId, ClipNodeId)>,
+
+ /// Set of clips that were already seen and included in clip_node_id
+ seen_clips: FastHashSet<ClipDataHandle>,
+
+ /// The build clip_node_id for this level of the stack
+ clip_node_id: ClipNodeId,
+}
+
+/// Used by the scene builder to build the clip-tree that is part of the built scene.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ClipTreeBuilder {
+ /// Clips defined by the display list
+ clip_map: FastHashMap<ClipId, ClipDataHandle>,
+
+ /// Clip-chains defined by the display list
+ clip_chains: Vec<ClipChain>,
+ clip_chain_map: FastHashMap<ClipChainId, usize>,
+
+ /// List of clips pushed/popped by grouping items, such as stacking contexts and iframes
+ clip_stack: Vec<ClipStackEntry>,
+
+ /// The tree we are building
+ tree: ClipTree,
+
+ /// A temporary buffer stored here to avoid constant heap allocs/frees
+ clip_handles_buffer: Vec<ClipDataHandle>,
+}
+
+impl ClipTreeBuilder {
+ pub fn new() -> Self {
+ ClipTreeBuilder {
+ clip_map: FastHashMap::default(),
+ clip_chain_map: FastHashMap::default(),
+ clip_chains: Vec::new(),
+ clip_stack: vec![
+ ClipStackEntry {
+ clip_node_id: ClipNodeId::NONE,
+ last_clip_chain_cache: None,
+ seen_clips: FastHashSet::default(),
+ },
+ ],
+ tree: ClipTree::new(),
+ clip_handles_buffer: Vec::new(),
+ }
+ }
+
+ /// Define a new rect clip
+ pub fn define_rect_clip(
+ &mut self,
+ id: ClipId,
+ handle: ClipDataHandle,
+ ) {
+ self.clip_map.insert(id, handle);
+ }
+
+ /// Define a new rounded rect clip
+ pub fn define_rounded_rect_clip(
+ &mut self,
+ id: ClipId,
+ handle: ClipDataHandle,
+ ) {
+ self.clip_map.insert(id, handle);
+ }
+
+ /// Define a image mask clip
+ pub fn define_image_mask_clip(
+ &mut self,
+ id: ClipId,
+ handle: ClipDataHandle,
+ ) {
+ self.clip_map.insert(id, handle);
+ }
+
+ /// Define a clip-chain
+ pub fn define_clip_chain<I: Iterator<Item = ClipId>>(
+ &mut self,
+ id: ClipChainId,
+ parent: Option<ClipChainId>,
+ clips: I,
+ ) {
+ let parent = parent.map(|ref id| self.clip_chain_map[id]);
+ let index = self.clip_chains.len();
+ let clips = clips.map(|clip_id| {
+ self.clip_map[&clip_id]
+ }).collect();
+ self.clip_chains.push(ClipChain {
+ parent,
+ clips,
+ });
+ self.clip_chain_map.insert(id, index);
+ }
+
+ /// Push a clip-chain that will be applied to any prims built prior to next pop
+ pub fn push_clip_chain(
+ &mut self,
+ clip_chain_id: Option<ClipChainId>,
+ reset_seen: bool,
+ ) {
+ let (mut clip_node_id, mut seen_clips) = {
+ let prev = self.clip_stack.last().unwrap();
+ (prev.clip_node_id, prev.seen_clips.clone())
+ };
+
+ if let Some(clip_chain_id) = clip_chain_id {
+ if clip_chain_id != ClipChainId::INVALID {
+ self.clip_handles_buffer.clear();
+
+ let clip_chain_index = self.clip_chain_map[&clip_chain_id];
+ ClipTreeBuilder::add_clips(
+ clip_chain_index,
+ &mut seen_clips,
+ &mut self.clip_handles_buffer,
+ &self.clip_chains,
+ );
+
+ clip_node_id = self.tree.add(
+ clip_node_id,
+ &self.clip_handles_buffer,
+ );
+ }
+ }
+
+ if reset_seen {
+ seen_clips.clear();
+ }
+
+ self.clip_stack.push(ClipStackEntry {
+ last_clip_chain_cache: None,
+ clip_node_id,
+ seen_clips,
+ });
+ }
+
+ /// Push a clip-id that will be applied to any prims built prior to next pop
+ pub fn push_clip_id(
+ &mut self,
+ clip_id: ClipId,
+ ) {
+ let (clip_node_id, mut seen_clips) = {
+ let prev = self.clip_stack.last().unwrap();
+ (prev.clip_node_id, prev.seen_clips.clone())
+ };
+
+ self.clip_handles_buffer.clear();
+ let clip_index = self.clip_map[&clip_id];
+
+ if seen_clips.insert(clip_index) {
+ self.clip_handles_buffer.push(clip_index);
+ }
+
+ let clip_node_id = self.tree.add(
+ clip_node_id,
+ &self.clip_handles_buffer,
+ );
+
+ self.clip_stack.push(ClipStackEntry {
+ last_clip_chain_cache: None,
+ seen_clips,
+ clip_node_id,
+ });
+ }
+
+ /// Pop a clip off the clip_stack, when exiting a grouping item
+ pub fn pop_clip(&mut self) {
+ self.clip_stack.pop().unwrap();
+ }
+
+ /// Add clips from a given clip-chain to the set of clips for a primitive during clip-set building
+ fn add_clips(
+ clip_chain_index: usize,
+ seen_clips: &mut FastHashSet<ClipDataHandle>,
+ output: &mut Vec<ClipDataHandle>,
+ clip_chains: &[ClipChain],
+ ) {
+ // TODO(gw): It's possible that we may see clip outputs that include identical clips
+ // (e.g. if there is a clip positioned by two spatial nodes, where one spatial
+ // node is a child of the other, and has an identity transform). If we ever
+ // see this in real-world cases, it might be worth checking for that here and
+ // excluding them, to ensure the shape of the tree matches what we need for
+ // finding shared_clips for tile caches etc.
+
+ let clip_chain = &clip_chains[clip_chain_index];
+
+ if let Some(parent) = clip_chain.parent {
+ ClipTreeBuilder::add_clips(
+ parent,
+ seen_clips,
+ output,
+ clip_chains,
+ );
+ }
+
+ for clip_index in clip_chain.clips.iter().rev() {
+ if seen_clips.insert(*clip_index) {
+ output.push(*clip_index);
+ }
+ }
+ }
+
+ /// Main entry point to build a path in the clip-tree for a given primitive
+ pub fn build_clip_set(
+ &mut self,
+ clip_chain_id: ClipChainId,
+ ) -> ClipNodeId {
+ let clip_stack = self.clip_stack.last_mut().unwrap();
+
+ if clip_chain_id == ClipChainId::INVALID {
+ clip_stack.clip_node_id
+ } else {
+ if let Some((cached_clip_chain, cached_clip_node)) = clip_stack.last_clip_chain_cache {
+ if cached_clip_chain == clip_chain_id {
+ return cached_clip_node;
+ }
+ }
+
+ let clip_chain_index = self.clip_chain_map[&clip_chain_id];
+
+ self.clip_handles_buffer.clear();
+
+ ClipTreeBuilder::add_clips(
+ clip_chain_index,
+ &mut clip_stack.seen_clips,
+ &mut self.clip_handles_buffer,
+ &self.clip_chains,
+ );
+
+ // We mutated the `clip_stack.seen_clips` in order to remove duplicate clips from
+ // the supplied `clip_chain_id`. Now step through and remove any clips we added
+ // to the set, so we don't get incorrect results next time `build_clip_set` is
+ // called for a different clip-chain. Doing it this way rather than cloning means
+ // we avoid heap allocations for each `build_clip_set` call.
+ for handle in &self.clip_handles_buffer {
+ clip_stack.seen_clips.remove(handle);
+ }
+
+ let clip_node_id = self.tree.add(
+ clip_stack.clip_node_id,
+ &self.clip_handles_buffer,
+ );
+
+ clip_stack.last_clip_chain_cache = Some((clip_chain_id, clip_node_id));
+
+ clip_node_id
+ }
+ }
+
+ /// Recursive impl to check if a clip-chain has complex (non-rectangular) clips
+ fn has_complex_clips_impl(
+ &self,
+ clip_chain_index: usize,
+ interners: &Interners,
+ ) -> bool {
+ let clip_chain = &self.clip_chains[clip_chain_index];
+
+ for clip_handle in &clip_chain.clips {
+ let clip_info = &interners.clip[*clip_handle];
+
+ if let ClipNodeKind::Complex = clip_info.key.kind.node_kind() {
+ return true;
+ }
+ }
+
+ match clip_chain.parent {
+ Some(parent) => self.has_complex_clips_impl(parent, interners),
+ None => false,
+ }
+ }
+
+ /// Check if a clip-chain has complex (non-rectangular) clips
+ pub fn clip_chain_has_complex_clips(
+ &self,
+ clip_chain_id: ClipChainId,
+ interners: &Interners,
+ ) -> bool {
+ let clip_chain_index = self.clip_chain_map[&clip_chain_id];
+ self.has_complex_clips_impl(clip_chain_index, interners)
+ }
+
+ /// Check if a clip-node has complex (non-rectangular) clips
+ pub fn clip_node_has_complex_clips(
+ &self,
+ clip_node_id: ClipNodeId,
+ interners: &Interners,
+ ) -> bool {
+ let mut current = clip_node_id;
+
+ while current != ClipNodeId::NONE {
+ let node = &self.tree.nodes[current.0 as usize];
+ let clip_info = &interners.clip[node.handle];
+
+ if let ClipNodeKind::Complex = clip_info.key.kind.node_kind() {
+ return true;
+ }
+
+ current = node.parent;
+ }
+
+ false
+ }
+
+ /// Finalize building and return the clip-tree
+ pub fn finalize(self) -> ClipTree {
+ self.tree
+ }
+
+ /// Get a clip node by id
+ pub fn get_node(&self, id: ClipNodeId) -> &ClipTreeNode {
+ assert!(id != ClipNodeId::NONE);
+
+ &self.tree.nodes[id.0 as usize]
+ }
+
+ /// Get a clip leaf by id
+ pub fn get_leaf(&self, id: ClipLeafId) -> &ClipTreeLeaf {
+ &self.tree.leaves[id.0 as usize]
+ }
+
+ /// Build a clip-leaf for a tile-cache
+ pub fn build_for_tile_cache(
+ &mut self,
+ clip_node_id: ClipNodeId,
+ extra_clips: &[ClipId],
+ ) -> ClipLeafId {
+ self.clip_handles_buffer.clear();
+
+ for clip_id in extra_clips {
+ let handle = self.clip_map[clip_id];
+ self.clip_handles_buffer.push(handle);
+ }
+
+ let node_id = self.tree.add(
+ clip_node_id,
+ &self.clip_handles_buffer,
+ );
+
+ let clip_leaf_id = ClipLeafId(self.tree.leaves.len() as u32);
+
+ self.tree.leaves.push(ClipTreeLeaf {
+ node_id,
+ local_clip_rect: LayoutRect::max_rect(),
+ });
+
+ clip_leaf_id
+ }
+
+ /// Build a clip-leaf for a picture
+ pub fn build_for_picture(
+ &mut self,
+ clip_node_id: ClipNodeId,
+ ) -> ClipLeafId {
+ let node_id = self.tree.add(
+ clip_node_id,
+ &[],
+ );
+
+ let clip_leaf_id = ClipLeafId(self.tree.leaves.len() as u32);
+
+ self.tree.leaves.push(ClipTreeLeaf {
+ node_id,
+ local_clip_rect: LayoutRect::max_rect(),
+ });
+
+ clip_leaf_id
+ }
+
+ /// Build a clip-leaf for a normal primitive
+ pub fn build_for_prim(
+ &mut self,
+ clip_node_id: ClipNodeId,
+ info: &LayoutPrimitiveInfo,
+ extra_clips: &[ClipItemKey],
+ interners: &mut Interners,
+ ) -> ClipLeafId {
+
+ let node_id = if extra_clips.is_empty() {
+ clip_node_id
+ } else {
+ // TODO(gw): Cache the previous build of clip-node / clip-leaf to handle cases where we get a
+ // lot of primitives referencing the same clip set (e.g. dl_mutate and similar tests)
+ self.clip_handles_buffer.clear();
+
+ for item in extra_clips {
+ // Intern this clip item, and store the handle
+ // in the clip chain node.
+ let handle = interners.clip.intern(item, || {
+ ClipInternData {
+ key: item.clone(),
+ }
+ });
+
+ self.clip_handles_buffer.push(handle);
+ }
+
+ self.tree.add(
+ clip_node_id,
+ &self.clip_handles_buffer,
+ )
+ };
+
+ let clip_leaf_id = ClipLeafId(self.tree.leaves.len() as u32);
+
+ self.tree.leaves.push(ClipTreeLeaf {
+ node_id,
+ local_clip_rect: info.clip_rect,
+ });
+
+ clip_leaf_id
+ }
+
+ // Find the LCA for two given clip nodes
+ pub fn find_lowest_common_ancestor(
+ &self,
+ node1: ClipNodeId,
+ node2: ClipNodeId,
+ ) -> ClipNodeId {
+ self.tree.find_lowest_common_ancestor(node1, node2)
+ }
+}
+
+// Type definitions for interning clip nodes.
+
+#[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq, Eq, Hash)]
+#[cfg_attr(any(feature = "serde"), derive(Deserialize, Serialize))]
+pub enum ClipIntern {}
+
+pub type ClipDataStore = intern::DataStore<ClipIntern>;
+pub type ClipDataHandle = intern::Handle<ClipIntern>;
+
+/// Helper to identify simple clips (normal rects) from other kinds of clips,
+/// which can often be handled via fast code paths.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, MallocSizeOf)]
+pub enum ClipNodeKind {
+ /// A normal clip rectangle, with Clip mode.
+ Rectangle,
+ /// A rectangle with ClipOut, or any other kind of clip.
+ Complex,
+}
+
+// Result of comparing a clip node instance against a local rect.
+#[derive(Debug)]
+enum ClipResult {
+ // The clip does not affect the region at all.
+ Accept,
+ // The clip prevents the region from being drawn.
+ Reject,
+ // The clip affects part of the region. This may
+ // require a clip mask, depending on other factors.
+ Partial,
+}
+
+// A clip node is a single clip source, along with some
+// positioning information and implementation details
+// that control where the GPU data for this clip source
+// can be found.
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct ClipNode {
+ pub item: ClipItem,
+}
+
+// Convert from an interning key for a clip item
+// to a clip node, which is cached in the document.
+impl From<ClipItemKey> for ClipNode {
+ fn from(item: ClipItemKey) -> Self {
+ let kind = match item.kind {
+ ClipItemKeyKind::Rectangle(rect, mode) => {
+ ClipItemKind::Rectangle { rect: rect.into(), mode }
+ }
+ ClipItemKeyKind::RoundedRectangle(rect, radius, mode) => {
+ ClipItemKind::RoundedRectangle {
+ rect: rect.into(),
+ radius: radius.into(),
+ mode,
+ }
+ }
+ ClipItemKeyKind::ImageMask(rect, image, polygon_handle) => {
+ ClipItemKind::Image {
+ image,
+ rect: rect.into(),
+ polygon_handle,
+ }
+ }
+ ClipItemKeyKind::BoxShadow(shadow_rect_fract_offset, shadow_rect_size, shadow_radius, prim_shadow_rect, blur_radius, clip_mode) => {
+ ClipItemKind::new_box_shadow(
+ shadow_rect_fract_offset.into(),
+ shadow_rect_size.into(),
+ shadow_radius.into(),
+ prim_shadow_rect.into(),
+ blur_radius.to_f32_px(),
+ clip_mode,
+ )
+ }
+ };
+
+ ClipNode {
+ item: ClipItem {
+ kind,
+ spatial_node_index: item.spatial_node_index,
+ },
+ }
+ }
+}
+
+// Flags that are attached to instances of clip nodes.
+bitflags! {
+ #[cfg_attr(feature = "capture", derive(Serialize))]
+ #[cfg_attr(feature = "replay", derive(Deserialize))]
+ #[derive(MallocSizeOf)]
+ pub struct ClipNodeFlags: u8 {
+ const SAME_SPATIAL_NODE = 0x1;
+ const SAME_COORD_SYSTEM = 0x2;
+ const USE_FAST_PATH = 0x4;
+ }
+}
+
+// When a clip node is found to be valid for a
+// clip chain instance, it's stored in an index
+// buffer style structure. This struct contains
+// an index to the node data itself, as well as
+// some flags describing how this clip node instance
+// is positioned.
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ClipNodeInstance {
+ pub handle: ClipDataHandle,
+ pub flags: ClipNodeFlags,
+ pub visible_tiles: Option<ops::Range<usize>>,
+}
+
+impl ClipNodeInstance {
+ pub fn has_visible_tiles(&self) -> bool {
+ self.visible_tiles.is_some()
+ }
+}
+
+// A range of clip node instances that were found by
+// building a clip chain instance.
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ClipNodeRange {
+ pub first: u32,
+ pub count: u32,
+}
+
+impl ClipNodeRange {
+ pub fn to_range(&self) -> ops::Range<usize> {
+ let start = self.first as usize;
+ let end = start + self.count as usize;
+
+ ops::Range {
+ start,
+ end,
+ }
+ }
+}
+
+/// A helper struct for converting between coordinate systems
+/// of clip sources and primitives.
+// todo(gw): optimize:
+// separate arrays for matrices
+// cache and only build as needed.
+//TODO: merge with `CoordinateSpaceMapping`?
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+enum ClipSpaceConversion {
+ Local,
+ ScaleOffset(ScaleOffset),
+ Transform(LayoutToWorldTransform),
+}
+
+impl ClipSpaceConversion {
+ /// Construct a new clip space converter between two spatial nodes.
+ fn new(
+ prim_spatial_node_index: SpatialNodeIndex,
+ clip_spatial_node_index: SpatialNodeIndex,
+ spatial_tree: &SpatialTree,
+ ) -> Self {
+ //Note: this code is different from `get_relative_transform` in a way that we only try
+ // getting the relative transform if it's Local or ScaleOffset,
+ // falling back to the world transform otherwise.
+ let clip_spatial_node = spatial_tree.get_spatial_node(clip_spatial_node_index);
+ let prim_spatial_node = spatial_tree.get_spatial_node(prim_spatial_node_index);
+
+ if prim_spatial_node_index == clip_spatial_node_index {
+ ClipSpaceConversion::Local
+ } else if prim_spatial_node.coordinate_system_id == clip_spatial_node.coordinate_system_id {
+ let scale_offset = prim_spatial_node.content_transform
+ .inverse()
+ .accumulate(&clip_spatial_node.content_transform);
+ ClipSpaceConversion::ScaleOffset(scale_offset)
+ } else {
+ ClipSpaceConversion::Transform(
+ spatial_tree
+ .get_world_transform(clip_spatial_node_index)
+ .into_transform()
+ )
+ }
+ }
+
+ fn to_flags(&self) -> ClipNodeFlags {
+ match *self {
+ ClipSpaceConversion::Local => {
+ ClipNodeFlags::SAME_SPATIAL_NODE | ClipNodeFlags::SAME_COORD_SYSTEM
+ }
+ ClipSpaceConversion::ScaleOffset(..) => {
+ ClipNodeFlags::SAME_COORD_SYSTEM
+ }
+ ClipSpaceConversion::Transform(..) => {
+ ClipNodeFlags::empty()
+ }
+ }
+ }
+}
+
+// Temporary information that is cached and reused
+// during building of a clip chain instance.
+#[derive(MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+struct ClipNodeInfo {
+ conversion: ClipSpaceConversion,
+ handle: ClipDataHandle,
+}
+
+impl ClipNodeInfo {
+ fn create_instance(
+ &self,
+ node: &ClipNode,
+ clipped_rect: &LayoutRect,
+ gpu_cache: &mut GpuCache,
+ resource_cache: &mut ResourceCache,
+ mask_tiles: &mut Vec<VisibleMaskImageTile>,
+ spatial_tree: &SpatialTree,
+ request_resources: bool,
+ ) -> Option<ClipNodeInstance> {
+ // Calculate some flags that are required for the segment
+ // building logic.
+ let mut flags = self.conversion.to_flags();
+
+ // Some clip shaders support a fast path mode for simple clips.
+ // TODO(gw): We could also apply fast path when segments are created, since we only write
+ // the mask for a single corner at a time then, so can always consider radii uniform.
+ let is_raster_2d =
+ flags.contains(ClipNodeFlags::SAME_COORD_SYSTEM) ||
+ spatial_tree
+ .get_world_viewport_transform(node.item.spatial_node_index)
+ .is_2d_axis_aligned();
+ if is_raster_2d && node.item.kind.supports_fast_path_rendering() {
+ flags |= ClipNodeFlags::USE_FAST_PATH;
+ }
+
+ let mut visible_tiles = None;
+
+ if let ClipItemKind::Image { rect, image, .. } = node.item.kind {
+ let request = ImageRequest {
+ key: image,
+ rendering: ImageRendering::Auto,
+ tile: None,
+ };
+
+ if let Some(props) = resource_cache.get_image_properties(image) {
+ if let Some(tile_size) = props.tiling {
+ let tile_range_start = mask_tiles.len();
+
+ // Bug 1648323 - It is unclear why on rare occasions we get
+ // a clipped_rect that does not intersect the clip's mask rect.
+ // defaulting to clipped_rect here results in zero repetitions
+ // which clips the primitive entirely.
+ let visible_rect =
+ clipped_rect.intersection(&rect).unwrap_or(*clipped_rect);
+
+ let repetitions = image_tiling::repetitions(
+ &rect,
+ &visible_rect,
+ rect.size(),
+ );
+
+ for Repetition { origin, .. } in repetitions {
+ let layout_image_rect = LayoutRect::from_origin_and_size(
+ origin,
+ rect.size(),
+ );
+ let tiles = image_tiling::tiles(
+ &layout_image_rect,
+ &visible_rect,
+ &props.visible_rect,
+ tile_size as i32,
+ );
+ for tile in tiles {
+ if request_resources {
+ resource_cache.request_image(
+ request.with_tile(tile.offset),
+ gpu_cache,
+ );
+ }
+ mask_tiles.push(VisibleMaskImageTile {
+ tile_offset: tile.offset,
+ tile_rect: tile.rect,
+ });
+ }
+ }
+ visible_tiles = Some(tile_range_start..mask_tiles.len());
+ } else if request_resources {
+ resource_cache.request_image(request, gpu_cache);
+ }
+ } else {
+ // If the supplied image key doesn't exist in the resource cache,
+ // skip the clip node since there is nothing to mask with.
+ warn!("Clip mask with missing image key {:?}", request.key);
+ return None;
+ }
+ }
+
+ Some(ClipNodeInstance {
+ handle: self.handle,
+ flags,
+ visible_tiles,
+ })
+ }
+}
+
+impl ClipNode {
+ pub fn update(
+ &mut self,
+ device_pixel_scale: DevicePixelScale,
+ ) {
+ match self.item.kind {
+ ClipItemKind::Image { .. } |
+ ClipItemKind::Rectangle { .. } |
+ ClipItemKind::RoundedRectangle { .. } => {}
+
+ ClipItemKind::BoxShadow { ref mut source } => {
+ // Quote from https://drafts.csswg.org/css-backgrounds-3/#shadow-blur
+ // "the image that would be generated by applying to the shadow a
+ // Gaussian blur with a standard deviation equal to half the blur radius."
+ let blur_radius_dp = source.blur_radius * 0.5;
+
+ // Create scaling from requested size to cache size.
+ let mut content_scale = LayoutToWorldScale::new(1.0) * device_pixel_scale;
+ content_scale.0 = clamp_to_scale_factor(content_scale.0, false);
+
+ // Create the cache key for this box-shadow render task.
+ let cache_size = to_cache_size(source.shadow_rect_alloc_size, &mut content_scale);
+
+ let bs_cache_key = BoxShadowCacheKey {
+ blur_radius_dp: (blur_radius_dp * content_scale.0).round() as i32,
+ clip_mode: source.clip_mode,
+ original_alloc_size: (source.original_alloc_size * content_scale).round().to_i32(),
+ br_top_left: (source.shadow_radius.top_left * content_scale).round().to_i32(),
+ br_top_right: (source.shadow_radius.top_right * content_scale).round().to_i32(),
+ br_bottom_right: (source.shadow_radius.bottom_right * content_scale).round().to_i32(),
+ br_bottom_left: (source.shadow_radius.bottom_left * content_scale).round().to_i32(),
+ device_pixel_scale: Au::from_f32_px(content_scale.0),
+ };
+
+ source.cache_key = Some((cache_size, bs_cache_key));
+ }
+ }
+ }
+}
+
+#[derive(Default)]
+pub struct ClipStoreScratchBuffer {
+ clip_node_instances: Vec<ClipNodeInstance>,
+ mask_tiles: Vec<VisibleMaskImageTile>,
+}
+
+/// The main clipping public interface that other modules access.
+#[derive(MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct ClipStore {
+ pub clip_node_instances: Vec<ClipNodeInstance>,
+ mask_tiles: Vec<VisibleMaskImageTile>,
+
+ active_clip_node_info: Vec<ClipNodeInfo>,
+ active_local_clip_rect: Option<LayoutRect>,
+ active_pic_coverage_rect: PictureRect,
+}
+
+// A clip chain instance is what gets built for a given clip
+// chain id + local primitive region + positioning node.
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct ClipChainInstance {
+ pub clips_range: ClipNodeRange,
+ // Combined clip rect for clips that are in the
+ // same coordinate system as the primitive.
+ pub local_clip_rect: LayoutRect,
+ pub has_non_local_clips: bool,
+ // If true, this clip chain requires allocation
+ // of a clip mask.
+ pub needs_mask: bool,
+ // Combined clip rect in picture space (may
+ // be more conservative that local_clip_rect).
+ pub pic_coverage_rect: PictureRect,
+ // Space, in which the `pic_coverage_rect` is defined.
+ pub pic_spatial_node_index: SpatialNodeIndex,
+}
+
+impl ClipChainInstance {
+ pub fn empty() -> Self {
+ ClipChainInstance {
+ clips_range: ClipNodeRange {
+ first: 0,
+ count: 0,
+ },
+ local_clip_rect: LayoutRect::zero(),
+ has_non_local_clips: false,
+ needs_mask: false,
+ pic_coverage_rect: PictureRect::zero(),
+ pic_spatial_node_index: SpatialNodeIndex::INVALID,
+ }
+ }
+}
+
+impl ClipStore {
+ pub fn new() -> Self {
+ ClipStore {
+ clip_node_instances: Vec::new(),
+ mask_tiles: Vec::new(),
+ active_clip_node_info: Vec::new(),
+ active_local_clip_rect: None,
+ active_pic_coverage_rect: PictureRect::max_rect(),
+ }
+ }
+
+ pub fn get_instance_from_range(
+ &self,
+ node_range: &ClipNodeRange,
+ index: u32,
+ ) -> &ClipNodeInstance {
+ &self.clip_node_instances[(node_range.first + index) as usize]
+ }
+
+ /// Setup the active clip chains for building a clip chain instance.
+ pub fn set_active_clips(
+ &mut self,
+ prim_spatial_node_index: SpatialNodeIndex,
+ pic_spatial_node_index: SpatialNodeIndex,
+ clip_leaf_id: ClipLeafId,
+ spatial_tree: &SpatialTree,
+ clip_data_store: &ClipDataStore,
+ clip_tree: &ClipTree,
+ ) {
+ self.active_clip_node_info.clear();
+ self.active_local_clip_rect = None;
+ self.active_pic_coverage_rect = PictureRect::max_rect();
+
+ let clip_root = clip_tree.current_clip_root();
+ let clip_leaf = clip_tree.get_leaf(clip_leaf_id);
+
+ let mut local_clip_rect = clip_leaf.local_clip_rect;
+ let mut current = clip_leaf.node_id;
+
+ while current != clip_root {
+ let node = clip_tree.get_node(current);
+
+ if !add_clip_node_to_current_chain(
+ node.handle,
+ prim_spatial_node_index,
+ pic_spatial_node_index,
+ &mut local_clip_rect,
+ &mut self.active_clip_node_info,
+ &mut self.active_pic_coverage_rect,
+ clip_data_store,
+ spatial_tree,
+ ) {
+ return;
+ }
+
+ current = node.parent;
+ }
+
+ self.active_local_clip_rect = Some(local_clip_rect);
+ }
+
+ /// Setup the active clip chains, based on an existing primitive clip chain instance.
+ pub fn set_active_clips_from_clip_chain(
+ &mut self,
+ prim_clip_chain: &ClipChainInstance,
+ prim_spatial_node_index: SpatialNodeIndex,
+ spatial_tree: &SpatialTree,
+ clip_data_store: &ClipDataStore,
+ ) {
+ // TODO(gw): Although this does less work than set_active_clips(), it does
+ // still do some unnecessary work (such as the clip space conversion).
+ // We could consider optimizing this if it ever shows up in a profile.
+
+ self.active_clip_node_info.clear();
+ self.active_local_clip_rect = Some(prim_clip_chain.local_clip_rect);
+ self.active_pic_coverage_rect = prim_clip_chain.pic_coverage_rect;
+
+ let clip_instances = &self
+ .clip_node_instances[prim_clip_chain.clips_range.to_range()];
+ for clip_instance in clip_instances {
+ let clip = &clip_data_store[clip_instance.handle];
+ let conversion = ClipSpaceConversion::new(
+ prim_spatial_node_index,
+ clip.item.spatial_node_index,
+ spatial_tree,
+ );
+ self.active_clip_node_info.push(ClipNodeInfo {
+ handle: clip_instance.handle,
+ conversion,
+ });
+ }
+ }
+
+ /// Given a clip-chain instance, return a safe rect within the visible region
+ /// that can be assumed to be unaffected by clip radii. Returns None if it
+ /// encounters any complex cases, just handling rounded rects in the same
+ /// coordinate system as the clip-chain for now.
+ pub fn get_inner_rect_for_clip_chain(
+ &self,
+ clip_chain: &ClipChainInstance,
+ clip_data_store: &ClipDataStore,
+ spatial_tree: &SpatialTree,
+ ) -> Option<PictureRect> {
+ let mut inner_rect = clip_chain.pic_coverage_rect;
+ let clip_instances = &self
+ .clip_node_instances[clip_chain.clips_range.to_range()];
+
+ for clip_instance in clip_instances {
+ // Don't handle mapping between coord systems for now
+ if !clip_instance.flags.contains(ClipNodeFlags::SAME_COORD_SYSTEM) {
+ return None;
+ }
+
+ let clip_node = &clip_data_store[clip_instance.handle];
+
+ match clip_node.item.kind {
+ // Ignore any clips which are complex or impossible to calculate
+ // inner rects for now
+ ClipItemKind::Rectangle { mode: ClipMode::ClipOut, .. } |
+ ClipItemKind::Image { .. } |
+ ClipItemKind::BoxShadow { .. } |
+ ClipItemKind::RoundedRectangle { mode: ClipMode::ClipOut, .. } => {
+ return None;
+ }
+ // Normal Clip rects are already handled by the clip-chain pic_coverage_rect,
+ // no need to do anything here
+ ClipItemKind::Rectangle { mode: ClipMode::Clip, .. } => {}
+ ClipItemKind::RoundedRectangle { mode: ClipMode::Clip, rect, radius } => {
+ // Get an inner rect for the rounded-rect clip
+ let local_inner_rect = match extract_inner_rect_safe(&rect, &radius) {
+ Some(rect) => rect,
+ None => return None,
+ };
+
+ // Map it from local -> picture space
+ let mapper = SpaceMapper::new_with_target(
+ clip_chain.pic_spatial_node_index,
+ clip_node.item.spatial_node_index,
+ PictureRect::max_rect(),
+ spatial_tree,
+ );
+
+ // Accumulate in to the inner_rect, in case there are multiple rounded-rect clips
+ if let Some(pic_inner_rect) = mapper.map(&local_inner_rect) {
+ inner_rect = inner_rect.intersection(&pic_inner_rect).unwrap_or(PictureRect::zero());
+ }
+ }
+ }
+ }
+
+ Some(inner_rect)
+ }
+
+ /// The main interface external code uses. Given a local primitive, positioning
+ /// information, and a clip chain id, build an optimized clip chain instance.
+ pub fn build_clip_chain_instance(
+ &mut self,
+ local_prim_rect: LayoutRect,
+ prim_to_pic_mapper: &SpaceMapper<LayoutPixel, PicturePixel>,
+ pic_to_world_mapper: &SpaceMapper<PicturePixel, WorldPixel>,
+ spatial_tree: &SpatialTree,
+ gpu_cache: &mut GpuCache,
+ resource_cache: &mut ResourceCache,
+ device_pixel_scale: DevicePixelScale,
+ world_rect: &WorldRect,
+ clip_data_store: &mut ClipDataStore,
+ request_resources: bool,
+ ) -> Option<ClipChainInstance> {
+ let local_clip_rect = match self.active_local_clip_rect {
+ Some(rect) => rect,
+ None => return None,
+ };
+ profile_scope!("build_clip_chain_instance");
+
+ let local_bounding_rect = local_prim_rect.intersection(&local_clip_rect)?;
+ let mut pic_coverage_rect = prim_to_pic_mapper.map(&local_bounding_rect)?;
+ let world_clip_rect = pic_to_world_mapper.map(&pic_coverage_rect)?;
+
+ // Now, we've collected all the clip nodes that *potentially* affect this
+ // primitive region, and reduced the size of the prim region as much as possible.
+
+ // Run through the clip nodes, and see which ones affect this prim region.
+
+ let first_clip_node_index = self.clip_node_instances.len() as u32;
+ let mut has_non_local_clips = false;
+ let mut needs_mask = false;
+
+ // For each potential clip node
+ for node_info in self.active_clip_node_info.drain(..) {
+ let node = &mut clip_data_store[node_info.handle];
+
+ // See how this clip affects the prim region.
+ let clip_result = match node_info.conversion {
+ ClipSpaceConversion::Local => {
+ node.item.kind.get_clip_result(&local_bounding_rect)
+ }
+ ClipSpaceConversion::ScaleOffset(ref scale_offset) => {
+ has_non_local_clips = true;
+ node.item.kind.get_clip_result(&scale_offset.unmap_rect(&local_bounding_rect))
+ }
+ ClipSpaceConversion::Transform(ref transform) => {
+ has_non_local_clips = true;
+ node.item.kind.get_clip_result_complex(
+ transform,
+ &world_clip_rect,
+ world_rect,
+ )
+ }
+ };
+
+ match clip_result {
+ ClipResult::Accept => {
+ // Doesn't affect the primitive at all, so skip adding to list
+ }
+ ClipResult::Reject => {
+ // Completely clips the supplied prim rect
+ return None;
+ }
+ ClipResult::Partial => {
+ // Needs a mask -> add to clip node indices
+
+ // TODO(gw): Ensure this only runs once on each node per frame?
+ node.update(device_pixel_scale);
+
+ // Create the clip node instance for this clip node
+ if let Some(instance) = node_info.create_instance(
+ node,
+ &local_bounding_rect,
+ gpu_cache,
+ resource_cache,
+ &mut self.mask_tiles,
+ spatial_tree,
+ request_resources,
+ ) {
+ // As a special case, a partial accept of a clip rect that is
+ // in the same coordinate system as the primitive doesn't need
+ // a clip mask. Instead, it can be handled by the primitive
+ // vertex shader as part of the local clip rect. This is an
+ // important optimization for reducing the number of clip
+ // masks that are allocated on common pages.
+ needs_mask |= match node.item.kind {
+ ClipItemKind::Rectangle { mode: ClipMode::ClipOut, .. } |
+ ClipItemKind::RoundedRectangle { .. } |
+ ClipItemKind::Image { .. } |
+ ClipItemKind::BoxShadow { .. } => {
+ true
+ }
+
+ ClipItemKind::Rectangle { mode: ClipMode::Clip, .. } => {
+ !instance.flags.contains(ClipNodeFlags::SAME_COORD_SYSTEM)
+ }
+ };
+
+ // Store this in the index buffer for this clip chain instance.
+ self.clip_node_instances.push(instance);
+ }
+ }
+ }
+ }
+
+ // Get the range identifying the clip nodes in the index buffer.
+ let clips_range = ClipNodeRange {
+ first: first_clip_node_index,
+ count: self.clip_node_instances.len() as u32 - first_clip_node_index,
+ };
+
+ // If this clip chain needs a mask, reduce the size of the mask allocation
+ // by any clips that were in the same space as the picture. This can result
+ // in much smaller clip mask allocations in some cases. Note that the ordering
+ // here is important - the reduction must occur *after* the clip item accept
+ // reject checks above, so that we don't eliminate masks accidentally (since
+ // we currently only support a local clip rect in the vertex shader).
+ if needs_mask {
+ pic_coverage_rect = pic_coverage_rect.intersection(&self.active_pic_coverage_rect)?;
+ }
+
+ // Return a valid clip chain instance
+ Some(ClipChainInstance {
+ clips_range,
+ has_non_local_clips,
+ local_clip_rect,
+ pic_coverage_rect,
+ pic_spatial_node_index: prim_to_pic_mapper.ref_spatial_node_index,
+ needs_mask,
+ })
+ }
+
+ pub fn begin_frame(&mut self, scratch: &mut ClipStoreScratchBuffer) {
+ mem::swap(&mut self.clip_node_instances, &mut scratch.clip_node_instances);
+ mem::swap(&mut self.mask_tiles, &mut scratch.mask_tiles);
+ self.clip_node_instances.clear();
+ self.mask_tiles.clear();
+ }
+
+ pub fn end_frame(&mut self, scratch: &mut ClipStoreScratchBuffer) {
+ mem::swap(&mut self.clip_node_instances, &mut scratch.clip_node_instances);
+ mem::swap(&mut self.mask_tiles, &mut scratch.mask_tiles);
+ }
+
+ pub fn visible_mask_tiles(&self, instance: &ClipNodeInstance) -> &[VisibleMaskImageTile] {
+ if let Some(range) = &instance.visible_tiles {
+ &self.mask_tiles[range.clone()]
+ } else {
+ &[]
+ }
+ }
+}
+
+pub struct ComplexTranslateIter<I> {
+ source: I,
+ offset: LayoutVector2D,
+}
+
+impl<I: Iterator<Item = ComplexClipRegion>> Iterator for ComplexTranslateIter<I> {
+ type Item = ComplexClipRegion;
+ fn next(&mut self) -> Option<Self::Item> {
+ self.source
+ .next()
+ .map(|mut complex| {
+ complex.rect = complex.rect.translate(self.offset);
+ complex
+ })
+ }
+}
+
+// The ClipItemKey is a hashable representation of the contents
+// of a clip item. It is used during interning to de-duplicate
+// clip nodes between frames and display lists. This allows quick
+// comparison of clip node equality by handle, and also allows
+// the uploaded GPU cache handle to be retained between display lists.
+// TODO(gw): Maybe we should consider constructing these directly
+// in the DL builder?
+#[derive(Copy, Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum ClipItemKeyKind {
+ Rectangle(RectangleKey, ClipMode),
+ RoundedRectangle(RectangleKey, BorderRadiusAu, ClipMode),
+ ImageMask(RectangleKey, ImageKey, Option<PolygonDataHandle>),
+ BoxShadow(PointKey, SizeKey, BorderRadiusAu, RectangleKey, Au, BoxShadowClipMode),
+}
+
+impl ClipItemKeyKind {
+ pub fn rectangle(rect: LayoutRect, mode: ClipMode) -> Self {
+ ClipItemKeyKind::Rectangle(rect.into(), mode)
+ }
+
+ pub fn rounded_rect(rect: LayoutRect, mut radii: BorderRadius, mode: ClipMode) -> Self {
+ if radii.is_zero() {
+ ClipItemKeyKind::rectangle(rect, mode)
+ } else {
+ ensure_no_corner_overlap(&mut radii, rect.size());
+ ClipItemKeyKind::RoundedRectangle(
+ rect.into(),
+ radii.into(),
+ mode,
+ )
+ }
+ }
+
+ pub fn image_mask(image_mask: &ImageMask, mask_rect: LayoutRect,
+ polygon_handle: Option<PolygonDataHandle>) -> Self {
+ ClipItemKeyKind::ImageMask(
+ mask_rect.into(),
+ image_mask.image,
+ polygon_handle,
+ )
+ }
+
+ pub fn box_shadow(
+ shadow_rect: LayoutRect,
+ shadow_radius: BorderRadius,
+ prim_shadow_rect: LayoutRect,
+ blur_radius: f32,
+ clip_mode: BoxShadowClipMode,
+ ) -> Self {
+ // Get the fractional offsets required to match the
+ // source rect with a minimal rect.
+ let fract_offset = LayoutPoint::new(
+ shadow_rect.min.x.fract().abs(),
+ shadow_rect.min.y.fract().abs(),
+ );
+
+ ClipItemKeyKind::BoxShadow(
+ fract_offset.into(),
+ shadow_rect.size().into(),
+ shadow_radius.into(),
+ prim_shadow_rect.into(),
+ Au::from_f32_px(blur_radius),
+ clip_mode,
+ )
+ }
+
+ pub fn node_kind(&self) -> ClipNodeKind {
+ match *self {
+ ClipItemKeyKind::Rectangle(_, ClipMode::Clip) => ClipNodeKind::Rectangle,
+
+ ClipItemKeyKind::Rectangle(_, ClipMode::ClipOut) |
+ ClipItemKeyKind::RoundedRectangle(..) |
+ ClipItemKeyKind::ImageMask(..) |
+ ClipItemKeyKind::BoxShadow(..) => ClipNodeKind::Complex,
+ }
+ }
+}
+
+#[derive(Debug, Copy, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ClipItemKey {
+ pub kind: ClipItemKeyKind,
+ pub spatial_node_index: SpatialNodeIndex,
+}
+
+/// The data available about an interned clip node during scene building
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ClipInternData {
+ pub key: ClipItemKey,
+}
+
+impl intern::InternDebug for ClipItemKey {}
+
+impl intern::Internable for ClipIntern {
+ type Key = ClipItemKey;
+ type StoreData = ClipNode;
+ type InternData = ClipInternData;
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_CLIPS;
+}
+
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum ClipItemKind {
+ Rectangle {
+ rect: LayoutRect,
+ mode: ClipMode,
+ },
+ RoundedRectangle {
+ rect: LayoutRect,
+ radius: BorderRadius,
+ mode: ClipMode,
+ },
+ Image {
+ image: ImageKey,
+ rect: LayoutRect,
+ polygon_handle: Option<PolygonDataHandle>,
+ },
+ BoxShadow {
+ source: BoxShadowClipSource,
+ },
+}
+
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ClipItem {
+ pub kind: ClipItemKind,
+ pub spatial_node_index: SpatialNodeIndex,
+}
+
+fn compute_box_shadow_parameters(
+ shadow_rect_fract_offset: LayoutPoint,
+ shadow_rect_size: LayoutSize,
+ mut shadow_radius: BorderRadius,
+ prim_shadow_rect: LayoutRect,
+ blur_radius: f32,
+ clip_mode: BoxShadowClipMode,
+) -> BoxShadowClipSource {
+ // Make sure corners don't overlap.
+ ensure_no_corner_overlap(&mut shadow_radius, shadow_rect_size);
+
+ let fract_size = LayoutSize::new(
+ shadow_rect_size.width.fract().abs(),
+ shadow_rect_size.height.fract().abs(),
+ );
+
+ // Create a minimal size primitive mask to blur. In this
+ // case, we ensure the size of each corner is the same,
+ // to simplify the shader logic that stretches the blurred
+ // result across the primitive.
+ let max_corner_width = shadow_radius.top_left.width
+ .max(shadow_radius.bottom_left.width)
+ .max(shadow_radius.top_right.width)
+ .max(shadow_radius.bottom_right.width);
+ let max_corner_height = shadow_radius.top_left.height
+ .max(shadow_radius.bottom_left.height)
+ .max(shadow_radius.top_right.height)
+ .max(shadow_radius.bottom_right.height);
+
+ // Get maximum distance that can be affected by given blur radius.
+ let blur_region = (BLUR_SAMPLE_SCALE * blur_radius).ceil();
+
+ // If the largest corner is smaller than the blur radius, we need to ensure
+ // that it's big enough that the corners don't affect the middle segments.
+ let used_corner_width = max_corner_width.max(blur_region);
+ let used_corner_height = max_corner_height.max(blur_region);
+
+ // Minimal nine-patch size, corner + internal + corner.
+ let min_shadow_rect_size = LayoutSize::new(
+ 2.0 * used_corner_width + blur_region,
+ 2.0 * used_corner_height + blur_region,
+ );
+
+ // The minimal rect to blur.
+ let mut minimal_shadow_rect = LayoutRect::from_origin_and_size(
+ LayoutPoint::new(
+ blur_region + shadow_rect_fract_offset.x,
+ blur_region + shadow_rect_fract_offset.y,
+ ),
+ LayoutSize::new(
+ min_shadow_rect_size.width + fract_size.width,
+ min_shadow_rect_size.height + fract_size.height,
+ ),
+ );
+
+ // If the width or height ends up being bigger than the original
+ // primitive shadow rect, just blur the entire rect along that
+ // axis and draw that as a simple blit. This is necessary for
+ // correctness, since the blur of one corner may affect the blur
+ // in another corner.
+ let mut stretch_mode_x = BoxShadowStretchMode::Stretch;
+ if shadow_rect_size.width < minimal_shadow_rect.width() {
+ minimal_shadow_rect.max.x = minimal_shadow_rect.min.x + shadow_rect_size.width;
+ stretch_mode_x = BoxShadowStretchMode::Simple;
+ }
+
+ let mut stretch_mode_y = BoxShadowStretchMode::Stretch;
+ if shadow_rect_size.height < minimal_shadow_rect.height() {
+ minimal_shadow_rect.max.y = minimal_shadow_rect.min.y + shadow_rect_size.height;
+ stretch_mode_y = BoxShadowStretchMode::Simple;
+ }
+
+ // Expand the shadow rect by enough room for the blur to take effect.
+ let shadow_rect_alloc_size = LayoutSize::new(
+ 2.0 * blur_region + minimal_shadow_rect.width().ceil(),
+ 2.0 * blur_region + minimal_shadow_rect.height().ceil(),
+ );
+
+ BoxShadowClipSource {
+ original_alloc_size: shadow_rect_alloc_size,
+ shadow_rect_alloc_size,
+ shadow_radius,
+ prim_shadow_rect,
+ blur_radius,
+ clip_mode,
+ stretch_mode_x,
+ stretch_mode_y,
+ render_task: None,
+ cache_key: None,
+ minimal_shadow_rect,
+ }
+}
+
+impl ClipItemKind {
+ pub fn new_box_shadow(
+ shadow_rect_fract_offset: LayoutPoint,
+ shadow_rect_size: LayoutSize,
+ mut shadow_radius: BorderRadius,
+ prim_shadow_rect: LayoutRect,
+ blur_radius: f32,
+ clip_mode: BoxShadowClipMode,
+ ) -> Self {
+ let mut source = compute_box_shadow_parameters(
+ shadow_rect_fract_offset,
+ shadow_rect_size,
+ shadow_radius,
+ prim_shadow_rect,
+ blur_radius,
+ clip_mode,
+ );
+
+ fn needed_downscaling(source: &BoxShadowClipSource) -> Option<f32> {
+ // This size is fairly arbitrary, but it's the same as the size that
+ // we use to avoid caching big blurred stacking contexts.
+ //
+ // If you change it, ensure that the reftests
+ // box-shadow-large-blur-radius-* still hit the downscaling path,
+ // and that they render correctly.
+ const MAX_SIZE: f32 = 2048.;
+
+ let max_dimension =
+ source.shadow_rect_alloc_size.width.max(source.shadow_rect_alloc_size.height);
+
+ if max_dimension > MAX_SIZE {
+ Some(MAX_SIZE / max_dimension)
+ } else {
+ None
+ }
+ }
+
+ if let Some(downscale) = needed_downscaling(&source) {
+ shadow_radius.bottom_left.height *= downscale;
+ shadow_radius.bottom_left.width *= downscale;
+ shadow_radius.bottom_right.height *= downscale;
+ shadow_radius.bottom_right.width *= downscale;
+ shadow_radius.top_left.height *= downscale;
+ shadow_radius.top_left.width *= downscale;
+ shadow_radius.top_right.height *= downscale;
+ shadow_radius.top_right.width *= downscale;
+
+ let original_alloc_size = source.shadow_rect_alloc_size;
+
+ source = compute_box_shadow_parameters(
+ shadow_rect_fract_offset * downscale,
+ shadow_rect_size * downscale,
+ shadow_radius,
+ prim_shadow_rect,
+ blur_radius * downscale,
+ clip_mode,
+ );
+ source.original_alloc_size = original_alloc_size;
+ }
+ ClipItemKind::BoxShadow { source }
+ }
+
+ /// Returns true if this clip mask can run through the fast path
+ /// for the given clip item type.
+ ///
+ /// Note: this logic has to match `ClipBatcher::add` behavior.
+ fn supports_fast_path_rendering(&self) -> bool {
+ match *self {
+ ClipItemKind::Rectangle { .. } |
+ ClipItemKind::Image { .. } |
+ ClipItemKind::BoxShadow { .. } => {
+ false
+ }
+ ClipItemKind::RoundedRectangle { ref radius, .. } => {
+ // The rounded clip rect fast path shader can only work
+ // if the radii are uniform.
+ radius.is_uniform().is_some()
+ }
+ }
+ }
+
+ // Get an optional clip rect that a clip source can provide to
+ // reduce the size of a primitive region. This is typically
+ // used to eliminate redundant clips, and reduce the size of
+ // any clip mask that eventually gets drawn.
+ pub fn get_local_clip_rect(&self) -> Option<LayoutRect> {
+ match *self {
+ ClipItemKind::Rectangle { rect, mode: ClipMode::Clip } => Some(rect),
+ ClipItemKind::Rectangle { mode: ClipMode::ClipOut, .. } => None,
+ ClipItemKind::RoundedRectangle { rect, mode: ClipMode::Clip, .. } => Some(rect),
+ ClipItemKind::RoundedRectangle { mode: ClipMode::ClipOut, .. } => None,
+ ClipItemKind::Image { rect, .. } => {
+ Some(rect)
+ }
+ ClipItemKind::BoxShadow { .. } => None,
+ }
+ }
+
+ fn get_clip_result_complex(
+ &self,
+ transform: &LayoutToWorldTransform,
+ prim_world_rect: &WorldRect,
+ world_rect: &WorldRect,
+ ) -> ClipResult {
+ let visible_rect = match prim_world_rect.intersection(world_rect) {
+ Some(rect) => rect,
+ None => return ClipResult::Reject,
+ };
+
+ let (clip_rect, inner_rect, mode) = match *self {
+ ClipItemKind::Rectangle { rect, mode } => {
+ (rect, Some(rect), mode)
+ }
+ ClipItemKind::RoundedRectangle { rect, ref radius, mode } => {
+ let inner_clip_rect = extract_inner_rect_safe(&rect, radius);
+ (rect, inner_clip_rect, mode)
+ }
+ ClipItemKind::Image { rect, .. } => {
+ (rect, None, ClipMode::Clip)
+ }
+ ClipItemKind::BoxShadow { .. } => {
+ return ClipResult::Partial;
+ }
+ };
+
+ if let Some(ref inner_clip_rect) = inner_rect {
+ if let Some(()) = projected_rect_contains(inner_clip_rect, transform, &visible_rect) {
+ return match mode {
+ ClipMode::Clip => ClipResult::Accept,
+ ClipMode::ClipOut => ClipResult::Reject,
+ };
+ }
+ }
+
+ match mode {
+ ClipMode::Clip => {
+ let outer_clip_rect = match project_rect(
+ transform,
+ &clip_rect,
+ &world_rect,
+ ) {
+ Some(outer_clip_rect) => outer_clip_rect,
+ None => return ClipResult::Partial,
+ };
+
+ match outer_clip_rect.intersection(prim_world_rect) {
+ Some(..) => {
+ ClipResult::Partial
+ }
+ None => {
+ ClipResult::Reject
+ }
+ }
+ }
+ ClipMode::ClipOut => ClipResult::Partial,
+ }
+ }
+
+ // Check how a given clip source affects a local primitive region.
+ fn get_clip_result(
+ &self,
+ prim_rect: &LayoutRect,
+ ) -> ClipResult {
+ match *self {
+ ClipItemKind::Rectangle { rect, mode: ClipMode::Clip } => {
+ if rect.contains_box(prim_rect) {
+ return ClipResult::Accept;
+ }
+
+ match rect.intersection(prim_rect) {
+ Some(..) => {
+ ClipResult::Partial
+ }
+ None => {
+ ClipResult::Reject
+ }
+ }
+ }
+ ClipItemKind::Rectangle { rect, mode: ClipMode::ClipOut } => {
+ if rect.contains_box(prim_rect) {
+ return ClipResult::Reject;
+ }
+
+ match rect.intersection(prim_rect) {
+ Some(_) => {
+ ClipResult::Partial
+ }
+ None => {
+ ClipResult::Accept
+ }
+ }
+ }
+ ClipItemKind::RoundedRectangle { rect, ref radius, mode: ClipMode::Clip } => {
+ // TODO(gw): Consider caching this in the ClipNode
+ // if it ever shows in profiles.
+ if rounded_rectangle_contains_box_quick(&rect, radius, &prim_rect) {
+ return ClipResult::Accept;
+ }
+
+ match rect.intersection(prim_rect) {
+ Some(..) => {
+ ClipResult::Partial
+ }
+ None => {
+ ClipResult::Reject
+ }
+ }
+ }
+ ClipItemKind::RoundedRectangle { rect, ref radius, mode: ClipMode::ClipOut } => {
+ // TODO(gw): Consider caching this in the ClipNode
+ // if it ever shows in profiles.
+ if rounded_rectangle_contains_box_quick(&rect, radius, &prim_rect) {
+ return ClipResult::Reject;
+ }
+
+ match rect.intersection(prim_rect) {
+ Some(_) => {
+ ClipResult::Partial
+ }
+ None => {
+ ClipResult::Accept
+ }
+ }
+ }
+ ClipItemKind::Image { rect, .. } => {
+ match rect.intersection(prim_rect) {
+ Some(..) => {
+ ClipResult::Partial
+ }
+ None => {
+ ClipResult::Reject
+ }
+ }
+ }
+ ClipItemKind::BoxShadow { .. } => {
+ ClipResult::Partial
+ }
+ }
+ }
+}
+
+/// Represents a local rect and a device space
+/// rectangles that are either outside or inside bounds.
+#[derive(Clone, Debug, PartialEq)]
+pub struct Geometry {
+ pub local_rect: LayoutRect,
+ pub device_rect: DeviceIntRect,
+}
+
+impl From<LayoutRect> for Geometry {
+ fn from(local_rect: LayoutRect) -> Self {
+ Geometry {
+ local_rect,
+ device_rect: DeviceIntRect::zero(),
+ }
+ }
+}
+
+pub fn rounded_rectangle_contains_point(
+ point: &LayoutPoint,
+ rect: &LayoutRect,
+ radii: &BorderRadius
+) -> bool {
+ if !rect.contains(*point) {
+ return false;
+ }
+
+ let top_left_center = rect.min + radii.top_left.to_vector();
+ if top_left_center.x > point.x && top_left_center.y > point.y &&
+ !Ellipse::new(radii.top_left).contains(*point - top_left_center.to_vector()) {
+ return false;
+ }
+
+ let bottom_right_center = rect.bottom_right() - radii.bottom_right.to_vector();
+ if bottom_right_center.x < point.x && bottom_right_center.y < point.y &&
+ !Ellipse::new(radii.bottom_right).contains(*point - bottom_right_center.to_vector()) {
+ return false;
+ }
+
+ let top_right_center = rect.top_right() +
+ LayoutVector2D::new(-radii.top_right.width, radii.top_right.height);
+ if top_right_center.x < point.x && top_right_center.y > point.y &&
+ !Ellipse::new(radii.top_right).contains(*point - top_right_center.to_vector()) {
+ return false;
+ }
+
+ let bottom_left_center = rect.bottom_left() +
+ LayoutVector2D::new(radii.bottom_left.width, -radii.bottom_left.height);
+ if bottom_left_center.x > point.x && bottom_left_center.y < point.y &&
+ !Ellipse::new(radii.bottom_left).contains(*point - bottom_left_center.to_vector()) {
+ return false;
+ }
+
+ true
+}
+
+/// Return true if the rounded rectangle described by `container` and `radii`
+/// definitely contains `containee`. May return false negatives, but never false
+/// positives.
+fn rounded_rectangle_contains_box_quick(
+ container: &LayoutRect,
+ radii: &BorderRadius,
+ containee: &LayoutRect,
+) -> bool {
+ if !container.contains_box(containee) {
+ return false;
+ }
+
+ /// Return true if `point` falls within `corner`. This only covers the
+ /// upper-left case; we transform the other corners into that form.
+ fn foul(point: LayoutPoint, corner: LayoutPoint) -> bool {
+ point.x < corner.x && point.y < corner.y
+ }
+
+ /// Flip `pt` about the y axis (i.e. negate `x`).
+ fn flip_x(pt: LayoutPoint) -> LayoutPoint {
+ LayoutPoint { x: -pt.x, .. pt }
+ }
+
+ /// Flip `pt` about the x axis (i.e. negate `y`).
+ fn flip_y(pt: LayoutPoint) -> LayoutPoint {
+ LayoutPoint { y: -pt.y, .. pt }
+ }
+
+ if foul(containee.top_left(), container.top_left() + radii.top_left) ||
+ foul(flip_x(containee.top_right()), flip_x(container.top_right()) + radii.top_right) ||
+ foul(flip_y(containee.bottom_left()), flip_y(container.bottom_left()) + radii.bottom_left) ||
+ foul(-containee.bottom_right(), -container.bottom_right() + radii.bottom_right)
+ {
+ return false;
+ }
+
+ true
+}
+
+/// Test where point p is relative to the infinite line that passes through the segment
+/// defined by p0 and p1. Point p is on the "left" of the line if the triangle (p0, p1, p)
+/// forms a counter-clockwise triangle.
+/// > 0 is left of the line
+/// < 0 is right of the line
+/// == 0 is on the line
+pub fn is_left_of_line(
+ p_x: f32,
+ p_y: f32,
+ p0_x: f32,
+ p0_y: f32,
+ p1_x: f32,
+ p1_y: f32,
+) -> f32 {
+ (p1_x - p0_x) * (p_y - p0_y) - (p_x - p0_x) * (p1_y - p0_y)
+}
+
+pub fn polygon_contains_point(
+ point: &LayoutPoint,
+ rect: &LayoutRect,
+ polygon: &PolygonKey,
+) -> bool {
+ if !rect.contains(*point) {
+ return false;
+ }
+
+ // p is a LayoutPoint that we'll be comparing to dimensionless PointKeys,
+ // which were created from LayoutPoints, so it all works out.
+ let p = LayoutPoint::new(point.x - rect.min.x, point.y - rect.min.y);
+
+ // Calculate a winding number for this point.
+ let mut winding_number: i32 = 0;
+
+ let count = polygon.point_count as usize;
+
+ for i in 0..count {
+ let p0 = polygon.points[i];
+ let p1 = polygon.points[(i + 1) % count];
+
+ if p0.y <= p.y {
+ if p1.y > p.y {
+ if is_left_of_line(p.x, p.y, p0.x, p0.y, p1.x, p1.y) > 0.0 {
+ winding_number = winding_number + 1;
+ }
+ }
+ } else if p1.y <= p.y {
+ if is_left_of_line(p.x, p.y, p0.x, p0.y, p1.x, p1.y) < 0.0 {
+ winding_number = winding_number - 1;
+ }
+ }
+ }
+
+ match polygon.fill_rule {
+ FillRule::Nonzero => winding_number != 0,
+ FillRule::Evenodd => winding_number.abs() % 2 == 1,
+ }
+}
+
+pub fn projected_rect_contains(
+ source_rect: &LayoutRect,
+ transform: &LayoutToWorldTransform,
+ target_rect: &WorldRect,
+) -> Option<()> {
+ let points = [
+ transform.transform_point2d(source_rect.top_left())?,
+ transform.transform_point2d(source_rect.top_right())?,
+ transform.transform_point2d(source_rect.bottom_right())?,
+ transform.transform_point2d(source_rect.bottom_left())?,
+ ];
+ let target_points = [
+ target_rect.top_left(),
+ target_rect.top_right(),
+ target_rect.bottom_right(),
+ target_rect.bottom_left(),
+ ];
+ // iterate the edges of the transformed polygon
+ for (a, b) in points
+ .iter()
+ .cloned()
+ .zip(points[1..].iter().cloned().chain(iter::once(points[0])))
+ {
+ // If this edge is redundant, it's a weird, case, and we shouldn't go
+ // length in trying to take the fast path (e.g. when the whole rectangle is a point).
+ // If any of edges of the target rectangle crosses the edge, it's not completely
+ // inside our transformed polygon either.
+ if a.approx_eq(&b) || target_points.iter().any(|&c| (b - a).cross(c - a) < 0.0) {
+ return None
+ }
+ }
+
+ Some(())
+}
+
+
+// Add a clip node into the list of clips to be processed
+// for the current clip chain. Returns false if the clip
+// results in the entire primitive being culled out.
+fn add_clip_node_to_current_chain(
+ handle: ClipDataHandle,
+ prim_spatial_node_index: SpatialNodeIndex,
+ pic_spatial_node_index: SpatialNodeIndex,
+ local_clip_rect: &mut LayoutRect,
+ clip_node_info: &mut Vec<ClipNodeInfo>,
+ pic_coverage_rect: &mut PictureRect,
+ clip_data_store: &ClipDataStore,
+ spatial_tree: &SpatialTree,
+) -> bool {
+ let clip_node = &clip_data_store[handle];
+
+ // Determine the most efficient way to convert between coordinate
+ // systems of the primitive and clip node.
+ let conversion = ClipSpaceConversion::new(
+ prim_spatial_node_index,
+ clip_node.item.spatial_node_index,
+ spatial_tree,
+ );
+
+ // If we can convert spaces, try to reduce the size of the region
+ // requested, and cache the conversion information for the next step.
+ if let Some(clip_rect) = clip_node.item.kind.get_local_clip_rect() {
+ match conversion {
+ ClipSpaceConversion::Local => {
+ *local_clip_rect = match local_clip_rect.intersection(&clip_rect) {
+ Some(rect) => rect,
+ None => return false,
+ };
+ }
+ ClipSpaceConversion::ScaleOffset(ref scale_offset) => {
+ let clip_rect = scale_offset.map_rect(&clip_rect);
+ *local_clip_rect = match local_clip_rect.intersection(&clip_rect) {
+ Some(rect) => rect,
+ None => return false,
+ };
+ }
+ ClipSpaceConversion::Transform(..) => {
+ // Map the local clip rect directly into the same space as the picture
+ // surface. This will often be the same space as the clip itself, which
+ // results in a reduction in allocated clip mask size.
+
+ // For simplicity, only apply this optimization if the clip is in the
+ // same coord system as the picture. There are some 'advanced' perspective
+ // clip tests in wrench that break without this check. Those cases are
+ // never used in Gecko, and we aim to remove support in WR for that
+ // in future to simplify the clipping pipeline.
+ let pic_coord_system = spatial_tree
+ .get_spatial_node(pic_spatial_node_index)
+ .coordinate_system_id;
+
+ let clip_coord_system = spatial_tree
+ .get_spatial_node(clip_node.item.spatial_node_index)
+ .coordinate_system_id;
+
+ if pic_coord_system == clip_coord_system {
+ let mapper = SpaceMapper::new_with_target(
+ pic_spatial_node_index,
+ clip_node.item.spatial_node_index,
+ PictureRect::max_rect(),
+ spatial_tree,
+ );
+
+ if let Some(pic_clip_rect) = mapper.map(&clip_rect) {
+ *pic_coverage_rect = pic_clip_rect
+ .intersection(pic_coverage_rect)
+ .unwrap_or(PictureRect::zero());
+ }
+ }
+ }
+ }
+ }
+
+ clip_node_info.push(ClipNodeInfo {
+ conversion,
+ handle,
+ });
+
+ true
+}
+
+#[cfg(test)]
+mod tests {
+ use super::projected_rect_contains;
+ use euclid::{Transform3D, rect};
+
+ #[test]
+ fn test_empty_projected_rect() {
+ assert_eq!(
+ None,
+ projected_rect_contains(
+ &rect(10.0, 10.0, 0.0, 0.0).to_box2d(),
+ &Transform3D::identity(),
+ &rect(20.0, 20.0, 10.0, 10.0).to_box2d(),
+ ),
+ "Empty rectangle is considered to include a non-empty!"
+ );
+ }
+}
+
+/// PolygonKeys get interned, because it's a convenient way to move the data
+/// for the polygons out of the ClipItemKind and ClipItemKeyKind enums. The
+/// polygon data is both interned and retrieved by the scene builder, and not
+/// accessed at all by the frame builder. Another oddity is that the
+/// PolygonKey contains the totality of the information about the polygon, so
+/// the InternData and StoreData types are both PolygonKey.
+#[derive(Copy, Clone, Debug, Hash, MallocSizeOf, PartialEq, Eq)]
+#[cfg_attr(any(feature = "serde"), derive(Deserialize, Serialize))]
+pub enum PolygonIntern {}
+
+pub type PolygonDataHandle = intern::Handle<PolygonIntern>;
+
+impl intern::InternDebug for PolygonKey {}
+
+impl intern::Internable for PolygonIntern {
+ type Key = PolygonKey;
+ type StoreData = PolygonKey;
+ type InternData = PolygonKey;
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_POLYGONS;
+}
diff --git a/gfx/wr/webrender/src/command_buffer.rs b/gfx/wr/webrender/src/command_buffer.rs
new file mode 100644
index 0000000000..d9fd945831
--- /dev/null
+++ b/gfx/wr/webrender/src/command_buffer.rs
@@ -0,0 +1,436 @@
+/* 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 api::units::PictureRect;
+use crate::{spatial_tree::SpatialNodeIndex, render_task_graph::RenderTaskId, surface::SurfaceTileDescriptor, picture::TileKey, renderer::GpuBufferAddress, FastHashMap, prim_store::PrimitiveInstanceIndex, gpu_cache::GpuCacheAddress};
+use crate::gpu_types::{QuadSegment, TransformPaletteId};
+use crate::segment::EdgeAaSegmentMask;
+
+/// A tightly packed command stored in a command buffer
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone)]
+pub struct Command(u32);
+
+impl Command {
+ /// Draw a simple primitive that needs prim instance index only.
+ const CMD_DRAW_SIMPLE_PRIM: u32 = 0x00000000;
+ /// Change the current spatial node.
+ const CMD_SET_SPATIAL_NODE: u32 = 0x10000000;
+ /// Draw a complex (3d-split) primitive, that has multiple GPU cache addresses.
+ const CMD_DRAW_COMPLEX_PRIM: u32 = 0x20000000;
+ /// Draw a primitive, that has a single GPU buffer addresses.
+ const CMD_DRAW_INSTANCE: u32 = 0x30000000;
+ /// Draw a generic quad primitive
+ const CMD_DRAW_QUAD: u32 = 0x40000000;
+ /// Set a list of variable-length segments
+ const CMD_SET_SEGMENTS: u32 = 0x50000000;
+
+ /// Bitmask for command bits of the command.
+ const CMD_MASK: u32 = 0xf0000000;
+ /// Bitmask for param bits of the command.
+ const PARAM_MASK: u32 = 0x0fffffff;
+
+ /// Encode drawing a simple primitive.
+ fn draw_simple_prim(prim_instance_index: PrimitiveInstanceIndex) -> Self {
+ Command(Command::CMD_DRAW_SIMPLE_PRIM | prim_instance_index.0)
+ }
+
+ /// Encode changing spatial node.
+ fn set_spatial_node(spatial_node_index: SpatialNodeIndex) -> Self {
+ Command(Command::CMD_SET_SPATIAL_NODE | spatial_node_index.0)
+ }
+
+ /// Encode a list of segments that follow
+ fn set_segments(count: usize) -> Self {
+ Command(Command::CMD_SET_SEGMENTS | count as u32)
+ }
+
+ /// Encode drawing a complex prim.
+ fn draw_complex_prim(prim_instance_index: PrimitiveInstanceIndex) -> Self {
+ Command(Command::CMD_DRAW_COMPLEX_PRIM | prim_instance_index.0)
+ }
+
+ fn draw_instance(prim_instance_index: PrimitiveInstanceIndex) -> Self {
+ Command(Command::CMD_DRAW_INSTANCE | prim_instance_index.0)
+ }
+
+ /// Encode arbitrary data word.
+ fn data(data: u32) -> Self {
+ Command(data)
+ }
+
+ fn draw_quad(prim_instance_index: PrimitiveInstanceIndex) -> Self {
+ Command(Command::CMD_DRAW_QUAD | prim_instance_index.0)
+ }
+}
+
+bitflags! {
+ /// Flags related to quad primitives
+ #[repr(transparent)]
+ #[cfg_attr(feature = "capture", derive(Serialize))]
+ #[cfg_attr(feature = "replay", derive(Deserialize))]
+ pub struct QuadFlags : u8 {
+ const IS_OPAQUE = 1 << 0;
+
+ /// If true, the prim is 2d and we can apply a clip to the task rect in vertex shader
+ const APPLY_DEVICE_CLIP = 1 << 1;
+
+ /// If true, the device-pixel scale is already applied, so ignore in vertex shaders
+ const IGNORE_DEVICE_PIXEL_SCALE = 1 << 2;
+ }
+}
+
+bitflags! {
+ /// Defines the space that a quad primitive is drawn in
+ #[repr(transparent)]
+ #[cfg_attr(feature = "capture", derive(Serialize))]
+ #[cfg_attr(feature = "replay", derive(Deserialize))]
+ pub struct MaskFlags : i32 {
+ const PRIM_SPACE = 1 << 0;
+ }
+}
+
+/// The unpacked equivalent to a `Command`.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub enum PrimitiveCommand {
+ Simple {
+ prim_instance_index: PrimitiveInstanceIndex,
+ },
+ Complex {
+ prim_instance_index: PrimitiveInstanceIndex,
+ gpu_address: GpuCacheAddress,
+ },
+ Instance {
+ prim_instance_index: PrimitiveInstanceIndex,
+ gpu_buffer_address: GpuBufferAddress,
+ },
+ Quad {
+ // TODO(gw): Used for bounding rect only, could possibly remove
+ prim_instance_index: PrimitiveInstanceIndex,
+ gpu_buffer_address: GpuBufferAddress,
+ transform_id: TransformPaletteId,
+ quad_flags: QuadFlags,
+ edge_flags: EdgeAaSegmentMask,
+ },
+}
+
+impl PrimitiveCommand {
+ pub fn simple(
+ prim_instance_index: PrimitiveInstanceIndex,
+ ) -> Self {
+ PrimitiveCommand::Simple {
+ prim_instance_index,
+ }
+ }
+
+ pub fn complex(
+ prim_instance_index: PrimitiveInstanceIndex,
+ gpu_address: GpuCacheAddress,
+ ) -> Self {
+ PrimitiveCommand::Complex {
+ prim_instance_index,
+ gpu_address,
+ }
+ }
+
+ pub fn quad(
+ prim_instance_index: PrimitiveInstanceIndex,
+ gpu_buffer_address: GpuBufferAddress,
+ transform_id: TransformPaletteId,
+ quad_flags: QuadFlags,
+ edge_flags: EdgeAaSegmentMask,
+ ) -> Self {
+ PrimitiveCommand::Quad {
+ prim_instance_index,
+ gpu_buffer_address,
+ transform_id,
+ quad_flags,
+ edge_flags,
+ }
+ }
+
+ pub fn instance(
+ prim_instance_index: PrimitiveInstanceIndex,
+ gpu_buffer_address: GpuBufferAddress,
+ ) -> Self {
+ PrimitiveCommand::Instance {
+ prim_instance_index,
+ gpu_buffer_address,
+ }
+ }
+}
+
+
+/// A list of commands describing how to draw a primitive list.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct CommandBuffer {
+ /// The encoded drawing commands.
+ commands: Vec<Command>,
+ /// Cached current spatial node.
+ current_spatial_node_index: SpatialNodeIndex,
+}
+
+impl CommandBuffer {
+ /// Construct a new cmd buffer.
+ pub fn new() -> Self {
+ CommandBuffer {
+ commands: Vec::new(),
+ current_spatial_node_index: SpatialNodeIndex::INVALID,
+ }
+ }
+
+ /// Push a list of segments in to the cmd buffer
+ pub fn set_segments(
+ &mut self,
+ segments: &[QuadSegment],
+ ) {
+ self.commands.push(Command::set_segments(segments.len()));
+ for segment in segments {
+ self.commands.push(Command::data(segment.task_id.index));
+ }
+ }
+
+ /// Add a primitive to the command buffer.
+ pub fn add_prim(
+ &mut self,
+ prim_cmd: &PrimitiveCommand,
+ spatial_node_index: SpatialNodeIndex,
+ ) {
+ if self.current_spatial_node_index != spatial_node_index {
+ self.commands.push(Command::set_spatial_node(spatial_node_index));
+ self.current_spatial_node_index = spatial_node_index;
+ }
+
+ self.add_cmd(prim_cmd);
+ }
+
+ /// Add a cmd to the command buffer.
+ pub fn add_cmd(
+ &mut self,
+ prim_cmd: &PrimitiveCommand,
+ ) {
+ match *prim_cmd {
+ PrimitiveCommand::Simple { prim_instance_index } => {
+ self.commands.push(Command::draw_simple_prim(prim_instance_index));
+ }
+ PrimitiveCommand::Complex { prim_instance_index, gpu_address } => {
+ self.commands.push(Command::draw_complex_prim(prim_instance_index));
+ self.commands.push(Command::data((gpu_address.u as u32) << 16 | gpu_address.v as u32));
+ }
+ PrimitiveCommand::Instance { prim_instance_index, gpu_buffer_address } => {
+ self.commands.push(Command::draw_instance(prim_instance_index));
+ self.commands.push(Command::data((gpu_buffer_address.u as u32) << 16 | gpu_buffer_address.v as u32));
+ }
+ PrimitiveCommand::Quad { prim_instance_index, gpu_buffer_address, transform_id, quad_flags, edge_flags } => {
+ self.commands.push(Command::draw_quad(prim_instance_index));
+ self.commands.push(Command::data((gpu_buffer_address.u as u32) << 16 | gpu_buffer_address.v as u32));
+ self.commands.push(Command::data(transform_id.0));
+ self.commands.push(Command::data((quad_flags.bits as u32) << 16 | edge_flags.bits() as u32));
+ }
+ }
+ }
+
+ /// Iterate the command list, calling a provided closure for each primitive draw command.
+ pub fn iter_prims<F>(
+ &self,
+ f: &mut F,
+ ) where F: FnMut(&PrimitiveCommand, SpatialNodeIndex, &[RenderTaskId]) {
+ let mut current_spatial_node_index = SpatialNodeIndex::INVALID;
+ let mut cmd_iter = self.commands.iter();
+ // TODO(gw): Consider pre-allocating this / Smallvec if it shows up in profiles.
+ let mut segments = Vec::new();
+
+ while let Some(cmd) = cmd_iter.next() {
+ let command = cmd.0 & Command::CMD_MASK;
+ let param = cmd.0 & Command::PARAM_MASK;
+
+ match command {
+ Command::CMD_DRAW_SIMPLE_PRIM => {
+ let prim_instance_index = PrimitiveInstanceIndex(param);
+ let cmd = PrimitiveCommand::simple(prim_instance_index);
+ f(&cmd, current_spatial_node_index, &[]);
+ }
+ Command::CMD_SET_SPATIAL_NODE => {
+ current_spatial_node_index = SpatialNodeIndex(param);
+ }
+ Command::CMD_DRAW_COMPLEX_PRIM => {
+ let prim_instance_index = PrimitiveInstanceIndex(param);
+ let data = cmd_iter.next().unwrap();
+ let gpu_address = GpuCacheAddress {
+ u: (data.0 >> 16) as u16,
+ v: (data.0 & 0xffff) as u16,
+ };
+ let cmd = PrimitiveCommand::complex(
+ prim_instance_index,
+ gpu_address,
+ );
+ f(&cmd, current_spatial_node_index, &[]);
+ }
+ Command::CMD_DRAW_QUAD => {
+ let prim_instance_index = PrimitiveInstanceIndex(param);
+ let data = cmd_iter.next().unwrap();
+ let transform_id = TransformPaletteId(cmd_iter.next().unwrap().0);
+ let bits = cmd_iter.next().unwrap().0;
+ let quad_flags = QuadFlags::from_bits((bits >> 16) as u8).unwrap();
+ let edge_flags = EdgeAaSegmentMask::from_bits((bits & 0xff) as u8).unwrap();
+ let gpu_buffer_address = GpuBufferAddress {
+ u: (data.0 >> 16) as u16,
+ v: (data.0 & 0xffff) as u16,
+ };
+ let cmd = PrimitiveCommand::quad(
+ prim_instance_index,
+ gpu_buffer_address,
+ transform_id,
+ quad_flags,
+ edge_flags,
+ );
+ f(&cmd, current_spatial_node_index, &segments);
+ segments.clear()
+ }
+ Command::CMD_DRAW_INSTANCE => {
+ let prim_instance_index = PrimitiveInstanceIndex(param);
+ let data = cmd_iter.next().unwrap();
+ let gpu_buffer_address = GpuBufferAddress {
+ u: (data.0 >> 16) as u16,
+ v: (data.0 & 0xffff) as u16,
+ };
+ let cmd = PrimitiveCommand::instance(
+ prim_instance_index,
+ gpu_buffer_address,
+ );
+ f(&cmd, current_spatial_node_index, &[]);
+ }
+ Command::CMD_SET_SEGMENTS => {
+ let count = param;
+ for _ in 0 .. count {
+ segments.push(RenderTaskId { index: cmd_iter.next().unwrap().0 });
+ }
+ }
+ _ => {
+ unreachable!();
+ }
+ }
+ }
+ }
+}
+
+/// Abstracts whether a command buffer is being built for a tiled (picture cache)
+/// or simple (child surface).
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum CommandBufferBuilderKind {
+ Tiled {
+ // TODO(gw): It might be worth storing this as a 2d-array instead
+ // of a hash map if it ever shows up in profiles. This is
+ // slightly complicated by the sub_slice_index in the
+ // TileKey structure - could have a 2 level array?
+ tiles: FastHashMap<TileKey, SurfaceTileDescriptor>,
+ },
+ Simple {
+ render_task_id: RenderTaskId,
+ root_task_id: Option<RenderTaskId>,
+ dirty_rect: PictureRect,
+ },
+ Invalid,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct CommandBufferBuilder {
+ pub kind: CommandBufferBuilderKind,
+
+ /// If a command buffer establishes a sub-graph, then at the end of constructing
+ /// the surface, the parent surface is supplied as an input dependency, and the
+ /// parent surface gets a duplicated (existing) task with the same location, and
+ /// with the sub-graph output as an input dependency.
+ pub establishes_sub_graph: bool,
+
+ /// If this surface builds a sub-graph, it will mark a task in the filter sub-graph
+ /// as a resolve source for the input from the parent surface.
+ pub resolve_source: Option<RenderTaskId>,
+
+ /// List of render tasks that depend on the task that will be created for this builder.
+ pub extra_dependencies: Vec<RenderTaskId>,
+}
+
+impl CommandBufferBuilder {
+ pub fn empty() -> Self {
+ CommandBufferBuilder {
+ kind: CommandBufferBuilderKind::Invalid,
+ establishes_sub_graph: false,
+ resolve_source: None,
+ extra_dependencies: Vec::new(),
+ }
+ }
+
+ /// Construct a tiled command buffer builder.
+ pub fn new_tiled(
+ tiles: FastHashMap<TileKey, SurfaceTileDescriptor>,
+ ) -> Self {
+ CommandBufferBuilder {
+ kind: CommandBufferBuilderKind::Tiled {
+ tiles,
+ },
+ establishes_sub_graph: false,
+ resolve_source: None,
+ extra_dependencies: Vec::new(),
+ }
+ }
+
+ /// Construct a simple command buffer builder.
+ pub fn new_simple(
+ render_task_id: RenderTaskId,
+ establishes_sub_graph: bool,
+ root_task_id: Option<RenderTaskId>,
+ dirty_rect: PictureRect,
+ ) -> Self {
+ CommandBufferBuilder {
+ kind: CommandBufferBuilderKind::Simple {
+ render_task_id,
+ root_task_id,
+ dirty_rect,
+ },
+ establishes_sub_graph,
+ resolve_source: None,
+ extra_dependencies: Vec::new(),
+ }
+ }
+}
+
+// Index into a command buffer stored in a `CommandBufferList`.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone)]
+pub struct CommandBufferIndex(pub u32);
+
+// Container for a list of command buffers that are built for a frame.
+pub struct CommandBufferList {
+ cmd_buffers: Vec<CommandBuffer>,
+}
+
+impl CommandBufferList {
+ pub fn new() -> Self {
+ CommandBufferList {
+ cmd_buffers: Vec::new(),
+ }
+ }
+
+ pub fn create_cmd_buffer(
+ &mut self,
+ ) -> CommandBufferIndex {
+ let index = CommandBufferIndex(self.cmd_buffers.len() as u32);
+ self.cmd_buffers.push(CommandBuffer::new());
+ index
+ }
+
+ pub fn get(&self, index: CommandBufferIndex) -> &CommandBuffer {
+ &self.cmd_buffers[index.0 as usize]
+ }
+
+ pub fn get_mut(&mut self, index: CommandBufferIndex) -> &mut CommandBuffer {
+ &mut self.cmd_buffers[index.0 as usize]
+ }
+}
diff --git a/gfx/wr/webrender/src/composite.rs b/gfx/wr/webrender/src/composite.rs
new file mode 100644
index 0000000000..508b5b7439
--- /dev/null
+++ b/gfx/wr/webrender/src/composite.rs
@@ -0,0 +1,1484 @@
+/* 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 api::{ColorF, YuvRangedColorSpace, YuvFormat, ImageRendering, ExternalImageId, ImageBufferKind};
+use api::units::*;
+use api::ColorDepth;
+use crate::image_source::resolve_image;
+use euclid::Box2D;
+use crate::gpu_cache::GpuCache;
+use crate::gpu_types::{ZBufferId, ZBufferIdGenerator};
+use crate::internal_types::TextureSource;
+use crate::picture::{ImageDependency, ResolvedSurfaceTexture, TileCacheInstance, TileId, TileSurface};
+use crate::prim_store::DeferredResolve;
+use crate::resource_cache::{ImageRequest, ResourceCache};
+use crate::util::{Preallocator, ScaleOffset};
+use crate::tile_cache::PictureCacheDebugInfo;
+use crate::device::Device;
+use std::{ops, u64, os::raw::c_void};
+
+/*
+ Types and definitions related to compositing picture cache tiles
+ and/or OS compositor integration.
+ */
+
+/// Describes details of an operation to apply to a native surface
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum NativeSurfaceOperationDetails {
+ CreateSurface {
+ id: NativeSurfaceId,
+ virtual_offset: DeviceIntPoint,
+ tile_size: DeviceIntSize,
+ is_opaque: bool,
+ },
+ CreateExternalSurface {
+ id: NativeSurfaceId,
+ is_opaque: bool,
+ },
+ CreateBackdropSurface {
+ id: NativeSurfaceId,
+ color: ColorF,
+ },
+ DestroySurface {
+ id: NativeSurfaceId,
+ },
+ CreateTile {
+ id: NativeTileId,
+ },
+ DestroyTile {
+ id: NativeTileId,
+ },
+ AttachExternalImage {
+ id: NativeSurfaceId,
+ external_image: ExternalImageId,
+ }
+}
+
+/// Describes an operation to apply to a native surface
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct NativeSurfaceOperation {
+ pub details: NativeSurfaceOperationDetails,
+}
+
+/// Describes the source surface information for a tile to be composited. This
+/// is the analog of the TileSurface type, with target surface information
+/// resolved such that it can be used by the renderer.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Clone)]
+pub enum CompositeTileSurface {
+ Texture {
+ surface: ResolvedSurfaceTexture,
+ },
+ Color {
+ color: ColorF,
+ },
+ Clear,
+ ExternalSurface {
+ external_surface_index: ResolvedExternalSurfaceIndex,
+ },
+}
+
+/// The surface format for a tile being composited.
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub enum CompositeSurfaceFormat {
+ Rgba,
+ Yuv,
+}
+
+bitflags! {
+ /// Optional features that can be opted-out of when compositing,
+ /// possibly allowing a fast path to be selected.
+ pub struct CompositeFeatures: u8 {
+ // UV coordinates do not require clamping, for example because the
+ // entire texture is being composited.
+ const NO_UV_CLAMP = 1 << 0;
+ // The texture sample should not be modulated by a specified color.
+ const NO_COLOR_MODULATION = 1 << 1;
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum TileKind {
+ Opaque,
+ Alpha,
+ Clear,
+}
+
+// Index in to the compositor transforms stored in `CompositeState`
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone)]
+pub struct CompositorTransformIndex(usize);
+
+impl CompositorTransformIndex {
+ pub const INVALID: CompositorTransformIndex = CompositorTransformIndex(!0);
+}
+
+/// Describes the geometry and surface of a tile to be composited
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Clone)]
+pub struct CompositeTile {
+ pub surface: CompositeTileSurface,
+ pub local_rect: PictureRect,
+ pub local_valid_rect: PictureRect,
+ pub local_dirty_rect: PictureRect,
+ pub device_clip_rect: DeviceRect,
+ pub z_id: ZBufferId,
+ pub kind: TileKind,
+ pub transform_index: CompositorTransformIndex,
+}
+
+pub fn tile_kind(surface: &CompositeTileSurface, is_opaque: bool) -> TileKind {
+ match surface {
+ // Color tiles are, by definition, opaque. We might support non-opaque color
+ // tiles if we ever find pages that have a lot of these.
+ CompositeTileSurface::Color { .. } => TileKind::Opaque,
+ // Clear tiles have a special bucket
+ CompositeTileSurface::Clear => TileKind::Clear,
+ CompositeTileSurface::Texture { .. }
+ | CompositeTileSurface::ExternalSurface { .. } => {
+ // Texture surfaces get bucketed by opaque/alpha, for z-rejection
+ // on the Draw compositor mode.
+ if is_opaque {
+ TileKind::Opaque
+ } else {
+ TileKind::Alpha
+ }
+ }
+ }
+}
+
+pub enum ExternalSurfaceDependency {
+ Yuv {
+ image_dependencies: [ImageDependency; 3],
+ color_space: YuvRangedColorSpace,
+ format: YuvFormat,
+ channel_bit_depth: u32,
+ },
+ Rgb {
+ image_dependency: ImageDependency,
+ },
+}
+
+/// Describes information about drawing a primitive as a compositor surface.
+/// For now, we support only YUV images as compositor surfaces, but in future
+/// this will also support RGBA images.
+pub struct ExternalSurfaceDescriptor {
+ // Normalized rectangle of this surface in local coordinate space
+ // TODO(gw): Fix up local_rect unit kinds in ExternalSurfaceDescriptor (many flow on effects)
+ pub local_surface_size: LayoutSize,
+ pub local_rect: PictureRect,
+ pub local_clip_rect: PictureRect,
+ pub clip_rect: DeviceRect,
+ pub transform_index: CompositorTransformIndex,
+ pub image_rendering: ImageRendering,
+ pub z_id: ZBufferId,
+ pub dependency: ExternalSurfaceDependency,
+ /// If native compositing is enabled, the native compositor surface handle.
+ /// Otherwise, this will be None
+ pub native_surface_id: Option<NativeSurfaceId>,
+ /// If the native surface needs to be updated, this will contain the size
+ /// of the native surface as Some(size). If not dirty, this is None.
+ pub update_params: Option<DeviceIntSize>,
+}
+
+/// Information about a plane in a YUV or RGB surface.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone)]
+pub struct ExternalPlaneDescriptor {
+ pub texture: TextureSource,
+ pub uv_rect: TexelRect,
+}
+
+impl ExternalPlaneDescriptor {
+ fn invalid() -> Self {
+ ExternalPlaneDescriptor {
+ texture: TextureSource::Invalid,
+ uv_rect: TexelRect::invalid(),
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub struct ResolvedExternalSurfaceIndex(pub usize);
+
+impl ResolvedExternalSurfaceIndex {
+ pub const INVALID: ResolvedExternalSurfaceIndex = ResolvedExternalSurfaceIndex(usize::MAX);
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum ResolvedExternalSurfaceColorData {
+ Yuv {
+ // YUV specific information
+ image_dependencies: [ImageDependency; 3],
+ planes: [ExternalPlaneDescriptor; 3],
+ color_space: YuvRangedColorSpace,
+ format: YuvFormat,
+ channel_bit_depth: u32,
+ },
+ Rgb {
+ image_dependency: ImageDependency,
+ plane: ExternalPlaneDescriptor,
+ },
+}
+
+/// An ExternalSurfaceDescriptor that has had image keys
+/// resolved to texture handles. This contains all the
+/// information that the compositor step in renderer
+/// needs to know.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ResolvedExternalSurface {
+ pub color_data: ResolvedExternalSurfaceColorData,
+ pub image_buffer_kind: ImageBufferKind,
+ // Update information for a native surface if it's dirty
+ pub update_params: Option<(NativeSurfaceId, DeviceIntSize)>,
+}
+
+/// Public interface specified in `WebRenderOptions` that configures
+/// how WR compositing will operate.
+pub enum CompositorConfig {
+ /// Let WR draw tiles via normal batching. This requires no special OS support.
+ Draw {
+ /// If this is zero, a full screen present occurs at the end of the
+ /// frame. This is the simplest and default mode. If this is non-zero,
+ /// then the operating system supports a form of 'partial present' where
+ /// only dirty regions of the framebuffer need to be updated.
+ max_partial_present_rects: usize,
+ /// If this is true, WR must draw the previous frames' dirty regions when
+ /// doing a partial present. This is used for EGL which requires the front
+ /// buffer to always be fully consistent.
+ draw_previous_partial_present_regions: bool,
+ /// A client provided interface to a compositor handling partial present.
+ /// Required if webrender must query the backbuffer's age.
+ partial_present: Option<Box<dyn PartialPresentCompositor>>,
+ },
+ /// Use a native OS compositor to draw tiles. This requires clients to implement
+ /// the Compositor trait, but can be significantly more power efficient on operating
+ /// systems that support it.
+ Native {
+ /// A client provided interface to a native / OS compositor.
+ compositor: Box<dyn Compositor>,
+ }
+}
+
+impl CompositorConfig {
+ pub fn compositor(&mut self) -> Option<&mut Box<dyn Compositor>> {
+ match self {
+ CompositorConfig::Native { ref mut compositor, .. } => {
+ Some(compositor)
+ }
+ CompositorConfig::Draw { .. } => {
+ None
+ }
+ }
+ }
+
+ pub fn partial_present(&mut self) -> Option<&mut Box<dyn PartialPresentCompositor>> {
+ match self {
+ CompositorConfig::Native { .. } => {
+ None
+ }
+ CompositorConfig::Draw { ref mut partial_present, .. } => {
+ partial_present.as_mut()
+ }
+ }
+ }
+
+}
+
+impl Default for CompositorConfig {
+ /// Default compositor config is full present without partial present.
+ fn default() -> Self {
+ CompositorConfig::Draw {
+ max_partial_present_rects: 0,
+ draw_previous_partial_present_regions: false,
+ partial_present: None,
+ }
+ }
+}
+
+/// This is a representation of `CompositorConfig` without the `Compositor` trait
+/// present. This allows it to be freely copied to other threads, such as the render
+/// backend where the frame builder can access it.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub enum CompositorKind {
+ /// WR handles compositing via drawing.
+ Draw {
+ /// Partial present support.
+ max_partial_present_rects: usize,
+ /// Draw previous regions when doing partial present.
+ draw_previous_partial_present_regions: bool,
+ },
+ /// Native OS compositor.
+ Native {
+ /// The capabilities of the underlying platform.
+ capabilities: CompositorCapabilities,
+ },
+}
+
+impl Default for CompositorKind {
+ /// Default compositor config is full present without partial present.
+ fn default() -> Self {
+ CompositorKind::Draw {
+ max_partial_present_rects: 0,
+ draw_previous_partial_present_regions: false,
+ }
+ }
+}
+
+impl CompositorKind {
+ pub fn get_virtual_surface_size(&self) -> i32 {
+ match self {
+ CompositorKind::Draw { .. } => 0,
+ CompositorKind::Native { capabilities, .. } => capabilities.virtual_surface_size,
+ }
+ }
+
+ pub fn should_redraw_on_invalidation(&self) -> bool {
+ match self {
+ CompositorKind::Draw { max_partial_present_rects, .. } => {
+ // When partial present is enabled, we need to force redraw.
+ *max_partial_present_rects > 0
+ }
+ CompositorKind::Native { capabilities, .. } => capabilities.redraw_on_invalidation,
+ }
+ }
+}
+
+/// The backing surface kind for a tile. Same as `TileSurface`, minus
+/// the texture cache handles, visibility masks etc.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(PartialEq, Clone)]
+pub enum TileSurfaceKind {
+ Texture,
+ Color {
+ color: ColorF,
+ },
+ Clear,
+}
+
+impl From<&TileSurface> for TileSurfaceKind {
+ fn from(surface: &TileSurface) -> Self {
+ match surface {
+ TileSurface::Texture { .. } => TileSurfaceKind::Texture,
+ TileSurface::Color { color } => TileSurfaceKind::Color { color: *color },
+ TileSurface::Clear => TileSurfaceKind::Clear,
+ }
+ }
+}
+
+/// Describes properties that identify a tile composition uniquely.
+/// The backing surface for this tile.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(PartialEq, Clone)]
+pub struct CompositeTileDescriptor {
+ pub tile_id: TileId,
+ pub surface_kind: TileSurfaceKind,
+}
+
+/// Describes the properties that identify a surface composition uniquely.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(PartialEq, Clone)]
+pub struct CompositeSurfaceDescriptor {
+ pub surface_id: Option<NativeSurfaceId>,
+ pub clip_rect: DeviceRect,
+ pub transform: CompositorSurfaceTransform,
+ // A list of image keys and generations that this compositor surface
+ // depends on. This avoids composites being skipped when the only
+ // thing that has changed is the generation of an compositor surface
+ // image dependency.
+ pub image_dependencies: [ImageDependency; 3],
+ pub image_rendering: ImageRendering,
+ // List of the surface information for each tile added to this virtual surface
+ pub tile_descriptors: Vec<CompositeTileDescriptor>,
+}
+
+/// Describes surface properties used to composite a frame. This
+/// is used to compare compositions between frames.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(PartialEq, Clone)]
+pub struct CompositeDescriptor {
+ pub surfaces: Vec<CompositeSurfaceDescriptor>,
+ pub external_surfaces_rect: DeviceRect,
+}
+
+impl CompositeDescriptor {
+ /// Construct an empty descriptor.
+ pub fn empty() -> Self {
+ CompositeDescriptor {
+ surfaces: Vec::new(),
+ external_surfaces_rect: DeviceRect::zero(),
+ }
+ }
+}
+
+pub struct CompositeStatePreallocator {
+ tiles: Preallocator,
+ external_surfaces: Preallocator,
+ occluders: Preallocator,
+ occluders_events: Preallocator,
+ occluders_active: Preallocator,
+ descriptor_surfaces: Preallocator,
+}
+
+impl CompositeStatePreallocator {
+ pub fn record(&mut self, state: &CompositeState) {
+ self.tiles.record_vec(&state.tiles);
+ self.external_surfaces.record_vec(&state.external_surfaces);
+ self.occluders.record_vec(&state.occluders.occluders);
+ self.occluders_events.record_vec(&state.occluders.events);
+ self.occluders_active.record_vec(&state.occluders.active);
+ self.descriptor_surfaces.record_vec(&state.descriptor.surfaces);
+ }
+
+ pub fn preallocate(&self, state: &mut CompositeState) {
+ self.tiles.preallocate_vec(&mut state.tiles);
+ self.external_surfaces.preallocate_vec(&mut state.external_surfaces);
+ self.occluders.preallocate_vec(&mut state.occluders.occluders);
+ self.occluders_events.preallocate_vec(&mut state.occluders.events);
+ self.occluders_active.preallocate_vec(&mut state.occluders.active);
+ self.descriptor_surfaces.preallocate_vec(&mut state.descriptor.surfaces);
+ }
+}
+
+impl Default for CompositeStatePreallocator {
+ fn default() -> Self {
+ CompositeStatePreallocator {
+ tiles: Preallocator::new(56),
+ external_surfaces: Preallocator::new(0),
+ occluders: Preallocator::new(16),
+ occluders_events: Preallocator::new(32),
+ occluders_active: Preallocator::new(16),
+ descriptor_surfaces: Preallocator::new(8),
+ }
+ }
+}
+
+/// A transform for either a picture cache or external compositor surface, stored
+/// in the `CompositeState` structure. This allows conversions from local rects
+/// to raster or device rects, without access to the spatial tree (e.g. during
+/// the render step where dirty rects are calculated). Since we know that we only
+/// handle scale and offset transforms for these types, we can store a single
+/// ScaleOffset rather than 4x4 matrix here for efficiency.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct CompositorTransform {
+ // Map from local rect of a composite tile to the real backing surface coords
+ local_to_surface: ScaleOffset,
+ // Map from surface coords to the final device space position
+ surface_to_device: ScaleOffset,
+ // Combined local -> surface -> device transform
+ local_to_device: ScaleOffset,
+}
+
+/// The list of tiles to be drawn this frame
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct CompositeState {
+ // TODO(gw): Consider splitting up CompositeState into separate struct types depending
+ // on the selected compositing mode. Many of the fields in this state struct
+ // are only applicable to either Native or Draw compositing mode.
+ /// List of tiles to be drawn by the Draw compositor.
+ /// Tiles are accumulated in this vector and sorted from front to back at the end of the
+ /// frame.
+ pub tiles: Vec<CompositeTile>,
+ /// List of primitives that were promoted to be compositor surfaces.
+ pub external_surfaces: Vec<ResolvedExternalSurface>,
+ /// Used to generate z-id values for tiles in the Draw compositor mode.
+ pub z_generator: ZBufferIdGenerator,
+ // If false, we can't rely on the dirty rects in the CompositeTile
+ // instances. This currently occurs during a scroll event, as a
+ // signal to refresh the whole screen. This is only a temporary
+ // measure until we integrate with OS compositors. In the meantime
+ // it gives us the ability to partial present for any non-scroll
+ // case as a simple win (e.g. video, animation etc).
+ pub dirty_rects_are_valid: bool,
+ /// The kind of compositor for picture cache tiles (e.g. drawn by WR, or OS compositor)
+ pub compositor_kind: CompositorKind,
+ /// List of registered occluders
+ pub occluders: Occluders,
+ /// Description of the surfaces and properties that are being composited.
+ pub descriptor: CompositeDescriptor,
+ /// Debugging information about the state of the pictures cached for regression testing.
+ pub picture_cache_debug: PictureCacheDebugInfo,
+ /// List of registered transforms used by picture cache or external surfaces
+ pub transforms: Vec<CompositorTransform>,
+ /// Whether we have low quality pinch zoom enabled
+ low_quality_pinch_zoom: bool,
+}
+
+impl CompositeState {
+ /// Construct a new state for compositing picture tiles. This is created
+ /// during each frame construction and passed to the renderer.
+ pub fn new(
+ compositor_kind: CompositorKind,
+ max_depth_ids: i32,
+ dirty_rects_are_valid: bool,
+ low_quality_pinch_zoom: bool,
+ ) -> Self {
+ CompositeState {
+ tiles: Vec::new(),
+ z_generator: ZBufferIdGenerator::new(max_depth_ids),
+ dirty_rects_are_valid,
+ compositor_kind,
+ occluders: Occluders::new(),
+ descriptor: CompositeDescriptor::empty(),
+ external_surfaces: Vec::new(),
+ picture_cache_debug: PictureCacheDebugInfo::new(),
+ transforms: Vec::new(),
+ low_quality_pinch_zoom,
+ }
+ }
+
+ /// Register use of a transform for a picture cache tile or external surface
+ pub fn register_transform(
+ &mut self,
+ local_to_surface: ScaleOffset,
+ surface_to_device: ScaleOffset,
+ ) -> CompositorTransformIndex {
+ let index = CompositorTransformIndex(self.transforms.len());
+
+ let local_to_device = local_to_surface.accumulate(&surface_to_device);
+
+ self.transforms.push(CompositorTransform {
+ local_to_surface,
+ surface_to_device,
+ local_to_device,
+ });
+
+ index
+ }
+
+ /// Calculate the device-space rect of a local compositor surface rect
+ pub fn get_device_rect(
+ &self,
+ local_rect: &PictureRect,
+ transform_index: CompositorTransformIndex,
+ ) -> DeviceRect {
+ let transform = &self.transforms[transform_index.0];
+ transform.local_to_device.map_rect(&local_rect).round()
+ }
+
+ /// Calculate the device-space rect of a local compositor surface rect, normalized
+ /// to the origin of a given point
+ pub fn get_surface_rect<T>(
+ &self,
+ local_sub_rect: &Box2D<f32, T>,
+ local_bounds: &Box2D<f32, T>,
+ transform_index: CompositorTransformIndex,
+ ) -> DeviceRect {
+ let transform = &self.transforms[transform_index.0];
+
+ let surface_bounds = transform.local_to_surface.map_rect(&local_bounds);
+ let surface_rect = transform.local_to_surface.map_rect(&local_sub_rect);
+
+ surface_rect
+ .round_out()
+ .translate(-surface_bounds.min.to_vector())
+ .round_out()
+ .intersection(&surface_bounds.size().round().into())
+ .unwrap_or_else(DeviceRect::zero)
+ }
+
+ /// Get the local -> device compositor transform
+ pub fn get_device_transform(
+ &self,
+ transform_index: CompositorTransformIndex,
+ ) -> ScaleOffset {
+ let transform = &self.transforms[transform_index.0];
+ transform.local_to_device
+ }
+
+ /// Get the surface -> device compositor transform
+ pub fn get_compositor_transform(
+ &self,
+ transform_index: CompositorTransformIndex,
+ ) -> ScaleOffset {
+ let transform = &self.transforms[transform_index.0];
+ transform.surface_to_device
+ }
+
+ /// Register an occluder during picture cache updates that can be
+ /// used during frame building to occlude tiles.
+ pub fn register_occluder(
+ &mut self,
+ z_id: ZBufferId,
+ rect: WorldRect,
+ ) {
+ let world_rect = rect.round().to_i32();
+
+ self.occluders.push(world_rect, z_id);
+ }
+
+ /// Add a picture cache to be composited
+ pub fn push_surface(
+ &mut self,
+ tile_cache: &TileCacheInstance,
+ device_clip_rect: DeviceRect,
+ resource_cache: &ResourceCache,
+ gpu_cache: &mut GpuCache,
+ deferred_resolves: &mut Vec<DeferredResolve>,
+ ) {
+ let slice_transform = self.get_compositor_transform(tile_cache.transform_index);
+
+ let image_rendering = if self.low_quality_pinch_zoom {
+ ImageRendering::Auto
+ } else {
+ ImageRendering::CrispEdges
+ };
+
+ if let Some(backdrop_surface) = &tile_cache.backdrop_surface {
+ // Use the backdrop native surface we created and add that to the composite state.
+ self.descriptor.surfaces.push(
+ CompositeSurfaceDescriptor {
+ surface_id: Some(backdrop_surface.id),
+ clip_rect: backdrop_surface.device_rect,
+ transform: slice_transform,
+ image_dependencies: [ImageDependency::INVALID; 3],
+ image_rendering,
+ tile_descriptors: Vec::new(),
+ }
+ );
+ }
+
+ for sub_slice in &tile_cache.sub_slices {
+ let mut surface_device_rect = DeviceRect::zero();
+
+ for tile in sub_slice.tiles.values() {
+ if !tile.is_visible {
+ // This can occur when a tile is found to be occluded during frame building.
+ continue;
+ }
+
+ // Accumulate this tile into the overall surface bounds. This is used below
+ // to clamp the size of the supplied clip rect to a reasonable value.
+ // NOTE: This clip rect must include the device_valid_rect rather than
+ // the tile device rect. This ensures that in the case of a picture
+ // cache slice that is smaller than a single tile, the clip rect in
+ // the composite descriptor will change if the position of that slice
+ // is changed. Otherwise, WR may conclude that no composite is needed
+ // if the tile itself was not invalidated due to changing content.
+ // See bug #1675414 for more detail.
+ surface_device_rect = surface_device_rect.union(&tile.device_valid_rect);
+ }
+
+ // Append the visible tiles from this sub-slice
+ self.tiles.extend_from_slice(&sub_slice.composite_tiles);
+
+ // If the clip rect is too large, it can cause accuracy and correctness problems
+ // for some native compositors (specifically, CoreAnimation in this case). To
+ // work around that, intersect the supplied clip rect with the current bounds
+ // of the native surface, which ensures it is a reasonable size.
+ let surface_clip_rect = device_clip_rect
+ .intersection(&surface_device_rect)
+ .unwrap_or(DeviceRect::zero());
+
+ // Only push tiles if they have valid clip rects.
+ if !surface_clip_rect.is_empty() {
+ // Add opaque surface before any compositor surfaces
+ if !sub_slice.opaque_tile_descriptors.is_empty() {
+ self.descriptor.surfaces.push(
+ CompositeSurfaceDescriptor {
+ surface_id: sub_slice.native_surface.as_ref().map(|s| s.opaque),
+ clip_rect: surface_clip_rect,
+ transform: slice_transform,
+ image_dependencies: [ImageDependency::INVALID; 3],
+ image_rendering,
+ tile_descriptors: sub_slice.opaque_tile_descriptors.clone(),
+ }
+ );
+ }
+
+ // Add alpha tiles after opaque surfaces
+ if !sub_slice.alpha_tile_descriptors.is_empty() {
+ self.descriptor.surfaces.push(
+ CompositeSurfaceDescriptor {
+ surface_id: sub_slice.native_surface.as_ref().map(|s| s.alpha),
+ clip_rect: surface_clip_rect,
+ transform: slice_transform,
+ image_dependencies: [ImageDependency::INVALID; 3],
+ image_rendering,
+ tile_descriptors: sub_slice.alpha_tile_descriptors.clone(),
+ }
+ );
+ }
+ }
+
+ // For each compositor surface that was promoted, build the
+ // information required for the compositor to draw it
+ for compositor_surface in &sub_slice.compositor_surfaces {
+ let external_surface = &compositor_surface.descriptor;
+
+ let clip_rect = external_surface
+ .clip_rect
+ .intersection(&device_clip_rect)
+ .unwrap_or_else(DeviceRect::zero);
+
+ // Skip compositor surfaces with empty clip rects.
+ if clip_rect.is_empty() {
+ continue;
+ }
+
+ let required_plane_count =
+ match external_surface.dependency {
+ ExternalSurfaceDependency::Yuv { format, .. } => {
+ format.get_plane_num()
+ },
+ ExternalSurfaceDependency::Rgb { .. } => {
+ 1
+ }
+ };
+
+ let mut image_dependencies = [ImageDependency::INVALID; 3];
+
+ for i in 0 .. required_plane_count {
+ let dependency = match external_surface.dependency {
+ ExternalSurfaceDependency::Yuv { image_dependencies, .. } => {
+ image_dependencies[i]
+ },
+ ExternalSurfaceDependency::Rgb { image_dependency, .. } => {
+ image_dependency
+ }
+ };
+ image_dependencies[i] = dependency;
+ }
+
+ // Get a new z_id for each compositor surface, to ensure correct ordering
+ // when drawing with the simple (Draw) compositor, and to schedule compositing
+ // of any required updates into the surfaces.
+ let needs_external_surface_update = match self.compositor_kind {
+ CompositorKind::Draw { .. } => true,
+ _ => external_surface.update_params.is_some(),
+ };
+ let external_surface_index = if needs_external_surface_update {
+ let external_surface_index = self.compute_external_surface_dependencies(
+ &external_surface,
+ &image_dependencies,
+ required_plane_count,
+ resource_cache,
+ gpu_cache,
+ deferred_resolves,
+ );
+ if external_surface_index == ResolvedExternalSurfaceIndex::INVALID {
+ continue;
+ }
+ external_surface_index
+ } else {
+ ResolvedExternalSurfaceIndex::INVALID
+ };
+
+ let surface = CompositeTileSurface::ExternalSurface { external_surface_index };
+ let local_rect = external_surface.local_surface_size.cast_unit().into();
+
+ let tile = CompositeTile {
+ kind: tile_kind(&surface, compositor_surface.is_opaque),
+ surface,
+ local_rect,
+ local_valid_rect: local_rect,
+ local_dirty_rect: local_rect,
+ device_clip_rect: clip_rect,
+ z_id: external_surface.z_id,
+ transform_index: external_surface.transform_index,
+ };
+
+ // Add a surface descriptor for each compositor surface. For the Draw
+ // compositor, this is used to avoid composites being skipped by adding
+ // a dependency on the compositor surface external image keys / generations.
+ self.descriptor.surfaces.push(
+ CompositeSurfaceDescriptor {
+ surface_id: external_surface.native_surface_id,
+ clip_rect,
+ transform: self.get_compositor_transform(external_surface.transform_index),
+ image_dependencies: image_dependencies,
+ image_rendering: external_surface.image_rendering,
+ tile_descriptors: Vec::new(),
+ }
+ );
+
+ let device_rect =
+ self.get_device_rect(&local_rect, external_surface.transform_index);
+ self.descriptor.external_surfaces_rect =
+ self.descriptor.external_surfaces_rect.union(&device_rect);
+
+ self.tiles.push(tile);
+ }
+ }
+ }
+
+ /// Compare this state vs. a previous frame state, and invalidate dirty rects if
+ /// the surface count has changed
+ pub fn update_dirty_rect_validity(
+ &mut self,
+ old_descriptor: &CompositeDescriptor,
+ ) {
+ // TODO(gw): Make this more robust in other cases - there are other situations where
+ // the surface count may be the same but we still need to invalidate the
+ // dirty rects (e.g. if the surface ordering changed, or the external
+ // surface itself is animated?)
+
+ if old_descriptor.surfaces.len() != self.descriptor.surfaces.len() {
+ self.dirty_rects_are_valid = false;
+ return;
+ }
+
+ // The entire area of external surfaces are treated as dirty, however,
+ // if a surface has moved or shrunk that is no longer valid, as we
+ // additionally need to ensure the area the surface used to occupy is
+ // composited.
+ if !self
+ .descriptor
+ .external_surfaces_rect
+ .contains_box(&old_descriptor.external_surfaces_rect)
+ {
+ self.dirty_rects_are_valid = false;
+ return;
+ }
+ }
+
+ fn compute_external_surface_dependencies(
+ &mut self,
+ external_surface: &ExternalSurfaceDescriptor,
+ image_dependencies: &[ImageDependency; 3],
+ required_plane_count: usize,
+ resource_cache: &ResourceCache,
+ gpu_cache: &mut GpuCache,
+ deferred_resolves: &mut Vec<DeferredResolve>,
+ ) -> ResolvedExternalSurfaceIndex {
+ let mut planes = [
+ ExternalPlaneDescriptor::invalid(),
+ ExternalPlaneDescriptor::invalid(),
+ ExternalPlaneDescriptor::invalid(),
+ ];
+
+ let mut valid_plane_count = 0;
+ for i in 0 .. required_plane_count {
+ let request = ImageRequest {
+ key: image_dependencies[i].key,
+ rendering: external_surface.image_rendering,
+ tile: None,
+ };
+
+ let cache_item = resolve_image(
+ request,
+ resource_cache,
+ gpu_cache,
+ deferred_resolves,
+ );
+
+ if cache_item.texture_id != TextureSource::Invalid {
+ valid_plane_count += 1;
+ let plane = &mut planes[i];
+ *plane = ExternalPlaneDescriptor {
+ texture: cache_item.texture_id,
+ uv_rect: cache_item.uv_rect.into(),
+ };
+ }
+ }
+
+ // Check if there are valid images added for each YUV plane
+ if valid_plane_count < required_plane_count {
+ warn!("Warnings: skip a YUV/RGB compositor surface, found {}/{} valid images",
+ valid_plane_count,
+ required_plane_count,
+ );
+ return ResolvedExternalSurfaceIndex::INVALID;
+ }
+
+ let external_surface_index = ResolvedExternalSurfaceIndex(self.external_surfaces.len());
+
+ // If the external surface descriptor reports that the native surface
+ // needs to be updated, create an update params tuple for the renderer
+ // to use.
+ let update_params = external_surface.update_params.map(|surface_size| {
+ (
+ external_surface.native_surface_id.expect("bug: no native surface!"),
+ surface_size
+ )
+ });
+
+ match external_surface.dependency {
+ ExternalSurfaceDependency::Yuv{ color_space, format, channel_bit_depth, .. } => {
+
+ let image_buffer_kind = planes[0].texture.image_buffer_kind();
+
+ self.external_surfaces.push(ResolvedExternalSurface {
+ color_data: ResolvedExternalSurfaceColorData::Yuv {
+ image_dependencies: *image_dependencies,
+ planes,
+ color_space,
+ format,
+ channel_bit_depth,
+ },
+ image_buffer_kind,
+ update_params,
+ });
+ },
+ ExternalSurfaceDependency::Rgb { .. } => {
+ let image_buffer_kind = planes[0].texture.image_buffer_kind();
+
+ self.external_surfaces.push(ResolvedExternalSurface {
+ color_data: ResolvedExternalSurfaceColorData::Rgb {
+ image_dependency: image_dependencies[0],
+ plane: planes[0],
+ },
+ image_buffer_kind,
+ update_params,
+ });
+ },
+ }
+ external_surface_index
+ }
+
+ pub fn end_frame(&mut self) {
+ // Sort tiles from front to back.
+ self.tiles.sort_by_key(|tile| tile.z_id.0);
+ }
+}
+
+/// An arbitrary identifier for a native (OS compositor) surface
+#[repr(C)]
+#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct NativeSurfaceId(pub u64);
+
+impl NativeSurfaceId {
+ /// A special id for the native surface that is used for debug / profiler overlays.
+ pub const DEBUG_OVERLAY: NativeSurfaceId = NativeSurfaceId(u64::MAX);
+}
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct NativeTileId {
+ pub surface_id: NativeSurfaceId,
+ pub x: i32,
+ pub y: i32,
+}
+
+impl NativeTileId {
+ /// A special id for the native surface that is used for debug / profiler overlays.
+ pub const DEBUG_OVERLAY: NativeTileId = NativeTileId {
+ surface_id: NativeSurfaceId::DEBUG_OVERLAY,
+ x: 0,
+ y: 0,
+ };
+}
+
+/// Information about a bound surface that the native compositor
+/// returns to WR.
+#[repr(C)]
+#[derive(Copy, Clone)]
+pub struct NativeSurfaceInfo {
+ /// An offset into the surface that WR should draw. Some compositing
+ /// implementations (notably, DirectComposition) use texture atlases
+ /// when the surface sizes are small. In this case, an offset can
+ /// be returned into the larger texture where WR should draw. This
+ /// can be (0, 0) if texture atlases are not used.
+ pub origin: DeviceIntPoint,
+ /// The ID of the FBO that WR should bind to, in order to draw to
+ /// the bound surface. On Windows (ANGLE) this will always be 0,
+ /// since creating a p-buffer sets the default framebuffer to
+ /// be the DirectComposition surface. On Mac, this will be non-zero,
+ /// since it identifies the IOSurface that has been bound to draw to.
+ // TODO(gw): This may need to be a larger / different type for WR
+ // backends that are not GL.
+ pub fbo_id: u32,
+}
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct CompositorCapabilities {
+ /// The virtual surface size used by the underlying platform.
+ pub virtual_surface_size: i32,
+ /// Whether the compositor requires redrawing on invalidation.
+ pub redraw_on_invalidation: bool,
+ /// The maximum number of dirty rects that can be provided per compositor
+ /// surface update. If this is zero, the entire compositor surface for
+ /// a given tile will be drawn if it's dirty.
+ pub max_update_rects: usize,
+ /// Whether or not this compositor will create surfaces for backdrops.
+ pub supports_surface_for_backdrop: bool,
+ /// Whether external compositor surface supports negative scaling.
+ pub supports_external_compositor_surface_negative_scaling: bool,
+}
+
+impl Default for CompositorCapabilities {
+ fn default() -> Self {
+ // The default set of compositor capabilities for a given platform.
+ // These should only be modified if a compositor diverges specifically
+ // from the default behavior so that compositors don't have to track
+ // which changes to this structure unless necessary.
+ CompositorCapabilities {
+ virtual_surface_size: 0,
+ redraw_on_invalidation: false,
+ // Assume compositors can do at least partial update of surfaces. If not,
+ // the native compositor should override this to be 0.
+ max_update_rects: 1,
+ supports_surface_for_backdrop: false,
+ supports_external_compositor_surface_negative_scaling: true,
+ }
+ }
+}
+
+#[repr(C)]
+#[derive(Copy, Clone, Debug)]
+pub enum WindowSizeMode {
+ Normal,
+ Minimized,
+ Maximized,
+ Fullscreen,
+ Invalid,
+}
+
+#[repr(C)]
+#[derive(Copy, Clone, Debug)]
+pub struct WindowVisibility {
+ pub size_mode: WindowSizeMode,
+ pub is_fully_occluded: bool,
+}
+
+impl Default for WindowVisibility {
+ fn default() -> Self {
+ WindowVisibility {
+ size_mode: WindowSizeMode::Normal,
+ is_fully_occluded: false,
+ }
+ }
+}
+
+/// The transform type to apply to Compositor surfaces.
+// TODO: Should transform from CompositorSurfacePixel instead, but this requires a cleanup of the
+// Compositor API to use CompositorSurface-space geometry instead of Device-space where necessary
+// to avoid a bunch of noisy cast_unit calls and make it actually type-safe. May be difficult due
+// to pervasive use of Device-space nomenclature inside WR.
+// pub struct CompositorSurfacePixel;
+pub type CompositorSurfaceTransform = ScaleOffset;
+
+/// Defines an interface to a native (OS level) compositor. If supplied
+/// by the client application, then picture cache slices will be
+/// composited by the OS compositor, rather than drawn via WR batches.
+pub trait Compositor {
+ /// Create a new OS compositor surface with the given properties.
+ fn create_surface(
+ &mut self,
+ device: &mut Device,
+ id: NativeSurfaceId,
+ virtual_offset: DeviceIntPoint,
+ tile_size: DeviceIntSize,
+ is_opaque: bool,
+ );
+
+ /// Create a new OS compositor surface that can be used with an
+ /// existing ExternalImageId, instead of being drawn to by WebRender.
+ /// Surfaces created by this can only be used with attach_external_image,
+ /// and not create_tile/destroy_tile/bind/unbind.
+ fn create_external_surface(
+ &mut self,
+ device: &mut Device,
+ id: NativeSurfaceId,
+ is_opaque: bool,
+ );
+
+ /// Create a new OS backdrop surface that will display a color.
+ fn create_backdrop_surface(
+ &mut self,
+ device: &mut Device,
+ id: NativeSurfaceId,
+ color: ColorF,
+ );
+
+ /// Destroy the surface with the specified id. WR may call this
+ /// at any time the surface is no longer required (including during
+ /// renderer deinit). It's the responsibility of the embedder
+ /// to ensure that the surface is only freed once the GPU is
+ /// no longer using the surface (if this isn't already handled
+ /// by the operating system).
+ fn destroy_surface(
+ &mut self,
+ device: &mut Device,
+ id: NativeSurfaceId,
+ );
+
+ /// Create a new OS compositor tile with the given properties.
+ fn create_tile(
+ &mut self,
+ device: &mut Device,
+ id: NativeTileId,
+ );
+
+ /// Destroy an existing compositor tile.
+ fn destroy_tile(
+ &mut self,
+ device: &mut Device,
+ id: NativeTileId,
+ );
+
+ /// Attaches an ExternalImageId to an OS compositor surface created
+ /// by create_external_surface, and uses that as the contents of
+ /// the surface. It is expected that a single surface will have
+ /// many different images attached (like one for each video frame).
+ fn attach_external_image(
+ &mut self,
+ device: &mut Device,
+ id: NativeSurfaceId,
+ external_image: ExternalImageId
+ );
+
+ /// Mark a tile as invalid before any surfaces are queued for
+ /// composition and before it is updated with bind. This is useful
+ /// for early composition, allowing for dependency tracking of which
+ /// surfaces can be composited early while others are still updating.
+ fn invalidate_tile(
+ &mut self,
+ _device: &mut Device,
+ _id: NativeTileId,
+ _valid_rect: DeviceIntRect
+ ) {}
+
+ /// Bind this surface such that WR can issue OpenGL commands
+ /// that will target the surface. Returns an (x, y) offset
+ /// where WR should draw into the surface. This can be set
+ /// to (0, 0) if the OS doesn't use texture atlases. The dirty
+ /// rect is a local surface rect that specifies which part
+ /// of the surface needs to be updated. If max_update_rects
+ /// in CompositeConfig is 0, this will always be the size
+ /// of the entire surface. The returned offset is only
+ /// relevant to compositors that store surfaces in a texture
+ /// atlas (that is, WR expects that the dirty rect doesn't
+ /// affect the coordinates of the returned origin).
+ fn bind(
+ &mut self,
+ device: &mut Device,
+ id: NativeTileId,
+ dirty_rect: DeviceIntRect,
+ valid_rect: DeviceIntRect,
+ ) -> NativeSurfaceInfo;
+
+ /// Unbind the surface. This is called by WR when it has
+ /// finished issuing OpenGL commands on the current surface.
+ fn unbind(
+ &mut self,
+ device: &mut Device,
+ );
+
+ /// Begin the frame
+ fn begin_frame(&mut self, device: &mut Device);
+
+ /// Add a surface to the visual tree to be composited. Visuals must
+ /// be added every frame, between the begin/end transaction call. The
+ /// z-order of the surfaces is determined by the order they are added
+ /// to the visual tree.
+ // TODO(gw): Adding visuals every frame makes the interface simple,
+ // but may have performance implications on some compositors?
+ // We might need to change the interface to maintain a visual
+ // tree that can be mutated?
+ // TODO(gw): We might need to add a concept of a hierachy in future.
+ fn add_surface(
+ &mut self,
+ device: &mut Device,
+ id: NativeSurfaceId,
+ transform: CompositorSurfaceTransform,
+ clip_rect: DeviceIntRect,
+ image_rendering: ImageRendering,
+ );
+
+ /// Notify the compositor that all tiles have been invalidated and all
+ /// native surfaces have been added, thus it is safe to start compositing
+ /// valid surfaces. The dirty rects array allows native compositors that
+ /// support partial present to skip copying unchanged areas.
+ /// Optionally provides a set of rectangles for the areas known to be
+ /// opaque, this is currently only computed if the caller is SwCompositor.
+ fn start_compositing(
+ &mut self,
+ _device: &mut Device,
+ _clear_color: ColorF,
+ _dirty_rects: &[DeviceIntRect],
+ _opaque_rects: &[DeviceIntRect],
+ ) {}
+
+ /// Commit any changes in the compositor tree for this frame. WR calls
+ /// this once when all surface and visual updates are complete, to signal
+ /// that the OS composite transaction should be applied.
+ fn end_frame(&mut self, device: &mut Device);
+
+ /// Enable/disable native compositor usage
+ fn enable_native_compositor(&mut self, device: &mut Device, enable: bool);
+
+ /// Safely deinitialize any remaining resources owned by the compositor.
+ fn deinit(&mut self, device: &mut Device);
+
+ /// Get the capabilities struct for this compositor. This is used to
+ /// specify what features a compositor supports, depending on the
+ /// underlying platform
+ fn get_capabilities(&self, device: &mut Device) -> CompositorCapabilities;
+
+ fn get_window_visibility(&self, device: &mut Device) -> WindowVisibility;
+}
+
+/// Information about the underlying data buffer of a mapped tile.
+#[repr(C)]
+#[derive(Copy, Clone)]
+pub struct MappedTileInfo {
+ pub data: *mut c_void,
+ pub stride: i32,
+}
+
+/// Descriptor for a locked surface that will be directly composited by SWGL.
+#[repr(C)]
+pub struct SWGLCompositeSurfaceInfo {
+ /// The number of YUV planes in the surface. 0 indicates non-YUV BGRA.
+ /// 1 is interleaved YUV. 2 is NV12. 3 is planar YUV.
+ pub yuv_planes: u32,
+ /// Textures for planes of the surface, or 0 if not applicable.
+ pub textures: [u32; 3],
+ /// Color space of surface if using a YUV format.
+ pub color_space: YuvRangedColorSpace,
+ /// Color depth of surface if using a YUV format.
+ pub color_depth: ColorDepth,
+ /// The actual source surface size before transformation.
+ pub size: DeviceIntSize,
+}
+
+/// A Compositor variant that supports mapping tiles into CPU memory.
+pub trait MappableCompositor: Compositor {
+ /// Map a tile's underlying buffer so it can be used as the backing for
+ /// a SWGL framebuffer. This is intended to be a replacement for 'bind'
+ /// in any compositors that intend to directly interoperate with SWGL
+ /// while supporting some form of native layers.
+ fn map_tile(
+ &mut self,
+ device: &mut Device,
+ id: NativeTileId,
+ dirty_rect: DeviceIntRect,
+ valid_rect: DeviceIntRect,
+ ) -> Option<MappedTileInfo>;
+
+ /// Unmap a tile that was was previously mapped via map_tile to signal
+ /// that SWGL is done rendering to the buffer.
+ fn unmap_tile(&mut self, device: &mut Device);
+
+ fn lock_composite_surface(
+ &mut self,
+ device: &mut Device,
+ ctx: *mut c_void,
+ external_image_id: ExternalImageId,
+ composite_info: *mut SWGLCompositeSurfaceInfo,
+ ) -> bool;
+ fn unlock_composite_surface(&mut self, device: &mut Device, ctx: *mut c_void, external_image_id: ExternalImageId);
+}
+
+/// Defines an interface to a non-native (application-level) Compositor which handles
+/// partial present. This is required if webrender must query the backbuffer's age.
+/// TODO: Use the Compositor trait for native and non-native compositors, and integrate
+/// this functionality there.
+pub trait PartialPresentCompositor {
+ /// Allows webrender to specify the total region that will be rendered to this frame,
+ /// ie the frame's dirty region and some previous frames' dirty regions, if applicable
+ /// (calculated using the buffer age). Must be called before anything has been rendered
+ /// to the main framebuffer.
+ fn set_buffer_damage_region(&mut self, rects: &[DeviceIntRect]);
+}
+
+/// Information about an opaque surface used to occlude tiles.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct Occluder {
+ z_id: ZBufferId,
+ world_rect: WorldIntRect,
+}
+
+// Whether this event is the start or end of a rectangle
+#[derive(Debug)]
+enum OcclusionEventKind {
+ Begin,
+ End,
+}
+
+// A list of events on the y-axis, with the rectangle range that it affects on the x-axis
+#[derive(Debug)]
+struct OcclusionEvent {
+ y: i32,
+ x_range: ops::Range<i32>,
+ kind: OcclusionEventKind,
+}
+
+impl OcclusionEvent {
+ fn new(y: i32, kind: OcclusionEventKind, x0: i32, x1: i32) -> Self {
+ OcclusionEvent {
+ y,
+ x_range: ops::Range {
+ start: x0,
+ end: x1,
+ },
+ kind,
+ }
+ }
+}
+
+/// List of registered occluders.
+///
+/// Also store a couple of vectors for reuse.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct Occluders {
+ occluders: Vec<Occluder>,
+
+ // The two vectors below are kept to avoid unnecessary reallocations in area().
+
+ #[cfg_attr(feature = "serde", serde(skip))]
+ events: Vec<OcclusionEvent>,
+
+ #[cfg_attr(feature = "serde", serde(skip))]
+ active: Vec<ops::Range<i32>>,
+}
+
+impl Occluders {
+ fn new() -> Self {
+ Occluders {
+ occluders: Vec::new(),
+ events: Vec::new(),
+ active: Vec::new(),
+ }
+ }
+
+ fn push(&mut self, world_rect: WorldIntRect, z_id: ZBufferId) {
+ self.occluders.push(Occluder { world_rect, z_id });
+ }
+
+ /// Returns true if a tile with the specified rectangle and z_id
+ /// is occluded by an opaque surface in front of it.
+ pub fn is_tile_occluded(
+ &mut self,
+ z_id: ZBufferId,
+ world_rect: WorldRect,
+ ) -> bool {
+ // It's often the case that a tile is only occluded by considering multiple
+ // picture caches in front of it (for example, the background tiles are
+ // often occluded by a combination of the content slice + the scrollbar slices).
+
+ // The basic algorithm is:
+ // For every occluder:
+ // If this occluder is in front of the tile we are querying:
+ // Clip the occluder rectangle to the query rectangle.
+ // Calculate the total non-overlapping area of those clipped occluders.
+ // If the cumulative area of those occluders is the same as the area of the query tile,
+ // Then the entire tile must be occluded and can be skipped during rasterization and compositing.
+
+ // Get the reference area we will compare against.
+ let world_rect = world_rect.round().to_i32();
+ let ref_area = world_rect.area();
+
+ // Calculate the non-overlapping area of the valid occluders.
+ let cover_area = self.area(z_id, &world_rect);
+ debug_assert!(cover_area <= ref_area);
+
+ // Check if the tile area is completely covered
+ ref_area == cover_area
+ }
+
+ /// Return the total area covered by a set of occluders, accounting for
+ /// overlapping areas between those rectangles.
+ fn area(
+ &mut self,
+ z_id: ZBufferId,
+ clip_rect: &WorldIntRect,
+ ) -> i32 {
+ // This implementation is based on the article https://leetcode.com/articles/rectangle-area-ii/.
+ // This is not a particularly efficient implementation (it skips building segment trees), however
+ // we typically use this where the length of the rectangles array is < 10, so simplicity is more important.
+
+ self.events.clear();
+ self.active.clear();
+
+ let mut area = 0;
+
+ // Step through each rectangle and build the y-axis event list
+ for occluder in &self.occluders {
+ // Only consider occluders in front of this rect
+ if occluder.z_id.0 < z_id.0 {
+ // Clip the source rect to the rectangle we care about, since we only
+ // want to record area for the tile we are comparing to.
+ if let Some(rect) = occluder.world_rect.intersection(clip_rect) {
+ let x0 = rect.min.x;
+ let x1 = x0 + rect.width();
+ self.events.push(OcclusionEvent::new(rect.min.y, OcclusionEventKind::Begin, x0, x1));
+ self.events.push(OcclusionEvent::new(rect.min.y + rect.height(), OcclusionEventKind::End, x0, x1));
+ }
+ }
+ }
+
+ // If we didn't end up with any valid events, the area must be 0
+ if self.events.is_empty() {
+ return 0;
+ }
+
+ // Sort the events by y-value
+ self.events.sort_by_key(|e| e.y);
+ let mut cur_y = self.events[0].y;
+
+ // Step through each y interval
+ for event in &self.events {
+ // This is the dimension of the y-axis we are accumulating areas for
+ let dy = event.y - cur_y;
+
+ // If we have active events covering x-ranges in this y-interval, process them
+ if dy != 0 && !self.active.is_empty() {
+ assert!(dy > 0);
+
+ // Step through the x-ranges, ordered by x0 of each event
+ self.active.sort_by_key(|i| i.start);
+ let mut query = 0;
+ let mut cur = self.active[0].start;
+
+ // Accumulate the non-overlapping x-interval that contributes to area for this y-interval.
+ for interval in &self.active {
+ cur = interval.start.max(cur);
+ query += (interval.end - cur).max(0);
+ cur = cur.max(interval.end);
+ }
+
+ // Accumulate total area for this y-interval
+ area += query * dy;
+ }
+
+ // Update the active events list
+ match event.kind {
+ OcclusionEventKind::Begin => {
+ self.active.push(event.x_range.clone());
+ }
+ OcclusionEventKind::End => {
+ let index = self.active.iter().position(|i| *i == event.x_range).unwrap();
+ self.active.remove(index);
+ }
+ }
+
+ cur_y = event.y;
+ }
+
+ area
+ }
+}
diff --git a/gfx/wr/webrender/src/compositor/mod.rs b/gfx/wr/webrender/src/compositor/mod.rs
new file mode 100644
index 0000000000..e517f22719
--- /dev/null
+++ b/gfx/wr/webrender/src/compositor/mod.rs
@@ -0,0 +1,6 @@
+/* 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/. */
+
+#[cfg(feature = "sw_compositor")]
+pub mod sw_compositor;
diff --git a/gfx/wr/webrender/src/compositor/sw_compositor.rs b/gfx/wr/webrender/src/compositor/sw_compositor.rs
new file mode 100644
index 0000000000..e623870c86
--- /dev/null
+++ b/gfx/wr/webrender/src/compositor/sw_compositor.rs
@@ -0,0 +1,1538 @@
+/* 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 gleam::{gl, gl::Gl};
+use std::cell::{Cell, UnsafeCell};
+use std::collections::{hash_map::HashMap, VecDeque};
+use std::ops::{Deref, DerefMut, Range};
+use std::ptr;
+use std::sync::atomic::{AtomicBool, AtomicI8, AtomicPtr, AtomicU32, AtomicU8, Ordering};
+use std::sync::{Arc, Condvar, Mutex, MutexGuard};
+use std::thread;
+use crate::{
+ api::units::*, api::ColorDepth, api::ColorF, api::ExternalImageId, api::ImageRendering, api::YuvRangedColorSpace,
+ Compositor, CompositorCapabilities, CompositorSurfaceTransform, NativeSurfaceId, NativeSurfaceInfo, NativeTileId,
+ profiler, MappableCompositor, SWGLCompositeSurfaceInfo, WindowVisibility,
+ device::Device,
+};
+
+pub struct SwTile {
+ x: i32,
+ y: i32,
+ fbo_id: u32,
+ color_id: u32,
+ valid_rect: DeviceIntRect,
+ /// Composition of tiles must be ordered such that any tiles that may overlap
+ /// an invalidated tile in an earlier surface only get drawn after that tile
+ /// is actually updated. We store a count of the number of overlapping invalid
+ /// here, that gets decremented when the invalid tiles are finally updated so
+ /// that we know when it is finally safe to draw. Must use a Cell as we might
+ /// be analyzing multiple tiles and surfaces
+ overlaps: Cell<u32>,
+ /// Whether the tile's contents has been invalidated
+ invalid: Cell<bool>,
+ /// Graph node for job dependencies of this tile
+ graph_node: SwCompositeGraphNodeRef,
+}
+
+impl SwTile {
+ fn new(x: i32, y: i32) -> Self {
+ SwTile {
+ x,
+ y,
+ fbo_id: 0,
+ color_id: 0,
+ valid_rect: DeviceIntRect::zero(),
+ overlaps: Cell::new(0),
+ invalid: Cell::new(false),
+ graph_node: SwCompositeGraphNode::new(),
+ }
+ }
+
+ /// The offset of the tile in the local space of the surface before any
+ /// transform is applied.
+ fn origin(&self, surface: &SwSurface) -> DeviceIntPoint {
+ DeviceIntPoint::new(self.x * surface.tile_size.width, self.y * surface.tile_size.height)
+ }
+
+ /// The offset valid rect positioned within the local space of the surface
+ /// before any transform is applied.
+ fn local_bounds(&self, surface: &SwSurface) -> DeviceIntRect {
+ self.valid_rect.translate(self.origin(surface).to_vector())
+ }
+
+ /// Bounds used for determining overlap dependencies. This may either be the
+ /// full tile bounds or the actual valid rect, depending on whether the tile
+ /// is invalidated this frame. These bounds are more conservative as such and
+ /// may differ from the precise bounds used to actually composite the tile.
+ fn overlap_rect(
+ &self,
+ surface: &SwSurface,
+ transform: &CompositorSurfaceTransform,
+ clip_rect: &DeviceIntRect,
+ ) -> Option<DeviceIntRect> {
+ let bounds = self.local_bounds(surface);
+ let device_rect = transform.map_rect(&bounds.to_f32()).round_out();
+ Some(device_rect.intersection(&clip_rect.to_f32())?.to_i32())
+ }
+
+ /// Determine if the tile's bounds may overlap the dependency rect if it were
+ /// to be composited at the given position.
+ fn may_overlap(
+ &self,
+ surface: &SwSurface,
+ transform: &CompositorSurfaceTransform,
+ clip_rect: &DeviceIntRect,
+ dep_rect: &DeviceIntRect,
+ ) -> bool {
+ self.overlap_rect(surface, transform, clip_rect)
+ .map_or(false, |r| r.intersects(dep_rect))
+ }
+
+ /// Get valid source and destination rectangles for composition of the tile
+ /// within a surface, bounded by the clipping rectangle. May return None if
+ /// it falls outside of the clip rect.
+ fn composite_rects(
+ &self,
+ surface: &SwSurface,
+ transform: &CompositorSurfaceTransform,
+ clip_rect: &DeviceIntRect,
+ ) -> Option<(DeviceIntRect, DeviceIntRect, bool, bool)> {
+ // Offset the valid rect to the appropriate surface origin.
+ let valid = self.local_bounds(surface);
+ // The destination rect is the valid rect transformed and then clipped.
+ let dest_rect = transform.map_rect(&valid.to_f32()).round_out();
+ if !dest_rect.intersects(&clip_rect.to_f32()) {
+ return None;
+ }
+ // To get a valid source rect, we need to inverse transform the clipped destination rect to find out the effect
+ // of the clip rect in source-space. After this, we subtract off the source-space valid rect origin to get
+ // a source rect that is now relative to the surface origin rather than absolute.
+ let inv_transform = transform.inverse();
+ let src_rect = inv_transform
+ .map_rect(&dest_rect)
+ .round()
+ .translate(-valid.min.to_vector().to_f32());
+ // Ensure source and dest rects when transformed from Box2D to Rect formats will still fit in an i32.
+ // If p0=i32::MIN and p1=i32::MAX, then evaluating the size with p1-p0 will overflow an i32 and not
+ // be representable.
+ if src_rect.size().try_cast::<i32>().is_none() ||
+ dest_rect.size().try_cast::<i32>().is_none() {
+ return None;
+ }
+ let flip_x = transform.scale.x < 0.0;
+ let flip_y = transform.scale.y < 0.0;
+ Some((src_rect.try_cast()?, dest_rect.try_cast()?, flip_x, flip_y))
+ }
+}
+
+pub struct SwSurface {
+ tile_size: DeviceIntSize,
+ is_opaque: bool,
+ tiles: Vec<SwTile>,
+ /// An attached external image for this surface.
+ external_image: Option<ExternalImageId>,
+}
+
+impl SwSurface {
+ fn new(tile_size: DeviceIntSize, is_opaque: bool) -> Self {
+ SwSurface {
+ tile_size,
+ is_opaque,
+ tiles: Vec::new(),
+ external_image: None,
+ }
+ }
+
+ /// Conserative approximation of local bounds of the surface by combining
+ /// the local bounds of all enclosed tiles.
+ fn local_bounds(&self) -> DeviceIntRect {
+ let mut bounds = DeviceIntRect::zero();
+ for tile in &self.tiles {
+ bounds = bounds.union(&tile.local_bounds(self));
+ }
+ bounds
+ }
+
+ /// The transformed and clipped conservative device-space bounds of the
+ /// surface.
+ fn device_bounds(
+ &self,
+ transform: &CompositorSurfaceTransform,
+ clip_rect: &DeviceIntRect,
+ ) -> Option<DeviceIntRect> {
+ let bounds = self.local_bounds();
+ let device_rect = transform.map_rect(&bounds.to_f32()).round_out();
+ Some(device_rect.intersection(&clip_rect.to_f32())?.to_i32())
+ }
+}
+
+fn image_rendering_to_gl_filter(filter: ImageRendering) -> gl::GLenum {
+ match filter {
+ ImageRendering::Pixelated => gl::NEAREST,
+ ImageRendering::Auto | ImageRendering::CrispEdges => gl::LINEAR,
+ }
+}
+
+/// A source for a composite job which can either be a single BGRA locked SWGL
+/// resource or a collection of SWGL resources representing a YUV surface.
+#[derive(Clone)]
+enum SwCompositeSource {
+ BGRA(swgl::LockedResource),
+ YUV(
+ swgl::LockedResource,
+ swgl::LockedResource,
+ swgl::LockedResource,
+ YuvRangedColorSpace,
+ ColorDepth,
+ ),
+}
+
+/// Mark ExternalImage's renderer field as safe to send to SwComposite thread.
+unsafe impl Send for SwCompositeSource {}
+
+/// A tile composition job to be processed by the SwComposite thread.
+/// Stores relevant details about the tile and where to composite it.
+#[derive(Clone)]
+struct SwCompositeJob {
+ /// Locked texture that will be unlocked immediately following the job
+ locked_src: SwCompositeSource,
+ /// Locked framebuffer that may be shared among many jobs
+ locked_dst: swgl::LockedResource,
+ src_rect: DeviceIntRect,
+ dst_rect: DeviceIntRect,
+ clipped_dst: DeviceIntRect,
+ opaque: bool,
+ flip_x: bool,
+ flip_y: bool,
+ filter: ImageRendering,
+ /// The total number of bands for this job
+ num_bands: u8,
+}
+
+impl SwCompositeJob {
+ /// Process a composite job
+ fn process(&self, band_index: i32) {
+ // Bands are allocated in reverse order, but we want to process them in increasing order.
+ let num_bands = self.num_bands as i32;
+ let band_index = num_bands - 1 - band_index;
+ // Calculate the Y extents for the job's band, starting at the current index and spanning to
+ // the following index.
+ let band_offset = (self.clipped_dst.height() * band_index) / num_bands;
+ let band_height = (self.clipped_dst.height() * (band_index + 1)) / num_bands - band_offset;
+ // Create a rect that is the intersection of the band with the clipped dest
+ let band_clip = DeviceIntRect::from_origin_and_size(
+ DeviceIntPoint::new(self.clipped_dst.min.x, self.clipped_dst.min.y + band_offset),
+ DeviceIntSize::new(self.clipped_dst.width(), band_height),
+ );
+ match self.locked_src {
+ SwCompositeSource::BGRA(ref resource) => {
+ self.locked_dst.composite(
+ resource,
+ self.src_rect.min.x,
+ self.src_rect.min.y,
+ self.src_rect.width(),
+ self.src_rect.height(),
+ self.dst_rect.min.x,
+ self.dst_rect.min.y,
+ self.dst_rect.width(),
+ self.dst_rect.height(),
+ self.opaque,
+ self.flip_x,
+ self.flip_y,
+ image_rendering_to_gl_filter(self.filter),
+ band_clip.min.x,
+ band_clip.min.y,
+ band_clip.width(),
+ band_clip.height(),
+ );
+ }
+ SwCompositeSource::YUV(ref y, ref u, ref v, color_space, color_depth) => {
+ let swgl_color_space = match color_space {
+ YuvRangedColorSpace::Rec601Narrow => swgl::YuvRangedColorSpace::Rec601Narrow,
+ YuvRangedColorSpace::Rec601Full => swgl::YuvRangedColorSpace::Rec601Full,
+ YuvRangedColorSpace::Rec709Narrow => swgl::YuvRangedColorSpace::Rec709Narrow,
+ YuvRangedColorSpace::Rec709Full => swgl::YuvRangedColorSpace::Rec709Full,
+ YuvRangedColorSpace::Rec2020Narrow => swgl::YuvRangedColorSpace::Rec2020Narrow,
+ YuvRangedColorSpace::Rec2020Full => swgl::YuvRangedColorSpace::Rec2020Full,
+ YuvRangedColorSpace::GbrIdentity => swgl::YuvRangedColorSpace::GbrIdentity,
+ };
+ self.locked_dst.composite_yuv(
+ y,
+ u,
+ v,
+ swgl_color_space,
+ color_depth.bit_depth(),
+ self.src_rect.min.x,
+ self.src_rect.min.y,
+ self.src_rect.width(),
+ self.src_rect.height(),
+ self.dst_rect.min.x,
+ self.dst_rect.min.y,
+ self.dst_rect.width(),
+ self.dst_rect.height(),
+ self.flip_x,
+ self.flip_y,
+ band_clip.min.x,
+ band_clip.min.y,
+ band_clip.width(),
+ band_clip.height(),
+ );
+ }
+ }
+ }
+}
+
+/// A reference to a SwCompositeGraph node that can be passed from the render
+/// thread to the SwComposite thread. Consistency of mutation is ensured in
+/// SwCompositeGraphNode via use of Atomic operations that prevent more than
+/// one thread from mutating SwCompositeGraphNode at once. This avoids using
+/// messy and not-thread-safe RefCells or expensive Mutexes inside the graph
+/// node and at least signals to the compiler that potentially unsafe coercions
+/// are occurring.
+#[derive(Clone)]
+struct SwCompositeGraphNodeRef(Arc<UnsafeCell<SwCompositeGraphNode>>);
+
+impl SwCompositeGraphNodeRef {
+ fn new(graph_node: SwCompositeGraphNode) -> Self {
+ SwCompositeGraphNodeRef(Arc::new(UnsafeCell::new(graph_node)))
+ }
+
+ fn get(&self) -> &SwCompositeGraphNode {
+ unsafe { &*self.0.get() }
+ }
+
+ fn get_mut(&self) -> &mut SwCompositeGraphNode {
+ unsafe { &mut *self.0.get() }
+ }
+
+ fn get_ptr_mut(&self) -> *mut SwCompositeGraphNode {
+ self.0.get()
+ }
+}
+
+unsafe impl Send for SwCompositeGraphNodeRef {}
+
+impl Deref for SwCompositeGraphNodeRef {
+ type Target = SwCompositeGraphNode;
+
+ fn deref(&self) -> &Self::Target {
+ self.get()
+ }
+}
+
+impl DerefMut for SwCompositeGraphNodeRef {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ self.get_mut()
+ }
+}
+
+/// Dependency graph of composite jobs to be completed. Keeps a list of child jobs that are dependent on the completion of this job.
+/// Also keeps track of the number of parent jobs that this job is dependent upon before it can be processed. Once there are no more
+/// in-flight parent jobs that it depends on, the graph node is finally added to the job queue for processing.
+struct SwCompositeGraphNode {
+ /// Job to be queued for this graph node once ready.
+ job: Option<SwCompositeJob>,
+ /// The number of remaining bands associated with this job. When this is
+ /// non-zero and the node has no more parents left, then the node is being
+ /// actively used by the composite thread to process jobs. Once it hits
+ /// zero, the owning thread (which brought it to zero) can safely retire
+ /// the node as no other thread is using it.
+ remaining_bands: AtomicU8,
+ /// The number of bands that are available for processing.
+ available_bands: AtomicI8,
+ /// Count of parents this graph node depends on. While this is non-zero the
+ /// node must ensure that it is only being actively mutated by the render
+ /// thread and otherwise never being accessed by the render thread.
+ parents: AtomicU32,
+ /// Graph nodes of child jobs that are dependent on this job
+ children: Vec<SwCompositeGraphNodeRef>,
+}
+
+unsafe impl Sync for SwCompositeGraphNode {}
+
+impl SwCompositeGraphNode {
+ fn new() -> SwCompositeGraphNodeRef {
+ SwCompositeGraphNodeRef::new(SwCompositeGraphNode {
+ job: None,
+ remaining_bands: AtomicU8::new(0),
+ available_bands: AtomicI8::new(0),
+ parents: AtomicU32::new(0),
+ children: Vec::new(),
+ })
+ }
+
+ /// Reset the node's state for a new frame
+ fn reset(&mut self) {
+ self.job = None;
+ self.remaining_bands.store(0, Ordering::SeqCst);
+ self.available_bands.store(0, Ordering::SeqCst);
+ // Initialize parents to 1 as sentinel dependency for uninitialized job
+ // to avoid queuing unitialized job as unblocked child dependency.
+ self.parents.store(1, Ordering::SeqCst);
+ self.children.clear();
+ }
+
+ /// Add a dependent child node to dependency list. Update its parent count.
+ fn add_child(&mut self, child: SwCompositeGraphNodeRef) {
+ child.parents.fetch_add(1, Ordering::SeqCst);
+ self.children.push(child);
+ }
+
+ /// Install a job for this node. Return whether or not the job has any unprocessed parents
+ /// that would block immediate composition.
+ fn set_job(&mut self, job: SwCompositeJob, num_bands: u8) -> bool {
+ self.job = Some(job);
+ self.remaining_bands.store(num_bands, Ordering::SeqCst);
+ self.available_bands.store(num_bands as _, Ordering::SeqCst);
+ // Subtract off the sentinel parent dependency now that job is initialized and check
+ // whether there are any remaining parent dependencies to see if this job is ready.
+ self.parents.fetch_sub(1, Ordering::SeqCst) <= 1
+ }
+
+ /// Take an available band if possible. Also return whether there are no more bands left
+ /// so the caller may properly clean up after.
+ fn take_band(&self) -> (Option<i32>, bool) {
+ let available = self.available_bands.fetch_sub(1, Ordering::SeqCst);
+ if available > 0 {
+ (Some(available as i32 - 1), available == 1)
+ } else {
+ (None, true)
+ }
+ }
+
+ /// Try to take the job from this node for processing and then process it within the current band.
+ fn process_job(&self, band_index: i32) {
+ if let Some(ref job) = self.job {
+ job.process(band_index);
+ }
+ }
+
+ /// After processing a band, check all child dependencies and remove this parent from
+ /// their dependency counts. If applicable, queue the new child bands for composition.
+ fn unblock_children(&mut self, thread: &SwCompositeThread) {
+ if self.remaining_bands.fetch_sub(1, Ordering::SeqCst) > 1 {
+ return;
+ }
+ // Clear the job to release any locked resources.
+ self.job = None;
+ let mut lock = None;
+ for child in self.children.drain(..) {
+ // Remove the child's parent dependency on this node. If there are no more
+ // parent dependencies left, send the child job bands for composition.
+ if child.parents.fetch_sub(1, Ordering::SeqCst) <= 1 {
+ if lock.is_none() {
+ lock = Some(thread.lock());
+ }
+ thread.send_job(lock.as_mut().unwrap(), child);
+ }
+ }
+ }
+}
+
+/// The SwComposite thread processes a queue of composite jobs, also signaling
+/// via a condition when all available jobs have been processed, as tracked by
+/// the job count.
+struct SwCompositeThread {
+ /// Queue of available composite jobs
+ jobs: Mutex<SwCompositeJobQueue>,
+ /// Cache of the current job being processed. This maintains a pointer to
+ /// the contents of the SwCompositeGraphNodeRef, which is safe due to the
+ /// fact that SwCompositor maintains a strong reference to the contents
+ /// in an SwTile to keep it alive while this is in use.
+ current_job: AtomicPtr<SwCompositeGraphNode>,
+ /// Condition signaled when either there are jobs available to process or
+ /// there are no more jobs left to process. Otherwise stated, this signals
+ /// when the job queue transitions from an empty to non-empty state or from
+ /// a non-empty to empty state.
+ jobs_available: Condvar,
+ /// Whether all available jobs have been processed.
+ jobs_completed: AtomicBool,
+ /// Whether the main thread is waiting for for job completeion.
+ waiting_for_jobs: AtomicBool,
+ /// Whether the SwCompositor is shutting down
+ shutting_down: AtomicBool,
+}
+
+/// The SwCompositeThread struct is shared between the SwComposite thread
+/// and the rendering thread so that both ends can access the job queue.
+unsafe impl Sync for SwCompositeThread {}
+
+/// A FIFO queue of composite jobs to be processed.
+type SwCompositeJobQueue = VecDeque<SwCompositeGraphNodeRef>;
+
+/// Locked access to the composite job queue.
+type SwCompositeThreadLock<'a> = MutexGuard<'a, SwCompositeJobQueue>;
+
+impl SwCompositeThread {
+ /// Create the SwComposite thread. Requires a SWGL context in which
+ /// to do the composition.
+ fn new() -> Arc<SwCompositeThread> {
+ let info = Arc::new(SwCompositeThread {
+ jobs: Mutex::new(SwCompositeJobQueue::new()),
+ current_job: AtomicPtr::new(ptr::null_mut()),
+ jobs_available: Condvar::new(),
+ jobs_completed: AtomicBool::new(true),
+ waiting_for_jobs: AtomicBool::new(false),
+ shutting_down: AtomicBool::new(false),
+ });
+ let result = info.clone();
+ let thread_name = "SwComposite";
+ thread::Builder::new()
+ .name(thread_name.into())
+ // The composite thread only calls into SWGL to composite, and we
+ // have potentially many composite threads for different windows,
+ // so using the default stack size is excessive. A reasonably small
+ // stack size should be more than enough for SWGL and reduce memory
+ // overhead.
+ // Bug 1731569 - Need at least 36K to avoid problems with ASAN.
+ .stack_size(40 * 1024)
+ .spawn(move || {
+ profiler::register_thread(thread_name);
+ // Process any available jobs. This will return a non-Ok
+ // result when the job queue is dropped, causing the thread
+ // to eventually exit.
+ while let Some((job, band)) = info.take_job(true) {
+ info.process_job(job, band);
+ }
+ profiler::unregister_thread();
+ })
+ .expect("Failed creating SwComposite thread");
+ result
+ }
+
+ fn deinit(&self) {
+ // Signal that the thread needs to exit.
+ self.shutting_down.store(true, Ordering::SeqCst);
+ // Wake up the thread in case it is blocked waiting for new jobs
+ self.jobs_available.notify_all();
+ }
+
+ /// Process a job contained in a dependency graph node received from the job queue.
+ /// Any child dependencies will be unblocked as appropriate after processing. The
+ /// job count will be updated to reflect this.
+ fn process_job(&self, graph_node: &mut SwCompositeGraphNode, band: i32) {
+ // Do the actual processing of the job contained in this node.
+ graph_node.process_job(band);
+ // Unblock any child dependencies now that this job has been processed.
+ graph_node.unblock_children(self);
+ }
+
+ /// Queue a tile for composition by adding to the queue and increasing the job count.
+ fn queue_composite(
+ &self,
+ locked_src: SwCompositeSource,
+ locked_dst: swgl::LockedResource,
+ src_rect: DeviceIntRect,
+ dst_rect: DeviceIntRect,
+ clip_rect: DeviceIntRect,
+ opaque: bool,
+ flip_x: bool,
+ flip_y: bool,
+ filter: ImageRendering,
+ mut graph_node: SwCompositeGraphNodeRef,
+ job_queue: &mut SwCompositeJobQueue,
+ ) {
+ // For jobs that would span a sufficiently large destination rectangle, split
+ // it into multiple horizontal bands so that multiple threads can process them.
+ let clipped_dst = match dst_rect.intersection(&clip_rect) {
+ Some(clipped_dst) => clipped_dst,
+ None => return,
+ };
+
+ let num_bands = if clipped_dst.width() >= 64 && clipped_dst.height() >= 64 {
+ (clipped_dst.height() / 64).min(4) as u8
+ } else {
+ 1
+ };
+ let job = SwCompositeJob {
+ locked_src,
+ locked_dst,
+ src_rect,
+ dst_rect,
+ clipped_dst,
+ opaque,
+ flip_x,
+ flip_y,
+ filter,
+ num_bands,
+ };
+ if graph_node.set_job(job, num_bands) {
+ self.send_job(job_queue, graph_node);
+ }
+ }
+
+ fn prepare_for_composites(&self) {
+ // Initially, the job queue is empty. Trivially, this means we consider all
+ // jobs queued so far as completed.
+ self.jobs_completed.store(true, Ordering::SeqCst);
+ }
+
+ /// Lock the thread for access to the job queue.
+ fn lock(&self) -> SwCompositeThreadLock {
+ self.jobs.lock().unwrap()
+ }
+
+ /// Send a job to the composite thread by adding it to the job queue.
+ /// Signal that this job has been added in case the queue was empty and the
+ /// SwComposite thread is waiting for jobs.
+ fn send_job(&self, queue: &mut SwCompositeJobQueue, job: SwCompositeGraphNodeRef) {
+ if queue.is_empty() {
+ self.jobs_completed.store(false, Ordering::SeqCst);
+ self.jobs_available.notify_all();
+ }
+ queue.push_back(job);
+ }
+
+ /// Try to get a band of work from the currently cached job when available.
+ /// If there is a job, but it has no available bands left, null out the job
+ /// so that other threads do not bother checking the job.
+ fn try_take_job(&self) -> Option<(&mut SwCompositeGraphNode, i32)> {
+ let current_job_ptr = self.current_job.load(Ordering::SeqCst);
+ if let Some(current_job) = unsafe { current_job_ptr.as_mut() } {
+ let (band, done) = current_job.take_band();
+ if done {
+ let _ = self.current_job.compare_exchange(
+ current_job_ptr,
+ ptr::null_mut(),
+ Ordering::SeqCst,
+ Ordering::SeqCst,
+ );
+ }
+ if let Some(band) = band {
+ return Some((current_job, band));
+ }
+ }
+ return None;
+ }
+
+ /// Take a job from the queue. Optionally block waiting for jobs to become
+ /// available if this is called from the SwComposite thread.
+ fn take_job(&self, wait: bool) -> Option<(&mut SwCompositeGraphNode, i32)> {
+ // First try checking the cached job outside the scope of the mutex.
+ // For jobs that have multiple bands, this allows us to avoid having
+ // to lock the mutex multiple times to check the job for each band.
+ if let Some((job, band)) = self.try_take_job() {
+ return Some((job, band));
+ }
+ // Lock the job queue while checking for available jobs. The lock
+ // won't be held while the job is processed later outside of this
+ // function so that other threads can pull from the queue meanwhile.
+ let mut jobs = self.lock();
+ loop {
+ // While inside the mutex, check the cached job again to see if it
+ // has been updated.
+ if let Some((job, band)) = self.try_take_job() {
+ return Some((job, band));
+ }
+ // If no cached job was available, try to take a job from the queue
+ // and install it as the current job.
+ if let Some(job) = jobs.pop_front() {
+ self.current_job.store(job.get_ptr_mut(), Ordering::SeqCst);
+ continue;
+ }
+ // Otherwise, the job queue is currently empty. Depending on the
+ // job status, we may either wait for jobs to become available or exit.
+ if wait {
+ // For the SwComposite thread, if we arrive here, the job queue
+ // is empty. Signal that all available jobs have been completed.
+ self.jobs_completed.store(true, Ordering::SeqCst);
+ if self.waiting_for_jobs.load(Ordering::SeqCst) {
+ // Wake the main thread if it is waiting for a change in job status.
+ self.jobs_available.notify_all();
+ } else if self.shutting_down.load(Ordering::SeqCst) {
+ // If SwComposite thread needs to shut down, then exit and stop
+ // waiting for jobs.
+ return None;
+ }
+ } else {
+ // If all available jobs have been completed by the SwComposite
+ // thread, then the main thread no longer needs to wait for any
+ // new jobs to appear in the queue and should exit.
+ if self.jobs_completed.load(Ordering::SeqCst) {
+ return None;
+ }
+ // Otherwise, signal that the main thread is waiting for jobs.
+ self.waiting_for_jobs.store(true, Ordering::SeqCst);
+ }
+ // Wait until jobs are added before checking the job queue again.
+ jobs = self.jobs_available.wait(jobs).unwrap();
+ if !wait {
+ // The main thread is done waiting for jobs.
+ self.waiting_for_jobs.store(false, Ordering::SeqCst);
+ }
+ }
+ }
+
+ /// Wait for all queued composition jobs to be processed.
+ /// Instead of blocking on the SwComposite thread to complete all jobs,
+ /// this may steal some jobs and attempt to process them while waiting.
+ /// This may optionally process jobs synchronously. When normally doing
+ /// asynchronous processing, the graph dependencies are relied upon to
+ /// properly order the jobs, which makes it safe for the render thread
+ /// to steal jobs from the composite thread without violating those
+ /// dependencies. Synchronous processing just disables this job stealing
+ /// so that the composite thread always handles the jobs in the order
+ /// they were queued without having to rely upon possibly unavailable
+ /// graph dependencies.
+ fn wait_for_composites(&self, sync: bool) {
+ // If processing asynchronously, try to steal jobs from the composite
+ // thread if it is busy.
+ if !sync {
+ while let Some((job, band)) = self.take_job(false) {
+ self.process_job(job, band);
+ }
+ // Once there are no more jobs, just fall through to waiting
+ // synchronously for the composite thread to finish processing.
+ }
+ // If processing synchronously, just wait for the composite thread
+ // to complete processing any in-flight jobs, then bail.
+ let mut jobs = self.lock();
+ // Signal that the main thread may wait for job completion so that the
+ // SwComposite thread can wake it up if necessary.
+ self.waiting_for_jobs.store(true, Ordering::SeqCst);
+ // Wait for job completion to ensure there are no more in-flight jobs.
+ while !self.jobs_completed.load(Ordering::SeqCst) {
+ jobs = self.jobs_available.wait(jobs).unwrap();
+ }
+ // Done waiting for job completion.
+ self.waiting_for_jobs.store(false, Ordering::SeqCst);
+ }
+}
+
+/// Parameters describing how to composite a surface within a frame
+type FrameSurface = (
+ NativeSurfaceId,
+ CompositorSurfaceTransform,
+ DeviceIntRect,
+ ImageRendering,
+);
+
+/// Adapter for RenderCompositors to work with SWGL that shuttles between
+/// WebRender and the RenderCompositr via the Compositor API.
+pub struct SwCompositor {
+ gl: swgl::Context,
+ compositor: Box<dyn MappableCompositor>,
+ use_native_compositor: bool,
+ surfaces: HashMap<NativeSurfaceId, SwSurface>,
+ frame_surfaces: Vec<FrameSurface>,
+ /// Any surface added after we're already compositing (i.e. debug overlay)
+ /// needs to be processed after those frame surfaces. For simplicity we
+ /// store them in a separate queue that gets processed later.
+ late_surfaces: Vec<FrameSurface>,
+ /// Any composite surfaces that were locked during the frame and need to be
+ /// unlocked. frame_surfaces and late_surfaces may be pruned, so we can't
+ /// rely on them to contain all surfaces that were actually locked and must
+ /// track those separately.
+ composite_surfaces: HashMap<ExternalImageId, SWGLCompositeSurfaceInfo>,
+ cur_tile: NativeTileId,
+ /// The maximum tile size required for any of the allocated surfaces.
+ max_tile_size: DeviceIntSize,
+ /// Reuse the same depth texture amongst all tiles in all surfaces.
+ /// This depth texture must be big enough to accommodate the largest used
+ /// tile size for any surface. The maximum requested tile size is tracked
+ /// to ensure that this depth texture is at least that big.
+ /// This is initialized when the first surface is created and freed when
+ /// the last surface is destroyed, to ensure compositors with no surfaces
+ /// are not holding on to extra memory.
+ depth_id: Option<u32>,
+ /// Instance of the SwComposite thread, only created if we are not relying
+ /// on a native RenderCompositor.
+ composite_thread: Option<Arc<SwCompositeThread>>,
+ /// SWGL locked resource for sharing framebuffer with SwComposite thread
+ locked_framebuffer: Option<swgl::LockedResource>,
+ /// Whether we are currently in the middle of compositing
+ is_compositing: bool,
+}
+
+impl SwCompositor {
+ pub fn new(
+ gl: swgl::Context,
+ compositor: Box<dyn MappableCompositor>,
+ use_native_compositor: bool,
+ ) -> Self {
+ // Only create the SwComposite thread if we're not using a native render
+ // compositor. Thus, we are compositing into the main software framebuffer,
+ // which benefits from compositing asynchronously while updating tiles.
+ let composite_thread = if !use_native_compositor {
+ Some(SwCompositeThread::new())
+ } else {
+ None
+ };
+ SwCompositor {
+ gl,
+ compositor,
+ use_native_compositor,
+ surfaces: HashMap::new(),
+ frame_surfaces: Vec::new(),
+ late_surfaces: Vec::new(),
+ composite_surfaces: HashMap::new(),
+ cur_tile: NativeTileId {
+ surface_id: NativeSurfaceId(0),
+ x: 0,
+ y: 0,
+ },
+ max_tile_size: DeviceIntSize::zero(),
+ depth_id: None,
+ composite_thread,
+ locked_framebuffer: None,
+ is_compositing: false,
+ }
+ }
+
+ fn deinit_tile(&self, tile: &SwTile) {
+ self.gl.delete_framebuffers(&[tile.fbo_id]);
+ self.gl.delete_textures(&[tile.color_id]);
+ }
+
+ fn deinit_surface(&self, surface: &SwSurface) {
+ for tile in &surface.tiles {
+ self.deinit_tile(tile);
+ }
+ }
+
+ /// Attempt to occlude any queued surfaces with an opaque occluder rect. If
+ /// an existing surface is occluded, we attempt to restrict its clip rect
+ /// so long as it can remain a single clip rect. Existing frame surfaces
+ /// that are opaque will be fused if possible with the supplied occluder
+ /// rect to further try and restrict any underlying surfaces.
+ fn occlude_surfaces(&mut self) {
+ // Check if inner rect is fully included in outer rect
+ fn includes(outer: &Range<i32>, inner: &Range<i32>) -> bool {
+ outer.start <= inner.start && outer.end >= inner.end
+ }
+
+ // Check if outer range overlaps either the start or end of a range. If
+ // there is overlap, return the portion of the inner range remaining
+ // after the overlap has been removed.
+ fn overlaps(outer: &Range<i32>, inner: &Range<i32>) -> Option<Range<i32>> {
+ if outer.start <= inner.start && outer.end >= inner.start {
+ Some(outer.end..inner.end.max(outer.end))
+ } else if outer.start <= inner.end && outer.end >= inner.end {
+ Some(inner.start..outer.start.max(inner.start))
+ } else {
+ None
+ }
+ }
+
+ fn set_x_range(rect: &mut DeviceIntRect, range: &Range<i32>) {
+ rect.min.x = range.start;
+ rect.max.x = range.end;
+ }
+
+ fn set_y_range(rect: &mut DeviceIntRect, range: &Range<i32>) {
+ rect.min.y = range.start;
+ rect.max.y = range.end;
+ }
+
+ fn union(base: Range<i32>, extra: Range<i32>) -> Range<i32> {
+ base.start.min(extra.start)..base.end.max(extra.end)
+ }
+
+ // Before we can try to occlude any surfaces, we need to fix their clip rects to tightly
+ // bound the valid region. The clip rect might otherwise enclose an invalid area that
+ // can't fully occlude anything even if the surface is opaque.
+ for &mut (ref id, ref transform, ref mut clip_rect, _) in &mut self.frame_surfaces {
+ if let Some(surface) = self.surfaces.get(id) {
+ // Restrict the clip rect to fall within the valid region of the surface.
+ *clip_rect = surface.device_bounds(transform, clip_rect).unwrap_or_default();
+ }
+ }
+
+ // For each frame surface, treat it as an occluder if it is non-empty and opaque. Look
+ // through the preceding surfaces to see if any can be occluded.
+ for occlude_index in 0..self.frame_surfaces.len() {
+ let (ref occlude_id, _, ref occlude_rect, _) = self.frame_surfaces[occlude_index];
+ match self.surfaces.get(occlude_id) {
+ Some(occluder) if occluder.is_opaque && !occlude_rect.is_empty() => {}
+ _ => continue,
+ }
+
+ // Traverse the queued surfaces for this frame in the reverse order of
+ // how they are composited, or rather, in order of visibility. For each
+ // surface, check if the occluder can restrict the clip rect such that
+ // the clip rect can remain a single rect. If the clip rect overlaps
+ // the occluder on one axis interval while remaining fully included in
+ // the occluder's other axis interval, then we can chop down the edge
+ // of the clip rect on the overlapped axis. Further, if the surface is
+ // opaque and its clip rect exactly matches the occluder rect on one
+ // axis interval while overlapping on the other, fuse it with the
+ // occluder rect before considering any underlying surfaces.
+ let (mut occlude_x, mut occlude_y) = (occlude_rect.x_range(), occlude_rect.y_range());
+ for &mut (ref id, _, ref mut clip_rect, _) in self.frame_surfaces[..occlude_index].iter_mut().rev() {
+ if let Some(surface) = self.surfaces.get(id) {
+ let (clip_x, clip_y) = (clip_rect.x_range(), clip_rect.y_range());
+ if includes(&occlude_x, &clip_x) {
+ if let Some(visible) = overlaps(&occlude_y, &clip_y) {
+ set_y_range(clip_rect, &visible);
+ if surface.is_opaque && occlude_x == clip_x {
+ occlude_y = union(occlude_y, visible);
+ }
+ }
+ } else if includes(&occlude_y, &clip_y) {
+ if let Some(visible) = overlaps(&occlude_x, &clip_x) {
+ set_x_range(clip_rect, &visible);
+ if surface.is_opaque && occlude_y == clip_y {
+ occlude_x = union(occlude_x, visible);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /// Reset tile dependency state for a new frame.
+ fn reset_overlaps(&mut self) {
+ for surface in self.surfaces.values_mut() {
+ for tile in &mut surface.tiles {
+ tile.overlaps.set(0);
+ tile.invalid.set(false);
+ tile.graph_node.reset();
+ }
+ }
+ }
+
+ /// Computes an overlap count for a tile that falls within the given composite
+ /// destination rectangle. This requires checking all surfaces currently queued for
+ /// composition so far in this frame and seeing if they have any invalidated tiles
+ /// whose destination rectangles would also overlap the supplied tile. If so, then the
+ /// increment the overlap count to account for all such dependencies on invalid tiles.
+ /// Tiles with the same overlap count will still be drawn with a stable ordering in
+ /// the order the surfaces were queued, so it is safe to ignore other possible sources
+ /// of composition ordering dependencies, as the later queued tile will still be drawn
+ /// later than the blocking tiles within that stable order. We assume that the tile's
+ /// surface hasn't yet been added to the current frame list of surfaces to composite
+ /// so that we only process potential blockers from surfaces that would come earlier
+ /// in composition.
+ fn init_overlaps(
+ &self,
+ overlap_id: &NativeSurfaceId,
+ overlap_surface: &SwSurface,
+ overlap_tile: &SwTile,
+ overlap_transform: &CompositorSurfaceTransform,
+ overlap_clip_rect: &DeviceIntRect,
+ ) {
+ // Record an extra overlap for an invalid tile to track the tile's dependency
+ // on its own future update.
+ let mut overlaps = if overlap_tile.invalid.get() { 1 } else { 0 };
+
+ let overlap_rect = match overlap_tile.overlap_rect(overlap_surface, overlap_transform, overlap_clip_rect) {
+ Some(overlap_rect) => overlap_rect,
+ None => {
+ overlap_tile.overlaps.set(overlaps);
+ return;
+ }
+ };
+
+ for &(ref id, ref transform, ref clip_rect, _) in &self.frame_surfaces {
+ // We only want to consider surfaces that were added before the current one we're
+ // checking for overlaps. If we find that surface, then we're done.
+ if id == overlap_id {
+ break;
+ }
+ // If the surface's clip rect doesn't overlap the tile's rect,
+ // then there is no need to check any tiles within the surface.
+ if !overlap_rect.intersects(clip_rect) {
+ continue;
+ }
+ if let Some(surface) = self.surfaces.get(id) {
+ for tile in &surface.tiles {
+ // If there is a deferred tile that might overlap the destination rectangle,
+ // record the overlap.
+ if tile.may_overlap(surface, transform, clip_rect, &overlap_rect) {
+ if tile.overlaps.get() > 0 {
+ overlaps += 1;
+ }
+ // Regardless of whether this tile is deferred, if it has dependency
+ // overlaps, then record that it is potentially a dependency parent.
+ tile.graph_node.get_mut().add_child(overlap_tile.graph_node.clone());
+ }
+ }
+ }
+ }
+ if overlaps > 0 {
+ // Has a dependency on some invalid tiles, so need to defer composition.
+ overlap_tile.overlaps.set(overlaps);
+ }
+ }
+
+ /// Helper function that queues a composite job to the current locked framebuffer
+ fn queue_composite(
+ &self,
+ surface: &SwSurface,
+ transform: &CompositorSurfaceTransform,
+ clip_rect: &DeviceIntRect,
+ filter: ImageRendering,
+ tile: &SwTile,
+ job_queue: &mut SwCompositeJobQueue,
+ ) {
+ if let Some(ref composite_thread) = self.composite_thread {
+ if let Some((src_rect, dst_rect, flip_x, flip_y)) = tile.composite_rects(surface, transform, clip_rect) {
+ let source = if let Some(ref external_image) = surface.external_image {
+ // If the surface has an attached external image, lock any textures supplied in the descriptor.
+ match self.composite_surfaces.get(external_image) {
+ Some(ref info) => match info.yuv_planes {
+ 0 => match self.gl.lock_texture(info.textures[0]) {
+ Some(texture) => SwCompositeSource::BGRA(texture),
+ None => return,
+ },
+ 3 => match (
+ self.gl.lock_texture(info.textures[0]),
+ self.gl.lock_texture(info.textures[1]),
+ self.gl.lock_texture(info.textures[2]),
+ ) {
+ (Some(y_texture), Some(u_texture), Some(v_texture)) => SwCompositeSource::YUV(
+ y_texture,
+ u_texture,
+ v_texture,
+ info.color_space,
+ info.color_depth,
+ ),
+ _ => return,
+ },
+ _ => panic!("unsupported number of YUV planes: {}", info.yuv_planes),
+ },
+ None => return,
+ }
+ } else if let Some(texture) = self.gl.lock_texture(tile.color_id) {
+ // Lock the texture representing the picture cache tile.
+ SwCompositeSource::BGRA(texture)
+ } else {
+ return;
+ };
+ if let Some(ref framebuffer) = self.locked_framebuffer {
+ composite_thread.queue_composite(
+ source,
+ framebuffer.clone(),
+ src_rect,
+ dst_rect,
+ *clip_rect,
+ surface.is_opaque,
+ flip_x,
+ flip_y,
+ filter,
+ tile.graph_node.clone(),
+ job_queue,
+ );
+ }
+ }
+ }
+ }
+
+ /// Lock a surface with an attached external image for compositing.
+ fn try_lock_composite_surface(&mut self, device: &mut Device, id: &NativeSurfaceId) {
+ if let Some(surface) = self.surfaces.get_mut(id) {
+ if let Some(external_image) = surface.external_image {
+ assert!(!surface.tiles.is_empty());
+ let mut tile = &mut surface.tiles[0];
+ if let Some(info) = self.composite_surfaces.get(&external_image) {
+ tile.valid_rect = DeviceIntRect::from_size(info.size);
+ return;
+ }
+ // If the surface has an attached external image, attempt to lock the external image
+ // for compositing. Yields a descriptor of textures and data necessary for their
+ // interpretation on success.
+ let mut info = SWGLCompositeSurfaceInfo {
+ yuv_planes: 0,
+ textures: [0; 3],
+ color_space: YuvRangedColorSpace::GbrIdentity,
+ color_depth: ColorDepth::Color8,
+ size: DeviceIntSize::zero(),
+ };
+ if self.compositor.lock_composite_surface(device, self.gl.into(), external_image, &mut info) {
+ tile.valid_rect = DeviceIntRect::from_size(info.size);
+ self.composite_surfaces.insert(external_image, info);
+ } else {
+ tile.valid_rect = DeviceIntRect::zero();
+ }
+ }
+ }
+ }
+
+ /// Look for any attached external images that have been locked and then unlock them.
+ fn unlock_composite_surfaces(&mut self, device: &mut Device) {
+ for &external_image in self.composite_surfaces.keys() {
+ self.compositor.unlock_composite_surface(device, self.gl.into(), external_image);
+ }
+ self.composite_surfaces.clear();
+ }
+
+ /// Issue composites for any tiles that are no longer blocked following a tile update.
+ /// We process all surfaces and tiles in the order they were queued.
+ fn flush_composites(&self, tile_id: &NativeTileId, surface: &SwSurface, tile: &SwTile) {
+ let composite_thread = match &self.composite_thread {
+ Some(composite_thread) => composite_thread,
+ None => return,
+ };
+
+ // Look for the tile in the frame list and composite it if it has no dependencies.
+ let mut frame_surfaces = self
+ .frame_surfaces
+ .iter()
+ .skip_while(|&(ref id, _, _, _)| *id != tile_id.surface_id);
+ let (overlap_rect, mut lock) = match frame_surfaces.next() {
+ Some(&(_, ref transform, ref clip_rect, filter)) => {
+ // Remove invalid tile's update dependency.
+ if tile.invalid.get() {
+ tile.overlaps.set(tile.overlaps.get() - 1);
+ }
+ // If the tile still has overlaps, keep deferring it till later.
+ if tile.overlaps.get() > 0 {
+ return;
+ }
+ // Otherwise, the tile's dependencies are all resolved, so composite it.
+ let mut lock = composite_thread.lock();
+ self.queue_composite(surface, transform, clip_rect, filter, tile, &mut lock);
+ // Finally, get the tile's overlap rect used for tracking dependencies
+ match tile.overlap_rect(surface, transform, clip_rect) {
+ Some(overlap_rect) => (overlap_rect, lock),
+ None => return,
+ }
+ }
+ None => return,
+ };
+
+ // Accumulate rects whose dependencies have been satisfied from this update.
+ // Store the union of all these bounds to quickly reject unaffected tiles.
+ let mut flushed_bounds = overlap_rect;
+ let mut flushed_rects = vec![overlap_rect];
+
+ // Check surfaces following the update in the frame list and see if they would overlap it.
+ for &(ref id, ref transform, ref clip_rect, filter) in frame_surfaces {
+ // If the clip rect doesn't overlap the conservative bounds, we can skip the whole surface.
+ if !flushed_bounds.intersects(clip_rect) {
+ continue;
+ }
+ if let Some(surface) = self.surfaces.get(&id) {
+ // Search through the surface's tiles for any blocked on this update and queue jobs for them.
+ for tile in &surface.tiles {
+ let mut overlaps = tile.overlaps.get();
+ // Only check tiles that have existing unresolved dependencies
+ if overlaps == 0 {
+ continue;
+ }
+ // Get this tile's overlap rect for tracking dependencies
+ let overlap_rect = match tile.overlap_rect(surface, transform, clip_rect) {
+ Some(overlap_rect) => overlap_rect,
+ None => continue,
+ };
+ // Do a quick check to see if the tile overlaps the conservative bounds.
+ if !overlap_rect.intersects(&flushed_bounds) {
+ continue;
+ }
+ // Decrement the overlap count if this tile is dependent on any flushed rects.
+ for flushed_rect in &flushed_rects {
+ if overlap_rect.intersects(flushed_rect) {
+ overlaps -= 1;
+ }
+ }
+ if overlaps != tile.overlaps.get() {
+ // If the overlap count changed, this tile had a dependency on some flush rects.
+ // If the count hit zero, it is ready to composite.
+ tile.overlaps.set(overlaps);
+ if overlaps == 0 {
+ self.queue_composite(surface, transform, clip_rect, filter, tile, &mut lock);
+ // Record that the tile got flushed to update any downwind dependencies.
+ flushed_bounds = flushed_bounds.union(&overlap_rect);
+ flushed_rects.push(overlap_rect);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+impl Compositor for SwCompositor {
+ fn create_surface(
+ &mut self,
+ device: &mut Device,
+ id: NativeSurfaceId,
+ virtual_offset: DeviceIntPoint,
+ tile_size: DeviceIntSize,
+ is_opaque: bool,
+ ) {
+ if self.use_native_compositor {
+ self.compositor.create_surface(device, id, virtual_offset, tile_size, is_opaque);
+ }
+ self.max_tile_size = DeviceIntSize::new(
+ self.max_tile_size.width.max(tile_size.width),
+ self.max_tile_size.height.max(tile_size.height),
+ );
+ if self.depth_id.is_none() {
+ self.depth_id = Some(self.gl.gen_textures(1)[0]);
+ }
+ self.surfaces.insert(id, SwSurface::new(tile_size, is_opaque));
+ }
+
+ fn create_external_surface(&mut self, device: &mut Device, id: NativeSurfaceId, is_opaque: bool) {
+ if self.use_native_compositor {
+ self.compositor.create_external_surface(device, id, is_opaque);
+ }
+ self.surfaces
+ .insert(id, SwSurface::new(DeviceIntSize::zero(), is_opaque));
+ }
+
+ fn create_backdrop_surface(&mut self, _device: &mut Device, _id: NativeSurfaceId, _color: ColorF) {
+ unreachable!("Not implemented.")
+ }
+
+ fn destroy_surface(&mut self, device: &mut Device, id: NativeSurfaceId) {
+ if let Some(surface) = self.surfaces.remove(&id) {
+ self.deinit_surface(&surface);
+ }
+ if self.use_native_compositor {
+ self.compositor.destroy_surface(device, id);
+ }
+ if self.surfaces.is_empty() {
+ if let Some(depth_id) = self.depth_id.take() {
+ self.gl.delete_textures(&[depth_id]);
+ }
+ }
+ }
+
+ fn deinit(&mut self, device: &mut Device) {
+ if let Some(ref composite_thread) = self.composite_thread {
+ composite_thread.deinit();
+ }
+
+ for surface in self.surfaces.values() {
+ self.deinit_surface(surface);
+ }
+
+ if let Some(depth_id) = self.depth_id.take() {
+ self.gl.delete_textures(&[depth_id]);
+ }
+
+ if self.use_native_compositor {
+ self.compositor.deinit(device);
+ }
+ }
+
+ fn create_tile(&mut self, device: &mut Device, id: NativeTileId) {
+ if self.use_native_compositor {
+ self.compositor.create_tile(device, id);
+ }
+ if let Some(surface) = self.surfaces.get_mut(&id.surface_id) {
+ let mut tile = SwTile::new(id.x, id.y);
+ tile.color_id = self.gl.gen_textures(1)[0];
+ tile.fbo_id = self.gl.gen_framebuffers(1)[0];
+ let mut prev_fbo = [0];
+ unsafe {
+ self.gl.get_integer_v(gl::DRAW_FRAMEBUFFER_BINDING, &mut prev_fbo);
+ }
+ self.gl.bind_framebuffer(gl::DRAW_FRAMEBUFFER, tile.fbo_id);
+ self.gl.framebuffer_texture_2d(
+ gl::DRAW_FRAMEBUFFER,
+ gl::COLOR_ATTACHMENT0,
+ gl::TEXTURE_2D,
+ tile.color_id,
+ 0,
+ );
+ self.gl.framebuffer_texture_2d(
+ gl::DRAW_FRAMEBUFFER,
+ gl::DEPTH_ATTACHMENT,
+ gl::TEXTURE_2D,
+ self.depth_id.expect("depth texture should be initialized"),
+ 0,
+ );
+ self.gl.bind_framebuffer(gl::DRAW_FRAMEBUFFER, prev_fbo[0] as gl::GLuint);
+
+ surface.tiles.push(tile);
+ }
+ }
+
+ fn destroy_tile(&mut self, device: &mut Device, id: NativeTileId) {
+ if let Some(surface) = self.surfaces.get_mut(&id.surface_id) {
+ if let Some(idx) = surface.tiles.iter().position(|t| t.x == id.x && t.y == id.y) {
+ let tile = surface.tiles.remove(idx);
+ self.deinit_tile(&tile);
+ }
+ }
+ if self.use_native_compositor {
+ self.compositor.destroy_tile(device, id);
+ }
+ }
+
+ fn attach_external_image(&mut self, device: &mut Device, id: NativeSurfaceId, external_image: ExternalImageId) {
+ if self.use_native_compositor {
+ self.compositor.attach_external_image(device, id, external_image);
+ }
+ if let Some(surface) = self.surfaces.get_mut(&id) {
+ // Surfaces with attached external images have a single tile at the origin encompassing
+ // the entire surface.
+ assert!(surface.tile_size.is_empty());
+ surface.external_image = Some(external_image);
+ if surface.tiles.is_empty() {
+ surface.tiles.push(SwTile::new(0, 0));
+ }
+ }
+ }
+
+ fn invalidate_tile(&mut self, device: &mut Device, id: NativeTileId, valid_rect: DeviceIntRect) {
+ if self.use_native_compositor {
+ self.compositor.invalidate_tile(device, id, valid_rect);
+ }
+ if let Some(surface) = self.surfaces.get_mut(&id.surface_id) {
+ if let Some(tile) = surface.tiles.iter_mut().find(|t| t.x == id.x && t.y == id.y) {
+ tile.invalid.set(true);
+ tile.valid_rect = valid_rect;
+ }
+ }
+ }
+
+ fn bind(&mut self, device: &mut Device, id: NativeTileId, dirty_rect: DeviceIntRect, valid_rect: DeviceIntRect) -> NativeSurfaceInfo {
+ let mut surface_info = NativeSurfaceInfo {
+ origin: DeviceIntPoint::zero(),
+ fbo_id: 0,
+ };
+
+ self.cur_tile = id;
+
+ if let Some(surface) = self.surfaces.get_mut(&id.surface_id) {
+ if let Some(tile) = surface.tiles.iter_mut().find(|t| t.x == id.x && t.y == id.y) {
+ assert_eq!(tile.valid_rect, valid_rect);
+ if valid_rect.is_empty() {
+ return surface_info;
+ }
+
+ let mut stride = 0;
+ let mut buf = ptr::null_mut();
+ if self.use_native_compositor {
+ if let Some(tile_info) = self.compositor.map_tile(device, id, dirty_rect, valid_rect) {
+ stride = tile_info.stride;
+ buf = tile_info.data;
+ }
+ }
+ self.gl.set_texture_buffer(
+ tile.color_id,
+ gl::RGBA8,
+ valid_rect.width(),
+ valid_rect.height(),
+ stride,
+ buf,
+ surface.tile_size.width,
+ surface.tile_size.height,
+ );
+ // Reallocate the shared depth buffer to fit the valid rect, but within
+ // a buffer sized to actually fit at least the maximum possible tile size.
+ // The maximum tile size is supplied to avoid reallocation by ensuring the
+ // allocated buffer is actually big enough to accommodate the largest tile
+ // size requested by any used surface, even though supplied valid rect may
+ // actually be much smaller than this. This will only force a texture
+ // reallocation inside SWGL if the maximum tile size has grown since the
+ // last time it was supplied, instead simply reusing the buffer if the max
+ // tile size is not bigger than what was previously allocated.
+ self.gl.set_texture_buffer(
+ self.depth_id.expect("depth texture should be initialized"),
+ gl::DEPTH_COMPONENT,
+ valid_rect.width(),
+ valid_rect.height(),
+ 0,
+ ptr::null_mut(),
+ self.max_tile_size.width,
+ self.max_tile_size.height,
+ );
+ surface_info.fbo_id = tile.fbo_id;
+ surface_info.origin -= valid_rect.min.to_vector();
+ }
+ }
+
+ surface_info
+ }
+
+ fn unbind(&mut self, device: &mut Device) {
+ let id = self.cur_tile;
+ if let Some(surface) = self.surfaces.get(&id.surface_id) {
+ if let Some(tile) = surface.tiles.iter().find(|t| t.x == id.x && t.y == id.y) {
+ if tile.valid_rect.is_empty() {
+ // If we didn't actually render anything, then just queue any
+ // dependencies.
+ self.flush_composites(&id, surface, tile);
+ return;
+ }
+
+ // Force any delayed clears to be resolved.
+ self.gl.resolve_framebuffer(tile.fbo_id);
+
+ if self.use_native_compositor {
+ self.compositor.unmap_tile(device);
+ } else {
+ // If we're not relying on a native compositor, then composite
+ // any tiles that are dependent on this tile being updated but
+ // are otherwise ready to composite.
+ self.flush_composites(&id, surface, tile);
+ }
+ }
+ }
+ }
+
+ fn begin_frame(&mut self, device: &mut Device) {
+ self.reset_overlaps();
+
+ if self.use_native_compositor {
+ self.compositor.begin_frame(device);
+ }
+ }
+
+ fn add_surface(
+ &mut self,
+ device: &mut Device,
+ id: NativeSurfaceId,
+ transform: CompositorSurfaceTransform,
+ clip_rect: DeviceIntRect,
+ filter: ImageRendering,
+ ) {
+ if self.use_native_compositor {
+ self.compositor.add_surface(device, id, transform, clip_rect, filter);
+ }
+
+ if self.composite_thread.is_some() {
+ // If the surface has an attached external image, try to lock that now.
+ self.try_lock_composite_surface(device, &id);
+
+ // If we're already busy compositing, then add to the queue of late
+ // surfaces instead of trying to sort into the main frame queue.
+ // These late surfaces will not have any overlap tracking done for
+ // them and must be processed synchronously at the end of the frame.
+ if self.is_compositing {
+ self.late_surfaces.push((id, transform, clip_rect, filter));
+ return;
+ }
+ }
+
+ self.frame_surfaces.push((id, transform, clip_rect, filter));
+ }
+
+ /// Now that all the dependency graph nodes have been built, start queuing
+ /// composition jobs. Any surfaces that get added after this point in the
+ /// frame will not have overlap dependencies assigned and so must instead
+ /// be added to the late_surfaces queue to be processed at the end of the
+ /// frame.
+ fn start_compositing(&mut self, device: &mut Device, clear_color: ColorF, dirty_rects: &[DeviceIntRect], _opaque_rects: &[DeviceIntRect]) {
+ self.is_compositing = true;
+
+ // Opaque rects are currently only computed here, not by WR itself, so we
+ // ignore the passed parameter and forward our own version onto the native
+ // compositor.
+ let mut opaque_rects: Vec<DeviceIntRect> = Vec::new();
+ for &(ref id, ref transform, ref clip_rect, _filter) in &self.frame_surfaces {
+ if let Some(surface) = self.surfaces.get(id) {
+ if !surface.is_opaque {
+ continue;
+ }
+
+ for tile in &surface.tiles {
+ if let Some(rect) = tile.overlap_rect(surface, transform, clip_rect) {
+ opaque_rects.push(rect);
+ }
+ }
+ }
+ }
+
+ self.compositor.start_compositing(device, clear_color, dirty_rects, &opaque_rects);
+
+ if let Some(dirty_rect) = dirty_rects
+ .iter()
+ .fold(DeviceIntRect::zero(), |acc, dirty_rect| acc.union(dirty_rect))
+ .to_non_empty()
+ {
+ // Factor dirty rect into surface clip rects
+ for &mut (_, _, ref mut clip_rect, _) in &mut self.frame_surfaces {
+ *clip_rect = clip_rect.intersection(&dirty_rect).unwrap_or_default();
+ }
+ }
+
+ self.occlude_surfaces();
+
+ // Discard surfaces that are entirely clipped out
+ self.frame_surfaces
+ .retain(|&(_, _, ref clip_rect, _)| !clip_rect.is_empty());
+
+ if let Some(ref composite_thread) = self.composite_thread {
+ // Compute overlap dependencies for surfaces.
+ for &(ref id, ref transform, ref clip_rect, _filter) in &self.frame_surfaces {
+ if let Some(surface) = self.surfaces.get(id) {
+ for tile in &surface.tiles {
+ self.init_overlaps(id, surface, tile, transform, clip_rect);
+ }
+ }
+ }
+
+ self.locked_framebuffer = self.gl.lock_framebuffer(0);
+
+ composite_thread.prepare_for_composites();
+
+ // Issue any initial composite jobs for the SwComposite thread.
+ let mut lock = composite_thread.lock();
+ for &(ref id, ref transform, ref clip_rect, filter) in &self.frame_surfaces {
+ if let Some(surface) = self.surfaces.get(id) {
+ for tile in &surface.tiles {
+ if tile.overlaps.get() == 0 {
+ // Not dependent on any tiles, so go ahead and composite now.
+ self.queue_composite(surface, transform, clip_rect, filter, tile, &mut lock);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fn end_frame(&mut self, device: &mut Device,) {
+ self.is_compositing = false;
+
+ if self.use_native_compositor {
+ self.compositor.end_frame(device);
+ } else if let Some(ref composite_thread) = self.composite_thread {
+ // Need to wait for the SwComposite thread to finish any queued jobs.
+ composite_thread.wait_for_composites(false);
+
+ if !self.late_surfaces.is_empty() {
+ // All of the main frame surface have been processed by now. But if there
+ // are any late surfaces, we need to kick off a new synchronous composite
+ // phase. These late surfaces don't have any overlap/dependency tracking,
+ // so we just queue them directly and wait synchronously for the composite
+ // thread to process them in order.
+ composite_thread.prepare_for_composites();
+ {
+ let mut lock = composite_thread.lock();
+ for &(ref id, ref transform, ref clip_rect, filter) in &self.late_surfaces {
+ if let Some(surface) = self.surfaces.get(id) {
+ for tile in &surface.tiles {
+ self.queue_composite(surface, transform, clip_rect, filter, tile, &mut lock);
+ }
+ }
+ }
+ }
+ composite_thread.wait_for_composites(true);
+ }
+
+ self.locked_framebuffer = None;
+
+ self.unlock_composite_surfaces(device);
+ }
+
+ self.frame_surfaces.clear();
+ self.late_surfaces.clear();
+
+ self.reset_overlaps();
+ }
+
+ fn enable_native_compositor(&mut self, device: &mut Device, enable: bool) {
+ // TODO: The SwComposite thread is not properly instantiated if this is
+ // ever actually toggled.
+ assert_eq!(self.use_native_compositor, enable);
+ self.compositor.enable_native_compositor(device, enable);
+ self.use_native_compositor = enable;
+ }
+
+ fn get_capabilities(&self, device: &mut Device) -> CompositorCapabilities {
+ self.compositor.get_capabilities(device)
+ }
+
+ fn get_window_visibility(&self, device: &mut Device) -> WindowVisibility {
+ self.compositor.get_window_visibility(device)
+ }
+}
diff --git a/gfx/wr/webrender/src/debug_colors.rs b/gfx/wr/webrender/src/debug_colors.rs
new file mode 100644
index 0000000000..4ce8887126
--- /dev/null
+++ b/gfx/wr/webrender/src/debug_colors.rs
@@ -0,0 +1,159 @@
+/* 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/. */
+
+#![allow(dead_code)]
+#![cfg_attr(feature = "cargo-clippy", allow(clippy::excessive_precision))]
+
+use api::ColorF;
+
+// A subset of the standard CSS colors, useful for defining GPU tag colors etc.
+
+pub const INDIGO: ColorF = ColorF { r: 0.294117647059, g: 0.0, b: 0.509803921569, a: 1.0 };
+pub const GOLD: ColorF = ColorF { r: 1.0, g: 0.843137254902, b: 0.0, a: 1.0 };
+pub const FIREBRICK: ColorF = ColorF { r: 0.698039215686, g: 0.133333333333, b: 0.133333333333, a: 1.0 };
+pub const INDIANRED: ColorF = ColorF { r: 0.803921568627, g: 0.360784313725, b: 0.360784313725, a: 1.0 };
+pub const YELLOW: ColorF = ColorF { r: 1.0, g: 1.0, b: 0.0, a: 1.0 };
+pub const DARKOLIVEGREEN: ColorF = ColorF { r: 0.333333333333, g: 0.419607843137, b: 0.18431372549, a: 1.0 };
+pub const DARKSEAGREEN: ColorF = ColorF { r: 0.560784313725, g: 0.737254901961, b: 0.560784313725, a: 1.0 };
+pub const SLATEGREY: ColorF = ColorF { r: 0.439215686275, g: 0.501960784314, b: 0.564705882353, a: 1.0 };
+pub const DARKSLATEGREY: ColorF = ColorF { r: 0.18431372549, g: 0.309803921569, b: 0.309803921569, a: 1.0 };
+pub const MEDIUMVIOLETRED: ColorF = ColorF { r: 0.780392156863, g: 0.0823529411765, b: 0.521568627451, a: 1.0 };
+pub const MEDIUMORCHID: ColorF = ColorF { r: 0.729411764706, g: 0.333333333333, b: 0.827450980392, a: 1.0 };
+pub const CHARTREUSE: ColorF = ColorF { r: 0.498039215686, g: 1.0, b: 0.0, a: 1.0 };
+pub const MEDIUMSLATEBLUE: ColorF = ColorF { r: 0.482352941176, g: 0.407843137255, b: 0.933333333333, a: 1.0 };
+pub const BLACK: ColorF = ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 };
+pub const SPRINGGREEN: ColorF = ColorF { r: 0.0, g: 1.0, b: 0.498039215686, a: 1.0 };
+pub const CRIMSON: ColorF = ColorF { r: 0.862745098039, g: 0.078431372549, b: 0.235294117647, a: 1.0 };
+pub const LIGHTSALMON: ColorF = ColorF { r: 1.0, g: 0.627450980392, b: 0.478431372549, a: 1.0 };
+pub const BROWN: ColorF = ColorF { r: 0.647058823529, g: 0.164705882353, b: 0.164705882353, a: 1.0 };
+pub const TURQUOISE: ColorF = ColorF { r: 0.250980392157, g: 0.878431372549, b: 0.81568627451, a: 1.0 };
+pub const OLIVEDRAB: ColorF = ColorF { r: 0.419607843137, g: 0.556862745098, b: 0.137254901961, a: 1.0 };
+pub const CYAN: ColorF = ColorF { r: 0.0, g: 1.0, b: 1.0, a: 1.0 };
+pub const SILVER: ColorF = ColorF { r: 0.752941176471, g: 0.752941176471, b: 0.752941176471, a: 1.0 };
+pub const SKYBLUE: ColorF = ColorF { r: 0.529411764706, g: 0.807843137255, b: 0.921568627451, a: 1.0 };
+pub const GRAY: ColorF = ColorF { r: 0.501960784314, g: 0.501960784314, b: 0.501960784314, a: 1.0 };
+pub const DARKTURQUOISE: ColorF = ColorF { r: 0.0, g: 0.807843137255, b: 0.819607843137, a: 1.0 };
+pub const GOLDENROD: ColorF = ColorF { r: 0.854901960784, g: 0.647058823529, b: 0.125490196078, a: 1.0 };
+pub const DARKGREEN: ColorF = ColorF { r: 0.0, g: 0.392156862745, b: 0.0, a: 1.0 };
+pub const DARKVIOLET: ColorF = ColorF { r: 0.580392156863, g: 0.0, b: 0.827450980392, a: 1.0 };
+pub const DARKGRAY: ColorF = ColorF { r: 0.662745098039, g: 0.662745098039, b: 0.662745098039, a: 1.0 };
+pub const LIGHTPINK: ColorF = ColorF { r: 1.0, g: 0.713725490196, b: 0.756862745098, a: 1.0 };
+pub const TEAL: ColorF = ColorF { r: 0.0, g: 0.501960784314, b: 0.501960784314, a: 1.0 };
+pub const DARKMAGENTA: ColorF = ColorF { r: 0.545098039216, g: 0.0, b: 0.545098039216, a: 1.0 };
+pub const LIGHTGOLDENRODYELLOW: ColorF = ColorF { r: 0.980392156863, g: 0.980392156863, b: 0.823529411765, a: 1.0 };
+pub const LAVENDER: ColorF = ColorF { r: 0.901960784314, g: 0.901960784314, b: 0.980392156863, a: 1.0 };
+pub const YELLOWGREEN: ColorF = ColorF { r: 0.603921568627, g: 0.803921568627, b: 0.196078431373, a: 1.0 };
+pub const THISTLE: ColorF = ColorF { r: 0.847058823529, g: 0.749019607843, b: 0.847058823529, a: 1.0 };
+pub const VIOLET: ColorF = ColorF { r: 0.933333333333, g: 0.509803921569, b: 0.933333333333, a: 1.0 };
+pub const NAVY: ColorF = ColorF { r: 0.0, g: 0.0, b: 0.501960784314, a: 1.0 };
+pub const DIMGREY: ColorF = ColorF { r: 0.411764705882, g: 0.411764705882, b: 0.411764705882, a: 1.0 };
+pub const ORCHID: ColorF = ColorF { r: 0.854901960784, g: 0.439215686275, b: 0.839215686275, a: 1.0 };
+pub const BLUE: ColorF = ColorF { r: 0.0, g: 0.0, b: 1.0, a: 1.0 };
+pub const GHOSTWHITE: ColorF = ColorF { r: 0.972549019608, g: 0.972549019608, b: 1.0, a: 1.0 };
+pub const HONEYDEW: ColorF = ColorF { r: 0.941176470588, g: 1.0, b: 0.941176470588, a: 1.0 };
+pub const CORNFLOWERBLUE: ColorF = ColorF { r: 0.392156862745, g: 0.58431372549, b: 0.929411764706, a: 1.0 };
+pub const DARKBLUE: ColorF = ColorF { r: 0.0, g: 0.0, b: 0.545098039216, a: 1.0 };
+pub const DARKKHAKI: ColorF = ColorF { r: 0.741176470588, g: 0.717647058824, b: 0.419607843137, a: 1.0 };
+pub const MEDIUMPURPLE: ColorF = ColorF { r: 0.576470588235, g: 0.439215686275, b: 0.858823529412, a: 1.0 };
+pub const CORNSILK: ColorF = ColorF { r: 1.0, g: 0.972549019608, b: 0.862745098039, a: 1.0 };
+pub const RED: ColorF = ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 };
+pub const BISQUE: ColorF = ColorF { r: 1.0, g: 0.894117647059, b: 0.76862745098, a: 1.0 };
+pub const SLATEGRAY: ColorF = ColorF { r: 0.439215686275, g: 0.501960784314, b: 0.564705882353, a: 1.0 };
+pub const DARKCYAN: ColorF = ColorF { r: 0.0, g: 0.545098039216, b: 0.545098039216, a: 1.0 };
+pub const KHAKI: ColorF = ColorF { r: 0.941176470588, g: 0.901960784314, b: 0.549019607843, a: 1.0 };
+pub const WHEAT: ColorF = ColorF { r: 0.960784313725, g: 0.870588235294, b: 0.701960784314, a: 1.0 };
+pub const DEEPSKYBLUE: ColorF = ColorF { r: 0.0, g: 0.749019607843, b: 1.0, a: 1.0 };
+pub const REBECCAPURPLE: ColorF = ColorF { r: 0.4, g: 0.2, b: 0.6, a: 1.0 };
+pub const DARKRED: ColorF = ColorF { r: 0.545098039216, g: 0.0, b: 0.0, a: 1.0 };
+pub const STEELBLUE: ColorF = ColorF { r: 0.274509803922, g: 0.509803921569, b: 0.705882352941, a: 1.0 };
+pub const ALICEBLUE: ColorF = ColorF { r: 0.941176470588, g: 0.972549019608, b: 1.0, a: 1.0 };
+pub const LIGHTSLATEGREY: ColorF = ColorF { r: 0.466666666667, g: 0.533333333333, b: 0.6, a: 1.0 };
+pub const GAINSBORO: ColorF = ColorF { r: 0.862745098039, g: 0.862745098039, b: 0.862745098039, a: 1.0 };
+pub const MEDIUMTURQUOISE: ColorF = ColorF { r: 0.282352941176, g: 0.819607843137, b: 0.8, a: 1.0 };
+pub const FLORALWHITE: ColorF = ColorF { r: 1.0, g: 0.980392156863, b: 0.941176470588, a: 1.0 };
+pub const CORAL: ColorF = ColorF { r: 1.0, g: 0.498039215686, b: 0.313725490196, a: 1.0 };
+pub const PURPLE: ColorF = ColorF { r: 0.501960784314, g: 0.0, b: 0.501960784314, a: 1.0 };
+pub const LIGHTGREY: ColorF = ColorF { r: 0.827450980392, g: 0.827450980392, b: 0.827450980392, a: 1.0 };
+pub const LIGHTCYAN: ColorF = ColorF { r: 0.878431372549, g: 1.0, b: 1.0, a: 1.0 };
+pub const DARKSALMON: ColorF = ColorF { r: 0.913725490196, g: 0.588235294118, b: 0.478431372549, a: 1.0 };
+pub const BEIGE: ColorF = ColorF { r: 0.960784313725, g: 0.960784313725, b: 0.862745098039, a: 1.0 };
+pub const AZURE: ColorF = ColorF { r: 0.941176470588, g: 1.0, b: 1.0, a: 1.0 };
+pub const LIGHTSTEELBLUE: ColorF = ColorF { r: 0.690196078431, g: 0.76862745098, b: 0.870588235294, a: 1.0 };
+pub const OLDLACE: ColorF = ColorF { r: 0.992156862745, g: 0.960784313725, b: 0.901960784314, a: 1.0 };
+pub const GREENYELLOW: ColorF = ColorF { r: 0.678431372549, g: 1.0, b: 0.18431372549, a: 1.0 };
+pub const ROYALBLUE: ColorF = ColorF { r: 0.254901960784, g: 0.411764705882, b: 0.882352941176, a: 1.0 };
+pub const LIGHTSEAGREEN: ColorF = ColorF { r: 0.125490196078, g: 0.698039215686, b: 0.666666666667, a: 1.0 };
+pub const MISTYROSE: ColorF = ColorF { r: 1.0, g: 0.894117647059, b: 0.882352941176, a: 1.0 };
+pub const SIENNA: ColorF = ColorF { r: 0.627450980392, g: 0.321568627451, b: 0.176470588235, a: 1.0 };
+pub const LIGHTCORAL: ColorF = ColorF { r: 0.941176470588, g: 0.501960784314, b: 0.501960784314, a: 1.0 };
+pub const ORANGERED: ColorF = ColorF { r: 1.0, g: 0.270588235294, b: 0.0, a: 1.0 };
+pub const NAVAJOWHITE: ColorF = ColorF { r: 1.0, g: 0.870588235294, b: 0.678431372549, a: 1.0 };
+pub const LIME: ColorF = ColorF { r: 0.0, g: 1.0, b: 0.0, a: 1.0 };
+pub const PALEGREEN: ColorF = ColorF { r: 0.596078431373, g: 0.98431372549, b: 0.596078431373, a: 1.0 };
+pub const BURLYWOOD: ColorF = ColorF { r: 0.870588235294, g: 0.721568627451, b: 0.529411764706, a: 1.0 };
+pub const SEASHELL: ColorF = ColorF { r: 1.0, g: 0.960784313725, b: 0.933333333333, a: 1.0 };
+pub const MEDIUMSPRINGGREEN: ColorF = ColorF { r: 0.0, g: 0.980392156863, b: 0.603921568627, a: 1.0 };
+pub const FUCHSIA: ColorF = ColorF { r: 1.0, g: 0.0, b: 1.0, a: 1.0 };
+pub const PAPAYAWHIP: ColorF = ColorF { r: 1.0, g: 0.937254901961, b: 0.835294117647, a: 1.0 };
+pub const BLANCHEDALMOND: ColorF = ColorF { r: 1.0, g: 0.921568627451, b: 0.803921568627, a: 1.0 };
+pub const PERU: ColorF = ColorF { r: 0.803921568627, g: 0.521568627451, b: 0.247058823529, a: 1.0 };
+pub const AQUAMARINE: ColorF = ColorF { r: 0.498039215686, g: 1.0, b: 0.83137254902, a: 1.0 };
+pub const WHITE: ColorF = ColorF { r: 1.0, g: 1.0, b: 1.0, a: 1.0 };
+pub const DARKSLATEGRAY: ColorF = ColorF { r: 0.18431372549, g: 0.309803921569, b: 0.309803921569, a: 1.0 };
+pub const TOMATO: ColorF = ColorF { r: 1.0, g: 0.388235294118, b: 0.278431372549, a: 1.0 };
+pub const IVORY: ColorF = ColorF { r: 1.0, g: 1.0, b: 0.941176470588, a: 1.0 };
+pub const DODGERBLUE: ColorF = ColorF { r: 0.117647058824, g: 0.564705882353, b: 1.0, a: 1.0 };
+pub const LEMONCHIFFON: ColorF = ColorF { r: 1.0, g: 0.980392156863, b: 0.803921568627, a: 1.0 };
+pub const CHOCOLATE: ColorF = ColorF { r: 0.823529411765, g: 0.411764705882, b: 0.117647058824, a: 1.0 };
+pub const ORANGE: ColorF = ColorF { r: 1.0, g: 0.647058823529, b: 0.0, a: 1.0 };
+pub const FORESTGREEN: ColorF = ColorF { r: 0.133333333333, g: 0.545098039216, b: 0.133333333333, a: 1.0 };
+pub const DARKGREY: ColorF = ColorF { r: 0.662745098039, g: 0.662745098039, b: 0.662745098039, a: 1.0 };
+pub const OLIVE: ColorF = ColorF { r: 0.501960784314, g: 0.501960784314, b: 0.0, a: 1.0 };
+pub const MINTCREAM: ColorF = ColorF { r: 0.960784313725, g: 1.0, b: 0.980392156863, a: 1.0 };
+pub const ANTIQUEWHITE: ColorF = ColorF { r: 0.980392156863, g: 0.921568627451, b: 0.843137254902, a: 1.0 };
+pub const DARKORANGE: ColorF = ColorF { r: 1.0, g: 0.549019607843, b: 0.0, a: 1.0 };
+pub const CADETBLUE: ColorF = ColorF { r: 0.372549019608, g: 0.619607843137, b: 0.627450980392, a: 1.0 };
+pub const MOCCASIN: ColorF = ColorF { r: 1.0, g: 0.894117647059, b: 0.709803921569, a: 1.0 };
+pub const LIMEGREEN: ColorF = ColorF { r: 0.196078431373, g: 0.803921568627, b: 0.196078431373, a: 1.0 };
+pub const SADDLEBROWN: ColorF = ColorF { r: 0.545098039216, g: 0.270588235294, b: 0.0745098039216, a: 1.0 };
+pub const GREY: ColorF = ColorF { r: 0.501960784314, g: 0.501960784314, b: 0.501960784314, a: 1.0 };
+pub const DARKSLATEBLUE: ColorF = ColorF { r: 0.282352941176, g: 0.239215686275, b: 0.545098039216, a: 1.0 };
+pub const LIGHTSKYBLUE: ColorF = ColorF { r: 0.529411764706, g: 0.807843137255, b: 0.980392156863, a: 1.0 };
+pub const DEEPPINK: ColorF = ColorF { r: 1.0, g: 0.078431372549, b: 0.576470588235, a: 1.0 };
+pub const PLUM: ColorF = ColorF { r: 0.866666666667, g: 0.627450980392, b: 0.866666666667, a: 1.0 };
+pub const AQUA: ColorF = ColorF { r: 0.0, g: 1.0, b: 1.0, a: 1.0 };
+pub const DARKGOLDENROD: ColorF = ColorF { r: 0.721568627451, g: 0.525490196078, b: 0.043137254902, a: 1.0 };
+pub const MAROON: ColorF = ColorF { r: 0.501960784314, g: 0.0, b: 0.0, a: 1.0 };
+pub const SANDYBROWN: ColorF = ColorF { r: 0.956862745098, g: 0.643137254902, b: 0.376470588235, a: 1.0 };
+pub const MAGENTA: ColorF = ColorF { r: 1.0, g: 0.0, b: 1.0, a: 1.0 };
+pub const TAN: ColorF = ColorF { r: 0.823529411765, g: 0.705882352941, b: 0.549019607843, a: 1.0 };
+pub const ROSYBROWN: ColorF = ColorF { r: 0.737254901961, g: 0.560784313725, b: 0.560784313725, a: 1.0 };
+pub const PINK: ColorF = ColorF { r: 1.0, g: 0.752941176471, b: 0.796078431373, a: 1.0 };
+pub const LIGHTBLUE: ColorF = ColorF { r: 0.678431372549, g: 0.847058823529, b: 0.901960784314, a: 1.0 };
+pub const PALEVIOLETRED: ColorF = ColorF { r: 0.858823529412, g: 0.439215686275, b: 0.576470588235, a: 1.0 };
+pub const MEDIUMSEAGREEN: ColorF = ColorF { r: 0.235294117647, g: 0.701960784314, b: 0.443137254902, a: 1.0 };
+pub const SLATEBLUE: ColorF = ColorF { r: 0.41568627451, g: 0.352941176471, b: 0.803921568627, a: 1.0 };
+pub const DIMGRAY: ColorF = ColorF { r: 0.411764705882, g: 0.411764705882, b: 0.411764705882, a: 1.0 };
+pub const POWDERBLUE: ColorF = ColorF { r: 0.690196078431, g: 0.878431372549, b: 0.901960784314, a: 1.0 };
+pub const SEAGREEN: ColorF = ColorF { r: 0.180392156863, g: 0.545098039216, b: 0.341176470588, a: 1.0 };
+pub const SNOW: ColorF = ColorF { r: 1.0, g: 0.980392156863, b: 0.980392156863, a: 1.0 };
+pub const MEDIUMBLUE: ColorF = ColorF { r: 0.0, g: 0.0, b: 0.803921568627, a: 1.0 };
+pub const MIDNIGHTBLUE: ColorF = ColorF { r: 0.0980392156863, g: 0.0980392156863, b: 0.439215686275, a: 1.0 };
+pub const PALETURQUOISE: ColorF = ColorF { r: 0.686274509804, g: 0.933333333333, b: 0.933333333333, a: 1.0 };
+pub const PALEGOLDENROD: ColorF = ColorF { r: 0.933333333333, g: 0.909803921569, b: 0.666666666667, a: 1.0 };
+pub const WHITESMOKE: ColorF = ColorF { r: 0.960784313725, g: 0.960784313725, b: 0.960784313725, a: 1.0 };
+pub const DARKORCHID: ColorF = ColorF { r: 0.6, g: 0.196078431373, b: 0.8, a: 1.0 };
+pub const SALMON: ColorF = ColorF { r: 0.980392156863, g: 0.501960784314, b: 0.447058823529, a: 1.0 };
+pub const LIGHTSLATEGRAY: ColorF = ColorF { r: 0.466666666667, g: 0.533333333333, b: 0.6, a: 1.0 };
+pub const LAWNGREEN: ColorF = ColorF { r: 0.486274509804, g: 0.988235294118, b: 0.0, a: 1.0 };
+pub const LIGHTGREEN: ColorF = ColorF { r: 0.564705882353, g: 0.933333333333, b: 0.564705882353, a: 1.0 };
+pub const LIGHTGRAY: ColorF = ColorF { r: 0.827450980392, g: 0.827450980392, b: 0.827450980392, a: 1.0 };
+pub const HOTPINK: ColorF = ColorF { r: 1.0, g: 0.411764705882, b: 0.705882352941, a: 1.0 };
+pub const LIGHTYELLOW: ColorF = ColorF { r: 1.0, g: 1.0, b: 0.878431372549, a: 1.0 };
+pub const LAVENDERBLUSH: ColorF = ColorF { r: 1.0, g: 0.941176470588, b: 0.960784313725, a: 1.0 };
+pub const LINEN: ColorF = ColorF { r: 0.980392156863, g: 0.941176470588, b: 0.901960784314, a: 1.0 };
+pub const MEDIUMAQUAMARINE: ColorF = ColorF { r: 0.4, g: 0.803921568627, b: 0.666666666667, a: 1.0 };
+pub const GREEN: ColorF = ColorF { r: 0.0, g: 0.501960784314, b: 0.0, a: 1.0 };
+pub const BLUEVIOLET: ColorF = ColorF { r: 0.541176470588, g: 0.16862745098, b: 0.886274509804, a: 1.0 };
+pub const PEACHPUFF: ColorF = ColorF { r: 1.0, g: 0.854901960784, b: 0.725490196078, a: 1.0 };
diff --git a/gfx/wr/webrender/src/debug_font_data.rs b/gfx/wr/webrender/src/debug_font_data.rs
new file mode 100644
index 0000000000..a891bf0d38
--- /dev/null
+++ b/gfx/wr/webrender/src/debug_font_data.rs
@@ -0,0 +1,1914 @@
+/* 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/. */
+
+#[derive(Debug)]
+pub struct BakedGlyph {
+ pub x0: u32,
+ pub y0: u32,
+ pub x1: u32,
+ pub y1: u32,
+ pub xo: f32,
+ pub yo: f32,
+ pub xa: f32,
+}
+
+pub const FIRST_GLYPH_INDEX: i32 = 32;
+pub const BMP_WIDTH: i32 = 128;
+pub const BMP_HEIGHT: i32 = 128;
+pub const FONT_SIZE: i32 = 19;
+
+pub const GLYPHS: [BakedGlyph; 96] = [
+ BakedGlyph {
+ x0: 1,
+ y0: 1,
+ x1: 1,
+ y1: 1,
+ xo: 0.000000,
+ yo: 0.000000,
+ xa: 3.864407,
+ },
+ BakedGlyph {
+ x0: 2,
+ y0: 1,
+ x1: 5,
+ y1: 14,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 4.644068,
+ },
+ BakedGlyph {
+ x0: 6,
+ y0: 1,
+ x1: 11,
+ y1: 6,
+ xo: 1.000000,
+ yo: -13.000000,
+ xa: 6.644068,
+ },
+ BakedGlyph {
+ x0: 12,
+ y0: 1,
+ x1: 23,
+ y1: 13,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 11.067797,
+ },
+ BakedGlyph {
+ x0: 24,
+ y0: 1,
+ x1: 32,
+ y1: 17,
+ xo: 1.000000,
+ yo: -14.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 33,
+ y0: 1,
+ x1: 46,
+ y1: 14,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 14.084745,
+ },
+ BakedGlyph {
+ x0: 47,
+ y0: 1,
+ x1: 58,
+ y1: 14,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 10.983051,
+ },
+ BakedGlyph {
+ x0: 59,
+ y0: 1,
+ x1: 61,
+ y1: 6,
+ xo: 1.000000,
+ yo: -13.000000,
+ xa: 4.067797,
+ },
+ BakedGlyph {
+ x0: 62,
+ y0: 1,
+ x1: 67,
+ y1: 19,
+ xo: 1.000000,
+ yo: -14.000000,
+ xa: 5.254237,
+ },
+ BakedGlyph {
+ x0: 68,
+ y0: 1,
+ x1: 72,
+ y1: 19,
+ xo: 0.000000,
+ yo: -14.000000,
+ xa: 5.254237,
+ },
+ BakedGlyph {
+ x0: 73,
+ y0: 1,
+ x1: 81,
+ y1: 8,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 8.000000,
+ },
+ BakedGlyph {
+ x0: 82,
+ y0: 1,
+ x1: 91,
+ y1: 11,
+ xo: 0.000000,
+ yo: -10.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 92,
+ y0: 1,
+ x1: 95,
+ y1: 6,
+ xo: 0.000000,
+ yo: -2.000000,
+ xa: 4.169492,
+ },
+ BakedGlyph {
+ x0: 96,
+ y0: 1,
+ x1: 101,
+ y1: 3,
+ xo: 0.000000,
+ yo: -6.000000,
+ xa: 4.779661,
+ },
+ BakedGlyph {
+ x0: 102,
+ y0: 1,
+ x1: 105,
+ y1: 4,
+ xo: 1.000000,
+ yo: -2.000000,
+ xa: 4.169492,
+ },
+ BakedGlyph {
+ x0: 106,
+ y0: 1,
+ x1: 114,
+ y1: 19,
+ xo: -1.000000,
+ yo: -14.000000,
+ xa: 6.084746,
+ },
+ BakedGlyph {
+ x0: 115,
+ y0: 1,
+ x1: 123,
+ y1: 14,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 1,
+ y0: 20,
+ x1: 6,
+ y1: 32,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 7,
+ y0: 20,
+ x1: 15,
+ y1: 32,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 16,
+ y0: 20,
+ x1: 24,
+ y1: 33,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 25,
+ y0: 20,
+ x1: 34,
+ y1: 32,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 35,
+ y0: 20,
+ x1: 43,
+ y1: 33,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 44,
+ y0: 20,
+ x1: 52,
+ y1: 33,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 53,
+ y0: 20,
+ x1: 61,
+ y1: 32,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 62,
+ y0: 20,
+ x1: 70,
+ y1: 33,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 71,
+ y0: 20,
+ x1: 79,
+ y1: 33,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 80,
+ y0: 20,
+ x1: 83,
+ y1: 30,
+ xo: 1.000000,
+ yo: -9.000000,
+ xa: 4.169492,
+ },
+ BakedGlyph {
+ x0: 84,
+ y0: 20,
+ x1: 88,
+ y1: 32,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 4.169492,
+ },
+ BakedGlyph {
+ x0: 89,
+ y0: 20,
+ x1: 98,
+ y1: 28,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 99,
+ y0: 20,
+ x1: 108,
+ y1: 26,
+ xo: 0.000000,
+ yo: -8.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 109,
+ y0: 20,
+ x1: 118,
+ y1: 28,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 119,
+ y0: 20,
+ x1: 125,
+ y1: 33,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 6.440678,
+ },
+ BakedGlyph {
+ x0: 1,
+ y0: 34,
+ x1: 15,
+ y1: 49,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 15.932203,
+ },
+ BakedGlyph {
+ x0: 16,
+ y0: 34,
+ x1: 27,
+ y1: 46,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 10.864407,
+ },
+ BakedGlyph {
+ x0: 28,
+ y0: 34,
+ x1: 37,
+ y1: 47,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 10.677966,
+ },
+ BakedGlyph {
+ x0: 38,
+ y0: 34,
+ x1: 47,
+ y1: 47,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 10.322034,
+ },
+ BakedGlyph {
+ x0: 48,
+ y0: 34,
+ x1: 58,
+ y1: 47,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 11.898305,
+ },
+ BakedGlyph {
+ x0: 59,
+ y0: 34,
+ x1: 67,
+ y1: 46,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 9.406779,
+ },
+ BakedGlyph {
+ x0: 68,
+ y0: 34,
+ x1: 76,
+ y1: 46,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 8.813560,
+ },
+ BakedGlyph {
+ x0: 77,
+ y0: 34,
+ x1: 86,
+ y1: 47,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 11.152542,
+ },
+ BakedGlyph {
+ x0: 87,
+ y0: 34,
+ x1: 97,
+ y1: 46,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 11.728813,
+ },
+ BakedGlyph {
+ x0: 98,
+ y0: 34,
+ x1: 100,
+ y1: 46,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 4.203390,
+ },
+ BakedGlyph {
+ x0: 101,
+ y0: 34,
+ x1: 108,
+ y1: 47,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 8.254237,
+ },
+ BakedGlyph {
+ x0: 109,
+ y0: 34,
+ x1: 118,
+ y1: 46,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 10.152542,
+ },
+ BakedGlyph {
+ x0: 1,
+ y0: 50,
+ x1: 9,
+ y1: 62,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 8.508474,
+ },
+ BakedGlyph {
+ x0: 10,
+ y0: 50,
+ x1: 23,
+ y1: 62,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 14.661017,
+ },
+ BakedGlyph {
+ x0: 24,
+ y0: 50,
+ x1: 34,
+ y1: 62,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 12.016949,
+ },
+ BakedGlyph {
+ x0: 35,
+ y0: 50,
+ x1: 47,
+ y1: 63,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 13.118644,
+ },
+ BakedGlyph {
+ x0: 48,
+ y0: 50,
+ x1: 57,
+ y1: 62,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 10.033898,
+ },
+ BakedGlyph {
+ x0: 58,
+ y0: 50,
+ x1: 70,
+ y1: 66,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 13.118644,
+ },
+ BakedGlyph {
+ x0: 71,
+ y0: 50,
+ x1: 81,
+ y1: 62,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 10.474576,
+ },
+ BakedGlyph {
+ x0: 82,
+ y0: 50,
+ x1: 91,
+ y1: 63,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 8.762712,
+ },
+ BakedGlyph {
+ x0: 92,
+ y0: 50,
+ x1: 101,
+ y1: 62,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 9.288136,
+ },
+ BakedGlyph {
+ x0: 102,
+ y0: 50,
+ x1: 112,
+ y1: 63,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 11.525424,
+ },
+ BakedGlyph {
+ x0: 113,
+ y0: 50,
+ x1: 124,
+ y1: 62,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 10.576271,
+ },
+ BakedGlyph {
+ x0: 1,
+ y0: 67,
+ x1: 16,
+ y1: 79,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 15.610169,
+ },
+ BakedGlyph {
+ x0: 17,
+ y0: 67,
+ x1: 27,
+ y1: 79,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 10.305085,
+ },
+ BakedGlyph {
+ x0: 28,
+ y0: 67,
+ x1: 38,
+ y1: 79,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 9.644068,
+ },
+ BakedGlyph {
+ x0: 39,
+ y0: 67,
+ x1: 48,
+ y1: 79,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 9.491526,
+ },
+ BakedGlyph {
+ x0: 49,
+ y0: 67,
+ x1: 54,
+ y1: 85,
+ xo: 1.000000,
+ yo: -14.000000,
+ xa: 5.254237,
+ },
+ BakedGlyph {
+ x0: 55,
+ y0: 67,
+ x1: 63,
+ y1: 85,
+ xo: -1.000000,
+ yo: -14.000000,
+ xa: 6.084746,
+ },
+ BakedGlyph {
+ x0: 64,
+ y0: 67,
+ x1: 68,
+ y1: 85,
+ xo: 0.000000,
+ yo: -14.000000,
+ xa: 5.254237,
+ },
+ BakedGlyph {
+ x0: 69,
+ y0: 67,
+ x1: 77,
+ y1: 74,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 78,
+ y0: 67,
+ x1: 88,
+ y1: 69,
+ xo: -1.000000,
+ yo: 2.000000,
+ xa: 8.305085,
+ },
+ BakedGlyph {
+ x0: 89,
+ y0: 67,
+ x1: 93,
+ y1: 72,
+ xo: 1.000000,
+ yo: -14.000000,
+ xa: 6.372881,
+ },
+ BakedGlyph {
+ x0: 94,
+ y0: 67,
+ x1: 102,
+ y1: 77,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 8.627119,
+ },
+ BakedGlyph {
+ x0: 103,
+ y0: 67,
+ x1: 111,
+ y1: 82,
+ xo: 1.000000,
+ yo: -14.000000,
+ xa: 9.881356,
+ },
+ BakedGlyph {
+ x0: 112,
+ y0: 67,
+ x1: 120,
+ y1: 77,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 7.796610,
+ },
+ BakedGlyph {
+ x0: 1,
+ y0: 86,
+ x1: 10,
+ y1: 101,
+ xo: 0.000000,
+ yo: -14.000000,
+ xa: 9.881356,
+ },
+ BakedGlyph {
+ x0: 11,
+ y0: 86,
+ x1: 20,
+ y1: 96,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 9.288136,
+ },
+ BakedGlyph {
+ x0: 21,
+ y0: 86,
+ x1: 27,
+ y1: 100,
+ xo: 1.000000,
+ yo: -14.000000,
+ xa: 6.372881,
+ },
+ BakedGlyph {
+ x0: 28,
+ y0: 86,
+ x1: 37,
+ y1: 99,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 9.711864,
+ },
+ BakedGlyph {
+ x0: 38,
+ y0: 86,
+ x1: 46,
+ y1: 100,
+ xo: 1.000000,
+ yo: -14.000000,
+ xa: 9.644068,
+ },
+ BakedGlyph {
+ x0: 47,
+ y0: 86,
+ x1: 49,
+ y1: 99,
+ xo: 1.000000,
+ yo: -13.000000,
+ xa: 4.016949,
+ },
+ BakedGlyph {
+ x0: 50,
+ y0: 86,
+ x1: 55,
+ y1: 103,
+ xo: -2.000000,
+ yo: -13.000000,
+ xa: 4.016949,
+ },
+ BakedGlyph {
+ x0: 56,
+ y0: 86,
+ x1: 64,
+ y1: 100,
+ xo: 1.000000,
+ yo: -14.000000,
+ xa: 8.389831,
+ },
+ BakedGlyph {
+ x0: 65,
+ y0: 86,
+ x1: 68,
+ y1: 101,
+ xo: 1.000000,
+ yo: -14.000000,
+ xa: 4.322034,
+ },
+ BakedGlyph {
+ x0: 69,
+ y0: 86,
+ x1: 82,
+ y1: 95,
+ xo: 1.000000,
+ yo: -9.000000,
+ xa: 14.627119,
+ },
+ BakedGlyph {
+ x0: 83,
+ y0: 86,
+ x1: 91,
+ y1: 95,
+ xo: 1.000000,
+ yo: -9.000000,
+ xa: 9.644068,
+ },
+ BakedGlyph {
+ x0: 92,
+ y0: 86,
+ x1: 101,
+ y1: 96,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 9.864407,
+ },
+ BakedGlyph {
+ x0: 102,
+ y0: 86,
+ x1: 110,
+ y1: 99,
+ xo: 1.000000,
+ yo: -9.000000,
+ xa: 9.881356,
+ },
+ BakedGlyph {
+ x0: 111,
+ y0: 86,
+ x1: 120,
+ y1: 99,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 9.881356,
+ },
+ BakedGlyph {
+ x0: 1,
+ y0: 104,
+ x1: 7,
+ y1: 113,
+ xo: 1.000000,
+ yo: -9.000000,
+ xa: 6.338983,
+ },
+ BakedGlyph {
+ x0: 8,
+ y0: 104,
+ x1: 15,
+ y1: 114,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 7.254237,
+ },
+ BakedGlyph {
+ x0: 16,
+ y0: 104,
+ x1: 22,
+ y1: 117,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 6.559322,
+ },
+ BakedGlyph {
+ x0: 23,
+ y0: 104,
+ x1: 31,
+ y1: 114,
+ xo: 1.000000,
+ yo: -9.000000,
+ xa: 9.644068,
+ },
+ BakedGlyph {
+ x0: 32,
+ y0: 104,
+ x1: 40,
+ y1: 113,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 8.135593,
+ },
+ BakedGlyph {
+ x0: 41,
+ y0: 104,
+ x1: 54,
+ y1: 113,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 13.135593,
+ },
+ BakedGlyph {
+ x0: 55,
+ y0: 104,
+ x1: 63,
+ y1: 113,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 8.457627,
+ },
+ BakedGlyph {
+ x0: 64,
+ y0: 104,
+ x1: 72,
+ y1: 117,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 8.033898,
+ },
+ BakedGlyph {
+ x0: 73,
+ y0: 104,
+ x1: 81,
+ y1: 113,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 7.711864,
+ },
+ BakedGlyph {
+ x0: 82,
+ y0: 104,
+ x1: 88,
+ y1: 122,
+ xo: 0.000000,
+ yo: -14.000000,
+ xa: 5.406780,
+ },
+ BakedGlyph {
+ x0: 89,
+ y0: 104,
+ x1: 91,
+ y1: 122,
+ xo: 1.000000,
+ yo: -14.000000,
+ xa: 4.440678,
+ },
+ BakedGlyph {
+ x0: 92,
+ y0: 104,
+ x1: 97,
+ y1: 122,
+ xo: 0.000000,
+ yo: -14.000000,
+ xa: 5.406780,
+ },
+ BakedGlyph {
+ x0: 98,
+ y0: 104,
+ x1: 107,
+ y1: 108,
+ xo: 0.000000,
+ yo: -7.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 108,
+ y0: 104,
+ x1: 116,
+ y1: 117,
+ xo: 0.000000,
+ yo: -13.000000,
+ xa: 8.474576,
+ },
+];
+
+pub const FONT_BITMAP: [u8; 16384] = [
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x34, 0xae, 0x00, 0x00, 0x81, 0x90, 0x00, 0xe0, 0x31, 0x00, 0x00, 0x00, 0x00, 0x06,
+ 0xba, 0x05, 0x00, 0x00, 0xa2, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0x0d, 0x00, 0x00, 0x00,
+ 0x00, 0x16, 0xb2, 0xec, 0xbc, 0x1f, 0x00, 0x00, 0x00, 0x49, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x0d, 0x9d, 0xeb, 0xe5, 0x89, 0x03, 0x00, 0x00, 0x00, 0x00, 0x81, 0x90, 0x00, 0x00, 0x00,
+ 0x00, 0x13, 0x00, 0x00, 0x11, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6c, 0x6b, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4a, 0x76, 0x00,
+ 0x3b, 0x70, 0x70, 0x70, 0x22, 0x00, 0x56, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x2d, 0x1e, 0x00, 0x00, 0x18, 0xac, 0xea, 0xd2, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x45, 0xe9, 0x00, 0x00, 0x8a, 0x99, 0x00, 0xf6, 0x2d, 0x00, 0x00, 0x00, 0x00, 0x2f,
+ 0xd8, 0x00, 0x00, 0x08, 0xf7, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0xad, 0x52, 0x00, 0x00, 0x00,
+ 0x00, 0xa5, 0x8f, 0x0c, 0x7d, 0xba, 0x00, 0x00, 0x06, 0xda, 0x3d, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x92, 0xb9, 0x20, 0x28, 0xd7, 0x64, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x99, 0x00, 0x00, 0x00,
+ 0x28, 0xdd, 0x03, 0x00, 0xa5, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x83, 0x7f, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb6, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xaa, 0x00,
+ 0x54, 0xa0, 0xa0, 0xa0, 0x31, 0x00, 0xbb, 0xe4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+ 0xe0, 0x34, 0x00, 0x0b, 0xd7, 0x95, 0x1d, 0x40, 0xe1, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x45, 0xe9, 0x00, 0x00, 0x7a, 0x88, 0x00, 0xe6, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x60,
+ 0xa8, 0x00, 0x00, 0x34, 0xd3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb2, 0x6b, 0x0b, 0x00, 0x00,
+ 0x00, 0xea, 0x22, 0x00, 0x10, 0xf7, 0x05, 0x00, 0x73, 0xab, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0xd6, 0x50, 0x00, 0x00, 0x81, 0x98, 0x00, 0x00, 0x00, 0x00, 0x7a, 0x88, 0x00, 0x00, 0x00,
+ 0xb4, 0x6a, 0x00, 0x00, 0x2e, 0xe4, 0x0d, 0x00, 0x00, 0x24, 0xcd, 0x61, 0x65, 0x62, 0x61, 0xcd,
+ 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb6, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9b, 0x83, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c,
+ 0xda, 0x00, 0x00, 0x5e, 0xd0, 0x03, 0x00, 0x00, 0x49, 0xe6, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x44, 0xe9, 0x00, 0x00, 0x5a, 0x67, 0x00, 0xbf, 0x02, 0x00, 0x1b, 0x86, 0x86, 0xc4,
+ 0xc5, 0x86, 0x86, 0xb0, 0xd9, 0x86, 0x24, 0x00, 0x00, 0x57, 0xc9, 0xea, 0xd3, 0xef, 0x85, 0x00,
+ 0x00, 0xed, 0x1e, 0x00, 0x0d, 0xf7, 0x07, 0x15, 0xe8, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0xbf, 0x65, 0x00, 0x00, 0xc6, 0x6a, 0x00, 0x00, 0x00, 0x00, 0x5a, 0x67, 0x00, 0x00, 0x47,
+ 0xd7, 0x04, 0x00, 0x00, 0x00, 0x9d, 0x85, 0x00, 0x00, 0x0d, 0x55, 0x8d, 0xcc, 0xcc, 0x8c, 0x54,
+ 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb6, 0x45, 0x00, 0x00, 0x00, 0x00, 0x01, 0xe0, 0x2e, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x96,
+ 0x81, 0x00, 0x00, 0xb7, 0x80, 0x00, 0x00, 0x00, 0x04, 0xf1, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x3d, 0xe3, 0x00, 0x00, 0x0f, 0x12, 0x00, 0x21, 0x00, 0x00, 0x13, 0x5f, 0x5f, 0xe0,
+ 0x86, 0x5f, 0x5f, 0xc2, 0x9f, 0x5f, 0x1a, 0x00, 0x16, 0xf9, 0x3d, 0x01, 0x00, 0x03, 0x13, 0x00,
+ 0x00, 0xae, 0x80, 0x04, 0x6c, 0xc4, 0x00, 0x96, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x56, 0xe2, 0x1f, 0x8e, 0xc2, 0x06, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x12, 0x00, 0x00, 0xc7,
+ 0x4f, 0x00, 0x00, 0x00, 0x00, 0x1a, 0xee, 0x0d, 0x00, 0x00, 0x00, 0x3b, 0xb4, 0xb5, 0x37, 0x00,
+ 0x00, 0x00, 0x05, 0x5f, 0x5f, 0x5f, 0xd1, 0x8a, 0x5f, 0x5f, 0x3a, 0x00, 0x0b, 0x76, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0xea,
+ 0x27, 0x00, 0x00, 0xd8, 0x4f, 0x00, 0x00, 0x00, 0x00, 0xc2, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x35, 0xd8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xf2,
+ 0x16, 0x00, 0x00, 0xc6, 0x3f, 0x00, 0x00, 0x00, 0x65, 0xc3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x1f, 0xc7, 0xef, 0xd1, 0x29, 0x2c, 0xe4, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x0d, 0xd1, 0xfa, 0x8f, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xf6,
+ 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0xca, 0x3b, 0x00, 0x00, 0x23, 0xec, 0x3c, 0x3d, 0xea, 0x21,
+ 0x00, 0x00, 0x07, 0x93, 0x93, 0x93, 0xe0, 0xb0, 0x93, 0x93, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4a, 0xcd,
+ 0x00, 0x00, 0x00, 0xf0, 0x3e, 0x00, 0x00, 0x00, 0x00, 0xb0, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x25, 0xc5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x25, 0xe4,
+ 0x00, 0x00, 0x03, 0xf4, 0x0f, 0x00, 0x00, 0x00, 0x4d, 0xec, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xba, 0x63, 0x07, 0x82, 0xc2, 0x8d, 0x0c, 0x00, 0x00, 0x00,
+ 0x24, 0xda, 0x7b, 0x97, 0xc2, 0x0b, 0x00, 0x01, 0xa2, 0x03, 0x00, 0x00, 0x00, 0x00, 0x2a, 0xe1,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa3, 0x69, 0x00, 0x00, 0x0a, 0x5b, 0x00, 0x00, 0x5d, 0x0a,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb6, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x74,
+ 0x00, 0x00, 0x00, 0xf6, 0x3a, 0x00, 0x00, 0x00, 0x00, 0xab, 0x85, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x09, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x1e, 0x67, 0xbe,
+ 0x1e, 0x1e, 0x40, 0xe4, 0x1e, 0x1e, 0x08, 0x00, 0x01, 0x96, 0xf2, 0x91, 0x2b, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x4c, 0xce, 0x02, 0x8c, 0xb1, 0x37, 0xa3, 0xa2, 0x00, 0x00, 0x00,
+ 0xc6, 0x75, 0x00, 0x01, 0x9c, 0xbe, 0x0a, 0x28, 0xd1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x57, 0xbc,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7d, 0x96, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb6, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0xf1, 0x1c,
+ 0x00, 0x00, 0x00, 0xde, 0x49, 0x00, 0x00, 0x00, 0x00, 0xbc, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0xcb, 0xea, 0xe2,
+ 0xcb, 0xcb, 0xe1, 0xeb, 0xcb, 0xcb, 0x37, 0x00, 0x00, 0x00, 0x2d, 0x93, 0xee, 0xb4, 0x1f, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x05, 0xd8, 0x40, 0x00, 0xe2, 0x2e, 0x00, 0x19, 0xf5, 0x02, 0x00, 0x13,
+ 0xff, 0x1d, 0x00, 0x00, 0x02, 0xa1, 0xbb, 0x94, 0x7b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x84, 0x96,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x56, 0xc4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb6, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x59, 0xc0, 0x00,
+ 0x00, 0x00, 0x00, 0xc4, 0x6e, 0x00, 0x00, 0x00, 0x00, 0xe2, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb6, 0x52,
+ 0x00, 0x00, 0x8b, 0x7d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xa7, 0xd7, 0x04,
+ 0x00, 0x00, 0x00, 0x00, 0x70, 0xad, 0x00, 0x00, 0xf2, 0x15, 0x00, 0x06, 0xf7, 0x0a, 0x00, 0x10,
+ 0xff, 0x23, 0x00, 0x00, 0x00, 0x03, 0xb2, 0xfb, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0x9b,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5b, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb3, 0x67, 0x00,
+ 0x00, 0x00, 0x00, 0x77, 0xb5, 0x00, 0x00, 0x00, 0x2b, 0xf5, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x33, 0x8c, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe8, 0x20,
+ 0x00, 0x00, 0xbd, 0x4b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0xfc, 0x2c,
+ 0x00, 0x00, 0x00, 0x13, 0xe7, 0x23, 0x00, 0x00, 0xc1, 0x5c, 0x00, 0x47, 0xd7, 0x00, 0x00, 0x00,
+ 0xb5, 0xaf, 0x0d, 0x00, 0x0a, 0x60, 0xe1, 0xbd, 0xa5, 0x01, 0x00, 0x00, 0x00, 0x00, 0x51, 0xc1,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x82, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0xf4, 0x13, 0x00,
+ 0x00, 0x00, 0x00, 0x19, 0xf3, 0x57, 0x00, 0x0b, 0xc0, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x7f, 0xfe, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1b, 0xec, 0x00,
+ 0x00, 0x01, 0xee, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0xef, 0x0c,
+ 0x00, 0x00, 0x00, 0x94, 0x8a, 0x00, 0x00, 0x00, 0x35, 0xe8, 0xc4, 0xed, 0x44, 0x00, 0x00, 0x00,
+ 0x15, 0xa9, 0xf1, 0xdc, 0xea, 0xaf, 0x1e, 0x07, 0xc7, 0x73, 0x00, 0x00, 0x00, 0x00, 0x24, 0xe6,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa8, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x67, 0xb3, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x3c, 0xea, 0xdb, 0xf2, 0xae, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x02, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x85, 0x9b, 0x61, 0x52, 0x6c, 0xd1, 0x89, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x34, 0x10, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x03, 0x1b, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xf4,
+ 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x35, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc1, 0x5a, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x2c, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x69, 0x8f, 0xe2, 0xa5, 0x28, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xba,
+ 0x5e, 0x00, 0x00, 0x00, 0x00, 0x26, 0xe9, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1d, 0xf2, 0x0b, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xad, 0x52, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x35,
+ 0xe1, 0x0a, 0x00, 0x00, 0x00, 0xaf, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0xa6, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x93, 0x46, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xa1, 0x7d, 0x00, 0x00, 0x3e, 0xd9, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x4d, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x1a, 0xd2, 0x03, 0x00, 0xa0, 0x4f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0xed, 0x06, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x1a, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x28, 0xb6, 0x00, 0x14, 0x93, 0xe1, 0xe8, 0x99, 0x42, 0x00, 0x00, 0x00,
+ 0x2b, 0xaa, 0xea, 0xf3, 0xca, 0x4e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2c, 0xbd,
+ 0x30, 0x00, 0x00, 0x00, 0x63, 0xbe, 0xbe, 0xbe, 0xbe, 0x94, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d,
+ 0x5a, 0x8c, 0x92, 0x00, 0x00, 0x9e, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0x3a, 0x00, 0x00, 0x39,
+ 0xbe, 0xed, 0xde, 0x94, 0x0f, 0x00, 0x00, 0x00, 0x47, 0xc5, 0xf0, 0xce, 0x62, 0x00, 0x00, 0x00,
+ 0x6a, 0x88, 0x00, 0x00, 0x00, 0x6a, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x2a, 0x29, 0x00, 0x02, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x18, 0x00, 0x00, 0x4a, 0x0a,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xd0, 0xf3, 0xe5, 0xa3, 0x17, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x36, 0xe8, 0xfb, 0x00, 0x98, 0x8a, 0x21, 0x16, 0x68, 0xfb, 0x1d, 0x00, 0x00,
+ 0x3a, 0x6a, 0x22, 0x11, 0x62, 0xf8, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0xd7, 0xed,
+ 0x41, 0x00, 0x00, 0x00, 0x8e, 0x8e, 0x38, 0x38, 0x38, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x42, 0xdd,
+ 0xa9, 0x5c, 0x2b, 0x00, 0x00, 0x39, 0x45, 0x45, 0x45, 0x45, 0x5a, 0xf1, 0x24, 0x00, 0x25, 0xef,
+ 0x54, 0x05, 0x17, 0xad, 0xb3, 0x00, 0x00, 0x3e, 0xe9, 0x46, 0x08, 0x39, 0xe1, 0x65, 0x00, 0x00,
+ 0xaf, 0xd8, 0x00, 0x00, 0x00, 0xaf, 0xd8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x50, 0xbc,
+ 0xe2, 0x4f, 0x00, 0x0a, 0xcb, 0xcb, 0xcb, 0xcb, 0xcb, 0xcb, 0xcb, 0x7c, 0x00, 0x02, 0xaa, 0xe9,
+ 0x8b, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x49, 0x0e, 0x16, 0xae, 0xb1, 0x00, 0x00, 0x00,
+ 0x00, 0x0d, 0xad, 0xdf, 0x61, 0xfb, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0xba, 0x7a, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xac, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0xc6, 0x50, 0xd4,
+ 0x41, 0x00, 0x00, 0x00, 0x9b, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x43, 0xf1, 0x36,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xae, 0x71, 0x00, 0x00, 0x7d, 0xa3,
+ 0x00, 0x00, 0x00, 0x18, 0xff, 0x13, 0x00, 0xb6, 0x78, 0x00, 0x00, 0x00, 0x4b, 0xe0, 0x04, 0x00,
+ 0x02, 0x05, 0x00, 0x00, 0x00, 0x02, 0x05, 0x00, 0x00, 0x00, 0x00, 0x13, 0x76, 0xdf, 0xbe, 0x53,
+ 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1d,
+ 0x84, 0xe6, 0xb1, 0x45, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0xeb, 0x00, 0x00, 0x00,
+ 0x00, 0x01, 0x58, 0x08, 0x1e, 0xfb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x84, 0xa8, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xaa, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87, 0x8a, 0x00, 0xd4,
+ 0x41, 0x00, 0x00, 0x00, 0xad, 0x4e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xd0, 0x54, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0xd4, 0x02, 0x00, 0x00, 0x8b, 0x93,
+ 0x00, 0x00, 0x00, 0x1e, 0xff, 0x18, 0x00, 0xe2, 0x4c, 0x00, 0x00, 0x00, 0x06, 0xfb, 0x24, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x9d, 0xe9, 0x94, 0x2a, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x06, 0x5b, 0xc5, 0xd6, 0x4e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, 0xbd, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x1e, 0xfb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc8, 0x69, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x3e, 0xe4, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x39, 0xd0, 0x07, 0x00, 0xd4,
+ 0x41, 0x00, 0x00, 0x00, 0xc5, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4a, 0xeb, 0x04, 0x01,
+ 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xcc, 0x58, 0x00, 0x00, 0x00, 0x45, 0xe5,
+ 0x1a, 0x00, 0x00, 0x74, 0xc5, 0x00, 0x00, 0xd2, 0x80, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x41, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0xd8, 0xcb, 0x58, 0x06, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x08, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x5c, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x21, 0x89, 0xef, 0x73, 0x00, 0x00, 0x00, 0x00, 0x2b, 0xe6, 0x32, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x1e, 0xfb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0xd1, 0x06, 0x00, 0x00,
+ 0x00, 0x00, 0x82, 0xd2, 0xed, 0x2e, 0x00, 0x00, 0x00, 0x00, 0x03, 0xcd, 0x3d, 0x00, 0x00, 0xd4,
+ 0x41, 0x00, 0x00, 0x00, 0xc5, 0xf6, 0xe8, 0xb5, 0x3e, 0x00, 0x00, 0x00, 0x71, 0xdc, 0xc6, 0xef,
+ 0xf3, 0xc1, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0xd8, 0x02, 0x00, 0x00, 0x00, 0x00, 0x5c,
+ 0xe9, 0x8e, 0x7d, 0xd8, 0x20, 0x00, 0x00, 0x82, 0xd4, 0x1c, 0x00, 0x00, 0x16, 0xed, 0x39, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x46, 0xb3, 0xe4, 0x81, 0x1b,
+ 0x00, 0x00, 0x00, 0x05, 0x5b, 0x5b, 0x5b, 0x5b, 0x5b, 0x5b, 0x5b, 0x37, 0x00, 0x00, 0x00, 0x01,
+ 0x47, 0xb2, 0xe5, 0x81, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x1d, 0xdf, 0x49, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x1e, 0xfb, 0x00, 0x00, 0x00, 0x00, 0x67, 0xe2, 0x1f, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x25, 0x4b, 0x95, 0xea, 0x3f, 0x00, 0x00, 0x00, 0x63, 0xac, 0x00, 0x00, 0x00, 0xd4,
+ 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0x6c, 0xee, 0x5c, 0x00, 0x00, 0x94, 0xb6, 0x29, 0x02,
+ 0x1d, 0x8a, 0xef, 0x11, 0x00, 0x00, 0x00, 0x00, 0xaf, 0x78, 0x00, 0x00, 0x00, 0x00, 0x03, 0x9e,
+ 0xc5, 0x6f, 0xc8, 0xc2, 0x19, 0x00, 0x00, 0x0c, 0xab, 0xfd, 0xc6, 0xcd, 0xdb, 0xff, 0x17, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x8d, 0xea,
+ 0xab, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x71, 0xda,
+ 0xc7, 0x5b, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xaa, 0x76, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x1e, 0xfb, 0x00, 0x00, 0x00, 0x6f, 0xe1, 0x24, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x6a, 0xe0, 0x01, 0x00, 0x02, 0xd9, 0x45, 0x16, 0x16, 0x16, 0xd7,
+ 0x51, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x51, 0xed, 0x01, 0x00, 0xa1, 0x81, 0x00, 0x00,
+ 0x00, 0x06, 0xf1, 0x65, 0x00, 0x00, 0x00, 0x0d, 0xf6, 0x20, 0x00, 0x00, 0x00, 0x00, 0x76, 0xc8,
+ 0x07, 0x00, 0x00, 0x63, 0xe6, 0x0a, 0x00, 0x00, 0x00, 0x16, 0x3b, 0x26, 0x3e, 0xf2, 0x00, 0x00,
+ 0x56, 0x70, 0x00, 0x00, 0x00, 0x4a, 0x76, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b,
+ 0x66, 0x47, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x83, 0x35,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x1e, 0xfb, 0x00, 0x00, 0x59, 0xe6, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xff, 0x1c, 0x00, 0x1f, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xf7,
+ 0xdc, 0x94, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0xff, 0x20, 0x00, 0x64, 0x92, 0x00, 0x00,
+ 0x00, 0x00, 0xb4, 0x81, 0x00, 0x00, 0x00, 0x5a, 0xc7, 0x00, 0x00, 0x00, 0x00, 0x00, 0xca, 0x72,
+ 0x00, 0x00, 0x00, 0x00, 0xdc, 0x4d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8d, 0x9b, 0x00, 0x00,
+ 0xbb, 0xe4, 0x00, 0x00, 0x00, 0x80, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x1e, 0xfb, 0x00, 0x13, 0xec, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x4c, 0xfc, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd4,
+ 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0xfe, 0x0d, 0x00, 0x21, 0xcc, 0x00, 0x00,
+ 0x00, 0x00, 0xca, 0x63, 0x00, 0x00, 0x00, 0x9b, 0x8e, 0x00, 0x00, 0x00, 0x00, 0x00, 0xca, 0x5e,
+ 0x00, 0x00, 0x00, 0x00, 0xce, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0xed, 0x18, 0x00, 0x00,
+ 0x09, 0x11, 0x00, 0x00, 0x00, 0x9b, 0x83, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x1e, 0xfb, 0x00, 0x6d, 0xb6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x25, 0x04, 0x00, 0x01, 0x32, 0xc1, 0xa8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd4,
+ 0x41, 0x00, 0x00, 0x22, 0x06, 0x00, 0x00, 0x10, 0xb0, 0xac, 0x00, 0x00, 0x00, 0xdd, 0x5b, 0x00,
+ 0x00, 0x4b, 0xec, 0x10, 0x00, 0x00, 0x00, 0xc6, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7a, 0xcb,
+ 0x11, 0x00, 0x00, 0x52, 0xeb, 0x14, 0x00, 0x00, 0x03, 0x22, 0x88, 0xda, 0x4b, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x01, 0xe0, 0x2e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x8e, 0x2b, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x1e, 0xfb, 0x00, 0x9b, 0xfb, 0xf6, 0xf6, 0xf6, 0xf6, 0xf6, 0x21, 0x00,
+ 0xa4, 0xf0, 0xcc, 0xe7, 0xfd, 0x9d, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd4,
+ 0x41, 0x00, 0x00, 0x8f, 0xf2, 0xce, 0xd2, 0xf4, 0xa2, 0x13, 0x00, 0x00, 0x00, 0x33, 0xe0, 0xd8,
+ 0xcd, 0xe0, 0x3a, 0x00, 0x00, 0x00, 0x01, 0xf1, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x91,
+ 0xea, 0xbe, 0xcf, 0xdc, 0x3f, 0x00, 0x00, 0x52, 0xe1, 0xbf, 0x89, 0x10, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x0b, 0x76, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0xfe, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x17, 0x39, 0x2e, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x12, 0x37, 0x30, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x24,
+ 0x28, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x0e, 0x34, 0x24, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x0e, 0x7d, 0xb2, 0xdc, 0xe5, 0xb8, 0x84, 0x14, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x30, 0xbd, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x45, 0xb2, 0xce, 0xd4,
+ 0xc2, 0x9a, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x90, 0xce, 0xf7, 0xea, 0xb6, 0x4c, 0x00,
+ 0x45, 0xb1, 0xcb, 0xd0, 0xb0, 0x8b, 0x34, 0x00, 0x00, 0x00, 0x00, 0x5a, 0xbe, 0xbe, 0xbe, 0xbe,
+ 0xbe, 0xbe, 0x3a, 0x00, 0x5a, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0x2a, 0x00, 0x00, 0x00, 0x11,
+ 0x8e, 0xcb, 0xf4, 0xdf, 0xbb, 0x5f, 0x00, 0x5a, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb4,
+ 0x27, 0x00, 0x5a, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x94, 0x00, 0x5a, 0x81, 0x00,
+ 0x00, 0x00, 0x00, 0x21, 0xb7, 0x2f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x54, 0xe4, 0x9c, 0x4a, 0x1d, 0x13, 0x45, 0x8c, 0xe7, 0x5c, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xa6, 0xec, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79, 0xc0, 0x2c, 0x27,
+ 0x39, 0x74, 0xeb, 0x79, 0x00, 0x00, 0x00, 0x37, 0xe5, 0x9f, 0x43, 0x16, 0x29, 0x63, 0x62, 0x00,
+ 0x79, 0xc1, 0x31, 0x2d, 0x4f, 0x7b, 0xe7, 0xa6, 0x0f, 0x00, 0x00, 0x79, 0xc2, 0x41, 0x41, 0x41,
+ 0x41, 0x41, 0x14, 0x00, 0x79, 0xc2, 0x41, 0x41, 0x41, 0x41, 0x41, 0x0e, 0x00, 0x00, 0x37, 0xe4,
+ 0x9e, 0x3b, 0x13, 0x28, 0x5b, 0x83, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf2,
+ 0x34, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0xc7, 0x00, 0x79, 0xad, 0x00,
+ 0x00, 0x00, 0x1b, 0xda, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x39, 0xeb, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0xe5, 0x44, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x1f, 0xed, 0x45, 0xea, 0x09, 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00,
+ 0x00, 0x00, 0x42, 0xf4, 0x06, 0x00, 0x08, 0xd9, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x10, 0xb6, 0xa5, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xda, 0x73,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf2,
+ 0x34, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0xc7, 0x00, 0x79, 0xad, 0x00,
+ 0x00, 0x1b, 0xd9, 0x6b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x07, 0xd6, 0x67, 0x00, 0x00, 0x19, 0x63, 0x86, 0x70, 0x36, 0x00, 0x53, 0xe2, 0x0e, 0x00,
+ 0x00, 0x00, 0x00, 0x89, 0x93, 0x00, 0xcb, 0x61, 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00,
+ 0x00, 0x00, 0x1b, 0xfd, 0x13, 0x00, 0x72, 0xd4, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0xf0, 0x4d, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x72, 0xd1, 0x03,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf2,
+ 0x34, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0xc7, 0x00, 0x79, 0xad, 0x00,
+ 0x1d, 0xd9, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x4f, 0xd4, 0x02, 0x00, 0x3f, 0xe7, 0x94, 0x64, 0x8d, 0xe1, 0x00, 0x00, 0xc8, 0x50, 0x00,
+ 0x00, 0x00, 0x08, 0xea, 0x29, 0x00, 0x60, 0xc9, 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00,
+ 0x00, 0x00, 0x80, 0xc2, 0x00, 0x00, 0xac, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x8d, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xac, 0x81, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf2,
+ 0x34, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0xc7, 0x00, 0x79, 0xad, 0x22,
+ 0xda, 0x5f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x7b, 0x99, 0x00, 0x05, 0xdc, 0x5a, 0x00, 0x00, 0x34, 0xe1, 0x00, 0x00, 0x91, 0x82, 0x00,
+ 0x00, 0x00, 0x5d, 0xc5, 0x00, 0x00, 0x0b, 0xf1, 0x32, 0x00, 0x00, 0x00, 0x79, 0xdf, 0x9c, 0x9c,
+ 0xa6, 0xd5, 0xcc, 0x12, 0x00, 0x00, 0xd7, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0xbb, 0x00, 0x79, 0xe3, 0xa9, 0xa9, 0xa9,
+ 0xa9, 0x75, 0x00, 0x00, 0x79, 0xdf, 0x9c, 0x9c, 0x9c, 0x9c, 0x5c, 0x00, 0x00, 0xd7, 0x5c, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79, 0xe2, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xfa,
+ 0x34, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0xc7, 0x00, 0x79, 0xcf, 0xe6,
+ 0x5b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0xa7, 0x70, 0x00, 0x2b, 0xf3, 0x01, 0x00, 0x00, 0x34, 0xe1, 0x00, 0x00, 0x6c, 0xab, 0x00,
+ 0x00, 0x00, 0xc1, 0x64, 0x00, 0x00, 0x00, 0x99, 0x97, 0x00, 0x00, 0x00, 0x79, 0xcc, 0x5f, 0x5f,
+ 0x66, 0x80, 0xd4, 0xc1, 0x0f, 0x00, 0xe2, 0x52, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6f, 0xc6, 0x00, 0x79, 0xc7, 0x52, 0x52, 0x52,
+ 0x52, 0x39, 0x00, 0x00, 0x79, 0xcd, 0x63, 0x63, 0x63, 0x63, 0x3b, 0x00, 0x00, 0xe2, 0x52, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x16, 0x8f, 0x00, 0x79, 0xca, 0x5b, 0x5b, 0x5b, 0x5b, 0x5b, 0x5b, 0xf7,
+ 0x34, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0xc7, 0x00, 0x79, 0xc5, 0xba,
+ 0xd2, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0xb9, 0x60, 0x00, 0x3d, 0xe4, 0x00, 0x00, 0x00, 0x34, 0xe1, 0x00, 0x00, 0x79, 0x98, 0x00,
+ 0x00, 0x24, 0xfb, 0x41, 0x34, 0x34, 0x34, 0x68, 0xf0, 0x0a, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00,
+ 0x00, 0x00, 0x08, 0xce, 0x80, 0x00, 0xbb, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x95, 0x98, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbb, 0x72, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x27, 0xff, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf2,
+ 0x34, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0xc7, 0x00, 0x79, 0xad, 0x01,
+ 0x74, 0xea, 0x3b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8f, 0x87, 0x00, 0x16, 0xfd, 0x15, 0x00, 0x00, 0x34, 0xe1, 0x00, 0x00, 0xb4, 0x6d, 0x00,
+ 0x00, 0x85, 0xe3, 0xc7, 0xc7, 0xc7, 0xc7, 0xc7, 0xee, 0x5e, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x89, 0xb2, 0x00, 0x90, 0xae, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xdb, 0x66, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xaf, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x27, 0xff, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf2,
+ 0x34, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0xc0, 0x00, 0x79, 0xad, 0x00,
+ 0x00, 0x50, 0xec, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x64, 0xb2, 0x00, 0x00, 0xa8, 0xae, 0x0e, 0x00, 0x3d, 0xfb, 0x16, 0x4b, 0xec, 0x0f, 0x00,
+ 0x02, 0xe2, 0x46, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0xbf, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0xb4, 0x90, 0x00, 0x21, 0xf5, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86, 0xce, 0x06, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0xf6, 0x32,
+ 0x00, 0x00, 0x00, 0x00, 0x27, 0xff, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf2,
+ 0x34, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb8, 0xa5, 0x00, 0x79, 0xad, 0x00,
+ 0x00, 0x00, 0x52, 0xee, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x2a, 0xf3, 0x24, 0x00, 0x0f, 0x96, 0xe4, 0xe3, 0xc4, 0xa4, 0xf0, 0xcd, 0x33, 0x00, 0x00,
+ 0x45, 0xe5, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0xfd, 0x22, 0x00, 0x79, 0xad, 0x00, 0x00,
+ 0x00, 0x21, 0x8d, 0xed, 0x2a, 0x00, 0x00, 0x74, 0xec, 0x50, 0x04, 0x00, 0x00, 0x11, 0x33, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x0d, 0x37, 0xb5, 0xdf, 0x27, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0xec,
+ 0x53, 0x05, 0x00, 0x00, 0x28, 0xff, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf2,
+ 0x34, 0x00, 0x79, 0xad, 0x00, 0x4f, 0x28, 0x00, 0x04, 0x48, 0xf5, 0x51, 0x00, 0x79, 0xad, 0x00,
+ 0x00, 0x00, 0x00, 0x6e, 0xe1, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x8a, 0xbf, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xa4, 0x8d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc2, 0x81, 0x00, 0x65, 0xf3, 0xeb, 0xe4,
+ 0xef, 0xe5, 0x97, 0x29, 0x00, 0x00, 0x00, 0x00, 0x4c, 0xdd, 0xf5, 0xd1, 0xe2, 0xf7, 0x95, 0x00,
+ 0x65, 0xf3, 0xef, 0xeb, 0xf0, 0xcc, 0x74, 0x07, 0x00, 0x00, 0x00, 0x79, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xbe, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4b,
+ 0xd9, 0xf7, 0xd1, 0xd4, 0xf2, 0xd1, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf2,
+ 0x34, 0x00, 0x79, 0xad, 0x00, 0x75, 0xf0, 0xd8, 0xe9, 0xfd, 0x86, 0x02, 0x00, 0x79, 0xad, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xad, 0xa9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x08, 0xcf, 0xbd, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x11, 0x15,
+ 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x3a, 0x30, 0x0a, 0x00, 0x00,
+ 0x00, 0x02, 0x10, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x10, 0x34, 0x24, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x30, 0x33, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x07, 0x78, 0xeb, 0xba, 0x91, 0x72, 0x7c, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x41, 0x68, 0x84, 0x76, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x5a, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb7, 0x37, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x6e, 0x77, 0x00, 0x00, 0x5a, 0x8e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x7a, 0x5d, 0x00, 0x00, 0x00, 0x13, 0x93, 0xcd, 0xf3, 0xd3, 0x9e, 0x1e, 0x00, 0x00, 0x00, 0x00,
+ 0x44, 0xaf, 0xca, 0xd3, 0xbc, 0x9b, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x93, 0xcd, 0xf3,
+ 0xd3, 0x9e, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x44, 0xaf, 0xca, 0xd3, 0xbc, 0x99, 0x36, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x09, 0x63, 0xab, 0xee, 0xed, 0xb8, 0x4b, 0x00, 0x00, 0x84, 0xbe, 0xbe, 0xbe,
+ 0xbe, 0xbe, 0xbe, 0xbe, 0xbb, 0x00, 0x64, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0xbe, 0x0a,
+ 0x00, 0x85, 0x6f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xb1, 0x34, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0xff, 0xc4, 0x01, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x1a, 0xf4, 0xb2, 0x00, 0x00, 0x79, 0xff, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xa4, 0x7d, 0x00, 0x00, 0x3c, 0xe8, 0x9c, 0x3f, 0x16, 0x39, 0x8c, 0xee, 0x4f, 0x00, 0x00, 0x00,
+ 0x79, 0xc2, 0x34, 0x2b, 0x3d, 0x7d, 0xed, 0x83, 0x00, 0x00, 0x00, 0x3c, 0xe8, 0x9c, 0x3f, 0x16,
+ 0x39, 0x8c, 0xee, 0x4f, 0x00, 0x00, 0x00, 0x79, 0xc2, 0x35, 0x2b, 0x3c, 0x7b, 0xed, 0x78, 0x00,
+ 0x00, 0x00, 0x00, 0x6a, 0xc2, 0x30, 0x08, 0x1e, 0x5e, 0x58, 0x00, 0x00, 0x2d, 0x41, 0x41, 0x41,
+ 0xf2, 0x6b, 0x41, 0x41, 0x40, 0x00, 0x86, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0xff, 0x0d,
+ 0x00, 0x63, 0xe1, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3d, 0xed, 0x07, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0xef, 0xdd, 0x4e, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x98, 0xcd, 0xc8, 0x00, 0x00, 0x79, 0xc4, 0xe6, 0x4b, 0x00, 0x00, 0x00, 0x00,
+ 0xa4, 0x7d, 0x00, 0x09, 0xdd, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0xed, 0x16, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x32, 0xfe, 0x27, 0x00, 0x09, 0xdd, 0x71, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x53, 0xed, 0x16, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x3b, 0xfd, 0x1a,
+ 0x00, 0x00, 0x02, 0xd8, 0x2b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xee, 0x38, 0x00, 0x00, 0x00, 0x00, 0x86, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0xff, 0x0d,
+ 0x00, 0x0f, 0xf7, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x95, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0xd8, 0x61, 0xcb, 0x01, 0x00,
+ 0x00, 0x00, 0x1f, 0xea, 0x4a, 0xdd, 0x00, 0x00, 0x79, 0xa9, 0x3d, 0xe9, 0x1f, 0x00, 0x00, 0x00,
+ 0xa4, 0x7d, 0x00, 0x76, 0xd4, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb9, 0x94, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0xdc, 0x5b, 0x00, 0x76, 0xd4, 0x03, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0xb9, 0x94, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0xeb, 0x49,
+ 0x00, 0x00, 0x09, 0xfc, 0x2f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xee, 0x38, 0x00, 0x00, 0x00, 0x00, 0x86, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0xff, 0x0d,
+ 0x00, 0x00, 0xac, 0x93, 0x00, 0x00, 0x00, 0x00, 0x04, 0xe9, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4e, 0xc4, 0x05, 0xdf, 0x4a, 0x00,
+ 0x00, 0x00, 0x97, 0x84, 0x27, 0xf3, 0x00, 0x00, 0x79, 0xa9, 0x00, 0x79, 0xc8, 0x06, 0x00, 0x00,
+ 0xa4, 0x7d, 0x00, 0xad, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0xcb, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x01, 0xed, 0x47, 0x00, 0xad, 0x81, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x62, 0xcb, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x09, 0xf9, 0x1f,
+ 0x00, 0x00, 0x00, 0xaf, 0xc5, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xee, 0x38, 0x00, 0x00, 0x00, 0x00, 0x86, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0xff, 0x0d,
+ 0x00, 0x00, 0x4f, 0xea, 0x05, 0x00, 0x00, 0x00, 0x49, 0xdc, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0xb1, 0x00, 0x6a, 0xc2, 0x00,
+ 0x00, 0x18, 0xed, 0x15, 0x14, 0xff, 0x09, 0x00, 0x79, 0xa9, 0x00, 0x02, 0xbc, 0x88, 0x00, 0x00,
+ 0xa4, 0x7d, 0x00, 0xd8, 0x5c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0xf4, 0x02, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x75, 0xeb, 0x10, 0x00, 0xd8, 0x5c, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x3e, 0xf4, 0x02, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x08, 0x95, 0xc4, 0x00,
+ 0x00, 0x00, 0x00, 0x0e, 0x9f, 0xf5, 0x9e, 0x2f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xee, 0x38, 0x00, 0x00, 0x00, 0x00, 0x86, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0xff, 0x0d,
+ 0x00, 0x00, 0x05, 0xe8, 0x4a, 0x00, 0x00, 0x00, 0xa4, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, 0x9d, 0x00, 0x09, 0xe8, 0x3a,
+ 0x00, 0x88, 0x92, 0x00, 0x03, 0xfd, 0x1b, 0x00, 0x79, 0xa9, 0x00, 0x00, 0x1b, 0xea, 0x3c, 0x00,
+ 0xa4, 0x7d, 0x00, 0xe1, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x35, 0xfa, 0x05, 0x00,
+ 0x79, 0xd9, 0x8a, 0x90, 0xa1, 0xdb, 0xda, 0x38, 0x00, 0x00, 0xe2, 0x54, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x35, 0xfc, 0x06, 0x00, 0x79, 0xe9, 0xba, 0xbd, 0xc9, 0xf2, 0x90, 0x0f, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x25, 0x92, 0xf2, 0xa9, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xee, 0x38, 0x00, 0x00, 0x00, 0x00, 0x86, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0xff, 0x0d,
+ 0x00, 0x00, 0x00, 0x8c, 0xa9, 0x00, 0x00, 0x0f, 0xf3, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x83, 0x90, 0x00, 0x00, 0x79, 0xb0,
+ 0x0d, 0xea, 0x1f, 0x00, 0x00, 0xf2, 0x29, 0x00, 0x79, 0xa9, 0x00, 0x00, 0x00, 0x57, 0xdc, 0x0b,
+ 0xa4, 0x7d, 0x00, 0xb8, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x59, 0xd6, 0x00, 0x00,
+ 0x79, 0xd2, 0x75, 0x6f, 0x5a, 0x37, 0x01, 0x00, 0x00, 0x00, 0xb9, 0x77, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x59, 0xdc, 0x00, 0x00, 0x79, 0xc2, 0x41, 0x40, 0x4a, 0xf0, 0x40, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0xc5, 0xb4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xee, 0x38, 0x00, 0x00, 0x00, 0x00, 0x84, 0xa4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0x0b,
+ 0x00, 0x00, 0x00, 0x29, 0xf7, 0x12, 0x00, 0x66, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x91, 0x85, 0x00, 0x00, 0x10, 0xf0,
+ 0x96, 0xa0, 0x00, 0x00, 0x00, 0xe7, 0x38, 0x00, 0x79, 0xa9, 0x00, 0x00, 0x00, 0x00, 0xab, 0x8a,
+ 0xa4, 0x7d, 0x00, 0x8b, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9a, 0xa9, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8b, 0xb8, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x9a, 0xab, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x62, 0xe3, 0x12, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0xfd, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xee, 0x38, 0x00, 0x00, 0x00, 0x00, 0x6b, 0xc2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0xef, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xbe, 0x6e, 0x00, 0xca, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9f, 0x7a, 0x00, 0x00, 0x00, 0x8e,
+ 0xff, 0x2d, 0x00, 0x00, 0x00, 0xdb, 0x47, 0x00, 0x79, 0xa9, 0x00, 0x00, 0x00, 0x00, 0x16, 0xe8,
+ 0xcf, 0x7d, 0x00, 0x1d, 0xf3, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0xf7, 0x31, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1d, 0xf4, 0x43, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x29, 0xf9, 0x32, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0xb1, 0xa4, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3a, 0xe9, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xee, 0x38, 0x00, 0x00, 0x00, 0x00, 0x3e, 0xf3, 0x16, 0x00, 0x00, 0x00, 0x00, 0x86, 0xc0, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x55, 0xd7, 0x35, 0xe4, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6f, 0x00, 0x00, 0x00, 0x15,
+ 0x46, 0x00, 0x00, 0x00, 0x00, 0xd0, 0x56, 0x00, 0x79, 0xa9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x61,
+ 0xff, 0x7d, 0x00, 0x00, 0x6b, 0xf1, 0x5c, 0x05, 0x00, 0x03, 0x4c, 0xe9, 0x84, 0x00, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6d, 0xf1, 0x5c, 0x05, 0x00,
+ 0x03, 0x4c, 0xe9, 0x84, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x1b, 0xf0, 0x44,
+ 0x00, 0x00, 0x15, 0x47, 0x02, 0x00, 0x00, 0x15, 0xb7, 0x7b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xee, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc4, 0xb7, 0x14, 0x00, 0x00, 0x4f, 0xf5, 0x4a, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x04, 0xe1, 0xd3, 0x7b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x79, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x34, 0x00, 0xbb, 0x64, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xc4, 0x65, 0x00, 0x79, 0xa9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+ 0xc4, 0x7d, 0x00, 0x00, 0x00, 0x41, 0xd4, 0xf7, 0xd4, 0xf4, 0xde, 0x52, 0x00, 0x00, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x43, 0xd4, 0xf7, 0xd4,
+ 0xf4, 0xd5, 0x5f, 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, 0xcc,
+ 0x01, 0x00, 0x33, 0xda, 0xee, 0xc9, 0xd9, 0xdc, 0x99, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xee, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0xaf, 0xf6, 0xd5, 0xe7, 0xeb, 0x58, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, 0xf7, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x35, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x94,
+ 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x1b, 0x38, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x32, 0x1f, 0x01, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12,
+ 0xd6, 0xa9, 0x37, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x0a, 0x75, 0xce, 0xf0, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x3e, 0xa4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0xb1,
+ 0x00, 0x18, 0xba, 0x29, 0x00, 0x00, 0x00, 0x00, 0x07, 0xac, 0x40, 0x00, 0x77, 0x85, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x07, 0xb2, 0x32, 0x00, 0x06, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0x7a,
+ 0x00, 0x15, 0x45, 0x45, 0x45, 0x08, 0x00, 0x18, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x3c, 0x45, 0x45, 0x27, 0x00, 0x00, 0x00, 0x07, 0xb1, 0x65, 0x00, 0x00, 0x00, 0x00, 0x12, 0xcf,
+ 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0x51, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c,
+ 0xc9, 0xea, 0xe5, 0xad, 0x20, 0x00, 0x00, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x1e, 0xa5, 0xde, 0xea, 0xc5, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x2c, 0xfc, 0x09, 0x00, 0x00, 0x00, 0x00, 0x39, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x62, 0xc5,
+ 0x00, 0x00, 0x81, 0xc9, 0x04, 0x00, 0x00, 0x00, 0x87, 0xbb, 0x01, 0x00, 0x28, 0xf8, 0x29, 0x00,
+ 0x00, 0x00, 0x00, 0x6f, 0xc4, 0x01, 0x00, 0x02, 0x41, 0x41, 0x41, 0x41, 0x41, 0x49, 0xe8, 0x72,
+ 0x00, 0x4e, 0xe4, 0x93, 0x93, 0x11, 0x00, 0x20, 0xee, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x7f, 0x93, 0xc8, 0x8f, 0x00, 0x00, 0x00, 0x75, 0xae, 0xe3, 0x14, 0x00, 0x00, 0x00, 0x03, 0x23,
+ 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x0d, 0x00, 0xae, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x09,
+ 0x35, 0x12, 0x1b, 0x8f, 0xd4, 0x03, 0x00, 0x89, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x1f, 0xeb, 0xb4, 0x47, 0x13, 0x3e, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x05, 0xf9, 0x33, 0x00, 0x00, 0x00, 0x05, 0xec, 0x96, 0x00, 0x00, 0x00, 0x00, 0x94, 0x96,
+ 0x00, 0x00, 0x05, 0xcd, 0x78, 0x00, 0x00, 0x37, 0xec, 0x1d, 0x00, 0x00, 0x00, 0x95, 0xa9, 0x00,
+ 0x00, 0x00, 0x0c, 0xe7, 0x35, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa4, 0xaa, 0x01,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc4, 0x51, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x00, 0x14, 0xe3, 0x19, 0x7d, 0x93, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0xe0, 0x76, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x05, 0xef, 0x2b, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0xa3, 0xb3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0xd2, 0x62, 0x00, 0x00, 0x00, 0x47, 0xee, 0xe7, 0x02, 0x00, 0x00, 0x00, 0xc7, 0x66,
+ 0x00, 0x00, 0x00, 0x2c, 0xf1, 0x2b, 0x08, 0xd6, 0x5d, 0x00, 0x00, 0x00, 0x00, 0x15, 0xee, 0x35,
+ 0x00, 0x00, 0x81, 0xa2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x66, 0xd9, 0x0d, 0x00,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6b, 0xab, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x00, 0x92, 0x7e, 0x00, 0x0a, 0xdf, 0x27, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0xc2, 0x3d, 0x00, 0x00, 0x00,
+ 0x03, 0x27, 0x3b, 0x24, 0xd7, 0x44, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0xe6, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x9f, 0x96, 0x00, 0x00, 0x00, 0x9c, 0x7a, 0xe1, 0x3e, 0x00, 0x00, 0x04, 0xf5, 0x31,
+ 0x00, 0x00, 0x00, 0x00, 0x75, 0xcb, 0x8e, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x69, 0xcd,
+ 0x03, 0x23, 0xe7, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0xee, 0x2f, 0x00, 0x00,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0xf2, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x26, 0xe0, 0x0b, 0x00, 0x00, 0x61, 0xb0, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x45,
+ 0xde, 0xd0, 0xb7, 0xce, 0xfc, 0x45, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x08, 0xfe, 0x2b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x6a, 0xca, 0x00, 0x00, 0x05, 0xec, 0x22, 0x8d, 0x92, 0x00, 0x00, 0x2d, 0xf5, 0x05,
+ 0x00, 0x00, 0x00, 0x00, 0x03, 0xcb, 0xf2, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xc7,
+ 0x6e, 0xb5, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xd0, 0x6d, 0x00, 0x00, 0x00,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb7, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0xaf, 0x62, 0x00, 0x00, 0x00, 0x02, 0xd0, 0x3f, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xf0,
+ 0x54, 0x00, 0x00, 0x00, 0xd4, 0x45, 0x00, 0x8a, 0xb6, 0xa9, 0xdd, 0xd7, 0x9c, 0x15, 0x00, 0x00,
+ 0x00, 0xf0, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x34, 0xf9, 0x09, 0x00, 0x48, 0xc6, 0x00, 0x33, 0xe4, 0x03, 0x00, 0x66, 0xc3, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x1e, 0xeb, 0xe9, 0x52, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2e,
+ 0xf4, 0xc9, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87, 0xb9, 0x01, 0x00, 0x00, 0x00,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5e, 0xba, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x31, 0x03, 0x00, 0x00, 0x00, 0x00, 0x26, 0x0d, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0xfc,
+ 0x04, 0x00, 0x00, 0x00, 0xd4, 0x45, 0x00, 0x8a, 0xdb, 0x56, 0x19, 0x2e, 0x95, 0xe3, 0x11, 0x00,
+ 0x00, 0xbf, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x04, 0xf0, 0x41, 0x00, 0xa7, 0x6a, 0x00, 0x00, 0xd6, 0x4a, 0x00, 0xa5, 0x84, 0x00,
+ 0x00, 0x00, 0x00, 0x02, 0xbd, 0x79, 0x49, 0xe9, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xc0, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x35, 0xec, 0x1c, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0xf3, 0x18, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0xf1,
+ 0x58, 0x00, 0x00, 0x00, 0xd4, 0x45, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xb4, 0x83, 0x00,
+ 0x00, 0x42, 0xef, 0x4c, 0x01, 0x00, 0x0b, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0xb2, 0x7f, 0x12, 0xef, 0x12, 0x00, 0x00, 0x76, 0xaf, 0x00, 0xe4, 0x41, 0x00,
+ 0x00, 0x00, 0x00, 0x6f, 0xc7, 0x03, 0x00, 0x9b, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xbe, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xcf, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xaa, 0x6e, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4b,
+ 0xe4, 0xdf, 0xce, 0xdb, 0xf0, 0x39, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x5b, 0xc7, 0x00,
+ 0x00, 0x00, 0x50, 0xe1, 0xf1, 0xda, 0xee, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x6d, 0xc7, 0x6c, 0xa2, 0x00, 0x00, 0x00, 0x17, 0xf6, 0x3e, 0xf4, 0x08, 0x00,
+ 0x00, 0x00, 0x1c, 0xed, 0x29, 0x00, 0x00, 0x0f, 0xe4, 0x52, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xbe, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0xc3, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x51, 0xc8, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x03, 0x1f, 0x2c, 0x1a, 0x01, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x44, 0xe8, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x15, 0x23, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x1c, 0xfb, 0xd9, 0x3a, 0x00, 0x00, 0x00, 0x00, 0xaa, 0xd7, 0xad, 0x00, 0x00,
+ 0x00, 0x00, 0xa8, 0x82, 0x00, 0x00, 0x00, 0x00, 0x53, 0xdf, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xbe, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0xec, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xef, 0x23, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x80, 0xcd, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xc6, 0xd0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0xff, 0x5c, 0x00, 0x00,
+ 0x00, 0x40, 0xe4, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbd, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xbe, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x59, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xcf,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9e, 0x7d, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xd3, 0x96, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xd6, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x26, 0x8e, 0xf0, 0x19, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe7, 0x31, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6e, 0xf4, 0xdc, 0xd5, 0xfd, 0xc8, 0x2d, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x91, 0x8b, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x1d, 0x2d, 0x0f, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0xe3, 0x02, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x4e, 0xec, 0xb6, 0xb6, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xdc, 0x40, 0x00,
+ 0x9d, 0xb6, 0xda, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x0b, 0x23, 0x23, 0x23, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0x10, 0x00,
+ 0x1e, 0x23, 0x23, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x0c, 0x00, 0x00, 0x00, 0x24, 0xae, 0xe4,
+ 0xa9, 0x40, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x11, 0x00, 0x00, 0x00, 0x00, 0x17, 0x98,
+ 0xd1, 0xe4, 0xce, 0xa5, 0x20, 0x00, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14,
+ 0x14, 0x00, 0x00, 0x00, 0x00, 0x14, 0x14, 0x00, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x02, 0x0e, 0x00, 0x00, 0x48, 0xb3, 0xd3, 0xe5, 0xa4, 0x3e, 0x2f, 0xa4, 0xdd, 0xdb, 0xa5,
+ 0x17, 0x00, 0x00, 0x48, 0xb1, 0xd2, 0xe6, 0xcf, 0x8e, 0x09, 0x00, 0x00, 0x00, 0x00, 0x1b, 0xa3,
+ 0xdf, 0xd8, 0x94, 0x0e, 0x00, 0x00, 0x49, 0xb7, 0xd7, 0xe7, 0xc5, 0x80, 0x08, 0x00, 0x00, 0x00,
+ 0x00, 0x11, 0x8f, 0xcb, 0xe7, 0xd4, 0xb1, 0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xab, 0x6c, 0x00, 0x00, 0x1f, 0xed, 0x72, 0x18,
+ 0x55, 0xd2, 0x8a, 0x00, 0x00, 0x00, 0x56, 0xbc, 0xe7, 0xec, 0x4e, 0x00, 0x00, 0x1d, 0xe9, 0x93,
+ 0x30, 0x18, 0x2f, 0xe3, 0x41, 0x00, 0x89, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb6,
+ 0xb6, 0x00, 0x00, 0x00, 0x00, 0xb6, 0xb6, 0x00, 0x89, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8d, 0x8a, 0x00, 0x00, 0x8a, 0xab, 0x24, 0x18, 0x43, 0xe0, 0xe3, 0x5f, 0x20, 0x29, 0xa4,
+ 0xc8, 0x00, 0x00, 0x8a, 0xac, 0x25, 0x17, 0x35, 0xbd, 0xab, 0x00, 0x00, 0x00, 0x1b, 0xea, 0xb6,
+ 0x3e, 0x4d, 0xc7, 0xd6, 0x09, 0x00, 0x8a, 0xac, 0x25, 0x1c, 0x6c, 0xd3, 0xcf, 0x07, 0x00, 0x00,
+ 0x15, 0xe3, 0xc8, 0x60, 0x19, 0x29, 0xc3, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0xa5, 0x8c, 0x00, 0x00,
+ 0x00, 0x46, 0xf7, 0x0d, 0x00, 0x0a, 0xf1, 0x45, 0x03, 0x01, 0x04, 0x00, 0x00, 0xa4, 0xa1, 0x00,
+ 0x00, 0x00, 0x00, 0xd8, 0x41, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33,
+ 0x33, 0x00, 0x00, 0x00, 0x00, 0x33, 0x33, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8f, 0x8a, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x68, 0xbf, 0x00, 0x00, 0x00, 0x11,
+ 0xfb, 0x1f, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x1d, 0xfa, 0x18, 0x00, 0x00, 0x9f, 0xb8, 0x00,
+ 0x00, 0x00, 0x00, 0xdb, 0x78, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x03, 0xe8, 0x78, 0x00, 0x00,
+ 0x9b, 0xcd, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0xe3, 0x4b, 0x16, 0x16,
+ 0x16, 0x1c, 0xf5, 0x2f, 0x00, 0x4d, 0xc5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x3e, 0x00,
+ 0x00, 0x00, 0x00, 0xd8, 0x41, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8f, 0x8a, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x3d, 0xe0, 0x00, 0x00, 0x00, 0x00,
+ 0xe2, 0x3a, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xe4, 0x3a, 0x00, 0x00, 0xe4, 0x6e, 0x00,
+ 0x00, 0x00, 0x00, 0x90, 0xbf, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x98, 0xc1, 0x00, 0x00,
+ 0xe2, 0x79, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x08, 0xfe, 0xdd, 0xd8, 0xd8,
+ 0xd8, 0xd8, 0xd8, 0x3a, 0x00, 0x87, 0x92, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xfe, 0x24, 0x00,
+ 0x00, 0x00, 0x00, 0xd8, 0x41, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x69,
+ 0x6d, 0x00, 0x00, 0x00, 0x00, 0x69, 0x6d, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8f, 0x8a, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x31, 0xe9, 0x00, 0x00, 0x00, 0x00,
+ 0xd5, 0x45, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xd2, 0x49, 0x00, 0x08, 0xfe, 0x2e, 0x00,
+ 0x00, 0x00, 0x00, 0x50, 0xe3, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x4b, 0xe7, 0x00, 0x08,
+ 0xfe, 0x2d, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x22, 0xa8, 0xdd, 0xd8, 0x9d, 0xc7, 0x6c, 0x00, 0x00, 0xf3, 0x4f, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0xe4, 0xc2, 0xc2, 0xc2, 0x10, 0x00, 0x00, 0xe3, 0x45, 0x00,
+ 0x00, 0x00, 0x00, 0xd8, 0x41, 0x00, 0x8a, 0xcf, 0xc2, 0xe4, 0xce, 0x8e, 0x0a, 0x00, 0x00, 0x8a,
+ 0x8f, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x0d, 0xad, 0x43, 0x00,
+ 0x00, 0x8f, 0x8a, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x30, 0xe9, 0x00, 0x00, 0x00, 0x00,
+ 0xd4, 0x45, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x49, 0x00, 0x00, 0xee, 0x5b, 0x00,
+ 0x00, 0x00, 0x00, 0x7e, 0xca, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x50, 0xd2, 0x00, 0x00,
+ 0xf2, 0x32, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x24, 0xed, 0x7f, 0x28, 0x1d, 0x62, 0xe8, 0x6c, 0x00, 0x00, 0xc3, 0xa2, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0xa2, 0x2b, 0x2b, 0x2b, 0x04, 0x00, 0x00, 0xa3, 0xaf, 0x00,
+ 0x00, 0x00, 0x00, 0xd9, 0x41, 0x00, 0x8a, 0xba, 0x37, 0x18, 0x35, 0xbd, 0xae, 0x00, 0x00, 0x8a,
+ 0x8f, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x8a, 0x8f, 0x00, 0x09, 0xbd, 0x7e, 0x00, 0x00,
+ 0x00, 0x8f, 0x8a, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x30, 0xe9, 0x00, 0x00, 0x00, 0x00,
+ 0xd4, 0x45, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x49, 0x00, 0x00, 0xba, 0xa5, 0x00,
+ 0x00, 0x00, 0x00, 0xc8, 0x94, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x93, 0xa0, 0x00, 0x00,
+ 0xc2, 0x74, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0xa5, 0x95, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0x46, 0xf1, 0x75, 0x1b,
+ 0x00, 0x03, 0x18, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xe1, 0xb6,
+ 0x62, 0x6e, 0xb6, 0xff, 0x41, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x1d, 0xfb, 0x18, 0x00, 0x8a,
+ 0x8f, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x8a, 0x8f, 0x08, 0xb8, 0x85, 0x00, 0x00, 0x00,
+ 0x00, 0x8f, 0x8a, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x30, 0xe9, 0x00, 0x00, 0x00, 0x00,
+ 0xd4, 0x45, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x49, 0x00, 0x00, 0x36, 0xf2, 0x77,
+ 0x0f, 0x18, 0x87, 0xf1, 0x1a, 0x00, 0x8a, 0xbf, 0x18, 0x00, 0x00, 0x4f, 0xf2, 0x28, 0x00, 0x00,
+ 0x44, 0xeb, 0x39, 0x00, 0x00, 0x22, 0xd4, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0xe8, 0x3d, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0x00, 0x4d, 0xdd, 0xfc,
+ 0xdb, 0xf2, 0x7a, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x6f,
+ 0x9e, 0x9c, 0x55, 0xde, 0x3d, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xe4, 0x3a, 0x00, 0x8a,
+ 0x8f, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x8a, 0x97, 0xb6, 0x88, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8f, 0x8a, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x30, 0xe9, 0x00, 0x00, 0x00, 0x00,
+ 0xd4, 0x45, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x49, 0x00, 0x00, 0x00, 0x48, 0xe3,
+ 0xf0, 0xf7, 0xd4, 0x32, 0x00, 0x00, 0x8a, 0xd3, 0xe7, 0xd5, 0xea, 0xdd, 0x40, 0x00, 0x00, 0x00,
+ 0x00, 0x56, 0xe8, 0xe4, 0xd9, 0xdf, 0xdc, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x08, 0xff, 0x25, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+ 0x25, 0x09, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x27, 0xff, 0x1c, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xd2, 0x49, 0x00, 0x8a,
+ 0x8f, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x8a, 0xdf, 0xf4, 0x38, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8f, 0x8a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x1e, 0x17, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x02, 0x20, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x1f, 0x1b, 0x01, 0xad, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0xed, 0x62, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x09, 0x00,
+ 0x00, 0x1f, 0x9e, 0xcc, 0x01, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x49, 0x00, 0x8a,
+ 0x8f, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x8a, 0x8f, 0x4f, 0xed, 0x48, 0x00, 0x00, 0x00,
+ 0x00, 0x8f, 0x8a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0xb8, 0xb5, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x63, 0xf2, 0xd8,
+ 0xdc, 0xd3, 0x8e, 0x22, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x49, 0x00, 0x8a,
+ 0x8f, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x8a, 0x8f, 0x00, 0x3e, 0xed, 0x3b, 0x00, 0x00,
+ 0x00, 0x8e, 0x8b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x31, 0xf6, 0x7f, 0x1c, 0x00, 0x00, 0xae, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x22,
+ 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x49, 0x00, 0x8a,
+ 0x8f, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8e, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x52, 0xe7, 0x1d, 0x00,
+ 0x00, 0x7b, 0xab, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x3f, 0xd8, 0xfc, 0xd2, 0xe0, 0xf1, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x49, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xa6, 0x69, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x92, 0xbc, 0x01,
+ 0x00, 0x1f, 0xdf, 0xa9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x2d, 0x1a, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x1c, 0xe0, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x03, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x03, 0xd1, 0xe1, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x1e, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x3d, 0xac, 0xd3, 0xe8, 0xd1, 0x0b, 0x00, 0x00, 0x2a, 0x91, 0xdc, 0xe5, 0xbd, 0x0a, 0x00,
+ 0x3e, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7d, 0x59, 0x00, 0x00, 0x00, 0x00, 0xb2, 0x24, 0x00,
+ 0x6b, 0x76, 0x00, 0x00, 0x00, 0x00, 0x4f, 0x86, 0x00, 0x62, 0x7e, 0x00, 0x00, 0x00, 0x01, 0xb6,
+ 0x24, 0x00, 0x00, 0x00, 0x54, 0x7c, 0x00, 0x17, 0xbc, 0x26, 0x00, 0x00, 0x00, 0x92, 0x57, 0x00,
+ 0x6b, 0x77, 0x00, 0x00, 0x00, 0x00, 0x6a, 0x6a, 0x00, 0x14, 0xc2, 0xc2, 0xc2, 0xc2, 0xc2, 0xaf,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x07, 0x00, 0x15, 0x34, 0x00, 0x0d, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x15, 0x1a, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x1c, 0xb6, 0xb6, 0xb6,
+ 0xb6, 0xb6, 0xb6, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8a, 0xab, 0x1f, 0x13, 0x2a, 0x00, 0x00, 0x00, 0xc2, 0x66, 0x0a, 0x16, 0x44, 0x00, 0x00,
+ 0x9c, 0x7d, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x75, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x30, 0x00,
+ 0x4d, 0xd4, 0x00, 0x00, 0x00, 0x00, 0xa3, 0x6f, 0x00, 0x41, 0xda, 0x00, 0x00, 0x00, 0x27, 0xff,
+ 0x5f, 0x00, 0x00, 0x00, 0xa5, 0x63, 0x00, 0x00, 0x7b, 0xc6, 0x03, 0x00, 0x5c, 0xd0, 0x06, 0x00,
+ 0x4d, 0xd8, 0x00, 0x00, 0x00, 0x00, 0xc1, 0x51, 0x00, 0x04, 0x2b, 0x2b, 0x2b, 0x2d, 0xcd, 0x96,
+ 0x00, 0x00, 0x00, 0x00, 0x11, 0xba, 0xd8, 0x35, 0x00, 0x4e, 0xbe, 0x00, 0xb0, 0xdc, 0x48, 0x00,
+ 0x00, 0x00, 0x00, 0x71, 0xdc, 0xde, 0xb6, 0x21, 0x00, 0x60, 0x8a, 0x00, 0x27, 0xbc, 0x23, 0x23,
+ 0x23, 0x23, 0x53, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0xfa, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x9c, 0x7d, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x75, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x30, 0x00,
+ 0x0b, 0xf6, 0x1c, 0x00, 0x00, 0x02, 0xe8, 0x25, 0x00, 0x06, 0xf2, 0x1d, 0x00, 0x00, 0x6a, 0xe9,
+ 0x9d, 0x00, 0x00, 0x01, 0xe5, 0x1b, 0x00, 0x00, 0x03, 0xc6, 0x76, 0x1c, 0xe7, 0x2d, 0x00, 0x00,
+ 0x0b, 0xf5, 0x26, 0x00, 0x00, 0x07, 0xf4, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc1, 0x05,
+ 0x00, 0x00, 0x00, 0x00, 0x78, 0xaa, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x46, 0xdb, 0x00,
+ 0x00, 0x00, 0x0a, 0xdb, 0x0c, 0x02, 0x69, 0xe5, 0xc0, 0xdb, 0x21, 0x00, 0x27, 0xb1, 0x00, 0x00,
+ 0x00, 0x00, 0x38, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xd4, 0xa4, 0x1b, 0x00, 0x00, 0x00, 0x00,
+ 0x9c, 0xe0, 0xc2, 0xc2, 0xbf, 0x00, 0x00, 0xa4, 0x75, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x30, 0x00,
+ 0x00, 0xad, 0x6b, 0x00, 0x00, 0x3c, 0xce, 0x00, 0x00, 0x00, 0xb1, 0x67, 0x00, 0x00, 0xb1, 0x67,
+ 0xe4, 0x01, 0x00, 0x31, 0xd1, 0x00, 0x00, 0x00, 0x00, 0x25, 0xec, 0xc9, 0x75, 0x00, 0x00, 0x00,
+ 0x00, 0xa8, 0x7b, 0x00, 0x00, 0x3e, 0xcc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0xe4, 0x19, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x9e, 0x70, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x08, 0xfe, 0x06,
+ 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, 0x2e, 0x0a, 0x00, 0x00, 0x27, 0xb1, 0x00, 0x00,
+ 0x00, 0x00, 0x38, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x99, 0xf2, 0xa7, 0x26, 0x00, 0x00,
+ 0x9c, 0x93, 0x2b, 0x2b, 0x2a, 0x00, 0x00, 0xa4, 0x75, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x30, 0x00,
+ 0x00, 0x56, 0xc1, 0x00, 0x00, 0x91, 0x75, 0x00, 0x00, 0x00, 0x6a, 0xb5, 0x00, 0x0a, 0xe7, 0x08,
+ 0xd7, 0x32, 0x00, 0x7d, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb2, 0xfc, 0x24, 0x00, 0x00, 0x00,
+ 0x00, 0x4d, 0xd9, 0x01, 0x00, 0x81, 0x8a, 0x00, 0x00, 0x00, 0x00, 0x13, 0xe4, 0x44, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xa4, 0x69, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x01, 0xff, 0x0d,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0xb1, 0x00, 0x00,
+ 0x00, 0x00, 0x38, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x84, 0xf1, 0x2b, 0x00,
+ 0x9c, 0x7d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9b, 0x84, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x30, 0x00,
+ 0x00, 0x08, 0xec, 0x22, 0x05, 0xe4, 0x19, 0x00, 0x00, 0x00, 0x17, 0xf4, 0x11, 0x51, 0xa7, 0x00,
+ 0x84, 0x84, 0x00, 0xd1, 0x31, 0x00, 0x00, 0x00, 0x00, 0x50, 0xd4, 0x80, 0xc8, 0x04, 0x00, 0x00,
+ 0x00, 0x04, 0xe1, 0x40, 0x00, 0xc9, 0x3c, 0x00, 0x00, 0x00, 0x00, 0xab, 0x8e, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xa4, 0x68, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0xff, 0x0d,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0xb1, 0x00, 0x00,
+ 0x00, 0x00, 0x38, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa6, 0x7f, 0x00,
+ 0x9c, 0x7d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xab, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x30, 0x00,
+ 0x00, 0x00, 0x8f, 0x86, 0x52, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0xad, 0x66, 0xa8, 0x50, 0x00,
+ 0x2d, 0xdc, 0x2c, 0xcd, 0x00, 0x00, 0x00, 0x00, 0x16, 0xe4, 0x2f, 0x02, 0xc3, 0x7d, 0x00, 0x00,
+ 0x00, 0x00, 0x77, 0xad, 0x1a, 0xdd, 0x01, 0x00, 0x00, 0x00, 0x57, 0xda, 0x08, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xa4, 0x64, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0xfc, 0x0d,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0xb1, 0x00, 0x00,
+ 0x00, 0x00, 0x38, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x14, 0x00, 0x00, 0x10, 0xe4, 0x39, 0x00,
+ 0x9c, 0x7d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0xf7, 0x3f, 0x00, 0x00, 0x02, 0xeb, 0x30, 0x00,
+ 0x00, 0x00, 0x25, 0xea, 0xc8, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49, 0xd3, 0xe2, 0x06, 0x00,
+ 0x00, 0xce, 0xc6, 0x6a, 0x00, 0x00, 0x00, 0x00, 0x9f, 0x81, 0x00, 0x00, 0x24, 0xef, 0x27, 0x00,
+ 0x00, 0x00, 0x10, 0xed, 0x92, 0x86, 0x00, 0x00, 0x00, 0x0c, 0xe3, 0x45, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xc0, 0x49, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0xe3, 0x26,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0xb1, 0x00, 0x00,
+ 0x00, 0x00, 0x38, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2d, 0xf2, 0xd7, 0xd5, 0xdf, 0x8c, 0x00, 0x00,
+ 0x9b, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5b, 0xeb, 0xe6, 0xdc, 0xf1, 0xda, 0x22, 0x00,
+ 0x00, 0x00, 0x00, 0xae, 0xcf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe1, 0x8d, 0x00, 0x00,
+ 0x00, 0x68, 0xf6, 0x10, 0x00, 0x00, 0x00, 0x3b, 0xdc, 0x08, 0x00, 0x00, 0x00, 0x7e, 0xb3, 0x00,
+ 0x00, 0x00, 0x00, 0x7f, 0xff, 0x2c, 0x00, 0x00, 0x00, 0x58, 0xfe, 0xee, 0xee, 0xee, 0xee, 0xee,
+ 0x0c, 0x00, 0x17, 0x8c, 0xd4, 0x09, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x76, 0xc9,
+ 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0xb1, 0x00, 0x00,
+ 0x00, 0x00, 0x38, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x20, 0x21, 0x00, 0x00, 0x00, 0x00,
+ 0x8a, 0x8e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x1d, 0x08, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x53, 0xc7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x26, 0xc3, 0xaf, 0x02, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x4e, 0xe4,
+ 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0xb1, 0x00, 0x00,
+ 0x00, 0x00, 0x38, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x5c, 0xd5, 0x0b, 0x00, 0x0f, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x17, 0xd9, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x02, 0xd3, 0x3d, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0xd7, 0x3b,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0xb1, 0x00, 0x00,
+ 0x00, 0x00, 0x38, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x08, 0xb5, 0xef, 0xd9, 0xec, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xc2, 0xc7, 0xe5, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xa5, 0x60, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0xf9, 0x0e,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0xb1, 0x00, 0x00,
+ 0x00, 0x00, 0x38, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x14, 0x24, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x14, 0x25, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xa4, 0x68, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0xff, 0x0d,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0xf3, 0xd8, 0xd8,
+ 0xd8, 0xd8, 0xe1, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xa4, 0x68, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0xff, 0x0d,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xa1, 0x6d, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x05, 0xff, 0x09,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x87, 0x8f, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x2a, 0xeb, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x24, 0xe1, 0xa3, 0x26, 0x00, 0x4e, 0xbe, 0x00, 0x80, 0xd9, 0x71, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x3c, 0x16, 0x00, 0x0b, 0x1a, 0x00, 0x3e, 0x20, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+];
+
diff --git a/gfx/wr/webrender/src/debug_item.rs b/gfx/wr/webrender/src/debug_item.rs
new file mode 100644
index 0000000000..1efea167ff
--- /dev/null
+++ b/gfx/wr/webrender/src/debug_item.rs
@@ -0,0 +1,26 @@
+/* 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 api::{units::*, ColorF};
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum DebugItem {
+ Text {
+ msg: String,
+ color: ColorF,
+ position: DevicePoint,
+ },
+ Rect {
+ outer_color: ColorF,
+ inner_color: ColorF,
+ rect: DeviceRect,
+ },
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct DebugMessage {
+ pub msg: String,
+ pub timestamp: u64,
+}
diff --git a/gfx/wr/webrender/src/device/gl.rs b/gfx/wr/webrender/src/device/gl.rs
new file mode 100644
index 0000000000..f240083bf7
--- /dev/null
+++ b/gfx/wr/webrender/src/device/gl.rs
@@ -0,0 +1,4814 @@
+/* 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 super::super::shader_source::{OPTIMIZED_SHADERS, UNOPTIMIZED_SHADERS};
+use api::{ImageDescriptor, ImageFormat, Parameter, BoolParameter, IntParameter, ImageRendering};
+use api::{MixBlendMode, ImageBufferKind, VoidPtrToSizeFn};
+use api::{CrashAnnotator, CrashAnnotation, CrashAnnotatorGuard};
+use api::units::*;
+use euclid::default::Transform3D;
+use gleam::gl;
+use crate::render_api::MemoryReport;
+use crate::internal_types::{FastHashMap, RenderTargetInfo, Swizzle, SwizzleSettings};
+use crate::util::round_up_to_multiple;
+use crate::profiler;
+use log::Level;
+use smallvec::SmallVec;
+use std::{
+ borrow::Cow,
+ cell::{Cell, RefCell},
+ cmp,
+ collections::hash_map::Entry,
+ marker::PhantomData,
+ mem,
+ num::NonZeroUsize,
+ os::raw::c_void,
+ ops::Add,
+ path::PathBuf,
+ ptr,
+ rc::Rc,
+ slice,
+ sync::Arc,
+ thread,
+ time::Duration,
+};
+use webrender_build::shader::{
+ ProgramSourceDigest, ShaderKind, ShaderVersion, build_shader_main_string,
+ build_shader_prefix_string, do_build_shader_string, shader_source_from_file,
+};
+use malloc_size_of::MallocSizeOfOps;
+
+/// Sequence number for frames, as tracked by the device layer.
+#[derive(Debug, Copy, Clone, PartialEq, Ord, Eq, PartialOrd)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct GpuFrameId(usize);
+
+impl GpuFrameId {
+ pub fn new(value: usize) -> Self {
+ GpuFrameId(value)
+ }
+}
+
+impl Add<usize> for GpuFrameId {
+ type Output = GpuFrameId;
+
+ fn add(self, other: usize) -> GpuFrameId {
+ GpuFrameId(self.0 + other)
+ }
+}
+
+pub struct TextureSlot(pub usize);
+
+// In some places we need to temporarily bind a texture to any slot.
+const DEFAULT_TEXTURE: TextureSlot = TextureSlot(0);
+
+#[repr(u32)]
+pub enum DepthFunction {
+ Always = gl::ALWAYS,
+ Less = gl::LESS,
+ LessEqual = gl::LEQUAL,
+}
+
+#[repr(u32)]
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum TextureFilter {
+ Nearest,
+ Linear,
+ Trilinear,
+}
+
+/// A structure defining a particular workflow of texture transfers.
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct TextureFormatPair<T> {
+ /// Format the GPU natively stores texels in.
+ pub internal: T,
+ /// Format we expect the users to provide the texels in.
+ pub external: T,
+}
+
+impl<T: Copy> From<T> for TextureFormatPair<T> {
+ fn from(value: T) -> Self {
+ TextureFormatPair {
+ internal: value,
+ external: value,
+ }
+ }
+}
+
+#[derive(Debug)]
+pub enum VertexAttributeKind {
+ F32,
+ U8Norm,
+ U16Norm,
+ I32,
+ U16,
+}
+
+#[derive(Debug)]
+pub struct VertexAttribute {
+ pub name: &'static str,
+ pub count: u32,
+ pub kind: VertexAttributeKind,
+}
+
+#[derive(Debug)]
+pub struct VertexDescriptor {
+ pub vertex_attributes: &'static [VertexAttribute],
+ pub instance_attributes: &'static [VertexAttribute],
+}
+
+enum FBOTarget {
+ Read,
+ Draw,
+}
+
+/// Method of uploading texel data from CPU to GPU.
+#[derive(Debug, Clone)]
+pub enum UploadMethod {
+ /// Just call `glTexSubImage` directly with the CPU data pointer
+ Immediate,
+ /// Accumulate the changes in PBO first before transferring to a texture.
+ PixelBuffer(VertexUsageHint),
+}
+
+/// Plain old data that can be used to initialize a texture.
+pub unsafe trait Texel: Copy {}
+unsafe impl Texel for u8 {}
+unsafe impl Texel for f32 {}
+
+/// Returns the size in bytes of a depth target with the given dimensions.
+fn depth_target_size_in_bytes(dimensions: &DeviceIntSize) -> usize {
+ // DEPTH24 textures generally reserve 3 bytes for depth and 1 byte
+ // for stencil, so we measure them as 32 bits.
+ let pixels = dimensions.width * dimensions.height;
+ (pixels as usize) * 4
+}
+
+pub fn get_gl_target(target: ImageBufferKind) -> gl::GLuint {
+ match target {
+ ImageBufferKind::Texture2D => gl::TEXTURE_2D,
+ ImageBufferKind::TextureRect => gl::TEXTURE_RECTANGLE,
+ ImageBufferKind::TextureExternal => gl::TEXTURE_EXTERNAL_OES,
+ }
+}
+
+pub fn from_gl_target(target: gl::GLuint) -> ImageBufferKind {
+ match target {
+ gl::TEXTURE_2D => ImageBufferKind::Texture2D,
+ gl::TEXTURE_RECTANGLE => ImageBufferKind::TextureRect,
+ gl::TEXTURE_EXTERNAL_OES => ImageBufferKind::TextureExternal,
+ _ => panic!("Unexpected target {:?}", target),
+ }
+}
+
+fn supports_extension(extensions: &[String], extension: &str) -> bool {
+ extensions.iter().any(|s| s == extension)
+}
+
+fn get_shader_version(gl: &dyn gl::Gl) -> ShaderVersion {
+ match gl.get_type() {
+ gl::GlType::Gl => ShaderVersion::Gl,
+ gl::GlType::Gles => ShaderVersion::Gles,
+ }
+}
+
+// Get an unoptimized shader string by name, from the built in resources or
+// an override path, if supplied.
+pub fn get_unoptimized_shader_source(shader_name: &str, base_path: Option<&PathBuf>) -> Cow<'static, str> {
+ if let Some(ref base) = base_path {
+ let shader_path = base.join(&format!("{}.glsl", shader_name));
+ Cow::Owned(shader_source_from_file(&shader_path))
+ } else {
+ Cow::Borrowed(
+ UNOPTIMIZED_SHADERS
+ .get(shader_name)
+ .expect("Shader not found")
+ .source
+ )
+ }
+}
+
+pub trait FileWatcherHandler: Send {
+ fn file_changed(&self, path: PathBuf);
+}
+
+impl VertexAttributeKind {
+ fn size_in_bytes(&self) -> u32 {
+ match *self {
+ VertexAttributeKind::F32 => 4,
+ VertexAttributeKind::U8Norm => 1,
+ VertexAttributeKind::U16Norm => 2,
+ VertexAttributeKind::I32 => 4,
+ VertexAttributeKind::U16 => 2,
+ }
+ }
+}
+
+impl VertexAttribute {
+ fn size_in_bytes(&self) -> u32 {
+ self.count * self.kind.size_in_bytes()
+ }
+
+ fn bind_to_vao(
+ &self,
+ attr_index: gl::GLuint,
+ divisor: gl::GLuint,
+ stride: gl::GLint,
+ offset: gl::GLuint,
+ gl: &dyn gl::Gl,
+ ) {
+ gl.enable_vertex_attrib_array(attr_index);
+ gl.vertex_attrib_divisor(attr_index, divisor);
+
+ match self.kind {
+ VertexAttributeKind::F32 => {
+ gl.vertex_attrib_pointer(
+ attr_index,
+ self.count as gl::GLint,
+ gl::FLOAT,
+ false,
+ stride,
+ offset,
+ );
+ }
+ VertexAttributeKind::U8Norm => {
+ gl.vertex_attrib_pointer(
+ attr_index,
+ self.count as gl::GLint,
+ gl::UNSIGNED_BYTE,
+ true,
+ stride,
+ offset,
+ );
+ }
+ VertexAttributeKind::U16Norm => {
+ gl.vertex_attrib_pointer(
+ attr_index,
+ self.count as gl::GLint,
+ gl::UNSIGNED_SHORT,
+ true,
+ stride,
+ offset,
+ );
+ }
+ VertexAttributeKind::I32 => {
+ gl.vertex_attrib_i_pointer(
+ attr_index,
+ self.count as gl::GLint,
+ gl::INT,
+ stride,
+ offset,
+ );
+ }
+ VertexAttributeKind::U16 => {
+ gl.vertex_attrib_i_pointer(
+ attr_index,
+ self.count as gl::GLint,
+ gl::UNSIGNED_SHORT,
+ stride,
+ offset,
+ );
+ }
+ }
+ }
+}
+
+impl VertexDescriptor {
+ fn instance_stride(&self) -> u32 {
+ self.instance_attributes
+ .iter()
+ .map(|attr| attr.size_in_bytes())
+ .sum()
+ }
+
+ fn bind_attributes(
+ attributes: &[VertexAttribute],
+ start_index: usize,
+ divisor: u32,
+ gl: &dyn gl::Gl,
+ vbo: VBOId,
+ ) {
+ vbo.bind(gl);
+
+ let stride: u32 = attributes
+ .iter()
+ .map(|attr| attr.size_in_bytes())
+ .sum();
+
+ let mut offset = 0;
+ for (i, attr) in attributes.iter().enumerate() {
+ let attr_index = (start_index + i) as gl::GLuint;
+ attr.bind_to_vao(attr_index, divisor, stride as _, offset, gl);
+ offset += attr.size_in_bytes();
+ }
+ }
+
+ fn bind(&self, gl: &dyn gl::Gl, main: VBOId, instance: VBOId, instance_divisor: u32) {
+ Self::bind_attributes(self.vertex_attributes, 0, 0, gl, main);
+
+ if !self.instance_attributes.is_empty() {
+ Self::bind_attributes(
+ self.instance_attributes,
+ self.vertex_attributes.len(),
+ instance_divisor,
+ gl,
+ instance,
+ );
+ }
+ }
+}
+
+impl VBOId {
+ fn bind(&self, gl: &dyn gl::Gl) {
+ gl.bind_buffer(gl::ARRAY_BUFFER, self.0);
+ }
+}
+
+impl IBOId {
+ fn bind(&self, gl: &dyn gl::Gl) {
+ gl.bind_buffer(gl::ELEMENT_ARRAY_BUFFER, self.0);
+ }
+}
+
+impl FBOId {
+ fn bind(&self, gl: &dyn gl::Gl, target: FBOTarget) {
+ let target = match target {
+ FBOTarget::Read => gl::READ_FRAMEBUFFER,
+ FBOTarget::Draw => gl::DRAW_FRAMEBUFFER,
+ };
+ gl.bind_framebuffer(target, self.0);
+ }
+}
+
+pub struct Stream<'a> {
+ attributes: &'a [VertexAttribute],
+ vbo: VBOId,
+}
+
+pub struct VBO<V> {
+ id: gl::GLuint,
+ target: gl::GLenum,
+ allocated_count: usize,
+ marker: PhantomData<V>,
+}
+
+impl<V> VBO<V> {
+ pub fn allocated_count(&self) -> usize {
+ self.allocated_count
+ }
+
+ pub fn stream_with<'a>(&self, attributes: &'a [VertexAttribute]) -> Stream<'a> {
+ debug_assert_eq!(
+ mem::size_of::<V>(),
+ attributes.iter().map(|a| a.size_in_bytes() as usize).sum::<usize>()
+ );
+ Stream {
+ attributes,
+ vbo: VBOId(self.id),
+ }
+ }
+}
+
+impl<T> Drop for VBO<T> {
+ fn drop(&mut self) {
+ debug_assert!(thread::panicking() || self.id == 0);
+ }
+}
+
+#[cfg_attr(feature = "replay", derive(Clone))]
+#[derive(Debug)]
+pub struct ExternalTexture {
+ id: gl::GLuint,
+ target: gl::GLuint,
+ uv_rect: TexelRect,
+ image_rendering: ImageRendering,
+}
+
+impl ExternalTexture {
+ pub fn new(
+ id: u32,
+ target: ImageBufferKind,
+ uv_rect: TexelRect,
+ image_rendering: ImageRendering,
+ ) -> Self {
+ ExternalTexture {
+ id,
+ target: get_gl_target(target),
+ uv_rect,
+ image_rendering,
+ }
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn internal_id(&self) -> gl::GLuint {
+ self.id
+ }
+
+ pub fn get_uv_rect(&self) -> TexelRect {
+ self.uv_rect
+ }
+}
+
+bitflags! {
+ #[derive(Default)]
+ pub struct TextureFlags: u32 {
+ /// This texture corresponds to one of the shared texture caches.
+ const IS_SHARED_TEXTURE_CACHE = 1 << 0;
+ }
+}
+
+/// WebRender interface to an OpenGL texture.
+///
+/// Because freeing a texture requires various device handles that are not
+/// reachable from this struct, manual destruction via `Device` is required.
+/// Our `Drop` implementation asserts that this has happened.
+#[derive(Debug)]
+pub struct Texture {
+ id: gl::GLuint,
+ target: gl::GLuint,
+ format: ImageFormat,
+ size: DeviceIntSize,
+ filter: TextureFilter,
+ flags: TextureFlags,
+ /// An internally mutable swizzling state that may change between batches.
+ active_swizzle: Cell<Swizzle>,
+ /// Framebuffer Object allowing this texture to be rendered to.
+ ///
+ /// Empty if this texture is not used as a render target or if a depth buffer is needed.
+ fbo: Option<FBOId>,
+ /// Same as the above, but with a depth buffer attached.
+ ///
+ /// FBOs are cheap to create but expensive to reconfigure (since doing so
+ /// invalidates framebuffer completeness caching). Moreover, rendering with
+ /// a depth buffer attached but the depth write+test disabled relies on the
+ /// driver to optimize it out of the rendering pass, which most drivers
+ /// probably do but, according to jgilbert, is best not to rely on.
+ ///
+ /// So we lazily generate a second list of FBOs with depth. This list is
+ /// empty if this texture is not used as a render target _or_ if it is, but
+ /// the depth buffer has never been requested.
+ ///
+ /// Note that we always fill fbo, and then lazily create fbo_with_depth
+ /// when needed. We could make both lazy (i.e. render targets would have one
+ /// or the other, but not both, unless they were actually used in both
+ /// configurations). But that would complicate a lot of logic in this module,
+ /// and FBOs are cheap enough to create.
+ fbo_with_depth: Option<FBOId>,
+ last_frame_used: GpuFrameId,
+}
+
+impl Texture {
+ pub fn get_dimensions(&self) -> DeviceIntSize {
+ self.size
+ }
+
+ pub fn get_format(&self) -> ImageFormat {
+ self.format
+ }
+
+ pub fn get_filter(&self) -> TextureFilter {
+ self.filter
+ }
+
+ pub fn get_target(&self) -> ImageBufferKind {
+ from_gl_target(self.target)
+ }
+
+ pub fn supports_depth(&self) -> bool {
+ self.fbo_with_depth.is_some()
+ }
+
+ pub fn last_frame_used(&self) -> GpuFrameId {
+ self.last_frame_used
+ }
+
+ pub fn used_in_frame(&self, frame_id: GpuFrameId) -> bool {
+ self.last_frame_used == frame_id
+ }
+
+ pub fn is_render_target(&self) -> bool {
+ self.fbo.is_some()
+ }
+
+ /// Returns true if this texture was used within `threshold` frames of
+ /// the current frame.
+ pub fn used_recently(&self, current_frame_id: GpuFrameId, threshold: usize) -> bool {
+ self.last_frame_used + threshold >= current_frame_id
+ }
+
+ /// Returns the flags for this texture.
+ pub fn flags(&self) -> &TextureFlags {
+ &self.flags
+ }
+
+ /// Returns a mutable borrow of the flags for this texture.
+ pub fn flags_mut(&mut self) -> &mut TextureFlags {
+ &mut self.flags
+ }
+
+ /// Returns the number of bytes (generally in GPU memory) that this texture
+ /// consumes.
+ pub fn size_in_bytes(&self) -> usize {
+ let bpp = self.format.bytes_per_pixel() as usize;
+ let w = self.size.width as usize;
+ let h = self.size.height as usize;
+ bpp * w * h
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn into_external(mut self) -> ExternalTexture {
+ let ext = ExternalTexture {
+ id: self.id,
+ target: self.target,
+ // TODO(gw): Support custom UV rect for external textures during captures
+ uv_rect: TexelRect::new(
+ 0.0,
+ 0.0,
+ self.size.width as f32,
+ self.size.height as f32,
+ ),
+ image_rendering: ImageRendering::Auto,
+ };
+ self.id = 0; // don't complain, moved out
+ ext
+ }
+}
+
+impl Drop for Texture {
+ fn drop(&mut self) {
+ debug_assert!(thread::panicking() || self.id == 0);
+ }
+}
+
+pub struct Program {
+ id: gl::GLuint,
+ u_transform: gl::GLint,
+ u_mode: gl::GLint,
+ u_texture_size: gl::GLint,
+ source_info: ProgramSourceInfo,
+ is_initialized: bool,
+}
+
+impl Program {
+ pub fn is_initialized(&self) -> bool {
+ self.is_initialized
+ }
+}
+
+impl Drop for Program {
+ fn drop(&mut self) {
+ debug_assert!(
+ thread::panicking() || self.id == 0,
+ "renderer::deinit not called"
+ );
+ }
+}
+
+pub struct CustomVAO {
+ id: gl::GLuint,
+}
+
+impl Drop for CustomVAO {
+ fn drop(&mut self) {
+ debug_assert!(
+ thread::panicking() || self.id == 0,
+ "renderer::deinit not called"
+ );
+ }
+}
+
+pub struct VAO {
+ id: gl::GLuint,
+ ibo_id: IBOId,
+ main_vbo_id: VBOId,
+ instance_vbo_id: VBOId,
+ instance_stride: usize,
+ instance_divisor: u32,
+ owns_vertices_and_indices: bool,
+}
+
+impl Drop for VAO {
+ fn drop(&mut self) {
+ debug_assert!(
+ thread::panicking() || self.id == 0,
+ "renderer::deinit not called"
+ );
+ }
+}
+
+#[derive(Debug)]
+pub struct PBO {
+ id: gl::GLuint,
+ reserved_size: usize,
+}
+
+impl PBO {
+ pub fn get_reserved_size(&self) -> usize {
+ self.reserved_size
+ }
+}
+
+impl Drop for PBO {
+ fn drop(&mut self) {
+ debug_assert!(
+ thread::panicking() || self.id == 0,
+ "renderer::deinit not called or PBO not returned to pool"
+ );
+ }
+}
+
+pub struct BoundPBO<'a> {
+ device: &'a mut Device,
+ pub data: &'a [u8]
+}
+
+impl<'a> Drop for BoundPBO<'a> {
+ fn drop(&mut self) {
+ self.device.gl.unmap_buffer(gl::PIXEL_PACK_BUFFER);
+ self.device.gl.bind_buffer(gl::PIXEL_PACK_BUFFER, 0);
+ }
+}
+
+#[derive(PartialEq, Eq, Hash, Debug, Copy, Clone)]
+pub struct FBOId(gl::GLuint);
+
+#[derive(PartialEq, Eq, Hash, Debug, Copy, Clone)]
+pub struct RBOId(gl::GLuint);
+
+#[derive(PartialEq, Eq, Hash, Debug, Copy, Clone)]
+pub struct VBOId(gl::GLuint);
+
+#[derive(PartialEq, Eq, Hash, Debug, Copy, Clone)]
+struct IBOId(gl::GLuint);
+
+#[derive(Clone, Debug)]
+enum ProgramSourceType {
+ Unoptimized,
+ Optimized(ShaderVersion),
+}
+
+#[derive(Clone, Debug)]
+pub struct ProgramSourceInfo {
+ base_filename: &'static str,
+ features: Vec<&'static str>,
+ full_name_cstr: Rc<std::ffi::CString>,
+ source_type: ProgramSourceType,
+ digest: ProgramSourceDigest,
+}
+
+impl ProgramSourceInfo {
+ fn new(
+ device: &Device,
+ name: &'static str,
+ features: &[&'static str],
+ ) -> Self {
+
+ // Compute the digest. Assuming the device has a `ProgramCache`, this
+ // will always be needed, whereas the source is rarely needed.
+
+ use std::collections::hash_map::DefaultHasher;
+ use std::hash::Hasher;
+
+ // Setup.
+ let mut hasher = DefaultHasher::new();
+ let gl_version = get_shader_version(&*device.gl());
+
+ // Hash the renderer name.
+ hasher.write(device.capabilities.renderer_name.as_bytes());
+
+ let full_name = Self::make_full_name(name, features);
+
+ let optimized_source = if device.use_optimized_shaders {
+ OPTIMIZED_SHADERS.get(&(gl_version, &full_name)).or_else(|| {
+ warn!("Missing optimized shader source for {}", &full_name);
+ None
+ })
+ } else {
+ None
+ };
+
+ let source_type = match optimized_source {
+ Some(source_and_digest) => {
+ // Optimized shader sources are used as-is, without any run-time processing.
+ // The vertex and fragment shaders are different, so must both be hashed.
+ // We use the hashes that were computed at build time, and verify it in debug builds.
+ if cfg!(debug_assertions) {
+ let mut h = DefaultHasher::new();
+ h.write(source_and_digest.vert_source.as_bytes());
+ h.write(source_and_digest.frag_source.as_bytes());
+ let d: ProgramSourceDigest = h.into();
+ let digest = d.to_string();
+ debug_assert_eq!(digest, source_and_digest.digest);
+ hasher.write(digest.as_bytes());
+ } else {
+ hasher.write(source_and_digest.digest.as_bytes());
+ }
+
+ ProgramSourceType::Optimized(gl_version)
+ }
+ None => {
+ // For non-optimized sources we compute the hash by walking the static strings
+ // in the same order as we would when concatenating the source, to avoid
+ // heap-allocating in the common case.
+ //
+ // Note that we cheat a bit to make the hashing more efficient. First, the only
+ // difference between the vertex and fragment shader is a single deterministic
+ // define, so we don't need to hash both. Second, we precompute the digest of the
+ // expanded source file at build time, and then just hash that digest here.
+ let override_path = device.resource_override_path.as_ref();
+ let source_and_digest = UNOPTIMIZED_SHADERS.get(&name).expect("Shader not found");
+
+ // Hash the prefix string.
+ build_shader_prefix_string(
+ gl_version,
+ &features,
+ ShaderKind::Vertex,
+ &name,
+ &mut |s| hasher.write(s.as_bytes()),
+ );
+
+ // Hash the shader file contents. We use a precomputed digest, and
+ // verify it in debug builds.
+ if override_path.is_some() || cfg!(debug_assertions) {
+ let mut h = DefaultHasher::new();
+ build_shader_main_string(
+ &name,
+ &|f| get_unoptimized_shader_source(f, override_path),
+ &mut |s| h.write(s.as_bytes())
+ );
+ let d: ProgramSourceDigest = h.into();
+ let digest = format!("{}", d);
+ debug_assert!(override_path.is_some() || digest == source_and_digest.digest);
+ hasher.write(digest.as_bytes());
+ } else {
+ hasher.write(source_and_digest.digest.as_bytes());
+ }
+
+ ProgramSourceType::Unoptimized
+ }
+ };
+
+ // Finish.
+ ProgramSourceInfo {
+ base_filename: name,
+ features: features.to_vec(),
+ full_name_cstr: Rc::new(std::ffi::CString::new(full_name).unwrap()),
+ source_type,
+ digest: hasher.into(),
+ }
+ }
+
+ fn compute_source(&self, device: &Device, kind: ShaderKind) -> String {
+ let full_name = self.full_name();
+ match self.source_type {
+ ProgramSourceType::Optimized(gl_version) => {
+ let shader = OPTIMIZED_SHADERS
+ .get(&(gl_version, &full_name))
+ .unwrap_or_else(|| panic!("Missing optimized shader source for {}", full_name));
+
+ match kind {
+ ShaderKind::Vertex => shader.vert_source.to_string(),
+ ShaderKind::Fragment => shader.frag_source.to_string(),
+ }
+ },
+ ProgramSourceType::Unoptimized => {
+ let mut src = String::new();
+ device.build_shader_string(
+ &self.features,
+ kind,
+ self.base_filename,
+ |s| src.push_str(s),
+ );
+ src
+ }
+ }
+ }
+
+ fn make_full_name(base_filename: &'static str, features: &[&'static str]) -> String {
+ if features.is_empty() {
+ base_filename.to_string()
+ } else {
+ format!("{}_{}", base_filename, features.join("_"))
+ }
+ }
+
+ fn full_name(&self) -> String {
+ Self::make_full_name(self.base_filename, &self.features)
+ }
+}
+
+#[cfg_attr(feature = "serialize_program", derive(Deserialize, Serialize))]
+pub struct ProgramBinary {
+ bytes: Vec<u8>,
+ format: gl::GLenum,
+ source_digest: ProgramSourceDigest,
+}
+
+impl ProgramBinary {
+ fn new(bytes: Vec<u8>,
+ format: gl::GLenum,
+ source_digest: ProgramSourceDigest) -> Self {
+ ProgramBinary {
+ bytes,
+ format,
+ source_digest,
+ }
+ }
+
+ /// Returns a reference to the source digest hash.
+ pub fn source_digest(&self) -> &ProgramSourceDigest {
+ &self.source_digest
+ }
+}
+
+/// The interfaces that an application can implement to handle ProgramCache update
+pub trait ProgramCacheObserver {
+ fn save_shaders_to_disk(&self, entries: Vec<Arc<ProgramBinary>>);
+ fn set_startup_shaders(&self, entries: Vec<Arc<ProgramBinary>>);
+ fn try_load_shader_from_disk(&self, digest: &ProgramSourceDigest, program_cache: &Rc<ProgramCache>);
+ fn notify_program_binary_failed(&self, program_binary: &Arc<ProgramBinary>);
+}
+
+struct ProgramCacheEntry {
+ /// The binary.
+ binary: Arc<ProgramBinary>,
+ /// True if the binary has been linked, i.e. used for rendering.
+ linked: bool,
+}
+
+pub struct ProgramCache {
+ entries: RefCell<FastHashMap<ProgramSourceDigest, ProgramCacheEntry>>,
+
+ /// Optional trait object that allows the client
+ /// application to handle ProgramCache updating
+ program_cache_handler: Option<Box<dyn ProgramCacheObserver>>,
+
+ /// Programs that have not yet been cached to disk (by program_cache_handler)
+ pending_entries: RefCell<Vec<Arc<ProgramBinary>>>,
+}
+
+impl ProgramCache {
+ pub fn new(program_cache_observer: Option<Box<dyn ProgramCacheObserver>>) -> Rc<Self> {
+ Rc::new(
+ ProgramCache {
+ entries: RefCell::new(FastHashMap::default()),
+ program_cache_handler: program_cache_observer,
+ pending_entries: RefCell::new(Vec::default()),
+ }
+ )
+ }
+
+ /// Save any new program binaries to the disk cache, and if startup has
+ /// just completed then write the list of shaders to load on next startup.
+ fn update_disk_cache(&self, startup_complete: bool) {
+ if let Some(ref handler) = self.program_cache_handler {
+ if !self.pending_entries.borrow().is_empty() {
+ let pending_entries = self.pending_entries.replace(Vec::default());
+ handler.save_shaders_to_disk(pending_entries);
+ }
+
+ if startup_complete {
+ let startup_shaders = self.entries.borrow().values()
+ .filter(|e| e.linked).map(|e| e.binary.clone())
+ .collect::<Vec<_>>();
+ handler.set_startup_shaders(startup_shaders);
+ }
+ }
+ }
+
+ /// Add a new ProgramBinary to the cache.
+ /// This function is typically used after compiling and linking a new program.
+ /// The binary will be saved to disk the next time update_disk_cache() is called.
+ fn add_new_program_binary(&self, program_binary: Arc<ProgramBinary>) {
+ self.pending_entries.borrow_mut().push(program_binary.clone());
+
+ let digest = program_binary.source_digest.clone();
+ let entry = ProgramCacheEntry {
+ binary: program_binary,
+ linked: true,
+ };
+ self.entries.borrow_mut().insert(digest, entry);
+ }
+
+ /// Load ProgramBinary to ProgramCache.
+ /// The function is typically used to load ProgramBinary from disk.
+ #[cfg(feature = "serialize_program")]
+ pub fn load_program_binary(&self, program_binary: Arc<ProgramBinary>) {
+ let digest = program_binary.source_digest.clone();
+ let entry = ProgramCacheEntry {
+ binary: program_binary,
+ linked: false,
+ };
+ self.entries.borrow_mut().insert(digest, entry);
+ }
+
+ /// Returns the number of bytes allocated for shaders in the cache.
+ pub fn report_memory(&self, op: VoidPtrToSizeFn) -> usize {
+ self.entries.borrow().values()
+ .map(|e| unsafe { op(e.binary.bytes.as_ptr() as *const c_void ) })
+ .sum()
+ }
+}
+
+#[derive(Debug, Copy, Clone)]
+pub enum VertexUsageHint {
+ Static,
+ Dynamic,
+ Stream,
+}
+
+impl VertexUsageHint {
+ fn to_gl(&self) -> gl::GLuint {
+ match *self {
+ VertexUsageHint::Static => gl::STATIC_DRAW,
+ VertexUsageHint::Dynamic => gl::DYNAMIC_DRAW,
+ VertexUsageHint::Stream => gl::STREAM_DRAW,
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug)]
+pub struct UniformLocation(gl::GLint);
+
+impl UniformLocation {
+ pub const INVALID: Self = UniformLocation(-1);
+}
+
+#[derive(Debug)]
+pub struct Capabilities {
+ /// Whether multisampled render targets are supported.
+ pub supports_multisampling: bool,
+ /// Whether the function `glCopyImageSubData` is available.
+ pub supports_copy_image_sub_data: bool,
+ /// Whether the RGBAF32 textures can be bound to framebuffers.
+ pub supports_color_buffer_float: bool,
+ /// Whether the device supports persistently mapped buffers, via glBufferStorage.
+ pub supports_buffer_storage: bool,
+ /// Whether advanced blend equations are supported.
+ pub supports_advanced_blend_equation: bool,
+ /// Whether dual-source blending is supported.
+ pub supports_dual_source_blending: bool,
+ /// Whether KHR_debug is supported for getting debug messages from
+ /// the driver.
+ pub supports_khr_debug: bool,
+ /// Whether we can configure texture units to do swizzling on sampling.
+ pub supports_texture_swizzle: bool,
+ /// Whether the driver supports uploading to textures from a non-zero
+ /// offset within a PBO.
+ pub supports_nonzero_pbo_offsets: bool,
+ /// Whether the driver supports specifying the texture usage up front.
+ pub supports_texture_usage: bool,
+ /// Whether offscreen render targets can be partially updated.
+ pub supports_render_target_partial_update: bool,
+ /// Whether we can use SSBOs.
+ pub supports_shader_storage_object: bool,
+ /// Whether to enforce that texture uploads be batched regardless of what
+ /// the pref says.
+ pub requires_batched_texture_uploads: Option<bool>,
+ /// Whether we are able to ue glClear to clear regions of an alpha render target.
+ /// If false, we must use a shader to clear instead.
+ pub supports_alpha_target_clears: bool,
+ /// Whether we must perform a full unscissored glClear on alpha targets
+ /// prior to rendering.
+ pub requires_alpha_target_full_clear: bool,
+ /// Whether clearing a render target (immediately after binding it) is faster using a scissor
+ /// rect to clear just the required area, or clearing the entire target without a scissor rect.
+ pub prefers_clear_scissor: bool,
+ /// Whether the driver can correctly invalidate render targets. This can be
+ /// a worthwhile optimization, but is buggy on some devices.
+ pub supports_render_target_invalidate: bool,
+ /// Whether the driver can reliably upload data to R8 format textures.
+ pub supports_r8_texture_upload: bool,
+ /// Whether the extension QCOM_tiled_rendering is supported.
+ pub supports_qcom_tiled_rendering: bool,
+ /// Whether clip-masking is supported natively by the GL implementation
+ /// rather than emulated in shaders.
+ pub uses_native_clip_mask: bool,
+ /// Whether anti-aliasing is supported natively by the GL implementation
+ /// rather than emulated in shaders.
+ pub uses_native_antialiasing: bool,
+ /// Whether the extension GL_OES_EGL_image_external_essl3 is supported. If true, external
+ /// textures can be used as normal. If false, external textures can only be rendered with
+ /// certain shaders, and must first be copied in to regular textures for others.
+ pub supports_image_external_essl3: bool,
+ /// Whether the VAO must be rebound after an attached VBO has been orphaned.
+ pub requires_vao_rebind_after_orphaning: bool,
+ /// The name of the renderer, as reported by GL
+ pub renderer_name: String,
+}
+
+#[derive(Clone, Debug)]
+pub enum ShaderError {
+ Compilation(String, String), // name, error message
+ Link(String, String), // name, error message
+}
+
+/// A refcounted depth target, which may be shared by multiple textures across
+/// the device.
+struct SharedDepthTarget {
+ /// The Render Buffer Object representing the depth target.
+ rbo_id: RBOId,
+ /// Reference count. When this drops to zero, the RBO is deleted.
+ refcount: usize,
+}
+
+#[cfg(debug_assertions)]
+impl Drop for SharedDepthTarget {
+ fn drop(&mut self) {
+ debug_assert!(thread::panicking() || self.refcount == 0);
+ }
+}
+
+/// Describes for which texture formats to use the glTexStorage*
+/// family of functions.
+#[derive(PartialEq, Debug)]
+enum TexStorageUsage {
+ Never,
+ NonBGRA8,
+ Always,
+}
+
+/// Describes a required alignment for a stride,
+/// which can either be represented in bytes or pixels.
+#[derive(Copy, Clone, Debug)]
+pub enum StrideAlignment {
+ Bytes(NonZeroUsize),
+ Pixels(NonZeroUsize),
+}
+
+impl StrideAlignment {
+ pub fn num_bytes(&self, format: ImageFormat) -> NonZeroUsize {
+ match *self {
+ Self::Bytes(bytes) => bytes,
+ Self::Pixels(pixels) => {
+ assert!(format.bytes_per_pixel() > 0);
+ NonZeroUsize::new(pixels.get() * format.bytes_per_pixel() as usize).unwrap()
+ }
+ }
+ }
+}
+
+// We get 24 bits of Z value - use up 22 bits of it to give us
+// 4 bits to account for GPU issues. This seems to manifest on
+// some GPUs under certain perspectives due to z interpolation
+// precision problems.
+const RESERVE_DEPTH_BITS: i32 = 2;
+
+pub struct Device {
+ gl: Rc<dyn gl::Gl>,
+
+ /// If non-None, |gl| points to a profiling wrapper, and this points to the
+ /// underling Gl instance.
+ base_gl: Option<Rc<dyn gl::Gl>>,
+
+ // device state
+ bound_textures: [gl::GLuint; 16],
+ bound_program: gl::GLuint,
+ bound_program_name: Rc<std::ffi::CString>,
+ bound_vao: gl::GLuint,
+ bound_read_fbo: (FBOId, DeviceIntPoint),
+ bound_draw_fbo: FBOId,
+ program_mode_id: UniformLocation,
+ default_read_fbo: FBOId,
+ default_draw_fbo: FBOId,
+
+ /// Track depth state for assertions. Note that the default FBO has depth,
+ /// so this defaults to true.
+ depth_available: bool,
+
+ upload_method: UploadMethod,
+ use_batched_texture_uploads: bool,
+ /// Whether to use draw calls instead of regular blitting commands.
+ ///
+ /// Note: this currently only applies to the batched texture uploads
+ /// path.
+ use_draw_calls_for_texture_copy: bool,
+ /// Number of pixels below which we prefer batched uploads.
+ batched_upload_threshold: i32,
+
+ // HW or API capabilities
+ capabilities: Capabilities,
+
+ color_formats: TextureFormatPair<ImageFormat>,
+ bgra_formats: TextureFormatPair<gl::GLuint>,
+ bgra_pixel_type: gl::GLuint,
+ swizzle_settings: SwizzleSettings,
+ depth_format: gl::GLuint,
+
+ /// Map from texture dimensions to shared depth buffers for render targets.
+ ///
+ /// Render targets often have the same width/height, so we can save memory
+ /// by sharing these across targets.
+ depth_targets: FastHashMap<DeviceIntSize, SharedDepthTarget>,
+
+ // debug
+ inside_frame: bool,
+ crash_annotator: Option<Box<dyn CrashAnnotator>>,
+ annotate_draw_call_crashes: bool,
+
+ // resources
+ resource_override_path: Option<PathBuf>,
+
+ /// Whether to use shaders that have been optimized at build time.
+ use_optimized_shaders: bool,
+
+ max_texture_size: i32,
+ cached_programs: Option<Rc<ProgramCache>>,
+
+ // Frame counter. This is used to map between CPU
+ // frames and GPU frames.
+ frame_id: GpuFrameId,
+
+ /// When to use glTexStorage*. We prefer this over glTexImage* because it
+ /// guarantees that mipmaps won't be generated (which they otherwise are on
+ /// some drivers, particularly ANGLE). However, it is not always supported
+ /// at all, or for BGRA8 format. If it's not supported for the required
+ /// format, we fall back to glTexImage*.
+ texture_storage_usage: TexStorageUsage,
+
+ /// Required stride alignment for pixel transfers. This may be required for
+ /// correctness reasons due to driver bugs, or for performance reasons to
+ /// ensure we remain on the fast-path for transfers.
+ required_pbo_stride: StrideAlignment,
+
+ /// Whether we must ensure the source strings passed to glShaderSource()
+ /// are null-terminated, to work around driver bugs.
+ requires_null_terminated_shader_source: bool,
+
+ /// Whether we must unbind any texture from GL_TEXTURE_EXTERNAL_OES before
+ /// binding to GL_TEXTURE_2D, to work around an android emulator bug.
+ requires_texture_external_unbind: bool,
+
+ ///
+ is_software_webrender: bool,
+
+ // GL extensions
+ extensions: Vec<String>,
+
+ /// Dumps the source of the shader with the given name
+ dump_shader_source: Option<String>,
+
+ surface_origin_is_top_left: bool,
+
+ /// A debug boolean for tracking if the shader program has been set after
+ /// a blend mode change.
+ ///
+ /// This is needed for compatibility with next-gen
+ /// GPU APIs that switch states using "pipeline object" that bundles
+ /// together the blending state with the shader.
+ ///
+ /// Having the constraint of always binding the shader last would allow
+ /// us to have the "pipeline object" bound at that time. Without this
+ /// constraint, we'd either have to eagerly bind the "pipeline object"
+ /// on changing either the shader or the blend more, or lazily bind it
+ /// at draw call time, neither of which is desirable.
+ #[cfg(debug_assertions)]
+ shader_is_ready: bool,
+
+ // count created/deleted textures to report in the profiler.
+ pub textures_created: u32,
+ pub textures_deleted: u32,
+}
+
+/// Contains the parameters necessary to bind a draw target.
+#[derive(Clone, Copy, Debug)]
+pub enum DrawTarget {
+ /// Use the device's default draw target, with the provided dimensions,
+ /// which are used to set the viewport.
+ Default {
+ /// Target rectangle to draw.
+ rect: FramebufferIntRect,
+ /// Total size of the target.
+ total_size: FramebufferIntSize,
+ surface_origin_is_top_left: bool,
+ },
+ /// Use the provided texture.
+ Texture {
+ /// Size of the texture in pixels
+ dimensions: DeviceIntSize,
+ /// Whether to draw with the texture's associated depth target
+ with_depth: bool,
+ /// FBO that corresponds to the selected layer / depth mode
+ fbo_id: FBOId,
+ /// Native GL texture ID
+ id: gl::GLuint,
+ /// Native GL texture target
+ target: gl::GLuint,
+ },
+ /// Use an FBO attached to an external texture.
+ External {
+ fbo: FBOId,
+ size: FramebufferIntSize,
+ },
+ /// An OS compositor surface
+ NativeSurface {
+ offset: DeviceIntPoint,
+ external_fbo_id: u32,
+ dimensions: DeviceIntSize,
+ },
+}
+
+impl DrawTarget {
+ pub fn new_default(size: DeviceIntSize, surface_origin_is_top_left: bool) -> Self {
+ let total_size = device_size_as_framebuffer_size(size);
+ DrawTarget::Default {
+ rect: total_size.into(),
+ total_size,
+ surface_origin_is_top_left,
+ }
+ }
+
+ /// Returns true if this draw target corresponds to the default framebuffer.
+ pub fn is_default(&self) -> bool {
+ match *self {
+ DrawTarget::Default {..} => true,
+ _ => false,
+ }
+ }
+
+ pub fn from_texture(
+ texture: &Texture,
+ with_depth: bool,
+ ) -> Self {
+ let fbo_id = if with_depth {
+ texture.fbo_with_depth.unwrap()
+ } else {
+ texture.fbo.unwrap()
+ };
+
+ DrawTarget::Texture {
+ dimensions: texture.get_dimensions(),
+ fbo_id,
+ with_depth,
+ id: texture.id,
+ target: texture.target,
+ }
+ }
+
+ /// Returns the dimensions of this draw-target.
+ pub fn dimensions(&self) -> DeviceIntSize {
+ match *self {
+ DrawTarget::Default { total_size, .. } => total_size.cast_unit(),
+ DrawTarget::Texture { dimensions, .. } => dimensions,
+ DrawTarget::External { size, .. } => size.cast_unit(),
+ DrawTarget::NativeSurface { dimensions, .. } => dimensions,
+ }
+ }
+
+ pub fn offset(&self) -> DeviceIntPoint {
+ match *self {
+ DrawTarget::Default { .. } |
+ DrawTarget::Texture { .. } |
+ DrawTarget::External { .. } => {
+ DeviceIntPoint::zero()
+ }
+ DrawTarget::NativeSurface { offset, .. } => offset,
+ }
+ }
+
+ pub fn to_framebuffer_rect(&self, device_rect: DeviceIntRect) -> FramebufferIntRect {
+ let mut fb_rect = device_rect_as_framebuffer_rect(&device_rect);
+ match *self {
+ DrawTarget::Default { ref rect, surface_origin_is_top_left, .. } => {
+ // perform a Y-flip here
+ if !surface_origin_is_top_left {
+ let w = fb_rect.width();
+ let h = fb_rect.height();
+ fb_rect.min.x = fb_rect.min.x + rect.min.x;
+ fb_rect.min.y = rect.max.y - fb_rect.max.y;
+ fb_rect.max.x = fb_rect.min.x + w;
+ fb_rect.max.y = fb_rect.min.y + h;
+ }
+ }
+ DrawTarget::Texture { .. } | DrawTarget::External { .. } | DrawTarget::NativeSurface { .. } => (),
+ }
+ fb_rect
+ }
+
+ pub fn surface_origin_is_top_left(&self) -> bool {
+ match *self {
+ DrawTarget::Default { surface_origin_is_top_left, .. } => surface_origin_is_top_left,
+ DrawTarget::Texture { .. } | DrawTarget::External { .. } | DrawTarget::NativeSurface { .. } => true,
+ }
+ }
+
+ /// Given a scissor rect, convert it to the right coordinate space
+ /// depending on the draw target kind. If no scissor rect was supplied,
+ /// returns a scissor rect that encloses the entire render target.
+ pub fn build_scissor_rect(
+ &self,
+ scissor_rect: Option<DeviceIntRect>,
+ ) -> FramebufferIntRect {
+ let dimensions = self.dimensions();
+
+ match scissor_rect {
+ Some(scissor_rect) => match *self {
+ DrawTarget::Default { ref rect, .. } => {
+ self.to_framebuffer_rect(scissor_rect)
+ .intersection(rect)
+ .unwrap_or_else(FramebufferIntRect::zero)
+ }
+ DrawTarget::NativeSurface { offset, .. } => {
+ device_rect_as_framebuffer_rect(&scissor_rect.translate(offset.to_vector()))
+ }
+ DrawTarget::Texture { .. } | DrawTarget::External { .. } => {
+ device_rect_as_framebuffer_rect(&scissor_rect)
+ }
+ }
+ None => {
+ FramebufferIntRect::from_size(
+ device_size_as_framebuffer_size(dimensions),
+ )
+ }
+ }
+ }
+}
+
+/// Contains the parameters necessary to bind a texture-backed read target.
+#[derive(Clone, Copy, Debug)]
+pub enum ReadTarget {
+ /// Use the device's default draw target.
+ Default,
+ /// Use the provided texture,
+ Texture {
+ /// ID of the FBO to read from.
+ fbo_id: FBOId,
+ },
+ /// Use an FBO attached to an external texture.
+ External {
+ fbo: FBOId,
+ },
+ /// An FBO bound to a native (OS compositor) surface
+ NativeSurface {
+ fbo_id: FBOId,
+ offset: DeviceIntPoint,
+ },
+}
+
+impl ReadTarget {
+ pub fn from_texture(
+ texture: &Texture,
+ ) -> Self {
+ ReadTarget::Texture {
+ fbo_id: texture.fbo.unwrap(),
+ }
+ }
+
+ fn offset(&self) -> DeviceIntPoint {
+ match *self {
+ ReadTarget::Default |
+ ReadTarget::Texture { .. } |
+ ReadTarget::External { .. } => {
+ DeviceIntPoint::zero()
+ }
+
+ ReadTarget::NativeSurface { offset, .. } => {
+ offset
+ }
+ }
+ }
+}
+
+impl From<DrawTarget> for ReadTarget {
+ fn from(t: DrawTarget) -> Self {
+ match t {
+ DrawTarget::Default { .. } => {
+ ReadTarget::Default
+ }
+ DrawTarget::NativeSurface { external_fbo_id, offset, .. } => {
+ ReadTarget::NativeSurface {
+ fbo_id: FBOId(external_fbo_id),
+ offset,
+ }
+ }
+ DrawTarget::Texture { fbo_id, .. } => {
+ ReadTarget::Texture { fbo_id }
+ }
+ DrawTarget::External { fbo, .. } => {
+ ReadTarget::External { fbo }
+ }
+ }
+ }
+}
+
+/// Parses the major, release, and patch versions from a GL_VERSION string on
+/// Mali devices. For example, for the version string
+/// "OpenGL ES 3.2 v1.r36p0-01eac0.28ab3a577f105e026887e2b4c93552fb" this
+/// returns Some((1, 36, 0)). Returns None if the version cannot be parsed.
+fn parse_mali_version(version_string: &str) -> Option<(u32, u32, u32)> {
+ let (_prefix, version_string) = version_string.split_once("v")?;
+ let (v_str, version_string) = version_string.split_once(".r")?;
+ let v = v_str.parse().ok()?;
+
+ let (r_str, version_string) = version_string.split_once("p")?;
+ let r = r_str.parse().ok()?;
+
+ let (p_str, _) = version_string.split_once("-")?;
+ let p = p_str.parse().ok()?;
+
+ Some((v, r, p))
+}
+
+impl Device {
+ pub fn new(
+ mut gl: Rc<dyn gl::Gl>,
+ crash_annotator: Option<Box<dyn CrashAnnotator>>,
+ resource_override_path: Option<PathBuf>,
+ use_optimized_shaders: bool,
+ upload_method: UploadMethod,
+ batched_upload_threshold: i32,
+ cached_programs: Option<Rc<ProgramCache>>,
+ allow_texture_storage_support: bool,
+ allow_texture_swizzling: bool,
+ dump_shader_source: Option<String>,
+ surface_origin_is_top_left: bool,
+ panic_on_gl_error: bool,
+ ) -> Device {
+ let mut max_texture_size = [0];
+ unsafe {
+ gl.get_integer_v(gl::MAX_TEXTURE_SIZE, &mut max_texture_size);
+ }
+
+ // We cap the max texture size at 16384. Some hardware report higher
+ // capabilities but get very unstable with very large textures.
+ // Bug 1702494 tracks re-evaluating this cap.
+ let max_texture_size = max_texture_size[0].min(16384);
+
+ let renderer_name = gl.get_string(gl::RENDERER);
+ info!("Renderer: {}", renderer_name);
+ let version_string = gl.get_string(gl::VERSION);
+ info!("Version: {}", version_string);
+ info!("Max texture size: {}", max_texture_size);
+
+ let mut extension_count = [0];
+ unsafe {
+ gl.get_integer_v(gl::NUM_EXTENSIONS, &mut extension_count);
+ }
+ let extension_count = extension_count[0] as gl::GLuint;
+ let mut extensions = Vec::new();
+ for i in 0 .. extension_count {
+ extensions.push(gl.get_string_i(gl::EXTENSIONS, i));
+ }
+
+ // On debug builds, assert that each GL call is error-free. We don't do
+ // this on release builds because the synchronous call can stall the
+ // pipeline.
+ let supports_khr_debug = supports_extension(&extensions, "GL_KHR_debug");
+ if panic_on_gl_error || cfg!(debug_assertions) {
+ gl = gl::ErrorReactingGl::wrap(gl, move |gl, name, code| {
+ if supports_khr_debug {
+ Self::log_driver_messages(gl);
+ }
+ error!("Caught GL error {:x} at {}", code, name);
+ panic!("Caught GL error {:x} at {}", code, name);
+ });
+ }
+
+ if supports_extension(&extensions, "GL_ANGLE_provoking_vertex") {
+ gl.provoking_vertex_angle(gl::FIRST_VERTEX_CONVENTION);
+ }
+
+ let supports_texture_usage = supports_extension(&extensions, "GL_ANGLE_texture_usage");
+
+ // Our common-case image data in Firefox is BGRA, so we make an effort
+ // to use BGRA as the internal texture storage format to avoid the need
+ // to swizzle during upload. Currently we only do this on GLES (and thus
+ // for Windows, via ANGLE).
+ //
+ // On Mac, Apple docs [1] claim that BGRA is a more efficient internal
+ // format, but they don't support it with glTextureStorage. As a workaround,
+ // we pretend that it's RGBA8 for the purposes of texture transfers,
+ // but swizzle R with B for the texture sampling.
+ //
+ // We also need our internal format types to be sized, since glTexStorage*
+ // will reject non-sized internal format types.
+ //
+ // Unfortunately, with GL_EXT_texture_format_BGRA8888, BGRA8 is not a
+ // valid internal format (for glTexImage* or glTexStorage*) unless
+ // GL_EXT_texture_storage is also available [2][3], which is usually
+ // not the case on GLES 3 as the latter's functionality has been
+ // included by default but the former has not been updated.
+ // The extension is available on ANGLE, but on Android this usually
+ // means we must fall back to using unsized BGRA and glTexImage*.
+ //
+ // Overall, we have the following factors in play when choosing the formats:
+ // - with glTexStorage, the internal format needs to match the external format,
+ // or the driver would have to do the conversion, which is slow
+ // - on desktop GL, there is no BGRA internal format. However, initializing
+ // the textures with glTexImage as RGBA appears to use BGRA internally,
+ // preferring BGRA external data [4].
+ // - when glTexStorage + BGRA internal format is not supported,
+ // and the external data is BGRA, we have the following options:
+ // 1. use glTexImage with RGBA internal format, this costs us VRAM for mipmaps
+ // 2. use glTexStorage with RGBA internal format, this costs us the conversion by the driver
+ // 3. pretend we are uploading RGBA and set up the swizzling of the texture unit - this costs us batch breaks
+ //
+ // [1] https://developer.apple.com/library/archive/documentation/
+ // GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_texturedata/
+ // opengl_texturedata.html#//apple_ref/doc/uid/TP40001987-CH407-SW22
+ // [2] https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_texture_format_BGRA8888.txt
+ // [3] https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_texture_storage.txt
+ // [4] http://http.download.nvidia.com/developer/Papers/2005/Fast_Texture_Transfers/Fast_Texture_Transfers.pdf
+
+ // On the android emulator glTexImage fails to create textures larger than 3379.
+ // So we must use glTexStorage instead. See bug 1591436.
+ let is_emulator = renderer_name.starts_with("Android Emulator");
+ let avoid_tex_image = is_emulator;
+ let mut gl_version = [0; 2];
+ unsafe {
+ gl.get_integer_v(gl::MAJOR_VERSION, &mut gl_version[0..1]);
+ gl.get_integer_v(gl::MINOR_VERSION, &mut gl_version[1..2]);
+ }
+ info!("GL context {:?} {}.{}", gl.get_type(), gl_version[0], gl_version[1]);
+
+ // We block texture storage on mac because it doesn't support BGRA
+ let supports_texture_storage = allow_texture_storage_support && !cfg!(target_os = "macos") &&
+ match gl.get_type() {
+ gl::GlType::Gl => supports_extension(&extensions, "GL_ARB_texture_storage"),
+ gl::GlType::Gles => true,
+ };
+
+ // The GL_EXT_texture_format_BGRA8888 extension allows us to use BGRA as an internal format
+ // with glTexImage on GLES. However, we can only use BGRA8 as an internal format for
+ // glTexStorage when GL_EXT_texture_storage is also explicitly supported. This is because
+ // glTexStorage was added in GLES 3, but GL_EXT_texture_format_BGRA8888 was written against
+ // GLES 2 and GL_EXT_texture_storage.
+ // To complicate things even further, some Intel devices claim to support both extensions
+ // but in practice do not allow BGRA to be used with glTexStorage.
+ let supports_gles_bgra = supports_extension(&extensions, "GL_EXT_texture_format_BGRA8888");
+ let supports_texture_storage_with_gles_bgra = supports_gles_bgra
+ && supports_extension(&extensions, "GL_EXT_texture_storage")
+ && !renderer_name.starts_with("Intel(R) HD Graphics for BayTrail")
+ && !renderer_name.starts_with("Intel(R) HD Graphics for Atom(TM) x5/x7");
+
+ let supports_texture_swizzle = allow_texture_swizzling &&
+ match gl.get_type() {
+ // see https://www.g-truc.net/post-0734.html
+ gl::GlType::Gl => gl_version >= [3, 3] ||
+ supports_extension(&extensions, "GL_ARB_texture_swizzle"),
+ gl::GlType::Gles => true,
+ };
+
+ let (color_formats, bgra_formats, bgra_pixel_type, bgra8_sampling_swizzle, texture_storage_usage) = match gl.get_type() {
+ // There is `glTexStorage`, use it and expect RGBA on the input.
+ gl::GlType::Gl if supports_texture_storage && supports_texture_swizzle => (
+ TextureFormatPair::from(ImageFormat::RGBA8),
+ TextureFormatPair { internal: gl::RGBA8, external: gl::RGBA },
+ gl::UNSIGNED_BYTE,
+ Swizzle::Bgra, // pretend it's RGBA, rely on swizzling
+ TexStorageUsage::Always
+ ),
+ // There is no `glTexStorage`, upload as `glTexImage` with BGRA input.
+ gl::GlType::Gl => (
+ TextureFormatPair { internal: ImageFormat::BGRA8, external: ImageFormat::BGRA8 },
+ TextureFormatPair { internal: gl::RGBA, external: gl::BGRA },
+ gl::UNSIGNED_INT_8_8_8_8_REV,
+ Swizzle::Rgba, // converted on uploads by the driver, no swizzling needed
+ TexStorageUsage::Never
+ ),
+ // glTexStorage is always supported in GLES 3, but because the GL_EXT_texture_storage
+ // extension is supported we can use glTexStorage with BGRA8 as the internal format.
+ // Prefer BGRA textures over RGBA.
+ gl::GlType::Gles if supports_texture_storage_with_gles_bgra => (
+ TextureFormatPair::from(ImageFormat::BGRA8),
+ TextureFormatPair { internal: gl::BGRA8_EXT, external: gl::BGRA_EXT },
+ gl::UNSIGNED_BYTE,
+ Swizzle::Rgba, // no conversion needed
+ TexStorageUsage::Always,
+ ),
+ // BGRA is not supported as an internal format with glTexStorage, therefore we will
+ // use RGBA textures instead and pretend BGRA data is RGBA when uploading.
+ // The swizzling will happen at the texture unit.
+ gl::GlType::Gles if supports_texture_swizzle => (
+ TextureFormatPair::from(ImageFormat::RGBA8),
+ TextureFormatPair { internal: gl::RGBA8, external: gl::RGBA },
+ gl::UNSIGNED_BYTE,
+ Swizzle::Bgra, // pretend it's RGBA, rely on swizzling
+ TexStorageUsage::Always,
+ ),
+ // BGRA is not supported as an internal format with glTexStorage, and we cannot use
+ // swizzling either. Therefore prefer BGRA textures over RGBA, but use glTexImage
+ // to initialize BGRA textures. glTexStorage can still be used for other formats.
+ gl::GlType::Gles if supports_gles_bgra && !avoid_tex_image => (
+ TextureFormatPair::from(ImageFormat::BGRA8),
+ TextureFormatPair::from(gl::BGRA_EXT),
+ gl::UNSIGNED_BYTE,
+ Swizzle::Rgba, // no conversion needed
+ TexStorageUsage::NonBGRA8,
+ ),
+ // Neither BGRA or swizzling are supported. GLES does not allow format conversion
+ // during upload so we must use RGBA textures and pretend BGRA data is RGBA when
+ // uploading. Images may be rendered incorrectly as a result.
+ gl::GlType::Gles => {
+ warn!("Neither BGRA or texture swizzling are supported. Images may be rendered incorrectly.");
+ (
+ TextureFormatPair::from(ImageFormat::RGBA8),
+ TextureFormatPair { internal: gl::RGBA8, external: gl::RGBA },
+ gl::UNSIGNED_BYTE,
+ Swizzle::Rgba,
+ TexStorageUsage::Always,
+ )
+ }
+ };
+
+ let is_software_webrender = renderer_name.starts_with("Software WebRender");
+ let upload_method = if is_software_webrender {
+ // Uploads in SWGL generally reduce to simple memory copies.
+ UploadMethod::Immediate
+ } else {
+ upload_method
+ };
+ // Prefer 24-bit depth format. While 16-bit depth also works, it may exhaust depth ids easily.
+ let depth_format = gl::DEPTH_COMPONENT24;
+
+ info!("GL texture cache {:?}, bgra {:?} swizzle {:?}, texture storage {:?}, depth {:?}",
+ color_formats, bgra_formats, bgra8_sampling_swizzle, texture_storage_usage, depth_format);
+
+ // On Mali-T devices glCopyImageSubData appears to stall the pipeline until any pending
+ // renders to the source texture have completed. On Mali-G, it has been observed to
+ // indefinitely hang in some circumstances. Using an alternative such as glBlitFramebuffer
+ // is preferable on such devices, so pretend we don't support glCopyImageSubData.
+ // See bugs 1669494 and 1677757.
+ let supports_copy_image_sub_data = if renderer_name.starts_with("Mali") {
+ false
+ } else {
+ supports_extension(&extensions, "GL_EXT_copy_image") ||
+ supports_extension(&extensions, "GL_ARB_copy_image")
+ };
+
+ // We have seen crashes on x86 PowerVR Rogue G6430 devices during GPU cache
+ // updates using the scatter shader. It seems likely that GL_EXT_color_buffer_float
+ // is broken. See bug 1709408.
+ let is_x86_powervr_rogue_g6430 = renderer_name.starts_with("PowerVR Rogue G6430")
+ && cfg!(target_arch = "x86");
+ let supports_color_buffer_float = match gl.get_type() {
+ gl::GlType::Gl => true,
+ gl::GlType::Gles if is_x86_powervr_rogue_g6430 => false,
+ gl::GlType::Gles => supports_extension(&extensions, "GL_EXT_color_buffer_float"),
+ };
+
+ let is_adreno = renderer_name.starts_with("Adreno");
+
+ // There appears to be a driver bug on older versions of the Adreno
+ // driver which prevents usage of persistenly mapped buffers.
+ // See bugs 1678585 and 1683936.
+ // TODO: only disable feature for affected driver versions.
+ let supports_buffer_storage = if is_adreno {
+ false
+ } else {
+ supports_extension(&extensions, "GL_EXT_buffer_storage") ||
+ supports_extension(&extensions, "GL_ARB_buffer_storage")
+ };
+
+ // KHR_blend_equation_advanced renders incorrectly on Adreno
+ // devices. This has only been confirmed up to Adreno 5xx, and has been
+ // fixed for Android 9, so this condition could be made more specific.
+ let supports_advanced_blend_equation =
+ supports_extension(&extensions, "GL_KHR_blend_equation_advanced") &&
+ !is_adreno;
+
+ let supports_dual_source_blending = match gl.get_type() {
+ gl::GlType::Gl => supports_extension(&extensions,"GL_ARB_blend_func_extended") &&
+ supports_extension(&extensions,"GL_ARB_explicit_attrib_location"),
+ gl::GlType::Gles => supports_extension(&extensions,"GL_EXT_blend_func_extended"),
+ };
+
+ // Software webrender relies on the unoptimized shader source.
+ let use_optimized_shaders = use_optimized_shaders && !is_software_webrender;
+
+ // On the android emulator, and possibly some Mali devices, glShaderSource
+ // can crash if the source strings are not null-terminated.
+ // See bug 1591945 and bug 1799722.
+ let requires_null_terminated_shader_source = is_emulator || renderer_name == "Mali-T628"
+ || renderer_name == "Mali-T720" || renderer_name == "Mali-T760";
+
+ // The android emulator gets confused if you don't explicitly unbind any texture
+ // from GL_TEXTURE_EXTERNAL_OES before binding another to GL_TEXTURE_2D. See bug 1636085.
+ let requires_texture_external_unbind = is_emulator;
+
+ let is_macos = cfg!(target_os = "macos");
+ // && renderer_name.starts_with("AMD");
+ // (XXX: we apply this restriction to all GPUs to handle switching)
+
+ let is_windows_angle = cfg!(target_os = "windows")
+ && renderer_name.starts_with("ANGLE");
+ let is_adreno_3xx = renderer_name.starts_with("Adreno (TM) 3");
+
+ // Some GPUs require the stride of the data during texture uploads to be
+ // aligned to certain requirements, either for correctness or performance
+ // reasons.
+ let required_pbo_stride = if is_adreno_3xx {
+ // On Adreno 3xx, alignments of < 128 bytes can result in corrupted
+ // glyphs. See bug 1696039.
+ StrideAlignment::Bytes(NonZeroUsize::new(128).unwrap())
+ } else if is_adreno {
+ // On later Adreno devices it must be a multiple of 64 *pixels* to
+ // hit the fast path, meaning value in bytes varies with the texture
+ // format. This is purely an optimization.
+ StrideAlignment::Pixels(NonZeroUsize::new(64).unwrap())
+ } else if is_macos {
+ // On AMD Mac, it must always be a multiple of 256 bytes.
+ // We apply this restriction to all GPUs to handle switching
+ StrideAlignment::Bytes(NonZeroUsize::new(256).unwrap())
+ } else if is_windows_angle {
+ // On ANGLE-on-D3D, PBO texture uploads get incorrectly truncated
+ // if the stride is greater than the width * bpp.
+ StrideAlignment::Bytes(NonZeroUsize::new(1).unwrap())
+ } else {
+ // Other platforms may have similar requirements and should be added
+ // here. The default value should be 4 bytes.
+ StrideAlignment::Bytes(NonZeroUsize::new(4).unwrap())
+ };
+
+ // On AMD Macs there is a driver bug which causes some texture uploads
+ // from a non-zero offset within a PBO to fail. See bug 1603783.
+ let supports_nonzero_pbo_offsets = !is_macos;
+
+ // We have encountered several issues when only partially updating render targets on a
+ // variety of Mali GPUs. As a precaution avoid doing so on all Midgard and Bifrost GPUs.
+ // Valhall (eg Mali-Gx7 onwards) appears to be unnaffected. See bug 1691955, bug 1558374,
+ // and bug 1663355.
+ let supports_render_target_partial_update = !(renderer_name.starts_with("Mali-T")
+ || renderer_name == "Mali-G31"
+ || renderer_name == "Mali-G51"
+ || renderer_name == "Mali-G71"
+ || renderer_name == "Mali-G52"
+ || renderer_name == "Mali-G72"
+ || renderer_name == "Mali-G76");
+
+ let supports_shader_storage_object = match gl.get_type() {
+ // see https://www.g-truc.net/post-0734.html
+ gl::GlType::Gl => supports_extension(&extensions, "GL_ARB_shader_storage_buffer_object"),
+ gl::GlType::Gles => gl_version >= [3, 1],
+ };
+
+ // SWGL uses swgl_clipMask() instead of implementing clip-masking in shaders.
+ // This allows certain shaders to potentially bypass the more expensive alpha-
+ // pass variants if they know the alpha-pass was only required to deal with
+ // clip-masking.
+ let uses_native_clip_mask = is_software_webrender;
+
+ // SWGL uses swgl_antiAlias() instead of implementing anti-aliasing in shaders.
+ // As above, this allows bypassing certain alpha-pass variants.
+ let uses_native_antialiasing = is_software_webrender;
+
+ // If running on android with a mesa driver (eg intel chromebooks), parse the mesa version.
+ let mut android_mesa_version = None;
+ if cfg!(target_os = "android") && renderer_name.starts_with("Mesa") {
+ if let Some((_, mesa_version)) = version_string.split_once("Mesa ") {
+ if let Some((major_str, _)) = mesa_version.split_once(".") {
+ if let Ok(major) = major_str.parse::<i32>() {
+ android_mesa_version = Some(major);
+ }
+ }
+ }
+ }
+
+ // If the device supports OES_EGL_image_external_essl3 we can use it to render
+ // external images. If not, we must use the ESSL 1.0 OES_EGL_image_external
+ // extension instead.
+ // Mesa versions prior to 20.0 do not implement textureSize(samplerExternalOES),
+ // so we must use the fallback path.
+ let supports_image_external_essl3 = match android_mesa_version {
+ Some(major) if major < 20 => false,
+ _ => supports_extension(&extensions, "GL_OES_EGL_image_external_essl3"),
+ };
+
+ let mut requires_batched_texture_uploads = None;
+ if is_software_webrender {
+ // No benefit to batching texture uploads with swgl.
+ requires_batched_texture_uploads = Some(false);
+ } else if renderer_name.starts_with("Mali-G") {
+ // On Mali-Gxx the driver really struggles with many small texture uploads,
+ // and handles fewer, larger uploads better.
+ requires_batched_texture_uploads = Some(true);
+ }
+
+ // On Mali-Txxx devices we have observed crashes during draw calls when rendering
+ // to an alpha target immediately after using glClear to clear regions of it.
+ // Using a shader to clear the regions avoids the crash. See bug 1638593.
+ let supports_alpha_target_clears = !renderer_name.starts_with("Mali-T");
+
+ // On Adreno 4xx devices with older drivers we have seen render tasks to alpha targets have
+ // no effect unless the target is fully cleared prior to rendering. See bug 1714227.
+ let is_adreno_4xx = renderer_name.starts_with("Adreno (TM) 4");
+ let requires_alpha_target_full_clear = is_adreno_4xx;
+
+ // Testing on Intel and nVidia GPUs, as well as software webrender, showed large performance
+ // wins applying a scissor rect when clearing render targets. Assume this is the best
+ // default. On mobile GPUs, however, it can be much more efficient to clear the entire
+ // render target. For now, enable the scissor everywhere except Android hardware
+ // webrender. We can tweak this further if needs be.
+ let prefers_clear_scissor = !cfg!(target_os = "android") || is_software_webrender;
+
+ let mut supports_render_target_invalidate = true;
+
+ // On PowerVR Rogue devices we have seen that invalidating render targets after we are done
+ // with them can incorrectly cause pending renders to be written to different targets
+ // instead. See bug 1719345.
+ let is_powervr_rogue = renderer_name.starts_with("PowerVR Rogue");
+ if is_powervr_rogue {
+ supports_render_target_invalidate = false;
+ }
+
+ // On Mali Valhall devices with a driver version v1.r36p0 we have seen that invalidating
+ // render targets can result in image corruption, perhaps due to subsequent reuses of the
+ // render target not correctly reinitializing them to a valid state. See bug 1787520.
+ if renderer_name.starts_with("Mali-G77")
+ || renderer_name.starts_with("Mali-G78")
+ || renderer_name.starts_with("Mali-G710")
+ {
+ match parse_mali_version(&version_string) {
+ Some(version) if version >= (1, 36, 0) => supports_render_target_invalidate = false,
+ _ => {}
+ }
+ }
+
+ // On Linux we we have seen uploads to R8 format textures result in
+ // corruption on some AMD cards.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1687554#c13
+ let supports_r8_texture_upload = if cfg!(target_os = "linux")
+ && renderer_name.starts_with("AMD Radeon RX")
+ {
+ false
+ } else {
+ true
+ };
+
+ // We have encountered rendering errors on a variety of Adreno GPUs specifically on driver
+ // version V@0490, so block this extension on that driver version.
+ let supports_qcom_tiled_rendering =
+ supports_extension(&extensions, "GL_QCOM_tiled_rendering")
+ && !(is_adreno && version_string.contains("V@0490"));
+
+ // On some Adreno 3xx devices the vertex array object must be unbound and rebound after
+ // an attached buffer has been orphaned.
+ let requires_vao_rebind_after_orphaning = is_adreno_3xx;
+
+ Device {
+ gl,
+ base_gl: None,
+ crash_annotator,
+ annotate_draw_call_crashes: false,
+ resource_override_path,
+ use_optimized_shaders,
+ upload_method,
+ use_batched_texture_uploads: requires_batched_texture_uploads.unwrap_or(false),
+ use_draw_calls_for_texture_copy: false,
+ batched_upload_threshold,
+
+ inside_frame: false,
+
+ capabilities: Capabilities {
+ supports_multisampling: false, //TODO
+ supports_copy_image_sub_data,
+ supports_color_buffer_float,
+ supports_buffer_storage,
+ supports_advanced_blend_equation,
+ supports_dual_source_blending,
+ supports_khr_debug,
+ supports_texture_swizzle,
+ supports_nonzero_pbo_offsets,
+ supports_texture_usage,
+ supports_render_target_partial_update,
+ supports_shader_storage_object,
+ requires_batched_texture_uploads,
+ supports_alpha_target_clears,
+ requires_alpha_target_full_clear,
+ prefers_clear_scissor,
+ supports_render_target_invalidate,
+ supports_r8_texture_upload,
+ supports_qcom_tiled_rendering,
+ uses_native_clip_mask,
+ uses_native_antialiasing,
+ supports_image_external_essl3,
+ requires_vao_rebind_after_orphaning,
+ renderer_name,
+ },
+
+ color_formats,
+ bgra_formats,
+ bgra_pixel_type,
+ swizzle_settings: SwizzleSettings {
+ bgra8_sampling_swizzle,
+ },
+ depth_format,
+
+ depth_targets: FastHashMap::default(),
+
+ bound_textures: [0; 16],
+ bound_program: 0,
+ bound_program_name: Rc::new(std::ffi::CString::new("").unwrap()),
+ bound_vao: 0,
+ bound_read_fbo: (FBOId(0), DeviceIntPoint::zero()),
+ bound_draw_fbo: FBOId(0),
+ program_mode_id: UniformLocation::INVALID,
+ default_read_fbo: FBOId(0),
+ default_draw_fbo: FBOId(0),
+
+ depth_available: true,
+
+ max_texture_size,
+ cached_programs,
+ frame_id: GpuFrameId(0),
+ extensions,
+ texture_storage_usage,
+ requires_null_terminated_shader_source,
+ requires_texture_external_unbind,
+ is_software_webrender,
+ required_pbo_stride,
+ dump_shader_source,
+ surface_origin_is_top_left,
+
+ #[cfg(debug_assertions)]
+ shader_is_ready: false,
+
+ textures_created: 0,
+ textures_deleted: 0,
+ }
+ }
+
+ pub fn gl(&self) -> &dyn gl::Gl {
+ &*self.gl
+ }
+
+ pub fn rc_gl(&self) -> &Rc<dyn gl::Gl> {
+ &self.gl
+ }
+
+ pub fn set_parameter(&mut self, param: &Parameter) {
+ match param {
+ Parameter::Bool(BoolParameter::PboUploads, enabled) => {
+ if !self.is_software_webrender {
+ self.upload_method = if *enabled {
+ UploadMethod::PixelBuffer(crate::ONE_TIME_USAGE_HINT)
+ } else {
+ UploadMethod::Immediate
+ };
+ }
+ }
+ Parameter::Bool(BoolParameter::BatchedUploads, enabled) => {
+ self.use_batched_texture_uploads = *enabled;
+ }
+ Parameter::Bool(BoolParameter::DrawCallsForTextureCopy, enabled) => {
+ if self.capabilities.requires_batched_texture_uploads.is_none() {
+ self.use_draw_calls_for_texture_copy = *enabled;
+ }
+ }
+ Parameter::Int(IntParameter::BatchedUploadThreshold, threshold) => {
+ self.batched_upload_threshold = *threshold;
+ }
+ _ => {}
+ }
+ }
+
+ /// Ensures that the maximum texture size is less than or equal to the
+ /// provided value. If the provided value is less than the value supported
+ /// by the driver, the latter is used.
+ pub fn clamp_max_texture_size(&mut self, size: i32) {
+ self.max_texture_size = self.max_texture_size.min(size);
+ }
+
+ /// Returns the limit on texture dimensions (width or height).
+ pub fn max_texture_size(&self) -> i32 {
+ self.max_texture_size
+ }
+
+ pub fn surface_origin_is_top_left(&self) -> bool {
+ self.surface_origin_is_top_left
+ }
+
+ pub fn get_capabilities(&self) -> &Capabilities {
+ &self.capabilities
+ }
+
+ pub fn preferred_color_formats(&self) -> TextureFormatPair<ImageFormat> {
+ self.color_formats.clone()
+ }
+
+ pub fn swizzle_settings(&self) -> Option<SwizzleSettings> {
+ if self.capabilities.supports_texture_swizzle {
+ Some(self.swizzle_settings)
+ } else {
+ None
+ }
+ }
+
+ pub fn depth_bits(&self) -> i32 {
+ match self.depth_format {
+ gl::DEPTH_COMPONENT16 => 16,
+ gl::DEPTH_COMPONENT24 => 24,
+ _ => panic!("Unknown depth format {:?}", self.depth_format),
+ }
+ }
+
+ // See gpu_types.rs where we declare the number of possible documents and
+ // number of items per document. This should match up with that.
+ pub fn max_depth_ids(&self) -> i32 {
+ return 1 << (self.depth_bits() - RESERVE_DEPTH_BITS);
+ }
+
+ pub fn ortho_near_plane(&self) -> f32 {
+ return -self.max_depth_ids() as f32;
+ }
+
+ pub fn ortho_far_plane(&self) -> f32 {
+ return (self.max_depth_ids() - 1) as f32;
+ }
+
+ pub fn required_pbo_stride(&self) -> StrideAlignment {
+ self.required_pbo_stride
+ }
+
+ pub fn upload_method(&self) -> &UploadMethod {
+ &self.upload_method
+ }
+
+ pub fn use_batched_texture_uploads(&self) -> bool {
+ self.use_batched_texture_uploads
+ }
+
+ pub fn use_draw_calls_for_texture_copy(&self) -> bool {
+ self.use_draw_calls_for_texture_copy
+ }
+
+ pub fn batched_upload_threshold(&self) -> i32 {
+ self.batched_upload_threshold
+ }
+
+ pub fn reset_state(&mut self) {
+ for i in 0 .. self.bound_textures.len() {
+ self.bound_textures[i] = 0;
+ self.gl.active_texture(gl::TEXTURE0 + i as gl::GLuint);
+ self.gl.bind_texture(gl::TEXTURE_2D, 0);
+ }
+
+ self.bound_vao = 0;
+ self.gl.bind_vertex_array(0);
+
+ self.bound_read_fbo = (self.default_read_fbo, DeviceIntPoint::zero());
+ self.gl.bind_framebuffer(gl::READ_FRAMEBUFFER, self.default_read_fbo.0);
+
+ self.bound_draw_fbo = self.default_draw_fbo;
+ self.gl.bind_framebuffer(gl::DRAW_FRAMEBUFFER, self.bound_draw_fbo.0);
+ }
+
+ #[cfg(debug_assertions)]
+ fn print_shader_errors(source: &str, log: &str) {
+ // hacky way to extract the offending lines
+ if !log.starts_with("0:") && !log.starts_with("0(") {
+ return;
+ }
+ let end_pos = match log[2..].chars().position(|c| !c.is_digit(10)) {
+ Some(pos) => 2 + pos,
+ None => return,
+ };
+ let base_line_number = match log[2 .. end_pos].parse::<usize>() {
+ Ok(number) if number >= 2 => number - 2,
+ _ => return,
+ };
+ for (line, prefix) in source.lines().skip(base_line_number).zip(&["|",">","|"]) {
+ error!("{}\t{}", prefix, line);
+ }
+ }
+
+ pub fn compile_shader(
+ &self,
+ name: &str,
+ shader_type: gl::GLenum,
+ source: &String,
+ ) -> Result<gl::GLuint, ShaderError> {
+ debug!("compile {}", name);
+ let id = self.gl.create_shader(shader_type);
+
+ let mut new_source = Cow::from(source.as_str());
+ // Ensure the source strings we pass to glShaderSource are
+ // null-terminated on buggy platforms.
+ if self.requires_null_terminated_shader_source {
+ new_source.to_mut().push('\0');
+ }
+
+ self.gl.shader_source(id, &[new_source.as_bytes()]);
+ self.gl.compile_shader(id);
+ let log = self.gl.get_shader_info_log(id);
+ let mut status = [0];
+ unsafe {
+ self.gl.get_shader_iv(id, gl::COMPILE_STATUS, &mut status);
+ }
+ if status[0] == 0 {
+ let type_str = match shader_type {
+ gl::VERTEX_SHADER => "vertex",
+ gl::FRAGMENT_SHADER => "fragment",
+ _ => panic!("Unexpected shader type {:x}", shader_type),
+ };
+ error!("Failed to compile {} shader: {}\n{}", type_str, name, log);
+ #[cfg(debug_assertions)]
+ Self::print_shader_errors(source, &log);
+ Err(ShaderError::Compilation(name.to_string(), log))
+ } else {
+ if !log.is_empty() {
+ warn!("Warnings detected on shader: {}\n{}", name, log);
+ }
+ Ok(id)
+ }
+ }
+
+ pub fn begin_frame(&mut self) -> GpuFrameId {
+ debug_assert!(!self.inside_frame);
+ self.inside_frame = true;
+ #[cfg(debug_assertions)]
+ {
+ self.shader_is_ready = false;
+ }
+
+ self.textures_created = 0;
+ self.textures_deleted = 0;
+
+ // If our profiler state has changed, apply or remove the profiling
+ // wrapper from our GL context.
+ let being_profiled = profiler::thread_is_being_profiled();
+ let using_wrapper = self.base_gl.is_some();
+
+ // We can usually unwind driver stacks on x86 so we don't need to manually instrument
+ // gl calls there. Timestamps can be pretty expensive on Windows (2us each and perhaps
+ // an opportunity to be descheduled?) which makes the profiles gathered with this
+ // turned on less useful so only profile on ARM.
+ if cfg!(any(target_arch = "arm", target_arch = "aarch64"))
+ && being_profiled
+ && !using_wrapper
+ {
+ fn note(name: &str, duration: Duration) {
+ profiler::add_text_marker("OpenGL Calls", name, duration);
+ }
+ let threshold = Duration::from_millis(1);
+ let wrapped = gl::ProfilingGl::wrap(self.gl.clone(), threshold, note);
+ let base = mem::replace(&mut self.gl, wrapped);
+ self.base_gl = Some(base);
+ } else if !being_profiled && using_wrapper {
+ self.gl = self.base_gl.take().unwrap();
+ }
+
+ // Retrieve the currently set FBO.
+ let mut default_read_fbo = [0];
+ unsafe {
+ self.gl.get_integer_v(gl::READ_FRAMEBUFFER_BINDING, &mut default_read_fbo);
+ }
+ self.default_read_fbo = FBOId(default_read_fbo[0] as gl::GLuint);
+ let mut default_draw_fbo = [0];
+ unsafe {
+ self.gl.get_integer_v(gl::DRAW_FRAMEBUFFER_BINDING, &mut default_draw_fbo);
+ }
+ self.default_draw_fbo = FBOId(default_draw_fbo[0] as gl::GLuint);
+
+ // Shader state
+ self.bound_program = 0;
+ self.program_mode_id = UniformLocation::INVALID;
+ self.gl.use_program(0);
+
+ // Reset common state
+ self.reset_state();
+
+ // Pixel op state
+ self.gl.pixel_store_i(gl::UNPACK_ALIGNMENT, 1);
+ self.gl.bind_buffer(gl::PIXEL_UNPACK_BUFFER, 0);
+
+ // Default is sampler 0, always
+ self.gl.active_texture(gl::TEXTURE0);
+
+ self.frame_id
+ }
+
+ fn bind_texture_impl(
+ &mut self,
+ slot: TextureSlot,
+ id: gl::GLuint,
+ target: gl::GLenum,
+ set_swizzle: Option<Swizzle>,
+ image_rendering: Option<ImageRendering>,
+ ) {
+ debug_assert!(self.inside_frame);
+
+ if self.bound_textures[slot.0] != id || set_swizzle.is_some() || image_rendering.is_some() {
+ self.gl.active_texture(gl::TEXTURE0 + slot.0 as gl::GLuint);
+ // The android emulator gets confused if you don't explicitly unbind any texture
+ // from GL_TEXTURE_EXTERNAL_OES before binding to GL_TEXTURE_2D. See bug 1636085.
+ if target == gl::TEXTURE_2D && self.requires_texture_external_unbind {
+ self.gl.bind_texture(gl::TEXTURE_EXTERNAL_OES, 0);
+ }
+ self.gl.bind_texture(target, id);
+ if let Some(swizzle) = set_swizzle {
+ if self.capabilities.supports_texture_swizzle {
+ let components = match swizzle {
+ Swizzle::Rgba => [gl::RED, gl::GREEN, gl::BLUE, gl::ALPHA],
+ Swizzle::Bgra => [gl::BLUE, gl::GREEN, gl::RED, gl::ALPHA],
+ };
+ self.gl.tex_parameter_i(target, gl::TEXTURE_SWIZZLE_R, components[0] as i32);
+ self.gl.tex_parameter_i(target, gl::TEXTURE_SWIZZLE_G, components[1] as i32);
+ self.gl.tex_parameter_i(target, gl::TEXTURE_SWIZZLE_B, components[2] as i32);
+ self.gl.tex_parameter_i(target, gl::TEXTURE_SWIZZLE_A, components[3] as i32);
+ } else {
+ debug_assert_eq!(swizzle, Swizzle::default());
+ }
+ }
+ if let Some(image_rendering) = image_rendering {
+ let filter = match image_rendering {
+ ImageRendering::Auto | ImageRendering::CrispEdges => gl::LINEAR,
+ ImageRendering::Pixelated => gl::NEAREST,
+ };
+ self.gl.tex_parameter_i(target, gl::TEXTURE_MIN_FILTER, filter as i32);
+ self.gl.tex_parameter_i(target, gl::TEXTURE_MAG_FILTER, filter as i32);
+ }
+ self.gl.active_texture(gl::TEXTURE0);
+ self.bound_textures[slot.0] = id;
+ }
+ }
+
+ pub fn bind_texture<S>(&mut self, slot: S, texture: &Texture, swizzle: Swizzle)
+ where
+ S: Into<TextureSlot>,
+ {
+ let old_swizzle = texture.active_swizzle.replace(swizzle);
+ let set_swizzle = if old_swizzle != swizzle {
+ Some(swizzle)
+ } else {
+ None
+ };
+ self.bind_texture_impl(slot.into(), texture.id, texture.target, set_swizzle, None);
+ }
+
+ pub fn bind_external_texture<S>(&mut self, slot: S, external_texture: &ExternalTexture)
+ where
+ S: Into<TextureSlot>,
+ {
+ self.bind_texture_impl(
+ slot.into(),
+ external_texture.id,
+ external_texture.target,
+ None,
+ Some(external_texture.image_rendering),
+ );
+ }
+
+ pub fn bind_read_target_impl(
+ &mut self,
+ fbo_id: FBOId,
+ offset: DeviceIntPoint,
+ ) {
+ debug_assert!(self.inside_frame);
+
+ if self.bound_read_fbo != (fbo_id, offset) {
+ fbo_id.bind(self.gl(), FBOTarget::Read);
+ }
+
+ self.bound_read_fbo = (fbo_id, offset);
+ }
+
+ pub fn bind_read_target(&mut self, target: ReadTarget) {
+ let fbo_id = match target {
+ ReadTarget::Default => self.default_read_fbo,
+ ReadTarget::Texture { fbo_id } => fbo_id,
+ ReadTarget::External { fbo } => fbo,
+ ReadTarget::NativeSurface { fbo_id, .. } => fbo_id,
+ };
+
+ self.bind_read_target_impl(fbo_id, target.offset())
+ }
+
+ fn bind_draw_target_impl(&mut self, fbo_id: FBOId) {
+ debug_assert!(self.inside_frame);
+
+ if self.bound_draw_fbo != fbo_id {
+ self.bound_draw_fbo = fbo_id;
+ fbo_id.bind(self.gl(), FBOTarget::Draw);
+ }
+ }
+
+ pub fn reset_read_target(&mut self) {
+ let fbo = self.default_read_fbo;
+ self.bind_read_target_impl(fbo, DeviceIntPoint::zero());
+ }
+
+
+ pub fn reset_draw_target(&mut self) {
+ let fbo = self.default_draw_fbo;
+ self.bind_draw_target_impl(fbo);
+ self.depth_available = true;
+ }
+
+ pub fn bind_draw_target(
+ &mut self,
+ target: DrawTarget,
+ ) {
+ let (fbo_id, rect, depth_available) = match target {
+ DrawTarget::Default { rect, .. } => {
+ (self.default_draw_fbo, rect, false)
+ }
+ DrawTarget::Texture { dimensions, fbo_id, with_depth, .. } => {
+ let rect = FramebufferIntRect::from_size(
+ device_size_as_framebuffer_size(dimensions),
+ );
+ (fbo_id, rect, with_depth)
+ },
+ DrawTarget::External { fbo, size } => {
+ (fbo, size.into(), false)
+ }
+ DrawTarget::NativeSurface { external_fbo_id, offset, dimensions, .. } => {
+ (
+ FBOId(external_fbo_id),
+ device_rect_as_framebuffer_rect(&DeviceIntRect::from_origin_and_size(offset, dimensions)),
+ true
+ )
+ }
+ };
+
+ self.depth_available = depth_available;
+ self.bind_draw_target_impl(fbo_id);
+ self.gl.viewport(
+ rect.min.x,
+ rect.min.y,
+ rect.width(),
+ rect.height(),
+ );
+ }
+
+ /// Creates an unbound FBO object. Additional attachment API calls are
+ /// required to make it complete.
+ pub fn create_fbo(&mut self) -> FBOId {
+ FBOId(self.gl.gen_framebuffers(1)[0])
+ }
+
+ /// Creates an FBO with the given texture bound as the color attachment.
+ pub fn create_fbo_for_external_texture(&mut self, texture_id: u32) -> FBOId {
+ let fbo = self.create_fbo();
+ fbo.bind(self.gl(), FBOTarget::Draw);
+ self.gl.framebuffer_texture_2d(
+ gl::DRAW_FRAMEBUFFER,
+ gl::COLOR_ATTACHMENT0,
+ gl::TEXTURE_2D,
+ texture_id,
+ 0,
+ );
+ debug_assert_eq!(
+ self.gl.check_frame_buffer_status(gl::DRAW_FRAMEBUFFER),
+ gl::FRAMEBUFFER_COMPLETE,
+ "Incomplete framebuffer",
+ );
+ self.bound_draw_fbo.bind(self.gl(), FBOTarget::Draw);
+ fbo
+ }
+
+ pub fn delete_fbo(&mut self, fbo: FBOId) {
+ self.gl.delete_framebuffers(&[fbo.0]);
+ }
+
+ pub fn bind_external_draw_target(&mut self, fbo_id: FBOId) {
+ debug_assert!(self.inside_frame);
+
+ if self.bound_draw_fbo != fbo_id {
+ self.bound_draw_fbo = fbo_id;
+ fbo_id.bind(self.gl(), FBOTarget::Draw);
+ }
+ }
+
+ /// Link a program, attaching the supplied vertex format.
+ ///
+ /// If `create_program()` finds a binary shader on disk, it will kick
+ /// off linking immediately, which some drivers (notably ANGLE) run
+ /// in parallel on background threads. As such, this function should
+ /// ideally be run sometime later, to give the driver time to do that
+ /// before blocking due to an API call accessing the shader.
+ ///
+ /// This generally means that the first run of the application will have
+ /// to do a bunch of blocking work to compile the shader from source, but
+ /// subsequent runs should load quickly.
+ pub fn link_program(
+ &mut self,
+ program: &mut Program,
+ descriptor: &VertexDescriptor,
+ ) -> Result<(), ShaderError> {
+ profile_scope!("compile shader");
+
+ let _guard = CrashAnnotatorGuard::new(
+ &self.crash_annotator,
+ CrashAnnotation::CompileShader,
+ &program.source_info.full_name_cstr
+ );
+
+ assert!(!program.is_initialized());
+ let mut build_program = true;
+ let info = &program.source_info;
+
+ // See if we hit the binary shader cache
+ if let Some(ref cached_programs) = self.cached_programs {
+ // If the shader is not in the cache, attempt to load it from disk
+ if cached_programs.entries.borrow().get(&program.source_info.digest).is_none() {
+ if let Some(ref handler) = cached_programs.program_cache_handler {
+ handler.try_load_shader_from_disk(&program.source_info.digest, cached_programs);
+ if let Some(entry) = cached_programs.entries.borrow().get(&program.source_info.digest) {
+ self.gl.program_binary(program.id, entry.binary.format, &entry.binary.bytes);
+ }
+ }
+ }
+
+ if let Some(entry) = cached_programs.entries.borrow_mut().get_mut(&info.digest) {
+ let mut link_status = [0];
+ unsafe {
+ self.gl.get_program_iv(program.id, gl::LINK_STATUS, &mut link_status);
+ }
+ if link_status[0] == 0 {
+ let error_log = self.gl.get_program_info_log(program.id);
+ error!(
+ "Failed to load a program object with a program binary: {} renderer {}\n{}",
+ &info.base_filename,
+ self.capabilities.renderer_name,
+ error_log
+ );
+ if let Some(ref program_cache_handler) = cached_programs.program_cache_handler {
+ program_cache_handler.notify_program_binary_failed(&entry.binary);
+ }
+ } else {
+ entry.linked = true;
+ build_program = false;
+ }
+ }
+ }
+
+ // If not, we need to do a normal compile + link pass.
+ if build_program {
+ // Compile the vertex shader
+ let vs_source = info.compute_source(self, ShaderKind::Vertex);
+ let vs_id = match self.compile_shader(&info.full_name(), gl::VERTEX_SHADER, &vs_source) {
+ Ok(vs_id) => vs_id,
+ Err(err) => return Err(err),
+ };
+
+ // Compile the fragment shader
+ let fs_source = info.compute_source(self, ShaderKind::Fragment);
+ let fs_id =
+ match self.compile_shader(&info.full_name(), gl::FRAGMENT_SHADER, &fs_source) {
+ Ok(fs_id) => fs_id,
+ Err(err) => {
+ self.gl.delete_shader(vs_id);
+ return Err(err);
+ }
+ };
+
+ // Check if shader source should be dumped
+ if Some(info.base_filename) == self.dump_shader_source.as_ref().map(String::as_ref) {
+ let path = std::path::Path::new(info.base_filename);
+ std::fs::write(path.with_extension("vert"), vs_source).unwrap();
+ std::fs::write(path.with_extension("frag"), fs_source).unwrap();
+ }
+
+ // Attach shaders
+ self.gl.attach_shader(program.id, vs_id);
+ self.gl.attach_shader(program.id, fs_id);
+
+ // Bind vertex attributes
+ for (i, attr) in descriptor
+ .vertex_attributes
+ .iter()
+ .chain(descriptor.instance_attributes.iter())
+ .enumerate()
+ {
+ self.gl
+ .bind_attrib_location(program.id, i as gl::GLuint, attr.name);
+ }
+
+ if self.cached_programs.is_some() {
+ self.gl.program_parameter_i(program.id, gl::PROGRAM_BINARY_RETRIEVABLE_HINT, gl::TRUE as gl::GLint);
+ }
+
+ // Link!
+ self.gl.link_program(program.id);
+
+ // GL recommends detaching and deleting shaders once the link
+ // is complete (whether successful or not). This allows the driver
+ // to free any memory associated with the parsing and compilation.
+ self.gl.detach_shader(program.id, vs_id);
+ self.gl.detach_shader(program.id, fs_id);
+ self.gl.delete_shader(vs_id);
+ self.gl.delete_shader(fs_id);
+
+ let mut link_status = [0];
+ unsafe {
+ self.gl.get_program_iv(program.id, gl::LINK_STATUS, &mut link_status);
+ }
+ if link_status[0] == 0 {
+ let error_log = self.gl.get_program_info_log(program.id);
+ error!(
+ "Failed to link shader program: {}\n{}",
+ &info.base_filename,
+ error_log
+ );
+ self.gl.delete_program(program.id);
+ return Err(ShaderError::Link(info.base_filename.to_owned(), error_log));
+ }
+
+ if let Some(ref cached_programs) = self.cached_programs {
+ if !cached_programs.entries.borrow().contains_key(&info.digest) {
+ let (buffer, format) = self.gl.get_program_binary(program.id);
+ if buffer.len() > 0 {
+ let binary = Arc::new(ProgramBinary::new(buffer, format, info.digest.clone()));
+ cached_programs.add_new_program_binary(binary);
+ }
+ }
+ }
+ }
+
+ // If we get here, the link succeeded, so get the uniforms.
+ program.is_initialized = true;
+ program.u_transform = self.gl.get_uniform_location(program.id, "uTransform");
+ program.u_mode = self.gl.get_uniform_location(program.id, "uMode");
+ program.u_texture_size = self.gl.get_uniform_location(program.id, "uTextureSize");
+
+ Ok(())
+ }
+
+ pub fn bind_program(&mut self, program: &Program) -> bool {
+ debug_assert!(self.inside_frame);
+ debug_assert!(program.is_initialized());
+ if !program.is_initialized() {
+ return false;
+ }
+ #[cfg(debug_assertions)]
+ {
+ self.shader_is_ready = true;
+ }
+
+ if self.bound_program != program.id {
+ self.gl.use_program(program.id);
+ self.bound_program = program.id;
+ self.bound_program_name = program.source_info.full_name_cstr.clone();
+ self.program_mode_id = UniformLocation(program.u_mode);
+ }
+ true
+ }
+
+ pub fn create_texture(
+ &mut self,
+ target: ImageBufferKind,
+ format: ImageFormat,
+ mut width: i32,
+ mut height: i32,
+ filter: TextureFilter,
+ render_target: Option<RenderTargetInfo>,
+ ) -> Texture {
+ debug_assert!(self.inside_frame);
+
+ if width > self.max_texture_size || height > self.max_texture_size {
+ error!("Attempting to allocate a texture of size {}x{} above the limit, trimming", width, height);
+ width = width.min(self.max_texture_size);
+ height = height.min(self.max_texture_size);
+ }
+
+ // Set up the texture book-keeping.
+ let mut texture = Texture {
+ id: self.gl.gen_textures(1)[0],
+ target: get_gl_target(target),
+ size: DeviceIntSize::new(width, height),
+ format,
+ filter,
+ active_swizzle: Cell::default(),
+ fbo: None,
+ fbo_with_depth: None,
+ last_frame_used: self.frame_id,
+ flags: TextureFlags::default(),
+ };
+ self.bind_texture(DEFAULT_TEXTURE, &texture, Swizzle::default());
+ self.set_texture_parameters(texture.target, filter);
+
+ if self.capabilities.supports_texture_usage && render_target.is_some() {
+ self.gl.tex_parameter_i(texture.target, gl::TEXTURE_USAGE_ANGLE, gl::FRAMEBUFFER_ATTACHMENT_ANGLE as gl::GLint);
+ }
+
+ // Allocate storage.
+ let desc = self.gl_describe_format(texture.format);
+
+ // Firefox doesn't use mipmaps, but Servo uses them for standalone image
+ // textures images larger than 512 pixels. This is the only case where
+ // we set the filter to trilinear.
+ let mipmap_levels = if texture.filter == TextureFilter::Trilinear {
+ let max_dimension = cmp::max(width, height);
+ ((max_dimension) as f64).log2() as gl::GLint + 1
+ } else {
+ 1
+ };
+
+ // We never want to upload texture data at the same time as allocating the texture.
+ self.gl.bind_buffer(gl::PIXEL_UNPACK_BUFFER, 0);
+
+ // Use glTexStorage where available, since it avoids allocating
+ // unnecessary mipmap storage and generally improves performance with
+ // stronger invariants.
+ let use_texture_storage = match self.texture_storage_usage {
+ TexStorageUsage::Always => true,
+ TexStorageUsage::NonBGRA8 => texture.format != ImageFormat::BGRA8,
+ TexStorageUsage::Never => false,
+ };
+ if use_texture_storage {
+ self.gl.tex_storage_2d(
+ texture.target,
+ mipmap_levels,
+ desc.internal,
+ texture.size.width as gl::GLint,
+ texture.size.height as gl::GLint,
+ );
+ } else {
+ self.gl.tex_image_2d(
+ texture.target,
+ 0,
+ desc.internal as gl::GLint,
+ texture.size.width as gl::GLint,
+ texture.size.height as gl::GLint,
+ 0,
+ desc.external,
+ desc.pixel_type,
+ None,
+ );
+ }
+
+ // Set up FBOs, if required.
+ if let Some(rt_info) = render_target {
+ self.init_fbos(&mut texture, false);
+ if rt_info.has_depth {
+ self.init_fbos(&mut texture, true);
+ }
+ }
+
+ self.textures_created += 1;
+
+ texture
+ }
+
+ fn set_texture_parameters(&mut self, target: gl::GLuint, filter: TextureFilter) {
+ let mag_filter = match filter {
+ TextureFilter::Nearest => gl::NEAREST,
+ TextureFilter::Linear | TextureFilter::Trilinear => gl::LINEAR,
+ };
+
+ let min_filter = match filter {
+ TextureFilter::Nearest => gl::NEAREST,
+ TextureFilter::Linear => gl::LINEAR,
+ TextureFilter::Trilinear => gl::LINEAR_MIPMAP_LINEAR,
+ };
+
+ self.gl
+ .tex_parameter_i(target, gl::TEXTURE_MAG_FILTER, mag_filter as gl::GLint);
+ self.gl
+ .tex_parameter_i(target, gl::TEXTURE_MIN_FILTER, min_filter as gl::GLint);
+
+ self.gl
+ .tex_parameter_i(target, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as gl::GLint);
+ self.gl
+ .tex_parameter_i(target, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as gl::GLint);
+ }
+
+ /// Copies the entire contents of one texture to another. The dest texture must be at least
+ /// as large as the source texture in each dimension. No scaling is performed, so if the dest
+ /// texture is larger than the source texture then some of its pixels will not be written to.
+ pub fn copy_entire_texture(
+ &mut self,
+ dst: &mut Texture,
+ src: &Texture,
+ ) {
+ debug_assert!(self.inside_frame);
+ debug_assert!(dst.size.width >= src.size.width);
+ debug_assert!(dst.size.height >= src.size.height);
+
+ self.copy_texture_sub_region(
+ src,
+ 0,
+ 0,
+ dst,
+ 0,
+ 0,
+ src.size.width as _,
+ src.size.height as _,
+ );
+ }
+
+ /// Copies the specified subregion from src_texture to dest_texture.
+ pub fn copy_texture_sub_region(
+ &mut self,
+ src_texture: &Texture,
+ src_x: usize,
+ src_y: usize,
+ dest_texture: &Texture,
+ dest_x: usize,
+ dest_y: usize,
+ width: usize,
+ height: usize,
+ ) {
+ if self.capabilities.supports_copy_image_sub_data {
+ assert_ne!(
+ src_texture.id, dest_texture.id,
+ "glCopyImageSubData's behaviour is undefined if src and dst images are identical and the rectangles overlap."
+ );
+ unsafe {
+ self.gl.copy_image_sub_data(
+ src_texture.id,
+ src_texture.target,
+ 0,
+ src_x as _,
+ src_y as _,
+ 0,
+ dest_texture.id,
+ dest_texture.target,
+ 0,
+ dest_x as _,
+ dest_y as _,
+ 0,
+ width as _,
+ height as _,
+ 1,
+ );
+ }
+ } else {
+ let src_offset = FramebufferIntPoint::new(src_x as i32, src_y as i32);
+ let dest_offset = FramebufferIntPoint::new(dest_x as i32, dest_y as i32);
+ let size = FramebufferIntSize::new(width as i32, height as i32);
+
+ self.blit_render_target(
+ ReadTarget::from_texture(src_texture),
+ FramebufferIntRect::from_origin_and_size(src_offset, size),
+ DrawTarget::from_texture(dest_texture, false),
+ FramebufferIntRect::from_origin_and_size(dest_offset, size),
+ // In most cases the filter shouldn't matter, as there is no scaling involved
+ // in the blit. We were previously using Linear, but this caused issues when
+ // blitting RGBAF32 textures on Mali, so use Nearest to be safe.
+ TextureFilter::Nearest,
+ );
+ }
+ }
+
+ /// Notifies the device that the contents of a render target are no longer
+ /// needed.
+ pub fn invalidate_render_target(&mut self, texture: &Texture) {
+ if self.capabilities.supports_render_target_invalidate {
+ let (fbo, attachments) = if texture.supports_depth() {
+ (&texture.fbo_with_depth,
+ &[gl::COLOR_ATTACHMENT0, gl::DEPTH_ATTACHMENT] as &[gl::GLenum])
+ } else {
+ (&texture.fbo, &[gl::COLOR_ATTACHMENT0] as &[gl::GLenum])
+ };
+
+ if let Some(fbo_id) = fbo {
+ let original_bound_fbo = self.bound_draw_fbo;
+ // Note: The invalidate extension may not be supported, in which
+ // case this is a no-op. That's ok though, because it's just a
+ // hint.
+ self.bind_external_draw_target(*fbo_id);
+ self.gl.invalidate_framebuffer(gl::FRAMEBUFFER, attachments);
+ self.bind_external_draw_target(original_bound_fbo);
+ }
+ }
+ }
+
+ /// Notifies the device that the contents of the current framebuffer's depth
+ /// attachment is no longer needed. Unlike invalidate_render_target, this can
+ /// be called even when the contents of the colour attachment is still required.
+ /// This should be called before unbinding the framebuffer at the end of a pass,
+ /// to allow tiled GPUs to avoid writing the contents back to memory.
+ pub fn invalidate_depth_target(&mut self) {
+ assert!(self.depth_available);
+ let attachments = if self.bound_draw_fbo == self.default_draw_fbo {
+ &[gl::DEPTH] as &[gl::GLenum]
+ } else {
+ &[gl::DEPTH_ATTACHMENT] as &[gl::GLenum]
+ };
+ self.gl.invalidate_framebuffer(gl::DRAW_FRAMEBUFFER, attachments);
+ }
+
+ /// Notifies the device that a render target is about to be reused.
+ ///
+ /// This method adds or removes a depth target as necessary.
+ pub fn reuse_render_target<T: Texel>(
+ &mut self,
+ texture: &mut Texture,
+ rt_info: RenderTargetInfo,
+ ) {
+ texture.last_frame_used = self.frame_id;
+
+ // Add depth support if needed.
+ if rt_info.has_depth && !texture.supports_depth() {
+ self.init_fbos(texture, true);
+ }
+ }
+
+ fn init_fbos(&mut self, texture: &mut Texture, with_depth: bool) {
+ let (fbo, depth_rb) = if with_depth {
+ let depth_target = self.acquire_depth_target(texture.get_dimensions());
+ (&mut texture.fbo_with_depth, Some(depth_target))
+ } else {
+ (&mut texture.fbo, None)
+ };
+
+ // Generate the FBOs.
+ assert!(fbo.is_none());
+ let fbo_id = FBOId(*self.gl.gen_framebuffers(1).first().unwrap());
+ *fbo = Some(fbo_id);
+
+ // Bind the FBOs.
+ let original_bound_fbo = self.bound_draw_fbo;
+
+ self.bind_external_draw_target(fbo_id);
+
+ self.gl.framebuffer_texture_2d(
+ gl::DRAW_FRAMEBUFFER,
+ gl::COLOR_ATTACHMENT0,
+ texture.target,
+ texture.id,
+ 0,
+ );
+
+ if let Some(depth_rb) = depth_rb {
+ self.gl.framebuffer_renderbuffer(
+ gl::DRAW_FRAMEBUFFER,
+ gl::DEPTH_ATTACHMENT,
+ gl::RENDERBUFFER,
+ depth_rb.0,
+ );
+ }
+
+ debug_assert_eq!(
+ self.gl.check_frame_buffer_status(gl::DRAW_FRAMEBUFFER),
+ gl::FRAMEBUFFER_COMPLETE,
+ "Incomplete framebuffer",
+ );
+
+ self.bind_external_draw_target(original_bound_fbo);
+ }
+
+ fn acquire_depth_target(&mut self, dimensions: DeviceIntSize) -> RBOId {
+ let gl = &self.gl;
+ let depth_format = self.depth_format;
+ let target = self.depth_targets.entry(dimensions).or_insert_with(|| {
+ let renderbuffer_ids = gl.gen_renderbuffers(1);
+ let depth_rb = renderbuffer_ids[0];
+ gl.bind_renderbuffer(gl::RENDERBUFFER, depth_rb);
+ gl.renderbuffer_storage(
+ gl::RENDERBUFFER,
+ depth_format,
+ dimensions.width as _,
+ dimensions.height as _,
+ );
+ SharedDepthTarget {
+ rbo_id: RBOId(depth_rb),
+ refcount: 0,
+ }
+ });
+ target.refcount += 1;
+ target.rbo_id
+ }
+
+ fn release_depth_target(&mut self, dimensions: DeviceIntSize) {
+ let mut entry = match self.depth_targets.entry(dimensions) {
+ Entry::Occupied(x) => x,
+ Entry::Vacant(..) => panic!("Releasing unknown depth target"),
+ };
+ debug_assert!(entry.get().refcount != 0);
+ entry.get_mut().refcount -= 1;
+ if entry.get().refcount == 0 {
+ let (_, target) = entry.remove_entry();
+ self.gl.delete_renderbuffers(&[target.rbo_id.0]);
+ }
+ }
+
+ /// Perform a blit between self.bound_read_fbo and self.bound_draw_fbo.
+ fn blit_render_target_impl(
+ &mut self,
+ src_rect: FramebufferIntRect,
+ dest_rect: FramebufferIntRect,
+ filter: TextureFilter,
+ ) {
+ debug_assert!(self.inside_frame);
+
+ let filter = match filter {
+ TextureFilter::Nearest => gl::NEAREST,
+ TextureFilter::Linear | TextureFilter::Trilinear => gl::LINEAR,
+ };
+
+ let src_x0 = src_rect.min.x + self.bound_read_fbo.1.x;
+ let src_y0 = src_rect.min.y + self.bound_read_fbo.1.y;
+
+ self.gl.blit_framebuffer(
+ src_x0,
+ src_y0,
+ src_x0 + src_rect.width(),
+ src_y0 + src_rect.height(),
+ dest_rect.min.x,
+ dest_rect.min.y,
+ dest_rect.max.x,
+ dest_rect.max.y,
+ gl::COLOR_BUFFER_BIT,
+ filter,
+ );
+ }
+
+ /// Perform a blit between src_target and dest_target.
+ /// This will overwrite self.bound_read_fbo and self.bound_draw_fbo.
+ pub fn blit_render_target(
+ &mut self,
+ src_target: ReadTarget,
+ src_rect: FramebufferIntRect,
+ dest_target: DrawTarget,
+ dest_rect: FramebufferIntRect,
+ filter: TextureFilter,
+ ) {
+ debug_assert!(self.inside_frame);
+
+ self.bind_read_target(src_target);
+
+ self.bind_draw_target(dest_target);
+
+ self.blit_render_target_impl(src_rect, dest_rect, filter);
+ }
+
+ /// Performs a blit while flipping vertically. Useful for blitting textures
+ /// (which use origin-bottom-left) to the main framebuffer (which uses
+ /// origin-top-left).
+ pub fn blit_render_target_invert_y(
+ &mut self,
+ src_target: ReadTarget,
+ src_rect: FramebufferIntRect,
+ dest_target: DrawTarget,
+ dest_rect: FramebufferIntRect,
+ ) {
+ debug_assert!(self.inside_frame);
+
+ let mut inverted_dest_rect = dest_rect;
+ inverted_dest_rect.min.y = dest_rect.max.y;
+ inverted_dest_rect.max.y = dest_rect.min.y;
+
+ self.blit_render_target(
+ src_target,
+ src_rect,
+ dest_target,
+ inverted_dest_rect,
+ TextureFilter::Linear,
+ );
+ }
+
+ pub fn delete_texture(&mut self, mut texture: Texture) {
+ debug_assert!(self.inside_frame);
+ let had_depth = texture.supports_depth();
+ if let Some(fbo) = texture.fbo {
+ self.gl.delete_framebuffers(&[fbo.0]);
+ texture.fbo = None;
+ }
+ if let Some(fbo) = texture.fbo_with_depth {
+ self.gl.delete_framebuffers(&[fbo.0]);
+ texture.fbo_with_depth = None;
+ }
+
+ if had_depth {
+ self.release_depth_target(texture.get_dimensions());
+ }
+
+ self.gl.delete_textures(&[texture.id]);
+
+ for bound_texture in &mut self.bound_textures {
+ if *bound_texture == texture.id {
+ *bound_texture = 0;
+ }
+ }
+
+ self.textures_deleted += 1;
+
+ // Disarm the assert in Texture::drop().
+ texture.id = 0;
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn delete_external_texture(&mut self, mut external: ExternalTexture) {
+ self.gl.delete_textures(&[external.id]);
+ external.id = 0;
+ }
+
+ pub fn delete_program(&mut self, mut program: Program) {
+ self.gl.delete_program(program.id);
+ program.id = 0;
+ }
+
+ /// Create a shader program and link it immediately.
+ pub fn create_program_linked(
+ &mut self,
+ base_filename: &'static str,
+ features: &[&'static str],
+ descriptor: &VertexDescriptor,
+ ) -> Result<Program, ShaderError> {
+ let mut program = self.create_program(base_filename, features)?;
+ self.link_program(&mut program, descriptor)?;
+ Ok(program)
+ }
+
+ /// Create a shader program. This does minimal amount of work to start
+ /// loading a binary shader. If a binary shader is found, we invoke
+ /// glProgramBinary, which, at least on ANGLE, will load and link the
+ /// binary on a background thread. This can speed things up later when
+ /// we invoke `link_program()`.
+ pub fn create_program(
+ &mut self,
+ base_filename: &'static str,
+ features: &[&'static str],
+ ) -> Result<Program, ShaderError> {
+ debug_assert!(self.inside_frame);
+
+ let source_info = ProgramSourceInfo::new(self, base_filename, features);
+
+ // Create program
+ let pid = self.gl.create_program();
+
+ // Attempt to load a cached binary if possible.
+ if let Some(ref cached_programs) = self.cached_programs {
+ if let Some(entry) = cached_programs.entries.borrow().get(&source_info.digest) {
+ self.gl.program_binary(pid, entry.binary.format, &entry.binary.bytes);
+ }
+ }
+
+ // Use 0 for the uniforms as they are initialized by link_program.
+ let program = Program {
+ id: pid,
+ u_transform: 0,
+ u_mode: 0,
+ u_texture_size: 0,
+ source_info,
+ is_initialized: false,
+ };
+
+ Ok(program)
+ }
+
+ fn build_shader_string<F: FnMut(&str)>(
+ &self,
+ features: &[&'static str],
+ kind: ShaderKind,
+ base_filename: &str,
+ output: F,
+ ) {
+ do_build_shader_string(
+ get_shader_version(&*self.gl),
+ features,
+ kind,
+ base_filename,
+ &|f| get_unoptimized_shader_source(f, self.resource_override_path.as_ref()),
+ output,
+ )
+ }
+
+ pub fn bind_shader_samplers<S>(&mut self, program: &Program, bindings: &[(&'static str, S)])
+ where
+ S: Into<TextureSlot> + Copy,
+ {
+ // bind_program() must be called before calling bind_shader_samplers
+ assert_eq!(self.bound_program, program.id);
+
+ for binding in bindings {
+ let u_location = self.gl.get_uniform_location(program.id, binding.0);
+ if u_location != -1 {
+ self.bind_program(program);
+ self.gl
+ .uniform_1i(u_location, binding.1.into().0 as gl::GLint);
+ }
+ }
+ }
+
+ pub fn get_uniform_location(&self, program: &Program, name: &str) -> UniformLocation {
+ UniformLocation(self.gl.get_uniform_location(program.id, name))
+ }
+
+ pub fn set_uniforms(
+ &self,
+ program: &Program,
+ transform: &Transform3D<f32>,
+ ) {
+ debug_assert!(self.inside_frame);
+ #[cfg(debug_assertions)]
+ debug_assert!(self.shader_is_ready);
+
+ self.gl
+ .uniform_matrix_4fv(program.u_transform, false, &transform.to_array());
+ }
+
+ pub fn switch_mode(&self, mode: i32) {
+ debug_assert!(self.inside_frame);
+ #[cfg(debug_assertions)]
+ debug_assert!(self.shader_is_ready);
+
+ self.gl.uniform_1i(self.program_mode_id.0, mode);
+ }
+
+ /// Sets the uTextureSize uniform. Most shaders do not require this to be called
+ /// as they use the textureSize GLSL function instead.
+ pub fn set_shader_texture_size(
+ &self,
+ program: &Program,
+ texture_size: DeviceSize,
+ ) {
+ debug_assert!(self.inside_frame);
+ #[cfg(debug_assertions)]
+ debug_assert!(self.shader_is_ready);
+
+ if program.u_texture_size != -1 {
+ self.gl.uniform_2f(program.u_texture_size, texture_size.width, texture_size.height);
+ }
+ }
+
+ pub fn create_pbo(&mut self) -> PBO {
+ let id = self.gl.gen_buffers(1)[0];
+ PBO {
+ id,
+ reserved_size: 0,
+ }
+ }
+
+ pub fn create_pbo_with_size(&mut self, size: usize) -> PBO {
+ let mut pbo = self.create_pbo();
+
+ self.gl.bind_buffer(gl::PIXEL_PACK_BUFFER, pbo.id);
+ self.gl.pixel_store_i(gl::PACK_ALIGNMENT, 1);
+ self.gl.buffer_data_untyped(
+ gl::PIXEL_PACK_BUFFER,
+ size as _,
+ ptr::null(),
+ gl::STREAM_READ,
+ );
+ self.gl.bind_buffer(gl::PIXEL_UNPACK_BUFFER, 0);
+
+ pbo.reserved_size = size;
+ pbo
+ }
+
+ pub fn read_pixels_into_pbo(
+ &mut self,
+ read_target: ReadTarget,
+ rect: DeviceIntRect,
+ format: ImageFormat,
+ pbo: &PBO,
+ ) {
+ let byte_size = rect.area() as usize * format.bytes_per_pixel() as usize;
+
+ assert!(byte_size <= pbo.reserved_size);
+
+ self.bind_read_target(read_target);
+
+ self.gl.bind_buffer(gl::PIXEL_PACK_BUFFER, pbo.id);
+ self.gl.pixel_store_i(gl::PACK_ALIGNMENT, 1);
+
+ let gl_format = self.gl_describe_format(format);
+
+ unsafe {
+ self.gl.read_pixels_into_pbo(
+ rect.min.x as _,
+ rect.min.y as _,
+ rect.width() as _,
+ rect.height() as _,
+ gl_format.read,
+ gl_format.pixel_type,
+ );
+ }
+
+ self.gl.bind_buffer(gl::PIXEL_PACK_BUFFER, 0);
+ }
+
+ pub fn map_pbo_for_readback<'a>(&'a mut self, pbo: &'a PBO) -> Option<BoundPBO<'a>> {
+ self.gl.bind_buffer(gl::PIXEL_PACK_BUFFER, pbo.id);
+
+ let buf_ptr = match self.gl.get_type() {
+ gl::GlType::Gl => {
+ self.gl.map_buffer(gl::PIXEL_PACK_BUFFER, gl::READ_ONLY)
+ }
+
+ gl::GlType::Gles => {
+ self.gl.map_buffer_range(
+ gl::PIXEL_PACK_BUFFER,
+ 0,
+ pbo.reserved_size as _,
+ gl::MAP_READ_BIT)
+ }
+ };
+
+ if buf_ptr.is_null() {
+ return None;
+ }
+
+ let buffer = unsafe { slice::from_raw_parts(buf_ptr as *const u8, pbo.reserved_size) };
+
+ Some(BoundPBO {
+ device: self,
+ data: buffer,
+ })
+ }
+
+ pub fn delete_pbo(&mut self, mut pbo: PBO) {
+ self.gl.delete_buffers(&[pbo.id]);
+ pbo.id = 0;
+ pbo.reserved_size = 0
+ }
+
+ /// Returns the size and stride in bytes required to upload an area of pixels
+ /// of the specified size, to a texture of the specified format.
+ pub fn required_upload_size_and_stride(&self, size: DeviceIntSize, format: ImageFormat) -> (usize, usize) {
+ assert!(size.width >= 0);
+ assert!(size.height >= 0);
+
+ let bytes_pp = format.bytes_per_pixel() as usize;
+ let width_bytes = size.width as usize * bytes_pp;
+
+ let dst_stride = round_up_to_multiple(width_bytes, self.required_pbo_stride.num_bytes(format));
+
+ // The size of the chunk should only need to be (height - 1) * dst_stride + width_bytes,
+ // however, the android emulator will error unless it is height * dst_stride.
+ // See bug 1587047 for details.
+ // Using the full final row also ensures that the offset of the next chunk is
+ // optimally aligned.
+ let dst_size = dst_stride * size.height as usize;
+
+ (dst_size, dst_stride)
+ }
+
+ /// Returns a `TextureUploader` which can be used to upload texture data to `texture`.
+ /// Once uploads have been performed the uploader must be flushed with `TextureUploader::flush()`.
+ pub fn upload_texture<'a>(
+ &mut self,
+ pbo_pool: &'a mut UploadPBOPool,
+ ) -> TextureUploader<'a> {
+ debug_assert!(self.inside_frame);
+
+ pbo_pool.begin_frame(self);
+
+ TextureUploader {
+ buffers: Vec::new(),
+ pbo_pool,
+ }
+ }
+
+ /// Performs an immediate (non-PBO) texture upload.
+ pub fn upload_texture_immediate<T: Texel>(
+ &mut self,
+ texture: &Texture,
+ pixels: &[T]
+ ) {
+ self.bind_texture(DEFAULT_TEXTURE, texture, Swizzle::default());
+ let desc = self.gl_describe_format(texture.format);
+ self.gl.tex_sub_image_2d(
+ texture.target,
+ 0,
+ 0,
+ 0,
+ texture.size.width as gl::GLint,
+ texture.size.height as gl::GLint,
+ desc.external,
+ desc.pixel_type,
+ texels_to_u8_slice(pixels),
+ );
+ }
+
+ pub fn read_pixels(&mut self, img_desc: &ImageDescriptor) -> Vec<u8> {
+ let desc = self.gl_describe_format(img_desc.format);
+ self.gl.read_pixels(
+ 0, 0,
+ img_desc.size.width as i32,
+ img_desc.size.height as i32,
+ desc.read,
+ desc.pixel_type,
+ )
+ }
+
+ /// Read rectangle of pixels into the specified output slice.
+ pub fn read_pixels_into(
+ &mut self,
+ rect: FramebufferIntRect,
+ format: ImageFormat,
+ output: &mut [u8],
+ ) {
+ let bytes_per_pixel = format.bytes_per_pixel();
+ let desc = self.gl_describe_format(format);
+ let size_in_bytes = (bytes_per_pixel * rect.area()) as usize;
+ assert_eq!(output.len(), size_in_bytes);
+
+ self.gl.flush();
+ self.gl.read_pixels_into_buffer(
+ rect.min.x as _,
+ rect.min.y as _,
+ rect.width() as _,
+ rect.height() as _,
+ desc.read,
+ desc.pixel_type,
+ output,
+ );
+ }
+
+ /// Get texels of a texture into the specified output slice.
+ pub fn get_tex_image_into(
+ &mut self,
+ texture: &Texture,
+ format: ImageFormat,
+ output: &mut [u8],
+ ) {
+ self.bind_texture(DEFAULT_TEXTURE, texture, Swizzle::default());
+ let desc = self.gl_describe_format(format);
+ self.gl.get_tex_image_into_buffer(
+ texture.target,
+ 0,
+ desc.external,
+ desc.pixel_type,
+ output,
+ );
+ }
+
+ /// Attaches the provided texture to the current Read FBO binding.
+ fn attach_read_texture_raw(&mut self, texture_id: gl::GLuint, target: gl::GLuint) {
+ self.gl.framebuffer_texture_2d(
+ gl::READ_FRAMEBUFFER,
+ gl::COLOR_ATTACHMENT0,
+ target,
+ texture_id,
+ 0,
+ )
+ }
+
+ pub fn attach_read_texture_external(
+ &mut self, texture_id: gl::GLuint, target: ImageBufferKind
+ ) {
+ self.attach_read_texture_raw(texture_id, get_gl_target(target))
+ }
+
+ pub fn attach_read_texture(&mut self, texture: &Texture) {
+ self.attach_read_texture_raw(texture.id, texture.target)
+ }
+
+ fn bind_vao_impl(&mut self, id: gl::GLuint) {
+ debug_assert!(self.inside_frame);
+
+ if self.bound_vao != id {
+ self.bound_vao = id;
+ self.gl.bind_vertex_array(id);
+ }
+ }
+
+ pub fn bind_vao(&mut self, vao: &VAO) {
+ self.bind_vao_impl(vao.id)
+ }
+
+ pub fn bind_custom_vao(&mut self, vao: &CustomVAO) {
+ self.bind_vao_impl(vao.id)
+ }
+
+ fn create_vao_with_vbos(
+ &mut self,
+ descriptor: &VertexDescriptor,
+ main_vbo_id: VBOId,
+ instance_vbo_id: VBOId,
+ instance_divisor: u32,
+ ibo_id: IBOId,
+ owns_vertices_and_indices: bool,
+ ) -> VAO {
+ let instance_stride = descriptor.instance_stride() as usize;
+ let vao_id = self.gl.gen_vertex_arrays(1)[0];
+
+ self.bind_vao_impl(vao_id);
+
+ descriptor.bind(self.gl(), main_vbo_id, instance_vbo_id, instance_divisor);
+ ibo_id.bind(self.gl()); // force it to be a part of VAO
+
+ VAO {
+ id: vao_id,
+ ibo_id,
+ main_vbo_id,
+ instance_vbo_id,
+ instance_stride,
+ instance_divisor,
+ owns_vertices_and_indices,
+ }
+ }
+
+ pub fn create_custom_vao(
+ &mut self,
+ streams: &[Stream],
+ ) -> CustomVAO {
+ debug_assert!(self.inside_frame);
+
+ let vao_id = self.gl.gen_vertex_arrays(1)[0];
+ self.bind_vao_impl(vao_id);
+
+ let mut attrib_index = 0;
+ for stream in streams {
+ VertexDescriptor::bind_attributes(
+ stream.attributes,
+ attrib_index,
+ 0,
+ self.gl(),
+ stream.vbo,
+ );
+ attrib_index += stream.attributes.len();
+ }
+
+ CustomVAO {
+ id: vao_id,
+ }
+ }
+
+ pub fn delete_custom_vao(&mut self, mut vao: CustomVAO) {
+ self.gl.delete_vertex_arrays(&[vao.id]);
+ vao.id = 0;
+ }
+
+ pub fn create_vbo<T>(&mut self) -> VBO<T> {
+ let ids = self.gl.gen_buffers(1);
+ VBO {
+ id: ids[0],
+ target: gl::ARRAY_BUFFER,
+ allocated_count: 0,
+ marker: PhantomData,
+ }
+ }
+
+ pub fn delete_vbo<T>(&mut self, mut vbo: VBO<T>) {
+ self.gl.delete_buffers(&[vbo.id]);
+ vbo.id = 0;
+ }
+
+ pub fn create_vao(&mut self, descriptor: &VertexDescriptor, instance_divisor: u32) -> VAO {
+ debug_assert!(self.inside_frame);
+
+ let buffer_ids = self.gl.gen_buffers(3);
+ let ibo_id = IBOId(buffer_ids[0]);
+ let main_vbo_id = VBOId(buffer_ids[1]);
+ let intance_vbo_id = VBOId(buffer_ids[2]);
+
+ self.create_vao_with_vbos(descriptor, main_vbo_id, intance_vbo_id, instance_divisor, ibo_id, true)
+ }
+
+ pub fn delete_vao(&mut self, mut vao: VAO) {
+ self.gl.delete_vertex_arrays(&[vao.id]);
+ vao.id = 0;
+
+ if vao.owns_vertices_and_indices {
+ self.gl.delete_buffers(&[vao.ibo_id.0]);
+ self.gl.delete_buffers(&[vao.main_vbo_id.0]);
+ }
+
+ self.gl.delete_buffers(&[vao.instance_vbo_id.0])
+ }
+
+ pub fn allocate_vbo<V>(
+ &mut self,
+ vbo: &mut VBO<V>,
+ count: usize,
+ usage_hint: VertexUsageHint,
+ ) {
+ debug_assert!(self.inside_frame);
+ vbo.allocated_count = count;
+
+ self.gl.bind_buffer(vbo.target, vbo.id);
+ self.gl.buffer_data_untyped(
+ vbo.target,
+ (count * mem::size_of::<V>()) as _,
+ ptr::null(),
+ usage_hint.to_gl(),
+ );
+ }
+
+ pub fn fill_vbo<V>(
+ &mut self,
+ vbo: &VBO<V>,
+ data: &[V],
+ offset: usize,
+ ) {
+ debug_assert!(self.inside_frame);
+ assert!(offset + data.len() <= vbo.allocated_count);
+ let stride = mem::size_of::<V>();
+
+ self.gl.bind_buffer(vbo.target, vbo.id);
+ self.gl.buffer_sub_data_untyped(
+ vbo.target,
+ (offset * stride) as _,
+ (data.len() * stride) as _,
+ data.as_ptr() as _,
+ );
+ }
+
+ fn update_vbo_data<V>(
+ &mut self,
+ vbo: VBOId,
+ vertices: &[V],
+ usage_hint: VertexUsageHint,
+ ) {
+ debug_assert!(self.inside_frame);
+
+ vbo.bind(self.gl());
+ gl::buffer_data(self.gl(), gl::ARRAY_BUFFER, vertices, usage_hint.to_gl());
+ }
+
+ pub fn create_vao_with_new_instances(
+ &mut self,
+ descriptor: &VertexDescriptor,
+ base_vao: &VAO,
+ ) -> VAO {
+ debug_assert!(self.inside_frame);
+
+ let buffer_ids = self.gl.gen_buffers(1);
+ let intance_vbo_id = VBOId(buffer_ids[0]);
+
+ self.create_vao_with_vbos(
+ descriptor,
+ base_vao.main_vbo_id,
+ intance_vbo_id,
+ base_vao.instance_divisor,
+ base_vao.ibo_id,
+ false,
+ )
+ }
+
+ pub fn update_vao_main_vertices<V>(
+ &mut self,
+ vao: &VAO,
+ vertices: &[V],
+ usage_hint: VertexUsageHint,
+ ) {
+ debug_assert_eq!(self.bound_vao, vao.id);
+ self.update_vbo_data(vao.main_vbo_id, vertices, usage_hint)
+ }
+
+ pub fn update_vao_instances<V: Clone>(
+ &mut self,
+ vao: &VAO,
+ instances: &[V],
+ usage_hint: VertexUsageHint,
+ // if `Some(count)`, each instance is repeated `count` times
+ repeat: Option<NonZeroUsize>,
+ ) {
+ debug_assert_eq!(self.bound_vao, vao.id);
+ debug_assert_eq!(vao.instance_stride as usize, mem::size_of::<V>());
+
+ match repeat {
+ Some(count) => {
+ let target = gl::ARRAY_BUFFER;
+ self.gl.bind_buffer(target, vao.instance_vbo_id.0);
+ let size = instances.len() * count.get() * mem::size_of::<V>();
+ self.gl.buffer_data_untyped(
+ target,
+ size as _,
+ ptr::null(),
+ usage_hint.to_gl(),
+ );
+
+ let ptr = match self.gl.get_type() {
+ gl::GlType::Gl => {
+ self.gl.map_buffer(target, gl::WRITE_ONLY)
+ }
+ gl::GlType::Gles => {
+ self.gl.map_buffer_range(target, 0, size as _, gl::MAP_WRITE_BIT)
+ }
+ };
+ assert!(!ptr.is_null());
+
+ let buffer_slice = unsafe {
+ slice::from_raw_parts_mut(ptr as *mut V, instances.len() * count.get())
+ };
+ for (quad, instance) in buffer_slice.chunks_mut(4).zip(instances) {
+ quad[0] = instance.clone();
+ quad[1] = instance.clone();
+ quad[2] = instance.clone();
+ quad[3] = instance.clone();
+ }
+ self.gl.unmap_buffer(target);
+ }
+ None => {
+ self.update_vbo_data(vao.instance_vbo_id, instances, usage_hint);
+ }
+ }
+
+ // On some devices the VAO must be manually unbound and rebound after an attached buffer has
+ // been orphaned. Failure to do so appeared to result in the orphaned buffer's contents
+ // being used for the subsequent draw call, rather than the new buffer's contents.
+ if self.capabilities.requires_vao_rebind_after_orphaning {
+ self.bind_vao_impl(0);
+ self.bind_vao_impl(vao.id);
+ }
+ }
+
+ pub fn update_vao_indices<I>(&mut self, vao: &VAO, indices: &[I], usage_hint: VertexUsageHint) {
+ debug_assert!(self.inside_frame);
+ debug_assert_eq!(self.bound_vao, vao.id);
+
+ vao.ibo_id.bind(self.gl());
+ gl::buffer_data(
+ self.gl(),
+ gl::ELEMENT_ARRAY_BUFFER,
+ indices,
+ usage_hint.to_gl(),
+ );
+ }
+
+ pub fn draw_triangles_u16(&mut self, first_vertex: i32, index_count: i32) {
+ debug_assert!(self.inside_frame);
+ #[cfg(debug_assertions)]
+ debug_assert!(self.shader_is_ready);
+
+ let _guard = if self.annotate_draw_call_crashes {
+ Some(CrashAnnotatorGuard::new(
+ &self.crash_annotator,
+ CrashAnnotation::DrawShader,
+ &self.bound_program_name,
+ ))
+ } else {
+ None
+ };
+
+ self.gl.draw_elements(
+ gl::TRIANGLES,
+ index_count,
+ gl::UNSIGNED_SHORT,
+ first_vertex as u32 * 2,
+ );
+ }
+
+ pub fn draw_triangles_u32(&mut self, first_vertex: i32, index_count: i32) {
+ debug_assert!(self.inside_frame);
+ #[cfg(debug_assertions)]
+ debug_assert!(self.shader_is_ready);
+
+ let _guard = if self.annotate_draw_call_crashes {
+ Some(CrashAnnotatorGuard::new(
+ &self.crash_annotator,
+ CrashAnnotation::DrawShader,
+ &self.bound_program_name,
+ ))
+ } else {
+ None
+ };
+
+ self.gl.draw_elements(
+ gl::TRIANGLES,
+ index_count,
+ gl::UNSIGNED_INT,
+ first_vertex as u32 * 4,
+ );
+ }
+
+ pub fn draw_nonindexed_points(&mut self, first_vertex: i32, vertex_count: i32) {
+ debug_assert!(self.inside_frame);
+ #[cfg(debug_assertions)]
+ debug_assert!(self.shader_is_ready);
+
+ let _guard = if self.annotate_draw_call_crashes {
+ Some(CrashAnnotatorGuard::new(
+ &self.crash_annotator,
+ CrashAnnotation::DrawShader,
+ &self.bound_program_name,
+ ))
+ } else {
+ None
+ };
+
+ self.gl.draw_arrays(gl::POINTS, first_vertex, vertex_count);
+ }
+
+ pub fn draw_nonindexed_lines(&mut self, first_vertex: i32, vertex_count: i32) {
+ debug_assert!(self.inside_frame);
+ #[cfg(debug_assertions)]
+ debug_assert!(self.shader_is_ready);
+
+ let _guard = if self.annotate_draw_call_crashes {
+ Some(CrashAnnotatorGuard::new(
+ &self.crash_annotator,
+ CrashAnnotation::DrawShader,
+ &self.bound_program_name,
+ ))
+ } else {
+ None
+ };
+
+ self.gl.draw_arrays(gl::LINES, first_vertex, vertex_count);
+ }
+
+ pub fn draw_indexed_triangles(&mut self, index_count: i32) {
+ debug_assert!(self.inside_frame);
+ #[cfg(debug_assertions)]
+ debug_assert!(self.shader_is_ready);
+
+ let _guard = if self.annotate_draw_call_crashes {
+ Some(CrashAnnotatorGuard::new(
+ &self.crash_annotator,
+ CrashAnnotation::DrawShader,
+ &self.bound_program_name,
+ ))
+ } else {
+ None
+ };
+
+ self.gl.draw_elements(
+ gl::TRIANGLES,
+ index_count,
+ gl::UNSIGNED_SHORT,
+ 0,
+ );
+ }
+
+ pub fn draw_indexed_triangles_instanced_u16(&mut self, index_count: i32, instance_count: i32) {
+ debug_assert!(self.inside_frame);
+ #[cfg(debug_assertions)]
+ debug_assert!(self.shader_is_ready);
+
+ let _guard = if self.annotate_draw_call_crashes {
+ Some(CrashAnnotatorGuard::new(
+ &self.crash_annotator,
+ CrashAnnotation::DrawShader,
+ &self.bound_program_name,
+ ))
+ } else {
+ None
+ };
+
+ self.gl.draw_elements_instanced(
+ gl::TRIANGLES,
+ index_count,
+ gl::UNSIGNED_SHORT,
+ 0,
+ instance_count,
+ );
+ }
+
+ pub fn end_frame(&mut self) {
+ self.reset_draw_target();
+ self.reset_read_target();
+
+ debug_assert!(self.inside_frame);
+ self.inside_frame = false;
+
+ self.gl.bind_texture(gl::TEXTURE_2D, 0);
+ self.gl.use_program(0);
+
+ for i in 0 .. self.bound_textures.len() {
+ self.gl.active_texture(gl::TEXTURE0 + i as gl::GLuint);
+ self.gl.bind_texture(gl::TEXTURE_2D, 0);
+ }
+
+ self.gl.active_texture(gl::TEXTURE0);
+
+ self.frame_id.0 += 1;
+
+ // Save any shaders compiled this frame to disk.
+ // If this is the tenth frame then treat startup as complete, meaning the
+ // current set of in-use shaders are the ones to load on the next startup.
+ if let Some(ref cache) = self.cached_programs {
+ cache.update_disk_cache(self.frame_id.0 == 10);
+ }
+ }
+
+ pub fn clear_target(
+ &self,
+ color: Option<[f32; 4]>,
+ depth: Option<f32>,
+ rect: Option<FramebufferIntRect>,
+ ) {
+ let mut clear_bits = 0;
+
+ if let Some(color) = color {
+ self.gl.clear_color(color[0], color[1], color[2], color[3]);
+ clear_bits |= gl::COLOR_BUFFER_BIT;
+ }
+
+ if let Some(depth) = depth {
+ if cfg!(debug_assertions) {
+ let mut mask = [0];
+ unsafe {
+ self.gl.get_boolean_v(gl::DEPTH_WRITEMASK, &mut mask);
+ }
+ assert_ne!(mask[0], 0);
+ }
+ self.gl.clear_depth(depth as f64);
+ clear_bits |= gl::DEPTH_BUFFER_BIT;
+ }
+
+ if clear_bits != 0 {
+ match rect {
+ Some(rect) => {
+ self.gl.enable(gl::SCISSOR_TEST);
+ self.gl.scissor(
+ rect.min.x,
+ rect.min.y,
+ rect.width(),
+ rect.height(),
+ );
+ self.gl.clear(clear_bits);
+ self.gl.disable(gl::SCISSOR_TEST);
+ }
+ None => {
+ self.gl.clear(clear_bits);
+ }
+ }
+ }
+ }
+
+ pub fn enable_depth(&self, depth_func: DepthFunction) {
+ assert!(self.depth_available, "Enabling depth test without depth target");
+ self.gl.enable(gl::DEPTH_TEST);
+ self.gl.depth_func(depth_func as gl::GLuint);
+ }
+
+ pub fn disable_depth(&self) {
+ self.gl.disable(gl::DEPTH_TEST);
+ }
+
+ pub fn enable_depth_write(&self) {
+ assert!(self.depth_available, "Enabling depth write without depth target");
+ self.gl.depth_mask(true);
+ }
+
+ pub fn disable_depth_write(&self) {
+ self.gl.depth_mask(false);
+ }
+
+ pub fn disable_stencil(&self) {
+ self.gl.disable(gl::STENCIL_TEST);
+ }
+
+ pub fn set_scissor_rect(&self, rect: FramebufferIntRect) {
+ self.gl.scissor(
+ rect.min.x,
+ rect.min.y,
+ rect.width(),
+ rect.height(),
+ );
+ }
+
+ pub fn enable_scissor(&self) {
+ self.gl.enable(gl::SCISSOR_TEST);
+ }
+
+ pub fn disable_scissor(&self) {
+ self.gl.disable(gl::SCISSOR_TEST);
+ }
+
+ pub fn enable_color_write(&self) {
+ self.gl.color_mask(true, true, true, true);
+ }
+
+ pub fn disable_color_write(&self) {
+ self.gl.color_mask(false, false, false, false);
+ }
+
+ pub fn set_blend(&mut self, enable: bool) {
+ if enable {
+ self.gl.enable(gl::BLEND);
+ } else {
+ self.gl.disable(gl::BLEND);
+ }
+ #[cfg(debug_assertions)]
+ {
+ self.shader_is_ready = false;
+ }
+ }
+
+ fn set_blend_factors(
+ &mut self,
+ color: (gl::GLenum, gl::GLenum),
+ alpha: (gl::GLenum, gl::GLenum),
+ ) {
+ self.gl.blend_equation(gl::FUNC_ADD);
+ if color == alpha {
+ self.gl.blend_func(color.0, color.1);
+ } else {
+ self.gl.blend_func_separate(color.0, color.1, alpha.0, alpha.1);
+ }
+ #[cfg(debug_assertions)]
+ {
+ self.shader_is_ready = false;
+ }
+ }
+
+ pub fn set_blend_mode_alpha(&mut self) {
+ self.set_blend_factors(
+ (gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA),
+ (gl::ONE, gl::ONE_MINUS_SRC_ALPHA),
+ );
+ }
+
+ pub fn set_blend_mode_premultiplied_alpha(&mut self) {
+ self.set_blend_factors(
+ (gl::ONE, gl::ONE_MINUS_SRC_ALPHA),
+ (gl::ONE, gl::ONE_MINUS_SRC_ALPHA),
+ );
+ }
+
+ pub fn set_blend_mode_premultiplied_dest_out(&mut self) {
+ self.set_blend_factors(
+ (gl::ZERO, gl::ONE_MINUS_SRC_ALPHA),
+ (gl::ZERO, gl::ONE_MINUS_SRC_ALPHA),
+ );
+ }
+
+ pub fn set_blend_mode_multiply(&mut self) {
+ self.set_blend_factors(
+ (gl::ZERO, gl::SRC_COLOR),
+ (gl::ZERO, gl::SRC_ALPHA),
+ );
+ }
+ pub fn set_blend_mode_subpixel_pass0(&mut self) {
+ self.set_blend_factors(
+ (gl::ZERO, gl::ONE_MINUS_SRC_COLOR),
+ (gl::ZERO, gl::ONE_MINUS_SRC_ALPHA),
+ );
+ }
+ pub fn set_blend_mode_subpixel_pass1(&mut self) {
+ self.set_blend_factors(
+ (gl::ONE, gl::ONE),
+ (gl::ONE, gl::ONE),
+ );
+ }
+ pub fn set_blend_mode_subpixel_with_bg_color_pass0(&mut self) {
+ self.set_blend_factors(
+ (gl::ZERO, gl::ONE_MINUS_SRC_COLOR),
+ (gl::ZERO, gl::ONE),
+ );
+ }
+ pub fn set_blend_mode_subpixel_with_bg_color_pass1(&mut self) {
+ self.set_blend_factors(
+ (gl::ONE_MINUS_DST_ALPHA, gl::ONE),
+ (gl::ZERO, gl::ONE),
+ );
+ }
+ pub fn set_blend_mode_subpixel_with_bg_color_pass2(&mut self) {
+ self.set_blend_factors(
+ (gl::ONE, gl::ONE),
+ (gl::ONE, gl::ONE_MINUS_SRC_ALPHA),
+ );
+ }
+ pub fn set_blend_mode_subpixel_dual_source(&mut self) {
+ self.set_blend_factors(
+ (gl::ONE, gl::ONE_MINUS_SRC1_COLOR),
+ (gl::ONE, gl::ONE_MINUS_SRC1_ALPHA),
+ );
+ }
+ pub fn set_blend_mode_multiply_dual_source(&mut self) {
+ self.set_blend_factors(
+ (gl::ONE_MINUS_DST_ALPHA, gl::ONE_MINUS_SRC1_COLOR),
+ (gl::ONE, gl::ONE_MINUS_SRC_ALPHA),
+ );
+ }
+ pub fn set_blend_mode_screen(&mut self) {
+ self.set_blend_factors(
+ (gl::ONE, gl::ONE_MINUS_SRC_COLOR),
+ (gl::ONE, gl::ONE_MINUS_SRC_ALPHA),
+ );
+ }
+ pub fn set_blend_mode_plus_lighter(&mut self) {
+ self.set_blend_factors(
+ (gl::ONE, gl::ONE),
+ (gl::ONE, gl::ONE),
+ );
+ }
+ pub fn set_blend_mode_exclusion(&mut self) {
+ self.set_blend_factors(
+ (gl::ONE_MINUS_DST_COLOR, gl::ONE_MINUS_SRC_COLOR),
+ (gl::ONE, gl::ONE_MINUS_SRC_ALPHA),
+ );
+ }
+ pub fn set_blend_mode_show_overdraw(&mut self) {
+ self.set_blend_factors(
+ (gl::ONE, gl::ONE_MINUS_SRC_ALPHA),
+ (gl::ONE, gl::ONE_MINUS_SRC_ALPHA),
+ );
+ }
+
+ pub fn set_blend_mode_max(&mut self) {
+ self.gl
+ .blend_func_separate(gl::ONE, gl::ONE, gl::ONE, gl::ONE);
+ self.gl.blend_equation_separate(gl::MAX, gl::FUNC_ADD);
+ #[cfg(debug_assertions)]
+ {
+ self.shader_is_ready = false;
+ }
+ }
+ pub fn set_blend_mode_min(&mut self) {
+ self.gl
+ .blend_func_separate(gl::ONE, gl::ONE, gl::ONE, gl::ONE);
+ self.gl.blend_equation_separate(gl::MIN, gl::FUNC_ADD);
+ #[cfg(debug_assertions)]
+ {
+ self.shader_is_ready = false;
+ }
+ }
+ pub fn set_blend_mode_advanced(&mut self, mode: MixBlendMode) {
+ self.gl.blend_equation(match mode {
+ MixBlendMode::Normal => {
+ // blend factor only make sense for the normal mode
+ self.gl.blend_func_separate(gl::ZERO, gl::SRC_COLOR, gl::ZERO, gl::SRC_ALPHA);
+ gl::FUNC_ADD
+ },
+ MixBlendMode::PlusLighter => {
+ return self.set_blend_mode_plus_lighter();
+ },
+ MixBlendMode::Multiply => gl::MULTIPLY_KHR,
+ MixBlendMode::Screen => gl::SCREEN_KHR,
+ MixBlendMode::Overlay => gl::OVERLAY_KHR,
+ MixBlendMode::Darken => gl::DARKEN_KHR,
+ MixBlendMode::Lighten => gl::LIGHTEN_KHR,
+ MixBlendMode::ColorDodge => gl::COLORDODGE_KHR,
+ MixBlendMode::ColorBurn => gl::COLORBURN_KHR,
+ MixBlendMode::HardLight => gl::HARDLIGHT_KHR,
+ MixBlendMode::SoftLight => gl::SOFTLIGHT_KHR,
+ MixBlendMode::Difference => gl::DIFFERENCE_KHR,
+ MixBlendMode::Exclusion => gl::EXCLUSION_KHR,
+ MixBlendMode::Hue => gl::HSL_HUE_KHR,
+ MixBlendMode::Saturation => gl::HSL_SATURATION_KHR,
+ MixBlendMode::Color => gl::HSL_COLOR_KHR,
+ MixBlendMode::Luminosity => gl::HSL_LUMINOSITY_KHR,
+ });
+ #[cfg(debug_assertions)]
+ {
+ self.shader_is_ready = false;
+ }
+ }
+
+ pub fn supports_extension(&self, extension: &str) -> bool {
+ supports_extension(&self.extensions, extension)
+ }
+
+ pub fn echo_driver_messages(&self) {
+ if self.capabilities.supports_khr_debug {
+ Device::log_driver_messages(self.gl());
+ }
+ }
+
+ fn log_driver_messages(gl: &dyn gl::Gl) {
+ for msg in gl.get_debug_messages() {
+ let level = match msg.severity {
+ gl::DEBUG_SEVERITY_HIGH => Level::Error,
+ gl::DEBUG_SEVERITY_MEDIUM => Level::Warn,
+ gl::DEBUG_SEVERITY_LOW => Level::Info,
+ gl::DEBUG_SEVERITY_NOTIFICATION => Level::Debug,
+ _ => Level::Trace,
+ };
+ let ty = match msg.ty {
+ gl::DEBUG_TYPE_ERROR => "error",
+ gl::DEBUG_TYPE_DEPRECATED_BEHAVIOR => "deprecated",
+ gl::DEBUG_TYPE_UNDEFINED_BEHAVIOR => "undefined",
+ gl::DEBUG_TYPE_PORTABILITY => "portability",
+ gl::DEBUG_TYPE_PERFORMANCE => "perf",
+ gl::DEBUG_TYPE_MARKER => "marker",
+ gl::DEBUG_TYPE_PUSH_GROUP => "group push",
+ gl::DEBUG_TYPE_POP_GROUP => "group pop",
+ gl::DEBUG_TYPE_OTHER => "other",
+ _ => "?",
+ };
+ log!(level, "({}) {}", ty, msg.message);
+ }
+ }
+
+ pub fn gl_describe_format(&self, format: ImageFormat) -> FormatDesc {
+ match format {
+ ImageFormat::R8 => FormatDesc {
+ internal: gl::R8,
+ external: gl::RED,
+ read: gl::RED,
+ pixel_type: gl::UNSIGNED_BYTE,
+ },
+ ImageFormat::R16 => FormatDesc {
+ internal: gl::R16,
+ external: gl::RED,
+ read: gl::RED,
+ pixel_type: gl::UNSIGNED_SHORT,
+ },
+ ImageFormat::BGRA8 => {
+ FormatDesc {
+ internal: self.bgra_formats.internal,
+ external: self.bgra_formats.external,
+ read: gl::BGRA,
+ pixel_type: self.bgra_pixel_type,
+ }
+ },
+ ImageFormat::RGBA8 => {
+ FormatDesc {
+ internal: gl::RGBA8,
+ external: gl::RGBA,
+ read: gl::RGBA,
+ pixel_type: gl::UNSIGNED_BYTE,
+ }
+ },
+ ImageFormat::RGBAF32 => FormatDesc {
+ internal: gl::RGBA32F,
+ external: gl::RGBA,
+ read: gl::RGBA,
+ pixel_type: gl::FLOAT,
+ },
+ ImageFormat::RGBAI32 => FormatDesc {
+ internal: gl::RGBA32I,
+ external: gl::RGBA_INTEGER,
+ read: gl::RGBA_INTEGER,
+ pixel_type: gl::INT,
+ },
+ ImageFormat::RG8 => FormatDesc {
+ internal: gl::RG8,
+ external: gl::RG,
+ read: gl::RG,
+ pixel_type: gl::UNSIGNED_BYTE,
+ },
+ ImageFormat::RG16 => FormatDesc {
+ internal: gl::RG16,
+ external: gl::RG,
+ read: gl::RG,
+ pixel_type: gl::UNSIGNED_SHORT,
+ },
+ }
+ }
+
+ /// Generates a memory report for the resources managed by the device layer.
+ pub fn report_memory(&self, size_op_funs: &MallocSizeOfOps, swgl: *mut c_void) -> MemoryReport {
+ let mut report = MemoryReport::default();
+ report.depth_target_textures += self.depth_targets_memory();
+
+ #[cfg(feature = "sw_compositor")]
+ if !swgl.is_null() {
+ report.swgl += swgl::Context::from(swgl).report_memory(size_op_funs.size_of_op);
+ }
+ // unconditionally use swgl stuff
+ let _ = size_op_funs;
+ let _ = swgl;
+ report
+ }
+
+ pub fn depth_targets_memory(&self) -> usize {
+ let mut total = 0;
+ for dim in self.depth_targets.keys() {
+ total += depth_target_size_in_bytes(dim);
+ }
+
+ total
+ }
+}
+
+pub struct FormatDesc {
+ /// Format the texel data is internally stored in within a texture.
+ pub internal: gl::GLenum,
+ /// Format that we expect the data to be provided when filling the texture.
+ pub external: gl::GLuint,
+ /// Format to read the texels as, so that they can be uploaded as `external`
+ /// later on.
+ pub read: gl::GLuint,
+ /// Associated pixel type.
+ pub pixel_type: gl::GLuint,
+}
+
+#[derive(Debug)]
+struct UploadChunk<'a> {
+ rect: DeviceIntRect,
+ stride: Option<i32>,
+ offset: usize,
+ format_override: Option<ImageFormat>,
+ texture: &'a Texture,
+}
+
+#[derive(Debug)]
+struct PixelBuffer<'a> {
+ size_used: usize,
+ // small vector avoids heap allocation for a single chunk
+ chunks: SmallVec<[UploadChunk<'a>; 1]>,
+ inner: UploadPBO,
+ mapping: &'a mut [mem::MaybeUninit<u8>],
+}
+
+impl<'a> PixelBuffer<'a> {
+ fn new(
+ pbo: UploadPBO,
+ ) -> Self {
+ let mapping = unsafe {
+ slice::from_raw_parts_mut(pbo.mapping.get_ptr().as_ptr(), pbo.pbo.reserved_size)
+ };
+ Self {
+ size_used: 0,
+ chunks: SmallVec::new(),
+ inner: pbo,
+ mapping,
+ }
+ }
+
+ fn flush_chunks(&mut self, device: &mut Device) {
+ for chunk in self.chunks.drain(..) {
+ TextureUploader::update_impl(device, chunk);
+ }
+ }
+}
+
+impl<'a> Drop for PixelBuffer<'a> {
+ fn drop(&mut self) {
+ assert_eq!(self.chunks.len(), 0, "PixelBuffer must be flushed before dropping.");
+ }
+}
+
+#[derive(Debug)]
+enum PBOMapping {
+ Unmapped,
+ Transient(ptr::NonNull<mem::MaybeUninit<u8>>),
+ Persistent(ptr::NonNull<mem::MaybeUninit<u8>>),
+}
+
+impl PBOMapping {
+ fn get_ptr(&self) -> ptr::NonNull<mem::MaybeUninit<u8>> {
+ match self {
+ PBOMapping::Unmapped => unreachable!("Cannot get pointer to unmapped PBO."),
+ PBOMapping::Transient(ptr) => *ptr,
+ PBOMapping::Persistent(ptr) => *ptr,
+ }
+ }
+}
+
+/// A PBO for uploading texture data, managed by UploadPBOPool.
+#[derive(Debug)]
+struct UploadPBO {
+ pbo: PBO,
+ mapping: PBOMapping,
+ can_recycle: bool,
+}
+
+impl UploadPBO {
+ fn empty() -> Self {
+ Self {
+ pbo: PBO {
+ id: 0,
+ reserved_size: 0,
+ },
+ mapping: PBOMapping::Unmapped,
+ can_recycle: false,
+ }
+ }
+}
+
+/// Allocates and recycles PBOs used for uploading texture data.
+/// Tries to allocate and recycle PBOs of a fixed size, but will make exceptions when
+/// a larger buffer is required or to work around driver bugs.
+pub struct UploadPBOPool {
+ /// Usage hint to provide to the driver for optimizations.
+ usage_hint: VertexUsageHint,
+ /// The preferred size, in bytes, of the buffers to allocate.
+ default_size: usize,
+ /// List of allocated PBOs ready to be re-used.
+ available_buffers: Vec<UploadPBO>,
+ /// PBOs which have been returned during the current frame,
+ /// and do not yet have an associated sync object.
+ returned_buffers: Vec<UploadPBO>,
+ /// PBOs which are waiting until their sync object is signalled,
+ /// indicating they can are ready to be re-used.
+ waiting_buffers: Vec<(gl::GLsync, Vec<UploadPBO>)>,
+ /// PBOs which have been orphaned.
+ /// We can recycle their IDs but must reallocate their storage.
+ orphaned_buffers: Vec<PBO>,
+}
+
+impl UploadPBOPool {
+ pub fn new(device: &mut Device, default_size: usize) -> Self {
+ let usage_hint = match device.upload_method {
+ UploadMethod::Immediate => VertexUsageHint::Stream,
+ UploadMethod::PixelBuffer(usage_hint) => usage_hint,
+ };
+ Self {
+ usage_hint,
+ default_size,
+ available_buffers: Vec::new(),
+ returned_buffers: Vec::new(),
+ waiting_buffers: Vec::new(),
+ orphaned_buffers: Vec::new(),
+ }
+ }
+
+ /// To be called at the beginning of a series of uploads.
+ /// Moves any buffers which are now ready to be used from the waiting list to the ready list.
+ pub fn begin_frame(&mut self, device: &mut Device) {
+ // Iterate through the waiting buffers and check if each fence has been signalled.
+ // If a fence is signalled, move its corresponding buffers to the available list.
+ // On error, delete the buffers. Stop when we find the first non-signalled fence,
+ // and clean up the signalled fences.
+ let mut first_not_signalled = self.waiting_buffers.len();
+ for (i, (sync, buffers)) in self.waiting_buffers.iter_mut().enumerate() {
+ match device.gl.client_wait_sync(*sync, 0, 0) {
+ gl::TIMEOUT_EXPIRED => {
+ first_not_signalled = i;
+ break;
+ },
+ gl::ALREADY_SIGNALED | gl::CONDITION_SATISFIED => {
+ self.available_buffers.extend(buffers.drain(..));
+ }
+ gl::WAIT_FAILED | _ => {
+ warn!("glClientWaitSync error in UploadPBOPool::begin_frame()");
+ for buffer in buffers.drain(..) {
+ device.delete_pbo(buffer.pbo);
+ }
+ }
+ }
+ }
+
+ // Delete signalled fences, and remove their now-empty Vecs from waiting_buffers.
+ for (sync, _) in self.waiting_buffers.drain(0..first_not_signalled) {
+ device.gl.delete_sync(sync);
+ }
+ }
+
+ // To be called at the end of a series of uploads.
+ // Creates a sync object, and adds the buffers returned during this frame to waiting_buffers.
+ pub fn end_frame(&mut self, device: &mut Device) {
+ if !self.returned_buffers.is_empty() {
+ let sync = device.gl.fence_sync(gl::SYNC_GPU_COMMANDS_COMPLETE, 0);
+ if !sync.is_null() {
+ self.waiting_buffers.push((sync, mem::replace(&mut self.returned_buffers, Vec::new())))
+ } else {
+ warn!("glFenceSync error in UploadPBOPool::end_frame()");
+
+ for buffer in self.returned_buffers.drain(..) {
+ device.delete_pbo(buffer.pbo);
+ }
+ }
+ }
+ }
+
+ /// Obtain a PBO, either by reusing an existing PBO or allocating a new one.
+ /// min_size specifies the minimum required size of the PBO. The returned PBO
+ /// may be larger than required.
+ fn get_pbo(&mut self, device: &mut Device, min_size: usize) -> Result<UploadPBO, String> {
+
+ // If min_size is smaller than our default size, then use the default size.
+ // The exception to this is when due to driver bugs we cannot upload from
+ // offsets other than zero within a PBO. In this case, there is no point in
+ // allocating buffers larger than required, as they cannot be shared.
+ let (can_recycle, size) = if min_size <= self.default_size && device.capabilities.supports_nonzero_pbo_offsets {
+ (true, self.default_size)
+ } else {
+ (false, min_size)
+ };
+
+ // Try to recycle an already allocated PBO.
+ if can_recycle {
+ if let Some(mut buffer) = self.available_buffers.pop() {
+ assert_eq!(buffer.pbo.reserved_size, size);
+ assert!(buffer.can_recycle);
+
+ device.gl.bind_buffer(gl::PIXEL_UNPACK_BUFFER, buffer.pbo.id);
+
+ match buffer.mapping {
+ PBOMapping::Unmapped => {
+ // If buffer was unmapped then transiently map it.
+ let ptr = device.gl.map_buffer_range(
+ gl::PIXEL_UNPACK_BUFFER,
+ 0,
+ buffer.pbo.reserved_size as _,
+ gl::MAP_WRITE_BIT | gl::MAP_UNSYNCHRONIZED_BIT,
+ ) as *mut _;
+
+ let ptr = ptr::NonNull::new(ptr).ok_or_else(
+ || format!("Failed to transiently map PBO of size {} bytes", buffer.pbo.reserved_size)
+ )?;
+
+ buffer.mapping = PBOMapping::Transient(ptr);
+ }
+ PBOMapping::Transient(_) => {
+ unreachable!("Transiently mapped UploadPBO must be unmapped before returning to pool.");
+ }
+ PBOMapping::Persistent(_) => {
+ }
+ }
+
+ return Ok(buffer);
+ }
+ }
+
+ // Try to recycle a PBO ID (but not its allocation) from a previously allocated PBO.
+ // If there are none available, create a new PBO.
+ let mut pbo = match self.orphaned_buffers.pop() {
+ Some(pbo) => pbo,
+ None => device.create_pbo(),
+ };
+
+ assert_eq!(pbo.reserved_size, 0);
+ pbo.reserved_size = size;
+
+ device.gl.bind_buffer(gl::PIXEL_UNPACK_BUFFER, pbo.id);
+ let mapping = if device.capabilities.supports_buffer_storage && can_recycle {
+ device.gl.buffer_storage(
+ gl::PIXEL_UNPACK_BUFFER,
+ pbo.reserved_size as _,
+ ptr::null(),
+ gl::MAP_WRITE_BIT | gl::MAP_PERSISTENT_BIT,
+ );
+ let ptr = device.gl.map_buffer_range(
+ gl::PIXEL_UNPACK_BUFFER,
+ 0,
+ pbo.reserved_size as _,
+ // GL_MAP_COHERENT_BIT doesn't seem to work on Adreno, so use glFlushMappedBufferRange.
+ // kvark notes that coherent memory can be faster on some platforms, such as nvidia,
+ // so in the future we could choose which to use at run time.
+ gl::MAP_WRITE_BIT | gl::MAP_PERSISTENT_BIT | gl::MAP_FLUSH_EXPLICIT_BIT,
+ ) as *mut _;
+
+ let ptr = ptr::NonNull::new(ptr).ok_or_else(
+ || format!("Failed to transiently map PBO of size {} bytes", pbo.reserved_size)
+ )?;
+
+ PBOMapping::Persistent(ptr)
+ } else {
+ device.gl.buffer_data_untyped(
+ gl::PIXEL_UNPACK_BUFFER,
+ pbo.reserved_size as _,
+ ptr::null(),
+ self.usage_hint.to_gl(),
+ );
+ let ptr = device.gl.map_buffer_range(
+ gl::PIXEL_UNPACK_BUFFER,
+ 0,
+ pbo.reserved_size as _,
+ // Unlike the above code path, where we are re-mapping a buffer that has previously been unmapped,
+ // this buffer has just been created there is no need for GL_MAP_UNSYNCHRONIZED_BIT.
+ gl::MAP_WRITE_BIT,
+ ) as *mut _;
+
+ let ptr = ptr::NonNull::new(ptr).ok_or_else(
+ || format!("Failed to transiently map PBO of size {} bytes", pbo.reserved_size)
+ )?;
+
+ PBOMapping::Transient(ptr)
+ };
+
+ Ok(UploadPBO { pbo, mapping, can_recycle })
+ }
+
+ /// Returns a PBO to the pool. If the PBO is recyclable it is placed in the waiting list.
+ /// Otherwise we orphan the allocation immediately, and will subsequently reuse just the ID.
+ fn return_pbo(&mut self, device: &mut Device, mut buffer: UploadPBO) {
+ assert!(
+ !matches!(buffer.mapping, PBOMapping::Transient(_)),
+ "Transiently mapped UploadPBO must be unmapped before returning to pool.",
+ );
+
+ if buffer.can_recycle {
+ self.returned_buffers.push(buffer);
+ } else {
+ device.gl.bind_buffer(gl::PIXEL_UNPACK_BUFFER, buffer.pbo.id);
+ device.gl.buffer_data_untyped(
+ gl::PIXEL_UNPACK_BUFFER,
+ 0,
+ ptr::null(),
+ gl::STREAM_DRAW,
+ );
+ buffer.pbo.reserved_size = 0;
+ self.orphaned_buffers.push(buffer.pbo);
+ }
+
+ device.gl.bind_buffer(gl::PIXEL_UNPACK_BUFFER, 0);
+ }
+
+ /// Frees all allocated buffers in response to a memory pressure event.
+ pub fn on_memory_pressure(&mut self, device: &mut Device) {
+ for buffer in self.available_buffers.drain(..) {
+ device.delete_pbo(buffer.pbo);
+ }
+ for buffer in self.returned_buffers.drain(..) {
+ device.delete_pbo(buffer.pbo)
+ }
+ for (sync, buffers) in self.waiting_buffers.drain(..) {
+ device.gl.delete_sync(sync);
+ for buffer in buffers {
+ device.delete_pbo(buffer.pbo)
+ }
+ }
+ // There is no need to delete orphaned PBOs on memory pressure.
+ }
+
+ /// Generates a memory report.
+ pub fn report_memory(&self) -> MemoryReport {
+ let mut report = MemoryReport::default();
+ for buffer in &self.available_buffers {
+ report.texture_upload_pbos += buffer.pbo.reserved_size;
+ }
+ for buffer in &self.returned_buffers {
+ report.texture_upload_pbos += buffer.pbo.reserved_size;
+ }
+ for (_, buffers) in &self.waiting_buffers {
+ for buffer in buffers {
+ report.texture_upload_pbos += buffer.pbo.reserved_size;
+ }
+ }
+ report
+ }
+
+ pub fn deinit(&mut self, device: &mut Device) {
+ for buffer in self.available_buffers.drain(..) {
+ device.delete_pbo(buffer.pbo);
+ }
+ for buffer in self.returned_buffers.drain(..) {
+ device.delete_pbo(buffer.pbo)
+ }
+ for (sync, buffers) in self.waiting_buffers.drain(..) {
+ device.gl.delete_sync(sync);
+ for buffer in buffers {
+ device.delete_pbo(buffer.pbo)
+ }
+ }
+ for pbo in self.orphaned_buffers.drain(..) {
+ device.delete_pbo(pbo);
+ }
+ }
+}
+
+/// Used to perform a series of texture uploads.
+/// Create using Device::upload_texture(). Perform a series of uploads using either
+/// upload(), or stage() and upload_staged(), then call flush().
+pub struct TextureUploader<'a> {
+ /// A list of buffers containing uploads that need to be flushed.
+ buffers: Vec<PixelBuffer<'a>>,
+ /// Pool used to obtain PBOs to fill with texture data.
+ pub pbo_pool: &'a mut UploadPBOPool,
+}
+
+impl<'a> Drop for TextureUploader<'a> {
+ fn drop(&mut self) {
+ assert!(
+ thread::panicking() || self.buffers.is_empty(),
+ "TextureUploader must be flushed before it is dropped."
+ );
+ }
+}
+
+/// A buffer used to manually stage data to be uploaded to a texture.
+/// Created by calling TextureUploader::stage(), the data can then be written to via get_mapping().
+#[derive(Debug)]
+pub struct UploadStagingBuffer<'a> {
+ /// The PixelBuffer containing this upload.
+ buffer: PixelBuffer<'a>,
+ /// The offset of this upload within the PixelBuffer.
+ offset: usize,
+ /// The size of this upload.
+ size: usize,
+ /// The stride of the data within the buffer.
+ stride: usize,
+}
+
+impl<'a> UploadStagingBuffer<'a> {
+ /// Returns the required stride of the data to be written to the buffer.
+ pub fn get_stride(&self) -> usize {
+ self.stride
+ }
+
+ /// Returns a mapping of the data in the buffer, to be written to.
+ pub fn get_mapping(&mut self) -> &mut [mem::MaybeUninit<u8>] {
+ &mut self.buffer.mapping[self.offset..self.offset + self.size]
+ }
+}
+
+impl<'a> TextureUploader<'a> {
+ /// Returns an UploadStagingBuffer which can be used to manually stage data to be uploaded.
+ /// Once the data has been staged, it can be uploaded with upload_staged().
+ pub fn stage(
+ &mut self,
+ device: &mut Device,
+ format: ImageFormat,
+ size: DeviceIntSize,
+ ) -> Result<UploadStagingBuffer<'a>, String> {
+ assert!(matches!(device.upload_method, UploadMethod::PixelBuffer(_)), "Texture uploads should only be staged when using pixel buffers.");
+
+ // for optimal PBO texture uploads the offset and stride of the data in
+ // the buffer may have to be a multiple of a certain value.
+ let (dst_size, dst_stride) = device.required_upload_size_and_stride(
+ size,
+ format,
+ );
+
+ // Find a pixel buffer with enough space remaining, creating a new one if required.
+ let buffer_index = self.buffers.iter().position(|buffer| {
+ buffer.size_used + dst_size <= buffer.inner.pbo.reserved_size
+ });
+ let buffer = match buffer_index {
+ Some(i) => self.buffers.swap_remove(i),
+ None => PixelBuffer::new(self.pbo_pool.get_pbo(device, dst_size)?),
+ };
+
+ if !device.capabilities.supports_nonzero_pbo_offsets {
+ assert_eq!(buffer.size_used, 0, "PBO uploads from non-zero offset are not supported.");
+ }
+ assert!(buffer.size_used + dst_size <= buffer.inner.pbo.reserved_size, "PixelBuffer is too small");
+
+ let offset = buffer.size_used;
+
+ Ok(UploadStagingBuffer {
+ buffer,
+ offset,
+ size: dst_size,
+ stride: dst_stride,
+ })
+ }
+
+ /// Uploads manually staged texture data to the specified texture.
+ pub fn upload_staged(
+ &mut self,
+ device: &mut Device,
+ texture: &'a Texture,
+ rect: DeviceIntRect,
+ format_override: Option<ImageFormat>,
+ mut staging_buffer: UploadStagingBuffer<'a>,
+ ) -> usize {
+ let size = staging_buffer.size;
+
+ staging_buffer.buffer.chunks.push(UploadChunk {
+ rect,
+ stride: Some(staging_buffer.stride as i32),
+ offset: staging_buffer.offset,
+ format_override,
+ texture,
+ });
+ staging_buffer.buffer.size_used += staging_buffer.size;
+
+ // Flush the buffer if it is full, otherwise return it to the uploader for further use.
+ if staging_buffer.buffer.size_used < staging_buffer.buffer.inner.pbo.reserved_size {
+ self.buffers.push(staging_buffer.buffer);
+ } else {
+ Self::flush_buffer(device, self.pbo_pool, staging_buffer.buffer);
+ }
+
+ size
+ }
+
+ /// Uploads texture data to the specified texture.
+ pub fn upload<T>(
+ &mut self,
+ device: &mut Device,
+ texture: &'a Texture,
+ mut rect: DeviceIntRect,
+ stride: Option<i32>,
+ format_override: Option<ImageFormat>,
+ data: *const T,
+ len: usize,
+ ) -> usize {
+ // Textures dimensions may have been clamped by the hardware. Crop the
+ // upload region to match.
+ let cropped = rect.intersection(
+ &DeviceIntRect::from_size(texture.get_dimensions())
+ );
+ if cfg!(debug_assertions) && cropped.map_or(true, |r| r != rect) {
+ warn!("Cropping texture upload {:?} to {:?}", rect, cropped);
+ }
+ rect = match cropped {
+ None => return 0,
+ Some(r) => r,
+ };
+
+ let bytes_pp = texture.format.bytes_per_pixel() as usize;
+ let width_bytes = rect.width() as usize * bytes_pp;
+
+ let src_stride = stride.map_or(width_bytes, |stride| {
+ assert!(stride >= 0);
+ stride as usize
+ });
+ let src_size = (rect.height() as usize - 1) * src_stride + width_bytes;
+ assert!(src_size <= len * mem::size_of::<T>());
+
+ match device.upload_method {
+ UploadMethod::Immediate => {
+ if cfg!(debug_assertions) {
+ let mut bound_buffer = [0];
+ unsafe {
+ device.gl.get_integer_v(gl::PIXEL_UNPACK_BUFFER_BINDING, &mut bound_buffer);
+ }
+ assert_eq!(bound_buffer[0], 0, "GL_PIXEL_UNPACK_BUFFER must not be bound for immediate uploads.");
+ }
+
+ Self::update_impl(device, UploadChunk {
+ rect,
+ stride: Some(src_stride as i32),
+ offset: data as _,
+ format_override,
+ texture,
+ });
+
+ width_bytes * rect.height() as usize
+ }
+ UploadMethod::PixelBuffer(_) => {
+ let mut staging_buffer = match self.stage(device, texture.format, rect.size()) {
+ Ok(staging_buffer) => staging_buffer,
+ Err(_) => return 0,
+ };
+ let dst_stride = staging_buffer.get_stride();
+
+ unsafe {
+ let src: &[mem::MaybeUninit<u8>] = slice::from_raw_parts(data as *const _, src_size);
+
+ if src_stride == dst_stride {
+ // the stride is already optimal, so simply copy
+ // the data as-is in to the buffer
+ staging_buffer.get_mapping()[..src_size].copy_from_slice(src);
+ } else {
+ // copy the data line-by-line in to the buffer so
+ // that it has an optimal stride
+ for y in 0..rect.height() as usize {
+ let src_start = y * src_stride;
+ let src_end = src_start + width_bytes;
+ let dst_start = y * staging_buffer.get_stride();
+ let dst_end = dst_start + width_bytes;
+
+ staging_buffer.get_mapping()[dst_start..dst_end].copy_from_slice(&src[src_start..src_end])
+ }
+ }
+ }
+
+ self.upload_staged(device, texture, rect, format_override, staging_buffer)
+ }
+ }
+ }
+
+ fn flush_buffer(device: &mut Device, pbo_pool: &mut UploadPBOPool, mut buffer: PixelBuffer) {
+ device.gl.bind_buffer(gl::PIXEL_UNPACK_BUFFER, buffer.inner.pbo.id);
+ match buffer.inner.mapping {
+ PBOMapping::Unmapped => unreachable!("UploadPBO should be mapped at this stage."),
+ PBOMapping::Transient(_) => {
+ device.gl.unmap_buffer(gl::PIXEL_UNPACK_BUFFER);
+ buffer.inner.mapping = PBOMapping::Unmapped;
+ }
+ PBOMapping::Persistent(_) => {
+ device.gl.flush_mapped_buffer_range(gl::PIXEL_UNPACK_BUFFER, 0, buffer.size_used as _);
+ }
+ }
+ buffer.flush_chunks(device);
+ let pbo = mem::replace(&mut buffer.inner, UploadPBO::empty());
+ pbo_pool.return_pbo(device, pbo);
+ }
+
+ /// Flushes all pending texture uploads. Must be called after all
+ /// required upload() or upload_staged() calls have been made.
+ pub fn flush(mut self, device: &mut Device) {
+ for buffer in self.buffers.drain(..) {
+ Self::flush_buffer(device, self.pbo_pool, buffer);
+ }
+
+ device.gl.bind_buffer(gl::PIXEL_UNPACK_BUFFER, 0);
+ }
+
+ fn update_impl(device: &mut Device, chunk: UploadChunk) {
+ device.bind_texture(DEFAULT_TEXTURE, chunk.texture, Swizzle::default());
+
+ let format = chunk.format_override.unwrap_or(chunk.texture.format);
+ let (gl_format, bpp, data_type) = match format {
+ ImageFormat::R8 => (gl::RED, 1, gl::UNSIGNED_BYTE),
+ ImageFormat::R16 => (gl::RED, 2, gl::UNSIGNED_SHORT),
+ ImageFormat::BGRA8 => (device.bgra_formats.external, 4, device.bgra_pixel_type),
+ ImageFormat::RGBA8 => (gl::RGBA, 4, gl::UNSIGNED_BYTE),
+ ImageFormat::RG8 => (gl::RG, 2, gl::UNSIGNED_BYTE),
+ ImageFormat::RG16 => (gl::RG, 4, gl::UNSIGNED_SHORT),
+ ImageFormat::RGBAF32 => (gl::RGBA, 16, gl::FLOAT),
+ ImageFormat::RGBAI32 => (gl::RGBA_INTEGER, 16, gl::INT),
+ };
+
+ let row_length = match chunk.stride {
+ Some(value) => value / bpp,
+ None => chunk.texture.size.width,
+ };
+
+ if chunk.stride.is_some() {
+ device.gl.pixel_store_i(
+ gl::UNPACK_ROW_LENGTH,
+ row_length as _,
+ );
+ }
+
+ let pos = chunk.rect.min;
+ let size = chunk.rect.size();
+
+ match chunk.texture.target {
+ gl::TEXTURE_2D | gl::TEXTURE_RECTANGLE | gl::TEXTURE_EXTERNAL_OES => {
+ device.gl.tex_sub_image_2d_pbo(
+ chunk.texture.target,
+ 0,
+ pos.x as _,
+ pos.y as _,
+ size.width as _,
+ size.height as _,
+ gl_format,
+ data_type,
+ chunk.offset,
+ );
+ }
+ _ => panic!("BUG: Unexpected texture target!"),
+ }
+
+ // If using tri-linear filtering, build the mip-map chain for this texture.
+ if chunk.texture.filter == TextureFilter::Trilinear {
+ device.gl.generate_mipmap(chunk.texture.target);
+ }
+
+ // Reset row length to 0, otherwise the stride would apply to all texture uploads.
+ if chunk.stride.is_some() {
+ device.gl.pixel_store_i(gl::UNPACK_ROW_LENGTH, 0 as _);
+ }
+ }
+}
+
+fn texels_to_u8_slice<T: Texel>(texels: &[T]) -> &[u8] {
+ unsafe {
+ slice::from_raw_parts(texels.as_ptr() as *const u8, texels.len() * mem::size_of::<T>())
+ }
+}
diff --git a/gfx/wr/webrender/src/device/mod.rs b/gfx/wr/webrender/src/device/mod.rs
new file mode 100644
index 0000000000..21684dea3e
--- /dev/null
+++ b/gfx/wr/webrender/src/device/mod.rs
@@ -0,0 +1,9 @@
+/* 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/. */
+
+mod gl;
+pub mod query_gl;
+
+pub use self::gl::*;
+pub use self::query_gl as query;
diff --git a/gfx/wr/webrender/src/device/query_gl.rs b/gfx/wr/webrender/src/device/query_gl.rs
new file mode 100644
index 0000000000..c7fd9a9070
--- /dev/null
+++ b/gfx/wr/webrender/src/device/query_gl.rs
@@ -0,0 +1,318 @@
+/* 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 gleam::gl;
+use std::mem;
+use std::rc::Rc;
+
+use crate::device::GpuFrameId;
+use crate::profiler::GpuProfileTag;
+
+#[derive(Copy, Clone, Debug)]
+pub enum GpuDebugMethod {
+ None,
+ MarkerEXT,
+ KHR,
+}
+
+#[derive(Debug, Clone)]
+pub struct GpuTimer {
+ pub tag: GpuProfileTag,
+ pub time_ns: u64,
+}
+
+#[derive(Debug, Clone)]
+pub struct GpuSampler {
+ pub tag: GpuProfileTag,
+ pub count: u64,
+}
+
+pub struct QuerySet<T> {
+ set: Vec<gl::GLuint>,
+ data: Vec<T>,
+ pending: gl::GLuint,
+}
+
+impl<T> QuerySet<T> {
+ fn new() -> Self {
+ QuerySet {
+ set: Vec::new(),
+ data: Vec::new(),
+ pending: 0,
+ }
+ }
+
+ fn reset(&mut self) {
+ self.data.clear();
+ self.pending = 0;
+ }
+
+ fn add(&mut self, value: T) -> Option<gl::GLuint> {
+ assert_eq!(self.pending, 0);
+ self.set.get(self.data.len()).cloned().map(|query_id| {
+ self.data.push(value);
+ self.pending = query_id;
+ query_id
+ })
+ }
+
+ fn take<F: Fn(&mut T, gl::GLuint)>(&mut self, fun: F) -> Vec<T> {
+ let mut data = mem::replace(&mut self.data, Vec::new());
+ for (value, &query) in data.iter_mut().zip(self.set.iter()) {
+ fun(value, query)
+ }
+ data
+ }
+}
+
+pub struct GpuFrameProfile {
+ gl: Rc<dyn gl::Gl>,
+ timers: QuerySet<GpuTimer>,
+ samplers: QuerySet<GpuSampler>,
+ frame_id: GpuFrameId,
+ inside_frame: bool,
+ debug_method: GpuDebugMethod,
+}
+
+impl GpuFrameProfile {
+ fn new(gl: Rc<dyn gl::Gl>, debug_method: GpuDebugMethod) -> Self {
+ GpuFrameProfile {
+ gl,
+ timers: QuerySet::new(),
+ samplers: QuerySet::new(),
+ frame_id: GpuFrameId::new(0),
+ inside_frame: false,
+ debug_method
+ }
+ }
+
+ fn enable_timers(&mut self, count: i32) {
+ self.timers.set = self.gl.gen_queries(count);
+ }
+
+ fn disable_timers(&mut self) {
+ if !self.timers.set.is_empty() {
+ self.gl.delete_queries(&self.timers.set);
+ }
+ self.timers.set = Vec::new();
+ }
+
+ fn enable_samplers(&mut self, count: i32) {
+ self.samplers.set = self.gl.gen_queries(count);
+ }
+
+ fn disable_samplers(&mut self) {
+ if !self.samplers.set.is_empty() {
+ self.gl.delete_queries(&self.samplers.set);
+ }
+ self.samplers.set = Vec::new();
+ }
+
+ fn begin_frame(&mut self, frame_id: GpuFrameId) {
+ self.frame_id = frame_id;
+ self.timers.reset();
+ self.samplers.reset();
+ self.inside_frame = true;
+ }
+
+ fn end_frame(&mut self) {
+ self.finish_timer();
+ self.finish_sampler();
+ self.inside_frame = false;
+ }
+
+ fn finish_timer(&mut self) {
+ debug_assert!(self.inside_frame);
+ if self.timers.pending != 0 {
+ self.gl.end_query(gl::TIME_ELAPSED);
+ self.timers.pending = 0;
+ }
+ }
+
+ fn finish_sampler(&mut self) {
+ debug_assert!(self.inside_frame);
+ if self.samplers.pending != 0 {
+ self.gl.end_query(gl::SAMPLES_PASSED);
+ self.samplers.pending = 0;
+ }
+ }
+
+ fn start_timer(&mut self, tag: GpuProfileTag) -> GpuTimeQuery {
+ self.finish_timer();
+
+ let marker = GpuMarker::new(&self.gl, tag.label, self.debug_method);
+
+ if let Some(query) = self.timers.add(GpuTimer { tag, time_ns: 0 }) {
+ self.gl.begin_query(gl::TIME_ELAPSED, query);
+ }
+
+ GpuTimeQuery(marker)
+ }
+
+ fn start_sampler(&mut self, tag: GpuProfileTag) -> GpuSampleQuery {
+ self.finish_sampler();
+
+ if let Some(query) = self.samplers.add(GpuSampler { tag, count: 0 }) {
+ self.gl.begin_query(gl::SAMPLES_PASSED, query);
+ }
+
+ GpuSampleQuery
+ }
+
+ fn build_samples(&mut self) -> (GpuFrameId, Vec<GpuTimer>, Vec<GpuSampler>) {
+ debug_assert!(!self.inside_frame);
+ let gl = &self.gl;
+
+ (
+ self.frame_id,
+ self.timers.take(|timer, query| {
+ timer.time_ns = gl.get_query_object_ui64v(query, gl::QUERY_RESULT)
+ }),
+ self.samplers.take(|sampler, query| {
+ sampler.count = gl.get_query_object_ui64v(query, gl::QUERY_RESULT)
+ }),
+ )
+ }
+}
+
+impl Drop for GpuFrameProfile {
+ fn drop(&mut self) {
+ self.disable_timers();
+ self.disable_samplers();
+ }
+}
+
+const NUM_PROFILE_FRAMES: usize = 4;
+
+pub struct GpuProfiler {
+ gl: Rc<dyn gl::Gl>,
+ frames: [GpuFrameProfile; NUM_PROFILE_FRAMES],
+ next_frame: usize,
+ debug_method: GpuDebugMethod
+}
+
+impl GpuProfiler {
+ pub fn new(gl: Rc<dyn gl::Gl>, debug_method: GpuDebugMethod) -> Self {
+ let f = || GpuFrameProfile::new(Rc::clone(&gl), debug_method);
+
+ let frames = [f(), f(), f(), f()];
+ GpuProfiler {
+ gl,
+ next_frame: 0,
+ frames,
+ debug_method
+ }
+ }
+
+ pub fn enable_timers(&mut self) {
+ const MAX_TIMERS_PER_FRAME: i32 = 256;
+
+ for frame in &mut self.frames {
+ frame.enable_timers(MAX_TIMERS_PER_FRAME);
+ }
+ }
+
+ pub fn disable_timers(&mut self) {
+ for frame in &mut self.frames {
+ frame.disable_timers();
+ }
+ }
+
+ pub fn enable_samplers(&mut self) {
+ const MAX_SAMPLERS_PER_FRAME: i32 = 16;
+ if cfg!(target_os = "macos") {
+ warn!("Expect macOS driver bugs related to sample queries")
+ }
+
+ for frame in &mut self.frames {
+ frame.enable_samplers(MAX_SAMPLERS_PER_FRAME);
+ }
+ }
+
+ pub fn disable_samplers(&mut self) {
+ for frame in &mut self.frames {
+ frame.disable_samplers();
+ }
+ }
+
+ pub fn build_samples(&mut self) -> (GpuFrameId, Vec<GpuTimer>, Vec<GpuSampler>) {
+ self.frames[self.next_frame].build_samples()
+ }
+
+ pub fn begin_frame(&mut self, frame_id: GpuFrameId) {
+ self.frames[self.next_frame].begin_frame(frame_id);
+ }
+
+ pub fn end_frame(&mut self) {
+ self.frames[self.next_frame].end_frame();
+ self.next_frame = (self.next_frame + 1) % self.frames.len();
+ }
+
+ pub fn start_timer(&mut self, tag: GpuProfileTag) -> GpuTimeQuery {
+ self.frames[self.next_frame].start_timer(tag)
+ }
+
+ pub fn start_sampler(&mut self, tag: GpuProfileTag) -> GpuSampleQuery {
+ self.frames[self.next_frame].start_sampler(tag)
+ }
+
+ pub fn finish_sampler(&mut self, _sampler: GpuSampleQuery) {
+ self.frames[self.next_frame].finish_sampler()
+ }
+
+ pub fn start_marker(&mut self, label: &str) -> GpuMarker {
+ GpuMarker::new(&self.gl, label, self.debug_method)
+ }
+
+ pub fn place_marker(&mut self, label: &str) {
+ GpuMarker::fire(&self.gl, label, self.debug_method)
+ }
+}
+
+#[must_use]
+pub struct GpuMarker {
+ gl: Option<(Rc<dyn gl::Gl>, GpuDebugMethod)>,
+}
+
+impl GpuMarker {
+ fn new(gl: &Rc<dyn gl::Gl>, message: &str, debug_method: GpuDebugMethod) -> Self {
+ let gl = match debug_method {
+ GpuDebugMethod::KHR => {
+ gl.push_debug_group_khr(gl::DEBUG_SOURCE_APPLICATION, 0, message);
+ Some((Rc::clone(gl), debug_method))
+ },
+ GpuDebugMethod::MarkerEXT => {
+ gl.push_group_marker_ext(message);
+ Some((Rc::clone(gl), debug_method))
+ },
+ GpuDebugMethod::None => None,
+ };
+ GpuMarker { gl }
+ }
+
+ fn fire(gl: &Rc<dyn gl::Gl>, message: &str, debug_method: GpuDebugMethod) {
+ match debug_method {
+ GpuDebugMethod::KHR => gl.debug_message_insert_khr(gl::DEBUG_SOURCE_APPLICATION, gl::DEBUG_TYPE_MARKER, 0, gl::DEBUG_SEVERITY_NOTIFICATION, message),
+ GpuDebugMethod::MarkerEXT => gl.insert_event_marker_ext(message),
+ GpuDebugMethod::None => {}
+ };
+ }
+}
+
+impl Drop for GpuMarker {
+ fn drop(&mut self) {
+ if let Some((ref gl, debug_method)) = self.gl {
+ match debug_method {
+ GpuDebugMethod::KHR => gl.pop_debug_group_khr(),
+ GpuDebugMethod::MarkerEXT => gl.pop_group_marker_ext(),
+ GpuDebugMethod::None => {}
+ };
+ }
+ }
+}
+
+#[must_use]
+pub struct GpuTimeQuery(GpuMarker);
+#[must_use]
+pub struct GpuSampleQuery;
diff --git a/gfx/wr/webrender/src/ellipse.rs b/gfx/wr/webrender/src/ellipse.rs
new file mode 100644
index 0000000000..fac6765984
--- /dev/null
+++ b/gfx/wr/webrender/src/ellipse.rs
@@ -0,0 +1,187 @@
+/* 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 api::units::*;
+use euclid::Size2D;
+use std::f32::consts::FRAC_PI_2;
+
+
+/// Number of steps to integrate arc length over.
+const STEP_COUNT: usize = 20;
+
+/// Represents an ellipse centred at a local space origin.
+#[derive(Debug, Clone)]
+pub struct Ellipse<U> {
+ pub radius: Size2D<f32, U>,
+ pub total_arc_length: f32,
+}
+
+impl<U> Ellipse<U> {
+ pub fn new(radius: Size2D<f32, U>) -> Ellipse<U> {
+ // Approximate the total length of the first quadrant of this ellipse.
+ let total_arc_length = get_simpson_length(FRAC_PI_2, radius.width, radius.height);
+
+ Ellipse {
+ radius,
+ total_arc_length,
+ }
+ }
+
+ /// Binary search to estimate the angle of an ellipse
+ /// for a given arc length. This only searches over the
+ /// first quadrant of an ellipse.
+ pub fn find_angle_for_arc_length(&self, arc_length: f32) -> f32 {
+ // Clamp arc length to [0, pi].
+ let arc_length = arc_length.max(0.0).min(self.total_arc_length);
+
+ let epsilon = 0.01;
+ let mut low = 0.0;
+ let mut high = FRAC_PI_2;
+ let mut theta = 0.0;
+ let mut new_low = 0.0;
+ let mut new_high = FRAC_PI_2;
+
+ while low <= high {
+ theta = 0.5 * (low + high);
+ let length = get_simpson_length(theta, self.radius.width, self.radius.height);
+
+ if (length - arc_length).abs() < epsilon {
+ break;
+ } else if length < arc_length {
+ new_low = theta;
+ } else {
+ new_high = theta;
+ }
+
+ // If we have stopped moving down the arc, the answer that we have is as good as
+ // it is going to get. We break to avoid going into an infinite loop.
+ if new_low == low && new_high == high {
+ break;
+ }
+
+ high = new_high;
+ low = new_low;
+ }
+
+ theta
+ }
+
+ /// Get a point and tangent on this ellipse from a given angle.
+ /// This only works for the first quadrant of the ellipse.
+ pub fn get_point_and_tangent(&self, theta: f32) -> (LayoutPoint, LayoutPoint) {
+ let (sin_theta, cos_theta) = theta.sin_cos();
+ let point = LayoutPoint::new(
+ self.radius.width * cos_theta,
+ self.radius.height * sin_theta,
+ );
+ let tangent = LayoutPoint::new(
+ -self.radius.width * sin_theta,
+ self.radius.height * cos_theta,
+ );
+ (point, tangent)
+ }
+
+ pub fn contains(&self, point: LayoutPoint) -> bool {
+ self.signed_distance(point.to_vector()) <= 0.0
+ }
+
+ /// Find the signed distance from this ellipse given a point.
+ /// Taken from http://www.iquilezles.org/www/articles/ellipsedist/ellipsedist.htm
+ fn signed_distance(&self, point: LayoutVector2D) -> f32 {
+ // This algorithm fails for circles, so we handle them here.
+ if self.radius.width == self.radius.height {
+ return point.length() - self.radius.width;
+ }
+
+ let mut p = LayoutVector2D::new(point.x.abs(), point.y.abs());
+ let mut ab = self.radius.to_vector();
+ if p.x > p.y {
+ p = p.yx();
+ ab = ab.yx();
+ }
+
+ let l = ab.y * ab.y - ab.x * ab.x;
+
+ let m = ab.x * p.x / l;
+ let n = ab.y * p.y / l;
+ let m2 = m * m;
+ let n2 = n * n;
+
+ let c = (m2 + n2 - 1.0) / 3.0;
+ let c3 = c * c * c;
+
+ let q = c3 + m2 * n2 * 2.0;
+ let d = c3 + m2 * n2;
+ let g = m + m * n2;
+
+ let co = if d < 0.0 {
+ let p = (q / c3).acos() / 3.0;
+ let s = p.cos();
+ let t = p.sin() * (3.0_f32).sqrt();
+ let rx = (-c * (s + t + 2.0) + m2).sqrt();
+ let ry = (-c * (s - t + 2.0) + m2).sqrt();
+ (ry + l.signum() * rx + g.abs() / (rx * ry) - m) / 2.0
+ } else {
+ let h = 2.0 * m * n * d.sqrt();
+ let s = (q + h).signum() * (q + h).abs().powf(1.0 / 3.0);
+ let u = (q - h).signum() * (q - h).abs().powf(1.0 / 3.0);
+ let rx = -s - u - c * 4.0 + 2.0 * m2;
+ let ry = (s - u) * (3.0_f32).sqrt();
+ let rm = (rx * rx + ry * ry).sqrt();
+ let p = ry / (rm - rx).sqrt();
+ (p + 2.0 * g / rm - m) / 2.0
+ };
+
+ let si = (1.0 - co * co).sqrt();
+ let r = LayoutVector2D::new(ab.x * co, ab.y * si);
+ (r - p).length() * (p.y - r.y).signum()
+ }
+}
+
+/// Use Simpsons rule to approximate the arc length of
+/// part of an ellipse. Note that this only works over
+/// the range of [0, pi/2].
+// TODO(gw): This is a simplistic way to estimate the
+// arc length of an ellipse segment. We can probably use
+// a faster / more accurate method!
+fn get_simpson_length(theta: f32, rx: f32, ry: f32) -> f32 {
+ let df = theta / STEP_COUNT as f32;
+ let mut sum = 0.0;
+
+ for i in 0 .. (STEP_COUNT + 1) {
+ let (sin_theta, cos_theta) = (i as f32 * df).sin_cos();
+ let a = rx * sin_theta;
+ let b = ry * cos_theta;
+ let y = (a * a + b * b).sqrt();
+ let q = if i == 0 || i == STEP_COUNT {
+ 1.0
+ } else if i % 2 == 0 {
+ 2.0
+ } else {
+ 4.0
+ };
+
+ sum += q * y;
+ }
+
+ (df / 3.0) * sum
+}
+
+#[cfg(test)]
+pub mod test {
+ use super::*;
+
+ #[test]
+ fn find_angle_for_arc_length_for_long_eclipse() {
+ // Ensure that finding the angle on giant ellipses produces and answer and
+ // doesn't send us into an infinite loop.
+ let ellipse = Ellipse::new(LayoutSize::new(57500.0, 25.0));
+ let _ = ellipse.find_angle_for_arc_length(55674.53);
+ assert!(true);
+
+ let ellipse = Ellipse::new(LayoutSize::new(25.0, 57500.0));
+ let _ = ellipse.find_angle_for_arc_length(55674.53);
+ assert!(true);
+ }
+}
diff --git a/gfx/wr/webrender/src/filterdata.rs b/gfx/wr/webrender/src/filterdata.rs
new file mode 100644
index 0000000000..d399b2252e
--- /dev/null
+++ b/gfx/wr/webrender/src/filterdata.rs
@@ -0,0 +1,219 @@
+/* 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::{hash};
+use crate::gpu_cache::{GpuCacheHandle};
+use crate::frame_builder::FrameBuildingState;
+use crate::gpu_cache::GpuDataRequest;
+use crate::intern;
+use api::{ComponentTransferFuncType};
+
+
+pub type FilterDataHandle = intern::Handle<FilterDataIntern>;
+
+#[derive(Debug, Clone, MallocSizeOf, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum SFilterDataComponent {
+ Identity,
+ Table(Vec<f32>),
+ Discrete(Vec<f32>),
+ Linear(f32, f32),
+ Gamma(f32, f32, f32),
+}
+
+impl Eq for SFilterDataComponent {}
+
+impl hash::Hash for SFilterDataComponent {
+ fn hash<H: hash::Hasher>(&self, state: &mut H) {
+ match self {
+ SFilterDataComponent::Identity => {
+ 0.hash(state);
+ }
+ SFilterDataComponent::Table(values) => {
+ 1.hash(state);
+ values.len().hash(state);
+ for val in values {
+ val.to_bits().hash(state);
+ }
+ }
+ SFilterDataComponent::Discrete(values) => {
+ 2.hash(state);
+ values.len().hash(state);
+ for val in values {
+ val.to_bits().hash(state);
+ }
+ }
+ SFilterDataComponent::Linear(a, b) => {
+ 3.hash(state);
+ a.to_bits().hash(state);
+ b.to_bits().hash(state);
+ }
+ SFilterDataComponent::Gamma(a, b, c) => {
+ 4.hash(state);
+ a.to_bits().hash(state);
+ b.to_bits().hash(state);
+ c.to_bits().hash(state);
+ }
+ }
+ }
+}
+
+impl SFilterDataComponent {
+ pub fn to_int(&self) -> u32 {
+ match self {
+ SFilterDataComponent::Identity => 0,
+ SFilterDataComponent::Table(_) => 1,
+ SFilterDataComponent::Discrete(_) => 2,
+ SFilterDataComponent::Linear(_, _) => 3,
+ SFilterDataComponent::Gamma(_, _, _) => 4,
+ }
+ }
+
+ pub fn from_functype_values(
+ func_type: ComponentTransferFuncType,
+ values: &[f32],
+ ) -> SFilterDataComponent {
+ match func_type {
+ ComponentTransferFuncType::Identity => SFilterDataComponent::Identity,
+ ComponentTransferFuncType::Table => SFilterDataComponent::Table(values.to_vec()),
+ ComponentTransferFuncType::Discrete => SFilterDataComponent::Discrete(values.to_vec()),
+ ComponentTransferFuncType::Linear => SFilterDataComponent::Linear(values[0], values[1]),
+ ComponentTransferFuncType::Gamma => SFilterDataComponent::Gamma(values[0], values[1], values[2]),
+ }
+ }
+}
+
+#[derive(Debug, Clone, MallocSizeOf, PartialEq, Eq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SFilterData {
+ pub r_func: SFilterDataComponent,
+ pub g_func: SFilterDataComponent,
+ pub b_func: SFilterDataComponent,
+ pub a_func: SFilterDataComponent,
+}
+
+#[derive(Debug, Clone, MallocSizeOf, PartialEq, Eq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SFilterDataKey {
+ pub data: SFilterData,
+}
+
+impl intern::InternDebug for SFilterDataKey {}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct SFilterDataTemplate {
+ pub data: SFilterData,
+ pub gpu_cache_handle: GpuCacheHandle,
+}
+
+impl From<SFilterDataKey> for SFilterDataTemplate {
+ fn from(item: SFilterDataKey) -> Self {
+ SFilterDataTemplate {
+ data: item.data,
+ gpu_cache_handle: GpuCacheHandle::new(),
+ }
+ }
+}
+
+impl SFilterData {
+ pub fn is_identity(&self) -> bool {
+ self.r_func == SFilterDataComponent::Identity
+ && self.g_func == SFilterDataComponent::Identity
+ && self.b_func == SFilterDataComponent::Identity
+ && self.a_func == SFilterDataComponent::Identity
+ }
+
+ pub fn update(&self, mut request: GpuDataRequest) {
+ push_component_transfer_data(&self.r_func, &mut request);
+ push_component_transfer_data(&self.g_func, &mut request);
+ push_component_transfer_data(&self.b_func, &mut request);
+ push_component_transfer_data(&self.a_func, &mut request);
+ assert!(!self.is_identity());
+ }
+}
+
+impl SFilterDataTemplate {
+ /// Update the GPU cache for a given filter data template. This may be called multiple
+ /// times per frame, by each primitive reference that refers to this interned
+ /// template. The initial request call to the GPU cache ensures that work is only
+ /// done if the cache entry is invalid (due to first use or eviction).
+ pub fn update(
+ &mut self,
+ frame_state: &mut FrameBuildingState,
+ ) {
+ if let Some(request) = frame_state.gpu_cache.request(&mut self.gpu_cache_handle) {
+ self.data.update(request);
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug, MallocSizeOf)]
+#[cfg_attr(any(feature = "serde"), derive(Deserialize, Serialize))]
+pub enum FilterDataIntern {}
+
+impl intern::Internable for FilterDataIntern {
+ type Key = SFilterDataKey;
+ type StoreData = SFilterDataTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_FILTER_DATA;
+}
+
+fn push_component_transfer_data(
+ func_comp: &SFilterDataComponent,
+ request: &mut GpuDataRequest,
+) {
+ match func_comp {
+ SFilterDataComponent::Identity => {}
+ SFilterDataComponent::Table(values) |
+ SFilterDataComponent::Discrete(values) => {
+ // Push a 256 entry lookup table.
+ assert!(values.len() > 0);
+ for i in 0 .. 64 {
+ let mut arr = [0.0 ; 4];
+ for j in 0 .. 4 {
+ if (values.len() == 1) || (i == 63 && j == 3) {
+ arr[j] = values[values.len()-1];
+ } else {
+ let c = ((4*i + j) as f32)/255.0;
+ match func_comp {
+ SFilterDataComponent::Table(_) => {
+ let n = (values.len()-1) as f32;
+ let k = (n * c).floor() as u32;
+ let ku = k as usize;
+ assert!(ku < values.len()-1);
+ arr[j] = values[ku] + (c*n - (k as f32)) * (values[ku+1] - values[ku]);
+ }
+ SFilterDataComponent::Discrete(_) => {
+ let n = values.len() as f32;
+ let k = (n * c).floor() as usize;
+ assert!(k < values.len());
+ arr[j] = values[k];
+ }
+ SFilterDataComponent::Identity |
+ SFilterDataComponent::Linear(_,_) |
+ SFilterDataComponent::Gamma(_,_,_) => {
+ unreachable!();
+ }
+ }
+
+ }
+ }
+
+ request.push(arr);
+ }
+ }
+ SFilterDataComponent::Linear(a, b) => {
+ request.push([*a, *b, 0.0, 0.0]);
+ }
+ SFilterDataComponent::Gamma(a, b, c) => {
+ request.push([*a, *b, *c, 0.0]);
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/frame_builder.rs b/gfx/wr/webrender/src/frame_builder.rs
new file mode 100644
index 0000000000..677c5be42e
--- /dev/null
+++ b/gfx/wr/webrender/src/frame_builder.rs
@@ -0,0 +1,1014 @@
+/* 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 api::{ColorF, DebugFlags, FontRenderMode, PremultipliedColorF};
+use api::units::*;
+use plane_split::BspSplitter;
+use crate::batch::{BatchBuilder, AlphaBatchBuilder, AlphaBatchContainer};
+use crate::clip::{ClipStore, ClipTree};
+use crate::command_buffer::{PrimitiveCommand, CommandBufferList, CommandBufferIndex};
+use crate::spatial_tree::{SpatialTree, SpatialNodeIndex};
+use crate::composite::{CompositorKind, CompositeState, CompositeStatePreallocator};
+use crate::debug_item::DebugItem;
+use crate::gpu_cache::{GpuCache, GpuCacheHandle};
+use crate::gpu_types::{PrimitiveHeaders, TransformPalette, ZBufferIdGenerator};
+use crate::gpu_types::{QuadSegment, TransformData};
+use crate::internal_types::{FastHashMap, PlaneSplitter, FrameId, FrameStamp};
+use crate::picture::{DirtyRegion, SliceId, TileCacheInstance};
+use crate::picture::{SurfaceInfo, SurfaceIndex};
+use crate::picture::{SubpixelMode, RasterConfig, PictureCompositeMode};
+use crate::prepare::{prepare_primitives};
+use crate::prim_store::{PictureIndex};
+use crate::prim_store::{DeferredResolve, PrimitiveInstance};
+use crate::profiler::{self, TransactionProfile};
+use crate::render_backend::{DataStores, ScratchBuffer};
+use crate::renderer::{GpuBuffer, GpuBufferBuilder};
+use crate::render_target::{RenderTarget, PictureCacheTarget, TextureCacheRenderTarget, PictureCacheTargetKind};
+use crate::render_target::{RenderTargetContext, RenderTargetKind, AlphaRenderTarget, ColorRenderTarget};
+use crate::render_task_graph::{RenderTaskGraph, Pass, SubPassSurface};
+use crate::render_task_graph::{RenderPass, RenderTaskGraphBuilder};
+use crate::render_task::{RenderTaskKind, StaticRenderTaskSurface};
+use crate::resource_cache::{ResourceCache};
+use crate::scene::{BuiltScene, SceneProperties};
+use crate::space::SpaceMapper;
+use crate::segment::SegmentBuilder;
+use crate::surface::SurfaceBuilder;
+use std::{f32, mem};
+use crate::util::{VecHelper, Preallocator};
+use crate::visibility::{update_prim_visibility, FrameVisibilityState, FrameVisibilityContext};
+
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct FrameBuilderConfig {
+ pub default_font_render_mode: FontRenderMode,
+ pub dual_source_blending_is_supported: bool,
+ /// True if we're running tests (i.e. via wrench).
+ pub testing: bool,
+ pub gpu_supports_fast_clears: bool,
+ pub gpu_supports_advanced_blend: bool,
+ pub advanced_blend_is_coherent: bool,
+ pub gpu_supports_render_target_partial_update: bool,
+ /// Whether ImageBufferKind::TextureExternal images must first be copied
+ /// to a regular texture before rendering.
+ pub external_images_require_copy: bool,
+ pub batch_lookback_count: usize,
+ pub background_color: Option<ColorF>,
+ pub compositor_kind: CompositorKind,
+ pub tile_size_override: Option<DeviceIntSize>,
+ pub max_surface_override: Option<usize>,
+ pub max_depth_ids: i32,
+ pub max_target_size: i32,
+ pub force_invalidation: bool,
+ pub is_software: bool,
+ pub low_quality_pinch_zoom: bool,
+ pub max_shared_surface_size: i32,
+}
+
+/// A set of common / global resources that are retained between
+/// new display lists, such that any GPU cache handles can be
+/// persisted even when a new display list arrives.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct FrameGlobalResources {
+ /// The image shader block for the most common / default
+ /// set of image parameters (color white, stretch == rect.size).
+ pub default_image_handle: GpuCacheHandle,
+
+ /// A GPU cache config for drawing transparent rectangle primitives.
+ /// This is used to 'cut out' overlay tiles where a compositor
+ /// surface exists.
+ pub default_transparent_rect_handle: GpuCacheHandle,
+}
+
+impl FrameGlobalResources {
+ pub fn empty() -> Self {
+ FrameGlobalResources {
+ default_image_handle: GpuCacheHandle::new(),
+ default_transparent_rect_handle: GpuCacheHandle::new(),
+ }
+ }
+
+ pub fn update(
+ &mut self,
+ gpu_cache: &mut GpuCache,
+ ) {
+ if let Some(mut request) = gpu_cache.request(&mut self.default_image_handle) {
+ request.push(PremultipliedColorF::WHITE);
+ request.push(PremultipliedColorF::WHITE);
+ request.push([
+ -1.0, // -ve means use prim rect for stretch size
+ 0.0,
+ 0.0,
+ 0.0,
+ ]);
+ }
+
+ if let Some(mut request) = gpu_cache.request(&mut self.default_transparent_rect_handle) {
+ request.push(PremultipliedColorF::TRANSPARENT);
+ }
+ }
+}
+
+pub struct FrameScratchBuffer {
+ dirty_region_stack: Vec<DirtyRegion>,
+ surface_stack: Vec<(PictureIndex, SurfaceIndex)>,
+}
+
+impl Default for FrameScratchBuffer {
+ fn default() -> Self {
+ FrameScratchBuffer {
+ dirty_region_stack: Vec::new(),
+ surface_stack: Vec::new(),
+ }
+ }
+}
+
+impl FrameScratchBuffer {
+ pub fn begin_frame(&mut self) {
+ self.dirty_region_stack.clear();
+ self.surface_stack.clear();
+ }
+}
+
+/// Produces the frames that are sent to the renderer.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct FrameBuilder {
+ pub globals: FrameGlobalResources,
+ #[cfg_attr(feature = "capture", serde(skip))]
+ prim_headers_prealloc: Preallocator,
+ #[cfg_attr(feature = "capture", serde(skip))]
+ composite_state_prealloc: CompositeStatePreallocator,
+ #[cfg_attr(feature = "capture", serde(skip))]
+ plane_splitters: Vec<PlaneSplitter>,
+}
+
+pub struct FrameBuildingContext<'a> {
+ pub global_device_pixel_scale: DevicePixelScale,
+ pub scene_properties: &'a SceneProperties,
+ pub global_screen_world_rect: WorldRect,
+ pub spatial_tree: &'a SpatialTree,
+ pub max_local_clip: LayoutRect,
+ pub debug_flags: DebugFlags,
+ pub fb_config: &'a FrameBuilderConfig,
+ pub root_spatial_node_index: SpatialNodeIndex,
+}
+
+pub struct FrameBuildingState<'a> {
+ pub rg_builder: &'a mut RenderTaskGraphBuilder,
+ pub clip_store: &'a mut ClipStore,
+ pub resource_cache: &'a mut ResourceCache,
+ pub gpu_cache: &'a mut GpuCache,
+ pub transforms: &'a mut TransformPalette,
+ pub segment_builder: SegmentBuilder,
+ pub surfaces: &'a mut Vec<SurfaceInfo>,
+ pub dirty_region_stack: Vec<DirtyRegion>,
+ pub composite_state: &'a mut CompositeState,
+ pub num_visible_primitives: u32,
+ pub plane_splitters: &'a mut [PlaneSplitter],
+ pub surface_builder: SurfaceBuilder,
+ pub cmd_buffers: &'a mut CommandBufferList,
+ pub clip_tree: &'a ClipTree,
+ pub frame_gpu_data: &'a mut GpuBufferBuilder,
+}
+
+impl<'a> FrameBuildingState<'a> {
+ /// Retrieve the current dirty region during primitive traversal.
+ pub fn current_dirty_region(&self) -> &DirtyRegion {
+ self.dirty_region_stack.last().unwrap()
+ }
+
+ /// Push a new dirty region for child primitives to cull / clip against.
+ pub fn push_dirty_region(&mut self, region: DirtyRegion) {
+ self.dirty_region_stack.push(region);
+ }
+
+ /// Pop the top dirty region from the stack.
+ pub fn pop_dirty_region(&mut self) {
+ self.dirty_region_stack.pop().unwrap();
+ }
+
+ /// Push a primitive command to a set of command buffers
+ pub fn push_prim(
+ &mut self,
+ cmd: &PrimitiveCommand,
+ spatial_node_index: SpatialNodeIndex,
+ targets: &[CommandBufferIndex],
+ ) {
+ for cmd_buffer_index in targets {
+ let cmd_buffer = self.cmd_buffers.get_mut(*cmd_buffer_index);
+ cmd_buffer.add_prim(cmd, spatial_node_index);
+ }
+ }
+
+ /// Push a command to a set of command buffers
+ pub fn push_cmd(
+ &mut self,
+ cmd: &PrimitiveCommand,
+ targets: &[CommandBufferIndex],
+ ) {
+ for cmd_buffer_index in targets {
+ let cmd_buffer = self.cmd_buffers.get_mut(*cmd_buffer_index);
+ cmd_buffer.add_cmd(cmd);
+ }
+ }
+
+ /// Set the active list of segments in a set of command buffers
+ pub fn set_segments(
+ &mut self,
+ segments: &[QuadSegment],
+ targets: &[CommandBufferIndex],
+ ) {
+ for cmd_buffer_index in targets {
+ let cmd_buffer = self.cmd_buffers.get_mut(*cmd_buffer_index);
+ cmd_buffer.set_segments(segments);
+ }
+ }
+}
+
+/// Immutable context of a picture when processing children.
+#[derive(Debug)]
+pub struct PictureContext {
+ pub pic_index: PictureIndex,
+ pub surface_spatial_node_index: SpatialNodeIndex,
+ pub raster_spatial_node_index: SpatialNodeIndex,
+ /// The surface that this picture will render on.
+ pub surface_index: SurfaceIndex,
+ pub dirty_region_count: usize,
+ pub subpixel_mode: SubpixelMode,
+}
+
+/// Mutable state of a picture that gets modified when
+/// the children are processed.
+pub struct PictureState {
+ pub map_local_to_pic: SpaceMapper<LayoutPixel, PicturePixel>,
+ pub map_pic_to_world: SpaceMapper<PicturePixel, WorldPixel>,
+}
+
+impl FrameBuilder {
+ pub fn new() -> Self {
+ FrameBuilder {
+ globals: FrameGlobalResources::empty(),
+ prim_headers_prealloc: Preallocator::new(0),
+ composite_state_prealloc: CompositeStatePreallocator::default(),
+ plane_splitters: Vec::new(),
+ }
+ }
+
+ /// Compute the contribution (bounding rectangles, and resources) of layers and their
+ /// primitives in screen space.
+ fn build_layer_screen_rects_and_cull_layers(
+ &mut self,
+ scene: &mut BuiltScene,
+ global_screen_world_rect: WorldRect,
+ resource_cache: &mut ResourceCache,
+ gpu_cache: &mut GpuCache,
+ rg_builder: &mut RenderTaskGraphBuilder,
+ global_device_pixel_scale: DevicePixelScale,
+ scene_properties: &SceneProperties,
+ transform_palette: &mut TransformPalette,
+ data_stores: &mut DataStores,
+ scratch: &mut ScratchBuffer,
+ debug_flags: DebugFlags,
+ composite_state: &mut CompositeState,
+ tile_caches: &mut FastHashMap<SliceId, Box<TileCacheInstance>>,
+ spatial_tree: &SpatialTree,
+ cmd_buffers: &mut CommandBufferList,
+ frame_gpu_data: &mut GpuBufferBuilder,
+ profile: &mut TransactionProfile,
+ ) {
+ profile_scope!("build_layer_screen_rects_and_cull_layers");
+
+ let root_spatial_node_index = spatial_tree.root_reference_frame_index();
+
+ const MAX_CLIP_COORD: f32 = 1.0e9;
+
+ // Reset all plane splitters. These are retained from frame to frame to reduce
+ // per-frame allocations
+ self.plane_splitters.resize_with(scene.num_plane_splitters, BspSplitter::new);
+ for splitter in &mut self.plane_splitters {
+ splitter.reset();
+ }
+
+ let frame_context = FrameBuildingContext {
+ global_device_pixel_scale,
+ scene_properties,
+ global_screen_world_rect,
+ spatial_tree,
+ max_local_clip: LayoutRect {
+ min: LayoutPoint::new(-MAX_CLIP_COORD, -MAX_CLIP_COORD),
+ max: LayoutPoint::new(MAX_CLIP_COORD, MAX_CLIP_COORD),
+ },
+ debug_flags,
+ fb_config: &scene.config,
+ root_spatial_node_index,
+ };
+
+ scene.picture_graph.build_update_passes(
+ &mut scene.prim_store.pictures,
+ &frame_context,
+ );
+
+ scene.picture_graph.assign_surfaces(
+ &mut scene.prim_store.pictures,
+ &mut scene.surfaces,
+ tile_caches,
+ &frame_context,
+ );
+
+ scene.picture_graph.propagate_bounding_rects(
+ &mut scene.prim_store.pictures,
+ &mut scene.surfaces,
+ &frame_context,
+ );
+
+ {
+ profile_scope!("UpdateVisibility");
+ profile_marker!("UpdateVisibility");
+ profile.start_time(profiler::FRAME_VISIBILITY_TIME);
+
+ let visibility_context = FrameVisibilityContext {
+ global_device_pixel_scale,
+ spatial_tree,
+ global_screen_world_rect,
+ debug_flags,
+ scene_properties,
+ config: scene.config,
+ root_spatial_node_index,
+ };
+
+ for pic_index in scene.tile_cache_pictures.iter().rev() {
+ let pic = &mut scene.prim_store.pictures[pic_index.0];
+
+ match pic.raster_config {
+ Some(RasterConfig { surface_index, composite_mode: PictureCompositeMode::TileCache { slice_id }, .. }) => {
+ let tile_cache = tile_caches
+ .get_mut(&slice_id)
+ .expect("bug: non-existent tile cache");
+
+ let mut visibility_state = FrameVisibilityState {
+ surface_stack: scratch.frame.surface_stack.take(),
+ resource_cache,
+ gpu_cache,
+ clip_store: &mut scene.clip_store,
+ scratch,
+ data_stores,
+ composite_state,
+ clip_tree: &mut scene.clip_tree,
+ };
+
+ // If we have a tile cache for this picture, see if any of the
+ // relative transforms have changed, which means we need to
+ // re-map the dependencies of any child primitives.
+ let surface = &scene.surfaces[surface_index.0];
+ let world_culling_rect = tile_cache.pre_update(
+ surface.unclipped_local_rect,
+ surface_index,
+ &visibility_context,
+ &mut visibility_state,
+ );
+
+ // Push a new surface, supplying the list of clips that should be
+ // ignored, since they are handled by clipping when drawing this surface.
+ visibility_state.push_surface(
+ *pic_index,
+ surface_index,
+ );
+ visibility_state.clip_tree.push_clip_root_node(tile_cache.shared_clip_node_id);
+
+ update_prim_visibility(
+ *pic_index,
+ None,
+ &world_culling_rect,
+ &mut scene.prim_store,
+ &mut scene.prim_instances,
+ &mut scene.surfaces,
+ true,
+ &visibility_context,
+ &mut visibility_state,
+ tile_cache,
+ );
+
+ // Build the dirty region(s) for this tile cache.
+ tile_cache.post_update(
+ &visibility_context,
+ &mut visibility_state,
+ );
+
+ visibility_state.clip_tree.pop_clip_root();
+ visibility_state.pop_surface();
+ visibility_state.scratch.frame.surface_stack = visibility_state.surface_stack.take();
+ }
+ _ => {
+ panic!("bug: not a tile cache");
+ }
+ }
+ }
+
+ profile.end_time(profiler::FRAME_VISIBILITY_TIME);
+ }
+
+ profile.start_time(profiler::FRAME_PREPARE_TIME);
+
+ let mut frame_state = FrameBuildingState {
+ rg_builder,
+ clip_store: &mut scene.clip_store,
+ resource_cache,
+ gpu_cache,
+ transforms: transform_palette,
+ segment_builder: SegmentBuilder::new(),
+ surfaces: &mut scene.surfaces,
+ dirty_region_stack: scratch.frame.dirty_region_stack.take(),
+ composite_state,
+ num_visible_primitives: 0,
+ plane_splitters: &mut self.plane_splitters,
+ surface_builder: SurfaceBuilder::new(),
+ cmd_buffers,
+ clip_tree: &mut scene.clip_tree,
+ frame_gpu_data,
+ };
+
+ // Push a default dirty region which culls primitives
+ // against the screen world rect, in absence of any
+ // other dirty regions.
+ let mut default_dirty_region = DirtyRegion::new(
+ root_spatial_node_index,
+ );
+ default_dirty_region.add_dirty_region(
+ frame_context.global_screen_world_rect.cast_unit(),
+ frame_context.spatial_tree,
+ );
+ frame_state.push_dirty_region(default_dirty_region);
+
+ for pic_index in &scene.tile_cache_pictures {
+ if let Some((pic_context, mut pic_state, mut prim_list)) = scene
+ .prim_store
+ .pictures[pic_index.0]
+ .take_context(
+ *pic_index,
+ None,
+ SubpixelMode::Allow,
+ &mut frame_state,
+ &frame_context,
+ &mut scratch.primitive,
+ tile_caches,
+ )
+ {
+ profile_marker!("PreparePrims");
+
+ prepare_primitives(
+ &mut scene.prim_store,
+ &mut prim_list,
+ &pic_context,
+ &mut pic_state,
+ &frame_context,
+ &mut frame_state,
+ data_stores,
+ &mut scratch.primitive,
+ tile_caches,
+ &mut scene.prim_instances,
+ );
+
+ let pic = &mut scene.prim_store.pictures[pic_index.0];
+ pic.restore_context(
+ *pic_index,
+ prim_list,
+ pic_context,
+ &scene.prim_instances,
+ &frame_context,
+ &mut frame_state,
+ );
+ }
+ }
+
+ frame_state.pop_dirty_region();
+ frame_state.surface_builder.finalize();
+ profile.end_time(profiler::FRAME_PREPARE_TIME);
+ profile.set(profiler::VISIBLE_PRIMITIVES, frame_state.num_visible_primitives);
+
+ scratch.frame.dirty_region_stack = frame_state.dirty_region_stack.take();
+
+ {
+ profile_marker!("BlockOnResources");
+
+ resource_cache.block_until_all_resources_added(
+ gpu_cache,
+ profile,
+ );
+ }
+ }
+
+ pub fn build(
+ &mut self,
+ scene: &mut BuiltScene,
+ resource_cache: &mut ResourceCache,
+ gpu_cache: &mut GpuCache,
+ rg_builder: &mut RenderTaskGraphBuilder,
+ stamp: FrameStamp,
+ device_origin: DeviceIntPoint,
+ scene_properties: &SceneProperties,
+ data_stores: &mut DataStores,
+ scratch: &mut ScratchBuffer,
+ debug_flags: DebugFlags,
+ tile_caches: &mut FastHashMap<SliceId, Box<TileCacheInstance>>,
+ spatial_tree: &mut SpatialTree,
+ dirty_rects_are_valid: bool,
+ profile: &mut TransactionProfile,
+ ) -> Frame {
+ profile_scope!("build");
+ profile_marker!("BuildFrame");
+
+ profile.set(profiler::PRIMITIVES, scene.prim_instances.len());
+ profile.set(profiler::PICTURE_CACHE_SLICES, scene.tile_cache_config.picture_cache_slice_count);
+ scratch.begin_frame();
+ gpu_cache.begin_frame(stamp);
+ resource_cache.begin_frame(stamp, gpu_cache, profile);
+
+ // TODO(gw): Follow up patches won't clear this, as they'll be assigned
+ // statically during scene building.
+ scene.surfaces.clear();
+
+ self.globals.update(gpu_cache);
+
+ spatial_tree.update_tree(scene_properties);
+ let mut transform_palette = spatial_tree.build_transform_palette();
+ scene.clip_store.begin_frame(&mut scratch.clip_store);
+
+ rg_builder.begin_frame(stamp.frame_id());
+
+ // TODO(dp): Remove me completely!!
+ let global_device_pixel_scale = DevicePixelScale::new(1.0);
+
+ let output_size = scene.output_rect.size();
+ let screen_world_rect = (scene.output_rect.to_f32() / global_device_pixel_scale).round_out();
+
+ let mut composite_state = CompositeState::new(
+ scene.config.compositor_kind,
+ scene.config.max_depth_ids,
+ dirty_rects_are_valid,
+ scene.config.low_quality_pinch_zoom,
+ );
+
+ self.composite_state_prealloc.preallocate(&mut composite_state);
+
+ let mut cmd_buffers = CommandBufferList::new();
+
+ // TODO(gw): Recycle backing vec buffers for gpu buffer builder between frames
+ let mut gpu_buffer_builder = GpuBufferBuilder::new();
+
+ self.build_layer_screen_rects_and_cull_layers(
+ scene,
+ screen_world_rect,
+ resource_cache,
+ gpu_cache,
+ rg_builder,
+ global_device_pixel_scale,
+ scene_properties,
+ &mut transform_palette,
+ data_stores,
+ scratch,
+ debug_flags,
+ &mut composite_state,
+ tile_caches,
+ spatial_tree,
+ &mut cmd_buffers,
+ &mut gpu_buffer_builder,
+ profile,
+ );
+
+ profile.start_time(profiler::FRAME_BATCHING_TIME);
+
+ let mut deferred_resolves = vec![];
+
+ // Finish creating the frame graph and build it.
+ let render_tasks = rg_builder.end_frame(
+ resource_cache,
+ gpu_cache,
+ &mut deferred_resolves,
+ scene.config.max_shared_surface_size,
+ );
+
+ let mut passes = Vec::new();
+ let mut has_texture_cache_tasks = false;
+ let mut prim_headers = PrimitiveHeaders::new();
+ self.prim_headers_prealloc.preallocate_vec(&mut prim_headers.headers_int);
+ self.prim_headers_prealloc.preallocate_vec(&mut prim_headers.headers_float);
+
+ {
+ profile_marker!("Batching");
+
+ // Used to generated a unique z-buffer value per primitive.
+ let mut z_generator = ZBufferIdGenerator::new(scene.config.max_depth_ids);
+ let use_dual_source_blending = scene.config.dual_source_blending_is_supported;
+
+ for pass in render_tasks.passes.iter().rev() {
+ let mut ctx = RenderTargetContext {
+ global_device_pixel_scale,
+ prim_store: &scene.prim_store,
+ clip_store: &scene.clip_store,
+ resource_cache,
+ use_dual_source_blending,
+ use_advanced_blending: scene.config.gpu_supports_advanced_blend,
+ break_advanced_blend_batches: !scene.config.advanced_blend_is_coherent,
+ batch_lookback_count: scene.config.batch_lookback_count,
+ spatial_tree,
+ data_stores,
+ surfaces: &scene.surfaces,
+ scratch: &mut scratch.primitive,
+ screen_world_rect,
+ globals: &self.globals,
+ tile_caches,
+ root_spatial_node_index: spatial_tree.root_reference_frame_index(),
+ };
+
+ let pass = build_render_pass(
+ pass,
+ output_size,
+ &mut ctx,
+ gpu_cache,
+ &mut gpu_buffer_builder,
+ &render_tasks,
+ &scene.clip_store,
+ &mut transform_palette,
+ &mut prim_headers,
+ &mut z_generator,
+ scene.config.gpu_supports_fast_clears,
+ &scene.prim_instances,
+ &cmd_buffers,
+ );
+
+ has_texture_cache_tasks |= !pass.texture_cache.is_empty();
+ has_texture_cache_tasks |= !pass.picture_cache.is_empty();
+
+ passes.push(pass);
+ }
+
+ let mut ctx = RenderTargetContext {
+ global_device_pixel_scale,
+ clip_store: &scene.clip_store,
+ prim_store: &scene.prim_store,
+ resource_cache,
+ use_dual_source_blending,
+ use_advanced_blending: scene.config.gpu_supports_advanced_blend,
+ break_advanced_blend_batches: !scene.config.advanced_blend_is_coherent,
+ batch_lookback_count: scene.config.batch_lookback_count,
+ spatial_tree,
+ data_stores,
+ surfaces: &scene.surfaces,
+ scratch: &mut scratch.primitive,
+ screen_world_rect,
+ globals: &self.globals,
+ tile_caches,
+ root_spatial_node_index: spatial_tree.root_reference_frame_index(),
+ };
+
+ self.build_composite_pass(
+ scene,
+ &mut ctx,
+ gpu_cache,
+ &mut deferred_resolves,
+ &mut composite_state,
+ );
+ }
+
+ profile.end_time(profiler::FRAME_BATCHING_TIME);
+
+ let gpu_cache_frame_id = gpu_cache.end_frame(profile).frame_id();
+
+ resource_cache.end_frame(profile);
+
+ self.prim_headers_prealloc.record_vec(&mut prim_headers.headers_int);
+ self.composite_state_prealloc.record(&composite_state);
+
+ composite_state.end_frame();
+ scene.clip_store.end_frame(&mut scratch.clip_store);
+ scratch.end_frame();
+
+ let gpu_buffer = gpu_buffer_builder.finalize(&render_tasks);
+
+ Frame {
+ device_rect: DeviceIntRect::from_origin_and_size(
+ device_origin,
+ scene.output_rect.size(),
+ ),
+ passes,
+ transform_palette: transform_palette.finish(),
+ render_tasks,
+ deferred_resolves,
+ gpu_cache_frame_id,
+ has_been_rendered: false,
+ has_texture_cache_tasks,
+ prim_headers,
+ debug_items: mem::replace(&mut scratch.primitive.debug_items, Vec::new()),
+ composite_state,
+ gpu_buffer,
+ }
+ }
+
+ fn build_composite_pass(
+ &self,
+ scene: &BuiltScene,
+ ctx: &RenderTargetContext,
+ gpu_cache: &mut GpuCache,
+ deferred_resolves: &mut Vec<DeferredResolve>,
+ composite_state: &mut CompositeState,
+ ) {
+ for pic_index in &scene.tile_cache_pictures {
+ let pic = &ctx.prim_store.pictures[pic_index.0];
+
+ match pic.raster_config {
+ Some(RasterConfig { composite_mode: PictureCompositeMode::TileCache { slice_id }, .. }) => {
+ // Tile cache instances are added to the composite config, rather than
+ // directly added to batches. This allows them to be drawn with various
+ // present modes during render, such as partial present etc.
+ let tile_cache = &ctx.tile_caches[&slice_id];
+ let map_local_to_world = SpaceMapper::new_with_target(
+ ctx.root_spatial_node_index,
+ tile_cache.spatial_node_index,
+ ctx.screen_world_rect,
+ ctx.spatial_tree,
+ );
+ let world_clip_rect = map_local_to_world
+ .map(&tile_cache.local_clip_rect)
+ .expect("bug: unable to map clip rect");
+ let device_clip_rect = (world_clip_rect * ctx.global_device_pixel_scale).round();
+
+ composite_state.push_surface(
+ tile_cache,
+ device_clip_rect,
+ ctx.resource_cache,
+ gpu_cache,
+ deferred_resolves,
+ );
+ }
+ _ => {
+ panic!("bug: found a top-level prim that isn't a tile cache");
+ }
+ }
+ }
+ }
+}
+
+/// Processes this pass to prepare it for rendering.
+///
+/// Among other things, this allocates output regions for each of our tasks
+/// (added via `add_render_task`) in a RenderTarget and assigns it into that
+/// target.
+pub fn build_render_pass(
+ src_pass: &Pass,
+ screen_size: DeviceIntSize,
+ ctx: &mut RenderTargetContext,
+ gpu_cache: &mut GpuCache,
+ gpu_buffer_builder: &mut GpuBufferBuilder,
+ render_tasks: &RenderTaskGraph,
+ clip_store: &ClipStore,
+ transforms: &mut TransformPalette,
+ prim_headers: &mut PrimitiveHeaders,
+ z_generator: &mut ZBufferIdGenerator,
+ gpu_supports_fast_clears: bool,
+ prim_instances: &[PrimitiveInstance],
+ cmd_buffers: &CommandBufferList,
+) -> RenderPass {
+ profile_scope!("build_render_pass");
+
+ // TODO(gw): In this initial frame graph work, we try to maintain the existing
+ // build_render_pass code as closely as possible, to make the review
+ // simpler and reduce chance of regressions. However, future work should
+ // include refactoring this to more closely match the built frame graph.
+ let mut pass = RenderPass::new(src_pass);
+
+ for sub_pass in &src_pass.sub_passes {
+ match sub_pass.surface {
+ SubPassSurface::Dynamic { target_kind, texture_id, used_rect } => {
+ match target_kind {
+ RenderTargetKind::Color => {
+ let mut target = ColorRenderTarget::new(
+ texture_id,
+ screen_size,
+ gpu_supports_fast_clears,
+ used_rect,
+ );
+
+ for task_id in &sub_pass.task_ids {
+ target.add_task(
+ *task_id,
+ ctx,
+ gpu_cache,
+ gpu_buffer_builder,
+ render_tasks,
+ clip_store,
+ transforms,
+ );
+ }
+
+ pass.color.targets.push(target);
+ }
+ RenderTargetKind::Alpha => {
+ let mut target = AlphaRenderTarget::new(
+ texture_id,
+ screen_size,
+ gpu_supports_fast_clears,
+ used_rect,
+ );
+
+ for task_id in &sub_pass.task_ids {
+ target.add_task(
+ *task_id,
+ ctx,
+ gpu_cache,
+ gpu_buffer_builder,
+ render_tasks,
+ clip_store,
+ transforms,
+ );
+ }
+
+ pass.alpha.targets.push(target);
+ }
+ }
+ }
+ SubPassSurface::Persistent { surface: StaticRenderTaskSurface::PictureCache { ref surface, .. }, .. } => {
+ assert_eq!(sub_pass.task_ids.len(), 1);
+ let task_id = sub_pass.task_ids[0];
+ let task = &render_tasks[task_id];
+ let target_rect = task.get_target_rect();
+ let mut gpu_buffer_builder = GpuBufferBuilder::new();
+
+ match task.kind {
+ RenderTaskKind::Picture(ref pic_task) => {
+ let cmd_buffer = cmd_buffers.get(pic_task.cmd_buffer_index);
+ let scissor_rect = pic_task.scissor_rect.expect("bug: must be set for cache tasks");
+ let valid_rect = pic_task.valid_rect.expect("bug: must be set for cache tasks");
+
+ let batcher = AlphaBatchBuilder::new(
+ screen_size,
+ ctx.break_advanced_blend_batches,
+ ctx.batch_lookback_count,
+ task_id,
+ task_id.into(),
+ );
+
+ let mut batch_builder = BatchBuilder::new(batcher);
+
+ cmd_buffer.iter_prims(&mut |cmd, spatial_node_index, segments| {
+ batch_builder.add_prim_to_batch(
+ cmd,
+ spatial_node_index,
+ ctx,
+ gpu_cache,
+ render_tasks,
+ prim_headers,
+ transforms,
+ pic_task.raster_spatial_node_index,
+ pic_task.surface_spatial_node_index,
+ z_generator,
+ prim_instances,
+ &mut gpu_buffer_builder,
+ segments,
+ );
+ });
+
+ let batcher = batch_builder.finalize();
+
+ let mut batch_containers = Vec::new();
+ let mut alpha_batch_container = AlphaBatchContainer::new(Some(scissor_rect));
+
+ batcher.build(
+ &mut batch_containers,
+ &mut alpha_batch_container,
+ target_rect,
+ None,
+ );
+ debug_assert!(batch_containers.is_empty());
+
+ let target = PictureCacheTarget {
+ surface: surface.clone(),
+ clear_color: pic_task.clear_color,
+ kind: PictureCacheTargetKind::Draw {
+ alpha_batch_container,
+ },
+ dirty_rect: scissor_rect,
+ valid_rect,
+ };
+
+ pass.picture_cache.push(target);
+ }
+ RenderTaskKind::TileComposite(ref tile_task) => {
+ let target = PictureCacheTarget {
+ surface: surface.clone(),
+ clear_color: Some(tile_task.clear_color),
+ kind: PictureCacheTargetKind::Blit {
+ task_id: tile_task.task_id.expect("bug: no source task_id set"),
+ sub_rect_offset: tile_task.sub_rect_offset,
+ },
+ dirty_rect: tile_task.scissor_rect,
+ valid_rect: tile_task.valid_rect,
+ };
+
+ pass.picture_cache.push(target);
+ }
+ _ => {
+ unreachable!();
+ }
+ };
+ }
+ SubPassSurface::Persistent { surface: StaticRenderTaskSurface::TextureCache { target_kind, texture, .. } } => {
+ let texture = pass.texture_cache
+ .entry(texture)
+ .or_insert_with(||
+ TextureCacheRenderTarget::new(target_kind)
+ );
+ for task_id in &sub_pass.task_ids {
+ texture.add_task(*task_id, render_tasks);
+ }
+ }
+ SubPassSurface::Persistent { surface: StaticRenderTaskSurface::ReadOnly { .. } } => {
+ panic!("Should not create a render pass for read-only task locations.");
+ }
+ }
+ }
+
+ pass.color.build(
+ ctx,
+ gpu_cache,
+ render_tasks,
+ prim_headers,
+ transforms,
+ z_generator,
+ prim_instances,
+ cmd_buffers,
+ );
+ pass.alpha.build(
+ ctx,
+ gpu_cache,
+ render_tasks,
+ prim_headers,
+ transforms,
+ z_generator,
+ prim_instances,
+ cmd_buffers,
+ );
+
+ pass
+}
+
+/// A rendering-oriented representation of the frame built by the render backend
+/// and presented to the renderer.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct Frame {
+ /// The rectangle to show the frame in, on screen.
+ pub device_rect: DeviceIntRect,
+ pub passes: Vec<RenderPass>,
+
+ pub transform_palette: Vec<TransformData>,
+ pub render_tasks: RenderTaskGraph,
+ pub prim_headers: PrimitiveHeaders,
+
+ /// The GPU cache frame that the contents of Self depend on
+ pub gpu_cache_frame_id: FrameId,
+
+ /// List of textures that we don't know about yet
+ /// from the backend thread. The render thread
+ /// will use a callback to resolve these and
+ /// patch the data structures.
+ pub deferred_resolves: Vec<DeferredResolve>,
+
+ /// True if this frame contains any render tasks
+ /// that write to the texture cache.
+ pub has_texture_cache_tasks: bool,
+
+ /// True if this frame has been drawn by the
+ /// renderer.
+ pub has_been_rendered: bool,
+
+ /// Debugging information to overlay for this frame.
+ pub debug_items: Vec<DebugItem>,
+
+ /// Contains picture cache tiles, and associated information.
+ /// Used by the renderer to composite tiles into the framebuffer,
+ /// or hand them off to an OS compositor.
+ pub composite_state: CompositeState,
+
+ /// Main GPU data buffer constructed (primarily) during the prepare
+ /// pass for primitives that were visible and dirty.
+ pub gpu_buffer: GpuBuffer,
+}
+
+impl Frame {
+ // This frame must be flushed if it writes to the
+ // texture cache, and hasn't been drawn yet.
+ pub fn must_be_drawn(&self) -> bool {
+ self.has_texture_cache_tasks && !self.has_been_rendered
+ }
+
+ // Returns true if this frame doesn't alter what is on screen currently.
+ pub fn is_nop(&self) -> bool {
+ // If there are no off-screen passes, that implies that there are no
+ // picture cache tiles, and no texture cache tasks being updates. If this
+ // is the case, we can consider the frame a nop (higher level checks
+ // test if a composite is needed due to picture cache surfaces moving
+ // or external surfaces being updated).
+ self.passes.is_empty()
+ }
+}
diff --git a/gfx/wr/webrender/src/freelist.rs b/gfx/wr/webrender/src/freelist.rs
new file mode 100644
index 0000000000..aa90aba03c
--- /dev/null
+++ b/gfx/wr/webrender/src/freelist.rs
@@ -0,0 +1,264 @@
+/* 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 generic backing store for caches.
+//!
+//! `FreeList` is a simple vector-backed data structure where each entry in the
+//! vector contains an Option<T>. It maintains an index-based (rather than
+//! pointer-based) free list to efficiently locate the next unused entry. If all
+//! entries are occupied, insertion appends a new element to the vector.
+//!
+//! It also supports both strong and weak handle semantics. There is exactly one
+//! (non-Clonable) strong handle per occupied entry, which must be passed by
+//! value into `free()` to release an entry. Strong handles can produce an
+//! unlimited number of (Clonable) weak handles, which are used to perform
+//! lookups which may fail of the entry has been freed. A per-entry epoch ensures
+//! that weak handle lookups properly fail even if the entry has been freed and
+//! reused.
+//!
+//! TODO(gw): Add an occupied list head, for fast iteration of the occupied list
+//! to implement retain() style functionality.
+
+use std::{fmt, u32};
+use std::marker::PhantomData;
+
+#[derive(Debug, Copy, Clone, MallocSizeOf, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct Epoch(u32);
+
+impl Epoch {
+ /// Mints a new epoch.
+ ///
+ /// We start at 1 so that 0 is always an invalid epoch.
+ fn new() -> Self {
+ Epoch(1)
+ }
+
+ /// Returns an always-invalid epoch.
+ fn invalid() -> Self {
+ Epoch(0)
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct FreeListHandle<M> {
+ index: u32,
+ epoch: Epoch,
+ _marker: PhantomData<M>,
+}
+
+/// More-compact textual representation for debug logging.
+impl<M> fmt::Debug for FreeListHandle<M> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.debug_struct("StrongHandle")
+ .field("index", &self.index)
+ .field("epoch", &self.epoch.0)
+ .finish()
+ }
+}
+
+impl<M> FreeListHandle<M> {
+ pub fn weak(&self) -> WeakFreeListHandle<M> {
+ WeakFreeListHandle {
+ index: self.index,
+ epoch: self.epoch,
+ _marker: PhantomData,
+ }
+ }
+
+ pub fn invalid() -> Self {
+ Self {
+ index: 0,
+ epoch: Epoch::invalid(),
+ _marker: PhantomData,
+ }
+ }
+
+ /// Returns true if this handle and the supplied weak handle reference
+ /// the same underlying location in the freelist.
+ pub fn matches(&self, weak_handle: &WeakFreeListHandle<M>) -> bool {
+ self.index == weak_handle.index &&
+ self.epoch == weak_handle.epoch
+ }
+}
+
+impl<M> Clone for WeakFreeListHandle<M> {
+ fn clone(&self) -> Self {
+ WeakFreeListHandle {
+ index: self.index,
+ epoch: self.epoch,
+ _marker: PhantomData,
+ }
+ }
+}
+
+impl<M> PartialEq for WeakFreeListHandle<M> {
+ fn eq(&self, other: &Self) -> bool {
+ self.index == other.index && self.epoch == other.epoch
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct WeakFreeListHandle<M> {
+ index: u32,
+ epoch: Epoch,
+ _marker: PhantomData<M>,
+}
+
+/// More-compact textual representation for debug logging.
+impl<M> fmt::Debug for WeakFreeListHandle<M> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.debug_struct("WeakHandle")
+ .field("index", &self.index)
+ .field("epoch", &self.epoch.0)
+ .finish()
+ }
+}
+
+impl<M> WeakFreeListHandle<M> {
+ /// Returns an always-invalid handle.
+ pub fn invalid() -> Self {
+ Self {
+ index: 0,
+ epoch: Epoch::invalid(),
+ _marker: PhantomData,
+ }
+ }
+}
+
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct Slot<T> {
+ next: Option<u32>,
+ epoch: Epoch,
+ value: Option<T>,
+}
+
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct FreeList<T, M> {
+ slots: Vec<Slot<T>>,
+ free_list_head: Option<u32>,
+ active_count: usize,
+ _marker: PhantomData<M>,
+}
+
+impl<T, M> FreeList<T, M> {
+ /// Mints a new `FreeList` with no entries.
+ ///
+ /// Triggers a 1-entry allocation.
+ pub fn new() -> Self {
+ // We guarantee that we never have zero entries by starting with one
+ // free entry. This allows WeakFreeListHandle::invalid() to work
+ // without adding any additional branches.
+ let first_slot = Slot {
+ next: None,
+ epoch: Epoch::new(),
+ value: None,
+ };
+ FreeList {
+ slots: vec![first_slot],
+ free_list_head: Some(0),
+ active_count: 0,
+ _marker: PhantomData,
+ }
+ }
+
+ pub fn clear(&mut self) {
+ self.slots.truncate(1);
+ self.slots[0] = Slot {
+ next: None,
+ epoch: Epoch::new(),
+ value: None,
+ };
+ self.free_list_head = Some(0);
+ self.active_count = 0;
+ }
+
+ #[allow(dead_code)]
+ pub fn get(&self, id: &FreeListHandle<M>) -> &T {
+ self.slots[id.index as usize].value.as_ref().unwrap()
+ }
+
+ #[allow(dead_code)]
+ pub fn get_mut(&mut self, id: &FreeListHandle<M>) -> &mut T {
+ self.slots[id.index as usize].value.as_mut().unwrap()
+ }
+
+ pub fn get_opt(&self, id: &WeakFreeListHandle<M>) -> Option<&T> {
+ let slot = &self.slots[id.index as usize];
+ if slot.epoch == id.epoch {
+ slot.value.as_ref()
+ } else {
+ None
+ }
+ }
+
+ pub fn get_opt_mut(&mut self, id: &WeakFreeListHandle<M>) -> Option<&mut T> {
+ let slot = &mut self.slots[id.index as usize];
+ if slot.epoch == id.epoch {
+ slot.value.as_mut()
+ } else {
+ None
+ }
+ }
+
+ pub fn insert(&mut self, item: T) -> FreeListHandle<M> {
+ self.active_count += 1;
+
+ match self.free_list_head {
+ Some(free_index) => {
+ let slot = &mut self.slots[free_index as usize];
+
+ // Remove from free list.
+ self.free_list_head = slot.next;
+ slot.next = None;
+ slot.value = Some(item);
+
+ FreeListHandle {
+ index: free_index,
+ epoch: slot.epoch,
+ _marker: PhantomData,
+ }
+ }
+ None => {
+ let index = self.slots.len() as u32;
+ let epoch = Epoch::new();
+
+ self.slots.push(Slot {
+ next: None,
+ epoch,
+ value: Some(item),
+ });
+
+ FreeListHandle {
+ index,
+ epoch,
+ _marker: PhantomData,
+ }
+ }
+ }
+ }
+
+ pub fn free(&mut self, id: FreeListHandle<M>) -> T {
+ self.active_count -= 1;
+ let slot = &mut self.slots[id.index as usize];
+ slot.next = self.free_list_head;
+ slot.epoch = Epoch(slot.epoch.0 + 1);
+ self.free_list_head = Some(id.index);
+ slot.value.take().unwrap()
+ }
+
+ #[allow(dead_code)]
+ pub fn len(&self) -> usize {
+ self.active_count
+ }
+}
diff --git a/gfx/wr/webrender/src/glyph_cache.rs b/gfx/wr/webrender/src/glyph_cache.rs
new file mode 100644
index 0000000000..0f895e3212
--- /dev/null
+++ b/gfx/wr/webrender/src/glyph_cache.rs
@@ -0,0 +1,208 @@
+/* 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 api::{FontKey, FontInstanceKey, IdNamespace};
+use glyph_rasterizer::{FontInstance, GlyphFormat, GlyphKey, GlyphRasterizer};
+use crate::internal_types::{FrameId, FrameStamp, FastHashMap};
+use crate::resource_cache::ResourceClassCache;
+use std::sync::Arc;
+use crate::texture_cache::{EvictionNotice, TextureCache};
+use crate::texture_cache::TextureCacheHandle;
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Clone, Debug)]
+pub struct CachedGlyphInfo {
+ pub format: GlyphFormat,
+ pub texture_cache_handle: TextureCacheHandle,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum GlyphCacheEntry {
+ // A glyph that has been successfully rasterized.
+ Cached(CachedGlyphInfo),
+ // A glyph that should not be rasterized (i.e. a space).
+ Blank,
+ // A glyph that has been submitted to the font backend for rasterization,
+ // but is still pending a result.
+ #[allow(dead_code)]
+ Pending,
+}
+
+impl GlyphCacheEntry {
+ fn has_been_evicted(&self, texture_cache: &TextureCache) -> bool {
+ match *self {
+ GlyphCacheEntry::Cached(ref glyph) => {
+ !texture_cache.is_allocated(&glyph.texture_cache_handle)
+ }
+ GlyphCacheEntry::Pending | GlyphCacheEntry::Blank => false,
+ }
+ }
+}
+
+#[allow(dead_code)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Clone)]
+pub enum CachedGlyphData {
+ Memory(Arc<Vec<u8>>),
+ Gpu,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Default)]
+pub struct GlyphKeyCacheInfo {
+ eviction_notice: EvictionNotice,
+ #[cfg(debug_assertions)]
+ #[allow(dead_code)]
+ #[cfg_attr(feature = "replay", serde(default))]
+ last_frame_used: FrameId,
+}
+
+pub type GlyphKeyCache = ResourceClassCache<GlyphKey, GlyphCacheEntry, GlyphKeyCacheInfo>;
+
+impl GlyphKeyCache {
+ pub fn eviction_notice(&self) -> &EvictionNotice {
+ &self.user_data.eviction_notice
+ }
+
+ fn clear_glyphs(&mut self) {
+ self.clear();
+ }
+
+ pub fn add_glyph(&mut self, key: GlyphKey, value: GlyphCacheEntry) {
+ self.insert(key, value);
+ }
+
+ fn clear_evicted(&mut self, texture_cache: &TextureCache) {
+ if self.eviction_notice().check() {
+ // If there are evictions, filter out any glyphs evicted from the
+ // texture cache from the glyph key cache.
+ self.retain(|_, entry| !entry.has_been_evicted(texture_cache));
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct GlyphCache {
+ glyph_key_caches: FastHashMap<FontInstance, GlyphKeyCache>,
+ current_frame: FrameId,
+}
+
+impl GlyphCache {
+ pub fn new() -> Self {
+ GlyphCache {
+ glyph_key_caches: FastHashMap::default(),
+ current_frame: Default::default(),
+ }
+ }
+
+ pub fn insert_glyph_key_cache_for_font(&mut self, font: &FontInstance) -> &mut GlyphKeyCache {
+ let cache = self.glyph_key_caches
+ .entry(font.clone())
+ .or_insert_with(GlyphKeyCache::new);
+ #[cfg(debug_assertions)]
+ {
+ cache.user_data.last_frame_used = self.current_frame;
+ }
+ cache
+ }
+
+ pub fn get_glyph_key_cache_for_font_mut(&mut self, font: &FontInstance) -> &mut GlyphKeyCache {
+ self.glyph_key_caches
+ .get_mut(font)
+ .expect("BUG: Unable to find glyph key cache!")
+ }
+
+ pub fn get_glyph_key_cache_for_font(&self, font: &FontInstance) -> &GlyphKeyCache {
+ self.glyph_key_caches
+ .get(font)
+ .expect("BUG: Unable to find glyph key cache!")
+ }
+
+ pub fn clear(&mut self) {
+ for (_, glyph_key_cache) in &mut self.glyph_key_caches {
+ glyph_key_cache.clear()
+ }
+ // We use this in on_memory_pressure where retaining memory allocations
+ // isn't desirable, so we completely remove the hash map instead of clearing it.
+ self.glyph_key_caches = FastHashMap::default();
+ }
+
+ pub fn delete_font_instances(
+ &mut self,
+ instance_keys: &[FontInstanceKey],
+ glyph_rasterizer: &mut GlyphRasterizer,
+ ) {
+ self.glyph_key_caches.retain(|k, cache| {
+ if instance_keys.contains(&k.instance_key) {
+ cache.clear_glyphs();
+ glyph_rasterizer.delete_font_instance(k);
+ false
+ } else {
+ true
+ }
+ });
+ }
+
+ pub fn delete_fonts(&mut self, font_keys: &[FontKey]) {
+ self.glyph_key_caches.retain(|k, cache| {
+ if font_keys.contains(&k.font_key) {
+ cache.clear_glyphs();
+ false
+ } else {
+ true
+ }
+ });
+ }
+
+ pub fn clear_namespace(&mut self, namespace: IdNamespace) {
+ self.glyph_key_caches.retain(|k, cache| {
+ if k.font_key.0 == namespace {
+ cache.clear_glyphs();
+ false
+ } else {
+ true
+ }
+ });
+ }
+
+ /// Clear out evicted entries from glyph key caches.
+ fn clear_evicted(&mut self, texture_cache: &TextureCache) {
+ for cache in self.glyph_key_caches.values_mut() {
+ // Scan for any glyph key caches that have evictions.
+ cache.clear_evicted(texture_cache);
+ }
+ }
+
+ /// If possible, remove entirely any empty glyph key caches.
+ fn clear_empty_caches(&mut self, glyph_rasterizer: &mut GlyphRasterizer) {
+ self.glyph_key_caches.retain(|key, cache| {
+ // Discard the glyph key cache if it has no valid glyphs.
+ if cache.is_empty() {
+ glyph_rasterizer.delete_font_instance(key);
+ false
+ } else {
+ true
+ }
+ });
+ }
+
+ pub fn begin_frame(
+ &mut self,
+ stamp: FrameStamp,
+ texture_cache: &mut TextureCache,
+ glyph_rasterizer: &mut GlyphRasterizer,
+ ) {
+ profile_scope!("begin_frame");
+ self.current_frame = stamp.frame_id();
+ self.clear_evicted(texture_cache);
+ // Clearing evicted glyphs and pruning excess usage might have produced empty caches,
+ // so get rid of them if possible.
+ self.clear_empty_caches(glyph_rasterizer);
+ }
+}
diff --git a/gfx/wr/webrender/src/gpu_cache.rs b/gfx/wr/webrender/src/gpu_cache.rs
new file mode 100644
index 0000000000..69219243d0
--- /dev/null
+++ b/gfx/wr/webrender/src/gpu_cache.rs
@@ -0,0 +1,937 @@
+/* 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/. */
+
+//! Overview of the GPU cache.
+//!
+//! The main goal of the GPU cache is to allow on-demand
+//! allocation and construction of GPU resources for the
+//! vertex shaders to consume.
+//!
+//! Every item that wants to be stored in the GPU cache
+//! should create a GpuCacheHandle that is used to refer
+//! to a cached GPU resource. Creating a handle is a
+//! cheap operation, that does *not* allocate room in the
+//! cache.
+//!
+//! On any frame when that data is required, the caller
+//! must request that handle, via ```request```. If the
+//! data is not in the cache, the user provided closure
+//! will be invoked to build the data.
+//!
+//! After ```end_frame``` has occurred, callers can
+//! use the ```get_address``` API to get the allocated
+//! address in the GPU cache of a given resource slot
+//! for this frame.
+
+use api::{DebugFlags, DocumentId, PremultipliedColorF};
+#[cfg(test)]
+use api::IdNamespace;
+use api::units::*;
+use euclid::{HomogeneousVector, Box2D};
+use crate::internal_types::{FastHashMap, FastHashSet, FrameStamp, FrameId};
+use crate::profiler::{self, TransactionProfile};
+use crate::prim_store::VECS_PER_SEGMENT;
+use crate::renderer::MAX_VERTEX_TEXTURE_WIDTH;
+use crate::util::VecHelper;
+use std::{u16, u32};
+use std::num::NonZeroU32;
+use std::ops::Add;
+use std::time::{Duration, Instant};
+
+
+/// At the time of this writing, Firefox uses about 15 GPU cache rows on
+/// startup, and then gradually works its way up to the mid-30s with normal
+/// browsing.
+pub const GPU_CACHE_INITIAL_HEIGHT: i32 = 20;
+const NEW_ROWS_PER_RESIZE: i32 = 10;
+
+/// The number of frames an entry can go unused before being evicted.
+const FRAMES_BEFORE_EVICTION: u64 = 10;
+
+/// The ratio of utilized blocks to total blocks for which we start the clock
+/// on reclaiming memory.
+const RECLAIM_THRESHOLD: f32 = 0.2;
+
+/// The amount of time utilization must be below the above threshold before we
+/// blow away the cache and rebuild it.
+const RECLAIM_DELAY_S: u64 = 5;
+
+#[derive(Debug, Copy, Clone, Eq, MallocSizeOf, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct Epoch(u32);
+
+impl Epoch {
+ fn next(&mut self) {
+ *self = Epoch(self.0.wrapping_add(1));
+ }
+}
+
+#[derive(Debug, Copy, Clone, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct CacheLocation {
+ block_index: BlockIndex,
+ epoch: Epoch,
+}
+
+/// A single texel in RGBAF32 texture - 16 bytes.
+#[derive(Copy, Clone, Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct GpuBlockData {
+ data: [f32; 4],
+}
+
+impl GpuBlockData {
+ pub const EMPTY: Self = GpuBlockData { data: [0.0; 4] };
+}
+
+/// Conversion helpers for GpuBlockData
+impl From<PremultipliedColorF> for GpuBlockData {
+ fn from(c: PremultipliedColorF) -> Self {
+ GpuBlockData {
+ data: [c.r, c.g, c.b, c.a],
+ }
+ }
+}
+
+impl From<[f32; 4]> for GpuBlockData {
+ fn from(data: [f32; 4]) -> Self {
+ GpuBlockData { data }
+ }
+}
+
+impl<P> From<Box2D<f32, P>> for GpuBlockData {
+ fn from(r: Box2D<f32, P>) -> Self {
+ GpuBlockData {
+ data: [
+ r.min.x,
+ r.min.y,
+ r.max.x,
+ r.max.y,
+ ],
+ }
+ }
+}
+
+impl<P> From<HomogeneousVector<f32, P>> for GpuBlockData {
+ fn from(v: HomogeneousVector<f32, P>) -> Self {
+ GpuBlockData {
+ data: [
+ v.x,
+ v.y,
+ v.z,
+ v.w,
+ ],
+ }
+ }
+}
+
+impl From<TexelRect> for GpuBlockData {
+ fn from(tr: TexelRect) -> Self {
+ GpuBlockData {
+ data: [tr.uv0.x, tr.uv0.y, tr.uv1.x, tr.uv1.y],
+ }
+ }
+}
+
+
+// A handle to a GPU resource.
+#[derive(Debug, Copy, Clone, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct GpuCacheHandle {
+ location: Option<CacheLocation>,
+}
+
+impl GpuCacheHandle {
+ pub fn new() -> Self {
+ GpuCacheHandle { location: None }
+ }
+
+ pub fn as_int(self, gpu_cache: &GpuCache) -> i32 {
+ gpu_cache.get_address(&self).as_int()
+ }
+}
+
+// A unique address in the GPU cache. These are uploaded
+// as part of the primitive instances, to allow the vertex
+// shader to fetch the specific data.
+#[derive(Copy, Debug, Clone, MallocSizeOf, Eq, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct GpuCacheAddress {
+ pub u: u16,
+ pub v: u16,
+}
+
+impl GpuCacheAddress {
+ fn new(u: usize, v: usize) -> Self {
+ GpuCacheAddress {
+ u: u as u16,
+ v: v as u16,
+ }
+ }
+
+ pub const INVALID: GpuCacheAddress = GpuCacheAddress {
+ u: u16::MAX,
+ v: u16::MAX,
+ };
+
+ pub fn as_int(self) -> i32 {
+ // TODO(gw): Temporarily encode GPU Cache addresses as a single int.
+ // In the future, we can change the PrimitiveInstanceData struct
+ // to use 2x u16 for the vertex attribute instead of an i32.
+ self.v as i32 * MAX_VERTEX_TEXTURE_WIDTH as i32 + self.u as i32
+ }
+}
+
+impl Add<usize> for GpuCacheAddress {
+ type Output = GpuCacheAddress;
+
+ fn add(self, other: usize) -> GpuCacheAddress {
+ GpuCacheAddress {
+ u: self.u + other as u16,
+ v: self.v,
+ }
+ }
+}
+
+// An entry in a free-list of blocks in the GPU cache.
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct Block {
+ // The location in the cache of this block.
+ address: GpuCacheAddress,
+ // The current epoch (generation) of this block.
+ epoch: Epoch,
+ // Index of the next free block in the list it
+ // belongs to (either a free-list or the
+ // occupied list).
+ next: Option<BlockIndex>,
+ // The last frame this block was referenced.
+ last_access_time: FrameId,
+}
+
+impl Block {
+ fn new(
+ address: GpuCacheAddress,
+ next: Option<BlockIndex>,
+ frame_id: FrameId,
+ epoch: Epoch,
+ ) -> Self {
+ Block {
+ address,
+ next,
+ last_access_time: frame_id,
+ epoch,
+ }
+ }
+
+ fn advance_epoch(&mut self, max_epoch: &mut Epoch) {
+ self.epoch.next();
+ if max_epoch.0 < self.epoch.0 {
+ max_epoch.0 = self.epoch.0;
+ }
+ }
+
+ /// Creates an invalid dummy block ID.
+ pub const INVALID: Block = Block {
+ address: GpuCacheAddress { u: 0, v: 0 },
+ epoch: Epoch(0),
+ next: None,
+ last_access_time: FrameId::INVALID,
+ };
+}
+
+/// Represents the index of a Block in the block array. We only create such
+/// structs for blocks that represent the start of a chunk.
+///
+/// Because we use Option<BlockIndex> in a lot of places, we use a NonZeroU32
+/// here and avoid ever using the index zero.
+#[derive(Debug, Copy, Clone, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct BlockIndex(NonZeroU32);
+
+impl BlockIndex {
+ fn new(idx: usize) -> Self {
+ debug_assert!(idx <= u32::MAX as usize);
+ BlockIndex(NonZeroU32::new(idx as u32).expect("Index zero forbidden"))
+ }
+
+ fn get(&self) -> usize {
+ self.0.get() as usize
+ }
+}
+
+// A row in the cache texture.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+struct Row {
+ // The fixed size of blocks that this row supports.
+ // Each row becomes a slab allocator for a fixed block size.
+ // This means no dealing with fragmentation within a cache
+ // row as items are allocated and freed.
+ block_count_per_item: usize,
+}
+
+impl Row {
+ fn new(block_count_per_item: usize) -> Self {
+ Row {
+ block_count_per_item,
+ }
+ }
+}
+
+// A list of update operations that can be applied on the cache
+// this frame. The list of updates is created by the render backend
+// during frame construction. It's passed to the render thread
+// where GL commands can be applied.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub enum GpuCacheUpdate {
+ Copy {
+ block_index: usize,
+ block_count: usize,
+ address: GpuCacheAddress,
+ },
+}
+
+/// Command to inform the debug display in the renderer when chunks are allocated
+/// or freed.
+#[derive(MallocSizeOf)]
+pub enum GpuCacheDebugCmd {
+ /// Describes an allocated chunk.
+ Alloc(GpuCacheDebugChunk),
+ /// Describes a freed chunk.
+ Free(GpuCacheAddress),
+}
+
+#[derive(Clone, MallocSizeOf)]
+pub struct GpuCacheDebugChunk {
+ pub address: GpuCacheAddress,
+ pub size: usize,
+}
+
+#[must_use]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct GpuCacheUpdateList {
+ /// The frame current update list was generated from.
+ pub frame_id: FrameId,
+ /// Whether the texture should be cleared before updates
+ /// are applied.
+ pub clear: bool,
+ /// The current height of the texture. The render thread
+ /// should resize the texture if required.
+ pub height: i32,
+ /// List of updates to apply.
+ pub updates: Vec<GpuCacheUpdate>,
+ /// A flat list of GPU blocks that are pending upload
+ /// to GPU memory.
+ pub blocks: Vec<GpuBlockData>,
+ /// Whole state GPU block metadata for debugging.
+ #[cfg_attr(feature = "serde", serde(skip))]
+ pub debug_commands: Vec<GpuCacheDebugCmd>,
+}
+
+// Holds the free lists of fixed size blocks. Mostly
+// just serves to work around the borrow checker.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+struct FreeBlockLists {
+ free_list_1: Option<BlockIndex>,
+ free_list_2: Option<BlockIndex>,
+ free_list_4: Option<BlockIndex>,
+ free_list_8: Option<BlockIndex>,
+ free_list_16: Option<BlockIndex>,
+ free_list_32: Option<BlockIndex>,
+ free_list_64: Option<BlockIndex>,
+ free_list_128: Option<BlockIndex>,
+ free_list_256: Option<BlockIndex>,
+ free_list_341: Option<BlockIndex>,
+ free_list_512: Option<BlockIndex>,
+ free_list_1024: Option<BlockIndex>,
+}
+
+impl FreeBlockLists {
+ fn new() -> Self {
+ FreeBlockLists {
+ free_list_1: None,
+ free_list_2: None,
+ free_list_4: None,
+ free_list_8: None,
+ free_list_16: None,
+ free_list_32: None,
+ free_list_64: None,
+ free_list_128: None,
+ free_list_256: None,
+ free_list_341: None,
+ free_list_512: None,
+ free_list_1024: None,
+ }
+ }
+
+ fn get_actual_block_count_and_free_list(
+ &mut self,
+ block_count: usize,
+ ) -> (usize, &mut Option<BlockIndex>) {
+ // Find the appropriate free list to use based on the block size.
+ //
+ // Note that we cheat a bit with the 341 bucket, since it's not quite
+ // a divisor of 1024, because purecss-francine allocates many 260-block
+ // chunks, and there's no reason we shouldn't pack these three to a row.
+ // This means the allocation statistics will under-report by one block
+ // for each row using 341-block buckets, which is fine.
+ debug_assert_eq!(MAX_VERTEX_TEXTURE_WIDTH, 1024, "Need to update bucketing");
+ match block_count {
+ 0 => panic!("Can't allocate zero sized blocks!"),
+ 1 => (1, &mut self.free_list_1),
+ 2 => (2, &mut self.free_list_2),
+ 3..=4 => (4, &mut self.free_list_4),
+ 5..=8 => (8, &mut self.free_list_8),
+ 9..=16 => (16, &mut self.free_list_16),
+ 17..=32 => (32, &mut self.free_list_32),
+ 33..=64 => (64, &mut self.free_list_64),
+ 65..=128 => (128, &mut self.free_list_128),
+ 129..=256 => (256, &mut self.free_list_256),
+ 257..=341 => (341, &mut self.free_list_341),
+ 342..=512 => (512, &mut self.free_list_512),
+ 513..=1024 => (1024, &mut self.free_list_1024),
+ _ => panic!("Can't allocate > MAX_VERTEX_TEXTURE_WIDTH per resource!"),
+ }
+ }
+}
+
+// CPU-side representation of the GPU resource cache texture.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+struct Texture {
+ // Current texture height
+ height: i32,
+ // All blocks that have been created for this texture
+ blocks: Vec<Block>,
+ // Metadata about each allocated row.
+ rows: Vec<Row>,
+ // The base Epoch for this texture.
+ base_epoch: Epoch,
+ // The maximum epoch reached. We track this along with the above so
+ // that we can rebuild the Texture and avoid collisions with handles
+ // allocated for the old texture.
+ max_epoch: Epoch,
+ // Free lists of available blocks for each supported
+ // block size in the texture. These are intrusive
+ // linked lists.
+ free_lists: FreeBlockLists,
+ // Linked list of currently occupied blocks. This
+ // makes it faster to iterate blocks looking for
+ // candidates to be evicted from the cache.
+ occupied_list_heads: FastHashMap<DocumentId, BlockIndex>,
+ // Pending blocks that have been written this frame
+ // and will need to be sent to the GPU.
+ pending_blocks: Vec<GpuBlockData>,
+ // Pending update commands.
+ updates: Vec<GpuCacheUpdate>,
+ // Profile stats
+ allocated_block_count: usize,
+ // The stamp at which we first reached our threshold for reclaiming `GpuCache`
+ // memory, or `None` if the threshold hasn't been reached.
+ #[cfg_attr(feature = "serde", serde(skip))]
+ reached_reclaim_threshold: Option<Instant>,
+ // List of debug commands to be sent to the renderer when the GPU cache
+ // debug display is enabled.
+ #[cfg_attr(feature = "serde", serde(skip))]
+ debug_commands: Vec<GpuCacheDebugCmd>,
+ // The current debug flags for the system.
+ debug_flags: DebugFlags,
+}
+
+impl Texture {
+ fn new(base_epoch: Epoch, debug_flags: DebugFlags) -> Self {
+ // Pre-fill the block array with one invalid block so that we never use
+ // 0 for a BlockIndex. This lets us use NonZeroU32 for BlockIndex, which
+ // saves memory.
+ let blocks = vec![Block::INVALID];
+
+ Texture {
+ height: GPU_CACHE_INITIAL_HEIGHT,
+ blocks,
+ rows: Vec::new(),
+ base_epoch,
+ max_epoch: base_epoch,
+ free_lists: FreeBlockLists::new(),
+ pending_blocks: Vec::new(),
+ updates: Vec::new(),
+ occupied_list_heads: FastHashMap::default(),
+ allocated_block_count: 0,
+ reached_reclaim_threshold: None,
+ debug_commands: Vec::new(),
+ debug_flags,
+ }
+ }
+
+ // Push new data into the cache. The ```pending_block_index``` field represents
+ // where the data was pushed into the texture ```pending_blocks``` array.
+ // Return the allocated address for this data.
+ fn push_data(
+ &mut self,
+ pending_block_index: Option<usize>,
+ block_count: usize,
+ frame_stamp: FrameStamp
+ ) -> CacheLocation {
+ debug_assert!(frame_stamp.is_valid());
+ // Find the appropriate free list to use based on the block size.
+ let (alloc_size, free_list) = self.free_lists
+ .get_actual_block_count_and_free_list(block_count);
+
+ // See if we need a new row (if free-list has nothing available)
+ if free_list.is_none() {
+ if self.rows.len() as i32 == self.height {
+ self.height += NEW_ROWS_PER_RESIZE;
+ }
+
+ // Create a new row.
+ let items_per_row = MAX_VERTEX_TEXTURE_WIDTH / alloc_size;
+ let row_index = self.rows.len();
+ self.rows.push(Row::new(alloc_size));
+
+ // Create a ```Block``` for each possible allocation address
+ // in this row, and link it in to the free-list for this
+ // block size.
+ let mut prev_block_index = None;
+ for i in 0 .. items_per_row {
+ let address = GpuCacheAddress::new(i * alloc_size, row_index);
+ let block_index = BlockIndex::new(self.blocks.len());
+ let block = Block::new(address, prev_block_index, frame_stamp.frame_id(), self.base_epoch);
+ self.blocks.push(block);
+ prev_block_index = Some(block_index);
+ }
+
+ *free_list = prev_block_index;
+ }
+
+ // Given the code above, it's now guaranteed that there is a block
+ // available in the appropriate free-list. Pull a block from the
+ // head of the list.
+ let free_block_index = free_list.take().unwrap();
+ let block = &mut self.blocks[free_block_index.get()];
+ *free_list = block.next;
+
+ // Add the block to the occupied linked list.
+ block.next = self.occupied_list_heads.get(&frame_stamp.document_id()).cloned();
+ block.last_access_time = frame_stamp.frame_id();
+ self.occupied_list_heads.insert(frame_stamp.document_id(), free_block_index);
+ self.allocated_block_count += alloc_size;
+
+ if let Some(pending_block_index) = pending_block_index {
+ // Add this update to the pending list of blocks that need
+ // to be updated on the GPU.
+ self.updates.push(GpuCacheUpdate::Copy {
+ block_index: pending_block_index,
+ block_count,
+ address: block.address,
+ });
+ }
+
+ // If we're using the debug display, communicate the allocation to the
+ // renderer thread. Note that we do this regardless of whether or not
+ // pending_block_index is None (if it is, the renderer thread will fill
+ // in the data via a deferred resolve, but the block is still considered
+ // allocated).
+ if self.debug_flags.contains(DebugFlags::GPU_CACHE_DBG) {
+ self.debug_commands.push(GpuCacheDebugCmd::Alloc(GpuCacheDebugChunk {
+ address: block.address,
+ size: block_count,
+ }));
+ }
+
+ CacheLocation {
+ block_index: free_block_index,
+ epoch: block.epoch,
+ }
+ }
+
+ // Run through the list of occupied cache blocks and evict
+ // any old blocks that haven't been referenced for a while.
+ fn evict_old_blocks(&mut self, frame_stamp: FrameStamp) {
+ debug_assert!(frame_stamp.is_valid());
+ // Prune any old items from the list to make room.
+ // Traverse the occupied linked list and see
+ // which items have not been used for a long time.
+ let mut current_block = self.occupied_list_heads.get(&frame_stamp.document_id()).map(|x| *x);
+ let mut prev_block: Option<BlockIndex> = None;
+
+ while let Some(index) = current_block {
+ let (next_block, should_unlink) = {
+ let block = &mut self.blocks[index.get()];
+
+ let next_block = block.next;
+ let mut should_unlink = false;
+
+ // If this resource has not been used in the last
+ // few frames, free it from the texture and mark
+ // as empty.
+ if block.last_access_time + FRAMES_BEFORE_EVICTION < frame_stamp.frame_id() {
+ should_unlink = true;
+
+ // Get the row metadata from the address.
+ let row = &mut self.rows[block.address.v as usize];
+
+ // Use the row metadata to determine which free-list
+ // this block belongs to.
+ let (_, free_list) = self.free_lists
+ .get_actual_block_count_and_free_list(row.block_count_per_item);
+
+ block.advance_epoch(&mut self.max_epoch);
+ block.next = *free_list;
+ *free_list = Some(index);
+
+ self.allocated_block_count -= row.block_count_per_item;
+
+ if self.debug_flags.contains(DebugFlags::GPU_CACHE_DBG) {
+ let cmd = GpuCacheDebugCmd::Free(block.address);
+ self.debug_commands.push(cmd);
+ }
+ };
+
+ (next_block, should_unlink)
+ };
+
+ // If the block was released, we will need to remove it
+ // from the occupied linked list.
+ if should_unlink {
+ match prev_block {
+ Some(prev_block) => {
+ self.blocks[prev_block.get()].next = next_block;
+ }
+ None => {
+ match next_block {
+ Some(next_block) => {
+ self.occupied_list_heads.insert(frame_stamp.document_id(), next_block);
+ }
+ None => {
+ self.occupied_list_heads.remove(&frame_stamp.document_id());
+ }
+ }
+ }
+ }
+ } else {
+ prev_block = current_block;
+ }
+
+ current_block = next_block;
+ }
+ }
+
+ /// Returns the ratio of utilized blocks.
+ fn utilization(&self) -> f32 {
+ let total_blocks = self.rows.len() * MAX_VERTEX_TEXTURE_WIDTH;
+ debug_assert!(total_blocks > 0);
+ let ratio = self.allocated_block_count as f32 / total_blocks as f32;
+ debug_assert!(0.0 <= ratio && ratio <= 1.0, "Bad ratio: {}", ratio);
+ ratio
+ }
+}
+
+
+/// A wrapper object for GPU data requests,
+/// works as a container that can only grow.
+#[must_use]
+pub struct GpuDataRequest<'a> {
+ //TODO: remove this, see
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1690546
+ #[allow(dead_code)]
+ handle: &'a mut GpuCacheHandle,
+ frame_stamp: FrameStamp,
+ start_index: usize,
+ max_block_count: usize,
+ texture: &'a mut Texture,
+}
+
+impl<'a> GpuDataRequest<'a> {
+ pub fn push<B>(&mut self, block: B)
+ where
+ B: Into<GpuBlockData>,
+ {
+ self.texture.pending_blocks.push(block.into());
+ }
+
+ // Write the GPU cache data for an individual segment.
+ pub fn write_segment(
+ &mut self,
+ local_rect: LayoutRect,
+ extra_data: [f32; 4],
+ ) {
+ let _ = VECS_PER_SEGMENT;
+ self.push(local_rect);
+ self.push(extra_data);
+ }
+
+ pub fn current_used_block_num(&self) -> usize {
+ self.texture.pending_blocks.len() - self.start_index
+ }
+}
+
+impl<'a> Drop for GpuDataRequest<'a> {
+ fn drop(&mut self) {
+ // Push the data to the texture pending updates list.
+ let block_count = self.current_used_block_num();
+ debug_assert!(block_count <= self.max_block_count);
+
+ let location = self.texture
+ .push_data(Some(self.start_index), block_count, self.frame_stamp);
+ self.handle.location = Some(location);
+ }
+}
+
+
+/// The main LRU cache interface.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct GpuCache {
+ /// Current FrameId.
+ now: FrameStamp,
+ /// CPU-side texture allocator.
+ texture: Texture,
+ /// Number of blocks requested this frame that don't
+ /// need to be re-uploaded.
+ saved_block_count: usize,
+ /// The current debug flags for the system.
+ debug_flags: DebugFlags,
+ /// Whether there is a pending clear to send with the
+ /// next update.
+ pending_clear: bool,
+ /// Indicates that prepare_for_frames has been called for this group of frames.
+ /// Used for sanity checks.
+ prepared_for_frames: bool,
+ /// This indicates that we performed a cleanup operation which requires all
+ /// documents to build a frame.
+ requires_frame_build: bool,
+ /// The set of documents which have had frames built in this update. Used for
+ /// sanity checks.
+ document_frames_to_build: FastHashSet<DocumentId>,
+}
+
+impl GpuCache {
+ pub fn new() -> Self {
+ let debug_flags = DebugFlags::empty();
+ GpuCache {
+ now: FrameStamp::INVALID,
+ texture: Texture::new(Epoch(0), debug_flags),
+ saved_block_count: 0,
+ debug_flags,
+ pending_clear: false,
+ prepared_for_frames: false,
+ requires_frame_build: false,
+ document_frames_to_build: FastHashSet::default(),
+ }
+ }
+
+ /// Creates a GpuCache and sets it up with a valid `FrameStamp`, which
+ /// is useful for avoiding panics when instantiating the `GpuCache`
+ /// directly from unit test code.
+ #[cfg(test)]
+ pub fn new_for_testing() -> Self {
+ let mut cache = Self::new();
+ let mut now = FrameStamp::first(DocumentId::new(IdNamespace(1), 1));
+ now.advance();
+ cache.prepared_for_frames = true;
+ cache.begin_frame(now);
+ cache
+ }
+
+ /// Drops everything in the GPU cache. Must not be called once gpu cache entries
+ /// for the next frame have already been requested.
+ pub fn clear(&mut self) {
+ assert!(self.texture.updates.is_empty(), "Clearing with pending updates");
+ let mut next_base_epoch = self.texture.max_epoch;
+ next_base_epoch.next();
+ self.texture = Texture::new(next_base_epoch, self.debug_flags);
+ self.saved_block_count = 0;
+ self.pending_clear = true;
+ self.requires_frame_build = true;
+ }
+
+ pub fn requires_frame_build(&self) -> bool {
+ self.requires_frame_build
+ }
+
+ pub fn prepare_for_frames(&mut self) {
+ self.prepared_for_frames = true;
+ if self.should_reclaim_memory() {
+ self.clear();
+ debug_assert!(self.document_frames_to_build.is_empty());
+ for &document_id in self.texture.occupied_list_heads.keys() {
+ self.document_frames_to_build.insert(document_id);
+ }
+ }
+ }
+
+ pub fn bookkeep_after_frames(&mut self) {
+ assert!(self.document_frames_to_build.is_empty());
+ assert!(self.prepared_for_frames);
+ self.requires_frame_build = false;
+ self.prepared_for_frames = false;
+ }
+
+ /// Begin a new frame.
+ pub fn begin_frame(&mut self, stamp: FrameStamp) {
+ debug_assert!(self.texture.pending_blocks.is_empty());
+ assert!(self.prepared_for_frames);
+ profile_scope!("begin_frame");
+ self.now = stamp;
+ self.texture.evict_old_blocks(self.now);
+ self.saved_block_count = 0;
+ }
+
+ // Invalidate a (possibly) existing block in the cache.
+ // This means the next call to request() for this location
+ // will rebuild the data and upload it to the GPU.
+ pub fn invalidate(&mut self, handle: &GpuCacheHandle) {
+ if let Some(ref location) = handle.location {
+ // don't invalidate blocks that are already re-assigned
+ if let Some(block) = self.texture.blocks.get_mut(location.block_index.get()) {
+ if block.epoch == location.epoch {
+ block.advance_epoch(&mut self.texture.max_epoch);
+ }
+ }
+ }
+ }
+
+ /// Request a resource be added to the cache. If the resource
+ /// is already in the cache, `None` will be returned.
+ pub fn request<'a>(&'a mut self, handle: &'a mut GpuCacheHandle) -> Option<GpuDataRequest<'a>> {
+ let mut max_block_count = MAX_VERTEX_TEXTURE_WIDTH;
+ // Check if the allocation for this handle is still valid.
+ if let Some(ref location) = handle.location {
+ if let Some(block) = self.texture.blocks.get_mut(location.block_index.get()) {
+ if block.epoch == location.epoch {
+ max_block_count = self.texture.rows[block.address.v as usize].block_count_per_item;
+ if block.last_access_time != self.now.frame_id() {
+ // Mark last access time to avoid evicting this block.
+ block.last_access_time = self.now.frame_id();
+ self.saved_block_count += max_block_count;
+ }
+ return None;
+ }
+ }
+ }
+
+ debug_assert!(self.now.is_valid());
+ Some(GpuDataRequest {
+ handle,
+ frame_stamp: self.now,
+ start_index: self.texture.pending_blocks.len(),
+ texture: &mut self.texture,
+ max_block_count,
+ })
+ }
+
+ // Push an array of data blocks to be uploaded to the GPU
+ // unconditionally for this frame. The cache handle will
+ // assert if the caller tries to retrieve the address
+ // of this handle on a subsequent frame. This is typically
+ // used for uploading data that changes every frame, and
+ // therefore makes no sense to try and cache.
+ pub fn push_per_frame_blocks(&mut self, blocks: &[GpuBlockData]) -> GpuCacheHandle {
+ let start_index = self.texture.pending_blocks.len();
+ self.texture.pending_blocks.extend_from_slice(blocks);
+ let location = self.texture
+ .push_data(Some(start_index), blocks.len(), self.now);
+ GpuCacheHandle {
+ location: Some(location),
+ }
+ }
+
+ // Reserve space in the cache for per-frame blocks that
+ // will be resolved by the render thread via the
+ // external image callback.
+ pub fn push_deferred_per_frame_blocks(&mut self, block_count: usize) -> GpuCacheHandle {
+ let location = self.texture.push_data(None, block_count, self.now);
+ GpuCacheHandle {
+ location: Some(location),
+ }
+ }
+
+ /// End the frame. Return the list of updates to apply to the
+ /// device specific cache texture.
+ pub fn end_frame(
+ &mut self,
+ profile: &mut TransactionProfile,
+ ) -> FrameStamp {
+ profile_scope!("end_frame");
+ profile.set(profiler::GPU_CACHE_ROWS_TOTAL, self.texture.rows.len());
+ profile.set(profiler::GPU_CACHE_BLOCKS_TOTAL, self.texture.allocated_block_count);
+ profile.set(profiler::GPU_CACHE_BLOCKS_SAVED, self.saved_block_count);
+
+ let reached_threshold =
+ self.texture.rows.len() > (GPU_CACHE_INITIAL_HEIGHT as usize) &&
+ self.texture.utilization() < RECLAIM_THRESHOLD;
+ if reached_threshold {
+ self.texture.reached_reclaim_threshold.get_or_insert_with(Instant::now);
+ } else {
+ self.texture.reached_reclaim_threshold = None;
+ }
+
+ self.document_frames_to_build.remove(&self.now.document_id());
+ self.now
+ }
+
+ /// Returns true if utilization has been low enough for long enough that we
+ /// should blow the cache away and rebuild it.
+ pub fn should_reclaim_memory(&self) -> bool {
+ self.texture.reached_reclaim_threshold
+ .map_or(false, |t| t.elapsed() > Duration::from_secs(RECLAIM_DELAY_S))
+ }
+
+ /// Extract the pending updates from the cache.
+ pub fn extract_updates(&mut self) -> GpuCacheUpdateList {
+ let clear = self.pending_clear;
+ self.pending_clear = false;
+ GpuCacheUpdateList {
+ frame_id: self.now.frame_id(),
+ clear,
+ height: self.texture.height,
+ debug_commands: self.texture.debug_commands.take_and_preallocate(),
+ updates: self.texture.updates.take_and_preallocate(),
+ blocks: self.texture.pending_blocks.take_and_preallocate(),
+ }
+ }
+
+ /// Sets the current debug flags for the system.
+ pub fn set_debug_flags(&mut self, flags: DebugFlags) {
+ self.debug_flags = flags;
+ self.texture.debug_flags = flags;
+ }
+
+ /// Get the actual GPU address in the texture for a given slot ID.
+ /// It's assumed at this point that the given slot has been requested
+ /// and built for this frame. Attempting to get the address for a
+ /// freed or pending slot will panic!
+ pub fn get_address(&self, id: &GpuCacheHandle) -> GpuCacheAddress {
+ let location = id.location.expect("handle not requested or allocated!");
+ let block = &self.texture.blocks[location.block_index.get()];
+ debug_assert_eq!(block.epoch, location.epoch);
+ debug_assert_eq!(block.last_access_time, self.now.frame_id());
+ block.address
+ }
+}
+
+#[test]
+#[cfg(target_pointer_width = "64")]
+fn test_struct_sizes() {
+ use std::mem;
+ // We can end up with a lot of blocks stored in the global vec, and keeping
+ // them small helps reduce memory overhead.
+ assert_eq!(mem::size_of::<Block>(), 24, "Block size changed");
+}
diff --git a/gfx/wr/webrender/src/gpu_types.rs b/gfx/wr/webrender/src/gpu_types.rs
new file mode 100644
index 0000000000..cc7d46e602
--- /dev/null
+++ b/gfx/wr/webrender/src/gpu_types.rs
@@ -0,0 +1,955 @@
+/* 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 api::{AlphaType, PremultipliedColorF, YuvFormat, YuvRangedColorSpace};
+use api::units::*;
+use crate::composite::CompositeFeatures;
+use crate::segment::EdgeAaSegmentMask;
+use crate::spatial_tree::{SpatialTree, SpatialNodeIndex};
+use crate::gpu_cache::{GpuCacheAddress, GpuDataRequest};
+use crate::internal_types::FastHashMap;
+use crate::prim_store::ClipData;
+use crate::render_task::RenderTaskAddress;
+use crate::render_task_graph::RenderTaskId;
+use crate::renderer::{ShaderColorMode, GpuBufferAddress};
+use std::i32;
+use crate::util::{TransformedRectKind, MatrixHelpers};
+use glyph_rasterizer::SubpixelDirection;
+use crate::util::{ScaleOffset, pack_as_float};
+
+// Contains type that must exactly match the same structures declared in GLSL.
+
+pub const VECS_PER_TRANSFORM: usize = 8;
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ZBufferId(pub i32);
+
+impl ZBufferId {
+ pub fn invalid() -> Self {
+ ZBufferId(i32::MAX)
+ }
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ZBufferIdGenerator {
+ next: i32,
+ max_depth_ids: i32,
+}
+
+impl ZBufferIdGenerator {
+ pub fn new(max_depth_ids: i32) -> Self {
+ ZBufferIdGenerator {
+ next: 0,
+ max_depth_ids,
+ }
+ }
+
+ pub fn next(&mut self) -> ZBufferId {
+ debug_assert!(self.next < self.max_depth_ids);
+ let id = ZBufferId(self.next);
+ self.next += 1;
+ id
+ }
+}
+
+#[derive(Clone, Debug)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct CopyInstance {
+ pub src_rect: DeviceRect,
+ pub dst_rect: DeviceRect,
+ pub dst_texture_size: DeviceSize,
+}
+
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(C)]
+pub enum RasterizationSpace {
+ Local = 0,
+ Screen = 1,
+}
+
+#[derive(Debug, Copy, Clone, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(C)]
+pub enum BoxShadowStretchMode {
+ Stretch = 0,
+ Simple = 1,
+}
+
+#[repr(i32)]
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum BlurDirection {
+ Horizontal = 0,
+ Vertical,
+}
+
+#[derive(Clone, Debug)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BlurInstance {
+ pub task_address: RenderTaskAddress,
+ pub src_task_address: RenderTaskAddress,
+ pub blur_direction: BlurDirection,
+}
+
+#[derive(Clone, Debug)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ScalingInstance {
+ pub target_rect: DeviceRect,
+ pub source_rect: DeviceRect,
+}
+
+#[derive(Clone, Debug)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SvgFilterInstance {
+ pub task_address: RenderTaskAddress,
+ pub input_1_task_address: RenderTaskAddress,
+ pub input_2_task_address: RenderTaskAddress,
+ pub kind: u16,
+ pub input_count: u16,
+ pub generic_int: u16,
+ pub extra_data_address: GpuCacheAddress,
+}
+
+#[derive(Copy, Clone, Debug, Hash, MallocSizeOf, PartialEq, Eq)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum BorderSegment {
+ TopLeft,
+ TopRight,
+ BottomRight,
+ BottomLeft,
+ Left,
+ Top,
+ Right,
+ Bottom,
+}
+
+#[derive(Debug, Clone)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BorderInstance {
+ pub task_origin: DevicePoint,
+ pub local_rect: DeviceRect,
+ pub color0: PremultipliedColorF,
+ pub color1: PremultipliedColorF,
+ pub flags: i32,
+ pub widths: DeviceSize,
+ pub radius: DeviceSize,
+ pub clip_params: [f32; 8],
+}
+
+#[derive(Copy, Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(C)]
+pub struct ClipMaskInstanceCommon {
+ pub sub_rect: DeviceRect,
+ pub task_origin: DevicePoint,
+ pub screen_origin: DevicePoint,
+ pub device_pixel_scale: f32,
+ pub clip_transform_id: TransformPaletteId,
+ pub prim_transform_id: TransformPaletteId,
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(C)]
+pub struct ClipMaskInstanceImage {
+ pub common: ClipMaskInstanceCommon,
+ pub tile_rect: LayoutRect,
+ pub resource_address: GpuCacheAddress,
+ pub local_rect: LayoutRect,
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(C)]
+pub struct ClipMaskInstanceRect {
+ pub common: ClipMaskInstanceCommon,
+ pub local_pos: LayoutPoint,
+ pub clip_data: ClipData,
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(C)]
+pub struct BoxShadowData {
+ pub src_rect_size: LayoutSize,
+ pub clip_mode: i32,
+ pub stretch_mode_x: i32,
+ pub stretch_mode_y: i32,
+ pub dest_rect: LayoutRect,
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(C)]
+pub struct ClipMaskInstanceBoxShadow {
+ pub common: ClipMaskInstanceCommon,
+ pub resource_address: GpuCacheAddress,
+ pub shadow_data: BoxShadowData,
+}
+
+/// A clipping primitive drawn into the clipping mask.
+/// Could be an image or a rectangle, which defines the
+/// way `address` is treated.
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(C)]
+pub struct ClipMaskInstance {
+ pub clip_transform_id: TransformPaletteId,
+ pub prim_transform_id: TransformPaletteId,
+ pub clip_data_address: GpuCacheAddress,
+ pub resource_address: GpuCacheAddress,
+ pub local_pos: LayoutPoint,
+ pub tile_rect: LayoutRect,
+ pub sub_rect: DeviceRect,
+ pub task_origin: DevicePoint,
+ pub screen_origin: DevicePoint,
+ pub device_pixel_scale: f32,
+}
+
+// 16 bytes per instance should be enough for anyone!
+#[repr(C)]
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PrimitiveInstanceData {
+ data: [i32; 4],
+}
+
+/// Specifies that an RGB CompositeInstance's UV coordinates are normalized.
+const UV_TYPE_NORMALIZED: u32 = 0;
+/// Specifies that an RGB CompositeInstance's UV coordinates are not normalized.
+const UV_TYPE_UNNORMALIZED: u32 = 1;
+
+/// A GPU-friendly representation of the `ScaleOffset` type
+#[derive(Clone, Debug)]
+#[repr(C)]
+pub struct CompositorTransform {
+ pub sx: f32,
+ pub sy: f32,
+ pub tx: f32,
+ pub ty: f32,
+}
+
+impl CompositorTransform {
+ pub fn identity() -> Self {
+ CompositorTransform {
+ sx: 1.0,
+ sy: 1.0,
+ tx: 0.0,
+ ty: 0.0,
+ }
+ }
+}
+
+impl From<ScaleOffset> for CompositorTransform {
+ fn from(scale_offset: ScaleOffset) -> Self {
+ CompositorTransform {
+ sx: scale_offset.scale.x,
+ sy: scale_offset.scale.y,
+ tx: scale_offset.offset.x,
+ ty: scale_offset.offset.y,
+ }
+ }
+}
+
+/// Vertex format for picture cache composite shader.
+/// When editing the members, update desc::COMPOSITE
+/// so its list of instance_attributes matches:
+#[derive(Clone, Debug)]
+#[repr(C)]
+pub struct CompositeInstance {
+ // Picture space destination rectangle of surface
+ rect: PictureRect,
+ // Device space destination clip rect for this surface
+ clip_rect: DeviceRect,
+ // Color for solid color tiles, white otherwise
+ color: PremultipliedColorF,
+
+ // Packed into a single vec4 (aParams)
+ _padding: f32,
+ color_space_or_uv_type: f32, // YuvColorSpace for YUV;
+ // UV coordinate space for RGB
+ yuv_format: f32, // YuvFormat
+ yuv_channel_bit_depth: f32,
+
+ // UV rectangles (pixel space) for color / yuv texture planes
+ uv_rects: [TexelRect; 3],
+
+ // A 2d scale + offset transform for the rect
+ transform: CompositorTransform,
+}
+
+impl CompositeInstance {
+ pub fn new(
+ rect: PictureRect,
+ clip_rect: DeviceRect,
+ color: PremultipliedColorF,
+ transform: CompositorTransform,
+ ) -> Self {
+ let uv = TexelRect::new(0.0, 0.0, 1.0, 1.0);
+ CompositeInstance {
+ rect,
+ clip_rect,
+ color,
+ _padding: 0.0,
+ color_space_or_uv_type: pack_as_float(UV_TYPE_NORMALIZED),
+ yuv_format: 0.0,
+ yuv_channel_bit_depth: 0.0,
+ uv_rects: [uv, uv, uv],
+ transform,
+ }
+ }
+
+ pub fn new_rgb(
+ rect: PictureRect,
+ clip_rect: DeviceRect,
+ color: PremultipliedColorF,
+ uv_rect: TexelRect,
+ transform: CompositorTransform,
+ ) -> Self {
+ CompositeInstance {
+ rect,
+ clip_rect,
+ color,
+ _padding: 0.0,
+ color_space_or_uv_type: pack_as_float(UV_TYPE_UNNORMALIZED),
+ yuv_format: 0.0,
+ yuv_channel_bit_depth: 0.0,
+ uv_rects: [uv_rect, uv_rect, uv_rect],
+ transform,
+ }
+ }
+
+ pub fn new_yuv(
+ rect: PictureRect,
+ clip_rect: DeviceRect,
+ yuv_color_space: YuvRangedColorSpace,
+ yuv_format: YuvFormat,
+ yuv_channel_bit_depth: u32,
+ uv_rects: [TexelRect; 3],
+ transform: CompositorTransform,
+ ) -> Self {
+ CompositeInstance {
+ rect,
+ clip_rect,
+ color: PremultipliedColorF::WHITE,
+ _padding: 0.0,
+ color_space_or_uv_type: pack_as_float(yuv_color_space as u32),
+ yuv_format: pack_as_float(yuv_format as u32),
+ yuv_channel_bit_depth: pack_as_float(yuv_channel_bit_depth),
+ uv_rects,
+ transform,
+ }
+ }
+
+ // Returns the CompositeFeatures that can be used to composite
+ // this RGB instance.
+ pub fn get_rgb_features(&self) -> CompositeFeatures {
+ let mut features = CompositeFeatures::empty();
+
+ // If the UV rect covers the entire texture then we can avoid UV clamping.
+ // We should try harder to determine this for unnormalized UVs too.
+ if self.color_space_or_uv_type == pack_as_float(UV_TYPE_NORMALIZED)
+ && self.uv_rects[0] == TexelRect::new(0.0, 0.0, 1.0, 1.0)
+ {
+ features |= CompositeFeatures::NO_UV_CLAMP;
+ }
+
+ if self.color == PremultipliedColorF::WHITE {
+ features |= CompositeFeatures::NO_COLOR_MODULATION
+ }
+
+ features
+ }
+}
+
+/// Vertex format for issuing colored quads.
+#[derive(Debug, Clone)]
+#[repr(C)]
+pub struct ClearInstance {
+ pub rect: [f32; 4],
+ pub color: [f32; 4],
+}
+
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PrimitiveHeaderIndex(pub i32);
+
+#[derive(Debug)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PrimitiveHeaders {
+ // The integer-type headers for a primitive.
+ pub headers_int: Vec<PrimitiveHeaderI>,
+ // The float-type headers for a primitive.
+ pub headers_float: Vec<PrimitiveHeaderF>,
+}
+
+impl PrimitiveHeaders {
+ pub fn new() -> PrimitiveHeaders {
+ PrimitiveHeaders {
+ headers_int: Vec::new(),
+ headers_float: Vec::new(),
+ }
+ }
+
+ // Add a new primitive header.
+ pub fn push(
+ &mut self,
+ prim_header: &PrimitiveHeader,
+ z: ZBufferId,
+ user_data: [i32; 4],
+ ) -> PrimitiveHeaderIndex {
+ debug_assert_eq!(self.headers_int.len(), self.headers_float.len());
+ let id = self.headers_float.len();
+
+ self.headers_float.push(PrimitiveHeaderF {
+ local_rect: prim_header.local_rect,
+ local_clip_rect: prim_header.local_clip_rect,
+ });
+
+ self.headers_int.push(PrimitiveHeaderI {
+ z,
+ unused: 0,
+ specific_prim_address: prim_header.specific_prim_address.as_int(),
+ transform_id: prim_header.transform_id,
+ user_data,
+ });
+
+ PrimitiveHeaderIndex(id as i32)
+ }
+}
+
+// This is a convenience type used to make it easier to pass
+// the common parts around during batching.
+#[derive(Debug)]
+pub struct PrimitiveHeader {
+ pub local_rect: LayoutRect,
+ pub local_clip_rect: LayoutRect,
+ pub specific_prim_address: GpuCacheAddress,
+ pub transform_id: TransformPaletteId,
+}
+
+// f32 parts of a primitive header
+#[derive(Debug)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PrimitiveHeaderF {
+ pub local_rect: LayoutRect,
+ pub local_clip_rect: LayoutRect,
+}
+
+// i32 parts of a primitive header
+// TODO(gw): Compress parts of these down to u16
+#[derive(Debug)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PrimitiveHeaderI {
+ pub z: ZBufferId,
+ pub specific_prim_address: i32,
+ pub transform_id: TransformPaletteId,
+ pub unused: i32, // To ensure required 16 byte alignment of vertex textures
+ pub user_data: [i32; 4],
+}
+
+pub struct GlyphInstance {
+ pub prim_header_index: PrimitiveHeaderIndex,
+}
+
+impl GlyphInstance {
+ pub fn new(
+ prim_header_index: PrimitiveHeaderIndex,
+ ) -> Self {
+ GlyphInstance {
+ prim_header_index,
+ }
+ }
+
+ // TODO(gw): Some of these fields can be moved to the primitive
+ // header since they are constant, and some can be
+ // compressed to a smaller size.
+ pub fn build(&self,
+ render_task: RenderTaskAddress,
+ clip_task: RenderTaskAddress,
+ subpx_dir: SubpixelDirection,
+ glyph_index_in_text_run: i32,
+ glyph_uv_rect: GpuCacheAddress,
+ color_mode: ShaderColorMode,
+ ) -> PrimitiveInstanceData {
+ PrimitiveInstanceData {
+ data: [
+ self.prim_header_index.0 as i32,
+ ((render_task.0 as i32) << 16)
+ | clip_task.0 as i32,
+ (subpx_dir as u32 as i32) << 24
+ | (color_mode as u32 as i32) << 16
+ | glyph_index_in_text_run,
+ glyph_uv_rect.as_int(),
+ ],
+ }
+ }
+}
+
+pub struct SplitCompositeInstance {
+ pub prim_header_index: PrimitiveHeaderIndex,
+ pub polygons_address: i32,
+ pub z: ZBufferId,
+ pub render_task_address: RenderTaskAddress,
+}
+
+impl From<SplitCompositeInstance> for PrimitiveInstanceData {
+ fn from(instance: SplitCompositeInstance) -> Self {
+ PrimitiveInstanceData {
+ data: [
+ instance.prim_header_index.0,
+ instance.polygons_address,
+ instance.z.0,
+ instance.render_task_address.0 as i32,
+ ],
+ }
+ }
+}
+
+#[derive(Copy, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct QuadInstance {
+ pub render_task_address: RenderTaskAddress,
+ pub prim_address: GpuBufferAddress,
+ pub z_id: ZBufferId,
+ pub transform_id: TransformPaletteId,
+ pub quad_flags: u8,
+ pub edge_flags: u8,
+ pub part_index: u8,
+ pub segment_index: u8,
+}
+
+impl From<QuadInstance> for PrimitiveInstanceData {
+ fn from(instance: QuadInstance) -> Self {
+ /*
+ [32 bits prim address]
+ [8 bits quad flags] [8 bits edge flags] [16 bits render task address]
+ [8 bits segment flags] [24 bits z_id]
+ [8 bits segment index] [24 bits xf_id]
+ */
+ PrimitiveInstanceData {
+ data: [
+ instance.prim_address.as_int(),
+ ((instance.quad_flags as i32) << 24) |
+ ((instance.edge_flags as i32) << 16) |
+ instance.render_task_address.0 as i32,
+ ((instance.part_index as i32) << 24) | instance.z_id.0,
+ ((instance.segment_index as i32) << 24) | instance.transform_id.0 as i32,
+ ],
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct QuadSegment {
+ pub rect: LayoutRect,
+ pub task_id: RenderTaskId,
+}
+
+#[repr(C)]
+#[derive(Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct MaskInstance {
+ pub prim: PrimitiveInstanceData,
+ pub clip_transform_id: TransformPaletteId,
+ pub clip_address: i32,
+ pub info: [i32; 2],
+}
+
+
+bitflags! {
+ // Note: This can use up to 12 bits due to how it will
+ // be packed in the instance data.
+
+ /// Flags that define how the common brush shader
+ /// code should process this instance.
+ #[cfg_attr(feature = "capture", derive(Serialize))]
+ #[cfg_attr(feature = "replay", derive(Deserialize))]
+ #[derive(MallocSizeOf)]
+ pub struct BrushFlags: u16 {
+ /// Apply perspective interpolation to UVs
+ const PERSPECTIVE_INTERPOLATION = 1;
+ /// Do interpolation relative to segment rect,
+ /// rather than primitive rect.
+ const SEGMENT_RELATIVE = 2;
+ /// Repeat UVs horizontally.
+ const SEGMENT_REPEAT_X = 4;
+ /// Repeat UVs vertically.
+ const SEGMENT_REPEAT_Y = 8;
+ /// Horizontally follow border-image-repeat: round.
+ const SEGMENT_REPEAT_X_ROUND = 16;
+ /// Vertically follow border-image-repeat: round.
+ const SEGMENT_REPEAT_Y_ROUND = 32;
+ /// Middle (fill) area of a border-image-repeat.
+ const SEGMENT_NINEPATCH_MIDDLE = 64;
+ /// The extra segment data is a texel rect.
+ const SEGMENT_TEXEL_RECT = 128;
+ /// Whether to force the anti-aliasing when the primitive
+ /// is axis-aligned.
+ const FORCE_AA = 256;
+ }
+}
+
+/// Convenience structure to encode into PrimitiveInstanceData.
+pub struct BrushInstance {
+ pub prim_header_index: PrimitiveHeaderIndex,
+ pub render_task_address: RenderTaskAddress,
+ pub clip_task_address: RenderTaskAddress,
+ pub segment_index: i32,
+ pub edge_flags: EdgeAaSegmentMask,
+ pub brush_flags: BrushFlags,
+ pub resource_address: i32,
+}
+
+impl From<BrushInstance> for PrimitiveInstanceData {
+ fn from(instance: BrushInstance) -> Self {
+ PrimitiveInstanceData {
+ data: [
+ instance.prim_header_index.0,
+ ((instance.render_task_address.0 as i32) << 16)
+ | instance.clip_task_address.0 as i32,
+ instance.segment_index
+ | ((instance.brush_flags.bits() as i32) << 16)
+ | ((instance.edge_flags.bits() as i32) << 28),
+ instance.resource_address,
+ ]
+ }
+ }
+}
+
+/// Convenience structure to encode into the image brush's user data.
+#[derive(Copy, Clone, Debug)]
+pub struct ImageBrushData {
+ pub color_mode: ShaderColorMode,
+ pub alpha_type: AlphaType,
+ pub raster_space: RasterizationSpace,
+ pub opacity: f32,
+}
+
+impl ImageBrushData {
+ #[inline]
+ pub fn encode(&self) -> [i32; 4] {
+ [
+ self.color_mode as i32 | ((self.alpha_type as i32) << 16),
+ self.raster_space as i32,
+ get_shader_opacity(self.opacity),
+ 0,
+ ]
+ }
+}
+
+// Represents the information about a transform palette
+// entry that is passed to shaders. It includes an index
+// into the transform palette, and a set of flags. The
+// only flag currently used determines whether the
+// transform is axis-aligned (and this should have
+// pixel snapping applied).
+#[derive(Copy, Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(C)]
+pub struct TransformPaletteId(pub u32);
+
+impl TransformPaletteId {
+ /// Identity transform ID.
+ pub const IDENTITY: Self = TransformPaletteId(0);
+
+ /// Extract the transform kind from the id.
+ pub fn transform_kind(&self) -> TransformedRectKind {
+ if (self.0 >> 23) == 0 {
+ TransformedRectKind::AxisAligned
+ } else {
+ TransformedRectKind::Complex
+ }
+ }
+
+ /// Override the kind of transform stored in this id. This can be useful in
+ /// cases where we don't want shaders to consider certain transforms axis-
+ /// aligned (i.e. perspective warp) even though we may still want to for the
+ /// general case.
+ pub fn override_transform_kind(&self, kind: TransformedRectKind) -> Self {
+ TransformPaletteId((self.0 & 0x7FFFFFu32) | ((kind as u32) << 23))
+ }
+}
+
+/// The GPU data payload for a transform palette entry.
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(C)]
+pub struct TransformData {
+ transform: LayoutToPictureTransform,
+ inv_transform: PictureToLayoutTransform,
+}
+
+impl TransformData {
+ fn invalid() -> Self {
+ TransformData {
+ transform: LayoutToPictureTransform::identity(),
+ inv_transform: PictureToLayoutTransform::identity(),
+ }
+ }
+}
+
+// Extra data stored about each transform palette entry.
+#[derive(Clone)]
+pub struct TransformMetadata {
+ transform_kind: TransformedRectKind,
+}
+
+impl TransformMetadata {
+ pub fn invalid() -> Self {
+ TransformMetadata {
+ transform_kind: TransformedRectKind::AxisAligned,
+ }
+ }
+}
+
+#[derive(Debug, Hash, Eq, PartialEq)]
+struct RelativeTransformKey {
+ from_index: SpatialNodeIndex,
+ to_index: SpatialNodeIndex,
+}
+
+// Stores a contiguous list of TransformData structs, that
+// are ready for upload to the GPU.
+// TODO(gw): For now, this only stores the complete local
+// to world transform for each spatial node. In
+// the future, the transform palette will support
+// specifying a coordinate system that the transform
+// should be relative to.
+pub struct TransformPalette {
+ transforms: Vec<TransformData>,
+ metadata: Vec<TransformMetadata>,
+ map: FastHashMap<RelativeTransformKey, usize>,
+}
+
+impl TransformPalette {
+ pub fn new(
+ count: usize,
+ ) -> Self {
+ let _ = VECS_PER_TRANSFORM;
+
+ let mut transforms = Vec::with_capacity(count);
+ let mut metadata = Vec::with_capacity(count);
+
+ transforms.push(TransformData::invalid());
+ metadata.push(TransformMetadata::invalid());
+
+ TransformPalette {
+ transforms,
+ metadata,
+ map: FastHashMap::default(),
+ }
+ }
+
+ pub fn finish(self) -> Vec<TransformData> {
+ self.transforms
+ }
+
+ fn get_index(
+ &mut self,
+ child_index: SpatialNodeIndex,
+ parent_index: SpatialNodeIndex,
+ spatial_tree: &SpatialTree,
+ ) -> usize {
+ if child_index == parent_index {
+ 0
+ } else {
+ let key = RelativeTransformKey {
+ from_index: child_index,
+ to_index: parent_index,
+ };
+
+ let metadata = &mut self.metadata;
+ let transforms = &mut self.transforms;
+
+ *self.map
+ .entry(key)
+ .or_insert_with(|| {
+ let transform = spatial_tree.get_relative_transform(
+ child_index,
+ parent_index,
+ )
+ .into_transform()
+ .with_destination::<PicturePixel>();
+
+ register_transform(
+ metadata,
+ transforms,
+ transform,
+ )
+ })
+ }
+ }
+
+ // Get a transform palette id for the given spatial node.
+ // TODO(gw): In the future, it will be possible to specify
+ // a coordinate system id here, to allow retrieving
+ // transforms in the local space of a given spatial node.
+ pub fn get_id(
+ &mut self,
+ from_index: SpatialNodeIndex,
+ to_index: SpatialNodeIndex,
+ spatial_tree: &SpatialTree,
+ ) -> TransformPaletteId {
+ let index = self.get_index(
+ from_index,
+ to_index,
+ spatial_tree,
+ );
+ let transform_kind = self.metadata[index].transform_kind as u32;
+ TransformPaletteId(
+ (index as u32) |
+ (transform_kind << 23)
+ )
+ }
+
+ pub fn get_custom(
+ &mut self,
+ transform: LayoutToPictureTransform,
+ ) -> TransformPaletteId {
+ let index = register_transform(
+ &mut self.metadata,
+ &mut self.transforms,
+ transform,
+ );
+
+ let transform_kind = self.metadata[index].transform_kind as u32;
+ TransformPaletteId(
+ (index as u32) |
+ (transform_kind << 23)
+ )
+ }
+}
+
+// Texture cache resources can be either a simple rect, or define
+// a polygon within a rect by specifying a UV coordinate for each
+// corner. This is useful for rendering screen-space rasterized
+// off-screen surfaces.
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum UvRectKind {
+ // The 2d bounds of the texture cache entry define the
+ // valid UV space for this texture cache entry.
+ Rect,
+ // The four vertices below define a quad within
+ // the texture cache entry rect. The shader can
+ // use a bilerp() to correctly interpolate a
+ // UV coord in the vertex shader.
+ Quad {
+ top_left: DeviceHomogeneousVector,
+ top_right: DeviceHomogeneousVector,
+ bottom_left: DeviceHomogeneousVector,
+ bottom_right: DeviceHomogeneousVector,
+ },
+}
+
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ImageSource {
+ pub p0: DevicePoint,
+ pub p1: DevicePoint,
+ // TODO: It appears that only glyphs make use of user_data (to store glyph offset
+ // and scale).
+ // Perhaps we should separate the two so we don't have to push an empty unused vec4
+ // for all image sources.
+ pub user_data: [f32; 4],
+ pub uv_rect_kind: UvRectKind,
+}
+
+impl ImageSource {
+ pub fn write_gpu_blocks(&self, request: &mut GpuDataRequest) {
+ // see fetch_image_resource in GLSL
+ // has to be VECS_PER_IMAGE_RESOURCE vectors
+ request.push([
+ self.p0.x,
+ self.p0.y,
+ self.p1.x,
+ self.p1.y,
+ ]);
+ request.push(self.user_data);
+
+ // If this is a polygon uv kind, then upload the four vertices.
+ if let UvRectKind::Quad { top_left, top_right, bottom_left, bottom_right } = self.uv_rect_kind {
+ // see fetch_image_resource_extra in GLSL
+ //Note: we really need only 3 components per point here: X, Y, and W
+ request.push(top_left);
+ request.push(top_right);
+ request.push(bottom_left);
+ request.push(bottom_right);
+ }
+ }
+}
+
+// Set the local -> world transform for a given spatial
+// node in the transform palette.
+fn register_transform(
+ metadatas: &mut Vec<TransformMetadata>,
+ transforms: &mut Vec<TransformData>,
+ transform: LayoutToPictureTransform,
+) -> usize {
+ // TODO: refactor the calling code to not even try
+ // registering a non-invertible transform.
+ let inv_transform = transform
+ .inverse()
+ .unwrap_or_else(PictureToLayoutTransform::identity);
+
+ let metadata = TransformMetadata {
+ transform_kind: transform.transform_kind()
+ };
+ let data = TransformData {
+ transform,
+ inv_transform,
+ };
+
+ let index = transforms.len();
+ metadatas.push(metadata);
+ transforms.push(data);
+
+ index
+}
+
+pub fn get_shader_opacity(opacity: f32) -> i32 {
+ (opacity * 65535.0).round() as i32
+}
diff --git a/gfx/wr/webrender/src/hit_test.rs b/gfx/wr/webrender/src/hit_test.rs
new file mode 100644
index 0000000000..34a7e7e404
--- /dev/null
+++ b/gfx/wr/webrender/src/hit_test.rs
@@ -0,0 +1,420 @@
+/* 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 api::{BorderRadius, ClipMode, HitTestResultItem, HitTestResult, ItemTag, PrimitiveFlags};
+use api::{PipelineId, ApiHitTester};
+use api::units::*;
+use crate::clip::{rounded_rectangle_contains_point, ClipNodeId, ClipTreeBuilder};
+use crate::clip::{polygon_contains_point, ClipItemKey, ClipItemKeyKind};
+use crate::prim_store::PolygonKey;
+use crate::scene_builder_thread::Interners;
+use crate::spatial_tree::{SpatialNodeIndex, SpatialTree, get_external_scroll_offset};
+use crate::internal_types::{FastHashMap, LayoutPrimitiveInfo};
+use std::sync::{Arc, Mutex};
+use crate::util::{LayoutToWorldFastTransform};
+
+pub struct SharedHitTester {
+ // We don't really need a mutex here. We could do with some sort of
+ // atomic-atomic-ref-counted pointer (an Arc which would let the pointer
+ // be swapped atomically like an AtomicPtr).
+ // In practive this shouldn't cause performance issues, though.
+ hit_tester: Mutex<Arc<HitTester>>,
+}
+
+impl SharedHitTester {
+ pub fn new() -> Self {
+ SharedHitTester {
+ hit_tester: Mutex::new(Arc::new(HitTester::empty())),
+ }
+ }
+
+ pub fn get_ref(&self) -> Arc<HitTester> {
+ let guard = self.hit_tester.lock().unwrap();
+ Arc::clone(&*guard)
+ }
+
+ pub(crate) fn update(&self, new_hit_tester: Arc<HitTester>) {
+ let mut guard = self.hit_tester.lock().unwrap();
+ *guard = new_hit_tester;
+ }
+}
+
+impl ApiHitTester for SharedHitTester {
+ fn hit_test(&self,
+ point: WorldPoint,
+ ) -> HitTestResult {
+ self.get_ref().hit_test(HitTest::new(point))
+ }
+}
+
+/// A copy of important spatial node data to use during hit testing. This a copy of
+/// data from the SpatialTree that will persist as a new frame is under construction,
+/// allowing hit tests consistent with the currently rendered frame.
+#[derive(MallocSizeOf)]
+struct HitTestSpatialNode {
+ /// The pipeline id of this node.
+ pipeline_id: PipelineId,
+
+ /// World transform for content transformed by this node.
+ world_content_transform: LayoutToWorldFastTransform,
+
+ /// World viewport transform for content transformed by this node.
+ world_viewport_transform: LayoutToWorldFastTransform,
+
+ /// The accumulated external scroll offset for this spatial node.
+ external_scroll_offset: LayoutVector2D,
+}
+
+#[derive(MallocSizeOf)]
+struct HitTestClipNode {
+ /// A particular point must be inside all of these regions to be considered clipped in
+ /// for the purposes of a hit test.
+ region: HitTestRegion,
+ /// The positioning node for this clip
+ spatial_node_index: SpatialNodeIndex,
+ /// Parent clip node
+ parent: ClipNodeId,
+}
+
+impl HitTestClipNode {
+ fn new(
+ item: &ClipItemKey,
+ interners: &Interners,
+ parent: ClipNodeId,
+ ) -> Self {
+ let region = match item.kind {
+ ClipItemKeyKind::Rectangle(rect, mode) => {
+ HitTestRegion::Rectangle(rect.into(), mode)
+ }
+ ClipItemKeyKind::RoundedRectangle(rect, radius, mode) => {
+ HitTestRegion::RoundedRectangle(rect.into(), radius.into(), mode)
+ }
+ ClipItemKeyKind::ImageMask(rect, _, polygon_handle) => {
+ if let Some(handle) = polygon_handle {
+ // Retrieve the polygon data from the interner.
+ let polygon = &interners.polygon[handle];
+ HitTestRegion::Polygon(rect.into(), *polygon)
+ } else {
+ HitTestRegion::Rectangle(rect.into(), ClipMode::Clip)
+ }
+ }
+ ClipItemKeyKind::BoxShadow(..) => HitTestRegion::Invalid,
+ };
+
+ HitTestClipNode {
+ region,
+ spatial_node_index: item.spatial_node_index,
+ parent,
+ }
+ }
+}
+
+#[derive(Clone, MallocSizeOf)]
+struct HitTestingItem {
+ rect: LayoutRect,
+ tag: ItemTag,
+ animation_id: u64,
+ is_backface_visible: bool,
+ spatial_node_index: SpatialNodeIndex,
+ clip_node_id: ClipNodeId,
+}
+
+impl HitTestingItem {
+ fn new(
+ tag: ItemTag,
+ animation_id: u64,
+ info: &LayoutPrimitiveInfo,
+ spatial_node_index: SpatialNodeIndex,
+ clip_node_id: ClipNodeId,
+ ) -> HitTestingItem {
+ HitTestingItem {
+ rect: info.rect,
+ tag,
+ animation_id,
+ is_backface_visible: info.flags.contains(PrimitiveFlags::IS_BACKFACE_VISIBLE),
+ spatial_node_index,
+ clip_node_id,
+ }
+ }
+}
+
+/// Statistics about allocation sizes of current hit tester,
+/// used to pre-allocate size of the next hit tester.
+pub struct HitTestingSceneStats {
+ pub clip_nodes_count: usize,
+ pub items_count: usize,
+}
+
+impl HitTestingSceneStats {
+ pub fn empty() -> Self {
+ HitTestingSceneStats {
+ clip_nodes_count: 0,
+ items_count: 0,
+ }
+ }
+}
+
+#[derive(MallocSizeOf, Debug, Copy, Clone)]
+pub struct ClipNodeIndex(u32);
+
+/// Defines the immutable part of a hit tester for a given scene.
+/// The hit tester is recreated each time a frame is built, since
+/// it relies on the current values of the spatial tree.
+/// However, the clip chain and item definitions don't change,
+/// so they are created once per scene, and shared between
+/// hit tester instances via Arc.
+#[derive(MallocSizeOf)]
+pub struct HitTestingScene {
+ clip_nodes: FastHashMap<ClipNodeId, HitTestClipNode>,
+
+ /// List of hit testing primitives.
+ items: Vec<HitTestingItem>,
+}
+
+impl HitTestingScene {
+ /// Construct a new hit testing scene, pre-allocating to size
+ /// provided by previous scene stats.
+ pub fn new(stats: &HitTestingSceneStats) -> Self {
+ HitTestingScene {
+ clip_nodes: FastHashMap::default(),
+ items: Vec::with_capacity(stats.items_count),
+ }
+ }
+
+ /// Get stats about the current scene allocation sizes.
+ pub fn get_stats(&self) -> HitTestingSceneStats {
+ HitTestingSceneStats {
+ clip_nodes_count: 0,
+ items_count: self.items.len(),
+ }
+ }
+
+ fn add_clip_node(
+ &mut self,
+ clip_node_id: ClipNodeId,
+ clip_tree_builder: &ClipTreeBuilder,
+ interners: &Interners,
+ ) {
+ if clip_node_id == ClipNodeId::NONE {
+ return;
+ }
+
+ if !self.clip_nodes.contains_key(&clip_node_id) {
+ let src_clip_node = clip_tree_builder.get_node(clip_node_id);
+ let clip_item = &interners.clip[src_clip_node.handle];
+
+ let clip_node = HitTestClipNode::new(
+ &clip_item.key,
+ interners,
+ src_clip_node.parent,
+ );
+
+ self.clip_nodes.insert(clip_node_id, clip_node);
+
+ self.add_clip_node(
+ src_clip_node.parent,
+ clip_tree_builder,
+ interners,
+ );
+ }
+ }
+
+ /// Add a hit testing primitive.
+ pub fn add_item(
+ &mut self,
+ tag: ItemTag,
+ anim_id: u64,
+ info: &LayoutPrimitiveInfo,
+ spatial_node_index: SpatialNodeIndex,
+ clip_node_id: ClipNodeId,
+ clip_tree_builder: &ClipTreeBuilder,
+ interners: &Interners,
+ ) {
+ self.add_clip_node(
+ clip_node_id,
+ clip_tree_builder,
+ interners,
+ );
+
+ let item = HitTestingItem::new(
+ tag,
+ anim_id,
+ info,
+ spatial_node_index,
+ clip_node_id,
+ );
+
+ self.items.push(item);
+ }
+}
+
+#[derive(MallocSizeOf)]
+enum HitTestRegion {
+ Invalid,
+ Rectangle(LayoutRect, ClipMode),
+ RoundedRectangle(LayoutRect, BorderRadius, ClipMode),
+ Polygon(LayoutRect, PolygonKey),
+}
+
+impl HitTestRegion {
+ fn contains(&self, point: &LayoutPoint) -> bool {
+ match *self {
+ HitTestRegion::Rectangle(ref rectangle, ClipMode::Clip) =>
+ rectangle.contains(*point),
+ HitTestRegion::Rectangle(ref rectangle, ClipMode::ClipOut) =>
+ !rectangle.contains(*point),
+ HitTestRegion::RoundedRectangle(rect, radii, ClipMode::Clip) =>
+ rounded_rectangle_contains_point(point, &rect, &radii),
+ HitTestRegion::RoundedRectangle(rect, radii, ClipMode::ClipOut) =>
+ !rounded_rectangle_contains_point(point, &rect, &radii),
+ HitTestRegion::Polygon(rect, polygon) =>
+ polygon_contains_point(point, &rect, &polygon),
+ HitTestRegion::Invalid => true,
+ }
+ }
+}
+
+#[derive(MallocSizeOf)]
+pub struct HitTester {
+ #[ignore_malloc_size_of = "Arc"]
+ scene: Arc<HitTestingScene>,
+ spatial_nodes: FastHashMap<SpatialNodeIndex, HitTestSpatialNode>,
+}
+
+impl HitTester {
+ pub fn empty() -> Self {
+ HitTester {
+ scene: Arc::new(HitTestingScene::new(&HitTestingSceneStats::empty())),
+ spatial_nodes: FastHashMap::default(),
+ }
+ }
+
+ pub fn new(
+ scene: Arc<HitTestingScene>,
+ spatial_tree: &SpatialTree,
+ ) -> HitTester {
+ let mut hit_tester = HitTester {
+ scene,
+ spatial_nodes: FastHashMap::default(),
+ };
+ hit_tester.read_spatial_tree(spatial_tree);
+ hit_tester
+ }
+
+ fn read_spatial_tree(
+ &mut self,
+ spatial_tree: &SpatialTree,
+ ) {
+ self.spatial_nodes.clear();
+ self.spatial_nodes.reserve(spatial_tree.spatial_node_count());
+
+ spatial_tree.visit_nodes(|index, node| {
+ //TODO: avoid inverting more than necessary:
+ // - if the coordinate system is non-invertible, no need to try any of these concrete transforms
+ // - if there are other places where inversion is needed, let's not repeat the step
+
+ self.spatial_nodes.insert(index, HitTestSpatialNode {
+ pipeline_id: node.pipeline_id,
+ world_content_transform: spatial_tree
+ .get_world_transform(index)
+ .into_fast_transform(),
+ world_viewport_transform: spatial_tree
+ .get_world_viewport_transform(index)
+ .into_fast_transform(),
+ external_scroll_offset: get_external_scroll_offset(spatial_tree, index),
+ });
+ });
+ }
+
+ pub fn hit_test(&self, test: HitTest) -> HitTestResult {
+ let mut result = HitTestResult::default();
+
+ let mut current_spatial_node_index = SpatialNodeIndex::INVALID;
+ let mut point_in_layer = None;
+
+ // For each hit test primitive
+ for item in self.scene.items.iter().rev() {
+ let scroll_node = &self.spatial_nodes[&item.spatial_node_index];
+ let pipeline_id = scroll_node.pipeline_id;
+
+ // Update the cached point in layer space, if the spatial node
+ // changed since last primitive.
+ if item.spatial_node_index != current_spatial_node_index {
+ point_in_layer = scroll_node
+ .world_content_transform
+ .inverse()
+ .and_then(|inverted| inverted.project_point2d(test.point));
+ current_spatial_node_index = item.spatial_node_index;
+ }
+
+ // Only consider hit tests on transformable layers.
+ let point_in_layer = match point_in_layer {
+ Some(p) => p,
+ None => continue,
+ };
+
+ // If the item's rect or clip rect don't contain this point, it's
+ // not a valid hit.
+ if !item.rect.contains(point_in_layer) {
+ continue;
+ }
+
+ // See if any of the clips for this primitive cull out the item.
+ let mut current_clip_node_id = item.clip_node_id;
+ let mut is_valid = true;
+
+ while current_clip_node_id != ClipNodeId::NONE {
+ let clip_node = &self.scene.clip_nodes[&current_clip_node_id];
+
+ let transform = self
+ .spatial_nodes[&clip_node.spatial_node_index]
+ .world_content_transform;
+ if let Some(transformed_point) = transform
+ .inverse()
+ .and_then(|inverted| inverted.project_point2d(test.point))
+ {
+ if !clip_node.region.contains(&transformed_point) {
+ is_valid = false;
+ break;
+ }
+ }
+
+ current_clip_node_id = clip_node.parent;
+ }
+
+ if !is_valid {
+ continue;
+ }
+
+ // Don't hit items with backface-visibility:hidden if they are facing the back.
+ if !item.is_backface_visible && scroll_node.world_content_transform.is_backface_visible() {
+ continue;
+ }
+
+ result.items.push(HitTestResultItem {
+ pipeline: pipeline_id,
+ tag: item.tag,
+ animation_id: item.animation_id,
+ });
+ }
+
+ result.items.dedup();
+ result
+ }
+}
+
+#[derive(MallocSizeOf)]
+pub struct HitTest {
+ point: WorldPoint,
+}
+
+impl HitTest {
+ pub fn new(
+ point: WorldPoint,
+ ) -> HitTest {
+ HitTest {
+ point,
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/image_source.rs b/gfx/wr/webrender/src/image_source.rs
new file mode 100644
index 0000000000..c2223ab6f1
--- /dev/null
+++ b/gfx/wr/webrender/src/image_source.rs
@@ -0,0 +1,93 @@
+/* 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/. */
+
+//! This module contains the logic to obtain a primitive's source texture and uv rect.
+//!
+//! Currently this is a somewhat involved process because the code grew into having ad-hoc
+//! ways to store this information depending on how the image data is produced. The goal
+//! is for any textured primitive to be able to read from any source (texture cache, render
+//! tasks, etc.) without primitive-specific code.
+
+use crate::api::ExternalImageType;
+use crate::api::units::*;
+use crate::gpu_cache::GpuCache;
+use crate::prim_store::DeferredResolve;
+use crate::renderer::BLOCKS_PER_UV_RECT;
+use crate::render_task_cache::RenderTaskCacheEntryHandle;
+use crate::resource_cache::{ResourceCache, ImageRequest, CacheItem};
+use crate::internal_types::{TextureSource, DeferredResolveIndex};
+
+/// Resolve a resource cache's imagre request into a texture cache item.
+pub fn resolve_image(
+ request: ImageRequest,
+ resource_cache: &ResourceCache,
+ gpu_cache: &mut GpuCache,
+ deferred_resolves: &mut Vec<DeferredResolve>,
+) -> CacheItem {
+ match resource_cache.get_image_properties(request.key) {
+ Some(image_properties) => {
+ // Check if an external image that needs to be resolved
+ // by the render thread.
+ match image_properties.external_image {
+ Some(external_image) => {
+ // This is an external texture - we will add it to
+ // the deferred resolves list to be patched by
+ // the render thread...
+ let cache_handle = gpu_cache.push_deferred_per_frame_blocks(BLOCKS_PER_UV_RECT);
+
+ let deferred_resolve_index = DeferredResolveIndex(deferred_resolves.len() as u32);
+
+ let image_buffer_kind = match external_image.image_type {
+ ExternalImageType::TextureHandle(target) => {
+ target
+ }
+ ExternalImageType::Buffer => {
+ // The ExternalImageType::Buffer should be handled by resource_cache.
+ // It should go through the non-external case.
+ panic!("Unexpected non-texture handle type");
+ }
+ };
+
+ let cache_item = CacheItem {
+ texture_id: TextureSource::External(deferred_resolve_index, image_buffer_kind),
+ uv_rect_handle: cache_handle,
+ uv_rect: DeviceIntRect::from_size(
+ image_properties.descriptor.size,
+ ),
+ user_data: [0.0; 4],
+ };
+
+ deferred_resolves.push(DeferredResolve {
+ image_properties,
+ address: gpu_cache.get_address(&cache_handle),
+ rendering: request.rendering,
+ });
+
+ cache_item
+ }
+ None => {
+ if let Ok(cache_item) = resource_cache.get_cached_image(request) {
+ cache_item
+ } else {
+ // There is no usable texture entry for the image key. Just return an invalid texture here.
+ CacheItem::invalid()
+ }
+ }
+ }
+ }
+ None => {
+ CacheItem::invalid()
+ }
+ }
+}
+
+pub fn resolve_cached_render_task(
+ handle: &RenderTaskCacheEntryHandle,
+ resource_cache: &ResourceCache,
+) -> CacheItem {
+ let rt_cache_entry = resource_cache
+ .get_cached_render_task(&handle);
+
+ resource_cache.get_texture_cache_item(&rt_cache_entry.handle)
+}
diff --git a/gfx/wr/webrender/src/image_tiling.rs b/gfx/wr/webrender/src/image_tiling.rs
new file mode 100644
index 0000000000..e2fb3b05b9
--- /dev/null
+++ b/gfx/wr/webrender/src/image_tiling.rs
@@ -0,0 +1,823 @@
+/* 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::api::TileSize;
+use crate::api::units::*;
+use crate::segment::EdgeAaSegmentMask;
+use euclid::{point2, size2};
+use std::i32;
+use std::ops::Range;
+
+/// If repetitions are far enough apart that only one is within
+/// the primitive rect, then we can simplify the parameters and
+/// treat the primitive as not repeated.
+/// This can let us avoid unnecessary work later to handle some
+/// of the parameters.
+pub fn simplify_repeated_primitive(
+ stretch_size: &LayoutSize,
+ tile_spacing: &mut LayoutSize,
+ prim_rect: &mut LayoutRect,
+) {
+ let stride = *stretch_size + *tile_spacing;
+
+ if stride.width >= prim_rect.width() {
+ tile_spacing.width = 0.0;
+ prim_rect.max.x = f32::min(prim_rect.min.x + stretch_size.width, prim_rect.max.x);
+ }
+ if stride.height >= prim_rect.height() {
+ tile_spacing.height = 0.0;
+ prim_rect.max.y = f32::min(prim_rect.min.y + stretch_size.height, prim_rect.max.y);
+ }
+}
+
+pub struct Repetition {
+ pub origin: LayoutPoint,
+ pub edge_flags: EdgeAaSegmentMask,
+}
+
+pub struct RepetitionIterator {
+ current_x: i32,
+ x_count: i32,
+ current_y: i32,
+ y_count: i32,
+ row_flags: EdgeAaSegmentMask,
+ current_origin: LayoutPoint,
+ initial_origin: LayoutPoint,
+ stride: LayoutSize,
+}
+
+impl RepetitionIterator {
+ pub fn num_repetitions(&self) -> usize {
+ (self.y_count * self.x_count) as usize
+ }
+}
+
+impl Iterator for RepetitionIterator {
+ type Item = Repetition;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.current_x == self.x_count {
+ self.current_y += 1;
+ if self.current_y >= self.y_count {
+ return None;
+ }
+ self.current_x = 0;
+
+ self.row_flags = EdgeAaSegmentMask::empty();
+ if self.current_y == self.y_count - 1 {
+ self.row_flags |= EdgeAaSegmentMask::BOTTOM;
+ }
+
+ self.current_origin.x = self.initial_origin.x;
+ self.current_origin.y += self.stride.height;
+ }
+
+ let mut edge_flags = self.row_flags;
+ if self.current_x == 0 {
+ edge_flags |= EdgeAaSegmentMask::LEFT;
+ }
+
+ if self.current_x == self.x_count - 1 {
+ edge_flags |= EdgeAaSegmentMask::RIGHT;
+ }
+
+ let repetition = Repetition {
+ origin: self.current_origin,
+ edge_flags,
+ };
+
+ self.current_origin.x += self.stride.width;
+ self.current_x += 1;
+
+ Some(repetition)
+ }
+}
+
+pub fn repetitions(
+ prim_rect: &LayoutRect,
+ visible_rect: &LayoutRect,
+ stride: LayoutSize,
+) -> RepetitionIterator {
+ let visible_rect = match prim_rect.intersection(&visible_rect) {
+ Some(rect) => rect,
+ None => {
+ return RepetitionIterator {
+ current_origin: LayoutPoint::zero(),
+ initial_origin: LayoutPoint::zero(),
+ current_x: 0,
+ current_y: 0,
+ x_count: 0,
+ y_count: 0,
+ stride,
+ row_flags: EdgeAaSegmentMask::empty(),
+ }
+ }
+ };
+
+ assert!(stride.width > 0.0);
+ assert!(stride.height > 0.0);
+
+ let nx = if visible_rect.min.x > prim_rect.min.x {
+ f32::floor((visible_rect.min.x - prim_rect.min.x) / stride.width)
+ } else {
+ 0.0
+ };
+
+ let ny = if visible_rect.min.y > prim_rect.min.y {
+ f32::floor((visible_rect.min.y - prim_rect.min.y) / stride.height)
+ } else {
+ 0.0
+ };
+
+ let x0 = prim_rect.min.x + nx * stride.width;
+ let y0 = prim_rect.min.y + ny * stride.height;
+
+ let x_most = visible_rect.max.x;
+ let y_most = visible_rect.max.y;
+
+ let x_count = f32::ceil((x_most - x0) / stride.width) as i32;
+ let y_count = f32::ceil((y_most - y0) / stride.height) as i32;
+
+ let mut row_flags = EdgeAaSegmentMask::TOP;
+ if y_count == 1 {
+ row_flags |= EdgeAaSegmentMask::BOTTOM;
+ }
+
+ RepetitionIterator {
+ current_origin: LayoutPoint::new(x0, y0),
+ initial_origin: LayoutPoint::new(x0, y0),
+ current_x: 0,
+ current_y: 0,
+ x_count,
+ y_count,
+ row_flags,
+ stride,
+ }
+}
+
+#[derive(Debug)]
+pub struct Tile {
+ pub rect: LayoutRect,
+ pub offset: TileOffset,
+ pub edge_flags: EdgeAaSegmentMask,
+}
+
+#[derive(Debug)]
+pub struct TileIteratorExtent {
+ /// Range of visible tiles to iterate over in number of tiles.
+ tile_range: Range<i32>,
+ /// Range of tiles of the full image including tiles that are culled out.
+ image_tiles: Range<i32>,
+ /// Size of the first tile in layout space.
+ first_tile_layout_size: f32,
+ /// Size of the last tile in layout space.
+ last_tile_layout_size: f32,
+ /// Position of blob point (0, 0) in layout space.
+ layout_tiling_origin: f32,
+ /// Position of the top-left corner of the primitive rect in layout space.
+ layout_prim_start: f32,
+}
+
+#[derive(Debug)]
+pub struct TileIterator {
+ current_tile: TileOffset,
+ x: TileIteratorExtent,
+ y: TileIteratorExtent,
+ regular_tile_size: LayoutSize,
+}
+
+impl Iterator for TileIterator {
+ type Item = Tile;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ // If we reach the end of a row, reset to the beginning of the next row.
+ if self.current_tile.x >= self.x.tile_range.end {
+ self.current_tile.y += 1;
+ self.current_tile.x = self.x.tile_range.start;
+ }
+
+ // Stop iterating if we reach the last tile. We may start here if there
+ // were no tiles to iterate over.
+ if self.current_tile.x >= self.x.tile_range.end || self.current_tile.y >= self.y.tile_range.end {
+ return None;
+ }
+
+ let tile_offset = self.current_tile;
+
+ let mut segment_rect = LayoutRect::from_origin_and_size(
+ LayoutPoint::new(
+ self.x.layout_tiling_origin + tile_offset.x as f32 * self.regular_tile_size.width,
+ self.y.layout_tiling_origin + tile_offset.y as f32 * self.regular_tile_size.height,
+ ),
+ self.regular_tile_size,
+ );
+
+ let mut edge_flags = EdgeAaSegmentMask::empty();
+
+ if tile_offset.x == self.x.image_tiles.start {
+ edge_flags |= EdgeAaSegmentMask::LEFT;
+ segment_rect.min.x = self.x.layout_prim_start;
+ // TODO(nical) we may not need to do this.
+ segment_rect.max.x = segment_rect.min.x + self.x.first_tile_layout_size;
+ }
+ if tile_offset.x == self.x.image_tiles.end - 1 {
+ edge_flags |= EdgeAaSegmentMask::RIGHT;
+ segment_rect.max.x = segment_rect.min.x + self.x.last_tile_layout_size;
+ }
+
+ if tile_offset.y == self.y.image_tiles.start {
+ segment_rect.min.y = self.y.layout_prim_start;
+ segment_rect.max.y = segment_rect.min.y + self.y.first_tile_layout_size;
+ edge_flags |= EdgeAaSegmentMask::TOP;
+ }
+ if tile_offset.y == self.y.image_tiles.end - 1 {
+ segment_rect.max.y = segment_rect.min.y + self.y.last_tile_layout_size;
+ edge_flags |= EdgeAaSegmentMask::BOTTOM;
+ }
+
+ assert!(tile_offset.y < self.y.tile_range.end);
+ let tile = Tile {
+ rect: segment_rect,
+ offset: tile_offset,
+ edge_flags,
+ };
+
+ self.current_tile.x += 1;
+
+ Some(tile)
+ }
+}
+
+pub fn tiles(
+ prim_rect: &LayoutRect,
+ visible_rect: &LayoutRect,
+ image_rect: &DeviceIntRect,
+ device_tile_size: i32,
+) -> TileIterator {
+ // The image resource is tiled. We have to generate an image primitive
+ // for each tile.
+ // We need to do this because the image is broken up into smaller tiles in the texture
+ // cache and the image shader is not able to work with this type of sparse representation.
+
+ // The tiling logic works as follows:
+ //
+ // +-#################-+ -+
+ // | #//| | |//# | | image size
+ // | #//| | |//# | |
+ // +-#--+----+----+--#-+ | -+
+ // | #//| | |//# | | | regular tile size
+ // | #//| | |//# | | |
+ // +-#--+----+----+--#-+ | -+-+
+ // | #//|////|////|//# | | | "leftover" height
+ // | ################# | -+ ---+
+ // +----+----+----+----+
+ //
+ // In the ascii diagram above, a large image is split into tiles of almost regular size.
+ // The tiles on the edges (hatched in the diagram) can be smaller than the regular tiles
+ // and are handled separately in the code (we'll call them boundary tiles).
+ //
+ // Each generated segment corresponds to a tile in the texture cache, with the
+ // assumption that the boundary tiles are sized to fit their own irregular size in the
+ // texture cache.
+ //
+ // Because we can have very large virtual images we iterate over the visible portion of
+ // the image in layer space instead of iterating over all device tiles.
+
+ let visible_rect = match prim_rect.intersection(&visible_rect) {
+ Some(rect) => rect,
+ None => {
+ return TileIterator {
+ current_tile: TileOffset::zero(),
+ x: TileIteratorExtent {
+ tile_range: 0..0,
+ image_tiles: 0..0,
+ first_tile_layout_size: 0.0,
+ last_tile_layout_size: 0.0,
+ layout_tiling_origin: 0.0,
+ layout_prim_start: prim_rect.min.x,
+ },
+ y: TileIteratorExtent {
+ tile_range: 0..0,
+ image_tiles: 0..0,
+ first_tile_layout_size: 0.0,
+ last_tile_layout_size: 0.0,
+ layout_tiling_origin: 0.0,
+ layout_prim_start: prim_rect.min.y,
+ },
+ regular_tile_size: LayoutSize::zero(),
+ }
+ }
+ };
+
+ // Size of regular tiles in layout space.
+ let layout_tile_size = LayoutSize::new(
+ device_tile_size as f32 / image_rect.width() as f32 * prim_rect.width(),
+ device_tile_size as f32 / image_rect.height() as f32 * prim_rect.height(),
+ );
+
+ // The decomposition logic is exactly the same on each axis so we reduce
+ // this to a 1-dimensional problem in an attempt to make the code simpler.
+
+ let x_extent = tiles_1d(
+ layout_tile_size.width,
+ visible_rect.x_range(),
+ prim_rect.min.x,
+ image_rect.x_range(),
+ device_tile_size,
+ );
+
+ let y_extent = tiles_1d(
+ layout_tile_size.height,
+ visible_rect.y_range(),
+ prim_rect.min.y,
+ image_rect.y_range(),
+ device_tile_size,
+ );
+
+ TileIterator {
+ current_tile: point2(
+ x_extent.tile_range.start,
+ y_extent.tile_range.start,
+ ),
+ x: x_extent,
+ y: y_extent,
+ regular_tile_size: layout_tile_size,
+ }
+}
+
+/// Decompose tiles along an arbitrary axis.
+///
+/// This does most of the heavy lifting needed for `tiles` but in a single dimension for
+/// the sake of simplicity since the problem is independent on the x and y axes.
+fn tiles_1d(
+ layout_tile_size: f32,
+ layout_visible_range: Range<f32>,
+ layout_prim_start: f32,
+ device_image_range: Range<i32>,
+ device_tile_size: i32,
+) -> TileIteratorExtent {
+ // A few sanity checks.
+ debug_assert!(layout_tile_size > 0.0);
+ debug_assert!(layout_visible_range.end >= layout_visible_range.start);
+ debug_assert!(device_image_range.end > device_image_range.start);
+ debug_assert!(device_tile_size > 0);
+
+ // Sizes of the boundary tiles in pixels.
+ let first_tile_device_size = first_tile_size_1d(&device_image_range, device_tile_size);
+ let last_tile_device_size = last_tile_size_1d(&device_image_range, device_tile_size);
+
+ // [start..end[ Range of tiles of this row/column (in number of tiles) without
+ // taking culling into account.
+ let image_tiles = tile_range_1d(&device_image_range, device_tile_size);
+
+ // Layout offset of tile (0, 0) with respect to the top-left corner of the display item.
+ let layout_offset = device_image_range.start as f32 * layout_tile_size / device_tile_size as f32;
+ // Position in layout space of tile (0, 0).
+ let layout_tiling_origin = layout_prim_start - layout_offset;
+
+ // [start..end[ Range of the visible tiles (because of culling).
+ let visible_tiles_start = f32::floor((layout_visible_range.start - layout_tiling_origin) / layout_tile_size) as i32;
+ let visible_tiles_end = f32::ceil((layout_visible_range.end - layout_tiling_origin) / layout_tile_size) as i32;
+
+ // Combine the above two to get the tiles in the image that are visible this frame.
+ let mut tiles_start = i32::max(image_tiles.start, visible_tiles_start);
+ let tiles_end = i32::min(image_tiles.end, visible_tiles_end);
+ if tiles_start > tiles_end {
+ tiles_start = tiles_end;
+ }
+
+ // The size in layout space of the boundary tiles.
+ let first_tile_layout_size = if tiles_start == image_tiles.start {
+ first_tile_device_size as f32 * layout_tile_size / device_tile_size as f32
+ } else {
+ // boundary tile was culled out, so the new first tile is a regularly sized tile.
+ layout_tile_size
+ };
+
+ // Same here.
+ let last_tile_layout_size = if tiles_end == image_tiles.end {
+ last_tile_device_size as f32 * layout_tile_size / device_tile_size as f32
+ } else {
+ layout_tile_size
+ };
+
+ TileIteratorExtent {
+ tile_range: tiles_start..tiles_end,
+ image_tiles,
+ first_tile_layout_size,
+ last_tile_layout_size,
+ layout_tiling_origin,
+ layout_prim_start,
+ }
+}
+
+/// Compute the range of tiles (in number of tiles) that intersect the provided
+/// image range (in pixels) in an arbitrary dimension.
+///
+/// ```ignore
+///
+/// 0
+/// :
+/// #-+---+---+---+---+---+--#
+/// # | | | | | | #
+/// #-+---+---+---+---+---+--#
+/// ^ : ^
+///
+/// +------------------------+ image_range
+/// +---+ regular_tile_size
+///
+/// ```
+fn tile_range_1d(
+ image_range: &Range<i32>,
+ regular_tile_size: i32,
+) -> Range<i32> {
+ // Integer division truncates towards zero so with negative values if the first/last
+ // tile isn't a full tile we can get offset by one which we account for here.
+
+ let mut start = image_range.start / regular_tile_size;
+ if image_range.start % regular_tile_size < 0 {
+ start -= 1;
+ }
+
+ let mut end = image_range.end / regular_tile_size;
+ if image_range.end % regular_tile_size > 0 {
+ end += 1;
+ }
+
+ start..end
+}
+
+// Sizes of the first boundary tile in pixels.
+//
+// It can be smaller than the regular tile size if the image is not a multiple
+// of the regular tile size.
+fn first_tile_size_1d(
+ image_range: &Range<i32>,
+ regular_tile_size: i32,
+) -> i32 {
+ // We have to account for how the % operation behaves for negative values.
+ let image_size = image_range.end - image_range.start;
+ i32::min(
+ match image_range.start % regular_tile_size {
+ // . #------+------+ .
+ // . #//////| | .
+ 0 => regular_tile_size,
+ // (zero) -> 0 . #--+------+ .
+ // . . #//| | .
+ // %(m): ~~>
+ m if m > 0 => regular_tile_size - m,
+ // . . #--+------+ 0 <- (zero)
+ // . . #//| | .
+ // %(m): <~~
+ m => -m,
+ },
+ image_size
+ )
+}
+
+// Sizes of the last boundary tile in pixels.
+//
+// It can be smaller than the regular tile size if the image is not a multiple
+// of the regular tile size.
+fn last_tile_size_1d(
+ image_range: &Range<i32>,
+ regular_tile_size: i32,
+) -> i32 {
+ // We have to account for how the modulo operation behaves for negative values.
+ let image_size = image_range.end - image_range.start;
+ i32::min(
+ match image_range.end % regular_tile_size {
+ // +------+------# .
+ // tiles: . | |//////# .
+ 0 => regular_tile_size,
+ // . +------+--# . 0 <- (zero)
+ // . | |//# . .
+ // modulo (m): <~~
+ m if m < 0 => regular_tile_size + m,
+ // (zero) -> 0 +------+--# . .
+ // . | |//# . .
+ // modulo (m): ~~>
+ m => m,
+ },
+ image_size,
+ )
+}
+
+pub fn compute_tile_rect(
+ image_rect: &DeviceIntRect,
+ regular_tile_size: TileSize,
+ tile: TileOffset,
+) -> DeviceIntRect {
+ let regular_tile_size = regular_tile_size as i32;
+ DeviceIntRect::from_origin_and_size(
+ point2(
+ compute_tile_origin_1d(image_rect.x_range(), regular_tile_size, tile.x as i32),
+ compute_tile_origin_1d(image_rect.y_range(), regular_tile_size, tile.y as i32),
+ ),
+ size2(
+ compute_tile_size_1d(image_rect.x_range(), regular_tile_size, tile.x as i32),
+ compute_tile_size_1d(image_rect.y_range(), regular_tile_size, tile.y as i32),
+ ),
+ )
+}
+
+fn compute_tile_origin_1d(
+ img_range: Range<i32>,
+ regular_tile_size: i32,
+ tile_offset: i32,
+) -> i32 {
+ let tile_range = tile_range_1d(&img_range, regular_tile_size);
+ if tile_offset == tile_range.start {
+ img_range.start
+ } else {
+ tile_offset * regular_tile_size
+ }
+}
+
+// Compute the width and height in pixels of a tile depending on its position in the image.
+pub fn compute_tile_size(
+ image_rect: &DeviceIntRect,
+ regular_tile_size: TileSize,
+ tile: TileOffset,
+) -> DeviceIntSize {
+ let regular_tile_size = regular_tile_size as i32;
+ size2(
+ compute_tile_size_1d(image_rect.x_range(), regular_tile_size, tile.x as i32),
+ compute_tile_size_1d(image_rect.y_range(), regular_tile_size, tile.y as i32),
+ )
+}
+
+fn compute_tile_size_1d(
+ img_range: Range<i32>,
+ regular_tile_size: i32,
+ tile_offset: i32,
+) -> i32 {
+ let tile_range = tile_range_1d(&img_range, regular_tile_size);
+
+ // Most tiles are going to have base_size as width and height,
+ // except for tiles around the edges that are shrunk to fit the image data.
+ let actual_size = if tile_offset == tile_range.start {
+ first_tile_size_1d(&img_range, regular_tile_size)
+ } else if tile_offset == tile_range.end - 1 {
+ last_tile_size_1d(&img_range, regular_tile_size)
+ } else {
+ regular_tile_size
+ };
+
+ assert!(actual_size > 0);
+
+ actual_size
+}
+
+pub fn compute_tile_range(
+ visible_area: &DeviceIntRect,
+ tile_size: u16,
+) -> TileRange {
+ let tile_size = tile_size as i32;
+ let x_range = tile_range_1d(&visible_area.x_range(), tile_size);
+ let y_range = tile_range_1d(&visible_area.y_range(), tile_size);
+
+ TileRange {
+ min: point2(x_range.start, y_range.start),
+ max: point2(x_range.end, y_range.end),
+ }
+}
+
+pub fn for_each_tile_in_range(
+ range: &TileRange,
+ mut callback: impl FnMut(TileOffset),
+) {
+ for y in range.y_range() {
+ for x in range.x_range() {
+ callback(point2(x, y));
+ }
+ }
+}
+
+pub fn compute_valid_tiles_if_bounds_change(
+ prev_rect: &DeviceIntRect,
+ new_rect: &DeviceIntRect,
+ tile_size: u16,
+) -> Option<TileRange> {
+ let intersection = match prev_rect.intersection(new_rect) {
+ Some(rect) => rect,
+ None => {
+ return Some(TileRange::zero());
+ }
+ };
+
+ let left = prev_rect.min.x != new_rect.min.x;
+ let right = prev_rect.max.x != new_rect.max.x;
+ let top = prev_rect.min.y != new_rect.min.y;
+ let bottom = prev_rect.max.y != new_rect.max.y;
+
+ if !left && !right && !top && !bottom {
+ // Bounds have not changed.
+ return None;
+ }
+
+ let tw = 1.0 / (tile_size as f32);
+ let th = 1.0 / (tile_size as f32);
+
+ let tiles = intersection
+ .cast::<f32>()
+ .scale(tw, th);
+
+ let min_x = if left { f32::ceil(tiles.min.x) } else { f32::floor(tiles.min.x) };
+ let min_y = if top { f32::ceil(tiles.min.y) } else { f32::floor(tiles.min.y) };
+ let max_x = if right { f32::floor(tiles.max.x) } else { f32::ceil(tiles.max.x) };
+ let max_y = if bottom { f32::floor(tiles.max.y) } else { f32::ceil(tiles.max.y) };
+
+ Some(TileRange {
+ min: point2(min_x as i32, min_y as i32),
+ max: point2(max_x as i32, max_y as i32),
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::collections::HashSet;
+ use euclid::rect;
+
+ // this checks some additional invariants
+ fn checked_for_each_tile(
+ prim_rect: &LayoutRect,
+ visible_rect: &LayoutRect,
+ device_image_rect: &DeviceIntRect,
+ device_tile_size: i32,
+ callback: &mut dyn FnMut(&LayoutRect, TileOffset, EdgeAaSegmentMask),
+ ) {
+ let mut coverage = LayoutRect::zero();
+ let mut seen_tiles = HashSet::new();
+ for tile in tiles(
+ prim_rect,
+ visible_rect,
+ device_image_rect,
+ device_tile_size,
+ ) {
+ // make sure we don't get sent duplicate tiles
+ assert!(!seen_tiles.contains(&tile.offset));
+ seen_tiles.insert(tile.offset);
+ coverage = coverage.union(&tile.rect);
+ assert!(prim_rect.contains_box(&tile.rect));
+ callback(&tile.rect, tile.offset, tile.edge_flags);
+ }
+ assert!(prim_rect.contains_box(&coverage));
+ assert!(coverage.contains_box(&visible_rect.intersection(&prim_rect).unwrap_or(LayoutRect::zero())));
+ }
+
+ #[test]
+ fn basic() {
+ let mut count = 0;
+ checked_for_each_tile(&rect(0., 0., 1000., 1000.).to_box2d(),
+ &rect(75., 75., 400., 400.).to_box2d(),
+ &rect(0, 0, 400, 400).to_box2d(),
+ 36,
+ &mut |_tile_rect, _tile_offset, _tile_flags| {
+ count += 1;
+ },
+ );
+ assert_eq!(count, 36);
+ }
+
+ #[test]
+ fn empty() {
+ let mut count = 0;
+ checked_for_each_tile(&rect(0., 0., 74., 74.).to_box2d(),
+ &rect(75., 75., 400., 400.).to_box2d(),
+ &rect(0, 0, 400, 400).to_box2d(),
+ 36,
+ &mut |_tile_rect, _tile_offset, _tile_flags| {
+ count += 1;
+ },
+ );
+ assert_eq!(count, 0);
+ }
+
+ #[test]
+ fn test_tiles_1d() {
+ // Exactly one full tile at positive offset.
+ let result = tiles_1d(64.0, -10000.0..10000.0, 0.0, 0..64, 64);
+ assert_eq!(result.tile_range.start, 0);
+ assert_eq!(result.tile_range.end, 1);
+ assert_eq!(result.first_tile_layout_size, 64.0);
+ assert_eq!(result.last_tile_layout_size, 64.0);
+
+ // Exactly one full tile at negative offset.
+ let result = tiles_1d(64.0, -10000.0..10000.0, -64.0, -64..0, 64);
+ assert_eq!(result.tile_range.start, -1);
+ assert_eq!(result.tile_range.end, 0);
+ assert_eq!(result.first_tile_layout_size, 64.0);
+ assert_eq!(result.last_tile_layout_size, 64.0);
+
+ // Two full tiles at negative and positive offsets.
+ let result = tiles_1d(64.0, -10000.0..10000.0, -64.0, -64..64, 64);
+ assert_eq!(result.tile_range.start, -1);
+ assert_eq!(result.tile_range.end, 1);
+ assert_eq!(result.first_tile_layout_size, 64.0);
+ assert_eq!(result.last_tile_layout_size, 64.0);
+
+ // One partial tile at positive offset, non-zero origin, culled out.
+ let result = tiles_1d(64.0, -100.0..10.0, 64.0, 64..310, 64);
+ assert_eq!(result.tile_range.start, result.tile_range.end);
+
+ // Two tiles at negative and positive offsets, one of which is culled out.
+ // The remaining tile is partially culled but it should still generate a full tile.
+ let result = tiles_1d(64.0, 10.0..10000.0, -64.0, -64..64, 64);
+ assert_eq!(result.tile_range.start, 0);
+ assert_eq!(result.tile_range.end, 1);
+ assert_eq!(result.first_tile_layout_size, 64.0);
+ assert_eq!(result.last_tile_layout_size, 64.0);
+ let result = tiles_1d(64.0, -10000.0..-10.0, -64.0, -64..64, 64);
+ assert_eq!(result.tile_range.start, -1);
+ assert_eq!(result.tile_range.end, 0);
+ assert_eq!(result.first_tile_layout_size, 64.0);
+ assert_eq!(result.last_tile_layout_size, 64.0);
+
+ // Stretched tile in layout space device tile size is 64 and layout tile size is 128.
+ // So the resulting tile sizes in layout space should be multiplied by two.
+ let result = tiles_1d(128.0, -10000.0..10000.0, -64.0, -64..32, 64);
+ assert_eq!(result.tile_range.start, -1);
+ assert_eq!(result.tile_range.end, 1);
+ assert_eq!(result.first_tile_layout_size, 128.0);
+ assert_eq!(result.last_tile_layout_size, 64.0);
+
+ // Two visible tiles (the rest is culled out).
+ let result = tiles_1d(10.0, 0.0..20.0, 0.0, 0..64, 64);
+ assert_eq!(result.tile_range.start, 0);
+ assert_eq!(result.tile_range.end, 1);
+ assert_eq!(result.first_tile_layout_size, 10.0);
+ assert_eq!(result.last_tile_layout_size, 10.0);
+
+ // Two visible tiles at negative layout offsets (the rest is culled out).
+ let result = tiles_1d(10.0, -20.0..0.0, -20.0, 0..64, 64);
+ assert_eq!(result.tile_range.start, 0);
+ assert_eq!(result.tile_range.end, 1);
+ assert_eq!(result.first_tile_layout_size, 10.0);
+ assert_eq!(result.last_tile_layout_size, 10.0);
+ }
+
+ #[test]
+ fn test_tile_range_1d() {
+ assert_eq!(tile_range_1d(&(0..256), 256), 0..1);
+ assert_eq!(tile_range_1d(&(0..257), 256), 0..2);
+ assert_eq!(tile_range_1d(&(-1..257), 256), -1..2);
+ assert_eq!(tile_range_1d(&(-256..256), 256), -1..1);
+ assert_eq!(tile_range_1d(&(-20..-10), 6), -4..-1);
+ assert_eq!(tile_range_1d(&(20..100), 256), 0..1);
+ }
+
+ #[test]
+ fn test_first_last_tile_size_1d() {
+ assert_eq!(first_tile_size_1d(&(0..10), 64), 10);
+ assert_eq!(first_tile_size_1d(&(-20..0), 64), 20);
+
+ assert_eq!(last_tile_size_1d(&(0..10), 64), 10);
+ assert_eq!(last_tile_size_1d(&(-20..0), 64), 20);
+ }
+
+ #[test]
+ fn doubly_partial_tiles() {
+ // In the following tests the image is a single tile and none of the sides of the tile
+ // align with the tile grid.
+ // This can only happen when we have a single non-aligned partial tile and no regular
+ // tiles.
+ assert_eq!(first_tile_size_1d(&(300..310), 64), 10);
+ assert_eq!(first_tile_size_1d(&(-20..-10), 64), 10);
+
+ assert_eq!(last_tile_size_1d(&(300..310), 64), 10);
+ assert_eq!(last_tile_size_1d(&(-20..-10), 64), 10);
+
+
+ // One partial tile at positve offset, non-zero origin.
+ let result = tiles_1d(64.0, -10000.0..10000.0, 0.0, 300..310, 64);
+ assert_eq!(result.tile_range.start, 4);
+ assert_eq!(result.tile_range.end, 5);
+ assert_eq!(result.first_tile_layout_size, 10.0);
+ assert_eq!(result.last_tile_layout_size, 10.0);
+ }
+
+ #[test]
+ fn smaller_than_tile_size_at_origin() {
+ let r = compute_tile_rect(
+ &rect(0, 0, 80, 80).to_box2d(),
+ 256,
+ point2(0, 0),
+ );
+
+ assert_eq!(r, rect(0, 0, 80, 80).to_box2d());
+ }
+
+ #[test]
+ fn smaller_than_tile_size_with_offset() {
+ let r = compute_tile_rect(
+ &rect(20, 20, 80, 80).to_box2d(),
+ 256,
+ point2(0, 0),
+ );
+
+ assert_eq!(r, rect(20, 20, 80, 80).to_box2d());
+ }
+}
diff --git a/gfx/wr/webrender/src/intern.rs b/gfx/wr/webrender/src/intern.rs
new file mode 100644
index 0000000000..2a0cde16d3
--- /dev/null
+++ b/gfx/wr/webrender/src/intern.rs
@@ -0,0 +1,470 @@
+/* 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/. */
+
+//! The interning module provides a generic data structure
+//! interning container. It is similar in concept to a
+//! traditional string interning container, but it is
+//! specialized to the WR thread model.
+//!
+//! There is an Interner structure, that lives in the
+//! scene builder thread, and a DataStore structure
+//! that lives in the frame builder thread.
+//!
+//! Hashing, interning and handle creation is done by
+//! the interner structure during scene building.
+//!
+//! Delta changes for the interner are pushed during
+//! a transaction to the frame builder. The frame builder
+//! is then able to access the content of the interned
+//! handles quickly, via array indexing.
+//!
+//! Epoch tracking ensures that the garbage collection
+//! step which the interner uses to remove items is
+//! only invoked on items that the frame builder thread
+//! is no longer referencing.
+//!
+//! Items in the data store are stored in a traditional
+//! free-list structure, for content access and memory
+//! usage efficiency.
+//!
+//! The epoch is incremented each time a scene is
+//! built. The most recently used scene epoch is
+//! stored inside each handle. This is then used for
+//! cache invalidation.
+
+use crate::internal_types::FastHashMap;
+use malloc_size_of::MallocSizeOf;
+use std::fmt::Debug;
+use std::hash::Hash;
+use std::marker::PhantomData;
+use std::{ops, u64};
+use crate::util::VecHelper;
+use crate::profiler::TransactionProfile;
+use peek_poke::PeekPoke;
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, Hash, MallocSizeOf, PartialEq, Eq)]
+struct Epoch(u32);
+
+/// A list of updates to be applied to the data store,
+/// provided by the interning structure.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct UpdateList<S> {
+ /// Items to insert.
+ pub insertions: Vec<Insertion<S>>,
+
+ /// Items to remove.
+ pub removals: Vec<Removal>,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct Insertion<S> {
+ pub index: usize,
+ pub uid: ItemUid,
+ pub value: S,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct Removal {
+ pub index: usize,
+ pub uid: ItemUid,
+}
+
+impl<S> UpdateList<S> {
+ fn new() -> UpdateList<S> {
+ UpdateList {
+ insertions: Vec::new(),
+ removals: Vec::new(),
+ }
+ }
+
+ fn take_and_preallocate(&mut self) -> UpdateList<S> {
+ UpdateList {
+ insertions: self.insertions.take_and_preallocate(),
+ removals: self.removals.take_and_preallocate(),
+ }
+ }
+}
+
+/// A globally, unique identifier
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, Eq, Hash, MallocSizeOf, PartialEq, PeekPoke, Default)]
+pub struct ItemUid {
+ uid: u64,
+}
+
+impl ItemUid {
+ // Intended for debug usage only
+ pub fn get_uid(&self) -> u64 {
+ self.uid
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Hash, MallocSizeOf, PartialEq, Eq)]
+pub struct Handle<I> {
+ index: u32,
+ epoch: Epoch,
+ _marker: PhantomData<I>,
+}
+
+impl<I> Clone for Handle<I> {
+ fn clone(&self) -> Self {
+ Handle {
+ index: self.index,
+ epoch: self.epoch,
+ _marker: self._marker,
+ }
+ }
+}
+
+impl<I> Copy for Handle<I> {}
+
+impl<I> Handle<I> {
+ pub fn uid(&self) -> ItemUid {
+ ItemUid {
+ // The index in the freelist + the epoch it was interned generates a stable
+ // unique id for an interned element.
+ uid: ((self.index as u64) << 32) | self.epoch.0 as u64
+ }
+ }
+
+ pub const INVALID: Self = Handle { index: !0, epoch: Epoch(!0), _marker: PhantomData };
+}
+
+pub trait InternDebug {
+ fn on_interned(&self, _uid: ItemUid) {}
+}
+
+/// The data store lives in the frame builder thread. It
+/// contains a free-list of items for fast access.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct DataStore<I: Internable> {
+ items: Vec<Option<I::StoreData>>,
+}
+
+impl<I: Internable> Default for DataStore<I> {
+ fn default() -> Self {
+ DataStore {
+ items: Vec::new(),
+ }
+ }
+}
+
+impl<I: Internable> DataStore<I> {
+ /// Apply any updates from the scene builder thread to
+ /// this data store.
+ pub fn apply_updates(
+ &mut self,
+ update_list: UpdateList<I::Key>,
+ profile: &mut TransactionProfile,
+ ) {
+ for insertion in update_list.insertions {
+ self.items
+ .entry(insertion.index)
+ .set(Some(insertion.value.into()));
+ }
+
+ for removal in update_list.removals {
+ self.items[removal.index] = None;
+ }
+
+ profile.set(I::PROFILE_COUNTER, self.items.len());
+ }
+}
+
+/// Retrieve an item from the store via handle
+impl<I: Internable> ops::Index<Handle<I>> for DataStore<I> {
+ type Output = I::StoreData;
+ fn index(&self, handle: Handle<I>) -> &I::StoreData {
+ self.items[handle.index as usize].as_ref().expect("Bad datastore lookup")
+ }
+}
+
+/// Retrieve a mutable item from the store via handle
+/// Retrieve an item from the store via handle
+impl<I: Internable> ops::IndexMut<Handle<I>> for DataStore<I> {
+ fn index_mut(&mut self, handle: Handle<I>) -> &mut I::StoreData {
+ self.items[handle.index as usize].as_mut().expect("Bad datastore lookup")
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+struct ItemDetails<I> {
+ /// Frame that this element was first interned
+ interned_epoch: Epoch,
+ /// Last frame this element was referenced (used to GC intern items)
+ last_used_epoch: Epoch,
+ /// Index into the freelist this item is located
+ index: usize,
+ /// Type marker for create_handle method
+ _marker: PhantomData<I>,
+}
+
+impl<I> ItemDetails<I> {
+ /// Construct a stable handle value from the item details
+ fn create_handle(&self) -> Handle<I> {
+ Handle {
+ index: self.index as u32,
+ epoch: self.interned_epoch,
+ _marker: PhantomData,
+ }
+ }
+}
+
+/// The main interning data structure. This lives in the
+/// scene builder thread, and handles hashing and interning
+/// unique data structures. It also manages a free-list for
+/// the items in the data store, which is synchronized via
+/// an update list of additions / removals.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct Interner<I: Internable> {
+ /// Uniquely map an interning key to a handle
+ map: FastHashMap<I::Key, ItemDetails<I>>,
+ /// List of free slots in the data store for re-use.
+ free_list: Vec<usize>,
+ /// Pending list of updates that need to be applied.
+ update_list: UpdateList<I::Key>,
+ /// The current epoch for the interner.
+ current_epoch: Epoch,
+ /// The information associated with each interned
+ /// item that can be accessed by the interner.
+ local_data: Vec<I::InternData>,
+}
+
+impl<I: Internable> Default for Interner<I> {
+ fn default() -> Self {
+ Interner {
+ map: FastHashMap::default(),
+ free_list: Vec::new(),
+ update_list: UpdateList::new(),
+ current_epoch: Epoch(1),
+ local_data: Vec::new(),
+ }
+ }
+}
+
+impl<I: Internable> Interner<I> {
+ /// Intern a data structure, and return a handle to
+ /// that data. The handle can then be stored in the
+ /// frame builder, and safely accessed via the data
+ /// store that lives in the frame builder thread.
+ /// The provided closure is invoked to build the
+ /// local data about an interned structure if the
+ /// key isn't already interned.
+ pub fn intern<F>(
+ &mut self,
+ data: &I::Key,
+ fun: F,
+ ) -> Handle<I> where F: FnOnce() -> I::InternData {
+ // Use get_mut rather than entry here to avoid
+ // cloning the (sometimes large) key in the common
+ // case, where the data already exists in the interner.
+ if let Some(details) = self.map.get_mut(data) {
+ // Update the last referenced frame for this element
+ details.last_used_epoch = self.current_epoch;
+ // Return a stable handle value for dependency checking
+ return details.create_handle();
+ }
+
+ // We need to intern a new data item. First, find out
+ // if there is a spare slot in the free-list that we
+ // can use. Otherwise, append to the end of the list.
+ let index = match self.free_list.pop() {
+ Some(index) => index,
+ None => self.local_data.len(),
+ };
+
+ // Generate a handle for access via the data store.
+ let handle = Handle {
+ index: index as u32,
+ epoch: self.current_epoch,
+ _marker: PhantomData,
+ };
+
+ let uid = handle.uid();
+
+ // Add a pending update to insert the new data.
+ self.update_list.insertions.push(Insertion {
+ index,
+ uid,
+ value: data.clone(),
+ });
+
+ #[cfg(debug_assertions)]
+ data.on_interned(uid);
+
+ // Store this handle so the next time it is
+ // interned, it gets re-used.
+ self.map.insert(data.clone(), ItemDetails {
+ interned_epoch: self.current_epoch,
+ last_used_epoch: self.current_epoch,
+ index,
+ _marker: PhantomData,
+ });
+
+ // Create the local data for this item that is
+ // being interned.
+ self.local_data.entry(index).set(fun());
+
+ handle
+ }
+
+ /// Retrieve the pending list of updates for an interner
+ /// that need to be applied to the data store. Also run
+ /// a GC step that removes old entries.
+ pub fn end_frame_and_get_pending_updates(&mut self) -> UpdateList<I::Key> {
+ let mut update_list = self.update_list.take_and_preallocate();
+
+ let free_list = &mut self.free_list;
+ let current_epoch = self.current_epoch.0;
+
+ // First, run a GC step. Walk through the handles, and
+ // if we find any that haven't been used for some time,
+ // remove them. If this ever shows up in profiles, we
+ // can make the GC step partial (scan only part of the
+ // map each frame). It also might make sense in the
+ // future to adjust how long items remain in the cache
+ // based on the current size of the list.
+ self.map.retain(|_, details| {
+ if details.last_used_epoch.0 + 10 < current_epoch {
+ // To expire an item:
+ // - Add index to the free-list for re-use.
+ // - Add an update to the data store to invalidate this slot.
+ // - Remove from the hash map.
+ free_list.push(details.index);
+ update_list.removals.push(Removal {
+ index: details.index,
+ uid: details.create_handle().uid(),
+ });
+ return false;
+ }
+
+ true
+ });
+
+ // Begin the next epoch
+ self.current_epoch = Epoch(self.current_epoch.0 + 1);
+
+ update_list
+ }
+}
+
+/// Retrieve the local data for an item from the interner via handle
+impl<I: Internable> ops::Index<Handle<I>> for Interner<I> {
+ type Output = I::InternData;
+ fn index(&self, handle: Handle<I>) -> &I::InternData {
+ &self.local_data[handle.index as usize]
+ }
+}
+
+/// Meta-macro to enumerate the various interner identifiers and types.
+///
+/// IMPORTANT: Keep this synchronized with the list in mozilla-central located at
+/// gfx/webrender_bindings/webrender_ffi.h
+///
+/// Note that this could be a lot less verbose if concat_idents! were stable. :-(
+#[macro_export]
+macro_rules! enumerate_interners {
+ ($macro_name: ident) => {
+ $macro_name! {
+ clip: ClipIntern,
+ prim: PrimitiveKeyKind,
+ normal_border: NormalBorderPrim,
+ image_border: ImageBorder,
+ image: Image,
+ yuv_image: YuvImage,
+ line_decoration: LineDecoration,
+ linear_grad: LinearGradient,
+ radial_grad: RadialGradient,
+ conic_grad: ConicGradient,
+ picture: Picture,
+ text_run: TextRun,
+ filter_data: FilterDataIntern,
+ backdrop_capture: BackdropCapture,
+ backdrop_render: BackdropRender,
+ polygon: PolygonIntern,
+ }
+ }
+}
+
+macro_rules! declare_interning_memory_report {
+ ( $( $name:ident: $ty:ident, )+ ) => {
+ ///
+ #[repr(C)]
+ #[derive(AddAssign, Clone, Debug, Default)]
+ pub struct InternerSubReport {
+ $(
+ ///
+ pub $name: usize,
+ )+
+ }
+ }
+}
+
+enumerate_interners!(declare_interning_memory_report);
+
+/// Memory report for interning-related data structures.
+/// cbindgen:derive-eq=false
+/// cbindgen:derive-ostream=false
+#[repr(C)]
+#[derive(Clone, Debug, Default)]
+pub struct InterningMemoryReport {
+ ///
+ pub interners: InternerSubReport,
+ ///
+ pub data_stores: InternerSubReport,
+}
+
+impl ::std::ops::AddAssign for InterningMemoryReport {
+ fn add_assign(&mut self, other: InterningMemoryReport) {
+ self.interners += other.interners;
+ self.data_stores += other.data_stores;
+ }
+}
+
+// The trick to make trait bounds configurable by features.
+mod dummy {
+ #[cfg(not(feature = "capture"))]
+ pub trait Serialize {}
+ #[cfg(not(feature = "capture"))]
+ impl<T> Serialize for T {}
+ #[cfg(not(feature = "replay"))]
+ pub trait Deserialize<'a> {}
+ #[cfg(not(feature = "replay"))]
+ impl<'a, T> Deserialize<'a> for T {}
+}
+#[cfg(feature = "capture")]
+use serde::Serialize as InternSerialize;
+#[cfg(not(feature = "capture"))]
+use self::dummy::Serialize as InternSerialize;
+#[cfg(feature = "replay")]
+use serde::Deserialize as InternDeserialize;
+#[cfg(not(feature = "replay"))]
+use self::dummy::Deserialize as InternDeserialize;
+
+/// Implement `Internable` for a type that wants to participate in interning.
+pub trait Internable: MallocSizeOf {
+ type Key: Eq + Hash + Clone + Debug + MallocSizeOf + InternDebug + InternSerialize + for<'a> InternDeserialize<'a>;
+ type StoreData: From<Self::Key> + MallocSizeOf + InternSerialize + for<'a> InternDeserialize<'a>;
+ type InternData: MallocSizeOf + InternSerialize + for<'a> InternDeserialize<'a>;
+
+ // Profile counter indices, see the list in profiler.rs
+ const PROFILE_COUNTER: usize;
+}
diff --git a/gfx/wr/webrender/src/internal_types.rs b/gfx/wr/webrender/src/internal_types.rs
new file mode 100644
index 0000000000..c76b7d362d
--- /dev/null
+++ b/gfx/wr/webrender/src/internal_types.rs
@@ -0,0 +1,761 @@
+/* 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 api::{ColorF, DocumentId, ExternalImageId, PrimitiveFlags, Parameter, RenderReasons};
+use api::{ImageFormat, NotificationRequest, Shadow, FilterOp, ImageBufferKind};
+use api::FramePublishId;
+use api::units::*;
+use api;
+use crate::render_api::DebugCommand;
+use crate::composite::NativeSurfaceOperation;
+use crate::device::TextureFilter;
+use crate::renderer::{FullFrameStats, PipelineInfo};
+use crate::gpu_cache::GpuCacheUpdateList;
+use crate::frame_builder::Frame;
+use crate::profiler::TransactionProfile;
+use crate::spatial_tree::SpatialNodeIndex;
+use crate::prim_store::PrimitiveInstanceIndex;
+use fxhash::FxHasher;
+use plane_split::BspSplitter;
+use smallvec::SmallVec;
+use std::{usize, i32};
+use std::collections::{HashMap, HashSet};
+use std::f32;
+use std::hash::BuildHasherDefault;
+use std::path::PathBuf;
+use std::sync::Arc;
+use std::time::{UNIX_EPOCH, SystemTime};
+use peek_poke::PeekPoke;
+
+#[cfg(any(feature = "capture", feature = "replay"))]
+use crate::capture::CaptureConfig;
+#[cfg(feature = "capture")]
+use crate::capture::ExternalCaptureImage;
+#[cfg(feature = "replay")]
+use crate::capture::PlainExternalImage;
+
+pub type FastHashMap<K, V> = HashMap<K, V, BuildHasherDefault<FxHasher>>;
+pub type FastHashSet<K> = HashSet<K, BuildHasherDefault<FxHasher>>;
+
+#[derive(Copy, Clone, Hash, MallocSizeOf, PartialEq, PartialOrd, Debug, Eq, Ord, PeekPoke)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct FrameId(u64);
+
+impl FrameId {
+ /// Returns a FrameId corresponding to the first frame.
+ ///
+ /// Note that we use 0 as the internal id here because the current code
+ /// increments the frame id at the beginning of the frame, rather than
+ /// at the end, and we want the first frame to be 1. It would probably
+ /// be sensible to move the advance() call to after frame-building, and
+ /// then make this method return FrameId(1).
+ pub fn first() -> Self {
+ FrameId(0)
+ }
+
+ /// Returns the backing u64 for this FrameId.
+ pub fn as_u64(&self) -> u64 {
+ self.0
+ }
+
+ /// Advances this FrameId to the next frame.
+ pub fn advance(&mut self) {
+ self.0 += 1;
+ }
+
+ /// An invalid sentinel FrameId, which will always compare less than
+ /// any valid FrameId.
+ pub const INVALID: FrameId = FrameId(0);
+}
+
+impl Default for FrameId {
+ fn default() -> Self {
+ FrameId::INVALID
+ }
+}
+
+impl ::std::ops::Add<u64> for FrameId {
+ type Output = Self;
+ fn add(self, other: u64) -> FrameId {
+ FrameId(self.0 + other)
+ }
+}
+
+impl ::std::ops::Sub<u64> for FrameId {
+ type Output = Self;
+ fn sub(self, other: u64) -> FrameId {
+ assert!(self.0 >= other, "Underflow subtracting FrameIds");
+ FrameId(self.0 - other)
+ }
+}
+
+/// Identifier to track a sequence of frames.
+///
+/// This is effectively a `FrameId` with a ridealong timestamp corresponding
+/// to when advance() was called, which allows for more nuanced cache eviction
+/// decisions. As such, we use the `FrameId` for equality and comparison, since
+/// we should never have two `FrameStamps` with the same id but different
+/// timestamps.
+#[derive(Copy, Clone, Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct FrameStamp {
+ id: FrameId,
+ time: SystemTime,
+ document_id: DocumentId,
+}
+
+impl Eq for FrameStamp {}
+
+impl PartialEq for FrameStamp {
+ fn eq(&self, other: &Self) -> bool {
+ // We should not be checking equality unless the documents are the same
+ debug_assert!(self.document_id == other.document_id);
+ self.id == other.id
+ }
+}
+
+impl PartialOrd for FrameStamp {
+ fn partial_cmp(&self, other: &Self) -> Option<::std::cmp::Ordering> {
+ self.id.partial_cmp(&other.id)
+ }
+}
+
+impl FrameStamp {
+ /// Gets the FrameId in this stamp.
+ pub fn frame_id(&self) -> FrameId {
+ self.id
+ }
+
+ /// Gets the time associated with this FrameStamp.
+ pub fn time(&self) -> SystemTime {
+ self.time
+ }
+
+ /// Gets the DocumentId in this stamp.
+ pub fn document_id(&self) -> DocumentId {
+ self.document_id
+ }
+
+ pub fn is_valid(&self) -> bool {
+ // If any fields are their default values, the whole struct should equal INVALID
+ debug_assert!((self.time != UNIX_EPOCH && self.id != FrameId(0) && self.document_id != DocumentId::INVALID) ||
+ *self == Self::INVALID);
+ self.document_id != DocumentId::INVALID
+ }
+
+ /// Returns a FrameStamp corresponding to the first frame.
+ pub fn first(document_id: DocumentId) -> Self {
+ FrameStamp {
+ id: FrameId::first(),
+ time: SystemTime::now(),
+ document_id,
+ }
+ }
+
+ /// Advances to a new frame.
+ pub fn advance(&mut self) {
+ self.id.advance();
+ self.time = SystemTime::now();
+ }
+
+ /// An invalid sentinel FrameStamp.
+ pub const INVALID: FrameStamp = FrameStamp {
+ id: FrameId(0),
+ time: UNIX_EPOCH,
+ document_id: DocumentId::INVALID,
+ };
+}
+
+/// Custom field embedded inside the Polygon struct of the plane-split crate.
+#[derive(Copy, Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct PlaneSplitAnchor {
+ pub spatial_node_index: SpatialNodeIndex,
+ pub instance_index: PrimitiveInstanceIndex,
+}
+
+impl PlaneSplitAnchor {
+ pub fn new(
+ spatial_node_index: SpatialNodeIndex,
+ instance_index: PrimitiveInstanceIndex,
+ ) -> Self {
+ PlaneSplitAnchor {
+ spatial_node_index,
+ instance_index,
+ }
+ }
+}
+
+impl Default for PlaneSplitAnchor {
+ fn default() -> Self {
+ PlaneSplitAnchor {
+ spatial_node_index: SpatialNodeIndex::INVALID,
+ instance_index: PrimitiveInstanceIndex(!0),
+ }
+ }
+}
+
+/// A concrete plane splitter type used in WebRender.
+pub type PlaneSplitter = BspSplitter<PlaneSplitAnchor>;
+
+/// An index into the scene's list of plane splitters
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct PlaneSplitterIndex(pub usize);
+
+/// An arbitrary number which we assume opacity is invisible below.
+const OPACITY_EPSILON: f32 = 0.001;
+
+/// Equivalent to api::FilterOp with added internal information
+#[derive(Clone, Debug, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum Filter {
+ Identity,
+ Blur {
+ width: f32,
+ height: f32,
+ should_inflate: bool,
+ },
+ Brightness(f32),
+ Contrast(f32),
+ Grayscale(f32),
+ HueRotate(f32),
+ Invert(f32),
+ Opacity(api::PropertyBinding<f32>, f32),
+ Saturate(f32),
+ Sepia(f32),
+ DropShadows(SmallVec<[Shadow; 1]>),
+ ColorMatrix(Box<[f32; 20]>),
+ SrgbToLinear,
+ LinearToSrgb,
+ ComponentTransfer,
+ Flood(ColorF),
+}
+
+impl Filter {
+ pub fn is_visible(&self) -> bool {
+ match *self {
+ Filter::Identity |
+ Filter::Blur { .. } |
+ Filter::Brightness(..) |
+ Filter::Contrast(..) |
+ Filter::Grayscale(..) |
+ Filter::HueRotate(..) |
+ Filter::Invert(..) |
+ Filter::Saturate(..) |
+ Filter::Sepia(..) |
+ Filter::DropShadows(..) |
+ Filter::ColorMatrix(..) |
+ Filter::SrgbToLinear |
+ Filter::LinearToSrgb |
+ Filter::ComponentTransfer => true,
+ Filter::Opacity(_, amount) => {
+ amount > OPACITY_EPSILON
+ },
+ Filter::Flood(color) => {
+ color.a > OPACITY_EPSILON
+ }
+ }
+ }
+
+ pub fn is_noop(&self) -> bool {
+ match *self {
+ Filter::Identity => false, // this is intentional
+ Filter::Blur { width, height, .. } => width == 0.0 && height == 0.0,
+ Filter::Brightness(amount) => amount == 1.0,
+ Filter::Contrast(amount) => amount == 1.0,
+ Filter::Grayscale(amount) => amount == 0.0,
+ Filter::HueRotate(amount) => amount == 0.0,
+ Filter::Invert(amount) => amount == 0.0,
+ Filter::Opacity(api::PropertyBinding::Value(amount), _) => amount >= 1.0,
+ Filter::Saturate(amount) => amount == 1.0,
+ Filter::Sepia(amount) => amount == 0.0,
+ Filter::DropShadows(ref shadows) => {
+ for shadow in shadows {
+ if shadow.offset.x != 0.0 || shadow.offset.y != 0.0 || shadow.blur_radius != 0.0 {
+ return false;
+ }
+ }
+
+ true
+ }
+ Filter::ColorMatrix(ref matrix) => {
+ **matrix == [
+ 1.0, 0.0, 0.0, 0.0,
+ 0.0, 1.0, 0.0, 0.0,
+ 0.0, 0.0, 1.0, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ 0.0, 0.0, 0.0, 0.0
+ ]
+ }
+ Filter::Opacity(api::PropertyBinding::Binding(..), _) |
+ Filter::SrgbToLinear |
+ Filter::LinearToSrgb |
+ Filter::ComponentTransfer |
+ Filter::Flood(..) => false,
+ }
+ }
+
+
+ pub fn as_int(&self) -> i32 {
+ // Must be kept in sync with brush_blend.glsl
+ match *self {
+ Filter::Identity => 0, // matches `Contrast(1)`
+ Filter::Contrast(..) => 0,
+ Filter::Grayscale(..) => 1,
+ Filter::HueRotate(..) => 2,
+ Filter::Invert(..) => 3,
+ Filter::Saturate(..) => 4,
+ Filter::Sepia(..) => 5,
+ Filter::Brightness(..) => 6,
+ Filter::ColorMatrix(..) => 7,
+ Filter::SrgbToLinear => 8,
+ Filter::LinearToSrgb => 9,
+ Filter::Flood(..) => 10,
+ Filter::ComponentTransfer => 11,
+ Filter::Blur { .. } => 12,
+ Filter::DropShadows(..) => 13,
+ Filter::Opacity(..) => 14,
+ }
+ }
+}
+
+impl From<FilterOp> for Filter {
+ fn from(op: FilterOp) -> Self {
+ match op {
+ FilterOp::Identity => Filter::Identity,
+ FilterOp::Blur(width, height) => Filter::Blur { width, height, should_inflate: true },
+ FilterOp::Brightness(b) => Filter::Brightness(b),
+ FilterOp::Contrast(c) => Filter::Contrast(c),
+ FilterOp::Grayscale(g) => Filter::Grayscale(g),
+ FilterOp::HueRotate(h) => Filter::HueRotate(h),
+ FilterOp::Invert(i) => Filter::Invert(i),
+ FilterOp::Opacity(binding, opacity) => Filter::Opacity(binding, opacity),
+ FilterOp::Saturate(s) => Filter::Saturate(s),
+ FilterOp::Sepia(s) => Filter::Sepia(s),
+ FilterOp::ColorMatrix(mat) => Filter::ColorMatrix(Box::new(mat)),
+ FilterOp::SrgbToLinear => Filter::SrgbToLinear,
+ FilterOp::LinearToSrgb => Filter::LinearToSrgb,
+ FilterOp::ComponentTransfer => Filter::ComponentTransfer,
+ FilterOp::DropShadow(shadow) => Filter::DropShadows(smallvec![shadow]),
+ FilterOp::Flood(color) => Filter::Flood(color),
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)]
+pub enum Swizzle {
+ Rgba,
+ Bgra,
+}
+
+impl Default for Swizzle {
+ fn default() -> Self {
+ Swizzle::Rgba
+ }
+}
+
+/// Swizzle settings of the texture cache.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)]
+pub struct SwizzleSettings {
+ /// Swizzle required on sampling a texture with BGRA8 format.
+ pub bgra8_sampling_swizzle: Swizzle,
+}
+
+/// An ID for a texture that is owned by the `texture_cache` module.
+///
+/// This can include atlases or standalone textures allocated via the texture
+/// cache (e.g. if an image is too large to be added to an atlas). The texture
+/// cache manages the allocation and freeing of these IDs, and the rendering
+/// thread maintains a map from cache texture ID to native texture.
+///
+/// We never reuse IDs, so we use a u64 here to be safe.
+#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct CacheTextureId(pub u32);
+
+impl CacheTextureId {
+ pub const INVALID: CacheTextureId = CacheTextureId(!0);
+}
+
+#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct DeferredResolveIndex(pub u32);
+
+/// Identifies the source of an input texture to a shader.
+#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum TextureSource {
+ /// Equivalent to `None`, allowing us to avoid using `Option`s everywhere.
+ Invalid,
+ /// An entry in the texture cache.
+ TextureCache(CacheTextureId, Swizzle),
+ /// An external image texture, mananged by the embedding.
+ External(DeferredResolveIndex, ImageBufferKind),
+ /// Select a dummy 1x1 white texture. This can be used by image
+ /// shaders that want to draw a solid color.
+ Dummy,
+}
+
+impl TextureSource {
+ pub fn image_buffer_kind(&self) -> ImageBufferKind {
+ match *self {
+ TextureSource::TextureCache(..) => ImageBufferKind::Texture2D,
+
+ TextureSource::External(_, image_buffer_kind) => image_buffer_kind,
+
+ // Render tasks use texture arrays for now.
+ TextureSource::Dummy => ImageBufferKind::Texture2D,
+
+ TextureSource::Invalid => ImageBufferKind::Texture2D,
+ }
+ }
+
+ #[inline]
+ pub fn is_compatible(
+ &self,
+ other: &TextureSource,
+ ) -> bool {
+ *self == TextureSource::Invalid ||
+ *other == TextureSource::Invalid ||
+ self == other
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RenderTargetInfo {
+ pub has_depth: bool,
+}
+
+#[derive(Debug)]
+pub enum TextureUpdateSource {
+ External {
+ id: ExternalImageId,
+ channel_index: u8,
+ },
+ Bytes { data: Arc<Vec<u8>> },
+ /// Clears the target area, rather than uploading any pixels. Used when the
+ /// texture cache debug display is active.
+ DebugClear,
+}
+
+/// Command to allocate, reallocate, or free a texture for the texture cache.
+#[derive(Debug)]
+pub struct TextureCacheAllocation {
+ /// The virtual ID (i.e. distinct from device ID) of the texture.
+ pub id: CacheTextureId,
+ /// Details corresponding to the operation in question.
+ pub kind: TextureCacheAllocationKind,
+}
+
+/// A little bit of extra information to make memory reports more useful
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum TextureCacheCategory {
+ Atlas,
+ Standalone,
+ PictureTile,
+ RenderTarget,
+}
+
+/// Information used when allocating / reallocating.
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+pub struct TextureCacheAllocInfo {
+ pub width: i32,
+ pub height: i32,
+ pub format: ImageFormat,
+ pub filter: TextureFilter,
+ pub target: ImageBufferKind,
+ /// Indicates whether this corresponds to one of the shared texture caches.
+ pub is_shared_cache: bool,
+ /// If true, this texture requires a depth target.
+ pub has_depth: bool,
+ pub category: TextureCacheCategory
+}
+
+/// Sub-operation-specific information for allocation operations.
+#[derive(Debug)]
+pub enum TextureCacheAllocationKind {
+ /// Performs an initial texture allocation.
+ Alloc(TextureCacheAllocInfo),
+ /// Reallocates the texture without preserving its contents.
+ Reset(TextureCacheAllocInfo),
+ /// Frees the texture and the corresponding cache ID.
+ Free,
+}
+
+/// Command to update the contents of the texture cache.
+#[derive(Debug)]
+pub struct TextureCacheUpdate {
+ pub rect: DeviceIntRect,
+ pub stride: Option<i32>,
+ pub offset: i32,
+ pub format_override: Option<ImageFormat>,
+ pub source: TextureUpdateSource,
+}
+
+/// Command to update the contents of the texture cache.
+#[derive(Debug)]
+pub struct TextureCacheCopy {
+ pub src_rect: DeviceIntRect,
+ pub dst_rect: DeviceIntRect,
+}
+
+/// Atomic set of commands to manipulate the texture cache, generated on the
+/// RenderBackend thread and executed on the Renderer thread.
+///
+/// The list of allocation operations is processed before the updates. This is
+/// important to allow coalescing of certain allocation operations.
+#[derive(Default)]
+pub struct TextureUpdateList {
+ /// Indicates that there was some kind of cleanup clear operation. Used for
+ /// sanity checks.
+ pub clears_shared_cache: bool,
+ /// Commands to alloc/realloc/free the textures. Processed first.
+ pub allocations: Vec<TextureCacheAllocation>,
+ /// Commands to update the contents of the textures. Processed second.
+ pub updates: FastHashMap<CacheTextureId, Vec<TextureCacheUpdate>>,
+ /// Commands to move items within the cache, these are applied before everything
+ /// else in the update list.
+ pub copies: FastHashMap<(CacheTextureId, CacheTextureId), Vec<TextureCacheCopy>>,
+}
+
+impl TextureUpdateList {
+ /// Mints a new `TextureUpdateList`.
+ pub fn new() -> Self {
+ TextureUpdateList {
+ clears_shared_cache: false,
+ allocations: Vec::new(),
+ updates: FastHashMap::default(),
+ copies: FastHashMap::default(),
+ }
+ }
+
+ /// Returns true if this is a no-op (no updates to be applied).
+ pub fn is_nop(&self) -> bool {
+ self.allocations.is_empty() && self.updates.is_empty()
+ }
+
+ /// Sets the clears_shared_cache flag for renderer-side sanity checks.
+ #[inline]
+ pub fn note_clear(&mut self) {
+ self.clears_shared_cache = true;
+ }
+
+ /// Pushes an update operation onto the list.
+ #[inline]
+ pub fn push_update(&mut self, id: CacheTextureId, update: TextureCacheUpdate) {
+ self.updates
+ .entry(id)
+ .or_default()
+ .push(update);
+ }
+
+ /// Sends a command to the Renderer to clear the portion of the shared region
+ /// we just freed. Used when the texture cache debugger is enabled.
+ #[cold]
+ pub fn push_debug_clear(
+ &mut self,
+ id: CacheTextureId,
+ origin: DeviceIntPoint,
+ width: i32,
+ height: i32,
+ ) {
+ let size = DeviceIntSize::new(width, height);
+ let rect = DeviceIntRect::from_origin_and_size(origin, size);
+ self.push_update(id, TextureCacheUpdate {
+ rect,
+ stride: None,
+ offset: 0,
+ format_override: None,
+ source: TextureUpdateSource::DebugClear,
+ });
+ }
+
+
+ /// Pushes an allocation operation onto the list.
+ pub fn push_alloc(&mut self, id: CacheTextureId, info: TextureCacheAllocInfo) {
+ debug_assert!(!self.allocations.iter().any(|x| x.id == id));
+ self.allocations.push(TextureCacheAllocation {
+ id,
+ kind: TextureCacheAllocationKind::Alloc(info),
+ });
+ }
+
+ /// Pushes a reallocation operation onto the list, potentially coalescing
+ /// with previous operations.
+ pub fn push_reset(&mut self, id: CacheTextureId, info: TextureCacheAllocInfo) {
+ self.debug_assert_coalesced(id);
+
+ // Drop any unapplied updates to the to-be-freed texture.
+ self.updates.remove(&id);
+
+ // Coallesce this realloc into a previous alloc or realloc, if available.
+ if let Some(cur) = self.allocations.iter_mut().find(|x| x.id == id) {
+ match cur.kind {
+ TextureCacheAllocationKind::Alloc(ref mut i) => *i = info,
+ TextureCacheAllocationKind::Reset(ref mut i) => *i = info,
+ TextureCacheAllocationKind::Free => panic!("Resetting freed texture"),
+ }
+ return
+ }
+
+ self.allocations.push(TextureCacheAllocation {
+ id,
+ kind: TextureCacheAllocationKind::Reset(info),
+ });
+ }
+
+ /// Pushes a free operation onto the list, potentially coalescing with
+ /// previous operations.
+ pub fn push_free(&mut self, id: CacheTextureId) {
+ self.debug_assert_coalesced(id);
+
+ // Drop any unapplied updates to the to-be-freed texture.
+ self.updates.remove(&id);
+
+ // Drop any allocations for it as well. If we happen to be allocating and
+ // freeing in the same batch, we can collapse them to a no-op.
+ let idx = self.allocations.iter().position(|x| x.id == id);
+ let removed_kind = idx.map(|i| self.allocations.remove(i).kind);
+ match removed_kind {
+ Some(TextureCacheAllocationKind::Alloc(..)) => { /* no-op! */ },
+ Some(TextureCacheAllocationKind::Free) => panic!("Double free"),
+ Some(TextureCacheAllocationKind::Reset(..)) |
+ None => {
+ self.allocations.push(TextureCacheAllocation {
+ id,
+ kind: TextureCacheAllocationKind::Free,
+ });
+ }
+ };
+ }
+
+ /// Push a copy operation from a texture to another.
+ ///
+ /// The source and destination rectangles must have the same size.
+ /// The copies are applied before every other operations in the
+ /// texture update list.
+ pub fn push_copy(
+ &mut self,
+ src_id: CacheTextureId, src_rect: &DeviceIntRect,
+ dst_id: CacheTextureId, dst_rect: &DeviceIntRect,
+ ) {
+ debug_assert_eq!(src_rect.size(), dst_rect.size());
+ self.copies.entry((src_id, dst_id))
+ .or_insert_with(Vec::new)
+ .push(TextureCacheCopy {
+ src_rect: *src_rect,
+ dst_rect: *dst_rect,
+ });
+ }
+
+ fn debug_assert_coalesced(&self, id: CacheTextureId) {
+ debug_assert!(
+ self.allocations.iter().filter(|x| x.id == id).count() <= 1,
+ "Allocations should have been coalesced",
+ );
+ }
+}
+
+/// A list of updates built by the render backend that should be applied
+/// by the renderer thread.
+pub struct ResourceUpdateList {
+ /// List of OS native surface create / destroy operations to apply.
+ pub native_surface_updates: Vec<NativeSurfaceOperation>,
+
+ /// Atomic set of texture cache updates to apply.
+ pub texture_updates: TextureUpdateList,
+}
+
+impl ResourceUpdateList {
+ /// Returns true if this update list has no effect.
+ pub fn is_nop(&self) -> bool {
+ self.texture_updates.is_nop() && self.native_surface_updates.is_empty()
+ }
+}
+
+/// Wraps a frame_builder::Frame, but conceptually could hold more information
+pub struct RenderedDocument {
+ pub frame: Frame,
+ pub is_new_scene: bool,
+ pub profile: TransactionProfile,
+ pub render_reasons: RenderReasons,
+ pub frame_stats: Option<FullFrameStats>
+}
+
+pub enum DebugOutput {
+ #[cfg(feature = "capture")]
+ SaveCapture(CaptureConfig, Vec<ExternalCaptureImage>),
+ #[cfg(feature = "replay")]
+ LoadCapture(CaptureConfig, Vec<PlainExternalImage>),
+}
+
+#[allow(dead_code)]
+pub enum ResultMsg {
+ DebugCommand(DebugCommand),
+ DebugOutput(DebugOutput),
+ RefreshShader(PathBuf),
+ UpdateGpuCache(GpuCacheUpdateList),
+ UpdateResources {
+ resource_updates: ResourceUpdateList,
+ memory_pressure: bool,
+ },
+ PublishPipelineInfo(PipelineInfo),
+ PublishDocument(
+ FramePublishId,
+ DocumentId,
+ RenderedDocument,
+ ResourceUpdateList,
+ ),
+ AppendNotificationRequests(Vec<NotificationRequest>),
+ SetParameter(Parameter),
+ ForceRedraw,
+}
+
+/// Primitive metadata we pass around in a bunch of places
+#[derive(Copy, Clone, Debug)]
+pub struct LayoutPrimitiveInfo {
+ /// NOTE: this is *ideally* redundant with the clip_rect
+ /// but that's an ongoing project, so for now it exists and is used :(
+ pub rect: LayoutRect,
+ pub clip_rect: LayoutRect,
+ pub flags: PrimitiveFlags,
+}
+
+impl LayoutPrimitiveInfo {
+ pub fn with_clip_rect(rect: LayoutRect, clip_rect: LayoutRect) -> Self {
+ Self {
+ rect,
+ clip_rect,
+ flags: PrimitiveFlags::default(),
+ }
+ }
+}
+
+// In some cases (e.g. printing) a pipeline is referenced multiple times by
+// a parent display list. This allows us to distinguish between them.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Copy, Clone, PartialEq, Debug, Eq, Hash)]
+pub struct PipelineInstanceId(u32);
+
+impl PipelineInstanceId {
+ pub fn new(id: u32) -> Self {
+ PipelineInstanceId(id)
+ }
+}
diff --git a/gfx/wr/webrender/src/lib.rs b/gfx/wr/webrender/src/lib.rs
new file mode 100644
index 0000000000..d319bc68bf
--- /dev/null
+++ b/gfx/wr/webrender/src/lib.rs
@@ -0,0 +1,194 @@
+/* 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 GPU based renderer for the web.
+
+It serves as an experimental render backend for [Servo](https://servo.org/),
+but it can also be used as such in a standalone application.
+
+# External dependencies
+WebRender currently depends on [FreeType](https://www.freetype.org/)
+
+# Api Structure
+The main entry point to WebRender is the [`crate::Renderer`].
+
+By calling [`Renderer::new(...)`](crate::Renderer::new) you get a [`Renderer`], as well as
+a [`RenderApiSender`](api::RenderApiSender). Your [`Renderer`] is responsible to render the
+previously processed frames onto the screen.
+
+By calling [`yourRenderApiSender.create_api()`](api::RenderApiSender::create_api), you'll
+get a [`RenderApi`](api::RenderApi) instance, which is responsible for managing resources
+and documents. A worker thread is used internally to untie the workload from the application
+thread and therefore be able to make better use of multicore systems.
+
+## Frame
+
+What is referred to as a `frame`, is the current geometry on the screen.
+A new Frame is created by calling [`set_display_list()`](api::Transaction::set_display_list)
+on the [`RenderApi`](api::RenderApi). When the geometry is processed, the application will be
+informed via a [`RenderNotifier`](api::RenderNotifier), a callback which you pass to
+[`Renderer::new`].
+More information about [stacking contexts][stacking_contexts].
+
+[`set_display_list()`](api::Transaction::set_display_list) also needs to be supplied with
+[`BuiltDisplayList`](api::BuiltDisplayList)s. These are obtained by finalizing a
+[`DisplayListBuilder`](api::DisplayListBuilder). These are used to draw your geometry. But it
+doesn't only contain trivial geometry, it can also store another
+[`StackingContext`](api::StackingContext), as they're nestable.
+
+[stacking_contexts]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context
+*/
+
+#![cfg_attr(feature = "cargo-clippy", allow(clippy::unreadable_literal, clippy::new_without_default, clippy::too_many_arguments))]
+
+
+// Cribbed from the |matches| crate, for simplicity.
+macro_rules! matches {
+ ($expression:expr, $($pattern:tt)+) => {
+ match $expression {
+ $($pattern)+ => true,
+ _ => false
+ }
+ }
+}
+
+#[macro_use]
+extern crate bitflags;
+#[macro_use]
+extern crate lazy_static;
+#[macro_use]
+extern crate log;
+#[macro_use]
+extern crate malloc_size_of_derive;
+#[cfg(any(feature = "serde"))]
+#[macro_use]
+extern crate serde;
+#[macro_use]
+extern crate tracy_rs;
+#[macro_use]
+extern crate derive_more;
+extern crate malloc_size_of;
+extern crate svg_fmt;
+
+#[macro_use]
+mod profiler;
+mod telemetry;
+
+mod batch;
+mod border;
+mod box_shadow;
+#[cfg(any(feature = "capture", feature = "replay"))]
+mod capture;
+mod clip;
+mod space;
+mod spatial_tree;
+mod command_buffer;
+mod composite;
+mod compositor;
+mod debug_colors;
+mod debug_font_data;
+mod debug_item;
+mod device;
+mod ellipse;
+mod filterdata;
+mod frame_builder;
+mod freelist;
+mod glyph_cache;
+mod gpu_cache;
+mod gpu_types;
+mod hit_test;
+mod internal_types;
+mod lru_cache;
+mod picture;
+mod picture_graph;
+mod prepare;
+mod prim_store;
+mod print_tree;
+mod render_backend;
+mod render_target;
+mod render_task_graph;
+mod render_task_cache;
+mod render_task;
+mod renderer;
+mod resource_cache;
+mod scene;
+mod scene_builder_thread;
+mod scene_building;
+mod screen_capture;
+mod segment;
+mod spatial_node;
+mod surface;
+mod texture_pack;
+mod texture_cache;
+mod tile_cache;
+mod util;
+mod visibility;
+mod api_resources;
+mod image_tiling;
+mod image_source;
+mod rectangle_occlusion;
+mod picture_textures;
+
+///
+pub mod intern;
+///
+pub mod render_api;
+
+pub mod shader_source {
+ include!(concat!(env!("OUT_DIR"), "/shaders.rs"));
+}
+
+extern crate bincode;
+extern crate byteorder;
+pub extern crate euclid;
+extern crate fxhash;
+extern crate gleam;
+extern crate num_traits;
+extern crate plane_split;
+extern crate rayon;
+#[cfg(feature = "ron")]
+extern crate ron;
+#[macro_use]
+extern crate smallvec;
+extern crate time;
+#[cfg(all(feature = "capture", feature = "png"))]
+extern crate png;
+#[cfg(test)]
+extern crate rand;
+
+pub extern crate api;
+extern crate webrender_build;
+
+#[doc(hidden)]
+pub use crate::composite::{CompositorConfig, Compositor, CompositorCapabilities, CompositorSurfaceTransform};
+pub use crate::composite::{NativeSurfaceId, NativeTileId, NativeSurfaceInfo, PartialPresentCompositor};
+pub use crate::composite::{MappableCompositor, MappedTileInfo, SWGLCompositeSurfaceInfo, WindowVisibility};
+pub use crate::device::{UploadMethod, VertexUsageHint, get_gl_target, get_unoptimized_shader_source};
+pub use crate::device::{ProgramBinary, ProgramCache, ProgramCacheObserver, FormatDesc};
+pub use crate::device::Device;
+pub use crate::profiler::{ProfilerHooks, set_profiler_hooks};
+pub use crate::renderer::{
+ CpuProfile, DebugFlags, GpuProfile, GraphicsApi,
+ GraphicsApiInfo, PipelineInfo, Renderer, RendererError, RenderResults,
+ RendererStats, Shaders, SharedShaders, ShaderPrecacheFlags,
+ MAX_VERTEX_TEXTURE_WIDTH,
+};
+pub use crate::renderer::init::{WebRenderOptions, create_webrender_instance, AsyncPropertySampler, SceneBuilderHooks, ONE_TIME_USAGE_HINT};
+pub use crate::hit_test::SharedHitTester;
+pub use crate::internal_types::FastHashMap;
+pub use crate::screen_capture::{AsyncScreenshotHandle, RecordedFrameHandle};
+pub use crate::texture_cache::TextureCacheConfig;
+pub use api as webrender_api;
+pub use webrender_build::shader::{ProgramSourceDigest, ShaderKind};
+pub use crate::picture::{TileDescriptor, TileId, InvalidationReason};
+pub use crate::picture::{PrimitiveCompareResult, CompareHelperResult};
+pub use crate::picture::{TileNode, TileNodeKind, TileOffset};
+pub use crate::intern::ItemUid;
+pub use crate::render_api::*;
+pub use crate::tile_cache::{PictureCacheDebugInfo, DirtyTileDebugInfo, TileDebugInfo, SliceDebugInfo};
+pub use glyph_rasterizer;
+
+#[cfg(feature = "sw_compositor")]
+pub use crate::compositor::sw_compositor;
diff --git a/gfx/wr/webrender/src/lru_cache.rs b/gfx/wr/webrender/src/lru_cache.rs
new file mode 100644
index 0000000000..d53119b77d
--- /dev/null
+++ b/gfx/wr/webrender/src/lru_cache.rs
@@ -0,0 +1,675 @@
+/* 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::freelist::{FreeList, FreeListHandle, WeakFreeListHandle};
+use std::{mem, num};
+
+/*
+ This module implements a least recently used cache structure, which is
+ used by the texture cache to manage the lifetime of items inside the
+ texture cache. It has a few special pieces of functionality that the
+ texture cache requires, but should be usable as a general LRU cache
+ type if useful in other areas.
+
+ The cache is implemented with two types of backing freelists. These allow
+ random access to the underlying data, while being efficient in both
+ memory access and allocation patterns.
+
+ The "entries" freelist stores the elements being cached (for example, the
+ CacheEntry structure for the texture cache). These elements are stored
+ in arbitrary order, reusing empty slots in the freelist where possible.
+
+ The "lru_index" freelists store the LRU tracking information. Although the
+ tracking elements are stored in arbitrary order inside a freelist for
+ efficiency, they use next/prev links to represent a doubly-linked list,
+ kept sorted in order of recent use. The next link is also used to store
+ the current freelist within the array when the element is not occupied.
+
+ The LRU cache allows having multiple LRU "partitions". Every entry is tracked
+ by exactly one partition at any time; all partitions refer to entries in the
+ shared freelist. Entries can move between partitions, if replace_or_insert is
+ called with a new partition index for an existing handle.
+ The partitioning is used by the texture cache so that, for example, allocating
+ more glyph entries does not cause eviction of image entries (which go into
+ a different shared texture). If an existing handle's entry is reallocated with
+ a new size, it might need to move from a shared texture to a standalone
+ texture; in this case the handle will move to a different LRU partition.
+ */
+
+/// Stores the data supplied by the user to be cached, and an index
+/// into the LRU tracking freelist for this element.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+struct LRUCacheEntry<T> {
+ /// The LRU partition that tracks this entry.
+ partition_index: u8,
+
+ /// The location of the LRU tracking element for this cache entry in the
+ /// right LRU partition.
+ lru_index: ItemIndex,
+
+ /// The cached data provided by the caller for this element.
+ value: T,
+}
+
+/// The main public interface to the LRU cache
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct LRUCache<T, M> {
+ /// A free list of cache entries, and indices into the LRU tracking list
+ entries: FreeList<LRUCacheEntry<T>, M>,
+ /// The LRU tracking list, allowing O(1) access to the oldest element
+ lru: Vec<LRUTracker<FreeListHandle<M>>>,
+}
+
+impl<T, M> LRUCache<T, M> {
+ /// Construct a new LRU cache
+ pub fn new(lru_partition_count: usize) -> Self {
+ assert!(lru_partition_count <= u8::MAX as usize + 1);
+ LRUCache {
+ entries: FreeList::new(),
+ lru: (0..lru_partition_count).map(|_| LRUTracker::new()).collect(),
+ }
+ }
+
+ /// Insert a new element into the cache. Returns a weak handle for callers to
+ /// access the data, since the lifetime is managed by the LRU algorithm and it
+ /// may be evicted at any time.
+ pub fn push_new(
+ &mut self,
+ partition_index: u8,
+ value: T,
+ ) -> WeakFreeListHandle<M> {
+ // It's a slightly awkward process to insert an element, since we don't know
+ // the index of the LRU tracking element until we've got a handle for the
+ // underlying cached data.
+
+ // Insert the data provided by the caller
+ let handle = self.entries.insert(LRUCacheEntry {
+ partition_index: 0,
+ lru_index: ItemIndex(num::NonZeroU32::new(1).unwrap()),
+ value
+ });
+
+ // Get a weak handle to return to the caller
+ let weak_handle = handle.weak();
+
+ // Add an LRU tracking node that owns the strong handle, and store the location
+ // of this inside the cache entry.
+ let entry = self.entries.get_mut(&handle);
+ let lru_index = self.lru[partition_index as usize].push_new(handle);
+ entry.partition_index = partition_index;
+ entry.lru_index = lru_index;
+
+ weak_handle
+ }
+
+ /// Get immutable access to the data at a given slot. Since this takes a weak
+ /// handle, it may have been evicted, so returns an Option.
+ pub fn get_opt(
+ &self,
+ handle: &WeakFreeListHandle<M>,
+ ) -> Option<&T> {
+ self.entries
+ .get_opt(handle)
+ .map(|entry| {
+ &entry.value
+ })
+ }
+
+ /// Get mutable access to the data at a given slot. Since this takes a weak
+ /// handle, it may have been evicted, so returns an Option.
+ pub fn get_opt_mut(
+ &mut self,
+ handle: &WeakFreeListHandle<M>,
+ ) -> Option<&mut T> {
+ self.entries
+ .get_opt_mut(handle)
+ .map(|entry| {
+ &mut entry.value
+ })
+ }
+
+ /// Return a reference to the oldest item in the cache, keeping it in the cache.
+ /// If the cache is empty, this will return None.
+ pub fn peek_oldest(&self, partition_index: u8) -> Option<&T> {
+ self.lru[partition_index as usize]
+ .peek_front()
+ .map(|handle| {
+ let entry = self.entries.get(handle);
+ &entry.value
+ })
+ }
+
+ /// Remove the oldest item from the cache. This is used to select elements to
+ /// be evicted. If the cache is empty, this will return None.
+ pub fn pop_oldest(
+ &mut self,
+ partition_index: u8,
+ ) -> Option<T> {
+ self.lru[partition_index as usize]
+ .pop_front()
+ .map(|handle| {
+ let entry = self.entries.free(handle);
+ entry.value
+ })
+ }
+
+ /// This is a special case of `push_new`, which is a requirement for the texture
+ /// cache. Sometimes, we want to replace the content of an existing handle if it
+ /// exists, or insert a new element if the handle is invalid (for example, if an
+ /// image is resized and it moves to a new location in the texture atlas). This
+ /// method returns the old cache entry if it existed, so it can be freed by the caller.
+ #[must_use]
+ pub fn replace_or_insert(
+ &mut self,
+ handle: &mut WeakFreeListHandle<M>,
+ partition_index: u8,
+ data: T,
+ ) -> Option<T> {
+ match self.entries.get_opt_mut(handle) {
+ Some(entry) => {
+ if entry.partition_index != partition_index {
+ // Move to a different partition.
+ let strong_handle = self.lru[entry.partition_index as usize].remove(entry.lru_index);
+ let lru_index = self.lru[partition_index as usize].push_new(strong_handle);
+ entry.partition_index = partition_index;
+ entry.lru_index = lru_index;
+ }
+ Some(mem::replace(&mut entry.value, data))
+ }
+ None => {
+ *handle = self.push_new(partition_index, data);
+ None
+ }
+ }
+ }
+
+ /// Manually evict a specific item.
+ pub fn remove(&mut self, handle: &WeakFreeListHandle<M>) -> Option<T> {
+ if let Some(entry) = self.entries.get_opt_mut(handle) {
+ let strong_handle = self.lru[entry.partition_index as usize].remove(entry.lru_index);
+ return Some(self.entries.free(strong_handle).value);
+ }
+
+ None
+ }
+
+ /// This is used by the calling code to signal that the element that this handle
+ /// references has been used on this frame. Internally, it updates the links in
+ /// the LRU tracking element to move this item to the end of the LRU list. Returns
+ /// the underlying data in case the client wants to mutate it.
+ pub fn touch(
+ &mut self,
+ handle: &WeakFreeListHandle<M>,
+ ) -> Option<&mut T> {
+ let lru = &mut self.lru;
+
+ self.entries
+ .get_opt_mut(handle)
+ .map(|entry| {
+ lru[entry.partition_index as usize].mark_used(entry.lru_index);
+ &mut entry.value
+ })
+ }
+
+ /// Try to validate that the state of the cache is consistent
+ #[cfg(test)]
+ fn validate(&self) {
+ for lru in &self.lru {
+ lru.validate();
+ }
+ }
+}
+
+/// Index of an LRU tracking element
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, MallocSizeOf)]
+struct ItemIndex(num::NonZeroU32);
+
+impl ItemIndex {
+ fn as_usize(&self) -> usize {
+ self.0.get() as usize
+ }
+}
+
+/// Stores a strong handle controlling the lifetime of the data in the LRU
+/// cache, and a doubly-linked list node specifying where in the current LRU
+/// order this element exists. These items are themselves backed by a freelist
+/// to minimize heap allocations and improve cache access patterns.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, MallocSizeOf)]
+struct Item<H> {
+ prev: Option<ItemIndex>,
+ next: Option<ItemIndex>,
+ handle: Option<H>,
+}
+
+/// Internal implementation of the LRU tracking list
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+struct LRUTracker<H> {
+ /// Current head of the list - this is the oldest item that will be evicted next.
+ head: Option<ItemIndex>,
+ /// Current tail of the list - this is the most recently used element.
+ tail: Option<ItemIndex>,
+ /// As tracking items are removed, they are stored in a freelist, to minimize heap allocations
+ free_list_head: Option<ItemIndex>,
+ /// The freelist that stores all the LRU tracking items
+ items: Vec<Item<H>>,
+}
+
+impl<H> LRUTracker<H> where H: std::fmt::Debug {
+ /// Construct a new LRU tracker
+ fn new() -> Self {
+ // Push a dummy entry in the vec that is never used. This ensures the NonZeroU32
+ // property is respected, and we never create an ItemIndex(0).
+ let items = vec![
+ Item {
+ prev: None,
+ next: None,
+ handle: None,
+ },
+ ];
+
+ LRUTracker {
+ head: None,
+ tail: None,
+ free_list_head: None,
+ items,
+ }
+ }
+
+ /// Internal function that takes an item index, and links it to the
+ /// end of the tracker list (makes it the newest item).
+ fn link_as_new_tail(
+ &mut self,
+ item_index: ItemIndex,
+ ) {
+ match (self.head, self.tail) {
+ (Some(..), Some(tail)) => {
+ // Both a head and a tail
+ self.items[item_index.as_usize()].prev = Some(tail);
+ self.items[item_index.as_usize()].next = None;
+
+ self.items[tail.as_usize()].next = Some(item_index);
+ self.tail = Some(item_index);
+ }
+ (None, None) => {
+ // No head/tail, currently empty list
+ self.items[item_index.as_usize()].prev = None;
+ self.items[item_index.as_usize()].next = None;
+
+ self.head = Some(item_index);
+ self.tail = Some(item_index);
+ }
+ (Some(..), None) | (None, Some(..)) => {
+ // Invalid state
+ unreachable!();
+ }
+ }
+ }
+
+ /// Internal function that takes an LRU item index, and removes it from
+ /// the current doubly linked list. Used during removal of items, and also
+ /// when items are moved to the back of the list as they're touched.
+ fn unlink(
+ &mut self,
+ item_index: ItemIndex,
+ ) {
+ let (next, prev) = {
+ let item = &self.items[item_index.as_usize()];
+ (item.next, item.prev)
+ };
+
+ match next {
+ Some(next) => {
+ self.items[next.as_usize()].prev = prev;
+ }
+ None => {
+ debug_assert_eq!(self.tail, Some(item_index));
+ self.tail = prev;
+ }
+ }
+
+ match prev {
+ Some(prev) => {
+ self.items[prev.as_usize()].next = next;
+ }
+ None => {
+ debug_assert_eq!(self.head, Some(item_index));
+ self.head = next;
+ }
+ }
+ }
+
+ /// Push a new LRU tracking item on to the back of the list, marking
+ /// it as the most recent item.
+ fn push_new(
+ &mut self,
+ handle: H,
+ ) -> ItemIndex {
+ // See if there is a slot available in the current free list
+ let item_index = match self.free_list_head {
+ Some(index) => {
+ // Reuse an existing slot
+ let item = &mut self.items[index.as_usize()];
+
+ assert!(item.handle.is_none());
+ item.handle = Some(handle);
+
+ self.free_list_head = item.next;
+
+ index
+ }
+ None => {
+ // No free slots available, push to the end of the array
+ let index = ItemIndex(num::NonZeroU32::new(self.items.len() as u32).unwrap());
+
+ self.items.push(Item {
+ prev: None,
+ next: None,
+ handle: Some(handle),
+ });
+
+ index
+ }
+ };
+
+ // Now link this element into the LRU list
+ self.link_as_new_tail(item_index);
+
+ item_index
+ }
+
+ /// Returns a reference to the oldest element, or None if the list is empty.
+ fn peek_front(&self) -> Option<&H> {
+ self.head.map(|head| self.items[head.as_usize()].handle.as_ref().unwrap())
+ }
+
+ /// Remove the oldest element from the front of the LRU list. Returns None
+ /// if the list is empty.
+ fn pop_front(
+ &mut self,
+ ) -> Option<H> {
+ let handle = match (self.head, self.tail) {
+ (Some(head), Some(tail)) => {
+ let item_index = head;
+
+ // Head and tail are the same - removing the only element
+ if head == tail {
+ self.head = None;
+ self.tail = None;
+ } else {
+ // Update the head of the list, popping the first element off
+ let new_head = self.items[head.as_usize()].next.unwrap();
+ self.head = Some(new_head);
+ self.items[new_head.as_usize()].prev = None;
+ }
+
+ // Add this item to the freelist for later use
+ self.items[item_index.as_usize()].next = self.free_list_head;
+ self.free_list_head = Some(item_index);
+
+ // Return the handle to the user
+ Some(self.items[item_index.as_usize()].handle.take().unwrap())
+ }
+ (None, None) => {
+ // List is empty
+ None
+ }
+ (Some(..), None) | (None, Some(..)) => {
+ // Invalid state
+ unreachable!();
+ }
+ };
+
+ handle
+ }
+
+ /// Manually remove an item from the LRU tracking list. This is used
+ /// when an element switches from one LRU partition to a different one.
+ fn remove(
+ &mut self,
+ index: ItemIndex,
+ ) -> H {
+ // Remove from the LRU list
+ self.unlink(index);
+
+ let handle = self.items[index.as_usize()].handle.take().unwrap();
+
+ // Add LRU item to the freelist for future use.
+ self.items[index.as_usize()].next = self.free_list_head;
+ self.free_list_head = Some(index);
+
+ handle
+ }
+
+ /// Called to mark that an item was used on this frame. It unlinks the
+ /// tracking item, and then re-links it to the back of the list.
+ fn mark_used(
+ &mut self,
+ index: ItemIndex,
+ ) {
+ self.unlink(index);
+ self.link_as_new_tail(index);
+ }
+
+ /// Try to validate that the state of the linked lists are consistent
+ #[cfg(test)]
+ fn validate(&self) {
+ use std::collections::HashSet;
+
+ // Must have a valid head/tail or be empty
+ assert!((self.head.is_none() && self.tail.is_none()) || (self.head.is_some() && self.tail.is_some()));
+
+ // If there is a head, the prev of the head must be none
+ if let Some(head) = self.head {
+ assert!(self.items[head.as_usize()].prev.is_none());
+ }
+
+ // If there is a tail, the next of the tail must be none
+ if let Some(tail) = self.tail {
+ assert!(self.items[tail.as_usize()].next.is_none());
+ }
+
+ // Collect all free and valid items, both in forwards and reverse order
+ let mut free_items = Vec::new();
+ let mut free_items_set = HashSet::new();
+ let mut valid_items_front = Vec::new();
+ let mut valid_items_front_set = HashSet::new();
+ let mut valid_items_reverse = Vec::new();
+ let mut valid_items_reverse_set = HashSet::new();
+
+ let mut current = self.free_list_head;
+ while let Some(index) = current {
+ let item = &self.items[index.as_usize()];
+ free_items.push(index);
+ assert!(free_items_set.insert(index));
+ current = item.next;
+ }
+
+ current = self.head;
+ while let Some(index) = current {
+ let item = &self.items[index.as_usize()];
+ valid_items_front.push(index);
+ assert!(valid_items_front_set.insert(index));
+ current = item.next;
+ }
+
+ current = self.tail;
+ while let Some(index) = current {
+ let item = &self.items[index.as_usize()];
+ valid_items_reverse.push(index);
+ assert!(!valid_items_reverse_set.contains(&index));
+ valid_items_reverse_set.insert(index);
+ current = item.prev;
+ }
+
+ // Ensure set lengths match the vec lengths (should be enforced by the assert check during insert anyway)
+ assert_eq!(valid_items_front.len(), valid_items_front_set.len());
+ assert_eq!(valid_items_reverse.len(), valid_items_reverse_set.len());
+
+ // Length of the array should equal free + valid items count + 1 (dummy entry)
+ assert_eq!(free_items.len() + valid_items_front.len() + 1, self.items.len());
+
+ // Should be same number of items whether iterating forwards or reverse
+ assert_eq!(valid_items_front.len(), valid_items_reverse.len());
+
+ // Ensure there are no items considered in the free list that are also in the valid list
+ assert!(free_items_set.intersection(&valid_items_reverse_set).collect::<HashSet<_>>().is_empty());
+ assert!(free_items_set.intersection(&valid_items_front_set).collect::<HashSet<_>>().is_empty());
+
+ // Should be the same number of items regardless of iteration direction
+ assert_eq!(valid_items_front_set.len(), valid_items_reverse_set.len());
+
+ // Ensure that the ordering is exactly the same, regardless of iteration direction
+ for (i0, i1) in valid_items_front.iter().zip(valid_items_reverse.iter().rev()) {
+ assert_eq!(i0, i1);
+ }
+ }
+}
+
+#[test]
+fn test_lru_tracker_push_peek() {
+ // Push elements, peek and ensure:
+ // - peek_oldest returns None before first element pushed
+ // - peek_oldest returns oldest element
+ // - subsequent calls to peek_oldest return same element (nothing was removed)
+ struct CacheMarker;
+ const NUM_ELEMENTS: usize = 50;
+
+ let mut cache: LRUCache<usize, CacheMarker> = LRUCache::new(1);
+ cache.validate();
+
+ assert_eq!(cache.peek_oldest(0), None);
+
+ for i in 0 .. NUM_ELEMENTS {
+ cache.push_new(0, i);
+ }
+ cache.validate();
+
+ assert_eq!(cache.peek_oldest(0), Some(&0));
+ assert_eq!(cache.peek_oldest(0), Some(&0));
+
+ cache.pop_oldest(0);
+ assert_eq!(cache.peek_oldest(0), Some(&1));
+}
+
+#[test]
+fn test_lru_tracker_push_pop() {
+ // Push elements, pop them all off and ensure:
+ // - Returned in oldest order
+ // - pop_oldest returns None after last element popped
+ struct CacheMarker;
+ const NUM_ELEMENTS: usize = 50;
+
+ let mut cache: LRUCache<usize, CacheMarker> = LRUCache::new(1);
+ cache.validate();
+
+ for i in 0 .. NUM_ELEMENTS {
+ cache.push_new(0, i);
+ }
+ cache.validate();
+
+ for i in 0 .. NUM_ELEMENTS {
+ assert_eq!(cache.pop_oldest(0), Some(i));
+ }
+ cache.validate();
+
+ assert_eq!(cache.pop_oldest(0), None);
+}
+
+#[test]
+fn test_lru_tracker_push_touch_pop() {
+ // Push elements, touch even handles, pop them all off and ensure:
+ // - Returned in correct order
+ // - pop_oldest returns None after last element popped
+ struct CacheMarker;
+ const NUM_ELEMENTS: usize = 50;
+
+ let mut cache: LRUCache<usize, CacheMarker> = LRUCache::new(1);
+ let mut handles = Vec::new();
+ cache.validate();
+
+ for i in 0 .. NUM_ELEMENTS {
+ handles.push(cache.push_new(0, i));
+ }
+ cache.validate();
+
+ for i in 0 .. NUM_ELEMENTS/2 {
+ cache.touch(&handles[i*2]);
+ }
+ cache.validate();
+
+ for i in 0 .. NUM_ELEMENTS/2 {
+ assert_eq!(cache.pop_oldest(0), Some(i*2+1));
+ }
+ cache.validate();
+ for i in 0 .. NUM_ELEMENTS/2 {
+ assert_eq!(cache.pop_oldest(0), Some(i*2));
+ }
+ cache.validate();
+
+ assert_eq!(cache.pop_oldest(0), None);
+}
+
+#[test]
+fn test_lru_tracker_push_get() {
+ // Push elements, ensure:
+ // - get access via weak handles works
+ struct CacheMarker;
+ const NUM_ELEMENTS: usize = 50;
+
+ let mut cache: LRUCache<usize, CacheMarker> = LRUCache::new(1);
+ let mut handles = Vec::new();
+ cache.validate();
+
+ for i in 0 .. NUM_ELEMENTS {
+ handles.push(cache.push_new(0, i));
+ }
+ cache.validate();
+
+ for i in 0 .. NUM_ELEMENTS/2 {
+ assert!(cache.get_opt(&handles[i]) == Some(&i));
+ }
+ cache.validate();
+}
+
+#[test]
+fn test_lru_tracker_push_replace_get() {
+ // Push elements, replace contents, ensure:
+ // - each element was replaced with new data correctly
+ // - replace_or_insert works for invalid handles
+ struct CacheMarker;
+ const NUM_ELEMENTS: usize = 50;
+
+ let mut cache: LRUCache<usize, CacheMarker> = LRUCache::new(1);
+ let mut handles = Vec::new();
+ cache.validate();
+
+ for i in 0 .. NUM_ELEMENTS {
+ handles.push(cache.push_new(0, i));
+ }
+ cache.validate();
+
+ for i in 0 .. NUM_ELEMENTS {
+ assert_eq!(cache.replace_or_insert(&mut handles[i], 0, i * 2), Some(i));
+ }
+ cache.validate();
+
+ for i in 0 .. NUM_ELEMENTS/2 {
+ assert!(cache.get_opt(&handles[i]) == Some(&(i * 2)));
+ }
+ cache.validate();
+
+ let mut empty_handle = WeakFreeListHandle::invalid();
+ assert_eq!(cache.replace_or_insert(&mut empty_handle, 0, 100), None);
+ assert_eq!(cache.get_opt(&empty_handle), Some(&100));
+}
diff --git a/gfx/wr/webrender/src/picture.rs b/gfx/wr/webrender/src/picture.rs
new file mode 100644
index 0000000000..5b1a232deb
--- /dev/null
+++ b/gfx/wr/webrender/src/picture.rs
@@ -0,0 +1,7199 @@
+/* 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 picture represents a dynamically rendered image.
+//!
+//! # Overview
+//!
+//! Pictures consists of:
+//!
+//! - A number of primitives that are drawn onto the picture.
+//! - A composite operation describing how to composite this
+//! picture into its parent.
+//! - A configuration describing how to draw the primitives on
+//! this picture (e.g. in screen space or local space).
+//!
+//! The tree of pictures are generated during scene building.
+//!
+//! Depending on their composite operations pictures can be rendered into
+//! intermediate targets or folded into their parent picture.
+//!
+//! ## Picture caching
+//!
+//! Pictures can be cached to reduce the amount of rasterization happening per
+//! frame.
+//!
+//! When picture caching is enabled, the scene is cut into a small number of slices,
+//! typically:
+//!
+//! - content slice
+//! - UI slice
+//! - background UI slice which is hidden by the other two slices most of the time.
+//!
+//! Each of these slice is made up of fixed-size large tiles of 2048x512 pixels
+//! (or 128x128 for the UI slice).
+//!
+//! Tiles can be either cached rasterized content into a texture or "clear tiles"
+//! that contain only a solid color rectangle rendered directly during the composite
+//! pass.
+//!
+//! ## Invalidation
+//!
+//! Each tile keeps track of the elements that affect it, which can be:
+//!
+//! - primitives
+//! - clips
+//! - image keys
+//! - opacity bindings
+//! - transforms
+//!
+//! These dependency lists are built each frame and compared to the previous frame to
+//! see if the tile changed.
+//!
+//! The tile's primitive dependency information is organized in a quadtree, each node
+//! storing an index buffer of tile primitive dependencies.
+//!
+//! The union of the invalidated leaves of each quadtree produces a per-tile dirty rect
+//! which defines the scissor rect used when replaying the tile's drawing commands and
+//! can be used for partial present.
+//!
+//! ## Display List shape
+//!
+//! WR will first look for an iframe item in the root stacking context to apply
+//! picture caching to. If that's not found, it will apply to the entire root
+//! stacking context of the display list. Apart from that, the format of the
+//! display list is not important to picture caching. Each time a new scroll root
+//! is encountered, a new picture cache slice will be created. If the display
+//! list contains more than some arbitrary number of slices (currently 8), the
+//! content will all be squashed into a single slice, in order to save GPU memory
+//! and compositing performance.
+//!
+//! ## Compositor Surfaces
+//!
+//! Sometimes, a primitive would prefer to exist as a native compositor surface.
+//! This allows a large and/or regularly changing primitive (such as a video, or
+//! webgl canvas) to be updated each frame without invalidating the content of
+//! tiles, and can provide a significant performance win and battery saving.
+//!
+//! Since drawing a primitive as a compositor surface alters the ordering of
+//! primitives in a tile, we use 'overlay tiles' to ensure correctness. If a
+//! tile has a compositor surface, _and_ that tile has primitives that overlap
+//! the compositor surface rect, the tile switches to be drawn in alpha mode.
+//!
+//! We rely on only promoting compositor surfaces that are opaque primitives.
+//! With this assumption, the tile(s) that intersect the compositor surface get
+//! a 'cutout' in the rectangle where the compositor surface exists (not the
+//! entire tile), allowing that tile to be drawn as an alpha tile after the
+//! compositor surface.
+//!
+//! Tiles are only drawn in overlay mode if there is content that exists on top
+//! of the compositor surface. Otherwise, we can draw the tiles in the normal fast
+//! path before the compositor surface is drawn. Use of the per-tile valid and
+//! dirty rects ensure that we do a minimal amount of per-pixel work here to
+//! blend the overlay tile (this is not always optimal right now, but will be
+//! improved as a follow up).
+
+use api::{MixBlendMode, PremultipliedColorF, FilterPrimitiveKind};
+use api::{PropertyBinding, PropertyBindingId, FilterPrimitive, RasterSpace};
+use api::{DebugFlags, ImageKey, ColorF, ColorU, PrimitiveFlags};
+use api::{ImageRendering, ColorDepth, YuvRangedColorSpace, YuvFormat, AlphaType};
+use api::units::*;
+use crate::command_buffer::PrimitiveCommand;
+use crate::box_shadow::BLUR_SAMPLE_SCALE;
+use crate::clip::{ClipStore, ClipChainInstance, ClipLeafId, ClipNodeId, ClipTreeBuilder};
+use crate::spatial_tree::{SpatialTree, CoordinateSpaceMapping, SpatialNodeIndex, VisibleFace};
+use crate::composite::{CompositorKind, CompositeState, NativeSurfaceId, NativeTileId, CompositeTileSurface, tile_kind};
+use crate::composite::{ExternalSurfaceDescriptor, ExternalSurfaceDependency, CompositeTileDescriptor, CompositeTile};
+use crate::composite::{CompositorTransformIndex};
+use crate::debug_colors;
+use euclid::{vec3, Point2D, Scale, Vector2D, Box2D};
+use euclid::approxeq::ApproxEq;
+use crate::filterdata::SFilterData;
+use crate::intern::ItemUid;
+use crate::internal_types::{FastHashMap, FastHashSet, PlaneSplitter, Filter, FrameId};
+use crate::internal_types::{PlaneSplitterIndex, PlaneSplitAnchor, TextureSource};
+use crate::frame_builder::{FrameBuildingContext, FrameBuildingState, PictureState, PictureContext};
+use crate::gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
+use crate::gpu_types::{UvRectKind, ZBufferId};
+use peek_poke::{PeekPoke, poke_into_vec, peek_from_slice, ensure_red_zone};
+use plane_split::{Clipper, Polygon};
+use crate::prim_store::{PrimitiveTemplateKind, PictureIndex, PrimitiveInstance, PrimitiveInstanceKind};
+use crate::prim_store::{ColorBindingStorage, ColorBindingIndex, PrimitiveScratchBuffer};
+use crate::print_tree::{PrintTree, PrintTreePrinter};
+use crate::render_backend::DataStores;
+use crate::render_task_graph::RenderTaskId;
+use crate::render_target::RenderTargetKind;
+use crate::render_task::{BlurTask, RenderTask, RenderTaskLocation, BlurTaskCache};
+use crate::render_task::{StaticRenderTaskSurface, RenderTaskKind};
+use crate::renderer::BlendMode;
+use crate::resource_cache::{ResourceCache, ImageGeneration, ImageRequest};
+use crate::space::SpaceMapper;
+use crate::scene::SceneProperties;
+use crate::spatial_tree::CoordinateSystemId;
+use crate::surface::{SurfaceDescriptor, SurfaceTileDescriptor};
+use smallvec::SmallVec;
+use std::{mem, u8, marker, u32};
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::collections::hash_map::Entry;
+use std::ops::Range;
+use crate::picture_textures::PictureCacheTextureHandle;
+use crate::util::{MaxRect, VecHelper, MatrixHelpers, Recycler, ScaleOffset};
+use crate::filterdata::{FilterDataHandle};
+use crate::tile_cache::{SliceDebugInfo, TileDebugInfo, DirtyTileDebugInfo};
+use crate::visibility::{PrimitiveVisibilityFlags, FrameVisibilityContext};
+use crate::visibility::{VisibilityState, FrameVisibilityState};
+use crate::scene_building::{SliceFlags};
+
+// Maximum blur radius for blur filter (different than box-shadow blur).
+// Taken from FilterNodeSoftware.cpp in Gecko.
+const MAX_BLUR_RADIUS: f32 = 100.;
+
+/// Specify whether a surface allows subpixel AA text rendering.
+#[derive(Debug, Copy, Clone)]
+pub enum SubpixelMode {
+ /// This surface allows subpixel AA text
+ Allow,
+ /// Subpixel AA text cannot be drawn on this surface
+ Deny,
+ /// Subpixel AA can be drawn on this surface, if not intersecting
+ /// with the excluded regions, and inside the allowed rect.
+ Conditional {
+ allowed_rect: PictureRect,
+ },
+}
+
+/// A comparable transform matrix, that compares with epsilon checks.
+#[derive(Debug, Clone)]
+struct MatrixKey {
+ m: [f32; 16],
+}
+
+impl PartialEq for MatrixKey {
+ fn eq(&self, other: &Self) -> bool {
+ const EPSILON: f32 = 0.001;
+
+ // TODO(gw): It's possible that we may need to adjust the epsilon
+ // to be tighter on most of the matrix, except the
+ // translation parts?
+ for (i, j) in self.m.iter().zip(other.m.iter()) {
+ if !i.approx_eq_eps(j, &EPSILON) {
+ return false;
+ }
+ }
+
+ true
+ }
+}
+
+/// A comparable scale-offset, that compares with epsilon checks.
+#[derive(Debug, Clone)]
+struct ScaleOffsetKey {
+ sx: f32,
+ sy: f32,
+ tx: f32,
+ ty: f32,
+}
+
+impl PartialEq for ScaleOffsetKey {
+ fn eq(&self, other: &Self) -> bool {
+ const EPSILON: f32 = 0.001;
+
+ self.sx.approx_eq_eps(&other.sx, &EPSILON) &&
+ self.sy.approx_eq_eps(&other.sy, &EPSILON) &&
+ self.tx.approx_eq_eps(&other.tx, &EPSILON) &&
+ self.ty.approx_eq_eps(&other.ty, &EPSILON)
+ }
+}
+
+/// A comparable / hashable version of a coordinate space mapping. Used to determine
+/// if a transform dependency for a tile has changed.
+#[derive(Debug, PartialEq, Clone)]
+enum TransformKey {
+ Local,
+ ScaleOffset {
+ so: ScaleOffsetKey,
+ },
+ Transform {
+ m: MatrixKey,
+ }
+}
+
+impl<Src, Dst> From<CoordinateSpaceMapping<Src, Dst>> for TransformKey {
+ fn from(transform: CoordinateSpaceMapping<Src, Dst>) -> TransformKey {
+ match transform {
+ CoordinateSpaceMapping::Local => {
+ TransformKey::Local
+ }
+ CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
+ TransformKey::ScaleOffset {
+ so: ScaleOffsetKey {
+ sx: scale_offset.scale.x,
+ sy: scale_offset.scale.y,
+ tx: scale_offset.offset.x,
+ ty: scale_offset.offset.y,
+ }
+ }
+ }
+ CoordinateSpaceMapping::Transform(ref m) => {
+ TransformKey::Transform {
+ m: MatrixKey {
+ m: m.to_array(),
+ },
+ }
+ }
+ }
+ }
+}
+
+/// Unit for tile coordinates.
+#[derive(Hash, Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
+pub struct TileCoordinate;
+
+// Geometry types for tile coordinates.
+pub type TileOffset = Point2D<i32, TileCoordinate>;
+pub type TileRect = Box2D<i32, TileCoordinate>;
+
+/// The maximum number of compositor surfaces that are allowed per picture cache. This
+/// is an arbitrary number that should be enough for common cases, but low enough to
+/// prevent performance and memory usage drastically degrading in pathological cases.
+pub const MAX_COMPOSITOR_SURFACES: usize = 4;
+
+/// The size in device pixels of a normal cached tile.
+pub const TILE_SIZE_DEFAULT: DeviceIntSize = DeviceIntSize {
+ width: 1024,
+ height: 512,
+ _unit: marker::PhantomData,
+};
+
+/// The size in device pixels of a tile for horizontal scroll bars
+pub const TILE_SIZE_SCROLLBAR_HORIZONTAL: DeviceIntSize = DeviceIntSize {
+ width: 1024,
+ height: 32,
+ _unit: marker::PhantomData,
+};
+
+/// The size in device pixels of a tile for vertical scroll bars
+pub const TILE_SIZE_SCROLLBAR_VERTICAL: DeviceIntSize = DeviceIntSize {
+ width: 32,
+ height: 1024,
+ _unit: marker::PhantomData,
+};
+
+/// The maximum size per axis of a surface,
+/// in WorldPixel coordinates.
+const MAX_SURFACE_SIZE: usize = 4096;
+/// Maximum size of a compositor surface.
+const MAX_COMPOSITOR_SURFACES_SIZE: f32 = 8192.0;
+
+/// Used to get unique tile IDs, even when the tile cache is
+/// destroyed between display lists / scenes.
+static NEXT_TILE_ID: AtomicUsize = AtomicUsize::new(0);
+
+fn clamp(value: i32, low: i32, high: i32) -> i32 {
+ value.max(low).min(high)
+}
+
+fn clampf(value: f32, low: f32, high: f32) -> f32 {
+ value.max(low).min(high)
+}
+
+/// An index into the prims array in a TileDescriptor.
+#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PrimitiveDependencyIndex(pub u32);
+
+/// Information about the state of a binding.
+#[derive(Debug)]
+pub struct BindingInfo<T> {
+ /// The current value retrieved from dynamic scene properties.
+ value: T,
+ /// True if it was changed (or is new) since the last frame build.
+ changed: bool,
+}
+
+/// Information stored in a tile descriptor for a binding.
+#[derive(Debug, PartialEq, Clone, Copy, PeekPoke)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum Binding<T> {
+ Value(T),
+ Binding(PropertyBindingId),
+}
+
+impl<T: Default> Default for Binding<T> {
+ fn default() -> Self {
+ Binding::Value(T::default())
+ }
+}
+
+impl<T> From<PropertyBinding<T>> for Binding<T> {
+ fn from(binding: PropertyBinding<T>) -> Binding<T> {
+ match binding {
+ PropertyBinding::Binding(key, _) => Binding::Binding(key.id),
+ PropertyBinding::Value(value) => Binding::Value(value),
+ }
+ }
+}
+
+pub type OpacityBinding = Binding<f32>;
+pub type OpacityBindingInfo = BindingInfo<f32>;
+
+pub type ColorBinding = Binding<ColorU>;
+pub type ColorBindingInfo = BindingInfo<ColorU>;
+
+#[derive(PeekPoke)]
+enum PrimitiveDependency {
+ OpacityBinding {
+ binding: OpacityBinding,
+ },
+ ColorBinding {
+ binding: ColorBinding,
+ },
+ SpatialNode {
+ index: SpatialNodeIndex,
+ },
+ Clip {
+ clip: ItemUid,
+ },
+ Image {
+ image: ImageDependency,
+ },
+}
+
+/// A dependency for a transform is defined by the spatial node index + frame it was used
+#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PeekPoke, Default)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SpatialNodeKey {
+ spatial_node_index: SpatialNodeIndex,
+ frame_id: FrameId,
+}
+
+/// A helper for comparing spatial nodes between frames. The comparisons
+/// are done by-value, so that if the shape of the spatial node tree
+/// changes, invalidations aren't done simply due to the spatial node
+/// index changing between display lists.
+struct SpatialNodeComparer {
+ /// The root spatial node index of the tile cache
+ ref_spatial_node_index: SpatialNodeIndex,
+ /// Maintains a map of currently active transform keys
+ spatial_nodes: FastHashMap<SpatialNodeKey, TransformKey>,
+ /// A cache of recent comparisons between prev and current spatial nodes
+ compare_cache: FastHashMap<(SpatialNodeKey, SpatialNodeKey), bool>,
+ /// A set of frames that we need to retain spatial node entries for
+ referenced_frames: FastHashSet<FrameId>,
+}
+
+impl SpatialNodeComparer {
+ /// Construct a new comparer
+ fn new() -> Self {
+ SpatialNodeComparer {
+ ref_spatial_node_index: SpatialNodeIndex::INVALID,
+ spatial_nodes: FastHashMap::default(),
+ compare_cache: FastHashMap::default(),
+ referenced_frames: FastHashSet::default(),
+ }
+ }
+
+ /// Advance to the next frame
+ fn next_frame(
+ &mut self,
+ ref_spatial_node_index: SpatialNodeIndex,
+ ) {
+ // Drop any node information for unreferenced frames, to ensure that the
+ // hashmap doesn't grow indefinitely!
+ let referenced_frames = &self.referenced_frames;
+ self.spatial_nodes.retain(|key, _| {
+ referenced_frames.contains(&key.frame_id)
+ });
+
+ // Update the root spatial node for this comparer
+ self.ref_spatial_node_index = ref_spatial_node_index;
+ self.compare_cache.clear();
+ self.referenced_frames.clear();
+ }
+
+ /// Register a transform that is used, and build the transform key for it if new.
+ fn register_used_transform(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ frame_id: FrameId,
+ spatial_tree: &SpatialTree,
+ ) {
+ let key = SpatialNodeKey {
+ spatial_node_index,
+ frame_id,
+ };
+
+ if let Entry::Vacant(entry) = self.spatial_nodes.entry(key) {
+ entry.insert(
+ get_transform_key(
+ spatial_node_index,
+ self.ref_spatial_node_index,
+ spatial_tree,
+ )
+ );
+ }
+ }
+
+ /// Return true if the transforms for two given spatial nodes are considered equivalent
+ fn are_transforms_equivalent(
+ &mut self,
+ prev_spatial_node_key: &SpatialNodeKey,
+ curr_spatial_node_key: &SpatialNodeKey,
+ ) -> bool {
+ let key = (*prev_spatial_node_key, *curr_spatial_node_key);
+ let spatial_nodes = &self.spatial_nodes;
+
+ *self.compare_cache
+ .entry(key)
+ .or_insert_with(|| {
+ let prev = &spatial_nodes[&prev_spatial_node_key];
+ let curr = &spatial_nodes[&curr_spatial_node_key];
+ curr == prev
+ })
+ }
+
+ /// Ensure that the comparer won't GC any nodes for a given frame id
+ fn retain_for_frame(&mut self, frame_id: FrameId) {
+ self.referenced_frames.insert(frame_id);
+ }
+}
+
+// Immutable context passed to picture cache tiles during pre_update
+struct TilePreUpdateContext {
+ /// Maps from picture cache coords -> world space coords.
+ pic_to_world_mapper: SpaceMapper<PicturePixel, WorldPixel>,
+
+ /// The optional background color of the picture cache instance
+ background_color: Option<ColorF>,
+
+ /// The visible part of the screen in world coords.
+ global_screen_world_rect: WorldRect,
+
+ /// Current size of tiles in picture units.
+ tile_size: PictureSize,
+
+ /// The current frame id for this picture cache
+ frame_id: FrameId,
+}
+
+// Immutable context passed to picture cache tiles during update_dirty_and_valid_rects
+struct TileUpdateDirtyContext<'a> {
+ /// Maps from picture cache coords -> world space coords.
+ pic_to_world_mapper: SpaceMapper<PicturePixel, WorldPixel>,
+
+ /// Global scale factor from world -> device pixels.
+ global_device_pixel_scale: DevicePixelScale,
+
+ /// Information about opacity bindings from the picture cache.
+ opacity_bindings: &'a FastHashMap<PropertyBindingId, OpacityBindingInfo>,
+
+ /// Information about color bindings from the picture cache.
+ color_bindings: &'a FastHashMap<PropertyBindingId, ColorBindingInfo>,
+
+ /// The local rect of the overall picture cache
+ local_rect: PictureRect,
+
+ /// If true, the scale factor of the root transform for this picture
+ /// cache changed, so we need to invalidate the tile and re-render.
+ invalidate_all: bool,
+}
+
+// Mutable state passed to picture cache tiles during update_dirty_and_valid_rects
+struct TileUpdateDirtyState<'a> {
+ /// Allow access to the texture cache for requesting tiles
+ resource_cache: &'a mut ResourceCache,
+
+ /// Current configuration and setup for compositing all the picture cache tiles in renderer.
+ composite_state: &'a mut CompositeState,
+
+ /// A cache of comparison results to avoid re-computation during invalidation.
+ compare_cache: &'a mut FastHashMap<PrimitiveComparisonKey, PrimitiveCompareResult>,
+
+ /// Information about transform node differences from last frame.
+ spatial_node_comparer: &'a mut SpatialNodeComparer,
+}
+
+// Immutable context passed to picture cache tiles during post_update
+struct TilePostUpdateContext {
+ /// The local clip rect (in picture space) of the entire picture cache
+ local_clip_rect: PictureRect,
+
+ /// The calculated backdrop information for this cache instance.
+ backdrop: Option<BackdropInfo>,
+
+ /// Current size in device pixels of tiles for this cache
+ current_tile_size: DeviceIntSize,
+
+ /// Pre-allocated z-id to assign to tiles during post_update.
+ z_id: ZBufferId,
+}
+
+// Mutable state passed to picture cache tiles during post_update
+struct TilePostUpdateState<'a> {
+ /// Allow access to the texture cache for requesting tiles
+ resource_cache: &'a mut ResourceCache,
+
+ /// Current configuration and setup for compositing all the picture cache tiles in renderer.
+ composite_state: &'a mut CompositeState,
+}
+
+/// Information about the dependencies of a single primitive instance.
+struct PrimitiveDependencyInfo {
+ /// Unique content identifier of the primitive.
+ prim_uid: ItemUid,
+
+ /// The (conservative) clipped area in picture space this primitive occupies.
+ prim_clip_box: PictureBox2D,
+
+ /// Image keys this primitive depends on.
+ images: SmallVec<[ImageDependency; 8]>,
+
+ /// Opacity bindings this primitive depends on.
+ opacity_bindings: SmallVec<[OpacityBinding; 4]>,
+
+ /// Color binding this primitive depends on.
+ color_binding: Option<ColorBinding>,
+
+ /// Clips that this primitive depends on.
+ clips: SmallVec<[ItemUid; 8]>,
+
+ /// Spatial nodes references by the clip dependencies of this primitive.
+ spatial_nodes: SmallVec<[SpatialNodeIndex; 4]>,
+}
+
+impl PrimitiveDependencyInfo {
+ /// Construct dependency info for a new primitive.
+ fn new(
+ prim_uid: ItemUid,
+ prim_clip_box: PictureBox2D,
+ ) -> Self {
+ PrimitiveDependencyInfo {
+ prim_uid,
+ images: SmallVec::new(),
+ opacity_bindings: SmallVec::new(),
+ color_binding: None,
+ prim_clip_box,
+ clips: SmallVec::new(),
+ spatial_nodes: SmallVec::new(),
+ }
+ }
+}
+
+/// A stable ID for a given tile, to help debugging. These are also used
+/// as unique identifiers for tile surfaces when using a native compositor.
+#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct TileId(pub usize);
+
+/// Uniquely identifies a tile within a picture cache slice
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, PartialEq, Hash, Eq)]
+pub struct TileKey {
+ // Tile index (x,y)
+ pub tile_offset: TileOffset,
+ // Sub-slice (z)
+ pub sub_slice_index: SubSliceIndex,
+}
+
+/// A descriptor for the kind of texture that a picture cache tile will
+/// be drawn into.
+#[derive(Debug)]
+pub enum SurfaceTextureDescriptor {
+ /// When using the WR compositor, the tile is drawn into an entry
+ /// in the WR texture cache.
+ TextureCache {
+ handle: Option<PictureCacheTextureHandle>,
+ },
+ /// When using an OS compositor, the tile is drawn into a native
+ /// surface identified by arbitrary id.
+ Native {
+ /// The arbitrary id of this tile.
+ id: Option<NativeTileId>,
+ },
+}
+
+/// This is the same as a `SurfaceTextureDescriptor` but has been resolved
+/// into a texture cache handle (if appropriate) that can be used by the
+/// batching and compositing code in the renderer.
+#[derive(Clone, Debug, Eq, PartialEq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum ResolvedSurfaceTexture {
+ TextureCache {
+ /// The texture ID to draw to.
+ texture: TextureSource,
+ },
+ Native {
+ /// The arbitrary id of this tile.
+ id: NativeTileId,
+ /// The size of the tile in device pixels.
+ size: DeviceIntSize,
+ }
+}
+
+impl SurfaceTextureDescriptor {
+ /// Create a resolved surface texture for this descriptor
+ pub fn resolve(
+ &self,
+ resource_cache: &ResourceCache,
+ size: DeviceIntSize,
+ ) -> ResolvedSurfaceTexture {
+ match self {
+ SurfaceTextureDescriptor::TextureCache { handle } => {
+ let texture = resource_cache
+ .picture_textures
+ .get_texture_source(handle.as_ref().unwrap());
+
+ ResolvedSurfaceTexture::TextureCache { texture }
+ }
+ SurfaceTextureDescriptor::Native { id } => {
+ ResolvedSurfaceTexture::Native {
+ id: id.expect("bug: native surface not allocated"),
+ size,
+ }
+ }
+ }
+ }
+}
+
+/// The backing surface for this tile.
+#[derive(Debug)]
+pub enum TileSurface {
+ Texture {
+ /// Descriptor for the surface that this tile draws into.
+ descriptor: SurfaceTextureDescriptor,
+ },
+ Color {
+ color: ColorF,
+ },
+ Clear,
+}
+
+impl TileSurface {
+ fn kind(&self) -> &'static str {
+ match *self {
+ TileSurface::Color { .. } => "Color",
+ TileSurface::Texture { .. } => "Texture",
+ TileSurface::Clear => "Clear",
+ }
+ }
+}
+
+/// Optional extra information returned by is_same when
+/// logging is enabled.
+#[derive(Debug, Copy, Clone, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum CompareHelperResult<T> {
+ /// Primitives match
+ Equal,
+ /// Counts differ
+ Count {
+ prev_count: u8,
+ curr_count: u8,
+ },
+ /// Sentinel
+ Sentinel,
+ /// Two items are not equal
+ NotEqual {
+ prev: T,
+ curr: T,
+ },
+ /// User callback returned true on item
+ PredicateTrue {
+ curr: T
+ },
+}
+
+/// The result of a primitive dependency comparison. Size is a u8
+/// since this is a hot path in the code, and keeping the data small
+/// is a performance win.
+#[derive(Debug, Copy, Clone, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(u8)]
+pub enum PrimitiveCompareResult {
+ /// Primitives match
+ Equal,
+ /// Something in the PrimitiveDescriptor was different
+ Descriptor,
+ /// The clip node content or spatial node changed
+ Clip,
+ /// The value of the transform changed
+ Transform,
+ /// An image dependency was dirty
+ Image,
+ /// The value of an opacity binding changed
+ OpacityBinding,
+ /// The value of a color binding changed
+ ColorBinding,
+}
+
+/// Debugging information about why a tile was invalidated
+#[derive(Debug,Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum InvalidationReason {
+ /// The background color changed
+ BackgroundColor,
+ /// The opaque state of the backing native surface changed
+ SurfaceOpacityChanged,
+ /// There was no backing texture (evicted or never rendered)
+ NoTexture,
+ /// There was no backing native surface (never rendered, or recreated)
+ NoSurface,
+ /// The primitive count in the dependency list was different
+ PrimCount,
+ /// The content of one of the primitives was different
+ Content,
+ // The compositor type changed
+ CompositorKindChanged,
+ // The valid region of the tile changed
+ ValidRectChanged,
+ // The overall scale of the picture cache changed
+ ScaleChanged,
+ // The content of the sampling surface changed
+ SurfaceContentChanged,
+}
+
+/// Information about a cached tile.
+pub struct Tile {
+ /// The grid position of this tile within the picture cache
+ pub tile_offset: TileOffset,
+ /// The current world rect of this tile.
+ pub world_tile_rect: WorldRect,
+ /// The current local rect of this tile.
+ pub local_tile_rect: PictureRect,
+ /// The picture space dirty rect for this tile.
+ pub local_dirty_rect: PictureRect,
+ /// The device space dirty rect for this tile.
+ /// TODO(gw): We have multiple dirty rects available due to the quadtree above. In future,
+ /// expose these as multiple dirty rects, which will help in some cases.
+ pub device_dirty_rect: DeviceRect,
+ /// World space rect that contains valid pixels region of this tile.
+ pub world_valid_rect: WorldRect,
+ /// Device space rect that contains valid pixels region of this tile.
+ pub device_valid_rect: DeviceRect,
+ /// Uniquely describes the content of this tile, in a way that can be
+ /// (reasonably) efficiently hashed and compared.
+ pub current_descriptor: TileDescriptor,
+ /// The content descriptor for this tile from the previous frame.
+ pub prev_descriptor: TileDescriptor,
+ /// Handle to the backing surface for this tile.
+ pub surface: Option<TileSurface>,
+ /// If true, this tile is marked valid, and the existing texture
+ /// cache handle can be used. Tiles are invalidated during the
+ /// build_dirty_regions method.
+ pub is_valid: bool,
+ /// If true, this tile intersects with the currently visible screen
+ /// rect, and will be drawn.
+ pub is_visible: bool,
+ /// The tile id is stable between display lists and / or frames,
+ /// if the tile is retained. Useful for debugging tile evictions.
+ pub id: TileId,
+ /// If true, the tile was determined to be opaque, which means blending
+ /// can be disabled when drawing it.
+ pub is_opaque: bool,
+ /// Root node of the quadtree dirty rect tracker.
+ root: TileNode,
+ /// The last rendered background color on this tile.
+ background_color: Option<ColorF>,
+ /// The first reason the tile was invalidated this frame.
+ invalidation_reason: Option<InvalidationReason>,
+ /// The local space valid rect for all primitives that affect this tile.
+ pub local_valid_rect: PictureBox2D,
+ /// z-buffer id for this tile
+ pub z_id: ZBufferId,
+ pub sub_graphs: Vec<(PictureRect, Vec<(PictureCompositeMode, SurfaceIndex)>)>,
+}
+
+impl Tile {
+ /// Construct a new, invalid tile.
+ fn new(tile_offset: TileOffset) -> Self {
+ let id = TileId(NEXT_TILE_ID.fetch_add(1, Ordering::Relaxed));
+
+ Tile {
+ tile_offset,
+ local_tile_rect: PictureRect::zero(),
+ world_tile_rect: WorldRect::zero(),
+ world_valid_rect: WorldRect::zero(),
+ device_valid_rect: DeviceRect::zero(),
+ local_dirty_rect: PictureRect::zero(),
+ device_dirty_rect: DeviceRect::zero(),
+ surface: None,
+ current_descriptor: TileDescriptor::new(),
+ prev_descriptor: TileDescriptor::new(),
+ is_valid: false,
+ is_visible: false,
+ id,
+ is_opaque: false,
+ root: TileNode::new_leaf(Vec::new()),
+ background_color: None,
+ invalidation_reason: None,
+ local_valid_rect: PictureBox2D::zero(),
+ z_id: ZBufferId::invalid(),
+ sub_graphs: Vec::new(),
+ }
+ }
+
+ /// Print debug information about this tile to a tree printer.
+ fn print(&self, pt: &mut dyn PrintTreePrinter) {
+ pt.new_level(format!("Tile {:?}", self.id));
+ pt.add_item(format!("local_tile_rect: {:?}", self.local_tile_rect));
+ pt.add_item(format!("background_color: {:?}", self.background_color));
+ pt.add_item(format!("invalidation_reason: {:?}", self.invalidation_reason));
+ self.current_descriptor.print(pt);
+ pt.end_level();
+ }
+
+ /// Check if the content of the previous and current tile descriptors match
+ fn update_dirty_rects(
+ &mut self,
+ ctx: &TileUpdateDirtyContext,
+ state: &mut TileUpdateDirtyState,
+ invalidation_reason: &mut Option<InvalidationReason>,
+ frame_context: &FrameVisibilityContext,
+ ) -> PictureRect {
+ let mut prim_comparer = PrimitiveComparer::new(
+ &self.prev_descriptor,
+ &self.current_descriptor,
+ state.resource_cache,
+ state.spatial_node_comparer,
+ ctx.opacity_bindings,
+ ctx.color_bindings,
+ );
+
+ let mut dirty_rect = PictureBox2D::zero();
+ self.root.update_dirty_rects(
+ &self.prev_descriptor.prims,
+ &self.current_descriptor.prims,
+ &mut prim_comparer,
+ &mut dirty_rect,
+ state.compare_cache,
+ invalidation_reason,
+ frame_context,
+ );
+
+ dirty_rect
+ }
+
+ /// Invalidate a tile based on change in content. This
+ /// must be called even if the tile is not currently
+ /// visible on screen. We might be able to improve this
+ /// later by changing how ComparableVec is used.
+ fn update_content_validity(
+ &mut self,
+ ctx: &TileUpdateDirtyContext,
+ state: &mut TileUpdateDirtyState,
+ frame_context: &FrameVisibilityContext,
+ ) {
+ // Check if the contents of the primitives, clips, and
+ // other dependencies are the same.
+ state.compare_cache.clear();
+ let mut invalidation_reason = None;
+ let dirty_rect = self.update_dirty_rects(
+ ctx,
+ state,
+ &mut invalidation_reason,
+ frame_context,
+ );
+ if !dirty_rect.is_empty() {
+ self.invalidate(
+ Some(dirty_rect),
+ invalidation_reason.expect("bug: no invalidation_reason"),
+ );
+ }
+ if ctx.invalidate_all {
+ self.invalidate(None, InvalidationReason::ScaleChanged);
+ }
+ // TODO(gw): We can avoid invalidating the whole tile in some cases here,
+ // but it should be a fairly rare invalidation case.
+ if self.current_descriptor.local_valid_rect != self.prev_descriptor.local_valid_rect {
+ self.invalidate(None, InvalidationReason::ValidRectChanged);
+ state.composite_state.dirty_rects_are_valid = false;
+ }
+ }
+
+ /// Invalidate this tile. If `invalidation_rect` is None, the entire
+ /// tile is invalidated.
+ fn invalidate(
+ &mut self,
+ invalidation_rect: Option<PictureRect>,
+ reason: InvalidationReason,
+ ) {
+ self.is_valid = false;
+
+ match invalidation_rect {
+ Some(rect) => {
+ self.local_dirty_rect = self.local_dirty_rect.union(&rect);
+ }
+ None => {
+ self.local_dirty_rect = self.local_tile_rect;
+ }
+ }
+
+ if self.invalidation_reason.is_none() {
+ self.invalidation_reason = Some(reason);
+ }
+ }
+
+ /// Called during pre_update of a tile cache instance. Allows the
+ /// tile to setup state before primitive dependency calculations.
+ fn pre_update(
+ &mut self,
+ ctx: &TilePreUpdateContext,
+ ) {
+ self.local_tile_rect = PictureRect::from_origin_and_size(
+ PicturePoint::new(
+ self.tile_offset.x as f32 * ctx.tile_size.width,
+ self.tile_offset.y as f32 * ctx.tile_size.height,
+ ),
+ ctx.tile_size,
+ );
+ // TODO(gw): This is a hack / fix for Box2D::union in euclid not working with
+ // zero sized rect accumulation. Once that lands, we'll revert this
+ // to be zero.
+ self.local_valid_rect = PictureBox2D::new(
+ PicturePoint::new( 1.0e32, 1.0e32),
+ PicturePoint::new(-1.0e32, -1.0e32),
+ );
+ self.invalidation_reason = None;
+ self.sub_graphs.clear();
+
+ self.world_tile_rect = ctx.pic_to_world_mapper
+ .map(&self.local_tile_rect)
+ .expect("bug: map local tile rect");
+
+ // Check if this tile is currently on screen.
+ self.is_visible = self.world_tile_rect.intersects(&ctx.global_screen_world_rect);
+
+ // If the tile isn't visible, early exit, skipping the normal set up to
+ // validate dependencies. Instead, we will only compare the current tile
+ // dependencies the next time it comes into view.
+ if !self.is_visible {
+ return;
+ }
+
+ if ctx.background_color != self.background_color {
+ self.invalidate(None, InvalidationReason::BackgroundColor);
+ self.background_color = ctx.background_color;
+ }
+
+ // Clear any dependencies so that when we rebuild them we
+ // can compare if the tile has the same content.
+ mem::swap(
+ &mut self.current_descriptor,
+ &mut self.prev_descriptor,
+ );
+ self.current_descriptor.clear();
+ self.root.clear(self.local_tile_rect);
+
+ // Since this tile is determined to be visible, it will get updated
+ // dependencies, so update the frame id we are storing dependencies for.
+ self.current_descriptor.last_updated_frame_id = ctx.frame_id;
+ }
+
+ /// Add dependencies for a given primitive to this tile.
+ fn add_prim_dependency(
+ &mut self,
+ info: &PrimitiveDependencyInfo,
+ ) {
+ // If this tile isn't currently visible, we don't want to update the dependencies
+ // for this tile, as an optimization, since it won't be drawn anyway.
+ if !self.is_visible {
+ return;
+ }
+
+ // Incorporate the bounding rect of the primitive in the local valid rect
+ // for this tile. This is used to minimize the size of the scissor rect
+ // during rasterization and the draw rect during composition of partial tiles.
+ self.local_valid_rect = self.local_valid_rect.union(&info.prim_clip_box);
+
+ // TODO(gw): The prim_clip_rect can be impacted by the clip rect of the display port,
+ // which can cause invalidations when a new display list with changed
+ // display port is received. To work around this, clamp the prim clip rect
+ // to the tile boundaries - if the clip hasn't affected the tile, then the
+ // changed clip can't affect the content of the primitive on this tile.
+ // In future, we could consider supplying the display port clip from Gecko
+ // in a different way (e.g. as a scroll frame clip) which still provides
+ // the desired clip for checkerboarding, but doesn't require this extra
+ // work below.
+
+ // TODO(gw): This is a hot part of the code - we could probably optimize further by:
+ // - Using min/max instead of clamps below (if we guarantee the rects are well formed)
+
+ let tile_p0 = self.local_tile_rect.min;
+ let tile_p1 = self.local_tile_rect.max;
+
+ let prim_clip_box = PictureBox2D::new(
+ PicturePoint::new(
+ clampf(info.prim_clip_box.min.x, tile_p0.x, tile_p1.x),
+ clampf(info.prim_clip_box.min.y, tile_p0.y, tile_p1.y),
+ ),
+ PicturePoint::new(
+ clampf(info.prim_clip_box.max.x, tile_p0.x, tile_p1.x),
+ clampf(info.prim_clip_box.max.y, tile_p0.y, tile_p1.y),
+ ),
+ );
+
+ // Update the tile descriptor, used for tile comparison during scene swaps.
+ let prim_index = PrimitiveDependencyIndex(self.current_descriptor.prims.len() as u32);
+
+ // Encode the deps for this primitive in the `dep_data` byte buffer
+ let dep_offset = self.current_descriptor.dep_data.len() as u32;
+ let mut dep_count = 0;
+
+ for clip in &info.clips {
+ dep_count += 1;
+ poke_into_vec(
+ &PrimitiveDependency::Clip {
+ clip: *clip,
+ },
+ &mut self.current_descriptor.dep_data,
+ );
+ }
+
+ for spatial_node_index in &info.spatial_nodes {
+ dep_count += 1;
+ poke_into_vec(
+ &PrimitiveDependency::SpatialNode {
+ index: *spatial_node_index,
+ },
+ &mut self.current_descriptor.dep_data,
+ );
+ }
+
+ for image in &info.images {
+ dep_count += 1;
+ poke_into_vec(
+ &PrimitiveDependency::Image {
+ image: *image,
+ },
+ &mut self.current_descriptor.dep_data,
+ );
+ }
+
+ for binding in &info.opacity_bindings {
+ dep_count += 1;
+ poke_into_vec(
+ &PrimitiveDependency::OpacityBinding {
+ binding: *binding,
+ },
+ &mut self.current_descriptor.dep_data,
+ );
+ }
+
+ if let Some(ref binding) = info.color_binding {
+ dep_count += 1;
+ poke_into_vec(
+ &PrimitiveDependency::ColorBinding {
+ binding: *binding,
+ },
+ &mut self.current_descriptor.dep_data,
+ );
+ }
+
+ self.current_descriptor.prims.push(PrimitiveDescriptor {
+ prim_uid: info.prim_uid,
+ prim_clip_box,
+ dep_offset,
+ dep_count,
+ });
+
+ // Add this primitive to the dirty rect quadtree.
+ self.root.add_prim(prim_index, &info.prim_clip_box);
+ }
+
+ /// Called during tile cache instance post_update. Allows invalidation and dirty
+ /// rect calculation after primitive dependencies have been updated.
+ fn update_dirty_and_valid_rects(
+ &mut self,
+ ctx: &TileUpdateDirtyContext,
+ state: &mut TileUpdateDirtyState,
+ frame_context: &FrameVisibilityContext,
+ ) {
+ // Ensure peek-poke constraint is met, that `dep_data` is large enough
+ ensure_red_zone::<PrimitiveDependency>(&mut self.current_descriptor.dep_data);
+
+ // Register the frame id of this tile with the spatial node comparer, to ensure
+ // that it doesn't GC any spatial nodes from the comparer that are referenced
+ // by this tile. Must be done before we early exit below, so that we retain
+ // spatial node info even for tiles that are currently not visible.
+ state.spatial_node_comparer.retain_for_frame(self.current_descriptor.last_updated_frame_id);
+
+ // If tile is not visible, just early out from here - we don't update dependencies
+ // so don't want to invalidate, merge, split etc. The tile won't need to be drawn
+ // (and thus updated / invalidated) until it is on screen again.
+ if !self.is_visible {
+ return;
+ }
+
+ // Calculate the overall valid rect for this tile.
+ self.current_descriptor.local_valid_rect = self.local_valid_rect;
+
+ // TODO(gw): In theory, the local tile rect should always have an
+ // intersection with the overall picture rect. In practice,
+ // due to some accuracy issues with how fract_offset (and
+ // fp accuracy) are used in the calling method, this isn't
+ // always true. In this case, it's safe to set the local
+ // valid rect to zero, which means it will be clipped out
+ // and not affect the scene. In future, we should fix the
+ // accuracy issue above, so that this assumption holds, but
+ // it shouldn't have any noticeable effect on performance
+ // or memory usage (textures should never get allocated).
+ self.current_descriptor.local_valid_rect = self.local_tile_rect
+ .intersection(&ctx.local_rect)
+ .and_then(|r| r.intersection(&self.current_descriptor.local_valid_rect))
+ .unwrap_or_else(PictureRect::zero);
+
+ // The device_valid_rect is referenced during `update_content_validity` so it
+ // must be updated here first.
+ self.world_valid_rect = ctx.pic_to_world_mapper
+ .map(&self.current_descriptor.local_valid_rect)
+ .expect("bug: map local valid rect");
+
+ // The device rect is guaranteed to be aligned on a device pixel - the round
+ // is just to deal with float accuracy. However, the valid rect is not
+ // always aligned to a device pixel. To handle this, round out to get all
+ // required pixels, and intersect with the tile device rect.
+ let device_rect = (self.world_tile_rect * ctx.global_device_pixel_scale).round();
+ self.device_valid_rect = (self.world_valid_rect * ctx.global_device_pixel_scale)
+ .round_out()
+ .intersection(&device_rect)
+ .unwrap_or_else(DeviceRect::zero);
+
+ // Invalidate the tile based on the content changing.
+ self.update_content_validity(ctx, state, frame_context);
+ }
+
+ /// Called during tile cache instance post_update. Allows invalidation and dirty
+ /// rect calculation after primitive dependencies have been updated.
+ fn post_update(
+ &mut self,
+ ctx: &TilePostUpdateContext,
+ state: &mut TilePostUpdateState,
+ frame_context: &FrameVisibilityContext,
+ ) {
+ // If tile is not visible, just early out from here - we don't update dependencies
+ // so don't want to invalidate, merge, split etc. The tile won't need to be drawn
+ // (and thus updated / invalidated) until it is on screen again.
+ if !self.is_visible {
+ return;
+ }
+
+ // If there are no primitives there is no need to draw or cache it.
+ // Bug 1719232 - The final device valid rect does not always describe a non-empty
+ // region. Cull the tile as a workaround.
+ if self.current_descriptor.prims.is_empty() || self.device_valid_rect.is_empty() {
+ // If there is a native compositor surface allocated for this (now empty) tile
+ // it must be freed here, otherwise the stale tile with previous contents will
+ // be composited. If the tile subsequently gets new primitives added to it, the
+ // surface will be re-allocated when it's added to the composite draw list.
+ if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { mut id, .. }, .. }) = self.surface.take() {
+ if let Some(id) = id.take() {
+ state.resource_cache.destroy_compositor_tile(id);
+ }
+ }
+
+ self.is_visible = false;
+ return;
+ }
+
+ // Check if this tile can be considered opaque. Opacity state must be updated only
+ // after all early out checks have been performed. Otherwise, we might miss updating
+ // the native surface next time this tile becomes visible.
+ let clipped_rect = self.current_descriptor.local_valid_rect
+ .intersection(&ctx.local_clip_rect)
+ .unwrap_or_else(PictureRect::zero);
+
+ let has_opaque_bg_color = self.background_color.map_or(false, |c| c.a >= 1.0);
+ let has_opaque_backdrop = ctx.backdrop.map_or(false, |b| b.opaque_rect.contains_box(&clipped_rect));
+ let is_opaque = has_opaque_bg_color || has_opaque_backdrop;
+
+ // Set the correct z_id for this tile
+ self.z_id = ctx.z_id;
+
+ if is_opaque != self.is_opaque {
+ // If opacity changed, the native compositor surface and all tiles get invalidated.
+ // (this does nothing if not using native compositor mode).
+ // TODO(gw): This property probably changes very rarely, so it is OK to invalidate
+ // everything in this case. If it turns out that this isn't true, we could
+ // consider other options, such as per-tile opacity (natively supported
+ // on CoreAnimation, and supported if backed by non-virtual surfaces in
+ // DirectComposition).
+ if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { ref mut id, .. }, .. }) = self.surface {
+ if let Some(id) = id.take() {
+ state.resource_cache.destroy_compositor_tile(id);
+ }
+ }
+
+ // Invalidate the entire tile to force a redraw.
+ self.invalidate(None, InvalidationReason::SurfaceOpacityChanged);
+ self.is_opaque = is_opaque;
+ }
+
+ // Check if the selected composite mode supports dirty rect updates. For Draw composite
+ // mode, we can always update the content with smaller dirty rects, unless there is a
+ // driver bug to workaround. For native composite mode, we can only use dirty rects if
+ // the compositor supports partial surface updates.
+ let (supports_dirty_rects, supports_simple_prims) = match state.composite_state.compositor_kind {
+ CompositorKind::Draw { .. } => {
+ (frame_context.config.gpu_supports_render_target_partial_update, true)
+ }
+ CompositorKind::Native { capabilities, .. } => {
+ (capabilities.max_update_rects > 0, false)
+ }
+ };
+
+ // TODO(gw): Consider using smaller tiles and/or tile splits for
+ // native compositors that don't support dirty rects.
+ if supports_dirty_rects {
+ // Only allow splitting for normal content sized tiles
+ if ctx.current_tile_size == state.resource_cache.picture_textures.default_tile_size() {
+ let max_split_level = 3;
+
+ // Consider splitting / merging dirty regions
+ self.root.maybe_merge_or_split(
+ 0,
+ &self.current_descriptor.prims,
+ max_split_level,
+ );
+ }
+ }
+
+ // The dirty rect will be set correctly by now. If the underlying platform
+ // doesn't support partial updates, and this tile isn't valid, force the dirty
+ // rect to be the size of the entire tile.
+ if !self.is_valid && !supports_dirty_rects {
+ self.local_dirty_rect = self.local_tile_rect;
+ }
+
+ // See if this tile is a simple color, in which case we can just draw
+ // it as a rect, and avoid allocating a texture surface and drawing it.
+ // TODO(gw): Initial native compositor interface doesn't support simple
+ // color tiles. We can definitely support this in DC, so this
+ // should be added as a follow up.
+ let is_simple_prim =
+ ctx.backdrop.map_or(false, |b| b.kind.is_some()) &&
+ self.current_descriptor.prims.len() == 1 &&
+ self.is_opaque &&
+ supports_simple_prims;
+
+ // Set up the backing surface for this tile.
+ let surface = if is_simple_prim {
+ // If we determine the tile can be represented by a color, set the
+ // surface unconditionally (this will drop any previously used
+ // texture cache backing surface).
+ match ctx.backdrop.unwrap().kind {
+ Some(BackdropKind::Color { color }) => {
+ TileSurface::Color {
+ color,
+ }
+ }
+ Some(BackdropKind::Clear) => {
+ TileSurface::Clear
+ }
+ None => {
+ // This should be prevented by the is_simple_prim check above.
+ unreachable!();
+ }
+ }
+ } else {
+ // If this tile will be backed by a surface, we want to retain
+ // the texture handle from the previous frame, if possible. If
+ // the tile was previously a color, or not set, then just set
+ // up a new texture cache handle.
+ match self.surface.take() {
+ Some(TileSurface::Texture { descriptor }) => {
+ // Reuse the existing descriptor and vis mask
+ TileSurface::Texture {
+ descriptor,
+ }
+ }
+ Some(TileSurface::Color { .. }) | Some(TileSurface::Clear) | None => {
+ // This is the case where we are constructing a tile surface that
+ // involves drawing to a texture. Create the correct surface
+ // descriptor depending on the compositing mode that will read
+ // the output.
+ let descriptor = match state.composite_state.compositor_kind {
+ CompositorKind::Draw { .. } => {
+ // For a texture cache entry, create an invalid handle that
+ // will be allocated when update_picture_cache is called.
+ SurfaceTextureDescriptor::TextureCache {
+ handle: None,
+ }
+ }
+ CompositorKind::Native { .. } => {
+ // Create a native surface surface descriptor, but don't allocate
+ // a surface yet. The surface is allocated *after* occlusion
+ // culling occurs, so that only visible tiles allocate GPU memory.
+ SurfaceTextureDescriptor::Native {
+ id: None,
+ }
+ }
+ };
+
+ TileSurface::Texture {
+ descriptor,
+ }
+ }
+ }
+ };
+
+ // Store the current surface backing info for use during batching.
+ self.surface = Some(surface);
+ }
+}
+
+/// Defines a key that uniquely identifies a primitive instance.
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PrimitiveDescriptor {
+ pub prim_uid: ItemUid,
+ pub prim_clip_box: PictureBox2D,
+ // TODO(gw): These two fields could be packed as a u24/u8
+ pub dep_offset: u32,
+ pub dep_count: u32,
+}
+
+impl PartialEq for PrimitiveDescriptor {
+ fn eq(&self, other: &Self) -> bool {
+ const EPSILON: f32 = 0.001;
+
+ if self.prim_uid != other.prim_uid {
+ return false;
+ }
+
+ if !self.prim_clip_box.min.x.approx_eq_eps(&other.prim_clip_box.min.x, &EPSILON) {
+ return false;
+ }
+ if !self.prim_clip_box.min.y.approx_eq_eps(&other.prim_clip_box.min.y, &EPSILON) {
+ return false;
+ }
+ if !self.prim_clip_box.max.x.approx_eq_eps(&other.prim_clip_box.max.x, &EPSILON) {
+ return false;
+ }
+ if !self.prim_clip_box.max.y.approx_eq_eps(&other.prim_clip_box.max.y, &EPSILON) {
+ return false;
+ }
+
+ if self.dep_count != other.dep_count {
+ return false;
+ }
+
+ true
+ }
+}
+
+/// Uniquely describes the content of this tile, in a way that can be
+/// (reasonably) efficiently hashed and compared.
+#[cfg_attr(any(feature="capture",feature="replay"), derive(Clone))]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct TileDescriptor {
+ /// List of primitive instance unique identifiers. The uid is guaranteed
+ /// to uniquely describe the content of the primitive template, while
+ /// the other parameters describe the clip chain and instance params.
+ prims: Vec<PrimitiveDescriptor>,
+
+ /// Picture space rect that contains valid pixels region of this tile.
+ pub local_valid_rect: PictureRect,
+
+ /// The last frame this tile had its dependencies updated (dependency updating is
+ /// skipped if a tile is off-screen).
+ last_updated_frame_id: FrameId,
+
+ /// Packed per-prim dependency information
+ dep_data: Vec<u8>,
+}
+
+impl TileDescriptor {
+ fn new() -> Self {
+ TileDescriptor {
+ local_valid_rect: PictureRect::zero(),
+ dep_data: Vec::new(),
+ prims: Vec::new(),
+ last_updated_frame_id: FrameId::INVALID,
+ }
+ }
+
+ /// Print debug information about this tile descriptor to a tree printer.
+ fn print(&self, pt: &mut dyn PrintTreePrinter) {
+ pt.new_level("current_descriptor".to_string());
+
+ pt.new_level("prims".to_string());
+ for prim in &self.prims {
+ pt.new_level(format!("prim uid={}", prim.prim_uid.get_uid()));
+ pt.add_item(format!("clip: p0={},{} p1={},{}",
+ prim.prim_clip_box.min.x,
+ prim.prim_clip_box.min.y,
+ prim.prim_clip_box.max.x,
+ prim.prim_clip_box.max.y,
+ ));
+ pt.end_level();
+ }
+ pt.end_level();
+
+ pt.end_level();
+ }
+
+ /// Clear the dependency information for a tile, when the dependencies
+ /// are being rebuilt.
+ fn clear(&mut self) {
+ self.local_valid_rect = PictureRect::zero();
+ self.prims.clear();
+ self.dep_data.clear();
+ }
+}
+
+/// Represents the dirty region of a tile cache picture.
+#[derive(Clone)]
+pub struct DirtyRegion {
+ /// The overall dirty rect, a combination of dirty_rects
+ pub combined: WorldRect,
+
+ /// Spatial node of the picture cache this region represents
+ spatial_node_index: SpatialNodeIndex,
+}
+
+impl DirtyRegion {
+ /// Construct a new dirty region tracker.
+ pub fn new(
+ spatial_node_index: SpatialNodeIndex,
+ ) -> Self {
+ DirtyRegion {
+ combined: WorldRect::zero(),
+ spatial_node_index,
+ }
+ }
+
+ /// Reset the dirty regions back to empty
+ pub fn reset(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ ) {
+ self.combined = WorldRect::zero();
+ self.spatial_node_index = spatial_node_index;
+ }
+
+ /// Add a dirty region to the tracker. Returns the visibility mask that corresponds to
+ /// this region in the tracker.
+ pub fn add_dirty_region(
+ &mut self,
+ rect_in_pic_space: PictureRect,
+ spatial_tree: &SpatialTree,
+ ) {
+ let map_pic_to_world = SpaceMapper::new_with_target(
+ spatial_tree.root_reference_frame_index(),
+ self.spatial_node_index,
+ WorldRect::max_rect(),
+ spatial_tree,
+ );
+
+ let world_rect = map_pic_to_world
+ .map(&rect_in_pic_space)
+ .expect("bug");
+
+ // Include this in the overall dirty rect
+ self.combined = self.combined.union(&world_rect);
+ }
+}
+
+// TODO(gw): Tidy this up by:
+// - Rename Clear variant to something more appropriate to what it does
+// - Add an Other variant for things like opaque gradient backdrops
+#[derive(Debug, Copy, Clone)]
+pub enum BackdropKind {
+ Color {
+ color: ColorF,
+ },
+ Clear,
+}
+
+/// Stores information about the calculated opaque backdrop of this slice.
+#[derive(Debug, Copy, Clone)]
+pub struct BackdropInfo {
+ /// The picture space rectangle that is known to be opaque. This is used
+ /// to determine where subpixel AA can be used, and where alpha blending
+ /// can be disabled.
+ pub opaque_rect: PictureRect,
+ /// If the backdrop covers the entire slice with an opaque color, this
+ /// will be set and can be used as a clear color for the slice's tiles.
+ pub spanning_opaque_color: Option<ColorF>,
+ /// Kind of the backdrop
+ pub kind: Option<BackdropKind>,
+ /// The picture space rectangle of the backdrop, if kind is set.
+ pub backdrop_rect: PictureRect,
+}
+
+impl BackdropInfo {
+ fn empty() -> Self {
+ BackdropInfo {
+ opaque_rect: PictureRect::zero(),
+ spanning_opaque_color: None,
+ kind: None,
+ backdrop_rect: PictureRect::zero(),
+ }
+ }
+}
+
+/// Represents the native surfaces created for a picture cache, if using
+/// a native compositor. An opaque and alpha surface is always created,
+/// but tiles are added to a surface based on current opacity. If the
+/// calculated opacity of a tile changes, the tile is invalidated and
+/// attached to a different native surface. This means that we don't
+/// need to invalidate the entire surface if only some tiles are changing
+/// opacity. It also means we can take advantage of opaque tiles on cache
+/// slices where only some of the tiles are opaque. There is an assumption
+/// that creating a native surface is cheap, and only when a tile is added
+/// to a surface is there a significant cost. This assumption holds true
+/// for the current native compositor implementations on Windows and Mac.
+pub struct NativeSurface {
+ /// Native surface for opaque tiles
+ pub opaque: NativeSurfaceId,
+ /// Native surface for alpha tiles
+ pub alpha: NativeSurfaceId,
+}
+
+/// Hash key for an external native compositor surface
+#[derive(PartialEq, Eq, Hash)]
+pub struct ExternalNativeSurfaceKey {
+ /// The YUV/RGB image keys that are used to draw this surface.
+ pub image_keys: [ImageKey; 3],
+ /// If this is not an 'external' compositor surface created via
+ /// Compositor::create_external_surface, this is set to the
+ /// current device size of the surface.
+ pub size: Option<DeviceIntSize>,
+}
+
+/// Information about a native compositor surface cached between frames.
+pub struct ExternalNativeSurface {
+ /// If true, the surface was used this frame. Used for a simple form
+ /// of GC to remove old surfaces.
+ pub used_this_frame: bool,
+ /// The native compositor surface handle
+ pub native_surface_id: NativeSurfaceId,
+ /// List of image keys, and current image generations, that are drawn in this surface.
+ /// The image generations are used to check if the compositor surface is dirty and
+ /// needs to be updated.
+ pub image_dependencies: [ImageDependency; 3],
+}
+
+/// The key that identifies a tile cache instance. For now, it's simple the index of
+/// the slice as it was created during scene building.
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SliceId(usize);
+
+impl SliceId {
+ pub fn new(index: usize) -> Self {
+ SliceId(index)
+ }
+}
+
+/// Information that is required to reuse or create a new tile cache. Created
+/// during scene building and passed to the render backend / frame builder.
+pub struct TileCacheParams {
+ // Index of the slice (also effectively the key of the tile cache, though we use SliceId where that matters)
+ pub slice: usize,
+ // Flags describing content of this cache (e.g. scrollbars)
+ pub slice_flags: SliceFlags,
+ // The anchoring spatial node / scroll root
+ pub spatial_node_index: SpatialNodeIndex,
+ // Optional background color of this tilecache. If present, can be used as an optimization
+ // to enable opaque blending and/or subpixel AA in more places.
+ pub background_color: Option<ColorF>,
+ // Node in the clip-tree that defines where we exclude clips from child prims
+ pub shared_clip_node_id: ClipNodeId,
+ // Clip leaf that is used to build the clip-chain for this tile cache.
+ pub shared_clip_leaf_id: Option<ClipLeafId>,
+ // Virtual surface sizes are always square, so this represents both the width and height
+ pub virtual_surface_size: i32,
+ // The number of compositor surfaces that are being requested for this tile cache.
+ // This is only a suggestion - the tile cache will clamp this as a reasonable number
+ // and only promote a limited number of surfaces.
+ pub compositor_surface_count: usize,
+}
+
+/// Defines which sub-slice (effectively a z-index) a primitive exists on within
+/// a picture cache instance.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
+pub struct SubSliceIndex(u8);
+
+impl SubSliceIndex {
+ pub const DEFAULT: SubSliceIndex = SubSliceIndex(0);
+
+ pub fn new(index: usize) -> Self {
+ SubSliceIndex(index as u8)
+ }
+
+ /// Return true if this sub-slice is the primary sub-slice (for now, we assume
+ /// that only the primary sub-slice may be opaque and support subpixel AA, for example).
+ pub fn is_primary(&self) -> bool {
+ self.0 == 0
+ }
+
+ /// Get an array index for this sub-slice
+ pub fn as_usize(&self) -> usize {
+ self.0 as usize
+ }
+}
+
+/// Wrapper struct around an external surface descriptor with a little more information
+/// that the picture caching code needs.
+pub struct CompositorSurface {
+ // External surface descriptor used by compositing logic
+ pub descriptor: ExternalSurfaceDescriptor,
+ // The compositor surface rect + any intersecting prims. Later prims that intersect
+ // with this must be added to the next sub-slice.
+ prohibited_rect: PictureRect,
+ // If the compositor surface content is opaque.
+ pub is_opaque: bool,
+}
+
+/// A SubSlice represents a potentially overlapping set of tiles within a picture cache. Most
+/// picture cache instances will have only a single sub-slice. The exception to this is when
+/// a picture cache has compositor surfaces, in which case sub slices are used to interleave
+/// content under or order the compositor surface(s).
+pub struct SubSlice {
+ /// Hash of tiles present in this picture.
+ pub tiles: FastHashMap<TileOffset, Box<Tile>>,
+ /// The allocated compositor surfaces for this picture cache. May be None if
+ /// not using native compositor, or if the surface was destroyed and needs
+ /// to be reallocated next time this surface contains valid tiles.
+ pub native_surface: Option<NativeSurface>,
+ /// List of compositor surfaces that have been promoted from primitives
+ /// in this tile cache.
+ pub compositor_surfaces: Vec<CompositorSurface>,
+ /// List of visible tiles to be composited for this subslice
+ pub composite_tiles: Vec<CompositeTile>,
+ /// Compositor descriptors of visible, opaque tiles (used by composite_state.push_surface)
+ pub opaque_tile_descriptors: Vec<CompositeTileDescriptor>,
+ /// Compositor descriptors of visible, alpha tiles (used by composite_state.push_surface)
+ pub alpha_tile_descriptors: Vec<CompositeTileDescriptor>,
+}
+
+impl SubSlice {
+ /// Construct a new sub-slice
+ fn new() -> Self {
+ SubSlice {
+ tiles: FastHashMap::default(),
+ native_surface: None,
+ compositor_surfaces: Vec::new(),
+ composite_tiles: Vec::new(),
+ opaque_tile_descriptors: Vec::new(),
+ alpha_tile_descriptors: Vec::new(),
+ }
+ }
+
+ /// Reset the list of compositor surfaces that follow this sub-slice.
+ /// Built per-frame, since APZ may change whether an image is suitable to be a compositor surface.
+ fn reset(&mut self) {
+ self.compositor_surfaces.clear();
+ self.composite_tiles.clear();
+ self.opaque_tile_descriptors.clear();
+ self.alpha_tile_descriptors.clear();
+ }
+
+ /// Resize the tile grid to match a new tile bounds
+ fn resize(&mut self, new_tile_rect: TileRect) -> FastHashMap<TileOffset, Box<Tile>> {
+ let mut old_tiles = mem::replace(&mut self.tiles, FastHashMap::default());
+ self.tiles.reserve(new_tile_rect.area() as usize);
+
+ for y in new_tile_rect.min.y .. new_tile_rect.max.y {
+ for x in new_tile_rect.min.x .. new_tile_rect.max.x {
+ let key = TileOffset::new(x, y);
+ let tile = old_tiles
+ .remove(&key)
+ .unwrap_or_else(|| {
+ Box::new(Tile::new(key))
+ });
+ self.tiles.insert(key, tile);
+ }
+ }
+
+ old_tiles
+ }
+}
+
+pub struct BackdropSurface {
+ pub id: NativeSurfaceId,
+ color: ColorF,
+ pub device_rect: DeviceRect,
+}
+
+/// Represents a cache of tiles that make up a picture primitives.
+pub struct TileCacheInstance {
+ /// Index of the tile cache / slice for this frame builder. It's determined
+ /// by the setup_picture_caching method during flattening, which splits the
+ /// picture tree into multiple slices. It's used as a simple input to the tile
+ /// keys. It does mean we invalidate tiles if a new layer gets inserted / removed
+ /// between display lists - this seems very unlikely to occur on most pages, but
+ /// can be revisited if we ever notice that.
+ pub slice: usize,
+ /// Propagated information about the slice
+ pub slice_flags: SliceFlags,
+ /// The currently selected tile size to use for this cache
+ pub current_tile_size: DeviceIntSize,
+ /// The list of sub-slices in this tile cache
+ pub sub_slices: Vec<SubSlice>,
+ /// The positioning node for this tile cache.
+ pub spatial_node_index: SpatialNodeIndex,
+ /// List of opacity bindings, with some extra information
+ /// about whether they changed since last frame.
+ opacity_bindings: FastHashMap<PropertyBindingId, OpacityBindingInfo>,
+ /// Switch back and forth between old and new bindings hashmaps to avoid re-allocating.
+ old_opacity_bindings: FastHashMap<PropertyBindingId, OpacityBindingInfo>,
+ /// A helper to compare transforms between previous and current frame.
+ spatial_node_comparer: SpatialNodeComparer,
+ /// List of color bindings, with some extra information
+ /// about whether they changed since last frame.
+ color_bindings: FastHashMap<PropertyBindingId, ColorBindingInfo>,
+ /// Switch back and forth between old and new bindings hashmaps to avoid re-allocating.
+ old_color_bindings: FastHashMap<PropertyBindingId, ColorBindingInfo>,
+ /// The current dirty region tracker for this picture.
+ pub dirty_region: DirtyRegion,
+ /// Current size of tiles in picture units.
+ tile_size: PictureSize,
+ /// Tile coords of the currently allocated grid.
+ tile_rect: TileRect,
+ /// Pre-calculated versions of the tile_rect above, used to speed up the
+ /// calculations in get_tile_coords_for_rect.
+ tile_bounds_p0: TileOffset,
+ tile_bounds_p1: TileOffset,
+ /// Local rect (unclipped) of the picture this cache covers.
+ pub local_rect: PictureRect,
+ /// The local clip rect, from the shared clips of this picture.
+ pub local_clip_rect: PictureRect,
+ /// The screen rect, transformed to local picture space.
+ pub screen_rect_in_pic_space: PictureRect,
+ /// The surface index that this tile cache will be drawn into.
+ surface_index: SurfaceIndex,
+ /// The background color from the renderer. If this is set opaque, we know it's
+ /// fine to clear the tiles to this and allow subpixel text on the first slice.
+ pub background_color: Option<ColorF>,
+ /// Information about the calculated backdrop content of this cache.
+ pub backdrop: BackdropInfo,
+ /// The allowed subpixel mode for this surface, which depends on the detected
+ /// opacity of the background.
+ pub subpixel_mode: SubpixelMode,
+ // Node in the clip-tree that defines where we exclude clips from child prims
+ pub shared_clip_node_id: ClipNodeId,
+ // Clip leaf that is used to build the clip-chain for this tile cache.
+ pub shared_clip_leaf_id: Option<ClipLeafId>,
+ /// The number of frames until this cache next evaluates what tile size to use.
+ /// If a picture rect size is regularly changing just around a size threshold,
+ /// we don't want to constantly invalidate and reallocate different tile size
+ /// configuration each frame.
+ frames_until_size_eval: usize,
+ /// For DirectComposition, virtual surfaces don't support negative coordinates. However,
+ /// picture cache tile coordinates can be negative. To handle this, we apply an offset
+ /// to each tile in DirectComposition. We want to change this as little as possible,
+ /// to avoid invalidating tiles. However, if we have a picture cache tile coordinate
+ /// which is outside the virtual surface bounds, we must change this to allow
+ /// correct remapping of the coordinates passed to BeginDraw in DC.
+ virtual_offset: DeviceIntPoint,
+ /// keep around the hash map used as compare_cache to avoid reallocating it each
+ /// frame.
+ compare_cache: FastHashMap<PrimitiveComparisonKey, PrimitiveCompareResult>,
+ /// 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>,
+ /// A cache of compositor surfaces that are retained between frames
+ pub external_native_surface_cache: FastHashMap<ExternalNativeSurfaceKey, ExternalNativeSurface>,
+ /// Current frame ID of this tile cache instance. Used for book-keeping / garbage collecting
+ frame_id: FrameId,
+ /// Registered transform in CompositeState for this picture cache
+ pub transform_index: CompositorTransformIndex,
+ /// Current transform mapping local picture space to compositor surface space
+ local_to_surface: ScaleOffset,
+ /// If true, we need to invalidate all tiles during `post_update`
+ invalidate_all_tiles: bool,
+ /// Current transform mapping compositor surface space to final device space
+ surface_to_device: ScaleOffset,
+ /// The current raster scale for tiles in this cache
+ current_raster_scale: f32,
+ /// Depth of off-screen surfaces that are currently pushed during dependency updates
+ current_surface_traversal_depth: usize,
+ /// A list of extra dirty invalidation tests that can only be checked once we
+ /// know the dirty rect of all tiles
+ deferred_dirty_tests: Vec<DeferredDirtyTest>,
+ /// Is there a backdrop associated with this cache
+ found_prims_after_backdrop: bool,
+ pub backdrop_surface: Option<BackdropSurface>,
+}
+
+enum SurfacePromotionResult {
+ Failed,
+ Success,
+}
+
+impl TileCacheInstance {
+ pub fn new(params: TileCacheParams) -> Self {
+ // Determine how many sub-slices we need. Clamp to an arbitrary limit to ensure
+ // we don't create a huge number of OS compositor tiles and sub-slices.
+ let sub_slice_count = params.compositor_surface_count.min(MAX_COMPOSITOR_SURFACES) + 1;
+
+ let mut sub_slices = Vec::with_capacity(sub_slice_count);
+ for _ in 0 .. sub_slice_count {
+ sub_slices.push(SubSlice::new());
+ }
+
+ TileCacheInstance {
+ slice: params.slice,
+ slice_flags: params.slice_flags,
+ spatial_node_index: params.spatial_node_index,
+ sub_slices,
+ opacity_bindings: FastHashMap::default(),
+ old_opacity_bindings: FastHashMap::default(),
+ spatial_node_comparer: SpatialNodeComparer::new(),
+ color_bindings: FastHashMap::default(),
+ old_color_bindings: FastHashMap::default(),
+ dirty_region: DirtyRegion::new(params.spatial_node_index),
+ tile_size: PictureSize::zero(),
+ tile_rect: TileRect::zero(),
+ tile_bounds_p0: TileOffset::zero(),
+ tile_bounds_p1: TileOffset::zero(),
+ local_rect: PictureRect::zero(),
+ local_clip_rect: PictureRect::zero(),
+ screen_rect_in_pic_space: PictureRect::zero(),
+ surface_index: SurfaceIndex(0),
+ background_color: params.background_color,
+ backdrop: BackdropInfo::empty(),
+ subpixel_mode: SubpixelMode::Allow,
+ shared_clip_node_id: params.shared_clip_node_id,
+ shared_clip_leaf_id: params.shared_clip_leaf_id,
+ current_tile_size: DeviceIntSize::zero(),
+ frames_until_size_eval: 0,
+ // Default to centering the virtual offset in the middle of the DC virtual surface
+ virtual_offset: DeviceIntPoint::new(
+ params.virtual_surface_size / 2,
+ params.virtual_surface_size / 2,
+ ),
+ compare_cache: FastHashMap::default(),
+ tile_size_override: None,
+ external_native_surface_cache: FastHashMap::default(),
+ frame_id: FrameId::INVALID,
+ transform_index: CompositorTransformIndex::INVALID,
+ surface_to_device: ScaleOffset::identity(),
+ local_to_surface: ScaleOffset::identity(),
+ invalidate_all_tiles: true,
+ current_raster_scale: 1.0,
+ current_surface_traversal_depth: 0,
+ deferred_dirty_tests: Vec::new(),
+ found_prims_after_backdrop: false,
+ backdrop_surface: None,
+ }
+ }
+
+ /// Return the total number of tiles allocated by this tile cache
+ pub fn tile_count(&self) -> usize {
+ self.tile_rect.area() as usize * self.sub_slices.len()
+ }
+
+ /// Trims memory held by the tile cache, such as native surfaces.
+ pub fn memory_pressure(&mut self, resource_cache: &mut ResourceCache) {
+ for sub_slice in &mut self.sub_slices {
+ for tile in sub_slice.tiles.values_mut() {
+ if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { ref mut id, .. }, .. }) = tile.surface {
+ // Reseting the id to None with take() ensures that a new
+ // tile will be allocated during the next frame build.
+ if let Some(id) = id.take() {
+ resource_cache.destroy_compositor_tile(id);
+ }
+ }
+ }
+ if let Some(native_surface) = sub_slice.native_surface.take() {
+ resource_cache.destroy_compositor_surface(native_surface.opaque);
+ resource_cache.destroy_compositor_surface(native_surface.alpha);
+ }
+ }
+ }
+
+ /// Reset this tile cache with the updated parameters from a new scene
+ /// that has arrived. This allows the tile cache to be retained across
+ /// new scenes.
+ pub fn prepare_for_new_scene(
+ &mut self,
+ params: TileCacheParams,
+ resource_cache: &mut ResourceCache,
+ ) {
+ // We should only receive updated state for matching slice key
+ assert_eq!(self.slice, params.slice);
+
+ // Determine how many sub-slices we need, based on how many compositor surface prims are
+ // in the supplied primitive list.
+ let required_sub_slice_count = params.compositor_surface_count.min(MAX_COMPOSITOR_SURFACES) + 1;
+
+ if self.sub_slices.len() != required_sub_slice_count {
+ self.tile_rect = TileRect::zero();
+
+ if self.sub_slices.len() > required_sub_slice_count {
+ let old_sub_slices = self.sub_slices.split_off(required_sub_slice_count);
+
+ for mut sub_slice in old_sub_slices {
+ for tile in sub_slice.tiles.values_mut() {
+ if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { ref mut id, .. }, .. }) = tile.surface {
+ if let Some(id) = id.take() {
+ resource_cache.destroy_compositor_tile(id);
+ }
+ }
+ }
+
+ if let Some(native_surface) = sub_slice.native_surface {
+ resource_cache.destroy_compositor_surface(native_surface.opaque);
+ resource_cache.destroy_compositor_surface(native_surface.alpha);
+ }
+ }
+ } else {
+ while self.sub_slices.len() < required_sub_slice_count {
+ self.sub_slices.push(SubSlice::new());
+ }
+ }
+ }
+
+ // Store the parameters from the scene builder for this slice. Other
+ // params in the tile cache are retained and reused, or are always
+ // updated during pre/post_update.
+ self.slice_flags = params.slice_flags;
+ self.spatial_node_index = params.spatial_node_index;
+ self.background_color = params.background_color;
+ self.shared_clip_leaf_id = params.shared_clip_leaf_id;
+ self.shared_clip_node_id = params.shared_clip_node_id;
+
+ // Since the slice flags may have changed, ensure we re-evaluate the
+ // appropriate tile size for this cache next update.
+ self.frames_until_size_eval = 0;
+ }
+
+ /// Destroy any manually managed resources before this picture cache is
+ /// destroyed, such as native compositor surfaces.
+ pub fn destroy(
+ self,
+ resource_cache: &mut ResourceCache,
+ ) {
+ for sub_slice in self.sub_slices {
+ if let Some(native_surface) = sub_slice.native_surface {
+ resource_cache.destroy_compositor_surface(native_surface.opaque);
+ resource_cache.destroy_compositor_surface(native_surface.alpha);
+ }
+ }
+
+ for (_, external_surface) in self.external_native_surface_cache {
+ resource_cache.destroy_compositor_surface(external_surface.native_surface_id)
+ }
+
+ if let Some(backdrop_surface) = &self.backdrop_surface {
+ resource_cache.destroy_compositor_surface(backdrop_surface.id);
+ }
+ }
+
+ /// Get the tile coordinates for a given rectangle.
+ fn get_tile_coords_for_rect(
+ &self,
+ rect: &PictureRect,
+ ) -> (TileOffset, TileOffset) {
+ // Get the tile coordinates in the picture space.
+ let mut p0 = TileOffset::new(
+ (rect.min.x / self.tile_size.width).floor() as i32,
+ (rect.min.y / self.tile_size.height).floor() as i32,
+ );
+
+ let mut p1 = TileOffset::new(
+ (rect.max.x / self.tile_size.width).ceil() as i32,
+ (rect.max.y / self.tile_size.height).ceil() as i32,
+ );
+
+ // Clamp the tile coordinates here to avoid looping over irrelevant tiles later on.
+ p0.x = clamp(p0.x, self.tile_bounds_p0.x, self.tile_bounds_p1.x);
+ p0.y = clamp(p0.y, self.tile_bounds_p0.y, self.tile_bounds_p1.y);
+ p1.x = clamp(p1.x, self.tile_bounds_p0.x, self.tile_bounds_p1.x);
+ p1.y = clamp(p1.y, self.tile_bounds_p0.y, self.tile_bounds_p1.y);
+
+ (p0, p1)
+ }
+
+ /// Update transforms, opacity, color bindings and tile rects.
+ pub fn pre_update(
+ &mut self,
+ pic_rect: PictureRect,
+ surface_index: SurfaceIndex,
+ frame_context: &FrameVisibilityContext,
+ frame_state: &mut FrameVisibilityState,
+ ) -> WorldRect {
+ self.surface_index = surface_index;
+ self.local_rect = pic_rect;
+ self.local_clip_rect = PictureRect::max_rect();
+ self.deferred_dirty_tests.clear();
+
+ for sub_slice in &mut self.sub_slices {
+ sub_slice.reset();
+ }
+
+ // Reset the opaque rect + subpixel mode, as they are calculated
+ // during the prim dependency checks.
+ self.backdrop = BackdropInfo::empty();
+
+ // Calculate the screen rect in picture space, for later comparison against
+ // backdrops, and prims potentially covering backdrops.
+ let pic_to_world_mapper = SpaceMapper::new_with_target(
+ frame_context.root_spatial_node_index,
+ self.spatial_node_index,
+ frame_context.global_screen_world_rect,
+ frame_context.spatial_tree,
+ );
+ self.screen_rect_in_pic_space = pic_to_world_mapper
+ .unmap(&frame_context.global_screen_world_rect)
+ .expect("unable to unmap screen rect");
+
+ // If there is a valid set of shared clips, build a clip chain instance for this,
+ // which will provide a local clip rect. This is useful for establishing things
+ // like whether the backdrop rect supplied by Gecko can be considered opaque.
+ if let Some(shared_clip_leaf_id) = self.shared_clip_leaf_id {
+ let map_local_to_surface = SpaceMapper::new(
+ self.spatial_node_index,
+ pic_rect,
+ );
+
+ frame_state.clip_store.set_active_clips(
+ self.spatial_node_index,
+ map_local_to_surface.ref_spatial_node_index,
+ shared_clip_leaf_id,
+ frame_context.spatial_tree,
+ &mut frame_state.data_stores.clip,
+ &frame_state.clip_tree,
+ );
+
+ let clip_chain_instance = frame_state.clip_store.build_clip_chain_instance(
+ pic_rect.cast_unit(),
+ &map_local_to_surface,
+ &pic_to_world_mapper,
+ frame_context.spatial_tree,
+ frame_state.gpu_cache,
+ frame_state.resource_cache,
+ frame_context.global_device_pixel_scale,
+ &frame_context.global_screen_world_rect,
+ &mut frame_state.data_stores.clip,
+ true,
+ );
+
+ // Ensure that if the entire picture cache is clipped out, the local
+ // clip rect is zero. This makes sure we don't register any occluders
+ // that are actually off-screen.
+ self.local_clip_rect = clip_chain_instance.map_or(PictureRect::zero(), |clip_chain_instance| {
+ clip_chain_instance.pic_coverage_rect
+ });
+ }
+
+ // Advance the current frame ID counter for this picture cache (must be done
+ // after any retained prev state is taken above).
+ self.frame_id.advance();
+
+ // Notify the spatial node comparer that a new frame has started, and the
+ // current reference spatial node for this tile cache.
+ self.spatial_node_comparer.next_frame(self.spatial_node_index);
+
+ // At the start of the frame, step through each current compositor surface
+ // and mark it as unused. Later, this is used to free old compositor surfaces.
+ // TODO(gw): In future, we might make this more sophisticated - for example,
+ // retaining them for >1 frame if unused, or retaining them in some
+ // kind of pool to reduce future allocations.
+ for external_native_surface in self.external_native_surface_cache.values_mut() {
+ external_native_surface.used_this_frame = false;
+ }
+
+ // 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 ||
+ self.tile_size_override != frame_context.config.tile_size_override {
+
+ // Work out what size tile is appropriate for this picture cache.
+ let desired_tile_size = match frame_context.config.tile_size_override {
+ Some(tile_size_override) => {
+ tile_size_override
+ }
+ None => {
+ if self.slice_flags.contains(SliceFlags::IS_SCROLLBAR) {
+ if pic_rect.width() <= pic_rect.height() {
+ TILE_SIZE_SCROLLBAR_VERTICAL
+ } else {
+ TILE_SIZE_SCROLLBAR_HORIZONTAL
+ }
+ } else {
+ frame_state.resource_cache.picture_textures.default_tile_size()
+ }
+ }
+ };
+
+ // If the desired tile size has changed, then invalidate and drop any
+ // existing tiles.
+ if desired_tile_size != self.current_tile_size {
+ for sub_slice in &mut self.sub_slices {
+ // Destroy any native surfaces on the tiles that will be dropped due
+ // to resizing.
+ if let Some(native_surface) = sub_slice.native_surface.take() {
+ frame_state.resource_cache.destroy_compositor_surface(native_surface.opaque);
+ frame_state.resource_cache.destroy_compositor_surface(native_surface.alpha);
+ }
+ sub_slice.tiles.clear();
+ }
+ self.tile_rect = TileRect::zero();
+ self.current_tile_size = desired_tile_size;
+ }
+
+ // 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;
+ }
+
+ // Get the complete scale-offset from local space to device space
+ let local_to_device = get_relative_scale_offset(
+ self.spatial_node_index,
+ frame_context.root_spatial_node_index,
+ frame_context.spatial_tree,
+ );
+
+ // Get the compositor transform, which depends on pinch-zoom mode
+ let mut surface_to_device = local_to_device;
+
+ if frame_context.config.low_quality_pinch_zoom {
+ surface_to_device.scale.x /= self.current_raster_scale;
+ surface_to_device.scale.y /= self.current_raster_scale;
+ } else {
+ surface_to_device.scale.x = 1.0;
+ surface_to_device.scale.y = 1.0;
+ }
+
+ // Use that compositor transform to calculate a relative local to surface
+ let local_to_surface = local_to_device.accumulate(&surface_to_device.inverse());
+
+ const EPSILON: f32 = 0.001;
+ let compositor_translation_changed =
+ !surface_to_device.offset.x.approx_eq_eps(&self.surface_to_device.offset.x, &EPSILON) ||
+ !surface_to_device.offset.y.approx_eq_eps(&self.surface_to_device.offset.y, &EPSILON);
+ let compositor_scale_changed =
+ !surface_to_device.scale.x.approx_eq_eps(&self.surface_to_device.scale.x, &EPSILON) ||
+ !surface_to_device.scale.y.approx_eq_eps(&self.surface_to_device.scale.y, &EPSILON);
+ let surface_scale_changed =
+ !local_to_surface.scale.x.approx_eq_eps(&self.local_to_surface.scale.x, &EPSILON) ||
+ !local_to_surface.scale.y.approx_eq_eps(&self.local_to_surface.scale.y, &EPSILON);
+
+ if compositor_translation_changed ||
+ compositor_scale_changed ||
+ surface_scale_changed ||
+ frame_context.config.force_invalidation {
+ frame_state.composite_state.dirty_rects_are_valid = false;
+ }
+
+ self.surface_to_device = surface_to_device;
+ self.local_to_surface = local_to_surface;
+ self.invalidate_all_tiles = surface_scale_changed || frame_context.config.force_invalidation;
+
+ // Do a hacky diff of opacity binding values from the last frame. This is
+ // used later on during tile invalidation tests.
+ let current_properties = frame_context.scene_properties.float_properties();
+ mem::swap(&mut self.opacity_bindings, &mut self.old_opacity_bindings);
+
+ self.opacity_bindings.clear();
+ for (id, value) in current_properties {
+ let changed = match self.old_opacity_bindings.get(id) {
+ Some(old_property) => !old_property.value.approx_eq(value),
+ None => true,
+ };
+ self.opacity_bindings.insert(*id, OpacityBindingInfo {
+ value: *value,
+ changed,
+ });
+ }
+
+ // Do a hacky diff of color binding values from the last frame. This is
+ // used later on during tile invalidation tests.
+ let current_properties = frame_context.scene_properties.color_properties();
+ mem::swap(&mut self.color_bindings, &mut self.old_color_bindings);
+
+ self.color_bindings.clear();
+ for (id, value) in current_properties {
+ let changed = match self.old_color_bindings.get(id) {
+ Some(old_property) => old_property.value != (*value).into(),
+ None => true,
+ };
+ self.color_bindings.insert(*id, ColorBindingInfo {
+ value: (*value).into(),
+ changed,
+ });
+ }
+
+ let world_tile_size = WorldSize::new(
+ self.current_tile_size.width as f32 / frame_context.global_device_pixel_scale.0,
+ self.current_tile_size.height as f32 / frame_context.global_device_pixel_scale.0,
+ );
+
+ self.tile_size = PictureSize::new(
+ world_tile_size.width / self.local_to_surface.scale.x,
+ world_tile_size.height / self.local_to_surface.scale.y,
+ );
+
+ // Inflate the needed rect a bit, so that we retain tiles that we have drawn
+ // but have just recently gone off-screen. This means that we avoid re-drawing
+ // tiles if the user is scrolling up and down small amounts, at the cost of
+ // a bit of extra texture memory.
+ let desired_rect_in_pic_space = self.screen_rect_in_pic_space
+ .inflate(0.0, 1.0 * self.tile_size.height);
+
+ let needed_rect_in_pic_space = desired_rect_in_pic_space
+ .intersection(&pic_rect)
+ .unwrap_or_else(Box2D::zero);
+
+ let p0 = needed_rect_in_pic_space.min;
+ let p1 = needed_rect_in_pic_space.max;
+
+ let x0 = (p0.x / self.tile_size.width).floor() as i32;
+ let x1 = (p1.x / self.tile_size.width).ceil() as i32;
+
+ let y0 = (p0.y / self.tile_size.height).floor() as i32;
+ let y1 = (p1.y / self.tile_size.height).ceil() as i32;
+
+ let new_tile_rect = TileRect {
+ min: TileOffset::new(x0, y0),
+ max: TileOffset::new(x1, y1),
+ };
+
+ // Determine whether the current bounds of the tile grid will exceed the
+ // bounds of the DC virtual surface, taking into account the current
+ // virtual offset. If so, we need to invalidate all tiles, and set up
+ // a new virtual offset, centered around the current tile grid.
+
+ let virtual_surface_size = frame_context.config.compositor_kind.get_virtual_surface_size();
+ // We only need to invalidate in this case if the underlying platform
+ // uses virtual surfaces.
+ if virtual_surface_size > 0 {
+ // Get the extremities of the tile grid after virtual offset is applied
+ let tx0 = self.virtual_offset.x + x0 * self.current_tile_size.width;
+ let ty0 = self.virtual_offset.y + y0 * self.current_tile_size.height;
+ let tx1 = self.virtual_offset.x + (x1+1) * self.current_tile_size.width;
+ let ty1 = self.virtual_offset.y + (y1+1) * self.current_tile_size.height;
+
+ let need_new_virtual_offset = tx0 < 0 ||
+ ty0 < 0 ||
+ tx1 >= virtual_surface_size ||
+ ty1 >= virtual_surface_size;
+
+ if need_new_virtual_offset {
+ // Calculate a new virtual offset, centered around the middle of the
+ // current tile grid. This means we won't need to invalidate and get
+ // a new offset for a long time!
+ self.virtual_offset = DeviceIntPoint::new(
+ (virtual_surface_size/2) - ((x0 + x1) / 2) * self.current_tile_size.width,
+ (virtual_surface_size/2) - ((y0 + y1) / 2) * self.current_tile_size.height,
+ );
+
+ // Invalidate all native tile surfaces. They will be re-allocated next time
+ // they are scheduled to be rasterized.
+ for sub_slice in &mut self.sub_slices {
+ for tile in sub_slice.tiles.values_mut() {
+ if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { ref mut id, .. }, .. }) = tile.surface {
+ if let Some(id) = id.take() {
+ frame_state.resource_cache.destroy_compositor_tile(id);
+ tile.surface = None;
+ // Invalidate the entire tile to force a redraw.
+ // TODO(gw): Add a new invalidation reason for virtual offset changing
+ tile.invalidate(None, InvalidationReason::CompositorKindChanged);
+ }
+ }
+ }
+
+ // Destroy the native virtual surfaces. They will be re-allocated next time a tile
+ // that references them is scheduled to draw.
+ if let Some(native_surface) = sub_slice.native_surface.take() {
+ frame_state.resource_cache.destroy_compositor_surface(native_surface.opaque);
+ frame_state.resource_cache.destroy_compositor_surface(native_surface.alpha);
+ }
+ }
+ }
+ }
+
+ // Rebuild the tile grid if the picture cache rect has changed.
+ if new_tile_rect != self.tile_rect {
+ for sub_slice in &mut self.sub_slices {
+ let mut old_tiles = sub_slice.resize(new_tile_rect);
+
+ // When old tiles that remain after the loop, dirty rects are not valid.
+ if !old_tiles.is_empty() {
+ frame_state.composite_state.dirty_rects_are_valid = false;
+ }
+
+ // Any old tiles that remain after the loop above are going to be dropped. For
+ // simple composite mode, the texture cache handle will expire and be collected
+ // by the texture cache. For native compositor mode, we need to explicitly
+ // invoke a callback to the client to destroy that surface.
+ frame_state.composite_state.destroy_native_tiles(
+ old_tiles.values_mut(),
+ frame_state.resource_cache,
+ );
+ }
+ }
+
+ // This is duplicated information from tile_rect, but cached here to avoid
+ // redundant calculations during get_tile_coords_for_rect
+ self.tile_bounds_p0 = TileOffset::new(x0, y0);
+ self.tile_bounds_p1 = TileOffset::new(x1, y1);
+ self.tile_rect = new_tile_rect;
+
+ let mut world_culling_rect = WorldRect::zero();
+
+ let mut ctx = TilePreUpdateContext {
+ pic_to_world_mapper,
+ background_color: self.background_color,
+ global_screen_world_rect: frame_context.global_screen_world_rect,
+ tile_size: self.tile_size,
+ frame_id: self.frame_id,
+ };
+
+ // Pre-update each tile
+ for sub_slice in &mut self.sub_slices {
+ for tile in sub_slice.tiles.values_mut() {
+ tile.pre_update(&ctx);
+
+ // Only include the tiles that are currently in view into the world culling
+ // rect. This is a very important optimization for a couple of reasons:
+ // (1) Primitives that intersect with tiles in the grid that are not currently
+ // visible can be skipped from primitive preparation, clip chain building
+ // and tile dependency updates.
+ // (2) When we need to allocate an off-screen surface for a child picture (for
+ // example a CSS filter) we clip the size of the GPU surface to the world
+ // culling rect below (to ensure we draw enough of it to be sampled by any
+ // tiles that reference it). Making the world culling rect only affected
+ // by visible tiles (rather than the entire virtual tile display port) can
+ // result in allocating _much_ smaller GPU surfaces for cases where the
+ // true off-screen surface size is very large.
+ if tile.is_visible {
+ world_culling_rect = world_culling_rect.union(&tile.world_tile_rect);
+ }
+ }
+
+ // The background color can only be applied to the first sub-slice.
+ ctx.background_color = None;
+ }
+
+ // If compositor mode is changed, need to drop all incompatible tiles.
+ match frame_context.config.compositor_kind {
+ CompositorKind::Draw { .. } => {
+ for sub_slice in &mut self.sub_slices {
+ for tile in sub_slice.tiles.values_mut() {
+ if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { ref mut id, .. }, .. }) = tile.surface {
+ if let Some(id) = id.take() {
+ frame_state.resource_cache.destroy_compositor_tile(id);
+ }
+ tile.surface = None;
+ // Invalidate the entire tile to force a redraw.
+ tile.invalidate(None, InvalidationReason::CompositorKindChanged);
+ }
+ }
+
+ if let Some(native_surface) = sub_slice.native_surface.take() {
+ frame_state.resource_cache.destroy_compositor_surface(native_surface.opaque);
+ frame_state.resource_cache.destroy_compositor_surface(native_surface.alpha);
+ }
+ }
+
+ for (_, external_surface) in self.external_native_surface_cache.drain() {
+ frame_state.resource_cache.destroy_compositor_surface(external_surface.native_surface_id)
+ }
+ }
+ CompositorKind::Native { .. } => {
+ // This could hit even when compositor mode is not changed,
+ // then we need to check if there are incompatible tiles.
+ for sub_slice in &mut self.sub_slices {
+ for tile in sub_slice.tiles.values_mut() {
+ if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::TextureCache { .. }, .. }) = tile.surface {
+ tile.surface = None;
+ // Invalidate the entire tile to force a redraw.
+ tile.invalidate(None, InvalidationReason::CompositorKindChanged);
+ }
+ }
+ }
+ }
+ }
+
+ world_culling_rect
+ }
+
+ fn can_promote_to_surface(
+ &mut self,
+ flags: PrimitiveFlags,
+ prim_clip_chain: &ClipChainInstance,
+ prim_spatial_node_index: SpatialNodeIndex,
+ is_root_tile_cache: bool,
+ sub_slice_index: usize,
+ frame_context: &FrameVisibilityContext,
+ ) -> SurfacePromotionResult {
+ // Check if this primitive _wants_ to be promoted to a compositor surface.
+ if !flags.contains(PrimitiveFlags::PREFER_COMPOSITOR_SURFACE) {
+ return SurfacePromotionResult::Failed;
+ }
+
+ // For now, only support a small (arbitrary) number of compositor surfaces.
+ if sub_slice_index == MAX_COMPOSITOR_SURFACES {
+ return SurfacePromotionResult::Failed;
+ }
+
+ // If a complex clip is being applied to this primitive, it can't be
+ // promoted directly to a compositor surface (we might be able to
+ // do this in limited cases in future, some native compositors do
+ // support rounded rect clips, for example)
+ if prim_clip_chain.needs_mask {
+ return SurfacePromotionResult::Failed;
+ }
+
+ // If not on the root picture cache, it has some kind of
+ // complex effect (such as a filter, mix-blend-mode or 3d transform).
+ if !is_root_tile_cache {
+ return SurfacePromotionResult::Failed;
+ }
+
+ let mapper : SpaceMapper<PicturePixel, WorldPixel> = SpaceMapper::new_with_target(
+ frame_context.root_spatial_node_index,
+ prim_spatial_node_index,
+ frame_context.global_screen_world_rect,
+ &frame_context.spatial_tree);
+ let transform = mapper.get_transform();
+ if !transform.is_2d_scale_translation() {
+ return SurfacePromotionResult::Failed;
+ }
+
+ if self.slice_flags.contains(SliceFlags::IS_ATOMIC) {
+ return SurfacePromotionResult::Failed;
+ }
+
+ SurfacePromotionResult::Success
+ }
+
+ fn setup_compositor_surfaces_yuv(
+ &mut self,
+ sub_slice_index: usize,
+ prim_info: &mut PrimitiveDependencyInfo,
+ flags: PrimitiveFlags,
+ local_prim_rect: LayoutRect,
+ prim_spatial_node_index: SpatialNodeIndex,
+ pic_coverage_rect: PictureRect,
+ frame_context: &FrameVisibilityContext,
+ image_dependencies: &[ImageDependency;3],
+ api_keys: &[ImageKey; 3],
+ resource_cache: &mut ResourceCache,
+ composite_state: &mut CompositeState,
+ gpu_cache: &mut GpuCache,
+ image_rendering: ImageRendering,
+ color_depth: ColorDepth,
+ color_space: YuvRangedColorSpace,
+ format: YuvFormat,
+ ) -> bool {
+ for &key in api_keys {
+ if key != ImageKey::DUMMY {
+ // TODO: See comment in setup_compositor_surfaces_rgb.
+ resource_cache.request_image(ImageRequest {
+ key,
+ rendering: image_rendering,
+ tile: None,
+ },
+ gpu_cache,
+ );
+ }
+ }
+
+ self.setup_compositor_surfaces_impl(
+ sub_slice_index,
+ prim_info,
+ flags,
+ local_prim_rect,
+ prim_spatial_node_index,
+ pic_coverage_rect,
+ frame_context,
+ ExternalSurfaceDependency::Yuv {
+ image_dependencies: *image_dependencies,
+ color_space,
+ format,
+ channel_bit_depth: color_depth.bit_depth(),
+ },
+ api_keys,
+ resource_cache,
+ composite_state,
+ image_rendering,
+ true,
+ )
+ }
+
+ fn setup_compositor_surfaces_rgb(
+ &mut self,
+ sub_slice_index: usize,
+ prim_info: &mut PrimitiveDependencyInfo,
+ flags: PrimitiveFlags,
+ local_prim_rect: LayoutRect,
+ prim_spatial_node_index: SpatialNodeIndex,
+ pic_coverage_rect: PictureRect,
+ frame_context: &FrameVisibilityContext,
+ image_dependency: ImageDependency,
+ api_key: ImageKey,
+ resource_cache: &mut ResourceCache,
+ composite_state: &mut CompositeState,
+ gpu_cache: &mut GpuCache,
+ image_rendering: ImageRendering,
+ ) -> bool {
+ let mut api_keys = [ImageKey::DUMMY; 3];
+ api_keys[0] = api_key;
+
+ // TODO: The picture compositing code requires images promoted
+ // into their own picture cache slices to be requested every
+ // frame even if they are not visible. However the image updates
+ // are only reached on the prepare pass for visible primitives.
+ // So we make sure to trigger an image request when promoting
+ // the image here.
+ resource_cache.request_image(ImageRequest {
+ key: api_key,
+ rendering: image_rendering,
+ tile: None,
+ },
+ gpu_cache,
+ );
+
+ let is_opaque = resource_cache.get_image_properties(api_key)
+ .map_or(false, |properties| properties.descriptor.is_opaque());
+
+ self.setup_compositor_surfaces_impl(
+ sub_slice_index,
+ prim_info,
+ flags,
+ local_prim_rect,
+ prim_spatial_node_index,
+ pic_coverage_rect,
+ frame_context,
+ ExternalSurfaceDependency::Rgb {
+ image_dependency,
+ },
+ &api_keys,
+ resource_cache,
+ composite_state,
+ image_rendering,
+ is_opaque,
+ )
+ }
+
+ // returns false if composition is not available for this surface,
+ // and the non-compositor path should be used to draw it instead.
+ fn setup_compositor_surfaces_impl(
+ &mut self,
+ sub_slice_index: usize,
+ prim_info: &mut PrimitiveDependencyInfo,
+ flags: PrimitiveFlags,
+ local_prim_rect: LayoutRect,
+ prim_spatial_node_index: SpatialNodeIndex,
+ pic_coverage_rect: PictureRect,
+ frame_context: &FrameVisibilityContext,
+ dependency: ExternalSurfaceDependency,
+ api_keys: &[ImageKey; 3],
+ resource_cache: &mut ResourceCache,
+ composite_state: &mut CompositeState,
+ image_rendering: ImageRendering,
+ is_opaque: bool,
+ ) -> bool {
+ let map_local_to_surface = SpaceMapper::new_with_target(
+ self.spatial_node_index,
+ prim_spatial_node_index,
+ self.local_rect,
+ frame_context.spatial_tree,
+ );
+
+ // Map the primitive local rect into picture space.
+ let prim_rect = match map_local_to_surface.map(&local_prim_rect) {
+ Some(rect) => rect,
+ None => return true,
+ };
+
+ // If the rect is invalid, no need to create dependencies.
+ if prim_rect.is_empty() {
+ return true;
+ }
+
+ let pic_to_world_mapper = SpaceMapper::new_with_target(
+ frame_context.root_spatial_node_index,
+ self.spatial_node_index,
+ frame_context.global_screen_world_rect,
+ frame_context.spatial_tree,
+ );
+
+ let world_clip_rect = pic_to_world_mapper
+ .map(&prim_info.prim_clip_box)
+ .expect("bug: unable to map clip to world space");
+
+ let is_visible = world_clip_rect.intersects(&frame_context.global_screen_world_rect);
+ if !is_visible {
+ return true;
+ }
+
+ let prim_offset = ScaleOffset::from_offset(local_prim_rect.min.to_vector().cast_unit());
+
+ let local_prim_to_device = get_relative_scale_offset(
+ prim_spatial_node_index,
+ frame_context.root_spatial_node_index,
+ frame_context.spatial_tree,
+ );
+
+ let normalized_prim_to_device = prim_offset.accumulate(&local_prim_to_device);
+
+ let local_to_surface = ScaleOffset::identity();
+ let surface_to_device = normalized_prim_to_device;
+
+ // If this primitive is an external image, and supports being used
+ // directly by a native compositor, then lookup the external image id
+ // so we can pass that through.
+ let mut external_image_id = if flags.contains(PrimitiveFlags::SUPPORTS_EXTERNAL_COMPOSITOR_SURFACE)
+ && image_rendering == ImageRendering::Auto {
+ resource_cache.get_image_properties(api_keys[0])
+ .and_then(|properties| properties.external_image)
+ .and_then(|image| Some(image.id))
+ } else {
+ None
+ };
+
+
+ if let CompositorKind::Native { capabilities, .. } = composite_state.compositor_kind {
+ if external_image_id.is_some() &&
+ !capabilities.supports_external_compositor_surface_negative_scaling &&
+ (surface_to_device.scale.x < 0.0 || surface_to_device.scale.y < 0.0) {
+ external_image_id = None;
+ }
+ }
+
+ let compositor_transform_index = composite_state.register_transform(
+ local_to_surface,
+ surface_to_device,
+ );
+
+ let surface_size = composite_state.get_surface_rect(
+ &local_prim_rect,
+ &local_prim_rect,
+ compositor_transform_index,
+ ).size();
+
+ let clip_rect = (world_clip_rect * frame_context.global_device_pixel_scale).round();
+
+ if surface_size.width >= MAX_COMPOSITOR_SURFACES_SIZE ||
+ surface_size.height >= MAX_COMPOSITOR_SURFACES_SIZE {
+ return false;
+ }
+
+ // When using native compositing, we need to find an existing native surface
+ // handle to use, or allocate a new one. For existing native surfaces, we can
+ // also determine whether this needs to be updated, depending on whether the
+ // image generation(s) of the planes have changed since last composite.
+ let (native_surface_id, update_params) = match composite_state.compositor_kind {
+ CompositorKind::Draw { .. } => {
+ (None, None)
+ }
+ CompositorKind::Native { .. } => {
+ let native_surface_size = surface_size.to_i32();
+
+ let key = ExternalNativeSurfaceKey {
+ image_keys: *api_keys,
+ size: if external_image_id.is_some() { None } else { Some(native_surface_size) },
+ };
+
+ let native_surface = self.external_native_surface_cache
+ .entry(key)
+ .or_insert_with(|| {
+ // No existing surface, so allocate a new compositor surface.
+ let native_surface_id = match external_image_id {
+ Some(_external_image) => {
+ // If we have a suitable external image, then create an external
+ // surface to attach to.
+ resource_cache.create_compositor_external_surface(is_opaque)
+ }
+ None => {
+ // Otherwise create a normal compositor surface and a single
+ // compositor tile that covers the entire surface.
+ let native_surface_id =
+ resource_cache.create_compositor_surface(
+ DeviceIntPoint::zero(),
+ native_surface_size,
+ is_opaque,
+ );
+
+ let tile_id = NativeTileId {
+ surface_id: native_surface_id,
+ x: 0,
+ y: 0,
+ };
+ resource_cache.create_compositor_tile(tile_id);
+
+ native_surface_id
+ }
+ };
+
+ ExternalNativeSurface {
+ used_this_frame: true,
+ native_surface_id,
+ image_dependencies: [ImageDependency::INVALID; 3],
+ }
+ });
+
+ // Mark that the surface is referenced this frame so that the
+ // backing native surface handle isn't freed.
+ native_surface.used_this_frame = true;
+
+ let update_params = match external_image_id {
+ Some(external_image) => {
+ // If this is an external image surface, then there's no update
+ // to be done. Just attach the current external image to the surface
+ // and we're done.
+ resource_cache.attach_compositor_external_image(
+ native_surface.native_surface_id,
+ external_image,
+ );
+ None
+ }
+ None => {
+ // If the image dependencies match, there is no need to update
+ // the backing native surface.
+ match dependency {
+ ExternalSurfaceDependency::Yuv{ image_dependencies, .. } => {
+ if image_dependencies == native_surface.image_dependencies {
+ None
+ } else {
+ Some(native_surface_size)
+ }
+ },
+ ExternalSurfaceDependency::Rgb{ image_dependency, .. } => {
+ if image_dependency == native_surface.image_dependencies[0] {
+ None
+ } else {
+ Some(native_surface_size)
+ }
+ },
+ }
+ }
+ };
+
+ (Some(native_surface.native_surface_id), update_params)
+ }
+ };
+
+ // For compositor surfaces, if we didn't find an earlier sub-slice to add to,
+ // we know we can append to the current slice.
+ assert!(sub_slice_index < self.sub_slices.len() - 1);
+ let sub_slice = &mut self.sub_slices[sub_slice_index];
+
+ // Each compositor surface allocates a unique z-id
+ sub_slice.compositor_surfaces.push(CompositorSurface {
+ prohibited_rect: pic_coverage_rect,
+ is_opaque,
+ descriptor: ExternalSurfaceDescriptor {
+ local_surface_size: local_prim_rect.size(),
+ local_rect: prim_rect,
+ local_clip_rect: prim_info.prim_clip_box,
+ dependency,
+ image_rendering,
+ clip_rect,
+ transform_index: compositor_transform_index,
+ z_id: ZBufferId::invalid(),
+ native_surface_id,
+ update_params,
+ },
+ });
+
+ true
+ }
+
+ /// Push an estimated rect for an off-screen surface during dependency updates. This is
+ /// a workaround / hack that allows the picture cache code to know when it should be
+ /// processing primitive dependencies as a single atomic unit. In future, we aim to remove
+ /// this hack by having the primitive dependencies stored _within_ each owning picture.
+ /// This is part of the work required to support child picture caching anyway!
+ pub fn push_surface(
+ &mut self,
+ estimated_local_rect: LayoutRect,
+ surface_spatial_node_index: SpatialNodeIndex,
+ spatial_tree: &SpatialTree,
+ ) {
+ // Only need to evaluate sub-slice regions if we have compositor surfaces present
+ if self.current_surface_traversal_depth == 0 && self.sub_slices.len() > 1 {
+ let map_local_to_surface = SpaceMapper::new_with_target(
+ self.spatial_node_index,
+ surface_spatial_node_index,
+ self.local_rect,
+ spatial_tree,
+ );
+
+ if let Some(pic_rect) = map_local_to_surface.map(&estimated_local_rect) {
+ // Find the first sub-slice we can add this primitive to (we want to add
+ // prims to the primary surface if possible, so they get subpixel AA).
+ for sub_slice in &mut self.sub_slices {
+ let mut intersects_prohibited_region = false;
+
+ for surface in &mut sub_slice.compositor_surfaces {
+ if pic_rect.intersects(&surface.prohibited_rect) {
+ surface.prohibited_rect = surface.prohibited_rect.union(&pic_rect);
+
+ intersects_prohibited_region = true;
+ }
+ }
+
+ if !intersects_prohibited_region {
+ break;
+ }
+ }
+ }
+ }
+
+ self.current_surface_traversal_depth += 1;
+ }
+
+ /// Pop an off-screen surface off the stack during dependency updates
+ pub fn pop_surface(&mut self) {
+ self.current_surface_traversal_depth -= 1;
+ }
+
+ /// Update the dependencies for each tile for a given primitive instance.
+ pub fn update_prim_dependencies(
+ &mut self,
+ prim_instance: &mut PrimitiveInstance,
+ prim_spatial_node_index: SpatialNodeIndex,
+ local_prim_rect: LayoutRect,
+ frame_context: &FrameVisibilityContext,
+ data_stores: &DataStores,
+ clip_store: &ClipStore,
+ pictures: &[PicturePrimitive],
+ resource_cache: &mut ResourceCache,
+ color_bindings: &ColorBindingStorage,
+ surface_stack: &[(PictureIndex, SurfaceIndex)],
+ composite_state: &mut CompositeState,
+ gpu_cache: &mut GpuCache,
+ scratch: &mut PrimitiveScratchBuffer,
+ is_root_tile_cache: bool,
+ surfaces: &mut [SurfaceInfo],
+ ) {
+ // This primitive exists on the last element on the current surface stack.
+ profile_scope!("update_prim_dependencies");
+ let prim_surface_index = surface_stack.last().unwrap().1;
+ let prim_clip_chain = &prim_instance.vis.clip_chain;
+
+ // Accumulate the exact (clipped) local rect in to the parent surface
+ let mut surface = &mut surfaces[prim_surface_index.0];
+ surface.clipped_local_rect = surface.clipped_local_rect.union(&prim_clip_chain.pic_coverage_rect);
+
+ // If the primitive is directly drawn onto this picture cache surface, then
+ // the pic_coverage_rect is in the same space. If not, we need to map it from
+ // the surface space into the picture cache space.
+ let on_picture_surface = prim_surface_index == self.surface_index;
+ let pic_coverage_rect = if on_picture_surface {
+ prim_clip_chain.pic_coverage_rect
+ } else {
+ // We want to get the rect in the tile cache surface space that this primitive
+ // occupies, in order to enable correct invalidation regions. Each surface
+ // that exists in the chain between this primitive and the tile cache surface
+ // may have an arbitrary inflation factor (for example, in the case of a series
+ // of nested blur elements). To account for this, step through the current
+ // surface stack, mapping the primitive rect into each surface space, including
+ // the inflation factor from each intermediate surface.
+ let mut current_pic_coverage_rect = prim_clip_chain.pic_coverage_rect;
+ let mut current_spatial_node_index = surfaces[prim_surface_index.0]
+ .surface_spatial_node_index;
+
+ for (pic_index, surface_index) in surface_stack.iter().rev() {
+ let surface = &surfaces[surface_index.0];
+ let pic = &pictures[pic_index.0];
+
+ let map_local_to_surface = SpaceMapper::new_with_target(
+ surface.surface_spatial_node_index,
+ current_spatial_node_index,
+ surface.unclipped_local_rect,
+ frame_context.spatial_tree,
+ );
+
+ // Map the rect into the parent surface, and inflate if this surface requires
+ // it. If the rect can't be mapping (e.g. due to an invalid transform) then
+ // just bail out from the dependencies and cull this primitive.
+ current_pic_coverage_rect = match map_local_to_surface.map(&current_pic_coverage_rect) {
+ Some(rect) => {
+ // TODO(gw): The casts here are a hack. We have some interface inconsistencies
+ // between layout/picture rects which don't really work with the
+ // current unit system, since sometimes the local rect of a picture
+ // is a LayoutRect, and sometimes it's a PictureRect. Consider how
+ // we can improve this?
+ pic.composite_mode.as_ref().unwrap().get_coverage(
+ surface,
+ Some(rect.cast_unit()),
+ ).cast_unit()
+ }
+ None => {
+ return;
+ }
+ };
+
+ current_spatial_node_index = surface.surface_spatial_node_index;
+ }
+
+ current_pic_coverage_rect
+ };
+
+ // Get the tile coordinates in the picture space.
+ let (p0, p1) = self.get_tile_coords_for_rect(&pic_coverage_rect);
+
+ // If the primitive is outside the tiling rects, it's known to not
+ // be visible.
+ if p0.x == p1.x || p0.y == p1.y {
+ return;
+ }
+
+ // Build the list of resources that this primitive has dependencies on.
+ let mut prim_info = PrimitiveDependencyInfo::new(
+ prim_instance.uid(),
+ pic_coverage_rect,
+ );
+
+ let mut sub_slice_index = self.sub_slices.len() - 1;
+
+ // Only need to evaluate sub-slice regions if we have compositor surfaces present
+ if sub_slice_index > 0 {
+ // Find the first sub-slice we can add this primitive to (we want to add
+ // prims to the primary surface if possible, so they get subpixel AA).
+ for (i, sub_slice) in self.sub_slices.iter_mut().enumerate() {
+ let mut intersects_prohibited_region = false;
+
+ for surface in &mut sub_slice.compositor_surfaces {
+ if pic_coverage_rect.intersects(&surface.prohibited_rect) {
+ surface.prohibited_rect = surface.prohibited_rect.union(&pic_coverage_rect);
+
+ intersects_prohibited_region = true;
+ }
+ }
+
+ if !intersects_prohibited_region {
+ sub_slice_index = i;
+ break;
+ }
+ }
+ }
+
+ // Include the prim spatial node, if differs relative to cache root.
+ if prim_spatial_node_index != self.spatial_node_index {
+ prim_info.spatial_nodes.push(prim_spatial_node_index);
+ }
+
+ // If there was a clip chain, add any clip dependencies to the list for this tile.
+ let clip_instances = &clip_store
+ .clip_node_instances[prim_clip_chain.clips_range.to_range()];
+ for clip_instance in clip_instances {
+ let clip = &data_stores.clip[clip_instance.handle];
+
+ prim_info.clips.push(clip_instance.handle.uid());
+
+ // If the clip has the same spatial node, the relative transform
+ // will always be the same, so there's no need to depend on it.
+ if clip.item.spatial_node_index != self.spatial_node_index
+ && !prim_info.spatial_nodes.contains(&clip.item.spatial_node_index) {
+ prim_info.spatial_nodes.push(clip.item.spatial_node_index);
+ }
+ }
+
+ // Certain primitives may select themselves to be a backdrop candidate, which is
+ // then applied below.
+ let mut backdrop_candidate = None;
+
+ // For pictures, we don't (yet) know the valid clip rect, so we can't correctly
+ // use it to calculate the local bounding rect for the tiles. If we include them
+ // then we may calculate a bounding rect that is too large, since it won't include
+ // the clip bounds of the picture. Excluding them from the bounding rect here
+ // fixes any correctness issues (the clips themselves are considered when we
+ // consider the bounds of the primitives that are *children* of the picture),
+ // however it does potentially result in some un-necessary invalidations of a
+ // tile (in cases where the picture local rect affects the tile, but the clip
+ // rect eventually means it doesn't affect that tile).
+ // TODO(gw): Get picture clips earlier (during the initial picture traversal
+ // pass) so that we can calculate these correctly.
+ match prim_instance.kind {
+ PrimitiveInstanceKind::Picture { pic_index,.. } => {
+ // Pictures can depend on animated opacity bindings.
+ let pic = &pictures[pic_index.0];
+ if let Some(PictureCompositeMode::Filter(Filter::Opacity(binding, _))) = pic.composite_mode {
+ prim_info.opacity_bindings.push(binding.into());
+ }
+ }
+ PrimitiveInstanceKind::Rectangle { data_handle, color_binding_index, .. } => {
+ // Rectangles can only form a backdrop candidate if they are known opaque.
+ // TODO(gw): We could resolve the opacity binding here, but the common
+ // case for background rects is that they don't have animated opacity.
+ let color = match data_stores.prim[data_handle].kind {
+ PrimitiveTemplateKind::Rectangle { color, .. } => {
+ frame_context.scene_properties.resolve_color(&color)
+ }
+ _ => unreachable!(),
+ };
+ if color.a >= 1.0 {
+ backdrop_candidate = Some(BackdropInfo {
+ opaque_rect: pic_coverage_rect,
+ spanning_opaque_color: None,
+ kind: Some(BackdropKind::Color { color }),
+ backdrop_rect: pic_coverage_rect,
+ });
+ }
+
+ if color_binding_index != ColorBindingIndex::INVALID {
+ prim_info.color_binding = Some(color_bindings[color_binding_index].into());
+ }
+ }
+ PrimitiveInstanceKind::Image { data_handle, ref mut is_compositor_surface, .. } => {
+ let image_key = &data_stores.image[data_handle];
+ let image_data = &image_key.kind;
+
+ let mut promote_to_surface = false;
+ match self.can_promote_to_surface(image_key.common.flags,
+ prim_clip_chain,
+ prim_spatial_node_index,
+ is_root_tile_cache,
+ sub_slice_index,
+ frame_context) {
+ SurfacePromotionResult::Failed => {
+ }
+ SurfacePromotionResult::Success => {
+ promote_to_surface = true;
+ }
+ }
+
+ // Native OS compositors (DC and CA, at least) support premultiplied alpha
+ // only. If we have an image that's not pre-multiplied alpha, we can't promote it.
+ if image_data.alpha_type == AlphaType::Alpha {
+ promote_to_surface = false;
+ }
+
+ if let Some(image_properties) = resource_cache.get_image_properties(image_data.key) {
+ // For an image to be a possible opaque backdrop, it must:
+ // - Have a valid, opaque image descriptor
+ // - Not use tiling (since they can fail to draw)
+ // - Not having any spacing / padding
+ // - Have opaque alpha in the instance (flattened) color
+ if image_properties.descriptor.is_opaque() &&
+ image_properties.tiling.is_none() &&
+ image_data.tile_spacing == LayoutSize::zero() &&
+ image_data.color.a >= 1.0 {
+ backdrop_candidate = Some(BackdropInfo {
+ opaque_rect: pic_coverage_rect,
+ spanning_opaque_color: None,
+ kind: None,
+ backdrop_rect: PictureRect::zero(),
+ });
+ }
+ }
+
+ if promote_to_surface {
+ promote_to_surface = self.setup_compositor_surfaces_rgb(
+ sub_slice_index,
+ &mut prim_info,
+ image_key.common.flags,
+ local_prim_rect,
+ prim_spatial_node_index,
+ pic_coverage_rect,
+ frame_context,
+ ImageDependency {
+ key: image_data.key,
+ generation: resource_cache.get_image_generation(image_data.key),
+ },
+ image_data.key,
+ resource_cache,
+ composite_state,
+ gpu_cache,
+ image_data.image_rendering,
+ );
+ }
+
+ *is_compositor_surface = promote_to_surface;
+
+ if promote_to_surface {
+ prim_instance.vis.state = VisibilityState::Culled;
+ return;
+ } else {
+ prim_info.images.push(ImageDependency {
+ key: image_data.key,
+ generation: resource_cache.get_image_generation(image_data.key),
+ });
+ }
+ }
+ PrimitiveInstanceKind::YuvImage { data_handle, ref mut is_compositor_surface, .. } => {
+ let prim_data = &data_stores.yuv_image[data_handle];
+ let mut promote_to_surface = match self.can_promote_to_surface(
+ prim_data.common.flags,
+ prim_clip_chain,
+ prim_spatial_node_index,
+ is_root_tile_cache,
+ sub_slice_index,
+ frame_context) {
+ SurfacePromotionResult::Failed => false,
+ SurfacePromotionResult::Success => true,
+ };
+
+ // TODO(gw): When we support RGBA images for external surfaces, we also
+ // need to check if opaque (YUV images are implicitly opaque).
+
+ // If this primitive is being promoted to a surface, construct an external
+ // surface descriptor for use later during batching and compositing. We only
+ // add the image keys for this primitive as a dependency if this is _not_
+ // a promoted surface, since we don't want the tiles to invalidate when the
+ // video content changes, if it's a compositor surface!
+ if promote_to_surface {
+ // Build dependency for each YUV plane, with current image generation for
+ // later detection of when the composited surface has changed.
+ let mut image_dependencies = [ImageDependency::INVALID; 3];
+ for (key, dep) in prim_data.kind.yuv_key.iter().cloned().zip(image_dependencies.iter_mut()) {
+ *dep = ImageDependency {
+ key,
+ generation: resource_cache.get_image_generation(key),
+ }
+ }
+
+ promote_to_surface = self.setup_compositor_surfaces_yuv(
+ sub_slice_index,
+ &mut prim_info,
+ prim_data.common.flags,
+ local_prim_rect,
+ prim_spatial_node_index,
+ pic_coverage_rect,
+ frame_context,
+ &image_dependencies,
+ &prim_data.kind.yuv_key,
+ resource_cache,
+ composite_state,
+ gpu_cache,
+ prim_data.kind.image_rendering,
+ prim_data.kind.color_depth,
+ prim_data.kind.color_space.with_range(prim_data.kind.color_range),
+ prim_data.kind.format,
+ );
+ }
+
+ // Store on the YUV primitive instance whether this is a promoted surface.
+ // This is used by the batching code to determine whether to draw the
+ // image to the content tiles, or just a transparent z-write.
+ *is_compositor_surface = promote_to_surface;
+
+ if promote_to_surface {
+ prim_instance.vis.state = VisibilityState::Culled;
+ return;
+ } else {
+ prim_info.images.extend(
+ prim_data.kind.yuv_key.iter().map(|key| {
+ ImageDependency {
+ key: *key,
+ generation: resource_cache.get_image_generation(*key),
+ }
+ })
+ );
+ }
+ }
+ PrimitiveInstanceKind::ImageBorder { data_handle, .. } => {
+ let border_data = &data_stores.image_border[data_handle].kind;
+ prim_info.images.push(ImageDependency {
+ key: border_data.request.key,
+ generation: resource_cache.get_image_generation(border_data.request.key),
+ });
+ }
+ PrimitiveInstanceKind::Clear { .. } => {
+ backdrop_candidate = Some(BackdropInfo {
+ opaque_rect: pic_coverage_rect,
+ spanning_opaque_color: None,
+ kind: Some(BackdropKind::Clear),
+ backdrop_rect: pic_coverage_rect,
+ });
+ }
+ PrimitiveInstanceKind::LinearGradient { data_handle, .. }
+ | PrimitiveInstanceKind::CachedLinearGradient { data_handle, .. } => {
+ let gradient_data = &data_stores.linear_grad[data_handle];
+ if gradient_data.stops_opacity.is_opaque
+ && gradient_data.tile_spacing == LayoutSize::zero()
+ {
+ backdrop_candidate = Some(BackdropInfo {
+ opaque_rect: pic_coverage_rect,
+ spanning_opaque_color: None,
+ kind: None,
+ backdrop_rect: PictureRect::zero(),
+ });
+ }
+ }
+ PrimitiveInstanceKind::ConicGradient { data_handle, .. } => {
+ let gradient_data = &data_stores.conic_grad[data_handle];
+ if gradient_data.stops_opacity.is_opaque
+ && gradient_data.tile_spacing == LayoutSize::zero()
+ {
+ backdrop_candidate = Some(BackdropInfo {
+ opaque_rect: pic_coverage_rect,
+ spanning_opaque_color: None,
+ kind: None,
+ backdrop_rect: PictureRect::zero(),
+ });
+ }
+ }
+ PrimitiveInstanceKind::RadialGradient { data_handle, .. } => {
+ let gradient_data = &data_stores.radial_grad[data_handle];
+ if gradient_data.stops_opacity.is_opaque
+ && gradient_data.tile_spacing == LayoutSize::zero()
+ {
+ backdrop_candidate = Some(BackdropInfo {
+ opaque_rect: pic_coverage_rect,
+ spanning_opaque_color: None,
+ kind: None,
+ backdrop_rect: PictureRect::zero(),
+ });
+ }
+ }
+ PrimitiveInstanceKind::BackdropCapture { .. } => {}
+ PrimitiveInstanceKind::BackdropRender { pic_index, .. } => {
+ // If the area that the backdrop covers in the space of the surface it draws on
+ // is empty, skip any sub-graph processing. This is not just a performance win,
+ // it also ensures that we don't do a deferred dirty test that invalidates a tile
+ // even if the tile isn't actually dirty, which can cause panics later in the
+ // WR pipeline.
+ if !pic_coverage_rect.is_empty() {
+ // Mark that we need the sub-graph this render depends on so that
+ // we don't skip it during the prepare pass
+ scratch.required_sub_graphs.insert(pic_index);
+
+ // If this is a sub-graph, register the bounds on any affected tiles
+ // so we know how much to expand the content tile by.
+
+ // Implicitly, we know that any slice with a sub-graph disables compositor
+ // surface promotion, so sub_slice_index will always be 0.
+ debug_assert_eq!(sub_slice_index, 0);
+ let sub_slice = &mut self.sub_slices[sub_slice_index];
+
+ let mut surface_info = Vec::new();
+ for (pic_index, surface_index) in surface_stack.iter().rev() {
+ let pic = &pictures[pic_index.0];
+ surface_info.push((pic.composite_mode.as_ref().unwrap().clone(), *surface_index));
+ }
+
+ for y in p0.y .. p1.y {
+ for x in p0.x .. p1.x {
+ let key = TileOffset::new(x, y);
+ let tile = sub_slice.tiles.get_mut(&key).expect("bug: no tile");
+ tile.sub_graphs.push((pic_coverage_rect, surface_info.clone()));
+ }
+ }
+
+ // For backdrop-filter, we need to check if any of the dirty rects
+ // in tiles that are affected by the filter primitive are dirty.
+ self.deferred_dirty_tests.push(DeferredDirtyTest {
+ tile_rect: TileRect::new(p0, p1),
+ prim_rect: pic_coverage_rect,
+ });
+ }
+ }
+ PrimitiveInstanceKind::LineDecoration { .. } |
+ PrimitiveInstanceKind::NormalBorder { .. } |
+ PrimitiveInstanceKind::TextRun { .. } => {
+ // These don't contribute dependencies
+ }
+ };
+
+ // Calculate the screen rect in local space. When we calculate backdrops, we
+ // care only that they cover the visible rect, and don't have any overlapping
+ // prims in the visible rect.
+ let visible_local_rect = self.local_rect.intersection(&self.screen_rect_in_pic_space).unwrap_or_default();
+ if pic_coverage_rect.intersects(&visible_local_rect) {
+ self.found_prims_after_backdrop = true;
+ }
+
+ // If this primitive considers itself a backdrop candidate, apply further
+ // checks to see if it matches all conditions to be a backdrop.
+ let mut vis_flags = PrimitiveVisibilityFlags::empty();
+ let sub_slice = &mut self.sub_slices[sub_slice_index];
+ if let Some(mut backdrop_candidate) = backdrop_candidate {
+ // Update whether the surface that this primitive exists on
+ // can be considered opaque. Any backdrop kind other than
+ // a clear primitive (e.g. color, gradient, image) can be
+ // considered.
+ match backdrop_candidate.kind {
+ Some(BackdropKind::Color { .. }) | None => {
+ let surface = &mut surfaces[prim_surface_index.0];
+
+ let is_same_coord_system = frame_context.spatial_tree.is_matching_coord_system(
+ prim_spatial_node_index,
+ surface.surface_spatial_node_index,
+ );
+
+ // To be an opaque backdrop, it must:
+ // - Be the same coordinate system (axis-aligned)
+ // - Have no clip mask
+ // - Have a rect that covers the surface local rect
+ if is_same_coord_system &&
+ !prim_clip_chain.needs_mask &&
+ prim_clip_chain.pic_coverage_rect.contains_box(&surface.unclipped_local_rect)
+ {
+ // Note that we use `prim_clip_chain.pic_clip_rect` here rather
+ // than `backdrop_candidate.opaque_rect`. The former is in the
+ // local space of the surface, the latter is in the local space
+ // of the top level tile-cache.
+ surface.is_opaque = true;
+ }
+ }
+ Some(BackdropKind::Clear) => {}
+ }
+
+ let is_suitable_backdrop = match backdrop_candidate.kind {
+ Some(BackdropKind::Clear) => {
+ // Clear prims are special - they always end up in their own slice,
+ // and always set the backdrop. In future, we hope to completely
+ // remove clear prims, since they don't integrate with the compositing
+ // system cleanly.
+ true
+ }
+ Some(BackdropKind::Color { .. }) | None => {
+ // Check a number of conditions to see if we can consider this
+ // primitive as an opaque backdrop rect. Several of these are conservative
+ // checks and could be relaxed in future. However, these checks
+ // are quick and capture the common cases of background rects and images.
+ // Specifically, we currently require:
+ // - The primitive is on the main picture cache surface.
+ // - Same coord system as picture cache (ensures rects are axis-aligned).
+ // - No clip masks exist.
+ let same_coord_system = frame_context.spatial_tree.is_matching_coord_system(
+ prim_spatial_node_index,
+ self.spatial_node_index,
+ );
+
+ same_coord_system && on_picture_surface
+ }
+ };
+
+ if sub_slice_index == 0 &&
+ is_suitable_backdrop &&
+ sub_slice.compositor_surfaces.is_empty() {
+
+ // If the backdrop candidate has a clip-mask, try to extract an opaque inner
+ // rect that is safe to use for subpixel rendering
+ if prim_clip_chain.needs_mask {
+ backdrop_candidate.opaque_rect = clip_store
+ .get_inner_rect_for_clip_chain(
+ prim_clip_chain,
+ &data_stores.clip,
+ frame_context.spatial_tree,
+ )
+ .unwrap_or(PictureRect::zero());
+ }
+
+ // We set the backdrop opaque_rect here, indicating the coverage area, which
+ // is useful for calculate_subpixel_mode. We will only set the backdrop kind
+ // if it covers the visible rect.
+ if backdrop_candidate.opaque_rect.contains_box(&self.backdrop.opaque_rect) {
+ self.backdrop.opaque_rect = backdrop_candidate.opaque_rect;
+ }
+
+ if let Some(kind) = backdrop_candidate.kind {
+ if backdrop_candidate.opaque_rect.contains_box(&visible_local_rect) {
+ self.found_prims_after_backdrop = false;
+ self.backdrop.kind = Some(kind);
+ self.backdrop.backdrop_rect = backdrop_candidate.opaque_rect;
+
+ // If we have a color backdrop that spans the entire local rect, mark
+ // the visibility flags of the primitive so it is skipped during batching
+ // (and also clears any previous primitives). Additionally, update our
+ // background color to match the backdrop color, which will ensure that
+ // our tiles are cleared to this color.
+ if let BackdropKind::Color { color } = kind {
+ if backdrop_candidate.opaque_rect.contains_box(&self.local_rect) {
+ vis_flags |= PrimitiveVisibilityFlags::IS_BACKDROP;
+ self.backdrop.spanning_opaque_color = Some(color);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Record any new spatial nodes in the used list.
+ for spatial_node_index in &prim_info.spatial_nodes {
+ self.spatial_node_comparer.register_used_transform(
+ *spatial_node_index,
+ self.frame_id,
+ frame_context.spatial_tree,
+ );
+ }
+
+ // Normalize the tile coordinates before adding to tile dependencies.
+ // For each affected tile, mark any of the primitive dependencies.
+ for y in p0.y .. p1.y {
+ for x in p0.x .. p1.x {
+ // TODO(gw): Convert to 2d array temporarily to avoid hash lookups per-tile?
+ let key = TileOffset::new(x, y);
+ let tile = sub_slice.tiles.get_mut(&key).expect("bug: no tile");
+
+ tile.add_prim_dependency(&prim_info);
+ }
+ }
+
+ prim_instance.vis.state = VisibilityState::Visible {
+ vis_flags,
+ sub_slice_index: SubSliceIndex::new(sub_slice_index),
+ };
+ }
+
+ /// Print debug information about this picture cache to a tree printer.
+ fn print(&self) {
+ // TODO(gw): This initial implementation is very basic - just printing
+ // the picture cache state to stdout. In future, we can
+ // make this dump each frame to a file, and produce a report
+ // stating which frames had invalidations. This will allow
+ // diff'ing the invalidation states in a visual tool.
+ let mut pt = PrintTree::new("Picture Cache");
+
+ pt.new_level(format!("Slice {:?}", self.slice));
+
+ pt.add_item(format!("background_color: {:?}", self.background_color));
+
+ for (sub_slice_index, sub_slice) in self.sub_slices.iter().enumerate() {
+ pt.new_level(format!("SubSlice {:?}", sub_slice_index));
+
+ for y in self.tile_bounds_p0.y .. self.tile_bounds_p1.y {
+ for x in self.tile_bounds_p0.x .. self.tile_bounds_p1.x {
+ let key = TileOffset::new(x, y);
+ let tile = &sub_slice.tiles[&key];
+ tile.print(&mut pt);
+ }
+ }
+
+ pt.end_level();
+ }
+
+ pt.end_level();
+ }
+
+ fn calculate_subpixel_mode(&self) -> SubpixelMode {
+ let has_opaque_bg_color = self.background_color.map_or(false, |c| c.a >= 1.0);
+
+ // If the overall tile cache is known opaque, subpixel AA is allowed everywhere
+ if has_opaque_bg_color {
+ return SubpixelMode::Allow;
+ }
+
+ // If we didn't find any valid opaque backdrop, no subpixel AA allowed
+ if self.backdrop.opaque_rect.is_empty() {
+ return SubpixelMode::Deny;
+ }
+
+ // If the opaque backdrop rect covers the entire tile cache surface,
+ // we can allow subpixel AA anywhere, skipping the per-text-run tests
+ // later on during primitive preparation.
+ if self.backdrop.opaque_rect.contains_box(&self.local_rect) {
+ return SubpixelMode::Allow;
+ }
+
+ // If none of the simple cases above match, we need test where we can support subpixel AA.
+ // TODO(gw): In future, it may make sense to have > 1 inclusion rect,
+ // but this handles the common cases.
+ // TODO(gw): If a text run gets animated such that it's moving in a way that is
+ // sometimes intersecting with the video rect, this can result in subpixel
+ // AA flicking on/off for that text run. It's probably very rare, but
+ // something we should handle in future.
+ SubpixelMode::Conditional {
+ allowed_rect: self.backdrop.opaque_rect,
+ }
+ }
+
+ /// Apply any updates after prim dependency updates. This applies
+ /// any late tile invalidations, and sets up the dirty rect and
+ /// set of tile blits.
+ pub fn post_update(
+ &mut self,
+ frame_context: &FrameVisibilityContext,
+ frame_state: &mut FrameVisibilityState,
+ ) {
+ assert!(self.current_surface_traversal_depth == 0);
+
+ self.dirty_region.reset(self.spatial_node_index);
+ self.subpixel_mode = self.calculate_subpixel_mode();
+
+ self.transform_index = frame_state.composite_state.register_transform(
+ self.local_to_surface,
+ // TODO(gw): Once we support scaling of picture cache tiles during compositing,
+ // that transform gets plugged in here!
+ self.surface_to_device,
+ );
+
+ let map_pic_to_world = SpaceMapper::new_with_target(
+ frame_context.root_spatial_node_index,
+ self.spatial_node_index,
+ frame_context.global_screen_world_rect,
+ frame_context.spatial_tree,
+ );
+
+ // A simple GC of the native external surface cache, to remove and free any
+ // surfaces that were not referenced during the update_prim_dependencies pass.
+ self.external_native_surface_cache.retain(|_, surface| {
+ if !surface.used_this_frame {
+ // If we removed an external surface, we need to mark the dirty rects as
+ // invalid so a full composite occurs on the next frame.
+ frame_state.composite_state.dirty_rects_are_valid = false;
+
+ frame_state.resource_cache.destroy_compositor_surface(surface.native_surface_id);
+ }
+
+ surface.used_this_frame
+ });
+
+ let pic_to_world_mapper = SpaceMapper::new_with_target(
+ frame_context.root_spatial_node_index,
+ self.spatial_node_index,
+ frame_context.global_screen_world_rect,
+ frame_context.spatial_tree,
+ );
+
+ let ctx = TileUpdateDirtyContext {
+ pic_to_world_mapper,
+ global_device_pixel_scale: frame_context.global_device_pixel_scale,
+ opacity_bindings: &self.opacity_bindings,
+ color_bindings: &self.color_bindings,
+ local_rect: self.local_rect,
+ invalidate_all: self.invalidate_all_tiles,
+ };
+
+ let mut state = TileUpdateDirtyState {
+ resource_cache: frame_state.resource_cache,
+ composite_state: frame_state.composite_state,
+ compare_cache: &mut self.compare_cache,
+ spatial_node_comparer: &mut self.spatial_node_comparer,
+ };
+
+ // Step through each tile and invalidate if the dependencies have changed. Determine
+ // the current opacity setting and whether it's changed.
+ for sub_slice in &mut self.sub_slices {
+ for tile in sub_slice.tiles.values_mut() {
+ tile.update_dirty_and_valid_rects(&ctx, &mut state, frame_context);
+ }
+ }
+
+ // Process any deferred dirty checks
+ for sub_slice in &mut self.sub_slices {
+ for dirty_test in self.deferred_dirty_tests.drain(..) {
+ // Calculate the total dirty rect from all tiles that this primitive affects
+ let mut total_dirty_rect = PictureRect::zero();
+
+ for y in dirty_test.tile_rect.min.y .. dirty_test.tile_rect.max.y {
+ for x in dirty_test.tile_rect.min.x .. dirty_test.tile_rect.max.x {
+ let key = TileOffset::new(x, y);
+ let tile = sub_slice.tiles.get_mut(&key).expect("bug: no tile");
+ total_dirty_rect = total_dirty_rect.union(&tile.local_dirty_rect);
+ }
+ }
+
+ // If that dirty rect intersects with the local rect of the primitive
+ // being checked, invalidate that region in all of the affected tiles.
+ // TODO(gw): This is somewhat conservative, we could be more clever
+ // here and avoid invalidating every tile when this changes.
+ // We could also store the dirty rect only when the prim
+ // is encountered, so that we don't invalidate if something
+ // *after* the query in the rendering order affects invalidation.
+ if total_dirty_rect.intersects(&dirty_test.prim_rect) {
+ for y in dirty_test.tile_rect.min.y .. dirty_test.tile_rect.max.y {
+ for x in dirty_test.tile_rect.min.x .. dirty_test.tile_rect.max.x {
+ let key = TileOffset::new(x, y);
+ let tile = sub_slice.tiles.get_mut(&key).expect("bug: no tile");
+ tile.invalidate(
+ Some(dirty_test.prim_rect),
+ InvalidationReason::SurfaceContentChanged,
+ );
+ }
+ }
+ }
+ }
+ }
+
+ let mut ctx = TilePostUpdateContext {
+ local_clip_rect: self.local_clip_rect,
+ backdrop: None,
+ current_tile_size: self.current_tile_size,
+ z_id: ZBufferId::invalid(),
+ };
+
+ let mut state = TilePostUpdateState {
+ resource_cache: frame_state.resource_cache,
+ composite_state: frame_state.composite_state,
+ };
+
+ for (i, sub_slice) in self.sub_slices.iter_mut().enumerate().rev() {
+ // The backdrop is only relevant for the first sub-slice
+ if i == 0 {
+ ctx.backdrop = Some(self.backdrop);
+ }
+
+ for compositor_surface in sub_slice.compositor_surfaces.iter_mut().rev() {
+ compositor_surface.descriptor.z_id = state.composite_state.z_generator.next();
+ }
+
+ ctx.z_id = state.composite_state.z_generator.next();
+
+ for tile in sub_slice.tiles.values_mut() {
+ tile.post_update(&ctx, &mut state, frame_context);
+ }
+ }
+
+ // Register any opaque external compositor surfaces as potential occluders. This
+ // is especially useful when viewing video in full-screen mode, as it is
+ // able to occlude every background tile (avoiding allocation, rasterizion
+ // and compositing).
+
+ for sub_slice in &self.sub_slices {
+ for compositor_surface in &sub_slice.compositor_surfaces {
+ if compositor_surface.is_opaque {
+ let local_surface_rect = compositor_surface
+ .descriptor
+ .local_rect
+ .intersection(&compositor_surface.descriptor.local_clip_rect)
+ .and_then(|r| {
+ r.intersection(&self.local_clip_rect)
+ });
+
+ if let Some(local_surface_rect) = local_surface_rect {
+ let world_surface_rect = map_pic_to_world
+ .map(&local_surface_rect)
+ .expect("bug: unable to map external surface to world space");
+
+ frame_state.composite_state.register_occluder(
+ compositor_surface.descriptor.z_id,
+ world_surface_rect,
+ );
+ }
+ }
+ }
+ }
+
+ // Register the opaque region of this tile cache as an occluder, which
+ // is used later in the frame to occlude other tiles.
+ if !self.backdrop.opaque_rect.is_empty() {
+ let z_id_backdrop = frame_state.composite_state.z_generator.next();
+
+ let backdrop_rect = self.backdrop.opaque_rect
+ .intersection(&self.local_rect)
+ .and_then(|r| {
+ r.intersection(&self.local_clip_rect)
+ });
+
+ if let Some(backdrop_rect) = backdrop_rect {
+ let world_backdrop_rect = map_pic_to_world
+ .map(&backdrop_rect)
+ .expect("bug: unable to map backdrop to world space");
+
+ // Since we register the entire backdrop rect, use the opaque z-id for the
+ // picture cache slice.
+ frame_state.composite_state.register_occluder(
+ z_id_backdrop,
+ world_backdrop_rect,
+ );
+ }
+ }
+ }
+}
+
+pub struct PictureScratchBuffer {
+ surface_stack: Vec<SurfaceIndex>,
+}
+
+impl Default for PictureScratchBuffer {
+ fn default() -> Self {
+ PictureScratchBuffer {
+ surface_stack: Vec::new(),
+ }
+ }
+}
+
+impl PictureScratchBuffer {
+ pub fn begin_frame(&mut self) {
+ self.surface_stack.clear();
+ }
+
+ pub fn recycle(&mut self, recycler: &mut Recycler) {
+ recycler.recycle_vec(&mut self.surface_stack);
+ }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SurfaceIndex(pub usize);
+
+/// Information about an offscreen surface. For now,
+/// it contains information about the size and coordinate
+/// system of the surface. In the future, it will contain
+/// information about the contents of the surface, which
+/// will allow surfaces to be cached / retained between
+/// frames and display lists.
+pub struct SurfaceInfo {
+ /// A local rect defining the size of this surface, in the
+ /// coordinate system of the surface itself. This contains
+ /// the unclipped bounding rect of child primitives.
+ pub unclipped_local_rect: PictureRect,
+ /// The local space coverage of child primitives after they are
+ /// are clipped to their owning clip-chain.
+ pub clipped_local_rect: PictureRect,
+ /// If true, we know this surface is completely opaque
+ pub is_opaque: bool,
+ /// The (conservative) valid part of this surface rect. Used
+ /// to reduce the size of render target allocation.
+ pub clipping_rect: PictureRect,
+ /// Helper structs for mapping local rects in different
+ /// coordinate systems into the surface coordinates.
+ pub map_local_to_surface: SpaceMapper<LayoutPixel, PicturePixel>,
+ /// Defines the positioning node for the surface itself,
+ /// and the rasterization root for this surface.
+ pub raster_spatial_node_index: SpatialNodeIndex,
+ pub surface_spatial_node_index: SpatialNodeIndex,
+ /// The device pixel ratio specific to this surface.
+ pub device_pixel_scale: DevicePixelScale,
+ /// The scale factors of the surface to world transform.
+ pub world_scale_factors: (f32, f32),
+ /// Local scale factors surface to raster transform
+ pub local_scale: (f32, f32),
+ /// If true, allow snapping on this and child surfaces
+ pub allow_snapping: bool,
+}
+
+impl SurfaceInfo {
+ pub fn new(
+ surface_spatial_node_index: SpatialNodeIndex,
+ raster_spatial_node_index: SpatialNodeIndex,
+ world_rect: WorldRect,
+ spatial_tree: &SpatialTree,
+ device_pixel_scale: DevicePixelScale,
+ world_scale_factors: (f32, f32),
+ local_scale: (f32, f32),
+ allow_snapping: bool,
+ ) -> Self {
+ let map_surface_to_world = SpaceMapper::new_with_target(
+ spatial_tree.root_reference_frame_index(),
+ surface_spatial_node_index,
+ world_rect,
+ spatial_tree,
+ );
+
+ let pic_bounds = map_surface_to_world
+ .unmap(&map_surface_to_world.bounds)
+ .unwrap_or_else(PictureRect::max_rect);
+
+ let map_local_to_surface = SpaceMapper::new(
+ surface_spatial_node_index,
+ pic_bounds,
+ );
+
+ SurfaceInfo {
+ unclipped_local_rect: PictureRect::zero(),
+ clipped_local_rect: PictureRect::zero(),
+ is_opaque: false,
+ clipping_rect: PictureRect::zero(),
+ map_local_to_surface,
+ raster_spatial_node_index,
+ surface_spatial_node_index,
+ device_pixel_scale,
+ world_scale_factors,
+ local_scale,
+ allow_snapping,
+ }
+ }
+
+ /// Clamps the blur radius depending on scale factors.
+ pub fn clamp_blur_radius(
+ &self,
+ x_blur_radius: f32,
+ y_blur_radius: f32,
+ ) -> (f32, f32) {
+ // Clamping must occur after scale factors are applied, but scale factors are not applied
+ // until later on. To clamp the blur radius, we first apply the scale factors and then clamp
+ // and finally revert the scale factors.
+
+ let sx_blur_radius = x_blur_radius * self.local_scale.0;
+ let sy_blur_radius = y_blur_radius * self.local_scale.1;
+
+ let largest_scaled_blur_radius = f32::max(
+ sx_blur_radius * self.world_scale_factors.0,
+ sy_blur_radius * self.world_scale_factors.1,
+ );
+
+ if largest_scaled_blur_radius > MAX_BLUR_RADIUS {
+ let sf = MAX_BLUR_RADIUS / largest_scaled_blur_radius;
+ (x_blur_radius * sf, y_blur_radius * sf)
+ } else {
+ // Return the original blur radius to avoid any rounding errors
+ (x_blur_radius, y_blur_radius)
+ }
+ }
+
+ pub fn map_to_device_rect(
+ &self,
+ local_rect: &PictureRect,
+ spatial_tree: &SpatialTree,
+ ) -> DeviceRect {
+ let raster_rect = if self.raster_spatial_node_index != self.surface_spatial_node_index {
+ assert_eq!(self.device_pixel_scale.0, 1.0);
+
+ let local_to_world = SpaceMapper::new_with_target(
+ spatial_tree.root_reference_frame_index(),
+ self.surface_spatial_node_index,
+ WorldRect::max_rect(),
+ spatial_tree,
+ );
+
+ local_to_world.map(&local_rect).unwrap()
+ } else {
+ local_rect.cast_unit()
+ };
+
+ raster_rect * self.device_pixel_scale
+ }
+
+ /// Clip and transform a local rect to a device rect suitable for allocating
+ /// a child off-screen surface of this surface (e.g. for clip-masks)
+ pub fn get_surface_rect(
+ &self,
+ local_rect: &PictureRect,
+ spatial_tree: &SpatialTree,
+ ) -> Option<DeviceRect> {
+ let local_rect = match local_rect.intersection(&self.clipping_rect) {
+ Some(rect) => rect,
+ None => return None,
+ };
+
+ let raster_rect = if self.raster_spatial_node_index != self.surface_spatial_node_index {
+ assert_eq!(self.device_pixel_scale.0, 1.0);
+
+ let local_to_world = SpaceMapper::new_with_target(
+ spatial_tree.root_reference_frame_index(),
+ self.surface_spatial_node_index,
+ WorldRect::max_rect(),
+ spatial_tree,
+ );
+
+ local_to_world.map(&local_rect).unwrap()
+ } else {
+ local_rect.cast_unit()
+ };
+
+ Some((raster_rect * self.device_pixel_scale).round_out())
+ }
+}
+
+/// Information from `get_surface_rects` about the allocated size, UV sampling
+/// parameters etc for an off-screen surface
+struct SurfaceAllocInfo {
+ task_size: DeviceIntSize,
+ needs_scissor_rect: bool,
+ clipped: DeviceRect,
+ unclipped: DeviceRect,
+ clipped_local: PictureRect,
+ uv_rect_kind: UvRectKind,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct RasterConfig {
+ /// How this picture should be composited into
+ /// the parent surface.
+ // TODO(gw): We should remove this and just use what is in PicturePrimitive
+ pub composite_mode: PictureCompositeMode,
+ /// Index to the surface descriptor for this
+ /// picture.
+ pub surface_index: SurfaceIndex,
+}
+
+bitflags! {
+ /// A set of flags describing why a picture may need a backing surface.
+ #[cfg_attr(feature = "capture", derive(Serialize))]
+ pub struct BlitReason: u32 {
+ /// Mix-blend-mode on a child that requires isolation.
+ const ISOLATE = 1;
+ /// Clip node that _might_ require a surface.
+ const CLIP = 2;
+ /// Preserve-3D requires a surface for plane-splitting.
+ const PRESERVE3D = 4;
+ /// A backdrop that is reused which requires a surface.
+ const BACKDROP = 8;
+ }
+}
+
+/// Specifies how this Picture should be composited
+/// onto the target it belongs to.
+#[allow(dead_code)]
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub enum PictureCompositeMode {
+ /// Apply CSS mix-blend-mode effect.
+ MixBlend(MixBlendMode),
+ /// Apply a CSS filter (except component transfer).
+ Filter(Filter),
+ /// Apply a component transfer filter.
+ ComponentTransferFilter(FilterDataHandle),
+ /// Draw to intermediate surface, copy straight across. This
+ /// is used for CSS isolation, and plane splitting.
+ Blit(BlitReason),
+ /// Used to cache a picture as a series of tiles.
+ TileCache {
+ slice_id: SliceId,
+ },
+ /// Apply an SVG filter
+ SvgFilter(Vec<FilterPrimitive>, Vec<SFilterData>),
+ /// A surface that is used as an input to another primitive
+ IntermediateSurface,
+}
+
+impl PictureCompositeMode {
+ pub fn get_rect(
+ &self,
+ surface: &SurfaceInfo,
+ sub_rect: Option<LayoutRect>,
+ ) -> LayoutRect {
+ let surface_rect = match sub_rect {
+ Some(sub_rect) => sub_rect,
+ None => surface.clipped_local_rect.cast_unit(),
+ };
+
+ match self {
+ PictureCompositeMode::Filter(Filter::Blur { width, height, should_inflate }) => {
+ if *should_inflate {
+ let (width_factor, height_factor) = surface.clamp_blur_radius(*width, *height);
+
+ surface_rect.inflate(
+ width_factor.ceil() * BLUR_SAMPLE_SCALE,
+ height_factor.ceil() * BLUR_SAMPLE_SCALE,
+ )
+ } else {
+ surface_rect
+ }
+ }
+ PictureCompositeMode::Filter(Filter::DropShadows(ref shadows)) => {
+ let mut max_blur_radius = 0.0;
+ for shadow in shadows {
+ max_blur_radius = f32::max(max_blur_radius, shadow.blur_radius);
+ }
+
+ let (max_blur_radius_x, max_blur_radius_y) = surface.clamp_blur_radius(
+ max_blur_radius,
+ max_blur_radius,
+ );
+ let blur_inflation_x = max_blur_radius_x * BLUR_SAMPLE_SCALE;
+ let blur_inflation_y = max_blur_radius_y * BLUR_SAMPLE_SCALE;
+
+ surface_rect.inflate(blur_inflation_x, blur_inflation_y)
+ }
+ PictureCompositeMode::SvgFilter(primitives, _) => {
+ let mut result_rect = surface_rect;
+ let mut output_rects = Vec::with_capacity(primitives.len());
+
+ for (cur_index, primitive) in primitives.iter().enumerate() {
+ let output_rect = match primitive.kind {
+ FilterPrimitiveKind::Blur(ref primitive) => {
+ let input = primitive.input.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(surface_rect);
+ let width_factor = primitive.width.round() * BLUR_SAMPLE_SCALE;
+ let height_factor = primitive.height.round() * BLUR_SAMPLE_SCALE;
+ input.inflate(width_factor, height_factor)
+ }
+ FilterPrimitiveKind::DropShadow(ref primitive) => {
+ let inflation_factor = primitive.shadow.blur_radius.ceil() * BLUR_SAMPLE_SCALE;
+ let input = primitive.input.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(surface_rect);
+ let shadow_rect = input.inflate(inflation_factor, inflation_factor);
+ input.union(&shadow_rect.translate(primitive.shadow.offset * Scale::new(1.0)))
+ }
+ FilterPrimitiveKind::Blend(ref primitive) => {
+ primitive.input1.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(surface_rect)
+ .union(&primitive.input2.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(surface_rect))
+ }
+ FilterPrimitiveKind::Composite(ref primitive) => {
+ primitive.input1.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(surface_rect)
+ .union(&primitive.input2.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(surface_rect))
+ }
+ FilterPrimitiveKind::Identity(ref primitive) =>
+ primitive.input.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(surface_rect),
+ FilterPrimitiveKind::Opacity(ref primitive) =>
+ primitive.input.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(surface_rect),
+ FilterPrimitiveKind::ColorMatrix(ref primitive) =>
+ primitive.input.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(surface_rect),
+ FilterPrimitiveKind::ComponentTransfer(ref primitive) =>
+ primitive.input.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(surface_rect),
+ FilterPrimitiveKind::Offset(ref primitive) => {
+ let input_rect = primitive.input.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(surface_rect);
+ input_rect.translate(primitive.offset * Scale::new(1.0))
+ },
+
+ FilterPrimitiveKind::Flood(..) => surface_rect,
+ };
+ output_rects.push(output_rect);
+ result_rect = result_rect.union(&output_rect);
+ }
+ result_rect
+ }
+ _ => {
+ surface_rect
+ }
+ }
+ }
+
+ pub fn get_coverage(
+ &self,
+ surface: &SurfaceInfo,
+ sub_rect: Option<LayoutRect>,
+ ) -> LayoutRect {
+ let surface_rect = match sub_rect {
+ Some(sub_rect) => sub_rect,
+ None => surface.clipped_local_rect.cast_unit(),
+ };
+
+ match self {
+ PictureCompositeMode::Filter(Filter::Blur { width, height, should_inflate }) => {
+ if *should_inflate {
+ let (width_factor, height_factor) = surface.clamp_blur_radius(*width, *height);
+
+ surface_rect.inflate(
+ width_factor.ceil() * BLUR_SAMPLE_SCALE,
+ height_factor.ceil() * BLUR_SAMPLE_SCALE,
+ )
+ } else {
+ surface_rect
+ }
+ }
+ PictureCompositeMode::Filter(Filter::DropShadows(ref shadows)) => {
+ let mut rect = surface_rect;
+
+ for shadow in shadows {
+ let (blur_radius_x, blur_radius_y) = surface.clamp_blur_radius(
+ shadow.blur_radius,
+ shadow.blur_radius,
+ );
+ let blur_inflation_x = blur_radius_x * BLUR_SAMPLE_SCALE;
+ let blur_inflation_y = blur_radius_y * BLUR_SAMPLE_SCALE;
+
+ let shadow_rect = surface_rect
+ .translate(shadow.offset)
+ .inflate(blur_inflation_x, blur_inflation_y);
+ rect = rect.union(&shadow_rect);
+ }
+
+ rect
+ }
+ PictureCompositeMode::SvgFilter(primitives, _) => {
+ let mut result_rect = surface_rect;
+ let mut output_rects = Vec::with_capacity(primitives.len());
+
+ for (cur_index, primitive) in primitives.iter().enumerate() {
+ let output_rect = match primitive.kind {
+ FilterPrimitiveKind::Blur(ref primitive) => {
+ let input = primitive.input.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(surface_rect);
+ let width_factor = primitive.width.round() * BLUR_SAMPLE_SCALE;
+ let height_factor = primitive.height.round() * BLUR_SAMPLE_SCALE;
+
+ input.inflate(width_factor, height_factor)
+ }
+ FilterPrimitiveKind::DropShadow(ref primitive) => {
+ let inflation_factor = primitive.shadow.blur_radius.ceil() * BLUR_SAMPLE_SCALE;
+ let input = primitive.input.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(surface_rect);
+ let shadow_rect = input.inflate(inflation_factor, inflation_factor);
+ input.union(&shadow_rect.translate(primitive.shadow.offset * Scale::new(1.0)))
+ }
+ FilterPrimitiveKind::Blend(ref primitive) => {
+ primitive.input1.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(surface_rect)
+ .union(&primitive.input2.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(surface_rect))
+ }
+ FilterPrimitiveKind::Composite(ref primitive) => {
+ primitive.input1.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(surface_rect)
+ .union(&primitive.input2.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(surface_rect))
+ }
+ FilterPrimitiveKind::Identity(ref primitive) =>
+ primitive.input.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(surface_rect),
+ FilterPrimitiveKind::Opacity(ref primitive) =>
+ primitive.input.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(surface_rect),
+ FilterPrimitiveKind::ColorMatrix(ref primitive) =>
+ primitive.input.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(surface_rect),
+ FilterPrimitiveKind::ComponentTransfer(ref primitive) =>
+ primitive.input.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(surface_rect),
+ FilterPrimitiveKind::Offset(ref primitive) => {
+ let input_rect = primitive.input.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(surface_rect);
+ input_rect.translate(primitive.offset * Scale::new(1.0))
+ },
+
+ FilterPrimitiveKind::Flood(..) => surface_rect,
+ };
+ output_rects.push(output_rect);
+ result_rect = result_rect.union(&output_rect);
+ }
+ result_rect
+ }
+ _ => {
+ surface_rect
+ }
+ }
+ }
+}
+
+/// Enum value describing the place of a picture in a 3D context.
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub enum Picture3DContext<C> {
+ /// The picture is not a part of 3D context sub-hierarchy.
+ Out,
+ /// The picture is a part of 3D context.
+ In {
+ /// Additional data per child for the case of this a root of 3D hierarchy.
+ root_data: Option<Vec<C>>,
+ /// The spatial node index of an "ancestor" element, i.e. one
+ /// that establishes the transformed element's containing block.
+ ///
+ /// See CSS spec draft for more details:
+ /// https://drafts.csswg.org/css-transforms-2/#accumulated-3d-transformation-matrix-computation
+ ancestor_index: SpatialNodeIndex,
+ /// Index in the built scene's array of plane splitters.
+ plane_splitter_index: PlaneSplitterIndex,
+ },
+}
+
+/// Information about a preserve-3D hierarchy child that has been plane-split
+/// and ordered according to the view direction.
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct OrderedPictureChild {
+ pub anchor: PlaneSplitAnchor,
+ pub gpu_address: GpuCacheAddress,
+}
+
+bitflags! {
+ /// A set of flags describing why a picture may need a backing surface.
+ #[cfg_attr(feature = "capture", derive(Serialize))]
+ pub struct ClusterFlags: u32 {
+ /// Whether this cluster is visible when the position node is a backface.
+ const IS_BACKFACE_VISIBLE = 1;
+ /// This flag is set during the first pass picture traversal, depending on whether
+ /// the cluster is visible or not. It's read during the second pass when primitives
+ /// consult their owning clusters to see if the primitive itself is visible.
+ const IS_VISIBLE = 2;
+ }
+}
+
+/// Descriptor for a cluster of primitives. For now, this is quite basic but will be
+/// extended to handle more spatial clustering of primitives.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct PrimitiveCluster {
+ /// The positioning node for this cluster.
+ pub spatial_node_index: SpatialNodeIndex,
+ /// The bounding rect of the cluster, in the local space of the spatial node.
+ /// This is used to quickly determine the overall bounding rect for a picture
+ /// during the first picture traversal, which is needed for local scale
+ /// determination, and render task size calculations.
+ bounding_rect: LayoutRect,
+ /// a part of the cluster that we know to be opaque if any. Does not always
+ /// describe the entire opaque region, but all content within that rect must
+ /// be opaque.
+ pub opaque_rect: LayoutRect,
+ /// The range of primitive instance indices associated with this cluster.
+ pub prim_range: Range<usize>,
+ /// Various flags / state for this cluster.
+ pub flags: ClusterFlags,
+}
+
+impl PrimitiveCluster {
+ /// Construct a new primitive cluster for a given positioning node.
+ fn new(
+ spatial_node_index: SpatialNodeIndex,
+ flags: ClusterFlags,
+ first_instance_index: usize,
+ ) -> Self {
+ PrimitiveCluster {
+ bounding_rect: LayoutRect::zero(),
+ opaque_rect: LayoutRect::zero(),
+ spatial_node_index,
+ flags,
+ prim_range: first_instance_index..first_instance_index
+ }
+ }
+
+ /// Return true if this cluster is compatible with the given params
+ pub fn is_compatible(
+ &self,
+ spatial_node_index: SpatialNodeIndex,
+ flags: ClusterFlags,
+ instance_index: usize,
+ ) -> bool {
+ self.flags == flags &&
+ self.spatial_node_index == spatial_node_index &&
+ instance_index == self.prim_range.end
+ }
+
+ pub fn prim_range(&self) -> Range<usize> {
+ self.prim_range.clone()
+ }
+
+ /// Add a primitive instance to this cluster, at the start or end
+ fn add_instance(
+ &mut self,
+ culling_rect: &LayoutRect,
+ instance_index: usize,
+ ) {
+ debug_assert_eq!(instance_index, self.prim_range.end);
+ self.bounding_rect = self.bounding_rect.union(culling_rect);
+ self.prim_range.end += 1;
+ }
+}
+
+/// A list of primitive instances that are added to a picture
+/// This ensures we can keep a list of primitives that
+/// are pictures, for a fast initial traversal of the picture
+/// tree without walking the instance list.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct PrimitiveList {
+ /// List of primitives grouped into clusters.
+ pub clusters: Vec<PrimitiveCluster>,
+ pub child_pictures: Vec<PictureIndex>,
+ /// The number of preferred compositor surfaces that were found when
+ /// adding prims to this list.
+ pub compositor_surface_count: usize,
+}
+
+impl PrimitiveList {
+ /// Construct an empty primitive list. This is
+ /// just used during the take_context / restore_context
+ /// borrow check dance, which will be removed as the
+ /// picture traversal pass is completed.
+ pub fn empty() -> Self {
+ PrimitiveList {
+ clusters: Vec::new(),
+ child_pictures: Vec::new(),
+ compositor_surface_count: 0,
+ }
+ }
+
+ pub fn merge(&mut self, other: PrimitiveList) {
+ self.clusters.extend(other.clusters);
+ self.child_pictures.extend(other.child_pictures);
+ self.compositor_surface_count += other.compositor_surface_count;
+ }
+
+ /// Add a primitive instance to the end of the list
+ pub fn add_prim(
+ &mut self,
+ prim_instance: PrimitiveInstance,
+ prim_rect: LayoutRect,
+ spatial_node_index: SpatialNodeIndex,
+ prim_flags: PrimitiveFlags,
+ prim_instances: &mut Vec<PrimitiveInstance>,
+ clip_tree_builder: &ClipTreeBuilder,
+ ) {
+ let mut flags = ClusterFlags::empty();
+
+ // Pictures are always put into a new cluster, to make it faster to
+ // iterate all pictures in a given primitive list.
+ match prim_instance.kind {
+ PrimitiveInstanceKind::Picture { pic_index, .. } => {
+ self.child_pictures.push(pic_index);
+ }
+ _ => {}
+ }
+
+ if prim_flags.contains(PrimitiveFlags::IS_BACKFACE_VISIBLE) {
+ flags.insert(ClusterFlags::IS_BACKFACE_VISIBLE);
+ }
+
+ if prim_flags.contains(PrimitiveFlags::PREFER_COMPOSITOR_SURFACE) {
+ self.compositor_surface_count += 1;
+ }
+
+ let clip_leaf = clip_tree_builder.get_leaf(prim_instance.clip_leaf_id);
+ let culling_rect = clip_leaf.local_clip_rect
+ .intersection(&prim_rect)
+ .unwrap_or_else(LayoutRect::zero);
+
+ let instance_index = prim_instances.len();
+ prim_instances.push(prim_instance);
+
+ if let Some(cluster) = self.clusters.last_mut() {
+ if cluster.is_compatible(spatial_node_index, flags, instance_index) {
+ cluster.add_instance(&culling_rect, instance_index);
+ return;
+ }
+ }
+
+ // Same idea with clusters, using a different distribution.
+ let clusters_len = self.clusters.len();
+ if clusters_len == self.clusters.capacity() {
+ let next_alloc = match clusters_len {
+ 1 ..= 15 => 16 - clusters_len,
+ 16 ..= 127 => 128 - clusters_len,
+ _ => clusters_len * 2,
+ };
+
+ self.clusters.reserve(next_alloc);
+ }
+
+ let mut cluster = PrimitiveCluster::new(
+ spatial_node_index,
+ flags,
+ instance_index,
+ );
+
+ cluster.add_instance(&culling_rect, instance_index);
+ self.clusters.push(cluster);
+ }
+
+ /// Returns true if there are no clusters (and thus primitives)
+ pub fn is_empty(&self) -> bool {
+ self.clusters.is_empty()
+ }
+}
+
+bitflags! {
+ #[cfg_attr(feature = "capture", derive(Serialize))]
+ /// Flags describing properties for a given PicturePrimitive
+ pub struct PictureFlags : u8 {
+ /// This picture is a resolve target (doesn't actually render content itself,
+ /// will have content copied in to it)
+ const IS_RESOLVE_TARGET = 1 << 0;
+ /// This picture establishes a sub-graph, which affects how SurfaceBuilder will
+ /// set up dependencies in the render task graph
+ const IS_SUB_GRAPH = 1 << 1;
+ /// If set, this picture should not apply snapping via changing the raster root
+ const DISABLE_SNAPPING = 1 << 2;
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct PicturePrimitive {
+ /// List of primitives, and associated info for this picture.
+ pub prim_list: PrimitiveList,
+
+ /// If false and transform ends up showing the back of the picture,
+ /// it will be considered invisible.
+ pub is_backface_visible: bool,
+
+ pub primary_render_task_id: Option<RenderTaskId>,
+ /// If a mix-blend-mode, contains the render task for
+ /// the readback of the framebuffer that we use to sample
+ /// from in the mix-blend-mode shader.
+ /// For drop-shadow filter, this will store the original
+ /// picture task which would be rendered on screen after
+ /// blur pass.
+ pub secondary_render_task_id: Option<RenderTaskId>,
+ /// How this picture should be composited.
+ /// If None, don't composite - just draw directly on parent surface.
+ pub composite_mode: Option<PictureCompositeMode>,
+
+ pub raster_config: Option<RasterConfig>,
+ pub context_3d: Picture3DContext<OrderedPictureChild>,
+
+ // Optional cache handles for storing extra data
+ // in the GPU cache, depending on the type of
+ // picture.
+ pub extra_gpu_data_handles: SmallVec<[GpuCacheHandle; 1]>,
+
+ /// The spatial node index of this picture when it is
+ /// composited into the parent picture.
+ pub spatial_node_index: SpatialNodeIndex,
+
+ /// Store the state of the previous local rect
+ /// for this picture. We need this in order to know when
+ /// to invalidate segments / drop-shadow gpu cache handles.
+ pub prev_local_rect: LayoutRect,
+
+ /// If false, this picture needs to (re)build segments
+ /// if it supports segment rendering. This can occur
+ /// if the local rect of the picture changes due to
+ /// transform animation and/or scrolling.
+ pub segments_are_valid: bool,
+
+ /// Set to true if we know for sure the picture is fully opaque.
+ pub is_opaque: bool,
+
+ /// Requested raster space for this picture
+ pub raster_space: RasterSpace,
+
+ /// Flags for this picture primitive
+ pub flags: PictureFlags,
+}
+
+impl PicturePrimitive {
+ pub fn print<T: PrintTreePrinter>(
+ &self,
+ pictures: &[Self],
+ self_index: PictureIndex,
+ pt: &mut T,
+ ) {
+ pt.new_level(format!("{:?}", self_index));
+ pt.add_item(format!("cluster_count: {:?}", self.prim_list.clusters.len()));
+ pt.add_item(format!("spatial_node_index: {:?}", self.spatial_node_index));
+ pt.add_item(format!("raster_config: {:?}", self.raster_config));
+ pt.add_item(format!("composite_mode: {:?}", self.composite_mode));
+ pt.add_item(format!("flags: {:?}", self.flags));
+
+ for child_pic_index in &self.prim_list.child_pictures {
+ pictures[child_pic_index.0].print(pictures, *child_pic_index, pt);
+ }
+
+ pt.end_level();
+ }
+
+ /// Returns true if this picture supports segmented rendering.
+ pub fn can_use_segments(&self) -> bool {
+ match self.raster_config {
+ // TODO(gw): Support brush segment rendering for filter and mix-blend
+ // shaders. It's possible this already works, but I'm just
+ // applying this optimization to Blit mode for now.
+ Some(RasterConfig { composite_mode: PictureCompositeMode::MixBlend(..), .. }) |
+ Some(RasterConfig { composite_mode: PictureCompositeMode::Filter(..), .. }) |
+ Some(RasterConfig { composite_mode: PictureCompositeMode::ComponentTransferFilter(..), .. }) |
+ Some(RasterConfig { composite_mode: PictureCompositeMode::TileCache { .. }, .. }) |
+ Some(RasterConfig { composite_mode: PictureCompositeMode::SvgFilter(..), .. }) |
+ Some(RasterConfig { composite_mode: PictureCompositeMode::IntermediateSurface, .. }) |
+ None => {
+ false
+ }
+ Some(RasterConfig { composite_mode: PictureCompositeMode::Blit(reason), ..}) => {
+ reason == BlitReason::CLIP
+ }
+ }
+ }
+
+ fn resolve_scene_properties(&mut self, properties: &SceneProperties) {
+ match self.composite_mode {
+ Some(PictureCompositeMode::Filter(ref mut filter)) => {
+ match *filter {
+ Filter::Opacity(ref binding, ref mut value) => {
+ *value = properties.resolve_float(binding);
+ }
+ _ => {}
+ }
+ }
+ _ => {}
+ }
+ }
+
+ pub fn is_visible(
+ &self,
+ spatial_tree: &SpatialTree,
+ ) -> bool {
+ if let Some(PictureCompositeMode::Filter(ref filter)) = self.composite_mode {
+ if !filter.is_visible() {
+ return false;
+ }
+ }
+
+ // For out-of-preserve-3d pictures, the backface visibility is determined by
+ // the local transform only.
+ // Note: we aren't taking the transform relative to the parent picture,
+ // since picture tree can be more dense than the corresponding spatial tree.
+ if !self.is_backface_visible {
+ if let Picture3DContext::Out = self.context_3d {
+ match spatial_tree.get_local_visible_face(self.spatial_node_index) {
+ VisibleFace::Front => {}
+ VisibleFace::Back => return false,
+ }
+ }
+ }
+
+ true
+ }
+
+ pub fn new_image(
+ composite_mode: Option<PictureCompositeMode>,
+ context_3d: Picture3DContext<OrderedPictureChild>,
+ prim_flags: PrimitiveFlags,
+ prim_list: PrimitiveList,
+ spatial_node_index: SpatialNodeIndex,
+ raster_space: RasterSpace,
+ flags: PictureFlags,
+ ) -> Self {
+ PicturePrimitive {
+ prim_list,
+ primary_render_task_id: None,
+ secondary_render_task_id: None,
+ composite_mode,
+ raster_config: None,
+ context_3d,
+ extra_gpu_data_handles: SmallVec::new(),
+ is_backface_visible: prim_flags.contains(PrimitiveFlags::IS_BACKFACE_VISIBLE),
+ spatial_node_index,
+ prev_local_rect: LayoutRect::zero(),
+ segments_are_valid: false,
+ is_opaque: false,
+ raster_space,
+ flags,
+ }
+ }
+
+ pub fn take_context(
+ &mut self,
+ pic_index: PictureIndex,
+ parent_surface_index: Option<SurfaceIndex>,
+ parent_subpixel_mode: SubpixelMode,
+ frame_state: &mut FrameBuildingState,
+ frame_context: &FrameBuildingContext,
+ scratch: &mut PrimitiveScratchBuffer,
+ tile_caches: &mut FastHashMap<SliceId, Box<TileCacheInstance>>,
+ ) -> Option<(PictureContext, PictureState, PrimitiveList)> {
+ self.primary_render_task_id = None;
+ self.secondary_render_task_id = None;
+
+ if !self.is_visible(frame_context.spatial_tree) {
+ return None;
+ }
+
+ profile_scope!("take_context");
+
+ let surface_index = match self.raster_config {
+ Some(ref raster_config) => raster_config.surface_index,
+ None => parent_surface_index.expect("bug: no parent"),
+ };
+ let surface_spatial_node_index = frame_state.surfaces[surface_index.0].surface_spatial_node_index;
+
+ let map_pic_to_world = SpaceMapper::new_with_target(
+ frame_context.root_spatial_node_index,
+ surface_spatial_node_index,
+ frame_context.global_screen_world_rect,
+ frame_context.spatial_tree,
+ );
+
+ let pic_bounds = map_pic_to_world
+ .unmap(&map_pic_to_world.bounds)
+ .unwrap_or_else(PictureRect::max_rect);
+
+ let map_local_to_pic = SpaceMapper::new(
+ surface_spatial_node_index,
+ pic_bounds,
+ );
+
+ match self.raster_config {
+ Some(RasterConfig { surface_index, composite_mode: PictureCompositeMode::TileCache { slice_id }, .. }) => {
+ let tile_cache = tile_caches.get_mut(&slice_id).unwrap();
+ let mut debug_info = SliceDebugInfo::new();
+ let mut surface_render_tasks = FastHashMap::default();
+ let mut surface_local_dirty_rect = PictureRect::zero();
+ let device_pixel_scale = frame_state
+ .surfaces[surface_index.0]
+ .device_pixel_scale;
+ let mut at_least_one_tile_visible = false;
+
+ // Get the overall world space rect of the picture cache. Used to clip
+ // the tile rects below for occlusion testing to the relevant area.
+ let world_clip_rect = map_pic_to_world
+ .map(&tile_cache.local_clip_rect)
+ .expect("bug: unable to map clip rect")
+ .round();
+ let device_clip_rect = (world_clip_rect * frame_context.global_device_pixel_scale).round();
+
+ for (sub_slice_index, sub_slice) in tile_cache.sub_slices.iter_mut().enumerate() {
+ for tile in sub_slice.tiles.values_mut() {
+ if tile.is_visible {
+ // Get the world space rect that this tile will actually occupy on screen
+ let world_draw_rect = world_clip_rect.intersection(&tile.world_valid_rect);
+
+ // If that draw rect is occluded by some set of tiles in front of it,
+ // then mark it as not visible and skip drawing. When it's not occluded
+ // it will fail this test, and get rasterized by the render task setup
+ // code below.
+ match world_draw_rect {
+ Some(world_draw_rect) => {
+ // Only check for occlusion on visible tiles that are fixed position.
+ if tile_cache.spatial_node_index == frame_context.root_spatial_node_index &&
+ frame_state.composite_state.occluders.is_tile_occluded(tile.z_id, world_draw_rect) {
+ // If this tile has an allocated native surface, free it, since it's completely
+ // occluded. We will need to re-allocate this surface if it becomes visible,
+ // but that's likely to be rare (e.g. when there is no content display list
+ // for a frame or two during a tab switch).
+ let surface = tile.surface.as_mut().expect("no tile surface set!");
+
+ if let TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { id, .. }, .. } = surface {
+ if let Some(id) = id.take() {
+ frame_state.resource_cache.destroy_compositor_tile(id);
+ }
+ }
+
+ tile.is_visible = false;
+
+ if frame_context.fb_config.testing {
+ debug_info.tiles.insert(
+ tile.tile_offset,
+ TileDebugInfo::Occluded,
+ );
+ }
+
+ continue;
+ }
+ }
+ None => {
+ tile.is_visible = false;
+ }
+ }
+ }
+
+ // If we get here, we want to ensure that the surface remains valid in the texture
+ // cache, _even if_ it's not visible due to clipping or being scrolled off-screen.
+ // This ensures that we retain valid tiles that are off-screen, but still in the
+ // display port of this tile cache instance.
+ if let Some(TileSurface::Texture { descriptor, .. }) = tile.surface.as_ref() {
+ if let SurfaceTextureDescriptor::TextureCache { handle: Some(handle), .. } = descriptor {
+ frame_state.resource_cache
+ .picture_textures.request(handle, frame_state.gpu_cache);
+ }
+ }
+
+ // If the tile has been found to be off-screen / clipped, skip any further processing.
+ if !tile.is_visible {
+ if frame_context.fb_config.testing {
+ debug_info.tiles.insert(
+ tile.tile_offset,
+ TileDebugInfo::Culled,
+ );
+ }
+
+ continue;
+ }
+
+ at_least_one_tile_visible = true;
+
+ if frame_context.debug_flags.contains(DebugFlags::PICTURE_CACHING_DBG) {
+ tile.root.draw_debug_rects(
+ &map_pic_to_world,
+ tile.is_opaque,
+ tile.current_descriptor.local_valid_rect,
+ scratch,
+ frame_context.global_device_pixel_scale,
+ );
+
+ let label_offset = DeviceVector2D::new(
+ 20.0 + sub_slice_index as f32 * 20.0,
+ 30.0 + sub_slice_index as f32 * 20.0,
+ );
+ let tile_device_rect = tile.world_tile_rect * frame_context.global_device_pixel_scale;
+ if tile_device_rect.height() >= label_offset.y {
+ let surface = tile.surface.as_ref().expect("no tile surface set!");
+
+ scratch.push_debug_string(
+ tile_device_rect.min + label_offset,
+ debug_colors::RED,
+ format!("{:?}: s={} is_opaque={} surface={} sub={}",
+ tile.id,
+ tile_cache.slice,
+ tile.is_opaque,
+ surface.kind(),
+ sub_slice_index,
+ ),
+ );
+ }
+ }
+
+ if let TileSurface::Texture { descriptor, .. } = tile.surface.as_mut().unwrap() {
+ match descriptor {
+ SurfaceTextureDescriptor::TextureCache { ref handle, .. } => {
+ let exists = handle.as_ref().map_or(false,
+ |handle| frame_state.resource_cache.picture_textures.entry_exists(handle)
+ );
+ // Invalidate if the backing texture was evicted.
+ if exists {
+ // Request the backing texture so it won't get evicted this frame.
+ // We specifically want to mark the tile texture as used, even
+ // if it's detected not visible below and skipped. This is because
+ // we maintain the set of tiles we care about based on visibility
+ // during pre_update. If a tile still exists after that, we are
+ // assuming that it's either visible or we want to retain it for
+ // a while in case it gets scrolled back onto screen soon.
+ // TODO(gw): Consider switching to manual eviction policy?
+ frame_state.resource_cache
+ .picture_textures
+ .request(handle.as_ref().unwrap(), frame_state.gpu_cache);
+ } else {
+ // If the texture was evicted on a previous frame, we need to assume
+ // that the entire tile rect is dirty.
+ tile.invalidate(None, InvalidationReason::NoTexture);
+ }
+ }
+ SurfaceTextureDescriptor::Native { id, .. } => {
+ if id.is_none() {
+ // There is no current surface allocation, so ensure the entire tile is invalidated
+ tile.invalidate(None, InvalidationReason::NoSurface);
+ }
+ }
+ }
+ }
+
+ // Ensure that the dirty rect doesn't extend outside the local valid rect.
+ tile.local_dirty_rect = tile.local_dirty_rect
+ .intersection(&tile.current_descriptor.local_valid_rect)
+ .unwrap_or_else(PictureRect::zero);
+
+ surface_local_dirty_rect = surface_local_dirty_rect.union(&tile.local_dirty_rect);
+
+ // Update the world/device dirty rect
+ let world_dirty_rect = map_pic_to_world.map(&tile.local_dirty_rect).expect("bug");
+
+ let device_rect = (tile.world_tile_rect * frame_context.global_device_pixel_scale).round();
+ tile.device_dirty_rect = (world_dirty_rect * frame_context.global_device_pixel_scale)
+ .round_out()
+ .intersection(&device_rect)
+ .unwrap_or_else(DeviceRect::zero);
+
+ if tile.is_valid {
+ if frame_context.fb_config.testing {
+ debug_info.tiles.insert(
+ tile.tile_offset,
+ TileDebugInfo::Valid,
+ );
+ }
+ } else {
+ // Add this dirty rect to the dirty region tracker. This must be done outside the if statement below,
+ // so that we include in the dirty region tiles that are handled by a background color only (no
+ // surface allocation).
+ tile_cache.dirty_region.add_dirty_region(
+ tile.local_dirty_rect,
+ frame_context.spatial_tree,
+ );
+
+ // Ensure that this texture is allocated.
+ if let TileSurface::Texture { ref mut descriptor } = tile.surface.as_mut().unwrap() {
+ match descriptor {
+ SurfaceTextureDescriptor::TextureCache { ref mut handle } => {
+
+ frame_state.resource_cache.picture_textures.update(
+ tile_cache.current_tile_size,
+ handle,
+ frame_state.gpu_cache,
+ &mut frame_state.resource_cache.texture_cache.next_id,
+ &mut frame_state.resource_cache.texture_cache.pending_updates,
+ );
+ }
+ SurfaceTextureDescriptor::Native { id } => {
+ if id.is_none() {
+ // Allocate a native surface id if we're in native compositing mode,
+ // and we don't have a surface yet (due to first frame, or destruction
+ // due to tile size changing etc).
+ if sub_slice.native_surface.is_none() {
+ let opaque = frame_state
+ .resource_cache
+ .create_compositor_surface(
+ tile_cache.virtual_offset,
+ tile_cache.current_tile_size,
+ true,
+ );
+
+ let alpha = frame_state
+ .resource_cache
+ .create_compositor_surface(
+ tile_cache.virtual_offset,
+ tile_cache.current_tile_size,
+ false,
+ );
+
+ sub_slice.native_surface = Some(NativeSurface {
+ opaque,
+ alpha,
+ });
+ }
+
+ // Create the tile identifier and allocate it.
+ let surface_id = if tile.is_opaque {
+ sub_slice.native_surface.as_ref().unwrap().opaque
+ } else {
+ sub_slice.native_surface.as_ref().unwrap().alpha
+ };
+
+ let tile_id = NativeTileId {
+ surface_id,
+ x: tile.tile_offset.x,
+ y: tile.tile_offset.y,
+ };
+
+ frame_state.resource_cache.create_compositor_tile(tile_id);
+
+ *id = Some(tile_id);
+ }
+ }
+ }
+
+ // The cast_unit() here is because the `content_origin` is expected to be in
+ // device pixels, however we're establishing raster roots for picture cache
+ // tiles meaning the `content_origin` needs to be in the local space of that root.
+ // TODO(gw): `content_origin` should actually be in RasterPixels to be consistent
+ // with both local / screen raster modes, but this involves a lot of
+ // changes to render task and picture code.
+ let content_origin_f = tile.local_tile_rect.min.cast_unit() * device_pixel_scale;
+ let content_origin = content_origin_f.round();
+ // TODO: these asserts used to have a threshold of 0.01 but failed intermittently the
+ // gfx/layers/apz/test/mochitest/test_group_double_tap_zoom-2.html test on android.
+ // moving the rectangles in space mapping conversion code to the Box2D representaton
+ // made the failure happen more often.
+ debug_assert!((content_origin_f.x - content_origin.x).abs() < 0.15);
+ debug_assert!((content_origin_f.y - content_origin.y).abs() < 0.15);
+
+ let surface = descriptor.resolve(
+ frame_state.resource_cache,
+ tile_cache.current_tile_size,
+ );
+
+ let scissor_rect = frame_state.composite_state.get_surface_rect(
+ &tile.local_dirty_rect,
+ &tile.local_tile_rect,
+ tile_cache.transform_index,
+ ).to_i32();
+
+ let valid_rect = frame_state.composite_state.get_surface_rect(
+ &tile.current_descriptor.local_valid_rect,
+ &tile.local_tile_rect,
+ tile_cache.transform_index,
+ ).to_i32();
+
+ let composite_task_size = tile_cache.current_tile_size;
+
+ let tile_key = TileKey {
+ sub_slice_index: SubSliceIndex::new(sub_slice_index),
+ tile_offset: tile.tile_offset,
+ };
+
+ let mut clear_color = ColorF::TRANSPARENT;
+
+ if SubSliceIndex::new(sub_slice_index).is_primary() {
+ if let Some(background_color) = tile_cache.background_color {
+ clear_color = background_color;
+ }
+
+ // If this picture cache has a spanning_opaque_color, we will use
+ // that as the clear color. The primitive that was detected as a
+ // spanning primitive will have been set with IS_BACKDROP, causing
+ // it to be skipped and removing everything added prior to it
+ // during batching.
+ if let Some(color) = tile_cache.backdrop.spanning_opaque_color {
+ clear_color = color;
+ }
+ }
+
+ let cmd_buffer_index = frame_state.cmd_buffers.create_cmd_buffer();
+
+ // TODO(gw): As a performance optimization, we could skip the resolve picture
+ // if the dirty rect is the same as the resolve rect (probably quite
+ // common for effects that scroll underneath a backdrop-filter, for example).
+ let use_tile_composite = !tile.sub_graphs.is_empty();
+
+ if use_tile_composite {
+ let mut local_content_rect = tile.local_dirty_rect;
+
+ for (sub_graph_rect, surface_stack) in &tile.sub_graphs {
+ if let Some(dirty_sub_graph_rect) = sub_graph_rect.intersection(&tile.local_dirty_rect) {
+ for (composite_mode, surface_index) in surface_stack {
+ let surface = &frame_state.surfaces[surface_index.0];
+
+ let rect = composite_mode.get_coverage(
+ surface,
+ Some(dirty_sub_graph_rect.cast_unit()),
+ ).cast_unit();
+
+ local_content_rect = local_content_rect.union(&rect);
+ }
+ }
+ }
+
+ // We know that we'll never need to sample > 300 device pixels outside the tile
+ // for blurring, so clamp the content rect here so that we don't try to allocate
+ // a really large surface in the case of a drop-shadow with large offset.
+ let max_content_rect = (tile.local_dirty_rect.cast_unit() * device_pixel_scale)
+ .inflate(
+ MAX_BLUR_RADIUS * BLUR_SAMPLE_SCALE,
+ MAX_BLUR_RADIUS * BLUR_SAMPLE_SCALE,
+ )
+ .round_out()
+ .to_i32();
+
+ let content_device_rect = (local_content_rect.cast_unit() * device_pixel_scale)
+ .round_out()
+ .to_i32();
+
+ let content_device_rect = content_device_rect
+ .intersection(&max_content_rect)
+ .expect("bug: no intersection with tile dirty rect");
+
+ let content_task_size = content_device_rect.size();
+ let normalized_content_rect = content_task_size.into();
+
+ let inner_offset = content_origin + scissor_rect.min.to_vector().to_f32();
+ let outer_offset = content_device_rect.min.to_f32();
+ let sub_rect_offset = (inner_offset - outer_offset).round().to_i32();
+
+ let render_task_id = frame_state.rg_builder.add().init(
+ RenderTask::new_dynamic(
+ content_task_size,
+ RenderTaskKind::new_picture(
+ content_task_size,
+ true,
+ content_device_rect.min.to_f32(),
+ surface_spatial_node_index,
+ // raster == surface implicitly for picture cache tiles
+ surface_spatial_node_index,
+ device_pixel_scale,
+ Some(normalized_content_rect),
+ None,
+ Some(clear_color),
+ cmd_buffer_index,
+ false,
+ )
+ ),
+ );
+
+ let composite_task_id = frame_state.rg_builder.add().init(
+ RenderTask::new(
+ RenderTaskLocation::Static {
+ surface: StaticRenderTaskSurface::PictureCache {
+ surface,
+ },
+ rect: composite_task_size.into(),
+ },
+ RenderTaskKind::new_tile_composite(
+ sub_rect_offset,
+ scissor_rect,
+ valid_rect,
+ clear_color,
+ ),
+ ),
+ );
+
+ surface_render_tasks.insert(
+ tile_key,
+ SurfaceTileDescriptor {
+ current_task_id: render_task_id,
+ composite_task_id: Some(composite_task_id),
+ dirty_rect: tile.local_dirty_rect,
+ },
+ );
+ } else {
+ let render_task_id = frame_state.rg_builder.add().init(
+ RenderTask::new(
+ RenderTaskLocation::Static {
+ surface: StaticRenderTaskSurface::PictureCache {
+ surface,
+ },
+ rect: composite_task_size.into(),
+ },
+ RenderTaskKind::new_picture(
+ composite_task_size,
+ true,
+ content_origin,
+ surface_spatial_node_index,
+ // raster == surface implicitly for picture cache tiles
+ surface_spatial_node_index,
+ device_pixel_scale,
+ Some(scissor_rect),
+ Some(valid_rect),
+ Some(clear_color),
+ cmd_buffer_index,
+ false,
+ )
+ ),
+ );
+
+ surface_render_tasks.insert(
+ tile_key,
+ SurfaceTileDescriptor {
+ current_task_id: render_task_id,
+ composite_task_id: None,
+ dirty_rect: tile.local_dirty_rect,
+ },
+ );
+ }
+ }
+
+ if frame_context.fb_config.testing {
+ debug_info.tiles.insert(
+ tile.tile_offset,
+ TileDebugInfo::Dirty(DirtyTileDebugInfo {
+ local_valid_rect: tile.current_descriptor.local_valid_rect,
+ local_dirty_rect: tile.local_dirty_rect,
+ }),
+ );
+ }
+ }
+
+ let surface = tile.surface.as_ref().expect("no tile surface set!");
+
+ let descriptor = CompositeTileDescriptor {
+ surface_kind: surface.into(),
+ tile_id: tile.id,
+ };
+
+ let (surface, is_opaque) = match surface {
+ TileSurface::Color { color } => {
+ (CompositeTileSurface::Color { color: *color }, true)
+ }
+ TileSurface::Clear => {
+ // Clear tiles are rendered with blend mode pre-multiply-dest-out.
+ (CompositeTileSurface::Clear, false)
+ }
+ TileSurface::Texture { descriptor, .. } => {
+ let surface = descriptor.resolve(frame_state.resource_cache, tile_cache.current_tile_size);
+ (
+ CompositeTileSurface::Texture { surface },
+ tile.is_opaque
+ )
+ }
+ };
+
+ if is_opaque {
+ sub_slice.opaque_tile_descriptors.push(descriptor);
+ } else {
+ sub_slice.alpha_tile_descriptors.push(descriptor);
+ }
+
+ let composite_tile = CompositeTile {
+ kind: tile_kind(&surface, is_opaque),
+ surface,
+ local_rect: tile.local_tile_rect,
+ local_valid_rect: tile.current_descriptor.local_valid_rect,
+ local_dirty_rect: tile.local_dirty_rect,
+ device_clip_rect,
+ z_id: tile.z_id,
+ transform_index: tile_cache.transform_index,
+ };
+
+ sub_slice.composite_tiles.push(composite_tile);
+
+ // Now that the tile is valid, reset the dirty rect.
+ tile.local_dirty_rect = PictureRect::zero();
+ tile.is_valid = true;
+ }
+
+ // Sort the tile descriptor lists, since iterating values in the tile_cache.tiles
+ // hashmap doesn't provide any ordering guarantees, but we want to detect the
+ // composite descriptor as equal if the tiles list is the same, regardless of
+ // ordering.
+ sub_slice.opaque_tile_descriptors.sort_by_key(|desc| desc.tile_id);
+ sub_slice.alpha_tile_descriptors.sort_by_key(|desc| desc.tile_id);
+ }
+
+ // Check to see if we should add backdrops as native surfaces.
+ let backdrop_rect = tile_cache.backdrop.backdrop_rect
+ .intersection(&tile_cache.local_rect)
+ .and_then(|r| {
+ r.intersection(&tile_cache.local_clip_rect)
+ });
+
+ let mut backdrop_in_use_and_visible = false;
+ if let Some(backdrop_rect) = backdrop_rect {
+ let supports_surface_for_backdrop = match frame_state.composite_state.compositor_kind {
+ CompositorKind::Draw { .. } => {
+ false
+ }
+ CompositorKind::Native { capabilities, .. } => {
+ capabilities.supports_surface_for_backdrop
+ }
+ };
+ if supports_surface_for_backdrop && !tile_cache.found_prims_after_backdrop && at_least_one_tile_visible {
+ if let Some(BackdropKind::Color { color }) = tile_cache.backdrop.kind {
+ backdrop_in_use_and_visible = true;
+
+ // We're going to let the compositor handle the backdrop as a native surface.
+ // Hide all of our sub_slice tiles so they aren't also trying to draw it.
+ for sub_slice in &mut tile_cache.sub_slices {
+ for tile in sub_slice.tiles.values_mut() {
+ tile.is_visible = false;
+ }
+ }
+
+ // Destroy our backdrop surface if it doesn't match the new color.
+ // TODO: This is a performance hit for animated color backdrops.
+ if let Some(backdrop_surface) = &tile_cache.backdrop_surface {
+ if backdrop_surface.color != color {
+ frame_state.resource_cache.destroy_compositor_surface(backdrop_surface.id);
+ tile_cache.backdrop_surface = None;
+ }
+ }
+
+ // Calculate the device_rect for the backdrop, which is just the backdrop_rect
+ // converted into world space and scaled to device pixels.
+ let world_backdrop_rect = map_pic_to_world.map(&backdrop_rect).expect("bug: unable to map backdrop rect");
+ let device_rect = (world_backdrop_rect * frame_context.global_device_pixel_scale).round();
+
+ // If we already have a backdrop surface, update the device rect. Otherwise, create
+ // a backdrop surface.
+ if let Some(backdrop_surface) = &mut tile_cache.backdrop_surface {
+ backdrop_surface.device_rect = device_rect;
+ } else {
+ // Create native compositor surface with color for the backdrop and store the id.
+ tile_cache.backdrop_surface = Some(BackdropSurface {
+ id: frame_state.resource_cache.create_compositor_backdrop_surface(color),
+ color,
+ device_rect,
+ });
+ }
+ }
+ }
+ }
+
+ if !backdrop_in_use_and_visible {
+ if let Some(backdrop_surface) = &tile_cache.backdrop_surface {
+ // We've already allocated a backdrop surface, but we're not using it.
+ // Tell the compositor to get rid of it.
+ frame_state.resource_cache.destroy_compositor_surface(backdrop_surface.id);
+ tile_cache.backdrop_surface = None;
+ }
+ }
+
+ // If invalidation debugging is enabled, dump the picture cache state to a tree printer.
+ if frame_context.debug_flags.contains(DebugFlags::INVALIDATION_DBG) {
+ tile_cache.print();
+ }
+
+ // If testing mode is enabled, write some information about the current state
+ // of this picture cache (made available in RenderResults).
+ if frame_context.fb_config.testing {
+ frame_state.composite_state
+ .picture_cache_debug
+ .slices
+ .insert(
+ tile_cache.slice,
+ debug_info,
+ );
+ }
+
+ let descriptor = SurfaceDescriptor::new_tiled(surface_render_tasks);
+
+ frame_state.surface_builder.push_surface(
+ surface_index,
+ false,
+ surface_local_dirty_rect,
+ descriptor,
+ frame_state.surfaces,
+ frame_state.rg_builder,
+ );
+ }
+ Some(ref mut raster_config) => {
+ let pic_rect = frame_state
+ .surfaces[raster_config.surface_index.0]
+ .clipped_local_rect;
+
+ let parent_surface_index = parent_surface_index.expect("bug: no parent for child surface");
+
+ // Layout space for the picture is picture space from the
+ // perspective of its child primitives.
+ let local_rect = pic_rect * Scale::new(1.0);
+
+ // If the precise rect changed since last frame, we need to invalidate
+ // any segments and gpu cache handles for drop-shadows.
+ // TODO(gw): Requiring storage of the `prev_precise_local_rect` here
+ // is a total hack. It's required because `prev_precise_local_rect`
+ // gets written to twice (during initial vis pass and also during
+ // prepare pass). The proper longer term fix for this is to make
+ // use of the conservative picture rect for segmenting (which should
+ // be done during scene building).
+ if local_rect != self.prev_local_rect {
+ match raster_config.composite_mode {
+ PictureCompositeMode::Filter(Filter::DropShadows(..)) => {
+ for handle in &self.extra_gpu_data_handles {
+ frame_state.gpu_cache.invalidate(handle);
+ }
+ }
+ _ => {}
+ }
+ // Invalidate any segments built for this picture, since the local
+ // rect has changed.
+ self.segments_are_valid = false;
+ self.prev_local_rect = local_rect;
+ }
+
+ let max_surface_size = frame_context
+ .fb_config
+ .max_surface_override
+ .unwrap_or(MAX_SURFACE_SIZE) as f32;
+
+ let surface_rects = match get_surface_rects(
+ raster_config.surface_index,
+ &raster_config.composite_mode,
+ parent_surface_index,
+ &mut frame_state.surfaces,
+ frame_context.spatial_tree,
+ max_surface_size,
+ ) {
+ Some(rects) => rects,
+ None => return None,
+ };
+
+ let (raster_spatial_node_index, device_pixel_scale) = {
+ let surface = &frame_state.surfaces[surface_index.0];
+ (surface.raster_spatial_node_index, surface.device_pixel_scale)
+ };
+ let can_use_shared_surface = !self.flags.contains(PictureFlags::IS_RESOLVE_TARGET);
+
+ let primary_render_task_id;
+ let surface_descriptor;
+ match raster_config.composite_mode {
+ PictureCompositeMode::TileCache { .. } => {
+ unreachable!("handled above");
+ }
+ PictureCompositeMode::Filter(Filter::Blur { width, height, .. }) => {
+ let surface = &frame_state.surfaces[raster_config.surface_index.0];
+ let (width, height) = surface.clamp_blur_radius(width, height);
+
+ let width_std_deviation = width * surface.local_scale.0 * device_pixel_scale.0;
+ let height_std_deviation = height * surface.local_scale.1 * device_pixel_scale.0;
+ let blur_std_deviation = DeviceSize::new(
+ width_std_deviation,
+ height_std_deviation,
+ );
+
+ let original_size = surface_rects.clipped.size();
+
+ // Adjust the size to avoid introducing sampling errors during the down-scaling passes.
+ // what would be even better is to rasterize the picture at the down-scaled size
+ // directly.
+ let adjusted_size = BlurTask::adjusted_blur_source_size(
+ original_size,
+ blur_std_deviation,
+ );
+
+ let cmd_buffer_index = frame_state.cmd_buffers.create_cmd_buffer();
+
+ // Since we (may have) adjusted the render task size for downscaling accuracy
+ // above, recalculate the uv rect for tasks that may sample from this blur output
+ let uv_rect_kind = calculate_uv_rect_kind(
+ DeviceRect::from_origin_and_size(surface_rects.clipped.min, adjusted_size.to_f32()),
+ surface_rects.unclipped,
+ );
+
+ let picture_task_id = frame_state.rg_builder.add().init(
+ RenderTask::new_dynamic(
+ adjusted_size,
+ RenderTaskKind::new_picture(
+ adjusted_size,
+ surface_rects.needs_scissor_rect,
+ surface_rects.clipped.min,
+ surface_spatial_node_index,
+ raster_spatial_node_index,
+ device_pixel_scale,
+ None,
+ None,
+ None,
+ cmd_buffer_index,
+ can_use_shared_surface,
+ )
+ ).with_uv_rect_kind(uv_rect_kind)
+ );
+
+ let blur_render_task_id = RenderTask::new_blur(
+ blur_std_deviation,
+ picture_task_id,
+ frame_state.rg_builder,
+ RenderTargetKind::Color,
+ None,
+ original_size.to_i32(),
+ );
+
+ primary_render_task_id = blur_render_task_id;
+
+ surface_descriptor = SurfaceDescriptor::new_chained(
+ picture_task_id,
+ blur_render_task_id,
+ surface_rects.clipped_local,
+ );
+ }
+ PictureCompositeMode::Filter(Filter::DropShadows(ref shadows)) => {
+ let surface = &frame_state.surfaces[raster_config.surface_index.0];
+
+ let device_rect = surface_rects.clipped;
+
+ let cmd_buffer_index = frame_state.cmd_buffers.create_cmd_buffer();
+
+ let picture_task_id = frame_state.rg_builder.add().init(
+ RenderTask::new_dynamic(
+ surface_rects.task_size,
+ RenderTaskKind::new_picture(
+ surface_rects.task_size,
+ surface_rects.needs_scissor_rect,
+ device_rect.min,
+ surface_spatial_node_index,
+ raster_spatial_node_index,
+ device_pixel_scale,
+ None,
+ None,
+ None,
+ cmd_buffer_index,
+ can_use_shared_surface,
+ ),
+ ).with_uv_rect_kind(surface_rects.uv_rect_kind)
+ );
+
+ let mut blur_tasks = BlurTaskCache::default();
+
+ self.extra_gpu_data_handles.resize(shadows.len(), GpuCacheHandle::new());
+
+ let mut blur_render_task_id = picture_task_id;
+ for shadow in shadows {
+ let (blur_radius_x, blur_radius_y) = surface.clamp_blur_radius(
+ shadow.blur_radius,
+ shadow.blur_radius,
+ );
+
+ blur_render_task_id = RenderTask::new_blur(
+ DeviceSize::new(
+ blur_radius_x * surface.local_scale.0 * device_pixel_scale.0,
+ blur_radius_y * surface.local_scale.1 * device_pixel_scale.0,
+ ),
+ picture_task_id,
+ frame_state.rg_builder,
+ RenderTargetKind::Color,
+ Some(&mut blur_tasks),
+ device_rect.size().to_i32(),
+ );
+ }
+
+ // Add this content picture as a dependency of the parent surface, to
+ // ensure it isn't free'd after the shadow uses it as an input.
+ frame_state.surface_builder.add_picture_render_task(picture_task_id);
+
+ primary_render_task_id = blur_render_task_id;
+ self.secondary_render_task_id = Some(picture_task_id);
+
+ surface_descriptor = SurfaceDescriptor::new_chained(
+ picture_task_id,
+ blur_render_task_id,
+ surface_rects.clipped_local,
+ );
+ }
+ PictureCompositeMode::MixBlend(mode) if BlendMode::from_mix_blend_mode(
+ mode,
+ frame_context.fb_config.gpu_supports_advanced_blend,
+ frame_context.fb_config.advanced_blend_is_coherent,
+ frame_context.fb_config.dual_source_blending_is_supported,
+ ).is_none() => {
+ let parent_surface = &frame_state.surfaces[parent_surface_index.0];
+
+ // Create a space mapper that will allow mapping from the local rect
+ // of the mix-blend primitive into the space of the surface that we
+ // need to read back from. Note that we use the parent's raster spatial
+ // node here, so that we are in the correct device space of the parent
+ // surface, whether it establishes a raster root or not.
+ let map_pic_to_parent = SpaceMapper::new_with_target(
+ parent_surface.surface_spatial_node_index,
+ surface_spatial_node_index,
+ parent_surface.clipping_rect,
+ frame_context.spatial_tree,
+ );
+ let pic_in_raster_space = map_pic_to_parent
+ .map(&pic_rect)
+ .expect("bug: unable to map mix-blend content into parent");
+
+ // Apply device pixel ratio for parent surface to get into device
+ // pixels for that surface.
+ let backdrop_rect = pic_in_raster_space;
+ let parent_surface_rect = parent_surface.clipping_rect;
+
+ // If there is no available parent surface to read back from (for example, if
+ // the parent surface is affected by a clip that doesn't affect the child
+ // surface), then create a dummy 16x16 readback. In future, we could alter
+ // the composite mode of this primitive to skip the mix-blend, but for simplicity
+ // we just create a dummy readback for now.
+
+ let readback_task_id = match backdrop_rect.intersection(&parent_surface_rect) {
+ Some(available_rect) => {
+ // Calculate the UV coords necessary for the shader to sampler
+ // from the primitive rect within the readback region. This is
+ // 0..1 for aligned surfaces, but doing it this way allows
+ // accurate sampling if the primitive bounds have fractional values.
+
+ let backdrop_rect = parent_surface.map_to_device_rect(
+ &backdrop_rect,
+ frame_context.spatial_tree,
+ );
+
+ let available_rect = parent_surface.map_to_device_rect(
+ &available_rect,
+ frame_context.spatial_tree,
+ ).round_out();
+
+ let backdrop_uv = calculate_uv_rect_kind(
+ available_rect,
+ backdrop_rect,
+ );
+
+ frame_state.rg_builder.add().init(
+ RenderTask::new_dynamic(
+ available_rect.size().to_i32(),
+ RenderTaskKind::new_readback(Some(available_rect.min)),
+ ).with_uv_rect_kind(backdrop_uv)
+ )
+ }
+ None => {
+ frame_state.rg_builder.add().init(
+ RenderTask::new_dynamic(
+ DeviceIntSize::new(16, 16),
+ RenderTaskKind::new_readback(None),
+ )
+ )
+ }
+ };
+
+ frame_state.surface_builder.add_child_render_task(
+ readback_task_id,
+ frame_state.rg_builder,
+ );
+
+ self.secondary_render_task_id = Some(readback_task_id);
+
+ let task_size = surface_rects.clipped.size().to_i32();
+
+ let cmd_buffer_index = frame_state.cmd_buffers.create_cmd_buffer();
+
+ let render_task_id = frame_state.rg_builder.add().init(
+ RenderTask::new_dynamic(
+ task_size,
+ RenderTaskKind::new_picture(
+ task_size,
+ surface_rects.needs_scissor_rect,
+ surface_rects.clipped.min,
+ surface_spatial_node_index,
+ raster_spatial_node_index,
+ device_pixel_scale,
+ None,
+ None,
+ None,
+ cmd_buffer_index,
+ can_use_shared_surface,
+ )
+ ).with_uv_rect_kind(surface_rects.uv_rect_kind)
+ );
+
+ primary_render_task_id = render_task_id;
+
+ surface_descriptor = SurfaceDescriptor::new_simple(
+ render_task_id,
+ surface_rects.clipped_local,
+ );
+ }
+ PictureCompositeMode::Filter(..) => {
+ let cmd_buffer_index = frame_state.cmd_buffers.create_cmd_buffer();
+
+ let render_task_id = frame_state.rg_builder.add().init(
+ RenderTask::new_dynamic(
+ surface_rects.task_size,
+ RenderTaskKind::new_picture(
+ surface_rects.task_size,
+ surface_rects.needs_scissor_rect,
+ surface_rects.clipped.min,
+ surface_spatial_node_index,
+ raster_spatial_node_index,
+ device_pixel_scale,
+ None,
+ None,
+ None,
+ cmd_buffer_index,
+ can_use_shared_surface,
+ )
+ ).with_uv_rect_kind(surface_rects.uv_rect_kind)
+ );
+
+ primary_render_task_id = render_task_id;
+
+ surface_descriptor = SurfaceDescriptor::new_simple(
+ render_task_id,
+ surface_rects.clipped_local,
+ );
+ }
+ PictureCompositeMode::ComponentTransferFilter(..) => {
+ let cmd_buffer_index = frame_state.cmd_buffers.create_cmd_buffer();
+
+ let render_task_id = frame_state.rg_builder.add().init(
+ RenderTask::new_dynamic(
+ surface_rects.task_size,
+ RenderTaskKind::new_picture(
+ surface_rects.task_size,
+ surface_rects.needs_scissor_rect,
+ surface_rects.clipped.min,
+ surface_spatial_node_index,
+ raster_spatial_node_index,
+ device_pixel_scale,
+ None,
+ None,
+ None,
+ cmd_buffer_index,
+ can_use_shared_surface,
+ )
+ ).with_uv_rect_kind(surface_rects.uv_rect_kind)
+ );
+
+ primary_render_task_id = render_task_id;
+
+ surface_descriptor = SurfaceDescriptor::new_simple(
+ render_task_id,
+ surface_rects.clipped_local,
+ );
+ }
+ PictureCompositeMode::MixBlend(..) |
+ PictureCompositeMode::Blit(_) => {
+ let cmd_buffer_index = frame_state.cmd_buffers.create_cmd_buffer();
+
+ let render_task_id = frame_state.rg_builder.add().init(
+ RenderTask::new_dynamic(
+ surface_rects.task_size,
+ RenderTaskKind::new_picture(
+ surface_rects.task_size,
+ surface_rects.needs_scissor_rect,
+ surface_rects.clipped.min,
+ surface_spatial_node_index,
+ raster_spatial_node_index,
+ device_pixel_scale,
+ None,
+ None,
+ None,
+ cmd_buffer_index,
+ can_use_shared_surface,
+ )
+ ).with_uv_rect_kind(surface_rects.uv_rect_kind)
+ );
+
+ primary_render_task_id = render_task_id;
+
+ surface_descriptor = SurfaceDescriptor::new_simple(
+ render_task_id,
+ surface_rects.clipped_local,
+ );
+ }
+ PictureCompositeMode::IntermediateSurface => {
+ if !scratch.required_sub_graphs.contains(&pic_index) {
+ return None;
+ }
+
+ // TODO(gw): Remove all the mostly duplicated code in each of these
+ // match cases (they used to be quite different).
+ let cmd_buffer_index = frame_state.cmd_buffers.create_cmd_buffer();
+
+ let render_task_id = frame_state.rg_builder.add().init(
+ RenderTask::new_dynamic(
+ surface_rects.task_size,
+ RenderTaskKind::new_picture(
+ surface_rects.task_size,
+ surface_rects.needs_scissor_rect,
+ surface_rects.clipped.min,
+ surface_spatial_node_index,
+ raster_spatial_node_index,
+ device_pixel_scale,
+ None,
+ None,
+ None,
+ cmd_buffer_index,
+ can_use_shared_surface,
+ )
+ ).with_uv_rect_kind(surface_rects.uv_rect_kind)
+ );
+
+ primary_render_task_id = render_task_id;
+
+ surface_descriptor = SurfaceDescriptor::new_simple(
+ render_task_id,
+ surface_rects.clipped_local,
+ );
+ }
+ PictureCompositeMode::SvgFilter(ref primitives, ref filter_datas) => {
+ let cmd_buffer_index = frame_state.cmd_buffers.create_cmd_buffer();
+
+ let picture_task_id = frame_state.rg_builder.add().init(
+ RenderTask::new_dynamic(
+ surface_rects.task_size,
+ RenderTaskKind::new_picture(
+ surface_rects.task_size,
+ surface_rects.needs_scissor_rect,
+ surface_rects.clipped.min,
+ surface_spatial_node_index,
+ raster_spatial_node_index,
+ device_pixel_scale,
+ None,
+ None,
+ None,
+ cmd_buffer_index,
+ can_use_shared_surface,
+ )
+ ).with_uv_rect_kind(surface_rects.uv_rect_kind)
+ );
+
+ let filter_task_id = RenderTask::new_svg_filter(
+ primitives,
+ filter_datas,
+ frame_state.rg_builder,
+ surface_rects.clipped.size().to_i32(),
+ surface_rects.uv_rect_kind,
+ picture_task_id,
+ device_pixel_scale,
+ );
+
+ primary_render_task_id = filter_task_id;
+
+ surface_descriptor = SurfaceDescriptor::new_chained(
+ picture_task_id,
+ filter_task_id,
+ surface_rects.clipped_local,
+ );
+ }
+ }
+
+ let is_sub_graph = self.flags.contains(PictureFlags::IS_SUB_GRAPH);
+
+ frame_state.surface_builder.push_surface(
+ raster_config.surface_index,
+ is_sub_graph,
+ surface_rects.clipped_local,
+ surface_descriptor,
+ frame_state.surfaces,
+ frame_state.rg_builder,
+ );
+
+ self.primary_render_task_id = Some(primary_render_task_id);
+ }
+ None => {}
+ };
+
+ let state = PictureState {
+ map_local_to_pic,
+ map_pic_to_world,
+ };
+
+ let mut dirty_region_count = 0;
+
+ // If this is a picture cache, push the dirty region to ensure any
+ // child primitives are culled and clipped to the dirty rect(s).
+ if let Some(RasterConfig { composite_mode: PictureCompositeMode::TileCache { slice_id }, .. }) = self.raster_config {
+ let dirty_region = tile_caches[&slice_id].dirty_region.clone();
+ frame_state.push_dirty_region(dirty_region);
+ dirty_region_count += 1;
+ }
+
+ // Disallow subpixel AA if an intermediate surface is needed.
+ // TODO(lsalzman): allow overriding parent if intermediate surface is opaque
+ let subpixel_mode = match self.raster_config {
+ Some(RasterConfig { ref composite_mode, .. }) => {
+ let subpixel_mode = match composite_mode {
+ PictureCompositeMode::TileCache { slice_id } => {
+ tile_caches[&slice_id].subpixel_mode
+ }
+ PictureCompositeMode::Blit(..) |
+ PictureCompositeMode::ComponentTransferFilter(..) |
+ PictureCompositeMode::Filter(..) |
+ PictureCompositeMode::MixBlend(..) |
+ PictureCompositeMode::IntermediateSurface |
+ PictureCompositeMode::SvgFilter(..) => {
+ // TODO(gw): We can take advantage of the same logic that
+ // exists in the opaque rect detection for tile
+ // caches, to allow subpixel text on other surfaces
+ // that can be detected as opaque.
+ SubpixelMode::Deny
+ }
+ };
+
+ subpixel_mode
+ }
+ None => {
+ SubpixelMode::Allow
+ }
+ };
+
+ // Still disable subpixel AA if parent forbids it
+ let subpixel_mode = match (parent_subpixel_mode, subpixel_mode) {
+ (SubpixelMode::Allow, SubpixelMode::Allow) => {
+ // Both parent and this surface unconditionally allow subpixel AA
+ SubpixelMode::Allow
+ }
+ (SubpixelMode::Allow, SubpixelMode::Conditional { allowed_rect }) => {
+ // Parent allows, but we are conditional subpixel AA
+ SubpixelMode::Conditional {
+ allowed_rect,
+ }
+ }
+ (SubpixelMode::Conditional { allowed_rect }, SubpixelMode::Allow) => {
+ // Propagate conditional subpixel mode to child pictures that allow subpixel AA
+ SubpixelMode::Conditional {
+ allowed_rect,
+ }
+ }
+ (SubpixelMode::Conditional { .. }, SubpixelMode::Conditional { ..}) => {
+ unreachable!("bug: only top level picture caches have conditional subpixel");
+ }
+ (SubpixelMode::Deny, _) | (_, SubpixelMode::Deny) => {
+ // Either parent or this surface explicitly deny subpixel, these take precedence
+ SubpixelMode::Deny
+ }
+ };
+
+ let context = PictureContext {
+ pic_index,
+ raster_spatial_node_index: frame_state.surfaces[surface_index.0].raster_spatial_node_index,
+ surface_spatial_node_index,
+ surface_index,
+ dirty_region_count,
+ subpixel_mode,
+ };
+
+ let prim_list = mem::replace(&mut self.prim_list, PrimitiveList::empty());
+
+ Some((context, state, prim_list))
+ }
+
+ pub fn restore_context(
+ &mut self,
+ pic_index: PictureIndex,
+ prim_list: PrimitiveList,
+ context: PictureContext,
+ prim_instances: &[PrimitiveInstance],
+ frame_context: &FrameBuildingContext,
+ frame_state: &mut FrameBuildingState,
+ ) {
+ // Pop any dirty regions this picture set
+ for _ in 0 .. context.dirty_region_count {
+ frame_state.pop_dirty_region();
+ }
+
+ if self.raster_config.is_some() {
+ frame_state.surface_builder.pop_surface(
+ pic_index,
+ frame_state.rg_builder,
+ frame_state.cmd_buffers,
+ );
+ }
+
+ if let Picture3DContext::In { root_data: Some(ref mut list), plane_splitter_index, .. } = self.context_3d {
+ let splitter = &mut frame_state.plane_splitters[plane_splitter_index.0];
+
+ // Resolve split planes via BSP
+ PicturePrimitive::resolve_split_planes(
+ splitter,
+ list,
+ &mut frame_state.gpu_cache,
+ &frame_context.spatial_tree,
+ );
+
+ // Add the child prims to the relevant command buffers
+ let mut cmd_buffer_targets = Vec::new();
+ for child in list {
+ let child_prim_instance = &prim_instances[child.anchor.instance_index.0 as usize];
+
+ if frame_state.surface_builder.get_cmd_buffer_targets_for_prim(
+ &child_prim_instance.vis,
+ &mut cmd_buffer_targets,
+ ) {
+ let prim_cmd = PrimitiveCommand::complex(
+ child.anchor.instance_index,
+ child.gpu_address
+ );
+
+ frame_state.push_prim(
+ &prim_cmd,
+ child.anchor.spatial_node_index,
+ &cmd_buffer_targets,
+ );
+ }
+ }
+ }
+
+ self.prim_list = prim_list;
+ }
+
+ /// Add a primitive instance to the plane splitter. The function would generate
+ /// an appropriate polygon, clip it against the frustum, and register with the
+ /// given plane splitter.
+ pub fn add_split_plane(
+ splitter: &mut PlaneSplitter,
+ spatial_tree: &SpatialTree,
+ prim_spatial_node_index: SpatialNodeIndex,
+ original_local_rect: LayoutRect,
+ combined_local_clip_rect: &LayoutRect,
+ world_rect: WorldRect,
+ plane_split_anchor: PlaneSplitAnchor,
+ ) -> bool {
+ let transform = spatial_tree
+ .get_world_transform(prim_spatial_node_index);
+ let matrix = transform.clone().into_transform().cast().to_untyped();
+
+ // Apply the local clip rect here, before splitting. This is
+ // because the local clip rect can't be applied in the vertex
+ // shader for split composites, since we are drawing polygons
+ // rather that rectangles. The interpolation still works correctly
+ // since we determine the UVs by doing a bilerp with a factor
+ // from the original local rect.
+ let local_rect = match original_local_rect
+ .intersection(combined_local_clip_rect)
+ {
+ Some(rect) => rect.cast(),
+ None => return false,
+ };
+ let world_rect = world_rect.cast();
+
+ match transform {
+ CoordinateSpaceMapping::Local => {
+ let polygon = Polygon::from_rect(
+ local_rect.to_rect() * Scale::new(1.0),
+ plane_split_anchor,
+ );
+ splitter.add(polygon);
+ }
+ CoordinateSpaceMapping::ScaleOffset(scale_offset) if scale_offset.scale == Vector2D::new(1.0, 1.0) => {
+ let inv_matrix = scale_offset.inverse().to_transform().cast();
+ let polygon = Polygon::from_transformed_rect_with_inverse(
+ local_rect.to_rect().to_untyped(),
+ &matrix,
+ &inv_matrix,
+ plane_split_anchor,
+ ).unwrap();
+ splitter.add(polygon);
+ }
+ CoordinateSpaceMapping::ScaleOffset(_) |
+ CoordinateSpaceMapping::Transform(_) => {
+ let mut clipper = Clipper::new();
+ let results = clipper.clip_transformed(
+ Polygon::from_rect(
+ local_rect.to_rect().to_untyped(),
+ plane_split_anchor,
+ ),
+ &matrix,
+ Some(world_rect.to_rect().to_untyped()),
+ );
+ if let Ok(results) = results {
+ for poly in results {
+ splitter.add(poly);
+ }
+ }
+ }
+ }
+
+ true
+ }
+
+ fn resolve_split_planes(
+ splitter: &mut PlaneSplitter,
+ ordered: &mut Vec<OrderedPictureChild>,
+ gpu_cache: &mut GpuCache,
+ spatial_tree: &SpatialTree,
+ ) {
+ ordered.clear();
+
+ // Process the accumulated split planes and order them for rendering.
+ // Z axis is directed at the screen, `sort` is ascending, and we need back-to-front order.
+ let sorted = splitter.sort(vec3(0.0, 0.0, 1.0));
+ ordered.reserve(sorted.len());
+ for poly in sorted {
+ let transform = match spatial_tree
+ .get_world_transform(poly.anchor.spatial_node_index)
+ .inverse()
+ {
+ Some(transform) => transform.into_transform(),
+ // logging this would be a bit too verbose
+ None => continue,
+ };
+
+ let local_points = [
+ transform.transform_point3d(poly.points[0].cast_unit().to_f32()),
+ transform.transform_point3d(poly.points[1].cast_unit().to_f32()),
+ transform.transform_point3d(poly.points[2].cast_unit().to_f32()),
+ transform.transform_point3d(poly.points[3].cast_unit().to_f32()),
+ ];
+
+ // If any of the points are un-transformable, just drop this
+ // plane from drawing.
+ if local_points.iter().any(|p| p.is_none()) {
+ continue;
+ }
+
+ let p0 = local_points[0].unwrap();
+ let p1 = local_points[1].unwrap();
+ let p2 = local_points[2].unwrap();
+ let p3 = local_points[3].unwrap();
+ let gpu_blocks = [
+ [p0.x, p0.y, p1.x, p1.y].into(),
+ [p2.x, p2.y, p3.x, p3.y].into(),
+ ];
+ let gpu_handle = gpu_cache.push_per_frame_blocks(&gpu_blocks);
+ let gpu_address = gpu_cache.get_address(&gpu_handle);
+
+ ordered.push(OrderedPictureChild {
+ anchor: poly.anchor,
+ gpu_address,
+ });
+ }
+ }
+
+ /// Do initial checks to determine whether this picture should be drawn as part of the
+ /// frame build.
+ pub fn pre_update(
+ &mut self,
+ frame_context: &FrameBuildingContext,
+ ) {
+ // Resolve animation properties
+ self.resolve_scene_properties(frame_context.scene_properties);
+ }
+
+ /// Called during initial picture traversal, before we know the
+ /// bounding rect of children. It is possible to determine the
+ /// surface / raster config now though.
+ pub fn assign_surface(
+ &mut self,
+ frame_context: &FrameBuildingContext,
+ parent_surface_index: Option<SurfaceIndex>,
+ tile_caches: &mut FastHashMap<SliceId, Box<TileCacheInstance>>,
+ surfaces: &mut Vec<SurfaceInfo>,
+ ) -> Option<SurfaceIndex> {
+ // Reset raster config in case we early out below.
+ self.raster_config = None;
+
+ match self.composite_mode {
+ Some(ref composite_mode) => {
+ let surface_spatial_node_index = self.spatial_node_index;
+
+ // Currently, we ensure that the scaling factor is >= 1.0 as a smaller scale factor can result in blurry output.
+ let mut min_scale;
+ let mut max_scale = 1.0e32;
+
+ // If a raster root is established, this surface should be scaled based on the scale factors of the surface raster to parent raster transform.
+ // This scaling helps ensure that the content in this surface does not become blurry or pixelated when composited in the parent surface.
+
+ let world_scale_factors = match parent_surface_index {
+ Some(parent_surface_index) => {
+ let parent_surface = &surfaces[parent_surface_index.0];
+
+ let local_to_surface = frame_context
+ .spatial_tree
+ .get_relative_transform(
+ surface_spatial_node_index,
+ parent_surface.surface_spatial_node_index,
+ );
+
+ // Since we can't determine reasonable scale factors for transforms
+ // with perspective, just use a scale of (1,1) for now, which is
+ // what Gecko does when it choosed to supplies a scale factor anyway.
+ // In future, we might be able to improve the quality here by taking
+ // into account the screen rect after clipping, but for now this gives
+ // better results than just taking the matrix scale factors.
+ let scale_factors = if local_to_surface.is_perspective() {
+ (1.0, 1.0)
+ } else {
+ local_to_surface.scale_factors()
+ };
+
+ let scale_factors = (
+ scale_factors.0 * parent_surface.world_scale_factors.0,
+ scale_factors.1 * parent_surface.world_scale_factors.1,
+ );
+
+ scale_factors
+ }
+ None => {
+ let local_to_surface_scale_factors = frame_context
+ .spatial_tree
+ .get_relative_transform(
+ surface_spatial_node_index,
+ frame_context.spatial_tree.root_reference_frame_index(),
+ )
+ .scale_factors();
+
+ let scale_factors = (
+ local_to_surface_scale_factors.0,
+ local_to_surface_scale_factors.1,
+ );
+
+ scale_factors
+ }
+ };
+
+ // TODO(gw): For now, we disable snapping on any sub-graph, as that implies
+ // that the spatial / raster node must be the same as the parent
+ // surface. In future, we may be able to support snapping in these
+ // cases (if it's even useful?) or perhaps add a ENABLE_SNAPPING
+ // picture flag, if the IS_SUB_GRAPH is ever useful in a different
+ // context.
+ let allow_snapping = !self.flags.contains(PictureFlags::DISABLE_SNAPPING);
+
+ // Check if there is perspective or if an SVG filter is applied, and thus whether a new
+ // rasterization root should be established.
+ let (device_pixel_scale, raster_spatial_node_index, local_scale, world_scale_factors) = match composite_mode {
+ PictureCompositeMode::TileCache { slice_id } => {
+ let tile_cache = tile_caches.get_mut(&slice_id).unwrap();
+
+ // We only update the raster scale if we're in high quality zoom mode, or there is no
+ // pinch-zoom active. This means that in low quality pinch-zoom, we retain the initial
+ // scale factor until the zoom ends, then select a high quality zoom factor for the next
+ // frame to be drawn.
+ let update_raster_scale =
+ !frame_context.fb_config.low_quality_pinch_zoom ||
+ !frame_context.spatial_tree.get_spatial_node(tile_cache.spatial_node_index).is_ancestor_or_self_zooming;
+
+ if update_raster_scale {
+ // Get the complete scale-offset from local space to device space
+ let local_to_device = get_relative_scale_offset(
+ tile_cache.spatial_node_index,
+ frame_context.root_spatial_node_index,
+ frame_context.spatial_tree,
+ );
+
+ tile_cache.current_raster_scale = local_to_device.scale.x;
+ }
+
+ // We may need to minify when zooming out picture cache tiles
+ min_scale = 0.0;
+
+ if frame_context.fb_config.low_quality_pinch_zoom {
+ // Force the scale for this tile cache to be the currently selected
+ // local raster scale, so we don't need to rasterize tiles during
+ // the pinch-zoom.
+ min_scale = tile_cache.current_raster_scale;
+ max_scale = tile_cache.current_raster_scale;
+ }
+
+ // Pick the largest scale factor of the transform for the scaling factor.
+ let scaling_factor = world_scale_factors.0.max(world_scale_factors.1).max(min_scale).min(max_scale);
+
+ let device_pixel_scale = Scale::new(scaling_factor);
+
+ (device_pixel_scale, surface_spatial_node_index, (1.0, 1.0), world_scale_factors)
+ }
+ _ => {
+ let surface_spatial_node = frame_context.spatial_tree.get_spatial_node(surface_spatial_node_index);
+
+ let enable_snapping =
+ allow_snapping &&
+ surface_spatial_node.coordinate_system_id == CoordinateSystemId::root() &&
+ surface_spatial_node.snapping_transform.is_some();
+
+ if enable_snapping {
+ let raster_spatial_node_index = frame_context.spatial_tree.root_reference_frame_index();
+
+ let local_to_raster_transform = frame_context
+ .spatial_tree
+ .get_relative_transform(
+ self.spatial_node_index,
+ raster_spatial_node_index,
+ );
+
+ let local_scale = local_to_raster_transform.scale_factors();
+
+ (Scale::new(1.0), raster_spatial_node_index, local_scale, (1.0, 1.0))
+ } else {
+ // If client supplied a specific local scale, use that instead of
+ // estimating from parent transform
+ let world_scale_factors = match self.raster_space {
+ RasterSpace::Screen => world_scale_factors,
+ RasterSpace::Local(scale) => (scale, scale),
+ };
+
+ let device_pixel_scale = Scale::new(world_scale_factors.0.max(world_scale_factors.1));
+
+ (device_pixel_scale, surface_spatial_node_index, (1.0, 1.0), world_scale_factors)
+ }
+ }
+ };
+
+ let surface = SurfaceInfo::new(
+ surface_spatial_node_index,
+ raster_spatial_node_index,
+ frame_context.global_screen_world_rect,
+ &frame_context.spatial_tree,
+ device_pixel_scale,
+ world_scale_factors,
+ local_scale,
+ allow_snapping,
+ );
+
+ let surface_index = SurfaceIndex(surfaces.len());
+ surfaces.push(surface);
+
+ self.raster_config = Some(RasterConfig {
+ composite_mode: composite_mode.clone(),
+ surface_index,
+ });
+
+ Some(surface_index)
+ }
+ None => {
+ None
+ }
+ }
+ }
+
+ /// Called after updating child pictures during the initial
+ /// picture traversal. Bounding rects are propagated from
+ /// child pictures up to parent picture surfaces, so that the
+ /// parent bounding rect includes any dynamic picture bounds.
+ pub fn propagate_bounding_rect(
+ &mut self,
+ surface_index: SurfaceIndex,
+ parent_surface_index: Option<SurfaceIndex>,
+ surfaces: &mut [SurfaceInfo],
+ frame_context: &FrameBuildingContext,
+ ) {
+ let surface = &mut surfaces[surface_index.0];
+
+ for cluster in &mut self.prim_list.clusters {
+ cluster.flags.remove(ClusterFlags::IS_VISIBLE);
+
+ // Skip the cluster if backface culled.
+ if !cluster.flags.contains(ClusterFlags::IS_BACKFACE_VISIBLE) {
+ // For in-preserve-3d primitives and pictures, the backface visibility is
+ // evaluated relative to the containing block.
+ if let Picture3DContext::In { ancestor_index, .. } = self.context_3d {
+ let mut face = VisibleFace::Front;
+ frame_context.spatial_tree.get_relative_transform_with_face(
+ cluster.spatial_node_index,
+ ancestor_index,
+ Some(&mut face),
+ );
+ if face == VisibleFace::Back {
+ continue
+ }
+ }
+ }
+
+ // No point including this cluster if it can't be transformed
+ let spatial_node = &frame_context
+ .spatial_tree
+ .get_spatial_node(cluster.spatial_node_index);
+ if !spatial_node.invertible {
+ continue;
+ }
+
+ // Map the cluster bounding rect into the space of the surface, and
+ // include it in the surface bounding rect.
+ surface.map_local_to_surface.set_target_spatial_node(
+ cluster.spatial_node_index,
+ frame_context.spatial_tree,
+ );
+
+ // Mark the cluster visible, since it passed the invertible and
+ // backface checks.
+ cluster.flags.insert(ClusterFlags::IS_VISIBLE);
+ if let Some(cluster_rect) = surface.map_local_to_surface.map(&cluster.bounding_rect) {
+ surface.unclipped_local_rect = surface.unclipped_local_rect.union(&cluster_rect);
+ }
+ }
+
+ // If this picture establishes a surface, then map the surface bounding
+ // rect into the parent surface coordinate space, and propagate that up
+ // to the parent.
+ if let Some(ref mut raster_config) = self.raster_config {
+ // Propagate up to parent surface, now that we know this surface's static rect
+ if let Some(parent_surface_index) = parent_surface_index {
+ let surface_rect = raster_config.composite_mode.get_coverage(
+ surface,
+ Some(surface.unclipped_local_rect.cast_unit()),
+ );
+
+ let parent_surface = &mut surfaces[parent_surface_index.0];
+ parent_surface.map_local_to_surface.set_target_spatial_node(
+ self.spatial_node_index,
+ frame_context.spatial_tree,
+ );
+
+ // Drop shadows draw both a content and shadow rect, so need to expand the local
+ // rect of any surfaces to be composited in parent surfaces correctly.
+
+ if let Some(parent_surface_rect) = parent_surface
+ .map_local_to_surface
+ .map(&surface_rect)
+ {
+ parent_surface.unclipped_local_rect =
+ parent_surface.unclipped_local_rect.union(&parent_surface_rect);
+ }
+ }
+ }
+ }
+
+ pub fn prepare_for_render(
+ &mut self,
+ frame_state: &mut FrameBuildingState,
+ data_stores: &mut DataStores,
+ ) -> bool {
+ let raster_config = match self.raster_config {
+ Some(ref mut raster_config) => raster_config,
+ None => {
+ return true
+ }
+ };
+
+ // TODO(gw): Almost all of the Picture types below use extra_gpu_cache_data
+ // to store the same type of data. The exception is the filter
+ // with a ColorMatrix, which stores the color matrix here. It's
+ // probably worth tidying this code up to be a bit more consistent.
+ // Perhaps store the color matrix after the common data, even though
+ // it's not used by that shader.
+
+ match raster_config.composite_mode {
+ PictureCompositeMode::TileCache { .. } => {}
+ PictureCompositeMode::Filter(Filter::Blur { .. }) => {}
+ PictureCompositeMode::Filter(Filter::DropShadows(ref shadows)) => {
+ self.extra_gpu_data_handles.resize(shadows.len(), GpuCacheHandle::new());
+ for (shadow, extra_handle) in shadows.iter().zip(self.extra_gpu_data_handles.iter_mut()) {
+ if let Some(mut request) = frame_state.gpu_cache.request(extra_handle) {
+ let surface = &frame_state.surfaces[raster_config.surface_index.0];
+ let prim_rect = surface.clipped_local_rect.cast_unit();
+
+ // Basic brush primitive header is (see end of prepare_prim_for_render_inner in prim_store.rs)
+ // [brush specific data]
+ // [segment_rect, segment data]
+ let (blur_inflation_x, blur_inflation_y) = surface.clamp_blur_radius(
+ shadow.blur_radius,
+ shadow.blur_radius,
+ );
+
+ let shadow_rect = prim_rect.inflate(
+ blur_inflation_x * BLUR_SAMPLE_SCALE,
+ blur_inflation_y * BLUR_SAMPLE_SCALE,
+ ).translate(shadow.offset);
+
+ // ImageBrush colors
+ request.push(shadow.color.premultiplied());
+ request.push(PremultipliedColorF::WHITE);
+ request.push([
+ shadow_rect.width(),
+ shadow_rect.height(),
+ 0.0,
+ 0.0,
+ ]);
+
+ // segment rect / extra data
+ request.push(shadow_rect);
+ request.push([0.0, 0.0, 0.0, 0.0]);
+ }
+ }
+ }
+ PictureCompositeMode::Filter(ref filter) => {
+ match *filter {
+ Filter::ColorMatrix(ref m) => {
+ if self.extra_gpu_data_handles.is_empty() {
+ self.extra_gpu_data_handles.push(GpuCacheHandle::new());
+ }
+ if let Some(mut request) = frame_state.gpu_cache.request(&mut self.extra_gpu_data_handles[0]) {
+ for i in 0..5 {
+ request.push([m[i*4], m[i*4+1], m[i*4+2], m[i*4+3]]);
+ }
+ }
+ }
+ Filter::Flood(ref color) => {
+ if self.extra_gpu_data_handles.is_empty() {
+ self.extra_gpu_data_handles.push(GpuCacheHandle::new());
+ }
+ if let Some(mut request) = frame_state.gpu_cache.request(&mut self.extra_gpu_data_handles[0]) {
+ request.push(color.to_array());
+ }
+ }
+ _ => {}
+ }
+ }
+ PictureCompositeMode::ComponentTransferFilter(handle) => {
+ let filter_data = &mut data_stores.filter_data[handle];
+ filter_data.update(frame_state);
+ }
+ PictureCompositeMode::MixBlend(..) |
+ PictureCompositeMode::Blit(_) |
+ PictureCompositeMode::IntermediateSurface |
+ PictureCompositeMode::SvgFilter(..) => {}
+ }
+
+ true
+ }
+}
+
+fn get_transform_key(
+ spatial_node_index: SpatialNodeIndex,
+ cache_spatial_node_index: SpatialNodeIndex,
+ spatial_tree: &SpatialTree,
+) -> TransformKey {
+ spatial_tree.get_relative_transform(
+ spatial_node_index,
+ cache_spatial_node_index,
+ ).into()
+}
+
+/// A key for storing primitive comparison results during tile dependency tests.
+#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
+struct PrimitiveComparisonKey {
+ prev_index: PrimitiveDependencyIndex,
+ curr_index: PrimitiveDependencyIndex,
+}
+
+/// Information stored an image dependency
+#[derive(Debug, Copy, Clone, PartialEq, PeekPoke, Default)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ImageDependency {
+ pub key: ImageKey,
+ pub generation: ImageGeneration,
+}
+
+impl ImageDependency {
+ pub const INVALID: ImageDependency = ImageDependency {
+ key: ImageKey::DUMMY,
+ generation: ImageGeneration::INVALID,
+ };
+}
+
+/// In some cases, we need to know the dirty rect of all tiles in order
+/// to correctly invalidate a primitive.
+#[derive(Debug)]
+struct DeferredDirtyTest {
+ /// The tile rect that the primitive being checked affects
+ tile_rect: TileRect,
+ /// The picture-cache local rect of the primitive being checked
+ prim_rect: PictureRect,
+}
+
+/// A helper struct to compare a primitive and all its sub-dependencies.
+struct PrimitiveComparer<'a> {
+ prev_data: &'a [u8],
+ curr_data: &'a [u8],
+ prev_frame_id: FrameId,
+ curr_frame_id: FrameId,
+ resource_cache: &'a ResourceCache,
+ spatial_node_comparer: &'a mut SpatialNodeComparer,
+ opacity_bindings: &'a FastHashMap<PropertyBindingId, OpacityBindingInfo>,
+ color_bindings: &'a FastHashMap<PropertyBindingId, ColorBindingInfo>,
+}
+
+impl<'a> PrimitiveComparer<'a> {
+ fn new(
+ prev: &'a TileDescriptor,
+ curr: &'a TileDescriptor,
+ resource_cache: &'a ResourceCache,
+ spatial_node_comparer: &'a mut SpatialNodeComparer,
+ opacity_bindings: &'a FastHashMap<PropertyBindingId, OpacityBindingInfo>,
+ color_bindings: &'a FastHashMap<PropertyBindingId, ColorBindingInfo>,
+ ) -> Self {
+ PrimitiveComparer {
+ prev_data: &prev.dep_data,
+ curr_data: &curr.dep_data,
+ prev_frame_id: prev.last_updated_frame_id,
+ curr_frame_id: curr.last_updated_frame_id,
+ resource_cache,
+ spatial_node_comparer,
+ opacity_bindings,
+ color_bindings,
+ }
+ }
+
+ /// Check if two primitive descriptors are the same.
+ fn compare_prim(
+ &mut self,
+ prev_desc: &PrimitiveDescriptor,
+ curr_desc: &PrimitiveDescriptor,
+ ) -> PrimitiveCompareResult {
+ let resource_cache = self.resource_cache;
+ let spatial_node_comparer = &mut self.spatial_node_comparer;
+ let opacity_bindings = self.opacity_bindings;
+ let color_bindings = self.color_bindings;
+
+ // Check equality of the PrimitiveDescriptor
+ if prev_desc != curr_desc {
+ return PrimitiveCompareResult::Descriptor;
+ }
+
+ let mut prev_dep_data = &self.prev_data[prev_desc.dep_offset as usize ..];
+ let mut curr_dep_data = &self.curr_data[curr_desc.dep_offset as usize ..];
+
+ let mut prev_dep = PrimitiveDependency::SpatialNode { index: SpatialNodeIndex::INVALID };
+ let mut curr_dep = PrimitiveDependency::SpatialNode { index: SpatialNodeIndex::INVALID };
+
+ debug_assert_eq!(prev_desc.dep_count, curr_desc.dep_count);
+
+ for _ in 0 .. prev_desc.dep_count {
+ prev_dep_data = peek_from_slice(prev_dep_data, &mut prev_dep);
+ curr_dep_data = peek_from_slice(curr_dep_data, &mut curr_dep);
+
+ match (&prev_dep, &curr_dep) {
+ (PrimitiveDependency::Clip { clip: prev }, PrimitiveDependency::Clip { clip: curr }) => {
+ if prev != curr {
+ return PrimitiveCompareResult::Clip;
+ }
+ }
+ (PrimitiveDependency::SpatialNode { index: prev }, PrimitiveDependency::SpatialNode { index: curr }) => {
+ let prev_key = SpatialNodeKey {
+ spatial_node_index: *prev,
+ frame_id: self.prev_frame_id,
+ };
+ let curr_key = SpatialNodeKey {
+ spatial_node_index: *curr,
+ frame_id: self.curr_frame_id,
+ };
+ if !spatial_node_comparer.are_transforms_equivalent(&prev_key, &curr_key) {
+ return PrimitiveCompareResult::Transform;
+ }
+ }
+ (PrimitiveDependency::OpacityBinding { binding: prev }, PrimitiveDependency::OpacityBinding { binding: curr }) => {
+ if prev != curr {
+ return PrimitiveCompareResult::OpacityBinding;
+ }
+
+ if let OpacityBinding::Binding(id) = curr {
+ if opacity_bindings
+ .get(id)
+ .map_or(true, |info| info.changed) {
+ return PrimitiveCompareResult::OpacityBinding;
+ }
+ }
+ }
+ (PrimitiveDependency::ColorBinding { binding: prev }, PrimitiveDependency::ColorBinding { binding: curr }) => {
+ if prev != curr {
+ return PrimitiveCompareResult::ColorBinding;
+ }
+
+ if let ColorBinding::Binding(id) = curr {
+ if color_bindings
+ .get(id)
+ .map_or(true, |info| info.changed) {
+ return PrimitiveCompareResult::ColorBinding;
+ }
+ }
+ }
+ (PrimitiveDependency::Image { image: prev }, PrimitiveDependency::Image { image: curr }) => {
+ if prev != curr {
+ return PrimitiveCompareResult::Image;
+ }
+
+ if resource_cache.get_image_generation(curr.key) != curr.generation {
+ return PrimitiveCompareResult::Image;
+ }
+ }
+ _ => {
+ // There was a mismatch between types of dependencies, so something changed
+ return PrimitiveCompareResult::Descriptor;
+ }
+ }
+ }
+
+ PrimitiveCompareResult::Equal
+ }
+}
+
+/// Details for a node in a quadtree that tracks dirty rects for a tile.
+#[cfg_attr(any(feature="capture",feature="replay"), derive(Clone))]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum TileNodeKind {
+ Leaf {
+ /// The index buffer of primitives that affected this tile previous frame
+ #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))]
+ prev_indices: Vec<PrimitiveDependencyIndex>,
+ /// The index buffer of primitives that affect this tile on this frame
+ #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))]
+ curr_indices: Vec<PrimitiveDependencyIndex>,
+ /// A bitset of which of the last 64 frames have been dirty for this leaf.
+ #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))]
+ dirty_tracker: u64,
+ /// The number of frames since this node split or merged.
+ #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))]
+ frames_since_modified: usize,
+ },
+ Node {
+ /// The four children of this node
+ children: Vec<TileNode>,
+ },
+}
+
+/// The kind of modification that a tile wants to do
+#[derive(Copy, Clone, PartialEq, Debug)]
+enum TileModification {
+ Split,
+ Merge,
+}
+
+/// A node in the dirty rect tracking quadtree.
+#[cfg_attr(any(feature="capture",feature="replay"), derive(Clone))]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct TileNode {
+ /// Leaf or internal node
+ pub kind: TileNodeKind,
+ /// Rect of this node in the same space as the tile cache picture
+ pub rect: PictureBox2D,
+}
+
+impl TileNode {
+ /// Construct a new leaf node, with the given primitive dependency index buffer
+ fn new_leaf(curr_indices: Vec<PrimitiveDependencyIndex>) -> Self {
+ TileNode {
+ kind: TileNodeKind::Leaf {
+ prev_indices: Vec::new(),
+ curr_indices,
+ dirty_tracker: 0,
+ frames_since_modified: 0,
+ },
+ rect: PictureBox2D::zero(),
+ }
+ }
+
+ /// Draw debug information about this tile node
+ fn draw_debug_rects(
+ &self,
+ pic_to_world_mapper: &SpaceMapper<PicturePixel, WorldPixel>,
+ is_opaque: bool,
+ local_valid_rect: PictureRect,
+ scratch: &mut PrimitiveScratchBuffer,
+ global_device_pixel_scale: DevicePixelScale,
+ ) {
+ match self.kind {
+ TileNodeKind::Leaf { dirty_tracker, .. } => {
+ let color = if (dirty_tracker & 1) != 0 {
+ debug_colors::RED
+ } else if is_opaque {
+ debug_colors::GREEN
+ } else {
+ debug_colors::YELLOW
+ };
+
+ if let Some(local_rect) = local_valid_rect.intersection(&self.rect) {
+ let world_rect = pic_to_world_mapper
+ .map(&local_rect)
+ .unwrap();
+ let device_rect = world_rect * global_device_pixel_scale;
+
+ let outer_color = color.scale_alpha(0.3);
+ let inner_color = outer_color.scale_alpha(0.5);
+ scratch.push_debug_rect(
+ device_rect.inflate(-3.0, -3.0),
+ outer_color,
+ inner_color
+ );
+ }
+ }
+ TileNodeKind::Node { ref children, .. } => {
+ for child in children.iter() {
+ child.draw_debug_rects(
+ pic_to_world_mapper,
+ is_opaque,
+ local_valid_rect,
+ scratch,
+ global_device_pixel_scale,
+ );
+ }
+ }
+ }
+ }
+
+ /// Calculate the four child rects for a given node
+ fn get_child_rects(
+ rect: &PictureBox2D,
+ result: &mut [PictureBox2D; 4],
+ ) {
+ let p0 = rect.min;
+ let p1 = rect.max;
+ let pc = p0 + rect.size() * 0.5;
+
+ *result = [
+ PictureBox2D::new(
+ p0,
+ pc,
+ ),
+ PictureBox2D::new(
+ PicturePoint::new(pc.x, p0.y),
+ PicturePoint::new(p1.x, pc.y),
+ ),
+ PictureBox2D::new(
+ PicturePoint::new(p0.x, pc.y),
+ PicturePoint::new(pc.x, p1.y),
+ ),
+ PictureBox2D::new(
+ pc,
+ p1,
+ ),
+ ];
+ }
+
+ /// Called during pre_update, to clear the current dependencies
+ fn clear(
+ &mut self,
+ rect: PictureBox2D,
+ ) {
+ self.rect = rect;
+
+ match self.kind {
+ TileNodeKind::Leaf { ref mut prev_indices, ref mut curr_indices, ref mut dirty_tracker, ref mut frames_since_modified } => {
+ // Swap current dependencies to be the previous frame
+ mem::swap(prev_indices, curr_indices);
+ curr_indices.clear();
+ // Note that another frame has passed in the dirty bit trackers
+ *dirty_tracker = *dirty_tracker << 1;
+ *frames_since_modified += 1;
+ }
+ TileNodeKind::Node { ref mut children, .. } => {
+ let mut child_rects = [PictureBox2D::zero(); 4];
+ TileNode::get_child_rects(&rect, &mut child_rects);
+ assert_eq!(child_rects.len(), children.len());
+
+ for (child, rect) in children.iter_mut().zip(child_rects.iter()) {
+ child.clear(*rect);
+ }
+ }
+ }
+ }
+
+ /// Add a primitive dependency to this node
+ fn add_prim(
+ &mut self,
+ index: PrimitiveDependencyIndex,
+ prim_rect: &PictureBox2D,
+ ) {
+ match self.kind {
+ TileNodeKind::Leaf { ref mut curr_indices, .. } => {
+ curr_indices.push(index);
+ }
+ TileNodeKind::Node { ref mut children, .. } => {
+ for child in children.iter_mut() {
+ if child.rect.intersects(prim_rect) {
+ child.add_prim(index, prim_rect);
+ }
+ }
+ }
+ }
+ }
+
+ /// Apply a merge or split operation to this tile, if desired
+ fn maybe_merge_or_split(
+ &mut self,
+ level: i32,
+ curr_prims: &[PrimitiveDescriptor],
+ max_split_levels: i32,
+ ) {
+ // Determine if this tile wants to split or merge
+ let mut tile_mod = None;
+
+ fn get_dirty_frames(
+ dirty_tracker: u64,
+ frames_since_modified: usize,
+ ) -> Option<u32> {
+ // Only consider splitting or merging at least 64 frames since we last changed
+ if frames_since_modified > 64 {
+ // Each bit in the tracker is a frame that was recently invalidated
+ Some(dirty_tracker.count_ones())
+ } else {
+ None
+ }
+ }
+
+ match self.kind {
+ TileNodeKind::Leaf { dirty_tracker, frames_since_modified, .. } => {
+ // Only consider splitting if the tree isn't too deep.
+ if level < max_split_levels {
+ if let Some(dirty_frames) = get_dirty_frames(dirty_tracker, frames_since_modified) {
+ // If the tile has invalidated > 50% of the recent number of frames, split.
+ if dirty_frames > 32 {
+ tile_mod = Some(TileModification::Split);
+ }
+ }
+ }
+ }
+ TileNodeKind::Node { ref children, .. } => {
+ // There's two conditions that cause a node to merge its children:
+ // (1) If _all_ the child nodes are constantly invalidating, then we are wasting
+ // CPU time tracking dependencies for each child, so merge them.
+ // (2) If _none_ of the child nodes are recently invalid, then the page content
+ // has probably changed, and we no longer need to track fine grained dependencies here.
+
+ let mut static_count = 0;
+ let mut changing_count = 0;
+
+ for child in children {
+ // Only consider merging nodes at the edge of the tree.
+ if let TileNodeKind::Leaf { dirty_tracker, frames_since_modified, .. } = child.kind {
+ if let Some(dirty_frames) = get_dirty_frames(dirty_tracker, frames_since_modified) {
+ if dirty_frames == 0 {
+ // Hasn't been invalidated for some time
+ static_count += 1;
+ } else if dirty_frames == 64 {
+ // Is constantly being invalidated
+ changing_count += 1;
+ }
+ }
+ }
+
+ // Only merge if all the child tiles are in agreement. Otherwise, we have some
+ // that are invalidating / static, and it's worthwhile tracking dependencies for
+ // them individually.
+ if static_count == 4 || changing_count == 4 {
+ tile_mod = Some(TileModification::Merge);
+ }
+ }
+ }
+ }
+
+ match tile_mod {
+ Some(TileModification::Split) => {
+ // To split a node, take the current dependency index buffer for this node, and
+ // split it into child index buffers.
+ let curr_indices = match self.kind {
+ TileNodeKind::Node { .. } => {
+ unreachable!("bug - only leaves can split");
+ }
+ TileNodeKind::Leaf { ref mut curr_indices, .. } => {
+ curr_indices.take()
+ }
+ };
+
+ let mut child_rects = [PictureBox2D::zero(); 4];
+ TileNode::get_child_rects(&self.rect, &mut child_rects);
+
+ let mut child_indices = [
+ Vec::new(),
+ Vec::new(),
+ Vec::new(),
+ Vec::new(),
+ ];
+
+ // Step through the index buffer, and add primitives to each of the children
+ // that they intersect.
+ for index in curr_indices {
+ let prim = &curr_prims[index.0 as usize];
+ for (child_rect, indices) in child_rects.iter().zip(child_indices.iter_mut()) {
+ if prim.prim_clip_box.intersects(child_rect) {
+ indices.push(index);
+ }
+ }
+ }
+
+ // Create the child nodes and switch from leaf -> node.
+ let children = child_indices
+ .iter_mut()
+ .map(|i| TileNode::new_leaf(mem::replace(i, Vec::new())))
+ .collect();
+
+ self.kind = TileNodeKind::Node {
+ children,
+ };
+ }
+ Some(TileModification::Merge) => {
+ // Construct a merged index buffer by collecting the dependency index buffers
+ // from each child, and merging them into a de-duplicated index buffer.
+ let merged_indices = match self.kind {
+ TileNodeKind::Node { ref mut children, .. } => {
+ let mut merged_indices = Vec::new();
+
+ for child in children.iter() {
+ let child_indices = match child.kind {
+ TileNodeKind::Leaf { ref curr_indices, .. } => {
+ curr_indices
+ }
+ TileNodeKind::Node { .. } => {
+ unreachable!("bug: child is not a leaf");
+ }
+ };
+ merged_indices.extend_from_slice(child_indices);
+ }
+
+ merged_indices.sort();
+ merged_indices.dedup();
+
+ merged_indices
+ }
+ TileNodeKind::Leaf { .. } => {
+ unreachable!("bug - trying to merge a leaf");
+ }
+ };
+
+ // Switch from a node to a leaf, with the combined index buffer
+ self.kind = TileNodeKind::Leaf {
+ prev_indices: Vec::new(),
+ curr_indices: merged_indices,
+ dirty_tracker: 0,
+ frames_since_modified: 0,
+ };
+ }
+ None => {
+ // If this node didn't merge / split, then recurse into children
+ // to see if they want to split / merge.
+ if let TileNodeKind::Node { ref mut children, .. } = self.kind {
+ for child in children.iter_mut() {
+ child.maybe_merge_or_split(
+ level+1,
+ curr_prims,
+ max_split_levels,
+ );
+ }
+ }
+ }
+ }
+ }
+
+ /// Update the dirty state of this node, building the overall dirty rect
+ fn update_dirty_rects(
+ &mut self,
+ prev_prims: &[PrimitiveDescriptor],
+ curr_prims: &[PrimitiveDescriptor],
+ prim_comparer: &mut PrimitiveComparer,
+ dirty_rect: &mut PictureBox2D,
+ compare_cache: &mut FastHashMap<PrimitiveComparisonKey, PrimitiveCompareResult>,
+ invalidation_reason: &mut Option<InvalidationReason>,
+ frame_context: &FrameVisibilityContext,
+ ) {
+ match self.kind {
+ TileNodeKind::Node { ref mut children, .. } => {
+ for child in children.iter_mut() {
+ child.update_dirty_rects(
+ prev_prims,
+ curr_prims,
+ prim_comparer,
+ dirty_rect,
+ compare_cache,
+ invalidation_reason,
+ frame_context,
+ );
+ }
+ }
+ TileNodeKind::Leaf { ref prev_indices, ref curr_indices, ref mut dirty_tracker, .. } => {
+ // If the index buffers are of different length, they must be different
+ if prev_indices.len() == curr_indices.len() {
+ // Walk each index buffer, comparing primitives
+ for (prev_index, curr_index) in prev_indices.iter().zip(curr_indices.iter()) {
+ let i0 = prev_index.0 as usize;
+ let i1 = curr_index.0 as usize;
+
+ // Compare the primitives, caching the result in a hash map
+ // to save comparisons in other tree nodes.
+ let key = PrimitiveComparisonKey {
+ prev_index: *prev_index,
+ curr_index: *curr_index,
+ };
+
+ let prim_compare_result = *compare_cache
+ .entry(key)
+ .or_insert_with(|| {
+ let prev = &prev_prims[i0];
+ let curr = &curr_prims[i1];
+ prim_comparer.compare_prim(prev, curr)
+ });
+
+ // If not the same, mark this node as dirty and update the dirty rect
+ if prim_compare_result != PrimitiveCompareResult::Equal {
+ if invalidation_reason.is_none() {
+ *invalidation_reason = Some(InvalidationReason::Content);
+ }
+ *dirty_rect = self.rect.union(dirty_rect);
+ *dirty_tracker = *dirty_tracker | 1;
+ break;
+ }
+ }
+ } else {
+ if invalidation_reason.is_none() {
+ *invalidation_reason = Some(InvalidationReason::PrimCount);
+ }
+ *dirty_rect = self.rect.union(dirty_rect);
+ *dirty_tracker = *dirty_tracker | 1;
+ }
+ }
+ }
+ }
+}
+
+impl CompositeState {
+ // A helper function to destroy all native surfaces for a given list of tiles
+ pub fn destroy_native_tiles<'a, I: Iterator<Item = &'a mut Box<Tile>>>(
+ &mut self,
+ tiles_iter: I,
+ resource_cache: &mut ResourceCache,
+ ) {
+ // Any old tiles that remain after the loop above are going to be dropped. For
+ // simple composite mode, the texture cache handle will expire and be collected
+ // by the texture cache. For native compositor mode, we need to explicitly
+ // invoke a callback to the client to destroy that surface.
+ if let CompositorKind::Native { .. } = self.compositor_kind {
+ for tile in tiles_iter {
+ // Only destroy native surfaces that have been allocated. It's
+ // possible for display port tiles to be created that never
+ // come on screen, and thus never get a native surface allocated.
+ if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { ref mut id, .. }, .. }) = tile.surface {
+ if let Some(id) = id.take() {
+ resource_cache.destroy_compositor_tile(id);
+ }
+ }
+ }
+ }
+ }
+}
+
+fn get_relative_scale_offset(
+ child_spatial_node_index: SpatialNodeIndex,
+ parent_spatial_node_index: SpatialNodeIndex,
+ spatial_tree: &SpatialTree,
+) -> ScaleOffset {
+ let transform = spatial_tree.get_relative_transform(
+ child_spatial_node_index,
+ parent_spatial_node_index,
+ );
+ let mut scale_offset = match transform {
+ CoordinateSpaceMapping::Local => ScaleOffset::identity(),
+ CoordinateSpaceMapping::ScaleOffset(scale_offset) => scale_offset,
+ CoordinateSpaceMapping::Transform(m) => {
+ ScaleOffset::from_transform(&m).expect("bug: pictures caches don't support complex transforms")
+ }
+ };
+
+ // Compositors expect things to be aligned on device pixels. Logic at a higher level ensures that is
+ // true, but floating point inaccuracy can sometimes result in small differences, so remove
+ // them here.
+ scale_offset.offset = scale_offset.offset.round();
+
+ scale_offset
+}
+
+pub fn calculate_screen_uv(
+ p: DevicePoint,
+ clipped: DeviceRect,
+) -> DeviceHomogeneousVector {
+ // TODO(gw): Switch to a simple mix, no bilerp / homogeneous vec needed anymore
+ DeviceHomogeneousVector::new(
+ (p.x - clipped.min.x) / (clipped.max.x - clipped.min.x),
+ (p.y - clipped.min.y) / (clipped.max.y - clipped.min.y),
+ 0.0,
+ 1.0,
+ )
+}
+
+fn get_surface_rects(
+ surface_index: SurfaceIndex,
+ composite_mode: &PictureCompositeMode,
+ parent_surface_index: SurfaceIndex,
+ surfaces: &mut [SurfaceInfo],
+ spatial_tree: &SpatialTree,
+ max_surface_size: f32,
+) -> Option<SurfaceAllocInfo> {
+ let parent_surface = &surfaces[parent_surface_index.0];
+
+ let local_to_parent = SpaceMapper::new_with_target(
+ parent_surface.surface_spatial_node_index,
+ surfaces[surface_index.0].surface_spatial_node_index,
+ parent_surface.clipping_rect,
+ spatial_tree,
+ );
+
+ let local_clip_rect = local_to_parent
+ .unmap(&parent_surface.clipping_rect)
+ .unwrap_or(PictureRect::max_rect())
+ .cast_unit();
+
+ let surface = &mut surfaces[surface_index.0];
+
+ let (clipped_local, unclipped_local) = match composite_mode {
+ PictureCompositeMode::Filter(Filter::DropShadows(ref shadows)) => {
+ let local_prim_rect = surface.clipped_local_rect;
+
+ let mut required_local_rect = match local_prim_rect.intersection(&local_clip_rect) {
+ Some(rect) => rect,
+ None => return None,
+ };
+
+ for shadow in shadows {
+ let (blur_radius_x, blur_radius_y) = surface.clamp_blur_radius(
+ shadow.blur_radius,
+ shadow.blur_radius,
+ );
+ let blur_inflation_x = blur_radius_x * BLUR_SAMPLE_SCALE;
+ let blur_inflation_y = blur_radius_y * BLUR_SAMPLE_SCALE;
+
+ let local_shadow_rect = local_prim_rect
+ .translate(shadow.offset.cast_unit());
+
+ if let Some(clipped_shadow_rect) = local_clip_rect.intersection(&local_shadow_rect) {
+ let required_shadow_rect = clipped_shadow_rect.inflate(blur_inflation_x, blur_inflation_y);
+
+ let local_clipped_shadow_rect = required_shadow_rect.translate(-shadow.offset.cast_unit());
+
+ required_local_rect = required_local_rect.union(&local_clipped_shadow_rect);
+ }
+ }
+
+ let unclipped = composite_mode.get_rect(surface, None);
+ let clipped = required_local_rect;
+
+ let clipped = match clipped.intersection(&unclipped.cast_unit()) {
+ Some(rect) => rect,
+ None => return None,
+ };
+
+ (clipped, unclipped)
+ }
+ _ => {
+ let surface_origin = surface.clipped_local_rect.min.to_vector().cast_unit();
+
+ let normalized_prim_rect = composite_mode
+ .get_rect(surface, None)
+ .translate(-surface_origin);
+
+ let normalized_clip_rect = local_clip_rect
+ .cast_unit()
+ .translate(-surface_origin);
+
+ let norm_clipped_rect = match normalized_prim_rect.intersection(&normalized_clip_rect) {
+ Some(rect) => rect,
+ None => return None,
+ };
+
+ let norm_clipped_rect = composite_mode.get_rect(surface, Some(norm_clipped_rect));
+
+ let norm_clipped_rect = match norm_clipped_rect.intersection(&normalized_prim_rect) {
+ Some(rect) => rect,
+ None => return None,
+ };
+
+ let unclipped = normalized_prim_rect.translate(surface_origin);
+ let clipped = norm_clipped_rect.translate(surface_origin);
+
+ (clipped.cast_unit(), unclipped.cast_unit())
+ }
+ };
+
+ let (mut clipped, mut unclipped) = if surface.raster_spatial_node_index != surface.surface_spatial_node_index {
+ assert_eq!(surface.device_pixel_scale.0, 1.0);
+
+ let local_to_world = SpaceMapper::new_with_target(
+ spatial_tree.root_reference_frame_index(),
+ surface.surface_spatial_node_index,
+ WorldRect::max_rect(),
+ spatial_tree,
+ );
+
+ let clipped = (local_to_world.map(&clipped_local.cast_unit()).unwrap() * surface.device_pixel_scale).round_out();
+ let unclipped = local_to_world.map(&unclipped_local).unwrap() * surface.device_pixel_scale;
+
+ (clipped, unclipped)
+ } else {
+ let clipped = (clipped_local.cast_unit() * surface.device_pixel_scale).round_out();
+ let unclipped = unclipped_local.cast_unit() * surface.device_pixel_scale;
+
+ (clipped, unclipped)
+ };
+
+ let task_size_f = clipped.size();
+
+ if task_size_f.width > max_surface_size || task_size_f.height > max_surface_size {
+ let max_dimension = clipped_local.width().max(clipped_local.height()).ceil();
+
+ surface.raster_spatial_node_index = surface.surface_spatial_node_index;
+ surface.device_pixel_scale = Scale::new(max_surface_size / max_dimension);
+
+ clipped = (clipped_local.cast_unit() * surface.device_pixel_scale).round();
+ unclipped = unclipped_local.cast_unit() * surface.device_pixel_scale;
+ }
+
+ let task_size = clipped.size().to_i32();
+ debug_assert!(task_size.width <= max_surface_size as i32);
+ debug_assert!(task_size.height <= max_surface_size as i32);
+
+ let uv_rect_kind = calculate_uv_rect_kind(
+ clipped,
+ unclipped,
+ );
+
+ // If the task size is zero sized, skip creation and drawing of it
+ if task_size.width == 0 || task_size.height == 0 {
+ return None;
+ }
+
+ // If the final clipped surface rect is not the same or larger as the unclipped
+ // local rect of the surface, we need to enable scissor rect (which disables
+ // merging batches between this and other render tasks allocated to the same
+ // render target). This is conservative - we could do better in future by
+ // distinguishing between clips that affect the surface itself vs. clips on
+ // child primitives that don't affect this.
+ let needs_scissor_rect = !clipped_local.contains_box(&surface.unclipped_local_rect);
+
+ Some(SurfaceAllocInfo {
+ task_size,
+ needs_scissor_rect,
+ clipped,
+ unclipped,
+ clipped_local,
+ uv_rect_kind,
+ })
+}
+
+fn calculate_uv_rect_kind(
+ clipped: DeviceRect,
+ unclipped: DeviceRect,
+) -> UvRectKind {
+ let top_left = calculate_screen_uv(
+ unclipped.top_left().cast_unit(),
+ clipped,
+ );
+
+ let top_right = calculate_screen_uv(
+ unclipped.top_right().cast_unit(),
+ clipped,
+ );
+
+ let bottom_left = calculate_screen_uv(
+ unclipped.bottom_left().cast_unit(),
+ clipped,
+ );
+
+ let bottom_right = calculate_screen_uv(
+ unclipped.bottom_right().cast_unit(),
+ clipped,
+ );
+
+ UvRectKind::Quad {
+ top_left,
+ top_right,
+ bottom_left,
+ bottom_right,
+ }
+}
+
+#[test]
+fn test_large_surface_scale_1() {
+ use crate::spatial_tree::{SceneSpatialTree, SpatialTree};
+
+ let mut cst = SceneSpatialTree::new();
+ let root_reference_frame_index = cst.root_reference_frame_index();
+
+ let mut spatial_tree = SpatialTree::new();
+ spatial_tree.apply_updates(cst.end_frame_and_get_pending_updates());
+ spatial_tree.update_tree(&SceneProperties::new());
+
+ let map_local_to_surface = SpaceMapper::new_with_target(
+ root_reference_frame_index,
+ root_reference_frame_index,
+ PictureRect::max_rect(),
+ &spatial_tree,
+ );
+
+ let mut surfaces = vec![
+ SurfaceInfo {
+ unclipped_local_rect: PictureRect::max_rect(),
+ clipped_local_rect: PictureRect::max_rect(),
+ is_opaque: true,
+ clipping_rect: PictureRect::max_rect(),
+ map_local_to_surface: map_local_to_surface.clone(),
+ raster_spatial_node_index: root_reference_frame_index,
+ surface_spatial_node_index: root_reference_frame_index,
+ device_pixel_scale: DevicePixelScale::new(1.0),
+ world_scale_factors: (1.0, 1.0),
+ local_scale: (1.0, 1.0),
+ allow_snapping: true,
+ },
+ SurfaceInfo {
+ unclipped_local_rect: PictureRect::new(
+ PicturePoint::new(52.76350021362305, 0.0),
+ PicturePoint::new(159.6738739013672, 35.0),
+ ),
+ clipped_local_rect: PictureRect::max_rect(),
+ is_opaque: true,
+ clipping_rect: PictureRect::max_rect(),
+ map_local_to_surface,
+ raster_spatial_node_index: root_reference_frame_index,
+ surface_spatial_node_index: root_reference_frame_index,
+ device_pixel_scale: DevicePixelScale::new(43.82798767089844),
+ world_scale_factors: (1.0, 1.0),
+ local_scale: (1.0, 1.0),
+ allow_snapping: true,
+ },
+ ];
+
+ get_surface_rects(
+ SurfaceIndex(1),
+ &PictureCompositeMode::Blit(BlitReason::ISOLATE),
+ SurfaceIndex(0),
+ &mut surfaces,
+ &spatial_tree,
+ MAX_SURFACE_SIZE as f32,
+ );
+}
diff --git a/gfx/wr/webrender/src/picture_graph.rs b/gfx/wr/webrender/src/picture_graph.rs
new file mode 100644
index 0000000000..1446784ca6
--- /dev/null
+++ b/gfx/wr/webrender/src/picture_graph.rs
@@ -0,0 +1,212 @@
+/* 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::frame_builder::FrameBuildingContext;
+use crate::internal_types::FastHashMap;
+use crate::prim_store::PictureIndex;
+use crate::picture::{PicturePrimitive, SurfaceIndex, SurfaceInfo};
+use crate::picture::{TileCacheInstance, SliceId};
+use smallvec::SmallVec;
+
+#[derive(Debug)]
+pub struct PictureInfo {
+ pub update_pass: Option<usize>,
+ pub surface_index: Option<SurfaceIndex>,
+ pub parent: Option<PictureIndex>,
+}
+
+/// A graph of picture dependencies, allowing updates to be processed without recursion
+/// by building a list of passes.
+pub struct PictureGraph {
+ roots: Vec<PictureIndex>,
+ pic_info: Vec<PictureInfo>,
+ update_passes: Vec<Vec<PictureIndex>>,
+}
+
+impl PictureGraph {
+ pub fn new() -> Self {
+ PictureGraph {
+ roots: Vec::new(),
+ pic_info: Vec::new(),
+ update_passes: Vec::new(),
+ }
+ }
+
+ /// Add a root picture to the graph
+ pub fn add_root(
+ &mut self,
+ pic_index: PictureIndex,
+ ) {
+ self.roots.push(pic_index);
+ }
+
+ /// Build a list of update passes based on the dependencies between pictures
+ pub fn build_update_passes(
+ &mut self,
+ pictures: &mut [PicturePrimitive],
+ frame_context: &FrameBuildingContext
+ ) {
+ self.pic_info.clear();
+ self.pic_info.reserve(pictures.len());
+
+ for _ in 0 .. pictures.len() {
+ self.pic_info.push(PictureInfo {
+ update_pass: None,
+ parent: None,
+ surface_index: None,
+ })
+ };
+
+ let mut max_pass_index = 0;
+
+ for pic_index in &self.roots {
+ assign_update_pass(
+ *pic_index,
+ None,
+ 0,
+ pictures,
+ &mut self.pic_info,
+ &mut max_pass_index,
+ frame_context,
+ );
+ }
+
+ let pass_count = max_pass_index + 1;
+
+ self.update_passes.clear();
+ self.update_passes.resize_with(pass_count, Vec::new);
+
+ for (pic_index, info) in self.pic_info.iter().enumerate() {
+ if let Some(update_pass) = info.update_pass {
+ let pass = &mut self.update_passes[update_pass];
+ pass.push(PictureIndex(pic_index));
+ }
+ }
+ }
+
+ /// Assign surfaces and scale factors to each picture (root -> leaf ordered pass)
+ pub fn assign_surfaces(
+ &mut self,
+ pictures: &mut [PicturePrimitive],
+ surfaces: &mut Vec<SurfaceInfo>,
+ tile_caches: &mut FastHashMap<SliceId, Box<TileCacheInstance>>,
+ frame_context: &FrameBuildingContext,
+ ) {
+ for pass in &self.update_passes {
+ for pic_index in pass {
+ let parent = self.pic_info[pic_index.0].parent;
+
+ let parent_surface_index = parent.map(|parent| {
+ // Can unwrap here as by the time we have a parent that parent's
+ // surface must have been assigned.
+ self.pic_info[parent.0].surface_index.unwrap()
+ });
+
+ let info = &mut self.pic_info[pic_index.0];
+
+ match pictures[pic_index.0].assign_surface(
+ frame_context,
+ parent_surface_index,
+ tile_caches,
+ surfaces,
+ ) {
+ Some(surface_index) => {
+ info.surface_index = Some(surface_index);
+ }
+ None => {
+ info.surface_index = Some(parent_surface_index.unwrap());
+ }
+ }
+ }
+ }
+ }
+
+ /// Propegate bounding rects from pictures to parents (leaf -> root ordered pass)
+ pub fn propagate_bounding_rects(
+ &mut self,
+ pictures: &mut [PicturePrimitive],
+ surfaces: &mut [SurfaceInfo],
+ frame_context: &FrameBuildingContext,
+ ) {
+ for pass in self.update_passes.iter().rev() {
+ for pic_index in pass {
+ let parent = self.pic_info[pic_index.0].parent;
+
+ let surface_index = self.pic_info[pic_index.0]
+ .surface_index
+ .expect("bug: no surface assigned during propagate_bounding_rects");
+
+ let parent_surface_index = parent.map(|parent| {
+ // Can unwrap here as by the time we have a parent that parent's
+ // surface must have been assigned.
+ self.pic_info[parent.0].surface_index.unwrap()
+ });
+
+ pictures[pic_index.0].propagate_bounding_rect(
+ surface_index,
+ parent_surface_index,
+ surfaces,
+ frame_context,
+ );
+ }
+ }
+ }
+}
+
+/// Recursive function that assigns pictures to the earliest pass possible that they
+/// can be processed in, while maintaining dependency ordering.
+fn assign_update_pass(
+ pic_index: PictureIndex,
+ parent_pic_index: Option<PictureIndex>,
+ pass: usize,
+ pictures: &mut [PicturePrimitive],
+ pic_info: &mut [PictureInfo],
+ max_pass_index: &mut usize,
+ frame_context: &FrameBuildingContext
+) {
+ let pic = &mut pictures[pic_index.0];
+ let info = &mut pic_info[pic_index.0];
+
+ info.parent = parent_pic_index;
+
+ // Run pre-update to resolve animation properties etc
+ pic.pre_update(frame_context);
+
+ let can_be_drawn = match info.update_pass {
+ Some(update_pass) => {
+ // No point in recursing into paths in the graph if this picture already
+ // has been set to update after this pass.
+ if update_pass > pass {
+ return;
+ }
+
+ true
+ }
+ None => {
+ // Check if this picture can be dropped from the graph we're building this frame
+ pic.is_visible(frame_context.spatial_tree)
+ }
+ };
+
+ if can_be_drawn {
+ info.update_pass = Some(pass);
+
+ *max_pass_index = pass.max(*max_pass_index);
+
+ let mut child_pictures: SmallVec<[PictureIndex; 8]> = SmallVec::new();
+ child_pictures.extend_from_slice(&pic.prim_list.child_pictures);
+
+ for child_pic_index in child_pictures {
+ assign_update_pass(
+ child_pic_index,
+ Some(pic_index),
+ pass + 1,
+ pictures,
+ pic_info,
+ max_pass_index,
+ frame_context,
+ );
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/picture_textures.rs b/gfx/wr/webrender/src/picture_textures.rs
new file mode 100644
index 0000000000..4345951bb9
--- /dev/null
+++ b/gfx/wr/webrender/src/picture_textures.rs
@@ -0,0 +1,382 @@
+/* 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::mem;
+use smallvec::SmallVec;
+use api::{ImageFormat, ImageBufferKind, DebugFlags};
+use api::units::*;
+use crate::device::TextureFilter;
+use crate::internal_types::{
+ CacheTextureId, TextureUpdateList, Swizzle, TextureCacheAllocInfo, TextureCacheCategory,
+ TextureSource, FrameStamp, FrameId,
+};
+use crate::profiler::{self, TransactionProfile};
+use crate::gpu_types::{ImageSource, UvRectKind};
+use crate::gpu_cache::{GpuCache, GpuCacheHandle};
+use crate::freelist::{FreeList, FreeListHandle, WeakFreeListHandle};
+
+
+#[derive(Debug, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum PictureCacheEntryMarker {}
+
+malloc_size_of::malloc_size_of_is_0!(PictureCacheEntryMarker);
+
+pub type PictureCacheTextureHandle = WeakFreeListHandle<PictureCacheEntryMarker>;
+
+use std::cmp;
+
+// Stores information related to a single entry in the texture
+// cache. This is stored for each item whether it's in the shared
+// cache or a standalone texture.
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PictureCacheEntry {
+ /// Size of the requested tile.
+ pub size: DeviceIntSize,
+ /// The last frame this item was requested for rendering.
+ // TODO(gw): This stamp is only used for picture cache tiles, and some checks
+ // in the glyph cache eviction code. We could probably remove it
+ // entirely in future (or move to EntryDetails::Picture).
+ pub last_access: FrameStamp,
+ /// Handle to the resource rect in the GPU cache.
+ pub uv_rect_handle: GpuCacheHandle,
+ /// Image format of the data that the entry expects.
+ pub filter: TextureFilter,
+ /// The actual device texture ID this is part of.
+ pub texture_id: CacheTextureId,
+}
+
+impl PictureCacheEntry {
+ fn update_gpu_cache(&mut self, gpu_cache: &mut GpuCache) {
+ if let Some(mut request) = gpu_cache.request(&mut self.uv_rect_handle) {
+ let origin = DeviceIntPoint::zero();
+ let image_source = ImageSource {
+ p0: origin.to_f32(),
+ p1: (origin + self.size).to_f32(),
+ uv_rect_kind: UvRectKind::Rect,
+ user_data: [0.0; 4],
+ };
+ image_source.write_gpu_blocks(&mut request);
+ }
+ }
+}
+
+/// The textures used to hold picture cache tiles.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct PictureTexture {
+ texture_id: CacheTextureId,
+ size: DeviceIntSize,
+ is_allocated: bool,
+ last_frame_used: FrameId,
+}
+
+/// The textures used to hold picture cache tiles.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PictureTextures {
+ /// Current list of textures in the pool
+ textures: Vec<PictureTexture>,
+ /// Default tile size for content tiles
+ default_tile_size: DeviceIntSize,
+ /// Number of currently allocated textures in the pool
+ allocated_texture_count: usize,
+ /// Texture filter to use for picture cache textures
+ filter: TextureFilter,
+
+ debug_flags: DebugFlags,
+
+ /// Cache of picture cache entries.
+ cache_entries: FreeList<PictureCacheEntry, PictureCacheEntryMarker>,
+ /// Strong handles for the picture_cache_entries FreeList.
+ cache_handles: Vec<FreeListHandle<PictureCacheEntryMarker>>,
+
+ now: FrameStamp,
+}
+
+impl PictureTextures {
+ pub fn new(
+ default_tile_size: DeviceIntSize,
+ filter: TextureFilter,
+ ) -> Self {
+ PictureTextures {
+ textures: Vec::new(),
+ default_tile_size,
+ allocated_texture_count: 0,
+ filter,
+ debug_flags: DebugFlags::empty(),
+ cache_entries: FreeList::new(),
+ cache_handles: Vec::new(),
+ now: FrameStamp::INVALID,
+ }
+ }
+
+ pub fn begin_frame(&mut self, stamp: FrameStamp, pending_updates: &mut TextureUpdateList) {
+ self.now = stamp;
+
+ // Expire picture cache tiles that haven't been referenced in the last frame.
+ // The picture cache code manually keeps tiles alive by calling `request` on
+ // them if it wants to retain a tile that is currently not visible.
+ self.expire_old_tiles(pending_updates);
+ }
+
+ pub fn default_tile_size(&self) -> DeviceIntSize {
+ self.default_tile_size
+ }
+
+ pub fn update(
+ &mut self,
+ tile_size: DeviceIntSize,
+ handle: &mut Option<PictureCacheTextureHandle>,
+ gpu_cache: &mut GpuCache,
+ next_texture_id: &mut CacheTextureId,
+ pending_updates: &mut TextureUpdateList,
+ ) {
+ debug_assert!(self.now.is_valid());
+ debug_assert!(tile_size.width > 0 && tile_size.height > 0);
+
+ let need_alloc = match handle {
+ None => true,
+ Some(handle) => {
+ // Check if the entry has been evicted.
+ !self.entry_exists(&handle)
+ },
+ };
+
+ if need_alloc {
+ let new_handle = self.get_or_allocate_tile(
+ tile_size,
+ next_texture_id,
+ pending_updates,
+ );
+
+ *handle = Some(new_handle);
+ }
+
+ if let Some(handle) = handle {
+ // Upload the resource rect and texture array layer.
+ self.cache_entries
+ .get_opt_mut(handle)
+ .expect("BUG: handle must be valid now")
+ .update_gpu_cache(gpu_cache);
+ } else {
+ panic!("The handle should be valid picture cache handle now")
+ }
+ }
+
+ pub fn get_or_allocate_tile(
+ &mut self,
+ tile_size: DeviceIntSize,
+ next_texture_id: &mut CacheTextureId,
+ pending_updates: &mut TextureUpdateList,
+ ) -> PictureCacheTextureHandle {
+ let mut texture_id = None;
+ self.allocated_texture_count += 1;
+
+ for texture in &mut self.textures {
+ if texture.size == tile_size && !texture.is_allocated {
+ // Found a target that's not currently in use which matches. Update
+ // the last_frame_used for GC purposes.
+ texture.is_allocated = true;
+ texture.last_frame_used = FrameId::INVALID;
+ texture_id = Some(texture.texture_id);
+ break;
+ }
+ }
+
+ // Need to create a new render target and add it to the pool
+
+ let texture_id = texture_id.unwrap_or_else(|| {
+ let texture_id = *next_texture_id;
+ next_texture_id.0 += 1;
+
+ // Push a command to allocate device storage of the right size / format.
+ let info = TextureCacheAllocInfo {
+ target: ImageBufferKind::Texture2D,
+ width: tile_size.width,
+ height: tile_size.height,
+ format: ImageFormat::RGBA8,
+ filter: self.filter,
+ is_shared_cache: false,
+ has_depth: true,
+ category: TextureCacheCategory::PictureTile,
+ };
+
+ pending_updates.push_alloc(texture_id, info);
+
+ self.textures.push(PictureTexture {
+ texture_id,
+ is_allocated: true,
+ size: tile_size,
+ last_frame_used: FrameId::INVALID,
+ });
+
+ texture_id
+ });
+
+ let cache_entry = PictureCacheEntry {
+ size: tile_size,
+ last_access: self.now,
+ uv_rect_handle: GpuCacheHandle::new(),
+ filter: self.filter,
+ texture_id,
+ };
+
+ // Add the cache entry to the picture_textures.cache_entries FreeList.
+ let strong_handle = self.cache_entries.insert(cache_entry);
+ let new_handle = strong_handle.weak();
+
+ self.cache_handles.push(strong_handle);
+
+ new_handle
+ }
+
+ pub fn free_tile(
+ &mut self,
+ id: CacheTextureId,
+ current_frame_id: FrameId,
+ pending_updates: &mut TextureUpdateList,
+ ) {
+ self.allocated_texture_count -= 1;
+
+ let texture = self.textures
+ .iter_mut()
+ .find(|t| t.texture_id == id)
+ .expect("bug: invalid texture id");
+
+ assert!(texture.is_allocated);
+ texture.is_allocated = false;
+
+ assert_eq!(texture.last_frame_used, FrameId::INVALID);
+ texture.last_frame_used = current_frame_id;
+
+ if self.debug_flags.contains(
+ DebugFlags::TEXTURE_CACHE_DBG |
+ DebugFlags::TEXTURE_CACHE_DBG_CLEAR_EVICTED)
+ {
+ pending_updates.push_debug_clear(
+ id,
+ DeviceIntPoint::zero(),
+ texture.size.width,
+ texture.size.height,
+ );
+ }
+ }
+
+ pub fn request(&mut self, handle: &PictureCacheTextureHandle, gpu_cache: &mut GpuCache) -> bool {
+ let entry = self.cache_entries.get_opt_mut(handle);
+ let now = self.now;
+ entry.map_or(true, |entry| {
+ // If an image is requested that is already in the cache,
+ // refresh the GPU cache data associated with this item.
+ entry.last_access = now;
+ entry.update_gpu_cache(gpu_cache);
+ false
+ })
+ }
+
+ pub fn get_texture_source(&self, handle: &PictureCacheTextureHandle) -> TextureSource {
+ let entry = self.cache_entries.get_opt(handle)
+ .expect("BUG: was dropped from cache or not updated!");
+
+ debug_assert_eq!(entry.last_access, self.now);
+
+ TextureSource::TextureCache(entry.texture_id, Swizzle::default())
+ }
+
+ /// Expire picture cache tiles that haven't been referenced in the last frame.
+ /// The picture cache code manually keeps tiles alive by calling `request` on
+ /// them if it wants to retain a tile that is currently not visible.
+ pub fn expire_old_tiles(&mut self, pending_updates: &mut TextureUpdateList) {
+ for i in (0 .. self.cache_handles.len()).rev() {
+ let evict = {
+ let entry = self.cache_entries.get(
+ &self.cache_handles[i]
+ );
+
+ // This function is called at the beginning of the frame,
+ // so we don't yet know which picture cache tiles will be
+ // requested this frame. Therefore only evict picture cache
+ // tiles which weren't requested in the *previous* frame.
+ entry.last_access.frame_id() < self.now.frame_id() - 1
+ };
+
+ if evict {
+ let handle = self.cache_handles.swap_remove(i);
+ let entry = self.cache_entries.free(handle);
+ self.free_tile(entry.texture_id, self.now.frame_id(), pending_updates);
+ }
+ }
+ }
+
+ pub fn clear(&mut self, pending_updates: &mut TextureUpdateList) {
+ for handle in mem::take(&mut self.cache_handles) {
+ let entry = self.cache_entries.free(handle);
+ self.free_tile(entry.texture_id, self.now.frame_id(), pending_updates);
+ }
+
+ for texture in self.textures.drain(..) {
+ pending_updates.push_free(texture.texture_id);
+ }
+ }
+
+ pub fn update_profile(&self, profile: &mut TransactionProfile) {
+ profile.set(profiler::PICTURE_TILES, self.textures.len());
+ }
+
+ /// Simple garbage collect of picture cache tiles
+ pub fn gc(
+ &mut self,
+ pending_updates: &mut TextureUpdateList,
+ ) {
+ // Allow the picture cache pool to keep 25% of the current allocated tile count
+ // as free textures to be reused. This ensures the allowed tile count is appropriate
+ // based on current window size.
+ let free_texture_count = self.textures.len() - self.allocated_texture_count;
+ let allowed_retained_count = (self.allocated_texture_count as f32 * 0.25).ceil() as usize;
+ let do_gc = free_texture_count > allowed_retained_count;
+
+ if do_gc {
+ // Sort the current pool by age, so that we remove oldest textures first
+ self.textures.sort_unstable_by_key(|t| cmp::Reverse(t.last_frame_used));
+
+ // We can't just use retain() because `PictureTexture` requires manual cleanup.
+ let mut allocated_targets = SmallVec::<[PictureTexture; 32]>::new();
+ let mut retained_targets = SmallVec::<[PictureTexture; 32]>::new();
+
+ for target in self.textures.drain(..) {
+ if target.is_allocated {
+ // Allocated targets can't be collected
+ allocated_targets.push(target);
+ } else if retained_targets.len() < allowed_retained_count {
+ // Retain the most recently used targets up to the allowed count
+ retained_targets.push(target);
+ } else {
+ // The rest of the targets get freed
+ assert_ne!(target.last_frame_used, FrameId::INVALID);
+ pending_updates.push_free(target.texture_id);
+ }
+ }
+
+ self.textures.extend(retained_targets);
+ self.textures.extend(allocated_targets);
+ }
+ }
+
+ pub fn entry_exists(&self, handle: &PictureCacheTextureHandle) -> bool {
+ self.cache_entries.get_opt(handle).is_some()
+ }
+
+ pub fn set_debug_flags(&mut self, flags: DebugFlags) {
+ self.debug_flags = flags;
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn filter(&self) -> TextureFilter {
+ self.filter
+ }
+}
diff --git a/gfx/wr/webrender/src/prepare.rs b/gfx/wr/webrender/src/prepare.rs
new file mode 100644
index 0000000000..ad718de7aa
--- /dev/null
+++ b/gfx/wr/webrender/src/prepare.rs
@@ -0,0 +1,2035 @@
+/* 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/. */
+
+//! # Prepare pass
+//!
+//! TODO: document this!
+
+use api::{ColorF, PremultipliedColorF, PropertyBinding};
+use api::{BoxShadowClipMode, BorderStyle, ClipMode};
+use api::units::*;
+use euclid::Scale;
+use smallvec::SmallVec;
+use crate::command_buffer::{PrimitiveCommand, QuadFlags, CommandBufferIndex};
+use crate::image_tiling::{self, Repetition};
+use crate::border::{get_max_scale_for_border, build_border_instances};
+use crate::clip::{ClipStore};
+use crate::spatial_tree::{SpatialNodeIndex, SpatialTree};
+use crate::clip::{ClipDataStore, ClipNodeFlags, ClipChainInstance, ClipItemKind};
+use crate::frame_builder::{FrameBuildingContext, FrameBuildingState, PictureContext, PictureState};
+use crate::gpu_cache::{GpuCacheHandle, GpuDataRequest};
+use crate::gpu_types::{BrushFlags, TransformPaletteId, QuadSegment};
+use crate::internal_types::{FastHashMap, PlaneSplitAnchor};
+use crate::picture::{PicturePrimitive, SliceId, ClusterFlags};
+use crate::picture::{PrimitiveList, PrimitiveCluster, SurfaceIndex, TileCacheInstance, SubpixelMode, Picture3DContext};
+use crate::prim_store::line_dec::MAX_LINE_DECORATION_RESOLUTION;
+use crate::prim_store::*;
+use crate::prim_store::gradient::GradientGpuBlockBuilder;
+use crate::render_backend::DataStores;
+use crate::render_task_graph::{RenderTaskId};
+use crate::render_task_cache::RenderTaskCacheKeyKind;
+use crate::render_task_cache::{RenderTaskCacheKey, to_cache_size, RenderTaskParent};
+use crate::render_task::{RenderTaskKind, RenderTask};
+use crate::renderer::{GpuBufferBuilder, GpuBufferAddress};
+use crate::segment::{EdgeAaSegmentMask, SegmentBuilder};
+use crate::util::{clamp_to_scale_factor, pack_as_float, MaxRect};
+use crate::visibility::{compute_conservative_visible_rect, PrimitiveVisibility, VisibilityState};
+
+
+const MAX_MASK_SIZE: f32 = 4096.0;
+
+const MIN_BRUSH_SPLIT_AREA: f32 = 128.0 * 128.0;
+
+pub fn prepare_primitives(
+ store: &mut PrimitiveStore,
+ prim_list: &mut PrimitiveList,
+ pic_context: &PictureContext,
+ pic_state: &mut PictureState,
+ frame_context: &FrameBuildingContext,
+ frame_state: &mut FrameBuildingState,
+ data_stores: &mut DataStores,
+ scratch: &mut PrimitiveScratchBuffer,
+ tile_caches: &mut FastHashMap<SliceId, Box<TileCacheInstance>>,
+ prim_instances: &mut Vec<PrimitiveInstance>,
+) {
+ profile_scope!("prepare_primitives");
+ let mut cmd_buffer_targets = Vec::new();
+
+ for cluster in &mut prim_list.clusters {
+ if !cluster.flags.contains(ClusterFlags::IS_VISIBLE) {
+ continue;
+ }
+ profile_scope!("cluster");
+ pic_state.map_local_to_pic.set_target_spatial_node(
+ cluster.spatial_node_index,
+ frame_context.spatial_tree,
+ );
+
+ for prim_instance_index in cluster.prim_range() {
+ if frame_state.surface_builder.get_cmd_buffer_targets_for_prim(
+ &prim_instances[prim_instance_index].vis,
+ &mut cmd_buffer_targets,
+ ) {
+ let plane_split_anchor = PlaneSplitAnchor::new(
+ cluster.spatial_node_index,
+ PrimitiveInstanceIndex(prim_instance_index as u32),
+ );
+
+ prepare_prim_for_render(
+ store,
+ prim_instance_index,
+ cluster,
+ pic_context,
+ pic_state,
+ frame_context,
+ frame_state,
+ plane_split_anchor,
+ data_stores,
+ scratch,
+ tile_caches,
+ prim_instances,
+ &cmd_buffer_targets,
+ );
+
+ frame_state.num_visible_primitives += 1;
+ continue;
+ }
+
+ // TODO(gw): Technically no need to clear visibility here, since from this point it
+ // only matters if it got added to a command buffer. Kept here for now to
+ // make debugging simpler, but perhaps we can remove / tidy this up.
+ prim_instances[prim_instance_index].clear_visibility();
+ }
+ }
+}
+
+fn can_use_clip_chain_for_quad_path(
+ clip_chain: &ClipChainInstance,
+ prim_spatial_node_index: SpatialNodeIndex,
+ raster_spatial_node_index: SpatialNodeIndex,
+ _clip_store: &ClipStore,
+ _data_stores: &DataStores,
+ spatial_tree: &SpatialTree,
+) -> bool {
+ let map_prim_to_surface = spatial_tree.get_relative_transform(
+ prim_spatial_node_index,
+ raster_spatial_node_index,
+ );
+ if !map_prim_to_surface.is_2d_axis_aligned() {
+ return false;
+ }
+ if map_prim_to_surface.is_perspective() {
+ return false;
+ }
+
+ !clip_chain.needs_mask
+
+ // TODO(gw): Temporarily disable the new clip-mask rendering path for now, while
+ // investigating a driver-specific shader optimization regression.
+
+ /*
+ if !clip_chain.needs_mask {
+ return true;
+ }
+
+ for i in 0 .. clip_chain.clips_range.count {
+ let clip_instance = clip_store.get_instance_from_range(&clip_chain.clips_range, i);
+ let clip_node = &data_stores.clip[clip_instance.handle];
+
+ // Temporary hack only for landing next stage...
+ if prim_spatial_node_index != clip_node.item.spatial_node_index {
+ return false;
+ }
+
+ match clip_node.item.kind {
+ ClipItemKind::RoundedRectangle { .. } => {
+ }
+ ClipItemKind::Rectangle { mode: ClipMode::ClipOut, .. } => {
+ }
+ ClipItemKind::Rectangle { mode: ClipMode::Clip, .. } => {
+ panic!("bug: xf rect found as mask, not in lcr?");
+ }
+ ClipItemKind::BoxShadow { .. } => {
+ // legacy path for box-shadows for now (move them to a separate primitive next)
+ return false;
+ }
+ ClipItemKind::Image { .. } => {
+ panic!("bug: image-masks not expected on rect/quads");
+ }
+ }
+ }
+
+ true
+ */
+}
+
+#[derive(Debug, Copy, Clone)]
+pub enum QuadRenderMode {
+ Direct,
+ Indirect,
+ NinePatch {
+ radius: f32,
+ clip_rect: LayoutRect,
+ clip_out: bool,
+ },
+}
+
+#[derive(Debug)]
+pub struct QuadRenderStrategy {
+ pub render_mode: QuadRenderMode,
+}
+
+fn get_prim_render_strategy(
+ clip_chain: &ClipChainInstance,
+ clip_store: &ClipStore,
+ data_stores: &DataStores,
+) -> QuadRenderStrategy {
+ let mut render_mode = QuadRenderMode::Direct;
+
+ if clip_chain.needs_mask {
+ for i in 0 .. clip_chain.clips_range.count {
+ let clip_instance = clip_store.get_instance_from_range(&clip_chain.clips_range, i);
+ let clip_node = &data_stores.clip[clip_instance.handle];
+
+ match clip_node.item.kind {
+ ClipItemKind::RoundedRectangle { ref radius, mode, rect, .. } => {
+ match render_mode {
+ QuadRenderMode::Direct => {
+ let max_corner_width = radius.top_left.width
+ .max(radius.bottom_left.width)
+ .max(radius.top_right.width)
+ .max(radius.bottom_right.width);
+ let max_corner_height = radius.top_left.height
+ .max(radius.bottom_left.height)
+ .max(radius.top_right.height)
+ .max(radius.bottom_right.height);
+
+ if clip_chain.pic_coverage_rect.area() > MIN_BRUSH_SPLIT_AREA &&
+ max_corner_width <= 0.5 * rect.size().width &&
+ max_corner_height <= 0.5 * rect.size().height {
+
+ let radius = max_corner_width.max(max_corner_height);
+
+ render_mode = QuadRenderMode::NinePatch {
+ radius,
+ clip_rect: rect,
+ clip_out: mode == ClipMode::ClipOut,
+ };
+ } else {
+ render_mode = QuadRenderMode::Indirect;
+ }
+ }
+ QuadRenderMode::NinePatch { .. } => {
+ render_mode = QuadRenderMode::Indirect;
+ }
+ QuadRenderMode::Indirect { .. } => {
+
+ }
+ }
+ }
+ ClipItemKind::Rectangle { mode: ClipMode::ClipOut, .. } => {
+ render_mode = QuadRenderMode::Indirect;
+ }
+ ClipItemKind::Rectangle { mode: ClipMode::Clip, .. } => {
+ panic!("bug: xf rects should not be on new clip path yet");
+ }
+ ClipItemKind::BoxShadow { .. } => {
+ panic!("bug: box-shadow clips not expected on non-legacy rect/quads");
+ }
+ ClipItemKind::Image { .. } => {
+ panic!("bug: image-masks not expected on rect/quads");
+ }
+ }
+ }
+ }
+
+ QuadRenderStrategy {
+ render_mode,
+ }
+}
+
+fn prepare_prim_for_render(
+ store: &mut PrimitiveStore,
+ prim_instance_index: usize,
+ cluster: &mut PrimitiveCluster,
+ pic_context: &PictureContext,
+ pic_state: &mut PictureState,
+ frame_context: &FrameBuildingContext,
+ frame_state: &mut FrameBuildingState,
+ plane_split_anchor: PlaneSplitAnchor,
+ data_stores: &mut DataStores,
+ scratch: &mut PrimitiveScratchBuffer,
+ tile_caches: &mut FastHashMap<SliceId, Box<TileCacheInstance>>,
+ prim_instances: &mut Vec<PrimitiveInstance>,
+ targets: &[CommandBufferIndex],
+) {
+ profile_scope!("prepare_prim_for_render");
+
+ // If we have dependencies, we need to prepare them first, in order
+ // to know the actual rect of this primitive.
+ // For example, scrolling may affect the location of an item in
+ // local space, which may force us to render this item on a larger
+ // picture target, if being composited.
+ let mut is_passthrough = false;
+ if let PrimitiveInstanceKind::Picture { pic_index, .. } = prim_instances[prim_instance_index].kind {
+ let pic = &mut store.pictures[pic_index.0];
+
+ // TODO(gw): Plan to remove pictures with no composite mode, so that we don't need
+ // to special case for pass through pictures.
+ is_passthrough = pic.composite_mode.is_none();
+
+ match pic.take_context(
+ pic_index,
+ Some(pic_context.surface_index),
+ pic_context.subpixel_mode,
+ frame_state,
+ frame_context,
+ scratch,
+ tile_caches,
+ ) {
+ Some((pic_context_for_children, mut pic_state_for_children, mut prim_list)) => {
+ prepare_primitives(
+ store,
+ &mut prim_list,
+ &pic_context_for_children,
+ &mut pic_state_for_children,
+ frame_context,
+ frame_state,
+ data_stores,
+ scratch,
+ tile_caches,
+ prim_instances,
+ );
+
+ // Restore the dependencies (borrow check dance)
+ store.pictures[pic_context_for_children.pic_index.0]
+ .restore_context(
+ pic_context_for_children.pic_index,
+ prim_list,
+ pic_context_for_children,
+ prim_instances,
+ frame_context,
+ frame_state,
+ );
+ }
+ None => {
+ return;
+ }
+ }
+ }
+
+ let prim_instance = &mut prim_instances[prim_instance_index];
+
+ if !is_passthrough {
+
+ // In this initial patch, we only support non-masked primitives through the new
+ // quad rendering path. Follow up patches will extend this to support masks, and
+ // then use by other primitives. In the new quad rendering path, we'll still want
+ // to skip the entry point to `update_clip_task` as that does old-style segmenting
+ // and mask generation.
+ let should_update_clip_task = match prim_instance.kind {
+ PrimitiveInstanceKind::Rectangle { ref mut use_legacy_path, .. } => {
+ *use_legacy_path = !can_use_clip_chain_for_quad_path(
+ &prim_instance.vis.clip_chain,
+ cluster.spatial_node_index,
+ pic_context.raster_spatial_node_index,
+ frame_state.clip_store,
+ data_stores,
+ frame_context.spatial_tree,
+ );
+
+ *use_legacy_path
+ }
+ _ => true,
+ };
+
+ if should_update_clip_task {
+ let prim_rect = data_stores.get_local_prim_rect(
+ prim_instance,
+ &store.pictures,
+ frame_state.surfaces,
+ );
+
+ if !update_clip_task(
+ prim_instance,
+ &prim_rect.min,
+ cluster.spatial_node_index,
+ pic_context.raster_spatial_node_index,
+ pic_context,
+ pic_state,
+ frame_context,
+ frame_state,
+ store,
+ data_stores,
+ scratch,
+ ) {
+ return;
+ }
+ }
+ }
+
+ prepare_interned_prim_for_render(
+ store,
+ PrimitiveInstanceIndex(prim_instance_index as u32),
+ prim_instance,
+ cluster,
+ plane_split_anchor,
+ pic_context,
+ pic_state,
+ frame_context,
+ frame_state,
+ data_stores,
+ scratch,
+ targets,
+ )
+}
+
+/// Prepare an interned primitive for rendering, by requesting
+/// resources, render tasks etc. This is equivalent to the
+/// prepare_prim_for_render_inner call for old style primitives.
+fn prepare_interned_prim_for_render(
+ store: &mut PrimitiveStore,
+ prim_instance_index: PrimitiveInstanceIndex,
+ prim_instance: &mut PrimitiveInstance,
+ cluster: &mut PrimitiveCluster,
+ plane_split_anchor: PlaneSplitAnchor,
+ pic_context: &PictureContext,
+ pic_state: &mut PictureState,
+ frame_context: &FrameBuildingContext,
+ frame_state: &mut FrameBuildingState,
+ data_stores: &mut DataStores,
+ scratch: &mut PrimitiveScratchBuffer,
+ targets: &[CommandBufferIndex],
+) {
+ let prim_spatial_node_index = cluster.spatial_node_index;
+ let device_pixel_scale = frame_state.surfaces[pic_context.surface_index.0].device_pixel_scale;
+
+ match &mut prim_instance.kind {
+ PrimitiveInstanceKind::LineDecoration { data_handle, ref mut render_task, .. } => {
+ profile_scope!("LineDecoration");
+ let prim_data = &mut data_stores.line_decoration[*data_handle];
+ let common_data = &mut prim_data.common;
+ let line_dec_data = &mut prim_data.kind;
+
+ // Update the template this instane references, which may refresh the GPU
+ // cache with any shared template data.
+ line_dec_data.update(common_data, frame_state);
+
+ // Work out the device pixel size to be used to cache this line decoration.
+
+ // If we have a cache key, it's a wavy / dashed / dotted line. Otherwise, it's
+ // a simple solid line.
+ if let Some(cache_key) = line_dec_data.cache_key.as_ref() {
+ // TODO(gw): These scale factors don't do a great job if the world transform
+ // contains perspective
+ let scale = frame_context
+ .spatial_tree
+ .get_world_transform(prim_spatial_node_index)
+ .scale_factors();
+
+ // Scale factors are normalized to a power of 2 to reduce the number of
+ // resolution changes.
+ // For frames with a changing scale transform round scale factors up to
+ // nearest power-of-2 boundary so that we don't keep having to redraw
+ // the content as it scales up and down. Rounding up to nearest
+ // power-of-2 boundary ensures we never scale up, only down --- avoiding
+ // jaggies. It also ensures we never scale down by more than a factor of
+ // 2, avoiding bad downscaling quality.
+ let scale_width = clamp_to_scale_factor(scale.0, false);
+ let scale_height = clamp_to_scale_factor(scale.1, false);
+ // Pick the maximum dimension as scale
+ let world_scale = LayoutToWorldScale::new(scale_width.max(scale_height));
+
+ let scale_factor = world_scale * Scale::new(1.0);
+ let task_size_f = (LayoutSize::from_au(cache_key.size) * scale_factor).ceil();
+ let mut task_size = if task_size_f.width > MAX_LINE_DECORATION_RESOLUTION as f32 ||
+ task_size_f.height > MAX_LINE_DECORATION_RESOLUTION as f32 {
+ let max_extent = task_size_f.width.max(task_size_f.height);
+ let task_scale_factor = Scale::new(MAX_LINE_DECORATION_RESOLUTION as f32 / max_extent);
+ let task_size = (LayoutSize::from_au(cache_key.size) * scale_factor * task_scale_factor)
+ .ceil().to_i32();
+ task_size
+ } else {
+ task_size_f.to_i32()
+ };
+
+ // It's plausible, due to float accuracy issues that the line decoration may be considered
+ // visible even if the scale factors are ~0. However, the render task allocation below requires
+ // that the size of the task is > 0. To work around this, ensure that the task size is at least
+ // 1x1 pixels
+ task_size.width = task_size.width.max(1);
+ task_size.height = task_size.height.max(1);
+
+ // Request a pre-rendered image task.
+ // TODO(gw): This match is a bit untidy, but it should disappear completely
+ // once the prepare_prims and batching are unified. When that
+ // happens, we can use the cache handle immediately, and not need
+ // to temporarily store it in the primitive instance.
+ *render_task = Some(frame_state.resource_cache.request_render_task(
+ RenderTaskCacheKey {
+ size: task_size,
+ kind: RenderTaskCacheKeyKind::LineDecoration(cache_key.clone()),
+ },
+ frame_state.gpu_cache,
+ frame_state.frame_gpu_data,
+ frame_state.rg_builder,
+ None,
+ false,
+ RenderTaskParent::Surface(pic_context.surface_index),
+ &mut frame_state.surface_builder,
+ |rg_builder, _| {
+ rg_builder.add().init(RenderTask::new_dynamic(
+ task_size,
+ RenderTaskKind::new_line_decoration(
+ cache_key.style,
+ cache_key.orientation,
+ cache_key.wavy_line_thickness.to_f32_px(),
+ LayoutSize::from_au(cache_key.size),
+ ),
+ ))
+ }
+ ));
+ }
+ }
+ PrimitiveInstanceKind::TextRun { run_index, data_handle, .. } => {
+ profile_scope!("TextRun");
+ let prim_data = &mut data_stores.text_run[*data_handle];
+ let run = &mut store.text_runs[*run_index];
+
+ prim_data.common.may_need_repetition = false;
+
+ // The glyph transform has to match `glyph_transform` in "ps_text_run" shader.
+ // It's relative to the rasterizing space of a glyph.
+ let transform = frame_context.spatial_tree
+ .get_relative_transform(
+ prim_spatial_node_index,
+ pic_context.raster_spatial_node_index,
+ )
+ .into_fast_transform();
+ let prim_offset = prim_data.common.prim_rect.min.to_vector() - run.reference_frame_relative_offset;
+
+ let surface = &frame_state.surfaces[pic_context.surface_index.0];
+
+ // If subpixel AA is disabled due to the backing surface the glyphs
+ // are being drawn onto, disable it (unless we are using the
+ // specifial subpixel mode that estimates background color).
+ let allow_subpixel = match prim_instance.vis.state {
+ VisibilityState::Culled |
+ VisibilityState::Unset |
+ VisibilityState::PassThrough => {
+ panic!("bug: invalid visibility state");
+ }
+ VisibilityState::Visible { sub_slice_index, .. } => {
+ // For now, we only allow subpixel AA on primary sub-slices. In future we
+ // may support other sub-slices if we find content that does this.
+ if sub_slice_index.is_primary() {
+ match pic_context.subpixel_mode {
+ SubpixelMode::Allow => true,
+ SubpixelMode::Deny => false,
+ SubpixelMode::Conditional { allowed_rect } => {
+ // Conditional mode allows subpixel AA to be enabled for this
+ // text run, so long as it's inside the allowed rect.
+ allowed_rect.contains_box(&prim_instance.vis.clip_chain.pic_coverage_rect)
+ }
+ }
+ } else {
+ false
+ }
+ }
+ };
+
+ run.request_resources(
+ prim_offset,
+ &prim_data.font,
+ &prim_data.glyphs,
+ &transform.to_transform().with_destination::<_>(),
+ surface,
+ prim_spatial_node_index,
+ allow_subpixel,
+ frame_context.fb_config.low_quality_pinch_zoom,
+ frame_state.resource_cache,
+ frame_state.gpu_cache,
+ frame_context.spatial_tree,
+ scratch,
+ );
+
+ // Update the template this instane references, which may refresh the GPU
+ // cache with any shared template data.
+ prim_data.update(frame_state);
+ }
+ PrimitiveInstanceKind::Clear { data_handle, .. } => {
+ profile_scope!("Clear");
+ let prim_data = &mut data_stores.prim[*data_handle];
+
+ prim_data.common.may_need_repetition = false;
+
+ // Update the template this instane references, which may refresh the GPU
+ // cache with any shared template data.
+ prim_data.update(frame_state, frame_context.scene_properties);
+ }
+ PrimitiveInstanceKind::NormalBorder { data_handle, ref mut render_task_ids, .. } => {
+ profile_scope!("NormalBorder");
+ let prim_data = &mut data_stores.normal_border[*data_handle];
+ let common_data = &mut prim_data.common;
+ let border_data = &mut prim_data.kind;
+
+ common_data.may_need_repetition =
+ matches!(border_data.border.top.style, BorderStyle::Dotted | BorderStyle::Dashed) ||
+ matches!(border_data.border.right.style, BorderStyle::Dotted | BorderStyle::Dashed) ||
+ matches!(border_data.border.bottom.style, BorderStyle::Dotted | BorderStyle::Dashed) ||
+ matches!(border_data.border.left.style, BorderStyle::Dotted | BorderStyle::Dashed);
+
+
+ // Update the template this instance references, which may refresh the GPU
+ // cache with any shared template data.
+ border_data.update(common_data, frame_state);
+
+ // TODO(gw): For now, the scale factors to rasterize borders at are
+ // based on the true world transform of the primitive. When
+ // raster roots with local scale are supported in future,
+ // that will need to be accounted for here.
+ let scale = frame_context
+ .spatial_tree
+ .get_world_transform(prim_spatial_node_index)
+ .scale_factors();
+
+ // Scale factors are normalized to a power of 2 to reduce the number of
+ // resolution changes.
+ // For frames with a changing scale transform round scale factors up to
+ // nearest power-of-2 boundary so that we don't keep having to redraw
+ // the content as it scales up and down. Rounding up to nearest
+ // power-of-2 boundary ensures we never scale up, only down --- avoiding
+ // jaggies. It also ensures we never scale down by more than a factor of
+ // 2, avoiding bad downscaling quality.
+ let scale_width = clamp_to_scale_factor(scale.0, false);
+ let scale_height = clamp_to_scale_factor(scale.1, false);
+ // Pick the maximum dimension as scale
+ let world_scale = LayoutToWorldScale::new(scale_width.max(scale_height));
+ let mut scale = world_scale * device_pixel_scale;
+ let max_scale = get_max_scale_for_border(border_data);
+ scale.0 = scale.0.min(max_scale.0);
+
+ // For each edge and corner, request the render task by content key
+ // from the render task cache. This ensures that the render task for
+ // this segment will be available for batching later in the frame.
+ let mut handles: SmallVec<[RenderTaskId; 8]> = SmallVec::new();
+
+ for segment in &border_data.border_segments {
+ // Update the cache key device size based on requested scale.
+ let cache_size = to_cache_size(segment.local_task_size, &mut scale);
+ let cache_key = RenderTaskCacheKey {
+ kind: RenderTaskCacheKeyKind::BorderSegment(segment.cache_key.clone()),
+ size: cache_size,
+ };
+
+ handles.push(frame_state.resource_cache.request_render_task(
+ cache_key,
+ frame_state.gpu_cache,
+ frame_state.frame_gpu_data,
+ frame_state.rg_builder,
+ None,
+ false, // TODO(gw): We don't calculate opacity for borders yet!
+ RenderTaskParent::Surface(pic_context.surface_index),
+ &mut frame_state.surface_builder,
+ |rg_builder, _| {
+ rg_builder.add().init(RenderTask::new_dynamic(
+ cache_size,
+ RenderTaskKind::new_border_segment(
+ build_border_instances(
+ &segment.cache_key,
+ cache_size,
+ &border_data.border,
+ scale,
+ )
+ ),
+ ))
+ }
+ ));
+ }
+
+ *render_task_ids = scratch
+ .border_cache_handles
+ .extend(handles);
+ }
+ PrimitiveInstanceKind::ImageBorder { data_handle, .. } => {
+ profile_scope!("ImageBorder");
+ let prim_data = &mut data_stores.image_border[*data_handle];
+
+ // TODO: get access to the ninepatch and to check whether we need support
+ // for repetitions in the shader.
+
+ // Update the template this instance references, which may refresh the GPU
+ // cache with any shared template data.
+ prim_data.kind.update(
+ &mut prim_data.common,
+ frame_state
+ );
+ }
+ PrimitiveInstanceKind::Rectangle { data_handle, segment_instance_index, color_binding_index, use_legacy_path, .. } => {
+ profile_scope!("Rectangle");
+
+ if *use_legacy_path {
+ let prim_data = &mut data_stores.prim[*data_handle];
+ prim_data.common.may_need_repetition = false;
+
+ // TODO(gw): Legacy rect rendering path - remove once we support masks on quad prims
+ if *color_binding_index != ColorBindingIndex::INVALID {
+ match store.color_bindings[*color_binding_index] {
+ PropertyBinding::Binding(..) => {
+ // We explicitly invalidate the gpu cache
+ // if the color is animating.
+ let gpu_cache_handle =
+ if *segment_instance_index == SegmentInstanceIndex::INVALID {
+ None
+ } else if *segment_instance_index == SegmentInstanceIndex::UNUSED {
+ Some(&prim_data.common.gpu_cache_handle)
+ } else {
+ Some(&scratch.segment_instances[*segment_instance_index].gpu_cache_handle)
+ };
+ if let Some(gpu_cache_handle) = gpu_cache_handle {
+ frame_state.gpu_cache.invalidate(gpu_cache_handle);
+ }
+ }
+ PropertyBinding::Value(..) => {},
+ }
+ }
+
+ // Update the template this instane references, which may refresh the GPU
+ // cache with any shared template data.
+ prim_data.update(
+ frame_state,
+ frame_context.scene_properties,
+ );
+
+ write_segment(
+ *segment_instance_index,
+ frame_state,
+ &mut scratch.segments,
+ &mut scratch.segment_instances,
+ |request| {
+ prim_data.kind.write_prim_gpu_blocks(
+ request,
+ frame_context.scene_properties,
+ );
+ }
+ );
+ } else {
+ let strategy = get_prim_render_strategy(
+ &prim_instance.vis.clip_chain,
+ frame_state.clip_store,
+ data_stores,
+ );
+
+ let prim_data = &data_stores.prim[*data_handle];
+
+ let (color, is_opaque) = match prim_data.kind {
+ PrimitiveTemplateKind::Clear => {
+ // Opaque black with operator dest out
+ (ColorF::BLACK, false)
+ }
+ PrimitiveTemplateKind::Rectangle { ref color, .. } => {
+ let color = frame_context.scene_properties.resolve_color(color);
+
+ (color, color.a >= 1.0)
+ }
+ };
+
+ let premul_color = color.premultiplied();
+
+ let map_prim_to_surface = frame_context.spatial_tree.get_relative_transform(
+ prim_spatial_node_index,
+ pic_context.raster_spatial_node_index,
+ );
+ let prim_is_2d_axis_aligned = map_prim_to_surface.is_2d_axis_aligned();
+
+ let mut quad_flags = QuadFlags::empty();
+ if is_opaque {
+ quad_flags |= QuadFlags::IS_OPAQUE;
+ }
+ quad_flags |= QuadFlags::APPLY_DEVICE_CLIP;
+
+ // TODO(gw): For now, we don't select per-edge AA at all if the primitive
+ // has a 2d transform, which matches existing behavior. However,
+ // as a follow up, we can now easily check if we have a 2d-aligned
+ // primitive on a subpixel boundary, and enable AA along those edge(s).
+ let aa_flags = if prim_is_2d_axis_aligned {
+ EdgeAaSegmentMask::empty()
+ } else {
+ EdgeAaSegmentMask::all()
+ };
+
+ let transform_id = frame_state.transforms.get_id(
+ prim_spatial_node_index,
+ pic_context.raster_spatial_node_index,
+ frame_context.spatial_tree,
+ );
+
+ // TODO(gw): Perhaps rather than writing untyped data here (we at least do validate
+ // the written block count) to gpu-buffer, we could add a trait for
+ // writing typed data?
+ let main_prim_address = write_prim_blocks(
+ frame_state.frame_gpu_data,
+ prim_data.common.prim_rect,
+ prim_instance.vis.clip_chain.local_clip_rect,
+ premul_color,
+ &[],
+ );
+
+ match strategy.render_mode {
+ QuadRenderMode::Direct => {
+ frame_state.push_prim(
+ &PrimitiveCommand::quad(
+ prim_instance_index,
+ main_prim_address,
+ transform_id,
+ quad_flags,
+ aa_flags,
+ ),
+ prim_spatial_node_index,
+ targets,
+ );
+ }
+ QuadRenderMode::Indirect => {
+ let surface = &frame_state.surfaces[pic_context.surface_index.0];
+ let clipped_surface_rect = surface.get_surface_rect(
+ &prim_instance.vis.clip_chain.pic_coverage_rect,
+ frame_context.spatial_tree,
+ ).expect("bug: what can cause this?");
+
+ let p0 = clipped_surface_rect.min.floor();
+ let p1 = clipped_surface_rect.max.ceil();
+
+ let x0 = p0.x;
+ let y0 = p0.y;
+ let x1 = p1.x;
+ let y1 = p1.y;
+
+ let segment = add_segment(
+ x0,
+ y0,
+ x1,
+ y1,
+ true,
+ prim_instance,
+ prim_spatial_node_index,
+ main_prim_address,
+ transform_id,
+ aa_flags,
+ quad_flags,
+ device_pixel_scale,
+ frame_state,
+ );
+
+ add_composite_prim(
+ prim_instance_index,
+ LayoutRect::new(LayoutPoint::new(x0, y0), LayoutPoint::new(x1, y1)),
+ premul_color,
+ quad_flags,
+ frame_state,
+ targets,
+ &[segment],
+ );
+ }
+ QuadRenderMode::NinePatch { clip_rect, radius, clip_out } => {
+ let surface = &frame_state.surfaces[pic_context.surface_index.0];
+ let clipped_surface_rect = surface.get_surface_rect(
+ &prim_instance.vis.clip_chain.pic_coverage_rect,
+ frame_context.spatial_tree,
+ ).expect("bug: what can cause this?");
+
+ let unclipped_surface_rect = surface.map_to_device_rect(
+ &prim_instance.vis.clip_chain.pic_coverage_rect,
+ frame_context.spatial_tree,
+ );
+
+ let local_corner_0 = LayoutRect::new(
+ clip_rect.min,
+ clip_rect.min + LayoutVector2D::new(radius, radius),
+ );
+
+ let local_corner_1 = LayoutRect::new(
+ clip_rect.max - LayoutVector2D::new(radius, radius),
+ clip_rect.max,
+ );
+
+ let pic_corner_0 = pic_state.map_local_to_pic.map(&local_corner_0).unwrap();
+ let pic_corner_1 = pic_state.map_local_to_pic.map(&local_corner_1).unwrap();
+
+ let surface_rect_0 = surface.map_to_device_rect(
+ &pic_corner_0,
+ frame_context.spatial_tree,
+ );
+
+ let surface_rect_1 = surface.map_to_device_rect(
+ &pic_corner_1,
+ frame_context.spatial_tree,
+ );
+
+ let p0 = surface_rect_0.min.floor();
+ let p1 = surface_rect_0.max.ceil();
+ let p2 = surface_rect_1.min.floor();
+ let p3 = surface_rect_1.max.ceil();
+
+ let mut x_coords = [p0.x, p1.x, p2.x, p3.x];
+ let mut y_coords = [p0.y, p1.y, p2.y, p3.y];
+
+ x_coords.sort_by(|a, b| a.partial_cmp(b).unwrap());
+ y_coords.sort_by(|a, b| a.partial_cmp(b).unwrap());
+
+ scratch.quad_segments.clear();
+
+ for y in 0 .. y_coords.len()-1 {
+ let y0 = y_coords[y];
+ let y1 = y_coords[y+1];
+
+ if y1 <= y0 {
+ continue;
+ }
+
+ for x in 0 .. x_coords.len()-1 {
+ let x0 = x_coords[x];
+ let x1 = x_coords[x+1];
+
+ if x1 <= x0 {
+ continue;
+ }
+
+ let create_task = if x == 1 || y == 1 {
+ if clip_out {
+ continue;
+ } else {
+ false
+ }
+ } else {
+ true
+ };
+
+ let r = DeviceRect::new(DevicePoint::new(x0, y0), DevicePoint::new(x1, y1));
+
+ let r = match r.intersection(&clipped_surface_rect) {
+ Some(r) => r,
+ None => {
+ continue;
+ }
+ };
+
+ let x0 = r.min.x;
+ let y0 = r.min.y;
+ let x1 = r.max.x;
+ let y1 = r.max.y;
+
+ let segment = add_segment(
+ x0,
+ y0,
+ x1,
+ y1,
+ create_task,
+ prim_instance,
+ prim_spatial_node_index,
+ main_prim_address,
+ transform_id,
+ aa_flags,
+ quad_flags,
+ device_pixel_scale,
+ frame_state,
+ );
+ scratch.quad_segments.push(segment);
+ }
+ }
+
+ add_composite_prim(
+ prim_instance_index,
+ unclipped_surface_rect.cast_unit(),
+ premul_color,
+ quad_flags,
+ frame_state,
+ targets,
+ &scratch.quad_segments,
+ );
+ }
+ }
+
+ return;
+ }
+ }
+ PrimitiveInstanceKind::YuvImage { data_handle, segment_instance_index, .. } => {
+ profile_scope!("YuvImage");
+ let prim_data = &mut data_stores.yuv_image[*data_handle];
+ let common_data = &mut prim_data.common;
+ let yuv_image_data = &mut prim_data.kind;
+
+ common_data.may_need_repetition = false;
+
+ // Update the template this instane references, which may refresh the GPU
+ // cache with any shared template data.
+ yuv_image_data.update(common_data, frame_state);
+
+ write_segment(
+ *segment_instance_index,
+ frame_state,
+ &mut scratch.segments,
+ &mut scratch.segment_instances,
+ |request| {
+ yuv_image_data.write_prim_gpu_blocks(request);
+ }
+ );
+ }
+ PrimitiveInstanceKind::Image { data_handle, image_instance_index, .. } => {
+ profile_scope!("Image");
+
+ let prim_data = &mut data_stores.image[*data_handle];
+ let common_data = &mut prim_data.common;
+ let image_data = &mut prim_data.kind;
+ let image_instance = &mut store.images[*image_instance_index];
+
+ // Update the template this instance references, which may refresh the GPU
+ // cache with any shared template data.
+ image_data.update(
+ common_data,
+ image_instance,
+ pic_context.surface_index,
+ prim_spatial_node_index,
+ frame_state,
+ frame_context,
+ &mut prim_instance.vis,
+ );
+
+ write_segment(
+ image_instance.segment_instance_index,
+ frame_state,
+ &mut scratch.segments,
+ &mut scratch.segment_instances,
+ |request| {
+ image_data.write_prim_gpu_blocks(request);
+ },
+ );
+ }
+ PrimitiveInstanceKind::LinearGradient { data_handle, ref mut visible_tiles_range, .. } => {
+ profile_scope!("LinearGradient");
+ let prim_data = &mut data_stores.linear_grad[*data_handle];
+
+ // Update the template this instane references, which may refresh the GPU
+ // cache with any shared template data.
+ prim_data.update(frame_state, pic_context.surface_index);
+
+ if prim_data.stretch_size.width >= prim_data.common.prim_rect.width() &&
+ prim_data.stretch_size.height >= prim_data.common.prim_rect.height() {
+
+ prim_data.common.may_need_repetition = false;
+ }
+
+ if prim_data.tile_spacing != LayoutSize::zero() {
+ // We are performing the decomposition on the CPU here, no need to
+ // have it in the shader.
+ prim_data.common.may_need_repetition = false;
+
+ *visible_tiles_range = decompose_repeated_gradient(
+ &prim_instance.vis,
+ &prim_data.common.prim_rect,
+ prim_spatial_node_index,
+ &prim_data.stretch_size,
+ &prim_data.tile_spacing,
+ frame_state,
+ &mut scratch.gradient_tiles,
+ &frame_context.spatial_tree,
+ Some(&mut |_, mut request| {
+ request.push([
+ prim_data.start_point.x,
+ prim_data.start_point.y,
+ prim_data.end_point.x,
+ prim_data.end_point.y,
+ ]);
+ request.push([
+ pack_as_float(prim_data.extend_mode as u32),
+ prim_data.stretch_size.width,
+ prim_data.stretch_size.height,
+ 0.0,
+ ]);
+ }),
+ );
+
+ if visible_tiles_range.is_empty() {
+ prim_instance.clear_visibility();
+ }
+ }
+
+ let stops_address = GradientGpuBlockBuilder::build(
+ prim_data.reverse_stops,
+ frame_state.frame_gpu_data,
+ &prim_data.stops,
+ );
+
+ // TODO(gw): Consider whether it's worth doing segment building
+ // for gradient primitives.
+ frame_state.push_prim(
+ &PrimitiveCommand::instance(prim_instance_index, stops_address),
+ prim_spatial_node_index,
+ targets,
+ );
+ return;
+ }
+ PrimitiveInstanceKind::CachedLinearGradient { data_handle, ref mut visible_tiles_range, .. } => {
+ profile_scope!("CachedLinearGradient");
+ let prim_data = &mut data_stores.linear_grad[*data_handle];
+ prim_data.common.may_need_repetition = prim_data.stretch_size.width < prim_data.common.prim_rect.width()
+ || prim_data.stretch_size.height < prim_data.common.prim_rect.height();
+
+ // Update the template this instance references, which may refresh the GPU
+ // cache with any shared template data.
+ prim_data.update(frame_state, pic_context.surface_index);
+
+ if prim_data.tile_spacing != LayoutSize::zero() {
+ prim_data.common.may_need_repetition = false;
+
+ *visible_tiles_range = decompose_repeated_gradient(
+ &prim_instance.vis,
+ &prim_data.common.prim_rect,
+ prim_spatial_node_index,
+ &prim_data.stretch_size,
+ &prim_data.tile_spacing,
+ frame_state,
+ &mut scratch.gradient_tiles,
+ &frame_context.spatial_tree,
+ None,
+ );
+
+ if visible_tiles_range.is_empty() {
+ prim_instance.clear_visibility();
+ }
+ }
+ }
+ PrimitiveInstanceKind::RadialGradient { data_handle, ref mut visible_tiles_range, .. } => {
+ profile_scope!("RadialGradient");
+ let prim_data = &mut data_stores.radial_grad[*data_handle];
+
+ prim_data.common.may_need_repetition = prim_data.stretch_size.width < prim_data.common.prim_rect.width()
+ || prim_data.stretch_size.height < prim_data.common.prim_rect.height();
+
+ // Update the template this instane references, which may refresh the GPU
+ // cache with any shared template data.
+ prim_data.update(frame_state, pic_context.surface_index);
+
+ if prim_data.tile_spacing != LayoutSize::zero() {
+ prim_data.common.may_need_repetition = false;
+
+ *visible_tiles_range = decompose_repeated_gradient(
+ &prim_instance.vis,
+ &prim_data.common.prim_rect,
+ prim_spatial_node_index,
+ &prim_data.stretch_size,
+ &prim_data.tile_spacing,
+ frame_state,
+ &mut scratch.gradient_tiles,
+ &frame_context.spatial_tree,
+ None,
+ );
+
+ if visible_tiles_range.is_empty() {
+ prim_instance.clear_visibility();
+ }
+ }
+
+ // TODO(gw): Consider whether it's worth doing segment building
+ // for gradient primitives.
+ }
+ PrimitiveInstanceKind::ConicGradient { data_handle, ref mut visible_tiles_range, .. } => {
+ profile_scope!("ConicGradient");
+ let prim_data = &mut data_stores.conic_grad[*data_handle];
+
+ prim_data.common.may_need_repetition = prim_data.stretch_size.width < prim_data.common.prim_rect.width()
+ || prim_data.stretch_size.height < prim_data.common.prim_rect.height();
+
+ // Update the template this instane references, which may refresh the GPU
+ // cache with any shared template data.
+ prim_data.update(frame_state, pic_context.surface_index);
+
+ if prim_data.tile_spacing != LayoutSize::zero() {
+ prim_data.common.may_need_repetition = false;
+
+ *visible_tiles_range = decompose_repeated_gradient(
+ &prim_instance.vis,
+ &prim_data.common.prim_rect,
+ prim_spatial_node_index,
+ &prim_data.stretch_size,
+ &prim_data.tile_spacing,
+ frame_state,
+ &mut scratch.gradient_tiles,
+ &frame_context.spatial_tree,
+ None,
+ );
+
+ if visible_tiles_range.is_empty() {
+ prim_instance.clear_visibility();
+ }
+ }
+
+ // TODO(gw): Consider whether it's worth doing segment building
+ // for gradient primitives.
+ }
+ PrimitiveInstanceKind::Picture { pic_index, segment_instance_index, .. } => {
+ profile_scope!("Picture");
+ let pic = &mut store.pictures[pic_index.0];
+
+ if pic.prepare_for_render(
+ frame_state,
+ data_stores,
+ ) {
+ if let Picture3DContext::In { root_data: None, plane_splitter_index, .. } = pic.context_3d {
+ let dirty_rect = frame_state.current_dirty_region().combined;
+ let splitter = &mut frame_state.plane_splitters[plane_splitter_index.0];
+ let surface_index = pic.raster_config.as_ref().unwrap().surface_index;
+ let surface = &frame_state.surfaces[surface_index.0];
+ let local_prim_rect = surface.clipped_local_rect.cast_unit();
+
+ PicturePrimitive::add_split_plane(
+ splitter,
+ frame_context.spatial_tree,
+ prim_spatial_node_index,
+ local_prim_rect,
+ &prim_instance.vis.clip_chain.local_clip_rect,
+ dirty_rect,
+ plane_split_anchor,
+ );
+ }
+
+ // If this picture uses segments, ensure the GPU cache is
+ // up to date with segment local rects.
+ // TODO(gw): This entire match statement above can now be
+ // refactored into prepare_interned_prim_for_render.
+ if pic.can_use_segments() {
+ write_segment(
+ *segment_instance_index,
+ frame_state,
+ &mut scratch.segments,
+ &mut scratch.segment_instances,
+ |request| {
+ request.push(PremultipliedColorF::WHITE);
+ request.push(PremultipliedColorF::WHITE);
+ request.push([
+ -1.0, // -ve means use prim rect for stretch size
+ 0.0,
+ 0.0,
+ 0.0,
+ ]);
+ }
+ );
+ }
+ } else {
+ prim_instance.clear_visibility();
+ }
+ }
+ PrimitiveInstanceKind::BackdropCapture { .. } => {
+ // Register the owner picture of this backdrop primitive as the
+ // target for resolve of the sub-graph
+ frame_state.surface_builder.register_resolve_source();
+ }
+ PrimitiveInstanceKind::BackdropRender { pic_index, .. } => {
+ match frame_state.surface_builder.sub_graph_output_map.get(pic_index).cloned() {
+ Some(sub_graph_output_id) => {
+ frame_state.surface_builder.add_child_render_task(
+ sub_graph_output_id,
+ frame_state.rg_builder,
+ );
+ }
+ None => {
+ // Backdrop capture was found not visible, didn't produce a sub-graph
+ // so we can just skip drawing
+ prim_instance.clear_visibility();
+ }
+ }
+ }
+ }
+
+ match prim_instance.vis.state {
+ VisibilityState::Unset => {
+ panic!("bug: invalid vis state");
+ }
+ VisibilityState::Visible { .. } => {
+ frame_state.push_prim(
+ &PrimitiveCommand::simple(prim_instance_index),
+ prim_spatial_node_index,
+ targets,
+ );
+ }
+ VisibilityState::PassThrough | VisibilityState::Culled => {}
+ }
+}
+
+
+fn write_segment<F>(
+ segment_instance_index: SegmentInstanceIndex,
+ frame_state: &mut FrameBuildingState,
+ segments: &mut SegmentStorage,
+ segment_instances: &mut SegmentInstanceStorage,
+ f: F,
+) where F: Fn(&mut GpuDataRequest) {
+ debug_assert_ne!(segment_instance_index, SegmentInstanceIndex::INVALID);
+ if segment_instance_index != SegmentInstanceIndex::UNUSED {
+ let segment_instance = &mut segment_instances[segment_instance_index];
+
+ if let Some(mut request) = frame_state.gpu_cache.request(&mut segment_instance.gpu_cache_handle) {
+ let segments = &segments[segment_instance.segments_range];
+
+ f(&mut request);
+
+ for segment in segments {
+ request.write_segment(
+ segment.local_rect,
+ [0.0; 4],
+ );
+ }
+ }
+ }
+}
+
+fn decompose_repeated_gradient(
+ prim_vis: &PrimitiveVisibility,
+ prim_local_rect: &LayoutRect,
+ prim_spatial_node_index: SpatialNodeIndex,
+ stretch_size: &LayoutSize,
+ tile_spacing: &LayoutSize,
+ frame_state: &mut FrameBuildingState,
+ gradient_tiles: &mut GradientTileStorage,
+ spatial_tree: &SpatialTree,
+ mut callback: Option<&mut dyn FnMut(&LayoutRect, GpuDataRequest)>,
+) -> GradientTileRange {
+ let tile_range = gradient_tiles.open_range();
+
+ // Tighten the clip rect because decomposing the repeated image can
+ // produce primitives that are partially covering the original image
+ // rect and we want to clip these extra parts out.
+ if let Some(tight_clip_rect) = prim_vis
+ .clip_chain
+ .local_clip_rect
+ .intersection(prim_local_rect) {
+
+ let visible_rect = compute_conservative_visible_rect(
+ &prim_vis.clip_chain,
+ frame_state.current_dirty_region().combined,
+ prim_spatial_node_index,
+ spatial_tree,
+ );
+ let stride = *stretch_size + *tile_spacing;
+
+ let repetitions = image_tiling::repetitions(prim_local_rect, &visible_rect, stride);
+ gradient_tiles.reserve(repetitions.num_repetitions());
+ for Repetition { origin, .. } in repetitions {
+ let mut handle = GpuCacheHandle::new();
+ let rect = LayoutRect::from_origin_and_size(
+ origin,
+ *stretch_size,
+ );
+
+ if let Some(callback) = &mut callback {
+ if let Some(request) = frame_state.gpu_cache.request(&mut handle) {
+ callback(&rect, request);
+ }
+ }
+
+ gradient_tiles.push(VisibleGradientTile {
+ local_rect: rect,
+ local_clip_rect: tight_clip_rect,
+ handle
+ });
+ }
+ }
+
+ // At this point if we don't have tiles to show it means we could probably
+ // have done a better a job at culling during an earlier stage.
+ gradient_tiles.close_range(tile_range)
+}
+
+
+fn update_clip_task_for_brush(
+ instance: &PrimitiveInstance,
+ prim_origin: &LayoutPoint,
+ prim_spatial_node_index: SpatialNodeIndex,
+ root_spatial_node_index: SpatialNodeIndex,
+ pic_context: &PictureContext,
+ pic_state: &mut PictureState,
+ frame_context: &FrameBuildingContext,
+ frame_state: &mut FrameBuildingState,
+ prim_store: &PrimitiveStore,
+ data_stores: &mut DataStores,
+ segments_store: &mut SegmentStorage,
+ segment_instances_store: &mut SegmentInstanceStorage,
+ clip_mask_instances: &mut Vec<ClipMaskKind>,
+ device_pixel_scale: DevicePixelScale,
+) -> Option<ClipTaskIndex> {
+ let segments = match instance.kind {
+ PrimitiveInstanceKind::TextRun { .. } |
+ PrimitiveInstanceKind::Clear { .. } |
+ PrimitiveInstanceKind::LineDecoration { .. } |
+ PrimitiveInstanceKind::BackdropCapture { .. } |
+ PrimitiveInstanceKind::BackdropRender { .. } => {
+ return None;
+ }
+ PrimitiveInstanceKind::Image { image_instance_index, .. } => {
+ let segment_instance_index = prim_store
+ .images[image_instance_index]
+ .segment_instance_index;
+
+ if segment_instance_index == SegmentInstanceIndex::UNUSED {
+ return None;
+ }
+
+ let segment_instance = &segment_instances_store[segment_instance_index];
+
+ &segments_store[segment_instance.segments_range]
+ }
+ PrimitiveInstanceKind::Picture { segment_instance_index, .. } => {
+ // Pictures may not support segment rendering at all (INVALID)
+ // or support segment rendering but choose not to due to size
+ // or some other factor (UNUSED).
+ if segment_instance_index == SegmentInstanceIndex::UNUSED ||
+ segment_instance_index == SegmentInstanceIndex::INVALID {
+ return None;
+ }
+
+ let segment_instance = &segment_instances_store[segment_instance_index];
+ &segments_store[segment_instance.segments_range]
+ }
+ PrimitiveInstanceKind::YuvImage { segment_instance_index, .. } => {
+ debug_assert!(segment_instance_index != SegmentInstanceIndex::INVALID);
+
+ if segment_instance_index == SegmentInstanceIndex::UNUSED {
+ return None;
+ }
+
+ let segment_instance = &segment_instances_store[segment_instance_index];
+
+ &segments_store[segment_instance.segments_range]
+ }
+ PrimitiveInstanceKind::Rectangle { use_legacy_path, segment_instance_index, .. } => {
+ assert!(use_legacy_path);
+ debug_assert!(segment_instance_index != SegmentInstanceIndex::INVALID);
+
+ if segment_instance_index == SegmentInstanceIndex::UNUSED {
+ return None;
+ }
+
+ let segment_instance = &segment_instances_store[segment_instance_index];
+
+ &segments_store[segment_instance.segments_range]
+ }
+ PrimitiveInstanceKind::ImageBorder { data_handle, .. } => {
+ let border_data = &data_stores.image_border[data_handle].kind;
+
+ // TODO: This is quite messy - once we remove legacy primitives we
+ // can change this to be a tuple match on (instance, template)
+ border_data.brush_segments.as_slice()
+ }
+ PrimitiveInstanceKind::NormalBorder { data_handle, .. } => {
+ let border_data = &data_stores.normal_border[data_handle].kind;
+
+ // TODO: This is quite messy - once we remove legacy primitives we
+ // can change this to be a tuple match on (instance, template)
+ border_data.brush_segments.as_slice()
+ }
+ PrimitiveInstanceKind::LinearGradient { data_handle, .. }
+ | PrimitiveInstanceKind::CachedLinearGradient { data_handle, .. } => {
+ let prim_data = &data_stores.linear_grad[data_handle];
+
+ // TODO: This is quite messy - once we remove legacy primitives we
+ // can change this to be a tuple match on (instance, template)
+ if prim_data.brush_segments.is_empty() {
+ return None;
+ }
+
+ prim_data.brush_segments.as_slice()
+ }
+ PrimitiveInstanceKind::RadialGradient { data_handle, .. } => {
+ let prim_data = &data_stores.radial_grad[data_handle];
+
+ // TODO: This is quite messy - once we remove legacy primitives we
+ // can change this to be a tuple match on (instance, template)
+ if prim_data.brush_segments.is_empty() {
+ return None;
+ }
+
+ prim_data.brush_segments.as_slice()
+ }
+ PrimitiveInstanceKind::ConicGradient { data_handle, .. } => {
+ let prim_data = &data_stores.conic_grad[data_handle];
+
+ // TODO: This is quite messy - once we remove legacy primitives we
+ // can change this to be a tuple match on (instance, template)
+ if prim_data.brush_segments.is_empty() {
+ return None;
+ }
+
+ prim_data.brush_segments.as_slice()
+ }
+ };
+
+ // If there are no segments, early out to avoid setting a valid
+ // clip task instance location below.
+ if segments.is_empty() {
+ return None;
+ }
+
+ // Set where in the clip mask instances array the clip mask info
+ // can be found for this primitive. Each segment will push the
+ // clip mask information for itself in update_clip_task below.
+ let clip_task_index = ClipTaskIndex(clip_mask_instances.len() as _);
+
+ // If we only built 1 segment, there is no point in re-running
+ // the clip chain builder. Instead, just use the clip chain
+ // instance that was built for the main primitive. This is a
+ // significant optimization for the common case.
+ if segments.len() == 1 {
+ let clip_mask_kind = update_brush_segment_clip_task(
+ &segments[0],
+ Some(&instance.vis.clip_chain),
+ root_spatial_node_index,
+ pic_context.surface_index,
+ frame_context,
+ frame_state,
+ &mut data_stores.clip,
+ device_pixel_scale,
+ );
+ clip_mask_instances.push(clip_mask_kind);
+ } else {
+ let dirty_world_rect = frame_state.current_dirty_region().combined;
+
+ for segment in segments {
+ // Build a clip chain for the smaller segment rect. This will
+ // often manage to eliminate most/all clips, and sometimes
+ // clip the segment completely.
+ frame_state.clip_store.set_active_clips_from_clip_chain(
+ &instance.vis.clip_chain,
+ prim_spatial_node_index,
+ &frame_context.spatial_tree,
+ &data_stores.clip,
+ );
+
+ let segment_clip_chain = frame_state
+ .clip_store
+ .build_clip_chain_instance(
+ segment.local_rect.translate(prim_origin.to_vector()),
+ &pic_state.map_local_to_pic,
+ &pic_state.map_pic_to_world,
+ &frame_context.spatial_tree,
+ frame_state.gpu_cache,
+ frame_state.resource_cache,
+ device_pixel_scale,
+ &dirty_world_rect,
+ &mut data_stores.clip,
+ false,
+ );
+
+ let clip_mask_kind = update_brush_segment_clip_task(
+ &segment,
+ segment_clip_chain.as_ref(),
+ root_spatial_node_index,
+ pic_context.surface_index,
+ frame_context,
+ frame_state,
+ &mut data_stores.clip,
+ device_pixel_scale,
+ );
+ clip_mask_instances.push(clip_mask_kind);
+ }
+ }
+
+ Some(clip_task_index)
+}
+
+pub fn update_clip_task(
+ instance: &mut PrimitiveInstance,
+ prim_origin: &LayoutPoint,
+ prim_spatial_node_index: SpatialNodeIndex,
+ root_spatial_node_index: SpatialNodeIndex,
+ pic_context: &PictureContext,
+ pic_state: &mut PictureState,
+ frame_context: &FrameBuildingContext,
+ frame_state: &mut FrameBuildingState,
+ prim_store: &mut PrimitiveStore,
+ data_stores: &mut DataStores,
+ scratch: &mut PrimitiveScratchBuffer,
+) -> bool {
+ let device_pixel_scale = frame_state.surfaces[pic_context.surface_index.0].device_pixel_scale;
+
+ build_segments_if_needed(
+ instance,
+ frame_state,
+ prim_store,
+ data_stores,
+ &mut scratch.segments,
+ &mut scratch.segment_instances,
+ );
+
+ // First try to render this primitive's mask using optimized brush rendering.
+ instance.vis.clip_task_index = if let Some(clip_task_index) = update_clip_task_for_brush(
+ instance,
+ prim_origin,
+ prim_spatial_node_index,
+ root_spatial_node_index,
+ pic_context,
+ pic_state,
+ frame_context,
+ frame_state,
+ prim_store,
+ data_stores,
+ &mut scratch.segments,
+ &mut scratch.segment_instances,
+ &mut scratch.clip_mask_instances,
+ device_pixel_scale,
+ ) {
+ clip_task_index
+ } else if instance.vis.clip_chain.needs_mask {
+ // Get a minimal device space rect, clipped to the screen that we
+ // need to allocate for the clip mask, as well as interpolated
+ // snap offsets.
+ let unadjusted_device_rect = match frame_state.surfaces[pic_context.surface_index.0].get_surface_rect(
+ &instance.vis.clip_chain.pic_coverage_rect,
+ frame_context.spatial_tree,
+ ) {
+ Some(rect) => rect,
+ None => return false,
+ };
+
+ let (device_rect, device_pixel_scale) = adjust_mask_scale_for_max_size(
+ unadjusted_device_rect,
+ device_pixel_scale,
+ );
+ let clip_task_id = RenderTaskKind::new_mask(
+ device_rect,
+ instance.vis.clip_chain.clips_range,
+ root_spatial_node_index,
+ frame_state.clip_store,
+ frame_state.gpu_cache,
+ frame_state.frame_gpu_data,
+ frame_state.resource_cache,
+ frame_state.rg_builder,
+ &mut data_stores.clip,
+ device_pixel_scale,
+ frame_context.fb_config,
+ &mut frame_state.surface_builder,
+ );
+ // Set the global clip mask instance for this primitive.
+ let clip_task_index = ClipTaskIndex(scratch.clip_mask_instances.len() as _);
+ scratch.clip_mask_instances.push(ClipMaskKind::Mask(clip_task_id));
+ instance.vis.clip_task_index = clip_task_index;
+ frame_state.surface_builder.add_child_render_task(
+ clip_task_id,
+ frame_state.rg_builder,
+ );
+ clip_task_index
+ } else {
+ ClipTaskIndex::INVALID
+ };
+
+ true
+}
+
+/// Write out to the clip mask instances array the correct clip mask
+/// config for this segment.
+pub fn update_brush_segment_clip_task(
+ segment: &BrushSegment,
+ clip_chain: Option<&ClipChainInstance>,
+ root_spatial_node_index: SpatialNodeIndex,
+ surface_index: SurfaceIndex,
+ frame_context: &FrameBuildingContext,
+ frame_state: &mut FrameBuildingState,
+ clip_data_store: &mut ClipDataStore,
+ device_pixel_scale: DevicePixelScale,
+) -> ClipMaskKind {
+ let clip_chain = match clip_chain {
+ Some(chain) => chain,
+ None => return ClipMaskKind::Clipped,
+ };
+ if !clip_chain.needs_mask ||
+ (!segment.may_need_clip_mask && !clip_chain.has_non_local_clips) {
+ return ClipMaskKind::None;
+ }
+
+ let device_rect = match frame_state.surfaces[surface_index.0].get_surface_rect(
+ &clip_chain.pic_coverage_rect,
+ frame_context.spatial_tree,
+ ) {
+ Some(rect) => rect,
+ None => return ClipMaskKind::Clipped,
+ };
+
+ let (device_rect, device_pixel_scale) = adjust_mask_scale_for_max_size(device_rect, device_pixel_scale);
+
+ let clip_task_id = RenderTaskKind::new_mask(
+ device_rect,
+ clip_chain.clips_range,
+ root_spatial_node_index,
+ frame_state.clip_store,
+ frame_state.gpu_cache,
+ frame_state.frame_gpu_data,
+ frame_state.resource_cache,
+ frame_state.rg_builder,
+ clip_data_store,
+ device_pixel_scale,
+ frame_context.fb_config,
+ &mut frame_state.surface_builder,
+ );
+
+ frame_state.surface_builder.add_child_render_task(
+ clip_task_id,
+ frame_state.rg_builder,
+ );
+ ClipMaskKind::Mask(clip_task_id)
+}
+
+
+fn write_brush_segment_description(
+ prim_local_rect: LayoutRect,
+ prim_local_clip_rect: LayoutRect,
+ clip_chain: &ClipChainInstance,
+ segment_builder: &mut SegmentBuilder,
+ clip_store: &ClipStore,
+ data_stores: &DataStores,
+) -> bool {
+ // If the brush is small, we want to skip building segments
+ // and just draw it as a single primitive with clip mask.
+ if prim_local_rect.area() < MIN_BRUSH_SPLIT_AREA {
+ return false;
+ }
+
+ // NOTE: The local clip rect passed to the segment builder must be the unmodified
+ // local clip rect from the clip leaf, not the local_clip_rect from the
+ // clip-chain instance. The clip-chain instance may have been reduced by
+ // clips that are in the same coordinate system, but not the same spatial
+ // node as the primitive. This can result in the clip for the segment building
+ // being affected by scrolling clips, which we can't handle (since the segments
+ // are not invalidated during frame building after being built).
+ segment_builder.initialize(
+ prim_local_rect,
+ None,
+ prim_local_clip_rect,
+ );
+
+ // Segment the primitive on all the local-space clip sources that we can.
+ for i in 0 .. clip_chain.clips_range.count {
+ let clip_instance = clip_store
+ .get_instance_from_range(&clip_chain.clips_range, i);
+ let clip_node = &data_stores.clip[clip_instance.handle];
+
+ // If this clip item is positioned by another positioning node, its relative position
+ // could change during scrolling. This means that we would need to resegment. Instead
+ // of doing that, only segment with clips that have the same positioning node.
+ // TODO(mrobinson, #2858): It may make sense to include these nodes, resegmenting only
+ // when necessary while scrolling.
+ if !clip_instance.flags.contains(ClipNodeFlags::SAME_SPATIAL_NODE) {
+ continue;
+ }
+
+ let (local_clip_rect, radius, mode) = match clip_node.item.kind {
+ ClipItemKind::RoundedRectangle { rect, radius, mode } => {
+ (rect, Some(radius), mode)
+ }
+ ClipItemKind::Rectangle { rect, mode } => {
+ (rect, None, mode)
+ }
+ ClipItemKind::BoxShadow { ref source } => {
+ // For inset box shadows, we can clip out any
+ // pixels that are inside the shadow region
+ // and are beyond the inner rect, as they can't
+ // be affected by the blur radius.
+ let inner_clip_mode = match source.clip_mode {
+ BoxShadowClipMode::Outset => None,
+ BoxShadowClipMode::Inset => Some(ClipMode::ClipOut),
+ };
+
+ // Push a region into the segment builder where the
+ // box-shadow can have an effect on the result. This
+ // ensures clip-mask tasks get allocated for these
+ // pixel regions, even if no other clips affect them.
+ segment_builder.push_mask_region(
+ source.prim_shadow_rect,
+ source.prim_shadow_rect.inflate(
+ -0.5 * source.original_alloc_size.width,
+ -0.5 * source.original_alloc_size.height,
+ ),
+ inner_clip_mode,
+ );
+
+ continue;
+ }
+ ClipItemKind::Image { .. } => {
+ // If we encounter an image mask, bail out from segment building.
+ // It's not possible to know which parts of the primitive are affected
+ // by the mask (without inspecting the pixels). We could do something
+ // better here in the future if it ever shows up as a performance issue
+ // (for instance, at least segment based on the bounding rect of the
+ // image mask if it's non-repeating).
+ return false;
+ }
+ };
+
+ segment_builder.push_clip_rect(local_clip_rect, radius, mode);
+ }
+
+ true
+}
+
+fn build_segments_if_needed(
+ instance: &mut PrimitiveInstance,
+ frame_state: &mut FrameBuildingState,
+ prim_store: &mut PrimitiveStore,
+ data_stores: &DataStores,
+ segments_store: &mut SegmentStorage,
+ segment_instances_store: &mut SegmentInstanceStorage,
+) {
+ let prim_clip_chain = &instance.vis.clip_chain;
+
+ // Usually, the primitive rect can be found from information
+ // in the instance and primitive template.
+ let prim_local_rect = data_stores.get_local_prim_rect(
+ instance,
+ &prim_store.pictures,
+ frame_state.surfaces,
+ );
+
+ let segment_instance_index = match instance.kind {
+ PrimitiveInstanceKind::Rectangle { use_legacy_path, ref mut segment_instance_index, .. } => {
+ assert!(use_legacy_path);
+ segment_instance_index
+ }
+ PrimitiveInstanceKind::YuvImage { ref mut segment_instance_index, .. } => {
+ segment_instance_index
+ }
+ PrimitiveInstanceKind::Image { data_handle, image_instance_index, .. } => {
+ let image_data = &data_stores.image[data_handle].kind;
+ let image_instance = &mut prim_store.images[image_instance_index];
+ //Note: tiled images don't support automatic segmentation,
+ // they strictly produce one segment per visible tile instead.
+ if frame_state
+ .resource_cache
+ .get_image_properties(image_data.key)
+ .and_then(|properties| properties.tiling)
+ .is_some()
+ {
+ image_instance.segment_instance_index = SegmentInstanceIndex::UNUSED;
+ return;
+ }
+ &mut image_instance.segment_instance_index
+ }
+ PrimitiveInstanceKind::Picture { ref mut segment_instance_index, pic_index, .. } => {
+ let pic = &mut prim_store.pictures[pic_index.0];
+
+ // If this picture supports segment rendering
+ if pic.can_use_segments() {
+ // If the segments have been invalidated, ensure the current
+ // index of segments is invalid. This ensures that the segment
+ // building logic below will be run.
+ if !pic.segments_are_valid {
+ *segment_instance_index = SegmentInstanceIndex::INVALID;
+ pic.segments_are_valid = true;
+ }
+
+ segment_instance_index
+ } else {
+ return;
+ }
+ }
+ PrimitiveInstanceKind::TextRun { .. } |
+ PrimitiveInstanceKind::NormalBorder { .. } |
+ PrimitiveInstanceKind::ImageBorder { .. } |
+ PrimitiveInstanceKind::Clear { .. } |
+ PrimitiveInstanceKind::LinearGradient { .. } |
+ PrimitiveInstanceKind::CachedLinearGradient { .. } |
+ PrimitiveInstanceKind::RadialGradient { .. } |
+ PrimitiveInstanceKind::ConicGradient { .. } |
+ PrimitiveInstanceKind::LineDecoration { .. } |
+ PrimitiveInstanceKind::BackdropCapture { .. } |
+ PrimitiveInstanceKind::BackdropRender { .. } => {
+ // These primitives don't support / need segments.
+ return;
+ }
+ };
+
+ if *segment_instance_index == SegmentInstanceIndex::INVALID {
+ let mut segments: SmallVec<[BrushSegment; 8]> = SmallVec::new();
+ let clip_leaf = frame_state.clip_tree.get_leaf(instance.clip_leaf_id);
+
+ if write_brush_segment_description(
+ prim_local_rect,
+ clip_leaf.local_clip_rect,
+ prim_clip_chain,
+ &mut frame_state.segment_builder,
+ frame_state.clip_store,
+ data_stores,
+ ) {
+ frame_state.segment_builder.build(|segment| {
+ segments.push(
+ BrushSegment::new(
+ segment.rect.translate(-prim_local_rect.min.to_vector()),
+ segment.has_mask,
+ segment.edge_flags,
+ [0.0; 4],
+ BrushFlags::PERSPECTIVE_INTERPOLATION,
+ ),
+ );
+ });
+ }
+
+ // If only a single segment is produced, there is no benefit to writing
+ // a segment instance array. Instead, just use the main primitive rect
+ // written into the GPU cache.
+ // TODO(gw): This is (sortof) a bandaid - due to a limitation in the current
+ // brush encoding, we can only support a total of up to 2^16 segments.
+ // This should be (more than) enough for any real world case, so for
+ // now we can handle this by skipping cases where we were generating
+ // segments where there is no benefit. The long term / robust fix
+ // for this is to move the segment building to be done as a more
+ // limited nine-patch system during scene building, removing arbitrary
+ // segmentation during frame-building (see bug #1617491).
+ if segments.len() <= 1 {
+ *segment_instance_index = SegmentInstanceIndex::UNUSED;
+ } else {
+ let segments_range = segments_store.extend(segments);
+
+ let instance = SegmentedInstance {
+ segments_range,
+ gpu_cache_handle: GpuCacheHandle::new(),
+ };
+
+ *segment_instance_index = segment_instances_store.push(instance);
+ };
+ }
+}
+
+// Ensures that the size of mask render tasks are within MAX_MASK_SIZE.
+fn adjust_mask_scale_for_max_size(device_rect: DeviceRect, device_pixel_scale: DevicePixelScale) -> (DeviceRect, DevicePixelScale) {
+ if device_rect.width() > MAX_MASK_SIZE || device_rect.height() > MAX_MASK_SIZE {
+ // round_out will grow by 1 integer pixel if origin is on a
+ // fractional position, so keep that margin for error with -1:
+ let scale = (MAX_MASK_SIZE - 1.0) /
+ f32::max(device_rect.width(), device_rect.height());
+ let new_device_pixel_scale = device_pixel_scale * Scale::new(scale);
+ let new_device_rect = (device_rect.to_f32() * Scale::new(scale))
+ .round_out();
+ (new_device_rect, new_device_pixel_scale)
+ } else {
+ (device_rect, device_pixel_scale)
+ }
+}
+
+fn write_prim_blocks(
+ builder: &mut GpuBufferBuilder,
+ prim_rect: LayoutRect,
+ clip_rect: LayoutRect,
+ color: PremultipliedColorF,
+ segments: &[QuadSegment],
+) -> GpuBufferAddress {
+ let mut writer = builder.write_blocks(3 + segments.len() * 2);
+
+ writer.push_one(prim_rect);
+ writer.push_one(clip_rect);
+ writer.push_one(color);
+
+ for segment in segments {
+ writer.push_one(segment.rect);
+ match segment.task_id {
+ RenderTaskId::INVALID => {
+ writer.push_one([0.0; 4]);
+ }
+ task_id => {
+ writer.push_render_task(task_id);
+ }
+ }
+ }
+
+ writer.finish()
+}
+
+fn add_segment(
+ x0: f32,
+ y0: f32,
+ x1: f32,
+ y1: f32,
+ create_task: bool,
+ prim_instance: &PrimitiveInstance,
+ prim_spatial_node_index: SpatialNodeIndex,
+ main_prim_address: GpuBufferAddress,
+ transform_id: TransformPaletteId,
+ aa_flags: EdgeAaSegmentMask,
+ quad_flags: QuadFlags,
+ device_pixel_scale: DevicePixelScale,
+ frame_state: &mut FrameBuildingState,
+) -> QuadSegment {
+ let task_size = DeviceSize::new(x1 - x0, y1 - y0).round().to_i32();
+ let content_origin = DevicePoint::new(x0, y0);
+
+ let rect = LayoutRect::new(
+ LayoutPoint::new(x0, y0),
+ LayoutPoint::new(x1, y1),
+ );
+
+ let task_id = if create_task {
+ let task_id = frame_state.rg_builder.add().init(RenderTask::new_dynamic(
+ task_size,
+ RenderTaskKind::new_prim(
+ prim_spatial_node_index,
+ device_pixel_scale,
+ content_origin,
+ main_prim_address,
+ transform_id,
+ aa_flags,
+ quad_flags,
+ prim_instance.vis.clip_chain.clips_range,
+ ),
+ ));
+
+ frame_state.surface_builder.add_child_render_task(
+ task_id,
+ frame_state.rg_builder,
+ );
+
+ task_id
+ } else {
+ RenderTaskId::INVALID
+ };
+
+ QuadSegment {
+ rect,
+ task_id,
+ }
+}
+
+fn add_composite_prim(
+ prim_instance_index: PrimitiveInstanceIndex,
+ rect: LayoutRect,
+ color: PremultipliedColorF,
+ quad_flags: QuadFlags,
+ frame_state: &mut FrameBuildingState,
+ targets: &[CommandBufferIndex],
+ segments: &[QuadSegment],
+) {
+ let composite_prim_address = write_prim_blocks(
+ frame_state.frame_gpu_data,
+ rect,
+ LayoutRect::max_rect(),
+ color,
+ segments,
+ );
+
+ frame_state.set_segments(
+ segments,
+ targets,
+ );
+
+ let mut composite_quad_flags = QuadFlags::IGNORE_DEVICE_PIXEL_SCALE;
+ if quad_flags.contains(QuadFlags::IS_OPAQUE) {
+ composite_quad_flags |= QuadFlags::IS_OPAQUE;
+ }
+
+ frame_state.push_cmd(
+ &PrimitiveCommand::quad(
+ prim_instance_index,
+ composite_prim_address,
+ TransformPaletteId::IDENTITY,
+ composite_quad_flags,
+ // TODO(gw): No AA on composite, unless we use it to apply 2d clips
+ EdgeAaSegmentMask::empty(),
+ ),
+ targets,
+ );
+}
diff --git a/gfx/wr/webrender/src/prim_store/backdrop.rs b/gfx/wr/webrender/src/prim_store/backdrop.rs
new file mode 100644
index 0000000000..7c106e47bc
--- /dev/null
+++ b/gfx/wr/webrender/src/prim_store/backdrop.rs
@@ -0,0 +1,175 @@
+/* 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 api::units::*;
+use crate::intern::{Internable, InternDebug, Handle as InternHandle};
+use crate::internal_types::LayoutPrimitiveInfo;
+use crate::prim_store::{
+ InternablePrimitive, PrimitiveInstanceKind, PrimKey, PrimTemplate,
+ PrimTemplateCommonData, PrimitiveStore, PictureIndex,
+};
+use crate::scene_building::IsVisible;
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, PartialEq, MallocSizeOf, Hash)]
+pub struct BackdropCapture {
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, PartialEq, MallocSizeOf, Hash)]
+pub struct BackdropRender {
+}
+
+impl From<BackdropCapture> for BackdropCaptureData {
+ fn from(_backdrop: BackdropCapture) -> Self {
+ BackdropCaptureData {
+ }
+ }
+}
+
+impl From<BackdropRender> for BackdropRenderData {
+ fn from(_backdrop: BackdropRender) -> Self {
+ BackdropRenderData {
+ }
+ }
+}
+
+pub type BackdropCaptureKey = PrimKey<BackdropCapture>;
+pub type BackdropRenderKey = PrimKey<BackdropRender>;
+
+impl BackdropCaptureKey {
+ pub fn new(
+ info: &LayoutPrimitiveInfo,
+ backdrop_capture: BackdropCapture,
+ ) -> Self {
+ BackdropCaptureKey {
+ common: info.into(),
+ kind: backdrop_capture,
+ }
+ }
+}
+
+impl BackdropRenderKey {
+ pub fn new(
+ info: &LayoutPrimitiveInfo,
+ backdrop_render: BackdropRender,
+ ) -> Self {
+ BackdropRenderKey {
+ common: info.into(),
+ kind: backdrop_render,
+ }
+ }
+}
+
+impl InternDebug for BackdropCaptureKey {}
+impl InternDebug for BackdropRenderKey {}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, MallocSizeOf)]
+pub struct BackdropCaptureData {
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, MallocSizeOf)]
+pub struct BackdropRenderData {
+}
+
+pub type BackdropCaptureTemplate = PrimTemplate<BackdropCaptureData>;
+pub type BackdropRenderTemplate = PrimTemplate<BackdropRenderData>;
+
+impl From<BackdropCaptureKey> for BackdropCaptureTemplate {
+ fn from(backdrop: BackdropCaptureKey) -> Self {
+ let common = PrimTemplateCommonData::with_key_common(backdrop.common);
+
+ BackdropCaptureTemplate {
+ common,
+ kind: backdrop.kind.into(),
+ }
+ }
+}
+
+impl From<BackdropRenderKey> for BackdropRenderTemplate {
+ fn from(backdrop: BackdropRenderKey) -> Self {
+ let common = PrimTemplateCommonData::with_key_common(backdrop.common);
+
+ BackdropRenderTemplate {
+ common,
+ kind: backdrop.kind.into(),
+ }
+ }
+}
+
+pub type BackdropCaptureDataHandle = InternHandle<BackdropCapture>;
+pub type BackdropRenderDataHandle = InternHandle<BackdropRender>;
+
+impl Internable for BackdropCapture {
+ type Key = BackdropCaptureKey;
+ type StoreData = BackdropCaptureTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_BACKDROP_CAPTURES;
+}
+
+impl Internable for BackdropRender {
+ type Key = BackdropRenderKey;
+ type StoreData = BackdropRenderTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_BACKDROP_RENDERS;
+}
+
+impl InternablePrimitive for BackdropCapture {
+ fn into_key(
+ self,
+ info: &LayoutPrimitiveInfo,
+ ) -> BackdropCaptureKey {
+ BackdropCaptureKey::new(info, self)
+ }
+
+ fn make_instance_kind(
+ _key: BackdropCaptureKey,
+ data_handle: BackdropCaptureDataHandle,
+ _prim_store: &mut PrimitiveStore,
+ _reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind {
+ PrimitiveInstanceKind::BackdropCapture {
+ data_handle,
+ }
+ }
+}
+
+impl InternablePrimitive for BackdropRender {
+ fn into_key(
+ self,
+ info: &LayoutPrimitiveInfo,
+ ) -> BackdropRenderKey {
+ BackdropRenderKey::new(info, self)
+ }
+
+ fn make_instance_kind(
+ _key: BackdropRenderKey,
+ data_handle: BackdropRenderDataHandle,
+ _prim_store: &mut PrimitiveStore,
+ _reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind {
+ PrimitiveInstanceKind::BackdropRender {
+ data_handle,
+ pic_index: PictureIndex::INVALID,
+ }
+ }
+}
+
+impl IsVisible for BackdropCapture {
+ fn is_visible(&self) -> bool {
+ true
+ }
+}
+
+impl IsVisible for BackdropRender {
+ fn is_visible(&self) -> bool {
+ true
+ }
+}
diff --git a/gfx/wr/webrender/src/prim_store/borders.rs b/gfx/wr/webrender/src/prim_store/borders.rs
new file mode 100644
index 0000000000..f2f08e66fe
--- /dev/null
+++ b/gfx/wr/webrender/src/prim_store/borders.rs
@@ -0,0 +1,387 @@
+/* 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 api::{NormalBorder, PremultipliedColorF, Shadow, RasterSpace};
+use api::units::*;
+use crate::border::create_border_segments;
+use crate::border::NormalBorderAu;
+use crate::scene_building::{CreateShadow, IsVisible};
+use crate::frame_builder::{FrameBuildingState};
+use crate::gpu_cache::GpuDataRequest;
+use crate::intern;
+use crate::internal_types::{LayoutPrimitiveInfo, FrameId};
+use crate::prim_store::{
+ BorderSegmentInfo, BrushSegment, NinePatchDescriptor, PrimKey,
+ PrimTemplate, PrimTemplateCommonData,
+ PrimitiveInstanceKind, PrimitiveOpacity,
+ PrimitiveStore, InternablePrimitive,
+};
+use crate::resource_cache::ImageRequest;
+use crate::render_task::RenderTask;
+use crate::render_task_graph::RenderTaskId;
+
+use super::storage;
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
+pub struct NormalBorderPrim {
+ pub border: NormalBorderAu,
+ pub widths: LayoutSideOffsetsAu,
+}
+
+pub type NormalBorderKey = PrimKey<NormalBorderPrim>;
+
+impl NormalBorderKey {
+ pub fn new(
+ info: &LayoutPrimitiveInfo,
+ normal_border: NormalBorderPrim,
+ ) -> Self {
+ NormalBorderKey {
+ common: info.into(),
+ kind: normal_border,
+ }
+ }
+}
+
+impl intern::InternDebug for NormalBorderKey {}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct NormalBorderData {
+ pub brush_segments: Vec<BrushSegment>,
+ pub border_segments: Vec<BorderSegmentInfo>,
+ pub border: NormalBorder,
+ pub widths: LayoutSideOffsets,
+}
+
+impl NormalBorderData {
+ /// Update the GPU cache for a given primitive template. This may be called multiple
+ /// times per frame, by each primitive reference that refers to this interned
+ /// template. The initial request call to the GPU cache ensures that work is only
+ /// done if the cache entry is invalid (due to first use or eviction).
+ pub fn update(
+ &mut self,
+ common: &mut PrimTemplateCommonData,
+ frame_state: &mut FrameBuildingState,
+ ) {
+ if let Some(ref mut request) = frame_state.gpu_cache.request(&mut common.gpu_cache_handle) {
+ self.write_prim_gpu_blocks(request, common.prim_rect.size());
+ self.write_segment_gpu_blocks(request);
+ }
+
+ common.opacity = PrimitiveOpacity::translucent();
+ }
+
+ fn write_prim_gpu_blocks(
+ &self,
+ request: &mut GpuDataRequest,
+ prim_size: LayoutSize
+ ) {
+ // Border primitives currently used for
+ // image borders, and run through the
+ // normal brush_image shader.
+ request.push(PremultipliedColorF::WHITE);
+ request.push(PremultipliedColorF::WHITE);
+ request.push([
+ prim_size.width,
+ prim_size.height,
+ 0.0,
+ 0.0,
+ ]);
+ }
+
+ fn write_segment_gpu_blocks(
+ &self,
+ request: &mut GpuDataRequest,
+ ) {
+ for segment in &self.brush_segments {
+ // has to match VECS_PER_SEGMENT
+ request.write_segment(
+ segment.local_rect,
+ segment.extra_data,
+ );
+ }
+ }
+}
+
+pub type NormalBorderTemplate = PrimTemplate<NormalBorderData>;
+
+impl From<NormalBorderKey> for NormalBorderTemplate {
+ fn from(key: NormalBorderKey) -> Self {
+ let common = PrimTemplateCommonData::with_key_common(key.common);
+
+ let mut border: NormalBorder = key.kind.border.into();
+ let widths = LayoutSideOffsets::from_au(key.kind.widths);
+
+ // FIXME(emilio): Is this the best place to do this?
+ border.normalize(&widths);
+
+ let mut brush_segments = Vec::new();
+ let mut border_segments = Vec::new();
+
+ create_border_segments(
+ common.prim_rect.size(),
+ &border,
+ &widths,
+ &mut border_segments,
+ &mut brush_segments,
+ );
+
+ NormalBorderTemplate {
+ common,
+ kind: NormalBorderData {
+ brush_segments,
+ border_segments,
+ border,
+ widths,
+ }
+ }
+ }
+}
+
+pub type NormalBorderDataHandle = intern::Handle<NormalBorderPrim>;
+
+impl intern::Internable for NormalBorderPrim {
+ type Key = NormalBorderKey;
+ type StoreData = NormalBorderTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_NORMAL_BORDERS;
+}
+
+impl InternablePrimitive for NormalBorderPrim {
+ fn into_key(
+ self,
+ info: &LayoutPrimitiveInfo,
+ ) -> NormalBorderKey {
+ NormalBorderKey::new(
+ info,
+ self,
+ )
+ }
+
+ fn make_instance_kind(
+ _key: NormalBorderKey,
+ data_handle: NormalBorderDataHandle,
+ _: &mut PrimitiveStore,
+ _reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind {
+ PrimitiveInstanceKind::NormalBorder {
+ data_handle,
+ render_task_ids: storage::Range::empty(),
+ }
+ }
+}
+
+impl CreateShadow for NormalBorderPrim {
+ fn create_shadow(
+ &self,
+ shadow: &Shadow,
+ _: bool,
+ _: RasterSpace,
+ ) -> Self {
+ let border = self.border.with_color(shadow.color.into());
+ NormalBorderPrim {
+ border,
+ widths: self.widths,
+ }
+ }
+}
+
+impl IsVisible for NormalBorderPrim {
+ fn is_visible(&self) -> bool {
+ true
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
+pub struct ImageBorder {
+ #[ignore_malloc_size_of = "Arc"]
+ pub request: ImageRequest,
+ pub nine_patch: NinePatchDescriptor,
+}
+
+pub type ImageBorderKey = PrimKey<ImageBorder>;
+
+impl ImageBorderKey {
+ pub fn new(
+ info: &LayoutPrimitiveInfo,
+ image_border: ImageBorder,
+ ) -> Self {
+ ImageBorderKey {
+ common: info.into(),
+ kind: image_border,
+ }
+ }
+}
+
+impl intern::InternDebug for ImageBorderKey {}
+
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct ImageBorderData {
+ #[ignore_malloc_size_of = "Arc"]
+ pub request: ImageRequest,
+ pub brush_segments: Vec<BrushSegment>,
+ pub src_color: Option<RenderTaskId>,
+ pub frame_id: FrameId,
+ pub is_opaque: bool,
+}
+
+impl ImageBorderData {
+ /// Update the GPU cache for a given primitive template. This may be called multiple
+ /// times per frame, by each primitive reference that refers to this interned
+ /// template. The initial request call to the GPU cache ensures that work is only
+ /// done if the cache entry is invalid (due to first use or eviction).
+ pub fn update(
+ &mut self,
+ common: &mut PrimTemplateCommonData,
+ frame_state: &mut FrameBuildingState,
+ ) {
+ if let Some(ref mut request) = frame_state.gpu_cache.request(&mut common.gpu_cache_handle) {
+ self.write_prim_gpu_blocks(request, &common.prim_rect.size());
+ self.write_segment_gpu_blocks(request);
+ }
+
+ let frame_id = frame_state.rg_builder.frame_id();
+ if self.frame_id != frame_id {
+ self.frame_id = frame_id;
+
+ let size = frame_state.resource_cache.request_image(
+ self.request,
+ frame_state.gpu_cache,
+ );
+
+ let task_id = frame_state.rg_builder.add().init(
+ RenderTask::new_image(size, self.request)
+ );
+
+ self.src_color = Some(task_id);
+
+ let image_properties = frame_state
+ .resource_cache
+ .get_image_properties(self.request.key);
+
+ self.is_opaque = image_properties
+ .map(|properties| properties.descriptor.is_opaque())
+ .unwrap_or(true);
+ }
+
+ common.opacity = PrimitiveOpacity { is_opaque: self.is_opaque };
+ }
+
+ fn write_prim_gpu_blocks(
+ &self,
+ request: &mut GpuDataRequest,
+ prim_size: &LayoutSize,
+ ) {
+ // Border primitives currently used for
+ // image borders, and run through the
+ // normal brush_image shader.
+ request.push(PremultipliedColorF::WHITE);
+ request.push(PremultipliedColorF::WHITE);
+ request.push([
+ prim_size.width,
+ prim_size.height,
+ 0.0,
+ 0.0,
+ ]);
+ }
+
+ fn write_segment_gpu_blocks(
+ &self,
+ request: &mut GpuDataRequest,
+ ) {
+ for segment in &self.brush_segments {
+ // has to match VECS_PER_SEGMENT
+ request.write_segment(
+ segment.local_rect,
+ segment.extra_data,
+ );
+ }
+ }
+}
+
+pub type ImageBorderTemplate = PrimTemplate<ImageBorderData>;
+
+impl From<ImageBorderKey> for ImageBorderTemplate {
+ fn from(key: ImageBorderKey) -> Self {
+ let common = PrimTemplateCommonData::with_key_common(key.common);
+
+ let brush_segments = key.kind.nine_patch.create_segments(common.prim_rect.size());
+ ImageBorderTemplate {
+ common,
+ kind: ImageBorderData {
+ request: key.kind.request,
+ brush_segments,
+ src_color: None,
+ frame_id: FrameId::INVALID,
+ is_opaque: false,
+ }
+ }
+ }
+}
+
+pub type ImageBorderDataHandle = intern::Handle<ImageBorder>;
+
+impl intern::Internable for ImageBorder {
+ type Key = ImageBorderKey;
+ type StoreData = ImageBorderTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_IMAGE_BORDERS;
+}
+
+impl InternablePrimitive for ImageBorder {
+ fn into_key(
+ self,
+ info: &LayoutPrimitiveInfo,
+ ) -> ImageBorderKey {
+ ImageBorderKey::new(
+ info,
+ self,
+ )
+ }
+
+ fn make_instance_kind(
+ _key: ImageBorderKey,
+ data_handle: ImageBorderDataHandle,
+ _: &mut PrimitiveStore,
+ _reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind {
+ PrimitiveInstanceKind::ImageBorder {
+ data_handle
+ }
+ }
+}
+
+impl IsVisible for ImageBorder {
+ fn is_visible(&self) -> bool {
+ true
+ }
+}
+
+#[test]
+#[cfg(target_pointer_width = "64")]
+fn test_struct_sizes() {
+ use std::mem;
+ // The sizes of these structures are critical for performance on a number of
+ // talos stress tests. If you get a failure here on CI, there's two possibilities:
+ // (a) You made a structure smaller than it currently is. Great work! Update the
+ // test expectations and move on.
+ // (b) You made a structure larger. This is not necessarily a problem, but should only
+ // be done with care, and after checking if talos performance regresses badly.
+ assert_eq!(mem::size_of::<NormalBorderPrim>(), 84, "NormalBorderPrim size changed");
+ assert_eq!(mem::size_of::<NormalBorderTemplate>(), 216, "NormalBorderTemplate size changed");
+ assert_eq!(mem::size_of::<NormalBorderKey>(), 104, "NormalBorderKey size changed");
+ assert_eq!(mem::size_of::<ImageBorder>(), 68, "ImageBorder size changed");
+ assert_eq!(mem::size_of::<ImageBorderTemplate>(), 104, "ImageBorderTemplate size changed");
+ assert_eq!(mem::size_of::<ImageBorderKey>(), 88, "ImageBorderKey size changed");
+}
diff --git a/gfx/wr/webrender/src/prim_store/gradient/conic.rs b/gfx/wr/webrender/src/prim_store/gradient/conic.rs
new file mode 100644
index 0000000000..d9c3f5d350
--- /dev/null
+++ b/gfx/wr/webrender/src/prim_store/gradient/conic.rs
@@ -0,0 +1,399 @@
+/* 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/. */
+
+//! Conic gradients
+//!
+//! Specification: https://drafts.csswg.org/css-images-4/#conic-gradients
+//!
+//! Conic gradients are rendered via cached render tasks and composited with the image brush.
+
+use euclid::vec2;
+use api::{ExtendMode, GradientStop, PremultipliedColorF};
+use api::units::*;
+use crate::scene_building::IsVisible;
+use crate::frame_builder::FrameBuildingState;
+use crate::intern::{Internable, InternDebug, Handle as InternHandle};
+use crate::internal_types::LayoutPrimitiveInfo;
+use crate::prim_store::{BrushSegment, GradientTileRange};
+use crate::prim_store::{PrimitiveInstanceKind, PrimitiveOpacity, FloatKey};
+use crate::prim_store::{PrimKeyCommonData, PrimTemplateCommonData, PrimitiveStore};
+use crate::prim_store::{NinePatchDescriptor, PointKey, SizeKey, InternablePrimitive};
+use crate::render_task::{RenderTask, RenderTaskKind};
+use crate::render_task_graph::RenderTaskId;
+use crate::render_task_cache::{RenderTaskCacheKeyKind, RenderTaskCacheKey, RenderTaskParent};
+use crate::renderer::GpuBufferAddress;
+use crate::picture::{SurfaceIndex};
+
+use std::{hash, ops::{Deref, DerefMut}};
+use super::{stops_and_min_alpha, GradientStopKey, GradientGpuBlockBuilder};
+
+/// Hashable conic gradient parameters, for use during prim interning.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, MallocSizeOf, PartialEq)]
+pub struct ConicGradientParams {
+ pub angle: f32, // in radians
+ pub start_offset: f32,
+ pub end_offset: f32,
+}
+
+impl Eq for ConicGradientParams {}
+
+impl hash::Hash for ConicGradientParams {
+ fn hash<H: hash::Hasher>(&self, state: &mut H) {
+ self.angle.to_bits().hash(state);
+ self.start_offset.to_bits().hash(state);
+ self.end_offset.to_bits().hash(state);
+ }
+}
+
+/// Identifying key for a line decoration.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, PartialEq, Hash, MallocSizeOf)]
+pub struct ConicGradientKey {
+ pub common: PrimKeyCommonData,
+ pub extend_mode: ExtendMode,
+ pub center: PointKey,
+ pub params: ConicGradientParams,
+ pub stretch_size: SizeKey,
+ pub stops: Vec<GradientStopKey>,
+ pub tile_spacing: SizeKey,
+ pub nine_patch: Option<Box<NinePatchDescriptor>>,
+}
+
+impl ConicGradientKey {
+ pub fn new(
+ info: &LayoutPrimitiveInfo,
+ conic_grad: ConicGradient,
+ ) -> Self {
+ ConicGradientKey {
+ common: info.into(),
+ extend_mode: conic_grad.extend_mode,
+ center: conic_grad.center,
+ params: conic_grad.params,
+ stretch_size: conic_grad.stretch_size,
+ stops: conic_grad.stops,
+ tile_spacing: conic_grad.tile_spacing,
+ nine_patch: conic_grad.nine_patch,
+ }
+ }
+}
+
+impl InternDebug for ConicGradientKey {}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct ConicGradientTemplate {
+ pub common: PrimTemplateCommonData,
+ pub extend_mode: ExtendMode,
+ pub center: DevicePoint,
+ pub params: ConicGradientParams,
+ pub task_size: DeviceIntSize,
+ pub scale: DeviceVector2D,
+ pub stretch_size: LayoutSize,
+ pub tile_spacing: LayoutSize,
+ pub brush_segments: Vec<BrushSegment>,
+ pub stops_opacity: PrimitiveOpacity,
+ pub stops: Vec<GradientStop>,
+ pub src_color: Option<RenderTaskId>,
+}
+
+impl Deref for ConicGradientTemplate {
+ type Target = PrimTemplateCommonData;
+ fn deref(&self) -> &Self::Target {
+ &self.common
+ }
+}
+
+impl DerefMut for ConicGradientTemplate {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.common
+ }
+}
+
+impl From<ConicGradientKey> for ConicGradientTemplate {
+ fn from(item: ConicGradientKey) -> Self {
+ let common = PrimTemplateCommonData::with_key_common(item.common);
+ let mut brush_segments = Vec::new();
+
+ if let Some(ref nine_patch) = item.nine_patch {
+ brush_segments = nine_patch.create_segments(common.prim_rect.size());
+ }
+
+ let (stops, min_alpha) = stops_and_min_alpha(&item.stops);
+
+ // Save opacity of the stops for use in
+ // selecting which pass this gradient
+ // should be drawn in.
+ let stops_opacity = PrimitiveOpacity::from_alpha(min_alpha);
+
+ let mut stretch_size: LayoutSize = item.stretch_size.into();
+ stretch_size.width = stretch_size.width.min(common.prim_rect.width());
+ stretch_size.height = stretch_size.height.min(common.prim_rect.height());
+
+ fn approx_eq(a: f32, b: f32) -> bool { (a - b).abs() < 0.01 }
+
+ // Attempt to detect some of the common configurations with hard gradient stops. Allow
+ // those a higher maximum resolution to avoid the worst cases of aliasing artifacts with
+ // large conic gradients. A better solution would be to go back to rendering very large
+ // conic gradients via a brush shader instead of caching all of them (unclear whether
+ // it is important enough to warrant the better solution).
+ let mut has_hard_stops = false;
+ let mut prev_stop = None;
+ let offset_range = item.params.end_offset - item.params.start_offset;
+ for stop in &stops {
+ if offset_range <= 0.0 {
+ break;
+ }
+ if let Some(prev_offset) = prev_stop {
+ // Check whether two consecutive stops are very close (hard stops).
+ if stop.offset < prev_offset + 0.005 / offset_range {
+ // a is the angle of the stop normalized into 0-1 space and repeating in the 0-0.25 range.
+ // If close to 0.0 or 0.25 it means the stop is vertical or horizontal. For those, the lower
+ // resolution isn't a big issue.
+ let a = item.params.angle / (2.0 * std::f32::consts::PI)
+ + item.params.start_offset
+ + stop.offset / offset_range;
+ let a = a.rem_euclid(0.25);
+
+ if !approx_eq(a, 0.0) && !approx_eq(a, 0.25) {
+ has_hard_stops = true;
+ break;
+ }
+ }
+ }
+ prev_stop = Some(stop.offset);
+ }
+
+ let max_size = if has_hard_stops {
+ 2048.0
+ } else {
+ 1024.0
+ };
+
+ // Avoid rendering enormous gradients. Radial gradients are mostly made of soft transitions,
+ // so it is unlikely that rendering at a higher resolution that 1024 would produce noticeable
+ // differences, especially with 8 bits per channel.
+ let mut task_size: DeviceSize = stretch_size.cast_unit();
+ let mut scale = vec2(1.0, 1.0);
+ if task_size.width > max_size {
+ scale.x = task_size.width / max_size;
+ task_size.width = max_size;
+ }
+ if task_size.height > max_size {
+ scale.y = task_size.height / max_size;
+ task_size.height = max_size;
+ }
+
+ ConicGradientTemplate {
+ common,
+ center: DevicePoint::new(item.center.x, item.center.y),
+ extend_mode: item.extend_mode,
+ params: item.params,
+ stretch_size,
+ task_size: task_size.ceil().to_i32(),
+ scale,
+ tile_spacing: item.tile_spacing.into(),
+ brush_segments,
+ stops_opacity,
+ stops,
+ src_color: None,
+ }
+ }
+}
+
+impl ConicGradientTemplate {
+ /// Update the GPU cache for a given primitive template. This may be called multiple
+ /// times per frame, by each primitive reference that refers to this interned
+ /// template. The initial request call to the GPU cache ensures that work is only
+ /// done if the cache entry is invalid (due to first use or eviction).
+ pub fn update(
+ &mut self,
+ frame_state: &mut FrameBuildingState,
+ parent_surface: SurfaceIndex,
+ ) {
+ if let Some(mut request) =
+ frame_state.gpu_cache.request(&mut self.common.gpu_cache_handle) {
+ // write_prim_gpu_blocks
+ request.push(PremultipliedColorF::WHITE);
+ request.push(PremultipliedColorF::WHITE);
+ request.push([
+ self.stretch_size.width,
+ self.stretch_size.height,
+ 0.0,
+ 0.0,
+ ]);
+
+ // write_segment_gpu_blocks
+ for segment in &self.brush_segments {
+ // has to match VECS_PER_SEGMENT
+ request.write_segment(
+ segment.local_rect,
+ segment.extra_data,
+ );
+ }
+ }
+
+ let cache_key = ConicGradientCacheKey {
+ size: self.task_size,
+ center: PointKey { x: self.center.x, y: self.center.y },
+ scale: PointKey { x: self.scale.x, y: self.scale.y },
+ start_offset: FloatKey(self.params.start_offset),
+ end_offset: FloatKey(self.params.end_offset),
+ angle: FloatKey(self.params.angle),
+ extend_mode: self.extend_mode,
+ stops: self.stops.iter().map(|stop| (*stop).into()).collect(),
+ };
+
+ let task_id = frame_state.resource_cache.request_render_task(
+ RenderTaskCacheKey {
+ size: self.task_size,
+ kind: RenderTaskCacheKeyKind::ConicGradient(cache_key),
+ },
+ frame_state.gpu_cache,
+ frame_state.frame_gpu_data,
+ frame_state.rg_builder,
+ None,
+ false,
+ RenderTaskParent::Surface(parent_surface),
+ &mut frame_state.surface_builder,
+ |rg_builder, gpu_buffer_builder| {
+ let stops = GradientGpuBlockBuilder::build(
+ false,
+ gpu_buffer_builder,
+ &self.stops,
+ );
+
+ rg_builder.add().init(RenderTask::new_dynamic(
+ self.task_size,
+ RenderTaskKind::ConicGradient(ConicGradientTask {
+ extend_mode: self.extend_mode,
+ scale: self.scale,
+ center: self.center,
+ params: self.params.clone(),
+ stops,
+ }),
+ ))
+ }
+ );
+
+ self.src_color = Some(task_id);
+
+ // Tile spacing is always handled by decomposing into separate draw calls so the
+ // primitive opacity is equivalent to stops opacity. This might change to being
+ // set to non-opaque in the presence of tile spacing if/when tile spacing is handled
+ // in the same way as with the image primitive.
+ self.opacity = self.stops_opacity;
+ }
+}
+
+pub type ConicGradientDataHandle = InternHandle<ConicGradient>;
+
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ConicGradient {
+ pub extend_mode: ExtendMode,
+ pub center: PointKey,
+ pub params: ConicGradientParams,
+ pub stretch_size: SizeKey,
+ pub stops: Vec<GradientStopKey>,
+ pub tile_spacing: SizeKey,
+ pub nine_patch: Option<Box<NinePatchDescriptor>>,
+}
+
+impl Internable for ConicGradient {
+ type Key = ConicGradientKey;
+ type StoreData = ConicGradientTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_CONIC_GRADIENTS;
+}
+
+impl InternablePrimitive for ConicGradient {
+ fn into_key(
+ self,
+ info: &LayoutPrimitiveInfo,
+ ) -> ConicGradientKey {
+ ConicGradientKey::new(info, self)
+ }
+
+ fn make_instance_kind(
+ _key: ConicGradientKey,
+ data_handle: ConicGradientDataHandle,
+ _prim_store: &mut PrimitiveStore,
+ _reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind {
+ PrimitiveInstanceKind::ConicGradient {
+ data_handle,
+ visible_tiles_range: GradientTileRange::empty(),
+ }
+ }
+}
+
+impl IsVisible for ConicGradient {
+ fn is_visible(&self) -> bool {
+ true
+ }
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ConicGradientTask {
+ pub extend_mode: ExtendMode,
+ pub center: DevicePoint,
+ pub scale: DeviceVector2D,
+ pub params: ConicGradientParams,
+ pub stops: GpuBufferAddress,
+}
+
+impl ConicGradientTask {
+ pub fn to_instance(&self, target_rect: &DeviceIntRect) -> ConicGradientInstance {
+ ConicGradientInstance {
+ task_rect: target_rect.to_f32(),
+ center: self.center,
+ scale: self.scale,
+ start_offset: self.params.start_offset,
+ end_offset: self.params.end_offset,
+ angle: self.params.angle,
+ extend_mode: self.extend_mode as i32,
+ gradient_stops_address: self.stops.as_int(),
+ }
+ }
+}
+
+/// The per-instance shader input of a radial gradient render task.
+///
+/// Must match the RADIAL_GRADIENT instance description in renderer/vertex.rs.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(C)]
+#[derive(Clone, Debug)]
+pub struct ConicGradientInstance {
+ pub task_rect: DeviceRect,
+ pub center: DevicePoint,
+ pub scale: DeviceVector2D,
+ pub start_offset: f32,
+ pub end_offset: f32,
+ pub angle: f32,
+ pub extend_mode: i32,
+ pub gradient_stops_address: i32,
+}
+
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ConicGradientCacheKey {
+ pub size: DeviceIntSize,
+ pub center: PointKey,
+ pub scale: PointKey,
+ pub start_offset: FloatKey,
+ pub end_offset: FloatKey,
+ pub angle: FloatKey,
+ pub extend_mode: ExtendMode,
+ pub stops: Vec<GradientStopKey>,
+}
+
diff --git a/gfx/wr/webrender/src/prim_store/gradient/linear.rs b/gfx/wr/webrender/src/prim_store/gradient/linear.rs
new file mode 100644
index 0000000000..85da4b670a
--- /dev/null
+++ b/gfx/wr/webrender/src/prim_store/gradient/linear.rs
@@ -0,0 +1,750 @@
+/* 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/. */
+
+//! Linear gradients
+//!
+//! Specification: https://drafts.csswg.org/css-images-4/#linear-gradients
+//!
+//! Linear gradients are rendered via cached render tasks and composited with the image brush.
+
+use euclid::approxeq::ApproxEq;
+use euclid::{point2, vec2, size2};
+use api::{ExtendMode, GradientStop, LineOrientation, PremultipliedColorF, ColorF, ColorU};
+use api::units::*;
+use crate::scene_building::IsVisible;
+use crate::frame_builder::FrameBuildingState;
+use crate::intern::{Internable, InternDebug, Handle as InternHandle};
+use crate::internal_types::LayoutPrimitiveInfo;
+use crate::image_tiling::simplify_repeated_primitive;
+use crate::prim_store::{BrushSegment, GradientTileRange};
+use crate::prim_store::{PrimitiveInstanceKind, PrimitiveOpacity};
+use crate::prim_store::{PrimKeyCommonData, PrimTemplateCommonData, PrimitiveStore};
+use crate::prim_store::{NinePatchDescriptor, PointKey, SizeKey, InternablePrimitive};
+use crate::render_task::{RenderTask, RenderTaskKind};
+use crate::render_task_graph::RenderTaskId;
+use crate::render_task_cache::{RenderTaskCacheKeyKind, RenderTaskCacheKey, RenderTaskParent};
+use crate::renderer::GpuBufferAddress;
+use crate::segment::EdgeAaSegmentMask;
+use crate::picture::{SurfaceIndex};
+use crate::util::pack_as_float;
+use super::{stops_and_min_alpha, GradientStopKey, GradientGpuBlockBuilder, apply_gradient_local_clip};
+use std::ops::{Deref, DerefMut};
+use std::mem::swap;
+
+pub const MAX_CACHED_SIZE: f32 = 1024.0;
+
+/// Identifying key for a linear gradient.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, PartialEq, Hash, MallocSizeOf)]
+pub struct LinearGradientKey {
+ pub common: PrimKeyCommonData,
+ pub extend_mode: ExtendMode,
+ pub start_point: PointKey,
+ pub end_point: PointKey,
+ pub stretch_size: SizeKey,
+ pub tile_spacing: SizeKey,
+ pub stops: Vec<GradientStopKey>,
+ pub reverse_stops: bool,
+ pub cached: bool,
+ pub nine_patch: Option<Box<NinePatchDescriptor>>,
+ pub edge_aa_mask: EdgeAaSegmentMask,
+}
+
+impl LinearGradientKey {
+ pub fn new(
+ info: &LayoutPrimitiveInfo,
+ linear_grad: LinearGradient,
+ ) -> Self {
+ LinearGradientKey {
+ common: info.into(),
+ extend_mode: linear_grad.extend_mode,
+ start_point: linear_grad.start_point,
+ end_point: linear_grad.end_point,
+ stretch_size: linear_grad.stretch_size,
+ tile_spacing: linear_grad.tile_spacing,
+ stops: linear_grad.stops,
+ reverse_stops: linear_grad.reverse_stops,
+ cached: linear_grad.cached,
+ nine_patch: linear_grad.nine_patch,
+ edge_aa_mask: linear_grad.edge_aa_mask,
+ }
+ }
+}
+
+impl InternDebug for LinearGradientKey {}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, MallocSizeOf)]
+pub struct LinearGradientTemplate {
+ pub common: PrimTemplateCommonData,
+ pub extend_mode: ExtendMode,
+ pub start_point: DevicePoint,
+ pub end_point: DevicePoint,
+ pub task_size: DeviceIntSize,
+ pub scale: DeviceVector2D,
+ pub stretch_size: LayoutSize,
+ pub tile_spacing: LayoutSize,
+ pub stops_opacity: PrimitiveOpacity,
+ pub stops: Vec<GradientStop>,
+ pub brush_segments: Vec<BrushSegment>,
+ pub reverse_stops: bool,
+ pub is_fast_path: bool,
+ pub cached: bool,
+ pub src_color: Option<RenderTaskId>,
+}
+
+impl Deref for LinearGradientTemplate {
+ type Target = PrimTemplateCommonData;
+ fn deref(&self) -> &Self::Target {
+ &self.common
+ }
+}
+
+impl DerefMut for LinearGradientTemplate {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.common
+ }
+}
+
+/// Perform a few optimizations to the gradient that are relevant to scene building.
+///
+/// Returns true if the gradient was decomposed into fast-path primitives, indicating
+/// that we shouldn't emit a regular gradient primitive after this returns.
+pub fn optimize_linear_gradient(
+ prim_rect: &mut LayoutRect,
+ tile_size: &mut LayoutSize,
+ mut tile_spacing: LayoutSize,
+ clip_rect: &LayoutRect,
+ start: &mut LayoutPoint,
+ end: &mut LayoutPoint,
+ extend_mode: ExtendMode,
+ stops: &mut [GradientStopKey],
+ // Callback called for each fast-path segment (rect, start end, stops).
+ callback: &mut dyn FnMut(&LayoutRect, LayoutPoint, LayoutPoint, &[GradientStopKey], EdgeAaSegmentMask)
+) -> bool {
+ // First sanitize the gradient parameters. See if we can remove repetitions,
+ // tighten the primitive bounds, etc.
+
+ simplify_repeated_primitive(&tile_size, &mut tile_spacing, prim_rect);
+
+ let vertical = start.x.approx_eq(&end.x);
+ let horizontal = start.y.approx_eq(&end.y);
+
+ let mut horizontally_tiled = prim_rect.width() > tile_size.width;
+ let mut vertically_tiled = prim_rect.height() > tile_size.height;
+
+ // Check whether the tiling is equivalent to stretching on either axis.
+ // Stretching the gradient is more efficient than repeating it.
+ if vertically_tiled && horizontal && tile_spacing.height == 0.0 {
+ tile_size.height = prim_rect.height();
+ vertically_tiled = false;
+ }
+
+ if horizontally_tiled && vertical && tile_spacing.width == 0.0 {
+ tile_size.width = prim_rect.width();
+ horizontally_tiled = false;
+ }
+
+ let offset = apply_gradient_local_clip(
+ prim_rect,
+ &tile_size,
+ &tile_spacing,
+ &clip_rect
+ );
+
+ // The size of gradient render tasks depends on the tile_size. No need to generate
+ // large stretch sizes that will be clipped to the bounds of the primitive.
+ tile_size.width = tile_size.width.min(prim_rect.width());
+ tile_size.height = tile_size.height.min(prim_rect.height());
+
+ *start += offset;
+ *end += offset;
+
+ // Next, in the case of axis-aligned gradients, see if it is worth
+ // decomposing the gradient into multiple gradients with only two
+ // gradient stops per segment to get a faster shader.
+
+ if extend_mode != ExtendMode::Clamp || stops.is_empty() {
+ return false;
+ }
+
+ if !vertical && !horizontal {
+ return false;
+ }
+
+ if vertical && horizontal {
+ return false;
+ }
+
+ if !tile_spacing.is_empty() || vertically_tiled || horizontally_tiled {
+ return false;
+ }
+
+ // If the gradient is small, no need to bother with decomposing it.
+ if (horizontal && tile_size.width < 256.0)
+ || (vertical && tile_size.height < 256.0) {
+
+ return false;
+ }
+
+ // Flip x and y if need be so that we only deal with the horizontal case.
+
+ // From now on don't return false. We are going modifying the caller's
+ // variables and not bother to restore them. If the control flow changes,
+ // Make sure to to restore &mut parameters to sensible values before
+ // returning false.
+
+ let adjust_rect = &mut |rect: &mut LayoutRect| {
+ if vertical {
+ swap(&mut rect.min.x, &mut rect.min.y);
+ swap(&mut rect.max.x, &mut rect.max.y);
+ }
+ };
+
+ let adjust_size = &mut |size: &mut LayoutSize| {
+ if vertical { swap(&mut size.width, &mut size.height); }
+ };
+
+ let adjust_point = &mut |p: &mut LayoutPoint| {
+ if vertical { swap(&mut p.x, &mut p.y); }
+ };
+
+ let clip_rect = match clip_rect.intersection(prim_rect) {
+ Some(clip) => clip,
+ None => {
+ return false;
+ }
+ };
+
+ adjust_rect(prim_rect);
+ adjust_point(start);
+ adjust_point(end);
+ adjust_size(tile_size);
+
+ let length = (end.x - start.x).abs();
+
+ // Decompose the gradient into simple segments. This lets us:
+ // - separate opaque from semi-transparent segments,
+ // - compress long segments into small render tasks,
+ // - make sure hard stops stay so even if the primitive is large.
+
+ let reverse_stops = start.x > end.x;
+
+ // Handle reverse stops so we can assume stops are arranged in increasing x.
+ if reverse_stops {
+ stops.reverse();
+ swap(start, end);
+ }
+
+ // Use fake gradient stop to emulate the potential constant color sections
+ // before and after the gradient endpoints.
+ let mut prev = *stops.first().unwrap();
+ let mut last = *stops.last().unwrap();
+
+ // Set the offsets of the fake stops to position them at the edges of the primitive.
+ prev.offset = -start.x / length;
+ last.offset = (tile_size.width - start.x) / length;
+ if reverse_stops {
+ prev.offset = 1.0 - prev.offset;
+ last.offset = 1.0 - last.offset;
+ }
+
+ let (side_edges, first_edge, last_edge) = if vertical {
+ (
+ EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::RIGHT,
+ EdgeAaSegmentMask::TOP,
+ EdgeAaSegmentMask::BOTTOM
+ )
+ } else {
+ (
+ EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::BOTTOM,
+ EdgeAaSegmentMask::LEFT,
+ EdgeAaSegmentMask::RIGHT
+ )
+ };
+
+ let mut is_first = true;
+ let last_offset = last.offset;
+ for stop in stops.iter().chain((&[last]).iter()) {
+ let prev_stop = prev;
+ prev = *stop;
+
+ if prev_stop.color.a == 0 && stop.color.a == 0 {
+ continue;
+ }
+
+
+ let prev_offset = if reverse_stops { 1.0 - prev_stop.offset } else { prev_stop.offset };
+ let offset = if reverse_stops { 1.0 - stop.offset } else { stop.offset };
+
+ // In layout space, relative to the primitive.
+ let segment_start = start.x + prev_offset * length;
+ let segment_end = start.x + offset * length;
+ let segment_length = segment_end - segment_start;
+
+ if segment_length <= 0.0 {
+ continue;
+ }
+
+ let mut segment_rect = *prim_rect;
+ segment_rect.min.x += segment_start;
+ segment_rect.max.x = segment_rect.min.x + segment_length;
+
+ let mut start = point2(0.0, 0.0);
+ let mut end = point2(segment_length, 0.0);
+
+ adjust_point(&mut start);
+ adjust_point(&mut end);
+ adjust_rect(&mut segment_rect);
+
+ let origin_before_clip = segment_rect.min;
+ segment_rect = match segment_rect.intersection(&clip_rect) {
+ Some(rect) => rect,
+ None => {
+ continue;
+ }
+ };
+ let offset = segment_rect.min - origin_before_clip;
+
+ // Account for the clipping since start and end are relative to the origin.
+ start -= offset;
+ end -= offset;
+
+ let mut edge_flags = side_edges;
+ if is_first {
+ edge_flags |= first_edge;
+ is_first = false;
+ }
+ if stop.offset == last_offset {
+ edge_flags |= last_edge;
+ }
+
+ callback(
+ &segment_rect,
+ start,
+ end,
+ &[
+ GradientStopKey { offset: 0.0, .. prev_stop },
+ GradientStopKey { offset: 1.0, .. *stop },
+ ],
+ edge_flags,
+ );
+ }
+
+ true
+}
+
+impl From<LinearGradientKey> for LinearGradientTemplate {
+ fn from(item: LinearGradientKey) -> Self {
+
+ let mut common = PrimTemplateCommonData::with_key_common(item.common);
+ common.edge_aa_mask = item.edge_aa_mask;
+
+ let (mut stops, min_alpha) = stops_and_min_alpha(&item.stops);
+
+ let mut brush_segments = Vec::new();
+
+ if let Some(ref nine_patch) = item.nine_patch {
+ brush_segments = nine_patch.create_segments(common.prim_rect.size());
+ }
+
+ // Save opacity of the stops for use in
+ // selecting which pass this gradient
+ // should be drawn in.
+ let stops_opacity = PrimitiveOpacity::from_alpha(min_alpha);
+
+ let start_point = DevicePoint::new(item.start_point.x, item.start_point.y);
+ let end_point = DevicePoint::new(item.end_point.x, item.end_point.y);
+ let tile_spacing: LayoutSize = item.tile_spacing.into();
+ let stretch_size: LayoutSize = item.stretch_size.into();
+ let mut task_size: DeviceSize = stretch_size.cast_unit();
+
+ let horizontal = start_point.y.approx_eq(&end_point.y);
+ let vertical = start_point.x.approx_eq(&end_point.x);
+
+ if horizontal {
+ // Completely horizontal, we can stretch the gradient vertically.
+ task_size.height = 1.0;
+ }
+
+ if vertical {
+ // Completely vertical, we can stretch the gradient horizontally.
+ task_size.width = 1.0;
+ }
+
+ // See if we can render the gradient using a special fast-path shader.
+ // The fast path path only works with two gradient stops.
+ let mut is_fast_path = false;
+ if item.cached && stops.len() == 2 && brush_segments.is_empty() {
+ if horizontal
+ && stretch_size.width >= common.prim_rect.width()
+ && start_point.x.approx_eq(&0.0)
+ && end_point.x.approx_eq(&stretch_size.width) {
+ is_fast_path = true;
+ task_size.width = task_size.width.min(256.0);
+ }
+ if vertical
+ && stretch_size.height >= common.prim_rect.height()
+ && start_point.y.approx_eq(&0.0)
+ && end_point.y.approx_eq(&stretch_size.height) {
+ is_fast_path = true;
+ task_size.height = task_size.height.min(256.0);
+ }
+
+ if stops[0].color == stops[1].color {
+ is_fast_path = true;
+ task_size = size2(1.0, 1.0);
+ }
+
+ if is_fast_path && item.reverse_stops {
+ // The fast path doesn't use the gradient gpu blocks builder so handle
+ // reversed stops here.
+ stops.swap(0, 1);
+ }
+ }
+
+ // Avoid rendering enormous gradients. Linear gradients are mostly made of soft transitions,
+ // so it is unlikely that rendering at a higher resolution than 1024 would produce noticeable
+ // differences, especially with 8 bits per channel.
+
+ let mut scale = vec2(1.0, 1.0);
+
+ if task_size.width > MAX_CACHED_SIZE {
+ scale.x = task_size.width / MAX_CACHED_SIZE;
+ task_size.width = MAX_CACHED_SIZE;
+ }
+
+ if task_size.height > MAX_CACHED_SIZE {
+ scale.y = task_size.height / MAX_CACHED_SIZE;
+ task_size.height = MAX_CACHED_SIZE;
+ }
+
+ LinearGradientTemplate {
+ common,
+ extend_mode: item.extend_mode,
+ start_point,
+ end_point,
+ task_size: task_size.ceil().to_i32(),
+ scale,
+ stretch_size,
+ tile_spacing,
+ stops_opacity,
+ stops,
+ brush_segments,
+ reverse_stops: item.reverse_stops,
+ is_fast_path,
+ cached: item.cached,
+ src_color: None,
+ }
+ }
+}
+
+impl LinearGradientTemplate {
+ /// Update the GPU cache for a given primitive template. This may be called multiple
+ /// times per frame, by each primitive reference that refers to this interned
+ /// template. The initial request call to the GPU cache ensures that work is only
+ /// done if the cache entry is invalid (due to first use or eviction).
+ pub fn update(
+ &mut self,
+ frame_state: &mut FrameBuildingState,
+ parent_surface: SurfaceIndex,
+ ) {
+ if let Some(mut request) = frame_state.gpu_cache.request(
+ &mut self.common.gpu_cache_handle
+ ) {
+
+ // Write_prim_gpu_blocks
+ if self.cached {
+ // We are using the image brush.
+ request.push(PremultipliedColorF::WHITE);
+ request.push(PremultipliedColorF::WHITE);
+ request.push([
+ self.stretch_size.width,
+ self.stretch_size.height,
+ 0.0,
+ 0.0,
+ ]);
+ } else {
+ // We are using the gradient brush.
+ request.push([
+ self.start_point.x,
+ self.start_point.y,
+ self.end_point.x,
+ self.end_point.y,
+ ]);
+ request.push([
+ pack_as_float(self.extend_mode as u32),
+ self.stretch_size.width,
+ self.stretch_size.height,
+ 0.0,
+ ]);
+ }
+
+ // write_segment_gpu_blocks
+ for segment in &self.brush_segments {
+ // has to match VECS_PER_SEGMENT
+ request.write_segment(
+ segment.local_rect,
+ segment.extra_data,
+ );
+ }
+ }
+
+ // Tile spacing is always handled by decomposing into separate draw calls so the
+ // primitive opacity is equivalent to stops opacity. This might change to being
+ // set to non-opaque in the presence of tile spacing if/when tile spacing is handled
+ // in the same way as with the image primitive.
+ self.opacity = self.stops_opacity;
+
+ if !self.cached {
+ return;
+ }
+
+ let task_id = if self.is_fast_path {
+ let orientation = if self.task_size.width > self.task_size.height {
+ LineOrientation::Horizontal
+ } else {
+ LineOrientation::Vertical
+ };
+
+ let gradient = FastLinearGradientTask {
+ color0: self.stops[0].color.into(),
+ color1: self.stops[1].color.into(),
+ orientation,
+ };
+
+ frame_state.resource_cache.request_render_task(
+ RenderTaskCacheKey {
+ size: self.task_size,
+ kind: RenderTaskCacheKeyKind::FastLinearGradient(gradient),
+ },
+ frame_state.gpu_cache,
+ frame_state.frame_gpu_data,
+ frame_state.rg_builder,
+ None,
+ false,
+ RenderTaskParent::Surface(parent_surface),
+ &mut frame_state.surface_builder,
+ |rg_builder, _| {
+ rg_builder.add().init(RenderTask::new_dynamic(
+ self.task_size,
+ RenderTaskKind::FastLinearGradient(gradient),
+ ))
+ }
+ )
+ } else {
+ let cache_key = LinearGradientCacheKey {
+ size: self.task_size,
+ start: PointKey { x: self.start_point.x, y: self.start_point.y },
+ end: PointKey { x: self.end_point.x, y: self.end_point.y },
+ scale: PointKey { x: self.scale.x, y: self.scale.y },
+ extend_mode: self.extend_mode,
+ stops: self.stops.iter().map(|stop| (*stop).into()).collect(),
+ reversed_stops: self.reverse_stops,
+ };
+
+ frame_state.resource_cache.request_render_task(
+ RenderTaskCacheKey {
+ size: self.task_size,
+ kind: RenderTaskCacheKeyKind::LinearGradient(cache_key),
+ },
+ frame_state.gpu_cache,
+ frame_state.frame_gpu_data,
+ frame_state.rg_builder,
+ None,
+ false,
+ RenderTaskParent::Surface(parent_surface),
+ &mut frame_state.surface_builder,
+ |rg_builder, gpu_buffer_builder| {
+ let stops = Some(GradientGpuBlockBuilder::build(
+ self.reverse_stops,
+ gpu_buffer_builder,
+ &self.stops,
+ ));
+
+ rg_builder.add().init(RenderTask::new_dynamic(
+ self.task_size,
+ RenderTaskKind::LinearGradient(LinearGradientTask {
+ start: self.start_point,
+ end: self.end_point,
+ scale: self.scale,
+ extend_mode: self.extend_mode,
+ stops: stops.unwrap(),
+ }),
+ ))
+ }
+ )
+ };
+
+ self.src_color = Some(task_id);
+ }
+}
+
+pub type LinearGradientDataHandle = InternHandle<LinearGradient>;
+
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct LinearGradient {
+ pub extend_mode: ExtendMode,
+ pub start_point: PointKey,
+ pub end_point: PointKey,
+ pub stretch_size: SizeKey,
+ pub tile_spacing: SizeKey,
+ pub stops: Vec<GradientStopKey>,
+ pub reverse_stops: bool,
+ pub nine_patch: Option<Box<NinePatchDescriptor>>,
+ pub cached: bool,
+ pub edge_aa_mask: EdgeAaSegmentMask,
+}
+
+impl Internable for LinearGradient {
+ type Key = LinearGradientKey;
+ type StoreData = LinearGradientTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_LINEAR_GRADIENTS;
+}
+
+impl InternablePrimitive for LinearGradient {
+ fn into_key(
+ self,
+ info: &LayoutPrimitiveInfo,
+ ) -> LinearGradientKey {
+ LinearGradientKey::new(info, self)
+ }
+
+ fn make_instance_kind(
+ key: LinearGradientKey,
+ data_handle: LinearGradientDataHandle,
+ _prim_store: &mut PrimitiveStore,
+ _reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind {
+ if key.cached {
+ PrimitiveInstanceKind::CachedLinearGradient {
+ data_handle,
+ visible_tiles_range: GradientTileRange::empty(),
+ }
+ } else {
+ PrimitiveInstanceKind::LinearGradient {
+ data_handle,
+ visible_tiles_range: GradientTileRange::empty(),
+ }
+ }
+ }
+}
+
+impl IsVisible for LinearGradient {
+ fn is_visible(&self) -> bool {
+ true
+ }
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct LinearGradientPrimitive {
+ pub cache_segments: Vec<CachedGradientSegment>,
+ pub visible_tiles_range: GradientTileRange,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct CachedGradientSegment {
+ pub render_task: RenderTaskId,
+ pub local_rect: LayoutRect,
+}
+
+
+#[derive(Copy, Clone, Debug, Hash, MallocSizeOf, PartialEq, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct FastLinearGradientTask {
+ pub color0: ColorU,
+ pub color1: ColorU,
+ pub orientation: LineOrientation,
+}
+
+impl FastLinearGradientTask {
+ pub fn to_instance(&self, target_rect: &DeviceIntRect) -> FastLinearGradientInstance {
+ FastLinearGradientInstance {
+ task_rect: target_rect.to_f32(),
+ color0: ColorF::from(self.color0).premultiplied(),
+ color1: ColorF::from(self.color1).premultiplied(),
+ axis_select: match self.orientation {
+ LineOrientation::Horizontal => 0.0,
+ LineOrientation::Vertical => 1.0,
+ },
+ }
+ }
+}
+
+pub type FastLinearGradientCacheKey = FastLinearGradientTask;
+
+/// The per-instance shader input of a fast-path linear gradient render task.
+///
+/// Must match the FAST_LINEAR_GRADIENT instance description in renderer/vertex.rs.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(C)]
+#[derive(Clone, Debug)]
+pub struct FastLinearGradientInstance {
+ pub task_rect: DeviceRect,
+ pub color0: PremultipliedColorF,
+ pub color1: PremultipliedColorF,
+ pub axis_select: f32,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct LinearGradientTask {
+ pub start: DevicePoint,
+ pub end: DevicePoint,
+ pub scale: DeviceVector2D,
+ pub extend_mode: ExtendMode,
+ pub stops: GpuBufferAddress,
+}
+
+impl LinearGradientTask {
+ pub fn to_instance(&self, target_rect: &DeviceIntRect) -> LinearGradientInstance {
+ LinearGradientInstance {
+ task_rect: target_rect.to_f32(),
+ start: self.start,
+ end: self.end,
+ scale: self.scale,
+ extend_mode: self.extend_mode as i32,
+ gradient_stops_address: self.stops.as_int(),
+ }
+ }
+}
+
+/// The per-instance shader input of a linear gradient render task.
+///
+/// Must match the LINEAR_GRADIENT instance description in renderer/vertex.rs.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(C)]
+#[derive(Clone, Debug)]
+pub struct LinearGradientInstance {
+ pub task_rect: DeviceRect,
+ pub start: DevicePoint,
+ pub end: DevicePoint,
+ pub scale: DeviceVector2D,
+ pub extend_mode: i32,
+ pub gradient_stops_address: i32,
+}
+
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct LinearGradientCacheKey {
+ pub size: DeviceIntSize,
+ pub start: PointKey,
+ pub end: PointKey,
+ pub scale: PointKey,
+ pub extend_mode: ExtendMode,
+ pub stops: Vec<GradientStopKey>,
+ pub reversed_stops: bool,
+}
diff --git a/gfx/wr/webrender/src/prim_store/gradient/mod.rs b/gfx/wr/webrender/src/prim_store/gradient/mod.rs
new file mode 100644
index 0000000000..d0b922c579
--- /dev/null
+++ b/gfx/wr/webrender/src/prim_store/gradient/mod.rs
@@ -0,0 +1,392 @@
+/* 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 api::{ColorF, ColorU, GradientStop, PremultipliedColorF};
+use api::units::{LayoutRect, LayoutSize, LayoutVector2D};
+use crate::renderer::{GpuBufferAddress, GpuBufferBuilder};
+use std::hash;
+
+mod linear;
+mod radial;
+mod conic;
+
+pub use linear::MAX_CACHED_SIZE as LINEAR_MAX_CACHED_SIZE;
+
+pub use linear::*;
+pub use radial::*;
+pub use conic::*;
+
+/// A hashable gradient stop that can be used in primitive keys.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, MallocSizeOf, PartialEq)]
+pub struct GradientStopKey {
+ pub offset: f32,
+ pub color: ColorU,
+}
+
+impl GradientStopKey {
+ pub fn empty() -> Self {
+ GradientStopKey {
+ offset: 0.0,
+ color: ColorU::new(0, 0, 0, 0),
+ }
+ }
+}
+
+impl Into<GradientStopKey> for GradientStop {
+ fn into(self) -> GradientStopKey {
+ GradientStopKey {
+ offset: self.offset,
+ color: self.color.into(),
+ }
+ }
+}
+
+// Convert `stop_keys` into a vector of `GradientStop`s, which is a more
+// convenient representation for the current gradient builder. Compute the
+// minimum stop alpha along the way.
+fn stops_and_min_alpha(stop_keys: &[GradientStopKey]) -> (Vec<GradientStop>, f32) {
+ let mut min_alpha: f32 = 1.0;
+ let stops = stop_keys.iter().map(|stop_key| {
+ let color: ColorF = stop_key.color.into();
+ min_alpha = min_alpha.min(color.a);
+
+ GradientStop {
+ offset: stop_key.offset,
+ color,
+ }
+ }).collect();
+
+ (stops, min_alpha)
+}
+
+impl Eq for GradientStopKey {}
+
+impl hash::Hash for GradientStopKey {
+ fn hash<H: hash::Hasher>(&self, state: &mut H) {
+ self.offset.to_bits().hash(state);
+ self.color.hash(state);
+ }
+}
+
+// The gradient entry index for the first color stop
+pub const GRADIENT_DATA_FIRST_STOP: usize = 0;
+// The gradient entry index for the last color stop
+pub const GRADIENT_DATA_LAST_STOP: usize = GRADIENT_DATA_SIZE - 1;
+
+// The start of the gradient data table
+pub const GRADIENT_DATA_TABLE_BEGIN: usize = GRADIENT_DATA_FIRST_STOP + 1;
+// The exclusive bound of the gradient data table
+pub const GRADIENT_DATA_TABLE_END: usize = GRADIENT_DATA_LAST_STOP;
+// The number of entries in the gradient data table.
+pub const GRADIENT_DATA_TABLE_SIZE: usize = 128;
+
+// The number of entries in a gradient data: GRADIENT_DATA_TABLE_SIZE + first stop entry + last stop entry
+pub const GRADIENT_DATA_SIZE: usize = GRADIENT_DATA_TABLE_SIZE + 2;
+
+/// An entry in a gradient data table representing a segment of the gradient
+/// color space.
+#[derive(Debug, Copy, Clone)]
+#[repr(C)]
+struct GradientDataEntry {
+ start_color: PremultipliedColorF,
+ end_step: PremultipliedColorF,
+}
+
+impl GradientDataEntry {
+ fn white() -> Self {
+ Self {
+ start_color: PremultipliedColorF::WHITE,
+ end_step: PremultipliedColorF::TRANSPARENT,
+ }
+ }
+}
+
+// TODO(gw): Tidy this up to be a free function / module?
+pub struct GradientGpuBlockBuilder {}
+
+impl GradientGpuBlockBuilder {
+ /// Generate a color ramp filling the indices in [start_idx, end_idx) and interpolating
+ /// from start_color to end_color.
+ fn fill_colors(
+ start_idx: usize,
+ end_idx: usize,
+ start_color: &PremultipliedColorF,
+ end_color: &PremultipliedColorF,
+ entries: &mut [GradientDataEntry; GRADIENT_DATA_SIZE],
+ prev_step: &PremultipliedColorF,
+ ) -> PremultipliedColorF {
+ // Calculate the color difference for individual steps in the ramp.
+ let inv_steps = 1.0 / (end_idx - start_idx) as f32;
+ let mut step = PremultipliedColorF {
+ r: (end_color.r - start_color.r) * inv_steps,
+ g: (end_color.g - start_color.g) * inv_steps,
+ b: (end_color.b - start_color.b) * inv_steps,
+ a: (end_color.a - start_color.a) * inv_steps,
+ };
+ // As a subtle form of compression, we ensure that the step values for
+ // each stop range are the same if and only if they belong to the same
+ // stop range. However, if two different stop ranges have the same step,
+ // we need to modify the steps so they compare unequally between ranges.
+ // This allows to quickly compare if two adjacent stops belong to the
+ // same range by comparing their steps.
+ if step == *prev_step {
+ // Modify the step alpha value as if by nextafter(). The difference
+ // here should be so small as to be unnoticeable, but yet allow it
+ // to compare differently.
+ step.a = f32::from_bits(if step.a == 0.0 { 1 } else { step.a.to_bits() + 1 });
+ }
+
+ let mut cur_color = *start_color;
+
+ // Walk the ramp writing start and end colors for each entry.
+ for index in start_idx .. end_idx {
+ let entry = &mut entries[index];
+ entry.start_color = cur_color;
+ cur_color.r += step.r;
+ cur_color.g += step.g;
+ cur_color.b += step.b;
+ cur_color.a += step.a;
+ entry.end_step = step;
+ }
+
+ step
+ }
+
+ /// Compute an index into the gradient entry table based on a gradient stop offset. This
+ /// function maps offsets from [0, 1] to indices in [GRADIENT_DATA_TABLE_BEGIN, GRADIENT_DATA_TABLE_END].
+ #[inline]
+ fn get_index(offset: f32) -> usize {
+ (offset.max(0.0).min(1.0) * GRADIENT_DATA_TABLE_SIZE as f32 +
+ GRADIENT_DATA_TABLE_BEGIN as f32)
+ .round() as usize
+ }
+
+ // Build the gradient data from the supplied stops, reversing them if necessary.
+ pub fn build(
+ reverse_stops: bool,
+ gpu_buffer_builder: &mut GpuBufferBuilder,
+ src_stops: &[GradientStop],
+ ) -> GpuBufferAddress {
+ // Preconditions (should be ensured by DisplayListBuilder):
+ // * we have at least two stops
+ // * first stop has offset 0.0
+ // * last stop has offset 1.0
+ let mut src_stops = src_stops.into_iter();
+ let mut cur_color = match src_stops.next() {
+ Some(stop) => {
+ debug_assert_eq!(stop.offset, 0.0);
+ stop.color.premultiplied()
+ }
+ None => {
+ error!("Zero gradient stops found!");
+ PremultipliedColorF::BLACK
+ }
+ };
+
+ // A table of gradient entries, with two colors per entry, that specify the start and end color
+ // within the segment of the gradient space represented by that entry. To lookup a gradient result,
+ // first the entry index is calculated to determine which two colors to interpolate between, then
+ // the offset within that entry bucket is used to interpolate between the two colors in that entry.
+ // This layout is motivated by the fact that if one naively tries to store a single color per entry
+ // and interpolate directly between entries, then hard stops will become softened because the end
+ // color of an entry actually differs from the start color of the next entry, even though they fall
+ // at the same edge offset in the gradient space. Instead, the two-color-per-entry layout preserves
+ // hard stops, as the end color for a given entry can differ from the start color for the following
+ // entry.
+ // Colors are stored in RGBA32F format (in the GPU cache). This table requires the gradient color
+ // stops to be normalized to the range [0, 1]. The first and last entries hold the first and last
+ // color stop colors respectively, while the entries in between hold the interpolated color stop
+ // values for the range [0, 1].
+ // As a further optimization, rather than directly storing the end color, the difference of the end
+ // color from the start color is stored instead, so that an entry can be evaluated more cheaply
+ // with start+diff*offset instead of mix(start,end,offset). Further, the color difference in two
+ // adjacent entries will always be the same if they were generated from the same set of stops/run.
+ // To allow fast searching of the table, if two adjacent entries generated from different sets of
+ // stops (a boundary) have the same difference, the floating-point bits of the stop will be nudged
+ // so that they compare differently without perceptibly altering the interpolation result. This way,
+ // one can quickly scan the table and recover runs just by comparing the color differences of the
+ // current and next entry.
+ // For example, a table with 2 inside entries (startR,startG,startB):(diffR,diffG,diffB) might look
+ // like so:
+ // first | 0.0 | 0.5 | last
+ // (0,0,0):(0,0,0) | (1,0,0):(-1,1,0) | (0,0,1):(0,1,-1) | (1,1,1):(0,0,0)
+ // ^ solid black ^ red to green ^ blue to green ^ solid white
+ let mut entries = [GradientDataEntry::white(); GRADIENT_DATA_SIZE];
+ let mut prev_step = cur_color;
+ if reverse_stops {
+ // Fill in the first entry (for reversed stops) with the first color stop
+ prev_step = GradientGpuBlockBuilder::fill_colors(
+ GRADIENT_DATA_LAST_STOP,
+ GRADIENT_DATA_LAST_STOP + 1,
+ &cur_color,
+ &cur_color,
+ &mut entries,
+ &prev_step,
+ );
+
+ // Fill in the center of the gradient table, generating a color ramp between each consecutive pair
+ // of gradient stops. Each iteration of a loop will fill the indices in [next_idx, cur_idx). The
+ // loop will then fill indices in [GRADIENT_DATA_TABLE_BEGIN, GRADIENT_DATA_TABLE_END).
+ let mut cur_idx = GRADIENT_DATA_TABLE_END;
+ for next in src_stops {
+ let next_color = next.color.premultiplied();
+ let next_idx = Self::get_index(1.0 - next.offset);
+
+ if next_idx < cur_idx {
+ prev_step = GradientGpuBlockBuilder::fill_colors(
+ next_idx,
+ cur_idx,
+ &next_color,
+ &cur_color,
+ &mut entries,
+ &prev_step,
+ );
+ cur_idx = next_idx;
+ }
+
+ cur_color = next_color;
+ }
+ if cur_idx != GRADIENT_DATA_TABLE_BEGIN {
+ error!("Gradient stops abruptly at {}, auto-completing to white", cur_idx);
+ }
+
+ // Fill in the last entry (for reversed stops) with the last color stop
+ GradientGpuBlockBuilder::fill_colors(
+ GRADIENT_DATA_FIRST_STOP,
+ GRADIENT_DATA_FIRST_STOP + 1,
+ &cur_color,
+ &cur_color,
+ &mut entries,
+ &prev_step,
+ );
+ } else {
+ // Fill in the first entry with the first color stop
+ prev_step = GradientGpuBlockBuilder::fill_colors(
+ GRADIENT_DATA_FIRST_STOP,
+ GRADIENT_DATA_FIRST_STOP + 1,
+ &cur_color,
+ &cur_color,
+ &mut entries,
+ &prev_step,
+ );
+
+ // Fill in the center of the gradient table, generating a color ramp between each consecutive pair
+ // of gradient stops. Each iteration of a loop will fill the indices in [cur_idx, next_idx). The
+ // loop will then fill indices in [GRADIENT_DATA_TABLE_BEGIN, GRADIENT_DATA_TABLE_END).
+ let mut cur_idx = GRADIENT_DATA_TABLE_BEGIN;
+ for next in src_stops {
+ let next_color = next.color.premultiplied();
+ let next_idx = Self::get_index(next.offset);
+
+ if next_idx > cur_idx {
+ prev_step = GradientGpuBlockBuilder::fill_colors(
+ cur_idx,
+ next_idx,
+ &cur_color,
+ &next_color,
+ &mut entries,
+ &prev_step,
+ );
+ cur_idx = next_idx;
+ }
+
+ cur_color = next_color;
+ }
+ if cur_idx != GRADIENT_DATA_TABLE_END {
+ error!("Gradient stops abruptly at {}, auto-completing to white", cur_idx);
+ }
+
+ // Fill in the last entry with the last color stop
+ GradientGpuBlockBuilder::fill_colors(
+ GRADIENT_DATA_LAST_STOP,
+ GRADIENT_DATA_LAST_STOP + 1,
+ &cur_color,
+ &cur_color,
+ &mut entries,
+ &prev_step,
+ );
+ }
+
+ let mut writer = gpu_buffer_builder.write_blocks(2 * entries.len());
+
+ for entry in entries {
+ writer.push_one(entry.start_color);
+ writer.push_one(entry.end_step);
+ }
+
+ writer.finish()
+ }
+}
+
+// If the gradient is not tiled we know that any content outside of the clip will not
+// be shown. Applying the clip early reduces how much of the gradient we
+// render and cache. We do this optimization separately on each axis.
+// Returns the offset between the new and old primitive rect origin, to apply to the
+// gradient parameters that are relative to the primitive origin.
+pub fn apply_gradient_local_clip(
+ prim_rect: &mut LayoutRect,
+ stretch_size: &LayoutSize,
+ tile_spacing: &LayoutSize,
+ clip_rect: &LayoutRect,
+) -> LayoutVector2D {
+ let w = prim_rect.max.x.min(clip_rect.max.x) - prim_rect.min.x;
+ let h = prim_rect.max.y.min(clip_rect.max.y) - prim_rect.min.y;
+ let is_tiled_x = w > stretch_size.width + tile_spacing.width;
+ let is_tiled_y = h > stretch_size.height + tile_spacing.height;
+
+ let mut offset = LayoutVector2D::new(0.0, 0.0);
+
+ if !is_tiled_x {
+ let diff = (clip_rect.min.x - prim_rect.min.x).min(prim_rect.width());
+ if diff > 0.0 {
+ prim_rect.min.x += diff;
+ offset.x = -diff;
+ }
+
+ let diff = prim_rect.max.x - clip_rect.max.x;
+ if diff > 0.0 {
+ prim_rect.max.x -= diff;
+ }
+ }
+
+ if !is_tiled_y {
+ let diff = (clip_rect.min.y - prim_rect.min.y).min(prim_rect.height());
+ if diff > 0.0 {
+ prim_rect.min.y += diff;
+ offset.y = -diff;
+ }
+
+ let diff = prim_rect.max.y - clip_rect.max.y;
+ if diff > 0.0 {
+ prim_rect.max.y -= diff;
+ }
+ }
+
+ offset
+}
+
+#[test]
+#[cfg(target_pointer_width = "64")]
+fn test_struct_sizes() {
+ use std::mem;
+ // The sizes of these structures are critical for performance on a number of
+ // talos stress tests. If you get a failure here on CI, there's two possibilities:
+ // (a) You made a structure smaller than it currently is. Great work! Update the
+ // test expectations and move on.
+ // (b) You made a structure larger. This is not necessarily a problem, but should only
+ // be done with care, and after checking if talos performance regresses badly.
+ assert_eq!(mem::size_of::<LinearGradient>(), 72, "LinearGradient size changed");
+ assert_eq!(mem::size_of::<LinearGradientTemplate>(), 144, "LinearGradientTemplate size changed");
+ assert_eq!(mem::size_of::<LinearGradientKey>(), 88, "LinearGradientKey size changed");
+
+ assert_eq!(mem::size_of::<RadialGradient>(), 72, "RadialGradient size changed");
+ assert_eq!(mem::size_of::<RadialGradientTemplate>(), 144, "RadialGradientTemplate size changed");
+ assert_eq!(mem::size_of::<RadialGradientKey>(), 96, "RadialGradientKey size changed");
+
+ assert_eq!(mem::size_of::<ConicGradient>(), 72, "ConicGradient size changed");
+ assert_eq!(mem::size_of::<ConicGradientTemplate>(), 144, "ConicGradientTemplate size changed");
+ assert_eq!(mem::size_of::<ConicGradientKey>(), 96, "ConicGradientKey size changed");
+}
diff --git a/gfx/wr/webrender/src/prim_store/gradient/radial.rs b/gfx/wr/webrender/src/prim_store/gradient/radial.rs
new file mode 100644
index 0000000000..f3f20f9a55
--- /dev/null
+++ b/gfx/wr/webrender/src/prim_store/gradient/radial.rs
@@ -0,0 +1,531 @@
+/* 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/. */
+
+//! Radial gradients
+//!
+//! Specification: https://drafts.csswg.org/css-images-4/#radial-gradients
+//!
+//! Radial gradients are rendered via cached render tasks and composited with the image brush.
+
+use euclid::{vec2, size2};
+use api::{ExtendMode, GradientStop, PremultipliedColorF, ColorU};
+use api::units::*;
+use crate::scene_building::IsVisible;
+use crate::frame_builder::FrameBuildingState;
+use crate::intern::{Internable, InternDebug, Handle as InternHandle};
+use crate::internal_types::LayoutPrimitiveInfo;
+use crate::prim_store::{BrushSegment, GradientTileRange, InternablePrimitive};
+use crate::prim_store::{PrimitiveInstanceKind, PrimitiveOpacity};
+use crate::prim_store::{PrimKeyCommonData, PrimTemplateCommonData, PrimitiveStore};
+use crate::prim_store::{NinePatchDescriptor, PointKey, SizeKey, FloatKey};
+use crate::render_task::{RenderTask, RenderTaskKind};
+use crate::render_task_graph::RenderTaskId;
+use crate::render_task_cache::{RenderTaskCacheKeyKind, RenderTaskCacheKey, RenderTaskParent};
+use crate::renderer::GpuBufferAddress;
+use crate::picture::{SurfaceIndex};
+
+use std::{hash, ops::{Deref, DerefMut}};
+use super::{
+ stops_and_min_alpha, GradientStopKey, GradientGpuBlockBuilder,
+ apply_gradient_local_clip,
+};
+
+/// Hashable radial gradient parameters, for use during prim interning.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, MallocSizeOf, PartialEq)]
+pub struct RadialGradientParams {
+ pub start_radius: f32,
+ pub end_radius: f32,
+ pub ratio_xy: f32,
+}
+
+impl Eq for RadialGradientParams {}
+
+impl hash::Hash for RadialGradientParams {
+ fn hash<H: hash::Hasher>(&self, state: &mut H) {
+ self.start_radius.to_bits().hash(state);
+ self.end_radius.to_bits().hash(state);
+ self.ratio_xy.to_bits().hash(state);
+ }
+}
+
+/// Identifying key for a radial gradient.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, PartialEq, Hash, MallocSizeOf)]
+pub struct RadialGradientKey {
+ pub common: PrimKeyCommonData,
+ pub extend_mode: ExtendMode,
+ pub center: PointKey,
+ pub params: RadialGradientParams,
+ pub stretch_size: SizeKey,
+ pub stops: Vec<GradientStopKey>,
+ pub tile_spacing: SizeKey,
+ pub nine_patch: Option<Box<NinePatchDescriptor>>,
+}
+
+impl RadialGradientKey {
+ pub fn new(
+ info: &LayoutPrimitiveInfo,
+ radial_grad: RadialGradient,
+ ) -> Self {
+ RadialGradientKey {
+ common: info.into(),
+ extend_mode: radial_grad.extend_mode,
+ center: radial_grad.center,
+ params: radial_grad.params,
+ stretch_size: radial_grad.stretch_size,
+ stops: radial_grad.stops,
+ tile_spacing: radial_grad.tile_spacing,
+ nine_patch: radial_grad.nine_patch,
+ }
+ }
+}
+
+impl InternDebug for RadialGradientKey {}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+#[derive(Debug)]
+pub struct RadialGradientTemplate {
+ pub common: PrimTemplateCommonData,
+ pub extend_mode: ExtendMode,
+ pub params: RadialGradientParams,
+ pub center: DevicePoint,
+ pub task_size: DeviceIntSize,
+ pub scale: DeviceVector2D,
+ pub stretch_size: LayoutSize,
+ pub tile_spacing: LayoutSize,
+ pub brush_segments: Vec<BrushSegment>,
+ pub stops_opacity: PrimitiveOpacity,
+ pub stops: Vec<GradientStop>,
+ pub src_color: Option<RenderTaskId>,
+}
+
+impl Deref for RadialGradientTemplate {
+ type Target = PrimTemplateCommonData;
+ fn deref(&self) -> &Self::Target {
+ &self.common
+ }
+}
+
+impl DerefMut for RadialGradientTemplate {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.common
+ }
+}
+
+impl From<RadialGradientKey> for RadialGradientTemplate {
+ fn from(item: RadialGradientKey) -> Self {
+ let common = PrimTemplateCommonData::with_key_common(item.common);
+ let mut brush_segments = Vec::new();
+
+ if let Some(ref nine_patch) = item.nine_patch {
+ brush_segments = nine_patch.create_segments(common.prim_rect.size());
+ }
+
+ let (stops, min_alpha) = stops_and_min_alpha(&item.stops);
+
+ // Save opacity of the stops for use in
+ // selecting which pass this gradient
+ // should be drawn in.
+ let stops_opacity = PrimitiveOpacity::from_alpha(min_alpha);
+
+ let mut stretch_size: LayoutSize = item.stretch_size.into();
+ stretch_size.width = stretch_size.width.min(common.prim_rect.width());
+ stretch_size.height = stretch_size.height.min(common.prim_rect.height());
+
+ // Avoid rendering enormous gradients. Radial gradients are mostly made of soft transitions,
+ // so it is unlikely that rendering at a higher resolution that 1024 would produce noticeable
+ // differences, especially with 8 bits per channel.
+ const MAX_SIZE: f32 = 1024.0;
+ let mut task_size: DeviceSize = stretch_size.cast_unit();
+ let mut scale = vec2(1.0, 1.0);
+ if task_size.width > MAX_SIZE {
+ scale.x = task_size.width/ MAX_SIZE;
+ task_size.width = MAX_SIZE;
+ }
+ if task_size.height > MAX_SIZE {
+ scale.y = task_size.height /MAX_SIZE;
+ task_size.height = MAX_SIZE;
+ }
+
+ RadialGradientTemplate {
+ common,
+ center: DevicePoint::new(item.center.x, item.center.y),
+ extend_mode: item.extend_mode,
+ params: item.params,
+ stretch_size,
+ task_size: task_size.ceil().to_i32(),
+ scale,
+ tile_spacing: item.tile_spacing.into(),
+ brush_segments,
+ stops_opacity,
+ stops,
+ src_color: None,
+ }
+ }
+}
+
+impl RadialGradientTemplate {
+ /// Update the GPU cache for a given primitive template. This may be called multiple
+ /// times per frame, by each primitive reference that refers to this interned
+ /// template. The initial request call to the GPU cache ensures that work is only
+ /// done if the cache entry is invalid (due to first use or eviction).
+ pub fn update(
+ &mut self,
+ frame_state: &mut FrameBuildingState,
+ parent_surface: SurfaceIndex,
+ ) {
+ if let Some(mut request) =
+ frame_state.gpu_cache.request(&mut self.common.gpu_cache_handle) {
+ // write_prim_gpu_blocks
+ request.push(PremultipliedColorF::WHITE);
+ request.push(PremultipliedColorF::WHITE);
+ request.push([
+ self.stretch_size.width,
+ self.stretch_size.height,
+ 0.0,
+ 0.0,
+ ]);
+
+ // write_segment_gpu_blocks
+ for segment in &self.brush_segments {
+ // has to match VECS_PER_SEGMENT
+ request.write_segment(
+ segment.local_rect,
+ segment.extra_data,
+ );
+ }
+ }
+
+ let task_size = self.task_size;
+ let cache_key = RadialGradientCacheKey {
+ size: task_size,
+ center: PointKey { x: self.center.x, y: self.center.y },
+ scale: PointKey { x: self.scale.x, y: self.scale.y },
+ start_radius: FloatKey(self.params.start_radius),
+ end_radius: FloatKey(self.params.end_radius),
+ ratio_xy: FloatKey(self.params.ratio_xy),
+ extend_mode: self.extend_mode,
+ stops: self.stops.iter().map(|stop| (*stop).into()).collect(),
+ };
+
+ let task_id = frame_state.resource_cache.request_render_task(
+ RenderTaskCacheKey {
+ size: task_size,
+ kind: RenderTaskCacheKeyKind::RadialGradient(cache_key),
+ },
+ frame_state.gpu_cache,
+ frame_state.frame_gpu_data,
+ frame_state.rg_builder,
+ None,
+ false,
+ RenderTaskParent::Surface(parent_surface),
+ &mut frame_state.surface_builder,
+ |rg_builder, gpu_buffer_builder| {
+ let stops = GradientGpuBlockBuilder::build(
+ false,
+ gpu_buffer_builder,
+ &self.stops,
+ );
+
+ rg_builder.add().init(RenderTask::new_dynamic(
+ task_size,
+ RenderTaskKind::RadialGradient(RadialGradientTask {
+ extend_mode: self.extend_mode,
+ center: self.center,
+ scale: self.scale,
+ params: self.params.clone(),
+ stops,
+ }),
+ ))
+ }
+ );
+
+ self.src_color = Some(task_id);
+
+ // Tile spacing is always handled by decomposing into separate draw calls so the
+ // primitive opacity is equivalent to stops opacity. This might change to being
+ // set to non-opaque in the presence of tile spacing if/when tile spacing is handled
+ // in the same way as with the image primitive.
+ self.opacity = self.stops_opacity;
+ }
+}
+
+pub type RadialGradientDataHandle = InternHandle<RadialGradient>;
+
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RadialGradient {
+ pub extend_mode: ExtendMode,
+ pub center: PointKey,
+ pub params: RadialGradientParams,
+ pub stretch_size: SizeKey,
+ pub stops: Vec<GradientStopKey>,
+ pub tile_spacing: SizeKey,
+ pub nine_patch: Option<Box<NinePatchDescriptor>>,
+}
+
+impl Internable for RadialGradient {
+ type Key = RadialGradientKey;
+ type StoreData = RadialGradientTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_RADIAL_GRADIENTS;
+}
+
+impl InternablePrimitive for RadialGradient {
+ fn into_key(
+ self,
+ info: &LayoutPrimitiveInfo,
+ ) -> RadialGradientKey {
+ RadialGradientKey::new(info, self)
+ }
+
+ fn make_instance_kind(
+ _key: RadialGradientKey,
+ data_handle: RadialGradientDataHandle,
+ _prim_store: &mut PrimitiveStore,
+ _reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind {
+ PrimitiveInstanceKind::RadialGradient {
+ data_handle,
+ visible_tiles_range: GradientTileRange::empty(),
+ }
+ }
+}
+
+impl IsVisible for RadialGradient {
+ fn is_visible(&self) -> bool {
+ true
+ }
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RadialGradientTask {
+ pub extend_mode: ExtendMode,
+ pub center: DevicePoint,
+ pub scale: DeviceVector2D,
+ pub params: RadialGradientParams,
+ pub stops: GpuBufferAddress,
+}
+
+impl RadialGradientTask {
+ pub fn to_instance(&self, target_rect: &DeviceIntRect) -> RadialGradientInstance {
+ RadialGradientInstance {
+ task_rect: target_rect.to_f32(),
+ center: self.center,
+ scale: self.scale,
+ start_radius: self.params.start_radius,
+ end_radius: self.params.end_radius,
+ ratio_xy: self.params.ratio_xy,
+ extend_mode: self.extend_mode as i32,
+ gradient_stops_address: self.stops.as_int(),
+ }
+ }
+}
+
+/// The per-instance shader input of a radial gradient render task.
+///
+/// Must match the RADIAL_GRADIENT instance description in renderer/vertex.rs.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(C)]
+#[derive(Clone, Debug)]
+pub struct RadialGradientInstance {
+ pub task_rect: DeviceRect,
+ pub center: DevicePoint,
+ pub scale: DeviceVector2D,
+ pub start_radius: f32,
+ pub end_radius: f32,
+ pub ratio_xy: f32,
+ pub extend_mode: i32,
+ pub gradient_stops_address: i32,
+}
+
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RadialGradientCacheKey {
+ pub size: DeviceIntSize,
+ pub center: PointKey,
+ pub scale: PointKey,
+ pub start_radius: FloatKey,
+ pub end_radius: FloatKey,
+ pub ratio_xy: FloatKey,
+ pub extend_mode: ExtendMode,
+ pub stops: Vec<GradientStopKey>,
+}
+
+/// Avoid invoking the radial gradient shader on large areas where the color is
+/// constant.
+///
+/// If the extend mode is set to clamp, the "interesting" part
+/// of the gradient is only in the bounds of the gradient's ellipse, and the rest
+/// is the color of the last gradient stop.
+///
+/// Sometimes we run into radial gradient with a small radius compared to the
+/// primitive bounds, which means a large area of the primitive is a constant color
+/// This function tries to detect that, potentially shrink the gradient primitive to only
+/// the useful part and if needed insert solid color primitives around the gradient where
+/// parts of it have been removed.
+pub fn optimize_radial_gradient(
+ prim_rect: &mut LayoutRect,
+ stretch_size: &mut LayoutSize,
+ center: &mut LayoutPoint,
+ tile_spacing: &mut LayoutSize,
+ clip_rect: &LayoutRect,
+ radius: LayoutSize,
+ end_offset: f32,
+ extend_mode: ExtendMode,
+ stops: &[GradientStopKey],
+ solid_parts: &mut dyn FnMut(&LayoutRect, ColorU),
+) {
+ let offset = apply_gradient_local_clip(
+ prim_rect,
+ stretch_size,
+ tile_spacing,
+ clip_rect
+ );
+
+ *center += offset;
+
+ if extend_mode != ExtendMode::Clamp || stops.is_empty() {
+ return;
+ }
+
+ // Bounding box of the "interesting" part of the gradient.
+ let min = prim_rect.min + center.to_vector() - radius.to_vector() * end_offset;
+ let max = prim_rect.min + center.to_vector() + radius.to_vector() * end_offset;
+
+ // The (non-repeated) gradient primitive rect.
+ let gradient_rect = LayoutRect::from_origin_and_size(
+ prim_rect.min,
+ *stretch_size,
+ );
+
+ // How much internal margin between the primitive bounds and the gradient's
+ // bounding rect (areas that are a constant color).
+ let mut l = (min.x - gradient_rect.min.x).max(0.0).floor();
+ let mut t = (min.y - gradient_rect.min.y).max(0.0).floor();
+ let mut r = (gradient_rect.max.x - max.x).max(0.0).floor();
+ let mut b = (gradient_rect.max.y - max.y).max(0.0).floor();
+
+ let is_tiled = prim_rect.width() > stretch_size.width + tile_spacing.width
+ || prim_rect.height() > stretch_size.height + tile_spacing.height;
+
+ let bg_color = stops.last().unwrap().color;
+
+ if bg_color.a != 0 && is_tiled {
+ // If the primitive has repetitions, it's not enough to insert solid rects around it,
+ // so bail out.
+ return;
+ }
+
+ // If the background is fully transparent, shrinking the primitive bounds as much as possible
+ // is always a win. If the background is not transparent, we have to insert solid rectangles
+ // around the shrunk parts.
+ // If the background is transparent and the primitive is tiled, the optimization may introduce
+ // tile spacing which forces the tiling to be manually decomposed.
+ // Either way, don't bother optimizing unless it saves a significant amount of pixels.
+ if bg_color.a != 0 || (is_tiled && tile_spacing.is_empty()) {
+ let threshold = 128.0;
+ if l < threshold { l = 0.0 }
+ if t < threshold { t = 0.0 }
+ if r < threshold { r = 0.0 }
+ if b < threshold { b = 0.0 }
+ }
+
+ if l + t + r + b == 0.0 {
+ // No adjustment to make;
+ return;
+ }
+
+ // Insert solid rectangles around the gradient, in the places where the primitive will be
+ // shrunk.
+ if bg_color.a != 0 {
+ if l != 0.0 && t != 0.0 {
+ let solid_rect = LayoutRect::from_origin_and_size(
+ gradient_rect.min,
+ size2(l, t),
+ );
+ solid_parts(&solid_rect, bg_color);
+ }
+
+ if l != 0.0 && b != 0.0 {
+ let solid_rect = LayoutRect::from_origin_and_size(
+ gradient_rect.bottom_left() - vec2(0.0, b),
+ size2(l, b),
+ );
+ solid_parts(&solid_rect, bg_color);
+ }
+
+ if t != 0.0 && r != 0.0 {
+ let solid_rect = LayoutRect::from_origin_and_size(
+ gradient_rect.top_right() - vec2(r, 0.0),
+ size2(r, t),
+ );
+ solid_parts(&solid_rect, bg_color);
+ }
+
+ if r != 0.0 && b != 0.0 {
+ let solid_rect = LayoutRect::from_origin_and_size(
+ gradient_rect.bottom_right() - vec2(r, b),
+ size2(r, b),
+ );
+ solid_parts(&solid_rect, bg_color);
+ }
+
+ if l != 0.0 {
+ let solid_rect = LayoutRect::from_origin_and_size(
+ gradient_rect.min + vec2(0.0, t),
+ size2(l, gradient_rect.height() - t - b),
+ );
+ solid_parts(&solid_rect, bg_color);
+ }
+
+ if r != 0.0 {
+ let solid_rect = LayoutRect::from_origin_and_size(
+ gradient_rect.top_right() + vec2(-r, t),
+ size2(r, gradient_rect.height() - t - b),
+ );
+ solid_parts(&solid_rect, bg_color);
+ }
+
+ if t != 0.0 {
+ let solid_rect = LayoutRect::from_origin_and_size(
+ gradient_rect.min + vec2(l, 0.0),
+ size2(gradient_rect.width() - l - r, t),
+ );
+ solid_parts(&solid_rect, bg_color);
+ }
+
+ if b != 0.0 {
+ let solid_rect = LayoutRect::from_origin_and_size(
+ gradient_rect.bottom_left() + vec2(l, -b),
+ size2(gradient_rect.width() - l - r, b),
+ );
+ solid_parts(&solid_rect, bg_color);
+ }
+ }
+
+ // Shrink the gradient primitive.
+
+ prim_rect.min.x += l;
+ prim_rect.min.y += t;
+
+ stretch_size.width -= l + r;
+ stretch_size.height -= b + t;
+
+ center.x -= l;
+ center.y -= t;
+
+ tile_spacing.width += l + r;
+ tile_spacing.height += t + b;
+}
diff --git a/gfx/wr/webrender/src/prim_store/image.rs b/gfx/wr/webrender/src/prim_store/image.rs
new file mode 100644
index 0000000000..9e1edc7f41
--- /dev/null
+++ b/gfx/wr/webrender/src/prim_store/image.rs
@@ -0,0 +1,682 @@
+/* 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 api::{
+ AlphaType, ColorDepth, ColorF, ColorU, ExternalImageData, ExternalImageType,
+ ImageKey as ApiImageKey, ImageBufferKind, ImageRendering, PremultipliedColorF,
+ RasterSpace, Shadow, YuvColorSpace, ColorRange, YuvFormat,
+};
+use api::units::*;
+use crate::scene_building::{CreateShadow, IsVisible};
+use crate::frame_builder::{FrameBuildingContext, FrameBuildingState};
+use crate::gpu_cache::{GpuCache, GpuDataRequest};
+use crate::intern::{Internable, InternDebug, Handle as InternHandle};
+use crate::internal_types::{LayoutPrimitiveInfo};
+use crate::picture::SurfaceIndex;
+use crate::prim_store::{
+ EdgeAaSegmentMask, PrimitiveInstanceKind,
+ PrimitiveOpacity, PrimKey,
+ PrimTemplate, PrimTemplateCommonData, PrimitiveStore, SegmentInstanceIndex,
+ SizeKey, InternablePrimitive,
+};
+use crate::render_target::RenderTargetKind;
+use crate::render_task_graph::RenderTaskId;
+use crate::render_task::RenderTask;
+use crate::render_task_cache::{
+ RenderTaskCacheKey, RenderTaskCacheKeyKind, RenderTaskParent
+};
+use crate::resource_cache::{ImageRequest, ImageProperties, ResourceCache};
+use crate::util::pack_as_float;
+use crate::visibility::{PrimitiveVisibility, compute_conservative_visible_rect};
+use crate::spatial_tree::SpatialNodeIndex;
+use crate::image_tiling;
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct VisibleImageTile {
+ pub src_color: RenderTaskId,
+ pub edge_flags: EdgeAaSegmentMask,
+ pub local_rect: LayoutRect,
+ pub local_clip_rect: LayoutRect,
+}
+
+// Key that identifies a unique (partial) image that is being
+// stored in the render task cache.
+#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ImageCacheKey {
+ pub request: ImageRequest,
+ pub texel_rect: Option<DeviceIntRect>,
+}
+
+/// Instance specific fields for an image primitive. These are
+/// currently stored in a separate array to avoid bloating the
+/// size of PrimitiveInstance. In the future, we should be able
+/// to remove this and store the information inline, by:
+/// (a) Removing opacity collapse / binding support completely.
+/// Once we have general picture caching, we don't need this.
+/// (b) Change visible_tiles to use Storage in the primitive
+/// scratch buffer. This will reduce the size of the
+/// visible_tiles field here, and save memory allocation
+/// when image tiling is used. I've left it as a Vec for
+/// now to reduce the number of changes, and because image
+/// tiling is very rare on real pages.
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct ImageInstance {
+ pub segment_instance_index: SegmentInstanceIndex,
+ pub tight_local_clip_rect: LayoutRect,
+ pub visible_tiles: Vec<VisibleImageTile>,
+ pub src_color: Option<RenderTaskId>,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, PartialEq, MallocSizeOf, Hash)]
+pub struct Image {
+ pub key: ApiImageKey,
+ pub stretch_size: SizeKey,
+ pub tile_spacing: SizeKey,
+ pub color: ColorU,
+ pub image_rendering: ImageRendering,
+ pub alpha_type: AlphaType,
+}
+
+pub type ImageKey = PrimKey<Image>;
+
+impl ImageKey {
+ pub fn new(
+ info: &LayoutPrimitiveInfo,
+ image: Image,
+ ) -> Self {
+ ImageKey {
+ common: info.into(),
+ kind: image,
+ }
+ }
+}
+
+impl InternDebug for ImageKey {}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, MallocSizeOf)]
+pub struct ImageData {
+ pub key: ApiImageKey,
+ pub stretch_size: LayoutSize,
+ pub tile_spacing: LayoutSize,
+ pub color: ColorF,
+ pub image_rendering: ImageRendering,
+ pub alpha_type: AlphaType,
+}
+
+impl From<Image> for ImageData {
+ fn from(image: Image) -> Self {
+ ImageData {
+ key: image.key,
+ color: image.color.into(),
+ stretch_size: image.stretch_size.into(),
+ tile_spacing: image.tile_spacing.into(),
+ image_rendering: image.image_rendering,
+ alpha_type: image.alpha_type,
+ }
+ }
+}
+
+impl ImageData {
+ /// Update the GPU cache for a given primitive template. This may be called multiple
+ /// times per frame, by each primitive reference that refers to this interned
+ /// template. The initial request call to the GPU cache ensures that work is only
+ /// done if the cache entry is invalid (due to first use or eviction).
+ pub fn update(
+ &mut self,
+ common: &mut PrimTemplateCommonData,
+ image_instance: &mut ImageInstance,
+ parent_surface: SurfaceIndex,
+ prim_spatial_node_index: SpatialNodeIndex,
+ frame_state: &mut FrameBuildingState,
+ frame_context: &FrameBuildingContext,
+ visibility: &mut PrimitiveVisibility,
+ ) {
+
+ let image_properties = frame_state
+ .resource_cache
+ .get_image_properties(self.key);
+
+ common.opacity = match &image_properties {
+ Some(properties) => {
+ if properties.descriptor.is_opaque() {
+ PrimitiveOpacity::from_alpha(self.color.a)
+ } else {
+ PrimitiveOpacity::translucent()
+ }
+ }
+ None => PrimitiveOpacity::opaque(),
+ };
+
+ if self.stretch_size.width >= common.prim_rect.width() &&
+ self.stretch_size.height >= common.prim_rect.height() {
+
+ common.may_need_repetition = false;
+ }
+
+ let request = ImageRequest {
+ key: self.key,
+ rendering: self.image_rendering,
+ tile: None,
+ };
+
+ match image_properties {
+ // Non-tiled (most common) path.
+ Some(ImageProperties { tiling: None, ref descriptor, ref external_image, .. }) => {
+ let mut size = frame_state.resource_cache.request_image(
+ request,
+ frame_state.gpu_cache,
+ );
+
+ let orig_task_id = frame_state.rg_builder.add().init(
+ RenderTask::new_image(size, request)
+ );
+
+ // On some devices we cannot render from an ImageBufferKind::TextureExternal
+ // source using most shaders, so must peform a copy to a regular texture first.
+ let task_id = if frame_context.fb_config.external_images_require_copy
+ && matches!(
+ external_image,
+ Some(ExternalImageData {
+ image_type: ExternalImageType::TextureHandle(
+ ImageBufferKind::TextureExternal
+ ),
+ ..
+ })
+ )
+ {
+ let target_kind = if descriptor.format.bytes_per_pixel() == 1 {
+ RenderTargetKind::Alpha
+ } else {
+ RenderTargetKind::Color
+ };
+
+ let task_id = RenderTask::new_scaling(
+ orig_task_id,
+ frame_state.rg_builder,
+ target_kind,
+ size
+ );
+
+ frame_state.surface_builder.add_child_render_task(
+ task_id,
+ frame_state.rg_builder,
+ );
+
+ task_id
+ } else {
+ orig_task_id
+ };
+
+ // Every frame, for cached items, we need to request the render
+ // task cache item. The closure will be invoked on the first
+ // time through, and any time the render task output has been
+ // evicted from the texture cache.
+ if self.tile_spacing == LayoutSize::zero() {
+ // Most common case.
+ image_instance.src_color = Some(task_id);
+ } else {
+ let padding = DeviceIntSideOffsets::new(
+ 0,
+ (self.tile_spacing.width * size.width as f32 / self.stretch_size.width) as i32,
+ (self.tile_spacing.height * size.height as f32 / self.stretch_size.height) as i32,
+ 0,
+ );
+
+ size.width += padding.horizontal();
+ size.height += padding.vertical();
+
+ if padding != DeviceIntSideOffsets::zero() {
+ common.opacity = PrimitiveOpacity::translucent();
+ }
+
+ let image_cache_key = ImageCacheKey {
+ request,
+ texel_rect: None,
+ };
+ let target_kind = if descriptor.format.bytes_per_pixel() == 1 {
+ RenderTargetKind::Alpha
+ } else {
+ RenderTargetKind::Color
+ };
+
+ // Request a pre-rendered image task.
+ let cached_task_handle = frame_state.resource_cache.request_render_task(
+ RenderTaskCacheKey {
+ size,
+ kind: RenderTaskCacheKeyKind::Image(image_cache_key),
+ },
+ frame_state.gpu_cache,
+ frame_state.frame_gpu_data,
+ frame_state.rg_builder,
+ None,
+ descriptor.is_opaque(),
+ RenderTaskParent::Surface(parent_surface),
+ &mut frame_state.surface_builder,
+ |rg_builder, _| {
+ // Create a task to blit from the texture cache to
+ // a normal transient render task surface.
+ // TODO: figure out if/when we can do a blit instead.
+ let cache_to_target_task_id = RenderTask::new_scaling_with_padding(
+ task_id,
+ rg_builder,
+ target_kind,
+ size,
+ padding,
+ );
+
+ // Create a task to blit the rect from the child render
+ // task above back into the right spot in the persistent
+ // render target cache.
+ RenderTask::new_blit(
+ size,
+ cache_to_target_task_id,
+ rg_builder,
+ )
+ }
+ );
+
+ image_instance.src_color = Some(cached_task_handle);
+ }
+ }
+ // Tiled image path.
+ Some(ImageProperties { tiling: Some(tile_size), visible_rect, .. }) => {
+ // we'll have a source handle per visible tile instead.
+ image_instance.src_color = None;
+
+ image_instance.visible_tiles.clear();
+ // TODO: rename the blob's visible_rect into something that doesn't conflict
+ // with the terminology we use during culling since it's not really the same
+ // thing.
+ let active_rect = visible_rect;
+
+ // Tighten the clip rect because decomposing the repeated image can
+ // produce primitives that are partially covering the original image
+ // rect and we want to clip these extra parts out.
+ let tight_clip_rect = visibility
+ .clip_chain
+ .local_clip_rect
+ .intersection(&common.prim_rect).unwrap();
+ image_instance.tight_local_clip_rect = tight_clip_rect;
+
+ let visible_rect = compute_conservative_visible_rect(
+ &visibility.clip_chain,
+ frame_state.current_dirty_region().combined,
+ prim_spatial_node_index,
+ frame_context.spatial_tree,
+ );
+
+ let base_edge_flags = edge_flags_for_tile_spacing(&self.tile_spacing);
+
+ let stride = self.stretch_size + self.tile_spacing;
+
+ // We are performing the decomposition on the CPU here, no need to
+ // have it in the shader.
+ common.may_need_repetition = false;
+
+ let repetitions = image_tiling::repetitions(
+ &common.prim_rect,
+ &visible_rect,
+ stride,
+ );
+
+ for image_tiling::Repetition { origin, edge_flags } in repetitions {
+ let edge_flags = base_edge_flags | edge_flags;
+
+ let layout_image_rect = LayoutRect::from_origin_and_size(
+ origin,
+ self.stretch_size,
+ );
+
+ let tiles = image_tiling::tiles(
+ &layout_image_rect,
+ &visible_rect,
+ &active_rect,
+ tile_size as i32,
+ );
+
+ for tile in tiles {
+ let request = request.with_tile(tile.offset);
+ let size = frame_state.resource_cache.request_image(
+ request,
+ frame_state.gpu_cache,
+ );
+
+ let task_id = frame_state.rg_builder.add().init(
+ RenderTask::new_image(size, request)
+ );
+
+ image_instance.visible_tiles.push(VisibleImageTile {
+ src_color: task_id,
+ edge_flags: tile.edge_flags & edge_flags,
+ local_rect: tile.rect,
+ local_clip_rect: tight_clip_rect,
+ });
+ }
+ }
+
+ if image_instance.visible_tiles.is_empty() {
+ // Mark as invisible
+ visibility.reset();
+ }
+ }
+ None => {
+ image_instance.src_color = None;
+ }
+ }
+
+ if let Some(mut request) = frame_state.gpu_cache.request(&mut common.gpu_cache_handle) {
+ self.write_prim_gpu_blocks(&mut request);
+ }
+ }
+
+ pub fn write_prim_gpu_blocks(&self, request: &mut GpuDataRequest) {
+ // Images are drawn as a white color, modulated by the total
+ // opacity coming from any collapsed property bindings.
+ // Size has to match `VECS_PER_SPECIFIC_BRUSH` from `brush_image.glsl` exactly.
+ request.push(self.color.premultiplied());
+ request.push(PremultipliedColorF::WHITE);
+ request.push([
+ self.stretch_size.width + self.tile_spacing.width,
+ self.stretch_size.height + self.tile_spacing.height,
+ 0.0,
+ 0.0,
+ ]);
+ }
+}
+
+fn edge_flags_for_tile_spacing(tile_spacing: &LayoutSize) -> EdgeAaSegmentMask {
+ let mut flags = EdgeAaSegmentMask::empty();
+
+ if tile_spacing.width > 0.0 {
+ flags |= EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::RIGHT;
+ }
+ if tile_spacing.height > 0.0 {
+ flags |= EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::BOTTOM;
+ }
+
+ flags
+}
+
+pub type ImageTemplate = PrimTemplate<ImageData>;
+
+impl From<ImageKey> for ImageTemplate {
+ fn from(image: ImageKey) -> Self {
+ let common = PrimTemplateCommonData::with_key_common(image.common);
+
+ ImageTemplate {
+ common,
+ kind: image.kind.into(),
+ }
+ }
+}
+
+pub type ImageDataHandle = InternHandle<Image>;
+
+impl Internable for Image {
+ type Key = ImageKey;
+ type StoreData = ImageTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_IMAGES;
+}
+
+impl InternablePrimitive for Image {
+ fn into_key(
+ self,
+ info: &LayoutPrimitiveInfo,
+ ) -> ImageKey {
+ ImageKey::new(info, self)
+ }
+
+ fn make_instance_kind(
+ _key: ImageKey,
+ data_handle: ImageDataHandle,
+ prim_store: &mut PrimitiveStore,
+ _reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind {
+ // TODO(gw): Refactor this to not need a separate image
+ // instance (see ImageInstance struct).
+ let image_instance_index = prim_store.images.push(ImageInstance {
+ segment_instance_index: SegmentInstanceIndex::INVALID,
+ tight_local_clip_rect: LayoutRect::zero(),
+ visible_tiles: Vec::new(),
+ src_color: None,
+ });
+
+ PrimitiveInstanceKind::Image {
+ data_handle,
+ image_instance_index,
+ is_compositor_surface: false,
+ }
+ }
+}
+
+impl CreateShadow for Image {
+ fn create_shadow(
+ &self,
+ shadow: &Shadow,
+ _: bool,
+ _: RasterSpace,
+ ) -> Self {
+ Image {
+ tile_spacing: self.tile_spacing,
+ stretch_size: self.stretch_size,
+ key: self.key,
+ image_rendering: self.image_rendering,
+ alpha_type: self.alpha_type,
+ color: shadow.color.into(),
+ }
+ }
+}
+
+impl IsVisible for Image {
+ fn is_visible(&self) -> bool {
+ true
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
+pub struct YuvImage {
+ pub color_depth: ColorDepth,
+ pub yuv_key: [ApiImageKey; 3],
+ pub format: YuvFormat,
+ pub color_space: YuvColorSpace,
+ pub color_range: ColorRange,
+ pub image_rendering: ImageRendering,
+}
+
+pub type YuvImageKey = PrimKey<YuvImage>;
+
+impl YuvImageKey {
+ pub fn new(
+ info: &LayoutPrimitiveInfo,
+ yuv_image: YuvImage,
+ ) -> Self {
+ YuvImageKey {
+ common: info.into(),
+ kind: yuv_image,
+ }
+ }
+}
+
+impl InternDebug for YuvImageKey {}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct YuvImageData {
+ pub color_depth: ColorDepth,
+ pub yuv_key: [ApiImageKey; 3],
+ pub src_yuv: [Option<RenderTaskId>; 3],
+ pub format: YuvFormat,
+ pub color_space: YuvColorSpace,
+ pub color_range: ColorRange,
+ pub image_rendering: ImageRendering,
+}
+
+impl From<YuvImage> for YuvImageData {
+ fn from(image: YuvImage) -> Self {
+ YuvImageData {
+ color_depth: image.color_depth,
+ yuv_key: image.yuv_key,
+ src_yuv: [None, None, None],
+ format: image.format,
+ color_space: image.color_space,
+ color_range: image.color_range,
+ image_rendering: image.image_rendering,
+ }
+ }
+}
+
+impl YuvImageData {
+ /// Update the GPU cache for a given primitive template. This may be called multiple
+ /// times per frame, by each primitive reference that refers to this interned
+ /// template. The initial request call to the GPU cache ensures that work is only
+ /// done if the cache entry is invalid (due to first use or eviction).
+ pub fn update(
+ &mut self,
+ common: &mut PrimTemplateCommonData,
+ frame_state: &mut FrameBuildingState,
+ ) {
+
+ self.src_yuv = [ None, None, None ];
+
+ let channel_num = self.format.get_plane_num();
+ debug_assert!(channel_num <= 3);
+ for channel in 0 .. channel_num {
+ let request = ImageRequest {
+ key: self.yuv_key[channel],
+ rendering: self.image_rendering,
+ tile: None,
+ };
+
+ let size = frame_state.resource_cache.request_image(
+ request,
+ frame_state.gpu_cache,
+ );
+
+ let task_id = frame_state.rg_builder.add().init(
+ RenderTask::new_image(size, request)
+ );
+
+ self.src_yuv[channel] = Some(task_id);
+ }
+
+ if let Some(mut request) = frame_state.gpu_cache.request(&mut common.gpu_cache_handle) {
+ self.write_prim_gpu_blocks(&mut request);
+ };
+
+ // YUV images never have transparency
+ common.opacity = PrimitiveOpacity::opaque();
+ }
+
+ pub fn request_resources(
+ &mut self,
+ resource_cache: &mut ResourceCache,
+ gpu_cache: &mut GpuCache,
+ ) {
+ let channel_num = self.format.get_plane_num();
+ debug_assert!(channel_num <= 3);
+ for channel in 0 .. channel_num {
+ resource_cache.request_image(
+ ImageRequest {
+ key: self.yuv_key[channel],
+ rendering: self.image_rendering,
+ tile: None,
+ },
+ gpu_cache,
+ );
+ }
+ }
+
+ pub fn write_prim_gpu_blocks(&self, request: &mut GpuDataRequest) {
+ let ranged_color_space = self.color_space.with_range(self.color_range);
+ request.push([
+ pack_as_float(self.color_depth.bit_depth()),
+ pack_as_float(ranged_color_space as u32),
+ pack_as_float(self.format as u32),
+ 0.0
+ ]);
+ }
+}
+
+pub type YuvImageTemplate = PrimTemplate<YuvImageData>;
+
+impl From<YuvImageKey> for YuvImageTemplate {
+ fn from(image: YuvImageKey) -> Self {
+ let common = PrimTemplateCommonData::with_key_common(image.common);
+
+ YuvImageTemplate {
+ common,
+ kind: image.kind.into(),
+ }
+ }
+}
+
+pub type YuvImageDataHandle = InternHandle<YuvImage>;
+
+impl Internable for YuvImage {
+ type Key = YuvImageKey;
+ type StoreData = YuvImageTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_YUV_IMAGES;
+}
+
+impl InternablePrimitive for YuvImage {
+ fn into_key(
+ self,
+ info: &LayoutPrimitiveInfo,
+ ) -> YuvImageKey {
+ YuvImageKey::new(info, self)
+ }
+
+ fn make_instance_kind(
+ _key: YuvImageKey,
+ data_handle: YuvImageDataHandle,
+ _prim_store: &mut PrimitiveStore,
+ _reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind {
+ PrimitiveInstanceKind::YuvImage {
+ data_handle,
+ segment_instance_index: SegmentInstanceIndex::INVALID,
+ is_compositor_surface: false,
+ }
+ }
+}
+
+impl IsVisible for YuvImage {
+ fn is_visible(&self) -> bool {
+ true
+ }
+}
+
+#[test]
+#[cfg(target_pointer_width = "64")]
+fn test_struct_sizes() {
+ use std::mem;
+ // The sizes of these structures are critical for performance on a number of
+ // talos stress tests. If you get a failure here on CI, there's two possibilities:
+ // (a) You made a structure smaller than it currently is. Great work! Update the
+ // test expectations and move on.
+ // (b) You made a structure larger. This is not necessarily a problem, but should only
+ // be done with care, and after checking if talos performance regresses badly.
+ assert_eq!(mem::size_of::<Image>(), 32, "Image size changed");
+ assert_eq!(mem::size_of::<ImageTemplate>(), 72, "ImageTemplate size changed");
+ assert_eq!(mem::size_of::<ImageKey>(), 52, "ImageKey size changed");
+ assert_eq!(mem::size_of::<YuvImage>(), 32, "YuvImage size changed");
+ assert_eq!(mem::size_of::<YuvImageTemplate>(), 84, "YuvImageTemplate size changed");
+ assert_eq!(mem::size_of::<YuvImageKey>(), 52, "YuvImageKey size changed");
+}
diff --git a/gfx/wr/webrender/src/prim_store/interned.rs b/gfx/wr/webrender/src/prim_store/interned.rs
new file mode 100644
index 0000000000..f536b50b09
--- /dev/null
+++ b/gfx/wr/webrender/src/prim_store/interned.rs
@@ -0,0 +1,14 @@
+/* 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/. */
+
+// list of all interned primitives to match enumerate_interners!
+
+pub use crate::prim_store::backdrop::{BackdropCapture, BackdropRender};
+pub use crate::prim_store::borders::{ImageBorder, NormalBorderPrim};
+pub use crate::prim_store::image::{Image, YuvImage};
+pub use crate::prim_store::line_dec::{LineDecoration};
+pub use crate::prim_store::gradient::{LinearGradient, RadialGradient, ConicGradient};
+pub use crate::prim_store::picture::Picture;
+pub use crate::prim_store::text_run::TextRun;
+
diff --git a/gfx/wr/webrender/src/prim_store/line_dec.rs b/gfx/wr/webrender/src/prim_store/line_dec.rs
new file mode 100644
index 0000000000..496bab7569
--- /dev/null
+++ b/gfx/wr/webrender/src/prim_store/line_dec.rs
@@ -0,0 +1,257 @@
+/* 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 api::{
+ ColorF, ColorU, RasterSpace,
+ LineOrientation, LineStyle, PremultipliedColorF, Shadow,
+};
+use api::units::*;
+use crate::scene_building::{CreateShadow, IsVisible};
+use crate::frame_builder::{FrameBuildingState};
+use crate::gpu_cache::GpuDataRequest;
+use crate::intern;
+use crate::internal_types::LayoutPrimitiveInfo;
+use crate::prim_store::{
+ PrimKey, PrimTemplate, PrimTemplateCommonData,
+ InternablePrimitive, PrimitiveStore,
+};
+use crate::prim_store::PrimitiveInstanceKind;
+
+/// Maximum resolution in device pixels at which line decorations are rasterized.
+pub const MAX_LINE_DECORATION_RESOLUTION: u32 = 4096;
+
+#[derive(Clone, Debug, Hash, MallocSizeOf, PartialEq, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct LineDecorationCacheKey {
+ pub style: LineStyle,
+ pub orientation: LineOrientation,
+ pub wavy_line_thickness: Au,
+ pub size: LayoutSizeAu,
+}
+
+/// Identifying key for a line decoration.
+#[derive(Clone, Debug, Hash, MallocSizeOf, PartialEq, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct LineDecoration {
+ // If the cache_key is Some(..) it is a line decoration
+ // that relies on a render task (e.g. wavy). If the
+ // cache key is None, it uses a fast path to draw the
+ // line decoration as a solid rect.
+ pub cache_key: Option<LineDecorationCacheKey>,
+ pub color: ColorU,
+}
+
+pub type LineDecorationKey = PrimKey<LineDecoration>;
+
+impl LineDecorationKey {
+ pub fn new(
+ info: &LayoutPrimitiveInfo,
+ line_dec: LineDecoration,
+ ) -> Self {
+ LineDecorationKey {
+ common: info.into(),
+ kind: line_dec,
+ }
+ }
+}
+
+impl intern::InternDebug for LineDecorationKey {}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct LineDecorationData {
+ pub cache_key: Option<LineDecorationCacheKey>,
+ pub color: ColorF,
+}
+
+impl LineDecorationData {
+ /// Update the GPU cache for a given primitive template. This may be called multiple
+ /// times per frame, by each primitive reference that refers to this interned
+ /// template. The initial request call to the GPU cache ensures that work is only
+ /// done if the cache entry is invalid (due to first use or eviction).
+ pub fn update(
+ &mut self,
+ common: &mut PrimTemplateCommonData,
+ frame_state: &mut FrameBuildingState,
+ ) {
+ if let Some(ref mut request) = frame_state.gpu_cache.request(&mut common.gpu_cache_handle) {
+ self.write_prim_gpu_blocks(request);
+ }
+ }
+
+ fn write_prim_gpu_blocks(
+ &self,
+ request: &mut GpuDataRequest
+ ) {
+ match self.cache_key.as_ref() {
+ Some(cache_key) => {
+ request.push(self.color.premultiplied());
+ request.push(PremultipliedColorF::WHITE);
+ request.push([
+ cache_key.size.width.to_f32_px(),
+ cache_key.size.height.to_f32_px(),
+ 0.0,
+ 0.0,
+ ]);
+ }
+ None => {
+ request.push(self.color.premultiplied());
+ }
+ }
+ }
+}
+
+pub type LineDecorationTemplate = PrimTemplate<LineDecorationData>;
+
+impl From<LineDecorationKey> for LineDecorationTemplate {
+ fn from(line_dec: LineDecorationKey) -> Self {
+ let common = PrimTemplateCommonData::with_key_common(line_dec.common);
+ LineDecorationTemplate {
+ common,
+ kind: LineDecorationData {
+ cache_key: line_dec.kind.cache_key,
+ color: line_dec.kind.color.into(),
+ }
+ }
+ }
+}
+
+pub type LineDecorationDataHandle = intern::Handle<LineDecoration>;
+
+impl intern::Internable for LineDecoration {
+ type Key = LineDecorationKey;
+ type StoreData = LineDecorationTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_LINE_DECORATIONS;
+}
+
+impl InternablePrimitive for LineDecoration {
+ fn into_key(
+ self,
+ info: &LayoutPrimitiveInfo,
+ ) -> LineDecorationKey {
+ LineDecorationKey::new(
+ info,
+ self,
+ )
+ }
+
+ fn make_instance_kind(
+ _key: LineDecorationKey,
+ data_handle: LineDecorationDataHandle,
+ _: &mut PrimitiveStore,
+ _reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind {
+ PrimitiveInstanceKind::LineDecoration {
+ data_handle,
+ render_task: None,
+ }
+ }
+}
+
+impl CreateShadow for LineDecoration {
+ fn create_shadow(
+ &self,
+ shadow: &Shadow,
+ _: bool,
+ _: RasterSpace,
+ ) -> Self {
+ LineDecoration {
+ color: shadow.color.into(),
+ cache_key: self.cache_key.clone(),
+ }
+ }
+}
+
+impl IsVisible for LineDecoration {
+ fn is_visible(&self) -> bool {
+ self.color.a > 0
+ }
+}
+
+/// Choose the decoration mask tile size for a given line.
+///
+/// Given a line with overall size `rect_size` and the given `orientation`,
+/// return the dimensions of a single mask tile for the decoration pattern
+/// described by `style` and `wavy_line_thickness`.
+///
+/// If `style` is `Solid`, no mask tile is necessary; return `None`. The other
+/// styles each have their own characteristic periods of repetition, so for each
+/// one, this function returns a `LayoutSize` with the right aspect ratio and
+/// whose specific size is convenient for the `cs_line_decoration.glsl` fragment
+/// shader to work with. The shader uses a local coordinate space in which the
+/// tile fills a rectangle with one corner at the origin, and with the size this
+/// function returns.
+///
+/// The returned size is not necessarily in pixels; device scaling and other
+/// concerns can still affect the actual task size.
+///
+/// Regardless of whether `orientation` is `Vertical` or `Horizontal`, the
+/// `width` and `height` of the returned size are always horizontal and
+/// vertical, respectively.
+pub fn get_line_decoration_size(
+ rect_size: &LayoutSize,
+ orientation: LineOrientation,
+ style: LineStyle,
+ wavy_line_thickness: f32,
+) -> Option<LayoutSize> {
+ let h = match orientation {
+ LineOrientation::Horizontal => rect_size.height,
+ LineOrientation::Vertical => rect_size.width,
+ };
+
+ // TODO(gw): The formulae below are based on the existing gecko and line
+ // shader code. They give reasonable results for most inputs,
+ // but could definitely do with a detailed pass to get better
+ // quality on a wider range of inputs!
+ // See nsCSSRendering::PaintDecorationLine in Gecko.
+
+ let (parallel, perpendicular) = match style {
+ LineStyle::Solid => {
+ return None;
+ }
+ LineStyle::Dashed => {
+ let dash_length = (3.0 * h).min(64.0).max(1.0);
+
+ (2.0 * dash_length, 4.0)
+ }
+ LineStyle::Dotted => {
+ let diameter = h.min(64.0).max(1.0);
+ let period = 2.0 * diameter;
+
+ (period, diameter)
+ }
+ LineStyle::Wavy => {
+ let line_thickness = wavy_line_thickness.max(1.0);
+ let slope_length = h - line_thickness;
+ let flat_length = ((line_thickness - 1.0) * 2.0).max(1.0);
+ let approx_period = 2.0 * (slope_length + flat_length);
+
+ (approx_period, h)
+ }
+ };
+
+ Some(match orientation {
+ LineOrientation::Horizontal => LayoutSize::new(parallel, perpendicular),
+ LineOrientation::Vertical => LayoutSize::new(perpendicular, parallel),
+ })
+}
+
+#[test]
+#[cfg(target_pointer_width = "64")]
+fn test_struct_sizes() {
+ use std::mem;
+ // The sizes of these structures are critical for performance on a number of
+ // talos stress tests. If you get a failure here on CI, there's two possibilities:
+ // (a) You made a structure smaller than it currently is. Great work! Update the
+ // test expectations and move on.
+ // (b) You made a structure larger. This is not necessarily a problem, but should only
+ // be done with care, and after checking if talos performance regresses badly.
+ assert_eq!(mem::size_of::<LineDecoration>(), 20, "LineDecoration size changed");
+ assert_eq!(mem::size_of::<LineDecorationTemplate>(), 60, "LineDecorationTemplate size changed");
+ assert_eq!(mem::size_of::<LineDecorationKey>(), 40, "LineDecorationKey size changed");
+}
diff --git a/gfx/wr/webrender/src/prim_store/mod.rs b/gfx/wr/webrender/src/prim_store/mod.rs
new file mode 100644
index 0000000000..a1dcf56ac7
--- /dev/null
+++ b/gfx/wr/webrender/src/prim_store/mod.rs
@@ -0,0 +1,1455 @@
+/* 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 api::{BorderRadius, ClipMode, ColorF, ColorU, RasterSpace};
+use api::{ImageRendering, RepeatMode, PrimitiveFlags};
+use api::{PremultipliedColorF, PropertyBinding, Shadow};
+use api::{PrimitiveKeyKind, FillRule, POLYGON_CLIP_VERTEX_MAX};
+use api::units::*;
+use euclid::{SideOffsets2D, Size2D};
+use malloc_size_of::MallocSizeOf;
+use crate::clip::ClipLeafId;
+use crate::segment::EdgeAaSegmentMask;
+use crate::border::BorderSegmentCacheKey;
+use crate::debug_item::{DebugItem, DebugMessage};
+use crate::debug_colors;
+use crate::scene_building::{CreateShadow, IsVisible};
+use crate::frame_builder::FrameBuildingState;
+use glyph_rasterizer::GlyphKey;
+use crate::gpu_cache::{GpuCacheAddress, GpuCacheHandle, GpuDataRequest};
+use crate::gpu_types::{BrushFlags, QuadSegment};
+use crate::intern;
+use crate::picture::PicturePrimitive;
+use crate::render_task_graph::RenderTaskId;
+use crate::resource_cache::ImageProperties;
+use crate::scene::SceneProperties;
+use std::{hash, ops, u32, usize};
+use crate::util::Recycler;
+use crate::internal_types::{FastHashSet, LayoutPrimitiveInfo};
+use crate::visibility::PrimitiveVisibility;
+
+pub mod backdrop;
+pub mod borders;
+pub mod gradient;
+pub mod image;
+pub mod line_dec;
+pub mod picture;
+pub mod text_run;
+pub mod interned;
+
+mod storage;
+
+use backdrop::{BackdropCaptureDataHandle, BackdropRenderDataHandle};
+use borders::{ImageBorderDataHandle, NormalBorderDataHandle};
+use gradient::{LinearGradientPrimitive, LinearGradientDataHandle, RadialGradientDataHandle, ConicGradientDataHandle};
+use image::{ImageDataHandle, ImageInstance, YuvImageDataHandle};
+use line_dec::LineDecorationDataHandle;
+use picture::PictureDataHandle;
+use text_run::{TextRunDataHandle, TextRunPrimitive};
+
+pub const VECS_PER_SEGMENT: usize = 2;
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, MallocSizeOf)]
+pub struct PrimitiveOpacity {
+ pub is_opaque: bool,
+}
+
+impl PrimitiveOpacity {
+ pub fn opaque() -> PrimitiveOpacity {
+ PrimitiveOpacity { is_opaque: true }
+ }
+
+ pub fn translucent() -> PrimitiveOpacity {
+ PrimitiveOpacity { is_opaque: false }
+ }
+
+ pub fn from_alpha(alpha: f32) -> PrimitiveOpacity {
+ PrimitiveOpacity {
+ is_opaque: alpha >= 1.0,
+ }
+ }
+}
+
+/// For external images, it's not possible to know the
+/// UV coords of the image (or the image data itself)
+/// until the render thread receives the frame and issues
+/// callbacks to the client application. For external
+/// images that are visible, a DeferredResolve is created
+/// that is stored in the frame. This allows the render
+/// thread to iterate this list and update any changed
+/// texture data and update the UV rect. Any filtering
+/// is handled externally for NativeTexture external
+/// images.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct DeferredResolve {
+ pub address: GpuCacheAddress,
+ pub image_properties: ImageProperties,
+ pub rendering: ImageRendering,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct ClipTaskIndex(pub u32);
+
+impl ClipTaskIndex {
+ pub const INVALID: ClipTaskIndex = ClipTaskIndex(0);
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, MallocSizeOf, Ord, PartialOrd)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PictureIndex(pub usize);
+
+impl PictureIndex {
+ pub const INVALID: PictureIndex = PictureIndex(!0);
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Copy, Debug, Clone, MallocSizeOf, PartialEq)]
+pub struct RectangleKey {
+ pub x0: f32,
+ pub y0: f32,
+ pub x1: f32,
+ pub y1: f32,
+}
+
+impl RectangleKey {
+ pub fn intersects(&self, other: &Self) -> bool {
+ self.x0 < other.x1
+ && other.x0 < self.x1
+ && self.y0 < other.y1
+ && other.y0 < self.y1
+ }
+}
+
+impl Eq for RectangleKey {}
+
+impl hash::Hash for RectangleKey {
+ fn hash<H: hash::Hasher>(&self, state: &mut H) {
+ self.x0.to_bits().hash(state);
+ self.y0.to_bits().hash(state);
+ self.x1.to_bits().hash(state);
+ self.y1.to_bits().hash(state);
+ }
+}
+
+impl From<RectangleKey> for LayoutRect {
+ fn from(key: RectangleKey) -> LayoutRect {
+ LayoutRect {
+ min: LayoutPoint::new(key.x0, key.y0),
+ max: LayoutPoint::new(key.x1, key.y1),
+ }
+ }
+}
+
+impl From<RectangleKey> for WorldRect {
+ fn from(key: RectangleKey) -> WorldRect {
+ WorldRect {
+ min: WorldPoint::new(key.x0, key.y0),
+ max: WorldPoint::new(key.x1, key.y1),
+ }
+ }
+}
+
+impl From<LayoutRect> for RectangleKey {
+ fn from(rect: LayoutRect) -> RectangleKey {
+ RectangleKey {
+ x0: rect.min.x,
+ y0: rect.min.y,
+ x1: rect.max.x,
+ y1: rect.max.y,
+ }
+ }
+}
+
+impl From<PictureRect> for RectangleKey {
+ fn from(rect: PictureRect) -> RectangleKey {
+ RectangleKey {
+ x0: rect.min.x,
+ y0: rect.min.y,
+ x1: rect.max.x,
+ y1: rect.max.y,
+ }
+ }
+}
+
+impl From<WorldRect> for RectangleKey {
+ fn from(rect: WorldRect) -> RectangleKey {
+ RectangleKey {
+ x0: rect.min.x,
+ y0: rect.min.y,
+ x1: rect.max.x,
+ y1: rect.max.y,
+ }
+ }
+}
+
+/// To create a fixed-size representation of a polygon, we use a fixed
+/// number of points. Our initialization method restricts us to values
+/// <= 32. If our constant POLYGON_CLIP_VERTEX_MAX is > 32, the Rust
+/// compiler will complain.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Copy, Debug, Clone, Hash, MallocSizeOf, PartialEq)]
+pub struct PolygonKey {
+ pub point_count: u8,
+ pub points: [PointKey; POLYGON_CLIP_VERTEX_MAX],
+ pub fill_rule: FillRule,
+}
+
+impl PolygonKey {
+ pub fn new(
+ points_layout: &Vec<LayoutPoint>,
+ fill_rule: FillRule,
+ ) -> Self {
+ // We have to fill fixed-size arrays with data from a Vec.
+ // We'll do this by initializing the arrays to known-good
+ // values then overwriting those values as long as our
+ // iterator provides values.
+ let mut points: [PointKey; POLYGON_CLIP_VERTEX_MAX] = [PointKey { x: 0.0, y: 0.0}; POLYGON_CLIP_VERTEX_MAX];
+
+ let mut point_count: u8 = 0;
+ for (src, dest) in points_layout.iter().zip(points.iter_mut()) {
+ *dest = (*src as LayoutPoint).into();
+ point_count = point_count + 1;
+ }
+
+ PolygonKey {
+ point_count,
+ points,
+ fill_rule,
+ }
+ }
+}
+
+impl Eq for PolygonKey {}
+
+/// A hashable SideOffset2D that can be used in primitive keys.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, MallocSizeOf, PartialEq)]
+pub struct SideOffsetsKey {
+ pub top: f32,
+ pub right: f32,
+ pub bottom: f32,
+ pub left: f32,
+}
+
+impl Eq for SideOffsetsKey {}
+
+impl hash::Hash for SideOffsetsKey {
+ fn hash<H: hash::Hasher>(&self, state: &mut H) {
+ self.top.to_bits().hash(state);
+ self.right.to_bits().hash(state);
+ self.bottom.to_bits().hash(state);
+ self.left.to_bits().hash(state);
+ }
+}
+
+impl From<SideOffsetsKey> for LayoutSideOffsets {
+ fn from(key: SideOffsetsKey) -> LayoutSideOffsets {
+ LayoutSideOffsets::new(
+ key.top,
+ key.right,
+ key.bottom,
+ key.left,
+ )
+ }
+}
+
+impl<U> From<SideOffsets2D<f32, U>> for SideOffsetsKey {
+ fn from(offsets: SideOffsets2D<f32, U>) -> SideOffsetsKey {
+ SideOffsetsKey {
+ top: offsets.top,
+ right: offsets.right,
+ bottom: offsets.bottom,
+ left: offsets.left,
+ }
+ }
+}
+
+/// A hashable size for using as a key during primitive interning.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Copy, Debug, Clone, MallocSizeOf, PartialEq)]
+pub struct SizeKey {
+ w: f32,
+ h: f32,
+}
+
+impl Eq for SizeKey {}
+
+impl hash::Hash for SizeKey {
+ fn hash<H: hash::Hasher>(&self, state: &mut H) {
+ self.w.to_bits().hash(state);
+ self.h.to_bits().hash(state);
+ }
+}
+
+impl From<SizeKey> for LayoutSize {
+ fn from(key: SizeKey) -> LayoutSize {
+ LayoutSize::new(key.w, key.h)
+ }
+}
+
+impl<U> From<Size2D<f32, U>> for SizeKey {
+ fn from(size: Size2D<f32, U>) -> SizeKey {
+ SizeKey {
+ w: size.width,
+ h: size.height,
+ }
+ }
+}
+
+/// A hashable vec for using as a key during primitive interning.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Copy, Debug, Clone, MallocSizeOf, PartialEq)]
+pub struct VectorKey {
+ pub x: f32,
+ pub y: f32,
+}
+
+impl Eq for VectorKey {}
+
+impl hash::Hash for VectorKey {
+ fn hash<H: hash::Hasher>(&self, state: &mut H) {
+ self.x.to_bits().hash(state);
+ self.y.to_bits().hash(state);
+ }
+}
+
+impl From<VectorKey> for LayoutVector2D {
+ fn from(key: VectorKey) -> LayoutVector2D {
+ LayoutVector2D::new(key.x, key.y)
+ }
+}
+
+impl From<VectorKey> for WorldVector2D {
+ fn from(key: VectorKey) -> WorldVector2D {
+ WorldVector2D::new(key.x, key.y)
+ }
+}
+
+impl From<LayoutVector2D> for VectorKey {
+ fn from(vec: LayoutVector2D) -> VectorKey {
+ VectorKey {
+ x: vec.x,
+ y: vec.y,
+ }
+ }
+}
+
+impl From<WorldVector2D> for VectorKey {
+ fn from(vec: WorldVector2D) -> VectorKey {
+ VectorKey {
+ x: vec.x,
+ y: vec.y,
+ }
+ }
+}
+
+/// A hashable point for using as a key during primitive interning.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, MallocSizeOf, PartialEq)]
+pub struct PointKey {
+ pub x: f32,
+ pub y: f32,
+}
+
+impl Eq for PointKey {}
+
+impl hash::Hash for PointKey {
+ fn hash<H: hash::Hasher>(&self, state: &mut H) {
+ self.x.to_bits().hash(state);
+ self.y.to_bits().hash(state);
+ }
+}
+
+impl From<PointKey> for LayoutPoint {
+ fn from(key: PointKey) -> LayoutPoint {
+ LayoutPoint::new(key.x, key.y)
+ }
+}
+
+impl From<LayoutPoint> for PointKey {
+ fn from(p: LayoutPoint) -> PointKey {
+ PointKey {
+ x: p.x,
+ y: p.y,
+ }
+ }
+}
+
+impl From<PicturePoint> for PointKey {
+ fn from(p: PicturePoint) -> PointKey {
+ PointKey {
+ x: p.x,
+ y: p.y,
+ }
+ }
+}
+
+impl From<WorldPoint> for PointKey {
+ fn from(p: WorldPoint) -> PointKey {
+ PointKey {
+ x: p.x,
+ y: p.y,
+ }
+ }
+}
+
+/// A hashable float for using as a key during primitive interning.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, MallocSizeOf, PartialEq)]
+pub struct FloatKey(f32);
+
+impl Eq for FloatKey {}
+
+impl hash::Hash for FloatKey {
+ fn hash<H: hash::Hasher>(&self, state: &mut H) {
+ self.0.to_bits().hash(state);
+ }
+}
+
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
+pub struct PrimKeyCommonData {
+ pub flags: PrimitiveFlags,
+ pub prim_rect: RectangleKey,
+}
+
+impl From<&LayoutPrimitiveInfo> for PrimKeyCommonData {
+ fn from(info: &LayoutPrimitiveInfo) -> Self {
+ PrimKeyCommonData {
+ flags: info.flags,
+ prim_rect: info.rect.into(),
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
+pub struct PrimKey<T: MallocSizeOf> {
+ pub common: PrimKeyCommonData,
+ pub kind: T,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
+pub struct PrimitiveKey {
+ pub common: PrimKeyCommonData,
+ pub kind: PrimitiveKeyKind,
+}
+
+impl PrimitiveKey {
+ pub fn new(
+ info: &LayoutPrimitiveInfo,
+ kind: PrimitiveKeyKind,
+ ) -> Self {
+ PrimitiveKey {
+ common: info.into(),
+ kind,
+ }
+ }
+}
+
+impl intern::InternDebug for PrimitiveKey {}
+
+/// The shared information for a given primitive. This is interned and retained
+/// both across frames and display lists, by comparing the matching PrimitiveKey.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub enum PrimitiveTemplateKind {
+ Rectangle {
+ color: PropertyBinding<ColorF>,
+ },
+ Clear,
+}
+
+impl PrimitiveTemplateKind {
+ /// Write any GPU blocks for the primitive template to the given request object.
+ pub fn write_prim_gpu_blocks(
+ &self,
+ request: &mut GpuDataRequest,
+ scene_properties: &SceneProperties,
+ ) {
+ match *self {
+ PrimitiveTemplateKind::Clear => {
+ // Opaque black with operator dest out
+ request.push(PremultipliedColorF::BLACK);
+ }
+ PrimitiveTemplateKind::Rectangle { ref color, .. } => {
+ request.push(scene_properties.resolve_color(color).premultiplied())
+ }
+ }
+ }
+}
+
+/// Construct the primitive template data from a primitive key. This
+/// is invoked when a primitive key is created and the interner
+/// doesn't currently contain a primitive with this key.
+impl From<PrimitiveKeyKind> for PrimitiveTemplateKind {
+ fn from(kind: PrimitiveKeyKind) -> Self {
+ match kind {
+ PrimitiveKeyKind::Clear => {
+ PrimitiveTemplateKind::Clear
+ }
+ PrimitiveKeyKind::Rectangle { color, .. } => {
+ PrimitiveTemplateKind::Rectangle {
+ color: color.into(),
+ }
+ }
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+#[derive(Debug)]
+pub struct PrimTemplateCommonData {
+ pub flags: PrimitiveFlags,
+ pub may_need_repetition: bool,
+ pub prim_rect: LayoutRect,
+ pub opacity: PrimitiveOpacity,
+ /// The GPU cache handle for a primitive template. Since this structure
+ /// is retained across display lists by interning, this GPU cache handle
+ /// also remains valid, which reduces the number of updates to the GPU
+ /// cache when a new display list is processed.
+ pub gpu_cache_handle: GpuCacheHandle,
+ /// Specifies the edges that are *allowed* to have anti-aliasing.
+ /// In other words EdgeAaSegmentFlags::all() does not necessarily mean all edges will
+ /// be anti-aliased, only that they could be.
+ ///
+ /// Use this to force disable anti-alasing on edges of the primitives.
+ pub edge_aa_mask: EdgeAaSegmentMask,
+}
+
+impl PrimTemplateCommonData {
+ pub fn with_key_common(common: PrimKeyCommonData) -> Self {
+ PrimTemplateCommonData {
+ flags: common.flags,
+ may_need_repetition: true,
+ prim_rect: common.prim_rect.into(),
+ gpu_cache_handle: GpuCacheHandle::new(),
+ opacity: PrimitiveOpacity::translucent(),
+ edge_aa_mask: EdgeAaSegmentMask::all(),
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct PrimTemplate<T> {
+ pub common: PrimTemplateCommonData,
+ pub kind: T,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct PrimitiveTemplate {
+ pub common: PrimTemplateCommonData,
+ pub kind: PrimitiveTemplateKind,
+}
+
+impl ops::Deref for PrimitiveTemplate {
+ type Target = PrimTemplateCommonData;
+ fn deref(&self) -> &Self::Target {
+ &self.common
+ }
+}
+
+impl ops::DerefMut for PrimitiveTemplate {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.common
+ }
+}
+
+impl From<PrimitiveKey> for PrimitiveTemplate {
+ fn from(item: PrimitiveKey) -> Self {
+ PrimitiveTemplate {
+ common: PrimTemplateCommonData::with_key_common(item.common),
+ kind: item.kind.into(),
+ }
+ }
+}
+
+impl PrimitiveTemplate {
+ /// Update the GPU cache for a given primitive template. This may be called multiple
+ /// times per frame, by each primitive reference that refers to this interned
+ /// template. The initial request call to the GPU cache ensures that work is only
+ /// done if the cache entry is invalid (due to first use or eviction).
+ pub fn update(
+ &mut self,
+ frame_state: &mut FrameBuildingState,
+ scene_properties: &SceneProperties,
+ ) {
+ if let Some(mut request) = frame_state.gpu_cache.request(&mut self.common.gpu_cache_handle) {
+ self.kind.write_prim_gpu_blocks(&mut request, scene_properties);
+ }
+
+ self.opacity = match self.kind {
+ PrimitiveTemplateKind::Clear => {
+ PrimitiveOpacity::translucent()
+ }
+ PrimitiveTemplateKind::Rectangle { ref color, .. } => {
+ PrimitiveOpacity::from_alpha(scene_properties.resolve_color(color).a)
+ }
+ };
+ }
+}
+
+type PrimitiveDataHandle = intern::Handle<PrimitiveKeyKind>;
+
+impl intern::Internable for PrimitiveKeyKind {
+ type Key = PrimitiveKey;
+ type StoreData = PrimitiveTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_PRIMITIVES;
+}
+
+impl InternablePrimitive for PrimitiveKeyKind {
+ fn into_key(
+ self,
+ info: &LayoutPrimitiveInfo,
+ ) -> PrimitiveKey {
+ PrimitiveKey::new(info, self)
+ }
+
+ fn make_instance_kind(
+ key: PrimitiveKey,
+ data_handle: PrimitiveDataHandle,
+ prim_store: &mut PrimitiveStore,
+ _reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind {
+ match key.kind {
+ PrimitiveKeyKind::Clear => {
+ PrimitiveInstanceKind::Clear {
+ data_handle
+ }
+ }
+ PrimitiveKeyKind::Rectangle { color, .. } => {
+ let color_binding_index = match color {
+ PropertyBinding::Binding(..) => {
+ prim_store.color_bindings.push(color)
+ }
+ PropertyBinding::Value(..) => ColorBindingIndex::INVALID,
+ };
+ PrimitiveInstanceKind::Rectangle {
+ data_handle,
+ segment_instance_index: SegmentInstanceIndex::INVALID,
+ color_binding_index,
+ use_legacy_path: false,
+ }
+ }
+ }
+ }
+}
+
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct VisibleMaskImageTile {
+ pub tile_offset: TileOffset,
+ pub tile_rect: LayoutRect,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct VisibleGradientTile {
+ pub handle: GpuCacheHandle,
+ pub local_rect: LayoutRect,
+ pub local_clip_rect: LayoutRect,
+}
+
+/// Information about how to cache a border segment,
+/// along with the current render task cache entry.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, MallocSizeOf)]
+pub struct BorderSegmentInfo {
+ pub local_task_size: LayoutSize,
+ pub cache_key: BorderSegmentCacheKey,
+}
+
+/// Represents the visibility state of a segment (wrt clip masks).
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[derive(Debug, Clone)]
+pub enum ClipMaskKind {
+ /// The segment has a clip mask, specified by the render task.
+ Mask(RenderTaskId),
+ /// The segment has no clip mask.
+ None,
+ /// The segment is made invisible / clipped completely.
+ Clipped,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, MallocSizeOf)]
+pub struct BrushSegment {
+ pub local_rect: LayoutRect,
+ pub may_need_clip_mask: bool,
+ pub edge_flags: EdgeAaSegmentMask,
+ pub extra_data: [f32; 4],
+ pub brush_flags: BrushFlags,
+}
+
+impl BrushSegment {
+ pub fn new(
+ local_rect: LayoutRect,
+ may_need_clip_mask: bool,
+ edge_flags: EdgeAaSegmentMask,
+ extra_data: [f32; 4],
+ brush_flags: BrushFlags,
+ ) -> Self {
+ Self {
+ local_rect,
+ may_need_clip_mask,
+ edge_flags,
+ extra_data,
+ brush_flags,
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct ClipRect {
+ rect: LayoutRect,
+ mode: f32,
+}
+
+#[derive(Debug, Clone)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct ClipCorner {
+ rect: LayoutRect,
+ outer_radius_x: f32,
+ outer_radius_y: f32,
+ inner_radius_x: f32,
+ inner_radius_y: f32,
+}
+
+impl ClipCorner {
+ fn uniform(rect: LayoutRect, outer_radius: f32, inner_radius: f32) -> ClipCorner {
+ ClipCorner {
+ rect,
+ outer_radius_x: outer_radius,
+ outer_radius_y: outer_radius,
+ inner_radius_x: inner_radius,
+ inner_radius_y: inner_radius,
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ClipData {
+ rect: ClipRect,
+ top_left: ClipCorner,
+ top_right: ClipCorner,
+ bottom_left: ClipCorner,
+ bottom_right: ClipCorner,
+}
+
+impl ClipData {
+ pub fn rounded_rect(size: LayoutSize, radii: &BorderRadius, mode: ClipMode) -> ClipData {
+ // TODO(gw): For simplicity, keep most of the clip GPU structs the
+ // same as they were, even though the origin is now always
+ // zero, since they are in the clip's local space. In future,
+ // we could reduce the GPU cache size of ClipData.
+ let rect = LayoutRect::from_size(size);
+
+ ClipData {
+ rect: ClipRect {
+ rect,
+ mode: mode as u32 as f32,
+ },
+ top_left: ClipCorner {
+ rect: LayoutRect::from_origin_and_size(
+ LayoutPoint::new(rect.min.x, rect.min.y),
+ LayoutSize::new(radii.top_left.width, radii.top_left.height),
+ ),
+ outer_radius_x: radii.top_left.width,
+ outer_radius_y: radii.top_left.height,
+ inner_radius_x: 0.0,
+ inner_radius_y: 0.0,
+ },
+ top_right: ClipCorner {
+ rect: LayoutRect::from_origin_and_size(
+ LayoutPoint::new(
+ rect.max.x - radii.top_right.width,
+ rect.min.y,
+ ),
+ LayoutSize::new(radii.top_right.width, radii.top_right.height),
+ ),
+ outer_radius_x: radii.top_right.width,
+ outer_radius_y: radii.top_right.height,
+ inner_radius_x: 0.0,
+ inner_radius_y: 0.0,
+ },
+ bottom_left: ClipCorner {
+ rect: LayoutRect::from_origin_and_size(
+ LayoutPoint::new(
+ rect.min.x,
+ rect.max.y - radii.bottom_left.height,
+ ),
+ LayoutSize::new(radii.bottom_left.width, radii.bottom_left.height),
+ ),
+ outer_radius_x: radii.bottom_left.width,
+ outer_radius_y: radii.bottom_left.height,
+ inner_radius_x: 0.0,
+ inner_radius_y: 0.0,
+ },
+ bottom_right: ClipCorner {
+ rect: LayoutRect::from_origin_and_size(
+ LayoutPoint::new(
+ rect.max.x - radii.bottom_right.width,
+ rect.max.y - radii.bottom_right.height,
+ ),
+ LayoutSize::new(radii.bottom_right.width, radii.bottom_right.height),
+ ),
+ outer_radius_x: radii.bottom_right.width,
+ outer_radius_y: radii.bottom_right.height,
+ inner_radius_x: 0.0,
+ inner_radius_y: 0.0,
+ },
+ }
+ }
+
+ pub fn uniform(size: LayoutSize, radius: f32, mode: ClipMode) -> ClipData {
+ // TODO(gw): For simplicity, keep most of the clip GPU structs the
+ // same as they were, even though the origin is now always
+ // zero, since they are in the clip's local space. In future,
+ // we could reduce the GPU cache size of ClipData.
+ let rect = LayoutRect::from_size(size);
+
+ ClipData {
+ rect: ClipRect {
+ rect,
+ mode: mode as u32 as f32,
+ },
+ top_left: ClipCorner::uniform(
+ LayoutRect::from_origin_and_size(
+ LayoutPoint::new(rect.min.x, rect.min.y),
+ LayoutSize::new(radius, radius),
+ ),
+ radius,
+ 0.0,
+ ),
+ top_right: ClipCorner::uniform(
+ LayoutRect::from_origin_and_size(
+ LayoutPoint::new(rect.max.x - radius, rect.min.y),
+ LayoutSize::new(radius, radius),
+ ),
+ radius,
+ 0.0,
+ ),
+ bottom_left: ClipCorner::uniform(
+ LayoutRect::from_origin_and_size(
+ LayoutPoint::new(rect.min.x, rect.max.y - radius),
+ LayoutSize::new(radius, radius),
+ ),
+ radius,
+ 0.0,
+ ),
+ bottom_right: ClipCorner::uniform(
+ LayoutRect::from_origin_and_size(
+ LayoutPoint::new(
+ rect.max.x - radius,
+ rect.max.y - radius,
+ ),
+ LayoutSize::new(radius, radius),
+ ),
+ radius,
+ 0.0,
+ ),
+ }
+ }
+}
+
+/// A hashable descriptor for nine-patches, used by image and
+/// gradient borders.
+#[derive(Debug, Clone, PartialEq, Eq, Hash, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct NinePatchDescriptor {
+ pub width: i32,
+ pub height: i32,
+ pub slice: DeviceIntSideOffsets,
+ pub fill: bool,
+ pub repeat_horizontal: RepeatMode,
+ pub repeat_vertical: RepeatMode,
+ pub widths: SideOffsetsKey,
+}
+
+impl IsVisible for PrimitiveKeyKind {
+ // Return true if the primary primitive is visible.
+ // Used to trivially reject non-visible primitives.
+ // TODO(gw): Currently, primitives other than those
+ // listed here are handled before the
+ // add_primitive() call. In the future
+ // we should move the logic for all other
+ // primitive types to use this.
+ fn is_visible(&self) -> bool {
+ match *self {
+ PrimitiveKeyKind::Clear => {
+ true
+ }
+ PrimitiveKeyKind::Rectangle { ref color, .. } => {
+ match *color {
+ PropertyBinding::Value(value) => value.a > 0,
+ PropertyBinding::Binding(..) => true,
+ }
+ }
+ }
+ }
+}
+
+impl CreateShadow for PrimitiveKeyKind {
+ // Create a clone of this PrimitiveContainer, applying whatever
+ // changes are necessary to the primitive to support rendering
+ // it as part of the supplied shadow.
+ fn create_shadow(
+ &self,
+ shadow: &Shadow,
+ _: bool,
+ _: RasterSpace,
+ ) -> PrimitiveKeyKind {
+ match *self {
+ PrimitiveKeyKind::Rectangle { .. } => {
+ PrimitiveKeyKind::Rectangle {
+ color: PropertyBinding::Value(shadow.color.into()),
+ }
+ }
+ PrimitiveKeyKind::Clear => {
+ panic!("bug: this prim is not supported in shadow contexts");
+ }
+ }
+ }
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub enum PrimitiveInstanceKind {
+ /// Direct reference to a Picture
+ Picture {
+ /// Handle to the common interned data for this primitive.
+ data_handle: PictureDataHandle,
+ pic_index: PictureIndex,
+ segment_instance_index: SegmentInstanceIndex,
+ },
+ /// A run of glyphs, with associated font parameters.
+ TextRun {
+ /// Handle to the common interned data for this primitive.
+ data_handle: TextRunDataHandle,
+ /// Index to the per instance scratch data for this primitive.
+ run_index: TextRunIndex,
+ },
+ /// A line decoration. cache_handle refers to a cached render
+ /// task handle, if this line decoration is not a simple solid.
+ LineDecoration {
+ /// Handle to the common interned data for this primitive.
+ data_handle: LineDecorationDataHandle,
+ // TODO(gw): For now, we need to store some information in
+ // the primitive instance that is created during
+ // prepare_prims and read during the batching pass.
+ // Once we unify the prepare_prims and batching to
+ // occur at the same time, we can remove most of
+ // the things we store here in the instance, and
+ // use them directly. This will remove cache_handle,
+ // but also the opacity, clip_task_id etc below.
+ render_task: Option<RenderTaskId>,
+ },
+ NormalBorder {
+ /// Handle to the common interned data for this primitive.
+ data_handle: NormalBorderDataHandle,
+ render_task_ids: storage::Range<RenderTaskId>,
+ },
+ ImageBorder {
+ /// Handle to the common interned data for this primitive.
+ data_handle: ImageBorderDataHandle,
+ },
+ Rectangle {
+ /// Handle to the common interned data for this primitive.
+ data_handle: PrimitiveDataHandle,
+ segment_instance_index: SegmentInstanceIndex,
+ color_binding_index: ColorBindingIndex,
+ use_legacy_path: bool,
+ },
+ YuvImage {
+ /// Handle to the common interned data for this primitive.
+ data_handle: YuvImageDataHandle,
+ segment_instance_index: SegmentInstanceIndex,
+ is_compositor_surface: bool,
+ },
+ Image {
+ /// Handle to the common interned data for this primitive.
+ data_handle: ImageDataHandle,
+ image_instance_index: ImageInstanceIndex,
+ is_compositor_surface: bool,
+ },
+ /// Always rendered directly into the picture. This tends to be
+ /// faster with SWGL.
+ LinearGradient {
+ /// Handle to the common interned data for this primitive.
+ data_handle: LinearGradientDataHandle,
+ visible_tiles_range: GradientTileRange,
+ },
+ /// Always rendered via a cached render task. Usually faster with
+ /// a GPU.
+ CachedLinearGradient {
+ /// Handle to the common interned data for this primitive.
+ data_handle: LinearGradientDataHandle,
+ visible_tiles_range: GradientTileRange,
+ },
+ RadialGradient {
+ /// Handle to the common interned data for this primitive.
+ data_handle: RadialGradientDataHandle,
+ visible_tiles_range: GradientTileRange,
+ },
+ ConicGradient {
+ /// Handle to the common interned data for this primitive.
+ data_handle: ConicGradientDataHandle,
+ visible_tiles_range: GradientTileRange,
+ },
+ /// Clear out a rect, used for special effects.
+ Clear {
+ /// Handle to the common interned data for this primitive.
+ data_handle: PrimitiveDataHandle,
+ },
+ /// Render a portion of a specified backdrop.
+ BackdropCapture {
+ data_handle: BackdropCaptureDataHandle,
+ },
+ BackdropRender {
+ data_handle: BackdropRenderDataHandle,
+ pic_index: PictureIndex,
+ },
+}
+
+impl PrimitiveInstanceKind {
+ pub fn as_pic(&self) -> PictureIndex {
+ match self {
+ PrimitiveInstanceKind::Picture { pic_index, .. } => *pic_index,
+ _ => panic!("bug: as_pic called on a prim that is not a picture"),
+ }
+ }
+}
+
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PrimitiveInstanceIndex(pub u32);
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct PrimitiveInstance {
+ /// Identifies the kind of primitive this
+ /// instance is, and references to where
+ /// the relevant information for the primitive
+ /// can be found.
+ pub kind: PrimitiveInstanceKind,
+
+ /// All information and state related to clip(s) for this primitive
+ pub clip_leaf_id: ClipLeafId,
+
+ /// Information related to the current visibility state of this
+ /// primitive.
+ // TODO(gw): Currently built each frame, but can be retained.
+ pub vis: PrimitiveVisibility,
+}
+
+impl PrimitiveInstance {
+ pub fn new(
+ kind: PrimitiveInstanceKind,
+ clip_leaf_id: ClipLeafId,
+ ) -> Self {
+ PrimitiveInstance {
+ kind,
+ vis: PrimitiveVisibility::new(),
+ clip_leaf_id,
+ }
+ }
+
+ // Reset any pre-frame state for this primitive.
+ pub fn reset(&mut self) {
+ self.vis.reset();
+ }
+
+ pub fn clear_visibility(&mut self) {
+ self.vis.reset();
+ }
+
+ pub fn uid(&self) -> intern::ItemUid {
+ match &self.kind {
+ PrimitiveInstanceKind::Clear { data_handle, .. } |
+ PrimitiveInstanceKind::Rectangle { data_handle, .. } => {
+ data_handle.uid()
+ }
+ PrimitiveInstanceKind::Image { data_handle, .. } => {
+ data_handle.uid()
+ }
+ PrimitiveInstanceKind::ImageBorder { data_handle, .. } => {
+ data_handle.uid()
+ }
+ PrimitiveInstanceKind::LineDecoration { data_handle, .. } => {
+ data_handle.uid()
+ }
+ PrimitiveInstanceKind::LinearGradient { data_handle, .. } => {
+ data_handle.uid()
+ }
+ PrimitiveInstanceKind::CachedLinearGradient { data_handle, .. } => {
+ data_handle.uid()
+ }
+ PrimitiveInstanceKind::NormalBorder { data_handle, .. } => {
+ data_handle.uid()
+ }
+ PrimitiveInstanceKind::Picture { data_handle, .. } => {
+ data_handle.uid()
+ }
+ PrimitiveInstanceKind::RadialGradient { data_handle, .. } => {
+ data_handle.uid()
+ }
+ PrimitiveInstanceKind::ConicGradient { data_handle, .. } => {
+ data_handle.uid()
+ }
+ PrimitiveInstanceKind::TextRun { data_handle, .. } => {
+ data_handle.uid()
+ }
+ PrimitiveInstanceKind::YuvImage { data_handle, .. } => {
+ data_handle.uid()
+ }
+ PrimitiveInstanceKind::BackdropCapture { data_handle, .. } => {
+ data_handle.uid()
+ }
+ PrimitiveInstanceKind::BackdropRender { data_handle, .. } => {
+ data_handle.uid()
+ }
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[derive(Debug)]
+pub struct SegmentedInstance {
+ pub gpu_cache_handle: GpuCacheHandle,
+ pub segments_range: SegmentsRange,
+}
+
+pub type GlyphKeyStorage = storage::Storage<GlyphKey>;
+pub type TextRunIndex = storage::Index<TextRunPrimitive>;
+pub type TextRunStorage = storage::Storage<TextRunPrimitive>;
+pub type ColorBindingIndex = storage::Index<PropertyBinding<ColorU>>;
+pub type ColorBindingStorage = storage::Storage<PropertyBinding<ColorU>>;
+pub type BorderHandleStorage = storage::Storage<RenderTaskId>;
+pub type SegmentStorage = storage::Storage<BrushSegment>;
+pub type SegmentsRange = storage::Range<BrushSegment>;
+pub type SegmentInstanceStorage = storage::Storage<SegmentedInstance>;
+pub type SegmentInstanceIndex = storage::Index<SegmentedInstance>;
+pub type ImageInstanceStorage = storage::Storage<ImageInstance>;
+pub type ImageInstanceIndex = storage::Index<ImageInstance>;
+pub type GradientTileStorage = storage::Storage<VisibleGradientTile>;
+pub type GradientTileRange = storage::Range<VisibleGradientTile>;
+pub type LinearGradientStorage = storage::Storage<LinearGradientPrimitive>;
+
+/// Contains various vecs of data that is used only during frame building,
+/// where we want to recycle the memory each new display list, to avoid constantly
+/// re-allocating and moving memory around. Written during primitive preparation,
+/// and read during batching.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct PrimitiveScratchBuffer {
+ /// Contains a list of clip mask instance parameters
+ /// per segment generated.
+ pub clip_mask_instances: Vec<ClipMaskKind>,
+
+ /// List of glyphs keys that are allocated by each
+ /// text run instance.
+ pub glyph_keys: GlyphKeyStorage,
+
+ /// List of render task handles for border segment instances
+ /// that have been added this frame.
+ pub border_cache_handles: BorderHandleStorage,
+
+ /// A list of brush segments that have been built for this scene.
+ pub segments: SegmentStorage,
+
+ /// A list of segment ranges and GPU cache handles for prim instances
+ /// that have opted into segment building. In future, this should be
+ /// removed in favor of segment building during primitive interning.
+ pub segment_instances: SegmentInstanceStorage,
+
+ /// A list of visible tiles that tiled gradients use to store
+ /// per-tile information.
+ pub gradient_tiles: GradientTileStorage,
+
+ /// List of debug display items for rendering.
+ pub debug_items: Vec<DebugItem>,
+
+ /// List of current debug messages to log on screen
+ messages: Vec<DebugMessage>,
+
+ /// Set of sub-graphs that are required, determined during visibility pass
+ pub required_sub_graphs: FastHashSet<PictureIndex>,
+
+ /// Temporary buffer for building segments in to during prepare pass
+ pub quad_segments: Vec<QuadSegment>,
+}
+
+impl Default for PrimitiveScratchBuffer {
+ fn default() -> Self {
+ PrimitiveScratchBuffer {
+ clip_mask_instances: Vec::new(),
+ glyph_keys: GlyphKeyStorage::new(0),
+ border_cache_handles: BorderHandleStorage::new(0),
+ segments: SegmentStorage::new(0),
+ segment_instances: SegmentInstanceStorage::new(0),
+ gradient_tiles: GradientTileStorage::new(0),
+ debug_items: Vec::new(),
+ messages: Vec::new(),
+ required_sub_graphs: FastHashSet::default(),
+ quad_segments: Vec::new(),
+ }
+ }
+}
+
+impl PrimitiveScratchBuffer {
+ pub fn recycle(&mut self, recycler: &mut Recycler) {
+ recycler.recycle_vec(&mut self.clip_mask_instances);
+ self.glyph_keys.recycle(recycler);
+ self.border_cache_handles.recycle(recycler);
+ self.segments.recycle(recycler);
+ self.segment_instances.recycle(recycler);
+ self.gradient_tiles.recycle(recycler);
+ recycler.recycle_vec(&mut self.debug_items);
+ recycler.recycle_vec(&mut self.quad_segments);
+ }
+
+ pub fn begin_frame(&mut self) {
+ // Clear the clip mask tasks for the beginning of the frame. Append
+ // a single kind representing no clip mask, at the ClipTaskIndex::INVALID
+ // location.
+ self.clip_mask_instances.clear();
+ self.clip_mask_instances.push(ClipMaskKind::None);
+ self.quad_segments.clear();
+
+ self.border_cache_handles.clear();
+
+ // TODO(gw): As in the previous code, the gradient tiles store GPU cache
+ // handles that are cleared (and thus invalidated + re-uploaded)
+ // every frame. This maintains the existing behavior, but we
+ // should fix this in the future to retain handles.
+ self.gradient_tiles.clear();
+
+ self.required_sub_graphs.clear();
+
+ self.debug_items.clear();
+ }
+
+ pub fn end_frame(&mut self) {
+ const MSGS_TO_RETAIN: usize = 32;
+ const TIME_TO_RETAIN: u64 = 2000000000;
+ const LINE_HEIGHT: f32 = 20.0;
+ const X0: f32 = 32.0;
+ const Y0: f32 = 32.0;
+ let now = time::precise_time_ns();
+
+ let msgs_to_remove = self.messages.len().max(MSGS_TO_RETAIN) - MSGS_TO_RETAIN;
+ let mut msgs_removed = 0;
+
+ self.messages.retain(|msg| {
+ if msgs_removed < msgs_to_remove {
+ msgs_removed += 1;
+ return false;
+ }
+
+ if msg.timestamp + TIME_TO_RETAIN < now {
+ return false;
+ }
+
+ true
+ });
+
+ let mut y = Y0 + self.messages.len() as f32 * LINE_HEIGHT;
+ let shadow_offset = 1.0;
+
+ for msg in &self.messages {
+ self.debug_items.push(DebugItem::Text {
+ position: DevicePoint::new(X0 + shadow_offset, y + shadow_offset),
+ color: debug_colors::BLACK,
+ msg: msg.msg.clone(),
+ });
+
+ self.debug_items.push(DebugItem::Text {
+ position: DevicePoint::new(X0, y),
+ color: debug_colors::RED,
+ msg: msg.msg.clone(),
+ });
+
+ y -= LINE_HEIGHT;
+ }
+ }
+
+ #[allow(dead_code)]
+ pub fn push_debug_rect(
+ &mut self,
+ rect: DeviceRect,
+ outer_color: ColorF,
+ inner_color: ColorF,
+ ) {
+ self.debug_items.push(DebugItem::Rect {
+ rect,
+ outer_color,
+ inner_color,
+ });
+ }
+
+ #[allow(dead_code)]
+ pub fn push_debug_string(
+ &mut self,
+ position: DevicePoint,
+ color: ColorF,
+ msg: String,
+ ) {
+ self.debug_items.push(DebugItem::Text {
+ position,
+ color,
+ msg,
+ });
+ }
+
+ #[allow(dead_code)]
+ pub fn log(
+ &mut self,
+ msg: String,
+ ) {
+ self.messages.push(DebugMessage {
+ msg,
+ timestamp: time::precise_time_ns(),
+ })
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Clone, Debug)]
+pub struct PrimitiveStoreStats {
+ picture_count: usize,
+ text_run_count: usize,
+ image_count: usize,
+ linear_gradient_count: usize,
+ color_binding_count: usize,
+}
+
+impl PrimitiveStoreStats {
+ pub fn empty() -> Self {
+ PrimitiveStoreStats {
+ picture_count: 0,
+ text_run_count: 0,
+ image_count: 0,
+ linear_gradient_count: 0,
+ color_binding_count: 0,
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct PrimitiveStore {
+ pub pictures: Vec<PicturePrimitive>,
+ pub text_runs: TextRunStorage,
+ pub linear_gradients: LinearGradientStorage,
+
+ /// A list of image instances. These are stored separately as
+ /// storing them inline in the instance makes the structure bigger
+ /// for other types.
+ pub images: ImageInstanceStorage,
+
+ /// animated color bindings for this primitive.
+ pub color_bindings: ColorBindingStorage,
+}
+
+impl PrimitiveStore {
+ pub fn new(stats: &PrimitiveStoreStats) -> PrimitiveStore {
+ PrimitiveStore {
+ pictures: Vec::with_capacity(stats.picture_count),
+ text_runs: TextRunStorage::new(stats.text_run_count),
+ images: ImageInstanceStorage::new(stats.image_count),
+ color_bindings: ColorBindingStorage::new(stats.color_binding_count),
+ linear_gradients: LinearGradientStorage::new(stats.linear_gradient_count),
+ }
+ }
+
+ pub fn get_stats(&self) -> PrimitiveStoreStats {
+ PrimitiveStoreStats {
+ picture_count: self.pictures.len(),
+ text_run_count: self.text_runs.len(),
+ image_count: self.images.len(),
+ linear_gradient_count: self.linear_gradients.len(),
+ color_binding_count: self.color_bindings.len(),
+ }
+ }
+
+ #[allow(unused)]
+ pub fn print_picture_tree(&self, root: PictureIndex) {
+ use crate::print_tree::PrintTree;
+ let mut pt = PrintTree::new("picture tree");
+ self.pictures[root.0].print(&self.pictures, root, &mut pt);
+ }
+}
+
+/// Trait for primitives that are directly internable.
+/// see SceneBuilder::add_primitive<P>
+pub trait InternablePrimitive: intern::Internable<InternData = ()> + Sized {
+ /// Build a new key from self with `info`.
+ fn into_key(
+ self,
+ info: &LayoutPrimitiveInfo,
+ ) -> Self::Key;
+
+ fn make_instance_kind(
+ key: Self::Key,
+ data_handle: intern::Handle<Self>,
+ prim_store: &mut PrimitiveStore,
+ reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind;
+}
+
+
+#[test]
+#[cfg(target_pointer_width = "64")]
+fn test_struct_sizes() {
+ use std::mem;
+ // The sizes of these structures are critical for performance on a number of
+ // talos stress tests. If you get a failure here on CI, there's two possibilities:
+ // (a) You made a structure smaller than it currently is. Great work! Update the
+ // test expectations and move on.
+ // (b) You made a structure larger. This is not necessarily a problem, but should only
+ // be done with care, and after checking if talos performance regresses badly.
+ assert_eq!(mem::size_of::<PrimitiveInstance>(), 88, "PrimitiveInstance size changed");
+ assert_eq!(mem::size_of::<PrimitiveInstanceKind>(), 24, "PrimitiveInstanceKind size changed");
+ assert_eq!(mem::size_of::<PrimitiveTemplate>(), 56, "PrimitiveTemplate size changed");
+ assert_eq!(mem::size_of::<PrimitiveTemplateKind>(), 28, "PrimitiveTemplateKind size changed");
+ assert_eq!(mem::size_of::<PrimitiveKey>(), 36, "PrimitiveKey size changed");
+ assert_eq!(mem::size_of::<PrimitiveKeyKind>(), 16, "PrimitiveKeyKind size changed");
+}
diff --git a/gfx/wr/webrender/src/prim_store/picture.rs b/gfx/wr/webrender/src/prim_store/picture.rs
new file mode 100644
index 0000000000..c3ec88783a
--- /dev/null
+++ b/gfx/wr/webrender/src/prim_store/picture.rs
@@ -0,0 +1,328 @@
+/* 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 api::{
+ ColorU, MixBlendMode, FilterPrimitiveInput, FilterPrimitiveKind, ColorSpace,
+ PropertyBinding, PropertyBindingId, CompositeOperator, RasterSpace,
+};
+use api::units::{Au, LayoutVector2D};
+use crate::scene_building::IsVisible;
+use crate::filterdata::SFilterData;
+use crate::intern::ItemUid;
+use crate::intern::{Internable, InternDebug, Handle as InternHandle};
+use crate::internal_types::{LayoutPrimitiveInfo, Filter};
+use crate::picture::PictureCompositeMode;
+use crate::prim_store::{
+ PrimitiveInstanceKind, PrimitiveStore, VectorKey,
+ InternablePrimitive,
+};
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, MallocSizeOf, PartialEq, Hash, Eq)]
+pub enum CompositeOperatorKey {
+ Over,
+ In,
+ Out,
+ Atop,
+ Xor,
+ Lighter,
+ Arithmetic([Au; 4]),
+}
+
+impl From<CompositeOperator> for CompositeOperatorKey {
+ fn from(operator: CompositeOperator) -> Self {
+ match operator {
+ CompositeOperator::Over => CompositeOperatorKey::Over,
+ CompositeOperator::In => CompositeOperatorKey::In,
+ CompositeOperator::Out => CompositeOperatorKey::Out,
+ CompositeOperator::Atop => CompositeOperatorKey::Atop,
+ CompositeOperator::Xor => CompositeOperatorKey::Xor,
+ CompositeOperator::Lighter => CompositeOperatorKey::Lighter,
+ CompositeOperator::Arithmetic(k_vals) => {
+ let k_vals = [
+ Au::from_f32_px(k_vals[0]),
+ Au::from_f32_px(k_vals[1]),
+ Au::from_f32_px(k_vals[2]),
+ Au::from_f32_px(k_vals[3]),
+ ];
+ CompositeOperatorKey::Arithmetic(k_vals)
+ }
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, MallocSizeOf, PartialEq, Hash, Eq)]
+pub enum FilterPrimitiveKey {
+ Identity(ColorSpace, FilterPrimitiveInput),
+ Flood(ColorSpace, ColorU),
+ Blend(ColorSpace, MixBlendMode, FilterPrimitiveInput, FilterPrimitiveInput),
+ Blur(ColorSpace, Au, Au, FilterPrimitiveInput),
+ Opacity(ColorSpace, Au, FilterPrimitiveInput),
+ ColorMatrix(ColorSpace, [Au; 20], FilterPrimitiveInput),
+ DropShadow(ColorSpace, (VectorKey, Au, ColorU), FilterPrimitiveInput),
+ ComponentTransfer(ColorSpace, FilterPrimitiveInput, Vec<SFilterData>),
+ Offset(ColorSpace, FilterPrimitiveInput, VectorKey),
+ Composite(ColorSpace, FilterPrimitiveInput, FilterPrimitiveInput, CompositeOperatorKey),
+}
+
+/// Represents a hashable description of how a picture primitive
+/// will be composited into its parent.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, MallocSizeOf, PartialEq, Hash, Eq)]
+pub enum PictureCompositeKey {
+ // No visual compositing effect
+ Identity,
+
+ // FilterOp
+ Blur(Au, Au, bool),
+ Brightness(Au),
+ Contrast(Au),
+ Grayscale(Au),
+ HueRotate(Au),
+ Invert(Au),
+ Opacity(Au),
+ OpacityBinding(PropertyBindingId, Au),
+ Saturate(Au),
+ Sepia(Au),
+ DropShadows(Vec<(VectorKey, Au, ColorU)>),
+ ColorMatrix([Au; 20]),
+ SrgbToLinear,
+ LinearToSrgb,
+ ComponentTransfer(ItemUid),
+ Flood(ColorU),
+ SvgFilter(Vec<FilterPrimitiveKey>),
+
+ // MixBlendMode
+ Multiply,
+ Screen,
+ Overlay,
+ Darken,
+ Lighten,
+ ColorDodge,
+ ColorBurn,
+ HardLight,
+ SoftLight,
+ Difference,
+ Exclusion,
+ Hue,
+ Saturation,
+ Color,
+ Luminosity,
+ PlusLighter,
+}
+
+impl From<Option<PictureCompositeMode>> for PictureCompositeKey {
+ fn from(mode: Option<PictureCompositeMode>) -> Self {
+ match mode {
+ Some(PictureCompositeMode::MixBlend(mode)) => {
+ match mode {
+ MixBlendMode::Normal => PictureCompositeKey::Identity,
+ MixBlendMode::Multiply => PictureCompositeKey::Multiply,
+ MixBlendMode::Screen => PictureCompositeKey::Screen,
+ MixBlendMode::Overlay => PictureCompositeKey::Overlay,
+ MixBlendMode::Darken => PictureCompositeKey::Darken,
+ MixBlendMode::Lighten => PictureCompositeKey::Lighten,
+ MixBlendMode::ColorDodge => PictureCompositeKey::ColorDodge,
+ MixBlendMode::ColorBurn => PictureCompositeKey::ColorBurn,
+ MixBlendMode::HardLight => PictureCompositeKey::HardLight,
+ MixBlendMode::SoftLight => PictureCompositeKey::SoftLight,
+ MixBlendMode::Difference => PictureCompositeKey::Difference,
+ MixBlendMode::Exclusion => PictureCompositeKey::Exclusion,
+ MixBlendMode::Hue => PictureCompositeKey::Hue,
+ MixBlendMode::Saturation => PictureCompositeKey::Saturation,
+ MixBlendMode::Color => PictureCompositeKey::Color,
+ MixBlendMode::Luminosity => PictureCompositeKey::Luminosity,
+ MixBlendMode::PlusLighter => PictureCompositeKey::PlusLighter,
+ }
+ }
+ Some(PictureCompositeMode::Filter(op)) => {
+ match op {
+ Filter::Blur { width, height, should_inflate } =>
+ PictureCompositeKey::Blur(Au::from_f32_px(width), Au::from_f32_px(height), should_inflate),
+ Filter::Brightness(value) => PictureCompositeKey::Brightness(Au::from_f32_px(value)),
+ Filter::Contrast(value) => PictureCompositeKey::Contrast(Au::from_f32_px(value)),
+ Filter::Grayscale(value) => PictureCompositeKey::Grayscale(Au::from_f32_px(value)),
+ Filter::HueRotate(value) => PictureCompositeKey::HueRotate(Au::from_f32_px(value)),
+ Filter::Invert(value) => PictureCompositeKey::Invert(Au::from_f32_px(value)),
+ Filter::Saturate(value) => PictureCompositeKey::Saturate(Au::from_f32_px(value)),
+ Filter::Sepia(value) => PictureCompositeKey::Sepia(Au::from_f32_px(value)),
+ Filter::SrgbToLinear => PictureCompositeKey::SrgbToLinear,
+ Filter::LinearToSrgb => PictureCompositeKey::LinearToSrgb,
+ Filter::Identity => PictureCompositeKey::Identity,
+ Filter::DropShadows(ref shadows) => {
+ PictureCompositeKey::DropShadows(
+ shadows.iter().map(|shadow| {
+ (shadow.offset.into(), Au::from_f32_px(shadow.blur_radius), shadow.color.into())
+ }).collect()
+ )
+ }
+ Filter::Opacity(binding, _) => {
+ match binding {
+ PropertyBinding::Value(value) => {
+ PictureCompositeKey::Opacity(Au::from_f32_px(value))
+ }
+ PropertyBinding::Binding(key, default) => {
+ PictureCompositeKey::OpacityBinding(key.id, Au::from_f32_px(default))
+ }
+ }
+ }
+ Filter::ColorMatrix(values) => {
+ let mut quantized_values: [Au; 20] = [Au(0); 20];
+ for (value, result) in values.iter().zip(quantized_values.iter_mut()) {
+ *result = Au::from_f32_px(*value);
+ }
+ PictureCompositeKey::ColorMatrix(quantized_values)
+ }
+ Filter::ComponentTransfer => unreachable!(),
+ Filter::Flood(color) => PictureCompositeKey::Flood(color.into()),
+ }
+ }
+ Some(PictureCompositeMode::ComponentTransferFilter(handle)) => {
+ PictureCompositeKey::ComponentTransfer(handle.uid())
+ }
+ Some(PictureCompositeMode::SvgFilter(filter_primitives, filter_data)) => {
+ PictureCompositeKey::SvgFilter(filter_primitives.into_iter().map(|primitive| {
+ match primitive.kind {
+ FilterPrimitiveKind::Identity(identity) => FilterPrimitiveKey::Identity(primitive.color_space, identity.input),
+ FilterPrimitiveKind::Blend(blend) => FilterPrimitiveKey::Blend(primitive.color_space, blend.mode, blend.input1, blend.input2),
+ FilterPrimitiveKind::Flood(flood) => FilterPrimitiveKey::Flood(primitive.color_space, flood.color.into()),
+ FilterPrimitiveKind::Blur(blur) =>
+ FilterPrimitiveKey::Blur(primitive.color_space, Au::from_f32_px(blur.width), Au::from_f32_px(blur.height), blur.input),
+ FilterPrimitiveKind::Opacity(opacity) =>
+ FilterPrimitiveKey::Opacity(primitive.color_space, Au::from_f32_px(opacity.opacity), opacity.input),
+ FilterPrimitiveKind::ColorMatrix(color_matrix) => {
+ let mut quantized_values: [Au; 20] = [Au(0); 20];
+ for (value, result) in color_matrix.matrix.iter().zip(quantized_values.iter_mut()) {
+ *result = Au::from_f32_px(*value);
+ }
+ FilterPrimitiveKey::ColorMatrix(primitive.color_space, quantized_values, color_matrix.input)
+ }
+ FilterPrimitiveKind::DropShadow(drop_shadow) => {
+ FilterPrimitiveKey::DropShadow(
+ primitive.color_space,
+ (
+ drop_shadow.shadow.offset.into(),
+ Au::from_f32_px(drop_shadow.shadow.blur_radius),
+ drop_shadow.shadow.color.into(),
+ ),
+ drop_shadow.input,
+ )
+ }
+ FilterPrimitiveKind::ComponentTransfer(component_transfer) =>
+ FilterPrimitiveKey::ComponentTransfer(primitive.color_space, component_transfer.input, filter_data.clone()),
+ FilterPrimitiveKind::Offset(info) =>
+ FilterPrimitiveKey::Offset(primitive.color_space, info.input, info.offset.into()),
+ FilterPrimitiveKind::Composite(info) =>
+ FilterPrimitiveKey::Composite(primitive.color_space, info.input1, info.input2, info.operator.into()),
+ }
+ }).collect())
+ }
+ Some(PictureCompositeMode::Blit(_)) |
+ Some(PictureCompositeMode::TileCache { .. }) |
+ Some(PictureCompositeMode::IntermediateSurface) |
+ None => {
+ PictureCompositeKey::Identity
+ }
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
+pub struct Picture {
+ pub composite_mode_key: PictureCompositeKey,
+ pub raster_space: RasterSpace,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
+pub struct PictureKey {
+ pub composite_mode_key: PictureCompositeKey,
+ pub raster_space: RasterSpace,
+}
+
+impl PictureKey {
+ pub fn new(
+ pic: Picture,
+ ) -> Self {
+ PictureKey {
+ composite_mode_key: pic.composite_mode_key,
+ raster_space: pic.raster_space,
+ }
+ }
+}
+
+impl InternDebug for PictureKey {}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct PictureData;
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct PictureTemplate;
+
+impl From<PictureKey> for PictureTemplate {
+ fn from(_: PictureKey) -> Self {
+ PictureTemplate
+ }
+}
+
+pub type PictureDataHandle = InternHandle<Picture>;
+
+impl Internable for Picture {
+ type Key = PictureKey;
+ type StoreData = PictureTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_PICTURES;
+}
+
+impl InternablePrimitive for Picture {
+ fn into_key(
+ self,
+ _: &LayoutPrimitiveInfo,
+ ) -> PictureKey {
+ PictureKey::new(self)
+ }
+
+ fn make_instance_kind(
+ _key: PictureKey,
+ _: PictureDataHandle,
+ _: &mut PrimitiveStore,
+ _reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind {
+ // Should never be hit as this method should not be
+ // called for pictures.
+ unreachable!();
+ }
+}
+
+impl IsVisible for Picture {
+ fn is_visible(&self) -> bool {
+ true
+ }
+}
+
+#[test]
+#[cfg(target_pointer_width = "64")]
+fn test_struct_sizes() {
+ use std::mem;
+ // The sizes of these structures are critical for performance on a number of
+ // talos stress tests. If you get a failure here on CI, there's two possibilities:
+ // (a) You made a structure smaller than it currently is. Great work! Update the
+ // test expectations and move on.
+ // (b) You made a structure larger. This is not necessarily a problem, but should only
+ // be done with care, and after checking if talos performance regresses badly.
+ assert_eq!(mem::size_of::<Picture>(), 96, "Picture size changed");
+ assert_eq!(mem::size_of::<PictureTemplate>(), 0, "PictureTemplate size changed");
+ assert_eq!(mem::size_of::<PictureKey>(), 96, "PictureKey size changed");
+}
diff --git a/gfx/wr/webrender/src/prim_store/storage.rs b/gfx/wr/webrender/src/prim_store/storage.rs
new file mode 100644
index 0000000000..4b99d87556
--- /dev/null
+++ b/gfx/wr/webrender/src/prim_store/storage.rs
@@ -0,0 +1,156 @@
+/* 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::{iter::Extend, ops, marker::PhantomData, u32};
+use crate::util::Recycler;
+
+#[derive(Debug, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct Index<T>(u32, PhantomData<T>);
+
+// We explicitly implement Copy + Clone instead of using #[derive(Copy, Clone)]
+// because we don't want to require that T implements Clone + Copy.
+impl<T> Clone for Index<T> {
+ fn clone(&self) -> Self { *self }
+}
+
+impl<T> Copy for Index<T> {}
+
+impl<T> PartialEq for Index<T> {
+ fn eq(&self, other: &Self) -> bool {
+ self.0 == other.0
+ }
+}
+
+impl<T> Index<T> {
+ fn new(idx: usize) -> Self {
+ debug_assert!(idx < u32::max_value() as usize);
+ Index(idx as u32, PhantomData)
+ }
+
+ pub const INVALID: Index<T> = Index(u32::MAX, PhantomData);
+ pub const UNUSED: Index<T> = Index(u32::MAX-1, PhantomData);
+}
+
+#[derive(Debug)]
+pub struct OpenRange<T> {
+ start: Index<T>,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct Range<T> {
+ pub start: Index<T>,
+ pub end: Index<T>,
+}
+
+// We explicitly implement Copy + Clone instead of using #[derive(Copy, Clone)]
+// because we don't want to require that T implements Clone + Copy.
+impl<T> Clone for Range<T> {
+ fn clone(&self) -> Self {
+ Range { start: self.start, end: self.end }
+ }
+}
+impl<T> Copy for Range<T> {}
+
+impl<T> Range<T> {
+ /// Create an empty `Range`
+ pub fn empty() -> Self {
+ Range {
+ start: Index::new(0),
+ end: Index::new(0),
+ }
+ }
+
+ /// Check for an empty `Range`
+ pub fn is_empty(self) -> bool {
+ self.start.0 >= self.end.0
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct Storage<T> {
+ data: Vec<T>,
+}
+
+impl<T> Storage<T> {
+ pub fn new(initial_capacity: usize) -> Self {
+ Storage {
+ data: Vec::with_capacity(initial_capacity),
+ }
+ }
+
+ pub fn len(&self) -> usize {
+ self.data.len()
+ }
+
+ pub fn clear(&mut self) {
+ self.data.clear();
+ }
+
+ pub fn push(&mut self, t: T) -> Index<T> {
+ let index = self.data.len();
+ self.data.push(t);
+ Index(index as u32, PhantomData)
+ }
+
+ pub fn reserve(&mut self, count: usize) {
+ self.data.reserve(count);
+ }
+
+ pub fn recycle(&mut self, recycler: &mut Recycler) {
+ recycler.recycle_vec(&mut self.data);
+ }
+
+ pub fn extend<II: IntoIterator<Item=T>>(&mut self, iter: II) -> Range<T> {
+ let range = self.open_range();
+ self.data.extend(iter);
+
+ self.close_range(range)
+ }
+
+ pub fn open_range(&self) -> OpenRange<T> {
+ OpenRange {
+ start: Index::new(self.data.len())
+ }
+ }
+
+ pub fn close_range(&self, range: OpenRange<T>) -> Range<T> {
+ Range {
+ start: range.start,
+ end: Index::new(self.data.len()),
+ }
+ }
+}
+
+impl<T> ops::Index<Index<T>> for Storage<T> {
+ type Output = T;
+ fn index(&self, index: Index<T>) -> &Self::Output {
+ &self.data[index.0 as usize]
+ }
+}
+
+impl<T> ops::IndexMut<Index<T>> for Storage<T> {
+ fn index_mut(&mut self, index: Index<T>) -> &mut Self::Output {
+ &mut self.data[index.0 as usize]
+ }
+}
+
+impl<T> ops::Index<Range<T>> for Storage<T> {
+ type Output = [T];
+ fn index(&self, index: Range<T>) -> &Self::Output {
+ let start = index.start.0 as _;
+ let end = index.end.0 as _;
+ &self.data[start..end]
+ }
+}
+
+impl<T> ops::IndexMut<Range<T>> for Storage<T> {
+ fn index_mut(&mut self, index: Range<T>) -> &mut Self::Output {
+ let start = index.start.0 as _;
+ let end = index.end.0 as _;
+ &mut self.data[start..end]
+ }
+}
diff --git a/gfx/wr/webrender/src/prim_store/text_run.rs b/gfx/wr/webrender/src/prim_store/text_run.rs
new file mode 100644
index 0000000000..61562306be
--- /dev/null
+++ b/gfx/wr/webrender/src/prim_store/text_run.rs
@@ -0,0 +1,505 @@
+/* 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 api::{ColorF, FontInstanceFlags, GlyphInstance, RasterSpace, Shadow};
+use api::units::{LayoutToWorldTransform, LayoutVector2D, RasterPixelScale, DevicePixelScale};
+use crate::scene_building::{CreateShadow, IsVisible};
+use crate::frame_builder::FrameBuildingState;
+use glyph_rasterizer::{FontInstance, FontTransform, GlyphKey, FONT_SIZE_LIMIT};
+use crate::gpu_cache::GpuCache;
+use crate::intern;
+use crate::internal_types::LayoutPrimitiveInfo;
+use crate::picture::SurfaceInfo;
+use crate::prim_store::{PrimitiveOpacity, PrimitiveScratchBuffer};
+use crate::prim_store::{PrimitiveStore, PrimKeyCommonData, PrimTemplateCommonData};
+use crate::renderer::{MAX_VERTEX_TEXTURE_WIDTH};
+use crate::resource_cache::{ResourceCache};
+use crate::util::{MatrixHelpers};
+use crate::prim_store::{InternablePrimitive, PrimitiveInstanceKind};
+use crate::spatial_tree::{SpatialTree, SpatialNodeIndex};
+use crate::space::SpaceSnapper;
+use crate::util::PrimaryArc;
+
+use std::ops;
+use std::sync::Arc;
+
+use super::storage;
+
+/// A run of glyphs, with associated font information.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
+pub struct TextRunKey {
+ pub common: PrimKeyCommonData,
+ pub font: FontInstance,
+ pub glyphs: PrimaryArc<Vec<GlyphInstance>>,
+ pub shadow: bool,
+ pub requested_raster_space: RasterSpace,
+}
+
+impl TextRunKey {
+ pub fn new(
+ info: &LayoutPrimitiveInfo,
+ text_run: TextRun,
+ ) -> Self {
+ TextRunKey {
+ common: info.into(),
+ font: text_run.font,
+ glyphs: PrimaryArc(text_run.glyphs),
+ shadow: text_run.shadow,
+ requested_raster_space: text_run.requested_raster_space,
+ }
+ }
+}
+
+impl intern::InternDebug for TextRunKey {}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct TextRunTemplate {
+ pub common: PrimTemplateCommonData,
+ pub font: FontInstance,
+ #[ignore_malloc_size_of = "Measured via PrimaryArc"]
+ pub glyphs: Arc<Vec<GlyphInstance>>,
+}
+
+impl ops::Deref for TextRunTemplate {
+ type Target = PrimTemplateCommonData;
+ fn deref(&self) -> &Self::Target {
+ &self.common
+ }
+}
+
+impl ops::DerefMut for TextRunTemplate {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.common
+ }
+}
+
+impl From<TextRunKey> for TextRunTemplate {
+ fn from(item: TextRunKey) -> Self {
+ let common = PrimTemplateCommonData::with_key_common(item.common);
+ TextRunTemplate {
+ common,
+ font: item.font,
+ glyphs: item.glyphs.0,
+ }
+ }
+}
+
+impl TextRunTemplate {
+ /// Update the GPU cache for a given primitive template. This may be called multiple
+ /// times per frame, by each primitive reference that refers to this interned
+ /// template. The initial request call to the GPU cache ensures that work is only
+ /// done if the cache entry is invalid (due to first use or eviction).
+ pub fn update(
+ &mut self,
+ frame_state: &mut FrameBuildingState,
+ ) {
+ self.write_prim_gpu_blocks(frame_state);
+ self.opacity = PrimitiveOpacity::translucent();
+ }
+
+ fn write_prim_gpu_blocks(
+ &mut self,
+ frame_state: &mut FrameBuildingState,
+ ) {
+ // corresponds to `fetch_glyph` in the shaders
+ if let Some(mut request) = frame_state.gpu_cache.request(&mut self.common.gpu_cache_handle) {
+ request.push(ColorF::from(self.font.color).premultiplied());
+ // this is the only case where we need to provide plain color to GPU
+ let bg_color = ColorF::from(self.font.bg_color);
+ request.push([bg_color.r, bg_color.g, bg_color.b, 1.0]);
+
+ let mut gpu_block = [0.0; 4];
+ for (i, src) in self.glyphs.iter().enumerate() {
+ // Two glyphs are packed per GPU block.
+
+ if (i & 1) == 0 {
+ gpu_block[0] = src.point.x;
+ gpu_block[1] = src.point.y;
+ } else {
+ gpu_block[2] = src.point.x;
+ gpu_block[3] = src.point.y;
+ request.push(gpu_block);
+ }
+ }
+
+ // Ensure the last block is added in the case
+ // of an odd number of glyphs.
+ if (self.glyphs.len() & 1) != 0 {
+ request.push(gpu_block);
+ }
+
+ assert!(request.current_used_block_num() <= MAX_VERTEX_TEXTURE_WIDTH);
+ }
+ }
+}
+
+pub type TextRunDataHandle = intern::Handle<TextRun>;
+
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct TextRun {
+ pub font: FontInstance,
+ #[ignore_malloc_size_of = "Measured via PrimaryArc"]
+ pub glyphs: Arc<Vec<GlyphInstance>>,
+ pub shadow: bool,
+ pub requested_raster_space: RasterSpace,
+}
+
+impl intern::Internable for TextRun {
+ type Key = TextRunKey;
+ type StoreData = TextRunTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_TEXT_RUNS;
+}
+
+impl InternablePrimitive for TextRun {
+ fn into_key(
+ self,
+ info: &LayoutPrimitiveInfo,
+ ) -> TextRunKey {
+ TextRunKey::new(
+ info,
+ self,
+ )
+ }
+
+ fn make_instance_kind(
+ key: TextRunKey,
+ data_handle: TextRunDataHandle,
+ prim_store: &mut PrimitiveStore,
+ reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind {
+ let run_index = prim_store.text_runs.push(TextRunPrimitive {
+ used_font: key.font.clone(),
+ glyph_keys_range: storage::Range::empty(),
+ reference_frame_relative_offset,
+ snapped_reference_frame_relative_offset: reference_frame_relative_offset,
+ shadow: key.shadow,
+ raster_scale: 1.0,
+ requested_raster_space: key.requested_raster_space,
+ });
+
+ PrimitiveInstanceKind::TextRun{ data_handle, run_index }
+ }
+}
+
+impl CreateShadow for TextRun {
+ fn create_shadow(
+ &self,
+ shadow: &Shadow,
+ blur_is_noop: bool,
+ current_raster_space: RasterSpace,
+ ) -> Self {
+ let mut font = FontInstance {
+ color: shadow.color.into(),
+ ..self.font.clone()
+ };
+ if shadow.blur_radius > 0.0 {
+ font.disable_subpixel_aa();
+ }
+
+ let requested_raster_space = if blur_is_noop {
+ current_raster_space
+ } else {
+ RasterSpace::Local(1.0)
+ };
+
+ TextRun {
+ font,
+ glyphs: self.glyphs.clone(),
+ shadow: true,
+ requested_raster_space,
+ }
+ }
+}
+
+impl IsVisible for TextRun {
+ fn is_visible(&self) -> bool {
+ self.font.color.a > 0
+ }
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct TextRunPrimitive {
+ pub used_font: FontInstance,
+ pub glyph_keys_range: storage::Range<GlyphKey>,
+ pub reference_frame_relative_offset: LayoutVector2D,
+ pub snapped_reference_frame_relative_offset: LayoutVector2D,
+ pub shadow: bool,
+ pub raster_scale: f32,
+ pub requested_raster_space: RasterSpace,
+}
+
+impl TextRunPrimitive {
+ pub fn update_font_instance(
+ &mut self,
+ specified_font: &FontInstance,
+ surface: &SurfaceInfo,
+ spatial_node_index: SpatialNodeIndex,
+ transform: &LayoutToWorldTransform,
+ mut allow_subpixel: bool,
+ raster_space: RasterSpace,
+ spatial_tree: &SpatialTree,
+ ) -> bool {
+ // If local raster space is specified, include that in the scale
+ // of the glyphs that get rasterized.
+ // TODO(gw): Once we support proper local space raster modes, this
+ // will implicitly be part of the device pixel ratio for
+ // the (cached) local space surface, and so this code
+ // will no longer be required.
+ let raster_scale = raster_space.local_scale().unwrap_or(1.0).max(0.001);
+
+ let dps = surface.device_pixel_scale.0;
+ let font_size = specified_font.size.to_f32_px();
+
+ // Small floating point error can accumulate in the raster * device_pixel scale.
+ // Round that to the nearest 100th of a scale factor to remove this error while
+ // still allowing reasonably accurate scale factors when a pinch-zoom is stopped
+ // at a fractional amount.
+ let quantized_scale = (dps * raster_scale * 100.0).round() / 100.0;
+ let mut device_font_size = font_size * quantized_scale;
+
+ // Check there is a valid transform that doesn't exceed the font size limit.
+ // Ensure the font is supposed to be rasterized in screen-space.
+ // Only support transforms that can be coerced to simple 2D transforms.
+ // Add texture padding to the rasterized glyph buffer when one anticipates
+ // the glyph will need to be scaled when rendered.
+ let (use_subpixel_aa, transform_glyphs, texture_padding, oversized) = if raster_space != RasterSpace::Screen ||
+ transform.has_perspective_component() || !transform.has_2d_inverse()
+ {
+ (false, false, true, device_font_size > FONT_SIZE_LIMIT)
+ } else if transform.exceeds_2d_scale((FONT_SIZE_LIMIT / device_font_size) as f64) {
+ (false, false, true, true)
+ } else {
+ (true, !transform.is_simple_2d_translation(), false, false)
+ };
+
+ let font_transform = if transform_glyphs {
+ // Get the font transform matrix (skew / scale) from the complete transform.
+ // Fold in the device pixel scale.
+ self.raster_scale = 1.0;
+ FontTransform::from(transform)
+ } else {
+ if oversized {
+ // Font sizes larger than the limit need to be scaled, thus can't use subpixels.
+ // In this case we adjust the font size and raster space to ensure
+ // we rasterize at the limit, to minimize the amount of scaling.
+ let limited_raster_scale = FONT_SIZE_LIMIT / (font_size * dps);
+ device_font_size = FONT_SIZE_LIMIT;
+
+ // Record the raster space the text needs to be snapped in. The original raster
+ // scale would have been too big.
+ self.raster_scale = limited_raster_scale;
+ } else {
+ // Record the raster space the text needs to be snapped in. We may have changed
+ // from RasterSpace::Screen due to a transform with perspective or without a 2d
+ // inverse, or it may have been RasterSpace::Local all along.
+ self.raster_scale = raster_scale;
+ }
+
+ // Rasterize the glyph without any transform
+ FontTransform::identity()
+ };
+
+ // TODO(aosmond): Snapping really ought to happen during scene building
+ // as much as possible. This will allow clips to be already adjusted
+ // based on the snapping requirements of the primitive. This may affect
+ // complex clips that create a different task, and when we rasterize
+ // glyphs without the transform (because the shader doesn't have the
+ // snap offsets to adjust its clip). These rects are fairly conservative
+ // to begin with and do not appear to be causing significant issues at
+ // this time.
+ self.snapped_reference_frame_relative_offset = if transform_glyphs {
+ // Don't touch the reference frame relative offset. We'll let the
+ // shader do the snapping in device pixels.
+ self.reference_frame_relative_offset
+ } else {
+ // TODO(dp): The SurfaceInfo struct needs to be updated to use RasterPixelScale
+ // rather than DevicePixelScale, however this is a large chunk of
+ // work that will be done as a follow up patch.
+ let raster_pixel_scale = RasterPixelScale::new(surface.device_pixel_scale.0);
+
+ // There may be an animation, so snap the reference frame relative
+ // offset such that it excludes the impact, if any.
+ let snap_to_device = SpaceSnapper::new_with_target(
+ surface.raster_spatial_node_index,
+ spatial_node_index,
+ raster_pixel_scale,
+ spatial_tree,
+ );
+ snap_to_device.snap_point(&self.reference_frame_relative_offset.to_point()).to_vector()
+ };
+
+ let mut flags = specified_font.flags;
+ if transform_glyphs {
+ flags |= FontInstanceFlags::TRANSFORM_GLYPHS;
+ }
+ if texture_padding {
+ flags |= FontInstanceFlags::TEXTURE_PADDING;
+ }
+
+ // If the transform or device size is different, then the caller of
+ // this method needs to know to rebuild the glyphs.
+ let cache_dirty =
+ self.used_font.transform != font_transform ||
+ self.used_font.size != device_font_size.into() ||
+ self.used_font.flags != flags;
+
+ // Construct used font instance from the specified font instance
+ self.used_font = FontInstance {
+ transform: font_transform,
+ size: device_font_size.into(),
+ flags,
+ ..specified_font.clone()
+ };
+
+ // If we are using special estimated background subpixel blending, then
+ // we can allow it regardless of what the surface says.
+ allow_subpixel |= self.used_font.bg_color.a != 0;
+
+ // If using local space glyphs, we don't want subpixel AA.
+ if !allow_subpixel || !use_subpixel_aa {
+ self.used_font.disable_subpixel_aa();
+
+ // Disable subpixel positioning for oversized glyphs to avoid
+ // thrashing the glyph cache with many subpixel variations of
+ // big glyph textures. A possible subpixel positioning error
+ // is small relative to the maximum font size and thus should
+ // not be very noticeable.
+ if oversized {
+ self.used_font.disable_subpixel_position();
+ }
+ }
+
+ cache_dirty
+ }
+
+ /// Gets the raster space to use when rendering this primitive.
+ /// Usually this would be the requested raster space. However, if
+ /// the primitive's spatial node or one of its ancestors is being pinch zoomed
+ /// then we round it. This prevents us rasterizing glyphs for every minor
+ /// change in zoom level, as that would be too expensive.
+ fn get_raster_space_for_prim(
+ &self,
+ prim_spatial_node_index: SpatialNodeIndex,
+ low_quality_pinch_zoom: bool,
+ device_pixel_scale: DevicePixelScale,
+ spatial_tree: &SpatialTree,
+ ) -> RasterSpace {
+ let prim_spatial_node = spatial_tree.get_spatial_node(prim_spatial_node_index);
+ if prim_spatial_node.is_ancestor_or_self_zooming {
+ if low_quality_pinch_zoom {
+ // In low-quality mode, we set the scale to be 1.0. However, the device-pixel
+ // scale selected for the zoom will be taken into account in the caller to this
+ // function when it's converted from local -> device pixels. Since in this mode
+ // the device-pixel scale is constant during the zoom, this gives the desired
+ // performance while also allowing the scale to be adjusted to a new factor at
+ // the end of a pinch-zoom.
+ RasterSpace::Local(1.0)
+ } else {
+ let root_spatial_node_index = spatial_tree.root_reference_frame_index();
+
+ // For high-quality mode, we quantize the exact scale factor as before. However,
+ // we want to _undo_ the effect of the device-pixel scale on the picture cache
+ // tiles (which changes now that they are raster roots). Divide the rounded value
+ // by the device-pixel scale so that the local -> device conversion has no effect.
+ let scale_factors = spatial_tree
+ .get_relative_transform(prim_spatial_node_index, root_spatial_node_index)
+ .scale_factors();
+
+ // Round the scale up to the nearest power of 2, but don't exceed 8.
+ let scale = scale_factors.0.max(scale_factors.1).min(8.0).max(1.0);
+ let rounded_up = 2.0f32.powf(scale.log2().ceil());
+
+ RasterSpace::Local(rounded_up / device_pixel_scale.0)
+ }
+ } else {
+ // Assume that if we have a RasterSpace::Local, it is frequently changing, in which
+ // case we want to undo the device-pixel scale, as we do above.
+ match self.requested_raster_space {
+ RasterSpace::Local(scale) => RasterSpace::Local(scale / device_pixel_scale.0),
+ RasterSpace::Screen => RasterSpace::Screen,
+ }
+ }
+ }
+
+ pub fn request_resources(
+ &mut self,
+ prim_offset: LayoutVector2D,
+ specified_font: &FontInstance,
+ glyphs: &[GlyphInstance],
+ transform: &LayoutToWorldTransform,
+ surface: &SurfaceInfo,
+ spatial_node_index: SpatialNodeIndex,
+ allow_subpixel: bool,
+ low_quality_pinch_zoom: bool,
+ resource_cache: &mut ResourceCache,
+ gpu_cache: &mut GpuCache,
+ spatial_tree: &SpatialTree,
+ scratch: &mut PrimitiveScratchBuffer,
+ ) {
+ let raster_space = self.get_raster_space_for_prim(
+ spatial_node_index,
+ low_quality_pinch_zoom,
+ surface.device_pixel_scale,
+ spatial_tree,
+ );
+
+ let cache_dirty = self.update_font_instance(
+ specified_font,
+ surface,
+ spatial_node_index,
+ transform,
+ allow_subpixel,
+ raster_space,
+ spatial_tree,
+ );
+
+ if self.glyph_keys_range.is_empty() || cache_dirty {
+ let subpx_dir = self.used_font.get_subpx_dir();
+
+ let dps = surface.device_pixel_scale.0;
+ let transform = match raster_space {
+ RasterSpace::Local(scale) => FontTransform::new(scale * dps, 0.0, 0.0, scale * dps),
+ RasterSpace::Screen => self.used_font.transform.scale(dps),
+ };
+
+ self.glyph_keys_range = scratch.glyph_keys.extend(
+ glyphs.iter().map(|src| {
+ let src_point = src.point + prim_offset;
+ let device_offset = transform.transform(&src_point);
+ GlyphKey::new(src.index, device_offset, subpx_dir)
+ }));
+ }
+
+ resource_cache.request_glyphs(
+ self.used_font.clone(),
+ &scratch.glyph_keys[self.glyph_keys_range],
+ gpu_cache,
+ );
+ }
+}
+
+/// These are linux only because FontInstancePlatformOptions varies in size by platform.
+#[test]
+#[cfg(target_os = "linux")]
+fn test_struct_sizes() {
+ use std::mem;
+ // The sizes of these structures are critical for performance on a number of
+ // talos stress tests. If you get a failure here on CI, there's two possibilities:
+ // (a) You made a structure smaller than it currently is. Great work! Update the
+ // test expectations and move on.
+ // (b) You made a structure larger. This is not necessarily a problem, but should only
+ // be done with care, and after checking if talos performance regresses badly.
+ assert_eq!(mem::size_of::<TextRun>(), 64, "TextRun size changed");
+ assert_eq!(mem::size_of::<TextRunTemplate>(), 80, "TextRunTemplate size changed");
+ assert_eq!(mem::size_of::<TextRunKey>(), 80, "TextRunKey size changed");
+ assert_eq!(mem::size_of::<TextRunPrimitive>(), 80, "TextRunPrimitive size changed");
+}
diff --git a/gfx/wr/webrender/src/print_tree.rs b/gfx/wr/webrender/src/print_tree.rs
new file mode 100644
index 0000000000..56d7852561
--- /dev/null
+++ b/gfx/wr/webrender/src/print_tree.rs
@@ -0,0 +1,122 @@
+/* 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::io::Write;
+use std::fmt::Write as FmtWrite;
+
+/// A struct that makes it easier to print out a pretty tree of data, which
+/// can be visually scanned more easily.
+pub struct PrintTree<W>
+where
+ W: Write
+{
+ /// The current level of recursion.
+ level: u32,
+
+ /// An item which is queued up, so that we can determine if we need
+ /// a mid-tree prefix or a branch ending prefix.
+ queued_item: Option<String>,
+
+ // We hold lines until they are done, and then output them all at
+ // once
+ line_buffer: String,
+
+ /// The sink to print to.
+ sink: W,
+}
+
+/// A trait that makes it easy to describe a pretty tree of data,
+/// regardless of the printing destination, to either print it
+/// directly to stdout, or serialize it as in the debugger
+pub trait PrintTreePrinter {
+ fn new_level(&mut self, title: String);
+ fn end_level(&mut self);
+ fn add_item(&mut self, text: String);
+}
+
+// The default does nothing but log
+impl PrintTree<std::io::Sink> {
+ pub fn new(title: &str) -> Self {
+ PrintTree::new_with_sink(title, std::io::sink())
+ }
+}
+
+impl<W> PrintTree<W>
+where
+ W: Write
+{
+ pub fn new_with_sink(title: &str, sink: W) -> Self {
+ let mut result = PrintTree {
+ level: 1,
+ queued_item: None,
+ line_buffer: String::new(),
+ sink,
+ };
+
+ writeln!(result.line_buffer, "\u{250c} {}", title).unwrap();
+ result.flush_line();
+ result
+ }
+
+ fn print_level_prefix(&mut self) {
+ for _ in 0 .. self.level {
+ write!(self.line_buffer, "\u{2502} ").unwrap();
+ }
+ }
+
+ fn flush_queued_item(&mut self, prefix: &str) {
+ if let Some(queued_item) = self.queued_item.take() {
+ self.print_level_prefix();
+ writeln!(self.line_buffer, "{} {}", prefix, queued_item).unwrap();
+ self.flush_line();
+ }
+ }
+
+ fn flush_line(&mut self) {
+ debug!("{}", self.line_buffer);
+ self.sink.write_all(self.line_buffer.as_bytes()).unwrap();
+ self.line_buffer.clear();
+ }
+}
+
+impl<W> PrintTreePrinter for PrintTree<W>
+where
+ W: Write
+{
+ /// Descend one level in the tree with the given title.
+ fn new_level(&mut self, title: String) {
+ self.flush_queued_item("\u{251C}\u{2500}");
+
+ self.print_level_prefix();
+ writeln!(self.line_buffer, "\u{251C}\u{2500} {}", title).unwrap();
+ self.flush_line();
+
+ self.level = self.level + 1;
+ }
+
+ /// Ascend one level in the tree.
+ fn end_level(&mut self) {
+ self.flush_queued_item("\u{2514}\u{2500}");
+ self.level = self.level - 1;
+ }
+
+ /// Add an item to the current level in the tree.
+ fn add_item(&mut self, text: String) {
+ self.flush_queued_item("\u{251C}\u{2500}");
+ self.queued_item = Some(text);
+ }
+}
+
+impl<W> Drop for PrintTree<W>
+where
+ W: Write
+{
+ fn drop(&mut self) {
+ self.flush_queued_item("\u{9492}\u{9472}");
+ }
+}
+
+pub trait PrintableTree {
+ fn print_with<T: PrintTreePrinter>(&self, pt: &mut T);
+}
diff --git a/gfx/wr/webrender/src/profiler.rs b/gfx/wr/webrender/src/profiler.rs
new file mode 100644
index 0000000000..ccf86e8647
--- /dev/null
+++ b/gfx/wr/webrender/src/profiler.rs
@@ -0,0 +1,1856 @@
+/* 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/. */
+
+//! # Overlay profiler
+//!
+//! ## Profiler UI string syntax
+//!
+//! Comma-separated list of of tokens with trailing and leading spaces trimmed.
+//! Each tokens can be:
+//! - A counter name with an optional prefix. The name corresponds to the displayed name (see the
+//! counters vector below.
+//! - By default (no prefix) the counter is shown as average + max over half a second.
+//! - With a '#' prefix the counter is shown as a graph.
+//! - With a '*' prefix the counter is shown as a change indicator.
+//! - Some special counters such as GPU time queries have specific visualizations ignoring prefixes.
+//! - A preset name to append the preset to the UI (see PROFILER_PRESETS).
+//! - An empty token to insert a bit of vertical space.
+//! - A '|' token to start a new column.
+//! - A '_' token to start a new row.
+
+use api::{ColorF, ColorU};
+use glyph_rasterizer::profiler::GlyphRasterizeProfiler;
+use crate::renderer::DebugRenderer;
+use crate::device::query::GpuTimer;
+use euclid::{Point2D, Rect, Size2D, vec2, default};
+use crate::internal_types::FastHashMap;
+use crate::renderer::{FullFrameStats, MAX_VERTEX_TEXTURE_WIDTH, init::wr_has_been_initialized};
+use api::units::DeviceIntSize;
+use std::collections::vec_deque::VecDeque;
+use std::fmt::{Write, Debug};
+use std::f32;
+use std::ops::Range;
+use std::time::Duration;
+use time::precise_time_ns;
+
+macro_rules! set_text {
+ ($dst:expr, $($arg:tt)*) => {
+ $dst.clear();
+ write!($dst, $($arg)*).unwrap();
+ };
+}
+
+const GRAPH_WIDTH: f32 = 1024.0;
+const GRAPH_HEIGHT: f32 = 320.0;
+const GRAPH_PADDING: f32 = 8.0;
+const GRAPH_FRAME_HEIGHT: f32 = 16.0;
+const PROFILE_SPACING: f32 = 15.0;
+const PROFILE_PADDING: f32 = 10.0;
+const BACKGROUND_COLOR: ColorU = ColorU { r: 20, g: 20, b: 20, a: 220 };
+
+const ONE_SECOND_NS: u64 = 1_000_000_000;
+
+/// Profiler UI string presets. Defined in the profiler UI string syntax, can contain other presets.
+static PROFILER_PRESETS: &'static[(&'static str, &'static str)] = &[
+ // Default view, doesn't show everything, but still shows quite a bit.
+ (&"Default", &"FPS,|,Slow indicators,_,Time graphs,|,Frame times, ,Transaction times, ,Frame stats, ,Memory, ,Interners,_,GPU time queries,_,Paint phase graph"),
+ // Smaller, less intrusive overview
+ (&"Compact", &"FPS, ,Frame times, ,Frame stats"),
+ // Even less intrusive, only slow transactions and frame indicators.
+ (&"Slow indicators", &"*Slow transaction,*Slow frame"),
+
+ // Counters:
+
+ // Timing information for per layout transaction stages.
+ (&"Transaction times", &"DisplayList,Scene building,Content send,API send"),
+ // Timing information for per-frame stages.
+ (&"Frame times", &"Frame CPU total,Frame building,Visibility,Prepare,Batching,Glyph resolve,Texture cache update,Shader build time,Renderer,GPU"),
+ // Stats about the content of the frame.
+ (&"Frame stats", &"Primitives,Visible primitives,Draw calls,Vertices,Color passes,Alpha passes,Rendered picture tiles,Rasterized glyphs"),
+ // Texture cache allocation stats.
+ (&"Texture cache stats", &"Atlas textures mem, Standalone textures mem, Picture tiles mem, Render targets mem, Depth targets mem, Atlas items mem,
+ Texture cache standalone pressure, Texture cache eviction count, Texture cache youngest evicted, ,
+ Atlas RGBA8 linear pixels, Atlas RGBA8 glyphs pixels, Atlas A8 glyphs pixels, Atlas A8 pixels, Atlas A16 pixels, Atlas RGBA8 nearest pixels,
+ Atlas RGBA8 linear textures, Atlas RGBA8 glyphs textures, Atlas A8 glyphs textures, Atlas A8 textures, Atlas A16 textures, Atlas RGBA8 nearest textures,
+ Atlas RGBA8 linear pressure, Atlas RGBA8 glyphs pressure, Atlas A8 glyphs pressure, Atlas A8 pressure, Atlas A16 pressure, Atlas RGBA8 nearest pressure,"
+ ),
+ // Graphs to investigate driver overhead of texture cache updates.
+ (&"Texture upload perf", &"#Texture cache update,#Texture cache upload, ,#Staging CPU allocation,#Staging GPU allocation,#Staging CPU copy,#Staging GPU copy,#Upload time, ,#Upload copy batches,#Rasterized glyphs, ,#Cache texture creation,#Cache texture deletion"),
+
+ // Graphs:
+
+ // Graph overview of time spent in WebRender's main stages.
+ (&"Time graphs", &"#DisplayList,#Scene building,#Blob rasterization, ,#Frame CPU total,#Frame building,#Renderer,#Texture cache update, ,#GPU,"),
+ // Useful when investigating render backend bottlenecks.
+ (&"Backend graphs", &"#Frame building, #Visibility, #Prepare, #Batching, #Glyph resolve"),
+ // Useful when investigating renderer bottlenecks.
+ (&"Renderer graphs", &"#Rendered picture tiles,#Draw calls,#Rasterized glyphs,#Texture uploads,#Texture uploads mem, ,#Texture cache update,#Renderer,"),
+
+ // Misc:
+
+ (&"Memory", &"Image templates,Image templates mem,Font templates,Font templates mem,DisplayList mem,Picture tiles mem"),
+ (&"Interners", "Interned primitives,Interned clips,Interned pictures,Interned text runs,Interned normal borders,Interned image borders,Interned images,Interned YUV images,Interned line decorations,Interned linear gradients,Interned radial gradients,Interned conic gradients,Interned filter data,Interned backdrops"),
+ // Gpu sampler queries (need the pref gfx.webrender.debug.gpu-sampler-queries).
+ (&"GPU samplers", &"Alpha targets samplers,Transparent pass samplers,Opaque pass samplers,Total samplers"),
+
+ (&"Render reasons", &"Reason scene, Reason animated property, Reason resource update, Reason async image, Reason clear resources, Reason APZ, Reason resize, Reason widget, Reason cache flush, Reason snapshot, Reason resource hook, Reason config change, Reason content sync, Reason flush, On vsync, Reason testing, Reason other"),
+];
+
+fn find_preset(name: &str) -> Option<&'static str> {
+ for preset in PROFILER_PRESETS {
+ if preset.0 == name {
+ return Some(preset.1);
+ }
+ }
+
+ None
+}
+
+// The indices here must match the PROFILE_COUNTERS array (checked at runtime).
+pub const FRAME_BUILDING_TIME: usize = 0;
+pub const FRAME_VISIBILITY_TIME: usize = 1;
+pub const FRAME_PREPARE_TIME: usize = 2;
+pub const FRAME_BATCHING_TIME: usize = 3;
+
+pub const RENDERER_TIME: usize = 4;
+pub const TOTAL_FRAME_CPU_TIME: usize = 5;
+pub const GPU_TIME: usize = 6;
+
+pub const CONTENT_SEND_TIME: usize = 7;
+pub const API_SEND_TIME: usize = 8;
+
+pub const DISPLAY_LIST_BUILD_TIME: usize = 9;
+pub const DISPLAY_LIST_MEM: usize = 10;
+
+pub const SCENE_BUILD_TIME: usize = 11;
+
+pub const SLOW_FRAME: usize = 12;
+pub const SLOW_TXN: usize = 13;
+
+pub const FRAME_TIME: usize = 14;
+
+pub const TEXTURE_UPLOADS: usize = 15;
+pub const TEXTURE_UPLOADS_MEM: usize = 16;
+pub const TEXTURE_CACHE_UPDATE_TIME: usize = 17;
+pub const CPU_TEXTURE_ALLOCATION_TIME: usize = 18;
+pub const STAGING_TEXTURE_ALLOCATION_TIME: usize = 19;
+pub const UPLOAD_CPU_COPY_TIME: usize = 20;
+pub const UPLOAD_GPU_COPY_TIME: usize = 21;
+pub const UPLOAD_TIME: usize = 22;
+pub const UPLOAD_NUM_COPY_BATCHES: usize = 23;
+pub const TOTAL_UPLOAD_TIME: usize = 24;
+pub const CREATE_CACHE_TEXTURE_TIME: usize = 25;
+pub const DELETE_CACHE_TEXTURE_TIME: usize = 26;
+pub const GPU_CACHE_UPLOAD_TIME: usize = 27;
+
+pub const RASTERIZED_BLOBS: usize = 28;
+pub const RASTERIZED_BLOB_TILES: usize = 29;
+pub const RASTERIZED_BLOBS_PX: usize = 30;
+pub const BLOB_RASTERIZATION_TIME: usize = 31;
+
+pub const RASTERIZED_GLYPHS: usize = 32;
+pub const GLYPH_RESOLVE_TIME: usize = 33;
+
+pub const DRAW_CALLS: usize = 34;
+pub const VERTICES: usize = 35;
+pub const PRIMITIVES: usize = 36;
+pub const VISIBLE_PRIMITIVES: usize = 37;
+
+pub const USED_TARGETS: usize = 38;
+pub const CREATED_TARGETS: usize = 39;
+pub const PICTURE_CACHE_SLICES: usize = 40;
+
+pub const COLOR_PASSES: usize = 41;
+pub const ALPHA_PASSES: usize = 42;
+pub const PICTURE_TILES: usize = 43;
+pub const RENDERED_PICTURE_TILES: usize = 44;
+
+pub const FONT_TEMPLATES: usize = 45;
+pub const FONT_TEMPLATES_MEM: usize = 46;
+pub const IMAGE_TEMPLATES: usize = 47;
+pub const IMAGE_TEMPLATES_MEM: usize = 48;
+
+pub const GPU_CACHE_ROWS_TOTAL: usize = 49;
+pub const GPU_CACHE_ROWS_UPDATED: usize = 50;
+pub const GPU_CACHE_BLOCKS_TOTAL: usize = 51;
+pub const GPU_CACHE_BLOCKS_UPDATED: usize = 52;
+pub const GPU_CACHE_BLOCKS_SAVED: usize = 53;
+
+// Atlas items represents the area occupied by items in the cache textures.
+// The actual texture memory allocated is ATLAS_TEXTURES_MEM.
+pub const ATLAS_ITEMS_MEM: usize = 54;
+pub const ATLAS_A8_PIXELS: usize = 55;
+pub const ATLAS_A8_TEXTURES: usize = 56;
+pub const ATLAS_A16_PIXELS: usize = 57;
+pub const ATLAS_A16_TEXTURES: usize = 58;
+pub const ATLAS_RGBA8_LINEAR_PIXELS: usize = 59;
+pub const ATLAS_RGBA8_LINEAR_TEXTURES: usize = 60;
+pub const ATLAS_RGBA8_NEAREST_PIXELS: usize = 61;
+pub const ATLAS_RGBA8_NEAREST_TEXTURES: usize = 62;
+pub const ATLAS_RGBA8_GLYPHS_PIXELS: usize = 63;
+pub const ATLAS_RGBA8_GLYPHS_TEXTURES: usize = 64;
+pub const ATLAS_A8_GLYPHS_PIXELS: usize = 65;
+pub const ATLAS_A8_GLYPHS_TEXTURES: usize = 66;
+pub const ATLAS_COLOR8_LINEAR_PRESSURE: usize = 67;
+pub const ATLAS_COLOR8_NEAREST_PRESSURE: usize = 68;
+pub const ATLAS_COLOR8_GLYPHS_PRESSURE: usize = 69;
+pub const ATLAS_ALPHA8_PRESSURE: usize = 70;
+pub const ATLAS_ALPHA8_GLYPHS_PRESSURE: usize = 71;
+pub const ATLAS_ALPHA16_PRESSURE: usize = 72;
+pub const ATLAS_STANDALONE_PRESSURE: usize = 73;
+
+pub const TEXTURE_CACHE_EVICTION_COUNT: usize = 74;
+pub const TEXTURE_CACHE_YOUNGEST_EVICTION: usize = 75;
+pub const EXTERNAL_IMAGE_BYTES: usize = 76;
+pub const ATLAS_TEXTURES_MEM: usize = 77;
+pub const STANDALONE_TEXTURES_MEM: usize = 78;
+pub const PICTURE_TILES_MEM: usize = 79;
+pub const RENDER_TARGET_MEM: usize = 80;
+
+pub const ALPHA_TARGETS_SAMPLERS: usize = 81;
+pub const TRANSPARENT_PASS_SAMPLERS: usize = 82;
+pub const OPAQUE_PASS_SAMPLERS: usize = 83;
+pub const TOTAL_SAMPLERS: usize = 84;
+
+pub const INTERNED_PRIMITIVES: usize = 85;
+pub const INTERNED_CLIPS: usize = 86;
+pub const INTERNED_TEXT_RUNS: usize = 87;
+pub const INTERNED_NORMAL_BORDERS: usize = 88;
+pub const INTERNED_IMAGE_BORDERS: usize = 89;
+pub const INTERNED_IMAGES: usize = 90;
+pub const INTERNED_YUV_IMAGES: usize = 91;
+pub const INTERNED_LINE_DECORATIONS: usize = 92;
+pub const INTERNED_LINEAR_GRADIENTS: usize = 93;
+pub const INTERNED_RADIAL_GRADIENTS: usize = 94;
+pub const INTERNED_CONIC_GRADIENTS: usize = 95;
+pub const INTERNED_PICTURES: usize = 96;
+pub const INTERNED_FILTER_DATA: usize = 97;
+pub const INTERNED_BACKDROP_CAPTURES: usize = 98;
+pub const INTERNED_BACKDROP_RENDERS: usize = 99;
+pub const INTERNED_POLYGONS: usize = 100;
+
+pub const DEPTH_TARGETS_MEM: usize = 101;
+
+pub const SHADER_BUILD_TIME: usize = 102;
+
+pub const RENDER_REASON_FIRST: usize = 103;
+pub const RENDER_REASON_SCENE: usize = 103;
+pub const RENDER_REASON_ANIMATED_PROPERTY: usize = 104;
+pub const RENDER_REASON_RESOURCE_UPDATE: usize = 105;
+pub const RENDER_REASON_ASYNC_IMAGE: usize = 106;
+pub const RENDER_REASON_CLEAR_RESOURCES: usize = 107;
+pub const RENDER_REASON_APZ: usize = 108;
+pub const RENDER_REASON_RESIZE: usize = 109;
+pub const RENDER_REASON_WIDGET: usize = 110;
+pub const RENDER_REASON_TEXTURE_CACHE_FLUSH: usize = 111;
+pub const RENDER_REASON_SNAPSHOT: usize = 112;
+pub const RENDER_REASON_POST_RESOURCE_UPDATE_HOOKS: usize = 113;
+pub const RENDER_REASON_CONFIG_CHANGE: usize = 114;
+pub const RENDER_REASON_CONTENT_SYNC: usize = 115;
+pub const RENDER_REASON_FLUSH: usize = 116;
+pub const RENDER_REASON_TESTING: usize = 117;
+pub const RENDER_REASON_OTHER: usize = 118;
+pub const RENDER_REASON_VSYNC: usize = 119;
+
+pub const TEXTURES_CREATED: usize = 120;
+pub const TEXTURES_DELETED: usize = 121;
+
+pub const NUM_PROFILER_EVENTS: usize = 122;
+
+pub struct Profiler {
+ counters: Vec<Counter>,
+ gpu_frames: ProfilerFrameCollection,
+ frame_stats: ProfilerFrameCollection,
+
+ start: u64,
+ avg_over_period: u64,
+ num_graph_samples: usize,
+
+ // For FPS computation. Updated in update().
+ frame_timestamps_within_last_second: Vec<u64>,
+
+ ui: Vec<Item>,
+}
+
+impl Profiler {
+ pub fn new() -> Self {
+
+ fn float(name: &'static str, unit: &'static str, index: usize, expected: Expected<f64>) -> CounterDescriptor {
+ CounterDescriptor { name, unit, show_as: ShowAs::Float, index, expected }
+ }
+
+ fn int(name: &'static str, unit: &'static str, index: usize, expected: Expected<i64>) -> CounterDescriptor {
+ CounterDescriptor { name, unit, show_as: ShowAs::Int, index, expected: expected.into_float() }
+ }
+
+ // Not in the list below:
+ // - "GPU time queries" shows the details of the GPU time queries if selected as a graph.
+ // - "GPU cache bars" shows some info about the GPU cache.
+
+ // TODO: This should be a global variable but to keep things readable we need to be able to
+ // use match in const fn which isn't supported by the current rustc version in gecko's build
+ // system.
+ let profile_counters = &[
+ float("Frame building", "ms", FRAME_BUILDING_TIME, expected(0.0..6.0).avg(0.0..3.0)),
+ float("Visibility", "ms", FRAME_VISIBILITY_TIME, expected(0.0..3.0).avg(0.0..2.0)),
+ float("Prepare", "ms", FRAME_PREPARE_TIME, expected(0.0..3.0).avg(0.0..2.0)),
+ float("Batching", "ms", FRAME_BATCHING_TIME, expected(0.0..3.0).avg(0.0..2.0)),
+
+ float("Renderer", "ms", RENDERER_TIME, expected(0.0..8.0).avg(0.0..5.0)),
+ float("Frame CPU total", "ms", TOTAL_FRAME_CPU_TIME, expected(0.0..15.0).avg(0.0..6.0)),
+ float("GPU", "ms", GPU_TIME, expected(0.0..15.0).avg(0.0..8.0)),
+
+ float("Content send", "ms", CONTENT_SEND_TIME, expected(0.0..1.0).avg(0.0..1.0)),
+ float("API send", "ms", API_SEND_TIME, expected(0.0..1.0).avg(0.0..0.4)),
+ float("DisplayList", "ms", DISPLAY_LIST_BUILD_TIME, expected(0.0..5.0).avg(0.0..3.0)),
+ float("DisplayList mem", "MB", DISPLAY_LIST_MEM, expected(0.0..20.0)),
+ float("Scene building", "ms", SCENE_BUILD_TIME, expected(0.0..4.0).avg(0.0..3.0)),
+
+ float("Slow frame", "", SLOW_FRAME, expected(0.0..0.0)),
+ float("Slow transaction", "", SLOW_TXN, expected(0.0..0.0)),
+
+ float("Frame", "ms", FRAME_TIME, Expected::none()),
+
+ int("Texture uploads", "", TEXTURE_UPLOADS, expected(0..10)),
+ float("Texture uploads mem", "MB", TEXTURE_UPLOADS_MEM, expected(0.0..10.0)),
+ float("Texture cache update", "ms", TEXTURE_CACHE_UPDATE_TIME, expected(0.0..3.0)),
+ float("Staging CPU allocation", "ms", CPU_TEXTURE_ALLOCATION_TIME, Expected::none()),
+ float("Staging GPU allocation", "ms", STAGING_TEXTURE_ALLOCATION_TIME, Expected::none()),
+ float("Staging CPU copy", "ms", UPLOAD_CPU_COPY_TIME, Expected::none()),
+ float("Staging GPU copy", "ms", UPLOAD_GPU_COPY_TIME, Expected::none()),
+ float("Upload time", "ms", UPLOAD_TIME, Expected::none()),
+ int("Upload copy batches", "", UPLOAD_NUM_COPY_BATCHES, Expected::none()),
+ float("Texture cache upload", "ms", TOTAL_UPLOAD_TIME, expected(0.0..5.0)),
+ float("Cache texture creation", "ms", CREATE_CACHE_TEXTURE_TIME, expected(0.0..2.0)),
+ float("Cache texture deletion", "ms", DELETE_CACHE_TEXTURE_TIME, expected(0.0..1.0)),
+ float("GPU cache upload", "ms", GPU_CACHE_UPLOAD_TIME, expected(0.0..2.0)),
+
+ int("Rasterized blobs", "", RASTERIZED_BLOBS, expected(0..15)),
+ int("Rasterized blob tiles", "", RASTERIZED_BLOB_TILES, expected(0..15)),
+ int("Rasterized blob pixels", "px", RASTERIZED_BLOBS_PX, expected(0..300_000)),
+ float("Blob rasterization", "ms", BLOB_RASTERIZATION_TIME, expected(0.0..8.0)),
+
+ int("Rasterized glyphs", "", RASTERIZED_GLYPHS, expected(0..15)),
+ float("Glyph resolve", "ms", GLYPH_RESOLVE_TIME, expected(0.0..4.0)),
+
+ int("Draw calls", "", DRAW_CALLS, expected(1..120).avg(1..90)),
+ int("Vertices", "", VERTICES, expected(10..5000)),
+ int("Primitives", "", PRIMITIVES, expected(10..5000)),
+ int("Visible primitives", "", VISIBLE_PRIMITIVES, expected(1..5000)),
+
+ int("Used targets", "", USED_TARGETS, expected(1..4)),
+ int("Created targets", "", CREATED_TARGETS, expected(0..3)),
+ int("Picture cache slices", "", PICTURE_CACHE_SLICES, expected(0..5)),
+
+ int("Color passes", "", COLOR_PASSES, expected(1..4)),
+ int("Alpha passes", "", ALPHA_PASSES, expected(0..3)),
+ int("Picture tiles", "", PICTURE_TILES, expected(0..15)),
+ int("Rendered picture tiles", "", RENDERED_PICTURE_TILES, expected(0..5)),
+
+ int("Font templates", "", FONT_TEMPLATES, expected(0..40)),
+ float("Font templates mem", "MB", FONT_TEMPLATES_MEM, expected(0.0..20.0)),
+ int("Image templates", "", IMAGE_TEMPLATES, expected(0..100)),
+ float("Image templates mem", "MB", IMAGE_TEMPLATES_MEM, expected(0.0..50.0)),
+
+ int("GPU cache rows total", "", GPU_CACHE_ROWS_TOTAL, expected(1..50)),
+ int("GPU cache rows updated", "", GPU_CACHE_ROWS_UPDATED, expected(0..25)),
+ int("GPU blocks total", "", GPU_CACHE_BLOCKS_TOTAL, expected(1..65_000)),
+ int("GPU blocks updated", "", GPU_CACHE_BLOCKS_UPDATED, expected(0..1000)),
+ int("GPU blocks saved", "", GPU_CACHE_BLOCKS_SAVED, expected(0..50_000)),
+
+ float("Atlas items mem", "MB", ATLAS_ITEMS_MEM, expected(0.0..100.0)),
+ int("Atlas A8 pixels", "px", ATLAS_A8_PIXELS, expected(0..1_000_000)),
+ int("Atlas A8 textures", "", ATLAS_A8_TEXTURES, expected(0..2)),
+ int("Atlas A16 pixels", "px", ATLAS_A16_PIXELS, expected(0..260_000)),
+ int("Atlas A16 textures", "", ATLAS_A16_TEXTURES, expected(0..2)),
+ int("Atlas RGBA8 linear pixels", "px", ATLAS_RGBA8_LINEAR_PIXELS, expected(0..8_000_000)),
+ int("Atlas RGBA8 linear textures", "", ATLAS_RGBA8_LINEAR_TEXTURES, expected(0..3)),
+ int("Atlas RGBA8 nearest pixels", "px", ATLAS_RGBA8_NEAREST_PIXELS, expected(0..260_000)),
+ int("Atlas RGBA8 nearest textures", "", ATLAS_RGBA8_NEAREST_TEXTURES, expected(0..2)),
+ int("Atlas RGBA8 glyphs pixels", "px", ATLAS_RGBA8_GLYPHS_PIXELS, expected(0..4_000_000)),
+ int("Atlas RGBA8 glyphs textures", "", ATLAS_RGBA8_GLYPHS_TEXTURES, expected(0..2)),
+ int("Atlas A8 glyphs pixels", "px", ATLAS_A8_GLYPHS_PIXELS, expected(0..4_000_000)),
+ int("Atlas A8 glyphs textures", "", ATLAS_A8_GLYPHS_TEXTURES, expected(0..2)),
+ float("Atlas RGBA8 linear pressure", "", ATLAS_COLOR8_LINEAR_PRESSURE, expected(0.0..1.0)),
+ float("Atlas RGBA8 nearest pressure", "", ATLAS_COLOR8_NEAREST_PRESSURE, expected(0.0..1.0)),
+ float("Atlas RGBA8 glyphs pressure", "", ATLAS_COLOR8_GLYPHS_PRESSURE, expected(0.0..1.0)),
+ float("Atlas A8 pressure", "", ATLAS_ALPHA8_PRESSURE, expected(0.0..1.0)),
+ float("Atlas A8 glyphs pressure", "", ATLAS_ALPHA8_GLYPHS_PRESSURE, expected(0.0..1.0)),
+ float("Atlas A16 pressure", "", ATLAS_ALPHA16_PRESSURE, expected(0.0..1.0)),
+ float("Texture cache standalone pressure", "", ATLAS_STANDALONE_PRESSURE, expected(0.0..1.0)),
+
+ int("Texture cache eviction count", "items", TEXTURE_CACHE_EVICTION_COUNT, Expected::none()),
+ int("Texture cache youngest evicted", "frames", TEXTURE_CACHE_YOUNGEST_EVICTION, Expected::none()),
+ float("External image mem", "MB", EXTERNAL_IMAGE_BYTES, Expected::none()),
+ float("Atlas textures mem", "MB", ATLAS_TEXTURES_MEM, Expected::none()),
+ float("Standalone textures mem", "MB", STANDALONE_TEXTURES_MEM, Expected::none()),
+ float("Picture tiles mem", "MB", PICTURE_TILES_MEM, expected(0.0..150.0)),
+ float("Render targets mem", "MB", RENDER_TARGET_MEM, Expected::none()),
+
+ float("Alpha targets samplers", "%", ALPHA_TARGETS_SAMPLERS, Expected::none()),
+ float("Transparent pass samplers", "%", TRANSPARENT_PASS_SAMPLERS, Expected::none()),
+ float("Opaque pass samplers", "%", OPAQUE_PASS_SAMPLERS, Expected::none()),
+ float("Total samplers", "%", TOTAL_SAMPLERS, Expected::none()),
+
+ int("Interned primitives", "", INTERNED_PRIMITIVES, Expected::none()),
+ int("Interned clips", "", INTERNED_CLIPS, Expected::none()),
+ int("Interned text runs", "", INTERNED_TEXT_RUNS, Expected::none()),
+ int("Interned normal borders", "", INTERNED_NORMAL_BORDERS, Expected::none()),
+ int("Interned image borders", "", INTERNED_IMAGE_BORDERS, Expected::none()),
+ int("Interned images", "", INTERNED_IMAGES, Expected::none()),
+ int("Interned YUV images", "", INTERNED_YUV_IMAGES, Expected::none()),
+ int("Interned line decorations", "", INTERNED_LINE_DECORATIONS, Expected::none()),
+ int("Interned linear gradients", "", INTERNED_LINEAR_GRADIENTS, Expected::none()),
+ int("Interned radial gradients", "", INTERNED_RADIAL_GRADIENTS, Expected::none()),
+ int("Interned conic gradients", "", INTERNED_CONIC_GRADIENTS, Expected::none()),
+ int("Interned pictures", "", INTERNED_PICTURES, Expected::none()),
+ int("Interned filter data", "", INTERNED_FILTER_DATA, Expected::none()),
+ int("Interned backdrop captures", "", INTERNED_BACKDROP_CAPTURES, Expected::none()),
+ int("Interned backdrop renders", "", INTERNED_BACKDROP_RENDERS, Expected::none()),
+ int("Interned polygons", "", INTERNED_POLYGONS, Expected::none()),
+
+ float("Depth targets mem", "MB", DEPTH_TARGETS_MEM, Expected::none()),
+ float("Shader build time", "ms", SHADER_BUILD_TIME, Expected::none()),
+ // We use the expected range to highlight render reasons that are happening.
+ float("Reason scene", "", RENDER_REASON_SCENE, expected(0.0..0.01)),
+ float("Reason animated property", "", RENDER_REASON_ANIMATED_PROPERTY, expected(0.0..0.01)),
+ float("Reason resource update", "", RENDER_REASON_RESOURCE_UPDATE, expected(0.0..0.01)),
+ float("Reason async image", "", RENDER_REASON_ASYNC_IMAGE, expected(0.0..0.01)),
+ float("Reason clear resources", "", RENDER_REASON_CLEAR_RESOURCES, expected(0.0..0.01)),
+ float("Reason APZ", "", RENDER_REASON_APZ, expected(0.0..0.01)),
+ float("Reason resize", "", RENDER_REASON_RESIZE, expected(0.0..0.01)),
+ float("Reason widget", "", RENDER_REASON_WIDGET, expected(0.0..0.01)),
+ float("Reason cache flush", "", RENDER_REASON_TEXTURE_CACHE_FLUSH, expected(0.0..0.01)),
+ float("Reason snapshot", "", RENDER_REASON_SNAPSHOT, expected(0.0..0.01)),
+ float("Reason resource hook", "", RENDER_REASON_POST_RESOURCE_UPDATE_HOOKS, expected(0.0..0.01)),
+ float("Reason config change", "", RENDER_REASON_CONFIG_CHANGE, expected(0.0..0.01)),
+ float("Reason content sync", "", RENDER_REASON_CONTENT_SYNC, expected(0.0..0.01)),
+ float("Reason flush", "", RENDER_REASON_FLUSH, expected(0.0..0.01)),
+ float("Reason testing", "", RENDER_REASON_TESTING, expected(0.0..0.01)),
+ float("Reason other", "", RENDER_REASON_OTHER, expected(0.0..0.01)),
+ float("On vsync", "", RENDER_REASON_VSYNC, expected(0.0..0.01)),
+
+ int("Textures created", "", TEXTURES_CREATED, expected(0..5)),
+ int("Textures deleted", "", TEXTURES_DELETED, Expected::none()),
+ ];
+
+ let mut counters = Vec::with_capacity(profile_counters.len());
+
+ for (idx, descriptor) in profile_counters.iter().enumerate() {
+ debug_assert_eq!(descriptor.index, idx);
+ counters.push(Counter::new(descriptor));
+ }
+
+ Profiler {
+ gpu_frames: ProfilerFrameCollection::new(),
+ frame_stats: ProfilerFrameCollection::new(),
+
+ counters,
+ start: precise_time_ns(),
+ avg_over_period: ONE_SECOND_NS / 2,
+
+ num_graph_samples: 500, // Would it be useful to control this via a pref?
+ frame_timestamps_within_last_second: Vec::new(),
+ ui: Vec::new(),
+ }
+ }
+
+ /// Sum a few counters and if the total amount is larger than a threshold, update
+ /// a specific counter.
+ ///
+ /// This is useful to monitor slow frame and slow transactions.
+ fn update_slow_event(&mut self, dst_counter: usize, counters: &[usize], threshold: f64) {
+ let mut total = 0.0;
+ for &counter in counters {
+ if self.counters[counter].value.is_finite() {
+ total += self.counters[counter].value;
+ }
+ }
+
+ if total > threshold {
+ self.counters[dst_counter].set(total);
+ }
+ }
+
+ // Call at the end of every frame, after setting the counter values and before drawing the counters.
+ pub fn update(&mut self) {
+ let now = precise_time_ns();
+ let update_avg = (now - self.start) > self.avg_over_period;
+ if update_avg {
+ self.start = now;
+ }
+ let one_second_ago = now - ONE_SECOND_NS;
+ self.frame_timestamps_within_last_second.retain(|t| *t > one_second_ago);
+ self.frame_timestamps_within_last_second.push(now);
+
+ self.update_slow_event(
+ SLOW_FRAME,
+ &[TOTAL_FRAME_CPU_TIME],
+ 15.0,
+ );
+ self.update_slow_event(
+ SLOW_TXN,
+ &[DISPLAY_LIST_BUILD_TIME, CONTENT_SEND_TIME, SCENE_BUILD_TIME],
+ 80.0
+ );
+
+ for counter in &mut self.counters {
+ counter.update(update_avg);
+ }
+ }
+
+ pub fn update_frame_stats(&mut self, stats: FullFrameStats) {
+ if stats.gecko_display_list_time != 0.0 {
+ self.frame_stats.push(stats.into());
+ }
+ }
+
+ pub fn set_gpu_time_queries(&mut self, gpu_queries: Vec<GpuTimer>) {
+ let mut gpu_time_ns = 0;
+ for sample in &gpu_queries {
+ gpu_time_ns += sample.time_ns;
+ }
+
+ self.gpu_frames.push(ProfilerFrame {
+ total_time: gpu_time_ns,
+ samples: gpu_queries
+ });
+
+ self.counters[GPU_TIME].set_f64(ns_to_ms(gpu_time_ns));
+ }
+
+ // Find the index of a counter by its name.
+ pub fn index_of(&self, name: &str) -> Option<usize> {
+ self.counters.iter().position(|counter| counter.name == name)
+ }
+
+ // Define the profiler UI, see comment about the syntax at the top of this file.
+ pub fn set_ui(&mut self, names: &str) {
+ let mut selection = Vec::new();
+
+ self.append_to_ui(&mut selection, names);
+
+ if selection == self.ui {
+ return;
+ }
+
+ for counter in &mut self.counters {
+ counter.disable_graph();
+ }
+
+ for item in &selection {
+ if let Item::Graph(idx) = item {
+ self.counters[*idx].enable_graph(self.num_graph_samples);
+ }
+ }
+
+ self.ui = selection;
+ }
+
+ fn append_to_ui(&mut self, selection: &mut Vec<Item>, names: &str) {
+ // Group successive counters together.
+ fn flush_counters(counters: &mut Vec<usize>, selection: &mut Vec<Item>) {
+ if !counters.is_empty() {
+ selection.push(Item::Counters(std::mem::take(counters)))
+ }
+ }
+
+ let mut counters = Vec::new();
+
+ for name in names.split(",") {
+ let name = name.trim();
+ let is_graph = name.starts_with("#");
+ let is_indicator = name.starts_with("*");
+ let name = if is_graph || is_indicator {
+ &name[1..]
+ } else {
+ name
+ };
+ // See comment about the ui string syntax at the top of this file.
+ match name {
+ "" => {
+ flush_counters(&mut counters, selection);
+ selection.push(Item::Space);
+ }
+ "|" => {
+ flush_counters(&mut counters, selection);
+ selection.push(Item::Column);
+ }
+ "_" => {
+ flush_counters(&mut counters, selection);
+ selection.push(Item::Row);
+ }
+ "FPS" => {
+ flush_counters(&mut counters, selection);
+ selection.push(Item::Fps);
+ }
+ "GPU time queries" => {
+ flush_counters(&mut counters, selection);
+ selection.push(Item::GpuTimeQueries);
+ }
+ "GPU cache bars" => {
+ flush_counters(&mut counters, selection);
+ selection.push(Item::GpuCacheBars);
+ }
+ "Paint phase graph" => {
+ flush_counters(&mut counters, selection);
+ selection.push(Item::PaintPhaseGraph);
+ }
+ _ => {
+ if let Some(idx) = self.index_of(name) {
+ if is_graph {
+ flush_counters(&mut counters, selection);
+ selection.push(Item::Graph(idx));
+ } else if is_indicator {
+ flush_counters(&mut counters, selection);
+ selection.push(Item::ChangeIndicator(idx));
+ } else {
+ counters.push(idx);
+ }
+ } else if let Some(preset_str) = find_preset(name) {
+ flush_counters(&mut counters, selection);
+ self.append_to_ui(selection, preset_str);
+ } else {
+ selection.push(Item::Text(format!("Unknonw counter: {}", name)));
+ }
+ }
+ }
+ }
+
+ flush_counters(&mut counters, selection);
+ }
+
+ pub fn set_counters(&mut self, counters: &mut TransactionProfile) {
+ for (id, evt) in counters.events.iter_mut().enumerate() {
+ if let Event::Value(val) = *evt {
+ self.counters[id].set(val);
+ }
+ *evt = Event::None;
+ }
+ }
+
+ pub fn get(&self, id: usize) -> Option<f64> {
+ self.counters[id].get()
+ }
+
+ fn draw_counters(
+ counters: &[Counter],
+ selected: &[usize],
+ mut x: f32, mut y: f32,
+ text_buffer: &mut String,
+ debug_renderer: &mut DebugRenderer,
+ ) -> default::Rect<f32> {
+ let line_height = debug_renderer.line_height();
+
+ x += PROFILE_PADDING;
+ y += PROFILE_PADDING;
+ let origin = default::Point2D::new(x, y);
+ y += line_height * 0.5;
+
+ let mut total_rect = Rect::zero();
+
+ let mut color_index = 0;
+ let colors = [
+ // Regular values,
+ ColorU::new(255, 255, 255, 255),
+ ColorU::new(255, 255, 0, 255),
+ // Unexpected values,
+ ColorU::new(255, 80, 0, 255),
+ ColorU::new(255, 0, 0, 255),
+ ];
+
+ for idx in selected {
+ // If The index is invalid, add some vertical space.
+ let counter = &counters[*idx];
+
+ let rect = debug_renderer.add_text(
+ x, y,
+ counter.name,
+ colors[color_index],
+ None,
+ );
+ color_index = (color_index + 1) % 2;
+
+ total_rect = total_rect.union(&rect);
+ y += line_height;
+ }
+
+ color_index = 0;
+ x = total_rect.max_x() + 60.0;
+ y = origin.y + line_height * 0.5;
+
+ for idx in selected {
+ let counter = &counters[*idx];
+ let expected_offset = if counter.has_unexpected_avg_max() { 2 } else { 0 };
+
+ counter.write_value(text_buffer);
+
+ let rect = debug_renderer.add_text(
+ x,
+ y,
+ &text_buffer,
+ colors[color_index + expected_offset],
+ None,
+ );
+ color_index = (color_index + 1) % 2;
+
+ total_rect = total_rect.union(&rect);
+ y += line_height;
+ }
+
+ total_rect = total_rect
+ .union(&Rect { origin, size: Size2D::new(1.0, 1.0) })
+ .inflate(PROFILE_PADDING, PROFILE_PADDING);
+
+ debug_renderer.add_quad(
+ total_rect.min_x(),
+ total_rect.min_y(),
+ total_rect.max_x(),
+ total_rect.max_y(),
+ BACKGROUND_COLOR,
+ BACKGROUND_COLOR,
+ );
+
+ total_rect
+ }
+
+ fn draw_graph(
+ counter: &Counter,
+ x: f32,
+ y: f32,
+ text_buffer: &mut String,
+ debug_renderer: &mut DebugRenderer,
+ ) -> default::Rect<f32> {
+ let graph = counter.graph.as_ref().unwrap();
+
+ let max_samples = graph.values.capacity() as f32;
+
+ let size = Size2D::new(max_samples, 100.0);
+ let line_height = debug_renderer.line_height();
+ let graph_rect = Rect::new(Point2D::new(x + PROFILE_PADDING, y + PROFILE_PADDING), size);
+ let mut rect = graph_rect.inflate(PROFILE_PADDING, PROFILE_PADDING);
+
+ let stats = graph.stats();
+
+ let text_color = ColorU::new(255, 255, 0, 255);
+ let text_origin = rect.origin + vec2(rect.size.width, 25.0);
+ set_text!(text_buffer, "{} ({})", counter.name, counter.unit);
+ debug_renderer.add_text(
+ text_origin.x,
+ text_origin.y,
+ if counter.unit == "" { counter.name } else { text_buffer },
+ ColorU::new(0, 255, 0, 255),
+ None,
+ );
+
+ set_text!(text_buffer, "Samples: {}", stats.samples);
+
+ debug_renderer.add_text(
+ text_origin.x,
+ text_origin.y + line_height,
+ text_buffer,
+ text_color,
+ None,
+ );
+
+ if stats.samples > 0 {
+ set_text!(text_buffer, "Min: {:.2} {}", stats.min, counter.unit);
+ debug_renderer.add_text(
+ text_origin.x,
+ text_origin.y + line_height * 2.0,
+ text_buffer,
+ text_color,
+ None,
+ );
+
+ set_text!(text_buffer, "Avg: {:.2} {}", stats.avg, counter.unit);
+ debug_renderer.add_text(
+ text_origin.x,
+ text_origin.y + line_height * 3.0,
+ text_buffer,
+ text_color,
+ None,
+ );
+
+ set_text!(text_buffer, "Max: {:.2} {}", stats.max, counter.unit);
+ debug_renderer.add_text(
+ text_origin.x,
+ text_origin.y + line_height * 4.0,
+ text_buffer,
+ text_color,
+ None,
+ );
+ }
+
+ rect.size.width += 220.0;
+ debug_renderer.add_quad(
+ rect.min_x(),
+ rect.min_y(),
+ rect.max_x(),
+ rect.max_y(),
+ BACKGROUND_COLOR,
+ BACKGROUND_COLOR,
+ );
+
+ let bx1 = graph_rect.max_x();
+ let by1 = graph_rect.max_y();
+
+ let w = graph_rect.size.width / max_samples;
+ let h = graph_rect.size.height;
+
+ let color_t0 = ColorU::new(0, 255, 0, 255);
+ let color_b0 = ColorU::new(0, 180, 0, 255);
+
+ let color_t2 = ColorU::new(255, 0, 0, 255);
+ let color_b2 = ColorU::new(180, 0, 0, 255);
+
+ for (index, sample) in graph.values.iter().enumerate() {
+ if !sample.is_finite() {
+ // NAN means no sample this frame.
+ continue;
+ }
+ let sample = *sample as f32;
+ let x1 = bx1 - index as f32 * w;
+ let x0 = x1 - w;
+
+ let y0 = by1 - (sample / stats.max as f32) as f32 * h;
+ let y1 = by1;
+
+ let (color_top, color_bottom) = if counter.is_unexpected_value(sample as f64) {
+ (color_t2, color_b2)
+ } else {
+ (color_t0, color_b0)
+ };
+
+ debug_renderer.add_quad(x0, y0, x1, y1, color_top, color_bottom);
+ }
+
+ rect
+ }
+
+
+ fn draw_change_indicator(
+ counter: &Counter,
+ x: f32, y: f32,
+ debug_renderer: &mut DebugRenderer
+ ) -> default::Rect<f32> {
+ let height = 10.0;
+ let width = 20.0;
+
+ // Draw the indicator red instead of blue if is is not within expected ranges.
+ let color = if counter.has_unexpected_value() || counter.has_unexpected_avg_max() {
+ ColorU::new(255, 20, 20, 255)
+ } else {
+ ColorU::new(0, 100, 250, 255)
+ };
+
+ let tx = counter.change_indicator as f32 * width;
+ debug_renderer.add_quad(
+ x,
+ y,
+ x + 15.0 * width,
+ y + height,
+ ColorU::new(0, 0, 0, 150),
+ ColorU::new(0, 0, 0, 150),
+ );
+
+ debug_renderer.add_quad(
+ x + tx,
+ y,
+ x + tx + width,
+ y + height,
+ color,
+ ColorU::new(25, 25, 25, 255),
+ );
+
+ Rect {
+ origin: Point2D::new(x, y),
+ size: Size2D::new(15.0 * width + 20.0, height),
+ }
+ }
+
+ fn draw_bar(
+ label: &str,
+ label_color: ColorU,
+ counters: &[(ColorU, usize)],
+ x: f32, y: f32,
+ debug_renderer: &mut DebugRenderer,
+ ) -> default::Rect<f32> {
+ let x = x + 8.0;
+ let y = y + 24.0;
+ let text_rect = debug_renderer.add_text(
+ x, y,
+ label,
+ label_color,
+ None,
+ );
+
+ let x_base = text_rect.max_x() + 10.0;
+ let width = 300.0;
+ let total_value = counters.last().unwrap().1;
+ let scale = width / total_value as f32;
+ let mut x_current = x_base;
+
+ for &(color, counter) in counters {
+ let x_stop = x_base + counter as f32 * scale;
+ debug_renderer.add_quad(
+ x_current,
+ text_rect.origin.y,
+ x_stop,
+ text_rect.max_y(),
+ color,
+ color,
+ );
+ x_current = x_stop;
+
+ }
+
+ let mut total_rect = text_rect;
+ total_rect.size.width += width + 10.0;
+
+ total_rect
+ }
+
+ fn draw_gpu_cache_bars(&self, x: f32, mut y: f32, text_buffer: &mut String, debug_renderer: &mut DebugRenderer) -> default::Rect<f32> {
+ let color_updated = ColorU::new(0xFF, 0, 0, 0xFF);
+ let color_free = ColorU::new(0, 0, 0xFF, 0xFF);
+ let color_saved = ColorU::new(0, 0xFF, 0, 0xFF);
+
+ let updated_blocks = self.get(GPU_CACHE_BLOCKS_UPDATED).unwrap_or(0.0) as usize;
+ let saved_blocks = self.get(GPU_CACHE_BLOCKS_SAVED).unwrap_or(0.0) as usize;
+ let allocated_blocks = self.get(GPU_CACHE_BLOCKS_TOTAL).unwrap_or(0.0) as usize;
+ let allocated_rows = self.get(GPU_CACHE_ROWS_TOTAL).unwrap_or(0.0) as usize;
+ let updated_rows = self.get(GPU_CACHE_ROWS_UPDATED).unwrap_or(0.0) as usize;
+ let requested_blocks = updated_blocks + saved_blocks;
+ let total_blocks = allocated_rows * MAX_VERTEX_TEXTURE_WIDTH;
+
+ set_text!(text_buffer, "GPU cache rows ({}):", allocated_rows);
+
+ let rect0 = Profiler::draw_bar(
+ text_buffer,
+ ColorU::new(0xFF, 0xFF, 0xFF, 0xFF),
+ &[
+ (color_updated, updated_rows),
+ (color_free, allocated_rows),
+ ],
+ x, y,
+ debug_renderer,
+ );
+
+ y = rect0.max_y();
+
+ let rect1 = Profiler::draw_bar(
+ "GPU cache blocks",
+ ColorU::new(0xFF, 0xFF, 0, 0xFF),
+ &[
+ (color_updated, updated_blocks),
+ (color_saved, requested_blocks),
+ (color_free, allocated_blocks),
+ (ColorU::new(0, 0, 0, 0xFF), total_blocks),
+ ],
+ x, y,
+ debug_renderer,
+ );
+
+ let total_rect = rect0.union(&rect1).inflate(10.0, 10.0);
+ debug_renderer.add_quad(
+ total_rect.origin.x,
+ total_rect.origin.y,
+ total_rect.origin.x + total_rect.size.width,
+ total_rect.origin.y + total_rect.size.height,
+ ColorF::new(0.1, 0.1, 0.1, 0.8).into(),
+ ColorF::new(0.2, 0.2, 0.2, 0.8).into(),
+ );
+
+ total_rect
+ }
+
+ // Draws a frame graph for a given frame collection.
+ fn draw_frame_graph(
+ frame_collection: &ProfilerFrameCollection,
+ x: f32, y: f32,
+ debug_renderer: &mut DebugRenderer,
+ ) -> default::Rect<f32> {
+ let mut has_data = false;
+ for frame in &frame_collection.frames {
+ if !frame.samples.is_empty() {
+ has_data = true;
+ break;
+ }
+ }
+
+ if !has_data {
+ return Rect::zero();
+ }
+
+ let graph_rect = Rect::new(
+ Point2D::new(x + GRAPH_PADDING, y + GRAPH_PADDING),
+ Size2D::new(GRAPH_WIDTH, GRAPH_HEIGHT),
+ );
+ let bounding_rect = graph_rect.inflate(GRAPH_PADDING, GRAPH_PADDING);
+
+ debug_renderer.add_quad(
+ bounding_rect.origin.x,
+ bounding_rect.origin.y,
+ bounding_rect.origin.x + bounding_rect.size.width,
+ bounding_rect.origin.y + bounding_rect.size.height,
+ BACKGROUND_COLOR,
+ BACKGROUND_COLOR,
+ );
+
+ let w = graph_rect.size.width;
+ let mut y0 = graph_rect.origin.y;
+
+ let mut max_time = frame_collection.frames
+ .iter()
+ .max_by_key(|f| f.total_time)
+ .unwrap()
+ .total_time as f32;
+
+ // If the max time is lower than 16ms, fix the scale
+ // at 16ms so that the graph is easier to interpret.
+ let baseline_ns = 16_000_000.0; // 16ms
+ max_time = max_time.max(baseline_ns);
+
+ let mut tags_present = FastHashMap::default();
+
+ for frame in &frame_collection.frames {
+ let y1 = y0 + GRAPH_FRAME_HEIGHT;
+
+ let mut current_ns = 0;
+ for sample in &frame.samples {
+ let x0 = graph_rect.origin.x + w * current_ns as f32 / max_time;
+ current_ns += sample.time_ns;
+ let x1 = graph_rect.origin.x + w * current_ns as f32 / max_time;
+ let mut bottom_color = sample.tag.color;
+ bottom_color.a *= 0.5;
+
+ debug_renderer.add_quad(
+ x0,
+ y0,
+ x1,
+ y1,
+ sample.tag.color.into(),
+ bottom_color.into(),
+ );
+
+ tags_present.insert(sample.tag.label, sample.tag.color);
+ }
+
+ y0 = y1;
+ }
+
+ // If the max time is higher than 16ms, show a vertical line at the
+ // 16ms mark.
+ if max_time > baseline_ns {
+ let x = graph_rect.origin.x + w * baseline_ns as f32 / max_time;
+ let height = frame_collection.frames.len() as f32 * GRAPH_FRAME_HEIGHT;
+
+ debug_renderer.add_quad(
+ x,
+ graph_rect.origin.y,
+ x + 4.0,
+ graph_rect.origin.y + height,
+ ColorU::new(120, 00, 00, 150),
+ ColorU::new(120, 00, 00, 100),
+ );
+ }
+
+
+ // Add a legend to see which color correspond to what primitive.
+ const LEGEND_SIZE: f32 = 20.0;
+ const PADDED_LEGEND_SIZE: f32 = 25.0;
+ if !tags_present.is_empty() {
+ debug_renderer.add_quad(
+ bounding_rect.max_x() + GRAPH_PADDING,
+ bounding_rect.origin.y,
+ bounding_rect.max_x() + GRAPH_PADDING + 200.0,
+ bounding_rect.origin.y + tags_present.len() as f32 * PADDED_LEGEND_SIZE + GRAPH_PADDING,
+ BACKGROUND_COLOR,
+ BACKGROUND_COLOR,
+ );
+ }
+
+ for (i, (label, &color)) in tags_present.iter().enumerate() {
+ let x0 = bounding_rect.origin.x + bounding_rect.size.width + GRAPH_PADDING * 2.0;
+ let y0 = bounding_rect.origin.y + GRAPH_PADDING + i as f32 * PADDED_LEGEND_SIZE;
+
+ debug_renderer.add_quad(
+ x0, y0, x0 + LEGEND_SIZE, y0 + LEGEND_SIZE,
+ color.into(),
+ color.into(),
+ );
+
+ debug_renderer.add_text(
+ x0 + PADDED_LEGEND_SIZE,
+ y0 + LEGEND_SIZE * 0.75,
+ label,
+ ColorU::new(255, 255, 0, 255),
+ None,
+ );
+ }
+
+ bounding_rect
+ }
+
+ pub fn draw_profile(
+ &mut self,
+ _frame_index: u64,
+ debug_renderer: &mut DebugRenderer,
+ device_size: DeviceIntSize,
+ ) {
+ let x_start = 20.0;
+ let mut y_start = 150.0;
+ let default_column_width = 400.0;
+
+ // set_text!(..) into this string instead of using format!(..) to avoid
+ // unnecessary allocations.
+ let mut text_buffer = String::with_capacity(32);
+
+ let mut column_width = default_column_width;
+ let mut max_y = y_start;
+
+ let mut x = x_start;
+ let mut y = y_start;
+
+ for elt in &self.ui {
+ let rect = match elt {
+ Item::Counters(indices) => {
+ Profiler::draw_counters(&self.counters, &indices, x, y, &mut text_buffer, debug_renderer)
+ }
+ Item::Graph(idx) => {
+ Profiler::draw_graph(&self.counters[*idx], x, y, &mut text_buffer, debug_renderer)
+ }
+ Item::ChangeIndicator(idx) => {
+ Profiler::draw_change_indicator(&self.counters[*idx], x, y, debug_renderer)
+ }
+ Item::GpuTimeQueries => {
+ Profiler::draw_frame_graph(&self.gpu_frames, x, y, debug_renderer)
+ }
+ Item::GpuCacheBars => {
+ self.draw_gpu_cache_bars(x, y, &mut text_buffer, debug_renderer)
+ }
+ Item::PaintPhaseGraph => {
+ Profiler::draw_frame_graph(&self.frame_stats, x, y, debug_renderer)
+ }
+ Item::Text(text) => {
+ let p = 10.0;
+ let mut rect = debug_renderer.add_text(
+ x + p,
+ y + p,
+ &text,
+ ColorU::new(255, 255, 255, 255),
+ None,
+ );
+ rect = rect.inflate(p, p);
+
+ debug_renderer.add_quad(
+ rect.origin.x,
+ rect.origin.y,
+ rect.max_x(),
+ rect.max_y(),
+ BACKGROUND_COLOR,
+ BACKGROUND_COLOR,
+ );
+
+ rect
+ }
+ Item::Fps => {
+ let fps = self.frame_timestamps_within_last_second.len();
+ set_text!(&mut text_buffer, "{} fps", fps);
+ let mut rect = debug_renderer.add_text(
+ x + PROFILE_PADDING,
+ y + PROFILE_PADDING + 5.0,
+ &text_buffer,
+ ColorU::new(255, 255, 255, 255),
+ None,
+ );
+ rect = rect.inflate(PROFILE_PADDING, PROFILE_PADDING);
+
+ debug_renderer.add_quad(
+ rect.min_x(),
+ rect.min_y(),
+ rect.max_x(),
+ rect.max_y(),
+ BACKGROUND_COLOR,
+ BACKGROUND_COLOR,
+ );
+
+ rect
+ }
+ Item::Space => {
+ Rect { origin: Point2D::new(x, y), size: Size2D::new(0.0, PROFILE_SPACING) }
+ }
+ Item::Column => {
+ max_y = max_y.max(y);
+ x += column_width + PROFILE_SPACING;
+ y = y_start;
+ column_width = default_column_width;
+
+ continue;
+ }
+ Item::Row => {
+ max_y = max_y.max(y);
+ y_start = max_y + PROFILE_SPACING;
+ y = y_start;
+ x = x_start;
+ column_width = default_column_width;
+
+ continue;
+ }
+ };
+
+ column_width = column_width.max(rect.size.width);
+ y = rect.max_y();
+
+ if y > device_size.height as f32 - 100.0 {
+ max_y = max_y.max(y);
+ x += column_width + PROFILE_SPACING;
+ y = y_start;
+ column_width = default_column_width;
+ }
+ }
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn dump_stats(&self, sink: &mut dyn std::io::Write) -> std::io::Result<()> {
+ for counter in &self.counters {
+ if counter.value.is_finite() {
+ writeln!(sink, "{} {:?}{}", counter.name, counter.value, counter.unit)?;
+ }
+ }
+
+ Ok(())
+ }
+}
+
+/// Defines the interface for hooking up an external profiler to WR.
+pub trait ProfilerHooks : Send + Sync {
+ /// Register a thread with the profiler.
+ fn register_thread(&self, thread_name: &str);
+
+ /// Unregister a thread with the profiler.
+ fn unregister_thread(&self);
+
+ /// Called at the beginning of a profile scope.
+ fn begin_marker(&self, label: &str);
+
+ /// Called at the end of a profile scope.
+ fn end_marker(&self, label: &str);
+
+ /// Called to mark an event happening.
+ fn event_marker(&self, label: &str);
+
+ /// Called with a duration to indicate a text marker that just ended. Text
+ /// markers allow different types of entries to be recorded on the same row
+ /// in the timeline, by adding labels to the entry.
+ ///
+ /// This variant is also useful when the caller only wants to record events
+ /// longer than a certain threshold, and thus they don't know in advance
+ /// whether the event will qualify.
+ fn add_text_marker(&self, label: &str, text: &str, duration: Duration);
+
+ /// Returns true if the current thread is being profiled.
+ fn thread_is_being_profiled(&self) -> bool;
+}
+
+/// The current global profiler callbacks, if set by embedder.
+pub static mut PROFILER_HOOKS: Option<&'static dyn ProfilerHooks> = None;
+
+/// Set the profiler callbacks, or None to disable the profiler.
+/// This function must only ever be called before any WR instances
+/// have been created, or the hooks will not be set.
+pub fn set_profiler_hooks(hooks: Option<&'static dyn ProfilerHooks>) {
+ if !wr_has_been_initialized() {
+ unsafe {
+ PROFILER_HOOKS = hooks;
+ }
+ }
+}
+
+/// A simple RAII style struct to manage a profile scope.
+pub struct ProfileScope {
+ name: &'static str,
+}
+
+
+/// Register a thread with the Gecko Profiler.
+pub fn register_thread(thread_name: &str) {
+ unsafe {
+ if let Some(ref hooks) = PROFILER_HOOKS {
+ hooks.register_thread(thread_name);
+ }
+ }
+}
+
+
+/// Unregister a thread with the Gecko Profiler.
+pub fn unregister_thread() {
+ unsafe {
+ if let Some(ref hooks) = PROFILER_HOOKS {
+ hooks.unregister_thread();
+ }
+ }
+}
+
+/// Records a marker of the given duration that just ended.
+pub fn add_text_marker(label: &str, text: &str, duration: Duration) {
+ unsafe {
+ if let Some(ref hooks) = PROFILER_HOOKS {
+ hooks.add_text_marker(label, text, duration);
+ }
+ }
+}
+
+/// Records a marker of the given duration that just ended.
+pub fn add_event_marker(label: &str) {
+ unsafe {
+ if let Some(ref hooks) = PROFILER_HOOKS {
+ hooks.event_marker(label);
+ }
+ }
+}
+
+/// Returns true if the current thread is being profiled.
+pub fn thread_is_being_profiled() -> bool {
+ unsafe {
+ PROFILER_HOOKS.map_or(false, |h| h.thread_is_being_profiled())
+ }
+}
+
+impl ProfileScope {
+ /// Begin a new profile scope
+ pub fn new(name: &'static str) -> Self {
+ unsafe {
+ if let Some(ref hooks) = PROFILER_HOOKS {
+ hooks.begin_marker(name);
+ }
+ }
+
+ ProfileScope {
+ name,
+ }
+ }
+}
+
+impl Drop for ProfileScope {
+ fn drop(&mut self) {
+ unsafe {
+ if let Some(ref hooks) = PROFILER_HOOKS {
+ hooks.end_marker(self.name);
+ }
+ }
+ }
+}
+
+/// A helper macro to define profile scopes.
+macro_rules! profile_marker {
+ ($string:expr) => {
+ let _scope = $crate::profiler::ProfileScope::new($string);
+ };
+}
+
+#[derive(Debug, Clone)]
+pub struct GpuProfileTag {
+ pub label: &'static str,
+ pub color: ColorF,
+}
+
+/// Ranges of expected value for a profile counter.
+#[derive(Clone, Debug)]
+pub struct Expected<T> {
+ pub range: Option<Range<T>>,
+ pub avg: Option<Range<T>>,
+}
+
+impl<T> Expected<T> {
+ const fn none() -> Self {
+ Expected {
+ range: None,
+ avg: None,
+ }
+ }
+}
+
+const fn expected<T>(range: Range<T>) -> Expected<T> {
+ Expected {
+ range: Some(range),
+ avg: None,
+ }
+}
+
+impl Expected<f64> {
+ const fn avg(mut self, avg: Range<f64>) -> Self {
+ self.avg = Some(avg);
+ self
+ }
+}
+
+impl Expected<i64> {
+ const fn avg(mut self, avg: Range<i64>) -> Self {
+ self.avg = Some(avg);
+ self
+ }
+
+ fn into_float(self) -> Expected<f64> {
+ Expected {
+ range: match self.range {
+ Some(r) => Some(r.start as f64 .. r.end as f64),
+ None => None,
+ },
+ avg: match self.avg {
+ Some(r) => Some(r.start as f64 .. r.end as f64),
+ None => None,
+ },
+ }
+ }
+}
+
+pub struct CounterDescriptor {
+ pub name: &'static str,
+ pub unit: &'static str,
+ pub index: usize,
+ pub show_as: ShowAs,
+ pub expected: Expected<f64>,
+}
+
+#[derive(Debug)]
+pub struct Counter {
+ pub name: &'static str,
+ pub unit: &'static str,
+ pub show_as: ShowAs,
+ pub expected: Expected<f64>,
+
+ ///
+ value: f64,
+ /// Number of samples in the current time slice.
+ num_samples: u64,
+ /// Sum of the values recorded during the current time slice.
+ sum: f64,
+ /// The max value in in-progress time slice.
+ next_max: f64,
+ /// The max value of the previous time slice (displayed).
+ max: f64,
+ /// The average value of the previous time slice (displayed).
+ avg: f64,
+ /// Incremented when the counter changes.
+ change_indicator: u8,
+
+ graph: Option<Graph>,
+}
+
+impl Counter {
+ pub fn new(descriptor: &CounterDescriptor) -> Self {
+ Counter {
+ name: descriptor.name,
+ unit: descriptor.unit,
+ show_as: descriptor.show_as,
+ expected: descriptor.expected.clone(),
+ value: std::f64::NAN,
+ num_samples: 0,
+ sum: 0.0,
+ next_max: 0.0,
+ max: 0.0,
+ avg: 0.0,
+ change_indicator: 0,
+ graph: None,
+ }
+ }
+ pub fn set_f64(&mut self, val: f64) {
+ self.value = val;
+ }
+
+ pub fn set<T>(&mut self, val: T) where T: Into<f64> {
+ self.set_f64(val.into());
+ }
+
+ pub fn get(&self) -> Option<f64> {
+ if self.value.is_finite() {
+ Some(self.value)
+ } else {
+ None
+ }
+ }
+
+ pub fn write_value(&self, output: &mut String) {
+ match self.show_as {
+ ShowAs::Float => {
+ set_text!(output, "{:.2} {} (max: {:.2})", self.avg, self.unit, self.max);
+ }
+ ShowAs::Int => {
+ set_text!(output, "{:.0} {} (max: {:.0})", self.avg.round(), self.unit, self.max.round());
+ }
+ }
+ }
+
+ pub fn enable_graph(&mut self, max_samples: usize) {
+ if self.graph.is_some() {
+ return;
+ }
+
+ self.graph = Some(Graph::new(max_samples));
+ }
+
+ pub fn disable_graph(&mut self) {
+ self.graph = None;
+ }
+
+ pub fn is_unexpected_value(&self, value: f64) -> bool {
+ if let Some(range) = &self.expected.range {
+ return value.is_finite() && value >= range.end;
+ }
+
+ false
+ }
+
+ pub fn has_unexpected_value(&self) -> bool {
+ self.is_unexpected_value(self.value)
+ }
+
+ pub fn has_unexpected_avg_max(&self) -> bool {
+ if let Some(range) = &self.expected.range {
+ if self.max.is_finite() && self.max >= range.end {
+ return true;
+ }
+ }
+
+ if let Some(range) = &self.expected.avg {
+ if self.avg < range.start || self.avg >= range.end {
+ return true;
+ }
+ }
+
+ false
+ }
+
+ fn update(&mut self, update_avg: bool) {
+ let updated = self.value.is_finite();
+ if updated {
+ self.next_max = self.next_max.max(self.value);
+ self.sum += self.value;
+ self.num_samples += 1;
+ self.change_indicator = (self.change_indicator + 1) % 15;
+ }
+
+ if let Some(graph) = &mut self.graph {
+ graph.set(self.value);
+ }
+
+ self.value = std::f64::NAN;
+
+ if update_avg {
+ if self.num_samples > 0 {
+ self.avg = self.sum / self.num_samples as f64;
+ self.max = self.next_max;
+ } else {
+ // There has been no sample in the averaging window, just show zero.
+ self.avg = 0.0;
+ self.max = 0.0;
+ }
+ self.sum = 0.0;
+ self.num_samples = 0;
+ self.next_max = std::f64::MIN;
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug)]
+pub enum Event {
+ Start(u64),
+ Value(f64),
+ None,
+}
+
+// std::convert::From/TryFrom can't deal with integer to f64 so we roll our own...
+pub trait EventValue {
+ fn into_f64(self) -> f64;
+}
+
+impl EventValue for f64 { fn into_f64(self) -> f64 { self } }
+impl EventValue for f32 { fn into_f64(self) -> f64 { self as f64 } }
+impl EventValue for u32 { fn into_f64(self) -> f64 { self as f64 } }
+impl EventValue for i32 { fn into_f64(self) -> f64 { self as f64 } }
+impl EventValue for u64 { fn into_f64(self) -> f64 { self as f64 } }
+impl EventValue for usize { fn into_f64(self) -> f64 { self as f64 } }
+
+/// A container for profiling information that moves along the rendering pipeline
+/// and is handed off to the profiler at the end.
+pub struct TransactionProfile {
+ pub events: Vec<Event>,
+}
+
+impl TransactionProfile {
+ pub fn new() -> Self {
+ TransactionProfile {
+ events: vec![Event::None; NUM_PROFILER_EVENTS],
+ }
+ }
+
+ pub fn start_time(&mut self, id: usize) {
+ let ns = precise_time_ns();
+ self.events[id] = Event::Start(ns);
+ }
+
+ pub fn end_time(&mut self, id: usize) -> f64 {
+ self.end_time_if_started(id).unwrap()
+ }
+
+ /// Similar to end_time, but doesn't panic if not matched with start_time.
+ pub fn end_time_if_started(&mut self, id: usize) -> Option<f64> {
+ if let Event::Start(start) = self.events[id] {
+ let now = precise_time_ns();
+ let time_ns = now - start;
+
+ let time_ms = ns_to_ms(time_ns);
+ self.events[id] = Event::Value(time_ms);
+
+ Some(time_ms)
+ } else {
+ None
+ }
+ }
+
+ pub fn set<T>(&mut self, id: usize, value: T) where T: EventValue {
+ self.set_f64(id, value.into_f64());
+ }
+
+
+ pub fn set_f64(&mut self, id: usize, value: f64) {
+ self.events[id] = Event::Value(value);
+ }
+
+ pub fn get(&self, id: usize) -> Option<f64> {
+ if let Event::Value(val) = self.events[id] {
+ Some(val)
+ } else {
+ None
+ }
+ }
+
+ pub fn get_or(&self, id: usize, or: f64) -> f64 {
+ self.get(id).unwrap_or(or)
+ }
+
+ pub fn add<T>(&mut self, id: usize, n: T) where T: EventValue {
+ let n = n.into_f64();
+
+ let evt = &mut self.events[id];
+
+ let val = match *evt {
+ Event::Value(v) => v + n,
+ Event::None => n,
+ Event::Start(..) => { panic!(); }
+ };
+
+ *evt = Event::Value(val);
+ }
+
+ pub fn inc(&mut self, id: usize) {
+ self.add(id, 1.0);
+ }
+
+ pub fn take(&mut self) -> Self {
+ TransactionProfile {
+ events: std::mem::take(&mut self.events),
+ }
+ }
+
+ pub fn take_and_reset(&mut self) -> Self {
+ let events = std::mem::take(&mut self.events);
+
+ *self = TransactionProfile::new();
+
+ TransactionProfile { events }
+ }
+
+ pub fn merge(&mut self, other: &mut Self) {
+ for i in 0..self.events.len() {
+ match (self.events[i], other.events[i]) {
+ (Event::Value(v1), Event::Value(v2)) => {
+ self.events[i] = Event::Value(v1.max(v2));
+ }
+ (Event::Value(_), _) => {}
+ (_, Event::Value(v2)) => {
+ self.events[i] = Event::Value(v2);
+ }
+ (Event::None, evt) => {
+ self.events[i] = evt;
+ }
+ (Event::Start(s1), Event::Start(s2)) => {
+ self.events[i] = Event::Start(s1.max(s2));
+ }
+ _=> {}
+ }
+ other.events[i] = Event::None;
+ }
+ }
+
+ pub fn clear(&mut self) {
+ for evt in &mut self.events {
+ *evt = Event::None;
+ }
+ }
+}
+
+impl GlyphRasterizeProfiler for TransactionProfile {
+ fn start_time(&mut self) {
+ let id = GLYPH_RESOLVE_TIME;
+ let ns = precise_time_ns();
+ self.events[id] = Event::Start(ns);
+ }
+
+ fn end_time(&mut self) -> f64 {
+ let id = GLYPH_RESOLVE_TIME;
+ self.end_time_if_started(id).unwrap()
+ }
+
+ fn set(&mut self, value: f64) {
+ let id = RASTERIZED_GLYPHS;
+ self.set_f64(id, value);
+ }
+}
+
+#[derive(Debug)]
+pub struct GraphStats {
+ pub min: f64,
+ pub avg: f64,
+ pub max: f64,
+ pub sum: f64,
+ pub samples: usize,
+}
+
+#[derive(Debug)]
+pub struct Graph {
+ values: VecDeque<f64>,
+}
+
+impl Graph {
+ fn new(max_samples: usize) -> Self {
+ let mut values = VecDeque::new();
+ values.reserve(max_samples);
+
+ Graph { values }
+ }
+
+ fn set(&mut self, val: f64) {
+ if self.values.len() == self.values.capacity() {
+ self.values.pop_back();
+ }
+ self.values.push_front(val);
+ }
+
+ pub fn stats(&self) -> GraphStats {
+ let mut stats = GraphStats {
+ min: f64::MAX,
+ avg: 0.0,
+ max: -f64::MAX,
+ sum: 0.0,
+ samples: 0,
+ };
+
+ let mut samples = 0;
+ for value in &self.values {
+ if value.is_finite() {
+ stats.min = stats.min.min(*value);
+ stats.max = stats.max.max(*value);
+ stats.sum += *value;
+ samples += 1;
+ }
+ }
+
+ if samples > 0 {
+ stats.avg = stats.sum / samples as f64;
+ stats.samples = samples;
+ }
+
+ stats
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum ShowAs {
+ Float,
+ Int,
+}
+
+struct ProfilerFrame {
+ total_time: u64,
+ samples: Vec<GpuTimer>,
+}
+
+struct ProfilerFrameCollection {
+ frames: VecDeque<ProfilerFrame>,
+}
+
+impl ProfilerFrameCollection {
+ fn new() -> Self {
+ ProfilerFrameCollection {
+ frames: VecDeque::new(),
+ }
+ }
+
+ fn push(&mut self, frame: ProfilerFrame) {
+ if self.frames.len() == 20 {
+ self.frames.pop_back();
+ }
+ self.frames.push_front(frame);
+ }
+}
+
+impl From<FullFrameStats> for ProfilerFrame {
+ fn from(stats: FullFrameStats) -> ProfilerFrame {
+ let new_sample = |time, label, color| -> GpuTimer {
+ let tag = GpuProfileTag {
+ label,
+ color
+ };
+
+ let time_ns = ms_to_ns(time);
+
+ GpuTimer {
+ tag, time_ns
+ }
+ };
+
+ let samples = vec![
+ new_sample(stats.gecko_display_list_time, "Gecko DL", ColorF { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }),
+ new_sample(stats.wr_display_list_time, "WR DL", ColorF { r: 0.0, g: 1.0, b: 1.0, a: 1.0 }),
+ new_sample(stats.scene_build_time, "Scene Build", ColorF { r: 1.0, g: 0.0, b: 1.0, a: 1.0 }),
+ new_sample(stats.frame_build_time, "Frame Build", ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }),
+ ];
+
+ ProfilerFrame {
+ total_time: ms_to_ns(stats.total()),
+ samples
+ }
+ }
+}
+
+pub fn ns_to_ms(ns: u64) -> f64 {
+ ns as f64 / 1_000_000.0
+}
+
+pub fn ms_to_ns(ms: f64) -> u64 {
+ (ms * 1_000_000.0) as u64
+}
+
+pub fn bytes_to_mb(bytes: usize) -> f64 {
+ bytes as f64 / 1_000_000.0
+}
+
+#[derive(Debug, PartialEq)]
+enum Item {
+ Counters(Vec<usize>),
+ Graph(usize),
+ ChangeIndicator(usize),
+ Fps,
+ GpuTimeQueries,
+ GpuCacheBars,
+ PaintPhaseGraph,
+ Text(String),
+ Space,
+ Column,
+ Row,
+}
+
diff --git a/gfx/wr/webrender/src/rectangle_occlusion.rs b/gfx/wr/webrender/src/rectangle_occlusion.rs
new file mode 100644
index 0000000000..a79e4ba026
--- /dev/null
+++ b/gfx/wr/webrender/src/rectangle_occlusion.rs
@@ -0,0 +1,208 @@
+/* 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 simple occlusion culling algorithm for axis-aligned rectangles.
+//!
+//! ## Output
+//!
+//! Occlusion culling results in two lists of rectangles:
+//!
+//! - The opaque list should be rendered first. None of its rectangles overlap so order doesn't matter
+//! within the opaque pass.
+//! - The non-opaque list (or alpha list) which should be rendered in back-to-front order after the opaque pass.
+//!
+//! The output has minimal overdraw (no overdraw at all for opaque items and as little as possible for alpha ones).
+//!
+//! ## Algorithm overview
+//!
+//! The occlusion culling algorithm works in front-to-back order, accumulating rectangle in opaque and non-opaque lists.
+//! Each time a rectangle is added, it is first tested against existing opaque rectangles and potentially split into visible
+//! sub-rectangles, or even discarded completely. The front-to-back order ensures that once a rectangle is added it does not
+//! have to be modified again, making the underlying data structure trivial (append-only).
+//!
+//! ## splitting
+//!
+//! Partially visible rectangles are split into up to 4 visible sub-rectangles by each intersecting occluder.
+//!
+//! ```ascii
+//! +----------------------+ +----------------------+
+//! | rectangle | | |
+//! | | | |
+//! | +-----------+ | +--+-----------+-------+
+//! | |occluder | | --> | |\\\\\\\\\\\| |
+//! | +-----------+ | +--+-----------+-------+
+//! | | | |
+//! +----------------------+ +----------------------+
+//! ```
+//!
+//! In the example above the rectangle is split into 4 visible parts with the central occluded part left out.
+//!
+//! This implementation favors longer horizontal bands instead creating nine-patches to deal with the corners.
+//! The advantage is that it produces less rectangles which is good for the performance of the algorithm and
+//! for SWGL which likes long horizontal spans, however it would cause artifacts if the resulting rectangles
+//! were to be drawn with a non-axis-aligned transformation.
+//!
+//! ## Performance
+//!
+//! The cost of the algorithm grows with the number of opaque rectangle as each new rectangle is tested against
+//! all previously added opaque rectangles.
+//!
+//! Note that opaque rectangles can either be added as opaque or non-opaque. This means a trade-off between
+//! overdraw and number of rectangles can be explored to adjust performance: Small opaque rectangles, especially
+//! towards the front of the scene, could be added as non-opaque to avoid causing many splits while adding only
+//! a small amount of overdraw.
+//!
+//! This implementation is intended to be used with a small number of (opaque) items. A similar implementation
+//! could use a spatial acceleration structure for opaque rectangles to perform better with a large amount of
+//! occluders.
+//!
+
+use euclid::point2;
+use smallvec::SmallVec;
+use api::units::*;
+
+/// A visible part of a rectangle after occlusion culling.
+#[derive(Debug, PartialEq)]
+pub struct Item {
+ pub rectangle: DeviceBox2D,
+ pub key: usize,
+}
+
+/// A builder that applies occlusion culling with rectangles provided in front-to-back order.
+pub struct FrontToBackBuilder {
+ opaque_items: Vec<Item>,
+ alpha_items: Vec<Item>,
+}
+
+impl FrontToBackBuilder {
+
+ /// Pre-allocating constructor.
+ pub fn with_capacity(opaque: usize, alpha: usize) -> Self {
+ FrontToBackBuilder {
+ opaque_items: Vec::with_capacity(opaque),
+ alpha_items: Vec::with_capacity(alpha),
+ }
+ }
+
+ /// Add a rectangle, potentially splitting it and discarding the occluded parts if any.
+ ///
+ /// Returns true the rectangle is at least partially visible.
+ pub fn add(&mut self, rect: &DeviceBox2D, is_opaque: bool, key: usize) -> bool {
+ let mut fragments: SmallVec<[DeviceBox2D; 16]> = SmallVec::new();
+ fragments.push(*rect);
+
+ for item in &self.opaque_items {
+ if fragments.is_empty() {
+ break;
+ }
+ if item.rectangle.intersects(rect) {
+ apply_occluder(&item.rectangle, &mut fragments);
+ }
+ }
+
+ let list = if is_opaque {
+ &mut self.opaque_items
+ } else {
+ &mut self.alpha_items
+ };
+
+ for rect in &fragments {
+ list.push(Item {
+ rectangle: *rect,
+ key,
+ });
+ }
+
+ !fragments.is_empty()
+ }
+
+ /// Returns true if the provided rect is at least partially visible, without adding it.
+ pub fn test(&self, rect: &DeviceBox2D) -> bool {
+ let mut fragments: SmallVec<[DeviceBox2D; 16]> = SmallVec::new();
+ fragments.push(*rect);
+
+ for item in &self.opaque_items {
+ if item.rectangle.intersects(rect) {
+ apply_occluder(&item.rectangle, &mut fragments);
+ }
+ }
+
+ !fragments.is_empty()
+ }
+
+ /// The visible opaque rectangles (front-to-back order).
+ pub fn opaque_items(&self) -> &[Item] {
+ &self.opaque_items
+ }
+
+ /// The visible non-opaque rectangles (front-to-back order).
+ pub fn alpha_items(&self) -> &[Item] {
+ &self.alpha_items
+ }
+}
+
+
+// Split out the parts of the rects in the provided vector
+fn apply_occluder(occluder: &DeviceBox2D, rects: &mut SmallVec<[DeviceBox2D; 16]>) {
+ // Iterate in reverse order so that we can push new rects at the back without
+ // visiting them;
+ let mut i = rects.len() - 1;
+ loop {
+ let r = rects[i];
+
+ if r.intersects(occluder) {
+ let top = r.min.y < occluder.min.y;
+ let bottom = r.max.y > occluder.max.y;
+ let left = r.min.x < occluder.min.x;
+ let right = r.max.x > occluder.max.x;
+
+ if top {
+ rects.push(DeviceBox2D {
+ min: r.min,
+ max: point2(r.max.x, occluder.min.y),
+ });
+ }
+
+ if bottom {
+ rects.push(DeviceBox2D {
+ min: point2(r.min.x, occluder.max.y),
+ max: r.max,
+ });
+ }
+
+ if left {
+ let min_y = r.min.y.max(occluder.min.y);
+ let max_y = r.max.y.min(occluder.max.y);
+ rects.push(DeviceBox2D {
+ min: point2(r.min.x, min_y),
+ max: point2(occluder.min.x, max_y),
+ });
+ }
+
+ if right {
+ let min_y = r.min.y.max(occluder.min.y);
+ let max_y = r.max.y.min(occluder.max.y);
+ rects.push(DeviceBox2D {
+ min: point2(occluder.max.x, min_y),
+ max: point2(r.max.x, max_y),
+ });
+ }
+
+ // Remove the original rectangle, replacing it with
+ // one of the new ones we just added, or popping it
+ // if it is the last item.
+ if i == rects.len() {
+ rects.pop();
+ } else {
+ rects.swap_remove(i);
+ }
+ }
+
+ if i == 0 {
+ break;
+ }
+
+ i -= 1;
+ }
+}
diff --git a/gfx/wr/webrender/src/render_api.rs b/gfx/wr/webrender/src/render_api.rs
new file mode 100644
index 0000000000..6180e45ac1
--- /dev/null
+++ b/gfx/wr/webrender/src/render_api.rs
@@ -0,0 +1,1416 @@
+/* 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/. */
+
+#![deny(missing_docs)]
+
+use std::cell::Cell;
+use std::fmt;
+use std::marker::PhantomData;
+use std::path::PathBuf;
+use std::sync::Arc;
+use std::u32;
+use time::precise_time_ns;
+use crate::api::channel::{Sender, single_msg_channel, unbounded_channel};
+use crate::api::{BuiltDisplayList, IdNamespace, ExternalScrollId, Parameter, BoolParameter};
+use crate::api::{FontKey, FontInstanceKey, NativeFontHandle};
+use crate::api::{BlobImageData, BlobImageKey, ImageData, ImageDescriptor, ImageKey, Epoch, QualitySettings};
+use crate::api::{BlobImageParams, BlobImageRequest, BlobImageResult, AsyncBlobImageRasterizer, BlobImageHandler};
+use crate::api::{DocumentId, PipelineId, PropertyBindingId, PropertyBindingKey, ExternalEvent};
+use crate::api::{HitTestResult, HitTesterRequest, ApiHitTester, PropertyValue, DynamicProperties};
+use crate::api::{SampledScrollOffset, TileSize, NotificationRequest, DebugFlags};
+use crate::api::{GlyphDimensionRequest, GlyphIndexRequest, GlyphIndex, GlyphDimensions};
+use crate::api::{FontInstanceOptions, FontInstancePlatformOptions, FontVariation, RenderReasons};
+use crate::api::DEFAULT_TILE_SIZE;
+use crate::api::units::*;
+use crate::api_resources::ApiResources;
+use glyph_rasterizer::SharedFontResources;
+use crate::scene_builder_thread::{SceneBuilderRequest, SceneBuilderResult};
+use crate::intern::InterningMemoryReport;
+use crate::profiler::{self, TransactionProfile};
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(any(feature = "serde"), derive(Deserialize, Serialize))]
+struct ResourceId(pub u32);
+
+/// Update of a persistent resource in WebRender.
+///
+/// ResourceUpdate changes keep theirs effect across display list changes.
+#[derive(Clone)]
+#[cfg_attr(any(feature = "serde"), derive(Deserialize, Serialize))]
+pub enum ResourceUpdate {
+ /// See `AddImage`.
+ AddImage(AddImage),
+ /// See `UpdateImage`.
+ UpdateImage(UpdateImage),
+ /// Delete an existing image resource.
+ ///
+ /// It is invalid to continue referring to the image key in any display list
+ /// in the transaction that contains the `DeleteImage` message and subsequent
+ /// transactions.
+ DeleteImage(ImageKey),
+ /// See `AddBlobImage`.
+ AddBlobImage(AddBlobImage),
+ /// See `UpdateBlobImage`.
+ UpdateBlobImage(UpdateBlobImage),
+ /// Delete existing blob image resource.
+ DeleteBlobImage(BlobImageKey),
+ /// See `AddBlobImage::visible_area`.
+ SetBlobImageVisibleArea(BlobImageKey, DeviceIntRect),
+ /// See `AddFont`.
+ AddFont(AddFont),
+ /// Deletes an already existing font resource.
+ ///
+ /// It is invalid to continue referring to the font key in any display list
+ /// in the transaction that contains the `DeleteImage` message and subsequent
+ /// transactions.
+ DeleteFont(FontKey),
+ /// See `AddFontInstance`.
+ AddFontInstance(AddFontInstance),
+ /// Deletes an already existing font instance resource.
+ ///
+ /// It is invalid to continue referring to the font instance in any display
+ /// list in the transaction that contains the `DeleteImage` message and
+ /// subsequent transactions.
+ DeleteFontInstance(FontInstanceKey),
+}
+
+impl fmt::Debug for ResourceUpdate {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ ResourceUpdate::AddImage(ref i) => f.write_fmt(format_args!(
+ "ResourceUpdate::AddImage size({:?})",
+ &i.descriptor.size
+ )),
+ ResourceUpdate::UpdateImage(ref i) => f.write_fmt(format_args!(
+ "ResourceUpdate::UpdateImage size({:?})",
+ &i.descriptor.size
+ )),
+ ResourceUpdate::AddBlobImage(ref i) => f.write_fmt(format_args!(
+ "ResourceUFpdate::AddBlobImage size({:?})",
+ &i.descriptor.size
+ )),
+ ResourceUpdate::UpdateBlobImage(i) => f.write_fmt(format_args!(
+ "ResourceUpdate::UpdateBlobImage size({:?})",
+ &i.descriptor.size
+ )),
+ ResourceUpdate::DeleteImage(..) => f.write_str("ResourceUpdate::DeleteImage"),
+ ResourceUpdate::DeleteBlobImage(..) => f.write_str("ResourceUpdate::DeleteBlobImage"),
+ ResourceUpdate::SetBlobImageVisibleArea(..) => f.write_str("ResourceUpdate::SetBlobImageVisibleArea"),
+ ResourceUpdate::AddFont(..) => f.write_str("ResourceUpdate::AddFont"),
+ ResourceUpdate::DeleteFont(..) => f.write_str("ResourceUpdate::DeleteFont"),
+ ResourceUpdate::AddFontInstance(..) => f.write_str("ResourceUpdate::AddFontInstance"),
+ ResourceUpdate::DeleteFontInstance(..) => f.write_str("ResourceUpdate::DeleteFontInstance"),
+ }
+ }
+}
+
+/// Whether to generate a frame, and if so, an id that allows tracking this
+/// transaction through the various frame stages.
+#[derive(Clone, Debug)]
+pub enum GenerateFrame {
+ /// Generate a frame if something changed.
+ Yes {
+ /// An id that allows tracking the frame transaction through the various
+ /// frame stages. Specified by the caller of generate_frame().
+ id: u64,
+ },
+ /// Don't generate a frame even if something has changed.
+ No,
+}
+
+impl GenerateFrame {
+ ///
+ pub fn as_bool(&self) -> bool {
+ match self {
+ GenerateFrame::Yes { .. } => true,
+ GenerateFrame::No => false,
+ }
+ }
+
+ /// Return the frame ID, if a frame is generated.
+ pub fn id(&self) -> Option<u64> {
+ match self {
+ GenerateFrame::Yes { id } => Some(*id),
+ GenerateFrame::No => None,
+ }
+ }
+}
+
+/// A Transaction is a group of commands to apply atomically to a document.
+///
+/// This mechanism ensures that:
+/// - no other message can be interleaved between two commands that need to be applied together.
+/// - no redundant work is performed if two commands in the same transaction cause the scene or
+/// the frame to be rebuilt.
+pub struct Transaction {
+ /// Operations affecting the scene (applied before scene building).
+ scene_ops: Vec<SceneMsg>,
+ /// Operations affecting the generation of frames (applied after scene building).
+ frame_ops: Vec<FrameMsg>,
+
+ notifications: Vec<NotificationRequest>,
+
+ /// Persistent resource updates to apply as part of this transaction.
+ pub resource_updates: Vec<ResourceUpdate>,
+
+ /// True if the transaction needs the scene building thread's attention.
+ /// False for things that can skip the scene builder, like APZ changes and
+ /// async images.
+ ///
+ /// Before this `Transaction` is converted to a `TransactionMsg`, we look
+ /// over its contents and set this if we're doing anything the scene builder
+ /// needs to know about, so this is only a default.
+ use_scene_builder_thread: bool,
+
+ /// Whether to generate a frame, and if so, an id that allows tracking this
+ /// transaction through the various frame stages. Specified by the caller of
+ /// generate_frame().
+ generate_frame: GenerateFrame,
+
+ /// Time when this transaction was constructed.
+ creation_time: u64,
+
+ /// Set to true in order to force re-rendering even if WebRender can't internally
+ /// detect that something has changed.
+ pub invalidate_rendered_frame: bool,
+
+ low_priority: bool,
+
+ ///
+ pub render_reasons: RenderReasons,
+}
+
+impl Transaction {
+ /// Constructor.
+ pub fn new() -> Self {
+ Transaction {
+ scene_ops: Vec::new(),
+ frame_ops: Vec::new(),
+ resource_updates: Vec::new(),
+ notifications: Vec::new(),
+ use_scene_builder_thread: true,
+ generate_frame: GenerateFrame::No,
+ creation_time: precise_time_ns(),
+ invalidate_rendered_frame: false,
+ low_priority: false,
+ render_reasons: RenderReasons::empty(),
+ }
+ }
+
+ /// Marks this transaction to allow it to skip going through the scene builder
+ /// thread.
+ ///
+ /// This is useful to avoid jank in transaction associated with animated
+ /// property updates, panning and zooming.
+ ///
+ /// Note that transactions that skip the scene builder thread can race ahead of
+ /// transactions that don't skip it.
+ pub fn skip_scene_builder(&mut self) {
+ self.use_scene_builder_thread = false;
+ }
+
+ /// Marks this transaction to enforce going through the scene builder thread.
+ pub fn use_scene_builder_thread(&mut self) {
+ self.use_scene_builder_thread = true;
+ }
+
+ /// Returns true if the transaction has no effect.
+ pub fn is_empty(&self) -> bool {
+ !self.generate_frame.as_bool() &&
+ !self.invalidate_rendered_frame &&
+ self.scene_ops.is_empty() &&
+ self.frame_ops.is_empty() &&
+ self.resource_updates.is_empty() &&
+ self.notifications.is_empty()
+ }
+
+ /// Update a pipeline's epoch.
+ pub fn update_epoch(&mut self, pipeline_id: PipelineId, epoch: Epoch) {
+ // We track epochs before and after scene building.
+ // This one will be applied to the pending scene right away:
+ self.scene_ops.push(SceneMsg::UpdateEpoch(pipeline_id, epoch));
+ // And this one will be applied to the currently built scene at the end
+ // of the transaction (potentially long after the scene_ops one).
+ self.frame_ops.push(FrameMsg::UpdateEpoch(pipeline_id, epoch));
+ // We could avoid the duplication here by storing the epoch updates in a
+ // separate array and let the render backend schedule the updates at the
+ // proper times, but it wouldn't make things simpler.
+ }
+
+ /// Sets the root pipeline.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webrender::api::{PipelineId};
+ /// # use webrender::api::units::{DeviceIntSize};
+ /// # use webrender::render_api::{RenderApiSender, Transaction};
+ /// # fn example() {
+ /// let pipeline_id = PipelineId(0, 0);
+ /// let mut txn = Transaction::new();
+ /// txn.set_root_pipeline(pipeline_id);
+ /// # }
+ /// ```
+ pub fn set_root_pipeline(&mut self, pipeline_id: PipelineId) {
+ self.scene_ops.push(SceneMsg::SetRootPipeline(pipeline_id));
+ }
+
+ /// Removes data associated with a pipeline from the internal data structures.
+ /// If the specified `pipeline_id` is for the root pipeline, the root pipeline
+ /// is reset back to `None`.
+ pub fn remove_pipeline(&mut self, pipeline_id: PipelineId) {
+ self.scene_ops.push(SceneMsg::RemovePipeline(pipeline_id));
+ }
+
+ /// Supplies a new frame to WebRender.
+ ///
+ /// Non-blocking, it notifies a worker process which processes the display list.
+ ///
+ /// Note: Scrolling doesn't require an own Frame.
+ ///
+ /// Arguments:
+ ///
+ /// * `epoch`: The unique Frame ID, monotonically increasing.
+ /// * `pipeline_id`: The ID of the pipeline that is supplying this display list.
+ /// * `display_list`: The root Display list used in this frame.
+ pub fn set_display_list(
+ &mut self,
+ epoch: Epoch,
+ (pipeline_id, mut display_list): (PipelineId, BuiltDisplayList),
+ ) {
+ display_list.set_send_time_ns(precise_time_ns());
+ self.scene_ops.push(
+ SceneMsg::SetDisplayList {
+ display_list,
+ epoch,
+ pipeline_id,
+ }
+ );
+ }
+
+ /// Add a set of persistent resource updates to apply as part of this transaction.
+ pub fn update_resources(&mut self, mut resources: Vec<ResourceUpdate>) {
+ self.resource_updates.append(&mut resources);
+ }
+
+ // Note: Gecko uses this to get notified when a transaction that contains
+ // potentially long blob rasterization or scene build is ready to be rendered.
+ // so that the tab-switching integration can react adequately when tab
+ // switching takes too long. For this use case when matters is that the
+ // notification doesn't fire before scene building and blob rasterization.
+
+ /// Trigger a notification at a certain stage of the rendering pipeline.
+ ///
+ /// Not that notification requests are skipped during serialization, so is is
+ /// best to use them for synchronization purposes and not for things that could
+ /// affect the WebRender's state.
+ pub fn notify(&mut self, event: NotificationRequest) {
+ self.notifications.push(event);
+ }
+
+ /// Setup the output region in the framebuffer for a given document.
+ pub fn set_document_view(
+ &mut self,
+ device_rect: DeviceIntRect,
+ ) {
+ window_size_sanity_check(device_rect.size());
+ self.scene_ops.push(
+ SceneMsg::SetDocumentView {
+ device_rect,
+ },
+ );
+ }
+
+ /// Set multiple scroll offsets with generations to the node identified by
+ /// the given external scroll id, the scroll offsets are relative to the
+ /// pre-scrolled offset for the scrolling layer.
+ pub fn set_scroll_offsets(
+ &mut self,
+ id: ExternalScrollId,
+ sampled_scroll_offsets: Vec<SampledScrollOffset>,
+ ) {
+ self.frame_ops.push(FrameMsg::SetScrollOffsets(id, sampled_scroll_offsets));
+ }
+
+ /// Set the current quality / performance settings for this document.
+ pub fn set_quality_settings(&mut self, settings: QualitySettings) {
+ self.scene_ops.push(SceneMsg::SetQualitySettings { settings });
+ }
+
+ ///
+ pub fn set_is_transform_async_zooming(&mut self, is_zooming: bool, animation_id: PropertyBindingId) {
+ self.frame_ops.push(FrameMsg::SetIsTransformAsyncZooming(is_zooming, animation_id));
+ }
+
+ /// Generate a new frame. When it's done and a RenderNotifier has been set
+ /// in `webrender::Renderer`, [new_frame_ready()][notifier] gets called.
+ /// Note that the notifier is called even if the frame generation was a
+ /// no-op; the arguments passed to `new_frame_ready` will provide information
+ /// as to when happened.
+ ///
+ /// [notifier]: trait.RenderNotifier.html#tymethod.new_frame_ready
+ pub fn generate_frame(&mut self, id: u64, reasons: RenderReasons) {
+ self.generate_frame = GenerateFrame::Yes{ id };
+ self.render_reasons |= reasons;
+ }
+
+ /// Invalidate rendered frame. It ensure that frame will be rendered during
+ /// next frame generation. WebRender could skip frame rendering if there
+ /// is no update.
+ /// But there are cases that needs to force rendering.
+ /// - Content of image is updated by reusing same ExternalImageId.
+ /// - Platform requests it if pixels become stale (like wakeup from standby).
+ pub fn invalidate_rendered_frame(&mut self, reasons: RenderReasons) {
+ self.invalidate_rendered_frame = true;
+ self.render_reasons |= reasons
+ }
+
+ /// Reset the list of animated property bindings that should be used to resolve
+ /// bindings in the current display list.
+ pub fn reset_dynamic_properties(&mut self) {
+ self.frame_ops.push(FrameMsg::ResetDynamicProperties);
+ }
+
+ /// Add to the list of animated property bindings that should be used to resolve
+ /// bindings in the current display list.
+ pub fn append_dynamic_properties(&mut self, properties: DynamicProperties) {
+ self.frame_ops.push(FrameMsg::AppendDynamicProperties(properties));
+ }
+
+ /// Add to the list of animated property bindings that should be used to
+ /// resolve bindings in the current display list. This is a convenience method
+ /// so the caller doesn't have to figure out all the dynamic properties before
+ /// setting them on the transaction but can do them incrementally.
+ pub fn append_dynamic_transform_properties(&mut self, transforms: Vec<PropertyValue<LayoutTransform>>) {
+ self.frame_ops.push(FrameMsg::AppendDynamicTransformProperties(transforms));
+ }
+
+ /// Consumes this object and just returns the frame ops.
+ pub fn get_frame_ops(self) -> Vec<FrameMsg> {
+ self.frame_ops
+ }
+
+ fn finalize(self, document_id: DocumentId) -> Box<TransactionMsg> {
+ Box::new(TransactionMsg {
+ document_id,
+ scene_ops: self.scene_ops,
+ frame_ops: self.frame_ops,
+ resource_updates: self.resource_updates,
+ notifications: self.notifications,
+ use_scene_builder_thread: self.use_scene_builder_thread,
+ generate_frame: self.generate_frame,
+ creation_time: Some(self.creation_time),
+ invalidate_rendered_frame: self.invalidate_rendered_frame,
+ low_priority: self.low_priority,
+ blob_rasterizer: None,
+ blob_requests: Vec::new(),
+ rasterized_blobs: Vec::new(),
+ profile: TransactionProfile::new(),
+ render_reasons: self.render_reasons,
+ })
+ }
+
+ /// See `ResourceUpdate::AddImage`.
+ pub fn add_image(
+ &mut self,
+ key: ImageKey,
+ descriptor: ImageDescriptor,
+ data: ImageData,
+ tiling: Option<TileSize>,
+ ) {
+ self.resource_updates.push(ResourceUpdate::AddImage(AddImage {
+ key,
+ descriptor,
+ data,
+ tiling,
+ }));
+ }
+
+ /// See `ResourceUpdate::UpdateImage`.
+ pub fn update_image(
+ &mut self,
+ key: ImageKey,
+ descriptor: ImageDescriptor,
+ data: ImageData,
+ dirty_rect: &ImageDirtyRect,
+ ) {
+ self.resource_updates.push(ResourceUpdate::UpdateImage(UpdateImage {
+ key,
+ descriptor,
+ data,
+ dirty_rect: *dirty_rect,
+ }));
+ }
+
+ /// See `ResourceUpdate::DeleteImage`.
+ pub fn delete_image(&mut self, key: ImageKey) {
+ self.resource_updates.push(ResourceUpdate::DeleteImage(key));
+ }
+
+ /// See `ResourceUpdate::AddBlobImage`.
+ pub fn add_blob_image(
+ &mut self,
+ key: BlobImageKey,
+ descriptor: ImageDescriptor,
+ data: Arc<BlobImageData>,
+ visible_rect: DeviceIntRect,
+ tile_size: Option<TileSize>,
+ ) {
+ self.resource_updates.push(
+ ResourceUpdate::AddBlobImage(AddBlobImage {
+ key,
+ descriptor,
+ data,
+ visible_rect,
+ tile_size: tile_size.unwrap_or(DEFAULT_TILE_SIZE),
+ })
+ );
+ }
+
+ /// See `ResourceUpdate::UpdateBlobImage`.
+ pub fn update_blob_image(
+ &mut self,
+ key: BlobImageKey,
+ descriptor: ImageDescriptor,
+ data: Arc<BlobImageData>,
+ visible_rect: DeviceIntRect,
+ dirty_rect: &BlobDirtyRect,
+ ) {
+ self.resource_updates.push(
+ ResourceUpdate::UpdateBlobImage(UpdateBlobImage {
+ key,
+ descriptor,
+ data,
+ visible_rect,
+ dirty_rect: *dirty_rect,
+ })
+ );
+ }
+
+ /// See `ResourceUpdate::DeleteBlobImage`.
+ pub fn delete_blob_image(&mut self, key: BlobImageKey) {
+ self.resource_updates.push(ResourceUpdate::DeleteBlobImage(key));
+ }
+
+ /// See `ResourceUpdate::SetBlobImageVisibleArea`.
+ pub fn set_blob_image_visible_area(&mut self, key: BlobImageKey, area: DeviceIntRect) {
+ self.resource_updates.push(ResourceUpdate::SetBlobImageVisibleArea(key, area));
+ }
+
+ /// See `ResourceUpdate::AddFont`.
+ pub fn add_raw_font(&mut self, key: FontKey, bytes: Vec<u8>, index: u32) {
+ self.resource_updates
+ .push(ResourceUpdate::AddFont(AddFont::Raw(key, Arc::new(bytes), index)));
+ }
+
+ /// See `ResourceUpdate::AddFont`.
+ pub fn add_native_font(&mut self, key: FontKey, native_handle: NativeFontHandle) {
+ self.resource_updates
+ .push(ResourceUpdate::AddFont(AddFont::Native(key, native_handle)));
+ }
+
+ /// See `ResourceUpdate::DeleteFont`.
+ pub fn delete_font(&mut self, key: FontKey) {
+ self.resource_updates.push(ResourceUpdate::DeleteFont(key));
+ }
+
+ /// See `ResourceUpdate::AddFontInstance`.
+ pub fn add_font_instance(
+ &mut self,
+ key: FontInstanceKey,
+ font_key: FontKey,
+ glyph_size: f32,
+ options: Option<FontInstanceOptions>,
+ platform_options: Option<FontInstancePlatformOptions>,
+ variations: Vec<FontVariation>,
+ ) {
+ self.resource_updates
+ .push(ResourceUpdate::AddFontInstance(AddFontInstance {
+ key,
+ font_key,
+ glyph_size,
+ options,
+ platform_options,
+ variations,
+ }));
+ }
+
+ /// See `ResourceUpdate::DeleteFontInstance`.
+ pub fn delete_font_instance(&mut self, key: FontInstanceKey) {
+ self.resource_updates.push(ResourceUpdate::DeleteFontInstance(key));
+ }
+
+ /// A hint that this transaction can be processed at a lower priority. High-
+ /// priority transactions can jump ahead of regular-priority transactions,
+ /// but both high- and regular-priority transactions are processed in order
+ /// relative to other transactions of the same priority.
+ pub fn set_low_priority(&mut self, low_priority: bool) {
+ self.low_priority = low_priority;
+ }
+
+ /// Returns whether this transaction is marked as low priority.
+ pub fn is_low_priority(&self) -> bool {
+ self.low_priority
+ }
+}
+
+///
+pub struct DocumentTransaction {
+ ///
+ pub document_id: DocumentId,
+ ///
+ pub transaction: Transaction,
+}
+
+/// Represents a transaction in the format sent through the channel.
+pub struct TransactionMsg {
+ ///
+ pub document_id: DocumentId,
+ /// Changes that require re-building the scene.
+ pub scene_ops: Vec<SceneMsg>,
+ /// Changes to animated properties that do not require re-building the scene.
+ pub frame_ops: Vec<FrameMsg>,
+ /// Updates to resources that persist across display lists.
+ pub resource_updates: Vec<ResourceUpdate>,
+ /// Whether to trigger frame building and rendering if something has changed.
+ pub generate_frame: GenerateFrame,
+ /// Creation time of this transaction.
+ pub creation_time: Option<u64>,
+ /// Whether to force frame building and rendering even if no changes are internally
+ /// observed.
+ pub invalidate_rendered_frame: bool,
+ /// Whether to enforce that this transaction go through the scene builder.
+ pub use_scene_builder_thread: bool,
+ ///
+ pub low_priority: bool,
+
+ /// Handlers to notify at certain points of the pipeline.
+ pub notifications: Vec<NotificationRequest>,
+ ///
+ pub blob_rasterizer: Option<Box<dyn AsyncBlobImageRasterizer>>,
+ ///
+ pub blob_requests: Vec<BlobImageParams>,
+ ///
+ pub rasterized_blobs: Vec<(BlobImageRequest, BlobImageResult)>,
+ /// Collect various data along the rendering pipeline to display it in the embedded profiler.
+ pub profile: TransactionProfile,
+ /// Keep track of who asks rendering to happen.
+ pub render_reasons: RenderReasons,
+}
+
+impl fmt::Debug for TransactionMsg {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ writeln!(f, "threaded={}, genframe={:?}, invalidate={}, low_priority={}",
+ self.use_scene_builder_thread,
+ self.generate_frame,
+ self.invalidate_rendered_frame,
+ self.low_priority,
+ ).unwrap();
+ for scene_op in &self.scene_ops {
+ writeln!(f, "\t\t{:?}", scene_op).unwrap();
+ }
+
+ for frame_op in &self.frame_ops {
+ writeln!(f, "\t\t{:?}", frame_op).unwrap();
+ }
+
+ for resource_update in &self.resource_updates {
+ writeln!(f, "\t\t{:?}", resource_update).unwrap();
+ }
+ Ok(())
+ }
+}
+
+impl TransactionMsg {
+ /// Returns true if this transaction has no effect.
+ pub fn is_empty(&self) -> bool {
+ !self.generate_frame.as_bool() &&
+ !self.invalidate_rendered_frame &&
+ self.scene_ops.is_empty() &&
+ self.frame_ops.is_empty() &&
+ self.resource_updates.is_empty() &&
+ self.notifications.is_empty()
+ }
+}
+
+/// Creates an image resource with provided parameters.
+///
+/// Must be matched with a `DeleteImage` at some point to prevent memory leaks.
+#[derive(Clone)]
+#[cfg_attr(any(feature = "serde"), derive(Deserialize, Serialize))]
+pub struct AddImage {
+ /// A key to identify the image resource.
+ pub key: ImageKey,
+ /// Properties of the image.
+ pub descriptor: ImageDescriptor,
+ /// The pixels of the image.
+ pub data: ImageData,
+ /// An optional tiling scheme to apply when storing the image's data
+ /// on the GPU. Applies to both width and heights of the tiles.
+ ///
+ /// Note that WebRender may internally chose to tile large images
+ /// even if this member is set to `None`.
+ pub tiling: Option<TileSize>,
+}
+
+/// Updates an already existing image resource.
+#[derive(Clone)]
+#[cfg_attr(any(feature = "serde"), derive(Deserialize, Serialize))]
+pub struct UpdateImage {
+ /// The key identfying the image resource to update.
+ pub key: ImageKey,
+ /// Properties of the image.
+ pub descriptor: ImageDescriptor,
+ /// The pixels of the image.
+ pub data: ImageData,
+ /// An optional dirty rect that lets WebRender optimize the amount of
+ /// data to transfer to the GPU.
+ ///
+ /// The data provided must still represent the entire image.
+ pub dirty_rect: ImageDirtyRect,
+}
+
+/// Creates a blob-image resource with provided parameters.
+///
+/// Must be matched with a `DeleteImage` at some point to prevent memory leaks.
+#[derive(Clone)]
+#[cfg_attr(any(feature = "serde"), derive(Deserialize, Serialize))]
+pub struct AddBlobImage {
+ /// A key to identify the blob-image resource.
+ pub key: BlobImageKey,
+ /// Properties of the image.
+ pub descriptor: ImageDescriptor,
+ /// The blob-image's serialized commands.
+ pub data: Arc<BlobImageData>,
+ /// The portion of the plane in the blob-image's internal coordinate
+ /// system that is stretched to fill the image display item.
+ ///
+ /// Unlike regular images, blob images are not limited in size. The
+ /// top-left corner of their internal coordinate system is also not
+ /// necessary at (0, 0).
+ /// This means that blob images can be updated to insert/remove content
+ /// in any direction to support panning and zooming.
+ pub visible_rect: DeviceIntRect,
+ /// The blob image's tile size to apply when rasterizing the blob-image
+ /// and when storing its rasterized data on the GPU.
+ /// Applies to both width and heights of the tiles.
+ ///
+ /// All blob images are tiled.
+ pub tile_size: TileSize,
+}
+
+/// Updates an already existing blob-image resource.
+#[derive(Clone)]
+#[cfg_attr(any(feature = "serde"), derive(Deserialize, Serialize))]
+pub struct UpdateBlobImage {
+ /// The key identfying the blob-image resource to update.
+ pub key: BlobImageKey,
+ /// Properties of the image.
+ pub descriptor: ImageDescriptor,
+ /// The blob-image's serialized commands.
+ pub data: Arc<BlobImageData>,
+ /// See `AddBlobImage::visible_rect`.
+ pub visible_rect: DeviceIntRect,
+ /// An optional dirty rect that lets WebRender optimize the amount of
+ /// data to to rasterize and transfer to the GPU.
+ pub dirty_rect: BlobDirtyRect,
+}
+
+/// Creates a font resource.
+///
+/// Must be matched with a corresponding `ResourceUpdate::DeleteFont` at some point to prevent
+/// memory leaks.
+#[derive(Clone)]
+#[cfg_attr(any(feature = "serde"), derive(Deserialize, Serialize))]
+pub enum AddFont {
+ ///
+ Raw(FontKey, Arc<Vec<u8>>, u32),
+ ///
+ Native(FontKey, NativeFontHandle),
+}
+
+/// Creates a font instance resource.
+///
+/// Must be matched with a corresponding `DeleteFontInstance` at some point
+/// to prevent memory leaks.
+#[derive(Clone)]
+#[cfg_attr(any(feature = "serde"), derive(Deserialize, Serialize))]
+pub struct AddFontInstance {
+ /// A key to identify the font instance.
+ pub key: FontInstanceKey,
+ /// The font resource's key.
+ pub font_key: FontKey,
+ /// Glyph size in app units.
+ pub glyph_size: f32,
+ ///
+ pub options: Option<FontInstanceOptions>,
+ ///
+ pub platform_options: Option<FontInstancePlatformOptions>,
+ ///
+ pub variations: Vec<FontVariation>,
+}
+
+/// Frame messages affect building the scene.
+pub enum SceneMsg {
+ ///
+ UpdateEpoch(PipelineId, Epoch),
+ ///
+ SetRootPipeline(PipelineId),
+ ///
+ RemovePipeline(PipelineId),
+ ///
+ SetDisplayList {
+ ///
+ display_list: BuiltDisplayList,
+ ///
+ epoch: Epoch,
+ ///
+ pipeline_id: PipelineId,
+ },
+ ///
+ SetDocumentView {
+ ///
+ device_rect: DeviceIntRect,
+ },
+ /// Set the current quality / performance configuration for this document.
+ SetQualitySettings {
+ /// The set of available quality / performance config values.
+ settings: QualitySettings,
+ },
+}
+
+/// Frame messages affect frame generation (applied after building the scene).
+pub enum FrameMsg {
+ ///
+ UpdateEpoch(PipelineId, Epoch),
+ ///
+ HitTest(WorldPoint, Sender<HitTestResult>),
+ ///
+ RequestHitTester(Sender<Arc<dyn ApiHitTester>>),
+ ///
+ SetScrollOffsets(ExternalScrollId, Vec<SampledScrollOffset>),
+ ///
+ ResetDynamicProperties,
+ ///
+ AppendDynamicProperties(DynamicProperties),
+ ///
+ AppendDynamicTransformProperties(Vec<PropertyValue<LayoutTransform>>),
+ ///
+ SetIsTransformAsyncZooming(bool, PropertyBindingId),
+}
+
+impl fmt::Debug for SceneMsg {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.write_str(match *self {
+ SceneMsg::UpdateEpoch(..) => "SceneMsg::UpdateEpoch",
+ SceneMsg::SetDisplayList { .. } => "SceneMsg::SetDisplayList",
+ SceneMsg::RemovePipeline(..) => "SceneMsg::RemovePipeline",
+ SceneMsg::SetDocumentView { .. } => "SceneMsg::SetDocumentView",
+ SceneMsg::SetRootPipeline(..) => "SceneMsg::SetRootPipeline",
+ SceneMsg::SetQualitySettings { .. } => "SceneMsg::SetQualitySettings",
+ })
+ }
+}
+
+impl fmt::Debug for FrameMsg {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.write_str(match *self {
+ FrameMsg::UpdateEpoch(..) => "FrameMsg::UpdateEpoch",
+ FrameMsg::HitTest(..) => "FrameMsg::HitTest",
+ FrameMsg::RequestHitTester(..) => "FrameMsg::RequestHitTester",
+ FrameMsg::SetScrollOffsets(..) => "FrameMsg::SetScrollOffsets",
+ FrameMsg::ResetDynamicProperties => "FrameMsg::ResetDynamicProperties",
+ FrameMsg::AppendDynamicProperties(..) => "FrameMsg::AppendDynamicProperties",
+ FrameMsg::AppendDynamicTransformProperties(..) => "FrameMsg::AppendDynamicTransformProperties",
+ FrameMsg::SetIsTransformAsyncZooming(..) => "FrameMsg::SetIsTransformAsyncZooming",
+ })
+ }
+}
+
+bitflags!{
+ /// Bit flags for WR stages to store in a capture.
+ // Note: capturing `FRAME` without `SCENE` is not currently supported.
+ pub struct CaptureBits: u8 {
+ ///
+ const SCENE = 0x1;
+ ///
+ const FRAME = 0x2;
+ ///
+ const TILE_CACHE = 0x4;
+ ///
+ const EXTERNAL_RESOURCES = 0x8;
+ }
+}
+
+bitflags!{
+ /// Mask for clearing caches in debug commands.
+ pub struct ClearCache: u8 {
+ ///
+ const IMAGES = 0b1;
+ ///
+ const GLYPHS = 0b10;
+ ///
+ const GLYPH_DIMENSIONS = 0b100;
+ ///
+ const RENDER_TASKS = 0b1000;
+ ///
+ const TEXTURE_CACHE = 0b10000;
+ /// Clear render target pool
+ const RENDER_TARGETS = 0b100000;
+ }
+}
+
+/// Information about a loaded capture of each document
+/// that is returned by `RenderBackend`.
+#[derive(Clone, Debug)]
+pub struct CapturedDocument {
+ ///
+ pub document_id: DocumentId,
+ ///
+ pub root_pipeline_id: Option<PipelineId>,
+}
+
+/// Update of the state of built-in debugging facilities.
+#[derive(Clone)]
+pub enum DebugCommand {
+ /// Sets the provided debug flags.
+ SetFlags(DebugFlags),
+ /// Save a capture of all the documents state.
+ SaveCapture(PathBuf, CaptureBits),
+ /// Load a capture of all the documents state.
+ LoadCapture(PathBuf, Option<(u32, u32)>, Sender<CapturedDocument>),
+ /// Start capturing a sequence of scene/frame changes.
+ StartCaptureSequence(PathBuf, CaptureBits),
+ /// Stop capturing a sequence of scene/frame changes.
+ StopCaptureSequence,
+ /// Clear cached resources, forcing them to be re-uploaded from templates.
+ ClearCaches(ClearCache),
+ /// Enable/disable native compositor usage
+ EnableNativeCompositor(bool),
+ /// Sets the maximum amount of existing batches to visit before creating a new one.
+ SetBatchingLookback(u32),
+ /// Invalidate GPU cache, forcing the update from the CPU mirror.
+ InvalidateGpuCache,
+ /// Causes the scene builder to pause for a given amount of milliseconds each time it
+ /// processes a transaction.
+ SimulateLongSceneBuild(u32),
+ /// Set an override tile size to use for picture caches
+ SetPictureTileSize(Option<DeviceIntSize>),
+ /// Set an override for max off-screen surface size
+ SetMaximumSurfaceSize(Option<usize>),
+}
+
+/// Message sent by the `RenderApi` to the render backend thread.
+pub enum ApiMsg {
+ /// Adds a new document namespace.
+ CloneApi(Sender<IdNamespace>),
+ /// Adds a new document namespace.
+ CloneApiByClient(IdNamespace),
+ /// Adds a new document with given initial size.
+ AddDocument(DocumentId, DeviceIntSize),
+ /// A message targeted at a particular document.
+ UpdateDocuments(Vec<Box<TransactionMsg>>),
+ /// Flush from the caches anything that isn't necessary, to free some memory.
+ MemoryPressure,
+ /// Collects a memory report.
+ ReportMemory(Sender<Box<MemoryReport>>),
+ /// Change debugging options.
+ DebugCommand(DebugCommand),
+ /// Message from the scene builder thread.
+ SceneBuilderResult(SceneBuilderResult),
+}
+
+impl fmt::Debug for ApiMsg {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.write_str(match *self {
+ ApiMsg::CloneApi(..) => "ApiMsg::CloneApi",
+ ApiMsg::CloneApiByClient(..) => "ApiMsg::CloneApiByClient",
+ ApiMsg::AddDocument(..) => "ApiMsg::AddDocument",
+ ApiMsg::UpdateDocuments(..) => "ApiMsg::UpdateDocuments",
+ ApiMsg::MemoryPressure => "ApiMsg::MemoryPressure",
+ ApiMsg::ReportMemory(..) => "ApiMsg::ReportMemory",
+ ApiMsg::DebugCommand(..) => "ApiMsg::DebugCommand",
+ ApiMsg::SceneBuilderResult(..) => "ApiMsg::SceneBuilderResult",
+ })
+ }
+}
+
+/// Allows the API to communicate with WebRender.
+///
+/// This object is created along with the `Renderer` and it's main use from a
+/// user perspective is to create one or several `RenderApi` objects.
+pub struct RenderApiSender {
+ api_sender: Sender<ApiMsg>,
+ scene_sender: Sender<SceneBuilderRequest>,
+ low_priority_scene_sender: Sender<SceneBuilderRequest>,
+ blob_image_handler: Option<Box<dyn BlobImageHandler>>,
+ fonts: SharedFontResources,
+}
+
+impl RenderApiSender {
+ /// Used internally by the `Renderer`.
+ pub fn new(
+ api_sender: Sender<ApiMsg>,
+ scene_sender: Sender<SceneBuilderRequest>,
+ low_priority_scene_sender: Sender<SceneBuilderRequest>,
+ blob_image_handler: Option<Box<dyn BlobImageHandler>>,
+ fonts: SharedFontResources,
+ ) -> Self {
+ RenderApiSender {
+ api_sender,
+ scene_sender,
+ low_priority_scene_sender,
+ blob_image_handler,
+ fonts,
+ }
+ }
+
+ /// Creates a new resource API object with a dedicated namespace.
+ pub fn create_api(&self) -> RenderApi {
+ let (sync_tx, sync_rx) = single_msg_channel();
+ let msg = ApiMsg::CloneApi(sync_tx);
+ self.api_sender.send(msg).expect("Failed to send CloneApi message");
+ let namespace_id = sync_rx.recv().expect("Failed to receive CloneApi reply");
+ RenderApi {
+ api_sender: self.api_sender.clone(),
+ scene_sender: self.scene_sender.clone(),
+ low_priority_scene_sender: self.low_priority_scene_sender.clone(),
+ namespace_id,
+ next_id: Cell::new(ResourceId(0)),
+ resources: ApiResources::new(
+ self.blob_image_handler.as_ref().map(|handler| handler.create_similar()),
+ self.fonts.clone(),
+ ),
+ }
+ }
+
+ /// Creates a new resource API object with a dedicated namespace.
+ /// Namespace id is allocated by client.
+ ///
+ /// The function could be used only when WebRenderOptions::namespace_alloc_by_client is true.
+ /// When the option is true, create_api() could not be used to prevent namespace id conflict.
+ pub fn create_api_by_client(&self, namespace_id: IdNamespace) -> RenderApi {
+ let msg = ApiMsg::CloneApiByClient(namespace_id);
+ self.api_sender.send(msg).expect("Failed to send CloneApiByClient message");
+ RenderApi {
+ api_sender: self.api_sender.clone(),
+ scene_sender: self.scene_sender.clone(),
+ low_priority_scene_sender: self.low_priority_scene_sender.clone(),
+ namespace_id,
+ next_id: Cell::new(ResourceId(0)),
+ resources: ApiResources::new(
+ self.blob_image_handler.as_ref().map(|handler| handler.create_similar()),
+ self.fonts.clone(),
+ ),
+ }
+ }
+}
+
+/// The main entry point to interact with WebRender.
+pub struct RenderApi {
+ api_sender: Sender<ApiMsg>,
+ scene_sender: Sender<SceneBuilderRequest>,
+ low_priority_scene_sender: Sender<SceneBuilderRequest>,
+ namespace_id: IdNamespace,
+ next_id: Cell<ResourceId>,
+ resources: ApiResources,
+}
+
+impl RenderApi {
+ /// Returns the namespace ID used by this API object.
+ pub fn get_namespace_id(&self) -> IdNamespace {
+ self.namespace_id
+ }
+
+ ///
+ pub fn create_sender(&self) -> RenderApiSender {
+ RenderApiSender::new(
+ self.api_sender.clone(),
+ self.scene_sender.clone(),
+ self.low_priority_scene_sender.clone(),
+ self.resources.blob_image_handler.as_ref().map(|handler| handler.create_similar()),
+ self.resources.get_fonts(),
+ )
+ }
+
+ /// Add a document to the WebRender instance.
+ ///
+ /// Instances can manage one or several documents (using the same render backend thread).
+ /// Each document will internally correspond to a single scene, and scenes are made of
+ /// one or several pipelines.
+ pub fn add_document(&self, initial_size: DeviceIntSize) -> DocumentId {
+ let new_id = self.next_unique_id();
+ self.add_document_with_id(initial_size, new_id)
+ }
+
+ /// See `add_document`
+ pub fn add_document_with_id(&self,
+ initial_size: DeviceIntSize,
+ id: u32) -> DocumentId {
+ window_size_sanity_check(initial_size);
+
+ let document_id = DocumentId::new(self.namespace_id, id);
+
+ // We send this message to both the render backend and the scene builder instead of having
+ // the scene builder thread forward it to the render backend as we do elswhere. This is because
+ // some transactions can skip the scene builder thread and we want to avoid them arriving before
+ // the render backend knows about the existence of the corresponding document id.
+ // It may not be necessary, though.
+ self.api_sender.send(
+ ApiMsg::AddDocument(document_id, initial_size)
+ ).unwrap();
+ self.scene_sender.send(
+ SceneBuilderRequest::AddDocument(document_id, initial_size)
+ ).unwrap();
+
+ document_id
+ }
+
+ /// Delete a document.
+ pub fn delete_document(&self, document_id: DocumentId) {
+ self.low_priority_scene_sender.send(
+ SceneBuilderRequest::DeleteDocument(document_id)
+ ).unwrap();
+ }
+
+ /// Generate a new font key
+ pub fn generate_font_key(&self) -> FontKey {
+ let new_id = self.next_unique_id();
+ FontKey::new(self.namespace_id, new_id)
+ }
+
+ /// Generate a new font instance key
+ pub fn generate_font_instance_key(&self) -> FontInstanceKey {
+ let new_id = self.next_unique_id();
+ FontInstanceKey::new(self.namespace_id, new_id)
+ }
+
+ /// Gets the dimensions for the supplied glyph keys
+ ///
+ /// Note: Internally, the internal texture cache doesn't store
+ /// 'empty' textures (height or width = 0)
+ /// This means that glyph dimensions e.g. for spaces (' ') will mostly be None.
+ pub fn get_glyph_dimensions(
+ &self,
+ key: FontInstanceKey,
+ glyph_indices: Vec<GlyphIndex>,
+ ) -> Vec<Option<GlyphDimensions>> {
+ let (sender, rx) = single_msg_channel();
+ let msg = SceneBuilderRequest::GetGlyphDimensions(GlyphDimensionRequest {
+ key,
+ glyph_indices,
+ sender
+ });
+ self.low_priority_scene_sender.send(msg).unwrap();
+ rx.recv().unwrap()
+ }
+
+ /// Gets the glyph indices for the supplied string. These
+ /// can be used to construct GlyphKeys.
+ pub fn get_glyph_indices(&self, key: FontKey, text: &str) -> Vec<Option<u32>> {
+ let (sender, rx) = single_msg_channel();
+ let msg = SceneBuilderRequest::GetGlyphIndices(GlyphIndexRequest {
+ key,
+ text: text.to_string(),
+ sender,
+ });
+ self.low_priority_scene_sender.send(msg).unwrap();
+ rx.recv().unwrap()
+ }
+
+ /// Creates an `ImageKey`.
+ pub fn generate_image_key(&self) -> ImageKey {
+ let new_id = self.next_unique_id();
+ ImageKey::new(self.namespace_id, new_id)
+ }
+
+ /// Creates a `BlobImageKey`.
+ pub fn generate_blob_image_key(&self) -> BlobImageKey {
+ BlobImageKey(self.generate_image_key())
+ }
+
+ /// A Gecko-specific notification mechanism to get some code executed on the
+ /// `Renderer`'s thread, mostly replaced by `NotificationHandler`. You should
+ /// probably use the latter instead.
+ pub fn send_external_event(&self, evt: ExternalEvent) {
+ let msg = SceneBuilderRequest::ExternalEvent(evt);
+ self.low_priority_scene_sender.send(msg).unwrap();
+ }
+
+ /// Notify WebRender that now is a good time to flush caches and release
+ /// as much memory as possible.
+ pub fn notify_memory_pressure(&self) {
+ self.api_sender.send(ApiMsg::MemoryPressure).unwrap();
+ }
+
+ /// Synchronously requests memory report.
+ pub fn report_memory(&self, _ops: malloc_size_of::MallocSizeOfOps) -> MemoryReport {
+ let (tx, rx) = single_msg_channel();
+ self.api_sender.send(ApiMsg::ReportMemory(tx)).unwrap();
+ *rx.recv().unwrap()
+ }
+
+ /// Update debugging flags.
+ pub fn set_debug_flags(&mut self, flags: DebugFlags) {
+ self.resources.set_debug_flags(flags);
+ let cmd = DebugCommand::SetFlags(flags);
+ self.api_sender.send(ApiMsg::DebugCommand(cmd)).unwrap();
+ }
+
+ /// Stop RenderBackend's task until shut down
+ pub fn stop_render_backend(&self) {
+ self.low_priority_scene_sender.send(SceneBuilderRequest::StopRenderBackend).unwrap();
+ }
+
+ /// Shut the WebRender instance down.
+ pub fn shut_down(&self, synchronously: bool) {
+ if synchronously {
+ let (tx, rx) = single_msg_channel();
+ self.low_priority_scene_sender.send(SceneBuilderRequest::ShutDown(Some(tx))).unwrap();
+ rx.recv().unwrap();
+ } else {
+ self.low_priority_scene_sender.send(SceneBuilderRequest::ShutDown(None)).unwrap();
+ }
+ }
+
+ /// Create a new unique key that can be used for
+ /// animated property bindings.
+ pub fn generate_property_binding_key<T: Copy>(&self) -> PropertyBindingKey<T> {
+ let new_id = self.next_unique_id();
+ PropertyBindingKey {
+ id: PropertyBindingId {
+ namespace: self.namespace_id,
+ uid: new_id,
+ },
+ _phantom: PhantomData,
+ }
+ }
+
+ #[inline]
+ fn next_unique_id(&self) -> u32 {
+ let ResourceId(id) = self.next_id.get();
+ self.next_id.set(ResourceId(id + 1));
+ id
+ }
+
+ // For use in Wrench only
+ #[doc(hidden)]
+ pub fn send_message(&self, msg: ApiMsg) {
+ self.api_sender.send(msg).unwrap();
+ }
+
+ /// Creates a transaction message from a single frame message.
+ fn frame_message(&self, msg: FrameMsg, document_id: DocumentId) -> Box<TransactionMsg> {
+ Box::new(TransactionMsg {
+ document_id,
+ scene_ops: Vec::new(),
+ frame_ops: vec![msg],
+ resource_updates: Vec::new(),
+ notifications: Vec::new(),
+ generate_frame: GenerateFrame::No,
+ creation_time: None,
+ invalidate_rendered_frame: false,
+ use_scene_builder_thread: false,
+ low_priority: false,
+ blob_rasterizer: None,
+ blob_requests: Vec::new(),
+ rasterized_blobs: Vec::new(),
+ profile: TransactionProfile::new(),
+ render_reasons: RenderReasons::empty(),
+ })
+ }
+
+ /// A helper method to send document messages.
+ fn send_frame_msg(&self, document_id: DocumentId, msg: FrameMsg) {
+ // This assertion fails on Servo use-cases, because it creates different
+ // `RenderApi` instances for layout and compositor.
+ //assert_eq!(document_id.0, self.namespace_id);
+ self.api_sender
+ .send(ApiMsg::UpdateDocuments(vec![self.frame_message(msg, document_id)]))
+ .unwrap()
+ }
+
+ /// Send a transaction to WebRender.
+ pub fn send_transaction(&mut self, document_id: DocumentId, transaction: Transaction) {
+ let mut transaction = transaction.finalize(document_id);
+
+ self.resources.update(&mut transaction);
+
+ if transaction.generate_frame.as_bool() {
+ transaction.profile.start_time(profiler::API_SEND_TIME);
+ transaction.profile.start_time(profiler::TOTAL_FRAME_CPU_TIME);
+ }
+
+ if transaction.use_scene_builder_thread {
+ let sender = if transaction.low_priority {
+ &mut self.low_priority_scene_sender
+ } else {
+ &mut self.scene_sender
+ };
+
+ sender.send(SceneBuilderRequest::Transactions(vec![transaction]))
+ .expect("send by scene sender failed");
+ } else {
+ self.api_sender.send(ApiMsg::UpdateDocuments(vec![transaction]))
+ .expect("send by api sender failed");
+ }
+ }
+
+ /// Does a hit test on display items in the specified document, at the given
+ /// point. If a pipeline_id is specified, it is used to further restrict the
+ /// hit results so that only items inside that pipeline are matched. The vector
+ /// of hit results will contain all display items that match, ordered from
+ /// front to back.
+ pub fn hit_test(&self,
+ document_id: DocumentId,
+ point: WorldPoint,
+ ) -> HitTestResult {
+ let (tx, rx) = single_msg_channel();
+
+ self.send_frame_msg(
+ document_id,
+ FrameMsg::HitTest(point, tx)
+ );
+ rx.recv().unwrap()
+ }
+
+ /// Synchronously request an object that can perform fast hit testing queries.
+ pub fn request_hit_tester(&self, document_id: DocumentId) -> HitTesterRequest {
+ let (tx, rx) = single_msg_channel();
+ self.send_frame_msg(
+ document_id,
+ FrameMsg::RequestHitTester(tx)
+ );
+
+ HitTesterRequest { rx }
+ }
+
+ // Some internal scheduling magic that leaked into the API.
+ // Buckle up and see APZUpdater.cpp for more info about what this is about.
+ #[doc(hidden)]
+ pub fn wake_scene_builder(&self) {
+ self.scene_sender.send(SceneBuilderRequest::WakeUp).unwrap();
+ }
+
+ /// Block until a round-trip to the scene builder thread has completed. This
+ /// ensures that any transactions (including ones deferred to the scene
+ /// builder thread) have been processed.
+ pub fn flush_scene_builder(&self) {
+ let (tx, rx) = single_msg_channel();
+ self.low_priority_scene_sender.send(SceneBuilderRequest::Flush(tx)).unwrap();
+ rx.recv().unwrap(); // Block until done.
+ }
+
+ /// Save a capture of the current frame state for debugging.
+ pub fn save_capture(&self, path: PathBuf, bits: CaptureBits) {
+ let msg = ApiMsg::DebugCommand(DebugCommand::SaveCapture(path, bits));
+ self.send_message(msg);
+ }
+
+ /// Load a capture of the current frame state for debugging.
+ pub fn load_capture(&self, path: PathBuf, ids: Option<(u32, u32)>) -> Vec<CapturedDocument> {
+ // First flush the scene builder otherwise async scenes might clobber
+ // the capture we are about to load.
+ self.flush_scene_builder();
+
+ let (tx, rx) = unbounded_channel();
+ let msg = ApiMsg::DebugCommand(DebugCommand::LoadCapture(path, ids, tx));
+ self.send_message(msg);
+
+ let mut documents = Vec::new();
+ while let Ok(captured_doc) = rx.recv() {
+ documents.push(captured_doc);
+ }
+ documents
+ }
+
+ /// Start capturing a sequence of frames.
+ pub fn start_capture_sequence(&self, path: PathBuf, bits: CaptureBits) {
+ let msg = ApiMsg::DebugCommand(DebugCommand::StartCaptureSequence(path, bits));
+ self.send_message(msg);
+ }
+
+ /// Stop capturing sequences of frames.
+ pub fn stop_capture_sequence(&self) {
+ let msg = ApiMsg::DebugCommand(DebugCommand::StopCaptureSequence);
+ self.send_message(msg);
+ }
+
+ /// Update the state of builtin debugging facilities.
+ pub fn send_debug_cmd(&self, cmd: DebugCommand) {
+ let msg = ApiMsg::DebugCommand(cmd);
+ self.send_message(msg);
+ }
+
+ /// Update a instance-global parameter.
+ pub fn set_parameter(&mut self, parameter: Parameter) {
+ if let Parameter::Bool(BoolParameter::Multithreading, enabled) = parameter {
+ self.resources.enable_multithreading(enabled);
+ }
+
+ let _ = self.low_priority_scene_sender.send(
+ SceneBuilderRequest::SetParameter(parameter)
+ );
+ }
+}
+
+impl Drop for RenderApi {
+ fn drop(&mut self) {
+ let msg = SceneBuilderRequest::ClearNamespace(self.namespace_id);
+ let _ = self.low_priority_scene_sender.send(msg);
+ }
+}
+
+
+fn window_size_sanity_check(size: DeviceIntSize) {
+ // Anything bigger than this will crash later when attempting to create
+ // a render task.
+ use crate::api::MAX_RENDER_TASK_SIZE;
+ if size.width > MAX_RENDER_TASK_SIZE || size.height > MAX_RENDER_TASK_SIZE {
+ panic!("Attempting to create a {}x{} window/document", size.width, size.height);
+ }
+}
+
+/// Collection of heap sizes, in bytes.
+/// cbindgen:derive-eq=false
+/// cbindgen:derive-ostream=false
+#[repr(C)]
+#[allow(missing_docs)]
+#[derive(AddAssign, Clone, Debug, Default)]
+pub struct MemoryReport {
+ //
+ // CPU Memory.
+ //
+ pub clip_stores: usize,
+ pub gpu_cache_metadata: usize,
+ pub gpu_cache_cpu_mirror: usize,
+ pub render_tasks: usize,
+ pub hit_testers: usize,
+ pub fonts: usize,
+ pub weak_fonts: usize,
+ pub images: usize,
+ pub rasterized_blobs: usize,
+ pub shader_cache: usize,
+ pub interning: InterningMemoryReport,
+ pub display_list: usize,
+ pub upload_staging_memory: usize,
+ pub swgl: usize,
+
+ //
+ // GPU memory.
+ //
+ pub gpu_cache_textures: usize,
+ pub vertex_data_textures: usize,
+ pub render_target_textures: usize,
+ pub picture_tile_textures: usize,
+ pub atlas_textures: usize,
+ pub standalone_textures: usize,
+ pub texture_cache_structures: usize,
+ pub depth_target_textures: usize,
+ pub texture_upload_pbos: usize,
+ pub swap_chain: usize,
+ pub render_texture_hosts: usize,
+ pub upload_staging_textures: usize,
+}
diff --git a/gfx/wr/webrender/src/render_backend.rs b/gfx/wr/webrender/src/render_backend.rs
new file mode 100644
index 0000000000..13e5fc87c8
--- /dev/null
+++ b/gfx/wr/webrender/src/render_backend.rs
@@ -0,0 +1,1946 @@
+/* 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/. */
+
+//! The high-level module responsible for managing the pipeline and preparing
+//! commands to be issued by the `Renderer`.
+//!
+//! See the comment at the top of the `renderer` module for a description of
+//! how these two pieces interact.
+
+use api::{DebugFlags, Parameter, BoolParameter, PrimitiveFlags};
+use api::{DocumentId, ExternalScrollId, HitTestResult};
+use api::{IdNamespace, PipelineId, RenderNotifier, SampledScrollOffset};
+use api::{NotificationRequest, Checkpoint, QualitySettings};
+use api::{FramePublishId, PrimitiveKeyKind, RenderReasons};
+use api::units::*;
+use api::channel::{single_msg_channel, Sender, Receiver};
+use crate::AsyncPropertySampler;
+#[cfg(any(feature = "capture", feature = "replay"))]
+use crate::render_api::CaptureBits;
+#[cfg(feature = "replay")]
+use crate::render_api::CapturedDocument;
+use crate::render_api::{MemoryReport, TransactionMsg, ResourceUpdate, ApiMsg, FrameMsg, ClearCache, DebugCommand};
+use crate::clip::{ClipIntern, PolygonIntern, ClipStoreScratchBuffer};
+use crate::filterdata::FilterDataIntern;
+#[cfg(any(feature = "capture", feature = "replay"))]
+use crate::capture::CaptureConfig;
+use crate::composite::{CompositorKind, CompositeDescriptor};
+use crate::frame_builder::{FrameBuilder, FrameBuilderConfig, FrameScratchBuffer};
+use glyph_rasterizer::{FontInstance};
+use crate::gpu_cache::GpuCache;
+use crate::hit_test::{HitTest, HitTester, SharedHitTester};
+use crate::intern::DataStore;
+#[cfg(any(feature = "capture", feature = "replay"))]
+use crate::internal_types::{DebugOutput};
+use crate::internal_types::{FastHashMap, RenderedDocument, ResultMsg, FrameId, FrameStamp};
+use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
+use crate::picture::{PictureScratchBuffer, SliceId, TileCacheInstance, TileCacheParams, SurfaceInfo, RasterConfig};
+use crate::picture::{PicturePrimitive};
+use crate::prim_store::{PrimitiveScratchBuffer, PrimitiveInstance};
+use crate::prim_store::{PrimitiveInstanceKind, PrimTemplateCommonData};
+use crate::prim_store::interned::*;
+use crate::profiler::{self, TransactionProfile};
+use crate::render_task_graph::RenderTaskGraphBuilder;
+use crate::renderer::{FullFrameStats, PipelineInfo};
+use crate::resource_cache::ResourceCache;
+#[cfg(feature = "replay")]
+use crate::resource_cache::PlainCacheOwn;
+#[cfg(feature = "replay")]
+use crate::resource_cache::PlainResources;
+#[cfg(feature = "replay")]
+use crate::scene::Scene;
+use crate::scene::{BuiltScene, SceneProperties};
+use crate::scene_builder_thread::*;
+use crate::spatial_tree::SpatialTree;
+#[cfg(feature = "replay")]
+use crate::spatial_tree::SceneSpatialTree;
+use crate::telemetry::Telemetry;
+#[cfg(feature = "serialize")]
+use serde::{Serialize, Deserialize};
+#[cfg(feature = "replay")]
+use std::collections::hash_map::Entry::{Occupied, Vacant};
+use std::sync::Arc;
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::{mem, u32};
+#[cfg(feature = "capture")]
+use std::path::PathBuf;
+#[cfg(feature = "replay")]
+use crate::frame_builder::Frame;
+use time::precise_time_ns;
+use core::time::Duration;
+use crate::util::{Recycler, VecHelper, drain_filter};
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Copy, Clone)]
+pub struct DocumentView {
+ scene: SceneView,
+}
+
+/// Some rendering parameters applying at the scene level.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Copy, Clone)]
+pub struct SceneView {
+ pub device_rect: DeviceIntRect,
+ pub quality_settings: QualitySettings,
+}
+
+enum RenderBackendStatus {
+ Continue,
+ StopRenderBackend,
+ ShutDown(Option<Sender<()>>),
+}
+
+macro_rules! declare_data_stores {
+ ( $( $name:ident : $ty:ty, )+ ) => {
+ /// A collection of resources that are shared by clips, primitives
+ /// between display lists.
+ #[cfg_attr(feature = "capture", derive(Serialize))]
+ #[cfg_attr(feature = "replay", derive(Deserialize))]
+ #[derive(Default)]
+ pub struct DataStores {
+ $(
+ pub $name: DataStore<$ty>,
+ )+
+ }
+
+ impl DataStores {
+ /// Reports CPU heap usage.
+ fn report_memory(&self, ops: &mut MallocSizeOfOps, r: &mut MemoryReport) {
+ $(
+ r.interning.data_stores.$name += self.$name.size_of(ops);
+ )+
+ }
+
+ fn apply_updates(
+ &mut self,
+ updates: InternerUpdates,
+ profile: &mut TransactionProfile,
+ ) {
+ $(
+ self.$name.apply_updates(
+ updates.$name,
+ profile,
+ );
+ )+
+ }
+ }
+ }
+}
+
+crate::enumerate_interners!(declare_data_stores);
+
+impl DataStores {
+ /// Returns the local rect for a primitive. For most primitives, this is
+ /// stored in the template. For pictures, this is stored inside the picture
+ /// primitive instance itself, since this is determined during frame building.
+ pub fn get_local_prim_rect(
+ &self,
+ prim_instance: &PrimitiveInstance,
+ pictures: &[PicturePrimitive],
+ surfaces: &[SurfaceInfo],
+ ) -> LayoutRect {
+ match prim_instance.kind {
+ PrimitiveInstanceKind::Picture { pic_index, .. } => {
+ let pic = &pictures[pic_index.0];
+
+ match pic.raster_config {
+ Some(RasterConfig { surface_index, ref composite_mode, .. }) => {
+ let surface = &surfaces[surface_index.0];
+
+ composite_mode.get_rect(surface, None)
+ }
+ None => {
+ panic!("bug: get_local_prim_rect should not be called for pass-through pictures");
+ }
+ }
+ }
+ _ => {
+ self.as_common_data(prim_instance).prim_rect
+ }
+ }
+ }
+
+ /// Returns the local coverage (space occupied) for a primitive. For most primitives,
+ /// this is stored in the template. For pictures, this is stored inside the picture
+ /// primitive instance itself, since this is determined during frame building.
+ pub fn get_local_prim_coverage_rect(
+ &self,
+ prim_instance: &PrimitiveInstance,
+ pictures: &[PicturePrimitive],
+ surfaces: &[SurfaceInfo],
+ ) -> LayoutRect {
+ match prim_instance.kind {
+ PrimitiveInstanceKind::Picture { pic_index, .. } => {
+ let pic = &pictures[pic_index.0];
+
+ match pic.raster_config {
+ Some(RasterConfig { surface_index, ref composite_mode, .. }) => {
+ let surface = &surfaces[surface_index.0];
+
+ composite_mode.get_coverage(surface, None)
+ }
+ None => {
+ panic!("bug: get_local_prim_coverage_rect should not be called for pass-through pictures");
+ }
+ }
+ }
+ _ => {
+ self.as_common_data(prim_instance).prim_rect
+ }
+ }
+ }
+
+ /// Returns true if this primitive might need repition.
+ // TODO(gw): This seems like the wrong place for this - maybe this flag should
+ // not be in the common prim template data?
+ pub fn prim_may_need_repetition(
+ &self,
+ prim_instance: &PrimitiveInstance,
+ ) -> bool {
+ match prim_instance.kind {
+ PrimitiveInstanceKind::Picture { .. } => {
+ false
+ }
+ _ => {
+ self.as_common_data(prim_instance).may_need_repetition
+ }
+ }
+ }
+
+ /// Returns true if this primitive has anti-aliasing enabled.
+ pub fn prim_has_anti_aliasing(
+ &self,
+ prim_instance: &PrimitiveInstance,
+ ) -> bool {
+ match prim_instance.kind {
+ PrimitiveInstanceKind::Picture { .. } => {
+ false
+ }
+ _ => {
+ self.as_common_data(prim_instance).flags.contains(PrimitiveFlags::ANTIALISED)
+ }
+ }
+ }
+
+ pub fn as_common_data(
+ &self,
+ prim_inst: &PrimitiveInstance
+ ) -> &PrimTemplateCommonData {
+ match prim_inst.kind {
+ PrimitiveInstanceKind::Rectangle { data_handle, .. } |
+ PrimitiveInstanceKind::Clear { data_handle, .. } => {
+ let prim_data = &self.prim[data_handle];
+ &prim_data.common
+ }
+ PrimitiveInstanceKind::Image { data_handle, .. } => {
+ let prim_data = &self.image[data_handle];
+ &prim_data.common
+ }
+ PrimitiveInstanceKind::ImageBorder { data_handle, .. } => {
+ let prim_data = &self.image_border[data_handle];
+ &prim_data.common
+ }
+ PrimitiveInstanceKind::LineDecoration { data_handle, .. } => {
+ let prim_data = &self.line_decoration[data_handle];
+ &prim_data.common
+ }
+ PrimitiveInstanceKind::LinearGradient { data_handle, .. }
+ | PrimitiveInstanceKind::CachedLinearGradient { data_handle, .. } => {
+ let prim_data = &self.linear_grad[data_handle];
+ &prim_data.common
+ }
+ PrimitiveInstanceKind::NormalBorder { data_handle, .. } => {
+ let prim_data = &self.normal_border[data_handle];
+ &prim_data.common
+ }
+ PrimitiveInstanceKind::Picture { .. } => {
+ panic!("BUG: picture prims don't have common data!");
+ }
+ PrimitiveInstanceKind::RadialGradient { data_handle, .. } => {
+ let prim_data = &self.radial_grad[data_handle];
+ &prim_data.common
+ }
+ PrimitiveInstanceKind::ConicGradient { data_handle, .. } => {
+ let prim_data = &self.conic_grad[data_handle];
+ &prim_data.common
+ }
+ PrimitiveInstanceKind::TextRun { data_handle, .. } => {
+ let prim_data = &self.text_run[data_handle];
+ &prim_data.common
+ }
+ PrimitiveInstanceKind::YuvImage { data_handle, .. } => {
+ let prim_data = &self.yuv_image[data_handle];
+ &prim_data.common
+ }
+ PrimitiveInstanceKind::BackdropCapture { data_handle, .. } => {
+ let prim_data = &self.backdrop_capture[data_handle];
+ &prim_data.common
+ }
+ PrimitiveInstanceKind::BackdropRender { data_handle, .. } => {
+ let prim_data = &self.backdrop_render[data_handle];
+ &prim_data.common
+ }
+ }
+ }
+}
+
+#[derive(Default)]
+pub struct ScratchBuffer {
+ pub primitive: PrimitiveScratchBuffer,
+ pub picture: PictureScratchBuffer,
+ pub frame: FrameScratchBuffer,
+ pub clip_store: ClipStoreScratchBuffer,
+}
+
+impl ScratchBuffer {
+ pub fn begin_frame(&mut self) {
+ self.primitive.begin_frame();
+ self.picture.begin_frame();
+ self.frame.begin_frame();
+ }
+
+ pub fn end_frame(&mut self) {
+ self.primitive.end_frame();
+ }
+
+ pub fn recycle(&mut self, recycler: &mut Recycler) {
+ self.primitive.recycle(recycler);
+ self.picture.recycle(recycler);
+ }
+
+ pub fn memory_pressure(&mut self) {
+ // TODO: causes browser chrome test crashes on windows.
+ //self.primitive = Default::default();
+ self.picture = Default::default();
+ self.frame = Default::default();
+ self.clip_store = Default::default();
+ }
+}
+
+struct Document {
+ /// The id of this document
+ id: DocumentId,
+
+ /// Temporary list of removed pipelines received from the scene builder
+ /// thread and forwarded to the renderer.
+ removed_pipelines: Vec<(PipelineId, DocumentId)>,
+
+ view: DocumentView,
+
+ /// The id and time of the current frame.
+ stamp: FrameStamp,
+
+ /// The latest built scene, usable to build frames.
+ /// received from the scene builder thread.
+ scene: BuiltScene,
+
+ /// The builder object that prodces frames, kept around to preserve some retained state.
+ frame_builder: FrameBuilder,
+
+ /// Allows graphs of render tasks to be created, and then built into an immutable graph output.
+ rg_builder: RenderTaskGraphBuilder,
+
+ /// A data structure to allow hit testing against rendered frames. This is updated
+ /// every time we produce a fully rendered frame.
+ hit_tester: Option<Arc<HitTester>>,
+ /// To avoid synchronous messaging we update a shared hit-tester that other threads
+ /// can query.
+ shared_hit_tester: Arc<SharedHitTester>,
+
+ /// Properties that are resolved during frame building and can be changed at any time
+ /// without requiring the scene to be re-built.
+ dynamic_properties: SceneProperties,
+
+ /// Track whether the last built frame is up to date or if it will need to be re-built
+ /// before rendering again.
+ frame_is_valid: bool,
+ hit_tester_is_valid: bool,
+ rendered_frame_is_valid: bool,
+ /// We track this information to be able to display debugging information from the
+ /// renderer.
+ has_built_scene: bool,
+
+ data_stores: DataStores,
+
+ /// Retained frame-building version of the spatial tree
+ spatial_tree: SpatialTree,
+
+ /// Contains various vecs of data that is used only during frame building,
+ /// where we want to recycle the memory each new display list, to avoid constantly
+ /// re-allocating and moving memory around.
+ scratch: ScratchBuffer,
+
+ #[cfg(feature = "replay")]
+ loaded_scene: Scene,
+
+ /// Tracks the state of the picture cache tiles that were composited on the previous frame.
+ prev_composite_descriptor: CompositeDescriptor,
+
+ /// Tracks if we need to invalidate dirty rects for this document, due to the picture
+ /// cache slice configuration having changed when a new scene is swapped in.
+ dirty_rects_are_valid: bool,
+
+ profile: TransactionProfile,
+ frame_stats: Option<FullFrameStats>,
+}
+
+impl Document {
+ pub fn new(
+ id: DocumentId,
+ size: DeviceIntSize,
+ ) -> Self {
+ Document {
+ id,
+ removed_pipelines: Vec::new(),
+ view: DocumentView {
+ scene: SceneView {
+ device_rect: size.into(),
+ quality_settings: QualitySettings::default(),
+ },
+ },
+ stamp: FrameStamp::first(id),
+ scene: BuiltScene::empty(),
+ frame_builder: FrameBuilder::new(),
+ hit_tester: None,
+ shared_hit_tester: Arc::new(SharedHitTester::new()),
+ dynamic_properties: SceneProperties::new(),
+ frame_is_valid: false,
+ hit_tester_is_valid: false,
+ rendered_frame_is_valid: false,
+ has_built_scene: false,
+ data_stores: DataStores::default(),
+ spatial_tree: SpatialTree::new(),
+ scratch: ScratchBuffer::default(),
+ #[cfg(feature = "replay")]
+ loaded_scene: Scene::new(),
+ prev_composite_descriptor: CompositeDescriptor::empty(),
+ dirty_rects_are_valid: true,
+ profile: TransactionProfile::new(),
+ rg_builder: RenderTaskGraphBuilder::new(),
+ frame_stats: None,
+ }
+ }
+
+ fn can_render(&self) -> bool {
+ self.scene.has_root_pipeline
+ }
+
+ fn has_pixels(&self) -> bool {
+ !self.view.scene.device_rect.is_empty()
+ }
+
+ fn process_frame_msg(
+ &mut self,
+ message: FrameMsg,
+ ) -> DocumentOps {
+ match message {
+ FrameMsg::UpdateEpoch(pipeline_id, epoch) => {
+ self.scene.pipeline_epochs.insert(pipeline_id, epoch);
+ }
+ FrameMsg::HitTest(point, tx) => {
+ if !self.hit_tester_is_valid {
+ self.rebuild_hit_tester();
+ }
+
+ let result = match self.hit_tester {
+ Some(ref hit_tester) => {
+ hit_tester.hit_test(HitTest::new(point))
+ }
+ None => HitTestResult { items: Vec::new() },
+ };
+
+ tx.send(result).unwrap();
+ }
+ FrameMsg::RequestHitTester(tx) => {
+ tx.send(self.shared_hit_tester.clone()).unwrap();
+ }
+ FrameMsg::SetScrollOffsets(id, offset) => {
+ profile_scope!("SetScrollOffset");
+
+ if self.set_scroll_offsets(id, offset) {
+ self.hit_tester_is_valid = false;
+ self.frame_is_valid = false;
+ }
+
+ return DocumentOps {
+ scroll: true,
+ ..DocumentOps::nop()
+ };
+ }
+ FrameMsg::ResetDynamicProperties => {
+ self.dynamic_properties.reset_properties();
+ }
+ FrameMsg::AppendDynamicProperties(property_bindings) => {
+ self.dynamic_properties.add_properties(property_bindings);
+ }
+ FrameMsg::AppendDynamicTransformProperties(property_bindings) => {
+ self.dynamic_properties.add_transforms(property_bindings);
+ }
+ FrameMsg::SetIsTransformAsyncZooming(is_zooming, animation_id) => {
+ if let Some(node_index) = self.spatial_tree.find_spatial_node_by_anim_id(animation_id) {
+ let node = self.spatial_tree.get_spatial_node_mut(node_index);
+
+ if node.is_async_zooming != is_zooming {
+ node.is_async_zooming = is_zooming;
+ self.frame_is_valid = false;
+ }
+ }
+ }
+ }
+
+ DocumentOps::nop()
+ }
+
+ fn build_frame(
+ &mut self,
+ resource_cache: &mut ResourceCache,
+ gpu_cache: &mut GpuCache,
+ debug_flags: DebugFlags,
+ tile_caches: &mut FastHashMap<SliceId, Box<TileCacheInstance>>,
+ frame_stats: Option<FullFrameStats>,
+ render_reasons: RenderReasons,
+ ) -> RenderedDocument {
+ let frame_build_start_time = precise_time_ns();
+
+ // Advance to the next frame.
+ self.stamp.advance();
+
+ assert!(self.stamp.frame_id() != FrameId::INVALID,
+ "First frame increment must happen before build_frame()");
+
+ let frame = {
+ let frame = self.frame_builder.build(
+ &mut self.scene,
+ resource_cache,
+ gpu_cache,
+ &mut self.rg_builder,
+ self.stamp,
+ self.view.scene.device_rect.min,
+ &self.dynamic_properties,
+ &mut self.data_stores,
+ &mut self.scratch,
+ debug_flags,
+ tile_caches,
+ &mut self.spatial_tree,
+ self.dirty_rects_are_valid,
+ &mut self.profile,
+ );
+
+ frame
+ };
+
+ self.frame_is_valid = true;
+ self.dirty_rects_are_valid = true;
+
+ let is_new_scene = self.has_built_scene;
+ self.has_built_scene = false;
+
+ let frame_build_time_ms =
+ profiler::ns_to_ms(precise_time_ns() - frame_build_start_time);
+ self.profile.set(profiler::FRAME_BUILDING_TIME, frame_build_time_ms);
+
+ let frame_stats = frame_stats.map(|mut stats| {
+ stats.frame_build_time += frame_build_time_ms;
+ stats
+ });
+
+ RenderedDocument {
+ frame,
+ is_new_scene,
+ profile: self.profile.take_and_reset(),
+ frame_stats: frame_stats,
+ render_reasons,
+ }
+ }
+
+ fn rebuild_hit_tester(&mut self) {
+ self.spatial_tree.update_tree(&self.dynamic_properties);
+
+ let hit_tester = Arc::new(self.scene.create_hit_tester(&self.spatial_tree));
+ self.hit_tester = Some(Arc::clone(&hit_tester));
+ self.shared_hit_tester.update(hit_tester);
+ self.hit_tester_is_valid = true;
+ }
+
+ pub fn updated_pipeline_info(&mut self) -> PipelineInfo {
+ let removed_pipelines = self.removed_pipelines.take_and_preallocate();
+ PipelineInfo {
+ epochs: self.scene.pipeline_epochs.iter()
+ .map(|(&pipeline_id, &epoch)| ((pipeline_id, self.id), epoch)).collect(),
+ removed_pipelines,
+ }
+ }
+
+ /// Returns true if the node actually changed position or false otherwise.
+ pub fn set_scroll_offsets(
+ &mut self,
+ id: ExternalScrollId,
+ offsets: Vec<SampledScrollOffset>,
+ ) -> bool {
+ self.spatial_tree.set_scroll_offsets(id, offsets)
+ }
+
+ /// Update the state of tile caches when a new scene is being swapped in to
+ /// the render backend. Retain / reuse existing caches if possible, and
+ /// destroy any now unused caches.
+ fn update_tile_caches_for_new_scene(
+ &mut self,
+ mut requested_tile_caches: FastHashMap<SliceId, TileCacheParams>,
+ tile_caches: &mut FastHashMap<SliceId, Box<TileCacheInstance>>,
+ resource_cache: &mut ResourceCache,
+ ) {
+ let mut new_tile_caches = FastHashMap::default();
+ new_tile_caches.reserve(requested_tile_caches.len());
+
+ // Step through the tile caches that are needed for the new scene, and see
+ // if we have an existing cache that can be reused.
+ for (slice_id, params) in requested_tile_caches.drain() {
+ let tile_cache = match tile_caches.remove(&slice_id) {
+ Some(mut existing_tile_cache) => {
+ // Found an existing cache - update the cache params and reuse it
+ existing_tile_cache.prepare_for_new_scene(
+ params,
+ resource_cache,
+ );
+ existing_tile_cache
+ }
+ None => {
+ // No cache exists so create a new one
+ Box::new(TileCacheInstance::new(params))
+ }
+ };
+
+ new_tile_caches.insert(slice_id, tile_cache);
+ }
+
+ // Replace current tile cache map, and return what was left over,
+ // which are now unused.
+ let unused_tile_caches = mem::replace(
+ tile_caches,
+ new_tile_caches,
+ );
+
+ if !unused_tile_caches.is_empty() {
+ // If the slice configuration changed, assume we can't rely on the
+ // current dirty rects for next composite
+ self.dirty_rects_are_valid = false;
+
+ // Destroy any native surfaces allocated by these unused caches
+ for (_, tile_cache) in unused_tile_caches {
+ tile_cache.destroy(resource_cache);
+ }
+ }
+ }
+
+ pub fn new_async_scene_ready(
+ &mut self,
+ mut built_scene: BuiltScene,
+ recycler: &mut Recycler,
+ tile_caches: &mut FastHashMap<SliceId, Box<TileCacheInstance>>,
+ resource_cache: &mut ResourceCache,
+ ) {
+ self.frame_is_valid = false;
+ self.hit_tester_is_valid = false;
+
+ self.update_tile_caches_for_new_scene(
+ mem::replace(&mut built_scene.tile_cache_config.tile_caches, FastHashMap::default()),
+ tile_caches,
+ resource_cache,
+ );
+
+ self.scene = built_scene;
+ self.scratch.recycle(recycler);
+ }
+}
+
+struct DocumentOps {
+ scroll: bool,
+}
+
+impl DocumentOps {
+ fn nop() -> Self {
+ DocumentOps {
+ scroll: false,
+ }
+ }
+}
+
+/// The unique id for WR resource identification.
+/// The namespace_id should start from 1.
+static NEXT_NAMESPACE_ID: AtomicUsize = AtomicUsize::new(1);
+
+#[cfg(any(feature = "capture", feature = "replay"))]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct PlainRenderBackend {
+ frame_config: FrameBuilderConfig,
+ documents: FastHashMap<DocumentId, DocumentView>,
+ resource_sequence_id: u32,
+}
+
+/// The render backend is responsible for transforming high level display lists into
+/// GPU-friendly work which is then submitted to the renderer in the form of a frame::Frame.
+///
+/// The render backend operates on its own thread.
+pub struct RenderBackend {
+ api_rx: Receiver<ApiMsg>,
+ result_tx: Sender<ResultMsg>,
+ scene_tx: Sender<SceneBuilderRequest>,
+
+ gpu_cache: GpuCache,
+ resource_cache: ResourceCache,
+
+ frame_config: FrameBuilderConfig,
+ default_compositor_kind: CompositorKind,
+ documents: FastHashMap<DocumentId, Document>,
+
+ notifier: Box<dyn RenderNotifier>,
+ sampler: Option<Box<dyn AsyncPropertySampler + Send>>,
+ size_of_ops: Option<MallocSizeOfOps>,
+ debug_flags: DebugFlags,
+ namespace_alloc_by_client: bool,
+
+ recycler: Recycler,
+
+ #[cfg(feature = "capture")]
+ /// If `Some`, do 'sequence capture' logging, recording updated documents,
+ /// frames, etc. This is set only through messages from the scene builder,
+ /// so all control of sequence capture goes through there.
+ capture_config: Option<CaptureConfig>,
+
+ #[cfg(feature = "replay")]
+ loaded_resource_sequence_id: u32,
+
+ /// A map of tile caches. These are stored in the backend as they are
+ /// persisted between both frame and scenes.
+ tile_caches: FastHashMap<SliceId, Box<TileCacheInstance>>,
+
+ /// The id of the latest PublishDocument
+ frame_publish_id: FramePublishId,
+}
+
+impl RenderBackend {
+ pub fn new(
+ api_rx: Receiver<ApiMsg>,
+ result_tx: Sender<ResultMsg>,
+ scene_tx: Sender<SceneBuilderRequest>,
+ resource_cache: ResourceCache,
+ notifier: Box<dyn RenderNotifier>,
+ frame_config: FrameBuilderConfig,
+ sampler: Option<Box<dyn AsyncPropertySampler + Send>>,
+ size_of_ops: Option<MallocSizeOfOps>,
+ debug_flags: DebugFlags,
+ namespace_alloc_by_client: bool,
+ ) -> RenderBackend {
+ RenderBackend {
+ api_rx,
+ result_tx,
+ scene_tx,
+ resource_cache,
+ gpu_cache: GpuCache::new(),
+ frame_config,
+ default_compositor_kind : frame_config.compositor_kind,
+ documents: FastHashMap::default(),
+ notifier,
+ sampler,
+ size_of_ops,
+ debug_flags,
+ namespace_alloc_by_client,
+ recycler: Recycler::new(),
+ #[cfg(feature = "capture")]
+ capture_config: None,
+ #[cfg(feature = "replay")]
+ loaded_resource_sequence_id: 0,
+ tile_caches: FastHashMap::default(),
+ frame_publish_id: FramePublishId::first(),
+ }
+ }
+
+ pub fn next_namespace_id() -> IdNamespace {
+ IdNamespace(NEXT_NAMESPACE_ID.fetch_add(1, Ordering::Relaxed) as u32)
+ }
+
+ pub fn run(&mut self) {
+ let mut frame_counter: u32 = 0;
+ let mut status = RenderBackendStatus::Continue;
+
+ if let Some(ref sampler) = self.sampler {
+ sampler.register();
+ }
+
+ while let RenderBackendStatus::Continue = status {
+ status = match self.api_rx.recv() {
+ Ok(msg) => {
+ self.process_api_msg(msg, &mut frame_counter)
+ }
+ Err(..) => { RenderBackendStatus::ShutDown(None) }
+ };
+ }
+
+ if let RenderBackendStatus::StopRenderBackend = status {
+ while let Ok(msg) = self.api_rx.recv() {
+ match msg {
+ ApiMsg::SceneBuilderResult(SceneBuilderResult::ExternalEvent(evt)) => {
+ self.notifier.external_event(evt);
+ }
+ ApiMsg::SceneBuilderResult(SceneBuilderResult::FlushComplete(tx)) => {
+ // If somebody's blocked waiting for a flush, how did they
+ // trigger the RB thread to shut down? This shouldn't happen
+ // but handle it gracefully anyway.
+ debug_assert!(false);
+ tx.send(()).ok();
+ }
+ ApiMsg::SceneBuilderResult(SceneBuilderResult::ShutDown(sender)) => {
+ info!("Recycling stats: {:?}", self.recycler);
+ status = RenderBackendStatus::ShutDown(sender);
+ break;
+ }
+ _ => {},
+ }
+ }
+ }
+
+ // Ensure we read everything the scene builder is sending us from
+ // inflight messages, otherwise the scene builder might panic.
+ while let Ok(msg) = self.api_rx.try_recv() {
+ match msg {
+ ApiMsg::SceneBuilderResult(SceneBuilderResult::FlushComplete(tx)) => {
+ // If somebody's blocked waiting for a flush, how did they
+ // trigger the RB thread to shut down? This shouldn't happen
+ // but handle it gracefully anyway.
+ debug_assert!(false);
+ tx.send(()).ok();
+ }
+ _ => {},
+ }
+ }
+
+ self.documents.clear();
+
+ self.notifier.shut_down();
+
+ if let Some(ref sampler) = self.sampler {
+ sampler.deregister();
+ }
+
+
+ if let RenderBackendStatus::ShutDown(Some(sender)) = status {
+ let _ = sender.send(());
+ }
+ }
+
+ fn process_transaction(
+ &mut self,
+ mut txns: Vec<Box<BuiltTransaction>>,
+ result_tx: Option<Sender<SceneSwapResult>>,
+ frame_counter: &mut u32,
+ ) -> bool {
+ self.prepare_for_frames();
+ self.maybe_force_nop_documents(
+ frame_counter,
+ |document_id| txns.iter().any(|txn| txn.document_id == document_id));
+
+ let mut built_frame = false;
+ for mut txn in txns.drain(..) {
+ let has_built_scene = txn.built_scene.is_some();
+
+ if let Some(doc) = self.documents.get_mut(&txn.document_id) {
+ doc.removed_pipelines.append(&mut txn.removed_pipelines);
+ doc.view.scene = txn.view;
+ doc.profile.merge(&mut txn.profile);
+
+ doc.frame_stats = if let Some(stats) = &doc.frame_stats {
+ Some(stats.merge(&txn.frame_stats))
+ } else {
+ Some(txn.frame_stats)
+ };
+
+ // Before updating the spatial tree, save the most recently sampled
+ // scroll offsets (which include async deltas).
+ let last_sampled_scroll_offsets = if self.sampler.is_some() {
+ Some(doc.spatial_tree.get_last_sampled_scroll_offsets())
+ } else {
+ None
+ };
+
+ if let Some(updates) = txn.spatial_tree_updates.take() {
+ doc.spatial_tree.apply_updates(updates);
+ }
+
+ if let Some(built_scene) = txn.built_scene.take() {
+ doc.new_async_scene_ready(
+ built_scene,
+ &mut self.recycler,
+ &mut self.tile_caches,
+ &mut self.resource_cache,
+ );
+ }
+
+ // If there are any additions or removals of clip modes
+ // during the scene build, apply them to the data store now.
+ // This needs to happen before we build the hit tester.
+ if let Some(updates) = txn.interner_updates.take() {
+ doc.data_stores.apply_updates(updates, &mut doc.profile);
+ }
+
+ // Apply the last sampled scroll offsets from the previous scene,
+ // to the current scene. The offsets are identified by scroll ids
+ // which are stable across scenes. This ensures that a hit test,
+ // which could occur in between post-swap hook and the call to
+ // update_document() below, does not observe raw main-thread offsets
+ // from the new scene that don't have async deltas applied to them.
+ if let Some(last_sampled) = last_sampled_scroll_offsets {
+ doc.spatial_tree
+ .apply_last_sampled_scroll_offsets(last_sampled);
+ }
+
+ // Build the hit tester while the APZ lock is held so that its content
+ // is in sync with the gecko APZ tree.
+ if !doc.hit_tester_is_valid {
+ doc.rebuild_hit_tester();
+ }
+
+ if let Some(ref tx) = result_tx {
+ let (resume_tx, resume_rx) = single_msg_channel();
+ tx.send(SceneSwapResult::Complete(resume_tx)).unwrap();
+ // Block until the post-swap hook has completed on
+ // the scene builder thread. We need to do this before
+ // we can sample from the sampler hook which might happen
+ // in the update_document call below.
+ resume_rx.recv().ok();
+ }
+
+ self.resource_cache.add_rasterized_blob_images(
+ txn.rasterized_blobs.take(),
+ &mut doc.profile,
+ );
+
+ } else {
+ // The document was removed while we were building it, skip it.
+ // TODO: we might want to just ensure that removed documents are
+ // always forwarded to the scene builder thread to avoid this case.
+ if let Some(ref tx) = result_tx {
+ tx.send(SceneSwapResult::Aborted).unwrap();
+ }
+ continue;
+ }
+
+ built_frame |= self.update_document(
+ txn.document_id,
+ txn.resource_updates.take(),
+ txn.frame_ops.take(),
+ txn.notifications.take(),
+ txn.render_frame,
+ RenderReasons::SCENE,
+ None,
+ txn.invalidate_rendered_frame,
+ frame_counter,
+ has_built_scene,
+ None,
+ );
+ }
+
+ built_frame
+ }
+
+ fn process_api_msg(
+ &mut self,
+ msg: ApiMsg,
+ frame_counter: &mut u32,
+ ) -> RenderBackendStatus {
+ match msg {
+ ApiMsg::CloneApi(sender) => {
+ assert!(!self.namespace_alloc_by_client);
+ sender.send(Self::next_namespace_id()).unwrap();
+ }
+ ApiMsg::CloneApiByClient(namespace_id) => {
+ assert!(self.namespace_alloc_by_client);
+ debug_assert!(!self.documents.iter().any(|(did, _doc)| did.namespace_id == namespace_id));
+ }
+ ApiMsg::AddDocument(document_id, initial_size) => {
+ let document = Document::new(
+ document_id,
+ initial_size,
+ );
+ let old = self.documents.insert(document_id, document);
+ debug_assert!(old.is_none());
+ }
+ ApiMsg::MemoryPressure => {
+ // This is drastic. It will basically flush everything out of the cache,
+ // and the next frame will have to rebuild all of its resources.
+ // We may want to look into something less extreme, but on the other hand this
+ // should only be used in situations where are running low enough on memory
+ // that we risk crashing if we don't do something about it.
+ // The advantage of clearing the cache completely is that it gets rid of any
+ // remaining fragmentation that could have persisted if we kept around the most
+ // recently used resources.
+ self.resource_cache.clear(ClearCache::all());
+
+ self.gpu_cache.clear();
+
+ for (_, doc) in &mut self.documents {
+ doc.scratch.memory_pressure();
+ for tile_cache in self.tile_caches.values_mut() {
+ tile_cache.memory_pressure(&mut self.resource_cache);
+ }
+ }
+
+ let resource_updates = self.resource_cache.pending_updates();
+ let msg = ResultMsg::UpdateResources {
+ resource_updates,
+ memory_pressure: true,
+ };
+ self.result_tx.send(msg).unwrap();
+ self.notifier.wake_up(false);
+ }
+ ApiMsg::ReportMemory(tx) => {
+ self.report_memory(tx);
+ }
+ ApiMsg::DebugCommand(option) => {
+ let msg = match option {
+ DebugCommand::SetPictureTileSize(tile_size) => {
+ self.frame_config.tile_size_override = tile_size;
+ self.update_frame_builder_config();
+
+ return RenderBackendStatus::Continue;
+ }
+ DebugCommand::SetMaximumSurfaceSize(surface_size) => {
+ self.frame_config.max_surface_override = surface_size;
+ self.update_frame_builder_config();
+
+ return RenderBackendStatus::Continue;
+ }
+ #[cfg(feature = "capture")]
+ DebugCommand::SaveCapture(root, bits) => {
+ let output = self.save_capture(root, bits);
+ ResultMsg::DebugOutput(output)
+ },
+ #[cfg(feature = "capture")]
+ DebugCommand::StartCaptureSequence(root, bits) => {
+ self.start_capture_sequence(root, bits);
+ return RenderBackendStatus::Continue;
+ },
+ #[cfg(feature = "capture")]
+ DebugCommand::StopCaptureSequence => {
+ self.stop_capture_sequence();
+ return RenderBackendStatus::Continue;
+ },
+ #[cfg(feature = "replay")]
+ DebugCommand::LoadCapture(path, ids, tx) => {
+ NEXT_NAMESPACE_ID.fetch_add(1, Ordering::Relaxed);
+ *frame_counter += 1;
+
+ let mut config = CaptureConfig::new(path, CaptureBits::all());
+ if let Some((scene_id, frame_id)) = ids {
+ config.scene_id = scene_id;
+ config.frame_id = frame_id;
+ }
+
+ self.load_capture(config);
+
+ for (id, doc) in &self.documents {
+ let captured = CapturedDocument {
+ document_id: *id,
+ root_pipeline_id: doc.loaded_scene.root_pipeline_id,
+ };
+ tx.send(captured).unwrap();
+ }
+
+ // Note: we can't pass `LoadCapture` here since it needs to arrive
+ // before the `PublishDocument` messages sent by `load_capture`.
+ return RenderBackendStatus::Continue;
+ }
+ DebugCommand::ClearCaches(mask) => {
+ self.resource_cache.clear(mask);
+ return RenderBackendStatus::Continue;
+ }
+ DebugCommand::EnableNativeCompositor(enable) => {
+ // Default CompositorKind should be Native
+ if let CompositorKind::Draw { .. } = self.default_compositor_kind {
+ unreachable!();
+ }
+
+ let compositor_kind = if enable {
+ self.default_compositor_kind
+ } else {
+ CompositorKind::default()
+ };
+
+ for (_, doc) in &mut self.documents {
+ doc.scene.config.compositor_kind = compositor_kind;
+ doc.frame_is_valid = false;
+ }
+
+ self.frame_config.compositor_kind = compositor_kind;
+ self.update_frame_builder_config();
+
+ // We don't want to forward this message to the renderer.
+ return RenderBackendStatus::Continue;
+ }
+ DebugCommand::SetBatchingLookback(count) => {
+ self.frame_config.batch_lookback_count = count as usize;
+ self.update_frame_builder_config();
+
+ return RenderBackendStatus::Continue;
+ }
+ DebugCommand::SimulateLongSceneBuild(time_ms) => {
+ let _ = self.scene_tx.send(SceneBuilderRequest::SimulateLongSceneBuild(time_ms));
+ return RenderBackendStatus::Continue;
+ }
+ DebugCommand::SetFlags(flags) => {
+ self.resource_cache.set_debug_flags(flags);
+ self.gpu_cache.set_debug_flags(flags);
+
+ let force_invalidation = flags.contains(DebugFlags::FORCE_PICTURE_INVALIDATION);
+ if self.frame_config.force_invalidation != force_invalidation {
+ self.frame_config.force_invalidation = force_invalidation;
+ self.update_frame_builder_config();
+ }
+
+ // If we're toggling on the GPU cache debug display, we
+ // need to blow away the cache. This is because we only
+ // send allocation/free notifications to the renderer
+ // thread when the debug display is enabled, and thus
+ // enabling it when the cache is partially populated will
+ // give the renderer an incomplete view of the world.
+ // And since we might as well drop all the debugging state
+ // from the renderer when we disable the debug display,
+ // we just clear the cache on toggle.
+ let changed = self.debug_flags ^ flags;
+ if changed.contains(DebugFlags::GPU_CACHE_DBG) {
+ self.gpu_cache.clear();
+ }
+ self.debug_flags = flags;
+
+ ResultMsg::DebugCommand(option)
+ }
+ _ => ResultMsg::DebugCommand(option),
+ };
+ self.result_tx.send(msg).unwrap();
+ self.notifier.wake_up(true);
+ }
+ ApiMsg::UpdateDocuments(transaction_msgs) => {
+ self.prepare_transactions(
+ transaction_msgs,
+ frame_counter,
+ );
+ }
+ ApiMsg::SceneBuilderResult(msg) => {
+ return self.process_scene_builder_result(msg, frame_counter);
+ }
+ }
+
+ RenderBackendStatus::Continue
+ }
+
+ fn process_scene_builder_result(
+ &mut self,
+ msg: SceneBuilderResult,
+ frame_counter: &mut u32,
+ ) -> RenderBackendStatus {
+ profile_scope!("sb_msg");
+
+ match msg {
+ SceneBuilderResult::Transactions(txns, result_tx) => {
+ self.process_transaction(
+ txns,
+ result_tx,
+ frame_counter,
+ );
+ self.bookkeep_after_frames();
+ },
+ #[cfg(feature = "capture")]
+ SceneBuilderResult::CapturedTransactions(txns, capture_config, result_tx) => {
+ if let Some(ref mut old_config) = self.capture_config {
+ assert!(old_config.scene_id <= capture_config.scene_id);
+ if old_config.scene_id < capture_config.scene_id {
+ old_config.scene_id = capture_config.scene_id;
+ old_config.frame_id = 0;
+ }
+ } else {
+ self.capture_config = Some(capture_config);
+ }
+
+ let built_frame = self.process_transaction(
+ txns,
+ result_tx,
+ frame_counter,
+ );
+
+ if built_frame {
+ self.save_capture_sequence();
+ }
+
+ self.bookkeep_after_frames();
+ },
+ #[cfg(feature = "capture")]
+ SceneBuilderResult::StopCaptureSequence => {
+ self.capture_config = None;
+ }
+ SceneBuilderResult::GetGlyphDimensions(request) => {
+ let mut glyph_dimensions = Vec::with_capacity(request.glyph_indices.len());
+ let instance_key = self.resource_cache.map_font_instance_key(request.key);
+ if let Some(base) = self.resource_cache.get_font_instance(instance_key) {
+ let font = FontInstance::from_base(Arc::clone(&base));
+ for glyph_index in &request.glyph_indices {
+ let glyph_dim = self.resource_cache.get_glyph_dimensions(&font, *glyph_index);
+ glyph_dimensions.push(glyph_dim);
+ }
+ }
+ request.sender.send(glyph_dimensions).unwrap();
+ }
+ SceneBuilderResult::GetGlyphIndices(request) => {
+ let mut glyph_indices = Vec::with_capacity(request.text.len());
+ let font_key = self.resource_cache.map_font_key(request.key);
+ for ch in request.text.chars() {
+ let index = self.resource_cache.get_glyph_index(font_key, ch);
+ glyph_indices.push(index);
+ }
+ request.sender.send(glyph_indices).unwrap();
+ }
+ SceneBuilderResult::FlushComplete(tx) => {
+ tx.send(()).ok();
+ }
+ SceneBuilderResult::ExternalEvent(evt) => {
+ self.notifier.external_event(evt);
+ }
+ SceneBuilderResult::ClearNamespace(id) => {
+ self.resource_cache.clear_namespace(id);
+ self.documents.retain(|doc_id, _doc| doc_id.namespace_id != id);
+ }
+ SceneBuilderResult::DeleteDocument(document_id) => {
+ self.documents.remove(&document_id);
+ }
+ SceneBuilderResult::SetParameter(param) => {
+ if let Parameter::Bool(BoolParameter::Multithreading, enabled) = param {
+ self.resource_cache.enable_multithreading(enabled);
+ }
+ let _ = self.result_tx.send(ResultMsg::SetParameter(param));
+ }
+ SceneBuilderResult::StopRenderBackend => {
+ return RenderBackendStatus::StopRenderBackend;
+ }
+ SceneBuilderResult::ShutDown(sender) => {
+ info!("Recycling stats: {:?}", self.recycler);
+ return RenderBackendStatus::ShutDown(sender);
+ }
+ }
+
+ RenderBackendStatus::Continue
+ }
+
+ fn update_frame_builder_config(&self) {
+ self.send_backend_message(
+ SceneBuilderRequest::SetFrameBuilderConfig(
+ self.frame_config.clone()
+ )
+ );
+ }
+
+ fn prepare_for_frames(&mut self) {
+ self.gpu_cache.prepare_for_frames();
+ }
+
+ fn bookkeep_after_frames(&mut self) {
+ self.gpu_cache.bookkeep_after_frames();
+ }
+
+ fn requires_frame_build(&mut self) -> bool {
+ self.gpu_cache.requires_frame_build()
+ }
+
+ fn prepare_transactions(
+ &mut self,
+ txns: Vec<Box<TransactionMsg>>,
+ frame_counter: &mut u32,
+ ) {
+ self.prepare_for_frames();
+ self.maybe_force_nop_documents(
+ frame_counter,
+ |document_id| txns.iter().any(|txn| txn.document_id == document_id));
+
+ let mut built_frame = false;
+ for mut txn in txns {
+ if txn.generate_frame.as_bool() {
+ txn.profile.end_time(profiler::API_SEND_TIME);
+ }
+
+ self.documents.get_mut(&txn.document_id).unwrap().profile.merge(&mut txn.profile);
+
+ built_frame |= self.update_document(
+ txn.document_id,
+ txn.resource_updates.take(),
+ txn.frame_ops.take(),
+ txn.notifications.take(),
+ txn.generate_frame.as_bool(),
+ txn.render_reasons,
+ txn.generate_frame.id(),
+ txn.invalidate_rendered_frame,
+ frame_counter,
+ false,
+ txn.creation_time,
+ );
+ }
+ if built_frame {
+ #[cfg(feature = "capture")]
+ self.save_capture_sequence();
+ }
+ self.bookkeep_after_frames();
+ }
+
+ /// In certain cases, resources shared by multiple documents have to run
+ /// maintenance operations, like cleaning up unused cache items. In those
+ /// cases, we are forced to build frames for all documents, however we
+ /// may not have a transaction ready for every document - this method
+ /// calls update_document with the details of a fake, nop transaction just
+ /// to force a frame build.
+ fn maybe_force_nop_documents<F>(&mut self,
+ frame_counter: &mut u32,
+ document_already_present: F) where
+ F: Fn(DocumentId) -> bool {
+ if self.requires_frame_build() {
+ let nop_documents : Vec<DocumentId> = self.documents.keys()
+ .cloned()
+ .filter(|key| !document_already_present(*key))
+ .collect();
+ #[allow(unused_variables)]
+ let mut built_frame = false;
+ for &document_id in &nop_documents {
+ built_frame |= self.update_document(
+ document_id,
+ Vec::default(),
+ Vec::default(),
+ Vec::default(),
+ false,
+ RenderReasons::empty(),
+ None,
+ false,
+ frame_counter,
+ false,
+ None);
+ }
+ #[cfg(feature = "capture")]
+ match built_frame {
+ true => self.save_capture_sequence(),
+ _ => {},
+ }
+ }
+ }
+
+ fn update_document(
+ &mut self,
+ document_id: DocumentId,
+ resource_updates: Vec<ResourceUpdate>,
+ mut frame_ops: Vec<FrameMsg>,
+ mut notifications: Vec<NotificationRequest>,
+ mut render_frame: bool,
+ render_reasons: RenderReasons,
+ generated_frame_id: Option<u64>,
+ invalidate_rendered_frame: bool,
+ frame_counter: &mut u32,
+ has_built_scene: bool,
+ start_time: Option<u64>
+ ) -> bool {
+ let requested_frame = render_frame;
+
+ let requires_frame_build = self.requires_frame_build();
+ let doc = self.documents.get_mut(&document_id).unwrap();
+
+ // If we have a sampler, get more frame ops from it and add them
+ // to the transaction. This is a hook to allow the WR user code to
+ // fiddle with things after a potentially long scene build, but just
+ // before rendering. This is useful for rendering with the latest
+ // async transforms.
+ if requested_frame {
+ if let Some(ref sampler) = self.sampler {
+ frame_ops.append(&mut sampler.sample(document_id, generated_frame_id));
+ }
+ }
+
+ doc.has_built_scene |= has_built_scene;
+
+ // TODO: this scroll variable doesn't necessarily mean we scrolled. It is only used
+ // for something wrench specific and we should remove it.
+ let mut scroll = false;
+ for frame_msg in frame_ops {
+ let op = doc.process_frame_msg(frame_msg);
+ scroll |= op.scroll;
+ }
+
+ for update in &resource_updates {
+ if let ResourceUpdate::UpdateImage(..) = update {
+ doc.frame_is_valid = false;
+ }
+ }
+
+ self.resource_cache.post_scene_building_update(
+ resource_updates,
+ &mut doc.profile,
+ );
+
+ if doc.dynamic_properties.flush_pending_updates() {
+ doc.frame_is_valid = false;
+ doc.hit_tester_is_valid = false;
+ }
+
+ if !doc.can_render() {
+ // TODO: this happens if we are building the first scene asynchronously and
+ // scroll at the same time. we should keep track of the fact that we skipped
+ // composition here and do it as soon as we receive the scene.
+ render_frame = false;
+ }
+
+ // Avoid re-building the frame if the current built frame is still valid.
+ // However, if the resource_cache requires a frame build, _always_ do that, unless
+ // doc.can_render() is false, as in that case a frame build can't happen anyway.
+ // We want to ensure we do this because even if the doc doesn't have pixels it
+ // can still try to access stale texture cache items.
+ let build_frame = (render_frame && !doc.frame_is_valid && doc.has_pixels()) ||
+ (requires_frame_build && doc.can_render());
+
+ // Request composite is true when we want to composite frame even when
+ // there is no frame update. This happens when video frame is updated under
+ // external image with NativeTexture or when platform requested to composite frame.
+ if invalidate_rendered_frame {
+ doc.rendered_frame_is_valid = false;
+ if doc.scene.config.compositor_kind.should_redraw_on_invalidation() {
+ let msg = ResultMsg::ForceRedraw;
+ self.result_tx.send(msg).unwrap();
+ }
+ }
+
+ if build_frame {
+ if start_time.is_some() {
+ Telemetry::record_time_to_frame_build(Duration::from_nanos(precise_time_ns() - start_time.unwrap()));
+ }
+ profile_scope!("generate frame");
+
+ *frame_counter += 1;
+
+ // borrow ck hack for profile_counters
+ let (pending_update, mut rendered_document) = {
+ let timer_id = Telemetry::start_framebuild_time();
+
+ let frame_stats = doc.frame_stats.take();
+
+ let rendered_document = doc.build_frame(
+ &mut self.resource_cache,
+ &mut self.gpu_cache,
+ self.debug_flags,
+ &mut self.tile_caches,
+ frame_stats,
+ render_reasons,
+ );
+
+ debug!("generated frame for document {:?} with {} passes",
+ document_id, rendered_document.frame.passes.len());
+
+ let msg = ResultMsg::UpdateGpuCache(self.gpu_cache.extract_updates());
+ self.result_tx.send(msg).unwrap();
+
+ Telemetry::stop_and_accumulate_framebuild_time(timer_id);
+
+ let pending_update = self.resource_cache.pending_updates();
+ (pending_update, rendered_document)
+ };
+
+ // Invalidate dirty rects if the compositing config has changed significantly
+ rendered_document
+ .frame
+ .composite_state
+ .update_dirty_rect_validity(&doc.prev_composite_descriptor);
+
+ // Build a small struct that represents the state of the tiles to be composited.
+ let composite_descriptor = rendered_document
+ .frame
+ .composite_state
+ .descriptor
+ .clone();
+
+ // If there are texture cache updates to apply, or if the produced
+ // frame is not a no-op, or the compositor state has changed,
+ // then we cannot skip compositing this frame.
+ if !pending_update.is_nop() ||
+ !rendered_document.frame.is_nop() ||
+ composite_descriptor != doc.prev_composite_descriptor {
+ doc.rendered_frame_is_valid = false;
+ }
+ doc.prev_composite_descriptor = composite_descriptor;
+
+ #[cfg(feature = "capture")]
+ match self.capture_config {
+ Some(ref mut config) => {
+ // FIXME(aosmond): document splitting causes multiple prepare frames
+ config.prepare_frame();
+
+ if config.bits.contains(CaptureBits::FRAME) {
+ let file_name = format!("frame-{}-{}", document_id.namespace_id.0, document_id.id);
+ config.serialize_for_frame(&rendered_document.frame, file_name);
+ }
+
+ let data_stores_name = format!("data-stores-{}-{}", document_id.namespace_id.0, document_id.id);
+ config.serialize_for_frame(&doc.data_stores, data_stores_name);
+
+ let frame_spatial_tree_name = format!("frame-spatial-tree-{}-{}", document_id.namespace_id.0, document_id.id);
+ config.serialize_for_frame::<SpatialTree, _>(&doc.spatial_tree, frame_spatial_tree_name);
+
+ let properties_name = format!("properties-{}-{}", document_id.namespace_id.0, document_id.id);
+ config.serialize_for_frame(&doc.dynamic_properties, properties_name);
+ },
+ None => {},
+ }
+
+ let msg = ResultMsg::PublishPipelineInfo(doc.updated_pipeline_info());
+ self.result_tx.send(msg).unwrap();
+
+ // Publish the frame
+ self.frame_publish_id.advance();
+ let msg = ResultMsg::PublishDocument(
+ self.frame_publish_id,
+ document_id,
+ rendered_document,
+ pending_update,
+ );
+ self.result_tx.send(msg).unwrap();
+ } else if requested_frame {
+ // WR-internal optimization to avoid doing a bunch of render work if
+ // there's no pixels. We still want to pretend to render and request
+ // a render to make sure that the callbacks (particularly the
+ // new_frame_ready callback below) has the right flags.
+ let msg = ResultMsg::PublishPipelineInfo(doc.updated_pipeline_info());
+ self.result_tx.send(msg).unwrap();
+ }
+
+ drain_filter(
+ &mut notifications,
+ |n| { n.when() == Checkpoint::FrameBuilt },
+ |n| { n.notify(); },
+ );
+
+ if !notifications.is_empty() {
+ self.result_tx.send(ResultMsg::AppendNotificationRequests(notifications)).unwrap();
+ }
+
+ // Always forward the transaction to the renderer if a frame was requested,
+ // otherwise gecko can get into a state where it waits (forever) for the
+ // transaction to complete before sending new work.
+ if requested_frame {
+ // If rendered frame is already valid, there is no need to render frame.
+ if doc.rendered_frame_is_valid {
+ render_frame = false;
+ } else if render_frame {
+ doc.rendered_frame_is_valid = true;
+ }
+ self.notifier.new_frame_ready(document_id, scroll, render_frame, self.frame_publish_id);
+ }
+
+ if !doc.hit_tester_is_valid {
+ doc.rebuild_hit_tester();
+ }
+
+ build_frame
+ }
+
+ fn send_backend_message(&self, msg: SceneBuilderRequest) {
+ self.scene_tx.send(msg).unwrap();
+ }
+
+ fn report_memory(&mut self, tx: Sender<Box<MemoryReport>>) {
+ let mut report = Box::new(MemoryReport::default());
+ let ops = self.size_of_ops.as_mut().unwrap();
+ let op = ops.size_of_op;
+ report.gpu_cache_metadata = self.gpu_cache.size_of(ops);
+ for doc in self.documents.values() {
+ report.clip_stores += doc.scene.clip_store.size_of(ops);
+ report.hit_testers += match &doc.hit_tester {
+ Some(hit_tester) => hit_tester.size_of(ops),
+ None => 0,
+ };
+
+ doc.data_stores.report_memory(ops, &mut report)
+ }
+
+ (*report) += self.resource_cache.report_memory(op);
+ report.texture_cache_structures = self.resource_cache
+ .texture_cache
+ .report_memory(ops);
+
+ // Send a message to report memory on the scene-builder thread, which
+ // will add its report to this one and send the result back to the original
+ // thread waiting on the request.
+ self.send_backend_message(
+ SceneBuilderRequest::ReportMemory(report, tx)
+ );
+ }
+
+ #[cfg(feature = "capture")]
+ fn save_capture_sequence(&mut self) {
+ if let Some(ref mut config) = self.capture_config {
+ let deferred = self.resource_cache.save_capture_sequence(config);
+
+ let backend = PlainRenderBackend {
+ frame_config: self.frame_config.clone(),
+ resource_sequence_id: config.resource_id,
+ documents: self.documents
+ .iter()
+ .map(|(id, doc)| (*id, doc.view))
+ .collect(),
+ };
+ config.serialize_for_frame(&backend, "backend");
+
+ if !deferred.is_empty() {
+ let msg = ResultMsg::DebugOutput(DebugOutput::SaveCapture(config.clone(), deferred));
+ self.result_tx.send(msg).unwrap();
+ }
+ }
+ }
+}
+
+impl RenderBackend {
+ #[cfg(feature = "capture")]
+ // Note: the mutable `self` is only needed here for resolving blob images
+ fn save_capture(
+ &mut self,
+ root: PathBuf,
+ bits: CaptureBits,
+ ) -> DebugOutput {
+ use std::fs;
+ use crate::render_task_graph::dump_render_tasks_as_svg;
+
+ debug!("capture: saving {:?}", root);
+ if !root.is_dir() {
+ if let Err(e) = fs::create_dir_all(&root) {
+ panic!("Unable to create capture dir: {:?}", e);
+ }
+ }
+ let config = CaptureConfig::new(root, bits);
+
+ if config.bits.contains(CaptureBits::FRAME) {
+ self.prepare_for_frames();
+ }
+
+ for (&id, doc) in &mut self.documents {
+ debug!("\tdocument {:?}", id);
+ if config.bits.contains(CaptureBits::FRAME) {
+ // Temporarily force invalidation otherwise the render task graph dump is empty.
+ let force_invalidation = std::mem::replace(&mut doc.scene.config.force_invalidation, true);
+
+ let rendered_document = doc.build_frame(
+ &mut self.resource_cache,
+ &mut self.gpu_cache,
+ self.debug_flags,
+ &mut self.tile_caches,
+ None,
+ RenderReasons::empty(),
+ );
+
+ doc.scene.config.force_invalidation = force_invalidation;
+
+ // 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
+ // they are applied on the `Renderer` side.
+ let msg_update_gpu_cache = ResultMsg::UpdateGpuCache(self.gpu_cache.extract_updates());
+ self.result_tx.send(msg_update_gpu_cache).unwrap();
+ //TODO: write down doc's pipeline info?
+ // it has `pipeline_epoch_map`,
+ // which may capture necessary details for some cases.
+ let file_name = format!("frame-{}-{}", id.namespace_id.0, id.id);
+ config.serialize_for_frame(&rendered_document.frame, file_name);
+ let file_name = format!("spatial-{}-{}", id.namespace_id.0, id.id);
+ config.serialize_tree_for_frame(&doc.spatial_tree, file_name);
+ let file_name = format!("built-primitives-{}-{}", id.namespace_id.0, id.id);
+ config.serialize_for_frame(&doc.scene.prim_store, file_name);
+ let file_name = format!("built-clips-{}-{}", id.namespace_id.0, id.id);
+ config.serialize_for_frame(&doc.scene.clip_store, file_name);
+ let file_name = format!("scratch-{}-{}", id.namespace_id.0, id.id);
+ config.serialize_for_frame(&doc.scratch.primitive, file_name);
+ let file_name = format!("render-tasks-{}-{}.svg", id.namespace_id.0, id.id);
+ let mut render_tasks_file = fs::File::create(&config.file_path_for_frame(file_name, "svg"))
+ .expect("Failed to open the SVG file.");
+ dump_render_tasks_as_svg(
+ &rendered_document.frame.render_tasks,
+ &mut render_tasks_file
+ ).unwrap();
+
+ let file_name = format!("texture-cache-color-linear-{}-{}.svg", id.namespace_id.0, id.id);
+ let mut texture_file = fs::File::create(&config.file_path_for_frame(file_name, "svg"))
+ .expect("Failed to open the SVG file.");
+ self.resource_cache.texture_cache.dump_color8_linear_as_svg(&mut texture_file).unwrap();
+
+ let file_name = format!("texture-cache-color8-glyphs-{}-{}.svg", id.namespace_id.0, id.id);
+ let mut texture_file = fs::File::create(&config.file_path_for_frame(file_name, "svg"))
+ .expect("Failed to open the SVG file.");
+ self.resource_cache.texture_cache.dump_color8_glyphs_as_svg(&mut texture_file).unwrap();
+
+ let file_name = format!("texture-cache-alpha8-glyphs-{}-{}.svg", id.namespace_id.0, id.id);
+ let mut texture_file = fs::File::create(&config.file_path_for_frame(file_name, "svg"))
+ .expect("Failed to open the SVG file.");
+ self.resource_cache.texture_cache.dump_alpha8_glyphs_as_svg(&mut texture_file).unwrap();
+
+ let file_name = format!("texture-cache-alpha8-linear-{}-{}.svg", id.namespace_id.0, id.id);
+ let mut texture_file = fs::File::create(&config.file_path_for_frame(file_name, "svg"))
+ .expect("Failed to open the SVG file.");
+ self.resource_cache.texture_cache.dump_alpha8_linear_as_svg(&mut texture_file).unwrap();
+ }
+
+ let data_stores_name = format!("data-stores-{}-{}", id.namespace_id.0, id.id);
+ config.serialize_for_frame(&doc.data_stores, data_stores_name);
+
+ let frame_spatial_tree_name = format!("frame-spatial-tree-{}-{}", id.namespace_id.0, id.id);
+ config.serialize_for_frame::<SpatialTree, _>(&doc.spatial_tree, frame_spatial_tree_name);
+
+ let properties_name = format!("properties-{}-{}", id.namespace_id.0, id.id);
+ config.serialize_for_frame(&doc.dynamic_properties, properties_name);
+ }
+
+ if config.bits.contains(CaptureBits::FRAME) {
+ // TODO: there is no guarantee that we won't hit this case, but we want to
+ // report it here if we do. If we don't, it will simply crash in
+ // Renderer::render_impl and give us less information about the source.
+ assert!(!self.requires_frame_build(), "Caches were cleared during a capture.");
+ self.bookkeep_after_frames();
+ }
+
+ debug!("\tscene builder");
+ self.send_backend_message(
+ SceneBuilderRequest::SaveScene(config.clone())
+ );
+
+ debug!("\tresource cache");
+ let (resources, deferred) = self.resource_cache.save_capture(&config.root);
+
+ info!("\tbackend");
+ let backend = PlainRenderBackend {
+ frame_config: self.frame_config.clone(),
+ resource_sequence_id: 0,
+ documents: self.documents
+ .iter()
+ .map(|(id, doc)| (*id, doc.view))
+ .collect(),
+ };
+
+ config.serialize_for_frame(&backend, "backend");
+ config.serialize_for_frame(&resources, "plain-resources");
+
+ if config.bits.contains(CaptureBits::FRAME) {
+ let msg_update_resources = ResultMsg::UpdateResources {
+ resource_updates: self.resource_cache.pending_updates(),
+ memory_pressure: false,
+ };
+ self.result_tx.send(msg_update_resources).unwrap();
+ // Save the texture/glyph/image caches.
+ info!("\tresource cache");
+ let caches = self.resource_cache.save_caches(&config.root);
+ config.serialize_for_resource(&caches, "resource_cache");
+ info!("\tgpu cache");
+ config.serialize_for_resource(&self.gpu_cache, "gpu_cache");
+ }
+
+ DebugOutput::SaveCapture(config, deferred)
+ }
+
+ #[cfg(feature = "capture")]
+ fn start_capture_sequence(
+ &mut self,
+ root: PathBuf,
+ bits: CaptureBits,
+ ) {
+ self.send_backend_message(
+ SceneBuilderRequest::StartCaptureSequence(CaptureConfig::new(root, bits))
+ );
+ }
+
+ #[cfg(feature = "capture")]
+ fn stop_capture_sequence(
+ &mut self,
+ ) {
+ self.send_backend_message(
+ SceneBuilderRequest::StopCaptureSequence
+ );
+ }
+
+ #[cfg(feature = "replay")]
+ fn load_capture(
+ &mut self,
+ mut config: CaptureConfig,
+ ) {
+ debug!("capture: loading {:?}", config.frame_root());
+ let backend = config.deserialize_for_frame::<PlainRenderBackend, _>("backend")
+ .expect("Unable to open backend.ron");
+
+ // If this is a capture sequence, then the ID will be non-zero, and won't
+ // match what is loaded, but for still captures, the ID will be zero.
+ let first_load = backend.resource_sequence_id == 0;
+ if self.loaded_resource_sequence_id != backend.resource_sequence_id || first_load {
+ // FIXME(aosmond): We clear the documents because when we update the
+ // resource cache, we actually wipe and reload, because we don't
+ // know what is the same and what has changed. If we were to keep as
+ // much of the resource cache state as possible, we could avoid
+ // flushing the document state (which has its own dependecies on the
+ // cache).
+ //
+ // FIXME(aosmond): If we try to load the next capture in the
+ // sequence too quickly, we may lose resources we depend on in the
+ // current frame. This can cause panics. Ideally we would not
+ // advance to the next frame until the FrameRendered event for all
+ // of the pipelines.
+ self.documents.clear();
+
+ config.resource_id = backend.resource_sequence_id;
+ self.loaded_resource_sequence_id = backend.resource_sequence_id;
+
+ let plain_resources = config.deserialize_for_resource::<PlainResources, _>("plain-resources")
+ .expect("Unable to open plain-resources.ron");
+ let caches_maybe = config.deserialize_for_resource::<PlainCacheOwn, _>("resource_cache");
+
+ // Note: it would be great to have `RenderBackend` to be split
+ // rather explicitly on what's used before and after scene building
+ // so that, for example, we never miss anything in the code below:
+
+ let plain_externals = self.resource_cache.load_capture(
+ plain_resources,
+ caches_maybe,
+ &config,
+ );
+
+ let msg_load = ResultMsg::DebugOutput(
+ DebugOutput::LoadCapture(config.clone(), plain_externals)
+ );
+ self.result_tx.send(msg_load).unwrap();
+
+ self.gpu_cache = match config.deserialize_for_resource::<GpuCache, _>("gpu_cache") {
+ Some(gpu_cache) => gpu_cache,
+ None => GpuCache::new(),
+ };
+ }
+
+ self.frame_config = backend.frame_config;
+
+ let mut scenes_to_build = Vec::new();
+
+ for (id, view) in backend.documents {
+ debug!("\tdocument {:?}", id);
+ let scene_name = format!("scene-{}-{}", id.namespace_id.0, id.id);
+ let scene = config.deserialize_for_scene::<Scene, _>(&scene_name)
+ .expect(&format!("Unable to open {}.ron", scene_name));
+
+ let scene_spatial_tree_name = format!("scene-spatial-tree-{}-{}", id.namespace_id.0, id.id);
+ let scene_spatial_tree = config.deserialize_for_scene::<SceneSpatialTree, _>(&scene_spatial_tree_name)
+ .expect(&format!("Unable to open {}.ron", scene_spatial_tree_name));
+
+ let interners_name = format!("interners-{}-{}", id.namespace_id.0, id.id);
+ let interners = config.deserialize_for_scene::<Interners, _>(&interners_name)
+ .expect(&format!("Unable to open {}.ron", interners_name));
+
+ let data_stores_name = format!("data-stores-{}-{}", id.namespace_id.0, id.id);
+ let data_stores = config.deserialize_for_frame::<DataStores, _>(&data_stores_name)
+ .expect(&format!("Unable to open {}.ron", data_stores_name));
+
+ let properties_name = format!("properties-{}-{}", id.namespace_id.0, id.id);
+ let properties = config.deserialize_for_frame::<SceneProperties, _>(&properties_name)
+ .expect(&format!("Unable to open {}.ron", properties_name));
+
+ let frame_spatial_tree_name = format!("frame-spatial-tree-{}-{}", id.namespace_id.0, id.id);
+ let frame_spatial_tree = config.deserialize_for_frame::<SpatialTree, _>(&frame_spatial_tree_name)
+ .expect(&format!("Unable to open {}.ron", frame_spatial_tree_name));
+
+ // Update the document if it still exists, rather than replace it entirely.
+ // This allows us to preserve state information such as the frame stamp,
+ // which is necessary for cache sanity.
+ match self.documents.entry(id) {
+ Occupied(entry) => {
+ let doc = entry.into_mut();
+ doc.view = view;
+ doc.loaded_scene = scene.clone();
+ doc.data_stores = data_stores;
+ doc.spatial_tree = frame_spatial_tree;
+ doc.dynamic_properties = properties;
+ doc.frame_is_valid = false;
+ doc.rendered_frame_is_valid = false;
+ doc.has_built_scene = false;
+ doc.hit_tester_is_valid = false;
+ }
+ Vacant(entry) => {
+ let doc = Document {
+ id,
+ scene: BuiltScene::empty(),
+ removed_pipelines: Vec::new(),
+ view,
+ stamp: FrameStamp::first(id),
+ frame_builder: FrameBuilder::new(),
+ dynamic_properties: properties,
+ hit_tester: None,
+ shared_hit_tester: Arc::new(SharedHitTester::new()),
+ frame_is_valid: false,
+ hit_tester_is_valid: false,
+ rendered_frame_is_valid: false,
+ has_built_scene: false,
+ data_stores,
+ scratch: ScratchBuffer::default(),
+ spatial_tree: frame_spatial_tree,
+ loaded_scene: scene.clone(),
+ prev_composite_descriptor: CompositeDescriptor::empty(),
+ dirty_rects_are_valid: false,
+ profile: TransactionProfile::new(),
+ rg_builder: RenderTaskGraphBuilder::new(),
+ frame_stats: None,
+ };
+ entry.insert(doc);
+ }
+ };
+
+ let frame_name = format!("frame-{}-{}", id.namespace_id.0, id.id);
+ let frame = config.deserialize_for_frame::<Frame, _>(frame_name);
+ let build_frame = match frame {
+ Some(frame) => {
+ info!("\tloaded a built frame with {} passes", frame.passes.len());
+
+ let msg_update = ResultMsg::UpdateGpuCache(self.gpu_cache.extract_updates());
+ self.result_tx.send(msg_update).unwrap();
+
+ self.frame_publish_id.advance();
+ let msg_publish = ResultMsg::PublishDocument(
+ self.frame_publish_id,
+ id,
+ RenderedDocument {
+ frame,
+ is_new_scene: true,
+ profile: TransactionProfile::new(),
+ render_reasons: RenderReasons::empty(),
+ frame_stats: None,
+ },
+ self.resource_cache.pending_updates(),
+ );
+ self.result_tx.send(msg_publish).unwrap();
+
+ self.notifier.new_frame_ready(id, false, true, self.frame_publish_id);
+
+ // We deserialized the state of the frame so we don't want to build
+ // it (but we do want to update the scene builder's state)
+ false
+ }
+ None => true,
+ };
+
+ scenes_to_build.push(LoadScene {
+ document_id: id,
+ scene,
+ view: view.scene.clone(),
+ config: self.frame_config.clone(),
+ fonts: self.resource_cache.get_fonts(),
+ build_frame,
+ interners,
+ spatial_tree: scene_spatial_tree,
+ });
+ }
+
+ if !scenes_to_build.is_empty() {
+ self.send_backend_message(
+ SceneBuilderRequest::LoadScenes(scenes_to_build)
+ );
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/render_target.rs b/gfx/wr/webrender/src/render_target.rs
new file mode 100644
index 0000000000..d8f62a6088
--- /dev/null
+++ b/gfx/wr/webrender/src/render_target.rs
@@ -0,0 +1,1035 @@
+/* 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 api::units::*;
+use api::{ColorF, ClipMode, ImageFormat, LineOrientation, BorderStyle};
+use crate::batch::{AlphaBatchBuilder, AlphaBatchContainer, BatchTextures, add_quad_to_batch};
+use crate::batch::{ClipBatcher, BatchBuilder, INVALID_SEGMENT_INDEX};
+use crate::command_buffer::{CommandBufferList};
+use crate::spatial_tree::SpatialTree;
+use crate::clip::{ClipStore, ClipNodeRange, ClipItemKind};
+use crate::frame_builder::{FrameGlobalResources};
+use crate::gpu_cache::{GpuCache, GpuCacheAddress};
+use crate::gpu_types::{BorderInstance, SvgFilterInstance, BlurDirection, BlurInstance, PrimitiveHeaders, ScalingInstance};
+use crate::gpu_types::{TransformPalette, ZBufferIdGenerator, TransformPaletteId, PrimitiveInstanceData, MaskInstance};
+use crate::gpu_types::{ZBufferId};
+use crate::internal_types::{FastHashMap, TextureSource, CacheTextureId};
+use crate::picture::{SliceId, SurfaceInfo, ResolvedSurfaceTexture, TileCacheInstance};
+use crate::prim_store::{PrimitiveInstance, PrimitiveStore, PrimitiveScratchBuffer};
+use crate::prim_store::gradient::{
+ FastLinearGradientInstance, LinearGradientInstance, RadialGradientInstance,
+ ConicGradientInstance,
+};
+use crate::renderer::{GpuBufferAddress, GpuBufferBuilder};
+use crate::render_backend::DataStores;
+use crate::render_task::{RenderTaskKind, RenderTaskAddress};
+use crate::render_task::{RenderTask, ScalingTask, SvgFilterInfo};
+use crate::render_task_graph::{RenderTaskGraph, RenderTaskId};
+use crate::resource_cache::ResourceCache;
+use crate::spatial_tree::{SpatialNodeIndex};
+
+
+const STYLE_SOLID: i32 = ((BorderStyle::Solid as i32) << 8) | ((BorderStyle::Solid as i32) << 16);
+const STYLE_MASK: i32 = 0x00FF_FF00;
+
+/// A tag used to identify the output format of a `RenderTarget`.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum RenderTargetKind {
+ Color, // RGBA8
+ Alpha, // R8
+}
+
+/// Identifies a given `RenderTarget` in a `RenderTargetList`.
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RenderTargetIndex(pub usize);
+
+pub struct RenderTargetContext<'a, 'rc> {
+ pub global_device_pixel_scale: DevicePixelScale,
+ pub prim_store: &'a PrimitiveStore,
+ pub clip_store: &'a ClipStore,
+ pub resource_cache: &'rc mut ResourceCache,
+ pub use_dual_source_blending: bool,
+ pub use_advanced_blending: bool,
+ pub break_advanced_blend_batches: bool,
+ pub batch_lookback_count: usize,
+ pub spatial_tree: &'a SpatialTree,
+ pub data_stores: &'a DataStores,
+ pub surfaces: &'a [SurfaceInfo],
+ pub scratch: &'a PrimitiveScratchBuffer,
+ pub screen_world_rect: WorldRect,
+ pub globals: &'a FrameGlobalResources,
+ pub tile_caches: &'a FastHashMap<SliceId, Box<TileCacheInstance>>,
+ pub root_spatial_node_index: SpatialNodeIndex,
+}
+
+/// Represents a number of rendering operations on a surface.
+///
+/// In graphics parlance, a "render target" usually means "a surface (texture or
+/// framebuffer) bound to the output of a shader". This trait has a slightly
+/// different meaning, in that it represents the operations on that surface
+/// _before_ it's actually bound and rendered. So a `RenderTarget` is built by
+/// the `RenderBackend` by inserting tasks, and then shipped over to the
+/// `Renderer` where a device surface is resolved and the tasks are transformed
+/// into draw commands on that surface.
+///
+/// We express this as a trait to generalize over color and alpha surfaces.
+/// a given `RenderTask` will draw to one or the other, depending on its type
+/// and sometimes on its parameters. See `RenderTask::target_kind`.
+pub trait RenderTarget {
+ /// Creates a new RenderTarget of the given type.
+ fn new(
+ texture_id: CacheTextureId,
+ screen_size: DeviceIntSize,
+ gpu_supports_fast_clears: bool,
+ used_rect: DeviceIntRect,
+ ) -> Self;
+
+ /// Optional hook to provide additional processing for the target at the
+ /// end of the build phase.
+ fn build(
+ &mut self,
+ _ctx: &mut RenderTargetContext,
+ _gpu_cache: &mut GpuCache,
+ _render_tasks: &RenderTaskGraph,
+ _prim_headers: &mut PrimitiveHeaders,
+ _transforms: &mut TransformPalette,
+ _z_generator: &mut ZBufferIdGenerator,
+ _prim_instances: &[PrimitiveInstance],
+ _cmd_buffers: &CommandBufferList,
+ ) {
+ }
+
+ /// Associates a `RenderTask` with this target. That task must be assigned
+ /// to a region returned by invoking `allocate()` on this target.
+ ///
+ /// TODO(gw): It's a bit odd that we need the deferred resolves and mutable
+ /// GPU cache here. They are typically used by the build step above. They
+ /// are used for the blit jobs to allow resolve_image to be called. It's a
+ /// bit of extra overhead to store the image key here and the resolve them
+ /// in the build step separately. BUT: if/when we add more texture cache
+ /// target jobs, we might want to tidy this up.
+ fn add_task(
+ &mut self,
+ task_id: RenderTaskId,
+ ctx: &RenderTargetContext,
+ gpu_cache: &mut GpuCache,
+ gpu_buffer_builder: &mut GpuBufferBuilder,
+ render_tasks: &RenderTaskGraph,
+ clip_store: &ClipStore,
+ transforms: &mut TransformPalette,
+ );
+
+ fn needs_depth(&self) -> bool;
+ fn texture_id(&self) -> CacheTextureId;
+}
+
+/// A series of `RenderTarget` instances, serving as the high-level container
+/// into which `RenderTasks` are assigned.
+///
+/// During the build phase, we iterate over the tasks in each `RenderPass`. For
+/// each task, we invoke `allocate()` on the `RenderTargetList`, which in turn
+/// attempts to allocate an output region in the last `RenderTarget` in the
+/// list. If allocation fails (or if the list is empty), a new `RenderTarget` is
+/// created and appended to the list. The build phase then assign the task into
+/// the target associated with the final allocation.
+///
+/// The result is that each `RenderPass` is associated with one or two
+/// `RenderTargetLists`, depending on whether we have all our tasks have the
+/// same `RenderTargetKind`. The lists are then shipped to the `Renderer`, which
+/// allocates a device texture array, with one slice per render target in the
+/// list.
+///
+/// The upshot of this scheme is that it maximizes batching. In a given pass,
+/// we need to do a separate batch for each individual render target. But with
+/// the texture array, we can expose the entirety of the previous pass to each
+/// task in the current pass in a single batch, which generally allows each
+/// task to be drawn in a single batch regardless of how many results from the
+/// previous pass it depends on.
+///
+/// Note that in some cases (like drop-shadows), we can depend on the output of
+/// a pass earlier than the immediately-preceding pass.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RenderTargetList<T> {
+ pub format: ImageFormat,
+ pub targets: Vec<T>,
+}
+
+impl<T: RenderTarget> RenderTargetList<T> {
+ pub fn new(
+ format: ImageFormat,
+ ) -> Self {
+ RenderTargetList {
+ format,
+ targets: Vec::new(),
+ }
+ }
+
+ pub fn build(
+ &mut self,
+ ctx: &mut RenderTargetContext,
+ gpu_cache: &mut GpuCache,
+ render_tasks: &RenderTaskGraph,
+ prim_headers: &mut PrimitiveHeaders,
+ transforms: &mut TransformPalette,
+ z_generator: &mut ZBufferIdGenerator,
+ prim_instances: &[PrimitiveInstance],
+ cmd_buffers: &CommandBufferList,
+ ) {
+ if self.targets.is_empty() {
+ return;
+ }
+
+ for target in &mut self.targets {
+ target.build(
+ ctx,
+ gpu_cache,
+ render_tasks,
+ prim_headers,
+ transforms,
+ z_generator,
+ prim_instances,
+ cmd_buffers,
+ );
+ }
+ }
+
+ pub fn needs_depth(&self) -> bool {
+ self.targets.iter().any(|target| target.needs_depth())
+ }
+}
+
+
+/// Contains the work (in the form of instance arrays) needed to fill a color
+/// color output surface (RGBA8).
+///
+/// See `RenderTarget`.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ColorRenderTarget {
+ pub alpha_batch_containers: Vec<AlphaBatchContainer>,
+ // List of blur operations to apply for this render target.
+ pub vertical_blurs: FastHashMap<TextureSource, Vec<BlurInstance>>,
+ pub horizontal_blurs: FastHashMap<TextureSource, Vec<BlurInstance>>,
+ pub scalings: FastHashMap<TextureSource, Vec<ScalingInstance>>,
+ pub svg_filters: Vec<(BatchTextures, Vec<SvgFilterInstance>)>,
+ pub blits: Vec<BlitJob>,
+ alpha_tasks: Vec<RenderTaskId>,
+ screen_size: DeviceIntSize,
+ pub texture_id: CacheTextureId,
+ // Track the used rect of the render target, so that
+ // we can set a scissor rect and only clear to the
+ // used portion of the target as an optimization.
+ pub used_rect: DeviceIntRect,
+ pub resolve_ops: Vec<ResolveOp>,
+ pub clear_color: Option<ColorF>,
+
+ pub prim_instances: Vec<PrimitiveInstanceData>,
+ pub mask_instances_fast: Vec<MaskInstance>,
+ pub mask_instances_slow: Vec<MaskInstance>,
+}
+
+impl RenderTarget for ColorRenderTarget {
+ fn new(
+ texture_id: CacheTextureId,
+ screen_size: DeviceIntSize,
+ _: bool,
+ used_rect: DeviceIntRect,
+ ) -> Self {
+ ColorRenderTarget {
+ alpha_batch_containers: Vec::new(),
+ vertical_blurs: FastHashMap::default(),
+ horizontal_blurs: FastHashMap::default(),
+ scalings: FastHashMap::default(),
+ svg_filters: Vec::new(),
+ blits: Vec::new(),
+ alpha_tasks: Vec::new(),
+ screen_size,
+ texture_id,
+ used_rect,
+ resolve_ops: Vec::new(),
+ clear_color: Some(ColorF::TRANSPARENT),
+ prim_instances: Vec::new(),
+ mask_instances_fast: Vec::new(),
+ mask_instances_slow: Vec::new(),
+ }
+ }
+
+ fn build(
+ &mut self,
+ ctx: &mut RenderTargetContext,
+ gpu_cache: &mut GpuCache,
+ render_tasks: &RenderTaskGraph,
+ prim_headers: &mut PrimitiveHeaders,
+ transforms: &mut TransformPalette,
+ z_generator: &mut ZBufferIdGenerator,
+ prim_instances: &[PrimitiveInstance],
+ cmd_buffers: &CommandBufferList,
+ ) {
+ profile_scope!("build");
+ let mut merged_batches = AlphaBatchContainer::new(None);
+ let mut gpu_buffer_builder = GpuBufferBuilder::new();
+
+ for task_id in &self.alpha_tasks {
+ profile_scope!("alpha_task");
+ let task = &render_tasks[*task_id];
+
+ match task.kind {
+ RenderTaskKind::Picture(ref pic_task) => {
+ let target_rect = task.get_target_rect();
+
+ let scissor_rect = if pic_task.can_merge {
+ None
+ } else {
+ Some(target_rect)
+ };
+
+ if !pic_task.can_use_shared_surface {
+ self.clear_color = pic_task.clear_color;
+ }
+
+ // TODO(gw): The type names of AlphaBatchBuilder and BatchBuilder
+ // are still confusing. Once more of the picture caching
+ // improvement code lands, the AlphaBatchBuilder and
+ // AlphaBatchList types will be collapsed into one, which
+ // should simplify coming up with better type names.
+ let alpha_batch_builder = AlphaBatchBuilder::new(
+ self.screen_size,
+ ctx.break_advanced_blend_batches,
+ ctx.batch_lookback_count,
+ *task_id,
+ (*task_id).into(),
+ );
+
+ let mut batch_builder = BatchBuilder::new(alpha_batch_builder);
+ let cmd_buffer = cmd_buffers.get(pic_task.cmd_buffer_index);
+
+ cmd_buffer.iter_prims(&mut |cmd, spatial_node_index, segments| {
+ batch_builder.add_prim_to_batch(
+ cmd,
+ spatial_node_index,
+ ctx,
+ gpu_cache,
+ render_tasks,
+ prim_headers,
+ transforms,
+ pic_task.raster_spatial_node_index,
+ pic_task.surface_spatial_node_index,
+ z_generator,
+ prim_instances,
+ &mut gpu_buffer_builder,
+ segments,
+ );
+ });
+
+ let alpha_batch_builder = batch_builder.finalize();
+
+ alpha_batch_builder.build(
+ &mut self.alpha_batch_containers,
+ &mut merged_batches,
+ target_rect,
+ scissor_rect,
+ );
+ }
+ _ => {
+ unreachable!();
+ }
+ }
+ }
+
+ if !merged_batches.is_empty() {
+ self.alpha_batch_containers.push(merged_batches);
+ }
+ }
+
+ fn texture_id(&self) -> CacheTextureId {
+ self.texture_id
+ }
+
+ fn add_task(
+ &mut self,
+ task_id: RenderTaskId,
+ ctx: &RenderTargetContext,
+ gpu_cache: &mut GpuCache,
+ gpu_buffer_builder: &mut GpuBufferBuilder,
+ render_tasks: &RenderTaskGraph,
+ _: &ClipStore,
+ transforms: &mut TransformPalette,
+ ) {
+ profile_scope!("add_task");
+ let task = &render_tasks[task_id];
+
+ match task.kind {
+ RenderTaskKind::Prim(ref info) => {
+ let render_task_address = task_id.into();
+
+ add_quad_to_batch(
+ render_task_address,
+ info.transform_id,
+ info.prim_address,
+ info.quad_flags,
+ info.edge_flags,
+ INVALID_SEGMENT_INDEX as u8,
+ RenderTaskId::INVALID,
+ ZBufferId(0),
+ render_tasks,
+ |_, instance| {
+ self.prim_instances.push(instance);
+ }
+ );
+
+ let mask_instances_fast = &mut self.mask_instances_fast;
+ let mask_instances_slow = &mut self.mask_instances_slow;
+
+ build_mask_tasks(
+ info.clip_node_range,
+ info.prim_spatial_node_index,
+ ctx.clip_store,
+ ctx.data_stores,
+ ctx.spatial_tree,
+ gpu_buffer_builder,
+ transforms,
+ |fast_path, clip_address, clip_transform_id| {
+ add_quad_to_batch(
+ render_task_address,
+ info.transform_id,
+ info.prim_address,
+ info.quad_flags,
+ info.edge_flags,
+ INVALID_SEGMENT_INDEX as u8,
+ RenderTaskId::INVALID,
+ ZBufferId(0),
+ render_tasks,
+ |_, prim| {
+ let instance = MaskInstance {
+ prim,
+ clip_transform_id,
+ clip_address: clip_address.as_int(),
+ info: [0; 2],
+ };
+
+ if fast_path {
+ mask_instances_fast.push(instance);
+ } else {
+ mask_instances_slow.push(instance);
+ }
+ }
+ );
+ }
+ );
+ }
+ RenderTaskKind::VerticalBlur(..) => {
+ add_blur_instances(
+ &mut self.vertical_blurs,
+ BlurDirection::Vertical,
+ task_id.into(),
+ task.children[0],
+ render_tasks,
+ );
+ }
+ RenderTaskKind::HorizontalBlur(..) => {
+ add_blur_instances(
+ &mut self.horizontal_blurs,
+ BlurDirection::Horizontal,
+ task_id.into(),
+ task.children[0],
+ render_tasks,
+ );
+ }
+ RenderTaskKind::Picture(ref pic_task) => {
+ if let Some(ref resolve_op) = pic_task.resolve_op {
+ self.resolve_ops.push(resolve_op.clone());
+ }
+ self.alpha_tasks.push(task_id);
+ }
+ RenderTaskKind::SvgFilter(ref task_info) => {
+ add_svg_filter_instances(
+ &mut self.svg_filters,
+ render_tasks,
+ &task_info.info,
+ task_id,
+ task.children.get(0).cloned(),
+ task.children.get(1).cloned(),
+ task_info.extra_gpu_cache_handle.map(|handle| gpu_cache.get_address(&handle)),
+ )
+ }
+ RenderTaskKind::Image(..) |
+ RenderTaskKind::Cached(..) |
+ RenderTaskKind::ClipRegion(..) |
+ RenderTaskKind::Border(..) |
+ RenderTaskKind::CacheMask(..) |
+ RenderTaskKind::FastLinearGradient(..) |
+ RenderTaskKind::LinearGradient(..) |
+ RenderTaskKind::RadialGradient(..) |
+ RenderTaskKind::ConicGradient(..) |
+ RenderTaskKind::TileComposite(..) |
+ RenderTaskKind::LineDecoration(..) => {
+ panic!("Should not be added to color target!");
+ }
+ RenderTaskKind::Readback(..) => {}
+ RenderTaskKind::Scaling(ref info) => {
+ add_scaling_instances(
+ info,
+ &mut self.scalings,
+ task,
+ task.children.first().map(|&child| &render_tasks[child]),
+ );
+ }
+ RenderTaskKind::Blit(ref task_info) => {
+ let target_rect = task.get_target_rect();
+ self.blits.push(BlitJob {
+ source: task_info.source,
+ target_rect,
+ });
+ }
+ #[cfg(test)]
+ RenderTaskKind::Test(..) => {}
+ }
+ }
+
+ fn needs_depth(&self) -> bool {
+ self.alpha_batch_containers.iter().any(|ab| {
+ !ab.opaque_batches.is_empty()
+ })
+ }
+}
+
+/// Contains the work (in the form of instance arrays) needed to fill an alpha
+/// output surface (R8).
+///
+/// See `RenderTarget`.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct AlphaRenderTarget {
+ pub clip_batcher: ClipBatcher,
+ // List of blur operations to apply for this render target.
+ pub vertical_blurs: FastHashMap<TextureSource, Vec<BlurInstance>>,
+ pub horizontal_blurs: FastHashMap<TextureSource, Vec<BlurInstance>>,
+ pub scalings: FastHashMap<TextureSource, Vec<ScalingInstance>>,
+ pub zero_clears: Vec<RenderTaskId>,
+ pub one_clears: Vec<RenderTaskId>,
+ pub texture_id: CacheTextureId,
+}
+
+impl RenderTarget for AlphaRenderTarget {
+ fn new(
+ texture_id: CacheTextureId,
+ _: DeviceIntSize,
+ gpu_supports_fast_clears: bool,
+ _: DeviceIntRect,
+ ) -> Self {
+ AlphaRenderTarget {
+ clip_batcher: ClipBatcher::new(gpu_supports_fast_clears),
+ vertical_blurs: FastHashMap::default(),
+ horizontal_blurs: FastHashMap::default(),
+ scalings: FastHashMap::default(),
+ zero_clears: Vec::new(),
+ one_clears: Vec::new(),
+ texture_id,
+ }
+ }
+
+ fn texture_id(&self) -> CacheTextureId {
+ self.texture_id
+ }
+
+ fn add_task(
+ &mut self,
+ task_id: RenderTaskId,
+ ctx: &RenderTargetContext,
+ gpu_cache: &mut GpuCache,
+ _: &mut GpuBufferBuilder,
+ render_tasks: &RenderTaskGraph,
+ clip_store: &ClipStore,
+ transforms: &mut TransformPalette,
+ ) {
+ profile_scope!("add_task");
+ let task = &render_tasks[task_id];
+ let target_rect = task.get_target_rect();
+
+ match task.kind {
+ RenderTaskKind::Prim(..) |
+ RenderTaskKind::Image(..) |
+ RenderTaskKind::Cached(..) |
+ RenderTaskKind::Readback(..) |
+ RenderTaskKind::Picture(..) |
+ RenderTaskKind::Blit(..) |
+ RenderTaskKind::Border(..) |
+ RenderTaskKind::LineDecoration(..) |
+ RenderTaskKind::FastLinearGradient(..) |
+ RenderTaskKind::LinearGradient(..) |
+ RenderTaskKind::RadialGradient(..) |
+ RenderTaskKind::ConicGradient(..) |
+ RenderTaskKind::TileComposite(..) |
+ RenderTaskKind::SvgFilter(..) => {
+ panic!("BUG: should not be added to alpha target!");
+ }
+ RenderTaskKind::VerticalBlur(..) => {
+ self.zero_clears.push(task_id);
+ add_blur_instances(
+ &mut self.vertical_blurs,
+ BlurDirection::Vertical,
+ task_id.into(),
+ task.children[0],
+ render_tasks,
+ );
+ }
+ RenderTaskKind::HorizontalBlur(..) => {
+ self.zero_clears.push(task_id);
+ add_blur_instances(
+ &mut self.horizontal_blurs,
+ BlurDirection::Horizontal,
+ task_id.into(),
+ task.children[0],
+ render_tasks,
+ );
+ }
+ RenderTaskKind::CacheMask(ref task_info) => {
+ let clear_to_one = self.clip_batcher.add(
+ task_info.clip_node_range,
+ task_info.root_spatial_node_index,
+ render_tasks,
+ gpu_cache,
+ clip_store,
+ transforms,
+ task_info.actual_rect,
+ task_info.device_pixel_scale,
+ target_rect.min.to_f32(),
+ task_info.actual_rect.min,
+ ctx,
+ );
+ if task_info.clear_to_one || clear_to_one {
+ self.one_clears.push(task_id);
+ }
+ }
+ RenderTaskKind::ClipRegion(ref region_task) => {
+ if region_task.clear_to_one {
+ self.one_clears.push(task_id);
+ }
+ let device_rect = DeviceRect::from_size(
+ target_rect.size().to_f32(),
+ );
+ self.clip_batcher.add_clip_region(
+ region_task.local_pos,
+ device_rect,
+ region_task.clip_data.clone(),
+ target_rect.min.to_f32(),
+ DevicePoint::zero(),
+ region_task.device_pixel_scale.0,
+ );
+ }
+ RenderTaskKind::Scaling(ref info) => {
+ add_scaling_instances(
+ info,
+ &mut self.scalings,
+ task,
+ task.children.first().map(|&child| &render_tasks[child]),
+ );
+ }
+ #[cfg(test)]
+ RenderTaskKind::Test(..) => {}
+ }
+ }
+
+ fn needs_depth(&self) -> bool {
+ false
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, PartialEq, Clone)]
+pub struct ResolveOp {
+ pub src_task_ids: Vec<RenderTaskId>,
+ pub dest_task_id: RenderTaskId,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum PictureCacheTargetKind {
+ Draw {
+ alpha_batch_container: AlphaBatchContainer,
+ },
+ Blit {
+ task_id: RenderTaskId,
+ sub_rect_offset: DeviceIntVector2D,
+ },
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PictureCacheTarget {
+ pub surface: ResolvedSurfaceTexture,
+ pub kind: PictureCacheTargetKind,
+ pub clear_color: Option<ColorF>,
+ pub dirty_rect: DeviceIntRect,
+ pub valid_rect: DeviceIntRect,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct TextureCacheRenderTarget {
+ pub target_kind: RenderTargetKind,
+ pub horizontal_blurs: FastHashMap<TextureSource, Vec<BlurInstance>>,
+ pub blits: Vec<BlitJob>,
+ pub border_segments_complex: Vec<BorderInstance>,
+ pub border_segments_solid: Vec<BorderInstance>,
+ pub clears: Vec<DeviceIntRect>,
+ pub line_decorations: Vec<LineDecorationJob>,
+ pub fast_linear_gradients: Vec<FastLinearGradientInstance>,
+ pub linear_gradients: Vec<LinearGradientInstance>,
+ pub radial_gradients: Vec<RadialGradientInstance>,
+ pub conic_gradients: Vec<ConicGradientInstance>,
+}
+
+impl TextureCacheRenderTarget {
+ pub fn new(target_kind: RenderTargetKind) -> Self {
+ TextureCacheRenderTarget {
+ target_kind,
+ horizontal_blurs: FastHashMap::default(),
+ blits: vec![],
+ border_segments_complex: vec![],
+ border_segments_solid: vec![],
+ clears: vec![],
+ line_decorations: vec![],
+ fast_linear_gradients: vec![],
+ linear_gradients: vec![],
+ radial_gradients: vec![],
+ conic_gradients: vec![],
+ }
+ }
+
+ pub fn add_task(
+ &mut self,
+ task_id: RenderTaskId,
+ render_tasks: &RenderTaskGraph,
+ ) {
+ profile_scope!("add_task");
+ let task_address = task_id.into();
+
+ let task = &render_tasks[task_id];
+ let target_rect = task.get_target_rect();
+
+ match task.kind {
+ RenderTaskKind::LineDecoration(ref info) => {
+ self.clears.push(target_rect);
+
+ self.line_decorations.push(LineDecorationJob {
+ task_rect: target_rect.to_f32(),
+ local_size: info.local_size,
+ style: info.style as i32,
+ axis_select: match info.orientation {
+ LineOrientation::Horizontal => 0.0,
+ LineOrientation::Vertical => 1.0,
+ },
+ wavy_line_thickness: info.wavy_line_thickness,
+ });
+ }
+ RenderTaskKind::HorizontalBlur(..) => {
+ add_blur_instances(
+ &mut self.horizontal_blurs,
+ BlurDirection::Horizontal,
+ task_address,
+ task.children[0],
+ render_tasks,
+ );
+ }
+ RenderTaskKind::Blit(ref task_info) => {
+ // Add a blit job to copy from an existing render
+ // task to this target.
+ self.blits.push(BlitJob {
+ source: task_info.source,
+ target_rect,
+ });
+ }
+ RenderTaskKind::Border(ref task_info) => {
+ self.clears.push(target_rect);
+
+ let task_origin = target_rect.min.to_f32();
+ // TODO(gw): Clone here instead of a move of this vec, since the frame
+ // graph is immutable by this point. It's rare that borders
+ // are drawn since they are persisted in the texture cache,
+ // but perhaps this could be improved in future.
+ let instances = task_info.instances.clone();
+ for mut instance in instances {
+ // TODO(gw): It may be better to store the task origin in
+ // the render task data instead of per instance.
+ instance.task_origin = task_origin;
+ if instance.flags & STYLE_MASK == STYLE_SOLID {
+ self.border_segments_solid.push(instance);
+ } else {
+ self.border_segments_complex.push(instance);
+ }
+ }
+ }
+ RenderTaskKind::FastLinearGradient(ref task_info) => {
+ self.fast_linear_gradients.push(task_info.to_instance(&target_rect));
+ }
+ RenderTaskKind::LinearGradient(ref task_info) => {
+ self.linear_gradients.push(task_info.to_instance(&target_rect));
+ }
+ RenderTaskKind::RadialGradient(ref task_info) => {
+ self.radial_gradients.push(task_info.to_instance(&target_rect));
+ }
+ RenderTaskKind::ConicGradient(ref task_info) => {
+ self.conic_gradients.push(task_info.to_instance(&target_rect));
+ }
+ RenderTaskKind::Prim(..) |
+ RenderTaskKind::Image(..) |
+ RenderTaskKind::Cached(..) |
+ RenderTaskKind::VerticalBlur(..) |
+ RenderTaskKind::Picture(..) |
+ RenderTaskKind::ClipRegion(..) |
+ RenderTaskKind::CacheMask(..) |
+ RenderTaskKind::Readback(..) |
+ RenderTaskKind::Scaling(..) |
+ RenderTaskKind::TileComposite(..) |
+ RenderTaskKind::SvgFilter(..) => {
+ panic!("BUG: unexpected task kind for texture cache target");
+ }
+ #[cfg(test)]
+ RenderTaskKind::Test(..) => {}
+ }
+ }
+}
+
+fn add_blur_instances(
+ instances: &mut FastHashMap<TextureSource, Vec<BlurInstance>>,
+ blur_direction: BlurDirection,
+ task_address: RenderTaskAddress,
+ src_task_id: RenderTaskId,
+ render_tasks: &RenderTaskGraph,
+) {
+ let source = render_tasks[src_task_id].get_texture_source();
+
+ let instance = BlurInstance {
+ task_address,
+ src_task_address: src_task_id.into(),
+ blur_direction,
+ };
+
+ instances
+ .entry(source)
+ .or_insert(Vec::new())
+ .push(instance);
+}
+
+fn add_scaling_instances(
+ task: &ScalingTask,
+ instances: &mut FastHashMap<TextureSource, Vec<ScalingInstance>>,
+ target_task: &RenderTask,
+ source_task: Option<&RenderTask>,
+) {
+ let target_rect = target_task
+ .get_target_rect()
+ .inner_box(task.padding)
+ .to_f32();
+
+ let source = source_task.unwrap().get_texture_source();
+
+ let source_rect = source_task.unwrap().get_target_rect().to_f32();
+
+ instances
+ .entry(source)
+ .or_insert(Vec::new())
+ .push(ScalingInstance {
+ target_rect,
+ source_rect,
+ });
+}
+
+fn build_mask_tasks<F>(
+ clips_range: ClipNodeRange,
+ prim_spatial_node_index: SpatialNodeIndex,
+ clip_store: &ClipStore,
+ data_stores: &DataStores,
+ spatial_tree: &SpatialTree,
+ gpu_buffer_builder: &mut GpuBufferBuilder,
+ transforms: &mut TransformPalette,
+ mut f: F,
+) where F: FnMut(bool, GpuBufferAddress, TransformPaletteId) {
+ for i in 0 .. clips_range.count {
+ let clip_instance = clip_store.get_instance_from_range(&clips_range, i);
+ let clip_node = &data_stores.clip[clip_instance.handle];
+
+ // TODO(gw): We know that the prim <-> clip mapping is 2d in this initial patch
+ // set, due to the checks in `can_use_clip_chain_for_quad_path`. The
+ // next set of patches needs to account for perspective here in how
+ // we draw the mask.
+
+ let clip_transform_id = transforms.get_id(
+ prim_spatial_node_index,
+ clip_node.item.spatial_node_index,
+ spatial_tree,
+ );
+
+ match clip_node.item.kind {
+ ClipItemKind::RoundedRectangle { rect, radius, mode } => {
+ let (fast_path, clip_address) = if radius.is_uniform().is_some() {
+ let mut writer = gpu_buffer_builder.write_blocks(3);
+ writer.push_one(rect);
+ writer.push_one([radius.top_left.width, 0.0, 0.0, 0.0]);
+ writer.push_one([mode as i32 as f32, 0.0, 0.0, 0.0]);
+ let clip_address = writer.finish();
+
+ (true, clip_address)
+ } else {
+ let mut writer = gpu_buffer_builder.write_blocks(4);
+ writer.push_one(rect);
+ writer.push_one([
+ radius.top_left.width,
+ radius.top_left.height,
+ radius.top_right.width,
+ radius.top_right.height,
+ ]);
+ writer.push_one([
+ radius.bottom_left.width,
+ radius.bottom_left.height,
+ radius.bottom_right.width,
+ radius.bottom_right.height,
+ ]);
+ writer.push_one([mode as i32 as f32, 0.0, 0.0, 0.0]);
+ let clip_address = writer.finish();
+
+ (false, clip_address)
+ };
+
+ f(fast_path, clip_address, clip_transform_id);
+ }
+ ClipItemKind::Rectangle { rect, mode: ClipMode::ClipOut, .. } => {
+ let mut writer = gpu_buffer_builder.write_blocks(3);
+ writer.push_one(rect);
+ writer.push_one([0.0, 0.0, 0.0, 0.0]);
+ writer.push_one([ClipMode::ClipOut as i32 as f32, 0.0, 0.0, 0.0]);
+ let clip_address = writer.finish();
+
+ f(true, clip_address, clip_transform_id);
+ }
+ ClipItemKind::Rectangle { mode: ClipMode::Clip, .. } => {
+ // Handled by local clip rect in vertex shader
+ }
+ ClipItemKind::BoxShadow { .. } => {
+ panic!("bug: box-shadow clips not expected on non-legacy rect/quads");
+ }
+ ClipItemKind::Image { .. } => {
+ panic!("bug: image-masks not expected on rect/quads");
+ }
+ }
+ }
+}
+
+fn add_svg_filter_instances(
+ instances: &mut Vec<(BatchTextures, Vec<SvgFilterInstance>)>,
+ render_tasks: &RenderTaskGraph,
+ filter: &SvgFilterInfo,
+ task_id: RenderTaskId,
+ input_1_task: Option<RenderTaskId>,
+ input_2_task: Option<RenderTaskId>,
+ extra_data_address: Option<GpuCacheAddress>,
+) {
+ let mut textures = BatchTextures::empty();
+
+ if let Some(id) = input_1_task {
+ textures.input.colors[0] = render_tasks[id].get_texture_source();
+ }
+
+ if let Some(id) = input_2_task {
+ textures.input.colors[1] = render_tasks[id].get_texture_source();
+ }
+
+ let kind = match filter {
+ SvgFilterInfo::Blend(..) => 0,
+ SvgFilterInfo::Flood(..) => 1,
+ SvgFilterInfo::LinearToSrgb => 2,
+ SvgFilterInfo::SrgbToLinear => 3,
+ SvgFilterInfo::Opacity(..) => 4,
+ SvgFilterInfo::ColorMatrix(..) => 5,
+ SvgFilterInfo::DropShadow(..) => 6,
+ SvgFilterInfo::Offset(..) => 7,
+ SvgFilterInfo::ComponentTransfer(..) => 8,
+ SvgFilterInfo::Identity => 9,
+ SvgFilterInfo::Composite(..) => 10,
+ };
+
+ let input_count = match filter {
+ SvgFilterInfo::Flood(..) => 0,
+
+ SvgFilterInfo::LinearToSrgb |
+ SvgFilterInfo::SrgbToLinear |
+ SvgFilterInfo::Opacity(..) |
+ SvgFilterInfo::ColorMatrix(..) |
+ SvgFilterInfo::Offset(..) |
+ SvgFilterInfo::ComponentTransfer(..) |
+ SvgFilterInfo::Identity => 1,
+
+ // Not techincally a 2 input filter, but we have 2 inputs here: original content & blurred content.
+ SvgFilterInfo::DropShadow(..) |
+ SvgFilterInfo::Blend(..) |
+ SvgFilterInfo::Composite(..) => 2,
+ };
+
+ let generic_int = match filter {
+ SvgFilterInfo::Blend(mode) => *mode as u16,
+ SvgFilterInfo::ComponentTransfer(data) =>
+ (data.r_func.to_int() << 12 |
+ data.g_func.to_int() << 8 |
+ data.b_func.to_int() << 4 |
+ data.a_func.to_int()) as u16,
+ SvgFilterInfo::Composite(operator) =>
+ operator.as_int() as u16,
+ SvgFilterInfo::LinearToSrgb |
+ SvgFilterInfo::SrgbToLinear |
+ SvgFilterInfo::Flood(..) |
+ SvgFilterInfo::Opacity(..) |
+ SvgFilterInfo::ColorMatrix(..) |
+ SvgFilterInfo::DropShadow(..) |
+ SvgFilterInfo::Offset(..) |
+ SvgFilterInfo::Identity => 0,
+ };
+
+ let instance = SvgFilterInstance {
+ task_address: task_id.into(),
+ input_1_task_address: input_1_task.map(|id| id.into()).unwrap_or(RenderTaskAddress(0)),
+ input_2_task_address: input_2_task.map(|id| id.into()).unwrap_or(RenderTaskAddress(0)),
+ kind,
+ input_count,
+ generic_int,
+ extra_data_address: extra_data_address.unwrap_or(GpuCacheAddress::INVALID),
+ };
+
+ for (ref mut batch_textures, ref mut batch) in instances.iter_mut() {
+ if let Some(combined_textures) = batch_textures.combine_textures(textures) {
+ batch.push(instance);
+ // Update the batch textures to the newly combined batch textures
+ *batch_textures = combined_textures;
+ return;
+ }
+ }
+
+ instances.push((textures, vec![instance]));
+}
+
+// Information required to do a blit from a source to a target.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BlitJob {
+ pub source: RenderTaskId,
+ pub target_rect: DeviceIntRect,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Clone, Debug)]
+pub struct LineDecorationJob {
+ pub task_rect: DeviceRect,
+ pub local_size: LayoutSize,
+ pub wavy_line_thickness: f32,
+ pub style: i32,
+ pub axis_select: f32,
+}
diff --git a/gfx/wr/webrender/src/render_task.rs b/gfx/wr/webrender/src/render_task.rs
new file mode 100644
index 0000000000..51fa71f943
--- /dev/null
+++ b/gfx/wr/webrender/src/render_task.rs
@@ -0,0 +1,1560 @@
+/* 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 api::{CompositeOperator, FilterPrimitive, FilterPrimitiveInput, FilterPrimitiveKind};
+use api::{LineStyle, LineOrientation, ClipMode, MixBlendMode, ColorF, ColorSpace};
+use api::MAX_RENDER_TASK_SIZE;
+use api::units::*;
+use crate::clip::{ClipDataStore, ClipItemKind, ClipStore, ClipNodeRange};
+use crate::command_buffer::{CommandBufferIndex, QuadFlags};
+use crate::spatial_tree::SpatialNodeIndex;
+use crate::filterdata::SFilterData;
+use crate::frame_builder::{FrameBuilderConfig};
+use crate::gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
+use crate::gpu_types::{BorderInstance, ImageSource, UvRectKind, TransformPaletteId};
+use crate::internal_types::{CacheTextureId, FastHashMap, TextureSource, Swizzle};
+use crate::picture::ResolvedSurfaceTexture;
+use crate::prim_store::ClipData;
+use crate::prim_store::gradient::{
+ FastLinearGradientTask, RadialGradientTask,
+ ConicGradientTask, LinearGradientTask,
+};
+use crate::resource_cache::{ResourceCache, ImageRequest};
+use std::{usize, f32, i32, u32};
+use crate::renderer::{GpuBufferAddress, GpuBufferBuilder};
+use crate::render_target::{ResolveOp, RenderTargetKind};
+use crate::render_task_graph::{PassId, RenderTaskId, RenderTaskGraphBuilder};
+use crate::render_task_cache::{RenderTaskCacheEntryHandle, RenderTaskCacheKey, RenderTaskCacheKeyKind, RenderTaskParent};
+use crate::segment::EdgeAaSegmentMask;
+use crate::surface::SurfaceBuilder;
+use smallvec::SmallVec;
+
+const FLOATS_PER_RENDER_TASK_INFO: usize = 8;
+pub const MAX_BLUR_STD_DEVIATION: f32 = 4.0;
+pub const MIN_DOWNSCALING_RT_SIZE: i32 = 8;
+
+fn render_task_sanity_check(size: &DeviceIntSize) {
+ if size.width > MAX_RENDER_TASK_SIZE ||
+ size.height > MAX_RENDER_TASK_SIZE {
+ error!("Attempting to create a render task of size {}x{}", size.width, size.height);
+ panic!();
+ }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RenderTaskAddress(pub u16);
+
+impl Into<RenderTaskAddress> for RenderTaskId {
+ fn into(self) -> RenderTaskAddress {
+ RenderTaskAddress(self.index as u16)
+ }
+}
+
+/// A render task location that targets a persistent output buffer which
+/// will be retained over multiple frames.
+#[derive(Clone, Debug, Eq, PartialEq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum StaticRenderTaskSurface {
+ /// The output of the `RenderTask` will be persisted beyond this frame, and
+ /// thus should be drawn into the `TextureCache`.
+ TextureCache {
+ /// Which texture in the texture cache should be drawn into.
+ texture: CacheTextureId,
+ /// What format this texture cache surface is
+ target_kind: RenderTargetKind,
+ },
+ /// Only used as a source for render tasks, can be any texture including an
+ /// external one.
+ ReadOnly {
+ source: TextureSource,
+ },
+ /// This render task will be drawn to a picture cache texture that is
+ /// persisted between both frames and scenes, if the content remains valid.
+ PictureCache {
+ /// Describes either a WR texture or a native OS compositor target
+ surface: ResolvedSurfaceTexture,
+ },
+}
+
+/// Identifies the output buffer location for a given `RenderTask`.
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum RenderTaskLocation {
+ // Towards the beginning of the frame, most task locations are typically not
+ // known yet, in which case they are set to one of the following variants:
+
+ /// A dynamic task that has not yet been allocated a texture and rect.
+ Unallocated {
+ /// Requested size of this render task
+ size: DeviceIntSize,
+ },
+ /// Will be replaced by a Static location after the texture cache update.
+ CacheRequest {
+ size: DeviceIntSize,
+ },
+ /// Same allocation as an existing task deeper in the dependency graph
+ Existing {
+ parent_task_id: RenderTaskId,
+ /// Requested size of this render task
+ size: DeviceIntSize,
+ },
+
+ // Before batching begins, we expect that locations have been resolved to
+ // one of the following variants:
+
+ /// The `RenderTask` should be drawn to a target provided by the atlas
+ /// allocator. This is the most common case.
+ Dynamic {
+ /// Texture that this task was allocated to render on
+ texture_id: CacheTextureId,
+ /// Rectangle in the texture this task occupies
+ rect: DeviceIntRect,
+ },
+ /// A task that is output to a persistent / retained target.
+ Static {
+ /// Target to draw to
+ surface: StaticRenderTaskSurface,
+ /// Rectangle in the texture this task occupies
+ rect: DeviceIntRect,
+ },
+}
+
+impl RenderTaskLocation {
+ /// Returns true if this is a dynamic location.
+ pub fn is_dynamic(&self) -> bool {
+ match *self {
+ RenderTaskLocation::Dynamic { .. } => true,
+ _ => false,
+ }
+ }
+
+ pub fn size(&self) -> DeviceIntSize {
+ match self {
+ RenderTaskLocation::Unallocated { size } => *size,
+ RenderTaskLocation::Dynamic { rect, .. } => rect.size(),
+ RenderTaskLocation::Static { rect, .. } => rect.size(),
+ RenderTaskLocation::CacheRequest { size } => *size,
+ RenderTaskLocation::Existing { size, .. } => *size,
+ }
+ }
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct CachedTask {
+ pub target_kind: RenderTargetKind,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct CacheMaskTask {
+ pub actual_rect: DeviceRect,
+ pub root_spatial_node_index: SpatialNodeIndex,
+ pub clip_node_range: ClipNodeRange,
+ pub device_pixel_scale: DevicePixelScale,
+ pub clear_to_one: bool,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ClipRegionTask {
+ pub local_pos: LayoutPoint,
+ pub device_pixel_scale: DevicePixelScale,
+ pub clip_data: ClipData,
+ pub clear_to_one: bool,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PrimTask {
+ pub device_pixel_scale: DevicePixelScale,
+ pub content_origin: DevicePoint,
+ pub prim_address: GpuBufferAddress,
+ pub prim_spatial_node_index: SpatialNodeIndex,
+ pub transform_id: TransformPaletteId,
+ pub edge_flags: EdgeAaSegmentMask,
+ pub quad_flags: QuadFlags,
+ pub clip_node_range: ClipNodeRange,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct TileCompositeTask {
+ pub clear_color: ColorF,
+ pub scissor_rect: DeviceIntRect,
+ pub valid_rect: DeviceIntRect,
+ pub task_id: Option<RenderTaskId>,
+ pub sub_rect_offset: DeviceIntVector2D,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PictureTask {
+ pub can_merge: bool,
+ pub content_origin: DevicePoint,
+ pub surface_spatial_node_index: SpatialNodeIndex,
+ pub raster_spatial_node_index: SpatialNodeIndex,
+ pub device_pixel_scale: DevicePixelScale,
+ pub clear_color: Option<ColorF>,
+ pub scissor_rect: Option<DeviceIntRect>,
+ pub valid_rect: Option<DeviceIntRect>,
+ pub cmd_buffer_index: CommandBufferIndex,
+ pub resolve_op: Option<ResolveOp>,
+
+ pub can_use_shared_surface: bool,
+}
+
+impl PictureTask {
+ /// Copy an existing picture task, but set a new command buffer for it to build in to.
+ /// Used for pictures that are split between render tasks (e.g. pre/post a backdrop
+ /// filter). Subsequent picture tasks never have a clear color as they are by definition
+ /// going to write to an existing target
+ pub fn duplicate(
+ &self,
+ cmd_buffer_index: CommandBufferIndex,
+ ) -> Self {
+ assert_eq!(self.resolve_op, None);
+
+ PictureTask {
+ clear_color: None,
+ cmd_buffer_index,
+ resolve_op: None,
+ can_use_shared_surface: false,
+ ..*self
+ }
+ }
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BlurTask {
+ pub blur_std_deviation: f32,
+ pub target_kind: RenderTargetKind,
+ pub blur_region: DeviceIntSize,
+}
+
+impl BlurTask {
+ // In order to do the blur down-scaling passes without introducing errors, we need the
+ // source of each down-scale pass to be a multuple of two. If need be, this inflates
+ // the source size so that each down-scale pass will sample correctly.
+ pub fn adjusted_blur_source_size(original_size: DeviceSize, mut std_dev: DeviceSize) -> DeviceIntSize {
+ let mut adjusted_size = original_size;
+ let mut scale_factor = 1.0;
+ while std_dev.width > MAX_BLUR_STD_DEVIATION && std_dev.height > MAX_BLUR_STD_DEVIATION {
+ if adjusted_size.width < MIN_DOWNSCALING_RT_SIZE as f32 ||
+ adjusted_size.height < MIN_DOWNSCALING_RT_SIZE as f32 {
+ break;
+ }
+ std_dev = std_dev * 0.5;
+ scale_factor *= 2.0;
+ adjusted_size = (original_size.to_f32() / scale_factor).ceil();
+ }
+
+ (adjusted_size * scale_factor).round().to_i32()
+ }
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ScalingTask {
+ pub target_kind: RenderTargetKind,
+ pub padding: DeviceIntSideOffsets,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BorderTask {
+ pub instances: Vec<BorderInstance>,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BlitTask {
+ pub source: RenderTaskId,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct LineDecorationTask {
+ pub wavy_line_thickness: f32,
+ pub style: LineStyle,
+ pub orientation: LineOrientation,
+ pub local_size: LayoutSize,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum SvgFilterInfo {
+ Blend(MixBlendMode),
+ Flood(ColorF),
+ LinearToSrgb,
+ SrgbToLinear,
+ Opacity(f32),
+ ColorMatrix(Box<[f32; 20]>),
+ DropShadow(ColorF),
+ Offset(DeviceVector2D),
+ ComponentTransfer(SFilterData),
+ Composite(CompositeOperator),
+ // TODO: This is used as a hack to ensure that a blur task's input is always in the blur's previous pass.
+ Identity,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SvgFilterTask {
+ pub info: SvgFilterInfo,
+ pub extra_gpu_cache_handle: Option<GpuCacheHandle>,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ReadbackTask {
+ // The offset of the rect that needs to be read back, in the
+ // device space of the surface that will be read back from.
+ // If this is None, there is no readback surface available
+ // and this is a dummy (empty) readback.
+ pub readback_origin: Option<DevicePoint>,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RenderTaskData {
+ pub data: [f32; FLOATS_PER_RENDER_TASK_INFO],
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum RenderTaskKind {
+ Image(ImageRequest),
+ Cached(CachedTask),
+ Picture(PictureTask),
+ CacheMask(CacheMaskTask),
+ ClipRegion(ClipRegionTask),
+ VerticalBlur(BlurTask),
+ HorizontalBlur(BlurTask),
+ Readback(ReadbackTask),
+ Scaling(ScalingTask),
+ Blit(BlitTask),
+ Border(BorderTask),
+ LineDecoration(LineDecorationTask),
+ FastLinearGradient(FastLinearGradientTask),
+ LinearGradient(LinearGradientTask),
+ RadialGradient(RadialGradientTask),
+ ConicGradient(ConicGradientTask),
+ SvgFilter(SvgFilterTask),
+ TileComposite(TileCompositeTask),
+ Prim(PrimTask),
+ #[cfg(test)]
+ Test(RenderTargetKind),
+}
+
+impl RenderTaskKind {
+ pub fn is_a_rendering_operation(&self) -> bool {
+ match self {
+ &RenderTaskKind::Image(..) => false,
+ &RenderTaskKind::Cached(..) => false,
+ _ => true,
+ }
+ }
+
+ /// Whether this task can be allocated on a shared render target surface
+ pub fn can_use_shared_surface(&self) -> bool {
+ match self {
+ &RenderTaskKind::Picture(ref info) => info.can_use_shared_surface,
+ _ => true,
+ }
+ }
+
+ pub fn should_advance_pass(&self) -> bool {
+ match self {
+ &RenderTaskKind::Image(..) => false,
+ &RenderTaskKind::Cached(..) => false,
+ _ => true,
+ }
+ }
+
+ pub fn as_str(&self) -> &'static str {
+ match *self {
+ RenderTaskKind::Image(..) => "Image",
+ RenderTaskKind::Cached(..) => "Cached",
+ RenderTaskKind::Picture(..) => "Picture",
+ RenderTaskKind::CacheMask(..) => "CacheMask",
+ RenderTaskKind::ClipRegion(..) => "ClipRegion",
+ RenderTaskKind::VerticalBlur(..) => "VerticalBlur",
+ RenderTaskKind::HorizontalBlur(..) => "HorizontalBlur",
+ RenderTaskKind::Readback(..) => "Readback",
+ RenderTaskKind::Scaling(..) => "Scaling",
+ RenderTaskKind::Blit(..) => "Blit",
+ RenderTaskKind::Border(..) => "Border",
+ RenderTaskKind::LineDecoration(..) => "LineDecoration",
+ RenderTaskKind::FastLinearGradient(..) => "FastLinearGradient",
+ RenderTaskKind::LinearGradient(..) => "LinearGradient",
+ RenderTaskKind::RadialGradient(..) => "RadialGradient",
+ RenderTaskKind::ConicGradient(..) => "ConicGradient",
+ RenderTaskKind::SvgFilter(..) => "SvgFilter",
+ RenderTaskKind::TileComposite(..) => "TileComposite",
+ RenderTaskKind::Prim(..) => "Prim",
+ #[cfg(test)]
+ RenderTaskKind::Test(..) => "Test",
+ }
+ }
+
+ pub fn target_kind(&self) -> RenderTargetKind {
+ match *self {
+ RenderTaskKind::Image(..) |
+ RenderTaskKind::LineDecoration(..) |
+ RenderTaskKind::Readback(..) |
+ RenderTaskKind::Border(..) |
+ RenderTaskKind::FastLinearGradient(..) |
+ RenderTaskKind::LinearGradient(..) |
+ RenderTaskKind::RadialGradient(..) |
+ RenderTaskKind::ConicGradient(..) |
+ RenderTaskKind::Picture(..) |
+ RenderTaskKind::Blit(..) |
+ RenderTaskKind::TileComposite(..) |
+ RenderTaskKind::Prim(..) |
+ RenderTaskKind::SvgFilter(..) => {
+ RenderTargetKind::Color
+ }
+
+ RenderTaskKind::ClipRegion(..) |
+ RenderTaskKind::CacheMask(..) => {
+ RenderTargetKind::Alpha
+ }
+
+ RenderTaskKind::VerticalBlur(ref task_info) |
+ RenderTaskKind::HorizontalBlur(ref task_info) => {
+ task_info.target_kind
+ }
+
+ RenderTaskKind::Scaling(ref task_info) => {
+ task_info.target_kind
+ }
+
+ RenderTaskKind::Cached(ref task_info) => {
+ task_info.target_kind
+ }
+
+ #[cfg(test)]
+ RenderTaskKind::Test(kind) => kind,
+ }
+ }
+
+ pub fn new_tile_composite(
+ sub_rect_offset: DeviceIntVector2D,
+ scissor_rect: DeviceIntRect,
+ valid_rect: DeviceIntRect,
+ clear_color: ColorF,
+ ) -> Self {
+ RenderTaskKind::TileComposite(TileCompositeTask {
+ task_id: None,
+ sub_rect_offset,
+ scissor_rect,
+ valid_rect,
+ clear_color,
+ })
+ }
+
+ pub fn new_picture(
+ size: DeviceIntSize,
+ needs_scissor_rect: bool,
+ content_origin: DevicePoint,
+ surface_spatial_node_index: SpatialNodeIndex,
+ raster_spatial_node_index: SpatialNodeIndex,
+ device_pixel_scale: DevicePixelScale,
+ scissor_rect: Option<DeviceIntRect>,
+ valid_rect: Option<DeviceIntRect>,
+ clear_color: Option<ColorF>,
+ cmd_buffer_index: CommandBufferIndex,
+ can_use_shared_surface: bool,
+ ) -> Self {
+ render_task_sanity_check(&size);
+
+ RenderTaskKind::Picture(PictureTask {
+ content_origin,
+ can_merge: !needs_scissor_rect,
+ surface_spatial_node_index,
+ raster_spatial_node_index,
+ device_pixel_scale,
+ scissor_rect,
+ valid_rect,
+ clear_color,
+ cmd_buffer_index,
+ resolve_op: None,
+ can_use_shared_surface,
+ })
+ }
+
+ pub fn new_prim(
+ prim_spatial_node_index: SpatialNodeIndex,
+ device_pixel_scale: DevicePixelScale,
+ content_origin: DevicePoint,
+ prim_address: GpuBufferAddress,
+ transform_id: TransformPaletteId,
+ edge_flags: EdgeAaSegmentMask,
+ quad_flags: QuadFlags,
+ clip_node_range: ClipNodeRange,
+ ) -> Self {
+ RenderTaskKind::Prim(PrimTask {
+ prim_spatial_node_index,
+ device_pixel_scale,
+ content_origin,
+ prim_address,
+ transform_id,
+ edge_flags,
+ quad_flags,
+ clip_node_range,
+ })
+ }
+
+ pub fn new_readback(
+ readback_origin: Option<DevicePoint>,
+ ) -> Self {
+ RenderTaskKind::Readback(
+ ReadbackTask {
+ readback_origin,
+ }
+ )
+ }
+
+ pub fn new_line_decoration(
+ style: LineStyle,
+ orientation: LineOrientation,
+ wavy_line_thickness: f32,
+ local_size: LayoutSize,
+ ) -> Self {
+ RenderTaskKind::LineDecoration(LineDecorationTask {
+ style,
+ orientation,
+ wavy_line_thickness,
+ local_size,
+ })
+ }
+
+ pub fn new_border_segment(
+ instances: Vec<BorderInstance>,
+ ) -> Self {
+ RenderTaskKind::Border(BorderTask {
+ instances,
+ })
+ }
+
+ pub fn new_rounded_rect_mask(
+ local_pos: LayoutPoint,
+ clip_data: ClipData,
+ device_pixel_scale: DevicePixelScale,
+ fb_config: &FrameBuilderConfig,
+ ) -> Self {
+ RenderTaskKind::ClipRegion(ClipRegionTask {
+ local_pos,
+ device_pixel_scale,
+ clip_data,
+ clear_to_one: fb_config.gpu_supports_fast_clears,
+ })
+ }
+
+ pub fn new_mask(
+ outer_rect: DeviceRect,
+ clip_node_range: ClipNodeRange,
+ root_spatial_node_index: SpatialNodeIndex,
+ clip_store: &mut ClipStore,
+ gpu_cache: &mut GpuCache,
+ gpu_buffer_builder: &mut GpuBufferBuilder,
+ resource_cache: &mut ResourceCache,
+ rg_builder: &mut RenderTaskGraphBuilder,
+ clip_data_store: &mut ClipDataStore,
+ device_pixel_scale: DevicePixelScale,
+ fb_config: &FrameBuilderConfig,
+ surface_builder: &mut SurfaceBuilder,
+ ) -> RenderTaskId {
+ // Step through the clip sources that make up this mask. If we find
+ // any box-shadow clip sources, request that image from the render
+ // task cache. This allows the blurred box-shadow rect to be cached
+ // in the texture cache across frames.
+ // TODO(gw): Consider moving this logic outside this function, especially
+ // as we add more clip sources that depend on render tasks.
+ // TODO(gw): If this ever shows up in a profile, we could pre-calculate
+ // whether a ClipSources contains any box-shadows and skip
+ // this iteration for the majority of cases.
+ let task_size = outer_rect.size().to_i32();
+
+ // If we have a potentially tiled clip mask, clear the mask area first. Otherwise,
+ // the first (primary) clip mask will overwrite all the clip mask pixels with
+ // blending disabled to set to the initial value.
+
+ let clip_task_id = rg_builder.add().init(
+ RenderTask::new_dynamic(
+ task_size,
+ RenderTaskKind::CacheMask(CacheMaskTask {
+ actual_rect: outer_rect,
+ clip_node_range,
+ root_spatial_node_index,
+ device_pixel_scale,
+ clear_to_one: fb_config.gpu_supports_fast_clears,
+ }),
+ )
+ );
+
+ for i in 0 .. clip_node_range.count {
+ let clip_instance = clip_store.get_instance_from_range(&clip_node_range, i);
+ let clip_node = &mut clip_data_store[clip_instance.handle];
+ match clip_node.item.kind {
+ ClipItemKind::BoxShadow { ref mut source } => {
+ let (cache_size, cache_key) = source.cache_key
+ .as_ref()
+ .expect("bug: no cache key set")
+ .clone();
+ let blur_radius_dp = cache_key.blur_radius_dp as f32;
+ let device_pixel_scale = DevicePixelScale::new(cache_key.device_pixel_scale.to_f32_px());
+
+ // Request a cacheable render task with a blurred, minimal
+ // sized box-shadow rect.
+ source.render_task = Some(resource_cache.request_render_task(
+ RenderTaskCacheKey {
+ size: cache_size,
+ kind: RenderTaskCacheKeyKind::BoxShadow(cache_key),
+ },
+ gpu_cache,
+ gpu_buffer_builder,
+ rg_builder,
+ None,
+ false,
+ RenderTaskParent::RenderTask(clip_task_id),
+ surface_builder,
+ |rg_builder, _| {
+ let clip_data = ClipData::rounded_rect(
+ source.minimal_shadow_rect.size(),
+ &source.shadow_radius,
+ ClipMode::Clip,
+ );
+
+ // Draw the rounded rect.
+ let mask_task_id = rg_builder.add().init(RenderTask::new_dynamic(
+ cache_size,
+ RenderTaskKind::new_rounded_rect_mask(
+ source.minimal_shadow_rect.min,
+ clip_data,
+ device_pixel_scale,
+ fb_config,
+ ),
+ ));
+
+ // Blur it
+ RenderTask::new_blur(
+ DeviceSize::new(blur_radius_dp, blur_radius_dp),
+ mask_task_id,
+ rg_builder,
+ RenderTargetKind::Alpha,
+ None,
+ cache_size,
+ )
+ }
+ ));
+ }
+ ClipItemKind::Rectangle { .. } |
+ ClipItemKind::RoundedRectangle { .. } |
+ ClipItemKind::Image { .. } => {}
+ }
+ }
+
+ clip_task_id
+ }
+
+ // Write (up to) 8 floats of data specific to the type
+ // of render task that is provided to the GPU shaders
+ // via a vertex texture.
+ pub fn write_task_data(
+ &self,
+ target_rect: DeviceIntRect,
+ ) -> RenderTaskData {
+ // NOTE: The ordering and layout of these structures are
+ // required to match both the GPU structures declared
+ // in prim_shared.glsl, and also the uses in submit_batch()
+ // in renderer.rs.
+ // TODO(gw): Maybe there's a way to make this stuff a bit
+ // more type-safe. Although, it will always need
+ // to be kept in sync with the GLSL code anyway.
+
+ let data = match self {
+ RenderTaskKind::Picture(ref task) => {
+ // Note: has to match `PICTURE_TYPE_*` in shaders
+ [
+ task.device_pixel_scale.0,
+ task.content_origin.x,
+ task.content_origin.y,
+ 0.0,
+ ]
+ }
+ RenderTaskKind::Prim(ref task) => {
+ [
+ // NOTE: This must match the render task data format for Picture tasks currently
+ task.device_pixel_scale.0,
+ task.content_origin.x,
+ task.content_origin.y,
+ 0.0,
+ ]
+ }
+ RenderTaskKind::CacheMask(ref task) => {
+ [
+ task.device_pixel_scale.0,
+ task.actual_rect.min.x,
+ task.actual_rect.min.y,
+ 0.0,
+ ]
+ }
+ RenderTaskKind::ClipRegion(ref task) => {
+ [
+ task.device_pixel_scale.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ ]
+ }
+ RenderTaskKind::VerticalBlur(ref task) |
+ RenderTaskKind::HorizontalBlur(ref task) => {
+ [
+ task.blur_std_deviation,
+ task.blur_region.width as f32,
+ task.blur_region.height as f32,
+ 0.0,
+ ]
+ }
+ RenderTaskKind::Image(..) |
+ RenderTaskKind::Cached(..) |
+ RenderTaskKind::Readback(..) |
+ RenderTaskKind::Scaling(..) |
+ RenderTaskKind::Border(..) |
+ RenderTaskKind::LineDecoration(..) |
+ RenderTaskKind::FastLinearGradient(..) |
+ RenderTaskKind::LinearGradient(..) |
+ RenderTaskKind::RadialGradient(..) |
+ RenderTaskKind::ConicGradient(..) |
+ RenderTaskKind::TileComposite(..) |
+ RenderTaskKind::Blit(..) => {
+ [0.0; 4]
+ }
+
+ RenderTaskKind::SvgFilter(ref task) => {
+ match task.info {
+ SvgFilterInfo::Opacity(opacity) => [opacity, 0.0, 0.0, 0.0],
+ SvgFilterInfo::Offset(offset) => [offset.x, offset.y, 0.0, 0.0],
+ _ => [0.0; 4]
+ }
+ }
+
+ #[cfg(test)]
+ RenderTaskKind::Test(..) => {
+ [0.0; 4]
+ }
+ };
+
+ RenderTaskData {
+ data: [
+ target_rect.min.x as f32,
+ target_rect.min.y as f32,
+ target_rect.max.x as f32,
+ target_rect.max.y as f32,
+ data[0],
+ data[1],
+ data[2],
+ data[3],
+ ]
+ }
+ }
+
+ pub fn write_gpu_blocks(
+ &mut self,
+ gpu_cache: &mut GpuCache,
+ ) {
+ if let RenderTaskKind::SvgFilter(ref mut filter_task) = self {
+ match filter_task.info {
+ SvgFilterInfo::ColorMatrix(ref matrix) => {
+ let handle = filter_task.extra_gpu_cache_handle.get_or_insert_with(GpuCacheHandle::new);
+ if let Some(mut request) = gpu_cache.request(handle) {
+ for i in 0..5 {
+ request.push([matrix[i*4], matrix[i*4+1], matrix[i*4+2], matrix[i*4+3]]);
+ }
+ }
+ }
+ SvgFilterInfo::DropShadow(color) |
+ SvgFilterInfo::Flood(color) => {
+ let handle = filter_task.extra_gpu_cache_handle.get_or_insert_with(GpuCacheHandle::new);
+ if let Some(mut request) = gpu_cache.request(handle) {
+ request.push(color.to_array());
+ }
+ }
+ SvgFilterInfo::ComponentTransfer(ref data) => {
+ let handle = filter_task.extra_gpu_cache_handle.get_or_insert_with(GpuCacheHandle::new);
+ if let Some(request) = gpu_cache.request(handle) {
+ data.update(request);
+ }
+ }
+ SvgFilterInfo::Composite(ref operator) => {
+ if let CompositeOperator::Arithmetic(k_vals) = operator {
+ let handle = filter_task.extra_gpu_cache_handle.get_or_insert_with(GpuCacheHandle::new);
+ if let Some(mut request) = gpu_cache.request(handle) {
+ request.push(*k_vals);
+ }
+ }
+ }
+ _ => {},
+ }
+ }
+ }
+}
+
+/// In order to avoid duplicating the down-scaling and blur passes when a picture has several blurs,
+/// we use a local (primitive-level) cache of the render tasks generated for a single shadowed primitive
+/// in a single frame.
+pub type BlurTaskCache = FastHashMap<BlurTaskKey, RenderTaskId>;
+
+/// Since we only use it within a single primitive, the key only needs to contain the down-scaling level
+/// and the blur std deviation.
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
+pub enum BlurTaskKey {
+ DownScale(u32),
+ Blur { downscale_level: u32, stddev_x: u32, stddev_y: u32 },
+}
+
+impl BlurTaskKey {
+ fn downscale_and_blur(downscale_level: u32, blur_stddev: DeviceSize) -> Self {
+ // Quantise the std deviations and store it as integers to work around
+ // Eq and Hash's f32 allergy.
+ // The blur radius is rounded before RenderTask::new_blur so we don't need
+ // a lot of precision.
+ const QUANTIZATION_FACTOR: f32 = 1024.0;
+ let stddev_x = (blur_stddev.width * QUANTIZATION_FACTOR) as u32;
+ let stddev_y = (blur_stddev.height * QUANTIZATION_FACTOR) as u32;
+ BlurTaskKey::Blur { downscale_level, stddev_x, stddev_y }
+ }
+}
+
+// The majority of render tasks have 0, 1 or 2 dependencies, except for pictures that
+// typically have dozens to hundreds of dependencies. SmallVec with 2 inline elements
+// avoids many tiny heap allocations in pages with a lot of text shadows and other
+// types of render tasks.
+pub type TaskDependencies = SmallVec<[RenderTaskId;2]>;
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RenderTask {
+ pub location: RenderTaskLocation,
+ pub children: TaskDependencies,
+ pub kind: RenderTaskKind,
+
+ // TODO(gw): These fields and perhaps others can become private once the
+ // frame_graph / render_task source files are unified / cleaned up.
+ pub free_after: PassId,
+ pub render_on: PassId,
+
+ /// The gpu cache handle for the render task's destination rect.
+ ///
+ /// Will be set to None if the render task is cached, in which case the texture cache
+ /// manages the handle.
+ pub uv_rect_handle: GpuCacheHandle,
+ pub cache_handle: Option<RenderTaskCacheEntryHandle>,
+ uv_rect_kind: UvRectKind,
+}
+
+impl RenderTask {
+ pub fn new(
+ location: RenderTaskLocation,
+ kind: RenderTaskKind,
+ ) -> Self {
+ render_task_sanity_check(&location.size());
+
+ RenderTask {
+ location,
+ children: TaskDependencies::new(),
+ kind,
+ free_after: PassId::MAX,
+ render_on: PassId::MIN,
+ uv_rect_handle: GpuCacheHandle::new(),
+ uv_rect_kind: UvRectKind::Rect,
+ cache_handle: None,
+ }
+ }
+
+ pub fn new_dynamic(
+ size: DeviceIntSize,
+ kind: RenderTaskKind,
+ ) -> Self {
+ RenderTask::new(
+ RenderTaskLocation::Unallocated { size },
+ kind,
+ )
+ }
+
+ pub fn with_uv_rect_kind(mut self, uv_rect_kind: UvRectKind) -> Self {
+ self.uv_rect_kind = uv_rect_kind;
+ self
+ }
+
+ pub fn new_image(
+ size: DeviceIntSize,
+ request: ImageRequest,
+ ) -> Self {
+ // Note: this is a special constructor for image render tasks that does not
+ // do the render task size sanity check. This is because with SWGL we purposefully
+ // avoid tiling large images. There is no upload with SWGL so whatever was
+ // successfully allocated earlier will be what shaders read, regardless of the size
+ // and copying into tiles would only slow things down.
+ // As a result we can run into very large images being added to the frame graph
+ // (this is covered by a few reftests on the CI).
+
+ RenderTask {
+ location: RenderTaskLocation::CacheRequest { size, },
+ children: TaskDependencies::new(),
+ kind: RenderTaskKind::Image(request),
+ free_after: PassId::MAX,
+ render_on: PassId::MIN,
+ uv_rect_handle: GpuCacheHandle::new(),
+ uv_rect_kind: UvRectKind::Rect,
+ cache_handle: None,
+ }
+ }
+
+
+ #[cfg(test)]
+ pub fn new_test(
+ location: RenderTaskLocation,
+ target: RenderTargetKind,
+ ) -> Self {
+ RenderTask {
+ location,
+ children: TaskDependencies::new(),
+ kind: RenderTaskKind::Test(target),
+ free_after: PassId::MAX,
+ render_on: PassId::MIN,
+ uv_rect_handle: GpuCacheHandle::new(),
+ uv_rect_kind: UvRectKind::Rect,
+ cache_handle: None,
+ }
+ }
+
+ pub fn new_blit(
+ size: DeviceIntSize,
+ source: RenderTaskId,
+ rg_builder: &mut RenderTaskGraphBuilder,
+ ) -> RenderTaskId {
+ // If this blit uses a render task as a source,
+ // ensure it's added as a child task. This will
+ // ensure it gets allocated in the correct pass
+ // and made available as an input when this task
+ // executes.
+
+ let blit_task_id = rg_builder.add().init(RenderTask::new_dynamic(
+ size,
+ RenderTaskKind::Blit(BlitTask { source }),
+ ));
+
+ rg_builder.add_dependency(blit_task_id, source);
+
+ blit_task_id
+ }
+
+ // Construct a render task to apply a blur to a primitive.
+ // The render task chain that is constructed looks like:
+ //
+ // PrimitiveCacheTask: Draw the primitives.
+ // ^
+ // |
+ // DownscalingTask(s): Each downscaling task reduces the size of render target to
+ // ^ half. Also reduce the std deviation to half until the std
+ // | deviation less than 4.0.
+ // |
+ // |
+ // VerticalBlurTask: Apply the separable vertical blur to the primitive.
+ // ^
+ // |
+ // HorizontalBlurTask: Apply the separable horizontal blur to the vertical blur.
+ // |
+ // +---- This is stored as the input task to the primitive shader.
+ //
+ pub fn new_blur(
+ blur_std_deviation: DeviceSize,
+ src_task_id: RenderTaskId,
+ rg_builder: &mut RenderTaskGraphBuilder,
+ target_kind: RenderTargetKind,
+ mut blur_cache: Option<&mut BlurTaskCache>,
+ blur_region: DeviceIntSize,
+ ) -> RenderTaskId {
+ // Adjust large std deviation value.
+ let mut adjusted_blur_std_deviation = blur_std_deviation;
+ let (blur_target_size, uv_rect_kind) = {
+ let src_task = rg_builder.get_task(src_task_id);
+ (src_task.location.size(), src_task.uv_rect_kind())
+ };
+ let mut adjusted_blur_target_size = blur_target_size;
+ let mut downscaling_src_task_id = src_task_id;
+ let mut scale_factor = 1.0;
+ let mut n_downscales = 1;
+ while adjusted_blur_std_deviation.width > MAX_BLUR_STD_DEVIATION &&
+ adjusted_blur_std_deviation.height > MAX_BLUR_STD_DEVIATION {
+ if adjusted_blur_target_size.width < MIN_DOWNSCALING_RT_SIZE ||
+ adjusted_blur_target_size.height < MIN_DOWNSCALING_RT_SIZE {
+ break;
+ }
+ adjusted_blur_std_deviation = adjusted_blur_std_deviation * 0.5;
+ scale_factor *= 2.0;
+ adjusted_blur_target_size = (blur_target_size.to_f32() / scale_factor).to_i32();
+
+ let cached_task = match blur_cache {
+ Some(ref mut cache) => cache.get(&BlurTaskKey::DownScale(n_downscales)).cloned(),
+ None => None,
+ };
+
+ downscaling_src_task_id = cached_task.unwrap_or_else(|| {
+ RenderTask::new_scaling(
+ downscaling_src_task_id,
+ rg_builder,
+ target_kind,
+ adjusted_blur_target_size,
+ )
+ });
+
+ if let Some(ref mut cache) = blur_cache {
+ cache.insert(BlurTaskKey::DownScale(n_downscales), downscaling_src_task_id);
+ }
+
+ n_downscales += 1;
+ }
+
+
+ let blur_key = BlurTaskKey::downscale_and_blur(n_downscales, adjusted_blur_std_deviation);
+
+ let cached_task = match blur_cache {
+ Some(ref mut cache) => cache.get(&blur_key).cloned(),
+ None => None,
+ };
+
+ let blur_region = blur_region / (scale_factor as i32);
+
+ let blur_task_id = cached_task.unwrap_or_else(|| {
+ let blur_task_v = rg_builder.add().init(RenderTask::new_dynamic(
+ adjusted_blur_target_size,
+ RenderTaskKind::VerticalBlur(BlurTask {
+ blur_std_deviation: adjusted_blur_std_deviation.height,
+ target_kind,
+ blur_region,
+ }),
+ ).with_uv_rect_kind(uv_rect_kind));
+ rg_builder.add_dependency(blur_task_v, downscaling_src_task_id);
+
+ let task_id = rg_builder.add().init(RenderTask::new_dynamic(
+ adjusted_blur_target_size,
+ RenderTaskKind::HorizontalBlur(BlurTask {
+ blur_std_deviation: adjusted_blur_std_deviation.width,
+ target_kind,
+ blur_region,
+ }),
+ ).with_uv_rect_kind(uv_rect_kind));
+ rg_builder.add_dependency(task_id, blur_task_v);
+
+ task_id
+ });
+
+ if let Some(ref mut cache) = blur_cache {
+ cache.insert(blur_key, blur_task_id);
+ }
+
+ blur_task_id
+ }
+
+ pub fn new_scaling(
+ src_task_id: RenderTaskId,
+ rg_builder: &mut RenderTaskGraphBuilder,
+ target_kind: RenderTargetKind,
+ size: DeviceIntSize,
+ ) -> RenderTaskId {
+ Self::new_scaling_with_padding(
+ src_task_id,
+ rg_builder,
+ target_kind,
+ size,
+ DeviceIntSideOffsets::zero(),
+ )
+ }
+
+ pub fn new_scaling_with_padding(
+ source: RenderTaskId,
+ rg_builder: &mut RenderTaskGraphBuilder,
+ target_kind: RenderTargetKind,
+ padded_size: DeviceIntSize,
+ padding: DeviceIntSideOffsets,
+ ) -> RenderTaskId {
+ let uv_rect_kind = rg_builder.get_task(source).uv_rect_kind();
+
+ let task_id = rg_builder.add().init(
+ RenderTask::new_dynamic(
+ padded_size,
+ RenderTaskKind::Scaling(ScalingTask {
+ target_kind,
+ padding,
+ }),
+ ).with_uv_rect_kind(uv_rect_kind)
+ );
+
+ rg_builder.add_dependency(task_id, source);
+
+ task_id
+ }
+
+ pub fn new_svg_filter(
+ filter_primitives: &[FilterPrimitive],
+ filter_datas: &[SFilterData],
+ rg_builder: &mut RenderTaskGraphBuilder,
+ content_size: DeviceIntSize,
+ uv_rect_kind: UvRectKind,
+ original_task_id: RenderTaskId,
+ device_pixel_scale: DevicePixelScale,
+ ) -> RenderTaskId {
+
+ if filter_primitives.is_empty() {
+ return original_task_id;
+ }
+
+ // Resolves the input to a filter primitive
+ let get_task_input = |
+ input: &FilterPrimitiveInput,
+ filter_primitives: &[FilterPrimitive],
+ rg_builder: &mut RenderTaskGraphBuilder,
+ cur_index: usize,
+ outputs: &[RenderTaskId],
+ original: RenderTaskId,
+ color_space: ColorSpace,
+ | {
+ // TODO(cbrewster): Not sure we can assume that the original input is sRGB.
+ let (mut task_id, input_color_space) = match input.to_index(cur_index) {
+ Some(index) => (outputs[index], filter_primitives[index].color_space),
+ None => (original, ColorSpace::Srgb),
+ };
+
+ match (input_color_space, color_space) {
+ (ColorSpace::Srgb, ColorSpace::LinearRgb) => {
+ task_id = RenderTask::new_svg_filter_primitive(
+ smallvec![task_id],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::SrgbToLinear,
+ rg_builder,
+ );
+ },
+ (ColorSpace::LinearRgb, ColorSpace::Srgb) => {
+ task_id = RenderTask::new_svg_filter_primitive(
+ smallvec![task_id],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::LinearToSrgb,
+ rg_builder,
+ );
+ },
+ _ => {},
+ }
+
+ task_id
+ };
+
+ let mut outputs = vec![];
+ let mut cur_filter_data = 0;
+ for (cur_index, primitive) in filter_primitives.iter().enumerate() {
+ let render_task_id = match primitive.kind {
+ FilterPrimitiveKind::Identity(ref identity) => {
+ // Identity does not create a task, it provides its input's render task
+ get_task_input(
+ &identity.input,
+ filter_primitives,
+ rg_builder,
+ cur_index,
+ &outputs,
+ original_task_id,
+ primitive.color_space
+ )
+ }
+ FilterPrimitiveKind::Blend(ref blend) => {
+ let input_1_task_id = get_task_input(
+ &blend.input1,
+ filter_primitives,
+ rg_builder,
+ cur_index,
+ &outputs,
+ original_task_id,
+ primitive.color_space
+ );
+ let input_2_task_id = get_task_input(
+ &blend.input2,
+ filter_primitives,
+ rg_builder,
+ cur_index,
+ &outputs,
+ original_task_id,
+ primitive.color_space
+ );
+
+ RenderTask::new_svg_filter_primitive(
+ smallvec![input_1_task_id, input_2_task_id],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::Blend(blend.mode),
+ rg_builder,
+ )
+ },
+ FilterPrimitiveKind::Flood(ref flood) => {
+ RenderTask::new_svg_filter_primitive(
+ smallvec![],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::Flood(flood.color),
+ rg_builder,
+ )
+ }
+ FilterPrimitiveKind::Blur(ref blur) => {
+ let width_std_deviation = blur.width * device_pixel_scale.0;
+ let height_std_deviation = blur.height * device_pixel_scale.0;
+ let input_task_id = get_task_input(
+ &blur.input,
+ filter_primitives,
+ rg_builder,
+ cur_index,
+ &outputs,
+ original_task_id,
+ primitive.color_space
+ );
+
+ RenderTask::new_blur(
+ DeviceSize::new(width_std_deviation, height_std_deviation),
+ // TODO: This is a hack to ensure that a blur task's input is always
+ // in the blur's previous pass.
+ RenderTask::new_svg_filter_primitive(
+ smallvec![input_task_id],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::Identity,
+ rg_builder,
+ ),
+ rg_builder,
+ RenderTargetKind::Color,
+ None,
+ content_size,
+ )
+ }
+ FilterPrimitiveKind::Opacity(ref opacity) => {
+ let input_task_id = get_task_input(
+ &opacity.input,
+ filter_primitives,
+ rg_builder,
+ cur_index,
+ &outputs,
+ original_task_id,
+ primitive.color_space
+ );
+
+ RenderTask::new_svg_filter_primitive(
+ smallvec![input_task_id],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::Opacity(opacity.opacity),
+ rg_builder,
+ )
+ }
+ FilterPrimitiveKind::ColorMatrix(ref color_matrix) => {
+ let input_task_id = get_task_input(
+ &color_matrix.input,
+ filter_primitives,
+ rg_builder,
+ cur_index,
+ &outputs,
+ original_task_id,
+ primitive.color_space
+ );
+
+ RenderTask::new_svg_filter_primitive(
+ smallvec![input_task_id],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::ColorMatrix(Box::new(color_matrix.matrix)),
+ rg_builder,
+ )
+ }
+ FilterPrimitiveKind::DropShadow(ref drop_shadow) => {
+ let input_task_id = get_task_input(
+ &drop_shadow.input,
+ filter_primitives,
+ rg_builder,
+ cur_index,
+ &outputs,
+ original_task_id,
+ primitive.color_space
+ );
+
+ let blur_std_deviation = drop_shadow.shadow.blur_radius * device_pixel_scale.0;
+ let offset = drop_shadow.shadow.offset * LayoutToWorldScale::new(1.0) * device_pixel_scale;
+
+ let offset_task_id = RenderTask::new_svg_filter_primitive(
+ smallvec![input_task_id],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::Offset(offset),
+ rg_builder,
+ );
+
+ let blur_task_id = RenderTask::new_blur(
+ DeviceSize::new(blur_std_deviation, blur_std_deviation),
+ offset_task_id,
+ rg_builder,
+ RenderTargetKind::Color,
+ None,
+ content_size,
+ );
+
+ RenderTask::new_svg_filter_primitive(
+ smallvec![input_task_id, blur_task_id],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::DropShadow(drop_shadow.shadow.color),
+ rg_builder,
+ )
+ }
+ FilterPrimitiveKind::ComponentTransfer(ref component_transfer) => {
+ let input_task_id = get_task_input(
+ &component_transfer.input,
+ filter_primitives,
+ rg_builder,
+ cur_index,
+ &outputs,
+ original_task_id,
+ primitive.color_space
+ );
+
+ let filter_data = &filter_datas[cur_filter_data];
+ cur_filter_data += 1;
+ if filter_data.is_identity() {
+ input_task_id
+ } else {
+ RenderTask::new_svg_filter_primitive(
+ smallvec![input_task_id],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::ComponentTransfer(filter_data.clone()),
+ rg_builder,
+ )
+ }
+ }
+ FilterPrimitiveKind::Offset(ref info) => {
+ let input_task_id = get_task_input(
+ &info.input,
+ filter_primitives,
+ rg_builder,
+ cur_index,
+ &outputs,
+ original_task_id,
+ primitive.color_space
+ );
+
+ let offset = info.offset * LayoutToWorldScale::new(1.0) * device_pixel_scale;
+ RenderTask::new_svg_filter_primitive(
+ smallvec![input_task_id],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::Offset(offset),
+ rg_builder,
+ )
+ }
+ FilterPrimitiveKind::Composite(info) => {
+ let input_1_task_id = get_task_input(
+ &info.input1,
+ filter_primitives,
+ rg_builder,
+ cur_index,
+ &outputs,
+ original_task_id,
+ primitive.color_space
+ );
+ let input_2_task_id = get_task_input(
+ &info.input2,
+ filter_primitives,
+ rg_builder,
+ cur_index,
+ &outputs,
+ original_task_id,
+ primitive.color_space
+ );
+
+ RenderTask::new_svg_filter_primitive(
+ smallvec![input_1_task_id, input_2_task_id],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::Composite(info.operator),
+ rg_builder,
+ )
+ }
+ };
+ outputs.push(render_task_id);
+ }
+
+ // The output of a filter is the output of the last primitive in the chain.
+ let mut render_task_id = *outputs.last().unwrap();
+
+ // Convert to sRGB if needed
+ if filter_primitives.last().unwrap().color_space == ColorSpace::LinearRgb {
+ render_task_id = RenderTask::new_svg_filter_primitive(
+ smallvec![render_task_id],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::LinearToSrgb,
+ rg_builder,
+ );
+ }
+
+ render_task_id
+ }
+
+ pub fn new_svg_filter_primitive(
+ tasks: TaskDependencies,
+ target_size: DeviceIntSize,
+ uv_rect_kind: UvRectKind,
+ info: SvgFilterInfo,
+ rg_builder: &mut RenderTaskGraphBuilder,
+ ) -> RenderTaskId {
+ let task_id = rg_builder.add().init(RenderTask::new_dynamic(
+ target_size,
+ RenderTaskKind::SvgFilter(SvgFilterTask {
+ extra_gpu_cache_handle: None,
+ info,
+ }),
+ ).with_uv_rect_kind(uv_rect_kind));
+
+ for child_id in tasks {
+ rg_builder.add_dependency(task_id, child_id);
+ }
+
+ task_id
+ }
+
+ pub fn uv_rect_kind(&self) -> UvRectKind {
+ self.uv_rect_kind
+ }
+
+ pub fn get_texture_address(&self, gpu_cache: &GpuCache) -> GpuCacheAddress {
+ gpu_cache.get_address(&self.uv_rect_handle)
+ }
+
+ pub fn get_target_texture(&self) -> CacheTextureId {
+ match self.location {
+ RenderTaskLocation::Dynamic { texture_id, .. } => {
+ assert_ne!(texture_id, CacheTextureId::INVALID);
+ texture_id
+ }
+ RenderTaskLocation::Existing { .. } |
+ RenderTaskLocation::CacheRequest { .. } |
+ RenderTaskLocation::Unallocated { .. } |
+ RenderTaskLocation::Static { .. } => {
+ unreachable!();
+ }
+ }
+ }
+
+ pub fn get_texture_source(&self) -> TextureSource {
+ match self.location {
+ RenderTaskLocation::Dynamic { texture_id, .. } => {
+ assert_ne!(texture_id, CacheTextureId::INVALID);
+ TextureSource::TextureCache(texture_id, Swizzle::default())
+ }
+ RenderTaskLocation::Static { surface: StaticRenderTaskSurface::ReadOnly { source }, .. } => {
+ source
+ }
+ RenderTaskLocation::Static { surface: StaticRenderTaskSurface::TextureCache { texture, .. }, .. } => {
+ TextureSource::TextureCache(texture, Swizzle::default())
+ }
+ RenderTaskLocation::Existing { .. } |
+ RenderTaskLocation::Static { .. } |
+ RenderTaskLocation::CacheRequest { .. } |
+ RenderTaskLocation::Unallocated { .. } => {
+ unreachable!();
+ }
+ }
+ }
+
+ pub fn get_target_rect(&self) -> DeviceIntRect {
+ match self.location {
+ // Previously, we only added render tasks after the entire
+ // primitive chain was determined visible. This meant that
+ // we could assert any render task in the list was also
+ // allocated (assigned to passes). Now, we add render
+ // tasks earlier, and the picture they belong to may be
+ // culled out later, so we can't assert that the task
+ // has been allocated.
+ // Render tasks that are created but not assigned to
+ // passes consume a row in the render task texture, but
+ // don't allocate any space in render targets nor
+ // draw any pixels.
+ // TODO(gw): Consider some kind of tag or other method
+ // to mark a task as unused explicitly. This
+ // would allow us to restore this debug check.
+ RenderTaskLocation::Dynamic { rect, .. } => rect,
+ RenderTaskLocation::Static { rect, .. } => rect,
+ RenderTaskLocation::Existing { .. } |
+ RenderTaskLocation::CacheRequest { .. } |
+ RenderTaskLocation::Unallocated { .. } => {
+ panic!("bug: get_target_rect called before allocating");
+ }
+ }
+ }
+
+ pub fn target_kind(&self) -> RenderTargetKind {
+ self.kind.target_kind()
+ }
+
+ pub fn write_gpu_blocks(
+ &mut self,
+ target_rect: DeviceIntRect,
+ gpu_cache: &mut GpuCache,
+ ) {
+ profile_scope!("write_gpu_blocks");
+
+ self.kind.write_gpu_blocks(gpu_cache);
+
+ if self.cache_handle.is_some() {
+ // The uv rect handle of cached render tasks is requested and set by the
+ // render task cache.
+ return;
+ }
+
+ if let Some(mut request) = gpu_cache.request(&mut self.uv_rect_handle) {
+ let p0 = target_rect.min.to_f32();
+ let p1 = target_rect.max.to_f32();
+ let image_source = ImageSource {
+ p0,
+ p1,
+ user_data: [0.0; 4],
+ uv_rect_kind: self.uv_rect_kind,
+ };
+ image_source.write_gpu_blocks(&mut request);
+ }
+ }
+
+ /// Called by the render task cache.
+ ///
+ /// Tells the render task that it is cached (which means its gpu cache
+ /// handle is managed by the texture cache).
+ pub fn mark_cached(&mut self, handle: RenderTaskCacheEntryHandle) {
+ self.cache_handle = Some(handle);
+ }
+}
diff --git a/gfx/wr/webrender/src/render_task_cache.rs b/gfx/wr/webrender/src/render_task_cache.rs
new file mode 100644
index 0000000000..0454c1214f
--- /dev/null
+++ b/gfx/wr/webrender/src/render_task_cache.rs
@@ -0,0 +1,379 @@
+/* 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 api::{ImageDescriptor, ImageDescriptorFlags, DirtyRect};
+use api::units::*;
+use crate::border::BorderSegmentCacheKey;
+use crate::box_shadow::{BoxShadowCacheKey};
+use crate::device::TextureFilter;
+use crate::freelist::{FreeList, FreeListHandle, WeakFreeListHandle};
+use crate::gpu_cache::GpuCache;
+use crate::internal_types::FastHashMap;
+use crate::picture::SurfaceIndex;
+use crate::prim_store::image::ImageCacheKey;
+use crate::prim_store::gradient::{
+ FastLinearGradientCacheKey, LinearGradientCacheKey, RadialGradientCacheKey,
+ ConicGradientCacheKey,
+};
+use crate::prim_store::line_dec::LineDecorationCacheKey;
+use crate::resource_cache::CacheItem;
+use std::{mem, usize, f32, i32};
+use crate::surface::SurfaceBuilder;
+use crate::texture_cache::{TextureCache, TextureCacheHandle, Eviction, TargetShader};
+use crate::renderer::GpuBufferBuilder;
+use crate::render_target::RenderTargetKind;
+use crate::render_task::{RenderTask, StaticRenderTaskSurface, RenderTaskLocation, RenderTaskKind, CachedTask};
+use crate::render_task_graph::{RenderTaskGraphBuilder, RenderTaskId};
+use euclid::Scale;
+
+const MAX_CACHE_TASK_SIZE: f32 = 4096.0;
+
+/// Describes a parent dependency for a render task. Render tasks
+/// may depend on a surface (e.g. when a surface uses a cached border)
+/// or an arbitrary render task (e.g. when a clip mask uses a blurred
+/// box-shadow input).
+pub enum RenderTaskParent {
+ /// Parent is a surface
+ Surface(SurfaceIndex),
+ /// Parent is a render task
+ RenderTask(RenderTaskId),
+}
+
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum RenderTaskCacheKeyKind {
+ BoxShadow(BoxShadowCacheKey),
+ Image(ImageCacheKey),
+ BorderSegment(BorderSegmentCacheKey),
+ LineDecoration(LineDecorationCacheKey),
+ FastLinearGradient(FastLinearGradientCacheKey),
+ LinearGradient(LinearGradientCacheKey),
+ RadialGradient(RadialGradientCacheKey),
+ ConicGradient(ConicGradientCacheKey),
+}
+
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RenderTaskCacheKey {
+ pub size: DeviceIntSize,
+ pub kind: RenderTaskCacheKeyKind,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RenderTaskCacheEntry {
+ user_data: Option<[f32; 4]>,
+ target_kind: RenderTargetKind,
+ is_opaque: bool,
+ frame_id: u64,
+ pub handle: TextureCacheHandle,
+ /// If a render task was generated for this cache entry on _this_ frame,
+ /// we need to track the task id here. This allows us to hook it up as
+ /// a dependency of any parent tasks that make a reqiest from the render
+ /// task cache.
+ pub render_task_id: Option<RenderTaskId>,
+}
+
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub enum RenderTaskCacheMarker {}
+
+// A cache of render tasks that are stored in the texture
+// cache for usage across frames.
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RenderTaskCache {
+ map: FastHashMap<RenderTaskCacheKey, FreeListHandle<RenderTaskCacheMarker>>,
+ cache_entries: FreeList<RenderTaskCacheEntry, RenderTaskCacheMarker>,
+ frame_id: u64,
+}
+
+pub type RenderTaskCacheEntryHandle = WeakFreeListHandle<RenderTaskCacheMarker>;
+
+impl RenderTaskCache {
+ pub fn new() -> Self {
+ RenderTaskCache {
+ map: FastHashMap::default(),
+ cache_entries: FreeList::new(),
+ frame_id: 0,
+ }
+ }
+
+ pub fn clear(&mut self) {
+ self.map.clear();
+ self.cache_entries.clear();
+ }
+
+ pub fn begin_frame(
+ &mut self,
+ texture_cache: &mut TextureCache,
+ ) {
+ self.frame_id += 1;
+ profile_scope!("begin_frame");
+ // Drop any items from the cache that have been
+ // evicted from the texture cache.
+ //
+ // This isn't actually necessary for the texture
+ // cache to be able to evict old render tasks.
+ // It will evict render tasks as required, since
+ // the access time in the texture cache entry will
+ // be stale if this task hasn't been requested
+ // for a while.
+ //
+ // Nonetheless, we should remove stale entries
+ // from here so that this hash map doesn't
+ // grow indefinitely!
+ let cache_entries = &mut self.cache_entries;
+ let frame_id = self.frame_id;
+
+ self.map.retain(|_, handle| {
+ let mut retain = texture_cache.is_allocated(
+ &cache_entries.get(handle).handle,
+ );
+ if retain {
+ let entry = cache_entries.get_mut(&handle);
+ if frame_id > entry.frame_id + 10 {
+ texture_cache.evict_handle(&entry.handle);
+ retain = false;
+ }
+ }
+
+ if !retain {
+ let handle = mem::replace(handle, FreeListHandle::invalid());
+ cache_entries.free(handle);
+ }
+
+ retain
+ });
+
+ // Clear out the render task ID of any remaining cache entries that were drawn
+ // on the previous frame, so we don't accidentally hook up stale dependencies
+ // when building the frame graph.
+ for (_, handle) in &self.map {
+ let entry = self.cache_entries.get_mut(handle);
+ entry.render_task_id = None;
+ }
+ }
+
+ fn alloc_render_task(
+ size: DeviceIntSize,
+ render_task: &mut RenderTask,
+ entry: &mut RenderTaskCacheEntry,
+ gpu_cache: &mut GpuCache,
+ texture_cache: &mut TextureCache,
+ ) {
+ // Find out what size to alloc in the texture cache.
+ let target_kind = render_task.target_kind();
+
+ // Select the right texture page to allocate from.
+ let image_format = match target_kind {
+ RenderTargetKind::Color => texture_cache.shared_color_expected_format(),
+ RenderTargetKind::Alpha => texture_cache.shared_alpha_expected_format(),
+ };
+
+ let flags = if entry.is_opaque {
+ ImageDescriptorFlags::IS_OPAQUE
+ } else {
+ ImageDescriptorFlags::empty()
+ };
+
+ let descriptor = ImageDescriptor::new(
+ size.width,
+ size.height,
+ image_format,
+ flags,
+ );
+
+ // Allocate space in the texture cache, but don't supply
+ // and CPU-side data to be uploaded.
+ texture_cache.update(
+ &mut entry.handle,
+ descriptor,
+ TextureFilter::Linear,
+ None,
+ entry.user_data.unwrap_or([0.0; 4]),
+ DirtyRect::All,
+ gpu_cache,
+ None,
+ render_task.uv_rect_kind(),
+ Eviction::Auto,
+ TargetShader::Default,
+ );
+
+ // Get the allocation details in the texture cache, and store
+ // this in the render task. The renderer will draw this task
+ // into the appropriate rect of the texture cache on this frame.
+ let (texture_id, uv_rect, _, _, _) =
+ texture_cache.get_cache_location(&entry.handle);
+
+ let surface = StaticRenderTaskSurface::TextureCache {
+ texture: texture_id,
+ target_kind,
+ };
+
+ render_task.location = RenderTaskLocation::Static {
+ surface,
+ rect: uv_rect.to_i32(),
+ };
+ }
+
+ pub fn request_render_task<F>(
+ &mut self,
+ key: RenderTaskCacheKey,
+ texture_cache: &mut TextureCache,
+ gpu_cache: &mut GpuCache,
+ gpu_buffer_builder: &mut GpuBufferBuilder,
+ rg_builder: &mut RenderTaskGraphBuilder,
+ user_data: Option<[f32; 4]>,
+ is_opaque: bool,
+ parent: RenderTaskParent,
+ surface_builder: &mut SurfaceBuilder,
+ f: F,
+ ) -> Result<RenderTaskId, ()>
+ where
+ F: FnOnce(&mut RenderTaskGraphBuilder, &mut GpuBufferBuilder) -> Result<RenderTaskId, ()>,
+ {
+ let frame_id = self.frame_id;
+ let size = key.size;
+ // Get the texture cache handle for this cache key,
+ // or create one.
+ let cache_entries = &mut self.cache_entries;
+ let entry_handle = self.map.entry(key).or_insert_with(|| {
+ let entry = RenderTaskCacheEntry {
+ handle: TextureCacheHandle::invalid(),
+ user_data,
+ target_kind: RenderTargetKind::Color, // will be set below.
+ is_opaque,
+ frame_id,
+ render_task_id: None,
+ };
+ cache_entries.insert(entry)
+ });
+ let cache_entry = cache_entries.get_mut(entry_handle);
+ cache_entry.frame_id = self.frame_id;
+
+ // Check if this texture cache handle is valid.
+ if texture_cache.request(&cache_entry.handle, gpu_cache) {
+ // Invoke user closure to get render task chain
+ // to draw this into the texture cache.
+ let render_task_id = f(rg_builder, gpu_buffer_builder)?;
+
+ cache_entry.user_data = user_data;
+ cache_entry.is_opaque = is_opaque;
+ cache_entry.render_task_id = Some(render_task_id);
+
+ let render_task = rg_builder.get_task_mut(render_task_id);
+ let task_size = render_task.location.size();
+
+ render_task.mark_cached(entry_handle.weak());
+ cache_entry.target_kind = render_task.kind.target_kind();
+
+ RenderTaskCache::alloc_render_task(
+ task_size,
+ render_task,
+ cache_entry,
+ gpu_cache,
+ texture_cache,
+ );
+ }
+
+ // If this render task cache is being drawn this frame, ensure we hook up the
+ // render task for it as a dependency of any render task that uses this as
+ // an input source.
+ if let Some(render_task_id) = cache_entry.render_task_id {
+ match parent {
+ // TODO(gw): Remove surface from here as a follow up patch, as it's now implicit
+ // due to using SurfaceBuilder
+ RenderTaskParent::Surface(_surface_index) => {
+ // If parent is a surface, use helper fn to add this dependency,
+ // which correctly takes account of the render task configuration
+ // of the surface.
+ surface_builder.add_child_render_task(
+ render_task_id,
+ rg_builder,
+ );
+ }
+ RenderTaskParent::RenderTask(parent_render_task_id) => {
+ // For render tasks, just add it as a direct dependency on the
+ // task graph builder.
+ rg_builder.add_dependency(
+ parent_render_task_id,
+ render_task_id,
+ );
+ }
+ }
+
+ return Ok(render_task_id);
+ }
+
+ let target_kind = cache_entry.target_kind;
+ let mut task = RenderTask::new(
+ RenderTaskLocation::CacheRequest { size, },
+ RenderTaskKind::Cached(CachedTask {
+ target_kind,
+ }),
+ );
+ task.mark_cached(entry_handle.weak());
+ let render_task_id = rg_builder.add().init(task);
+
+ Ok(render_task_id)
+ }
+
+ pub fn get_cache_entry(
+ &self,
+ handle: &RenderTaskCacheEntryHandle,
+ ) -> &RenderTaskCacheEntry {
+ self.cache_entries
+ .get_opt(handle)
+ .expect("bug: invalid render task cache handle")
+ }
+
+ #[allow(dead_code)]
+ pub fn get_cache_item_for_render_task(&self,
+ texture_cache: &TextureCache,
+ key: &RenderTaskCacheKey)
+ -> CacheItem {
+ // Get the texture cache handle for this cache key.
+ let handle = self.map.get(key).unwrap();
+ let cache_entry = self.cache_entries.get(handle);
+ texture_cache.get(&cache_entry.handle)
+ }
+
+ #[allow(dead_code)]
+ pub fn get_allocated_size_for_render_task(&self,
+ texture_cache: &TextureCache,
+ key: &RenderTaskCacheKey)
+ -> Option<usize> {
+ let handle = self.map.get(key).unwrap();
+ let cache_entry = self.cache_entries.get(handle);
+ texture_cache.get_allocated_size(&cache_entry.handle)
+ }
+}
+
+// TODO(gw): Rounding the content rect here to device pixels is not
+// technically correct. Ideally we should ceil() here, and ensure that
+// the extra part pixel in the case of fractional sizes is correctly
+// handled. For now, just use rounding which passes the existing
+// Gecko tests.
+// Note: zero-square tasks are prohibited in WR task graph, so
+// we ensure each dimension to be at least the length of 1 after rounding.
+pub fn to_cache_size(size: LayoutSize, device_pixel_scale: &mut Scale<f32, LayoutPixel, DevicePixel>) -> DeviceIntSize {
+ let mut device_size = (size * *device_pixel_scale).round();
+
+ if device_size.width > MAX_CACHE_TASK_SIZE || device_size.height > MAX_CACHE_TASK_SIZE {
+ let scale = MAX_CACHE_TASK_SIZE / f32::max(device_size.width, device_size.height);
+ *device_pixel_scale = *device_pixel_scale * Scale::new(scale);
+ device_size = (size * *device_pixel_scale).round();
+ }
+
+ DeviceIntSize::new(
+ 1.max(device_size.width as i32),
+ 1.max(device_size.height as i32),
+ )
+}
diff --git a/gfx/wr/webrender/src/render_task_graph.rs b/gfx/wr/webrender/src/render_task_graph.rs
new file mode 100644
index 0000000000..29ecf66a2e
--- /dev/null
+++ b/gfx/wr/webrender/src/render_task_graph.rs
@@ -0,0 +1,1234 @@
+// 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/.
+
+//! This module contains the render task graph.
+//!
+//! Code associated with creating specific render tasks is in the render_task
+//! module.
+
+use api::units::*;
+use api::ImageFormat;
+use crate::gpu_cache::{GpuCache, GpuCacheAddress};
+use crate::internal_types::{TextureSource, CacheTextureId, FastHashMap, FastHashSet, FrameId};
+use crate::render_task::{StaticRenderTaskSurface, RenderTaskLocation, RenderTask};
+use crate::render_target::RenderTargetKind;
+use crate::render_task::{RenderTaskData, RenderTaskKind};
+use crate::resource_cache::ResourceCache;
+use crate::texture_pack::GuillotineAllocator;
+use crate::prim_store::DeferredResolve;
+use crate::image_source::{resolve_image, resolve_cached_render_task};
+use crate::util::VecHelper;
+use smallvec::SmallVec;
+use std::mem;
+use topological_sort::TopologicalSort;
+
+use crate::render_target::{RenderTargetList, ColorRenderTarget};
+use crate::render_target::{PictureCacheTarget, TextureCacheRenderTarget, AlphaRenderTarget};
+use crate::util::Allocation;
+use std::{usize, f32};
+
+/// If we ever need a larger texture than the ideal, we better round it up to a
+/// reasonable number in order to have a bit of leeway in case the size of this
+/// this target is changing each frame.
+const TEXTURE_DIMENSION_MASK: i32 = 0xFF;
+
+/// Allows initializing a render task directly into the render task buffer.
+///
+/// See utils::VecHelpers. RenderTask is fairly large so avoiding the move when
+/// pushing into the vector can save a lot of expensive memcpys on pages with many
+/// render tasks.
+pub struct RenderTaskAllocation<'a> {
+ pub alloc: Allocation<'a, RenderTask>,
+}
+
+impl<'l> RenderTaskAllocation<'l> {
+ #[inline(always)]
+ pub fn init(self, value: RenderTask) -> RenderTaskId {
+ RenderTaskId {
+ index: self.alloc.init(value) as u32,
+ }
+ }
+}
+
+#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
+#[derive(MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RenderTaskId {
+ pub index: u32,
+}
+
+impl RenderTaskId {
+ pub const INVALID: RenderTaskId = RenderTaskId {
+ index: u32::MAX,
+ };
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, PartialOrd, Ord)]
+pub struct PassId(usize);
+
+impl PassId {
+ pub const MIN: PassId = PassId(0);
+ pub const MAX: PassId = PassId(!0 - 1);
+ pub const INVALID: PassId = PassId(!0 - 2);
+}
+
+/// An internal representation of a dynamic surface that tasks can be
+/// allocated into. Maintains some extra metadata about each surface
+/// during the graph build.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct Surface {
+ /// Whether this is a color or alpha render target
+ kind: RenderTargetKind,
+ /// Allocator for this surface texture
+ allocator: GuillotineAllocator,
+ /// We can only allocate into this for reuse if it's a shared surface
+ is_shared: bool,
+ /// The pass that we can free this surface after (guaranteed
+ /// to be the same for all tasks assigned to this surface)
+ free_after: PassId,
+}
+
+impl Surface {
+ /// Allocate a rect within a shared surfce. Returns None if the
+ /// format doesn't match, or allocation fails.
+ fn alloc_rect(
+ &mut self,
+ size: DeviceIntSize,
+ kind: RenderTargetKind,
+ is_shared: bool,
+ free_after: PassId,
+ ) -> Option<DeviceIntPoint> {
+ if self.kind == kind && self.is_shared == is_shared && self.free_after == free_after {
+ self.allocator
+ .allocate(&size)
+ .map(|(_slice, origin)| origin)
+ } else {
+ None
+ }
+ }
+}
+
+/// A sub-pass can draw to either a dynamic (temporary render target) surface,
+/// or a persistent surface (texture or picture cache).
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug)]
+pub enum SubPassSurface {
+ /// A temporary (intermediate) surface.
+ Dynamic {
+ /// The renderer texture id
+ texture_id: CacheTextureId,
+ /// Color / alpha render target
+ target_kind: RenderTargetKind,
+ /// The rectangle occupied by tasks in this surface. Used as a clear
+ /// optimization on some GPUs.
+ used_rect: DeviceIntRect,
+ },
+ Persistent {
+ /// Reference to the texture or picture cache surface being drawn to.
+ surface: StaticRenderTaskSurface,
+ },
+}
+
+/// A subpass is a specific render target, and a list of tasks to draw to it.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SubPass {
+ /// The surface this subpass draws to
+ pub surface: SubPassSurface,
+ /// The tasks assigned to this subpass.
+ pub task_ids: Vec<RenderTaskId>,
+}
+
+/// A pass expresses dependencies between tasks. Each pass consists of a number
+/// of subpasses.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct Pass {
+ /// The tasks assigned to this render pass
+ pub task_ids: Vec<RenderTaskId>,
+ /// The subpasses that make up this dependency pass
+ pub sub_passes: Vec<SubPass>,
+ /// A list of intermediate surfaces that can be invalidated after
+ /// this pass completes.
+ pub textures_to_invalidate: Vec<CacheTextureId>,
+}
+
+/// The RenderTaskGraph is the immutable representation of the render task graph. It is
+/// built by the RenderTaskGraphBuilder, and is constructed once per frame.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RenderTaskGraph {
+ /// List of tasks added to the graph
+ pub tasks: Vec<RenderTask>,
+
+ /// The passes that were created, based on dependencies between tasks
+ pub passes: Vec<Pass>,
+
+ /// Current frame id, used for debug validation
+ frame_id: FrameId,
+
+ /// GPU specific data for each task that is made available to shaders
+ pub task_data: Vec<RenderTaskData>,
+
+ /// Total number of intermediate surfaces that will be drawn to, used for test validation.
+ #[cfg(test)]
+ surface_count: usize,
+
+ /// Total number of real allocated textures that will be drawn to, used for test validation.
+ #[cfg(test)]
+ unique_surfaces: FastHashSet<CacheTextureId>,
+}
+
+/// The persistent interface that is used during frame building to construct the
+/// frame graph.
+pub struct RenderTaskGraphBuilder {
+ /// List of tasks added to the builder
+ tasks: Vec<RenderTask>,
+
+ /// List of task roots
+ roots: FastHashSet<RenderTaskId>,
+
+ /// Current frame id, used for debug validation
+ frame_id: FrameId,
+
+ /// A list of texture surfaces that can be freed at the end of a pass. Retained
+ /// here to reduce heap allocations.
+ textures_to_free: FastHashSet<CacheTextureId>,
+
+ // Keep a map of `texture_id` to metadata about surfaces that are currently
+ // borrowed from the render target pool.
+ active_surfaces: FastHashMap<CacheTextureId, Surface>,
+}
+
+impl RenderTaskGraphBuilder {
+ /// Construct a new graph builder. Typically constructed once and maintained
+ /// over many frames, to avoid extra heap allocations where possible.
+ pub fn new() -> Self {
+ RenderTaskGraphBuilder {
+ tasks: Vec::new(),
+ roots: FastHashSet::default(),
+ frame_id: FrameId::INVALID,
+ textures_to_free: FastHashSet::default(),
+ active_surfaces: FastHashMap::default(),
+ }
+ }
+
+ pub fn frame_id(&self) -> FrameId {
+ self.frame_id
+ }
+
+ /// Begin a new frame
+ pub fn begin_frame(&mut self, frame_id: FrameId) {
+ self.frame_id = frame_id;
+ self.roots.clear();
+ }
+
+ /// Get immutable access to a task
+ // TODO(gw): There's only a couple of places that existing code needs to access
+ // a task during the building step. Perhaps we can remove this?
+ pub fn get_task(
+ &self,
+ task_id: RenderTaskId,
+ ) -> &RenderTask {
+ &self.tasks[task_id.index as usize]
+ }
+
+ /// Get mutable access to a task
+ // TODO(gw): There's only a couple of places that existing code needs to access
+ // a task during the building step. Perhaps we can remove this?
+ pub fn get_task_mut(
+ &mut self,
+ task_id: RenderTaskId,
+ ) -> &mut RenderTask {
+ &mut self.tasks[task_id.index as usize]
+ }
+
+ /// Add a new task to the graph.
+ pub fn add(&mut self) -> RenderTaskAllocation {
+ // Assume every task is a root to start with
+ self.roots.insert(
+ RenderTaskId { index: self.tasks.len() as u32 }
+ );
+
+ RenderTaskAllocation {
+ alloc: self.tasks.alloc(),
+ }
+ }
+
+ /// Express a dependency, such that `task_id` depends on `input` as a texture source.
+ pub fn add_dependency(
+ &mut self,
+ task_id: RenderTaskId,
+ input: RenderTaskId,
+ ) {
+ self.tasks[task_id.index as usize].children.push(input);
+
+ // Once a task is an input, it's no longer a root
+ self.roots.remove(&input);
+ }
+
+ /// End the graph building phase and produce the immutable task graph for this frame
+ pub fn end_frame(
+ &mut self,
+ resource_cache: &mut ResourceCache,
+ gpu_cache: &mut GpuCache,
+ deferred_resolves: &mut Vec<DeferredResolve>,
+ max_shared_surface_size: i32,
+ ) -> RenderTaskGraph {
+ // Copy the render tasks over to the immutable graph output
+ let task_count = self.tasks.len();
+ let tasks = mem::replace(
+ &mut self.tasks,
+ Vec::with_capacity(task_count),
+ );
+
+ let mut graph = RenderTaskGraph {
+ tasks,
+ passes: Vec::new(),
+ task_data: Vec::with_capacity(task_count),
+ frame_id: self.frame_id,
+ #[cfg(test)]
+ surface_count: 0,
+ #[cfg(test)]
+ unique_surfaces: FastHashSet::default(),
+ };
+
+ // First, use a topological sort of the dependency graph to split the task set in to
+ // a list of passes. This is necessary because when we have a complex graph (e.g. due
+ // to a large number of sibling backdrop-filter primitives) traversing it via a simple
+ // recursion can be too slow. The second pass determines when the last time a render task
+ // is used as an input, and assigns what pass the surface backing that render task can
+ // be freed (the surface is then returned to the render target pool and may be aliased
+ // or reused during subsequent passes).
+
+ let mut pass_count = 0;
+ let mut passes = Vec::new();
+ let mut task_sorter = TopologicalSort::<RenderTaskId>::new();
+
+ // Iterate the task list, and add all the dependencies to the topo sort
+ for (parent_id, task) in graph.tasks.iter().enumerate() {
+ let parent_id = RenderTaskId { index: parent_id as u32 };
+
+ for child_id in &task.children {
+ task_sorter.add_dependency(
+ parent_id,
+ *child_id,
+ );
+ }
+ }
+
+ // Pop the sorted passes off the topological sort
+ loop {
+ // Get the next set of tasks that can be drawn
+ let tasks = task_sorter.pop_all();
+
+ // If there are no tasks left, we're done
+ if tasks.is_empty() {
+ // If the task sorter itself isn't empty but we couldn't pop off any
+ // tasks, that implies a circular dependency in the task graph
+ assert!(task_sorter.is_empty());
+ break;
+ } else {
+ // Assign the `render_on` field to the task
+ for task_id in &tasks {
+ graph.tasks[task_id.index as usize].render_on = PassId(pass_count);
+ }
+
+ // Store the task list for this pass, used later for `assign_free_pass`.
+ passes.push(tasks);
+ pass_count += 1;
+ }
+ }
+
+ // Always create at least one pass for root tasks
+ pass_count = pass_count.max(1);
+
+ // Determine which pass each task can be freed on, which depends on which is
+ // the last task that has this as an input. This must be done in top-down
+ // pass order to ensure that RenderTaskLocation::Existing references are
+ // visited in the correct order
+ for pass in passes {
+ for task_id in pass {
+ assign_free_pass(
+ task_id,
+ &mut graph,
+ );
+ }
+ }
+
+ // Construct passes array for tasks to be assigned to below
+ for _ in 0 .. pass_count {
+ graph.passes.push(Pass {
+ task_ids: Vec::new(),
+ sub_passes: Vec::new(),
+ textures_to_invalidate: Vec::new(),
+ });
+ }
+
+ // Assign tasks to each pass based on their `render_on` attribute
+ for (index, task) in graph.tasks.iter().enumerate() {
+ if task.kind.is_a_rendering_operation() {
+ let id = RenderTaskId { index: index as u32 };
+ graph.passes[task.render_on.0].task_ids.push(id);
+ }
+ }
+
+ // At this point, tasks are assigned to each dependency pass. Now we
+ // can go through each pass and create sub-passes, assigning each task
+ // to a target and destination rect.
+ assert!(self.active_surfaces.is_empty());
+
+ for (pass_id, pass) in graph.passes.iter_mut().enumerate().rev() {
+ assert!(self.textures_to_free.is_empty());
+
+ for task_id in &pass.task_ids {
+
+ let task_location = graph.tasks[task_id.index as usize].location.clone();
+
+ match task_location {
+ RenderTaskLocation::Unallocated { size } => {
+ let task = &mut graph.tasks[task_id.index as usize];
+
+ let mut location = None;
+ let kind = task.kind.target_kind();
+
+ // If a task is used as part of an existing-chain then we can't
+ // safely share it (nor would we want to).
+ let can_use_shared_surface =
+ task.kind.can_use_shared_surface() &&
+ task.free_after != PassId::INVALID;
+
+ if can_use_shared_surface {
+ // If we can use a shared surface, step through the existing shared
+ // surfaces for this subpass, and see if we can allocate the task
+ // to one of these targets.
+ for sub_pass in &mut pass.sub_passes {
+ if let SubPassSurface::Dynamic { texture_id, ref mut used_rect, .. } = sub_pass.surface {
+ let surface = self.active_surfaces.get_mut(&texture_id).unwrap();
+ if let Some(p) = surface.alloc_rect(size, kind, true, task.free_after) {
+ location = Some((texture_id, p));
+ *used_rect = used_rect.union(&DeviceIntRect::from_origin_and_size(p, size));
+ sub_pass.task_ids.push(*task_id);
+ break;
+ }
+ }
+ }
+ }
+
+ if location.is_none() {
+ // If it wasn't possible to allocate the task to a shared surface, get a new
+ // render target from the resource cache pool/
+
+ // If this is a really large task, don't bother allocating it as a potential
+ // shared surface for other tasks.
+
+ let can_use_shared_surface = can_use_shared_surface &&
+ size.width <= max_shared_surface_size &&
+ size.height <= max_shared_surface_size;
+
+ let surface_size = if can_use_shared_surface {
+ DeviceIntSize::new(
+ max_shared_surface_size,
+ max_shared_surface_size,
+ )
+ } else {
+ // Round up size here to avoid constant re-allocs during resizing
+ DeviceIntSize::new(
+ (size.width + TEXTURE_DIMENSION_MASK) & !TEXTURE_DIMENSION_MASK,
+ (size.height + TEXTURE_DIMENSION_MASK) & !TEXTURE_DIMENSION_MASK,
+ )
+ };
+
+ let format = match kind {
+ RenderTargetKind::Color => ImageFormat::RGBA8,
+ RenderTargetKind::Alpha => ImageFormat::R8,
+ };
+
+ // Get render target of appropriate size and format from resource cache
+ let texture_id = resource_cache.get_or_create_render_target_from_pool(
+ surface_size,
+ format,
+ );
+
+ // Allocate metadata we need about this surface while it's active
+ let mut surface = Surface {
+ kind,
+ allocator: GuillotineAllocator::new(Some(surface_size)),
+ is_shared: can_use_shared_surface,
+ free_after: task.free_after,
+ };
+
+ // Allocation of the task must fit in this new surface!
+ let p = surface.alloc_rect(
+ size,
+ kind,
+ can_use_shared_surface,
+ task.free_after,
+ ).expect("bug: alloc must succeed!");
+
+ location = Some((texture_id, p));
+
+ // Store the metadata about this newly active surface. We should never
+ // get a target surface with the same texture_id as a currently active surface.
+ let _prev_surface = self.active_surfaces.insert(texture_id, surface);
+ assert!(_prev_surface.is_none());
+
+ // Store some information about surface allocations if in test mode
+ #[cfg(test)]
+ {
+ graph.surface_count += 1;
+ graph.unique_surfaces.insert(texture_id);
+ }
+
+ // Add the target as a new subpass for this render pass.
+ pass.sub_passes.push(SubPass {
+ surface: SubPassSurface::Dynamic {
+ texture_id,
+ target_kind: kind,
+ used_rect: DeviceIntRect::from_origin_and_size(p, size),
+ },
+ task_ids: vec![*task_id],
+ });
+ }
+
+ // By now, we must have allocated a surface and rect for this task, so assign it!
+ assert!(location.is_some());
+ task.location = RenderTaskLocation::Dynamic {
+ texture_id: location.unwrap().0,
+ rect: DeviceIntRect::from_origin_and_size(location.unwrap().1, size),
+ };
+ }
+ RenderTaskLocation::Existing { parent_task_id, size: existing_size, .. } => {
+ let parent_task_location = graph.tasks[parent_task_id.index as usize].location.clone();
+
+ match parent_task_location {
+ RenderTaskLocation::Unallocated { .. } |
+ RenderTaskLocation::CacheRequest { .. } |
+ RenderTaskLocation::Existing { .. } => {
+ panic!("bug: reference to existing task must be allocated by now");
+ }
+ RenderTaskLocation::Dynamic { texture_id, rect, .. } => {
+ assert_eq!(existing_size, rect.size());
+
+ let kind = graph.tasks[parent_task_id.index as usize].kind.target_kind();
+
+ // A sub-pass is always created in this case, as existing tasks by definition can't be shared.
+ pass.sub_passes.push(SubPass {
+ surface: SubPassSurface::Dynamic {
+ texture_id,
+ target_kind: kind,
+ used_rect: rect, // clear will be skipped due to no-op check anyway
+ },
+ task_ids: vec![*task_id],
+ });
+
+ let task = &mut graph.tasks[task_id.index as usize];
+ task.location = parent_task_location;
+ }
+ RenderTaskLocation::Static { .. } => {
+ unreachable!("bug: not possible since we don't dup static locations");
+ }
+ }
+ }
+ RenderTaskLocation::Static { ref surface, .. } => {
+ // No need to allocate for this surface, since it's a persistent
+ // target. Instead, just create a new sub-pass for it.
+ pass.sub_passes.push(SubPass {
+ surface: SubPassSurface::Persistent {
+ surface: surface.clone(),
+ },
+ task_ids: vec![*task_id],
+ });
+ }
+ RenderTaskLocation::CacheRequest { .. } => {
+ // No need to allocate nor to create a sub-path for read-only locations.
+ }
+ RenderTaskLocation::Dynamic { .. } => {
+ // Dynamic tasks shouldn't be allocated by this point
+ panic!("bug: encountered an already allocated task");
+ }
+ }
+
+ // Return the shared surfaces from this pass
+ let task = &graph.tasks[task_id.index as usize];
+ for child_id in &task.children {
+ let child_task = &graph.tasks[child_id.index as usize];
+ match child_task.location {
+ RenderTaskLocation::Unallocated { .. } |
+ RenderTaskLocation::Existing { .. } => panic!("bug: must be allocated"),
+ RenderTaskLocation::Dynamic { texture_id, .. } => {
+ // If this task can be freed after this pass, include it in the
+ // unique set of textures to be returned to the render target pool below.
+ if child_task.free_after == PassId(pass_id) {
+ self.textures_to_free.insert(texture_id);
+ }
+ }
+ RenderTaskLocation::Static { .. } => {}
+ RenderTaskLocation::CacheRequest { .. } => {}
+ }
+ }
+ }
+
+ // Return no longer used textures to the pool, so that they can be reused / aliased
+ // by later passes.
+ for texture_id in self.textures_to_free.drain() {
+ resource_cache.return_render_target_to_pool(texture_id);
+ self.active_surfaces.remove(&texture_id).unwrap();
+ pass.textures_to_invalidate.push(texture_id);
+ }
+ }
+
+ // By now, all surfaces that were borrowed from the render target pool must
+ // be returned to the resource cache, or we are leaking intermediate surfaces!
+ assert!(self.active_surfaces.is_empty());
+
+ // Each task is now allocated to a surface and target rect. Write that to the
+ // GPU blocks and task_data. After this point, the graph is returned and is
+ // considered to be immutable for the rest of the frame building process.
+
+ for task in &mut graph.tasks {
+ // First check whether the render task texture and uv rects are managed
+ // externally. This is the case for image tasks and cached tasks. In both
+ // cases it results in a finding the information in the texture cache.
+ let cache_item = if let Some(ref cache_handle) = task.cache_handle {
+ Some(resolve_cached_render_task(
+ cache_handle,
+ resource_cache,
+ ))
+ } else if let RenderTaskKind::Image(request) = &task.kind {
+ Some(resolve_image(
+ *request,
+ resource_cache,
+ gpu_cache,
+ deferred_resolves,
+ ))
+ } else {
+ // General case (non-cached non-image tasks).
+ None
+ };
+
+ if let Some(cache_item) = cache_item {
+ // Update the render task even if the item is invalid.
+ // We'll handle it later and it's easier to not have to
+ // deal with unexpected location variants like
+ // RenderTaskLocation::CacheRequest when we do.
+ let source = cache_item.texture_id;
+ task.uv_rect_handle = cache_item.uv_rect_handle;
+ task.location = RenderTaskLocation::Static {
+ surface: StaticRenderTaskSurface::ReadOnly { source },
+ rect: cache_item.uv_rect,
+ };
+ }
+ // Give the render task an opportunity to add any
+ // information to the GPU cache, if appropriate.
+ let target_rect = task.get_target_rect();
+
+ task.write_gpu_blocks(
+ target_rect,
+ gpu_cache,
+ );
+
+ graph.task_data.push(
+ task.kind.write_task_data(target_rect)
+ );
+ }
+
+ graph
+ }
+}
+
+impl RenderTaskGraph {
+ /// Print the render task graph to console
+ #[allow(dead_code)]
+ pub fn print(
+ &self,
+ ) {
+ debug!("-- RenderTaskGraph --");
+
+ for (i, task) in self.tasks.iter().enumerate() {
+ debug!("Task {} [{}]: render_on={} free_after={} children={:?}",
+ i,
+ task.kind.as_str(),
+ task.render_on.0,
+ task.free_after.0,
+ task.children,
+ );
+ }
+
+ for (p, pass) in self.passes.iter().enumerate() {
+ debug!("Pass {}:", p);
+
+ for (s, sub_pass) in pass.sub_passes.iter().enumerate() {
+ debug!("\tSubPass {}: {:?}",
+ s,
+ sub_pass.surface,
+ );
+
+ for task_id in &sub_pass.task_ids {
+ debug!("\t\tTask {:?}", task_id.index);
+ }
+ }
+ }
+ }
+
+ pub fn resolve_texture(
+ &self,
+ task_id: impl Into<Option<RenderTaskId>>,
+ ) -> Option<TextureSource> {
+ let task_id = task_id.into()?;
+ let task = &self[task_id];
+
+ match task.get_texture_source() {
+ TextureSource::Invalid => None,
+ source => Some(source),
+ }
+ }
+
+ pub fn resolve_location(
+ &self,
+ task_id: impl Into<Option<RenderTaskId>>,
+ gpu_cache: &GpuCache,
+ ) -> Option<(GpuCacheAddress, TextureSource)> {
+ self.resolve_impl(task_id.into()?, gpu_cache)
+ }
+
+ fn resolve_impl(
+ &self,
+ task_id: RenderTaskId,
+ gpu_cache: &GpuCache,
+ ) -> Option<(GpuCacheAddress, TextureSource)> {
+ let task = &self[task_id];
+ let texture_source = task.get_texture_source();
+
+ if let TextureSource::Invalid = texture_source {
+ return None;
+ }
+
+ let uv_address = task.get_texture_address(gpu_cache);
+
+ Some((uv_address, texture_source))
+ }
+
+
+ #[cfg(test)]
+ pub fn new_for_testing() -> Self {
+ RenderTaskGraph {
+ tasks: Vec::new(),
+ passes: Vec::new(),
+ frame_id: FrameId::INVALID,
+ task_data: Vec::new(),
+ surface_count: 0,
+ unique_surfaces: FastHashSet::default(),
+ }
+ }
+
+ /// Return the surface and texture counts, used for testing
+ #[cfg(test)]
+ pub fn surface_counts(&self) -> (usize, usize) {
+ (self.surface_count, self.unique_surfaces.len())
+ }
+
+ /// Return current frame id, used for validation
+ #[cfg(debug_assertions)]
+ pub fn frame_id(&self) -> FrameId {
+ self.frame_id
+ }
+}
+
+/// Batching uses index access to read information about tasks
+impl std::ops::Index<RenderTaskId> for RenderTaskGraph {
+ type Output = RenderTask;
+ fn index(&self, id: RenderTaskId) -> &RenderTask {
+ &self.tasks[id.index as usize]
+ }
+}
+
+fn assign_free_pass(
+ id: RenderTaskId,
+ graph: &mut RenderTaskGraph,
+) {
+ let task = &mut graph.tasks[id.index as usize];
+ let render_on = task.render_on;
+
+ let mut child_task_ids: SmallVec<[RenderTaskId; 8]> = SmallVec::new();
+ child_task_ids.extend_from_slice(&task.children);
+
+ for child_id in child_task_ids {
+ let child_location = graph.tasks[child_id.index as usize].location.clone();
+
+ // Each dynamic child task can free its backing surface after the last
+ // task that references it as an input. Using min here ensures the
+ // safe time to free this surface in the presence of multiple paths
+ // to this task from the root(s).
+ match child_location {
+ RenderTaskLocation::CacheRequest { .. } => {}
+ RenderTaskLocation::Static { .. } => {
+ // never get freed anyway, so can leave untouched
+ // (could validate that they remain at PassId::MIN)
+ }
+ RenderTaskLocation::Dynamic { .. } => {
+ panic!("bug: should not be allocated yet");
+ }
+ RenderTaskLocation::Unallocated { .. } => {
+ let child_task = &mut graph.tasks[child_id.index as usize];
+
+ if child_task.free_after != PassId::INVALID {
+ child_task.free_after = child_task.free_after.min(render_on);
+ }
+ }
+ RenderTaskLocation::Existing { parent_task_id, .. } => {
+ let parent_task = &mut graph.tasks[parent_task_id.index as usize];
+ parent_task.free_after = PassId::INVALID;
+
+ let child_task = &mut graph.tasks[child_id.index as usize];
+
+ if child_task.free_after != PassId::INVALID {
+ child_task.free_after = child_task.free_after.min(render_on);
+ }
+ }
+ }
+ }
+}
+
+/// A render pass represents a set of rendering operations that don't depend on one
+/// another.
+///
+/// A render pass can have several render targets if there wasn't enough space in one
+/// target to do all of the rendering for that pass. See `RenderTargetList`.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RenderPass {
+ /// The subpasses that describe targets being rendered to in this pass
+ pub alpha: RenderTargetList<AlphaRenderTarget>,
+ pub color: RenderTargetList<ColorRenderTarget>,
+ pub texture_cache: FastHashMap<CacheTextureId, TextureCacheRenderTarget>,
+ pub picture_cache: Vec<PictureCacheTarget>,
+ pub textures_to_invalidate: Vec<CacheTextureId>,
+}
+
+impl RenderPass {
+ /// Creates an intermediate off-screen pass.
+ pub fn new(src: &Pass) -> Self {
+ RenderPass {
+ color: RenderTargetList::new(
+ ImageFormat::RGBA8,
+ ),
+ alpha: RenderTargetList::new(
+ ImageFormat::R8,
+ ),
+ texture_cache: FastHashMap::default(),
+ picture_cache: Vec::new(),
+ textures_to_invalidate: src.textures_to_invalidate.clone(),
+ }
+ }
+}
+
+// Dump an SVG visualization of the render graph for debugging purposes
+#[cfg(feature = "capture")]
+pub fn dump_render_tasks_as_svg(
+ render_tasks: &RenderTaskGraph,
+ output: &mut dyn std::io::Write,
+) -> std::io::Result<()> {
+ use svg_fmt::*;
+
+ let node_width = 80.0;
+ let node_height = 30.0;
+ let vertical_spacing = 8.0;
+ let horizontal_spacing = 20.0;
+ let margin = 10.0;
+ let text_size = 10.0;
+
+ let mut pass_rects = Vec::new();
+ let mut nodes = vec![None; render_tasks.tasks.len()];
+
+ let mut x = margin;
+ let mut max_y: f32 = 0.0;
+
+ #[derive(Clone)]
+ struct Node {
+ rect: Rectangle,
+ label: Text,
+ size: Text,
+ }
+
+ for pass in render_tasks.passes.iter().rev() {
+ let mut layout = VerticalLayout::new(x, margin, node_width);
+
+ for task_id in &pass.task_ids {
+ let task_index = task_id.index as usize;
+ let task = &render_tasks.tasks[task_index];
+
+ let rect = layout.push_rectangle(node_height);
+
+ let tx = rect.x + rect.w / 2.0;
+ let ty = rect.y + 10.0;
+
+ let label = text(tx, ty, format!("{}", task.kind.as_str()));
+ let size = text(tx, ty + 12.0, format!("{:?}", task.location.size()));
+
+ nodes[task_index] = Some(Node { rect, label, size });
+
+ layout.advance(vertical_spacing);
+ }
+
+ pass_rects.push(layout.total_rectangle());
+
+ x += node_width + horizontal_spacing;
+ max_y = max_y.max(layout.y + margin);
+ }
+
+ let mut links = Vec::new();
+ for node_index in 0..nodes.len() {
+ if nodes[node_index].is_none() {
+ continue;
+ }
+
+ let task = &render_tasks.tasks[node_index];
+ for dep in &task.children {
+ let dep_index = dep.index as usize;
+
+ if let (&Some(ref node), &Some(ref dep_node)) = (&nodes[node_index], &nodes[dep_index]) {
+ links.push((
+ dep_node.rect.x + dep_node.rect.w,
+ dep_node.rect.y + dep_node.rect.h / 2.0,
+ node.rect.x,
+ node.rect.y + node.rect.h / 2.0,
+ ));
+ }
+ }
+ }
+
+ let svg_w = x + margin;
+ let svg_h = max_y + margin;
+ writeln!(output, "{}", BeginSvg { w: svg_w, h: svg_h })?;
+
+ // Background.
+ writeln!(output,
+ " {}",
+ rectangle(0.0, 0.0, svg_w, svg_h)
+ .inflate(1.0, 1.0)
+ .fill(rgb(50, 50, 50))
+ )?;
+
+ // Passes.
+ for rect in pass_rects {
+ writeln!(output,
+ " {}",
+ rect.inflate(3.0, 3.0)
+ .border_radius(4.0)
+ .opacity(0.4)
+ .fill(black())
+ )?;
+ }
+
+ // Links.
+ for (x1, y1, x2, y2) in links {
+ dump_task_dependency_link(output, x1, y1, x2, y2);
+ }
+
+ // Tasks.
+ for node in &nodes {
+ if let Some(node) = node {
+ writeln!(output,
+ " {}",
+ node.rect
+ .clone()
+ .fill(black())
+ .border_radius(3.0)
+ .opacity(0.5)
+ .offset(0.0, 2.0)
+ )?;
+ writeln!(output,
+ " {}",
+ node.rect
+ .clone()
+ .fill(rgb(200, 200, 200))
+ .border_radius(3.0)
+ .opacity(0.8)
+ )?;
+
+ writeln!(output,
+ " {}",
+ node.label
+ .clone()
+ .size(text_size)
+ .align(Align::Center)
+ .color(rgb(50, 50, 50))
+ )?;
+ writeln!(output,
+ " {}",
+ node.size
+ .clone()
+ .size(text_size * 0.7)
+ .align(Align::Center)
+ .color(rgb(50, 50, 50))
+ )?;
+ }
+ }
+
+ writeln!(output, "{}", EndSvg)
+}
+
+#[allow(dead_code)]
+fn dump_task_dependency_link(
+ output: &mut dyn std::io::Write,
+ x1: f32, y1: f32,
+ x2: f32, y2: f32,
+) {
+ use svg_fmt::*;
+
+ // If the link is a straight horizontal line and spans over multiple passes, it
+ // is likely to go straight though unrelated nodes in a way that makes it look like
+ // they are connected, so we bend the line upward a bit to avoid that.
+ let simple_path = (y1 - y2).abs() > 1.0 || (x2 - x1) < 45.0;
+
+ let mid_x = (x1 + x2) / 2.0;
+ if simple_path {
+ write!(output, " {}",
+ path().move_to(x1, y1)
+ .cubic_bezier_to(mid_x, y1, mid_x, y2, x2, y2)
+ .fill(Fill::None)
+ .stroke(Stroke::Color(rgb(100, 100, 100), 3.0))
+ ).unwrap();
+ } else {
+ let ctrl1_x = (mid_x + x1) / 2.0;
+ let ctrl2_x = (mid_x + x2) / 2.0;
+ let ctrl_y = y1 - 25.0;
+ write!(output, " {}",
+ path().move_to(x1, y1)
+ .cubic_bezier_to(ctrl1_x, y1, ctrl1_x, ctrl_y, mid_x, ctrl_y)
+ .cubic_bezier_to(ctrl2_x, ctrl_y, ctrl2_x, y2, x2, y2)
+ .fill(Fill::None)
+ .stroke(Stroke::Color(rgb(100, 100, 100), 3.0))
+ ).unwrap();
+ }
+}
+
+/// Construct a picture cache render task location for testing
+#[cfg(test)]
+fn pc_target(
+ surface_id: u64,
+ tile_x: i32,
+ tile_y: i32,
+) -> RenderTaskLocation {
+ use crate::{
+ composite::{NativeSurfaceId, NativeTileId},
+ picture::ResolvedSurfaceTexture,
+ };
+
+ let width = 512;
+ let height = 512;
+
+ RenderTaskLocation::Static {
+ surface: StaticRenderTaskSurface::PictureCache {
+ surface: ResolvedSurfaceTexture::Native {
+ id: NativeTileId {
+ surface_id: NativeSurfaceId(surface_id),
+ x: tile_x,
+ y: tile_y,
+ },
+ size: DeviceIntSize::new(width, height),
+ },
+ },
+ rect: DeviceIntSize::new(width, height).into(),
+ }
+}
+
+#[cfg(test)]
+impl RenderTaskGraphBuilder {
+ fn test_expect(
+ mut self,
+ pass_count: usize,
+ total_surface_count: usize,
+ unique_surfaces: &[(i32, i32, ImageFormat)],
+ ) {
+ use crate::internal_types::FrameStamp;
+ use api::{DocumentId, IdNamespace};
+
+ let mut rc = ResourceCache::new_for_testing();
+ let mut gc = GpuCache::new();
+
+ let mut frame_stamp = FrameStamp::first(DocumentId::new(IdNamespace(1), 1));
+ frame_stamp.advance();
+ gc.prepare_for_frames();
+ gc.begin_frame(frame_stamp);
+
+ let g = self.end_frame(&mut rc, &mut gc, &mut Vec::new(), 2048);
+ g.print();
+
+ assert_eq!(g.passes.len(), pass_count);
+ assert_eq!(g.surface_counts(), (total_surface_count, unique_surfaces.len()));
+
+ rc.validate_surfaces(unique_surfaces);
+ }
+}
+
+/// Construct a testing render task with given location
+#[cfg(test)]
+fn task_location(location: RenderTaskLocation) -> RenderTask {
+ RenderTask::new_test(
+ location,
+ RenderTargetKind::Color,
+ )
+}
+
+/// Construct a dynamic render task location for testing
+#[cfg(test)]
+fn task_dynamic(size: i32) -> RenderTask {
+ RenderTask::new_test(
+ RenderTaskLocation::Unallocated { size: DeviceIntSize::new(size, size) },
+ RenderTargetKind::Color,
+ )
+}
+
+#[test]
+fn fg_test_1() {
+ // Test that a root target can be used as an input for readbacks
+ // This functionality isn't currently used, but will be in future.
+
+ let mut gb = RenderTaskGraphBuilder::new();
+
+ let root_target = pc_target(0, 0, 0);
+
+ let root = gb.add().init(task_location(root_target.clone()));
+
+ let readback = gb.add().init(task_dynamic(100));
+ gb.add_dependency(readback, root);
+
+ let mix_blend_content = gb.add().init(task_dynamic(50));
+
+ let content = gb.add().init(task_location(root_target));
+ gb.add_dependency(content, readback);
+ gb.add_dependency(content, mix_blend_content);
+
+ gb.test_expect(3, 1, &[
+ (2048, 2048, ImageFormat::RGBA8),
+ ]);
+}
+
+#[test]
+fn fg_test_3() {
+ // Test that small targets are allocated in a shared surface, and that large
+ // tasks are allocated in a rounded up texture size.
+
+ let mut gb = RenderTaskGraphBuilder::new();
+
+ let pc_root = gb.add().init(task_location(pc_target(0, 0, 0)));
+
+ let child_pic_0 = gb.add().init(task_dynamic(128));
+ let child_pic_1 = gb.add().init(task_dynamic(3000));
+
+ gb.add_dependency(pc_root, child_pic_0);
+ gb.add_dependency(pc_root, child_pic_1);
+
+ gb.test_expect(2, 2, &[
+ (2048, 2048, ImageFormat::RGBA8),
+ (3072, 3072, ImageFormat::RGBA8),
+ ]);
+}
+
+#[test]
+fn fg_test_4() {
+ // Test that for a simple dependency chain of tasks, that render
+ // target surfaces are aliased and reused between passes where possible.
+
+ let mut gb = RenderTaskGraphBuilder::new();
+
+ let pc_root = gb.add().init(task_location(pc_target(0, 0, 0)));
+
+ let child_pic_0 = gb.add().init(task_dynamic(128));
+ let child_pic_1 = gb.add().init(task_dynamic(128));
+ let child_pic_2 = gb.add().init(task_dynamic(128));
+
+ gb.add_dependency(pc_root, child_pic_0);
+ gb.add_dependency(child_pic_0, child_pic_1);
+ gb.add_dependency(child_pic_1, child_pic_2);
+
+ gb.test_expect(4, 3, &[
+ (2048, 2048, ImageFormat::RGBA8),
+ (2048, 2048, ImageFormat::RGBA8),
+ ]);
+}
+
+#[test]
+fn fg_test_5() {
+ // Test that a task that is used as an input by direct parent and also
+ // distance ancestor are scheduled correctly, and allocates the correct
+ // number of passes, taking advantage of surface reuse / aliasing where feasible.
+
+ let mut gb = RenderTaskGraphBuilder::new();
+
+ let pc_root = gb.add().init(task_location(pc_target(0, 0, 0)));
+
+ let child_pic_0 = gb.add().init(task_dynamic(128));
+ let child_pic_1 = gb.add().init(task_dynamic(64));
+ let child_pic_2 = gb.add().init(task_dynamic(32));
+ let child_pic_3 = gb.add().init(task_dynamic(16));
+
+ gb.add_dependency(pc_root, child_pic_0);
+ gb.add_dependency(child_pic_0, child_pic_1);
+ gb.add_dependency(child_pic_1, child_pic_2);
+ gb.add_dependency(child_pic_2, child_pic_3);
+ gb.add_dependency(pc_root, child_pic_3);
+
+ gb.test_expect(5, 4, &[
+ (2048, 2048, ImageFormat::RGBA8),
+ (2048, 2048, ImageFormat::RGBA8),
+ (2048, 2048, ImageFormat::RGBA8),
+ ]);
+}
+
+#[test]
+fn fg_test_6() {
+ // Test that a task that is used as an input dependency by two parent
+ // tasks is correctly allocated and freed.
+
+ let mut gb = RenderTaskGraphBuilder::new();
+
+ let pc_root_1 = gb.add().init(task_location(pc_target(0, 0, 0)));
+ let pc_root_2 = gb.add().init(task_location(pc_target(0, 1, 0)));
+
+ let child_pic = gb.add().init(task_dynamic(128));
+
+ gb.add_dependency(pc_root_1, child_pic);
+ gb.add_dependency(pc_root_2, child_pic);
+
+ gb.test_expect(2, 1, &[
+ (2048, 2048, ImageFormat::RGBA8),
+ ]);
+}
+
+#[test]
+fn fg_test_7() {
+ // Test that a standalone surface is not incorrectly used to
+ // allocate subsequent shared task rects.
+
+ let mut gb = RenderTaskGraphBuilder::new();
+
+ let pc_root = gb.add().init(task_location(pc_target(0, 0, 0)));
+
+ let child0 = gb.add().init(task_dynamic(16));
+ let child1 = gb.add().init(task_dynamic(16));
+
+ let child2 = gb.add().init(task_dynamic(16));
+ let child3 = gb.add().init(task_dynamic(16));
+
+ gb.add_dependency(pc_root, child0);
+ gb.add_dependency(child0, child1);
+ gb.add_dependency(pc_root, child1);
+
+ gb.add_dependency(pc_root, child2);
+ gb.add_dependency(child2, child3);
+
+ gb.test_expect(3, 3, &[
+ (2048, 2048, ImageFormat::RGBA8),
+ (2048, 2048, ImageFormat::RGBA8),
+ (2048, 2048, ImageFormat::RGBA8),
+ ]);
+}
diff --git a/gfx/wr/webrender/src/renderer/debug.rs b/gfx/wr/webrender/src/renderer/debug.rs
new file mode 100644
index 0000000000..7e16d15d76
--- /dev/null
+++ b/gfx/wr/webrender/src/renderer/debug.rs
@@ -0,0 +1,415 @@
+/* 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 api::{ColorU, ImageFormat, ImageBufferKind};
+use api::units::*;
+use crate::debug_font_data;
+use crate::device::{Device, Program, Texture, TextureSlot, VertexDescriptor, ShaderError, VAO};
+use crate::device::{TextureFilter, VertexAttribute, VertexAttributeKind, VertexUsageHint};
+use euclid::{Point2D, Rect, Size2D, Transform3D, default};
+use crate::internal_types::Swizzle;
+use std::f32;
+
+#[derive(Debug, Copy, Clone)]
+enum DebugSampler {
+ Font,
+}
+
+impl Into<TextureSlot> for DebugSampler {
+ fn into(self) -> TextureSlot {
+ match self {
+ DebugSampler::Font => TextureSlot(0),
+ }
+ }
+}
+
+const DESC_FONT: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[
+ VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aColor",
+ count: 4,
+ kind: VertexAttributeKind::U8Norm,
+ },
+ VertexAttribute {
+ name: "aColorTexCoord",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ ],
+ instance_attributes: &[],
+};
+
+const DESC_COLOR: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[
+ VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aColor",
+ count: 4,
+ kind: VertexAttributeKind::U8Norm,
+ },
+ ],
+ instance_attributes: &[],
+};
+
+#[repr(C)]
+pub struct DebugFontVertex {
+ pub x: f32,
+ pub y: f32,
+ pub color: ColorU,
+ pub u: f32,
+ pub v: f32,
+}
+
+impl DebugFontVertex {
+ pub fn new(x: f32, y: f32, u: f32, v: f32, color: ColorU) -> DebugFontVertex {
+ DebugFontVertex { x, y, color, u, v }
+ }
+}
+
+#[repr(C)]
+pub struct DebugColorVertex {
+ pub x: f32,
+ pub y: f32,
+ pub color: ColorU,
+}
+
+impl DebugColorVertex {
+ pub fn new(x: f32, y: f32, color: ColorU) -> DebugColorVertex {
+ DebugColorVertex { x, y, color }
+ }
+}
+
+pub struct DebugRenderer {
+ font_vertices: Vec<DebugFontVertex>,
+ font_indices: Vec<u32>,
+ font_program: Program,
+ font_vao: VAO,
+ font_texture: Texture,
+
+ tri_vertices: Vec<DebugColorVertex>,
+ tri_indices: Vec<u32>,
+ tri_vao: VAO,
+ line_vertices: Vec<DebugColorVertex>,
+ line_vao: VAO,
+ color_program: Program,
+}
+
+impl DebugRenderer {
+ pub fn new(device: &mut Device) -> Result<Self, ShaderError> {
+ let font_program = device.create_program_linked(
+ "debug_font",
+ &[],
+ &DESC_FONT,
+ )?;
+ device.bind_program(&font_program);
+ device.bind_shader_samplers(&font_program, &[("sColor0", DebugSampler::Font)]);
+
+ let color_program = device.create_program_linked(
+ "debug_color",
+ &[],
+ &DESC_COLOR,
+ )?;
+
+ let font_vao = device.create_vao(&DESC_FONT, 1);
+ let line_vao = device.create_vao(&DESC_COLOR, 1);
+ let tri_vao = device.create_vao(&DESC_COLOR, 1);
+
+ let font_texture = device.create_texture(
+ ImageBufferKind::Texture2D,
+ ImageFormat::R8,
+ debug_font_data::BMP_WIDTH,
+ debug_font_data::BMP_HEIGHT,
+ TextureFilter::Linear,
+ None,
+ );
+ device.upload_texture_immediate(
+ &font_texture,
+ &debug_font_data::FONT_BITMAP
+ );
+
+ Ok(DebugRenderer {
+ font_vertices: Vec::new(),
+ font_indices: Vec::new(),
+ line_vertices: Vec::new(),
+ tri_vao,
+ tri_vertices: Vec::new(),
+ tri_indices: Vec::new(),
+ font_program,
+ color_program,
+ font_vao,
+ line_vao,
+ font_texture,
+ })
+ }
+
+ pub fn deinit(self, device: &mut Device) {
+ device.delete_texture(self.font_texture);
+ device.delete_program(self.font_program);
+ device.delete_program(self.color_program);
+ device.delete_vao(self.tri_vao);
+ device.delete_vao(self.line_vao);
+ device.delete_vao(self.font_vao);
+ }
+
+ pub fn line_height(&self) -> f32 {
+ debug_font_data::FONT_SIZE as f32 * 1.1
+ }
+
+ /// Draws a line of text at the provided starting coordinates.
+ ///
+ /// If |bounds| is specified, glyphs outside the bounds are discarded.
+ ///
+ /// Y-coordinates is relative to screen top, along with everything else in
+ /// this file.
+ pub fn add_text(
+ &mut self,
+ x: f32,
+ y: f32,
+ text: &str,
+ color: ColorU,
+ bounds: Option<DeviceRect>,
+ ) -> default::Rect<f32> {
+ let mut x_start = x;
+ let ipw = 1.0 / debug_font_data::BMP_WIDTH as f32;
+ let iph = 1.0 / debug_font_data::BMP_HEIGHT as f32;
+
+ let mut min_x = f32::MAX;
+ let mut max_x = -f32::MAX;
+ let mut min_y = f32::MAX;
+ let mut max_y = -f32::MAX;
+
+ for c in text.chars() {
+ let c = c as usize - debug_font_data::FIRST_GLYPH_INDEX as usize;
+ if c < debug_font_data::GLYPHS.len() {
+ let glyph = &debug_font_data::GLYPHS[c];
+
+ let x0 = (x_start + glyph.xo + 0.5).floor();
+ let y0 = (y + glyph.yo + 0.5).floor();
+
+ let x1 = x0 + glyph.x1 as f32 - glyph.x0 as f32;
+ let y1 = y0 + glyph.y1 as f32 - glyph.y0 as f32;
+
+ // If either corner of the glyph will end up out of bounds, drop it.
+ if let Some(b) = bounds {
+ let rect = DeviceRect {
+ min: DevicePoint::new(x0, y0),
+ max: DevicePoint::new(x1, y1),
+ };
+ if !b.contains_box(&rect) {
+ continue;
+ }
+ }
+
+ let s0 = glyph.x0 as f32 * ipw;
+ let t0 = glyph.y0 as f32 * iph;
+ let s1 = glyph.x1 as f32 * ipw;
+ let t1 = glyph.y1 as f32 * iph;
+
+ x_start += glyph.xa;
+
+ let vertex_count = self.font_vertices.len() as u32;
+
+ self.font_vertices
+ .push(DebugFontVertex::new(x0, y0, s0, t0, color));
+ self.font_vertices
+ .push(DebugFontVertex::new(x1, y0, s1, t0, color));
+ self.font_vertices
+ .push(DebugFontVertex::new(x0, y1, s0, t1, color));
+ self.font_vertices
+ .push(DebugFontVertex::new(x1, y1, s1, t1, color));
+
+ self.font_indices.push(vertex_count + 0);
+ self.font_indices.push(vertex_count + 1);
+ self.font_indices.push(vertex_count + 2);
+ self.font_indices.push(vertex_count + 2);
+ self.font_indices.push(vertex_count + 1);
+ self.font_indices.push(vertex_count + 3);
+
+ min_x = min_x.min(x0);
+ max_x = max_x.max(x1);
+ min_y = min_y.min(y0);
+ max_y = max_y.max(y1);
+ }
+ }
+
+ Rect::new(
+ Point2D::new(min_x, min_y),
+ Size2D::new(max_x - min_x, max_y - min_y),
+ )
+ }
+
+ pub fn add_quad(
+ &mut self,
+ x0: f32,
+ y0: f32,
+ x1: f32,
+ y1: f32,
+ color_top: ColorU,
+ color_bottom: ColorU,
+ ) {
+ let vertex_count = self.tri_vertices.len() as u32;
+
+ self.tri_vertices
+ .push(DebugColorVertex::new(x0, y0, color_top));
+ self.tri_vertices
+ .push(DebugColorVertex::new(x1, y0, color_top));
+ self.tri_vertices
+ .push(DebugColorVertex::new(x0, y1, color_bottom));
+ self.tri_vertices
+ .push(DebugColorVertex::new(x1, y1, color_bottom));
+
+ self.tri_indices.push(vertex_count + 0);
+ self.tri_indices.push(vertex_count + 1);
+ self.tri_indices.push(vertex_count + 2);
+ self.tri_indices.push(vertex_count + 2);
+ self.tri_indices.push(vertex_count + 1);
+ self.tri_indices.push(vertex_count + 3);
+ }
+
+ #[allow(dead_code)]
+ pub fn add_line(&mut self, x0: i32, y0: i32, color0: ColorU, x1: i32, y1: i32, color1: ColorU) {
+ self.line_vertices
+ .push(DebugColorVertex::new(x0 as f32, y0 as f32, color0));
+ self.line_vertices
+ .push(DebugColorVertex::new(x1 as f32, y1 as f32, color1));
+ }
+
+
+ pub fn add_rect(&mut self, rect: &DeviceIntRect, color: ColorU) {
+ let p0 = rect.min;
+ let p1 = rect.max;
+ self.add_line(p0.x, p0.y, color, p1.x, p0.y, color);
+ self.add_line(p1.x, p0.y, color, p1.x, p1.y, color);
+ self.add_line(p1.x, p1.y, color, p0.x, p1.y, color);
+ self.add_line(p0.x, p1.y, color, p0.x, p0.y, color);
+ }
+
+ pub fn render(
+ &mut self,
+ device: &mut Device,
+ viewport_size: Option<DeviceIntSize>,
+ scale: f32,
+ surface_origin_is_top_left: bool,
+ ) {
+ if let Some(viewport_size) = viewport_size {
+ device.disable_depth();
+ device.set_blend(true);
+ device.set_blend_mode_premultiplied_alpha();
+
+ let (bottom, top) = if surface_origin_is_top_left {
+ (0.0, viewport_size.height as f32 * scale)
+ } else {
+ (viewport_size.height as f32 * scale, 0.0)
+ };
+
+ let projection = Transform3D::ortho(
+ 0.0,
+ viewport_size.width as f32 * scale,
+ bottom,
+ top,
+ device.ortho_near_plane(),
+ device.ortho_far_plane(),
+ );
+
+ // Triangles
+ if !self.tri_vertices.is_empty() {
+ device.bind_program(&self.color_program);
+ device.set_uniforms(&self.color_program, &projection);
+ device.bind_vao(&self.tri_vao);
+ device.update_vao_indices(&self.tri_vao, &self.tri_indices, VertexUsageHint::Dynamic);
+ device.update_vao_main_vertices(
+ &self.tri_vao,
+ &self.tri_vertices,
+ VertexUsageHint::Dynamic,
+ );
+ device.draw_triangles_u32(0, self.tri_indices.len() as i32);
+ }
+
+ // Lines
+ if !self.line_vertices.is_empty() {
+ device.bind_program(&self.color_program);
+ device.set_uniforms(&self.color_program, &projection);
+ device.bind_vao(&self.line_vao);
+ device.update_vao_main_vertices(
+ &self.line_vao,
+ &self.line_vertices,
+ VertexUsageHint::Dynamic,
+ );
+ device.draw_nonindexed_lines(0, self.line_vertices.len() as i32);
+ }
+
+ // Glyph
+ if !self.font_indices.is_empty() {
+ device.bind_program(&self.font_program);
+ device.set_uniforms(&self.font_program, &projection);
+ device.bind_texture(DebugSampler::Font, &self.font_texture, Swizzle::default());
+ device.bind_vao(&self.font_vao);
+ device.update_vao_indices(&self.font_vao, &self.font_indices, VertexUsageHint::Dynamic);
+ device.update_vao_main_vertices(
+ &self.font_vao,
+ &self.font_vertices,
+ VertexUsageHint::Dynamic,
+ );
+ device.draw_triangles_u32(0, self.font_indices.len() as i32);
+ }
+ }
+
+ self.font_indices.clear();
+ self.font_vertices.clear();
+ self.line_vertices.clear();
+ self.tri_vertices.clear();
+ self.tri_indices.clear();
+ }
+}
+
+pub struct LazyInitializedDebugRenderer {
+ debug_renderer: Option<DebugRenderer>,
+ failed: bool,
+}
+
+impl LazyInitializedDebugRenderer {
+ pub fn new() -> Self {
+ Self {
+ debug_renderer: None,
+ failed: false,
+ }
+ }
+
+ pub fn get_mut<'a>(&'a mut self, device: &mut Device) -> Option<&'a mut DebugRenderer> {
+ if self.failed {
+ return None;
+ }
+ if self.debug_renderer.is_none() {
+ match DebugRenderer::new(device) {
+ Ok(renderer) => { self.debug_renderer = Some(renderer); }
+ Err(_) => {
+ // The shader compilation code already logs errors.
+ self.failed = true;
+ }
+ }
+ }
+
+ self.debug_renderer.as_mut()
+ }
+
+ /// Returns mut ref to `debug::DebugRenderer` if one already exists, otherwise returns `None`.
+ pub fn try_get_mut<'a>(&'a mut self) -> Option<&'a mut DebugRenderer> {
+ self.debug_renderer.as_mut()
+ }
+
+ pub fn deinit(self, device: &mut Device) {
+ if let Some(debug_renderer) = self.debug_renderer {
+ debug_renderer.deinit(device);
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/renderer/gpu_buffer.rs b/gfx/wr/webrender/src/renderer/gpu_buffer.rs
new file mode 100644
index 0000000000..05543d51ee
--- /dev/null
+++ b/gfx/wr/webrender/src/renderer/gpu_buffer.rs
@@ -0,0 +1,326 @@
+/* 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/. */
+
+/*
+
+ TODO:
+ Recycle GpuBuffers in a pool (support return from render thread)
+ Efficiently allow writing to buffer (better push interface)
+ Support other texel types (e.g. i32)
+
+ */
+
+use crate::renderer::MAX_VERTEX_TEXTURE_WIDTH;
+use api::units::{DeviceIntRect, DeviceIntSize, LayoutRect, PictureRect, DeviceRect};
+use api::{PremultipliedColorF};
+use crate::device::Texel;
+use crate::render_task_graph::{RenderTaskGraph, RenderTaskId};
+
+
+unsafe impl Texel for GpuBufferBlock {}
+
+/// A single texel in RGBAF32 texture - 16 bytes.
+#[derive(Copy, Clone, Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct GpuBufferBlock {
+ data: [f32; 4],
+}
+
+#[derive(Copy, Debug, Clone, MallocSizeOf, Eq, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct GpuBufferAddress {
+ pub u: u16,
+ pub v: u16,
+}
+
+impl GpuBufferAddress {
+ #[allow(dead_code)]
+ pub fn as_int(self) -> i32 {
+ // TODO(gw): Temporarily encode GPU Cache addresses as a single int.
+ // In the future, we can change the PrimitiveInstanceData struct
+ // to use 2x u16 for the vertex attribute instead of an i32.
+ self.v as i32 * MAX_VERTEX_TEXTURE_WIDTH as i32 + self.u as i32
+ }
+}
+
+impl GpuBufferBlock {
+ pub const EMPTY: Self = GpuBufferBlock { data: [0.0; 4] };
+}
+
+impl Into<GpuBufferBlock> for LayoutRect {
+ fn into(self) -> GpuBufferBlock {
+ GpuBufferBlock {
+ data: [
+ self.min.x,
+ self.min.y,
+ self.max.x,
+ self.max.y,
+ ],
+ }
+ }
+}
+
+impl Into<GpuBufferBlock> for PictureRect {
+ fn into(self) -> GpuBufferBlock {
+ GpuBufferBlock {
+ data: [
+ self.min.x,
+ self.min.y,
+ self.max.x,
+ self.max.y,
+ ],
+ }
+ }
+}
+
+impl Into<GpuBufferBlock> for DeviceRect {
+ fn into(self) -> GpuBufferBlock {
+ GpuBufferBlock {
+ data: [
+ self.min.x,
+ self.min.y,
+ self.max.x,
+ self.max.y,
+ ],
+ }
+ }
+}
+
+impl Into<GpuBufferBlock> for PremultipliedColorF {
+ fn into(self) -> GpuBufferBlock {
+ GpuBufferBlock {
+ data: [
+ self.r,
+ self.g,
+ self.b,
+ self.a,
+ ],
+ }
+ }
+}
+
+impl Into<GpuBufferBlock> for DeviceIntRect {
+ fn into(self) -> GpuBufferBlock {
+ GpuBufferBlock {
+ data: [
+ self.min.x as f32,
+ self.min.y as f32,
+ self.max.x as f32,
+ self.max.y as f32,
+ ],
+ }
+ }
+}
+
+impl Into<GpuBufferBlock> for [f32; 4] {
+ fn into(self) -> GpuBufferBlock {
+ GpuBufferBlock {
+ data: self,
+ }
+ }
+}
+
+/// Record a patch to the GPU buffer for a render task
+struct DeferredBlock {
+ task_id: RenderTaskId,
+ index: usize,
+}
+
+/// Interface to allow writing multiple GPU blocks, possibly of different types
+pub struct GpuBufferWriter<'a> {
+ buffer: &'a mut Vec<GpuBufferBlock>,
+ deferred: &'a mut Vec<DeferredBlock>,
+ index: usize,
+ block_count: usize,
+}
+
+impl<'a> GpuBufferWriter<'a> {
+ fn new(
+ buffer: &'a mut Vec<GpuBufferBlock>,
+ deferred: &'a mut Vec<DeferredBlock>,
+ index: usize,
+ block_count: usize,
+ ) -> Self {
+ GpuBufferWriter {
+ buffer,
+ deferred,
+ index,
+ block_count,
+ }
+ }
+
+ /// Push one (16 byte) block of data in to the writer
+ pub fn push_one<B>(&mut self, block: B) where B: Into<GpuBufferBlock> {
+ self.buffer.push(block.into());
+ }
+
+ /// Push a reference to a render task in to the writer. Once the render
+ /// task graph is resolved, this will be patched with the UV rect of the task
+ pub fn push_render_task(&mut self, task_id: RenderTaskId) {
+ self.deferred.push(DeferredBlock {
+ task_id,
+ index: self.buffer.len(),
+ });
+ self.buffer.push(GpuBufferBlock::EMPTY);
+ }
+
+ /// Close this writer, returning the GPU address of this set of block(s).
+ pub fn finish(self) -> GpuBufferAddress {
+ assert_eq!(self.buffer.len(), self.index + self.block_count);
+
+ GpuBufferAddress {
+ u: (self.index % MAX_VERTEX_TEXTURE_WIDTH) as u16,
+ v: (self.index / MAX_VERTEX_TEXTURE_WIDTH) as u16,
+ }
+ }
+}
+
+impl<'a> Drop for GpuBufferWriter<'a> {
+ fn drop(&mut self) {
+ assert_eq!(self.buffer.len(), self.index + self.block_count, "Claimed block_count was not written");
+ }
+}
+
+pub struct GpuBufferBuilder {
+ data: Vec<GpuBufferBlock>,
+ deferred: Vec<DeferredBlock>,
+}
+
+impl GpuBufferBuilder {
+ pub fn new() -> Self {
+ GpuBufferBuilder {
+ data: Vec::new(),
+ deferred: Vec::new(),
+ }
+ }
+
+ #[allow(dead_code)]
+ pub fn push(
+ &mut self,
+ blocks: &[GpuBufferBlock],
+ ) -> GpuBufferAddress {
+ assert!(blocks.len() <= MAX_VERTEX_TEXTURE_WIDTH);
+
+ if (self.data.len() % MAX_VERTEX_TEXTURE_WIDTH) + blocks.len() > MAX_VERTEX_TEXTURE_WIDTH {
+ while self.data.len() % MAX_VERTEX_TEXTURE_WIDTH != 0 {
+ self.data.push(GpuBufferBlock::EMPTY);
+ }
+ }
+
+ let index = self.data.len();
+
+ self.data.extend_from_slice(blocks);
+
+ GpuBufferAddress {
+ u: (index % MAX_VERTEX_TEXTURE_WIDTH) as u16,
+ v: (index / MAX_VERTEX_TEXTURE_WIDTH) as u16,
+ }
+ }
+
+ /// Begin writing a specific number of blocks
+ pub fn write_blocks(
+ &mut self,
+ block_count: usize,
+ ) -> GpuBufferWriter {
+ assert!(block_count <= MAX_VERTEX_TEXTURE_WIDTH);
+
+ if (self.data.len() % MAX_VERTEX_TEXTURE_WIDTH) + block_count > MAX_VERTEX_TEXTURE_WIDTH {
+ while self.data.len() % MAX_VERTEX_TEXTURE_WIDTH != 0 {
+ self.data.push(GpuBufferBlock::EMPTY);
+ }
+ }
+
+ let index = self.data.len();
+
+ GpuBufferWriter::new(
+ &mut self.data,
+ &mut self.deferred,
+ index,
+ block_count,
+ )
+ }
+
+ pub fn finalize(
+ mut self,
+ render_tasks: &RenderTaskGraph,
+ ) -> GpuBuffer {
+ let required_len = (self.data.len() + MAX_VERTEX_TEXTURE_WIDTH-1) & !(MAX_VERTEX_TEXTURE_WIDTH-1);
+
+ for _ in 0 .. required_len - self.data.len() {
+ self.data.push(GpuBufferBlock::EMPTY);
+ }
+
+ let len = self.data.len();
+ assert!(len % MAX_VERTEX_TEXTURE_WIDTH == 0);
+
+ // At this point, we know that the render task graph has been built, and we can
+ // query the location of any dynamic (render target) or static (texture cache)
+ // task. This allows us to patch the UV rects in to the GPU buffer before upload
+ // to the GPU.
+ for block in self.deferred.drain(..) {
+ let render_task = &render_tasks[block.task_id];
+ let target_rect = render_task.get_target_rect();
+ self.data[block.index] = target_rect.into();
+ }
+
+ GpuBuffer {
+ data: self.data,
+ size: DeviceIntSize::new(MAX_VERTEX_TEXTURE_WIDTH as i32, (len / MAX_VERTEX_TEXTURE_WIDTH) as i32),
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct GpuBuffer {
+ pub data: Vec<GpuBufferBlock>,
+ pub size: DeviceIntSize,
+}
+
+impl GpuBuffer {
+ pub fn is_empty(&self) -> bool {
+ self.data.is_empty()
+ }
+}
+
+
+#[test]
+fn test_gpu_buffer_sizing_push() {
+ let render_task_graph = RenderTaskGraph::new_for_testing();
+ let mut builder = GpuBufferBuilder::new();
+
+ let row = vec![GpuBufferBlock::EMPTY; MAX_VERTEX_TEXTURE_WIDTH];
+ builder.push(&row);
+
+ builder.push(&[GpuBufferBlock::EMPTY]);
+ builder.push(&[GpuBufferBlock::EMPTY]);
+
+ let buffer = builder.finalize(&render_task_graph);
+ assert_eq!(buffer.data.len(), MAX_VERTEX_TEXTURE_WIDTH * 2);
+}
+
+#[test]
+fn test_gpu_buffer_sizing_writer() {
+ let render_task_graph = RenderTaskGraph::new_for_testing();
+ let mut builder = GpuBufferBuilder::new();
+
+ let mut writer = builder.write_blocks(MAX_VERTEX_TEXTURE_WIDTH);
+ for _ in 0 .. MAX_VERTEX_TEXTURE_WIDTH {
+ writer.push_one(GpuBufferBlock::EMPTY);
+ }
+ writer.finish();
+
+ let mut writer = builder.write_blocks(1);
+ writer.push_one(GpuBufferBlock::EMPTY);
+ writer.finish();
+
+ let mut writer = builder.write_blocks(1);
+ writer.push_one(GpuBufferBlock::EMPTY);
+ writer.finish();
+
+ let buffer = builder.finalize(&render_task_graph);
+ assert_eq!(buffer.data.len(), MAX_VERTEX_TEXTURE_WIDTH * 2);
+}
diff --git a/gfx/wr/webrender/src/renderer/gpu_cache.rs b/gfx/wr/webrender/src/renderer/gpu_cache.rs
new file mode 100644
index 0000000000..fde649cb08
--- /dev/null
+++ b/gfx/wr/webrender/src/renderer/gpu_cache.rs
@@ -0,0 +1,525 @@
+/* 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::{cmp, mem};
+use api::units::*;
+use malloc_size_of::MallocSizeOfOps;
+use crate::{
+ device::{CustomVAO, Device, DrawTarget, Program, ReadTarget, Texture, TextureFilter, UploadPBOPool, VBO},
+ gpu_cache::{GpuBlockData, GpuCacheUpdate, GpuCacheUpdateList},
+ internal_types::{RenderTargetInfo, Swizzle},
+ prim_store::DeferredResolve,
+ profiler,
+ render_api::MemoryReport,
+ internal_types::FrameId,
+};
+
+/// Enabling this toggle would force the GPU cache scattered texture to
+/// be resized every frame, which enables GPU debuggers to see if this
+/// is performed correctly.
+const GPU_CACHE_RESIZE_TEST: bool = false;
+
+/// Tracks the state of each row in the GPU cache texture.
+struct CacheRow {
+ /// Mirrored block data on CPU for this row. We store a copy of
+ /// the data on the CPU side to improve upload batching.
+ cpu_blocks: Box<[GpuBlockData; super::MAX_VERTEX_TEXTURE_WIDTH]>,
+ /// The first offset in this row that is dirty.
+ min_dirty: u16,
+ /// The last offset in this row that is dirty.
+ max_dirty: u16,
+}
+
+impl CacheRow {
+ fn new() -> Self {
+ CacheRow {
+ cpu_blocks: Box::new([GpuBlockData::EMPTY; super::MAX_VERTEX_TEXTURE_WIDTH]),
+ min_dirty: super::MAX_VERTEX_TEXTURE_WIDTH as _,
+ max_dirty: 0,
+ }
+ }
+
+ fn is_dirty(&self) -> bool {
+ return self.min_dirty < self.max_dirty;
+ }
+
+ fn clear_dirty(&mut self) {
+ self.min_dirty = super::MAX_VERTEX_TEXTURE_WIDTH as _;
+ self.max_dirty = 0;
+ }
+
+ fn add_dirty(&mut self, block_offset: usize, block_count: usize) {
+ self.min_dirty = self.min_dirty.min(block_offset as _);
+ self.max_dirty = self.max_dirty.max((block_offset + block_count) as _);
+ }
+
+ fn dirty_blocks(&self) -> &[GpuBlockData] {
+ return &self.cpu_blocks[self.min_dirty as usize .. self.max_dirty as usize];
+ }
+}
+
+/// The bus over which CPU and GPU versions of the GPU cache
+/// get synchronized.
+enum GpuCacheBus {
+ /// PBO-based updates, currently operate on a row granularity.
+ /// Therefore, are subject to fragmentation issues.
+ PixelBuffer {
+ /// Per-row data.
+ rows: Vec<CacheRow>,
+ },
+ /// Shader-based scattering updates. Currently rendered by a set
+ /// of points into the GPU texture, each carrying a `GpuBlockData`.
+ Scatter {
+ /// Special program to run the scattered update.
+ program: Program,
+ /// VAO containing the source vertex buffers.
+ vao: CustomVAO,
+ /// VBO for positional data, supplied as normalized `u16`.
+ buf_position: VBO<[u16; 2]>,
+ /// VBO for gpu block data.
+ buf_value: VBO<GpuBlockData>,
+ /// Currently stored block count.
+ count: usize,
+ },
+}
+
+/// The device-specific representation of the cache texture in gpu_cache.rs
+pub struct GpuCacheTexture {
+ texture: Option<Texture>,
+ bus: GpuCacheBus,
+}
+
+impl GpuCacheTexture {
+ /// Ensures that we have an appropriately-sized texture.
+ fn ensure_texture(&mut self, device: &mut Device, height: i32) {
+ // If we already have a texture that works, we're done.
+ if self.texture.as_ref().map_or(false, |t| t.get_dimensions().height >= height) {
+ if GPU_CACHE_RESIZE_TEST {
+ // Special debug mode - resize the texture even though it's fine.
+ } else {
+ return;
+ }
+ }
+
+ // Take the old texture, if any.
+ let blit_source = self.texture.take();
+
+ // Create the new texture.
+ assert!(height >= 2, "Height is too small for ANGLE");
+ let new_size = DeviceIntSize::new(super::MAX_VERTEX_TEXTURE_WIDTH as _, height);
+ // GpuCacheBus::Scatter always requires the texture to be a render target. For
+ // GpuCacheBus::PixelBuffer, we only create the texture with a render target if
+ // RGBAF32 render targets are actually supported, and only if glCopyImageSubData
+ // is not. glCopyImageSubData does not require a render target to copy the texture
+ // data, and if neither RGBAF32 render targets nor glCopyImageSubData is supported,
+ // we simply re-upload the entire contents rather than copying upon resize.
+ let supports_copy_image_sub_data = device.get_capabilities().supports_copy_image_sub_data;
+ let supports_color_buffer_float = device.get_capabilities().supports_color_buffer_float;
+ let rt_info = if matches!(self.bus, GpuCacheBus::PixelBuffer { .. })
+ && (supports_copy_image_sub_data || !supports_color_buffer_float)
+ {
+ None
+ } else {
+ Some(RenderTargetInfo { has_depth: false })
+ };
+ let mut texture = device.create_texture(
+ api::ImageBufferKind::Texture2D,
+ api::ImageFormat::RGBAF32,
+ new_size.width,
+ new_size.height,
+ TextureFilter::Nearest,
+ rt_info,
+ );
+
+ // Copy the contents of the previous texture, if applicable.
+ if let Some(blit_source) = blit_source {
+ if !supports_copy_image_sub_data && !supports_color_buffer_float {
+ // Cannot copy texture, so must re-upload everything.
+ match self.bus {
+ GpuCacheBus::PixelBuffer { ref mut rows } => {
+ for row in rows {
+ row.add_dirty(0, super::MAX_VERTEX_TEXTURE_WIDTH);
+ }
+ }
+ GpuCacheBus::Scatter { .. } => {
+ panic!("Texture must be copyable to use scatter GPU cache bus method");
+ }
+ }
+ } else {
+ device.copy_entire_texture(&mut texture, &blit_source);
+ }
+ device.delete_texture(blit_source);
+ }
+
+ self.texture = Some(texture);
+ }
+
+ pub fn new(device: &mut Device, use_scatter: bool) -> Result<Self, super::RendererError> {
+ use super::desc::GPU_CACHE_UPDATE;
+
+ let bus = if use_scatter {
+ assert!(
+ device.get_capabilities().supports_color_buffer_float,
+ "GpuCache scatter method requires EXT_color_buffer_float",
+ );
+ let program = device.create_program_linked(
+ "gpu_cache_update",
+ &[],
+ &GPU_CACHE_UPDATE,
+ )?;
+ let buf_position = device.create_vbo();
+ let buf_value = device.create_vbo();
+ //Note: the vertex attributes have to be supplied in the same order
+ // as for program creation, but each assigned to a different stream.
+ let vao = device.create_custom_vao(&[
+ buf_position.stream_with(&GPU_CACHE_UPDATE.vertex_attributes[0..1]),
+ buf_value .stream_with(&GPU_CACHE_UPDATE.vertex_attributes[1..2]),
+ ]);
+ GpuCacheBus::Scatter {
+ program,
+ vao,
+ buf_position,
+ buf_value,
+ count: 0,
+ }
+ } else {
+ GpuCacheBus::PixelBuffer {
+ rows: Vec::new(),
+ }
+ };
+
+ Ok(GpuCacheTexture {
+ texture: None,
+ bus,
+ })
+ }
+
+ pub fn deinit(mut self, device: &mut Device) {
+ if let Some(t) = self.texture.take() {
+ device.delete_texture(t);
+ }
+ if let GpuCacheBus::Scatter { program, vao, buf_position, buf_value, .. } = self.bus {
+ device.delete_program(program);
+ device.delete_custom_vao(vao);
+ device.delete_vbo(buf_position);
+ device.delete_vbo(buf_value);
+ }
+ }
+
+ pub fn get_height(&self) -> i32 {
+ self.texture.as_ref().map_or(0, |t| t.get_dimensions().height)
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn get_texture(&self) -> &Texture {
+ self.texture.as_ref().unwrap()
+ }
+
+ fn prepare_for_updates(
+ &mut self,
+ device: &mut Device,
+ total_block_count: usize,
+ max_height: i32,
+ ) {
+ self.ensure_texture(device, max_height);
+ match self.bus {
+ GpuCacheBus::PixelBuffer { .. } => {},
+ GpuCacheBus::Scatter {
+ ref mut buf_position,
+ ref mut buf_value,
+ ref mut count,
+ ..
+ } => {
+ *count = 0;
+ if total_block_count > buf_value.allocated_count() {
+ device.allocate_vbo(buf_position, total_block_count, super::ONE_TIME_USAGE_HINT);
+ device.allocate_vbo(buf_value, total_block_count, super::ONE_TIME_USAGE_HINT);
+ }
+ }
+ }
+ }
+
+ pub fn invalidate(&mut self) {
+ match self.bus {
+ GpuCacheBus::PixelBuffer { ref mut rows, .. } => {
+ info!("Invalidating GPU caches");
+ for row in rows {
+ row.add_dirty(0, super::MAX_VERTEX_TEXTURE_WIDTH);
+ }
+ }
+ GpuCacheBus::Scatter { .. } => {
+ warn!("Unable to invalidate scattered GPU cache");
+ }
+ }
+ }
+
+ fn update(&mut self, device: &mut Device, updates: &GpuCacheUpdateList) {
+ match self.bus {
+ GpuCacheBus::PixelBuffer { ref mut rows, .. } => {
+ for update in &updates.updates {
+ match *update {
+ GpuCacheUpdate::Copy {
+ block_index,
+ block_count,
+ address,
+ } => {
+ let row = address.v as usize;
+
+ // Ensure that the CPU-side shadow copy of the GPU cache data has enough
+ // rows to apply this patch.
+ while rows.len() <= row {
+ // Add a new row.
+ rows.push(CacheRow::new());
+ }
+
+ // Copy the blocks from the patch array in the shadow CPU copy.
+ let block_offset = address.u as usize;
+ let data = &mut rows[row].cpu_blocks;
+ for i in 0 .. block_count {
+ data[block_offset + i] = updates.blocks[block_index + i];
+ }
+
+ // This row is dirty (needs to be updated in GPU texture).
+ rows[row].add_dirty(block_offset, block_count);
+ }
+ }
+ }
+ }
+ GpuCacheBus::Scatter {
+ ref buf_position,
+ ref buf_value,
+ ref mut count,
+ ..
+ } => {
+ //TODO: re-use this heap allocation
+ // Unused positions will be left as 0xFFFF, which translates to
+ // (1.0, 1.0) in the vertex output position and gets culled out
+ let mut position_data = vec![[!0u16; 2]; updates.blocks.len()];
+ let size = self.texture.as_ref().unwrap().get_dimensions().to_usize();
+
+ for update in &updates.updates {
+ match *update {
+ GpuCacheUpdate::Copy {
+ block_index,
+ block_count,
+ address,
+ } => {
+ // Convert the absolute texel position into normalized
+ let y = ((2*address.v as usize + 1) << 15) / size.height;
+ for i in 0 .. block_count {
+ let x = ((2*address.u as usize + 2*i + 1) << 15) / size.width;
+ position_data[block_index + i] = [x as _, y as _];
+ }
+ }
+ }
+ }
+
+ device.fill_vbo(buf_value, &updates.blocks, *count);
+ device.fill_vbo(buf_position, &position_data, *count);
+ *count += position_data.len();
+ }
+ }
+ }
+
+ fn flush(&mut self, device: &mut Device, pbo_pool: &mut UploadPBOPool) -> usize {
+ let texture = self.texture.as_ref().unwrap();
+ match self.bus {
+ GpuCacheBus::PixelBuffer { ref mut rows } => {
+ let rows_dirty = rows
+ .iter()
+ .filter(|row| row.is_dirty())
+ .count();
+ if rows_dirty == 0 {
+ return 0
+ }
+
+ let mut uploader = device.upload_texture(pbo_pool);
+
+ for (row_index, row) in rows.iter_mut().enumerate() {
+ if !row.is_dirty() {
+ continue;
+ }
+
+ let blocks = row.dirty_blocks();
+ let rect = DeviceIntRect::from_origin_and_size(
+ DeviceIntPoint::new(row.min_dirty as i32, row_index as i32),
+ DeviceIntSize::new(blocks.len() as i32, 1),
+ );
+
+ uploader.upload(device, texture, rect, None, None, blocks.as_ptr(), blocks.len());
+
+ row.clear_dirty();
+ }
+
+ uploader.flush(device);
+
+ rows_dirty
+ }
+ GpuCacheBus::Scatter { ref program, ref vao, count, .. } => {
+ device.disable_depth();
+ device.set_blend(false);
+ device.bind_program(program);
+ device.bind_custom_vao(vao);
+ device.bind_draw_target(
+ DrawTarget::from_texture(
+ texture,
+ false,
+ ),
+ );
+ device.draw_nonindexed_points(0, count as _);
+ 0
+ }
+ }
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn remove_texture(&mut self, device: &mut Device) {
+ if let Some(t) = self.texture.take() {
+ device.delete_texture(t);
+ }
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn load_from_data(&mut self, texture: Texture, data: Vec<u8>) {
+ assert!(self.texture.is_none());
+ match self.bus {
+ GpuCacheBus::PixelBuffer { ref mut rows, .. } => {
+ let dim = texture.get_dimensions();
+ let blocks = unsafe {
+ std::slice::from_raw_parts(
+ data.as_ptr() as *const GpuBlockData,
+ data.len() / mem::size_of::<GpuBlockData>(),
+ )
+ };
+ // fill up the CPU cache from the contents we just loaded
+ rows.clear();
+ rows.extend((0 .. dim.height).map(|_| CacheRow::new()));
+ let chunks = blocks.chunks(super::MAX_VERTEX_TEXTURE_WIDTH);
+ debug_assert_eq!(chunks.len(), rows.len());
+ for (row, chunk) in rows.iter_mut().zip(chunks) {
+ row.cpu_blocks.copy_from_slice(chunk);
+ }
+ }
+ GpuCacheBus::Scatter { .. } => {}
+ }
+ self.texture = Some(texture);
+ }
+
+ pub fn report_memory_to(&self, report: &mut MemoryReport, size_op_funs: &MallocSizeOfOps) {
+ if let GpuCacheBus::PixelBuffer{ref rows, ..} = self.bus {
+ for row in rows.iter() {
+ report.gpu_cache_cpu_mirror += unsafe { (size_op_funs.size_of_op)(row.cpu_blocks.as_ptr() as *const _) };
+ }
+ }
+
+ // GPU cache GPU memory.
+ report.gpu_cache_textures +=
+ self.texture.as_ref().map_or(0, |t| t.size_in_bytes());
+ }
+}
+
+impl super::Renderer {
+ pub fn update_gpu_cache(&mut self) {
+ let _gm = self.gpu_profiler.start_marker("gpu cache update");
+
+ // For an artificial stress test of GPU cache resizing,
+ // always pass an extra update list with at least one block in it.
+ let gpu_cache_height = self.gpu_cache_texture.get_height();
+ if gpu_cache_height != 0 && GPU_CACHE_RESIZE_TEST {
+ self.pending_gpu_cache_updates.push(GpuCacheUpdateList {
+ frame_id: FrameId::INVALID,
+ clear: false,
+ height: gpu_cache_height,
+ blocks: vec![[1f32; 4].into()],
+ updates: Vec::new(),
+ debug_commands: Vec::new(),
+ });
+ }
+
+ let (updated_blocks, max_requested_height) = self
+ .pending_gpu_cache_updates
+ .iter()
+ .fold((0, gpu_cache_height), |(count, height), list| {
+ (count + list.blocks.len(), cmp::max(height, list.height))
+ });
+
+ if max_requested_height > self.get_max_texture_size() && !self.gpu_cache_overflow {
+ self.gpu_cache_overflow = true;
+ self.renderer_errors.push(super::RendererError::MaxTextureSize);
+ }
+
+ // Note: if we decide to switch to scatter-style GPU cache update
+ // permanently, we can have this code nicer with `BufferUploader` kind
+ // of helper, similarly to how `TextureUploader` API is used.
+ self.gpu_cache_texture.prepare_for_updates(
+ &mut self.device,
+ updated_blocks,
+ max_requested_height,
+ );
+
+ for update_list in self.pending_gpu_cache_updates.drain(..) {
+ assert!(update_list.height <= max_requested_height);
+ if update_list.frame_id > self.gpu_cache_frame_id {
+ self.gpu_cache_frame_id = update_list.frame_id
+ }
+ self.gpu_cache_texture
+ .update(&mut self.device, &update_list);
+ }
+
+ self.profile.start_time(profiler::GPU_CACHE_UPLOAD_TIME);
+ let updated_rows = self.gpu_cache_texture.flush(
+ &mut self.device,
+ &mut self.texture_upload_pbo_pool
+ );
+ self.gpu_cache_upload_time += self.profile.end_time(profiler::GPU_CACHE_UPLOAD_TIME);
+
+ self.profile.set(profiler::GPU_CACHE_ROWS_UPDATED, updated_rows);
+ self.profile.set(profiler::GPU_CACHE_BLOCKS_UPDATED, updated_blocks);
+ }
+
+ pub fn prepare_gpu_cache(
+ &mut self,
+ deferred_resolves: &[DeferredResolve],
+ ) -> Result<(), super::RendererError> {
+ if self.pending_gpu_cache_clear {
+ let use_scatter =
+ matches!(self.gpu_cache_texture.bus, GpuCacheBus::Scatter { .. });
+ let new_cache = GpuCacheTexture::new(&mut self.device, use_scatter)?;
+ let old_cache = mem::replace(&mut self.gpu_cache_texture, new_cache);
+ old_cache.deinit(&mut self.device);
+ self.pending_gpu_cache_clear = false;
+ }
+
+ let deferred_update_list = self.update_deferred_resolves(deferred_resolves);
+ self.pending_gpu_cache_updates.extend(deferred_update_list);
+
+ self.update_gpu_cache();
+
+ // Note: the texture might have changed during the `update`,
+ // so we need to bind it here.
+ self.device.bind_texture(
+ super::TextureSampler::GpuCache,
+ self.gpu_cache_texture.texture.as_ref().unwrap(),
+ Swizzle::default(),
+ );
+
+ Ok(())
+ }
+
+ pub fn read_gpu_cache(&mut self) -> (DeviceIntSize, Vec<u8>) {
+ let texture = self.gpu_cache_texture.texture.as_ref().unwrap();
+ let size = device_size_as_framebuffer_size(texture.get_dimensions());
+ let mut texels = vec![0; (size.width * size.height * 16) as usize];
+ self.device.begin_frame();
+ self.device.bind_read_target(ReadTarget::from_texture(texture));
+ self.device.read_pixels_into(
+ size.into(),
+ api::ImageFormat::RGBAF32,
+ &mut texels,
+ );
+ self.device.reset_read_target();
+ self.device.end_frame();
+ (texture.get_dimensions(), texels)
+ }
+}
diff --git a/gfx/wr/webrender/src/renderer/init.rs b/gfx/wr/webrender/src/renderer/init.rs
new file mode 100644
index 0000000000..767f9c4b9e
--- /dev/null
+++ b/gfx/wr/webrender/src/renderer/init.rs
@@ -0,0 +1,788 @@
+/* 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 api::{BlobImageHandler, ColorF, IdNamespace, DocumentId, CrashAnnotator};
+use api::{VoidPtrToSizeFn, FontRenderMode, ImageFormat};
+use api::{RenderNotifier, ImageBufferKind};
+use api::units::*;
+use api::channel::unbounded_channel;
+pub use api::DebugFlags;
+
+use crate::render_api::{RenderApiSender, FrameMsg};
+use crate::composite::{CompositorKind, CompositorConfig};
+use crate::device::{
+ UploadMethod, UploadPBOPool, VertexUsageHint, Device, ProgramCache, TextureFilter
+};
+use crate::frame_builder::FrameBuilderConfig;
+use crate::glyph_cache::GlyphCache;
+use glyph_rasterizer::{GlyphRasterizer, SharedFontResources};
+use crate::gpu_types::PrimitiveInstanceData;
+use crate::internal_types::{FastHashMap, FastHashSet, FrameId};
+use crate::picture;
+use crate::profiler::{self, Profiler, TransactionProfile};
+use crate::device::query::{GpuProfiler, GpuDebugMethod};
+use crate::render_backend::RenderBackend;
+use crate::resource_cache::ResourceCache;
+use crate::scene_builder_thread::{SceneBuilderThread, SceneBuilderThreadChannels, LowPrioritySceneBuilderThread};
+use crate::texture_cache::{TextureCache, TextureCacheConfig};
+use crate::picture_textures::PictureTextures;
+use crate::renderer::{
+ debug, gpu_cache, vertex, gl,
+ Renderer, DebugOverlayState, BufferDamageTracker, PipelineInfo, TextureResolver,
+ RendererError, ShaderPrecacheFlags, VERTEX_DATA_TEXTURE_COUNT,
+ upload::UploadTexturePool,
+ shade::{Shaders, SharedShaders},
+};
+
+use std::{
+ mem,
+ thread,
+ cell::RefCell,
+ collections::VecDeque,
+ rc::Rc,
+ sync::{Arc, atomic::{AtomicBool, Ordering}},
+ num::NonZeroUsize,
+ path::PathBuf,
+};
+
+use tracy_rs::register_thread_with_profiler;
+use rayon::{ThreadPool, ThreadPoolBuilder};
+use malloc_size_of::MallocSizeOfOps;
+
+/// Use this hint for all vertex data re-initialization. This allows
+/// the driver to better re-use RBOs internally.
+pub const ONE_TIME_USAGE_HINT: VertexUsageHint = VertexUsageHint::Stream;
+
+/// Is only false if no WR instances have ever been created.
+static HAS_BEEN_INITIALIZED: AtomicBool = AtomicBool::new(false);
+
+/// Returns true if a WR instance has ever been initialized in this process.
+pub fn wr_has_been_initialized() -> bool {
+ HAS_BEEN_INITIALIZED.load(Ordering::SeqCst)
+}
+
+/// Allows callers to hook in at certain points of the async scene build. These
+/// functions are all called from the scene builder thread.
+pub trait SceneBuilderHooks {
+ /// This is called exactly once, when the scene builder thread is started
+ /// and before it processes anything.
+ fn register(&self);
+ /// This is called before each scene build starts.
+ fn pre_scene_build(&self);
+ /// This is called before each scene swap occurs.
+ fn pre_scene_swap(&self);
+ /// This is called after each scene swap occurs. The PipelineInfo contains
+ /// the updated epochs and pipelines removed in the new scene compared to
+ /// the old scene.
+ fn post_scene_swap(&self, document_id: &Vec<DocumentId>, info: PipelineInfo);
+ /// This is called after a resource update operation on the scene builder
+ /// thread, in the case where resource updates were applied without a scene
+ /// build.
+ fn post_resource_update(&self, document_ids: &Vec<DocumentId>);
+ /// This is called after a scene build completes without any changes being
+ /// made. We guarantee that each pre_scene_build call will be matched with
+ /// exactly one of post_scene_swap, post_resource_update or
+ /// post_empty_scene_build.
+ fn post_empty_scene_build(&self);
+ /// This is a generic callback which provides an opportunity to run code
+ /// on the scene builder thread. This is called as part of the main message
+ /// loop of the scene builder thread, but outside of any specific message
+ /// handler.
+ fn poke(&self);
+ /// This is called exactly once, when the scene builder thread is about to
+ /// terminate.
+ fn deregister(&self);
+}
+
+/// Allows callers to hook into the main render_backend loop and provide
+/// additional frame ops for generate_frame transactions. These functions
+/// are all called from the render backend thread.
+pub trait AsyncPropertySampler {
+ /// This is called exactly once, when the render backend thread is started
+ /// and before it processes anything.
+ fn register(&self);
+ /// This is called for each transaction with the generate_frame flag set
+ /// (i.e. that will trigger a render). The list of frame messages returned
+ /// are processed as though they were part of the original transaction.
+ fn sample(&self, document_id: DocumentId, generated_frame_id: Option<u64>) -> Vec<FrameMsg>;
+ /// This is called exactly once, when the render backend thread is about to
+ /// terminate.
+ fn deregister(&self);
+}
+
+pub struct WebRenderOptions {
+ pub resource_override_path: Option<PathBuf>,
+ /// Whether to use shaders that have been optimized at build time.
+ pub use_optimized_shaders: bool,
+ pub enable_aa: bool,
+ pub enable_dithering: bool,
+ pub max_recorded_profiles: usize,
+ pub precache_flags: ShaderPrecacheFlags,
+ /// Enable sub-pixel anti-aliasing if a fast implementation is available.
+ pub enable_subpixel_aa: bool,
+ pub clear_color: ColorF,
+ pub enable_clear_scissor: Option<bool>,
+ pub max_internal_texture_size: Option<i32>,
+ pub image_tiling_threshold: i32,
+ pub upload_method: UploadMethod,
+ /// The default size in bytes for PBOs used to upload texture data.
+ pub upload_pbo_default_size: usize,
+ pub batched_upload_threshold: i32,
+ pub workers: Option<Arc<ThreadPool>>,
+ pub enable_multithreading: bool,
+ pub blob_image_handler: Option<Box<dyn BlobImageHandler>>,
+ pub crash_annotator: Option<Box<dyn CrashAnnotator>>,
+ pub size_of_op: Option<VoidPtrToSizeFn>,
+ pub enclosing_size_of_op: Option<VoidPtrToSizeFn>,
+ pub cached_programs: Option<Rc<ProgramCache>>,
+ pub debug_flags: DebugFlags,
+ pub renderer_id: Option<u64>,
+ pub scene_builder_hooks: Option<Box<dyn SceneBuilderHooks + Send>>,
+ pub sampler: Option<Box<dyn AsyncPropertySampler + Send>>,
+ pub support_low_priority_transactions: bool,
+ pub namespace_alloc_by_client: bool,
+ /// If namespaces are allocated by the client, then the namespace for fonts
+ /// must also be allocated by the client to avoid namespace collisions with
+ /// the backend.
+ pub shared_font_namespace: Option<IdNamespace>,
+ pub testing: bool,
+ /// Set to true if this GPU supports hardware fast clears as a performance
+ /// optimization. Likely requires benchmarking on various GPUs to see if
+ /// it is a performance win. The default is false, which tends to be best
+ /// performance on lower end / integrated GPUs.
+ pub gpu_supports_fast_clears: bool,
+ pub allow_dual_source_blending: bool,
+ pub allow_advanced_blend_equation: bool,
+ /// If true, allow textures to be initialized with glTexStorage.
+ /// This affects VRAM consumption and data upload paths.
+ pub allow_texture_storage_support: bool,
+ /// If true, we allow the data uploaded in a different format from the
+ /// one expected by the driver, pretending the format is matching, and
+ /// swizzling the components on all the shader sampling.
+ pub allow_texture_swizzling: bool,
+ /// Use `ps_clear` shader with batched quad rendering to clear the rects
+ /// in texture cache and picture cache tasks.
+ /// This helps to work around some Intel drivers
+ /// that incorrectly synchronize clears to following draws.
+ pub clear_caches_with_quads: bool,
+ /// Output the source of the shader with the given name.
+ pub dump_shader_source: Option<String>,
+ pub surface_origin_is_top_left: bool,
+ /// The configuration options defining how WR composites the final scene.
+ pub compositor_config: CompositorConfig,
+ pub enable_gpu_markers: bool,
+ /// If true, panic whenever a GL error occurs. This has a significant
+ /// performance impact, so only use when debugging specific problems!
+ pub panic_on_gl_error: bool,
+ pub picture_tile_size: Option<DeviceIntSize>,
+ pub texture_cache_config: TextureCacheConfig,
+ /// If true, we'll use instanced vertex attributes. Each instace is a quad.
+ /// If false, we'll duplicate the instance attributes per vertex and issue
+ /// regular indexed draws instead.
+ pub enable_instancing: bool,
+ /// If true, we'll reject contexts backed by a software rasterizer, except
+ /// Software WebRender.
+ pub reject_software_rasterizer: bool,
+ /// If enabled, pinch-zoom will apply the zoom factor during compositing
+ /// of picture cache tiles. This is higher performance (tiles are not
+ /// re-rasterized during zoom) but lower quality result. For most display
+ /// items, if the zoom factor is relatively small, bilinear filtering should
+ /// make the result look quite close to the high-quality zoom, except for glyphs.
+ pub low_quality_pinch_zoom: bool,
+ pub max_shared_surface_size: i32,
+}
+
+impl WebRenderOptions {
+ /// Number of batches to look back in history for adding the current
+ /// transparent instance into.
+ const BATCH_LOOKBACK_COUNT: usize = 10;
+
+ /// Since we are re-initializing the instance buffers on every draw call,
+ /// the driver has to internally manage PBOs in flight.
+ /// It's typically done by bucketing up to a specific limit, and then
+ /// just individually managing the largest buffers.
+ /// Having a limit here allows the drivers to more easily manage
+ /// the PBOs for us.
+ const MAX_INSTANCE_BUFFER_SIZE: usize = 0x20000; // actual threshold in macOS GL drivers
+}
+
+impl Default for WebRenderOptions {
+ fn default() -> Self {
+ WebRenderOptions {
+ resource_override_path: None,
+ use_optimized_shaders: false,
+ enable_aa: true,
+ enable_dithering: false,
+ debug_flags: DebugFlags::empty(),
+ max_recorded_profiles: 0,
+ precache_flags: ShaderPrecacheFlags::empty(),
+ enable_subpixel_aa: false,
+ clear_color: ColorF::new(1.0, 1.0, 1.0, 1.0),
+ enable_clear_scissor: None,
+ max_internal_texture_size: None,
+ image_tiling_threshold: 4096,
+ // This is best as `Immediate` on Angle, or `Pixelbuffer(Dynamic)` on GL,
+ // but we are unable to make this decision here, so picking the reasonable medium.
+ upload_method: UploadMethod::PixelBuffer(ONE_TIME_USAGE_HINT),
+ upload_pbo_default_size: 512 * 512 * 4,
+ batched_upload_threshold: 512 * 512,
+ workers: None,
+ enable_multithreading: true,
+ blob_image_handler: None,
+ crash_annotator: None,
+ size_of_op: None,
+ enclosing_size_of_op: None,
+ renderer_id: None,
+ cached_programs: None,
+ scene_builder_hooks: None,
+ sampler: None,
+ support_low_priority_transactions: false,
+ namespace_alloc_by_client: false,
+ shared_font_namespace: None,
+ testing: false,
+ gpu_supports_fast_clears: false,
+ allow_dual_source_blending: true,
+ allow_advanced_blend_equation: false,
+ allow_texture_storage_support: true,
+ allow_texture_swizzling: true,
+ clear_caches_with_quads: true,
+ dump_shader_source: None,
+ surface_origin_is_top_left: false,
+ compositor_config: CompositorConfig::default(),
+ enable_gpu_markers: true,
+ panic_on_gl_error: false,
+ picture_tile_size: None,
+ texture_cache_config: TextureCacheConfig::DEFAULT,
+ // Disabling instancing means more vertex data to upload and potentially
+ // process by the vertex shaders.
+ enable_instancing: true,
+ reject_software_rasterizer: false,
+ low_quality_pinch_zoom: false,
+ max_shared_surface_size: 2048,
+ }
+ }
+}
+
+/// Initializes WebRender and creates a `Renderer` and `RenderApiSender`.
+///
+/// # Examples
+/// Initializes a `Renderer` with some reasonable values. For more information see
+/// [`WebRenderOptions`][WebRenderOptions].
+///
+/// ```rust,ignore
+/// # use webrender::renderer::Renderer;
+/// # use std::path::PathBuf;
+/// let opts = webrender::WebRenderOptions {
+/// device_pixel_ratio: 1.0,
+/// resource_override_path: None,
+/// enable_aa: false,
+/// };
+/// let (renderer, sender) = Renderer::new(opts);
+/// ```
+/// [WebRenderOptions]: struct.WebRenderOptions.html
+pub fn create_webrender_instance(
+ gl: Rc<dyn gl::Gl>,
+ notifier: Box<dyn RenderNotifier>,
+ mut options: WebRenderOptions,
+ shaders: Option<&SharedShaders>,
+) -> Result<(Renderer, RenderApiSender), RendererError> {
+ if !wr_has_been_initialized() {
+ // If the profiler feature is enabled, try to load the profiler shared library
+ // if the path was provided.
+ #[cfg(feature = "profiler")]
+ unsafe {
+ if let Ok(ref tracy_path) = std::env::var("WR_TRACY_PATH") {
+ let ok = tracy_rs::load(tracy_path);
+ info!("Load tracy from {} -> {}", tracy_path, ok);
+ }
+ }
+
+ register_thread_with_profiler("Compositor".to_owned());
+ }
+
+ HAS_BEEN_INITIALIZED.store(true, Ordering::SeqCst);
+
+ let (api_tx, api_rx) = unbounded_channel();
+ let (result_tx, result_rx) = unbounded_channel();
+ let gl_type = gl.get_type();
+
+ let mut device = Device::new(
+ gl,
+ options.crash_annotator.clone(),
+ options.resource_override_path.clone(),
+ options.use_optimized_shaders,
+ options.upload_method.clone(),
+ options.batched_upload_threshold,
+ options.cached_programs.take(),
+ options.allow_texture_storage_support,
+ options.allow_texture_swizzling,
+ options.dump_shader_source.take(),
+ options.surface_origin_is_top_left,
+ options.panic_on_gl_error,
+ );
+
+ let color_cache_formats = device.preferred_color_formats();
+ let swizzle_settings = device.swizzle_settings();
+ let use_dual_source_blending =
+ device.get_capabilities().supports_dual_source_blending &&
+ options.allow_dual_source_blending;
+ let ext_blend_equation_advanced =
+ options.allow_advanced_blend_equation &&
+ device.get_capabilities().supports_advanced_blend_equation;
+ let ext_blend_equation_advanced_coherent =
+ device.supports_extension("GL_KHR_blend_equation_advanced_coherent");
+
+ let enable_clear_scissor = options
+ .enable_clear_scissor
+ .unwrap_or(device.get_capabilities().prefers_clear_scissor);
+
+ // 2048 is the minimum that the texture cache can work with.
+ const MIN_TEXTURE_SIZE: i32 = 2048;
+ let mut max_internal_texture_size = device.max_texture_size();
+ if max_internal_texture_size < MIN_TEXTURE_SIZE {
+ // Broken GL contexts can return a max texture size of zero (See #1260).
+ // Better to gracefully fail now than panic as soon as a texture is allocated.
+ error!(
+ "Device reporting insufficient max texture size ({})",
+ max_internal_texture_size
+ );
+ return Err(RendererError::MaxTextureSize);
+ }
+ if let Some(internal_limit) = options.max_internal_texture_size {
+ assert!(internal_limit >= MIN_TEXTURE_SIZE);
+ max_internal_texture_size = max_internal_texture_size.min(internal_limit);
+ }
+
+ if options.reject_software_rasterizer {
+ let renderer_name_lc = device.get_capabilities().renderer_name.to_lowercase();
+ if renderer_name_lc.contains("llvmpipe") || renderer_name_lc.contains("softpipe") || renderer_name_lc.contains("software rasterizer") {
+ return Err(RendererError::SoftwareRasterizer);
+ }
+ }
+
+ let image_tiling_threshold = options.image_tiling_threshold
+ .min(max_internal_texture_size);
+
+ device.begin_frame();
+
+ let shaders = match shaders {
+ Some(shaders) => Rc::clone(shaders),
+ None => Rc::new(RefCell::new(Shaders::new(&mut device, gl_type, &options)?)),
+ };
+
+ let dither_matrix_texture = if options.enable_dithering {
+ let dither_matrix: [u8; 64] = [
+ 0,
+ 48,
+ 12,
+ 60,
+ 3,
+ 51,
+ 15,
+ 63,
+ 32,
+ 16,
+ 44,
+ 28,
+ 35,
+ 19,
+ 47,
+ 31,
+ 8,
+ 56,
+ 4,
+ 52,
+ 11,
+ 59,
+ 7,
+ 55,
+ 40,
+ 24,
+ 36,
+ 20,
+ 43,
+ 27,
+ 39,
+ 23,
+ 2,
+ 50,
+ 14,
+ 62,
+ 1,
+ 49,
+ 13,
+ 61,
+ 34,
+ 18,
+ 46,
+ 30,
+ 33,
+ 17,
+ 45,
+ 29,
+ 10,
+ 58,
+ 6,
+ 54,
+ 9,
+ 57,
+ 5,
+ 53,
+ 42,
+ 26,
+ 38,
+ 22,
+ 41,
+ 25,
+ 37,
+ 21,
+ ];
+
+ let texture = device.create_texture(
+ ImageBufferKind::Texture2D,
+ ImageFormat::R8,
+ 8,
+ 8,
+ TextureFilter::Nearest,
+ None,
+ );
+ device.upload_texture_immediate(&texture, &dither_matrix);
+
+ Some(texture)
+ } else {
+ None
+ };
+
+ let max_primitive_instance_count =
+ WebRenderOptions::MAX_INSTANCE_BUFFER_SIZE / mem::size_of::<PrimitiveInstanceData>();
+ let vaos = vertex::RendererVAOs::new(
+ &mut device,
+ if options.enable_instancing { None } else { NonZeroUsize::new(max_primitive_instance_count) },
+ );
+
+ let texture_upload_pbo_pool = UploadPBOPool::new(&mut device, options.upload_pbo_default_size);
+ let staging_texture_pool = UploadTexturePool::new();
+ let texture_resolver = TextureResolver::new(&mut device);
+
+ let mut vertex_data_textures = Vec::new();
+ for _ in 0 .. VERTEX_DATA_TEXTURE_COUNT {
+ vertex_data_textures.push(vertex::VertexDataTextures::new());
+ }
+
+ // On some (mostly older, integrated) GPUs, the normal GPU texture cache update path
+ // doesn't work well when running on ANGLE, causing CPU stalls inside D3D and/or the
+ // GPU driver. See https://bugzilla.mozilla.org/show_bug.cgi?id=1576637 for much
+ // more detail. To reduce the number of code paths we have active that require testing,
+ // we will enable the GPU cache scatter update path on all devices running with ANGLE.
+ // We want a better solution long-term, but for now this is a significant performance
+ // improvement on HD4600 era GPUs, and shouldn't hurt performance in a noticeable
+ // way on other systems running under ANGLE.
+ let is_software = device.get_capabilities().renderer_name.starts_with("Software");
+
+ // On other GL platforms, like macOS or Android, creating many PBOs is very inefficient.
+ // This is what happens in GPU cache updates in PBO path. Instead, we switch everything
+ // except software GL to use the GPU scattered updates.
+ let supports_scatter = device.get_capabilities().supports_color_buffer_float;
+ let gpu_cache_texture = gpu_cache::GpuCacheTexture::new(
+ &mut device,
+ supports_scatter && !is_software,
+ )?;
+
+ device.end_frame();
+
+ let backend_notifier = notifier.clone();
+
+ let clear_alpha_targets_with_quads = !device.get_capabilities().supports_alpha_target_clears;
+
+ let prefer_subpixel_aa = options.enable_subpixel_aa && use_dual_source_blending;
+ let default_font_render_mode = match (options.enable_aa, prefer_subpixel_aa) {
+ (true, true) => FontRenderMode::Subpixel,
+ (true, false) => FontRenderMode::Alpha,
+ (false, _) => FontRenderMode::Mono,
+ };
+
+ let compositor_kind = match options.compositor_config {
+ CompositorConfig::Draw { max_partial_present_rects, draw_previous_partial_present_regions, .. } => {
+ CompositorKind::Draw { max_partial_present_rects, draw_previous_partial_present_regions }
+ }
+ CompositorConfig::Native { ref compositor } => {
+ let capabilities = compositor.get_capabilities(&mut device);
+
+ CompositorKind::Native {
+ capabilities,
+ }
+ }
+ };
+
+ let config = FrameBuilderConfig {
+ default_font_render_mode,
+ dual_source_blending_is_supported: use_dual_source_blending,
+ testing: options.testing,
+ gpu_supports_fast_clears: options.gpu_supports_fast_clears,
+ gpu_supports_advanced_blend: ext_blend_equation_advanced,
+ advanced_blend_is_coherent: ext_blend_equation_advanced_coherent,
+ gpu_supports_render_target_partial_update: device.get_capabilities().supports_render_target_partial_update,
+ external_images_require_copy: !device.get_capabilities().supports_image_external_essl3,
+ batch_lookback_count: WebRenderOptions::BATCH_LOOKBACK_COUNT,
+ background_color: Some(options.clear_color),
+ compositor_kind,
+ tile_size_override: None,
+ max_surface_override: None,
+ max_depth_ids: device.max_depth_ids(),
+ max_target_size: max_internal_texture_size,
+ force_invalidation: false,
+ is_software,
+ low_quality_pinch_zoom: options.low_quality_pinch_zoom,
+ max_shared_surface_size: options.max_shared_surface_size,
+ };
+ info!("WR {:?}", config);
+
+ let debug_flags = options.debug_flags;
+ let size_of_op = options.size_of_op;
+ let enclosing_size_of_op = options.enclosing_size_of_op;
+ let make_size_of_ops =
+ move || size_of_op.map(|o| MallocSizeOfOps::new(o, enclosing_size_of_op));
+ let workers = options
+ .workers
+ .take()
+ .unwrap_or_else(|| {
+ let worker = ThreadPoolBuilder::new()
+ .thread_name(|idx|{ format!("WRWorker#{}", idx) })
+ .start_handler(move |idx| {
+ register_thread_with_profiler(format!("WRWorker#{}", idx));
+ profiler::register_thread(&format!("WRWorker#{}", idx));
+ })
+ .exit_handler(move |_idx| {
+ profiler::unregister_thread();
+ })
+ .build();
+ Arc::new(worker.unwrap())
+ });
+ let sampler = options.sampler;
+ let namespace_alloc_by_client = options.namespace_alloc_by_client;
+
+ // Ensure shared font keys exist within their own unique namespace so
+ // that they don't accidentally collide across Renderer instances.
+ let font_namespace = if namespace_alloc_by_client {
+ options.shared_font_namespace.expect("Shared font namespace must be allocated by client")
+ } else {
+ RenderBackend::next_namespace_id()
+ };
+ let fonts = SharedFontResources::new(font_namespace);
+
+ let blob_image_handler = options.blob_image_handler.take();
+ let scene_builder_hooks = options.scene_builder_hooks;
+ let rb_thread_name = format!("WRRenderBackend#{}", options.renderer_id.unwrap_or(0));
+ let scene_thread_name = format!("WRSceneBuilder#{}", options.renderer_id.unwrap_or(0));
+ let lp_scene_thread_name = format!("WRSceneBuilderLP#{}", options.renderer_id.unwrap_or(0));
+ let glyph_rasterizer = GlyphRasterizer::new(workers, device.get_capabilities().supports_r8_texture_upload);
+
+ let (scene_builder_channels, scene_tx) =
+ SceneBuilderThreadChannels::new(api_tx.clone());
+
+ let sb_fonts = fonts.clone();
+
+ thread::Builder::new().name(scene_thread_name.clone()).spawn(move || {
+ register_thread_with_profiler(scene_thread_name.clone());
+ profiler::register_thread(&scene_thread_name);
+
+ let mut scene_builder = SceneBuilderThread::new(
+ config,
+ sb_fonts,
+ make_size_of_ops(),
+ scene_builder_hooks,
+ scene_builder_channels,
+ );
+ scene_builder.run();
+
+ profiler::unregister_thread();
+ })?;
+
+ let low_priority_scene_tx = if options.support_low_priority_transactions {
+ let (low_priority_scene_tx, low_priority_scene_rx) = unbounded_channel();
+ let lp_builder = LowPrioritySceneBuilderThread {
+ rx: low_priority_scene_rx,
+ tx: scene_tx.clone(),
+ };
+
+ thread::Builder::new().name(lp_scene_thread_name.clone()).spawn(move || {
+ register_thread_with_profiler(lp_scene_thread_name.clone());
+ profiler::register_thread(&lp_scene_thread_name);
+
+ let mut scene_builder = lp_builder;
+ scene_builder.run();
+
+ profiler::unregister_thread();
+ })?;
+
+ low_priority_scene_tx
+ } else {
+ scene_tx.clone()
+ };
+
+ let rb_blob_handler = blob_image_handler
+ .as_ref()
+ .map(|handler| handler.create_similar());
+
+ let texture_cache_config = options.texture_cache_config.clone();
+ let mut picture_tile_size = options.picture_tile_size.unwrap_or(picture::TILE_SIZE_DEFAULT);
+ // Clamp the picture tile size to reasonable values.
+ picture_tile_size.width = picture_tile_size.width.max(128).min(4096);
+ picture_tile_size.height = picture_tile_size.height.max(128).min(4096);
+
+ let picture_texture_filter = if options.low_quality_pinch_zoom {
+ TextureFilter::Linear
+ } else {
+ TextureFilter::Nearest
+ };
+
+ let rb_scene_tx = scene_tx.clone();
+ let rb_fonts = fonts.clone();
+ let enable_multithreading = options.enable_multithreading;
+ thread::Builder::new().name(rb_thread_name.clone()).spawn(move || {
+ register_thread_with_profiler(rb_thread_name.clone());
+ profiler::register_thread(&rb_thread_name);
+
+ let texture_cache = TextureCache::new(
+ max_internal_texture_size,
+ image_tiling_threshold,
+ color_cache_formats,
+ swizzle_settings,
+ &texture_cache_config,
+ );
+
+ let picture_textures = PictureTextures::new(
+ picture_tile_size,
+ picture_texture_filter,
+ );
+
+ let glyph_cache = GlyphCache::new();
+
+ let mut resource_cache = ResourceCache::new(
+ texture_cache,
+ picture_textures,
+ glyph_rasterizer,
+ glyph_cache,
+ rb_fonts,
+ rb_blob_handler,
+ );
+
+ resource_cache.enable_multithreading(enable_multithreading);
+
+ let mut backend = RenderBackend::new(
+ api_rx,
+ result_tx,
+ rb_scene_tx,
+ resource_cache,
+ backend_notifier,
+ config,
+ sampler,
+ make_size_of_ops(),
+ debug_flags,
+ namespace_alloc_by_client,
+ );
+ backend.run();
+ profiler::unregister_thread();
+ })?;
+
+ let debug_method = if !options.enable_gpu_markers {
+ // The GPU markers are disabled.
+ GpuDebugMethod::None
+ } else if device.supports_extension("GL_KHR_debug") {
+ GpuDebugMethod::KHR
+ } else if device.supports_extension("GL_EXT_debug_marker") {
+ GpuDebugMethod::MarkerEXT
+ } else {
+ warn!("asking to enable_gpu_markers but no supporting extension was found");
+ GpuDebugMethod::None
+ };
+
+ info!("using {:?}", debug_method);
+
+ let gpu_profiler = GpuProfiler::new(Rc::clone(device.rc_gl()), debug_method);
+ #[cfg(feature = "capture")]
+ let read_fbo = device.create_fbo();
+
+ let mut renderer = Renderer {
+ result_rx,
+ api_tx: api_tx.clone(),
+ device,
+ active_documents: FastHashMap::default(),
+ pending_texture_updates: Vec::new(),
+ pending_texture_cache_updates: false,
+ pending_native_surface_updates: Vec::new(),
+ pending_gpu_cache_updates: Vec::new(),
+ pending_gpu_cache_clear: false,
+ pending_shader_updates: Vec::new(),
+ shaders,
+ debug: debug::LazyInitializedDebugRenderer::new(),
+ debug_flags: DebugFlags::empty(),
+ profile: TransactionProfile::new(),
+ frame_counter: 0,
+ resource_upload_time: 0.0,
+ gpu_cache_upload_time: 0.0,
+ profiler: Profiler::new(),
+ max_recorded_profiles: options.max_recorded_profiles,
+ clear_color: options.clear_color,
+ enable_clear_scissor,
+ enable_advanced_blend_barriers: !ext_blend_equation_advanced_coherent,
+ clear_caches_with_quads: options.clear_caches_with_quads,
+ clear_alpha_targets_with_quads,
+ last_time: 0,
+ gpu_profiler,
+ vaos,
+ vertex_data_textures,
+ current_vertex_data_textures: 0,
+ pipeline_info: PipelineInfo::default(),
+ dither_matrix_texture,
+ external_image_handler: None,
+ size_of_ops: make_size_of_ops(),
+ cpu_profiles: VecDeque::new(),
+ gpu_profiles: VecDeque::new(),
+ gpu_cache_texture,
+ gpu_cache_debug_chunks: Vec::new(),
+ gpu_cache_frame_id: FrameId::INVALID,
+ gpu_cache_overflow: false,
+ texture_upload_pbo_pool,
+ staging_texture_pool,
+ texture_resolver,
+ renderer_errors: Vec::new(),
+ async_frame_recorder: None,
+ async_screenshots: None,
+ #[cfg(feature = "capture")]
+ read_fbo,
+ #[cfg(feature = "replay")]
+ owned_external_images: FastHashMap::default(),
+ notifications: Vec::new(),
+ device_size: None,
+ zoom_debug_texture: None,
+ cursor_position: DeviceIntPoint::zero(),
+ shared_texture_cache_cleared: false,
+ documents_seen: FastHashSet::default(),
+ force_redraw: true,
+ compositor_config: options.compositor_config,
+ current_compositor_kind: compositor_kind,
+ allocated_native_surfaces: FastHashSet::default(),
+ debug_overlay_state: DebugOverlayState::new(),
+ buffer_damage_tracker: BufferDamageTracker::default(),
+ max_primitive_instance_count,
+ enable_instancing: options.enable_instancing,
+ consecutive_oom_frames: 0,
+ target_frame_publish_id: None,
+ pending_result_msg: None,
+ };
+
+ // We initially set the flags to default and then now call set_debug_flags
+ // to ensure any potential transition when enabling a flag is run.
+ renderer.set_debug_flags(debug_flags);
+
+ let sender = RenderApiSender::new(
+ api_tx,
+ scene_tx,
+ low_priority_scene_tx,
+ blob_image_handler,
+ fonts,
+ );
+ Ok((renderer, sender))
+}
diff --git a/gfx/wr/webrender/src/renderer/mod.rs b/gfx/wr/webrender/src/renderer/mod.rs
new file mode 100644
index 0000000000..b44c107a66
--- /dev/null
+++ b/gfx/wr/webrender/src/renderer/mod.rs
@@ -0,0 +1,5923 @@
+/* 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/. */
+
+//! The high-level module responsible for interfacing with the GPU.
+//!
+//! Much of WebRender's design is driven by separating work into different
+//! threads. To avoid the complexities of multi-threaded GPU access, we restrict
+//! all communication with the GPU to one thread, the render thread. But since
+//! issuing GPU commands is often a bottleneck, we move everything else (i.e.
+//! the computation of what commands to issue) to another thread, the
+//! RenderBackend thread. The RenderBackend, in turn, may delegate work to other
+//! thread (like the SceneBuilder threads or Rayon workers), but the
+//! Render-vs-RenderBackend distinction is the most important.
+//!
+//! The consumer is responsible for initializing the render thread before
+//! calling into WebRender, which means that this module also serves as the
+//! initial entry point into WebRender, and is responsible for spawning the
+//! various other threads discussed above. That said, WebRender initialization
+//! returns both the `Renderer` instance as well as a channel for communicating
+//! directly with the `RenderBackend`. Aside from a few high-level operations
+//! like 'render now', most of interesting commands from the consumer go over
+//! that channel and operate on the `RenderBackend`.
+//!
+//! ## Space conversion guidelines
+//! At this stage, we shuld be operating with `DevicePixel` and `FramebufferPixel` only.
+//! "Framebuffer" space represents the final destination of our rendeing,
+//! and it happens to be Y-flipped on OpenGL. The conversion is done as follows:
+//! - for rasterized primitives, the orthographics projection transforms
+//! the content rectangle to -1 to 1
+//! - the viewport transformation is setup to map the whole range to
+//! the framebuffer rectangle provided by the document view, stored in `DrawTarget`
+//! - all the direct framebuffer operations, like blitting, reading pixels, and setting
+//! up the scissor, are accepting already transformed coordinates, which we can get by
+//! calling `DrawTarget::to_framebuffer_rect`
+
+use api::{ColorF, ColorU, MixBlendMode};
+use api::{DocumentId, Epoch, ExternalImageHandler, RenderReasons};
+#[cfg(feature = "replay")]
+use api::ExternalImageId;
+use api::{ExternalImageSource, ExternalImageType, ImageFormat, PremultipliedColorF};
+use api::{PipelineId, ImageRendering, Checkpoint, NotificationRequest, ImageBufferKind};
+#[cfg(feature = "replay")]
+use api::ExternalImage;
+use api::FramePublishId;
+use api::units::*;
+use api::channel::{Sender, Receiver};
+pub use api::DebugFlags;
+use core::time::Duration;
+
+use crate::render_api::{DebugCommand, ApiMsg, MemoryReport};
+use crate::batch::{AlphaBatchContainer, BatchKind, BatchFeatures, BatchTextures, BrushBatchKind, ClipBatchList};
+#[cfg(any(feature = "capture", feature = "replay"))]
+use crate::capture::{CaptureConfig, ExternalCaptureImage, PlainExternalImage};
+use crate::composite::{CompositeState, CompositeTileSurface, ResolvedExternalSurface, CompositorSurfaceTransform};
+use crate::composite::{CompositorKind, Compositor, NativeTileId, CompositeFeatures, CompositeSurfaceFormat, ResolvedExternalSurfaceColorData};
+use crate::composite::{CompositorConfig, NativeSurfaceOperationDetails, NativeSurfaceId, NativeSurfaceOperation};
+use crate::composite::{TileKind};
+use crate::debug_colors;
+use crate::device::{DepthFunction, Device, DrawTarget, ExternalTexture, GpuFrameId, UploadPBOPool};
+use crate::device::{ReadTarget, ShaderError, Texture, TextureFilter, TextureFlags, TextureSlot};
+use crate::device::query::{GpuSampler, GpuTimer};
+#[cfg(feature = "capture")]
+use crate::device::FBOId;
+use crate::debug_item::DebugItem;
+use crate::frame_builder::Frame;
+use glyph_rasterizer::GlyphFormat;
+use crate::gpu_cache::{GpuCacheUpdate, GpuCacheUpdateList};
+use crate::gpu_cache::{GpuCacheDebugChunk, GpuCacheDebugCmd};
+use crate::gpu_types::{ScalingInstance, SvgFilterInstance, CopyInstance, MaskInstance, PrimitiveInstanceData};
+use crate::gpu_types::{BlurInstance, ClearInstance, CompositeInstance, CompositorTransform};
+use crate::internal_types::{TextureSource, TextureCacheCategory, FrameId};
+#[cfg(any(feature = "capture", feature = "replay"))]
+use crate::internal_types::DebugOutput;
+use crate::internal_types::{CacheTextureId, FastHashMap, FastHashSet, RenderedDocument, ResultMsg};
+use crate::internal_types::{TextureCacheAllocInfo, TextureCacheAllocationKind, TextureUpdateList};
+use crate::internal_types::{RenderTargetInfo, Swizzle, DeferredResolveIndex};
+use crate::picture::ResolvedSurfaceTexture;
+use crate::prim_store::DeferredResolve;
+use crate::profiler::{self, GpuProfileTag, TransactionProfile};
+use crate::profiler::{Profiler, add_event_marker, add_text_marker, thread_is_being_profiled};
+use crate::device::query::GpuProfiler;
+use crate::render_target::{ResolveOp};
+use crate::render_task_graph::{RenderTaskGraph};
+use crate::render_task::{RenderTask, RenderTaskKind, ReadbackTask};
+use crate::screen_capture::AsyncScreenshotGrabber;
+use crate::render_target::{AlphaRenderTarget, ColorRenderTarget, PictureCacheTarget, PictureCacheTargetKind};
+use crate::render_target::{RenderTarget, TextureCacheRenderTarget};
+use crate::render_target::{RenderTargetKind, BlitJob};
+use crate::telemetry::Telemetry;
+use crate::tile_cache::PictureCacheDebugInfo;
+use crate::util::drain_filter;
+use crate::rectangle_occlusion as occlusion;
+use upload::{upload_to_texture_cache, UploadTexturePool};
+use init::*;
+
+use euclid::{rect, Transform3D, Scale, default};
+use gleam::gl;
+use malloc_size_of::MallocSizeOfOps;
+
+#[cfg(feature = "replay")]
+use std::sync::Arc;
+
+use std::{
+ cell::RefCell,
+ collections::VecDeque,
+ f32,
+ ffi::c_void,
+ mem,
+ num::NonZeroUsize,
+ path::PathBuf,
+ rc::Rc,
+};
+#[cfg(any(feature = "capture", feature = "replay"))]
+use std::collections::hash_map::Entry;
+use time::precise_time_ns;
+
+mod debug;
+mod gpu_buffer;
+mod gpu_cache;
+mod shade;
+mod vertex;
+mod upload;
+pub(crate) mod init;
+
+pub use debug::DebugRenderer;
+pub use shade::{Shaders, SharedShaders};
+pub use vertex::{desc, VertexArrayKind, MAX_VERTEX_TEXTURE_WIDTH};
+pub use gpu_buffer::{GpuBuffer, GpuBufferBuilder, GpuBufferAddress};
+
+/// The size of the array of each type of vertex data texture that
+/// is round-robin-ed each frame during bind_frame_data. Doing this
+/// helps avoid driver stalls while updating the texture in some
+/// drivers. The size of these textures are typically very small
+/// (e.g. < 16 kB) so it's not a huge waste of memory. Despite that,
+/// this is a short-term solution - we want to find a better way
+/// to provide this frame data, which will likely involve some
+/// combination of UBO/SSBO usage. Although this only affects some
+/// platforms, it's enabled on all platforms to reduce testing
+/// differences between platforms.
+pub const VERTEX_DATA_TEXTURE_COUNT: usize = 3;
+
+/// Number of GPU blocks per UV rectangle provided for an image.
+pub const BLOCKS_PER_UV_RECT: usize = 2;
+
+const GPU_TAG_BRUSH_OPACITY: GpuProfileTag = GpuProfileTag {
+ label: "B_Opacity",
+ color: debug_colors::DARKMAGENTA,
+};
+const GPU_TAG_BRUSH_LINEAR_GRADIENT: GpuProfileTag = GpuProfileTag {
+ label: "B_LinearGradient",
+ color: debug_colors::POWDERBLUE,
+};
+const GPU_TAG_BRUSH_YUV_IMAGE: GpuProfileTag = GpuProfileTag {
+ label: "B_YuvImage",
+ color: debug_colors::DARKGREEN,
+};
+const GPU_TAG_BRUSH_MIXBLEND: GpuProfileTag = GpuProfileTag {
+ label: "B_MixBlend",
+ color: debug_colors::MAGENTA,
+};
+const GPU_TAG_BRUSH_BLEND: GpuProfileTag = GpuProfileTag {
+ label: "B_Blend",
+ color: debug_colors::ORANGE,
+};
+const GPU_TAG_BRUSH_IMAGE: GpuProfileTag = GpuProfileTag {
+ label: "B_Image",
+ color: debug_colors::SPRINGGREEN,
+};
+const GPU_TAG_BRUSH_SOLID: GpuProfileTag = GpuProfileTag {
+ label: "B_Solid",
+ color: debug_colors::RED,
+};
+const GPU_TAG_CACHE_CLIP: GpuProfileTag = GpuProfileTag {
+ label: "C_Clip",
+ color: debug_colors::PURPLE,
+};
+const GPU_TAG_CACHE_BORDER: GpuProfileTag = GpuProfileTag {
+ label: "C_Border",
+ color: debug_colors::CORNSILK,
+};
+const GPU_TAG_CACHE_LINE_DECORATION: GpuProfileTag = GpuProfileTag {
+ label: "C_LineDecoration",
+ color: debug_colors::YELLOWGREEN,
+};
+const GPU_TAG_CACHE_FAST_LINEAR_GRADIENT: GpuProfileTag = GpuProfileTag {
+ label: "C_FastLinearGradient",
+ color: debug_colors::BROWN,
+};
+const GPU_TAG_CACHE_LINEAR_GRADIENT: GpuProfileTag = GpuProfileTag {
+ label: "C_LinearGradient",
+ color: debug_colors::BROWN,
+};
+const GPU_TAG_CACHE_RADIAL_GRADIENT: GpuProfileTag = GpuProfileTag {
+ label: "C_RadialGradient",
+ color: debug_colors::BROWN,
+};
+const GPU_TAG_CACHE_CONIC_GRADIENT: GpuProfileTag = GpuProfileTag {
+ label: "C_ConicGradient",
+ color: debug_colors::BROWN,
+};
+const GPU_TAG_SETUP_TARGET: GpuProfileTag = GpuProfileTag {
+ label: "target init",
+ color: debug_colors::SLATEGREY,
+};
+const GPU_TAG_SETUP_DATA: GpuProfileTag = GpuProfileTag {
+ label: "data init",
+ color: debug_colors::LIGHTGREY,
+};
+const GPU_TAG_PRIM_SPLIT_COMPOSITE: GpuProfileTag = GpuProfileTag {
+ label: "SplitComposite",
+ color: debug_colors::DARKBLUE,
+};
+const GPU_TAG_PRIM_TEXT_RUN: GpuProfileTag = GpuProfileTag {
+ label: "TextRun",
+ color: debug_colors::BLUE,
+};
+const GPU_TAG_PRIMITIVE: GpuProfileTag = GpuProfileTag {
+ label: "Primitive",
+ color: debug_colors::RED,
+};
+const GPU_TAG_INDIRECT_PRIM: GpuProfileTag = GpuProfileTag {
+ label: "Primitive (indirect)",
+ color: debug_colors::YELLOWGREEN,
+};
+const GPU_TAG_INDIRECT_MASK: GpuProfileTag = GpuProfileTag {
+ label: "Mask (indirect)",
+ color: debug_colors::IVORY,
+};
+const GPU_TAG_BLUR: GpuProfileTag = GpuProfileTag {
+ label: "Blur",
+ color: debug_colors::VIOLET,
+};
+const GPU_TAG_BLIT: GpuProfileTag = GpuProfileTag {
+ label: "Blit",
+ color: debug_colors::LIME,
+};
+const GPU_TAG_SCALE: GpuProfileTag = GpuProfileTag {
+ label: "Scale",
+ color: debug_colors::GHOSTWHITE,
+};
+const GPU_SAMPLER_TAG_ALPHA: GpuProfileTag = GpuProfileTag {
+ label: "Alpha targets",
+ color: debug_colors::BLACK,
+};
+const GPU_SAMPLER_TAG_OPAQUE: GpuProfileTag = GpuProfileTag {
+ label: "Opaque pass",
+ color: debug_colors::BLACK,
+};
+const GPU_SAMPLER_TAG_TRANSPARENT: GpuProfileTag = GpuProfileTag {
+ label: "Transparent pass",
+ color: debug_colors::BLACK,
+};
+const GPU_TAG_SVG_FILTER: GpuProfileTag = GpuProfileTag {
+ label: "SvgFilter",
+ color: debug_colors::LEMONCHIFFON,
+};
+const GPU_TAG_COMPOSITE: GpuProfileTag = GpuProfileTag {
+ label: "Composite",
+ color: debug_colors::TOMATO,
+};
+const GPU_TAG_CLEAR: GpuProfileTag = GpuProfileTag {
+ label: "Clear",
+ color: debug_colors::CHOCOLATE,
+};
+
+/// The clear color used for the texture cache when the debug display is enabled.
+/// We use a shade of blue so that we can still identify completely blue items in
+/// the texture cache.
+pub const TEXTURE_CACHE_DBG_CLEAR_COLOR: [f32; 4] = [0.0, 0.0, 0.8, 1.0];
+
+impl BatchKind {
+ fn sampler_tag(&self) -> GpuProfileTag {
+ match *self {
+ BatchKind::SplitComposite => GPU_TAG_PRIM_SPLIT_COMPOSITE,
+ BatchKind::Brush(kind) => {
+ match kind {
+ BrushBatchKind::Solid => GPU_TAG_BRUSH_SOLID,
+ BrushBatchKind::Image(..) => GPU_TAG_BRUSH_IMAGE,
+ BrushBatchKind::Blend => GPU_TAG_BRUSH_BLEND,
+ BrushBatchKind::MixBlend { .. } => GPU_TAG_BRUSH_MIXBLEND,
+ BrushBatchKind::YuvImage(..) => GPU_TAG_BRUSH_YUV_IMAGE,
+ BrushBatchKind::LinearGradient => GPU_TAG_BRUSH_LINEAR_GRADIENT,
+ BrushBatchKind::Opacity => GPU_TAG_BRUSH_OPACITY,
+ }
+ }
+ BatchKind::TextRun(_) => GPU_TAG_PRIM_TEXT_RUN,
+ BatchKind::Primitive => GPU_TAG_PRIMITIVE,
+ }
+ }
+}
+
+fn flag_changed(before: DebugFlags, after: DebugFlags, select: DebugFlags) -> Option<bool> {
+ if before & select != after & select {
+ Some(after.contains(select))
+ } else {
+ None
+ }
+}
+
+#[repr(C)]
+#[derive(Copy, Clone, Debug)]
+pub enum ShaderColorMode {
+ FromRenderPassMode = 0,
+ Alpha = 1,
+ SubpixelWithBgColorPass0 = 2,
+ SubpixelWithBgColorPass1 = 3,
+ SubpixelWithBgColorPass2 = 4,
+ SubpixelDualSource = 5,
+ BitmapShadow = 6,
+ ColorBitmap = 7,
+ Image = 8,
+ MultiplyDualSource = 9,
+}
+
+impl From<GlyphFormat> for ShaderColorMode {
+ fn from(format: GlyphFormat) -> ShaderColorMode {
+ match format {
+ GlyphFormat::Alpha |
+ GlyphFormat::TransformedAlpha |
+ GlyphFormat::Bitmap => ShaderColorMode::Alpha,
+ GlyphFormat::Subpixel | GlyphFormat::TransformedSubpixel => {
+ panic!("Subpixel glyph formats must be handled separately.");
+ }
+ GlyphFormat::ColorBitmap => ShaderColorMode::ColorBitmap,
+ }
+ }
+}
+
+/// Enumeration of the texture samplers used across the various WebRender shaders.
+///
+/// Each variant corresponds to a uniform declared in shader source. We only bind
+/// the variants we need for a given shader, so not every variant is bound for every
+/// batch.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub(crate) enum TextureSampler {
+ Color0,
+ Color1,
+ Color2,
+ GpuCache,
+ TransformPalette,
+ RenderTasks,
+ Dither,
+ PrimitiveHeadersF,
+ PrimitiveHeadersI,
+ ClipMask,
+ GpuBuffer,
+}
+
+impl TextureSampler {
+ pub(crate) fn color(n: usize) -> TextureSampler {
+ match n {
+ 0 => TextureSampler::Color0,
+ 1 => TextureSampler::Color1,
+ 2 => TextureSampler::Color2,
+ _ => {
+ panic!("There are only 3 color samplers.");
+ }
+ }
+ }
+}
+
+impl Into<TextureSlot> for TextureSampler {
+ fn into(self) -> TextureSlot {
+ match self {
+ TextureSampler::Color0 => TextureSlot(0),
+ TextureSampler::Color1 => TextureSlot(1),
+ TextureSampler::Color2 => TextureSlot(2),
+ TextureSampler::GpuCache => TextureSlot(3),
+ TextureSampler::TransformPalette => TextureSlot(4),
+ TextureSampler::RenderTasks => TextureSlot(5),
+ TextureSampler::Dither => TextureSlot(6),
+ TextureSampler::PrimitiveHeadersF => TextureSlot(7),
+ TextureSampler::PrimitiveHeadersI => TextureSlot(8),
+ TextureSampler::ClipMask => TextureSlot(9),
+ TextureSampler::GpuBuffer => TextureSlot(10),
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum GraphicsApi {
+ OpenGL,
+}
+
+#[derive(Clone, Debug)]
+pub struct GraphicsApiInfo {
+ pub kind: GraphicsApi,
+ pub renderer: String,
+ pub version: String,
+}
+
+#[derive(Debug)]
+pub struct GpuProfile {
+ pub frame_id: GpuFrameId,
+ pub paint_time_ns: u64,
+}
+
+impl GpuProfile {
+ fn new(frame_id: GpuFrameId, timers: &[GpuTimer]) -> GpuProfile {
+ let mut paint_time_ns = 0;
+ for timer in timers {
+ paint_time_ns += timer.time_ns;
+ }
+ GpuProfile {
+ frame_id,
+ paint_time_ns,
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct CpuProfile {
+ pub frame_id: GpuFrameId,
+ pub backend_time_ns: u64,
+ pub composite_time_ns: u64,
+ pub draw_calls: usize,
+}
+
+impl CpuProfile {
+ fn new(
+ frame_id: GpuFrameId,
+ backend_time_ns: u64,
+ composite_time_ns: u64,
+ draw_calls: usize,
+ ) -> CpuProfile {
+ CpuProfile {
+ frame_id,
+ backend_time_ns,
+ composite_time_ns,
+ draw_calls,
+ }
+ }
+}
+
+/// The selected partial present mode for a given frame.
+#[derive(Debug, Copy, Clone)]
+enum PartialPresentMode {
+ /// The device supports fewer dirty rects than the number of dirty rects
+ /// that WR produced. In this case, the WR dirty rects are union'ed into
+ /// a single dirty rect, that is provided to the caller.
+ Single {
+ dirty_rect: DeviceRect,
+ },
+}
+
+struct CacheTexture {
+ texture: Texture,
+ category: TextureCacheCategory,
+}
+
+/// Helper struct for resolving device Textures for use during rendering passes.
+///
+/// Manages the mapping between the at-a-distance texture handles used by the
+/// `RenderBackend` (which does not directly interface with the GPU) and actual
+/// device texture handles.
+struct TextureResolver {
+ /// A map to resolve texture cache IDs to native textures.
+ texture_cache_map: FastHashMap<CacheTextureId, CacheTexture>,
+
+ /// Map of external image IDs to native textures.
+ external_images: FastHashMap<DeferredResolveIndex, ExternalTexture>,
+
+ /// A special 1x1 dummy texture used for shaders that expect to work with
+ /// the output of the previous pass but are actually running in the first
+ /// pass.
+ dummy_cache_texture: Texture,
+}
+
+impl TextureResolver {
+ fn new(device: &mut Device) -> TextureResolver {
+ let dummy_cache_texture = device
+ .create_texture(
+ ImageBufferKind::Texture2D,
+ ImageFormat::RGBA8,
+ 1,
+ 1,
+ TextureFilter::Linear,
+ None,
+ );
+ device.upload_texture_immediate(
+ &dummy_cache_texture,
+ &[0xff, 0xff, 0xff, 0xff],
+ );
+
+ TextureResolver {
+ texture_cache_map: FastHashMap::default(),
+ external_images: FastHashMap::default(),
+ dummy_cache_texture,
+ }
+ }
+
+ fn deinit(self, device: &mut Device) {
+ device.delete_texture(self.dummy_cache_texture);
+
+ for (_id, item) in self.texture_cache_map {
+ device.delete_texture(item.texture);
+ }
+ }
+
+ fn begin_frame(&mut self) {
+ }
+
+ fn end_pass(
+ &mut self,
+ device: &mut Device,
+ textures_to_invalidate: &[CacheTextureId],
+ ) {
+ // For any texture that is no longer needed, immediately
+ // invalidate it so that tiled GPUs don't need to resolve it
+ // back to memory.
+ for texture_id in textures_to_invalidate {
+ let render_target = &self.texture_cache_map[texture_id].texture;
+ device.invalidate_render_target(render_target);
+ }
+ }
+
+ // Bind a source texture to the device.
+ fn bind(&self, texture_id: &TextureSource, sampler: TextureSampler, device: &mut Device) -> Swizzle {
+ match *texture_id {
+ TextureSource::Invalid => {
+ Swizzle::default()
+ }
+ TextureSource::Dummy => {
+ let swizzle = Swizzle::default();
+ device.bind_texture(sampler, &self.dummy_cache_texture, swizzle);
+ swizzle
+ }
+ TextureSource::External(ref index, _) => {
+ let texture = self.external_images
+ .get(index)
+ .expect("BUG: External image should be resolved by now");
+ device.bind_external_texture(sampler, texture);
+ Swizzle::default()
+ }
+ TextureSource::TextureCache(index, swizzle) => {
+ let texture = &self.texture_cache_map[&index].texture;
+ device.bind_texture(sampler, texture, swizzle);
+ swizzle
+ }
+ }
+ }
+
+ // Get the real (OpenGL) texture ID for a given source texture.
+ // For a texture cache texture, the IDs are stored in a vector
+ // map for fast access.
+ fn resolve(&self, texture_id: &TextureSource) -> Option<(&Texture, Swizzle)> {
+ match *texture_id {
+ TextureSource::Invalid => None,
+ TextureSource::Dummy => {
+ Some((&self.dummy_cache_texture, Swizzle::default()))
+ }
+ TextureSource::External(..) => {
+ panic!("BUG: External textures cannot be resolved, they can only be bound.");
+ }
+ TextureSource::TextureCache(index, swizzle) => {
+ Some((&self.texture_cache_map[&index].texture, swizzle))
+ }
+ }
+ }
+
+ // Retrieve the deferred / resolved UV rect if an external texture, otherwise
+ // return the default supplied UV rect.
+ fn get_uv_rect(
+ &self,
+ source: &TextureSource,
+ default_value: TexelRect,
+ ) -> TexelRect {
+ match source {
+ TextureSource::External(ref index, _) => {
+ let texture = self.external_images
+ .get(index)
+ .expect("BUG: External image should be resolved by now");
+ texture.get_uv_rect()
+ }
+ _ => {
+ default_value
+ }
+ }
+ }
+
+ /// Returns the size of the texture in pixels
+ fn get_texture_size(&self, texture: &TextureSource) -> DeviceIntSize {
+ match *texture {
+ TextureSource::Invalid => DeviceIntSize::zero(),
+ TextureSource::TextureCache(id, _) => {
+ self.texture_cache_map[&id].texture.get_dimensions()
+ },
+ TextureSource::External(index, _) => {
+ let uv_rect = self.external_images[&index].get_uv_rect();
+ (uv_rect.uv1 - uv_rect.uv0).abs().to_size().to_i32()
+ },
+ TextureSource::Dummy => DeviceIntSize::new(1, 1),
+ }
+ }
+
+ fn report_memory(&self) -> MemoryReport {
+ let mut report = MemoryReport::default();
+
+ // We're reporting GPU memory rather than heap-allocations, so we don't
+ // use size_of_op.
+ for item in self.texture_cache_map.values() {
+ let counter = match item.category {
+ TextureCacheCategory::Atlas => &mut report.atlas_textures,
+ TextureCacheCategory::Standalone => &mut report.standalone_textures,
+ TextureCacheCategory::PictureTile => &mut report.picture_tile_textures,
+ TextureCacheCategory::RenderTarget => &mut report.render_target_textures,
+ };
+ *counter += item.texture.size_in_bytes();
+ }
+
+ report
+ }
+
+ fn update_profile(&self, profile: &mut TransactionProfile) {
+ let mut external_image_bytes = 0;
+ for img in self.external_images.values() {
+ let uv_rect = img.get_uv_rect();
+ let size = (uv_rect.uv1 - uv_rect.uv0).abs().to_size().to_i32();
+
+ // Assume 4 bytes per pixels which is true most of the time but
+ // not always.
+ let bpp = 4;
+ external_image_bytes += size.area() as usize * bpp;
+ }
+
+ profile.set(profiler::EXTERNAL_IMAGE_BYTES, profiler::bytes_to_mb(external_image_bytes));
+ }
+
+ fn get_cache_texture_mut(&mut self, id: &CacheTextureId) -> &mut Texture {
+ &mut self.texture_cache_map
+ .get_mut(id)
+ .expect("bug: texture not allocated")
+ .texture
+ }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum BlendMode {
+ None,
+ Alpha,
+ PremultipliedAlpha,
+ PremultipliedDestOut,
+ SubpixelDualSource,
+ SubpixelWithBgColor,
+ Advanced(MixBlendMode),
+ MultiplyDualSource,
+ Screen,
+ Exclusion,
+ PlusLighter,
+}
+
+impl BlendMode {
+ /// Decides when a given mix-blend-mode can be implemented in terms of
+ /// simple blending, dual-source blending, advanced blending, or not at
+ /// all based on available capabilities.
+ pub fn from_mix_blend_mode(
+ mode: MixBlendMode,
+ advanced_blend: bool,
+ coherent: bool,
+ dual_source: bool,
+ ) -> Option<BlendMode> {
+ // If we emulate a mix-blend-mode via simple or dual-source blending,
+ // care must be taken to output alpha As + Ad*(1-As) regardless of what
+ // the RGB output is to comply with the mix-blend-mode spec.
+ Some(match mode {
+ // If we have coherent advanced blend, just use that.
+ _ if advanced_blend && coherent => BlendMode::Advanced(mode),
+ // Screen can be implemented as Cs + Cd - Cs*Cd => Cs + Cd*(1-Cs)
+ MixBlendMode::Screen => BlendMode::Screen,
+ // Exclusion can be implemented as Cs + Cd - 2*Cs*Cd => Cs*(1-Cd) + Cd*(1-Cs)
+ MixBlendMode::Exclusion => BlendMode::Exclusion,
+ // PlusLighter is basically a clamped add.
+ MixBlendMode::PlusLighter => BlendMode::PlusLighter,
+ // Multiply can be implemented as Cs*Cd + Cs*(1-Ad) + Cd*(1-As) => Cs*(1-Ad) + Cd*(1 - SRC1=(As-Cs))
+ MixBlendMode::Multiply if dual_source => BlendMode::MultiplyDualSource,
+ // Otherwise, use advanced blend without coherency if available.
+ _ if advanced_blend => BlendMode::Advanced(mode),
+ // If advanced blend is not available, then we have to use brush_mix_blend.
+ _ => return None,
+ })
+ }
+}
+
+/// Information about the state of the debugging / profiler overlay in native compositing mode.
+struct DebugOverlayState {
+ /// True if any of the current debug flags will result in drawing a debug overlay.
+ is_enabled: bool,
+
+ /// The current size of the debug overlay surface. None implies that the
+ /// debug surface isn't currently allocated.
+ current_size: Option<DeviceIntSize>,
+}
+
+impl DebugOverlayState {
+ fn new() -> Self {
+ DebugOverlayState {
+ is_enabled: false,
+ current_size: None,
+ }
+ }
+}
+
+/// Tracks buffer damage rects over a series of frames.
+#[derive(Debug, Default)]
+pub(crate) struct BufferDamageTracker {
+ damage_rects: [DeviceRect; 2],
+ current_offset: usize,
+}
+
+impl BufferDamageTracker {
+ /// Sets the damage rect for the current frame. Should only be called *after*
+ /// get_damage_rect() has been called to get the current backbuffer's damage rect.
+ fn push_dirty_rect(&mut self, rect: &DeviceRect) {
+ self.damage_rects[self.current_offset] = rect.clone();
+ self.current_offset = match self.current_offset {
+ 0 => self.damage_rects.len() - 1,
+ n => n - 1,
+ }
+ }
+
+ /// Gets the damage rect for the current backbuffer, given the backbuffer's age.
+ /// (The number of frames since it was previously the backbuffer.)
+ /// Returns an empty rect if the buffer is valid, and None if the entire buffer is invalid.
+ fn get_damage_rect(&self, buffer_age: usize) -> Option<DeviceRect> {
+ match buffer_age {
+ // 0 means this is a new buffer, so is completely invalid.
+ 0 => None,
+ // 1 means this backbuffer was also the previous frame's backbuffer
+ // (so must have been copied to the frontbuffer). It is therefore entirely valid.
+ 1 => Some(DeviceRect::zero()),
+ // We must calculate the union of the damage rects since this buffer was previously
+ // the backbuffer.
+ n if n <= self.damage_rects.len() + 1 => {
+ Some(
+ self.damage_rects.iter()
+ .cycle()
+ .skip(self.current_offset + 1)
+ .take(n - 1)
+ .fold(DeviceRect::zero(), |acc, r| acc.union(r))
+ )
+ }
+ // The backbuffer is older than the number of frames for which we track,
+ // so we treat it as entirely invalid.
+ _ => None,
+ }
+ }
+}
+
+/// The renderer is responsible for submitting to the GPU the work prepared by the
+/// RenderBackend.
+///
+/// We have a separate `Renderer` instance for each instance of WebRender (generally
+/// one per OS window), and all instances share the same thread.
+pub struct Renderer {
+ result_rx: Receiver<ResultMsg>,
+ api_tx: Sender<ApiMsg>,
+ pub device: Device,
+ pending_texture_updates: Vec<TextureUpdateList>,
+ /// True if there are any TextureCacheUpdate pending.
+ pending_texture_cache_updates: bool,
+ pending_native_surface_updates: Vec<NativeSurfaceOperation>,
+ pending_gpu_cache_updates: Vec<GpuCacheUpdateList>,
+ pending_gpu_cache_clear: bool,
+ pending_shader_updates: Vec<PathBuf>,
+ active_documents: FastHashMap<DocumentId, RenderedDocument>,
+
+ shaders: Rc<RefCell<Shaders>>,
+
+ max_recorded_profiles: usize,
+
+ clear_color: ColorF,
+ enable_clear_scissor: bool,
+ enable_advanced_blend_barriers: bool,
+ clear_caches_with_quads: bool,
+ clear_alpha_targets_with_quads: bool,
+
+ debug: debug::LazyInitializedDebugRenderer,
+ debug_flags: DebugFlags,
+ profile: TransactionProfile,
+ frame_counter: u64,
+ resource_upload_time: f64,
+ gpu_cache_upload_time: f64,
+ profiler: Profiler,
+
+ last_time: u64,
+
+ pub gpu_profiler: GpuProfiler,
+ vaos: vertex::RendererVAOs,
+
+ gpu_cache_texture: gpu_cache::GpuCacheTexture,
+ vertex_data_textures: Vec<vertex::VertexDataTextures>,
+ current_vertex_data_textures: usize,
+
+ /// When the GPU cache debugger is enabled, we keep track of the live blocks
+ /// in the GPU cache so that we can use them for the debug display. This
+ /// member stores those live blocks, indexed by row.
+ gpu_cache_debug_chunks: Vec<Vec<GpuCacheDebugChunk>>,
+
+ gpu_cache_frame_id: FrameId,
+ gpu_cache_overflow: bool,
+
+ pipeline_info: PipelineInfo,
+
+ // Manages and resolves source textures IDs to real texture IDs.
+ texture_resolver: TextureResolver,
+
+ texture_upload_pbo_pool: UploadPBOPool,
+ staging_texture_pool: UploadTexturePool,
+
+ dither_matrix_texture: Option<Texture>,
+
+ /// Optional trait object that allows the client
+ /// application to provide external buffers for image data.
+ external_image_handler: Option<Box<dyn ExternalImageHandler>>,
+
+ /// Optional function pointers for measuring memory used by a given
+ /// heap-allocated pointer.
+ size_of_ops: Option<MallocSizeOfOps>,
+
+ pub renderer_errors: Vec<RendererError>,
+
+ pub(in crate) async_frame_recorder: Option<AsyncScreenshotGrabber>,
+ pub(in crate) async_screenshots: Option<AsyncScreenshotGrabber>,
+
+ /// List of profile results from previous frames. Can be retrieved
+ /// via get_frame_profiles().
+ cpu_profiles: VecDeque<CpuProfile>,
+ gpu_profiles: VecDeque<GpuProfile>,
+
+ /// Notification requests to be fulfilled after rendering.
+ notifications: Vec<NotificationRequest>,
+
+ device_size: Option<DeviceIntSize>,
+
+ /// A lazily created texture for the zoom debugging widget.
+ zoom_debug_texture: Option<Texture>,
+
+ /// The current mouse position. This is used for debugging
+ /// functionality only, such as the debug zoom widget.
+ cursor_position: DeviceIntPoint,
+
+ /// Guards to check if we might be rendering a frame with expired texture
+ /// cache entries.
+ shared_texture_cache_cleared: bool,
+
+ /// The set of documents which we've seen a publish for since last render.
+ documents_seen: FastHashSet<DocumentId>,
+
+ #[cfg(feature = "capture")]
+ read_fbo: FBOId,
+ #[cfg(feature = "replay")]
+ owned_external_images: FastHashMap<(ExternalImageId, u8), ExternalTexture>,
+
+ /// The compositing config, affecting how WR composites into the final scene.
+ compositor_config: CompositorConfig,
+
+ current_compositor_kind: CompositorKind,
+
+ /// Maintains a set of allocated native composite surfaces. This allows any
+ /// currently allocated surfaces to be cleaned up as soon as deinit() is
+ /// called (the normal bookkeeping for native surfaces exists in the
+ /// render backend thread).
+ allocated_native_surfaces: FastHashSet<NativeSurfaceId>,
+
+ /// If true, partial present state has been reset and everything needs to
+ /// be drawn on the next render.
+ force_redraw: bool,
+
+ /// State related to the debug / profiling overlays
+ debug_overlay_state: DebugOverlayState,
+
+ /// Tracks the dirty rectangles from previous frames. Used on platforms
+ /// that require keeping the front buffer fully correct when doing
+ /// partial present (e.g. unix desktop with EGL_EXT_buffer_age).
+ buffer_damage_tracker: BufferDamageTracker,
+
+ max_primitive_instance_count: usize,
+ enable_instancing: bool,
+
+ /// Count consecutive oom frames to detectif we are stuck unable to render
+ /// in a loop.
+ consecutive_oom_frames: u32,
+
+ /// update() defers processing of ResultMsg, if frame_publish_id of
+ /// ResultMsg::PublishDocument exceeds target_frame_publish_id.
+ target_frame_publish_id: Option<FramePublishId>,
+
+ /// Hold a next ResultMsg that will be handled by update().
+ pending_result_msg: Option<ResultMsg>,
+}
+
+#[derive(Debug)]
+pub enum RendererError {
+ Shader(ShaderError),
+ Thread(std::io::Error),
+ MaxTextureSize,
+ SoftwareRasterizer,
+ OutOfMemory,
+}
+
+impl From<ShaderError> for RendererError {
+ fn from(err: ShaderError) -> Self {
+ RendererError::Shader(err)
+ }
+}
+
+impl From<std::io::Error> for RendererError {
+ fn from(err: std::io::Error) -> Self {
+ RendererError::Thread(err)
+ }
+}
+
+impl Renderer {
+ pub fn device_size(&self) -> Option<DeviceIntSize> {
+ self.device_size
+ }
+
+ /// Update the current position of the debug cursor.
+ pub fn set_cursor_position(
+ &mut self,
+ position: DeviceIntPoint,
+ ) {
+ self.cursor_position = position;
+ }
+
+ pub fn get_max_texture_size(&self) -> i32 {
+ self.device.max_texture_size()
+ }
+
+ pub fn get_graphics_api_info(&self) -> GraphicsApiInfo {
+ GraphicsApiInfo {
+ kind: GraphicsApi::OpenGL,
+ version: self.device.gl().get_string(gl::VERSION),
+ renderer: self.device.gl().get_string(gl::RENDERER),
+ }
+ }
+
+ pub fn preferred_color_format(&self) -> ImageFormat {
+ self.device.preferred_color_formats().external
+ }
+
+ pub fn required_texture_stride_alignment(&self, format: ImageFormat) -> usize {
+ self.device.required_pbo_stride().num_bytes(format).get()
+ }
+
+ pub fn set_clear_color(&mut self, color: ColorF) {
+ self.clear_color = color;
+ }
+
+ pub fn flush_pipeline_info(&mut self) -> PipelineInfo {
+ mem::replace(&mut self.pipeline_info, PipelineInfo::default())
+ }
+
+ /// Returns the Epoch of the current frame in a pipeline.
+ pub fn current_epoch(&self, document_id: DocumentId, pipeline_id: PipelineId) -> Option<Epoch> {
+ self.pipeline_info.epochs.get(&(pipeline_id, document_id)).cloned()
+ }
+
+ fn get_next_result_msg(&mut self) -> Option<ResultMsg> {
+ if self.pending_result_msg.is_none() {
+ if let Ok(msg) = self.result_rx.try_recv() {
+ self.pending_result_msg = Some(msg);
+ }
+ }
+
+ match (&self.pending_result_msg, &self.target_frame_publish_id) {
+ (Some(ResultMsg::PublishDocument(frame_publish_id, _, _, _)), Some(target_id)) => {
+ if frame_publish_id > target_id {
+ return None;
+ }
+ }
+ _ => {}
+ }
+
+ self.pending_result_msg.take()
+ }
+
+ /// Processes the result queue.
+ ///
+ /// Should be called before `render()`, as texture cache updates are done here.
+ pub fn update(&mut self) {
+ profile_scope!("update");
+
+ // Pull any pending results and return the most recent.
+ while let Some(msg) = self.get_next_result_msg() {
+ match msg {
+ ResultMsg::PublishPipelineInfo(mut pipeline_info) => {
+ for ((pipeline_id, document_id), epoch) in pipeline_info.epochs {
+ self.pipeline_info.epochs.insert((pipeline_id, document_id), epoch);
+ }
+ self.pipeline_info.removed_pipelines.extend(pipeline_info.removed_pipelines.drain(..));
+ }
+ ResultMsg::PublishDocument(
+ _,
+ document_id,
+ mut doc,
+ resource_update_list,
+ ) => {
+ // Add a new document to the active set
+
+ // If the document we are replacing must be drawn (in order to
+ // update the texture cache), issue a render just to
+ // off-screen targets, ie pass None to render_impl. We do this
+ // because a) we don't need to render to the main framebuffer
+ // so it is cheaper not to, and b) doing so without a
+ // subsequent present would break partial present.
+ if let Some(mut prev_doc) = self.active_documents.remove(&document_id) {
+ doc.profile.merge(&mut prev_doc.profile);
+
+ if prev_doc.frame.must_be_drawn() {
+ prev_doc.render_reasons |= RenderReasons::TEXTURE_CACHE_FLUSH;
+ self.render_impl(
+ document_id,
+ &mut prev_doc,
+ None,
+ 0,
+ ).ok();
+ }
+ }
+
+ self.active_documents.insert(document_id, doc);
+
+ // IMPORTANT: The pending texture cache updates must be applied
+ // *after* the previous frame has been rendered above
+ // (if neceessary for a texture cache update). For
+ // an example of why this is required:
+ // 1) Previous frame contains a render task that
+ // targets Texture X.
+ // 2) New frame contains a texture cache update which
+ // frees Texture X.
+ // 3) bad stuff happens.
+
+ //TODO: associate `document_id` with target window
+ self.pending_texture_cache_updates |= !resource_update_list.texture_updates.updates.is_empty();
+ self.pending_texture_updates.push(resource_update_list.texture_updates);
+ self.pending_native_surface_updates.extend(resource_update_list.native_surface_updates);
+ self.documents_seen.insert(document_id);
+ }
+ ResultMsg::UpdateGpuCache(mut list) => {
+ if list.clear {
+ self.pending_gpu_cache_clear = true;
+ }
+ if list.clear {
+ self.gpu_cache_debug_chunks = Vec::new();
+ }
+ for cmd in mem::replace(&mut list.debug_commands, Vec::new()) {
+ match cmd {
+ GpuCacheDebugCmd::Alloc(chunk) => {
+ let row = chunk.address.v as usize;
+ if row >= self.gpu_cache_debug_chunks.len() {
+ self.gpu_cache_debug_chunks.resize(row + 1, Vec::new());
+ }
+ self.gpu_cache_debug_chunks[row].push(chunk);
+ },
+ GpuCacheDebugCmd::Free(address) => {
+ let chunks = &mut self.gpu_cache_debug_chunks[address.v as usize];
+ let pos = chunks.iter()
+ .position(|x| x.address == address).unwrap();
+ chunks.remove(pos);
+ },
+ }
+ }
+ self.pending_gpu_cache_updates.push(list);
+ }
+ ResultMsg::UpdateResources {
+ resource_updates,
+ memory_pressure,
+ } => {
+ if memory_pressure {
+ // If a memory pressure event arrives _after_ a new scene has
+ // been published that writes persistent targets (i.e. cached
+ // render tasks to the texture cache, or picture cache tiles)
+ // but _before_ the next update/render loop, those targets
+ // will not be updated due to the active_documents list being
+ // cleared at the end of this message. To work around that,
+ // if any of the existing documents have not rendered yet, and
+ // have picture/texture cache targets, force a render so that
+ // those targets are updated.
+ let active_documents = mem::replace(
+ &mut self.active_documents,
+ FastHashMap::default(),
+ );
+ for (doc_id, mut doc) in active_documents {
+ if doc.frame.must_be_drawn() {
+ // As this render will not be presented, we must pass None to
+ // render_impl. This avoids interfering with partial present
+ // logic, as well as being more efficient.
+ self.render_impl(
+ doc_id,
+ &mut doc,
+ None,
+ 0,
+ ).ok();
+ }
+ }
+ }
+
+ self.pending_texture_cache_updates |= !resource_updates.texture_updates.updates.is_empty();
+ self.pending_texture_updates.push(resource_updates.texture_updates);
+ self.pending_native_surface_updates.extend(resource_updates.native_surface_updates);
+ self.device.begin_frame();
+
+ self.update_texture_cache();
+ self.update_native_surfaces();
+
+ // Flush the render target pool on memory pressure.
+ //
+ // This needs to be separate from the block below because
+ // the device module asserts if we delete textures while
+ // not in a frame.
+ if memory_pressure {
+ self.texture_upload_pbo_pool.on_memory_pressure(&mut self.device);
+ self.staging_texture_pool.delete_textures(&mut self.device);
+ }
+
+ self.device.end_frame();
+ }
+ ResultMsg::AppendNotificationRequests(mut notifications) => {
+ // We need to know specifically if there are any pending
+ // TextureCacheUpdate updates in any of the entries in
+ // pending_texture_updates. They may simply be nops, which do not
+ // need to prevent issuing the notification, and if so, may not
+ // cause a timely frame render to occur to wake up any listeners.
+ if !self.pending_texture_cache_updates {
+ drain_filter(
+ &mut notifications,
+ |n| { n.when() == Checkpoint::FrameTexturesUpdated },
+ |n| { n.notify(); },
+ );
+ }
+ self.notifications.append(&mut notifications);
+ }
+ ResultMsg::ForceRedraw => {
+ self.force_redraw = true;
+ }
+ ResultMsg::RefreshShader(path) => {
+ self.pending_shader_updates.push(path);
+ }
+ ResultMsg::SetParameter(ref param) => {
+ self.device.set_parameter(param);
+ }
+ ResultMsg::DebugOutput(output) => match output {
+ #[cfg(feature = "capture")]
+ DebugOutput::SaveCapture(config, deferred) => {
+ self.save_capture(config, deferred);
+ }
+ #[cfg(feature = "replay")]
+ DebugOutput::LoadCapture(config, plain_externals) => {
+ self.active_documents.clear();
+ self.load_capture(config, plain_externals);
+ }
+ },
+ ResultMsg::DebugCommand(command) => {
+ self.handle_debug_command(command);
+ }
+ }
+ }
+ }
+
+ /// update() defers processing of ResultMsg, if frame_publish_id of
+ /// ResultMsg::PublishDocument exceeds target_frame_publish_id.
+ pub fn set_target_frame_publish_id(&mut self, publish_id: FramePublishId) {
+ self.target_frame_publish_id = Some(publish_id);
+ }
+
+ fn handle_debug_command(&mut self, command: DebugCommand) {
+ match command {
+ DebugCommand::SetPictureTileSize(_) |
+ DebugCommand::SetMaximumSurfaceSize(_) => {
+ panic!("Should be handled by render backend");
+ }
+ DebugCommand::SaveCapture(..) |
+ DebugCommand::LoadCapture(..) |
+ DebugCommand::StartCaptureSequence(..) |
+ DebugCommand::StopCaptureSequence => {
+ panic!("Capture commands are not welcome here! Did you build with 'capture' feature?")
+ }
+ DebugCommand::ClearCaches(_)
+ | DebugCommand::SimulateLongSceneBuild(_)
+ | DebugCommand::EnableNativeCompositor(_)
+ | DebugCommand::SetBatchingLookback(_) => {}
+ DebugCommand::InvalidateGpuCache => {
+ self.gpu_cache_texture.invalidate();
+ }
+ DebugCommand::SetFlags(flags) => {
+ self.set_debug_flags(flags);
+ }
+ }
+ }
+
+ /// Set a callback for handling external images.
+ pub fn set_external_image_handler(&mut self, handler: Box<dyn ExternalImageHandler>) {
+ self.external_image_handler = Some(handler);
+ }
+
+ /// Retrieve (and clear) the current list of recorded frame profiles.
+ pub fn get_frame_profiles(&mut self) -> (Vec<CpuProfile>, Vec<GpuProfile>) {
+ let cpu_profiles = self.cpu_profiles.drain(..).collect();
+ let gpu_profiles = self.gpu_profiles.drain(..).collect();
+ (cpu_profiles, gpu_profiles)
+ }
+
+ /// Reset the current partial present state. This forces the entire framebuffer
+ /// to be refreshed next time `render` is called.
+ pub fn force_redraw(&mut self) {
+ self.force_redraw = true;
+ }
+
+ /// Renders the current frame.
+ ///
+ /// A Frame is supplied by calling [`generate_frame()`][webrender_api::Transaction::generate_frame].
+ /// buffer_age is the age of the current backbuffer. It is only relevant if partial present
+ /// is active, otherwise 0 should be passed here.
+ pub fn render(
+ &mut self,
+ device_size: DeviceIntSize,
+ buffer_age: usize,
+ ) -> Result<RenderResults, Vec<RendererError>> {
+ self.device_size = Some(device_size);
+
+ // TODO(gw): We want to make the active document that is
+ // being rendered configurable via the public
+ // API in future. For now, just select the last
+ // added document as the active one to render
+ // (Gecko only ever creates a single document
+ // per renderer right now).
+ let doc_id = self.active_documents.keys().last().cloned();
+
+ let result = match doc_id {
+ Some(doc_id) => {
+ // Remove the doc from the map to appease the borrow checker
+ let mut doc = self.active_documents
+ .remove(&doc_id)
+ .unwrap();
+
+ let result = self.render_impl(
+ doc_id,
+ &mut doc,
+ Some(device_size),
+ buffer_age,
+ );
+
+ self.active_documents.insert(doc_id, doc);
+
+ result
+ }
+ None => {
+ self.last_time = precise_time_ns();
+ Ok(RenderResults::default())
+ }
+ };
+
+ drain_filter(
+ &mut self.notifications,
+ |n| { n.when() == Checkpoint::FrameRendered },
+ |n| { n.notify(); },
+ );
+
+ let mut oom = false;
+ if let Err(ref errors) = result {
+ for error in errors {
+ if matches!(error, &RendererError::OutOfMemory) {
+ oom = true;
+ break;
+ }
+ }
+ }
+
+ if oom {
+ let _ = self.api_tx.send(ApiMsg::MemoryPressure);
+ // Ensure we don't get stuck in a loop.
+ self.consecutive_oom_frames += 1;
+ assert!(self.consecutive_oom_frames < 5, "Renderer out of memory");
+ } else {
+ self.consecutive_oom_frames = 0;
+ }
+
+ // This is the end of the rendering pipeline. If some notifications are is still there,
+ // just clear them and they will autimatically fire the Checkpoint::TransactionDropped
+ // event. Otherwise they would just pile up in this vector forever.
+ self.notifications.clear();
+
+ tracy_frame_marker!();
+
+ result
+ }
+
+ /// Update the state of any debug / profiler overlays. This is currently only needed
+ /// when running with the native compositor enabled.
+ fn update_debug_overlay(
+ &mut self,
+ framebuffer_size: DeviceIntSize,
+ has_debug_items: bool,
+ ) {
+ // If any of the following debug flags are set, something will be drawn on the debug overlay.
+ self.debug_overlay_state.is_enabled = has_debug_items || self.debug_flags.intersects(
+ DebugFlags::PROFILER_DBG |
+ DebugFlags::RENDER_TARGET_DBG |
+ DebugFlags::TEXTURE_CACHE_DBG |
+ DebugFlags::EPOCHS |
+ DebugFlags::GPU_CACHE_DBG |
+ DebugFlags::PICTURE_CACHING_DBG |
+ DebugFlags::PRIMITIVE_DBG |
+ DebugFlags::ZOOM_DBG |
+ DebugFlags::WINDOW_VISIBILITY_DBG
+ );
+
+ // Update the debug overlay surface, if we are running in native compositor mode.
+ if let CompositorKind::Native { .. } = self.current_compositor_kind {
+ let compositor = self.compositor_config.compositor().unwrap();
+
+ // If there is a current surface, destroy it if we don't need it for this frame, or if
+ // the size has changed.
+ if let Some(current_size) = self.debug_overlay_state.current_size {
+ if !self.debug_overlay_state.is_enabled || current_size != framebuffer_size {
+ compositor.destroy_surface(&mut self.device, NativeSurfaceId::DEBUG_OVERLAY);
+ self.debug_overlay_state.current_size = None;
+ }
+ }
+
+ // Allocate a new surface, if we need it and there isn't one.
+ if self.debug_overlay_state.is_enabled && self.debug_overlay_state.current_size.is_none() {
+ compositor.create_surface(
+ &mut self.device,
+ NativeSurfaceId::DEBUG_OVERLAY,
+ DeviceIntPoint::zero(),
+ framebuffer_size,
+ false,
+ );
+ compositor.create_tile(
+ &mut self.device,
+ NativeTileId::DEBUG_OVERLAY,
+ );
+ self.debug_overlay_state.current_size = Some(framebuffer_size);
+ }
+ }
+ }
+
+ /// Bind a draw target for the debug / profiler overlays, if required.
+ fn bind_debug_overlay(&mut self, device_size: DeviceIntSize) -> Option<DrawTarget> {
+ // Debug overlay setup are only required in native compositing mode
+ if self.debug_overlay_state.is_enabled {
+ if let CompositorKind::Native { .. } = self.current_compositor_kind {
+ let compositor = self.compositor_config.compositor().unwrap();
+ let surface_size = self.debug_overlay_state.current_size.unwrap();
+
+ // Ensure old surface is invalidated before binding
+ compositor.invalidate_tile(
+ &mut self.device,
+ NativeTileId::DEBUG_OVERLAY,
+ DeviceIntRect::from_size(surface_size),
+ );
+ // Bind the native surface
+ let surface_info = compositor.bind(
+ &mut self.device,
+ NativeTileId::DEBUG_OVERLAY,
+ DeviceIntRect::from_size(surface_size),
+ DeviceIntRect::from_size(surface_size),
+ );
+
+ // Bind the native surface to current FBO target
+ let draw_target = DrawTarget::NativeSurface {
+ offset: surface_info.origin,
+ external_fbo_id: surface_info.fbo_id,
+ dimensions: surface_size,
+ };
+ self.device.bind_draw_target(draw_target);
+
+ // When native compositing, clear the debug overlay each frame.
+ self.device.clear_target(
+ Some([0.0, 0.0, 0.0, 0.0]),
+ None, // debug renderer does not use depth
+ None,
+ );
+
+ Some(draw_target)
+ } else {
+ // If we're not using the native compositor, then the default
+ // frame buffer is already bound. Create a DrawTarget for it and
+ // return it.
+ Some(DrawTarget::new_default(device_size, self.device.surface_origin_is_top_left()))
+ }
+ } else {
+ None
+ }
+ }
+
+ /// Unbind the draw target for debug / profiler overlays, if required.
+ fn unbind_debug_overlay(&mut self) {
+ // Debug overlay setup are only required in native compositing mode
+ if self.debug_overlay_state.is_enabled {
+ if let CompositorKind::Native { .. } = self.current_compositor_kind {
+ let compositor = self.compositor_config.compositor().unwrap();
+ // Unbind the draw target and add it to the visual tree to be composited
+ compositor.unbind(&mut self.device);
+
+ compositor.add_surface(
+ &mut self.device,
+ NativeSurfaceId::DEBUG_OVERLAY,
+ CompositorSurfaceTransform::identity(),
+ DeviceIntRect::from_size(
+ self.debug_overlay_state.current_size.unwrap(),
+ ),
+ ImageRendering::Auto,
+ );
+ }
+ }
+ }
+
+ // If device_size is None, don't render to the main frame buffer. This is useful to
+ // update texture cache render tasks but avoid doing a full frame render. If the
+ // render is not going to be presented, then this must be set to None, as performing a
+ // composite without a present will confuse partial present.
+ fn render_impl(
+ &mut self,
+ doc_id: DocumentId,
+ active_doc: &mut RenderedDocument,
+ device_size: Option<DeviceIntSize>,
+ buffer_age: usize,
+ ) -> Result<RenderResults, Vec<RendererError>> {
+ profile_scope!("render");
+ let mut results = RenderResults::default();
+ self.profile.start_time(profiler::RENDERER_TIME);
+
+ self.staging_texture_pool.begin_frame();
+
+ let compositor_kind = active_doc.frame.composite_state.compositor_kind;
+ // CompositorKind is updated
+ if self.current_compositor_kind != compositor_kind {
+ let enable = match (self.current_compositor_kind, compositor_kind) {
+ (CompositorKind::Native { .. }, CompositorKind::Draw { .. }) => {
+ if self.debug_overlay_state.current_size.is_some() {
+ self.compositor_config
+ .compositor()
+ .unwrap()
+ .destroy_surface(&mut self.device, NativeSurfaceId::DEBUG_OVERLAY);
+ self.debug_overlay_state.current_size = None;
+ }
+ false
+ }
+ (CompositorKind::Draw { .. }, CompositorKind::Native { .. }) => {
+ true
+ }
+ (current_compositor_kind, active_doc_compositor_kind) => {
+ warn!("Compositor mismatch, assuming this is Wrench running. Current {:?}, active {:?}",
+ current_compositor_kind, active_doc_compositor_kind);
+ false
+ }
+ };
+
+ if let Some(config) = self.compositor_config.compositor() {
+ config.enable_native_compositor(&mut self.device, enable);
+ }
+ self.current_compositor_kind = compositor_kind;
+ }
+
+ // The texture resolver scope should be outside of any rendering, including
+ // debug rendering. This ensures that when we return render targets to the
+ // pool via glInvalidateFramebuffer, we don't do any debug rendering after
+ // that point. Otherwise, the bind / invalidate / bind logic trips up the
+ // render pass logic in tiled / mobile GPUs, resulting in an extra copy /
+ // resolve step when the debug overlay is enabled.
+ self.texture_resolver.begin_frame();
+
+ if let Some(device_size) = device_size {
+ self.update_gpu_profile(device_size);
+ }
+
+ let cpu_frame_id = {
+ let _gm = self.gpu_profiler.start_marker("begin frame");
+ let frame_id = self.device.begin_frame();
+ self.gpu_profiler.begin_frame(frame_id);
+
+ self.device.disable_scissor();
+ self.device.disable_depth();
+ self.set_blend(false, FramebufferKind::Main);
+ //self.update_shaders();
+
+ self.update_texture_cache();
+ self.update_native_surfaces();
+
+ frame_id
+ };
+
+ if let Some(device_size) = device_size {
+ // Inform the client that we are starting a composition transaction if native
+ // compositing is enabled. This needs to be done early in the frame, so that
+ // we can create debug overlays after drawing the main surfaces.
+ if let CompositorKind::Native { .. } = self.current_compositor_kind {
+ let compositor = self.compositor_config.compositor().unwrap();
+ compositor.begin_frame(&mut self.device);
+ }
+
+ // Update the state of the debug overlay surface, ensuring that
+ // the compositor mode has a suitable surface to draw to, if required.
+ self.update_debug_overlay(device_size, !active_doc.frame.debug_items.is_empty());
+ }
+
+ let frame = &mut active_doc.frame;
+ let profile = &mut active_doc.profile;
+ assert!(self.current_compositor_kind == frame.composite_state.compositor_kind);
+
+ if self.shared_texture_cache_cleared {
+ assert!(self.documents_seen.contains(&doc_id),
+ "Cleared texture cache without sending new document frame.");
+ }
+
+ match self.prepare_gpu_cache(&frame.deferred_resolves) {
+ Ok(..) => {
+ assert!(frame.gpu_cache_frame_id <= self.gpu_cache_frame_id,
+ "Received frame depends on a later GPU cache epoch ({:?}) than one we received last via `UpdateGpuCache` ({:?})",
+ frame.gpu_cache_frame_id, self.gpu_cache_frame_id);
+
+ {
+ profile_scope!("gl.flush");
+ self.device.gl().flush(); // early start on gpu cache updates
+ }
+
+ self.draw_frame(
+ frame,
+ device_size,
+ buffer_age,
+ &mut results,
+ );
+
+ // TODO(nical): do this automatically by selecting counters in the wr profiler
+ // Profile marker for the number of invalidated picture cache
+ if thread_is_being_profiled() {
+ let duration = Duration::new(0,0);
+ if let Some(n) = self.profile.get(profiler::RENDERED_PICTURE_TILES) {
+ let message = (n as usize).to_string();
+ add_text_marker("NumPictureCacheInvalidated", &message, duration);
+ }
+ }
+
+ if device_size.is_some() {
+ self.draw_frame_debug_items(&frame.debug_items);
+ }
+
+ self.profile.merge(profile);
+ }
+ Err(e) => {
+ self.renderer_errors.push(e);
+ }
+ }
+
+ self.unlock_external_images(&frame.deferred_resolves);
+
+ let _gm = self.gpu_profiler.start_marker("end frame");
+ self.gpu_profiler.end_frame();
+
+ let debug_overlay = device_size.and_then(|device_size| {
+ // Bind a surface to draw the debug / profiler information to.
+ self.bind_debug_overlay(device_size).map(|draw_target| {
+ self.draw_render_target_debug(&draw_target);
+ self.draw_texture_cache_debug(&draw_target);
+ self.draw_gpu_cache_debug(device_size);
+ self.draw_zoom_debug(device_size);
+ self.draw_epoch_debug();
+ self.draw_window_visibility_debug();
+ draw_target
+ })
+ });
+
+ let t = self.profile.end_time(profiler::RENDERER_TIME);
+ self.profile.end_time_if_started(profiler::TOTAL_FRAME_CPU_TIME);
+ Telemetry::record_renderer_time(Duration::from_micros((t * 1000.00) as u64));
+ if self.profile.get(profiler::SHADER_BUILD_TIME).is_none() {
+ Telemetry::record_renderer_time_no_sc(Duration::from_micros((t * 1000.00) as u64));
+ }
+
+ let current_time = precise_time_ns();
+ if device_size.is_some() {
+ let time = profiler::ns_to_ms(current_time - self.last_time);
+ self.profile.set(profiler::FRAME_TIME, time);
+ }
+
+ if self.max_recorded_profiles > 0 {
+ while self.cpu_profiles.len() >= self.max_recorded_profiles {
+ self.cpu_profiles.pop_front();
+ }
+ let cpu_profile = CpuProfile::new(
+ cpu_frame_id,
+ (self.profile.get_or(profiler::FRAME_BUILDING_TIME, 0.0) * 1000000.0) as u64,
+ (self.profile.get_or(profiler::RENDERER_TIME, 0.0) * 1000000.0) as u64,
+ self.profile.get_or(profiler::DRAW_CALLS, 0.0) as usize,
+ );
+ self.cpu_profiles.push_back(cpu_profile);
+ }
+
+ if thread_is_being_profiled() {
+ let duration = Duration::new(0,0);
+ let message = (self.profile.get_or(profiler::DRAW_CALLS, 0.0) as usize).to_string();
+ add_text_marker("NumDrawCalls", &message, duration);
+ }
+
+ let report = self.texture_resolver.report_memory();
+ self.profile.set(profiler::RENDER_TARGET_MEM, profiler::bytes_to_mb(report.render_target_textures));
+ self.profile.set(profiler::PICTURE_TILES_MEM, profiler::bytes_to_mb(report.picture_tile_textures));
+ self.profile.set(profiler::ATLAS_TEXTURES_MEM, profiler::bytes_to_mb(report.atlas_textures));
+ self.profile.set(profiler::STANDALONE_TEXTURES_MEM, profiler::bytes_to_mb(report.standalone_textures));
+
+ self.profile.set(profiler::DEPTH_TARGETS_MEM, profiler::bytes_to_mb(self.device.depth_targets_memory()));
+
+ self.profile.set(profiler::TEXTURES_CREATED, self.device.textures_created);
+ self.profile.set(profiler::TEXTURES_DELETED, self.device.textures_deleted);
+
+ results.stats.texture_upload_mb = self.profile.get_or(profiler::TEXTURE_UPLOADS_MEM, 0.0);
+ self.frame_counter += 1;
+ results.stats.resource_upload_time = self.resource_upload_time;
+ self.resource_upload_time = 0.0;
+ results.stats.gpu_cache_upload_time = self.gpu_cache_upload_time;
+ self.gpu_cache_upload_time = 0.0;
+
+ if let Some(stats) = active_doc.frame_stats.take() {
+ // Copy the full frame stats to RendererStats
+ results.stats.merge(&stats);
+
+ self.profiler.update_frame_stats(stats);
+ }
+
+ // Turn the render reasons bitflags into something we can see in the profiler.
+ // For now this is just a binary yes/no for each bit, which means that when looking
+ // at "Render reasons" in the profiler HUD the average view indicates the proportion
+ // of frames that had the bit set over a half second window whereas max shows whether
+ // the bit as been set at least once during that time window.
+ // We could implement better ways to visualize this information.
+ let add_markers = thread_is_being_profiled();
+ for i in 0..RenderReasons::NUM_BITS {
+ let counter = profiler::RENDER_REASON_FIRST + i as usize;
+ let mut val = 0.0;
+ let reason_bit = RenderReasons::from_bits_truncate(1 << i);
+ if active_doc.render_reasons.contains(reason_bit) {
+ val = 1.0;
+ if add_markers {
+ let event_str = format!("Render reason {:?}", reason_bit);
+ add_event_marker(&event_str);
+ }
+ }
+ self.profile.set(counter, val);
+ }
+ active_doc.render_reasons = RenderReasons::empty();
+
+
+ self.texture_resolver.update_profile(&mut self.profile);
+
+ // Note: this clears the values in self.profile.
+ self.profiler.set_counters(&mut self.profile);
+
+ // Note: profile counters must be set before this or they will count for next frame.
+ self.profiler.update();
+
+ if self.debug_flags.intersects(DebugFlags::PROFILER_DBG | DebugFlags::PROFILER_CAPTURE) {
+ if let Some(device_size) = device_size {
+ //TODO: take device/pixel ratio into equation?
+ if let Some(debug_renderer) = self.debug.get_mut(&mut self.device) {
+ self.profiler.draw_profile(
+ self.frame_counter,
+ debug_renderer,
+ device_size,
+ );
+ }
+ }
+ }
+
+ if self.debug_flags.contains(DebugFlags::ECHO_DRIVER_MESSAGES) {
+ self.device.echo_driver_messages();
+ }
+
+ if let Some(debug_renderer) = self.debug.try_get_mut() {
+ let small_screen = self.debug_flags.contains(DebugFlags::SMALL_SCREEN);
+ let scale = if small_screen { 1.6 } else { 1.0 };
+ // TODO(gw): Tidy this up so that compositor config integrates better
+ // with the (non-compositor) surface y-flip options.
+ let surface_origin_is_top_left = match self.current_compositor_kind {
+ CompositorKind::Native { .. } => true,
+ CompositorKind::Draw { .. } => self.device.surface_origin_is_top_left(),
+ };
+ // If there is a debug overlay, render it. Otherwise, just clear
+ // the debug renderer.
+ debug_renderer.render(
+ &mut self.device,
+ debug_overlay.and(device_size),
+ scale,
+ surface_origin_is_top_left,
+ );
+ }
+
+ self.staging_texture_pool.end_frame(&mut self.device);
+ self.texture_upload_pbo_pool.end_frame(&mut self.device);
+ self.device.end_frame();
+
+ if debug_overlay.is_some() {
+ self.last_time = current_time;
+
+ // Unbind the target for the debug overlay. No debug or profiler drawing
+ // can occur afer this point.
+ self.unbind_debug_overlay();
+ }
+
+ if device_size.is_some() {
+ // Inform the client that we are finished this composition transaction if native
+ // compositing is enabled. This must be called after any debug / profiling compositor
+ // surfaces have been drawn and added to the visual tree.
+ if let CompositorKind::Native { .. } = self.current_compositor_kind {
+ profile_scope!("compositor.end_frame");
+ let compositor = self.compositor_config.compositor().unwrap();
+ compositor.end_frame(&mut self.device);
+ }
+ }
+
+ self.documents_seen.clear();
+ self.shared_texture_cache_cleared = false;
+
+ self.check_gl_errors();
+
+ if self.renderer_errors.is_empty() {
+ Ok(results)
+ } else {
+ Err(mem::replace(&mut self.renderer_errors, Vec::new()))
+ }
+ }
+
+ fn update_gpu_profile(&mut self, device_size: DeviceIntSize) {
+ let _gm = self.gpu_profiler.start_marker("build samples");
+ // Block CPU waiting for last frame's GPU profiles to arrive.
+ // In general this shouldn't block unless heavily GPU limited.
+ let (gpu_frame_id, timers, samplers) = self.gpu_profiler.build_samples();
+
+ if self.max_recorded_profiles > 0 {
+ while self.gpu_profiles.len() >= self.max_recorded_profiles {
+ self.gpu_profiles.pop_front();
+ }
+
+ self.gpu_profiles.push_back(GpuProfile::new(gpu_frame_id, &timers));
+ }
+
+ self.profiler.set_gpu_time_queries(timers);
+
+ if !samplers.is_empty() {
+ let screen_fraction = 1.0 / device_size.to_f32().area();
+
+ fn accumulate_sampler_value(description: &str, samplers: &[GpuSampler]) -> f32 {
+ let mut accum = 0.0;
+ for sampler in samplers {
+ if sampler.tag.label != description {
+ continue;
+ }
+
+ accum += sampler.count as f32;
+ }
+
+ accum
+ }
+
+ let alpha_targets = accumulate_sampler_value(&"Alpha targets", &samplers) * screen_fraction;
+ let transparent_pass = accumulate_sampler_value(&"Transparent pass", &samplers) * screen_fraction;
+ let opaque_pass = accumulate_sampler_value(&"Opaque pass", &samplers) * screen_fraction;
+ self.profile.set(profiler::ALPHA_TARGETS_SAMPLERS, alpha_targets);
+ self.profile.set(profiler::TRANSPARENT_PASS_SAMPLERS, transparent_pass);
+ self.profile.set(profiler::OPAQUE_PASS_SAMPLERS, opaque_pass);
+ self.profile.set(profiler::TOTAL_SAMPLERS, alpha_targets + transparent_pass + opaque_pass);
+ }
+ }
+
+ fn update_texture_cache(&mut self) {
+ profile_scope!("update_texture_cache");
+
+ let _gm = self.gpu_profiler.start_marker("texture cache update");
+ let mut pending_texture_updates = mem::replace(&mut self.pending_texture_updates, vec![]);
+ self.pending_texture_cache_updates = false;
+
+ self.profile.start_time(profiler::TEXTURE_CACHE_UPDATE_TIME);
+
+ let mut create_cache_texture_time = 0;
+ let mut delete_cache_texture_time = 0;
+
+ for update_list in pending_texture_updates.drain(..) {
+ // Handle copies from one texture to another.
+ for ((src_tex, dst_tex), copies) in &update_list.copies {
+
+ let dest_texture = &self.texture_resolver.texture_cache_map[&dst_tex].texture;
+ let dst_texture_size = dest_texture.get_dimensions().to_f32();
+
+ let mut copy_instances = Vec::new();
+ for copy in copies {
+ copy_instances.push(CopyInstance {
+ src_rect: copy.src_rect.to_f32(),
+ dst_rect: copy.dst_rect.to_f32(),
+ dst_texture_size,
+ });
+ }
+
+ let draw_target = DrawTarget::from_texture(dest_texture, false);
+ self.device.bind_draw_target(draw_target);
+
+ self.shaders
+ .borrow_mut()
+ .ps_copy
+ .bind(
+ &mut self.device,
+ &Transform3D::identity(),
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+
+ self.draw_instanced_batch(
+ &copy_instances,
+ VertexArrayKind::Copy,
+ &BatchTextures::composite_rgb(
+ TextureSource::TextureCache(*src_tex, Swizzle::default())
+ ),
+ &mut RendererStats::default(),
+ );
+ }
+
+ // Find any textures that will need to be deleted in this group of allocations.
+ let mut pending_deletes = Vec::new();
+ for allocation in &update_list.allocations {
+ let old = self.texture_resolver.texture_cache_map.remove(&allocation.id);
+ match allocation.kind {
+ TextureCacheAllocationKind::Alloc(_) => {
+ assert!(old.is_none(), "Renderer and backend disagree!");
+ }
+ TextureCacheAllocationKind::Reset(_) |
+ TextureCacheAllocationKind::Free => {
+ assert!(old.is_some(), "Renderer and backend disagree!");
+ }
+ }
+ if let Some(old) = old {
+
+ // Regenerate the cache allocation info so we can search through deletes for reuse.
+ let size = old.texture.get_dimensions();
+ let info = TextureCacheAllocInfo {
+ width: size.width,
+ height: size.height,
+ format: old.texture.get_format(),
+ filter: old.texture.get_filter(),
+ target: old.texture.get_target(),
+ is_shared_cache: old.texture.flags().contains(TextureFlags::IS_SHARED_TEXTURE_CACHE),
+ has_depth: old.texture.supports_depth(),
+ category: old.category,
+ };
+ pending_deletes.push((old.texture, info));
+ }
+ }
+ // Look for any alloc or reset that has matching alloc info and save it from being deleted.
+ let mut reused_textures = VecDeque::with_capacity(pending_deletes.len());
+ for allocation in &update_list.allocations {
+ match allocation.kind {
+ TextureCacheAllocationKind::Alloc(ref info) |
+ TextureCacheAllocationKind::Reset(ref info) => {
+ reused_textures.push_back(
+ pending_deletes.iter()
+ .position(|(_, old_info)| *old_info == *info)
+ .map(|index| pending_deletes.swap_remove(index).0)
+ );
+ }
+ TextureCacheAllocationKind::Free => {}
+ }
+ }
+
+ // Now that we've saved as many deletions for reuse as we can, actually delete whatever is left.
+ if !pending_deletes.is_empty() {
+ let delete_texture_start = precise_time_ns();
+ for (texture, _) in pending_deletes {
+ add_event_marker("TextureCacheFree");
+ self.device.delete_texture(texture);
+ }
+ delete_cache_texture_time += precise_time_ns() - delete_texture_start;
+ }
+
+ for allocation in update_list.allocations {
+ match allocation.kind {
+ TextureCacheAllocationKind::Alloc(_) => add_event_marker("TextureCacheAlloc"),
+ TextureCacheAllocationKind::Reset(_) => add_event_marker("TextureCacheReset"),
+ TextureCacheAllocationKind::Free => {}
+ };
+ match allocation.kind {
+ TextureCacheAllocationKind::Alloc(ref info) |
+ TextureCacheAllocationKind::Reset(ref info) => {
+ let create_cache_texture_start = precise_time_ns();
+ // Create a new native texture, as requested by the texture cache.
+ // If we managed to reuse a deleted texture, then prefer that instead.
+ //
+ // Ensure no PBO is bound when creating the texture storage,
+ // or GL will attempt to read data from there.
+ let mut texture = reused_textures.pop_front().unwrap_or(None).unwrap_or_else(|| {
+ self.device.create_texture(
+ info.target,
+ info.format,
+ info.width,
+ info.height,
+ info.filter,
+ // This needs to be a render target because some render
+ // tasks get rendered into the texture cache.
+ Some(RenderTargetInfo { has_depth: info.has_depth }),
+ )
+ });
+
+ if info.is_shared_cache {
+ texture.flags_mut()
+ .insert(TextureFlags::IS_SHARED_TEXTURE_CACHE);
+
+ // On Mali-Gxx devices we use batched texture uploads as it performs much better.
+ // However, due to another driver bug we must ensure the textures are fully cleared,
+ // otherwise we get visual artefacts when blitting to the texture cache.
+ if self.device.use_batched_texture_uploads() &&
+ !self.device.get_capabilities().supports_render_target_partial_update
+ {
+ self.clear_texture(&texture, [0.0; 4]);
+ }
+
+ // Textures in the cache generally don't need to be cleared,
+ // but we do so if the debug display is active to make it
+ // easier to identify unallocated regions.
+ if self.debug_flags.contains(DebugFlags::TEXTURE_CACHE_DBG) {
+ self.clear_texture(&texture, TEXTURE_CACHE_DBG_CLEAR_COLOR);
+ }
+ }
+
+ create_cache_texture_time += precise_time_ns() - create_cache_texture_start;
+
+ self.texture_resolver.texture_cache_map.insert(allocation.id, CacheTexture {
+ texture,
+ category: info.category,
+ });
+ }
+ TextureCacheAllocationKind::Free => {}
+ };
+ }
+
+ upload_to_texture_cache(self, update_list.updates);
+
+ self.check_gl_errors();
+ }
+
+ if create_cache_texture_time > 0 {
+ self.profile.set(
+ profiler::CREATE_CACHE_TEXTURE_TIME,
+ profiler::ns_to_ms(create_cache_texture_time)
+ );
+ }
+ if delete_cache_texture_time > 0 {
+ self.profile.set(
+ profiler::DELETE_CACHE_TEXTURE_TIME,
+ profiler::ns_to_ms(delete_cache_texture_time)
+ )
+ }
+
+ let t = self.profile.end_time(profiler::TEXTURE_CACHE_UPDATE_TIME);
+ self.resource_upload_time += t;
+ Telemetry::record_texture_cache_update_time(Duration::from_micros((t * 1000.00) as u64));
+
+ drain_filter(
+ &mut self.notifications,
+ |n| { n.when() == Checkpoint::FrameTexturesUpdated },
+ |n| { n.notify(); },
+ );
+ }
+
+ fn check_gl_errors(&mut self) {
+ let err = self.device.gl().get_error();
+ if err == gl::OUT_OF_MEMORY {
+ self.renderer_errors.push(RendererError::OutOfMemory);
+ }
+
+ // Probably should check for other errors?
+ }
+
+ fn bind_textures(&mut self, textures: &BatchTextures) {
+ for i in 0 .. 3 {
+ self.texture_resolver.bind(
+ &textures.input.colors[i],
+ TextureSampler::color(i),
+ &mut self.device,
+ );
+ }
+
+ self.texture_resolver.bind(
+ &textures.clip_mask,
+ TextureSampler::ClipMask,
+ &mut self.device,
+ );
+
+ // TODO: this probably isn't the best place for this.
+ if let Some(ref texture) = self.dither_matrix_texture {
+ self.device.bind_texture(TextureSampler::Dither, texture, Swizzle::default());
+ }
+ }
+
+ fn draw_instanced_batch<T: Clone>(
+ &mut self,
+ data: &[T],
+ vertex_array_kind: VertexArrayKind,
+ textures: &BatchTextures,
+ stats: &mut RendererStats,
+ ) {
+ self.bind_textures(textures);
+
+ // If we end up with an empty draw call here, that means we have
+ // probably introduced unnecessary batch breaks during frame
+ // building - so we should be catching this earlier and removing
+ // the batch.
+ debug_assert!(!data.is_empty());
+
+ let vao = &self.vaos[vertex_array_kind];
+ self.device.bind_vao(vao);
+
+ let chunk_size = if self.debug_flags.contains(DebugFlags::DISABLE_BATCHING) {
+ 1
+ } else if vertex_array_kind == VertexArrayKind::Primitive {
+ self.max_primitive_instance_count
+ } else {
+ data.len()
+ };
+
+ for chunk in data.chunks(chunk_size) {
+ if self.enable_instancing {
+ self.device
+ .update_vao_instances(vao, chunk, ONE_TIME_USAGE_HINT, None);
+ self.device
+ .draw_indexed_triangles_instanced_u16(6, chunk.len() as i32);
+ } else {
+ self.device
+ .update_vao_instances(vao, chunk, ONE_TIME_USAGE_HINT, NonZeroUsize::new(4));
+ self.device
+ .draw_indexed_triangles(6 * chunk.len() as i32);
+ }
+ self.profile.inc(profiler::DRAW_CALLS);
+ stats.total_draw_calls += 1;
+ }
+
+ self.profile.add(profiler::VERTICES, 6 * data.len());
+ }
+
+ fn handle_readback_composite(
+ &mut self,
+ draw_target: DrawTarget,
+ uses_scissor: bool,
+ backdrop: &RenderTask,
+ readback: &RenderTask,
+ ) {
+ // Extract the rectangle in the backdrop surface's device space of where
+ // we need to read from.
+ let readback_origin = match readback.kind {
+ RenderTaskKind::Readback(ReadbackTask { readback_origin: Some(o), .. }) => o,
+ RenderTaskKind::Readback(ReadbackTask { readback_origin: None, .. }) => {
+ // If this is a dummy readback, just early out. We know that the
+ // clear of the target will ensure the task rect is already zero alpha,
+ // so it won't affect the rendering output.
+ return;
+ }
+ _ => unreachable!(),
+ };
+
+ if uses_scissor {
+ self.device.disable_scissor();
+ }
+
+ let texture_source = TextureSource::TextureCache(
+ readback.get_target_texture(),
+ Swizzle::default(),
+ );
+ let (cache_texture, _) = self.texture_resolver
+ .resolve(&texture_source).expect("bug: no source texture");
+
+ // Before submitting the composite batch, do the
+ // framebuffer readbacks that are needed for each
+ // composite operation in this batch.
+ let readback_rect = readback.get_target_rect();
+ let backdrop_rect = backdrop.get_target_rect();
+ let (backdrop_screen_origin, _) = match backdrop.kind {
+ RenderTaskKind::Picture(ref task_info) => (task_info.content_origin, task_info.device_pixel_scale),
+ _ => panic!("bug: composite on non-picture?"),
+ };
+
+ // Bind the FBO to blit the backdrop to.
+ // Called per-instance in case the FBO changes. The device will skip
+ // the GL call if the requested target is already bound.
+ let cache_draw_target = DrawTarget::from_texture(
+ cache_texture,
+ false,
+ );
+
+ // Get the rect that we ideally want, in space of the parent surface
+ let wanted_rect = DeviceRect::from_origin_and_size(
+ readback_origin,
+ readback_rect.size().to_f32(),
+ );
+
+ // Get the rect that is available on the parent surface. It may be smaller
+ // than desired because this is a picture cache tile covering only part of
+ // the wanted rect and/or because the parent surface was clipped.
+ let avail_rect = DeviceRect::from_origin_and_size(
+ backdrop_screen_origin,
+ backdrop_rect.size().to_f32(),
+ );
+
+ if let Some(int_rect) = wanted_rect.intersection(&avail_rect) {
+ // If there is a valid intersection, work out the correct origins and
+ // sizes of the copy rects, and do the blit.
+ let copy_size = int_rect.size().to_i32();
+
+ let src_origin = backdrop_rect.min.to_f32() +
+ int_rect.min.to_vector() -
+ backdrop_screen_origin.to_vector();
+
+ let src = DeviceIntRect::from_origin_and_size(
+ src_origin.to_i32(),
+ copy_size,
+ );
+
+ let dest_origin = readback_rect.min.to_f32() +
+ int_rect.min.to_vector() -
+ readback_origin.to_vector();
+
+ let dest = DeviceIntRect::from_origin_and_size(
+ dest_origin.to_i32(),
+ copy_size,
+ );
+
+ // Should always be drawing to picture cache tiles or off-screen surface!
+ debug_assert!(!draw_target.is_default());
+ let device_to_framebuffer = Scale::new(1i32);
+
+ self.device.blit_render_target(
+ draw_target.into(),
+ src * device_to_framebuffer,
+ cache_draw_target,
+ dest * device_to_framebuffer,
+ TextureFilter::Linear,
+ );
+ }
+
+ // Restore draw target to current pass render target, and reset
+ // the read target.
+ self.device.bind_draw_target(draw_target);
+ self.device.reset_read_target();
+
+ if uses_scissor {
+ self.device.enable_scissor();
+ }
+ }
+
+ fn handle_resolves(
+ &mut self,
+ resolve_ops: &[ResolveOp],
+ render_tasks: &RenderTaskGraph,
+ draw_target: DrawTarget,
+ ) {
+ if resolve_ops.is_empty() {
+ return;
+ }
+
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_BLIT);
+
+ for resolve_op in resolve_ops {
+ self.handle_resolve(
+ resolve_op,
+ render_tasks,
+ draw_target,
+ );
+ }
+
+ self.device.reset_read_target();
+ }
+
+ fn handle_prims(
+ &mut self,
+ prim_instances: &[PrimitiveInstanceData],
+ mask_instances_fast: &[MaskInstance],
+ mask_instances_slow: &[MaskInstance],
+ projection: &default::Transform3D<f32>,
+ stats: &mut RendererStats,
+ ) {
+ if prim_instances.is_empty() {
+ return;
+ }
+
+ {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_INDIRECT_PRIM);
+
+ self.device.disable_depth_write();
+ self.set_blend(false, FramebufferKind::Other);
+
+ self.shaders.borrow_mut().ps_quad_textured.bind(
+ &mut self.device,
+ projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+
+ self.draw_instanced_batch(
+ prim_instances,
+ VertexArrayKind::Primitive,
+ &BatchTextures::empty(),
+ stats,
+ );
+ }
+
+ {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_INDIRECT_MASK);
+
+ self.set_blend(true, FramebufferKind::Other);
+ self.set_blend_mode_multiply(FramebufferKind::Other);
+
+ if !mask_instances_fast.is_empty() {
+ self.shaders.borrow_mut().ps_mask_fast.bind(
+ &mut self.device,
+ projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+
+ self.draw_instanced_batch(
+ mask_instances_fast,
+ VertexArrayKind::Mask,
+ &BatchTextures::empty(),
+ stats,
+ );
+ }
+
+ if !mask_instances_slow.is_empty() {
+ self.shaders.borrow_mut().ps_mask.bind(
+ &mut self.device,
+ projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+
+ self.draw_instanced_batch(
+ mask_instances_slow,
+ VertexArrayKind::Mask,
+ &BatchTextures::empty(),
+ stats,
+ );
+ }
+ }
+ }
+
+ fn handle_blits(
+ &mut self,
+ blits: &[BlitJob],
+ render_tasks: &RenderTaskGraph,
+ draw_target: DrawTarget,
+ ) {
+ if blits.is_empty() {
+ return;
+ }
+
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_BLIT);
+
+ // TODO(gw): For now, we don't bother batching these by source texture.
+ // If if ever shows up as an issue, we can easily batch them.
+ for blit in blits {
+ let (source, source_rect) = {
+ // A blit from the child render task into this target.
+ // TODO(gw): Support R8 format here once we start
+ // creating mips for alpha masks.
+ let task = &render_tasks[blit.source];
+ let source_rect = task.get_target_rect();
+ let source_texture = task.get_texture_source();
+
+ (source_texture, source_rect)
+ };
+
+ debug_assert_eq!(source_rect.size(), blit.target_rect.size());
+ let (texture, swizzle) = self.texture_resolver
+ .resolve(&source)
+ .expect("BUG: invalid source texture");
+
+ if swizzle != Swizzle::default() {
+ error!("Swizzle {:?} can't be handled by a blit", swizzle);
+ }
+
+ let read_target = DrawTarget::from_texture(
+ texture,
+ false,
+ );
+
+ self.device.blit_render_target(
+ read_target.into(),
+ read_target.to_framebuffer_rect(source_rect),
+ draw_target,
+ draw_target.to_framebuffer_rect(blit.target_rect),
+ TextureFilter::Linear,
+ );
+ }
+ }
+
+ fn handle_scaling(
+ &mut self,
+ scalings: &FastHashMap<TextureSource, Vec<ScalingInstance>>,
+ projection: &default::Transform3D<f32>,
+ stats: &mut RendererStats,
+ ) {
+ if scalings.is_empty() {
+ return
+ }
+
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_SCALE);
+
+ for (source, instances) in scalings {
+ let buffer_kind = source.image_buffer_kind();
+
+ // When the source texture is an external texture, the UV rect is not known
+ // when the external surface descriptor is created, because external textures
+ // are not resolved until the lock() callback is invoked at the start of the
+ // frame render. We must therefore override the source rects now.
+ let uv_override_instances;
+ let instances = match source {
+ TextureSource::External(..) => {
+ uv_override_instances = instances.iter().map(|instance| {
+ let texel_rect: TexelRect = self.texture_resolver.get_uv_rect(
+ &source,
+ instance.source_rect.cast().into()
+ ).into();
+ ScalingInstance {
+ target_rect: instance.target_rect,
+ source_rect: DeviceRect::new(texel_rect.uv0, texel_rect.uv1),
+ }
+ }).collect::<Vec<_>>();
+ &uv_override_instances
+ }
+ _ => &instances
+ };
+
+ self.shaders
+ .borrow_mut()
+ .get_scale_shader(buffer_kind)
+ .bind(
+ &mut self.device,
+ &projection,
+ Some(self.texture_resolver.get_texture_size(source).to_f32()),
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+
+ self.draw_instanced_batch(
+ instances,
+ VertexArrayKind::Scale,
+ &BatchTextures::composite_rgb(*source),
+ stats,
+ );
+ }
+ }
+
+ fn handle_svg_filters(
+ &mut self,
+ textures: &BatchTextures,
+ svg_filters: &[SvgFilterInstance],
+ projection: &default::Transform3D<f32>,
+ stats: &mut RendererStats,
+ ) {
+ if svg_filters.is_empty() {
+ return;
+ }
+
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_SVG_FILTER);
+
+ self.shaders.borrow_mut().cs_svg_filter.bind(
+ &mut self.device,
+ &projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+
+ self.draw_instanced_batch(
+ &svg_filters,
+ VertexArrayKind::SvgFilter,
+ textures,
+ stats,
+ );
+ }
+
+ fn handle_resolve(
+ &mut self,
+ resolve_op: &ResolveOp,
+ render_tasks: &RenderTaskGraph,
+ draw_target: DrawTarget,
+ ) {
+ for src_task_id in &resolve_op.src_task_ids {
+ let src_task = &render_tasks[*src_task_id];
+ let src_info = match src_task.kind {
+ RenderTaskKind::Picture(ref info) => info,
+ _ => panic!("bug: not a picture"),
+ };
+ let src_task_rect = src_task.get_target_rect().to_f32();
+
+ let dest_task = &render_tasks[resolve_op.dest_task_id];
+ let dest_info = match dest_task.kind {
+ RenderTaskKind::Picture(ref info) => info,
+ _ => panic!("bug: not a picture"),
+ };
+ let dest_task_rect = dest_task.get_target_rect().to_f32();
+
+ // Get the rect that we ideally want, in space of the parent surface
+ let wanted_rect = DeviceRect::from_origin_and_size(
+ dest_info.content_origin,
+ dest_task_rect.size().to_f32(),
+ ).cast_unit() * dest_info.device_pixel_scale.inverse();
+
+ // Get the rect that is available on the parent surface. It may be smaller
+ // than desired because this is a picture cache tile covering only part of
+ // the wanted rect and/or because the parent surface was clipped.
+ let avail_rect = DeviceRect::from_origin_and_size(
+ src_info.content_origin,
+ src_task_rect.size().to_f32(),
+ ).cast_unit() * src_info.device_pixel_scale.inverse();
+
+ if let Some(device_int_rect) = wanted_rect.intersection(&avail_rect) {
+ let src_int_rect = (device_int_rect * src_info.device_pixel_scale).cast_unit();
+ let dest_int_rect = (device_int_rect * dest_info.device_pixel_scale).cast_unit();
+
+ // If there is a valid intersection, work out the correct origins and
+ // sizes of the copy rects, and do the blit.
+
+ let src_origin = src_task_rect.min.to_f32() +
+ src_int_rect.min.to_vector() -
+ src_info.content_origin.to_vector();
+
+ let src = DeviceIntRect::from_origin_and_size(
+ src_origin.to_i32(),
+ src_int_rect.size().round().to_i32(),
+ );
+
+ let dest_origin = dest_task_rect.min.to_f32() +
+ dest_int_rect.min.to_vector() -
+ dest_info.content_origin.to_vector();
+
+ let dest = DeviceIntRect::from_origin_and_size(
+ dest_origin.to_i32(),
+ dest_int_rect.size().round().to_i32(),
+ );
+
+ let texture_source = TextureSource::TextureCache(
+ src_task.get_target_texture(),
+ Swizzle::default(),
+ );
+ let (cache_texture, _) = self.texture_resolver
+ .resolve(&texture_source).expect("bug: no source texture");
+
+ let read_target = ReadTarget::from_texture(cache_texture);
+
+ // Should always be drawing to picture cache tiles or off-screen surface!
+ debug_assert!(!draw_target.is_default());
+ let device_to_framebuffer = Scale::new(1i32);
+
+ self.device.blit_render_target(
+ read_target,
+ src * device_to_framebuffer,
+ draw_target,
+ dest * device_to_framebuffer,
+ TextureFilter::Linear,
+ );
+ }
+ }
+ }
+
+ fn draw_picture_cache_target(
+ &mut self,
+ target: &PictureCacheTarget,
+ draw_target: DrawTarget,
+ projection: &default::Transform3D<f32>,
+ render_tasks: &RenderTaskGraph,
+ stats: &mut RendererStats,
+ ) {
+ profile_scope!("draw_picture_cache_target");
+
+ self.profile.inc(profiler::RENDERED_PICTURE_TILES);
+ let _gm = self.gpu_profiler.start_marker("picture cache target");
+ let framebuffer_kind = FramebufferKind::Other;
+
+ {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_SETUP_TARGET);
+ self.device.bind_draw_target(draw_target);
+
+ if self.device.get_capabilities().supports_qcom_tiled_rendering {
+ self.device.gl().start_tiling_qcom(
+ target.dirty_rect.min.x.max(0) as _,
+ target.dirty_rect.min.y.max(0) as _,
+ target.dirty_rect.width() as _,
+ target.dirty_rect.height() as _,
+ 0,
+ );
+ }
+
+ self.device.enable_depth_write();
+ self.set_blend(false, framebuffer_kind);
+
+ let clear_color = target.clear_color.map(|c| c.to_array());
+ let scissor_rect = if self.device.get_capabilities().supports_render_target_partial_update
+ && (target.dirty_rect != target.valid_rect
+ || self.device.get_capabilities().prefers_clear_scissor)
+ {
+ Some(target.dirty_rect)
+ } else {
+ None
+ };
+ match scissor_rect {
+ // If updating only a dirty rect within a picture cache target, the
+ // clear must also be scissored to that dirty region.
+ Some(r) if self.clear_caches_with_quads => {
+ self.device.enable_depth(DepthFunction::Always);
+ // Save the draw call count so that our reftests don't get confused...
+ let old_draw_call_count = stats.total_draw_calls;
+ if clear_color.is_none() {
+ self.device.disable_color_write();
+ }
+ let instance = ClearInstance {
+ rect: [
+ r.min.x as f32, r.min.y as f32,
+ r.max.x as f32, r.max.y as f32,
+ ],
+ color: clear_color.unwrap_or([0.0; 4]),
+ };
+ self.shaders.borrow_mut().ps_clear.bind(
+ &mut self.device,
+ &projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+ self.draw_instanced_batch(
+ &[instance],
+ VertexArrayKind::Clear,
+ &BatchTextures::empty(),
+ stats,
+ );
+ if clear_color.is_none() {
+ self.device.enable_color_write();
+ }
+ stats.total_draw_calls = old_draw_call_count;
+ self.device.disable_depth();
+ }
+ other => {
+ let scissor_rect = other.map(|rect| {
+ draw_target.build_scissor_rect(Some(rect))
+ });
+ self.device.clear_target(clear_color, Some(1.0), scissor_rect);
+ }
+ };
+ self.device.disable_depth_write();
+ }
+
+ match target.kind {
+ PictureCacheTargetKind::Draw { ref alpha_batch_container } => {
+ self.draw_alpha_batch_container(
+ alpha_batch_container,
+ draw_target,
+ framebuffer_kind,
+ projection,
+ render_tasks,
+ stats,
+ );
+ }
+ PictureCacheTargetKind::Blit { task_id, sub_rect_offset } => {
+ let src_task = &render_tasks[task_id];
+ let (texture, _swizzle) = self.texture_resolver
+ .resolve(&src_task.get_texture_source())
+ .expect("BUG: invalid source texture");
+
+ let src_task_rect = src_task.get_target_rect();
+
+ let p0 = src_task_rect.min + sub_rect_offset;
+ let p1 = p0 + target.dirty_rect.size();
+ let src_rect = DeviceIntRect::new(p0, p1);
+
+ // TODO(gw): In future, it'd be tidier to have the draw target offset
+ // for DC surfaces handled by `blit_render_target`. However,
+ // for now they are only ever written to here.
+ let target_rect = target
+ .dirty_rect
+ .translate(draw_target.offset().to_vector())
+ .cast_unit();
+
+ self.device.blit_render_target(
+ ReadTarget::from_texture(texture),
+ src_rect.cast_unit(),
+ draw_target,
+ target_rect,
+ TextureFilter::Nearest,
+ );
+ }
+ }
+
+ self.device.invalidate_depth_target();
+ if self.device.get_capabilities().supports_qcom_tiled_rendering {
+ self.device.gl().end_tiling_qcom(gl::COLOR_BUFFER_BIT0_QCOM);
+ }
+ }
+
+ /// Draw an alpha batch container into a given draw target. This is used
+ /// by both color and picture cache target kinds.
+ fn draw_alpha_batch_container(
+ &mut self,
+ alpha_batch_container: &AlphaBatchContainer,
+ draw_target: DrawTarget,
+ framebuffer_kind: FramebufferKind,
+ projection: &default::Transform3D<f32>,
+ render_tasks: &RenderTaskGraph,
+ stats: &mut RendererStats,
+ ) {
+ let uses_scissor = alpha_batch_container.task_scissor_rect.is_some();
+
+ if uses_scissor {
+ self.device.enable_scissor();
+ let scissor_rect = draw_target.build_scissor_rect(
+ alpha_batch_container.task_scissor_rect,
+ );
+ self.device.set_scissor_rect(scissor_rect)
+ }
+
+ if !alpha_batch_container.opaque_batches.is_empty()
+ && !self.debug_flags.contains(DebugFlags::DISABLE_OPAQUE_PASS) {
+ let _gl = self.gpu_profiler.start_marker("opaque batches");
+ let opaque_sampler = self.gpu_profiler.start_sampler(GPU_SAMPLER_TAG_OPAQUE);
+ self.set_blend(false, framebuffer_kind);
+ //Note: depth equality is needed for split planes
+ self.device.enable_depth(DepthFunction::LessEqual);
+ self.device.enable_depth_write();
+
+ // Draw opaque batches front-to-back for maximum
+ // z-buffer efficiency!
+ for batch in alpha_batch_container
+ .opaque_batches
+ .iter()
+ .rev()
+ {
+ if should_skip_batch(&batch.key.kind, self.debug_flags) {
+ continue;
+ }
+
+ self.shaders.borrow_mut()
+ .get(&batch.key, batch.features, self.debug_flags, &self.device)
+ .bind(
+ &mut self.device, projection, None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+
+ let _timer = self.gpu_profiler.start_timer(batch.key.kind.sampler_tag());
+ self.draw_instanced_batch(
+ &batch.instances,
+ VertexArrayKind::Primitive,
+ &batch.key.textures,
+ stats
+ );
+ }
+
+ self.device.disable_depth_write();
+ self.gpu_profiler.finish_sampler(opaque_sampler);
+ } else {
+ self.device.disable_depth();
+ }
+
+ if !alpha_batch_container.alpha_batches.is_empty()
+ && !self.debug_flags.contains(DebugFlags::DISABLE_ALPHA_PASS) {
+ let _gl = self.gpu_profiler.start_marker("alpha batches");
+ let transparent_sampler = self.gpu_profiler.start_sampler(GPU_SAMPLER_TAG_TRANSPARENT);
+ self.set_blend(true, framebuffer_kind);
+
+ let mut prev_blend_mode = BlendMode::None;
+ let shaders_rc = self.shaders.clone();
+
+ for batch in &alpha_batch_container.alpha_batches {
+ if should_skip_batch(&batch.key.kind, self.debug_flags) {
+ continue;
+ }
+
+ let mut shaders = shaders_rc.borrow_mut();
+ let shader = shaders.get(
+ &batch.key,
+ batch.features | BatchFeatures::ALPHA_PASS,
+ self.debug_flags,
+ &self.device,
+ );
+
+ if batch.key.blend_mode != prev_blend_mode {
+ match batch.key.blend_mode {
+ _ if self.debug_flags.contains(DebugFlags::SHOW_OVERDRAW) &&
+ framebuffer_kind == FramebufferKind::Main => {
+ self.device.set_blend_mode_show_overdraw();
+ }
+ BlendMode::None => {
+ unreachable!("bug: opaque blend in alpha pass");
+ }
+ BlendMode::Alpha => {
+ self.device.set_blend_mode_alpha();
+ }
+ BlendMode::PremultipliedAlpha => {
+ self.device.set_blend_mode_premultiplied_alpha();
+ }
+ BlendMode::PremultipliedDestOut => {
+ self.device.set_blend_mode_premultiplied_dest_out();
+ }
+ BlendMode::SubpixelDualSource => {
+ self.device.set_blend_mode_subpixel_dual_source();
+ }
+ BlendMode::SubpixelWithBgColor => {
+ // Using the three pass "component alpha with font smoothing
+ // background color" rendering technique:
+ //
+ // /webrender/doc/text-rendering.md
+ //
+ self.device.set_blend_mode_subpixel_with_bg_color_pass0();
+ // need to make sure the shader is bound
+ shader.bind(
+ &mut self.device,
+ projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+ self.device.switch_mode(ShaderColorMode::SubpixelWithBgColorPass0 as _);
+ }
+ BlendMode::Advanced(mode) => {
+ if self.enable_advanced_blend_barriers {
+ self.device.gl().blend_barrier_khr();
+ }
+ self.device.set_blend_mode_advanced(mode);
+ }
+ BlendMode::MultiplyDualSource => {
+ self.device.set_blend_mode_multiply_dual_source();
+ }
+ BlendMode::Screen => {
+ self.device.set_blend_mode_screen();
+ }
+ BlendMode::Exclusion => {
+ self.device.set_blend_mode_exclusion();
+ }
+ BlendMode::PlusLighter => {
+ self.device.set_blend_mode_plus_lighter();
+ }
+ }
+ prev_blend_mode = batch.key.blend_mode;
+ }
+
+ // Handle special case readback for composites.
+ if let BatchKind::Brush(BrushBatchKind::MixBlend { task_id, backdrop_id }) = batch.key.kind {
+ // composites can't be grouped together because
+ // they may overlap and affect each other.
+ debug_assert_eq!(batch.instances.len(), 1);
+ self.handle_readback_composite(
+ draw_target,
+ uses_scissor,
+ &render_tasks[task_id],
+ &render_tasks[backdrop_id],
+ );
+ }
+
+ let _timer = self.gpu_profiler.start_timer(batch.key.kind.sampler_tag());
+ shader.bind(
+ &mut self.device,
+ projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+
+ self.draw_instanced_batch(
+ &batch.instances,
+ VertexArrayKind::Primitive,
+ &batch.key.textures,
+ stats
+ );
+
+ if batch.key.blend_mode == BlendMode::SubpixelWithBgColor {
+ self.set_blend_mode_subpixel_with_bg_color_pass1(framebuffer_kind);
+ // re-binding the shader after the blend mode change
+ shader.bind(
+ &mut self.device,
+ projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+ self.device.switch_mode(ShaderColorMode::SubpixelWithBgColorPass1 as _);
+
+ // When drawing the 2nd and 3rd passes, we know that the VAO, textures etc
+ // are all set up from the previous draw_instanced_batch call,
+ // so just issue a draw call here to avoid re-uploading the
+ // instances and re-binding textures etc.
+ self.device
+ .draw_indexed_triangles_instanced_u16(6, batch.instances.len() as i32);
+
+ self.set_blend_mode_subpixel_with_bg_color_pass2(framebuffer_kind);
+ // re-binding the shader after the blend mode change
+ shader.bind(
+ &mut self.device,
+ projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+ self.device.switch_mode(ShaderColorMode::SubpixelWithBgColorPass2 as _);
+
+ self.device
+ .draw_indexed_triangles_instanced_u16(6, batch.instances.len() as i32);
+ }
+
+ if batch.key.blend_mode == BlendMode::SubpixelWithBgColor {
+ prev_blend_mode = BlendMode::None;
+ }
+ }
+
+ self.set_blend(false, framebuffer_kind);
+ self.gpu_profiler.finish_sampler(transparent_sampler);
+ }
+
+ self.device.disable_depth();
+ if uses_scissor {
+ self.device.disable_scissor();
+ }
+ }
+
+ /// Rasterize any external compositor surfaces that require updating
+ fn update_external_native_surfaces(
+ &mut self,
+ external_surfaces: &[ResolvedExternalSurface],
+ results: &mut RenderResults,
+ ) {
+ if external_surfaces.is_empty() {
+ return;
+ }
+
+ let opaque_sampler = self.gpu_profiler.start_sampler(GPU_SAMPLER_TAG_OPAQUE);
+
+ self.device.disable_depth();
+ self.set_blend(false, FramebufferKind::Main);
+
+ for surface in external_surfaces {
+ // See if this surface needs to be updated
+ let (native_surface_id, surface_size) = match surface.update_params {
+ Some(params) => params,
+ None => continue,
+ };
+
+ // When updating an external surface, the entire surface rect is used
+ // for all of the draw, dirty, valid and clip rect parameters.
+ let surface_rect = surface_size.into();
+
+ // Bind the native compositor surface to update
+ let surface_info = self.compositor_config
+ .compositor()
+ .unwrap()
+ .bind(
+ &mut self.device,
+ NativeTileId {
+ surface_id: native_surface_id,
+ x: 0,
+ y: 0,
+ },
+ surface_rect,
+ surface_rect,
+ );
+
+ // Bind the native surface to current FBO target
+ let draw_target = DrawTarget::NativeSurface {
+ offset: surface_info.origin,
+ external_fbo_id: surface_info.fbo_id,
+ dimensions: surface_size,
+ };
+ self.device.bind_draw_target(draw_target);
+
+ let projection = Transform3D::ortho(
+ 0.0,
+ surface_size.width as f32,
+ 0.0,
+ surface_size.height as f32,
+ self.device.ortho_near_plane(),
+ self.device.ortho_far_plane(),
+ );
+
+ let ( textures, instance ) = match surface.color_data {
+ ResolvedExternalSurfaceColorData::Yuv{
+ ref planes, color_space, format, channel_bit_depth, .. } => {
+
+ // Bind an appropriate YUV shader for the texture format kind
+ self.shaders
+ .borrow_mut()
+ .get_composite_shader(
+ CompositeSurfaceFormat::Yuv,
+ surface.image_buffer_kind,
+ CompositeFeatures::empty(),
+ ).bind(
+ &mut self.device,
+ &projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+
+ let textures = BatchTextures::composite_yuv(
+ planes[0].texture,
+ planes[1].texture,
+ planes[2].texture,
+ );
+
+ // When the texture is an external texture, the UV rect is not known when
+ // the external surface descriptor is created, because external textures
+ // are not resolved until the lock() callback is invoked at the start of
+ // the frame render. To handle this, query the texture resolver for the
+ // UV rect if it's an external texture, otherwise use the default UV rect.
+ let uv_rects = [
+ self.texture_resolver.get_uv_rect(&textures.input.colors[0], planes[0].uv_rect),
+ self.texture_resolver.get_uv_rect(&textures.input.colors[1], planes[1].uv_rect),
+ self.texture_resolver.get_uv_rect(&textures.input.colors[2], planes[2].uv_rect),
+ ];
+
+ let instance = CompositeInstance::new_yuv(
+ surface_rect.cast_unit().to_f32(),
+ surface_rect.to_f32(),
+ // z-id is not relevant when updating a native compositor surface.
+ // TODO(gw): Support compositor surfaces without z-buffer, for memory / perf win here.
+ color_space,
+ format,
+ channel_bit_depth,
+ uv_rects,
+ CompositorTransform::identity(),
+ );
+
+ ( textures, instance )
+ },
+ ResolvedExternalSurfaceColorData::Rgb{ ref plane, .. } => {
+ self.shaders
+ .borrow_mut()
+ .get_composite_shader(
+ CompositeSurfaceFormat::Rgba,
+ surface.image_buffer_kind,
+ CompositeFeatures::empty(),
+ ).bind(
+ &mut self.device,
+ &projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+
+ let textures = BatchTextures::composite_rgb(plane.texture);
+ let uv_rect = self.texture_resolver.get_uv_rect(&textures.input.colors[0], plane.uv_rect);
+ let instance = CompositeInstance::new_rgb(
+ surface_rect.cast_unit().to_f32(),
+ surface_rect.to_f32(),
+ PremultipliedColorF::WHITE,
+ uv_rect,
+ CompositorTransform::identity(),
+ );
+
+ ( textures, instance )
+ },
+ };
+
+ self.draw_instanced_batch(
+ &[instance],
+ VertexArrayKind::Composite,
+ &textures,
+ &mut results.stats,
+ );
+
+ self.compositor_config
+ .compositor()
+ .unwrap()
+ .unbind(&mut self.device);
+ }
+
+ self.gpu_profiler.finish_sampler(opaque_sampler);
+ }
+
+ /// Draw a list of tiles to the framebuffer
+ fn draw_tile_list<'a, I: Iterator<Item = &'a occlusion::Item>>(
+ &mut self,
+ tiles_iter: I,
+ composite_state: &CompositeState,
+ external_surfaces: &[ResolvedExternalSurface],
+ projection: &default::Transform3D<f32>,
+ stats: &mut RendererStats,
+ ) {
+ let mut current_shader_params = (
+ CompositeSurfaceFormat::Rgba,
+ ImageBufferKind::Texture2D,
+ CompositeFeatures::empty(),
+ None,
+ );
+ let mut current_textures = BatchTextures::empty();
+ let mut instances = Vec::new();
+
+ self.shaders
+ .borrow_mut()
+ .get_composite_shader(
+ current_shader_params.0,
+ current_shader_params.1,
+ current_shader_params.2,
+ ).bind(
+ &mut self.device,
+ projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+
+ for item in tiles_iter {
+ let tile = &composite_state.tiles[item.key];
+
+ let clip_rect = item.rectangle;
+ let tile_rect = tile.local_rect;
+ let transform = composite_state.get_device_transform(tile.transform_index).into();
+
+ // Work out the draw params based on the tile surface
+ let (instance, textures, shader_params) = match tile.surface {
+ CompositeTileSurface::Color { color } => {
+ let dummy = TextureSource::Dummy;
+ let image_buffer_kind = dummy.image_buffer_kind();
+ let instance = CompositeInstance::new(
+ tile_rect,
+ clip_rect,
+ color.premultiplied(),
+ transform,
+ );
+ let features = instance.get_rgb_features();
+ (
+ instance,
+ BatchTextures::composite_rgb(dummy),
+ (CompositeSurfaceFormat::Rgba, image_buffer_kind, features, None),
+ )
+ }
+ CompositeTileSurface::Texture { surface: ResolvedSurfaceTexture::TextureCache { texture } } => {
+ let instance = CompositeInstance::new(
+ tile_rect,
+ clip_rect,
+ PremultipliedColorF::WHITE,
+ transform,
+ );
+ let features = instance.get_rgb_features();
+ (
+ instance,
+ BatchTextures::composite_rgb(texture),
+ (
+ CompositeSurfaceFormat::Rgba,
+ ImageBufferKind::Texture2D,
+ features,
+ None,
+ ),
+ )
+ }
+ CompositeTileSurface::ExternalSurface { external_surface_index } => {
+ let surface = &external_surfaces[external_surface_index.0];
+
+ match surface.color_data {
+ ResolvedExternalSurfaceColorData::Yuv{ ref planes, color_space, format, channel_bit_depth, .. } => {
+ let textures = BatchTextures::composite_yuv(
+ planes[0].texture,
+ planes[1].texture,
+ planes[2].texture,
+ );
+
+ // When the texture is an external texture, the UV rect is not known when
+ // the external surface descriptor is created, because external textures
+ // are not resolved until the lock() callback is invoked at the start of
+ // the frame render. To handle this, query the texture resolver for the
+ // UV rect if it's an external texture, otherwise use the default UV rect.
+ let uv_rects = [
+ self.texture_resolver.get_uv_rect(&textures.input.colors[0], planes[0].uv_rect),
+ self.texture_resolver.get_uv_rect(&textures.input.colors[1], planes[1].uv_rect),
+ self.texture_resolver.get_uv_rect(&textures.input.colors[2], planes[2].uv_rect),
+ ];
+
+ (
+ CompositeInstance::new_yuv(
+ tile_rect,
+ clip_rect,
+ color_space,
+ format,
+ channel_bit_depth,
+ uv_rects,
+ transform,
+ ),
+ textures,
+ (
+ CompositeSurfaceFormat::Yuv,
+ surface.image_buffer_kind,
+ CompositeFeatures::empty(),
+ None
+ ),
+ )
+ },
+ ResolvedExternalSurfaceColorData::Rgb { ref plane, .. } => {
+ let uv_rect = self.texture_resolver.get_uv_rect(&plane.texture, plane.uv_rect);
+ let instance = CompositeInstance::new_rgb(
+ tile_rect,
+ clip_rect,
+ PremultipliedColorF::WHITE,
+ uv_rect,
+ transform,
+ );
+ let features = instance.get_rgb_features();
+ (
+ instance,
+ BatchTextures::composite_rgb(plane.texture),
+ (
+ CompositeSurfaceFormat::Rgba,
+ surface.image_buffer_kind,
+ features,
+ Some(self.texture_resolver.get_texture_size(&plane.texture).to_f32()),
+ ),
+ )
+ },
+ }
+ }
+ CompositeTileSurface::Clear => {
+ let dummy = TextureSource::Dummy;
+ let image_buffer_kind = dummy.image_buffer_kind();
+ let instance = CompositeInstance::new(
+ tile_rect,
+ clip_rect,
+ PremultipliedColorF::BLACK,
+ transform,
+ );
+ let features = instance.get_rgb_features();
+ (
+ instance,
+ BatchTextures::composite_rgb(dummy),
+ (CompositeSurfaceFormat::Rgba, image_buffer_kind, features, None),
+ )
+ }
+ CompositeTileSurface::Texture { surface: ResolvedSurfaceTexture::Native { .. } } => {
+ unreachable!("bug: found native surface in simple composite path");
+ }
+ };
+
+ // Flush batch if shader params or textures changed
+ let flush_batch = !current_textures.is_compatible_with(&textures) ||
+ shader_params != current_shader_params;
+
+ if flush_batch {
+ if !instances.is_empty() {
+ self.draw_instanced_batch(
+ &instances,
+ VertexArrayKind::Composite,
+ &current_textures,
+ stats,
+ );
+ instances.clear();
+ }
+ }
+
+ if shader_params != current_shader_params {
+ self.shaders
+ .borrow_mut()
+ .get_composite_shader(shader_params.0, shader_params.1, shader_params.2)
+ .bind(
+ &mut self.device,
+ projection,
+ shader_params.3,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+
+ current_shader_params = shader_params;
+ }
+
+ current_textures = textures;
+
+ // Add instance to current batch
+ instances.push(instance);
+ }
+
+ // Flush the last batch
+ if !instances.is_empty() {
+ self.draw_instanced_batch(
+ &instances,
+ VertexArrayKind::Composite,
+ &current_textures,
+ stats,
+ );
+ }
+ }
+
+ /// Composite picture cache tiles into the framebuffer. This is currently
+ /// the only way that picture cache tiles get drawn. In future, the tiles
+ /// will often be handed to the OS compositor, and this method will be
+ /// rarely used.
+ fn composite_simple(
+ &mut self,
+ composite_state: &CompositeState,
+ draw_target: DrawTarget,
+ projection: &default::Transform3D<f32>,
+ results: &mut RenderResults,
+ partial_present_mode: Option<PartialPresentMode>,
+ ) {
+ let _gm = self.gpu_profiler.start_marker("framebuffer");
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_COMPOSITE);
+
+ self.device.bind_draw_target(draw_target);
+ self.device.disable_depth_write();
+ self.device.disable_depth();
+
+ // If using KHR_partial_update, call eglSetDamageRegion.
+ // This must be called exactly once per frame, and prior to any rendering to the main
+ // framebuffer. Additionally, on Mali-G77 we encountered rendering issues when calling
+ // this earlier in the frame, during offscreen render passes. So call it now, immediately
+ // before rendering to the main framebuffer. See bug 1685276 for details.
+ if let Some(partial_present) = self.compositor_config.partial_present() {
+ if let Some(PartialPresentMode::Single { dirty_rect }) = partial_present_mode {
+ partial_present.set_buffer_damage_region(&[dirty_rect.to_i32()]);
+ }
+ }
+
+ let cap = composite_state.tiles.len();
+
+ let mut occlusion = occlusion::FrontToBackBuilder::with_capacity(cap, cap);
+ let mut clear_tiles = Vec::new();
+
+ for (idx, tile) in composite_state.tiles.iter().enumerate() {
+ // Clear tiles overwrite whatever is under them, so they are treated as opaque.
+ let is_opaque = tile.kind != TileKind::Alpha;
+
+ let device_tile_box = composite_state.get_device_rect(
+ &tile.local_rect,
+ tile.transform_index
+ );
+
+ // Determine a clip rect to apply to this tile, depending on what
+ // the partial present mode is.
+ let partial_clip_rect = match partial_present_mode {
+ Some(PartialPresentMode::Single { dirty_rect }) => dirty_rect,
+ None => device_tile_box,
+ };
+
+ // Simple compositor needs the valid rect in device space to match clip rect
+ let device_valid_rect = composite_state
+ .get_device_rect(&tile.local_valid_rect, tile.transform_index);
+
+ let rect = device_tile_box
+ .intersection_unchecked(&tile.device_clip_rect)
+ .intersection_unchecked(&partial_clip_rect)
+ .intersection_unchecked(&device_valid_rect);
+
+ if rect.is_empty() {
+ continue;
+ }
+
+ if tile.kind == TileKind::Clear {
+ // Clear tiles are specific to how we render the window buttons on
+ // Windows 8. They clobber what's under them so they can be treated as opaque,
+ // but require a different blend state so they will be rendered after the opaque
+ // tiles and before transparent ones.
+ clear_tiles.push(occlusion::Item { rectangle: rect, key: idx });
+ continue;
+ }
+
+ occlusion.add(&rect, is_opaque, idx);
+ }
+
+ // Clear the framebuffer
+ let clear_color = Some(self.clear_color.to_array());
+
+ match partial_present_mode {
+ Some(PartialPresentMode::Single { dirty_rect }) => {
+ // There is no need to clear if the dirty rect is occluded. Additionally,
+ // on Mali-G77 we have observed artefacts when calling glClear (even with
+ // the empty scissor rect set) after calling eglSetDamageRegion with an
+ // empty damage region. So avoid clearing in that case. See bug 1709548.
+ if !dirty_rect.is_empty() && occlusion.test(&dirty_rect) {
+ // We have a single dirty rect, so clear only that
+ self.device.clear_target(clear_color,
+ None,
+ Some(draw_target.to_framebuffer_rect(dirty_rect.to_i32())));
+ }
+ }
+ None => {
+ // Partial present is disabled, so clear the entire framebuffer
+ self.device.clear_target(clear_color,
+ None,
+ None);
+ }
+ }
+
+ // We are only interested in tiles backed with actual cached pixels so we don't
+ // count clear tiles here.
+ let num_tiles = composite_state.tiles
+ .iter()
+ .filter(|tile| tile.kind != TileKind::Clear).count();
+ self.profile.set(profiler::PICTURE_TILES, num_tiles);
+
+ if !occlusion.opaque_items().is_empty() {
+ let opaque_sampler = self.gpu_profiler.start_sampler(GPU_SAMPLER_TAG_OPAQUE);
+ self.set_blend(false, FramebufferKind::Main);
+ self.draw_tile_list(
+ occlusion.opaque_items().iter(),
+ &composite_state,
+ &composite_state.external_surfaces,
+ projection,
+ &mut results.stats,
+ );
+ self.gpu_profiler.finish_sampler(opaque_sampler);
+ }
+
+ if !clear_tiles.is_empty() {
+ let transparent_sampler = self.gpu_profiler.start_sampler(GPU_SAMPLER_TAG_TRANSPARENT);
+ self.set_blend(true, FramebufferKind::Main);
+ self.device.set_blend_mode_premultiplied_dest_out();
+ self.draw_tile_list(
+ clear_tiles.iter(),
+ &composite_state,
+ &composite_state.external_surfaces,
+ projection,
+ &mut results.stats,
+ );
+ self.gpu_profiler.finish_sampler(transparent_sampler);
+ }
+
+ // Draw alpha tiles
+ if !occlusion.alpha_items().is_empty() {
+ let transparent_sampler = self.gpu_profiler.start_sampler(GPU_SAMPLER_TAG_TRANSPARENT);
+ self.set_blend(true, FramebufferKind::Main);
+ self.set_blend_mode_premultiplied_alpha(FramebufferKind::Main);
+ self.draw_tile_list(
+ occlusion.alpha_items().iter().rev(),
+ &composite_state,
+ &composite_state.external_surfaces,
+ projection,
+ &mut results.stats,
+ );
+ self.gpu_profiler.finish_sampler(transparent_sampler);
+ }
+ }
+
+ fn draw_color_target(
+ &mut self,
+ draw_target: DrawTarget,
+ target: &ColorRenderTarget,
+ clear_depth: Option<f32>,
+ render_tasks: &RenderTaskGraph,
+ projection: &default::Transform3D<f32>,
+ stats: &mut RendererStats,
+ ) {
+ profile_scope!("draw_color_target");
+
+ self.profile.inc(profiler::COLOR_PASSES);
+ let _gm = self.gpu_profiler.start_marker("color target");
+
+ // sanity check for the depth buffer
+ if let DrawTarget::Texture { with_depth, .. } = draw_target {
+ assert!(with_depth >= target.needs_depth());
+ }
+
+ let framebuffer_kind = if draw_target.is_default() {
+ FramebufferKind::Main
+ } else {
+ FramebufferKind::Other
+ };
+
+ {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_SETUP_TARGET);
+ self.device.bind_draw_target(draw_target);
+
+ if self.device.get_capabilities().supports_qcom_tiled_rendering {
+ let preserve_mask = match target.clear_color {
+ Some(_) => 0,
+ None => gl::COLOR_BUFFER_BIT0_QCOM,
+ };
+ self.device.gl().start_tiling_qcom(
+ target.used_rect.min.x.max(0) as _,
+ target.used_rect.min.y.max(0) as _,
+ target.used_rect.width() as _,
+ target.used_rect.height() as _,
+ preserve_mask,
+ );
+ }
+
+ self.device.disable_depth();
+ self.set_blend(false, framebuffer_kind);
+
+ if clear_depth.is_some() {
+ self.device.enable_depth_write();
+ }
+
+ let clear_color = target
+ .clear_color
+ .map(|color| color.to_array());
+
+ let clear_rect = match draw_target {
+ DrawTarget::NativeSurface { .. } => {
+ unreachable!("bug: native compositor surface in child target");
+ }
+ DrawTarget::Default { rect, total_size, .. } if rect.min == FramebufferIntPoint::zero() && rect.size() == total_size => {
+ // whole screen is covered, no need for scissor
+ None
+ }
+ DrawTarget::Default { rect, .. } => {
+ Some(rect)
+ }
+ DrawTarget::Texture { .. } if self.enable_clear_scissor => {
+ // TODO(gw): Applying a scissor rect and minimal clear here
+ // is a very large performance win on the Intel and nVidia
+ // GPUs that I have tested with. It's possible it may be a
+ // performance penalty on other GPU types - we should test this
+ // and consider different code paths.
+ //
+ // Note: The above measurements were taken when render
+ // target slices were minimum 2048x2048. Now that we size
+ // them adaptively, this may be less of a win (except perhaps
+ // on a mostly-unused last slice of a large texture array).
+ Some(draw_target.to_framebuffer_rect(target.used_rect))
+ }
+ DrawTarget::Texture { .. } | DrawTarget::External { .. } => {
+ None
+ }
+ };
+
+ self.device.clear_target(
+ clear_color,
+ clear_depth,
+ clear_rect,
+ );
+
+ if clear_depth.is_some() {
+ self.device.disable_depth_write();
+ }
+ }
+
+ // Handle any resolves from parent pictures to this target
+ self.handle_resolves(
+ &target.resolve_ops,
+ render_tasks,
+ draw_target,
+ );
+
+ // Handle any blits from the texture cache to this target.
+ self.handle_blits(
+ &target.blits,
+ render_tasks,
+ draw_target,
+ );
+
+ // Draw any blurs for this target.
+ // Blurs are rendered as a standard 2-pass
+ // separable implementation.
+ // TODO(gw): In the future, consider having
+ // fast path blur shaders for common
+ // blur radii with fixed weights.
+ if !target.vertical_blurs.is_empty() || !target.horizontal_blurs.is_empty() {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_BLUR);
+
+ self.set_blend(false, framebuffer_kind);
+ self.shaders.borrow_mut().cs_blur_rgba8
+ .bind(&mut self.device, projection, None, &mut self.renderer_errors, &mut self.profile);
+
+ if !target.vertical_blurs.is_empty() {
+ self.draw_blurs(
+ &target.vertical_blurs,
+ stats,
+ );
+ }
+
+ if !target.horizontal_blurs.is_empty() {
+ self.draw_blurs(
+ &target.horizontal_blurs,
+ stats,
+ );
+ }
+ }
+
+ self.handle_scaling(
+ &target.scalings,
+ projection,
+ stats,
+ );
+
+ for (ref textures, ref filters) in &target.svg_filters {
+ self.handle_svg_filters(
+ textures,
+ filters,
+ projection,
+ stats,
+ );
+ }
+
+ for alpha_batch_container in &target.alpha_batch_containers {
+ self.draw_alpha_batch_container(
+ alpha_batch_container,
+ draw_target,
+ framebuffer_kind,
+ projection,
+ render_tasks,
+ stats,
+ );
+ }
+
+ self.handle_prims(
+ &target.prim_instances,
+ &target.mask_instances_fast,
+ &target.mask_instances_slow,
+ projection,
+ stats,
+ );
+
+ if clear_depth.is_some() {
+ self.device.invalidate_depth_target();
+ }
+ if self.device.get_capabilities().supports_qcom_tiled_rendering {
+ self.device.gl().end_tiling_qcom(gl::COLOR_BUFFER_BIT0_QCOM);
+ }
+ }
+
+ fn draw_blurs(
+ &mut self,
+ blurs: &FastHashMap<TextureSource, Vec<BlurInstance>>,
+ stats: &mut RendererStats,
+ ) {
+ for (texture, blurs) in blurs {
+ let textures = BatchTextures::composite_rgb(
+ *texture,
+ );
+
+ self.draw_instanced_batch(
+ blurs,
+ VertexArrayKind::Blur,
+ &textures,
+ stats,
+ );
+ }
+ }
+
+ /// Draw all the instances in a clip batcher list to the current target.
+ fn draw_clip_batch_list(
+ &mut self,
+ list: &ClipBatchList,
+ draw_target: &DrawTarget,
+ projection: &default::Transform3D<f32>,
+ stats: &mut RendererStats,
+ ) {
+ if self.debug_flags.contains(DebugFlags::DISABLE_CLIP_MASKS) {
+ return;
+ }
+
+ // draw rounded cornered rectangles
+ if !list.slow_rectangles.is_empty() {
+ let _gm2 = self.gpu_profiler.start_marker("slow clip rectangles");
+ self.shaders.borrow_mut().cs_clip_rectangle_slow.bind(
+ &mut self.device,
+ projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+ self.draw_instanced_batch(
+ &list.slow_rectangles,
+ VertexArrayKind::ClipRect,
+ &BatchTextures::empty(),
+ stats,
+ );
+ }
+ if !list.fast_rectangles.is_empty() {
+ let _gm2 = self.gpu_profiler.start_marker("fast clip rectangles");
+ self.shaders.borrow_mut().cs_clip_rectangle_fast.bind(
+ &mut self.device,
+ projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+ self.draw_instanced_batch(
+ &list.fast_rectangles,
+ VertexArrayKind::ClipRect,
+ &BatchTextures::empty(),
+ stats,
+ );
+ }
+
+ // draw box-shadow clips
+ for (mask_texture_id, items) in list.box_shadows.iter() {
+ let _gm2 = self.gpu_profiler.start_marker("box-shadows");
+ let textures = BatchTextures::composite_rgb(*mask_texture_id);
+ self.shaders.borrow_mut().cs_clip_box_shadow
+ .bind(&mut self.device, projection, None, &mut self.renderer_errors, &mut self.profile);
+ self.draw_instanced_batch(
+ items,
+ VertexArrayKind::ClipBoxShadow,
+ &textures,
+ stats,
+ );
+ }
+
+ // draw image masks
+ let mut using_scissor = false;
+ for ((mask_texture_id, clip_rect), items) in list.images.iter() {
+ let _gm2 = self.gpu_profiler.start_marker("clip images");
+ // Some image masks may require scissoring to ensure they don't draw
+ // outside their task's target bounds. Axis-aligned primitives will
+ // be clamped inside the shader and should not require scissoring.
+ // TODO: We currently assume scissor state is off by default for
+ // alpha targets here, but in the future we may want to track the
+ // current scissor state so that this can be properly saved and
+ // restored here.
+ if let Some(clip_rect) = clip_rect {
+ if !using_scissor {
+ self.device.enable_scissor();
+ using_scissor = true;
+ }
+ let scissor_rect = draw_target.build_scissor_rect(Some(*clip_rect));
+ self.device.set_scissor_rect(scissor_rect);
+ } else if using_scissor {
+ self.device.disable_scissor();
+ using_scissor = false;
+ }
+ let textures = BatchTextures::composite_rgb(*mask_texture_id);
+ self.shaders.borrow_mut().cs_clip_image
+ .bind(&mut self.device, projection, None, &mut self.renderer_errors, &mut self.profile);
+ self.draw_instanced_batch(
+ items,
+ VertexArrayKind::ClipImage,
+ &textures,
+ stats,
+ );
+ }
+ if using_scissor {
+ self.device.disable_scissor();
+ }
+ }
+
+ fn draw_alpha_target(
+ &mut self,
+ draw_target: DrawTarget,
+ target: &AlphaRenderTarget,
+ projection: &default::Transform3D<f32>,
+ render_tasks: &RenderTaskGraph,
+ stats: &mut RendererStats,
+ ) {
+ profile_scope!("draw_alpha_target");
+
+ self.profile.inc(profiler::ALPHA_PASSES);
+ let _gm = self.gpu_profiler.start_marker("alpha target");
+ let alpha_sampler = self.gpu_profiler.start_sampler(GPU_SAMPLER_TAG_ALPHA);
+
+ {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_SETUP_TARGET);
+ self.device.bind_draw_target(draw_target);
+ self.device.disable_depth();
+ self.device.disable_depth_write();
+ self.set_blend(false, FramebufferKind::Other);
+
+ let zero_color = [0.0, 0.0, 0.0, 0.0];
+ let one_color = [1.0, 1.0, 1.0, 1.0];
+
+ // On some Adreno 4xx devices we have seen render tasks to alpha targets have no
+ // effect unless the target is fully cleared prior to rendering. See bug 1714227.
+ if self.device.get_capabilities().requires_alpha_target_full_clear {
+ self.device.clear_target(
+ Some(zero_color),
+ None,
+ None,
+ );
+ }
+
+ // On some Mali-T devices we have observed crashes in subsequent draw calls
+ // immediately after clearing the alpha render target regions with glClear().
+ // Using the shader to clear the regions avoids the crash. See bug 1638593.
+ if self.clear_alpha_targets_with_quads
+ && !(target.zero_clears.is_empty() && target.one_clears.is_empty())
+ {
+ let zeroes = target.zero_clears
+ .iter()
+ .map(|task_id| {
+ let rect = render_tasks[*task_id].get_target_rect().to_f32();
+ ClearInstance {
+ rect: [
+ rect.min.x, rect.min.y,
+ rect.max.x, rect.max.y,
+ ],
+ color: zero_color,
+ }
+ });
+
+ let ones = target.one_clears
+ .iter()
+ .map(|task_id| {
+ let rect = render_tasks[*task_id].get_target_rect().to_f32();
+ ClearInstance {
+ rect: [
+ rect.min.x, rect.min.y,
+ rect.max.x, rect.max.y,
+ ],
+ color: one_color,
+ }
+ });
+
+ let instances = zeroes.chain(ones).collect::<Vec<_>>();
+ self.shaders.borrow_mut().ps_clear.bind(
+ &mut self.device,
+ &projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+ self.draw_instanced_batch(
+ &instances,
+ VertexArrayKind::Clear,
+ &BatchTextures::empty(),
+ stats,
+ );
+ } else {
+ // TODO(gw): Applying a scissor rect and minimal clear here
+ // is a very large performance win on the Intel and nVidia
+ // GPUs that I have tested with. It's possible it may be a
+ // performance penalty on other GPU types - we should test this
+ // and consider different code paths.
+ for &task_id in &target.zero_clears {
+ let rect = render_tasks[task_id].get_target_rect();
+ self.device.clear_target(
+ Some(zero_color),
+ None,
+ Some(draw_target.to_framebuffer_rect(rect)),
+ );
+ }
+
+ for &task_id in &target.one_clears {
+ let rect = render_tasks[task_id].get_target_rect();
+ self.device.clear_target(
+ Some(one_color),
+ None,
+ Some(draw_target.to_framebuffer_rect(rect)),
+ );
+ }
+ }
+ }
+
+ // Draw any blurs for this target.
+ // Blurs are rendered as a standard 2-pass
+ // separable implementation.
+ // TODO(gw): In the future, consider having
+ // fast path blur shaders for common
+ // blur radii with fixed weights.
+ if !target.vertical_blurs.is_empty() || !target.horizontal_blurs.is_empty() {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_BLUR);
+
+ self.shaders.borrow_mut().cs_blur_a8
+ .bind(&mut self.device, projection, None, &mut self.renderer_errors, &mut self.profile);
+
+ if !target.vertical_blurs.is_empty() {
+ self.draw_blurs(
+ &target.vertical_blurs,
+ stats,
+ );
+ }
+
+ if !target.horizontal_blurs.is_empty() {
+ self.draw_blurs(
+ &target.horizontal_blurs,
+ stats,
+ );
+ }
+ }
+
+ self.handle_scaling(
+ &target.scalings,
+ projection,
+ stats,
+ );
+
+ // Draw the clip items into the tiled alpha mask.
+ {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_CACHE_CLIP);
+
+ // TODO(gw): Consider grouping multiple clip masks per shader
+ // invocation here to reduce memory bandwith further?
+
+ // Draw the primary clip mask - since this is the first mask
+ // for the task, we can disable blending, knowing that it will
+ // overwrite every pixel in the mask area.
+ self.set_blend(false, FramebufferKind::Other);
+ self.draw_clip_batch_list(
+ &target.clip_batcher.primary_clips,
+ &draw_target,
+ projection,
+ stats,
+ );
+
+ // switch to multiplicative blending for secondary masks, using
+ // multiplicative blending to accumulate clips into the mask.
+ self.set_blend(true, FramebufferKind::Other);
+ self.set_blend_mode_multiply(FramebufferKind::Other);
+ self.draw_clip_batch_list(
+ &target.clip_batcher.secondary_clips,
+ &draw_target,
+ projection,
+ stats,
+ );
+ }
+
+ self.gpu_profiler.finish_sampler(alpha_sampler);
+ }
+
+ fn draw_texture_cache_target(
+ &mut self,
+ texture: &CacheTextureId,
+ target: &TextureCacheRenderTarget,
+ render_tasks: &RenderTaskGraph,
+ stats: &mut RendererStats,
+ ) {
+ profile_scope!("draw_texture_cache_target");
+
+ self.device.disable_depth();
+ self.device.disable_depth_write();
+
+ self.set_blend(false, FramebufferKind::Other);
+
+ let texture = &self.texture_resolver.texture_cache_map[texture].texture;
+ let target_size = texture.get_dimensions();
+
+ let projection = Transform3D::ortho(
+ 0.0,
+ target_size.width as f32,
+ 0.0,
+ target_size.height as f32,
+ self.device.ortho_near_plane(),
+ self.device.ortho_far_plane(),
+ );
+
+ let draw_target = DrawTarget::from_texture(
+ texture,
+ false,
+ );
+ self.device.bind_draw_target(draw_target);
+
+ {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_CLEAR);
+
+ self.device.disable_depth();
+ self.device.disable_depth_write();
+ self.set_blend(false, FramebufferKind::Other);
+
+ let color = [0.0, 0.0, 0.0, 0.0];
+ if self.clear_caches_with_quads && !target.clears.is_empty() {
+ let instances = target.clears
+ .iter()
+ .map(|r| ClearInstance {
+ rect: [
+ r.min.x as f32, r.min.y as f32,
+ r.max.x as f32, r.max.y as f32,
+ ],
+ color,
+ })
+ .collect::<Vec<_>>();
+ self.shaders.borrow_mut().ps_clear.bind(
+ &mut self.device,
+ &projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+ self.draw_instanced_batch(
+ &instances,
+ VertexArrayKind::Clear,
+ &BatchTextures::empty(),
+ stats,
+ );
+ } else {
+ for rect in &target.clears {
+ self.device.clear_target(
+ Some(color),
+ None,
+ Some(draw_target.to_framebuffer_rect(*rect)),
+ );
+ }
+ }
+
+ // Handle any blits to this texture from child tasks.
+ self.handle_blits(
+ &target.blits,
+ render_tasks,
+ draw_target,
+ );
+ }
+
+ // Draw any borders for this target.
+ if !target.border_segments_solid.is_empty() ||
+ !target.border_segments_complex.is_empty()
+ {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_CACHE_BORDER);
+
+ self.set_blend(true, FramebufferKind::Other);
+ self.set_blend_mode_premultiplied_alpha(FramebufferKind::Other);
+
+ if !target.border_segments_solid.is_empty() {
+ self.shaders.borrow_mut().cs_border_solid.bind(
+ &mut self.device,
+ &projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+
+ self.draw_instanced_batch(
+ &target.border_segments_solid,
+ VertexArrayKind::Border,
+ &BatchTextures::empty(),
+ stats,
+ );
+ }
+
+ if !target.border_segments_complex.is_empty() {
+ self.shaders.borrow_mut().cs_border_segment.bind(
+ &mut self.device,
+ &projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+
+ self.draw_instanced_batch(
+ &target.border_segments_complex,
+ VertexArrayKind::Border,
+ &BatchTextures::empty(),
+ stats,
+ );
+ }
+
+ self.set_blend(false, FramebufferKind::Other);
+ }
+
+ // Draw any line decorations for this target.
+ if !target.line_decorations.is_empty() {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_CACHE_LINE_DECORATION);
+
+ self.set_blend(true, FramebufferKind::Other);
+ self.set_blend_mode_premultiplied_alpha(FramebufferKind::Other);
+
+ self.shaders.borrow_mut().cs_line_decoration.bind(
+ &mut self.device,
+ &projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+
+ self.draw_instanced_batch(
+ &target.line_decorations,
+ VertexArrayKind::LineDecoration,
+ &BatchTextures::empty(),
+ stats,
+ );
+
+ self.set_blend(false, FramebufferKind::Other);
+ }
+
+ // Draw any fast path linear gradients for this target.
+ if !target.fast_linear_gradients.is_empty() {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_CACHE_FAST_LINEAR_GRADIENT);
+
+ self.set_blend(false, FramebufferKind::Other);
+
+ self.shaders.borrow_mut().cs_fast_linear_gradient.bind(
+ &mut self.device,
+ &projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+
+ self.draw_instanced_batch(
+ &target.fast_linear_gradients,
+ VertexArrayKind::FastLinearGradient,
+ &BatchTextures::empty(),
+ stats,
+ );
+ }
+
+ // Draw any linear gradients for this target.
+ if !target.linear_gradients.is_empty() {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_CACHE_LINEAR_GRADIENT);
+
+ self.set_blend(false, FramebufferKind::Other);
+
+ self.shaders.borrow_mut().cs_linear_gradient.bind(
+ &mut self.device,
+ &projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+
+ if let Some(ref texture) = self.dither_matrix_texture {
+ self.device.bind_texture(TextureSampler::Dither, texture, Swizzle::default());
+ }
+
+ self.draw_instanced_batch(
+ &target.linear_gradients,
+ VertexArrayKind::LinearGradient,
+ &BatchTextures::empty(),
+ stats,
+ );
+ }
+
+ // Draw any radial gradients for this target.
+ if !target.radial_gradients.is_empty() {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_CACHE_RADIAL_GRADIENT);
+
+ self.set_blend(false, FramebufferKind::Other);
+
+ self.shaders.borrow_mut().cs_radial_gradient.bind(
+ &mut self.device,
+ &projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+
+ if let Some(ref texture) = self.dither_matrix_texture {
+ self.device.bind_texture(TextureSampler::Dither, texture, Swizzle::default());
+ }
+
+ self.draw_instanced_batch(
+ &target.radial_gradients,
+ VertexArrayKind::RadialGradient,
+ &BatchTextures::empty(),
+ stats,
+ );
+ }
+
+ // Draw any conic gradients for this target.
+ if !target.conic_gradients.is_empty() {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_CACHE_CONIC_GRADIENT);
+
+ self.set_blend(false, FramebufferKind::Other);
+
+ self.shaders.borrow_mut().cs_conic_gradient.bind(
+ &mut self.device,
+ &projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ );
+
+ if let Some(ref texture) = self.dither_matrix_texture {
+ self.device.bind_texture(TextureSampler::Dither, texture, Swizzle::default());
+ }
+
+ self.draw_instanced_batch(
+ &target.conic_gradients,
+ VertexArrayKind::ConicGradient,
+ &BatchTextures::empty(),
+ stats,
+ );
+ }
+
+ // Draw any blurs for this target.
+ if !target.horizontal_blurs.is_empty() {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_BLUR);
+
+ {
+ let mut shaders = self.shaders.borrow_mut();
+ match target.target_kind {
+ RenderTargetKind::Alpha => &mut shaders.cs_blur_a8,
+ RenderTargetKind::Color => &mut shaders.cs_blur_rgba8,
+ }.bind(&mut self.device, &projection, None, &mut self.renderer_errors, &mut self.profile);
+ }
+
+ self.draw_blurs(
+ &target.horizontal_blurs,
+ stats,
+ );
+ }
+ }
+
+ fn update_deferred_resolves(&mut self, deferred_resolves: &[DeferredResolve]) -> Option<GpuCacheUpdateList> {
+ // The first thing we do is run through any pending deferred
+ // resolves, and use a callback to get the UV rect for this
+ // custom item. Then we patch the resource_rects structure
+ // here before it's uploaded to the GPU.
+ if deferred_resolves.is_empty() {
+ return None;
+ }
+
+ let handler = self.external_image_handler
+ .as_mut()
+ .expect("Found external image, but no handler set!");
+
+ let mut list = GpuCacheUpdateList {
+ frame_id: FrameId::INVALID,
+ clear: false,
+ height: self.gpu_cache_texture.get_height(),
+ blocks: Vec::new(),
+ updates: Vec::new(),
+ debug_commands: Vec::new(),
+ };
+
+ for (i, deferred_resolve) in deferred_resolves.iter().enumerate() {
+ self.gpu_profiler.place_marker("deferred resolve");
+ let props = &deferred_resolve.image_properties;
+ let ext_image = props
+ .external_image
+ .expect("BUG: Deferred resolves must be external images!");
+ // Provide rendering information for NativeTexture external images.
+ let image = handler.lock(ext_image.id, ext_image.channel_index);
+ let texture_target = match ext_image.image_type {
+ ExternalImageType::TextureHandle(target) => target,
+ ExternalImageType::Buffer => {
+ panic!("not a suitable image type in update_deferred_resolves()");
+ }
+ };
+
+ // In order to produce the handle, the external image handler may call into
+ // the GL context and change some states.
+ self.device.reset_state();
+
+ let texture = match image.source {
+ ExternalImageSource::NativeTexture(texture_id) => {
+ ExternalTexture::new(
+ texture_id,
+ texture_target,
+ image.uv,
+ deferred_resolve.rendering,
+ )
+ }
+ ExternalImageSource::Invalid => {
+ warn!("Invalid ext-image");
+ debug!(
+ "For ext_id:{:?}, channel:{}.",
+ ext_image.id,
+ ext_image.channel_index
+ );
+ // Just use 0 as the gl handle for this failed case.
+ ExternalTexture::new(
+ 0,
+ texture_target,
+ image.uv,
+ deferred_resolve.rendering,
+ )
+ }
+ ExternalImageSource::RawData(_) => {
+ panic!("Raw external data is not expected for deferred resolves!");
+ }
+ };
+
+ self.texture_resolver
+ .external_images
+ .insert(DeferredResolveIndex(i as u32), texture);
+
+ list.updates.push(GpuCacheUpdate::Copy {
+ block_index: list.blocks.len(),
+ block_count: BLOCKS_PER_UV_RECT,
+ address: deferred_resolve.address,
+ });
+ list.blocks.push(image.uv.into());
+ list.blocks.push([0f32; 4].into());
+ }
+
+ Some(list)
+ }
+
+ fn unlock_external_images(
+ &mut self,
+ deferred_resolves: &[DeferredResolve],
+ ) {
+ if !self.texture_resolver.external_images.is_empty() {
+ let handler = self.external_image_handler
+ .as_mut()
+ .expect("Found external image, but no handler set!");
+
+ for (index, _) in self.texture_resolver.external_images.drain() {
+ let props = &deferred_resolves[index.0 as usize].image_properties;
+ let ext_image = props
+ .external_image
+ .expect("BUG: Deferred resolves must be external images!");
+ handler.unlock(ext_image.id, ext_image.channel_index);
+ }
+ }
+ }
+
+ /// Update the dirty rects based on current compositing mode and config
+ // TODO(gw): This can be tidied up significantly once the Draw compositor
+ // is implemented in terms of the compositor trait.
+ fn calculate_dirty_rects(
+ &mut self,
+ buffer_age: usize,
+ composite_state: &CompositeState,
+ draw_target_dimensions: DeviceIntSize,
+ results: &mut RenderResults,
+ ) -> Option<PartialPresentMode> {
+ let mut partial_present_mode = None;
+
+ let (max_partial_present_rects, draw_previous_partial_present_regions) = match self.current_compositor_kind {
+ CompositorKind::Native { .. } => {
+ // Assume that we can return a single dirty rect for native
+ // compositor for now, and that there is no buffer-age functionality.
+ // These params can be exposed by the compositor capabilities struct
+ // as the Draw compositor is ported to use it.
+ (1, false)
+ }
+ CompositorKind::Draw { draw_previous_partial_present_regions, max_partial_present_rects } => {
+ (max_partial_present_rects, draw_previous_partial_present_regions)
+ }
+ };
+
+ if max_partial_present_rects > 0 {
+ let prev_frames_damage_rect = if let Some(..) = self.compositor_config.partial_present() {
+ self.buffer_damage_tracker
+ .get_damage_rect(buffer_age)
+ .or_else(|| Some(DeviceRect::from_size(draw_target_dimensions.to_f32())))
+ } else {
+ None
+ };
+
+ let can_use_partial_present =
+ composite_state.dirty_rects_are_valid &&
+ !self.force_redraw &&
+ !(prev_frames_damage_rect.is_none() && draw_previous_partial_present_regions) &&
+ !self.debug_overlay_state.is_enabled;
+
+ if can_use_partial_present {
+ let mut combined_dirty_rect = DeviceRect::zero();
+ let fb_rect = DeviceRect::from_size(draw_target_dimensions.to_f32());
+
+ // Work out how many dirty rects WR produced, and if that's more than
+ // what the device supports.
+ for tile in &composite_state.tiles {
+ if tile.kind == TileKind::Clear {
+ continue;
+ }
+ let dirty_rect = composite_state.get_device_rect(
+ &tile.local_dirty_rect,
+ tile.transform_index,
+ );
+
+ // In pathological cases where a tile is extremely zoomed, it
+ // may end up with device coords outside the range of an i32,
+ // so clamp it to the frame buffer rect here, before it gets
+ // casted to an i32 rect below.
+ if let Some(dirty_rect) = dirty_rect.intersection(&fb_rect) {
+ combined_dirty_rect = combined_dirty_rect.union(&dirty_rect);
+ }
+ }
+
+ let combined_dirty_rect = combined_dirty_rect.round();
+ let combined_dirty_rect_i32 = combined_dirty_rect.to_i32();
+ // Return this frame's dirty region. If nothing has changed, don't return any dirty
+ // rects at all (the client can use this as a signal to skip present completely).
+ if !combined_dirty_rect.is_empty() {
+ results.dirty_rects.push(combined_dirty_rect_i32);
+ }
+
+ // Track this frame's dirty region, for calculating subsequent frames' damage.
+ if draw_previous_partial_present_regions {
+ self.buffer_damage_tracker.push_dirty_rect(&combined_dirty_rect);
+ }
+
+ // If the implementation requires manually keeping the buffer consistent,
+ // then we must combine this frame's dirty region with that of previous frames
+ // to determine the total_dirty_rect. The is used to determine what region we
+ // render to, and is what we send to the compositor as the buffer damage region
+ // (eg for KHR_partial_update).
+ let total_dirty_rect = if draw_previous_partial_present_regions {
+ combined_dirty_rect.union(&prev_frames_damage_rect.unwrap())
+ } else {
+ combined_dirty_rect
+ };
+
+ partial_present_mode = Some(PartialPresentMode::Single {
+ dirty_rect: total_dirty_rect,
+ });
+ } else {
+ // If we don't have a valid partial present scenario, return a single
+ // dirty rect to the client that covers the entire framebuffer.
+ let fb_rect = DeviceIntRect::from_size(
+ draw_target_dimensions,
+ );
+ results.dirty_rects.push(fb_rect);
+
+ if draw_previous_partial_present_regions {
+ self.buffer_damage_tracker.push_dirty_rect(&fb_rect.to_f32());
+ }
+ }
+
+ self.force_redraw = false;
+ }
+
+ partial_present_mode
+ }
+
+ fn bind_frame_data(&mut self, frame: &mut Frame) {
+ profile_scope!("bind_frame_data");
+
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_SETUP_DATA);
+
+ self.vertex_data_textures[self.current_vertex_data_textures].update(
+ &mut self.device,
+ &mut self.texture_upload_pbo_pool,
+ frame,
+ );
+ self.current_vertex_data_textures =
+ (self.current_vertex_data_textures + 1) % VERTEX_DATA_TEXTURE_COUNT;
+ }
+
+ fn update_native_surfaces(&mut self) {
+ profile_scope!("update_native_surfaces");
+
+ match self.compositor_config {
+ CompositorConfig::Native { ref mut compositor, .. } => {
+ for op in self.pending_native_surface_updates.drain(..) {
+ match op.details {
+ NativeSurfaceOperationDetails::CreateSurface { id, virtual_offset, tile_size, is_opaque } => {
+ let _inserted = self.allocated_native_surfaces.insert(id);
+ debug_assert!(_inserted, "bug: creating existing surface");
+ compositor.create_surface(
+ &mut self.device,
+ id,
+ virtual_offset,
+ tile_size,
+ is_opaque,
+ );
+ }
+ NativeSurfaceOperationDetails::CreateExternalSurface { id, is_opaque } => {
+ let _inserted = self.allocated_native_surfaces.insert(id);
+ debug_assert!(_inserted, "bug: creating existing surface");
+ compositor.create_external_surface(
+ &mut self.device,
+ id,
+ is_opaque,
+ );
+ }
+ NativeSurfaceOperationDetails::CreateBackdropSurface { id, color } => {
+ let _inserted = self.allocated_native_surfaces.insert(id);
+ debug_assert!(_inserted, "bug: creating existing surface");
+ compositor.create_backdrop_surface(
+ &mut self.device,
+ id,
+ color,
+ );
+ }
+ NativeSurfaceOperationDetails::DestroySurface { id } => {
+ let _existed = self.allocated_native_surfaces.remove(&id);
+ debug_assert!(_existed, "bug: removing unknown surface");
+ compositor.destroy_surface(&mut self.device, id);
+ }
+ NativeSurfaceOperationDetails::CreateTile { id } => {
+ compositor.create_tile(&mut self.device, id);
+ }
+ NativeSurfaceOperationDetails::DestroyTile { id } => {
+ compositor.destroy_tile(&mut self.device, id);
+ }
+ NativeSurfaceOperationDetails::AttachExternalImage { id, external_image } => {
+ compositor.attach_external_image(&mut self.device, id, external_image);
+ }
+ }
+ }
+ }
+ CompositorConfig::Draw { .. } => {
+ // Ensure nothing is added in simple composite mode, since otherwise
+ // memory will leak as this doesn't get drained
+ debug_assert!(self.pending_native_surface_updates.is_empty());
+ }
+ }
+ }
+
+ fn draw_frame(
+ &mut self,
+ frame: &mut Frame,
+ device_size: Option<DeviceIntSize>,
+ buffer_age: usize,
+ results: &mut RenderResults,
+ ) {
+ profile_scope!("draw_frame");
+
+ // These markers seem to crash a lot on Android, see bug 1559834
+ #[cfg(not(target_os = "android"))]
+ let _gm = self.gpu_profiler.start_marker("draw frame");
+
+ if frame.passes.is_empty() {
+ frame.has_been_rendered = true;
+ return;
+ }
+
+ self.device.disable_depth_write();
+ self.set_blend(false, FramebufferKind::Other);
+ self.device.disable_stencil();
+
+ self.bind_frame_data(frame);
+
+ // Upload experimental GPU buffer texture if there is any data present
+ // TODO: Recycle these textures, upload via PBO or best approach for platform
+ let gpu_buffer_texture = if frame.gpu_buffer.is_empty() {
+ None
+ } else {
+ let gpu_buffer_texture = self.device.create_texture(
+ ImageBufferKind::Texture2D,
+ ImageFormat::RGBAF32,
+ frame.gpu_buffer.size.width,
+ frame.gpu_buffer.size.height,
+ TextureFilter::Nearest,
+ None,
+ );
+
+ self.device.bind_texture(
+ TextureSampler::GpuBuffer,
+ &gpu_buffer_texture,
+ Swizzle::default(),
+ );
+
+ self.device.upload_texture_immediate(
+ &gpu_buffer_texture,
+ &frame.gpu_buffer.data,
+ );
+
+ Some(gpu_buffer_texture)
+ };
+
+ // Determine the present mode and dirty rects, if device_size
+ // is Some(..). If it's None, no composite will occur and only
+ // picture cache and texture cache targets will be updated.
+ // TODO(gw): Split Frame so that it's clearer when a composite
+ // is occurring.
+ let present_mode = device_size.and_then(|device_size| {
+ self.calculate_dirty_rects(
+ buffer_age,
+ &frame.composite_state,
+ device_size,
+ results,
+ )
+ });
+
+ // If we have a native OS compositor, then make use of that interface to
+ // specify how to composite each of the picture cache surfaces. First, we
+ // need to find each tile that may be bound and updated later in the frame
+ // and invalidate it so that the native render compositor knows that these
+ // tiles can't be composited early. Next, after all such tiles have been
+ // invalidated, then we queue surfaces for native composition by the render
+ // compositor before we actually update the tiles. This allows the render
+ // compositor to start early composition while the tiles are updating.
+ if let CompositorKind::Native { .. } = self.current_compositor_kind {
+ let compositor = self.compositor_config.compositor().unwrap();
+ // Invalidate any native surface tiles that might be updated by passes.
+ if !frame.has_been_rendered {
+ for tile in &frame.composite_state.tiles {
+ if tile.kind == TileKind::Clear {
+ continue;
+ }
+ if !tile.local_dirty_rect.is_empty() {
+ if let CompositeTileSurface::Texture { surface: ResolvedSurfaceTexture::Native { id, .. } } = tile.surface {
+ let valid_rect = frame.composite_state.get_surface_rect(
+ &tile.local_valid_rect,
+ &tile.local_rect,
+ tile.transform_index,
+ ).to_i32();
+
+ compositor.invalidate_tile(&mut self.device, id, valid_rect);
+ }
+ }
+ }
+ }
+ // Ensure any external surfaces that might be used during early composition
+ // are invalidated first so that the native compositor can properly schedule
+ // composition to happen only when the external surface is updated.
+ // See update_external_native_surfaces for more details.
+ for surface in &frame.composite_state.external_surfaces {
+ if let Some((native_surface_id, size)) = surface.update_params {
+ let surface_rect = size.into();
+ compositor.invalidate_tile(&mut self.device, NativeTileId { surface_id: native_surface_id, x: 0, y: 0 }, surface_rect);
+ }
+ }
+ // Finally queue native surfaces for early composition, if applicable. By now,
+ // we have already invalidated any tiles that such surfaces may depend upon, so
+ // the native render compositor can keep track of when to actually schedule
+ // composition as surfaces are updated.
+ if device_size.is_some() {
+ frame.composite_state.composite_native(
+ self.clear_color,
+ &results.dirty_rects,
+ &mut self.device,
+ &mut **compositor,
+ );
+ }
+ }
+
+ for (_pass_index, pass) in frame.passes.iter_mut().enumerate() {
+ #[cfg(not(target_os = "android"))]
+ let _gm = self.gpu_profiler.start_marker(&format!("pass {}", _pass_index));
+
+ profile_scope!("offscreen target");
+
+ // If this frame has already been drawn, then any texture
+ // cache targets have already been updated and can be
+ // skipped this time.
+ if !frame.has_been_rendered {
+ for (&texture_id, target) in &pass.texture_cache {
+ self.draw_texture_cache_target(
+ &texture_id,
+ target,
+ &frame.render_tasks,
+ &mut results.stats,
+ );
+ }
+
+ if !pass.picture_cache.is_empty() {
+ self.profile.inc(profiler::COLOR_PASSES);
+ }
+
+ // Draw picture caching tiles for this pass.
+ for picture_target in &pass.picture_cache {
+ results.stats.color_target_count += 1;
+
+ let draw_target = match picture_target.surface {
+ ResolvedSurfaceTexture::TextureCache { ref texture } => {
+ let (texture, _) = self.texture_resolver
+ .resolve(texture)
+ .expect("bug");
+
+ DrawTarget::from_texture(
+ texture,
+ true,
+ )
+ }
+ ResolvedSurfaceTexture::Native { id, size } => {
+ let surface_info = match self.current_compositor_kind {
+ CompositorKind::Native { .. } => {
+ let compositor = self.compositor_config.compositor().unwrap();
+ compositor.bind(
+ &mut self.device,
+ id,
+ picture_target.dirty_rect,
+ picture_target.valid_rect,
+ )
+ }
+ CompositorKind::Draw { .. } => {
+ unreachable!();
+ }
+ };
+
+ DrawTarget::NativeSurface {
+ offset: surface_info.origin,
+ external_fbo_id: surface_info.fbo_id,
+ dimensions: size,
+ }
+ }
+ };
+
+ let projection = Transform3D::ortho(
+ 0.0,
+ draw_target.dimensions().width as f32,
+ 0.0,
+ draw_target.dimensions().height as f32,
+ self.device.ortho_near_plane(),
+ self.device.ortho_far_plane(),
+ );
+
+ self.draw_picture_cache_target(
+ picture_target,
+ draw_target,
+ &projection,
+ &frame.render_tasks,
+ &mut results.stats,
+ );
+
+ // Native OS surfaces must be unbound at the end of drawing to them
+ if let ResolvedSurfaceTexture::Native { .. } = picture_target.surface {
+ match self.current_compositor_kind {
+ CompositorKind::Native { .. } => {
+ let compositor = self.compositor_config.compositor().unwrap();
+ compositor.unbind(&mut self.device);
+ }
+ CompositorKind::Draw { .. } => {
+ unreachable!();
+ }
+ }
+ }
+ }
+ }
+
+ for target in &pass.alpha.targets {
+ results.stats.alpha_target_count += 1;
+
+ let texture_id = target.texture_id();
+
+ let alpha_tex = self.texture_resolver.get_cache_texture_mut(&texture_id);
+
+ let draw_target = DrawTarget::from_texture(
+ alpha_tex,
+ false,
+ );
+
+ let projection = Transform3D::ortho(
+ 0.0,
+ draw_target.dimensions().width as f32,
+ 0.0,
+ draw_target.dimensions().height as f32,
+ self.device.ortho_near_plane(),
+ self.device.ortho_far_plane(),
+ );
+
+ self.draw_alpha_target(
+ draw_target,
+ target,
+ &projection,
+ &frame.render_tasks,
+ &mut results.stats,
+ );
+ }
+
+ let color_rt_info = RenderTargetInfo { has_depth: pass.color.needs_depth() };
+
+ for target in &pass.color.targets {
+ results.stats.color_target_count += 1;
+
+ let texture_id = target.texture_id();
+
+ let color_tex = self.texture_resolver.get_cache_texture_mut(&texture_id);
+
+ self.device.reuse_render_target::<u8>(
+ color_tex,
+ color_rt_info,
+ );
+
+ let draw_target = DrawTarget::from_texture(
+ color_tex,
+ target.needs_depth(),
+ );
+
+ let projection = Transform3D::ortho(
+ 0.0,
+ draw_target.dimensions().width as f32,
+ 0.0,
+ draw_target.dimensions().height as f32,
+ self.device.ortho_near_plane(),
+ self.device.ortho_far_plane(),
+ );
+
+ let clear_depth = if target.needs_depth() {
+ Some(1.0)
+ } else {
+ None
+ };
+
+ self.draw_color_target(
+ draw_target,
+ target,
+ clear_depth,
+ &frame.render_tasks,
+ &projection,
+ &mut results.stats,
+ );
+ }
+
+ // Only end the pass here and invalidate previous textures for
+ // off-screen targets. Deferring return of the inputs to the
+ // frame buffer until the implicit end_pass in end_frame allows
+ // debug draw overlays to be added without triggering a copy
+ // resolve stage in mobile / tiled GPUs.
+ self.texture_resolver.end_pass(
+ &mut self.device,
+ &pass.textures_to_invalidate,
+ );
+ {
+ profile_scope!("gl.flush");
+ self.device.gl().flush();
+ }
+ }
+
+ self.composite_frame(
+ frame,
+ device_size,
+ results,
+ present_mode,
+ );
+
+ if let Some(gpu_buffer_texture) = gpu_buffer_texture {
+ self.device.delete_texture(gpu_buffer_texture);
+ }
+
+ frame.has_been_rendered = true;
+ }
+
+ fn composite_frame(
+ &mut self,
+ frame: &mut Frame,
+ device_size: Option<DeviceIntSize>,
+ results: &mut RenderResults,
+ present_mode: Option<PartialPresentMode>,
+ ) {
+ profile_scope!("main target");
+
+ if let Some(device_size) = device_size {
+ results.stats.color_target_count += 1;
+ results.picture_cache_debug = mem::replace(
+ &mut frame.composite_state.picture_cache_debug,
+ PictureCacheDebugInfo::new(),
+ );
+
+ let size = frame.device_rect.size().to_f32();
+ let surface_origin_is_top_left = self.device.surface_origin_is_top_left();
+ let (bottom, top) = if surface_origin_is_top_left {
+ (0.0, size.height)
+ } else {
+ (size.height, 0.0)
+ };
+
+ let projection = Transform3D::ortho(
+ 0.0,
+ size.width,
+ bottom,
+ top,
+ self.device.ortho_near_plane(),
+ self.device.ortho_far_plane(),
+ );
+
+ let fb_scale = Scale::<_, _, FramebufferPixel>::new(1i32);
+ let mut fb_rect = frame.device_rect * fb_scale;
+
+ if !surface_origin_is_top_left {
+ let h = fb_rect.height();
+ fb_rect.min.y = device_size.height - fb_rect.max.y;
+ fb_rect.max.y = fb_rect.min.y + h;
+ }
+
+ let draw_target = DrawTarget::Default {
+ rect: fb_rect,
+ total_size: device_size * fb_scale,
+ surface_origin_is_top_left,
+ };
+
+ // If we have a native OS compositor, then make use of that interface
+ // to specify how to composite each of the picture cache surfaces.
+ match self.current_compositor_kind {
+ CompositorKind::Native { .. } => {
+ // We have already queued surfaces for early native composition by this point.
+ // All that is left is to finally update any external native surfaces that were
+ // invalidated so that composition can complete.
+ self.update_external_native_surfaces(
+ &frame.composite_state.external_surfaces,
+ results,
+ );
+ }
+ CompositorKind::Draw { .. } => {
+ self.composite_simple(
+ &frame.composite_state,
+ draw_target,
+ &projection,
+ results,
+ present_mode,
+ );
+ }
+ }
+ } else {
+ // Rendering a frame without presenting it will confuse the partial
+ // present logic, so force a full present for the next frame.
+ self.force_redraw();
+ }
+ }
+
+ pub fn debug_renderer(&mut self) -> Option<&mut DebugRenderer> {
+ self.debug.get_mut(&mut self.device)
+ }
+
+ pub fn get_debug_flags(&self) -> DebugFlags {
+ self.debug_flags
+ }
+
+ pub fn set_debug_flags(&mut self, flags: DebugFlags) {
+ if let Some(enabled) = flag_changed(self.debug_flags, flags, DebugFlags::GPU_TIME_QUERIES) {
+ if enabled {
+ self.gpu_profiler.enable_timers();
+ } else {
+ self.gpu_profiler.disable_timers();
+ }
+ }
+ if let Some(enabled) = flag_changed(self.debug_flags, flags, DebugFlags::GPU_SAMPLE_QUERIES) {
+ if enabled {
+ self.gpu_profiler.enable_samplers();
+ } else {
+ self.gpu_profiler.disable_samplers();
+ }
+ }
+
+ self.debug_flags = flags;
+ }
+
+ pub fn set_profiler_ui(&mut self, ui_str: &str) {
+ self.profiler.set_ui(ui_str);
+ }
+
+ fn draw_frame_debug_items(&mut self, items: &[DebugItem]) {
+ if items.is_empty() {
+ return;
+ }
+
+ let debug_renderer = match self.debug.get_mut(&mut self.device) {
+ Some(render) => render,
+ None => return,
+ };
+
+ for item in items {
+ match item {
+ DebugItem::Rect { rect, outer_color, inner_color } => {
+ debug_renderer.add_quad(
+ rect.min.x,
+ rect.min.y,
+ rect.max.x,
+ rect.max.y,
+ (*inner_color).into(),
+ (*inner_color).into(),
+ );
+
+ debug_renderer.add_rect(
+ &rect.to_i32(),
+ (*outer_color).into(),
+ );
+ }
+ DebugItem::Text { ref msg, position, color } => {
+ debug_renderer.add_text(
+ position.x,
+ position.y,
+ msg,
+ (*color).into(),
+ None,
+ );
+ }
+ }
+ }
+ }
+
+ fn draw_render_target_debug(&mut self, draw_target: &DrawTarget) {
+ if !self.debug_flags.contains(DebugFlags::RENDER_TARGET_DBG) {
+ return;
+ }
+
+ let debug_renderer = match self.debug.get_mut(&mut self.device) {
+ Some(render) => render,
+ None => return,
+ };
+
+ let textures = self.texture_resolver
+ .texture_cache_map
+ .values()
+ .filter(|item| item.category == TextureCacheCategory::RenderTarget)
+ .map(|item| &item.texture)
+ .collect::<Vec<&Texture>>();
+
+ Self::do_debug_blit(
+ &mut self.device,
+ debug_renderer,
+ textures,
+ draw_target,
+ 0,
+ &|_| [0.0, 1.0, 0.0, 1.0], // Use green for all RTs.
+ );
+ }
+
+ fn draw_zoom_debug(
+ &mut self,
+ device_size: DeviceIntSize,
+ ) {
+ if !self.debug_flags.contains(DebugFlags::ZOOM_DBG) {
+ return;
+ }
+
+ let debug_renderer = match self.debug.get_mut(&mut self.device) {
+ Some(render) => render,
+ None => return,
+ };
+
+ let source_size = DeviceIntSize::new(64, 64);
+ let target_size = DeviceIntSize::new(1024, 1024);
+
+ let source_origin = DeviceIntPoint::new(
+ (self.cursor_position.x - source_size.width / 2)
+ .min(device_size.width - source_size.width)
+ .max(0),
+ (self.cursor_position.y - source_size.height / 2)
+ .min(device_size.height - source_size.height)
+ .max(0),
+ );
+
+ let source_rect = DeviceIntRect::from_origin_and_size(
+ source_origin,
+ source_size,
+ );
+
+ let target_rect = DeviceIntRect::from_origin_and_size(
+ DeviceIntPoint::new(
+ device_size.width - target_size.width - 64,
+ device_size.height - target_size.height - 64,
+ ),
+ target_size,
+ );
+
+ let texture_rect = FramebufferIntRect::from_size(
+ source_rect.size().cast_unit(),
+ );
+
+ debug_renderer.add_rect(
+ &target_rect.inflate(1, 1),
+ debug_colors::RED.into(),
+ );
+
+ if self.zoom_debug_texture.is_none() {
+ let texture = self.device.create_texture(
+ ImageBufferKind::Texture2D,
+ ImageFormat::BGRA8,
+ source_rect.width(),
+ source_rect.height(),
+ TextureFilter::Nearest,
+ Some(RenderTargetInfo { has_depth: false }),
+ );
+
+ self.zoom_debug_texture = Some(texture);
+ }
+
+ // Copy frame buffer into the zoom texture
+ let read_target = DrawTarget::new_default(device_size, self.device.surface_origin_is_top_left());
+ self.device.blit_render_target(
+ read_target.into(),
+ read_target.to_framebuffer_rect(source_rect),
+ DrawTarget::from_texture(
+ self.zoom_debug_texture.as_ref().unwrap(),
+ false,
+ ),
+ texture_rect,
+ TextureFilter::Nearest,
+ );
+
+ // Draw the zoom texture back to the framebuffer
+ self.device.blit_render_target(
+ ReadTarget::from_texture(
+ self.zoom_debug_texture.as_ref().unwrap(),
+ ),
+ texture_rect,
+ read_target,
+ read_target.to_framebuffer_rect(target_rect),
+ TextureFilter::Nearest,
+ );
+ }
+
+ fn draw_texture_cache_debug(&mut self, draw_target: &DrawTarget) {
+ if !self.debug_flags.contains(DebugFlags::TEXTURE_CACHE_DBG) {
+ return;
+ }
+
+ let debug_renderer = match self.debug.get_mut(&mut self.device) {
+ Some(render) => render,
+ None => return,
+ };
+
+ let textures = self.texture_resolver
+ .texture_cache_map
+ .values()
+ .filter(|item| item.category == TextureCacheCategory::Atlas)
+ .map(|item| &item.texture)
+ .collect::<Vec<&Texture>>();
+
+ fn select_color(texture: &Texture) -> [f32; 4] {
+ if texture.flags().contains(TextureFlags::IS_SHARED_TEXTURE_CACHE) {
+ [1.0, 0.5, 0.0, 1.0] // Orange for shared.
+ } else {
+ [1.0, 0.0, 1.0, 1.0] // Fuchsia for standalone.
+ }
+ }
+
+ Self::do_debug_blit(
+ &mut self.device,
+ debug_renderer,
+ textures,
+ draw_target,
+ if self.debug_flags.contains(DebugFlags::RENDER_TARGET_DBG) { 544 } else { 0 },
+ &select_color,
+ );
+ }
+
+ fn do_debug_blit(
+ device: &mut Device,
+ debug_renderer: &mut DebugRenderer,
+ mut textures: Vec<&Texture>,
+ draw_target: &DrawTarget,
+ bottom: i32,
+ select_color: &dyn Fn(&Texture) -> [f32; 4],
+ ) {
+ let mut spacing = 16;
+ let mut size = 512;
+
+ let device_size = draw_target.dimensions();
+ let fb_width = device_size.width;
+ let fb_height = device_size.height;
+ let surface_origin_is_top_left = draw_target.surface_origin_is_top_left();
+
+ let num_textures = textures.len() as i32;
+
+ if num_textures * (size + spacing) > fb_width {
+ let factor = fb_width as f32 / (num_textures * (size + spacing)) as f32;
+ size = (size as f32 * factor) as i32;
+ spacing = (spacing as f32 * factor) as i32;
+ }
+
+ let text_height = 14; // Visually approximated.
+ let text_margin = 1;
+ let tag_height = text_height + text_margin * 2;
+ let tag_y = fb_height - (bottom + spacing + tag_height);
+ let image_y = tag_y - size;
+
+ // Sort the display by size (in bytes), so that left-to-right is
+ // largest-to-smallest.
+ //
+ // Note that the vec here is in increasing order, because the elements
+ // get drawn right-to-left.
+ textures.sort_by_key(|t| t.size_in_bytes());
+
+ let mut i = 0;
+ for texture in textures.iter() {
+ let dimensions = texture.get_dimensions();
+ let src_rect = FramebufferIntRect::from_size(
+ FramebufferIntSize::new(dimensions.width as i32, dimensions.height as i32),
+ );
+
+ let x = fb_width - (spacing + size) * (i as i32 + 1);
+
+ // If we have more targets than fit on one row in screen, just early exit.
+ if x > fb_width {
+ return;
+ }
+
+ // Draw the info tag.
+ let tag_rect = rect(x, tag_y, size, tag_height).to_box2d();
+ let tag_color = select_color(texture);
+ device.clear_target(
+ Some(tag_color),
+ None,
+ Some(draw_target.to_framebuffer_rect(tag_rect)),
+ );
+
+ // Draw the dimensions onto the tag.
+ let dim = texture.get_dimensions();
+ let text_rect = tag_rect.inflate(-text_margin, -text_margin);
+ debug_renderer.add_text(
+ text_rect.min.x as f32,
+ text_rect.max.y as f32, // Top-relative.
+ &format!("{}x{}", dim.width, dim.height),
+ ColorU::new(0, 0, 0, 255),
+ Some(tag_rect.to_f32())
+ );
+
+ // Blit the contents of the texture.
+ let dest_rect = draw_target.to_framebuffer_rect(rect(x, image_y, size, size).to_box2d());
+ let read_target = ReadTarget::from_texture(texture);
+
+ if surface_origin_is_top_left {
+ device.blit_render_target(
+ read_target,
+ src_rect,
+ *draw_target,
+ dest_rect,
+ TextureFilter::Linear,
+ );
+ } else {
+ // Invert y.
+ device.blit_render_target_invert_y(
+ read_target,
+ src_rect,
+ *draw_target,
+ dest_rect,
+ );
+ }
+ i += 1;
+ }
+ }
+
+ fn draw_epoch_debug(&mut self) {
+ if !self.debug_flags.contains(DebugFlags::EPOCHS) {
+ return;
+ }
+
+ let debug_renderer = match self.debug.get_mut(&mut self.device) {
+ Some(render) => render,
+ None => return,
+ };
+
+ let dy = debug_renderer.line_height();
+ let x0: f32 = 30.0;
+ let y0: f32 = 30.0;
+ let mut y = y0;
+ let mut text_width = 0.0;
+ for ((pipeline, document_id), epoch) in &self.pipeline_info.epochs {
+ y += dy;
+ let w = debug_renderer.add_text(
+ x0, y,
+ &format!("({:?}, {:?}): {:?}", pipeline, document_id, epoch),
+ ColorU::new(255, 255, 0, 255),
+ None,
+ ).size.width;
+ text_width = f32::max(text_width, w);
+ }
+
+ let margin = 10.0;
+ debug_renderer.add_quad(
+ x0 - margin,
+ y0 - margin,
+ x0 + text_width + margin,
+ y + margin,
+ ColorU::new(25, 25, 25, 200),
+ ColorU::new(51, 51, 51, 200),
+ );
+ }
+
+ fn draw_window_visibility_debug(&mut self) {
+ if !self.debug_flags.contains(DebugFlags::WINDOW_VISIBILITY_DBG) {
+ return;
+ }
+
+ let debug_renderer = match self.debug.get_mut(&mut self.device) {
+ Some(render) => render,
+ None => return,
+ };
+
+ let x: f32 = 30.0;
+ let y: f32 = 40.0;
+
+ if let CompositorConfig::Native { ref mut compositor, .. } = self.compositor_config {
+ let visibility = compositor.get_window_visibility(&mut self.device);
+ let color = if visibility.is_fully_occluded {
+ ColorU::new(255, 0, 0, 255)
+
+ } else {
+ ColorU::new(0, 0, 255, 255)
+ };
+
+ debug_renderer.add_text(
+ x, y,
+ &format!("{:?}", visibility),
+ color,
+ None,
+ );
+ }
+
+
+ }
+
+ fn draw_gpu_cache_debug(&mut self, device_size: DeviceIntSize) {
+ if !self.debug_flags.contains(DebugFlags::GPU_CACHE_DBG) {
+ return;
+ }
+
+ let debug_renderer = match self.debug.get_mut(&mut self.device) {
+ Some(render) => render,
+ None => return,
+ };
+
+ let (x_off, y_off) = (30f32, 30f32);
+ let height = self.gpu_cache_texture.get_height()
+ .min(device_size.height - (y_off as i32) * 2) as usize;
+ debug_renderer.add_quad(
+ x_off,
+ y_off,
+ x_off + MAX_VERTEX_TEXTURE_WIDTH as f32,
+ y_off + height as f32,
+ ColorU::new(80, 80, 80, 80),
+ ColorU::new(80, 80, 80, 80),
+ );
+
+ let upper = self.gpu_cache_debug_chunks.len().min(height);
+ for chunk in self.gpu_cache_debug_chunks[0..upper].iter().flatten() {
+ let color = ColorU::new(250, 0, 0, 200);
+ debug_renderer.add_quad(
+ x_off + chunk.address.u as f32,
+ y_off + chunk.address.v as f32,
+ x_off + chunk.address.u as f32 + chunk.size as f32,
+ y_off + chunk.address.v as f32 + 1.0,
+ color,
+ color,
+ );
+ }
+ }
+
+ /// Pass-through to `Device::read_pixels_into`, used by Gecko's WR bindings.
+ pub fn read_pixels_into(&mut self, rect: FramebufferIntRect, format: ImageFormat, output: &mut [u8]) {
+ self.device.read_pixels_into(rect, format, output);
+ }
+
+ pub fn read_pixels_rgba8(&mut self, rect: FramebufferIntRect) -> Vec<u8> {
+ let mut pixels = vec![0; (rect.area() * 4) as usize];
+ self.device.read_pixels_into(rect, ImageFormat::RGBA8, &mut pixels);
+ pixels
+ }
+
+ // De-initialize the Renderer safely, assuming the GL is still alive and active.
+ pub fn deinit(mut self) {
+ //Note: this is a fake frame, only needed because texture deletion is require to happen inside a frame
+ self.device.begin_frame();
+ // If we are using a native compositor, ensure that any remaining native
+ // surfaces are freed.
+ if let CompositorConfig::Native { mut compositor, .. } = self.compositor_config {
+ for id in self.allocated_native_surfaces.drain() {
+ compositor.destroy_surface(&mut self.device, id);
+ }
+ // Destroy the debug overlay surface, if currently allocated.
+ if self.debug_overlay_state.current_size.is_some() {
+ compositor.destroy_surface(&mut self.device, NativeSurfaceId::DEBUG_OVERLAY);
+ }
+ compositor.deinit(&mut self.device);
+ }
+ self.gpu_cache_texture.deinit(&mut self.device);
+ if let Some(dither_matrix_texture) = self.dither_matrix_texture {
+ self.device.delete_texture(dither_matrix_texture);
+ }
+ if let Some(zoom_debug_texture) = self.zoom_debug_texture {
+ self.device.delete_texture(zoom_debug_texture);
+ }
+ for textures in self.vertex_data_textures.drain(..) {
+ textures.deinit(&mut self.device);
+ }
+ self.texture_upload_pbo_pool.deinit(&mut self.device);
+ self.staging_texture_pool.delete_textures(&mut self.device);
+ self.texture_resolver.deinit(&mut self.device);
+ self.vaos.deinit(&mut self.device);
+ self.debug.deinit(&mut self.device);
+
+ if let Ok(shaders) = Rc::try_unwrap(self.shaders) {
+ shaders.into_inner().deinit(&mut self.device);
+ }
+
+ if let Some(async_screenshots) = self.async_screenshots.take() {
+ async_screenshots.deinit(&mut self.device);
+ }
+
+ if let Some(async_frame_recorder) = self.async_frame_recorder.take() {
+ async_frame_recorder.deinit(&mut self.device);
+ }
+
+ #[cfg(feature = "capture")]
+ self.device.delete_fbo(self.read_fbo);
+ #[cfg(feature = "replay")]
+ for (_, ext) in self.owned_external_images {
+ self.device.delete_external_texture(ext);
+ }
+ self.device.end_frame();
+ }
+
+ fn size_of<T>(&self, ptr: *const T) -> usize {
+ let ops = self.size_of_ops.as_ref().unwrap();
+ unsafe { ops.malloc_size_of(ptr) }
+ }
+
+ /// Collects a memory report.
+ pub fn report_memory(&self, swgl: *mut c_void) -> MemoryReport {
+ let mut report = MemoryReport::default();
+
+ // GPU cache CPU memory.
+ self.gpu_cache_texture.report_memory_to(&mut report, self.size_of_ops.as_ref().unwrap());
+
+ self.staging_texture_pool.report_memory_to(&mut report, self.size_of_ops.as_ref().unwrap());
+
+ // Render task CPU memory.
+ for (_id, doc) in &self.active_documents {
+ report.render_tasks += self.size_of(doc.frame.render_tasks.tasks.as_ptr());
+ report.render_tasks += self.size_of(doc.frame.render_tasks.task_data.as_ptr());
+ }
+
+ // Vertex data GPU memory.
+ for textures in &self.vertex_data_textures {
+ report.vertex_data_textures += textures.size_in_bytes();
+ }
+
+ // Texture cache and render target GPU memory.
+ report += self.texture_resolver.report_memory();
+
+ // Texture upload PBO memory.
+ report += self.texture_upload_pbo_pool.report_memory();
+
+ // Textures held internally within the device layer.
+ report += self.device.report_memory(self.size_of_ops.as_ref().unwrap(), swgl);
+
+ report
+ }
+
+ // Sets the blend mode. Blend is unconditionally set if the "show overdraw" debugging mode is
+ // enabled.
+ fn set_blend(&mut self, mut blend: bool, framebuffer_kind: FramebufferKind) {
+ if framebuffer_kind == FramebufferKind::Main &&
+ self.debug_flags.contains(DebugFlags::SHOW_OVERDRAW) {
+ blend = true
+ }
+ self.device.set_blend(blend)
+ }
+
+ fn set_blend_mode_multiply(&mut self, framebuffer_kind: FramebufferKind) {
+ if framebuffer_kind == FramebufferKind::Main &&
+ self.debug_flags.contains(DebugFlags::SHOW_OVERDRAW) {
+ self.device.set_blend_mode_show_overdraw();
+ } else {
+ self.device.set_blend_mode_multiply();
+ }
+ }
+
+ fn set_blend_mode_premultiplied_alpha(&mut self, framebuffer_kind: FramebufferKind) {
+ if framebuffer_kind == FramebufferKind::Main &&
+ self.debug_flags.contains(DebugFlags::SHOW_OVERDRAW) {
+ self.device.set_blend_mode_show_overdraw();
+ } else {
+ self.device.set_blend_mode_premultiplied_alpha();
+ }
+ }
+
+ fn set_blend_mode_subpixel_with_bg_color_pass1(&mut self, framebuffer_kind: FramebufferKind) {
+ if framebuffer_kind == FramebufferKind::Main &&
+ self.debug_flags.contains(DebugFlags::SHOW_OVERDRAW) {
+ self.device.set_blend_mode_show_overdraw();
+ } else {
+ self.device.set_blend_mode_subpixel_with_bg_color_pass1();
+ }
+ }
+
+ fn set_blend_mode_subpixel_with_bg_color_pass2(&mut self, framebuffer_kind: FramebufferKind) {
+ if framebuffer_kind == FramebufferKind::Main &&
+ self.debug_flags.contains(DebugFlags::SHOW_OVERDRAW) {
+ self.device.set_blend_mode_show_overdraw();
+ } else {
+ self.device.set_blend_mode_subpixel_with_bg_color_pass2();
+ }
+ }
+
+ /// Clears the texture with a given color.
+ fn clear_texture(&mut self, texture: &Texture, color: [f32; 4]) {
+ self.device.bind_draw_target(DrawTarget::from_texture(
+ &texture,
+ false,
+ ));
+ self.device.clear_target(Some(color), None, None);
+ }
+}
+
+bitflags! {
+ /// Flags that control how shaders are pre-cached, if at all.
+ #[derive(Default)]
+ pub struct ShaderPrecacheFlags: u32 {
+ /// Needed for const initialization
+ const EMPTY = 0;
+
+ /// Only start async compile
+ const ASYNC_COMPILE = 1 << 2;
+
+ /// Do a full compile/link during startup
+ const FULL_COMPILE = 1 << 3;
+ }
+}
+
+/// The cumulative times spent in each painting phase to generate this frame.
+#[derive(Debug, Default)]
+pub struct FullFrameStats {
+ pub full_display_list: bool,
+ pub gecko_display_list_time: f64,
+ pub wr_display_list_time: f64,
+ pub scene_build_time: f64,
+ pub frame_build_time: f64,
+}
+
+impl FullFrameStats {
+ pub fn merge(&self, other: &FullFrameStats) -> Self {
+ Self {
+ full_display_list: self.full_display_list || other.full_display_list,
+ gecko_display_list_time: self.gecko_display_list_time + other.gecko_display_list_time,
+ wr_display_list_time: self.wr_display_list_time + other.wr_display_list_time,
+ scene_build_time: self.scene_build_time + other.scene_build_time,
+ frame_build_time: self.frame_build_time + other.frame_build_time
+ }
+ }
+
+ pub fn total(&self) -> f64 {
+ self.gecko_display_list_time + self.wr_display_list_time + self.scene_build_time + self.frame_build_time
+ }
+}
+
+/// Some basic statistics about the rendered scene, used in Gecko, as
+/// well as in wrench reftests to ensure that tests are batching and/or
+/// allocating on render targets as we expect them to.
+#[repr(C)]
+#[derive(Debug, Default)]
+pub struct RendererStats {
+ pub total_draw_calls: usize,
+ pub alpha_target_count: usize,
+ pub color_target_count: usize,
+ pub texture_upload_mb: f64,
+ pub resource_upload_time: f64,
+ pub gpu_cache_upload_time: f64,
+ pub gecko_display_list_time: f64,
+ pub wr_display_list_time: f64,
+ pub scene_build_time: f64,
+ pub frame_build_time: f64,
+ pub full_display_list: bool,
+ pub full_paint: bool,
+}
+
+impl RendererStats {
+ pub fn merge(&mut self, stats: &FullFrameStats) {
+ self.gecko_display_list_time = stats.gecko_display_list_time;
+ self.wr_display_list_time = stats.wr_display_list_time;
+ self.scene_build_time = stats.scene_build_time;
+ self.frame_build_time = stats.frame_build_time;
+ self.full_display_list = stats.full_display_list;
+ self.full_paint = true;
+ }
+}
+
+/// Return type from render(), which contains some repr(C) statistics as well as
+/// some non-repr(C) data.
+#[derive(Debug, Default)]
+pub struct RenderResults {
+ /// Statistics about the frame that was rendered.
+ pub stats: RendererStats,
+
+ /// A list of the device dirty rects that were updated
+ /// this frame.
+ /// TODO(gw): This is an initial interface, likely to change in future.
+ /// TODO(gw): The dirty rects here are currently only useful when scrolling
+ /// is not occurring. They are still correct in the case of
+ /// scrolling, but will be very large (until we expose proper
+ /// OS compositor support where the dirty rects apply to a
+ /// specific picture cache slice / OS compositor surface).
+ pub dirty_rects: Vec<DeviceIntRect>,
+
+ /// Information about the state of picture cache tiles. This is only
+ /// allocated and stored if config.testing is true (such as wrench)
+ pub picture_cache_debug: PictureCacheDebugInfo,
+}
+
+#[cfg(any(feature = "capture", feature = "replay"))]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct PlainTexture {
+ data: String,
+ size: DeviceIntSize,
+ format: ImageFormat,
+ filter: TextureFilter,
+ has_depth: bool,
+ category: Option<TextureCacheCategory>,
+}
+
+
+#[cfg(any(feature = "capture", feature = "replay"))]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct PlainRenderer {
+ device_size: Option<DeviceIntSize>,
+ gpu_cache: PlainTexture,
+ gpu_cache_frame_id: FrameId,
+ textures: FastHashMap<CacheTextureId, PlainTexture>,
+}
+
+#[cfg(any(feature = "capture", feature = "replay"))]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct PlainExternalResources {
+ images: Vec<ExternalCaptureImage>
+}
+
+#[cfg(feature = "replay")]
+enum CapturedExternalImageData {
+ NativeTexture(gl::GLuint),
+ Buffer(Arc<Vec<u8>>),
+}
+
+#[cfg(feature = "replay")]
+struct DummyExternalImageHandler {
+ data: FastHashMap<(ExternalImageId, u8), (CapturedExternalImageData, TexelRect)>,
+}
+
+#[cfg(feature = "replay")]
+impl ExternalImageHandler for DummyExternalImageHandler {
+ fn lock(&mut self, key: ExternalImageId, channel_index: u8) -> ExternalImage {
+ let (ref captured_data, ref uv) = self.data[&(key, channel_index)];
+ ExternalImage {
+ uv: *uv,
+ source: match *captured_data {
+ CapturedExternalImageData::NativeTexture(tid) => ExternalImageSource::NativeTexture(tid),
+ CapturedExternalImageData::Buffer(ref arc) => ExternalImageSource::RawData(&*arc),
+ }
+ }
+ }
+ fn unlock(&mut self, _key: ExternalImageId, _channel_index: u8) {}
+}
+
+#[derive(Default)]
+pub struct PipelineInfo {
+ pub epochs: FastHashMap<(PipelineId, DocumentId), Epoch>,
+ pub removed_pipelines: Vec<(PipelineId, DocumentId)>,
+}
+
+impl Renderer {
+ #[cfg(feature = "capture")]
+ fn save_texture(
+ texture: &Texture, category: Option<TextureCacheCategory>, name: &str, root: &PathBuf, device: &mut Device
+ ) -> PlainTexture {
+ use std::fs;
+ use std::io::Write;
+
+ let short_path = format!("textures/{}.raw", name);
+
+ let bytes_per_pixel = texture.get_format().bytes_per_pixel();
+ let read_format = texture.get_format();
+ let rect_size = texture.get_dimensions();
+
+ let mut file = fs::File::create(root.join(&short_path))
+ .expect(&format!("Unable to create {}", short_path));
+ let bytes_per_texture = (rect_size.width * rect_size.height * bytes_per_pixel) as usize;
+ let mut data = vec![0; bytes_per_texture];
+
+ //TODO: instead of reading from an FBO with `read_pixels*`, we could
+ // read from textures directly with `get_tex_image*`.
+
+ let rect = device_size_as_framebuffer_size(rect_size).into();
+
+ device.attach_read_texture(texture);
+ #[cfg(feature = "png")]
+ {
+ let mut png_data;
+ let (data_ref, format) = match texture.get_format() {
+ ImageFormat::RGBAF32 => {
+ png_data = vec![0; (rect_size.width * rect_size.height * 4) as usize];
+ device.read_pixels_into(rect, ImageFormat::RGBA8, &mut png_data);
+ (&png_data, ImageFormat::RGBA8)
+ }
+ fm => (&data, fm),
+ };
+ CaptureConfig::save_png(
+ root.join(format!("textures/{}-{}.png", name, 0)),
+ rect_size, format,
+ None,
+ data_ref,
+ );
+ }
+ device.read_pixels_into(rect, read_format, &mut data);
+ file.write_all(&data)
+ .unwrap();
+
+ PlainTexture {
+ data: short_path,
+ size: rect_size,
+ format: texture.get_format(),
+ filter: texture.get_filter(),
+ has_depth: texture.supports_depth(),
+ category,
+ }
+ }
+
+ #[cfg(feature = "replay")]
+ fn load_texture(
+ target: ImageBufferKind,
+ plain: &PlainTexture,
+ rt_info: Option<RenderTargetInfo>,
+ root: &PathBuf,
+ device: &mut Device
+ ) -> (Texture, Vec<u8>)
+ {
+ use std::fs::File;
+ use std::io::Read;
+
+ let mut texels = Vec::new();
+ File::open(root.join(&plain.data))
+ .expect(&format!("Unable to open texture at {}", plain.data))
+ .read_to_end(&mut texels)
+ .unwrap();
+
+ let texture = device.create_texture(
+ target,
+ plain.format,
+ plain.size.width,
+ plain.size.height,
+ plain.filter,
+ rt_info,
+ );
+ device.upload_texture_immediate(&texture, &texels);
+
+ (texture, texels)
+ }
+
+ #[cfg(feature = "capture")]
+ fn save_capture(
+ &mut self,
+ config: CaptureConfig,
+ deferred_images: Vec<ExternalCaptureImage>,
+ ) {
+ use std::fs;
+ use std::io::Write;
+ use api::ExternalImageData;
+ use crate::render_api::CaptureBits;
+
+ let root = config.resource_root();
+
+ self.device.begin_frame();
+ let _gm = self.gpu_profiler.start_marker("read GPU data");
+ self.device.bind_read_target_impl(self.read_fbo, DeviceIntPoint::zero());
+
+ if config.bits.contains(CaptureBits::EXTERNAL_RESOURCES) && !deferred_images.is_empty() {
+ info!("saving external images");
+ let mut arc_map = FastHashMap::<*const u8, String>::default();
+ let mut tex_map = FastHashMap::<u32, String>::default();
+ let handler = self.external_image_handler
+ .as_mut()
+ .expect("Unable to lock the external image handler!");
+ for def in &deferred_images {
+ info!("\t{}", def.short_path);
+ let ExternalImageData { id, channel_index, image_type } = def.external;
+ // The image rendering parameter is irrelevant because no filtering happens during capturing.
+ let ext_image = handler.lock(id, channel_index);
+ let (data, short_path) = match ext_image.source {
+ ExternalImageSource::RawData(data) => {
+ let arc_id = arc_map.len() + 1;
+ match arc_map.entry(data.as_ptr()) {
+ Entry::Occupied(e) => {
+ (None, e.get().clone())
+ }
+ Entry::Vacant(e) => {
+ let short_path = format!("externals/d{}.raw", arc_id);
+ (Some(data.to_vec()), e.insert(short_path).clone())
+ }
+ }
+ }
+ ExternalImageSource::NativeTexture(gl_id) => {
+ let tex_id = tex_map.len() + 1;
+ match tex_map.entry(gl_id) {
+ Entry::Occupied(e) => {
+ (None, e.get().clone())
+ }
+ Entry::Vacant(e) => {
+ let target = match image_type {
+ ExternalImageType::TextureHandle(target) => target,
+ ExternalImageType::Buffer => unreachable!(),
+ };
+ info!("\t\tnative texture of target {:?}", target);
+ self.device.attach_read_texture_external(gl_id, target);
+ let data = self.device.read_pixels(&def.descriptor);
+ let short_path = format!("externals/t{}.raw", tex_id);
+ (Some(data), e.insert(short_path).clone())
+ }
+ }
+ }
+ ExternalImageSource::Invalid => {
+ info!("\t\tinvalid source!");
+ (None, String::new())
+ }
+ };
+ if let Some(bytes) = data {
+ fs::File::create(root.join(&short_path))
+ .expect(&format!("Unable to create {}", short_path))
+ .write_all(&bytes)
+ .unwrap();
+ #[cfg(feature = "png")]
+ CaptureConfig::save_png(
+ root.join(&short_path).with_extension("png"),
+ def.descriptor.size,
+ def.descriptor.format,
+ def.descriptor.stride,
+ &bytes,
+ );
+ }
+ let plain = PlainExternalImage {
+ data: short_path,
+ external: def.external,
+ uv: ext_image.uv,
+ };
+ config.serialize_for_resource(&plain, &def.short_path);
+ }
+ for def in &deferred_images {
+ handler.unlock(def.external.id, def.external.channel_index);
+ }
+ let plain_external = PlainExternalResources {
+ images: deferred_images,
+ };
+ config.serialize_for_resource(&plain_external, "external_resources");
+ }
+
+ if config.bits.contains(CaptureBits::FRAME) {
+ let path_textures = root.join("textures");
+ if !path_textures.is_dir() {
+ fs::create_dir(&path_textures).unwrap();
+ }
+
+ info!("saving GPU cache");
+ self.update_gpu_cache(); // flush pending updates
+ let mut plain_self = PlainRenderer {
+ device_size: self.device_size,
+ gpu_cache: Self::save_texture(
+ self.gpu_cache_texture.get_texture(),
+ None, "gpu", &root, &mut self.device,
+ ),
+ gpu_cache_frame_id: self.gpu_cache_frame_id,
+ textures: FastHashMap::default(),
+ };
+
+ info!("saving cached textures");
+ for (id, item) in &self.texture_resolver.texture_cache_map {
+ let file_name = format!("cache-{}", plain_self.textures.len() + 1);
+ info!("\t{}", file_name);
+ let plain = Self::save_texture(&item.texture, Some(item.category), &file_name, &root, &mut self.device);
+ plain_self.textures.insert(*id, plain);
+ }
+
+ config.serialize_for_resource(&plain_self, "renderer");
+ }
+
+ self.device.reset_read_target();
+ self.device.end_frame();
+
+ let mut stats_file = fs::File::create(config.root.join("profiler-stats.txt"))
+ .expect(&format!("Unable to create profiler-stats.txt"));
+ if self.debug_flags.intersects(DebugFlags::PROFILER_DBG | DebugFlags::PROFILER_CAPTURE) {
+ self.profiler.dump_stats(&mut stats_file).unwrap();
+ } else {
+ writeln!(stats_file, "Turn on PROFILER_DBG or PROFILER_CAPTURE to get stats here!").unwrap();
+ }
+
+ info!("done.");
+ }
+
+ #[cfg(feature = "replay")]
+ fn load_capture(
+ &mut self,
+ config: CaptureConfig,
+ plain_externals: Vec<PlainExternalImage>,
+ ) {
+ use std::{fs::File, io::Read};
+
+ info!("loading external buffer-backed images");
+ assert!(self.texture_resolver.external_images.is_empty());
+ let mut raw_map = FastHashMap::<String, Arc<Vec<u8>>>::default();
+ let mut image_handler = DummyExternalImageHandler {
+ data: FastHashMap::default(),
+ };
+
+ let root = config.resource_root();
+
+ // Note: this is a `SCENE` level population of the external image handlers
+ // It would put both external buffers and texture into the map.
+ // But latter are going to be overwritten later in this function
+ // if we are in the `FRAME` level.
+ for plain_ext in plain_externals {
+ let data = match raw_map.entry(plain_ext.data) {
+ Entry::Occupied(e) => e.get().clone(),
+ Entry::Vacant(e) => {
+ let mut buffer = Vec::new();
+ File::open(root.join(e.key()))
+ .expect(&format!("Unable to open {}", e.key()))
+ .read_to_end(&mut buffer)
+ .unwrap();
+ e.insert(Arc::new(buffer)).clone()
+ }
+ };
+ let ext = plain_ext.external;
+ let value = (CapturedExternalImageData::Buffer(data), plain_ext.uv);
+ image_handler.data.insert((ext.id, ext.channel_index), value);
+ }
+
+ if let Some(external_resources) = config.deserialize_for_resource::<PlainExternalResources, _>("external_resources") {
+ info!("loading external texture-backed images");
+ let mut native_map = FastHashMap::<String, gl::GLuint>::default();
+ for ExternalCaptureImage { short_path, external, descriptor } in external_resources.images {
+ let target = match external.image_type {
+ ExternalImageType::TextureHandle(target) => target,
+ ExternalImageType::Buffer => continue,
+ };
+ let plain_ext = config.deserialize_for_resource::<PlainExternalImage, _>(&short_path)
+ .expect(&format!("Unable to read {}.ron", short_path));
+ let key = (external.id, external.channel_index);
+
+ let tid = match native_map.entry(plain_ext.data) {
+ Entry::Occupied(e) => e.get().clone(),
+ Entry::Vacant(e) => {
+ let plain_tex = PlainTexture {
+ data: e.key().clone(),
+ size: descriptor.size,
+ format: descriptor.format,
+ filter: TextureFilter::Linear,
+ has_depth: false,
+ category: None,
+ };
+ let t = Self::load_texture(
+ target,
+ &plain_tex,
+ None,
+ &root,
+ &mut self.device
+ );
+ let extex = t.0.into_external();
+ self.owned_external_images.insert(key, extex.clone());
+ e.insert(extex.internal_id()).clone()
+ }
+ };
+
+ let value = (CapturedExternalImageData::NativeTexture(tid), plain_ext.uv);
+ image_handler.data.insert(key, value);
+ }
+ }
+
+ self.device.begin_frame();
+ self.gpu_cache_texture.remove_texture(&mut self.device);
+
+ if let Some(renderer) = config.deserialize_for_resource::<PlainRenderer, _>("renderer") {
+ info!("loading cached textures");
+ self.device_size = renderer.device_size;
+
+ for (_id, item) in self.texture_resolver.texture_cache_map.drain() {
+ self.device.delete_texture(item.texture);
+ }
+ for (id, texture) in renderer.textures {
+ info!("\t{}", texture.data);
+ let target = ImageBufferKind::Texture2D;
+ let t = Self::load_texture(
+ target,
+ &texture,
+ Some(RenderTargetInfo { has_depth: texture.has_depth }),
+ &root,
+ &mut self.device
+ );
+ self.texture_resolver.texture_cache_map.insert(id, CacheTexture {
+ texture: t.0,
+ category: texture.category.unwrap_or(TextureCacheCategory::Standalone),
+ });
+ }
+
+ info!("loading gpu cache");
+ let (t, gpu_cache_data) = Self::load_texture(
+ ImageBufferKind::Texture2D,
+ &renderer.gpu_cache,
+ Some(RenderTargetInfo { has_depth: false }),
+ &root,
+ &mut self.device,
+ );
+ self.gpu_cache_texture.load_from_data(t, gpu_cache_data);
+ self.gpu_cache_frame_id = renderer.gpu_cache_frame_id;
+ } else {
+ info!("loading cached textures");
+ self.device.begin_frame();
+ for (_id, item) in self.texture_resolver.texture_cache_map.drain() {
+ self.device.delete_texture(item.texture);
+ }
+ }
+ self.device.end_frame();
+
+ self.external_image_handler = Some(Box::new(image_handler) as Box<_>);
+ info!("done.");
+ }
+}
+
+#[derive(Clone, Copy, PartialEq)]
+enum FramebufferKind {
+ Main,
+ Other,
+}
+
+fn should_skip_batch(kind: &BatchKind, flags: DebugFlags) -> bool {
+ match kind {
+ BatchKind::TextRun(_) => {
+ flags.contains(DebugFlags::DISABLE_TEXT_PRIMS)
+ }
+ BatchKind::Brush(BrushBatchKind::LinearGradient) => {
+ flags.contains(DebugFlags::DISABLE_GRADIENT_PRIMS)
+ }
+ _ => false,
+ }
+}
+
+impl CompositeState {
+ /// Use the client provided native compositor interface to add all picture
+ /// cache tiles to the OS compositor
+ fn composite_native(
+ &self,
+ clear_color: ColorF,
+ dirty_rects: &[DeviceIntRect],
+ device: &mut Device,
+ compositor: &mut dyn Compositor,
+ ) {
+ // Add each surface to the visual tree. z-order is implicit based on
+ // order added. Offset and clip rect apply to all tiles within this
+ // surface.
+ for surface in &self.descriptor.surfaces {
+ compositor.add_surface(
+ device,
+ surface.surface_id.expect("bug: no native surface allocated"),
+ surface.transform,
+ surface.clip_rect.to_i32(),
+ surface.image_rendering,
+ );
+ }
+ compositor.start_compositing(device, clear_color, dirty_rects, &[]);
+ }
+}
+
+mod tests {
+ #[test]
+ fn test_buffer_damage_tracker() {
+ use super::BufferDamageTracker;
+ use api::units::{DevicePoint, DeviceRect, DeviceSize};
+
+ let mut tracker = BufferDamageTracker::default();
+ assert_eq!(tracker.get_damage_rect(0), None);
+ assert_eq!(tracker.get_damage_rect(1), Some(DeviceRect::zero()));
+ assert_eq!(tracker.get_damage_rect(2), Some(DeviceRect::zero()));
+ assert_eq!(tracker.get_damage_rect(3), Some(DeviceRect::zero()));
+ assert_eq!(tracker.get_damage_rect(4), None);
+
+ let damage1 = DeviceRect::from_origin_and_size(DevicePoint::new(10.0, 10.0), DeviceSize::new(10.0, 10.0));
+ let damage2 = DeviceRect::from_origin_and_size(DevicePoint::new(20.0, 20.0), DeviceSize::new(10.0, 10.0));
+ let combined = damage1.union(&damage2);
+
+ tracker.push_dirty_rect(&damage1);
+ assert_eq!(tracker.get_damage_rect(0), None);
+ assert_eq!(tracker.get_damage_rect(1), Some(DeviceRect::zero()));
+ assert_eq!(tracker.get_damage_rect(2), Some(damage1));
+ assert_eq!(tracker.get_damage_rect(3), Some(damage1));
+ assert_eq!(tracker.get_damage_rect(4), None);
+
+ tracker.push_dirty_rect(&damage2);
+ assert_eq!(tracker.get_damage_rect(0), None);
+ assert_eq!(tracker.get_damage_rect(1), Some(DeviceRect::zero()));
+ assert_eq!(tracker.get_damage_rect(2), Some(damage2));
+ assert_eq!(tracker.get_damage_rect(3), Some(combined));
+ assert_eq!(tracker.get_damage_rect(4), None);
+ }
+}
diff --git a/gfx/wr/webrender/src/renderer/shade.rs b/gfx/wr/webrender/src/renderer/shade.rs
new file mode 100644
index 0000000000..72b6578a4f
--- /dev/null
+++ b/gfx/wr/webrender/src/renderer/shade.rs
@@ -0,0 +1,1496 @@
+/* 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 api::{ImageBufferKind, units::DeviceSize};
+use crate::batch::{BatchKey, BatchKind, BrushBatchKind, BatchFeatures};
+use crate::composite::{CompositeFeatures, CompositeSurfaceFormat};
+use crate::device::{Device, Program, ShaderError};
+use euclid::default::Transform3D;
+use glyph_rasterizer::GlyphFormat;
+use crate::renderer::{
+ desc,
+ BlendMode, DebugFlags, RendererError, WebRenderOptions,
+ TextureSampler, VertexArrayKind, ShaderPrecacheFlags,
+};
+use crate::profiler::{self, TransactionProfile, ns_to_ms};
+
+use gleam::gl::GlType;
+use time::precise_time_ns;
+
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use webrender_build::shader::{ShaderFeatures, ShaderFeatureFlags, get_shader_features};
+
+/// Which extension version to use for texture external support.
+#[derive(Clone, Copy, Debug, PartialEq)]
+enum TextureExternalVersion {
+ // GL_OES_EGL_image_external_essl3 (Compatible with ESSL 3.0 and
+ // later shaders, but not supported on all GLES 3 devices.)
+ ESSL3,
+ // GL_OES_EGL_image_external (Compatible with ESSL 1.0 shaders)
+ ESSL1,
+}
+
+fn get_feature_string(kind: ImageBufferKind, texture_external_version: TextureExternalVersion) -> &'static str {
+ match (kind, texture_external_version) {
+ (ImageBufferKind::Texture2D, _) => "TEXTURE_2D",
+ (ImageBufferKind::TextureRect, _) => "TEXTURE_RECT",
+ (ImageBufferKind::TextureExternal, TextureExternalVersion::ESSL3) => "TEXTURE_EXTERNAL",
+ (ImageBufferKind::TextureExternal, TextureExternalVersion::ESSL1) => "TEXTURE_EXTERNAL_ESSL1",
+ }
+}
+
+fn has_platform_support(kind: ImageBufferKind, gl_type: &GlType) -> bool {
+ match (kind, gl_type) {
+ (ImageBufferKind::Texture2D, _) => true,
+ (ImageBufferKind::TextureRect, &GlType::Gles) => false,
+ (ImageBufferKind::TextureRect, &GlType::Gl) => true,
+ (ImageBufferKind::TextureExternal, &GlType::Gles) => true,
+ (ImageBufferKind::TextureExternal, &GlType::Gl) => false,
+ }
+}
+
+pub const IMAGE_BUFFER_KINDS: [ImageBufferKind; 3] = [
+ ImageBufferKind::Texture2D,
+ ImageBufferKind::TextureRect,
+ ImageBufferKind::TextureExternal,
+];
+
+const ADVANCED_BLEND_FEATURE: &str = "ADVANCED_BLEND";
+const ALPHA_FEATURE: &str = "ALPHA_PASS";
+const DEBUG_OVERDRAW_FEATURE: &str = "DEBUG_OVERDRAW";
+const DITHERING_FEATURE: &str = "DITHERING";
+const DUAL_SOURCE_FEATURE: &str = "DUAL_SOURCE_BLENDING";
+const FAST_PATH_FEATURE: &str = "FAST_PATH";
+
+pub(crate) enum ShaderKind {
+ Primitive,
+ Cache(VertexArrayKind),
+ ClipCache(VertexArrayKind),
+ Brush,
+ Text,
+ #[allow(dead_code)]
+ VectorStencil,
+ #[allow(dead_code)]
+ VectorCover,
+ #[allow(dead_code)]
+ Resolve,
+ Composite,
+ Clear,
+ Copy,
+}
+
+pub struct LazilyCompiledShader {
+ program: Option<Program>,
+ name: &'static str,
+ kind: ShaderKind,
+ cached_projection: Transform3D<f32>,
+ features: Vec<&'static str>,
+}
+
+impl LazilyCompiledShader {
+ pub(crate) fn new(
+ kind: ShaderKind,
+ name: &'static str,
+ unsorted_features: &[&'static str],
+ device: &mut Device,
+ precache_flags: ShaderPrecacheFlags,
+ shader_list: &ShaderFeatures,
+ profile: &mut TransactionProfile,
+ ) -> Result<Self, ShaderError> {
+
+ let mut features = unsorted_features.to_vec();
+ features.sort();
+
+ // Ensure this shader config is in the available shader list so that we get
+ // alerted if the list gets out-of-date when shaders or features are added.
+ let config = features.join(",");
+ assert!(
+ shader_list.get(name).map_or(false, |f| f.contains(&config)),
+ "shader \"{}\" with features \"{}\" not in available shader list",
+ name,
+ config,
+ );
+
+ let mut shader = LazilyCompiledShader {
+ program: None,
+ name,
+ kind,
+ //Note: this isn't really the default state, but there is no chance
+ // an actual projection passed here would accidentally match.
+ cached_projection: Transform3D::identity(),
+ features,
+ };
+
+ if precache_flags.intersects(ShaderPrecacheFlags::ASYNC_COMPILE | ShaderPrecacheFlags::FULL_COMPILE) {
+ let t0 = precise_time_ns();
+ shader.get_internal(device, precache_flags, profile)?;
+ let t1 = precise_time_ns();
+ debug!("[C: {:.1} ms ] Precache {} {:?}",
+ (t1 - t0) as f64 / 1000000.0,
+ name,
+ unsorted_features
+ );
+ }
+
+ Ok(shader)
+ }
+
+ pub fn bind(
+ &mut self,
+ device: &mut Device,
+ projection: &Transform3D<f32>,
+ texture_size: Option<DeviceSize>,
+ renderer_errors: &mut Vec<RendererError>,
+ profile: &mut TransactionProfile,
+ ) {
+ let update_projection = self.cached_projection != *projection;
+ let program = match self.get_internal(device, ShaderPrecacheFlags::FULL_COMPILE, profile) {
+ Ok(program) => program,
+ Err(e) => {
+ renderer_errors.push(RendererError::from(e));
+ return;
+ }
+ };
+ device.bind_program(program);
+ if let Some(texture_size) = texture_size {
+ device.set_shader_texture_size(program, texture_size);
+ }
+ if update_projection {
+ device.set_uniforms(program, projection);
+ // thanks NLL for this (`program` technically borrows `self`)
+ self.cached_projection = *projection;
+ }
+ }
+
+ fn get_internal(
+ &mut self,
+ device: &mut Device,
+ precache_flags: ShaderPrecacheFlags,
+ profile: &mut TransactionProfile,
+ ) -> Result<&mut Program, ShaderError> {
+ if self.program.is_none() {
+ let start_time = precise_time_ns();
+ let program = match self.kind {
+ ShaderKind::Primitive | ShaderKind::Brush | ShaderKind::Text | ShaderKind::Resolve | ShaderKind::Clear | ShaderKind::Copy => {
+ create_prim_shader(
+ self.name,
+ device,
+ &self.features,
+ )
+ }
+ ShaderKind::Cache(..) => {
+ create_prim_shader(
+ self.name,
+ device,
+ &self.features,
+ )
+ }
+ ShaderKind::VectorStencil => {
+ create_prim_shader(
+ self.name,
+ device,
+ &self.features,
+ )
+ }
+ ShaderKind::VectorCover => {
+ create_prim_shader(
+ self.name,
+ device,
+ &self.features,
+ )
+ }
+ ShaderKind::Composite => {
+ create_prim_shader(
+ self.name,
+ device,
+ &self.features,
+ )
+ }
+ ShaderKind::ClipCache(..) => {
+ create_clip_shader(
+ self.name,
+ device,
+ &self.features,
+ )
+ }
+ };
+ self.program = Some(program?);
+
+ let end_time = precise_time_ns();
+ profile.add(profiler::SHADER_BUILD_TIME, ns_to_ms(end_time - start_time));
+ }
+
+ let program = self.program.as_mut().unwrap();
+
+ if precache_flags.contains(ShaderPrecacheFlags::FULL_COMPILE) && !program.is_initialized() {
+ let start_time = precise_time_ns();
+
+ let vertex_format = match self.kind {
+ ShaderKind::Primitive |
+ ShaderKind::Brush |
+ ShaderKind::Text => VertexArrayKind::Primitive,
+ ShaderKind::Cache(format) => format,
+ ShaderKind::VectorStencil => VertexArrayKind::VectorStencil,
+ ShaderKind::VectorCover => VertexArrayKind::VectorCover,
+ ShaderKind::ClipCache(format) => format,
+ ShaderKind::Resolve => VertexArrayKind::Resolve,
+ ShaderKind::Composite => VertexArrayKind::Composite,
+ ShaderKind::Clear => VertexArrayKind::Clear,
+ ShaderKind::Copy => VertexArrayKind::Copy,
+ };
+
+ let vertex_descriptor = match vertex_format {
+ VertexArrayKind::Primitive => &desc::PRIM_INSTANCES,
+ VertexArrayKind::LineDecoration => &desc::LINE,
+ VertexArrayKind::FastLinearGradient => &desc::FAST_LINEAR_GRADIENT,
+ VertexArrayKind::LinearGradient => &desc::LINEAR_GRADIENT,
+ VertexArrayKind::RadialGradient => &desc::RADIAL_GRADIENT,
+ VertexArrayKind::ConicGradient => &desc::CONIC_GRADIENT,
+ VertexArrayKind::Blur => &desc::BLUR,
+ VertexArrayKind::ClipImage => &desc::CLIP_IMAGE,
+ VertexArrayKind::ClipRect => &desc::CLIP_RECT,
+ VertexArrayKind::ClipBoxShadow => &desc::CLIP_BOX_SHADOW,
+ VertexArrayKind::VectorStencil => &desc::VECTOR_STENCIL,
+ VertexArrayKind::VectorCover => &desc::VECTOR_COVER,
+ VertexArrayKind::Border => &desc::BORDER,
+ VertexArrayKind::Scale => &desc::SCALE,
+ VertexArrayKind::Resolve => &desc::RESOLVE,
+ VertexArrayKind::SvgFilter => &desc::SVG_FILTER,
+ VertexArrayKind::Composite => &desc::COMPOSITE,
+ VertexArrayKind::Clear => &desc::CLEAR,
+ VertexArrayKind::Copy => &desc::COPY,
+ VertexArrayKind::Mask => &desc::MASK,
+ };
+
+ device.link_program(program, vertex_descriptor)?;
+ device.bind_program(program);
+ match self.kind {
+ ShaderKind::ClipCache(..) => {
+ device.bind_shader_samplers(
+ &program,
+ &[
+ ("sColor0", TextureSampler::Color0),
+ ("sTransformPalette", TextureSampler::TransformPalette),
+ ("sRenderTasks", TextureSampler::RenderTasks),
+ ("sGpuCache", TextureSampler::GpuCache),
+ ("sPrimitiveHeadersF", TextureSampler::PrimitiveHeadersF),
+ ("sPrimitiveHeadersI", TextureSampler::PrimitiveHeadersI),
+ ("sGpuBuffer", TextureSampler::GpuBuffer),
+ ],
+ );
+ }
+ _ => {
+ device.bind_shader_samplers(
+ &program,
+ &[
+ ("sColor0", TextureSampler::Color0),
+ ("sColor1", TextureSampler::Color1),
+ ("sColor2", TextureSampler::Color2),
+ ("sDither", TextureSampler::Dither),
+ ("sTransformPalette", TextureSampler::TransformPalette),
+ ("sRenderTasks", TextureSampler::RenderTasks),
+ ("sGpuCache", TextureSampler::GpuCache),
+ ("sPrimitiveHeadersF", TextureSampler::PrimitiveHeadersF),
+ ("sPrimitiveHeadersI", TextureSampler::PrimitiveHeadersI),
+ ("sClipMask", TextureSampler::ClipMask),
+ ("sGpuBuffer", TextureSampler::GpuBuffer),
+ ],
+ );
+ }
+ }
+
+ let end_time = precise_time_ns();
+ profile.add(profiler::SHADER_BUILD_TIME, ns_to_ms(end_time - start_time));
+ }
+
+ Ok(program)
+ }
+
+ fn deinit(self, device: &mut Device) {
+ if let Some(program) = self.program {
+ device.delete_program(program);
+ }
+ }
+}
+
+// A brush shader supports two modes:
+// opaque:
+// Used for completely opaque primitives,
+// or inside segments of partially
+// opaque primitives. Assumes no need
+// for clip masks, AA etc.
+// alpha:
+// Used for brush primitives in the alpha
+// pass. Assumes that AA should be applied
+// along the primitive edge, and also that
+// clip mask is present.
+struct BrushShader {
+ opaque: LazilyCompiledShader,
+ alpha: LazilyCompiledShader,
+ advanced_blend: Option<LazilyCompiledShader>,
+ dual_source: Option<LazilyCompiledShader>,
+ debug_overdraw: LazilyCompiledShader,
+}
+
+impl BrushShader {
+ fn new(
+ name: &'static str,
+ device: &mut Device,
+ features: &[&'static str],
+ precache_flags: ShaderPrecacheFlags,
+ shader_list: &ShaderFeatures,
+ use_advanced_blend: bool,
+ use_dual_source: bool,
+ profile: &mut TransactionProfile,
+ ) -> Result<Self, ShaderError> {
+ let opaque_features = features.to_vec();
+ let opaque = LazilyCompiledShader::new(
+ ShaderKind::Brush,
+ name,
+ &opaque_features,
+ device,
+ precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ let mut alpha_features = opaque_features.to_vec();
+ alpha_features.push(ALPHA_FEATURE);
+
+ let alpha = LazilyCompiledShader::new(
+ ShaderKind::Brush,
+ name,
+ &alpha_features,
+ device,
+ precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ let advanced_blend = if use_advanced_blend {
+ let mut advanced_blend_features = alpha_features.to_vec();
+ advanced_blend_features.push(ADVANCED_BLEND_FEATURE);
+
+ let shader = LazilyCompiledShader::new(
+ ShaderKind::Brush,
+ name,
+ &advanced_blend_features,
+ device,
+ precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ Some(shader)
+ } else {
+ None
+ };
+
+ let dual_source = if use_dual_source {
+ let mut dual_source_features = alpha_features.to_vec();
+ dual_source_features.push(DUAL_SOURCE_FEATURE);
+
+ let shader = LazilyCompiledShader::new(
+ ShaderKind::Brush,
+ name,
+ &dual_source_features,
+ device,
+ precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ Some(shader)
+ } else {
+ None
+ };
+
+ let mut debug_overdraw_features = features.to_vec();
+ debug_overdraw_features.push(DEBUG_OVERDRAW_FEATURE);
+
+ let debug_overdraw = LazilyCompiledShader::new(
+ ShaderKind::Brush,
+ name,
+ &debug_overdraw_features,
+ device,
+ precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ Ok(BrushShader {
+ opaque,
+ alpha,
+ advanced_blend,
+ dual_source,
+ debug_overdraw,
+ })
+ }
+
+ fn get(&mut self, blend_mode: BlendMode, features: BatchFeatures, debug_flags: DebugFlags)
+ -> &mut LazilyCompiledShader {
+ match blend_mode {
+ _ if debug_flags.contains(DebugFlags::SHOW_OVERDRAW) => &mut self.debug_overdraw,
+ BlendMode::None => &mut self.opaque,
+ BlendMode::Alpha |
+ BlendMode::PremultipliedAlpha |
+ BlendMode::PremultipliedDestOut |
+ BlendMode::SubpixelWithBgColor |
+ BlendMode::Screen |
+ BlendMode::PlusLighter |
+ BlendMode::Exclusion => {
+ if features.contains(BatchFeatures::ALPHA_PASS) {
+ &mut self.alpha
+ } else {
+ &mut self.opaque
+ }
+ }
+ BlendMode::Advanced(_) => {
+ self.advanced_blend
+ .as_mut()
+ .expect("bug: no advanced blend shader loaded")
+ }
+ BlendMode::SubpixelDualSource |
+ BlendMode::MultiplyDualSource => {
+ self.dual_source
+ .as_mut()
+ .expect("bug: no dual source shader loaded")
+ }
+ }
+ }
+
+ fn deinit(self, device: &mut Device) {
+ self.opaque.deinit(device);
+ self.alpha.deinit(device);
+ if let Some(advanced_blend) = self.advanced_blend {
+ advanced_blend.deinit(device);
+ }
+ if let Some(dual_source) = self.dual_source {
+ dual_source.deinit(device);
+ }
+ self.debug_overdraw.deinit(device);
+ }
+}
+
+pub struct TextShader {
+ simple: LazilyCompiledShader,
+ glyph_transform: LazilyCompiledShader,
+ debug_overdraw: LazilyCompiledShader,
+}
+
+impl TextShader {
+ fn new(
+ name: &'static str,
+ device: &mut Device,
+ features: &[&'static str],
+ precache_flags: ShaderPrecacheFlags,
+ shader_list: &ShaderFeatures,
+ profile: &mut TransactionProfile,
+ ) -> Result<Self, ShaderError> {
+ let mut simple_features = features.to_vec();
+ simple_features.push("ALPHA_PASS");
+ simple_features.push("TEXTURE_2D");
+
+ let simple = LazilyCompiledShader::new(
+ ShaderKind::Text,
+ name,
+ &simple_features,
+ device,
+ precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ let mut glyph_transform_features = features.to_vec();
+ glyph_transform_features.push("GLYPH_TRANSFORM");
+ glyph_transform_features.push("ALPHA_PASS");
+ glyph_transform_features.push("TEXTURE_2D");
+
+ let glyph_transform = LazilyCompiledShader::new(
+ ShaderKind::Text,
+ name,
+ &glyph_transform_features,
+ device,
+ precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ let mut debug_overdraw_features = features.to_vec();
+ debug_overdraw_features.push("DEBUG_OVERDRAW");
+ debug_overdraw_features.push("TEXTURE_2D");
+
+ let debug_overdraw = LazilyCompiledShader::new(
+ ShaderKind::Text,
+ name,
+ &debug_overdraw_features,
+ device,
+ precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ Ok(TextShader { simple, glyph_transform, debug_overdraw })
+ }
+
+ pub fn get(
+ &mut self,
+ glyph_format: GlyphFormat,
+ debug_flags: DebugFlags,
+ ) -> &mut LazilyCompiledShader {
+ match glyph_format {
+ _ if debug_flags.contains(DebugFlags::SHOW_OVERDRAW) => &mut self.debug_overdraw,
+ GlyphFormat::Alpha |
+ GlyphFormat::Subpixel |
+ GlyphFormat::Bitmap |
+ GlyphFormat::ColorBitmap => &mut self.simple,
+ GlyphFormat::TransformedAlpha |
+ GlyphFormat::TransformedSubpixel => &mut self.glyph_transform,
+ }
+ }
+
+ fn deinit(self, device: &mut Device) {
+ self.simple.deinit(device);
+ self.glyph_transform.deinit(device);
+ self.debug_overdraw.deinit(device);
+ }
+}
+
+fn create_prim_shader(
+ name: &'static str,
+ device: &mut Device,
+ features: &[&'static str],
+) -> Result<Program, ShaderError> {
+ debug!("PrimShader {}", name);
+
+ device.create_program(name, features)
+}
+
+fn create_clip_shader(
+ name: &'static str,
+ device: &mut Device,
+ features: &[&'static str],
+) -> Result<Program, ShaderError> {
+ debug!("ClipShader {}", name);
+
+ device.create_program(name, features)
+}
+
+// NB: If you add a new shader here, make sure to deinitialize it
+// in `Shaders::deinit()` below.
+pub struct Shaders {
+ // These are "cache shaders". These shaders are used to
+ // draw intermediate results to cache targets. The results
+ // of these shaders are then used by the primitive shaders.
+ pub cs_blur_a8: LazilyCompiledShader,
+ pub cs_blur_rgba8: LazilyCompiledShader,
+ pub cs_border_segment: LazilyCompiledShader,
+ pub cs_border_solid: LazilyCompiledShader,
+ pub cs_scale: Vec<Option<LazilyCompiledShader>>,
+ pub cs_line_decoration: LazilyCompiledShader,
+ pub cs_fast_linear_gradient: LazilyCompiledShader,
+ pub cs_linear_gradient: LazilyCompiledShader,
+ pub cs_radial_gradient: LazilyCompiledShader,
+ pub cs_conic_gradient: LazilyCompiledShader,
+ pub cs_svg_filter: LazilyCompiledShader,
+
+ // Brush shaders
+ brush_solid: BrushShader,
+ brush_image: Vec<Option<BrushShader>>,
+ brush_fast_image: Vec<Option<BrushShader>>,
+ brush_blend: BrushShader,
+ brush_mix_blend: BrushShader,
+ brush_yuv_image: Vec<Option<BrushShader>>,
+ brush_linear_gradient: BrushShader,
+ brush_opacity: BrushShader,
+ brush_opacity_aa: BrushShader,
+
+ /// These are "cache clip shaders". These shaders are used to
+ /// draw clip instances into the cached clip mask. The results
+ /// of these shaders are also used by the primitive shaders.
+ pub cs_clip_rectangle_slow: LazilyCompiledShader,
+ pub cs_clip_rectangle_fast: LazilyCompiledShader,
+ pub cs_clip_box_shadow: LazilyCompiledShader,
+ pub cs_clip_image: LazilyCompiledShader,
+
+ // The are "primitive shaders". These shaders draw and blend
+ // final results on screen. They are aware of tile boundaries.
+ // Most draw directly to the framebuffer, but some use inputs
+ // from the cache shaders to draw. Specifically, the box
+ // shadow primitive shader stretches the box shadow cache
+ // output, and the cache_image shader blits the results of
+ // a cache shader (e.g. blur) to the screen.
+ pub ps_text_run: TextShader,
+ pub ps_text_run_dual_source: Option<TextShader>,
+
+ ps_split_composite: LazilyCompiledShader,
+ pub ps_quad_textured: LazilyCompiledShader,
+ pub ps_mask: LazilyCompiledShader,
+ pub ps_mask_fast: LazilyCompiledShader,
+ pub ps_clear: LazilyCompiledShader,
+ pub ps_copy: LazilyCompiledShader,
+
+ pub composite: CompositorShaders,
+}
+
+impl Shaders {
+ pub fn new(
+ device: &mut Device,
+ gl_type: GlType,
+ options: &WebRenderOptions,
+ ) -> Result<Self, ShaderError> {
+ // We have to pass a profile around a bunch but we aren't recording the initialization
+ // so use a dummy one.
+ let profile = &mut TransactionProfile::new();
+
+ let use_dual_source_blending =
+ device.get_capabilities().supports_dual_source_blending &&
+ options.allow_dual_source_blending;
+ let use_advanced_blend_equation =
+ device.get_capabilities().supports_advanced_blend_equation &&
+ options.allow_advanced_blend_equation;
+
+ let texture_external_version = if device.get_capabilities().supports_image_external_essl3 {
+ TextureExternalVersion::ESSL3
+ } else {
+ TextureExternalVersion::ESSL1
+ };
+ let mut shader_flags = get_shader_feature_flags(gl_type, texture_external_version);
+ shader_flags.set(ShaderFeatureFlags::ADVANCED_BLEND_EQUATION, use_advanced_blend_equation);
+ shader_flags.set(ShaderFeatureFlags::DUAL_SOURCE_BLENDING, use_dual_source_blending);
+ shader_flags.set(ShaderFeatureFlags::DITHERING, options.enable_dithering);
+ let shader_list = get_shader_features(shader_flags);
+
+ let brush_solid = BrushShader::new(
+ "brush_solid",
+ device,
+ &[],
+ options.precache_flags,
+ &shader_list,
+ false /* advanced blend */,
+ false /* dual source */,
+ profile,
+ )?;
+
+ let brush_blend = BrushShader::new(
+ "brush_blend",
+ device,
+ &[],
+ options.precache_flags,
+ &shader_list,
+ false /* advanced blend */,
+ false /* dual source */,
+ profile,
+ )?;
+
+ let brush_mix_blend = BrushShader::new(
+ "brush_mix_blend",
+ device,
+ &[],
+ options.precache_flags,
+ &shader_list,
+ false /* advanced blend */,
+ false /* dual source */,
+ profile,
+ )?;
+
+ let brush_linear_gradient = BrushShader::new(
+ "brush_linear_gradient",
+ device,
+ if options.enable_dithering {
+ &[DITHERING_FEATURE]
+ } else {
+ &[]
+ },
+ options.precache_flags,
+ &shader_list,
+ false /* advanced blend */,
+ false /* dual source */,
+ profile,
+ )?;
+
+ let brush_opacity_aa = BrushShader::new(
+ "brush_opacity",
+ device,
+ &["ANTIALIASING"],
+ options.precache_flags,
+ &shader_list,
+ false /* advanced blend */,
+ false /* dual source */,
+ profile,
+ )?;
+
+ let brush_opacity = BrushShader::new(
+ "brush_opacity",
+ device,
+ &[],
+ options.precache_flags,
+ &shader_list,
+ false /* advanced blend */,
+ false /* dual source */,
+ profile,
+ )?;
+
+ let cs_blur_a8 = LazilyCompiledShader::new(
+ ShaderKind::Cache(VertexArrayKind::Blur),
+ "cs_blur",
+ &["ALPHA_TARGET"],
+ device,
+ options.precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ let cs_blur_rgba8 = LazilyCompiledShader::new(
+ ShaderKind::Cache(VertexArrayKind::Blur),
+ "cs_blur",
+ &["COLOR_TARGET"],
+ device,
+ options.precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ let cs_svg_filter = LazilyCompiledShader::new(
+ ShaderKind::Cache(VertexArrayKind::SvgFilter),
+ "cs_svg_filter",
+ &[],
+ device,
+ options.precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ let ps_mask = LazilyCompiledShader::new(
+ ShaderKind::Cache(VertexArrayKind::Mask),
+ "ps_quad_mask",
+ &[],
+ device,
+ options.precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ let ps_mask_fast = LazilyCompiledShader::new(
+ ShaderKind::Cache(VertexArrayKind::Mask),
+ "ps_quad_mask",
+ &[FAST_PATH_FEATURE],
+ device,
+ options.precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ let cs_clip_rectangle_slow = LazilyCompiledShader::new(
+ ShaderKind::ClipCache(VertexArrayKind::ClipRect),
+ "cs_clip_rectangle",
+ &[],
+ device,
+ options.precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ let cs_clip_rectangle_fast = LazilyCompiledShader::new(
+ ShaderKind::ClipCache(VertexArrayKind::ClipRect),
+ "cs_clip_rectangle",
+ &[FAST_PATH_FEATURE],
+ device,
+ options.precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ let cs_clip_box_shadow = LazilyCompiledShader::new(
+ ShaderKind::ClipCache(VertexArrayKind::ClipBoxShadow),
+ "cs_clip_box_shadow",
+ &["TEXTURE_2D"],
+ device,
+ options.precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ let cs_clip_image = LazilyCompiledShader::new(
+ ShaderKind::ClipCache(VertexArrayKind::ClipImage),
+ "cs_clip_image",
+ &["TEXTURE_2D"],
+ device,
+ options.precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ let mut cs_scale = Vec::new();
+ let scale_shader_num = IMAGE_BUFFER_KINDS.len();
+ // PrimitiveShader is not clonable. Use push() to initialize the vec.
+ for _ in 0 .. scale_shader_num {
+ cs_scale.push(None);
+ }
+ for image_buffer_kind in &IMAGE_BUFFER_KINDS {
+ if has_platform_support(*image_buffer_kind, &gl_type) {
+ let feature_string = get_feature_string(
+ *image_buffer_kind,
+ texture_external_version,
+ );
+
+ let mut features = Vec::new();
+ if feature_string != "" {
+ features.push(feature_string);
+ }
+
+ let shader = LazilyCompiledShader::new(
+ ShaderKind::Cache(VertexArrayKind::Scale),
+ "cs_scale",
+ &features,
+ device,
+ options.precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ let index = Self::get_compositing_shader_index(
+ *image_buffer_kind,
+ );
+ cs_scale[index] = Some(shader);
+ }
+ }
+
+ // TODO(gw): The split composite + text shader are special cases - the only
+ // shaders used during normal scene rendering that aren't a brush
+ // shader. Perhaps we can unify these in future?
+
+ let ps_text_run = TextShader::new("ps_text_run",
+ device,
+ &[],
+ options.precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ let ps_text_run_dual_source = if use_dual_source_blending {
+ let dual_source_features = vec![DUAL_SOURCE_FEATURE];
+ Some(TextShader::new("ps_text_run",
+ device,
+ &dual_source_features,
+ options.precache_flags,
+ &shader_list,
+ profile,
+ )?)
+ } else {
+ None
+ };
+
+ let ps_quad_textured = LazilyCompiledShader::new(
+ ShaderKind::Primitive,
+ "ps_quad_textured",
+ &[],
+ device,
+ options.precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ let ps_split_composite = LazilyCompiledShader::new(
+ ShaderKind::Primitive,
+ "ps_split_composite",
+ &[],
+ device,
+ options.precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ let ps_clear = LazilyCompiledShader::new(
+ ShaderKind::Clear,
+ "ps_clear",
+ &[],
+ device,
+ options.precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ let ps_copy = LazilyCompiledShader::new(
+ ShaderKind::Copy,
+ "ps_copy",
+ &[],
+ device,
+ options.precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ // All image configuration.
+ let mut image_features = Vec::new();
+ let mut brush_image = Vec::new();
+ let mut brush_fast_image = Vec::new();
+ // PrimitiveShader is not clonable. Use push() to initialize the vec.
+ for _ in 0 .. IMAGE_BUFFER_KINDS.len() {
+ brush_image.push(None);
+ brush_fast_image.push(None);
+ }
+ for buffer_kind in 0 .. IMAGE_BUFFER_KINDS.len() {
+ if !has_platform_support(IMAGE_BUFFER_KINDS[buffer_kind], &gl_type)
+ // Brush shaders are not ESSL1 compatible
+ || (IMAGE_BUFFER_KINDS[buffer_kind] == ImageBufferKind::TextureExternal
+ && texture_external_version == TextureExternalVersion::ESSL1)
+ {
+ continue;
+ }
+
+ let feature_string = get_feature_string(
+ IMAGE_BUFFER_KINDS[buffer_kind],
+ texture_external_version,
+ );
+ if feature_string != "" {
+ image_features.push(feature_string);
+ }
+
+ brush_fast_image[buffer_kind] = Some(BrushShader::new(
+ "brush_image",
+ device,
+ &image_features,
+ options.precache_flags,
+ &shader_list,
+ use_advanced_blend_equation,
+ use_dual_source_blending,
+ profile,
+ )?);
+
+ image_features.push("REPETITION");
+ image_features.push("ANTIALIASING");
+
+ brush_image[buffer_kind] = Some(BrushShader::new(
+ "brush_image",
+ device,
+ &image_features,
+ options.precache_flags,
+ &shader_list,
+ use_advanced_blend_equation,
+ use_dual_source_blending,
+ profile,
+ )?);
+
+ image_features.clear();
+ }
+
+ // All yuv_image configuration.
+ let mut yuv_features = Vec::new();
+ let mut rgba_features = Vec::new();
+ let mut fast_path_features = Vec::new();
+ let yuv_shader_num = IMAGE_BUFFER_KINDS.len();
+ let mut brush_yuv_image = Vec::new();
+ // PrimitiveShader is not clonable. Use push() to initialize the vec.
+ for _ in 0 .. yuv_shader_num {
+ brush_yuv_image.push(None);
+ }
+ for image_buffer_kind in &IMAGE_BUFFER_KINDS {
+ if has_platform_support(*image_buffer_kind, &gl_type) {
+ yuv_features.push("YUV");
+ fast_path_features.push("FAST_PATH");
+
+ let index = Self::get_compositing_shader_index(
+ *image_buffer_kind,
+ );
+
+ let feature_string = get_feature_string(
+ *image_buffer_kind,
+ texture_external_version,
+ );
+ if feature_string != "" {
+ yuv_features.push(feature_string);
+ rgba_features.push(feature_string);
+ fast_path_features.push(feature_string);
+ }
+
+ // YUV shaders are not compatible with ESSL1
+ if *image_buffer_kind != ImageBufferKind::TextureExternal ||
+ texture_external_version == TextureExternalVersion::ESSL3 {
+ let brush_shader = BrushShader::new(
+ "brush_yuv_image",
+ device,
+ &yuv_features,
+ options.precache_flags,
+ &shader_list,
+ false /* advanced blend */,
+ false /* dual source */,
+ profile,
+ )?;
+ brush_yuv_image[index] = Some(brush_shader);
+ }
+
+ yuv_features.clear();
+ rgba_features.clear();
+ fast_path_features.clear();
+ }
+ }
+
+ let cs_line_decoration = LazilyCompiledShader::new(
+ ShaderKind::Cache(VertexArrayKind::LineDecoration),
+ "cs_line_decoration",
+ &[],
+ device,
+ options.precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ let cs_fast_linear_gradient = LazilyCompiledShader::new(
+ ShaderKind::Cache(VertexArrayKind::FastLinearGradient),
+ "cs_fast_linear_gradient",
+ &[],
+ device,
+ options.precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ let cs_linear_gradient = LazilyCompiledShader::new(
+ ShaderKind::Cache(VertexArrayKind::LinearGradient),
+ "cs_linear_gradient",
+ &[],
+ device,
+ options.precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ let cs_radial_gradient = LazilyCompiledShader::new(
+ ShaderKind::Cache(VertexArrayKind::RadialGradient),
+ "cs_radial_gradient",
+ &[],
+ device,
+ options.precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ let cs_conic_gradient = LazilyCompiledShader::new(
+ ShaderKind::Cache(VertexArrayKind::ConicGradient),
+ "cs_conic_gradient",
+ &[],
+ device,
+ options.precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ let cs_border_segment = LazilyCompiledShader::new(
+ ShaderKind::Cache(VertexArrayKind::Border),
+ "cs_border_segment",
+ &[],
+ device,
+ options.precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ let cs_border_solid = LazilyCompiledShader::new(
+ ShaderKind::Cache(VertexArrayKind::Border),
+ "cs_border_solid",
+ &[],
+ device,
+ options.precache_flags,
+ &shader_list,
+ profile,
+ )?;
+
+ let composite = CompositorShaders::new(device, options.precache_flags, gl_type)?;
+
+ Ok(Shaders {
+ cs_blur_a8,
+ cs_blur_rgba8,
+ cs_border_segment,
+ cs_line_decoration,
+ cs_fast_linear_gradient,
+ cs_linear_gradient,
+ cs_radial_gradient,
+ cs_conic_gradient,
+ cs_border_solid,
+ cs_scale,
+ cs_svg_filter,
+ brush_solid,
+ brush_image,
+ brush_fast_image,
+ brush_blend,
+ brush_mix_blend,
+ brush_yuv_image,
+ brush_linear_gradient,
+ brush_opacity,
+ brush_opacity_aa,
+ cs_clip_rectangle_slow,
+ cs_clip_rectangle_fast,
+ cs_clip_box_shadow,
+ cs_clip_image,
+ ps_text_run,
+ ps_text_run_dual_source,
+ ps_quad_textured,
+ ps_mask,
+ ps_mask_fast,
+ ps_split_composite,
+ ps_clear,
+ ps_copy,
+ composite,
+ })
+ }
+
+ fn get_compositing_shader_index(buffer_kind: ImageBufferKind) -> usize {
+ buffer_kind as usize
+ }
+
+ pub fn get_composite_shader(
+ &mut self,
+ format: CompositeSurfaceFormat,
+ buffer_kind: ImageBufferKind,
+ features: CompositeFeatures,
+ ) -> &mut LazilyCompiledShader {
+ self.composite.get(format, buffer_kind, features)
+ }
+
+ pub fn get_scale_shader(
+ &mut self,
+ buffer_kind: ImageBufferKind,
+ ) -> &mut LazilyCompiledShader {
+ let shader_index = Self::get_compositing_shader_index(buffer_kind);
+ self.cs_scale[shader_index]
+ .as_mut()
+ .expect("bug: unsupported scale shader requested")
+ }
+
+ pub fn get(&
+ mut self,
+ key: &BatchKey,
+ mut features: BatchFeatures,
+ debug_flags: DebugFlags,
+ device: &Device,
+ ) -> &mut LazilyCompiledShader {
+ match key.kind {
+ BatchKind::Primitive => {
+ &mut self.ps_quad_textured
+ }
+ BatchKind::SplitComposite => {
+ &mut self.ps_split_composite
+ }
+ BatchKind::Brush(brush_kind) => {
+ // SWGL uses a native anti-aliasing implementation that bypasses the shader.
+ // Don't consider it in that case when deciding whether or not to use
+ // an alpha-pass shader.
+ if device.get_capabilities().uses_native_antialiasing {
+ features.remove(BatchFeatures::ANTIALIASING);
+ }
+ let brush_shader = match brush_kind {
+ BrushBatchKind::Solid => {
+ &mut self.brush_solid
+ }
+ BrushBatchKind::Image(image_buffer_kind) => {
+ if features.contains(BatchFeatures::ANTIALIASING) ||
+ features.contains(BatchFeatures::REPETITION) {
+
+ self.brush_image[image_buffer_kind as usize]
+ .as_mut()
+ .expect("Unsupported image shader kind")
+ } else {
+ self.brush_fast_image[image_buffer_kind as usize]
+ .as_mut()
+ .expect("Unsupported image shader kind")
+ }
+ }
+ BrushBatchKind::Blend => {
+ &mut self.brush_blend
+ }
+ BrushBatchKind::MixBlend { .. } => {
+ &mut self.brush_mix_blend
+ }
+ BrushBatchKind::LinearGradient => {
+ // SWGL uses a native clip mask implementation that bypasses the shader.
+ // Don't consider it in that case when deciding whether or not to use
+ // an alpha-pass shader.
+ if device.get_capabilities().uses_native_clip_mask {
+ features.remove(BatchFeatures::CLIP_MASK);
+ }
+ // Gradient brushes can optimistically use the opaque shader even
+ // with a blend mode if they don't require any features.
+ if !features.intersects(
+ BatchFeatures::ANTIALIASING
+ | BatchFeatures::REPETITION
+ | BatchFeatures::CLIP_MASK,
+ ) {
+ features.remove(BatchFeatures::ALPHA_PASS);
+ }
+ match brush_kind {
+ BrushBatchKind::LinearGradient => &mut self.brush_linear_gradient,
+ _ => panic!(),
+ }
+ }
+ BrushBatchKind::YuvImage(image_buffer_kind, ..) => {
+ let shader_index =
+ Self::get_compositing_shader_index(image_buffer_kind);
+ self.brush_yuv_image[shader_index]
+ .as_mut()
+ .expect("Unsupported YUV shader kind")
+ }
+ BrushBatchKind::Opacity => {
+ if features.contains(BatchFeatures::ANTIALIASING) {
+ &mut self.brush_opacity_aa
+ } else {
+ &mut self.brush_opacity
+ }
+ }
+ };
+ brush_shader.get(key.blend_mode, features, debug_flags)
+ }
+ BatchKind::TextRun(glyph_format) => {
+ let text_shader = match key.blend_mode {
+ BlendMode::SubpixelDualSource => self.ps_text_run_dual_source.as_mut().unwrap(),
+ _ => &mut self.ps_text_run,
+ };
+ text_shader.get(glyph_format, debug_flags)
+ }
+ }
+ }
+
+ pub fn deinit(mut self, device: &mut Device) {
+ for shader in self.cs_scale {
+ if let Some(shader) = shader {
+ shader.deinit(device);
+ }
+ }
+ self.cs_blur_a8.deinit(device);
+ self.cs_blur_rgba8.deinit(device);
+ self.cs_svg_filter.deinit(device);
+ self.brush_solid.deinit(device);
+ self.brush_blend.deinit(device);
+ self.brush_mix_blend.deinit(device);
+ self.brush_linear_gradient.deinit(device);
+ self.brush_opacity.deinit(device);
+ self.brush_opacity_aa.deinit(device);
+ self.cs_clip_rectangle_slow.deinit(device);
+ self.cs_clip_rectangle_fast.deinit(device);
+ self.cs_clip_box_shadow.deinit(device);
+ self.cs_clip_image.deinit(device);
+ self.ps_text_run.deinit(device);
+ if let Some(shader) = self.ps_text_run_dual_source {
+ shader.deinit(device);
+ }
+ for shader in self.brush_image {
+ if let Some(shader) = shader {
+ shader.deinit(device);
+ }
+ }
+ for shader in self.brush_fast_image {
+ if let Some(shader) = shader {
+ shader.deinit(device);
+ }
+ }
+ for shader in self.brush_yuv_image {
+ if let Some(shader) = shader {
+ shader.deinit(device);
+ }
+ }
+ self.cs_border_solid.deinit(device);
+ self.cs_fast_linear_gradient.deinit(device);
+ self.cs_linear_gradient.deinit(device);
+ self.cs_radial_gradient.deinit(device);
+ self.cs_conic_gradient.deinit(device);
+ self.cs_line_decoration.deinit(device);
+ self.cs_border_segment.deinit(device);
+ self.ps_split_composite.deinit(device);
+ self.ps_quad_textured.deinit(device);
+ self.ps_mask.deinit(device);
+ self.ps_mask_fast.deinit(device);
+ self.ps_clear.deinit(device);
+ self.ps_copy.deinit(device);
+ self.composite.deinit(device);
+ }
+}
+
+pub type SharedShaders = Rc<RefCell<Shaders>>;
+
+pub struct CompositorShaders {
+ // Composite shaders. These are very simple shaders used to composite
+ // picture cache tiles into the framebuffer on platforms that do not have an
+ // OS Compositor (or we cannot use it). Such an OS Compositor (such as
+ // DirectComposite or CoreAnimation) handles the composition of the picture
+ // cache tiles at a lower level (e.g. in DWM for Windows); in that case we
+ // directly hand the picture cache surfaces over to the OS Compositor, and
+ // our own Composite shaders below never run.
+ // To composite external (RGB) surfaces we need various permutations of
+ // shaders with WR_FEATURE flags on or off based on the type of image
+ // buffer we're sourcing from (see IMAGE_BUFFER_KINDS).
+ rgba: Vec<Option<LazilyCompiledShader>>,
+ // A faster set of rgba composite shaders that do not support UV clamping
+ // or color modulation.
+ rgba_fast_path: Vec<Option<LazilyCompiledShader>>,
+ // The same set of composite shaders but with WR_FEATURE_YUV added.
+ yuv: Vec<Option<LazilyCompiledShader>>,
+}
+
+impl CompositorShaders {
+ pub fn new(
+ device: &mut Device,
+ precache_flags: ShaderPrecacheFlags,
+ gl_type: GlType,
+ ) -> Result<Self, ShaderError> {
+ // We have to pass a profile around a bunch but we aren't recording the initialization
+ // so use a dummy one.
+ let mut profile = TransactionProfile::new();
+
+ let mut yuv_features = Vec::new();
+ let mut rgba_features = Vec::new();
+ let mut fast_path_features = Vec::new();
+ let mut rgba = Vec::new();
+ let mut rgba_fast_path = Vec::new();
+ let mut yuv = Vec::new();
+
+ let texture_external_version = if device.get_capabilities().supports_image_external_essl3 {
+ TextureExternalVersion::ESSL3
+ } else {
+ TextureExternalVersion::ESSL1
+ };
+
+ let feature_flags = get_shader_feature_flags(gl_type, texture_external_version);
+ let shader_list = get_shader_features(feature_flags);
+
+ for _ in 0..IMAGE_BUFFER_KINDS.len() {
+ yuv.push(None);
+ rgba.push(None);
+ rgba_fast_path.push(None);
+ }
+
+ for image_buffer_kind in &IMAGE_BUFFER_KINDS {
+ if !has_platform_support(*image_buffer_kind, &gl_type) {
+ continue;
+ }
+
+ yuv_features.push("YUV");
+ fast_path_features.push("FAST_PATH");
+
+ let index = Self::get_shader_index(*image_buffer_kind);
+
+ let feature_string = get_feature_string(
+ *image_buffer_kind,
+ texture_external_version,
+ );
+ if feature_string != "" {
+ yuv_features.push(feature_string);
+ rgba_features.push(feature_string);
+ fast_path_features.push(feature_string);
+ }
+
+ // YUV shaders are not compatible with ESSL1
+ if *image_buffer_kind != ImageBufferKind::TextureExternal ||
+ texture_external_version == TextureExternalVersion::ESSL3 {
+
+ yuv[index] = Some(LazilyCompiledShader::new(
+ ShaderKind::Composite,
+ "composite",
+ &yuv_features,
+ device,
+ precache_flags,
+ &shader_list,
+ &mut profile,
+ )?);
+ }
+
+ rgba[index] = Some(LazilyCompiledShader::new(
+ ShaderKind::Composite,
+ "composite",
+ &rgba_features,
+ device,
+ precache_flags,
+ &shader_list,
+ &mut profile,
+ )?);
+
+ rgba_fast_path[index] = Some(LazilyCompiledShader::new(
+ ShaderKind::Composite,
+ "composite",
+ &fast_path_features,
+ device,
+ precache_flags,
+ &shader_list,
+ &mut profile,
+ )?);
+
+ yuv_features.clear();
+ rgba_features.clear();
+ fast_path_features.clear();
+ }
+
+ Ok(CompositorShaders {
+ rgba,
+ rgba_fast_path,
+ yuv,
+ })
+ }
+
+ pub fn get(
+ &mut self,
+ format: CompositeSurfaceFormat,
+ buffer_kind: ImageBufferKind,
+ features: CompositeFeatures,
+ ) -> &mut LazilyCompiledShader {
+ match format {
+ CompositeSurfaceFormat::Rgba => {
+ if features.contains(CompositeFeatures::NO_UV_CLAMP)
+ && features.contains(CompositeFeatures::NO_COLOR_MODULATION)
+ {
+ let shader_index = Self::get_shader_index(buffer_kind);
+ self.rgba_fast_path[shader_index]
+ .as_mut()
+ .expect("bug: unsupported rgba fast path shader requested")
+ } else {
+ let shader_index = Self::get_shader_index(buffer_kind);
+ self.rgba[shader_index]
+ .as_mut()
+ .expect("bug: unsupported rgba shader requested")
+ }
+ }
+ CompositeSurfaceFormat::Yuv => {
+ let shader_index = Self::get_shader_index(buffer_kind);
+ self.yuv[shader_index]
+ .as_mut()
+ .expect("bug: unsupported yuv shader requested")
+ }
+ }
+ }
+
+ fn get_shader_index(buffer_kind: ImageBufferKind) -> usize {
+ buffer_kind as usize
+ }
+
+ pub fn deinit(&mut self, device: &mut Device) {
+ for shader in self.rgba.drain(..) {
+ if let Some(shader) = shader {
+ shader.deinit(device);
+ }
+ }
+ for shader in self.rgba_fast_path.drain(..) {
+ if let Some(shader) = shader {
+ shader.deinit(device);
+ }
+ }
+ for shader in self.yuv.drain(..) {
+ if let Some(shader) = shader {
+ shader.deinit(device);
+ }
+ }
+ }
+}
+
+fn get_shader_feature_flags(gl_type: GlType, texture_external_version: TextureExternalVersion) -> ShaderFeatureFlags {
+ match gl_type {
+ GlType::Gl => ShaderFeatureFlags::GL,
+ GlType::Gles => {
+ let texture_external_flag = match texture_external_version {
+ TextureExternalVersion::ESSL3 => ShaderFeatureFlags::TEXTURE_EXTERNAL,
+ TextureExternalVersion::ESSL1 => ShaderFeatureFlags::TEXTURE_EXTERNAL_ESSL1,
+ };
+ ShaderFeatureFlags::GLES | texture_external_flag
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/renderer/upload.rs b/gfx/wr/webrender/src/renderer/upload.rs
new file mode 100644
index 0000000000..0ba053cd76
--- /dev/null
+++ b/gfx/wr/webrender/src/renderer/upload.rs
@@ -0,0 +1,847 @@
+/* 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/. */
+
+//! This module contains the convoluted logic that goes into uploading content into
+//! the texture cache's textures.
+//!
+//! We need to support various combinations of code paths depending on the quirks of
+//! each hardware/driver configuration:
+//! - direct upload,
+//! - staged upload via a pixel buffer object,
+//! - staged upload via a direct upload to a staging texture where PBO's aren't supported,
+//! - copy from the staging to destination textures, either via blits or batched draw calls.
+//!
+//! Conceptually a lot of this logic should probably be in the device module, but some code
+//! here relies on submitting draw calls via the renderer.
+
+
+use std::mem;
+use std::collections::VecDeque;
+use std::sync::Arc;
+use std::time::Duration;
+use euclid::{Transform3D, point2};
+use time::precise_time_ns;
+use malloc_size_of::MallocSizeOfOps;
+use api::units::*;
+use api::{ExternalImageSource, ImageBufferKind, ImageFormat};
+use crate::renderer::{
+ Renderer, VertexArrayKind, RendererStats, TextureSampler, TEXTURE_CACHE_DBG_CLEAR_COLOR
+};
+use crate::internal_types::{
+ FastHashMap, TextureUpdateSource, Swizzle, TextureCacheUpdate,
+ CacheTextureId, RenderTargetInfo,
+};
+use crate::device::{
+ Device, UploadMethod, Texture, DrawTarget, UploadStagingBuffer, TextureFlags, TextureUploader,
+ TextureFilter,
+};
+use crate::gpu_types::CopyInstance;
+use crate::batch::BatchTextures;
+use crate::texture_pack::{GuillotineAllocator, FreeRectSlice};
+use crate::profiler;
+use crate::render_api::MemoryReport;
+
+pub const BATCH_UPLOAD_TEXTURE_SIZE: DeviceIntSize = DeviceIntSize::new(512, 512);
+
+/// Upload a number of items to texture cache textures.
+///
+/// This is the main entry point of the texture cache upload code.
+/// See also the module documentation for more information.
+pub fn upload_to_texture_cache(
+ renderer: &mut Renderer,
+ update_list: FastHashMap<CacheTextureId, Vec<TextureCacheUpdate>>,
+) {
+
+ let mut stats = UploadStats {
+ num_draw_calls: 0,
+ upload_time: 0,
+ cpu_buffer_alloc_time: 0,
+ texture_alloc_time: 0,
+ cpu_copy_time: 0,
+ gpu_copy_commands_time: 0,
+ bytes_uploaded: 0,
+ items_uploaded: 0,
+ };
+
+ let upload_total_start = precise_time_ns();
+
+ let mut batch_upload_textures = Vec::new();
+
+ // A list of copies that must be performed from the temporary textures to the texture cache.
+ let mut batch_upload_copies = Vec::new();
+
+ // For each texture format, this stores a list of staging buffers
+ // and a texture allocator for packing the buffers.
+ let mut batch_upload_buffers = FastHashMap::default();
+
+ // For best performance we use a single TextureUploader for all uploads.
+ // This allows us to fill PBOs more efficiently and therefore allocate fewer PBOs.
+ let mut uploader = renderer.device.upload_texture(
+ &mut renderer.texture_upload_pbo_pool,
+ );
+
+ let num_updates = update_list.len();
+
+ for (texture_id, updates) in update_list {
+ let texture = &renderer.texture_resolver.texture_cache_map[&texture_id].texture;
+ for update in updates {
+ let TextureCacheUpdate { rect, stride, offset, format_override, source } = update;
+ let mut arc_data = None;
+ let dummy_data;
+ let data = match source {
+ TextureUpdateSource::Bytes { ref data } => {
+ arc_data = Some(data.clone());
+ &data[offset as usize ..]
+ }
+ TextureUpdateSource::External { id, channel_index } => {
+ let handler = renderer.external_image_handler
+ .as_mut()
+ .expect("Found external image, but no handler set!");
+ // The filter is only relevant for NativeTexture external images.
+ match handler.lock(id, channel_index).source {
+ ExternalImageSource::RawData(data) => {
+ &data[offset as usize ..]
+ }
+ ExternalImageSource::Invalid => {
+ // Create a local buffer to fill the pbo.
+ let bpp = texture.get_format().bytes_per_pixel();
+ let width = stride.unwrap_or(rect.width() * bpp);
+ let total_size = width * rect.height();
+ // WR haven't support RGBAF32 format in texture_cache, so
+ // we use u8 type here.
+ dummy_data = vec![0xFFu8; total_size as usize];
+ &dummy_data
+ }
+ ExternalImageSource::NativeTexture(eid) => {
+ panic!("Unexpected external texture {:?} for the texture cache update of {:?}", eid, id);
+ }
+ }
+ }
+ TextureUpdateSource::DebugClear => {
+ let draw_target = DrawTarget::from_texture(
+ texture,
+ false,
+ );
+ renderer.device.bind_draw_target(draw_target);
+ renderer.device.clear_target(
+ Some(TEXTURE_CACHE_DBG_CLEAR_COLOR),
+ None,
+ Some(draw_target.to_framebuffer_rect(update.rect.to_i32()))
+ );
+
+ continue;
+ }
+ };
+
+ stats.items_uploaded += 1;
+
+ let use_batch_upload = renderer.device.use_batched_texture_uploads() &&
+ texture.flags().contains(TextureFlags::IS_SHARED_TEXTURE_CACHE) &&
+ rect.width() <= BATCH_UPLOAD_TEXTURE_SIZE.width &&
+ rect.height() <= BATCH_UPLOAD_TEXTURE_SIZE.height &&
+ rect.area() < renderer.device.batched_upload_threshold();
+
+ if use_batch_upload
+ && arc_data.is_some()
+ && matches!(renderer.device.upload_method(), &UploadMethod::Immediate)
+ && rect.area() > BATCH_UPLOAD_TEXTURE_SIZE.area() / 2 {
+ skip_staging_buffer(
+ &mut renderer.device,
+ &mut renderer.staging_texture_pool,
+ rect,
+ stride,
+ arc_data.unwrap(),
+ texture_id,
+ texture,
+ &mut batch_upload_buffers,
+ &mut batch_upload_textures,
+ &mut batch_upload_copies,
+ &mut stats,
+ );
+ } else if use_batch_upload {
+ copy_into_staging_buffer(
+ &mut renderer.device,
+ &mut uploader,
+ &mut renderer.staging_texture_pool,
+ rect,
+ stride,
+ data,
+ texture_id,
+ texture,
+ &mut batch_upload_buffers,
+ &mut batch_upload_textures,
+ &mut batch_upload_copies,
+ &mut stats,
+ );
+ } else {
+ let upload_start_time = precise_time_ns();
+
+ stats.bytes_uploaded += uploader.upload(
+ &mut renderer.device,
+ texture,
+ rect,
+ stride,
+ format_override,
+ data.as_ptr(),
+ data.len()
+ );
+
+ stats.upload_time += precise_time_ns() - upload_start_time;
+ }
+
+ if let TextureUpdateSource::External { id, channel_index } = source {
+ let handler = renderer.external_image_handler
+ .as_mut()
+ .expect("Found external image, but no handler set!");
+ handler.unlock(id, channel_index);
+ }
+ }
+ }
+
+ let upload_start_time = precise_time_ns();
+ // Upload batched texture updates to their temporary textures.
+ for batch_buffer in batch_upload_buffers.into_iter().map(|(_, (_, buffers))| buffers).flatten() {
+ let texture = &batch_upload_textures[batch_buffer.texture_index];
+ match batch_buffer.staging_buffer {
+ StagingBufferKind::Pbo(pbo) => {
+ stats.bytes_uploaded += uploader.upload_staged(
+ &mut renderer.device,
+ texture,
+ DeviceIntRect::from_size(texture.get_dimensions()),
+ None,
+ pbo,
+ );
+ }
+ StagingBufferKind::CpuBuffer { bytes, .. } => {
+ let bpp = texture.get_format().bytes_per_pixel();
+ stats.bytes_uploaded += uploader.upload(
+ &mut renderer.device,
+ texture,
+ batch_buffer.upload_rect,
+ Some(BATCH_UPLOAD_TEXTURE_SIZE.width * bpp),
+ None,
+ bytes.as_ptr(),
+ bytes.len()
+ );
+ renderer.staging_texture_pool.return_temporary_buffer(bytes);
+ }
+ StagingBufferKind::Image { bytes, stride } => {
+ stats.bytes_uploaded += uploader.upload(
+ &mut renderer.device,
+ texture,
+ batch_buffer.upload_rect,
+ stride,
+ None,
+ bytes.as_ptr(),
+ bytes.len()
+ );
+ }
+ }
+ }
+ stats.upload_time += precise_time_ns() - upload_start_time;
+
+
+ // Flush all uploads, batched or otherwise.
+ let flush_start_time = precise_time_ns();
+ uploader.flush(&mut renderer.device);
+ stats.upload_time += precise_time_ns() - flush_start_time;
+
+ if !batch_upload_copies.is_empty() {
+ // Copy updates that were batch uploaded to their correct destination in the texture cache.
+ // Sort them by destination and source to minimize framebuffer binding changes.
+ batch_upload_copies.sort_unstable_by_key(|b| (b.dest_texture_id.0, b.src_texture_index));
+
+ let gpu_copy_start = precise_time_ns();
+
+ if renderer.device.use_draw_calls_for_texture_copy() {
+ // Some drivers have a very high CPU overhead when submitting hundreds of small blit
+ // commands (low end intel drivers on Windows for example can take take 100+ ms submitting a
+ // few hundred blits). In this case we do the copy with batched draw calls.
+ copy_from_staging_to_cache_using_draw_calls(
+ renderer,
+ &mut stats,
+ &batch_upload_textures,
+ batch_upload_copies,
+ );
+ } else {
+ copy_from_staging_to_cache(
+ renderer,
+ &batch_upload_textures,
+ batch_upload_copies,
+ );
+ }
+
+ stats.gpu_copy_commands_time += precise_time_ns() - gpu_copy_start;
+ }
+
+ for texture in batch_upload_textures.drain(..) {
+ renderer.staging_texture_pool.return_texture(texture);
+ }
+
+ // Update the profile counters. We use add instead of set because
+ // this function can be called several times per frame.
+ // We don't update the counters when their value is zero, so that
+ // the profiler can treat them as events and we can get notified
+ // when they happen.
+
+ let upload_total = precise_time_ns() - upload_total_start;
+ renderer.profile.add(
+ profiler::TOTAL_UPLOAD_TIME,
+ profiler::ns_to_ms(upload_total)
+ );
+
+ if num_updates > 0 {
+ renderer.profile.add(profiler::TEXTURE_UPLOADS, num_updates);
+ }
+
+ if stats.bytes_uploaded > 0 {
+ renderer.profile.add(
+ profiler::TEXTURE_UPLOADS_MEM,
+ profiler::bytes_to_mb(stats.bytes_uploaded)
+ );
+ }
+
+ if stats.cpu_copy_time > 0 {
+ renderer.profile.add(
+ profiler::UPLOAD_CPU_COPY_TIME,
+ profiler::ns_to_ms(stats.cpu_copy_time)
+ );
+ }
+ if stats.upload_time > 0 {
+ renderer.profile.add(
+ profiler::UPLOAD_TIME,
+ profiler::ns_to_ms(stats.upload_time)
+ );
+ }
+ if stats.texture_alloc_time > 0 {
+ renderer.profile.add(
+ profiler::STAGING_TEXTURE_ALLOCATION_TIME,
+ profiler::ns_to_ms(stats.texture_alloc_time)
+ );
+ }
+ if stats.cpu_buffer_alloc_time > 0 {
+ renderer.profile.add(
+ profiler::CPU_TEXTURE_ALLOCATION_TIME,
+ profiler::ns_to_ms(stats.cpu_buffer_alloc_time)
+ );
+ }
+ if stats.num_draw_calls > 0{
+ renderer.profile.add(
+ profiler::UPLOAD_NUM_COPY_BATCHES,
+ stats.num_draw_calls
+ );
+ }
+
+ if stats.gpu_copy_commands_time > 0 {
+ renderer.profile.add(
+ profiler::UPLOAD_GPU_COPY_TIME,
+ profiler::ns_to_ms(stats.gpu_copy_commands_time)
+ );
+ }
+
+ let add_markers = profiler::thread_is_being_profiled();
+ if add_markers && stats.bytes_uploaded > 0 {
+ let details = format!("{} bytes uploaded, {} items", stats.bytes_uploaded, stats.items_uploaded);
+ profiler::add_text_marker(&"Texture uploads", &details, Duration::from_nanos(upload_total));
+ }
+}
+
+/// Copy an item into a batched upload staging buffer.
+fn copy_into_staging_buffer<'a>(
+ device: &mut Device,
+ uploader: &mut TextureUploader< 'a>,
+ staging_texture_pool: &mut UploadTexturePool,
+ update_rect: DeviceIntRect,
+ update_stride: Option<i32>,
+ data: &[u8],
+ dest_texture_id: CacheTextureId,
+ texture: &Texture,
+ batch_upload_buffers: &mut FastHashMap<ImageFormat, (GuillotineAllocator, Vec<BatchUploadBuffer<'a>>)>,
+ batch_upload_textures: &mut Vec<Texture>,
+ batch_upload_copies: &mut Vec<BatchUploadCopy>,
+ stats: &mut UploadStats
+) {
+ let (allocator, buffers) = batch_upload_buffers.entry(texture.get_format())
+ .or_insert_with(|| (GuillotineAllocator::new(None), Vec::new()));
+
+ // Allocate a region within the staging buffer for this update. If there is
+ // no room in an existing buffer then allocate another texture and buffer.
+ let (slice, origin) = match allocator.allocate(&update_rect.size()) {
+ Some((slice, origin)) => (slice, origin),
+ None => {
+ let new_slice = FreeRectSlice(buffers.len() as u32);
+ allocator.extend(new_slice, BATCH_UPLOAD_TEXTURE_SIZE, update_rect.size());
+
+ let texture_alloc_time_start = precise_time_ns();
+ let staging_texture = staging_texture_pool.get_texture(device, texture.get_format());
+ stats.texture_alloc_time = precise_time_ns() - texture_alloc_time_start;
+
+ let texture_index = batch_upload_textures.len();
+ batch_upload_textures.push(staging_texture);
+
+ let cpu_buffer_alloc_start_time = precise_time_ns();
+ let staging_buffer = match device.upload_method() {
+ UploadMethod::Immediate => StagingBufferKind::CpuBuffer {
+ bytes: staging_texture_pool.get_temporary_buffer(),
+ },
+ UploadMethod::PixelBuffer(_) => {
+ let pbo = uploader.stage(
+ device,
+ texture.get_format(),
+ BATCH_UPLOAD_TEXTURE_SIZE,
+ ).unwrap();
+
+ StagingBufferKind::Pbo(pbo)
+ }
+ };
+ stats.cpu_buffer_alloc_time += precise_time_ns() - cpu_buffer_alloc_start_time;
+
+ buffers.push(BatchUploadBuffer {
+ staging_buffer,
+ texture_index,
+ upload_rect: DeviceIntRect::zero()
+ });
+
+ (new_slice, DeviceIntPoint::zero())
+ }
+ };
+ let buffer = &mut buffers[slice.0 as usize];
+ let allocated_rect = DeviceIntRect::from_origin_and_size(origin, update_rect.size());
+ buffer.upload_rect = buffer.upload_rect.union(&allocated_rect);
+
+ batch_upload_copies.push(BatchUploadCopy {
+ src_texture_index: buffer.texture_index,
+ src_offset: allocated_rect.min,
+ dest_texture_id,
+ dest_offset: update_rect.min,
+ size: update_rect.size(),
+ });
+
+ unsafe {
+ let memcpy_start_time = precise_time_ns();
+ let bpp = texture.get_format().bytes_per_pixel() as usize;
+ let width_bytes = update_rect.width() as usize * bpp;
+ let src_stride = update_stride.map_or(width_bytes, |stride| {
+ assert!(stride >= 0);
+ stride as usize
+ });
+ let src_size = (update_rect.height() as usize - 1) * src_stride + width_bytes;
+ assert!(src_size <= data.len());
+
+ let src: &[mem::MaybeUninit<u8>] = std::slice::from_raw_parts(data.as_ptr() as *const _, src_size);
+ let (dst_stride, dst) = match &mut buffer.staging_buffer {
+ StagingBufferKind::Pbo(buffer) => (
+ buffer.get_stride(),
+ buffer.get_mapping(),
+ ),
+ StagingBufferKind::CpuBuffer { bytes } => (
+ BATCH_UPLOAD_TEXTURE_SIZE.width as usize * bpp,
+ &mut bytes[..],
+ ),
+ StagingBufferKind::Image { .. } => unreachable!(),
+ };
+
+ // copy the data line-by-line in to the buffer so that we do not overwrite
+ // any other region of the buffer.
+ for y in 0..allocated_rect.height() as usize {
+ let src_start = y * src_stride;
+ let src_end = src_start + width_bytes;
+ let dst_start = (allocated_rect.min.y as usize + y as usize) * dst_stride +
+ allocated_rect.min.x as usize * bpp;
+ let dst_end = dst_start + width_bytes;
+
+ dst[dst_start..dst_end].copy_from_slice(&src[src_start..src_end])
+ }
+
+ stats.cpu_copy_time += precise_time_ns() - memcpy_start_time;
+ }
+}
+
+/// Take this code path instead of copying into a staging CPU buffer when the image
+/// we would copy is large enough that it's unlikely anything else would fit in the
+/// buffer, therefore we might as well copy directly from the source image's pixels.
+fn skip_staging_buffer<'a>(
+ device: &mut Device,
+ staging_texture_pool: &mut UploadTexturePool,
+ update_rect: DeviceIntRect,
+ stride: Option<i32>,
+ data: Arc<Vec<u8>>,
+ dest_texture_id: CacheTextureId,
+ texture: &Texture,
+ batch_upload_buffers: &mut FastHashMap<ImageFormat, (GuillotineAllocator, Vec<BatchUploadBuffer<'a>>)>,
+ batch_upload_textures: &mut Vec<Texture>,
+ batch_upload_copies: &mut Vec<BatchUploadCopy>,
+ stats: &mut UploadStats
+) {
+ let (_, buffers) = batch_upload_buffers.entry(texture.get_format())
+ .or_insert_with(|| (GuillotineAllocator::new(None), Vec::new()));
+
+ let texture_alloc_time_start = precise_time_ns();
+ let staging_texture = staging_texture_pool.get_texture(device, texture.get_format());
+ stats.texture_alloc_time = precise_time_ns() - texture_alloc_time_start;
+
+ let texture_index = batch_upload_textures.len();
+ batch_upload_textures.push(staging_texture);
+
+ buffers.push(BatchUploadBuffer {
+ staging_buffer: StagingBufferKind::Image { bytes: data, stride },
+ texture_index,
+ upload_rect: DeviceIntRect::from_size(update_rect.size())
+ });
+
+ batch_upload_copies.push(BatchUploadCopy {
+ src_texture_index: texture_index,
+ src_offset: point2(0, 0),
+ dest_texture_id,
+ dest_offset: update_rect.min,
+ size: update_rect.size(),
+ });
+}
+
+
+/// Copy from the staging PBOs or textures to texture cache textures using blit commands.
+///
+/// Using blits instead of draw calls is supposedly more efficient but some drivers have
+/// a very high per-command overhead so in some configurations we end up using
+/// copy_from_staging_to_cache_using_draw_calls instead.
+fn copy_from_staging_to_cache(
+ renderer: &mut Renderer,
+ batch_upload_textures: &[Texture],
+ batch_upload_copies: Vec<BatchUploadCopy>,
+) {
+ for copy in batch_upload_copies {
+ let dest_texture = &renderer.texture_resolver.texture_cache_map[&copy.dest_texture_id].texture;
+
+ renderer.device.copy_texture_sub_region(
+ &batch_upload_textures[copy.src_texture_index],
+ copy.src_offset.x as _,
+ copy.src_offset.y as _,
+ dest_texture,
+ copy.dest_offset.x as _,
+ copy.dest_offset.y as _,
+ copy.size.width as _,
+ copy.size.height as _,
+ );
+ }
+}
+
+/// Generate and submit composite shader batches to copy from
+/// the staging textures to the destination cache textures.
+///
+/// If this shows up in GPU time ptofiles we could replace it with
+/// a simpler shader (composite.glsl is already quite simple).
+fn copy_from_staging_to_cache_using_draw_calls(
+ renderer: &mut Renderer,
+ stats: &mut UploadStats,
+ batch_upload_textures: &[Texture],
+ batch_upload_copies: Vec<BatchUploadCopy>,
+) {
+ let mut copy_instances = Vec::new();
+ let mut prev_src = None;
+ let mut prev_dst = None;
+ let mut dst_texture_size = DeviceSize::new(0.0, 0.0);
+
+ for copy in batch_upload_copies {
+
+ let src_changed = prev_src != Some(copy.src_texture_index);
+ let dst_changed = prev_dst != Some(copy.dest_texture_id);
+
+ if (src_changed || dst_changed) && !copy_instances.is_empty() {
+ renderer.draw_instanced_batch(
+ &copy_instances,
+ VertexArrayKind::Copy,
+ // We bind the staging texture manually because it isn't known
+ // to the texture resolver.
+ &BatchTextures::empty(),
+ &mut RendererStats::default(),
+ );
+
+ stats.num_draw_calls += 1;
+ copy_instances.clear();
+ }
+
+ if dst_changed {
+ let dest_texture = &renderer.texture_resolver.texture_cache_map[&copy.dest_texture_id].texture;
+ dst_texture_size = dest_texture.get_dimensions().to_f32();
+
+ let draw_target = DrawTarget::from_texture(dest_texture, false);
+ renderer.device.bind_draw_target(draw_target);
+
+ renderer.shaders
+ .borrow_mut()
+ .ps_copy
+ .bind(
+ &mut renderer.device,
+ &Transform3D::identity(),
+ None,
+ &mut renderer.renderer_errors,
+ &mut renderer.profile,
+ );
+
+ prev_dst = Some(copy.dest_texture_id);
+ }
+
+ if src_changed {
+ renderer.device.bind_texture(
+ TextureSampler::Color0,
+ &batch_upload_textures[copy.src_texture_index],
+ Swizzle::default(),
+ );
+
+ prev_src = Some(copy.src_texture_index)
+ }
+
+ let src_rect = DeviceRect::from_origin_and_size(
+ copy.src_offset.to_f32(),
+ copy.size.to_f32(),
+ );
+
+ let dst_rect = DeviceRect::from_origin_and_size(
+ copy.dest_offset.to_f32(),
+ copy.size.to_f32(),
+ );
+
+ copy_instances.push(CopyInstance {
+ src_rect,
+ dst_rect,
+ dst_texture_size,
+ });
+ }
+
+ if !copy_instances.is_empty() {
+ renderer.draw_instanced_batch(
+ &copy_instances,
+ VertexArrayKind::Copy,
+ &BatchTextures::empty(),
+ &mut RendererStats::default(),
+ );
+
+ stats.num_draw_calls += 1;
+ }
+}
+
+/// A very basic pool to avoid reallocating staging textures as well as staging
+/// CPU side buffers.
+pub struct UploadTexturePool {
+ /// The textures in the pool associated with a last used frame index.
+ ///
+ /// The outer array corresponds to each of teh three supported texture formats.
+ textures: [VecDeque<(Texture, u64)>; 3],
+ // Frame at which to deallocate some textures if there are too many in the pool,
+ // for each format.
+ delay_texture_deallocation: [u64; 3],
+ current_frame: u64,
+
+ /// Temporary buffers that are used when using staging uploads + glTexImage2D.
+ ///
+ /// Temporary buffers aren't used asynchronously so they can be reused every frame.
+ /// To keep things simple we always allocate enough memory for formats with four bytes
+ /// per pixel (more than we need for alpha-only textures but it works just as well).
+ temporary_buffers: Vec<Vec<mem::MaybeUninit<u8>>>,
+ min_temporary_buffers: usize,
+ delay_buffer_deallocation: u64,
+}
+
+impl UploadTexturePool {
+ pub fn new() -> Self {
+ UploadTexturePool {
+ textures: [VecDeque::new(), VecDeque::new(), VecDeque::new()],
+ delay_texture_deallocation: [0; 3],
+ current_frame: 0,
+ temporary_buffers: Vec::new(),
+ min_temporary_buffers: 0,
+ delay_buffer_deallocation: 0,
+ }
+ }
+
+ fn format_index(&self, format: ImageFormat) -> usize {
+ match format {
+ ImageFormat::RGBA8 => 0,
+ ImageFormat::BGRA8 => 1,
+ ImageFormat::R8 => 2,
+ _ => { panic!("unexpected format"); }
+ }
+ }
+
+ pub fn begin_frame(&mut self) {
+ self.current_frame += 1;
+ self.min_temporary_buffers = self.temporary_buffers.len();
+ }
+
+ /// Create or reuse a staging texture.
+ ///
+ /// See also return_texture.
+ pub fn get_texture(&mut self, device: &mut Device, format: ImageFormat) -> Texture {
+
+ // First try to reuse a texture from the pool.
+ // "available" here means hasn't been used for 2 frames to avoid stalls.
+ // No need to scan the vector. Newer textures are always pushed at the back
+ // of the vector so we know the first element is the least recently used.
+ let format_idx = self.format_index(format);
+ let can_reuse = self.textures[format_idx].get(0)
+ .map(|tex| self.current_frame - tex.1 > 2)
+ .unwrap_or(false);
+
+ if can_reuse {
+ return self.textures[format_idx].pop_front().unwrap().0;
+ }
+
+ // If we couldn't find an available texture, create a new one.
+
+ device.create_texture(
+ ImageBufferKind::Texture2D,
+ format,
+ BATCH_UPLOAD_TEXTURE_SIZE.width,
+ BATCH_UPLOAD_TEXTURE_SIZE.height,
+ TextureFilter::Nearest,
+ // Currently we need render target support as we always use glBlitFramebuffer
+ // to copy the texture data. Instead, we should use glCopyImageSubData on some
+ // platforms, and avoid creating the FBOs in that case.
+ Some(RenderTargetInfo { has_depth: false }),
+ )
+ }
+
+ /// Hand the staging texture back to the pool after being done with uploads.
+ ///
+ /// The texture must have been obtained from this pool via get_texture.
+ pub fn return_texture(&mut self, texture: Texture) {
+ let format_idx = self.format_index(texture.get_format());
+ self.textures[format_idx].push_back((texture, self.current_frame));
+ }
+
+ /// Create or reuse a temporary CPU buffer.
+ ///
+ /// These buffers are used in the batched upload path when PBOs are not supported.
+ /// Content is first written to the temporary buffer and uploaded via a single
+ /// glTexSubImage2D call.
+ pub fn get_temporary_buffer(&mut self) -> Vec<mem::MaybeUninit<u8>> {
+ let buffer = self.temporary_buffers.pop().unwrap_or_else(|| {
+ vec![mem::MaybeUninit::new(0); BATCH_UPLOAD_TEXTURE_SIZE.area() as usize * 4]
+ });
+ self.min_temporary_buffers = self.min_temporary_buffers.min(self.temporary_buffers.len());
+ buffer
+ }
+
+ /// Return memory that was obtained from this pool via get_temporary_buffer.
+ pub fn return_temporary_buffer(&mut self, buffer: Vec<mem::MaybeUninit<u8>>) {
+ assert_eq!(buffer.len(), BATCH_UPLOAD_TEXTURE_SIZE.area() as usize * 4);
+ self.temporary_buffers.push(buffer);
+ }
+
+ /// Deallocate this pool's CPU and GPU memory.
+ pub fn delete_textures(&mut self, device: &mut Device) {
+ for format in &mut self.textures {
+ while let Some(texture) = format.pop_back() {
+ device.delete_texture(texture.0)
+ }
+ }
+ self.temporary_buffers.clear();
+ }
+
+ /// Deallocate some textures if there are too many for a long time.
+ pub fn end_frame(&mut self, device: &mut Device) {
+ for format_idx in 0..self.textures.len() {
+ // Count the number of reusable staging textures.
+ // if it stays high for a large number of frames, truncate it back to 8-ish
+ // over multiple frames.
+
+ let mut num_reusable_textures = 0;
+ for texture in &self.textures[format_idx] {
+ if self.current_frame - texture.1 > 2 {
+ num_reusable_textures += 1;
+ }
+ }
+
+ if num_reusable_textures < 8 {
+ // Don't deallocate textures for another 120 frames.
+ self.delay_texture_deallocation[format_idx] = self.current_frame + 120;
+ }
+
+ // Deallocate up to 4 staging textures every frame.
+ let to_remove = if self.current_frame > self.delay_texture_deallocation[format_idx] {
+ num_reusable_textures.min(4)
+ } else {
+ 0
+ };
+
+ for _ in 0..to_remove {
+ let texture = self.textures[format_idx].pop_front().unwrap().0;
+ device.delete_texture(texture);
+ }
+ }
+
+ // Similar logic for temporary CPU buffers. Our calls to get and return
+ // temporary buffers should have been balanced for this frame, but the call
+ // get_temporary_buffer will allocate a buffer if the vec is empty. Since we
+ // carry these buffers from frame to frame, we keep track of the smallest
+ // length of the temporary_buffers vec that we encountered this frame. Those
+ // buffers were not touched and we deallocate some if there are a lot of them.
+ let unused_buffers = self.min_temporary_buffers;
+ if unused_buffers < 8 {
+ self.delay_buffer_deallocation = self.current_frame + 120;
+ }
+ let to_remove = if self.current_frame > self.delay_buffer_deallocation {
+ unused_buffers.min(4)
+ } else {
+ 0
+ };
+ for _ in 0..to_remove {
+ // Unlike textures it doesn't matter whether we pop from the front or back
+ // of the vector.
+ self.temporary_buffers.pop();
+ }
+ }
+
+ pub fn report_memory_to(&self, report: &mut MemoryReport, size_op_funs: &MallocSizeOfOps) {
+ for buf in &self.temporary_buffers {
+ report.upload_staging_memory += unsafe { (size_op_funs.size_of_op)(buf.as_ptr() as *const _) };
+ }
+
+ for format in &self.textures {
+ for texture in format {
+ report.upload_staging_textures += texture.0.size_in_bytes();
+ }
+ }
+ }
+}
+
+struct UploadStats {
+ num_draw_calls: u32,
+ upload_time: u64,
+ cpu_buffer_alloc_time: u64,
+ texture_alloc_time: u64,
+ cpu_copy_time: u64,
+ gpu_copy_commands_time: u64,
+ bytes_uploaded: usize,
+ items_uploaded: usize,
+}
+
+#[derive(Debug)]
+enum StagingBufferKind<'a> {
+ Pbo(UploadStagingBuffer<'a>),
+ CpuBuffer { bytes: Vec<mem::MaybeUninit<u8>> },
+ Image { bytes: Arc<Vec<u8>>, stride: Option<i32> },
+}
+#[derive(Debug)]
+struct BatchUploadBuffer<'a> {
+ staging_buffer: StagingBufferKind<'a>,
+ texture_index: usize,
+ // A rectangle containing all items going into this staging texture, so
+ // that we can avoid uploading the entire area if we are using glTexSubImage2d.
+ upload_rect: DeviceIntRect,
+}
+
+// On some devices performing many small texture uploads is slow, so instead we batch
+// updates in to a small number of uploads to temporary textures, then copy from those
+// textures to the correct place in the texture cache.
+// A list of temporary textures that batches of updates are uploaded to.
+#[derive(Debug)]
+struct BatchUploadCopy {
+ // Index within batch_upload_textures
+ src_texture_index: usize,
+ src_offset: DeviceIntPoint,
+ dest_texture_id: CacheTextureId,
+ dest_offset: DeviceIntPoint,
+ size: DeviceIntSize,
+}
diff --git a/gfx/wr/webrender/src/renderer/vertex.rs b/gfx/wr/webrender/src/renderer/vertex.rs
new file mode 100644
index 0000000000..ff555363d8
--- /dev/null
+++ b/gfx/wr/webrender/src/renderer/vertex.rs
@@ -0,0 +1,1154 @@
+/* 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/. */
+
+//! Rendering logic related to the vertex shaders and their states, uncluding
+//! - Vertex Array Objects
+//! - vertex layout descriptors
+//! - textures bound at vertex stage
+
+use std::{marker::PhantomData, mem, num::NonZeroUsize, ops};
+use api::units::*;
+use crate::{
+ device::{
+ Device, Texture, TextureFilter, TextureUploader, UploadPBOPool, VertexUsageHint, VAO,
+ },
+ frame_builder::Frame,
+ gpu_types::{PrimitiveHeaderI, PrimitiveHeaderF, TransformData},
+ internal_types::Swizzle,
+ render_task::RenderTaskData,
+};
+
+pub const VERTEX_TEXTURE_EXTRA_ROWS: i32 = 10;
+
+pub const MAX_VERTEX_TEXTURE_WIDTH: usize = webrender_build::MAX_VERTEX_TEXTURE_WIDTH;
+
+pub mod desc {
+ use crate::device::{VertexAttribute, VertexAttributeKind, VertexDescriptor};
+
+ pub const PRIM_INSTANCES: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[VertexAttribute {
+ name: "aData",
+ count: 4,
+ kind: VertexAttributeKind::I32,
+ }],
+ };
+
+ pub const BLUR: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ VertexAttribute {
+ name: "aBlurRenderTaskAddress",
+ count: 1,
+ kind: VertexAttributeKind::U16,
+ },
+ VertexAttribute {
+ name: "aBlurSourceTaskAddress",
+ count: 1,
+ kind: VertexAttributeKind::U16,
+ },
+ VertexAttribute {
+ name: "aBlurDirection",
+ count: 1,
+ kind: VertexAttributeKind::I32,
+ },
+ ],
+ };
+
+ pub const LINE: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ VertexAttribute {
+ name: "aTaskRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aLocalSize",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aWavyLineThickness",
+ count: 1,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aStyle",
+ count: 1,
+ kind: VertexAttributeKind::I32,
+ },
+ VertexAttribute {
+ name: "aAxisSelect",
+ count: 1,
+ kind: VertexAttributeKind::F32,
+ },
+ ],
+ };
+
+ pub const FAST_LINEAR_GRADIENT: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ VertexAttribute {
+ name: "aTaskRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aColor0",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aColor1",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aAxisSelect",
+ count: 1,
+ kind: VertexAttributeKind::F32,
+ },
+ ],
+ };
+
+ pub const LINEAR_GRADIENT: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ VertexAttribute {
+ name: "aTaskRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aStartPoint",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aEndPoint",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aScale",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aExtendMode",
+ count: 1,
+ kind: VertexAttributeKind::I32,
+ },
+ VertexAttribute {
+ name: "aGradientStopsAddress",
+ count: 1,
+ kind: VertexAttributeKind::I32,
+ },
+ ],
+ };
+
+ pub const RADIAL_GRADIENT: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ VertexAttribute {
+ name: "aTaskRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aCenter",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aScale",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aStartRadius",
+ count: 1,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aEndRadius",
+ count: 1,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aXYRatio",
+ count: 1,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aExtendMode",
+ count: 1,
+ kind: VertexAttributeKind::I32,
+ },
+ VertexAttribute {
+ name: "aGradientStopsAddress",
+ count: 1,
+ kind: VertexAttributeKind::I32,
+ },
+ ],
+ };
+
+ pub const CONIC_GRADIENT: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ VertexAttribute {
+ name: "aTaskRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aCenter",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aScale",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aStartOffset",
+ count: 1,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aEndOffset",
+ count: 1,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aAngle",
+ count: 1,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aExtendMode",
+ count: 1,
+ kind: VertexAttributeKind::I32,
+ },
+ VertexAttribute {
+ name: "aGradientStopsAddress",
+ count: 1,
+ kind: VertexAttributeKind::I32,
+ },
+ ],
+ };
+
+ pub const BORDER: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ VertexAttribute {
+ name: "aTaskOrigin",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aColor0",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aColor1",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aFlags",
+ count: 1,
+ kind: VertexAttributeKind::I32,
+ },
+ VertexAttribute {
+ name: "aWidths",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aRadii",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipParams1",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipParams2",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ ],
+ };
+
+ pub const SCALE: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ VertexAttribute {
+ name: "aScaleTargetRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aScaleSourceRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ ],
+ };
+
+ pub const CLIP_RECT: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ // common clip attributes
+ VertexAttribute {
+ name: "aClipDeviceArea",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipOrigins",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aDevicePixelScale",
+ count: 1,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aTransformIds",
+ count: 2,
+ kind: VertexAttributeKind::I32,
+ },
+ // specific clip attributes
+ VertexAttribute {
+ name: "aClipLocalPos",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipLocalRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipMode",
+ count: 1,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipRect_TL",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipRadii_TL",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipRect_TR",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipRadii_TR",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipRect_BL",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipRadii_BL",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipRect_BR",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipRadii_BR",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ ],
+ };
+
+ pub const CLIP_BOX_SHADOW: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ // common clip attributes
+ VertexAttribute {
+ name: "aClipDeviceArea",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipOrigins",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aDevicePixelScale",
+ count: 1,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aTransformIds",
+ count: 2,
+ kind: VertexAttributeKind::I32,
+ },
+ // specific clip attributes
+ VertexAttribute {
+ name: "aClipDataResourceAddress",
+ count: 2,
+ kind: VertexAttributeKind::U16,
+ },
+ VertexAttribute {
+ name: "aClipSrcRectSize",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipMode",
+ count: 1,
+ kind: VertexAttributeKind::I32,
+ },
+ VertexAttribute {
+ name: "aStretchMode",
+ count: 2,
+ kind: VertexAttributeKind::I32,
+ },
+ VertexAttribute {
+ name: "aClipDestRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ ],
+ };
+
+ pub const CLIP_IMAGE: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ // common clip attributes
+ VertexAttribute {
+ name: "aClipDeviceArea",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipOrigins",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aDevicePixelScale",
+ count: 1,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aTransformIds",
+ count: 2,
+ kind: VertexAttributeKind::I32,
+ },
+ // specific clip attributes
+ VertexAttribute {
+ name: "aClipTileRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipDataResourceAddress",
+ count: 2,
+ kind: VertexAttributeKind::U16,
+ },
+ VertexAttribute {
+ name: "aClipLocalRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ ],
+ };
+
+ pub const GPU_CACHE_UPDATE: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[
+ VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U16Norm,
+ },
+ VertexAttribute {
+ name: "aValue",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ ],
+ instance_attributes: &[],
+ };
+
+ pub const RESOLVE: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[VertexAttribute {
+ name: "aRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ }],
+ };
+
+ pub const SVG_FILTER: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ VertexAttribute {
+ name: "aFilterRenderTaskAddress",
+ count: 1,
+ kind: VertexAttributeKind::U16,
+ },
+ VertexAttribute {
+ name: "aFilterInput1TaskAddress",
+ count: 1,
+ kind: VertexAttributeKind::U16,
+ },
+ VertexAttribute {
+ name: "aFilterInput2TaskAddress",
+ count: 1,
+ kind: VertexAttributeKind::U16,
+ },
+ VertexAttribute {
+ name: "aFilterKind",
+ count: 1,
+ kind: VertexAttributeKind::U16,
+ },
+ VertexAttribute {
+ name: "aFilterInputCount",
+ count: 1,
+ kind: VertexAttributeKind::U16,
+ },
+ VertexAttribute {
+ name: "aFilterGenericInt",
+ count: 1,
+ kind: VertexAttributeKind::U16,
+ },
+ VertexAttribute {
+ name: "aFilterExtraDataAddress",
+ count: 2,
+ kind: VertexAttributeKind::U16,
+ },
+ ],
+ };
+
+ pub const MASK: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ VertexAttribute {
+ name: "aData",
+ count: 4,
+ kind: VertexAttributeKind::I32,
+ },
+ VertexAttribute {
+ name: "aClipData",
+ count: 4,
+ kind: VertexAttributeKind::I32,
+ },
+ ],
+ };
+
+ pub const VECTOR_STENCIL: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ VertexAttribute {
+ name: "aFromPosition",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aCtrlPosition",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aToPosition",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aFromNormal",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aCtrlNormal",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aToNormal",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aPathID",
+ count: 1,
+ kind: VertexAttributeKind::U16,
+ },
+ VertexAttribute {
+ name: "aPad",
+ count: 1,
+ kind: VertexAttributeKind::U16,
+ },
+ ],
+ };
+
+ pub const VECTOR_COVER: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ VertexAttribute {
+ name: "aTargetRect",
+ count: 4,
+ kind: VertexAttributeKind::I32,
+ },
+ VertexAttribute {
+ name: "aStencilOrigin",
+ count: 2,
+ kind: VertexAttributeKind::I32,
+ },
+ VertexAttribute {
+ name: "aSubpixel",
+ count: 1,
+ kind: VertexAttributeKind::U16,
+ },
+ VertexAttribute {
+ name: "aPad",
+ count: 1,
+ kind: VertexAttributeKind::U16,
+ },
+ ],
+ };
+
+ pub const COMPOSITE: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ VertexAttribute {
+ name: "aLocalRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aDeviceClipRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aColor",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aParams",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aUvRect0",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aUvRect1",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aUvRect2",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aTransform",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ ],
+ };
+
+ pub const CLEAR: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ VertexAttribute {
+ name: "aRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aColor",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ ],
+ };
+
+ pub const COPY: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ VertexAttribute {
+ name: "a_src_rect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "a_dst_rect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "a_dst_texture_size",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ ],
+ };
+}
+
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub enum VertexArrayKind {
+ Primitive,
+ Blur,
+ ClipImage,
+ ClipRect,
+ ClipBoxShadow,
+ VectorStencil,
+ VectorCover,
+ Border,
+ Scale,
+ LineDecoration,
+ FastLinearGradient,
+ LinearGradient,
+ RadialGradient,
+ ConicGradient,
+ Resolve,
+ SvgFilter,
+ Composite,
+ Clear,
+ Copy,
+ Mask,
+}
+
+pub struct VertexDataTexture<T> {
+ texture: Option<Texture>,
+ format: api::ImageFormat,
+ _marker: PhantomData<T>,
+}
+
+impl<T> VertexDataTexture<T> {
+ pub fn new(format: api::ImageFormat) -> Self {
+ Self {
+ texture: None,
+ format,
+ _marker: PhantomData,
+ }
+ }
+
+ /// Returns a borrow of the GPU texture. Panics if it hasn't been initialized.
+ pub fn texture(&self) -> &Texture {
+ self.texture.as_ref().unwrap()
+ }
+
+ /// Returns an estimate of the GPU memory consumed by this VertexDataTexture.
+ pub fn size_in_bytes(&self) -> usize {
+ self.texture.as_ref().map_or(0, |t| t.size_in_bytes())
+ }
+
+ pub fn update<'a>(
+ &'a mut self,
+ device: &mut Device,
+ texture_uploader: &mut TextureUploader<'a>,
+ data: &mut Vec<T>,
+ ) {
+ debug_assert!(mem::size_of::<T>() % 16 == 0);
+ let texels_per_item = mem::size_of::<T>() / 16;
+ let items_per_row = MAX_VERTEX_TEXTURE_WIDTH / texels_per_item;
+ debug_assert_ne!(items_per_row, 0);
+
+ // Ensure we always end up with a texture when leaving this method.
+ let mut len = data.len();
+ if len == 0 {
+ if self.texture.is_some() {
+ return;
+ }
+ data.reserve(items_per_row);
+ len = items_per_row;
+ } else {
+ // Extend the data array to have enough capacity to upload at least
+ // a multiple of the row size. This ensures memory safety when the
+ // array is passed to OpenGL to upload to the GPU.
+ let extra = len % items_per_row;
+ if extra != 0 {
+ let padding = items_per_row - extra;
+ data.reserve(padding);
+ len += padding;
+ }
+ }
+
+ let needed_height = (len / items_per_row) as i32;
+ let existing_height = self
+ .texture
+ .as_ref()
+ .map_or(0, |t| t.get_dimensions().height);
+
+ // Create a new texture if needed.
+ //
+ // These textures are generally very small, which is why we don't bother
+ // with incremental updates and just re-upload every frame. For most pages
+ // they're one row each, and on stress tests like css-francine they end up
+ // in the 6-14 range. So we size the texture tightly to what we need (usually
+ // 1), and shrink it if the waste would be more than `VERTEX_TEXTURE_EXTRA_ROWS`
+ // rows. This helps with memory overhead, especially because there are several
+ // instances of these textures per Renderer.
+ if needed_height > existing_height
+ || needed_height + VERTEX_TEXTURE_EXTRA_ROWS < existing_height
+ {
+ // Drop the existing texture, if any.
+ if let Some(t) = self.texture.take() {
+ device.delete_texture(t);
+ }
+
+ let texture = device.create_texture(
+ api::ImageBufferKind::Texture2D,
+ self.format,
+ MAX_VERTEX_TEXTURE_WIDTH as i32,
+ // Ensure height is at least two to work around
+ // https://bugs.chromium.org/p/angleproject/issues/detail?id=3039
+ needed_height.max(2),
+ TextureFilter::Nearest,
+ None,
+ );
+ self.texture = Some(texture);
+ }
+
+ // Note: the actual width can be larger than the logical one, with a few texels
+ // of each row unused at the tail. This is needed because there is still hardware
+ // (like Intel iGPUs) that prefers power-of-two sizes of textures ([1]).
+ //
+ // [1] https://software.intel.com/en-us/articles/opengl-performance-tips-power-of-two-textures-have-better-performance
+ let logical_width = if needed_height == 1 {
+ data.len() * texels_per_item
+ } else {
+ MAX_VERTEX_TEXTURE_WIDTH - (MAX_VERTEX_TEXTURE_WIDTH % texels_per_item)
+ };
+
+ let rect = DeviceIntRect::from_size(
+ DeviceIntSize::new(logical_width as i32, needed_height),
+ );
+
+ debug_assert!(len <= data.capacity(), "CPU copy will read out of bounds");
+ texture_uploader.upload(
+ device,
+ self.texture(),
+ rect,
+ None,
+ None,
+ data.as_ptr(),
+ len,
+ );
+ }
+
+ pub fn deinit(mut self, device: &mut Device) {
+ if let Some(t) = self.texture.take() {
+ device.delete_texture(t);
+ }
+ }
+}
+
+pub struct VertexDataTextures {
+ prim_header_f_texture: VertexDataTexture<PrimitiveHeaderF>,
+ prim_header_i_texture: VertexDataTexture<PrimitiveHeaderI>,
+ transforms_texture: VertexDataTexture<TransformData>,
+ render_task_texture: VertexDataTexture<RenderTaskData>,
+}
+
+impl VertexDataTextures {
+ pub fn new() -> Self {
+ VertexDataTextures {
+ prim_header_f_texture: VertexDataTexture::new(api::ImageFormat::RGBAF32),
+ prim_header_i_texture: VertexDataTexture::new(api::ImageFormat::RGBAI32),
+ transforms_texture: VertexDataTexture::new(api::ImageFormat::RGBAF32),
+ render_task_texture: VertexDataTexture::new(api::ImageFormat::RGBAF32),
+ }
+ }
+
+ pub fn update(&mut self, device: &mut Device, pbo_pool: &mut UploadPBOPool, frame: &mut Frame) {
+ let mut texture_uploader = device.upload_texture(pbo_pool);
+ self.prim_header_f_texture.update(
+ device,
+ &mut texture_uploader,
+ &mut frame.prim_headers.headers_float,
+ );
+ self.prim_header_i_texture.update(
+ device,
+ &mut texture_uploader,
+ &mut frame.prim_headers.headers_int,
+ );
+ self.transforms_texture
+ .update(device, &mut texture_uploader, &mut frame.transform_palette);
+ self.render_task_texture.update(
+ device,
+ &mut texture_uploader,
+ &mut frame.render_tasks.task_data,
+ );
+
+ // Flush and drop the texture uploader now, so that
+ // we can borrow the textures to bind them.
+ texture_uploader.flush(device);
+
+ device.bind_texture(
+ super::TextureSampler::PrimitiveHeadersF,
+ &self.prim_header_f_texture.texture(),
+ Swizzle::default(),
+ );
+ device.bind_texture(
+ super::TextureSampler::PrimitiveHeadersI,
+ &self.prim_header_i_texture.texture(),
+ Swizzle::default(),
+ );
+ device.bind_texture(
+ super::TextureSampler::TransformPalette,
+ &self.transforms_texture.texture(),
+ Swizzle::default(),
+ );
+ device.bind_texture(
+ super::TextureSampler::RenderTasks,
+ &self.render_task_texture.texture(),
+ Swizzle::default(),
+ );
+ }
+
+ pub fn size_in_bytes(&self) -> usize {
+ self.prim_header_f_texture.size_in_bytes()
+ + self.prim_header_i_texture.size_in_bytes()
+ + self.transforms_texture.size_in_bytes()
+ + self.render_task_texture.size_in_bytes()
+ }
+
+ pub fn deinit(self, device: &mut Device) {
+ self.transforms_texture.deinit(device);
+ self.prim_header_f_texture.deinit(device);
+ self.prim_header_i_texture.deinit(device);
+ self.render_task_texture.deinit(device);
+ }
+}
+
+pub struct RendererVAOs {
+ prim_vao: VAO,
+ blur_vao: VAO,
+ clip_rect_vao: VAO,
+ clip_box_shadow_vao: VAO,
+ clip_image_vao: VAO,
+ border_vao: VAO,
+ line_vao: VAO,
+ scale_vao: VAO,
+ fast_linear_gradient_vao: VAO,
+ linear_gradient_vao: VAO,
+ radial_gradient_vao: VAO,
+ conic_gradient_vao: VAO,
+ resolve_vao: VAO,
+ svg_filter_vao: VAO,
+ composite_vao: VAO,
+ clear_vao: VAO,
+ copy_vao: VAO,
+ mask_vao: VAO,
+}
+
+impl RendererVAOs {
+ pub fn new(device: &mut Device, indexed_quads: Option<NonZeroUsize>) -> Self {
+ const QUAD_INDICES: [u16; 6] = [0, 1, 2, 2, 1, 3];
+ const QUAD_VERTICES: [[u8; 2]; 4] = [[0, 0], [0xFF, 0], [0, 0xFF], [0xFF, 0xFF]];
+
+ let instance_divisor = if indexed_quads.is_some() { 0 } else { 1 };
+ let prim_vao = device.create_vao(&desc::PRIM_INSTANCES, instance_divisor);
+
+ device.bind_vao(&prim_vao);
+ match indexed_quads {
+ Some(count) => {
+ assert!(count.get() < u16::MAX as usize);
+ let quad_indices = (0 .. count.get() as u16)
+ .flat_map(|instance| QUAD_INDICES.iter().map(move |&index| instance * 4 + index))
+ .collect::<Vec<_>>();
+ device.update_vao_indices(&prim_vao, &quad_indices, VertexUsageHint::Static);
+ let quad_vertices = (0 .. count.get() as u16)
+ .flat_map(|_| QUAD_VERTICES.iter().cloned())
+ .collect::<Vec<_>>();
+ device.update_vao_main_vertices(&prim_vao, &quad_vertices, VertexUsageHint::Static);
+ }
+ None => {
+ device.update_vao_indices(&prim_vao, &QUAD_INDICES, VertexUsageHint::Static);
+ device.update_vao_main_vertices(&prim_vao, &QUAD_VERTICES, VertexUsageHint::Static);
+ }
+ }
+
+ RendererVAOs {
+ blur_vao: device.create_vao_with_new_instances(&desc::BLUR, &prim_vao),
+ clip_rect_vao: device.create_vao_with_new_instances(&desc::CLIP_RECT, &prim_vao),
+ clip_box_shadow_vao: device
+ .create_vao_with_new_instances(&desc::CLIP_BOX_SHADOW, &prim_vao),
+ clip_image_vao: device.create_vao_with_new_instances(&desc::CLIP_IMAGE, &prim_vao),
+ border_vao: device.create_vao_with_new_instances(&desc::BORDER, &prim_vao),
+ scale_vao: device.create_vao_with_new_instances(&desc::SCALE, &prim_vao),
+ line_vao: device.create_vao_with_new_instances(&desc::LINE, &prim_vao),
+ fast_linear_gradient_vao: device.create_vao_with_new_instances(&desc::FAST_LINEAR_GRADIENT, &prim_vao),
+ linear_gradient_vao: device.create_vao_with_new_instances(&desc::LINEAR_GRADIENT, &prim_vao),
+ radial_gradient_vao: device.create_vao_with_new_instances(&desc::RADIAL_GRADIENT, &prim_vao),
+ conic_gradient_vao: device.create_vao_with_new_instances(&desc::CONIC_GRADIENT, &prim_vao),
+ resolve_vao: device.create_vao_with_new_instances(&desc::RESOLVE, &prim_vao),
+ svg_filter_vao: device.create_vao_with_new_instances(&desc::SVG_FILTER, &prim_vao),
+ composite_vao: device.create_vao_with_new_instances(&desc::COMPOSITE, &prim_vao),
+ clear_vao: device.create_vao_with_new_instances(&desc::CLEAR, &prim_vao),
+ copy_vao: device.create_vao_with_new_instances(&desc::COPY, &prim_vao),
+ mask_vao: device.create_vao_with_new_instances(&desc::MASK, &prim_vao),
+ prim_vao,
+ }
+ }
+
+ pub fn deinit(self, device: &mut Device) {
+ device.delete_vao(self.prim_vao);
+ device.delete_vao(self.resolve_vao);
+ device.delete_vao(self.clip_rect_vao);
+ device.delete_vao(self.clip_box_shadow_vao);
+ device.delete_vao(self.clip_image_vao);
+ device.delete_vao(self.fast_linear_gradient_vao);
+ device.delete_vao(self.linear_gradient_vao);
+ device.delete_vao(self.radial_gradient_vao);
+ device.delete_vao(self.conic_gradient_vao);
+ device.delete_vao(self.blur_vao);
+ device.delete_vao(self.line_vao);
+ device.delete_vao(self.border_vao);
+ device.delete_vao(self.scale_vao);
+ device.delete_vao(self.svg_filter_vao);
+ device.delete_vao(self.composite_vao);
+ device.delete_vao(self.clear_vao);
+ device.delete_vao(self.copy_vao);
+ device.delete_vao(self.mask_vao);
+ }
+}
+
+impl ops::Index<VertexArrayKind> for RendererVAOs {
+ type Output = VAO;
+ fn index(&self, kind: VertexArrayKind) -> &VAO {
+ match kind {
+ VertexArrayKind::Primitive => &self.prim_vao,
+ VertexArrayKind::ClipImage => &self.clip_image_vao,
+ VertexArrayKind::ClipRect => &self.clip_rect_vao,
+ VertexArrayKind::ClipBoxShadow => &self.clip_box_shadow_vao,
+ VertexArrayKind::Blur => &self.blur_vao,
+ VertexArrayKind::VectorStencil | VertexArrayKind::VectorCover => unreachable!(),
+ VertexArrayKind::Border => &self.border_vao,
+ VertexArrayKind::Scale => &self.scale_vao,
+ VertexArrayKind::LineDecoration => &self.line_vao,
+ VertexArrayKind::FastLinearGradient => &self.fast_linear_gradient_vao,
+ VertexArrayKind::LinearGradient => &self.linear_gradient_vao,
+ VertexArrayKind::RadialGradient => &self.radial_gradient_vao,
+ VertexArrayKind::ConicGradient => &self.conic_gradient_vao,
+ VertexArrayKind::Resolve => &self.resolve_vao,
+ VertexArrayKind::SvgFilter => &self.svg_filter_vao,
+ VertexArrayKind::Composite => &self.composite_vao,
+ VertexArrayKind::Clear => &self.clear_vao,
+ VertexArrayKind::Copy => &self.copy_vao,
+ VertexArrayKind::Mask => &self.mask_vao,
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/resource_cache.rs b/gfx/wr/webrender/src/resource_cache.rs
new file mode 100644
index 0000000000..9459bf86da
--- /dev/null
+++ b/gfx/wr/webrender/src/resource_cache.rs
@@ -0,0 +1,2308 @@
+/* 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 api::{BlobImageRequest, RasterizedBlobImage, ImageFormat, ImageDescriptorFlags};
+use api::{DebugFlags, FontInstanceKey, FontKey, FontTemplate, GlyphIndex};
+use api::{ExternalImageData, ExternalImageType, ExternalImageId, BlobImageResult};
+use api::{DirtyRect, GlyphDimensions, IdNamespace, DEFAULT_TILE_SIZE};
+use api::{ColorF, ImageData, ImageDescriptor, ImageKey, ImageRendering, TileSize};
+use api::{BlobImageHandler, BlobImageKey, VoidPtrToSizeFn};
+use api::units::*;
+use euclid::size2;
+use crate::{render_api::{ClearCache, AddFont, ResourceUpdate, MemoryReport}, util::WeakTable};
+use crate::image_tiling::{compute_tile_size, compute_tile_range};
+#[cfg(feature = "capture")]
+use crate::capture::ExternalCaptureImage;
+#[cfg(feature = "replay")]
+use crate::capture::PlainExternalImage;
+#[cfg(any(feature = "replay", feature = "png", feature="capture"))]
+use crate::capture::CaptureConfig;
+use crate::composite::{NativeSurfaceId, NativeSurfaceOperation, NativeTileId, NativeSurfaceOperationDetails};
+use crate::device::TextureFilter;
+use crate::glyph_cache::{GlyphCache, CachedGlyphInfo};
+use crate::glyph_cache::GlyphCacheEntry;
+use glyph_rasterizer::{GLYPH_FLASHING, FontInstance, GlyphFormat, GlyphKey, GlyphRasterizer, GlyphRasterJob};
+use glyph_rasterizer::{SharedFontResources, BaseFontInstance};
+use crate::gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
+use crate::gpu_types::UvRectKind;
+use crate::internal_types::{
+ CacheTextureId, FastHashMap, FastHashSet, TextureSource, ResourceUpdateList,
+ FrameId, FrameStamp,
+};
+use crate::profiler::{self, TransactionProfile, bytes_to_mb};
+use crate::render_task_graph::{RenderTaskId, RenderTaskGraphBuilder};
+use crate::render_task_cache::{RenderTaskCache, RenderTaskCacheKey, RenderTaskParent};
+use crate::render_task_cache::{RenderTaskCacheEntry, RenderTaskCacheEntryHandle};
+use crate::renderer::GpuBufferBuilder;
+use crate::surface::SurfaceBuilder;
+use euclid::point2;
+use smallvec::SmallVec;
+use std::collections::hash_map::Entry::{self, Occupied, Vacant};
+use std::collections::hash_map::{Iter, IterMut};
+use std::collections::VecDeque;
+use std::{cmp, mem};
+use std::fmt::Debug;
+use std::hash::Hash;
+use std::os::raw::c_void;
+#[cfg(any(feature = "capture", feature = "replay"))]
+use std::path::PathBuf;
+use std::sync::Arc;
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::u32;
+use crate::texture_cache::{TextureCache, TextureCacheHandle, Eviction, TargetShader};
+use crate::picture_textures::PictureTextures;
+use peek_poke::PeekPoke;
+
+// Counter for generating unique native surface ids
+static NEXT_NATIVE_SURFACE_ID: AtomicUsize = AtomicUsize::new(0);
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct GlyphFetchResult {
+ pub index_in_text_run: i32,
+ pub uv_rect_address: GpuCacheAddress,
+ pub offset: DevicePoint,
+ pub size: DeviceIntSize,
+ pub scale: f32,
+}
+
+// These coordinates are always in texels.
+// They are converted to normalized ST
+// values in the vertex shader. The reason
+// for this is that the texture may change
+// dimensions (e.g. the pages in a texture
+// atlas can grow). When this happens, by
+// storing the coordinates as texel values
+// we don't need to go through and update
+// various CPU-side structures.
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct CacheItem {
+ pub texture_id: TextureSource,
+ pub uv_rect_handle: GpuCacheHandle,
+ pub uv_rect: DeviceIntRect,
+ pub user_data: [f32; 4],
+}
+
+impl CacheItem {
+ pub fn invalid() -> Self {
+ CacheItem {
+ texture_id: TextureSource::Invalid,
+ uv_rect_handle: GpuCacheHandle::new(),
+ uv_rect: DeviceIntRect::zero(),
+ user_data: [0.0; 4],
+ }
+ }
+
+ pub fn is_valid(&self) -> bool {
+ self.texture_id != TextureSource::Invalid
+ }
+}
+
+/// Represents the backing store of an image in the cache.
+/// This storage can take several forms.
+#[derive(Clone, Debug)]
+pub enum CachedImageData {
+ /// A simple series of bytes, provided by the embedding and owned by WebRender.
+ /// The format is stored out-of-band, currently in ImageDescriptor.
+ Raw(Arc<Vec<u8>>),
+ /// An series of commands that can be rasterized into an image via an
+ /// embedding-provided callback.
+ ///
+ /// The commands are stored elsewhere and this variant is used as a placeholder.
+ Blob,
+ /// An image owned by the embedding, and referenced by WebRender. This may
+ /// take the form of a texture or a heap-allocated buffer.
+ External(ExternalImageData),
+}
+
+impl From<ImageData> for CachedImageData {
+ fn from(img_data: ImageData) -> Self {
+ match img_data {
+ ImageData::Raw(data) => CachedImageData::Raw(data),
+ ImageData::External(data) => CachedImageData::External(data),
+ }
+ }
+}
+
+impl CachedImageData {
+ /// Returns true if this represents a blob.
+ #[inline]
+ pub fn is_blob(&self) -> bool {
+ match *self {
+ CachedImageData::Blob => true,
+ _ => false,
+ }
+ }
+
+ /// Returns true if this variant of CachedImageData should go through the texture
+ /// cache.
+ #[inline]
+ pub fn uses_texture_cache(&self) -> bool {
+ match *self {
+ CachedImageData::External(ref ext_data) => match ext_data.image_type {
+ ExternalImageType::TextureHandle(_) => false,
+ ExternalImageType::Buffer => true,
+ },
+ CachedImageData::Blob => true,
+ CachedImageData::Raw(_) => true,
+ }
+ }
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ImageProperties {
+ pub descriptor: ImageDescriptor,
+ pub external_image: Option<ExternalImageData>,
+ pub tiling: Option<TileSize>,
+ // Potentially a subset of the image's total rectangle. This rectangle is what
+ // we map to the (layout space) display item bounds.
+ pub visible_rect: DeviceIntRect,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq)]
+enum State {
+ Idle,
+ AddResources,
+ QueryResources,
+}
+
+/// Post scene building state.
+type RasterizedBlob = FastHashMap<TileOffset, RasterizedBlobImage>;
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, PartialEq, PeekPoke, Default)]
+pub struct ImageGeneration(pub u32);
+
+impl ImageGeneration {
+ pub const INVALID: ImageGeneration = ImageGeneration(u32::MAX);
+}
+
+struct ImageResource {
+ data: CachedImageData,
+ descriptor: ImageDescriptor,
+ tiling: Option<TileSize>,
+ /// This is used to express images that are virtually very large
+ /// but with only a visible sub-set that is valid at a given time.
+ visible_rect: DeviceIntRect,
+ generation: ImageGeneration,
+}
+
+#[derive(Clone, Debug)]
+pub struct ImageTiling {
+ pub image_size: DeviceIntSize,
+ pub tile_size: TileSize,
+}
+
+#[derive(Default)]
+struct ImageTemplates {
+ images: FastHashMap<ImageKey, ImageResource>,
+}
+
+impl ImageTemplates {
+ fn insert(&mut self, key: ImageKey, resource: ImageResource) {
+ self.images.insert(key, resource);
+ }
+
+ fn remove(&mut self, key: ImageKey) -> Option<ImageResource> {
+ self.images.remove(&key)
+ }
+
+ fn get(&self, key: ImageKey) -> Option<&ImageResource> {
+ self.images.get(&key)
+ }
+
+ fn get_mut(&mut self, key: ImageKey) -> Option<&mut ImageResource> {
+ self.images.get_mut(&key)
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct CachedImageInfo {
+ texture_cache_handle: TextureCacheHandle,
+ dirty_rect: ImageDirtyRect,
+ manual_eviction: bool,
+}
+
+impl CachedImageInfo {
+ fn mark_unused(&mut self, texture_cache: &mut TextureCache) {
+ texture_cache.evict_handle(&self.texture_cache_handle);
+ self.manual_eviction = false;
+ }
+}
+
+#[cfg(debug_assertions)]
+impl Drop for CachedImageInfo {
+ fn drop(&mut self) {
+ debug_assert!(!self.manual_eviction, "Manual eviction requires cleanup");
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ResourceClassCache<K: Hash + Eq, V, U: Default> {
+ resources: FastHashMap<K, V>,
+ pub user_data: U,
+}
+
+impl<K, V, U> ResourceClassCache<K, V, U>
+where
+ K: Clone + Hash + Eq + Debug,
+ U: Default,
+{
+ pub fn new() -> Self {
+ ResourceClassCache {
+ resources: FastHashMap::default(),
+ user_data: Default::default(),
+ }
+ }
+
+ pub fn get(&self, key: &K) -> &V {
+ self.resources.get(key)
+ .expect("Didn't find a cached resource with that ID!")
+ }
+
+ pub fn try_get(&self, key: &K) -> Option<&V> {
+ self.resources.get(key)
+ }
+
+ pub fn insert(&mut self, key: K, value: V) {
+ self.resources.insert(key, value);
+ }
+
+ pub fn remove(&mut self, key: &K) -> Option<V> {
+ self.resources.remove(key)
+ }
+
+ pub fn get_mut(&mut self, key: &K) -> &mut V {
+ self.resources.get_mut(key)
+ .expect("Didn't find a cached resource with that ID!")
+ }
+
+ pub fn try_get_mut(&mut self, key: &K) -> Option<&mut V> {
+ self.resources.get_mut(key)
+ }
+
+ pub fn entry(&mut self, key: K) -> Entry<K, V> {
+ self.resources.entry(key)
+ }
+
+ pub fn iter(&self) -> Iter<K, V> {
+ self.resources.iter()
+ }
+
+ pub fn iter_mut(&mut self) -> IterMut<K, V> {
+ self.resources.iter_mut()
+ }
+
+ pub fn is_empty(&mut self) -> bool {
+ self.resources.is_empty()
+ }
+
+ pub fn clear(&mut self) {
+ self.resources.clear();
+ }
+
+ pub fn retain<F>(&mut self, f: F)
+ where
+ F: FnMut(&K, &mut V) -> bool,
+ {
+ self.resources.retain(f);
+ }
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct CachedImageKey {
+ pub rendering: ImageRendering,
+ pub tile: Option<TileOffset>,
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ImageRequest {
+ pub key: ImageKey,
+ pub rendering: ImageRendering,
+ pub tile: Option<TileOffset>,
+}
+
+impl ImageRequest {
+ pub fn with_tile(&self, offset: TileOffset) -> Self {
+ ImageRequest {
+ key: self.key,
+ rendering: self.rendering,
+ tile: Some(offset),
+ }
+ }
+
+ pub fn is_untiled_auto(&self) -> bool {
+ self.tile.is_none() && self.rendering == ImageRendering::Auto
+ }
+}
+
+impl Into<BlobImageRequest> for ImageRequest {
+ fn into(self) -> BlobImageRequest {
+ BlobImageRequest {
+ key: BlobImageKey(self.key),
+ tile: self.tile.unwrap(),
+ }
+ }
+}
+
+impl Into<CachedImageKey> for ImageRequest {
+ fn into(self) -> CachedImageKey {
+ CachedImageKey {
+ rendering: self.rendering,
+ tile: self.tile,
+ }
+ }
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Clone, Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum ImageCacheError {
+ OverLimitSize,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+enum ImageResult {
+ UntiledAuto(CachedImageInfo),
+ Multi(ResourceClassCache<CachedImageKey, CachedImageInfo, ()>),
+ Err(ImageCacheError),
+}
+
+impl ImageResult {
+ /// Releases any texture cache entries held alive by this ImageResult.
+ fn drop_from_cache(&mut self, texture_cache: &mut TextureCache) {
+ match *self {
+ ImageResult::UntiledAuto(ref mut entry) => {
+ entry.mark_unused(texture_cache);
+ },
+ ImageResult::Multi(ref mut entries) => {
+ for entry in entries.resources.values_mut() {
+ entry.mark_unused(texture_cache);
+ }
+ },
+ ImageResult::Err(_) => {},
+ }
+ }
+}
+
+type ImageCache = ResourceClassCache<ImageKey, ImageResult, ()>;
+
+struct Resources {
+ fonts: SharedFontResources,
+ image_templates: ImageTemplates,
+ // We keep a set of Weak references to the fonts so that we're able to include them in memory
+ // reports even if only the OS is holding on to the Vec<u8>. PtrWeakHashSet will periodically
+ // drop any references that have gone dead.
+ weak_fonts: WeakTable
+}
+
+// We only use this to report glyph dimensions to the user of the API, so using
+// the font instance key should be enough. If we start using it to cache dimensions
+// for internal font instances we should change the hash key accordingly.
+pub type GlyphDimensionsCache = FastHashMap<(FontInstanceKey, GlyphIndex), Option<GlyphDimensions>>;
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct BlobImageRasterizerEpoch(usize);
+
+/// Internal information about allocated render targets in the pool
+struct RenderTarget {
+ size: DeviceIntSize,
+ format: ImageFormat,
+ texture_id: CacheTextureId,
+ /// If true, this is currently leant out, and not available to other passes
+ is_active: bool,
+ last_frame_used: FrameId,
+}
+
+impl RenderTarget {
+ fn size_in_bytes(&self) -> usize {
+ let bpp = self.format.bytes_per_pixel() as usize;
+ (self.size.width * self.size.height) as usize * bpp
+ }
+
+ /// Returns true if this texture was used within `threshold` frames of
+ /// the current frame.
+ pub fn used_recently(&self, current_frame_id: FrameId, threshold: u64) -> bool {
+ self.last_frame_used + threshold >= current_frame_id
+ }
+}
+
+/// High-level container for resources managed by the `RenderBackend`.
+///
+/// This includes a variety of things, including images, fonts, and glyphs,
+/// which may be stored as memory buffers, GPU textures, or handles to resources
+/// managed by the OS or other parts of WebRender.
+pub struct ResourceCache {
+ cached_glyphs: GlyphCache,
+ cached_images: ImageCache,
+ cached_render_tasks: RenderTaskCache,
+
+ resources: Resources,
+ state: State,
+ current_frame_id: FrameId,
+
+ #[cfg(feature = "capture")]
+ /// Used for capture sequences. If the resource cache is updated, then we
+ /// mark it as dirty. When the next frame is captured in the sequence, we
+ /// dump the state of the resource cache.
+ capture_dirty: bool,
+
+ pub texture_cache: TextureCache,
+ pub picture_textures: PictureTextures,
+
+ /// TODO(gw): We should expire (parts of) this cache semi-regularly!
+ cached_glyph_dimensions: GlyphDimensionsCache,
+ glyph_rasterizer: GlyphRasterizer,
+
+ /// The set of images that aren't present or valid in the texture cache,
+ /// and need to be rasterized and/or uploaded this frame. This includes
+ /// both blobs and regular images.
+ pending_image_requests: FastHashSet<ImageRequest>,
+
+ rasterized_blob_images: FastHashMap<BlobImageKey, RasterizedBlob>,
+
+ /// A log of the last three frames worth of deleted image keys kept
+ /// for debugging purposes.
+ deleted_blob_keys: VecDeque<Vec<BlobImageKey>>,
+
+ /// We keep one around to be able to call clear_namespace
+ /// after the api object is deleted. For most purposes the
+ /// api object's blob handler should be used instead.
+ blob_image_handler: Option<Box<dyn BlobImageHandler>>,
+
+ /// A list of queued compositor surface updates to apply next frame.
+ pending_native_surface_updates: Vec<NativeSurfaceOperation>,
+
+ image_templates_memory: usize,
+ font_templates_memory: usize,
+
+ /// A pool of render targets for use by the render task graph
+ render_target_pool: Vec<RenderTarget>,
+}
+
+impl ResourceCache {
+ pub fn new(
+ texture_cache: TextureCache,
+ picture_textures: PictureTextures,
+ glyph_rasterizer: GlyphRasterizer,
+ cached_glyphs: GlyphCache,
+ fonts: SharedFontResources,
+ blob_image_handler: Option<Box<dyn BlobImageHandler>>,
+ ) -> Self {
+ ResourceCache {
+ cached_glyphs,
+ cached_images: ResourceClassCache::new(),
+ cached_render_tasks: RenderTaskCache::new(),
+ resources: Resources {
+ fonts,
+ image_templates: ImageTemplates::default(),
+ weak_fonts: WeakTable::new(),
+ },
+ cached_glyph_dimensions: FastHashMap::default(),
+ texture_cache,
+ picture_textures,
+ state: State::Idle,
+ current_frame_id: FrameId::INVALID,
+ pending_image_requests: FastHashSet::default(),
+ glyph_rasterizer,
+ rasterized_blob_images: FastHashMap::default(),
+ // We want to keep three frames worth of delete blob keys
+ deleted_blob_keys: vec![Vec::new(), Vec::new(), Vec::new()].into(),
+ blob_image_handler,
+ pending_native_surface_updates: Vec::new(),
+ #[cfg(feature = "capture")]
+ capture_dirty: true,
+ image_templates_memory: 0,
+ font_templates_memory: 0,
+ render_target_pool: Vec::new(),
+ }
+ }
+
+ /// Construct a resource cache for use in unit tests.
+ #[cfg(test)]
+ pub fn new_for_testing() -> Self {
+ use rayon::ThreadPoolBuilder;
+
+ let texture_cache = TextureCache::new_for_testing(
+ 4096,
+ ImageFormat::RGBA8,
+ );
+ let workers = Arc::new(ThreadPoolBuilder::new().build().unwrap());
+ let glyph_rasterizer = GlyphRasterizer::new(workers, true);
+ let cached_glyphs = GlyphCache::new();
+ let fonts = SharedFontResources::new(IdNamespace(0));
+ let picture_textures = PictureTextures::new(
+ crate::picture::TILE_SIZE_DEFAULT,
+ TextureFilter::Nearest,
+ );
+
+ ResourceCache::new(
+ texture_cache,
+ picture_textures,
+ glyph_rasterizer,
+ cached_glyphs,
+ fonts,
+ None,
+ )
+ }
+
+ pub fn max_texture_size(&self) -> i32 {
+ self.texture_cache.max_texture_size()
+ }
+
+ /// Maximum texture size before we consider it preferrable to break the texture
+ /// into tiles.
+ pub fn tiling_threshold(&self) -> i32 {
+ self.texture_cache.tiling_threshold()
+ }
+
+ pub fn enable_multithreading(&mut self, enable: bool) {
+ self.glyph_rasterizer.enable_multithreading(enable);
+ }
+
+ fn should_tile(limit: i32, descriptor: &ImageDescriptor, data: &CachedImageData) -> bool {
+ let size_check = descriptor.size.width > limit || descriptor.size.height > limit;
+ match *data {
+ CachedImageData::Raw(_) | CachedImageData::Blob => size_check,
+ CachedImageData::External(info) => {
+ // External handles already represent existing textures so it does
+ // not make sense to tile them into smaller ones.
+ info.image_type == ExternalImageType::Buffer && size_check
+ }
+ }
+ }
+
+ // Request the texture cache item for a cacheable render
+ // task. If the item is already cached, the texture cache
+ // handle will be returned. Otherwise, the user supplied
+ // closure will be invoked to generate the render task
+ // chain that is required to draw this task.
+ pub fn request_render_task<F>(
+ &mut self,
+ key: RenderTaskCacheKey,
+ gpu_cache: &mut GpuCache,
+ gpu_buffer_builder: &mut GpuBufferBuilder,
+ rg_builder: &mut RenderTaskGraphBuilder,
+ user_data: Option<[f32; 4]>,
+ is_opaque: bool,
+ parent: RenderTaskParent,
+ surface_builder: &mut SurfaceBuilder,
+ f: F,
+ ) -> RenderTaskId
+ where
+ F: FnOnce(&mut RenderTaskGraphBuilder, &mut GpuBufferBuilder) -> RenderTaskId,
+ {
+ self.cached_render_tasks.request_render_task(
+ key,
+ &mut self.texture_cache,
+ gpu_cache,
+ gpu_buffer_builder,
+ rg_builder,
+ user_data,
+ is_opaque,
+ parent,
+ surface_builder,
+ |render_graph, gpu_buffer_builder| Ok(f(render_graph, gpu_buffer_builder))
+ ).expect("Failed to request a render task from the resource cache!")
+ }
+
+ pub fn post_scene_building_update(
+ &mut self,
+ updates: Vec<ResourceUpdate>,
+ profile: &mut TransactionProfile,
+ ) {
+ // TODO, there is potential for optimization here, by processing updates in
+ // bulk rather than one by one (for example by sorting allocations by size or
+ // in a way that reduces fragmentation in the atlas).
+ #[cfg(feature = "capture")]
+ match updates.is_empty() {
+ false => self.capture_dirty = true,
+ _ => {},
+ }
+
+ for update in updates {
+ match update {
+ ResourceUpdate::AddImage(img) => {
+ if let ImageData::Raw(ref bytes) = img.data {
+ self.image_templates_memory += bytes.len();
+ profile.set(profiler::IMAGE_TEMPLATES_MEM, bytes_to_mb(self.image_templates_memory));
+ }
+ self.add_image_template(
+ img.key,
+ img.descriptor,
+ img.data.into(),
+ &img.descriptor.size.into(),
+ img.tiling,
+ );
+ profile.set(profiler::IMAGE_TEMPLATES, self.resources.image_templates.images.len());
+ }
+ ResourceUpdate::UpdateImage(img) => {
+ self.update_image_template(img.key, img.descriptor, img.data.into(), &img.dirty_rect);
+ }
+ ResourceUpdate::AddBlobImage(img) => {
+ self.add_image_template(
+ img.key.as_image(),
+ img.descriptor,
+ CachedImageData::Blob,
+ &img.visible_rect,
+ Some(img.tile_size),
+ );
+ }
+ ResourceUpdate::UpdateBlobImage(img) => {
+ self.update_image_template(
+ img.key.as_image(),
+ img.descriptor,
+ CachedImageData::Blob,
+ &to_image_dirty_rect(
+ &img.dirty_rect
+ ),
+ );
+ self.discard_tiles_outside_visible_area(img.key, &img.visible_rect); // TODO: remove?
+ self.set_image_visible_rect(img.key.as_image(), &img.visible_rect);
+ }
+ ResourceUpdate::DeleteImage(img) => {
+ self.delete_image_template(img);
+ profile.set(profiler::IMAGE_TEMPLATES, self.resources.image_templates.images.len());
+ profile.set(profiler::IMAGE_TEMPLATES_MEM, bytes_to_mb(self.image_templates_memory));
+ }
+ ResourceUpdate::DeleteBlobImage(img) => {
+ self.delete_image_template(img.as_image());
+ }
+ ResourceUpdate::DeleteFont(font) => {
+ if let Some(shared_key) = self.resources.fonts.font_keys.delete_key(&font) {
+ self.delete_font_template(shared_key);
+ if let Some(ref mut handler) = &mut self.blob_image_handler {
+ handler.delete_font(shared_key);
+ }
+ profile.set(profiler::FONT_TEMPLATES, self.resources.fonts.templates.len());
+ profile.set(profiler::FONT_TEMPLATES_MEM, bytes_to_mb(self.font_templates_memory));
+ }
+ }
+ ResourceUpdate::DeleteFontInstance(font) => {
+ if let Some(shared_key) = self.resources.fonts.instance_keys.delete_key(&font) {
+ self.delete_font_instance(shared_key);
+ }
+ if let Some(ref mut handler) = &mut self.blob_image_handler {
+ handler.delete_font_instance(font);
+ }
+ }
+ ResourceUpdate::SetBlobImageVisibleArea(key, area) => {
+ self.discard_tiles_outside_visible_area(key, &area);
+ self.set_image_visible_rect(key.as_image(), &area);
+ }
+ ResourceUpdate::AddFont(font) => {
+ // The shared key was already added in ApiResources, but the first time it is
+ // seen on the backend we still need to do some extra initialization here.
+ let (key, template) = match font {
+ AddFont::Raw(key, bytes, index) => {
+ (key, FontTemplate::Raw(bytes, index))
+ }
+ AddFont::Native(key, native_font_handle) => {
+ (key, FontTemplate::Native(native_font_handle))
+ }
+ };
+ let shared_key = self.resources.fonts.font_keys.map_key(&key);
+ if !self.glyph_rasterizer.has_font(shared_key) {
+ self.add_font_template(shared_key, template);
+ profile.set(profiler::FONT_TEMPLATES, self.resources.fonts.templates.len());
+ profile.set(profiler::FONT_TEMPLATES_MEM, bytes_to_mb(self.font_templates_memory));
+ }
+ }
+ ResourceUpdate::AddFontInstance(..) => {
+ // Already added in ApiResources.
+ }
+ }
+ }
+ }
+
+ pub fn add_rasterized_blob_images(
+ &mut self,
+ images: Vec<(BlobImageRequest, BlobImageResult)>,
+ profile: &mut TransactionProfile,
+ ) {
+ for (request, result) in images {
+ let data = match result {
+ Ok(data) => data,
+ Err(..) => {
+ warn!("Failed to rasterize a blob image");
+ continue;
+ }
+ };
+
+ profile.add(profiler::RASTERIZED_BLOBS_PX, data.rasterized_rect.area());
+
+ // First make sure we have an entry for this key (using a placeholder
+ // if need be).
+ let tiles = self.rasterized_blob_images.entry(request.key).or_insert_with(
+ || { RasterizedBlob::default() }
+ );
+
+ tiles.insert(request.tile, data);
+
+ match self.cached_images.try_get_mut(&request.key.as_image()) {
+ Some(&mut ImageResult::Multi(ref mut entries)) => {
+ let cached_key = CachedImageKey {
+ rendering: ImageRendering::Auto, // TODO(nical)
+ tile: Some(request.tile),
+ };
+ if let Some(entry) = entries.try_get_mut(&cached_key) {
+ entry.dirty_rect = DirtyRect::All;
+ }
+ }
+ _ => {}
+ }
+ }
+ }
+
+ pub fn add_font_template(&mut self, font_key: FontKey, template: FontTemplate) {
+ // Push the new font to the font renderer, and also store
+ // it locally for glyph metric requests.
+ if let FontTemplate::Raw(ref data, _) = template {
+ self.resources.weak_fonts.insert(Arc::downgrade(data));
+ self.font_templates_memory += data.len();
+ }
+ self.glyph_rasterizer.add_font(font_key, template.clone());
+ self.resources.fonts.templates.add_font(font_key, template);
+ }
+
+ pub fn delete_font_template(&mut self, font_key: FontKey) {
+ self.glyph_rasterizer.delete_font(font_key);
+ if let Some(FontTemplate::Raw(data, _)) = self.resources.fonts.templates.delete_font(&font_key) {
+ self.font_templates_memory -= data.len();
+ }
+ self.cached_glyphs.delete_fonts(&[font_key]);
+ }
+
+ pub fn delete_font_instance(&mut self, instance_key: FontInstanceKey) {
+ self.resources.fonts.instances.delete_font_instance(instance_key);
+ }
+
+ pub fn get_font_instance(&self, instance_key: FontInstanceKey) -> Option<Arc<BaseFontInstance>> {
+ self.resources.fonts.instances.get_font_instance(instance_key)
+ }
+
+ pub fn get_fonts(&self) -> SharedFontResources {
+ self.resources.fonts.clone()
+ }
+
+ pub fn add_image_template(
+ &mut self,
+ image_key: ImageKey,
+ descriptor: ImageDescriptor,
+ data: CachedImageData,
+ visible_rect: &DeviceIntRect,
+ mut tiling: Option<TileSize>,
+ ) {
+ if let Some(ref mut tile_size) = tiling {
+ // Sanitize the value since it can be set by a pref.
+ *tile_size = (*tile_size).max(16).min(2048);
+ }
+
+ if tiling.is_none() && Self::should_tile(self.tiling_threshold(), &descriptor, &data) {
+ // We aren't going to be able to upload a texture this big, so tile it, even
+ // if tiling was not requested.
+ tiling = Some(DEFAULT_TILE_SIZE);
+ }
+
+ let resource = ImageResource {
+ descriptor,
+ data,
+ tiling,
+ visible_rect: *visible_rect,
+ generation: ImageGeneration(0),
+ };
+
+ self.resources.image_templates.insert(image_key, resource);
+ }
+
+ pub fn update_image_template(
+ &mut self,
+ image_key: ImageKey,
+ descriptor: ImageDescriptor,
+ data: CachedImageData,
+ dirty_rect: &ImageDirtyRect,
+ ) {
+ let tiling_threshold = self.tiling_threshold();
+ let image = match self.resources.image_templates.get_mut(image_key) {
+ Some(res) => res,
+ None => panic!("Attempt to update non-existent image"),
+ };
+
+ let mut tiling = image.tiling;
+ if tiling.is_none() && Self::should_tile(tiling_threshold, &descriptor, &data) {
+ tiling = Some(DEFAULT_TILE_SIZE);
+ }
+
+ // Each cache entry stores its own copy of the image's dirty rect. This allows them to be
+ // updated independently.
+ match self.cached_images.try_get_mut(&image_key) {
+ Some(&mut ImageResult::UntiledAuto(ref mut entry)) => {
+ entry.dirty_rect = entry.dirty_rect.union(dirty_rect);
+ }
+ Some(&mut ImageResult::Multi(ref mut entries)) => {
+ for (key, entry) in entries.iter_mut() {
+ // We want the dirty rect relative to the tile and not the whole image.
+ let local_dirty_rect = match (tiling, key.tile) {
+ (Some(tile_size), Some(tile)) => {
+ dirty_rect.map(|mut rect|{
+ let tile_offset = DeviceIntPoint::new(
+ tile.x as i32,
+ tile.y as i32,
+ ) * tile_size as i32;
+ rect = rect.translate(-tile_offset.to_vector());
+
+ let tile_rect = compute_tile_size(
+ &descriptor.size.into(),
+ tile_size,
+ tile,
+ ).into();
+
+ rect.intersection(&tile_rect).unwrap_or_else(DeviceIntRect::zero)
+ })
+ }
+ (None, Some(..)) => DirtyRect::All,
+ _ => *dirty_rect,
+ };
+ entry.dirty_rect = entry.dirty_rect.union(&local_dirty_rect);
+ }
+ }
+ _ => {}
+ }
+
+ if image.descriptor.format != descriptor.format {
+ // could be a stronger warning/error?
+ trace!("Format change {:?} -> {:?}", image.descriptor.format, descriptor.format);
+ }
+ *image = ImageResource {
+ descriptor,
+ data,
+ tiling,
+ visible_rect: descriptor.size.into(),
+ generation: ImageGeneration(image.generation.0 + 1),
+ };
+ }
+
+ pub fn delete_image_template(&mut self, image_key: ImageKey) {
+ // Remove the template.
+ let value = self.resources.image_templates.remove(image_key);
+
+ // Release the corresponding texture cache entry, if any.
+ if let Some(mut cached) = self.cached_images.remove(&image_key) {
+ cached.drop_from_cache(&mut self.texture_cache);
+ }
+
+ match value {
+ Some(image) => if image.data.is_blob() {
+ if let CachedImageData::Raw(data) = image.data {
+ self.image_templates_memory -= data.len();
+ }
+
+ let blob_key = BlobImageKey(image_key);
+ self.deleted_blob_keys.back_mut().unwrap().push(blob_key);
+ self.rasterized_blob_images.remove(&blob_key);
+ },
+ None => {
+ warn!("Delete the non-exist key");
+ debug!("key={:?}", image_key);
+ }
+ }
+ }
+
+ /// Return the current generation of an image template
+ pub fn get_image_generation(&self, key: ImageKey) -> ImageGeneration {
+ self.resources
+ .image_templates
+ .get(key)
+ .map_or(ImageGeneration::INVALID, |template| template.generation)
+ }
+
+ /// Requests an image to ensure that it will be in the texture cache this frame.
+ ///
+ /// returns the size in device pixel of the image or tile.
+ pub fn request_image(
+ &mut self,
+ request: ImageRequest,
+ gpu_cache: &mut GpuCache,
+ ) -> DeviceIntSize {
+ debug_assert_eq!(self.state, State::AddResources);
+
+ let template = match self.resources.image_templates.get(request.key) {
+ Some(template) => template,
+ None => {
+ warn!("ERROR: Trying to render deleted / non-existent key");
+ debug!("key={:?}", request.key);
+ return DeviceIntSize::zero();
+ }
+ };
+
+ let size = match request.tile {
+ Some(tile) => compute_tile_size(&template.visible_rect, template.tiling.unwrap(), tile),
+ None => template.descriptor.size,
+ };
+
+ // Images that don't use the texture cache can early out.
+ if !template.data.uses_texture_cache() {
+ return size;
+ }
+
+ let side_size =
+ template.tiling.map_or(cmp::max(template.descriptor.size.width, template.descriptor.size.height),
+ |tile_size| tile_size as i32);
+ if side_size > self.texture_cache.max_texture_size() {
+ // The image or tiling size is too big for hardware texture size.
+ warn!("Dropping image, image:(w:{},h:{}, tile:{}) is too big for hardware!",
+ template.descriptor.size.width, template.descriptor.size.height, template.tiling.unwrap_or(0));
+ self.cached_images.insert(request.key, ImageResult::Err(ImageCacheError::OverLimitSize));
+ return DeviceIntSize::zero();
+ }
+
+ let storage = match self.cached_images.entry(request.key) {
+ Occupied(e) => {
+ // We might have an existing untiled entry, and need to insert
+ // a second entry. In such cases we need to move the old entry
+ // out first, replacing it with a dummy entry, and then creating
+ // the tiled/multi-entry variant.
+ let entry = e.into_mut();
+ if !request.is_untiled_auto() {
+ let untiled_entry = match entry {
+ &mut ImageResult::UntiledAuto(ref mut entry) => {
+ Some(mem::replace(entry, CachedImageInfo {
+ texture_cache_handle: TextureCacheHandle::invalid(),
+ dirty_rect: DirtyRect::All,
+ manual_eviction: false,
+ }))
+ }
+ _ => None
+ };
+
+ if let Some(untiled_entry) = untiled_entry {
+ let mut entries = ResourceClassCache::new();
+ let untiled_key = CachedImageKey {
+ rendering: ImageRendering::Auto,
+ tile: None,
+ };
+ entries.insert(untiled_key, untiled_entry);
+ *entry = ImageResult::Multi(entries);
+ }
+ }
+ entry
+ }
+ Vacant(entry) => {
+ entry.insert(if request.is_untiled_auto() {
+ ImageResult::UntiledAuto(CachedImageInfo {
+ texture_cache_handle: TextureCacheHandle::invalid(),
+ dirty_rect: DirtyRect::All,
+ manual_eviction: false,
+ })
+ } else {
+ ImageResult::Multi(ResourceClassCache::new())
+ })
+ }
+ };
+
+ // If this image exists in the texture cache, *and* the dirty rect
+ // in the cache is empty, then it is valid to use as-is.
+ let entry = match *storage {
+ ImageResult::UntiledAuto(ref mut entry) => entry,
+ ImageResult::Multi(ref mut entries) => {
+ entries.entry(request.into())
+ .or_insert(CachedImageInfo {
+ texture_cache_handle: TextureCacheHandle::invalid(),
+ dirty_rect: DirtyRect::All,
+ manual_eviction: false,
+ })
+ },
+ ImageResult::Err(_) => panic!("Errors should already have been handled"),
+ };
+
+ let needs_upload = self.texture_cache.request(&entry.texture_cache_handle, gpu_cache);
+
+ if !needs_upload && entry.dirty_rect.is_empty() {
+ return size;
+ }
+
+ if !self.pending_image_requests.insert(request) {
+ return size;
+ }
+
+ if template.data.is_blob() {
+ let request: BlobImageRequest = request.into();
+ let missing = match self.rasterized_blob_images.get(&request.key) {
+ Some(tiles) => !tiles.contains_key(&request.tile),
+ _ => true,
+ };
+
+ assert!(!missing);
+ }
+
+ size
+ }
+
+ fn discard_tiles_outside_visible_area(
+ &mut self,
+ key: BlobImageKey,
+ area: &DeviceIntRect
+ ) {
+ let tile_size = match self.resources.image_templates.get(key.as_image()) {
+ Some(template) => template.tiling.unwrap(),
+ None => {
+ //debug!("Missing image template (key={:?})!", key);
+ return;
+ }
+ };
+
+ let tiles = match self.rasterized_blob_images.get_mut(&key) {
+ Some(tiles) => tiles,
+ _ => { return; }
+ };
+
+ let tile_range = compute_tile_range(
+ &area,
+ tile_size,
+ );
+
+ tiles.retain(|tile, _| { tile_range.contains(*tile) });
+
+ let texture_cache = &mut self.texture_cache;
+ match self.cached_images.try_get_mut(&key.as_image()) {
+ Some(&mut ImageResult::Multi(ref mut entries)) => {
+ entries.retain(|key, entry| {
+ if key.tile.is_none() || tile_range.contains(key.tile.unwrap()) {
+ return true;
+ }
+ entry.mark_unused(texture_cache);
+ return false;
+ });
+ }
+ _ => {}
+ }
+ }
+
+ fn set_image_visible_rect(&mut self, key: ImageKey, rect: &DeviceIntRect) {
+ if let Some(image) = self.resources.image_templates.get_mut(key) {
+ image.visible_rect = *rect;
+ image.descriptor.size = rect.size();
+ }
+ }
+
+ pub fn request_glyphs(
+ &mut self,
+ mut font: FontInstance,
+ glyph_keys: &[GlyphKey],
+ gpu_cache: &mut GpuCache,
+ ) {
+ debug_assert_eq!(self.state, State::AddResources);
+
+ self.glyph_rasterizer.prepare_font(&mut font);
+ let glyph_key_cache = self.cached_glyphs.insert_glyph_key_cache_for_font(&font);
+ let texture_cache = &mut self.texture_cache;
+ self.glyph_rasterizer.request_glyphs(
+ font,
+ glyph_keys,
+ |key| {
+ if let Some(entry) = glyph_key_cache.try_get(key) {
+ match entry {
+ GlyphCacheEntry::Cached(ref glyph) => {
+ // Skip the glyph if it is already has a valid texture cache handle.
+ if !texture_cache.request(&glyph.texture_cache_handle, gpu_cache) {
+ return false;
+ }
+ // This case gets hit when we already rasterized the glyph, but the
+ // glyph has been evicted from the texture cache. Just force it to
+ // pending so it gets rematerialized.
+ }
+ // Otherwise, skip the entry if it is blank or pending.
+ GlyphCacheEntry::Blank | GlyphCacheEntry::Pending => return false,
+ }
+ };
+
+ glyph_key_cache.add_glyph(*key, GlyphCacheEntry::Pending);
+
+ true
+ }
+ );
+ }
+
+ pub fn pending_updates(&mut self) -> ResourceUpdateList {
+ ResourceUpdateList {
+ texture_updates: self.texture_cache.pending_updates(),
+ native_surface_updates: mem::replace(&mut self.pending_native_surface_updates, Vec::new()),
+ }
+ }
+
+ pub fn fetch_glyphs<F>(
+ &self,
+ mut font: FontInstance,
+ glyph_keys: &[GlyphKey],
+ fetch_buffer: &mut Vec<GlyphFetchResult>,
+ gpu_cache: &mut GpuCache,
+ mut f: F,
+ ) where
+ F: FnMut(TextureSource, GlyphFormat, &[GlyphFetchResult]),
+ {
+ debug_assert_eq!(self.state, State::QueryResources);
+
+ self.glyph_rasterizer.prepare_font(&mut font);
+ let glyph_key_cache = self.cached_glyphs.get_glyph_key_cache_for_font(&font);
+
+ let mut current_texture_id = TextureSource::Invalid;
+ let mut current_glyph_format = GlyphFormat::Subpixel;
+ debug_assert!(fetch_buffer.is_empty());
+
+ for (loop_index, key) in glyph_keys.iter().enumerate() {
+ let (cache_item, glyph_format) = match *glyph_key_cache.get(key) {
+ GlyphCacheEntry::Cached(ref glyph) => {
+ (self.texture_cache.get(&glyph.texture_cache_handle), glyph.format)
+ }
+ GlyphCacheEntry::Blank | GlyphCacheEntry::Pending => continue,
+ };
+ if current_texture_id != cache_item.texture_id ||
+ current_glyph_format != glyph_format {
+ if !fetch_buffer.is_empty() {
+ f(current_texture_id, current_glyph_format, fetch_buffer);
+ fetch_buffer.clear();
+ }
+ current_texture_id = cache_item.texture_id;
+ current_glyph_format = glyph_format;
+ }
+ fetch_buffer.push(GlyphFetchResult {
+ index_in_text_run: loop_index as i32,
+ uv_rect_address: gpu_cache.get_address(&cache_item.uv_rect_handle),
+ offset: DevicePoint::new(cache_item.user_data[0], cache_item.user_data[1]),
+ size: cache_item.uv_rect.size(),
+ scale: cache_item.user_data[2],
+ });
+ }
+
+ if !fetch_buffer.is_empty() {
+ f(current_texture_id, current_glyph_format, fetch_buffer);
+ fetch_buffer.clear();
+ }
+ }
+
+ pub fn map_font_key(&self, key: FontKey) -> FontKey {
+ self.resources.fonts.font_keys.map_key(&key)
+ }
+
+ pub fn map_font_instance_key(&self, key: FontInstanceKey) -> FontInstanceKey {
+ self.resources.fonts.instance_keys.map_key(&key)
+ }
+
+ pub fn get_glyph_dimensions(
+ &mut self,
+ font: &FontInstance,
+ glyph_index: GlyphIndex,
+ ) -> Option<GlyphDimensions> {
+ match self.cached_glyph_dimensions.entry((font.instance_key, glyph_index)) {
+ Occupied(entry) => *entry.get(),
+ Vacant(entry) => *entry.insert(
+ self.glyph_rasterizer
+ .get_glyph_dimensions(font, glyph_index),
+ ),
+ }
+ }
+
+ pub fn get_glyph_index(&mut self, font_key: FontKey, ch: char) -> Option<u32> {
+ self.glyph_rasterizer.get_glyph_index(font_key, ch)
+ }
+
+ #[inline]
+ pub fn get_cached_image(&self, request: ImageRequest) -> Result<CacheItem, ()> {
+ debug_assert_eq!(self.state, State::QueryResources);
+ let image_info = self.get_image_info(request)?;
+ Ok(self.get_texture_cache_item(&image_info.texture_cache_handle))
+ }
+
+ pub fn get_cached_render_task(
+ &self,
+ handle: &RenderTaskCacheEntryHandle,
+ ) -> &RenderTaskCacheEntry {
+ self.cached_render_tasks.get_cache_entry(handle)
+ }
+
+ #[inline]
+ fn get_image_info(&self, request: ImageRequest) -> Result<&CachedImageInfo, ()> {
+ // TODO(Jerry): add a debug option to visualize the corresponding area for
+ // the Err() case of CacheItem.
+ match *self.cached_images.get(&request.key) {
+ ImageResult::UntiledAuto(ref image_info) => Ok(image_info),
+ ImageResult::Multi(ref entries) => Ok(entries.get(&request.into())),
+ ImageResult::Err(_) => Err(()),
+ }
+ }
+
+ #[inline]
+ pub fn get_texture_cache_item(&self, handle: &TextureCacheHandle) -> CacheItem {
+ self.texture_cache.get(handle)
+ }
+
+ pub fn get_image_properties(&self, image_key: ImageKey) -> Option<ImageProperties> {
+ let image_template = &self.resources.image_templates.get(image_key);
+
+ image_template.map(|image_template| {
+ let external_image = match image_template.data {
+ CachedImageData::External(ext_image) => match ext_image.image_type {
+ ExternalImageType::TextureHandle(_) => Some(ext_image),
+ // external buffer uses resource_cache.
+ ExternalImageType::Buffer => None,
+ },
+ // raw and blob image are all using resource_cache.
+ CachedImageData::Raw(..) | CachedImageData::Blob => None,
+ };
+
+ ImageProperties {
+ descriptor: image_template.descriptor,
+ external_image,
+ tiling: image_template.tiling,
+ visible_rect: image_template.visible_rect,
+ }
+ })
+ }
+
+ pub fn begin_frame(&mut self, stamp: FrameStamp, gpu_cache: &mut GpuCache, profile: &mut TransactionProfile) {
+ profile_scope!("begin_frame");
+ debug_assert_eq!(self.state, State::Idle);
+ self.state = State::AddResources;
+ self.texture_cache.begin_frame(stamp, profile);
+ self.picture_textures.begin_frame(stamp, &mut self.texture_cache.pending_updates);
+
+ self.cached_glyphs.begin_frame(
+ stamp,
+ &mut self.texture_cache,
+ &mut self.glyph_rasterizer,
+ );
+ self.cached_render_tasks.begin_frame(&mut self.texture_cache);
+ self.current_frame_id = stamp.frame_id();
+
+ // pop the old frame and push a new one
+ self.deleted_blob_keys.pop_front();
+ self.deleted_blob_keys.push_back(Vec::new());
+
+ self.texture_cache.run_compaction(gpu_cache);
+ }
+
+ pub fn block_until_all_resources_added(
+ &mut self,
+ gpu_cache: &mut GpuCache,
+ profile: &mut TransactionProfile,
+ ) {
+ profile_scope!("block_until_all_resources_added");
+
+ debug_assert_eq!(self.state, State::AddResources);
+ self.state = State::QueryResources;
+
+ let cached_glyphs = &mut self.cached_glyphs;
+ let texture_cache = &mut self.texture_cache;
+
+ self.glyph_rasterizer.resolve_glyphs(
+ |job, can_use_r8_format| {
+ let GlyphRasterJob { font, key, result } = job;
+ let glyph_key_cache = cached_glyphs.get_glyph_key_cache_for_font_mut(&*font);
+ let glyph_info = match result {
+ Err(_) => GlyphCacheEntry::Blank,
+ Ok(ref glyph) if glyph.width == 0 || glyph.height == 0 => {
+ GlyphCacheEntry::Blank
+ }
+ Ok(glyph) => {
+ let mut texture_cache_handle = TextureCacheHandle::invalid();
+ texture_cache.request(&texture_cache_handle, gpu_cache);
+ texture_cache.update(
+ &mut texture_cache_handle,
+ ImageDescriptor {
+ size: size2(glyph.width, glyph.height),
+ stride: None,
+ format: glyph.format.image_format(can_use_r8_format),
+ flags: ImageDescriptorFlags::empty(),
+ offset: 0,
+ },
+ TextureFilter::Linear,
+ Some(CachedImageData::Raw(Arc::new(glyph.bytes))),
+ [glyph.left, -glyph.top, glyph.scale, 0.0],
+ DirtyRect::All,
+ gpu_cache,
+ Some(glyph_key_cache.eviction_notice()),
+ UvRectKind::Rect,
+ Eviction::Auto,
+ TargetShader::Text,
+ );
+ GlyphCacheEntry::Cached(CachedGlyphInfo {
+ texture_cache_handle,
+ format: glyph.format,
+ })
+ }
+ };
+ glyph_key_cache.insert(key, glyph_info);
+ },
+ profile,
+ );
+
+ // Apply any updates of new / updated images (incl. blobs) to the texture cache.
+ self.update_texture_cache(gpu_cache);
+ }
+
+ fn update_texture_cache(&mut self, gpu_cache: &mut GpuCache) {
+ profile_scope!("update_texture_cache");
+ for request in self.pending_image_requests.drain() {
+ let image_template = self.resources.image_templates.get_mut(request.key).unwrap();
+ debug_assert!(image_template.data.uses_texture_cache());
+
+ let mut updates: SmallVec<[(CachedImageData, Option<DeviceIntRect>); 1]> = SmallVec::new();
+
+ match image_template.data {
+ CachedImageData::Raw(..) | CachedImageData::External(..) => {
+ // Safe to clone here since the Raw image data is an
+ // Arc, and the external image data is small.
+ updates.push((image_template.data.clone(), None));
+ }
+ CachedImageData::Blob => {
+ let blob_image = self.rasterized_blob_images.get_mut(&BlobImageKey(request.key)).unwrap();
+ let img = &blob_image[&request.tile.unwrap()];
+ updates.push((
+ CachedImageData::Raw(Arc::clone(&img.data)),
+ Some(img.rasterized_rect)
+ ));
+ }
+ };
+
+ for (image_data, blob_rasterized_rect) in updates {
+ let entry = match *self.cached_images.get_mut(&request.key) {
+ ImageResult::UntiledAuto(ref mut entry) => entry,
+ ImageResult::Multi(ref mut entries) => entries.get_mut(&request.into()),
+ ImageResult::Err(_) => panic!("Update requested for invalid entry")
+ };
+
+ let mut descriptor = image_template.descriptor.clone();
+ let mut dirty_rect = entry.dirty_rect.replace_with_empty();
+
+ if let Some(tile) = request.tile {
+ let tile_size = image_template.tiling.unwrap();
+ let clipped_tile_size = compute_tile_size(&image_template.visible_rect, tile_size, tile);
+ // The tiled image could be stored on the CPU as one large image or be
+ // already broken up into tiles. This affects the way we compute the stride
+ // and offset.
+ let tiled_on_cpu = image_template.data.is_blob();
+ if !tiled_on_cpu {
+ // we don't expect to have partial tiles at the top and left of non-blob
+ // images.
+ debug_assert_eq!(image_template.visible_rect.min, point2(0, 0));
+ let bpp = descriptor.format.bytes_per_pixel();
+ let stride = descriptor.compute_stride();
+ descriptor.stride = Some(stride);
+ descriptor.offset +=
+ tile.y as i32 * tile_size as i32 * stride +
+ tile.x as i32 * tile_size as i32 * bpp;
+ }
+
+ descriptor.size = clipped_tile_size;
+ }
+
+ // If we are uploading the dirty region of a blob image we might have several
+ // rects to upload so we use each of these rasterized rects rather than the
+ // overall dirty rect of the image.
+ if let Some(rect) = blob_rasterized_rect {
+ dirty_rect = DirtyRect::Partial(rect);
+ }
+
+ let filter = match request.rendering {
+ ImageRendering::Pixelated => {
+ TextureFilter::Nearest
+ }
+ ImageRendering::Auto | ImageRendering::CrispEdges => {
+ // If the texture uses linear filtering, enable mipmaps and
+ // trilinear filtering, for better image quality. We only
+ // support this for now on textures that are not placed
+ // into the shared cache. This accounts for any image
+ // that is > 512 in either dimension, so it should cover
+ // the most important use cases. We may want to support
+ // mip-maps on shared cache items in the future.
+ if descriptor.allow_mipmaps() &&
+ descriptor.size.width > 512 &&
+ descriptor.size.height > 512 &&
+ !self.texture_cache.is_allowed_in_shared_cache(
+ TextureFilter::Linear,
+ &descriptor,
+ ) {
+ TextureFilter::Trilinear
+ } else {
+ TextureFilter::Linear
+ }
+ }
+ };
+
+ let eviction = if image_template.data.is_blob() {
+ entry.manual_eviction = true;
+ Eviction::Manual
+ } else {
+ Eviction::Auto
+ };
+
+ //Note: at this point, the dirty rectangle is local to the descriptor space
+ self.texture_cache.update(
+ &mut entry.texture_cache_handle,
+ descriptor,
+ filter,
+ Some(image_data),
+ [0.0; 4],
+ dirty_rect,
+ gpu_cache,
+ None,
+ UvRectKind::Rect,
+ eviction,
+ TargetShader::Default,
+ );
+ }
+ }
+ }
+
+ pub fn create_compositor_backdrop_surface(
+ &mut self,
+ color: ColorF
+ ) -> NativeSurfaceId {
+ let id = NativeSurfaceId(NEXT_NATIVE_SURFACE_ID.fetch_add(1, Ordering::Relaxed) as u64);
+
+ self.pending_native_surface_updates.push(
+ NativeSurfaceOperation {
+ details: NativeSurfaceOperationDetails::CreateBackdropSurface {
+ id,
+ color,
+ },
+ }
+ );
+
+ id
+ }
+
+ /// Queue up allocation of a new OS native compositor surface with the
+ /// specified tile size.
+ pub fn create_compositor_surface(
+ &mut self,
+ virtual_offset: DeviceIntPoint,
+ tile_size: DeviceIntSize,
+ is_opaque: bool,
+ ) -> NativeSurfaceId {
+ let id = NativeSurfaceId(NEXT_NATIVE_SURFACE_ID.fetch_add(1, Ordering::Relaxed) as u64);
+
+ self.pending_native_surface_updates.push(
+ NativeSurfaceOperation {
+ details: NativeSurfaceOperationDetails::CreateSurface {
+ id,
+ virtual_offset,
+ tile_size,
+ is_opaque,
+ },
+ }
+ );
+
+ id
+ }
+
+ pub fn create_compositor_external_surface(
+ &mut self,
+ is_opaque: bool,
+ ) -> NativeSurfaceId {
+ let id = NativeSurfaceId(NEXT_NATIVE_SURFACE_ID.fetch_add(1, Ordering::Relaxed) as u64);
+
+ self.pending_native_surface_updates.push(
+ NativeSurfaceOperation {
+ details: NativeSurfaceOperationDetails::CreateExternalSurface {
+ id,
+ is_opaque,
+ },
+ }
+ );
+
+ id
+ }
+
+ /// Queue up destruction of an existing native OS surface. This is used when
+ /// a picture cache surface is dropped or resized.
+ pub fn destroy_compositor_surface(
+ &mut self,
+ id: NativeSurfaceId,
+ ) {
+ self.pending_native_surface_updates.push(
+ NativeSurfaceOperation {
+ details: NativeSurfaceOperationDetails::DestroySurface {
+ id,
+ }
+ }
+ );
+ }
+
+ /// Queue construction of a native compositor tile on a given surface.
+ pub fn create_compositor_tile(
+ &mut self,
+ id: NativeTileId,
+ ) {
+ self.pending_native_surface_updates.push(
+ NativeSurfaceOperation {
+ details: NativeSurfaceOperationDetails::CreateTile {
+ id,
+ },
+ }
+ );
+ }
+
+ /// Queue destruction of a native compositor tile.
+ pub fn destroy_compositor_tile(
+ &mut self,
+ id: NativeTileId,
+ ) {
+ self.pending_native_surface_updates.push(
+ NativeSurfaceOperation {
+ details: NativeSurfaceOperationDetails::DestroyTile {
+ id,
+ },
+ }
+ );
+ }
+
+ pub fn attach_compositor_external_image(
+ &mut self,
+ id: NativeSurfaceId,
+ external_image: ExternalImageId,
+ ) {
+ self.pending_native_surface_updates.push(
+ NativeSurfaceOperation {
+ details: NativeSurfaceOperationDetails::AttachExternalImage {
+ id,
+ external_image,
+ },
+ }
+ );
+ }
+
+
+ pub fn end_frame(&mut self, profile: &mut TransactionProfile) {
+ debug_assert_eq!(self.state, State::QueryResources);
+ profile_scope!("end_frame");
+ self.state = State::Idle;
+
+ // GC the render target pool, if it's currently > 64 MB in size.
+ //
+ // We use a simple scheme whereby we drop any texture that hasn't been used
+ // in the last 60 frames, until we are below the size threshold. This should
+ // generally prevent any sustained build-up of unused textures, unless we don't
+ // generate frames for a long period. This can happen when the window is
+ // minimized, and we probably want to flush all the WebRender caches in that case [1].
+ // There is also a second "red line" memory threshold which prevents
+ // memory exhaustion if many render targets are allocated within a small
+ // number of frames. For now this is set at 320 MB (10x the normal memory threshold).
+ //
+ // [1] https://bugzilla.mozilla.org/show_bug.cgi?id=1494099
+ self.gc_render_targets(
+ 64 * 1024 * 1024,
+ 32 * 1024 * 1024 * 10,
+ 60,
+ );
+
+ self.texture_cache.end_frame(profile);
+ self.picture_textures.gc(
+ &mut self.texture_cache.pending_updates,
+ );
+
+ self.picture_textures.update_profile(profile);
+ }
+
+ pub fn set_debug_flags(&mut self, flags: DebugFlags) {
+ GLYPH_FLASHING.store(flags.contains(DebugFlags::GLYPH_FLASHING), std::sync::atomic::Ordering::Relaxed);
+ self.texture_cache.set_debug_flags(flags);
+ self.picture_textures.set_debug_flags(flags);
+ }
+
+ pub fn clear(&mut self, what: ClearCache) {
+ if what.contains(ClearCache::IMAGES) {
+ for (_key, mut cached) in self.cached_images.resources.drain() {
+ cached.drop_from_cache(&mut self.texture_cache);
+ }
+ }
+ if what.contains(ClearCache::GLYPHS) {
+ self.cached_glyphs.clear();
+ }
+ if what.contains(ClearCache::GLYPH_DIMENSIONS) {
+ self.cached_glyph_dimensions.clear();
+ }
+ if what.contains(ClearCache::RENDER_TASKS) {
+ self.cached_render_tasks.clear();
+ }
+ if what.contains(ClearCache::TEXTURE_CACHE) {
+ self.texture_cache.clear_all();
+ self.picture_textures.clear(&mut self.texture_cache.pending_updates);
+ }
+ if what.contains(ClearCache::RENDER_TARGETS) {
+ self.clear_render_target_pool();
+ }
+ }
+
+ pub fn clear_namespace(&mut self, namespace: IdNamespace) {
+ self.clear_images(|k| k.0 == namespace);
+
+ // First clear out any non-shared resources associated with the namespace.
+ self.resources.fonts.instances.clear_namespace(namespace);
+ let deleted_keys = self.resources.fonts.templates.clear_namespace(namespace);
+ self.glyph_rasterizer.delete_fonts(&deleted_keys);
+ self.cached_glyphs.clear_namespace(namespace);
+ if let Some(handler) = &mut self.blob_image_handler {
+ handler.clear_namespace(namespace);
+ }
+
+ // Check for any shared instance keys that were remapped from the namespace.
+ let shared_instance_keys = self.resources.fonts.instance_keys.clear_namespace(namespace);
+ if !shared_instance_keys.is_empty() {
+ self.resources.fonts.instances.delete_font_instances(&shared_instance_keys);
+ self.cached_glyphs.delete_font_instances(&shared_instance_keys, &mut self.glyph_rasterizer);
+ // Blob font instances are not shared across namespaces, so there is no
+ // need to call the handler for them individually.
+ }
+
+ // Finally check for any shared font keys that were remapped from the namespace.
+ let shared_keys = self.resources.fonts.font_keys.clear_namespace(namespace);
+ if !shared_keys.is_empty() {
+ self.glyph_rasterizer.delete_fonts(&shared_keys);
+ self.resources.fonts.templates.delete_fonts(&shared_keys);
+ self.cached_glyphs.delete_fonts(&shared_keys);
+ if let Some(handler) = &mut self.blob_image_handler {
+ for &key in &shared_keys {
+ handler.delete_font(key);
+ }
+ }
+ }
+ }
+
+ /// Reports the CPU heap usage of this ResourceCache.
+ ///
+ /// NB: It would be much better to use the derive(MallocSizeOf) machinery
+ /// here, but the Arcs complicate things. The two ways to handle that would
+ /// be to either (a) Implement MallocSizeOf manually for the things that own
+ /// them and manually avoid double-counting, or (b) Use the "seen this pointer
+ /// yet" machinery from the proper malloc_size_of crate. We can do this if/when
+ /// more accurate memory reporting on these resources becomes a priority.
+ pub fn report_memory(&self, op: VoidPtrToSizeFn) -> MemoryReport {
+ let mut report = MemoryReport::default();
+
+ let mut seen_fonts = std::collections::HashSet::new();
+ // Measure fonts. We only need the templates here, because the instances
+ // don't have big buffers.
+ for (_, font) in self.resources.fonts.templates.lock().iter() {
+ if let FontTemplate::Raw(ref raw, _) = font {
+ report.fonts += unsafe { op(raw.as_ptr() as *const c_void) };
+ seen_fonts.insert(raw.as_ptr());
+ }
+ }
+
+ for font in self.resources.weak_fonts.iter() {
+ if !seen_fonts.contains(&font.as_ptr()) {
+ report.weak_fonts += unsafe { op(font.as_ptr() as *const c_void) };
+ }
+ }
+
+ // Measure images.
+ for (_, image) in self.resources.image_templates.images.iter() {
+ report.images += match image.data {
+ CachedImageData::Raw(ref v) => unsafe { op(v.as_ptr() as *const c_void) },
+ CachedImageData::Blob | CachedImageData::External(..) => 0,
+ }
+ }
+
+ // Mesure rasterized blobs.
+ // TODO(gw): Temporarily disabled while we roll back a crash. We can re-enable
+ // these when that crash is fixed.
+ /*
+ for (_, image) in self.rasterized_blob_images.iter() {
+ let mut accumulate = |b: &RasterizedBlobImage| {
+ report.rasterized_blobs += unsafe { op(b.data.as_ptr() as *const c_void) };
+ };
+ match image {
+ RasterizedBlob::Tiled(map) => map.values().for_each(&mut accumulate),
+ RasterizedBlob::NonTiled(vec) => vec.iter().for_each(&mut accumulate),
+ };
+ }
+ */
+
+ report
+ }
+
+ /// Properly deletes all images matching the predicate.
+ fn clear_images<F: Fn(&ImageKey) -> bool>(&mut self, f: F) {
+ let keys = self.resources.image_templates.images.keys().filter(|k| f(*k))
+ .cloned().collect::<SmallVec<[ImageKey; 16]>>();
+
+ for key in keys {
+ self.delete_image_template(key);
+ }
+
+ #[cfg(features="leak_checks")]
+ let check_leaks = true;
+ #[cfg(not(features="leak_checks"))]
+ let check_leaks = false;
+
+ if check_leaks {
+ let blob_f = |key: &BlobImageKey| { f(&key.as_image()) };
+ assert!(!self.resources.image_templates.images.keys().any(&f));
+ assert!(!self.cached_images.resources.keys().any(&f));
+ assert!(!self.rasterized_blob_images.keys().any(&blob_f));
+ }
+ }
+
+ /// Get a render target from the pool, or allocate a new one if none are
+ /// currently available that match the requested parameters.
+ pub fn get_or_create_render_target_from_pool(
+ &mut self,
+ size: DeviceIntSize,
+ format: ImageFormat,
+ ) -> CacheTextureId {
+ for target in &mut self.render_target_pool {
+ if target.size == size &&
+ target.format == format &&
+ !target.is_active {
+ // Found a target that's not currently in use which matches. Update
+ // the last_frame_used for GC purposes.
+ target.is_active = true;
+ target.last_frame_used = self.current_frame_id;
+ return target.texture_id;
+ }
+ }
+
+ // Need to create a new render target and add it to the pool
+
+ let texture_id = self.texture_cache.alloc_render_target(
+ size,
+ format,
+ );
+
+ self.render_target_pool.push(RenderTarget {
+ size,
+ format,
+ texture_id,
+ is_active: true,
+ last_frame_used: self.current_frame_id,
+ });
+
+ texture_id
+ }
+
+ /// Return a render target to the pool.
+ pub fn return_render_target_to_pool(
+ &mut self,
+ id: CacheTextureId,
+ ) {
+ let target = self.render_target_pool
+ .iter_mut()
+ .find(|t| t.texture_id == id)
+ .expect("bug: invalid render target id");
+
+ assert!(target.is_active);
+ target.is_active = false;
+ }
+
+ /// Clear all current render targets (e.g. on memory pressure)
+ fn clear_render_target_pool(
+ &mut self,
+ ) {
+ for target in self.render_target_pool.drain(..) {
+ debug_assert!(!target.is_active);
+ self.texture_cache.free_render_target(target.texture_id);
+ }
+ }
+
+ /// Garbage collect and remove old render targets from the pool that haven't
+ /// been used for some time.
+ fn gc_render_targets(
+ &mut self,
+ total_bytes_threshold: usize,
+ total_bytes_red_line_threshold: usize,
+ frames_threshold: u64,
+ ) {
+ // Get the total GPU memory size used by the current render target pool
+ let mut rt_pool_size_in_bytes: usize = self.render_target_pool
+ .iter()
+ .map(|t| t.size_in_bytes())
+ .sum();
+
+ // If the total size of the pool is less than the threshold, don't bother
+ // trying to GC any targets
+ if rt_pool_size_in_bytes <= total_bytes_threshold {
+ return;
+ }
+
+ // Sort the current pool by age, so that we remove oldest textures first
+ self.render_target_pool.sort_by_key(|t| t.last_frame_used);
+
+ // We can't just use retain() because `RenderTarget` requires manual cleanup.
+ let mut retained_targets = SmallVec::<[RenderTarget; 8]>::new();
+
+ for target in self.render_target_pool.drain(..) {
+ assert!(!target.is_active);
+
+ // Drop oldest textures until we are under the allowed size threshold.
+ // However, if it's been used in very recently, it is always kept around,
+ // which ensures we don't thrash texture allocations on pages that do
+ // require a very large render target pool and are regularly changing.
+ let above_red_line = rt_pool_size_in_bytes > total_bytes_red_line_threshold;
+ let above_threshold = rt_pool_size_in_bytes > total_bytes_threshold;
+ let used_recently = target.used_recently(self.current_frame_id, frames_threshold);
+ let used_this_frame = target.last_frame_used == self.current_frame_id;
+
+ if !used_this_frame && (above_red_line || (above_threshold && !used_recently)) {
+ rt_pool_size_in_bytes -= target.size_in_bytes();
+ self.texture_cache.free_render_target(target.texture_id);
+ } else {
+ retained_targets.push(target);
+ }
+ }
+
+ self.render_target_pool.extend(retained_targets);
+ }
+
+ #[cfg(test)]
+ pub fn validate_surfaces(
+ &self,
+ expected_surfaces: &[(i32, i32, ImageFormat)],
+ ) {
+ assert_eq!(expected_surfaces.len(), self.render_target_pool.len());
+
+ for (expected, surface) in expected_surfaces.iter().zip(self.render_target_pool.iter()) {
+ assert_eq!(DeviceIntSize::new(expected.0, expected.1), surface.size);
+ assert_eq!(expected.2, surface.format);
+ }
+ }
+}
+
+impl Drop for ResourceCache {
+ fn drop(&mut self) {
+ self.clear_images(|_| true);
+ }
+}
+
+#[cfg(any(feature = "capture", feature = "replay"))]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct PlainFontTemplate {
+ data: String,
+ index: u32,
+}
+
+#[cfg(any(feature = "capture", feature = "replay"))]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct PlainImageTemplate {
+ data: String,
+ descriptor: ImageDescriptor,
+ tiling: Option<TileSize>,
+ generation: ImageGeneration,
+}
+
+#[cfg(any(feature = "capture", feature = "replay"))]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PlainResources {
+ font_templates: FastHashMap<FontKey, PlainFontTemplate>,
+ font_instances: Vec<BaseFontInstance>,
+ image_templates: FastHashMap<ImageKey, PlainImageTemplate>,
+}
+
+#[cfg(feature = "capture")]
+#[derive(Serialize)]
+pub struct PlainCacheRef<'a> {
+ current_frame_id: FrameId,
+ glyphs: &'a GlyphCache,
+ glyph_dimensions: &'a GlyphDimensionsCache,
+ images: &'a ImageCache,
+ render_tasks: &'a RenderTaskCache,
+ textures: &'a TextureCache,
+ picture_textures: &'a PictureTextures,
+}
+
+#[cfg(feature = "replay")]
+#[derive(Deserialize)]
+pub struct PlainCacheOwn {
+ current_frame_id: FrameId,
+ glyphs: GlyphCache,
+ glyph_dimensions: GlyphDimensionsCache,
+ images: ImageCache,
+ render_tasks: RenderTaskCache,
+ textures: TextureCache,
+ picture_textures: PictureTextures,
+}
+
+#[cfg(feature = "replay")]
+const NATIVE_FONT: &'static [u8] = include_bytes!("../res/Proggy.ttf");
+
+// This currently only casts the unit but will soon apply an offset
+fn to_image_dirty_rect(blob_dirty_rect: &BlobDirtyRect) -> ImageDirtyRect {
+ match *blob_dirty_rect {
+ DirtyRect::Partial(rect) => DirtyRect::Partial(rect.cast_unit()),
+ DirtyRect::All => DirtyRect::All,
+ }
+}
+
+impl ResourceCache {
+ #[cfg(feature = "capture")]
+ pub fn save_capture(
+ &mut self, root: &PathBuf
+ ) -> (PlainResources, Vec<ExternalCaptureImage>) {
+ use std::fs;
+ use std::io::Write;
+
+ info!("saving resource cache");
+ let res = &self.resources;
+ let path_fonts = root.join("fonts");
+ if !path_fonts.is_dir() {
+ fs::create_dir(&path_fonts).unwrap();
+ }
+ let path_images = root.join("images");
+ if !path_images.is_dir() {
+ fs::create_dir(&path_images).unwrap();
+ }
+ let path_blobs = root.join("blobs");
+ if !path_blobs.is_dir() {
+ fs::create_dir(&path_blobs).unwrap();
+ }
+ let path_externals = root.join("externals");
+ if !path_externals.is_dir() {
+ fs::create_dir(&path_externals).unwrap();
+ }
+
+ info!("\tfont templates");
+ let mut font_paths = FastHashMap::default();
+ for template in res.fonts.templates.lock().values() {
+ let data: &[u8] = match *template {
+ FontTemplate::Raw(ref arc, _) => arc,
+ FontTemplate::Native(_) => continue,
+ };
+ let font_id = res.fonts.templates.len() + 1;
+ let entry = match font_paths.entry(data.as_ptr()) {
+ Entry::Occupied(_) => continue,
+ Entry::Vacant(e) => e,
+ };
+ let file_name = format!("{}.raw", font_id);
+ let short_path = format!("fonts/{}", file_name);
+ fs::File::create(path_fonts.join(file_name))
+ .expect(&format!("Unable to create {}", short_path))
+ .write_all(data)
+ .unwrap();
+ entry.insert(short_path);
+ }
+
+ info!("\timage templates");
+ let mut image_paths = FastHashMap::default();
+ let mut other_paths = FastHashMap::default();
+ let mut num_blobs = 0;
+ let mut external_images = Vec::new();
+ for (&key, template) in res.image_templates.images.iter() {
+ let desc = &template.descriptor;
+ match template.data {
+ CachedImageData::Raw(ref arc) => {
+ let image_id = image_paths.len() + 1;
+ let entry = match image_paths.entry(arc.as_ptr()) {
+ Entry::Occupied(_) => continue,
+ Entry::Vacant(e) => e,
+ };
+
+ #[cfg(feature = "png")]
+ CaptureConfig::save_png(
+ root.join(format!("images/{}.png", image_id)),
+ desc.size,
+ desc.format,
+ desc.stride,
+ &arc,
+ );
+ let file_name = format!("{}.raw", image_id);
+ let short_path = format!("images/{}", file_name);
+ fs::File::create(path_images.join(file_name))
+ .expect(&format!("Unable to create {}", short_path))
+ .write_all(&*arc)
+ .unwrap();
+ entry.insert(short_path);
+ }
+ CachedImageData::Blob => {
+ warn!("Tiled blob images aren't supported yet");
+ let result = RasterizedBlobImage {
+ rasterized_rect: desc.size.into(),
+ data: Arc::new(vec![0; desc.compute_total_size() as usize])
+ };
+
+ assert_eq!(result.rasterized_rect.size(), desc.size);
+ assert_eq!(result.data.len(), desc.compute_total_size() as usize);
+
+ num_blobs += 1;
+ #[cfg(feature = "png")]
+ CaptureConfig::save_png(
+ root.join(format!("blobs/{}.png", num_blobs)),
+ desc.size,
+ desc.format,
+ desc.stride,
+ &result.data,
+ );
+ let file_name = format!("{}.raw", num_blobs);
+ let short_path = format!("blobs/{}", file_name);
+ let full_path = path_blobs.clone().join(&file_name);
+ fs::File::create(full_path)
+ .expect(&format!("Unable to create {}", short_path))
+ .write_all(&result.data)
+ .unwrap();
+ other_paths.insert(key, short_path);
+ }
+ CachedImageData::External(ref ext) => {
+ let short_path = format!("externals/{}", external_images.len() + 1);
+ other_paths.insert(key, short_path.clone());
+ external_images.push(ExternalCaptureImage {
+ short_path,
+ descriptor: desc.clone(),
+ external: ext.clone(),
+ });
+ }
+ }
+ }
+
+ let mut font_templates = FastHashMap::default();
+ let mut font_remap = FastHashMap::default();
+ // Generate a map from duplicate font keys to their template.
+ for key in res.fonts.font_keys.keys() {
+ let shared_key = res.fonts.font_keys.map_key(&key);
+ let template = match res.fonts.templates.get_font(&shared_key) {
+ Some(template) => template,
+ None => {
+ debug!("Failed serializing font template {:?}", key);
+ continue;
+ }
+ };
+ let plain_font = match template {
+ FontTemplate::Raw(arc, index) => {
+ PlainFontTemplate {
+ data: font_paths[&arc.as_ptr()].clone(),
+ index,
+ }
+ }
+ #[cfg(not(target_os = "macos"))]
+ FontTemplate::Native(native) => {
+ PlainFontTemplate {
+ data: native.path.to_string_lossy().to_string(),
+ index: native.index,
+ }
+ }
+ #[cfg(target_os = "macos")]
+ FontTemplate::Native(native) => {
+ PlainFontTemplate {
+ data: native.name,
+ index: 0,
+ }
+ }
+ };
+ font_templates.insert(key, plain_font);
+ // Generate a reverse map from a shared key to a representive key.
+ font_remap.insert(shared_key, key);
+ }
+ let mut font_instances = Vec::new();
+ // Build a list of duplicate instance keys.
+ for instance_key in res.fonts.instance_keys.keys() {
+ let shared_key = res.fonts.instance_keys.map_key(&instance_key);
+ let instance = match res.fonts.instances.get_font_instance(shared_key) {
+ Some(instance) => instance,
+ None => {
+ debug!("Failed serializing font instance {:?}", instance_key);
+ continue;
+ }
+ };
+ // Target the instance towards a representive duplicate font key. The font key will be
+ // de-duplicated on load to an appropriate shared key.
+ font_instances.push(BaseFontInstance {
+ font_key: font_remap.get(&instance.font_key).cloned().unwrap_or(instance.font_key),
+ instance_key,
+ ..(*instance).clone()
+ });
+ }
+ let resources = PlainResources {
+ font_templates,
+ font_instances,
+ image_templates: res.image_templates.images
+ .iter()
+ .map(|(key, template)| {
+ (*key, PlainImageTemplate {
+ data: match template.data {
+ CachedImageData::Raw(ref arc) => image_paths[&arc.as_ptr()].clone(),
+ _ => other_paths[key].clone(),
+ },
+ descriptor: template.descriptor.clone(),
+ tiling: template.tiling,
+ generation: template.generation,
+ })
+ })
+ .collect(),
+ };
+
+ (resources, external_images)
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn save_caches(&self, _root: &PathBuf) -> PlainCacheRef {
+ PlainCacheRef {
+ current_frame_id: self.current_frame_id,
+ glyphs: &self.cached_glyphs,
+ glyph_dimensions: &self.cached_glyph_dimensions,
+ images: &self.cached_images,
+ render_tasks: &self.cached_render_tasks,
+ textures: &self.texture_cache,
+ picture_textures: &self.picture_textures,
+ }
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn load_capture(
+ &mut self,
+ resources: PlainResources,
+ caches: Option<PlainCacheOwn>,
+ config: &CaptureConfig,
+ ) -> Vec<PlainExternalImage> {
+ use std::{fs, path::Path};
+ use crate::texture_cache::TextureCacheConfig;
+
+ info!("loading resource cache");
+ //TODO: instead of filling the local path to Arc<data> map as we process
+ // each of the resource types, we could go through all of the local paths
+ // and fill out the map as the first step.
+ let mut raw_map = FastHashMap::<String, Arc<Vec<u8>>>::default();
+
+ self.clear(ClearCache::all());
+ self.clear_images(|_| true);
+
+ match caches {
+ Some(cached) => {
+ self.current_frame_id = cached.current_frame_id;
+ self.cached_glyphs = cached.glyphs;
+ self.cached_glyph_dimensions = cached.glyph_dimensions;
+ self.cached_images = cached.images;
+ self.cached_render_tasks = cached.render_tasks;
+ self.texture_cache = cached.textures;
+ self.picture_textures = cached.picture_textures;
+ }
+ None => {
+ self.current_frame_id = FrameId::INVALID;
+ self.texture_cache = TextureCache::new(
+ self.texture_cache.max_texture_size(),
+ self.texture_cache.tiling_threshold(),
+ self.texture_cache.color_formats(),
+ self.texture_cache.swizzle_settings(),
+ &TextureCacheConfig::DEFAULT,
+ );
+ self.picture_textures = PictureTextures::new(
+ self.picture_textures.default_tile_size(),
+ self.picture_textures.filter(),
+ );
+ }
+ }
+
+ self.glyph_rasterizer.reset();
+ let res = &mut self.resources;
+ res.fonts.templates.clear();
+ res.fonts.instances.clear();
+ res.image_templates.images.clear();
+
+ info!("\tfont templates...");
+ let root = config.resource_root();
+ let native_font_replacement = Arc::new(NATIVE_FONT.to_vec());
+ for (key, plain_template) in resources.font_templates {
+ let arc = match raw_map.entry(plain_template.data) {
+ Entry::Occupied(e) => {
+ e.get().clone()
+ }
+ Entry::Vacant(e) => {
+ let file_path = if Path::new(e.key()).is_absolute() {
+ PathBuf::from(e.key())
+ } else {
+ root.join(e.key())
+ };
+ let arc = match fs::read(file_path) {
+ Ok(buffer) => Arc::new(buffer),
+ Err(err) => {
+ error!("Unable to open font template {:?}: {:?}", e.key(), err);
+ Arc::clone(&native_font_replacement)
+ }
+ };
+ e.insert(arc).clone()
+ }
+ };
+
+ let template = FontTemplate::Raw(arc, plain_template.index);
+ // Only add the template if this is the first time it has been seen.
+ if let Some(shared_key) = res.fonts.font_keys.add_key(&key, &template) {
+ self.glyph_rasterizer.add_font(shared_key, template.clone());
+ res.fonts.templates.add_font(shared_key, template);
+ }
+ }
+
+ info!("\tfont instances...");
+ for instance in resources.font_instances {
+ // Target the instance to a shared font key.
+ let base = BaseFontInstance {
+ font_key: res.fonts.font_keys.map_key(&instance.font_key),
+ ..instance
+ };
+ if let Some(shared_instance) = res.fonts.instance_keys.add_key(base) {
+ res.fonts.instances.add_font_instance(shared_instance);
+ }
+ }
+
+ info!("\timage templates...");
+ let mut external_images = Vec::new();
+ for (key, template) in resources.image_templates {
+ let data = match config.deserialize_for_resource::<PlainExternalImage, _>(&template.data) {
+ Some(plain) => {
+ let ext_data = plain.external;
+ external_images.push(plain);
+ CachedImageData::External(ext_data)
+ }
+ None => {
+ let arc = match raw_map.entry(template.data) {
+ Entry::Occupied(e) => {
+ e.get().clone()
+ }
+ Entry::Vacant(e) => {
+ let buffer = fs::read(root.join(e.key()))
+ .expect(&format!("Unable to open {}", e.key()));
+ e.insert(Arc::new(buffer))
+ .clone()
+ }
+ };
+ CachedImageData::Raw(arc)
+ }
+ };
+
+ res.image_templates.images.insert(key, ImageResource {
+ data,
+ descriptor: template.descriptor,
+ tiling: template.tiling,
+ visible_rect: template.descriptor.size.into(),
+ generation: template.generation,
+ });
+ }
+
+ external_images
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn save_capture_sequence(&mut self, config: &mut CaptureConfig) -> Vec<ExternalCaptureImage> {
+ if self.capture_dirty {
+ self.capture_dirty = false;
+ config.prepare_resource();
+ let (resources, deferred) = self.save_capture(&config.resource_root());
+ config.serialize_for_resource(&resources, "plain-resources.ron");
+ deferred
+ } else {
+ Vec::new()
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/scene.rs b/gfx/wr/webrender/src/scene.rs
new file mode 100644
index 0000000000..51df9dd110
--- /dev/null
+++ b/gfx/wr/webrender/src/scene.rs
@@ -0,0 +1,364 @@
+/* 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 api::{BuiltDisplayList, DisplayListWithCache, ColorF, DynamicProperties, Epoch, FontRenderMode};
+use api::{PipelineId, PropertyBinding, PropertyBindingId, PropertyValue, MixBlendMode, StackingContext};
+use api::units::*;
+use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
+use crate::render_api::MemoryReport;
+use crate::composite::CompositorKind;
+use crate::clip::{ClipStore, ClipTree};
+use crate::spatial_tree::SpatialTree;
+use crate::frame_builder::{FrameBuilderConfig};
+use crate::hit_test::{HitTester, HitTestingScene, HitTestingSceneStats};
+use crate::internal_types::FastHashMap;
+use crate::picture::SurfaceInfo;
+use crate::picture_graph::PictureGraph;
+use crate::prim_store::{PrimitiveStore, PrimitiveStoreStats, PictureIndex, PrimitiveInstance};
+use crate::tile_cache::TileCacheConfig;
+use std::sync::Arc;
+
+/// Stores a map of the animated property bindings for the current display list. These
+/// can be used to animate the transform and/or opacity of a display list without
+/// re-submitting the display list itself.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SceneProperties {
+ transform_properties: FastHashMap<PropertyBindingId, LayoutTransform>,
+ float_properties: FastHashMap<PropertyBindingId, f32>,
+ color_properties: FastHashMap<PropertyBindingId, ColorF>,
+ current_properties: DynamicProperties,
+ pending_properties: Option<DynamicProperties>,
+}
+
+impl SceneProperties {
+ pub fn new() -> Self {
+ SceneProperties {
+ transform_properties: FastHashMap::default(),
+ float_properties: FastHashMap::default(),
+ color_properties: FastHashMap::default(),
+ current_properties: DynamicProperties::default(),
+ pending_properties: None,
+ }
+ }
+
+ /// Reset the pending properties without flush.
+ pub fn reset_properties(&mut self) {
+ self.pending_properties = None;
+ }
+
+ /// Add to the current property list for this display list.
+ pub fn add_properties(&mut self, properties: DynamicProperties) {
+ let mut pending_properties = self.pending_properties
+ .take()
+ .unwrap_or_default();
+
+ pending_properties.extend(properties);
+
+ self.pending_properties = Some(pending_properties);
+ }
+
+ /// Add to the current transform property list for this display list.
+ pub fn add_transforms(&mut self, transforms: Vec<PropertyValue<LayoutTransform>>) {
+ let mut pending_properties = self.pending_properties
+ .take()
+ .unwrap_or_default();
+
+ pending_properties.transforms.extend(transforms);
+
+ self.pending_properties = Some(pending_properties);
+ }
+
+ /// Flush any pending updates to the scene properties. Returns
+ /// true if the properties have changed since the last flush
+ /// was called. This code allows properties to be changed by
+ /// multiple reset_properties, add_properties and add_transforms calls
+ /// during a single transaction, and still correctly determine if any
+ /// properties have changed. This can have significant power
+ /// saving implications, allowing a frame build to be skipped
+ /// if the properties haven't changed in many cases.
+ pub fn flush_pending_updates(&mut self) -> bool {
+ let mut properties_changed = false;
+
+ if let Some(ref pending_properties) = self.pending_properties {
+ if *pending_properties != self.current_properties {
+ self.transform_properties.clear();
+ self.float_properties.clear();
+ self.color_properties.clear();
+
+ for property in &pending_properties.transforms {
+ self.transform_properties
+ .insert(property.key.id, property.value);
+ }
+
+ for property in &pending_properties.floats {
+ self.float_properties
+ .insert(property.key.id, property.value);
+ }
+
+ for property in &pending_properties.colors {
+ self.color_properties
+ .insert(property.key.id, property.value);
+ }
+
+ self.current_properties = pending_properties.clone();
+ properties_changed = true;
+ }
+ }
+
+ properties_changed
+ }
+
+ /// Get the current value for a transform property.
+ pub fn resolve_layout_transform(
+ &self,
+ property: &PropertyBinding<LayoutTransform>,
+ ) -> LayoutTransform {
+ match *property {
+ PropertyBinding::Value(value) => value,
+ PropertyBinding::Binding(ref key, v) => {
+ self.transform_properties
+ .get(&key.id)
+ .cloned()
+ .unwrap_or(v)
+ }
+ }
+ }
+
+ /// Get the current value for a float property.
+ pub fn resolve_float(
+ &self,
+ property: &PropertyBinding<f32>
+ ) -> f32 {
+ match *property {
+ PropertyBinding::Value(value) => value,
+ PropertyBinding::Binding(ref key, v) => {
+ self.float_properties
+ .get(&key.id)
+ .cloned()
+ .unwrap_or(v)
+ }
+ }
+ }
+
+ pub fn float_properties(&self) -> &FastHashMap<PropertyBindingId, f32> {
+ &self.float_properties
+ }
+
+ /// Get the current value for a color property.
+ pub fn resolve_color(
+ &self,
+ property: &PropertyBinding<ColorF>
+ ) -> ColorF {
+ match *property {
+ PropertyBinding::Value(value) => value,
+ PropertyBinding::Binding(ref key, v) => {
+ self.color_properties
+ .get(&key.id)
+ .cloned()
+ .unwrap_or(v)
+ }
+ }
+ }
+
+ pub fn color_properties(&self) -> &FastHashMap<PropertyBindingId, ColorF> {
+ &self.color_properties
+ }
+
+}
+
+/// A representation of the layout within the display port for a given document or iframe.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Clone)]
+pub struct ScenePipeline {
+ pub display_list: DisplayListWithCache,
+}
+
+/// A complete representation of the layout bundling visible pipelines together.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Clone)]
+pub struct Scene {
+ pub root_pipeline_id: Option<PipelineId>,
+ pub pipelines: FastHashMap<PipelineId, ScenePipeline>,
+ pub pipeline_epochs: FastHashMap<PipelineId, Epoch>,
+}
+
+impl Scene {
+ pub fn new() -> Self {
+ Scene {
+ root_pipeline_id: None,
+ pipelines: FastHashMap::default(),
+ pipeline_epochs: FastHashMap::default(),
+ }
+ }
+
+ pub fn set_root_pipeline_id(&mut self, pipeline_id: PipelineId) {
+ self.root_pipeline_id = Some(pipeline_id);
+ }
+
+ pub fn set_display_list(
+ &mut self,
+ pipeline_id: PipelineId,
+ epoch: Epoch,
+ display_list: BuiltDisplayList,
+ ) {
+ // Adds a cache to the given display list. If this pipeline already had
+ // a display list before, that display list is updated and used instead.
+ let display_list = match self.pipelines.remove(&pipeline_id) {
+ Some(mut pipeline) => {
+ pipeline.display_list.update(display_list);
+ pipeline.display_list
+ }
+ None => DisplayListWithCache::new_from_list(display_list)
+ };
+
+ let new_pipeline = ScenePipeline {
+ display_list,
+ };
+
+ self.pipelines.insert(pipeline_id, new_pipeline);
+ self.pipeline_epochs.insert(pipeline_id, epoch);
+ }
+
+ pub fn remove_pipeline(&mut self, pipeline_id: PipelineId) {
+ if self.root_pipeline_id == Some(pipeline_id) {
+ self.root_pipeline_id = None;
+ }
+ self.pipelines.remove(&pipeline_id);
+ self.pipeline_epochs.remove(&pipeline_id);
+ }
+
+ pub fn update_epoch(&mut self, pipeline_id: PipelineId, epoch: Epoch) {
+ self.pipeline_epochs.insert(pipeline_id, epoch);
+ }
+
+ pub fn has_root_pipeline(&self) -> bool {
+ if let Some(ref root_id) = self.root_pipeline_id {
+ return self.pipelines.contains_key(root_id);
+ }
+
+ false
+ }
+
+ pub fn report_memory(
+ &self,
+ ops: &mut MallocSizeOfOps,
+ report: &mut MemoryReport
+ ) {
+ for (_, pipeline) in &self.pipelines {
+ report.display_list += pipeline.display_list.size_of(ops)
+ }
+ }
+}
+
+pub trait StackingContextHelpers {
+ fn mix_blend_mode_for_compositing(&self) -> Option<MixBlendMode>;
+}
+
+impl StackingContextHelpers for StackingContext {
+ fn mix_blend_mode_for_compositing(&self) -> Option<MixBlendMode> {
+ match self.mix_blend_mode {
+ MixBlendMode::Normal => None,
+ _ => Some(self.mix_blend_mode),
+ }
+ }
+}
+
+
+/// WebRender's internal representation of the scene.
+pub struct BuiltScene {
+ pub has_root_pipeline: bool,
+ pub pipeline_epochs: FastHashMap<PipelineId, Epoch>,
+ pub output_rect: DeviceIntRect,
+ pub prim_store: PrimitiveStore,
+ pub clip_store: ClipStore,
+ pub config: FrameBuilderConfig,
+ pub hit_testing_scene: Arc<HitTestingScene>,
+ pub tile_cache_config: TileCacheConfig,
+ pub tile_cache_pictures: Vec<PictureIndex>,
+ pub picture_graph: PictureGraph,
+ pub num_plane_splitters: usize,
+ pub prim_instances: Vec<PrimitiveInstance>,
+ pub surfaces: Vec<SurfaceInfo>,
+ pub clip_tree: ClipTree,
+}
+
+impl BuiltScene {
+ pub fn empty() -> Self {
+ BuiltScene {
+ has_root_pipeline: false,
+ pipeline_epochs: FastHashMap::default(),
+ output_rect: DeviceIntRect::zero(),
+ prim_store: PrimitiveStore::new(&PrimitiveStoreStats::empty()),
+ clip_store: ClipStore::new(),
+ hit_testing_scene: Arc::new(HitTestingScene::new(&HitTestingSceneStats::empty())),
+ tile_cache_config: TileCacheConfig::new(0),
+ tile_cache_pictures: Vec::new(),
+ picture_graph: PictureGraph::new(),
+ num_plane_splitters: 0,
+ prim_instances: Vec::new(),
+ surfaces: Vec::new(),
+ clip_tree: ClipTree::new(),
+ config: FrameBuilderConfig {
+ default_font_render_mode: FontRenderMode::Mono,
+ dual_source_blending_is_supported: false,
+ testing: false,
+ gpu_supports_fast_clears: false,
+ gpu_supports_advanced_blend: false,
+ advanced_blend_is_coherent: false,
+ gpu_supports_render_target_partial_update: true,
+ external_images_require_copy: false,
+ batch_lookback_count: 0,
+ background_color: None,
+ compositor_kind: CompositorKind::default(),
+ tile_size_override: None,
+ max_surface_override: None,
+ max_depth_ids: 0,
+ max_target_size: 0,
+ force_invalidation: false,
+ is_software: false,
+ low_quality_pinch_zoom: false,
+ max_shared_surface_size: 2048,
+ },
+ }
+ }
+
+ /// Get the memory usage statistics to pre-allocate for the next scene.
+ pub fn get_stats(&self) -> SceneStats {
+ SceneStats {
+ prim_store_stats: self.prim_store.get_stats(),
+ hit_test_stats: self.hit_testing_scene.get_stats(),
+ }
+ }
+
+ pub fn create_hit_tester(
+ &mut self,
+ spatial_tree: &SpatialTree,
+ ) -> HitTester {
+ HitTester::new(
+ Arc::clone(&self.hit_testing_scene),
+ spatial_tree,
+ )
+ }
+}
+
+/// Stores the allocation sizes of various arrays in the built
+/// scene. This is retrieved from the current frame builder
+/// and used to reserve an approximately correct capacity of
+/// the arrays for the next scene that is getting built.
+pub struct SceneStats {
+ pub prim_store_stats: PrimitiveStoreStats,
+ pub hit_test_stats: HitTestingSceneStats,
+}
+
+impl SceneStats {
+ pub fn empty() -> Self {
+ SceneStats {
+ prim_store_stats: PrimitiveStoreStats::empty(),
+ hit_test_stats: HitTestingSceneStats::empty(),
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/scene_builder_thread.rs b/gfx/wr/webrender/src/scene_builder_thread.rs
new file mode 100644
index 0000000000..c139c6aa97
--- /dev/null
+++ b/gfx/wr/webrender/src/scene_builder_thread.rs
@@ -0,0 +1,794 @@
+/* This Source Code Form is subject to the terms of the Mozilla Publi
+ * 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 api::{AsyncBlobImageRasterizer, BlobImageResult, Parameter};
+use api::{DocumentId, PipelineId, ExternalEvent, BlobImageRequest};
+use api::{NotificationRequest, Checkpoint, IdNamespace, QualitySettings};
+use api::{PrimitiveKeyKind, GlyphDimensionRequest, GlyphIndexRequest};
+use api::channel::{unbounded_channel, single_msg_channel, Receiver, Sender};
+use api::units::*;
+use crate::render_api::{ApiMsg, FrameMsg, SceneMsg, ResourceUpdate, TransactionMsg, MemoryReport};
+#[cfg(feature = "capture")]
+use crate::capture::CaptureConfig;
+use crate::frame_builder::FrameBuilderConfig;
+use crate::scene_building::SceneBuilder;
+use crate::clip::{ClipIntern, PolygonIntern};
+use crate::filterdata::FilterDataIntern;
+use glyph_rasterizer::SharedFontResources;
+use crate::intern::{Internable, Interner, UpdateList};
+use crate::internal_types::{FastHashMap, FastHashSet};
+use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
+use crate::prim_store::backdrop::{BackdropCapture, BackdropRender};
+use crate::prim_store::borders::{ImageBorder, NormalBorderPrim};
+use crate::prim_store::gradient::{LinearGradient, RadialGradient, ConicGradient};
+use crate::prim_store::image::{Image, YuvImage};
+use crate::prim_store::line_dec::LineDecoration;
+use crate::prim_store::picture::Picture;
+use crate::prim_store::text_run::TextRun;
+use crate::profiler::{self, TransactionProfile};
+use crate::render_backend::SceneView;
+use crate::renderer::{FullFrameStats, PipelineInfo};
+use crate::scene::{Scene, BuiltScene, SceneStats};
+use crate::spatial_tree::{SceneSpatialTree, SpatialTreeUpdates};
+use crate::telemetry::Telemetry;
+use crate::SceneBuilderHooks;
+use std::iter;
+use time::precise_time_ns;
+use crate::util::drain_filter;
+use std::thread;
+use std::time::Duration;
+
+fn rasterize_blobs(txn: &mut TransactionMsg, is_low_priority: bool) {
+ profile_scope!("rasterize_blobs");
+
+ if let Some(ref mut rasterizer) = txn.blob_rasterizer {
+ let mut rasterized_blobs = rasterizer.rasterize(&txn.blob_requests, is_low_priority);
+ // try using the existing allocation if our current list is empty
+ if txn.rasterized_blobs.is_empty() {
+ txn.rasterized_blobs = rasterized_blobs;
+ } else {
+ txn.rasterized_blobs.append(&mut rasterized_blobs);
+ }
+ }
+}
+
+/// Represent the remaining work associated to a transaction after the scene building
+/// phase as well as the result of scene building itself if applicable.
+pub struct BuiltTransaction {
+ pub document_id: DocumentId,
+ pub built_scene: Option<BuiltScene>,
+ pub view: SceneView,
+ pub resource_updates: Vec<ResourceUpdate>,
+ pub rasterized_blobs: Vec<(BlobImageRequest, BlobImageResult)>,
+ pub blob_rasterizer: Option<Box<dyn AsyncBlobImageRasterizer>>,
+ pub frame_ops: Vec<FrameMsg>,
+ pub removed_pipelines: Vec<(PipelineId, DocumentId)>,
+ pub notifications: Vec<NotificationRequest>,
+ pub interner_updates: Option<InternerUpdates>,
+ pub spatial_tree_updates: Option<SpatialTreeUpdates>,
+ pub render_frame: bool,
+ pub invalidate_rendered_frame: bool,
+ pub profile: TransactionProfile,
+ pub frame_stats: FullFrameStats,
+}
+
+#[cfg(feature = "replay")]
+pub struct LoadScene {
+ pub document_id: DocumentId,
+ pub scene: Scene,
+ pub fonts: SharedFontResources,
+ pub view: SceneView,
+ pub config: FrameBuilderConfig,
+ pub build_frame: bool,
+ pub interners: Interners,
+ pub spatial_tree: SceneSpatialTree,
+}
+
+/// Message to the scene builder thread.
+pub enum SceneBuilderRequest {
+ Transactions(Vec<Box<TransactionMsg>>),
+ AddDocument(DocumentId, DeviceIntSize),
+ DeleteDocument(DocumentId),
+ GetGlyphDimensions(GlyphDimensionRequest),
+ GetGlyphIndices(GlyphIndexRequest),
+ ClearNamespace(IdNamespace),
+ SimulateLongSceneBuild(u32),
+ ExternalEvent(ExternalEvent),
+ WakeUp,
+ StopRenderBackend,
+ ShutDown(Option<Sender<()>>),
+ Flush(Sender<()>),
+ SetFrameBuilderConfig(FrameBuilderConfig),
+ SetParameter(Parameter),
+ ReportMemory(Box<MemoryReport>, Sender<Box<MemoryReport>>),
+ #[cfg(feature = "capture")]
+ SaveScene(CaptureConfig),
+ #[cfg(feature = "replay")]
+ LoadScenes(Vec<LoadScene>),
+ #[cfg(feature = "capture")]
+ StartCaptureSequence(CaptureConfig),
+ #[cfg(feature = "capture")]
+ StopCaptureSequence,
+}
+
+// Message from scene builder to render backend.
+pub enum SceneBuilderResult {
+ Transactions(Vec<Box<BuiltTransaction>>, Option<Sender<SceneSwapResult>>),
+ ExternalEvent(ExternalEvent),
+ FlushComplete(Sender<()>),
+ DeleteDocument(DocumentId),
+ ClearNamespace(IdNamespace),
+ GetGlyphDimensions(GlyphDimensionRequest),
+ GetGlyphIndices(GlyphIndexRequest),
+ SetParameter(Parameter),
+ StopRenderBackend,
+ ShutDown(Option<Sender<()>>),
+
+ #[cfg(feature = "capture")]
+ /// The same as `Transactions`, but also supplies a `CaptureConfig` that the
+ /// render backend should use for sequence capture, until the next
+ /// `CapturedTransactions` or `StopCaptureSequence` result.
+ CapturedTransactions(Vec<Box<BuiltTransaction>>, CaptureConfig, Option<Sender<SceneSwapResult>>),
+
+ #[cfg(feature = "capture")]
+ /// The scene builder has stopped sequence capture, so the render backend
+ /// should do the same.
+ StopCaptureSequence,
+}
+
+// Message from render backend to scene builder to indicate the
+// scene swap was completed. We need a separate channel for this
+// so that they don't get mixed with SceneBuilderRequest messages.
+pub enum SceneSwapResult {
+ Complete(Sender<()>),
+ Aborted,
+}
+
+macro_rules! declare_interners {
+ ( $( $name:ident : $ty:ident, )+ ) => {
+ /// This struct contains all items that can be shared between
+ /// display lists. We want to intern and share the same clips,
+ /// primitives and other things between display lists so that:
+ /// - GPU cache handles remain valid, reducing GPU cache updates.
+ /// - Comparison of primitives and pictures between two
+ /// display lists is (a) fast (b) done during scene building.
+ #[cfg_attr(feature = "capture", derive(Serialize))]
+ #[cfg_attr(feature = "replay", derive(Deserialize))]
+ #[derive(Default)]
+ pub struct Interners {
+ $(
+ pub $name: Interner<$ty>,
+ )+
+ }
+
+ $(
+ impl AsMut<Interner<$ty>> for Interners {
+ fn as_mut(&mut self) -> &mut Interner<$ty> {
+ &mut self.$name
+ }
+ }
+ )+
+
+ pub struct InternerUpdates {
+ $(
+ pub $name: UpdateList<<$ty as Internable>::Key>,
+ )+
+ }
+
+ impl Interners {
+ /// Reports CPU heap memory used by the interners.
+ fn report_memory(
+ &self,
+ ops: &mut MallocSizeOfOps,
+ r: &mut MemoryReport,
+ ) {
+ $(
+ r.interning.interners.$name += self.$name.size_of(ops);
+ )+
+ }
+
+ fn end_frame_and_get_pending_updates(&mut self) -> InternerUpdates {
+ InternerUpdates {
+ $(
+ $name: self.$name.end_frame_and_get_pending_updates(),
+ )+
+ }
+ }
+ }
+ }
+}
+
+crate::enumerate_interners!(declare_interners);
+
+// A document in the scene builder contains the current scene,
+// as well as a persistent clip interner. This allows clips
+// to be de-duplicated, and persisted in the GPU cache between
+// display lists.
+struct Document {
+ scene: Scene,
+ interners: Interners,
+ stats: SceneStats,
+ view: SceneView,
+ spatial_tree: SceneSpatialTree,
+}
+
+impl Document {
+ fn new(device_rect: DeviceIntRect) -> Self {
+ Document {
+ scene: Scene::new(),
+ interners: Interners::default(),
+ stats: SceneStats::empty(),
+ spatial_tree: SceneSpatialTree::new(),
+ view: SceneView {
+ device_rect,
+ quality_settings: QualitySettings::default(),
+ },
+ }
+ }
+}
+
+pub struct SceneBuilderThread {
+ documents: FastHashMap<DocumentId, Document>,
+ rx: Receiver<SceneBuilderRequest>,
+ tx: Sender<ApiMsg>,
+ config: FrameBuilderConfig,
+ fonts: SharedFontResources,
+ size_of_ops: Option<MallocSizeOfOps>,
+ hooks: Option<Box<dyn SceneBuilderHooks + Send>>,
+ simulate_slow_ms: u32,
+ removed_pipelines: FastHashSet<PipelineId>,
+ #[cfg(feature = "capture")]
+ capture_config: Option<CaptureConfig>,
+}
+
+pub struct SceneBuilderThreadChannels {
+ rx: Receiver<SceneBuilderRequest>,
+ tx: Sender<ApiMsg>,
+}
+
+impl SceneBuilderThreadChannels {
+ pub fn new(
+ tx: Sender<ApiMsg>
+ ) -> (Self, Sender<SceneBuilderRequest>) {
+ let (in_tx, in_rx) = unbounded_channel();
+ (
+ Self {
+ rx: in_rx,
+ tx,
+ },
+ in_tx,
+ )
+ }
+}
+
+impl SceneBuilderThread {
+ pub fn new(
+ config: FrameBuilderConfig,
+ fonts: SharedFontResources,
+ size_of_ops: Option<MallocSizeOfOps>,
+ hooks: Option<Box<dyn SceneBuilderHooks + Send>>,
+ channels: SceneBuilderThreadChannels,
+ ) -> Self {
+ let SceneBuilderThreadChannels { rx, tx } = channels;
+
+ Self {
+ documents: Default::default(),
+ rx,
+ tx,
+ config,
+ fonts,
+ size_of_ops,
+ hooks,
+ simulate_slow_ms: 0,
+ removed_pipelines: FastHashSet::default(),
+ #[cfg(feature = "capture")]
+ capture_config: None,
+ }
+ }
+
+ /// Send a message to the render backend thread.
+ ///
+ /// We first put something in the result queue and then send a wake-up
+ /// message to the api queue that the render backend is blocking on.
+ pub fn send(&self, msg: SceneBuilderResult) {
+ self.tx.send(ApiMsg::SceneBuilderResult(msg)).unwrap();
+ }
+
+ /// The scene builder thread's event loop.
+ pub fn run(&mut self) {
+ if let Some(ref hooks) = self.hooks {
+ hooks.register();
+ }
+
+ loop {
+ tracy_begin_frame!("scene_builder_thread");
+
+ match self.rx.recv() {
+ Ok(SceneBuilderRequest::WakeUp) => {}
+ Ok(SceneBuilderRequest::Flush(tx)) => {
+ self.send(SceneBuilderResult::FlushComplete(tx));
+ }
+ Ok(SceneBuilderRequest::Transactions(txns)) => {
+ let built_txns : Vec<Box<BuiltTransaction>> = txns.into_iter()
+ .map(|txn| self.process_transaction(*txn))
+ .collect();
+ #[cfg(feature = "capture")]
+ match built_txns.iter().any(|txn| txn.built_scene.is_some()) {
+ true => self.save_capture_sequence(),
+ _ => {},
+ }
+ self.forward_built_transactions(built_txns);
+ }
+ Ok(SceneBuilderRequest::AddDocument(document_id, initial_size)) => {
+ let old = self.documents.insert(document_id, Document::new(
+ initial_size.into(),
+ ));
+ debug_assert!(old.is_none());
+ }
+ Ok(SceneBuilderRequest::DeleteDocument(document_id)) => {
+ self.documents.remove(&document_id);
+ self.send(SceneBuilderResult::DeleteDocument(document_id));
+ }
+ Ok(SceneBuilderRequest::ClearNamespace(id)) => {
+ self.documents.retain(|doc_id, _doc| doc_id.namespace_id != id);
+ self.send(SceneBuilderResult::ClearNamespace(id));
+ }
+ Ok(SceneBuilderRequest::ExternalEvent(evt)) => {
+ self.send(SceneBuilderResult::ExternalEvent(evt));
+ }
+ Ok(SceneBuilderRequest::GetGlyphDimensions(request)) => {
+ self.send(SceneBuilderResult::GetGlyphDimensions(request));
+ }
+ Ok(SceneBuilderRequest::GetGlyphIndices(request)) => {
+ self.send(SceneBuilderResult::GetGlyphIndices(request));
+ }
+ Ok(SceneBuilderRequest::StopRenderBackend) => {
+ self.send(SceneBuilderResult::StopRenderBackend);
+ }
+ Ok(SceneBuilderRequest::ShutDown(sync)) => {
+ self.send(SceneBuilderResult::ShutDown(sync));
+ break;
+ }
+ Ok(SceneBuilderRequest::SimulateLongSceneBuild(time_ms)) => {
+ self.simulate_slow_ms = time_ms
+ }
+ Ok(SceneBuilderRequest::ReportMemory(mut report, tx)) => {
+ (*report) += self.report_memory();
+ tx.send(report).unwrap();
+ }
+ Ok(SceneBuilderRequest::SetFrameBuilderConfig(cfg)) => {
+ self.config = cfg;
+ }
+ Ok(SceneBuilderRequest::SetParameter(prop)) => {
+ self.send(SceneBuilderResult::SetParameter(prop));
+ }
+ #[cfg(feature = "replay")]
+ Ok(SceneBuilderRequest::LoadScenes(msg)) => {
+ self.load_scenes(msg);
+ }
+ #[cfg(feature = "capture")]
+ Ok(SceneBuilderRequest::SaveScene(config)) => {
+ self.save_scene(config);
+ }
+ #[cfg(feature = "capture")]
+ Ok(SceneBuilderRequest::StartCaptureSequence(config)) => {
+ self.start_capture_sequence(config);
+ }
+ #[cfg(feature = "capture")]
+ Ok(SceneBuilderRequest::StopCaptureSequence) => {
+ // FIXME(aosmond): clear config for frames and resource cache without scene
+ // rebuild?
+ self.capture_config = None;
+ self.send(SceneBuilderResult::StopCaptureSequence);
+ }
+ Err(_) => {
+ break;
+ }
+ }
+
+ if let Some(ref hooks) = self.hooks {
+ hooks.poke();
+ }
+
+ tracy_end_frame!("scene_builder_thread");
+ }
+
+ if let Some(ref hooks) = self.hooks {
+ hooks.deregister();
+ }
+ }
+
+ #[cfg(feature = "capture")]
+ fn save_scene(&mut self, config: CaptureConfig) {
+ for (id, doc) in &self.documents {
+ let interners_name = format!("interners-{}-{}", id.namespace_id.0, id.id);
+ config.serialize_for_scene(&doc.interners, interners_name);
+
+ let scene_spatial_tree_name = format!("scene-spatial-tree-{}-{}", id.namespace_id.0, id.id);
+ config.serialize_for_scene(&doc.spatial_tree, scene_spatial_tree_name);
+
+ use crate::render_api::CaptureBits;
+ if config.bits.contains(CaptureBits::SCENE) {
+ let file_name = format!("scene-{}-{}", id.namespace_id.0, id.id);
+ config.serialize_for_scene(&doc.scene, file_name);
+ }
+ }
+ }
+
+ #[cfg(feature = "replay")]
+ fn load_scenes(&mut self, scenes: Vec<LoadScene>) {
+ for mut item in scenes {
+ self.config = item.config;
+
+ let mut built_scene = None;
+ let mut interner_updates = None;
+ let mut spatial_tree_updates = None;
+
+ if item.scene.has_root_pipeline() {
+ built_scene = Some(SceneBuilder::build(
+ &item.scene,
+ item.fonts,
+ &item.view,
+ &self.config,
+ &mut item.interners,
+ &mut item.spatial_tree,
+ &SceneStats::empty(),
+ ));
+
+ interner_updates = Some(
+ item.interners.end_frame_and_get_pending_updates()
+ );
+
+ spatial_tree_updates = Some(
+ item.spatial_tree.end_frame_and_get_pending_updates()
+ );
+ }
+
+ self.documents.insert(
+ item.document_id,
+ Document {
+ scene: item.scene,
+ interners: item.interners,
+ stats: SceneStats::empty(),
+ view: item.view.clone(),
+ spatial_tree: item.spatial_tree,
+ },
+ );
+
+ let txns = vec![Box::new(BuiltTransaction {
+ document_id: item.document_id,
+ render_frame: item.build_frame,
+ invalidate_rendered_frame: false,
+ built_scene,
+ view: item.view,
+ resource_updates: Vec::new(),
+ rasterized_blobs: Vec::new(),
+ blob_rasterizer: None,
+ frame_ops: Vec::new(),
+ removed_pipelines: Vec::new(),
+ notifications: Vec::new(),
+ interner_updates,
+ spatial_tree_updates,
+ profile: TransactionProfile::new(),
+ frame_stats: FullFrameStats::default(),
+ })];
+
+ self.forward_built_transactions(txns);
+ }
+ }
+
+ #[cfg(feature = "capture")]
+ fn save_capture_sequence(
+ &mut self,
+ ) {
+ if let Some(ref mut config) = self.capture_config {
+ config.prepare_scene();
+ for (id, doc) in &self.documents {
+ let interners_name = format!("interners-{}-{}", id.namespace_id.0, id.id);
+ config.serialize_for_scene(&doc.interners, interners_name);
+
+ use crate::render_api::CaptureBits;
+ if config.bits.contains(CaptureBits::SCENE) {
+ let file_name = format!("scene-{}-{}", id.namespace_id.0, id.id);
+ config.serialize_for_scene(&doc.scene, file_name);
+ }
+ }
+ }
+ }
+
+ #[cfg(feature = "capture")]
+ fn start_capture_sequence(
+ &mut self,
+ config: CaptureConfig,
+ ) {
+ self.capture_config = Some(config);
+ self.save_capture_sequence();
+ }
+
+ /// Do the bulk of the work of the scene builder thread.
+ fn process_transaction(&mut self, mut txn: TransactionMsg) -> Box<BuiltTransaction> {
+ profile_scope!("process_transaction");
+
+ if let Some(ref hooks) = self.hooks {
+ hooks.pre_scene_build();
+ }
+
+ let doc = self.documents.get_mut(&txn.document_id).unwrap();
+ let scene = &mut doc.scene;
+
+ let mut profile = txn.profile.take();
+
+ let scene_build_start = precise_time_ns();
+ let mut removed_pipelines = Vec::new();
+ let mut rebuild_scene = false;
+ let mut frame_stats = FullFrameStats::default();
+
+ for message in txn.scene_ops.drain(..) {
+ match message {
+ SceneMsg::UpdateEpoch(pipeline_id, epoch) => {
+ scene.update_epoch(pipeline_id, epoch);
+ }
+ SceneMsg::SetQualitySettings { settings } => {
+ doc.view.quality_settings = settings;
+ }
+ SceneMsg::SetDocumentView { device_rect } => {
+ doc.view.device_rect = device_rect;
+ }
+ SceneMsg::SetDisplayList {
+ epoch,
+ pipeline_id,
+ display_list,
+ } => {
+ let (builder_start_time_ns, builder_end_time_ns, send_time_ns) =
+ display_list.times();
+ let content_send_time = profiler::ns_to_ms(precise_time_ns() - send_time_ns);
+ let dl_build_time = profiler::ns_to_ms(builder_end_time_ns - builder_start_time_ns);
+ profile.set(profiler::CONTENT_SEND_TIME, content_send_time);
+ profile.set(profiler::DISPLAY_LIST_BUILD_TIME, dl_build_time);
+ profile.set(profiler::DISPLAY_LIST_MEM, profiler::bytes_to_mb(display_list.size_in_bytes()));
+
+ let (gecko_display_list_time, full_display_list) = display_list.gecko_display_list_stats();
+ frame_stats.full_display_list = full_display_list;
+ frame_stats.gecko_display_list_time = gecko_display_list_time;
+ frame_stats.wr_display_list_time += dl_build_time;
+
+ if self.removed_pipelines.contains(&pipeline_id) {
+ continue;
+ }
+
+ // Note: We could further reduce the amount of unnecessary scene
+ // building by keeping track of which pipelines are used by the
+ // scene (bug 1490751).
+ rebuild_scene = true;
+
+ scene.set_display_list(
+ pipeline_id,
+ epoch,
+ display_list,
+ );
+ }
+ SceneMsg::SetRootPipeline(pipeline_id) => {
+ if scene.root_pipeline_id != Some(pipeline_id) {
+ rebuild_scene = true;
+ scene.set_root_pipeline_id(pipeline_id);
+ }
+ }
+ SceneMsg::RemovePipeline(pipeline_id) => {
+ scene.remove_pipeline(pipeline_id);
+ self.removed_pipelines.insert(pipeline_id);
+ removed_pipelines.push((pipeline_id, txn.document_id));
+ }
+ }
+ }
+
+ self.removed_pipelines.clear();
+
+ let mut built_scene = None;
+ let mut interner_updates = None;
+ let mut spatial_tree_updates = None;
+
+ if scene.has_root_pipeline() && rebuild_scene {
+
+ let built = SceneBuilder::build(
+ &scene,
+ self.fonts.clone(),
+ &doc.view,
+ &self.config,
+ &mut doc.interners,
+ &mut doc.spatial_tree,
+ &doc.stats,
+ );
+
+ // Update the allocation stats for next scene
+ doc.stats = built.get_stats();
+
+ // Retrieve the list of updates from the clip interner.
+ interner_updates = Some(
+ doc.interners.end_frame_and_get_pending_updates()
+ );
+
+ spatial_tree_updates = Some(
+ doc.spatial_tree.end_frame_and_get_pending_updates()
+ );
+
+ built_scene = Some(built);
+ }
+
+ let scene_build_time_ms =
+ profiler::ns_to_ms(precise_time_ns() - scene_build_start);
+ profile.set(profiler::SCENE_BUILD_TIME, scene_build_time_ms);
+
+ frame_stats.scene_build_time += scene_build_time_ms;
+
+ if !txn.blob_requests.is_empty() {
+ profile.start_time(profiler::BLOB_RASTERIZATION_TIME);
+
+ let is_low_priority = false;
+ rasterize_blobs(&mut txn, is_low_priority);
+
+ profile.end_time(profiler::BLOB_RASTERIZATION_TIME);
+ Telemetry::record_rasterize_blobs_time(Duration::from_micros((profile.get(profiler::BLOB_RASTERIZATION_TIME).unwrap() * 1000.00) as u64));
+ }
+
+ drain_filter(
+ &mut txn.notifications,
+ |n| { n.when() == Checkpoint::SceneBuilt },
+ |n| { n.notify(); },
+ );
+
+ if self.simulate_slow_ms > 0 {
+ thread::sleep(Duration::from_millis(self.simulate_slow_ms as u64));
+ }
+
+ Box::new(BuiltTransaction {
+ document_id: txn.document_id,
+ render_frame: txn.generate_frame.as_bool(),
+ invalidate_rendered_frame: txn.invalidate_rendered_frame,
+ built_scene,
+ view: doc.view,
+ rasterized_blobs: txn.rasterized_blobs,
+ resource_updates: txn.resource_updates,
+ blob_rasterizer: txn.blob_rasterizer,
+ frame_ops: txn.frame_ops,
+ removed_pipelines,
+ notifications: txn.notifications,
+ interner_updates,
+ spatial_tree_updates,
+ profile,
+ frame_stats,
+ })
+ }
+
+ /// Send the results of process_transaction back to the render backend.
+ fn forward_built_transactions(&mut self, txns: Vec<Box<BuiltTransaction>>) {
+ let (pipeline_info, result_tx, result_rx) = match self.hooks {
+ Some(ref hooks) => {
+ if txns.iter().any(|txn| txn.built_scene.is_some()) {
+ let info = PipelineInfo {
+ epochs: txns.iter()
+ .filter(|txn| txn.built_scene.is_some())
+ .map(|txn| {
+ txn.built_scene.as_ref().unwrap()
+ .pipeline_epochs.iter()
+ .zip(iter::repeat(txn.document_id))
+ .map(|((&pipeline_id, &epoch), document_id)| ((pipeline_id, document_id), epoch))
+ }).flatten().collect(),
+ removed_pipelines: txns.iter()
+ .map(|txn| txn.removed_pipelines.clone())
+ .flatten().collect(),
+ };
+
+ let (tx, rx) = single_msg_channel();
+ let txn = txns.iter().find(|txn| txn.built_scene.is_some()).unwrap();
+ Telemetry::record_scenebuild_time(Duration::from_millis(txn.profile.get(profiler::SCENE_BUILD_TIME).unwrap() as u64));
+ hooks.pre_scene_swap();
+
+ (Some(info), Some(tx), Some(rx))
+ } else {
+ (None, None, None)
+ }
+ }
+ _ => (None, None, None)
+ };
+
+ let timer_id = Telemetry::start_sceneswap_time();
+ let document_ids = txns.iter().map(|txn| txn.document_id).collect();
+ let have_resources_updates : Vec<DocumentId> = if pipeline_info.is_none() {
+ txns.iter()
+ .filter(|txn| !txn.resource_updates.is_empty() || txn.invalidate_rendered_frame)
+ .map(|txn| txn.document_id)
+ .collect()
+ } else {
+ Vec::new()
+ };
+
+ #[cfg(feature = "capture")]
+ match self.capture_config {
+ Some(ref config) => self.send(SceneBuilderResult::CapturedTransactions(txns, config.clone(), result_tx)),
+ None => self.send(SceneBuilderResult::Transactions(txns, result_tx)),
+ };
+
+ #[cfg(not(feature = "capture"))]
+ self.send(SceneBuilderResult::Transactions(txns, result_tx));
+
+ if let Some(pipeline_info) = pipeline_info {
+ // Block until the swap is done, then invoke the hook.
+ let swap_result = result_rx.unwrap().recv();
+ Telemetry::stop_and_accumulate_sceneswap_time(timer_id);
+ self.hooks.as_ref().unwrap().post_scene_swap(&document_ids,
+ pipeline_info);
+ // Once the hook is done, allow the RB thread to resume
+ if let Ok(SceneSwapResult::Complete(resume_tx)) = swap_result {
+ resume_tx.send(()).ok();
+ }
+ } else {
+ Telemetry::cancel_sceneswap_time(timer_id);
+ if !have_resources_updates.is_empty() {
+ if let Some(ref hooks) = self.hooks {
+ hooks.post_resource_update(&have_resources_updates);
+ }
+ } else if let Some(ref hooks) = self.hooks {
+ hooks.post_empty_scene_build();
+ }
+ }
+ }
+
+ /// Reports CPU heap memory used by the SceneBuilder.
+ fn report_memory(&mut self) -> MemoryReport {
+ let ops = self.size_of_ops.as_mut().unwrap();
+ let mut report = MemoryReport::default();
+ for doc in self.documents.values() {
+ doc.interners.report_memory(ops, &mut report);
+ doc.scene.report_memory(ops, &mut report);
+ }
+
+ report
+ }
+}
+
+/// A scene builder thread which executes expensive operations such as blob rasterization
+/// with a lower priority than the normal scene builder thread.
+///
+/// After rasterizing blobs, the secene building request is forwarded to the normal scene
+/// builder where the FrameBuilder is generated.
+pub struct LowPrioritySceneBuilderThread {
+ pub rx: Receiver<SceneBuilderRequest>,
+ pub tx: Sender<SceneBuilderRequest>,
+}
+
+impl LowPrioritySceneBuilderThread {
+ pub fn run(&mut self) {
+ loop {
+ match self.rx.recv() {
+ Ok(SceneBuilderRequest::Transactions(mut txns)) => {
+ let txns : Vec<Box<TransactionMsg>> = txns.drain(..)
+ .map(|txn| self.process_transaction(txn))
+ .collect();
+ self.tx.send(SceneBuilderRequest::Transactions(txns)).unwrap();
+ }
+ Ok(SceneBuilderRequest::ShutDown(sync)) => {
+ self.tx.send(SceneBuilderRequest::ShutDown(sync)).unwrap();
+ break;
+ }
+ Ok(other) => {
+ self.tx.send(other).unwrap();
+ }
+ Err(_) => {
+ break;
+ }
+ }
+ }
+ }
+
+ fn process_transaction(&mut self, mut txn: Box<TransactionMsg>) -> Box<TransactionMsg> {
+ let is_low_priority = true;
+ txn.profile.start_time(profiler::BLOB_RASTERIZATION_TIME);
+ rasterize_blobs(&mut txn, is_low_priority);
+ txn.profile.end_time(profiler::BLOB_RASTERIZATION_TIME);
+ Telemetry::record_rasterize_blobs_time(Duration::from_micros((txn.profile.get(profiler::BLOB_RASTERIZATION_TIME).unwrap() * 1000.00) as u64));
+ txn.blob_requests = Vec::new();
+
+ txn
+ }
+}
diff --git a/gfx/wr/webrender/src/scene_building.rs b/gfx/wr/webrender/src/scene_building.rs
new file mode 100644
index 0000000000..1d792c7c8f
--- /dev/null
+++ b/gfx/wr/webrender/src/scene_building.rs
@@ -0,0 +1,4123 @@
+/* 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/. */
+
+//! # Scene building
+//!
+//! Scene building is the phase during which display lists, a representation built for
+//! serialization, are turned into a scene, webrender's internal representation that is
+//! suited for rendering frames.
+//!
+//! This phase is happening asynchronously on the scene builder thread.
+//!
+//! # General algorithm
+//!
+//! The important aspects of scene building are:
+//! - Building up primitive lists (much of the cost of scene building goes here).
+//! - Creating pictures for content that needs to be rendered into a surface, be it so that
+//! filters can be applied or for caching purposes.
+//! - Maintaining a temporary stack of stacking contexts to keep track of some of the
+//! drawing states.
+//! - Stitching multiple display lists which reference each other (without cycles) into
+//! a single scene (see build_reference_frame).
+//! - Interning, which detects when some of the retained state stays the same between display
+//! lists.
+//!
+//! The scene builder linearly traverses the serialized display list which is naturally
+//! ordered back-to-front, accumulating primitives in the top-most stacking context's
+//! primitive list.
+//! At the end of each stacking context (see pop_stacking_context), its primitive list is
+//! either handed over to a picture if one is created, or it is concatenated into the parent
+//! stacking context's primitive list.
+//!
+//! The flow of the algorithm is mostly linear except when handling:
+//! - shadow stacks (see push_shadow and pop_all_shadows),
+//! - backdrop filters (see add_backdrop_filter)
+//!
+
+use api::{AlphaType, BorderDetails, BorderDisplayItem, BuiltDisplayListIter, BuiltDisplayList, PrimitiveFlags};
+use api::{ClipId, ColorF, CommonItemProperties, ComplexClipRegion, ComponentTransferFuncType, RasterSpace};
+use api::{DisplayItem, DisplayItemRef, ExtendMode, ExternalScrollId, FilterData};
+use api::{FilterOp, FilterPrimitive, FontInstanceKey, FontSize, GlyphInstance, GlyphOptions, GradientStop};
+use api::{IframeDisplayItem, ImageKey, ImageRendering, ItemRange, ColorDepth, QualitySettings};
+use api::{LineOrientation, LineStyle, NinePatchBorderSource, PipelineId, MixBlendMode, StackingContextFlags};
+use api::{PropertyBinding, ReferenceFrameKind, ScrollFrameDescriptor, ReferenceFrameMapper};
+use api::{APZScrollGeneration, HasScrollLinkedEffect, Shadow, SpatialId, StickyFrameDescriptor, ImageMask, ItemTag};
+use api::{ClipMode, PrimitiveKeyKind, TransformStyle, YuvColorSpace, ColorRange, YuvData, TempFilterData};
+use api::{ReferenceTransformBinding, Rotation, FillRule, SpatialTreeItem, ReferenceFrameDescriptor};
+use api::units::*;
+use crate::image_tiling::simplify_repeated_primitive;
+use crate::clip::{ClipItemKey, ClipStore, ClipItemKeyKind};
+use crate::clip::{ClipInternData, ClipNodeId, ClipLeafId};
+use crate::clip::{PolygonDataHandle, ClipTreeBuilder};
+use crate::segment::EdgeAaSegmentMask;
+use crate::spatial_tree::{SceneSpatialTree, SpatialNodeContainer, SpatialNodeIndex, get_external_scroll_offset};
+use crate::frame_builder::{FrameBuilderConfig};
+use glyph_rasterizer::{FontInstance, SharedFontResources};
+use crate::hit_test::HitTestingScene;
+use crate::intern::Interner;
+use crate::internal_types::{FastHashMap, LayoutPrimitiveInfo, Filter, PlaneSplitterIndex, PipelineInstanceId};
+use crate::picture::{Picture3DContext, PictureCompositeMode, PicturePrimitive};
+use crate::picture::{BlitReason, OrderedPictureChild, PrimitiveList, SurfaceInfo, PictureFlags};
+use crate::picture_graph::PictureGraph;
+use crate::prim_store::{PrimitiveInstance};
+use crate::prim_store::{PrimitiveInstanceKind, NinePatchDescriptor, PrimitiveStore};
+use crate::prim_store::{InternablePrimitive, SegmentInstanceIndex, PictureIndex};
+use crate::prim_store::{PolygonKey};
+use crate::prim_store::backdrop::{BackdropCapture, BackdropRender};
+use crate::prim_store::borders::{ImageBorder, NormalBorderPrim};
+use crate::prim_store::gradient::{
+ GradientStopKey, LinearGradient, RadialGradient, RadialGradientParams, ConicGradient,
+ ConicGradientParams, optimize_radial_gradient, apply_gradient_local_clip,
+ optimize_linear_gradient, self,
+};
+use crate::prim_store::image::{Image, YuvImage};
+use crate::prim_store::line_dec::{LineDecoration, LineDecorationCacheKey, get_line_decoration_size};
+use crate::prim_store::picture::{Picture, PictureCompositeKey, PictureKey};
+use crate::prim_store::text_run::TextRun;
+use crate::render_backend::SceneView;
+use crate::resource_cache::ImageRequest;
+use crate::scene::{Scene, ScenePipeline, BuiltScene, SceneStats, StackingContextHelpers};
+use crate::scene_builder_thread::Interners;
+use crate::space::SpaceSnapper;
+use crate::spatial_node::{
+ ReferenceFrameInfo, StickyFrameInfo, ScrollFrameKind, SpatialNodeUid, SpatialNodeType
+};
+use crate::tile_cache::TileCacheBuilder;
+use euclid::approxeq::ApproxEq;
+use std::{f32, mem, usize};
+use std::collections::vec_deque::VecDeque;
+use std::sync::Arc;
+use crate::util::{VecHelper, MaxRect};
+use crate::filterdata::{SFilterDataComponent, SFilterData, SFilterDataKey};
+
+/// Offsets primitives (and clips) by the external scroll offset
+/// supplied to scroll nodes.
+pub struct ScrollOffsetMapper {
+ pub current_spatial_node: SpatialNodeIndex,
+ pub current_offset: LayoutVector2D,
+}
+
+impl ScrollOffsetMapper {
+ fn new() -> Self {
+ ScrollOffsetMapper {
+ current_spatial_node: SpatialNodeIndex::INVALID,
+ current_offset: LayoutVector2D::zero(),
+ }
+ }
+
+ /// Return the accumulated external scroll offset for a spatial
+ /// node. This caches the last result, which is the common case,
+ /// or defers to the spatial tree to build the value.
+ fn external_scroll_offset(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ spatial_tree: &SceneSpatialTree,
+ ) -> LayoutVector2D {
+ if spatial_node_index != self.current_spatial_node {
+ self.current_spatial_node = spatial_node_index;
+ self.current_offset = get_external_scroll_offset(spatial_tree, spatial_node_index);
+ }
+
+ self.current_offset
+ }
+}
+
+/// A data structure that keeps track of mapping between API Ids for spatials and the indices
+/// used internally in the SpatialTree to avoid having to do HashMap lookups for primitives
+/// and clips during frame building.
+#[derive(Default)]
+pub struct NodeIdToIndexMapper {
+ spatial_node_map: FastHashMap<SpatialId, SpatialNodeIndex>,
+}
+
+impl NodeIdToIndexMapper {
+ fn add_spatial_node(&mut self, id: SpatialId, index: SpatialNodeIndex) {
+ let _old_value = self.spatial_node_map.insert(id, index);
+ assert!(_old_value.is_none());
+ }
+
+ fn get_spatial_node_index(&self, id: SpatialId) -> SpatialNodeIndex {
+ self.spatial_node_map[&id]
+ }
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct CompositeOps {
+ // Requires only a single texture as input (e.g. most filters)
+ pub filters: Vec<Filter>,
+ pub filter_datas: Vec<FilterData>,
+ pub filter_primitives: Vec<FilterPrimitive>,
+
+ // Requires two source textures (e.g. mix-blend-mode)
+ pub mix_blend_mode: Option<MixBlendMode>,
+}
+
+impl CompositeOps {
+ pub fn new(
+ filters: Vec<Filter>,
+ filter_datas: Vec<FilterData>,
+ filter_primitives: Vec<FilterPrimitive>,
+ mix_blend_mode: Option<MixBlendMode>
+ ) -> Self {
+ CompositeOps {
+ filters,
+ filter_datas,
+ filter_primitives,
+ mix_blend_mode,
+ }
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.filters.is_empty() &&
+ self.filter_primitives.is_empty() &&
+ self.mix_blend_mode.is_none()
+ }
+
+ /// Returns true if this CompositeOps contains any filters that affect
+ /// the content (false if no filters, or filters are all no-ops).
+ fn has_valid_filters(&self) -> bool {
+ // For each filter, create a new image with that composite mode.
+ let mut current_filter_data_index = 0;
+ for filter in &self.filters {
+ match filter {
+ Filter::ComponentTransfer => {
+ let filter_data =
+ &self.filter_datas[current_filter_data_index];
+ let filter_data = filter_data.sanitize();
+ current_filter_data_index = current_filter_data_index + 1;
+ if filter_data.is_identity() {
+ continue
+ } else {
+ return true;
+ }
+ }
+ _ => {
+ if filter.is_noop() {
+ continue;
+ } else {
+ return true;
+ }
+ }
+ }
+ }
+
+ if !self.filter_primitives.is_empty() {
+ return true;
+ }
+
+ false
+ }
+}
+
+/// Represents the current input for a picture chain builder (either a
+/// prim list from the stacking context, or a wrapped picture instance).
+enum PictureSource {
+ PrimitiveList {
+ prim_list: PrimitiveList,
+ },
+ WrappedPicture {
+ instance: PrimitiveInstance,
+ },
+}
+
+/// Helper struct to build picture chains during scene building from
+/// a flattened stacking context struct.
+struct PictureChainBuilder {
+ /// The current input source for the next picture
+ current: PictureSource,
+
+ /// Positioning node for this picture chain
+ spatial_node_index: SpatialNodeIndex,
+ /// Prim flags for any pictures in this chain
+ flags: PrimitiveFlags,
+ /// Requested raster space for enclosing stacking context
+ raster_space: RasterSpace,
+ /// If true, set first picture as a resolve target
+ set_resolve_target: bool,
+ /// If true, mark the last picture as a sub-graph
+ establishes_sub_graph: bool,
+}
+
+impl PictureChainBuilder {
+ /// Create a new picture chain builder, from a primitive list
+ fn from_prim_list(
+ prim_list: PrimitiveList,
+ flags: PrimitiveFlags,
+ spatial_node_index: SpatialNodeIndex,
+ raster_space: RasterSpace,
+ is_sub_graph: bool,
+ ) -> Self {
+ PictureChainBuilder {
+ current: PictureSource::PrimitiveList {
+ prim_list,
+ },
+ spatial_node_index,
+ flags,
+ raster_space,
+ establishes_sub_graph: is_sub_graph,
+ set_resolve_target: is_sub_graph,
+ }
+ }
+
+ /// Create a new picture chain builder, from a picture wrapper instance
+ fn from_instance(
+ instance: PrimitiveInstance,
+ flags: PrimitiveFlags,
+ spatial_node_index: SpatialNodeIndex,
+ raster_space: RasterSpace,
+ ) -> Self {
+ PictureChainBuilder {
+ current: PictureSource::WrappedPicture {
+ instance,
+ },
+ flags,
+ spatial_node_index,
+ raster_space,
+ establishes_sub_graph: false,
+ set_resolve_target: false,
+ }
+ }
+
+ /// Wrap the existing content with a new picture with the given parameters
+ #[must_use]
+ fn add_picture(
+ self,
+ composite_mode: PictureCompositeMode,
+ clip_node_id: ClipNodeId,
+ context_3d: Picture3DContext<OrderedPictureChild>,
+ interners: &mut Interners,
+ prim_store: &mut PrimitiveStore,
+ prim_instances: &mut Vec<PrimitiveInstance>,
+ clip_tree_builder: &mut ClipTreeBuilder,
+ ) -> PictureChainBuilder {
+ let prim_list = match self.current {
+ PictureSource::PrimitiveList { prim_list } => {
+ prim_list
+ }
+ PictureSource::WrappedPicture { instance } => {
+ let mut prim_list = PrimitiveList::empty();
+
+ prim_list.add_prim(
+ instance,
+ LayoutRect::zero(),
+ self.spatial_node_index,
+ self.flags,
+ prim_instances,
+ clip_tree_builder,
+ );
+
+ prim_list
+ }
+ };
+
+ let flags = if self.set_resolve_target {
+ PictureFlags::IS_RESOLVE_TARGET
+ } else {
+ PictureFlags::empty()
+ };
+
+ let pic_index = PictureIndex(prim_store.pictures
+ .alloc()
+ .init(PicturePrimitive::new_image(
+ Some(composite_mode.clone()),
+ context_3d,
+ self.flags,
+ prim_list,
+ self.spatial_node_index,
+ self.raster_space,
+ flags,
+ ))
+ );
+
+ let instance = create_prim_instance(
+ pic_index,
+ Some(composite_mode).into(),
+ self.raster_space,
+ clip_node_id,
+ interners,
+ clip_tree_builder,
+ );
+
+ PictureChainBuilder {
+ current: PictureSource::WrappedPicture {
+ instance,
+ },
+ spatial_node_index: self.spatial_node_index,
+ flags: self.flags,
+ raster_space: self.raster_space,
+ // We are now on a subsequent picture, so set_resolve_target has been handled
+ set_resolve_target: false,
+ establishes_sub_graph: self.establishes_sub_graph,
+ }
+ }
+
+ /// Finish building this picture chain. Set the clip chain on the outermost picture
+ fn finalize(
+ self,
+ clip_node_id: ClipNodeId,
+ interners: &mut Interners,
+ prim_store: &mut PrimitiveStore,
+ clip_tree_builder: &mut ClipTreeBuilder,
+ ) -> PrimitiveInstance {
+ let mut flags = PictureFlags::empty();
+ if self.establishes_sub_graph {
+ flags |= PictureFlags::IS_SUB_GRAPH;
+ }
+
+ match self.current {
+ PictureSource::WrappedPicture { instance } => {
+ let pic_index = instance.kind.as_pic();
+ prim_store.pictures[pic_index.0].flags |= flags;
+
+ instance
+ }
+ PictureSource::PrimitiveList { prim_list } => {
+ if self.set_resolve_target {
+ flags |= PictureFlags::IS_RESOLVE_TARGET;
+ }
+
+ // If no picture was created for this stacking context, create a
+ // pass-through wrapper now. This is only needed in 1-2 edge cases
+ // now, and will be removed as a follow up.
+ let pic_index = PictureIndex(prim_store.pictures
+ .alloc()
+ .init(PicturePrimitive::new_image(
+ None,
+ Picture3DContext::Out,
+ self.flags,
+ prim_list,
+ self.spatial_node_index,
+ self.raster_space,
+ flags,
+ ))
+ );
+
+ create_prim_instance(
+ pic_index,
+ None.into(),
+ self.raster_space,
+ clip_node_id,
+ interners,
+ clip_tree_builder,
+ )
+ }
+ }
+ }
+
+ /// Returns true if this builder wraps a picture
+ #[allow(dead_code)]
+ fn has_picture(&self) -> bool {
+ match self.current {
+ PictureSource::WrappedPicture { .. } => true,
+ PictureSource::PrimitiveList { .. } => false,
+ }
+ }
+}
+
+bitflags! {
+ /// Slice flags
+ pub struct SliceFlags : u8 {
+ /// Slice created by a prim that has PrimitiveFlags::IS_SCROLLBAR_CONTAINER
+ const IS_SCROLLBAR = 1;
+ /// Represents an atomic container (can't split out compositor surfaces in this slice)
+ const IS_ATOMIC = 2;
+ }
+}
+
+/// A structure that converts a serialized display list into a form that WebRender
+/// can use to later build a frame. This structure produces a BuiltScene. Public
+/// members are typically those that are destructured into the BuiltScene.
+pub struct SceneBuilder<'a> {
+ /// The scene that we are currently building.
+ scene: &'a Scene,
+
+ /// The map of all font instances.
+ fonts: SharedFontResources,
+
+ /// The data structure that converts between ClipId/SpatialId and the various
+ /// index types that the SpatialTree uses.
+ id_to_index_mapper_stack: Vec<NodeIdToIndexMapper>,
+
+ /// A stack of stacking context properties.
+ sc_stack: Vec<FlattenedStackingContext>,
+
+ /// Stack of spatial node indices forming containing block for 3d contexts
+ containing_block_stack: Vec<SpatialNodeIndex>,
+
+ /// Stack of requested raster spaces for stacking contexts
+ raster_space_stack: Vec<RasterSpace>,
+
+ /// Maintains state for any currently active shadows
+ pending_shadow_items: VecDeque<ShadowItem>,
+
+ /// The SpatialTree that we are currently building during building.
+ pub spatial_tree: &'a mut SceneSpatialTree,
+
+ /// The store of primitives.
+ pub prim_store: PrimitiveStore,
+
+ /// Information about all primitives involved in hit testing.
+ pub hit_testing_scene: HitTestingScene,
+
+ /// The store which holds all complex clipping information.
+ pub clip_store: ClipStore,
+
+ /// The configuration to use for the FrameBuilder. We consult this in
+ /// order to determine the default font.
+ pub config: FrameBuilderConfig,
+
+ /// Reference to the set of data that is interned across display lists.
+ interners: &'a mut Interners,
+
+ /// Helper struct to map stacking context coords <-> reference frame coords.
+ rf_mapper: ReferenceFrameMapper,
+
+ /// Helper struct to map spatial nodes to external scroll offsets.
+ external_scroll_mapper: ScrollOffsetMapper,
+
+ /// The current recursion depth of iframes encountered. Used to restrict picture
+ /// caching slices to only the top-level content frame.
+ iframe_size: Vec<LayoutSize>,
+
+ /// Clip-chain for root iframes applied to any tile caches created within this iframe
+ root_iframe_clip: Option<ClipId>,
+
+ /// The current quality / performance settings for this scene.
+ quality_settings: QualitySettings,
+
+ /// Maintains state about the list of tile caches being built for this scene.
+ tile_cache_builder: TileCacheBuilder,
+
+ /// A helper struct to snap local rects in device space. During frame
+ /// building we may establish new raster roots, however typically that is in
+ /// cases where we won't be applying snapping (e.g. has perspective), or in
+ /// edge cases (e.g. SVG filter) where we can accept slightly incorrect
+ /// behaviour in favour of getting the common case right.
+ snap_to_device: SpaceSnapper,
+
+ /// A DAG that represents dependencies between picture primitives. This builds
+ /// a set of passes to run various picture processing passes in during frame
+ /// building, in a way that pictures are processed before (or after) their
+ /// dependencies, without relying on recursion for those passes.
+ picture_graph: PictureGraph,
+
+ /// Keep track of allocated plane splitters for this scene. A plane
+ /// splitter is allocated whenever we encounter a new 3d rendering context.
+ /// They are stored outside the picture since it makes it easier for them
+ /// to be referenced by both the owning 3d rendering context and the child
+ /// pictures that contribute to the splitter.
+ /// During scene building "allocating" a splitter is just incrementing an index.
+ /// Splitter objects themselves are allocated and recycled in the frame builder.
+ next_plane_splitter_index: usize,
+
+ /// A list of all primitive instances in the scene. We store them as a single
+ /// array so that multiple different systems (e.g. tile-cache, visibility, property
+ /// animation bindings) can store index buffers to prim instances.
+ prim_instances: Vec<PrimitiveInstance>,
+
+ /// A map of pipeline ids encountered during scene build - used to create unique
+ /// pipeline instance ids as they are encountered.
+ pipeline_instance_ids: FastHashMap<PipelineId, u32>,
+
+ /// A list of surfaces (backing textures) that are relevant for this scene.
+ /// Every picture is assigned to a surface (either a new surface if the picture
+ /// has a composite mode, or the parent surface if it's a pass-through).
+ surfaces: Vec<SurfaceInfo>,
+
+ /// Used to build a ClipTree from the clip-chains, clips and state during scene building.
+ clip_tree_builder: ClipTreeBuilder,
+}
+
+impl<'a> SceneBuilder<'a> {
+ pub fn build(
+ scene: &Scene,
+ fonts: SharedFontResources,
+ view: &SceneView,
+ frame_builder_config: &FrameBuilderConfig,
+ interners: &mut Interners,
+ spatial_tree: &mut SceneSpatialTree,
+ stats: &SceneStats,
+ ) -> BuiltScene {
+ profile_scope!("build_scene");
+
+ // We checked that the root pipeline is available on the render backend.
+ let root_pipeline_id = scene.root_pipeline_id.unwrap();
+ let root_pipeline = scene.pipelines.get(&root_pipeline_id).unwrap();
+ let root_reference_frame_index = spatial_tree.root_reference_frame_index();
+
+ // During scene building, we assume a 1:1 picture -> raster pixel scale
+ let snap_to_device = SpaceSnapper::new(
+ root_reference_frame_index,
+ RasterPixelScale::new(1.0),
+ );
+
+ let mut builder = SceneBuilder {
+ scene,
+ spatial_tree,
+ fonts,
+ config: *frame_builder_config,
+ id_to_index_mapper_stack: Vec::new(),
+ hit_testing_scene: HitTestingScene::new(&stats.hit_test_stats),
+ pending_shadow_items: VecDeque::new(),
+ sc_stack: Vec::new(),
+ containing_block_stack: Vec::new(),
+ raster_space_stack: vec![RasterSpace::Screen],
+ prim_store: PrimitiveStore::new(&stats.prim_store_stats),
+ clip_store: ClipStore::new(),
+ interners,
+ rf_mapper: ReferenceFrameMapper::new(),
+ external_scroll_mapper: ScrollOffsetMapper::new(),
+ iframe_size: Vec::new(),
+ root_iframe_clip: None,
+ quality_settings: view.quality_settings,
+ tile_cache_builder: TileCacheBuilder::new(
+ root_reference_frame_index,
+ frame_builder_config.background_color,
+ ),
+ snap_to_device,
+ picture_graph: PictureGraph::new(),
+ next_plane_splitter_index: 0,
+ prim_instances: Vec::new(),
+ pipeline_instance_ids: FastHashMap::default(),
+ surfaces: Vec::new(),
+ clip_tree_builder: ClipTreeBuilder::new(),
+ };
+
+ builder.build_all(
+ root_pipeline_id,
+ &root_pipeline,
+ );
+
+ // Construct the picture cache primitive instance(s) from the tile cache builder
+ let (tile_cache_config, tile_cache_pictures) = builder.tile_cache_builder.build(
+ &builder.config,
+ &mut builder.prim_store,
+ &builder.spatial_tree,
+ &builder.prim_instances,
+ &mut builder.clip_tree_builder,
+ );
+
+ // Add all the tile cache pictures as roots of the picture graph
+ for pic_index in &tile_cache_pictures {
+ builder.picture_graph.add_root(*pic_index);
+ SceneBuilder::finalize_picture(
+ *pic_index,
+ &mut builder.prim_store.pictures,
+ None,
+ );
+ }
+
+ let clip_tree = builder.clip_tree_builder.finalize();
+
+ BuiltScene {
+ has_root_pipeline: scene.has_root_pipeline(),
+ pipeline_epochs: scene.pipeline_epochs.clone(),
+ output_rect: view.device_rect.size().into(),
+ hit_testing_scene: Arc::new(builder.hit_testing_scene),
+ prim_store: builder.prim_store,
+ clip_store: builder.clip_store,
+ config: builder.config,
+ tile_cache_config,
+ tile_cache_pictures,
+ picture_graph: builder.picture_graph,
+ num_plane_splitters: builder.next_plane_splitter_index,
+ prim_instances: builder.prim_instances,
+ surfaces: builder.surfaces,
+ clip_tree,
+ }
+ }
+
+ /// Traverse the picture prim list and update any late-set spatial nodes
+ // TODO(gw): This is somewhat hacky - it's unfortunate we need to do this, but it's
+ // because we can't determine the scroll root until we have checked all the
+ // primitives in the slice. Perhaps we could simplify this by doing some
+ // work earlier in the DL builder, so we know what scroll root will be picked?
+ fn finalize_picture(
+ pic_index: PictureIndex,
+ pictures: &mut [PicturePrimitive],
+ parent_spatial_node_index: Option<SpatialNodeIndex>,
+ ) {
+ // Extract the prim_list (borrow check) and select the spatial node to
+ // assign to unknown clusters
+ let (mut prim_list, spatial_node_index) = {
+ let pic = &mut pictures[pic_index.0];
+ assert_ne!(pic.spatial_node_index, SpatialNodeIndex::UNKNOWN);
+
+ if pic.flags.contains(PictureFlags::IS_RESOLVE_TARGET) {
+ pic.flags |= PictureFlags::DISABLE_SNAPPING;
+ }
+
+ // If we're a surface, use that spatial node, otherwise the parent
+ let spatial_node_index = match pic.composite_mode {
+ Some(_) => pic.spatial_node_index,
+ None => parent_spatial_node_index.expect("bug: no parent"),
+ };
+
+ (
+ mem::replace(&mut pic.prim_list, PrimitiveList::empty()),
+ spatial_node_index,
+ )
+ };
+
+ // Update the spatial node of any unknown clusters
+ for cluster in &mut prim_list.clusters {
+ if cluster.spatial_node_index == SpatialNodeIndex::UNKNOWN {
+ cluster.spatial_node_index = spatial_node_index;
+ }
+ }
+
+ // Update the spatial node of any child pictures
+ for child_pic_index in &prim_list.child_pictures {
+ let child_pic = &mut pictures[child_pic_index.0];
+
+ if child_pic.spatial_node_index == SpatialNodeIndex::UNKNOWN {
+ child_pic.spatial_node_index = spatial_node_index;
+ }
+
+ // Recurse into child pictures which may also have unknown spatial nodes
+ SceneBuilder::finalize_picture(
+ *child_pic_index,
+ pictures,
+ Some(spatial_node_index),
+ );
+
+ if pictures[child_pic_index.0].flags.contains(PictureFlags::DISABLE_SNAPPING) {
+ pictures[pic_index.0].flags |= PictureFlags::DISABLE_SNAPPING;
+ }
+ }
+
+ // Restore the prim_list
+ pictures[pic_index.0].prim_list = prim_list;
+ }
+
+ /// Retrieve the current offset to allow converting a stacking context
+ /// relative coordinate to be relative to the owing reference frame,
+ /// also considering any external scroll offset on the provided
+ /// spatial node.
+ fn current_offset(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ ) -> LayoutVector2D {
+ // Get the current offset from stacking context <-> reference frame space.
+ let rf_offset = self.rf_mapper.current_offset();
+
+ // Get the external scroll offset, if applicable.
+ let scroll_offset = self
+ .external_scroll_mapper
+ .external_scroll_offset(
+ spatial_node_index,
+ self.spatial_tree,
+ );
+
+ rf_offset + scroll_offset
+ }
+
+ fn build_spatial_tree_for_display_list(
+ &mut self,
+ dl: &BuiltDisplayList,
+ pipeline_id: PipelineId,
+ instance_id: PipelineInstanceId,
+ ) {
+ dl.iter_spatial_tree(|item| {
+ match item {
+ SpatialTreeItem::ScrollFrame(descriptor) => {
+ let parent_space = self.get_space(descriptor.parent_space);
+ self.build_scroll_frame(
+ descriptor,
+ parent_space,
+ pipeline_id,
+ instance_id,
+ );
+ }
+ SpatialTreeItem::ReferenceFrame(descriptor) => {
+ let parent_space = self.get_space(descriptor.parent_spatial_id);
+ self.build_reference_frame(
+ descriptor,
+ parent_space,
+ pipeline_id,
+ instance_id,
+ );
+ }
+ SpatialTreeItem::StickyFrame(descriptor) => {
+ let parent_space = self.get_space(descriptor.parent_spatial_id);
+ self.build_sticky_frame(
+ descriptor,
+ parent_space,
+ instance_id,
+ );
+ }
+ SpatialTreeItem::Invalid => {
+ unreachable!();
+ }
+ }
+ });
+ }
+
+ fn build_all(
+ &mut self,
+ root_pipeline_id: PipelineId,
+ root_pipeline: &ScenePipeline,
+ ) {
+ enum ContextKind<'a> {
+ Root,
+ StackingContext {
+ sc_info: StackingContextInfo,
+ },
+ ReferenceFrame,
+ Iframe {
+ parent_traversal: BuiltDisplayListIter<'a>,
+ }
+ }
+ struct BuildContext<'a> {
+ pipeline_id: PipelineId,
+ kind: ContextKind<'a>,
+ }
+
+ self.id_to_index_mapper_stack.push(NodeIdToIndexMapper::default());
+
+ let instance_id = self.get_next_instance_id_for_pipeline(root_pipeline_id);
+
+ self.push_root(
+ root_pipeline_id,
+ instance_id,
+ );
+ self.build_spatial_tree_for_display_list(
+ &root_pipeline.display_list.display_list,
+ root_pipeline_id,
+ instance_id,
+ );
+
+ let mut stack = vec![BuildContext {
+ pipeline_id: root_pipeline_id,
+ kind: ContextKind::Root,
+ }];
+ let mut traversal = root_pipeline.display_list.iter();
+
+ 'outer: while let Some(bc) = stack.pop() {
+ loop {
+ let item = match traversal.next() {
+ Some(item) => item,
+ None => break,
+ };
+
+ match item.item() {
+ DisplayItem::PushStackingContext(ref info) => {
+ profile_scope!("build_stacking_context");
+ let spatial_node_index = self.get_space(info.spatial_id);
+ let mut subtraversal = item.sub_iter();
+ // Avoid doing unnecessary work for empty stacking contexts.
+ if subtraversal.current_stacking_context_empty() {
+ subtraversal.skip_current_stacking_context();
+ traversal = subtraversal;
+ continue;
+ }
+
+ let composition_operations = CompositeOps::new(
+ filter_ops_for_compositing(item.filters()),
+ filter_datas_for_compositing(item.filter_datas()),
+ filter_primitives_for_compositing(item.filter_primitives()),
+ info.stacking_context.mix_blend_mode_for_compositing(),
+ );
+
+ let sc_info = self.push_stacking_context(
+ composition_operations,
+ info.stacking_context.transform_style,
+ info.prim_flags,
+ spatial_node_index,
+ info.stacking_context.clip_chain_id,
+ info.stacking_context.raster_space,
+ info.stacking_context.flags,
+ );
+
+ self.rf_mapper.push_offset(info.origin.to_vector());
+ let new_context = BuildContext {
+ pipeline_id: bc.pipeline_id,
+ kind: ContextKind::StackingContext {
+ sc_info,
+ },
+ };
+ stack.push(bc);
+ stack.push(new_context);
+
+ subtraversal.merge_debug_stats_from(&mut traversal);
+ traversal = subtraversal;
+ continue 'outer;
+ }
+ DisplayItem::PushReferenceFrame(..) => {
+ profile_scope!("build_reference_frame");
+ let mut subtraversal = item.sub_iter();
+
+ self.rf_mapper.push_scope();
+ let new_context = BuildContext {
+ pipeline_id: bc.pipeline_id,
+ kind: ContextKind::ReferenceFrame,
+ };
+ stack.push(bc);
+ stack.push(new_context);
+
+ subtraversal.merge_debug_stats_from(&mut traversal);
+ traversal = subtraversal;
+ continue 'outer;
+ }
+ DisplayItem::PopReferenceFrame |
+ DisplayItem::PopStackingContext => break,
+ DisplayItem::Iframe(ref info) => {
+ profile_scope!("iframe");
+
+ let space = self.get_space(info.space_and_clip.spatial_id);
+ let subtraversal = match self.push_iframe(info, space) {
+ Some(pair) => pair,
+ None => continue,
+ };
+
+ let new_context = BuildContext {
+ pipeline_id: info.pipeline_id,
+ kind: ContextKind::Iframe {
+ parent_traversal: mem::replace(&mut traversal, subtraversal),
+ },
+ };
+ stack.push(bc);
+ stack.push(new_context);
+ continue 'outer;
+ }
+ _ => {
+ self.build_item(item);
+ }
+ };
+ }
+
+ match bc.kind {
+ ContextKind::Root => {}
+ ContextKind::StackingContext { sc_info } => {
+ self.rf_mapper.pop_offset();
+ self.pop_stacking_context(sc_info);
+ }
+ ContextKind::ReferenceFrame => {
+ self.rf_mapper.pop_scope();
+ }
+ ContextKind::Iframe { parent_traversal } => {
+ self.iframe_size.pop();
+ self.rf_mapper.pop_scope();
+ self.clip_tree_builder.pop_clip();
+ self.clip_tree_builder.pop_clip();
+
+ if self.iframe_size.is_empty() {
+ assert!(self.root_iframe_clip.is_some());
+ self.root_iframe_clip = None;
+ self.add_tile_cache_barrier_if_needed(SliceFlags::empty());
+ }
+
+ self.id_to_index_mapper_stack.pop().unwrap();
+
+ traversal = parent_traversal;
+ }
+ }
+
+ // TODO: factor this out to be part of capture
+ if cfg!(feature = "display_list_stats") {
+ let stats = traversal.debug_stats();
+ let total_bytes: usize = stats.iter().map(|(_, stats)| stats.num_bytes).sum();
+ debug!("item, total count, total bytes, % of DL bytes, bytes per item");
+ for (label, stats) in stats {
+ debug!("{}, {}, {}kb, {}%, {}",
+ label,
+ stats.total_count,
+ stats.num_bytes / 1000,
+ ((stats.num_bytes as f32 / total_bytes.max(1) as f32) * 100.0) as usize,
+ stats.num_bytes / stats.total_count.max(1));
+ }
+ debug!("");
+ }
+ }
+
+ debug_assert!(self.sc_stack.is_empty());
+
+ self.id_to_index_mapper_stack.pop().unwrap();
+ assert!(self.id_to_index_mapper_stack.is_empty());
+ }
+
+ fn build_sticky_frame(
+ &mut self,
+ info: &StickyFrameDescriptor,
+ parent_node_index: SpatialNodeIndex,
+ instance_id: PipelineInstanceId,
+ ) {
+ let sticky_frame_info = StickyFrameInfo::new(
+ info.bounds,
+ info.margins,
+ info.vertical_offset_bounds,
+ info.horizontal_offset_bounds,
+ info.previously_applied_offset,
+ );
+
+ let index = self.spatial_tree.add_sticky_frame(
+ parent_node_index,
+ sticky_frame_info,
+ info.id.pipeline_id(),
+ info.key,
+ instance_id,
+ );
+ self.id_to_index_mapper_stack.last_mut().unwrap().add_spatial_node(info.id, index);
+ }
+
+ fn build_reference_frame(
+ &mut self,
+ info: &ReferenceFrameDescriptor,
+ parent_space: SpatialNodeIndex,
+ pipeline_id: PipelineId,
+ instance_id: PipelineInstanceId,
+ ) {
+ let transform = match info.reference_frame.transform {
+ ReferenceTransformBinding::Static { binding } => binding,
+ ReferenceTransformBinding::Computed { scale_from, vertical_flip, rotation } => {
+ let content_size = &self.iframe_size.last().unwrap();
+
+ let mut transform = if let Some(scale_from) = scale_from {
+ // If we have a 90/270 degree rotation, then scale_from
+ // and content_size are in different coordinate spaces and
+ // we need to swap width/height for them to be correct.
+ match rotation {
+ Rotation::Degree0 |
+ Rotation::Degree180 => {
+ LayoutTransform::scale(
+ content_size.width / scale_from.width,
+ content_size.height / scale_from.height,
+ 1.0
+ )
+ },
+ Rotation::Degree90 |
+ Rotation::Degree270 => {
+ LayoutTransform::scale(
+ content_size.height / scale_from.width,
+ content_size.width / scale_from.height,
+ 1.0
+ )
+
+ }
+ }
+ } else {
+ LayoutTransform::identity()
+ };
+
+ if vertical_flip {
+ let content_size = &self.iframe_size.last().unwrap();
+ let content_height = match rotation {
+ Rotation::Degree0 | Rotation::Degree180 => content_size.height,
+ Rotation::Degree90 | Rotation::Degree270 => content_size.width,
+ };
+ transform = transform
+ .then_translate(LayoutVector3D::new(0.0, content_height, 0.0))
+ .pre_scale(1.0, -1.0, 1.0);
+ }
+
+ let rotate = rotation.to_matrix(**content_size);
+ let transform = transform.then(&rotate);
+
+ PropertyBinding::Value(transform)
+ },
+ };
+
+ self.push_reference_frame(
+ info.reference_frame.id,
+ parent_space,
+ pipeline_id,
+ info.reference_frame.transform_style,
+ transform,
+ info.reference_frame.kind,
+ info.origin.to_vector(),
+ SpatialNodeUid::external(info.reference_frame.key, pipeline_id, instance_id),
+ );
+ }
+
+ fn build_scroll_frame(
+ &mut self,
+ info: &ScrollFrameDescriptor,
+ parent_node_index: SpatialNodeIndex,
+ pipeline_id: PipelineId,
+ instance_id: PipelineInstanceId,
+ ) {
+ // This is useful when calculating scroll extents for the
+ // SpatialNode::scroll(..) API as well as for properly setting sticky
+ // positioning offsets.
+ let content_size = info.content_rect.size();
+
+ self.add_scroll_frame(
+ info.scroll_frame_id,
+ parent_node_index,
+ info.external_id,
+ pipeline_id,
+ &info.frame_rect,
+ &content_size,
+ ScrollFrameKind::Explicit,
+ info.external_scroll_offset,
+ info.scroll_offset_generation,
+ info.has_scroll_linked_effect,
+ SpatialNodeUid::external(info.key, pipeline_id, instance_id),
+ );
+ }
+
+ /// Advance and return the next instance id for a given pipeline id
+ fn get_next_instance_id_for_pipeline(
+ &mut self,
+ pipeline_id: PipelineId,
+ ) -> PipelineInstanceId {
+ let next_instance = self.pipeline_instance_ids
+ .entry(pipeline_id)
+ .or_insert(0);
+
+ let instance_id = PipelineInstanceId::new(*next_instance);
+ *next_instance += 1;
+
+ instance_id
+ }
+
+ fn push_iframe(
+ &mut self,
+ info: &IframeDisplayItem,
+ spatial_node_index: SpatialNodeIndex,
+ ) -> Option<BuiltDisplayListIter<'a>> {
+ let iframe_pipeline_id = info.pipeline_id;
+ let pipeline = match self.scene.pipelines.get(&iframe_pipeline_id) {
+ Some(pipeline) => pipeline,
+ None => {
+ debug_assert!(info.ignore_missing_pipeline);
+ return None
+ },
+ };
+
+ self.clip_tree_builder.push_clip_chain(Some(info.space_and_clip.clip_chain_id), false);
+
+ // TODO(gw): This is the only remaining call site that relies on ClipId parenting, remove me!
+ self.add_rect_clip_node(
+ ClipId::root(iframe_pipeline_id),
+ info.space_and_clip.spatial_id,
+ &info.clip_rect,
+ );
+
+ self.clip_tree_builder.push_clip_id(ClipId::root(iframe_pipeline_id));
+
+ let instance_id = self.get_next_instance_id_for_pipeline(iframe_pipeline_id);
+
+ self.id_to_index_mapper_stack.push(NodeIdToIndexMapper::default());
+
+ let bounds = self.snap_rect(
+ &info.bounds,
+ spatial_node_index,
+ );
+
+ let spatial_node_index = self.push_reference_frame(
+ SpatialId::root_reference_frame(iframe_pipeline_id),
+ spatial_node_index,
+ iframe_pipeline_id,
+ TransformStyle::Flat,
+ PropertyBinding::Value(LayoutTransform::identity()),
+ ReferenceFrameKind::Transform {
+ is_2d_scale_translation: true,
+ should_snap: true,
+ paired_with_perspective: false,
+ },
+ bounds.min.to_vector(),
+ SpatialNodeUid::root_reference_frame(iframe_pipeline_id, instance_id),
+ );
+
+ let iframe_rect = LayoutRect::from_size(bounds.size());
+ let is_root_pipeline = self.iframe_size.is_empty();
+
+ self.add_scroll_frame(
+ SpatialId::root_scroll_node(iframe_pipeline_id),
+ spatial_node_index,
+ ExternalScrollId(0, iframe_pipeline_id),
+ iframe_pipeline_id,
+ &iframe_rect,
+ &bounds.size(),
+ ScrollFrameKind::PipelineRoot {
+ is_root_pipeline,
+ },
+ LayoutVector2D::zero(),
+ APZScrollGeneration::default(),
+ HasScrollLinkedEffect::No,
+ SpatialNodeUid::root_scroll_frame(iframe_pipeline_id, instance_id),
+ );
+
+ // If this is a root iframe, force a new tile cache both before and after
+ // adding primitives for this iframe.
+ if self.iframe_size.is_empty() {
+ assert!(self.root_iframe_clip.is_none());
+ self.root_iframe_clip = Some(ClipId::root(iframe_pipeline_id));
+ self.add_tile_cache_barrier_if_needed(SliceFlags::empty());
+ }
+ self.iframe_size.push(info.bounds.size());
+ self.rf_mapper.push_scope();
+
+ self.build_spatial_tree_for_display_list(
+ &pipeline.display_list.display_list,
+ iframe_pipeline_id,
+ instance_id,
+ );
+
+ Some(pipeline.display_list.iter())
+ }
+
+ fn get_space(
+ &self,
+ spatial_id: SpatialId,
+ ) -> SpatialNodeIndex {
+ self.id_to_index_mapper_stack.last().unwrap().get_spatial_node_index(spatial_id)
+ }
+
+ fn get_clip_node(
+ &mut self,
+ clip_chain_id: api::ClipChainId,
+ ) -> ClipNodeId {
+ self.clip_tree_builder.build_clip_set(
+ clip_chain_id,
+ )
+ }
+
+ fn process_common_properties(
+ &mut self,
+ common: &CommonItemProperties,
+ bounds: Option<&LayoutRect>,
+ ) -> (LayoutPrimitiveInfo, LayoutRect, SpatialNodeIndex, ClipNodeId) {
+ let spatial_node_index = self.get_space(common.spatial_id);
+ let current_offset = self.current_offset(spatial_node_index);
+
+ let unsnapped_clip_rect = common.clip_rect.translate(current_offset);
+ let unsnapped_rect = bounds.map(|bounds| {
+ bounds.translate(current_offset)
+ });
+
+ // If no bounds rect is given, default to clip rect.
+ let (rect, clip_rect) = if common.flags.contains(PrimitiveFlags::ANTIALISED) {
+ (unsnapped_rect.unwrap_or(unsnapped_clip_rect), unsnapped_clip_rect)
+ } else {
+ let clip_rect = self.snap_rect(
+ &unsnapped_clip_rect,
+ spatial_node_index,
+ );
+
+ let rect = unsnapped_rect.map_or(clip_rect, |bounds| {
+ self.snap_rect(
+ &bounds,
+ spatial_node_index,
+ )
+ });
+
+ (rect, clip_rect)
+ };
+
+ let clip_node_id = self.get_clip_node(
+ common.clip_chain_id,
+ );
+
+ let layout = LayoutPrimitiveInfo {
+ rect,
+ clip_rect,
+ flags: common.flags,
+ };
+
+ (layout, unsnapped_rect.unwrap_or(unsnapped_clip_rect), spatial_node_index, clip_node_id)
+ }
+
+ fn process_common_properties_with_bounds(
+ &mut self,
+ common: &CommonItemProperties,
+ bounds: &LayoutRect,
+ ) -> (LayoutPrimitiveInfo, LayoutRect, SpatialNodeIndex, ClipNodeId) {
+ self.process_common_properties(
+ common,
+ Some(bounds),
+ )
+ }
+
+ pub fn snap_rect(
+ &mut self,
+ rect: &LayoutRect,
+ target_spatial_node: SpatialNodeIndex,
+ ) -> LayoutRect {
+ self.snap_to_device.set_target_spatial_node(
+ target_spatial_node,
+ self.spatial_tree,
+ );
+ self.snap_to_device.snap_rect(&rect)
+ }
+
+ fn build_item<'b>(
+ &'b mut self,
+ item: DisplayItemRef,
+ ) {
+ match *item.item() {
+ DisplayItem::Image(ref info) => {
+ profile_scope!("image");
+
+ let (layout, _, spatial_node_index, clip_node_id) = self.process_common_properties_with_bounds(
+ &info.common,
+ &info.bounds,
+ );
+
+ self.add_image(
+ spatial_node_index,
+ clip_node_id,
+ &layout,
+ layout.rect.size(),
+ LayoutSize::zero(),
+ info.image_key,
+ info.image_rendering,
+ info.alpha_type,
+ info.color,
+ );
+ }
+ DisplayItem::RepeatingImage(ref info) => {
+ profile_scope!("repeating_image");
+
+ let (layout, unsnapped_rect, spatial_node_index, clip_node_id) = self.process_common_properties_with_bounds(
+ &info.common,
+ &info.bounds,
+ );
+
+ let stretch_size = process_repeat_size(
+ &layout.rect,
+ &unsnapped_rect,
+ info.stretch_size,
+ );
+
+ self.add_image(
+ spatial_node_index,
+ clip_node_id,
+ &layout,
+ stretch_size,
+ info.tile_spacing,
+ info.image_key,
+ info.image_rendering,
+ info.alpha_type,
+ info.color,
+ );
+ }
+ DisplayItem::YuvImage(ref info) => {
+ profile_scope!("yuv_image");
+
+ let (layout, _, spatial_node_index, clip_node_id) = self.process_common_properties_with_bounds(
+ &info.common,
+ &info.bounds,
+ );
+
+ self.add_yuv_image(
+ spatial_node_index,
+ clip_node_id,
+ &layout,
+ info.yuv_data,
+ info.color_depth,
+ info.color_space,
+ info.color_range,
+ info.image_rendering,
+ );
+ }
+ DisplayItem::Text(ref info) => {
+ profile_scope!("text");
+
+ // TODO(aosmond): Snapping text primitives does not make much sense, given the
+ // primitive bounds and clip are supposed to be conservative, not definitive.
+ // E.g. they should be able to grow and not impact the output. However there
+ // are subtle interactions between the primitive origin and the glyph offset
+ // which appear to be significant (presumably due to some sort of accumulated
+ // error throughout the layers). We should fix this at some point.
+ let (layout, _, spatial_node_index, clip_node_id) = self.process_common_properties_with_bounds(
+ &info.common,
+ &info.bounds,
+ );
+
+ self.add_text(
+ spatial_node_index,
+ clip_node_id,
+ &layout,
+ &info.font_key,
+ &info.color,
+ item.glyphs(),
+ info.glyph_options,
+ );
+ }
+ DisplayItem::Rectangle(ref info) => {
+ profile_scope!("rect");
+
+ let (layout, _, spatial_node_index, clip_node_id) = self.process_common_properties_with_bounds(
+ &info.common,
+ &info.bounds,
+ );
+
+ self.add_primitive(
+ spatial_node_index,
+ clip_node_id,
+ &layout,
+ Vec::new(),
+ PrimitiveKeyKind::Rectangle {
+ color: info.color.into(),
+ },
+ );
+
+ if info.common.flags.contains(PrimitiveFlags::CHECKERBOARD_BACKGROUND) {
+ self.add_tile_cache_barrier_if_needed(SliceFlags::empty());
+ }
+ }
+ DisplayItem::HitTest(ref info) => {
+ profile_scope!("hit_test");
+
+ let spatial_node_index = self.get_space(info.spatial_id);
+ let current_offset = self.current_offset(spatial_node_index);
+ let unsnapped_rect = info.rect.translate(current_offset);
+
+ let rect = self.snap_rect(
+ &unsnapped_rect,
+ spatial_node_index,
+ );
+
+ let layout = LayoutPrimitiveInfo {
+ rect,
+ clip_rect: rect,
+ flags: info.flags,
+ };
+
+ let spatial_node = self.spatial_tree.get_node_info(spatial_node_index);
+ let anim_id: u64 = match spatial_node.node_type {
+ SpatialNodeType::ReferenceFrame(ReferenceFrameInfo {
+ source_transform: PropertyBinding::Binding(key, _),
+ ..
+ }) => key.clone().into(),
+ _ => 0,
+ };
+
+ let clip_node_id = self.get_clip_node(info.clip_chain_id);
+
+ self.add_primitive_to_hit_testing_list(
+ &layout,
+ spatial_node_index,
+ clip_node_id,
+ info.tag,
+ anim_id,
+ );
+ }
+ DisplayItem::ClearRectangle(ref info) => {
+ profile_scope!("clear");
+
+ let (layout, _, spatial_node_index, clip_node_id) = self.process_common_properties_with_bounds(
+ &info.common,
+ &info.bounds,
+ );
+
+ self.add_clear_rectangle(
+ spatial_node_index,
+ clip_node_id,
+ &layout,
+ );
+ }
+ DisplayItem::Line(ref info) => {
+ profile_scope!("line");
+
+ let (layout, _, spatial_node_index, clip_node_id) = self.process_common_properties_with_bounds(
+ &info.common,
+ &info.area,
+ );
+
+ self.add_line(
+ spatial_node_index,
+ clip_node_id,
+ &layout,
+ info.wavy_line_thickness,
+ info.orientation,
+ info.color,
+ info.style,
+ );
+ }
+ DisplayItem::Gradient(ref info) => {
+ profile_scope!("gradient");
+
+ if !info.gradient.is_valid() {
+ return;
+ }
+
+ let (mut layout, unsnapped_rect, spatial_node_index, clip_node_id) = self.process_common_properties_with_bounds(
+ &info.common,
+ &info.bounds,
+ );
+
+ let mut tile_size = process_repeat_size(
+ &layout.rect,
+ &unsnapped_rect,
+ info.tile_size,
+ );
+
+ let mut stops = read_gradient_stops(item.gradient_stops());
+ let mut start = info.gradient.start_point;
+ let mut end = info.gradient.end_point;
+ let flags = layout.flags;
+
+ let optimized = optimize_linear_gradient(
+ &mut layout.rect,
+ &mut tile_size,
+ info.tile_spacing,
+ &layout.clip_rect,
+ &mut start,
+ &mut end,
+ info.gradient.extend_mode,
+ &mut stops,
+ &mut |rect, start, end, stops, edge_aa_mask| {
+ let layout = LayoutPrimitiveInfo { rect: *rect, clip_rect: *rect, flags };
+ if let Some(prim_key_kind) = self.create_linear_gradient_prim(
+ &layout,
+ start,
+ end,
+ stops.to_vec(),
+ ExtendMode::Clamp,
+ rect.size(),
+ LayoutSize::zero(),
+ None,
+ edge_aa_mask,
+ ) {
+ self.add_nonshadowable_primitive(
+ spatial_node_index,
+ clip_node_id,
+ &layout,
+ Vec::new(),
+ prim_key_kind,
+ );
+ }
+ }
+ );
+
+ if !optimized && !tile_size.ceil().is_empty() {
+ if let Some(prim_key_kind) = self.create_linear_gradient_prim(
+ &layout,
+ start,
+ end,
+ stops,
+ info.gradient.extend_mode,
+ tile_size,
+ info.tile_spacing,
+ None,
+ EdgeAaSegmentMask::all(),
+ ) {
+ self.add_nonshadowable_primitive(
+ spatial_node_index,
+ clip_node_id,
+ &layout,
+ Vec::new(),
+ prim_key_kind,
+ );
+ }
+ }
+ }
+ DisplayItem::RadialGradient(ref info) => {
+ profile_scope!("radial");
+
+ if !info.gradient.is_valid() {
+ return;
+ }
+
+ let (mut layout, unsnapped_rect, spatial_node_index, clip_node_id) = self.process_common_properties_with_bounds(
+ &info.common,
+ &info.bounds,
+ );
+
+ let mut center = info.gradient.center;
+
+ let stops = read_gradient_stops(item.gradient_stops());
+
+ let mut tile_size = process_repeat_size(
+ &layout.rect,
+ &unsnapped_rect,
+ info.tile_size,
+ );
+
+ let mut prim_rect = layout.rect;
+ let mut tile_spacing = info.tile_spacing;
+ optimize_radial_gradient(
+ &mut prim_rect,
+ &mut tile_size,
+ &mut center,
+ &mut tile_spacing,
+ &layout.clip_rect,
+ info.gradient.radius,
+ info.gradient.end_offset,
+ info.gradient.extend_mode,
+ &stops,
+ &mut |solid_rect, color| {
+ self.add_nonshadowable_primitive(
+ spatial_node_index,
+ clip_node_id,
+ &LayoutPrimitiveInfo {
+ rect: *solid_rect,
+ .. layout
+ },
+ Vec::new(),
+ PrimitiveKeyKind::Rectangle { color: PropertyBinding::Value(color) },
+ );
+ }
+ );
+
+ // TODO: create_radial_gradient_prim already calls
+ // this, but it leaves the info variable that is
+ // passed to add_nonshadowable_primitive unmodified
+ // which can cause issues.
+ simplify_repeated_primitive(&tile_size, &mut tile_spacing, &mut prim_rect);
+
+ if !tile_size.ceil().is_empty() {
+ layout.rect = prim_rect;
+ let prim_key_kind = self.create_radial_gradient_prim(
+ &layout,
+ center,
+ info.gradient.start_offset * info.gradient.radius.width,
+ info.gradient.end_offset * info.gradient.radius.width,
+ info.gradient.radius.width / info.gradient.radius.height,
+ stops,
+ info.gradient.extend_mode,
+ tile_size,
+ tile_spacing,
+ None,
+ );
+
+ self.add_nonshadowable_primitive(
+ spatial_node_index,
+ clip_node_id,
+ &layout,
+ Vec::new(),
+ prim_key_kind,
+ );
+ }
+ }
+ DisplayItem::ConicGradient(ref info) => {
+ profile_scope!("conic");
+
+ if !info.gradient.is_valid() {
+ return;
+ }
+
+ let (mut layout, unsnapped_rect, spatial_node_index, clip_node_id) = self.process_common_properties_with_bounds(
+ &info.common,
+ &info.bounds,
+ );
+
+ let tile_size = process_repeat_size(
+ &layout.rect,
+ &unsnapped_rect,
+ info.tile_size,
+ );
+
+ let offset = apply_gradient_local_clip(
+ &mut layout.rect,
+ &tile_size,
+ &info.tile_spacing,
+ &layout.clip_rect,
+ );
+ let center = info.gradient.center + offset;
+
+ if !tile_size.ceil().is_empty() {
+ let prim_key_kind = self.create_conic_gradient_prim(
+ &layout,
+ center,
+ info.gradient.angle,
+ info.gradient.start_offset,
+ info.gradient.end_offset,
+ item.gradient_stops(),
+ info.gradient.extend_mode,
+ tile_size,
+ info.tile_spacing,
+ None,
+ );
+
+ self.add_nonshadowable_primitive(
+ spatial_node_index,
+ clip_node_id,
+ &layout,
+ Vec::new(),
+ prim_key_kind,
+ );
+ }
+ }
+ DisplayItem::BoxShadow(ref info) => {
+ profile_scope!("box_shadow");
+
+ let (layout, _, spatial_node_index, clip_node_id) = self.process_common_properties_with_bounds(
+ &info.common,
+ &info.box_bounds,
+ );
+
+ self.add_box_shadow(
+ spatial_node_index,
+ clip_node_id,
+ &layout,
+ &info.offset,
+ info.color,
+ info.blur_radius,
+ info.spread_radius,
+ info.border_radius,
+ info.clip_mode,
+ );
+ }
+ DisplayItem::Border(ref info) => {
+ profile_scope!("border");
+
+ let (layout, _, spatial_node_index, clip_node_id) = self.process_common_properties_with_bounds(
+ &info.common,
+ &info.bounds,
+ );
+
+ self.add_border(
+ spatial_node_index,
+ clip_node_id,
+ &layout,
+ info,
+ item.gradient_stops(),
+ );
+ }
+ DisplayItem::ImageMaskClip(ref info) => {
+ profile_scope!("image_clip");
+
+ self.add_image_mask_clip_node(
+ info.id,
+ info.spatial_id,
+ &info.image_mask,
+ info.fill_rule,
+ item.points(),
+ );
+ }
+ DisplayItem::RoundedRectClip(ref info) => {
+ profile_scope!("rounded_clip");
+
+ self.add_rounded_rect_clip_node(
+ info.id,
+ info.spatial_id,
+ &info.clip,
+ );
+ }
+ DisplayItem::RectClip(ref info) => {
+ profile_scope!("rect_clip");
+
+ self.add_rect_clip_node(
+ info.id,
+ info.spatial_id,
+ &info.clip_rect,
+ );
+ }
+ DisplayItem::ClipChain(ref info) => {
+ profile_scope!("clip_chain");
+
+ self.clip_tree_builder.define_clip_chain(
+ info.id,
+ info.parent,
+ item.clip_chain_items().into_iter(),
+ );
+ },
+ DisplayItem::BackdropFilter(ref info) => {
+ profile_scope!("backdrop");
+
+ let (layout, _, spatial_node_index, clip_node_id) = self.process_common_properties(
+ &info.common,
+ None,
+ );
+
+ let filters = filter_ops_for_compositing(item.filters());
+ let filter_datas = filter_datas_for_compositing(item.filter_datas());
+ let filter_primitives = filter_primitives_for_compositing(item.filter_primitives());
+
+ self.add_backdrop_filter(
+ spatial_node_index,
+ clip_node_id,
+ &layout,
+ filters,
+ filter_datas,
+ filter_primitives,
+ );
+ }
+
+ // Do nothing; these are dummy items for the display list parser
+ DisplayItem::SetGradientStops |
+ DisplayItem::SetFilterOps |
+ DisplayItem::SetFilterData |
+ DisplayItem::SetFilterPrimitives |
+ DisplayItem::SetPoints => {}
+
+ // Special items that are handled in the parent method
+ DisplayItem::PushStackingContext(..) |
+ DisplayItem::PushReferenceFrame(..) |
+ DisplayItem::PopReferenceFrame |
+ DisplayItem::PopStackingContext |
+ DisplayItem::Iframe(_) => {
+ unreachable!("Handled in `build_all`")
+ }
+
+ DisplayItem::ReuseItems(key) |
+ DisplayItem::RetainedItems(key) => {
+ unreachable!("Iterator logic error: {:?}", key);
+ }
+
+ DisplayItem::PushShadow(info) => {
+ profile_scope!("push_shadow");
+
+ let spatial_node_index = self.get_space(info.space_and_clip.spatial_id);
+
+ self.push_shadow(
+ info.shadow,
+ spatial_node_index,
+ info.space_and_clip.clip_chain_id,
+ info.should_inflate,
+ );
+ }
+ DisplayItem::PopAllShadows => {
+ profile_scope!("pop_all_shadows");
+
+ self.pop_all_shadows();
+ }
+ }
+ }
+
+ /// Create a primitive and add it to the prim store. This method doesn't
+ /// add the primitive to the draw list, so can be used for creating
+ /// sub-primitives.
+ ///
+ /// TODO(djg): Can this inline into `add_interned_prim_to_draw_list`
+ fn create_primitive<P>(
+ &mut self,
+ info: &LayoutPrimitiveInfo,
+ spatial_node_index: SpatialNodeIndex,
+ clip_leaf_id: ClipLeafId,
+ prim: P,
+ ) -> PrimitiveInstance
+ where
+ P: InternablePrimitive,
+ Interners: AsMut<Interner<P>>,
+ {
+ // Build a primitive key.
+ let prim_key = prim.into_key(info);
+
+ let current_offset = self.current_offset(spatial_node_index);
+ let interner = self.interners.as_mut();
+ let prim_data_handle = interner
+ .intern(&prim_key, || ());
+
+ let instance_kind = P::make_instance_kind(
+ prim_key,
+ prim_data_handle,
+ &mut self.prim_store,
+ current_offset,
+ );
+
+ PrimitiveInstance::new(
+ instance_kind,
+ clip_leaf_id,
+ )
+ }
+
+ fn add_primitive_to_hit_testing_list(
+ &mut self,
+ info: &LayoutPrimitiveInfo,
+ spatial_node_index: SpatialNodeIndex,
+ clip_node_id: ClipNodeId,
+ tag: ItemTag,
+ anim_id: u64,
+ ) {
+ self.hit_testing_scene.add_item(
+ tag,
+ anim_id,
+ info,
+ spatial_node_index,
+ clip_node_id,
+ &self.clip_tree_builder,
+ self.interners,
+ );
+ }
+
+ /// Add an already created primitive to the draw lists.
+ pub fn add_primitive_to_draw_list(
+ &mut self,
+ prim_instance: PrimitiveInstance,
+ prim_rect: LayoutRect,
+ spatial_node_index: SpatialNodeIndex,
+ flags: PrimitiveFlags,
+ ) {
+ // Add primitive to the top-most stacking context on the stack.
+
+ // If we have a valid stacking context, the primitive gets added to that.
+ // Otherwise, it gets added to a top-level picture cache slice.
+
+ match self.sc_stack.last_mut() {
+ Some(stacking_context) => {
+ stacking_context.prim_list.add_prim(
+ prim_instance,
+ prim_rect,
+ spatial_node_index,
+ flags,
+ &mut self.prim_instances,
+ &self.clip_tree_builder,
+ );
+ }
+ None => {
+ self.tile_cache_builder.add_prim(
+ prim_instance,
+ prim_rect,
+ spatial_node_index,
+ flags,
+ self.spatial_tree,
+ self.interners,
+ &self.quality_settings,
+ &mut self.prim_instances,
+ &self.clip_tree_builder,
+ );
+ }
+ }
+ }
+
+ /// Convenience interface that creates a primitive entry and adds it
+ /// to the draw list.
+ fn add_nonshadowable_primitive<P>(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ clip_node_id: ClipNodeId,
+ info: &LayoutPrimitiveInfo,
+ clip_items: Vec<ClipItemKey>,
+ prim: P,
+ )
+ where
+ P: InternablePrimitive + IsVisible,
+ Interners: AsMut<Interner<P>>,
+ {
+ if prim.is_visible() {
+ let clip_leaf_id = self.clip_tree_builder.build_for_prim(
+ clip_node_id,
+ info,
+ &clip_items,
+ &mut self.interners,
+ );
+
+ self.add_prim_to_draw_list(
+ info,
+ spatial_node_index,
+ clip_leaf_id,
+ prim,
+ );
+ }
+ }
+
+ pub fn add_primitive<P>(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ clip_node_id: ClipNodeId,
+ info: &LayoutPrimitiveInfo,
+ clip_items: Vec<ClipItemKey>,
+ prim: P,
+ )
+ where
+ P: InternablePrimitive + IsVisible,
+ Interners: AsMut<Interner<P>>,
+ ShadowItem: From<PendingPrimitive<P>>
+ {
+ // If a shadow context is not active, then add the primitive
+ // directly to the parent picture.
+ if self.pending_shadow_items.is_empty() {
+ self.add_nonshadowable_primitive(
+ spatial_node_index,
+ clip_node_id,
+ info,
+ clip_items,
+ prim,
+ );
+ } else {
+ debug_assert!(clip_items.is_empty(), "No per-prim clips expected for shadowed primitives");
+
+ // There is an active shadow context. Store as a pending primitive
+ // for processing during pop_all_shadows.
+ self.pending_shadow_items.push_back(PendingPrimitive {
+ spatial_node_index,
+ clip_node_id,
+ info: *info,
+ prim,
+ }.into());
+ }
+ }
+
+ fn add_prim_to_draw_list<P>(
+ &mut self,
+ info: &LayoutPrimitiveInfo,
+ spatial_node_index: SpatialNodeIndex,
+ clip_leaf_id: ClipLeafId,
+ prim: P,
+ )
+ where
+ P: InternablePrimitive,
+ Interners: AsMut<Interner<P>>,
+ {
+ let prim_instance = self.create_primitive(
+ info,
+ spatial_node_index,
+ clip_leaf_id,
+ prim,
+ );
+ self.add_primitive_to_draw_list(
+ prim_instance,
+ info.rect,
+ spatial_node_index,
+ info.flags,
+ );
+ }
+
+ fn make_current_slice_atomic_if_required(&mut self) {
+ let has_non_wrapping_sc = self.sc_stack
+ .iter()
+ .position(|sc| {
+ !sc.flags.contains(StackingContextFlags::WRAPS_BACKDROP_FILTER)
+ })
+ .is_some();
+
+ if has_non_wrapping_sc {
+ return;
+ }
+
+ // Shadows can only exist within a stacking context
+ assert!(self.pending_shadow_items.is_empty());
+ self.tile_cache_builder.make_current_slice_atomic();
+ }
+
+ /// If no stacking contexts are present (i.e. we are adding prims to a tile
+ /// cache), set a barrier to force creation of a slice before the next prim
+ fn add_tile_cache_barrier_if_needed(
+ &mut self,
+ slice_flags: SliceFlags,
+ ) {
+ if self.sc_stack.is_empty() {
+ // Shadows can only exist within a stacking context
+ assert!(self.pending_shadow_items.is_empty());
+
+ self.tile_cache_builder.add_tile_cache_barrier(
+ slice_flags,
+ self.root_iframe_clip,
+ );
+ }
+ }
+
+ /// Push a new stacking context. Returns context that must be passed to pop_stacking_context().
+ fn push_stacking_context(
+ &mut self,
+ composite_ops: CompositeOps,
+ transform_style: TransformStyle,
+ prim_flags: PrimitiveFlags,
+ spatial_node_index: SpatialNodeIndex,
+ clip_chain_id: Option<api::ClipChainId>,
+ requested_raster_space: RasterSpace,
+ flags: StackingContextFlags,
+ ) -> StackingContextInfo {
+ profile_scope!("push_stacking_context");
+
+ let clip_node_id = match clip_chain_id {
+ Some(id) => {
+ self.clip_tree_builder.build_clip_set(id)
+ }
+ None => {
+ self.clip_tree_builder.build_clip_set(api::ClipChainId::INVALID)
+ }
+ };
+
+ self.clip_tree_builder.push_clip_chain(
+ clip_chain_id,
+ !composite_ops.is_empty(),
+ );
+
+ let new_space = match (self.raster_space_stack.last(), requested_raster_space) {
+ // If no parent space, just use the requested space
+ (None, _) => requested_raster_space,
+ // If screen, use the parent
+ (Some(parent_space), RasterSpace::Screen) => *parent_space,
+ // If currently screen, select the requested
+ (Some(RasterSpace::Screen), space) => space,
+ // If both local, take the maximum scale
+ (Some(RasterSpace::Local(parent_scale)), RasterSpace::Local(scale)) => RasterSpace::Local(parent_scale.max(scale)),
+ };
+ self.raster_space_stack.push(new_space);
+
+ // Get the transform-style of the parent stacking context,
+ // which determines if we *might* need to draw this on
+ // an intermediate surface for plane splitting purposes.
+ let (parent_is_3d, extra_3d_instance, plane_splitter_index) = match self.sc_stack.last_mut() {
+ Some(ref mut sc) if sc.is_3d() => {
+ let (flat_items_context_3d, plane_splitter_index) = match sc.context_3d {
+ Picture3DContext::In { ancestor_index, plane_splitter_index, .. } => {
+ (
+ Picture3DContext::In {
+ root_data: None,
+ ancestor_index,
+ plane_splitter_index,
+ },
+ plane_splitter_index,
+ )
+ }
+ Picture3DContext::Out => panic!("Unexpected out of 3D context"),
+ };
+ // Cut the sequence of flat children before starting a child stacking context,
+ // so that the relative order between them and our current SC is preserved.
+ let extra_instance = sc.cut_item_sequence(
+ &mut self.prim_store,
+ &mut self.interners,
+ Some(PictureCompositeMode::Blit(BlitReason::PRESERVE3D)),
+ flat_items_context_3d,
+ &mut self.clip_tree_builder,
+ );
+ let extra_instance = extra_instance.map(|(_, instance)| {
+ ExtendedPrimitiveInstance {
+ instance,
+ spatial_node_index: sc.spatial_node_index,
+ flags: sc.prim_flags,
+ }
+ });
+ (true, extra_instance, Some(plane_splitter_index))
+ },
+ _ => (false, None, None),
+ };
+
+ if let Some(instance) = extra_3d_instance {
+ self.add_primitive_instance_to_3d_root(instance);
+ }
+
+ // If this is preserve-3d *or* the parent is, then this stacking
+ // context is participating in the 3d rendering context. In that
+ // case, hoist the picture up to the 3d rendering context
+ // container, so that it's rendered as a sibling with other
+ // elements in this context.
+ let participating_in_3d_context =
+ composite_ops.is_empty() &&
+ (parent_is_3d || transform_style == TransformStyle::Preserve3D);
+
+ let context_3d = if participating_in_3d_context {
+ // Get the spatial node index of the containing block, which
+ // defines the context of backface-visibility.
+ let ancestor_index = self.containing_block_stack
+ .last()
+ .cloned()
+ .unwrap_or(self.spatial_tree.root_reference_frame_index());
+
+ let plane_splitter_index = plane_splitter_index.unwrap_or_else(|| {
+ let index = self.next_plane_splitter_index;
+ self.next_plane_splitter_index += 1;
+ PlaneSplitterIndex(index)
+ });
+
+ Picture3DContext::In {
+ root_data: if parent_is_3d {
+ None
+ } else {
+ Some(Vec::new())
+ },
+ plane_splitter_index,
+ ancestor_index,
+ }
+ } else {
+ Picture3DContext::Out
+ };
+
+ // Force an intermediate surface if the stacking context has a
+ // complex clip node. In the future, we may decide during
+ // prepare step to skip the intermediate surface if the
+ // clip node doesn't affect the stacking context rect.
+ let mut blit_reason = BlitReason::empty();
+
+ // If this stacking context has any complex clips, we need to draw it
+ // to an off-screen surface.
+ if let Some(clip_chain_id) = clip_chain_id {
+ if self.clip_tree_builder.clip_chain_has_complex_clips(clip_chain_id, &self.interners) {
+ blit_reason |= BlitReason::CLIP;
+ }
+ }
+
+ // Check if we know this stacking context is redundant (doesn't need a surface)
+ // The check for blend-container redundancy is more involved so it's handled below.
+ let mut is_redundant = FlattenedStackingContext::is_redundant(
+ &context_3d,
+ &composite_ops,
+ blit_reason,
+ self.sc_stack.last(),
+ prim_flags,
+ );
+
+ // If the stacking context is a blend container, and if we're at the top level
+ // of the stacking context tree, we may be able to make this blend container into a tile
+ // cache. This means that we get caching and correct scrolling invalidation for
+ // root level blend containers. For these cases, the readbacks of the backdrop
+ // are handled by doing partial reads of the picture cache tiles during rendering.
+ if flags.contains(StackingContextFlags::IS_BLEND_CONTAINER) {
+ // Check if we're inside a stacking context hierarchy with an existing surface
+ match self.sc_stack.last() {
+ Some(_) => {
+ // If we are already inside a stacking context hierarchy with a surface, then we
+ // need to do the normal isolate of this blend container as a regular surface
+ blit_reason |= BlitReason::ISOLATE;
+ is_redundant = false;
+ }
+ None => {
+ // If the current slice is empty, then we can just mark the slice as
+ // atomic (so that compositor surfaces don't get promoted within it)
+ // and use that slice as the backing surface for the blend container
+ if self.tile_cache_builder.is_current_slice_empty() &&
+ self.spatial_tree.is_root_coord_system(spatial_node_index) &&
+ !self.clip_tree_builder.clip_node_has_complex_clips(clip_node_id, &self.interners)
+ {
+ self.add_tile_cache_barrier_if_needed(SliceFlags::IS_ATOMIC);
+ self.tile_cache_builder.make_current_slice_atomic();
+ } else {
+ // If the slice wasn't empty, we need to isolate a separate surface
+ // to ensure that the content already in the slice is not used as
+ // an input to the mix-blend composite
+ blit_reason |= BlitReason::ISOLATE;
+ is_redundant = false;
+ }
+ }
+ }
+ }
+
+ // If stacking context is a scrollbar, force a new slice for the primitives
+ // within. The stacking context will be redundant and removed by above check.
+ let set_tile_cache_barrier = prim_flags.contains(PrimitiveFlags::IS_SCROLLBAR_CONTAINER);
+
+ if set_tile_cache_barrier {
+ self.add_tile_cache_barrier_if_needed(SliceFlags::IS_SCROLLBAR);
+ }
+
+ let mut sc_info = StackingContextInfo {
+ pop_stacking_context: false,
+ pop_containing_block: false,
+ set_tile_cache_barrier,
+ };
+
+ // If this is not 3d, then it establishes an ancestor root for child 3d contexts.
+ if !participating_in_3d_context {
+ sc_info.pop_containing_block = true;
+ self.containing_block_stack.push(spatial_node_index);
+ }
+
+ // If not redundant, create a stacking context to hold primitive clusters
+ if !is_redundant {
+ sc_info.pop_stacking_context = true;
+
+ // Push the SC onto the stack, so we know how to handle things in
+ // pop_stacking_context.
+ self.sc_stack.push(FlattenedStackingContext {
+ prim_list: PrimitiveList::empty(),
+ prim_flags,
+ spatial_node_index,
+ clip_node_id,
+ composite_ops,
+ blit_reason,
+ transform_style,
+ context_3d,
+ flags,
+ raster_space: new_space,
+ });
+ }
+
+ sc_info
+ }
+
+ fn pop_stacking_context(
+ &mut self,
+ info: StackingContextInfo,
+ ) {
+ profile_scope!("pop_stacking_context");
+
+ self.clip_tree_builder.pop_clip();
+
+ // Pop off current raster space (pushed unconditionally in push_stacking_context)
+ self.raster_space_stack.pop().unwrap();
+
+ // If the stacking context formed a containing block, pop off the stack
+ if info.pop_containing_block {
+ self.containing_block_stack.pop().unwrap();
+ }
+
+ if info.set_tile_cache_barrier {
+ self.add_tile_cache_barrier_if_needed(SliceFlags::empty());
+ }
+
+ // If the stacking context was otherwise redundant, early exit
+ if !info.pop_stacking_context {
+ return;
+ }
+
+ let stacking_context = self.sc_stack.pop().unwrap();
+
+ let mut source = match stacking_context.context_3d {
+ // TODO(gw): For now, as soon as this picture is in
+ // a 3D context, we draw it to an intermediate
+ // surface and apply plane splitting. However,
+ // there is a large optimization opportunity here.
+ // During culling, we can check if there is actually
+ // perspective present, and skip the plane splitting
+ // completely when that is not the case.
+ Picture3DContext::In { ancestor_index, plane_splitter_index, .. } => {
+ let composite_mode = Some(
+ PictureCompositeMode::Blit(BlitReason::PRESERVE3D | stacking_context.blit_reason)
+ );
+
+ // Add picture for this actual stacking context contents to render into.
+ let pic_index = PictureIndex(self.prim_store.pictures
+ .alloc()
+ .init(PicturePrimitive::new_image(
+ composite_mode.clone(),
+ Picture3DContext::In { root_data: None, ancestor_index, plane_splitter_index },
+ stacking_context.prim_flags,
+ stacking_context.prim_list,
+ stacking_context.spatial_node_index,
+ stacking_context.raster_space,
+ PictureFlags::empty(),
+ ))
+ );
+
+ let instance = create_prim_instance(
+ pic_index,
+ composite_mode.into(),
+ stacking_context.raster_space,
+ stacking_context.clip_node_id,
+ &mut self.interners,
+ &mut self.clip_tree_builder,
+ );
+
+ PictureChainBuilder::from_instance(
+ instance,
+ stacking_context.prim_flags,
+ stacking_context.spatial_node_index,
+ stacking_context.raster_space,
+ )
+ }
+ Picture3DContext::Out => {
+ if stacking_context.blit_reason.is_empty() {
+ PictureChainBuilder::from_prim_list(
+ stacking_context.prim_list,
+ stacking_context.prim_flags,
+ stacking_context.spatial_node_index,
+ stacking_context.raster_space,
+ false,
+ )
+ } else {
+ let composite_mode = Some(
+ PictureCompositeMode::Blit(stacking_context.blit_reason)
+ );
+
+ // Add picture for this actual stacking context contents to render into.
+ let pic_index = PictureIndex(self.prim_store.pictures
+ .alloc()
+ .init(PicturePrimitive::new_image(
+ composite_mode.clone(),
+ Picture3DContext::Out,
+ stacking_context.prim_flags,
+ stacking_context.prim_list,
+ stacking_context.spatial_node_index,
+ stacking_context.raster_space,
+ PictureFlags::empty(),
+ ))
+ );
+
+ let instance = create_prim_instance(
+ pic_index,
+ composite_mode.into(),
+ stacking_context.raster_space,
+ stacking_context.clip_node_id,
+ &mut self.interners,
+ &mut self.clip_tree_builder,
+ );
+
+ PictureChainBuilder::from_instance(
+ instance,
+ stacking_context.prim_flags,
+ stacking_context.spatial_node_index,
+ stacking_context.raster_space,
+ )
+ }
+ }
+ };
+
+ // If establishing a 3d context, the `cur_instance` represents
+ // a picture with all the *trailing* immediate children elements.
+ // We append this to the preserve-3D picture set and make a container picture of them.
+ if let Picture3DContext::In { root_data: Some(mut prims), ancestor_index, plane_splitter_index } = stacking_context.context_3d {
+ let instance = source.finalize(
+ ClipNodeId::NONE,
+ &mut self.interners,
+ &mut self.prim_store,
+ &mut self.clip_tree_builder,
+ );
+
+ prims.push(ExtendedPrimitiveInstance {
+ instance,
+ spatial_node_index: stacking_context.spatial_node_index,
+ flags: stacking_context.prim_flags,
+ });
+
+ let mut prim_list = PrimitiveList::empty();
+
+ // Web content often specifies `preserve-3d` on pages that don't actually need
+ // a 3d rendering context (as a hint / hack to convince other browsers to
+ // layerize these elements to an off-screen surface). Detect cases where the
+ // preserve-3d has no effect on correctness and convert them to pass-through
+ // pictures instead. This has two benefits for WR:
+ //
+ // (1) We get correct subpixel-snapping behavior between preserve-3d elements
+ // that don't have complex transforms without additional complexity of
+ // handling subpixel-snapping across different surfaces.
+ // (2) We can draw this content directly in to the parent surface / tile cache,
+ // which is a performance win by avoiding allocating, drawing,
+ // plane-splitting and blitting an off-screen surface.
+ let mut needs_3d_context = false;
+
+ for ext_prim in prims.drain(..) {
+ // If all the preserve-3d elements are in the root coordinate system, we
+ // know that there is no need for a true 3d rendering context / plane-split.
+ // TODO(gw): We can expand this in future to handle this in more cases
+ // (e.g. a non-root coord system that is 2d within the 3d context).
+ if !self.spatial_tree.is_root_coord_system(ext_prim.spatial_node_index) {
+ needs_3d_context = true;
+ }
+
+ prim_list.add_prim(
+ ext_prim.instance,
+ LayoutRect::zero(),
+ ext_prim.spatial_node_index,
+ ext_prim.flags,
+ &mut self.prim_instances,
+ &self.clip_tree_builder,
+ );
+ }
+
+ let context_3d = if needs_3d_context {
+ Picture3DContext::In {
+ root_data: Some(Vec::new()),
+ ancestor_index,
+ plane_splitter_index,
+ }
+ } else {
+ // If we didn't need a 3d rendering context, walk the child pictures
+ // that make up this context and disable the off-screen surface and
+ // 3d render context.
+ for child_pic_index in &prim_list.child_pictures {
+ let child_pic = &mut self.prim_store.pictures[child_pic_index.0];
+ child_pic.composite_mode = None;
+ child_pic.context_3d = Picture3DContext::Out;
+ }
+
+ Picture3DContext::Out
+ };
+
+ // This is the acttual picture representing our 3D hierarchy root.
+ let pic_index = PictureIndex(self.prim_store.pictures
+ .alloc()
+ .init(PicturePrimitive::new_image(
+ None,
+ context_3d,
+ stacking_context.prim_flags,
+ prim_list,
+ stacking_context.spatial_node_index,
+ stacking_context.raster_space,
+ PictureFlags::empty(),
+ ))
+ );
+
+ let instance = create_prim_instance(
+ pic_index,
+ PictureCompositeKey::Identity,
+ stacking_context.raster_space,
+ stacking_context.clip_node_id,
+ &mut self.interners,
+ &mut self.clip_tree_builder,
+ );
+
+ source = PictureChainBuilder::from_instance(
+ instance,
+ stacking_context.prim_flags,
+ stacking_context.spatial_node_index,
+ stacking_context.raster_space,
+ );
+ }
+
+ let has_filters = stacking_context.composite_ops.has_valid_filters();
+
+ source = self.wrap_prim_with_filters(
+ source,
+ stacking_context.clip_node_id,
+ stacking_context.composite_ops.filters,
+ stacking_context.composite_ops.filter_primitives,
+ stacking_context.composite_ops.filter_datas,
+ None,
+ );
+
+ // Same for mix-blend-mode, except we can skip if this primitive is the first in the parent
+ // stacking context.
+ // From https://drafts.fxtf.org/compositing-1/#generalformula, the formula for blending is:
+ // Cs = (1 - ab) x Cs + ab x Blend(Cb, Cs)
+ // where
+ // Cs = Source color
+ // ab = Backdrop alpha
+ // Cb = Backdrop color
+ //
+ // If we're the first primitive within a stacking context, then we can guarantee that the
+ // backdrop alpha will be 0, and then the blend equation collapses to just
+ // Cs = Cs, and the blend mode isn't taken into account at all.
+ if let Some(mix_blend_mode) = stacking_context.composite_ops.mix_blend_mode {
+ let composite_mode = PictureCompositeMode::MixBlend(mix_blend_mode);
+
+ source = source.add_picture(
+ composite_mode,
+ stacking_context.clip_node_id,
+ Picture3DContext::Out,
+ &mut self.interners,
+ &mut self.prim_store,
+ &mut self.prim_instances,
+ &mut self.clip_tree_builder,
+ );
+ }
+
+ // Set the stacking context clip on the outermost picture in the chain,
+ // unless we already set it on the leaf picture.
+ let cur_instance = source.finalize(
+ stacking_context.clip_node_id,
+ &mut self.interners,
+ &mut self.prim_store,
+ &mut self.clip_tree_builder,
+ );
+
+ // The primitive instance for the remainder of flat children of this SC
+ // if it's a part of 3D hierarchy but not the root of it.
+ let trailing_children_instance = match self.sc_stack.last_mut() {
+ // Preserve3D path (only relevant if there are no filters/mix-blend modes)
+ Some(ref parent_sc) if !has_filters && parent_sc.is_3d() => {
+ Some(cur_instance)
+ }
+ // Regular parenting path
+ Some(ref mut parent_sc) => {
+ parent_sc.prim_list.add_prim(
+ cur_instance,
+ LayoutRect::zero(),
+ stacking_context.spatial_node_index,
+ stacking_context.prim_flags,
+ &mut self.prim_instances,
+ &self.clip_tree_builder,
+ );
+ None
+ }
+ // This must be the root stacking context
+ None => {
+ self.add_primitive_to_draw_list(
+ cur_instance,
+ LayoutRect::zero(),
+ stacking_context.spatial_node_index,
+ stacking_context.prim_flags,
+ );
+
+ None
+ }
+ };
+
+ // finally, if there any outstanding 3D primitive instances,
+ // find the 3D hierarchy root and add them there.
+ if let Some(instance) = trailing_children_instance {
+ self.add_primitive_instance_to_3d_root(ExtendedPrimitiveInstance {
+ instance,
+ spatial_node_index: stacking_context.spatial_node_index,
+ flags: stacking_context.prim_flags,
+ });
+ }
+
+ assert!(
+ self.pending_shadow_items.is_empty(),
+ "Found unpopped shadows when popping stacking context!"
+ );
+ }
+
+ pub fn push_reference_frame(
+ &mut self,
+ reference_frame_id: SpatialId,
+ parent_index: SpatialNodeIndex,
+ pipeline_id: PipelineId,
+ transform_style: TransformStyle,
+ source_transform: PropertyBinding<LayoutTransform>,
+ kind: ReferenceFrameKind,
+ origin_in_parent_reference_frame: LayoutVector2D,
+ uid: SpatialNodeUid,
+ ) -> SpatialNodeIndex {
+ let index = self.spatial_tree.add_reference_frame(
+ parent_index,
+ transform_style,
+ source_transform,
+ kind,
+ origin_in_parent_reference_frame,
+ pipeline_id,
+ uid,
+ );
+ self.id_to_index_mapper_stack.last_mut().unwrap().add_spatial_node(reference_frame_id, index);
+
+ index
+ }
+
+ fn push_root(
+ &mut self,
+ pipeline_id: PipelineId,
+ instance: PipelineInstanceId,
+ ) {
+ let spatial_node_index = self.push_reference_frame(
+ SpatialId::root_reference_frame(pipeline_id),
+ self.spatial_tree.root_reference_frame_index(),
+ pipeline_id,
+ TransformStyle::Flat,
+ PropertyBinding::Value(LayoutTransform::identity()),
+ ReferenceFrameKind::Transform {
+ is_2d_scale_translation: true,
+ should_snap: true,
+ paired_with_perspective: false,
+ },
+ LayoutVector2D::zero(),
+ SpatialNodeUid::root_reference_frame(pipeline_id, instance),
+ );
+
+ let viewport_rect = LayoutRect::max_rect();
+
+ self.add_scroll_frame(
+ SpatialId::root_scroll_node(pipeline_id),
+ spatial_node_index,
+ ExternalScrollId(0, pipeline_id),
+ pipeline_id,
+ &viewport_rect,
+ &viewport_rect.size(),
+ ScrollFrameKind::PipelineRoot {
+ is_root_pipeline: true,
+ },
+ LayoutVector2D::zero(),
+ APZScrollGeneration::default(),
+ HasScrollLinkedEffect::No,
+ SpatialNodeUid::root_scroll_frame(pipeline_id, instance),
+ );
+ }
+
+ fn add_image_mask_clip_node(
+ &mut self,
+ new_node_id: ClipId,
+ spatial_id: SpatialId,
+ image_mask: &ImageMask,
+ fill_rule: FillRule,
+ points_range: ItemRange<LayoutPoint>,
+ ) {
+ let spatial_node_index = self.get_space(spatial_id);
+
+ let snapped_mask_rect = self.snap_rect(
+ &image_mask.rect,
+ spatial_node_index,
+ );
+ let points: Vec<LayoutPoint> = points_range.iter().collect();
+
+ // If any points are provided, then intern a polygon with the points and fill rule.
+ let mut polygon_handle: Option<PolygonDataHandle> = None;
+ if points.len() > 0 {
+ let item = PolygonKey::new(&points, fill_rule);
+
+ let handle = self
+ .interners
+ .polygon
+ .intern(&item, || item);
+ polygon_handle = Some(handle);
+ }
+
+ let item = ClipItemKey {
+ kind: ClipItemKeyKind::image_mask(image_mask, snapped_mask_rect, polygon_handle),
+ spatial_node_index,
+ };
+
+ let handle = self
+ .interners
+ .clip
+ .intern(&item, || {
+ ClipInternData {
+ key: item,
+ }
+ });
+
+ self.clip_tree_builder.define_image_mask_clip(
+ new_node_id,
+ handle,
+ );
+ }
+
+ /// Add a new rectangle clip, positioned by the spatial node in the `space_and_clip`.
+ fn add_rect_clip_node(
+ &mut self,
+ new_node_id: ClipId,
+ spatial_id: SpatialId,
+ clip_rect: &LayoutRect,
+ ) {
+ let spatial_node_index = self.get_space(spatial_id);
+
+ let snapped_clip_rect = self.snap_rect(
+ clip_rect,
+ spatial_node_index,
+ );
+
+ let item = ClipItemKey {
+ kind: ClipItemKeyKind::rectangle(snapped_clip_rect, ClipMode::Clip),
+ spatial_node_index,
+ };
+ let handle = self
+ .interners
+ .clip
+ .intern(&item, || {
+ ClipInternData {
+ key: item,
+ }
+ });
+
+ self.clip_tree_builder.define_rect_clip(
+ new_node_id,
+ handle,
+ );
+ }
+
+ fn add_rounded_rect_clip_node(
+ &mut self,
+ new_node_id: ClipId,
+ spatial_id: SpatialId,
+ clip: &ComplexClipRegion,
+ ) {
+ let spatial_node_index = self.get_space(spatial_id);
+
+ let snapped_region_rect = self.snap_rect(
+ &clip.rect,
+ spatial_node_index,
+ );
+ let item = ClipItemKey {
+ kind: ClipItemKeyKind::rounded_rect(
+ snapped_region_rect,
+ clip.radii,
+ clip.mode,
+ ),
+ spatial_node_index,
+ };
+
+ let handle = self
+ .interners
+ .clip
+ .intern(&item, || {
+ ClipInternData {
+ key: item,
+ }
+ });
+
+ self.clip_tree_builder.define_rounded_rect_clip(
+ new_node_id,
+ handle,
+ );
+ }
+
+ pub fn add_scroll_frame(
+ &mut self,
+ new_node_id: SpatialId,
+ parent_node_index: SpatialNodeIndex,
+ external_id: ExternalScrollId,
+ pipeline_id: PipelineId,
+ frame_rect: &LayoutRect,
+ content_size: &LayoutSize,
+ frame_kind: ScrollFrameKind,
+ external_scroll_offset: LayoutVector2D,
+ scroll_offset_generation: APZScrollGeneration,
+ has_scroll_linked_effect: HasScrollLinkedEffect,
+ uid: SpatialNodeUid,
+ ) -> SpatialNodeIndex {
+ let node_index = self.spatial_tree.add_scroll_frame(
+ parent_node_index,
+ external_id,
+ pipeline_id,
+ frame_rect,
+ content_size,
+ frame_kind,
+ external_scroll_offset,
+ scroll_offset_generation,
+ has_scroll_linked_effect,
+ uid,
+ );
+ self.id_to_index_mapper_stack.last_mut().unwrap().add_spatial_node(new_node_id, node_index);
+ node_index
+ }
+
+ pub fn push_shadow(
+ &mut self,
+ shadow: Shadow,
+ spatial_node_index: SpatialNodeIndex,
+ clip_chain_id: api::ClipChainId,
+ should_inflate: bool,
+ ) {
+ self.clip_tree_builder.push_clip_chain(Some(clip_chain_id), false);
+
+ // Store this shadow in the pending list, for processing
+ // during pop_all_shadows.
+ self.pending_shadow_items.push_back(ShadowItem::Shadow(PendingShadow {
+ shadow,
+ spatial_node_index,
+ should_inflate,
+ }));
+ }
+
+ pub fn pop_all_shadows(
+ &mut self,
+ ) {
+ assert!(!self.pending_shadow_items.is_empty(), "popped shadows, but none were present");
+
+ let mut items = mem::replace(&mut self.pending_shadow_items, VecDeque::new());
+
+ //
+ // The pending_shadow_items queue contains a list of shadows and primitives
+ // that were pushed during the active shadow context. To process these, we:
+ //
+ // Iterate the list, popping an item from the front each iteration.
+ //
+ // If the item is a shadow:
+ // - Create a shadow picture primitive.
+ // - Add *any* primitives that remain in the item list to this shadow.
+ // If the item is a primitive:
+ // - Add that primitive as a normal item (if alpha > 0)
+ //
+
+ while let Some(item) = items.pop_front() {
+ match item {
+ ShadowItem::Shadow(pending_shadow) => {
+ // Quote from https://drafts.csswg.org/css-backgrounds-3/#shadow-blur
+ // "the image that would be generated by applying to the shadow a
+ // Gaussian blur with a standard deviation equal to half the blur radius."
+ let std_deviation = pending_shadow.shadow.blur_radius * 0.5;
+
+ // Add any primitives that come after this shadow in the item
+ // list to this shadow.
+ let mut prim_list = PrimitiveList::empty();
+ let blur_filter = Filter::Blur {
+ width: std_deviation,
+ height: std_deviation,
+ should_inflate: pending_shadow.should_inflate,
+ };
+ let blur_is_noop = blur_filter.is_noop();
+
+ for item in &items {
+ let (instance, info, spatial_node_index) = match item {
+ ShadowItem::Image(ref pending_image) => {
+ self.create_shadow_prim(
+ &pending_shadow,
+ pending_image,
+ blur_is_noop,
+ )
+ }
+ ShadowItem::LineDecoration(ref pending_line_dec) => {
+ self.create_shadow_prim(
+ &pending_shadow,
+ pending_line_dec,
+ blur_is_noop,
+ )
+ }
+ ShadowItem::NormalBorder(ref pending_border) => {
+ self.create_shadow_prim(
+ &pending_shadow,
+ pending_border,
+ blur_is_noop,
+ )
+ }
+ ShadowItem::Primitive(ref pending_primitive) => {
+ self.create_shadow_prim(
+ &pending_shadow,
+ pending_primitive,
+ blur_is_noop,
+ )
+ }
+ ShadowItem::TextRun(ref pending_text_run) => {
+ self.create_shadow_prim(
+ &pending_shadow,
+ pending_text_run,
+ blur_is_noop,
+ )
+ }
+ _ => {
+ continue;
+ }
+ };
+
+ if blur_is_noop {
+ self.add_primitive_to_draw_list(
+ instance,
+ info.rect,
+ spatial_node_index,
+ info.flags,
+ );
+ } else {
+ prim_list.add_prim(
+ instance,
+ info.rect,
+ spatial_node_index,
+ info.flags,
+ &mut self.prim_instances,
+ &self.clip_tree_builder,
+ );
+ }
+ }
+
+ // No point in adding a shadow here if there were no primitives
+ // added to the shadow.
+ if !prim_list.is_empty() {
+ // Create a picture that the shadow primitives will be added to. If the
+ // blur radius is 0, the code in Picture::prepare_for_render will
+ // detect this and mark the picture to be drawn directly into the
+ // parent picture, which avoids an intermediate surface and blur.
+ assert!(!blur_filter.is_noop());
+ let composite_mode = Some(PictureCompositeMode::Filter(blur_filter));
+ let composite_mode_key = composite_mode.clone().into();
+ let raster_space = RasterSpace::Screen;
+
+ // Create the primitive to draw the shadow picture into the scene.
+ let shadow_pic_index = PictureIndex(self.prim_store.pictures
+ .alloc()
+ .init(PicturePrimitive::new_image(
+ composite_mode,
+ Picture3DContext::Out,
+ PrimitiveFlags::IS_BACKFACE_VISIBLE,
+ prim_list,
+ pending_shadow.spatial_node_index,
+ raster_space,
+ PictureFlags::empty(),
+ ))
+ );
+
+ let shadow_pic_key = PictureKey::new(
+ Picture { composite_mode_key, raster_space },
+ );
+
+ let shadow_prim_data_handle = self.interners
+ .picture
+ .intern(&shadow_pic_key, || ());
+
+ let clip_node_id = self.clip_tree_builder.build_clip_set(api::ClipChainId::INVALID);
+
+ let shadow_prim_instance = PrimitiveInstance::new(
+ PrimitiveInstanceKind::Picture {
+ data_handle: shadow_prim_data_handle,
+ pic_index: shadow_pic_index,
+ segment_instance_index: SegmentInstanceIndex::INVALID,
+ },
+ self.clip_tree_builder.build_for_picture(clip_node_id),
+ );
+
+ // Add the shadow primitive. This must be done before pushing this
+ // picture on to the shadow stack, to avoid infinite recursion!
+ self.add_primitive_to_draw_list(
+ shadow_prim_instance,
+ LayoutRect::zero(),
+ pending_shadow.spatial_node_index,
+ PrimitiveFlags::IS_BACKFACE_VISIBLE,
+ );
+ }
+
+ self.clip_tree_builder.pop_clip();
+ }
+ ShadowItem::Image(pending_image) => {
+ self.add_shadow_prim_to_draw_list(
+ pending_image,
+ )
+ },
+ ShadowItem::LineDecoration(pending_line_dec) => {
+ self.add_shadow_prim_to_draw_list(
+ pending_line_dec,
+ )
+ },
+ ShadowItem::NormalBorder(pending_border) => {
+ self.add_shadow_prim_to_draw_list(
+ pending_border,
+ )
+ },
+ ShadowItem::Primitive(pending_primitive) => {
+ self.add_shadow_prim_to_draw_list(
+ pending_primitive,
+ )
+ },
+ ShadowItem::TextRun(pending_text_run) => {
+ self.add_shadow_prim_to_draw_list(
+ pending_text_run,
+ )
+ },
+ }
+ }
+
+ debug_assert!(items.is_empty());
+ self.pending_shadow_items = items;
+ }
+
+ fn create_shadow_prim<P>(
+ &mut self,
+ pending_shadow: &PendingShadow,
+ pending_primitive: &PendingPrimitive<P>,
+ blur_is_noop: bool,
+ ) -> (PrimitiveInstance, LayoutPrimitiveInfo, SpatialNodeIndex)
+ where
+ P: InternablePrimitive + CreateShadow,
+ Interners: AsMut<Interner<P>>,
+ {
+ // Offset the local rect and clip rect by the shadow offset. The pending
+ // primitive has already been snapped, but we will need to snap the
+ // shadow after translation. We don't need to worry about the size
+ // changing because the shadow has the same raster space as the
+ // primitive, and thus we know the size is already rounded.
+ let mut info = pending_primitive.info.clone();
+ info.rect = info.rect.translate(pending_shadow.shadow.offset);
+ info.clip_rect = info.clip_rect.translate(pending_shadow.shadow.offset);
+
+ let clip_set = self.clip_tree_builder.build_for_prim(
+ pending_primitive.clip_node_id,
+ &info,
+ &[],
+ &mut self.interners,
+ );
+
+ // Construct and add a primitive for the given shadow.
+ let shadow_prim_instance = self.create_primitive(
+ &info,
+ pending_primitive.spatial_node_index,
+ clip_set,
+ pending_primitive.prim.create_shadow(
+ &pending_shadow.shadow,
+ blur_is_noop,
+ self.raster_space_stack.last().cloned().unwrap(),
+ ),
+ );
+
+ (shadow_prim_instance, info, pending_primitive.spatial_node_index)
+ }
+
+ fn add_shadow_prim_to_draw_list<P>(
+ &mut self,
+ pending_primitive: PendingPrimitive<P>,
+ ) where
+ P: InternablePrimitive + IsVisible,
+ Interners: AsMut<Interner<P>>,
+ {
+ // For a normal primitive, if it has alpha > 0, then we add this
+ // as a normal primitive to the parent picture.
+ if pending_primitive.prim.is_visible() {
+ let clip_set = self.clip_tree_builder.build_for_prim(
+ pending_primitive.clip_node_id,
+ &pending_primitive.info,
+ &[],
+ &mut self.interners,
+ );
+
+ self.add_prim_to_draw_list(
+ &pending_primitive.info,
+ pending_primitive.spatial_node_index,
+ clip_set,
+ pending_primitive.prim,
+ );
+ }
+ }
+
+ pub fn add_clear_rectangle(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ clip_node_id: ClipNodeId,
+ info: &LayoutPrimitiveInfo,
+ ) {
+ // Clear prims must be in their own picture cache slice to
+ // be composited correctly.
+ self.add_tile_cache_barrier_if_needed(SliceFlags::empty());
+
+ self.add_primitive(
+ spatial_node_index,
+ clip_node_id,
+ info,
+ Vec::new(),
+ PrimitiveKeyKind::Clear,
+ );
+
+ self.add_tile_cache_barrier_if_needed(SliceFlags::empty());
+ }
+
+ pub fn add_line(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ clip_node_id: ClipNodeId,
+ info: &LayoutPrimitiveInfo,
+ wavy_line_thickness: f32,
+ orientation: LineOrientation,
+ color: ColorF,
+ style: LineStyle,
+ ) {
+ // For line decorations, we can construct the render task cache key
+ // here during scene building, since it doesn't depend on device
+ // pixel ratio or transform.
+ let size = get_line_decoration_size(
+ &info.rect.size(),
+ orientation,
+ style,
+ wavy_line_thickness,
+ );
+
+ let cache_key = size.map(|size| {
+ LineDecorationCacheKey {
+ style,
+ orientation,
+ wavy_line_thickness: Au::from_f32_px(wavy_line_thickness),
+ size: size.to_au(),
+ }
+ });
+
+ self.add_primitive(
+ spatial_node_index,
+ clip_node_id,
+ &info,
+ Vec::new(),
+ LineDecoration {
+ cache_key,
+ color: color.into(),
+ },
+ );
+ }
+
+ pub fn add_border(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ clip_node_id: ClipNodeId,
+ info: &LayoutPrimitiveInfo,
+ border_item: &BorderDisplayItem,
+ gradient_stops: ItemRange<GradientStop>,
+ ) {
+ match border_item.details {
+ BorderDetails::NinePatch(ref border) => {
+ let nine_patch = NinePatchDescriptor {
+ width: border.width,
+ height: border.height,
+ slice: border.slice,
+ fill: border.fill,
+ repeat_horizontal: border.repeat_horizontal,
+ repeat_vertical: border.repeat_vertical,
+ widths: border_item.widths.into(),
+ };
+
+ match border.source {
+ NinePatchBorderSource::Image(key, rendering) => {
+ let prim = ImageBorder {
+ request: ImageRequest {
+ key,
+ rendering,
+ tile: None,
+ },
+ nine_patch,
+ };
+
+ self.add_nonshadowable_primitive(
+ spatial_node_index,
+ clip_node_id,
+ info,
+ Vec::new(),
+ prim,
+ );
+ }
+ NinePatchBorderSource::Gradient(gradient) => {
+ let prim = match self.create_linear_gradient_prim(
+ &info,
+ gradient.start_point,
+ gradient.end_point,
+ read_gradient_stops(gradient_stops),
+ gradient.extend_mode,
+ LayoutSize::new(border.height as f32, border.width as f32),
+ LayoutSize::zero(),
+ Some(Box::new(nine_patch)),
+ EdgeAaSegmentMask::all(),
+ ) {
+ Some(prim) => prim,
+ None => return,
+ };
+
+ self.add_nonshadowable_primitive(
+ spatial_node_index,
+ clip_node_id,
+ info,
+ Vec::new(),
+ prim,
+ );
+ }
+ NinePatchBorderSource::RadialGradient(gradient) => {
+ let prim = self.create_radial_gradient_prim(
+ &info,
+ gradient.center,
+ gradient.start_offset * gradient.radius.width,
+ gradient.end_offset * gradient.radius.width,
+ gradient.radius.width / gradient.radius.height,
+ read_gradient_stops(gradient_stops),
+ gradient.extend_mode,
+ LayoutSize::new(border.height as f32, border.width as f32),
+ LayoutSize::zero(),
+ Some(Box::new(nine_patch)),
+ );
+
+ self.add_nonshadowable_primitive(
+ spatial_node_index,
+ clip_node_id,
+ info,
+ Vec::new(),
+ prim,
+ );
+ }
+ NinePatchBorderSource::ConicGradient(gradient) => {
+ let prim = self.create_conic_gradient_prim(
+ &info,
+ gradient.center,
+ gradient.angle,
+ gradient.start_offset,
+ gradient.end_offset,
+ gradient_stops,
+ gradient.extend_mode,
+ LayoutSize::new(border.height as f32, border.width as f32),
+ LayoutSize::zero(),
+ Some(Box::new(nine_patch)),
+ );
+
+ self.add_nonshadowable_primitive(
+ spatial_node_index,
+ clip_node_id,
+ info,
+ Vec::new(),
+ prim,
+ );
+ }
+ };
+ }
+ BorderDetails::Normal(ref border) => {
+ self.add_normal_border(
+ info,
+ border,
+ border_item.widths,
+ spatial_node_index,
+ clip_node_id,
+ );
+ }
+ }
+ }
+
+ pub fn create_linear_gradient_prim(
+ &mut self,
+ info: &LayoutPrimitiveInfo,
+ start_point: LayoutPoint,
+ end_point: LayoutPoint,
+ stops: Vec<GradientStopKey>,
+ extend_mode: ExtendMode,
+ stretch_size: LayoutSize,
+ mut tile_spacing: LayoutSize,
+ nine_patch: Option<Box<NinePatchDescriptor>>,
+ edge_aa_mask: EdgeAaSegmentMask,
+ ) -> Option<LinearGradient> {
+ let mut prim_rect = info.rect;
+ simplify_repeated_primitive(&stretch_size, &mut tile_spacing, &mut prim_rect);
+
+ let mut has_hard_stops = false;
+ let mut is_entirely_transparent = true;
+ let mut prev_stop = None;
+ for stop in &stops {
+ if Some(stop.offset) == prev_stop {
+ has_hard_stops = true;
+ }
+ prev_stop = Some(stop.offset);
+ if stop.color.a > 0 {
+ is_entirely_transparent = false;
+ }
+ }
+
+ // If all the stops have no alpha, then this
+ // gradient can't contribute to the scene.
+ if is_entirely_transparent {
+ return None;
+ }
+
+ // Try to ensure that if the gradient is specified in reverse, then so long as the stops
+ // are also supplied in reverse that the rendered result will be equivalent. To do this,
+ // a reference orientation for the gradient line must be chosen, somewhat arbitrarily, so
+ // just designate the reference orientation as start < end. Aligned gradient rendering
+ // manages to produce the same result regardless of orientation, so don't worry about
+ // reversing in that case.
+ let reverse_stops = start_point.x > end_point.x ||
+ (start_point.x == end_point.x && start_point.y > end_point.y);
+
+ // To get reftests exactly matching with reverse start/end
+ // points, it's necessary to reverse the gradient
+ // line in some cases.
+ let (sp, ep) = if reverse_stops {
+ (end_point, start_point)
+ } else {
+ (start_point, end_point)
+ };
+
+ // We set a limit to the resolution at which cached gradients are rendered.
+ // For most gradients this is fine but when there are hard stops this causes
+ // noticeable artifacts. If so, fall back to non-cached gradients.
+ let max = gradient::LINEAR_MAX_CACHED_SIZE;
+ let caching_causes_artifacts = has_hard_stops && (stretch_size.width > max || stretch_size.height > max);
+
+ let is_tiled = prim_rect.width() > stretch_size.width
+ || prim_rect.height() > stretch_size.height;
+ // SWGL has a fast-path that can render gradients faster than it can sample from the
+ // texture cache so we disable caching in this configuration. Cached gradients are
+ // faster on hardware.
+ let cached = (!self.config.is_software || is_tiled) && !caching_causes_artifacts;
+
+ Some(LinearGradient {
+ extend_mode,
+ start_point: sp.into(),
+ end_point: ep.into(),
+ stretch_size: stretch_size.into(),
+ tile_spacing: tile_spacing.into(),
+ stops,
+ reverse_stops,
+ nine_patch,
+ cached,
+ edge_aa_mask,
+ })
+ }
+
+ pub fn create_radial_gradient_prim(
+ &mut self,
+ info: &LayoutPrimitiveInfo,
+ center: LayoutPoint,
+ start_radius: f32,
+ end_radius: f32,
+ ratio_xy: f32,
+ stops: Vec<GradientStopKey>,
+ extend_mode: ExtendMode,
+ stretch_size: LayoutSize,
+ mut tile_spacing: LayoutSize,
+ nine_patch: Option<Box<NinePatchDescriptor>>,
+ ) -> RadialGradient {
+ let mut prim_rect = info.rect;
+ simplify_repeated_primitive(&stretch_size, &mut tile_spacing, &mut prim_rect);
+
+ let params = RadialGradientParams {
+ start_radius,
+ end_radius,
+ ratio_xy,
+ };
+
+ RadialGradient {
+ extend_mode,
+ center: center.into(),
+ params,
+ stretch_size: stretch_size.into(),
+ tile_spacing: tile_spacing.into(),
+ nine_patch,
+ stops,
+ }
+ }
+
+ pub fn create_conic_gradient_prim(
+ &mut self,
+ info: &LayoutPrimitiveInfo,
+ center: LayoutPoint,
+ angle: f32,
+ start_offset: f32,
+ end_offset: f32,
+ stops: ItemRange<GradientStop>,
+ extend_mode: ExtendMode,
+ stretch_size: LayoutSize,
+ mut tile_spacing: LayoutSize,
+ nine_patch: Option<Box<NinePatchDescriptor>>,
+ ) -> ConicGradient {
+ let mut prim_rect = info.rect;
+ simplify_repeated_primitive(&stretch_size, &mut tile_spacing, &mut prim_rect);
+
+ let stops = stops.iter().map(|stop| {
+ GradientStopKey {
+ offset: stop.offset,
+ color: stop.color.into(),
+ }
+ }).collect();
+
+ ConicGradient {
+ extend_mode,
+ center: center.into(),
+ params: ConicGradientParams { angle, start_offset, end_offset },
+ stretch_size: stretch_size.into(),
+ tile_spacing: tile_spacing.into(),
+ nine_patch,
+ stops,
+ }
+ }
+
+ pub fn add_text(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ clip_node_id: ClipNodeId,
+ prim_info: &LayoutPrimitiveInfo,
+ font_instance_key: &FontInstanceKey,
+ text_color: &ColorF,
+ glyph_range: ItemRange<GlyphInstance>,
+ glyph_options: Option<GlyphOptions>,
+ ) {
+ let offset = self.current_offset(spatial_node_index);
+
+ let text_run = {
+ let shared_key = self.fonts.instance_keys.map_key(font_instance_key);
+ let font_instance = match self.fonts.instances.get_font_instance(shared_key) {
+ Some(instance) => instance,
+ None => {
+ warn!("Unknown font instance key");
+ debug!("key={:?} shared={:?}", font_instance_key, shared_key);
+ return;
+ }
+ };
+
+ // Trivial early out checks
+ if font_instance.size <= FontSize::zero() {
+ return;
+ }
+
+ // TODO(gw): Use a proper algorithm to select
+ // whether this item should be rendered with
+ // subpixel AA!
+ let mut render_mode = self.config
+ .default_font_render_mode
+ .limit_by(font_instance.render_mode);
+ let mut flags = font_instance.flags;
+ if let Some(options) = glyph_options {
+ render_mode = render_mode.limit_by(options.render_mode);
+ flags |= options.flags;
+ }
+
+ let font = FontInstance::new(
+ font_instance,
+ (*text_color).into(),
+ render_mode,
+ flags,
+ );
+
+ // TODO(gw): It'd be nice not to have to allocate here for creating
+ // the primitive key, when the common case is that the
+ // hash will match and we won't end up creating a new
+ // primitive template.
+ let prim_offset = prim_info.rect.min.to_vector() - offset;
+ let glyphs = glyph_range
+ .iter()
+ .map(|glyph| {
+ GlyphInstance {
+ index: glyph.index,
+ point: glyph.point - prim_offset,
+ }
+ })
+ .collect();
+
+ // Query the current requested raster space (stack handled by push/pop
+ // stacking context).
+ let requested_raster_space = self.raster_space_stack
+ .last()
+ .cloned()
+ .unwrap();
+
+ TextRun {
+ glyphs: Arc::new(glyphs),
+ font,
+ shadow: false,
+ requested_raster_space,
+ }
+ };
+
+ self.add_primitive(
+ spatial_node_index,
+ clip_node_id,
+ prim_info,
+ Vec::new(),
+ text_run,
+ );
+ }
+
+ pub fn add_image(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ clip_node_id: ClipNodeId,
+ info: &LayoutPrimitiveInfo,
+ stretch_size: LayoutSize,
+ mut tile_spacing: LayoutSize,
+ image_key: ImageKey,
+ image_rendering: ImageRendering,
+ alpha_type: AlphaType,
+ color: ColorF,
+ ) {
+ let mut prim_rect = info.rect;
+ simplify_repeated_primitive(&stretch_size, &mut tile_spacing, &mut prim_rect);
+ let info = LayoutPrimitiveInfo {
+ rect: prim_rect,
+ .. *info
+ };
+
+ self.add_primitive(
+ spatial_node_index,
+ clip_node_id,
+ &info,
+ Vec::new(),
+ Image {
+ key: image_key,
+ tile_spacing: tile_spacing.into(),
+ stretch_size: stretch_size.into(),
+ color: color.into(),
+ image_rendering,
+ alpha_type,
+ },
+ );
+ }
+
+ pub fn add_yuv_image(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ clip_node_id: ClipNodeId,
+ info: &LayoutPrimitiveInfo,
+ yuv_data: YuvData,
+ color_depth: ColorDepth,
+ color_space: YuvColorSpace,
+ color_range: ColorRange,
+ image_rendering: ImageRendering,
+ ) {
+ let format = yuv_data.get_format();
+ let yuv_key = match yuv_data {
+ YuvData::NV12(plane_0, plane_1) => [plane_0, plane_1, ImageKey::DUMMY],
+ YuvData::P010(plane_0, plane_1) => [plane_0, plane_1, ImageKey::DUMMY],
+ YuvData::PlanarYCbCr(plane_0, plane_1, plane_2) => [plane_0, plane_1, plane_2],
+ YuvData::InterleavedYCbCr(plane_0) => [plane_0, ImageKey::DUMMY, ImageKey::DUMMY],
+ };
+
+ self.add_nonshadowable_primitive(
+ spatial_node_index,
+ clip_node_id,
+ info,
+ Vec::new(),
+ YuvImage {
+ color_depth,
+ yuv_key,
+ format,
+ color_space,
+ color_range,
+ image_rendering,
+ },
+ );
+ }
+
+ fn add_primitive_instance_to_3d_root(
+ &mut self,
+ prim: ExtendedPrimitiveInstance,
+ ) {
+ // find the 3D root and append to the children list
+ for sc in self.sc_stack.iter_mut().rev() {
+ match sc.context_3d {
+ Picture3DContext::In { root_data: Some(ref mut prims), .. } => {
+ prims.push(prim);
+ break;
+ }
+ Picture3DContext::In { .. } => {}
+ Picture3DContext::Out => panic!("Unable to find 3D root"),
+ }
+ }
+ }
+
+ #[allow(dead_code)]
+ pub fn add_backdrop_filter(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ clip_node_id: ClipNodeId,
+ info: &LayoutPrimitiveInfo,
+ filters: Vec<Filter>,
+ filter_datas: Vec<FilterData>,
+ filter_primitives: Vec<FilterPrimitive>,
+ ) {
+ // We don't know the spatial node for a backdrop filter, as it's whatever is the
+ // backdrop root, but we can't know this if the root is a picture cache slice
+ // (which is the common case). It will get resolved later during `finalize_picture`.
+ let filter_spatial_node_index = SpatialNodeIndex::UNKNOWN;
+
+ self.make_current_slice_atomic_if_required();
+
+ // Ensure we create a clip-chain for the capture primitive that matches
+ // the render primitive, otherwise one might get culled while the other
+ // is considered visible.
+ let clip_leaf_id = self.clip_tree_builder.build_for_prim(
+ clip_node_id,
+ info,
+ &[],
+ &mut self.interners,
+ );
+
+ // Create the backdrop prim - this is a placeholder which sets the size of resolve
+ // picture that reads from the backdrop root
+ let backdrop_capture_instance = self.create_primitive(
+ info,
+ spatial_node_index,
+ clip_leaf_id,
+ BackdropCapture {
+ },
+ );
+
+ // Create a prim_list for this backdrop prim and add to a picture chain builder, which
+ // is needed for the call to `wrap_prim_with_filters` below
+ let mut prim_list = PrimitiveList::empty();
+ prim_list.add_prim(
+ backdrop_capture_instance,
+ info.rect,
+ spatial_node_index,
+ info.flags,
+ &mut self.prim_instances,
+ &self.clip_tree_builder,
+ );
+
+ let mut source = PictureChainBuilder::from_prim_list(
+ prim_list,
+ info.flags,
+ filter_spatial_node_index,
+ RasterSpace::Screen,
+ true,
+ );
+
+ // Wrap the backdrop primitive picture with the filters that were specified. This
+ // produces a picture chain with 1+ pictures with the filter composite modes set.
+ source = self.wrap_prim_with_filters(
+ source,
+ clip_node_id,
+ filters,
+ filter_primitives,
+ filter_datas,
+ Some(false),
+ );
+
+ // If all the filters were no-ops (e.g. opacity(0)) then we don't get a picture here
+ // and we can skip adding the backdrop-filter.
+ if source.has_picture() {
+ source = source.add_picture(
+ PictureCompositeMode::IntermediateSurface,
+ clip_node_id,
+ Picture3DContext::Out,
+ &mut self.interners,
+ &mut self.prim_store,
+ &mut self.prim_instances,
+ &mut self.clip_tree_builder,
+ );
+
+ let filtered_instance = source.finalize(
+ clip_node_id,
+ &mut self.interners,
+ &mut self.prim_store,
+ &mut self.clip_tree_builder,
+ );
+
+ // Extract the pic index for the intermediate surface. We need to
+ // supply this to the capture prim below.
+ let output_pic_index = match filtered_instance.kind {
+ PrimitiveInstanceKind::Picture { pic_index, .. } => pic_index,
+ _ => panic!("bug: not a picture"),
+ };
+
+ // Find which stacking context (or root tile cache) to add the
+ // backdrop-filter chain to
+ let sc_index = self.sc_stack.iter().rposition(|sc| {
+ !sc.flags.contains(StackingContextFlags::WRAPS_BACKDROP_FILTER)
+ });
+
+ match sc_index {
+ Some(sc_index) => {
+ self.sc_stack[sc_index].prim_list.add_prim(
+ filtered_instance,
+ info.rect,
+ filter_spatial_node_index,
+ info.flags,
+ &mut self.prim_instances,
+ &self.clip_tree_builder,
+ );
+ }
+ None => {
+ self.tile_cache_builder.add_prim(
+ filtered_instance,
+ info.rect,
+ filter_spatial_node_index,
+ info.flags,
+ self.spatial_tree,
+ self.interners,
+ &self.quality_settings,
+ &mut self.prim_instances,
+ &self.clip_tree_builder,
+ );
+ }
+ }
+
+ // Add the prim that renders the result of the backdrop filter chain
+ let mut backdrop_render_instance = self.create_primitive(
+ info,
+ spatial_node_index,
+ clip_leaf_id,
+ BackdropRender {
+ },
+ );
+
+ // Set up the picture index for the backdrop-filter output in the prim
+ // that will draw it
+ match backdrop_render_instance.kind {
+ PrimitiveInstanceKind::BackdropRender { ref mut pic_index, .. } => {
+ assert_eq!(*pic_index, PictureIndex::INVALID);
+ *pic_index = output_pic_index;
+ }
+ _ => panic!("bug: unexpected prim kind"),
+ }
+
+ self.add_primitive_to_draw_list(
+ backdrop_render_instance,
+ info.rect,
+ spatial_node_index,
+ info.flags,
+ );
+ }
+ }
+
+ #[must_use]
+ fn wrap_prim_with_filters(
+ &mut self,
+ mut source: PictureChainBuilder,
+ clip_node_id: ClipNodeId,
+ mut filter_ops: Vec<Filter>,
+ mut filter_primitives: Vec<FilterPrimitive>,
+ filter_datas: Vec<FilterData>,
+ should_inflate_override: Option<bool>,
+ ) -> PictureChainBuilder {
+ // TODO(cbrewster): Currently CSS and SVG filters live side by side in WebRender, but unexpected results will
+ // happen if they are used simulataneously. Gecko only provides either filter ops or filter primitives.
+ // At some point, these two should be combined and CSS filters should be expressed in terms of SVG filters.
+ assert!(filter_ops.is_empty() || filter_primitives.is_empty(),
+ "Filter ops and filter primitives are not allowed on the same stacking context.");
+
+ // For each filter, create a new image with that composite mode.
+ let mut current_filter_data_index = 0;
+ for filter in &mut filter_ops {
+ let composite_mode = match filter {
+ Filter::ComponentTransfer => {
+ let filter_data =
+ &filter_datas[current_filter_data_index];
+ let filter_data = filter_data.sanitize();
+ current_filter_data_index = current_filter_data_index + 1;
+ if filter_data.is_identity() {
+ continue
+ } else {
+ let filter_data_key = SFilterDataKey {
+ data:
+ SFilterData {
+ r_func: SFilterDataComponent::from_functype_values(
+ filter_data.func_r_type, &filter_data.r_values),
+ g_func: SFilterDataComponent::from_functype_values(
+ filter_data.func_g_type, &filter_data.g_values),
+ b_func: SFilterDataComponent::from_functype_values(
+ filter_data.func_b_type, &filter_data.b_values),
+ a_func: SFilterDataComponent::from_functype_values(
+ filter_data.func_a_type, &filter_data.a_values),
+ },
+ };
+
+ let handle = self.interners
+ .filter_data
+ .intern(&filter_data_key, || ());
+ PictureCompositeMode::ComponentTransferFilter(handle)
+ }
+ }
+ _ => {
+ if filter.is_noop() {
+ continue;
+ } else {
+ let mut filter = filter.clone();
+
+ // backdrop-filter spec says that blurs should assume edgeMode=Duplicate
+ // We can do this by not inflating the bounds, which means the blur
+ // shader will duplicate pixels outside the sample rect
+ if let Some(should_inflate_override) = should_inflate_override {
+ if let Filter::Blur { ref mut should_inflate, .. } = filter {
+ *should_inflate = should_inflate_override;
+ }
+ }
+
+ PictureCompositeMode::Filter(filter)
+ }
+ }
+ };
+
+ source = source.add_picture(
+ composite_mode,
+ clip_node_id,
+ Picture3DContext::Out,
+ &mut self.interners,
+ &mut self.prim_store,
+ &mut self.prim_instances,
+ &mut self.clip_tree_builder,
+ );
+ }
+
+ if !filter_primitives.is_empty() {
+ let filter_datas = filter_datas.iter()
+ .map(|filter_data| filter_data.sanitize())
+ .map(|filter_data| {
+ SFilterData {
+ r_func: SFilterDataComponent::from_functype_values(
+ filter_data.func_r_type, &filter_data.r_values),
+ g_func: SFilterDataComponent::from_functype_values(
+ filter_data.func_g_type, &filter_data.g_values),
+ b_func: SFilterDataComponent::from_functype_values(
+ filter_data.func_b_type, &filter_data.b_values),
+ a_func: SFilterDataComponent::from_functype_values(
+ filter_data.func_a_type, &filter_data.a_values),
+ }
+ })
+ .collect();
+
+ // Sanitize filter inputs
+ for primitive in &mut filter_primitives {
+ primitive.sanitize();
+ }
+
+ let composite_mode = PictureCompositeMode::SvgFilter(
+ filter_primitives,
+ filter_datas,
+ );
+
+ source = source.add_picture(
+ composite_mode,
+ clip_node_id,
+ Picture3DContext::Out,
+ &mut self.interners,
+ &mut self.prim_store,
+ &mut self.prim_instances,
+ &mut self.clip_tree_builder,
+ );
+ }
+
+ source
+ }
+}
+
+
+pub trait CreateShadow {
+ fn create_shadow(
+ &self,
+ shadow: &Shadow,
+ blur_is_noop: bool,
+ current_raster_space: RasterSpace,
+ ) -> Self;
+}
+
+pub trait IsVisible {
+ fn is_visible(&self) -> bool;
+}
+
+/// A primitive instance + some extra information about the primitive. This is
+/// stored when constructing 3d rendering contexts, which involve cutting
+/// primitive lists.
+struct ExtendedPrimitiveInstance {
+ instance: PrimitiveInstance,
+ spatial_node_index: SpatialNodeIndex,
+ flags: PrimitiveFlags,
+}
+
+/// Internal tracking information about the currently pushed stacking context.
+/// Used to track what operations need to happen when a stacking context is popped.
+struct StackingContextInfo {
+ /// If true, pop and entry from the containing block stack.
+ pop_containing_block: bool,
+ /// If true, pop an entry from the flattened stacking context stack.
+ pop_stacking_context: bool,
+ /// If true, set a tile cache barrier when popping the stacking context.
+ set_tile_cache_barrier: bool,
+}
+
+/// Properties of a stacking context that are maintained
+/// during creation of the scene. These structures are
+/// not persisted after the initial scene build.
+struct FlattenedStackingContext {
+ /// The list of primitive instances added to this stacking context.
+ prim_list: PrimitiveList,
+
+ /// Primitive instance flags for compositing this stacking context
+ prim_flags: PrimitiveFlags,
+
+ /// The positioning node for this stacking context
+ spatial_node_index: SpatialNodeIndex,
+
+ /// The clip chain for this stacking context
+ clip_node_id: ClipNodeId,
+
+ /// The list of filters / mix-blend-mode for this
+ /// stacking context.
+ composite_ops: CompositeOps,
+
+ /// Bitfield of reasons this stacking context needs to
+ /// be an offscreen surface.
+ blit_reason: BlitReason,
+
+ /// CSS transform-style property.
+ transform_style: TransformStyle,
+
+ /// Defines the relationship to a preserve-3D hiearachy.
+ context_3d: Picture3DContext<ExtendedPrimitiveInstance>,
+
+ /// Flags identifying the type of container (among other things) this stacking context is
+ flags: StackingContextFlags,
+
+ /// Requested raster space for this stacking context
+ raster_space: RasterSpace,
+}
+
+impl FlattenedStackingContext {
+ /// Return true if the stacking context has a valid preserve-3d property
+ pub fn is_3d(&self) -> bool {
+ self.transform_style == TransformStyle::Preserve3D && self.composite_ops.is_empty()
+ }
+
+ /// Return true if the stacking context isn't needed.
+ pub fn is_redundant(
+ context_3d: &Picture3DContext<ExtendedPrimitiveInstance>,
+ composite_ops: &CompositeOps,
+ blit_reason: BlitReason,
+ parent: Option<&FlattenedStackingContext>,
+ prim_flags: PrimitiveFlags,
+ ) -> bool {
+ // Any 3d context is required
+ if let Picture3DContext::In { .. } = context_3d {
+ return false;
+ }
+
+ // If any filters are present that affect the output
+ if composite_ops.has_valid_filters() {
+ return false;
+ }
+
+ // If a mix-blend is active, we'll need to apply it in most cases
+ if composite_ops.mix_blend_mode.is_some() {
+ match parent {
+ Some(ref parent) => {
+ // However, if the parent stacking context is empty, then the mix-blend
+ // is a no-op, and we can skip it
+ if !parent.prim_list.is_empty() {
+ return false;
+ }
+ }
+ None => {
+ // TODO(gw): For now, we apply mix-blend ops that may be no-ops on a root
+ // level picture cache slice. We could apply a similar optimization
+ // to above with a few extra checks here, but it's probably quite rare.
+ return false;
+ }
+ }
+ }
+
+ // If need to isolate in surface due to clipping / mix-blend-mode
+ if !blit_reason.is_empty() {
+ return false;
+ }
+
+ // If backface visibility is explicitly set.
+ if !prim_flags.contains(PrimitiveFlags::IS_BACKFACE_VISIBLE) {
+ return false;
+ }
+
+ // It is redundant!
+ true
+ }
+
+ /// Cut the sequence of the immediate children recorded so far and generate a picture from them.
+ pub fn cut_item_sequence(
+ &mut self,
+ prim_store: &mut PrimitiveStore,
+ interners: &mut Interners,
+ composite_mode: Option<PictureCompositeMode>,
+ flat_items_context_3d: Picture3DContext<OrderedPictureChild>,
+ clip_tree_builder: &mut ClipTreeBuilder,
+ ) -> Option<(PictureIndex, PrimitiveInstance)> {
+ if self.prim_list.is_empty() {
+ return None
+ }
+
+ let pic_index = PictureIndex(prim_store.pictures
+ .alloc()
+ .init(PicturePrimitive::new_image(
+ composite_mode.clone(),
+ flat_items_context_3d,
+ self.prim_flags,
+ mem::replace(&mut self.prim_list, PrimitiveList::empty()),
+ self.spatial_node_index,
+ self.raster_space,
+ PictureFlags::empty(),
+ ))
+ );
+
+ let prim_instance = create_prim_instance(
+ pic_index,
+ composite_mode.into(),
+ self.raster_space,
+ self.clip_node_id,
+ interners,
+ clip_tree_builder,
+ );
+
+ Some((pic_index, prim_instance))
+ }
+}
+
+/// A primitive that is added while a shadow context is
+/// active is stored as a pending primitive and only
+/// added to pictures during pop_all_shadows.
+pub struct PendingPrimitive<T> {
+ spatial_node_index: SpatialNodeIndex,
+ clip_node_id: ClipNodeId,
+ info: LayoutPrimitiveInfo,
+ prim: T,
+}
+
+/// As shadows are pushed, they are stored as pending
+/// shadows, and handled at once during pop_all_shadows.
+pub struct PendingShadow {
+ shadow: Shadow,
+ should_inflate: bool,
+ spatial_node_index: SpatialNodeIndex,
+}
+
+pub enum ShadowItem {
+ Shadow(PendingShadow),
+ Image(PendingPrimitive<Image>),
+ LineDecoration(PendingPrimitive<LineDecoration>),
+ NormalBorder(PendingPrimitive<NormalBorderPrim>),
+ Primitive(PendingPrimitive<PrimitiveKeyKind>),
+ TextRun(PendingPrimitive<TextRun>),
+}
+
+impl From<PendingPrimitive<Image>> for ShadowItem {
+ fn from(image: PendingPrimitive<Image>) -> Self {
+ ShadowItem::Image(image)
+ }
+}
+
+impl From<PendingPrimitive<LineDecoration>> for ShadowItem {
+ fn from(line_dec: PendingPrimitive<LineDecoration>) -> Self {
+ ShadowItem::LineDecoration(line_dec)
+ }
+}
+
+impl From<PendingPrimitive<NormalBorderPrim>> for ShadowItem {
+ fn from(border: PendingPrimitive<NormalBorderPrim>) -> Self {
+ ShadowItem::NormalBorder(border)
+ }
+}
+
+impl From<PendingPrimitive<PrimitiveKeyKind>> for ShadowItem {
+ fn from(container: PendingPrimitive<PrimitiveKeyKind>) -> Self {
+ ShadowItem::Primitive(container)
+ }
+}
+
+impl From<PendingPrimitive<TextRun>> for ShadowItem {
+ fn from(text_run: PendingPrimitive<TextRun>) -> Self {
+ ShadowItem::TextRun(text_run)
+ }
+}
+
+fn create_prim_instance(
+ pic_index: PictureIndex,
+ composite_mode_key: PictureCompositeKey,
+ raster_space: RasterSpace,
+ clip_node_id: ClipNodeId,
+ interners: &mut Interners,
+ clip_tree_builder: &mut ClipTreeBuilder,
+) -> PrimitiveInstance {
+ let pic_key = PictureKey::new(
+ Picture {
+ composite_mode_key,
+ raster_space,
+ },
+ );
+
+ let data_handle = interners
+ .picture
+ .intern(&pic_key, || ());
+
+ PrimitiveInstance::new(
+ PrimitiveInstanceKind::Picture {
+ data_handle,
+ pic_index,
+ segment_instance_index: SegmentInstanceIndex::INVALID,
+ },
+ clip_tree_builder.build_for_picture(
+ clip_node_id,
+ ),
+ )
+}
+
+fn filter_ops_for_compositing(
+ input_filters: ItemRange<FilterOp>,
+) -> Vec<Filter> {
+ // TODO(gw): Now that we resolve these later on,
+ // we could probably make it a bit
+ // more efficient than cloning these here.
+ input_filters.iter().map(|filter| filter.into()).collect()
+}
+
+fn filter_datas_for_compositing(
+ input_filter_datas: &[TempFilterData],
+) -> Vec<FilterData> {
+ // TODO(gw): Now that we resolve these later on,
+ // we could probably make it a bit
+ // more efficient than cloning these here.
+ let mut filter_datas = vec![];
+ for temp_filter_data in input_filter_datas {
+ let func_types : Vec<ComponentTransferFuncType> = temp_filter_data.func_types.iter().collect();
+ debug_assert!(func_types.len() == 4);
+ filter_datas.push( FilterData {
+ func_r_type: func_types[0],
+ r_values: temp_filter_data.r_values.iter().collect(),
+ func_g_type: func_types[1],
+ g_values: temp_filter_data.g_values.iter().collect(),
+ func_b_type: func_types[2],
+ b_values: temp_filter_data.b_values.iter().collect(),
+ func_a_type: func_types[3],
+ a_values: temp_filter_data.a_values.iter().collect(),
+ });
+ }
+ filter_datas
+}
+
+fn filter_primitives_for_compositing(
+ input_filter_primitives: ItemRange<FilterPrimitive>,
+) -> Vec<FilterPrimitive> {
+ // Resolve these in the flattener?
+ // TODO(gw): Now that we resolve these later on,
+ // we could probably make it a bit
+ // more efficient than cloning these here.
+ input_filter_primitives.iter().map(|primitive| primitive).collect()
+}
+
+fn process_repeat_size(
+ snapped_rect: &LayoutRect,
+ unsnapped_rect: &LayoutRect,
+ repeat_size: LayoutSize,
+) -> LayoutSize {
+ // FIXME(aosmond): The tile size is calculated based on several parameters
+ // during display list building. It may produce a slightly different result
+ // than the bounds due to floating point error accumulation, even though in
+ // theory they should be the same. We do a fuzzy check here to paper over
+ // that. It may make more sense to push the original parameters into scene
+ // building and let it do a saner calculation with more information (e.g.
+ // the snapped values).
+ const EPSILON: f32 = 0.001;
+ LayoutSize::new(
+ if repeat_size.width.approx_eq_eps(&unsnapped_rect.width(), &EPSILON) {
+ snapped_rect.width()
+ } else {
+ repeat_size.width
+ },
+ if repeat_size.height.approx_eq_eps(&unsnapped_rect.height(), &EPSILON) {
+ snapped_rect.height()
+ } else {
+ repeat_size.height
+ },
+ )
+}
+
+fn read_gradient_stops(stops: ItemRange<GradientStop>) -> Vec<GradientStopKey> {
+ stops.iter().map(|stop| {
+ GradientStopKey {
+ offset: stop.offset,
+ color: stop.color.into(),
+ }
+ }).collect()
+}
diff --git a/gfx/wr/webrender/src/screen_capture.rs b/gfx/wr/webrender/src/screen_capture.rs
new file mode 100644
index 0000000000..3cc500f3fa
--- /dev/null
+++ b/gfx/wr/webrender/src/screen_capture.rs
@@ -0,0 +1,495 @@
+/* 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/. */
+
+//! Screen capture infrastructure for the Gecko Profiler and Composition Recorder.
+
+use std::collections::HashMap;
+
+use api::{ImageFormat, ImageBufferKind};
+use api::units::*;
+use gleam::gl::GlType;
+
+use crate::device::{Device, PBO, DrawTarget, ReadTarget, Texture, TextureFilter};
+use crate::internal_types::RenderTargetInfo;
+use crate::renderer::Renderer;
+use crate::util::round_up_to_multiple;
+
+/// A handle to a screenshot that is being asynchronously captured and scaled.
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+pub struct AsyncScreenshotHandle(usize);
+
+/// A handle to a recorded frame that was captured.
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct RecordedFrameHandle(usize);
+
+/// An asynchronously captured screenshot bound to a PBO which has not yet been mapped for copying.
+struct AsyncScreenshot {
+ /// The PBO that will contain the screenshot data.
+ pbo: PBO,
+ /// The size of the screenshot.
+ screenshot_size: DeviceIntSize,
+ /// The stride of the data in the PBO.
+ buffer_stride: usize,
+ /// Thge image format of the screenshot.
+ image_format: ImageFormat,
+}
+
+/// How the `AsyncScreenshotGrabber` captures frames.
+#[derive(Debug, Eq, PartialEq)]
+enum AsyncScreenshotGrabberMode {
+ /// Capture screenshots for the Gecko profiler.
+ ///
+ /// This mode will asynchronously scale the screenshots captured.
+ ProfilerScreenshots,
+
+ /// Capture screenshots for the CompositionRecorder.
+ ///
+ /// This mode does not scale the captured screenshots.
+ CompositionRecorder,
+}
+
+/// Renderer infrastructure for capturing screenshots and scaling them asynchronously.
+pub(in crate) struct AsyncScreenshotGrabber {
+ /// The textures used to scale screenshots.
+ scaling_textures: Vec<Texture>,
+ /// PBOs available to be used for screenshot readback.
+ available_pbos: Vec<PBO>,
+ /// PBOs containing screenshots that are awaiting readback.
+ awaiting_readback: HashMap<AsyncScreenshotHandle, AsyncScreenshot>,
+ /// The handle for the net PBO that will be inserted into `in_use_pbos`.
+ next_pbo_handle: usize,
+ /// The mode the grabber operates in.
+ mode: AsyncScreenshotGrabberMode,
+}
+
+impl Default for AsyncScreenshotGrabber {
+ fn default() -> Self {
+ AsyncScreenshotGrabber {
+ scaling_textures: Vec::new(),
+ available_pbos: Vec::new(),
+ awaiting_readback: HashMap::new(),
+ next_pbo_handle: 1,
+ mode: AsyncScreenshotGrabberMode::ProfilerScreenshots,
+ }
+ }
+}
+
+impl AsyncScreenshotGrabber {
+ /// Create a new AsyncScreenshotGrabber for the composition recorder.
+ pub fn new_composition_recorder() -> Self {
+ let mut recorder = Self::default();
+ recorder.mode = AsyncScreenshotGrabberMode::CompositionRecorder;
+
+ recorder
+ }
+
+ /// Deinitialize the allocated textures and PBOs.
+ pub fn deinit(self, device: &mut Device) {
+ for texture in self.scaling_textures {
+ device.delete_texture(texture);
+ }
+
+ for pbo in self.available_pbos {
+ device.delete_pbo(pbo);
+ }
+
+ for (_, async_screenshot) in self.awaiting_readback {
+ device.delete_pbo(async_screenshot.pbo);
+ }
+ }
+
+ /// Take a screenshot and scale it asynchronously.
+ ///
+ /// The returned handle can be used to access the mapped screenshot data via
+ /// `map_and_recycle_screenshot`.
+ /// The returned size is the size of the screenshot.
+ pub fn get_screenshot(
+ &mut self,
+ device: &mut Device,
+ window_rect: DeviceIntRect,
+ buffer_size: DeviceIntSize,
+ image_format: ImageFormat,
+ ) -> (AsyncScreenshotHandle, DeviceIntSize) {
+ let screenshot_size = match self.mode {
+ AsyncScreenshotGrabberMode::ProfilerScreenshots => {
+ assert_ne!(window_rect.width(), 0);
+ assert_ne!(window_rect.height(), 0);
+
+ let scale = (buffer_size.width as f32 / window_rect.width() as f32)
+ .min(buffer_size.height as f32 / window_rect.height() as f32);
+
+ (window_rect.size().to_f32() * scale).round().to_i32()
+ }
+
+ AsyncScreenshotGrabberMode::CompositionRecorder => {
+ assert_eq!(buffer_size, window_rect.size());
+ buffer_size
+ }
+ };
+
+ assert!(screenshot_size.width <= buffer_size.width);
+ assert!(screenshot_size.height <= buffer_size.height);
+
+ // To ensure that we hit the fast path when reading from a
+ // framebuffer we must ensure that the width of the area we read
+ // is a multiple of the device's optimal pixel-transfer stride.
+ // The read_size should therefore be the screenshot_size with the width
+ // increased to a suitable value. We will also pass this value to
+ // scale_screenshot() as the min_texture_size, to ensure the texture is
+ // large enough to read from. In CompositionRecorder mode we read
+ // directly from the default framebuffer so are unable choose this size.
+ let read_size = match self.mode {
+ AsyncScreenshotGrabberMode::ProfilerScreenshots => {
+ let stride = (screenshot_size.width * image_format.bytes_per_pixel()) as usize;
+ let rounded = round_up_to_multiple(stride, device.required_pbo_stride().num_bytes(image_format));
+ let optimal_width = rounded as i32 / image_format.bytes_per_pixel();
+
+ DeviceIntSize::new(
+ optimal_width,
+ screenshot_size.height,
+ )
+ }
+ AsyncScreenshotGrabberMode::CompositionRecorder => buffer_size,
+ };
+ let required_size = read_size.area() as usize * image_format.bytes_per_pixel() as usize;
+
+ // Find an available PBO with the required size, creating a new one if necessary.
+ let pbo = {
+ let mut reusable_pbo = None;
+ while let Some(pbo) = self.available_pbos.pop() {
+ if pbo.get_reserved_size() != required_size {
+ device.delete_pbo(pbo);
+ } else {
+ reusable_pbo = Some(pbo);
+ break;
+ }
+ };
+
+ reusable_pbo.unwrap_or_else(|| device.create_pbo_with_size(required_size))
+ };
+ assert_eq!(pbo.get_reserved_size(), required_size);
+
+ let read_target = match self.mode {
+ AsyncScreenshotGrabberMode::ProfilerScreenshots => {
+ self.scale_screenshot(
+ device,
+ ReadTarget::Default,
+ window_rect,
+ buffer_size,
+ read_size,
+ screenshot_size,
+ image_format,
+ 0,
+ );
+
+ ReadTarget::from_texture(&self.scaling_textures[0])
+ }
+
+ AsyncScreenshotGrabberMode::CompositionRecorder => ReadTarget::Default,
+ };
+
+ device.read_pixels_into_pbo(
+ read_target,
+ DeviceIntRect::from_size(read_size),
+ image_format,
+ &pbo,
+ );
+
+ let handle = AsyncScreenshotHandle(self.next_pbo_handle);
+ self.next_pbo_handle += 1;
+
+ self.awaiting_readback.insert(
+ handle,
+ AsyncScreenshot {
+ pbo,
+ screenshot_size,
+ buffer_stride: (read_size.width * image_format.bytes_per_pixel()) as usize,
+ image_format,
+ },
+ );
+
+ (handle, screenshot_size)
+ }
+
+ /// Take the screenshot in the given `ReadTarget` and scale it to `dest_size` recursively.
+ ///
+ /// Each scaling operation scales only by a factor of two to preserve quality.
+ ///
+ /// Textures are scaled such that `scaling_textures[n]` is half the size of
+ /// `scaling_textures[n+1]`.
+ ///
+ /// After the scaling completes, the final screenshot will be in
+ /// `scaling_textures[0]`.
+ ///
+ /// The size of `scaling_textures[0]` will be increased to `min_texture_size`
+ /// so that an optimally-sized area can be read from it.
+ fn scale_screenshot(
+ &mut self,
+ device: &mut Device,
+ read_target: ReadTarget,
+ read_target_rect: DeviceIntRect,
+ buffer_size: DeviceIntSize,
+ min_texture_size: DeviceIntSize,
+ dest_size: DeviceIntSize,
+ image_format: ImageFormat,
+ level: usize,
+ ) {
+ assert_eq!(self.mode, AsyncScreenshotGrabberMode::ProfilerScreenshots);
+
+ let texture_size = {
+ let size = buffer_size * (1 << level);
+ DeviceIntSize::new(
+ size.width.max(min_texture_size.width),
+ size.height.max(min_texture_size.height),
+ )
+ };
+
+ // If we haven't created a texture for this level, or the existing
+ // texture is the wrong size, then create a new one.
+ if level == self.scaling_textures.len() || self.scaling_textures[level].get_dimensions() != texture_size {
+ let texture = device.create_texture(
+ ImageBufferKind::Texture2D,
+ image_format,
+ texture_size.width,
+ texture_size.height,
+ TextureFilter::Linear,
+ Some(RenderTargetInfo { has_depth: false }),
+ );
+ if level == self.scaling_textures.len() {
+ self.scaling_textures.push(texture);
+ } else {
+ let old_texture = std::mem::replace(&mut self.scaling_textures[level], texture);
+ device.delete_texture(old_texture);
+ }
+ }
+ assert_eq!(self.scaling_textures[level].get_dimensions(), texture_size);
+
+ let (read_target, read_target_rect) = if read_target_rect.width() > 2 * dest_size.width {
+ self.scale_screenshot(
+ device,
+ read_target,
+ read_target_rect,
+ buffer_size,
+ min_texture_size,
+ dest_size * 2,
+ image_format,
+ level + 1,
+ );
+
+ (
+ ReadTarget::from_texture(&self.scaling_textures[level + 1]),
+ DeviceIntRect::from_size(dest_size * 2),
+ )
+ } else {
+ (read_target, read_target_rect)
+ };
+
+ let draw_target = DrawTarget::from_texture(&self.scaling_textures[level], false);
+
+ let draw_target_rect = draw_target
+ .to_framebuffer_rect(DeviceIntRect::from_size(dest_size));
+
+ let read_target_rect = device_rect_as_framebuffer_rect(&read_target_rect);
+
+ if level == 0 && !device.surface_origin_is_top_left() {
+ device.blit_render_target_invert_y(
+ read_target,
+ read_target_rect,
+ draw_target,
+ draw_target_rect,
+ );
+ } else {
+ device.blit_render_target(
+ read_target,
+ read_target_rect,
+ draw_target,
+ draw_target_rect,
+ TextureFilter::Linear,
+ );
+ }
+ }
+
+ /// Map the contents of the screenshot given by the handle and copy it into
+ /// the given buffer.
+ pub fn map_and_recycle_screenshot(
+ &mut self,
+ device: &mut Device,
+ handle: AsyncScreenshotHandle,
+ dst_buffer: &mut [u8],
+ dst_stride: usize,
+ ) -> bool {
+ let AsyncScreenshot {
+ pbo,
+ screenshot_size,
+ buffer_stride,
+ image_format,
+ } = match self.awaiting_readback.remove(&handle) {
+ Some(screenshot) => screenshot,
+ None => return false,
+ };
+
+ let gl_type = device.gl().get_type();
+
+ let success = if let Some(bound_pbo) = device.map_pbo_for_readback(&pbo) {
+ let src_buffer = &bound_pbo.data;
+ let src_stride = buffer_stride;
+ let src_width =
+ screenshot_size.width as usize * image_format.bytes_per_pixel() as usize;
+
+ for (src_slice, dst_slice) in self
+ .iter_src_buffer_chunked(gl_type, src_buffer, src_stride)
+ .zip(dst_buffer.chunks_mut(dst_stride))
+ .take(screenshot_size.height as usize)
+ {
+ dst_slice[.. src_width].copy_from_slice(&src_slice[.. src_width]);
+ }
+
+ true
+ } else {
+ false
+ };
+
+ match self.mode {
+ AsyncScreenshotGrabberMode::ProfilerScreenshots => self.available_pbos.push(pbo),
+ AsyncScreenshotGrabberMode::CompositionRecorder => device.delete_pbo(pbo),
+ }
+
+ success
+ }
+
+ fn iter_src_buffer_chunked<'a>(
+ &self,
+ gl_type: GlType,
+ src_buffer: &'a [u8],
+ src_stride: usize,
+ ) -> Box<dyn Iterator<Item = &'a [u8]> + 'a> {
+ use AsyncScreenshotGrabberMode::*;
+
+ let is_angle = cfg!(windows) && gl_type == GlType::Gles;
+
+ if self.mode == CompositionRecorder && !is_angle {
+ // This is a non-ANGLE configuration. in this case, the recorded frames were captured
+ // upside down, so we have to flip them right side up.
+ Box::new(src_buffer.chunks(src_stride).rev())
+ } else {
+ // This is either an ANGLE configuration in the `CompositionRecorder` mode or a
+ // non-ANGLE configuration in the `ProfilerScreenshots` mode. In either case, the
+ // captured frames are right-side up.
+ Box::new(src_buffer.chunks(src_stride))
+ }
+ }
+}
+
+// Screen-capture specific Renderer impls.
+impl Renderer {
+ /// Record a frame for the Composition Recorder.
+ ///
+ /// The returned handle can be passed to `map_recorded_frame` to copy it into
+ /// a buffer.
+ /// The returned size is the size of the frame.
+ pub fn record_frame(
+ &mut self,
+ image_format: ImageFormat,
+ ) -> Option<(RecordedFrameHandle, DeviceIntSize)> {
+ let device_size = self.device_size()?;
+ self.device.begin_frame();
+
+ let (handle, _) = self
+ .async_frame_recorder
+ .get_or_insert_with(AsyncScreenshotGrabber::new_composition_recorder)
+ .get_screenshot(
+ &mut self.device,
+ DeviceIntRect::from_size(device_size),
+ device_size,
+ image_format,
+ );
+
+ self.device.end_frame();
+
+ Some((RecordedFrameHandle(handle.0), device_size))
+ }
+
+ /// Map a frame captured for the composition recorder into the given buffer.
+ pub fn map_recorded_frame(
+ &mut self,
+ handle: RecordedFrameHandle,
+ dst_buffer: &mut [u8],
+ dst_stride: usize,
+ ) -> bool {
+ if let Some(async_frame_recorder) = self.async_frame_recorder.as_mut() {
+ async_frame_recorder.map_and_recycle_screenshot(
+ &mut self.device,
+ AsyncScreenshotHandle(handle.0),
+ dst_buffer,
+ dst_stride,
+ )
+ } else {
+ false
+ }
+ }
+
+ /// Free the data structures used by the composition recorder.
+ pub fn release_composition_recorder_structures(&mut self) {
+ if let Some(async_frame_recorder) = self.async_frame_recorder.take() {
+ self.device.begin_frame();
+ async_frame_recorder.deinit(&mut self.device);
+ self.device.end_frame();
+ }
+ }
+
+ /// Take a screenshot and scale it asynchronously.
+ ///
+ /// The returned handle can be used to access the mapped screenshot data via
+ /// `map_and_recycle_screenshot`.
+ ///
+ /// The returned size is the size of the screenshot.
+ pub fn get_screenshot_async(
+ &mut self,
+ window_rect: DeviceIntRect,
+ buffer_size: DeviceIntSize,
+ image_format: ImageFormat,
+ ) -> (AsyncScreenshotHandle, DeviceIntSize) {
+ self.device.begin_frame();
+
+ let handle = self
+ .async_screenshots
+ .get_or_insert_with(AsyncScreenshotGrabber::default)
+ .get_screenshot(&mut self.device, window_rect, buffer_size, image_format);
+
+ self.device.end_frame();
+
+ handle
+ }
+
+ /// Map the contents of the screenshot given by the handle and copy it into
+ /// the given buffer.
+ pub fn map_and_recycle_screenshot(
+ &mut self,
+ handle: AsyncScreenshotHandle,
+ dst_buffer: &mut [u8],
+ dst_stride: usize,
+ ) -> bool {
+ if let Some(async_screenshots) = self.async_screenshots.as_mut() {
+ async_screenshots.map_and_recycle_screenshot(
+ &mut self.device,
+ handle,
+ dst_buffer,
+ dst_stride,
+ )
+ } else {
+ false
+ }
+ }
+
+ /// Release the screenshot grabbing structures that the profiler was using.
+ pub fn release_profiler_structures(&mut self) {
+ if let Some(async_screenshots) = self.async_screenshots.take() {
+ self.device.begin_frame();
+ async_screenshots.deinit(&mut self.device);
+ self.device.end_frame();
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/segment.rs b/gfx/wr/webrender/src/segment.rs
new file mode 100644
index 0000000000..55e76f33f2
--- /dev/null
+++ b/gfx/wr/webrender/src/segment.rs
@@ -0,0 +1,1374 @@
+/* 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/. */
+
+//! Primitive segmentation
+//!
+//! # Overview
+//!
+//! Segmenting is the process of breaking rectangular primitives into smaller rectangular
+//! primitives in order to extract parts that could benefit from a fast paths.
+//!
+//! Typically this is used to allow fully opaque segments to be rendered in the opaque
+//! pass. For example when an opaque rectangle has a non-axis-aligned transform applied,
+//! we usually have to apply some anti-aliasing around the edges which requires alpha
+//! blending. By segmenting the edges out of the center of the primitive, we can keep a
+//! large amount of pixels in the opaque pass.
+//! Segmenting also lets us avoids rasterizing parts of clip masks that we know to have
+//! no effect or to be fully masking. For example by segmenting the corners of a rounded
+//! rectangle clip, we can optimize both rendering the mask and the primitive by only
+//! rasterize the corners in the mask and not applying any clipping to the segments of
+//! the primitive that don't overlap the borders.
+//!
+//! It is a flexible system in the sense that different sources of segmentation (for
+//! example two rounded rectangle clips) can affect the segmentation, and the possibility
+//! to segment some effects such as specific clip kinds does not necessarily mean the
+//! primitive will actually be segmented.
+//!
+//! ## Segments and clipping
+//!
+//! Segments of a primitive can be either not clipped, fully clipped, or partially clipped.
+//! In the first two case we don't need a clip mask. For each partially masked segments, a
+//! mask is rasterized using a render task. All of the interesting steps happen during frame
+//! building.
+//!
+//! - The first step is to determine the segmentation and write the associated GPU data.
+//! See `PrimitiveInstance::build_segments_if_needed` and `write_brush_segment_description`
+//! in `prim_store/mod.rs` which uses the segment builder of this module.
+//! - The second step is to generate the mask render tasks.
+//! See `BrushSegment::update_clip_task` and `RenderTask::new_mask`. For each segment that
+//! needs a mask, the contribution of all clips that affect the segment is added to the
+//! mask's render task.
+//! - Segments are assigned to batches (See `batch.rs`). Segments of a given primitive can
+//! be assigned to different batches.
+//!
+//! See also the [`clip` module documentation][clip.rs] for details about how clipping
+//! information is represented.
+//!
+//!
+//! [clip.rs]: ../clip/index.html
+//!
+
+use api::{BorderRadius, ClipMode};
+use api::units::*;
+use std::{cmp, usize};
+use crate::util::{extract_inner_rect_safe};
+use smallvec::SmallVec;
+
+// We don't want to generate too many segments in edge cases, as it will result in a lot of
+// clip mask overhead, and possibly exceeding the maximum row size of the GPU cache.
+const MAX_SEGMENTS: usize = 64;
+
+bitflags! {
+ // Note: This can use up to 4 bits due to how it will be packed in
+ // the instance data.
+
+ /// Each bit of the edge AA mask is:
+ /// 0, when the edge of the primitive needs to be considered for AA
+ /// 1, when the edge of the segment needs to be considered for AA
+ ///
+ /// *Note*: the bit values have to match the shader logic in
+ /// `write_transform_vertex()` function.
+ #[cfg_attr(feature = "capture", derive(Serialize))]
+ #[cfg_attr(feature = "replay", derive(Deserialize))]
+ #[derive(MallocSizeOf)]
+ pub struct EdgeAaSegmentMask: u8 {
+ ///
+ const LEFT = 0x1;
+ ///
+ const TOP = 0x2;
+ ///
+ const RIGHT = 0x4;
+ ///
+ const BOTTOM = 0x8;
+ }
+}
+
+bitflags! {
+ pub struct ItemFlags: u8 {
+ const X_ACTIVE = 0x1;
+ const Y_ACTIVE = 0x2;
+ const HAS_MASK = 0x4;
+ }
+}
+
+// The segment builder outputs a list of these segments.
+#[derive(Debug, PartialEq)]
+pub struct Segment {
+ pub rect: LayoutRect,
+ pub has_mask: bool,
+ pub edge_flags: EdgeAaSegmentMask,
+ pub region_x: usize,
+ pub region_y: usize,
+}
+
+// The segment builder creates a list of x/y axis events
+// that are used to build a segment list. Right now, we
+// don't bother providing a list of *which* clip regions
+// are active for a given segment. Instead, if there is
+// any clip mask present in a segment, we will just end
+// up drawing each of the masks to that segment clip.
+// This is a fairly rare case, but we can detect this
+// in the future and only apply clip masks that are
+// relevant to each segment region.
+// TODO(gw): Provide clip region info with each segment.
+#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd)]
+enum EventKind {
+ // Beginning of a clip (rounded) rect.
+ BeginClip,
+ // End of a clip (rounded) rect.
+ EndClip,
+ // Begin the next region in the primitive.
+ BeginRegion,
+}
+
+// Events must be ordered such that when the coordinates
+// of two events are the same, the end events are processed
+// before the begin events. This ensures that we're able
+// to detect which regions are active for a given segment.
+impl Ord for EventKind {
+ fn cmp(&self, other: &EventKind) -> cmp::Ordering {
+ match (*self, *other) {
+ (EventKind::BeginRegion, EventKind::BeginRegion) => {
+ panic!("bug: regions must be non-overlapping")
+ }
+ (EventKind::EndClip, EventKind::BeginRegion) |
+ (EventKind::BeginRegion, EventKind::BeginClip) => {
+ cmp::Ordering::Less
+ }
+ (EventKind::BeginClip, EventKind::BeginRegion) |
+ (EventKind::BeginRegion, EventKind::EndClip) => {
+ cmp::Ordering::Greater
+ }
+ (EventKind::BeginClip, EventKind::BeginClip) |
+ (EventKind::EndClip, EventKind::EndClip) => {
+ cmp::Ordering::Equal
+ }
+ (EventKind::BeginClip, EventKind::EndClip) => {
+ cmp::Ordering::Greater
+ }
+ (EventKind::EndClip, EventKind::BeginClip) => {
+ cmp::Ordering::Less
+ }
+ }
+ }
+}
+
+// A x/y event where we will create a vertex in the
+// segment builder.
+#[derive(Debug, Eq, PartialEq, PartialOrd)]
+struct Event {
+ value: Au,
+ item_index: ItemIndex,
+ kind: EventKind,
+}
+
+impl Ord for Event {
+ fn cmp(&self, other: &Event) -> cmp::Ordering {
+ self.value
+ .cmp(&other.value)
+ .then(self.kind.cmp(&other.kind))
+ }
+}
+
+impl Event {
+ fn begin(value: f32, index: usize) -> Event {
+ Event {
+ value: Au::from_f32_px(value),
+ item_index: ItemIndex(index),
+ kind: EventKind::BeginClip,
+ }
+ }
+
+ fn end(value: f32, index: usize) -> Event {
+ Event {
+ value: Au::from_f32_px(value),
+ item_index: ItemIndex(index),
+ kind: EventKind::EndClip,
+ }
+ }
+
+ fn region(value: f32) -> Event {
+ Event {
+ value: Au::from_f32_px(value),
+ kind: EventKind::BeginRegion,
+ item_index: ItemIndex(usize::MAX),
+ }
+ }
+
+ fn update(
+ &self,
+ flag: ItemFlags,
+ items: &mut [Item],
+ region: &mut usize,
+ ) {
+ let is_active = match self.kind {
+ EventKind::BeginClip => true,
+ EventKind::EndClip => false,
+ EventKind::BeginRegion => {
+ *region += 1;
+ return;
+ }
+ };
+
+ items[self.item_index.0].flags.set(flag, is_active);
+ }
+}
+
+// An item that provides some kind of clip region (either
+// a clip in/out rect, or a mask region).
+#[derive(Debug)]
+struct Item {
+ rect: LayoutRect,
+ mode: Option<ClipMode>,
+ flags: ItemFlags,
+}
+
+impl Item {
+ fn new(
+ rect: LayoutRect,
+ mode: Option<ClipMode>,
+ has_mask: bool,
+ ) -> Item {
+ let flags = if has_mask {
+ ItemFlags::HAS_MASK
+ } else {
+ ItemFlags::empty()
+ };
+
+ Item {
+ rect,
+ mode,
+ flags,
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd)]
+struct ItemIndex(usize);
+
+// The main public interface to the segment module.
+pub struct SegmentBuilder {
+ items: Vec<Item>,
+ inner_rect: Option<LayoutRect>,
+ bounding_rect: Option<LayoutRect>,
+ has_interesting_clips: bool,
+
+ #[cfg(debug_assertions)]
+ initialized: bool,
+}
+
+impl SegmentBuilder {
+ // Create a new segment builder, supplying the primitive
+ // local rect and associated local clip rect.
+ pub fn new() -> SegmentBuilder {
+ SegmentBuilder {
+ items: Vec::with_capacity(4),
+ bounding_rect: None,
+ inner_rect: None,
+ has_interesting_clips: false,
+ #[cfg(debug_assertions)]
+ initialized: false,
+ }
+ }
+
+ pub fn initialize(
+ &mut self,
+ local_rect: LayoutRect,
+ inner_rect: Option<LayoutRect>,
+ local_clip_rect: LayoutRect,
+ ) {
+ self.items.clear();
+ self.inner_rect = inner_rect;
+ self.bounding_rect = Some(local_rect);
+
+ self.push_clip_rect(local_rect, None, ClipMode::Clip);
+ self.push_clip_rect(local_clip_rect, None, ClipMode::Clip);
+
+ // This must be set after the push_clip_rect calls above, since we
+ // want to skip segment building if those are the only clips.
+ self.has_interesting_clips = false;
+
+ #[cfg(debug_assertions)]
+ {
+ self.initialized = true;
+ }
+ }
+
+ // Push a region defined by an inner and outer rect where there
+ // is a mask required. This ensures that segments which intersect
+ // with these areas will get a clip mask task allocated. This
+ // is currently used to mark where a box-shadow region can affect
+ // the pixels of a clip-mask. It might be useful for other types
+ // such as dashed and dotted borders in the future.
+ pub fn push_mask_region(
+ &mut self,
+ outer_rect: LayoutRect,
+ inner_rect: LayoutRect,
+ inner_clip_mode: Option<ClipMode>,
+ ) {
+ self.has_interesting_clips = true;
+
+ if inner_rect.is_empty() {
+ self.items.push(Item::new(
+ outer_rect,
+ None,
+ true
+ ));
+ return;
+ }
+
+ debug_assert!(outer_rect.contains_box(&inner_rect));
+
+ let p0 = outer_rect.min;
+ let p1 = inner_rect.min;
+ let p2 = inner_rect.max;
+ let p3 = outer_rect.max;
+
+ let segments = &[
+ LayoutRect {
+ min: LayoutPoint::new(p0.x, p0.y),
+ max: LayoutPoint::new(p1.x, p1.y),
+ },
+ LayoutRect {
+ min: LayoutPoint::new(p2.x, p0.y),
+ max: LayoutPoint::new(p3.x, p1.y),
+ },
+ LayoutRect {
+ min: LayoutPoint::new(p2.x, p2.y),
+ max: LayoutPoint::new(p3.x, p3.y),
+ },
+ LayoutRect {
+ min: LayoutPoint::new(p0.x, p2.y),
+ max: LayoutPoint::new(p1.x, p3.y),
+ },
+ LayoutRect {
+ min: LayoutPoint::new(p1.x, p0.y),
+ max: LayoutPoint::new(p2.x, p1.y),
+ },
+ LayoutRect {
+ min: LayoutPoint::new(p2.x, p1.y),
+ max: LayoutPoint::new(p3.x, p2.y),
+ },
+ LayoutRect {
+ min: LayoutPoint::new(p1.x, p2.y),
+ max: LayoutPoint::new(p2.x, p3.y),
+ },
+ LayoutRect {
+ min: LayoutPoint::new(p0.x, p1.y),
+ max: LayoutPoint::new(p1.x, p2.y),
+ },
+ ];
+
+ self.items.reserve(segments.len() + 1);
+
+ for segment in segments {
+ self.items.push(Item::new(
+ *segment,
+ None,
+ true
+ ));
+ }
+
+ if inner_clip_mode.is_some() {
+ self.items.push(Item::new(
+ inner_rect,
+ inner_clip_mode,
+ false,
+ ));
+ }
+ }
+
+ // Push some kind of clipping region into the segment builder.
+ // If radius is None, it's a simple rect.
+ pub fn push_clip_rect(
+ &mut self,
+ rect: LayoutRect,
+ radius: Option<BorderRadius>,
+ mode: ClipMode,
+ ) {
+ self.has_interesting_clips = true;
+
+ // Keep track of a minimal bounding rect for the set of
+ // segments that will be generated.
+ if mode == ClipMode::Clip {
+ self.bounding_rect = self.bounding_rect.and_then(|bounding_rect| {
+ bounding_rect.intersection(&rect)
+ });
+ }
+ let mode = Some(mode);
+
+ match radius {
+ Some(radius) => {
+ // For a rounded rect, try to create a nine-patch where there
+ // is a clip item for each corner, inner and edge region.
+ match extract_inner_rect_safe(&rect, &radius) {
+ Some(inner) => {
+ let p0 = rect.min;
+ let p1 = inner.min;
+ let p2 = inner.max;
+ let p3 = rect.max;
+
+ self.items.reserve(9);
+
+ let corner_segments = &[
+ LayoutRect {
+ min: LayoutPoint::new(p0.x, p0.y),
+ max: LayoutPoint::new(p1.x, p1.y),
+ },
+ LayoutRect {
+ min: LayoutPoint::new(p2.x, p0.y),
+ max: LayoutPoint::new(p3.x, p1.y),
+ },
+ LayoutRect {
+ min: LayoutPoint::new(p2.x, p2.y),
+ max: LayoutPoint::new(p3.x, p3.y),
+ },
+ LayoutRect {
+ min: LayoutPoint::new(p0.x, p2.y),
+ max: LayoutPoint::new(p1.x, p3.y),
+ },
+ ];
+
+ for segment in corner_segments {
+ self.items.push(Item::new(
+ *segment,
+ mode,
+ true
+ ));
+ }
+
+ let other_segments = &[
+ LayoutRect {
+ min: LayoutPoint::new(p1.x, p0.y),
+ max: LayoutPoint::new(p2.x, p1.y),
+ },
+ LayoutRect {
+ min: LayoutPoint::new(p2.x, p1.y),
+ max: LayoutPoint::new(p3.x, p2.y),
+ },
+ LayoutRect {
+ min: LayoutPoint::new(p1.x, p2.y),
+ max: LayoutPoint::new(p2.x, p3.y),
+ },
+ LayoutRect {
+ min: LayoutPoint::new(p0.x, p1.y),
+ max: LayoutPoint::new(p1.x, p2.y),
+ },
+ LayoutRect {
+ min: LayoutPoint::new(p1.x, p1.y),
+ max: LayoutPoint::new(p2.x, p2.y),
+ },
+ ];
+
+ for segment in other_segments {
+ self.items.push(Item::new(
+ *segment,
+ mode,
+ false,
+ ));
+ }
+ }
+ None => {
+ // If we get here, we could not extract an inner rectangle
+ // for this clip region. This can occur in cases such as
+ // a rounded rect where the top-left and bottom-left radii
+ // result in overlapping rects. In that case, just create
+ // a single clip region for the entire rounded rect.
+ self.items.push(Item::new(
+ rect,
+ mode,
+ true,
+ ))
+ }
+ }
+ }
+ None => {
+ // For a simple rect, just create one clipping item.
+ self.items.push(Item::new(
+ rect,
+ mode,
+ false,
+ ))
+ }
+ }
+ }
+
+ // Consume this segment builder and produce a list of segments.
+ pub fn build<F>(&mut self, mut f: F) where F: FnMut(&Segment) {
+ #[cfg(debug_assertions)]
+ debug_assert!(self.initialized);
+
+ #[cfg(debug_assertions)]
+ {
+ self.initialized = false;
+ }
+
+ let bounding_rect = match self.bounding_rect {
+ Some(bounding_rect) => bounding_rect,
+ None => return,
+ };
+
+ if !self.has_interesting_clips {
+ // There were no additional clips added, so don't bother building segments.
+ // Just emit a single segment for the bounding rect of the primitive.
+ f(&Segment {
+ edge_flags: EdgeAaSegmentMask::all(),
+ region_x: 0,
+ region_y: 0,
+ has_mask: false,
+ rect: bounding_rect,
+ });
+ return
+ }
+
+ // First, filter out any items that don't intersect
+ // with the visible bounding rect.
+ self.items.retain(|item| item.rect.intersects(&bounding_rect));
+
+ // Create events for each item
+ let mut x_events : SmallVec<[Event; 4]> = SmallVec::new();
+ let mut y_events : SmallVec<[Event; 4]> = SmallVec::new();
+
+ for (item_index, item) in self.items.iter().enumerate() {
+ let p0 = item.rect.min;
+ let p1 = item.rect.max;
+
+ x_events.push(Event::begin(p0.x, item_index));
+ x_events.push(Event::end(p1.x, item_index));
+ y_events.push(Event::begin(p0.y, item_index));
+ y_events.push(Event::end(p1.y, item_index));
+ }
+
+ // Add the region events, if provided.
+ if let Some(inner_rect) = self.inner_rect {
+ x_events.push(Event::region(inner_rect.min.x));
+ x_events.push(Event::region(inner_rect.max.x));
+
+ y_events.push(Event::region(inner_rect.min.y));
+ y_events.push(Event::region(inner_rect.max.y));
+ }
+
+ // Get the minimal bounding rect in app units. We will
+ // work in fixed point in order to avoid float precision
+ // error while handling events.
+ let p0 = LayoutPointAu::new(
+ Au::from_f32_px(bounding_rect.min.x),
+ Au::from_f32_px(bounding_rect.min.y),
+ );
+
+ let p1 = LayoutPointAu::new(
+ Au::from_f32_px(bounding_rect.max.x),
+ Au::from_f32_px(bounding_rect.max.y),
+ );
+
+ // Sort the events in ascending order.
+ x_events.sort();
+ y_events.sort();
+
+ // Generate segments from the event lists, by sweeping the y-axis
+ // and then the x-axis for each event. This can generate a significant
+ // number of segments, but most importantly, it ensures that there are
+ // no t-junctions in the generated segments. It's probably possible
+ // to come up with more efficient segmentation algorithms, at least
+ // for simple / common cases.
+
+ // Each coordinate is clamped to the bounds of the minimal
+ // bounding rect. This ensures that we don't generate segments
+ // outside that bounding rect, but does allow correctly handling
+ // clips where the clip region starts outside the minimal
+ // rect but still intersects with it.
+
+ let mut prev_y = clamp(p0.y, y_events[0].value, p1.y);
+ let mut region_y = 0;
+ let mut segments : SmallVec<[_; 16]> = SmallVec::new();
+ let mut x_count = 0;
+ let mut y_count = 0;
+
+ for ey in &y_events {
+ let cur_y = clamp(p0.y, ey.value, p1.y);
+
+ if cur_y != prev_y {
+ let mut prev_x = clamp(p0.x, x_events[0].value, p1.x);
+ let mut region_x = 0;
+
+ for ex in &x_events {
+ let cur_x = clamp(p0.x, ex.value, p1.x);
+
+ if cur_x != prev_x {
+ segments.push(emit_segment_if_needed(
+ prev_x,
+ prev_y,
+ cur_x,
+ cur_y,
+ region_x,
+ region_y,
+ &self.items,
+ ));
+
+ prev_x = cur_x;
+ if y_count == 0 {
+ x_count += 1;
+ }
+ }
+
+ ex.update(
+ ItemFlags::X_ACTIVE,
+ &mut self.items,
+ &mut region_x,
+ );
+ }
+
+ prev_y = cur_y;
+ y_count += 1;
+ }
+
+ ey.update(
+ ItemFlags::Y_ACTIVE,
+ &mut self.items,
+ &mut region_y,
+ );
+ }
+
+ // If we created more than 64 segments, just bail out and draw it as a single primitive
+ // with a single mask, to avoid overhead of excessive amounts of segments. This can only
+ // happen in pathological cases, for example a cascade of a dozen or more overlapping
+ // and intersecting rounded clips.
+ if segments.len() > MAX_SEGMENTS {
+ f(&Segment {
+ edge_flags: EdgeAaSegmentMask::all(),
+ region_x: 0,
+ region_y: 0,
+ has_mask: true,
+ rect: bounding_rect,
+ });
+ return
+ }
+
+ // Run user supplied closure for each valid segment.
+ debug_assert_eq!(segments.len(), x_count * y_count);
+ for y in 0 .. y_count {
+ for x in 0 .. x_count {
+ let mut edge_flags = EdgeAaSegmentMask::empty();
+
+ if x == 0 || segments[y * x_count + x - 1].is_none() {
+ edge_flags |= EdgeAaSegmentMask::LEFT;
+ }
+ if x == x_count-1 || segments[y * x_count + x + 1].is_none() {
+ edge_flags |= EdgeAaSegmentMask::RIGHT;
+ }
+ if y == 0 || segments[(y-1) * x_count + x].is_none() {
+ edge_flags |= EdgeAaSegmentMask::TOP;
+ }
+ if y == y_count-1 || segments[(y+1) * x_count + x].is_none() {
+ edge_flags |= EdgeAaSegmentMask::BOTTOM;
+ }
+
+ if let Some(ref mut segment) = segments[y * x_count + x] {
+ segment.edge_flags = edge_flags;
+ f(segment);
+ }
+ }
+ }
+ }
+}
+
+fn clamp(low: Au, value: Au, high: Au) -> Au {
+ value.max(low).min(high)
+}
+
+fn emit_segment_if_needed(
+ x0: Au,
+ y0: Au,
+ x1: Au,
+ y1: Au,
+ region_x: usize,
+ region_y: usize,
+ items: &[Item],
+) -> Option<Segment> {
+ debug_assert!(x1 > x0);
+ debug_assert!(y1 > y0);
+
+ // TODO(gw): Don't scan the whole list of items for
+ // each segment rect. Store active list
+ // in a hash set or similar if this ever
+ // shows up in a profile.
+ let mut has_clip_mask = false;
+
+ for item in items {
+ if item.flags.contains(ItemFlags::X_ACTIVE | ItemFlags::Y_ACTIVE) {
+ has_clip_mask |= item.flags.contains(ItemFlags::HAS_MASK);
+
+ if item.mode == Some(ClipMode::ClipOut) && !item.flags.contains(ItemFlags::HAS_MASK) {
+ return None;
+ }
+ }
+ }
+
+ let segment_rect = LayoutRect {
+ min: LayoutPoint::new(
+ x0.to_f32_px(),
+ y0.to_f32_px(),
+ ),
+ max: LayoutPoint::new(
+ x1.to_f32_px(),
+ y1.to_f32_px(),
+ ),
+ };
+
+ Some(Segment {
+ rect: segment_rect,
+ has_mask: has_clip_mask,
+ edge_flags: EdgeAaSegmentMask::empty(),
+ region_x,
+ region_y,
+ })
+}
+
+#[cfg(test)]
+mod test {
+ use api::{BorderRadius, ClipMode};
+ use api::units::{LayoutPoint, LayoutRect};
+ use super::{Segment, SegmentBuilder, EdgeAaSegmentMask};
+ use std::cmp;
+
+ fn rect(x0: f32, y0: f32, x1: f32, y1: f32) -> LayoutRect {
+ LayoutRect {
+ min: LayoutPoint::new(x0, y0),
+ max: LayoutPoint::new(x1, y1),
+ }
+ }
+
+ fn seg(
+ x0: f32,
+ y0: f32,
+ x1: f32,
+ y1: f32,
+ has_mask: bool,
+ edge_flags: Option<EdgeAaSegmentMask>,
+ ) -> Segment {
+ seg_region(x0, y0, x1, y1, 0, 0, has_mask, edge_flags)
+ }
+
+ fn seg_region(
+ x0: f32,
+ y0: f32,
+ x1: f32,
+ y1: f32,
+ region_x: usize,
+ region_y: usize,
+ has_mask: bool,
+ edge_flags: Option<EdgeAaSegmentMask>,
+ ) -> Segment {
+ Segment {
+ rect: LayoutRect {
+ min: LayoutPoint::new(x0, y0),
+ max: LayoutPoint::new(x1, y1),
+ },
+ has_mask,
+ edge_flags: edge_flags.unwrap_or(EdgeAaSegmentMask::empty()),
+ region_x,
+ region_y,
+ }
+ }
+
+ fn segment_sorter(s0: &Segment, s1: &Segment) -> cmp::Ordering {
+ let r0 = &s0.rect;
+ let r1 = &s1.rect;
+
+ (
+ (r0.min.x, r0.min.y, r0.max.x, r0.max.y)
+ ).partial_cmp(&
+ (r1.min.x, r1.min.y, r1.max.x, r1.max.y)
+ ).unwrap()
+ }
+
+ fn seg_test(
+ local_rect: LayoutRect,
+ inner_rect: Option<LayoutRect>,
+ local_clip_rect: LayoutRect,
+ clips: &[(LayoutRect, Option<BorderRadius>, ClipMode)],
+ expected_segments: &mut [Segment]
+ ) {
+ let mut sb = SegmentBuilder::new();
+ sb.initialize(
+ local_rect,
+ inner_rect,
+ local_clip_rect,
+ );
+ sb.push_clip_rect(local_rect, None, ClipMode::Clip);
+ sb.push_clip_rect(local_clip_rect, None, ClipMode::Clip);
+ let mut segments = Vec::new();
+ for &(rect, radius, mode) in clips {
+ sb.push_clip_rect(rect, radius, mode);
+ }
+ sb.build(|segment| {
+ segments.push(Segment {
+ ..*segment
+ });
+ });
+ segments.sort_by(segment_sorter);
+ expected_segments.sort_by(segment_sorter);
+ assert_eq!(
+ segments.len(),
+ expected_segments.len(),
+ "segments\n{:?}\nexpected\n{:?}\n",
+ segments,
+ expected_segments
+ );
+ for (segment, expected) in segments.iter().zip(expected_segments.iter()) {
+ assert_eq!(segment, expected);
+ }
+ }
+
+ #[test]
+ fn segment_empty() {
+ seg_test(
+ rect(0.0, 0.0, 0.0, 0.0),
+ None,
+ rect(0.0, 0.0, 0.0, 0.0),
+ &[],
+ &mut [],
+ );
+ }
+
+ #[test]
+ fn segment_single() {
+ seg_test(
+ rect(10.0, 20.0, 30.0, 40.0),
+ None,
+ rect(10.0, 20.0, 30.0, 40.0),
+ &[],
+ &mut [
+ seg(10.0, 20.0, 30.0, 40.0, false,
+ Some(EdgeAaSegmentMask::LEFT |
+ EdgeAaSegmentMask::TOP |
+ EdgeAaSegmentMask::RIGHT |
+ EdgeAaSegmentMask::BOTTOM
+ )
+ ),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_single_clip() {
+ seg_test(
+ rect(10.0, 20.0, 30.0, 40.0),
+ None,
+ rect(10.0, 20.0, 25.0, 35.0),
+ &[],
+ &mut [
+ seg(10.0, 20.0, 25.0, 35.0, false,
+ Some(EdgeAaSegmentMask::LEFT |
+ EdgeAaSegmentMask::TOP |
+ EdgeAaSegmentMask::RIGHT |
+ EdgeAaSegmentMask::BOTTOM
+ )
+ ),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_inner_clip() {
+ seg_test(
+ rect(10.0, 20.0, 30.0, 40.0),
+ None,
+ rect(15.0, 25.0, 25.0, 35.0),
+ &[],
+ &mut [
+ seg(15.0, 25.0, 25.0, 35.0, false,
+ Some(EdgeAaSegmentMask::LEFT |
+ EdgeAaSegmentMask::TOP |
+ EdgeAaSegmentMask::RIGHT |
+ EdgeAaSegmentMask::BOTTOM
+ )
+ ),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_outer_clip() {
+ seg_test(
+ rect(15.0, 25.0, 25.0, 35.0),
+ None,
+ rect(10.0, 20.0, 30.0, 40.0),
+ &[],
+ &mut [
+ seg(15.0, 25.0, 25.0, 35.0, false,
+ Some(EdgeAaSegmentMask::LEFT |
+ EdgeAaSegmentMask::TOP |
+ EdgeAaSegmentMask::RIGHT |
+ EdgeAaSegmentMask::BOTTOM
+ )
+ ),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_clip_int() {
+ seg_test(
+ rect(10.0, 20.0, 30.0, 40.0),
+ None,
+ rect(20.0, 10.0, 40.0, 30.0),
+ &[],
+ &mut [
+ seg(20.0, 20.0, 30.0, 30.0, false,
+ Some(EdgeAaSegmentMask::LEFT |
+ EdgeAaSegmentMask::TOP |
+ EdgeAaSegmentMask::RIGHT |
+ EdgeAaSegmentMask::BOTTOM
+ )
+ ),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_clip_disjoint() {
+ seg_test(
+ rect(10.0, 20.0, 30.0, 40.0),
+ None,
+ rect(30.0, 20.0, 50.0, 40.0),
+ &[],
+ &mut [],
+ );
+ }
+
+ #[test]
+ fn segment_clips() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ None,
+ rect(-1000.0, -1000.0, 1000.0, 1000.0),
+ &[
+ (rect(20.0, 20.0, 40.0, 40.0), None, ClipMode::Clip),
+ (rect(40.0, 20.0, 60.0, 40.0), None, ClipMode::Clip),
+ ],
+ &mut [
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_rounded_clip() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ None,
+ rect(-1000.0, -1000.0, 1000.0, 1000.0),
+ &[
+ (rect(20.0, 20.0, 60.0, 60.0), Some(BorderRadius::uniform(10.0)), ClipMode::Clip),
+ ],
+ &mut [
+ // corners
+ seg(20.0, 20.0, 30.0, 30.0, true, Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::TOP)),
+ seg(20.0, 50.0, 30.0, 60.0, true, Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::BOTTOM)),
+ seg(50.0, 20.0, 60.0, 30.0, true, Some(EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::TOP)),
+ seg(50.0, 50.0, 60.0, 60.0, true, Some(EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::BOTTOM)),
+
+ // inner
+ seg(30.0, 30.0, 50.0, 50.0, false, None),
+
+ // edges
+ seg(30.0, 20.0, 50.0, 30.0, false, Some(EdgeAaSegmentMask::TOP)),
+ seg(30.0, 50.0, 50.0, 60.0, false, Some(EdgeAaSegmentMask::BOTTOM)),
+ seg(20.0, 30.0, 30.0, 50.0, false, Some(EdgeAaSegmentMask::LEFT)),
+ seg(50.0, 30.0, 60.0, 50.0, false, Some(EdgeAaSegmentMask::RIGHT)),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_clip_out() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ None,
+ rect(-1000.0, -1000.0, 2000.0, 2000.0),
+ &[
+ (rect(20.0, 20.0, 60.0, 60.0), None, ClipMode::ClipOut),
+ ],
+ &mut [
+ seg(0.0, 0.0, 20.0, 20.0, false, Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::LEFT)),
+ seg(20.0, 0.0, 60.0, 20.0, false, Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::BOTTOM)),
+ seg(60.0, 0.0, 100.0, 20.0, false, Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::RIGHT)),
+
+ seg(0.0, 20.0, 20.0, 60.0, false, Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::RIGHT)),
+ seg(60.0, 20.0, 100.0, 60.0, false, Some(EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::LEFT)),
+
+ seg(0.0, 60.0, 20.0, 100.0, false, Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::BOTTOM)),
+ seg(20.0, 60.0, 60.0, 100.0, false, Some(EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::TOP)),
+ seg(60.0, 60.0, 100.0, 100.0, false, Some(EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::RIGHT)),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_rounded_clip_out() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ None,
+ rect(-1000.0, -1000.0, 2000.0, 2000.0),
+ &[
+ (rect(20.0, 20.0, 60.0, 60.0), Some(BorderRadius::uniform(10.0)), ClipMode::ClipOut),
+ ],
+ &mut [
+ // top row
+ seg(0.0, 0.0, 20.0, 20.0, false, Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::LEFT)),
+ seg(20.0, 0.0, 30.0, 20.0, false, Some(EdgeAaSegmentMask::TOP)),
+ seg(30.0, 0.0, 50.0, 20.0, false, Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::BOTTOM)),
+ seg(50.0, 0.0, 60.0, 20.0, false, Some(EdgeAaSegmentMask::TOP)),
+ seg(60.0, 0.0, 100.0, 20.0, false, Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::RIGHT)),
+
+ // left
+ seg(0.0, 20.0, 20.0, 30.0, false, Some(EdgeAaSegmentMask::LEFT)),
+ seg(0.0, 30.0, 20.0, 50.0, false, Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::RIGHT)),
+ seg(0.0, 50.0, 20.0, 60.0, false, Some(EdgeAaSegmentMask::LEFT)),
+
+ // right
+ seg(60.0, 20.0, 100.0, 30.0, false, Some(EdgeAaSegmentMask::RIGHT)),
+ seg(60.0, 30.0, 100.0, 50.0, false, Some(EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::LEFT)),
+ seg(60.0, 50.0, 100.0, 60.0, false, Some(EdgeAaSegmentMask::RIGHT)),
+
+ // bottom row
+ seg(0.0, 60.0, 20.0, 100.0, false, Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::BOTTOM)),
+ seg(20.0, 60.0, 30.0, 100.0, false, Some(EdgeAaSegmentMask::BOTTOM)),
+ seg(30.0, 60.0, 50.0, 100.0, false, Some(EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::TOP)),
+ seg(50.0, 60.0, 60.0, 100.0, false, Some(EdgeAaSegmentMask::BOTTOM)),
+ seg(60.0, 60.0, 100.0, 100.0, false, Some(EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::BOTTOM)),
+
+ // inner corners
+ seg(20.0, 20.0, 30.0, 30.0, true, Some(EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::BOTTOM)),
+ seg(20.0, 50.0, 30.0, 60.0, true, Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::RIGHT)),
+ seg(50.0, 20.0, 60.0, 30.0, true, Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::BOTTOM)),
+ seg(50.0, 50.0, 60.0, 60.0, true, Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::TOP)),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_clip_in_clip_out() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ None,
+ rect(-1000.0, -1000.0, 2000.0, 2000.0),
+ &[
+ (rect(20.0, 20.0, 60.0, 60.0), None, ClipMode::Clip),
+ (rect(50.0, 50.0, 80.0, 80.0), None, ClipMode::ClipOut),
+ ],
+ &mut [
+ seg(20.0, 20.0, 50.0, 50.0, false, Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::TOP)),
+ seg(50.0, 20.0, 60.0, 50.0, false, Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::BOTTOM)),
+ seg(20.0, 50.0, 50.0, 60.0, false, Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::RIGHT)),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_rounded_clip_overlap() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ None,
+ rect(0.0, 0.0, 100.0, 100.0),
+ &[
+ (rect(0.0, 0.0, 10.0, 10.0), None, ClipMode::ClipOut),
+ (rect(0.0, 0.0, 100.0, 100.0), Some(BorderRadius::uniform(10.0)), ClipMode::Clip),
+ ],
+ &mut [
+ // corners
+ seg(0.0, 90.0, 10.0, 100.0, true, Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::BOTTOM)),
+ seg(90.0, 0.0, 100.0, 10.0, true, Some(EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::TOP)),
+ seg(90.0, 90.0, 100.0, 100.0, true, Some(EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::BOTTOM)),
+
+ // inner
+ seg(10.0, 10.0, 90.0, 90.0, false, None),
+
+ // edges
+ seg(10.0, 0.0, 90.0, 10.0, false, Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::LEFT)),
+ seg(10.0, 90.0, 90.0, 100.0, false, Some(EdgeAaSegmentMask::BOTTOM)),
+ seg(0.0, 10.0, 10.0, 90.0, false, Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::TOP)),
+ seg(90.0, 10.0, 100.0, 90.0, false, Some(EdgeAaSegmentMask::RIGHT)),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_rounded_clip_overlap_reverse() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ None,
+ rect(0.0, 0.0, 100.0, 100.0),
+ &[
+ (rect(10.0, 10.0, 90.0, 90.0), None, ClipMode::Clip),
+ (rect(0.0, 0.0, 100.0, 100.0), Some(BorderRadius::uniform(10.0)), ClipMode::Clip),
+ ],
+ &mut [
+ seg(10.0, 10.0, 90.0, 90.0, false,
+ Some(EdgeAaSegmentMask::LEFT |
+ EdgeAaSegmentMask::TOP |
+ EdgeAaSegmentMask::RIGHT |
+ EdgeAaSegmentMask::BOTTOM
+ )
+ ),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_clip_in_clip_out_overlap() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ None,
+ rect(0.0, 0.0, 100.0, 100.0),
+ &[
+ (rect(10.0, 10.0, 90.0, 90.0), None, ClipMode::Clip),
+ (rect(10.0, 10.0, 90.0, 90.0), None, ClipMode::ClipOut),
+ ],
+ &mut [
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_event_order() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ None,
+ rect(0.0, 0.0, 100.0, 100.0),
+ &[
+ (rect(0.0, 0.0, 100.0, 90.0), None, ClipMode::ClipOut),
+ ],
+ &mut [
+ seg(0.0, 90.0, 100.0, 100.0, false, Some(
+ EdgeAaSegmentMask::LEFT |
+ EdgeAaSegmentMask::RIGHT |
+ EdgeAaSegmentMask::BOTTOM |
+ EdgeAaSegmentMask::TOP
+ )),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_region_simple() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ Some(rect(20.0, 40.0, 60.0, 80.0)),
+ rect(0.0, 0.0, 100.0, 100.0),
+ &[
+ ],
+ &mut [
+ seg_region(
+ 0.0, 0.0,
+ 20.0, 40.0,
+ 0, 0,
+ false,
+ Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::TOP)
+ ),
+
+ seg_region(
+ 20.0, 0.0,
+ 60.0, 40.0,
+ 1, 0,
+ false,
+ Some(EdgeAaSegmentMask::TOP)
+ ),
+
+ seg_region(
+ 60.0, 0.0,
+ 100.0, 40.0,
+ 2, 0,
+ false,
+ Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::RIGHT)
+ ),
+
+ seg_region(
+ 0.0, 40.0,
+ 20.0, 80.0,
+ 0, 1,
+ false,
+ Some(EdgeAaSegmentMask::LEFT)
+ ),
+
+ seg_region(
+ 20.0, 40.0,
+ 60.0, 80.0,
+ 1, 1,
+ false,
+ None,
+ ),
+
+ seg_region(
+ 60.0, 40.0,
+ 100.0, 80.0,
+ 2, 1,
+ false,
+ Some(EdgeAaSegmentMask::RIGHT)
+ ),
+
+ seg_region(
+ 0.0, 80.0,
+ 20.0, 100.0,
+ 0, 2,
+ false,
+ Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::BOTTOM)
+ ),
+
+ seg_region(
+ 20.0, 80.0,
+ 60.0, 100.0,
+ 1, 2,
+ false,
+ Some(EdgeAaSegmentMask::BOTTOM),
+ ),
+
+ seg_region(
+ 60.0, 80.0,
+ 100.0, 100.0,
+ 2, 2,
+ false,
+ Some(EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::BOTTOM)
+ ),
+
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_region_clip() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ Some(rect(20.0, 40.0, 60.0, 80.0)),
+ rect(0.0, 0.0, 100.0, 100.0),
+ &[
+ (rect(0.0, 0.0, 100.0, 90.0), None, ClipMode::ClipOut),
+ ],
+ &mut [
+ seg_region(
+ 0.0, 90.0,
+ 20.0, 100.0,
+ 0, 2,
+ false,
+ Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::TOP)
+ ),
+
+ seg_region(
+ 20.0, 90.0,
+ 60.0, 100.0,
+ 1, 2,
+ false,
+ Some(EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::TOP),
+ ),
+
+ seg_region(
+ 60.0, 90.0,
+ 100.0, 100.0,
+ 2, 2,
+ false,
+ Some(EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::TOP)
+ ),
+
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_region_clip2() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ Some(rect(20.0, 20.0, 80.0, 80.0)),
+ rect(0.0, 0.0, 100.0, 100.0),
+ &[
+ (rect(20.0, 20.0, 100.0, 100.0), None, ClipMode::ClipOut),
+ ],
+ &mut [
+ seg_region(
+ 0.0, 0.0,
+ 20.0, 20.0,
+ 0, 0,
+ false,
+ Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::TOP)
+ ),
+
+ seg_region(
+ 20.0, 0.0,
+ 80.0, 20.0,
+ 1, 0,
+ false,
+ Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::BOTTOM),
+ ),
+
+ seg_region(
+ 80.0, 0.0,
+ 100.0, 20.0,
+ 2, 0,
+ false,
+ Some(EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::BOTTOM)
+ ),
+
+ seg_region(
+ 0.0, 20.0,
+ 20.0, 80.0,
+ 0, 1,
+ false,
+ Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::RIGHT)
+ ),
+
+ seg_region(
+ 0.0, 80.0,
+ 20.0, 100.0,
+ 0, 2,
+ false,
+ Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::RIGHT)
+ ),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_region_clip3() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ Some(rect(20.0, 20.0, 80.0, 80.0)),
+ rect(0.0, 0.0, 100.0, 100.0),
+ &[
+ (rect(10.0, 10.0, 30.0, 30.0), None, ClipMode::Clip),
+ ],
+ &mut [
+ seg_region(
+ 10.0, 10.0,
+ 20.0, 20.0,
+ 0, 0,
+ false,
+ Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::LEFT),
+ ),
+
+ seg_region(
+ 20.0, 10.0,
+ 30.0, 20.0,
+ 1, 0,
+ false,
+ Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::RIGHT),
+ ),
+
+ seg_region(
+ 10.0, 20.0,
+ 20.0, 30.0,
+ 0, 1,
+ false,
+ Some(EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::LEFT),
+ ),
+
+ seg_region(
+ 20.0, 20.0,
+ 30.0, 30.0,
+ 1, 1,
+ false,
+ Some(EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::RIGHT),
+ ),
+ ],
+ );
+ }
+}
diff --git a/gfx/wr/webrender/src/space.rs b/gfx/wr/webrender/src/space.rs
new file mode 100644
index 0000000000..518bee6a67
--- /dev/null
+++ b/gfx/wr/webrender/src/space.rs
@@ -0,0 +1,269 @@
+/* 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/. */
+
+
+//! Utilities to deal with coordinate spaces.
+
+use std::fmt;
+
+use euclid::{Transform3D, Box2D, Point2D, Vector2D};
+
+use api::units::*;
+use crate::spatial_tree::{SpatialTree, CoordinateSpaceMapping, SpatialNodeIndex, VisibleFace, SpatialNodeContainer};
+use crate::util::project_rect;
+use crate::util::{MatrixHelpers, ScaleOffset, RectHelpers, PointHelpers};
+
+
+#[derive(Debug, Clone)]
+pub struct SpaceMapper<F, T> {
+ kind: CoordinateSpaceMapping<F, T>,
+ pub ref_spatial_node_index: SpatialNodeIndex,
+ pub current_target_spatial_node_index: SpatialNodeIndex,
+ pub bounds: Box2D<f32, T>,
+ visible_face: VisibleFace,
+}
+
+impl<F, T> SpaceMapper<F, T> where F: fmt::Debug {
+ pub fn new(
+ ref_spatial_node_index: SpatialNodeIndex,
+ bounds: Box2D<f32, T>,
+ ) -> Self {
+ SpaceMapper {
+ kind: CoordinateSpaceMapping::Local,
+ ref_spatial_node_index,
+ current_target_spatial_node_index: ref_spatial_node_index,
+ bounds,
+ visible_face: VisibleFace::Front,
+ }
+ }
+
+ pub fn new_with_target(
+ ref_spatial_node_index: SpatialNodeIndex,
+ target_node_index: SpatialNodeIndex,
+ bounds: Box2D<f32, T>,
+ spatial_tree: &SpatialTree,
+ ) -> Self {
+ let mut mapper = Self::new(ref_spatial_node_index, bounds);
+ mapper.set_target_spatial_node(target_node_index, spatial_tree);
+ mapper
+ }
+
+ pub fn set_target_spatial_node(
+ &mut self,
+ target_node_index: SpatialNodeIndex,
+ spatial_tree: &SpatialTree,
+ ) {
+ if target_node_index == self.current_target_spatial_node_index {
+ return
+ }
+
+ let ref_spatial_node = spatial_tree.get_spatial_node(self.ref_spatial_node_index);
+ let target_spatial_node = spatial_tree.get_spatial_node(target_node_index);
+ self.visible_face = VisibleFace::Front;
+
+ self.kind = if self.ref_spatial_node_index == target_node_index {
+ CoordinateSpaceMapping::Local
+ } else if ref_spatial_node.coordinate_system_id == target_spatial_node.coordinate_system_id {
+ let scale_offset = ref_spatial_node.content_transform
+ .inverse()
+ .accumulate(&target_spatial_node.content_transform);
+ CoordinateSpaceMapping::ScaleOffset(scale_offset)
+ } else {
+ let transform = spatial_tree
+ .get_relative_transform_with_face(
+ target_node_index,
+ self.ref_spatial_node_index,
+ Some(&mut self.visible_face),
+ )
+ .into_transform()
+ .with_source::<F>()
+ .with_destination::<T>();
+ CoordinateSpaceMapping::Transform(transform)
+ };
+
+ self.current_target_spatial_node_index = target_node_index;
+ }
+
+ pub fn get_transform(&self) -> Transform3D<f32, F, T> {
+ match self.kind {
+ CoordinateSpaceMapping::Local => {
+ Transform3D::identity()
+ }
+ CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
+ scale_offset.to_transform()
+ }
+ CoordinateSpaceMapping::Transform(transform) => {
+ transform
+ }
+ }
+ }
+
+ pub fn unmap(&self, rect: &Box2D<f32, T>) -> Option<Box2D<f32, F>> {
+ match self.kind {
+ CoordinateSpaceMapping::Local => {
+ Some(rect.cast_unit())
+ }
+ CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
+ Some(scale_offset.unmap_rect(rect))
+ }
+ CoordinateSpaceMapping::Transform(ref transform) => {
+ transform.inverse_rect_footprint(rect)
+ }
+ }
+ }
+
+ pub fn map(&self, rect: &Box2D<f32, F>) -> Option<Box2D<f32, T>> {
+ match self.kind {
+ CoordinateSpaceMapping::Local => {
+ Some(rect.cast_unit())
+ }
+ CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
+ Some(scale_offset.map_rect(rect))
+ }
+ CoordinateSpaceMapping::Transform(ref transform) => {
+ match project_rect(transform, rect, &self.bounds) {
+ Some(bounds) => {
+ Some(bounds)
+ }
+ None => {
+ warn!("parent relative transform can't transform the primitive rect for {:?}", rect);
+ None
+ }
+ }
+ }
+ }
+ }
+
+ // Attempt to return a rect that is contained in the mapped rect.
+ pub fn map_inner_bounds(&self, rect: &Box2D<f32, F>) -> Option<Box2D<f32, T>> {
+ match self.kind {
+ CoordinateSpaceMapping::Local => {
+ Some(rect.cast_unit())
+ }
+ CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
+ Some(scale_offset.map_rect(rect))
+ }
+ CoordinateSpaceMapping::Transform(..) => {
+ // We could figure out a rect that is contained in the transformed rect but
+ // for now we do the simple thing here and bail out.
+ return None;
+ }
+ }
+ }
+
+ // Map a local space point to the target coordinate space
+ pub fn map_point(&self, p: Point2D<f32, F>) -> Option<Point2D<f32, T>> {
+ match self.kind {
+ CoordinateSpaceMapping::Local => {
+ Some(p.cast_unit())
+ }
+ CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
+ Some(scale_offset.map_point(&p))
+ }
+ CoordinateSpaceMapping::Transform(ref transform) => {
+ transform.transform_point2d(p)
+ }
+ }
+ }
+
+ pub fn map_vector(&self, v: Vector2D<f32, F>) -> Vector2D<f32, T> {
+ match self.kind {
+ CoordinateSpaceMapping::Local => {
+ v.cast_unit()
+ }
+ CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
+ scale_offset.map_vector(&v)
+ }
+ CoordinateSpaceMapping::Transform(ref transform) => {
+ transform.transform_vector2d(v)
+ }
+ }
+ }
+}
+
+
+#[derive(Clone, Debug)]
+pub struct SpaceSnapper {
+ ref_spatial_node_index: SpatialNodeIndex,
+ current_target_spatial_node_index: SpatialNodeIndex,
+ snapping_transform: Option<ScaleOffset>,
+ raster_pixel_scale: RasterPixelScale,
+}
+
+impl SpaceSnapper {
+ pub fn new(
+ ref_spatial_node_index: SpatialNodeIndex,
+ raster_pixel_scale: RasterPixelScale,
+ ) -> Self {
+ SpaceSnapper {
+ ref_spatial_node_index,
+ current_target_spatial_node_index: SpatialNodeIndex::INVALID,
+ snapping_transform: None,
+ raster_pixel_scale,
+ }
+ }
+
+ pub fn new_with_target<S: SpatialNodeContainer>(
+ ref_spatial_node_index: SpatialNodeIndex,
+ target_node_index: SpatialNodeIndex,
+ raster_pixel_scale: RasterPixelScale,
+ spatial_tree: &S,
+ ) -> Self {
+ let mut snapper = SpaceSnapper {
+ ref_spatial_node_index,
+ current_target_spatial_node_index: SpatialNodeIndex::INVALID,
+ snapping_transform: None,
+ raster_pixel_scale,
+ };
+
+ snapper.set_target_spatial_node(target_node_index, spatial_tree);
+ snapper
+ }
+
+ pub fn set_target_spatial_node<S: SpatialNodeContainer>(
+ &mut self,
+ target_node_index: SpatialNodeIndex,
+ spatial_tree: &S,
+ ) {
+ if target_node_index == self.current_target_spatial_node_index {
+ return
+ }
+
+ let ref_snap = spatial_tree.get_node_info(self.ref_spatial_node_index).snapping_transform;
+ let target_snap = spatial_tree.get_node_info(target_node_index).snapping_transform;
+
+ self.current_target_spatial_node_index = target_node_index;
+ self.snapping_transform = match (ref_snap, target_snap) {
+ (Some(ref ref_scale_offset), Some(ref target_scale_offset)) => {
+ Some(ref_scale_offset
+ .inverse()
+ .accumulate(target_scale_offset)
+ .scale(self.raster_pixel_scale.0))
+ }
+ _ => None,
+ };
+ }
+
+ pub fn snap_rect<F>(&self, rect: &Box2D<f32, F>) -> Box2D<f32, F> where F: fmt::Debug {
+ debug_assert!(self.current_target_spatial_node_index != SpatialNodeIndex::INVALID);
+ match self.snapping_transform {
+ Some(ref scale_offset) => {
+ let snapped_device_rect: DeviceRect = scale_offset.map_rect(rect).snap();
+ scale_offset.unmap_rect(&snapped_device_rect)
+ }
+ None => *rect,
+ }
+ }
+
+ pub fn snap_point<F>(&self, point: &Point2D<f32, F>) -> Point2D<f32, F> where F: fmt::Debug {
+ debug_assert!(self.current_target_spatial_node_index != SpatialNodeIndex::INVALID);
+ match self.snapping_transform {
+ Some(ref scale_offset) => {
+ let snapped_device_vector : DevicePoint = scale_offset.map_point(point).snap();
+ scale_offset.unmap_point(&snapped_device_vector)
+ }
+ None => *point,
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/spatial_node.rs b/gfx/wr/webrender/src/spatial_node.rs
new file mode 100644
index 0000000000..9a2039e37b
--- /dev/null
+++ b/gfx/wr/webrender/src/spatial_node.rs
@@ -0,0 +1,995 @@
+
+/* 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 api::{ExternalScrollId, PipelineId, PropertyBinding, PropertyBindingId, ReferenceFrameKind};
+use api::{APZScrollGeneration, HasScrollLinkedEffect, SampledScrollOffset};
+use api::{TransformStyle, StickyOffsetBounds, SpatialTreeItemKey};
+use api::units::*;
+use crate::internal_types::PipelineInstanceId;
+use crate::spatial_tree::{CoordinateSystem, SpatialNodeIndex, TransformUpdateState};
+use crate::spatial_tree::{CoordinateSystemId};
+use euclid::{Vector2D, SideOffsets2D};
+use crate::scene::SceneProperties;
+use crate::util::{LayoutFastTransform, MatrixHelpers, ScaleOffset, TransformedRectKind, PointHelpers};
+
+/// The kind of a spatial node uid. These are required because we currently create external
+/// nodes during DL building, but the internal nodes aren't created until scene building.
+/// TODO(gw): The internal scroll and reference frames are not used in any important way
+// by Gecko - they were primarily useful for Servo. So we should plan to remove
+// them completely.
+#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum SpatialNodeUidKind {
+ /// The root node of the entire spatial tree
+ Root,
+ /// Internal scroll frame created during scene building for each iframe
+ InternalScrollFrame,
+ /// Internal reference frame created during scene building for each iframe
+ InternalReferenceFrame,
+ /// A normal spatial node uid, defined by a caller provided unique key
+ External {
+ key: SpatialTreeItemKey,
+ },
+}
+
+/// A unique identifier for a spatial node, that is stable across display lists
+#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SpatialNodeUid {
+ /// The unique key for a given pipeline for this uid
+ pub kind: SpatialNodeUidKind,
+ /// Pipeline id to namespace key kinds
+ pub pipeline_id: PipelineId,
+ /// Instance of this pipeline id
+ pub instance_id: PipelineInstanceId,
+}
+
+impl SpatialNodeUid {
+ pub fn root() -> Self {
+ SpatialNodeUid {
+ kind: SpatialNodeUidKind::Root,
+ pipeline_id: PipelineId::dummy(),
+ instance_id: PipelineInstanceId::new(0),
+ }
+ }
+
+ pub fn root_scroll_frame(
+ pipeline_id: PipelineId,
+ instance_id: PipelineInstanceId,
+ ) -> Self {
+ SpatialNodeUid {
+ kind: SpatialNodeUidKind::InternalScrollFrame,
+ pipeline_id,
+ instance_id,
+ }
+ }
+
+ pub fn root_reference_frame(
+ pipeline_id: PipelineId,
+ instance_id: PipelineInstanceId,
+ ) -> Self {
+ SpatialNodeUid {
+ kind: SpatialNodeUidKind::InternalReferenceFrame,
+ pipeline_id,
+ instance_id,
+ }
+ }
+
+ pub fn external(
+ key: SpatialTreeItemKey,
+ pipeline_id: PipelineId,
+ instance_id: PipelineInstanceId,
+ ) -> Self {
+ SpatialNodeUid {
+ kind: SpatialNodeUidKind::External {
+ key,
+ },
+ pipeline_id,
+ instance_id,
+ }
+ }
+}
+
+/// Defines the content of a spatial node. If the values in the descriptor don't
+/// change, that means the rest of the fields in a spatial node will end up with
+/// the same result
+#[derive(Clone, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SpatialNodeDescriptor {
+ /// The type of this node and any data associated with that node type.
+ pub node_type: SpatialNodeType,
+
+ /// Pipeline that this layer belongs to
+ pub pipeline_id: PipelineId,
+}
+
+#[derive(Clone, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum SpatialNodeType {
+ /// A special kind of node that adjusts its position based on the position
+ /// of its parent node and a given set of sticky positioning offset bounds.
+ /// Sticky positioned is described in the CSS Positioned Layout Module Level 3 here:
+ /// https://www.w3.org/TR/css-position-3/#sticky-pos
+ StickyFrame(StickyFrameInfo),
+
+ /// Transforms it's content, but doesn't clip it. Can also be adjusted
+ /// by scroll events or setting scroll offsets.
+ ScrollFrame(ScrollFrameInfo),
+
+ /// A reference frame establishes a new coordinate space in the tree.
+ ReferenceFrame(ReferenceFrameInfo),
+}
+
+/// Information about a spatial node that can be queried during either scene of
+/// frame building.
+pub struct SpatialNodeInfo<'a> {
+ /// The type of this node and any data associated with that node type.
+ pub node_type: &'a SpatialNodeType,
+
+ /// Parent spatial node. If this is None, we are the root node.
+ pub parent: Option<SpatialNodeIndex>,
+
+ /// Snapping scale/offset relative to the coordinate system. If None, then
+ /// we should not snap entities bound to this spatial node.
+ pub snapping_transform: Option<ScaleOffset>,
+}
+
+/// Scene building specific representation of a spatial node, which is a much
+/// lighter subset of a full spatial node constructed and used for frame building
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(PartialEq)]
+pub struct SceneSpatialNode {
+ /// Snapping scale/offset relative to the coordinate system. If None, then
+ /// we should not snap entities bound to this spatial node.
+ pub snapping_transform: Option<ScaleOffset>,
+
+ /// Parent spatial node. If this is None, we are the root node.
+ pub parent: Option<SpatialNodeIndex>,
+
+ /// Descriptor describing how this spatial node behaves
+ pub descriptor: SpatialNodeDescriptor,
+
+ /// If true, this spatial node is known to exist in the root coordinate
+ /// system in all cases (it has no animated or complex transforms)
+ pub is_root_coord_system: bool,
+}
+
+impl SceneSpatialNode {
+ pub fn new_reference_frame(
+ parent_index: Option<SpatialNodeIndex>,
+ transform_style: TransformStyle,
+ source_transform: PropertyBinding<LayoutTransform>,
+ kind: ReferenceFrameKind,
+ origin_in_parent_reference_frame: LayoutVector2D,
+ pipeline_id: PipelineId,
+ is_root_coord_system: bool,
+ is_pipeline_root: bool,
+ ) -> Self {
+ let info = ReferenceFrameInfo {
+ transform_style,
+ source_transform,
+ kind,
+ origin_in_parent_reference_frame,
+ is_pipeline_root,
+ };
+ Self::new(
+ pipeline_id,
+ parent_index,
+ SpatialNodeType::ReferenceFrame(info),
+ is_root_coord_system,
+ )
+ }
+
+ pub fn new_scroll_frame(
+ pipeline_id: PipelineId,
+ parent_index: SpatialNodeIndex,
+ external_id: ExternalScrollId,
+ frame_rect: &LayoutRect,
+ content_size: &LayoutSize,
+ frame_kind: ScrollFrameKind,
+ external_scroll_offset: LayoutVector2D,
+ offset_generation: APZScrollGeneration,
+ has_scroll_linked_effect: HasScrollLinkedEffect,
+ is_root_coord_system: bool,
+ ) -> Self {
+ let node_type = SpatialNodeType::ScrollFrame(ScrollFrameInfo::new(
+ *frame_rect,
+ LayoutSize::new(
+ (content_size.width - frame_rect.width()).max(0.0),
+ (content_size.height - frame_rect.height()).max(0.0)
+ ),
+ external_id,
+ frame_kind,
+ external_scroll_offset,
+ offset_generation,
+ has_scroll_linked_effect,
+ )
+ );
+
+ Self::new(
+ pipeline_id,
+ Some(parent_index),
+ node_type,
+ is_root_coord_system,
+ )
+ }
+
+ pub fn new_sticky_frame(
+ parent_index: SpatialNodeIndex,
+ sticky_frame_info: StickyFrameInfo,
+ pipeline_id: PipelineId,
+ is_root_coord_system: bool,
+ ) -> Self {
+ Self::new(
+ pipeline_id,
+ Some(parent_index),
+ SpatialNodeType::StickyFrame(sticky_frame_info),
+ is_root_coord_system,
+ )
+ }
+
+ fn new(
+ pipeline_id: PipelineId,
+ parent_index: Option<SpatialNodeIndex>,
+ node_type: SpatialNodeType,
+ is_root_coord_system: bool,
+ ) -> Self {
+ SceneSpatialNode {
+ parent: parent_index,
+ descriptor: SpatialNodeDescriptor {
+ pipeline_id,
+ node_type,
+ },
+ snapping_transform: None,
+ is_root_coord_system,
+ }
+ }
+}
+
+/// Contains information common among all types of SpatialTree nodes.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SpatialNode {
+ /// The scale/offset of the viewport for this spatial node, relative to the
+ /// coordinate system. Includes any accumulated scrolling offsets from nodes
+ /// between our reference frame and this node.
+ pub viewport_transform: ScaleOffset,
+
+ /// Content scale/offset relative to the coordinate system.
+ pub content_transform: ScaleOffset,
+
+ /// Snapping scale/offset relative to the coordinate system. If None, then
+ /// we should not snap entities bound to this spatial node.
+ pub snapping_transform: Option<ScaleOffset>,
+
+ /// The axis-aligned coordinate system id of this node.
+ pub coordinate_system_id: CoordinateSystemId,
+
+ /// The current transform kind of this node.
+ pub transform_kind: TransformedRectKind,
+
+ /// Pipeline that this layer belongs to
+ pub pipeline_id: PipelineId,
+
+ /// Parent layer. If this is None, we are the root node.
+ pub parent: Option<SpatialNodeIndex>,
+
+ /// Child layers
+ pub children: Vec<SpatialNodeIndex>,
+
+ /// The type of this node and any data associated with that node type.
+ pub node_type: SpatialNodeType,
+
+ /// True if this node is transformed by an invertible transform. If not, display items
+ /// transformed by this node will not be displayed and display items not transformed by this
+ /// node will not be clipped by clips that are transformed by this node.
+ pub invertible: bool,
+
+ /// Whether this specific node is currently being async zoomed.
+ /// Should be set when a SetIsTransformAsyncZooming FrameMsg is received.
+ pub is_async_zooming: bool,
+
+ /// Whether this node or any of its ancestors is being pinch zoomed.
+ /// This is calculated in update(). This will be used to decide whether
+ /// to override corresponding picture's raster space as an optimisation.
+ pub is_ancestor_or_self_zooming: bool,
+}
+
+/// Snap an offset to be incorporated into a transform, where the local space
+/// may be considered the world space. We assume raster scale is 1.0, which
+/// may not always be correct if there are intermediate surfaces used, however
+/// those are either cases where snapping is not important (e.g. has perspective
+/// or is not axis aligned), or an edge case (e.g. SVG filters) which we can accept
+/// imperfection for now.
+fn snap_offset<OffsetUnits, ScaleUnits>(
+ offset: Vector2D<f32, OffsetUnits>,
+ scale: Vector2D<f32, ScaleUnits>,
+) -> Vector2D<f32, OffsetUnits> {
+ let world_offset = WorldPoint::new(offset.x * scale.x, offset.y * scale.y);
+ let snapped_world_offset = world_offset.snap();
+ Vector2D::new(
+ if scale.x != 0.0 { snapped_world_offset.x / scale.x } else { offset.x },
+ if scale.y != 0.0 { snapped_world_offset.y / scale.y } else { offset.y },
+ )
+}
+
+impl SpatialNode {
+ pub fn add_child(&mut self, child: SpatialNodeIndex) {
+ self.children.push(child);
+ }
+
+ pub fn set_scroll_offsets(&mut self, mut offsets: Vec<SampledScrollOffset>) -> bool {
+ debug_assert!(offsets.len() > 0);
+
+ let scrolling = match self.node_type {
+ SpatialNodeType::ScrollFrame(ref mut scrolling) => scrolling,
+ _ => {
+ warn!("Tried to scroll a non-scroll node.");
+ return false;
+ }
+ };
+
+ for element in offsets.iter_mut() {
+ element.offset = -element.offset - scrolling.external_scroll_offset;
+ }
+
+ if scrolling.offsets == offsets {
+ return false;
+ }
+
+ scrolling.offsets = offsets;
+ true
+ }
+
+ pub fn mark_uninvertible(
+ &mut self,
+ state: &TransformUpdateState,
+ ) {
+ self.invertible = false;
+ self.viewport_transform = ScaleOffset::identity();
+ self.content_transform = ScaleOffset::identity();
+ self.coordinate_system_id = state.current_coordinate_system_id;
+ }
+
+ pub fn update(
+ &mut self,
+ state_stack: &[TransformUpdateState],
+ coord_systems: &mut Vec<CoordinateSystem>,
+ scene_properties: &SceneProperties,
+ ) {
+ let state = state_stack.last().unwrap();
+
+ self.is_ancestor_or_self_zooming = self.is_async_zooming | state.is_ancestor_or_self_zooming;
+
+ // If any of our parents was not rendered, we are not rendered either and can just
+ // quit here.
+ if !state.invertible {
+ self.mark_uninvertible(state);
+ return;
+ }
+
+ self.update_transform(
+ state_stack,
+ coord_systems,
+ scene_properties,
+ );
+
+ if !self.invertible {
+ self.mark_uninvertible(state);
+ }
+ }
+
+ pub fn update_transform(
+ &mut self,
+ state_stack: &[TransformUpdateState],
+ coord_systems: &mut Vec<CoordinateSystem>,
+ scene_properties: &SceneProperties,
+ ) {
+ let state = state_stack.last().unwrap();
+
+ // Start by assuming we're invertible
+ self.invertible = true;
+
+ match self.node_type {
+ SpatialNodeType::ReferenceFrame(ref mut info) => {
+ let mut cs_scale_offset = ScaleOffset::identity();
+ let mut coordinate_system_id = state.current_coordinate_system_id;
+
+ // Resolve the transform against any property bindings.
+ let source_transform = {
+ let source_transform = scene_properties.resolve_layout_transform(&info.source_transform);
+ if let ReferenceFrameKind::Transform { is_2d_scale_translation: true, .. } = info.kind {
+ assert!(source_transform.is_2d_scale_translation(), "Reference frame was marked as only having 2d scale or translation");
+ }
+
+ LayoutFastTransform::from(source_transform)
+ };
+
+ // Do a change-basis operation on the perspective matrix using
+ // the scroll offset.
+ let source_transform = match info.kind {
+ ReferenceFrameKind::Perspective { scrolling_relative_to: Some(external_id) } => {
+ let mut scroll_offset = LayoutVector2D::zero();
+
+ for parent_state in state_stack.iter().rev() {
+ if let Some(parent_external_id) = parent_state.external_id {
+ if parent_external_id == external_id {
+ break;
+ }
+ }
+
+ scroll_offset += parent_state.scroll_offset;
+ }
+
+ // Do a change-basis operation on the
+ // perspective matrix using the scroll offset.
+ source_transform
+ .pre_translate(scroll_offset)
+ .then_translate(-scroll_offset)
+ }
+ ReferenceFrameKind::Perspective { scrolling_relative_to: None } |
+ ReferenceFrameKind::Transform { .. } => source_transform,
+ };
+
+ let resolved_transform =
+ LayoutFastTransform::with_vector(info.origin_in_parent_reference_frame)
+ .pre_transform(&source_transform);
+
+ // The transformation for this viewport in world coordinates is the transformation for
+ // our parent reference frame, plus any accumulated scrolling offsets from nodes
+ // between our reference frame and this node. Finally, we also include
+ // whatever local transformation this reference frame provides.
+ let relative_transform = resolved_transform
+ .then_translate(snap_offset(state.parent_accumulated_scroll_offset, state.coordinate_system_relative_scale_offset.scale))
+ .to_transform()
+ .with_destination::<LayoutPixel>();
+
+ let mut reset_cs_id = match info.transform_style {
+ TransformStyle::Preserve3D => !state.preserves_3d,
+ TransformStyle::Flat => state.preserves_3d,
+ };
+
+ // We reset the coordinate system upon either crossing the preserve-3d context boundary,
+ // or simply a 3D transformation.
+ if !reset_cs_id {
+ // Try to update our compatible coordinate system transform. If we cannot, start a new
+ // incompatible coordinate system.
+ match ScaleOffset::from_transform(&relative_transform) {
+ Some(ref scale_offset) => {
+ // We generally do not want to snap animated transforms as it causes jitter.
+ // However, we do want to snap the visual viewport offset when scrolling.
+ // This may still cause jitter when zooming, unfortunately.
+ let mut maybe_snapped = scale_offset.clone();
+ if let ReferenceFrameKind::Transform { should_snap: true, .. } = info.kind {
+ maybe_snapped.offset = snap_offset(
+ scale_offset.offset,
+ state.coordinate_system_relative_scale_offset.scale,
+ );
+ }
+ cs_scale_offset =
+ state.coordinate_system_relative_scale_offset.accumulate(&maybe_snapped);
+ }
+ None => reset_cs_id = true,
+ }
+ }
+ if reset_cs_id {
+ // If we break 2D axis alignment or have a perspective component, we need to start a
+ // new incompatible coordinate system with which we cannot share clips without masking.
+ let transform = relative_transform.then(
+ &state.coordinate_system_relative_scale_offset.to_transform()
+ );
+
+ // Push that new coordinate system and record the new id.
+ let coord_system = {
+ let parent_system = &coord_systems[state.current_coordinate_system_id.0 as usize];
+ let mut cur_transform = transform;
+ if parent_system.should_flatten {
+ cur_transform.flatten_z_output();
+ }
+ let world_transform = cur_transform.then(&parent_system.world_transform);
+ let determinant = world_transform.determinant();
+ self.invertible = determinant != 0.0 && !determinant.is_nan();
+
+ CoordinateSystem {
+ transform,
+ world_transform,
+ should_flatten: match (info.transform_style, info.kind) {
+ (TransformStyle::Flat, ReferenceFrameKind::Transform { .. }) => true,
+ (_, _) => false,
+ },
+ parent: Some(state.current_coordinate_system_id),
+ }
+ };
+ coordinate_system_id = CoordinateSystemId(coord_systems.len() as u32);
+ coord_systems.push(coord_system);
+ }
+
+ // Ensure that the current coordinate system ID is propagated to child
+ // nodes, even if we encounter a node that is not invertible. This ensures
+ // that the invariant in get_relative_transform is not violated.
+ self.coordinate_system_id = coordinate_system_id;
+ self.viewport_transform = cs_scale_offset;
+ self.content_transform = cs_scale_offset;
+ }
+ _ => {
+ // We calculate this here to avoid a double-borrow later.
+ let sticky_offset = self.calculate_sticky_offset(
+ &state.nearest_scrolling_ancestor_offset,
+ &state.nearest_scrolling_ancestor_viewport,
+ );
+
+ // The transformation for the bounds of our viewport is the parent reference frame
+ // transform, plus any accumulated scroll offset from our parents, plus any offset
+ // provided by our own sticky positioning.
+ let accumulated_offset = state.parent_accumulated_scroll_offset + sticky_offset;
+ self.viewport_transform = state.coordinate_system_relative_scale_offset
+ .offset(snap_offset(accumulated_offset, state.coordinate_system_relative_scale_offset.scale).to_untyped());
+
+ // The transformation for any content inside of us is the viewport transformation, plus
+ // whatever scrolling offset we supply as well.
+ let added_offset = accumulated_offset + self.scroll_offset();
+ self.content_transform = state.coordinate_system_relative_scale_offset
+ .offset(snap_offset(added_offset, state.coordinate_system_relative_scale_offset.scale).to_untyped());
+
+ if let SpatialNodeType::StickyFrame(ref mut info) = self.node_type {
+ info.current_offset = sticky_offset;
+ }
+
+ self.coordinate_system_id = state.current_coordinate_system_id;
+ }
+ }
+
+ //TODO: remove the field entirely?
+ self.transform_kind = if self.coordinate_system_id.0 == 0 {
+ TransformedRectKind::AxisAligned
+ } else {
+ TransformedRectKind::Complex
+ };
+ }
+
+ fn calculate_sticky_offset(
+ &self,
+ viewport_scroll_offset: &LayoutVector2D,
+ viewport_rect: &LayoutRect,
+ ) -> LayoutVector2D {
+ let info = match self.node_type {
+ SpatialNodeType::StickyFrame(ref info) => info,
+ _ => return LayoutVector2D::zero(),
+ };
+
+ if info.margins.top.is_none() && info.margins.bottom.is_none() &&
+ info.margins.left.is_none() && info.margins.right.is_none() {
+ return LayoutVector2D::zero();
+ }
+
+ // The viewport and margins of the item establishes the maximum amount that it can
+ // be offset in order to keep it on screen. Since we care about the relationship
+ // between the scrolled content and unscrolled viewport we adjust the viewport's
+ // position by the scroll offset in order to work with their relative positions on the
+ // page.
+ let mut sticky_rect = info.frame_rect.translate(*viewport_scroll_offset);
+
+ let mut sticky_offset = LayoutVector2D::zero();
+ if let Some(margin) = info.margins.top {
+ let top_viewport_edge = viewport_rect.min.y + margin;
+ if sticky_rect.min.y < top_viewport_edge {
+ // If the sticky rect is positioned above the top edge of the viewport (plus margin)
+ // we move it down so that it is fully inside the viewport.
+ sticky_offset.y = top_viewport_edge - sticky_rect.min.y;
+ } else if info.previously_applied_offset.y > 0.0 &&
+ sticky_rect.min.y > top_viewport_edge {
+ // However, if the sticky rect is positioned *below* the top edge of the viewport
+ // and there is already some offset applied to the sticky rect's position, then
+ // we need to move it up so that it remains at the correct position. This
+ // makes sticky_offset.y negative and effectively reduces the amount of the
+ // offset that was already applied. We limit the reduction so that it can, at most,
+ // cancel out the already-applied offset, but should never end up adjusting the
+ // position the other way.
+ sticky_offset.y = top_viewport_edge - sticky_rect.min.y;
+ sticky_offset.y = sticky_offset.y.max(-info.previously_applied_offset.y);
+ }
+ }
+
+ // If we don't have a sticky-top offset (sticky_offset.y + info.previously_applied_offset.y
+ // == 0), or if we have a previously-applied bottom offset (previously_applied_offset.y < 0)
+ // then we check for handling the bottom margin case. Note that the "don't have a sticky-top
+ // offset" case includes the case where we *had* a sticky-top offset but we reduced it to
+ // zero in the above block.
+ if sticky_offset.y + info.previously_applied_offset.y <= 0.0 {
+ if let Some(margin) = info.margins.bottom {
+ // If sticky_offset.y is nonzero that means we must have set it
+ // in the sticky-top handling code above, so this item must have
+ // both top and bottom sticky margins. We adjust the item's rect
+ // by the top-sticky offset, and then combine any offset from
+ // the bottom-sticky calculation into sticky_offset below.
+ sticky_rect.min.y += sticky_offset.y;
+ sticky_rect.max.y += sticky_offset.y;
+
+ // Same as the above case, but inverted for bottom-sticky items. Here
+ // we adjust items upwards, resulting in a negative sticky_offset.y,
+ // or reduce the already-present upward adjustment, resulting in a positive
+ // sticky_offset.y.
+ let bottom_viewport_edge = viewport_rect.max.y - margin;
+ if sticky_rect.max.y > bottom_viewport_edge {
+ sticky_offset.y += bottom_viewport_edge - sticky_rect.max.y;
+ } else if info.previously_applied_offset.y < 0.0 &&
+ sticky_rect.max.y < bottom_viewport_edge {
+ sticky_offset.y += bottom_viewport_edge - sticky_rect.max.y;
+ sticky_offset.y = sticky_offset.y.min(-info.previously_applied_offset.y);
+ }
+ }
+ }
+
+ // Same as above, but for the x-axis.
+ if let Some(margin) = info.margins.left {
+ let left_viewport_edge = viewport_rect.min.x + margin;
+ if sticky_rect.min.x < left_viewport_edge {
+ sticky_offset.x = left_viewport_edge - sticky_rect.min.x;
+ } else if info.previously_applied_offset.x > 0.0 &&
+ sticky_rect.min.x > left_viewport_edge {
+ sticky_offset.x = left_viewport_edge - sticky_rect.min.x;
+ sticky_offset.x = sticky_offset.x.max(-info.previously_applied_offset.x);
+ }
+ }
+
+ if sticky_offset.x + info.previously_applied_offset.x <= 0.0 {
+ if let Some(margin) = info.margins.right {
+ sticky_rect.min.x += sticky_offset.x;
+ sticky_rect.max.x += sticky_offset.x;
+ let right_viewport_edge = viewport_rect.max.x - margin;
+ if sticky_rect.max.x > right_viewport_edge {
+ sticky_offset.x += right_viewport_edge - sticky_rect.max.x;
+ } else if info.previously_applied_offset.x < 0.0 &&
+ sticky_rect.max.x < right_viewport_edge {
+ sticky_offset.x += right_viewport_edge - sticky_rect.max.x;
+ sticky_offset.x = sticky_offset.x.min(-info.previously_applied_offset.x);
+ }
+ }
+ }
+
+ // The total "sticky offset" (which is the sum that was already applied by
+ // the calling code, stored in info.previously_applied_offset, and the extra amount we
+ // computed as a result of scrolling, stored in sticky_offset) needs to be
+ // clamped to the provided bounds.
+ let clamp_adjusted = |value: f32, adjust: f32, bounds: &StickyOffsetBounds| {
+ (value + adjust).max(bounds.min).min(bounds.max) - adjust
+ };
+ sticky_offset.y = clamp_adjusted(sticky_offset.y,
+ info.previously_applied_offset.y,
+ &info.vertical_offset_bounds);
+ sticky_offset.x = clamp_adjusted(sticky_offset.x,
+ info.previously_applied_offset.x,
+ &info.horizontal_offset_bounds);
+
+ sticky_offset
+ }
+
+ pub fn prepare_state_for_children(&self, state: &mut TransformUpdateState) {
+ state.current_coordinate_system_id = self.coordinate_system_id;
+ state.is_ancestor_or_self_zooming = self.is_async_zooming;
+ state.invertible &= self.invertible;
+
+ // The transformation we are passing is the transformation of the parent
+ // reference frame and the offset is the accumulated offset of all the nodes
+ // between us and the parent reference frame. If we are a reference frame,
+ // we need to reset both these values.
+ match self.node_type {
+ SpatialNodeType::StickyFrame(ref info) => {
+ // We don't translate the combined rect by the sticky offset, because sticky
+ // offsets actually adjust the node position itself, whereas scroll offsets
+ // only apply to contents inside the node.
+ state.parent_accumulated_scroll_offset += info.current_offset;
+ // We want nested sticky items to take into account the shift
+ // we applied as well.
+ state.nearest_scrolling_ancestor_offset += info.current_offset;
+ state.preserves_3d = false;
+ state.external_id = None;
+ state.scroll_offset = info.current_offset;
+ }
+ SpatialNodeType::ScrollFrame(ref scrolling) => {
+ state.parent_accumulated_scroll_offset += scrolling.offset();
+ state.nearest_scrolling_ancestor_offset = scrolling.offset();
+ state.nearest_scrolling_ancestor_viewport = scrolling.viewport_rect;
+ state.preserves_3d = false;
+ state.external_id = Some(scrolling.external_id);
+ state.scroll_offset = scrolling.offset() + scrolling.external_scroll_offset;
+ }
+ SpatialNodeType::ReferenceFrame(ref info) => {
+ state.external_id = None;
+ state.scroll_offset = LayoutVector2D::zero();
+ state.preserves_3d = info.transform_style == TransformStyle::Preserve3D;
+ state.parent_accumulated_scroll_offset = LayoutVector2D::zero();
+ state.coordinate_system_relative_scale_offset = self.content_transform;
+ let translation = -info.origin_in_parent_reference_frame;
+ state.nearest_scrolling_ancestor_viewport =
+ state.nearest_scrolling_ancestor_viewport
+ .translate(translation);
+ }
+ }
+ }
+
+ pub fn scroll_offset(&self) -> LayoutVector2D {
+ match self.node_type {
+ SpatialNodeType::ScrollFrame(ref scrolling) => scrolling.offset(),
+ _ => LayoutVector2D::zero(),
+ }
+ }
+
+ pub fn matches_external_id(&self, external_id: ExternalScrollId) -> bool {
+ match self.node_type {
+ SpatialNodeType::ScrollFrame(ref info) if info.external_id == external_id => true,
+ _ => false,
+ }
+ }
+
+ /// Returns true for ReferenceFrames whose source_transform is
+ /// bound to the property binding id.
+ pub fn is_transform_bound_to_property(&self, id: PropertyBindingId) -> bool {
+ if let SpatialNodeType::ReferenceFrame(ref info) = self.node_type {
+ if let PropertyBinding::Binding(key, _) = info.source_transform {
+ id == key.id
+ } else {
+ false
+ }
+ } else {
+ false
+ }
+ }
+}
+
+/// Defines whether we have an implicit scroll frame for a pipeline root,
+/// or an explicitly defined scroll frame from the display list.
+#[derive(Copy, Clone, Debug, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum ScrollFrameKind {
+ PipelineRoot {
+ is_root_pipeline: bool,
+ },
+ Explicit,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ScrollFrameInfo {
+ /// The rectangle of the viewport of this scroll frame. This is important for
+ /// positioning of items inside child StickyFrames.
+ pub viewport_rect: LayoutRect,
+
+ /// Amount that this ScrollFrame can scroll in both directions.
+ pub scrollable_size: LayoutSize,
+
+ /// An external id to identify this scroll frame to API clients. This
+ /// allows setting scroll positions via the API without relying on ClipsIds
+ /// which may change between frames.
+ pub external_id: ExternalScrollId,
+
+ /// Stores whether this is a scroll frame added implicitly by WR when adding
+ /// a pipeline (either the root or an iframe). We need to exclude these
+ /// when searching for scroll roots we care about for picture caching.
+ /// TODO(gw): I think we can actually completely remove the implicit
+ /// scroll frame being added by WR, and rely on the embedder
+ /// to define scroll frames. However, that involves API changes
+ /// so we will use this as a temporary hack!
+ pub frame_kind: ScrollFrameKind,
+
+ /// Amount that visual components attached to this scroll node have been
+ /// pre-scrolled in their local coordinates.
+ pub external_scroll_offset: LayoutVector2D,
+
+ /// A set of a pair of negated scroll offset and scroll generation of this
+ /// scroll node. The negated scroll offset is including the pre-scrolled
+ /// amount. If, for example, a scroll node was pre-scrolled to y=10 (10
+ /// pixels down from the initial unscrolled position), then
+ /// `external_scroll_offset` would be (0,10), and this `offset` field would
+ /// be (0,-10). If WebRender is then asked to change the scroll position by
+ /// an additional 10 pixels (without changing the pre-scroll amount in the
+ /// display list), `external_scroll_offset` would remain at (0,10) and
+ /// `offset` would change to (0,-20).
+ pub offsets: Vec<SampledScrollOffset>,
+
+ /// The generation of the external_scroll_offset.
+ /// This is used to pick up the most appropriate scroll offset sampled
+ /// off the main thread.
+ pub offset_generation: APZScrollGeneration,
+
+ /// Whether the document containing this scroll frame has any scroll-linked
+ /// effect or not.
+ pub has_scroll_linked_effect: HasScrollLinkedEffect,
+}
+
+/// Manages scrolling offset.
+impl ScrollFrameInfo {
+ pub fn new(
+ viewport_rect: LayoutRect,
+ scrollable_size: LayoutSize,
+ external_id: ExternalScrollId,
+ frame_kind: ScrollFrameKind,
+ external_scroll_offset: LayoutVector2D,
+ offset_generation: APZScrollGeneration,
+ has_scroll_linked_effect: HasScrollLinkedEffect,
+ ) -> ScrollFrameInfo {
+ ScrollFrameInfo {
+ viewport_rect,
+ scrollable_size,
+ external_id,
+ frame_kind,
+ external_scroll_offset,
+ offsets: vec![SampledScrollOffset{
+ // If this scroll frame is a newly created one, using
+ // `external_scroll_offset` and `offset_generation` is correct.
+ // If this scroll frame is a result of updating an existing
+ // scroll frame and if there have already been sampled async
+ // scroll offsets by APZ, then these offsets will be replaced in
+ // SpatialTree::set_scroll_offsets via a
+ // RenderBackend::update_document call.
+ offset: -external_scroll_offset,
+ generation: offset_generation.clone(),
+ }],
+ offset_generation,
+ has_scroll_linked_effect,
+ }
+ }
+
+ pub fn offset(&self) -> LayoutVector2D {
+ debug_assert!(self.offsets.len() > 0, "There should be at least one sampled offset!");
+
+ if self.has_scroll_linked_effect == HasScrollLinkedEffect::No {
+ // If there's no scroll-linked effect, use the one-frame delay offset.
+ return self.offsets.first().map_or(LayoutVector2D::zero(), |sampled| sampled.offset);
+ }
+
+ match self.offsets.iter().find(|sampled| sampled.generation == self.offset_generation) {
+ // If we found an offset having the same generation, use it.
+ Some(sampled) => sampled.offset,
+ // If we don't have any offset having the same generation, i.e.
+ // the generation of this scroll frame is behind sampled offsets,
+ // use the first queued sampled offset.
+ _ => self.offsets.first().map_or(LayoutVector2D::zero(), |sampled| sampled.offset),
+ }
+ }
+}
+
+/// Contains information about reference frames.
+#[derive(Copy, Clone, Debug, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ReferenceFrameInfo {
+ /// The source transform and perspective matrices provided by the stacking context
+ /// that forms this reference frame. We maintain the property binding information
+ /// here so that we can resolve the animated transform and update the tree each
+ /// frame.
+ pub source_transform: PropertyBinding<LayoutTransform>,
+ pub transform_style: TransformStyle,
+ pub kind: ReferenceFrameKind,
+
+ /// The original, not including the transform and relative to the parent reference frame,
+ /// origin of this reference frame. This is already rolled into the `transform' property, but
+ /// we also store it here to properly transform the viewport for sticky positioning.
+ pub origin_in_parent_reference_frame: LayoutVector2D,
+
+ /// True if this is the root reference frame for a given pipeline. This is only used
+ /// by the hit-test code, perhaps we can change the interface to not require this.
+ pub is_pipeline_root: bool,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct StickyFrameInfo {
+ pub frame_rect: LayoutRect,
+ pub margins: SideOffsets2D<Option<f32>, LayoutPixel>,
+ pub vertical_offset_bounds: StickyOffsetBounds,
+ pub horizontal_offset_bounds: StickyOffsetBounds,
+ pub previously_applied_offset: LayoutVector2D,
+ pub current_offset: LayoutVector2D,
+}
+
+impl StickyFrameInfo {
+ pub fn new(
+ frame_rect: LayoutRect,
+ margins: SideOffsets2D<Option<f32>, LayoutPixel>,
+ vertical_offset_bounds: StickyOffsetBounds,
+ horizontal_offset_bounds: StickyOffsetBounds,
+ previously_applied_offset: LayoutVector2D
+ ) -> StickyFrameInfo {
+ StickyFrameInfo {
+ frame_rect,
+ margins,
+ vertical_offset_bounds,
+ horizontal_offset_bounds,
+ previously_applied_offset,
+ current_offset: LayoutVector2D::zero(),
+ }
+ }
+}
+
+#[test]
+fn test_cst_perspective_relative_scroll() {
+ // Verify that when computing the offset from a perspective transform
+ // to a relative scroll node that any external scroll offset is
+ // ignored. This is because external scroll offsets are not
+ // propagated across reference frame boundaries.
+
+ // It's not currently possible to verify this with a wrench reftest,
+ // since wrench doesn't understand external scroll ids. When wrench
+ // supports this, we could also verify with a reftest.
+
+ use crate::spatial_tree::{SceneSpatialTree, SpatialTree};
+ use euclid::Angle;
+
+ let mut cst = SceneSpatialTree::new();
+ let pipeline_id = PipelineId::dummy();
+ let ext_scroll_id = ExternalScrollId(1, pipeline_id);
+ let transform = LayoutTransform::rotation(0.0, 0.0, 1.0, Angle::degrees(45.0));
+ let pid = PipelineInstanceId::new(0);
+
+ let root = cst.add_reference_frame(
+ cst.root_reference_frame_index(),
+ TransformStyle::Flat,
+ PropertyBinding::Value(LayoutTransform::identity()),
+ ReferenceFrameKind::Transform {
+ is_2d_scale_translation: false,
+ should_snap: false,
+ paired_with_perspective: false,
+ },
+ LayoutVector2D::zero(),
+ pipeline_id,
+ SpatialNodeUid::external(SpatialTreeItemKey::new(0, 0), PipelineId::dummy(), pid),
+ );
+
+ let scroll_frame_1 = cst.add_scroll_frame(
+ root,
+ ext_scroll_id,
+ pipeline_id,
+ &LayoutRect::from_size(LayoutSize::new(100.0, 100.0)),
+ &LayoutSize::new(100.0, 500.0),
+ ScrollFrameKind::Explicit,
+ LayoutVector2D::zero(),
+ APZScrollGeneration::default(),
+ HasScrollLinkedEffect::No,
+ SpatialNodeUid::external(SpatialTreeItemKey::new(0, 1), PipelineId::dummy(), pid),
+ );
+
+ let scroll_frame_2 = cst.add_scroll_frame(
+ scroll_frame_1,
+ ExternalScrollId(2, pipeline_id),
+ pipeline_id,
+ &LayoutRect::from_size(LayoutSize::new(100.0, 100.0)),
+ &LayoutSize::new(100.0, 500.0),
+ ScrollFrameKind::Explicit,
+ LayoutVector2D::new(0.0, 50.0),
+ APZScrollGeneration::default(),
+ HasScrollLinkedEffect::No,
+ SpatialNodeUid::external(SpatialTreeItemKey::new(0, 3), PipelineId::dummy(), pid),
+ );
+
+ let ref_frame = cst.add_reference_frame(
+ scroll_frame_2,
+ TransformStyle::Preserve3D,
+ PropertyBinding::Value(transform),
+ ReferenceFrameKind::Perspective {
+ scrolling_relative_to: Some(ext_scroll_id),
+ },
+ LayoutVector2D::zero(),
+ pipeline_id,
+ SpatialNodeUid::external(SpatialTreeItemKey::new(0, 4), PipelineId::dummy(), pid),
+ );
+
+ let mut st = SpatialTree::new();
+ st.apply_updates(cst.end_frame_and_get_pending_updates());
+ st.update_tree(&SceneProperties::new());
+
+ let world_transform = st.get_world_transform(ref_frame).into_transform().cast_unit();
+ let ref_transform = transform.then_translate(LayoutVector3D::new(0.0, -50.0, 0.0));
+ assert!(world_transform.approx_eq(&ref_transform));
+}
+
diff --git a/gfx/wr/webrender/src/spatial_tree.rs b/gfx/wr/webrender/src/spatial_tree.rs
new file mode 100644
index 0000000000..bdb62c81a0
--- /dev/null
+++ b/gfx/wr/webrender/src/spatial_tree.rs
@@ -0,0 +1,1998 @@
+/* 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 api::{ExternalScrollId, PropertyBinding, ReferenceFrameKind, TransformStyle, PropertyBindingId};
+use api::{APZScrollGeneration, HasScrollLinkedEffect, PipelineId, SampledScrollOffset, SpatialTreeItemKey};
+use api::units::*;
+use euclid::Transform3D;
+use crate::gpu_types::TransformPalette;
+use crate::internal_types::{FastHashMap, FastHashSet, PipelineInstanceId};
+use crate::print_tree::{PrintableTree, PrintTree, PrintTreePrinter};
+use crate::scene::SceneProperties;
+use crate::spatial_node::{ReferenceFrameInfo, SpatialNode, SpatialNodeType, StickyFrameInfo, SpatialNodeDescriptor};
+use crate::spatial_node::{SpatialNodeUid, ScrollFrameKind, SceneSpatialNode, SpatialNodeInfo, SpatialNodeUidKind};
+use std::{ops, u32};
+use crate::util::{FastTransform, LayoutToWorldFastTransform, MatrixHelpers, ScaleOffset, scale_factors};
+use smallvec::SmallVec;
+use std::collections::hash_map::Entry;
+use crate::util::TransformedRectKind;
+use peek_poke::PeekPoke;
+
+
+/// An id that identifies coordinate systems in the SpatialTree. Each
+/// coordinate system has an id and those ids will be shared when the coordinates
+/// system are the same or are in the same axis-aligned space. This allows
+/// for optimizing mask generation.
+#[derive(Debug, Copy, Clone, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct CoordinateSystemId(pub u32);
+
+/// A node in the hierarchy of coordinate system
+/// transforms.
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct CoordinateSystem {
+ pub transform: LayoutTransform,
+ pub world_transform: LayoutToWorldTransform,
+ pub should_flatten: bool,
+ pub parent: Option<CoordinateSystemId>,
+}
+
+impl CoordinateSystem {
+ fn root() -> Self {
+ CoordinateSystem {
+ transform: LayoutTransform::identity(),
+ world_transform: LayoutToWorldTransform::identity(),
+ should_flatten: false,
+ parent: None,
+ }
+ }
+}
+
+#[derive(Debug, Copy, Clone, Eq, Hash, MallocSizeOf, PartialEq, PeekPoke, Default)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SpatialNodeIndex(pub u32);
+
+impl SpatialNodeIndex {
+ pub const INVALID: SpatialNodeIndex = SpatialNodeIndex(u32::MAX);
+
+ /// May be set on a cluster / picture during scene building if the spatial
+ /// node is not known at this time. It must be set to a valid value before
+ /// scene building is complete (by `finalize_picture`). In future, we could
+ /// make this type-safe with a wrapper type to ensure we know when a spatial
+ /// node index may have an unknown value.
+ pub const UNKNOWN: SpatialNodeIndex = SpatialNodeIndex(u32::MAX - 1);
+}
+
+// In some cases, the conversion from CSS pixels to device pixels can result in small
+// rounding errors when calculating the scrollable distance of a scroll frame. Apply
+// a small epsilon so that we don't detect these frames as "real" scroll frames.
+const MIN_SCROLLABLE_AMOUNT: f32 = 0.01;
+
+// The minimum size for a scroll frame for it to be considered for a scroll root.
+const MIN_SCROLL_ROOT_SIZE: f32 = 128.0;
+
+impl SpatialNodeIndex {
+ pub fn new(index: usize) -> Self {
+ debug_assert!(index < ::std::u32::MAX as usize);
+ SpatialNodeIndex(index as u32)
+ }
+}
+
+impl CoordinateSystemId {
+ pub fn root() -> Self {
+ CoordinateSystemId(0)
+ }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub enum VisibleFace {
+ Front,
+ Back,
+}
+
+impl Default for VisibleFace {
+ fn default() -> Self {
+ VisibleFace::Front
+ }
+}
+
+impl ops::Not for VisibleFace {
+ type Output = Self;
+ fn not(self) -> Self {
+ match self {
+ VisibleFace::Front => VisibleFace::Back,
+ VisibleFace::Back => VisibleFace::Front,
+ }
+ }
+}
+
+/// Allows functions and methods to retrieve common information about
+/// a spatial node, whether during scene or frame building
+pub trait SpatialNodeContainer {
+ /// Get the common information for a given spatial node
+ fn get_node_info(&self, index: SpatialNodeIndex) -> SpatialNodeInfo;
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+enum StoreElement<T> {
+ Empty,
+ Occupied(T),
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct Store<T> {
+ elements: Vec<StoreElement<T>>,
+ free_indices: Vec<usize>,
+}
+
+impl<T> Store<T> {
+ fn new() -> Self {
+ Store {
+ elements: Vec::new(),
+ free_indices: Vec::new(),
+ }
+ }
+
+ fn insert(&mut self, element: T) -> usize {
+ match self.free_indices.pop() {
+ Some(index) => {
+ match &mut self.elements[index] {
+ e @ StoreElement::Empty => *e = StoreElement::Occupied(element),
+ StoreElement::Occupied(..) => panic!("bug: slot already occupied"),
+ };
+ index
+ }
+ None => {
+ let index = self.elements.len();
+ self.elements.push(StoreElement::Occupied(element));
+ index
+ }
+ }
+ }
+
+ fn set(&mut self, index: usize, element: T) {
+ match &mut self.elements[index] {
+ StoreElement::Empty => panic!("bug: set on empty element!"),
+ StoreElement::Occupied(ref mut entry) => *entry = element,
+ }
+ }
+
+ fn free(&mut self, index: usize) -> T {
+ self.free_indices.push(index);
+
+ let value = std::mem::replace(&mut self.elements[index], StoreElement::Empty);
+
+ match value {
+ StoreElement::Occupied(value) => value,
+ StoreElement::Empty => panic!("bug: freeing an empty slot"),
+ }
+ }
+}
+
+impl<T> ops::Index<usize> for Store<T> {
+ type Output = T;
+ fn index(&self, index: usize) -> &Self::Output {
+ match self.elements[index] {
+ StoreElement::Occupied(ref e) => e,
+ StoreElement::Empty => panic!("bug: indexing an empty element!"),
+ }
+ }
+}
+
+impl<T> ops::IndexMut<usize> for Store<T> {
+ fn index_mut(&mut self, index: usize) -> &mut T {
+ match self.elements[index] {
+ StoreElement::Occupied(ref mut e) => e,
+ StoreElement::Empty => panic!("bug: indexing an empty element!"),
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct SpatialNodeEntry {
+ index: usize,
+ last_used: u64,
+}
+
+/// The representation of the spatial tree during scene building, which is
+/// mostly write-only, with a small number of queries for snapping,
+/// picture cache building
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SceneSpatialTree {
+ /// Nodes which determine the positions (offsets and transforms) for primitives
+ /// and clips.
+ spatial_nodes: Store<SceneSpatialNode>,
+
+ /// A set of the uids we've encountered for spatial nodes, used to assert that
+ /// we're not seeing duplicates. Likely to be removed once we rely on this feature.
+ spatial_node_map: FastHashMap<SpatialNodeUid, SpatialNodeEntry>,
+
+ root_reference_frame_index: SpatialNodeIndex,
+
+ frame_counter: u64,
+ updates: SpatialTreeUpdates,
+
+ /// A debug check that the caller never adds a spatial node with duplicate
+ /// uid, since that can cause badness if it occurs (e.g. a malformed spatial
+ /// tree and infinite loops in is_ancestor etc)
+ spatial_nodes_set: FastHashSet<SpatialNodeUid>,
+}
+
+impl SpatialNodeContainer for SceneSpatialTree {
+ fn get_node_info(&self, index: SpatialNodeIndex) -> SpatialNodeInfo {
+ let node = &self.spatial_nodes[index.0 as usize];
+
+ SpatialNodeInfo {
+ parent: node.parent,
+ node_type: &node.descriptor.node_type,
+ snapping_transform: node.snapping_transform,
+ }
+ }
+}
+
+impl SceneSpatialTree {
+ pub fn new() -> Self {
+ let mut tree = SceneSpatialTree {
+ spatial_nodes: Store::new(),
+ spatial_node_map: FastHashMap::default(),
+ root_reference_frame_index: SpatialNodeIndex(0),
+ frame_counter: 0,
+ updates: SpatialTreeUpdates::new(),
+ spatial_nodes_set: FastHashSet::default(),
+ };
+
+ let node = SceneSpatialNode::new_reference_frame(
+ None,
+ TransformStyle::Flat,
+ PropertyBinding::Value(LayoutTransform::identity()),
+ ReferenceFrameKind::Transform {
+ should_snap: true,
+ is_2d_scale_translation: true,
+ paired_with_perspective: false,
+ },
+ LayoutVector2D::zero(),
+ PipelineId::dummy(),
+ true,
+ true,
+ );
+
+ tree.add_spatial_node(node, SpatialNodeUid::root());
+
+ tree
+ }
+
+ pub fn is_root_coord_system(&self, index: SpatialNodeIndex) -> bool {
+ self.spatial_nodes[index.0 as usize].is_root_coord_system
+ }
+
+ /// Complete building this scene, return the updates to apply to the frame spatial tree
+ pub fn end_frame_and_get_pending_updates(&mut self) -> SpatialTreeUpdates {
+ self.updates.root_reference_frame_index = self.root_reference_frame_index;
+ self.spatial_nodes_set.clear();
+
+ let now = self.frame_counter;
+ let spatial_nodes = &mut self.spatial_nodes;
+ let updates = &mut self.updates;
+
+ self.spatial_node_map.get_mut(&SpatialNodeUid::root()).unwrap().last_used = now;
+
+ self.spatial_node_map.retain(|_, entry| {
+ if entry.last_used + 10 < now {
+ spatial_nodes.free(entry.index);
+ updates.updates.push(SpatialTreeUpdate::Remove {
+ index: entry.index,
+ });
+ return false;
+ }
+
+ true
+ });
+
+ let updates = std::mem::replace(&mut self.updates, SpatialTreeUpdates::new());
+
+ self.frame_counter += 1;
+
+ updates
+ }
+
+ /// Check if a given spatial node is an ancestor of another spatial node.
+ pub fn is_ancestor(
+ &self,
+ maybe_parent: SpatialNodeIndex,
+ maybe_child: SpatialNodeIndex,
+ ) -> bool {
+ // Early out if same node
+ if maybe_parent == maybe_child {
+ return false;
+ }
+
+ let mut current_node = maybe_child;
+
+ while current_node != self.root_reference_frame_index {
+ let node = self.get_node_info(current_node);
+ current_node = node.parent.expect("bug: no parent");
+
+ if current_node == maybe_parent {
+ return true;
+ }
+ }
+
+ false
+ }
+
+ /// Find the spatial node that is the scroll root for a given spatial node.
+ /// A scroll root is the first spatial node when found travelling up the
+ /// spatial node tree that is an explicit scroll frame.
+ pub fn find_scroll_root(
+ &self,
+ spatial_node_index: SpatialNodeIndex,
+ ) -> SpatialNodeIndex {
+ let mut real_scroll_root = self.root_reference_frame_index;
+ let mut outermost_scroll_root = self.root_reference_frame_index;
+ let mut node_index = spatial_node_index;
+
+ while node_index != self.root_reference_frame_index {
+ let node = self.get_node_info(node_index);
+ match node.node_type {
+ SpatialNodeType::ReferenceFrame(ref info) => {
+ match info.kind {
+ ReferenceFrameKind::Transform { is_2d_scale_translation: true, .. } => {
+ // We can handle scroll nodes that pass through a 2d scale/translation node
+ }
+ ReferenceFrameKind::Transform { is_2d_scale_translation: false, .. } |
+ ReferenceFrameKind::Perspective { .. } => {
+ // When a reference frame is encountered, forget any scroll roots
+ // we have encountered, as they may end up with a non-axis-aligned transform.
+ real_scroll_root = self.root_reference_frame_index;
+ outermost_scroll_root = self.root_reference_frame_index;
+ }
+ }
+ }
+ SpatialNodeType::StickyFrame(..) => {}
+ SpatialNodeType::ScrollFrame(ref info) => {
+ match info.frame_kind {
+ ScrollFrameKind::PipelineRoot { is_root_pipeline } => {
+ // Once we encounter a pipeline root, there is no need to look further
+ if is_root_pipeline {
+ break;
+ }
+ }
+ ScrollFrameKind::Explicit => {
+ // Store the closest scroll root we find to the root, for use
+ // later on, even if it's not actually scrollable.
+ outermost_scroll_root = node_index;
+
+ // If the scroll root has no scrollable area, we don't want to
+ // consider it. This helps pages that have a nested scroll root
+ // within a redundant scroll root to avoid selecting the wrong
+ // reference spatial node for a picture cache.
+ if info.scrollable_size.width > MIN_SCROLLABLE_AMOUNT ||
+ info.scrollable_size.height > MIN_SCROLLABLE_AMOUNT {
+ // Since we are skipping redundant scroll roots, we may end up
+ // selecting inner scroll roots that are very small. There is
+ // no performance benefit to creating a slice for these roots,
+ // as they are cheap to rasterize. The size comparison is in
+ // local-space, but makes for a reasonable estimate. The value
+ // is arbitrary, but is generally small enough to ignore things
+ // like scroll roots around text input elements.
+ if info.viewport_rect.width() > MIN_SCROLL_ROOT_SIZE &&
+ info.viewport_rect.height() > MIN_SCROLL_ROOT_SIZE {
+ // If we've found a root that is scrollable, and a reasonable
+ // size, select that as the current root for this node
+ real_scroll_root = node_index;
+ }
+ }
+ }
+ }
+ }
+ }
+ node_index = node.parent.expect("unable to find parent node");
+ }
+
+ // If we didn't find any real (scrollable) frames, then return the outermost
+ // redundant scroll frame. This is important so that we can correctly find
+ // the clips defined on the content which should be handled when drawing the
+ // picture cache tiles (by definition these clips are ancestors of the
+ // scroll root selected for the picture cache).
+ if real_scroll_root == self.root_reference_frame_index {
+ outermost_scroll_root
+ } else {
+ real_scroll_root
+ }
+ }
+
+ /// The root reference frame, which is the true root of the SpatialTree.
+ pub fn root_reference_frame_index(&self) -> SpatialNodeIndex {
+ self.root_reference_frame_index
+ }
+
+ fn add_spatial_node(
+ &mut self,
+ mut node: SceneSpatialNode,
+ uid: SpatialNodeUid,
+ ) -> SpatialNodeIndex {
+ let parent_snapping_transform = match node.parent {
+ Some(parent_index) => {
+ self.get_node_info(parent_index).snapping_transform
+ }
+ None => {
+ Some(ScaleOffset::identity())
+ }
+ };
+
+ node.snapping_transform = calculate_snapping_transform(
+ parent_snapping_transform,
+ &node.descriptor.node_type,
+ );
+
+ // Ensure a node with the same uid hasn't been added during this scene build
+ assert!(self.spatial_nodes_set.insert(uid), "duplicate key {:?}", uid);
+
+ let index = match self.spatial_node_map.entry(uid) {
+ Entry::Occupied(mut e) => {
+ let e = e.get_mut();
+ e.last_used = self.frame_counter;
+
+ let existing_node = &self.spatial_nodes[e.index];
+
+ if *existing_node != node {
+ self.updates.updates.push(SpatialTreeUpdate::Update {
+ index: e.index,
+ parent: node.parent,
+ descriptor: node.descriptor.clone(),
+ });
+ self.spatial_nodes.set(e.index, node);
+ }
+
+ e.index
+ }
+ Entry::Vacant(e) => {
+ let descriptor = node.descriptor.clone();
+ let parent = node.parent;
+
+ let index = self.spatial_nodes.insert(node);
+
+ e.insert(SpatialNodeEntry {
+ index,
+ last_used: self.frame_counter,
+ });
+
+ self.updates.updates.push(SpatialTreeUpdate::Insert {
+ index,
+ descriptor,
+ parent,
+ });
+
+ index
+ }
+ };
+
+ SpatialNodeIndex(index as u32)
+ }
+
+ pub fn add_reference_frame(
+ &mut self,
+ parent_index: SpatialNodeIndex,
+ transform_style: TransformStyle,
+ source_transform: PropertyBinding<LayoutTransform>,
+ kind: ReferenceFrameKind,
+ origin_in_parent_reference_frame: LayoutVector2D,
+ pipeline_id: PipelineId,
+ uid: SpatialNodeUid,
+ ) -> SpatialNodeIndex {
+ // Determine if this reference frame creates a new static coordinate system
+ let new_static_coord_system = match kind {
+ ReferenceFrameKind::Transform { is_2d_scale_translation: true, .. } => {
+ // Client has guaranteed this transform will only be axis-aligned
+ false
+ }
+ ReferenceFrameKind::Transform { is_2d_scale_translation: false, .. } | ReferenceFrameKind::Perspective { .. } => {
+ // Even if client hasn't promised it's an axis-aligned transform, we can still
+ // check this so long as the transform isn't animated (and thus could change to
+ // anything by APZ during frame building)
+ match source_transform {
+ PropertyBinding::Value(m) => {
+ !m.is_2d_scale_translation()
+ }
+ PropertyBinding::Binding(..) => {
+ // Animated, so assume it may introduce a complex transform
+ true
+ }
+ }
+ }
+ };
+
+ let is_root_coord_system = !new_static_coord_system &&
+ self.spatial_nodes[parent_index.0 as usize].is_root_coord_system;
+ let is_pipeline_root = match uid.kind {
+ SpatialNodeUidKind::InternalReferenceFrame { .. } => true,
+ _ => false,
+ };
+
+ let node = SceneSpatialNode::new_reference_frame(
+ Some(parent_index),
+ transform_style,
+ source_transform,
+ kind,
+ origin_in_parent_reference_frame,
+ pipeline_id,
+ is_root_coord_system,
+ is_pipeline_root,
+ );
+ self.add_spatial_node(node, uid)
+ }
+
+ pub fn add_scroll_frame(
+ &mut self,
+ parent_index: SpatialNodeIndex,
+ external_id: ExternalScrollId,
+ pipeline_id: PipelineId,
+ frame_rect: &LayoutRect,
+ content_size: &LayoutSize,
+ frame_kind: ScrollFrameKind,
+ external_scroll_offset: LayoutVector2D,
+ scroll_offset_generation: APZScrollGeneration,
+ has_scroll_linked_effect: HasScrollLinkedEffect,
+ uid: SpatialNodeUid,
+ ) -> SpatialNodeIndex {
+ // Scroll frames are only 2d translations - they can't introduce a new static coord system
+ let is_root_coord_system = self.spatial_nodes[parent_index.0 as usize].is_root_coord_system;
+
+ let node = SceneSpatialNode::new_scroll_frame(
+ pipeline_id,
+ parent_index,
+ external_id,
+ frame_rect,
+ content_size,
+ frame_kind,
+ external_scroll_offset,
+ scroll_offset_generation,
+ has_scroll_linked_effect,
+ is_root_coord_system,
+ );
+ self.add_spatial_node(node, uid)
+ }
+
+ pub fn add_sticky_frame(
+ &mut self,
+ parent_index: SpatialNodeIndex,
+ sticky_frame_info: StickyFrameInfo,
+ pipeline_id: PipelineId,
+ key: SpatialTreeItemKey,
+ instance_id: PipelineInstanceId,
+ ) -> SpatialNodeIndex {
+ // Sticky frames are only 2d translations - they can't introduce a new static coord system
+ let is_root_coord_system = self.spatial_nodes[parent_index.0 as usize].is_root_coord_system;
+ let uid = SpatialNodeUid::external(key, pipeline_id, instance_id);
+
+ let node = SceneSpatialNode::new_sticky_frame(
+ parent_index,
+ sticky_frame_info,
+ pipeline_id,
+ is_root_coord_system,
+ );
+ self.add_spatial_node(node, uid)
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum SpatialTreeUpdate {
+ Insert {
+ index: usize,
+ parent: Option<SpatialNodeIndex>,
+ descriptor: SpatialNodeDescriptor,
+ },
+ Update {
+ index: usize,
+ parent: Option<SpatialNodeIndex>,
+ descriptor: SpatialNodeDescriptor,
+ },
+ Remove {
+ index: usize,
+ },
+}
+
+/// The delta updates to apply after building a new scene to the retained frame building
+/// tree.
+// TODO(gw): During the initial scaffolding work, this is the exact same as previous
+// behavior - that is, a complete list of new spatial nodes. In future, this
+// will instead be a list of deltas to apply to the frame spatial tree.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SpatialTreeUpdates {
+ root_reference_frame_index: SpatialNodeIndex,
+ updates: Vec<SpatialTreeUpdate>,
+}
+
+impl SpatialTreeUpdates {
+ fn new() -> Self {
+ SpatialTreeUpdates {
+ root_reference_frame_index: SpatialNodeIndex::INVALID,
+ updates: Vec::new(),
+ }
+ }
+}
+
+/// Represents the spatial tree during frame building, which is mostly
+/// read-only, apart from the tree update at the start of the frame
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SpatialTree {
+ /// Nodes which determine the positions (offsets and transforms) for primitives
+ /// and clips.
+ spatial_nodes: Vec<SpatialNode>,
+
+ /// A list of transforms that establish new coordinate systems.
+ /// Spatial nodes only establish a new coordinate system when
+ /// they have a transform that is not a simple 2d translation.
+ coord_systems: Vec<CoordinateSystem>,
+
+ root_reference_frame_index: SpatialNodeIndex,
+
+ /// Stack of current state for each parent node while traversing and updating tree
+ update_state_stack: Vec<TransformUpdateState>,
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct TransformUpdateState {
+ pub parent_reference_frame_transform: LayoutToWorldFastTransform,
+ pub parent_accumulated_scroll_offset: LayoutVector2D,
+ pub nearest_scrolling_ancestor_offset: LayoutVector2D,
+ pub nearest_scrolling_ancestor_viewport: LayoutRect,
+
+ /// An id for keeping track of the axis-aligned space of this node. This is used in
+ /// order to to track what kinds of clip optimizations can be done for a particular
+ /// display list item, since optimizations can usually only be done among
+ /// coordinate systems which are relatively axis aligned.
+ pub current_coordinate_system_id: CoordinateSystemId,
+
+ /// Scale and offset from the coordinate system that started this compatible coordinate system.
+ pub coordinate_system_relative_scale_offset: ScaleOffset,
+
+ /// True if this node is transformed by an invertible transform. If not, display items
+ /// transformed by this node will not be displayed and display items not transformed by this
+ /// node will not be clipped by clips that are transformed by this node.
+ pub invertible: bool,
+
+ /// True if this node is a part of Preserve3D hierarchy.
+ pub preserves_3d: bool,
+
+ /// True if the any parent nodes are currently zooming
+ pub is_ancestor_or_self_zooming: bool,
+
+ /// Set to true if this state represents a scroll node with external id
+ pub external_id: Option<ExternalScrollId>,
+
+ /// The node scroll offset if this state is a scroll/sticky node. Zero if a reference frame.
+ pub scroll_offset: LayoutVector2D,
+}
+
+/// Transformation between two nodes in the spatial tree that can sometimes be
+/// encoded more efficiently than with a full matrix.
+#[derive(Debug, Clone)]
+pub enum CoordinateSpaceMapping<Src, Dst> {
+ Local,
+ ScaleOffset(ScaleOffset),
+ Transform(Transform3D<f32, Src, Dst>),
+}
+
+impl<Src, Dst> CoordinateSpaceMapping<Src, Dst> {
+ pub fn into_transform(self) -> Transform3D<f32, Src, Dst> {
+ match self {
+ CoordinateSpaceMapping::Local => Transform3D::identity(),
+ CoordinateSpaceMapping::ScaleOffset(scale_offset) => scale_offset.to_transform(),
+ CoordinateSpaceMapping::Transform(transform) => transform,
+ }
+ }
+
+ pub fn into_fast_transform(self) -> FastTransform<Src, Dst> {
+ match self {
+ CoordinateSpaceMapping::Local => FastTransform::identity(),
+ CoordinateSpaceMapping::ScaleOffset(scale_offset) => FastTransform::with_scale_offset(scale_offset),
+ CoordinateSpaceMapping::Transform(transform) => FastTransform::with_transform(transform),
+ }
+ }
+
+ pub fn is_perspective(&self) -> bool {
+ match *self {
+ CoordinateSpaceMapping::Local |
+ CoordinateSpaceMapping::ScaleOffset(_) => false,
+ CoordinateSpaceMapping::Transform(ref transform) => transform.has_perspective_component(),
+ }
+ }
+
+ pub fn is_2d_axis_aligned(&self) -> bool {
+ match *self {
+ CoordinateSpaceMapping::Local |
+ CoordinateSpaceMapping::ScaleOffset(_) => true,
+ CoordinateSpaceMapping::Transform(ref transform) => transform.preserves_2d_axis_alignment(),
+ }
+ }
+
+ pub fn scale_factors(&self) -> (f32, f32) {
+ match *self {
+ CoordinateSpaceMapping::Local => (1.0, 1.0),
+ CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => (scale_offset.scale.x.abs(), scale_offset.scale.y.abs()),
+ CoordinateSpaceMapping::Transform(ref transform) => scale_factors(transform),
+ }
+ }
+
+ pub fn inverse(&self) -> Option<CoordinateSpaceMapping<Dst, Src>> {
+ match *self {
+ CoordinateSpaceMapping::Local => Some(CoordinateSpaceMapping::Local),
+ CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
+ Some(CoordinateSpaceMapping::ScaleOffset(scale_offset.inverse()))
+ }
+ CoordinateSpaceMapping::Transform(ref transform) => {
+ transform.inverse().map(CoordinateSpaceMapping::Transform)
+ }
+ }
+ }
+}
+
+enum TransformScroll {
+ Scrolled,
+ Unscrolled,
+}
+
+impl SpatialNodeContainer for SpatialTree {
+ fn get_node_info(&self, index: SpatialNodeIndex) -> SpatialNodeInfo {
+ let node = self.get_spatial_node(index);
+
+ SpatialNodeInfo {
+ parent: node.parent,
+ node_type: &node.node_type,
+ snapping_transform: node.snapping_transform,
+ }
+ }
+}
+
+impl SpatialTree {
+ pub fn new() -> Self {
+ SpatialTree {
+ spatial_nodes: Vec::new(),
+ coord_systems: Vec::new(),
+ root_reference_frame_index: SpatialNodeIndex::INVALID,
+ update_state_stack: Vec::new(),
+ }
+ }
+
+ fn visit_node_impl_mut<F>(
+ &mut self,
+ index: SpatialNodeIndex,
+ f: &mut F,
+ ) where F: FnMut(SpatialNodeIndex, &mut SpatialNode) {
+ let mut child_indices: SmallVec<[SpatialNodeIndex; 8]> = SmallVec::new();
+
+ let node = self.get_spatial_node_mut(index);
+ f(index, node);
+ child_indices.extend_from_slice(&node.children);
+
+ for child_index in child_indices {
+ self.visit_node_impl_mut(child_index, f);
+ }
+ }
+
+ fn visit_node_impl<F>(
+ &self,
+ index: SpatialNodeIndex,
+ f: &mut F,
+ ) where F: FnMut(SpatialNodeIndex, &SpatialNode) {
+ let node = self.get_spatial_node(index);
+
+ f(index, node);
+
+ for child_index in &node.children {
+ self.visit_node_impl(*child_index, f);
+ }
+ }
+
+ /// Visit all nodes from the root of the tree, invoking a closure on each one
+ pub fn visit_nodes<F>(&self, mut f: F) where F: FnMut(SpatialNodeIndex, &SpatialNode) {
+ if self.root_reference_frame_index == SpatialNodeIndex::INVALID {
+ return;
+ }
+
+ self.visit_node_impl(self.root_reference_frame_index, &mut f);
+ }
+
+ /// Visit all nodes from the root of the tree, invoking a closure on each one
+ pub fn visit_nodes_mut<F>(&mut self, mut f: F) where F: FnMut(SpatialNodeIndex, &mut SpatialNode) {
+ if self.root_reference_frame_index == SpatialNodeIndex::INVALID {
+ return;
+ }
+
+ self.visit_node_impl_mut(self.root_reference_frame_index, &mut f);
+ }
+
+ /// Apply updates from a new scene to the frame spatial tree
+ pub fn apply_updates(
+ &mut self,
+ updates: SpatialTreeUpdates,
+ ) {
+ self.root_reference_frame_index = updates.root_reference_frame_index;
+
+ for update in updates.updates {
+ match update {
+ SpatialTreeUpdate::Insert { index, parent, descriptor } => {
+ if let Some(parent) = parent {
+ self.get_spatial_node_mut(parent).add_child(SpatialNodeIndex(index as u32));
+ }
+
+ let node = SpatialNode {
+ viewport_transform: ScaleOffset::identity(),
+ content_transform: ScaleOffset::identity(),
+ snapping_transform: None,
+ coordinate_system_id: CoordinateSystemId(0),
+ transform_kind: TransformedRectKind::AxisAligned,
+ parent,
+ children: Vec::new(),
+ pipeline_id: descriptor.pipeline_id,
+ node_type: descriptor.node_type,
+ invertible: true,
+ is_async_zooming: false,
+ is_ancestor_or_self_zooming: false,
+ };
+
+ assert!(index <= self.spatial_nodes.len());
+ if index < self.spatial_nodes.len() {
+ self.spatial_nodes[index] = node;
+ } else {
+ self.spatial_nodes.push(node);
+ }
+ }
+ SpatialTreeUpdate::Update { index, descriptor, parent } => {
+ let current_parent = self.spatial_nodes[index].parent;
+
+ if current_parent != parent {
+ if let Some(current_parent) = current_parent {
+ let i = self.spatial_nodes[current_parent.0 as usize]
+ .children
+ .iter()
+ .position(|e| e.0 as usize == index)
+ .expect("bug: not found!");
+ self.spatial_nodes[current_parent.0 as usize].children.remove(i);
+ }
+
+ let new_parent = parent.expect("todo: is this valid?");
+ self.spatial_nodes[new_parent.0 as usize].add_child(SpatialNodeIndex(index as u32));
+ }
+
+ let node = &mut self.spatial_nodes[index];
+
+ node.node_type = descriptor.node_type;
+ node.pipeline_id = descriptor.pipeline_id;
+ node.parent = parent;
+ }
+ SpatialTreeUpdate::Remove { index, .. } => {
+ let node = &mut self.spatial_nodes[index];
+
+ // Set the pipeline id to be invalid, so that even though this array
+ // entry still exists we can easily see it's invalid when debugging.
+ node.pipeline_id = PipelineId::dummy();
+
+ if let Some(parent) = node.parent {
+ let i = self.spatial_nodes[parent.0 as usize]
+ .children
+ .iter()
+ .position(|e| e.0 as usize == index)
+ .expect("bug: not found!");
+ self.spatial_nodes[parent.0 as usize].children.remove(i);
+ }
+ }
+ }
+ }
+
+ self.visit_nodes_mut(|_, node| {
+ match node.node_type {
+ SpatialNodeType::ScrollFrame(ref mut info) => {
+ info.offsets = vec![SampledScrollOffset{
+ offset: -info.external_scroll_offset,
+ generation: info.offset_generation,
+ }];
+ }
+ SpatialNodeType::StickyFrame(ref mut info) => {
+ info.current_offset = LayoutVector2D::zero();
+ }
+ SpatialNodeType::ReferenceFrame(..) => {}
+ }
+ });
+ }
+
+ pub fn get_last_sampled_scroll_offsets(
+ &self,
+ ) -> FastHashMap<ExternalScrollId, Vec<SampledScrollOffset>> {
+ let mut result = FastHashMap::default();
+ self.visit_nodes(|_, node| {
+ if let SpatialNodeType::ScrollFrame(ref scrolling) = node.node_type {
+ result.insert(scrolling.external_id, scrolling.offsets.clone());
+ }
+ });
+ result
+ }
+
+ pub fn apply_last_sampled_scroll_offsets(
+ &mut self,
+ last_sampled_offsets: FastHashMap<ExternalScrollId, Vec<SampledScrollOffset>>,
+ ) {
+ self.visit_nodes_mut(|_, node| {
+ if let SpatialNodeType::ScrollFrame(ref mut scrolling) = node.node_type {
+ if let Some(offsets) = last_sampled_offsets.get(&scrolling.external_id) {
+ scrolling.offsets = offsets.clone();
+ }
+ }
+ });
+ }
+
+ pub fn get_spatial_node(&self, index: SpatialNodeIndex) -> &SpatialNode {
+ &self.spatial_nodes[index.0 as usize]
+ }
+
+ pub fn get_spatial_node_mut(&mut self, index: SpatialNodeIndex) -> &mut SpatialNode {
+ &mut self.spatial_nodes[index.0 as usize]
+ }
+
+ /// Get total number of spatial nodes
+ pub fn spatial_node_count(&self) -> usize {
+ self.spatial_nodes.len()
+ }
+
+ pub fn find_spatial_node_by_anim_id(
+ &self,
+ id: PropertyBindingId,
+ ) -> Option<SpatialNodeIndex> {
+ let mut node_index = None;
+
+ self.visit_nodes(|index, node| {
+ if node.is_transform_bound_to_property(id) {
+ debug_assert!(node_index.is_none()); // Multiple nodes with same anim id
+ node_index = Some(index);
+ }
+ });
+
+ node_index
+ }
+
+ /// Calculate the relative transform from `child_index` to `parent_index`.
+ /// This method will panic if the nodes are not connected!
+ pub fn get_relative_transform(
+ &self,
+ child_index: SpatialNodeIndex,
+ parent_index: SpatialNodeIndex,
+ ) -> CoordinateSpaceMapping<LayoutPixel, LayoutPixel> {
+ self.get_relative_transform_with_face(child_index, parent_index, None)
+ }
+
+ /// Calculate the relative transform from `child_index` to `parent_index`.
+ /// This method will panic if the nodes are not connected!
+ /// Also, switch the visible face to `Back` if at any stage where the
+ /// combined transform is flattened, we see the back face.
+ pub fn get_relative_transform_with_face(
+ &self,
+ child_index: SpatialNodeIndex,
+ parent_index: SpatialNodeIndex,
+ mut visible_face: Option<&mut VisibleFace>,
+ ) -> CoordinateSpaceMapping<LayoutPixel, LayoutPixel> {
+ if child_index == parent_index {
+ return CoordinateSpaceMapping::Local;
+ }
+
+ let child = self.get_spatial_node(child_index);
+ let parent = self.get_spatial_node(parent_index);
+
+ // TODO(gw): We expect this never to fail, but it's possible that it might due to
+ // either (a) a bug in WR / Gecko, or (b) some obscure real-world content
+ // that we're unaware of. If we ever hit this, please open a bug with any
+ // repro steps!
+ assert!(
+ child.coordinate_system_id.0 >= parent.coordinate_system_id.0,
+ "bug: this is an unexpected case - please open a bug and talk to #gfx team!",
+ );
+
+ if child.coordinate_system_id == parent.coordinate_system_id {
+ let scale_offset = parent.content_transform
+ .inverse()
+ .accumulate(&child.content_transform);
+ return CoordinateSpaceMapping::ScaleOffset(scale_offset);
+ }
+
+ let mut coordinate_system_id = child.coordinate_system_id;
+ let mut transform = child.content_transform.to_transform();
+
+ // we need to update the associated parameters of a transform in two cases:
+ // 1) when the flattening happens, so that we don't lose that original 3D aspects
+ // 2) when we reach the end of iteration, so that our result is up to date
+
+ while coordinate_system_id != parent.coordinate_system_id {
+ let coord_system = &self.coord_systems[coordinate_system_id.0 as usize];
+
+ if coord_system.should_flatten {
+ if let Some(ref mut face) = visible_face {
+ if transform.is_backface_visible() {
+ **face = VisibleFace::Back;
+ }
+ }
+ transform.flatten_z_output();
+ }
+
+ coordinate_system_id = coord_system.parent.expect("invalid parent!");
+ transform = transform.then(&coord_system.transform);
+ }
+
+ transform = transform.then(
+ &parent.content_transform
+ .inverse()
+ .to_transform(),
+ );
+ if let Some(face) = visible_face {
+ if transform.is_backface_visible() {
+ *face = VisibleFace::Back;
+ }
+ }
+
+ CoordinateSpaceMapping::Transform(transform)
+ }
+
+ /// Returns true if both supplied spatial nodes are in the same coordinate system
+ /// (implies the relative transform produce axis-aligned rects).
+ pub fn is_matching_coord_system(
+ &self,
+ index0: SpatialNodeIndex,
+ index1: SpatialNodeIndex,
+ ) -> bool {
+ let node0 = self.get_spatial_node(index0);
+ let node1 = self.get_spatial_node(index1);
+
+ node0.coordinate_system_id == node1.coordinate_system_id
+ }
+
+ fn get_world_transform_impl(
+ &self,
+ index: SpatialNodeIndex,
+ scroll: TransformScroll,
+ ) -> CoordinateSpaceMapping<LayoutPixel, WorldPixel> {
+ let child = self.get_spatial_node(index);
+
+ if child.coordinate_system_id.0 == 0 {
+ if index == self.root_reference_frame_index {
+ CoordinateSpaceMapping::Local
+ } else {
+ CoordinateSpaceMapping::ScaleOffset(child.content_transform)
+ }
+ } else {
+ let system = &self.coord_systems[child.coordinate_system_id.0 as usize];
+ let scale_offset = match scroll {
+ TransformScroll::Scrolled => &child.content_transform,
+ TransformScroll::Unscrolled => &child.viewport_transform,
+ };
+ let transform = scale_offset
+ .to_transform()
+ .then(&system.world_transform);
+
+ CoordinateSpaceMapping::Transform(transform)
+ }
+ }
+
+ /// Calculate the relative transform from `index` to the root.
+ pub fn get_world_transform(
+ &self,
+ index: SpatialNodeIndex,
+ ) -> CoordinateSpaceMapping<LayoutPixel, WorldPixel> {
+ self.get_world_transform_impl(index, TransformScroll::Scrolled)
+ }
+
+ /// Calculate the relative transform from `index` to the root.
+ /// Unlike `get_world_transform`, this variant doesn't account for the local scroll offset.
+ pub fn get_world_viewport_transform(
+ &self,
+ index: SpatialNodeIndex,
+ ) -> CoordinateSpaceMapping<LayoutPixel, WorldPixel> {
+ self.get_world_transform_impl(index, TransformScroll::Unscrolled)
+ }
+
+ /// The root reference frame, which is the true root of the SpatialTree.
+ pub fn root_reference_frame_index(&self) -> SpatialNodeIndex {
+ self.root_reference_frame_index
+ }
+
+ pub fn set_scroll_offsets(
+ &mut self,
+ id: ExternalScrollId,
+ offsets: Vec<SampledScrollOffset>,
+ ) -> bool {
+ let mut did_change = false;
+
+ self.visit_nodes_mut(|_, node| {
+ if node.matches_external_id(id) {
+ did_change |= node.set_scroll_offsets(offsets.clone());
+ }
+ });
+
+ did_change
+ }
+
+ pub fn update_tree(
+ &mut self,
+ scene_properties: &SceneProperties,
+ ) {
+ if self.root_reference_frame_index == SpatialNodeIndex::INVALID {
+ return;
+ }
+
+ profile_scope!("update_tree");
+ self.coord_systems.clear();
+ self.coord_systems.push(CoordinateSystem::root());
+
+ let root_node_index = self.root_reference_frame_index();
+ assert!(self.update_state_stack.is_empty());
+
+ let state = TransformUpdateState {
+ parent_reference_frame_transform: LayoutVector2D::zero().into(),
+ parent_accumulated_scroll_offset: LayoutVector2D::zero(),
+ nearest_scrolling_ancestor_offset: LayoutVector2D::zero(),
+ nearest_scrolling_ancestor_viewport: LayoutRect::zero(),
+ current_coordinate_system_id: CoordinateSystemId::root(),
+ coordinate_system_relative_scale_offset: ScaleOffset::identity(),
+ invertible: true,
+ preserves_3d: false,
+ is_ancestor_or_self_zooming: false,
+ external_id: None,
+ scroll_offset: LayoutVector2D::zero(),
+ };
+ self.update_state_stack.push(state);
+
+ self.update_node(
+ root_node_index,
+ scene_properties,
+ );
+
+ self.update_state_stack.pop().unwrap();
+ }
+
+ fn update_node(
+ &mut self,
+ node_index: SpatialNodeIndex,
+ scene_properties: &SceneProperties,
+ ) {
+ let parent_snapping_transform = match self.get_spatial_node(node_index).parent {
+ Some(parent_index) => {
+ self.get_node_info(parent_index).snapping_transform
+ }
+ None => {
+ Some(ScaleOffset::identity())
+ }
+ };
+
+ let node = &mut self.spatial_nodes[node_index.0 as usize];
+
+ node.snapping_transform = calculate_snapping_transform(
+ parent_snapping_transform,
+ &node.node_type,
+ );
+
+ node.update(
+ &self.update_state_stack,
+ &mut self.coord_systems,
+ scene_properties,
+ );
+
+ if !node.children.is_empty() {
+ let mut child_state = self.update_state_stack.last().unwrap().clone();
+ node.prepare_state_for_children(&mut child_state);
+ self.update_state_stack.push(child_state);
+
+ let mut child_indices: SmallVec<[SpatialNodeIndex; 8]> = SmallVec::new();
+ child_indices.extend_from_slice(&node.children);
+
+ for child_index in child_indices {
+ self.update_node(
+ child_index,
+ scene_properties,
+ );
+ }
+
+ self.update_state_stack.pop().unwrap();
+ }
+ }
+
+ pub fn build_transform_palette(&self) -> TransformPalette {
+ profile_scope!("build_transform_palette");
+ TransformPalette::new(self.spatial_nodes.len())
+ }
+
+ fn print_node<T: PrintTreePrinter>(
+ &self,
+ index: SpatialNodeIndex,
+ pt: &mut T,
+ ) {
+ let node = self.get_spatial_node(index);
+ match node.node_type {
+ SpatialNodeType::StickyFrame(ref sticky_frame_info) => {
+ pt.new_level(format!("StickyFrame"));
+ pt.add_item(format!("sticky info: {:?}", sticky_frame_info));
+ }
+ SpatialNodeType::ScrollFrame(ref scrolling_info) => {
+ pt.new_level(format!("ScrollFrame"));
+ pt.add_item(format!("viewport: {:?}", scrolling_info.viewport_rect));
+ pt.add_item(format!("scrollable_size: {:?}", scrolling_info.scrollable_size));
+ pt.add_item(format!("scroll offset: {:?}", scrolling_info.offset()));
+ pt.add_item(format!("external_scroll_offset: {:?}", scrolling_info.external_scroll_offset));
+ pt.add_item(format!("offset generation: {:?}", scrolling_info.offset_generation));
+ if scrolling_info.has_scroll_linked_effect == HasScrollLinkedEffect::Yes {
+ pt.add_item("has scroll-linked effect".to_string());
+ }
+ pt.add_item(format!("kind: {:?}", scrolling_info.frame_kind));
+ }
+ SpatialNodeType::ReferenceFrame(ref info) => {
+ pt.new_level(format!("ReferenceFrame"));
+ pt.add_item(format!("kind: {:?}", info.kind));
+ pt.add_item(format!("transform_style: {:?}", info.transform_style));
+ pt.add_item(format!("source_transform: {:?}", info.source_transform));
+ pt.add_item(format!("origin_in_parent_reference_frame: {:?}", info.origin_in_parent_reference_frame));
+ }
+ }
+
+ pt.add_item(format!("index: {:?}", index));
+ pt.add_item(format!("content_transform: {:?}", node.content_transform));
+ pt.add_item(format!("viewport_transform: {:?}", node.viewport_transform));
+ pt.add_item(format!("snapping_transform: {:?}", node.snapping_transform));
+ pt.add_item(format!("coordinate_system_id: {:?}", node.coordinate_system_id));
+
+ for child_index in &node.children {
+ self.print_node(*child_index, pt);
+ }
+
+ pt.end_level();
+ }
+
+ /// Get the visible face of the transfrom from the specified node to its parent.
+ pub fn get_local_visible_face(&self, node_index: SpatialNodeIndex) -> VisibleFace {
+ let node = self.get_spatial_node(node_index);
+ let mut face = VisibleFace::Front;
+ if let Some(mut parent_index) = node.parent {
+ // Check if the parent is perspective. In CSS, a stacking context may
+ // have both perspective and a regular transformation. Gecko translates the
+ // perspective into a different `nsDisplayPerspective` and `nsDisplayTransform` items.
+ // On WebRender side, we end up with 2 different reference frames:
+ // one has kind of "transform", and it's parented to another of "perspective":
+ // https://searchfox.org/mozilla-central/rev/72c7cef167829b6f1e24cae216fa261934c455fc/layout/generic/nsIFrame.cpp#3716
+ if let SpatialNodeType::ReferenceFrame(ReferenceFrameInfo { kind: ReferenceFrameKind::Transform {
+ paired_with_perspective: true,
+ ..
+ }, .. }) = node.node_type {
+ let parent = self.get_spatial_node(parent_index);
+ match parent.node_type {
+ SpatialNodeType::ReferenceFrame(ReferenceFrameInfo {
+ kind: ReferenceFrameKind::Perspective { .. },
+ ..
+ }) => {
+ parent_index = parent.parent.unwrap();
+ }
+ _ => {
+ log::error!("Unexpected parent {:?} is not perspective", parent_index);
+ }
+ }
+ }
+
+ self.get_relative_transform_with_face(node_index, parent_index, Some(&mut face));
+ }
+ face
+ }
+
+ #[allow(dead_code)]
+ pub fn print(&self) {
+ if self.root_reference_frame_index != SpatialNodeIndex::INVALID {
+ let mut buf = Vec::<u8>::new();
+ {
+ let mut pt = PrintTree::new_with_sink("spatial tree", &mut buf);
+ self.print_with(&mut pt);
+ }
+ // If running in Gecko, set RUST_LOG=webrender::spatial_tree=debug
+ // to get this logging to be emitted to stderr/logcat.
+ debug!("{}", std::str::from_utf8(&buf).unwrap_or("(Tree printer emitted non-utf8)"));
+ }
+ }
+}
+
+impl PrintableTree for SpatialTree {
+ fn print_with<T: PrintTreePrinter>(&self, pt: &mut T) {
+ if self.root_reference_frame_index != SpatialNodeIndex::INVALID {
+ self.print_node(self.root_reference_frame_index(), pt);
+ }
+ }
+}
+
+/// Calculate the accumulated external scroll offset for a given spatial node.
+pub fn get_external_scroll_offset<S: SpatialNodeContainer>(
+ spatial_tree: &S,
+ node_index: SpatialNodeIndex,
+) -> LayoutVector2D {
+ let mut offset = LayoutVector2D::zero();
+ let mut current_node = Some(node_index);
+
+ while let Some(node_index) = current_node {
+ let node_info = spatial_tree.get_node_info(node_index);
+
+ match node_info.node_type {
+ SpatialNodeType::ScrollFrame(ref scrolling) => {
+ offset += scrolling.external_scroll_offset;
+ }
+ SpatialNodeType::StickyFrame(..) => {
+ // Doesn't provide any external scroll offset
+ }
+ SpatialNodeType::ReferenceFrame(..) => {
+ // External scroll offsets are not propagated across
+ // reference frames.
+ break;
+ }
+ }
+
+ current_node = node_info.parent;
+ }
+
+ offset
+}
+
+fn calculate_snapping_transform(
+ parent_snapping_transform: Option<ScaleOffset>,
+ node_type: &SpatialNodeType,
+) -> Option<ScaleOffset> {
+ // We need to incorporate the parent scale/offset with the child.
+ // If the parent does not have a scale/offset, then we know we are
+ // not 2d axis aligned and thus do not need to snap its children
+ // either.
+ let parent_scale_offset = match parent_snapping_transform {
+ Some(parent_snapping_transform) => parent_snapping_transform,
+ None => return None,
+ };
+
+ let scale_offset = match node_type {
+ SpatialNodeType::ReferenceFrame(ref info) => {
+ match info.source_transform {
+ PropertyBinding::Value(ref value) => {
+ // We can only get a ScaleOffset if the transform is 2d axis
+ // aligned.
+ match ScaleOffset::from_transform(value) {
+ Some(scale_offset) => {
+ let origin_offset = info.origin_in_parent_reference_frame;
+ ScaleOffset::from_offset(origin_offset.to_untyped())
+ .accumulate(&scale_offset)
+ }
+ None => return None,
+ }
+ }
+
+ // Assume animations start at the identity transform for snapping purposes.
+ // We still want to incorporate the reference frame offset however.
+ // TODO(aosmond): Is there a better known starting point?
+ PropertyBinding::Binding(..) => {
+ let origin_offset = info.origin_in_parent_reference_frame;
+ ScaleOffset::from_offset(origin_offset.to_untyped())
+ }
+ }
+ }
+ _ => ScaleOffset::identity(),
+ };
+
+ Some(parent_scale_offset.accumulate(&scale_offset))
+}
+
+#[cfg(test)]
+fn add_reference_frame(
+ cst: &mut SceneSpatialTree,
+ parent: SpatialNodeIndex,
+ transform: LayoutTransform,
+ origin_in_parent_reference_frame: LayoutVector2D,
+ key: SpatialTreeItemKey,
+) -> SpatialNodeIndex {
+ let pid = PipelineInstanceId::new(0);
+
+ cst.add_reference_frame(
+ parent,
+ TransformStyle::Preserve3D,
+ PropertyBinding::Value(transform),
+ ReferenceFrameKind::Transform {
+ is_2d_scale_translation: false,
+ should_snap: false,
+ paired_with_perspective: false,
+ },
+ origin_in_parent_reference_frame,
+ PipelineId::dummy(),
+ SpatialNodeUid::external(key, PipelineId::dummy(), pid),
+ )
+}
+
+#[cfg(test)]
+fn test_pt(
+ px: f32,
+ py: f32,
+ cst: &SpatialTree,
+ child: SpatialNodeIndex,
+ parent: SpatialNodeIndex,
+ expected_x: f32,
+ expected_y: f32,
+) {
+ use euclid::approxeq::ApproxEq;
+ const EPSILON: f32 = 0.0001;
+
+ let p = LayoutPoint::new(px, py);
+ let m = cst.get_relative_transform(child, parent).into_transform();
+ let pt = m.transform_point2d(p).unwrap();
+ assert!(pt.x.approx_eq_eps(&expected_x, &EPSILON) &&
+ pt.y.approx_eq_eps(&expected_y, &EPSILON),
+ "p: {:?} -> {:?}\nm={:?}",
+ p, pt, m,
+ );
+}
+
+#[test]
+fn test_cst_simple_translation() {
+ // Basic translations only
+
+ let mut cst = SceneSpatialTree::new();
+ let root_reference_frame_index = cst.root_reference_frame_index();
+
+ let root = add_reference_frame(
+ &mut cst,
+ root_reference_frame_index,
+ LayoutTransform::identity(),
+ LayoutVector2D::zero(),
+ SpatialTreeItemKey::new(0, 0),
+ );
+
+ let child1 = add_reference_frame(
+ &mut cst,
+ root,
+ LayoutTransform::translation(100.0, 0.0, 0.0),
+ LayoutVector2D::zero(),
+ SpatialTreeItemKey::new(0, 1),
+ );
+
+ let child2 = add_reference_frame(
+ &mut cst,
+ child1,
+ LayoutTransform::translation(0.0, 50.0, 0.0),
+ LayoutVector2D::zero(),
+ SpatialTreeItemKey::new(0, 2),
+ );
+
+ let child3 = add_reference_frame(
+ &mut cst,
+ child2,
+ LayoutTransform::translation(200.0, 200.0, 0.0),
+ LayoutVector2D::zero(),
+ SpatialTreeItemKey::new(0, 3),
+ );
+
+ let mut st = SpatialTree::new();
+ st.apply_updates(cst.end_frame_and_get_pending_updates());
+ st.update_tree(&SceneProperties::new());
+
+ test_pt(100.0, 100.0, &st, child1, root, 200.0, 100.0);
+ test_pt(100.0, 100.0, &st, child2, root, 200.0, 150.0);
+ test_pt(100.0, 100.0, &st, child2, child1, 100.0, 150.0);
+ test_pt(100.0, 100.0, &st, child3, root, 400.0, 350.0);
+}
+
+#[test]
+fn test_cst_simple_scale() {
+ // Basic scale only
+
+ let mut cst = SceneSpatialTree::new();
+ let root_reference_frame_index = cst.root_reference_frame_index();
+
+ let root = add_reference_frame(
+ &mut cst,
+ root_reference_frame_index,
+ LayoutTransform::identity(),
+ LayoutVector2D::zero(),
+ SpatialTreeItemKey::new(0, 0),
+ );
+
+ let child1 = add_reference_frame(
+ &mut cst,
+ root,
+ LayoutTransform::scale(4.0, 1.0, 1.0),
+ LayoutVector2D::zero(),
+ SpatialTreeItemKey::new(0, 1),
+ );
+
+ let child2 = add_reference_frame(
+ &mut cst,
+ child1,
+ LayoutTransform::scale(1.0, 2.0, 1.0),
+ LayoutVector2D::zero(),
+ SpatialTreeItemKey::new(0, 2),
+ );
+
+ let child3 = add_reference_frame(
+ &mut cst,
+ child2,
+ LayoutTransform::scale(2.0, 2.0, 1.0),
+ LayoutVector2D::zero(),
+ SpatialTreeItemKey::new(0, 3),
+ );
+
+ let mut st = SpatialTree::new();
+ st.apply_updates(cst.end_frame_and_get_pending_updates());
+ st.update_tree(&SceneProperties::new());
+
+ test_pt(100.0, 100.0, &st, child1, root, 400.0, 100.0);
+ test_pt(100.0, 100.0, &st, child2, root, 400.0, 200.0);
+ test_pt(100.0, 100.0, &st, child3, root, 800.0, 400.0);
+ test_pt(100.0, 100.0, &st, child2, child1, 100.0, 200.0);
+ test_pt(100.0, 100.0, &st, child3, child1, 200.0, 400.0);
+}
+
+#[test]
+fn test_cst_scale_translation() {
+ // Scale + translation
+
+ let mut cst = SceneSpatialTree::new();
+ let root_reference_frame_index = cst.root_reference_frame_index();
+
+ let root = add_reference_frame(
+ &mut cst,
+ root_reference_frame_index,
+ LayoutTransform::identity(),
+ LayoutVector2D::zero(),
+ SpatialTreeItemKey::new(0, 0),
+ );
+
+ let child1 = add_reference_frame(
+ &mut cst,
+ root,
+ LayoutTransform::translation(100.0, 50.0, 0.0),
+ LayoutVector2D::zero(),
+ SpatialTreeItemKey::new(0, 1),
+ );
+
+ let child2 = add_reference_frame(
+ &mut cst,
+ child1,
+ LayoutTransform::scale(2.0, 4.0, 1.0),
+ LayoutVector2D::zero(),
+ SpatialTreeItemKey::new(0, 2),
+ );
+
+ let child3 = add_reference_frame(
+ &mut cst,
+ child2,
+ LayoutTransform::translation(200.0, -100.0, 0.0),
+ LayoutVector2D::zero(),
+ SpatialTreeItemKey::new(0, 3),
+ );
+
+ let child4 = add_reference_frame(
+ &mut cst,
+ child3,
+ LayoutTransform::scale(3.0, 2.0, 1.0),
+ LayoutVector2D::zero(),
+ SpatialTreeItemKey::new(0, 4),
+ );
+
+ let mut st = SpatialTree::new();
+ st.apply_updates(cst.end_frame_and_get_pending_updates());
+ st.update_tree(&SceneProperties::new());
+
+ test_pt(100.0, 100.0, &st, child1, root, 200.0, 150.0);
+ test_pt(100.0, 100.0, &st, child2, root, 300.0, 450.0);
+ test_pt(100.0, 100.0, &st, child4, root, 1100.0, 450.0);
+
+ test_pt(0.0, 0.0, &st, child4, child1, 400.0, -400.0);
+ test_pt(100.0, 100.0, &st, child4, child1, 1000.0, 400.0);
+ test_pt(100.0, 100.0, &st, child2, child1, 200.0, 400.0);
+
+ test_pt(100.0, 100.0, &st, child3, child1, 600.0, 0.0);
+}
+
+#[test]
+fn test_cst_translation_rotate() {
+ // Rotation + translation
+ use euclid::Angle;
+
+ let mut cst = SceneSpatialTree::new();
+ let root_reference_frame_index = cst.root_reference_frame_index();
+
+ let root = add_reference_frame(
+ &mut cst,
+ root_reference_frame_index,
+ LayoutTransform::identity(),
+ LayoutVector2D::zero(),
+ SpatialTreeItemKey::new(0, 0),
+ );
+
+ let child1 = add_reference_frame(
+ &mut cst,
+ root,
+ LayoutTransform::rotation(0.0, 0.0, 1.0, Angle::degrees(-90.0)),
+ LayoutVector2D::zero(),
+ SpatialTreeItemKey::new(0, 1),
+ );
+
+ let mut st = SpatialTree::new();
+ st.apply_updates(cst.end_frame_and_get_pending_updates());
+ st.update_tree(&SceneProperties::new());
+
+ test_pt(100.0, 0.0, &st, child1, root, 0.0, -100.0);
+}
+
+#[test]
+fn test_is_ancestor1() {
+ let mut st = SceneSpatialTree::new();
+ let root_reference_frame_index = st.root_reference_frame_index();
+
+ let root = add_reference_frame(
+ &mut st,
+ root_reference_frame_index,
+ LayoutTransform::identity(),
+ LayoutVector2D::zero(),
+ SpatialTreeItemKey::new(0, 0),
+ );
+
+ let child1_0 = add_reference_frame(
+ &mut st,
+ root,
+ LayoutTransform::identity(),
+ LayoutVector2D::zero(),
+ SpatialTreeItemKey::new(0, 1),
+ );
+
+ let child1_1 = add_reference_frame(
+ &mut st,
+ child1_0,
+ LayoutTransform::identity(),
+ LayoutVector2D::zero(),
+ SpatialTreeItemKey::new(0, 2),
+ );
+
+ let child2 = add_reference_frame(
+ &mut st,
+ root,
+ LayoutTransform::identity(),
+ LayoutVector2D::zero(),
+ SpatialTreeItemKey::new(0, 3),
+ );
+
+ assert!(!st.is_ancestor(root, root));
+ assert!(!st.is_ancestor(child1_0, child1_0));
+ assert!(!st.is_ancestor(child1_1, child1_1));
+ assert!(!st.is_ancestor(child2, child2));
+
+ assert!(st.is_ancestor(root, child1_0));
+ assert!(st.is_ancestor(root, child1_1));
+ assert!(st.is_ancestor(child1_0, child1_1));
+
+ assert!(!st.is_ancestor(child1_0, root));
+ assert!(!st.is_ancestor(child1_1, root));
+ assert!(!st.is_ancestor(child1_1, child1_0));
+
+ assert!(st.is_ancestor(root, child2));
+ assert!(!st.is_ancestor(child2, root));
+
+ assert!(!st.is_ancestor(child1_0, child2));
+ assert!(!st.is_ancestor(child1_1, child2));
+ assert!(!st.is_ancestor(child2, child1_0));
+ assert!(!st.is_ancestor(child2, child1_1));
+}
+
+/// Tests that we select the correct scroll root in the simple case.
+#[test]
+fn test_find_scroll_root_simple() {
+ let mut st = SceneSpatialTree::new();
+ let pid = PipelineInstanceId::new(0);
+
+ let root = st.add_reference_frame(
+ st.root_reference_frame_index(),
+ TransformStyle::Flat,
+ PropertyBinding::Value(LayoutTransform::identity()),
+ ReferenceFrameKind::Transform {
+ is_2d_scale_translation: true,
+ should_snap: true,
+ paired_with_perspective: false,
+ },
+ LayoutVector2D::new(0.0, 0.0),
+ PipelineId::dummy(),
+ SpatialNodeUid::external(SpatialTreeItemKey::new(0, 0), PipelineId::dummy(), pid),
+ );
+
+ let scroll = st.add_scroll_frame(
+ root,
+ ExternalScrollId(1, PipelineId::dummy()),
+ PipelineId::dummy(),
+ &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
+ &LayoutSize::new(800.0, 400.0),
+ ScrollFrameKind::Explicit,
+ LayoutVector2D::new(0.0, 0.0),
+ APZScrollGeneration::default(),
+ HasScrollLinkedEffect::No,
+ SpatialNodeUid::external(SpatialTreeItemKey::new(0, 1), PipelineId::dummy(), pid),
+ );
+
+ assert_eq!(st.find_scroll_root(scroll), scroll);
+}
+
+/// Tests that we select the root scroll frame rather than the subframe if both are scrollable.
+#[test]
+fn test_find_scroll_root_sub_scroll_frame() {
+ let mut st = SceneSpatialTree::new();
+ let pid = PipelineInstanceId::new(0);
+
+ let root = st.add_reference_frame(
+ st.root_reference_frame_index(),
+ TransformStyle::Flat,
+ PropertyBinding::Value(LayoutTransform::identity()),
+ ReferenceFrameKind::Transform {
+ is_2d_scale_translation: true,
+ should_snap: true,
+ paired_with_perspective: false,
+ },
+ LayoutVector2D::new(0.0, 0.0),
+ PipelineId::dummy(),
+ SpatialNodeUid::external(SpatialTreeItemKey::new(0, 0), PipelineId::dummy(), pid),
+ );
+
+ let root_scroll = st.add_scroll_frame(
+ root,
+ ExternalScrollId(1, PipelineId::dummy()),
+ PipelineId::dummy(),
+ &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
+ &LayoutSize::new(800.0, 400.0),
+ ScrollFrameKind::Explicit,
+ LayoutVector2D::new(0.0, 0.0),
+ APZScrollGeneration::default(),
+ HasScrollLinkedEffect::No,
+ SpatialNodeUid::external(SpatialTreeItemKey::new(0, 1), PipelineId::dummy(), pid),
+ );
+
+ let sub_scroll = st.add_scroll_frame(
+ root_scroll,
+ ExternalScrollId(1, PipelineId::dummy()),
+ PipelineId::dummy(),
+ &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
+ &LayoutSize::new(800.0, 400.0),
+ ScrollFrameKind::Explicit,
+ LayoutVector2D::new(0.0, 0.0),
+ APZScrollGeneration::default(),
+ HasScrollLinkedEffect::No,
+ SpatialNodeUid::external(SpatialTreeItemKey::new(0, 2), PipelineId::dummy(), pid),
+ );
+
+ assert_eq!(st.find_scroll_root(sub_scroll), root_scroll);
+}
+
+/// Tests that we select the sub scroll frame when the root scroll frame is not scrollable.
+#[test]
+fn test_find_scroll_root_not_scrollable() {
+ let mut st = SceneSpatialTree::new();
+ let pid = PipelineInstanceId::new(0);
+
+ let root = st.add_reference_frame(
+ st.root_reference_frame_index(),
+ TransformStyle::Flat,
+ PropertyBinding::Value(LayoutTransform::identity()),
+ ReferenceFrameKind::Transform {
+ is_2d_scale_translation: true,
+ should_snap: true,
+ paired_with_perspective: false,
+ },
+ LayoutVector2D::new(0.0, 0.0),
+ PipelineId::dummy(),
+ SpatialNodeUid::external(SpatialTreeItemKey::new(0, 0), PipelineId::dummy(), pid),
+ );
+
+ let root_scroll = st.add_scroll_frame(
+ root,
+ ExternalScrollId(1, PipelineId::dummy()),
+ PipelineId::dummy(),
+ &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
+ &LayoutSize::new(400.0, 400.0),
+ ScrollFrameKind::Explicit,
+ LayoutVector2D::new(0.0, 0.0),
+ APZScrollGeneration::default(),
+ HasScrollLinkedEffect::No,
+ SpatialNodeUid::external(SpatialTreeItemKey::new(0, 1), PipelineId::dummy(), pid),
+ );
+
+ let sub_scroll = st.add_scroll_frame(
+ root_scroll,
+ ExternalScrollId(1, PipelineId::dummy()),
+ PipelineId::dummy(),
+ &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
+ &LayoutSize::new(800.0, 400.0),
+ ScrollFrameKind::Explicit,
+ LayoutVector2D::new(0.0, 0.0),
+ APZScrollGeneration::default(),
+ HasScrollLinkedEffect::No,
+ SpatialNodeUid::external(SpatialTreeItemKey::new(0, 2), PipelineId::dummy(), pid),
+ );
+
+ assert_eq!(st.find_scroll_root(sub_scroll), sub_scroll);
+}
+
+/// Tests that we select the sub scroll frame when the root scroll frame is too small.
+#[test]
+fn test_find_scroll_root_too_small() {
+ let mut st = SceneSpatialTree::new();
+ let pid = PipelineInstanceId::new(0);
+
+ let root = st.add_reference_frame(
+ st.root_reference_frame_index(),
+ TransformStyle::Flat,
+ PropertyBinding::Value(LayoutTransform::identity()),
+ ReferenceFrameKind::Transform {
+ is_2d_scale_translation: true,
+ should_snap: true,
+ paired_with_perspective: false,
+ },
+ LayoutVector2D::new(0.0, 0.0),
+ PipelineId::dummy(),
+ SpatialNodeUid::external(SpatialTreeItemKey::new(0, 0), PipelineId::dummy(), pid),
+ );
+
+ let root_scroll = st.add_scroll_frame(
+ root,
+ ExternalScrollId(1, PipelineId::dummy()),
+ PipelineId::dummy(),
+ &LayoutRect::from_size(LayoutSize::new(MIN_SCROLL_ROOT_SIZE, MIN_SCROLL_ROOT_SIZE)),
+ &LayoutSize::new(1000.0, 1000.0),
+ ScrollFrameKind::Explicit,
+ LayoutVector2D::new(0.0, 0.0),
+ APZScrollGeneration::default(),
+ HasScrollLinkedEffect::No,
+ SpatialNodeUid::external(SpatialTreeItemKey::new(0, 1), PipelineId::dummy(), pid),
+ );
+
+ let sub_scroll = st.add_scroll_frame(
+ root_scroll,
+ ExternalScrollId(1, PipelineId::dummy()),
+ PipelineId::dummy(),
+ &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
+ &LayoutSize::new(800.0, 400.0),
+ ScrollFrameKind::Explicit,
+ LayoutVector2D::new(0.0, 0.0),
+ APZScrollGeneration::default(),
+ HasScrollLinkedEffect::No,
+ SpatialNodeUid::external(SpatialTreeItemKey::new(0, 2), PipelineId::dummy(), pid),
+ );
+
+ assert_eq!(st.find_scroll_root(sub_scroll), sub_scroll);
+}
+
+/// Tests that we select the root scroll node, even if it is not scrollable,
+/// when encountering a non-axis-aligned transform.
+#[test]
+fn test_find_scroll_root_perspective() {
+ let mut st = SceneSpatialTree::new();
+ let pid = PipelineInstanceId::new(0);
+
+ let root = st.add_reference_frame(
+ st.root_reference_frame_index(),
+ TransformStyle::Flat,
+ PropertyBinding::Value(LayoutTransform::identity()),
+ ReferenceFrameKind::Transform {
+ is_2d_scale_translation: true,
+ should_snap: true,
+ paired_with_perspective: false,
+ },
+ LayoutVector2D::new(0.0, 0.0),
+ PipelineId::dummy(),
+ SpatialNodeUid::external(SpatialTreeItemKey::new(0, 0), PipelineId::dummy(), pid),
+ );
+
+ let root_scroll = st.add_scroll_frame(
+ root,
+ ExternalScrollId(1, PipelineId::dummy()),
+ PipelineId::dummy(),
+ &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
+ &LayoutSize::new(400.0, 400.0),
+ ScrollFrameKind::Explicit,
+ LayoutVector2D::new(0.0, 0.0),
+ APZScrollGeneration::default(),
+ HasScrollLinkedEffect::No,
+ SpatialNodeUid::external(SpatialTreeItemKey::new(0, 1), PipelineId::dummy(), pid),
+ );
+
+ let perspective = st.add_reference_frame(
+ root_scroll,
+ TransformStyle::Flat,
+ PropertyBinding::Value(LayoutTransform::identity()),
+ ReferenceFrameKind::Perspective {
+ scrolling_relative_to: None,
+ },
+ LayoutVector2D::new(0.0, 0.0),
+ PipelineId::dummy(),
+ SpatialNodeUid::external(SpatialTreeItemKey::new(0, 2), PipelineId::dummy(), pid),
+ );
+
+ let sub_scroll = st.add_scroll_frame(
+ perspective,
+ ExternalScrollId(1, PipelineId::dummy()),
+ PipelineId::dummy(),
+ &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
+ &LayoutSize::new(800.0, 400.0),
+ ScrollFrameKind::Explicit,
+ LayoutVector2D::new(0.0, 0.0),
+ APZScrollGeneration::default(),
+ HasScrollLinkedEffect::No,
+ SpatialNodeUid::external(SpatialTreeItemKey::new(0, 3), PipelineId::dummy(), pid),
+ );
+
+ assert_eq!(st.find_scroll_root(sub_scroll), root_scroll);
+}
+
+/// Tests that encountering a 2D scale or translation transform does not prevent
+/// us from selecting the sub scroll frame if the root scroll frame is unscrollable.
+#[test]
+fn test_find_scroll_root_2d_scale() {
+ let mut st = SceneSpatialTree::new();
+ let pid = PipelineInstanceId::new(0);
+
+ let root = st.add_reference_frame(
+ st.root_reference_frame_index(),
+ TransformStyle::Flat,
+ PropertyBinding::Value(LayoutTransform::identity()),
+ ReferenceFrameKind::Transform {
+ is_2d_scale_translation: true,
+ should_snap: true,
+ paired_with_perspective: false,
+ },
+ LayoutVector2D::new(0.0, 0.0),
+ PipelineId::dummy(),
+ SpatialNodeUid::external(SpatialTreeItemKey::new(0, 0), PipelineId::dummy(), pid),
+ );
+
+ let root_scroll = st.add_scroll_frame(
+ root,
+ ExternalScrollId(1, PipelineId::dummy()),
+ PipelineId::dummy(),
+ &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
+ &LayoutSize::new(400.0, 400.0),
+ ScrollFrameKind::Explicit,
+ LayoutVector2D::new(0.0, 0.0),
+ APZScrollGeneration::default(),
+ HasScrollLinkedEffect::No,
+ SpatialNodeUid::external(SpatialTreeItemKey::new(0, 1), PipelineId::dummy(), pid),
+ );
+
+ let scale = st.add_reference_frame(
+ root_scroll,
+ TransformStyle::Flat,
+ PropertyBinding::Value(LayoutTransform::identity()),
+ ReferenceFrameKind::Transform {
+ is_2d_scale_translation: true,
+ should_snap: false,
+ paired_with_perspective: false,
+ },
+ LayoutVector2D::new(0.0, 0.0),
+ PipelineId::dummy(),
+ SpatialNodeUid::external(SpatialTreeItemKey::new(0, 2), PipelineId::dummy(), pid),
+ );
+
+ let sub_scroll = st.add_scroll_frame(
+ scale,
+ ExternalScrollId(1, PipelineId::dummy()),
+ PipelineId::dummy(),
+ &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
+ &LayoutSize::new(800.0, 400.0),
+ ScrollFrameKind::Explicit,
+ LayoutVector2D::new(0.0, 0.0),
+ APZScrollGeneration::default(),
+ HasScrollLinkedEffect::No,
+ SpatialNodeUid::external(SpatialTreeItemKey::new(0, 3), PipelineId::dummy(), pid),
+ );
+
+ assert_eq!(st.find_scroll_root(sub_scroll), sub_scroll);
+}
diff --git a/gfx/wr/webrender/src/surface.rs b/gfx/wr/webrender/src/surface.rs
new file mode 100644
index 0000000000..44be7ae279
--- /dev/null
+++ b/gfx/wr/webrender/src/surface.rs
@@ -0,0 +1,612 @@
+/* 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 api::units::*;
+use crate::command_buffer::{CommandBufferBuilderKind, CommandBufferList, CommandBufferBuilder, CommandBufferIndex};
+use crate::internal_types::FastHashMap;
+use crate::picture::{SurfaceInfo, SurfaceIndex, TileKey, SubSliceIndex, MAX_COMPOSITOR_SURFACES};
+use crate::prim_store::{PictureIndex};
+use crate::render_task_graph::{RenderTaskId, RenderTaskGraphBuilder};
+use crate::render_target::ResolveOp;
+use crate::render_task::{RenderTask, RenderTaskKind, RenderTaskLocation};
+use crate::visibility::{VisibilityState, PrimitiveVisibility};
+
+/*
+ Contains functionality to help building the render task graph from a series of off-screen
+ surfaces that are created during the prepare pass. For now, it maintains existing behavior.
+ A future patch will add support for surface sub-graphs, while ensuring the render task
+ graph itself is built correctly with dependencies regardless of the surface kind (chained,
+ tiled, simple).
+ */
+
+// Information about the render task(s) for a given tile
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SurfaceTileDescriptor {
+ /// Target render task for commands added to this tile. This is changed
+ /// each time a sub-graph is encountered on this tile
+ pub current_task_id: RenderTaskId,
+ /// The compositing task for this tile, if required. This is only needed
+ /// when a tile contains one or more sub-graphs.
+ pub composite_task_id: Option<RenderTaskId>,
+ /// Dirty rect for this tile
+ pub dirty_rect: PictureRect,
+}
+
+// Details of how a surface is rendered
+pub enum SurfaceDescriptorKind {
+ // Picture cache tiles
+ Tiled {
+ tiles: FastHashMap<TileKey, SurfaceTileDescriptor>,
+ },
+ // A single surface (e.g. for an opacity filter)
+ Simple {
+ render_task_id: RenderTaskId,
+ dirty_rect: PictureRect,
+ },
+ // A surface with 1+ intermediate tasks (e.g. blur)
+ Chained {
+ render_task_id: RenderTaskId,
+ root_task_id: RenderTaskId,
+ dirty_rect: PictureRect,
+ },
+}
+
+// Describes how a surface is rendered
+pub struct SurfaceDescriptor {
+ kind: SurfaceDescriptorKind,
+}
+
+impl SurfaceDescriptor {
+ // Create a picture cache tiled surface
+ pub fn new_tiled(
+ tiles: FastHashMap<TileKey, SurfaceTileDescriptor>,
+ ) -> Self {
+ SurfaceDescriptor {
+ kind: SurfaceDescriptorKind::Tiled {
+ tiles,
+ },
+ }
+ }
+
+ // Create a chained surface (e.g. blur)
+ pub fn new_chained(
+ render_task_id: RenderTaskId,
+ root_task_id: RenderTaskId,
+ dirty_rect: PictureRect,
+ ) -> Self {
+ SurfaceDescriptor {
+ kind: SurfaceDescriptorKind::Chained {
+ render_task_id,
+ root_task_id,
+ dirty_rect,
+ },
+ }
+ }
+
+ // Create a simple surface (e.g. opacity)
+ pub fn new_simple(
+ render_task_id: RenderTaskId,
+ dirty_rect: PictureRect,
+ ) -> Self {
+ SurfaceDescriptor {
+ kind: SurfaceDescriptorKind::Simple {
+ render_task_id,
+ dirty_rect,
+ },
+ }
+ }
+}
+
+// Describes a list of command buffers that we are adding primitives to
+// for a given surface. These are created from a command buffer builder
+// as an optimization - skipping the indirection pic_task -> cmd_buffer_index
+struct CommandBufferTargets {
+ available_cmd_buffers: Vec<Vec<(PictureRect, CommandBufferIndex)>>,
+}
+
+impl CommandBufferTargets {
+ fn new() -> Self {
+ CommandBufferTargets {
+ available_cmd_buffers: vec![Vec::new(); MAX_COMPOSITOR_SURFACES+1],
+ }
+ }
+
+ fn init(
+ &mut self,
+ cb: &CommandBufferBuilder,
+ rg_builder: &RenderTaskGraphBuilder,
+ ) {
+ for available_cmd_buffers in &mut self.available_cmd_buffers {
+ available_cmd_buffers.clear();
+ }
+
+ match cb.kind {
+ CommandBufferBuilderKind::Tiled { ref tiles, .. } => {
+ for (key, desc) in tiles {
+ let task = rg_builder.get_task(desc.current_task_id);
+ match task.kind {
+ RenderTaskKind::Picture(ref info) => {
+ let available_cmd_buffers = &mut self.available_cmd_buffers[key.sub_slice_index.as_usize()];
+ available_cmd_buffers.push((desc.dirty_rect, info.cmd_buffer_index));
+ }
+ _ => unreachable!("bug: not a picture"),
+ }
+ }
+ }
+ CommandBufferBuilderKind::Simple { render_task_id, dirty_rect, .. } => {
+ let task = rg_builder.get_task(render_task_id);
+ match task.kind {
+ RenderTaskKind::Picture(ref info) => {
+ for sub_slice_buffer in &mut self.available_cmd_buffers {
+ sub_slice_buffer.push((dirty_rect, info.cmd_buffer_index));
+ }
+ }
+ _ => unreachable!("bug: not a picture"),
+ }
+ }
+ CommandBufferBuilderKind::Invalid => {}
+ };
+ }
+
+ /// For a given rect and sub-slice, get a list of command buffers to write commands to
+ fn get_cmd_buffer_targets_for_rect(
+ &mut self,
+ rect: &PictureRect,
+ sub_slice_index: SubSliceIndex,
+ targets: &mut Vec<CommandBufferIndex>,
+ ) -> bool {
+
+ for (dirty_rect, cmd_buffer_index) in &self.available_cmd_buffers[sub_slice_index.as_usize()] {
+ if dirty_rect.intersects(rect) {
+ targets.push(*cmd_buffer_index);
+ }
+ }
+
+ !targets.is_empty()
+ }
+}
+
+// Main helper interface to build a graph of surfaces. In future patches this
+// will support building sub-graphs.
+pub struct SurfaceBuilder {
+ // The currently set cmd buffer targets (updated during push/pop)
+ current_cmd_buffers: CommandBufferTargets,
+ // Stack of surfaces that are parents to the current targets
+ builder_stack: Vec<CommandBufferBuilder>,
+ // A map of the output render tasks from any sub-graphs that haven't
+ // been consumed by BackdropRender prims yet
+ pub sub_graph_output_map: FastHashMap<PictureIndex, RenderTaskId>,
+}
+
+impl SurfaceBuilder {
+ pub fn new() -> Self {
+ SurfaceBuilder {
+ current_cmd_buffers: CommandBufferTargets::new(),
+ builder_stack: Vec::new(),
+ sub_graph_output_map: FastHashMap::default(),
+ }
+ }
+
+ /// Register the current surface as the source of a resolve for the task sub-graph that
+ /// is currently on the surface builder stack.
+ pub fn register_resolve_source(
+ &mut self,
+ ) {
+ let surface_task_id = match self.builder_stack.last().unwrap().kind {
+ CommandBufferBuilderKind::Tiled { .. } | CommandBufferBuilderKind::Invalid => {
+ panic!("bug: only supported for non-tiled surfaces");
+ }
+ CommandBufferBuilderKind::Simple { render_task_id, .. } => render_task_id,
+ };
+
+ for builder in self.builder_stack.iter_mut().rev() {
+ if builder.establishes_sub_graph {
+ assert_eq!(builder.resolve_source, None);
+ builder.resolve_source = Some(surface_task_id);
+ return;
+ }
+ }
+
+ unreachable!("bug: resolve source with no sub-graph");
+ }
+
+ pub fn push_surface(
+ &mut self,
+ surface_index: SurfaceIndex,
+ is_sub_graph: bool,
+ clipping_rect: PictureRect,
+ descriptor: SurfaceDescriptor,
+ surfaces: &mut [SurfaceInfo],
+ rg_builder: &RenderTaskGraphBuilder,
+ ) {
+ // Init the surface
+ surfaces[surface_index.0].clipping_rect = clipping_rect;
+
+ let builder = match descriptor.kind {
+ SurfaceDescriptorKind::Tiled { tiles } => {
+ CommandBufferBuilder::new_tiled(
+ tiles,
+ )
+ }
+ SurfaceDescriptorKind::Simple { render_task_id, dirty_rect, .. } => {
+ CommandBufferBuilder::new_simple(
+ render_task_id,
+ is_sub_graph,
+ None,
+ dirty_rect,
+ )
+ }
+ SurfaceDescriptorKind::Chained { render_task_id, root_task_id, dirty_rect, .. } => {
+ CommandBufferBuilder::new_simple(
+ render_task_id,
+ is_sub_graph,
+ Some(root_task_id),
+ dirty_rect,
+ )
+ }
+ };
+
+ self.current_cmd_buffers.init(&builder, rg_builder);
+ self.builder_stack.push(builder);
+ }
+
+ // Add a child render task (e.g. a render task cache item, or a clip mask) as a
+ // dependency of the current surface
+ pub fn add_child_render_task(
+ &mut self,
+ child_task_id: RenderTaskId,
+ rg_builder: &mut RenderTaskGraphBuilder,
+ ) {
+ let builder = self.builder_stack.last().unwrap();
+
+ match builder.kind {
+ CommandBufferBuilderKind::Tiled { ref tiles } => {
+ for (_, descriptor) in tiles {
+ rg_builder.add_dependency(
+ descriptor.current_task_id,
+ child_task_id,
+ );
+ }
+ }
+ CommandBufferBuilderKind::Simple { render_task_id, .. } => {
+ rg_builder.add_dependency(
+ render_task_id,
+ child_task_id,
+ );
+ }
+ CommandBufferBuilderKind::Invalid { .. } => {}
+ }
+ }
+
+ // Add a picture render task as a dependency of the parent surface. This is a
+ // special case with extra complexity as the root of the surface may change
+ // when inside a sub-graph. It's currently only needed for drop-shadow effects.
+ pub fn add_picture_render_task(
+ &mut self,
+ child_task_id: RenderTaskId,
+ ) {
+ self.builder_stack
+ .last_mut()
+ .unwrap()
+ .extra_dependencies
+ .push(child_task_id);
+ }
+
+ // Get a list of command buffer indices that primitives should be pushed
+ // to for a given current visbility / dirty state
+ pub fn get_cmd_buffer_targets_for_prim(
+ &mut self,
+ vis: &PrimitiveVisibility,
+ targets: &mut Vec<CommandBufferIndex>,
+ ) -> bool {
+ targets.clear();
+
+ match vis.state {
+ VisibilityState::Unset => {
+ panic!("bug: invalid vis state");
+ }
+ VisibilityState::Culled => {
+ false
+ }
+ VisibilityState::Visible { sub_slice_index, .. } => {
+ self.current_cmd_buffers.get_cmd_buffer_targets_for_rect(
+ &vis.clip_chain.pic_coverage_rect,
+ sub_slice_index,
+ targets,
+ )
+ }
+ VisibilityState::PassThrough => {
+ true
+ }
+ }
+ }
+
+ // Finish adding primitives and child tasks to a surface and pop it off the stack
+ pub fn pop_surface(
+ &mut self,
+ pic_index: PictureIndex,
+ rg_builder: &mut RenderTaskGraphBuilder,
+ cmd_buffers: &mut CommandBufferList,
+ ) {
+ let builder = self.builder_stack.pop().unwrap();
+
+ if builder.establishes_sub_graph {
+ // If we are popping a sub-graph off the stack the dependency setup is rather more complex...
+ match builder.kind {
+ CommandBufferBuilderKind::Tiled { .. } | CommandBufferBuilderKind::Invalid => {
+ unreachable!("bug: sub-graphs can only be simple surfaces");
+ }
+ CommandBufferBuilderKind::Simple { render_task_id: child_render_task_id, root_task_id: child_root_task_id, .. } => {
+ // Get info about the resolve operation to copy from parent surface or tiles to the picture cache task
+ if let Some(resolve_task_id) = builder.resolve_source {
+ let mut src_task_ids = Vec::new();
+
+ // Make the output of the sub-graph a dependency of the new replacement tile task
+ let _old = self.sub_graph_output_map.insert(
+ pic_index,
+ child_root_task_id.unwrap_or(child_render_task_id),
+ );
+ debug_assert!(_old.is_none());
+
+ // Set up dependencies for the sub-graph. The basic concepts below are the same, but for
+ // tiled surfaces are a little more complex as there are multiple tasks to set up.
+ // (a) Set up new task(s) on parent surface that write to the same location
+ // (b) Set up a resolve target to copy from parent surface tasks(s) to the resolve target
+ // (c) Make the old parent surface tasks input dependencies of the resolve target
+ // (d) Make the sub-graph output an input dependency of the new task(s).
+
+ match self.builder_stack.last_mut().unwrap().kind {
+ CommandBufferBuilderKind::Tiled { ref mut tiles } => {
+ let keys: Vec<TileKey> = tiles.keys().cloned().collect();
+
+ // For each tile in parent surface
+ for key in keys {
+ let descriptor = tiles.remove(&key).unwrap();
+ let parent_task_id = descriptor.current_task_id;
+ let parent_task = rg_builder.get_task_mut(parent_task_id);
+
+ match parent_task.location {
+ RenderTaskLocation::Unallocated { .. } | RenderTaskLocation::Existing { .. } => {
+ // Get info about the parent tile task location and params
+ let location = RenderTaskLocation::Existing {
+ parent_task_id,
+ size: parent_task.location.size(),
+ };
+
+ let pic_task = match parent_task.kind {
+ RenderTaskKind::Picture(ref mut pic_task) => {
+ let cmd_buffer_index = cmd_buffers.create_cmd_buffer();
+ let new_pic_task = pic_task.duplicate(cmd_buffer_index);
+
+ // Add the resolve src to copy from tile -> picture input task
+ src_task_ids.push(parent_task_id);
+
+ new_pic_task
+ }
+ _ => panic!("bug: not a picture"),
+ };
+
+ // Make the existing tile an input dependency of the resolve target
+ rg_builder.add_dependency(
+ resolve_task_id,
+ parent_task_id,
+ );
+
+ // Create the new task to replace the tile task
+ let new_task_id = rg_builder.add().init(
+ RenderTask::new(
+ location, // draw to same place
+ RenderTaskKind::Picture(pic_task),
+ ),
+ );
+
+ // Ensure that the parent task will get scheduled earlier during
+ // pass assignment since we are reusing the existing surface,
+ // even though it's not technically needed for rendering order.
+ rg_builder.add_dependency(
+ new_task_id,
+ parent_task_id,
+ );
+
+ // Update the surface builder with the now current target for future primitives
+ tiles.insert(
+ key,
+ SurfaceTileDescriptor {
+ current_task_id: new_task_id,
+ ..descriptor
+ },
+ );
+ }
+ RenderTaskLocation::Static { .. } => {
+ // Update the surface builder with the now current target for future primitives
+ tiles.insert(
+ key,
+ descriptor,
+ );
+ }
+ _ => {
+ panic!("bug: unexpected task location");
+ }
+ }
+ }
+ }
+ CommandBufferBuilderKind::Simple { render_task_id: ref mut parent_task_id, .. } => {
+ let parent_task = rg_builder.get_task_mut(*parent_task_id);
+
+ // Get info about the parent tile task location and params
+ let location = RenderTaskLocation::Existing {
+ parent_task_id: *parent_task_id,
+ size: parent_task.location.size(),
+ };
+ let pic_task = match parent_task.kind {
+ RenderTaskKind::Picture(ref mut pic_task) => {
+ let cmd_buffer_index = cmd_buffers.create_cmd_buffer();
+
+ let new_pic_task = pic_task.duplicate(cmd_buffer_index);
+
+ // Add the resolve src to copy from tile -> picture input task
+ src_task_ids.push(*parent_task_id);
+
+ new_pic_task
+ }
+ _ => panic!("bug: not a picture"),
+ };
+
+ // Make the existing surface an input dependency of the resolve target
+ rg_builder.add_dependency(
+ resolve_task_id,
+ *parent_task_id,
+ );
+
+ // Create the new task to replace the parent surface task
+ let new_task_id = rg_builder.add().init(
+ RenderTask::new(
+ location, // draw to same place
+ RenderTaskKind::Picture(pic_task),
+ ),
+ );
+
+ // Ensure that the parent task will get scheduled earlier during
+ // pass assignment since we are reusing the existing surface,
+ // even though it's not technically needed for rendering order.
+ rg_builder.add_dependency(
+ new_task_id,
+ *parent_task_id,
+ );
+
+ // Update the surface builder with the now current target for future primitives
+ *parent_task_id = new_task_id;
+ }
+ CommandBufferBuilderKind::Invalid => {
+ unreachable!();
+ }
+ }
+
+ let dest_task = rg_builder.get_task_mut(resolve_task_id);
+
+ match dest_task.kind {
+ RenderTaskKind::Picture(ref mut dest_task_info) => {
+ assert!(dest_task_info.resolve_op.is_none());
+ dest_task_info.resolve_op = Some(ResolveOp {
+ src_task_ids,
+ dest_task_id: resolve_task_id,
+ })
+ }
+ _ => {
+ unreachable!("bug: not a picture");
+ }
+ }
+ }
+
+ // This can occur if there is an edge case where the resolve target is found
+ // not visible even though the filter chain was (for example, in the case of
+ // an extreme scale causing floating point inaccuracies). Adding a dependency
+ // here is also a safety in case for some reason the backdrop render primitive
+ // doesn't pick up the dependency, ensuring that it gets scheduled and freed
+ // as early as possible.
+ match self.builder_stack.last().unwrap().kind {
+ CommandBufferBuilderKind::Tiled { ref tiles } => {
+ // For a tiled render task, add as a dependency to every tile.
+ for (_, descriptor) in tiles {
+ rg_builder.add_dependency(
+ descriptor.current_task_id,
+ child_root_task_id.unwrap_or(child_render_task_id),
+ );
+ }
+ }
+ CommandBufferBuilderKind::Simple { render_task_id: parent_task_id, .. } => {
+ rg_builder.add_dependency(
+ parent_task_id,
+ child_root_task_id.unwrap_or(child_render_task_id),
+ );
+ }
+ CommandBufferBuilderKind::Invalid => {
+ unreachable!();
+ }
+ }
+ }
+ }
+ } else {
+ match builder.kind {
+ CommandBufferBuilderKind::Tiled { ref tiles } => {
+ for (_, descriptor) in tiles {
+ if let Some(composite_task_id) = descriptor.composite_task_id {
+ rg_builder.add_dependency(
+ composite_task_id,
+ descriptor.current_task_id,
+ );
+
+ let composite_task = rg_builder.get_task_mut(composite_task_id);
+ match composite_task.kind {
+ RenderTaskKind::TileComposite(ref mut info) => {
+ info.task_id = Some(descriptor.current_task_id);
+ }
+ _ => unreachable!("bug: not a tile composite"),
+ }
+ }
+ }
+ }
+ CommandBufferBuilderKind::Simple { render_task_id: child_task_id, root_task_id: child_root_task_id, .. } => {
+ match self.builder_stack.last().unwrap().kind {
+ CommandBufferBuilderKind::Tiled { ref tiles } => {
+ // For a tiled render task, add as a dependency to every tile.
+ for (_, descriptor) in tiles {
+ rg_builder.add_dependency(
+ descriptor.current_task_id,
+ child_root_task_id.unwrap_or(child_task_id),
+ );
+ }
+ }
+ CommandBufferBuilderKind::Simple { render_task_id: parent_task_id, .. } => {
+ rg_builder.add_dependency(
+ parent_task_id,
+ child_root_task_id.unwrap_or(child_task_id),
+ );
+ }
+ CommandBufferBuilderKind::Invalid => {
+ unreachable!();
+ }
+ }
+ }
+ CommandBufferBuilderKind::Invalid => {
+ unreachable!();
+ }
+ }
+ }
+
+ // Step through the dependencies for this builder and add them to the finalized
+ // render task root(s) for this surface
+ match builder.kind {
+ CommandBufferBuilderKind::Tiled { ref tiles } => {
+ for (_, descriptor) in tiles {
+ for task_id in &builder.extra_dependencies {
+ rg_builder.add_dependency(
+ descriptor.current_task_id,
+ *task_id,
+ );
+ }
+ }
+ }
+ CommandBufferBuilderKind::Simple { render_task_id, .. } => {
+ for task_id in &builder.extra_dependencies {
+ rg_builder.add_dependency(
+ render_task_id,
+ *task_id,
+ );
+ }
+ }
+ CommandBufferBuilderKind::Invalid { .. } => {}
+ }
+
+ // Set up the cmd-buffer targets to write prims into the popped surface
+ self.current_cmd_buffers.init(
+ self.builder_stack.last().unwrap_or(&CommandBufferBuilder::empty()), rg_builder
+ );
+ }
+
+ pub fn finalize(self) {
+ assert!(self.builder_stack.is_empty());
+ }
+}
diff --git a/gfx/wr/webrender/src/telemetry.rs b/gfx/wr/webrender/src/telemetry.rs
new file mode 100644
index 0000000000..9b4c02d68e
--- /dev/null
+++ b/gfx/wr/webrender/src/telemetry.rs
@@ -0,0 +1,41 @@
+/* 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 glean::TimerId;
+#[cfg(feature = "gecko")]
+use firefox_on_glean::metrics::wr;
+use std::time::Duration;
+
+pub struct Telemetry;
+
+/// Defines the interface for hooking up an external telemetry reporter to WR.
+#[cfg(not(feature = "gecko"))]
+impl Telemetry {
+ pub fn record_rasterize_blobs_time(_duration: Duration) { }
+ pub fn start_framebuild_time() -> TimerId { TimerId { id: 0 } }
+ pub fn stop_and_accumulate_framebuild_time(_id: TimerId) { }
+ pub fn record_renderer_time(_duration: Duration) { }
+ pub fn record_renderer_time_no_sc(_duration: Duration) { }
+ pub fn record_scenebuild_time(_duration: Duration) { }
+ pub fn start_sceneswap_time() -> TimerId { TimerId { id: 0 } }
+ pub fn stop_and_accumulate_sceneswap_time(_id: TimerId) { }
+ pub fn cancel_sceneswap_time(_id: TimerId) { }
+ pub fn record_texture_cache_update_time(_duration: Duration) { }
+ pub fn record_time_to_frame_build(_duration: Duration) { }
+}
+
+#[cfg(feature = "gecko")]
+impl Telemetry {
+ pub fn record_rasterize_blobs_time(duration: Duration) { wr::rasterize_blobs_time.accumulate_raw_duration(duration); }
+ pub fn start_framebuild_time() -> TimerId { wr::framebuild_time.start() }
+ pub fn stop_and_accumulate_framebuild_time(id: TimerId) { wr::framebuild_time.stop_and_accumulate(id); }
+ pub fn record_renderer_time(duration: Duration) { wr::renderer_time.accumulate_raw_duration(duration); }
+ pub fn record_renderer_time_no_sc(duration: Duration) { wr::renderer_time_no_sc.accumulate_raw_duration(duration); }
+ pub fn record_scenebuild_time(duration: Duration) { wr::scenebuild_time.accumulate_raw_duration(duration); }
+ pub fn start_sceneswap_time() -> TimerId { wr::sceneswap_time.start() }
+ pub fn stop_and_accumulate_sceneswap_time(id: TimerId) { wr::sceneswap_time.stop_and_accumulate(id); }
+ pub fn cancel_sceneswap_time(id: TimerId) { wr::sceneswap_time.cancel(id); }
+ pub fn record_texture_cache_update_time(duration: Duration) { wr::texture_cache_update_time.accumulate_raw_duration(duration); }
+ pub fn record_time_to_frame_build(duration: Duration) { wr::time_to_frame_build.accumulate_raw_duration(duration); }
+}
diff --git a/gfx/wr/webrender/src/texture_cache.rs b/gfx/wr/webrender/src/texture_cache.rs
new file mode 100644
index 0000000000..77b88cc08b
--- /dev/null
+++ b/gfx/wr/webrender/src/texture_cache.rs
@@ -0,0 +1,1707 @@
+/* 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 api::{DirtyRect, ExternalImageType, ImageFormat, ImageBufferKind};
+use api::{DebugFlags, ImageDescriptor};
+use api::units::*;
+#[cfg(test)]
+use api::{DocumentId, IdNamespace};
+use crate::device::{TextureFilter, TextureFormatPair};
+use crate::freelist::{FreeList, FreeListHandle, WeakFreeListHandle};
+use crate::gpu_cache::{GpuCache, GpuCacheHandle};
+use crate::gpu_types::{ImageSource, UvRectKind};
+use crate::internal_types::{
+ CacheTextureId, Swizzle, SwizzleSettings, FrameStamp, FrameId,
+ TextureUpdateList, TextureUpdateSource, TextureSource,
+ TextureCacheAllocInfo, TextureCacheUpdate, TextureCacheCategory,
+};
+use crate::lru_cache::LRUCache;
+use crate::profiler::{self, TransactionProfile};
+use crate::resource_cache::{CacheItem, CachedImageData};
+use crate::texture_pack::{
+ AllocatorList, AllocId, AtlasAllocatorList, ShelfAllocator, ShelfAllocatorOptions,
+};
+use std::cell::Cell;
+use std::mem;
+use std::rc::Rc;
+use euclid::size2;
+use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
+
+/// Information about which shader will use the entry.
+///
+/// For batching purposes, it's beneficial to group some items in their
+/// own textures if we know that they are used by a specific shader.
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum TargetShader {
+ Default,
+ Text,
+}
+
+/// The size of each region in shared cache texture arrays.
+pub const TEXTURE_REGION_DIMENSIONS: i32 = 512;
+
+/// Items in the texture cache can either be standalone textures,
+/// or a sub-rect inside the shared cache.
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum EntryDetails {
+ Standalone {
+ /// Number of bytes this entry allocates
+ size_in_bytes: usize,
+ },
+ Cache {
+ /// Origin within the texture layer where this item exists.
+ origin: DeviceIntPoint,
+ /// ID of the allocation specific to its allocator.
+ alloc_id: AllocId,
+ /// The allocated size in bytes for this entry.
+ allocated_size_in_bytes: usize,
+ },
+}
+
+impl EntryDetails {
+ fn describe(&self) -> DeviceIntPoint {
+ match *self {
+ EntryDetails::Standalone { .. } => DeviceIntPoint::zero(),
+ EntryDetails::Cache { origin, .. } => origin,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum AutoCacheEntryMarker {}
+
+#[derive(Debug, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum ManualCacheEntryMarker {}
+
+// Stores information related to a single entry in the texture
+// cache. This is stored for each item whether it's in the shared
+// cache or a standalone texture.
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct CacheEntry {
+ /// Size of the requested item, in device pixels. Does not include any
+ /// padding for alignment that the allocator may have added to this entry's
+ /// allocation.
+ pub size: DeviceIntSize,
+ /// Details specific to standalone or shared items.
+ pub details: EntryDetails,
+ /// Arbitrary user data associated with this item.
+ pub user_data: [f32; 4],
+ /// The last frame this item was requested for rendering.
+ // TODO(gw): This stamp is only used for picture cache tiles, and some checks
+ // in the glyph cache eviction code. We could probably remove it
+ // entirely in future (or move to PictureCacheEntry).
+ pub last_access: FrameStamp,
+ /// Handle to the resource rect in the GPU cache.
+ pub uv_rect_handle: GpuCacheHandle,
+ /// Image format of the data that the entry expects.
+ pub input_format: ImageFormat,
+ pub filter: TextureFilter,
+ pub swizzle: Swizzle,
+ /// The actual device texture ID this is part of.
+ pub texture_id: CacheTextureId,
+ /// Optional notice when the entry is evicted from the cache.
+ pub eviction_notice: Option<EvictionNotice>,
+ /// The type of UV rect this entry specifies.
+ pub uv_rect_kind: UvRectKind,
+
+ pub shader: TargetShader,
+}
+
+malloc_size_of::malloc_size_of_is_0!(
+ CacheEntry,
+ AutoCacheEntryMarker, ManualCacheEntryMarker
+);
+
+impl CacheEntry {
+ // Create a new entry for a standalone texture.
+ fn new_standalone(
+ texture_id: CacheTextureId,
+ last_access: FrameStamp,
+ params: &CacheAllocParams,
+ swizzle: Swizzle,
+ size_in_bytes: usize,
+ ) -> Self {
+ CacheEntry {
+ size: params.descriptor.size,
+ user_data: params.user_data,
+ last_access,
+ details: EntryDetails::Standalone {
+ size_in_bytes,
+ },
+ texture_id,
+ input_format: params.descriptor.format,
+ filter: params.filter,
+ swizzle,
+ uv_rect_handle: GpuCacheHandle::new(),
+ eviction_notice: None,
+ uv_rect_kind: params.uv_rect_kind,
+ shader: TargetShader::Default,
+ }
+ }
+
+ // Update the GPU cache for this texture cache entry.
+ // This ensures that the UV rect, and texture layer index
+ // are up to date in the GPU cache for vertex shaders
+ // to fetch from.
+ fn update_gpu_cache(&mut self, gpu_cache: &mut GpuCache) {
+ if let Some(mut request) = gpu_cache.request(&mut self.uv_rect_handle) {
+ let origin = self.details.describe();
+ let image_source = ImageSource {
+ p0: origin.to_f32(),
+ p1: (origin + self.size).to_f32(),
+ user_data: self.user_data,
+ uv_rect_kind: self.uv_rect_kind,
+ };
+ image_source.write_gpu_blocks(&mut request);
+ }
+ }
+
+ fn evict(&self) {
+ if let Some(eviction_notice) = self.eviction_notice.as_ref() {
+ eviction_notice.notify();
+ }
+ }
+
+ fn alternative_input_format(&self) -> ImageFormat {
+ match self.input_format {
+ ImageFormat::RGBA8 => ImageFormat::BGRA8,
+ ImageFormat::BGRA8 => ImageFormat::RGBA8,
+ other => other,
+ }
+ }
+}
+
+
+/// A texture cache handle is a weak reference to a cache entry.
+///
+/// If the handle has not been inserted into the cache yet, or if the entry was
+/// previously inserted and then evicted, lookup of the handle will fail, and
+/// the cache handle needs to re-upload this item to the texture cache (see
+/// request() below).
+
+#[derive(MallocSizeOf,Clone,PartialEq,Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum TextureCacheHandle {
+ /// A fresh handle.
+ Empty,
+
+ /// A handle for an entry with automatic eviction.
+ Auto(WeakFreeListHandle<AutoCacheEntryMarker>),
+
+ /// A handle for an entry with manual eviction.
+ Manual(WeakFreeListHandle<ManualCacheEntryMarker>)
+}
+
+impl TextureCacheHandle {
+ pub fn invalid() -> Self {
+ TextureCacheHandle::Empty
+ }
+}
+
+/// Describes the eviction policy for a given entry in the texture cache.
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum Eviction {
+ /// The entry will be evicted under the normal rules (which differ between
+ /// standalone and shared entries).
+ Auto,
+ /// The entry will not be evicted until the policy is explicitly set to a
+ /// different value.
+ Manual,
+}
+
+// An eviction notice is a shared condition useful for detecting
+// when a TextureCacheHandle gets evicted from the TextureCache.
+// It is optionally installed to the TextureCache when an update()
+// is scheduled. A single notice may be shared among any number of
+// TextureCacheHandle updates. The notice may then be subsequently
+// checked to see if any of the updates using it have been evicted.
+#[derive(Clone, Debug, Default)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct EvictionNotice {
+ evicted: Rc<Cell<bool>>,
+}
+
+impl EvictionNotice {
+ fn notify(&self) {
+ self.evicted.set(true);
+ }
+
+ pub fn check(&self) -> bool {
+ if self.evicted.get() {
+ self.evicted.set(false);
+ true
+ } else {
+ false
+ }
+ }
+}
+
+/// The different budget types for the texture cache. Each type has its own
+/// memory budget. Once the budget is exceeded, entries with automatic eviction
+/// are evicted. Entries with manual eviction share the same budget but are not
+/// evicted once the budget is exceeded.
+/// Keeping separate budgets ensures that we don't evict entries from unrelated
+/// textures if one texture gets full.
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[repr(u8)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+enum BudgetType {
+ SharedColor8Linear,
+ SharedColor8Nearest,
+ SharedColor8Glyphs,
+ SharedAlpha8,
+ SharedAlpha8Glyphs,
+ SharedAlpha16,
+ Standalone,
+}
+
+impl BudgetType {
+ pub const COUNT: usize = 7;
+
+ pub const VALUES: [BudgetType; BudgetType::COUNT] = [
+ BudgetType::SharedColor8Linear,
+ BudgetType::SharedColor8Nearest,
+ BudgetType::SharedColor8Glyphs,
+ BudgetType::SharedAlpha8,
+ BudgetType::SharedAlpha8Glyphs,
+ BudgetType::SharedAlpha16,
+ BudgetType::Standalone,
+ ];
+
+ pub const PRESSURE_COUNTERS: [usize; BudgetType::COUNT] = [
+ profiler::ATLAS_COLOR8_LINEAR_PRESSURE,
+ profiler::ATLAS_COLOR8_NEAREST_PRESSURE,
+ profiler::ATLAS_COLOR8_GLYPHS_PRESSURE,
+ profiler::ATLAS_ALPHA8_PRESSURE,
+ profiler::ATLAS_ALPHA8_GLYPHS_PRESSURE,
+ profiler::ATLAS_ALPHA16_PRESSURE,
+ profiler::ATLAS_STANDALONE_PRESSURE,
+ ];
+
+ pub fn iter() -> impl Iterator<Item = BudgetType> {
+ BudgetType::VALUES.iter().cloned()
+ }
+}
+
+/// A set of lazily allocated, fixed size, texture arrays for each format the
+/// texture cache supports.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct SharedTextures {
+ color8_nearest: AllocatorList<ShelfAllocator, TextureParameters>,
+ alpha8_linear: AllocatorList<ShelfAllocator, TextureParameters>,
+ alpha8_glyphs: AllocatorList<ShelfAllocator, TextureParameters>,
+ alpha16_linear: AllocatorList<ShelfAllocator, TextureParameters>,
+ color8_linear: AllocatorList<ShelfAllocator, TextureParameters>,
+ color8_glyphs: AllocatorList<ShelfAllocator, TextureParameters>,
+ bytes_per_texture_of_type: [i32 ; BudgetType::COUNT],
+ next_compaction_idx: usize,
+}
+
+impl SharedTextures {
+ /// Mints a new set of shared textures.
+ fn new(color_formats: TextureFormatPair<ImageFormat>, config: &TextureCacheConfig) -> Self {
+ let mut bytes_per_texture_of_type = [0 ; BudgetType::COUNT];
+
+ // Used primarily for cached shadow masks. There can be lots of
+ // these on some pages like francine, but most pages don't use it
+ // much.
+ // Most content tends to fit into two 512x512 textures. We are
+ // conservatively using 1024x1024 to fit everything in a single
+ // texture and avoid breaking batches, but it's worth checking
+ // whether it would actually lead to a lot of batch breaks in
+ // practice.
+ let alpha8_linear = AllocatorList::new(
+ config.alpha8_texture_size,
+ ShelfAllocatorOptions {
+ num_columns: 1,
+ alignment: size2(8, 8),
+ .. ShelfAllocatorOptions::default()
+ },
+ TextureParameters {
+ formats: TextureFormatPair::from(ImageFormat::R8),
+ filter: TextureFilter::Linear,
+ },
+ );
+ bytes_per_texture_of_type[BudgetType::SharedAlpha8 as usize] =
+ config.alpha8_texture_size * config.alpha8_texture_size;
+
+ // The cache for alpha glyphs (separate to help with batching).
+ let alpha8_glyphs = AllocatorList::new(
+ config.alpha8_glyph_texture_size,
+ ShelfAllocatorOptions {
+ num_columns: if config.alpha8_glyph_texture_size >= 1024 { 2 } else { 1 },
+ alignment: size2(4, 8),
+ .. ShelfAllocatorOptions::default()
+ },
+ TextureParameters {
+ formats: TextureFormatPair::from(ImageFormat::R8),
+ filter: TextureFilter::Linear,
+ },
+ );
+ bytes_per_texture_of_type[BudgetType::SharedAlpha8Glyphs as usize] =
+ config.alpha8_glyph_texture_size * config.alpha8_glyph_texture_size;
+
+ // Used for experimental hdr yuv texture support, but not used in
+ // production Firefox.
+ let alpha16_linear = AllocatorList::new(
+ config.alpha16_texture_size,
+ ShelfAllocatorOptions {
+ num_columns: if config.alpha16_texture_size >= 1024 { 2 } else { 1 },
+ alignment: size2(8, 8),
+ .. ShelfAllocatorOptions::default()
+ },
+ TextureParameters {
+ formats: TextureFormatPair::from(ImageFormat::R16),
+ filter: TextureFilter::Linear,
+ },
+ );
+ bytes_per_texture_of_type[BudgetType::SharedAlpha16 as usize] =
+ ImageFormat::R16.bytes_per_pixel() *
+ config.alpha16_texture_size * config.alpha16_texture_size;
+
+ // The primary cache for images, etc.
+ let color8_linear = AllocatorList::new(
+ config.color8_linear_texture_size,
+ ShelfAllocatorOptions {
+ num_columns: if config.color8_linear_texture_size >= 1024 { 2 } else { 1 },
+ alignment: size2(16, 16),
+ .. ShelfAllocatorOptions::default()
+ },
+ TextureParameters {
+ formats: color_formats.clone(),
+ filter: TextureFilter::Linear,
+ },
+ );
+ bytes_per_texture_of_type[BudgetType::SharedColor8Linear as usize] =
+ color_formats.internal.bytes_per_pixel() *
+ config.color8_linear_texture_size * config.color8_linear_texture_size;
+
+ // The cache for subpixel-AA and bitmap glyphs (separate to help with batching).
+ let color8_glyphs = AllocatorList::new(
+ config.color8_glyph_texture_size,
+ ShelfAllocatorOptions {
+ num_columns: if config.color8_glyph_texture_size >= 1024 { 2 } else { 1 },
+ alignment: size2(4, 8),
+ .. ShelfAllocatorOptions::default()
+ },
+ TextureParameters {
+ formats: color_formats.clone(),
+ filter: TextureFilter::Linear,
+ },
+ );
+ bytes_per_texture_of_type[BudgetType::SharedColor8Glyphs as usize] =
+ color_formats.internal.bytes_per_pixel() *
+ config.color8_glyph_texture_size * config.color8_glyph_texture_size;
+
+ // Used for image-rendering: crisp. This is mostly favicons, which
+ // are small. Some other images use it too, but those tend to be
+ // larger than 512x512 and thus don't use the shared cache anyway.
+ let color8_nearest = AllocatorList::new(
+ config.color8_nearest_texture_size,
+ ShelfAllocatorOptions::default(),
+ TextureParameters {
+ formats: color_formats.clone(),
+ filter: TextureFilter::Nearest,
+ }
+ );
+ bytes_per_texture_of_type[BudgetType::SharedColor8Nearest as usize] =
+ color_formats.internal.bytes_per_pixel() *
+ config.color8_nearest_texture_size * config.color8_nearest_texture_size;
+
+ Self {
+ alpha8_linear,
+ alpha8_glyphs,
+ alpha16_linear,
+ color8_linear,
+ color8_glyphs,
+ color8_nearest,
+ bytes_per_texture_of_type,
+ next_compaction_idx: 0,
+ }
+ }
+
+ /// Clears each texture in the set, with the given set of pending updates.
+ fn clear(&mut self, updates: &mut TextureUpdateList) {
+ let texture_dealloc_cb = &mut |texture_id| {
+ updates.push_free(texture_id);
+ };
+
+ self.alpha8_linear.clear(texture_dealloc_cb);
+ self.alpha8_glyphs.clear(texture_dealloc_cb);
+ self.alpha16_linear.clear(texture_dealloc_cb);
+ self.color8_linear.clear(texture_dealloc_cb);
+ self.color8_nearest.clear(texture_dealloc_cb);
+ self.color8_glyphs.clear(texture_dealloc_cb);
+ }
+
+ /// Returns a mutable borrow for the shared texture array matching the parameters.
+ fn select(
+ &mut self, external_format: ImageFormat, filter: TextureFilter, shader: TargetShader,
+ ) -> (&mut dyn AtlasAllocatorList<TextureParameters>, BudgetType) {
+ match external_format {
+ ImageFormat::R8 => {
+ assert_eq!(filter, TextureFilter::Linear);
+ match shader {
+ TargetShader::Text => {
+ (&mut self.alpha8_glyphs, BudgetType::SharedAlpha8Glyphs)
+ },
+ _ => (&mut self.alpha8_linear, BudgetType::SharedAlpha8),
+ }
+ }
+ ImageFormat::R16 => {
+ assert_eq!(filter, TextureFilter::Linear);
+ (&mut self.alpha16_linear, BudgetType::SharedAlpha16)
+ }
+ ImageFormat::RGBA8 |
+ ImageFormat::BGRA8 => {
+ match (filter, shader) {
+ (TextureFilter::Linear, TargetShader::Text) => {
+ (&mut self.color8_glyphs, BudgetType::SharedColor8Glyphs)
+ },
+ (TextureFilter::Linear, _) => {
+ (&mut self.color8_linear, BudgetType::SharedColor8Linear)
+ },
+ (TextureFilter::Nearest, _) => {
+ (&mut self.color8_nearest, BudgetType::SharedColor8Nearest)
+ },
+ _ => panic!("Unexpected filter {:?}", filter),
+ }
+ }
+ _ => panic!("Unexpected format {:?}", external_format),
+ }
+ }
+
+ /// How many bytes a single texture of the given type takes up, for the
+ /// configured texture sizes.
+ fn bytes_per_shared_texture(&self, budget_type: BudgetType) -> usize {
+ self.bytes_per_texture_of_type[budget_type as usize] as usize
+ }
+
+ fn has_multiple_textures(&self, budget_type: BudgetType) -> bool {
+ match budget_type {
+ BudgetType::SharedColor8Linear => self.color8_linear.allocated_textures() > 1,
+ BudgetType::SharedColor8Nearest => self.color8_nearest.allocated_textures() > 1,
+ BudgetType::SharedColor8Glyphs => self.color8_glyphs.allocated_textures() > 1,
+ BudgetType::SharedAlpha8 => self.alpha8_linear.allocated_textures() > 1,
+ BudgetType::SharedAlpha8Glyphs => self.alpha8_glyphs.allocated_textures() > 1,
+ BudgetType::SharedAlpha16 => self.alpha16_linear.allocated_textures() > 1,
+ BudgetType::Standalone => false,
+ }
+ }
+}
+
+/// Container struct for the various parameters used in cache allocation.
+struct CacheAllocParams {
+ descriptor: ImageDescriptor,
+ filter: TextureFilter,
+ user_data: [f32; 4],
+ uv_rect_kind: UvRectKind,
+ shader: TargetShader,
+}
+
+/// Startup parameters for the texture cache.
+///
+/// Texture sizes must be at least 512.
+#[derive(Clone)]
+pub struct TextureCacheConfig {
+ pub color8_linear_texture_size: i32,
+ pub color8_nearest_texture_size: i32,
+ pub color8_glyph_texture_size: i32,
+ pub alpha8_texture_size: i32,
+ pub alpha8_glyph_texture_size: i32,
+ pub alpha16_texture_size: i32,
+}
+
+impl TextureCacheConfig {
+ pub const DEFAULT: Self = TextureCacheConfig {
+ color8_linear_texture_size: 2048,
+ color8_nearest_texture_size: 512,
+ color8_glyph_texture_size: 2048,
+ alpha8_texture_size: 1024,
+ alpha8_glyph_texture_size: 2048,
+ alpha16_texture_size: 512,
+ };
+}
+
+/// General-purpose manager for images in GPU memory. This includes images,
+/// rasterized glyphs, rasterized blobs, cached render tasks, etc.
+///
+/// The texture cache is owned and managed by the RenderBackend thread, and
+/// produces a series of commands to manipulate the textures on the Renderer
+/// thread. These commands are executed before any rendering is performed for
+/// a given frame.
+///
+/// Entries in the texture cache are not guaranteed to live past the end of the
+/// frame in which they are requested, and may be evicted. The API supports
+/// querying whether an entry is still available.
+///
+/// The TextureCache is different from the GpuCache in that the former stores
+/// images, whereas the latter stores data and parameters for use in the shaders.
+/// This means that the texture cache can be visualized, which is a good way to
+/// understand how it works. Enabling gfx.webrender.debug.texture-cache shows a
+/// live view of its contents in Firefox.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct TextureCache {
+ /// Set of texture arrays in different formats used for the shared cache.
+ shared_textures: SharedTextures,
+
+ /// Maximum texture size supported by hardware.
+ max_texture_size: i32,
+
+ /// Maximum texture size before it is considered preferable to break the
+ /// texture into tiles.
+ tiling_threshold: i32,
+
+ /// Settings on using texture unit swizzling.
+ swizzle: Option<SwizzleSettings>,
+
+ /// The current set of debug flags.
+ debug_flags: DebugFlags,
+
+ /// The next unused virtual texture ID. Monotonically increasing.
+ pub next_id: CacheTextureId,
+
+ /// A list of allocations and updates that need to be applied to the texture
+ /// cache in the rendering thread this frame.
+ #[cfg_attr(all(feature = "serde", any(feature = "capture", feature = "replay")), serde(skip))]
+ pub pending_updates: TextureUpdateList,
+
+ /// The current `FrameStamp`. Used for cache eviction policies.
+ now: FrameStamp,
+
+ /// Cache of texture cache handles with automatic lifetime management, evicted
+ /// in a least-recently-used order.
+ lru_cache: LRUCache<CacheEntry, AutoCacheEntryMarker>,
+
+ /// Cache of texture cache entries with manual liftime management.
+ manual_entries: FreeList<CacheEntry, ManualCacheEntryMarker>,
+
+ /// Strong handles for the manual_entries FreeList.
+ manual_handles: Vec<FreeListHandle<ManualCacheEntryMarker>>,
+
+ /// Memory usage of allocated entries in all of the shared or standalone
+ /// textures. Includes both manually and automatically evicted entries.
+ bytes_allocated: [usize ; BudgetType::COUNT],
+}
+
+impl TextureCache {
+ /// The maximum number of items that will be evicted per frame. This limit helps avoid jank
+ /// on frames where we want to evict a large number of items. Instead, we'd prefer to drop
+ /// the items incrementally over a number of frames, even if that means the total allocated
+ /// size of the cache is above the desired threshold for a small number of frames.
+ const MAX_EVICTIONS_PER_FRAME: usize = 32;
+
+ pub fn new(
+ max_texture_size: i32,
+ tiling_threshold: i32,
+ color_formats: TextureFormatPair<ImageFormat>,
+ swizzle: Option<SwizzleSettings>,
+ config: &TextureCacheConfig,
+ ) -> Self {
+ let pending_updates = TextureUpdateList::new();
+
+ // Shared texture cache controls swizzling on a per-entry basis, assuming that
+ // the texture as a whole doesn't need to be swizzled (but only some entries do).
+ // It would be possible to support this, but not needed at the moment.
+ assert!(color_formats.internal != ImageFormat::BGRA8 ||
+ swizzle.map_or(true, |s| s.bgra8_sampling_swizzle == Swizzle::default())
+ );
+
+ let next_texture_id = CacheTextureId(1);
+
+ TextureCache {
+ shared_textures: SharedTextures::new(color_formats, config),
+ max_texture_size,
+ tiling_threshold,
+ swizzle,
+ debug_flags: DebugFlags::empty(),
+ next_id: next_texture_id,
+ pending_updates,
+ now: FrameStamp::INVALID,
+ lru_cache: LRUCache::new(BudgetType::COUNT),
+ manual_entries: FreeList::new(),
+ manual_handles: Vec::new(),
+ bytes_allocated: [0 ; BudgetType::COUNT],
+ }
+ }
+
+ /// Creates a TextureCache and sets it up with a valid `FrameStamp`, which
+ /// is useful for avoiding panics when instantiating the `TextureCache`
+ /// directly from unit test code.
+ #[cfg(test)]
+ pub fn new_for_testing(
+ max_texture_size: i32,
+ image_format: ImageFormat,
+ ) -> Self {
+ let mut cache = Self::new(
+ max_texture_size,
+ max_texture_size,
+ TextureFormatPair::from(image_format),
+ None,
+ &TextureCacheConfig::DEFAULT,
+ );
+ let mut now = FrameStamp::first(DocumentId::new(IdNamespace(1), 1));
+ now.advance();
+ cache.begin_frame(now, &mut TransactionProfile::new());
+ cache
+ }
+
+ pub fn set_debug_flags(&mut self, flags: DebugFlags) {
+ self.debug_flags = flags;
+ }
+
+ /// Clear all entries in the texture cache. This is a fairly drastic
+ /// step that should only be called very rarely.
+ pub fn clear_all(&mut self) {
+ // Evict all manual eviction handles
+ let manual_handles = mem::replace(
+ &mut self.manual_handles,
+ Vec::new(),
+ );
+ for handle in manual_handles {
+ let entry = self.manual_entries.free(handle);
+ self.evict_impl(entry);
+ }
+
+ // Evict all auto (LRU) cache handles
+ for budget_type in BudgetType::iter() {
+ while let Some(entry) = self.lru_cache.pop_oldest(budget_type as u8) {
+ entry.evict();
+ self.free(&entry);
+ }
+ }
+
+ // Free the picture and shared textures
+ self.shared_textures.clear(&mut self.pending_updates);
+ self.pending_updates.note_clear();
+ }
+
+ /// Called at the beginning of each frame.
+ pub fn begin_frame(&mut self, stamp: FrameStamp, profile: &mut TransactionProfile) {
+ debug_assert!(!self.now.is_valid());
+ profile_scope!("begin_frame");
+ self.now = stamp;
+
+ // Texture cache eviction is done at the start of the frame. This ensures that
+ // we won't evict items that have been requested on this frame.
+ // It also frees up space in the cache for items allocated later in the frame
+ // potentially reducing texture allocations and fragmentation.
+ self.evict_items_from_cache_if_required(profile);
+ }
+
+ pub fn end_frame(&mut self, profile: &mut TransactionProfile) {
+ debug_assert!(self.now.is_valid());
+
+ let updates = &mut self.pending_updates; // To avoid referring to self in the closure.
+ let callback = &mut|texture_id| { updates.push_free(texture_id); };
+
+ // Release of empty shared textures is done at the end of the frame. That way, if the
+ // eviction at the start of the frame frees up a texture, that is then subsequently
+ // used during the frame, we avoid doing a free/alloc for it.
+ self.shared_textures.alpha8_linear.release_empty_textures(callback);
+ self.shared_textures.alpha8_glyphs.release_empty_textures(callback);
+ self.shared_textures.alpha16_linear.release_empty_textures(callback);
+ self.shared_textures.color8_linear.release_empty_textures(callback);
+ self.shared_textures.color8_nearest.release_empty_textures(callback);
+ self.shared_textures.color8_glyphs.release_empty_textures(callback);
+
+ for budget in BudgetType::iter() {
+ let threshold = self.get_eviction_threshold(budget);
+ let pressure = self.bytes_allocated[budget as usize] as f32 / threshold as f32;
+ profile.set(BudgetType::PRESSURE_COUNTERS[budget as usize], pressure);
+ }
+
+ profile.set(profiler::ATLAS_A8_PIXELS, self.shared_textures.alpha8_linear.allocated_space());
+ profile.set(profiler::ATLAS_A8_TEXTURES, self.shared_textures.alpha8_linear.allocated_textures());
+ profile.set(profiler::ATLAS_A8_GLYPHS_PIXELS, self.shared_textures.alpha8_glyphs.allocated_space());
+ profile.set(profiler::ATLAS_A8_GLYPHS_TEXTURES, self.shared_textures.alpha8_glyphs.allocated_textures());
+ profile.set(profiler::ATLAS_A16_PIXELS, self.shared_textures.alpha16_linear.allocated_space());
+ profile.set(profiler::ATLAS_A16_TEXTURES, self.shared_textures.alpha16_linear.allocated_textures());
+ profile.set(profiler::ATLAS_RGBA8_LINEAR_PIXELS, self.shared_textures.color8_linear.allocated_space());
+ profile.set(profiler::ATLAS_RGBA8_LINEAR_TEXTURES, self.shared_textures.color8_linear.allocated_textures());
+ profile.set(profiler::ATLAS_RGBA8_NEAREST_PIXELS, self.shared_textures.color8_nearest.allocated_space());
+ profile.set(profiler::ATLAS_RGBA8_NEAREST_TEXTURES, self.shared_textures.color8_nearest.allocated_textures());
+ profile.set(profiler::ATLAS_RGBA8_GLYPHS_PIXELS, self.shared_textures.color8_glyphs.allocated_space());
+ profile.set(profiler::ATLAS_RGBA8_GLYPHS_TEXTURES, self.shared_textures.color8_glyphs.allocated_textures());
+
+ let shared_bytes = [
+ BudgetType::SharedColor8Linear,
+ BudgetType::SharedColor8Nearest,
+ BudgetType::SharedColor8Glyphs,
+ BudgetType::SharedAlpha8,
+ BudgetType::SharedAlpha8Glyphs,
+ BudgetType::SharedAlpha16,
+ ].iter().map(|b| self.bytes_allocated[*b as usize]).sum();
+
+ profile.set(profiler::ATLAS_ITEMS_MEM, profiler::bytes_to_mb(shared_bytes));
+
+ self.now = FrameStamp::INVALID;
+ }
+
+ pub fn run_compaction(&mut self, gpu_cache: &mut GpuCache) {
+ // Use the same order as BudgetType::VALUES so that we can index self.bytes_allocated
+ // with the same index.
+ let allocator_lists = [
+ &mut self.shared_textures.color8_linear,
+ &mut self.shared_textures.color8_nearest,
+ &mut self.shared_textures.color8_glyphs,
+ &mut self.shared_textures.alpha8_linear,
+ &mut self.shared_textures.alpha8_glyphs,
+ &mut self.shared_textures.alpha16_linear,
+ ];
+
+ // Pick a texture type on which to try to run the compaction logic this frame.
+ let idx = self.shared_textures.next_compaction_idx;
+
+ // Number of moved pixels after which we stop attempting to move more items for this frame.
+ // The constant is up for adjustment, the main goal is to avoid causing frame spikes on
+ // low end GPUs.
+ let area_threshold = 512*512;
+
+ let mut changes = Vec::new();
+ allocator_lists[idx].try_compaction(area_threshold, &mut changes);
+
+ if changes.is_empty() {
+ // Nothing to do, we'll try another texture type next frame.
+ self.shared_textures.next_compaction_idx = (self.shared_textures.next_compaction_idx + 1) % allocator_lists.len();
+ }
+
+ for change in changes {
+ let bpp = allocator_lists[idx].texture_parameters().formats.internal.bytes_per_pixel();
+
+ // While the area of the image does not change, the area it occupies in the texture
+ // atlas may (in other words the number of wasted pixels can change), so we have
+ // to keep track of that.
+ let old_bytes = (change.old_rect.area() * bpp) as usize;
+ let new_bytes = (change.new_rect.area() * bpp) as usize;
+ self.bytes_allocated[idx] -= old_bytes;
+ self.bytes_allocated[idx] += new_bytes;
+
+ let entry = match change.handle {
+ TextureCacheHandle::Auto(handle) => self.lru_cache.get_opt_mut(&handle).unwrap(),
+ TextureCacheHandle::Manual(handle) => self.manual_entries.get_opt_mut(&handle).unwrap(),
+ TextureCacheHandle::Empty => { panic!("invalid handle"); }
+ };
+ entry.texture_id = change.new_tex;
+ entry.details = EntryDetails::Cache {
+ origin: change.new_rect.min,
+ alloc_id: change.new_id,
+ allocated_size_in_bytes: new_bytes,
+ };
+
+ gpu_cache.invalidate(&entry.uv_rect_handle);
+ entry.uv_rect_handle = GpuCacheHandle::new();
+
+ let src_rect = DeviceIntRect::from_origin_and_size(change.old_rect.min, entry.size);
+ let dst_rect = DeviceIntRect::from_origin_and_size(change.new_rect.min, entry.size);
+
+ self.pending_updates.push_copy(change.old_tex, &src_rect, change.new_tex, &dst_rect);
+
+ if self.debug_flags.contains(
+ DebugFlags::TEXTURE_CACHE_DBG |
+ DebugFlags::TEXTURE_CACHE_DBG_CLEAR_EVICTED)
+ {
+ self.pending_updates.push_debug_clear(
+ change.old_tex,
+ src_rect.min,
+ src_rect.width(),
+ src_rect.height(),
+ );
+ }
+ }
+ }
+
+ // Request an item in the texture cache. All images that will
+ // be used on a frame *must* have request() called on their
+ // handle, to update the last used timestamp and ensure
+ // that resources are not flushed from the cache too early.
+ //
+ // Returns true if the image needs to be uploaded to the
+ // texture cache (either never uploaded, or has been
+ // evicted on a previous frame).
+ pub fn request(&mut self, handle: &TextureCacheHandle, gpu_cache: &mut GpuCache) -> bool {
+ let now = self.now;
+ let entry = match handle {
+ TextureCacheHandle::Empty => None,
+ TextureCacheHandle::Auto(handle) => {
+ // Call touch rather than get_opt_mut so that the LRU index
+ // knows that the entry has been used.
+ self.lru_cache.touch(handle)
+ },
+ TextureCacheHandle::Manual(handle) => {
+ self.manual_entries.get_opt_mut(handle)
+ },
+ };
+ entry.map_or(true, |entry| {
+ // If an image is requested that is already in the cache,
+ // refresh the GPU cache data associated with this item.
+ entry.last_access = now;
+ entry.update_gpu_cache(gpu_cache);
+ false
+ })
+ }
+
+ fn get_entry_opt(&self, handle: &TextureCacheHandle) -> Option<&CacheEntry> {
+ match handle {
+ TextureCacheHandle::Empty => None,
+ TextureCacheHandle::Auto(handle) => self.lru_cache.get_opt(handle),
+ TextureCacheHandle::Manual(handle) => self.manual_entries.get_opt(handle),
+ }
+ }
+
+ fn get_entry_opt_mut(&mut self, handle: &TextureCacheHandle) -> Option<&mut CacheEntry> {
+ match handle {
+ TextureCacheHandle::Empty => None,
+ TextureCacheHandle::Auto(handle) => self.lru_cache.get_opt_mut(handle),
+ TextureCacheHandle::Manual(handle) => self.manual_entries.get_opt_mut(handle),
+ }
+ }
+
+ // Returns true if the image needs to be uploaded to the
+ // texture cache (either never uploaded, or has been
+ // evicted on a previous frame).
+ pub fn needs_upload(&self, handle: &TextureCacheHandle) -> bool {
+ !self.is_allocated(handle)
+ }
+
+ pub fn max_texture_size(&self) -> i32 {
+ self.max_texture_size
+ }
+
+ pub fn tiling_threshold(&self) -> i32 {
+ self.tiling_threshold
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn color_formats(&self) -> TextureFormatPair<ImageFormat> {
+ self.shared_textures.color8_linear.texture_parameters().formats.clone()
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn swizzle_settings(&self) -> Option<SwizzleSettings> {
+ self.swizzle
+ }
+
+ pub fn pending_updates(&mut self) -> TextureUpdateList {
+ mem::replace(&mut self.pending_updates, TextureUpdateList::new())
+ }
+
+ // Update the data stored by a given texture cache handle.
+ pub fn update(
+ &mut self,
+ handle: &mut TextureCacheHandle,
+ descriptor: ImageDescriptor,
+ filter: TextureFilter,
+ data: Option<CachedImageData>,
+ user_data: [f32; 4],
+ mut dirty_rect: ImageDirtyRect,
+ gpu_cache: &mut GpuCache,
+ eviction_notice: Option<&EvictionNotice>,
+ uv_rect_kind: UvRectKind,
+ eviction: Eviction,
+ shader: TargetShader,
+ ) {
+ debug_assert!(self.now.is_valid());
+ // Determine if we need to allocate texture cache memory
+ // for this item. We need to reallocate if any of the following
+ // is true:
+ // - Never been in the cache
+ // - Has been in the cache but was evicted.
+ // - Exists in the cache but dimensions / format have changed.
+ let realloc = match self.get_entry_opt(handle) {
+ Some(entry) => {
+ entry.size != descriptor.size || (entry.input_format != descriptor.format &&
+ entry.alternative_input_format() != descriptor.format)
+ }
+ None => {
+ // Not allocated, or was previously allocated but has been evicted.
+ true
+ }
+ };
+
+ if realloc {
+ let params = CacheAllocParams { descriptor, filter, user_data, uv_rect_kind, shader };
+ self.allocate(&params, handle, eviction);
+
+ // If we reallocated, we need to upload the whole item again.
+ dirty_rect = DirtyRect::All;
+ }
+
+ let entry = self.get_entry_opt_mut(handle)
+ .expect("BUG: There must be an entry at this handle now");
+
+ // Install the new eviction notice for this update, if applicable.
+ entry.eviction_notice = eviction_notice.cloned();
+ entry.uv_rect_kind = uv_rect_kind;
+
+ // Invalidate the contents of the resource rect in the GPU cache.
+ // This ensures that the update_gpu_cache below will add
+ // the new information to the GPU cache.
+ //TODO: only invalidate if the parameters change?
+ gpu_cache.invalidate(&entry.uv_rect_handle);
+
+ // Upload the resource rect and texture array layer.
+ entry.update_gpu_cache(gpu_cache);
+
+ // Create an update command, which the render thread processes
+ // to upload the new image data into the correct location
+ // in GPU memory.
+ if let Some(data) = data {
+ // If the swizzling is supported, we always upload in the internal
+ // texture format (thus avoiding the conversion by the driver).
+ // Otherwise, pass the external format to the driver.
+ let origin = entry.details.describe();
+ let texture_id = entry.texture_id;
+ let size = entry.size;
+ let use_upload_format = self.swizzle.is_none();
+ let op = TextureCacheUpdate::new_update(
+ data,
+ &descriptor,
+ origin,
+ size,
+ use_upload_format,
+ &dirty_rect,
+ );
+ self.pending_updates.push_update(texture_id, op);
+ }
+ }
+
+ // Check if a given texture handle has a valid allocation
+ // in the texture cache.
+ pub fn is_allocated(&self, handle: &TextureCacheHandle) -> bool {
+ self.get_entry_opt(handle).is_some()
+ }
+
+ // Return the allocated size of the texture handle's associated data,
+ // or otherwise indicate the handle is invalid.
+ pub fn get_allocated_size(&self, handle: &TextureCacheHandle) -> Option<usize> {
+ self.get_entry_opt(handle).map(|entry| {
+ (entry.input_format.bytes_per_pixel() * entry.size.area()) as usize
+ })
+ }
+
+ // Retrieve the details of an item in the cache. This is used
+ // during batch creation to provide the resource rect address
+ // to the shaders and texture ID to the batching logic.
+ // This function will assert in debug modes if the caller
+ // tries to get a handle that was not requested this frame.
+ pub fn get(&self, handle: &TextureCacheHandle) -> CacheItem {
+ let (texture_id, uv_rect, swizzle, uv_rect_handle, user_data) = self.get_cache_location(handle);
+ CacheItem {
+ uv_rect_handle,
+ texture_id: TextureSource::TextureCache(
+ texture_id,
+ swizzle,
+ ),
+ uv_rect,
+ user_data,
+ }
+ }
+
+ /// A more detailed version of get(). This allows access to the actual
+ /// device rect of the cache allocation.
+ ///
+ /// Returns a tuple identifying the texture, the layer, the region,
+ /// and its GPU handle.
+ pub fn get_cache_location(
+ &self,
+ handle: &TextureCacheHandle,
+ ) -> (CacheTextureId, DeviceIntRect, Swizzle, GpuCacheHandle, [f32; 4]) {
+ let entry = self
+ .get_entry_opt(handle)
+ .expect("BUG: was dropped from cache or not updated!");
+ debug_assert_eq!(entry.last_access, self.now);
+ let origin = entry.details.describe();
+ (
+ entry.texture_id,
+ DeviceIntRect::from_origin_and_size(origin, entry.size),
+ entry.swizzle,
+ entry.uv_rect_handle,
+ entry.user_data,
+ )
+ }
+
+ /// Internal helper function to evict a strong texture cache handle
+ fn evict_impl(
+ &mut self,
+ entry: CacheEntry,
+ ) {
+ entry.evict();
+ self.free(&entry);
+ }
+
+ /// Evict a texture cache handle that was previously set to be in manual
+ /// eviction mode.
+ pub fn evict_handle(&mut self, handle: &TextureCacheHandle) {
+ match handle {
+ TextureCacheHandle::Manual(handle) => {
+ // Find the strong handle that matches this weak handle. If this
+ // ever shows up in profiles, we can make it a hash (but the number
+ // of manual eviction handles is typically small).
+ // Alternatively, we could make a more forgiving FreeList variant
+ // which does not differentiate between strong and weak handles.
+ let index = self.manual_handles.iter().position(|strong_handle| {
+ strong_handle.matches(handle)
+ });
+ if let Some(index) = index {
+ let handle = self.manual_handles.swap_remove(index);
+ let entry = self.manual_entries.free(handle);
+ self.evict_impl(entry);
+ }
+ }
+ TextureCacheHandle::Auto(handle) => {
+ if let Some(entry) = self.lru_cache.remove(handle) {
+ self.evict_impl(entry);
+ }
+ }
+ _ => {}
+ }
+ }
+
+ pub fn dump_color8_linear_as_svg(&self, output: &mut dyn std::io::Write) -> std::io::Result<()> {
+ self.shared_textures.color8_linear.dump_as_svg(output)
+ }
+
+ pub fn dump_color8_glyphs_as_svg(&self, output: &mut dyn std::io::Write) -> std::io::Result<()> {
+ self.shared_textures.color8_glyphs.dump_as_svg(output)
+ }
+
+ pub fn dump_alpha8_glyphs_as_svg(&self, output: &mut dyn std::io::Write) -> std::io::Result<()> {
+ self.shared_textures.alpha8_glyphs.dump_as_svg(output)
+ }
+
+ pub fn dump_alpha8_linear_as_svg(&self, output: &mut dyn std::io::Write) -> std::io::Result<()> {
+ self.shared_textures.alpha8_linear.dump_as_svg(output)
+ }
+
+ /// Get the eviction threshold, in bytes, for the given budget type.
+ fn get_eviction_threshold(&self, budget_type: BudgetType) -> usize {
+ if budget_type == BudgetType::Standalone {
+ // For standalone textures, the only reason to evict textures is
+ // to save GPU memory. Batching / draw call concerns do not apply
+ // to standalone textures, because unused textures don't cause
+ // extra draw calls.
+ return 8 * 1024 * 1024;
+ }
+
+ // For shared textures, evicting an entry only frees up GPU memory if it
+ // causes one of the shared textures to become empty, so we want to avoid
+ // getting slightly above the capacity of a texture.
+ // The other concern for shared textures is batching: The entries that
+ // are needed in the current frame should be distributed across as few
+ // shared textures as possible, to minimize the number of draw calls.
+ // Ideally we only want one texture per type under simple workloads.
+
+ let bytes_per_texture = self.shared_textures.bytes_per_shared_texture(budget_type);
+
+ // Number of allocated bytes under which we don't bother with evicting anything
+ // from the cache. Above the threshold we consider evicting the coldest items
+ // depending on how cold they are.
+ //
+ // Above all else we want to make sure that even after a heavy workload, the
+ // shared cache settles back to a single texture atlas per type over some reasonable
+ // period of time.
+ // This is achieved by the compaction logic which will try to consolidate items that
+ // are spread over multiple textures into few ones, and by evicting old items
+ // so that the compaction logic has room to do its job.
+ //
+ // The other goal is to leave enough empty space in the texture atlases
+ // so that we are not too likely to have to allocate a new texture atlas on
+ // the next frame if we switch to a new tab or load a new page. That's why
+ // the following thresholds are rather low. Note that even when above the threshold,
+ // we only evict cold items and ramp up the eviction pressure depending on the amount
+ // of allocated memory (See should_continue_evicting).
+ let ideal_utilization = match budget_type {
+ BudgetType::SharedAlpha8Glyphs | BudgetType::SharedColor8Glyphs => {
+ // Glyphs are usually small and tightly packed so they waste very little
+ // space in the cache.
+ bytes_per_texture * 2 / 3
+ }
+ _ => {
+ // Other types of images come with a variety of sizes making them more
+ // prone to wasting pixels and causing fragmentation issues so we put
+ // more pressure on them.
+ bytes_per_texture / 3
+ }
+ };
+
+ ideal_utilization
+ }
+
+ /// Returns whether to continue eviction and how cold an item need to be to be evicted.
+ ///
+ /// If the None is returned, stop evicting.
+ /// If the Some(n) is returned, continue evicting if the coldest item hasn't been used
+ /// for more than n frames.
+ fn should_continue_evicting(
+ &self,
+ budget_type: BudgetType,
+ eviction_count: usize,
+ ) -> Option<u64> {
+
+ let threshold = self.get_eviction_threshold(budget_type);
+ let bytes_allocated = self.bytes_allocated[budget_type as usize];
+
+ let uses_multiple_atlases = self.shared_textures.has_multiple_textures(budget_type);
+
+ // If current memory usage is below selected threshold, we can stop evicting items
+ // except when using shared texture atlases and more than one texture is in use.
+ // This is not very common but can happen due to fragmentation and the only way
+ // to get rid of that fragmentation is to continue evicting.
+ if bytes_allocated < threshold && !uses_multiple_atlases {
+ return None;
+ }
+
+ // Number of frames since last use that is considered too recent for eviction,
+ // depending on the cache pressure.
+ let age_theshold = match bytes_allocated / threshold {
+ 0 => 400,
+ 1 => 200,
+ 2 => 100,
+ 3 => 50,
+ 4 => 25,
+ 5 => 10,
+ 6 => 5,
+ _ => 1,
+ };
+
+ // If current memory usage is significantly more than the threshold, keep evicting this frame
+ if bytes_allocated > 4 * threshold {
+ return Some(age_theshold);
+ }
+
+ // Otherwise, only allow evicting up to a certain number of items per frame. This allows evictions
+ // to be spread over a number of frames, to avoid frame spikes.
+ if eviction_count < Self::MAX_EVICTIONS_PER_FRAME {
+ return Some(age_theshold)
+ }
+
+ None
+ }
+
+
+ /// Evict old items from the shared and standalone caches, if we're over a
+ /// threshold memory usage value
+ fn evict_items_from_cache_if_required(&mut self, profile: &mut TransactionProfile) {
+ let previous_frame_id = self.now.frame_id() - 1;
+ let mut eviction_count = 0;
+ let mut youngest_evicted = FrameId::first();
+
+ for budget in BudgetType::iter() {
+ while let Some(age_threshold) = self.should_continue_evicting(
+ budget,
+ eviction_count,
+ ) {
+ if let Some(entry) = self.lru_cache.peek_oldest(budget as u8) {
+ // Only evict this item if it wasn't used in the previous frame. The reason being that if it
+ // was used the previous frame then it will likely be used in this frame too, and we don't
+ // want to be continually evicting and reuploading the item every frame.
+ if entry.last_access.frame_id() + age_threshold > previous_frame_id {
+ // Since the LRU cache is ordered by frame access, we can break out of the loop here because
+ // we know that all remaining items were also used in the previous frame (or more recently).
+ break;
+ }
+ if entry.last_access.frame_id() > youngest_evicted {
+ youngest_evicted = entry.last_access.frame_id();
+ }
+ let entry = self.lru_cache.pop_oldest(budget as u8).unwrap();
+ entry.evict();
+ self.free(&entry);
+ eviction_count += 1;
+ } else {
+ // The LRU cache is empty, all remaining items use manual
+ // eviction. In this case, there's nothing we can do until
+ // the calling code manually evicts items to reduce the
+ // allocated cache size.
+ break;
+ }
+ }
+ }
+
+ if eviction_count > 0 {
+ profile.set(profiler::TEXTURE_CACHE_EVICTION_COUNT, eviction_count);
+ profile.set(
+ profiler::TEXTURE_CACHE_YOUNGEST_EVICTION,
+ self.now.frame_id().as_u64() - youngest_evicted.as_u64()
+ );
+ }
+ }
+
+ // Free a cache entry from the standalone list or shared cache.
+ fn free(&mut self, entry: &CacheEntry) {
+ match entry.details {
+ EntryDetails::Standalone { size_in_bytes, .. } => {
+ self.bytes_allocated[BudgetType::Standalone as usize] -= size_in_bytes;
+
+ // This is a standalone texture allocation. Free it directly.
+ self.pending_updates.push_free(entry.texture_id);
+ }
+ EntryDetails::Cache { origin, alloc_id, allocated_size_in_bytes } => {
+ let (allocator_list, budget_type) = self.shared_textures.select(
+ entry.input_format,
+ entry.filter,
+ entry.shader,
+ );
+
+ allocator_list.deallocate(entry.texture_id, alloc_id);
+
+ self.bytes_allocated[budget_type as usize] -= allocated_size_in_bytes;
+
+ if self.debug_flags.contains(
+ DebugFlags::TEXTURE_CACHE_DBG |
+ DebugFlags::TEXTURE_CACHE_DBG_CLEAR_EVICTED)
+ {
+ self.pending_updates.push_debug_clear(
+ entry.texture_id,
+ origin,
+ entry.size.width,
+ entry.size.height,
+ );
+ }
+ }
+ }
+ }
+
+ /// Allocate a block from the shared cache.
+ fn allocate_from_shared_cache(
+ &mut self,
+ params: &CacheAllocParams,
+ ) -> (CacheEntry, BudgetType) {
+ let (allocator_list, budget_type) = self.shared_textures.select(
+ params.descriptor.format,
+ params.filter,
+ params.shader,
+ );
+
+ // To avoid referring to self in the closure.
+ let next_id = &mut self.next_id;
+ let pending_updates = &mut self.pending_updates;
+
+ let (texture_id, alloc_id, allocated_rect) = allocator_list.allocate(
+ params.descriptor.size,
+ &mut |size, parameters| {
+ let texture_id = *next_id;
+ next_id.0 += 1;
+ pending_updates.push_alloc(
+ texture_id,
+ TextureCacheAllocInfo {
+ target: ImageBufferKind::Texture2D,
+ width: size.width,
+ height: size.height,
+ format: parameters.formats.internal,
+ filter: parameters.filter,
+ is_shared_cache: true,
+ has_depth: false,
+ category: TextureCacheCategory::Atlas,
+ },
+ );
+
+ texture_id
+ },
+ );
+
+ let formats = &allocator_list.texture_parameters().formats;
+
+ let swizzle = if formats.external == params.descriptor.format {
+ Swizzle::default()
+ } else {
+ match self.swizzle {
+ Some(_) => Swizzle::Bgra,
+ None => Swizzle::default(),
+ }
+ };
+
+ let bpp = formats.internal.bytes_per_pixel();
+ let allocated_size_in_bytes = (allocated_rect.area() * bpp) as usize;
+ self.bytes_allocated[budget_type as usize] += allocated_size_in_bytes;
+
+ (CacheEntry {
+ size: params.descriptor.size,
+ user_data: params.user_data,
+ last_access: self.now,
+ details: EntryDetails::Cache {
+ origin: allocated_rect.min,
+ alloc_id,
+ allocated_size_in_bytes,
+ },
+ uv_rect_handle: GpuCacheHandle::new(),
+ input_format: params.descriptor.format,
+ filter: params.filter,
+ swizzle,
+ texture_id,
+ eviction_notice: None,
+ uv_rect_kind: params.uv_rect_kind,
+ shader: params.shader
+ }, budget_type)
+ }
+
+ // Returns true if the given image descriptor *may* be
+ // placed in the shared texture cache.
+ pub fn is_allowed_in_shared_cache(
+ &self,
+ filter: TextureFilter,
+ descriptor: &ImageDescriptor,
+ ) -> bool {
+ let mut allowed_in_shared_cache = true;
+
+ if matches!(descriptor.format, ImageFormat::RGBA8 | ImageFormat::BGRA8)
+ && filter == TextureFilter::Linear
+ {
+ // Allow the maximum that can fit in the linear color texture's two column layout.
+ let max = self.shared_textures.color8_linear.size() / 2;
+ allowed_in_shared_cache = descriptor.size.width.max(descriptor.size.height) <= max;
+ } else if descriptor.size.width > TEXTURE_REGION_DIMENSIONS {
+ allowed_in_shared_cache = false;
+ }
+
+ if descriptor.size.height > TEXTURE_REGION_DIMENSIONS {
+ allowed_in_shared_cache = false;
+ }
+
+ // TODO(gw): For now, alpha formats of the texture cache can only be linearly sampled.
+ // Nearest sampling gets a standalone texture.
+ // This is probably rare enough that it can be fixed up later.
+ if filter == TextureFilter::Nearest &&
+ descriptor.format.bytes_per_pixel() <= 2
+ {
+ allowed_in_shared_cache = false;
+ }
+
+ allowed_in_shared_cache
+ }
+
+ /// Allocate a render target via the pending updates sent to the renderer
+ pub fn alloc_render_target(
+ &mut self,
+ size: DeviceIntSize,
+ format: ImageFormat,
+ ) -> CacheTextureId {
+ let texture_id = self.next_id;
+ self.next_id.0 += 1;
+
+ // Push a command to allocate device storage of the right size / format.
+ let info = TextureCacheAllocInfo {
+ target: ImageBufferKind::Texture2D,
+ width: size.width,
+ height: size.height,
+ format,
+ filter: TextureFilter::Linear,
+ is_shared_cache: false,
+ has_depth: false,
+ category: TextureCacheCategory::RenderTarget,
+ };
+
+ self.pending_updates.push_alloc(texture_id, info);
+
+ texture_id
+ }
+
+ /// Free an existing render target
+ pub fn free_render_target(
+ &mut self,
+ id: CacheTextureId,
+ ) {
+ self.pending_updates.push_free(id);
+ }
+
+ /// Allocates a new standalone cache entry.
+ fn allocate_standalone_entry(
+ &mut self,
+ params: &CacheAllocParams,
+ ) -> (CacheEntry, BudgetType) {
+ let texture_id = self.next_id;
+ self.next_id.0 += 1;
+
+ // Push a command to allocate device storage of the right size / format.
+ let info = TextureCacheAllocInfo {
+ target: ImageBufferKind::Texture2D,
+ width: params.descriptor.size.width,
+ height: params.descriptor.size.height,
+ format: params.descriptor.format,
+ filter: params.filter,
+ is_shared_cache: false,
+ has_depth: false,
+ category: TextureCacheCategory::Standalone,
+ };
+
+ let size_in_bytes = (info.width * info.height * info.format.bytes_per_pixel()) as usize;
+ self.bytes_allocated[BudgetType::Standalone as usize] += size_in_bytes;
+
+ self.pending_updates.push_alloc(texture_id, info);
+
+ // Special handing for BGRA8 textures that may need to be swizzled.
+ let swizzle = if params.descriptor.format == ImageFormat::BGRA8 {
+ self.swizzle.map(|s| s.bgra8_sampling_swizzle)
+ } else {
+ None
+ };
+
+ (CacheEntry::new_standalone(
+ texture_id,
+ self.now,
+ params,
+ swizzle.unwrap_or_default(),
+ size_in_bytes,
+ ), BudgetType::Standalone)
+ }
+
+ /// Allocates a cache entry for the given parameters, and updates the
+ /// provided handle to point to the new entry.
+ fn allocate(
+ &mut self,
+ params: &CacheAllocParams,
+ handle: &mut TextureCacheHandle,
+ eviction: Eviction,
+ ) {
+ debug_assert!(self.now.is_valid());
+ assert!(!params.descriptor.size.is_empty());
+
+ // If this image doesn't qualify to go in the shared (batching) cache,
+ // allocate a standalone entry.
+ let use_shared_cache = self.is_allowed_in_shared_cache(params.filter, &params.descriptor);
+ let (new_cache_entry, budget_type) = if use_shared_cache {
+ self.allocate_from_shared_cache(params)
+ } else {
+ self.allocate_standalone_entry(params)
+ };
+
+ let details = new_cache_entry.details.clone();
+ let texture_id = new_cache_entry.texture_id;
+
+ // If the handle points to a valid cache entry, we want to replace the
+ // cache entry with our newly updated location. We also need to ensure
+ // that the storage (region or standalone) associated with the previous
+ // entry here gets freed.
+ //
+ // If the handle is invalid, we need to insert the data, and append the
+ // result to the corresponding vector.
+ let old_entry = match (&mut *handle, eviction) {
+ (TextureCacheHandle::Auto(handle), Eviction::Auto) => {
+ self.lru_cache.replace_or_insert(handle, budget_type as u8, new_cache_entry)
+ },
+ (TextureCacheHandle::Manual(handle), Eviction::Manual) => {
+ let entry = self.manual_entries.get_opt_mut(handle)
+ .expect("Don't call this after evicting");
+ Some(mem::replace(entry, new_cache_entry))
+ },
+ (TextureCacheHandle::Manual(_), Eviction::Auto) |
+ (TextureCacheHandle::Auto(_), Eviction::Manual) => {
+ panic!("Can't change eviction policy after initial allocation");
+ },
+ (TextureCacheHandle::Empty, Eviction::Auto) => {
+ let new_handle = self.lru_cache.push_new(budget_type as u8, new_cache_entry);
+ *handle = TextureCacheHandle::Auto(new_handle);
+ None
+ },
+ (TextureCacheHandle::Empty, Eviction::Manual) => {
+ let manual_handle = self.manual_entries.insert(new_cache_entry);
+ let new_handle = manual_handle.weak();
+ self.manual_handles.push(manual_handle);
+ *handle = TextureCacheHandle::Manual(new_handle);
+ None
+ },
+ };
+ if let Some(old_entry) = old_entry {
+ old_entry.evict();
+ self.free(&old_entry);
+ }
+
+ if let EntryDetails::Cache { alloc_id, .. } = details {
+ let allocator_list = self.shared_textures.select(
+ params.descriptor.format,
+ params.filter,
+ params.shader,
+ ).0;
+
+ allocator_list.set_handle(texture_id, alloc_id, handle);
+ }
+ }
+
+ pub fn shared_alpha_expected_format(&self) -> ImageFormat {
+ self.shared_textures.alpha8_linear.texture_parameters().formats.external
+ }
+
+ pub fn shared_color_expected_format(&self) -> ImageFormat {
+ self.shared_textures.color8_linear.texture_parameters().formats.external
+ }
+
+
+ #[cfg(test)]
+ pub fn total_allocated_bytes_for_testing(&self) -> usize {
+ BudgetType::iter().map(|b| self.bytes_allocated[b as usize]).sum()
+ }
+
+ pub fn report_memory(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.lru_cache.size_of(ops)
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct TextureParameters {
+ pub formats: TextureFormatPair<ImageFormat>,
+ pub filter: TextureFilter,
+}
+
+impl TextureCacheUpdate {
+ // Constructs a TextureCacheUpdate operation to be passed to the
+ // rendering thread in order to do an upload to the right
+ // location in the texture cache.
+ fn new_update(
+ data: CachedImageData,
+ descriptor: &ImageDescriptor,
+ origin: DeviceIntPoint,
+ size: DeviceIntSize,
+ use_upload_format: bool,
+ dirty_rect: &ImageDirtyRect,
+ ) -> TextureCacheUpdate {
+ let source = match data {
+ CachedImageData::Blob => {
+ panic!("The vector image should have been rasterized.");
+ }
+ CachedImageData::External(ext_image) => match ext_image.image_type {
+ ExternalImageType::TextureHandle(_) => {
+ panic!("External texture handle should not go through texture_cache.");
+ }
+ ExternalImageType::Buffer => TextureUpdateSource::External {
+ id: ext_image.id,
+ channel_index: ext_image.channel_index,
+ },
+ },
+ CachedImageData::Raw(bytes) => {
+ let finish = descriptor.offset +
+ descriptor.size.width * descriptor.format.bytes_per_pixel() +
+ (descriptor.size.height - 1) * descriptor.compute_stride();
+ assert!(bytes.len() >= finish as usize);
+
+ TextureUpdateSource::Bytes { data: bytes }
+ }
+ };
+ let format_override = if use_upload_format {
+ Some(descriptor.format)
+ } else {
+ None
+ };
+
+ match *dirty_rect {
+ DirtyRect::Partial(dirty) => {
+ // the dirty rectangle doesn't have to be within the area but has to intersect it, at least
+ let stride = descriptor.compute_stride();
+ let offset = descriptor.offset + dirty.min.y * stride + dirty.min.x * descriptor.format.bytes_per_pixel();
+
+ TextureCacheUpdate {
+ rect: DeviceIntRect::from_origin_and_size(
+ DeviceIntPoint::new(origin.x + dirty.min.x, origin.y + dirty.min.y),
+ DeviceIntSize::new(
+ dirty.width().min(size.width - dirty.min.x),
+ dirty.height().min(size.height - dirty.min.y),
+ ),
+ ),
+ source,
+ stride: Some(stride),
+ offset,
+ format_override,
+ }
+ }
+ DirtyRect::All => {
+ TextureCacheUpdate {
+ rect: DeviceIntRect::from_origin_and_size(origin, size),
+ source,
+ stride: descriptor.stride,
+ offset: descriptor.offset,
+ format_override,
+ }
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod test_texture_cache {
+ #[test]
+ fn check_allocation_size_balance() {
+ // Allocate some glyphs, observe the total allocation size, and free
+ // the glyphs again. Check that the total allocation size is back at the
+ // original value.
+
+ use crate::texture_cache::{TextureCache, TextureCacheHandle, Eviction, TargetShader};
+ use crate::gpu_cache::GpuCache;
+ use crate::device::TextureFilter;
+ use crate::gpu_types::UvRectKind;
+ use api::{ImageDescriptor, ImageDescriptorFlags, ImageFormat, DirtyRect};
+ use api::units::*;
+ use euclid::size2;
+ let mut texture_cache = TextureCache::new_for_testing(2048, ImageFormat::BGRA8);
+ let mut gpu_cache = GpuCache::new_for_testing();
+
+ let sizes: &[DeviceIntSize] = &[
+ size2(23, 27),
+ size2(15, 22),
+ size2(11, 5),
+ size2(20, 25),
+ size2(38, 41),
+ size2(11, 19),
+ size2(13, 21),
+ size2(37, 40),
+ size2(13, 15),
+ size2(14, 16),
+ size2(10, 9),
+ size2(25, 28),
+ ];
+
+ let bytes_at_start = texture_cache.total_allocated_bytes_for_testing();
+
+ let handles: Vec<TextureCacheHandle> = sizes.iter().map(|size| {
+ let mut texture_cache_handle = TextureCacheHandle::invalid();
+ texture_cache.request(&texture_cache_handle, &mut gpu_cache);
+ texture_cache.update(
+ &mut texture_cache_handle,
+ ImageDescriptor {
+ size: *size,
+ stride: None,
+ format: ImageFormat::BGRA8,
+ flags: ImageDescriptorFlags::empty(),
+ offset: 0,
+ },
+ TextureFilter::Linear,
+ None,
+ [0.0; 4],
+ DirtyRect::All,
+ &mut gpu_cache,
+ None,
+ UvRectKind::Rect,
+ Eviction::Manual,
+ TargetShader::Text,
+ );
+ texture_cache_handle
+ }).collect();
+
+ let bytes_after_allocating = texture_cache.total_allocated_bytes_for_testing();
+ assert!(bytes_after_allocating > bytes_at_start);
+
+ for handle in handles {
+ texture_cache.evict_handle(&handle);
+ }
+
+ let bytes_at_end = texture_cache.total_allocated_bytes_for_testing();
+ assert_eq!(bytes_at_end, bytes_at_start);
+ }
+}
diff --git a/gfx/wr/webrender/src/texture_pack/guillotine.rs b/gfx/wr/webrender/src/texture_pack/guillotine.rs
new file mode 100644
index 0000000000..68a08caf2f
--- /dev/null
+++ b/gfx/wr/webrender/src/texture_pack/guillotine.rs
@@ -0,0 +1,284 @@
+/* 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 api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize};
+
+//TODO: gather real-world statistics on the bin usage in order to assist the decision
+// on where to place the size thresholds.
+
+const NUM_BINS: usize = 3;
+/// The minimum number of pixels on each side that we require for rects to be classified as
+/// particular bin of freelists.
+const MIN_RECT_AXIS_SIZES: [i32; NUM_BINS] = [1, 16, 32];
+
+#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
+struct FreeListBin(u8);
+
+#[derive(Debug, Clone, Copy)]
+struct FreeListIndex(usize);
+
+impl FreeListBin {
+ fn for_size(size: &DeviceIntSize) -> Self {
+ MIN_RECT_AXIS_SIZES
+ .iter()
+ .enumerate()
+ .rev()
+ .find(|(_, &min_size)| min_size <= size.width && min_size <= size.height)
+ .map(|(id, _)| FreeListBin(id as u8))
+ .unwrap_or_else(|| panic!("Unable to find a bin for {:?}!", size))
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct FreeRectSlice(pub u32);
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct FreeRect {
+ slice: FreeRectSlice,
+ rect: DeviceIntRect,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct FreeRectSize {
+ width: i16,
+ height: i16,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct Bin {
+ // Store sizes with fewer bits per item and in a separate array to speed up
+ // the search.
+ sizes: Vec<FreeRectSize>,
+ rects: Vec<FreeRect>,
+}
+
+/// A texture allocator using the guillotine algorithm.
+///
+/// See sections 2.2 and 2.2.5 in "A Thousand Ways to Pack the Bin - A Practical Approach to Two-
+/// Dimensional Rectangle Bin Packing":
+///
+/// http://clb.demon.fi/files/RectangleBinPack.pdf
+///
+/// This approach was chosen because of its simplicity and good performance.
+///
+/// Note: the allocations are spread across multiple textures, and also are binned
+/// orthogonally in order to speed up the search.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct GuillotineAllocator {
+ bins: [Bin; NUM_BINS],
+}
+
+impl GuillotineAllocator {
+ pub fn new(initial_size: Option<DeviceIntSize>) -> Self {
+ let mut allocator = GuillotineAllocator {
+ bins: [
+ Bin { rects: Vec::new(), sizes: Vec::new() },
+ Bin { rects: Vec::new(), sizes: Vec::new() },
+ Bin { rects: Vec::new(), sizes: Vec::new() },
+ ],
+ };
+
+ if let Some(initial_size) = initial_size {
+ allocator.push(
+ FreeRectSlice(0),
+ initial_size.into(),
+ );
+ }
+
+ allocator
+ }
+
+ fn push(&mut self, slice: FreeRectSlice, rect: DeviceIntRect) {
+ let id = FreeListBin::for_size(&rect.size()).0 as usize;
+ self.bins[id].rects.push(FreeRect {
+ slice,
+ rect,
+ });
+ self.bins[id].sizes.push(FreeRectSize {
+ width: rect.width() as i16,
+ height: rect.height() as i16,
+ });
+ }
+
+ /// Find a suitable rect in the free list. We choose the first fit.
+ fn find_index_of_best_rect(
+ &self,
+ requested_dimensions: &DeviceIntSize,
+ ) -> Option<(FreeListBin, FreeListIndex)> {
+
+ let start_bin = FreeListBin::for_size(&requested_dimensions);
+
+ let w = requested_dimensions.width as i16;
+ let h = requested_dimensions.height as i16;
+ (start_bin.0 .. NUM_BINS as u8)
+ .find_map(|id| {
+ self.bins[id as usize].sizes
+ .iter()
+ .position(|candidate| w <= candidate.width && h <= candidate.height)
+ .map(|index| (FreeListBin(id), FreeListIndex(index)))
+ })
+ }
+
+ // Split that results in the single largest area (Min Area Split Rule, MINAS).
+ fn split_guillotine(&mut self, chosen: &FreeRect, requested_dimensions: &DeviceIntSize) {
+ let candidate_free_rect_to_right = DeviceIntRect::from_origin_and_size(
+ DeviceIntPoint::new(
+ chosen.rect.min.x + requested_dimensions.width,
+ chosen.rect.min.y,
+ ),
+ DeviceIntSize::new(
+ chosen.rect.width() - requested_dimensions.width,
+ requested_dimensions.height,
+ ),
+ );
+ let candidate_free_rect_to_bottom = DeviceIntRect::from_origin_and_size(
+ DeviceIntPoint::new(
+ chosen.rect.min.x,
+ chosen.rect.min.y + requested_dimensions.height,
+ ),
+ DeviceIntSize::new(
+ requested_dimensions.width,
+ chosen.rect.height() - requested_dimensions.height,
+ ),
+ );
+
+ // Guillotine the rectangle.
+ let new_free_rect_to_right;
+ let new_free_rect_to_bottom;
+ if candidate_free_rect_to_right.area() > candidate_free_rect_to_bottom.area() {
+ new_free_rect_to_right = DeviceIntRect::from_origin_and_size(
+ candidate_free_rect_to_right.min,
+ DeviceIntSize::new(
+ candidate_free_rect_to_right.width(),
+ chosen.rect.height(),
+ ),
+ );
+ new_free_rect_to_bottom = candidate_free_rect_to_bottom
+ } else {
+ new_free_rect_to_right = candidate_free_rect_to_right;
+ new_free_rect_to_bottom = DeviceIntRect::from_origin_and_size(
+ candidate_free_rect_to_bottom.min,
+ DeviceIntSize::new(
+ chosen.rect.width(),
+ candidate_free_rect_to_bottom.height(),
+ ),
+ )
+ }
+
+ // Add the guillotined rects back to the free list.
+ if !new_free_rect_to_right.is_empty() {
+ self.push(chosen.slice, new_free_rect_to_right);
+ }
+ if !new_free_rect_to_bottom.is_empty() {
+ self.push(chosen.slice, new_free_rect_to_bottom);
+ }
+ }
+
+ pub fn allocate(
+ &mut self, requested_dimensions: &DeviceIntSize
+ ) -> Option<(FreeRectSlice, DeviceIntPoint)> {
+ let mut requested_dimensions = *requested_dimensions;
+ // Round up the size to a multiple of 8. This reduces the fragmentation
+ // of the atlas.
+ requested_dimensions.width = (requested_dimensions.width + 7) & !7;
+ requested_dimensions.height = (requested_dimensions.height + 7) & !7;
+
+ if requested_dimensions.width == 0 || requested_dimensions.height == 0 {
+ return Some((FreeRectSlice(0), DeviceIntPoint::new(0, 0)));
+ }
+
+ let (bin, index) = self.find_index_of_best_rect(&requested_dimensions)?;
+
+ // Remove the rect from the free list and decide how to guillotine it.
+ let chosen = self.bins[bin.0 as usize].rects.swap_remove(index.0);
+ self.bins[bin.0 as usize].sizes.swap_remove(index.0);
+ self.split_guillotine(&chosen, &requested_dimensions);
+
+ // Return the result.
+ Some((chosen.slice, chosen.rect.min))
+ }
+
+ /// Add a new slice to the allocator, and immediately allocate a rect from it.
+ pub fn extend(
+ &mut self,
+ slice: FreeRectSlice,
+ total_size: DeviceIntSize,
+ requested_dimensions: DeviceIntSize,
+ ) {
+ self.split_guillotine(
+ &FreeRect { slice, rect: total_size.into() },
+ &requested_dimensions
+ );
+ }
+}
+
+#[cfg(test)]
+fn random_fill(count: usize, texture_size: i32) -> f32 {
+ use rand::{thread_rng, Rng};
+
+ let total_rect = DeviceIntRect::from_size(
+ DeviceIntSize::new(texture_size, texture_size),
+ );
+ let mut rng = thread_rng();
+ let mut allocator = GuillotineAllocator::new(None);
+
+ // check for empty allocation
+ assert_eq!(
+ allocator.allocate(&DeviceIntSize::new(0, 12)),
+ Some((FreeRectSlice(0), DeviceIntPoint::zero())),
+ );
+
+ let mut slices: Vec<Vec<DeviceIntRect>> = Vec::new();
+ let mut requested_area = 0f32;
+ // fill up the allocator
+ for _ in 0 .. count {
+ let size = DeviceIntSize::new(
+ rng.gen_range(1, texture_size),
+ rng.gen_range(1, texture_size),
+ );
+ requested_area += size.area() as f32;
+
+ match allocator.allocate(&size) {
+ Some((slice, origin)) => {
+ let rect = DeviceIntRect::from_origin_and_size(origin, size);
+ assert_eq!(None, slices[slice.0 as usize].iter().find(|r| r.intersects(&rect)));
+ assert!(total_rect.contains_box(&rect));
+ slices[slice.0 as usize].push(rect);
+ }
+ None => {
+ allocator.extend(FreeRectSlice(slices.len() as u32), total_rect.size(), size);
+ let rect = DeviceIntRect::from_size(size);
+ slices.push(vec![rect]);
+ }
+ }
+ }
+ // validate the free rects
+ for (i, bin) in allocator.bins.iter().enumerate() {
+ for fr in &bin.rects {
+ assert_eq!(FreeListBin(i as u8), FreeListBin::for_size(&fr.rect.size()));
+ assert_eq!(None, slices[fr.slice.0 as usize].iter().find(|r| r.intersects(&fr.rect)));
+ assert!(total_rect.contains_box(&fr.rect));
+ slices[fr.slice.0 as usize].push(fr.rect);
+ }
+ }
+
+ let allocated_area = slices.len() as f32 * (texture_size * texture_size) as f32;
+ requested_area / allocated_area
+}
+
+#[test]
+fn test_small() {
+ random_fill(100, 100);
+}
+
+#[test]
+fn test_large() {
+ random_fill(1000, 10000);
+}
diff --git a/gfx/wr/webrender/src/texture_pack/mod.rs b/gfx/wr/webrender/src/texture_pack/mod.rs
new file mode 100644
index 0000000000..f89a82b0a1
--- /dev/null
+++ b/gfx/wr/webrender/src/texture_pack/mod.rs
@@ -0,0 +1,441 @@
+/* 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/. */
+
+mod guillotine;
+use crate::texture_cache::TextureCacheHandle;
+use crate::internal_types::FastHashMap;
+pub use guillotine::*;
+
+/* 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 api::units::*;
+use crate::internal_types::CacheTextureId;
+use euclid::{point2, size2, default::Box2D};
+use smallvec::SmallVec;
+
+pub use etagere::AllocatorOptions as ShelfAllocatorOptions;
+pub use etagere::BucketedAtlasAllocator as BucketedShelfAllocator;
+pub use etagere::AtlasAllocator as ShelfAllocator;
+
+/// ID of an allocation within a given allocator.
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct AllocId(pub u32);
+
+pub trait AtlasAllocator {
+ /// Specific parameters of the allocator.
+ type Parameters;
+ /// Constructor
+ fn new(size: i32, parameters: &Self::Parameters) -> Self;
+ /// Allocate a rectangle.
+ fn allocate(&mut self, size: DeviceIntSize) -> Option<(AllocId, DeviceIntRect)>;
+ /// Deallocate a rectangle and return its size.
+ fn deallocate(&mut self, id: AllocId);
+ /// Return true if there is no live allocations.
+ fn is_empty(&self) -> bool;
+ /// Allocated area in pixels.
+ fn allocated_space(&self) -> i32;
+ /// Write a debug visualization of the atlas fitting in the provided rectangle.
+ ///
+ /// This is inserted in a larger dump so it shouldn't contain the xml start/end tags.
+ fn dump_into_svg(&self, rect: &Box2D<f32>, output: &mut dyn std::io::Write) -> std::io::Result<()>;
+}
+
+pub trait AtlasAllocatorList<TextureParameters> {
+ /// Allocate a rectangle.
+ ///
+ /// If allocation fails, call the provided callback, add a new allocator to the list and try again.
+ fn allocate(
+ &mut self,
+ size: DeviceIntSize,
+ texture_alloc_cb: &mut dyn FnMut(DeviceIntSize, &TextureParameters) -> CacheTextureId,
+ ) -> (CacheTextureId, AllocId, DeviceIntRect);
+
+ fn set_handle(&mut self, texture_id: CacheTextureId, alloc_id: AllocId, handle: &TextureCacheHandle);
+
+ fn remove_handle(&mut self, texture_id: CacheTextureId, alloc_id: AllocId);
+
+ /// Deallocate a rectangle and return its size.
+ fn deallocate(&mut self, texture_id: CacheTextureId, alloc_id: AllocId);
+
+ fn texture_parameters(&self) -> &TextureParameters;
+}
+
+/// A number of 2D textures (single layer), with their own atlas allocator.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct TextureUnit<Allocator> {
+ allocator: Allocator,
+ handles: FastHashMap<AllocId, TextureCacheHandle>,
+ texture_id: CacheTextureId,
+ // The texture might become empty during a frame where we copy items out
+ // of it, in which case we want to postpone deleting the texture to the
+ // next frame.
+ delay_deallocation: bool,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct AllocatorList<Allocator: AtlasAllocator, TextureParameters> {
+ units: SmallVec<[TextureUnit<Allocator>; 1]>,
+ size: i32,
+ atlas_parameters: Allocator::Parameters,
+ texture_parameters: TextureParameters,
+}
+
+impl<Allocator: AtlasAllocator, TextureParameters> AllocatorList<Allocator, TextureParameters> {
+ pub fn new(
+ size: i32,
+ atlas_parameters: Allocator::Parameters,
+ texture_parameters: TextureParameters,
+ ) -> Self {
+ AllocatorList {
+ units: SmallVec::new(),
+ size,
+ atlas_parameters,
+ texture_parameters,
+ }
+ }
+
+ pub fn allocate(
+ &mut self,
+ requested_size: DeviceIntSize,
+ texture_alloc_cb: &mut dyn FnMut(DeviceIntSize, &TextureParameters) -> CacheTextureId,
+ ) -> (CacheTextureId, AllocId, DeviceIntRect) {
+ // Try to allocate from one of the existing textures.
+ for unit in &mut self.units {
+ if let Some((alloc_id, rect)) = unit.allocator.allocate(requested_size) {
+ return (unit.texture_id, alloc_id, rect);
+ }
+ }
+
+ // Need to create a new texture to hold the allocation.
+ let texture_id = texture_alloc_cb(size2(self.size, self.size), &self.texture_parameters);
+ let unit_index = self.units.len();
+
+ self.units.push(TextureUnit {
+ allocator: Allocator::new(self.size, &self.atlas_parameters),
+ handles: FastHashMap::default(),
+ texture_id,
+ delay_deallocation: false,
+ });
+
+ let (alloc_id, rect) = self.units[unit_index]
+ .allocator
+ .allocate(requested_size)
+ .unwrap();
+
+ (texture_id, alloc_id, rect)
+ }
+
+ pub fn deallocate(&mut self, texture_id: CacheTextureId, alloc_id: AllocId) {
+ let unit = self.units
+ .iter_mut()
+ .find(|unit| unit.texture_id == texture_id)
+ .expect("Unable to find the associated texture array unit");
+
+ unit.handles.remove(&alloc_id);
+ unit.allocator.deallocate(alloc_id);
+ }
+
+ pub fn release_empty_textures<'l>(&mut self, texture_dealloc_cb: &'l mut dyn FnMut(CacheTextureId)) {
+ self.units.retain(|unit| {
+ if unit.allocator.is_empty() && !unit.delay_deallocation {
+ texture_dealloc_cb(unit.texture_id);
+
+ false
+ } else{
+ unit.delay_deallocation = false;
+ true
+ }
+ });
+ }
+
+ pub fn clear(&mut self, texture_dealloc_cb: &mut dyn FnMut(CacheTextureId)) {
+ for unit in self.units.drain(..) {
+ texture_dealloc_cb(unit.texture_id);
+ }
+ }
+
+ #[allow(dead_code)]
+ pub fn dump_as_svg(&self, output: &mut dyn std::io::Write) -> std::io::Result<()> {
+ use svg_fmt::*;
+
+ let num_arrays = self.units.len() as f32;
+
+ let text_spacing = 15.0;
+ let unit_spacing = 30.0;
+ let texture_size = self.size as f32 / 2.0;
+
+ let svg_w = unit_spacing * 2.0 + texture_size;
+ let svg_h = unit_spacing + num_arrays * (texture_size + text_spacing + unit_spacing);
+
+ writeln!(output, "{}", BeginSvg { w: svg_w, h: svg_h })?;
+
+ // Background.
+ writeln!(output,
+ " {}",
+ rectangle(0.0, 0.0, svg_w, svg_h)
+ .inflate(1.0, 1.0)
+ .fill(rgb(50, 50, 50))
+ )?;
+
+ let mut y = unit_spacing;
+ for unit in &self.units {
+ writeln!(output, " {}", text(unit_spacing, y, format!("{:?}", unit.texture_id)).color(rgb(230, 230, 230)))?;
+
+ let rect = Box2D {
+ min: point2(unit_spacing, y),
+ max: point2(unit_spacing + texture_size, y + texture_size),
+ };
+
+ unit.allocator.dump_into_svg(&rect, output)?;
+
+ y += unit_spacing + texture_size + text_spacing;
+ }
+
+ writeln!(output, "{}", EndSvg)
+ }
+
+ pub fn allocated_space(&self) -> i32 {
+ let mut accum = 0;
+ for unit in &self.units {
+ accum += unit.allocator.allocated_space();
+ }
+
+ accum
+ }
+
+ pub fn allocated_textures(&self) -> usize {
+ self.units.len()
+ }
+
+ pub fn size(&self) -> i32 { self.size }
+}
+
+impl<Allocator: AtlasAllocator, TextureParameters> AtlasAllocatorList<TextureParameters>
+for AllocatorList<Allocator, TextureParameters> {
+ fn allocate(
+ &mut self,
+ requested_size: DeviceIntSize,
+ texture_alloc_cb: &mut dyn FnMut(DeviceIntSize, &TextureParameters) -> CacheTextureId,
+ ) -> (CacheTextureId, AllocId, DeviceIntRect) {
+ self.allocate(requested_size, texture_alloc_cb)
+ }
+
+ fn set_handle(&mut self, texture_id: CacheTextureId, alloc_id: AllocId, handle: &TextureCacheHandle) {
+ let unit = self.units
+ .iter_mut()
+ .find(|unit| unit.texture_id == texture_id)
+ .expect("Unable to find the associated texture array unit");
+ unit.handles.insert(alloc_id, handle.clone());
+ }
+
+ fn remove_handle(&mut self, texture_id: CacheTextureId, alloc_id: AllocId) {
+ let unit = self.units
+ .iter_mut()
+ .find(|unit| unit.texture_id == texture_id)
+ .expect("Unable to find the associated texture array unit");
+ unit.handles.remove(&alloc_id);
+ }
+
+ fn deallocate(&mut self, texture_id: CacheTextureId, alloc_id: AllocId) {
+ self.deallocate(texture_id, alloc_id);
+ }
+
+ fn texture_parameters(&self) -> &TextureParameters {
+ &self.texture_parameters
+ }
+}
+
+impl AtlasAllocator for BucketedShelfAllocator {
+ type Parameters = ShelfAllocatorOptions;
+
+ fn new(size: i32, options: &Self::Parameters) -> Self {
+ BucketedShelfAllocator::with_options(size2(size, size), options)
+ }
+
+ fn allocate(&mut self, size: DeviceIntSize) -> Option<(AllocId, DeviceIntRect)> {
+ self.allocate(size.to_untyped()).map(|alloc| {
+ (AllocId(alloc.id.serialize()), alloc.rectangle.cast_unit())
+ })
+ }
+
+ fn deallocate(&mut self, id: AllocId) {
+ self.deallocate(etagere::AllocId::deserialize(id.0));
+ }
+
+ fn is_empty(&self) -> bool {
+ self.is_empty()
+ }
+
+ fn allocated_space(&self) -> i32 {
+ self.allocated_space()
+ }
+
+ fn dump_into_svg(&self, rect: &Box2D<f32>, output: &mut dyn std::io::Write) -> std::io::Result<()> {
+ self.dump_into_svg(Some(&rect.to_i32().cast_unit()), output)
+ }
+}
+
+impl AtlasAllocator for ShelfAllocator {
+ type Parameters = ShelfAllocatorOptions;
+
+ fn new(size: i32, options: &Self::Parameters) -> Self {
+ ShelfAllocator::with_options(size2(size, size), options)
+ }
+
+ fn allocate(&mut self, size: DeviceIntSize) -> Option<(AllocId, DeviceIntRect)> {
+ self.allocate(size.to_untyped()).map(|alloc| {
+ (AllocId(alloc.id.serialize()), alloc.rectangle.cast_unit())
+ })
+ }
+
+ fn deallocate(&mut self, id: AllocId) {
+ self.deallocate(etagere::AllocId::deserialize(id.0));
+ }
+
+ fn is_empty(&self) -> bool {
+ self.is_empty()
+ }
+
+ fn allocated_space(&self) -> i32 {
+ self.allocated_space()
+ }
+
+ fn dump_into_svg(&self, rect: &Box2D<f32>, output: &mut dyn std::io::Write) -> std::io::Result<()> {
+ self.dump_into_svg(Some(&rect.to_i32().cast_unit()), output)
+ }
+}
+
+pub struct CompactionChange {
+ pub handle: TextureCacheHandle,
+ pub old_id: AllocId,
+ pub old_tex: CacheTextureId,
+ pub old_rect: DeviceIntRect,
+ pub new_id: AllocId,
+ pub new_tex: CacheTextureId,
+ pub new_rect: DeviceIntRect,
+}
+
+impl<P> AllocatorList<ShelfAllocator, P> {
+ /// Attempt to move some allocations from a texture to another to reduce the number of textures.
+ pub fn try_compaction(
+ &mut self,
+ max_pixels: i32,
+ changes: &mut Vec<CompactionChange>,
+ ) {
+ // The goal here is to consolidate items in the first texture by moving them from the last.
+
+ if self.units.len() < 2 {
+ // Nothing to do we are already "compact".
+ return;
+ }
+
+ let last_unit = self.units.len() - 1;
+ let mut pixels = 0;
+ while let Some(alloc) = self.units[last_unit].allocator.iter().next() {
+ // For each allocation in the last texture, try to allocate it in the first one.
+ let new_alloc = match self.units[0].allocator.allocate(alloc.rectangle.size()) {
+ Some(new_alloc) => new_alloc,
+ None => {
+ // Stop when we fail to fit an item into the first texture.
+ // We could potentially fit another smaller item in there but we take it as
+ // an indication that the texture is more or less full, and we'll eventually
+ // manage to move the items later if they still exist as other items expire,
+ // which is what matters.
+ break;
+ }
+ };
+
+ // The item was successfully reallocated in the first texture, we can proceed
+ // with removing it from the last.
+
+ // We keep track of the texture cache handle for each allocation, make sure
+ // the new allocation has the proper handle.
+ let alloc_id = AllocId(alloc.id.serialize());
+ let new_alloc_id = AllocId(new_alloc.id.serialize());
+ let handle = self.units[last_unit].handles.get(&alloc_id).unwrap().clone();
+ self.units[0].handles.insert(new_alloc_id, handle.clone());
+
+ // Remove the allocation for the last texture.
+ self.units[last_unit].handles.remove(&alloc_id);
+ self.units[last_unit].allocator.deallocate(alloc.id);
+
+ // Prevent the texture from being deleted on the same frame.
+ self.units[last_unit].delay_deallocation = true;
+
+ // Record the change so that the texture cache can do additional bookkeeping.
+ changes.push(CompactionChange {
+ handle,
+ old_id: AllocId(alloc.id.serialize()),
+ old_tex: self.units[last_unit].texture_id,
+ old_rect: alloc.rectangle.cast_unit(),
+ new_id: AllocId(new_alloc.id.serialize()),
+ new_tex: self.units[0].texture_id,
+ new_rect: new_alloc.rectangle.cast_unit(),
+ });
+
+ // We are not in a hurry to move all allocations we can in one go, as long as we
+ // eventually have a chance to move them all within a reasonable amount of time.
+ // It's best to spread the load over multiple frames to avoid sudden spikes, so we
+ // stop after we have passed a certain threshold.
+ pixels += alloc.rectangle.area();
+ if pixels > max_pixels {
+ break;
+ }
+ }
+ }
+
+}
+
+#[test]
+fn bug_1680769() {
+ let mut allocators: AllocatorList<ShelfAllocator, ()> = AllocatorList::new(
+ 1024,
+ ShelfAllocatorOptions::default(),
+ (),
+ );
+
+ let mut allocations = Vec::new();
+ let mut next_id = CacheTextureId(0);
+ let alloc_cb = &mut |_: DeviceIntSize, _: &()| {
+ let texture_id = next_id;
+ next_id.0 += 1;
+
+ texture_id
+ };
+
+ // Make some allocations, forcing the the creation of multiple textures.
+ for _ in 0..50 {
+ let alloc = allocators.allocate(size2(256, 256), alloc_cb);
+ allocators.set_handle(alloc.0, alloc.1, &TextureCacheHandle::Empty);
+ allocations.push(alloc);
+ }
+
+ // Deallocate everything.
+ // It should empty all atlases and we still have textures allocated because
+ // we haven't called release_empty_textures yet.
+ for alloc in allocations.drain(..) {
+ allocators.deallocate(alloc.0, alloc.1);
+ }
+
+ // Allocate something else.
+ // Bug 1680769 was causing this allocation to be duplicated and leaked in
+ // all textures.
+ allocations.push(allocators.allocate(size2(8, 8), alloc_cb));
+
+ // Deallocate all known allocations.
+ for alloc in allocations.drain(..) {
+ allocators.deallocate(alloc.0, alloc.1);
+ }
+
+ // If we have leaked items, this won't manage to remove all textures.
+ allocators.release_empty_textures(&mut |_| {});
+
+ assert_eq!(allocators.allocated_textures(), 0);
+}
diff --git a/gfx/wr/webrender/src/tile_cache.rs b/gfx/wr/webrender/src/tile_cache.rs
new file mode 100644
index 0000000000..3b2600d0c0
--- /dev/null
+++ b/gfx/wr/webrender/src/tile_cache.rs
@@ -0,0 +1,745 @@
+/* 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 api::{ColorF, PrimitiveFlags, QualitySettings, RasterSpace, ClipId};
+use api::units::*;
+use crate::clip::{ClipNodeKind, ClipLeafId, ClipNodeId, ClipTreeBuilder};
+use crate::frame_builder::FrameBuilderConfig;
+use crate::internal_types::{FastHashMap};
+use crate::picture::{PrimitiveList, PictureCompositeMode, PicturePrimitive, SliceId};
+use crate::picture::{Picture3DContext, TileCacheParams, TileOffset, PictureFlags};
+use crate::prim_store::{PrimitiveInstance, PrimitiveStore, PictureIndex};
+use crate::scene_building::SliceFlags;
+use crate::scene_builder_thread::Interners;
+use crate::spatial_tree::{SpatialNodeIndex, SceneSpatialTree};
+use crate::util::VecHelper;
+use std::mem;
+
+/*
+ Types and functionality related to picture caching. In future, we'll
+ move more and more of the existing functionality out of picture.rs
+ and into here.
+ */
+
+// If the page would create too many slices (an arbitrary definition where
+// it's assumed the GPU memory + compositing overhead would be too high)
+// then create a single picture cache for the remaining content. This at
+// least means that we can cache small content changes efficiently when
+// scrolling isn't occurring. Scrolling regions will be handled reasonably
+// efficiently by the dirty rect tracking (since it's likely that if the
+// page has so many slices there isn't a single major scroll region).
+const MAX_CACHE_SLICES: usize = 12;
+
+struct SliceDescriptor {
+ prim_list: PrimitiveList,
+ scroll_root: SpatialNodeIndex,
+ shared_clip_node_id: ClipNodeId,
+}
+
+enum SliceKind {
+ Default {
+ secondary_slices: Vec<SliceDescriptor>,
+ },
+ Atomic {
+ prim_list: PrimitiveList,
+ },
+}
+
+impl SliceKind {
+ fn default() -> Self {
+ SliceKind::Default {
+ secondary_slices: Vec::new(),
+ }
+ }
+}
+
+struct PrimarySlice {
+ /// Whether this slice is atomic or has secondary slice(s)
+ kind: SliceKind,
+ /// Optional background color of this slice
+ background_color: Option<ColorF>,
+ /// Optional root clip for the iframe
+ iframe_clip: Option<ClipId>,
+ /// Information about how to draw and composite this slice
+ slice_flags: SliceFlags,
+}
+
+impl PrimarySlice {
+ fn new(
+ slice_flags: SliceFlags,
+ iframe_clip: Option<ClipId>,
+ background_color: Option<ColorF>,
+ ) -> Self {
+ PrimarySlice {
+ kind: SliceKind::default(),
+ background_color,
+ iframe_clip,
+ slice_flags,
+ }
+ }
+
+ fn has_too_many_slices(&self) -> bool {
+ match self.kind {
+ SliceKind::Atomic { .. } => false,
+ SliceKind::Default { ref secondary_slices } => secondary_slices.len() > MAX_CACHE_SLICES,
+ }
+ }
+
+ fn merge(&mut self) {
+ self.slice_flags |= SliceFlags::IS_ATOMIC;
+
+ let old = mem::replace(
+ &mut self.kind,
+ SliceKind::Default { secondary_slices: Vec::new() },
+ );
+
+ self.kind = match old {
+ SliceKind::Default { mut secondary_slices } => {
+ let mut prim_list = PrimitiveList::empty();
+
+ for descriptor in secondary_slices.drain(..) {
+ prim_list.merge(descriptor.prim_list);
+ }
+
+ SliceKind::Atomic {
+ prim_list,
+ }
+ }
+ atomic => atomic,
+ }
+ }
+}
+
+/// Used during scene building to construct the list of pending tile caches.
+pub struct TileCacheBuilder {
+ /// List of tile caches that have been created so far (last in the list is currently active).
+ primary_slices: Vec<PrimarySlice>,
+ /// Cache the previous scroll root search for a spatial node, since they are often the same.
+ prev_scroll_root_cache: (SpatialNodeIndex, SpatialNodeIndex),
+ /// Handle to the root reference frame
+ root_spatial_node_index: SpatialNodeIndex,
+}
+
+/// The output of a tile cache builder, containing all details needed to construct the
+/// tile cache(s) for the next scene, and retain tiles from the previous frame when sent
+/// send to the frame builder.
+pub struct TileCacheConfig {
+ /// Mapping of slice id to the parameters needed to construct this tile cache.
+ pub tile_caches: FastHashMap<SliceId, TileCacheParams>,
+ /// Number of picture cache slices that were created (for profiler)
+ pub picture_cache_slice_count: usize,
+}
+
+impl TileCacheConfig {
+ pub fn new(picture_cache_slice_count: usize) -> Self {
+ TileCacheConfig {
+ tile_caches: FastHashMap::default(),
+ picture_cache_slice_count,
+ }
+ }
+}
+
+impl TileCacheBuilder {
+ /// Construct a new tile cache builder.
+ pub fn new(
+ root_spatial_node_index: SpatialNodeIndex,
+ background_color: Option<ColorF>,
+ ) -> Self {
+ TileCacheBuilder {
+ primary_slices: vec![PrimarySlice::new(SliceFlags::empty(), None, background_color)],
+ prev_scroll_root_cache: (SpatialNodeIndex::INVALID, SpatialNodeIndex::INVALID),
+ root_spatial_node_index,
+ }
+ }
+
+ pub fn make_current_slice_atomic(&mut self) {
+ self.primary_slices
+ .last_mut()
+ .unwrap()
+ .merge();
+ }
+
+ /// Returns true if the current slice has no primitives added yet
+ pub fn is_current_slice_empty(&self) -> bool {
+ match self.primary_slices.last() {
+ Some(slice) => {
+ match slice.kind {
+ SliceKind::Default { ref secondary_slices } => {
+ secondary_slices.is_empty()
+ }
+ SliceKind::Atomic { ref prim_list } => {
+ prim_list.is_empty()
+ }
+ }
+ }
+ None => {
+ true
+ }
+ }
+ }
+
+ /// Set a barrier that forces a new tile cache next time a prim is added.
+ pub fn add_tile_cache_barrier(
+ &mut self,
+ slice_flags: SliceFlags,
+ iframe_clip: Option<ClipId>,
+ ) {
+ let new_slice = PrimarySlice::new(
+ slice_flags,
+ iframe_clip,
+ None,
+ );
+
+ self.primary_slices.push(new_slice);
+ }
+
+ /// Create a new tile cache for an existing prim_list
+ fn build_tile_cache(
+ &mut self,
+ prim_list: PrimitiveList,
+ spatial_tree: &SceneSpatialTree,
+ prim_instances: &[PrimitiveInstance],
+ clip_tree_builder: &ClipTreeBuilder,
+ ) -> Option<SliceDescriptor> {
+ if prim_list.is_empty() {
+ return None;
+ }
+
+ // Iterate the clusters and determine which is the most commonly occurring
+ // scroll root. This is a reasonable heuristic to decide which spatial node
+ // should be considered the scroll root of this tile cache, in order to
+ // minimize the invalidations that occur due to scrolling. It's often the
+ // case that a blend container will have only a single scroll root.
+ let mut scroll_root_occurrences = FastHashMap::default();
+
+ for cluster in &prim_list.clusters {
+ // If we encounter a cluster which has an unknown spatial node,
+ // we don't include that in the set of spatial nodes that we
+ // are trying to find scroll roots for. Later on, in finalize_picture,
+ // the cluster spatial node will be updated to the selected scroll root.
+ if cluster.spatial_node_index == SpatialNodeIndex::UNKNOWN {
+ continue;
+ }
+
+ let scroll_root = find_scroll_root(
+ cluster.spatial_node_index,
+ &mut self.prev_scroll_root_cache,
+ spatial_tree,
+ );
+
+ *scroll_root_occurrences.entry(scroll_root).or_insert(0) += 1;
+ }
+
+ // We can't just select the most commonly occurring scroll root in this
+ // primitive list. If that is a nested scroll root, there may be
+ // primitives in the list that are outside that scroll root, which
+ // can cause panics when calculating relative transforms. To ensure
+ // this doesn't happen, only retain scroll root candidates that are
+ // also ancestors of every other scroll root candidate.
+ let scroll_roots: Vec<SpatialNodeIndex> = scroll_root_occurrences
+ .keys()
+ .cloned()
+ .collect();
+
+ scroll_root_occurrences.retain(|parent_spatial_node_index, _| {
+ scroll_roots.iter().all(|child_spatial_node_index| {
+ parent_spatial_node_index == child_spatial_node_index ||
+ spatial_tree.is_ancestor(
+ *parent_spatial_node_index,
+ *child_spatial_node_index,
+ )
+ })
+ });
+
+ // Select the scroll root by finding the most commonly occurring one
+ let scroll_root = scroll_root_occurrences
+ .iter()
+ .max_by_key(|entry | entry.1)
+ .map(|(spatial_node_index, _)| *spatial_node_index)
+ .unwrap_or(self.root_spatial_node_index);
+
+ // Work out which clips are shared by all prim instances and can thus be applied
+ // at the tile cache level. In future, we aim to remove this limitation by knowing
+ // during initial scene build which are the relevant compositor clips, but for now
+ // this is unlikely to be a significant cost.
+ let mut shared_clip_node_id = None;
+
+ for cluster in &prim_list.clusters {
+ for prim_instance in &prim_instances[cluster.prim_range()] {
+ let leaf = clip_tree_builder.get_leaf(prim_instance.clip_leaf_id);
+
+ // TODO(gw): Need to cache last clip-node id here?
+ shared_clip_node_id = match shared_clip_node_id {
+ Some(current) => {
+ Some(clip_tree_builder.find_lowest_common_ancestor(current, leaf.node_id))
+ }
+ None => {
+ Some(leaf.node_id)
+ }
+ }
+ }
+ }
+
+ let shared_clip_node_id = shared_clip_node_id.expect("bug: no shared clip root");
+
+ Some(SliceDescriptor {
+ scroll_root,
+ shared_clip_node_id,
+ prim_list,
+ })
+ }
+
+ /// Add a primitive, either to the current tile cache, or a new one, depending on various conditions.
+ pub fn add_prim(
+ &mut self,
+ prim_instance: PrimitiveInstance,
+ prim_rect: LayoutRect,
+ spatial_node_index: SpatialNodeIndex,
+ prim_flags: PrimitiveFlags,
+ spatial_tree: &SceneSpatialTree,
+ interners: &Interners,
+ quality_settings: &QualitySettings,
+ prim_instances: &mut Vec<PrimitiveInstance>,
+ clip_tree_builder: &ClipTreeBuilder,
+ ) {
+ let primary_slice = self.primary_slices.last_mut().unwrap();
+
+ match primary_slice.kind {
+ SliceKind::Atomic { ref mut prim_list } => {
+ prim_list.add_prim(
+ prim_instance,
+ prim_rect,
+ spatial_node_index,
+ prim_flags,
+ prim_instances,
+ clip_tree_builder,
+ );
+ }
+ SliceKind::Default { ref mut secondary_slices } => {
+ assert_ne!(spatial_node_index, SpatialNodeIndex::UNKNOWN);
+
+ // Check if we want to create a new slice based on the current / next scroll root
+ let scroll_root = find_scroll_root(
+ spatial_node_index,
+ &mut self.prev_scroll_root_cache,
+ spatial_tree,
+ );
+
+ let current_scroll_root = secondary_slices
+ .last()
+ .map(|p| p.scroll_root);
+
+ let mut want_new_tile_cache = secondary_slices.is_empty();
+
+ if let Some(current_scroll_root) = current_scroll_root {
+ want_new_tile_cache |= match (current_scroll_root, scroll_root) {
+ (_, _) if current_scroll_root == self.root_spatial_node_index && scroll_root == self.root_spatial_node_index => {
+ // Both current slice and this cluster are fixed position, no need to cut
+ false
+ }
+ (_, _) if current_scroll_root == self.root_spatial_node_index => {
+ // A real scroll root is being established, so create a cache slice
+ true
+ }
+ (_, _) if scroll_root == self.root_spatial_node_index => {
+ // If quality settings force subpixel AA over performance, skip creating
+ // a slice for the fixed position element(s) here.
+ if quality_settings.force_subpixel_aa_where_possible {
+ false
+ } else {
+ // A fixed position slice is encountered within a scroll root. Only create
+ // a slice in this case if all the clips referenced by this cluster are also
+ // fixed position. There's no real point in creating slices for these cases,
+ // since we'll have to rasterize them as the scrolling clip moves anyway. It
+ // also allows us to retain subpixel AA in these cases. For these types of
+ // slices, the intra-slice dirty rect handling typically works quite well
+ // (a common case is parallax scrolling effects).
+ let mut create_slice = true;
+
+ let leaf = clip_tree_builder.get_leaf(prim_instance.clip_leaf_id);
+ let mut current_node_id = leaf.node_id;
+
+ while current_node_id != ClipNodeId::NONE {
+ let node = clip_tree_builder.get_node(current_node_id);
+
+ let clip_node_data = &interners.clip[node.handle];
+
+ let spatial_root = find_scroll_root(
+ clip_node_data.key.spatial_node_index,
+ &mut self.prev_scroll_root_cache,
+ spatial_tree,
+ );
+
+ if spatial_root != self.root_spatial_node_index {
+ create_slice = false;
+ break;
+ }
+
+ current_node_id = node.parent;
+ }
+
+ create_slice
+ }
+ }
+ (curr_scroll_root, scroll_root) => {
+ // Two scrolling roots - only need a new slice if they differ
+ curr_scroll_root != scroll_root
+ }
+ };
+
+ // Update the list of clips that apply to this primitive instance, to track which are the
+ // shared clips for this tile cache that can be applied during compositing.
+
+ let shared_clip_node_id = find_shared_clip_root(
+ current_scroll_root,
+ prim_instance.clip_leaf_id,
+ spatial_tree,
+ clip_tree_builder,
+ interners,
+ );
+
+ let current_shared_clip_node_id = secondary_slices.last().unwrap().shared_clip_node_id;
+
+ // If the shared clips are not compatible, create a new slice.
+ want_new_tile_cache |= shared_clip_node_id != current_shared_clip_node_id;
+ }
+
+ if want_new_tile_cache {
+
+ let shared_clip_node_id = find_shared_clip_root(
+ scroll_root,
+ prim_instance.clip_leaf_id,
+ spatial_tree,
+ clip_tree_builder,
+ interners,
+ );
+
+ secondary_slices.push(SliceDescriptor {
+ prim_list: PrimitiveList::empty(),
+ scroll_root,
+ shared_clip_node_id,
+ });
+ }
+
+ secondary_slices
+ .last_mut()
+ .unwrap()
+ .prim_list
+ .add_prim(
+ prim_instance,
+ prim_rect,
+ spatial_node_index,
+ prim_flags,
+ prim_instances,
+ clip_tree_builder,
+ );
+ }
+ }
+ }
+
+ /// Consume this object and build the list of tile cache primitives
+ pub fn build(
+ mut self,
+ config: &FrameBuilderConfig,
+ prim_store: &mut PrimitiveStore,
+ spatial_tree: &SceneSpatialTree,
+ prim_instances: &[PrimitiveInstance],
+ clip_tree_builder: &mut ClipTreeBuilder,
+ ) -> (TileCacheConfig, Vec<PictureIndex>) {
+ let mut result = TileCacheConfig::new(self.primary_slices.len());
+ let mut tile_cache_pictures = Vec::new();
+ let primary_slices = std::mem::replace(&mut self.primary_slices, Vec::new());
+
+ for mut primary_slice in primary_slices {
+
+ if primary_slice.has_too_many_slices() {
+ primary_slice.merge();
+ }
+
+ match primary_slice.kind {
+ SliceKind::Atomic { prim_list } => {
+ if let Some(descriptor) = self.build_tile_cache(
+ prim_list,
+ spatial_tree,
+ prim_instances,
+ clip_tree_builder,
+ ) {
+ create_tile_cache(
+ primary_slice.slice_flags,
+ descriptor.scroll_root,
+ primary_slice.iframe_clip,
+ descriptor.prim_list,
+ primary_slice.background_color,
+ descriptor.shared_clip_node_id,
+ prim_store,
+ config,
+ &mut result.tile_caches,
+ &mut tile_cache_pictures,
+ clip_tree_builder,
+ );
+ }
+ }
+ SliceKind::Default { secondary_slices } => {
+ for descriptor in secondary_slices {
+ create_tile_cache(
+ primary_slice.slice_flags,
+ descriptor.scroll_root,
+ primary_slice.iframe_clip,
+ descriptor.prim_list,
+ primary_slice.background_color,
+ descriptor.shared_clip_node_id,
+ prim_store,
+ config,
+ &mut result.tile_caches,
+ &mut tile_cache_pictures,
+ clip_tree_builder,
+ );
+ }
+ }
+ }
+ }
+
+ (result, tile_cache_pictures)
+ }
+}
+
+/// Find the scroll root for a given spatial node
+fn find_scroll_root(
+ spatial_node_index: SpatialNodeIndex,
+ prev_scroll_root_cache: &mut (SpatialNodeIndex, SpatialNodeIndex),
+ spatial_tree: &SceneSpatialTree,
+) -> SpatialNodeIndex {
+ if prev_scroll_root_cache.0 == spatial_node_index {
+ return prev_scroll_root_cache.1;
+ }
+
+ let scroll_root = spatial_tree.find_scroll_root(spatial_node_index);
+ *prev_scroll_root_cache = (spatial_node_index, scroll_root);
+
+ scroll_root
+}
+
+fn find_shared_clip_root(
+ scroll_root: SpatialNodeIndex,
+ clip_leaf_id: ClipLeafId,
+ spatial_tree: &SceneSpatialTree,
+ clip_tree_builder: &ClipTreeBuilder,
+ interners: &Interners,
+) -> ClipNodeId {
+ let leaf = clip_tree_builder.get_leaf(clip_leaf_id);
+ let mut current_node_id = leaf.node_id;
+
+ while current_node_id != ClipNodeId::NONE {
+ let node = clip_tree_builder.get_node(current_node_id);
+
+ let clip_node_data = &interners.clip[node.handle];
+
+ if let ClipNodeKind::Rectangle = clip_node_data.key.kind.node_kind() {
+ let is_ancestor = spatial_tree.is_ancestor(
+ clip_node_data.key.spatial_node_index,
+ scroll_root,
+ );
+
+ let has_complex_clips = clip_tree_builder.clip_node_has_complex_clips(
+ current_node_id,
+ interners,
+ );
+
+ if is_ancestor && !has_complex_clips {
+ break;
+ }
+ }
+
+ current_node_id = node.parent;
+ }
+
+ current_node_id
+}
+
+/// Given a PrimitiveList and scroll root, construct a tile cache primitive instance
+/// that wraps the primitive list.
+fn create_tile_cache(
+ slice_flags: SliceFlags,
+ scroll_root: SpatialNodeIndex,
+ iframe_clip: Option<ClipId>,
+ prim_list: PrimitiveList,
+ background_color: Option<ColorF>,
+ shared_clip_node_id: ClipNodeId,
+ prim_store: &mut PrimitiveStore,
+ frame_builder_config: &FrameBuilderConfig,
+ tile_caches: &mut FastHashMap<SliceId, TileCacheParams>,
+ tile_cache_pictures: &mut Vec<PictureIndex>,
+ clip_tree_builder: &mut ClipTreeBuilder,
+) {
+ // Accumulate any clip instances from the iframe_clip into the shared clips
+ // that will be applied by this tile cache during compositing.
+ let mut additional_clips = Vec::new();
+
+ if let Some(clip_id) = iframe_clip {
+ additional_clips.push(clip_id);
+ }
+
+ let shared_clip_leaf_id = Some(clip_tree_builder.build_for_tile_cache(
+ shared_clip_node_id,
+ &additional_clips,
+ ));
+
+ // Build a clip-chain for the tile cache, that contains any of the shared clips
+ // we will apply when drawing the tiles. In all cases provided by Gecko, these
+ // are rectangle clips with a scale/offset transform only, and get handled as
+ // a simple local clip rect in the vertex shader. However, this should in theory
+ // also work with any complex clips, such as rounded rects and image masks, by
+ // producing a clip mask that is applied to the picture cache tiles.
+
+ let slice = tile_cache_pictures.len();
+
+ let background_color = if slice == 0 {
+ background_color
+ } else {
+ None
+ };
+
+ let slice_id = SliceId::new(slice);
+
+ // Store some information about the picture cache slice. This is used when we swap the
+ // new scene into the frame builder to either reuse existing slices, or create new ones.
+ tile_caches.insert(slice_id, TileCacheParams {
+ slice,
+ slice_flags,
+ spatial_node_index: scroll_root,
+ background_color,
+ shared_clip_node_id,
+ shared_clip_leaf_id,
+ virtual_surface_size: frame_builder_config.compositor_kind.get_virtual_surface_size(),
+ compositor_surface_count: prim_list.compositor_surface_count,
+ });
+
+ let pic_index = prim_store.pictures.alloc().init(PicturePrimitive::new_image(
+ Some(PictureCompositeMode::TileCache { slice_id }),
+ Picture3DContext::Out,
+ PrimitiveFlags::IS_BACKFACE_VISIBLE,
+ prim_list,
+ scroll_root,
+ RasterSpace::Screen,
+ PictureFlags::empty(),
+ ));
+
+ tile_cache_pictures.push(PictureIndex(pic_index));
+}
+
+/// Debug information about a set of picture cache slices, exposed via RenderResults
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PictureCacheDebugInfo {
+ pub slices: FastHashMap<usize, SliceDebugInfo>,
+}
+
+impl PictureCacheDebugInfo {
+ pub fn new() -> Self {
+ PictureCacheDebugInfo {
+ slices: FastHashMap::default(),
+ }
+ }
+
+ /// Convenience method to retrieve a given slice. Deliberately panics
+ /// if the slice isn't present.
+ pub fn slice(&self, slice: usize) -> &SliceDebugInfo {
+ &self.slices[&slice]
+ }
+}
+
+impl Default for PictureCacheDebugInfo {
+ fn default() -> PictureCacheDebugInfo {
+ PictureCacheDebugInfo::new()
+ }
+}
+
+/// Debug information about a set of picture cache tiles, exposed via RenderResults
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SliceDebugInfo {
+ pub tiles: FastHashMap<TileOffset, TileDebugInfo>,
+}
+
+impl SliceDebugInfo {
+ pub fn new() -> Self {
+ SliceDebugInfo {
+ tiles: FastHashMap::default(),
+ }
+ }
+
+ /// Convenience method to retrieve a given tile. Deliberately panics
+ /// if the tile isn't present.
+ pub fn tile(&self, x: i32, y: i32) -> &TileDebugInfo {
+ &self.tiles[&TileOffset::new(x, y)]
+ }
+}
+
+/// Debug information about a tile that was dirty and was rasterized
+#[derive(Debug, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct DirtyTileDebugInfo {
+ pub local_valid_rect: PictureRect,
+ pub local_dirty_rect: PictureRect,
+}
+
+/// Debug information about the state of a tile
+#[derive(Debug, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum TileDebugInfo {
+ /// Tile was occluded by a tile in front of it
+ Occluded,
+ /// Tile was culled (not visible in current display port)
+ Culled,
+ /// Tile was valid (no rasterization was done) and visible
+ Valid,
+ /// Tile was dirty, and was updated
+ Dirty(DirtyTileDebugInfo),
+}
+
+impl TileDebugInfo {
+ pub fn is_occluded(&self) -> bool {
+ match self {
+ TileDebugInfo::Occluded => true,
+ TileDebugInfo::Culled |
+ TileDebugInfo::Valid |
+ TileDebugInfo::Dirty(..) => false,
+ }
+ }
+
+ pub fn is_valid(&self) -> bool {
+ match self {
+ TileDebugInfo::Valid => true,
+ TileDebugInfo::Culled |
+ TileDebugInfo::Occluded |
+ TileDebugInfo::Dirty(..) => false,
+ }
+ }
+
+ pub fn is_culled(&self) -> bool {
+ match self {
+ TileDebugInfo::Culled => true,
+ TileDebugInfo::Valid |
+ TileDebugInfo::Occluded |
+ TileDebugInfo::Dirty(..) => false,
+ }
+ }
+
+ pub fn as_dirty(&self) -> &DirtyTileDebugInfo {
+ match self {
+ TileDebugInfo::Occluded |
+ TileDebugInfo::Culled |
+ TileDebugInfo::Valid => {
+ panic!("not a dirty tile!");
+ }
+ TileDebugInfo::Dirty(ref info) => {
+ info
+ }
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/util.rs b/gfx/wr/webrender/src/util.rs
new file mode 100644
index 0000000000..ff6cfd7167
--- /dev/null
+++ b/gfx/wr/webrender/src/util.rs
@@ -0,0 +1,1631 @@
+/* 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 api::BorderRadius;
+use api::units::*;
+use euclid::{Point2D, Rect, Box2D, Size2D, Vector2D, point2, point3};
+use euclid::{default, Transform2D, Transform3D, Scale};
+use malloc_size_of::{MallocShallowSizeOf, MallocSizeOf, MallocSizeOfOps};
+use plane_split::{Clipper, Polygon};
+use std::{i32, f32, fmt, ptr};
+use std::borrow::Cow;
+use std::num::NonZeroUsize;
+use std::os::raw::c_void;
+use std::sync::Arc;
+use std::mem::replace;
+
+
+// Matches the definition of SK_ScalarNearlyZero in Skia.
+const NEARLY_ZERO: f32 = 1.0 / 4096.0;
+
+/// A typesafe helper that separates new value construction from
+/// vector growing, allowing LLVM to ideally construct the element in place.
+pub struct Allocation<'a, T: 'a> {
+ vec: &'a mut Vec<T>,
+ index: usize,
+}
+
+impl<'a, T> Allocation<'a, T> {
+ // writing is safe because alloc() ensured enough capacity
+ // and `Allocation` holds a mutable borrow to prevent anyone else
+ // from breaking this invariant.
+ #[inline(always)]
+ pub fn init(self, value: T) -> usize {
+ unsafe {
+ ptr::write(self.vec.as_mut_ptr().add(self.index), value);
+ self.vec.set_len(self.index + 1);
+ }
+ self.index
+ }
+}
+
+/// An entry into a vector, similar to `std::collections::hash_map::Entry`.
+pub enum VecEntry<'a, T: 'a> {
+ Vacant(Allocation<'a, T>),
+ Occupied(&'a mut T),
+}
+
+impl<'a, T> VecEntry<'a, T> {
+ #[inline(always)]
+ pub fn set(self, value: T) {
+ match self {
+ VecEntry::Vacant(alloc) => { alloc.init(value); }
+ VecEntry::Occupied(slot) => { *slot = value; }
+ }
+ }
+}
+
+pub trait VecHelper<T> {
+ /// Growns the vector by a single entry, returning the allocation.
+ fn alloc(&mut self) -> Allocation<T>;
+ /// Either returns an existing elemenet, or grows the vector by one.
+ /// Doesn't expect indices to be higher than the current length.
+ fn entry(&mut self, index: usize) -> VecEntry<T>;
+
+ /// Equivalent to `mem::replace(&mut vec, Vec::new())`
+ fn take(&mut self) -> Self;
+
+ /// Call clear and return self (useful for chaining with calls that move the vector).
+ fn cleared(self) -> Self;
+
+ /// Functionally equivalent to `mem::replace(&mut vec, Vec::new())` but tries
+ /// to keep the allocation in the caller if it is empty or replace it with a
+ /// pre-allocated vector.
+ fn take_and_preallocate(&mut self) -> Self;
+}
+
+impl<T> VecHelper<T> for Vec<T> {
+ fn alloc(&mut self) -> Allocation<T> {
+ let index = self.len();
+ if self.capacity() == index {
+ self.reserve(1);
+ }
+ Allocation {
+ vec: self,
+ index,
+ }
+ }
+
+ fn entry(&mut self, index: usize) -> VecEntry<T> {
+ if index < self.len() {
+ VecEntry::Occupied(unsafe {
+ self.get_unchecked_mut(index)
+ })
+ } else {
+ assert_eq!(index, self.len());
+ VecEntry::Vacant(self.alloc())
+ }
+ }
+
+ fn take(&mut self) -> Self {
+ replace(self, Vec::new())
+ }
+
+ fn cleared(mut self) -> Self {
+ self.clear();
+
+ self
+ }
+
+ fn take_and_preallocate(&mut self) -> Self {
+ let len = self.len();
+ if len == 0 {
+ self.clear();
+ return Vec::new();
+ }
+ replace(self, Vec::with_capacity(len + 8))
+ }
+}
+
+
+// Represents an optimized transform where there is only
+// a scale and translation (which are guaranteed to maintain
+// an axis align rectangle under transformation). The
+// scaling is applied first, followed by the translation.
+// TODO(gw): We should try and incorporate F <-> T units here,
+// but it's a bit tricky to do that now with the
+// way the current spatial tree works.
+#[repr(C)]
+#[derive(Debug, Clone, Copy, MallocSizeOf, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ScaleOffset {
+ pub scale: euclid::Vector2D<f32, euclid::UnknownUnit>,
+ pub offset: euclid::Vector2D<f32, euclid::UnknownUnit>,
+}
+
+impl ScaleOffset {
+ pub fn new(sx: f32, sy: f32, tx: f32, ty: f32) -> Self {
+ ScaleOffset {
+ scale: Vector2D::new(sx, sy),
+ offset: Vector2D::new(tx, ty),
+ }
+ }
+
+ pub fn identity() -> Self {
+ ScaleOffset {
+ scale: Vector2D::new(1.0, 1.0),
+ offset: Vector2D::zero(),
+ }
+ }
+
+ // Construct a ScaleOffset from a transform. Returns
+ // None if the matrix is not a pure scale / translation.
+ pub fn from_transform<F, T>(
+ m: &Transform3D<f32, F, T>,
+ ) -> Option<ScaleOffset> {
+
+ // To check that we have a pure scale / translation:
+ // Every field must match an identity matrix, except:
+ // - Any value present in tx,ty
+ // - Any value present in sx,sy
+
+ if m.m12.abs() > NEARLY_ZERO ||
+ m.m13.abs() > NEARLY_ZERO ||
+ m.m14.abs() > NEARLY_ZERO ||
+ m.m21.abs() > NEARLY_ZERO ||
+ m.m23.abs() > NEARLY_ZERO ||
+ m.m24.abs() > NEARLY_ZERO ||
+ m.m31.abs() > NEARLY_ZERO ||
+ m.m32.abs() > NEARLY_ZERO ||
+ (m.m33 - 1.0).abs() > NEARLY_ZERO ||
+ m.m34.abs() > NEARLY_ZERO ||
+ m.m43.abs() > NEARLY_ZERO ||
+ (m.m44 - 1.0).abs() > NEARLY_ZERO {
+ return None;
+ }
+
+ Some(ScaleOffset {
+ scale: Vector2D::new(m.m11, m.m22),
+ offset: Vector2D::new(m.m41, m.m42),
+ })
+ }
+
+ pub fn from_offset(offset: default::Vector2D<f32>) -> Self {
+ ScaleOffset {
+ scale: Vector2D::new(1.0, 1.0),
+ offset,
+ }
+ }
+
+ pub fn from_scale(scale: default::Vector2D<f32>) -> Self {
+ ScaleOffset {
+ scale,
+ offset: Vector2D::new(0.0, 0.0),
+ }
+ }
+
+ pub fn inverse(&self) -> Self {
+ ScaleOffset {
+ scale: Vector2D::new(
+ 1.0 / self.scale.x,
+ 1.0 / self.scale.y,
+ ),
+ offset: Vector2D::new(
+ -self.offset.x / self.scale.x,
+ -self.offset.y / self.scale.y,
+ ),
+ }
+ }
+
+ pub fn offset(&self, offset: default::Vector2D<f32>) -> Self {
+ self.accumulate(
+ &ScaleOffset {
+ scale: Vector2D::new(1.0, 1.0),
+ offset,
+ }
+ )
+ }
+
+ pub fn scale(&self, scale: f32) -> Self {
+ self.accumulate(
+ &ScaleOffset {
+ scale: Vector2D::new(scale, scale),
+ offset: Vector2D::zero(),
+ }
+ )
+ }
+
+ /// Produce a ScaleOffset that includes both self and other.
+ /// The 'self' ScaleOffset is applied after other.
+ /// This is equivalent to `Transform3D::pre_transform`.
+ pub fn accumulate(&self, other: &ScaleOffset) -> Self {
+ ScaleOffset {
+ scale: Vector2D::new(
+ self.scale.x * other.scale.x,
+ self.scale.y * other.scale.y,
+ ),
+ offset: Vector2D::new(
+ self.offset.x + self.scale.x * other.offset.x,
+ self.offset.y + self.scale.y * other.offset.y,
+ ),
+ }
+ }
+
+ pub fn map_rect<F, T>(&self, rect: &Box2D<f32, F>) -> Box2D<f32, T> {
+ // TODO(gw): The logic below can return an unexpected result if the supplied
+ // rect is invalid (has size < 0). Since Gecko currently supplied
+ // invalid rects in some cases, adding a max(0) here ensures that
+ // mapping an invalid rect retains the property that rect.is_empty()
+ // will return true (the mapped rect output will have size 0 instead
+ // of a negative size). In future we could catch / assert / fix
+ // these invalid rects earlier, and assert here instead.
+
+ let w = rect.width().max(0.0);
+ let h = rect.height().max(0.0);
+
+ let mut x0 = rect.min.x * self.scale.x + self.offset.x;
+ let mut y0 = rect.min.y * self.scale.y + self.offset.y;
+
+ let mut sx = w * self.scale.x;
+ let mut sy = h * self.scale.y;
+ // Handle negative scale. Previously, branchless float math was used to find the
+ // min / max vertices and size. However, that sequence of operations was producind
+ // additional floating point accuracy on android emulator builds, causing one test
+ // to fail an assert. Instead, we retain the same math as previously, and adjust
+ // the origin / size if required.
+
+ if self.scale.x < 0.0 {
+ x0 += sx;
+ sx = -sx;
+ }
+ if self.scale.y < 0.0 {
+ y0 += sy;
+ sy = -sy;
+ }
+
+ Box2D::from_origin_and_size(
+ Point2D::new(x0, y0),
+ Size2D::new(sx, sy),
+ )
+ }
+
+ pub fn unmap_rect<F, T>(&self, rect: &Box2D<f32, F>) -> Box2D<f32, T> {
+ // TODO(gw): The logic below can return an unexpected result if the supplied
+ // rect is invalid (has size < 0). Since Gecko currently supplied
+ // invalid rects in some cases, adding a max(0) here ensures that
+ // mapping an invalid rect retains the property that rect.is_empty()
+ // will return true (the mapped rect output will have size 0 instead
+ // of a negative size). In future we could catch / assert / fix
+ // these invalid rects earlier, and assert here instead.
+
+ let w = rect.width().max(0.0);
+ let h = rect.height().max(0.0);
+
+ let mut x0 = (rect.min.x - self.offset.x) / self.scale.x;
+ let mut y0 = (rect.min.y - self.offset.y) / self.scale.y;
+
+ let mut sx = w / self.scale.x;
+ let mut sy = h / self.scale.y;
+
+ // Handle negative scale. Previously, branchless float math was used to find the
+ // min / max vertices and size. However, that sequence of operations was producind
+ // additional floating point accuracy on android emulator builds, causing one test
+ // to fail an assert. Instead, we retain the same math as previously, and adjust
+ // the origin / size if required.
+
+ if self.scale.x < 0.0 {
+ x0 += sx;
+ sx = -sx;
+ }
+ if self.scale.y < 0.0 {
+ y0 += sy;
+ sy = -sy;
+ }
+
+ Box2D::from_origin_and_size(
+ Point2D::new(x0, y0),
+ Size2D::new(sx, sy),
+ )
+ }
+
+ pub fn map_vector<F, T>(&self, vector: &Vector2D<f32, F>) -> Vector2D<f32, T> {
+ Vector2D::new(
+ vector.x * self.scale.x,
+ vector.y * self.scale.y,
+ )
+ }
+
+ pub fn unmap_vector<F, T>(&self, vector: &Vector2D<f32, F>) -> Vector2D<f32, T> {
+ Vector2D::new(
+ vector.x / self.scale.x,
+ vector.y / self.scale.y,
+ )
+ }
+
+ pub fn map_point<F, T>(&self, point: &Point2D<f32, F>) -> Point2D<f32, T> {
+ Point2D::new(
+ point.x * self.scale.x + self.offset.x,
+ point.y * self.scale.y + self.offset.y,
+ )
+ }
+
+ pub fn unmap_point<F, T>(&self, point: &Point2D<f32, F>) -> Point2D<f32, T> {
+ Point2D::new(
+ (point.x - self.offset.x) / self.scale.x,
+ (point.y - self.offset.y) / self.scale.y,
+ )
+ }
+
+ pub fn to_transform<F, T>(&self) -> Transform3D<f32, F, T> {
+ Transform3D::new(
+ self.scale.x,
+ 0.0,
+ 0.0,
+ 0.0,
+
+ 0.0,
+ self.scale.y,
+ 0.0,
+ 0.0,
+
+ 0.0,
+ 0.0,
+ 1.0,
+ 0.0,
+
+ self.offset.x,
+ self.offset.y,
+ 0.0,
+ 1.0,
+ )
+ }
+}
+
+// TODO: Implement these in euclid!
+pub trait MatrixHelpers<Src, Dst> {
+ /// A port of the preserves2dAxisAlignment function in Skia.
+ /// Defined in the SkMatrix44 class.
+ fn preserves_2d_axis_alignment(&self) -> bool;
+ fn has_perspective_component(&self) -> bool;
+ fn has_2d_inverse(&self) -> bool;
+ /// Check if the matrix post-scaling on either the X or Y axes could cause geometry
+ /// transformed by this matrix to have scaling exceeding the supplied limit.
+ fn exceeds_2d_scale(&self, limit: f64) -> bool;
+ fn inverse_project(&self, target: &Point2D<f32, Dst>) -> Option<Point2D<f32, Src>>;
+ fn inverse_rect_footprint(&self, rect: &Box2D<f32, Dst>) -> Option<Box2D<f32, Src>>;
+ fn transform_kind(&self) -> TransformedRectKind;
+ fn is_simple_translation(&self) -> bool;
+ fn is_simple_2d_translation(&self) -> bool;
+ fn is_2d_scale_translation(&self) -> bool;
+ /// Return the determinant of the 2D part of the matrix.
+ fn determinant_2d(&self) -> f32;
+ /// This function returns a point in the `Src` space that projects into zero XY.
+ /// It ignores the Z coordinate and is usable for "flattened" transformations,
+ /// since they are not generally inversible.
+ fn inverse_project_2d_origin(&self) -> Option<Point2D<f32, Src>>;
+ /// Turn Z transformation into identity. This is useful when crossing "flat"
+ /// transform styled stacking contexts upon traversing the coordinate systems.
+ fn flatten_z_output(&mut self);
+
+ fn cast_unit<NewSrc, NewDst>(&self) -> Transform3D<f32, NewSrc, NewDst>;
+}
+
+impl<Src, Dst> MatrixHelpers<Src, Dst> for Transform3D<f32, Src, Dst> {
+ fn preserves_2d_axis_alignment(&self) -> bool {
+ if self.m14 != 0.0 || self.m24 != 0.0 {
+ return false;
+ }
+
+ let mut col0 = 0;
+ let mut col1 = 0;
+ let mut row0 = 0;
+ let mut row1 = 0;
+
+ if self.m11.abs() > NEARLY_ZERO {
+ col0 += 1;
+ row0 += 1;
+ }
+ if self.m12.abs() > NEARLY_ZERO {
+ col1 += 1;
+ row0 += 1;
+ }
+ if self.m21.abs() > NEARLY_ZERO {
+ col0 += 1;
+ row1 += 1;
+ }
+ if self.m22.abs() > NEARLY_ZERO {
+ col1 += 1;
+ row1 += 1;
+ }
+
+ col0 < 2 && col1 < 2 && row0 < 2 && row1 < 2
+ }
+
+ fn has_perspective_component(&self) -> bool {
+ self.m14.abs() > NEARLY_ZERO ||
+ self.m24.abs() > NEARLY_ZERO ||
+ self.m34.abs() > NEARLY_ZERO ||
+ (self.m44 - 1.0).abs() > NEARLY_ZERO
+ }
+
+ fn has_2d_inverse(&self) -> bool {
+ self.determinant_2d() != 0.0
+ }
+
+ fn exceeds_2d_scale(&self, limit: f64) -> bool {
+ let limit2 = (limit * limit) as f32;
+ self.m11 * self.m11 + self.m12 * self.m12 > limit2 ||
+ self.m21 * self.m21 + self.m22 * self.m22 > limit2
+ }
+
+ /// Find out a point in `Src` that would be projected into the `target`.
+ fn inverse_project(&self, target: &Point2D<f32, Dst>) -> Option<Point2D<f32, Src>> {
+ // form the linear equation for the hyperplane intersection
+ let m = Transform2D::<f32, Src, Dst>::new(
+ self.m11 - target.x * self.m14, self.m12 - target.y * self.m14,
+ self.m21 - target.x * self.m24, self.m22 - target.y * self.m24,
+ self.m41 - target.x * self.m44, self.m42 - target.y * self.m44,
+ );
+ let inv = m.inverse()?;
+ // we found the point, now check if it maps to the positive hemisphere
+ if inv.m31 * self.m14 + inv.m32 * self.m24 + self.m44 > 0.0 {
+ Some(Point2D::new(inv.m31, inv.m32))
+ } else {
+ None
+ }
+ }
+
+ fn inverse_rect_footprint(&self, rect: &Box2D<f32, Dst>) -> Option<Box2D<f32, Src>> {
+ Some(Box2D::from_points(&[
+ self.inverse_project(&rect.top_left())?,
+ self.inverse_project(&rect.top_right())?,
+ self.inverse_project(&rect.bottom_left())?,
+ self.inverse_project(&rect.bottom_right())?,
+ ]))
+ }
+
+ fn transform_kind(&self) -> TransformedRectKind {
+ if self.preserves_2d_axis_alignment() {
+ TransformedRectKind::AxisAligned
+ } else {
+ TransformedRectKind::Complex
+ }
+ }
+
+ fn is_simple_translation(&self) -> bool {
+ if (self.m11 - 1.0).abs() > NEARLY_ZERO ||
+ (self.m22 - 1.0).abs() > NEARLY_ZERO ||
+ (self.m33 - 1.0).abs() > NEARLY_ZERO ||
+ (self.m44 - 1.0).abs() > NEARLY_ZERO {
+ return false;
+ }
+
+ self.m12.abs() < NEARLY_ZERO && self.m13.abs() < NEARLY_ZERO &&
+ self.m14.abs() < NEARLY_ZERO && self.m21.abs() < NEARLY_ZERO &&
+ self.m23.abs() < NEARLY_ZERO && self.m24.abs() < NEARLY_ZERO &&
+ self.m31.abs() < NEARLY_ZERO && self.m32.abs() < NEARLY_ZERO &&
+ self.m34.abs() < NEARLY_ZERO
+ }
+
+ fn is_simple_2d_translation(&self) -> bool {
+ if !self.is_simple_translation() {
+ return false;
+ }
+
+ self.m43.abs() < NEARLY_ZERO
+ }
+
+ /* is this...
+ * X 0 0 0
+ * 0 Y 0 0
+ * 0 0 1 0
+ * a b 0 1
+ */
+ fn is_2d_scale_translation(&self) -> bool {
+ (self.m33 - 1.0).abs() < NEARLY_ZERO &&
+ (self.m44 - 1.0).abs() < NEARLY_ZERO &&
+ self.m12.abs() < NEARLY_ZERO && self.m13.abs() < NEARLY_ZERO && self.m14.abs() < NEARLY_ZERO &&
+ self.m21.abs() < NEARLY_ZERO && self.m23.abs() < NEARLY_ZERO && self.m24.abs() < NEARLY_ZERO &&
+ self.m31.abs() < NEARLY_ZERO && self.m32.abs() < NEARLY_ZERO && self.m34.abs() < NEARLY_ZERO &&
+ self.m43.abs() < NEARLY_ZERO
+ }
+
+ fn determinant_2d(&self) -> f32 {
+ self.m11 * self.m22 - self.m12 * self.m21
+ }
+
+ fn inverse_project_2d_origin(&self) -> Option<Point2D<f32, Src>> {
+ let det = self.determinant_2d();
+ if det != 0.0 {
+ let x = (self.m21 * self.m42 - self.m41 * self.m22) / det;
+ let y = (self.m12 * self.m41 - self.m11 * self.m42) / det;
+ Some(Point2D::new(x, y))
+ } else {
+ None
+ }
+ }
+
+ fn flatten_z_output(&mut self) {
+ self.m13 = 0.0;
+ self.m23 = 0.0;
+ self.m33 = 1.0;
+ self.m43 = 0.0;
+ //Note: we used to zero out m3? as well, see "reftests/flatten-all-flat.yaml" test
+ }
+
+ fn cast_unit<NewSrc, NewDst>(&self) -> Transform3D<f32, NewSrc, NewDst> {
+ Transform3D::new(
+ self.m11, self.m12, self.m13, self.m14,
+ self.m21, self.m22, self.m23, self.m24,
+ self.m31, self.m32, self.m33, self.m34,
+ self.m41, self.m42, self.m43, self.m44,
+ )
+ }
+}
+
+pub trait PointHelpers<U>
+where
+ Self: Sized,
+{
+ fn snap(&self) -> Self;
+}
+
+impl<U> PointHelpers<U> for Point2D<f32, U> {
+ fn snap(&self) -> Self {
+ Point2D::new(
+ (self.x + 0.5).floor(),
+ (self.y + 0.5).floor(),
+ )
+ }
+}
+
+pub trait RectHelpers<U>
+where
+ Self: Sized,
+{
+ fn from_floats(x0: f32, y0: f32, x1: f32, y1: f32) -> Self;
+ fn snap(&self) -> Self;
+}
+
+impl<U> RectHelpers<U> for Rect<f32, U> {
+ fn from_floats(x0: f32, y0: f32, x1: f32, y1: f32) -> Self {
+ Rect::new(
+ Point2D::new(x0, y0),
+ Size2D::new(x1 - x0, y1 - y0),
+ )
+ }
+
+ fn snap(&self) -> Self {
+ let origin = Point2D::new(
+ (self.origin.x + 0.5).floor(),
+ (self.origin.y + 0.5).floor(),
+ );
+ Rect::new(
+ origin,
+ Size2D::new(
+ (self.origin.x + self.size.width + 0.5).floor() - origin.x,
+ (self.origin.y + self.size.height + 0.5).floor() - origin.y,
+ ),
+ )
+ }
+}
+
+impl<U> RectHelpers<U> for Box2D<f32, U> {
+ fn from_floats(x0: f32, y0: f32, x1: f32, y1: f32) -> Self {
+ Box2D {
+ min: Point2D::new(x0, y0),
+ max: Point2D::new(x1, y1),
+ }
+ }
+
+ fn snap(&self) -> Self {
+ self.round()
+ }
+}
+
+pub trait VectorHelpers<U>
+where
+ Self: Sized,
+{
+ fn snap(&self) -> Self;
+}
+
+impl<U> VectorHelpers<U> for Vector2D<f32, U> {
+ fn snap(&self) -> Self {
+ Vector2D::new(
+ (self.x + 0.5).floor(),
+ (self.y + 0.5).floor(),
+ )
+ }
+}
+
+pub fn lerp(a: f32, b: f32, t: f32) -> f32 {
+ (b - a) * t + a
+}
+
+#[repr(u32)]
+#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum TransformedRectKind {
+ AxisAligned = 0,
+ Complex = 1,
+}
+
+#[inline(always)]
+pub fn pack_as_float(value: u32) -> f32 {
+ value as f32 + 0.5
+}
+
+#[inline]
+fn extract_inner_rect_impl<U>(
+ rect: &Box2D<f32, U>,
+ radii: &BorderRadius,
+ k: f32,
+) -> Option<Box2D<f32, U>> {
+ // `k` defines how much border is taken into account
+ // We enforce the offsets to be rounded to pixel boundaries
+ // by `ceil`-ing and `floor`-ing them
+
+ let xl = (k * radii.top_left.width.max(radii.bottom_left.width)).ceil();
+ let xr = (rect.width() - k * radii.top_right.width.max(radii.bottom_right.width)).floor();
+ let yt = (k * radii.top_left.height.max(radii.top_right.height)).ceil();
+ let yb =
+ (rect.height() - k * radii.bottom_left.height.max(radii.bottom_right.height)).floor();
+
+ if xl <= xr && yt <= yb {
+ Some(Box2D::from_origin_and_size(
+ Point2D::new(rect.min.x + xl, rect.min.y + yt),
+ Size2D::new(xr - xl, yb - yt),
+ ))
+ } else {
+ None
+ }
+}
+
+/// Return an aligned rectangle that is inside the clip region and doesn't intersect
+/// any of the bounding rectangles of the rounded corners.
+pub fn extract_inner_rect_safe<U>(
+ rect: &Box2D<f32, U>,
+ radii: &BorderRadius,
+) -> Option<Box2D<f32, U>> {
+ // value of `k==1.0` is used for extraction of the corner rectangles
+ // see `SEGMENT_CORNER_*` in `clip_shared.glsl`
+ extract_inner_rect_impl(rect, radii, 1.0)
+}
+
+#[cfg(test)]
+use euclid::vec3;
+
+#[cfg(test)]
+pub mod test {
+ use super::*;
+ use euclid::default::{Point2D, Size2D, Transform3D};
+ use euclid::{Angle, approxeq::ApproxEq};
+ use std::f32::consts::PI;
+ use crate::clip::{is_left_of_line, polygon_contains_point};
+ use crate::prim_store::PolygonKey;
+ use api::FillRule;
+
+ #[test]
+ fn inverse_project() {
+ let m0 = Transform3D::identity();
+ let p0 = Point2D::new(1.0, 2.0);
+ // an identical transform doesn't need any inverse projection
+ assert_eq!(m0.inverse_project(&p0), Some(p0));
+ let m1 = Transform3D::rotation(0.0, 1.0, 0.0, Angle::radians(-PI / 3.0));
+ // rotation by 60 degrees would imply scaling of X component by a factor of 2
+ assert_eq!(m1.inverse_project(&p0), Some(Point2D::new(2.0, 2.0)));
+ }
+
+ #[test]
+ fn inverse_project_footprint() {
+ let m = Transform3D::new(
+ 0.477499992, 0.135000005, -1.0, 0.000624999986,
+ -0.642787635, 0.766044438, 0.0, 0.0,
+ 0.766044438, 0.642787635, 0.0, 0.0,
+ 1137.10986, 113.71286, 402.0, 0.748749971,
+ );
+ let r = Box2D::from_size(Size2D::new(804.0, 804.0));
+ {
+ let points = &[
+ r.top_left(),
+ r.top_right(),
+ r.bottom_left(),
+ r.bottom_right(),
+ ];
+ let mi = m.inverse().unwrap();
+ // In this section, we do the forward and backward transformation
+ // to confirm that its bijective.
+ // We also do the inverse projection path, and confirm it functions the same way.
+ info!("Points:");
+ for p in points {
+ let pp = m.transform_point2d_homogeneous(*p);
+ let p3 = pp.to_point3d().unwrap();
+ let pi = mi.transform_point3d_homogeneous(p3);
+ let px = pi.to_point2d().unwrap();
+ let py = m.inverse_project(&pp.to_point2d().unwrap()).unwrap();
+ info!("\t{:?} -> {:?} -> {:?} -> ({:?} -> {:?}, {:?})", p, pp, p3, pi, px, py);
+ assert!(px.approx_eq_eps(p, &Point2D::new(0.001, 0.001)));
+ assert!(py.approx_eq_eps(p, &Point2D::new(0.001, 0.001)));
+ }
+ }
+ // project
+ let rp = project_rect(&m, &r, &Box2D::from_size(Size2D::new(1000.0, 1000.0))).unwrap();
+ info!("Projected {:?}", rp);
+ // one of the points ends up in the negative hemisphere
+ assert_eq!(m.inverse_project(&rp.min), None);
+ // inverse
+ if let Some(ri) = m.inverse_rect_footprint(&rp) {
+ // inverse footprint should be larger, since it doesn't know the original Z
+ assert!(ri.contains_box(&r), "Inverse {:?}", ri);
+ }
+ }
+
+ fn validate_convert(xref: &LayoutTransform) {
+ let so = ScaleOffset::from_transform(xref).unwrap();
+ let xf = so.to_transform();
+ assert!(xref.approx_eq(&xf));
+ }
+
+ #[test]
+ fn negative_scale_map_unmap() {
+ let xref = LayoutTransform::scale(1.0, -1.0, 1.0)
+ .pre_translate(LayoutVector3D::new(124.0, 38.0, 0.0));
+ let so = ScaleOffset::from_transform(&xref).unwrap();
+ let local_rect = Box2D {
+ min: LayoutPoint::new(50.0, -100.0),
+ max: LayoutPoint::new(250.0, 300.0),
+ };
+
+ let mapped_rect = so.map_rect::<LayoutPixel, DevicePixel>(&local_rect);
+ let xf_rect = project_rect(
+ &xref,
+ &local_rect,
+ &LayoutRect::max_rect(),
+ ).unwrap();
+
+ assert!(mapped_rect.min.x.approx_eq(&xf_rect.min.x));
+ assert!(mapped_rect.min.y.approx_eq(&xf_rect.min.y));
+ assert!(mapped_rect.max.x.approx_eq(&xf_rect.max.x));
+ assert!(mapped_rect.max.y.approx_eq(&xf_rect.max.y));
+
+ let unmapped_rect = so.unmap_rect::<DevicePixel, LayoutPixel>(&mapped_rect);
+ assert!(unmapped_rect.min.x.approx_eq(&local_rect.min.x));
+ assert!(unmapped_rect.min.y.approx_eq(&local_rect.min.y));
+ assert!(unmapped_rect.max.x.approx_eq(&local_rect.max.x));
+ assert!(unmapped_rect.max.y.approx_eq(&local_rect.max.y));
+ }
+
+ #[test]
+ fn scale_offset_convert() {
+ let xref = LayoutTransform::translation(130.0, 200.0, 0.0);
+ validate_convert(&xref);
+
+ let xref = LayoutTransform::scale(13.0, 8.0, 1.0);
+ validate_convert(&xref);
+
+ let xref = LayoutTransform::scale(0.5, 0.5, 1.0)
+ .pre_translate(LayoutVector3D::new(124.0, 38.0, 0.0));
+ validate_convert(&xref);
+
+ let xref = LayoutTransform::scale(30.0, 11.0, 1.0)
+ .then_translate(vec3(50.0, 240.0, 0.0));
+ validate_convert(&xref);
+ }
+
+ fn validate_inverse(xref: &LayoutTransform) {
+ let s0 = ScaleOffset::from_transform(xref).unwrap();
+ let s1 = s0.inverse().accumulate(&s0);
+ assert!((s1.scale.x - 1.0).abs() < NEARLY_ZERO &&
+ (s1.scale.y - 1.0).abs() < NEARLY_ZERO &&
+ s1.offset.x.abs() < NEARLY_ZERO &&
+ s1.offset.y.abs() < NEARLY_ZERO,
+ "{:?}",
+ s1);
+ }
+
+ #[test]
+ fn scale_offset_inverse() {
+ let xref = LayoutTransform::translation(130.0, 200.0, 0.0);
+ validate_inverse(&xref);
+
+ let xref = LayoutTransform::scale(13.0, 8.0, 1.0);
+ validate_inverse(&xref);
+
+ let xref = LayoutTransform::translation(124.0, 38.0, 0.0).
+ then_scale(0.5, 0.5, 1.0);
+
+ validate_inverse(&xref);
+
+ let xref = LayoutTransform::scale(30.0, 11.0, 1.0)
+ .then_translate(vec3(50.0, 240.0, 0.0));
+ validate_inverse(&xref);
+ }
+
+ fn validate_accumulate(x0: &LayoutTransform, x1: &LayoutTransform) {
+ let x = x1.then(&x0);
+
+ let s0 = ScaleOffset::from_transform(x0).unwrap();
+ let s1 = ScaleOffset::from_transform(x1).unwrap();
+
+ let s = s0.accumulate(&s1).to_transform();
+
+ assert!(x.approx_eq(&s), "{:?}\n{:?}", x, s);
+ }
+
+ #[test]
+ fn scale_offset_accumulate() {
+ let x0 = LayoutTransform::translation(130.0, 200.0, 0.0);
+ let x1 = LayoutTransform::scale(7.0, 3.0, 1.0);
+
+ validate_accumulate(&x0, &x1);
+ }
+
+ #[test]
+ fn inverse_project_2d_origin() {
+ let mut m = Transform3D::identity();
+ assert_eq!(m.inverse_project_2d_origin(), Some(Point2D::zero()));
+ m.m11 = 0.0;
+ assert_eq!(m.inverse_project_2d_origin(), None);
+ m.m21 = -2.0;
+ m.m22 = 0.0;
+ m.m12 = -0.5;
+ m.m41 = 1.0;
+ m.m42 = 0.5;
+ let origin = m.inverse_project_2d_origin().unwrap();
+ assert_eq!(origin, Point2D::new(1.0, 0.5));
+ assert_eq!(m.transform_point2d(origin), Some(Point2D::zero()));
+ }
+
+ #[test]
+ fn polygon_clip_is_left_of_point() {
+ // Define points of a line through (1, -3) and (-2, 6) to test against.
+ // If the triplet consisting of these two points and the test point
+ // form a counter-clockwise triangle, then the test point is on the
+ // left. The easiest way to visualize this is with an "ascending"
+ // line from low-Y to high-Y.
+ let p0_x = 1.0;
+ let p0_y = -3.0;
+ let p1_x = -2.0;
+ let p1_y = 6.0;
+
+ // Test some points to the left of the line.
+ assert!(is_left_of_line(-9.0, 0.0, p0_x, p0_y, p1_x, p1_y) > 0.0);
+ assert!(is_left_of_line(-1.0, 1.0, p0_x, p0_y, p1_x, p1_y) > 0.0);
+ assert!(is_left_of_line(1.0, -4.0, p0_x, p0_y, p1_x, p1_y) > 0.0);
+
+ // Test some points on the line.
+ assert!(is_left_of_line(-3.0, 9.0, p0_x, p0_y, p1_x, p1_y) == 0.0);
+ assert!(is_left_of_line(0.0, 0.0, p0_x, p0_y, p1_x, p1_y) == 0.0);
+ assert!(is_left_of_line(100.0, -300.0, p0_x, p0_y, p1_x, p1_y) == 0.0);
+
+ // Test some points to the right of the line.
+ assert!(is_left_of_line(0.0, 1.0, p0_x, p0_y, p1_x, p1_y) < 0.0);
+ assert!(is_left_of_line(-4.0, 13.0, p0_x, p0_y, p1_x, p1_y) < 0.0);
+ assert!(is_left_of_line(5.0, -12.0, p0_x, p0_y, p1_x, p1_y) < 0.0);
+ }
+
+ #[test]
+ fn polygon_clip_contains_point() {
+ // We define the points of a self-overlapping polygon, which we will
+ // use to create polygons with different windings and fill rules.
+ let p0 = LayoutPoint::new(4.0, 4.0);
+ let p1 = LayoutPoint::new(6.0, 4.0);
+ let p2 = LayoutPoint::new(4.0, 7.0);
+ let p3 = LayoutPoint::new(2.0, 1.0);
+ let p4 = LayoutPoint::new(8.0, 1.0);
+ let p5 = LayoutPoint::new(6.0, 7.0);
+
+ let poly_clockwise_nonzero = PolygonKey::new(
+ &[p5, p4, p3, p2, p1, p0].to_vec(), FillRule::Nonzero
+ );
+ let poly_clockwise_evenodd = PolygonKey::new(
+ &[p5, p4, p3, p2, p1, p0].to_vec(), FillRule::Evenodd
+ );
+ let poly_counter_clockwise_nonzero = PolygonKey::new(
+ &[p0, p1, p2, p3, p4, p5].to_vec(), FillRule::Nonzero
+ );
+ let poly_counter_clockwise_evenodd = PolygonKey::new(
+ &[p0, p1, p2, p3, p4, p5].to_vec(), FillRule::Evenodd
+ );
+
+ // We define a rect that provides a bounding clip area of
+ // the polygon.
+ let rect = LayoutRect::from_size(LayoutSize::new(10.0, 10.0));
+
+ // And we'll test three points of interest.
+ let p_inside_once = LayoutPoint::new(5.0, 3.0);
+ let p_inside_twice = LayoutPoint::new(5.0, 5.0);
+ let p_outside = LayoutPoint::new(9.0, 9.0);
+
+ // We should get the same results for both clockwise and
+ // counter-clockwise polygons.
+ // For nonzero polygons, the inside twice point is considered inside.
+ for poly_nonzero in vec![poly_clockwise_nonzero, poly_counter_clockwise_nonzero].iter() {
+ assert_eq!(polygon_contains_point(&p_inside_once, &rect, &poly_nonzero), true);
+ assert_eq!(polygon_contains_point(&p_inside_twice, &rect, &poly_nonzero), true);
+ assert_eq!(polygon_contains_point(&p_outside, &rect, &poly_nonzero), false);
+ }
+ // For evenodd polygons, the inside twice point is considered outside.
+ for poly_evenodd in vec![poly_clockwise_evenodd, poly_counter_clockwise_evenodd].iter() {
+ assert_eq!(polygon_contains_point(&p_inside_once, &rect, &poly_evenodd), true);
+ assert_eq!(polygon_contains_point(&p_inside_twice, &rect, &poly_evenodd), false);
+ assert_eq!(polygon_contains_point(&p_outside, &rect, &poly_evenodd), false);
+ }
+ }
+}
+
+pub trait MaxRect {
+ fn max_rect() -> Self;
+}
+
+impl MaxRect for DeviceIntRect {
+ fn max_rect() -> Self {
+ DeviceIntRect::from_origin_and_size(
+ DeviceIntPoint::new(i32::MIN / 2, i32::MIN / 2),
+ DeviceIntSize::new(i32::MAX, i32::MAX),
+ )
+ }
+}
+
+impl<U> MaxRect for Rect<f32, U> {
+ fn max_rect() -> Self {
+ // Having an unlimited bounding box is fine up until we try
+ // to cast it to `i32`, where we get `-2147483648` for any
+ // values larger than or equal to 2^31.
+ //
+ // Note: clamping to i32::MIN and i32::MAX is not a solution,
+ // with explanation left as an exercise for the reader.
+ const MAX_COORD: f32 = 1.0e9;
+
+ Rect::new(
+ Point2D::new(-MAX_COORD, -MAX_COORD),
+ Size2D::new(2.0 * MAX_COORD, 2.0 * MAX_COORD),
+ )
+ }
+}
+
+impl<U> MaxRect for Box2D<f32, U> {
+ fn max_rect() -> Self {
+ // Having an unlimited bounding box is fine up until we try
+ // to cast it to `i32`, where we get `-2147483648` for any
+ // values larger than or equal to 2^31.
+ //
+ // Note: clamping to i32::MIN and i32::MAX is not a solution,
+ // with explanation left as an exercise for the reader.
+ const MAX_COORD: f32 = 1.0e9;
+
+ Box2D::new(
+ Point2D::new(-MAX_COORD, -MAX_COORD),
+ Point2D::new(MAX_COORD, MAX_COORD),
+ )
+ }
+}
+
+/// An enum that tries to avoid expensive transformation matrix calculations
+/// when possible when dealing with non-perspective axis-aligned transformations.
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum FastTransform<Src, Dst> {
+ /// A simple offset, which can be used without doing any matrix math.
+ Offset(Vector2D<f32, Src>),
+
+ /// A 2D transformation with an inverse.
+ Transform {
+ transform: Transform3D<f32, Src, Dst>,
+ inverse: Option<Transform3D<f32, Dst, Src>>,
+ is_2d: bool,
+ },
+}
+
+impl<Src, Dst> Clone for FastTransform<Src, Dst> {
+ fn clone(&self) -> Self {
+ *self
+ }
+}
+
+impl<Src, Dst> Copy for FastTransform<Src, Dst> { }
+
+impl<Src, Dst> FastTransform<Src, Dst> {
+ pub fn identity() -> Self {
+ FastTransform::Offset(Vector2D::zero())
+ }
+
+ pub fn with_vector(offset: Vector2D<f32, Src>) -> Self {
+ FastTransform::Offset(offset)
+ }
+
+ pub fn with_scale_offset(scale_offset: ScaleOffset) -> Self {
+ if scale_offset.scale == Vector2D::new(1.0, 1.0) {
+ FastTransform::Offset(Vector2D::from_untyped(scale_offset.offset))
+ } else {
+ FastTransform::Transform {
+ transform: scale_offset.to_transform(),
+ inverse: Some(scale_offset.inverse().to_transform()),
+ is_2d: true,
+ }
+ }
+ }
+
+ #[inline(always)]
+ pub fn with_transform(transform: Transform3D<f32, Src, Dst>) -> Self {
+ if transform.is_simple_2d_translation() {
+ return FastTransform::Offset(Vector2D::new(transform.m41, transform.m42));
+ }
+ let inverse = transform.inverse();
+ let is_2d = transform.is_2d();
+ FastTransform::Transform { transform, inverse, is_2d}
+ }
+
+ pub fn to_transform(&self) -> Cow<Transform3D<f32, Src, Dst>> {
+ match *self {
+ FastTransform::Offset(offset) => Cow::Owned(
+ Transform3D::translation(offset.x, offset.y, 0.0)
+ ),
+ FastTransform::Transform { ref transform, .. } => Cow::Borrowed(transform),
+ }
+ }
+
+ /// Return true if this is an identity transform
+ #[allow(unused)]
+ pub fn is_identity(&self)-> bool {
+ match *self {
+ FastTransform::Offset(offset) => {
+ offset == Vector2D::zero()
+ }
+ FastTransform::Transform { ref transform, .. } => {
+ *transform == Transform3D::identity()
+ }
+ }
+ }
+
+ pub fn then<NewDst>(&self, other: &FastTransform<Dst, NewDst>) -> FastTransform<Src, NewDst> {
+ match *self {
+ FastTransform::Offset(offset) => match *other {
+ FastTransform::Offset(other_offset) => {
+ FastTransform::Offset(offset + other_offset * Scale::<_, _, Src>::new(1.0))
+ }
+ FastTransform::Transform { transform: ref other_transform, .. } => {
+ FastTransform::with_transform(
+ other_transform
+ .with_source::<Src>()
+ .pre_translate(offset.to_3d())
+ )
+ }
+ }
+ FastTransform::Transform { ref transform, ref inverse, is_2d } => match *other {
+ FastTransform::Offset(other_offset) => {
+ FastTransform::with_transform(
+ transform
+ .then_translate(other_offset.to_3d())
+ .with_destination::<NewDst>()
+ )
+ }
+ FastTransform::Transform { transform: ref other_transform, inverse: ref other_inverse, is_2d: other_is_2d } => {
+ FastTransform::Transform {
+ transform: transform.then(other_transform),
+ inverse: inverse.as_ref().and_then(|self_inv|
+ other_inverse.as_ref().map(|other_inv| other_inv.then(self_inv))
+ ),
+ is_2d: is_2d & other_is_2d,
+ }
+ }
+ }
+ }
+ }
+
+ pub fn pre_transform<NewSrc>(
+ &self,
+ other: &FastTransform<NewSrc, Src>
+ ) -> FastTransform<NewSrc, Dst> {
+ other.then(self)
+ }
+
+ pub fn pre_translate(&self, other_offset: Vector2D<f32, Src>) -> Self {
+ match *self {
+ FastTransform::Offset(offset) =>
+ FastTransform::Offset(offset + other_offset),
+ FastTransform::Transform { transform, .. } =>
+ FastTransform::with_transform(transform.pre_translate(other_offset.to_3d()))
+ }
+ }
+
+ pub fn then_translate(&self, other_offset: Vector2D<f32, Dst>) -> Self {
+ match *self {
+ FastTransform::Offset(offset) => {
+ FastTransform::Offset(offset + other_offset * Scale::<_, _, Src>::new(1.0))
+ }
+ FastTransform::Transform { ref transform, .. } => {
+ let transform = transform.then_translate(other_offset.to_3d());
+ FastTransform::with_transform(transform)
+ }
+ }
+ }
+
+ #[inline(always)]
+ pub fn is_backface_visible(&self) -> bool {
+ match *self {
+ FastTransform::Offset(..) => false,
+ FastTransform::Transform { inverse: None, .. } => false,
+ //TODO: fix this properly by taking "det|M33| * det|M34| > 0"
+ // see https://www.w3.org/Bugs/Public/show_bug.cgi?id=23014
+ FastTransform::Transform { inverse: Some(ref inverse), .. } => inverse.m33 < 0.0,
+ }
+ }
+
+ #[inline(always)]
+ pub fn transform_point2d(&self, point: Point2D<f32, Src>) -> Option<Point2D<f32, Dst>> {
+ match *self {
+ FastTransform::Offset(offset) => {
+ let new_point = point + offset;
+ Some(Point2D::from_untyped(new_point.to_untyped()))
+ }
+ FastTransform::Transform { ref transform, .. } => transform.transform_point2d(point),
+ }
+ }
+
+ #[inline(always)]
+ pub fn project_point2d(&self, point: Point2D<f32, Src>) -> Option<Point2D<f32, Dst>> {
+ match* self {
+ FastTransform::Offset(..) => self.transform_point2d(point),
+ FastTransform::Transform{ref transform, ..} => {
+ // Find a value for z that will transform to 0.
+
+ // The transformed value of z is computed as:
+ // z' = point.x * self.m13 + point.y * self.m23 + z * self.m33 + self.m43
+
+ // Solving for z when z' = 0 gives us:
+ let z = -(point.x * transform.m13 + point.y * transform.m23 + transform.m43) / transform.m33;
+
+ transform.transform_point3d(point3(point.x, point.y, z)).map(| p3 | point2(p3.x, p3.y))
+ }
+ }
+ }
+
+ #[inline(always)]
+ pub fn inverse(&self) -> Option<FastTransform<Dst, Src>> {
+ match *self {
+ FastTransform::Offset(offset) =>
+ Some(FastTransform::Offset(Vector2D::new(-offset.x, -offset.y))),
+ FastTransform::Transform { transform, inverse: Some(inverse), is_2d, } =>
+ Some(FastTransform::Transform {
+ transform: inverse,
+ inverse: Some(transform),
+ is_2d
+ }),
+ FastTransform::Transform { inverse: None, .. } => None,
+
+ }
+ }
+}
+
+impl<Src, Dst> From<Transform3D<f32, Src, Dst>> for FastTransform<Src, Dst> {
+ fn from(transform: Transform3D<f32, Src, Dst>) -> Self {
+ FastTransform::with_transform(transform)
+ }
+}
+
+impl<Src, Dst> From<Vector2D<f32, Src>> for FastTransform<Src, Dst> {
+ fn from(vector: Vector2D<f32, Src>) -> Self {
+ FastTransform::with_vector(vector)
+ }
+}
+
+pub type LayoutFastTransform = FastTransform<LayoutPixel, LayoutPixel>;
+pub type LayoutToWorldFastTransform = FastTransform<LayoutPixel, WorldPixel>;
+
+pub fn project_rect<F, T>(
+ transform: &Transform3D<f32, F, T>,
+ rect: &Box2D<f32, F>,
+ bounds: &Box2D<f32, T>,
+) -> Option<Box2D<f32, T>>
+ where F: fmt::Debug
+{
+ let homogens = [
+ transform.transform_point2d_homogeneous(rect.top_left()),
+ transform.transform_point2d_homogeneous(rect.top_right()),
+ transform.transform_point2d_homogeneous(rect.bottom_left()),
+ transform.transform_point2d_homogeneous(rect.bottom_right()),
+ ];
+
+ // Note: we only do the full frustum collision when the polygon approaches the camera plane.
+ // Otherwise, it will be clamped to the screen bounds anyway.
+ if homogens.iter().any(|h| h.w <= 0.0 || h.w.is_nan()) {
+ let mut clipper = Clipper::new();
+ let polygon = Polygon::from_rect(rect.to_rect().cast().cast_unit(), 1);
+
+ let planes = match Clipper::<usize>::frustum_planes(
+ &transform.cast_unit().cast(),
+ Some(bounds.to_rect().cast_unit().to_f64()),
+ ) {
+ Ok(planes) => planes,
+ Err(..) => return None,
+ };
+
+ for plane in planes {
+ clipper.add(plane);
+ }
+
+ let results = clipper.clip(polygon);
+ if results.is_empty() {
+ return None
+ }
+
+ Some(Box2D::from_points(results
+ .into_iter()
+ // filter out parts behind the view plane
+ .flat_map(|poly| &poly.points)
+ .map(|p| {
+ let mut homo = transform.transform_point2d_homogeneous(p.to_2d().to_f32().cast_unit());
+ homo.w = homo.w.max(0.00000001); // avoid infinite values
+ homo.to_point2d().unwrap()
+ })
+ ))
+ } else {
+ // we just checked for all the points to be in positive hemisphere, so `unwrap` is valid
+ Some(Box2D::from_points(&[
+ homogens[0].to_point2d().unwrap(),
+ homogens[1].to_point2d().unwrap(),
+ homogens[2].to_point2d().unwrap(),
+ homogens[3].to_point2d().unwrap(),
+ ]))
+ }
+}
+
+/// Run the first callback over all elements in the array. If the callback returns true,
+/// the element is removed from the array and moved to a second callback.
+///
+/// This is a simple implementation waiting for Vec::drain_filter to be stable.
+/// When that happens, code like:
+///
+/// let filter = |op| {
+/// match *op {
+/// Enum::Foo | Enum::Bar => true,
+/// Enum::Baz => false,
+/// }
+/// };
+/// drain_filter(
+/// &mut ops,
+/// filter,
+/// |op| {
+/// match op {
+/// Enum::Foo => { foo(); }
+/// Enum::Bar => { bar(); }
+/// Enum::Baz => { unreachable!(); }
+/// }
+/// },
+/// );
+///
+/// Can be rewritten as:
+///
+/// let filter = |op| {
+/// match *op {
+/// Enum::Foo | Enum::Bar => true,
+/// Enum::Baz => false,
+/// }
+/// };
+/// for op in ops.drain_filter(filter) {
+/// match op {
+/// Enum::Foo => { foo(); }
+/// Enum::Bar => { bar(); }
+/// Enum::Baz => { unreachable!(); }
+/// }
+/// }
+///
+/// See https://doc.rust-lang.org/std/vec/struct.Vec.html#method.drain_filter
+pub fn drain_filter<T, Filter, Action>(
+ vec: &mut Vec<T>,
+ mut filter: Filter,
+ mut action: Action,
+)
+where
+ Filter: FnMut(&mut T) -> bool,
+ Action: FnMut(T)
+{
+ let mut i = 0;
+ while i != vec.len() {
+ if filter(&mut vec[i]) {
+ action(vec.remove(i));
+ } else {
+ i += 1;
+ }
+ }
+}
+
+
+#[derive(Debug)]
+pub struct Recycler {
+ pub num_allocations: usize,
+}
+
+impl Recycler {
+ /// Maximum extra capacity that a recycled vector is allowed to have. If the actual capacity
+ /// is larger, we re-allocate the vector storage with lower capacity.
+ const MAX_EXTRA_CAPACITY_PERCENT: usize = 200;
+ /// Minimum extra capacity to keep when re-allocating the vector storage.
+ const MIN_EXTRA_CAPACITY_PERCENT: usize = 20;
+ /// Minimum sensible vector length to consider for re-allocation.
+ const MIN_VECTOR_LENGTH: usize = 16;
+
+ pub fn new() -> Self {
+ Recycler {
+ num_allocations: 0,
+ }
+ }
+
+ /// Clear a vector for re-use, while retaining the backing memory buffer. May shrink the buffer
+ /// if it's currently much larger than was actually used.
+ pub fn recycle_vec<T>(&mut self, vec: &mut Vec<T>) {
+ let extra_capacity = (vec.capacity() - vec.len()) * 100 / vec.len().max(Self::MIN_VECTOR_LENGTH);
+
+ if extra_capacity > Self::MAX_EXTRA_CAPACITY_PERCENT {
+ // Reduce capacity of the buffer if it is a lot larger than it needs to be. This prevents
+ // a frame with exceptionally large allocations to cause subsequent frames to retain
+ // more memory than they need.
+ //TODO: use `shrink_to` when it's stable
+ *vec = Vec::with_capacity(vec.len() + vec.len() * Self::MIN_EXTRA_CAPACITY_PERCENT / 100);
+ self.num_allocations += 1;
+ } else {
+ vec.clear();
+ }
+ }
+}
+
+/// Record the size of a data structure to preallocate a similar size
+/// at the next frame and avoid growing it too many time.
+#[derive(Copy, Clone, Debug)]
+pub struct Preallocator {
+ size: usize,
+}
+
+impl Preallocator {
+ pub fn new(initial_size: usize) -> Self {
+ Preallocator {
+ size: initial_size,
+ }
+ }
+
+ /// Record the size of a vector to preallocate it the next frame.
+ pub fn record_vec<T>(&mut self, vec: &Vec<T>) {
+ let len = vec.len();
+ if len > self.size {
+ self.size = len;
+ } else {
+ self.size = (self.size + len) / 2;
+ }
+ }
+
+ /// The size that we'll preallocate the vector with.
+ pub fn preallocation_size(&self) -> usize {
+ // Round up to multiple of 16 to avoid small tiny
+ // variations causing reallocations.
+ (self.size + 15) & !15
+ }
+
+ /// Preallocate vector storage.
+ ///
+ /// The preallocated amount depends on the length recorded in the last
+ /// record_vec call.
+ pub fn preallocate_vec<T>(&self, vec: &mut Vec<T>) {
+ let len = vec.len();
+ let cap = self.preallocation_size();
+ if len < cap {
+ vec.reserve(cap - len);
+ }
+ }
+}
+
+impl Default for Preallocator {
+ fn default() -> Self {
+ Self::new(0)
+ }
+}
+
+/// Arc wrapper to support measurement via MallocSizeOf.
+///
+/// Memory reporting for Arcs is tricky because of the risk of double-counting.
+/// One way to measure them is to keep a table of pointers that have already been
+/// traversed. The other way is to use knowledge of the program structure to
+/// identify which Arc instances should be measured and which should be skipped to
+/// avoid double-counting.
+///
+/// This struct implements the second approach. It identifies the "main" pointer
+/// to the Arc-ed resource, and measures the buffer as if it were an owned pointer.
+/// The programmer should ensure that there is at most one PrimaryArc for a given
+/// underlying ArcInner.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub struct PrimaryArc<T>(pub Arc<T>);
+
+impl<T> ::std::ops::Deref for PrimaryArc<T> {
+ type Target = Arc<T>;
+
+ #[inline]
+ fn deref(&self) -> &Arc<T> {
+ &self.0
+ }
+}
+
+impl<T> MallocShallowSizeOf for PrimaryArc<T> {
+ fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ unsafe {
+ // This is a bit sketchy, but std::sync::Arc doesn't expose the
+ // base pointer.
+ let raw_arc_ptr: *const Arc<T> = &self.0;
+ let raw_ptr_ptr: *const *const c_void = raw_arc_ptr as _;
+ let raw_ptr = *raw_ptr_ptr;
+ (ops.size_of_op)(raw_ptr)
+ }
+ }
+}
+
+impl<T: MallocSizeOf> MallocSizeOf for PrimaryArc<T> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.shallow_size_of(ops) + (**self).size_of(ops)
+ }
+}
+
+/// Computes the scale factors of this matrix; that is,
+/// the amounts each basis vector is scaled by.
+///
+/// This code comes from gecko gfx/2d/Matrix.h with the following
+/// modifications:
+///
+/// * Removed `xMajor` parameter.
+/// * All arithmetics is done with double precision.
+pub fn scale_factors<Src, Dst>(
+ mat: &Transform3D<f32, Src, Dst>
+) -> (f32, f32) {
+ let m11 = mat.m11 as f64;
+ let m12 = mat.m12 as f64;
+ // Determinant is just of the 2D component.
+ let det = m11 * mat.m22 as f64 - m12 * mat.m21 as f64;
+ if det == 0.0 {
+ return (0.0, 0.0);
+ }
+
+ // ignore mirroring
+ let det = det.abs();
+
+ let major = (m11 * m11 + m12 * m12).sqrt();
+ let minor = if major != 0.0 { det / major } else { 0.0 };
+
+ (major as f32, minor as f32)
+}
+
+#[test]
+fn scale_factors_large() {
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1748499
+ let mat = Transform3D::<f32, (), ()>::new(
+ 1.6534229920333123e27, 3.673100922561787e27, 0.0, 0.0,
+ -3.673100922561787e27, 1.6534229920333123e27, 0.0, 0.0,
+ 0.0, 0.0, 1.0, 0.0,
+ -828140552192.0, -1771307401216.0, 0.0, 1.0,
+ );
+ let (major, minor) = scale_factors(&mat);
+ assert!(major.is_normal() && minor.is_normal());
+}
+
+/// Clamp scaling factor to a power of two.
+///
+/// This code comes from gecko gfx/thebes/gfxUtils.cpp with the following
+/// modification:
+///
+/// * logs are taken in base 2 instead of base e.
+pub fn clamp_to_scale_factor(val: f32, round_down: bool) -> f32 {
+ // Arbitary scale factor limitation. We can increase this
+ // for better scaling performance at the cost of worse
+ // quality.
+ const SCALE_RESOLUTION: f32 = 2.0;
+
+ // Negative scaling is just a flip and irrelevant to
+ // our resolution calculation.
+ let val = val.abs();
+
+ let (val, inverse) = if val < 1.0 {
+ (1.0 / val, true)
+ } else {
+ (val, false)
+ };
+
+ let power = val.log2() / SCALE_RESOLUTION.log2();
+
+ // If power is within 1e-5 of an integer, round to nearest to
+ // prevent floating point errors, otherwise round up to the
+ // next integer value.
+ let power = if (power - power.round()).abs() < 1e-5 {
+ power.round()
+ } else if inverse != round_down {
+ // Use floor when we are either inverted or rounding down, but
+ // not both.
+ power.floor()
+ } else {
+ // Otherwise, ceil when we are not inverted and not rounding
+ // down, or we are inverted and rounding down.
+ power.ceil()
+ };
+
+ let scale = SCALE_RESOLUTION.powf(power);
+
+ if inverse {
+ 1.0 / scale
+ } else {
+ scale
+ }
+}
+
+/// Rounds a value up to the nearest multiple of mul
+pub fn round_up_to_multiple(val: usize, mul: NonZeroUsize) -> usize {
+ match val % mul.get() {
+ 0 => val,
+ rem => val - rem + mul.get(),
+ }
+}
+
+
+#[macro_export]
+macro_rules! c_str {
+ ($lit:expr) => {
+ unsafe {
+ std::ffi::CStr::from_ptr(concat!($lit, "\0").as_ptr()
+ as *const std::os::raw::c_char)
+ }
+ }
+}
+
+/// This is inspired by the `weak-table` crate.
+/// It holds a Vec of weak pointers that are garbage collected as the Vec
+pub struct WeakTable {
+ inner: Vec<std::sync::Weak<Vec<u8>>>
+}
+
+impl WeakTable {
+ pub fn new() -> WeakTable {
+ WeakTable { inner: Vec::new() }
+ }
+ pub fn insert(&mut self, x: std::sync::Weak<Vec<u8>>) {
+ if self.inner.len() == self.inner.capacity() {
+ self.remove_expired();
+
+ // We want to make sure that we change capacity()
+ // even if remove_expired() removes some entries
+ // so that we don't repeatedly hit remove_expired()
+ if self.inner.len() * 3 < self.inner.capacity() {
+ // We use a different multiple for shrinking then
+ // expanding so that we we don't accidentally
+ // oscilate.
+ self.inner.shrink_to_fit();
+ } else {
+ // Otherwise double our size
+ self.inner.reserve(self.inner.len())
+ }
+ }
+ self.inner.push(x);
+ }
+
+ fn remove_expired(&mut self) {
+ self.inner.retain(|x| x.strong_count() > 0)
+ }
+
+ pub fn iter(&self) -> impl Iterator<Item = Arc<Vec<u8>>> + '_ {
+ self.inner.iter().filter_map(|x| x.upgrade())
+ }
+}
+
+#[test]
+fn weak_table() {
+ let mut tbl = WeakTable::new();
+ let mut things = Vec::new();
+ let target_count = 50;
+ for _ in 0..target_count {
+ things.push(Arc::new(vec![4]));
+ }
+ for i in &things {
+ tbl.insert(Arc::downgrade(i))
+ }
+ assert_eq!(tbl.inner.len(), target_count);
+ drop(things);
+ assert_eq!(tbl.iter().count(), 0);
+
+ // make sure that we shrink the table if it gets too big
+ // by adding a bunch of dead items
+ for _ in 0..target_count*2 {
+ tbl.insert(Arc::downgrade(&Arc::new(vec![5])))
+ }
+ assert!(tbl.inner.capacity() <= 4);
+}
diff --git a/gfx/wr/webrender/src/visibility.rs b/gfx/wr/webrender/src/visibility.rs
new file mode 100644
index 0000000000..d11c4ff598
--- /dev/null
+++ b/gfx/wr/webrender/src/visibility.rs
@@ -0,0 +1,382 @@
+/* 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/. */
+
+//! # Visibility pass
+//!
+//! TODO: document what this pass does!
+//!
+
+use api::{DebugFlags};
+use api::units::*;
+use std::{usize};
+use crate::clip::ClipStore;
+use crate::composite::CompositeState;
+use crate::spatial_tree::{SpatialTree, SpatialNodeIndex};
+use crate::clip::{ClipChainInstance, ClipTree};
+use crate::frame_builder::FrameBuilderConfig;
+use crate::gpu_cache::GpuCache;
+use crate::picture::{PictureCompositeMode, ClusterFlags, SurfaceInfo, TileCacheInstance};
+use crate::picture::{SurfaceIndex, RasterConfig, SubSliceIndex};
+use crate::prim_store::{ClipTaskIndex, PictureIndex, PrimitiveInstanceKind};
+use crate::prim_store::{PrimitiveStore, PrimitiveInstance};
+use crate::render_backend::{DataStores, ScratchBuffer};
+use crate::resource_cache::ResourceCache;
+use crate::scene::SceneProperties;
+use crate::space::SpaceMapper;
+use crate::util::{MaxRect};
+
+pub struct FrameVisibilityContext<'a> {
+ pub spatial_tree: &'a SpatialTree,
+ pub global_screen_world_rect: WorldRect,
+ pub global_device_pixel_scale: DevicePixelScale,
+ pub debug_flags: DebugFlags,
+ pub scene_properties: &'a SceneProperties,
+ pub config: FrameBuilderConfig,
+ pub root_spatial_node_index: SpatialNodeIndex,
+}
+
+pub struct FrameVisibilityState<'a> {
+ pub clip_store: &'a mut ClipStore,
+ pub resource_cache: &'a mut ResourceCache,
+ pub gpu_cache: &'a mut GpuCache,
+ pub scratch: &'a mut ScratchBuffer,
+ pub data_stores: &'a mut DataStores,
+ pub clip_tree: &'a mut ClipTree,
+ pub composite_state: &'a mut CompositeState,
+ /// A stack of currently active off-screen surfaces during the
+ /// visibility frame traversal.
+ pub surface_stack: Vec<(PictureIndex, SurfaceIndex)>,
+}
+
+impl<'a> FrameVisibilityState<'a> {
+ pub fn push_surface(
+ &mut self,
+ pic_index: PictureIndex,
+ surface_index: SurfaceIndex,
+ ) {
+ self.surface_stack.push((pic_index, surface_index));
+ }
+
+ pub fn pop_surface(&mut self) {
+ self.surface_stack.pop().unwrap();
+ }
+}
+
+bitflags! {
+ /// A set of bitflags that can be set in the visibility information
+ /// for a primitive instance. This can be used to control how primitives
+ /// are treated during batching.
+ // TODO(gw): We should also move `is_compositor_surface` to be part of
+ // this flags struct.
+ #[cfg_attr(feature = "capture", derive(Serialize))]
+ pub struct PrimitiveVisibilityFlags: u8 {
+ /// Implies that this primitive covers the entire picture cache slice,
+ /// and can thus be dropped during batching and drawn with clear color.
+ const IS_BACKDROP = 1;
+ }
+}
+
+/// Contains the current state of the primitive's visibility.
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub enum VisibilityState {
+ /// Uninitialized - this should never be encountered after prim reset
+ Unset,
+ /// Culled for being off-screen, or not possible to render (e.g. missing image resource)
+ Culled,
+ /// A picture that doesn't have a surface - primitives are composed into the
+ /// parent picture with a surface.
+ PassThrough,
+ /// A primitive that has been found to be visible
+ Visible {
+ /// A set of flags that define how this primitive should be handled
+ /// during batching of visible primitives.
+ vis_flags: PrimitiveVisibilityFlags,
+
+ /// Sub-slice within the picture cache that this prim exists on
+ sub_slice_index: SubSliceIndex,
+ },
+}
+
+/// Information stored for a visible primitive about the visible
+/// rect and associated clip information.
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct PrimitiveVisibility {
+ /// The clip chain instance that was built for this primitive.
+ pub clip_chain: ClipChainInstance,
+
+ /// Current visibility state of the primitive.
+ // TODO(gw): Move more of the fields from this struct into
+ // the state enum.
+ pub state: VisibilityState,
+
+ /// An index into the clip task instances array in the primitive
+ /// store. If this is ClipTaskIndex::INVALID, then the primitive
+ /// has no clip mask. Otherwise, it may store the offset of the
+ /// global clip mask task for this primitive, or the first of
+ /// a list of clip task ids (one per segment).
+ pub clip_task_index: ClipTaskIndex,
+}
+
+impl PrimitiveVisibility {
+ pub fn new() -> Self {
+ PrimitiveVisibility {
+ state: VisibilityState::Unset,
+ clip_chain: ClipChainInstance::empty(),
+ clip_task_index: ClipTaskIndex::INVALID,
+ }
+ }
+
+ pub fn reset(&mut self) {
+ self.state = VisibilityState::Culled;
+ self.clip_task_index = ClipTaskIndex::INVALID;
+ }
+}
+
+pub fn update_prim_visibility(
+ pic_index: PictureIndex,
+ parent_surface_index: Option<SurfaceIndex>,
+ world_culling_rect: &WorldRect,
+ store: &PrimitiveStore,
+ prim_instances: &mut [PrimitiveInstance],
+ surfaces: &mut [SurfaceInfo],
+ is_root_tile_cache: bool,
+ frame_context: &FrameVisibilityContext,
+ frame_state: &mut FrameVisibilityState,
+ tile_cache: &mut TileCacheInstance,
+ ) {
+ let pic = &store.pictures[pic_index.0];
+
+ let (surface_index, pop_surface) = match pic.raster_config {
+ Some(RasterConfig { surface_index, composite_mode: PictureCompositeMode::TileCache { .. }, .. }) => {
+ (surface_index, false)
+ }
+ Some(ref raster_config) => {
+ frame_state.push_surface(
+ pic_index,
+ raster_config.surface_index,
+ );
+
+ let surface_local_rect = surfaces[raster_config.surface_index.0]
+ .unclipped_local_rect
+ .cast_unit();
+
+ // Let the picture cache know that we are pushing an off-screen
+ // surface, so it can treat dependencies of surface atomically.
+ tile_cache.push_surface(
+ surface_local_rect,
+ pic.spatial_node_index,
+ frame_context.spatial_tree,
+ );
+
+ (raster_config.surface_index, true)
+ }
+ None => {
+ (parent_surface_index.expect("bug: pass-through with no parent"), false)
+ }
+ };
+
+ let surface = &surfaces[surface_index.0 as usize];
+ let device_pixel_scale = surface.device_pixel_scale;
+ let mut map_local_to_surface = surface.map_local_to_surface.clone();
+ let map_surface_to_world = SpaceMapper::new_with_target(
+ frame_context.root_spatial_node_index,
+ surface.surface_spatial_node_index,
+ frame_context.global_screen_world_rect,
+ frame_context.spatial_tree,
+ );
+
+ for cluster in &pic.prim_list.clusters {
+ profile_scope!("cluster");
+
+ // Each prim instance must have reset called each frame, to clear
+ // indices into various scratch buffers. If this doesn't occur,
+ // the primitive may incorrectly be considered visible, which can
+ // cause unexpected conditions to occur later during the frame.
+ // Primitive instances are normally reset in the main loop below,
+ // but we must also reset them in the rare case that the cluster
+ // visibility has changed (due to an invalid transform and/or
+ // backface visibility changing for this cluster).
+ // TODO(gw): This is difficult to test for in CI - as a follow up,
+ // we should add a debug flag that validates the prim
+ // instance is always reset every frame to catch similar
+ // issues in future.
+ for prim_instance in &mut prim_instances[cluster.prim_range()] {
+ prim_instance.reset();
+ }
+
+ // Get the cluster and see if is visible
+ if !cluster.flags.contains(ClusterFlags::IS_VISIBLE) {
+ continue;
+ }
+
+ map_local_to_surface.set_target_spatial_node(
+ cluster.spatial_node_index,
+ frame_context.spatial_tree,
+ );
+
+ for prim_instance_index in cluster.prim_range() {
+ if let PrimitiveInstanceKind::Picture { pic_index, .. } = prim_instances[prim_instance_index].kind {
+ if !store.pictures[pic_index.0].is_visible(frame_context.spatial_tree) {
+ continue;
+ }
+
+ let is_passthrough = match store.pictures[pic_index.0].raster_config {
+ Some(..) => false,
+ None => true,
+ };
+
+ if !is_passthrough {
+ frame_state.clip_tree.push_clip_root_leaf(
+ prim_instances[prim_instance_index].clip_leaf_id,
+ );
+ }
+
+ update_prim_visibility(
+ pic_index,
+ Some(surface_index),
+ world_culling_rect,
+ store,
+ prim_instances,
+ surfaces,
+ false,
+ frame_context,
+ frame_state,
+ tile_cache,
+ );
+
+ if is_passthrough {
+ // Pass through pictures are always considered visible in all dirty tiles.
+ prim_instances[prim_instance_index].vis.state = VisibilityState::PassThrough;
+
+ continue;
+ } else {
+ frame_state.clip_tree.pop_clip_root();
+ }
+ }
+
+ let prim_instance = &mut prim_instances[prim_instance_index];
+
+ let local_coverage_rect = frame_state.data_stores.get_local_prim_coverage_rect(
+ prim_instance,
+ &store.pictures,
+ surfaces,
+ );
+
+ frame_state.clip_store.set_active_clips(
+ cluster.spatial_node_index,
+ map_local_to_surface.ref_spatial_node_index,
+ prim_instance.clip_leaf_id,
+ &frame_context.spatial_tree,
+ &frame_state.data_stores.clip,
+ frame_state.clip_tree,
+ );
+
+ let clip_chain = frame_state
+ .clip_store
+ .build_clip_chain_instance(
+ local_coverage_rect,
+ &map_local_to_surface,
+ &map_surface_to_world,
+ &frame_context.spatial_tree,
+ frame_state.gpu_cache,
+ frame_state.resource_cache,
+ device_pixel_scale,
+ &world_culling_rect,
+ &mut frame_state.data_stores.clip,
+ true,
+ );
+
+ prim_instance.vis.clip_chain = match clip_chain {
+ Some(clip_chain) => clip_chain,
+ None => {
+ continue;
+ }
+ };
+
+ tile_cache.update_prim_dependencies(
+ prim_instance,
+ cluster.spatial_node_index,
+ // It's OK to pass the local_coverage_rect here as it's only used by primitives
+ // (for compositor surfaces) that don't have inflation anyway.
+ local_coverage_rect,
+ frame_context,
+ frame_state.data_stores,
+ frame_state.clip_store,
+ &store.pictures,
+ frame_state.resource_cache,
+ &store.color_bindings,
+ &frame_state.surface_stack,
+ &mut frame_state.composite_state,
+ &mut frame_state.gpu_cache,
+ &mut frame_state.scratch.primitive,
+ is_root_tile_cache,
+ surfaces,
+ );
+ }
+ }
+
+ if pop_surface {
+ frame_state.pop_surface();
+ }
+
+ if let Some(ref rc) = pic.raster_config {
+ match rc.composite_mode {
+ PictureCompositeMode::TileCache { .. } => {}
+ _ => {
+ // Pop the off-screen surface from the picture cache stack
+ tile_cache.pop_surface();
+ }
+ }
+ }
+}
+
+pub fn compute_conservative_visible_rect(
+ clip_chain: &ClipChainInstance,
+ world_culling_rect: WorldRect,
+ prim_spatial_node_index: SpatialNodeIndex,
+ spatial_tree: &SpatialTree,
+) -> LayoutRect {
+ let root_spatial_node_index = spatial_tree.root_reference_frame_index();
+
+ // Mapping from picture space -> world space
+ let map_pic_to_world: SpaceMapper<PicturePixel, WorldPixel> = SpaceMapper::new_with_target(
+ root_spatial_node_index,
+ clip_chain.pic_spatial_node_index,
+ world_culling_rect,
+ spatial_tree,
+ );
+
+ // Mapping from local space -> picture space
+ let map_local_to_pic: SpaceMapper<LayoutPixel, PicturePixel> = SpaceMapper::new_with_target(
+ clip_chain.pic_spatial_node_index,
+ prim_spatial_node_index,
+ PictureRect::max_rect(),
+ spatial_tree,
+ );
+
+ // Unmap the world culling rect from world -> picture space. If this mapping fails due
+ // to matrix weirdness, best we can do is use the clip chain's local clip rect.
+ let pic_culling_rect = match map_pic_to_world.unmap(&world_culling_rect) {
+ Some(rect) => rect,
+ None => return clip_chain.local_clip_rect,
+ };
+
+ // Intersect the unmapped world culling rect with the primitive's clip chain rect that
+ // is in picture space (the clip-chain already takes into account the bounds of the
+ // primitive local_rect and local_clip_rect). If there is no intersection here, the
+ // primitive is not visible at all.
+ let pic_culling_rect = match pic_culling_rect.intersection(&clip_chain.pic_coverage_rect) {
+ Some(rect) => rect,
+ None => return LayoutRect::zero(),
+ };
+
+ // Unmap the picture culling rect from picture -> local space. If this mapping fails due
+ // to matrix weirdness, best we can do is use the clip chain's local clip rect.
+ match map_local_to_pic.unmap(&pic_culling_rect) {
+ Some(rect) => rect,
+ None => clip_chain.local_clip_rect,
+ }
+}
diff --git a/gfx/wr/webrender/tests/angle_shader_validation.rs b/gfx/wr/webrender/tests/angle_shader_validation.rs
new file mode 100644
index 0000000000..d6fe618de0
--- /dev/null
+++ b/gfx/wr/webrender/tests/angle_shader_validation.rs
@@ -0,0 +1,75 @@
+/* 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/. */
+
+extern crate mozangle;
+extern crate webrender;
+extern crate webrender_build;
+
+use mozangle::shaders::{BuiltInResources, Output, ShaderSpec, ShaderValidator};
+use webrender_build::shader::{ShaderFeatureFlags, ShaderVersion, build_shader_strings, get_shader_features};
+
+// from glslang
+const FRAGMENT_SHADER: u32 = 0x8B30;
+const VERTEX_SHADER: u32 = 0x8B31;
+
+#[test]
+fn validate_shaders() {
+ mozangle::shaders::initialize().unwrap();
+
+ let resources = BuiltInResources::default();
+ let vs_validator =
+ ShaderValidator::new(VERTEX_SHADER, ShaderSpec::Gles3, Output::Essl, &resources).unwrap();
+
+ let fs_validator =
+ ShaderValidator::new(FRAGMENT_SHADER, ShaderSpec::Gles3, Output::Essl, &resources).unwrap();
+
+ for (shader, configs) in get_shader_features(ShaderFeatureFlags::GLES) {
+ for config in configs {
+ let features = config.split(",").filter(|f| !f.is_empty()).collect::<Vec<_>>();
+
+ let (vs, fs) = build_shader_strings(
+ ShaderVersion::Gles,
+ &features,
+ shader,
+ &|f| webrender::get_unoptimized_shader_source(f, None)
+ );
+
+ let full_shader_name = format!("{} {}", shader, config);
+ validate(&vs_validator, &full_shader_name, vs);
+ validate(&fs_validator, &full_shader_name, fs);
+ }
+ }
+}
+
+fn validate(validator: &ShaderValidator, name: &str, source: String) {
+ // Check for each `switch` to have a `default`, see
+ // https://github.com/servo/webrender/wiki/Driver-issues#lack-of-default-case-in-a-switch
+ assert_eq!(source.matches("switch").count(), source.matches("default:").count(),
+ "Shader '{}' doesn't have all `switch` covered with `default` cases", name);
+ // Run Angle validator
+ match validator.compile_and_translate(&[&source]) {
+ Ok(_) => {
+ // Ensure that the shader uses at most 16 varying vectors. This counts the number of
+ // vectors assuming that the driver does not perform additional packing. The spec states
+ // that the driver should pack varyings, however, on some Adreno 3xx devices we have
+ // observed that this is not the case. See bug 1695912.
+ let varying_vectors = validator.get_num_unpacked_varying_vectors();
+ let max_varying_vectors = 16;
+ assert!(
+ varying_vectors <= max_varying_vectors,
+ "Shader {} uses {} varying vectors. Max allowed {}",
+ name, varying_vectors, max_varying_vectors
+ );
+
+ println!("Shader translated succesfully: {}", name);
+ }
+ Err(_) => {
+ panic!(
+ "Shader compilation failed: {}\n{}",
+ name,
+ validator.info_log()
+ );
+ }
+ }
+}
diff --git a/gfx/wr/webrender_api/Cargo.toml b/gfx/wr/webrender_api/Cargo.toml
new file mode 100644
index 0000000000..bb462bd825
--- /dev/null
+++ b/gfx/wr/webrender_api/Cargo.toml
@@ -0,0 +1,28 @@
+[package]
+name = "webrender_api"
+version = "0.62.0"
+authors = ["Glenn Watson <gw@intuitionlibrary.com>"]
+license = "MPL-2.0"
+repository = "https://github.com/servo/webrender"
+description = "Public API for WebRender"
+edition = "2018"
+
+[features]
+nightly = ["euclid/unstable", "serde/unstable"]
+serialize = []
+deserialize = []
+display_list_stats = []
+
+[dependencies]
+app_units = "0.7"
+bitflags = "1.2"
+byteorder = "1.2.1"
+euclid = { version = "0.22.6", features = ["serde"] }
+malloc_size_of_derive = "0.1"
+serde = { version = "1.0", features = ["rc"] }
+serde_derive = "1.0"
+serde_bytes = "0.11"
+time = "0.1"
+malloc_size_of = { version = "0.0.2", path = "../wr_malloc_size_of", package = "wr_malloc_size_of" }
+peek-poke = { version = "0.3", path = "../peek-poke", features = ["extras"] }
+crossbeam-channel = "0.5"
diff --git a/gfx/wr/webrender_api/src/channel.rs b/gfx/wr/webrender_api/src/channel.rs
new file mode 100644
index 0000000000..7d21c6e433
--- /dev/null
+++ b/gfx/wr/webrender_api/src/channel.rs
@@ -0,0 +1,180 @@
+/* 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::{Epoch, PipelineId};
+use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+use std::io::{self, Cursor, Error, ErrorKind, Read};
+use std::mem;
+
+pub use crossbeam_channel as crossbeam;
+
+#[cfg(not(target_os = "windows"))]
+pub use crossbeam_channel::{Sender, Receiver};
+
+#[cfg(target_os = "windows")]
+pub use std::sync::mpsc::{Sender, Receiver};
+
+#[derive(Clone)]
+pub struct Payload {
+ /// An epoch used to get the proper payload for a pipeline id frame request.
+ ///
+ /// TODO(emilio): Is this still relevant? We send the messages for the same
+ /// pipeline in order, so we shouldn't need it. Seems like this was only
+ /// wallpapering (in most cases) the underlying problem in #991.
+ pub epoch: Epoch,
+ /// A pipeline id to key the payload with, along with the epoch.
+ pub pipeline_id: PipelineId,
+ pub display_list_data: Vec<u8>,
+}
+
+impl Payload {
+ /// Convert the payload to a raw byte vector, in order for it to be
+ /// efficiently shared via shmem, for example.
+ /// This is a helper static method working on a slice.
+ pub fn construct_data(epoch: Epoch, pipeline_id: PipelineId, dl_data: &[u8]) -> Vec<u8> {
+ let mut data = Vec::with_capacity(
+ mem::size_of::<u32>() + 2 * mem::size_of::<u32>() + mem::size_of::<u64>() + dl_data.len(),
+ );
+ data.write_u32::<LittleEndian>(epoch.0).unwrap();
+ data.write_u32::<LittleEndian>(pipeline_id.0).unwrap();
+ data.write_u32::<LittleEndian>(pipeline_id.1).unwrap();
+ data.write_u64::<LittleEndian>(dl_data.len() as u64)
+ .unwrap();
+ data.extend_from_slice(dl_data);
+ data
+ }
+ /// Convert the payload to a raw byte vector, in order for it to be
+ /// efficiently shared via shmem, for example.
+ pub fn to_data(&self) -> Vec<u8> {
+ Self::construct_data(self.epoch, self.pipeline_id, &self.display_list_data)
+ }
+
+ /// Deserializes the given payload from a raw byte vector.
+ pub fn from_data(data: &[u8]) -> Payload {
+ let mut payload_reader = Cursor::new(data);
+ let epoch = Epoch(payload_reader.read_u32::<LittleEndian>().unwrap());
+ let pipeline_id = PipelineId(
+ payload_reader.read_u32::<LittleEndian>().unwrap(),
+ payload_reader.read_u32::<LittleEndian>().unwrap(),
+ );
+
+ let dl_size = payload_reader.read_u64::<LittleEndian>().unwrap() as usize;
+ let mut built_display_list_data = vec![0; dl_size];
+ payload_reader
+ .read_exact(&mut built_display_list_data[..])
+ .unwrap();
+
+ assert_eq!(payload_reader.position(), data.len() as u64);
+
+ Payload {
+ epoch,
+ pipeline_id,
+ display_list_data: built_display_list_data,
+ }
+ }
+}
+
+pub type PayloadSender = MsgSender<Payload>;
+
+pub type PayloadReceiver = MsgReceiver<Payload>;
+
+pub struct MsgReceiver<T> {
+ rx: Receiver<T>,
+}
+
+impl<T> MsgReceiver<T> {
+ pub fn recv(&self) -> Result<T, Error> {
+ self.rx.recv().map_err(|e| io::Error::new(ErrorKind::Other, e.to_string()))
+ }
+
+ pub fn to_crossbeam_receiver(self) -> Receiver<T> {
+ self.rx
+ }
+}
+
+#[derive(Clone)]
+pub struct MsgSender<T> {
+ tx: Sender<T>,
+}
+
+impl<T> MsgSender<T> {
+ pub fn send(&self, data: T) -> Result<(), Error> {
+ self.tx.send(data).map_err(|_| Error::new(ErrorKind::Other, "cannot send on closed channel"))
+ }
+}
+
+pub fn payload_channel() -> Result<(PayloadSender, PayloadReceiver), Error> {
+ let (tx, rx) = unbounded_channel();
+ Ok((PayloadSender { tx }, PayloadReceiver { rx }))
+}
+
+pub fn msg_channel<T>() -> Result<(MsgSender<T>, MsgReceiver<T>), Error> {
+ let (tx, rx) = unbounded_channel();
+ Ok((MsgSender { tx }, MsgReceiver { rx }))
+}
+
+///
+/// These serialize methods are needed to satisfy the compiler
+/// which uses these implementations for the recording tool.
+/// The recording tool only outputs messages that don't contain
+/// Senders or Receivers, so in theory these should never be
+/// called in the in-process config. If they are called,
+/// there may be a bug in the messages that the replay tool is writing.
+///
+
+impl<T> Serialize for MsgSender<T> {
+ fn serialize<S: Serializer>(&self, _: S) -> Result<S::Ok, S::Error> {
+ unreachable!();
+ }
+}
+
+impl<'de, T> Deserialize<'de> for MsgSender<T> {
+ fn deserialize<D>(_: D) -> Result<MsgSender<T>, D::Error>
+ where D: Deserializer<'de> {
+ unreachable!();
+ }
+}
+
+/// A create a channel intended for one-shot uses, for example the channels
+/// created to block on a synchronous query and then discarded,
+#[cfg(not(target_os = "windows"))]
+pub fn single_msg_channel<T>() -> (Sender<T>, Receiver<T>) {
+ crossbeam_channel::bounded(1)
+}
+
+/// A fast MPMC message channel that can hold a fixed number of messages.
+///
+/// If the channel is full, the sender will block upon sending extra messages
+/// until the receiver has consumed some messages.
+/// The capacity parameter should be chosen either:
+/// - high enough to avoid blocking on the common cases,
+/// - or, on the contrary, using the blocking behavior as a means to prevent
+/// fast producers from building up work faster than it is consumed.
+#[cfg(not(target_os = "windows"))]
+pub fn fast_channel<T>(capacity: usize) -> (Sender<T>, Receiver<T>) {
+ crossbeam_channel::bounded(capacity)
+}
+
+/// Creates an MPMC channel that is a bit slower than the fast_channel but doesn't
+/// have a limit on the number of messages held at a given time and therefore
+/// doesn't block when sending.
+#[cfg(not(target_os = "windows"))]
+pub use crossbeam_channel::unbounded as unbounded_channel;
+
+
+#[cfg(target_os = "windows")]
+pub fn fast_channel<T>(_cap: usize) -> (Sender<T>, Receiver<T>) {
+ std::sync::mpsc::channel()
+}
+
+#[cfg(target_os = "windows")]
+pub fn unbounded_channel<T>() -> (Sender<T>, Receiver<T>) {
+ std::sync::mpsc::channel()
+}
+
+#[cfg(target_os = "windows")]
+pub fn single_msg_channel<T>() -> (Sender<T>, Receiver<T>) {
+ std::sync::mpsc::channel()
+}
diff --git a/gfx/wr/webrender_api/src/color.rs b/gfx/wr/webrender_api/src/color.rs
new file mode 100644
index 0000000000..4b0a43a10d
--- /dev/null
+++ b/gfx/wr/webrender_api/src/color.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 peek_poke::PeekPoke;
+use std::cmp;
+use std::hash::{Hash, Hasher};
+
+/// Represents pre-multiplied RGBA colors with floating point numbers.
+///
+/// All components must be between 0.0 and 1.0.
+/// An alpha value of 1.0 is opaque while 0.0 is fully transparent.
+///
+/// In premultiplied colors transitions to transparent always look "nice"
+/// therefore they are used in CSS gradients.
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq, PartialOrd, Serialize)]
+pub struct PremultipliedColorF {
+ pub r: f32,
+ pub g: f32,
+ pub b: f32,
+ pub a: f32,
+}
+
+#[allow(missing_docs)]
+impl PremultipliedColorF {
+ pub const BLACK: PremultipliedColorF = PremultipliedColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 };
+ pub const TRANSPARENT: PremultipliedColorF = PremultipliedColorF { r: 0.0, g: 0.0, b: 0.0, a: 0.0 };
+ pub const WHITE: PremultipliedColorF = PremultipliedColorF { r: 1.0, g: 1.0, b: 1.0, a: 1.0 };
+
+ pub fn to_array(&self) -> [f32; 4] {
+ [self.r, self.g, self.b, self.a]
+ }
+}
+
+/// Represents RGBA screen colors with floating point numbers.
+///
+/// All components must be between 0.0 and 1.0.
+/// An alpha value of 1.0 is opaque while 0.0 is fully transparent.
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, Deserialize, MallocSizeOf, PartialEq, Serialize, PeekPoke)]
+pub struct ColorF {
+ pub r: f32,
+ pub g: f32,
+ pub b: f32,
+ pub a: f32,
+}
+
+#[allow(missing_docs)]
+impl ColorF {
+ pub const BLACK: ColorF = ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 };
+ pub const TRANSPARENT: ColorF = ColorF { r: 0.0, g: 0.0, b: 0.0, a: 0.0 };
+ pub const WHITE: ColorF = ColorF { r: 1.0, g: 1.0, b: 1.0, a: 1.0 };
+
+ /// Constructs a new `ColorF` from its components.
+ pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
+ ColorF { r, g, b, a }
+ }
+
+ /// Multiply the RGB channels (but not alpha) with a given factor.
+ pub fn scale_rgb(&self, scale: f32) -> Self {
+ ColorF {
+ r: self.r * scale,
+ g: self.g * scale,
+ b: self.b * scale,
+ a: self.a,
+ }
+ }
+
+ // Scale the alpha by a given factor.
+ pub fn scale_alpha(&self, scale: f32) -> Self {
+ ColorF {
+ r: self.r,
+ g: self.g,
+ b: self.b,
+ a: self.a * scale,
+ }
+ }
+
+ pub fn to_array(&self) -> [f32; 4] {
+ [self.r, self.g, self.b, self.a]
+ }
+
+ /// Multiply the RGB components with the alpha channel.
+ pub fn premultiplied(&self) -> PremultipliedColorF {
+ let c = self.scale_rgb(self.a);
+ PremultipliedColorF { r: c.r, g: c.g, b: c.b, a: c.a }
+ }
+}
+
+// Floats don't impl Hash/Eq/Ord...
+impl Eq for PremultipliedColorF {}
+impl Ord for PremultipliedColorF {
+ fn cmp(&self, other: &Self) -> cmp::Ordering {
+ self.partial_cmp(other).unwrap_or(cmp::Ordering::Equal)
+ }
+}
+
+#[cfg_attr(feature = "cargo-clippy", allow(clippy::derive_hash_xor_eq))]
+impl Hash for PremultipliedColorF {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ // Note: this is inconsistent with the Eq impl for -0.0 (don't care).
+ self.r.to_bits().hash(state);
+ self.g.to_bits().hash(state);
+ self.b.to_bits().hash(state);
+ self.a.to_bits().hash(state);
+ }
+}
+
+/// Represents RGBA screen colors with one byte per channel.
+///
+/// If the alpha value `a` is 255 the color is opaque.
+#[repr(C)]
+#[derive(Clone, Copy, Hash, Eq, Debug, Deserialize, MallocSizeOf, PartialEq)]
+#[derive(PartialOrd, Ord, Serialize, PeekPoke, Default)]
+pub struct ColorU {
+ pub r: u8,
+ pub g: u8,
+ pub b: u8,
+ pub a: u8,
+}
+
+impl ColorU {
+ /// Constructs a new additive `ColorU` from its components.
+ pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
+ ColorU { r, g, b, a }
+ }
+}
+
+fn round_to_int(x: f32) -> u8 {
+ debug_assert!((0.0 <= x) && (x <= 1.0), "{} should be between 0 and 1", x);
+ let f = (255.0 * x) + 0.5;
+ let val = f.floor();
+ debug_assert!(val <= 255.0);
+ val as u8
+}
+
+// TODO: We shouldn't really convert back to `ColorU` ever,
+// since it's lossy. One of the blockers is that all of our debug colors
+// are specified in `ColorF`. Changing it to `ColorU` would be nice.
+impl From<ColorF> for ColorU {
+ fn from(color: ColorF) -> Self {
+ ColorU {
+ r: round_to_int(color.r),
+ g: round_to_int(color.g),
+ b: round_to_int(color.b),
+ a: round_to_int(color.a),
+ }
+ }
+}
+
+impl From<ColorU> for ColorF {
+ fn from(color: ColorU) -> Self {
+ ColorF {
+ r: color.r as f32 / 255.0,
+ g: color.g as f32 / 255.0,
+ b: color.b as f32 / 255.0,
+ a: color.a as f32 / 255.0,
+ }
+ }
+}
diff --git a/gfx/wr/webrender_api/src/display_item.rs b/gfx/wr/webrender_api/src/display_item.rs
new file mode 100644
index 0000000000..675386cfae
--- /dev/null
+++ b/gfx/wr/webrender_api/src/display_item.rs
@@ -0,0 +1,1801 @@
+/* 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, Angle};
+use peek_poke::PeekPoke;
+use std::ops::Not;
+// local imports
+use crate::font;
+use crate::{APZScrollGeneration, HasScrollLinkedEffect, PipelineId, PropertyBinding};
+use crate::color::ColorF;
+use crate::image::{ColorDepth, ImageKey};
+use crate::units::*;
+use std::hash::{Hash, Hasher};
+
+// ******************************************************************
+// * NOTE: some of these structs have an "IMPLICIT" comment. *
+// * This indicates that the BuiltDisplayList will have serialized *
+// * a list of values nearby that this item consumes. The traversal *
+// * iterator should handle finding these. DebugDisplayItem should *
+// * make them explicit. *
+// ******************************************************************
+
+/// A tag that can be used to identify items during hit testing. If the tag
+/// is missing then the item doesn't take part in hit testing at all. This
+/// is composed of two numbers. In Servo, the first is an identifier while the
+/// second is used to select the cursor that should be used during mouse
+/// movement. In Gecko, the first is a scrollframe identifier, while the second
+/// is used to store various flags that APZ needs to properly process input
+/// events.
+pub type ItemTag = (u64, u16);
+
+/// An identifier used to refer to previously sent display items. Currently it
+/// refers to individual display items, but this may change later.
+pub type ItemKey = u16;
+
+bitflags! {
+ #[repr(C)]
+ #[derive(Deserialize, MallocSizeOf, Serialize, PeekPoke)]
+ pub struct PrimitiveFlags: u8 {
+ /// The CSS backface-visibility property (yes, it can be really granular)
+ const IS_BACKFACE_VISIBLE = 1 << 0;
+ /// If set, this primitive represents a scroll bar container
+ const IS_SCROLLBAR_CONTAINER = 1 << 1;
+ /// This is used as a performance hint - this primitive may be promoted to a native
+ /// compositor surface under certain (implementation specific) conditions. This
+ /// is typically used for large videos, and canvas elements.
+ const PREFER_COMPOSITOR_SURFACE = 1 << 2;
+ /// If set, this primitive can be passed directly to the compositor via its
+ /// ExternalImageId, and the compositor will use the native image directly.
+ /// Used as a further extension on top of PREFER_COMPOSITOR_SURFACE.
+ const SUPPORTS_EXTERNAL_COMPOSITOR_SURFACE = 1 << 3;
+ /// This flags disables snapping and forces anti-aliasing even if the primitive is axis-aligned.
+ const ANTIALISED = 1 << 4;
+ /// If true, this primitive is used as a background for checkerboarding
+ const CHECKERBOARD_BACKGROUND = 1 << 5;
+ }
+}
+
+impl Default for PrimitiveFlags {
+ fn default() -> Self {
+ PrimitiveFlags::IS_BACKFACE_VISIBLE
+ }
+}
+
+/// A grouping of fields a lot of display items need, just to avoid
+/// repeating these over and over in this file.
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct CommonItemProperties {
+ /// Bounds of the display item to clip to. Many items are logically
+ /// infinite, and rely on this clip_rect to define their bounds
+ /// (solid colors, background-images, gradients, etc).
+ pub clip_rect: LayoutRect,
+ /// Additional clips
+ pub clip_chain_id: ClipChainId,
+ /// The coordinate-space the item is in (yes, it can be really granular)
+ pub spatial_id: SpatialId,
+ /// Various flags describing properties of this primitive.
+ pub flags: PrimitiveFlags,
+}
+
+impl CommonItemProperties {
+ /// Convenience for tests.
+ pub fn new(
+ clip_rect: LayoutRect,
+ space_and_clip: SpaceAndClipInfo,
+ ) -> Self {
+ Self {
+ clip_rect,
+ spatial_id: space_and_clip.spatial_id,
+ clip_chain_id: space_and_clip.clip_chain_id,
+ flags: PrimitiveFlags::default(),
+ }
+ }
+}
+
+/// Per-primitive information about the nodes in the clip tree and
+/// the spatial tree that the primitive belongs to.
+///
+/// Note: this is a separate struct from `PrimitiveInfo` because
+/// it needs indirectional mapping during the DL flattening phase,
+/// turning into `ScrollNodeAndClipChain`.
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct SpaceAndClipInfo {
+ pub spatial_id: SpatialId,
+ pub clip_chain_id: ClipChainId,
+}
+
+impl SpaceAndClipInfo {
+ /// Create a new space/clip info associated with the root
+ /// scroll frame.
+ pub fn root_scroll(pipeline_id: PipelineId) -> Self {
+ SpaceAndClipInfo {
+ spatial_id: SpatialId::root_scroll_node(pipeline_id),
+ clip_chain_id: ClipChainId::INVALID,
+ }
+ }
+}
+
+/// Defines a caller provided key that is unique for a given spatial node, and is stable across
+/// display lists. WR uses this to determine which spatial nodes are added / removed for a new
+/// display list. The content itself is arbitrary and opaque to WR, the only thing that matters
+/// is that it's unique and stable between display lists.
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize, PeekPoke, Default, Eq, Hash)]
+pub struct SpatialTreeItemKey {
+ key0: u64,
+ key1: u64,
+}
+
+impl SpatialTreeItemKey {
+ pub fn new(key0: u64, key1: u64) -> Self {
+ SpatialTreeItemKey {
+ key0,
+ key1,
+ }
+ }
+}
+
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub enum SpatialTreeItem {
+ ScrollFrame(ScrollFrameDescriptor),
+ ReferenceFrame(ReferenceFrameDescriptor),
+ StickyFrame(StickyFrameDescriptor),
+ Invalid,
+}
+
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub enum DisplayItem {
+ // These are the "real content" display items
+ Rectangle(RectangleDisplayItem),
+ ClearRectangle(ClearRectangleDisplayItem),
+ HitTest(HitTestDisplayItem),
+ Text(TextDisplayItem),
+ Line(LineDisplayItem),
+ Border(BorderDisplayItem),
+ BoxShadow(BoxShadowDisplayItem),
+ PushShadow(PushShadowDisplayItem),
+ Gradient(GradientDisplayItem),
+ RadialGradient(RadialGradientDisplayItem),
+ ConicGradient(ConicGradientDisplayItem),
+ Image(ImageDisplayItem),
+ RepeatingImage(RepeatingImageDisplayItem),
+ YuvImage(YuvImageDisplayItem),
+ BackdropFilter(BackdropFilterDisplayItem),
+
+ // Clips
+ RectClip(RectClipDisplayItem),
+ RoundedRectClip(RoundedRectClipDisplayItem),
+ ImageMaskClip(ImageMaskClipDisplayItem),
+ ClipChain(ClipChainItem),
+
+ // Spaces and Frames that content can be scoped under.
+ Iframe(IframeDisplayItem),
+ PushReferenceFrame(ReferenceFrameDisplayListItem),
+ PushStackingContext(PushStackingContextDisplayItem),
+
+ // These marker items indicate an array of data follows, to be used for the
+ // next non-marker item.
+ SetGradientStops,
+ SetFilterOps,
+ SetFilterData,
+ SetFilterPrimitives,
+ SetPoints,
+
+ // These marker items terminate a scope introduced by a previous item.
+ PopReferenceFrame,
+ PopStackingContext,
+ PopAllShadows,
+
+ ReuseItems(ItemKey),
+ RetainedItems(ItemKey),
+}
+
+/// This is a "complete" version of the DisplayItem, with all implicit trailing
+/// arrays included, for debug serialization (captures).
+#[cfg(any(feature = "serialize", feature = "deserialize"))]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "deserialize", derive(Deserialize))]
+pub enum DebugDisplayItem {
+ Rectangle(RectangleDisplayItem),
+ ClearRectangle(ClearRectangleDisplayItem),
+ HitTest(HitTestDisplayItem),
+ Text(TextDisplayItem, Vec<font::GlyphInstance>),
+ Line(LineDisplayItem),
+ Border(BorderDisplayItem),
+ BoxShadow(BoxShadowDisplayItem),
+ PushShadow(PushShadowDisplayItem),
+ Gradient(GradientDisplayItem),
+ RadialGradient(RadialGradientDisplayItem),
+ ConicGradient(ConicGradientDisplayItem),
+ Image(ImageDisplayItem),
+ RepeatingImage(RepeatingImageDisplayItem),
+ YuvImage(YuvImageDisplayItem),
+ BackdropFilter(BackdropFilterDisplayItem),
+
+ ImageMaskClip(ImageMaskClipDisplayItem),
+ RoundedRectClip(RoundedRectClipDisplayItem),
+ RectClip(RectClipDisplayItem),
+ ClipChain(ClipChainItem, Vec<ClipId>),
+
+ Iframe(IframeDisplayItem),
+ PushReferenceFrame(ReferenceFrameDisplayListItem),
+ PushStackingContext(PushStackingContextDisplayItem),
+
+ SetGradientStops(Vec<GradientStop>),
+ SetFilterOps(Vec<FilterOp>),
+ SetFilterData(FilterData),
+ SetFilterPrimitives(Vec<FilterPrimitive>),
+ SetPoints(Vec<LayoutPoint>),
+
+ PopReferenceFrame,
+ PopStackingContext,
+ PopAllShadows,
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct ImageMaskClipDisplayItem {
+ pub id: ClipId,
+ pub spatial_id: SpatialId,
+ pub image_mask: ImageMask,
+ pub fill_rule: FillRule,
+} // IMPLICIT points: Vec<LayoutPoint>
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct RectClipDisplayItem {
+ pub id: ClipId,
+ pub spatial_id: SpatialId,
+ pub clip_rect: LayoutRect,
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct RoundedRectClipDisplayItem {
+ pub id: ClipId,
+ pub spatial_id: SpatialId,
+ pub clip: ComplexClipRegion,
+}
+
+/// The minimum and maximum allowable offset for a sticky frame in a single dimension.
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct StickyOffsetBounds {
+ /// The minimum offset for this frame, typically a negative value, which specifies how
+ /// far in the negative direction the sticky frame can offset its contents in this
+ /// dimension.
+ pub min: f32,
+
+ /// The maximum offset for this frame, typically a positive value, which specifies how
+ /// far in the positive direction the sticky frame can offset its contents in this
+ /// dimension.
+ pub max: f32,
+}
+
+impl StickyOffsetBounds {
+ pub fn new(min: f32, max: f32) -> StickyOffsetBounds {
+ StickyOffsetBounds { min, max }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct StickyFrameDescriptor {
+ pub id: SpatialId,
+ pub parent_spatial_id: SpatialId,
+ pub bounds: LayoutRect,
+
+ /// The margins that should be maintained between the edge of the parent viewport and this
+ /// sticky frame. A margin of None indicates that the sticky frame should not stick at all
+ /// to that particular edge of the viewport.
+ pub margins: SideOffsets2D<Option<f32>, LayoutPixel>,
+
+ /// The minimum and maximum vertical offsets for this sticky frame. Ignoring these constraints,
+ /// the sticky frame will continue to stick to the edge of the viewport as its original
+ /// position is scrolled out of view. Constraints specify a maximum and minimum offset from the
+ /// original position relative to non-sticky content within the same scrolling frame.
+ pub vertical_offset_bounds: StickyOffsetBounds,
+
+ /// The minimum and maximum horizontal offsets for this sticky frame. Ignoring these constraints,
+ /// the sticky frame will continue to stick to the edge of the viewport as its original
+ /// position is scrolled out of view. Constraints specify a maximum and minimum offset from the
+ /// original position relative to non-sticky content within the same scrolling frame.
+ pub horizontal_offset_bounds: StickyOffsetBounds,
+
+ /// The amount of offset that has already been applied to the sticky frame. A positive y
+ /// component this field means that a top-sticky item was in a scrollframe that has been
+ /// scrolled down, such that the sticky item's position needed to be offset downwards by
+ /// `previously_applied_offset.y`. A negative y component corresponds to the upward offset
+ /// applied due to bottom-stickiness. The x-axis works analogously.
+ pub previously_applied_offset: LayoutVector2D,
+
+ /// A unique (per-pipeline) key for this spatial that is stable across display lists.
+ pub key: SpatialTreeItemKey,
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct ScrollFrameDescriptor {
+ /// The id of the space this scroll frame creates
+ pub scroll_frame_id: SpatialId,
+ /// The size of the contents this contains (so the backend knows how far it can scroll).
+ // FIXME: this can *probably* just be a size? Origin seems to just get thrown out.
+ pub content_rect: LayoutRect,
+ pub frame_rect: LayoutRect,
+ pub parent_space: SpatialId,
+ pub external_id: ExternalScrollId,
+ /// The amount this scrollframe has already been scrolled by, in the caller.
+ /// This means that all the display items that are inside the scrollframe
+ /// will have their coordinates shifted by this amount, and this offset
+ /// should be added to those display item coordinates in order to get a
+ /// normalized value that is consistent across display lists.
+ pub external_scroll_offset: LayoutVector2D,
+ /// The generation of the external_scroll_offset.
+ pub scroll_offset_generation: APZScrollGeneration,
+ /// Whether this scrollframe document has any scroll-linked effect or not.
+ pub has_scroll_linked_effect: HasScrollLinkedEffect,
+ /// A unique (per-pipeline) key for this spatial that is stable across display lists.
+ pub key: SpatialTreeItemKey,
+}
+
+/// A solid or an animating color to draw (may not actually be a rectangle due to complex clips)
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct RectangleDisplayItem {
+ pub common: CommonItemProperties,
+ pub bounds: LayoutRect,
+ pub color: PropertyBinding<ColorF>,
+}
+
+/// Clears all colors from the area, making it possible to cut holes in the window.
+/// (useful for things like the macos frosted-glass effect).
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct ClearRectangleDisplayItem {
+ pub common: CommonItemProperties,
+ pub bounds: LayoutRect,
+}
+
+/// A minimal hit-testable item for the parent browser's convenience, and is
+/// slimmer than a RectangleDisplayItem (no color). The existence of this as a
+/// distinct item also makes it easier to inspect/debug display items.
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct HitTestDisplayItem {
+ pub rect: LayoutRect,
+ pub clip_chain_id: ClipChainId,
+ pub spatial_id: SpatialId,
+ pub flags: PrimitiveFlags,
+ pub tag: ItemTag,
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct LineDisplayItem {
+ pub common: CommonItemProperties,
+ /// We need a separate rect from common.clip_rect to encode cute
+ /// tricks that firefox does to make a series of text-decorations seamlessly
+ /// line up -- snapping the decorations to a multiple of their period, and
+ /// then clipping them to their "proper" area. This rect is that "logical"
+ /// snapped area that may be clipped to the right size by the clip_rect.
+ pub area: LayoutRect,
+ /// Whether the rect is interpretted as vertical or horizontal
+ pub orientation: LineOrientation,
+ /// This could potentially be implied from area, but we currently prefer
+ /// that this is the responsibility of the layout engine. Value irrelevant
+ /// for non-wavy lines.
+ // FIXME: this was done before we could use tagged unions in enums, but now
+ // it should just be part of LineStyle::Wavy.
+ pub wavy_line_thickness: f32,
+ pub color: ColorF,
+ pub style: LineStyle,
+}
+
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize, Eq, Hash, PeekPoke)]
+pub enum LineOrientation {
+ Vertical,
+ Horizontal,
+}
+
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize, Eq, Hash, PeekPoke)]
+pub enum LineStyle {
+ Solid,
+ Dotted,
+ Dashed,
+ Wavy,
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct TextDisplayItem {
+ pub common: CommonItemProperties,
+ /// The area all the glyphs should be found in. Strictly speaking this isn't
+ /// necessarily needed, but layout engines should already "know" this, and we
+ /// use it cull and size things quickly before glyph layout is done. Currently
+ /// the glyphs *can* be outside these bounds, but that should imply they
+ /// can be cut off.
+ // FIXME: these are currently sometimes ignored to keep some old wrench tests
+ // working, but we should really just fix the tests!
+ pub bounds: LayoutRect,
+ pub font_key: font::FontInstanceKey,
+ pub color: ColorF,
+ pub glyph_options: Option<font::GlyphOptions>,
+} // IMPLICIT: glyphs: Vec<font::GlyphInstance>
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, MallocSizeOf, PartialEq, Serialize, PeekPoke)]
+pub struct NormalBorder {
+ pub left: BorderSide,
+ pub right: BorderSide,
+ pub top: BorderSide,
+ pub bottom: BorderSide,
+ pub radius: BorderRadius,
+ /// Whether to apply anti-aliasing on the border corners.
+ ///
+ /// Note that for this to be `false` and work, this requires the borders to
+ /// be solid, and no border-radius.
+ pub do_aa: bool,
+}
+
+impl NormalBorder {
+ fn can_disable_antialiasing(&self) -> bool {
+ fn is_valid(style: BorderStyle) -> bool {
+ style == BorderStyle::Solid || style == BorderStyle::None
+ }
+
+ self.radius.is_zero() &&
+ is_valid(self.top.style) &&
+ is_valid(self.left.style) &&
+ is_valid(self.bottom.style) &&
+ is_valid(self.right.style)
+ }
+
+ /// Normalizes a border so that we don't render disallowed stuff, like inset
+ /// borders that are less than two pixels wide.
+ #[inline]
+ pub fn normalize(&mut self, widths: &LayoutSideOffsets) {
+ debug_assert!(
+ self.do_aa || self.can_disable_antialiasing(),
+ "Unexpected disabled-antialiasing in a border, likely won't work or will be ignored"
+ );
+
+ #[inline]
+ fn renders_small_border_solid(style: BorderStyle) -> bool {
+ match style {
+ BorderStyle::Groove |
+ BorderStyle::Ridge => true,
+ _ => false,
+ }
+ }
+
+ let normalize_side = |side: &mut BorderSide, width: f32| {
+ if renders_small_border_solid(side.style) && width < 2. {
+ side.style = BorderStyle::Solid;
+ }
+ };
+
+ normalize_side(&mut self.left, widths.left);
+ normalize_side(&mut self.right, widths.right);
+ normalize_side(&mut self.top, widths.top);
+ normalize_side(&mut self.bottom, widths.bottom);
+ }
+}
+
+#[repr(u8)]
+#[derive(Debug, Copy, Clone, MallocSizeOf, PartialEq, Serialize, Deserialize, Eq, Hash, PeekPoke)]
+pub enum RepeatMode {
+ Stretch,
+ Repeat,
+ Round,
+ Space,
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub enum NinePatchBorderSource {
+ Image(ImageKey, ImageRendering),
+ Gradient(Gradient),
+ RadialGradient(RadialGradient),
+ ConicGradient(ConicGradient),
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct NinePatchBorder {
+ /// Describes what to use as the 9-patch source image. If this is an image,
+ /// it will be stretched to fill the size given by width x height.
+ pub source: NinePatchBorderSource,
+
+ /// The width of the 9-part image.
+ pub width: i32,
+
+ /// The height of the 9-part image.
+ pub height: i32,
+
+ /// Distances from each edge where the image should be sliced up. These
+ /// values are in 9-part-image space (the same space as width and height),
+ /// and the resulting image parts will be used to fill the corresponding
+ /// parts of the border as given by the border widths. This can lead to
+ /// stretching.
+ /// Slices can be overlapping. In that case, the same pixels from the
+ /// 9-part image will show up in multiple parts of the resulting border.
+ pub slice: DeviceIntSideOffsets,
+
+ /// Controls whether the center of the 9 patch image is rendered or
+ /// ignored. The center is never rendered if the slices are overlapping.
+ pub fill: bool,
+
+ /// Determines what happens if the horizontal side parts of the 9-part
+ /// image have a different size than the horizontal parts of the border.
+ pub repeat_horizontal: RepeatMode,
+
+ /// Determines what happens if the vertical side parts of the 9-part
+ /// image have a different size than the vertical parts of the border.
+ pub repeat_vertical: RepeatMode,
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub enum BorderDetails {
+ Normal(NormalBorder),
+ NinePatch(NinePatchBorder),
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct BorderDisplayItem {
+ pub common: CommonItemProperties,
+ pub bounds: LayoutRect,
+ pub widths: LayoutSideOffsets,
+ pub details: BorderDetails,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub enum BorderRadiusKind {
+ Uniform,
+ NonUniform,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize, PeekPoke)]
+pub struct BorderRadius {
+ pub top_left: LayoutSize,
+ pub top_right: LayoutSize,
+ pub bottom_left: LayoutSize,
+ pub bottom_right: LayoutSize,
+}
+
+impl Default for BorderRadius {
+ fn default() -> Self {
+ BorderRadius {
+ top_left: LayoutSize::zero(),
+ top_right: LayoutSize::zero(),
+ bottom_left: LayoutSize::zero(),
+ bottom_right: LayoutSize::zero(),
+ }
+ }
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, Deserialize, MallocSizeOf, PartialEq, Serialize, PeekPoke)]
+pub struct BorderSide {
+ pub color: ColorF,
+ pub style: BorderStyle,
+}
+
+#[repr(u32)]
+#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize, Hash, Eq, PeekPoke)]
+pub enum BorderStyle {
+ None = 0,
+ Solid = 1,
+ Double = 2,
+ Dotted = 3,
+ Dashed = 4,
+ Hidden = 5,
+ Groove = 6,
+ Ridge = 7,
+ Inset = 8,
+ Outset = 9,
+}
+
+impl BorderStyle {
+ pub fn is_hidden(self) -> bool {
+ self == BorderStyle::Hidden || self == BorderStyle::None
+ }
+}
+
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize, PeekPoke)]
+pub enum BoxShadowClipMode {
+ Outset = 0,
+ Inset = 1,
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct BoxShadowDisplayItem {
+ pub common: CommonItemProperties,
+ pub box_bounds: LayoutRect,
+ pub offset: LayoutVector2D,
+ pub color: ColorF,
+ pub blur_radius: f32,
+ pub spread_radius: f32,
+ pub border_radius: BorderRadius,
+ pub clip_mode: BoxShadowClipMode,
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct PushShadowDisplayItem {
+ pub space_and_clip: SpaceAndClipInfo,
+ pub shadow: Shadow,
+ pub should_inflate: bool,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct Shadow {
+ pub offset: LayoutVector2D,
+ pub color: ColorF,
+ pub blur_radius: f32,
+}
+
+#[repr(u8)]
+#[derive(Debug, Copy, Clone, Hash, Eq, MallocSizeOf, PartialEq, Serialize, Deserialize, Ord, PartialOrd, PeekPoke)]
+pub enum ExtendMode {
+ Clamp,
+ Repeat,
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct Gradient {
+ pub start_point: LayoutPoint,
+ pub end_point: LayoutPoint,
+ pub extend_mode: ExtendMode,
+} // IMPLICIT: stops: Vec<GradientStop>
+
+impl Gradient {
+ pub fn is_valid(&self) -> bool {
+ self.start_point.x.is_finite() &&
+ self.start_point.y.is_finite() &&
+ self.end_point.x.is_finite() &&
+ self.end_point.y.is_finite()
+ }
+}
+
+/// The area
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct GradientDisplayItem {
+ /// NOTE: common.clip_rect is the area the gradient covers
+ pub common: CommonItemProperties,
+ /// The area to tile the gradient over (first tile starts at origin of this rect)
+ // FIXME: this should ideally just be `tile_origin` here, with the clip_rect
+ // defining the bounds of the item. Needs non-trivial backend changes.
+ pub bounds: LayoutRect,
+ /// How big a tile of the of the gradient should be (common case: bounds.size)
+ pub tile_size: LayoutSize,
+ /// The space between tiles of the gradient (common case: 0)
+ pub tile_spacing: LayoutSize,
+ pub gradient: Gradient,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, Deserialize, MallocSizeOf, PartialEq, Serialize, PeekPoke)]
+pub struct GradientStop {
+ pub offset: f32,
+ pub color: ColorF,
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct RadialGradient {
+ pub center: LayoutPoint,
+ pub radius: LayoutSize,
+ pub start_offset: f32,
+ pub end_offset: f32,
+ pub extend_mode: ExtendMode,
+} // IMPLICIT stops: Vec<GradientStop>
+
+impl RadialGradient {
+ pub fn is_valid(&self) -> bool {
+ self.center.x.is_finite() &&
+ self.center.y.is_finite() &&
+ self.start_offset.is_finite() &&
+ self.end_offset.is_finite()
+ }
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct ConicGradient {
+ pub center: LayoutPoint,
+ pub angle: f32,
+ pub start_offset: f32,
+ pub end_offset: f32,
+ pub extend_mode: ExtendMode,
+} // IMPLICIT stops: Vec<GradientStop>
+
+impl ConicGradient {
+ pub fn is_valid(&self) -> bool {
+ self.center.x.is_finite() &&
+ self.center.y.is_finite() &&
+ self.angle.is_finite() &&
+ self.start_offset.is_finite() &&
+ self.end_offset.is_finite()
+ }
+}
+
+/// Just an abstraction for bundling up a bunch of clips into a "super clip".
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct ClipChainItem {
+ pub id: ClipChainId,
+ pub parent: Option<ClipChainId>,
+} // IMPLICIT clip_ids: Vec<ClipId>
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct RadialGradientDisplayItem {
+ pub common: CommonItemProperties,
+ /// The area to tile the gradient over (first tile starts at origin of this rect)
+ // FIXME: this should ideally just be `tile_origin` here, with the clip_rect
+ // defining the bounds of the item. Needs non-trivial backend changes.
+ pub bounds: LayoutRect,
+ pub gradient: RadialGradient,
+ pub tile_size: LayoutSize,
+ pub tile_spacing: LayoutSize,
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct ConicGradientDisplayItem {
+ pub common: CommonItemProperties,
+ /// The area to tile the gradient over (first tile starts at origin of this rect)
+ // FIXME: this should ideally just be `tile_origin` here, with the clip_rect
+ // defining the bounds of the item. Needs non-trivial backend changes.
+ pub bounds: LayoutRect,
+ pub gradient: ConicGradient,
+ pub tile_size: LayoutSize,
+ pub tile_spacing: LayoutSize,
+}
+
+/// Renders a filtered region of its backdrop
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct BackdropFilterDisplayItem {
+ pub common: CommonItemProperties,
+}
+// IMPLICIT: filters: Vec<FilterOp>, filter_datas: Vec<FilterData>, filter_primitives: Vec<FilterPrimitive>
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct ReferenceFrameDisplayListItem {
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct ReferenceFrameDescriptor {
+ pub origin: LayoutPoint,
+ pub parent_spatial_id: SpatialId,
+ pub reference_frame: ReferenceFrame,
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub enum ReferenceFrameKind {
+ /// A normal transform matrix, may contain perspective (the CSS transform property)
+ Transform {
+ /// Optionally marks the transform as only ever having a simple 2D scale or translation,
+ /// allowing for optimizations.
+ is_2d_scale_translation: bool,
+ /// Marks that the transform should be snapped. Used for transforms which animate in
+ /// response to scrolling, eg for zooming or dynamic toolbar fixed-positioning.
+ should_snap: bool,
+ /// Marks the transform being a part of the CSS stacking context that also has
+ /// a perspective. In this case, backface visibility takes this perspective into
+ /// account.
+ paired_with_perspective: bool,
+ },
+ /// A perspective transform, that optionally scrolls relative to a specific scroll node
+ Perspective {
+ scrolling_relative_to: Option<ExternalScrollId>,
+ }
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub enum Rotation {
+ Degree0,
+ Degree90,
+ Degree180,
+ Degree270,
+}
+
+impl Rotation {
+ pub fn to_matrix(
+ &self,
+ size: LayoutSize,
+ ) -> LayoutTransform {
+ let (shift_center_to_origin, angle) = match self {
+ Rotation::Degree0 => {
+ (LayoutTransform::translation(-size.width / 2., -size.height / 2., 0.), Angle::degrees(0.))
+ },
+ Rotation::Degree90 => {
+ (LayoutTransform::translation(-size.height / 2., -size.width / 2., 0.), Angle::degrees(90.))
+ },
+ Rotation::Degree180 => {
+ (LayoutTransform::translation(-size.width / 2., -size.height / 2., 0.), Angle::degrees(180.))
+ },
+ Rotation::Degree270 => {
+ (LayoutTransform::translation(-size.height / 2., -size.width / 2., 0.), Angle::degrees(270.))
+ },
+ };
+ let shift_origin_to_center = LayoutTransform::translation(size.width / 2., size.height / 2., 0.);
+
+ shift_center_to_origin
+ .then(&LayoutTransform::rotation(0., 0., 1.0, angle))
+ .then(&shift_origin_to_center)
+ }
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub enum ReferenceTransformBinding {
+ /// Standard reference frame which contains a precomputed transform.
+ Static {
+ binding: PropertyBinding<LayoutTransform>,
+ },
+ /// Computed reference frame which dynamically calculates the transform
+ /// based on the given parameters. The reference is the content size of
+ /// the parent iframe, which is affected by snapping.
+ ///
+ /// This is used when a transform depends on the layout size of an
+ /// element, otherwise the difference between the unsnapped size
+ /// used in the transform, and the snapped size calculated during scene
+ /// building can cause seaming.
+ Computed {
+ scale_from: Option<LayoutSize>,
+ vertical_flip: bool,
+ rotation: Rotation,
+ },
+}
+
+impl Default for ReferenceTransformBinding {
+ fn default() -> Self {
+ ReferenceTransformBinding::Static {
+ binding: Default::default(),
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct ReferenceFrame {
+ pub kind: ReferenceFrameKind,
+ pub transform_style: TransformStyle,
+ /// The transform matrix, either the perspective matrix or the transform
+ /// matrix.
+ pub transform: ReferenceTransformBinding,
+ pub id: SpatialId,
+ /// A unique (per-pipeline) key for this spatial that is stable across display lists.
+ pub key: SpatialTreeItemKey,
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct PushStackingContextDisplayItem {
+ pub origin: LayoutPoint,
+ pub spatial_id: SpatialId,
+ pub prim_flags: PrimitiveFlags,
+ pub stacking_context: StackingContext,
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct StackingContext {
+ pub transform_style: TransformStyle,
+ pub mix_blend_mode: MixBlendMode,
+ pub clip_chain_id: Option<ClipChainId>,
+ pub raster_space: RasterSpace,
+ pub flags: StackingContextFlags,
+}
+// IMPLICIT: filters: Vec<FilterOp>, filter_datas: Vec<FilterData>, filter_primitives: Vec<FilterPrimitive>
+
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, PeekPoke)]
+pub enum TransformStyle {
+ Flat = 0,
+ Preserve3D = 1,
+}
+
+/// Configure whether the contents of a stacking context
+/// should be rasterized in local space or screen space.
+/// Local space rasterized pictures are typically used
+/// when we want to cache the output, and performance is
+/// important. Note that this is a performance hint only,
+/// which WR may choose to ignore.
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq, MallocSizeOf, Serialize, PeekPoke)]
+#[repr(u8)]
+pub enum RasterSpace {
+ // Rasterize in local-space, applying supplied scale to primitives.
+ // Best performance, but lower quality.
+ Local(f32),
+
+ // Rasterize the picture in screen-space, including rotation / skew etc in
+ // the rasterized element. Best quality, but slower performance. Note that
+ // any stacking context with a perspective transform will be rasterized
+ // in local-space, even if this is set.
+ Screen,
+}
+
+impl RasterSpace {
+ pub fn local_scale(self) -> Option<f32> {
+ match self {
+ RasterSpace::Local(scale) => Some(scale),
+ RasterSpace::Screen => None,
+ }
+ }
+}
+
+impl Eq for RasterSpace {}
+
+impl Hash for RasterSpace {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ match self {
+ RasterSpace::Screen => {
+ 0.hash(state);
+ }
+ RasterSpace::Local(scale) => {
+ // Note: this is inconsistent with the Eq impl for -0.0 (don't care).
+ 1.hash(state);
+ scale.to_bits().hash(state);
+ }
+ }
+ }
+}
+
+bitflags! {
+ #[repr(C)]
+ #[derive(Deserialize, MallocSizeOf, Serialize, PeekPoke)]
+ pub struct StackingContextFlags: u8 {
+ /// If true, this stacking context is a blend container than contains
+ /// mix-blend-mode children (and should thus be isolated).
+ const IS_BLEND_CONTAINER = 1 << 0;
+ /// If true, this stacking context is a wrapper around a backdrop-filter (e.g. for
+ /// a clip-mask). This is needed to allow the correct selection of a backdrop root
+ /// since a clip-mask stacking context creates a parent surface.
+ const WRAPS_BACKDROP_FILTER = 1 << 1;
+ }
+}
+
+impl Default for StackingContextFlags {
+ fn default() -> Self {
+ StackingContextFlags::empty()
+ }
+}
+
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize, PeekPoke)]
+pub enum MixBlendMode {
+ Normal = 0,
+ Multiply = 1,
+ Screen = 2,
+ Overlay = 3,
+ Darken = 4,
+ Lighten = 5,
+ ColorDodge = 6,
+ ColorBurn = 7,
+ HardLight = 8,
+ SoftLight = 9,
+ Difference = 10,
+ Exclusion = 11,
+ Hue = 12,
+ Saturation = 13,
+ Color = 14,
+ Luminosity = 15,
+ PlusLighter = 16,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize, PeekPoke)]
+pub enum ColorSpace {
+ Srgb,
+ LinearRgb,
+}
+
+/// Available composite operoations for the composite filter primitive
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize, PeekPoke)]
+pub enum CompositeOperator {
+ Over,
+ In,
+ Atop,
+ Out,
+ Xor,
+ Lighter,
+ Arithmetic([f32; 4]),
+}
+
+impl CompositeOperator {
+ // This must stay in sync with the composite operator defines in cs_svg_filter.glsl
+ pub fn as_int(&self) -> u32 {
+ match self {
+ CompositeOperator::Over => 0,
+ CompositeOperator::In => 1,
+ CompositeOperator::Out => 2,
+ CompositeOperator::Atop => 3,
+ CompositeOperator::Xor => 4,
+ CompositeOperator::Lighter => 5,
+ CompositeOperator::Arithmetic(..) => 6,
+ }
+ }
+}
+
+/// An input to a SVG filter primitive.
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize, PeekPoke)]
+pub enum FilterPrimitiveInput {
+ /// The input is the original graphic that the filter is being applied to.
+ Original,
+ /// The input is the output of the previous filter primitive in the filter primitive chain.
+ Previous,
+ /// The input is the output of the filter primitive at the given index in the filter primitive chain.
+ OutputOfPrimitiveIndex(usize),
+}
+
+impl FilterPrimitiveInput {
+ /// Gets the index of the input.
+ /// Returns `None` if the source graphic is the input.
+ pub fn to_index(self, cur_index: usize) -> Option<usize> {
+ match self {
+ FilterPrimitiveInput::Previous if cur_index > 0 => Some(cur_index - 1),
+ FilterPrimitiveInput::OutputOfPrimitiveIndex(index) => Some(index),
+ _ => None,
+ }
+ }
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct BlendPrimitive {
+ pub input1: FilterPrimitiveInput,
+ pub input2: FilterPrimitiveInput,
+ pub mode: MixBlendMode,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct FloodPrimitive {
+ pub color: ColorF,
+}
+
+impl FloodPrimitive {
+ pub fn sanitize(&mut self) {
+ self.color.r = self.color.r.min(1.0).max(0.0);
+ self.color.g = self.color.g.min(1.0).max(0.0);
+ self.color.b = self.color.b.min(1.0).max(0.0);
+ self.color.a = self.color.a.min(1.0).max(0.0);
+ }
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct BlurPrimitive {
+ pub input: FilterPrimitiveInput,
+ pub width: f32,
+ pub height: f32,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct OpacityPrimitive {
+ pub input: FilterPrimitiveInput,
+ pub opacity: f32,
+}
+
+impl OpacityPrimitive {
+ pub fn sanitize(&mut self) {
+ self.opacity = self.opacity.min(1.0).max(0.0);
+ }
+}
+
+/// cbindgen:derive-eq=false
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct ColorMatrixPrimitive {
+ pub input: FilterPrimitiveInput,
+ pub matrix: [f32; 20],
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct DropShadowPrimitive {
+ pub input: FilterPrimitiveInput,
+ pub shadow: Shadow,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct ComponentTransferPrimitive {
+ pub input: FilterPrimitiveInput,
+ // Component transfer data is stored in FilterData.
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct IdentityPrimitive {
+ pub input: FilterPrimitiveInput,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct OffsetPrimitive {
+ pub input: FilterPrimitiveInput,
+ pub offset: LayoutVector2D,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct CompositePrimitive {
+ pub input1: FilterPrimitiveInput,
+ pub input2: FilterPrimitiveInput,
+ pub operator: CompositeOperator,
+}
+
+/// See: https://github.com/eqrion/cbindgen/issues/9
+/// cbindgen:derive-eq=false
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub enum FilterPrimitiveKind {
+ Identity(IdentityPrimitive),
+ Blend(BlendPrimitive),
+ Flood(FloodPrimitive),
+ Blur(BlurPrimitive),
+ // TODO: Support animated opacity?
+ Opacity(OpacityPrimitive),
+ /// cbindgen:derive-eq=false
+ ColorMatrix(ColorMatrixPrimitive),
+ DropShadow(DropShadowPrimitive),
+ ComponentTransfer(ComponentTransferPrimitive),
+ Offset(OffsetPrimitive),
+ Composite(CompositePrimitive),
+}
+
+impl Default for FilterPrimitiveKind {
+ fn default() -> Self {
+ FilterPrimitiveKind::Identity(IdentityPrimitive::default())
+ }
+}
+
+impl FilterPrimitiveKind {
+ pub fn sanitize(&mut self) {
+ match self {
+ FilterPrimitiveKind::Flood(flood) => flood.sanitize(),
+ FilterPrimitiveKind::Opacity(opacity) => opacity.sanitize(),
+
+ // No sanitization needed.
+ FilterPrimitiveKind::Identity(..) |
+ FilterPrimitiveKind::Blend(..) |
+ FilterPrimitiveKind::ColorMatrix(..) |
+ FilterPrimitiveKind::Offset(..) |
+ FilterPrimitiveKind::Composite(..) |
+ FilterPrimitiveKind::Blur(..) |
+ FilterPrimitiveKind::DropShadow(..) |
+ // Component transfer's filter data is sanitized separately.
+ FilterPrimitiveKind::ComponentTransfer(..) => {}
+ }
+ }
+}
+
+/// SVG Filter Primitive.
+/// See: https://github.com/eqrion/cbindgen/issues/9
+/// cbindgen:derive-eq=false
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct FilterPrimitive {
+ pub kind: FilterPrimitiveKind,
+ pub color_space: ColorSpace,
+}
+
+impl FilterPrimitive {
+ pub fn sanitize(&mut self) {
+ self.kind.sanitize();
+ }
+}
+
+/// CSS filter.
+#[repr(C)]
+#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize, PeekPoke)]
+pub enum FilterOp {
+ /// Filter that does no transformation of the colors, needed for
+ /// debug purposes only.
+ Identity,
+ Blur(f32, f32),
+ Brightness(f32),
+ Contrast(f32),
+ Grayscale(f32),
+ HueRotate(f32),
+ Invert(f32),
+ Opacity(PropertyBinding<f32>, f32),
+ Saturate(f32),
+ Sepia(f32),
+ DropShadow(Shadow),
+ ColorMatrix([f32; 20]),
+ SrgbToLinear,
+ LinearToSrgb,
+ ComponentTransfer,
+ Flood(ColorF),
+}
+
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize, PeekPoke)]
+pub enum ComponentTransferFuncType {
+ Identity = 0,
+ Table = 1,
+ Discrete = 2,
+ Linear = 3,
+ Gamma = 4,
+}
+
+#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
+pub struct FilterData {
+ pub func_r_type: ComponentTransferFuncType,
+ pub r_values: Vec<f32>,
+ pub func_g_type: ComponentTransferFuncType,
+ pub g_values: Vec<f32>,
+ pub func_b_type: ComponentTransferFuncType,
+ pub b_values: Vec<f32>,
+ pub func_a_type: ComponentTransferFuncType,
+ pub a_values: Vec<f32>,
+}
+
+fn sanitize_func_type(
+ func_type: ComponentTransferFuncType,
+ values: &[f32],
+) -> ComponentTransferFuncType {
+ if values.is_empty() {
+ return ComponentTransferFuncType::Identity;
+ }
+ if values.len() < 2 && func_type == ComponentTransferFuncType::Linear {
+ return ComponentTransferFuncType::Identity;
+ }
+ if values.len() < 3 && func_type == ComponentTransferFuncType::Gamma {
+ return ComponentTransferFuncType::Identity;
+ }
+ func_type
+}
+
+fn sanitize_values(
+ func_type: ComponentTransferFuncType,
+ values: &[f32],
+) -> bool {
+ if values.len() < 2 && func_type == ComponentTransferFuncType::Linear {
+ return false;
+ }
+ if values.len() < 3 && func_type == ComponentTransferFuncType::Gamma {
+ return false;
+ }
+ true
+}
+
+impl FilterData {
+ /// Ensure that the number of values matches up with the function type.
+ pub fn sanitize(&self) -> FilterData {
+ FilterData {
+ func_r_type: sanitize_func_type(self.func_r_type, &self.r_values),
+ r_values:
+ if sanitize_values(self.func_r_type, &self.r_values) {
+ self.r_values.clone()
+ } else {
+ Vec::new()
+ },
+ func_g_type: sanitize_func_type(self.func_g_type, &self.g_values),
+ g_values:
+ if sanitize_values(self.func_g_type, &self.g_values) {
+ self.g_values.clone()
+ } else {
+ Vec::new()
+ },
+
+ func_b_type: sanitize_func_type(self.func_b_type, &self.b_values),
+ b_values:
+ if sanitize_values(self.func_b_type, &self.b_values) {
+ self.b_values.clone()
+ } else {
+ Vec::new()
+ },
+
+ func_a_type: sanitize_func_type(self.func_a_type, &self.a_values),
+ a_values:
+ if sanitize_values(self.func_a_type, &self.a_values) {
+ self.a_values.clone()
+ } else {
+ Vec::new()
+ },
+
+ }
+ }
+
+ pub fn is_identity(&self) -> bool {
+ self.func_r_type == ComponentTransferFuncType::Identity &&
+ self.func_g_type == ComponentTransferFuncType::Identity &&
+ self.func_b_type == ComponentTransferFuncType::Identity &&
+ self.func_a_type == ComponentTransferFuncType::Identity
+ }
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct IframeDisplayItem {
+ pub bounds: LayoutRect,
+ pub clip_rect: LayoutRect,
+ pub space_and_clip: SpaceAndClipInfo,
+ pub pipeline_id: PipelineId,
+ pub ignore_missing_pipeline: bool,
+}
+
+/// This describes an image that fills the specified area. It stretches or shrinks
+/// the image as necessary. While RepeatingImageDisplayItem could otherwise provide
+/// a superset of the functionality, it has been problematic inferring the desired
+/// repetition properties when snapping changes the size of the primitive.
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct ImageDisplayItem {
+ pub common: CommonItemProperties,
+ /// The area to tile the image over (first tile starts at origin of this rect)
+ // FIXME: this should ideally just be `tile_origin` here, with the clip_rect
+ // defining the bounds of the item. Needs non-trivial backend changes.
+ pub bounds: LayoutRect,
+ pub image_key: ImageKey,
+ pub image_rendering: ImageRendering,
+ pub alpha_type: AlphaType,
+ /// A hack used by gecko to color a simple bitmap font used for tofu glyphs
+ pub color: ColorF,
+}
+
+/// This describes a background-image and its tiling. It repeats in a grid to fill
+/// the specified area.
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct RepeatingImageDisplayItem {
+ pub common: CommonItemProperties,
+ /// The area to tile the image over (first tile starts at origin of this rect)
+ // FIXME: this should ideally just be `tile_origin` here, with the clip_rect
+ // defining the bounds of the item. Needs non-trivial backend changes.
+ pub bounds: LayoutRect,
+ /// How large to make a single tile of the image (common case: bounds.size)
+ pub stretch_size: LayoutSize,
+ /// The space between tiles (common case: 0)
+ pub tile_spacing: LayoutSize,
+ pub image_key: ImageKey,
+ pub image_rendering: ImageRendering,
+ pub alpha_type: AlphaType,
+ /// A hack used by gecko to color a simple bitmap font used for tofu glyphs
+ pub color: ColorF,
+}
+
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize, PeekPoke)]
+pub enum ImageRendering {
+ Auto = 0,
+ CrispEdges = 1,
+ Pixelated = 2,
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize, PeekPoke)]
+pub enum AlphaType {
+ Alpha = 0,
+ PremultipliedAlpha = 1,
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct YuvImageDisplayItem {
+ pub common: CommonItemProperties,
+ pub bounds: LayoutRect,
+ pub yuv_data: YuvData,
+ pub color_depth: ColorDepth,
+ pub color_space: YuvColorSpace,
+ pub color_range: ColorRange,
+ pub image_rendering: ImageRendering,
+}
+
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize, PeekPoke)]
+pub enum YuvColorSpace {
+ Rec601 = 0,
+ Rec709 = 1,
+ Rec2020 = 2,
+ Identity = 3, // aka GBR as per ISO/IEC 23091-2:2019
+}
+
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize, PeekPoke)]
+pub enum ColorRange {
+ Limited = 0,
+ Full = 1,
+}
+
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize, PeekPoke)]
+pub enum YuvRangedColorSpace {
+ Rec601Narrow = 0,
+ Rec601Full = 1,
+ Rec709Narrow = 2,
+ Rec709Full = 3,
+ Rec2020Narrow = 4,
+ Rec2020Full = 5,
+ GbrIdentity = 6,
+}
+
+impl YuvColorSpace {
+ pub fn with_range(self, range: ColorRange) -> YuvRangedColorSpace {
+ match self {
+ YuvColorSpace::Identity => YuvRangedColorSpace::GbrIdentity,
+ YuvColorSpace::Rec601 => {
+ match range {
+ ColorRange::Limited => YuvRangedColorSpace::Rec601Narrow,
+ ColorRange::Full => YuvRangedColorSpace::Rec601Full,
+ }
+ }
+ YuvColorSpace::Rec709 => {
+ match range {
+ ColorRange::Limited => YuvRangedColorSpace::Rec709Narrow,
+ ColorRange::Full => YuvRangedColorSpace::Rec709Full,
+ }
+ }
+ YuvColorSpace::Rec2020 => {
+ match range {
+ ColorRange::Limited => YuvRangedColorSpace::Rec2020Narrow,
+ ColorRange::Full => YuvRangedColorSpace::Rec2020Full,
+ }
+ }
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, PeekPoke)]
+pub enum YuvData {
+ NV12(ImageKey, ImageKey), // (Y channel, CbCr interleaved channel)
+ P010(ImageKey, ImageKey), // (Y channel, CbCr interleaved channel)
+ PlanarYCbCr(ImageKey, ImageKey, ImageKey), // (Y channel, Cb channel, Cr Channel)
+ InterleavedYCbCr(ImageKey), // (YCbCr interleaved channel)
+}
+
+impl YuvData {
+ pub fn get_format(&self) -> YuvFormat {
+ match *self {
+ YuvData::NV12(..) => YuvFormat::NV12,
+ YuvData::P010(..) => YuvFormat::P010,
+ YuvData::PlanarYCbCr(..) => YuvFormat::PlanarYCbCr,
+ YuvData::InterleavedYCbCr(..) => YuvFormat::InterleavedYCbCr,
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize, PeekPoke)]
+pub enum YuvFormat {
+ NV12 = 0,
+ P010 = 1,
+ PlanarYCbCr = 2,
+ InterleavedYCbCr = 3,
+}
+
+impl YuvFormat {
+ pub fn get_plane_num(self) -> usize {
+ match self {
+ YuvFormat::NV12 | YuvFormat::P010 => 2,
+ YuvFormat::PlanarYCbCr => 3,
+ YuvFormat::InterleavedYCbCr => 1,
+ }
+ }
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct ImageMask {
+ pub image: ImageKey,
+ pub rect: LayoutRect,
+}
+
+impl ImageMask {
+ /// Get a local clipping rect contributed by this mask.
+ pub fn get_local_clip_rect(&self) -> Option<LayoutRect> {
+ Some(self.rect)
+ }
+}
+
+#[repr(C)]
+#[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq, Serialize, Deserialize, Eq, Hash, PeekPoke)]
+pub enum ClipMode {
+ Clip, // Pixels inside the region are visible.
+ ClipOut, // Pixels outside the region are visible.
+}
+
+impl Not for ClipMode {
+ type Output = ClipMode;
+
+ fn not(self) -> ClipMode {
+ match self {
+ ClipMode::Clip => ClipMode::ClipOut,
+ ClipMode::ClipOut => ClipMode::Clip,
+ }
+ }
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub struct ComplexClipRegion {
+ /// The boundaries of the rectangle.
+ pub rect: LayoutRect,
+ /// Border radii of this rectangle.
+ pub radii: BorderRadius,
+ /// Whether we are clipping inside or outside
+ /// the region.
+ pub mode: ClipMode,
+}
+
+impl BorderRadius {
+ pub fn zero() -> BorderRadius {
+ BorderRadius {
+ top_left: LayoutSize::new(0.0, 0.0),
+ top_right: LayoutSize::new(0.0, 0.0),
+ bottom_left: LayoutSize::new(0.0, 0.0),
+ bottom_right: LayoutSize::new(0.0, 0.0),
+ }
+ }
+
+ pub fn uniform(radius: f32) -> BorderRadius {
+ BorderRadius {
+ top_left: LayoutSize::new(radius, radius),
+ top_right: LayoutSize::new(radius, radius),
+ bottom_left: LayoutSize::new(radius, radius),
+ bottom_right: LayoutSize::new(radius, radius),
+ }
+ }
+
+ pub fn uniform_size(radius: LayoutSize) -> BorderRadius {
+ BorderRadius {
+ top_left: radius,
+ top_right: radius,
+ bottom_left: radius,
+ bottom_right: radius,
+ }
+ }
+
+ pub fn is_uniform(&self) -> Option<f32> {
+ match self.is_uniform_size() {
+ Some(radius) if radius.width == radius.height => Some(radius.width),
+ _ => None,
+ }
+ }
+
+ pub fn is_uniform_size(&self) -> Option<LayoutSize> {
+ let uniform_radius = self.top_left;
+ if self.top_right == uniform_radius && self.bottom_left == uniform_radius &&
+ self.bottom_right == uniform_radius
+ {
+ Some(uniform_radius)
+ } else {
+ None
+ }
+ }
+
+ /// Return whether, in each corner, the radius in *either* direction is zero.
+ /// This means that none of the corners are rounded.
+ pub fn is_zero(&self) -> bool {
+ let corner_is_zero = |corner: &LayoutSize| corner.width == 0.0 || corner.height == 0.0;
+ corner_is_zero(&self.top_left) &&
+ corner_is_zero(&self.top_right) &&
+ corner_is_zero(&self.bottom_right) &&
+ corner_is_zero(&self.bottom_left)
+ }
+}
+
+impl ComplexClipRegion {
+ /// Create a new complex clip region.
+ pub fn new(
+ rect: LayoutRect,
+ radii: BorderRadius,
+ mode: ClipMode,
+ ) -> Self {
+ ComplexClipRegion { rect, radii, mode }
+ }
+}
+
+impl ComplexClipRegion {
+ /// Get a local clipping rect contributed by this clip region.
+ pub fn get_local_clip_rect(&self) -> Option<LayoutRect> {
+ match self.mode {
+ ClipMode::Clip => {
+ Some(self.rect)
+ }
+ ClipMode::ClipOut => {
+ None
+ }
+ }
+ }
+}
+
+pub const POLYGON_CLIP_VERTEX_MAX: usize = 32;
+
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize, Eq, Hash, PeekPoke)]
+pub enum FillRule {
+ Nonzero = 0x1, // Behaves as the SVG fill-rule definition for nonzero.
+ Evenodd = 0x2, // Behaves as the SVG fill-rule definition for evenodd.
+}
+
+impl From<u8> for FillRule {
+ fn from(fill_rule: u8) -> Self {
+ match fill_rule {
+ 0x1 => FillRule::Nonzero,
+ 0x2 => FillRule::Evenodd,
+ _ => panic!("Unexpected FillRule value."),
+ }
+ }
+}
+
+impl From<FillRule> for u8 {
+ fn from(fill_rule: FillRule) -> Self {
+ match fill_rule {
+ FillRule::Nonzero => 0x1,
+ FillRule::Evenodd => 0x2,
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize, PeekPoke)]
+pub struct ClipChainId(pub u64, pub PipelineId);
+
+impl ClipChainId {
+ pub const INVALID: Self = ClipChainId(!0, PipelineId::INVALID);
+}
+
+/// A reference to a clipping node defining how an item is clipped.
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, PeekPoke)]
+pub struct ClipId(pub usize, pub PipelineId);
+
+impl Default for ClipId {
+ fn default() -> Self {
+ ClipId::invalid()
+ }
+}
+
+const ROOT_CLIP_ID: usize = 0;
+
+impl ClipId {
+ /// Return the root clip ID - effectively doing no clipping.
+ pub fn root(pipeline_id: PipelineId) -> Self {
+ ClipId(ROOT_CLIP_ID, pipeline_id)
+ }
+
+ /// Return an invalid clip ID - needed in places where we carry
+ /// one but need to not attempt to use it.
+ pub fn invalid() -> Self {
+ ClipId(!0, PipelineId::dummy())
+ }
+
+ pub fn pipeline_id(&self) -> PipelineId {
+ match *self {
+ ClipId(_, pipeline_id) => pipeline_id,
+ }
+ }
+
+ pub fn is_root(&self) -> bool {
+ match *self {
+ ClipId(id, _) => id == ROOT_CLIP_ID,
+ }
+ }
+
+ pub fn is_valid(&self) -> bool {
+ match *self {
+ ClipId(id, _) => id != !0,
+ }
+ }
+}
+
+/// A reference to a spatial node defining item positioning.
+#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize, PeekPoke)]
+pub struct SpatialId(pub usize, PipelineId);
+
+const ROOT_REFERENCE_FRAME_SPATIAL_ID: usize = 0;
+const ROOT_SCROLL_NODE_SPATIAL_ID: usize = 1;
+
+impl SpatialId {
+ pub fn new(spatial_node_index: usize, pipeline_id: PipelineId) -> Self {
+ SpatialId(spatial_node_index, pipeline_id)
+ }
+
+ pub fn root_reference_frame(pipeline_id: PipelineId) -> Self {
+ SpatialId(ROOT_REFERENCE_FRAME_SPATIAL_ID, pipeline_id)
+ }
+
+ pub fn root_scroll_node(pipeline_id: PipelineId) -> Self {
+ SpatialId(ROOT_SCROLL_NODE_SPATIAL_ID, pipeline_id)
+ }
+
+ pub fn pipeline_id(&self) -> PipelineId {
+ self.1
+ }
+}
+
+/// An external identifier that uniquely identifies a scroll frame independent of its ClipId, which
+/// may change from frame to frame. This should be unique within a pipeline. WebRender makes no
+/// attempt to ensure uniqueness. The zero value is reserved for use by the root scroll node of
+/// every pipeline, which always has an external id.
+///
+/// When setting display lists with the `preserve_frame_state` this id is used to preserve scroll
+/// offsets between different sets of SpatialNodes which are ScrollFrames.
+#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize, PeekPoke)]
+#[repr(C)]
+pub struct ExternalScrollId(pub u64, pub PipelineId);
+
+impl ExternalScrollId {
+ pub fn pipeline_id(&self) -> PipelineId {
+ self.1
+ }
+
+ pub fn is_root(&self) -> bool {
+ self.0 == 0
+ }
+}
+
+impl DisplayItem {
+ pub fn debug_name(&self) -> &'static str {
+ match *self {
+ DisplayItem::Border(..) => "border",
+ DisplayItem::BoxShadow(..) => "box_shadow",
+ DisplayItem::ClearRectangle(..) => "clear_rectangle",
+ DisplayItem::HitTest(..) => "hit_test",
+ DisplayItem::RectClip(..) => "rect_clip",
+ DisplayItem::RoundedRectClip(..) => "rounded_rect_clip",
+ DisplayItem::ImageMaskClip(..) => "image_mask_clip",
+ DisplayItem::ClipChain(..) => "clip_chain",
+ DisplayItem::ConicGradient(..) => "conic_gradient",
+ DisplayItem::Gradient(..) => "gradient",
+ DisplayItem::Iframe(..) => "iframe",
+ DisplayItem::Image(..) => "image",
+ DisplayItem::RepeatingImage(..) => "repeating_image",
+ DisplayItem::Line(..) => "line",
+ DisplayItem::PopAllShadows => "pop_all_shadows",
+ DisplayItem::PopReferenceFrame => "pop_reference_frame",
+ DisplayItem::PopStackingContext => "pop_stacking_context",
+ DisplayItem::PushShadow(..) => "push_shadow",
+ DisplayItem::PushReferenceFrame(..) => "push_reference_frame",
+ DisplayItem::PushStackingContext(..) => "push_stacking_context",
+ DisplayItem::SetFilterOps => "set_filter_ops",
+ DisplayItem::SetFilterData => "set_filter_data",
+ DisplayItem::SetFilterPrimitives => "set_filter_primitives",
+ DisplayItem::SetPoints => "set_points",
+ DisplayItem::RadialGradient(..) => "radial_gradient",
+ DisplayItem::Rectangle(..) => "rectangle",
+ DisplayItem::SetGradientStops => "set_gradient_stops",
+ DisplayItem::ReuseItems(..) => "reuse_item",
+ DisplayItem::RetainedItems(..) => "retained_items",
+ DisplayItem::Text(..) => "text",
+ DisplayItem::YuvImage(..) => "yuv_image",
+ DisplayItem::BackdropFilter(..) => "backdrop_filter",
+ }
+ }
+}
+
+macro_rules! impl_default_for_enums {
+ ($($enum:ident => $init:expr ),+) => {
+ $(impl Default for $enum {
+ #[allow(unused_imports)]
+ fn default() -> Self {
+ use $enum::*;
+ $init
+ }
+ })*
+ }
+}
+
+impl_default_for_enums! {
+ DisplayItem => PopStackingContext,
+ LineOrientation => Vertical,
+ LineStyle => Solid,
+ RepeatMode => Stretch,
+ NinePatchBorderSource => Image(ImageKey::default(), ImageRendering::Auto),
+ BorderDetails => Normal(NormalBorder::default()),
+ BorderRadiusKind => Uniform,
+ BorderStyle => None,
+ BoxShadowClipMode => Outset,
+ ExtendMode => Clamp,
+ FilterOp => Identity,
+ ComponentTransferFuncType => Identity,
+ ClipMode => Clip,
+ FillRule => Nonzero,
+ ReferenceFrameKind => Transform {
+ is_2d_scale_translation: false,
+ should_snap: false,
+ paired_with_perspective: false,
+ },
+ Rotation => Degree0,
+ TransformStyle => Flat,
+ RasterSpace => Local(f32::default()),
+ MixBlendMode => Normal,
+ ImageRendering => Auto,
+ AlphaType => Alpha,
+ YuvColorSpace => Rec601,
+ YuvRangedColorSpace => Rec601Narrow,
+ ColorRange => Limited,
+ YuvData => NV12(ImageKey::default(), ImageKey::default()),
+ YuvFormat => NV12,
+ FilterPrimitiveInput => Original,
+ ColorSpace => Srgb,
+ CompositeOperator => Over
+}
diff --git a/gfx/wr/webrender_api/src/display_item_cache.rs b/gfx/wr/webrender_api/src/display_item_cache.rs
new file mode 100644
index 0000000000..6cd1eb3e3a
--- /dev/null
+++ b/gfx/wr/webrender_api/src/display_item_cache.rs
@@ -0,0 +1,115 @@
+/* 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::display_item::*;
+use crate::display_list::*;
+use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
+
+#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
+pub struct CachedDisplayItem {
+ item: DisplayItem,
+ data: Vec<u8>,
+}
+
+impl CachedDisplayItem {
+ pub fn display_item(&self) -> &DisplayItem {
+ &self.item
+ }
+
+ pub fn data_as_item_range<T>(&self) -> ItemRange<T> {
+ ItemRange::new(&self.data)
+ }
+}
+
+impl MallocSizeOf for CachedDisplayItem {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.data.size_of(ops)
+ }
+}
+
+impl From<DisplayItemRef<'_, '_>> for CachedDisplayItem {
+ fn from(item_ref: DisplayItemRef) -> Self {
+ let item = item_ref.item();
+
+ match item {
+ DisplayItem::Text(..) => CachedDisplayItem {
+ item: *item,
+ data: item_ref.glyphs().bytes().to_vec(),
+ },
+ _ => CachedDisplayItem {
+ item: *item,
+ data: Vec::new(),
+ },
+ }
+ }
+}
+
+#[derive(Clone, Deserialize, MallocSizeOf, Serialize)]
+struct CacheEntry {
+ items: Vec<CachedDisplayItem>,
+ occupied: bool,
+}
+
+#[derive(Clone, Deserialize, MallocSizeOf, Serialize)]
+pub struct DisplayItemCache {
+ entries: Vec<CacheEntry>,
+}
+
+impl DisplayItemCache {
+ fn add_item(&mut self, key: ItemKey, item: CachedDisplayItem) {
+ let mut entry = &mut self.entries[key as usize];
+ entry.items.push(item);
+ entry.occupied = true;
+ }
+
+ fn clear_entry(&mut self, key: ItemKey) {
+ let mut entry = &mut self.entries[key as usize];
+ entry.items.clear();
+ entry.occupied = false;
+ }
+
+ fn grow_if_needed(&mut self, capacity: usize) {
+ if capacity > self.entries.len() {
+ self.entries.resize_with(capacity, || CacheEntry {
+ items: Vec::new(),
+ occupied: false,
+ });
+ }
+ }
+
+ pub fn get_items(&self, key: ItemKey) -> &[CachedDisplayItem] {
+ let entry = &self.entries[key as usize];
+ debug_assert!(entry.occupied);
+ entry.items.as_slice()
+ }
+
+ pub fn new() -> Self {
+ Self {
+ entries: Vec::new(),
+ }
+ }
+
+ pub fn update(&mut self, display_list: &BuiltDisplayList) {
+ self.grow_if_needed(display_list.cache_size());
+
+ let mut iter = display_list.cache_data_iter();
+ let mut current_key: Option<ItemKey> = None;
+ loop {
+ let item = match iter.next() {
+ Some(item) => item,
+ None => break,
+ };
+
+ if let DisplayItem::RetainedItems(key) = item.item() {
+ current_key = Some(*key);
+ self.clear_entry(*key);
+ continue;
+ }
+
+ let key = current_key.expect("Missing RetainedItems marker");
+ let cached_item = CachedDisplayItem::from(item);
+ self.add_item(key, cached_item);
+ }
+ }
+}
diff --git a/gfx/wr/webrender_api/src/display_list.rs b/gfx/wr/webrender_api/src/display_list.rs
new file mode 100644
index 0000000000..f1eea85ca3
--- /dev/null
+++ b/gfx/wr/webrender_api/src/display_list.rs
@@ -0,0 +1,2338 @@
+/* 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 peek_poke::{ensure_red_zone, peek_from_slice, poke_extend_vec, strip_red_zone};
+use peek_poke::{poke_inplace_slice, poke_into_vec, Poke};
+#[cfg(feature = "deserialize")]
+use serde::de::Deserializer;
+#[cfg(feature = "serialize")]
+use serde::ser::Serializer;
+use serde::{Deserialize, Serialize};
+use std::io::Write;
+use std::marker::PhantomData;
+use std::ops::Range;
+use std::mem;
+use std::collections::HashMap;
+use time::precise_time_ns;
+use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
+// local imports
+use crate::display_item as di;
+use crate::display_item_cache::*;
+use crate::{APZScrollGeneration, HasScrollLinkedEffect, PipelineId, PropertyBinding};
+use crate::gradient_builder::GradientBuilder;
+use crate::color::ColorF;
+use crate::font::{FontInstanceKey, GlyphInstance, GlyphOptions};
+use crate::image::{ColorDepth, ImageKey};
+use crate::units::*;
+
+
+// We don't want to push a long text-run. If a text-run is too long, split it into several parts.
+// This needs to be set to (renderer::MAX_VERTEX_TEXTURE_WIDTH - VECS_PER_TEXT_RUN) * 2
+pub const MAX_TEXT_RUN_LENGTH: usize = 2040;
+
+// See ROOT_REFERENCE_FRAME_SPATIAL_ID and ROOT_SCROLL_NODE_SPATIAL_ID
+// TODO(mrobinson): It would be a good idea to eliminate the root scroll frame which is only
+// used by Servo.
+const FIRST_SPATIAL_NODE_INDEX: usize = 2;
+
+// See ROOT_SCROLL_NODE_SPATIAL_ID
+const FIRST_CLIP_NODE_INDEX: usize = 1;
+
+#[derive(Debug, Copy, Clone, PartialEq)]
+enum BuildState {
+ Idle,
+ Build,
+}
+
+#[repr(C)]
+#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
+pub struct ItemRange<'a, T> {
+ bytes: &'a [u8],
+ _boo: PhantomData<T>,
+}
+
+impl<'a, T> Copy for ItemRange<'a, T> {}
+impl<'a, T> Clone for ItemRange<'a, T> {
+ fn clone(&self) -> Self {
+ *self
+ }
+}
+
+impl<'a, T> Default for ItemRange<'a, T> {
+ fn default() -> Self {
+ ItemRange {
+ bytes: Default::default(),
+ _boo: PhantomData,
+ }
+ }
+}
+
+impl<'a, T> ItemRange<'a, T> {
+ pub fn new(bytes: &'a [u8]) -> Self {
+ Self {
+ bytes,
+ _boo: PhantomData
+ }
+ }
+
+ pub fn is_empty(&self) -> bool {
+ // Nothing more than space for a length (0).
+ self.bytes.len() <= mem::size_of::<usize>()
+ }
+
+ pub fn bytes(&self) -> &[u8] {
+ &self.bytes
+ }
+}
+
+impl<'a, T: Default> ItemRange<'a, T> {
+ pub fn iter(&self) -> AuxIter<'a, T> {
+ AuxIter::new(T::default(), self.bytes)
+ }
+}
+
+impl<'a, T> IntoIterator for ItemRange<'a, T>
+where
+ T: Copy + Default + peek_poke::Peek,
+{
+ type Item = T;
+ type IntoIter = AuxIter<'a, T>;
+ fn into_iter(self) -> Self::IntoIter {
+ self.iter()
+ }
+}
+
+#[derive(Copy, Clone)]
+pub struct TempFilterData<'a> {
+ pub func_types: ItemRange<'a, di::ComponentTransferFuncType>,
+ pub r_values: ItemRange<'a, f32>,
+ pub g_values: ItemRange<'a, f32>,
+ pub b_values: ItemRange<'a, f32>,
+ pub a_values: ItemRange<'a, f32>,
+}
+
+#[derive(Default, Clone)]
+pub struct DisplayListPayload {
+ /// Serde encoded bytes. Mostly DisplayItems, but some mixed in slices.
+ pub items_data: Vec<u8>,
+
+ /// Serde encoded DisplayItemCache structs
+ pub cache_data: Vec<u8>,
+
+ /// Serde encoded SpatialTreeItem structs
+ pub spatial_tree: Vec<u8>,
+}
+
+impl DisplayListPayload {
+ fn default() -> Self {
+ DisplayListPayload {
+ items_data: Vec::new(),
+ cache_data: Vec::new(),
+ spatial_tree: Vec::new(),
+ }
+ }
+
+ fn new(capacity: DisplayListCapacity) -> Self {
+ let mut payload = Self::default();
+
+ // We can safely ignore the preallocations failing, since we aren't
+ // certain about how much memory we need, and this gives a chance for
+ // the memory pressure events to run.
+ if let Err(_) = payload.items_data.try_reserve(capacity.items_size) {
+ return Self::default();
+ }
+ if let Err(_) = payload.cache_data.try_reserve(capacity.cache_size) {
+ return Self::default();
+ }
+ if let Err(_) = payload.spatial_tree.try_reserve(capacity.spatial_tree_size) {
+ return Self::default();
+ }
+ payload
+ }
+
+ fn clear(&mut self) {
+ self.items_data.clear();
+ self.cache_data.clear();
+ self.spatial_tree.clear();
+ }
+
+ fn size_in_bytes(&self) -> usize {
+ self.items_data.len() +
+ self.cache_data.len() +
+ self.spatial_tree.len()
+ }
+
+ #[cfg(feature = "serialize")]
+ fn create_debug_spatial_tree_items(&self) -> Vec<di::SpatialTreeItem> {
+ let mut items = Vec::new();
+
+ iter_spatial_tree(&self.spatial_tree, |item| {
+ items.push(*item);
+ });
+
+ items
+ }
+}
+
+impl MallocSizeOf for DisplayListPayload {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.items_data.size_of(ops) +
+ self.cache_data.size_of(ops) +
+ self.spatial_tree.size_of(ops)
+ }
+}
+
+/// A display list.
+#[derive(Default, Clone)]
+pub struct BuiltDisplayList {
+ payload: DisplayListPayload,
+ descriptor: BuiltDisplayListDescriptor,
+}
+
+#[repr(C)]
+#[derive(Copy, Clone, Deserialize, Serialize)]
+pub enum GeckoDisplayListType {
+ None,
+ Partial(f64),
+ Full(f64),
+}
+
+impl Default for GeckoDisplayListType {
+ fn default() -> Self { GeckoDisplayListType::None }
+}
+
+/// Describes the memory layout of a display list.
+///
+/// A display list consists of some number of display list items, followed by a number of display
+/// items.
+#[repr(C)]
+#[derive(Copy, Clone, Default, Deserialize, Serialize)]
+pub struct BuiltDisplayListDescriptor {
+ /// Gecko specific information about the display list.
+ gecko_display_list_type: GeckoDisplayListType,
+ /// The first IPC time stamp: before any work has been done
+ builder_start_time: u64,
+ /// The second IPC time stamp: after serialization
+ builder_finish_time: u64,
+ /// The third IPC time stamp: just before sending
+ send_start_time: u64,
+ /// The amount of clipping nodes created while building this display list.
+ total_clip_nodes: usize,
+ /// The amount of spatial nodes created while building this display list.
+ total_spatial_nodes: usize,
+ /// The size of the cache for this display list.
+ cache_size: usize,
+}
+
+#[derive(Clone)]
+pub struct DisplayListWithCache {
+ pub display_list: BuiltDisplayList,
+ cache: DisplayItemCache,
+}
+
+impl DisplayListWithCache {
+ pub fn iter(&self) -> BuiltDisplayListIter {
+ self.display_list.iter_with_cache(&self.cache)
+ }
+
+ pub fn new_from_list(display_list: BuiltDisplayList) -> Self {
+ let mut cache = DisplayItemCache::new();
+ cache.update(&display_list);
+
+ DisplayListWithCache {
+ display_list,
+ cache
+ }
+ }
+
+ pub fn update(&mut self, display_list: BuiltDisplayList) {
+ self.cache.update(&display_list);
+ self.display_list = display_list;
+ }
+
+ pub fn descriptor(&self) -> &BuiltDisplayListDescriptor {
+ self.display_list.descriptor()
+ }
+
+ pub fn times(&self) -> (u64, u64, u64) {
+ self.display_list.times()
+ }
+
+ pub fn items_data(&self) -> &[u8] {
+ self.display_list.items_data()
+ }
+}
+
+impl MallocSizeOf for DisplayListWithCache {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.display_list.payload.size_of(ops) + self.cache.size_of(ops)
+ }
+}
+
+/// A debug (human-readable) representation of a built display list that
+/// can be used for capture and replay.
+#[cfg(any(feature = "serialize", feature = "deserialize"))]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "deserialize", derive(Deserialize))]
+struct DisplayListCapture {
+ display_items: Vec<di::DebugDisplayItem>,
+ spatial_tree_items: Vec<di::SpatialTreeItem>,
+ descriptor: BuiltDisplayListDescriptor,
+}
+
+#[cfg(feature = "serialize")]
+impl Serialize for DisplayListWithCache {
+ fn serialize<S: Serializer>(
+ &self,
+ serializer: S
+ ) -> Result<S::Ok, S::Error> {
+ let display_items = BuiltDisplayList::create_debug_display_items(self.iter());
+ let spatial_tree_items = self.display_list.payload.create_debug_spatial_tree_items();
+
+ let dl = DisplayListCapture {
+ display_items,
+ spatial_tree_items,
+ descriptor: self.display_list.descriptor,
+ };
+
+ dl.serialize(serializer)
+ }
+}
+
+#[cfg(feature = "deserialize")]
+impl<'de> Deserialize<'de> for DisplayListWithCache {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ use crate::display_item::DisplayItem as Real;
+ use crate::display_item::DebugDisplayItem as Debug;
+
+ let capture = DisplayListCapture::deserialize(deserializer)?;
+
+ let mut spatial_tree = Vec::new();
+ for item in capture.spatial_tree_items {
+ poke_into_vec(&item, &mut spatial_tree);
+ }
+ ensure_red_zone::<di::SpatialTreeItem>(&mut spatial_tree);
+
+ let mut items_data = Vec::new();
+ let mut temp = Vec::new();
+ for complete in capture.display_items {
+ let item = match complete {
+ Debug::ClipChain(v, clip_chain_ids) => {
+ DisplayListBuilder::push_iter_impl(&mut temp, clip_chain_ids);
+ Real::ClipChain(v)
+ }
+ Debug::Text(v, glyphs) => {
+ DisplayListBuilder::push_iter_impl(&mut temp, glyphs);
+ Real::Text(v)
+ },
+ Debug::Iframe(v) => {
+ Real::Iframe(v)
+ }
+ Debug::PushReferenceFrame(v) => {
+ Real::PushReferenceFrame(v)
+ }
+ Debug::SetFilterOps(filters) => {
+ DisplayListBuilder::push_iter_impl(&mut temp, filters);
+ Real::SetFilterOps
+ },
+ Debug::SetFilterData(filter_data) => {
+ let func_types: Vec<di::ComponentTransferFuncType> =
+ [filter_data.func_r_type,
+ filter_data.func_g_type,
+ filter_data.func_b_type,
+ filter_data.func_a_type].to_vec();
+ DisplayListBuilder::push_iter_impl(&mut temp, func_types);
+ DisplayListBuilder::push_iter_impl(&mut temp, filter_data.r_values);
+ DisplayListBuilder::push_iter_impl(&mut temp, filter_data.g_values);
+ DisplayListBuilder::push_iter_impl(&mut temp, filter_data.b_values);
+ DisplayListBuilder::push_iter_impl(&mut temp, filter_data.a_values);
+ Real::SetFilterData
+ },
+ Debug::SetFilterPrimitives(filter_primitives) => {
+ DisplayListBuilder::push_iter_impl(&mut temp, filter_primitives);
+ Real::SetFilterPrimitives
+ }
+ Debug::SetGradientStops(stops) => {
+ DisplayListBuilder::push_iter_impl(&mut temp, stops);
+ Real::SetGradientStops
+ },
+ Debug::SetPoints(points) => {
+ DisplayListBuilder::push_iter_impl(&mut temp, points);
+ Real::SetPoints
+ },
+ Debug::RectClip(v) => Real::RectClip(v),
+ Debug::RoundedRectClip(v) => Real::RoundedRectClip(v),
+ Debug::ImageMaskClip(v) => Real::ImageMaskClip(v),
+ Debug::Rectangle(v) => Real::Rectangle(v),
+ Debug::ClearRectangle(v) => Real::ClearRectangle(v),
+ Debug::HitTest(v) => Real::HitTest(v),
+ Debug::Line(v) => Real::Line(v),
+ Debug::Image(v) => Real::Image(v),
+ Debug::RepeatingImage(v) => Real::RepeatingImage(v),
+ Debug::YuvImage(v) => Real::YuvImage(v),
+ Debug::Border(v) => Real::Border(v),
+ Debug::BoxShadow(v) => Real::BoxShadow(v),
+ Debug::Gradient(v) => Real::Gradient(v),
+ Debug::RadialGradient(v) => Real::RadialGradient(v),
+ Debug::ConicGradient(v) => Real::ConicGradient(v),
+ Debug::PushStackingContext(v) => Real::PushStackingContext(v),
+ Debug::PushShadow(v) => Real::PushShadow(v),
+ Debug::BackdropFilter(v) => Real::BackdropFilter(v),
+
+ Debug::PopStackingContext => Real::PopStackingContext,
+ Debug::PopReferenceFrame => Real::PopReferenceFrame,
+ Debug::PopAllShadows => Real::PopAllShadows,
+ };
+ poke_into_vec(&item, &mut items_data);
+ // the aux data is serialized after the item, hence the temporary
+ items_data.extend(temp.drain(..));
+ }
+
+ // Add `DisplayItem::max_size` zone of zeroes to the end of display list
+ // so there is at least this amount available in the display list during
+ // serialization.
+ ensure_red_zone::<di::DisplayItem>(&mut items_data);
+
+ Ok(DisplayListWithCache {
+ display_list: BuiltDisplayList {
+ descriptor: capture.descriptor,
+ payload: DisplayListPayload {
+ cache_data: Vec::new(),
+ items_data,
+ spatial_tree,
+ },
+ },
+ cache: DisplayItemCache::new(),
+ })
+ }
+}
+
+pub struct BuiltDisplayListIter<'a> {
+ data: &'a [u8],
+ cache: Option<&'a DisplayItemCache>,
+ pending_items: std::slice::Iter<'a, CachedDisplayItem>,
+ cur_cached_item: Option<&'a CachedDisplayItem>,
+ cur_item: di::DisplayItem,
+ cur_stops: ItemRange<'a, di::GradientStop>,
+ cur_glyphs: ItemRange<'a, GlyphInstance>,
+ cur_filters: ItemRange<'a, di::FilterOp>,
+ cur_filter_data: Vec<TempFilterData<'a>>,
+ cur_filter_primitives: ItemRange<'a, di::FilterPrimitive>,
+ cur_clip_chain_items: ItemRange<'a, di::ClipId>,
+ cur_points: ItemRange<'a, LayoutPoint>,
+ peeking: Peek,
+ /// Should just be initialized but never populated in release builds
+ debug_stats: DebugStats,
+}
+
+/// Internal info used for more detailed analysis of serialized display lists
+#[allow(dead_code)]
+struct DebugStats {
+ /// Last address in the buffer we pointed to, for computing serialized sizes
+ last_addr: usize,
+ stats: HashMap<&'static str, ItemStats>,
+}
+
+impl DebugStats {
+ #[cfg(feature = "display_list_stats")]
+ fn _update_entry(&mut self, name: &'static str, item_count: usize, byte_count: usize) {
+ let entry = self.stats.entry(name).or_default();
+ entry.total_count += item_count;
+ entry.num_bytes += byte_count;
+ }
+
+ /// Computes the number of bytes we've processed since we last called
+ /// this method, so we can compute the serialized size of a display item.
+ #[cfg(feature = "display_list_stats")]
+ fn debug_num_bytes(&mut self, data: &[u8]) -> usize {
+ let old_addr = self.last_addr;
+ let new_addr = data.as_ptr() as usize;
+ let delta = new_addr - old_addr;
+ self.last_addr = new_addr;
+
+ delta
+ }
+
+ /// Logs stats for the last deserialized display item
+ #[cfg(feature = "display_list_stats")]
+ fn log_item(&mut self, data: &[u8], item: &di::DisplayItem) {
+ let num_bytes = self.debug_num_bytes(data);
+ self._update_entry(item.debug_name(), 1, num_bytes);
+ }
+
+ /// Logs the stats for the given serialized slice
+ #[cfg(feature = "display_list_stats")]
+ fn log_slice<T: Copy + Default + peek_poke::Peek>(
+ &mut self,
+ slice_name: &'static str,
+ range: &ItemRange<T>,
+ ) {
+ // Run this so log_item_stats is accurate, but ignore its result
+ // because log_slice_stats may be called after multiple slices have been
+ // processed, and the `range` has everything we need.
+ self.last_addr = range.bytes.as_ptr() as usize + range.bytes.len();
+
+ self._update_entry(slice_name, range.iter().len(), range.bytes.len());
+ }
+
+ #[cfg(not(feature = "display_list_stats"))]
+ fn log_slice<T>(&mut self, _slice_name: &str, _range: &ItemRange<T>) {
+ /* no-op */
+ }
+}
+
+/// Stats for an individual item
+#[derive(Copy, Clone, Debug, Default)]
+pub struct ItemStats {
+ /// How many instances of this kind of item we deserialized
+ pub total_count: usize,
+ /// How many bytes we processed for this kind of item
+ pub num_bytes: usize,
+}
+
+pub struct DisplayItemRef<'a: 'b, 'b> {
+ iter: &'b BuiltDisplayListIter<'a>,
+}
+
+// Some of these might just become ItemRanges
+impl<'a, 'b> DisplayItemRef<'a, 'b> {
+ // Creates a new iterator where this element's iterator is, to hack around borrowck.
+ pub fn sub_iter(&self) -> BuiltDisplayListIter<'a> {
+ self.iter.sub_iter()
+ }
+
+ pub fn item(&self) -> &di::DisplayItem {
+ self.iter.current_item()
+ }
+
+ pub fn clip_chain_items(&self) -> ItemRange<di::ClipId> {
+ self.iter.cur_clip_chain_items
+ }
+
+ pub fn points(&self) -> ItemRange<LayoutPoint> {
+ self.iter.cur_points
+ }
+
+ pub fn glyphs(&self) -> ItemRange<GlyphInstance> {
+ self.iter.glyphs()
+ }
+
+ pub fn gradient_stops(&self) -> ItemRange<di::GradientStop> {
+ self.iter.gradient_stops()
+ }
+
+ pub fn filters(&self) -> ItemRange<di::FilterOp> {
+ self.iter.cur_filters
+ }
+
+ pub fn filter_datas(&self) -> &Vec<TempFilterData> {
+ &self.iter.cur_filter_data
+ }
+
+ pub fn filter_primitives(&self) -> ItemRange<di::FilterPrimitive> {
+ self.iter.cur_filter_primitives
+ }
+}
+
+#[derive(PartialEq)]
+enum Peek {
+ StartPeeking,
+ IsPeeking,
+ NotPeeking,
+}
+
+#[derive(Clone)]
+pub struct AuxIter<'a, T> {
+ item: T,
+ data: &'a [u8],
+ size: usize,
+// _boo: PhantomData<T>,
+}
+
+impl BuiltDisplayList {
+ pub fn from_data(
+ payload: DisplayListPayload,
+ descriptor: BuiltDisplayListDescriptor,
+ ) -> Self {
+ BuiltDisplayList {
+ payload,
+ descriptor,
+ }
+ }
+
+ pub fn into_data(self) -> (DisplayListPayload, BuiltDisplayListDescriptor) {
+ (self.payload, self.descriptor)
+ }
+
+ pub fn items_data(&self) -> &[u8] {
+ &self.payload.items_data
+ }
+
+ pub fn cache_data(&self) -> &[u8] {
+ &self.payload.cache_data
+ }
+
+ pub fn descriptor(&self) -> &BuiltDisplayListDescriptor {
+ &self.descriptor
+ }
+
+ pub fn set_send_time_ns(&mut self, time: u64) {
+ self.descriptor.send_start_time = time;
+ }
+
+ pub fn times(&self) -> (u64, u64, u64) {
+ (
+ self.descriptor.builder_start_time,
+ self.descriptor.builder_finish_time,
+ self.descriptor.send_start_time,
+ )
+ }
+
+ pub fn gecko_display_list_stats(&self) -> (f64, bool) {
+ match self.descriptor.gecko_display_list_type {
+ GeckoDisplayListType::Full(duration) => (duration, true),
+ GeckoDisplayListType::Partial(duration) => (duration, false),
+ _ => (0.0, false)
+ }
+ }
+
+ pub fn total_clip_nodes(&self) -> usize {
+ self.descriptor.total_clip_nodes
+ }
+
+ pub fn total_spatial_nodes(&self) -> usize {
+ self.descriptor.total_spatial_nodes
+ }
+
+ pub fn iter(&self) -> BuiltDisplayListIter {
+ BuiltDisplayListIter::new(self.items_data(), None)
+ }
+
+ pub fn cache_data_iter(&self) -> BuiltDisplayListIter {
+ BuiltDisplayListIter::new(self.cache_data(), None)
+ }
+
+ pub fn iter_with_cache<'a>(
+ &'a self,
+ cache: &'a DisplayItemCache
+ ) -> BuiltDisplayListIter<'a> {
+ BuiltDisplayListIter::new(self.items_data(), Some(cache))
+ }
+
+ pub fn cache_size(&self) -> usize {
+ self.descriptor.cache_size
+ }
+
+ pub fn size_in_bytes(&self) -> usize {
+ self.payload.size_in_bytes()
+ }
+
+ pub fn iter_spatial_tree<F>(&self, f: F) where F: FnMut(&di::SpatialTreeItem) {
+ iter_spatial_tree(&self.payload.spatial_tree, f)
+ }
+
+ #[cfg(feature = "serialize")]
+ pub fn create_debug_display_items(
+ mut iterator: BuiltDisplayListIter,
+ ) -> Vec<di::DebugDisplayItem> {
+ use di::DisplayItem as Real;
+ use di::DebugDisplayItem as Debug;
+ let mut debug_items = Vec::new();
+
+ while let Some(item) = iterator.next_raw() {
+ let serial_di = match *item.item() {
+ Real::ClipChain(v) => Debug::ClipChain(
+ v,
+ item.iter.cur_clip_chain_items.iter().collect()
+ ),
+ Real::Text(v) => Debug::Text(
+ v,
+ item.iter.cur_glyphs.iter().collect()
+ ),
+ Real::SetFilterOps => Debug::SetFilterOps(
+ item.iter.cur_filters.iter().collect()
+ ),
+ Real::SetFilterData => {
+ debug_assert!(!item.iter.cur_filter_data.is_empty(),
+ "next_raw should have populated cur_filter_data");
+ let temp_filter_data = &item.iter.cur_filter_data[item.iter.cur_filter_data.len()-1];
+
+ let func_types: Vec<di::ComponentTransferFuncType> =
+ temp_filter_data.func_types.iter().collect();
+ debug_assert!(func_types.len() == 4,
+ "someone changed the number of filter funcs without updating this code");
+ Debug::SetFilterData(di::FilterData {
+ func_r_type: func_types[0],
+ r_values: temp_filter_data.r_values.iter().collect(),
+ func_g_type: func_types[1],
+ g_values: temp_filter_data.g_values.iter().collect(),
+ func_b_type: func_types[2],
+ b_values: temp_filter_data.b_values.iter().collect(),
+ func_a_type: func_types[3],
+ a_values: temp_filter_data.a_values.iter().collect(),
+ })
+ },
+ Real::SetFilterPrimitives => Debug::SetFilterPrimitives(
+ item.iter.cur_filter_primitives.iter().collect()
+ ),
+ Real::SetGradientStops => Debug::SetGradientStops(
+ item.iter.cur_stops.iter().collect()
+ ),
+ Real::SetPoints => Debug::SetPoints(
+ item.iter.cur_points.iter().collect()
+ ),
+ Real::RectClip(v) => Debug::RectClip(v),
+ Real::RoundedRectClip(v) => Debug::RoundedRectClip(v),
+ Real::ImageMaskClip(v) => Debug::ImageMaskClip(v),
+ Real::Rectangle(v) => Debug::Rectangle(v),
+ Real::ClearRectangle(v) => Debug::ClearRectangle(v),
+ Real::HitTest(v) => Debug::HitTest(v),
+ Real::Line(v) => Debug::Line(v),
+ Real::Image(v) => Debug::Image(v),
+ Real::RepeatingImage(v) => Debug::RepeatingImage(v),
+ Real::YuvImage(v) => Debug::YuvImage(v),
+ Real::Border(v) => Debug::Border(v),
+ Real::BoxShadow(v) => Debug::BoxShadow(v),
+ Real::Gradient(v) => Debug::Gradient(v),
+ Real::RadialGradient(v) => Debug::RadialGradient(v),
+ Real::ConicGradient(v) => Debug::ConicGradient(v),
+ Real::Iframe(v) => Debug::Iframe(v),
+ Real::PushReferenceFrame(v) => Debug::PushReferenceFrame(v),
+ Real::PushStackingContext(v) => Debug::PushStackingContext(v),
+ Real::PushShadow(v) => Debug::PushShadow(v),
+ Real::BackdropFilter(v) => Debug::BackdropFilter(v),
+
+ Real::PopReferenceFrame => Debug::PopReferenceFrame,
+ Real::PopStackingContext => Debug::PopStackingContext,
+ Real::PopAllShadows => Debug::PopAllShadows,
+ Real::ReuseItems(_) |
+ Real::RetainedItems(_) => unreachable!("Unexpected item"),
+ };
+ debug_items.push(serial_di);
+ }
+
+ debug_items
+ }
+}
+
+/// Returns the byte-range the slice occupied.
+fn skip_slice<'a, T: peek_poke::Peek>(data: &mut &'a [u8]) -> ItemRange<'a, T> {
+ let mut skip_offset = 0usize;
+ *data = peek_from_slice(data, &mut skip_offset);
+ let (skip, rest) = data.split_at(skip_offset);
+
+ // Adjust data pointer to skip read values
+ *data = rest;
+
+ ItemRange {
+ bytes: skip,
+ _boo: PhantomData,
+ }
+}
+
+impl<'a> BuiltDisplayListIter<'a> {
+ pub fn new(
+ data: &'a [u8],
+ cache: Option<&'a DisplayItemCache>,
+ ) -> Self {
+ Self {
+ data,
+ cache,
+ pending_items: [].iter(),
+ cur_cached_item: None,
+ cur_item: di::DisplayItem::PopStackingContext,
+ cur_stops: ItemRange::default(),
+ cur_glyphs: ItemRange::default(),
+ cur_filters: ItemRange::default(),
+ cur_filter_data: Vec::new(),
+ cur_filter_primitives: ItemRange::default(),
+ cur_clip_chain_items: ItemRange::default(),
+ cur_points: ItemRange::default(),
+ peeking: Peek::NotPeeking,
+ debug_stats: DebugStats {
+ last_addr: data.as_ptr() as usize,
+ stats: HashMap::default(),
+ },
+ }
+ }
+
+ pub fn sub_iter(&self) -> Self {
+ let mut iter = BuiltDisplayListIter::new(
+ self.data, self.cache
+ );
+ iter.pending_items = self.pending_items.clone();
+ iter
+ }
+
+ pub fn current_item(&self) -> &di::DisplayItem {
+ match self.cur_cached_item {
+ Some(cached_item) => cached_item.display_item(),
+ None => &self.cur_item
+ }
+ }
+
+ fn cached_item_range_or<T>(
+ &self,
+ data: ItemRange<'a, T>
+ ) -> ItemRange<'a, T> {
+ match self.cur_cached_item {
+ Some(cached_item) => cached_item.data_as_item_range(),
+ None => data,
+ }
+ }
+
+ pub fn glyphs(&self) -> ItemRange<GlyphInstance> {
+ self.cached_item_range_or(self.cur_glyphs)
+ }
+
+ pub fn gradient_stops(&self) -> ItemRange<di::GradientStop> {
+ self.cached_item_range_or(self.cur_stops)
+ }
+
+ fn advance_pending_items(&mut self) -> bool {
+ self.cur_cached_item = self.pending_items.next();
+ self.cur_cached_item.is_some()
+ }
+
+ pub fn next<'b>(&'b mut self) -> Option<DisplayItemRef<'a, 'b>> {
+ use crate::DisplayItem::*;
+
+ match self.peeking {
+ Peek::IsPeeking => {
+ self.peeking = Peek::NotPeeking;
+ return Some(self.as_ref());
+ }
+ Peek::StartPeeking => {
+ self.peeking = Peek::IsPeeking;
+ }
+ Peek::NotPeeking => { /* do nothing */ }
+ }
+
+ // Don't let these bleed into another item
+ self.cur_stops = ItemRange::default();
+ self.cur_clip_chain_items = ItemRange::default();
+ self.cur_points = ItemRange::default();
+ self.cur_filters = ItemRange::default();
+ self.cur_filter_primitives = ItemRange::default();
+ self.cur_filter_data.clear();
+
+ loop {
+ self.next_raw()?;
+ match self.cur_item {
+ SetGradientStops |
+ SetFilterOps |
+ SetFilterData |
+ SetFilterPrimitives |
+ SetPoints => {
+ // These are marker items for populating other display items, don't yield them.
+ continue;
+ }
+ _ => {
+ break;
+ }
+ }
+ }
+
+ Some(self.as_ref())
+ }
+
+ /// Gets the next display item, even if it's a dummy. Also doesn't handle peeking
+ /// and may leave irrelevant ranges live (so a Clip may have GradientStops if
+ /// for some reason you ask).
+ pub fn next_raw<'b>(&'b mut self) -> Option<DisplayItemRef<'a, 'b>> {
+ use crate::DisplayItem::*;
+
+ if self.advance_pending_items() {
+ return Some(self.as_ref());
+ }
+
+ // A "red zone" of DisplayItem::max_size() bytes has been added to the
+ // end of the serialized display list. If this amount, or less, is
+ // remaining then we've reached the end of the display list.
+ if self.data.len() <= di::DisplayItem::max_size() {
+ return None;
+ }
+
+ self.data = peek_from_slice(self.data, &mut self.cur_item);
+ self.log_item_stats();
+
+ match self.cur_item {
+ SetGradientStops => {
+ self.cur_stops = skip_slice::<di::GradientStop>(&mut self.data);
+ self.debug_stats.log_slice("set_gradient_stops.stops", &self.cur_stops);
+ }
+ SetFilterOps => {
+ self.cur_filters = skip_slice::<di::FilterOp>(&mut self.data);
+ self.debug_stats.log_slice("set_filter_ops.ops", &self.cur_filters);
+ }
+ SetFilterData => {
+ self.cur_filter_data.push(TempFilterData {
+ func_types: skip_slice::<di::ComponentTransferFuncType>(&mut self.data),
+ r_values: skip_slice::<f32>(&mut self.data),
+ g_values: skip_slice::<f32>(&mut self.data),
+ b_values: skip_slice::<f32>(&mut self.data),
+ a_values: skip_slice::<f32>(&mut self.data),
+ });
+
+ let data = *self.cur_filter_data.last().unwrap();
+ self.debug_stats.log_slice("set_filter_data.func_types", &data.func_types);
+ self.debug_stats.log_slice("set_filter_data.r_values", &data.r_values);
+ self.debug_stats.log_slice("set_filter_data.g_values", &data.g_values);
+ self.debug_stats.log_slice("set_filter_data.b_values", &data.b_values);
+ self.debug_stats.log_slice("set_filter_data.a_values", &data.a_values);
+ }
+ SetFilterPrimitives => {
+ self.cur_filter_primitives = skip_slice::<di::FilterPrimitive>(&mut self.data);
+ self.debug_stats.log_slice("set_filter_primitives.primitives", &self.cur_filter_primitives);
+ }
+ SetPoints => {
+ self.cur_points = skip_slice::<LayoutPoint>(&mut self.data);
+ self.debug_stats.log_slice("set_points.points", &self.cur_points);
+ }
+ ClipChain(_) => {
+ self.cur_clip_chain_items = skip_slice::<di::ClipId>(&mut self.data);
+ self.debug_stats.log_slice("clip_chain.clip_ids", &self.cur_clip_chain_items);
+ }
+ Text(_) => {
+ self.cur_glyphs = skip_slice::<GlyphInstance>(&mut self.data);
+ self.debug_stats.log_slice("text.glyphs", &self.cur_glyphs);
+ }
+ ReuseItems(key) => {
+ match self.cache {
+ Some(cache) => {
+ self.pending_items = cache.get_items(key).iter();
+ self.advance_pending_items();
+ }
+ None => {
+ unreachable!("Cache marker without cache!");
+ }
+ }
+ }
+ _ => { /* do nothing */ }
+ }
+
+ Some(self.as_ref())
+ }
+
+ pub fn as_ref<'b>(&'b self) -> DisplayItemRef<'a, 'b> {
+ DisplayItemRef {
+ iter: self,
+ }
+ }
+
+ pub fn skip_current_stacking_context(&mut self) {
+ let mut depth = 0;
+ while let Some(item) = self.next() {
+ match *item.item() {
+ di::DisplayItem::PushStackingContext(..) => depth += 1,
+ di::DisplayItem::PopStackingContext if depth == 0 => return,
+ di::DisplayItem::PopStackingContext => depth -= 1,
+ _ => {}
+ }
+ }
+ }
+
+ pub fn current_stacking_context_empty(&mut self) -> bool {
+ match self.peek() {
+ Some(item) => *item.item() == di::DisplayItem::PopStackingContext,
+ None => true,
+ }
+ }
+
+ pub fn peek<'b>(&'b mut self) -> Option<DisplayItemRef<'a, 'b>> {
+ if self.peeking == Peek::NotPeeking {
+ self.peeking = Peek::StartPeeking;
+ self.next()
+ } else {
+ Some(self.as_ref())
+ }
+ }
+
+ /// Get the debug stats for what this iterator has deserialized.
+ /// Should always be empty in release builds.
+ pub fn debug_stats(&mut self) -> Vec<(&'static str, ItemStats)> {
+ let mut result = self.debug_stats.stats.drain().collect::<Vec<_>>();
+ result.sort_by_key(|stats| stats.0);
+ result
+ }
+
+ /// Adds the debug stats from another to our own, assuming we are a sub-iter of the other
+ /// (so we can ignore where they were in the traversal).
+ pub fn merge_debug_stats_from(&mut self, other: &mut Self) {
+ for (key, other_entry) in other.debug_stats.stats.iter() {
+ let entry = self.debug_stats.stats.entry(key).or_default();
+
+ entry.total_count += other_entry.total_count;
+ entry.num_bytes += other_entry.num_bytes;
+ }
+ }
+
+ /// Logs stats for the last deserialized display item
+ #[cfg(feature = "display_list_stats")]
+ fn log_item_stats(&mut self) {
+ self.debug_stats.log_item(self.data, &self.cur_item);
+ }
+
+ #[cfg(not(feature = "display_list_stats"))]
+ fn log_item_stats(&mut self) { /* no-op */ }
+}
+
+impl<'a, T> AuxIter<'a, T> {
+ pub fn new(item: T, mut data: &'a [u8]) -> Self {
+ let mut size = 0usize;
+ if !data.is_empty() {
+ data = peek_from_slice(data, &mut size);
+ };
+
+ AuxIter {
+ item,
+ data,
+ size,
+// _boo: PhantomData,
+ }
+ }
+}
+
+impl<'a, T: Copy + peek_poke::Peek> Iterator for AuxIter<'a, T> {
+ type Item = T;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.size == 0 {
+ None
+ } else {
+ self.size -= 1;
+ self.data = peek_from_slice(self.data, &mut self.item);
+ Some(self.item)
+ }
+ }
+
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ (self.size, Some(self.size))
+ }
+}
+
+impl<'a, T: Copy + peek_poke::Peek> ::std::iter::ExactSizeIterator for AuxIter<'a, T> {}
+
+#[derive(Clone, Debug)]
+pub struct SaveState {
+ dl_items_len: usize,
+ dl_cache_len: usize,
+ next_clip_index: usize,
+ next_spatial_index: usize,
+ next_clip_chain_id: u64,
+}
+
+/// DisplayListSection determines the target buffer for the display items.
+pub enum DisplayListSection {
+ /// The main/default buffer: contains item data and item group markers.
+ Data,
+ /// Auxiliary buffer: contains the item data for item groups.
+ CacheData,
+ /// Temporary buffer: contains the data for pending item group. Flushed to
+ /// one of the buffers above, after item grouping finishes.
+ Chunk,
+}
+
+/// A small portion of a normal spatial node that we store during DL construction to
+/// enable snapping and reference frame <-> stacking context coord mapping. In future
+/// we'll aim to remove this and have the full spatial tree available during DL build.
+#[derive(Clone)]
+pub struct SpatialNodeInfo {
+ /// The total external scroll offset applicable at this node
+ accumulated_external_scroll_offset: LayoutVector2D,
+}
+
+impl SpatialNodeInfo {
+ fn identity() -> Self {
+ SpatialNodeInfo {
+ accumulated_external_scroll_offset: LayoutVector2D::zero(),
+ }
+ }
+}
+
+pub struct DisplayListBuilder {
+ payload: DisplayListPayload,
+ pub pipeline_id: PipelineId,
+
+ pending_chunk: Vec<u8>,
+ writing_to_chunk: bool,
+
+ next_clip_index: usize,
+ next_spatial_index: usize,
+ next_clip_chain_id: u64,
+ builder_start_time: u64,
+
+ save_state: Option<SaveState>,
+
+ cache_size: usize,
+ serialized_content_buffer: Option<String>,
+ state: BuildState,
+
+ /// Helper struct to map stacking context coords <-> reference frame coords.
+ rf_mapper: ReferenceFrameMapper,
+
+ /// Minimal info about encountered spatial nodes to allow snapping during DL building
+ spatial_nodes: Vec<SpatialNodeInfo>,
+}
+
+#[repr(C)]
+struct DisplayListCapacity {
+ items_size: usize,
+ cache_size: usize,
+ spatial_tree_size: usize,
+}
+
+impl DisplayListCapacity {
+ fn empty() -> Self {
+ DisplayListCapacity {
+ items_size: 0,
+ cache_size: 0,
+ spatial_tree_size: 0,
+ }
+ }
+}
+
+impl DisplayListBuilder {
+ pub fn new(pipeline_id: PipelineId) -> Self {
+ DisplayListBuilder {
+ payload: DisplayListPayload::new(DisplayListCapacity::empty()),
+ pipeline_id,
+
+ pending_chunk: Vec::new(),
+ writing_to_chunk: false,
+
+ next_clip_index: FIRST_CLIP_NODE_INDEX,
+ next_spatial_index: FIRST_SPATIAL_NODE_INDEX,
+ next_clip_chain_id: 0,
+ builder_start_time: 0,
+ save_state: None,
+ cache_size: 0,
+ serialized_content_buffer: None,
+ state: BuildState::Idle,
+
+ rf_mapper: ReferenceFrameMapper::new(),
+ spatial_nodes: vec![SpatialNodeInfo::identity(); FIRST_SPATIAL_NODE_INDEX + 1],
+ }
+ }
+
+ fn reset(&mut self) {
+ self.payload.clear();
+ self.pending_chunk.clear();
+ self.writing_to_chunk = false;
+
+ self.next_clip_index = FIRST_CLIP_NODE_INDEX;
+ self.next_spatial_index = FIRST_SPATIAL_NODE_INDEX;
+ self.next_clip_chain_id = 0;
+
+ self.save_state = None;
+ self.cache_size = 0;
+ self.serialized_content_buffer = None;
+
+ self.rf_mapper = ReferenceFrameMapper::new();
+ self.spatial_nodes = vec![SpatialNodeInfo::identity(); FIRST_SPATIAL_NODE_INDEX + 1];
+ }
+
+ /// Saves the current display list state, so it may be `restore()`'d.
+ ///
+ /// # Conditions:
+ ///
+ /// * Doesn't support popping clips that were pushed before the save.
+ /// * Doesn't support nested saves.
+ /// * Must call `clear_save()` if the restore becomes unnecessary.
+ pub fn save(&mut self) {
+ assert!(self.save_state.is_none(), "DisplayListBuilder doesn't support nested saves");
+
+ self.save_state = Some(SaveState {
+ dl_items_len: self.payload.items_data.len(),
+ dl_cache_len: self.payload.cache_data.len(),
+ next_clip_index: self.next_clip_index,
+ next_spatial_index: self.next_spatial_index,
+ next_clip_chain_id: self.next_clip_chain_id,
+ });
+ }
+
+ /// Restores the state of the builder to when `save()` was last called.
+ pub fn restore(&mut self) {
+ let state = self.save_state.take().expect("No save to restore DisplayListBuilder from");
+
+ self.payload.items_data.truncate(state.dl_items_len);
+ self.payload.cache_data.truncate(state.dl_cache_len);
+ self.next_clip_index = state.next_clip_index;
+ self.next_spatial_index = state.next_spatial_index;
+ self.next_clip_chain_id = state.next_clip_chain_id;
+ }
+
+ /// Discards the builder's save (indicating the attempted operation was successful).
+ pub fn clear_save(&mut self) {
+ self.save_state.take().expect("No save to clear in DisplayListBuilder");
+ }
+
+ /// Emits a debug representation of display items in the list, for debugging
+ /// purposes. If the range's start parameter is specified, only display
+ /// items starting at that index (inclusive) will be printed. If the range's
+ /// end parameter is specified, only display items before that index
+ /// (exclusive) will be printed. Calling this function with end <= start is
+ /// allowed but is just a waste of CPU cycles. The function emits the
+ /// debug representation of the selected display items, one per line, with
+ /// the given indent, to the provided sink object. The return value is
+ /// the total number of items in the display list, which allows the
+ /// caller to subsequently invoke this function to only dump the newly-added
+ /// items.
+ pub fn emit_display_list<W>(
+ &mut self,
+ indent: usize,
+ range: Range<Option<usize>>,
+ mut sink: W,
+ ) -> usize
+ where
+ W: Write
+ {
+ let mut temp = BuiltDisplayList::default();
+ ensure_red_zone::<di::DisplayItem>(&mut self.payload.items_data);
+ ensure_red_zone::<di::DisplayItem>(&mut self.payload.cache_data);
+ mem::swap(&mut temp.payload, &mut self.payload);
+
+ let mut index: usize = 0;
+ {
+ let mut cache = DisplayItemCache::new();
+ cache.update(&temp);
+ let mut iter = temp.iter_with_cache(&cache);
+ while let Some(item) = iter.next_raw() {
+ if index >= range.start.unwrap_or(0) && range.end.map_or(true, |e| index < e) {
+ writeln!(sink, "{}{:?}", " ".repeat(indent), item.item()).unwrap();
+ }
+ index += 1;
+ }
+ }
+
+ self.payload = temp.payload;
+ strip_red_zone::<di::DisplayItem>(&mut self.payload.items_data);
+ strip_red_zone::<di::DisplayItem>(&mut self.payload.cache_data);
+ index
+ }
+
+ /// Print the display items in the list to stdout.
+ pub fn dump_serialized_display_list(&mut self) {
+ self.serialized_content_buffer = Some(String::new());
+ }
+
+ fn add_to_display_list_dump<T: std::fmt::Debug>(&mut self, item: T) {
+ if let Some(ref mut content) = self.serialized_content_buffer {
+ use std::fmt::Write;
+ write!(content, "{:?}\n", item).expect("DL dump write failed.");
+ }
+ }
+
+ /// Returns the default section that DisplayListBuilder will write to,
+ /// if no section is specified explicitly.
+ fn default_section(&self) -> DisplayListSection {
+ if self.writing_to_chunk {
+ DisplayListSection::Chunk
+ } else {
+ DisplayListSection::Data
+ }
+ }
+
+ fn buffer_from_section(
+ &mut self,
+ section: DisplayListSection
+ ) -> &mut Vec<u8> {
+ match section {
+ DisplayListSection::Data => &mut self.payload.items_data,
+ DisplayListSection::CacheData => &mut self.payload.cache_data,
+ DisplayListSection::Chunk => &mut self.pending_chunk,
+ }
+ }
+
+ #[inline]
+ pub fn push_item_to_section(
+ &mut self,
+ item: &di::DisplayItem,
+ section: DisplayListSection,
+ ) {
+ debug_assert_eq!(self.state, BuildState::Build);
+ poke_into_vec(item, self.buffer_from_section(section));
+ self.add_to_display_list_dump(item);
+ }
+
+ /// Add an item to the display list.
+ ///
+ /// NOTE: It is usually preferable to use the specialized methods to push
+ /// display items. Pushing unexpected or invalid items here may
+ /// result in WebRender panicking or behaving in unexpected ways.
+ #[inline]
+ pub fn push_item(&mut self, item: &di::DisplayItem) {
+ self.push_item_to_section(item, self.default_section());
+ }
+
+ #[inline]
+ pub fn push_spatial_tree_item(&mut self, item: &di::SpatialTreeItem) {
+ debug_assert_eq!(self.state, BuildState::Build);
+ poke_into_vec(item, &mut self.payload.spatial_tree);
+ }
+
+ fn push_iter_impl<I>(data: &mut Vec<u8>, iter_source: I)
+ where
+ I: IntoIterator,
+ I::IntoIter: ExactSizeIterator,
+ I::Item: Poke,
+ {
+ let iter = iter_source.into_iter();
+ let len = iter.len();
+ // Format:
+ // payload_byte_size: usize, item_count: usize, [I; item_count]
+
+ // Track the the location of where to write byte size with offsets
+ // instead of pointers because data may be moved in memory during
+ // `serialize_iter_fast`.
+ let byte_size_offset = data.len();
+
+ // We write a dummy value so there's room for later
+ poke_into_vec(&0usize, data);
+ poke_into_vec(&len, data);
+ let count = poke_extend_vec(iter, data);
+ debug_assert_eq!(len, count, "iterator.len() returned two different values");
+
+ // Add red zone
+ ensure_red_zone::<I::Item>(data);
+
+ // Now write the actual byte_size
+ let final_offset = data.len();
+ debug_assert!(final_offset >= (byte_size_offset + mem::size_of::<usize>()),
+ "space was never allocated for this array's byte_size");
+ let byte_size = final_offset - byte_size_offset - mem::size_of::<usize>();
+ poke_inplace_slice(&byte_size, &mut data[byte_size_offset..]);
+ }
+
+ /// Push items from an iterator to the display list.
+ ///
+ /// NOTE: Pushing unexpected or invalid items to the display list
+ /// may result in panic and confusion.
+ pub fn push_iter<I>(&mut self, iter: I)
+ where
+ I: IntoIterator,
+ I::IntoIter: ExactSizeIterator,
+ I::Item: Poke,
+ {
+ assert_eq!(self.state, BuildState::Build);
+
+ let mut buffer = self.buffer_from_section(self.default_section());
+ Self::push_iter_impl(&mut buffer, iter);
+ }
+
+ pub fn push_rect(
+ &mut self,
+ common: &di::CommonItemProperties,
+ bounds: LayoutRect,
+ color: ColorF,
+ ) {
+ let item = di::DisplayItem::Rectangle(di::RectangleDisplayItem {
+ common: *common,
+ color: PropertyBinding::Value(color),
+ bounds,
+ });
+ self.push_item(&item);
+ }
+
+ pub fn push_rect_with_animation(
+ &mut self,
+ common: &di::CommonItemProperties,
+ bounds: LayoutRect,
+ color: PropertyBinding<ColorF>,
+ ) {
+ let item = di::DisplayItem::Rectangle(di::RectangleDisplayItem {
+ common: *common,
+ color,
+ bounds,
+ });
+ self.push_item(&item);
+ }
+
+ pub fn push_clear_rect(
+ &mut self,
+ common: &di::CommonItemProperties,
+ bounds: LayoutRect,
+ ) {
+ let item = di::DisplayItem::ClearRectangle(di::ClearRectangleDisplayItem {
+ common: *common,
+ bounds,
+ });
+ self.push_item(&item);
+ }
+
+ pub fn push_hit_test(
+ &mut self,
+ rect: LayoutRect,
+ clip_chain_id: di::ClipChainId,
+ spatial_id: di::SpatialId,
+ flags: di::PrimitiveFlags,
+ tag: di::ItemTag,
+ ) {
+ let item = di::DisplayItem::HitTest(di::HitTestDisplayItem {
+ rect,
+ clip_chain_id,
+ spatial_id,
+ flags,
+ tag,
+ });
+ self.push_item(&item);
+ }
+
+ pub fn push_line(
+ &mut self,
+ common: &di::CommonItemProperties,
+ area: &LayoutRect,
+ wavy_line_thickness: f32,
+ orientation: di::LineOrientation,
+ color: &ColorF,
+ style: di::LineStyle,
+ ) {
+ let item = di::DisplayItem::Line(di::LineDisplayItem {
+ common: *common,
+ area: *area,
+ wavy_line_thickness,
+ orientation,
+ color: *color,
+ style,
+ });
+
+ self.push_item(&item);
+ }
+
+ pub fn push_image(
+ &mut self,
+ common: &di::CommonItemProperties,
+ bounds: LayoutRect,
+ image_rendering: di::ImageRendering,
+ alpha_type: di::AlphaType,
+ key: ImageKey,
+ color: ColorF,
+ ) {
+ let item = di::DisplayItem::Image(di::ImageDisplayItem {
+ common: *common,
+ bounds,
+ image_key: key,
+ image_rendering,
+ alpha_type,
+ color,
+ });
+
+ self.push_item(&item);
+ }
+
+ pub fn push_repeating_image(
+ &mut self,
+ common: &di::CommonItemProperties,
+ bounds: LayoutRect,
+ stretch_size: LayoutSize,
+ tile_spacing: LayoutSize,
+ image_rendering: di::ImageRendering,
+ alpha_type: di::AlphaType,
+ key: ImageKey,
+ color: ColorF,
+ ) {
+ let item = di::DisplayItem::RepeatingImage(di::RepeatingImageDisplayItem {
+ common: *common,
+ bounds,
+ image_key: key,
+ stretch_size,
+ tile_spacing,
+ image_rendering,
+ alpha_type,
+ color,
+ });
+
+ self.push_item(&item);
+ }
+
+ /// Push a yuv image. All planar data in yuv image should use the same buffer type.
+ pub fn push_yuv_image(
+ &mut self,
+ common: &di::CommonItemProperties,
+ bounds: LayoutRect,
+ yuv_data: di::YuvData,
+ color_depth: ColorDepth,
+ color_space: di::YuvColorSpace,
+ color_range: di::ColorRange,
+ image_rendering: di::ImageRendering,
+ ) {
+ let item = di::DisplayItem::YuvImage(di::YuvImageDisplayItem {
+ common: *common,
+ bounds,
+ yuv_data,
+ color_depth,
+ color_space,
+ color_range,
+ image_rendering,
+ });
+ self.push_item(&item);
+ }
+
+ pub fn push_text(
+ &mut self,
+ common: &di::CommonItemProperties,
+ bounds: LayoutRect,
+ glyphs: &[GlyphInstance],
+ font_key: FontInstanceKey,
+ color: ColorF,
+ glyph_options: Option<GlyphOptions>,
+ ) {
+ let item = di::DisplayItem::Text(di::TextDisplayItem {
+ common: *common,
+ bounds,
+ color,
+ font_key,
+ glyph_options,
+ });
+
+ for split_glyphs in glyphs.chunks(MAX_TEXT_RUN_LENGTH) {
+ self.push_item(&item);
+ self.push_iter(split_glyphs);
+ }
+ }
+
+ /// NOTE: gradients must be pushed in the order they're created
+ /// because create_gradient stores the stops in anticipation.
+ pub fn create_gradient(
+ &mut self,
+ start_point: LayoutPoint,
+ end_point: LayoutPoint,
+ stops: Vec<di::GradientStop>,
+ extend_mode: di::ExtendMode,
+ ) -> di::Gradient {
+ let mut builder = GradientBuilder::with_stops(stops);
+ let gradient = builder.gradient(start_point, end_point, extend_mode);
+ self.push_stops(builder.stops());
+ gradient
+ }
+
+ /// NOTE: gradients must be pushed in the order they're created
+ /// because create_gradient stores the stops in anticipation.
+ pub fn create_radial_gradient(
+ &mut self,
+ center: LayoutPoint,
+ radius: LayoutSize,
+ stops: Vec<di::GradientStop>,
+ extend_mode: di::ExtendMode,
+ ) -> di::RadialGradient {
+ let mut builder = GradientBuilder::with_stops(stops);
+ let gradient = builder.radial_gradient(center, radius, extend_mode);
+ self.push_stops(builder.stops());
+ gradient
+ }
+
+ /// NOTE: gradients must be pushed in the order they're created
+ /// because create_gradient stores the stops in anticipation.
+ pub fn create_conic_gradient(
+ &mut self,
+ center: LayoutPoint,
+ angle: f32,
+ stops: Vec<di::GradientStop>,
+ extend_mode: di::ExtendMode,
+ ) -> di::ConicGradient {
+ let mut builder = GradientBuilder::with_stops(stops);
+ let gradient = builder.conic_gradient(center, angle, extend_mode);
+ self.push_stops(builder.stops());
+ gradient
+ }
+
+ pub fn push_border(
+ &mut self,
+ common: &di::CommonItemProperties,
+ bounds: LayoutRect,
+ widths: LayoutSideOffsets,
+ details: di::BorderDetails,
+ ) {
+ let item = di::DisplayItem::Border(di::BorderDisplayItem {
+ common: *common,
+ bounds,
+ details,
+ widths,
+ });
+
+ self.push_item(&item);
+ }
+
+ pub fn push_box_shadow(
+ &mut self,
+ common: &di::CommonItemProperties,
+ box_bounds: LayoutRect,
+ offset: LayoutVector2D,
+ color: ColorF,
+ blur_radius: f32,
+ spread_radius: f32,
+ border_radius: di::BorderRadius,
+ clip_mode: di::BoxShadowClipMode,
+ ) {
+ let item = di::DisplayItem::BoxShadow(di::BoxShadowDisplayItem {
+ common: *common,
+ box_bounds,
+ offset,
+ color,
+ blur_radius,
+ spread_radius,
+ border_radius,
+ clip_mode,
+ });
+
+ self.push_item(&item);
+ }
+
+ /// Pushes a linear gradient to be displayed.
+ ///
+ /// The gradient itself is described in the
+ /// `gradient` parameter. It is drawn on
+ /// a "tile" with the dimensions from `tile_size`.
+ /// These tiles are now repeated to the right and
+ /// to the bottom infinitely. If `tile_spacing`
+ /// is not zero spacers with the given dimensions
+ /// are inserted between the tiles as seams.
+ ///
+ /// The origin of the tiles is given in `layout.rect.origin`.
+ /// If the gradient should only be displayed once limit
+ /// the `layout.rect.size` to a single tile.
+ /// The gradient is only visible within the local clip.
+ pub fn push_gradient(
+ &mut self,
+ common: &di::CommonItemProperties,
+ bounds: LayoutRect,
+ gradient: di::Gradient,
+ tile_size: LayoutSize,
+ tile_spacing: LayoutSize,
+ ) {
+ let item = di::DisplayItem::Gradient(di::GradientDisplayItem {
+ common: *common,
+ bounds,
+ gradient,
+ tile_size,
+ tile_spacing,
+ });
+
+ self.push_item(&item);
+ }
+
+ /// Pushes a radial gradient to be displayed.
+ ///
+ /// See [`push_gradient`](#method.push_gradient) for explanation.
+ pub fn push_radial_gradient(
+ &mut self,
+ common: &di::CommonItemProperties,
+ bounds: LayoutRect,
+ gradient: di::RadialGradient,
+ tile_size: LayoutSize,
+ tile_spacing: LayoutSize,
+ ) {
+ let item = di::DisplayItem::RadialGradient(di::RadialGradientDisplayItem {
+ common: *common,
+ bounds,
+ gradient,
+ tile_size,
+ tile_spacing,
+ });
+
+ self.push_item(&item);
+ }
+
+ /// Pushes a conic gradient to be displayed.
+ ///
+ /// See [`push_gradient`](#method.push_gradient) for explanation.
+ pub fn push_conic_gradient(
+ &mut self,
+ common: &di::CommonItemProperties,
+ bounds: LayoutRect,
+ gradient: di::ConicGradient,
+ tile_size: LayoutSize,
+ tile_spacing: LayoutSize,
+ ) {
+ let item = di::DisplayItem::ConicGradient(di::ConicGradientDisplayItem {
+ common: *common,
+ bounds,
+ gradient,
+ tile_size,
+ tile_spacing,
+ });
+
+ self.push_item(&item);
+ }
+
+ pub fn push_reference_frame(
+ &mut self,
+ origin: LayoutPoint,
+ parent_spatial_id: di::SpatialId,
+ transform_style: di::TransformStyle,
+ transform: PropertyBinding<LayoutTransform>,
+ kind: di::ReferenceFrameKind,
+ key: di::SpatialTreeItemKey,
+ ) -> di::SpatialId {
+ let id = self.generate_spatial_index();
+
+ let current_offset = self.current_offset(parent_spatial_id);
+ let origin = origin + current_offset;
+
+ self.add_spatial_node_info(
+ id,
+ LayoutVector2D::zero(),
+ );
+
+ let descriptor = di::SpatialTreeItem::ReferenceFrame(di::ReferenceFrameDescriptor {
+ parent_spatial_id,
+ origin,
+ reference_frame: di::ReferenceFrame {
+ transform_style,
+ transform: di::ReferenceTransformBinding::Static {
+ binding: transform,
+ },
+ kind,
+ id,
+ key,
+ },
+ });
+ self.push_spatial_tree_item(&descriptor);
+
+ self.rf_mapper.push_scope();
+
+ let item = di::DisplayItem::PushReferenceFrame(di::ReferenceFrameDisplayListItem {
+ });
+ self.push_item(&item);
+
+ id
+ }
+
+ pub fn push_computed_frame(
+ &mut self,
+ origin: LayoutPoint,
+ parent_spatial_id: di::SpatialId,
+ scale_from: Option<LayoutSize>,
+ vertical_flip: bool,
+ rotation: di::Rotation,
+ key: di::SpatialTreeItemKey,
+ ) -> di::SpatialId {
+ let id = self.generate_spatial_index();
+
+ let current_offset = self.current_offset(parent_spatial_id);
+ let origin = origin + current_offset;
+
+ let descriptor = di::SpatialTreeItem::ReferenceFrame(di::ReferenceFrameDescriptor {
+ parent_spatial_id,
+ origin,
+ reference_frame: di::ReferenceFrame {
+ transform_style: di::TransformStyle::Flat,
+ transform: di::ReferenceTransformBinding::Computed {
+ scale_from,
+ vertical_flip,
+ rotation,
+ },
+ kind: di::ReferenceFrameKind::Transform {
+ is_2d_scale_translation: false,
+ should_snap: false,
+ paired_with_perspective: false,
+ },
+ id,
+ key,
+ },
+ });
+ self.push_spatial_tree_item(&descriptor);
+
+ self.rf_mapper.push_scope();
+
+ let item = di::DisplayItem::PushReferenceFrame(di::ReferenceFrameDisplayListItem {
+ });
+ self.push_item(&item);
+
+ id
+ }
+
+ pub fn pop_reference_frame(&mut self) {
+ self.rf_mapper.pop_scope();
+ self.push_item(&di::DisplayItem::PopReferenceFrame);
+ }
+
+ pub fn push_stacking_context(
+ &mut self,
+ origin: LayoutPoint,
+ spatial_id: di::SpatialId,
+ prim_flags: di::PrimitiveFlags,
+ clip_chain_id: Option<di::ClipChainId>,
+ transform_style: di::TransformStyle,
+ mix_blend_mode: di::MixBlendMode,
+ filters: &[di::FilterOp],
+ filter_datas: &[di::FilterData],
+ filter_primitives: &[di::FilterPrimitive],
+ raster_space: di::RasterSpace,
+ flags: di::StackingContextFlags,
+ ) {
+ self.push_filters(filters, filter_datas, filter_primitives);
+
+ let item = di::DisplayItem::PushStackingContext(di::PushStackingContextDisplayItem {
+ origin,
+ spatial_id,
+ prim_flags,
+ stacking_context: di::StackingContext {
+ transform_style,
+ mix_blend_mode,
+ clip_chain_id,
+ raster_space,
+ flags,
+ },
+ });
+
+ self.rf_mapper.push_offset(origin.to_vector());
+ self.push_item(&item);
+ }
+
+ /// Helper for examples/ code.
+ pub fn push_simple_stacking_context(
+ &mut self,
+ origin: LayoutPoint,
+ spatial_id: di::SpatialId,
+ prim_flags: di::PrimitiveFlags,
+ ) {
+ self.push_simple_stacking_context_with_filters(
+ origin,
+ spatial_id,
+ prim_flags,
+ &[],
+ &[],
+ &[],
+ );
+ }
+
+ /// Helper for examples/ code.
+ pub fn push_simple_stacking_context_with_filters(
+ &mut self,
+ origin: LayoutPoint,
+ spatial_id: di::SpatialId,
+ prim_flags: di::PrimitiveFlags,
+ filters: &[di::FilterOp],
+ filter_datas: &[di::FilterData],
+ filter_primitives: &[di::FilterPrimitive],
+ ) {
+ self.push_stacking_context(
+ origin,
+ spatial_id,
+ prim_flags,
+ None,
+ di::TransformStyle::Flat,
+ di::MixBlendMode::Normal,
+ filters,
+ filter_datas,
+ filter_primitives,
+ di::RasterSpace::Screen,
+ di::StackingContextFlags::empty(),
+ );
+ }
+
+ pub fn pop_stacking_context(&mut self) {
+ self.rf_mapper.pop_offset();
+ self.push_item(&di::DisplayItem::PopStackingContext);
+ }
+
+ pub fn push_stops(&mut self, stops: &[di::GradientStop]) {
+ if stops.is_empty() {
+ return;
+ }
+ self.push_item(&di::DisplayItem::SetGradientStops);
+ self.push_iter(stops);
+ }
+
+ pub fn push_backdrop_filter(
+ &mut self,
+ common: &di::CommonItemProperties,
+ filters: &[di::FilterOp],
+ filter_datas: &[di::FilterData],
+ filter_primitives: &[di::FilterPrimitive],
+ ) {
+ self.push_filters(filters, filter_datas, filter_primitives);
+
+ let item = di::DisplayItem::BackdropFilter(di::BackdropFilterDisplayItem {
+ common: *common,
+ });
+ self.push_item(&item);
+ }
+
+ pub fn push_filters(
+ &mut self,
+ filters: &[di::FilterOp],
+ filter_datas: &[di::FilterData],
+ filter_primitives: &[di::FilterPrimitive],
+ ) {
+ if !filters.is_empty() {
+ self.push_item(&di::DisplayItem::SetFilterOps);
+ self.push_iter(filters);
+ }
+
+ for filter_data in filter_datas {
+ let func_types = [
+ filter_data.func_r_type, filter_data.func_g_type,
+ filter_data.func_b_type, filter_data.func_a_type];
+ self.push_item(&di::DisplayItem::SetFilterData);
+ self.push_iter(&func_types);
+ self.push_iter(&filter_data.r_values);
+ self.push_iter(&filter_data.g_values);
+ self.push_iter(&filter_data.b_values);
+ self.push_iter(&filter_data.a_values);
+ }
+
+ if !filter_primitives.is_empty() {
+ self.push_item(&di::DisplayItem::SetFilterPrimitives);
+ self.push_iter(filter_primitives);
+ }
+ }
+
+ fn generate_clip_index(&mut self) -> di::ClipId {
+ self.next_clip_index += 1;
+ di::ClipId(self.next_clip_index - 1, self.pipeline_id)
+ }
+
+ fn generate_spatial_index(&mut self) -> di::SpatialId {
+ self.next_spatial_index += 1;
+ di::SpatialId::new(self.next_spatial_index - 1, self.pipeline_id)
+ }
+
+ fn generate_clip_chain_id(&mut self) -> di::ClipChainId {
+ self.next_clip_chain_id += 1;
+ di::ClipChainId(self.next_clip_chain_id - 1, self.pipeline_id)
+ }
+
+ pub fn define_scroll_frame(
+ &mut self,
+ parent_space: di::SpatialId,
+ external_id: di::ExternalScrollId,
+ content_rect: LayoutRect,
+ frame_rect: LayoutRect,
+ external_scroll_offset: LayoutVector2D,
+ scroll_offset_generation: APZScrollGeneration,
+ has_scroll_linked_effect: HasScrollLinkedEffect,
+ key: di::SpatialTreeItemKey,
+ ) -> di::SpatialId {
+ let scroll_frame_id = self.generate_spatial_index();
+ let current_offset = self.current_offset(parent_space);
+
+ let parent = self.spatial_nodes[parent_space.0].clone();
+
+ self.add_spatial_node_info(
+ scroll_frame_id,
+ parent.accumulated_external_scroll_offset + external_scroll_offset,
+ );
+
+ let descriptor = di::SpatialTreeItem::ScrollFrame(di::ScrollFrameDescriptor {
+ content_rect,
+ frame_rect: frame_rect.translate(current_offset),
+ parent_space,
+ scroll_frame_id,
+ external_id,
+ external_scroll_offset,
+ scroll_offset_generation,
+ has_scroll_linked_effect,
+ key,
+ });
+
+ self.push_spatial_tree_item(&descriptor);
+
+ scroll_frame_id
+ }
+
+ pub fn define_clip_chain<I>(
+ &mut self,
+ parent: Option<di::ClipChainId>,
+ clips: I,
+ ) -> di::ClipChainId
+ where
+ I: IntoIterator<Item = di::ClipId>,
+ I::IntoIter: ExactSizeIterator + Clone,
+ {
+ let id = self.generate_clip_chain_id();
+ self.push_item(&di::DisplayItem::ClipChain(di::ClipChainItem { id, parent }));
+ self.push_iter(clips);
+ id
+ }
+
+ pub fn define_clip_image_mask(
+ &mut self,
+ spatial_id: di::SpatialId,
+ image_mask: di::ImageMask,
+ points: &[LayoutPoint],
+ fill_rule: di::FillRule,
+ ) -> di::ClipId {
+ let id = self.generate_clip_index();
+
+ let current_offset = self.current_offset(spatial_id);
+
+ let image_mask = di::ImageMask {
+ rect: image_mask.rect.translate(current_offset),
+ ..image_mask
+ };
+
+ let item = di::DisplayItem::ImageMaskClip(di::ImageMaskClipDisplayItem {
+ id,
+ spatial_id,
+ image_mask,
+ fill_rule,
+ });
+
+ // We only need to supply points if there are at least 3, which is the
+ // minimum to specify a polygon. BuiltDisplayListIter.next ensures that points
+ // are cleared between processing other display items, so we'll correctly get
+ // zero points when no SetPoints item has been pushed.
+ if points.len() >= 3 {
+ self.push_item(&di::DisplayItem::SetPoints);
+ self.push_iter(points);
+ }
+ self.push_item(&item);
+ id
+ }
+
+ pub fn define_clip_rect(
+ &mut self,
+ spatial_id: di::SpatialId,
+ clip_rect: LayoutRect,
+ ) -> di::ClipId {
+ let id = self.generate_clip_index();
+
+ let current_offset = self.current_offset(spatial_id);
+ let clip_rect = clip_rect.translate(current_offset);
+
+ let item = di::DisplayItem::RectClip(di::RectClipDisplayItem {
+ id,
+ spatial_id,
+ clip_rect,
+ });
+
+ self.push_item(&item);
+ id
+ }
+
+ pub fn define_clip_rounded_rect(
+ &mut self,
+ spatial_id: di::SpatialId,
+ clip: di::ComplexClipRegion,
+ ) -> di::ClipId {
+ let id = self.generate_clip_index();
+
+ let current_offset = self.current_offset(spatial_id);
+
+ let clip = di::ComplexClipRegion {
+ rect: clip.rect.translate(current_offset),
+ ..clip
+ };
+
+ let item = di::DisplayItem::RoundedRectClip(di::RoundedRectClipDisplayItem {
+ id,
+ spatial_id,
+ clip,
+ });
+
+ self.push_item(&item);
+ id
+ }
+
+ pub fn define_sticky_frame(
+ &mut self,
+ parent_spatial_id: di::SpatialId,
+ frame_rect: LayoutRect,
+ margins: SideOffsets2D<Option<f32>, LayoutPixel>,
+ vertical_offset_bounds: di::StickyOffsetBounds,
+ horizontal_offset_bounds: di::StickyOffsetBounds,
+ previously_applied_offset: LayoutVector2D,
+ key: di::SpatialTreeItemKey,
+ ) -> di::SpatialId {
+ let id = self.generate_spatial_index();
+ let current_offset = self.current_offset(parent_spatial_id);
+ let parent = self.spatial_nodes[parent_spatial_id.0].clone();
+
+ self.add_spatial_node_info(
+ id,
+ parent.accumulated_external_scroll_offset,
+ );
+
+ let descriptor = di::SpatialTreeItem::StickyFrame(di::StickyFrameDescriptor {
+ parent_spatial_id,
+ id,
+ bounds: frame_rect.translate(current_offset),
+ margins,
+ vertical_offset_bounds,
+ horizontal_offset_bounds,
+ previously_applied_offset,
+ key,
+ });
+
+ self.push_spatial_tree_item(&descriptor);
+ id
+ }
+
+ pub fn push_iframe(
+ &mut self,
+ bounds: LayoutRect,
+ clip_rect: LayoutRect,
+ space_and_clip: &di::SpaceAndClipInfo,
+ pipeline_id: PipelineId,
+ ignore_missing_pipeline: bool
+ ) {
+ let current_offset = self.current_offset(space_and_clip.spatial_id);
+ let bounds = bounds.translate(current_offset);
+ let clip_rect = clip_rect.translate(current_offset);
+
+ let item = di::DisplayItem::Iframe(di::IframeDisplayItem {
+ bounds,
+ clip_rect,
+ space_and_clip: *space_and_clip,
+ pipeline_id,
+ ignore_missing_pipeline,
+ });
+ self.push_item(&item);
+ }
+
+ pub fn push_shadow(
+ &mut self,
+ space_and_clip: &di::SpaceAndClipInfo,
+ shadow: di::Shadow,
+ should_inflate: bool,
+ ) {
+ let item = di::DisplayItem::PushShadow(di::PushShadowDisplayItem {
+ space_and_clip: *space_and_clip,
+ shadow,
+ should_inflate,
+ });
+ self.push_item(&item);
+ }
+
+ pub fn pop_all_shadows(&mut self) {
+ self.push_item(&di::DisplayItem::PopAllShadows);
+ }
+
+ pub fn start_item_group(&mut self) {
+ debug_assert!(!self.writing_to_chunk);
+ debug_assert!(self.pending_chunk.is_empty());
+
+ self.writing_to_chunk = true;
+ }
+
+ fn flush_pending_item_group(&mut self, key: di::ItemKey) {
+ // Push RetainedItems-marker to cache_data section.
+ self.push_retained_items(key);
+
+ // Push pending chunk to cache_data section.
+ self.payload.cache_data.append(&mut self.pending_chunk);
+
+ // Push ReuseItems-marker to data section.
+ self.push_reuse_items(key);
+ }
+
+ pub fn finish_item_group(&mut self, key: di::ItemKey) -> bool {
+ debug_assert!(self.writing_to_chunk);
+ self.writing_to_chunk = false;
+
+ if self.pending_chunk.is_empty() {
+ return false;
+ }
+
+ self.flush_pending_item_group(key);
+ true
+ }
+
+ pub fn cancel_item_group(&mut self, discard: bool) {
+ debug_assert!(self.writing_to_chunk);
+ self.writing_to_chunk = false;
+
+ if discard {
+ self.pending_chunk.clear();
+ } else {
+ // Push pending chunk to data section.
+ self.payload.items_data.append(&mut self.pending_chunk);
+ }
+ }
+
+ pub fn push_reuse_items(&mut self, key: di::ItemKey) {
+ self.push_item_to_section(
+ &di::DisplayItem::ReuseItems(key),
+ DisplayListSection::Data
+ );
+ }
+
+ fn push_retained_items(&mut self, key: di::ItemKey) {
+ self.push_item_to_section(
+ &di::DisplayItem::RetainedItems(key),
+ DisplayListSection::CacheData
+ );
+ }
+
+ pub fn set_cache_size(&mut self, cache_size: usize) {
+ self.cache_size = cache_size;
+ }
+
+ pub fn begin(&mut self) {
+ assert_eq!(self.state, BuildState::Idle);
+ self.state = BuildState::Build;
+ self.builder_start_time = precise_time_ns();
+ self.reset();
+ }
+
+ pub fn end(&mut self) -> (PipelineId, BuiltDisplayList) {
+ assert_eq!(self.state, BuildState::Build);
+ assert!(self.save_state.is_none(), "Finalized DisplayListBuilder with a pending save");
+
+ if let Some(content) = self.serialized_content_buffer.take() {
+ println!("-- WebRender display list for {:?} --\n{}",
+ self.pipeline_id, content);
+ }
+
+ // Add `DisplayItem::max_size` zone of zeroes to the end of display list
+ // so there is at least this amount available in the display list during
+ // serialization.
+ ensure_red_zone::<di::DisplayItem>(&mut self.payload.items_data);
+ ensure_red_zone::<di::DisplayItem>(&mut self.payload.cache_data);
+ ensure_red_zone::<di::SpatialTreeItem>(&mut self.payload.spatial_tree);
+
+ // While the first display list after tab-switch can be large, the
+ // following ones are always smaller thanks to interning. We attempt
+ // to reserve the same capacity again, although it may fail. Memory
+ // pressure events will cause us to release our buffers if we ask for
+ // too much. See bug 1531819 for related OOM issues.
+ let next_capacity = DisplayListCapacity {
+ cache_size: self.payload.cache_data.len(),
+ items_size: self.payload.items_data.len(),
+ spatial_tree_size: self.payload.spatial_tree.len(),
+ };
+ let payload = mem::replace(
+ &mut self.payload,
+ DisplayListPayload::new(next_capacity),
+ );
+ let end_time = precise_time_ns();
+
+ self.state = BuildState::Idle;
+
+ (
+ self.pipeline_id,
+ BuiltDisplayList {
+ descriptor: BuiltDisplayListDescriptor {
+ gecko_display_list_type: GeckoDisplayListType::None,
+ builder_start_time: self.builder_start_time,
+ builder_finish_time: end_time,
+ send_start_time: end_time,
+ total_clip_nodes: self.next_clip_index,
+ total_spatial_nodes: self.next_spatial_index,
+ cache_size: self.cache_size,
+ },
+ payload,
+ },
+ )
+ }
+
+ /// Retrieve the current offset to allow converting a stacking context
+ /// relative coordinate to be relative to the owing reference frame,
+ /// also considering any external scroll offset on the provided
+ /// spatial node.
+ fn current_offset(
+ &mut self,
+ spatial_id: di::SpatialId,
+ ) -> LayoutVector2D {
+ // Get the current offset from stacking context <-> reference frame space.
+ let rf_offset = self.rf_mapper.current_offset();
+
+ // Get the external scroll offset, if applicable.
+ let scroll_offset = self.spatial_nodes[spatial_id.0].accumulated_external_scroll_offset;
+
+ rf_offset + scroll_offset
+ }
+
+ /// Add info about a spatial node that is needed during DL building.
+ fn add_spatial_node_info(
+ &mut self,
+ id: di::SpatialId,
+ accumulated_external_scroll_offset: LayoutVector2D,
+ ) {
+ self.spatial_nodes.resize(id.0 + 1, SpatialNodeInfo::identity());
+
+ let info = &mut self.spatial_nodes[id.0];
+ info.accumulated_external_scroll_offset = accumulated_external_scroll_offset;
+ }
+}
+
+fn iter_spatial_tree<F>(spatial_tree: &[u8], mut f: F) where F: FnMut(&di::SpatialTreeItem) {
+ let mut src = spatial_tree;
+ let mut item = di::SpatialTreeItem::Invalid;
+
+ while src.len() > di::SpatialTreeItem::max_size() {
+ src = peek_from_slice(src, &mut item);
+ f(&item);
+ }
+}
+
+/// The offset stack for a given reference frame.
+#[derive(Clone)]
+struct ReferenceFrameState {
+ /// A stack of current offsets from the current reference frame scope.
+ offsets: Vec<LayoutVector2D>,
+}
+
+/// Maps from stacking context layout coordinates into reference frame
+/// relative coordinates.
+#[derive(Clone)]
+pub struct ReferenceFrameMapper {
+ /// A stack of reference frame scopes.
+ frames: Vec<ReferenceFrameState>,
+}
+
+impl ReferenceFrameMapper {
+ pub fn new() -> Self {
+ ReferenceFrameMapper {
+ frames: vec![
+ ReferenceFrameState {
+ offsets: vec![
+ LayoutVector2D::zero(),
+ ],
+ }
+ ],
+ }
+ }
+
+ /// Push a new scope. This resets the current offset to zero, and is
+ /// used when a new reference frame or iframe is pushed.
+ pub fn push_scope(&mut self) {
+ self.frames.push(ReferenceFrameState {
+ offsets: vec![
+ LayoutVector2D::zero(),
+ ],
+ });
+ }
+
+ /// Pop a reference frame scope off the stack.
+ pub fn pop_scope(&mut self) {
+ self.frames.pop().unwrap();
+ }
+
+ /// Push a new offset for the current scope. This is used when
+ /// a new stacking context is pushed.
+ pub fn push_offset(&mut self, offset: LayoutVector2D) {
+ let frame = self.frames.last_mut().unwrap();
+ let current_offset = *frame.offsets.last().unwrap();
+ frame.offsets.push(current_offset + offset);
+ }
+
+ /// Pop a local stacking context offset from the current scope.
+ pub fn pop_offset(&mut self) {
+ let frame = self.frames.last_mut().unwrap();
+ frame.offsets.pop().unwrap();
+ }
+
+ /// Retrieve the current offset to allow converting a stacking context
+ /// relative coordinate to be relative to the owing reference frame.
+ /// TODO(gw): We could perhaps have separate coordinate spaces for this,
+ /// however that's going to either mean a lot of changes to
+ /// public API code, or a lot of changes to internal code.
+ /// Before doing that, we should revisit how Gecko would
+ /// prefer to provide coordinates.
+ /// TODO(gw): For now, this includes only the reference frame relative
+ /// offset. Soon, we will expand this to include the initial
+ /// scroll offsets that are now available on scroll nodes. This
+ /// will allow normalizing the coordinates even between display
+ /// lists where APZ has scrolled the content.
+ pub fn current_offset(&self) -> LayoutVector2D {
+ *self.frames.last().unwrap().offsets.last().unwrap()
+ }
+}
diff --git a/gfx/wr/webrender_api/src/font.rs b/gfx/wr/webrender_api/src/font.rs
new file mode 100644
index 0000000000..286742bff6
--- /dev/null
+++ b/gfx/wr/webrender_api/src/font.rs
@@ -0,0 +1,435 @@
+/* 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 peek_poke::PeekPoke;
+use std::cmp::Ordering;
+use std::hash::{Hash, Hasher};
+#[cfg(not(target_os = "macos"))]
+use std::path::PathBuf;
+use std::sync::Arc;
+// local imports
+use crate::IdNamespace;
+use crate::channel::Sender;
+use crate::color::ColorU;
+use crate::units::LayoutPoint;
+
+/// Hashable floating-point storage for font size.
+#[repr(C)]
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, Deserialize, Serialize)]
+pub struct FontSize(pub f32);
+
+impl Ord for FontSize {
+ fn cmp(&self, other: &FontSize) -> Ordering {
+ self.partial_cmp(other).unwrap_or(Ordering::Equal)
+ }
+}
+
+impl Eq for FontSize {}
+
+impl Hash for FontSize {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.0.to_bits().hash(state);
+ }
+}
+
+impl From<f32> for FontSize {
+ fn from(size: f32) -> Self { FontSize(size) }
+}
+
+impl From<FontSize> for f32 {
+ fn from(size: FontSize) -> Self { size.0 }
+}
+
+impl FontSize {
+ pub fn zero() -> Self { FontSize(0.0) }
+
+ pub fn from_f32_px(size: f32) -> Self { FontSize(size) }
+
+ pub fn to_f32_px(&self) -> f32 { self.0 }
+
+ pub fn from_f64_px(size: f64) -> Self { FontSize(size as f32) }
+
+ pub fn to_f64_px(&self) -> f64 { self.0 as f64 }
+}
+
+#[cfg(not(target_os = "macos"))]
+#[derive(Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
+pub struct NativeFontHandle {
+ pub path: PathBuf,
+ pub index: u32,
+}
+
+#[cfg(target_os = "macos")]
+#[derive(Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
+pub struct NativeFontHandle {
+ pub name: String,
+}
+
+#[repr(C)]
+#[derive(Copy, Clone, Deserialize, Serialize, Debug)]
+pub struct GlyphDimensions {
+ pub left: i32,
+ pub top: i32,
+ pub width: i32,
+ pub height: i32,
+ pub advance: f32,
+}
+
+pub struct GlyphDimensionRequest {
+ pub key: FontInstanceKey,
+ pub glyph_indices: Vec<GlyphIndex>,
+ pub sender: Sender<Vec<Option<GlyphDimensions>>>,
+}
+
+pub struct GlyphIndexRequest {
+ pub key: FontKey,
+ pub text: String,
+ pub sender: Sender<Vec<Option<u32>>>,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize, Ord, PartialOrd)]
+pub struct FontKey(pub IdNamespace, pub u32);
+
+impl FontKey {
+ pub fn new(namespace: IdNamespace, key: u32) -> FontKey {
+ FontKey(namespace, key)
+ }
+}
+
+/// Container for the raw data describing a font. This might be a stream of
+/// bytes corresponding to a downloaded font, or a handle to a native font from
+/// the operating system.
+///
+/// Note that fonts need to be instantiated before being used, which involves
+/// assigning size and various other options. The word 'template' here is
+/// intended to distinguish this data from instance-specific data.
+#[derive(Debug, Clone, Hash, Eq, PartialEq)]
+pub enum FontTemplate {
+ Raw(Arc<Vec<u8>>, u32),
+ Native(NativeFontHandle),
+}
+
+#[repr(u8)]
+#[derive(Debug, Copy, Clone, Hash, Eq, MallocSizeOf, PartialEq, Serialize, Deserialize, Ord, PartialOrd, PeekPoke)]
+pub enum FontRenderMode {
+ Mono = 0,
+ Alpha,
+ Subpixel,
+}
+
+impl Default for FontRenderMode {
+ fn default() -> Self {
+ FontRenderMode::Mono
+ }
+}
+
+impl FontRenderMode {
+ // Combine two font render modes such that the lesser amount of AA limits the AA of the result.
+ pub fn limit_by(self, other: FontRenderMode) -> FontRenderMode {
+ match (self, other) {
+ (FontRenderMode::Subpixel, _) | (_, FontRenderMode::Mono) => other,
+ _ => self,
+ }
+ }
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialOrd, Deserialize, Serialize)]
+pub struct FontVariation {
+ pub tag: u32,
+ pub value: f32,
+}
+
+impl Ord for FontVariation {
+ fn cmp(&self, other: &FontVariation) -> Ordering {
+ self.tag.cmp(&other.tag)
+ .then(self.value.to_bits().cmp(&other.value.to_bits()))
+ }
+}
+
+impl PartialEq for FontVariation {
+ fn eq(&self, other: &FontVariation) -> bool {
+ self.tag == other.tag &&
+ self.value.to_bits() == other.value.to_bits()
+ }
+}
+
+impl Eq for FontVariation {}
+
+impl Hash for FontVariation {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.tag.hash(state);
+ self.value.to_bits().hash(state);
+ }
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Deserialize, Hash, Eq, PartialEq, PartialOrd, Ord, Serialize, PeekPoke)]
+pub struct GlyphOptions {
+ pub render_mode: FontRenderMode,
+ pub flags: FontInstanceFlags,
+}
+
+impl Default for GlyphOptions {
+ fn default() -> Self {
+ GlyphOptions {
+ render_mode: FontRenderMode::Subpixel,
+ flags: FontInstanceFlags::empty(),
+ }
+ }
+}
+
+bitflags! {
+ #[repr(C)]
+ #[derive(Deserialize, MallocSizeOf, Serialize, PeekPoke)]
+ pub struct FontInstanceFlags: u32 {
+ // Common flags
+ // Use native synthetic bold, if supported.
+ const SYNTHETIC_BOLD = 1 << 1;
+ const EMBEDDED_BITMAPS = 1 << 2;
+ const SUBPIXEL_BGR = 1 << 3;
+ const TRANSPOSE = 1 << 4;
+ const FLIP_X = 1 << 5;
+ const FLIP_Y = 1 << 6;
+ const SUBPIXEL_POSITION = 1 << 7;
+ const VERTICAL = 1 << 8;
+ // Explicitly use multi-strike bold emulation.
+ const MULTISTRIKE_BOLD = 1 << 9;
+
+ // Internal flags
+ const TRANSFORM_GLYPHS = 1 << 12;
+ const TEXTURE_PADDING = 1 << 13;
+
+ // Windows flags
+ const FORCE_GDI = 1 << 16;
+ const FORCE_SYMMETRIC = 1 << 17;
+ const NO_SYMMETRIC = 1 << 18;
+
+ // Mac flags
+ const FONT_SMOOTHING = 1 << 16;
+
+ // FreeType flags
+ const FORCE_AUTOHINT = 1 << 16;
+ const NO_AUTOHINT = 1 << 17;
+ const VERTICAL_LAYOUT = 1 << 18;
+ const LCD_VERTICAL = 1 << 19;
+ }
+}
+
+impl Default for FontInstanceFlags {
+ #[cfg(target_os = "windows")]
+ fn default() -> FontInstanceFlags {
+ FontInstanceFlags::SUBPIXEL_POSITION
+ }
+
+ #[cfg(target_os = "macos")]
+ fn default() -> FontInstanceFlags {
+ FontInstanceFlags::SUBPIXEL_POSITION |
+ FontInstanceFlags::FONT_SMOOTHING
+ }
+
+ #[cfg(not(any(target_os = "macos", target_os = "windows")))]
+ fn default() -> FontInstanceFlags {
+ FontInstanceFlags::SUBPIXEL_POSITION
+ }
+}
+
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Deserialize, Hash, Eq, MallocSizeOf, PartialEq, PartialOrd, Ord, Serialize)]
+pub struct SyntheticItalics {
+ // Angle in degrees (-90..90) for synthetic italics in 8.8 fixed-point.
+ pub angle: i16,
+}
+
+impl SyntheticItalics {
+ pub const ANGLE_SCALE: f32 = 256.0;
+
+ pub fn from_degrees(degrees: f32) -> Self {
+ SyntheticItalics { angle: (degrees.max(-89.0).min(89.0) * Self::ANGLE_SCALE) as i16 }
+ }
+
+ pub fn to_degrees(self) -> f32 {
+ self.angle as f32 / Self::ANGLE_SCALE
+ }
+
+ pub fn to_radians(self) -> f32 {
+ self.to_degrees().to_radians()
+ }
+
+ pub fn to_skew(self) -> f32 {
+ self.to_radians().tan()
+ }
+
+ pub fn enabled() -> Self {
+ Self::from_degrees(14.0)
+ }
+
+ pub fn disabled() -> Self {
+ SyntheticItalics { angle: 0 }
+ }
+
+ pub fn is_enabled(self) -> bool {
+ self.angle != 0
+ }
+}
+
+impl Default for SyntheticItalics {
+ fn default() -> Self {
+ SyntheticItalics::disabled()
+ }
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Deserialize, Hash, Eq, MallocSizeOf, PartialEq, PartialOrd, Ord, Serialize)]
+pub struct FontInstanceOptions {
+ pub render_mode: FontRenderMode,
+ pub flags: FontInstanceFlags,
+ /// When bg_color.a is != 0 and render_mode is FontRenderMode::Subpixel,
+ /// the text will be rendered with bg_color.r/g/b as an opaque estimated
+ /// background color.
+ pub bg_color: ColorU,
+ pub synthetic_italics: SyntheticItalics,
+}
+
+impl Default for FontInstanceOptions {
+ fn default() -> FontInstanceOptions {
+ FontInstanceOptions {
+ render_mode: FontRenderMode::Subpixel,
+ flags: Default::default(),
+ bg_color: ColorU::new(0, 0, 0, 0),
+ synthetic_italics: SyntheticItalics::disabled(),
+ }
+ }
+}
+
+#[cfg(target_os = "windows")]
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Deserialize, Hash, Eq, MallocSizeOf, PartialEq, PartialOrd, Ord, Serialize)]
+pub struct FontInstancePlatformOptions {
+ pub gamma: u16, // percent
+ pub contrast: u8, // percent
+ pub cleartype_level: u8, // percent
+}
+
+#[cfg(target_os = "windows")]
+impl Default for FontInstancePlatformOptions {
+ fn default() -> FontInstancePlatformOptions {
+ FontInstancePlatformOptions {
+ gamma: 180, // Default DWrite gamma
+ contrast: 100,
+ cleartype_level: 100,
+ }
+ }
+}
+
+#[cfg(target_os = "macos")]
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Deserialize, Hash, Eq, MallocSizeOf, PartialEq, PartialOrd, Ord, Serialize)]
+pub struct FontInstancePlatformOptions {
+ pub unused: u32,
+}
+
+#[cfg(target_os = "macos")]
+impl Default for FontInstancePlatformOptions {
+ fn default() -> FontInstancePlatformOptions {
+ FontInstancePlatformOptions {
+ unused: 0,
+ }
+ }
+}
+
+#[cfg(not(any(target_os = "macos", target_os = "windows")))]
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, PartialOrd, Ord, Serialize)]
+pub enum FontLCDFilter {
+ None,
+ Default,
+ Light,
+ Legacy,
+}
+
+#[cfg(not(any(target_os = "macos", target_os = "windows")))]
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, PartialOrd, Ord, Serialize)]
+pub enum FontHinting {
+ None,
+ Mono,
+ Light,
+ Normal,
+ LCD,
+}
+
+#[cfg(not(any(target_os = "macos", target_os = "windows")))]
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Deserialize, Hash, Eq, MallocSizeOf, PartialEq, PartialOrd, Ord, Serialize)]
+pub struct FontInstancePlatformOptions {
+ pub lcd_filter: FontLCDFilter,
+ pub hinting: FontHinting,
+}
+
+#[cfg(not(any(target_os = "macos", target_os = "windows")))]
+impl Default for FontInstancePlatformOptions {
+ fn default() -> FontInstancePlatformOptions {
+ FontInstancePlatformOptions {
+ lcd_filter: FontLCDFilter::Default,
+ hinting: FontHinting::LCD,
+ }
+ }
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Ord, PartialOrd, MallocSizeOf, PeekPoke)]
+#[derive(Deserialize, Serialize)]
+pub struct FontInstanceKey(pub IdNamespace, pub u32);
+
+impl FontInstanceKey {
+ pub fn new(namespace: IdNamespace, key: u32) -> FontInstanceKey {
+ FontInstanceKey(namespace, key)
+ }
+}
+
+/// Data corresponding to an instantiation of a font, with size and
+/// other options specified.
+///
+/// Note that the actual font is stored out-of-band in `FontTemplate`.
+#[derive(Clone)]
+pub struct FontInstanceData {
+ pub font_key: FontKey,
+ pub size: f32,
+ pub options: Option<FontInstanceOptions>,
+ pub platform_options: Option<FontInstancePlatformOptions>,
+ pub variations: Vec<FontVariation>,
+}
+
+pub type GlyphIndex = u32;
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize, PeekPoke)]
+pub struct GlyphInstance {
+ pub index: GlyphIndex,
+ pub point: LayoutPoint,
+}
+
+impl Default for GlyphInstance {
+ fn default() -> Self {
+ GlyphInstance {
+ index: 0,
+ point: LayoutPoint::zero(),
+ }
+ }
+}
+
+impl Eq for GlyphInstance {}
+
+#[cfg_attr(feature = "cargo-clippy", allow(clippy::derive_hash_xor_eq))]
+impl Hash for GlyphInstance {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ // Note: this is inconsistent with the Eq impl for -0.0 (don't care).
+ self.index.hash(state);
+ self.point.x.to_bits().hash(state);
+ self.point.y.to_bits().hash(state);
+ }
+}
diff --git a/gfx/wr/webrender_api/src/gradient_builder.rs b/gfx/wr/webrender_api/src/gradient_builder.rs
new file mode 100644
index 0000000000..6347396f79
--- /dev/null
+++ b/gfx/wr/webrender_api/src/gradient_builder.rs
@@ -0,0 +1,180 @@
+/* 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::display_item as di;
+use crate::units::*;
+
+
+/// Construct a gradient to be used in display lists.
+///
+/// Each gradient needs at least two stops.
+pub struct GradientBuilder {
+ stops: Vec<di::GradientStop>,
+}
+
+impl GradientBuilder {
+ /// Create a new gradient builder.
+ pub fn new() -> Self {
+ GradientBuilder {
+ stops: Vec::new(),
+ }
+ }
+
+ /// Create a gradient builder with a list of stops.
+ pub fn with_stops(stops: Vec<di::GradientStop>) -> GradientBuilder {
+ GradientBuilder { stops }
+ }
+
+ /// Push an additional stop for the gradient.
+ pub fn push(&mut self, stop: di::GradientStop) {
+ self.stops.push(stop);
+ }
+
+ /// Get a reference to the list of stops.
+ pub fn stops(&self) -> &[di::GradientStop] {
+ self.stops.as_ref()
+ }
+
+ /// Return the gradient stops vector.
+ pub fn into_stops(self) -> Vec<di::GradientStop> {
+ self.stops
+ }
+
+ /// Produce a linear gradient, normalize the stops.
+ pub fn gradient(
+ &mut self,
+ start_point: LayoutPoint,
+ end_point: LayoutPoint,
+ extend_mode: di::ExtendMode,
+ ) -> di::Gradient {
+ let (start_offset, end_offset) = self.normalize(extend_mode);
+ let start_to_end = end_point - start_point;
+
+ di::Gradient {
+ start_point: start_point + start_to_end * start_offset,
+ end_point: start_point + start_to_end * end_offset,
+ extend_mode,
+ }
+ }
+
+ /// Produce a radial gradient, normalize the stops.
+ ///
+ /// Will replace the gradient with a single color
+ /// if the radius negative.
+ pub fn radial_gradient(
+ &mut self,
+ center: LayoutPoint,
+ radius: LayoutSize,
+ extend_mode: di::ExtendMode,
+ ) -> di::RadialGradient {
+ if radius.width <= 0.0 || radius.height <= 0.0 {
+ // The shader cannot handle a non positive radius. So
+ // reuse the stops vector and construct an equivalent
+ // gradient.
+ let last_color = self.stops.last().unwrap().color;
+
+ self.stops.clear();
+ self.stops.push(di::GradientStop { offset: 0.0, color: last_color, });
+ self.stops.push(di::GradientStop { offset: 1.0, color: last_color, });
+
+ return di::RadialGradient {
+ center,
+ radius: LayoutSize::new(1.0, 1.0),
+ start_offset: 0.0,
+ end_offset: 1.0,
+ extend_mode,
+ };
+ }
+
+ let (start_offset, end_offset) =
+ self.normalize(extend_mode);
+
+ di::RadialGradient {
+ center,
+ radius,
+ start_offset,
+ end_offset,
+ extend_mode,
+ }
+ }
+
+ /// Produce a conic gradient, normalize the stops.
+ pub fn conic_gradient(
+ &mut self,
+ center: LayoutPoint,
+ angle: f32,
+ extend_mode: di::ExtendMode,
+ ) -> di::ConicGradient {
+ let (start_offset, end_offset) =
+ self.normalize(extend_mode);
+
+ di::ConicGradient {
+ center,
+ angle,
+ start_offset,
+ end_offset,
+ extend_mode,
+ }
+ }
+
+ /// Gradients can be defined with stops outside the range of [0, 1]
+ /// when this happens the gradient needs to be normalized by adjusting
+ /// the gradient stops and gradient line into an equivalent gradient
+ /// with stops in the range [0, 1]. this is done by moving the beginning
+ /// of the gradient line to where stop[0] and the end of the gradient line
+ /// to stop[n-1]. this function adjusts the stops in place, and returns
+ /// the amount to adjust the gradient line start and stop.
+ fn normalize(&mut self, extend_mode: di::ExtendMode) -> (f32, f32) {
+ let stops = &mut self.stops;
+ assert!(stops.len() >= 2);
+
+ let first = *stops.first().unwrap();
+ let last = *stops.last().unwrap();
+
+ // Express the assertion so that if one of the offsets is NaN, we don't panic
+ // and instead take the branch that handles degenerate gradients.
+ assert!(!(first.offset > last.offset));
+
+ let stops_delta = last.offset - first.offset;
+
+ if stops_delta > 0.000001 {
+ for stop in stops {
+ stop.offset = (stop.offset - first.offset) / stops_delta;
+ }
+
+ (first.offset, last.offset)
+ } else {
+ // We have a degenerate gradient and can't accurately transform the stops
+ // what happens here depends on the repeat behavior, but in any case
+ // we reconstruct the gradient stops to something simpler and equivalent
+ stops.clear();
+
+ match extend_mode {
+ di::ExtendMode::Clamp => {
+ // This gradient is two colors split at the offset of the stops,
+ // so create a gradient with two colors split at 0.5 and adjust
+ // the gradient line so 0.5 is at the offset of the stops
+ stops.push(di::GradientStop { color: first.color, offset: 0.0, });
+ stops.push(di::GradientStop { color: first.color, offset: 0.5, });
+ stops.push(di::GradientStop { color: last.color, offset: 0.5, });
+ stops.push(di::GradientStop { color: last.color, offset: 1.0, });
+
+ let offset = last.offset;
+
+ (offset - 0.5, offset + 0.5)
+ }
+ di::ExtendMode::Repeat => {
+ // A repeating gradient with stops that are all in the same
+ // position should just display the last color. I believe the
+ // spec says that it should be the average color of the gradient,
+ // but this matches what Gecko and Blink does
+ stops.push(di::GradientStop { color: last.color, offset: 0.0, });
+ stops.push(di::GradientStop { color: last.color, offset: 1.0, });
+
+ (0.0, 1.0)
+ }
+ }
+ }
+ }
+}
diff --git a/gfx/wr/webrender_api/src/image.rs b/gfx/wr/webrender_api/src/image.rs
new file mode 100644
index 0000000000..4ce73f9e46
--- /dev/null
+++ b/gfx/wr/webrender_api/src/image.rs
@@ -0,0 +1,580 @@
+/* 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/. */
+
+#![deny(missing_docs)]
+
+use euclid::{size2, Box2D, num::Zero};
+use peek_poke::PeekPoke;
+use std::ops::{Add, Sub};
+use std::sync::Arc;
+// local imports
+use crate::{IdNamespace, TileSize};
+use crate::font::{FontInstanceKey, FontInstanceData, FontKey, FontTemplate};
+use crate::units::*;
+
+/// The default tile size for blob images and regular images larger than
+/// the maximum texture size.
+pub const DEFAULT_TILE_SIZE: TileSize = 512;
+
+/// An opaque identifier describing an image registered with WebRender.
+/// This is used as a handle to reference images, and is used as the
+/// hash map key for the actual image storage in the `ResourceCache`.
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize, PeekPoke)]
+pub struct ImageKey(pub IdNamespace, pub u32);
+
+impl Default for ImageKey {
+ fn default() -> Self {
+ ImageKey::DUMMY
+ }
+}
+
+impl ImageKey {
+ /// Placeholder Image key, used to represent None.
+ pub const DUMMY: Self = ImageKey(IdNamespace(0), 0);
+
+ /// Mints a new ImageKey. The given ID must be unique.
+ pub fn new(namespace: IdNamespace, key: u32) -> Self {
+ ImageKey(namespace, key)
+ }
+}
+
+/// An opaque identifier describing a blob image registered with WebRender.
+/// This is used as a handle to reference blob images, and can be used as an
+/// image in display items.
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
+pub struct BlobImageKey(pub ImageKey);
+
+impl BlobImageKey {
+ /// Interpret this blob image as an image for a display item.
+ pub fn as_image(self) -> ImageKey {
+ self.0
+ }
+}
+
+/// An arbitrary identifier for an external image provided by the
+/// application. It must be a unique identifier for each external
+/// image.
+#[repr(C)]
+#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
+pub struct ExternalImageId(pub u64);
+
+/// The source for an external image.
+pub enum ExternalImageSource<'a> {
+ /// A raw pixel buffer.
+ RawData(&'a [u8]),
+ /// A gl::GLuint texture handle.
+ NativeTexture(u32),
+ /// An invalid source.
+ Invalid,
+}
+
+/// The data that an external client should provide about
+/// an external image. For instance, if providing video frames,
+/// the application could call wr.render() whenever a new
+/// video frame is ready. Note that the UV coords are supplied
+/// in texel-space!
+pub struct ExternalImage<'a> {
+ /// UV coordinates for the image.
+ pub uv: TexelRect,
+ /// The source for this image's contents.
+ pub source: ExternalImageSource<'a>,
+}
+
+/// The interfaces that an application can implement to support providing
+/// external image buffers.
+/// When the application passes an external image to WR, it should keep that
+/// external image life time. People could check the epoch id in RenderNotifier
+/// at the client side to make sure that the external image is not used by WR.
+/// Then, do the clean up for that external image.
+pub trait ExternalImageHandler {
+ /// Lock the external image. Then, WR could start to read the image content.
+ /// The WR client should not change the image content until the unlock()
+ /// call.
+ fn lock(&mut self, key: ExternalImageId, channel_index: u8) -> ExternalImage;
+ /// Unlock the external image. WR should not read the image content
+ /// after this call.
+ fn unlock(&mut self, key: ExternalImageId, channel_index: u8);
+}
+
+/// Specifies the type of texture target in driver terms.
+#[repr(u8)]
+#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
+pub enum ImageBufferKind {
+ /// Standard texture. This maps to GL_TEXTURE_2D in OpenGL.
+ Texture2D = 0,
+ /// Rectangle texture. This maps to GL_TEXTURE_RECTANGLE in OpenGL. This
+ /// is similar to a standard texture, with a few subtle differences
+ /// (no mipmaps, non-power-of-two dimensions, different coordinate space)
+ /// that make it useful for representing the kinds of textures we use
+ /// in WebRender. See https://www.khronos.org/opengl/wiki/Rectangle_Texture
+ /// for background on Rectangle textures.
+ TextureRect = 1,
+ /// External texture. This maps to GL_TEXTURE_EXTERNAL_OES in OpenGL, which
+ /// is an extension. This is used for image formats that OpenGL doesn't
+ /// understand, particularly YUV. See
+ /// https://www.khronos.org/registry/OpenGL/extensions/OES/OES_EGL_image_external.txt
+ TextureExternal = 2,
+}
+
+/// Storage format identifier for externally-managed images.
+#[repr(u8)]
+#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
+pub enum ExternalImageType {
+ /// The image is texture-backed.
+ TextureHandle(ImageBufferKind),
+ /// The image is heap-allocated by the embedding.
+ Buffer,
+}
+
+/// Descriptor for external image resources. See `ImageData`.
+#[repr(C)]
+#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
+pub struct ExternalImageData {
+ /// The identifier of this external image, provided by the embedding.
+ pub id: ExternalImageId,
+ /// For multi-plane images (i.e. YUV), indicates the plane of the
+ /// original image that this struct represents. 0 for single-plane images.
+ pub channel_index: u8,
+ /// Storage format identifier.
+ pub image_type: ExternalImageType,
+}
+
+/// Specifies the format of a series of pixels, in driver terms.
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
+pub enum ImageFormat {
+ /// One-channel, byte storage. The "red" doesn't map to the color
+ /// red per se, and is just the way that OpenGL has historically referred
+ /// to single-channel buffers.
+ R8 = 1,
+ /// One-channel, short storage
+ R16 = 2,
+ /// Four channels, byte storage.
+ BGRA8 = 3,
+ /// Four channels, float storage.
+ RGBAF32 = 4,
+ /// Two-channels, byte storage. Similar to `R8`, this just means
+ /// "two channels" rather than "red and green".
+ RG8 = 5,
+ /// Two-channels, short storage. Similar to `R16`, this just means
+ /// "two channels" rather than "red and green".
+ RG16 = 6,
+
+ /// Four channels, signed integer storage.
+ RGBAI32 = 7,
+ /// Four channels, byte storage.
+ RGBA8 = 8,
+}
+
+impl ImageFormat {
+ /// Returns the number of bytes per pixel for the given format.
+ pub fn bytes_per_pixel(self) -> i32 {
+ match self {
+ ImageFormat::R8 => 1,
+ ImageFormat::R16 => 2,
+ ImageFormat::BGRA8 => 4,
+ ImageFormat::RGBAF32 => 16,
+ ImageFormat::RG8 => 2,
+ ImageFormat::RG16 => 4,
+ ImageFormat::RGBAI32 => 16,
+ ImageFormat::RGBA8 => 4,
+ }
+ }
+}
+
+/// Specifies the color depth of an image. Currently only used for YUV images.
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize, PeekPoke)]
+pub enum ColorDepth {
+ /// 8 bits image (most common)
+ Color8,
+ /// 10 bits image
+ Color10,
+ /// 12 bits image
+ Color12,
+ /// 16 bits image
+ Color16,
+}
+
+impl Default for ColorDepth {
+ fn default() -> Self {
+ ColorDepth::Color8
+ }
+}
+
+impl ColorDepth {
+ /// Return the numerical bit depth value for the type.
+ pub fn bit_depth(self) -> u32 {
+ match self {
+ ColorDepth::Color8 => 8,
+ ColorDepth::Color10 => 10,
+ ColorDepth::Color12 => 12,
+ ColorDepth::Color16 => 16,
+ }
+ }
+ /// 10 and 12 bits images are encoded using 16 bits integer, we need to
+ /// rescale the 10 or 12 bits value to extend to 16 bits.
+ pub fn rescaling_factor(self) -> f32 {
+ match self {
+ ColorDepth::Color8 => 1.0,
+ ColorDepth::Color10 => 64.0,
+ ColorDepth::Color12 => 16.0,
+ ColorDepth::Color16 => 1.0,
+ }
+ }
+}
+
+bitflags! {
+ /// Various flags that are part of an image descriptor.
+ #[derive(Deserialize, Serialize)]
+ pub struct ImageDescriptorFlags: u32 {
+ /// Whether this image is opaque, or has an alpha channel. Avoiding blending
+ /// for opaque surfaces is an important optimization.
+ const IS_OPAQUE = 1;
+ /// Whether to allow the driver to automatically generate mipmaps. If images
+ /// are already downscaled appropriately, mipmap generation can be wasted
+ /// work, and cause performance problems on some cards/drivers.
+ ///
+ /// See https://github.com/servo/webrender/pull/2555/
+ const ALLOW_MIPMAPS = 2;
+ }
+}
+
+/// Metadata (but not storage) describing an image In WebRender.
+#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
+pub struct ImageDescriptor {
+ /// Format of the image data.
+ pub format: ImageFormat,
+ /// Width and length of the image data, in pixels.
+ pub size: DeviceIntSize,
+ /// The number of bytes from the start of one row to the next. If non-None,
+ /// `compute_stride` will return this value, otherwise it returns
+ /// `width * bpp`. Different source of images have different alignment
+ /// constraints for rows, so the stride isn't always equal to width * bpp.
+ pub stride: Option<i32>,
+ /// Offset in bytes of the first pixel of this image in its backing buffer.
+ /// This is used for tiling, wherein WebRender extracts chunks of input images
+ /// in order to cache, manipulate, and render them individually. This offset
+ /// tells the texture upload machinery where to find the bytes to upload for
+ /// this tile. Non-tiled images generally set this to zero.
+ pub offset: i32,
+ /// Various bool flags related to this descriptor.
+ pub flags: ImageDescriptorFlags,
+}
+
+impl ImageDescriptor {
+ /// Mints a new ImageDescriptor.
+ pub fn new(
+ width: i32,
+ height: i32,
+ format: ImageFormat,
+ flags: ImageDescriptorFlags,
+ ) -> Self {
+ ImageDescriptor {
+ size: size2(width, height),
+ format,
+ stride: None,
+ offset: 0,
+ flags,
+ }
+ }
+
+ /// Returns the stride, either via an explicit stride stashed on the object
+ /// or by the default computation.
+ pub fn compute_stride(&self) -> i32 {
+ self.stride.unwrap_or(self.size.width * self.format.bytes_per_pixel())
+ }
+
+ /// Computes the total size of the image, in bytes.
+ pub fn compute_total_size(&self) -> i32 {
+ self.compute_stride() * self.size.height
+ }
+
+ /// Computes the bounding rectangle for the image, rooted at (0, 0).
+ pub fn full_rect(&self) -> DeviceIntRect {
+ DeviceIntRect::from_origin_and_size(
+ DeviceIntPoint::zero(),
+ self.size,
+ )
+ }
+
+ /// Returns true if this descriptor is opaque
+ pub fn is_opaque(&self) -> bool {
+ self.flags.contains(ImageDescriptorFlags::IS_OPAQUE)
+ }
+
+ /// Returns true if this descriptor allows mipmaps
+ pub fn allow_mipmaps(&self) -> bool {
+ self.flags.contains(ImageDescriptorFlags::ALLOW_MIPMAPS)
+ }
+}
+
+/// Represents the backing store of an arbitrary series of pixels for display by
+/// WebRender. This storage can take several forms.
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub enum ImageData {
+ /// A simple series of bytes, provided by the embedding and owned by WebRender.
+ /// The format is stored out-of-band, currently in ImageDescriptor.
+ Raw(#[serde(with = "serde_image_data_raw")] Arc<Vec<u8>>),
+ /// An image owned by the embedding, and referenced by WebRender. This may
+ /// take the form of a texture or a heap-allocated buffer.
+ External(ExternalImageData),
+}
+
+mod serde_image_data_raw {
+ extern crate serde_bytes;
+
+ use std::sync::Arc;
+ use serde::{Deserializer, Serializer};
+
+ pub fn serialize<S: Serializer>(bytes: &Arc<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error> {
+ serde_bytes::serialize(bytes.as_slice(), serializer)
+ }
+
+ pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Arc<Vec<u8>>, D::Error> {
+ serde_bytes::deserialize(deserializer).map(Arc::new)
+ }
+}
+
+impl ImageData {
+ /// Mints a new raw ImageData, taking ownership of the bytes.
+ pub fn new(bytes: Vec<u8>) -> Self {
+ ImageData::Raw(Arc::new(bytes))
+ }
+
+ /// Mints a new raw ImageData from Arc-ed bytes.
+ pub fn new_shared(bytes: Arc<Vec<u8>>) -> Self {
+ ImageData::Raw(bytes)
+ }
+}
+
+/// The resources exposed by the resource cache available for use by the blob rasterizer.
+pub trait BlobImageResources {
+ /// Returns the `FontTemplate` for the given key.
+ fn get_font_data(&self, key: FontKey) -> Option<FontTemplate>;
+ /// Returns the `FontInstanceData` for the given key, if found.
+ fn get_font_instance_data(&self, key: FontInstanceKey) -> Option<FontInstanceData>;
+}
+
+/// A handler on the render backend that can create rasterizer objects which will
+/// be sent to the scene builder thread to execute the rasterization.
+///
+/// The handler is responsible for collecting resources, managing/updating blob commands
+/// and creating the rasterizer objects, but isn't expected to do any rasterization itself.
+pub trait BlobImageHandler: Send {
+ /// Creates a snapshot of the current state of blob images in the handler.
+ fn create_blob_rasterizer(&mut self) -> Box<dyn AsyncBlobImageRasterizer>;
+
+ /// Creates an empty blob handler of the same type.
+ ///
+ /// This is used to allow creating new API endpoints with blob handlers installed on them.
+ fn create_similar(&self) -> Box<dyn BlobImageHandler>;
+
+ /// A hook to let the blob image handler update any state related to resources that
+ /// are not bundled in the blob recording itself.
+ fn prepare_resources(
+ &mut self,
+ services: &dyn BlobImageResources,
+ requests: &[BlobImageParams],
+ );
+
+ /// Register a blob image.
+ fn add(&mut self, key: BlobImageKey, data: Arc<BlobImageData>, visible_rect: &DeviceIntRect,
+ tile_size: TileSize);
+
+ /// Update an already registered blob image.
+ fn update(&mut self, key: BlobImageKey, data: Arc<BlobImageData>, visible_rect: &DeviceIntRect,
+ dirty_rect: &BlobDirtyRect);
+
+ /// Delete an already registered blob image.
+ fn delete(&mut self, key: BlobImageKey);
+
+ /// A hook to let the handler clean up any state related to a font which the resource
+ /// cache is about to delete.
+ fn delete_font(&mut self, key: FontKey);
+
+ /// A hook to let the handler clean up any state related to a font instance which the
+ /// resource cache is about to delete.
+ fn delete_font_instance(&mut self, key: FontInstanceKey);
+
+ /// A hook to let the handler clean up any state related a given namespace before the
+ /// resource cache deletes them.
+ fn clear_namespace(&mut self, namespace: IdNamespace);
+
+ /// Whether to allow rendering blobs on multiple threads.
+ fn enable_multithreading(&mut self, enable: bool);
+}
+
+/// A group of rasterization requests to execute synchronously on the scene builder thread.
+pub trait AsyncBlobImageRasterizer : Send {
+ /// Rasterize the requests.
+ ///
+ /// Gecko uses te priority hint to schedule work in a way that minimizes the risk
+ /// of high priority work being blocked by (or enqued behind) low priority work.
+ fn rasterize(
+ &mut self,
+ requests: &[BlobImageParams],
+ low_priority: bool
+ ) -> Vec<(BlobImageRequest, BlobImageResult)>;
+}
+
+
+/// Input parameters for the BlobImageRasterizer.
+#[derive(Copy, Clone, Debug)]
+pub struct BlobImageParams {
+ /// A key that identifies the blob image rasterization request.
+ pub request: BlobImageRequest,
+ /// Description of the format of the blob's output image.
+ pub descriptor: BlobImageDescriptor,
+ /// An optional sub-rectangle of the image to avoid re-rasterizing
+ /// the entire image when only a portion is updated.
+ ///
+ /// If set to None the entire image is rasterized.
+ pub dirty_rect: BlobDirtyRect,
+}
+
+/// The possible states of a Dirty rect.
+///
+/// This exists because people kept getting confused with `Option<Box2D>`.
+#[derive(Debug, Serialize, Deserialize)]
+pub enum DirtyRect<T: Copy, U> {
+ /// Everything is Dirty, equivalent to Partial(image_bounds)
+ All,
+ /// Some specific amount is dirty
+ Partial(Box2D<T, U>)
+}
+
+impl<T, U> DirtyRect<T, U>
+where
+ T: Copy + Clone
+ + PartialOrd + PartialEq
+ + Add<T, Output = T>
+ + Sub<T, Output = T>
+ + Zero
+{
+ /// Creates an empty DirtyRect (indicating nothing is invalid)
+ pub fn empty() -> Self {
+ DirtyRect::Partial(Box2D::zero())
+ }
+
+ /// Returns whether the dirty rect is empty
+ pub fn is_empty(&self) -> bool {
+ match self {
+ DirtyRect::All => false,
+ DirtyRect::Partial(rect) => rect.is_empty(),
+ }
+ }
+
+ /// Replaces self with the empty rect and returns the old value.
+ pub fn replace_with_empty(&mut self) -> Self {
+ ::std::mem::replace(self, DirtyRect::empty())
+ }
+
+ /// Maps over the contents of Partial.
+ pub fn map<F>(self, func: F) -> Self
+ where F: FnOnce(Box2D<T, U>) -> Box2D<T, U>,
+ {
+ use crate::DirtyRect::*;
+
+ match self {
+ All => All,
+ Partial(rect) => Partial(func(rect)),
+ }
+ }
+
+ /// Unions the dirty rects.
+ pub fn union(&self, other: &Self) -> Self {
+ use crate::DirtyRect::*;
+
+ match (*self, *other) {
+ (All, _) | (_, All) => All,
+ (Partial(rect1), Partial(rect2)) => Partial(rect1.union(&rect2)),
+ }
+ }
+
+ /// Intersects the dirty rects.
+ pub fn intersection(&self, other: &Self) -> Self {
+ use crate::DirtyRect::*;
+
+ match (*self, *other) {
+ (All, rect) | (rect, All) => rect,
+ (Partial(rect1), Partial(rect2)) => {
+ Partial(rect1.intersection(&rect2).unwrap_or_else(Box2D::zero))
+ }
+ }
+ }
+
+ /// Converts the dirty rect into a subrect of the given one via intersection.
+ pub fn to_subrect_of(&self, rect: &Box2D<T, U>) -> Box2D<T, U> {
+ use crate::DirtyRect::*;
+
+ match *self {
+ All => *rect,
+ Partial(dirty_rect) => {
+ dirty_rect.intersection(rect).unwrap_or_else(Box2D::zero)
+ }
+ }
+ }
+}
+
+impl<T: Copy, U> Copy for DirtyRect<T, U> {}
+impl<T: Copy, U> Clone for DirtyRect<T, U> {
+ fn clone(&self) -> Self { *self }
+}
+
+impl<T: Copy, U> From<Box2D<T, U>> for DirtyRect<T, U> {
+ fn from(rect: Box2D<T, U>) -> Self {
+ DirtyRect::Partial(rect)
+ }
+}
+
+/// Backing store for blob image command streams.
+pub type BlobImageData = Vec<u8>;
+
+/// Result type for blob raserization.
+pub type BlobImageResult = Result<RasterizedBlobImage, BlobImageError>;
+
+/// Metadata (but not storage) for a blob image.
+#[repr(C)]
+#[derive(Copy, Clone, Debug)]
+pub struct BlobImageDescriptor {
+ /// Surface of the image or tile to render in the same coordinate space as
+ /// the drawing commands.
+ pub rect: LayoutIntRect,
+ /// Format for the data in the backing store.
+ pub format: ImageFormat,
+}
+
+/// Representation of a rasterized blob image. This is obtained by passing
+/// `BlobImageData` to the embedding via the rasterization callback.
+pub struct RasterizedBlobImage {
+ /// The rectangle that was rasterized in device pixels, relative to the
+ /// image or tile.
+ pub rasterized_rect: DeviceIntRect,
+ /// Backing store. The format is stored out of band in `BlobImageDescriptor`.
+ pub data: Arc<Vec<u8>>,
+}
+
+/// Error code for when blob rasterization failed.
+#[derive(Clone, Debug)]
+pub enum BlobImageError {
+ /// Out of memory.
+ Oom,
+ /// Other failure, embedding-specified.
+ Other(String),
+}
+
+
+
+/// A key identifying blob image rasterization work requested from the blob
+/// image rasterizer.
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub struct BlobImageRequest {
+ /// Unique handle to the image.
+ pub key: BlobImageKey,
+ /// Tiling offset in number of tiles.
+ pub tile: TileOffset,
+}
diff --git a/gfx/wr/webrender_api/src/lib.rs b/gfx/wr/webrender_api/src/lib.rs
new file mode 100644
index 0000000000..77e7a95358
--- /dev/null
+++ b/gfx/wr/webrender_api/src/lib.rs
@@ -0,0 +1,768 @@
+/* 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/. */
+
+//! The `webrender_api` crate contains an assortment types and functions used
+//! by WebRender consumers as well as, in many cases, WebRender itself.
+//!
+//! This separation allows Servo to parallelize compilation across `webrender`
+//! and other crates that depend on `webrender_api`. So in practice, we put
+//! things in this crate when Servo needs to use them. Firefox depends on the
+//! `webrender` crate directly, and so this distinction is not really relevant
+//! there.
+
+#![cfg_attr(feature = "nightly", feature(nonzero))]
+#![cfg_attr(feature = "cargo-clippy", allow(clippy::float_cmp, clippy::too_many_arguments))]
+#![cfg_attr(feature = "cargo-clippy", allow(clippy::unreadable_literal, clippy::new_without_default))]
+
+pub extern crate crossbeam_channel;
+pub extern crate euclid;
+
+extern crate app_units;
+#[macro_use]
+extern crate bitflags;
+extern crate byteorder;
+#[cfg(feature = "nightly")]
+extern crate core;
+#[macro_use]
+extern crate malloc_size_of_derive;
+extern crate serde;
+#[macro_use]
+extern crate serde_derive;
+extern crate time;
+
+extern crate malloc_size_of;
+extern crate peek_poke;
+
+pub mod channel;
+mod color;
+mod display_item;
+mod display_item_cache;
+mod display_list;
+mod font;
+mod gradient_builder;
+mod image;
+pub mod units;
+
+pub use crate::color::*;
+pub use crate::display_item::*;
+pub use crate::display_item_cache::DisplayItemCache;
+pub use crate::display_list::*;
+pub use crate::font::*;
+pub use crate::gradient_builder::*;
+pub use crate::image::*;
+
+use crate::units::*;
+use crate::channel::Receiver;
+use std::marker::PhantomData;
+use std::sync::Arc;
+use std::os::raw::c_void;
+use peek_poke::PeekPoke;
+
+/// Defined here for cbindgen
+pub const MAX_RENDER_TASK_SIZE: i32 = 16384;
+
+/// Width and height in device pixels of image tiles.
+pub type TileSize = u16;
+
+/// Various settings that the caller can select based on desired tradeoffs
+/// between rendering quality and performance / power usage.
+#[derive(Copy, Clone, Deserialize, Serialize)]
+pub struct QualitySettings {
+ /// If true, disable creating separate picture cache slices when the
+ /// scroll root changes. This gives maximum opportunity to find an
+ /// opaque background, which enables subpixel AA. However, it is
+ /// usually significantly more expensive to render when scrolling.
+ pub force_subpixel_aa_where_possible: bool,
+}
+
+impl Default for QualitySettings {
+ fn default() -> Self {
+ QualitySettings {
+ // Prefer performance over maximum subpixel AA quality, since WR
+ // already enables subpixel AA in more situations than other browsers.
+ force_subpixel_aa_where_possible: false,
+ }
+ }
+}
+
+/// An epoch identifies the state of a pipeline in time.
+///
+/// This is mostly used as a synchronization mechanism to observe how/when particular pipeline
+/// updates propagate through WebRender and are applied at various stages.
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
+pub struct Epoch(pub u32);
+
+impl Epoch {
+ /// Magic invalid epoch value.
+ pub fn invalid() -> Epoch {
+ Epoch(u32::MAX)
+ }
+}
+
+/// ID namespaces uniquely identify different users of WebRender's API.
+///
+/// For example in Gecko each content process uses a separate id namespace.
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, Eq, MallocSizeOf, PartialEq, Hash, Ord, PartialOrd, PeekPoke)]
+#[derive(Deserialize, Serialize)]
+pub struct IdNamespace(pub u32);
+
+/// A key uniquely identifying a WebRender document.
+///
+/// Instances can manage one or several documents (using the same render backend thread).
+/// Each document will internally correspond to a single scene, and scenes are made of
+/// one or several pipelines.
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize, PeekPoke)]
+pub struct DocumentId {
+ ///
+ pub namespace_id: IdNamespace,
+ ///
+ pub id: u32,
+}
+
+impl DocumentId {
+ ///
+ pub fn new(namespace_id: IdNamespace, id: u32) -> Self {
+ DocumentId {
+ namespace_id,
+ id,
+ }
+ }
+
+ ///
+ pub const INVALID: DocumentId = DocumentId { namespace_id: IdNamespace(0), id: 0 };
+}
+
+/// This type carries no valuable semantics for WR. However, it reflects the fact that
+/// clients (Servo) may generate pipelines by different semi-independent sources.
+/// These pipelines still belong to the same `IdNamespace` and the same `DocumentId`.
+/// Having this extra Id field enables them to generate `PipelineId` without collision.
+pub type PipelineSourceId = u32;
+
+/// From the point of view of WR, `PipelineId` is completely opaque and generic as long as
+/// it's clonable, serializable, comparable, and hashable.
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize, PeekPoke)]
+pub struct PipelineId(pub PipelineSourceId, pub u32);
+
+impl Default for PipelineId {
+ fn default() -> Self {
+ PipelineId::dummy()
+ }
+}
+
+impl PipelineId {
+ ///
+ pub fn dummy() -> Self {
+ PipelineId(!0, !0)
+ }
+
+ pub const INVALID: Self = PipelineId(!0, !0);
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
+pub struct FramePublishId(pub u64);
+
+impl FramePublishId {
+ /// Returns a FramePublishId corresponding to the first frame.
+ ///
+ /// Note that we use 0 as the internal id here because the current code
+ /// increments the frame publish id just before ResultMsg::PublishDocument,
+ /// and we want the first id to be 1.
+ pub fn first() -> Self {
+ FramePublishId(0)
+ }
+
+ /// Advances this FramePublishId to the next.
+ pub fn advance(&mut self) {
+ self.0 += 1;
+ }
+
+ /// An invalid sentinel FramePublishId, which will always compare less than
+ /// any valid FrameId.
+ pub const INVALID: Self = FramePublishId(0);
+}
+
+impl Default for FramePublishId {
+ fn default() -> Self {
+ FramePublishId::INVALID
+ }
+}
+
+/// An opaque pointer-sized value.
+#[repr(C)]
+#[derive(Clone)]
+pub struct ExternalEvent {
+ raw: usize,
+}
+
+unsafe impl Send for ExternalEvent {}
+
+impl ExternalEvent {
+ /// Creates the event from an opaque pointer-sized value.
+ pub fn from_raw(raw: usize) -> Self {
+ ExternalEvent { raw }
+ }
+ /// Consumes self to make it obvious that the event should be forwarded only once.
+ pub fn unwrap(self) -> usize {
+ self.raw
+ }
+}
+
+pub type APZScrollGeneration = u64;
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize, Default)]
+pub struct SampledScrollOffset {
+ pub offset: LayoutVector2D,
+ pub generation: APZScrollGeneration,
+}
+
+/// A flag in each scrollable frame to represent whether the owner of the frame document
+/// has any scroll-linked effect.
+/// See https://firefox-source-docs.mozilla.org/performance/scroll-linked_effects.html
+/// for a definition of scroll-linked effect.
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize, PeekPoke)]
+pub enum HasScrollLinkedEffect {
+ Yes,
+ No,
+}
+
+impl Default for HasScrollLinkedEffect {
+ fn default() -> Self {
+ HasScrollLinkedEffect::No
+ }
+}
+
+/// A handler to integrate WebRender with the thread that contains the `Renderer`.
+pub trait RenderNotifier: Send {
+ ///
+ fn clone(&self) -> Box<dyn RenderNotifier>;
+ /// Wake the thread containing the `Renderer` up (after updates have been put
+ /// in the renderer's queue).
+ fn wake_up(
+ &self,
+ composite_needed: bool,
+ );
+ /// Notify the thread containing the `Renderer` that a new frame is ready.
+ fn new_frame_ready(&self, _: DocumentId, scrolled: bool, composite_needed: bool, frame_publish_id: FramePublishId);
+ /// A Gecko-specific notification mechanism to get some code executed on the
+ /// `Renderer`'s thread, mostly replaced by `NotificationHandler`. You should
+ /// probably use the latter instead.
+ fn external_event(&self, _evt: ExternalEvent) {
+ unimplemented!()
+ }
+ /// Notify the thread containing the `Renderer` that the render backend has been
+ /// shut down.
+ fn shut_down(&self) {}
+}
+
+/// A stage of the rendering pipeline.
+#[repr(u32)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum Checkpoint {
+ ///
+ SceneBuilt,
+ ///
+ FrameBuilt,
+ ///
+ FrameTexturesUpdated,
+ ///
+ FrameRendered,
+ /// NotificationRequests get notified with this if they get dropped without having been
+ /// notified. This provides the guarantee that if a request is created it will get notified.
+ TransactionDropped,
+}
+
+/// A handler to notify when a transaction reaches certain stages of the rendering
+/// pipeline.
+pub trait NotificationHandler : Send + Sync {
+ /// Entry point of the handler to implement. Invoked by WebRender.
+ fn notify(&self, when: Checkpoint);
+}
+
+/// A request to notify a handler when the transaction reaches certain stages of the
+/// rendering pipeline.
+///
+/// The request is guaranteed to be notified once and only once, even if the transaction
+/// is dropped before the requested check-point.
+pub struct NotificationRequest {
+ handler: Option<Box<dyn NotificationHandler>>,
+ when: Checkpoint,
+}
+
+impl NotificationRequest {
+ /// Constructor.
+ pub fn new(when: Checkpoint, handler: Box<dyn NotificationHandler>) -> Self {
+ NotificationRequest {
+ handler: Some(handler),
+ when,
+ }
+ }
+
+ /// The specified stage at which point the handler should be notified.
+ pub fn when(&self) -> Checkpoint { self.when }
+
+ /// Called by WebRender at specified stages to notify the registered handler.
+ pub fn notify(mut self) {
+ if let Some(handler) = self.handler.take() {
+ handler.notify(self.when);
+ }
+ }
+}
+
+/// An object that can perform hit-testing without doing synchronous queries to
+/// the RenderBackendThread.
+pub trait ApiHitTester: Send + Sync {
+ /// Does a hit test on display items in the specified document, at the given
+ /// point. The vector of hit results will contain all display items that match,
+ /// ordered from front to back.
+ fn hit_test(&self, point: WorldPoint) -> HitTestResult;
+}
+
+/// A hit tester requested to the render backend thread but not necessarily ready yet.
+///
+/// The request should be resolved as late as possible to reduce the likelihood of blocking.
+pub struct HitTesterRequest {
+ #[doc(hidden)]
+ pub rx: Receiver<Arc<dyn ApiHitTester>>,
+}
+
+impl HitTesterRequest {
+ /// Block until the hit tester is available and return it, consuming teh request.
+ pub fn resolve(self) -> Arc<dyn ApiHitTester> {
+ self.rx.recv().unwrap()
+ }
+}
+
+/// Describe an item that matched a hit-test query.
+#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
+pub struct HitTestResultItem {
+ /// The pipeline that the display item that was hit belongs to.
+ pub pipeline: PipelineId,
+
+ /// The tag of the hit display item.
+ pub tag: ItemTag,
+
+ /// The animation id from the stacking context.
+ pub animation_id: u64,
+}
+
+/// Returned by `RenderApi::hit_test`.
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
+pub struct HitTestResult {
+ /// List of items that are match the hit-test query.
+ pub items: Vec<HitTestResultItem>,
+}
+
+impl Drop for NotificationRequest {
+ fn drop(&mut self) {
+ if let Some(ref mut handler) = self.handler {
+ handler.notify(Checkpoint::TransactionDropped);
+ }
+ }
+}
+
+// This Clone impl yields an "empty" request because we don't want the requests
+// to be notified twice so the request is owned by only one of the API messages
+// (the original one) after the clone.
+// This works in practice because the notifications requests are used for
+// synchronization so we don't need to include them in the recording mechanism
+// in wrench that clones the messages.
+impl Clone for NotificationRequest {
+ fn clone(&self) -> Self {
+ NotificationRequest {
+ when: self.when,
+ handler: None,
+ }
+ }
+}
+
+
+/// A key to identify an animated property binding.
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, Deserialize, MallocSizeOf, PartialEq, Serialize, Eq, Hash, PeekPoke)]
+pub struct PropertyBindingId {
+ pub namespace: IdNamespace,
+ pub uid: u32,
+}
+
+impl PropertyBindingId {
+ /// Constructor.
+ pub fn new(value: u64) -> Self {
+ PropertyBindingId {
+ namespace: IdNamespace((value >> 32) as u32),
+ uid: value as u32,
+ }
+ }
+
+ /// Decompose the ID back into the raw integer.
+ pub fn to_u64(&self) -> u64 {
+ ((self.namespace.0 as u64) << 32) | self.uid as u64
+ }
+}
+
+/// A unique key that is used for connecting animated property
+/// values to bindings in the display list.
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize, PeekPoke)]
+pub struct PropertyBindingKey<T> {
+ ///
+ pub id: PropertyBindingId,
+ #[doc(hidden)]
+ pub _phantom: PhantomData<T>,
+}
+
+/// Construct a property value from a given key and value.
+impl<T: Copy> PropertyBindingKey<T> {
+ ///
+ pub fn with(self, value: T) -> PropertyValue<T> {
+ PropertyValue { key: self, value }
+ }
+}
+
+impl<T> Into<u64> for PropertyBindingKey<T> {
+ fn into(self) -> u64 {
+ self.id.to_u64()
+ }
+}
+
+impl<T> PropertyBindingKey<T> {
+ /// Constructor.
+ pub fn new(value: u64) -> Self {
+ PropertyBindingKey {
+ id: PropertyBindingId::new(value),
+ _phantom: PhantomData,
+ }
+ }
+}
+
+/// A binding property can either be a specific value
+/// (the normal, non-animated case) or point to a binding location
+/// to fetch the current value from.
+/// Note that Binding has also a non-animated value, the value is
+/// used for the case where the animation is still in-delay phase
+/// (i.e. the animation doesn't produce any animation values).
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize, PeekPoke)]
+pub enum PropertyBinding<T> {
+ /// Non-animated value.
+ Value(T),
+ /// Animated binding.
+ Binding(PropertyBindingKey<T>, T),
+}
+
+impl<T: Default> Default for PropertyBinding<T> {
+ fn default() -> Self {
+ PropertyBinding::Value(Default::default())
+ }
+}
+
+impl<T> From<T> for PropertyBinding<T> {
+ fn from(value: T) -> PropertyBinding<T> {
+ PropertyBinding::Value(value)
+ }
+}
+
+impl From<PropertyBindingKey<ColorF>> for PropertyBindingKey<ColorU> {
+ fn from(key: PropertyBindingKey<ColorF>) -> PropertyBindingKey<ColorU> {
+ PropertyBindingKey {
+ id: key.id.clone(),
+ _phantom: PhantomData,
+ }
+ }
+}
+
+impl From<PropertyBindingKey<ColorU>> for PropertyBindingKey<ColorF> {
+ fn from(key: PropertyBindingKey<ColorU>) -> PropertyBindingKey<ColorF> {
+ PropertyBindingKey {
+ id: key.id.clone(),
+ _phantom: PhantomData,
+ }
+ }
+}
+
+impl From<PropertyBinding<ColorF>> for PropertyBinding<ColorU> {
+ fn from(value: PropertyBinding<ColorF>) -> PropertyBinding<ColorU> {
+ match value {
+ PropertyBinding::Value(value) => PropertyBinding::Value(value.into()),
+ PropertyBinding::Binding(k, v) => {
+ PropertyBinding::Binding(k.into(), v.into())
+ }
+ }
+ }
+}
+
+impl From<PropertyBinding<ColorU>> for PropertyBinding<ColorF> {
+ fn from(value: PropertyBinding<ColorU>) -> PropertyBinding<ColorF> {
+ match value {
+ PropertyBinding::Value(value) => PropertyBinding::Value(value.into()),
+ PropertyBinding::Binding(k, v) => {
+ PropertyBinding::Binding(k.into(), v.into())
+ }
+ }
+ }
+}
+
+/// The current value of an animated property. This is
+/// supplied by the calling code.
+#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
+pub struct PropertyValue<T> {
+ ///
+ pub key: PropertyBindingKey<T>,
+ ///
+ pub value: T,
+}
+
+/// When using `generate_frame()`, a list of `PropertyValue` structures
+/// can optionally be supplied to provide the current value of any
+/// animated properties.
+#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Default)]
+pub struct DynamicProperties {
+ /// transform list
+ pub transforms: Vec<PropertyValue<LayoutTransform>>,
+ /// opacity
+ pub floats: Vec<PropertyValue<f32>>,
+ /// background color
+ pub colors: Vec<PropertyValue<ColorF>>,
+}
+
+impl DynamicProperties {
+ /// Extend the properties.
+ pub fn extend(&mut self, other: Self) {
+ self.transforms.extend(other.transforms);
+ self.floats.extend(other.floats);
+ self.colors.extend(other.colors);
+ }
+}
+
+/// A C function that takes a pointer to a heap allocation and returns its size.
+///
+/// This is borrowed from the malloc_size_of crate, upon which we want to avoid
+/// a dependency from WebRender.
+pub type VoidPtrToSizeFn = unsafe extern "C" fn(ptr: *const c_void) -> usize;
+
+/// A configuration option that can be changed at runtime.
+///
+/// # Adding a new configuration option
+///
+/// - Add a new enum variant here.
+/// - Add the entry in WR_BOOL_PARAMETER_LIST in gfxPlatform.cpp.
+/// - React to the parameter change anywhere in WebRender where a SetParam message is received.
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum Parameter {
+ Bool(BoolParameter, bool),
+ Int(IntParameter, i32),
+}
+
+/// Boolean configuration option.
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[repr(u32)]
+pub enum BoolParameter {
+ PboUploads = 0,
+ Multithreading = 1,
+ BatchedUploads = 2,
+ DrawCallsForTextureCopy = 3,
+}
+
+/// Integer configuration option.
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[repr(u32)]
+pub enum IntParameter {
+ BatchedUploadThreshold = 0,
+}
+
+bitflags! {
+ /// Flags to track why we are rendering.
+ #[repr(C)]
+ #[derive(Default, Deserialize, MallocSizeOf, Serialize)]
+ pub struct RenderReasons: u32 {
+ /// Equivalent of empty() for the C++ side.
+ const NONE = 0;
+ const SCENE = 1 << 0;
+ const ANIMATED_PROPERTY = 1 << 1;
+ const RESOURCE_UPDATE = 1 << 2;
+ const ASYNC_IMAGE = 1 << 3;
+ const CLEAR_RESOURCES = 1 << 4;
+ const APZ = 1 << 5;
+ /// Window resize
+ const RESIZE = 1 << 6;
+ /// Various widget-related reasons
+ const WIDGET = 1 << 7;
+ /// See Frame::must_be_drawn
+ const TEXTURE_CACHE_FLUSH = 1 << 8;
+ const SNAPSHOT = 1 << 9;
+ const POST_RESOURCE_UPDATES_HOOK = 1 << 10;
+ const CONFIG_CHANGE = 1 << 11;
+ const CONTENT_SYNC = 1 << 12;
+ const FLUSH = 1 << 13;
+ const TESTING = 1 << 14;
+ const OTHER = 1 << 15;
+ /// Vsync isn't actually "why" we render but it can be useful
+ /// to see which frames were driven by the vsync scheduler so
+ /// we store a bit for it.
+ const VSYNC = 1 << 16;
+ const SKIPPED_COMPOSITE = 1 << 17;
+ /// Gecko does some special things when it starts observing vsync
+ /// so it can be useful to know what frames are associated with it.
+ const START_OBSERVING_VSYNC = 1 << 18;
+ const ASYNC_IMAGE_COMPOSITE_UNTIL = 1 << 19;
+ }
+}
+
+impl RenderReasons {
+ pub const NUM_BITS: u32 = 17;
+}
+
+bitflags! {
+ /// Flags to enable/disable various builtin debugging tools.
+ #[repr(C)]
+ #[derive(Default, Deserialize, MallocSizeOf, Serialize)]
+ pub struct DebugFlags: u32 {
+ /// Display the frame profiler on screen.
+ const PROFILER_DBG = 1 << 0;
+ /// Display intermediate render targets on screen.
+ const RENDER_TARGET_DBG = 1 << 1;
+ /// Display all texture cache pages on screen.
+ const TEXTURE_CACHE_DBG = 1 << 2;
+ /// Display GPU timing results.
+ const GPU_TIME_QUERIES = 1 << 3;
+ /// Query the number of pixels that pass the depth test divided and show it
+ /// in the profiler as a percentage of the number of pixels in the screen
+ /// (window width times height).
+ const GPU_SAMPLE_QUERIES = 1 << 4;
+ /// Render each quad with their own draw call.
+ ///
+ /// Terrible for performance but can help with understanding the drawing
+ /// order when inspecting renderdoc or apitrace recordings.
+ const DISABLE_BATCHING = 1 << 5;
+ /// Display the pipeline epochs.
+ const EPOCHS = 1 << 6;
+ /// Print driver messages to stdout.
+ const ECHO_DRIVER_MESSAGES = 1 << 7;
+ /// Show an overlay displaying overdraw amount.
+ const SHOW_OVERDRAW = 1 << 8;
+ /// Display the contents of GPU cache.
+ const GPU_CACHE_DBG = 1 << 9;
+ /// Clear evicted parts of the texture cache for debugging purposes.
+ const TEXTURE_CACHE_DBG_CLEAR_EVICTED = 1 << 10;
+ /// Show picture caching debug overlay
+ const PICTURE_CACHING_DBG = 1 << 11;
+ /// Highlight all primitives with colors based on kind.
+ const PRIMITIVE_DBG = 1 << 12;
+ /// Draw a zoom widget showing part of the framebuffer zoomed in.
+ const ZOOM_DBG = 1 << 13;
+ /// Scale the debug renderer down for a smaller screen. This will disrupt
+ /// any mapping between debug display items and page content, so shouldn't
+ /// be used with overlays like the picture caching or primitive display.
+ const SMALL_SCREEN = 1 << 14;
+ /// Disable various bits of the WebRender pipeline, to help narrow
+ /// down where slowness might be coming from.
+ const DISABLE_OPAQUE_PASS = 1 << 15;
+ ///
+ const DISABLE_ALPHA_PASS = 1 << 16;
+ ///
+ const DISABLE_CLIP_MASKS = 1 << 17;
+ ///
+ const DISABLE_TEXT_PRIMS = 1 << 18;
+ ///
+ const DISABLE_GRADIENT_PRIMS = 1 << 19;
+ ///
+ const OBSCURE_IMAGES = 1 << 20;
+ /// Taint the transparent area of the glyphs with a random opacity to easily
+ /// see when glyphs are re-rasterized.
+ const GLYPH_FLASHING = 1 << 21;
+ /// The profiler only displays information that is out of the ordinary.
+ const SMART_PROFILER = 1 << 22;
+ /// If set, dump picture cache invalidation debug to console.
+ const INVALIDATION_DBG = 1 << 23;
+ /// Collect and dump profiler statistics to captures.
+ const PROFILER_CAPTURE = (1 as u32) << 25; // need "as u32" until we have cbindgen#556
+ /// Invalidate picture tiles every frames (useful when inspecting GPU work in external tools).
+ const FORCE_PICTURE_INVALIDATION = (1 as u32) << 26;
+ /// Display window visibility on screen.
+ const WINDOW_VISIBILITY_DBG = 1 << 27;
+ /// Render large blobs with at a smaller size (incorrectly). This is a temporary workaround for
+ /// fuzzing.
+ const RESTRICT_BLOB_SIZE = 1 << 28;
+ }
+}
+
+/// Information specific to a primitive type that
+/// uniquely identifies a primitive template by key.
+#[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash, Serialize, Deserialize)]
+pub enum PrimitiveKeyKind {
+ /// Clear an existing rect, used for special effects on some platforms.
+ Clear,
+ ///
+ Rectangle {
+ ///
+ color: PropertyBinding<ColorU>,
+ },
+}
+
+///
+#[derive(Clone, Copy, Debug)]
+pub enum ScrollLocation {
+ /// Scroll by a certain amount.
+ Delta(LayoutVector2D),
+ /// Scroll to very top of element.
+ Start,
+ /// Scroll to very bottom of element.
+ End,
+}
+
+/// Crash annotations included in crash reports.
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub enum CrashAnnotation {
+ CompileShader = 0,
+ DrawShader = 1,
+}
+
+/// Handler to expose support for annotating crash reports.
+pub trait CrashAnnotator : Send {
+ fn set(&self, annotation: CrashAnnotation, value: &std::ffi::CStr);
+ fn clear(&self, annotation: CrashAnnotation);
+ fn box_clone(&self) -> Box<dyn CrashAnnotator>;
+}
+
+impl Clone for Box<dyn CrashAnnotator> {
+ fn clone(&self) -> Box<dyn CrashAnnotator> {
+ self.box_clone()
+ }
+}
+
+/// Guard to add a crash annotation at creation, and clear it at destruction.
+pub struct CrashAnnotatorGuard<'a> {
+ annotator: &'a Option<Box<dyn CrashAnnotator>>,
+ annotation: CrashAnnotation,
+}
+
+impl<'a> CrashAnnotatorGuard<'a> {
+ pub fn new(
+ annotator: &'a Option<Box<dyn CrashAnnotator>>,
+ annotation: CrashAnnotation,
+ value: &std::ffi::CStr,
+ ) -> Self {
+ if let Some(ref annotator) = annotator {
+ annotator.set(annotation, value);
+ }
+ Self {
+ annotator,
+ annotation,
+ }
+ }
+}
+
+impl<'a> Drop for CrashAnnotatorGuard<'a> {
+ fn drop(&mut self) {
+ if let Some(ref annotator) = self.annotator {
+ annotator.clear(self.annotation);
+ }
+ }
+}
diff --git a/gfx/wr/webrender_api/src/units.rs b/gfx/wr/webrender_api/src/units.rs
new file mode 100644
index 0000000000..2d4cc34aab
--- /dev/null
+++ b/gfx/wr/webrender_api/src/units.rs
@@ -0,0 +1,346 @@
+/* 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 collection of coordinate spaces and their corresponding Point, Size and Rect types.
+//!
+//! Physical pixels take into account the device pixel ratio and their dimensions tend
+//! to correspond to the allocated size of resources in memory, while logical pixels
+//! don't have the device pixel ratio applied which means they are agnostic to the usage
+//! of hidpi screens and the like.
+//!
+//! The terms "layer" and "stacking context" can be used interchangeably
+//! in the context of coordinate systems.
+
+pub use app_units::Au;
+use euclid::{Length, Rect, Scale, Size2D, Transform3D, Translation2D};
+use euclid::{Point2D, Point3D, Vector2D, Vector3D, SideOffsets2D, Box2D};
+use euclid::HomogeneousVector;
+use peek_poke::PeekPoke;
+// local imports
+use crate::image::DirtyRect;
+
+/// Geometry in the coordinate system of the render target (screen or intermediate
+/// surface) in physical pixels.
+#[derive(Hash, Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
+pub struct DevicePixel;
+
+pub type DeviceIntRect = Box2D<i32, DevicePixel>;
+pub type DeviceIntPoint = Point2D<i32, DevicePixel>;
+pub type DeviceIntSize = Size2D<i32, DevicePixel>;
+pub type DeviceIntLength = Length<i32, DevicePixel>;
+pub type DeviceIntSideOffsets = SideOffsets2D<i32, DevicePixel>;
+pub type DeviceIntVector2D = Vector2D<i32, DevicePixel>;
+
+pub type DeviceRect = Box2D<f32, DevicePixel>;
+pub type DeviceBox2D = Box2D<f32, DevicePixel>;
+pub type DevicePoint = Point2D<f32, DevicePixel>;
+pub type DeviceVector2D = Vector2D<f32, DevicePixel>;
+pub type DeviceSize = Size2D<f32, DevicePixel>;
+pub type DeviceHomogeneousVector = HomogeneousVector<f32, DevicePixel>;
+
+/// Geometry in the coordinate system of the framebuffer in physical pixels.
+/// It's Y-flipped comparing to DevicePixel.
+#[derive(Hash, Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
+pub struct FramebufferPixel;
+
+pub type FramebufferIntPoint = Point2D<i32, FramebufferPixel>;
+pub type FramebufferIntSize = Size2D<i32, FramebufferPixel>;
+pub type FramebufferIntRect = Box2D<i32, FramebufferPixel>;
+
+/// Geometry in the coordinate system of a Picture (intermediate
+/// surface) in physical pixels.
+#[derive(Hash, Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
+pub struct PicturePixel;
+
+pub type PictureIntRect = Box2D<i32, PicturePixel>;
+pub type PictureIntPoint = Point2D<i32, PicturePixel>;
+pub type PictureIntSize = Size2D<i32, PicturePixel>;
+pub type PictureRect = Box2D<f32, PicturePixel>;
+pub type PicturePoint = Point2D<f32, PicturePixel>;
+pub type PictureSize = Size2D<f32, PicturePixel>;
+pub type PicturePoint3D = Point3D<f32, PicturePixel>;
+pub type PictureVector2D = Vector2D<f32, PicturePixel>;
+pub type PictureVector3D = Vector3D<f32, PicturePixel>;
+pub type PictureBox2D = Box2D<f32, PicturePixel>;
+
+/// Geometry gets rasterized in a given root coordinate space. This
+/// is often the root spatial node (world space), but may be a local
+/// space for a variety of reasons (e.g. perspective).
+#[derive(Hash, Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
+pub struct RasterPixel;
+
+pub type RasterIntRect = Box2D<i32, RasterPixel>;
+pub type RasterIntPoint = Point2D<i32, RasterPixel>;
+pub type RasterIntSize = Size2D<i32, RasterPixel>;
+pub type RasterRect = Box2D<f32, RasterPixel>;
+pub type RasterPoint = Point2D<f32, RasterPixel>;
+pub type RasterSize = Size2D<f32, RasterPixel>;
+pub type RasterPoint3D = Point3D<f32, RasterPixel>;
+pub type RasterVector2D = Vector2D<f32, RasterPixel>;
+pub type RasterVector3D = Vector3D<f32, RasterPixel>;
+
+/// Geometry in a stacking context's local coordinate space (logical pixels).
+#[derive(Hash, Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, Ord, PartialOrd, Deserialize, Serialize, PeekPoke)]
+pub struct LayoutPixel;
+
+pub type LayoutRect = Box2D<f32, LayoutPixel>;
+pub type LayoutPoint = Point2D<f32, LayoutPixel>;
+pub type LayoutPoint3D = Point3D<f32, LayoutPixel>;
+pub type LayoutVector2D = Vector2D<f32, LayoutPixel>;
+pub type LayoutVector3D = Vector3D<f32, LayoutPixel>;
+pub type LayoutSize = Size2D<f32, LayoutPixel>;
+pub type LayoutSideOffsets = SideOffsets2D<f32, LayoutPixel>;
+
+pub type LayoutIntRect = Box2D<i32, LayoutPixel>;
+pub type LayoutIntPoint = Point2D<i32, LayoutPixel>;
+pub type LayoutIntSize = Size2D<i32, LayoutPixel>;
+
+/// Geometry in the document's coordinate space (logical pixels).
+#[derive(Hash, Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
+pub struct WorldPixel;
+
+pub type WorldRect = Box2D<f32, WorldPixel>;
+pub type WorldIntRect = Box2D<i32, WorldPixel>;
+pub type WorldPoint = Point2D<f32, WorldPixel>;
+pub type WorldSize = Size2D<f32, WorldPixel>;
+pub type WorldPoint3D = Point3D<f32, WorldPixel>;
+pub type WorldVector2D = Vector2D<f32, WorldPixel>;
+pub type WorldVector3D = Vector3D<f32, WorldPixel>;
+
+/// Offset in number of tiles.
+#[derive(Hash, Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
+pub struct Tiles;
+pub type TileOffset = Point2D<i32, Tiles>;
+pub type TileRange = Box2D<i32, Tiles>;
+
+/// Scaling ratio from world pixels to device pixels.
+pub type DevicePixelScale = Scale<f32, WorldPixel, DevicePixel>;
+/// Scaling ratio from layout to world. Used for cases where we know the layout
+/// is in world space, or specifically want to treat it this way.
+pub type LayoutToWorldScale = Scale<f32, LayoutPixel, WorldPixel>;
+/// A complete scaling ratio from layout space to device pixel space.
+pub type LayoutToDeviceScale = Scale<f32, LayoutPixel, DevicePixel>;
+
+pub type LayoutTransform = Transform3D<f32, LayoutPixel, LayoutPixel>;
+pub type LayoutToWorldTransform = Transform3D<f32, LayoutPixel, WorldPixel>;
+pub type WorldToLayoutTransform = Transform3D<f32, WorldPixel, LayoutPixel>;
+
+pub type LayoutToPictureTransform = Transform3D<f32, LayoutPixel, PicturePixel>;
+pub type PictureToLayoutTransform = Transform3D<f32, PicturePixel, LayoutPixel>;
+
+pub type LayoutToRasterTransform = Transform3D<f32, LayoutPixel, RasterPixel>;
+pub type RasterToLayoutTransform = Transform3D<f32, RasterPixel, LayoutPixel>;
+
+pub type PictureToRasterTransform = Transform3D<f32, PicturePixel, RasterPixel>;
+pub type RasterToPictureTransform = Transform3D<f32, RasterPixel, PicturePixel>;
+
+/// Scaling ratio from picture pixels to raster pixels (e.g. if scaling a picture surface up/down).
+pub type RasterPixelScale = Scale<f32, PicturePixel, RasterPixel>;
+
+// Fixed position coordinates, to avoid float precision errors.
+pub type LayoutPointAu = Point2D<Au, LayoutPixel>;
+pub type LayoutRectAu = Box2D<Au, LayoutPixel>;
+pub type LayoutSizeAu = Size2D<Au, LayoutPixel>;
+pub type LayoutVector2DAu = Vector2D<Au, LayoutPixel>;
+pub type LayoutSideOffsetsAu = SideOffsets2D<Au, LayoutPixel>;
+
+pub type ImageDirtyRect = DirtyRect<i32, DevicePixel>;
+pub type BlobDirtyRect = DirtyRect<i32, LayoutPixel>;
+
+pub type BlobToDeviceTranslation = Translation2D<i32, LayoutPixel, DevicePixel>;
+
+/// Stores two coordinates in texel space. The coordinates
+/// are stored in texel coordinates because the texture atlas
+/// may grow. Storing them as texel coords and normalizing
+/// the UVs in the vertex shader means nothing needs to be
+/// updated on the CPU when the texture size changes.
+#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct TexelRect {
+ pub uv0: DevicePoint,
+ pub uv1: DevicePoint,
+}
+
+impl TexelRect {
+ pub fn new(u0: f32, v0: f32, u1: f32, v1: f32) -> Self {
+ TexelRect {
+ uv0: DevicePoint::new(u0, v0),
+ uv1: DevicePoint::new(u1, v1),
+ }
+ }
+
+ pub fn invalid() -> Self {
+ TexelRect {
+ uv0: DevicePoint::new(-1.0, -1.0),
+ uv1: DevicePoint::new(-1.0, -1.0),
+ }
+ }
+}
+
+impl Into<TexelRect> for DeviceIntRect {
+ fn into(self) -> TexelRect {
+ TexelRect {
+ uv0: self.min.to_f32(),
+ uv1: self.max.to_f32(),
+ }
+ }
+}
+
+const MAX_AU_FLOAT: f32 = 1.0e6;
+
+pub trait AuHelpers<T> {
+ fn from_au(data: T) -> Self;
+ fn to_au(&self) -> T;
+}
+
+impl AuHelpers<LayoutSizeAu> for LayoutSize {
+ fn from_au(size: LayoutSizeAu) -> Self {
+ LayoutSize::new(
+ size.width.to_f32_px(),
+ size.height.to_f32_px(),
+ )
+ }
+
+ fn to_au(&self) -> LayoutSizeAu {
+ let width = self.width.min(2.0 * MAX_AU_FLOAT);
+ let height = self.height.min(2.0 * MAX_AU_FLOAT);
+
+ LayoutSizeAu::new(
+ Au::from_f32_px(width),
+ Au::from_f32_px(height),
+ )
+ }
+}
+
+impl AuHelpers<LayoutVector2DAu> for LayoutVector2D {
+ fn from_au(size: LayoutVector2DAu) -> Self {
+ LayoutVector2D::new(
+ size.x.to_f32_px(),
+ size.y.to_f32_px(),
+ )
+ }
+
+ fn to_au(&self) -> LayoutVector2DAu {
+ LayoutVector2DAu::new(
+ Au::from_f32_px(self.x),
+ Au::from_f32_px(self.y),
+ )
+ }
+}
+
+impl AuHelpers<LayoutPointAu> for LayoutPoint {
+ fn from_au(point: LayoutPointAu) -> Self {
+ LayoutPoint::new(
+ point.x.to_f32_px(),
+ point.y.to_f32_px(),
+ )
+ }
+
+ fn to_au(&self) -> LayoutPointAu {
+ let x = self.x.min(MAX_AU_FLOAT).max(-MAX_AU_FLOAT);
+ let y = self.y.min(MAX_AU_FLOAT).max(-MAX_AU_FLOAT);
+
+ LayoutPointAu::new(
+ Au::from_f32_px(x),
+ Au::from_f32_px(y),
+ )
+ }
+}
+
+impl AuHelpers<LayoutRectAu> for LayoutRect {
+ fn from_au(rect: LayoutRectAu) -> Self {
+ LayoutRect {
+ min: LayoutPoint::from_au(rect.min),
+ max: LayoutPoint::from_au(rect.max),
+ }
+ }
+
+ fn to_au(&self) -> LayoutRectAu {
+ LayoutRectAu {
+ min: self.min.to_au(),
+ max: self.max.to_au(),
+ }
+ }
+}
+
+impl AuHelpers<LayoutSideOffsetsAu> for LayoutSideOffsets {
+ fn from_au(offsets: LayoutSideOffsetsAu) -> Self {
+ LayoutSideOffsets::new(
+ offsets.top.to_f32_px(),
+ offsets.right.to_f32_px(),
+ offsets.bottom.to_f32_px(),
+ offsets.left.to_f32_px(),
+ )
+ }
+
+ fn to_au(&self) -> LayoutSideOffsetsAu {
+ LayoutSideOffsetsAu::new(
+ Au::from_f32_px(self.top),
+ Au::from_f32_px(self.right),
+ Au::from_f32_px(self.bottom),
+ Au::from_f32_px(self.left),
+ )
+ }
+}
+
+pub trait RectExt {
+ type Point;
+ fn top_left(&self) -> Self::Point;
+ fn top_right(&self) -> Self::Point;
+ fn bottom_left(&self) -> Self::Point;
+ fn bottom_right(&self) -> Self::Point;
+}
+
+impl<U> RectExt for Rect<f32, U> {
+ type Point = Point2D<f32, U>;
+ fn top_left(&self) -> Self::Point {
+ self.min()
+ }
+ fn top_right(&self) -> Self::Point {
+ Point2D::new(self.max_x(), self.min_y())
+ }
+ fn bottom_left(&self) -> Self::Point {
+ Point2D::new(self.min_x(), self.max_y())
+ }
+ fn bottom_right(&self) -> Self::Point {
+ self.max()
+ }
+}
+
+impl<U> RectExt for Box2D<f32, U> {
+ type Point = Point2D<f32, U>;
+ fn top_left(&self) -> Self::Point {
+ self.min
+ }
+ fn top_right(&self) -> Self::Point {
+ Point2D::new(self.max.x, self.min.y)
+ }
+ fn bottom_left(&self) -> Self::Point {
+ Point2D::new(self.min.x, self.max.y)
+ }
+ fn bottom_right(&self) -> Self::Point {
+ self.max
+ }
+}
+
+// A few helpers to convert to cast between coordinate spaces that are often equivalent.
+
+#[inline]
+pub fn layout_rect_as_picture_rect(layout_rect: &LayoutRect) -> PictureRect {
+ layout_rect.cast_unit()
+}
+
+#[inline]
+pub fn layout_vector_as_picture_vector(layout_vector: LayoutVector2D) -> PictureVector2D {
+ layout_vector.cast_unit()
+}
+
+#[inline]
+pub fn device_size_as_framebuffer_size(framebuffer_size: DeviceIntSize) -> FramebufferIntSize {
+ framebuffer_size.cast_unit()
+}
+
+#[inline]
+pub fn device_rect_as_framebuffer_rect(framebuffer_rect: &DeviceIntRect) -> FramebufferIntRect {
+ framebuffer_rect.cast_unit()
+}
diff --git a/gfx/wr/webrender_build/Cargo.toml b/gfx/wr/webrender_build/Cargo.toml
new file mode 100644
index 0000000000..acd50834fd
--- /dev/null
+++ b/gfx/wr/webrender_build/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "webrender_build"
+authors = ["The Servo Project Developers"]
+version = "0.0.2"
+license = "MPL-2.0"
+repository = "https://github.com/servo/webrender"
+description = "Code shared between precompilation (build.rs) and the rest of WebRender"
+edition = "2018"
+
+[features]
+serialize_program = ["serde"]
+
+[dependencies]
+bitflags = "1.2"
+lazy_static = "1"
+serde = { optional = true, version = "1.0", features = ["serde_derive"] }
diff --git a/gfx/wr/webrender_build/src/lib.rs b/gfx/wr/webrender_build/src/lib.rs
new file mode 100644
index 0000000000..23438a478e
--- /dev/null
+++ b/gfx/wr/webrender_build/src/lib.rs
@@ -0,0 +1,19 @@
+/* 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 bitflags;
+
+#[macro_use]
+extern crate lazy_static;
+
+#[cfg(any(feature = "serde"))]
+#[macro_use]
+extern crate serde;
+
+pub mod shader;
+pub mod shader_features;
+
+/// This must be known at build-time as the shaders depend on it.
+pub const MAX_VERTEX_TEXTURE_WIDTH: usize = 1024;
diff --git a/gfx/wr/webrender_build/src/shader.rs b/gfx/wr/webrender_build/src/shader.rs
new file mode 100644
index 0000000000..5db5d738e7
--- /dev/null
+++ b/gfx/wr/webrender_build/src/shader.rs
@@ -0,0 +1,240 @@
+/* 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/. */
+
+//! Functionality for managing source code for shaders.
+//!
+//! This module is used during precompilation (build.rs) and regular compilation,
+//! so it has minimal dependencies.
+
+use std::borrow::Cow;
+use std::fs::File;
+use std::io::Read;
+use std::path::Path;
+use std::collections::HashSet;
+use std::collections::hash_map::DefaultHasher;
+use crate::MAX_VERTEX_TEXTURE_WIDTH;
+
+pub use crate::shader_features::*;
+
+lazy_static! {
+ static ref MAX_VERTEX_TEXTURE_WIDTH_STRING: String = MAX_VERTEX_TEXTURE_WIDTH.to_string();
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum ShaderKind {
+ Vertex,
+ Fragment,
+}
+
+#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
+pub enum ShaderVersion {
+ Gl,
+ Gles,
+}
+
+impl ShaderVersion {
+ /// Return the full variant name, for use in code generation.
+ pub fn variant_name(&self) -> &'static str {
+ match self {
+ ShaderVersion::Gl => "ShaderVersion::Gl",
+ ShaderVersion::Gles => "ShaderVersion::Gles",
+ }
+ }
+}
+
+#[derive(PartialEq, Eq, Hash, Debug, Clone, Default)]
+#[cfg_attr(feature = "serialize_program", derive(Deserialize, Serialize))]
+pub struct ProgramSourceDigest(u64);
+
+impl ::std::fmt::Display for ProgramSourceDigest {
+ fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
+ write!(f, "{:02x}", self.0)
+ }
+}
+
+impl From<DefaultHasher> for ProgramSourceDigest {
+ fn from(hasher: DefaultHasher) -> Self {
+ use std::hash::Hasher;
+ ProgramSourceDigest(hasher.finish())
+ }
+}
+
+const SHADER_IMPORT: &str = "#include ";
+
+pub struct ShaderSourceParser {
+ included: HashSet<String>,
+}
+
+impl ShaderSourceParser {
+ pub fn new() -> Self {
+ ShaderSourceParser {
+ included: HashSet::new(),
+ }
+ }
+
+ /// Parses a shader string for imports. Imports are recursively processed, and
+ /// prepended to the output stream.
+ pub fn parse<F: FnMut(&str), G: Fn(&str) -> Cow<'static, str>>(
+ &mut self,
+ source: Cow<'static, str>,
+ get_source: &G,
+ output: &mut F,
+ ) {
+ for line in source.lines() {
+ if line.starts_with(SHADER_IMPORT) {
+ let imports = line[SHADER_IMPORT.len() ..].split(',');
+
+ // For each import, get the source, and recurse.
+ for import in imports {
+ if self.included.insert(import.into()) {
+ let include = get_source(import);
+ self.parse(include, get_source, output);
+ } else {
+ output(&format!("// {} is already included\n", import));
+ }
+ }
+ } else {
+ output(line);
+ output("\n");
+ }
+ }
+ }
+}
+
+/// Reads a shader source file from disk into a String.
+pub fn shader_source_from_file(shader_path: &Path) -> String {
+ assert!(shader_path.exists(), "Shader not found {:?}", shader_path);
+ let mut source = String::new();
+ File::open(&shader_path)
+ .expect("Shader not found")
+ .read_to_string(&mut source)
+ .unwrap();
+ source
+}
+
+/// Creates heap-allocated strings for both vertex and fragment shaders.
+pub fn build_shader_strings<G: Fn(&str) -> Cow<'static, str>>(
+ gl_version: ShaderVersion,
+ features: &[&str],
+ base_filename: &str,
+ get_source: &G,
+) -> (String, String) {
+ let mut vs_source = String::new();
+ do_build_shader_string(
+ gl_version,
+ features,
+ ShaderKind::Vertex,
+ base_filename,
+ get_source,
+ |s| vs_source.push_str(s),
+ );
+
+ let mut fs_source = String::new();
+ do_build_shader_string(
+ gl_version,
+ features,
+ ShaderKind::Fragment,
+ base_filename,
+ get_source,
+ |s| fs_source.push_str(s),
+ );
+
+ (vs_source, fs_source)
+}
+
+/// Walks the given shader string and applies the output to the provided
+/// callback. Assuming an override path is not used, does no heap allocation
+/// and no I/O.
+pub fn do_build_shader_string<F: FnMut(&str), G: Fn(&str) -> Cow<'static, str>>(
+ gl_version: ShaderVersion,
+ features: &[&str],
+ kind: ShaderKind,
+ base_filename: &str,
+ get_source: &G,
+ mut output: F,
+) {
+ build_shader_prefix_string(gl_version, features, kind, base_filename, &mut output);
+ build_shader_main_string(base_filename, get_source, &mut output);
+}
+
+/// Walks the prefix section of the shader string, which manages the various
+/// defines for features etc.
+pub fn build_shader_prefix_string<F: FnMut(&str)>(
+ gl_version: ShaderVersion,
+ features: &[&str],
+ kind: ShaderKind,
+ base_filename: &str,
+ output: &mut F,
+) {
+ // GLSL requires that the version number comes first.
+ let gl_version_string = match gl_version {
+ ShaderVersion::Gl => "#version 150\n",
+ ShaderVersion::Gles if features.contains(&"TEXTURE_EXTERNAL_ESSL1") => "#version 100\n",
+ ShaderVersion::Gles => "#version 300 es\n",
+ };
+ output(gl_version_string);
+
+ // Insert the shader name to make debugging easier.
+ output("// shader: ");
+ output(base_filename);
+ output(" ");
+ for (i, feature) in features.iter().enumerate() {
+ output(feature);
+ if i != features.len() - 1 {
+ output(",");
+ }
+ }
+ output("\n");
+
+ // Define a constant depending on whether we are compiling VS or FS.
+ let kind_string = match kind {
+ ShaderKind::Vertex => "#define WR_VERTEX_SHADER\n",
+ ShaderKind::Fragment => "#define WR_FRAGMENT_SHADER\n",
+ };
+ output(kind_string);
+
+ // detect which platform we're targeting
+ let is_macos = match std::env::var("CARGO_CFG_TARGET_OS") {
+ Ok(os) => os == "macos",
+ // if this is not called from build.rs (e.g. the gpu_cache_update shader or
+ // if the optimized shader pref is disabled) we want to use the runtime value
+ Err(_) => cfg!(target_os = "macos"),
+ };
+ let is_android = match std::env::var("CARGO_CFG_TARGET_OS") {
+ Ok(os) => os == "android",
+ Err(_) => cfg!(target_os = "android"),
+ };
+ if is_macos {
+ output("#define PLATFORM_MACOS\n");
+ } else if is_android {
+ output("#define PLATFORM_ANDROID\n");
+ }
+
+ // Define a constant for the vertex texture width.
+ output("#define WR_MAX_VERTEX_TEXTURE_WIDTH ");
+ output(&MAX_VERTEX_TEXTURE_WIDTH_STRING);
+ output("U\n");
+
+ // Add any defines for features that were passed by the caller.
+ for feature in features {
+ assert!(!feature.is_empty());
+ output("#define WR_FEATURE_");
+ output(feature);
+ output("\n");
+ }
+}
+
+/// Walks the main .glsl file, including any imports.
+pub fn build_shader_main_string<F: FnMut(&str), G: Fn(&str) -> Cow<'static, str>>(
+ base_filename: &str,
+ get_source: &G,
+ output: &mut F,
+) {
+ let shared_source = get_source(base_filename);
+ ShaderSourceParser::new().parse(
+ shared_source,
+ &|f| get_source(f),
+ output
+ );
+}
diff --git a/gfx/wr/webrender_build/src/shader_features.rs b/gfx/wr/webrender_build/src/shader_features.rs
new file mode 100644
index 0000000000..db16cdf703
--- /dev/null
+++ b/gfx/wr/webrender_build/src/shader_features.rs
@@ -0,0 +1,240 @@
+/* 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::collections::HashMap;
+
+bitflags! {
+ #[derive(Default)]
+ pub struct ShaderFeatureFlags: u32 {
+ const GL = 1 << 0;
+ const GLES = 1 << 1;
+
+ const ADVANCED_BLEND_EQUATION = 1 << 8;
+ const DUAL_SOURCE_BLENDING = 1 << 9;
+ const DITHERING = 1 << 10;
+ const TEXTURE_EXTERNAL = 1 << 11;
+ const TEXTURE_EXTERNAL_ESSL1 = 1 << 12;
+ const DEBUG = 1 << 13;
+ }
+}
+
+pub type ShaderFeatures = HashMap<&'static str, Vec<String>>;
+
+/// Builder for a list of features.
+#[derive(Clone)]
+struct FeatureList<'a> {
+ list: Vec<&'a str>,
+}
+
+impl<'a> FeatureList<'a> {
+ fn new() -> Self {
+ FeatureList {
+ list: Vec::new(),
+ }
+ }
+
+ fn add(&mut self, feature: &'a str) {
+ assert!(!feature.contains(','));
+ self.list.push(feature);
+ }
+
+ fn with(&self, feature: &'a str) -> Self {
+ let mut other = self.clone();
+ other.add(feature);
+ other
+ }
+
+ fn concat(&self, other: &Self) -> Self {
+ let mut list = self.list.clone();
+ list.extend_from_slice(&other.list);
+ FeatureList {
+ list
+ }
+ }
+
+ fn finish(&mut self) -> String {
+ self.list.sort_unstable();
+ self.list.join(",")
+ }
+}
+
+/// Computes available shaders and their features for the given feature flags.
+pub fn get_shader_features(flags: ShaderFeatureFlags) -> ShaderFeatures {
+ let mut shaders = ShaderFeatures::new();
+
+ // Clip shaders
+ shaders.insert("cs_clip_rectangle", vec![String::new(), "FAST_PATH".to_string()]);
+ shaders.insert("cs_clip_image", vec!["TEXTURE_2D".to_string()]);
+ shaders.insert("cs_clip_box_shadow", vec!["TEXTURE_2D".to_string()]);
+
+ // Cache shaders
+ shaders.insert("cs_blur", vec!["ALPHA_TARGET".to_string(), "COLOR_TARGET".to_string()]);
+
+ shaders.insert("ps_quad_mask", vec![String::new(), "FAST_PATH".to_string()]);
+
+ for name in &[
+ "cs_line_decoration",
+ "cs_fast_linear_gradient",
+ "cs_border_segment",
+ "cs_border_solid",
+ "cs_svg_filter",
+ ] {
+ shaders.insert(name, vec![String::new()]);
+ }
+
+ for name in &[
+ "cs_linear_gradient",
+ "cs_radial_gradient",
+ "cs_conic_gradient",
+ ] {
+ let mut features = Vec::new();
+ features.push(String::new());
+ if flags.contains(ShaderFeatureFlags::DITHERING) {
+ features.push("DITHERING".to_string());
+ }
+ shaders.insert(name, features);
+ }
+
+ let mut base_prim_features = FeatureList::new();
+
+ // Brush shaders
+ let mut brush_alpha_features = base_prim_features.with("ALPHA_PASS");
+ for name in &["brush_solid", "brush_blend", "brush_mix_blend"] {
+ let mut features: Vec<String> = Vec::new();
+ features.push(base_prim_features.finish());
+ features.push(brush_alpha_features.finish());
+ features.push("DEBUG_OVERDRAW".to_string());
+ shaders.insert(name, features);
+ }
+ for name in &["brush_linear_gradient"] {
+ let mut features: Vec<String> = Vec::new();
+ let mut list = FeatureList::new();
+ if flags.contains(ShaderFeatureFlags::DITHERING) {
+ list.add("DITHERING");
+ }
+ features.push(list.concat(&base_prim_features).finish());
+ features.push(list.concat(&brush_alpha_features).finish());
+ features.push(list.with("DEBUG_OVERDRAW").finish());
+ shaders.insert(name, features);
+ }
+
+ {
+ let mut features: Vec<String> = Vec::new();
+ features.push(base_prim_features.finish());
+ features.push(brush_alpha_features.finish());
+ features.push(base_prim_features.with("ANTIALIASING").finish());
+ features.push(brush_alpha_features.with("ANTIALIASING").finish());
+ features.push("ANTIALIASING,DEBUG_OVERDRAW".to_string());
+ features.push("DEBUG_OVERDRAW".to_string());
+ shaders.insert("brush_opacity", features);
+ }
+
+ // Image brush shaders
+ let mut texture_types = vec!["TEXTURE_2D"];
+ if flags.contains(ShaderFeatureFlags::GL) {
+ texture_types.push("TEXTURE_RECT");
+ }
+ if flags.contains(ShaderFeatureFlags::TEXTURE_EXTERNAL) {
+ texture_types.push("TEXTURE_EXTERNAL");
+ }
+ let mut image_features: Vec<String> = Vec::new();
+ for texture_type in &texture_types {
+ let mut fast = FeatureList::new();
+ if !texture_type.is_empty() {
+ fast.add(texture_type);
+ }
+ image_features.push(fast.concat(&base_prim_features).finish());
+ image_features.push(fast.concat(&brush_alpha_features).finish());
+ image_features.push(fast.with("DEBUG_OVERDRAW").finish());
+ let mut slow = fast.clone();
+ slow.add("REPETITION");
+ slow.add("ANTIALIASING");
+ image_features.push(slow.concat(&base_prim_features).finish());
+ image_features.push(slow.concat(&brush_alpha_features).finish());
+ image_features.push(slow.with("DEBUG_OVERDRAW").finish());
+ if flags.contains(ShaderFeatureFlags::ADVANCED_BLEND_EQUATION) {
+ let advanced_blend_features = brush_alpha_features.with("ADVANCED_BLEND");
+ image_features.push(fast.concat(&advanced_blend_features).finish());
+ image_features.push(slow.concat(&advanced_blend_features).finish());
+ }
+ if flags.contains(ShaderFeatureFlags::DUAL_SOURCE_BLENDING) {
+ let dual_source_features = brush_alpha_features.with("DUAL_SOURCE_BLENDING");
+ image_features.push(fast.concat(&dual_source_features).finish());
+ image_features.push(slow.concat(&dual_source_features).finish());
+ }
+ }
+ shaders.insert("brush_image", image_features);
+
+ let mut composite_texture_types = texture_types.clone();
+ if flags.contains(ShaderFeatureFlags::TEXTURE_EXTERNAL_ESSL1) {
+ composite_texture_types.push("TEXTURE_EXTERNAL_ESSL1");
+ }
+ let mut composite_features: Vec<String> = Vec::new();
+ for texture_type in &composite_texture_types {
+ let base = texture_type.to_string();
+ composite_features.push(base);
+ }
+ shaders.insert("cs_scale", composite_features.clone());
+
+ // YUV image brush and composite shaders
+ let mut yuv_features: Vec<String> = Vec::new();
+ for texture_type in &texture_types {
+ let mut list = FeatureList::new();
+ if !texture_type.is_empty() {
+ list.add(texture_type);
+ }
+ list.add("YUV");
+ composite_features.push(list.finish());
+ yuv_features.push(list.concat(&base_prim_features).finish());
+ yuv_features.push(list.concat(&brush_alpha_features).finish());
+ yuv_features.push(list.with("DEBUG_OVERDRAW").finish());
+ }
+ shaders.insert("brush_yuv_image", yuv_features);
+
+ // Fast path composite shaders
+ for texture_type in &composite_texture_types {
+ let mut list = FeatureList::new();
+ if !texture_type.is_empty() {
+ list.add(texture_type);
+ }
+ list.add("FAST_PATH");
+ composite_features.push(list.finish());
+ }
+ shaders.insert("composite", composite_features);
+
+ // Prim shaders
+ let mut text_types = vec![""];
+ if flags.contains(ShaderFeatureFlags::DUAL_SOURCE_BLENDING) {
+ text_types.push("DUAL_SOURCE_BLENDING");
+ }
+ let mut text_features: Vec<String> = Vec::new();
+ for text_type in &text_types {
+ let mut list = base_prim_features.with("TEXTURE_2D");
+ if !text_type.is_empty() {
+ list.add(text_type);
+ }
+ let mut alpha_list = list.with("ALPHA_PASS");
+ text_features.push(alpha_list.finish());
+ text_features.push(alpha_list.with("GLYPH_TRANSFORM").finish());
+ text_features.push(list.with("DEBUG_OVERDRAW").finish());
+ }
+ shaders.insert("ps_text_run", text_features);
+
+ shaders.insert("ps_split_composite", vec![base_prim_features.finish()]);
+
+ shaders.insert("ps_quad_textured", vec![base_prim_features.finish()]);
+
+ shaders.insert("ps_clear", vec![base_prim_features.finish()]);
+
+ shaders.insert("ps_copy", vec![base_prim_features.finish()]);
+
+ if flags.contains(ShaderFeatureFlags::DEBUG) {
+ for name in &["debug_color", "debug_font"] {
+ shaders.insert(name, vec![String::new()]);
+ }
+ }
+
+ shaders
+}
+
diff --git a/gfx/wr/wr_glyph_rasterizer/Cargo.toml b/gfx/wr/wr_glyph_rasterizer/Cargo.toml
new file mode 100644
index 0000000000..de53902ec5
--- /dev/null
+++ b/gfx/wr/wr_glyph_rasterizer/Cargo.toml
@@ -0,0 +1,51 @@
+[package]
+name = "wr_glyph_rasterizer"
+version = "0.1.0"
+authors = ["The Mozilla Project Developers"]
+description = "A glyph rasterizer for WebRender"
+license = "MPL-2.0"
+edition = "2018"
+
+[features]
+default = ["static_freetype"]
+dynamic_freetype = []
+static_freetype = ["freetype/freetype-sys"]
+capture = ["api/serialize", "serde", "smallvec/serde"]
+replay = ["api/deserialize", "serde", "smallvec/serde"]
+gecko = ["firefox-on-glean", "glean"]
+
+[dependencies]
+api = { version = "0.62.0", path = "../webrender_api", package = "webrender_api" }
+euclid = { version = "0.22.0", features = ["serde"] }
+malloc_size_of = { version = "0.0.2", path = "../wr_malloc_size_of", package = "wr_malloc_size_of" }
+malloc_size_of_derive = "0.1"
+rayon = "1"
+smallvec = "1"
+tracy-rs = "0.1.2"
+log = "0.4"
+lazy_static = "1"
+fxhash = "0.2.1"
+glean = { version = "52.7.0", optional = true }
+firefox-on-glean = { version = "0.1.0", optional = true }
+serde = { optional = true, version = "1.0", features = ["serde_derive"] }
+
+[dev-dependencies]
+env_logger = { version = "0.10", default_features = false }
+euclid = "0.22"
+gleam = "0.15"
+glutin = "0.28"
+rayon = "1"
+winit = "0.26"
+
+[target.'cfg(any(target_os = "android", all(unix, not(target_os = "macos"))))'.dependencies]
+freetype = { version = "0.7", default-features = false }
+libc = "0.2"
+
+[target.'cfg(target_os = "windows")'.dependencies]
+dwrote = "0.11"
+
+[target.'cfg(target_os = "macos")'.dependencies]
+core-foundation = "0.9.2"
+core-graphics = "0.22.3"
+core-text = { version = "19.2", default-features = false }
+objc = "0.2"
diff --git a/gfx/wr/wr_glyph_rasterizer/README.md b/gfx/wr/wr_glyph_rasterizer/README.md
new file mode 100644
index 0000000000..0c23554e44
--- /dev/null
+++ b/gfx/wr/wr_glyph_rasterizer/README.md
@@ -0,0 +1,3 @@
+# A glyph rasterizer for webrender
+
+## Benchmark
diff --git a/gfx/wr/wr_glyph_rasterizer/examples/basic.rs b/gfx/wr/wr_glyph_rasterizer/examples/basic.rs
new file mode 100644
index 0000000000..4c46004cdf
--- /dev/null
+++ b/gfx/wr/wr_glyph_rasterizer/examples/basic.rs
@@ -0,0 +1,177 @@
+/* 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::sync::Arc;
+use std::mem;
+
+use api::{
+ IdNamespace, FontTemplate, FontKey, FontInstanceKey, FontInstanceOptions,
+ FontInstancePlatformOptions, ColorF, FontInstanceFlags, units::DevicePoint,
+};
+use glutin::ContextBuilder;
+use glutin::dpi::PhysicalSize;
+use glutin::event::{Event, WindowEvent};
+use glutin::event_loop::{ControlFlow, EventLoop};
+use glutin::window::WindowBuilder;
+use rayon::ThreadPoolBuilder;
+use wr_glyph_rasterizer::RasterizedGlyph;
+use wr_glyph_rasterizer::{
+ SharedFontResources, BaseFontInstance, GlyphRasterizer, FontInstance, GlyphKey,
+ SubpixelDirection, profiler::GlyphRasterizeProfiler,
+};
+
+#[path = "common/boilerplate.rs"]
+mod boilerplate;
+
+struct Profiler;
+
+impl GlyphRasterizeProfiler for Profiler {
+ fn start_time(&mut self) {}
+ fn end_time(&mut self) -> f64 {
+ 0.
+ }
+ fn set(&mut self, _value: f64) {}
+}
+
+fn load_glyphs() -> Vec<RasterizedGlyph> {
+ let namespace = IdNamespace(0);
+ let mut fonts = SharedFontResources::new(namespace);
+
+ let font_key = FontKey::new(namespace, 0);
+ let raw_font_data = include_bytes!("../../wrench/reftests/text/FreeSans.ttf");
+ let font_template = FontTemplate::Raw(Arc::new(raw_font_data.to_vec()), 0);
+ let shared_font_key = fonts
+ .font_keys
+ .add_key(&font_key, &font_template)
+ .expect("Failed to add font key");
+
+ let font_instance_key = FontInstanceKey::new(namespace, 1);
+ fonts.templates.add_font(shared_font_key, font_template);
+ assert!(fonts.templates.has_font(&shared_font_key));
+
+ // AddFontInstance will only be processed here, not in the resource cache, so it
+ // is safe to take the options rather than clone them.
+ let base = BaseFontInstance::new(
+ font_instance_key,
+ shared_font_key,
+ 32.,
+ mem::take(&mut Some(FontInstanceOptions::default())),
+ mem::take(&mut Some(FontInstancePlatformOptions::default())),
+ mem::take(&mut Vec::new()),
+ );
+ let shared_instance = fonts
+ .instance_keys
+ .add_key(base)
+ .expect("Failed to add font instance key");
+ fonts.instances.add_font_instance(shared_instance);
+
+ let workers = {
+ let worker = ThreadPoolBuilder::new()
+ .thread_name(|idx| format!("WRWorker#{}", idx))
+ .build();
+ Arc::new(worker.unwrap())
+ };
+ let mut glyph_rasterizer = GlyphRasterizer::new(workers, false);
+
+ glyph_rasterizer.add_font(
+ shared_font_key,
+ fonts
+ .templates
+ .get_font(&shared_font_key)
+ .expect("Could not find FontTemplate"),
+ );
+
+ let mut font = FontInstance::new(
+ fonts
+ .instances
+ .get_font_instance(font_instance_key)
+ .expect("Could not found BaseFontInstance"),
+ ColorF::BLACK.into(),
+ api::FontRenderMode::Alpha,
+ FontInstanceFlags::SUBPIXEL_POSITION | FontInstanceFlags::FONT_SMOOTHING,
+ );
+
+ glyph_rasterizer.prepare_font(&mut font);
+
+ let glyph_keys = [
+ glyph_rasterizer
+ .get_glyph_index(shared_font_key, 'A')
+ .expect("Failed to get glyph index"),
+ glyph_rasterizer
+ .get_glyph_index(shared_font_key, 'B')
+ .expect("Failed to get glyph index"),
+ glyph_rasterizer
+ .get_glyph_index(shared_font_key, 'C')
+ .expect("Failed to get glyph index"),
+ ];
+
+ let glyph_keys = glyph_keys.map(|g| {
+ GlyphKey::new(
+ g,
+ DevicePoint::new(100., 100.),
+ SubpixelDirection::Horizontal,
+ )
+ });
+
+ glyph_rasterizer.request_glyphs(font, &glyph_keys, |_| true);
+
+ let mut glyphs = vec![];
+ glyph_rasterizer.resolve_glyphs(
+ |job, _| {
+ if let Ok(glyph) = job.result {
+ glyphs.push(glyph);
+ }
+ },
+ &mut Profiler,
+ );
+
+ glyphs
+}
+
+fn main() {
+ let glyphs = load_glyphs();
+
+ let el = EventLoop::new();
+ let wb = WindowBuilder::new()
+ .with_title("A fantastic window!")
+ .with_inner_size(PhysicalSize::new(1900. as f64, 1300. as f64));
+
+ let windowed_context = ContextBuilder::new()
+ .with_gl(glutin::GlRequest::GlThenGles {
+ opengl_version: (3, 2),
+ opengles_version: (3, 0),
+ })
+ .build_windowed(wb, &el)
+ .unwrap();
+
+ let windowed_context = unsafe { windowed_context.make_current().unwrap() };
+
+ let gl = boilerplate::load(windowed_context.context(), glyphs);
+
+ el.run(move |event, _, control_flow| {
+ *control_flow = ControlFlow::Wait;
+
+ match event {
+ Event::LoopDestroyed => (),
+ Event::WindowEvent { event, .. } => match event {
+ WindowEvent::Resized(physical_size) => windowed_context.resize(physical_size),
+ WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
+ _ => (),
+ },
+ Event::RedrawRequested(_) => {
+ let size = windowed_context.window().inner_size();
+ let scale_factor = windowed_context.window().scale_factor();
+ gl.draw_frame(
+ size.width as f32,
+ size.height as f32,
+ [0., 0., 0., 1.0],
+ [1., 1., 1., 1.0],
+ scale_factor as f32,
+ );
+ windowed_context.swap_buffers().unwrap();
+ }
+ _ => (),
+ }
+ });
+}
diff --git a/gfx/wr/wr_glyph_rasterizer/examples/common/boilerplate.rs b/gfx/wr/wr_glyph_rasterizer/examples/common/boilerplate.rs
new file mode 100644
index 0000000000..9590f92b4c
--- /dev/null
+++ b/gfx/wr/wr_glyph_rasterizer/examples/common/boilerplate.rs
@@ -0,0 +1,298 @@
+/* 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 api::ImageFormat;
+use euclid::{Transform3D, UnknownUnit};
+use glutin::{self, PossiblyCurrent};
+use gleam::gl;
+use wr_glyph_rasterizer::{RasterizedGlyph, GlyphFormat};
+
+use std::{ffi::CStr, rc::Rc};
+
+#[allow(unused)]
+pub struct Gl {
+ pub gl: Rc<dyn gl::Gl>,
+ program: gl::GLuint,
+ vb: gl::GLuint,
+ vao: gl::GLuint,
+ vs: gl::GLuint,
+ fs: gl::GLuint,
+ textures: Vec<gl::GLuint>,
+ u_transform: i32,
+ u_text_color: i32,
+ u_sampler_color: i32,
+ glyphs: Vec<RasterizedGlyph>,
+}
+
+pub fn load(gl_context: &glutin::Context<PossiblyCurrent>, glyphs: Vec<RasterizedGlyph>) -> Gl {
+ env_logger::init();
+
+ #[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(),
+ );
+ }
+
+ let gl = match gl_context.get_api() {
+ glutin::Api::OpenGl => unsafe {
+ gl::GlFns::load_with(|symbol| gl_context.get_proc_address(symbol) as *const _)
+ },
+ glutin::Api::OpenGlEs => unsafe {
+ gl::GlesFns::load_with(|symbol| gl_context.get_proc_address(symbol) as *const _)
+ },
+ glutin::Api::WebGl => unimplemented!(),
+ };
+
+ let version = unsafe {
+ let data = CStr::from_ptr(gl.get_string(gl::VERSION).as_ptr() as *const _)
+ .to_bytes()
+ .to_vec();
+ String::from_utf8(data).unwrap()
+ };
+
+ println!("OpenGL version {}", version);
+
+ let vs = gl.create_shader(gl::VERTEX_SHADER);
+ gl.shader_source(vs, &[VS_SRC]);
+ gl.compile_shader(vs);
+
+ let fs = gl.create_shader(gl::FRAGMENT_SHADER);
+ gl.shader_source(fs, &[FS_SRC]);
+ gl.compile_shader(fs);
+
+ let program = gl.create_program();
+ gl.attach_shader(program, vs);
+ gl.attach_shader(program, fs);
+ gl.link_program(program);
+ gl.use_program(program);
+
+ let vb = gl.gen_buffers(1)[0];
+ gl.bind_buffer(gl::ARRAY_BUFFER, vb);
+ gl.buffer_data_untyped(
+ gl::ARRAY_BUFFER,
+ (6 * 4 * std::mem::size_of::<f32>()) as gl::types::GLsizeiptr,
+ std::ptr::null(),
+ gl::DYNAMIC_DRAW,
+ );
+
+ let vao = gl.gen_vertex_arrays(1)[0];
+ gl.bind_vertex_array(vao);
+
+ let u_transform = gl.get_uniform_location(program, "uTransform");
+ let u_text_color = gl.get_uniform_location(program, "uTextColor");
+ let u_sampler_color = gl.get_uniform_location(program, "uSamplerColor");
+ let i_position = gl.get_attrib_location(program, "iPosition");
+ let i_tex_coords = gl.get_attrib_location(program, "iTexCoords");
+ gl.vertex_attrib_pointer(
+ i_position as gl::types::GLuint,
+ 2,
+ gl::FLOAT,
+ false,
+ 4 * std::mem::size_of::<f32>() as gl::types::GLsizei,
+ 0,
+ );
+ gl.vertex_attrib_pointer(
+ i_tex_coords as gl::types::GLuint,
+ 2,
+ gl::FLOAT,
+ false,
+ 4 * std::mem::size_of::<f32>() as gl::types::GLsizei,
+ (2 * std::mem::size_of::<f32>()) as gl::types::GLuint,
+ );
+ gl.enable_vertex_attrib_array(i_position as gl::types::GLuint);
+ gl.enable_vertex_attrib_array(i_tex_coords as gl::types::GLuint);
+
+ let textures = create_texture(&gl, &glyphs);
+
+ gl.bind_buffer(gl::ARRAY_BUFFER, 0);
+ gl.bind_vertex_array(0);
+
+ unsafe { log_shader(&gl, vs) };
+ unsafe { log_shader(&gl, fs) };
+
+ Gl {
+ gl,
+ program,
+ vb,
+ vao,
+ u_transform,
+ u_text_color,
+ u_sampler_color,
+ glyphs,
+ textures,
+ vs,
+ fs,
+ }
+}
+
+fn create_texture(gl: &Rc<dyn gl::Gl>, glyphs: &[RasterizedGlyph]) -> Vec<gl::GLuint> {
+ let textures = gl.gen_textures(glyphs.len() as gl::types::GLsizei);
+ for (i, glyph) in glyphs.iter().enumerate() {
+ let (internal_format, external_format) = get_texture_format(&glyph.format);
+ let texture = textures[i];
+ gl.bind_texture(gl::TEXTURE_2D, texture);
+ gl.tex_parameter_i(
+ gl::TEXTURE_2D,
+ gl::TEXTURE_MAG_FILTER,
+ gl::LINEAR as gl::GLint,
+ );
+ gl.tex_parameter_i(
+ gl::TEXTURE_2D,
+ gl::TEXTURE_MIN_FILTER,
+ gl::LINEAR as gl::GLint,
+ );
+ gl.tex_parameter_i(
+ gl::TEXTURE_2D,
+ gl::TEXTURE_WRAP_S,
+ gl::CLAMP_TO_EDGE as gl::GLint,
+ );
+ gl.tex_parameter_i(
+ gl::TEXTURE_2D,
+ gl::TEXTURE_WRAP_T,
+ gl::CLAMP_TO_EDGE as gl::GLint,
+ );
+ // TODO: use tex_storage_2d
+ gl.tex_image_2d(
+ gl::TEXTURE_2D,
+ 0,
+ internal_format as gl::GLint,
+ glyph.width,
+ glyph.height,
+ 0,
+ external_format,
+ gl::UNSIGNED_BYTE,
+ Some(&glyph.bytes),
+ );
+
+ gl.pixel_store_i(gl::UNPACK_ALIGNMENT, 1);
+ gl.enable(gl::BLEND);
+ gl.blend_func(gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA);
+ }
+
+ textures
+}
+
+fn get_texture_format(format: &GlyphFormat) -> (gl::GLuint, gl::GLuint) {
+ match format.image_format(false) {
+ ImageFormat::BGRA8 => (gl::RGBA, gl::BGRA),
+ _ => unimplemented!(),
+ }
+}
+
+unsafe fn log_shader(gl: &Rc<dyn gl::Gl>, shader: gl::GLuint) {
+ let log = gl.get_shader_info_log(shader);
+ if log.len() != 0 {
+ println!("[ERROR] {}", log);
+ }
+}
+
+impl Gl {
+ pub fn draw_frame(
+ &self,
+ width: f32,
+ height: f32,
+ text_color: [f32; 4],
+ background_color: [f32; 4],
+ scale_factor: f32,
+ ) {
+ let projection: Transform3D<f32, UnknownUnit, UnknownUnit> =
+ Transform3D::ortho(0., width, height, 0., -1., 1.);
+ self.gl
+ .uniform_matrix_4fv(self.u_transform, false, &projection.to_array());
+ self.gl.uniform_4fv(self.u_text_color, &text_color);
+
+ self.gl.active_texture(gl::TEXTURE0);
+
+ self.gl.bind_vertex_array(self.vao);
+
+ self.gl.clear_color(
+ background_color[0],
+ background_color[1],
+ background_color[2],
+ background_color[3],
+ );
+ self.gl.clear(gl::COLOR_BUFFER_BIT);
+
+ let mut ax = 0.;
+ for (i, glyph) in self.glyphs.iter().enumerate() {
+ let texture = self.textures[i];
+
+ let x = ax + glyph.left;
+ let y = glyph.top;
+
+ let w = (glyph.width as f32) * scale_factor;
+ let h = (glyph.height as f32) * scale_factor;
+
+ #[rustfmt::skip]
+ let vertices = [
+ x, y, 0.0, 0.0,
+ x, y + h, 0.0, 1.0,
+ x + w, y + h, 1.0, 1.0,
+
+ x, y, 0.0, 0.0,
+ x + w, y + h, 1.0, 1.0,
+ x + w, y, 1.0, 0.0
+ ];
+
+ self.gl.uniform_1i(self.u_sampler_color, 0);
+ self.gl.bind_texture(gl::TEXTURE_2D, texture);
+ self.gl.bind_buffer(gl::ARRAY_BUFFER, self.vb);
+ self.gl.buffer_data_untyped(
+ gl::ARRAY_BUFFER,
+ (vertices.len() * std::mem::size_of::<f32>()) as gl::GLsizeiptr,
+ vertices.as_ptr() as *const _,
+ gl::DYNAMIC_DRAW,
+ );
+ self.gl.bind_buffer(gl::ARRAY_BUFFER, 0);
+
+ self.gl.draw_arrays(gl::TRIANGLES, 0, 6);
+
+ ax += (glyph.left * scale_factor) + (glyph.width as f32 * scale_factor);
+ }
+ self.gl.bind_vertex_array(0);
+ self.gl.bind_texture(gl::TEXTURE_2D, 0);
+
+ unsafe {
+ log_shader(&self.gl, self.vs);
+ log_shader(&self.gl, self.fs);
+ };
+ }
+}
+
+const VS_SRC: &[u8] = b"
+#version 150
+
+in vec2 iPosition;
+in vec2 iTexCoords;
+
+uniform mat4 uTransform;
+
+out vec2 vColorTexCoord;
+
+void main() {
+ gl_Position = uTransform * vec4(iPosition, 0.0, 1.0);
+ vColorTexCoord = iTexCoords;
+}
+\0";
+
+const FS_SRC: &[u8] = b"
+#version 150
+
+in vec2 vColorTexCoord;
+
+uniform sampler2D uSamplerColor;
+uniform vec4 uTextColor;
+
+out vec4 oFragmentColor;
+
+void main() {
+ vec4 alpha = vec4(1.0, 1.0, 1.0, texture(uSamplerColor, vColorTexCoord).r);
+ oFragmentColor = uTextColor * alpha;
+}
+\0";
diff --git a/gfx/wr/wr_glyph_rasterizer/src/gamma_lut.rs b/gfx/wr/wr_glyph_rasterizer/src/gamma_lut.rs
new file mode 100644
index 0000000000..15075bacbf
--- /dev/null
+++ b/gfx/wr/wr_glyph_rasterizer/src/gamma_lut.rs
@@ -0,0 +1,413 @@
+/* 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/. */
+
+/*!
+Gamma correction lookup tables.
+
+This is a port of Skia gamma LUT logic into Rust, used by WebRender.
+*/
+//#![warn(missing_docs)] //TODO
+#![allow(dead_code)]
+
+use api::ColorU;
+use std::cmp::max;
+
+/// Color space responsible for converting between lumas and luminances.
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum LuminanceColorSpace {
+ /// Linear space - no conversion involved.
+ Linear,
+ /// Simple gamma space - uses the `luminance ^ gamma` function.
+ Gamma(f32),
+ /// Srgb space.
+ Srgb,
+}
+
+impl LuminanceColorSpace {
+ pub fn new(gamma: f32) -> LuminanceColorSpace {
+ if gamma == 1.0 {
+ LuminanceColorSpace::Linear
+ } else if gamma == 0.0 {
+ LuminanceColorSpace::Srgb
+ } else {
+ LuminanceColorSpace::Gamma(gamma)
+ }
+ }
+
+ pub fn to_luma(&self, luminance: f32) -> f32 {
+ match *self {
+ LuminanceColorSpace::Linear => luminance,
+ LuminanceColorSpace::Gamma(gamma) => luminance.powf(gamma),
+ LuminanceColorSpace::Srgb => {
+ //The magic numbers are derived from the sRGB specification.
+ //See http://www.color.org/chardata/rgb/srgb.xalter .
+ if luminance <= 0.04045 {
+ luminance / 12.92
+ } else {
+ ((luminance + 0.055) / 1.055).powf(2.4)
+ }
+ }
+ }
+ }
+
+ pub fn from_luma(&self, luma: f32) -> f32 {
+ match *self {
+ LuminanceColorSpace::Linear => luma,
+ LuminanceColorSpace::Gamma(gamma) => luma.powf(1. / gamma),
+ LuminanceColorSpace::Srgb => {
+ //The magic numbers are derived from the sRGB specification.
+ //See http://www.color.org/chardata/rgb/srgb.xalter .
+ if luma <= 0.0031308 {
+ luma * 12.92
+ } else {
+ 1.055 * luma.powf(1./2.4) - 0.055
+ }
+ }
+ }
+ }
+}
+
+//TODO: tests
+fn round_to_u8(x : f32) -> u8 {
+ let v = (x + 0.5).floor() as i32;
+ assert!(0 <= v && v < 0x100);
+ v as u8
+}
+
+//TODO: tests
+/*
+ * Scales base <= 2^N-1 to 2^8-1
+ * @param N [1, 8] the number of bits used by base.
+ * @param base the number to be scaled to [0, 255].
+ */
+fn scale255(n: u8, mut base: u8) -> u8 {
+ base <<= 8 - n;
+ let mut lum = base;
+ let mut i = n;
+
+ while i < 8 {
+ lum |= base >> i;
+ i += n;
+ }
+
+ lum
+}
+
+// Computes the luminance from the given r, g, and b in accordance with
+// SK_LUM_COEFF_X. For correct results, r, g, and b should be in linear space.
+fn compute_luminance(r: u8, g: u8, b: u8) -> u8 {
+ // The following is
+ // r * SK_LUM_COEFF_R + g * SK_LUM_COEFF_G + b * SK_LUM_COEFF_B
+ // with SK_LUM_COEFF_X in 1.8 fixed point (rounding adjusted to sum to 256).
+ let val: u32 = r as u32 * 54 + g as u32 * 183 + b as u32 * 19;
+ assert!(val < 0x10000);
+ (val >> 8) as u8
+}
+
+// Skia uses 3 bits per channel for luminance.
+const LUM_BITS: u8 = 3;
+// Mask of the highest used bits.
+const LUM_MASK: u8 = ((1 << LUM_BITS) - 1) << (8 - LUM_BITS);
+
+pub trait ColorLut {
+ fn quantize(&self) -> ColorU;
+ fn quantized_floor(&self) -> ColorU;
+ fn quantized_ceil(&self) -> ColorU;
+ fn luminance(&self) -> u8;
+ fn luminance_color(&self) -> ColorU;
+}
+
+impl ColorLut for ColorU {
+ // Compute a canonical color that is equivalent to the input color
+ // for preblend table lookups. The alpha channel is never used for
+ // preblending, so overwrite it with opaque.
+ fn quantize(&self) -> ColorU {
+ ColorU::new(
+ scale255(LUM_BITS, self.r >> (8 - LUM_BITS)),
+ scale255(LUM_BITS, self.g >> (8 - LUM_BITS)),
+ scale255(LUM_BITS, self.b >> (8 - LUM_BITS)),
+ 255,
+ )
+ }
+
+ // Quantize to the smallest value that yields the same table index.
+ fn quantized_floor(&self) -> ColorU {
+ ColorU::new(
+ self.r & LUM_MASK,
+ self.g & LUM_MASK,
+ self.b & LUM_MASK,
+ 255,
+ )
+ }
+
+ // Quantize to the largest value that yields the same table index.
+ fn quantized_ceil(&self) -> ColorU {
+ ColorU::new(
+ self.r | !LUM_MASK,
+ self.g | !LUM_MASK,
+ self.b | !LUM_MASK,
+ 255,
+ )
+ }
+
+ // Compute a luminance value suitable for grayscale preblend table
+ // lookups.
+ fn luminance(&self) -> u8 {
+ compute_luminance(self.r, self.g, self.b)
+ }
+
+ // Make a grayscale color from the computed luminance.
+ fn luminance_color(&self) -> ColorU {
+ let lum = self.luminance();
+ ColorU::new(lum, lum, lum, self.a)
+ }
+}
+
+// This will invert the gamma applied by CoreGraphics,
+// so we can get linear values.
+// CoreGraphics obscurely defaults to 2.0 as the smoothing gamma value.
+// The color space used does not appear to affect this choice.
+#[cfg(target_os="macos")]
+fn get_inverse_gamma_table_coregraphics_smoothing() -> [u8; 256] {
+ let mut table = [0u8; 256];
+
+ for (i, v) in table.iter_mut().enumerate() {
+ let x = i as f32 / 255.0;
+ *v = round_to_u8(x * x * 255.0);
+ }
+
+ table
+}
+
+// A value of 0.5 for SK_GAMMA_CONTRAST appears to be a good compromise.
+// With lower values small text appears washed out (though correctly so).
+// With higher values lcd fringing is worse and the smoothing effect of
+// partial coverage is diminished.
+fn apply_contrast(srca: f32, contrast: f32) -> f32 {
+ srca + ((1.0 - srca) * contrast * srca)
+}
+
+// The approach here is not necessarily the one with the lowest error
+// See https://bel.fi/alankila/lcd/alpcor.html for a similar kind of thing
+// that just search for the adjusted alpha value
+pub fn build_gamma_correcting_lut(table: &mut [u8; 256], src: u8, contrast: f32,
+ src_space: LuminanceColorSpace,
+ dst_convert: LuminanceColorSpace) {
+ let src = src as f32 / 255.0;
+ let lin_src = src_space.to_luma(src);
+ // Guess at the dst. The perceptual inverse provides smaller visual
+ // discontinuities when slight changes to desaturated colors cause a channel
+ // to map to a different correcting lut with neighboring srcI.
+ // See https://code.google.com/p/chromium/issues/detail?id=141425#c59 .
+ let dst = 1.0 - src;
+ let lin_dst = dst_convert.to_luma(dst);
+
+ // Contrast value tapers off to 0 as the src luminance becomes white
+ let adjusted_contrast = contrast * lin_dst;
+
+ // Remove discontinuity and instability when src is close to dst.
+ // The value 1/256 is arbitrary and appears to contain the instability.
+ if (src - dst).abs() < (1.0 / 256.0) {
+ let mut ii : f32 = 0.0;
+ for v in table.iter_mut() {
+ let raw_srca = ii / 255.0;
+ let srca = apply_contrast(raw_srca, adjusted_contrast);
+
+ *v = round_to_u8(255.0 * srca);
+ ii += 1.0;
+ }
+ } else {
+ // Avoid slow int to float conversion.
+ let mut ii : f32 = 0.0;
+ for v in table.iter_mut() {
+ // 'raw_srca += 1.0f / 255.0f' and even
+ // 'raw_srca = i * (1.0f / 255.0f)' can add up to more than 1.0f.
+ // When this happens the table[255] == 0x0 instead of 0xff.
+ // See http://code.google.com/p/chromium/issues/detail?id=146466
+ let raw_srca = ii / 255.0;
+ let srca = apply_contrast(raw_srca, adjusted_contrast);
+ assert!(srca <= 1.0);
+ let dsta = 1.0 - srca;
+
+ // Calculate the output we want.
+ let lin_out = lin_src * srca + dsta * lin_dst;
+ assert!(lin_out <= 1.0);
+ let out = dst_convert.from_luma(lin_out);
+
+ // Undo what the blit blend will do.
+ // i.e. given the formula for OVER: out = src * result + (1 - result) * dst
+ // solving for result gives:
+ let result = (out - dst) / (src - dst);
+
+ *v = round_to_u8(255.0 * result);
+ debug!("Setting {:?} to {:?}", ii as u8, *v);
+
+ ii += 1.0;
+ }
+ }
+}
+
+pub struct GammaLut {
+ tables: [[u8; 256]; 1 << LUM_BITS],
+ #[cfg(target_os="macos")]
+ cg_inverse_gamma: [u8; 256],
+}
+
+impl GammaLut {
+ // Skia actually makes 9 gamma tables, then based on the luminance color,
+ // fetches the RGB gamma table for that color.
+ fn generate_tables(&mut self, contrast: f32, paint_gamma: f32, device_gamma: f32) {
+ let paint_color_space = LuminanceColorSpace::new(paint_gamma);
+ let device_color_space = LuminanceColorSpace::new(device_gamma);
+
+ for (i, entry) in self.tables.iter_mut().enumerate() {
+ let luminance = scale255(LUM_BITS, i as u8);
+ build_gamma_correcting_lut(entry,
+ luminance,
+ contrast,
+ paint_color_space,
+ device_color_space);
+ }
+ }
+
+ pub fn table_count(&self) -> usize {
+ self.tables.len()
+ }
+
+ pub fn get_table(&self, color: u8) -> &[u8; 256] {
+ &self.tables[(color >> (8 - LUM_BITS)) as usize]
+ }
+
+ pub fn new(contrast: f32, paint_gamma: f32, device_gamma: f32) -> GammaLut {
+ #[cfg(target_os="macos")]
+ let mut table = GammaLut {
+ tables: [[0; 256]; 1 << LUM_BITS],
+ cg_inverse_gamma: get_inverse_gamma_table_coregraphics_smoothing(),
+ };
+ #[cfg(not(target_os="macos"))]
+ let mut table = GammaLut {
+ tables: [[0; 256]; 1 << LUM_BITS],
+ };
+
+ table.generate_tables(contrast, paint_gamma, device_gamma);
+
+ table
+ }
+
+ // Assumes pixels are in BGRA format. Assumes pixel values are in linear space already.
+ pub fn preblend(&self, pixels: &mut [u8], color: ColorU) {
+ let table_r = self.get_table(color.r);
+ let table_g = self.get_table(color.g);
+ let table_b = self.get_table(color.b);
+
+ for pixel in pixels.chunks_mut(4) {
+ let (b, g, r) = (table_b[pixel[0] as usize], table_g[pixel[1] as usize], table_r[pixel[2] as usize]);
+ pixel[0] = b;
+ pixel[1] = g;
+ pixel[2] = r;
+ pixel[3] = max(max(b, g), r);
+ }
+ }
+
+ // Assumes pixels are in BGRA format. Assumes pixel values are in linear space already.
+ pub fn preblend_scaled(&self, pixels: &mut [u8], color: ColorU, percent: u8) {
+ if percent >= 100 {
+ self.preblend(pixels, color);
+ return;
+ }
+
+ let table_r = self.get_table(color.r);
+ let table_g = self.get_table(color.g);
+ let table_b = self.get_table(color.b);
+ let scale = (percent as i32 * 256) / 100;
+
+ for pixel in pixels.chunks_mut(4) {
+ let (mut b, g, mut r) = (
+ table_b[pixel[0] as usize] as i32,
+ table_g[pixel[1] as usize] as i32,
+ table_r[pixel[2] as usize] as i32,
+ );
+ b = g + (((b - g) * scale) >> 8);
+ r = g + (((r - g) * scale) >> 8);
+ pixel[0] = b as u8;
+ pixel[1] = g as u8;
+ pixel[2] = r as u8;
+ pixel[3] = max(max(b, g), r) as u8;
+ }
+ }
+
+ #[cfg(target_os="macos")]
+ pub fn coregraphics_convert_to_linear(&self, pixels: &mut [u8]) {
+ for pixel in pixels.chunks_mut(4) {
+ pixel[0] = self.cg_inverse_gamma[pixel[0] as usize];
+ pixel[1] = self.cg_inverse_gamma[pixel[1] as usize];
+ pixel[2] = self.cg_inverse_gamma[pixel[2] as usize];
+ }
+ }
+
+ // Assumes pixels are in BGRA format. Assumes pixel values are in linear space already.
+ pub fn preblend_grayscale(&self, pixels: &mut [u8], color: ColorU) {
+ let table_g = self.get_table(color.g);
+
+ for pixel in pixels.chunks_mut(4) {
+ let luminance = compute_luminance(pixel[2], pixel[1], pixel[0]);
+ let alpha = table_g[luminance as usize];
+ pixel[0] = alpha;
+ pixel[1] = alpha;
+ pixel[2] = alpha;
+ pixel[3] = alpha;
+ }
+ }
+
+} // end impl GammaLut
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn over(dst: u32, src: u32, alpha: u32) -> u32 {
+ (src * alpha + dst * (255 - alpha))/255
+ }
+
+ fn overf(dst: f32, src: f32, alpha: f32) -> f32 {
+ ((src * alpha + dst * (255. - alpha))/255.) as f32
+ }
+
+
+ fn absdiff(a: u32, b: u32) -> u32 {
+ if a < b { b - a } else { a - b }
+ }
+
+ #[test]
+ fn gamma() {
+ let mut table = [0u8; 256];
+ let g = 2.0;
+ let space = LuminanceColorSpace::Gamma(g);
+ let mut src : u32 = 131;
+ while src < 256 {
+ build_gamma_correcting_lut(&mut table, src as u8, 0., space, space);
+ let mut max_diff = 0;
+ let mut dst = 0;
+ while dst < 256 {
+ for alpha in 0u32..256 {
+ let preblend = table[alpha as usize];
+ let lin_dst = (dst as f32 / 255.).powf(g) * 255.;
+ let lin_src = (src as f32 / 255.).powf(g) * 255.;
+
+ let preblend_result = over(dst, src, preblend as u32);
+ let true_result = ((overf(lin_dst, lin_src, alpha as f32) / 255.).powf(1. / g) * 255.) as u32;
+ let diff = absdiff(preblend_result, true_result);
+ //debug!("{} -- {} {} = {}", alpha, preblend_result, true_result, diff);
+ max_diff = max(max_diff, diff);
+ }
+
+ //debug!("{} {} max {}", src, dst, max_diff);
+ assert!(max_diff <= 33);
+ dst += 1;
+
+ }
+ src += 1;
+ }
+ }
+} // end mod
diff --git a/gfx/wr/wr_glyph_rasterizer/src/lib.rs b/gfx/wr/wr_glyph_rasterizer/src/lib.rs
new file mode 100644
index 0000000000..27ceb71992
--- /dev/null
+++ b/gfx/wr/wr_glyph_rasterizer/src/lib.rs
@@ -0,0 +1,60 @@
+/* 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 glyph rasterizer for webrender
+//!
+//! ## Overview
+//!
+//! ## Usage
+//!
+
+#[cfg(any(target_os = "macos", target_os = "windows"))]
+mod gamma_lut;
+mod rasterizer;
+mod telemetry;
+mod types;
+
+pub mod profiler;
+
+pub use rasterizer::*;
+pub use types::*;
+
+#[macro_use]
+extern crate malloc_size_of_derive;
+#[macro_use]
+extern crate tracy_rs;
+#[macro_use]
+extern crate log;
+#[macro_use]
+extern crate lazy_static;
+#[macro_use]
+extern crate smallvec;
+
+#[cfg(any(feature = "serde"))]
+#[macro_use]
+extern crate serde;
+
+extern crate malloc_size_of;
+
+pub mod platform {
+ #[cfg(target_os = "macos")]
+ pub use crate::platform::macos::font;
+ #[cfg(any(target_os = "android", all(unix, not(target_os = "macos"))))]
+ pub use crate::platform::unix::font;
+ #[cfg(target_os = "windows")]
+ pub use crate::platform::windows::font;
+
+ #[cfg(target_os = "macos")]
+ pub mod macos {
+ pub mod font;
+ }
+ #[cfg(any(target_os = "android", all(unix, not(target_os = "macos"))))]
+ pub mod unix {
+ pub mod font;
+ }
+ #[cfg(target_os = "windows")]
+ pub mod windows {
+ pub mod font;
+ }
+}
diff --git a/gfx/wr/wr_glyph_rasterizer/src/platform/macos/font.rs b/gfx/wr/wr_glyph_rasterizer/src/platform/macos/font.rs
new file mode 100644
index 0000000000..804dbc93e8
--- /dev/null
+++ b/gfx/wr/wr_glyph_rasterizer/src/platform/macos/font.rs
@@ -0,0 +1,899 @@
+/* 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 api::{ColorF, ColorU, FontKey, FontRenderMode, FontSize, GlyphDimensions};
+use api::{FontInstanceFlags, FontVariation, NativeFontHandle};
+use core_foundation::data::CFData;
+use core_foundation::base::TCFType;
+use core_foundation::dictionary::CFDictionary;
+use core_foundation::number::{CFNumber};
+use core_foundation::string::CFString;
+use core_graphics::base::{kCGImageAlphaNoneSkipFirst, kCGImageAlphaPremultipliedFirst};
+use core_graphics::base::{kCGBitmapByteOrder32Little};
+use core_graphics::color_space::CGColorSpace;
+use core_graphics::context::CGContext;
+use core_graphics::context::{CGBlendMode, CGTextDrawingMode};
+use core_graphics::font::{CGFont, CGGlyph};
+use core_graphics::geometry::{CGAffineTransform, CGPoint, CGSize};
+use core_graphics::geometry::{CG_AFFINE_TRANSFORM_IDENTITY, CGRect};
+use core_text;
+use core_text::font::CTFont;
+use core_text::font_descriptor::{CTFontDescriptor, kCTFontDefaultOrientation, kCTFontVariationAttribute};
+use core_text::font_manager;
+use euclid::default::Size2D;
+use crate::gamma_lut::{ColorLut, GammaLut};
+use crate::rasterizer::{FontInstance, FontTransform, GlyphKey};
+use crate::rasterizer::{GlyphFormat, GlyphRasterError, GlyphRasterResult, RasterizedGlyph};
+use crate::types::FastHashMap;
+use std::collections::hash_map::Entry;
+use std::sync::Arc;
+
+const INITIAL_CG_CONTEXT_SIDE_LENGTH: u32 = 32;
+
+pub struct FontContext {
+ ct_font_descs: FastHashMap<FontKey, CTFontDescriptor>,
+ // Table mapping a sized font key with variations to its instantiated CoreText font.
+ ct_fonts: FastHashMap<(FontKey, FontSize, Vec<FontVariation>), CTFont>,
+ #[allow(dead_code)]
+ graphics_context: GraphicsContext,
+ #[allow(dead_code)]
+ gamma_lut: GammaLut,
+}
+
+// core text is safe to use on multiple threads and non-shareable resources are
+// all hidden inside their font context.
+unsafe impl Send for FontContext {}
+
+struct GlyphMetrics {
+ rasterized_left: i32,
+ #[allow(dead_code)]
+ rasterized_descent: i32,
+ rasterized_ascent: i32,
+ rasterized_width: i32,
+ rasterized_height: i32,
+ advance: f32,
+}
+
+// There are a number of different OS prefs that control whether or not
+// requesting font smoothing actually results in subpixel AA. This gets even
+// murkier in newer macOS versions that deprecate subpixel AA, with the prefs
+// potentially interacting and overriding each other. In an attempt to future-
+// proof things against any new prefs or interpretation of those prefs in
+// future macOS versions, we do a check here to request font smoothing and see
+// what result it actually gives us much like Skia does. We need to check for
+// each of three potential results and process them in the font backend in
+// distinct ways:
+// 1) subpixel AA (differing RGB channels) with dilation
+// 2) grayscale AA (matching RGB channels) with dilation, a compatibility mode
+// 3) grayscale AA without dilation as if font smoothing was not requested
+// We can discern between case 1 and the rest by checking if the subpixels differ.
+// We can discern between cases 2 and 3 by rendering with and without smoothing
+// and comparing the two to determine if there was some dilation.
+// This returns the actual FontRenderMode needed to support each case, if any.
+fn determine_font_smoothing_mode() -> Option<FontRenderMode> {
+ let mut smooth_context = CGContext::create_bitmap_context(
+ None,
+ 12,
+ 12,
+ 8,
+ 12 * 4,
+ &CGColorSpace::create_device_rgb(),
+ kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Little,
+ );
+ smooth_context.set_should_smooth_fonts(true);
+ smooth_context.set_should_antialias(true);
+ smooth_context.set_rgb_fill_color(1.0, 1.0, 1.0, 1.0);
+ let mut gray_context = CGContext::create_bitmap_context(
+ None,
+ 12,
+ 12,
+ 8,
+ 12 * 4,
+ &CGColorSpace::create_device_rgb(),
+ kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Little,
+ );
+ gray_context.set_should_smooth_fonts(false);
+ gray_context.set_should_antialias(true);
+ gray_context.set_rgb_fill_color(1.0, 1.0, 1.0, 1.0);
+
+ // Autorelease pool for CTFont
+ objc::rc::autoreleasepool(|| {
+ // Lucida Grande 12 is the default fallback font in Firefox
+ let ct_font = core_text::font::new_from_name("Lucida Grande", 12.).unwrap();
+ let point = CGPoint { x: 0., y: 0. };
+ let glyph = 'X' as CGGlyph;
+ ct_font.draw_glyphs(&[glyph], &[point], smooth_context.clone());
+ ct_font.draw_glyphs(&[glyph], &[point], gray_context.clone());
+ });
+
+ let mut mode = None;
+ for (smooth, gray) in smooth_context.data().chunks(4).zip(gray_context.data().chunks(4)) {
+ if smooth[0] != smooth[1] || smooth[1] != smooth[2] {
+ return Some(FontRenderMode::Subpixel);
+ }
+ if smooth[0] != gray[0] || smooth[1] != gray[1] || smooth[2] != gray[2] {
+ mode = Some(FontRenderMode::Alpha);
+ }
+ }
+ return mode;
+}
+
+// We cache the font smoothing mode globally, rather than storing it in each FontContext,
+// to avoid having to determine this redundantly in each context and to avoid needing to
+// lock them to access this setting in prepare_font.
+lazy_static! {
+ static ref FONT_SMOOTHING_MODE: Option<FontRenderMode> = determine_font_smoothing_mode();
+}
+
+fn should_use_white_on_black(color: ColorU) -> bool {
+ let (r, g, b) = (color.r as u32, color.g as u32, color.b as u32);
+ // These thresholds were determined on 10.12 by observing what CG does.
+ r >= 85 && g >= 85 && b >= 85 && r + g + b >= 2 * 255
+}
+
+fn get_glyph_metrics(
+ ct_font: &CTFont,
+ transform: Option<&CGAffineTransform>,
+ glyph: CGGlyph,
+ x_offset: f64,
+ y_offset: f64,
+ extra_width: f64,
+) -> GlyphMetrics {
+ let mut bounds = ct_font.get_bounding_rects_for_glyphs(kCTFontDefaultOrientation, &[glyph]);
+
+ if bounds.origin.x.is_nan() || bounds.origin.y.is_nan() || bounds.size.width.is_nan() ||
+ bounds.size.height.is_nan()
+ {
+ // If an unexpected glyph index is requested, core text will return NaN values
+ // which causes us to do bad thing as the value is cast into an integer and
+ // overflow when expanding the bounds a few lines below.
+ // Instead we are better off returning zero-sized metrics because this special
+ // case is handled by the callers of this method.
+ return GlyphMetrics {
+ rasterized_left: 0,
+ rasterized_width: 0,
+ rasterized_height: 0,
+ rasterized_ascent: 0,
+ rasterized_descent: 0,
+ advance: 0.0,
+ };
+ }
+
+ let mut advance = CGSize { width: 0.0, height: 0.0 };
+ unsafe {
+ ct_font.get_advances_for_glyphs(kCTFontDefaultOrientation, &glyph, &mut advance, 1);
+ }
+
+ if bounds.size.width > 0.0 {
+ bounds.size.width += extra_width;
+ }
+ if advance.width > 0.0 {
+ advance.width += extra_width;
+ }
+
+ if let Some(transform) = transform {
+ bounds = bounds.apply_transform(transform);
+ }
+
+ // First round out to pixel boundaries
+ // CG Origin is bottom left
+ let mut left = bounds.origin.x.floor() as i32;
+ let mut bottom = bounds.origin.y.floor() as i32;
+ let mut right = (bounds.origin.x + bounds.size.width + x_offset).ceil() as i32;
+ let mut top = (bounds.origin.y + bounds.size.height + y_offset).ceil() as i32;
+
+ // Expand the bounds by 1 pixel, to give CG room for anti-aliasing.
+ // Note that this outset is to allow room for LCD smoothed glyphs. However, the correct outset
+ // is not currently known, as CG dilates the outlines by some percentage.
+ // This is taken from Skia.
+ left -= 1;
+ bottom -= 1;
+ right += 1;
+ top += 1;
+
+ let width = right - left;
+ let height = top - bottom;
+
+ GlyphMetrics {
+ rasterized_left: left,
+ rasterized_width: width,
+ rasterized_height: height,
+ rasterized_ascent: top,
+ rasterized_descent: -bottom,
+ advance: advance.width as f32,
+ }
+}
+
+fn new_ct_font_with_variations(ct_font_desc: &CTFontDescriptor, size: f64, variations: &[FontVariation]) -> CTFont {
+ let ct_font = core_text::font::new_from_descriptor(ct_font_desc, size);
+ if variations.is_empty() {
+ return ct_font;
+ }
+ let mut vals: Vec<(CFNumber, CFNumber)> = Vec::with_capacity(variations.len() as usize);
+ for variation in variations {
+ vals.push((CFNumber::from(variation.tag as i64), CFNumber::from(variation.value as f64)));
+ }
+ if vals.is_empty() {
+ return ct_font;
+ }
+ let vals_dict = CFDictionary::from_CFType_pairs(&vals);
+ let variation_attribute = unsafe { CFString::wrap_under_get_rule(kCTFontVariationAttribute) };
+ let attrs_dict = CFDictionary::from_CFType_pairs(&[(variation_attribute, vals_dict)]);
+ let ct_var_font_desc = ct_font.copy_descriptor().create_copy_with_attributes(attrs_dict.to_untyped()).unwrap();
+ core_text::font::new_from_descriptor(&ct_var_font_desc, size)
+
+}
+
+// We rely on Gecko to determine whether the font may have color glyphs to avoid
+// needing to load the font ahead of time to query its symbolic traits.
+fn is_bitmap_font(font: &FontInstance) -> bool {
+ font.flags.contains(FontInstanceFlags::EMBEDDED_BITMAPS)
+}
+
+impl FontContext {
+ pub fn distribute_across_threads() -> bool {
+ true
+ }
+
+ pub fn new() -> FontContext {
+ debug!("Test for subpixel AA support: {:?}", *FONT_SMOOTHING_MODE);
+
+ // Force CG to use sRGB color space to gamma correct.
+ let contrast = 0.0;
+ let gamma = 0.0;
+
+ FontContext {
+ ct_font_descs: FastHashMap::default(),
+ ct_fonts: FastHashMap::default(),
+ graphics_context: GraphicsContext::new(),
+ gamma_lut: GammaLut::new(contrast, gamma, gamma),
+ }
+ }
+
+ pub fn add_raw_font(&mut self, font_key: &FontKey, bytes: Arc<Vec<u8>>, index: u32) {
+ if self.ct_font_descs.contains_key(font_key) {
+ return;
+ }
+
+ assert_eq!(index, 0);
+ let data = CFData::from_arc(bytes);
+ let ct_font_desc = match font_manager::create_font_descriptor_with_data(data) {
+ Err(_) => return,
+ Ok(cg_font) => cg_font,
+ };
+ self.ct_font_descs.insert(*font_key, ct_font_desc);
+ }
+
+ pub fn add_native_font(&mut self, font_key: &FontKey, native_font_handle: NativeFontHandle) {
+ if self.ct_font_descs.contains_key(font_key) {
+ return;
+ }
+
+ // There's no way great way to go from a CGFont to a CTFontDescriptor
+ // We could use the postscript name but that doesn't work for the
+ // system UI fonts on newer macOS versions. Instead we create a CTFont
+ // and use the descriptor for that. Normally we'd try to avoid new_from_CGFont
+ // because that adds the CGFont to the descriptor cache which can keep the CGFont
+ // around for a long time, but that should be ok for non-web (native) fonts.
+ let cf_name = CFString::new(&native_font_handle.name);
+
+ // For "hidden" system fonts, whose names start with a period,
+ // we can't instantiate CTFonts via a descriptor. We're really
+ // supposed to use CTFontCreateUIFontForLanguage, but for now
+ // we just use the CGFont.
+ let desc = if native_font_handle.name.starts_with('.') {
+ let cg_font = match CGFont::from_name(&cf_name) {
+ Ok(cg_font) => cg_font,
+ Err(_) => {
+ // If for some reason we failed to load a font descriptor, then our
+ // only options are to either abort or substitute a fallback font.
+ // It is preferable to use a fallback font instead so that rendering
+ // can at least still proceed in some fashion without erroring.
+ // Lucida Grande is the fallback font in Gecko, so use that here.
+ CGFont::from_name(&CFString::from_static_string("Lucida Grande"))
+ .expect("couldn't find font with postscript name and couldn't load fallback font")
+ }
+ };
+ core_text::font::new_from_CGFont(&cg_font, 0.).copy_descriptor()
+ } else {
+ core_text::font_descriptor::new_from_postscript_name(&cf_name)
+ };
+
+ self.ct_font_descs
+ .insert(*font_key, desc);
+ }
+
+ pub fn delete_font(&mut self, font_key: &FontKey) {
+ if let Some(_) = self.ct_font_descs.remove(font_key) {
+ self.ct_fonts.retain(|k, _| k.0 != *font_key);
+ }
+ }
+
+ pub fn delete_font_instance(&mut self, instance: &FontInstance) {
+ // Remove the CoreText font corresponding to this instance.
+ let size = FontSize::from_f64_px(instance.get_transformed_size());
+ self.ct_fonts.remove(&(instance.font_key, size, instance.variations.clone()));
+ }
+
+ fn get_ct_font(
+ &mut self,
+ font_key: FontKey,
+ size: f64,
+ variations: &[FontVariation],
+ ) -> Option<CTFont> {
+ // Interacting with CoreText can create autorelease garbage.
+ objc::rc::autoreleasepool(|| {
+ match self.ct_fonts.entry((font_key, FontSize::from_f64_px(size), variations.to_vec())) {
+ Entry::Occupied(entry) => Some((*entry.get()).clone()),
+ Entry::Vacant(entry) => {
+ let ct_font_desc = self.ct_font_descs.get(&font_key)?;
+ let ct_font = new_ct_font_with_variations(ct_font_desc, size, variations);
+ entry.insert(ct_font.clone());
+ Some(ct_font)
+ }
+ }
+ })
+ }
+
+ pub fn get_glyph_index(&mut self, font_key: FontKey, ch: char) -> Option<u32> {
+ let character = ch as u16;
+ let mut glyph = 0;
+
+ self.get_ct_font(font_key, 16.0, &[])
+ .and_then(|ct_font| {
+ unsafe {
+ let result = ct_font.get_glyphs_for_characters(&character, &mut glyph, 1);
+
+ if result {
+ Some(glyph as u32)
+ } else {
+ None
+ }
+ }
+ })
+ }
+
+ pub fn get_glyph_dimensions(
+ &mut self,
+ font: &FontInstance,
+ key: &GlyphKey,
+ ) -> Option<GlyphDimensions> {
+ let (x_scale, y_scale) = font.transform.compute_scale().unwrap_or((1.0, 1.0));
+ let size = font.size.to_f64_px() * y_scale;
+ self.get_ct_font(font.font_key, size, &font.variations)
+ .and_then(|ct_font| {
+ let glyph = key.index() as CGGlyph;
+ let bitmap = is_bitmap_font(font);
+ let (mut shape, (x_offset, y_offset)) = if bitmap {
+ (FontTransform::identity(), (0.0, 0.0))
+ } else {
+ (font.transform.invert_scale(y_scale, y_scale), font.get_subpx_offset(key))
+ };
+ if font.flags.contains(FontInstanceFlags::FLIP_X) {
+ shape = shape.flip_x();
+ }
+ if font.flags.contains(FontInstanceFlags::FLIP_Y) {
+ shape = shape.flip_y();
+ }
+ if font.flags.contains(FontInstanceFlags::TRANSPOSE) {
+ shape = shape.swap_xy();
+ }
+ let (mut tx, mut ty) = (0.0, 0.0);
+ if font.synthetic_italics.is_enabled() {
+ let (shape_, (tx_, ty_)) = font.synthesize_italics(shape, size);
+ shape = shape_;
+ tx = tx_;
+ ty = ty_;
+ }
+ let transform = if !shape.is_identity() || (tx, ty) != (0.0, 0.0) {
+ Some(CGAffineTransform {
+ a: shape.scale_x as f64,
+ b: -shape.skew_y as f64,
+ c: -shape.skew_x as f64,
+ d: shape.scale_y as f64,
+ tx: tx,
+ ty: -ty,
+ })
+ } else {
+ None
+ };
+ let (strike_scale, pixel_step) = if bitmap {
+ (y_scale, 1.0)
+ } else {
+ (x_scale, y_scale / x_scale)
+ };
+ let extra_strikes = font.get_extra_strikes(
+ FontInstanceFlags::SYNTHETIC_BOLD | FontInstanceFlags::MULTISTRIKE_BOLD,
+ strike_scale,
+ );
+ let metrics = get_glyph_metrics(
+ &ct_font,
+ transform.as_ref(),
+ glyph,
+ x_offset,
+ y_offset,
+ extra_strikes as f64 * pixel_step,
+ );
+ if metrics.rasterized_width == 0 || metrics.rasterized_height == 0 {
+ None
+ } else {
+ Some(GlyphDimensions {
+ left: metrics.rasterized_left,
+ top: metrics.rasterized_ascent,
+ width: metrics.rasterized_width,
+ height: metrics.rasterized_height,
+ advance: metrics.advance,
+ })
+ }
+ })
+ }
+
+ // Assumes the pixels here are linear values from CG
+ fn gamma_correct_pixels(
+ &self,
+ pixels: &mut Vec<u8>,
+ render_mode: FontRenderMode,
+ color: ColorU,
+ ) {
+ // Then convert back to gamma corrected values.
+ match render_mode {
+ FontRenderMode::Alpha => {
+ self.gamma_lut.preblend_grayscale(pixels, color);
+ }
+ FontRenderMode::Subpixel => {
+ self.gamma_lut.preblend(pixels, color);
+ }
+ _ => {} // Again, give mono untouched since only the alpha matters.
+ }
+ }
+
+ #[allow(dead_code)]
+ fn print_glyph_data(&mut self, data: &[u8], width: usize, height: usize) {
+ // Rust doesn't have step_by support on stable :(
+ debug!("Width is: {:?} height: {:?}", width, height);
+ for i in 0 .. height {
+ let current_height = i * width * 4;
+
+ for pixel in data[current_height .. current_height + (width * 4)].chunks(4) {
+ let b = pixel[0];
+ let g = pixel[1];
+ let r = pixel[2];
+ let a = pixel[3];
+ debug!("({}, {}, {}, {}) ", r, g, b, a);
+ }
+ }
+ }
+
+ pub fn prepare_font(font: &mut FontInstance) {
+ if is_bitmap_font(font) {
+ // Render mode is ignored for bitmap fonts. Also, avoid normalizing the color
+ // in case CoreText needs the current color for rendering glyph color layers.
+ font.render_mode = FontRenderMode::Mono;
+ font.disable_subpixel_position();
+ return;
+ }
+ // Sanitize the render mode for font smoothing. If font smoothing is supported,
+ // then we just need to ensure the render mode is limited to what is supported.
+ // If font smoothing is actually disabled, then we need to fall back to grayscale.
+ if font.flags.contains(FontInstanceFlags::FONT_SMOOTHING) ||
+ font.render_mode == FontRenderMode::Subpixel {
+ match *FONT_SMOOTHING_MODE {
+ Some(mode) => {
+ font.render_mode = font.render_mode.limit_by(mode);
+ font.flags.insert(FontInstanceFlags::FONT_SMOOTHING);
+ }
+ None => {
+ font.render_mode = font.render_mode.limit_by(FontRenderMode::Alpha);
+ font.flags.remove(FontInstanceFlags::FONT_SMOOTHING);
+ }
+ }
+ }
+ match font.render_mode {
+ FontRenderMode::Mono => {
+ // In mono mode the color of the font is irrelevant.
+ font.color = ColorU::new(255, 255, 255, 255);
+ // Subpixel positioning is disabled in mono mode.
+ font.disable_subpixel_position();
+ }
+ FontRenderMode::Alpha => {
+ font.color = if font.flags.contains(FontInstanceFlags::FONT_SMOOTHING) {
+ // Only the G channel is used to index grayscale tables,
+ // so use R and B to preserve light/dark determination.
+ let ColorU { g, a, .. } = font.color.luminance_color().quantized_ceil();
+ let rb = if should_use_white_on_black(font.color) { 255 } else { 0 };
+ ColorU::new(rb, g, rb, a)
+ } else {
+ ColorU::new(255, 255, 255, 255)
+ };
+ }
+ FontRenderMode::Subpixel => {
+ // Quantization may change the light/dark determination, so quantize in the
+ // direction necessary to respect the threshold.
+ font.color = if should_use_white_on_black(font.color) {
+ font.color.quantized_ceil()
+ } else {
+ font.color.quantized_floor()
+ };
+ }
+ }
+ }
+
+ pub fn begin_rasterize(_font: &FontInstance) {
+ }
+
+ pub fn end_rasterize(_font: &FontInstance) {
+ }
+
+ pub fn rasterize_glyph(&mut self, font: &FontInstance, key: &GlyphKey) -> GlyphRasterResult {
+ objc::rc::autoreleasepool(|| {
+ let (x_scale, y_scale) = font.transform.compute_scale().unwrap_or((1.0, 1.0));
+ let size = font.size.to_f64_px() * y_scale;
+ let ct_font =
+ self.get_ct_font(font.font_key, size, &font.variations).ok_or(GlyphRasterError::LoadFailed)?;
+ let glyph_type = if is_bitmap_font(font) {
+ GlyphType::Bitmap
+ } else {
+ GlyphType::Vector
+ };
+
+ let (mut shape, (x_offset, y_offset)) = match glyph_type {
+ GlyphType::Bitmap => (FontTransform::identity(), (0.0, 0.0)),
+ GlyphType::Vector => {
+ (font.transform.invert_scale(y_scale, y_scale), font.get_subpx_offset(key))
+ }
+ };
+ if font.flags.contains(FontInstanceFlags::FLIP_X) {
+ shape = shape.flip_x();
+ }
+ if font.flags.contains(FontInstanceFlags::FLIP_Y) {
+ shape = shape.flip_y();
+ }
+ if font.flags.contains(FontInstanceFlags::TRANSPOSE) {
+ shape = shape.swap_xy();
+ }
+ let (mut tx, mut ty) = (0.0, 0.0);
+ if font.synthetic_italics.is_enabled() {
+ let (shape_, (tx_, ty_)) = font.synthesize_italics(shape, size);
+ shape = shape_;
+ tx = tx_;
+ ty = ty_;
+ }
+ let transform = if !shape.is_identity() || (tx, ty) != (0.0, 0.0) {
+ Some(CGAffineTransform {
+ a: shape.scale_x as f64,
+ b: -shape.skew_y as f64,
+ c: -shape.skew_x as f64,
+ d: shape.scale_y as f64,
+ tx: tx,
+ ty: -ty,
+ })
+ } else {
+ None
+ };
+
+ let glyph = key.index() as CGGlyph;
+ let (strike_scale, pixel_step) = if glyph_type == GlyphType::Bitmap {
+ (y_scale, 1.0)
+ } else {
+ (x_scale, y_scale / x_scale)
+ };
+ let extra_strikes = font.get_extra_strikes(
+ FontInstanceFlags::SYNTHETIC_BOLD | FontInstanceFlags::MULTISTRIKE_BOLD,
+ strike_scale,
+ );
+ let metrics = get_glyph_metrics(
+ &ct_font,
+ transform.as_ref(),
+ glyph,
+ x_offset,
+ y_offset,
+ extra_strikes as f64 * pixel_step,
+ );
+ if metrics.rasterized_width == 0 || metrics.rasterized_height == 0 {
+ return Err(GlyphRasterError::LoadFailed);
+ }
+
+ let raster_size = Size2D::new(
+ metrics.rasterized_width as u32,
+ metrics.rasterized_height as u32
+ );
+
+ // If the font render mode is Alpha, we support two different ways to
+ // compute the grayscale mask, depending on the value of the platform
+ // options' font_smoothing flag:
+ // - Alpha + smoothing:
+ // We will recover a grayscale mask from a subpixel rasterization, in
+ // such a way that the result looks as close to subpixel text
+ // blending as we can make it. This involves gamma correction,
+ // luminance computations and preblending based on the text color,
+ // just like with the Subpixel render mode.
+ // - Alpha without smoothing:
+ // We will ask CoreGraphics to rasterize the text with font_smoothing
+ // off. This will cause it to use grayscale anti-aliasing with
+ // comparatively thin text. This method of text rendering is not
+ // gamma-aware.
+ //
+ // For subpixel rasterization, starting with macOS 10.11, CoreGraphics
+ // uses different glyph dilation based on the text color. Bright text
+ // uses less font dilation (looks thinner) than dark text.
+ // As a consequence, when we ask CG to rasterize with subpixel AA, we
+ // will render white-on-black text as opposed to black-on-white text if
+ // the text color brightness exceeds a certain threshold. This applies
+ // to both the Subpixel and the "Alpha + smoothing" modes, but not to
+ // the "Alpha without smoothing" and Mono modes.
+ //
+ // Fonts with color glyphs may, depending on the state within per-glyph
+ // table data, require the current font color to determine the output
+ // color. For such fonts we must thus supply the current font color just
+ // in case it is necessary.
+ let use_white_on_black = should_use_white_on_black(font.color);
+ let use_font_smoothing = font.flags.contains(FontInstanceFlags::FONT_SMOOTHING);
+ let (antialias, smooth, text_color, bg_color, invert) = match glyph_type {
+ GlyphType::Bitmap => (true, false, ColorF::from(font.color), ColorF::TRANSPARENT, false),
+ GlyphType::Vector => {
+ match (font.render_mode, use_font_smoothing) {
+ (FontRenderMode::Subpixel, _) |
+ (FontRenderMode::Alpha, true) => if use_white_on_black {
+ (true, true, ColorF::WHITE, ColorF::BLACK, false)
+ } else {
+ (true, true, ColorF::BLACK, ColorF::WHITE, true)
+ },
+ (FontRenderMode::Alpha, false) => (true, false, ColorF::BLACK, ColorF::WHITE, true),
+ (FontRenderMode::Mono, _) => (false, false, ColorF::BLACK, ColorF::WHITE, true),
+ }
+ }
+ };
+
+ {
+ let cg_context = self.graphics_context.get_context(&raster_size, glyph_type);
+
+ // These are always true in Gecko, even for non-AA fonts
+ cg_context.set_allows_font_subpixel_positioning(true);
+ cg_context.set_should_subpixel_position_fonts(true);
+
+ // Don't quantize because we're doing it already.
+ cg_context.set_allows_font_subpixel_quantization(false);
+ cg_context.set_should_subpixel_quantize_fonts(false);
+
+ cg_context.set_should_smooth_fonts(smooth);
+ cg_context.set_should_antialias(antialias);
+
+ // Fill the background. This could be opaque white, opaque black, or
+ // transparency.
+ cg_context.set_rgb_fill_color(
+ bg_color.r.into(),
+ bg_color.g.into(),
+ bg_color.b.into(),
+ bg_color.a.into(),
+ );
+ let rect = CGRect {
+ origin: CGPoint { x: 0.0, y: 0.0 },
+ size: CGSize {
+ width: metrics.rasterized_width as f64,
+ height: metrics.rasterized_height as f64,
+ },
+ };
+
+ // Make sure we use the Copy blend mode, or else we'll get the Porter-Duff OVER
+ // operator, which can't clear to the transparent color!
+ cg_context.set_blend_mode(CGBlendMode::Copy);
+ cg_context.fill_rect(rect);
+ cg_context.set_blend_mode(CGBlendMode::Normal);
+
+ // Set the text color and draw the glyphs.
+ cg_context.set_rgb_fill_color(
+ text_color.r.into(),
+ text_color.g.into(),
+ text_color.b.into(),
+ 1.0,
+ );
+ cg_context.set_text_drawing_mode(CGTextDrawingMode::CGTextFill);
+
+ // CG Origin is bottom left, WR is top left. Need -y offset
+ let mut draw_origin = CGPoint {
+ x: -metrics.rasterized_left as f64 + x_offset + tx,
+ y: metrics.rasterized_descent as f64 - y_offset - ty,
+ };
+
+ if let Some(transform) = transform {
+ cg_context.set_text_matrix(&transform);
+
+ draw_origin = draw_origin.apply_transform(&transform.invert());
+ } else {
+ // Make sure to reset this because some previous glyph rasterization might have
+ // changed it.
+ cg_context.set_text_matrix(&CG_AFFINE_TRANSFORM_IDENTITY);
+ }
+
+ ct_font.draw_glyphs(&[glyph], &[draw_origin], cg_context.clone());
+
+ // We'd like to render all the strikes in a single ct_font.draw_glyphs call,
+ // passing an array of glyph IDs and an array of origins, but unfortunately
+ // with some fonts, Core Text may inappropriately pixel-snap the rasterization,
+ // such that the strikes overprint instead of being offset. Rendering the
+ // strikes with individual draw_glyphs calls avoids this.
+ // (See https://bugzilla.mozilla.org/show_bug.cgi?id=1633397 for details.)
+ for i in 1 ..= extra_strikes {
+ let origin = CGPoint {
+ x: draw_origin.x + i as f64 * pixel_step,
+ y: draw_origin.y,
+ };
+ ct_font.draw_glyphs(&[glyph], &[origin], cg_context.clone());
+ }
+ }
+
+ let mut rasterized_pixels = self.graphics_context
+ .get_rasterized_pixels(&raster_size, glyph_type);
+
+ if glyph_type == GlyphType::Vector {
+ // We rendered text into an opaque surface. The code below needs to
+ // ignore the current value of each pixel's alpha channel. But it's
+ // allowed to write to the alpha channel, because we're done calling
+ // CG functions now.
+
+ if smooth {
+ // Convert to linear space for subpixel AA.
+ // We explicitly do not do this for grayscale AA ("Alpha without
+ // smoothing" or Mono) because those rendering modes are not
+ // gamma-aware in CoreGraphics.
+ self.gamma_lut.coregraphics_convert_to_linear(
+ &mut rasterized_pixels,
+ );
+ }
+
+ for pixel in rasterized_pixels.chunks_mut(4) {
+ if invert {
+ pixel[0] = 255 - pixel[0];
+ pixel[1] = 255 - pixel[1];
+ pixel[2] = 255 - pixel[2];
+ }
+
+ // Set alpha to the value of the green channel. For grayscale
+ // text, all three channels have the same value anyway.
+ // For subpixel text, the mask's alpha only makes a difference
+ // when computing the destination alpha on destination pixels
+ // that are not completely opaque. Picking an alpha value
+ // that's somehow based on the mask at least ensures that text
+ // blending doesn't modify the destination alpha on pixels where
+ // the mask is entirely zero.
+ pixel[3] = pixel[1];
+ }
+
+ if smooth {
+ // Convert back from linear space into device space, and perform
+ // some "preblending" based on the text color.
+ // In Alpha + smoothing mode, this will also convert subpixel AA
+ // into grayscale AA.
+ self.gamma_correct_pixels(
+ &mut rasterized_pixels,
+ font.render_mode,
+ font.color,
+ );
+ }
+ }
+
+ Ok(RasterizedGlyph {
+ left: metrics.rasterized_left as f32,
+ top: metrics.rasterized_ascent as f32,
+ width: metrics.rasterized_width,
+ height: metrics.rasterized_height,
+ scale: match glyph_type {
+ GlyphType::Bitmap => y_scale.recip() as f32,
+ GlyphType::Vector => 1.0,
+ },
+ format: match glyph_type {
+ GlyphType::Bitmap => GlyphFormat::ColorBitmap,
+ GlyphType::Vector => font.get_glyph_format(),
+ },
+ bytes: rasterized_pixels,
+ })})
+ }
+}
+
+// Avoids taking locks by recycling Core Graphics contexts.
+#[allow(dead_code)]
+struct GraphicsContext {
+ vector_context: CGContext,
+ vector_context_size: Size2D<u32>,
+ bitmap_context: CGContext,
+ bitmap_context_size: Size2D<u32>,
+}
+
+impl GraphicsContext {
+ fn new() -> GraphicsContext {
+ let size = Size2D::new(INITIAL_CG_CONTEXT_SIDE_LENGTH, INITIAL_CG_CONTEXT_SIDE_LENGTH);
+ GraphicsContext {
+ vector_context: GraphicsContext::create_cg_context(&size, GlyphType::Vector),
+ vector_context_size: size,
+ bitmap_context: GraphicsContext::create_cg_context(&size, GlyphType::Bitmap),
+ bitmap_context_size: size,
+ }
+ }
+
+ #[allow(dead_code)]
+ fn get_context(&mut self, size: &Size2D<u32>, glyph_type: GlyphType)
+ -> &mut CGContext {
+ let (cached_context, cached_size) = match glyph_type {
+ GlyphType::Vector => {
+ (&mut self.vector_context, &mut self.vector_context_size)
+ }
+ GlyphType::Bitmap => {
+ (&mut self.bitmap_context, &mut self.bitmap_context_size)
+ }
+ };
+ let rounded_size = Size2D::new(size.width.next_power_of_two(),
+ size.height.next_power_of_two());
+ if rounded_size.width > cached_size.width || rounded_size.height > cached_size.height {
+ *cached_size = Size2D::new(u32::max(cached_size.width, rounded_size.width),
+ u32::max(cached_size.height, rounded_size.height));
+ *cached_context = GraphicsContext::create_cg_context(cached_size, glyph_type);
+ }
+ cached_context
+ }
+
+ #[allow(dead_code)]
+ fn get_rasterized_pixels(&mut self, size: &Size2D<u32>, glyph_type: GlyphType)
+ -> Vec<u8> {
+ let (cached_context, cached_size) = match glyph_type {
+ GlyphType::Vector => (&mut self.vector_context, &self.vector_context_size),
+ GlyphType::Bitmap => (&mut self.bitmap_context, &self.bitmap_context_size),
+ };
+ let cached_data = cached_context.data();
+ let cached_stride = cached_size.width as usize * 4;
+
+ let result_len = size.width as usize * size.height as usize * 4;
+ let mut result = Vec::with_capacity(result_len);
+ for y in (cached_size.height - size.height)..cached_size.height {
+ let cached_start = y as usize * cached_stride;
+ let cached_end = cached_start + size.width as usize * 4;
+ result.extend_from_slice(&cached_data[cached_start..cached_end]);
+ }
+ debug_assert_eq!(result.len(), result_len);
+ result
+ }
+
+ fn create_cg_context(size: &Size2D<u32>, glyph_type: GlyphType) -> CGContext {
+ // The result of rasterization, in all render modes, is going to be a
+ // BGRA surface with white text on transparency using premultiplied
+ // alpha. For subpixel text, the RGB values will be the mask value for
+ // the individual components. For bitmap glyphs, the RGB values will be
+ // the (premultiplied) color of the pixel. For Alpha and Mono, each
+ // pixel will have R==G==B==A at the end of this function.
+ // We access the color channels in little-endian order.
+ // The CGContext will create and own our pixel buffer.
+ // In the non-Bitmap cases, we will ask CoreGraphics to draw text onto
+ // an opaque background. In order to hit the most efficient path in CG
+ // for this, we will tell CG that the CGContext is opaque, by passing
+ // an "[...]AlphaNone[...]" context flag. This creates a slight
+ // contradiction to the way we use the buffer after CG is done with it,
+ // because we will convert it into text-on-transparency. But that's ok;
+ // we still get four bytes per pixel and CG won't mess with the alpha
+ // channel after we've stopped calling CG functions. We just need to
+ // make sure that we don't look at the alpha values of the pixels that
+ // we get from CG, and compute our own alpha value only from RGB.
+ // Note that CG requires kCGBitmapByteOrder32Little in order to do
+ // subpixel AA at all (which we need it to do in both Subpixel and
+ // Alpha+smoothing mode). But little-endian is what we want anyway, so
+ // this works out nicely.
+ let color_type = match glyph_type {
+ GlyphType::Vector => kCGImageAlphaNoneSkipFirst,
+ GlyphType::Bitmap => kCGImageAlphaPremultipliedFirst,
+ };
+
+ CGContext::create_bitmap_context(None,
+ size.width as usize,
+ size.height as usize,
+ 8,
+ size.width as usize * 4,
+ &CGColorSpace::create_device_rgb(),
+ kCGBitmapByteOrder32Little | color_type)
+ }
+}
+
+#[derive(Clone, Copy, PartialEq, Debug)]
+enum GlyphType {
+ Vector,
+ Bitmap,
+}
+
diff --git a/gfx/wr/wr_glyph_rasterizer/src/platform/unix/font.rs b/gfx/wr/wr_glyph_rasterizer/src/platform/unix/font.rs
new file mode 100644
index 0000000000..5885d8f9a2
--- /dev/null
+++ b/gfx/wr/wr_glyph_rasterizer/src/platform/unix/font.rs
@@ -0,0 +1,1099 @@
+/* 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 api::{ColorU, GlyphDimensions, FontKey, FontRenderMode};
+use api::{FontInstancePlatformOptions, FontLCDFilter, FontHinting};
+use api::{FontInstanceFlags, FontTemplate, FontVariation, NativeFontHandle};
+use freetype::freetype::{FT_BBox, FT_Outline_Translate, FT_Pixel_Mode, FT_Render_Mode};
+use freetype::freetype::{FT_Done_Face, FT_Error, FT_Get_Char_Index, FT_Int32};
+use freetype::freetype::{FT_Done_FreeType, FT_Library_SetLcdFilter, FT_Pos};
+use freetype::freetype::{FT_F26Dot6, FT_Face, FT_Glyph_Format, FT_Long, FT_UInt};
+use freetype::freetype::{FT_GlyphSlot, FT_LcdFilter, FT_New_Face, FT_New_Memory_Face};
+use freetype::freetype::{FT_Init_FreeType, FT_Load_Glyph, FT_Render_Glyph};
+use freetype::freetype::{FT_Library, FT_Outline_Get_CBox, FT_Set_Char_Size, FT_Select_Size};
+use freetype::freetype::{FT_Fixed, FT_Matrix, FT_Set_Transform, FT_String, FT_ULong, FT_Vector};
+use freetype::freetype::{FT_Err_Unimplemented_Feature, FT_MulFix, FT_Outline_Embolden};
+use freetype::freetype::{FT_LOAD_COLOR, FT_LOAD_DEFAULT, FT_LOAD_FORCE_AUTOHINT};
+use freetype::freetype::{FT_LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH, FT_LOAD_NO_AUTOHINT};
+use freetype::freetype::{FT_LOAD_NO_BITMAP, FT_LOAD_NO_HINTING};
+use freetype::freetype::{FT_FACE_FLAG_SCALABLE, FT_FACE_FLAG_FIXED_SIZES};
+use freetype::freetype::{FT_FACE_FLAG_MULTIPLE_MASTERS};
+use freetype::succeeded;
+use crate::rasterizer::{FontInstance, GlyphFormat, GlyphKey};
+use crate::rasterizer::{GlyphRasterError, GlyphRasterResult, RasterizedGlyph};
+use crate::types::FastHashMap;
+#[cfg(any(not(target_os = "android"), feature = "dynamic_freetype"))]
+use libc::{dlsym, RTLD_DEFAULT};
+use libc::free;
+use std::{cmp, mem, ptr, slice};
+use std::cmp::max;
+use std::ffi::CString;
+use std::sync::{Arc, Condvar, Mutex, MutexGuard};
+
+// These constants are not present in the freetype
+// bindings due to bindgen not handling the way
+// the macros are defined.
+//const FT_LOAD_TARGET_NORMAL: FT_UInt = 0 << 16;
+const FT_LOAD_TARGET_LIGHT: FT_UInt = 1 << 16;
+const FT_LOAD_TARGET_MONO: FT_UInt = 2 << 16;
+const FT_LOAD_TARGET_LCD: FT_UInt = 3 << 16;
+const FT_LOAD_TARGET_LCD_V: FT_UInt = 4 << 16;
+
+#[repr(C)]
+struct FT_Var_Axis {
+ pub name: *mut FT_String,
+ pub minimum: FT_Fixed,
+ pub def: FT_Fixed,
+ pub maximum: FT_Fixed,
+ pub tag: FT_ULong,
+ pub strid: FT_UInt,
+}
+
+#[repr(C)]
+struct FT_Var_Named_Style {
+ pub coords: *mut FT_Fixed,
+ pub strid: FT_UInt,
+ pub psid: FT_UInt,
+}
+
+#[repr(C)]
+struct FT_MM_Var {
+ pub num_axis: FT_UInt,
+ pub num_designs: FT_UInt,
+ pub num_namedstyles: FT_UInt,
+ pub axis: *mut FT_Var_Axis,
+ pub namedstyle: *mut FT_Var_Named_Style,
+}
+
+#[inline]
+pub fn unimplemented(error: FT_Error) -> bool {
+ error == FT_Err_Unimplemented_Feature as FT_Error
+}
+
+// Use dlsym to check for symbols. If not available. just return an unimplemented error.
+#[cfg(any(not(target_os = "android"), feature = "dynamic_freetype"))]
+macro_rules! ft_dyn_fn {
+ ($func_name:ident($($arg_name:ident:$arg_type:ty),*) -> FT_Error) => {
+ #[allow(non_snake_case)]
+ unsafe fn $func_name($($arg_name:$arg_type),*) -> FT_Error {
+ extern "C" fn unimpl_func($(_:$arg_type),*) -> FT_Error {
+ FT_Err_Unimplemented_Feature as FT_Error
+ }
+ lazy_static! {
+ static ref FUNC: unsafe extern "C" fn($($arg_type),*) -> FT_Error = {
+ unsafe {
+ let cname = CString::new(stringify!($func_name)).unwrap();
+ let ptr = dlsym(RTLD_DEFAULT, cname.as_ptr());
+ if !ptr.is_null() { mem::transmute(ptr) } else { unimpl_func }
+ }
+ };
+ }
+ (*FUNC)($($arg_name),*)
+ }
+ }
+}
+
+// On Android, just statically link in the symbols...
+#[cfg(all(target_os = "android", not(feature = "dynamic_freetype")))]
+macro_rules! ft_dyn_fn {
+ ($($proto:tt)+) => { extern "C" { fn $($proto)+; } }
+}
+
+ft_dyn_fn!(FT_Get_MM_Var(face: FT_Face, desc: *mut *mut FT_MM_Var) -> FT_Error);
+ft_dyn_fn!(FT_Done_MM_Var(library: FT_Library, desc: *mut FT_MM_Var) -> FT_Error);
+ft_dyn_fn!(FT_Set_Var_Design_Coordinates(face: FT_Face, num_vals: FT_UInt, vals: *mut FT_Fixed) -> FT_Error);
+ft_dyn_fn!(FT_Get_Var_Design_Coordinates(face: FT_Face, num_vals: FT_UInt, vals: *mut FT_Fixed) -> FT_Error);
+
+extern "C" {
+ fn FT_GlyphSlot_Embolden(slot: FT_GlyphSlot);
+}
+
+// Custom version of FT_GlyphSlot_Embolden to be less aggressive with outline
+// fonts than the default implementation in FreeType.
+#[no_mangle]
+pub extern "C" fn mozilla_glyphslot_embolden_less(slot: FT_GlyphSlot) {
+ if slot.is_null() {
+ return;
+ }
+
+ let slot_ = unsafe { &mut *slot };
+ let format = slot_.format;
+ if format != FT_Glyph_Format::FT_GLYPH_FORMAT_OUTLINE {
+ // For non-outline glyphs, just fall back to FreeType's function.
+ unsafe { FT_GlyphSlot_Embolden(slot) };
+ return;
+ }
+
+ let face_ = unsafe { *slot_.face };
+
+ // FT_GlyphSlot_Embolden uses a divisor of 24 here; we'll be only half as
+ // bold.
+ let size_ = unsafe { *face_.size };
+ let strength =
+ unsafe { FT_MulFix(face_.units_per_EM as FT_Long,
+ size_.metrics.y_scale) / 48 };
+ unsafe { FT_Outline_Embolden(&mut slot_.outline, strength) };
+
+ // Adjust metrics to suit the fattened glyph.
+ if slot_.advance.x != 0 {
+ slot_.advance.x += strength;
+ }
+ if slot_.advance.y != 0 {
+ slot_.advance.y += strength;
+ }
+ slot_.metrics.width += strength;
+ slot_.metrics.height += strength;
+ slot_.metrics.horiAdvance += strength;
+ slot_.metrics.vertAdvance += strength;
+ slot_.metrics.horiBearingY += strength;
+}
+
+struct CachedFont {
+ template: FontTemplate,
+ face: FT_Face,
+ mm_var: *mut FT_MM_Var,
+ variations: Vec<FontVariation>,
+}
+
+impl Drop for CachedFont {
+ fn drop(&mut self) {
+ unsafe {
+ if !self.mm_var.is_null() &&
+ unimplemented(FT_Done_MM_Var((*(*self.face).glyph).library, self.mm_var)) {
+ free(self.mm_var as _);
+ }
+
+ FT_Done_Face(self.face);
+ }
+ }
+}
+
+struct FontCache {
+ lib: FT_Library,
+ // Maps a template to a cached font that may be used across all threads.
+ fonts: FastHashMap<FontTemplate, Arc<Mutex<CachedFont>>>,
+ // The current LCD filter installed in the library.
+ lcd_filter: FontLCDFilter,
+ // The number of threads currently relying on the LCD filter state.
+ lcd_filter_uses: usize,
+}
+
+// FreeType resources are safe to move between threads as long as they
+// are not concurrently accessed. In our case, everything is behind a
+// Mutex so it is safe to move them between threads.
+unsafe impl Send for CachedFont {}
+unsafe impl Send for FontCache {}
+
+impl FontCache {
+ fn new() -> Self {
+ let mut lib: FT_Library = ptr::null_mut();
+ let result = unsafe { FT_Init_FreeType(&mut lib) };
+ if succeeded(result) {
+ // Ensure the library uses the default LCD filter initially.
+ unsafe { FT_Library_SetLcdFilter(lib, FT_LcdFilter::FT_LCD_FILTER_DEFAULT) };
+ } else {
+ panic!("Failed to initialize FreeType - {}", result)
+ }
+
+ FontCache {
+ lib,
+ fonts: FastHashMap::default(),
+ lcd_filter: FontLCDFilter::Default,
+ lcd_filter_uses: 0,
+ }
+ }
+
+ fn add_font(&mut self, template: FontTemplate) -> Result<Arc<Mutex<CachedFont>>, FT_Error> {
+ if let Some(cached) = self.fonts.get(&template) {
+ return Ok(cached.clone());
+ }
+ unsafe {
+ let mut face: FT_Face = ptr::null_mut();
+ let result = match template {
+ FontTemplate::Raw(ref bytes, index) => {
+ FT_New_Memory_Face(
+ self.lib,
+ bytes.as_ptr(),
+ bytes.len() as FT_Long,
+ index as FT_Long,
+ &mut face,
+ )
+ }
+ FontTemplate::Native(NativeFontHandle { ref path, index }) => {
+ let str = path.as_os_str().to_str().unwrap();
+ let cstr = CString::new(str).unwrap();
+ FT_New_Face(
+ self.lib,
+ cstr.as_ptr(),
+ index as FT_Long,
+ &mut face,
+ )
+ }
+ };
+ if !succeeded(result) || face.is_null() {
+ return Err(result);
+ }
+ let mut mm_var = ptr::null_mut();
+ if ((*face).face_flags & (FT_FACE_FLAG_MULTIPLE_MASTERS as FT_Long)) != 0 &&
+ succeeded(FT_Get_MM_Var(face, &mut mm_var)) {
+ // Calling this before FT_Set_Var_Design_Coordinates avoids a bug with font variations
+ // not initialized properly in the font face, even if we ignore the result.
+ // See bug 1647035.
+ let mut tmp = [0; 16];
+ let res = FT_Get_Var_Design_Coordinates(
+ face,
+ (*mm_var).num_axis.min(16),
+ tmp.as_mut_ptr()
+ );
+ debug_assert!(succeeded(res));
+ }
+ let cached = Arc::new(Mutex::new(CachedFont {
+ template: template.clone(),
+ face,
+ mm_var,
+ variations: Vec::new(),
+ }));
+ self.fonts.insert(template, cached.clone());
+ Ok(cached)
+ }
+ }
+
+ fn delete_font(&mut self, cached: Arc<Mutex<CachedFont>>) {
+ self.fonts.remove(&cached.lock().unwrap().template);
+ }
+}
+
+impl Drop for FontCache {
+ fn drop(&mut self) {
+ self.fonts.clear();
+ unsafe {
+ FT_Done_FreeType(self.lib);
+ }
+ }
+}
+
+lazy_static! {
+ static ref FONT_CACHE: Mutex<FontCache> = Mutex::new(FontCache::new());
+ static ref LCD_FILTER_UNUSED: Condvar = Condvar::new();
+}
+
+pub struct FontContext {
+ fonts: FastHashMap<FontKey, Arc<Mutex<CachedFont>>>,
+}
+
+fn get_skew_bounds(bottom: i32, top: i32, skew_factor: f32, _vertical: bool) -> (f32, f32) {
+ let skew_min = (bottom as f32 + 0.5) * skew_factor;
+ let skew_max = (top as f32 - 0.5) * skew_factor;
+ // Negative skew factor may switch the sense of skew_min and skew_max.
+ (skew_min.min(skew_max).floor(), skew_min.max(skew_max).ceil())
+}
+
+fn skew_bitmap(
+ bitmap: &[u8],
+ width: usize,
+ height: usize,
+ left: i32,
+ top: i32,
+ skew_factor: f32,
+ vertical: bool, // TODO: vertical skew not yet implemented!
+) -> (Vec<u8>, usize, i32) {
+ let stride = width * 4;
+ // Calculate the skewed horizontal offsets of the bottom and top of the glyph.
+ let (skew_min, skew_max) = get_skew_bounds(top - height as i32, top, skew_factor, vertical);
+ // Allocate enough extra width for the min/max skew offsets.
+ let skew_width = width + (skew_max - skew_min) as usize;
+ let mut skew_buffer = vec![0u8; skew_width * height * 4];
+ for y in 0 .. height {
+ // Calculate a skew offset at the vertical center of the current row.
+ let offset = (top as f32 - y as f32 - 0.5) * skew_factor - skew_min;
+ // Get a blend factor in 0..256 constant across all pixels in the row.
+ let blend = (offset.fract() * 256.0) as u32;
+ let src_row = y * stride;
+ let dest_row = (y * skew_width + offset.floor() as usize) * 4;
+ let mut prev_px = [0u32; 4];
+ for (src, dest) in
+ bitmap[src_row .. src_row + stride].chunks(4).zip(
+ skew_buffer[dest_row .. dest_row + stride].chunks_mut(4)
+ ) {
+ let px = [src[0] as u32, src[1] as u32, src[2] as u32, src[3] as u32];
+ // Blend current pixel with previous pixel based on blend factor.
+ let next_px = [px[0] * blend, px[1] * blend, px[2] * blend, px[3] * blend];
+ dest[0] = ((((px[0] << 8) - next_px[0]) + prev_px[0] + 128) >> 8) as u8;
+ dest[1] = ((((px[1] << 8) - next_px[1]) + prev_px[1] + 128) >> 8) as u8;
+ dest[2] = ((((px[2] << 8) - next_px[2]) + prev_px[2] + 128) >> 8) as u8;
+ dest[3] = ((((px[3] << 8) - next_px[3]) + prev_px[3] + 128) >> 8) as u8;
+ // Save the remainder for blending onto the next pixel.
+ prev_px = next_px;
+ }
+ // If the skew misaligns the final pixel, write out the remainder.
+ if blend > 0 {
+ let dest = &mut skew_buffer[dest_row + stride .. dest_row + stride + 4];
+ dest[0] = ((prev_px[0] + 128) >> 8) as u8;
+ dest[1] = ((prev_px[1] + 128) >> 8) as u8;
+ dest[2] = ((prev_px[2] + 128) >> 8) as u8;
+ dest[3] = ((prev_px[3] + 128) >> 8) as u8;
+ }
+ }
+ (skew_buffer, skew_width, left + skew_min as i32)
+}
+
+fn transpose_bitmap(bitmap: &[u8], width: usize, height: usize) -> Vec<u8> {
+ let mut transposed = vec![0u8; width * height * 4];
+ for (y, row) in bitmap.chunks(width * 4).enumerate() {
+ let mut offset = y * 4;
+ for src in row.chunks(4) {
+ transposed[offset .. offset + 4].copy_from_slice(src);
+ offset += height * 4;
+ }
+ }
+ transposed
+}
+
+fn flip_bitmap_x(bitmap: &mut [u8], width: usize, height: usize) {
+ assert!(bitmap.len() == width * height * 4);
+ let pixels = unsafe { slice::from_raw_parts_mut(bitmap.as_mut_ptr() as *mut u32, width * height) };
+ for row in pixels.chunks_mut(width) {
+ row.reverse();
+ }
+}
+
+fn flip_bitmap_y(bitmap: &mut [u8], width: usize, height: usize) {
+ assert!(bitmap.len() == width * height * 4);
+ let pixels = unsafe { slice::from_raw_parts_mut(bitmap.as_mut_ptr() as *mut u32, width * height) };
+ for y in 0 .. height / 2 {
+ let low_row = y * width;
+ let high_row = (height - 1 - y) * width;
+ for x in 0 .. width {
+ pixels.swap(low_row + x, high_row + x);
+ }
+ }
+}
+
+impl FontContext {
+ pub fn distribute_across_threads() -> bool {
+ false
+ }
+
+ pub fn new() -> FontContext {
+ FontContext {
+ fonts: FastHashMap::default(),
+ }
+ }
+
+ pub fn add_raw_font(&mut self, font_key: &FontKey, bytes: Arc<Vec<u8>>, index: u32) {
+ if !self.fonts.contains_key(font_key) {
+ let len = bytes.len();
+ match FONT_CACHE.lock().unwrap().add_font(FontTemplate::Raw(bytes, index)) {
+ Ok(font) => self.fonts.insert(*font_key, font),
+ Err(result) => panic!("adding raw font failed: {} bytes, err={:?}", len, result),
+ };
+ }
+ }
+
+ pub fn add_native_font(&mut self, font_key: &FontKey, native_font_handle: NativeFontHandle) {
+ if !self.fonts.contains_key(font_key) {
+ let path = native_font_handle.path.to_string_lossy().into_owned();
+ match FONT_CACHE.lock().unwrap().add_font(FontTemplate::Native(native_font_handle)) {
+ Ok(font) => self.fonts.insert(*font_key, font),
+ Err(result) => panic!("adding native font failed: file={} err={:?}", path, result),
+ };
+ }
+ }
+
+ pub fn delete_font(&mut self, font_key: &FontKey) {
+ if let Some(cached) = self.fonts.remove(font_key) {
+ // If the only references to this font are the FontCache and this FontContext,
+ // then delete the font as there are no other existing users.
+ if Arc::strong_count(&cached) <= 2 {
+ FONT_CACHE.lock().unwrap().delete_font(cached);
+ }
+ }
+ }
+
+ pub fn delete_font_instance(&mut self, _instance: &FontInstance) {
+ }
+
+ fn load_glyph(&mut self, font: &FontInstance, glyph: &GlyphKey)
+ -> Option<(MutexGuard<CachedFont>, FT_GlyphSlot, f32)> {
+ let mut cached = self.fonts.get(&font.font_key)?.lock().ok()?;
+ let face = cached.face;
+
+ let mm_var = cached.mm_var;
+ if !mm_var.is_null() && font.variations != cached.variations {
+ cached.variations.clear();
+ cached.variations.extend_from_slice(&font.variations);
+
+ unsafe {
+ let num_axis = (*mm_var).num_axis;
+ let mut coords: Vec<FT_Fixed> = Vec::with_capacity(num_axis as usize);
+ for i in 0 .. num_axis {
+ let axis = (*mm_var).axis.offset(i as isize);
+ let mut value = (*axis).def;
+ for var in &font.variations {
+ if var.tag as FT_ULong == (*axis).tag {
+ value = (var.value * 65536.0 + 0.5) as FT_Fixed;
+ value = cmp::min(value, (*axis).maximum);
+ value = cmp::max(value, (*axis).minimum);
+ break;
+ }
+ }
+ coords.push(value);
+ }
+ let res = FT_Set_Var_Design_Coordinates(face, num_axis, coords.as_mut_ptr());
+ debug_assert!(succeeded(res));
+ }
+ }
+
+ let mut load_flags = FT_LOAD_DEFAULT;
+ let FontInstancePlatformOptions { mut hinting, .. } = font.platform_options.unwrap_or_default();
+ // Disable hinting if there is a non-axis-aligned transform.
+ if font.synthetic_italics.is_enabled() ||
+ ((font.transform.scale_x != 0.0 || font.transform.scale_y != 0.0) &&
+ (font.transform.skew_x != 0.0 || font.transform.skew_y != 0.0)) {
+ hinting = FontHinting::None;
+ }
+ match (hinting, font.render_mode) {
+ (FontHinting::None, _) => load_flags |= FT_LOAD_NO_HINTING,
+ (FontHinting::Mono, _) => load_flags = FT_LOAD_TARGET_MONO,
+ (FontHinting::Light, _) => load_flags = FT_LOAD_TARGET_LIGHT,
+ (FontHinting::LCD, FontRenderMode::Subpixel) => {
+ load_flags = if font.flags.contains(FontInstanceFlags::LCD_VERTICAL) {
+ FT_LOAD_TARGET_LCD_V
+ } else {
+ FT_LOAD_TARGET_LCD
+ };
+ if font.flags.contains(FontInstanceFlags::FORCE_AUTOHINT) {
+ load_flags |= FT_LOAD_FORCE_AUTOHINT;
+ }
+ }
+ _ => {
+ if font.flags.contains(FontInstanceFlags::FORCE_AUTOHINT) {
+ load_flags |= FT_LOAD_FORCE_AUTOHINT;
+ }
+ }
+ }
+
+ if font.flags.contains(FontInstanceFlags::NO_AUTOHINT) {
+ load_flags |= FT_LOAD_NO_AUTOHINT;
+ }
+ if !font.flags.contains(FontInstanceFlags::EMBEDDED_BITMAPS) {
+ load_flags |= FT_LOAD_NO_BITMAP;
+ }
+
+ let face_flags = unsafe { (*face).face_flags };
+ if (face_flags & (FT_FACE_FLAG_FIXED_SIZES as FT_Long)) != 0 {
+ // We only set FT_LOAD_COLOR if there are bitmap strikes;
+ // COLR (color-layer) fonts are handled internally by Gecko, and
+ // WebRender is just asked to paint individual layers.
+ load_flags |= FT_LOAD_COLOR;
+ }
+
+ load_flags |= FT_LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH;
+
+ let (x_scale, y_scale) = font.transform.compute_scale().unwrap_or((1.0, 1.0));
+ let req_size = font.size.to_f64_px();
+
+ let mut result = if (face_flags & (FT_FACE_FLAG_FIXED_SIZES as FT_Long)) != 0 &&
+ (face_flags & (FT_FACE_FLAG_SCALABLE as FT_Long)) == 0 &&
+ (load_flags & FT_LOAD_NO_BITMAP) == 0 {
+ unsafe { FT_Set_Transform(face, ptr::null_mut(), ptr::null_mut()) };
+ self.choose_bitmap_size(face, req_size * y_scale)
+ } else {
+ let mut shape = font.transform.invert_scale(x_scale, y_scale);
+ if font.flags.contains(FontInstanceFlags::FLIP_X) {
+ shape = shape.flip_x();
+ }
+ if font.flags.contains(FontInstanceFlags::FLIP_Y) {
+ shape = shape.flip_y();
+ }
+ if font.flags.contains(FontInstanceFlags::TRANSPOSE) {
+ shape = shape.swap_xy();
+ }
+ let (mut tx, mut ty) = (0.0, 0.0);
+ if font.synthetic_italics.is_enabled() {
+ let (shape_, (tx_, ty_)) = font.synthesize_italics(shape, y_scale * req_size);
+ shape = shape_;
+ tx = tx_;
+ ty = ty_;
+ };
+ let mut ft_shape = FT_Matrix {
+ xx: (shape.scale_x * 65536.0) as FT_Fixed,
+ xy: (shape.skew_x * -65536.0) as FT_Fixed,
+ yx: (shape.skew_y * -65536.0) as FT_Fixed,
+ yy: (shape.scale_y * 65536.0) as FT_Fixed,
+ };
+ // The delta vector for FT_Set_Transform is in units of 1/64 pixel.
+ let mut ft_delta = FT_Vector {
+ x: (tx * 64.0) as FT_F26Dot6,
+ y: (ty * -64.0) as FT_F26Dot6,
+ };
+ unsafe {
+ FT_Set_Transform(face, &mut ft_shape, &mut ft_delta);
+ FT_Set_Char_Size(
+ face,
+ (req_size * x_scale * 64.0 + 0.5) as FT_F26Dot6,
+ (req_size * y_scale * 64.0 + 0.5) as FT_F26Dot6,
+ 0,
+ 0,
+ )
+ }
+ };
+
+ if !succeeded(result) {
+ error!("Unable to set glyph size and transform: {}", result);
+ //let raw_error = unsafe { FT_Error_String(result) };
+ //if !raw_error.is_ptr() {
+ // error!("\tcode {:?}", CStr::from_ptr(raw_error));
+ //}
+ debug!(
+ "\t[{}] for size {:?} and scale {:?} from font {:?}",
+ glyph.index(),
+ req_size,
+ (x_scale, y_scale),
+ font.font_key,
+ );
+ return None;
+ }
+
+ result = unsafe { FT_Load_Glyph(face, glyph.index() as FT_UInt, load_flags as FT_Int32) };
+ if !succeeded(result) {
+ error!("Unable to load glyph: {}", result);
+ //let raw_error = unsafe { FT_Error_String(result) };
+ //if !raw_error.is_ptr() {
+ // error!("\tcode {:?}", CStr::from_ptr(raw_error));
+ //}
+ debug!(
+ "\t[{}] with flags {:?} from font {:?}",
+ glyph.index(),
+ load_flags,
+ font.font_key,
+ );
+ return None;
+ }
+
+ let slot = unsafe { (*face).glyph };
+ assert!(slot != ptr::null_mut());
+
+ if font.flags.contains(FontInstanceFlags::SYNTHETIC_BOLD) {
+ mozilla_glyphslot_embolden_less(slot);
+ }
+
+ let format = unsafe { (*slot).format };
+ match format {
+ FT_Glyph_Format::FT_GLYPH_FORMAT_BITMAP => {
+ let bitmap_size = unsafe { (*(*(*slot).face).size).metrics.y_ppem };
+ Some((cached, slot, req_size as f32 / bitmap_size as f32))
+ }
+ FT_Glyph_Format::FT_GLYPH_FORMAT_OUTLINE => Some((cached, slot, 1.0)),
+ _ => {
+ error!("Unsupported format");
+ debug!("format={:?}", format);
+ None
+ }
+ }
+ }
+
+ fn pad_bounding_box(font: &FontInstance, cbox: &mut FT_BBox) {
+ // Apply extra pixel of padding for subpixel AA, due to the filter.
+ if font.render_mode == FontRenderMode::Subpixel {
+ // Using an LCD filter may add one full pixel to each side if support is built in.
+ // As of FreeType 2.8.1, an LCD filter is always used regardless of settings
+ // if support for the patent-encumbered LCD filter algorithms is not built in.
+ // Thus, the only reasonable way to guess padding is to unconditonally add it if
+ // subpixel AA is used.
+ let lcd_extra_pixels = 1;
+ let padding = (lcd_extra_pixels * 64) as FT_Pos;
+ if font.flags.contains(FontInstanceFlags::LCD_VERTICAL) {
+ cbox.yMin -= padding;
+ cbox.yMax += padding;
+ } else {
+ cbox.xMin -= padding;
+ cbox.xMax += padding;
+ }
+ }
+ }
+
+ // Get the bounding box for a glyph, accounting for sub-pixel positioning.
+ fn get_bounding_box(
+ slot: FT_GlyphSlot,
+ font: &FontInstance,
+ glyph: &GlyphKey,
+ scale: f32,
+ ) -> FT_BBox {
+ // Get the estimated bounding box from FT (control points).
+ let mut cbox = FT_BBox { xMin: 0, yMin: 0, xMax: 0, yMax: 0 };
+
+ unsafe {
+ FT_Outline_Get_CBox(&(*slot).outline, &mut cbox);
+ }
+
+ // For spaces and other non-printable characters, early out.
+ if unsafe { (*slot).outline.n_contours } == 0 {
+ return cbox;
+ }
+
+ Self::pad_bounding_box(font, &mut cbox);
+
+ // Offset the bounding box by subpixel positioning.
+ // Convert to 26.6 fixed point format for FT.
+ let (dx, dy) = font.get_subpx_offset(glyph);
+ let (dx, dy) = (
+ (dx / scale as f64 * 64.0 + 0.5) as FT_Pos,
+ -(dy / scale as f64 * 64.0 + 0.5) as FT_Pos,
+ );
+ cbox.xMin += dx;
+ cbox.xMax += dx;
+ cbox.yMin += dy;
+ cbox.yMax += dy;
+
+ // Outset the box to device pixel boundaries
+ cbox.xMin &= !63;
+ cbox.yMin &= !63;
+ cbox.xMax = (cbox.xMax + 63) & !63;
+ cbox.yMax = (cbox.yMax + 63) & !63;
+
+ cbox
+ }
+
+ fn get_glyph_dimensions_impl(
+ slot: FT_GlyphSlot,
+ font: &FontInstance,
+ glyph: &GlyphKey,
+ scale: f32,
+ use_transform: bool,
+ ) -> Option<GlyphDimensions> {
+ let format = unsafe { (*slot).format };
+ let (mut left, mut top, mut width, mut height) = match format {
+ FT_Glyph_Format::FT_GLYPH_FORMAT_BITMAP => {
+ unsafe { (
+ (*slot).bitmap_left as i32,
+ (*slot).bitmap_top as i32,
+ (*slot).bitmap.width as i32,
+ (*slot).bitmap.rows as i32,
+ ) }
+ }
+ FT_Glyph_Format::FT_GLYPH_FORMAT_OUTLINE => {
+ let cbox = Self::get_bounding_box(slot, font, glyph, scale);
+ (
+ (cbox.xMin >> 6) as i32,
+ (cbox.yMax >> 6) as i32,
+ ((cbox.xMax - cbox.xMin) >> 6) as i32,
+ ((cbox.yMax - cbox.yMin) >> 6) as i32,
+ )
+ }
+ _ => return None,
+ };
+ let mut advance = unsafe { (*slot).metrics.horiAdvance as f32 / 64.0 };
+ if use_transform {
+ if scale != 1.0 {
+ let x0 = left as f32 * scale;
+ let x1 = width as f32 * scale + x0;
+ let y1 = top as f32 * scale;
+ let y0 = y1 - height as f32 * scale;
+ left = x0.round() as i32;
+ top = y1.round() as i32;
+ width = (x1.ceil() - x0.floor()) as i32;
+ height = (y1.ceil() - y0.floor()) as i32;
+ advance *= scale;
+ }
+ // An outline glyph's cbox would have already been transformed inside FT_Load_Glyph,
+ // so only handle bitmap glyphs which are not handled by FT_Load_Glyph.
+ if format == FT_Glyph_Format::FT_GLYPH_FORMAT_BITMAP {
+ if font.synthetic_italics.is_enabled() {
+ let (skew_min, skew_max) = get_skew_bounds(
+ top - height as i32,
+ top,
+ font.synthetic_italics.to_skew(),
+ font.flags.contains(FontInstanceFlags::VERTICAL),
+ );
+ left += skew_min as i32;
+ width += (skew_max - skew_min) as i32;
+ }
+ if font.flags.contains(FontInstanceFlags::TRANSPOSE) {
+ mem::swap(&mut width, &mut height);
+ mem::swap(&mut left, &mut top);
+ left -= width as i32;
+ top += height as i32;
+ }
+ if font.flags.contains(FontInstanceFlags::FLIP_X) {
+ left = -(left + width as i32);
+ }
+ if font.flags.contains(FontInstanceFlags::FLIP_Y) {
+ top = -(top - height as i32);
+ }
+ }
+ }
+ Some(GlyphDimensions {
+ left,
+ top,
+ width,
+ height,
+ advance,
+ })
+ }
+
+ pub fn get_glyph_index(&mut self, font_key: FontKey, ch: char) -> Option<u32> {
+ let cached = self.fonts.get(&font_key)?.lock().ok()?;
+ let face = cached.face;
+ unsafe {
+ let idx = FT_Get_Char_Index(face, ch as _);
+ if idx != 0 {
+ Some(idx)
+ } else {
+ None
+ }
+ }
+ }
+
+ pub fn get_glyph_dimensions(
+ &mut self,
+ font: &FontInstance,
+ key: &GlyphKey,
+ ) -> Option<GlyphDimensions> {
+ let (_cached, slot, scale) = self.load_glyph(font, key)?;
+ Self::get_glyph_dimensions_impl(slot, &font, key, scale, true)
+ }
+
+ fn choose_bitmap_size(&self, face: FT_Face, requested_size: f64) -> FT_Error {
+ let mut best_dist = unsafe { *(*face).available_sizes.offset(0) }.y_ppem as f64 / 64.0 - requested_size;
+ let mut best_size = 0;
+ let num_fixed_sizes = unsafe { (*face).num_fixed_sizes };
+ for i in 1 .. num_fixed_sizes {
+ // Distance is positive if strike is larger than desired size,
+ // or negative if smaller. If previously a found smaller strike,
+ // then prefer a larger strike. Otherwise, minimize distance.
+ let dist = unsafe { *(*face).available_sizes.offset(i as isize) }.y_ppem as f64 / 64.0 - requested_size;
+ if (best_dist < 0.0 && dist >= best_dist) || dist.abs() <= best_dist {
+ best_dist = dist;
+ best_size = i;
+ }
+ }
+ unsafe { FT_Select_Size(face, best_size) }
+ }
+
+ pub fn prepare_font(font: &mut FontInstance) {
+ match font.render_mode {
+ FontRenderMode::Mono => {
+ // In mono mode the color of the font is irrelevant.
+ font.color = ColorU::new(0xFF, 0xFF, 0xFF, 0xFF);
+ // Subpixel positioning is disabled in mono mode.
+ font.disable_subpixel_position();
+ }
+ FontRenderMode::Alpha | FontRenderMode::Subpixel => {
+ // We don't do any preblending with FreeType currently, so the color is not used.
+ font.color = ColorU::new(0xFF, 0xFF, 0xFF, 0xFF);
+ }
+ }
+ }
+
+ fn rasterize_glyph_outline(
+ slot: FT_GlyphSlot,
+ font: &FontInstance,
+ key: &GlyphKey,
+ scale: f32,
+ ) -> bool {
+ // Get the subpixel offsets in FT 26.6 format.
+ let (dx, dy) = font.get_subpx_offset(key);
+ let (dx, dy) = (
+ (dx / scale as f64 * 64.0 + 0.5) as FT_Pos,
+ -(dy / scale as f64 * 64.0 + 0.5) as FT_Pos,
+ );
+
+ // Move the outline curves to be at the origin, taking
+ // into account the subpixel positioning.
+ unsafe {
+ let outline = &(*slot).outline;
+ let mut cbox = FT_BBox { xMin: 0, yMin: 0, xMax: 0, yMax: 0 };
+ FT_Outline_Get_CBox(outline, &mut cbox);
+ Self::pad_bounding_box(font, &mut cbox);
+ FT_Outline_Translate(
+ outline,
+ dx - ((cbox.xMin + dx) & !63),
+ dy - ((cbox.yMin + dy) & !63),
+ );
+ }
+
+ let render_mode = match font.render_mode {
+ FontRenderMode::Mono => FT_Render_Mode::FT_RENDER_MODE_MONO,
+ FontRenderMode::Alpha => FT_Render_Mode::FT_RENDER_MODE_NORMAL,
+ FontRenderMode::Subpixel => if font.flags.contains(FontInstanceFlags::LCD_VERTICAL) {
+ FT_Render_Mode::FT_RENDER_MODE_LCD_V
+ } else {
+ FT_Render_Mode::FT_RENDER_MODE_LCD
+ },
+ };
+ let result = unsafe { FT_Render_Glyph(slot, render_mode) };
+ if !succeeded(result) {
+ error!("Unable to rasterize");
+ debug!(
+ "{:?} with {:?}, {:?}",
+ key,
+ render_mode,
+ result
+ );
+ false
+ } else {
+ true
+ }
+ }
+
+ pub fn begin_rasterize(font: &FontInstance) {
+ // The global LCD filter state is only used in subpixel rendering modes.
+ if font.render_mode == FontRenderMode::Subpixel {
+ let mut cache = FONT_CACHE.lock().unwrap();
+ let FontInstancePlatformOptions { lcd_filter, .. } = font.platform_options.unwrap_or_default();
+ // Check if the current LCD filter matches the requested one.
+ if cache.lcd_filter != lcd_filter {
+ // If the filter doesn't match, we have to wait for all other currently rasterizing threads
+ // that may use the LCD filter state to finish before we can override it.
+ while cache.lcd_filter_uses != 0 {
+ cache = LCD_FILTER_UNUSED.wait(cache).unwrap();
+ }
+ // Finally set the LCD filter to the requested one now that the library is unused.
+ cache.lcd_filter = lcd_filter;
+ let filter = match lcd_filter {
+ FontLCDFilter::None => FT_LcdFilter::FT_LCD_FILTER_NONE,
+ FontLCDFilter::Default => FT_LcdFilter::FT_LCD_FILTER_DEFAULT,
+ FontLCDFilter::Light => FT_LcdFilter::FT_LCD_FILTER_LIGHT,
+ FontLCDFilter::Legacy => FT_LcdFilter::FT_LCD_FILTER_LEGACY,
+ };
+ unsafe {
+ let result = FT_Library_SetLcdFilter(cache.lib, filter);
+ // Setting the legacy filter may fail, so just use the default filter instead.
+ if !succeeded(result) {
+ FT_Library_SetLcdFilter(cache.lib, FT_LcdFilter::FT_LCD_FILTER_DEFAULT);
+ }
+ }
+ }
+ cache.lcd_filter_uses += 1;
+ }
+ }
+
+ pub fn end_rasterize(font: &FontInstance) {
+ if font.render_mode == FontRenderMode::Subpixel {
+ let mut cache = FONT_CACHE.lock().unwrap();
+ // If this is the last use of the LCD filter, then signal that the LCD filter isn't used.
+ cache.lcd_filter_uses -= 1;
+ if cache.lcd_filter_uses == 0 {
+ LCD_FILTER_UNUSED.notify_all();
+ }
+ }
+ }
+
+ pub fn rasterize_glyph(&mut self, font: &FontInstance, key: &GlyphKey) -> GlyphRasterResult {
+ let (_cached, slot, scale) = self.load_glyph(font, key)
+ .ok_or(GlyphRasterError::LoadFailed)?;
+
+ // Get dimensions of the glyph, to see if we need to rasterize it.
+ // Don't apply scaling to the dimensions, as the glyph cache needs to know the actual
+ // footprint of the glyph.
+ let dimensions = Self::get_glyph_dimensions_impl(slot, font, key, scale, false)
+ .ok_or(GlyphRasterError::LoadFailed)?;
+ let GlyphDimensions { mut left, mut top, width, height, .. } = dimensions;
+
+ // For spaces and other non-printable characters, early out.
+ if width == 0 || height == 0 {
+ return Err(GlyphRasterError::LoadFailed);
+ }
+
+ let format = unsafe { (*slot).format };
+ match format {
+ FT_Glyph_Format::FT_GLYPH_FORMAT_BITMAP => {}
+ FT_Glyph_Format::FT_GLYPH_FORMAT_OUTLINE => {
+ if !Self::rasterize_glyph_outline(slot, font, key, scale) {
+ return Err(GlyphRasterError::LoadFailed);
+ }
+ }
+ _ => {
+ error!("Unsupported format");
+ debug!("format={:?}", format);
+ return Err(GlyphRasterError::LoadFailed);
+ }
+ };
+
+ debug!(
+ "Rasterizing {:?} as {:?} with dimensions {:?}",
+ key,
+ font.render_mode,
+ dimensions
+ );
+
+ let bitmap = unsafe { &(*slot).bitmap };
+ let pixel_mode = unsafe { mem::transmute(bitmap.pixel_mode as u32) };
+ let (mut actual_width, mut actual_height) = match pixel_mode {
+ FT_Pixel_Mode::FT_PIXEL_MODE_LCD => {
+ assert!(bitmap.width % 3 == 0);
+ ((bitmap.width / 3) as usize, bitmap.rows as usize)
+ }
+ FT_Pixel_Mode::FT_PIXEL_MODE_LCD_V => {
+ assert!(bitmap.rows % 3 == 0);
+ (bitmap.width as usize, (bitmap.rows / 3) as usize)
+ }
+ FT_Pixel_Mode::FT_PIXEL_MODE_MONO |
+ FT_Pixel_Mode::FT_PIXEL_MODE_GRAY |
+ FT_Pixel_Mode::FT_PIXEL_MODE_BGRA => {
+ (bitmap.width as usize, bitmap.rows as usize)
+ }
+ _ => panic!("Unsupported mode"),
+ };
+
+ // If we need padding, we will need to expand the buffer size.
+ let (buffer_width, buffer_height, padding) = if font.use_texture_padding() {
+ (actual_width + 2, actual_height + 2, 1)
+ } else {
+ (actual_width, actual_height, 0)
+ };
+
+ let mut final_buffer = vec![0u8; buffer_width * buffer_height * 4];
+
+ // Extract the final glyph from FT format into BGRA8 format, which is
+ // what WR expects.
+ let subpixel_bgr = font.flags.contains(FontInstanceFlags::SUBPIXEL_BGR);
+ let mut src_row = bitmap.buffer;
+ let mut dest = 4 * padding * (padding + buffer_width);
+ let actual_end = final_buffer.len() - 4 * padding * (buffer_width + 1);
+ while dest < actual_end {
+ let mut src = src_row;
+ let row_end = dest + actual_width * 4;
+ match pixel_mode {
+ FT_Pixel_Mode::FT_PIXEL_MODE_MONO => {
+ while dest < row_end {
+ // Cast the byte to signed so that we can left shift each bit into
+ // the top bit, then right shift to fill out the bits with 0s or 1s.
+ let mut byte: i8 = unsafe { *src as i8 };
+ src = unsafe { src.offset(1) };
+ let byte_end = cmp::min(row_end, dest + 8 * 4);
+ while dest < byte_end {
+ let alpha = (byte >> 7) as u8;
+ final_buffer[dest + 0] = alpha;
+ final_buffer[dest + 1] = alpha;
+ final_buffer[dest + 2] = alpha;
+ final_buffer[dest + 3] = alpha;
+ dest += 4;
+ byte <<= 1;
+ }
+ }
+ }
+ FT_Pixel_Mode::FT_PIXEL_MODE_GRAY => {
+ while dest < row_end {
+ let alpha = unsafe { *src };
+ final_buffer[dest + 0] = alpha;
+ final_buffer[dest + 1] = alpha;
+ final_buffer[dest + 2] = alpha;
+ final_buffer[dest + 3] = alpha;
+ src = unsafe { src.offset(1) };
+ dest += 4;
+ }
+ }
+ FT_Pixel_Mode::FT_PIXEL_MODE_LCD => {
+ while dest < row_end {
+ let (mut r, g, mut b) = unsafe { (*src, *src.offset(1), *src.offset(2)) };
+ if subpixel_bgr {
+ mem::swap(&mut r, &mut b);
+ }
+ final_buffer[dest + 0] = b;
+ final_buffer[dest + 1] = g;
+ final_buffer[dest + 2] = r;
+ final_buffer[dest + 3] = max(max(b, g), r);
+ src = unsafe { src.offset(3) };
+ dest += 4;
+ }
+ }
+ FT_Pixel_Mode::FT_PIXEL_MODE_LCD_V => {
+ while dest < row_end {
+ let (mut r, g, mut b) =
+ unsafe { (*src, *src.offset(bitmap.pitch as isize),
+ *src.offset((2 * bitmap.pitch) as isize)) };
+ if subpixel_bgr {
+ mem::swap(&mut r, &mut b);
+ }
+ final_buffer[dest + 0] = b;
+ final_buffer[dest + 1] = g;
+ final_buffer[dest + 2] = r;
+ final_buffer[dest + 3] = max(max(b, g), r);
+ src = unsafe { src.offset(1) };
+ dest += 4;
+ }
+ src_row = unsafe { src_row.offset((2 * bitmap.pitch) as isize) };
+ }
+ FT_Pixel_Mode::FT_PIXEL_MODE_BGRA => {
+ // The source is premultiplied BGRA data.
+ let dest_slice = &mut final_buffer[dest .. row_end];
+ let src_slice = unsafe { slice::from_raw_parts(src, dest_slice.len()) };
+ dest_slice.copy_from_slice(src_slice);
+ }
+ _ => panic!("Unsupported mode"),
+ }
+ src_row = unsafe { src_row.offset(bitmap.pitch as isize) };
+ dest = row_end + 8 * padding;
+ }
+
+ if font.use_texture_padding() {
+ left -= padding as i32;
+ top += padding as i32;
+ actual_width = buffer_width;
+ actual_height = buffer_height;
+ }
+
+ match format {
+ FT_Glyph_Format::FT_GLYPH_FORMAT_BITMAP => {
+ if font.synthetic_italics.is_enabled() {
+ let (skew_buffer, skew_width, skew_left) = skew_bitmap(
+ &final_buffer,
+ actual_width,
+ actual_height,
+ left,
+ top,
+ font.synthetic_italics.to_skew(),
+ font.flags.contains(FontInstanceFlags::VERTICAL),
+ );
+ final_buffer = skew_buffer;
+ actual_width = skew_width;
+ left = skew_left;
+ }
+ if font.flags.contains(FontInstanceFlags::TRANSPOSE) {
+ final_buffer = transpose_bitmap(&final_buffer, actual_width, actual_height);
+ mem::swap(&mut actual_width, &mut actual_height);
+ mem::swap(&mut left, &mut top);
+ left -= actual_width as i32;
+ top += actual_height as i32;
+ }
+ if font.flags.contains(FontInstanceFlags::FLIP_X) {
+ flip_bitmap_x(&mut final_buffer, actual_width, actual_height);
+ left = -(left + actual_width as i32);
+ }
+ if font.flags.contains(FontInstanceFlags::FLIP_Y) {
+ flip_bitmap_y(&mut final_buffer, actual_width, actual_height);
+ top = -(top - actual_height as i32);
+ }
+ }
+ FT_Glyph_Format::FT_GLYPH_FORMAT_OUTLINE => {
+ unsafe {
+ left += (*slot).bitmap_left;
+ top += (*slot).bitmap_top - height as i32;
+ }
+ }
+ _ => {}
+ }
+
+ let glyph_format = match (pixel_mode, format) {
+ (FT_Pixel_Mode::FT_PIXEL_MODE_LCD, _) |
+ (FT_Pixel_Mode::FT_PIXEL_MODE_LCD_V, _) => font.get_subpixel_glyph_format(),
+ (FT_Pixel_Mode::FT_PIXEL_MODE_BGRA, _) => GlyphFormat::ColorBitmap,
+ (_, FT_Glyph_Format::FT_GLYPH_FORMAT_BITMAP) => GlyphFormat::Bitmap,
+ _ => font.get_alpha_glyph_format(),
+ };
+
+ Ok(RasterizedGlyph {
+ left: left as f32,
+ top: top as f32,
+ width: actual_width as i32,
+ height: actual_height as i32,
+ scale,
+ format: glyph_format,
+ bytes: final_buffer,
+ })
+ }
+}
+
diff --git a/gfx/wr/wr_glyph_rasterizer/src/platform/windows/font.rs b/gfx/wr/wr_glyph_rasterizer/src/platform/windows/font.rs
new file mode 100644
index 0000000000..81232d70c4
--- /dev/null
+++ b/gfx/wr/wr_glyph_rasterizer/src/platform/windows/font.rs
@@ -0,0 +1,639 @@
+/* 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 api::{FontInstanceFlags, FontKey, FontRenderMode, FontVariation};
+use api::{ColorU, GlyphDimensions, NativeFontHandle};
+use dwrote;
+use crate::gamma_lut::{ColorLut, GammaLut};
+use crate::rasterizer::{FontInstance, FontTransform, GlyphKey};
+use crate::rasterizer::{GlyphFormat, GlyphRasterError, GlyphRasterResult, RasterizedGlyph};
+use crate::rasterizer::apply_multistrike_bold;
+use crate::types::{FastHashMap, FastHashSet};
+use std::borrow::Borrow;
+use std::collections::hash_map::Entry;
+use std::hash::{Hash, Hasher};
+use std::path::Path;
+use std::sync::{Arc, Mutex};
+use api::FontInstancePlatformOptions;
+use std::mem;
+
+lazy_static! {
+ static ref DEFAULT_FONT_DESCRIPTOR: dwrote::FontDescriptor = dwrote::FontDescriptor {
+ family_name: "Arial".to_owned(),
+ weight: dwrote::FontWeight::Regular,
+ stretch: dwrote::FontStretch::Normal,
+ style: dwrote::FontStyle::Normal,
+ };
+}
+
+type CachedFontKey = Arc<Path>;
+
+// A cached dwrote font file that is shared among all faces.
+// Each face holds a CachedFontKey to keep track of how many users of the font there are.
+struct CachedFont {
+ key: CachedFontKey,
+ file: dwrote::FontFile,
+}
+
+// FontFile contains a ComPtr<IDWriteFontFile>, but DWrite font files are threadsafe.
+unsafe impl Send for CachedFont {}
+
+impl PartialEq for CachedFont {
+ fn eq(&self, other: &CachedFont) -> bool {
+ self.key == other.key
+ }
+}
+impl Eq for CachedFont {}
+
+impl Hash for CachedFont {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.key.hash(state);
+ }
+}
+
+impl Borrow<Path> for CachedFont {
+ fn borrow(&self) -> &Path {
+ &*self.key
+ }
+}
+
+lazy_static! {
+ // This is effectively a weak map of dwrote FontFiles. CachedFonts are entered into the
+ // cache when there are any FontFaces using them. CachedFonts are removed from the cache
+ // when there are no more FontFaces using them at all.
+ static ref FONT_CACHE: Mutex<FastHashSet<CachedFont>> = Mutex::new(FastHashSet::default());
+}
+
+struct FontFace {
+ cached: Option<CachedFontKey>,
+ file: dwrote::FontFile,
+ index: u32,
+ face: dwrote::FontFace,
+}
+
+pub struct FontContext {
+ fonts: FastHashMap<FontKey, FontFace>,
+ variations: FastHashMap<(FontKey, dwrote::DWRITE_FONT_SIMULATIONS, Vec<FontVariation>), dwrote::FontFace>,
+ gamma_luts: FastHashMap<(u16, u8), GammaLut>,
+}
+
+// DirectWrite is safe to use on multiple threads and non-shareable resources are
+// all hidden inside their font context.
+unsafe impl Send for FontContext {}
+
+fn dwrite_texture_type(render_mode: FontRenderMode) -> dwrote::DWRITE_TEXTURE_TYPE {
+ match render_mode {
+ FontRenderMode::Mono => dwrote::DWRITE_TEXTURE_ALIASED_1x1,
+ FontRenderMode::Alpha |
+ FontRenderMode::Subpixel => dwrote::DWRITE_TEXTURE_CLEARTYPE_3x1,
+ }
+}
+
+fn dwrite_measure_mode(
+ font: &FontInstance,
+ bitmaps: bool,
+) -> dwrote::DWRITE_MEASURING_MODE {
+ if bitmaps || font.flags.contains(FontInstanceFlags::FORCE_GDI) {
+ dwrote::DWRITE_MEASURING_MODE_GDI_CLASSIC
+ } else {
+ match font.render_mode {
+ FontRenderMode::Mono => dwrote::DWRITE_MEASURING_MODE_GDI_CLASSIC,
+ FontRenderMode::Alpha | FontRenderMode::Subpixel => dwrote::DWRITE_MEASURING_MODE_NATURAL,
+ }
+ }
+}
+
+fn dwrite_render_mode(
+ font_face: &dwrote::FontFace,
+ font: &FontInstance,
+ em_size: f32,
+ measure_mode: dwrote::DWRITE_MEASURING_MODE,
+ bitmaps: bool,
+) -> dwrote::DWRITE_RENDERING_MODE {
+ let dwrite_render_mode = match font.render_mode {
+ FontRenderMode::Mono => dwrote::DWRITE_RENDERING_MODE_ALIASED,
+ FontRenderMode::Alpha | FontRenderMode::Subpixel => {
+ if bitmaps || font.flags.contains(FontInstanceFlags::FORCE_GDI) {
+ dwrote::DWRITE_RENDERING_MODE_GDI_CLASSIC
+ } else if font.flags.contains(FontInstanceFlags::FORCE_SYMMETRIC) {
+ dwrote::DWRITE_RENDERING_MODE_CLEARTYPE_NATURAL_SYMMETRIC
+ } else if font.flags.contains(FontInstanceFlags::NO_SYMMETRIC) {
+ dwrote::DWRITE_RENDERING_MODE_CLEARTYPE_NATURAL
+ } else {
+ font_face.get_recommended_rendering_mode_default_params(em_size, 1.0, measure_mode)
+ }
+ }
+ };
+
+ if dwrite_render_mode == dwrote::DWRITE_RENDERING_MODE_OUTLINE {
+ // Outline mode is not supported
+ return dwrote::DWRITE_RENDERING_MODE_CLEARTYPE_NATURAL_SYMMETRIC;
+ }
+
+ dwrite_render_mode
+}
+
+fn is_bitmap_font(font: &FontInstance) -> bool {
+ // If bitmaps are requested, then treat as a bitmap font to disable transforms.
+ // If mono AA is requested, let that take priority over using bitmaps.
+ font.render_mode != FontRenderMode::Mono &&
+ font.flags.contains(FontInstanceFlags::EMBEDDED_BITMAPS)
+}
+
+impl FontContext {
+ pub fn distribute_across_threads() -> bool {
+ true
+ }
+
+ pub fn new() -> FontContext {
+ FontContext {
+ fonts: FastHashMap::default(),
+ variations: FastHashMap::default(),
+ gamma_luts: FastHashMap::default(),
+ }
+ }
+
+ fn add_font_descriptor(&mut self, font_key: &FontKey, desc: &dwrote::FontDescriptor) {
+ let system_fc = dwrote::FontCollection::get_system(false);
+ if let Some(font) = system_fc.get_font_from_descriptor(desc) {
+ let face = font.create_font_face();
+ let file = face.get_files().pop().unwrap();
+ let index = face.get_index();
+ self.fonts.insert(*font_key, FontFace { cached: None, file, index, face });
+ }
+ }
+
+ pub fn add_raw_font(&mut self, font_key: &FontKey, data: Arc<Vec<u8>>, index: u32) {
+ if self.fonts.contains_key(font_key) {
+ return;
+ }
+
+ if let Some(file) = dwrote::FontFile::new_from_data(data) {
+ if let Ok(face) = file.create_face(index, dwrote::DWRITE_FONT_SIMULATIONS_NONE) {
+ self.fonts.insert(*font_key, FontFace { cached: None, file, index, face });
+ return;
+ }
+ }
+ // XXX add_raw_font needs to have a way to return an error
+ debug!("DWrite WR failed to load font from data, using Arial instead");
+ self.add_font_descriptor(font_key, &DEFAULT_FONT_DESCRIPTOR);
+ }
+
+ pub fn add_native_font(&mut self, font_key: &FontKey, font_handle: NativeFontHandle) {
+ if self.fonts.contains_key(font_key) {
+ return;
+ }
+
+ let index = font_handle.index;
+ let mut cache = FONT_CACHE.lock().unwrap();
+ // Check to see if the font is already in the cache. If so, reuse it.
+ if let Some(font) = cache.get(font_handle.path.as_path()) {
+ if let Ok(face) = font.file.create_face(index, dwrote::DWRITE_FONT_SIMULATIONS_NONE) {
+ self.fonts.insert(
+ *font_key,
+ FontFace { cached: Some(font.key.clone()), file: font.file.clone(), index, face },
+ );
+ return;
+ }
+ }
+ if let Some(file) = dwrote::FontFile::new_from_path(&font_handle.path) {
+ // The font is not in the cache yet, so try to create the font and insert it in the cache.
+ if let Ok(face) = file.create_face(index, dwrote::DWRITE_FONT_SIMULATIONS_NONE) {
+ let key: CachedFontKey = font_handle.path.into();
+ self.fonts.insert(
+ *font_key,
+ FontFace { cached: Some(key.clone()), file: file.clone(), index, face },
+ );
+ cache.insert(CachedFont { key, file });
+ return;
+ }
+ }
+
+ // XXX add_native_font needs to have a way to return an error
+ debug!("DWrite WR failed to load font from path, using Arial instead");
+ self.add_font_descriptor(font_key, &DEFAULT_FONT_DESCRIPTOR);
+ }
+
+ pub fn delete_font(&mut self, font_key: &FontKey) {
+ if let Some(face) = self.fonts.remove(font_key) {
+ self.variations.retain(|k, _| k.0 != *font_key);
+ // Check if this was a cached font.
+ if let Some(key) = face.cached {
+ let mut cache = FONT_CACHE.lock().unwrap();
+ // If there are only two references left, that means only this face and
+ // the cache are using the font. So remove it from the cache.
+ if Arc::strong_count(&key) == 2 {
+ cache.remove(&*key);
+ }
+ }
+ }
+ }
+
+ pub fn delete_font_instance(&mut self, instance: &FontInstance) {
+ // Ensure we don't keep around excessive amounts of stale variations.
+ if !instance.variations.is_empty() {
+ let sims = if instance.flags.contains(FontInstanceFlags::SYNTHETIC_BOLD) {
+ dwrote::DWRITE_FONT_SIMULATIONS_BOLD
+ } else {
+ dwrote::DWRITE_FONT_SIMULATIONS_NONE
+ };
+ self.variations.remove(&(instance.font_key, sims, instance.variations.clone()));
+ }
+ }
+
+ // Assumes RGB format from dwrite, which is 3 bytes per pixel as dwrite
+ // doesn't output an alpha value via GlyphRunAnalysis::CreateAlphaTexture
+ #[allow(dead_code)]
+ fn print_glyph_data(&self, data: &[u8], width: usize, height: usize) {
+ // Rust doesn't have step_by support on stable :(
+ for i in 0 .. height {
+ let current_height = i * width * 3;
+
+ for pixel in data[current_height .. current_height + (width * 3)].chunks(3) {
+ let r = pixel[0];
+ let g = pixel[1];
+ let b = pixel[2];
+ debug!("({}, {}, {}) ", r, g, b,);
+ }
+ }
+ }
+
+ fn get_font_face(
+ &mut self,
+ font: &FontInstance,
+ ) -> &dwrote::FontFace {
+ if !font.flags.contains(FontInstanceFlags::SYNTHETIC_BOLD) &&
+ font.variations.is_empty() {
+ return &self.fonts.get(&font.font_key).unwrap().face;
+ }
+ let sims = if font.flags.contains(FontInstanceFlags::SYNTHETIC_BOLD) {
+ dwrote::DWRITE_FONT_SIMULATIONS_BOLD
+ } else {
+ dwrote::DWRITE_FONT_SIMULATIONS_NONE
+ };
+ match self.variations.entry((font.font_key, sims, font.variations.clone())) {
+ Entry::Occupied(entry) => entry.into_mut(),
+ Entry::Vacant(entry) => {
+ let normal_face = self.fonts.get(&font.font_key).unwrap();
+ if !font.variations.is_empty() {
+ if let Some(var_face) = normal_face.face.create_font_face_with_variations(
+ sims,
+ &font.variations.iter().map(|var| {
+ dwrote::DWRITE_FONT_AXIS_VALUE {
+ // OpenType tags are big-endian, but DWrite wants little-endian.
+ axisTag: var.tag.swap_bytes(),
+ value: var.value,
+ }
+ }).collect::<Vec<_>>(),
+ ) {
+ return entry.insert(var_face);
+ }
+ }
+ let var_face = normal_face.file
+ .create_face(normal_face.index, sims)
+ .unwrap_or_else(|_| normal_face.face.clone());
+ entry.insert(var_face)
+ }
+ }
+ }
+
+ fn create_glyph_analysis(
+ &mut self,
+ font: &FontInstance,
+ key: &GlyphKey,
+ size: f32,
+ transform: Option<dwrote::DWRITE_MATRIX>,
+ bitmaps: bool,
+ ) -> Result<(dwrote::GlyphRunAnalysis, dwrote::DWRITE_TEXTURE_TYPE, dwrote::RECT), dwrote::HRESULT> {
+ let face = self.get_font_face(font);
+ let glyph = key.index() as u16;
+ let advance = 0.0f32;
+ let offset = dwrote::GlyphOffset {
+ advanceOffset: 0.0,
+ ascenderOffset: 0.0,
+ };
+
+ let glyph_run = dwrote::DWRITE_GLYPH_RUN {
+ fontFace: unsafe { face.as_ptr() },
+ fontEmSize: size, // size in DIPs (1/96", same as CSS pixels)
+ glyphCount: 1,
+ glyphIndices: &glyph,
+ glyphAdvances: &advance,
+ glyphOffsets: &offset,
+ isSideways: 0,
+ bidiLevel: 0,
+ };
+
+ let dwrite_measure_mode = dwrite_measure_mode(font, bitmaps);
+ let dwrite_render_mode = dwrite_render_mode(
+ face,
+ font,
+ size,
+ dwrite_measure_mode,
+ bitmaps,
+ );
+
+ let analysis = dwrote::GlyphRunAnalysis::create(
+ &glyph_run,
+ 1.0,
+ transform,
+ dwrite_render_mode,
+ dwrite_measure_mode,
+ 0.0,
+ 0.0,
+ )?;
+ let texture_type = dwrite_texture_type(font.render_mode);
+ let bounds = analysis.get_alpha_texture_bounds(texture_type)?;
+ // If the bounds are empty, then we might not be able to render the glyph with cleartype.
+ // Try again with aliased rendering to check if that works instead.
+ if font.render_mode != FontRenderMode::Mono &&
+ (bounds.left == bounds.right || bounds.top == bounds.bottom) {
+ let analysis2 = dwrote::GlyphRunAnalysis::create(
+ &glyph_run,
+ 1.0,
+ transform,
+ dwrote::DWRITE_RENDERING_MODE_ALIASED,
+ dwrite_measure_mode,
+ 0.0,
+ 0.0,
+ )?;
+ let bounds2 = analysis2.get_alpha_texture_bounds(dwrote::DWRITE_TEXTURE_ALIASED_1x1)?;
+ if bounds2.left != bounds2.right && bounds2.top != bounds2.bottom {
+ return Ok((analysis2, dwrote::DWRITE_TEXTURE_ALIASED_1x1, bounds2));
+ }
+ }
+ Ok((analysis, texture_type, bounds))
+ }
+
+ pub fn get_glyph_index(&mut self, font_key: FontKey, ch: char) -> Option<u32> {
+ let face = &self.fonts.get(&font_key).unwrap().face;
+ let indices = face.get_glyph_indices(&[ch as u32]);
+ indices.first().map(|idx| *idx as u32)
+ }
+
+ pub fn get_glyph_dimensions(
+ &mut self,
+ font: &FontInstance,
+ key: &GlyphKey,
+ ) -> Option<GlyphDimensions> {
+ let (size, x_scale, y_scale, bitmaps, transform) = Self::get_glyph_parameters(font, key);
+ let (_, _, bounds) = self.create_glyph_analysis(font, key, size, transform, bitmaps).ok()?;
+
+ let width = (bounds.right - bounds.left) as i32;
+ let height = (bounds.bottom - bounds.top) as i32;
+
+ // Alpha texture bounds can sometimes return an empty rect
+ // Such as for spaces
+ if width == 0 || height == 0 {
+ return None;
+ }
+
+ let (strike_scale, pixel_step) = if bitmaps {
+ (y_scale, 1.0)
+ } else {
+ (x_scale, y_scale / x_scale)
+ };
+ let extra_strikes = font.get_extra_strikes(FontInstanceFlags::MULTISTRIKE_BOLD, strike_scale);
+ let extra_width = extra_strikes as f64 * pixel_step;
+
+ let face = self.get_font_face(font);
+ face.get_design_glyph_metrics(&[key.index() as u16], false)
+ .first()
+ .map(|metrics| {
+ let em_size = size / 16.;
+ let design_units_per_pixel = face.metrics().metrics0().designUnitsPerEm as f32 / 16. as f32;
+ let scaled_design_units_to_pixels = em_size / design_units_per_pixel;
+ let advance = metrics.advanceWidth as f32 * scaled_design_units_to_pixels;
+
+ GlyphDimensions {
+ left: bounds.left,
+ top: -bounds.top,
+ width: width + extra_width.ceil() as i32,
+ height,
+ advance: advance + extra_width as f32,
+ }
+ })
+ }
+
+ // DWrite ClearType gives us values in RGB, but WR expects BGRA.
+ fn convert_to_bgra(
+ &self,
+ pixels: &[u8],
+ width: usize,
+ height: usize,
+ texture_type: dwrote::DWRITE_TEXTURE_TYPE,
+ render_mode: FontRenderMode,
+ bitmaps: bool,
+ subpixel_bgr: bool,
+ padding: usize,
+ ) -> (Vec<u8>, bool) {
+ let (buffer_width, buffer_height) = (width + padding * 2, height + padding * 2);
+ let buffer_length = buffer_width * buffer_height * 4;
+ let mut bgra_pixels: Vec<u8> = vec![0; buffer_length];
+
+ match (texture_type, render_mode, bitmaps) {
+ (dwrote::DWRITE_TEXTURE_ALIASED_1x1, _, _) => {
+ assert!(width * height == pixels.len());
+ let mut i = 0;
+ for row in padding .. height + padding {
+ let row_offset = row * buffer_width;
+ for col in padding .. width + padding {
+ let offset = (row_offset + col) * 4;
+ let alpha = pixels[i];
+ i += 1;
+ bgra_pixels[offset + 0] = alpha;
+ bgra_pixels[offset + 1] = alpha;
+ bgra_pixels[offset + 2] = alpha;
+ bgra_pixels[offset + 3] = alpha;
+ }
+ }
+ (bgra_pixels, false)
+ }
+ (_, FontRenderMode::Subpixel, false) => {
+ assert!(width * height * 3 == pixels.len());
+ let mut i = 0;
+ for row in padding .. height + padding {
+ let row_offset = row * buffer_width;
+ for col in padding .. width + padding {
+ let offset = (row_offset + col) * 4;
+ let (mut r, g, mut b) = (pixels[i + 0], pixels[i + 1], pixels[i + 2]);
+ if subpixel_bgr {
+ mem::swap(&mut r, &mut b);
+ }
+ i += 3;
+ bgra_pixels[offset + 0] = b;
+ bgra_pixels[offset + 1] = g;
+ bgra_pixels[offset + 2] = r;
+ bgra_pixels[offset + 3] = 0xff;
+ }
+ }
+ (bgra_pixels, true)
+ }
+ _ => {
+ assert!(width * height * 3 == pixels.len());
+ let mut i = 0;
+ for row in padding .. height + padding {
+ let row_offset = row * buffer_width;
+ for col in padding .. width + padding {
+ let offset = (row_offset + col) * 4;
+ // Only take the G channel, as its closest to D2D
+ let alpha = pixels[i + 1] as u8;
+ i += 3;
+ bgra_pixels[offset + 0] = alpha;
+ bgra_pixels[offset + 1] = alpha;
+ bgra_pixels[offset + 2] = alpha;
+ bgra_pixels[offset + 3] = alpha;
+ }
+ }
+ (bgra_pixels, false)
+ }
+ }
+ }
+
+ pub fn prepare_font(font: &mut FontInstance) {
+ match font.render_mode {
+ FontRenderMode::Mono => {
+ // In mono mode the color of the font is irrelevant.
+ font.color = ColorU::new(255, 255, 255, 255);
+ // Subpixel positioning is disabled in mono mode.
+ font.disable_subpixel_position();
+ }
+ FontRenderMode::Alpha => {
+ font.color = font.color.luminance_color().quantize();
+ }
+ FontRenderMode::Subpixel => {
+ font.color = font.color.quantize();
+ }
+ }
+ }
+
+ fn get_glyph_parameters(font: &FontInstance, key: &GlyphKey)
+ -> (f32, f64, f64, bool, Option<dwrote::DWRITE_MATRIX>) {
+ let (x_scale, y_scale) = font.transform.compute_scale().unwrap_or((1.0, 1.0));
+ let scaled_size = font.size.to_f64_px() * y_scale;
+ let bitmaps = is_bitmap_font(font);
+ let (mut shape, (mut x_offset, mut y_offset)) = if bitmaps {
+ (FontTransform::identity(), (0.0, 0.0))
+ } else {
+ (font.transform.invert_scale(y_scale, y_scale), font.get_subpx_offset(key))
+ };
+ if font.flags.contains(FontInstanceFlags::FLIP_X) {
+ shape = shape.flip_x();
+ }
+ if font.flags.contains(FontInstanceFlags::FLIP_Y) {
+ shape = shape.flip_y();
+ }
+ if font.flags.contains(FontInstanceFlags::TRANSPOSE) {
+ shape = shape.swap_xy();
+ }
+ let (mut tx, mut ty) = (0.0, 0.0);
+ if font.synthetic_italics.is_enabled() {
+ let (shape_, (tx_, ty_)) = font.synthesize_italics(shape, scaled_size);
+ shape = shape_;
+ tx = tx_;
+ ty = ty_;
+ };
+ x_offset += tx;
+ y_offset += ty;
+ let transform = if !shape.is_identity() || (x_offset, y_offset) != (0.0, 0.0) {
+ Some(dwrote::DWRITE_MATRIX {
+ m11: shape.scale_x,
+ m12: shape.skew_y,
+ m21: shape.skew_x,
+ m22: shape.scale_y,
+ dx: x_offset as f32,
+ dy: y_offset as f32,
+ })
+ } else {
+ None
+ };
+ (scaled_size as f32, x_scale, y_scale, bitmaps, transform)
+ }
+
+ pub fn begin_rasterize(_font: &FontInstance) {
+ }
+
+ pub fn end_rasterize(_font: &FontInstance) {
+ }
+
+ pub fn rasterize_glyph(&mut self, font: &FontInstance, key: &GlyphKey) -> GlyphRasterResult {
+ let (size, x_scale, y_scale, bitmaps, transform) = Self::get_glyph_parameters(font, key);
+ let (analysis, texture_type, bounds) = self.create_glyph_analysis(font, key, size, transform, bitmaps)
+ .or(Err(GlyphRasterError::LoadFailed))?;
+ let mut width = (bounds.right - bounds.left) as i32;
+ let height = (bounds.bottom - bounds.top) as i32;
+ // Alpha texture bounds can sometimes return an empty rect
+ // Such as for spaces
+ if width == 0 || height == 0 {
+ return Err(GlyphRasterError::LoadFailed);
+ }
+
+ let pixels = analysis.create_alpha_texture(texture_type, bounds).or(Err(GlyphRasterError::LoadFailed))?;
+ let padding = if font.use_texture_padding() { 1 } else { 0 };
+ let (mut bgra_pixels, is_subpixel) = self.convert_to_bgra(
+ &pixels,
+ width as usize,
+ height as usize,
+ texture_type,
+ font.render_mode,
+ bitmaps,
+ font.flags.contains(FontInstanceFlags::SUBPIXEL_BGR),
+ padding as usize,
+ );
+
+ // Apply multistrike bold, if necessary, and replace the current pixels with it.
+ let (strike_scale, pixel_step) = if bitmaps {
+ (y_scale, 1.0)
+ } else {
+ (x_scale, y_scale / x_scale)
+ };
+ let extra_strikes = font.get_extra_strikes(FontInstanceFlags::MULTISTRIKE_BOLD, strike_scale);
+ if extra_strikes > 0 {
+ let (bold_pixels, bold_width) = apply_multistrike_bold(
+ &bgra_pixels,
+ (width + padding * 2) as usize,
+ (height + padding * 2) as usize,
+ is_subpixel,
+ extra_strikes,
+ pixel_step,
+ );
+ width = bold_width as i32 - padding * 2;
+ bgra_pixels = bold_pixels;
+ }
+
+ let FontInstancePlatformOptions { gamma, contrast, cleartype_level, .. } =
+ font.platform_options.unwrap_or_default();
+ let gamma_lut = self.gamma_luts
+ .entry((gamma, contrast))
+ .or_insert_with(||
+ GammaLut::new(
+ contrast as f32 / 100.0,
+ gamma as f32 / 100.0,
+ gamma as f32 / 100.0,
+ ));
+ if is_subpixel {
+ gamma_lut.preblend_scaled(&mut bgra_pixels, font.color, cleartype_level);
+ } else {
+ gamma_lut.preblend(&mut bgra_pixels, font.color);
+ }
+
+ let format = if bitmaps {
+ GlyphFormat::Bitmap
+ } else if texture_type == dwrote::DWRITE_TEXTURE_ALIASED_1x1 {
+ font.get_alpha_glyph_format()
+ } else {
+ font.get_glyph_format()
+ };
+
+ Ok(RasterizedGlyph {
+ left: (bounds.left - padding) as f32,
+ top: (-bounds.top + padding) as f32,
+ width: width + padding * 2,
+ height: height + padding * 2,
+ scale: (if bitmaps { y_scale.recip() } else { 1.0 }) as f32,
+ format,
+ bytes: bgra_pixels,
+ })
+ }
+}
diff --git a/gfx/wr/wr_glyph_rasterizer/src/profiler.rs b/gfx/wr/wr_glyph_rasterizer/src/profiler.rs
new file mode 100644
index 0000000000..89d126e3fc
--- /dev/null
+++ b/gfx/wr/wr_glyph_rasterizer/src/profiler.rs
@@ -0,0 +1,12 @@
+/* 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 profiler for profiling rasterize time.
+pub trait GlyphRasterizeProfiler {
+ fn start_time(&mut self);
+
+ fn end_time(&mut self) -> f64;
+
+ fn set(&mut self, value: f64);
+}
diff --git a/gfx/wr/wr_glyph_rasterizer/src/rasterizer.rs b/gfx/wr/wr_glyph_rasterizer/src/rasterizer.rs
new file mode 100644
index 0000000000..5153e60c54
--- /dev/null
+++ b/gfx/wr/wr_glyph_rasterizer/src/rasterizer.rs
@@ -0,0 +1,1879 @@
+/* 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 api::{FontInstanceData, FontInstanceFlags, FontInstanceKey};
+use api::{FontInstanceOptions, FontInstancePlatformOptions};
+use api::{FontKey, FontRenderMode, FontSize, FontTemplate, FontVariation};
+use api::{ColorU, GlyphIndex, GlyphDimensions, SyntheticItalics};
+use api::{IdNamespace, BlobImageResources};
+use api::channel::crossbeam::{unbounded, Receiver, Sender};
+use api::units::*;
+use api::ImageFormat;
+use crate::platform::font::FontContext;
+use crate::profiler::GlyphRasterizeProfiler;
+use crate::types::{FastHashMap, FastHashSet};
+use crate::telemetry::Telemetry;
+use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
+use rayon::ThreadPool;
+use rayon::prelude::*;
+use euclid::approxeq::ApproxEq;
+use smallvec::SmallVec;
+use std::cmp;
+use std::cell::Cell;
+use std::hash::{Hash, Hasher};
+use std::mem;
+use std::ops::Deref;
+use std::sync::{Arc, Condvar, Mutex, MutexGuard, Weak};
+use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
+use std::sync::atomic::{AtomicBool, Ordering};
+
+pub static GLYPH_FLASHING: AtomicBool = AtomicBool::new(false);
+
+impl FontContexts {
+ /// Get access to the font context associated to the current thread.
+ pub fn lock_current_context(&self) -> MutexGuard<FontContext> {
+ match self.current_worker_id() {
+ Some(id) => self.lock_context(id),
+ None => self.lock_any_context(),
+ }
+ }
+
+ pub(in super) fn current_worker_id(&self) -> Option<usize> {
+ self.workers.current_thread_index()
+ }
+}
+
+thread_local! {
+ pub static SEED: Cell<u32> = Cell::new(0);
+}
+
+// super simple random to avoid dependency on rand
+fn random() -> u32 {
+ SEED.with(|seed| {
+ seed.set(seed.get().wrapping_mul(22695477).wrapping_add(1));
+ seed.get()
+ })
+}
+
+impl GlyphRasterizer {
+ pub fn request_glyphs<F>(
+ &mut self,
+ font: FontInstance,
+ glyph_keys: &[GlyphKey],
+ mut handle: F,
+ )
+ where F: FnMut(&GlyphKey) -> bool
+ {
+ assert!(self.has_font(font.font_key));
+
+ let mut batch_size = 0;
+
+ // select glyphs that have not been requested yet.
+ for key in glyph_keys {
+ if !handle(key) {
+ continue;
+ }
+
+ // Increment the total number of glyphs that are pending. This is used to determine
+ // later whether to use worker threads for the remaining glyphs during resolve time.
+ self.pending_glyph_count += 1;
+ self.glyph_request_count += 1;
+
+ // Find a batch container for the font instance for this glyph. Use get_mut to avoid
+ // cloning the font instance, since this is the common path.
+ match self.pending_glyph_requests.get_mut(&font) {
+ Some(container) => {
+ container.push(*key);
+ batch_size = container.len();
+ }
+ None => {
+ // If no batch exists for this font instance, add the glyph to a new one.
+ self.pending_glyph_requests.insert(
+ font.clone(),
+ smallvec![*key],
+ );
+ }
+ }
+ }
+
+ // If the batch for this font instance is big enough, kick off an async
+ // job to start rasterizing these glyphs on other threads now.
+ if batch_size >= 8 {
+ let container = self.pending_glyph_requests.get_mut(&font).unwrap();
+ let glyphs = mem::replace(container, SmallVec::new());
+ self.flush_glyph_requests(font, glyphs, true);
+ }
+ }
+
+ pub fn enable_multithreading(&mut self, enable: bool) {
+ self.enable_multithreading = enable;
+ }
+
+ /// Internal method to flush a list of glyph requests to a set of worker threads,
+ /// or process on this thread if there isn't much work to do (in which case the
+ /// overhead of processing these on a thread is unlikely to be a performance win).
+ fn flush_glyph_requests(
+ &mut self,
+ font: FontInstance,
+ glyphs: SmallVec<[GlyphKey; 16]>,
+ use_workers: bool,
+ ) {
+ let font = Arc::new(font);
+ let font_contexts = Arc::clone(&self.font_contexts);
+ self.pending_glyph_jobs += glyphs.len();
+ self.pending_glyph_count -= glyphs.len();
+
+ let can_use_r8_format = self.can_use_r8_format;
+
+ let job_font = font.clone();
+ let process_glyph = move |key: &GlyphKey| -> GlyphRasterJob {
+ profile_scope!("glyph-raster");
+ let mut context = font_contexts.lock_current_context();
+ let mut job = GlyphRasterJob {
+ font: Arc::clone(&job_font),
+ key: key.clone(),
+ result: context.rasterize_glyph(&job_font, key),
+ };
+
+ if let Ok(ref mut glyph) = job.result {
+ // Sanity check.
+ let bpp = 4; // We always render glyphs in 32 bits RGBA format.
+ assert_eq!(
+ glyph.bytes.len(),
+ bpp * (glyph.width * glyph.height) as usize
+ );
+
+ // a quick-and-dirty monochrome over
+ fn over(dst: u8, src: u8) -> u8 {
+ let a = src as u32;
+ let a = 256 - a;
+ let dst = ((dst as u32 * a) >> 8) as u8;
+ src + dst
+ }
+
+ if GLYPH_FLASHING.load(Ordering::Relaxed) {
+ let color = (random() & 0xff) as u8;
+ for i in &mut glyph.bytes {
+ *i = over(*i, color);
+ }
+ }
+
+ assert_eq!((glyph.left.fract(), glyph.top.fract()), (0.0, 0.0));
+
+ // Check if the glyph has a bitmap that needs to be downscaled.
+ glyph.downscale_bitmap_if_required(&job_font);
+
+ // Convert from BGRA8 to R8 if required. In the future we can make it the
+ // backends' responsibility to output glyphs in the desired format,
+ // potentially reducing the number of copies.
+ if glyph.format.image_format(can_use_r8_format).bytes_per_pixel() == 1 {
+ glyph.bytes = glyph.bytes
+ .chunks_mut(4)
+ .map(|pixel| pixel[3])
+ .collect::<Vec<_>>();
+ }
+ }
+
+ job
+ };
+
+ // if the number of glyphs is small, do it inline to avoid the threading overhead;
+ // send the result into glyph_tx so downstream code can't tell the difference.
+ if self.enable_multithreading && use_workers {
+ // spawn an async task to get off of the render backend thread as early as
+ // possible and in that task use rayon's fork join dispatch to rasterize the
+ // glyphs in the thread pool.
+ profile_scope!("spawning process_glyph jobs");
+ self.workers.install(|| {
+ FontContext::begin_rasterize(&font);
+ // If the FontContext supports distributing a font across multiple threads,
+ // then use par_iter so different glyphs of the same font are processed on
+ // multiple threads.
+ if FontContext::distribute_across_threads() {
+ glyphs.par_iter().for_each(|key| {
+ let job = process_glyph(key);
+ self.glyph_tx.send(job).unwrap();
+ });
+ } else {
+ // For FontContexts that prefer to localize a font to a single thread,
+ // just process all the glyphs on the same worker to avoid contention.
+ for key in glyphs {
+ let job = process_glyph(&key);
+ self.glyph_tx.send(job).unwrap();
+ }
+ }
+ FontContext::end_rasterize(&font);
+ });
+ } else {
+ FontContext::begin_rasterize(&font);
+ for key in glyphs {
+ let job = process_glyph(&key);
+ self.glyph_tx.send(job).unwrap();
+ }
+ FontContext::end_rasterize(&font);
+ }
+ }
+
+ pub fn resolve_glyphs<F, G>(
+ &mut self,
+ mut handle: F,
+ profile: &mut G,
+ )
+ where
+ F: FnMut(GlyphRasterJob, bool),
+ G: GlyphRasterizeProfiler,
+ {
+ profile.start_time();
+ let timer_id = Telemetry::start_rasterize_glyphs_time();
+
+ // Work around the borrow checker, since we call flush_glyph_requests below
+ let mut pending_glyph_requests = mem::replace(
+ &mut self.pending_glyph_requests,
+ FastHashMap::default(),
+ );
+ // If we have a large amount of remaining work to do, spawn to worker threads,
+ // even if that work is shared among a number of different font instances.
+ let use_workers = self.pending_glyph_count >= 8;
+ for (font, pending_glyphs) in pending_glyph_requests.drain() {
+ self.flush_glyph_requests(
+ font,
+ pending_glyphs,
+ use_workers,
+ );
+ }
+ // Restore this so that we don't heap allocate next frame
+ self.pending_glyph_requests = pending_glyph_requests;
+ debug_assert_eq!(self.pending_glyph_count, 0);
+ debug_assert!(self.pending_glyph_requests.is_empty());
+
+ if self.glyph_request_count > 0 {
+ profile.set(self.glyph_request_count as f64);
+ self.glyph_request_count = 0;
+ }
+
+ profile_scope!("resolve_glyphs");
+ // TODO: rather than blocking until all pending glyphs are available
+ // we could try_recv and steal work from the thread pool to take advantage
+ // of the fact that this thread is alive and we avoid the added latency
+ // of blocking it.
+ let mut jobs = {
+ profile_scope!("blocking wait on glyph_rx");
+ self.glyph_rx.iter().take(self.pending_glyph_jobs).collect::<Vec<_>>()
+ };
+ assert_eq!(jobs.len(), self.pending_glyph_jobs, "BUG: Didn't receive all pending glyphs!");
+ self.pending_glyph_jobs = 0;
+
+ // Ensure that the glyphs are always processed in the same
+ // order for a given text run (since iterating a hash set doesn't
+ // guarantee order). This can show up as very small float inaccuracy
+ // differences in rasterizers due to the different coordinates
+ // that text runs get associated with by the texture cache allocator.
+ jobs.sort_by(|a, b| (*a.font).cmp(&*b.font).then(a.key.cmp(&b.key)));
+
+ for job in jobs {
+ handle(job, self.can_use_r8_format);
+ }
+
+ // Now that we are done with the critical path (rendering the glyphs),
+ // we can schedule removing the fonts if needed.
+ self.remove_dead_fonts();
+
+ Telemetry::stop_and_accumulate_rasterize_glyphs_time(timer_id);
+ profile.end_time();
+ }
+}
+
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct FontTransform {
+ pub scale_x: f32,
+ pub skew_x: f32,
+ pub skew_y: f32,
+ pub scale_y: f32,
+}
+
+// Floats don't impl Hash/Eq/Ord...
+impl Eq for FontTransform {}
+impl Ord for FontTransform {
+ fn cmp(&self, other: &Self) -> cmp::Ordering {
+ self.partial_cmp(other).unwrap_or(cmp::Ordering::Equal)
+ }
+}
+impl Hash for FontTransform {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ // Note: this is inconsistent with the Eq impl for -0.0 (don't care).
+ self.scale_x.to_bits().hash(state);
+ self.skew_x.to_bits().hash(state);
+ self.skew_y.to_bits().hash(state);
+ self.scale_y.to_bits().hash(state);
+ }
+}
+
+impl FontTransform {
+ const QUANTIZE_SCALE: f32 = 1024.0;
+
+ pub fn new(scale_x: f32, skew_x: f32, skew_y: f32, scale_y: f32) -> Self {
+ FontTransform { scale_x, skew_x, skew_y, scale_y }
+ }
+
+ pub fn identity() -> Self {
+ FontTransform::new(1.0, 0.0, 0.0, 1.0)
+ }
+
+ #[allow(dead_code)]
+ pub fn is_identity(&self) -> bool {
+ *self == FontTransform::identity()
+ }
+
+ pub fn quantize(&self) -> Self {
+ FontTransform::new(
+ (self.scale_x * Self::QUANTIZE_SCALE).round() / Self::QUANTIZE_SCALE,
+ (self.skew_x * Self::QUANTIZE_SCALE).round() / Self::QUANTIZE_SCALE,
+ (self.skew_y * Self::QUANTIZE_SCALE).round() / Self::QUANTIZE_SCALE,
+ (self.scale_y * Self::QUANTIZE_SCALE).round() / Self::QUANTIZE_SCALE,
+ )
+ }
+
+ #[allow(dead_code)]
+ pub fn determinant(&self) -> f64 {
+ self.scale_x as f64 * self.scale_y as f64 - self.skew_y as f64 * self.skew_x as f64
+ }
+
+ #[allow(dead_code)]
+ pub fn compute_scale(&self) -> Option<(f64, f64)> {
+ let det = self.determinant();
+ if det != 0.0 {
+ let x_scale = (self.scale_x as f64).hypot(self.skew_y as f64);
+ let y_scale = det.abs() / x_scale;
+ Some((x_scale, y_scale))
+ } else {
+ None
+ }
+ }
+
+ #[allow(dead_code)]
+ pub fn pre_scale(&self, scale_x: f32, scale_y: f32) -> Self {
+ FontTransform::new(
+ self.scale_x * scale_x,
+ self.skew_x * scale_y,
+ self.skew_y * scale_x,
+ self.scale_y * scale_y,
+ )
+ }
+
+ #[allow(dead_code)]
+ pub fn scale(&self, scale: f32) -> Self { self.pre_scale(scale, scale) }
+
+ #[allow(dead_code)]
+ pub fn invert_scale(&self, x_scale: f64, y_scale: f64) -> Self {
+ self.pre_scale(x_scale.recip() as f32, y_scale.recip() as f32)
+ }
+
+ pub fn synthesize_italics(&self, angle: SyntheticItalics, size: f64, vertical: bool) -> (Self, (f64, f64)) {
+ let skew_factor = angle.to_skew();
+ if vertical {
+ // origin delta to be applied so that we effectively skew around
+ // the middle rather than edge of the glyph
+ let (tx, ty) = (0.0, -size * 0.5 * skew_factor as f64);
+ (FontTransform::new(
+ self.scale_x + self.skew_x * skew_factor,
+ self.skew_x,
+ self.skew_y + self.scale_y * skew_factor,
+ self.scale_y,
+ ), (self.scale_x as f64 * tx + self.skew_x as f64 * ty,
+ self.skew_y as f64 * tx + self.scale_y as f64 * ty))
+ } else {
+ (FontTransform::new(
+ self.scale_x,
+ self.skew_x - self.scale_x * skew_factor,
+ self.skew_y,
+ self.scale_y - self.skew_y * skew_factor,
+ ), (0.0, 0.0))
+ }
+ }
+
+ pub fn swap_xy(&self) -> Self {
+ FontTransform::new(self.skew_x, self.scale_x, self.scale_y, self.skew_y)
+ }
+
+ pub fn flip_x(&self) -> Self {
+ FontTransform::new(-self.scale_x, self.skew_x, -self.skew_y, self.scale_y)
+ }
+
+ pub fn flip_y(&self) -> Self {
+ FontTransform::new(self.scale_x, -self.skew_x, self.skew_y, -self.scale_y)
+ }
+
+ pub fn transform(&self, point: &LayoutPoint) -> DevicePoint {
+ DevicePoint::new(
+ self.scale_x * point.x + self.skew_x * point.y,
+ self.skew_y * point.x + self.scale_y * point.y,
+ )
+ }
+
+ pub fn get_subpx_dir(&self) -> SubpixelDirection {
+ if self.skew_y.approx_eq(&0.0) {
+ // The X axis is not projected onto the Y axis
+ SubpixelDirection::Horizontal
+ } else if self.scale_x.approx_eq(&0.0) {
+ // The X axis has been swapped with the Y axis
+ SubpixelDirection::Vertical
+ } else {
+ // Use subpixel precision on all axes
+ SubpixelDirection::Mixed
+ }
+ }
+}
+
+impl<'a> From<&'a LayoutToWorldTransform> for FontTransform {
+ fn from(xform: &'a LayoutToWorldTransform) -> Self {
+ FontTransform::new(xform.m11, xform.m21, xform.m12, xform.m22)
+ }
+}
+
+// Some platforms (i.e. Windows) may have trouble rasterizing glyphs above this size.
+// Ensure glyph sizes are reasonably limited to avoid that scenario.
+pub const FONT_SIZE_LIMIT: f32 = 320.0;
+
+/// Immutable description of a font instance's shared state.
+///
+/// `BaseFontInstance` can be identified by a `FontInstanceKey` to avoid hashing it.
+#[derive(Clone, Debug, Ord, PartialOrd, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BaseFontInstance {
+ ///
+ pub instance_key: FontInstanceKey,
+ ///
+ pub font_key: FontKey,
+ ///
+ pub size: FontSize,
+ ///
+ pub options: FontInstanceOptions,
+ ///
+ #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))]
+ pub platform_options: Option<FontInstancePlatformOptions>,
+ ///
+ pub variations: Vec<FontVariation>,
+}
+
+impl BaseFontInstance {
+ pub fn new(
+ instance_key: FontInstanceKey,
+ font_key: FontKey,
+ size: f32,
+ options: Option<FontInstanceOptions>,
+ platform_options: Option<FontInstancePlatformOptions>,
+ variations: Vec<FontVariation>,
+ ) -> Self {
+ BaseFontInstance {
+ instance_key,
+ font_key,
+ size: size.into(),
+ options: options.unwrap_or_default(),
+ platform_options,
+ variations,
+ }
+ }
+}
+
+impl Deref for BaseFontInstance {
+ type Target = FontInstanceOptions;
+ fn deref(&self) -> &FontInstanceOptions {
+ &self.options
+ }
+}
+
+impl Hash for BaseFontInstance {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ // Skip the instance key.
+ self.font_key.hash(state);
+ self.size.hash(state);
+ self.options.hash(state);
+ self.platform_options.hash(state);
+ self.variations.hash(state);
+ }
+}
+
+impl PartialEq for BaseFontInstance {
+ fn eq(&self, other: &BaseFontInstance) -> bool {
+ // Skip the instance key.
+ self.font_key == other.font_key &&
+ self.size == other.size &&
+ self.options == other.options &&
+ self.platform_options == other.platform_options &&
+ self.variations == other.variations
+ }
+}
+impl Eq for BaseFontInstance {}
+
+struct MappedFontKey {
+ font_key: FontKey,
+ template: FontTemplate,
+}
+
+struct FontKeyMapLocked {
+ namespace: IdNamespace,
+ next_id: u32,
+ template_map: FastHashMap<FontTemplate, Arc<MappedFontKey>>,
+ key_map: FastHashMap<FontKey, Arc<MappedFontKey>>,
+}
+
+/// A shared map from fonts key local to a namespace to shared font keys that
+/// can be shared across many namespaces. Local keys are tracked in a hashmap
+/// that stores a strong reference per mapping so that their count can be
+/// tracked. A map of font templates is used to hash font templates to their
+/// final shared key. The shared key will stay alive so long as there are
+/// any strong references to the mapping entry. Care must be taken when
+/// clearing namespaces of shared keys as this may trigger shared font keys
+/// to expire which require individual processing. Shared font keys will be
+/// created within the provided unique namespace.
+#[derive(Clone)]
+pub struct FontKeyMap(Arc<RwLock<FontKeyMapLocked>>);
+
+impl FontKeyMap {
+ pub fn new(namespace: IdNamespace) -> Self {
+ FontKeyMap(Arc::new(RwLock::new(FontKeyMapLocked {
+ namespace,
+ next_id: 1,
+ template_map: FastHashMap::default(),
+ key_map: FastHashMap::default(),
+ })))
+ }
+
+ fn lock(&self) -> RwLockReadGuard<FontKeyMapLocked> {
+ self.0.read().unwrap()
+ }
+
+ fn lock_mut(&mut self) -> RwLockWriteGuard<FontKeyMapLocked> {
+ self.0.write().unwrap()
+ }
+
+ pub fn keys(&self) -> Vec<FontKey> {
+ self.lock().key_map.keys().cloned().collect()
+ }
+
+ pub fn map_key(&self, font_key: &FontKey) -> FontKey {
+ match self.lock().key_map.get(font_key) {
+ Some(mapped) => mapped.font_key,
+ None => *font_key,
+ }
+ }
+
+ pub fn add_key(&mut self, font_key: &FontKey, template: &FontTemplate) -> Option<FontKey> {
+ let mut locked = self.lock_mut();
+ if locked.key_map.contains_key(font_key) {
+ return None;
+ }
+ if let Some(mapped) = locked.template_map.get(template).cloned() {
+ locked.key_map.insert(*font_key, mapped);
+ return None;
+ }
+ let shared_key = FontKey::new(locked.namespace, locked.next_id);
+ locked.next_id += 1;
+ let mapped = Arc::new(MappedFontKey {
+ font_key: shared_key,
+ template: template.clone(),
+ });
+ locked.template_map.insert(template.clone(), mapped.clone());
+ locked.key_map.insert(*font_key, mapped);
+ Some(shared_key)
+ }
+
+ pub fn delete_key(&mut self, font_key: &FontKey) -> Option<FontKey> {
+ let mut locked = self.lock_mut();
+ let mapped = match locked.key_map.remove(font_key) {
+ Some(mapped) => mapped,
+ None => return Some(*font_key),
+ };
+ if Arc::strong_count(&mapped) <= 2 {
+ // Only the last mapped key and template map point to it.
+ locked.template_map.remove(&mapped.template);
+ Some(mapped.font_key)
+ } else {
+ None
+ }
+ }
+
+ pub fn clear_namespace(&mut self, namespace: IdNamespace) -> Vec<FontKey> {
+ let mut locked = self.lock_mut();
+ locked.key_map.retain(|key, _| {
+ if key.0 == namespace {
+ false
+ } else {
+ true
+ }
+ });
+ let mut deleted_keys = Vec::new();
+ locked.template_map.retain(|_, mapped| {
+ if Arc::strong_count(mapped) <= 1 {
+ // Only the template map points to it.
+ deleted_keys.push(mapped.font_key);
+ false
+ } else {
+ true
+ }
+ });
+ deleted_keys
+ }
+}
+
+type FontTemplateMapLocked = FastHashMap<FontKey, FontTemplate>;
+
+/// A map of font keys to font templates that might hold both namespace-local
+/// font templates as well as shared templates.
+#[derive(Clone)]
+pub struct FontTemplateMap(Arc<RwLock<FontTemplateMapLocked>>);
+
+impl FontTemplateMap {
+ pub fn new() -> Self {
+ FontTemplateMap(Arc::new(RwLock::new(FastHashMap::default())))
+ }
+
+ pub fn lock(&self) -> RwLockReadGuard<FontTemplateMapLocked> {
+ self.0.read().unwrap()
+ }
+
+ fn lock_mut(&mut self) -> RwLockWriteGuard<FontTemplateMapLocked> {
+ self.0.write().unwrap()
+ }
+
+ pub fn clear(&mut self) {
+ self.lock_mut().clear();
+ }
+
+ pub fn len(&self) -> usize {
+ self.lock().len()
+ }
+
+ pub fn has_font(&self, key: &FontKey) -> bool {
+ self.lock().contains_key(key)
+ }
+
+ pub fn get_font(&self, key: &FontKey) -> Option<FontTemplate> {
+ self.lock().get(key).cloned()
+ }
+
+ pub fn add_font(&mut self, key: FontKey, template: FontTemplate) -> bool {
+ self.lock_mut().insert(key, template).is_none()
+ }
+
+ pub fn delete_font(&mut self, key: &FontKey) -> Option<FontTemplate> {
+ self.lock_mut().remove(key)
+ }
+
+ pub fn delete_fonts(&mut self, keys: &[FontKey]) {
+ if !keys.is_empty() {
+ let mut map = self.lock_mut();
+ for key in keys {
+ map.remove(key);
+ }
+ }
+ }
+
+ pub fn clear_namespace(&mut self, namespace: IdNamespace) -> Vec<FontKey> {
+ let mut deleted_keys = Vec::new();
+ self.lock_mut().retain(|key, _| {
+ if key.0 == namespace {
+ deleted_keys.push(*key);
+ false
+ } else {
+ true
+ }
+ });
+ deleted_keys
+ }
+}
+
+struct FontInstanceKeyMapLocked {
+ namespace: IdNamespace,
+ next_id: u32,
+ instances: FastHashSet<Arc<BaseFontInstance>>,
+ key_map: FastHashMap<FontInstanceKey, Weak<BaseFontInstance>>,
+}
+
+/// A map of namespace-local font instance keys to shared keys. Weak references
+/// are used to track the liveness of each key mapping as other consumers of
+/// BaseFontInstance might hold strong references to the entry. A mapping from
+/// BaseFontInstance to the shared key is then used to determine which shared
+/// key to assign to that instance. When the weak count of the mapping is zero,
+/// the entry is allowed to expire. Again, care must be taken when clearing
+/// a namespace within the key map as it may cause shared key expirations that
+/// require individual processing. Shared instance keys will be created within
+/// the provided unique namespace.
+#[derive(Clone)]
+pub struct FontInstanceKeyMap(Arc<RwLock<FontInstanceKeyMapLocked>>);
+
+impl FontInstanceKeyMap {
+ pub fn new(namespace: IdNamespace) -> Self {
+ FontInstanceKeyMap(Arc::new(RwLock::new(FontInstanceKeyMapLocked {
+ namespace,
+ next_id: 1,
+ instances: FastHashSet::default(),
+ key_map: FastHashMap::default(),
+ })))
+ }
+
+ fn lock(&self) -> RwLockReadGuard<FontInstanceKeyMapLocked> {
+ self.0.read().unwrap()
+ }
+
+ fn lock_mut(&mut self) -> RwLockWriteGuard<FontInstanceKeyMapLocked> {
+ self.0.write().unwrap()
+ }
+
+ pub fn keys(&self) -> Vec<FontInstanceKey> {
+ self.lock().key_map.keys().cloned().collect()
+ }
+
+ pub fn map_key(&self, key: &FontInstanceKey) -> FontInstanceKey {
+ match self.lock().key_map.get(key).and_then(|weak| weak.upgrade()) {
+ Some(mapped) => mapped.instance_key,
+ None => *key,
+ }
+ }
+
+ pub fn add_key(&mut self, mut instance: BaseFontInstance) -> Option<Arc<BaseFontInstance>> {
+ let mut locked = self.lock_mut();
+ if locked.key_map.contains_key(&instance.instance_key) {
+ return None;
+ }
+ if let Some(weak) = locked.instances.get(&instance).map(|mapped| Arc::downgrade(mapped)) {
+ locked.key_map.insert(instance.instance_key, weak);
+ return None;
+ }
+ let unmapped_key = instance.instance_key;
+ instance.instance_key = FontInstanceKey::new(locked.namespace, locked.next_id);
+ locked.next_id += 1;
+ let shared_instance = Arc::new(instance);
+ locked.instances.insert(shared_instance.clone());
+ locked.key_map.insert(unmapped_key, Arc::downgrade(&shared_instance));
+ Some(shared_instance)
+ }
+
+ pub fn delete_key(&mut self, key: &FontInstanceKey) -> Option<FontInstanceKey> {
+ let mut locked = self.lock_mut();
+ let mapped = match locked.key_map.remove(key).and_then(|weak| weak.upgrade()) {
+ Some(mapped) => mapped,
+ None => return Some(*key),
+ };
+ if Arc::weak_count(&mapped) == 0 {
+ // Only the instance set points to it.
+ locked.instances.remove(&mapped);
+ Some(mapped.instance_key)
+ } else {
+ None
+ }
+ }
+
+ pub fn clear_namespace(&mut self, namespace: IdNamespace) -> Vec<FontInstanceKey> {
+ let mut locked = self.lock_mut();
+ locked.key_map.retain(|key, _| {
+ if key.0 == namespace {
+ false
+ } else {
+ true
+ }
+ });
+ let mut deleted_keys = Vec::new();
+ locked.instances.retain(|mapped| {
+ if Arc::weak_count(mapped) == 0 {
+ // Only the instance set points to it.
+ deleted_keys.push(mapped.instance_key);
+ false
+ } else {
+ true
+ }
+ });
+ deleted_keys
+ }
+}
+
+type FontInstanceMapLocked = FastHashMap<FontInstanceKey, Arc<BaseFontInstance>>;
+
+/// A map of font instance data accessed concurrently from multiple threads.
+#[derive(Clone)]
+pub struct FontInstanceMap(Arc<RwLock<FontInstanceMapLocked>>);
+
+impl FontInstanceMap {
+ /// Creates an empty shared map.
+ pub fn new() -> Self {
+ FontInstanceMap(Arc::new(RwLock::new(FastHashMap::default())))
+ }
+
+ /// Acquires a read lock on the shared map.
+ pub fn lock(&self) -> RwLockReadGuard<FontInstanceMapLocked> {
+ self.0.read().unwrap()
+ }
+
+ /// Acquires a read lock on the shared map.
+ fn lock_mut(&mut self) -> RwLockWriteGuard<FontInstanceMapLocked> {
+ self.0.write().unwrap()
+ }
+
+ ///
+ pub fn clear(&mut self) {
+ self.lock_mut().clear();
+ }
+
+ ///
+ pub fn get_font_instance_data(&self, key: FontInstanceKey) -> Option<FontInstanceData> {
+ match self.lock().get(&key) {
+ Some(instance) => Some(FontInstanceData {
+ font_key: instance.font_key,
+ size: instance.size.into(),
+ options: Some(FontInstanceOptions {
+ render_mode: instance.render_mode,
+ flags: instance.flags,
+ bg_color: instance.bg_color,
+ synthetic_italics: instance.synthetic_italics,
+ }),
+ platform_options: instance.platform_options,
+ variations: instance.variations.clone(),
+ }),
+ None => None,
+ }
+ }
+
+ ///
+ pub fn get_font_instance(&self, instance_key: FontInstanceKey) -> Option<Arc<BaseFontInstance>> {
+ let instance_map = self.lock();
+ instance_map.get(&instance_key).cloned()
+ }
+
+ ///
+ pub fn add_font_instance(&mut self, instance: Arc<BaseFontInstance>) {
+ self.lock_mut().insert(instance.instance_key, instance);
+ }
+
+ ///
+ pub fn delete_font_instance(&mut self, instance_key: FontInstanceKey) {
+ self.lock_mut().remove(&instance_key);
+ }
+
+ ///
+ pub fn delete_font_instances(&mut self, keys: &[FontInstanceKey]) {
+ if !keys.is_empty() {
+ let mut map = self.lock_mut();
+ for key in keys {
+ map.remove(key);
+ }
+ }
+ }
+
+ ///
+ pub fn clear_namespace(&mut self, namespace: IdNamespace) {
+ self.lock_mut().retain(|key, _| key.0 != namespace);
+ }
+}
+
+/// Shared font resources that may need to be passed between multiple threads
+/// such as font templates and font instances. They are individually protected
+/// by locks to ensure safety.
+#[derive(Clone)]
+pub struct SharedFontResources {
+ pub templates: FontTemplateMap,
+ pub instances: FontInstanceMap,
+ pub font_keys: FontKeyMap,
+ pub instance_keys: FontInstanceKeyMap,
+}
+
+impl SharedFontResources {
+ pub fn new(namespace: IdNamespace) -> Self {
+ SharedFontResources {
+ templates: FontTemplateMap::new(),
+ instances: FontInstanceMap::new(),
+ font_keys: FontKeyMap::new(namespace),
+ instance_keys: FontInstanceKeyMap::new(namespace),
+ }
+ }
+}
+
+impl BlobImageResources for SharedFontResources {
+ fn get_font_data(&self, key: FontKey) -> Option<FontTemplate> {
+ let shared_key = self.font_keys.map_key(&key);
+ self.templates.get_font(&shared_key)
+ }
+
+ fn get_font_instance_data(&self, key: FontInstanceKey) -> Option<FontInstanceData> {
+ let shared_key = self.instance_keys.map_key(&key);
+ self.instances.get_font_instance_data(shared_key)
+ }
+}
+
+/// A mutable font instance description.
+///
+/// Performance is sensitive to the size of this structure, so it should only contain
+/// the fields that we need to modify from the original base font instance.
+#[derive(Clone, Debug, Ord, PartialOrd)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct FontInstance {
+ pub base: Arc<BaseFontInstance>,
+ pub transform: FontTransform,
+ pub render_mode: FontRenderMode,
+ pub flags: FontInstanceFlags,
+ pub color: ColorU,
+ // The font size is in *device/raster* pixels, not logical pixels.
+ // It is stored as an f32 since we need sub-pixel sizes.
+ pub size: FontSize,
+}
+
+impl Hash for FontInstance {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ // Hash only the base instance's key to avoid the cost of hashing
+ // the rest.
+ self.base.instance_key.hash(state);
+ self.transform.hash(state);
+ self.render_mode.hash(state);
+ self.flags.hash(state);
+ self.color.hash(state);
+ self.size.hash(state);
+ }
+}
+
+impl PartialEq for FontInstance {
+ fn eq(&self, other: &FontInstance) -> bool {
+ // Compare only the base instance's key.
+ self.base.instance_key == other.base.instance_key &&
+ self.transform == other.transform &&
+ self.render_mode == other.render_mode &&
+ self.flags == other.flags &&
+ self.color == other.color &&
+ self.size == other.size
+ }
+}
+impl Eq for FontInstance {}
+
+impl Deref for FontInstance {
+ type Target = BaseFontInstance;
+ fn deref(&self) -> &BaseFontInstance {
+ self.base.as_ref()
+ }
+}
+
+impl MallocSizeOf for FontInstance {
+ fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { 0 }
+}
+
+impl FontInstance {
+ pub fn new(
+ base: Arc<BaseFontInstance>,
+ color: ColorU,
+ render_mode: FontRenderMode,
+ flags: FontInstanceFlags,
+ ) -> Self {
+ FontInstance {
+ transform: FontTransform::identity(),
+ color,
+ size: base.size,
+ base,
+ render_mode,
+ flags,
+ }
+ }
+
+ pub fn from_base(
+ base: Arc<BaseFontInstance>,
+ ) -> Self {
+ let color = ColorU::new(0, 0, 0, 255);
+ let render_mode = base.render_mode;
+ let flags = base.flags;
+ Self::new(base, color, render_mode, flags)
+ }
+
+ pub fn use_texture_padding(&self) -> bool {
+ self.flags.contains(FontInstanceFlags::TEXTURE_PADDING)
+ }
+
+ pub fn use_transform_glyphs(&self) -> bool {
+ self.flags.contains(FontInstanceFlags::TRANSFORM_GLYPHS)
+ }
+
+ pub fn get_alpha_glyph_format(&self) -> GlyphFormat {
+ if self.use_transform_glyphs() { GlyphFormat::TransformedAlpha } else { GlyphFormat::Alpha }
+ }
+
+ pub fn get_subpixel_glyph_format(&self) -> GlyphFormat {
+ if self.use_transform_glyphs() { GlyphFormat::TransformedSubpixel } else { GlyphFormat::Subpixel }
+ }
+
+ pub fn disable_subpixel_aa(&mut self) {
+ self.render_mode = self.render_mode.limit_by(FontRenderMode::Alpha);
+ }
+
+ pub fn disable_subpixel_position(&mut self) {
+ self.flags.remove(FontInstanceFlags::SUBPIXEL_POSITION);
+ }
+
+ pub fn use_subpixel_position(&self) -> bool {
+ self.flags.contains(FontInstanceFlags::SUBPIXEL_POSITION) &&
+ self.render_mode != FontRenderMode::Mono
+ }
+
+ pub fn get_subpx_dir(&self) -> SubpixelDirection {
+ if self.use_subpixel_position() {
+ let mut subpx_dir = self.transform.get_subpx_dir();
+ if self.flags.contains(FontInstanceFlags::TRANSPOSE) {
+ subpx_dir = subpx_dir.swap_xy();
+ }
+ subpx_dir
+ } else {
+ SubpixelDirection::None
+ }
+ }
+
+ #[allow(dead_code)]
+ pub fn get_subpx_offset(&self, glyph: &GlyphKey) -> (f64, f64) {
+ if self.use_subpixel_position() {
+ let (dx, dy) = glyph.subpixel_offset();
+ (dx.into(), dy.into())
+ } else {
+ (0.0, 0.0)
+ }
+ }
+
+ #[allow(dead_code)]
+ pub fn get_glyph_format(&self) -> GlyphFormat {
+ match self.render_mode {
+ FontRenderMode::Mono | FontRenderMode::Alpha => self.get_alpha_glyph_format(),
+ FontRenderMode::Subpixel => self.get_subpixel_glyph_format(),
+ }
+ }
+
+ #[allow(dead_code)]
+ pub fn get_extra_strikes(&self, flags: FontInstanceFlags, x_scale: f64) -> usize {
+ if self.flags.intersects(flags) {
+ let mut bold_offset = self.size.to_f64_px() / 48.0;
+ if bold_offset < 1.0 {
+ bold_offset = 0.25 + 0.75 * bold_offset;
+ }
+ (bold_offset * x_scale).max(1.0).round() as usize
+ } else {
+ 0
+ }
+ }
+
+ pub fn synthesize_italics(&self, transform: FontTransform, size: f64) -> (FontTransform, (f64, f64)) {
+ transform.synthesize_italics(self.synthetic_italics, size, self.flags.contains(FontInstanceFlags::VERTICAL))
+ }
+
+ #[allow(dead_code)]
+ pub fn get_transformed_size(&self) -> f64 {
+ let (_, y_scale) = self.transform.compute_scale().unwrap_or((1.0, 1.0));
+ self.size.to_f64_px() * y_scale
+ }
+}
+
+#[repr(u32)]
+#[derive(Copy, Clone, Hash, PartialEq, Eq, Debug, Ord, PartialOrd)]
+pub enum SubpixelDirection {
+ None = 0,
+ Horizontal,
+ Vertical,
+ Mixed,
+}
+
+impl SubpixelDirection {
+ // Limit the subpixel direction to what is supported by the glyph format.
+ pub fn limit_by(self, glyph_format: GlyphFormat) -> Self {
+ match glyph_format {
+ GlyphFormat::Bitmap |
+ GlyphFormat::ColorBitmap => SubpixelDirection::None,
+ _ => self,
+ }
+ }
+
+ pub fn swap_xy(self) -> Self {
+ match self {
+ SubpixelDirection::None | SubpixelDirection::Mixed => self,
+ SubpixelDirection::Horizontal => SubpixelDirection::Vertical,
+ SubpixelDirection::Vertical => SubpixelDirection::Horizontal,
+ }
+ }
+}
+
+#[repr(u8)]
+#[derive(Hash, Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum SubpixelOffset {
+ Zero = 0,
+ Quarter = 1,
+ Half = 2,
+ ThreeQuarters = 3,
+}
+
+impl SubpixelOffset {
+ // Skia quantizes subpixel offsets into 1/4 increments.
+ // Given the absolute position, return the quantized increment
+ fn quantize(pos: f32) -> Self {
+ // Following the conventions of Gecko and Skia, we want
+ // to quantize the subpixel position, such that abs(pos) gives:
+ // [0.0, 0.125) -> Zero
+ // [0.125, 0.375) -> Quarter
+ // [0.375, 0.625) -> Half
+ // [0.625, 0.875) -> ThreeQuarters,
+ // [0.875, 1.0) -> Zero
+ // The unit tests below check for this.
+ let apos = ((pos - pos.floor()) * 8.0) as i32;
+
+ match apos {
+ 1..=2 => SubpixelOffset::Quarter,
+ 3..=4 => SubpixelOffset::Half,
+ 5..=6 => SubpixelOffset::ThreeQuarters,
+ _ => SubpixelOffset::Zero,
+ }
+ }
+}
+
+impl Into<f64> for SubpixelOffset {
+ fn into(self) -> f64 {
+ match self {
+ SubpixelOffset::Zero => 0.0,
+ SubpixelOffset::Quarter => 0.25,
+ SubpixelOffset::Half => 0.5,
+ SubpixelOffset::ThreeQuarters => 0.75,
+ }
+ }
+}
+
+#[derive(Copy, Clone, Hash, PartialEq, Eq, Debug, Ord, PartialOrd)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct GlyphKey(u32);
+
+impl GlyphKey {
+ pub fn new(
+ index: u32,
+ point: DevicePoint,
+ subpx_dir: SubpixelDirection,
+ ) -> Self {
+ let (dx, dy) = match subpx_dir {
+ SubpixelDirection::None => (0.0, 0.0),
+ SubpixelDirection::Horizontal => (point.x, 0.0),
+ SubpixelDirection::Vertical => (0.0, point.y),
+ SubpixelDirection::Mixed => (point.x, point.y),
+ };
+ let sox = SubpixelOffset::quantize(dx);
+ let soy = SubpixelOffset::quantize(dy);
+ assert_eq!(0, index & 0xF0000000);
+
+ GlyphKey(index | (sox as u32) << 28 | (soy as u32) << 30)
+ }
+
+ pub fn index(&self) -> GlyphIndex {
+ self.0 & 0x0FFFFFFF
+ }
+
+ fn subpixel_offset(&self) -> (SubpixelOffset, SubpixelOffset) {
+ let x = (self.0 >> 28) as u8 & 3;
+ let y = (self.0 >> 30) as u8 & 3;
+ unsafe {
+ (mem::transmute(x), mem::transmute(y))
+ }
+ }
+}
+
+#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[allow(dead_code)]
+pub enum GlyphFormat {
+ Alpha,
+ TransformedAlpha,
+ Subpixel,
+ TransformedSubpixel,
+ Bitmap,
+ ColorBitmap,
+}
+
+impl GlyphFormat {
+ /// Returns the ImageFormat that a glyph should be stored as in the texture cache.
+ /// can_use_r8_format should be set false on platforms where we have encountered
+ /// issues with R8 textures, so that we do not use them for glyphs.
+ pub fn image_format(&self, can_use_r8_format: bool) -> ImageFormat {
+ match *self {
+ GlyphFormat::Alpha |
+ GlyphFormat::TransformedAlpha |
+ GlyphFormat::Bitmap => {
+ if can_use_r8_format {
+ ImageFormat::R8
+ } else {
+ ImageFormat::BGRA8
+ }
+ }
+ GlyphFormat::Subpixel |
+ GlyphFormat::TransformedSubpixel |
+ GlyphFormat::ColorBitmap => ImageFormat::BGRA8,
+ }
+ }
+}
+
+#[allow(dead_code)]
+#[inline]
+fn blend_strike_pixel(dest: u8, src: u32, src_alpha: u32) -> u8 {
+ // Assume premultiplied alpha such that src and dest are already multiplied
+ // by their respective alpha values and in range 0..=255. The rounded over
+ // blend is then (src * 255 + dest * (255 - src_alpha) + 128) / 255.
+ // We approximate (x + 128) / 255 as (x + 128 + ((x + 128) >> 8)) >> 8.
+ let x = src * 255 + dest as u32 * (255 - src_alpha) + 128;
+ ((x + (x >> 8)) >> 8) as u8
+}
+
+// Blends a single strike at a given offset into a destination buffer, assuming
+// the destination has been allocated with enough extra space to accommodate the
+// offset.
+#[allow(dead_code)]
+fn blend_strike(
+ dest_bitmap: &mut [u8],
+ src_bitmap: &[u8],
+ width: usize,
+ height: usize,
+ subpixel_mask: bool,
+ offset: f64,
+) {
+ let dest_stride = dest_bitmap.len() / height;
+ let src_stride = width * 4;
+ let offset_integer = offset.floor() as usize * 4;
+ let offset_fract = (offset.fract() * 256.0) as u32;
+ for (src_row, dest_row) in src_bitmap.chunks(src_stride).zip(dest_bitmap.chunks_mut(dest_stride)) {
+ let mut prev_px = [0u32; 4];
+ let dest_row_offset = &mut dest_row[offset_integer .. offset_integer + src_stride];
+ for (src, dest) in src_row.chunks(4).zip(dest_row_offset.chunks_mut(4)) {
+ let px = [src[0] as u32, src[1] as u32, src[2] as u32, src[3] as u32];
+ // Blend current pixel with previous pixel based on fractional offset.
+ let next_px = [px[0] * offset_fract,
+ px[1] * offset_fract,
+ px[2] * offset_fract,
+ px[3] * offset_fract];
+ let offset_px = [(((px[0] << 8) - next_px[0]) + prev_px[0] + 128) >> 8,
+ (((px[1] << 8) - next_px[1]) + prev_px[1] + 128) >> 8,
+ (((px[2] << 8) - next_px[2]) + prev_px[2] + 128) >> 8,
+ (((px[3] << 8) - next_px[3]) + prev_px[3] + 128) >> 8];
+ if subpixel_mask {
+ // Subpixel masks assume each component is an independent weight.
+ dest[0] = blend_strike_pixel(dest[0], offset_px[0], offset_px[0]);
+ dest[1] = blend_strike_pixel(dest[1], offset_px[1], offset_px[1]);
+ dest[2] = blend_strike_pixel(dest[2], offset_px[2], offset_px[2]);
+ dest[3] = blend_strike_pixel(dest[3], offset_px[3], offset_px[3]);
+ } else {
+ // Otherwise assume we have a premultiplied alpha BGRA value.
+ dest[0] = blend_strike_pixel(dest[0], offset_px[0], offset_px[3]);
+ dest[1] = blend_strike_pixel(dest[1], offset_px[1], offset_px[3]);
+ dest[2] = blend_strike_pixel(dest[2], offset_px[2], offset_px[3]);
+ dest[3] = blend_strike_pixel(dest[3], offset_px[3], offset_px[3]);
+ }
+ // Save the remainder for blending onto the next pixel.
+ prev_px = next_px;
+ }
+ if offset_fract > 0 {
+ // When there is fractional offset, there will be a remaining value
+ // from the previous pixel but no next pixel, so just use that.
+ let dest = &mut dest_row[offset_integer + src_stride .. ];
+ let offset_px = [(prev_px[0] + 128) >> 8,
+ (prev_px[1] + 128) >> 8,
+ (prev_px[2] + 128) >> 8,
+ (prev_px[3] + 128) >> 8];
+ if subpixel_mask {
+ dest[0] = blend_strike_pixel(dest[0], offset_px[0], offset_px[0]);
+ dest[1] = blend_strike_pixel(dest[1], offset_px[1], offset_px[1]);
+ dest[2] = blend_strike_pixel(dest[2], offset_px[2], offset_px[2]);
+ dest[3] = blend_strike_pixel(dest[3], offset_px[3], offset_px[3]);
+ } else {
+ dest[0] = blend_strike_pixel(dest[0], offset_px[0], offset_px[3]);
+ dest[1] = blend_strike_pixel(dest[1], offset_px[1], offset_px[3]);
+ dest[2] = blend_strike_pixel(dest[2], offset_px[2], offset_px[3]);
+ dest[3] = blend_strike_pixel(dest[3], offset_px[3], offset_px[3]);
+ }
+ }
+ }
+}
+
+// Applies multistrike bold to a source bitmap. This assumes the source bitmap
+// is a tighly packed slice of BGRA pixel values of exactly the specified width
+// and height. The specified extra strikes and pixel step control where to put
+// each strike. The pixel step is allowed to have a fractional offset and does
+// not strictly need to be integer.
+#[allow(dead_code)]
+pub fn apply_multistrike_bold(
+ src_bitmap: &[u8],
+ width: usize,
+ height: usize,
+ subpixel_mask: bool,
+ extra_strikes: usize,
+ pixel_step: f64,
+) -> (Vec<u8>, usize) {
+ let src_stride = width * 4;
+ // The amount of extra width added to the bitmap from the extra strikes.
+ let extra_width = (extra_strikes as f64 * pixel_step).ceil() as usize;
+ let dest_width = width + extra_width;
+ let dest_stride = dest_width * 4;
+ // Zero out the initial bitmap so any extra width is cleared.
+ let mut dest_bitmap = vec![0u8; dest_stride * height];
+ for (src_row, dest_row) in src_bitmap.chunks(src_stride).zip(dest_bitmap.chunks_mut(dest_stride)) {
+ // Copy the initial bitmap strike rows directly from the source.
+ dest_row[0 .. src_stride].copy_from_slice(src_row);
+ }
+ // Finally blend each extra strike in turn.
+ for i in 1 ..= extra_strikes {
+ let offset = i as f64 * pixel_step;
+ blend_strike(&mut dest_bitmap, src_bitmap, width, height, subpixel_mask, offset);
+ }
+ (dest_bitmap, dest_width)
+}
+
+pub struct RasterizedGlyph {
+ pub top: f32,
+ pub left: f32,
+ pub width: i32,
+ pub height: i32,
+ pub scale: f32,
+ pub format: GlyphFormat,
+ pub bytes: Vec<u8>,
+}
+
+impl RasterizedGlyph {
+ #[allow(dead_code)]
+ pub fn downscale_bitmap_if_required(&mut self, font: &FontInstance) {
+ // Check if the glyph is going to be downscaled in the shader. If the scaling is
+ // less than 0.5, that means bilinear filtering can't effectively filter the glyph
+ // without aliasing artifacts.
+ //
+ // Instead of fixing this by mipmapping the glyph cache texture, rather manually
+ // produce the appropriate mip level for individual glyphs where bilinear filtering
+ // will still produce acceptable results.
+ match self.format {
+ GlyphFormat::Bitmap | GlyphFormat::ColorBitmap => {},
+ _ => return,
+ }
+ let (x_scale, y_scale) = font.transform.compute_scale().unwrap_or((1.0, 1.0));
+ let upscaled = x_scale.max(y_scale) as f32;
+ let mut new_scale = self.scale;
+ if new_scale * upscaled <= 0.0 {
+ return;
+ }
+ let mut steps = 0;
+ while new_scale * upscaled <= 0.5 {
+ new_scale *= 2.0;
+ steps += 1;
+ }
+ // If no mipping is necessary, just bail.
+ if steps == 0 {
+ return;
+ }
+
+ // Calculate the actual size of the mip level.
+ let new_width = (self.width as usize + (1 << steps) - 1) >> steps;
+ let new_height = (self.height as usize + (1 << steps) - 1) >> steps;
+ let mut new_bytes: Vec<u8> = Vec::with_capacity(new_width * new_height * 4);
+
+ // Produce destination pixels by applying a box filter to the source pixels.
+ // The box filter corresponds to how graphics drivers may generate mipmaps.
+ for y in 0 .. new_height {
+ for x in 0 .. new_width {
+ // Calculate the number of source samples that contribute to the destination pixel.
+ let src_y = y << steps;
+ let src_x = x << steps;
+ let y_samples = (1 << steps).min(self.height as usize - src_y);
+ let x_samples = (1 << steps).min(self.width as usize - src_x);
+ let num_samples = (x_samples * y_samples) as u32;
+
+ let mut src_idx = (src_y * self.width as usize + src_x) * 4;
+ // Initialize the accumulator with half an increment so that when later divided
+ // by the sample count, it will effectively round the accumulator to the nearest
+ // increment.
+ let mut accum = [num_samples / 2; 4];
+ // Accumulate all the contributing source sampless.
+ for _ in 0 .. y_samples {
+ for _ in 0 .. x_samples {
+ accum[0] += self.bytes[src_idx + 0] as u32;
+ accum[1] += self.bytes[src_idx + 1] as u32;
+ accum[2] += self.bytes[src_idx + 2] as u32;
+ accum[3] += self.bytes[src_idx + 3] as u32;
+ src_idx += 4;
+ }
+ src_idx += (self.width as usize - x_samples) * 4;
+ }
+
+ // Finally, divide by the sample count to get the mean value for the new pixel.
+ new_bytes.extend_from_slice(&[
+ (accum[0] / num_samples) as u8,
+ (accum[1] / num_samples) as u8,
+ (accum[2] / num_samples) as u8,
+ (accum[3] / num_samples) as u8,
+ ]);
+ }
+ }
+
+ // Fix the bounds for the new glyph data.
+ self.top /= (1 << steps) as f32;
+ self.left /= (1 << steps) as f32;
+ self.width = new_width as i32;
+ self.height = new_height as i32;
+ self.scale = new_scale;
+ self.bytes = new_bytes;
+ }
+}
+
+pub struct FontContexts {
+ // These worker are mostly accessed from their corresponding worker threads.
+ // The goal is that there should be no noticeable contention on the mutexes.
+ worker_contexts: Vec<Mutex<FontContext>>,
+ // Stored here as a convenience to get the current thread index.
+ #[allow(dead_code)]
+ workers: Arc<ThreadPool>,
+ locked_mutex: Mutex<bool>,
+ locked_cond: Condvar,
+}
+
+impl FontContexts {
+ /// Get access to any particular font context.
+ ///
+ /// The id is an index between 0 and num_worker_contexts for font contexts
+ /// associated to the thread pool.
+ pub fn lock_context(&self, id: usize) -> MutexGuard<FontContext> {
+ self.worker_contexts[id].lock().unwrap()
+ }
+
+ // Find a context that is currently unlocked to use, otherwise defaulting
+ // to the first context.
+ pub fn lock_any_context(&self) -> MutexGuard<FontContext> {
+ for context in &self.worker_contexts {
+ if let Ok(mutex) = context.try_lock() {
+ return mutex;
+ }
+ }
+ self.lock_context(0)
+ }
+
+ // number of contexts associated to workers
+ pub fn num_worker_contexts(&self) -> usize {
+ self.worker_contexts.len()
+ }
+}
+
+pub trait AsyncForEach<T> {
+ fn async_for_each<F: Fn(MutexGuard<T>) + Send + 'static>(&self, f: F);
+}
+
+impl AsyncForEach<FontContext> for Arc<FontContexts> {
+ fn async_for_each<F: Fn(MutexGuard<FontContext>) + Send + 'static>(&self, f: F) {
+ // Reset the locked condition.
+ let mut locked = self.locked_mutex.lock().unwrap();
+ *locked = false;
+
+ // Arc that can be safely moved into a spawn closure.
+ let font_contexts = self.clone();
+ // Spawn a new thread on which to run the for-each off the main thread.
+ self.workers.spawn(move || {
+ // Lock the shared and worker contexts up front.
+ let mut locks = Vec::with_capacity(font_contexts.num_worker_contexts());
+ for i in 0 .. font_contexts.num_worker_contexts() {
+ locks.push(font_contexts.lock_context(i));
+ }
+
+ // Signal the locked condition now that all contexts are locked.
+ *font_contexts.locked_mutex.lock().unwrap() = true;
+ font_contexts.locked_cond.notify_all();
+
+ // Now that everything is locked, proceed to processing each locked context.
+ for context in locks {
+ f(context);
+ }
+ });
+
+ // Wait for locked condition before resuming. Safe to proceed thereafter
+ // since any other thread that needs to use a FontContext will try to lock
+ // it first.
+ while !*locked {
+ locked = self.locked_cond.wait(locked).unwrap();
+ }
+ }
+}
+
+pub struct GlyphRasterizer {
+ #[allow(dead_code)]
+ workers: Arc<ThreadPool>,
+ font_contexts: Arc<FontContexts>,
+
+ /// The current set of loaded fonts.
+ fonts: FastHashSet<FontKey>,
+
+ /// The current number of individual glyphs waiting in pending batches.
+ pending_glyph_count: usize,
+
+ /// The current number of glyph request jobs that have been kicked to worker threads.
+ pending_glyph_jobs: usize,
+
+ /// The number of glyphs requested this frame.
+ glyph_request_count: usize,
+
+ /// A map of current glyph request batches.
+ pending_glyph_requests: FastHashMap<FontInstance, SmallVec<[GlyphKey; 16]>>,
+
+ // Receives the rendered glyphs.
+ glyph_rx: Receiver<GlyphRasterJob>,
+ glyph_tx: Sender<GlyphRasterJob>,
+
+ // We defer removing fonts to the end of the frame so that:
+ // - this work is done outside of the critical path,
+ // - we don't have to worry about the ordering of events if a font is used on
+ // a frame where it is used (although it seems unlikely).
+ fonts_to_remove: Vec<FontKey>,
+ // Defer removal of font instances, as for fonts.
+ font_instances_to_remove: Vec<FontInstance>,
+
+ // Whether to parallelize glyph rasterization with rayon.
+ enable_multithreading: bool,
+
+ // Whether glyphs can be rasterized in r8 format when it makes sense.
+ can_use_r8_format: bool,
+}
+
+impl GlyphRasterizer {
+ pub fn new(workers: Arc<ThreadPool>, can_use_r8_format: bool) -> Self {
+ let (glyph_tx, glyph_rx) = unbounded();
+
+ let num_workers = workers.current_num_threads();
+ let mut contexts = Vec::with_capacity(num_workers);
+
+ for _ in 0 .. num_workers {
+ contexts.push(Mutex::new(FontContext::new()));
+ }
+
+ let font_context = FontContexts {
+ worker_contexts: contexts,
+ workers: Arc::clone(&workers),
+ locked_mutex: Mutex::new(false),
+ locked_cond: Condvar::new(),
+ };
+
+ GlyphRasterizer {
+ font_contexts: Arc::new(font_context),
+ fonts: FastHashSet::default(),
+ pending_glyph_jobs: 0,
+ pending_glyph_count: 0,
+ glyph_request_count: 0,
+ glyph_rx,
+ glyph_tx,
+ workers,
+ fonts_to_remove: Vec::new(),
+ font_instances_to_remove: Vec::new(),
+ enable_multithreading: true,
+ pending_glyph_requests: FastHashMap::default(),
+ can_use_r8_format,
+ }
+ }
+
+ pub fn add_font(&mut self, font_key: FontKey, template: FontTemplate) {
+ if self.fonts.insert(font_key.clone()) {
+ // Only add font to FontContexts if not previously added.
+ self.font_contexts.async_for_each(move |mut context| {
+ context.add_font(&font_key, &template);
+ });
+ }
+ }
+
+ pub fn delete_font(&mut self, font_key: FontKey) {
+ self.fonts_to_remove.push(font_key);
+ }
+
+ pub fn delete_fonts(&mut self, font_keys: &[FontKey]) {
+ self.fonts_to_remove.extend_from_slice(font_keys);
+ }
+
+ pub fn delete_font_instance(&mut self, instance: &FontInstance) {
+ self.font_instances_to_remove.push(instance.clone());
+ }
+
+ pub fn prepare_font(&self, font: &mut FontInstance) {
+ FontContext::prepare_font(font);
+
+ // Quantize the transform to minimize thrashing of the glyph cache, but
+ // only quantize the transform when preparing to access the glyph cache.
+ // This way, the glyph subpixel positions, which are calculated before
+ // this, can still use the precise transform which is required to match
+ // the subpixel positions computed for glyphs in the text run shader.
+ font.transform = font.transform.quantize();
+ }
+
+ pub fn has_font(&self, font_key: FontKey) -> bool {
+ self.fonts.contains(&font_key)
+ }
+
+ pub fn get_glyph_dimensions(
+ &mut self,
+ font: &FontInstance,
+ glyph_index: GlyphIndex,
+ ) -> Option<GlyphDimensions> {
+ let glyph_key = GlyphKey::new(
+ glyph_index,
+ DevicePoint::zero(),
+ SubpixelDirection::None,
+ );
+
+ self.font_contexts
+ .lock_any_context()
+ .get_glyph_dimensions(font, &glyph_key)
+ }
+
+ pub fn get_glyph_index(&mut self, font_key: FontKey, ch: char) -> Option<u32> {
+ self.font_contexts
+ .lock_any_context()
+ .get_glyph_index(font_key, ch)
+ }
+
+ fn remove_dead_fonts(&mut self) {
+ if self.fonts_to_remove.is_empty() && self.font_instances_to_remove.is_empty() {
+ return
+ }
+
+ profile_scope!("remove_dead_fonts");
+ let mut fonts_to_remove = mem::replace(& mut self.fonts_to_remove, Vec::new());
+ // Only remove font from FontContexts if previously added.
+ fonts_to_remove.retain(|font| self.fonts.remove(font));
+ let font_instances_to_remove = mem::replace(& mut self.font_instances_to_remove, Vec::new());
+ self.font_contexts.async_for_each(move |mut context| {
+ for font_key in &fonts_to_remove {
+ context.delete_font(font_key);
+ }
+ for instance in &font_instances_to_remove {
+ context.delete_font_instance(instance);
+ }
+ });
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn reset(&mut self) {
+ //TODO: any signals need to be sent to the workers?
+ self.pending_glyph_jobs = 0;
+ self.pending_glyph_count = 0;
+ self.glyph_request_count = 0;
+ self.fonts_to_remove.clear();
+ self.font_instances_to_remove.clear();
+ }
+}
+
+trait AddFont {
+ fn add_font(&mut self, font_key: &FontKey, template: &FontTemplate);
+}
+
+impl AddFont for FontContext {
+ fn add_font(&mut self, font_key: &FontKey, template: &FontTemplate) {
+ match *template {
+ FontTemplate::Raw(ref bytes, index) => {
+ self.add_raw_font(font_key, bytes.clone(), index);
+ }
+ FontTemplate::Native(ref native_font_handle) => {
+ self.add_native_font(font_key, (*native_font_handle).clone());
+ }
+ }
+ }
+}
+
+#[allow(dead_code)]
+pub struct GlyphRasterJob {
+ pub font: Arc<FontInstance>,
+ pub key: GlyphKey,
+ pub result: GlyphRasterResult,
+}
+
+#[allow(dead_code)]
+#[derive(Debug)]
+pub enum GlyphRasterError {
+ LoadFailed,
+}
+
+#[allow(dead_code)]
+pub type GlyphRasterResult = Result<RasterizedGlyph, GlyphRasterError>;
+
+#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct GpuGlyphCacheKey(pub u32);
+
+#[cfg(test)]
+mod test_glyph_rasterizer {
+ use crate::profiler::GlyphRasterizeProfiler;
+
+ struct Profiler;
+ impl GlyphRasterizeProfiler for Profiler {
+ fn start_time(&mut self) {}
+ fn end_time(&mut self) -> f64 {
+ 0.
+ }
+ fn set(&mut self, _value: f64) {}
+ }
+
+ #[test]
+ fn rasterize_200_glyphs() {
+ // This test loads a font from disc, the renders 4 requests containing
+ // 50 glyphs each, deletes the font and waits for the result.
+
+ use rayon::ThreadPoolBuilder;
+ use std::fs::File;
+ use std::io::Read;
+ use api::{FontKey, FontInstanceKey, FontTemplate, IdNamespace};
+ use api::units::DevicePoint;
+ use std::sync::Arc;
+ use crate::rasterizer::{FontInstance, BaseFontInstance, GlyphKey, GlyphRasterizer};
+
+ let worker = ThreadPoolBuilder::new()
+ .thread_name(|idx|{ format!("WRWorker#{}", idx) })
+ .build();
+ let workers = Arc::new(worker.unwrap());
+ let mut glyph_rasterizer = GlyphRasterizer::new(workers, true);
+ let mut font_file =
+ File::open("../wrench/reftests/text/VeraBd.ttf").expect("Couldn't open font file");
+ let mut font_data = vec![];
+ font_file
+ .read_to_end(&mut font_data)
+ .expect("failed to read font file");
+
+ let font_key = FontKey::new(IdNamespace(0), 0);
+ glyph_rasterizer.add_font(font_key, FontTemplate::Raw(Arc::new(font_data), 0));
+
+ let font = FontInstance::from_base(Arc::new(BaseFontInstance::new(
+ FontInstanceKey::new(IdNamespace(0), 0),
+ font_key,
+ 32.0,
+ None,
+ None,
+ Vec::new(),
+ )));
+
+ let subpx_dir = font.get_subpx_dir();
+
+ let mut glyph_keys = Vec::with_capacity(200);
+ for i in 0 .. 200 {
+ glyph_keys.push(GlyphKey::new(
+ i,
+ DevicePoint::zero(),
+ subpx_dir,
+ ));
+ }
+
+ for i in 0 .. 4 {
+ glyph_rasterizer.request_glyphs(
+ font.clone(),
+ &glyph_keys[(50 * i) .. (50 * (i + 1))],
+ |_| true,
+ );
+ }
+
+ glyph_rasterizer.delete_font(font_key);
+
+ glyph_rasterizer.resolve_glyphs(
+ |_, _| {},
+ &mut Profiler,
+ );
+ }
+
+ #[test]
+ fn rasterize_large_glyphs() {
+ // This test loads a font from disc and rasterize a few glyphs with a size of 200px to check
+ // that the texture cache handles them properly.
+ use rayon::ThreadPoolBuilder;
+ use std::fs::File;
+ use std::io::Read;
+ use api::{FontKey, FontInstanceKey, FontTemplate, IdNamespace};
+ use api::units::DevicePoint;
+ use std::sync::Arc;
+ use crate::rasterizer::{FontInstance, BaseFontInstance, GlyphKey, GlyphRasterizer};
+
+ let worker = ThreadPoolBuilder::new()
+ .thread_name(|idx|{ format!("WRWorker#{}", idx) })
+ .build();
+ let workers = Arc::new(worker.unwrap());
+ let mut glyph_rasterizer = GlyphRasterizer::new(workers, true);
+ let mut font_file =
+ File::open("../wrench/reftests/text/VeraBd.ttf").expect("Couldn't open font file");
+ let mut font_data = vec![];
+ font_file
+ .read_to_end(&mut font_data)
+ .expect("failed to read font file");
+
+ let font_key = FontKey::new(IdNamespace(0), 0);
+ glyph_rasterizer.add_font(font_key, FontTemplate::Raw(Arc::new(font_data), 0));
+
+ let font = FontInstance::from_base(Arc::new(BaseFontInstance::new(
+ FontInstanceKey::new(IdNamespace(0), 0),
+ font_key,
+ 200.0,
+ None,
+ None,
+ Vec::new(),
+ )));
+
+ let subpx_dir = font.get_subpx_dir();
+
+ let mut glyph_keys = Vec::with_capacity(10);
+ for i in 0 .. 10 {
+ glyph_keys.push(GlyphKey::new(
+ i,
+ DevicePoint::zero(),
+ subpx_dir,
+ ));
+ }
+
+ glyph_rasterizer.request_glyphs(
+ font.clone(),
+ &glyph_keys,
+ |_| true,
+ );
+
+ glyph_rasterizer.delete_font(font_key);
+
+ glyph_rasterizer.resolve_glyphs(
+ |_, _| {},
+ &mut Profiler,
+ );
+ }
+
+ #[test]
+ fn test_subpx_quantize() {
+ use crate::rasterizer::SubpixelOffset;
+
+ assert_eq!(SubpixelOffset::quantize(0.0), SubpixelOffset::Zero);
+ assert_eq!(SubpixelOffset::quantize(-0.0), SubpixelOffset::Zero);
+
+ assert_eq!(SubpixelOffset::quantize(0.1), SubpixelOffset::Zero);
+ assert_eq!(SubpixelOffset::quantize(0.01), SubpixelOffset::Zero);
+ assert_eq!(SubpixelOffset::quantize(0.05), SubpixelOffset::Zero);
+ assert_eq!(SubpixelOffset::quantize(0.12), SubpixelOffset::Zero);
+ assert_eq!(SubpixelOffset::quantize(0.124), SubpixelOffset::Zero);
+
+ assert_eq!(SubpixelOffset::quantize(0.125), SubpixelOffset::Quarter);
+ assert_eq!(SubpixelOffset::quantize(0.2), SubpixelOffset::Quarter);
+ assert_eq!(SubpixelOffset::quantize(0.25), SubpixelOffset::Quarter);
+ assert_eq!(SubpixelOffset::quantize(0.33), SubpixelOffset::Quarter);
+ assert_eq!(SubpixelOffset::quantize(0.374), SubpixelOffset::Quarter);
+
+ assert_eq!(SubpixelOffset::quantize(0.375), SubpixelOffset::Half);
+ assert_eq!(SubpixelOffset::quantize(0.4), SubpixelOffset::Half);
+ assert_eq!(SubpixelOffset::quantize(0.5), SubpixelOffset::Half);
+ assert_eq!(SubpixelOffset::quantize(0.58), SubpixelOffset::Half);
+ assert_eq!(SubpixelOffset::quantize(0.624), SubpixelOffset::Half);
+
+ assert_eq!(SubpixelOffset::quantize(0.625), SubpixelOffset::ThreeQuarters);
+ assert_eq!(SubpixelOffset::quantize(0.67), SubpixelOffset::ThreeQuarters);
+ assert_eq!(SubpixelOffset::quantize(0.7), SubpixelOffset::ThreeQuarters);
+ assert_eq!(SubpixelOffset::quantize(0.78), SubpixelOffset::ThreeQuarters);
+ assert_eq!(SubpixelOffset::quantize(0.874), SubpixelOffset::ThreeQuarters);
+
+ assert_eq!(SubpixelOffset::quantize(0.875), SubpixelOffset::Zero);
+ assert_eq!(SubpixelOffset::quantize(0.89), SubpixelOffset::Zero);
+ assert_eq!(SubpixelOffset::quantize(0.91), SubpixelOffset::Zero);
+ assert_eq!(SubpixelOffset::quantize(0.967), SubpixelOffset::Zero);
+ assert_eq!(SubpixelOffset::quantize(0.999), SubpixelOffset::Zero);
+
+ assert_eq!(SubpixelOffset::quantize(-1.0), SubpixelOffset::Zero);
+ assert_eq!(SubpixelOffset::quantize(1.0), SubpixelOffset::Zero);
+ assert_eq!(SubpixelOffset::quantize(1.5), SubpixelOffset::Half);
+ assert_eq!(SubpixelOffset::quantize(-1.625), SubpixelOffset::Half);
+ assert_eq!(SubpixelOffset::quantize(-4.33), SubpixelOffset::ThreeQuarters);
+ }
+}
diff --git a/gfx/wr/wr_glyph_rasterizer/src/telemetry.rs b/gfx/wr/wr_glyph_rasterizer/src/telemetry.rs
new file mode 100644
index 0000000000..28000440b2
--- /dev/null
+++ b/gfx/wr/wr_glyph_rasterizer/src/telemetry.rs
@@ -0,0 +1,28 @@
+/* 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/. */
+
+#[cfg(feature = "gecko")]
+use glean::TimerId;
+#[cfg(feature = "gecko")]
+use firefox_on_glean::metrics::wr;
+
+#[cfg(not(feature = "gecko"))]
+pub struct TimerId;
+
+pub struct Telemetry;
+
+/// Defines the interface for hooking up an external telemetry reporter to WR.
+#[cfg(not(feature = "gecko"))]
+impl Telemetry {
+ // Start rasterize glyph time collection
+ pub fn start_rasterize_glyphs_time() -> TimerId { return TimerId {}; }
+ // End rasterize glyph time collection
+ pub fn stop_and_accumulate_rasterize_glyphs_time(_id: TimerId) { }
+}
+
+#[cfg(feature = "gecko")]
+impl Telemetry {
+ pub fn start_rasterize_glyphs_time() -> TimerId { wr::rasterize_glyphs_time.start() }
+ pub fn stop_and_accumulate_rasterize_glyphs_time(id: TimerId) { wr::rasterize_glyphs_time.stop_and_accumulate(id); }
+}
diff --git a/gfx/wr/wr_glyph_rasterizer/src/types.rs b/gfx/wr/wr_glyph_rasterizer/src/types.rs
new file mode 100644
index 0000000000..665ab0f86b
--- /dev/null
+++ b/gfx/wr/wr_glyph_rasterizer/src/types.rs
@@ -0,0 +1,10 @@
+/* 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 fxhash::FxHasher;
+use std::collections::{HashMap, HashSet};
+use std::hash::BuildHasherDefault;
+
+pub type FastHashMap<K, V> = HashMap<K, V, BuildHasherDefault<FxHasher>>;
+pub type FastHashSet<K> = HashSet<K, BuildHasherDefault<FxHasher>>;
diff --git a/gfx/wr/wr_malloc_size_of/Cargo.toml b/gfx/wr/wr_malloc_size_of/Cargo.toml
new file mode 100644
index 0000000000..d5e5f0d10c
--- /dev/null
+++ b/gfx/wr/wr_malloc_size_of/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+authors = ["The Servo Project Developers"]
+description = "Internal utility to measure memory usage in WebRender."
+name = "wr_malloc_size_of"
+version = "0.0.2"
+license = "MIT/Apache-2.0"
+edition = "2018"
+
+[lib]
+path = "lib.rs"
+
+[dependencies]
+app_units = "0.7"
+euclid = "0.22"
diff --git a/gfx/wr/wr_malloc_size_of/LICENSE-APACHE b/gfx/wr/wr_malloc_size_of/LICENSE-APACHE
new file mode 100644
index 0000000000..16fe87b06e
--- /dev/null
+++ b/gfx/wr/wr_malloc_size_of/LICENSE-APACHE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/gfx/wr/wr_malloc_size_of/LICENSE-MIT b/gfx/wr/wr_malloc_size_of/LICENSE-MIT
new file mode 100644
index 0000000000..31aa79387f
--- /dev/null
+++ b/gfx/wr/wr_malloc_size_of/LICENSE-MIT
@@ -0,0 +1,23 @@
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/gfx/wr/wr_malloc_size_of/lib.rs b/gfx/wr/wr_malloc_size_of/lib.rs
new file mode 100644
index 0000000000..ed432e2a50
--- /dev/null
+++ b/gfx/wr/wr_malloc_size_of/lib.rs
@@ -0,0 +1,451 @@
+// Copyright 2016-2017 The Servo Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+//! A reduced fork of Firefox's malloc_size_of crate, for bundling with WebRender.
+
+extern crate app_units;
+extern crate euclid;
+
+use std::hash::{BuildHasher, Hash};
+use std::mem::size_of;
+use std::ops::Range;
+use std::os::raw::c_void;
+use std::path::PathBuf;
+
+/// A C function that takes a pointer to a heap allocation and returns its size.
+type VoidPtrToSizeFn = unsafe extern "C" fn(ptr: *const c_void) -> usize;
+
+/// Operations used when measuring heap usage of data structures.
+pub struct MallocSizeOfOps {
+ /// A function that returns the size of a heap allocation.
+ pub size_of_op: VoidPtrToSizeFn,
+
+ /// Like `size_of_op`, but can take an interior pointer. Optional because
+ /// not all allocators support this operation. If it's not provided, some
+ /// memory measurements will actually be computed estimates rather than
+ /// real and accurate measurements.
+ pub enclosing_size_of_op: Option<VoidPtrToSizeFn>,
+}
+
+impl MallocSizeOfOps {
+ pub fn new(
+ size_of: VoidPtrToSizeFn,
+ malloc_enclosing_size_of: Option<VoidPtrToSizeFn>,
+ ) -> Self {
+ MallocSizeOfOps {
+ size_of_op: size_of,
+ enclosing_size_of_op: malloc_enclosing_size_of,
+ }
+ }
+
+ /// Check if an allocation is empty. This relies on knowledge of how Rust
+ /// handles empty allocations, which may change in the future.
+ fn is_empty<T: ?Sized>(ptr: *const T) -> bool {
+ // The correct condition is this:
+ // `ptr as usize <= ::std::mem::align_of::<T>()`
+ // But we can't call align_of() on a ?Sized T. So we approximate it
+ // with the following. 256 is large enough that it should always be
+ // larger than the required alignment, but small enough that it is
+ // always in the first page of memory and therefore not a legitimate
+ // address.
+ ptr as *const usize as usize <= 256
+ }
+
+ /// Call `size_of_op` on `ptr`, first checking that the allocation isn't
+ /// empty, because some types (such as `Vec`) utilize empty allocations.
+ pub unsafe fn malloc_size_of<T: ?Sized>(&self, ptr: *const T) -> usize {
+ if MallocSizeOfOps::is_empty(ptr) {
+ 0
+ } else {
+ (self.size_of_op)(ptr as *const c_void)
+ }
+ }
+
+ /// Is an `enclosing_size_of_op` available?
+ pub fn has_malloc_enclosing_size_of(&self) -> bool {
+ self.enclosing_size_of_op.is_some()
+ }
+
+ /// Call `enclosing_size_of_op`, which must be available, on `ptr`, which
+ /// must not be empty.
+ pub unsafe fn malloc_enclosing_size_of<T>(&self, ptr: *const T) -> usize {
+ assert!(!MallocSizeOfOps::is_empty(ptr));
+ (self.enclosing_size_of_op.unwrap())(ptr as *const c_void)
+ }
+}
+
+/// Trait for measuring the "deep" heap usage of a data structure. This is the
+/// most commonly-used of the traits.
+pub trait MallocSizeOf {
+ /// Measure the heap usage of all descendant heap-allocated structures, but
+ /// not the space taken up by the value itself.
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize;
+}
+
+/// Trait for measuring the "shallow" heap usage of a container.
+pub trait MallocShallowSizeOf {
+ /// Measure the heap usage of immediate heap-allocated descendant
+ /// structures, but not the space taken up by the value itself. Anything
+ /// beyond the immediate descendants must be measured separately, using
+ /// iteration.
+ fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize;
+}
+
+impl MallocSizeOf for String {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ unsafe { ops.malloc_size_of(self.as_ptr()) }
+ }
+}
+
+impl<T: ?Sized> MallocShallowSizeOf for Box<T> {
+ fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ unsafe { ops.malloc_size_of(&**self) }
+ }
+}
+
+impl<T: MallocSizeOf + ?Sized> MallocSizeOf for Box<T> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.shallow_size_of(ops) + (**self).size_of(ops)
+ }
+}
+
+impl MallocSizeOf for () {
+ fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize {
+ 0
+ }
+}
+
+impl<T1, T2> MallocSizeOf for (T1, T2)
+where
+ T1: MallocSizeOf,
+ T2: MallocSizeOf,
+{
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.0.size_of(ops) + self.1.size_of(ops)
+ }
+}
+
+impl<T1, T2, T3> MallocSizeOf for (T1, T2, T3)
+where
+ T1: MallocSizeOf,
+ T2: MallocSizeOf,
+ T3: MallocSizeOf,
+{
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.0.size_of(ops) + self.1.size_of(ops) + self.2.size_of(ops)
+ }
+}
+
+impl<T1, T2, T3, T4> MallocSizeOf for (T1, T2, T3, T4)
+where
+ T1: MallocSizeOf,
+ T2: MallocSizeOf,
+ T3: MallocSizeOf,
+ T4: MallocSizeOf,
+{
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.0.size_of(ops) + self.1.size_of(ops) + self.2.size_of(ops) + self.3.size_of(ops)
+ }
+}
+
+impl<T: MallocSizeOf> MallocSizeOf for Option<T> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ if let Some(val) = self.as_ref() {
+ val.size_of(ops)
+ } else {
+ 0
+ }
+ }
+}
+
+impl<T: MallocSizeOf, E: MallocSizeOf> MallocSizeOf for Result<T, E> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ match *self {
+ Ok(ref x) => x.size_of(ops),
+ Err(ref e) => e.size_of(ops),
+ }
+ }
+}
+
+impl<T: MallocSizeOf + Copy> MallocSizeOf for std::cell::Cell<T> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.get().size_of(ops)
+ }
+}
+
+impl<T: MallocSizeOf> MallocSizeOf for std::cell::RefCell<T> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.borrow().size_of(ops)
+ }
+}
+
+impl<'a, B: ?Sized + ToOwned> MallocSizeOf for std::borrow::Cow<'a, B>
+where
+ B::Owned: MallocSizeOf,
+{
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ match *self {
+ std::borrow::Cow::Borrowed(_) => 0,
+ std::borrow::Cow::Owned(ref b) => b.size_of(ops),
+ }
+ }
+}
+
+impl<T: MallocSizeOf> MallocSizeOf for [T] {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ let mut n = 0;
+ for elem in self.iter() {
+ n += elem.size_of(ops);
+ }
+ n
+ }
+}
+
+impl<T> MallocShallowSizeOf for Vec<T> {
+ fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ unsafe { ops.malloc_size_of(self.as_ptr()) }
+ }
+}
+
+impl<T: MallocSizeOf> MallocSizeOf for Vec<T> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ let mut n = self.shallow_size_of(ops);
+ for elem in self.iter() {
+ n += elem.size_of(ops);
+ }
+ n
+ }
+}
+
+macro_rules! malloc_size_of_hash_set {
+ ($ty:ty) => {
+ impl<T, S> MallocShallowSizeOf for $ty
+ where
+ T: Eq + Hash,
+ S: BuildHasher,
+ {
+ fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ if ops.has_malloc_enclosing_size_of() {
+ // The first value from the iterator gives us an interior pointer.
+ // `ops.malloc_enclosing_size_of()` then gives us the storage size.
+ // This assumes that the `HashSet`'s contents (values and hashes)
+ // are all stored in a single contiguous heap allocation.
+ self.iter()
+ .next()
+ .map_or(0, |t| unsafe { ops.malloc_enclosing_size_of(t) })
+ } else {
+ // An estimate.
+ self.capacity() * (size_of::<T>() + size_of::<usize>())
+ }
+ }
+ }
+
+ impl<T, S> MallocSizeOf for $ty
+ where
+ T: Eq + Hash + MallocSizeOf,
+ S: BuildHasher,
+ {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ let mut n = self.shallow_size_of(ops);
+ for t in self.iter() {
+ n += t.size_of(ops);
+ }
+ n
+ }
+ }
+ };
+}
+
+malloc_size_of_hash_set!(std::collections::HashSet<T, S>);
+
+macro_rules! malloc_size_of_hash_map {
+ ($ty:ty) => {
+ impl<K, V, S> MallocShallowSizeOf for $ty
+ where
+ K: Eq + Hash,
+ S: BuildHasher,
+ {
+ fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ // See the implementation for std::collections::HashSet for details.
+ if ops.has_malloc_enclosing_size_of() {
+ self.values()
+ .next()
+ .map_or(0, |v| unsafe { ops.malloc_enclosing_size_of(v) })
+ } else {
+ self.capacity() * (size_of::<V>() + size_of::<K>() + size_of::<usize>())
+ }
+ }
+ }
+
+ impl<K, V, S> MallocSizeOf for $ty
+ where
+ K: Eq + Hash + MallocSizeOf,
+ V: MallocSizeOf,
+ S: BuildHasher,
+ {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ let mut n = self.shallow_size_of(ops);
+ for (k, v) in self.iter() {
+ n += k.size_of(ops);
+ n += v.size_of(ops);
+ }
+ n
+ }
+ }
+ };
+}
+
+malloc_size_of_hash_map!(std::collections::HashMap<K, V, S>);
+
+// PhantomData is always 0.
+impl<T> MallocSizeOf for std::marker::PhantomData<T> {
+ fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize {
+ 0
+ }
+}
+
+impl MallocSizeOf for PathBuf {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ match self.to_str() {
+ Some(s) => unsafe { ops.malloc_size_of(s.as_ptr()) },
+ None => self.as_os_str().len(),
+ }
+ }
+}
+
+impl<T: MallocSizeOf, Unit> MallocSizeOf for euclid::Length<T, Unit> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.0.size_of(ops)
+ }
+}
+
+impl<T: MallocSizeOf, Src, Dst> MallocSizeOf for euclid::Scale<T, Src, Dst> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.0.size_of(ops)
+ }
+}
+
+impl<T: MallocSizeOf, U> MallocSizeOf for euclid::Point2D<T, U> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.x.size_of(ops) + self.y.size_of(ops)
+ }
+}
+
+impl<T: MallocSizeOf, U> MallocSizeOf for euclid::Rect<T, U> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.origin.size_of(ops) + self.size.size_of(ops)
+ }
+}
+
+impl<T: MallocSizeOf, U> MallocSizeOf for euclid::Box2D<T, U> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.min.size_of(ops) + self.max.size_of(ops)
+ }
+}
+
+impl<T: MallocSizeOf, U> MallocSizeOf for euclid::SideOffsets2D<T, U> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.top.size_of(ops) +
+ self.right.size_of(ops) +
+ self.bottom.size_of(ops) +
+ self.left.size_of(ops)
+ }
+}
+
+impl<T: MallocSizeOf, U> MallocSizeOf for euclid::Size2D<T, U> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.width.size_of(ops) + self.height.size_of(ops)
+ }
+}
+
+impl<T: MallocSizeOf, Src, Dst> MallocSizeOf for euclid::Transform2D<T, Src, Dst> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.m11.size_of(ops) +
+ self.m12.size_of(ops) +
+ self.m21.size_of(ops) +
+ self.m22.size_of(ops) +
+ self.m31.size_of(ops) +
+ self.m32.size_of(ops)
+ }
+}
+
+impl<T: MallocSizeOf, Src, Dst> MallocSizeOf for euclid::Transform3D<T, Src, Dst> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.m11.size_of(ops) +
+ self.m12.size_of(ops) +
+ self.m13.size_of(ops) +
+ self.m14.size_of(ops) +
+ self.m21.size_of(ops) +
+ self.m22.size_of(ops) +
+ self.m23.size_of(ops) +
+ self.m24.size_of(ops) +
+ self.m31.size_of(ops) +
+ self.m32.size_of(ops) +
+ self.m33.size_of(ops) +
+ self.m34.size_of(ops) +
+ self.m41.size_of(ops) +
+ self.m42.size_of(ops) +
+ self.m43.size_of(ops) +
+ self.m44.size_of(ops)
+ }
+}
+
+impl<T: MallocSizeOf, U> MallocSizeOf for euclid::Vector2D<T, U> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.x.size_of(ops) + self.y.size_of(ops)
+ }
+}
+
+/// For use on types where size_of() returns 0.
+#[macro_export]
+macro_rules! malloc_size_of_is_0(
+ ($($ty:ty),+) => (
+ $(
+ impl $crate::MallocSizeOf for $ty {
+ #[inline(always)]
+ fn size_of(&self, _: &mut $crate::MallocSizeOfOps) -> usize {
+ 0
+ }
+ }
+ )+
+ );
+ ($($ty:ident<$($gen:ident),+>),+) => (
+ $(
+ impl<$($gen: $crate::MallocSizeOf),+> $crate::MallocSizeOf for $ty<$($gen),+> {
+ #[inline(always)]
+ fn size_of(&self, _: &mut $crate::MallocSizeOfOps) -> usize {
+ 0
+ }
+ }
+ )+
+ );
+);
+
+malloc_size_of_is_0!(bool, char, str);
+malloc_size_of_is_0!(u8, u16, u32, u64, u128, usize);
+malloc_size_of_is_0!(i8, i16, i32, i64, i128, isize);
+malloc_size_of_is_0!(f32, f64);
+
+malloc_size_of_is_0!(std::sync::atomic::AtomicBool);
+malloc_size_of_is_0!(std::sync::atomic::AtomicIsize);
+malloc_size_of_is_0!(std::sync::atomic::AtomicUsize);
+
+malloc_size_of_is_0!(std::num::NonZeroUsize);
+malloc_size_of_is_0!(std::num::NonZeroU32);
+
+malloc_size_of_is_0!(std::time::Duration);
+malloc_size_of_is_0!(std::time::Instant);
+malloc_size_of_is_0!(std::time::SystemTime);
+
+malloc_size_of_is_0!(Range<u8>, Range<u16>, Range<u32>, Range<u64>, Range<usize>);
+malloc_size_of_is_0!(Range<i8>, Range<i16>, Range<i32>, Range<i64>, Range<isize>);
+malloc_size_of_is_0!(Range<f32>, Range<f64>);
+
+malloc_size_of_is_0!(app_units::Au);
diff --git a/gfx/wr/wrench/.gitignore b/gfx/wr/wrench/.gitignore
new file mode 100644
index 0000000000..341d13d0f7
--- /dev/null
+++ b/gfx/wr/wrench/.gitignore
@@ -0,0 +1,7 @@
+Cargo.lock
+target/
+*#
+*~
+yaml_frames/
+json_frames/
+bin_frames/
diff --git a/gfx/wr/wrench/Cargo.toml b/gfx/wr/wrench/Cargo.toml
new file mode 100644
index 0000000000..1c434b7894
--- /dev/null
+++ b/gfx/wr/wrench/Cargo.toml
@@ -0,0 +1,76 @@
+[package]
+name = "wrench"
+version = "0.3.0"
+authors = ["Vladimir Vukicevic <vladimir@pobox.com>"]
+build = "build.rs"
+license = "MPL-2.0"
+edition = "2018"
+
+# Required by cargo-apk to build for Android
+[lib]
+crate-type = ["lib", "cdylib"]
+path = "src/main.rs"
+
+[dependencies]
+base64 = "0.13"
+env_logger = { version = "0.10", optional = true, default_features = false }
+gleam = "0.15"
+glutin = "0.28"
+clap = { version = "3.1", features = ["yaml"] }
+glsl-lang = { version = "0.2", features = ["lexer-v2-full"] }
+log = "0.4"
+yaml-rust = "0.4"
+serde_json = "1.0"
+time = "0.1"
+chrono = "0.4"
+crossbeam = "0.2"
+osmesa-sys = { version = "0.1.2", optional = true }
+osmesa-src = { version = "0.2", git = "https://github.com/servo/osmesa-src", optional = true }
+webrender = { path = "../webrender", features = ["capture", "replay", "png", "profiler", "dynamic_freetype", "leak_checks"] }
+webrender_build = { path = "../webrender_build" }
+winit = "0.26"
+serde = { version = "1.0", features = ["derive"] }
+semver = "1.0.12"
+swgl = { path = "../swgl", optional = true }
+tracy-rs = "0.1.2"
+
+[dependencies.image]
+version = "0.23"
+default-features = false
+features = ["png"]
+
+[target.'cfg(target_os = "macos")'.dependencies]
+core-graphics = "0.22"
+core-foundation = "0.9"
+
+[features]
+default = [ "env_logger" ]
+headless = [ "osmesa-sys", "osmesa-src" ]
+software = [ "swgl" ]
+
+[target.'cfg(target_os = "windows")'.dependencies]
+dwrote = "0.11"
+mozangle = { version = "0.3.2", features = ["egl"] }
+
+[target.'cfg(target_os = "android")'.dependencies]
+libc = "0.2"
+ndk-glue = "0.5"
+
+[target.'cfg(all(unix, not(target_os = "android")))'.dependencies]
+font-loader = "0.11"
+
+# Configuration information used when building wrench as an APK.
+[package.metadata.android]
+package = "org.mozilla.wrench"
+build_targets = [ "armv7-linux-androideabi", "i686-linux-android" ]
+opengles_version_major = 3
+opengles_version_minor = 0
+
+[package.metadata.android.sdk]
+# keep it in sync with android-sdk-version in android-sdk.configure
+target_sdk_version = 33
+min_sdk_version = 18
+
+[package.metadata.android.application]
+label = "Wrench"
+debuggable = true
diff --git a/gfx/wr/wrench/README.md b/gfx/wr/wrench/README.md
new file mode 100644
index 0000000000..6a60c6bb56
--- /dev/null
+++ b/gfx/wr/wrench/README.md
@@ -0,0 +1,26 @@
+# wrench
+
+`wrench` is a tool for debugging webrender outside of a browser engine.
+
+## Build
+
+Build `wrench` with `cargo build --release` within the `wrench` directory.
+
+## headless
+
+`wrench` has an optional headless mode for use in continuous integration. To run in headless mode, instead of using `cargo run -- args`, use `./headless.py args`.
+
+## `show`
+
+If you are working on gecko integration you can capture a frame via the following steps.
+* Visit about:support and check that the "Compositing" value in the "Graphics" table says "WebRender". Enable `gfx.webrender.all` in about:config if necessary to enable WebRender.
+* Hit ctrl-shift-3 to capture the frame. The data will be put in `~/wr-capture`.
+* View the capture with `wrench show ~/wr-capture`.
+
+## `reftest`
+
+Wrench also has a reftest system for catching regressions.
+* To run all reftests, run `script/headless.py reftest`
+* To run specific reftests, run `script/headless.py reftest path/to/test/or/dir`
+* To examine test failures, use the [reftest analyzer](https://hg.mozilla.org/mozilla-central/raw-file/tip/layout/tools/reftest/reftest-analyzer.xhtml)
+* To add a new reftest, create an example frame and a reference frame in `reftests/` and then add an entry to `reftests/reftest.list`
diff --git a/gfx/wr/wrench/android.txt b/gfx/wr/wrench/android.txt
new file mode 100644
index 0000000000..27563a2213
--- /dev/null
+++ b/gfx/wr/wrench/android.txt
@@ -0,0 +1,100 @@
+Running Wrench on Android devices.
+==================================
+
+Setting up the environment:
+---------------------------
+
+Follow the steps at https://github.com/rust-windowing/android-rs-glue#setting-up-your-environment, with exceptions:
+ - No need to download the Android NDK and SDK, we will use the ones downloaded by Gecko in ~/.mozbuild/
+
+ - Install both the i686-linux-android and armv7-linux-androideabi rust
+ targets, as the APK will include native libraries with both architectures.
+
+ - Don't install currently published version of cargo-apk, as it is missing the following
+ required patch: https://github.com/rust-windowing/android-ndk-rs/pull/236
+
+ Instead, install the git master version like so:
+ cargo install --git https://github.com/rust-windowing/android-ndk-rs cargo-apk
+
+ - Consider adding ~/.mozbuild/android-sdk-linux/platform-tools to your path, for the adb commands below.
+
+Compiling and running:
+----------------------
+
+ Compile wrench:
+ cd wrench
+ export ANDROID_SDK_ROOT=$HOME/.mozbuild/android-sdk-linux # exact path may vary
+ export ANDROID_NDK_ROOT=$HOME/.mozbuild/android-ndk-r21d # exact path may vary
+ cargo apk build --lib
+
+ Install the APK:
+ adb install -r ../target/debug/apk/wrench.apk
+
+ Set command line arguments and env vars for wrench:
+ adb shell
+ mkdir /data/data/org.mozilla.wrench/files/wrench
+ echo "load reftests/aa/rounded-rects.yaml" >/data/data/org.mozilla.wrench/files/wrench/args
+ echo "env: WRENCH_REFTEST_CONDITION_EMULATOR=1" >>/data/data/org.mozilla.wrench/files/wrench/args # If you're using the emulator
+ echo "env: WRENCH_REFTEST_CONDITION_DEVICE=1" >>/data/data/org.mozilla.wrench/files/wrench/args # If you're using a device
+ exit
+
+ Push reftests (if you need these files for what you're doing):
+ adb push reftests /data/data/org.mozilla.wrench/files/wrench/
+
+ Run the application:
+ adb shell am start -n org.mozilla.wrench/android.app.NativeActivity
+ (or click the icon in the launcher)
+
+ Inspect the output:
+ stdout and stderr will be redirected to the logcat, however, long lines will be truncated
+ meaning the reftest analyzer will not work correctly. We therefore additionally redirect the
+ output to the file /data/data/org.mozilla.wrench/files/wrench/stdout.
+
+Release mode:
+-------------
+
+ Building in release does work as well. Use the following steps to compile wrench:
+ cd wrench
+ export ANDROID_SDK_ROOT=$HOME/.mozbuild/android-sdk-linux # exact path may vary
+ export ANDROID_NDK_ROOT=$HOME/.mozbuild/android-ndk-r21d # exact path may vary
+ cargo apk build --lib --release
+
+ Now the APK at ../target/release/apk/wrench.apk
+ should be signed and installable (you may need to uninstall the debug APK first if you
+ have that installed).
+
+Running reftests like a boss (on a local emulator):
+---------------------------------------------------
+
+ First, compile wrench as described above (debug mode).
+ Then, from the root gecko source dir, run:
+ ./mach python testing/mozharness/scripts/android_wrench.py --config testing/mozharness/configs/android/wrench.py
+ This will automatically do the following:
+ - Download the blessed android AVDs from taskcluster
+ - Start the emulator (using your ~/.mozbuild/android-sdk-linux emulator binaries)
+ - Install the debug APK (from gfx/wr/wrench/target/debug/apk/wrench.apk)
+ - Copy the reftests to the sdcard
+ - Write an args file to the sdcard
+ - Run wrench
+ - Wait for wrench to finish running
+ - Scrape the logcat for reftest output
+ Other logs (e.g. full logcat) can be found in your ~/.wrench/logs folder. Note that
+ this will also leave the android emulator running, so repeating the command will be
+ even faster the next time around as it won't need to redownload the AVDs or restart
+ the emulator. It will reinstall the APK and re-push the reftests folder though.
+
+ If you want to use a release APK (runs much faster), build it as per the "Release mode"
+ instructions above and set the WRENCH_APK env var to point to the APK:
+ to point to it:
+ export WRENCH_APK=gfx/wr/target/release/apk/wrench.apk
+ ./mach python testing/mozharness/scripts/android_wrench.py --config testing/mozharness/configs/android/wrench.py
+
+Running reftests like a boss (on a local device):
+-------------------------------------------------
+
+ Same steps as running on a local emulator, except you need to do this:
+ export DEVICE_SERIAL=<your device's serial>
+ before running the `./mach python` command. You can get the serial of
+ your device by running `adb devices` with the device plugged in. When running
+ on a device, the android_emulator_wrench.py script will skip the steps to
+ download the AVDs and start the emulator.
diff --git a/gfx/wr/wrench/benchmarks/aligned-gradient.yaml b/gfx/wr/wrench/benchmarks/aligned-gradient.yaml
new file mode 100644
index 0000000000..b0e2f0a645
--- /dev/null
+++ b/gfx/wr/wrench/benchmarks/aligned-gradient.yaml
@@ -0,0 +1,62 @@
+root:
+ items:
+ - type: gradient
+ bounds: [ 0, 0, 1980, 1080]
+ start: [ 0, -2000 ]
+ end: [ 0, 4000 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: gradient
+ bounds: [ 0, 0, 1980, 1080]
+ start: [ 0, -2000 ]
+ end: [ 0, 4000 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: gradient
+ bounds: [ 0, 0, 1980, 1080]
+ start: [ 0, -2000 ]
+ end: [ 0, 4000 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: gradient
+ bounds: [ 0, 0, 1980, 1080]
+ start: [ 0, -2000 ]
+ end: [ 0, 4000 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: gradient
+ bounds: [ 0, 0, 1980, 1080]
+ start: [ 0, -2000 ]
+ end: [ 0, 4000 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: gradient
+ bounds: [ 0, 0, 1980, 1080]
+ start: [ 0, -2000 ]
+ end: [ 0, 4000 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: gradient
+ bounds: [ 0, 0, 1980, 1080]
+ start: [ 0, -2000 ]
+ end: [ 0, 4000 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: gradient
+ bounds: [ 0, 0, 1980, 1080]
+ start: [ 0, -2000 ]
+ end: [ 0, 4000 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: gradient
+ bounds: [ 0, 0, 1980, 1080]
+ start: [ 0, -2000 ]
+ end: [ 0, 4000 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: gradient
+ bounds: [ 0, 0, 1980, 1080]
+ start: [ 0, -2000 ]
+ end: [ 0, 4000 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
diff --git a/gfx/wr/wrench/benchmarks/benchmarks.list b/gfx/wr/wrench/benchmarks/benchmarks.list
new file mode 100644
index 0000000000..c7235dcfd2
--- /dev/null
+++ b/gfx/wr/wrench/benchmarks/benchmarks.list
@@ -0,0 +1,11 @@
+aligned-gradient.yaml
+unaligned-gradient.yaml
+simple-batching.yaml
+large-boxshadow-ellipse.yaml
+large-boxshadow-ellipse-2.yaml
+large-clip-rect.yaml
+transforms-simple.yaml
+text-rendering.yaml
+many-images.yaml
+large-blur-radius.yaml
+
diff --git a/gfx/wr/wrench/benchmarks/box-shadow-large.yaml b/gfx/wr/wrench/benchmarks/box-shadow-large.yaml
new file mode 100644
index 0000000000..83c8ce5326
--- /dev/null
+++ b/gfx/wr/wrench/benchmarks/box-shadow-large.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: box-shadow
+ bounds: [ 100, 100, 800, 800 ]
+ blur-radius: 20
+ border-radius: {
+ top-left: [20, 20],
+ top-right: [10, 10],
+ bottom-left: [25, 25],
+ bottom-right: [100, 100],
+ }
+ color: blue
+ clip-mode: outset
diff --git a/gfx/wr/wrench/benchmarks/clip-clear.yaml b/gfx/wr/wrench/benchmarks/clip-clear.yaml
new file mode 100644
index 0000000000..f23440a3f1
--- /dev/null
+++ b/gfx/wr/wrench/benchmarks/clip-clear.yaml
@@ -0,0 +1,48 @@
+# Benchmark to test the cost of clears on a clip mask target that
+# is large but has low usage.
+---
+root:
+ items:
+ -
+ bounds: 0 0 1000 1000
+ type: stacking-context
+ items:
+ - type: clip
+ bounds: [50, 50, 300, 300]
+ complex:
+ - rect: [50, 50, 300, 300]
+ radius: 50
+ items:
+ - type: rect
+ bounds: 50 50 300 300
+ color: red
+ - type: rect
+ bounds: 50 50 300 300
+ color: red
+ - type: rect
+ bounds: 50 50 300 300
+ color: red
+ - type: rect
+ bounds: 50 50 300 300
+ color: red
+ - type: rect
+ bounds: 50 50 300 300
+ color: red
+ - type: rect
+ bounds: 50 50 300 300
+ color: red
+ - type: rect
+ bounds: 50 50 300 300
+ color: red
+ - type: rect
+ bounds: 50 50 300 300
+ color: red
+ - type: rect
+ bounds: 50 50 300 300
+ color: red
+ - type: rect
+ bounds: 50 50 300 300
+ color: red
+ - type: rect
+ bounds: 50 50 300 300
+ color: red
diff --git a/gfx/wr/wrench/benchmarks/large-blur-radius.yaml b/gfx/wr/wrench/benchmarks/large-blur-radius.yaml
new file mode 100644
index 0000000000..55cfec7643
--- /dev/null
+++ b/gfx/wr/wrench/benchmarks/large-blur-radius.yaml
@@ -0,0 +1,10 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: 100 100 1024 1024
+ filters: blur(100, 100)
+ items:
+ - type: rect
+ bounds: 0 0 1024 1024
+ color: red
diff --git a/gfx/wr/wrench/benchmarks/large-boxshadow-ellipse-2.yaml b/gfx/wr/wrench/benchmarks/large-boxshadow-ellipse-2.yaml
new file mode 100644
index 0000000000..bcb57434ec
--- /dev/null
+++ b/gfx/wr/wrench/benchmarks/large-boxshadow-ellipse-2.yaml
@@ -0,0 +1,15 @@
+---
+root:
+ items:
+ - type: box-shadow
+ bounds: [ 0, 0, 1024, 1024 ]
+ color: green
+ clip-mode: inset
+ blur-radius: 10000
+ border-radius: {
+ top-left: [500, 700],
+ top-right: [500, 700],
+ bottom-left: [600, 400],
+ bottom-right: [600, 400],
+ }
+
diff --git a/gfx/wr/wrench/benchmarks/large-boxshadow-ellipse.yaml b/gfx/wr/wrench/benchmarks/large-boxshadow-ellipse.yaml
new file mode 100644
index 0000000000..a9670f78fa
--- /dev/null
+++ b/gfx/wr/wrench/benchmarks/large-boxshadow-ellipse.yaml
@@ -0,0 +1,15 @@
+---
+root:
+ items:
+ - type: box-shadow
+ bounds: [ 0, 0, 1024, 1024 ]
+ color: green
+ clip-mode: outset
+ blur-radius: 10
+ border-radius: {
+ top-left: [10, 30],
+ top-right: [10, 30],
+ bottom-left: [30, 10],
+ bottom-right: [30, 10],
+ }
+
diff --git a/gfx/wr/wrench/benchmarks/large-clip-rect.yaml b/gfx/wr/wrench/benchmarks/large-clip-rect.yaml
new file mode 100644
index 0000000000..7bd8f1f6a5
--- /dev/null
+++ b/gfx/wr/wrench/benchmarks/large-clip-rect.yaml
@@ -0,0 +1,33 @@
+---
+root:
+ items:
+ - type: clip
+ bounds: [0, 0, 1024, 1024]
+ complex:
+ - rect: [0, 0, 1024, 1024]
+ radius: 16
+ items:
+ - type: rect
+ bounds: [0, 0, 1024, 1024]
+ color: red
+ - type: rect
+ bounds: [0, 0, 1024, 1024]
+ color: red
+ - type: rect
+ bounds: [0, 0, 1024, 1024]
+ color: red
+ - type: rect
+ bounds: [0, 0, 1024, 1024]
+ color: red
+ - type: rect
+ bounds: [0, 0, 1024, 1024]
+ color: red
+ - type: rect
+ bounds: [0, 0, 1024, 1024]
+ color: red
+ - type: rect
+ bounds: [0, 0, 1024, 1024]
+ color: red
+ - type: rect
+ bounds: [0, 0, 1024, 1024]
+ color: red
diff --git a/gfx/wr/wrench/benchmarks/many-box-shadows.yaml b/gfx/wr/wrench/benchmarks/many-box-shadows.yaml
new file mode 100644
index 0000000000..e016f8e0c3
--- /dev/null
+++ b/gfx/wr/wrench/benchmarks/many-box-shadows.yaml
@@ -0,0 +1,115 @@
+---
+root:
+ items:
+ -
+ type: "stacking-context"
+ items:
+ -
+ bounds: [68, 431, 539, 805]
+ "clip-rect": [5, 390, 665, 932]
+ "backface-visible": true
+ type: "box-shadow"
+ "box-bounds": [68, 431, 539, 805]
+ offset: [0, 22.5]
+ color: 0 0 0 0.1020
+ "blur-radius": 45
+ "spread-radius": 0
+ "clip-mode": outset
+ -
+ bounds: [668, 431, 539, 805]
+ "clip-rect": [605, 390, 665, 932]
+ "backface-visible": true
+ type: "box-shadow"
+ "box-bounds": [668, 431, 539, 805]
+ offset: [0, 22.5]
+ color: 0 0 0 0.1020
+ "blur-radius": 45
+ "spread-radius": 0
+ "clip-mode": outset
+ -
+ bounds: [1268, 431, 540, 805]
+ "clip-rect": [1205, 390, 666, 932]
+ "backface-visible": true
+ type: "box-shadow"
+ "box-bounds": [1268, 431, 540, 805]
+ offset: [0, 22.5]
+ color: 0 0 0 0.1020
+ "blur-radius": 45
+ "spread-radius": 0
+ "clip-mode": outset
+ -
+ bounds: [68, 1284, 539, 771]
+ "clip-rect": [5, 1244, 665, 897]
+ "backface-visible": true
+ type: "box-shadow"
+ "box-bounds": [68, 1284, 539, 771]
+ offset: [0, 22.5]
+ color: 0 0 0 0.1020
+ "blur-radius": 45
+ "spread-radius": 0
+ "clip-mode": outset
+ -
+ bounds: [668, 1284, 539, 771]
+ "clip-rect": [605, 1244, 665, 897]
+ "backface-visible": true
+ type: "box-shadow"
+ "box-bounds": [668, 1284, 539, 771]
+ offset: [0, 22.5]
+ color: 0 0 0 0.1020
+ "blur-radius": 45
+ "spread-radius": 0
+ "clip-mode": outset
+ -
+ bounds: [1268, 1284, 540, 771]
+ "clip-rect": [1205, 1244, 666, 897]
+ "backface-visible": true
+ type: "box-shadow"
+ "box-bounds": [1268, 1284, 540, 771]
+ offset: [0, 22.5]
+ color: 0 0 0 0.1020
+ "blur-radius": 45
+ "spread-radius": 0
+ "clip-mode": outset
+ -
+ bounds: [68, 2103, 539, 839]
+ "clip-rect": [5, 2063, 665, 965]
+ "backface-visible": true
+ type: "box-shadow"
+ "box-bounds": [68, 2103, 539, 839]
+ offset: [0, 22.5]
+ color: 0 0 0 0.1020
+ "blur-radius": 45
+ "spread-radius": 0
+ "clip-mode": outset
+ -
+ bounds: [668, 2103, 539, 839]
+ "clip-rect": [605, 2063, 665, 965]
+ "backface-visible": true
+ type: "box-shadow"
+ "box-bounds": [668, 2103, 539, 839]
+ offset: [0, 22.5]
+ color: 0 0 0 0.1020
+ "blur-radius": 45
+ "spread-radius": 0
+ "clip-mode": outset
+ -
+ bounds: [1268, 2103, 540, 839]
+ "clip-rect": [1205, 2063, 666, 965]
+ "backface-visible": true
+ type: "box-shadow"
+ "box-bounds": [1268, 2103, 540, 839]
+ offset: [0, 22.5]
+ color: 0 0 0 0.1020
+ "blur-radius": 45
+ "spread-radius": 0
+ "clip-mode": outset
+ -
+ bounds: [0, 0, 1875, 154]
+ "clip-rect": [-2, 0, 1879, 158]
+ type: "box-shadow"
+ "box-bounds": [0, 0, 1875, 154]
+ offset: [0, 1.5]
+ color: 0 0 0 0.4000
+ "blur-radius": 1.5
+ "spread-radius": 0
+ "clip-mode": outset
diff --git a/gfx/wr/wrench/benchmarks/many-images.yaml b/gfx/wr/wrench/benchmarks/many-images.yaml
new file mode 100644
index 0000000000..963fdef881
--- /dev/null
+++ b/gfx/wr/wrench/benchmarks/many-images.yaml
@@ -0,0 +1,16386 @@
+root:
+ items:
+ - image: solid-color(0, 0, 0, 255, 8, 8)
+ bounds: 0 0 8 8
+ - image: solid-color(1, 0, 0, 255, 8, 8)
+ bounds: 8 0 8 8
+ - image: solid-color(2, 0, 0, 255, 8, 8)
+ bounds: 16 0 8 8
+ - image: solid-color(3, 0, 0, 255, 8, 8)
+ bounds: 24 0 8 8
+ - image: solid-color(4, 0, 0, 255, 8, 8)
+ bounds: 32 0 8 8
+ - image: solid-color(5, 0, 0, 255, 8, 8)
+ bounds: 40 0 8 8
+ - image: solid-color(6, 0, 0, 255, 8, 8)
+ bounds: 48 0 8 8
+ - image: solid-color(7, 0, 0, 255, 8, 8)
+ bounds: 56 0 8 8
+ - image: solid-color(8, 0, 0, 255, 8, 8)
+ bounds: 64 0 8 8
+ - image: solid-color(9, 0, 0, 255, 8, 8)
+ bounds: 72 0 8 8
+ - image: solid-color(10, 0, 0, 255, 8, 8)
+ bounds: 80 0 8 8
+ - image: solid-color(11, 0, 0, 255, 8, 8)
+ bounds: 88 0 8 8
+ - image: solid-color(12, 0, 0, 255, 8, 8)
+ bounds: 96 0 8 8
+ - image: solid-color(13, 0, 0, 255, 8, 8)
+ bounds: 104 0 8 8
+ - image: solid-color(14, 0, 0, 255, 8, 8)
+ bounds: 112 0 8 8
+ - image: solid-color(15, 0, 0, 255, 8, 8)
+ bounds: 120 0 8 8
+ - image: solid-color(16, 0, 0, 255, 8, 8)
+ bounds: 128 0 8 8
+ - image: solid-color(17, 0, 0, 255, 8, 8)
+ bounds: 136 0 8 8
+ - image: solid-color(18, 0, 0, 255, 8, 8)
+ bounds: 144 0 8 8
+ - image: solid-color(19, 0, 0, 255, 8, 8)
+ bounds: 152 0 8 8
+ - image: solid-color(20, 0, 0, 255, 8, 8)
+ bounds: 160 0 8 8
+ - image: solid-color(21, 0, 0, 255, 8, 8)
+ bounds: 168 0 8 8
+ - image: solid-color(22, 0, 0, 255, 8, 8)
+ bounds: 176 0 8 8
+ - image: solid-color(23, 0, 0, 255, 8, 8)
+ bounds: 184 0 8 8
+ - image: solid-color(24, 0, 0, 255, 8, 8)
+ bounds: 192 0 8 8
+ - image: solid-color(25, 0, 0, 255, 8, 8)
+ bounds: 200 0 8 8
+ - image: solid-color(26, 0, 0, 255, 8, 8)
+ bounds: 208 0 8 8
+ - image: solid-color(27, 0, 0, 255, 8, 8)
+ bounds: 216 0 8 8
+ - image: solid-color(28, 0, 0, 255, 8, 8)
+ bounds: 224 0 8 8
+ - image: solid-color(29, 0, 0, 255, 8, 8)
+ bounds: 232 0 8 8
+ - image: solid-color(30, 0, 0, 255, 8, 8)
+ bounds: 240 0 8 8
+ - image: solid-color(31, 0, 0, 255, 8, 8)
+ bounds: 248 0 8 8
+ - image: solid-color(32, 0, 0, 255, 8, 8)
+ bounds: 256 0 8 8
+ - image: solid-color(33, 0, 0, 255, 8, 8)
+ bounds: 264 0 8 8
+ - image: solid-color(34, 0, 0, 255, 8, 8)
+ bounds: 272 0 8 8
+ - image: solid-color(35, 0, 0, 255, 8, 8)
+ bounds: 280 0 8 8
+ - image: solid-color(36, 0, 0, 255, 8, 8)
+ bounds: 288 0 8 8
+ - image: solid-color(37, 0, 0, 255, 8, 8)
+ bounds: 296 0 8 8
+ - image: solid-color(38, 0, 0, 255, 8, 8)
+ bounds: 304 0 8 8
+ - image: solid-color(39, 0, 0, 255, 8, 8)
+ bounds: 312 0 8 8
+ - image: solid-color(40, 0, 0, 255, 8, 8)
+ bounds: 320 0 8 8
+ - image: solid-color(41, 0, 0, 255, 8, 8)
+ bounds: 328 0 8 8
+ - image: solid-color(42, 0, 0, 255, 8, 8)
+ bounds: 336 0 8 8
+ - image: solid-color(43, 0, 0, 255, 8, 8)
+ bounds: 344 0 8 8
+ - image: solid-color(44, 0, 0, 255, 8, 8)
+ bounds: 352 0 8 8
+ - image: solid-color(45, 0, 0, 255, 8, 8)
+ bounds: 360 0 8 8
+ - image: solid-color(46, 0, 0, 255, 8, 8)
+ bounds: 368 0 8 8
+ - image: solid-color(47, 0, 0, 255, 8, 8)
+ bounds: 376 0 8 8
+ - image: solid-color(48, 0, 0, 255, 8, 8)
+ bounds: 384 0 8 8
+ - image: solid-color(49, 0, 0, 255, 8, 8)
+ bounds: 392 0 8 8
+ - image: solid-color(50, 0, 0, 255, 8, 8)
+ bounds: 400 0 8 8
+ - image: solid-color(51, 0, 0, 255, 8, 8)
+ bounds: 408 0 8 8
+ - image: solid-color(52, 0, 0, 255, 8, 8)
+ bounds: 416 0 8 8
+ - image: solid-color(53, 0, 0, 255, 8, 8)
+ bounds: 424 0 8 8
+ - image: solid-color(54, 0, 0, 255, 8, 8)
+ bounds: 432 0 8 8
+ - image: solid-color(55, 0, 0, 255, 8, 8)
+ bounds: 440 0 8 8
+ - image: solid-color(56, 0, 0, 255, 8, 8)
+ bounds: 448 0 8 8
+ - image: solid-color(57, 0, 0, 255, 8, 8)
+ bounds: 456 0 8 8
+ - image: solid-color(58, 0, 0, 255, 8, 8)
+ bounds: 464 0 8 8
+ - image: solid-color(59, 0, 0, 255, 8, 8)
+ bounds: 472 0 8 8
+ - image: solid-color(60, 0, 0, 255, 8, 8)
+ bounds: 480 0 8 8
+ - image: solid-color(61, 0, 0, 255, 8, 8)
+ bounds: 488 0 8 8
+ - image: solid-color(62, 0, 0, 255, 8, 8)
+ bounds: 496 0 8 8
+ - image: solid-color(63, 0, 0, 255, 8, 8)
+ bounds: 504 0 8 8
+ - image: solid-color(64, 0, 0, 255, 8, 8)
+ bounds: 512 0 8 8
+ - image: solid-color(65, 0, 0, 255, 8, 8)
+ bounds: 520 0 8 8
+ - image: solid-color(66, 0, 0, 255, 8, 8)
+ bounds: 528 0 8 8
+ - image: solid-color(67, 0, 0, 255, 8, 8)
+ bounds: 536 0 8 8
+ - image: solid-color(68, 0, 0, 255, 8, 8)
+ bounds: 544 0 8 8
+ - image: solid-color(69, 0, 0, 255, 8, 8)
+ bounds: 552 0 8 8
+ - image: solid-color(70, 0, 0, 255, 8, 8)
+ bounds: 560 0 8 8
+ - image: solid-color(71, 0, 0, 255, 8, 8)
+ bounds: 568 0 8 8
+ - image: solid-color(72, 0, 0, 255, 8, 8)
+ bounds: 576 0 8 8
+ - image: solid-color(73, 0, 0, 255, 8, 8)
+ bounds: 584 0 8 8
+ - image: solid-color(74, 0, 0, 255, 8, 8)
+ bounds: 592 0 8 8
+ - image: solid-color(75, 0, 0, 255, 8, 8)
+ bounds: 600 0 8 8
+ - image: solid-color(76, 0, 0, 255, 8, 8)
+ bounds: 608 0 8 8
+ - image: solid-color(77, 0, 0, 255, 8, 8)
+ bounds: 616 0 8 8
+ - image: solid-color(78, 0, 0, 255, 8, 8)
+ bounds: 624 0 8 8
+ - image: solid-color(79, 0, 0, 255, 8, 8)
+ bounds: 632 0 8 8
+ - image: solid-color(80, 0, 0, 255, 8, 8)
+ bounds: 640 0 8 8
+ - image: solid-color(81, 0, 0, 255, 8, 8)
+ bounds: 648 0 8 8
+ - image: solid-color(82, 0, 0, 255, 8, 8)
+ bounds: 656 0 8 8
+ - image: solid-color(83, 0, 0, 255, 8, 8)
+ bounds: 664 0 8 8
+ - image: solid-color(84, 0, 0, 255, 8, 8)
+ bounds: 672 0 8 8
+ - image: solid-color(85, 0, 0, 255, 8, 8)
+ bounds: 680 0 8 8
+ - image: solid-color(86, 0, 0, 255, 8, 8)
+ bounds: 688 0 8 8
+ - image: solid-color(87, 0, 0, 255, 8, 8)
+ bounds: 696 0 8 8
+ - image: solid-color(88, 0, 0, 255, 8, 8)
+ bounds: 704 0 8 8
+ - image: solid-color(89, 0, 0, 255, 8, 8)
+ bounds: 712 0 8 8
+ - image: solid-color(90, 0, 0, 255, 8, 8)
+ bounds: 720 0 8 8
+ - image: solid-color(91, 0, 0, 255, 8, 8)
+ bounds: 728 0 8 8
+ - image: solid-color(92, 0, 0, 255, 8, 8)
+ bounds: 736 0 8 8
+ - image: solid-color(93, 0, 0, 255, 8, 8)
+ bounds: 744 0 8 8
+ - image: solid-color(94, 0, 0, 255, 8, 8)
+ bounds: 752 0 8 8
+ - image: solid-color(95, 0, 0, 255, 8, 8)
+ bounds: 760 0 8 8
+ - image: solid-color(96, 0, 0, 255, 8, 8)
+ bounds: 768 0 8 8
+ - image: solid-color(97, 0, 0, 255, 8, 8)
+ bounds: 776 0 8 8
+ - image: solid-color(98, 0, 0, 255, 8, 8)
+ bounds: 784 0 8 8
+ - image: solid-color(99, 0, 0, 255, 8, 8)
+ bounds: 792 0 8 8
+ - image: solid-color(100, 0, 0, 255, 8, 8)
+ bounds: 800 0 8 8
+ - image: solid-color(101, 0, 0, 255, 8, 8)
+ bounds: 808 0 8 8
+ - image: solid-color(102, 0, 0, 255, 8, 8)
+ bounds: 816 0 8 8
+ - image: solid-color(103, 0, 0, 255, 8, 8)
+ bounds: 824 0 8 8
+ - image: solid-color(104, 0, 0, 255, 8, 8)
+ bounds: 832 0 8 8
+ - image: solid-color(105, 0, 0, 255, 8, 8)
+ bounds: 840 0 8 8
+ - image: solid-color(106, 0, 0, 255, 8, 8)
+ bounds: 848 0 8 8
+ - image: solid-color(107, 0, 0, 255, 8, 8)
+ bounds: 856 0 8 8
+ - image: solid-color(108, 0, 0, 255, 8, 8)
+ bounds: 864 0 8 8
+ - image: solid-color(109, 0, 0, 255, 8, 8)
+ bounds: 872 0 8 8
+ - image: solid-color(110, 0, 0, 255, 8, 8)
+ bounds: 880 0 8 8
+ - image: solid-color(111, 0, 0, 255, 8, 8)
+ bounds: 888 0 8 8
+ - image: solid-color(112, 0, 0, 255, 8, 8)
+ bounds: 896 0 8 8
+ - image: solid-color(113, 0, 0, 255, 8, 8)
+ bounds: 904 0 8 8
+ - image: solid-color(114, 0, 0, 255, 8, 8)
+ bounds: 912 0 8 8
+ - image: solid-color(115, 0, 0, 255, 8, 8)
+ bounds: 920 0 8 8
+ - image: solid-color(116, 0, 0, 255, 8, 8)
+ bounds: 928 0 8 8
+ - image: solid-color(117, 0, 0, 255, 8, 8)
+ bounds: 936 0 8 8
+ - image: solid-color(118, 0, 0, 255, 8, 8)
+ bounds: 944 0 8 8
+ - image: solid-color(119, 0, 0, 255, 8, 8)
+ bounds: 952 0 8 8
+ - image: solid-color(120, 0, 0, 255, 8, 8)
+ bounds: 960 0 8 8
+ - image: solid-color(121, 0, 0, 255, 8, 8)
+ bounds: 968 0 8 8
+ - image: solid-color(122, 0, 0, 255, 8, 8)
+ bounds: 976 0 8 8
+ - image: solid-color(123, 0, 0, 255, 8, 8)
+ bounds: 984 0 8 8
+ - image: solid-color(124, 0, 0, 255, 8, 8)
+ bounds: 992 0 8 8
+ - image: solid-color(125, 0, 0, 255, 8, 8)
+ bounds: 1000 0 8 8
+ - image: solid-color(126, 0, 0, 255, 8, 8)
+ bounds: 1008 0 8 8
+ - image: solid-color(127, 0, 0, 255, 8, 8)
+ bounds: 1016 0 8 8
+ - image: solid-color(0, 1, 0, 255, 8, 8)
+ bounds: 0 8 8 8
+ - image: solid-color(1, 1, 0, 255, 8, 8)
+ bounds: 8 8 8 8
+ - image: solid-color(2, 1, 0, 255, 8, 8)
+ bounds: 16 8 8 8
+ - image: solid-color(3, 1, 0, 255, 8, 8)
+ bounds: 24 8 8 8
+ - image: solid-color(4, 1, 0, 255, 8, 8)
+ bounds: 32 8 8 8
+ - image: solid-color(5, 1, 0, 255, 8, 8)
+ bounds: 40 8 8 8
+ - image: solid-color(6, 1, 0, 255, 8, 8)
+ bounds: 48 8 8 8
+ - image: solid-color(7, 1, 0, 255, 8, 8)
+ bounds: 56 8 8 8
+ - image: solid-color(8, 1, 0, 255, 8, 8)
+ bounds: 64 8 8 8
+ - image: solid-color(9, 1, 0, 255, 8, 8)
+ bounds: 72 8 8 8
+ - image: solid-color(10, 1, 0, 255, 8, 8)
+ bounds: 80 8 8 8
+ - image: solid-color(11, 1, 0, 255, 8, 8)
+ bounds: 88 8 8 8
+ - image: solid-color(12, 1, 0, 255, 8, 8)
+ bounds: 96 8 8 8
+ - image: solid-color(13, 1, 0, 255, 8, 8)
+ bounds: 104 8 8 8
+ - image: solid-color(14, 1, 0, 255, 8, 8)
+ bounds: 112 8 8 8
+ - image: solid-color(15, 1, 0, 255, 8, 8)
+ bounds: 120 8 8 8
+ - image: solid-color(16, 1, 0, 255, 8, 8)
+ bounds: 128 8 8 8
+ - image: solid-color(17, 1, 0, 255, 8, 8)
+ bounds: 136 8 8 8
+ - image: solid-color(18, 1, 0, 255, 8, 8)
+ bounds: 144 8 8 8
+ - image: solid-color(19, 1, 0, 255, 8, 8)
+ bounds: 152 8 8 8
+ - image: solid-color(20, 1, 0, 255, 8, 8)
+ bounds: 160 8 8 8
+ - image: solid-color(21, 1, 0, 255, 8, 8)
+ bounds: 168 8 8 8
+ - image: solid-color(22, 1, 0, 255, 8, 8)
+ bounds: 176 8 8 8
+ - image: solid-color(23, 1, 0, 255, 8, 8)
+ bounds: 184 8 8 8
+ - image: solid-color(24, 1, 0, 255, 8, 8)
+ bounds: 192 8 8 8
+ - image: solid-color(25, 1, 0, 255, 8, 8)
+ bounds: 200 8 8 8
+ - image: solid-color(26, 1, 0, 255, 8, 8)
+ bounds: 208 8 8 8
+ - image: solid-color(27, 1, 0, 255, 8, 8)
+ bounds: 216 8 8 8
+ - image: solid-color(28, 1, 0, 255, 8, 8)
+ bounds: 224 8 8 8
+ - image: solid-color(29, 1, 0, 255, 8, 8)
+ bounds: 232 8 8 8
+ - image: solid-color(30, 1, 0, 255, 8, 8)
+ bounds: 240 8 8 8
+ - image: solid-color(31, 1, 0, 255, 8, 8)
+ bounds: 248 8 8 8
+ - image: solid-color(32, 1, 0, 255, 8, 8)
+ bounds: 256 8 8 8
+ - image: solid-color(33, 1, 0, 255, 8, 8)
+ bounds: 264 8 8 8
+ - image: solid-color(34, 1, 0, 255, 8, 8)
+ bounds: 272 8 8 8
+ - image: solid-color(35, 1, 0, 255, 8, 8)
+ bounds: 280 8 8 8
+ - image: solid-color(36, 1, 0, 255, 8, 8)
+ bounds: 288 8 8 8
+ - image: solid-color(37, 1, 0, 255, 8, 8)
+ bounds: 296 8 8 8
+ - image: solid-color(38, 1, 0, 255, 8, 8)
+ bounds: 304 8 8 8
+ - image: solid-color(39, 1, 0, 255, 8, 8)
+ bounds: 312 8 8 8
+ - image: solid-color(40, 1, 0, 255, 8, 8)
+ bounds: 320 8 8 8
+ - image: solid-color(41, 1, 0, 255, 8, 8)
+ bounds: 328 8 8 8
+ - image: solid-color(42, 1, 0, 255, 8, 8)
+ bounds: 336 8 8 8
+ - image: solid-color(43, 1, 0, 255, 8, 8)
+ bounds: 344 8 8 8
+ - image: solid-color(44, 1, 0, 255, 8, 8)
+ bounds: 352 8 8 8
+ - image: solid-color(45, 1, 0, 255, 8, 8)
+ bounds: 360 8 8 8
+ - image: solid-color(46, 1, 0, 255, 8, 8)
+ bounds: 368 8 8 8
+ - image: solid-color(47, 1, 0, 255, 8, 8)
+ bounds: 376 8 8 8
+ - image: solid-color(48, 1, 0, 255, 8, 8)
+ bounds: 384 8 8 8
+ - image: solid-color(49, 1, 0, 255, 8, 8)
+ bounds: 392 8 8 8
+ - image: solid-color(50, 1, 0, 255, 8, 8)
+ bounds: 400 8 8 8
+ - image: solid-color(51, 1, 0, 255, 8, 8)
+ bounds: 408 8 8 8
+ - image: solid-color(52, 1, 0, 255, 8, 8)
+ bounds: 416 8 8 8
+ - image: solid-color(53, 1, 0, 255, 8, 8)
+ bounds: 424 8 8 8
+ - image: solid-color(54, 1, 0, 255, 8, 8)
+ bounds: 432 8 8 8
+ - image: solid-color(55, 1, 0, 255, 8, 8)
+ bounds: 440 8 8 8
+ - image: solid-color(56, 1, 0, 255, 8, 8)
+ bounds: 448 8 8 8
+ - image: solid-color(57, 1, 0, 255, 8, 8)
+ bounds: 456 8 8 8
+ - image: solid-color(58, 1, 0, 255, 8, 8)
+ bounds: 464 8 8 8
+ - image: solid-color(59, 1, 0, 255, 8, 8)
+ bounds: 472 8 8 8
+ - image: solid-color(60, 1, 0, 255, 8, 8)
+ bounds: 480 8 8 8
+ - image: solid-color(61, 1, 0, 255, 8, 8)
+ bounds: 488 8 8 8
+ - image: solid-color(62, 1, 0, 255, 8, 8)
+ bounds: 496 8 8 8
+ - image: solid-color(63, 1, 0, 255, 8, 8)
+ bounds: 504 8 8 8
+ - image: solid-color(64, 1, 0, 255, 8, 8)
+ bounds: 512 8 8 8
+ - image: solid-color(65, 1, 0, 255, 8, 8)
+ bounds: 520 8 8 8
+ - image: solid-color(66, 1, 0, 255, 8, 8)
+ bounds: 528 8 8 8
+ - image: solid-color(67, 1, 0, 255, 8, 8)
+ bounds: 536 8 8 8
+ - image: solid-color(68, 1, 0, 255, 8, 8)
+ bounds: 544 8 8 8
+ - image: solid-color(69, 1, 0, 255, 8, 8)
+ bounds: 552 8 8 8
+ - image: solid-color(70, 1, 0, 255, 8, 8)
+ bounds: 560 8 8 8
+ - image: solid-color(71, 1, 0, 255, 8, 8)
+ bounds: 568 8 8 8
+ - image: solid-color(72, 1, 0, 255, 8, 8)
+ bounds: 576 8 8 8
+ - image: solid-color(73, 1, 0, 255, 8, 8)
+ bounds: 584 8 8 8
+ - image: solid-color(74, 1, 0, 255, 8, 8)
+ bounds: 592 8 8 8
+ - image: solid-color(75, 1, 0, 255, 8, 8)
+ bounds: 600 8 8 8
+ - image: solid-color(76, 1, 0, 255, 8, 8)
+ bounds: 608 8 8 8
+ - image: solid-color(77, 1, 0, 255, 8, 8)
+ bounds: 616 8 8 8
+ - image: solid-color(78, 1, 0, 255, 8, 8)
+ bounds: 624 8 8 8
+ - image: solid-color(79, 1, 0, 255, 8, 8)
+ bounds: 632 8 8 8
+ - image: solid-color(80, 1, 0, 255, 8, 8)
+ bounds: 640 8 8 8
+ - image: solid-color(81, 1, 0, 255, 8, 8)
+ bounds: 648 8 8 8
+ - image: solid-color(82, 1, 0, 255, 8, 8)
+ bounds: 656 8 8 8
+ - image: solid-color(83, 1, 0, 255, 8, 8)
+ bounds: 664 8 8 8
+ - image: solid-color(84, 1, 0, 255, 8, 8)
+ bounds: 672 8 8 8
+ - image: solid-color(85, 1, 0, 255, 8, 8)
+ bounds: 680 8 8 8
+ - image: solid-color(86, 1, 0, 255, 8, 8)
+ bounds: 688 8 8 8
+ - image: solid-color(87, 1, 0, 255, 8, 8)
+ bounds: 696 8 8 8
+ - image: solid-color(88, 1, 0, 255, 8, 8)
+ bounds: 704 8 8 8
+ - image: solid-color(89, 1, 0, 255, 8, 8)
+ bounds: 712 8 8 8
+ - image: solid-color(90, 1, 0, 255, 8, 8)
+ bounds: 720 8 8 8
+ - image: solid-color(91, 1, 0, 255, 8, 8)
+ bounds: 728 8 8 8
+ - image: solid-color(92, 1, 0, 255, 8, 8)
+ bounds: 736 8 8 8
+ - image: solid-color(93, 1, 0, 255, 8, 8)
+ bounds: 744 8 8 8
+ - image: solid-color(94, 1, 0, 255, 8, 8)
+ bounds: 752 8 8 8
+ - image: solid-color(95, 1, 0, 255, 8, 8)
+ bounds: 760 8 8 8
+ - image: solid-color(96, 1, 0, 255, 8, 8)
+ bounds: 768 8 8 8
+ - image: solid-color(97, 1, 0, 255, 8, 8)
+ bounds: 776 8 8 8
+ - image: solid-color(98, 1, 0, 255, 8, 8)
+ bounds: 784 8 8 8
+ - image: solid-color(99, 1, 0, 255, 8, 8)
+ bounds: 792 8 8 8
+ - image: solid-color(100, 1, 0, 255, 8, 8)
+ bounds: 800 8 8 8
+ - image: solid-color(101, 1, 0, 255, 8, 8)
+ bounds: 808 8 8 8
+ - image: solid-color(102, 1, 0, 255, 8, 8)
+ bounds: 816 8 8 8
+ - image: solid-color(103, 1, 0, 255, 8, 8)
+ bounds: 824 8 8 8
+ - image: solid-color(104, 1, 0, 255, 8, 8)
+ bounds: 832 8 8 8
+ - image: solid-color(105, 1, 0, 255, 8, 8)
+ bounds: 840 8 8 8
+ - image: solid-color(106, 1, 0, 255, 8, 8)
+ bounds: 848 8 8 8
+ - image: solid-color(107, 1, 0, 255, 8, 8)
+ bounds: 856 8 8 8
+ - image: solid-color(108, 1, 0, 255, 8, 8)
+ bounds: 864 8 8 8
+ - image: solid-color(109, 1, 0, 255, 8, 8)
+ bounds: 872 8 8 8
+ - image: solid-color(110, 1, 0, 255, 8, 8)
+ bounds: 880 8 8 8
+ - image: solid-color(111, 1, 0, 255, 8, 8)
+ bounds: 888 8 8 8
+ - image: solid-color(112, 1, 0, 255, 8, 8)
+ bounds: 896 8 8 8
+ - image: solid-color(113, 1, 0, 255, 8, 8)
+ bounds: 904 8 8 8
+ - image: solid-color(114, 1, 0, 255, 8, 8)
+ bounds: 912 8 8 8
+ - image: solid-color(115, 1, 0, 255, 8, 8)
+ bounds: 920 8 8 8
+ - image: solid-color(116, 1, 0, 255, 8, 8)
+ bounds: 928 8 8 8
+ - image: solid-color(117, 1, 0, 255, 8, 8)
+ bounds: 936 8 8 8
+ - image: solid-color(118, 1, 0, 255, 8, 8)
+ bounds: 944 8 8 8
+ - image: solid-color(119, 1, 0, 255, 8, 8)
+ bounds: 952 8 8 8
+ - image: solid-color(120, 1, 0, 255, 8, 8)
+ bounds: 960 8 8 8
+ - image: solid-color(121, 1, 0, 255, 8, 8)
+ bounds: 968 8 8 8
+ - image: solid-color(122, 1, 0, 255, 8, 8)
+ bounds: 976 8 8 8
+ - image: solid-color(123, 1, 0, 255, 8, 8)
+ bounds: 984 8 8 8
+ - image: solid-color(124, 1, 0, 255, 8, 8)
+ bounds: 992 8 8 8
+ - image: solid-color(125, 1, 0, 255, 8, 8)
+ bounds: 1000 8 8 8
+ - image: solid-color(126, 1, 0, 255, 8, 8)
+ bounds: 1008 8 8 8
+ - image: solid-color(127, 1, 0, 255, 8, 8)
+ bounds: 1016 8 8 8
+ - image: solid-color(0, 2, 0, 255, 8, 8)
+ bounds: 0 16 8 8
+ - image: solid-color(1, 2, 0, 255, 8, 8)
+ bounds: 8 16 8 8
+ - image: solid-color(2, 2, 0, 255, 8, 8)
+ bounds: 16 16 8 8
+ - image: solid-color(3, 2, 0, 255, 8, 8)
+ bounds: 24 16 8 8
+ - image: solid-color(4, 2, 0, 255, 8, 8)
+ bounds: 32 16 8 8
+ - image: solid-color(5, 2, 0, 255, 8, 8)
+ bounds: 40 16 8 8
+ - image: solid-color(6, 2, 0, 255, 8, 8)
+ bounds: 48 16 8 8
+ - image: solid-color(7, 2, 0, 255, 8, 8)
+ bounds: 56 16 8 8
+ - image: solid-color(8, 2, 0, 255, 8, 8)
+ bounds: 64 16 8 8
+ - image: solid-color(9, 2, 0, 255, 8, 8)
+ bounds: 72 16 8 8
+ - image: solid-color(10, 2, 0, 255, 8, 8)
+ bounds: 80 16 8 8
+ - image: solid-color(11, 2, 0, 255, 8, 8)
+ bounds: 88 16 8 8
+ - image: solid-color(12, 2, 0, 255, 8, 8)
+ bounds: 96 16 8 8
+ - image: solid-color(13, 2, 0, 255, 8, 8)
+ bounds: 104 16 8 8
+ - image: solid-color(14, 2, 0, 255, 8, 8)
+ bounds: 112 16 8 8
+ - image: solid-color(15, 2, 0, 255, 8, 8)
+ bounds: 120 16 8 8
+ - image: solid-color(16, 2, 0, 255, 8, 8)
+ bounds: 128 16 8 8
+ - image: solid-color(17, 2, 0, 255, 8, 8)
+ bounds: 136 16 8 8
+ - image: solid-color(18, 2, 0, 255, 8, 8)
+ bounds: 144 16 8 8
+ - image: solid-color(19, 2, 0, 255, 8, 8)
+ bounds: 152 16 8 8
+ - image: solid-color(20, 2, 0, 255, 8, 8)
+ bounds: 160 16 8 8
+ - image: solid-color(21, 2, 0, 255, 8, 8)
+ bounds: 168 16 8 8
+ - image: solid-color(22, 2, 0, 255, 8, 8)
+ bounds: 176 16 8 8
+ - image: solid-color(23, 2, 0, 255, 8, 8)
+ bounds: 184 16 8 8
+ - image: solid-color(24, 2, 0, 255, 8, 8)
+ bounds: 192 16 8 8
+ - image: solid-color(25, 2, 0, 255, 8, 8)
+ bounds: 200 16 8 8
+ - image: solid-color(26, 2, 0, 255, 8, 8)
+ bounds: 208 16 8 8
+ - image: solid-color(27, 2, 0, 255, 8, 8)
+ bounds: 216 16 8 8
+ - image: solid-color(28, 2, 0, 255, 8, 8)
+ bounds: 224 16 8 8
+ - image: solid-color(29, 2, 0, 255, 8, 8)
+ bounds: 232 16 8 8
+ - image: solid-color(30, 2, 0, 255, 8, 8)
+ bounds: 240 16 8 8
+ - image: solid-color(31, 2, 0, 255, 8, 8)
+ bounds: 248 16 8 8
+ - image: solid-color(32, 2, 0, 255, 8, 8)
+ bounds: 256 16 8 8
+ - image: solid-color(33, 2, 0, 255, 8, 8)
+ bounds: 264 16 8 8
+ - image: solid-color(34, 2, 0, 255, 8, 8)
+ bounds: 272 16 8 8
+ - image: solid-color(35, 2, 0, 255, 8, 8)
+ bounds: 280 16 8 8
+ - image: solid-color(36, 2, 0, 255, 8, 8)
+ bounds: 288 16 8 8
+ - image: solid-color(37, 2, 0, 255, 8, 8)
+ bounds: 296 16 8 8
+ - image: solid-color(38, 2, 0, 255, 8, 8)
+ bounds: 304 16 8 8
+ - image: solid-color(39, 2, 0, 255, 8, 8)
+ bounds: 312 16 8 8
+ - image: solid-color(40, 2, 0, 255, 8, 8)
+ bounds: 320 16 8 8
+ - image: solid-color(41, 2, 0, 255, 8, 8)
+ bounds: 328 16 8 8
+ - image: solid-color(42, 2, 0, 255, 8, 8)
+ bounds: 336 16 8 8
+ - image: solid-color(43, 2, 0, 255, 8, 8)
+ bounds: 344 16 8 8
+ - image: solid-color(44, 2, 0, 255, 8, 8)
+ bounds: 352 16 8 8
+ - image: solid-color(45, 2, 0, 255, 8, 8)
+ bounds: 360 16 8 8
+ - image: solid-color(46, 2, 0, 255, 8, 8)
+ bounds: 368 16 8 8
+ - image: solid-color(47, 2, 0, 255, 8, 8)
+ bounds: 376 16 8 8
+ - image: solid-color(48, 2, 0, 255, 8, 8)
+ bounds: 384 16 8 8
+ - image: solid-color(49, 2, 0, 255, 8, 8)
+ bounds: 392 16 8 8
+ - image: solid-color(50, 2, 0, 255, 8, 8)
+ bounds: 400 16 8 8
+ - image: solid-color(51, 2, 0, 255, 8, 8)
+ bounds: 408 16 8 8
+ - image: solid-color(52, 2, 0, 255, 8, 8)
+ bounds: 416 16 8 8
+ - image: solid-color(53, 2, 0, 255, 8, 8)
+ bounds: 424 16 8 8
+ - image: solid-color(54, 2, 0, 255, 8, 8)
+ bounds: 432 16 8 8
+ - image: solid-color(55, 2, 0, 255, 8, 8)
+ bounds: 440 16 8 8
+ - image: solid-color(56, 2, 0, 255, 8, 8)
+ bounds: 448 16 8 8
+ - image: solid-color(57, 2, 0, 255, 8, 8)
+ bounds: 456 16 8 8
+ - image: solid-color(58, 2, 0, 255, 8, 8)
+ bounds: 464 16 8 8
+ - image: solid-color(59, 2, 0, 255, 8, 8)
+ bounds: 472 16 8 8
+ - image: solid-color(60, 2, 0, 255, 8, 8)
+ bounds: 480 16 8 8
+ - image: solid-color(61, 2, 0, 255, 8, 8)
+ bounds: 488 16 8 8
+ - image: solid-color(62, 2, 0, 255, 8, 8)
+ bounds: 496 16 8 8
+ - image: solid-color(63, 2, 0, 255, 8, 8)
+ bounds: 504 16 8 8
+ - image: solid-color(64, 2, 0, 255, 8, 8)
+ bounds: 512 16 8 8
+ - image: solid-color(65, 2, 0, 255, 8, 8)
+ bounds: 520 16 8 8
+ - image: solid-color(66, 2, 0, 255, 8, 8)
+ bounds: 528 16 8 8
+ - image: solid-color(67, 2, 0, 255, 8, 8)
+ bounds: 536 16 8 8
+ - image: solid-color(68, 2, 0, 255, 8, 8)
+ bounds: 544 16 8 8
+ - image: solid-color(69, 2, 0, 255, 8, 8)
+ bounds: 552 16 8 8
+ - image: solid-color(70, 2, 0, 255, 8, 8)
+ bounds: 560 16 8 8
+ - image: solid-color(71, 2, 0, 255, 8, 8)
+ bounds: 568 16 8 8
+ - image: solid-color(72, 2, 0, 255, 8, 8)
+ bounds: 576 16 8 8
+ - image: solid-color(73, 2, 0, 255, 8, 8)
+ bounds: 584 16 8 8
+ - image: solid-color(74, 2, 0, 255, 8, 8)
+ bounds: 592 16 8 8
+ - image: solid-color(75, 2, 0, 255, 8, 8)
+ bounds: 600 16 8 8
+ - image: solid-color(76, 2, 0, 255, 8, 8)
+ bounds: 608 16 8 8
+ - image: solid-color(77, 2, 0, 255, 8, 8)
+ bounds: 616 16 8 8
+ - image: solid-color(78, 2, 0, 255, 8, 8)
+ bounds: 624 16 8 8
+ - image: solid-color(79, 2, 0, 255, 8, 8)
+ bounds: 632 16 8 8
+ - image: solid-color(80, 2, 0, 255, 8, 8)
+ bounds: 640 16 8 8
+ - image: solid-color(81, 2, 0, 255, 8, 8)
+ bounds: 648 16 8 8
+ - image: solid-color(82, 2, 0, 255, 8, 8)
+ bounds: 656 16 8 8
+ - image: solid-color(83, 2, 0, 255, 8, 8)
+ bounds: 664 16 8 8
+ - image: solid-color(84, 2, 0, 255, 8, 8)
+ bounds: 672 16 8 8
+ - image: solid-color(85, 2, 0, 255, 8, 8)
+ bounds: 680 16 8 8
+ - image: solid-color(86, 2, 0, 255, 8, 8)
+ bounds: 688 16 8 8
+ - image: solid-color(87, 2, 0, 255, 8, 8)
+ bounds: 696 16 8 8
+ - image: solid-color(88, 2, 0, 255, 8, 8)
+ bounds: 704 16 8 8
+ - image: solid-color(89, 2, 0, 255, 8, 8)
+ bounds: 712 16 8 8
+ - image: solid-color(90, 2, 0, 255, 8, 8)
+ bounds: 720 16 8 8
+ - image: solid-color(91, 2, 0, 255, 8, 8)
+ bounds: 728 16 8 8
+ - image: solid-color(92, 2, 0, 255, 8, 8)
+ bounds: 736 16 8 8
+ - image: solid-color(93, 2, 0, 255, 8, 8)
+ bounds: 744 16 8 8
+ - image: solid-color(94, 2, 0, 255, 8, 8)
+ bounds: 752 16 8 8
+ - image: solid-color(95, 2, 0, 255, 8, 8)
+ bounds: 760 16 8 8
+ - image: solid-color(96, 2, 0, 255, 8, 8)
+ bounds: 768 16 8 8
+ - image: solid-color(97, 2, 0, 255, 8, 8)
+ bounds: 776 16 8 8
+ - image: solid-color(98, 2, 0, 255, 8, 8)
+ bounds: 784 16 8 8
+ - image: solid-color(99, 2, 0, 255, 8, 8)
+ bounds: 792 16 8 8
+ - image: solid-color(100, 2, 0, 255, 8, 8)
+ bounds: 800 16 8 8
+ - image: solid-color(101, 2, 0, 255, 8, 8)
+ bounds: 808 16 8 8
+ - image: solid-color(102, 2, 0, 255, 8, 8)
+ bounds: 816 16 8 8
+ - image: solid-color(103, 2, 0, 255, 8, 8)
+ bounds: 824 16 8 8
+ - image: solid-color(104, 2, 0, 255, 8, 8)
+ bounds: 832 16 8 8
+ - image: solid-color(105, 2, 0, 255, 8, 8)
+ bounds: 840 16 8 8
+ - image: solid-color(106, 2, 0, 255, 8, 8)
+ bounds: 848 16 8 8
+ - image: solid-color(107, 2, 0, 255, 8, 8)
+ bounds: 856 16 8 8
+ - image: solid-color(108, 2, 0, 255, 8, 8)
+ bounds: 864 16 8 8
+ - image: solid-color(109, 2, 0, 255, 8, 8)
+ bounds: 872 16 8 8
+ - image: solid-color(110, 2, 0, 255, 8, 8)
+ bounds: 880 16 8 8
+ - image: solid-color(111, 2, 0, 255, 8, 8)
+ bounds: 888 16 8 8
+ - image: solid-color(112, 2, 0, 255, 8, 8)
+ bounds: 896 16 8 8
+ - image: solid-color(113, 2, 0, 255, 8, 8)
+ bounds: 904 16 8 8
+ - image: solid-color(114, 2, 0, 255, 8, 8)
+ bounds: 912 16 8 8
+ - image: solid-color(115, 2, 0, 255, 8, 8)
+ bounds: 920 16 8 8
+ - image: solid-color(116, 2, 0, 255, 8, 8)
+ bounds: 928 16 8 8
+ - image: solid-color(117, 2, 0, 255, 8, 8)
+ bounds: 936 16 8 8
+ - image: solid-color(118, 2, 0, 255, 8, 8)
+ bounds: 944 16 8 8
+ - image: solid-color(119, 2, 0, 255, 8, 8)
+ bounds: 952 16 8 8
+ - image: solid-color(120, 2, 0, 255, 8, 8)
+ bounds: 960 16 8 8
+ - image: solid-color(121, 2, 0, 255, 8, 8)
+ bounds: 968 16 8 8
+ - image: solid-color(122, 2, 0, 255, 8, 8)
+ bounds: 976 16 8 8
+ - image: solid-color(123, 2, 0, 255, 8, 8)
+ bounds: 984 16 8 8
+ - image: solid-color(124, 2, 0, 255, 8, 8)
+ bounds: 992 16 8 8
+ - image: solid-color(125, 2, 0, 255, 8, 8)
+ bounds: 1000 16 8 8
+ - image: solid-color(126, 2, 0, 255, 8, 8)
+ bounds: 1008 16 8 8
+ - image: solid-color(127, 2, 0, 255, 8, 8)
+ bounds: 1016 16 8 8
+ - image: solid-color(0, 3, 0, 255, 8, 8)
+ bounds: 0 24 8 8
+ - image: solid-color(1, 3, 0, 255, 8, 8)
+ bounds: 8 24 8 8
+ - image: solid-color(2, 3, 0, 255, 8, 8)
+ bounds: 16 24 8 8
+ - image: solid-color(3, 3, 0, 255, 8, 8)
+ bounds: 24 24 8 8
+ - image: solid-color(4, 3, 0, 255, 8, 8)
+ bounds: 32 24 8 8
+ - image: solid-color(5, 3, 0, 255, 8, 8)
+ bounds: 40 24 8 8
+ - image: solid-color(6, 3, 0, 255, 8, 8)
+ bounds: 48 24 8 8
+ - image: solid-color(7, 3, 0, 255, 8, 8)
+ bounds: 56 24 8 8
+ - image: solid-color(8, 3, 0, 255, 8, 8)
+ bounds: 64 24 8 8
+ - image: solid-color(9, 3, 0, 255, 8, 8)
+ bounds: 72 24 8 8
+ - image: solid-color(10, 3, 0, 255, 8, 8)
+ bounds: 80 24 8 8
+ - image: solid-color(11, 3, 0, 255, 8, 8)
+ bounds: 88 24 8 8
+ - image: solid-color(12, 3, 0, 255, 8, 8)
+ bounds: 96 24 8 8
+ - image: solid-color(13, 3, 0, 255, 8, 8)
+ bounds: 104 24 8 8
+ - image: solid-color(14, 3, 0, 255, 8, 8)
+ bounds: 112 24 8 8
+ - image: solid-color(15, 3, 0, 255, 8, 8)
+ bounds: 120 24 8 8
+ - image: solid-color(16, 3, 0, 255, 8, 8)
+ bounds: 128 24 8 8
+ - image: solid-color(17, 3, 0, 255, 8, 8)
+ bounds: 136 24 8 8
+ - image: solid-color(18, 3, 0, 255, 8, 8)
+ bounds: 144 24 8 8
+ - image: solid-color(19, 3, 0, 255, 8, 8)
+ bounds: 152 24 8 8
+ - image: solid-color(20, 3, 0, 255, 8, 8)
+ bounds: 160 24 8 8
+ - image: solid-color(21, 3, 0, 255, 8, 8)
+ bounds: 168 24 8 8
+ - image: solid-color(22, 3, 0, 255, 8, 8)
+ bounds: 176 24 8 8
+ - image: solid-color(23, 3, 0, 255, 8, 8)
+ bounds: 184 24 8 8
+ - image: solid-color(24, 3, 0, 255, 8, 8)
+ bounds: 192 24 8 8
+ - image: solid-color(25, 3, 0, 255, 8, 8)
+ bounds: 200 24 8 8
+ - image: solid-color(26, 3, 0, 255, 8, 8)
+ bounds: 208 24 8 8
+ - image: solid-color(27, 3, 0, 255, 8, 8)
+ bounds: 216 24 8 8
+ - image: solid-color(28, 3, 0, 255, 8, 8)
+ bounds: 224 24 8 8
+ - image: solid-color(29, 3, 0, 255, 8, 8)
+ bounds: 232 24 8 8
+ - image: solid-color(30, 3, 0, 255, 8, 8)
+ bounds: 240 24 8 8
+ - image: solid-color(31, 3, 0, 255, 8, 8)
+ bounds: 248 24 8 8
+ - image: solid-color(32, 3, 0, 255, 8, 8)
+ bounds: 256 24 8 8
+ - image: solid-color(33, 3, 0, 255, 8, 8)
+ bounds: 264 24 8 8
+ - image: solid-color(34, 3, 0, 255, 8, 8)
+ bounds: 272 24 8 8
+ - image: solid-color(35, 3, 0, 255, 8, 8)
+ bounds: 280 24 8 8
+ - image: solid-color(36, 3, 0, 255, 8, 8)
+ bounds: 288 24 8 8
+ - image: solid-color(37, 3, 0, 255, 8, 8)
+ bounds: 296 24 8 8
+ - image: solid-color(38, 3, 0, 255, 8, 8)
+ bounds: 304 24 8 8
+ - image: solid-color(39, 3, 0, 255, 8, 8)
+ bounds: 312 24 8 8
+ - image: solid-color(40, 3, 0, 255, 8, 8)
+ bounds: 320 24 8 8
+ - image: solid-color(41, 3, 0, 255, 8, 8)
+ bounds: 328 24 8 8
+ - image: solid-color(42, 3, 0, 255, 8, 8)
+ bounds: 336 24 8 8
+ - image: solid-color(43, 3, 0, 255, 8, 8)
+ bounds: 344 24 8 8
+ - image: solid-color(44, 3, 0, 255, 8, 8)
+ bounds: 352 24 8 8
+ - image: solid-color(45, 3, 0, 255, 8, 8)
+ bounds: 360 24 8 8
+ - image: solid-color(46, 3, 0, 255, 8, 8)
+ bounds: 368 24 8 8
+ - image: solid-color(47, 3, 0, 255, 8, 8)
+ bounds: 376 24 8 8
+ - image: solid-color(48, 3, 0, 255, 8, 8)
+ bounds: 384 24 8 8
+ - image: solid-color(49, 3, 0, 255, 8, 8)
+ bounds: 392 24 8 8
+ - image: solid-color(50, 3, 0, 255, 8, 8)
+ bounds: 400 24 8 8
+ - image: solid-color(51, 3, 0, 255, 8, 8)
+ bounds: 408 24 8 8
+ - image: solid-color(52, 3, 0, 255, 8, 8)
+ bounds: 416 24 8 8
+ - image: solid-color(53, 3, 0, 255, 8, 8)
+ bounds: 424 24 8 8
+ - image: solid-color(54, 3, 0, 255, 8, 8)
+ bounds: 432 24 8 8
+ - image: solid-color(55, 3, 0, 255, 8, 8)
+ bounds: 440 24 8 8
+ - image: solid-color(56, 3, 0, 255, 8, 8)
+ bounds: 448 24 8 8
+ - image: solid-color(57, 3, 0, 255, 8, 8)
+ bounds: 456 24 8 8
+ - image: solid-color(58, 3, 0, 255, 8, 8)
+ bounds: 464 24 8 8
+ - image: solid-color(59, 3, 0, 255, 8, 8)
+ bounds: 472 24 8 8
+ - image: solid-color(60, 3, 0, 255, 8, 8)
+ bounds: 480 24 8 8
+ - image: solid-color(61, 3, 0, 255, 8, 8)
+ bounds: 488 24 8 8
+ - image: solid-color(62, 3, 0, 255, 8, 8)
+ bounds: 496 24 8 8
+ - image: solid-color(63, 3, 0, 255, 8, 8)
+ bounds: 504 24 8 8
+ - image: solid-color(64, 3, 0, 255, 8, 8)
+ bounds: 512 24 8 8
+ - image: solid-color(65, 3, 0, 255, 8, 8)
+ bounds: 520 24 8 8
+ - image: solid-color(66, 3, 0, 255, 8, 8)
+ bounds: 528 24 8 8
+ - image: solid-color(67, 3, 0, 255, 8, 8)
+ bounds: 536 24 8 8
+ - image: solid-color(68, 3, 0, 255, 8, 8)
+ bounds: 544 24 8 8
+ - image: solid-color(69, 3, 0, 255, 8, 8)
+ bounds: 552 24 8 8
+ - image: solid-color(70, 3, 0, 255, 8, 8)
+ bounds: 560 24 8 8
+ - image: solid-color(71, 3, 0, 255, 8, 8)
+ bounds: 568 24 8 8
+ - image: solid-color(72, 3, 0, 255, 8, 8)
+ bounds: 576 24 8 8
+ - image: solid-color(73, 3, 0, 255, 8, 8)
+ bounds: 584 24 8 8
+ - image: solid-color(74, 3, 0, 255, 8, 8)
+ bounds: 592 24 8 8
+ - image: solid-color(75, 3, 0, 255, 8, 8)
+ bounds: 600 24 8 8
+ - image: solid-color(76, 3, 0, 255, 8, 8)
+ bounds: 608 24 8 8
+ - image: solid-color(77, 3, 0, 255, 8, 8)
+ bounds: 616 24 8 8
+ - image: solid-color(78, 3, 0, 255, 8, 8)
+ bounds: 624 24 8 8
+ - image: solid-color(79, 3, 0, 255, 8, 8)
+ bounds: 632 24 8 8
+ - image: solid-color(80, 3, 0, 255, 8, 8)
+ bounds: 640 24 8 8
+ - image: solid-color(81, 3, 0, 255, 8, 8)
+ bounds: 648 24 8 8
+ - image: solid-color(82, 3, 0, 255, 8, 8)
+ bounds: 656 24 8 8
+ - image: solid-color(83, 3, 0, 255, 8, 8)
+ bounds: 664 24 8 8
+ - image: solid-color(84, 3, 0, 255, 8, 8)
+ bounds: 672 24 8 8
+ - image: solid-color(85, 3, 0, 255, 8, 8)
+ bounds: 680 24 8 8
+ - image: solid-color(86, 3, 0, 255, 8, 8)
+ bounds: 688 24 8 8
+ - image: solid-color(87, 3, 0, 255, 8, 8)
+ bounds: 696 24 8 8
+ - image: solid-color(88, 3, 0, 255, 8, 8)
+ bounds: 704 24 8 8
+ - image: solid-color(89, 3, 0, 255, 8, 8)
+ bounds: 712 24 8 8
+ - image: solid-color(90, 3, 0, 255, 8, 8)
+ bounds: 720 24 8 8
+ - image: solid-color(91, 3, 0, 255, 8, 8)
+ bounds: 728 24 8 8
+ - image: solid-color(92, 3, 0, 255, 8, 8)
+ bounds: 736 24 8 8
+ - image: solid-color(93, 3, 0, 255, 8, 8)
+ bounds: 744 24 8 8
+ - image: solid-color(94, 3, 0, 255, 8, 8)
+ bounds: 752 24 8 8
+ - image: solid-color(95, 3, 0, 255, 8, 8)
+ bounds: 760 24 8 8
+ - image: solid-color(96, 3, 0, 255, 8, 8)
+ bounds: 768 24 8 8
+ - image: solid-color(97, 3, 0, 255, 8, 8)
+ bounds: 776 24 8 8
+ - image: solid-color(98, 3, 0, 255, 8, 8)
+ bounds: 784 24 8 8
+ - image: solid-color(99, 3, 0, 255, 8, 8)
+ bounds: 792 24 8 8
+ - image: solid-color(100, 3, 0, 255, 8, 8)
+ bounds: 800 24 8 8
+ - image: solid-color(101, 3, 0, 255, 8, 8)
+ bounds: 808 24 8 8
+ - image: solid-color(102, 3, 0, 255, 8, 8)
+ bounds: 816 24 8 8
+ - image: solid-color(103, 3, 0, 255, 8, 8)
+ bounds: 824 24 8 8
+ - image: solid-color(104, 3, 0, 255, 8, 8)
+ bounds: 832 24 8 8
+ - image: solid-color(105, 3, 0, 255, 8, 8)
+ bounds: 840 24 8 8
+ - image: solid-color(106, 3, 0, 255, 8, 8)
+ bounds: 848 24 8 8
+ - image: solid-color(107, 3, 0, 255, 8, 8)
+ bounds: 856 24 8 8
+ - image: solid-color(108, 3, 0, 255, 8, 8)
+ bounds: 864 24 8 8
+ - image: solid-color(109, 3, 0, 255, 8, 8)
+ bounds: 872 24 8 8
+ - image: solid-color(110, 3, 0, 255, 8, 8)
+ bounds: 880 24 8 8
+ - image: solid-color(111, 3, 0, 255, 8, 8)
+ bounds: 888 24 8 8
+ - image: solid-color(112, 3, 0, 255, 8, 8)
+ bounds: 896 24 8 8
+ - image: solid-color(113, 3, 0, 255, 8, 8)
+ bounds: 904 24 8 8
+ - image: solid-color(114, 3, 0, 255, 8, 8)
+ bounds: 912 24 8 8
+ - image: solid-color(115, 3, 0, 255, 8, 8)
+ bounds: 920 24 8 8
+ - image: solid-color(116, 3, 0, 255, 8, 8)
+ bounds: 928 24 8 8
+ - image: solid-color(117, 3, 0, 255, 8, 8)
+ bounds: 936 24 8 8
+ - image: solid-color(118, 3, 0, 255, 8, 8)
+ bounds: 944 24 8 8
+ - image: solid-color(119, 3, 0, 255, 8, 8)
+ bounds: 952 24 8 8
+ - image: solid-color(120, 3, 0, 255, 8, 8)
+ bounds: 960 24 8 8
+ - image: solid-color(121, 3, 0, 255, 8, 8)
+ bounds: 968 24 8 8
+ - image: solid-color(122, 3, 0, 255, 8, 8)
+ bounds: 976 24 8 8
+ - image: solid-color(123, 3, 0, 255, 8, 8)
+ bounds: 984 24 8 8
+ - image: solid-color(124, 3, 0, 255, 8, 8)
+ bounds: 992 24 8 8
+ - image: solid-color(125, 3, 0, 255, 8, 8)
+ bounds: 1000 24 8 8
+ - image: solid-color(126, 3, 0, 255, 8, 8)
+ bounds: 1008 24 8 8
+ - image: solid-color(127, 3, 0, 255, 8, 8)
+ bounds: 1016 24 8 8
+ - image: solid-color(0, 4, 0, 255, 8, 8)
+ bounds: 0 32 8 8
+ - image: solid-color(1, 4, 0, 255, 8, 8)
+ bounds: 8 32 8 8
+ - image: solid-color(2, 4, 0, 255, 8, 8)
+ bounds: 16 32 8 8
+ - image: solid-color(3, 4, 0, 255, 8, 8)
+ bounds: 24 32 8 8
+ - image: solid-color(4, 4, 0, 255, 8, 8)
+ bounds: 32 32 8 8
+ - image: solid-color(5, 4, 0, 255, 8, 8)
+ bounds: 40 32 8 8
+ - image: solid-color(6, 4, 0, 255, 8, 8)
+ bounds: 48 32 8 8
+ - image: solid-color(7, 4, 0, 255, 8, 8)
+ bounds: 56 32 8 8
+ - image: solid-color(8, 4, 0, 255, 8, 8)
+ bounds: 64 32 8 8
+ - image: solid-color(9, 4, 0, 255, 8, 8)
+ bounds: 72 32 8 8
+ - image: solid-color(10, 4, 0, 255, 8, 8)
+ bounds: 80 32 8 8
+ - image: solid-color(11, 4, 0, 255, 8, 8)
+ bounds: 88 32 8 8
+ - image: solid-color(12, 4, 0, 255, 8, 8)
+ bounds: 96 32 8 8
+ - image: solid-color(13, 4, 0, 255, 8, 8)
+ bounds: 104 32 8 8
+ - image: solid-color(14, 4, 0, 255, 8, 8)
+ bounds: 112 32 8 8
+ - image: solid-color(15, 4, 0, 255, 8, 8)
+ bounds: 120 32 8 8
+ - image: solid-color(16, 4, 0, 255, 8, 8)
+ bounds: 128 32 8 8
+ - image: solid-color(17, 4, 0, 255, 8, 8)
+ bounds: 136 32 8 8
+ - image: solid-color(18, 4, 0, 255, 8, 8)
+ bounds: 144 32 8 8
+ - image: solid-color(19, 4, 0, 255, 8, 8)
+ bounds: 152 32 8 8
+ - image: solid-color(20, 4, 0, 255, 8, 8)
+ bounds: 160 32 8 8
+ - image: solid-color(21, 4, 0, 255, 8, 8)
+ bounds: 168 32 8 8
+ - image: solid-color(22, 4, 0, 255, 8, 8)
+ bounds: 176 32 8 8
+ - image: solid-color(23, 4, 0, 255, 8, 8)
+ bounds: 184 32 8 8
+ - image: solid-color(24, 4, 0, 255, 8, 8)
+ bounds: 192 32 8 8
+ - image: solid-color(25, 4, 0, 255, 8, 8)
+ bounds: 200 32 8 8
+ - image: solid-color(26, 4, 0, 255, 8, 8)
+ bounds: 208 32 8 8
+ - image: solid-color(27, 4, 0, 255, 8, 8)
+ bounds: 216 32 8 8
+ - image: solid-color(28, 4, 0, 255, 8, 8)
+ bounds: 224 32 8 8
+ - image: solid-color(29, 4, 0, 255, 8, 8)
+ bounds: 232 32 8 8
+ - image: solid-color(30, 4, 0, 255, 8, 8)
+ bounds: 240 32 8 8
+ - image: solid-color(31, 4, 0, 255, 8, 8)
+ bounds: 248 32 8 8
+ - image: solid-color(32, 4, 0, 255, 8, 8)
+ bounds: 256 32 8 8
+ - image: solid-color(33, 4, 0, 255, 8, 8)
+ bounds: 264 32 8 8
+ - image: solid-color(34, 4, 0, 255, 8, 8)
+ bounds: 272 32 8 8
+ - image: solid-color(35, 4, 0, 255, 8, 8)
+ bounds: 280 32 8 8
+ - image: solid-color(36, 4, 0, 255, 8, 8)
+ bounds: 288 32 8 8
+ - image: solid-color(37, 4, 0, 255, 8, 8)
+ bounds: 296 32 8 8
+ - image: solid-color(38, 4, 0, 255, 8, 8)
+ bounds: 304 32 8 8
+ - image: solid-color(39, 4, 0, 255, 8, 8)
+ bounds: 312 32 8 8
+ - image: solid-color(40, 4, 0, 255, 8, 8)
+ bounds: 320 32 8 8
+ - image: solid-color(41, 4, 0, 255, 8, 8)
+ bounds: 328 32 8 8
+ - image: solid-color(42, 4, 0, 255, 8, 8)
+ bounds: 336 32 8 8
+ - image: solid-color(43, 4, 0, 255, 8, 8)
+ bounds: 344 32 8 8
+ - image: solid-color(44, 4, 0, 255, 8, 8)
+ bounds: 352 32 8 8
+ - image: solid-color(45, 4, 0, 255, 8, 8)
+ bounds: 360 32 8 8
+ - image: solid-color(46, 4, 0, 255, 8, 8)
+ bounds: 368 32 8 8
+ - image: solid-color(47, 4, 0, 255, 8, 8)
+ bounds: 376 32 8 8
+ - image: solid-color(48, 4, 0, 255, 8, 8)
+ bounds: 384 32 8 8
+ - image: solid-color(49, 4, 0, 255, 8, 8)
+ bounds: 392 32 8 8
+ - image: solid-color(50, 4, 0, 255, 8, 8)
+ bounds: 400 32 8 8
+ - image: solid-color(51, 4, 0, 255, 8, 8)
+ bounds: 408 32 8 8
+ - image: solid-color(52, 4, 0, 255, 8, 8)
+ bounds: 416 32 8 8
+ - image: solid-color(53, 4, 0, 255, 8, 8)
+ bounds: 424 32 8 8
+ - image: solid-color(54, 4, 0, 255, 8, 8)
+ bounds: 432 32 8 8
+ - image: solid-color(55, 4, 0, 255, 8, 8)
+ bounds: 440 32 8 8
+ - image: solid-color(56, 4, 0, 255, 8, 8)
+ bounds: 448 32 8 8
+ - image: solid-color(57, 4, 0, 255, 8, 8)
+ bounds: 456 32 8 8
+ - image: solid-color(58, 4, 0, 255, 8, 8)
+ bounds: 464 32 8 8
+ - image: solid-color(59, 4, 0, 255, 8, 8)
+ bounds: 472 32 8 8
+ - image: solid-color(60, 4, 0, 255, 8, 8)
+ bounds: 480 32 8 8
+ - image: solid-color(61, 4, 0, 255, 8, 8)
+ bounds: 488 32 8 8
+ - image: solid-color(62, 4, 0, 255, 8, 8)
+ bounds: 496 32 8 8
+ - image: solid-color(63, 4, 0, 255, 8, 8)
+ bounds: 504 32 8 8
+ - image: solid-color(64, 4, 0, 255, 8, 8)
+ bounds: 512 32 8 8
+ - image: solid-color(65, 4, 0, 255, 8, 8)
+ bounds: 520 32 8 8
+ - image: solid-color(66, 4, 0, 255, 8, 8)
+ bounds: 528 32 8 8
+ - image: solid-color(67, 4, 0, 255, 8, 8)
+ bounds: 536 32 8 8
+ - image: solid-color(68, 4, 0, 255, 8, 8)
+ bounds: 544 32 8 8
+ - image: solid-color(69, 4, 0, 255, 8, 8)
+ bounds: 552 32 8 8
+ - image: solid-color(70, 4, 0, 255, 8, 8)
+ bounds: 560 32 8 8
+ - image: solid-color(71, 4, 0, 255, 8, 8)
+ bounds: 568 32 8 8
+ - image: solid-color(72, 4, 0, 255, 8, 8)
+ bounds: 576 32 8 8
+ - image: solid-color(73, 4, 0, 255, 8, 8)
+ bounds: 584 32 8 8
+ - image: solid-color(74, 4, 0, 255, 8, 8)
+ bounds: 592 32 8 8
+ - image: solid-color(75, 4, 0, 255, 8, 8)
+ bounds: 600 32 8 8
+ - image: solid-color(76, 4, 0, 255, 8, 8)
+ bounds: 608 32 8 8
+ - image: solid-color(77, 4, 0, 255, 8, 8)
+ bounds: 616 32 8 8
+ - image: solid-color(78, 4, 0, 255, 8, 8)
+ bounds: 624 32 8 8
+ - image: solid-color(79, 4, 0, 255, 8, 8)
+ bounds: 632 32 8 8
+ - image: solid-color(80, 4, 0, 255, 8, 8)
+ bounds: 640 32 8 8
+ - image: solid-color(81, 4, 0, 255, 8, 8)
+ bounds: 648 32 8 8
+ - image: solid-color(82, 4, 0, 255, 8, 8)
+ bounds: 656 32 8 8
+ - image: solid-color(83, 4, 0, 255, 8, 8)
+ bounds: 664 32 8 8
+ - image: solid-color(84, 4, 0, 255, 8, 8)
+ bounds: 672 32 8 8
+ - image: solid-color(85, 4, 0, 255, 8, 8)
+ bounds: 680 32 8 8
+ - image: solid-color(86, 4, 0, 255, 8, 8)
+ bounds: 688 32 8 8
+ - image: solid-color(87, 4, 0, 255, 8, 8)
+ bounds: 696 32 8 8
+ - image: solid-color(88, 4, 0, 255, 8, 8)
+ bounds: 704 32 8 8
+ - image: solid-color(89, 4, 0, 255, 8, 8)
+ bounds: 712 32 8 8
+ - image: solid-color(90, 4, 0, 255, 8, 8)
+ bounds: 720 32 8 8
+ - image: solid-color(91, 4, 0, 255, 8, 8)
+ bounds: 728 32 8 8
+ - image: solid-color(92, 4, 0, 255, 8, 8)
+ bounds: 736 32 8 8
+ - image: solid-color(93, 4, 0, 255, 8, 8)
+ bounds: 744 32 8 8
+ - image: solid-color(94, 4, 0, 255, 8, 8)
+ bounds: 752 32 8 8
+ - image: solid-color(95, 4, 0, 255, 8, 8)
+ bounds: 760 32 8 8
+ - image: solid-color(96, 4, 0, 255, 8, 8)
+ bounds: 768 32 8 8
+ - image: solid-color(97, 4, 0, 255, 8, 8)
+ bounds: 776 32 8 8
+ - image: solid-color(98, 4, 0, 255, 8, 8)
+ bounds: 784 32 8 8
+ - image: solid-color(99, 4, 0, 255, 8, 8)
+ bounds: 792 32 8 8
+ - image: solid-color(100, 4, 0, 255, 8, 8)
+ bounds: 800 32 8 8
+ - image: solid-color(101, 4, 0, 255, 8, 8)
+ bounds: 808 32 8 8
+ - image: solid-color(102, 4, 0, 255, 8, 8)
+ bounds: 816 32 8 8
+ - image: solid-color(103, 4, 0, 255, 8, 8)
+ bounds: 824 32 8 8
+ - image: solid-color(104, 4, 0, 255, 8, 8)
+ bounds: 832 32 8 8
+ - image: solid-color(105, 4, 0, 255, 8, 8)
+ bounds: 840 32 8 8
+ - image: solid-color(106, 4, 0, 255, 8, 8)
+ bounds: 848 32 8 8
+ - image: solid-color(107, 4, 0, 255, 8, 8)
+ bounds: 856 32 8 8
+ - image: solid-color(108, 4, 0, 255, 8, 8)
+ bounds: 864 32 8 8
+ - image: solid-color(109, 4, 0, 255, 8, 8)
+ bounds: 872 32 8 8
+ - image: solid-color(110, 4, 0, 255, 8, 8)
+ bounds: 880 32 8 8
+ - image: solid-color(111, 4, 0, 255, 8, 8)
+ bounds: 888 32 8 8
+ - image: solid-color(112, 4, 0, 255, 8, 8)
+ bounds: 896 32 8 8
+ - image: solid-color(113, 4, 0, 255, 8, 8)
+ bounds: 904 32 8 8
+ - image: solid-color(114, 4, 0, 255, 8, 8)
+ bounds: 912 32 8 8
+ - image: solid-color(115, 4, 0, 255, 8, 8)
+ bounds: 920 32 8 8
+ - image: solid-color(116, 4, 0, 255, 8, 8)
+ bounds: 928 32 8 8
+ - image: solid-color(117, 4, 0, 255, 8, 8)
+ bounds: 936 32 8 8
+ - image: solid-color(118, 4, 0, 255, 8, 8)
+ bounds: 944 32 8 8
+ - image: solid-color(119, 4, 0, 255, 8, 8)
+ bounds: 952 32 8 8
+ - image: solid-color(120, 4, 0, 255, 8, 8)
+ bounds: 960 32 8 8
+ - image: solid-color(121, 4, 0, 255, 8, 8)
+ bounds: 968 32 8 8
+ - image: solid-color(122, 4, 0, 255, 8, 8)
+ bounds: 976 32 8 8
+ - image: solid-color(123, 4, 0, 255, 8, 8)
+ bounds: 984 32 8 8
+ - image: solid-color(124, 4, 0, 255, 8, 8)
+ bounds: 992 32 8 8
+ - image: solid-color(125, 4, 0, 255, 8, 8)
+ bounds: 1000 32 8 8
+ - image: solid-color(126, 4, 0, 255, 8, 8)
+ bounds: 1008 32 8 8
+ - image: solid-color(127, 4, 0, 255, 8, 8)
+ bounds: 1016 32 8 8
+ - image: solid-color(0, 5, 0, 255, 8, 8)
+ bounds: 0 40 8 8
+ - image: solid-color(1, 5, 0, 255, 8, 8)
+ bounds: 8 40 8 8
+ - image: solid-color(2, 5, 0, 255, 8, 8)
+ bounds: 16 40 8 8
+ - image: solid-color(3, 5, 0, 255, 8, 8)
+ bounds: 24 40 8 8
+ - image: solid-color(4, 5, 0, 255, 8, 8)
+ bounds: 32 40 8 8
+ - image: solid-color(5, 5, 0, 255, 8, 8)
+ bounds: 40 40 8 8
+ - image: solid-color(6, 5, 0, 255, 8, 8)
+ bounds: 48 40 8 8
+ - image: solid-color(7, 5, 0, 255, 8, 8)
+ bounds: 56 40 8 8
+ - image: solid-color(8, 5, 0, 255, 8, 8)
+ bounds: 64 40 8 8
+ - image: solid-color(9, 5, 0, 255, 8, 8)
+ bounds: 72 40 8 8
+ - image: solid-color(10, 5, 0, 255, 8, 8)
+ bounds: 80 40 8 8
+ - image: solid-color(11, 5, 0, 255, 8, 8)
+ bounds: 88 40 8 8
+ - image: solid-color(12, 5, 0, 255, 8, 8)
+ bounds: 96 40 8 8
+ - image: solid-color(13, 5, 0, 255, 8, 8)
+ bounds: 104 40 8 8
+ - image: solid-color(14, 5, 0, 255, 8, 8)
+ bounds: 112 40 8 8
+ - image: solid-color(15, 5, 0, 255, 8, 8)
+ bounds: 120 40 8 8
+ - image: solid-color(16, 5, 0, 255, 8, 8)
+ bounds: 128 40 8 8
+ - image: solid-color(17, 5, 0, 255, 8, 8)
+ bounds: 136 40 8 8
+ - image: solid-color(18, 5, 0, 255, 8, 8)
+ bounds: 144 40 8 8
+ - image: solid-color(19, 5, 0, 255, 8, 8)
+ bounds: 152 40 8 8
+ - image: solid-color(20, 5, 0, 255, 8, 8)
+ bounds: 160 40 8 8
+ - image: solid-color(21, 5, 0, 255, 8, 8)
+ bounds: 168 40 8 8
+ - image: solid-color(22, 5, 0, 255, 8, 8)
+ bounds: 176 40 8 8
+ - image: solid-color(23, 5, 0, 255, 8, 8)
+ bounds: 184 40 8 8
+ - image: solid-color(24, 5, 0, 255, 8, 8)
+ bounds: 192 40 8 8
+ - image: solid-color(25, 5, 0, 255, 8, 8)
+ bounds: 200 40 8 8
+ - image: solid-color(26, 5, 0, 255, 8, 8)
+ bounds: 208 40 8 8
+ - image: solid-color(27, 5, 0, 255, 8, 8)
+ bounds: 216 40 8 8
+ - image: solid-color(28, 5, 0, 255, 8, 8)
+ bounds: 224 40 8 8
+ - image: solid-color(29, 5, 0, 255, 8, 8)
+ bounds: 232 40 8 8
+ - image: solid-color(30, 5, 0, 255, 8, 8)
+ bounds: 240 40 8 8
+ - image: solid-color(31, 5, 0, 255, 8, 8)
+ bounds: 248 40 8 8
+ - image: solid-color(32, 5, 0, 255, 8, 8)
+ bounds: 256 40 8 8
+ - image: solid-color(33, 5, 0, 255, 8, 8)
+ bounds: 264 40 8 8
+ - image: solid-color(34, 5, 0, 255, 8, 8)
+ bounds: 272 40 8 8
+ - image: solid-color(35, 5, 0, 255, 8, 8)
+ bounds: 280 40 8 8
+ - image: solid-color(36, 5, 0, 255, 8, 8)
+ bounds: 288 40 8 8
+ - image: solid-color(37, 5, 0, 255, 8, 8)
+ bounds: 296 40 8 8
+ - image: solid-color(38, 5, 0, 255, 8, 8)
+ bounds: 304 40 8 8
+ - image: solid-color(39, 5, 0, 255, 8, 8)
+ bounds: 312 40 8 8
+ - image: solid-color(40, 5, 0, 255, 8, 8)
+ bounds: 320 40 8 8
+ - image: solid-color(41, 5, 0, 255, 8, 8)
+ bounds: 328 40 8 8
+ - image: solid-color(42, 5, 0, 255, 8, 8)
+ bounds: 336 40 8 8
+ - image: solid-color(43, 5, 0, 255, 8, 8)
+ bounds: 344 40 8 8
+ - image: solid-color(44, 5, 0, 255, 8, 8)
+ bounds: 352 40 8 8
+ - image: solid-color(45, 5, 0, 255, 8, 8)
+ bounds: 360 40 8 8
+ - image: solid-color(46, 5, 0, 255, 8, 8)
+ bounds: 368 40 8 8
+ - image: solid-color(47, 5, 0, 255, 8, 8)
+ bounds: 376 40 8 8
+ - image: solid-color(48, 5, 0, 255, 8, 8)
+ bounds: 384 40 8 8
+ - image: solid-color(49, 5, 0, 255, 8, 8)
+ bounds: 392 40 8 8
+ - image: solid-color(50, 5, 0, 255, 8, 8)
+ bounds: 400 40 8 8
+ - image: solid-color(51, 5, 0, 255, 8, 8)
+ bounds: 408 40 8 8
+ - image: solid-color(52, 5, 0, 255, 8, 8)
+ bounds: 416 40 8 8
+ - image: solid-color(53, 5, 0, 255, 8, 8)
+ bounds: 424 40 8 8
+ - image: solid-color(54, 5, 0, 255, 8, 8)
+ bounds: 432 40 8 8
+ - image: solid-color(55, 5, 0, 255, 8, 8)
+ bounds: 440 40 8 8
+ - image: solid-color(56, 5, 0, 255, 8, 8)
+ bounds: 448 40 8 8
+ - image: solid-color(57, 5, 0, 255, 8, 8)
+ bounds: 456 40 8 8
+ - image: solid-color(58, 5, 0, 255, 8, 8)
+ bounds: 464 40 8 8
+ - image: solid-color(59, 5, 0, 255, 8, 8)
+ bounds: 472 40 8 8
+ - image: solid-color(60, 5, 0, 255, 8, 8)
+ bounds: 480 40 8 8
+ - image: solid-color(61, 5, 0, 255, 8, 8)
+ bounds: 488 40 8 8
+ - image: solid-color(62, 5, 0, 255, 8, 8)
+ bounds: 496 40 8 8
+ - image: solid-color(63, 5, 0, 255, 8, 8)
+ bounds: 504 40 8 8
+ - image: solid-color(64, 5, 0, 255, 8, 8)
+ bounds: 512 40 8 8
+ - image: solid-color(65, 5, 0, 255, 8, 8)
+ bounds: 520 40 8 8
+ - image: solid-color(66, 5, 0, 255, 8, 8)
+ bounds: 528 40 8 8
+ - image: solid-color(67, 5, 0, 255, 8, 8)
+ bounds: 536 40 8 8
+ - image: solid-color(68, 5, 0, 255, 8, 8)
+ bounds: 544 40 8 8
+ - image: solid-color(69, 5, 0, 255, 8, 8)
+ bounds: 552 40 8 8
+ - image: solid-color(70, 5, 0, 255, 8, 8)
+ bounds: 560 40 8 8
+ - image: solid-color(71, 5, 0, 255, 8, 8)
+ bounds: 568 40 8 8
+ - image: solid-color(72, 5, 0, 255, 8, 8)
+ bounds: 576 40 8 8
+ - image: solid-color(73, 5, 0, 255, 8, 8)
+ bounds: 584 40 8 8
+ - image: solid-color(74, 5, 0, 255, 8, 8)
+ bounds: 592 40 8 8
+ - image: solid-color(75, 5, 0, 255, 8, 8)
+ bounds: 600 40 8 8
+ - image: solid-color(76, 5, 0, 255, 8, 8)
+ bounds: 608 40 8 8
+ - image: solid-color(77, 5, 0, 255, 8, 8)
+ bounds: 616 40 8 8
+ - image: solid-color(78, 5, 0, 255, 8, 8)
+ bounds: 624 40 8 8
+ - image: solid-color(79, 5, 0, 255, 8, 8)
+ bounds: 632 40 8 8
+ - image: solid-color(80, 5, 0, 255, 8, 8)
+ bounds: 640 40 8 8
+ - image: solid-color(81, 5, 0, 255, 8, 8)
+ bounds: 648 40 8 8
+ - image: solid-color(82, 5, 0, 255, 8, 8)
+ bounds: 656 40 8 8
+ - image: solid-color(83, 5, 0, 255, 8, 8)
+ bounds: 664 40 8 8
+ - image: solid-color(84, 5, 0, 255, 8, 8)
+ bounds: 672 40 8 8
+ - image: solid-color(85, 5, 0, 255, 8, 8)
+ bounds: 680 40 8 8
+ - image: solid-color(86, 5, 0, 255, 8, 8)
+ bounds: 688 40 8 8
+ - image: solid-color(87, 5, 0, 255, 8, 8)
+ bounds: 696 40 8 8
+ - image: solid-color(88, 5, 0, 255, 8, 8)
+ bounds: 704 40 8 8
+ - image: solid-color(89, 5, 0, 255, 8, 8)
+ bounds: 712 40 8 8
+ - image: solid-color(90, 5, 0, 255, 8, 8)
+ bounds: 720 40 8 8
+ - image: solid-color(91, 5, 0, 255, 8, 8)
+ bounds: 728 40 8 8
+ - image: solid-color(92, 5, 0, 255, 8, 8)
+ bounds: 736 40 8 8
+ - image: solid-color(93, 5, 0, 255, 8, 8)
+ bounds: 744 40 8 8
+ - image: solid-color(94, 5, 0, 255, 8, 8)
+ bounds: 752 40 8 8
+ - image: solid-color(95, 5, 0, 255, 8, 8)
+ bounds: 760 40 8 8
+ - image: solid-color(96, 5, 0, 255, 8, 8)
+ bounds: 768 40 8 8
+ - image: solid-color(97, 5, 0, 255, 8, 8)
+ bounds: 776 40 8 8
+ - image: solid-color(98, 5, 0, 255, 8, 8)
+ bounds: 784 40 8 8
+ - image: solid-color(99, 5, 0, 255, 8, 8)
+ bounds: 792 40 8 8
+ - image: solid-color(100, 5, 0, 255, 8, 8)
+ bounds: 800 40 8 8
+ - image: solid-color(101, 5, 0, 255, 8, 8)
+ bounds: 808 40 8 8
+ - image: solid-color(102, 5, 0, 255, 8, 8)
+ bounds: 816 40 8 8
+ - image: solid-color(103, 5, 0, 255, 8, 8)
+ bounds: 824 40 8 8
+ - image: solid-color(104, 5, 0, 255, 8, 8)
+ bounds: 832 40 8 8
+ - image: solid-color(105, 5, 0, 255, 8, 8)
+ bounds: 840 40 8 8
+ - image: solid-color(106, 5, 0, 255, 8, 8)
+ bounds: 848 40 8 8
+ - image: solid-color(107, 5, 0, 255, 8, 8)
+ bounds: 856 40 8 8
+ - image: solid-color(108, 5, 0, 255, 8, 8)
+ bounds: 864 40 8 8
+ - image: solid-color(109, 5, 0, 255, 8, 8)
+ bounds: 872 40 8 8
+ - image: solid-color(110, 5, 0, 255, 8, 8)
+ bounds: 880 40 8 8
+ - image: solid-color(111, 5, 0, 255, 8, 8)
+ bounds: 888 40 8 8
+ - image: solid-color(112, 5, 0, 255, 8, 8)
+ bounds: 896 40 8 8
+ - image: solid-color(113, 5, 0, 255, 8, 8)
+ bounds: 904 40 8 8
+ - image: solid-color(114, 5, 0, 255, 8, 8)
+ bounds: 912 40 8 8
+ - image: solid-color(115, 5, 0, 255, 8, 8)
+ bounds: 920 40 8 8
+ - image: solid-color(116, 5, 0, 255, 8, 8)
+ bounds: 928 40 8 8
+ - image: solid-color(117, 5, 0, 255, 8, 8)
+ bounds: 936 40 8 8
+ - image: solid-color(118, 5, 0, 255, 8, 8)
+ bounds: 944 40 8 8
+ - image: solid-color(119, 5, 0, 255, 8, 8)
+ bounds: 952 40 8 8
+ - image: solid-color(120, 5, 0, 255, 8, 8)
+ bounds: 960 40 8 8
+ - image: solid-color(121, 5, 0, 255, 8, 8)
+ bounds: 968 40 8 8
+ - image: solid-color(122, 5, 0, 255, 8, 8)
+ bounds: 976 40 8 8
+ - image: solid-color(123, 5, 0, 255, 8, 8)
+ bounds: 984 40 8 8
+ - image: solid-color(124, 5, 0, 255, 8, 8)
+ bounds: 992 40 8 8
+ - image: solid-color(125, 5, 0, 255, 8, 8)
+ bounds: 1000 40 8 8
+ - image: solid-color(126, 5, 0, 255, 8, 8)
+ bounds: 1008 40 8 8
+ - image: solid-color(127, 5, 0, 255, 8, 8)
+ bounds: 1016 40 8 8
+ - image: solid-color(0, 6, 0, 255, 8, 8)
+ bounds: 0 48 8 8
+ - image: solid-color(1, 6, 0, 255, 8, 8)
+ bounds: 8 48 8 8
+ - image: solid-color(2, 6, 0, 255, 8, 8)
+ bounds: 16 48 8 8
+ - image: solid-color(3, 6, 0, 255, 8, 8)
+ bounds: 24 48 8 8
+ - image: solid-color(4, 6, 0, 255, 8, 8)
+ bounds: 32 48 8 8
+ - image: solid-color(5, 6, 0, 255, 8, 8)
+ bounds: 40 48 8 8
+ - image: solid-color(6, 6, 0, 255, 8, 8)
+ bounds: 48 48 8 8
+ - image: solid-color(7, 6, 0, 255, 8, 8)
+ bounds: 56 48 8 8
+ - image: solid-color(8, 6, 0, 255, 8, 8)
+ bounds: 64 48 8 8
+ - image: solid-color(9, 6, 0, 255, 8, 8)
+ bounds: 72 48 8 8
+ - image: solid-color(10, 6, 0, 255, 8, 8)
+ bounds: 80 48 8 8
+ - image: solid-color(11, 6, 0, 255, 8, 8)
+ bounds: 88 48 8 8
+ - image: solid-color(12, 6, 0, 255, 8, 8)
+ bounds: 96 48 8 8
+ - image: solid-color(13, 6, 0, 255, 8, 8)
+ bounds: 104 48 8 8
+ - image: solid-color(14, 6, 0, 255, 8, 8)
+ bounds: 112 48 8 8
+ - image: solid-color(15, 6, 0, 255, 8, 8)
+ bounds: 120 48 8 8
+ - image: solid-color(16, 6, 0, 255, 8, 8)
+ bounds: 128 48 8 8
+ - image: solid-color(17, 6, 0, 255, 8, 8)
+ bounds: 136 48 8 8
+ - image: solid-color(18, 6, 0, 255, 8, 8)
+ bounds: 144 48 8 8
+ - image: solid-color(19, 6, 0, 255, 8, 8)
+ bounds: 152 48 8 8
+ - image: solid-color(20, 6, 0, 255, 8, 8)
+ bounds: 160 48 8 8
+ - image: solid-color(21, 6, 0, 255, 8, 8)
+ bounds: 168 48 8 8
+ - image: solid-color(22, 6, 0, 255, 8, 8)
+ bounds: 176 48 8 8
+ - image: solid-color(23, 6, 0, 255, 8, 8)
+ bounds: 184 48 8 8
+ - image: solid-color(24, 6, 0, 255, 8, 8)
+ bounds: 192 48 8 8
+ - image: solid-color(25, 6, 0, 255, 8, 8)
+ bounds: 200 48 8 8
+ - image: solid-color(26, 6, 0, 255, 8, 8)
+ bounds: 208 48 8 8
+ - image: solid-color(27, 6, 0, 255, 8, 8)
+ bounds: 216 48 8 8
+ - image: solid-color(28, 6, 0, 255, 8, 8)
+ bounds: 224 48 8 8
+ - image: solid-color(29, 6, 0, 255, 8, 8)
+ bounds: 232 48 8 8
+ - image: solid-color(30, 6, 0, 255, 8, 8)
+ bounds: 240 48 8 8
+ - image: solid-color(31, 6, 0, 255, 8, 8)
+ bounds: 248 48 8 8
+ - image: solid-color(32, 6, 0, 255, 8, 8)
+ bounds: 256 48 8 8
+ - image: solid-color(33, 6, 0, 255, 8, 8)
+ bounds: 264 48 8 8
+ - image: solid-color(34, 6, 0, 255, 8, 8)
+ bounds: 272 48 8 8
+ - image: solid-color(35, 6, 0, 255, 8, 8)
+ bounds: 280 48 8 8
+ - image: solid-color(36, 6, 0, 255, 8, 8)
+ bounds: 288 48 8 8
+ - image: solid-color(37, 6, 0, 255, 8, 8)
+ bounds: 296 48 8 8
+ - image: solid-color(38, 6, 0, 255, 8, 8)
+ bounds: 304 48 8 8
+ - image: solid-color(39, 6, 0, 255, 8, 8)
+ bounds: 312 48 8 8
+ - image: solid-color(40, 6, 0, 255, 8, 8)
+ bounds: 320 48 8 8
+ - image: solid-color(41, 6, 0, 255, 8, 8)
+ bounds: 328 48 8 8
+ - image: solid-color(42, 6, 0, 255, 8, 8)
+ bounds: 336 48 8 8
+ - image: solid-color(43, 6, 0, 255, 8, 8)
+ bounds: 344 48 8 8
+ - image: solid-color(44, 6, 0, 255, 8, 8)
+ bounds: 352 48 8 8
+ - image: solid-color(45, 6, 0, 255, 8, 8)
+ bounds: 360 48 8 8
+ - image: solid-color(46, 6, 0, 255, 8, 8)
+ bounds: 368 48 8 8
+ - image: solid-color(47, 6, 0, 255, 8, 8)
+ bounds: 376 48 8 8
+ - image: solid-color(48, 6, 0, 255, 8, 8)
+ bounds: 384 48 8 8
+ - image: solid-color(49, 6, 0, 255, 8, 8)
+ bounds: 392 48 8 8
+ - image: solid-color(50, 6, 0, 255, 8, 8)
+ bounds: 400 48 8 8
+ - image: solid-color(51, 6, 0, 255, 8, 8)
+ bounds: 408 48 8 8
+ - image: solid-color(52, 6, 0, 255, 8, 8)
+ bounds: 416 48 8 8
+ - image: solid-color(53, 6, 0, 255, 8, 8)
+ bounds: 424 48 8 8
+ - image: solid-color(54, 6, 0, 255, 8, 8)
+ bounds: 432 48 8 8
+ - image: solid-color(55, 6, 0, 255, 8, 8)
+ bounds: 440 48 8 8
+ - image: solid-color(56, 6, 0, 255, 8, 8)
+ bounds: 448 48 8 8
+ - image: solid-color(57, 6, 0, 255, 8, 8)
+ bounds: 456 48 8 8
+ - image: solid-color(58, 6, 0, 255, 8, 8)
+ bounds: 464 48 8 8
+ - image: solid-color(59, 6, 0, 255, 8, 8)
+ bounds: 472 48 8 8
+ - image: solid-color(60, 6, 0, 255, 8, 8)
+ bounds: 480 48 8 8
+ - image: solid-color(61, 6, 0, 255, 8, 8)
+ bounds: 488 48 8 8
+ - image: solid-color(62, 6, 0, 255, 8, 8)
+ bounds: 496 48 8 8
+ - image: solid-color(63, 6, 0, 255, 8, 8)
+ bounds: 504 48 8 8
+ - image: solid-color(64, 6, 0, 255, 8, 8)
+ bounds: 512 48 8 8
+ - image: solid-color(65, 6, 0, 255, 8, 8)
+ bounds: 520 48 8 8
+ - image: solid-color(66, 6, 0, 255, 8, 8)
+ bounds: 528 48 8 8
+ - image: solid-color(67, 6, 0, 255, 8, 8)
+ bounds: 536 48 8 8
+ - image: solid-color(68, 6, 0, 255, 8, 8)
+ bounds: 544 48 8 8
+ - image: solid-color(69, 6, 0, 255, 8, 8)
+ bounds: 552 48 8 8
+ - image: solid-color(70, 6, 0, 255, 8, 8)
+ bounds: 560 48 8 8
+ - image: solid-color(71, 6, 0, 255, 8, 8)
+ bounds: 568 48 8 8
+ - image: solid-color(72, 6, 0, 255, 8, 8)
+ bounds: 576 48 8 8
+ - image: solid-color(73, 6, 0, 255, 8, 8)
+ bounds: 584 48 8 8
+ - image: solid-color(74, 6, 0, 255, 8, 8)
+ bounds: 592 48 8 8
+ - image: solid-color(75, 6, 0, 255, 8, 8)
+ bounds: 600 48 8 8
+ - image: solid-color(76, 6, 0, 255, 8, 8)
+ bounds: 608 48 8 8
+ - image: solid-color(77, 6, 0, 255, 8, 8)
+ bounds: 616 48 8 8
+ - image: solid-color(78, 6, 0, 255, 8, 8)
+ bounds: 624 48 8 8
+ - image: solid-color(79, 6, 0, 255, 8, 8)
+ bounds: 632 48 8 8
+ - image: solid-color(80, 6, 0, 255, 8, 8)
+ bounds: 640 48 8 8
+ - image: solid-color(81, 6, 0, 255, 8, 8)
+ bounds: 648 48 8 8
+ - image: solid-color(82, 6, 0, 255, 8, 8)
+ bounds: 656 48 8 8
+ - image: solid-color(83, 6, 0, 255, 8, 8)
+ bounds: 664 48 8 8
+ - image: solid-color(84, 6, 0, 255, 8, 8)
+ bounds: 672 48 8 8
+ - image: solid-color(85, 6, 0, 255, 8, 8)
+ bounds: 680 48 8 8
+ - image: solid-color(86, 6, 0, 255, 8, 8)
+ bounds: 688 48 8 8
+ - image: solid-color(87, 6, 0, 255, 8, 8)
+ bounds: 696 48 8 8
+ - image: solid-color(88, 6, 0, 255, 8, 8)
+ bounds: 704 48 8 8
+ - image: solid-color(89, 6, 0, 255, 8, 8)
+ bounds: 712 48 8 8
+ - image: solid-color(90, 6, 0, 255, 8, 8)
+ bounds: 720 48 8 8
+ - image: solid-color(91, 6, 0, 255, 8, 8)
+ bounds: 728 48 8 8
+ - image: solid-color(92, 6, 0, 255, 8, 8)
+ bounds: 736 48 8 8
+ - image: solid-color(93, 6, 0, 255, 8, 8)
+ bounds: 744 48 8 8
+ - image: solid-color(94, 6, 0, 255, 8, 8)
+ bounds: 752 48 8 8
+ - image: solid-color(95, 6, 0, 255, 8, 8)
+ bounds: 760 48 8 8
+ - image: solid-color(96, 6, 0, 255, 8, 8)
+ bounds: 768 48 8 8
+ - image: solid-color(97, 6, 0, 255, 8, 8)
+ bounds: 776 48 8 8
+ - image: solid-color(98, 6, 0, 255, 8, 8)
+ bounds: 784 48 8 8
+ - image: solid-color(99, 6, 0, 255, 8, 8)
+ bounds: 792 48 8 8
+ - image: solid-color(100, 6, 0, 255, 8, 8)
+ bounds: 800 48 8 8
+ - image: solid-color(101, 6, 0, 255, 8, 8)
+ bounds: 808 48 8 8
+ - image: solid-color(102, 6, 0, 255, 8, 8)
+ bounds: 816 48 8 8
+ - image: solid-color(103, 6, 0, 255, 8, 8)
+ bounds: 824 48 8 8
+ - image: solid-color(104, 6, 0, 255, 8, 8)
+ bounds: 832 48 8 8
+ - image: solid-color(105, 6, 0, 255, 8, 8)
+ bounds: 840 48 8 8
+ - image: solid-color(106, 6, 0, 255, 8, 8)
+ bounds: 848 48 8 8
+ - image: solid-color(107, 6, 0, 255, 8, 8)
+ bounds: 856 48 8 8
+ - image: solid-color(108, 6, 0, 255, 8, 8)
+ bounds: 864 48 8 8
+ - image: solid-color(109, 6, 0, 255, 8, 8)
+ bounds: 872 48 8 8
+ - image: solid-color(110, 6, 0, 255, 8, 8)
+ bounds: 880 48 8 8
+ - image: solid-color(111, 6, 0, 255, 8, 8)
+ bounds: 888 48 8 8
+ - image: solid-color(112, 6, 0, 255, 8, 8)
+ bounds: 896 48 8 8
+ - image: solid-color(113, 6, 0, 255, 8, 8)
+ bounds: 904 48 8 8
+ - image: solid-color(114, 6, 0, 255, 8, 8)
+ bounds: 912 48 8 8
+ - image: solid-color(115, 6, 0, 255, 8, 8)
+ bounds: 920 48 8 8
+ - image: solid-color(116, 6, 0, 255, 8, 8)
+ bounds: 928 48 8 8
+ - image: solid-color(117, 6, 0, 255, 8, 8)
+ bounds: 936 48 8 8
+ - image: solid-color(118, 6, 0, 255, 8, 8)
+ bounds: 944 48 8 8
+ - image: solid-color(119, 6, 0, 255, 8, 8)
+ bounds: 952 48 8 8
+ - image: solid-color(120, 6, 0, 255, 8, 8)
+ bounds: 960 48 8 8
+ - image: solid-color(121, 6, 0, 255, 8, 8)
+ bounds: 968 48 8 8
+ - image: solid-color(122, 6, 0, 255, 8, 8)
+ bounds: 976 48 8 8
+ - image: solid-color(123, 6, 0, 255, 8, 8)
+ bounds: 984 48 8 8
+ - image: solid-color(124, 6, 0, 255, 8, 8)
+ bounds: 992 48 8 8
+ - image: solid-color(125, 6, 0, 255, 8, 8)
+ bounds: 1000 48 8 8
+ - image: solid-color(126, 6, 0, 255, 8, 8)
+ bounds: 1008 48 8 8
+ - image: solid-color(127, 6, 0, 255, 8, 8)
+ bounds: 1016 48 8 8
+ - image: solid-color(0, 7, 0, 255, 8, 8)
+ bounds: 0 56 8 8
+ - image: solid-color(1, 7, 0, 255, 8, 8)
+ bounds: 8 56 8 8
+ - image: solid-color(2, 7, 0, 255, 8, 8)
+ bounds: 16 56 8 8
+ - image: solid-color(3, 7, 0, 255, 8, 8)
+ bounds: 24 56 8 8
+ - image: solid-color(4, 7, 0, 255, 8, 8)
+ bounds: 32 56 8 8
+ - image: solid-color(5, 7, 0, 255, 8, 8)
+ bounds: 40 56 8 8
+ - image: solid-color(6, 7, 0, 255, 8, 8)
+ bounds: 48 56 8 8
+ - image: solid-color(7, 7, 0, 255, 8, 8)
+ bounds: 56 56 8 8
+ - image: solid-color(8, 7, 0, 255, 8, 8)
+ bounds: 64 56 8 8
+ - image: solid-color(9, 7, 0, 255, 8, 8)
+ bounds: 72 56 8 8
+ - image: solid-color(10, 7, 0, 255, 8, 8)
+ bounds: 80 56 8 8
+ - image: solid-color(11, 7, 0, 255, 8, 8)
+ bounds: 88 56 8 8
+ - image: solid-color(12, 7, 0, 255, 8, 8)
+ bounds: 96 56 8 8
+ - image: solid-color(13, 7, 0, 255, 8, 8)
+ bounds: 104 56 8 8
+ - image: solid-color(14, 7, 0, 255, 8, 8)
+ bounds: 112 56 8 8
+ - image: solid-color(15, 7, 0, 255, 8, 8)
+ bounds: 120 56 8 8
+ - image: solid-color(16, 7, 0, 255, 8, 8)
+ bounds: 128 56 8 8
+ - image: solid-color(17, 7, 0, 255, 8, 8)
+ bounds: 136 56 8 8
+ - image: solid-color(18, 7, 0, 255, 8, 8)
+ bounds: 144 56 8 8
+ - image: solid-color(19, 7, 0, 255, 8, 8)
+ bounds: 152 56 8 8
+ - image: solid-color(20, 7, 0, 255, 8, 8)
+ bounds: 160 56 8 8
+ - image: solid-color(21, 7, 0, 255, 8, 8)
+ bounds: 168 56 8 8
+ - image: solid-color(22, 7, 0, 255, 8, 8)
+ bounds: 176 56 8 8
+ - image: solid-color(23, 7, 0, 255, 8, 8)
+ bounds: 184 56 8 8
+ - image: solid-color(24, 7, 0, 255, 8, 8)
+ bounds: 192 56 8 8
+ - image: solid-color(25, 7, 0, 255, 8, 8)
+ bounds: 200 56 8 8
+ - image: solid-color(26, 7, 0, 255, 8, 8)
+ bounds: 208 56 8 8
+ - image: solid-color(27, 7, 0, 255, 8, 8)
+ bounds: 216 56 8 8
+ - image: solid-color(28, 7, 0, 255, 8, 8)
+ bounds: 224 56 8 8
+ - image: solid-color(29, 7, 0, 255, 8, 8)
+ bounds: 232 56 8 8
+ - image: solid-color(30, 7, 0, 255, 8, 8)
+ bounds: 240 56 8 8
+ - image: solid-color(31, 7, 0, 255, 8, 8)
+ bounds: 248 56 8 8
+ - image: solid-color(32, 7, 0, 255, 8, 8)
+ bounds: 256 56 8 8
+ - image: solid-color(33, 7, 0, 255, 8, 8)
+ bounds: 264 56 8 8
+ - image: solid-color(34, 7, 0, 255, 8, 8)
+ bounds: 272 56 8 8
+ - image: solid-color(35, 7, 0, 255, 8, 8)
+ bounds: 280 56 8 8
+ - image: solid-color(36, 7, 0, 255, 8, 8)
+ bounds: 288 56 8 8
+ - image: solid-color(37, 7, 0, 255, 8, 8)
+ bounds: 296 56 8 8
+ - image: solid-color(38, 7, 0, 255, 8, 8)
+ bounds: 304 56 8 8
+ - image: solid-color(39, 7, 0, 255, 8, 8)
+ bounds: 312 56 8 8
+ - image: solid-color(40, 7, 0, 255, 8, 8)
+ bounds: 320 56 8 8
+ - image: solid-color(41, 7, 0, 255, 8, 8)
+ bounds: 328 56 8 8
+ - image: solid-color(42, 7, 0, 255, 8, 8)
+ bounds: 336 56 8 8
+ - image: solid-color(43, 7, 0, 255, 8, 8)
+ bounds: 344 56 8 8
+ - image: solid-color(44, 7, 0, 255, 8, 8)
+ bounds: 352 56 8 8
+ - image: solid-color(45, 7, 0, 255, 8, 8)
+ bounds: 360 56 8 8
+ - image: solid-color(46, 7, 0, 255, 8, 8)
+ bounds: 368 56 8 8
+ - image: solid-color(47, 7, 0, 255, 8, 8)
+ bounds: 376 56 8 8
+ - image: solid-color(48, 7, 0, 255, 8, 8)
+ bounds: 384 56 8 8
+ - image: solid-color(49, 7, 0, 255, 8, 8)
+ bounds: 392 56 8 8
+ - image: solid-color(50, 7, 0, 255, 8, 8)
+ bounds: 400 56 8 8
+ - image: solid-color(51, 7, 0, 255, 8, 8)
+ bounds: 408 56 8 8
+ - image: solid-color(52, 7, 0, 255, 8, 8)
+ bounds: 416 56 8 8
+ - image: solid-color(53, 7, 0, 255, 8, 8)
+ bounds: 424 56 8 8
+ - image: solid-color(54, 7, 0, 255, 8, 8)
+ bounds: 432 56 8 8
+ - image: solid-color(55, 7, 0, 255, 8, 8)
+ bounds: 440 56 8 8
+ - image: solid-color(56, 7, 0, 255, 8, 8)
+ bounds: 448 56 8 8
+ - image: solid-color(57, 7, 0, 255, 8, 8)
+ bounds: 456 56 8 8
+ - image: solid-color(58, 7, 0, 255, 8, 8)
+ bounds: 464 56 8 8
+ - image: solid-color(59, 7, 0, 255, 8, 8)
+ bounds: 472 56 8 8
+ - image: solid-color(60, 7, 0, 255, 8, 8)
+ bounds: 480 56 8 8
+ - image: solid-color(61, 7, 0, 255, 8, 8)
+ bounds: 488 56 8 8
+ - image: solid-color(62, 7, 0, 255, 8, 8)
+ bounds: 496 56 8 8
+ - image: solid-color(63, 7, 0, 255, 8, 8)
+ bounds: 504 56 8 8
+ - image: solid-color(64, 7, 0, 255, 8, 8)
+ bounds: 512 56 8 8
+ - image: solid-color(65, 7, 0, 255, 8, 8)
+ bounds: 520 56 8 8
+ - image: solid-color(66, 7, 0, 255, 8, 8)
+ bounds: 528 56 8 8
+ - image: solid-color(67, 7, 0, 255, 8, 8)
+ bounds: 536 56 8 8
+ - image: solid-color(68, 7, 0, 255, 8, 8)
+ bounds: 544 56 8 8
+ - image: solid-color(69, 7, 0, 255, 8, 8)
+ bounds: 552 56 8 8
+ - image: solid-color(70, 7, 0, 255, 8, 8)
+ bounds: 560 56 8 8
+ - image: solid-color(71, 7, 0, 255, 8, 8)
+ bounds: 568 56 8 8
+ - image: solid-color(72, 7, 0, 255, 8, 8)
+ bounds: 576 56 8 8
+ - image: solid-color(73, 7, 0, 255, 8, 8)
+ bounds: 584 56 8 8
+ - image: solid-color(74, 7, 0, 255, 8, 8)
+ bounds: 592 56 8 8
+ - image: solid-color(75, 7, 0, 255, 8, 8)
+ bounds: 600 56 8 8
+ - image: solid-color(76, 7, 0, 255, 8, 8)
+ bounds: 608 56 8 8
+ - image: solid-color(77, 7, 0, 255, 8, 8)
+ bounds: 616 56 8 8
+ - image: solid-color(78, 7, 0, 255, 8, 8)
+ bounds: 624 56 8 8
+ - image: solid-color(79, 7, 0, 255, 8, 8)
+ bounds: 632 56 8 8
+ - image: solid-color(80, 7, 0, 255, 8, 8)
+ bounds: 640 56 8 8
+ - image: solid-color(81, 7, 0, 255, 8, 8)
+ bounds: 648 56 8 8
+ - image: solid-color(82, 7, 0, 255, 8, 8)
+ bounds: 656 56 8 8
+ - image: solid-color(83, 7, 0, 255, 8, 8)
+ bounds: 664 56 8 8
+ - image: solid-color(84, 7, 0, 255, 8, 8)
+ bounds: 672 56 8 8
+ - image: solid-color(85, 7, 0, 255, 8, 8)
+ bounds: 680 56 8 8
+ - image: solid-color(86, 7, 0, 255, 8, 8)
+ bounds: 688 56 8 8
+ - image: solid-color(87, 7, 0, 255, 8, 8)
+ bounds: 696 56 8 8
+ - image: solid-color(88, 7, 0, 255, 8, 8)
+ bounds: 704 56 8 8
+ - image: solid-color(89, 7, 0, 255, 8, 8)
+ bounds: 712 56 8 8
+ - image: solid-color(90, 7, 0, 255, 8, 8)
+ bounds: 720 56 8 8
+ - image: solid-color(91, 7, 0, 255, 8, 8)
+ bounds: 728 56 8 8
+ - image: solid-color(92, 7, 0, 255, 8, 8)
+ bounds: 736 56 8 8
+ - image: solid-color(93, 7, 0, 255, 8, 8)
+ bounds: 744 56 8 8
+ - image: solid-color(94, 7, 0, 255, 8, 8)
+ bounds: 752 56 8 8
+ - image: solid-color(95, 7, 0, 255, 8, 8)
+ bounds: 760 56 8 8
+ - image: solid-color(96, 7, 0, 255, 8, 8)
+ bounds: 768 56 8 8
+ - image: solid-color(97, 7, 0, 255, 8, 8)
+ bounds: 776 56 8 8
+ - image: solid-color(98, 7, 0, 255, 8, 8)
+ bounds: 784 56 8 8
+ - image: solid-color(99, 7, 0, 255, 8, 8)
+ bounds: 792 56 8 8
+ - image: solid-color(100, 7, 0, 255, 8, 8)
+ bounds: 800 56 8 8
+ - image: solid-color(101, 7, 0, 255, 8, 8)
+ bounds: 808 56 8 8
+ - image: solid-color(102, 7, 0, 255, 8, 8)
+ bounds: 816 56 8 8
+ - image: solid-color(103, 7, 0, 255, 8, 8)
+ bounds: 824 56 8 8
+ - image: solid-color(104, 7, 0, 255, 8, 8)
+ bounds: 832 56 8 8
+ - image: solid-color(105, 7, 0, 255, 8, 8)
+ bounds: 840 56 8 8
+ - image: solid-color(106, 7, 0, 255, 8, 8)
+ bounds: 848 56 8 8
+ - image: solid-color(107, 7, 0, 255, 8, 8)
+ bounds: 856 56 8 8
+ - image: solid-color(108, 7, 0, 255, 8, 8)
+ bounds: 864 56 8 8
+ - image: solid-color(109, 7, 0, 255, 8, 8)
+ bounds: 872 56 8 8
+ - image: solid-color(110, 7, 0, 255, 8, 8)
+ bounds: 880 56 8 8
+ - image: solid-color(111, 7, 0, 255, 8, 8)
+ bounds: 888 56 8 8
+ - image: solid-color(112, 7, 0, 255, 8, 8)
+ bounds: 896 56 8 8
+ - image: solid-color(113, 7, 0, 255, 8, 8)
+ bounds: 904 56 8 8
+ - image: solid-color(114, 7, 0, 255, 8, 8)
+ bounds: 912 56 8 8
+ - image: solid-color(115, 7, 0, 255, 8, 8)
+ bounds: 920 56 8 8
+ - image: solid-color(116, 7, 0, 255, 8, 8)
+ bounds: 928 56 8 8
+ - image: solid-color(117, 7, 0, 255, 8, 8)
+ bounds: 936 56 8 8
+ - image: solid-color(118, 7, 0, 255, 8, 8)
+ bounds: 944 56 8 8
+ - image: solid-color(119, 7, 0, 255, 8, 8)
+ bounds: 952 56 8 8
+ - image: solid-color(120, 7, 0, 255, 8, 8)
+ bounds: 960 56 8 8
+ - image: solid-color(121, 7, 0, 255, 8, 8)
+ bounds: 968 56 8 8
+ - image: solid-color(122, 7, 0, 255, 8, 8)
+ bounds: 976 56 8 8
+ - image: solid-color(123, 7, 0, 255, 8, 8)
+ bounds: 984 56 8 8
+ - image: solid-color(124, 7, 0, 255, 8, 8)
+ bounds: 992 56 8 8
+ - image: solid-color(125, 7, 0, 255, 8, 8)
+ bounds: 1000 56 8 8
+ - image: solid-color(126, 7, 0, 255, 8, 8)
+ bounds: 1008 56 8 8
+ - image: solid-color(127, 7, 0, 255, 8, 8)
+ bounds: 1016 56 8 8
+ - image: solid-color(0, 8, 0, 255, 8, 8)
+ bounds: 0 64 8 8
+ - image: solid-color(1, 8, 0, 255, 8, 8)
+ bounds: 8 64 8 8
+ - image: solid-color(2, 8, 0, 255, 8, 8)
+ bounds: 16 64 8 8
+ - image: solid-color(3, 8, 0, 255, 8, 8)
+ bounds: 24 64 8 8
+ - image: solid-color(4, 8, 0, 255, 8, 8)
+ bounds: 32 64 8 8
+ - image: solid-color(5, 8, 0, 255, 8, 8)
+ bounds: 40 64 8 8
+ - image: solid-color(6, 8, 0, 255, 8, 8)
+ bounds: 48 64 8 8
+ - image: solid-color(7, 8, 0, 255, 8, 8)
+ bounds: 56 64 8 8
+ - image: solid-color(8, 8, 0, 255, 8, 8)
+ bounds: 64 64 8 8
+ - image: solid-color(9, 8, 0, 255, 8, 8)
+ bounds: 72 64 8 8
+ - image: solid-color(10, 8, 0, 255, 8, 8)
+ bounds: 80 64 8 8
+ - image: solid-color(11, 8, 0, 255, 8, 8)
+ bounds: 88 64 8 8
+ - image: solid-color(12, 8, 0, 255, 8, 8)
+ bounds: 96 64 8 8
+ - image: solid-color(13, 8, 0, 255, 8, 8)
+ bounds: 104 64 8 8
+ - image: solid-color(14, 8, 0, 255, 8, 8)
+ bounds: 112 64 8 8
+ - image: solid-color(15, 8, 0, 255, 8, 8)
+ bounds: 120 64 8 8
+ - image: solid-color(16, 8, 0, 255, 8, 8)
+ bounds: 128 64 8 8
+ - image: solid-color(17, 8, 0, 255, 8, 8)
+ bounds: 136 64 8 8
+ - image: solid-color(18, 8, 0, 255, 8, 8)
+ bounds: 144 64 8 8
+ - image: solid-color(19, 8, 0, 255, 8, 8)
+ bounds: 152 64 8 8
+ - image: solid-color(20, 8, 0, 255, 8, 8)
+ bounds: 160 64 8 8
+ - image: solid-color(21, 8, 0, 255, 8, 8)
+ bounds: 168 64 8 8
+ - image: solid-color(22, 8, 0, 255, 8, 8)
+ bounds: 176 64 8 8
+ - image: solid-color(23, 8, 0, 255, 8, 8)
+ bounds: 184 64 8 8
+ - image: solid-color(24, 8, 0, 255, 8, 8)
+ bounds: 192 64 8 8
+ - image: solid-color(25, 8, 0, 255, 8, 8)
+ bounds: 200 64 8 8
+ - image: solid-color(26, 8, 0, 255, 8, 8)
+ bounds: 208 64 8 8
+ - image: solid-color(27, 8, 0, 255, 8, 8)
+ bounds: 216 64 8 8
+ - image: solid-color(28, 8, 0, 255, 8, 8)
+ bounds: 224 64 8 8
+ - image: solid-color(29, 8, 0, 255, 8, 8)
+ bounds: 232 64 8 8
+ - image: solid-color(30, 8, 0, 255, 8, 8)
+ bounds: 240 64 8 8
+ - image: solid-color(31, 8, 0, 255, 8, 8)
+ bounds: 248 64 8 8
+ - image: solid-color(32, 8, 0, 255, 8, 8)
+ bounds: 256 64 8 8
+ - image: solid-color(33, 8, 0, 255, 8, 8)
+ bounds: 264 64 8 8
+ - image: solid-color(34, 8, 0, 255, 8, 8)
+ bounds: 272 64 8 8
+ - image: solid-color(35, 8, 0, 255, 8, 8)
+ bounds: 280 64 8 8
+ - image: solid-color(36, 8, 0, 255, 8, 8)
+ bounds: 288 64 8 8
+ - image: solid-color(37, 8, 0, 255, 8, 8)
+ bounds: 296 64 8 8
+ - image: solid-color(38, 8, 0, 255, 8, 8)
+ bounds: 304 64 8 8
+ - image: solid-color(39, 8, 0, 255, 8, 8)
+ bounds: 312 64 8 8
+ - image: solid-color(40, 8, 0, 255, 8, 8)
+ bounds: 320 64 8 8
+ - image: solid-color(41, 8, 0, 255, 8, 8)
+ bounds: 328 64 8 8
+ - image: solid-color(42, 8, 0, 255, 8, 8)
+ bounds: 336 64 8 8
+ - image: solid-color(43, 8, 0, 255, 8, 8)
+ bounds: 344 64 8 8
+ - image: solid-color(44, 8, 0, 255, 8, 8)
+ bounds: 352 64 8 8
+ - image: solid-color(45, 8, 0, 255, 8, 8)
+ bounds: 360 64 8 8
+ - image: solid-color(46, 8, 0, 255, 8, 8)
+ bounds: 368 64 8 8
+ - image: solid-color(47, 8, 0, 255, 8, 8)
+ bounds: 376 64 8 8
+ - image: solid-color(48, 8, 0, 255, 8, 8)
+ bounds: 384 64 8 8
+ - image: solid-color(49, 8, 0, 255, 8, 8)
+ bounds: 392 64 8 8
+ - image: solid-color(50, 8, 0, 255, 8, 8)
+ bounds: 400 64 8 8
+ - image: solid-color(51, 8, 0, 255, 8, 8)
+ bounds: 408 64 8 8
+ - image: solid-color(52, 8, 0, 255, 8, 8)
+ bounds: 416 64 8 8
+ - image: solid-color(53, 8, 0, 255, 8, 8)
+ bounds: 424 64 8 8
+ - image: solid-color(54, 8, 0, 255, 8, 8)
+ bounds: 432 64 8 8
+ - image: solid-color(55, 8, 0, 255, 8, 8)
+ bounds: 440 64 8 8
+ - image: solid-color(56, 8, 0, 255, 8, 8)
+ bounds: 448 64 8 8
+ - image: solid-color(57, 8, 0, 255, 8, 8)
+ bounds: 456 64 8 8
+ - image: solid-color(58, 8, 0, 255, 8, 8)
+ bounds: 464 64 8 8
+ - image: solid-color(59, 8, 0, 255, 8, 8)
+ bounds: 472 64 8 8
+ - image: solid-color(60, 8, 0, 255, 8, 8)
+ bounds: 480 64 8 8
+ - image: solid-color(61, 8, 0, 255, 8, 8)
+ bounds: 488 64 8 8
+ - image: solid-color(62, 8, 0, 255, 8, 8)
+ bounds: 496 64 8 8
+ - image: solid-color(63, 8, 0, 255, 8, 8)
+ bounds: 504 64 8 8
+ - image: solid-color(64, 8, 0, 255, 8, 8)
+ bounds: 512 64 8 8
+ - image: solid-color(65, 8, 0, 255, 8, 8)
+ bounds: 520 64 8 8
+ - image: solid-color(66, 8, 0, 255, 8, 8)
+ bounds: 528 64 8 8
+ - image: solid-color(67, 8, 0, 255, 8, 8)
+ bounds: 536 64 8 8
+ - image: solid-color(68, 8, 0, 255, 8, 8)
+ bounds: 544 64 8 8
+ - image: solid-color(69, 8, 0, 255, 8, 8)
+ bounds: 552 64 8 8
+ - image: solid-color(70, 8, 0, 255, 8, 8)
+ bounds: 560 64 8 8
+ - image: solid-color(71, 8, 0, 255, 8, 8)
+ bounds: 568 64 8 8
+ - image: solid-color(72, 8, 0, 255, 8, 8)
+ bounds: 576 64 8 8
+ - image: solid-color(73, 8, 0, 255, 8, 8)
+ bounds: 584 64 8 8
+ - image: solid-color(74, 8, 0, 255, 8, 8)
+ bounds: 592 64 8 8
+ - image: solid-color(75, 8, 0, 255, 8, 8)
+ bounds: 600 64 8 8
+ - image: solid-color(76, 8, 0, 255, 8, 8)
+ bounds: 608 64 8 8
+ - image: solid-color(77, 8, 0, 255, 8, 8)
+ bounds: 616 64 8 8
+ - image: solid-color(78, 8, 0, 255, 8, 8)
+ bounds: 624 64 8 8
+ - image: solid-color(79, 8, 0, 255, 8, 8)
+ bounds: 632 64 8 8
+ - image: solid-color(80, 8, 0, 255, 8, 8)
+ bounds: 640 64 8 8
+ - image: solid-color(81, 8, 0, 255, 8, 8)
+ bounds: 648 64 8 8
+ - image: solid-color(82, 8, 0, 255, 8, 8)
+ bounds: 656 64 8 8
+ - image: solid-color(83, 8, 0, 255, 8, 8)
+ bounds: 664 64 8 8
+ - image: solid-color(84, 8, 0, 255, 8, 8)
+ bounds: 672 64 8 8
+ - image: solid-color(85, 8, 0, 255, 8, 8)
+ bounds: 680 64 8 8
+ - image: solid-color(86, 8, 0, 255, 8, 8)
+ bounds: 688 64 8 8
+ - image: solid-color(87, 8, 0, 255, 8, 8)
+ bounds: 696 64 8 8
+ - image: solid-color(88, 8, 0, 255, 8, 8)
+ bounds: 704 64 8 8
+ - image: solid-color(89, 8, 0, 255, 8, 8)
+ bounds: 712 64 8 8
+ - image: solid-color(90, 8, 0, 255, 8, 8)
+ bounds: 720 64 8 8
+ - image: solid-color(91, 8, 0, 255, 8, 8)
+ bounds: 728 64 8 8
+ - image: solid-color(92, 8, 0, 255, 8, 8)
+ bounds: 736 64 8 8
+ - image: solid-color(93, 8, 0, 255, 8, 8)
+ bounds: 744 64 8 8
+ - image: solid-color(94, 8, 0, 255, 8, 8)
+ bounds: 752 64 8 8
+ - image: solid-color(95, 8, 0, 255, 8, 8)
+ bounds: 760 64 8 8
+ - image: solid-color(96, 8, 0, 255, 8, 8)
+ bounds: 768 64 8 8
+ - image: solid-color(97, 8, 0, 255, 8, 8)
+ bounds: 776 64 8 8
+ - image: solid-color(98, 8, 0, 255, 8, 8)
+ bounds: 784 64 8 8
+ - image: solid-color(99, 8, 0, 255, 8, 8)
+ bounds: 792 64 8 8
+ - image: solid-color(100, 8, 0, 255, 8, 8)
+ bounds: 800 64 8 8
+ - image: solid-color(101, 8, 0, 255, 8, 8)
+ bounds: 808 64 8 8
+ - image: solid-color(102, 8, 0, 255, 8, 8)
+ bounds: 816 64 8 8
+ - image: solid-color(103, 8, 0, 255, 8, 8)
+ bounds: 824 64 8 8
+ - image: solid-color(104, 8, 0, 255, 8, 8)
+ bounds: 832 64 8 8
+ - image: solid-color(105, 8, 0, 255, 8, 8)
+ bounds: 840 64 8 8
+ - image: solid-color(106, 8, 0, 255, 8, 8)
+ bounds: 848 64 8 8
+ - image: solid-color(107, 8, 0, 255, 8, 8)
+ bounds: 856 64 8 8
+ - image: solid-color(108, 8, 0, 255, 8, 8)
+ bounds: 864 64 8 8
+ - image: solid-color(109, 8, 0, 255, 8, 8)
+ bounds: 872 64 8 8
+ - image: solid-color(110, 8, 0, 255, 8, 8)
+ bounds: 880 64 8 8
+ - image: solid-color(111, 8, 0, 255, 8, 8)
+ bounds: 888 64 8 8
+ - image: solid-color(112, 8, 0, 255, 8, 8)
+ bounds: 896 64 8 8
+ - image: solid-color(113, 8, 0, 255, 8, 8)
+ bounds: 904 64 8 8
+ - image: solid-color(114, 8, 0, 255, 8, 8)
+ bounds: 912 64 8 8
+ - image: solid-color(115, 8, 0, 255, 8, 8)
+ bounds: 920 64 8 8
+ - image: solid-color(116, 8, 0, 255, 8, 8)
+ bounds: 928 64 8 8
+ - image: solid-color(117, 8, 0, 255, 8, 8)
+ bounds: 936 64 8 8
+ - image: solid-color(118, 8, 0, 255, 8, 8)
+ bounds: 944 64 8 8
+ - image: solid-color(119, 8, 0, 255, 8, 8)
+ bounds: 952 64 8 8
+ - image: solid-color(120, 8, 0, 255, 8, 8)
+ bounds: 960 64 8 8
+ - image: solid-color(121, 8, 0, 255, 8, 8)
+ bounds: 968 64 8 8
+ - image: solid-color(122, 8, 0, 255, 8, 8)
+ bounds: 976 64 8 8
+ - image: solid-color(123, 8, 0, 255, 8, 8)
+ bounds: 984 64 8 8
+ - image: solid-color(124, 8, 0, 255, 8, 8)
+ bounds: 992 64 8 8
+ - image: solid-color(125, 8, 0, 255, 8, 8)
+ bounds: 1000 64 8 8
+ - image: solid-color(126, 8, 0, 255, 8, 8)
+ bounds: 1008 64 8 8
+ - image: solid-color(127, 8, 0, 255, 8, 8)
+ bounds: 1016 64 8 8
+ - image: solid-color(0, 9, 0, 255, 8, 8)
+ bounds: 0 72 8 8
+ - image: solid-color(1, 9, 0, 255, 8, 8)
+ bounds: 8 72 8 8
+ - image: solid-color(2, 9, 0, 255, 8, 8)
+ bounds: 16 72 8 8
+ - image: solid-color(3, 9, 0, 255, 8, 8)
+ bounds: 24 72 8 8
+ - image: solid-color(4, 9, 0, 255, 8, 8)
+ bounds: 32 72 8 8
+ - image: solid-color(5, 9, 0, 255, 8, 8)
+ bounds: 40 72 8 8
+ - image: solid-color(6, 9, 0, 255, 8, 8)
+ bounds: 48 72 8 8
+ - image: solid-color(7, 9, 0, 255, 8, 8)
+ bounds: 56 72 8 8
+ - image: solid-color(8, 9, 0, 255, 8, 8)
+ bounds: 64 72 8 8
+ - image: solid-color(9, 9, 0, 255, 8, 8)
+ bounds: 72 72 8 8
+ - image: solid-color(10, 9, 0, 255, 8, 8)
+ bounds: 80 72 8 8
+ - image: solid-color(11, 9, 0, 255, 8, 8)
+ bounds: 88 72 8 8
+ - image: solid-color(12, 9, 0, 255, 8, 8)
+ bounds: 96 72 8 8
+ - image: solid-color(13, 9, 0, 255, 8, 8)
+ bounds: 104 72 8 8
+ - image: solid-color(14, 9, 0, 255, 8, 8)
+ bounds: 112 72 8 8
+ - image: solid-color(15, 9, 0, 255, 8, 8)
+ bounds: 120 72 8 8
+ - image: solid-color(16, 9, 0, 255, 8, 8)
+ bounds: 128 72 8 8
+ - image: solid-color(17, 9, 0, 255, 8, 8)
+ bounds: 136 72 8 8
+ - image: solid-color(18, 9, 0, 255, 8, 8)
+ bounds: 144 72 8 8
+ - image: solid-color(19, 9, 0, 255, 8, 8)
+ bounds: 152 72 8 8
+ - image: solid-color(20, 9, 0, 255, 8, 8)
+ bounds: 160 72 8 8
+ - image: solid-color(21, 9, 0, 255, 8, 8)
+ bounds: 168 72 8 8
+ - image: solid-color(22, 9, 0, 255, 8, 8)
+ bounds: 176 72 8 8
+ - image: solid-color(23, 9, 0, 255, 8, 8)
+ bounds: 184 72 8 8
+ - image: solid-color(24, 9, 0, 255, 8, 8)
+ bounds: 192 72 8 8
+ - image: solid-color(25, 9, 0, 255, 8, 8)
+ bounds: 200 72 8 8
+ - image: solid-color(26, 9, 0, 255, 8, 8)
+ bounds: 208 72 8 8
+ - image: solid-color(27, 9, 0, 255, 8, 8)
+ bounds: 216 72 8 8
+ - image: solid-color(28, 9, 0, 255, 8, 8)
+ bounds: 224 72 8 8
+ - image: solid-color(29, 9, 0, 255, 8, 8)
+ bounds: 232 72 8 8
+ - image: solid-color(30, 9, 0, 255, 8, 8)
+ bounds: 240 72 8 8
+ - image: solid-color(31, 9, 0, 255, 8, 8)
+ bounds: 248 72 8 8
+ - image: solid-color(32, 9, 0, 255, 8, 8)
+ bounds: 256 72 8 8
+ - image: solid-color(33, 9, 0, 255, 8, 8)
+ bounds: 264 72 8 8
+ - image: solid-color(34, 9, 0, 255, 8, 8)
+ bounds: 272 72 8 8
+ - image: solid-color(35, 9, 0, 255, 8, 8)
+ bounds: 280 72 8 8
+ - image: solid-color(36, 9, 0, 255, 8, 8)
+ bounds: 288 72 8 8
+ - image: solid-color(37, 9, 0, 255, 8, 8)
+ bounds: 296 72 8 8
+ - image: solid-color(38, 9, 0, 255, 8, 8)
+ bounds: 304 72 8 8
+ - image: solid-color(39, 9, 0, 255, 8, 8)
+ bounds: 312 72 8 8
+ - image: solid-color(40, 9, 0, 255, 8, 8)
+ bounds: 320 72 8 8
+ - image: solid-color(41, 9, 0, 255, 8, 8)
+ bounds: 328 72 8 8
+ - image: solid-color(42, 9, 0, 255, 8, 8)
+ bounds: 336 72 8 8
+ - image: solid-color(43, 9, 0, 255, 8, 8)
+ bounds: 344 72 8 8
+ - image: solid-color(44, 9, 0, 255, 8, 8)
+ bounds: 352 72 8 8
+ - image: solid-color(45, 9, 0, 255, 8, 8)
+ bounds: 360 72 8 8
+ - image: solid-color(46, 9, 0, 255, 8, 8)
+ bounds: 368 72 8 8
+ - image: solid-color(47, 9, 0, 255, 8, 8)
+ bounds: 376 72 8 8
+ - image: solid-color(48, 9, 0, 255, 8, 8)
+ bounds: 384 72 8 8
+ - image: solid-color(49, 9, 0, 255, 8, 8)
+ bounds: 392 72 8 8
+ - image: solid-color(50, 9, 0, 255, 8, 8)
+ bounds: 400 72 8 8
+ - image: solid-color(51, 9, 0, 255, 8, 8)
+ bounds: 408 72 8 8
+ - image: solid-color(52, 9, 0, 255, 8, 8)
+ bounds: 416 72 8 8
+ - image: solid-color(53, 9, 0, 255, 8, 8)
+ bounds: 424 72 8 8
+ - image: solid-color(54, 9, 0, 255, 8, 8)
+ bounds: 432 72 8 8
+ - image: solid-color(55, 9, 0, 255, 8, 8)
+ bounds: 440 72 8 8
+ - image: solid-color(56, 9, 0, 255, 8, 8)
+ bounds: 448 72 8 8
+ - image: solid-color(57, 9, 0, 255, 8, 8)
+ bounds: 456 72 8 8
+ - image: solid-color(58, 9, 0, 255, 8, 8)
+ bounds: 464 72 8 8
+ - image: solid-color(59, 9, 0, 255, 8, 8)
+ bounds: 472 72 8 8
+ - image: solid-color(60, 9, 0, 255, 8, 8)
+ bounds: 480 72 8 8
+ - image: solid-color(61, 9, 0, 255, 8, 8)
+ bounds: 488 72 8 8
+ - image: solid-color(62, 9, 0, 255, 8, 8)
+ bounds: 496 72 8 8
+ - image: solid-color(63, 9, 0, 255, 8, 8)
+ bounds: 504 72 8 8
+ - image: solid-color(64, 9, 0, 255, 8, 8)
+ bounds: 512 72 8 8
+ - image: solid-color(65, 9, 0, 255, 8, 8)
+ bounds: 520 72 8 8
+ - image: solid-color(66, 9, 0, 255, 8, 8)
+ bounds: 528 72 8 8
+ - image: solid-color(67, 9, 0, 255, 8, 8)
+ bounds: 536 72 8 8
+ - image: solid-color(68, 9, 0, 255, 8, 8)
+ bounds: 544 72 8 8
+ - image: solid-color(69, 9, 0, 255, 8, 8)
+ bounds: 552 72 8 8
+ - image: solid-color(70, 9, 0, 255, 8, 8)
+ bounds: 560 72 8 8
+ - image: solid-color(71, 9, 0, 255, 8, 8)
+ bounds: 568 72 8 8
+ - image: solid-color(72, 9, 0, 255, 8, 8)
+ bounds: 576 72 8 8
+ - image: solid-color(73, 9, 0, 255, 8, 8)
+ bounds: 584 72 8 8
+ - image: solid-color(74, 9, 0, 255, 8, 8)
+ bounds: 592 72 8 8
+ - image: solid-color(75, 9, 0, 255, 8, 8)
+ bounds: 600 72 8 8
+ - image: solid-color(76, 9, 0, 255, 8, 8)
+ bounds: 608 72 8 8
+ - image: solid-color(77, 9, 0, 255, 8, 8)
+ bounds: 616 72 8 8
+ - image: solid-color(78, 9, 0, 255, 8, 8)
+ bounds: 624 72 8 8
+ - image: solid-color(79, 9, 0, 255, 8, 8)
+ bounds: 632 72 8 8
+ - image: solid-color(80, 9, 0, 255, 8, 8)
+ bounds: 640 72 8 8
+ - image: solid-color(81, 9, 0, 255, 8, 8)
+ bounds: 648 72 8 8
+ - image: solid-color(82, 9, 0, 255, 8, 8)
+ bounds: 656 72 8 8
+ - image: solid-color(83, 9, 0, 255, 8, 8)
+ bounds: 664 72 8 8
+ - image: solid-color(84, 9, 0, 255, 8, 8)
+ bounds: 672 72 8 8
+ - image: solid-color(85, 9, 0, 255, 8, 8)
+ bounds: 680 72 8 8
+ - image: solid-color(86, 9, 0, 255, 8, 8)
+ bounds: 688 72 8 8
+ - image: solid-color(87, 9, 0, 255, 8, 8)
+ bounds: 696 72 8 8
+ - image: solid-color(88, 9, 0, 255, 8, 8)
+ bounds: 704 72 8 8
+ - image: solid-color(89, 9, 0, 255, 8, 8)
+ bounds: 712 72 8 8
+ - image: solid-color(90, 9, 0, 255, 8, 8)
+ bounds: 720 72 8 8
+ - image: solid-color(91, 9, 0, 255, 8, 8)
+ bounds: 728 72 8 8
+ - image: solid-color(92, 9, 0, 255, 8, 8)
+ bounds: 736 72 8 8
+ - image: solid-color(93, 9, 0, 255, 8, 8)
+ bounds: 744 72 8 8
+ - image: solid-color(94, 9, 0, 255, 8, 8)
+ bounds: 752 72 8 8
+ - image: solid-color(95, 9, 0, 255, 8, 8)
+ bounds: 760 72 8 8
+ - image: solid-color(96, 9, 0, 255, 8, 8)
+ bounds: 768 72 8 8
+ - image: solid-color(97, 9, 0, 255, 8, 8)
+ bounds: 776 72 8 8
+ - image: solid-color(98, 9, 0, 255, 8, 8)
+ bounds: 784 72 8 8
+ - image: solid-color(99, 9, 0, 255, 8, 8)
+ bounds: 792 72 8 8
+ - image: solid-color(100, 9, 0, 255, 8, 8)
+ bounds: 800 72 8 8
+ - image: solid-color(101, 9, 0, 255, 8, 8)
+ bounds: 808 72 8 8
+ - image: solid-color(102, 9, 0, 255, 8, 8)
+ bounds: 816 72 8 8
+ - image: solid-color(103, 9, 0, 255, 8, 8)
+ bounds: 824 72 8 8
+ - image: solid-color(104, 9, 0, 255, 8, 8)
+ bounds: 832 72 8 8
+ - image: solid-color(105, 9, 0, 255, 8, 8)
+ bounds: 840 72 8 8
+ - image: solid-color(106, 9, 0, 255, 8, 8)
+ bounds: 848 72 8 8
+ - image: solid-color(107, 9, 0, 255, 8, 8)
+ bounds: 856 72 8 8
+ - image: solid-color(108, 9, 0, 255, 8, 8)
+ bounds: 864 72 8 8
+ - image: solid-color(109, 9, 0, 255, 8, 8)
+ bounds: 872 72 8 8
+ - image: solid-color(110, 9, 0, 255, 8, 8)
+ bounds: 880 72 8 8
+ - image: solid-color(111, 9, 0, 255, 8, 8)
+ bounds: 888 72 8 8
+ - image: solid-color(112, 9, 0, 255, 8, 8)
+ bounds: 896 72 8 8
+ - image: solid-color(113, 9, 0, 255, 8, 8)
+ bounds: 904 72 8 8
+ - image: solid-color(114, 9, 0, 255, 8, 8)
+ bounds: 912 72 8 8
+ - image: solid-color(115, 9, 0, 255, 8, 8)
+ bounds: 920 72 8 8
+ - image: solid-color(116, 9, 0, 255, 8, 8)
+ bounds: 928 72 8 8
+ - image: solid-color(117, 9, 0, 255, 8, 8)
+ bounds: 936 72 8 8
+ - image: solid-color(118, 9, 0, 255, 8, 8)
+ bounds: 944 72 8 8
+ - image: solid-color(119, 9, 0, 255, 8, 8)
+ bounds: 952 72 8 8
+ - image: solid-color(120, 9, 0, 255, 8, 8)
+ bounds: 960 72 8 8
+ - image: solid-color(121, 9, 0, 255, 8, 8)
+ bounds: 968 72 8 8
+ - image: solid-color(122, 9, 0, 255, 8, 8)
+ bounds: 976 72 8 8
+ - image: solid-color(123, 9, 0, 255, 8, 8)
+ bounds: 984 72 8 8
+ - image: solid-color(124, 9, 0, 255, 8, 8)
+ bounds: 992 72 8 8
+ - image: solid-color(125, 9, 0, 255, 8, 8)
+ bounds: 1000 72 8 8
+ - image: solid-color(126, 9, 0, 255, 8, 8)
+ bounds: 1008 72 8 8
+ - image: solid-color(127, 9, 0, 255, 8, 8)
+ bounds: 1016 72 8 8
+ - image: solid-color(0, 10, 0, 255, 8, 8)
+ bounds: 0 80 8 8
+ - image: solid-color(1, 10, 0, 255, 8, 8)
+ bounds: 8 80 8 8
+ - image: solid-color(2, 10, 0, 255, 8, 8)
+ bounds: 16 80 8 8
+ - image: solid-color(3, 10, 0, 255, 8, 8)
+ bounds: 24 80 8 8
+ - image: solid-color(4, 10, 0, 255, 8, 8)
+ bounds: 32 80 8 8
+ - image: solid-color(5, 10, 0, 255, 8, 8)
+ bounds: 40 80 8 8
+ - image: solid-color(6, 10, 0, 255, 8, 8)
+ bounds: 48 80 8 8
+ - image: solid-color(7, 10, 0, 255, 8, 8)
+ bounds: 56 80 8 8
+ - image: solid-color(8, 10, 0, 255, 8, 8)
+ bounds: 64 80 8 8
+ - image: solid-color(9, 10, 0, 255, 8, 8)
+ bounds: 72 80 8 8
+ - image: solid-color(10, 10, 0, 255, 8, 8)
+ bounds: 80 80 8 8
+ - image: solid-color(11, 10, 0, 255, 8, 8)
+ bounds: 88 80 8 8
+ - image: solid-color(12, 10, 0, 255, 8, 8)
+ bounds: 96 80 8 8
+ - image: solid-color(13, 10, 0, 255, 8, 8)
+ bounds: 104 80 8 8
+ - image: solid-color(14, 10, 0, 255, 8, 8)
+ bounds: 112 80 8 8
+ - image: solid-color(15, 10, 0, 255, 8, 8)
+ bounds: 120 80 8 8
+ - image: solid-color(16, 10, 0, 255, 8, 8)
+ bounds: 128 80 8 8
+ - image: solid-color(17, 10, 0, 255, 8, 8)
+ bounds: 136 80 8 8
+ - image: solid-color(18, 10, 0, 255, 8, 8)
+ bounds: 144 80 8 8
+ - image: solid-color(19, 10, 0, 255, 8, 8)
+ bounds: 152 80 8 8
+ - image: solid-color(20, 10, 0, 255, 8, 8)
+ bounds: 160 80 8 8
+ - image: solid-color(21, 10, 0, 255, 8, 8)
+ bounds: 168 80 8 8
+ - image: solid-color(22, 10, 0, 255, 8, 8)
+ bounds: 176 80 8 8
+ - image: solid-color(23, 10, 0, 255, 8, 8)
+ bounds: 184 80 8 8
+ - image: solid-color(24, 10, 0, 255, 8, 8)
+ bounds: 192 80 8 8
+ - image: solid-color(25, 10, 0, 255, 8, 8)
+ bounds: 200 80 8 8
+ - image: solid-color(26, 10, 0, 255, 8, 8)
+ bounds: 208 80 8 8
+ - image: solid-color(27, 10, 0, 255, 8, 8)
+ bounds: 216 80 8 8
+ - image: solid-color(28, 10, 0, 255, 8, 8)
+ bounds: 224 80 8 8
+ - image: solid-color(29, 10, 0, 255, 8, 8)
+ bounds: 232 80 8 8
+ - image: solid-color(30, 10, 0, 255, 8, 8)
+ bounds: 240 80 8 8
+ - image: solid-color(31, 10, 0, 255, 8, 8)
+ bounds: 248 80 8 8
+ - image: solid-color(32, 10, 0, 255, 8, 8)
+ bounds: 256 80 8 8
+ - image: solid-color(33, 10, 0, 255, 8, 8)
+ bounds: 264 80 8 8
+ - image: solid-color(34, 10, 0, 255, 8, 8)
+ bounds: 272 80 8 8
+ - image: solid-color(35, 10, 0, 255, 8, 8)
+ bounds: 280 80 8 8
+ - image: solid-color(36, 10, 0, 255, 8, 8)
+ bounds: 288 80 8 8
+ - image: solid-color(37, 10, 0, 255, 8, 8)
+ bounds: 296 80 8 8
+ - image: solid-color(38, 10, 0, 255, 8, 8)
+ bounds: 304 80 8 8
+ - image: solid-color(39, 10, 0, 255, 8, 8)
+ bounds: 312 80 8 8
+ - image: solid-color(40, 10, 0, 255, 8, 8)
+ bounds: 320 80 8 8
+ - image: solid-color(41, 10, 0, 255, 8, 8)
+ bounds: 328 80 8 8
+ - image: solid-color(42, 10, 0, 255, 8, 8)
+ bounds: 336 80 8 8
+ - image: solid-color(43, 10, 0, 255, 8, 8)
+ bounds: 344 80 8 8
+ - image: solid-color(44, 10, 0, 255, 8, 8)
+ bounds: 352 80 8 8
+ - image: solid-color(45, 10, 0, 255, 8, 8)
+ bounds: 360 80 8 8
+ - image: solid-color(46, 10, 0, 255, 8, 8)
+ bounds: 368 80 8 8
+ - image: solid-color(47, 10, 0, 255, 8, 8)
+ bounds: 376 80 8 8
+ - image: solid-color(48, 10, 0, 255, 8, 8)
+ bounds: 384 80 8 8
+ - image: solid-color(49, 10, 0, 255, 8, 8)
+ bounds: 392 80 8 8
+ - image: solid-color(50, 10, 0, 255, 8, 8)
+ bounds: 400 80 8 8
+ - image: solid-color(51, 10, 0, 255, 8, 8)
+ bounds: 408 80 8 8
+ - image: solid-color(52, 10, 0, 255, 8, 8)
+ bounds: 416 80 8 8
+ - image: solid-color(53, 10, 0, 255, 8, 8)
+ bounds: 424 80 8 8
+ - image: solid-color(54, 10, 0, 255, 8, 8)
+ bounds: 432 80 8 8
+ - image: solid-color(55, 10, 0, 255, 8, 8)
+ bounds: 440 80 8 8
+ - image: solid-color(56, 10, 0, 255, 8, 8)
+ bounds: 448 80 8 8
+ - image: solid-color(57, 10, 0, 255, 8, 8)
+ bounds: 456 80 8 8
+ - image: solid-color(58, 10, 0, 255, 8, 8)
+ bounds: 464 80 8 8
+ - image: solid-color(59, 10, 0, 255, 8, 8)
+ bounds: 472 80 8 8
+ - image: solid-color(60, 10, 0, 255, 8, 8)
+ bounds: 480 80 8 8
+ - image: solid-color(61, 10, 0, 255, 8, 8)
+ bounds: 488 80 8 8
+ - image: solid-color(62, 10, 0, 255, 8, 8)
+ bounds: 496 80 8 8
+ - image: solid-color(63, 10, 0, 255, 8, 8)
+ bounds: 504 80 8 8
+ - image: solid-color(64, 10, 0, 255, 8, 8)
+ bounds: 512 80 8 8
+ - image: solid-color(65, 10, 0, 255, 8, 8)
+ bounds: 520 80 8 8
+ - image: solid-color(66, 10, 0, 255, 8, 8)
+ bounds: 528 80 8 8
+ - image: solid-color(67, 10, 0, 255, 8, 8)
+ bounds: 536 80 8 8
+ - image: solid-color(68, 10, 0, 255, 8, 8)
+ bounds: 544 80 8 8
+ - image: solid-color(69, 10, 0, 255, 8, 8)
+ bounds: 552 80 8 8
+ - image: solid-color(70, 10, 0, 255, 8, 8)
+ bounds: 560 80 8 8
+ - image: solid-color(71, 10, 0, 255, 8, 8)
+ bounds: 568 80 8 8
+ - image: solid-color(72, 10, 0, 255, 8, 8)
+ bounds: 576 80 8 8
+ - image: solid-color(73, 10, 0, 255, 8, 8)
+ bounds: 584 80 8 8
+ - image: solid-color(74, 10, 0, 255, 8, 8)
+ bounds: 592 80 8 8
+ - image: solid-color(75, 10, 0, 255, 8, 8)
+ bounds: 600 80 8 8
+ - image: solid-color(76, 10, 0, 255, 8, 8)
+ bounds: 608 80 8 8
+ - image: solid-color(77, 10, 0, 255, 8, 8)
+ bounds: 616 80 8 8
+ - image: solid-color(78, 10, 0, 255, 8, 8)
+ bounds: 624 80 8 8
+ - image: solid-color(79, 10, 0, 255, 8, 8)
+ bounds: 632 80 8 8
+ - image: solid-color(80, 10, 0, 255, 8, 8)
+ bounds: 640 80 8 8
+ - image: solid-color(81, 10, 0, 255, 8, 8)
+ bounds: 648 80 8 8
+ - image: solid-color(82, 10, 0, 255, 8, 8)
+ bounds: 656 80 8 8
+ - image: solid-color(83, 10, 0, 255, 8, 8)
+ bounds: 664 80 8 8
+ - image: solid-color(84, 10, 0, 255, 8, 8)
+ bounds: 672 80 8 8
+ - image: solid-color(85, 10, 0, 255, 8, 8)
+ bounds: 680 80 8 8
+ - image: solid-color(86, 10, 0, 255, 8, 8)
+ bounds: 688 80 8 8
+ - image: solid-color(87, 10, 0, 255, 8, 8)
+ bounds: 696 80 8 8
+ - image: solid-color(88, 10, 0, 255, 8, 8)
+ bounds: 704 80 8 8
+ - image: solid-color(89, 10, 0, 255, 8, 8)
+ bounds: 712 80 8 8
+ - image: solid-color(90, 10, 0, 255, 8, 8)
+ bounds: 720 80 8 8
+ - image: solid-color(91, 10, 0, 255, 8, 8)
+ bounds: 728 80 8 8
+ - image: solid-color(92, 10, 0, 255, 8, 8)
+ bounds: 736 80 8 8
+ - image: solid-color(93, 10, 0, 255, 8, 8)
+ bounds: 744 80 8 8
+ - image: solid-color(94, 10, 0, 255, 8, 8)
+ bounds: 752 80 8 8
+ - image: solid-color(95, 10, 0, 255, 8, 8)
+ bounds: 760 80 8 8
+ - image: solid-color(96, 10, 0, 255, 8, 8)
+ bounds: 768 80 8 8
+ - image: solid-color(97, 10, 0, 255, 8, 8)
+ bounds: 776 80 8 8
+ - image: solid-color(98, 10, 0, 255, 8, 8)
+ bounds: 784 80 8 8
+ - image: solid-color(99, 10, 0, 255, 8, 8)
+ bounds: 792 80 8 8
+ - image: solid-color(100, 10, 0, 255, 8, 8)
+ bounds: 800 80 8 8
+ - image: solid-color(101, 10, 0, 255, 8, 8)
+ bounds: 808 80 8 8
+ - image: solid-color(102, 10, 0, 255, 8, 8)
+ bounds: 816 80 8 8
+ - image: solid-color(103, 10, 0, 255, 8, 8)
+ bounds: 824 80 8 8
+ - image: solid-color(104, 10, 0, 255, 8, 8)
+ bounds: 832 80 8 8
+ - image: solid-color(105, 10, 0, 255, 8, 8)
+ bounds: 840 80 8 8
+ - image: solid-color(106, 10, 0, 255, 8, 8)
+ bounds: 848 80 8 8
+ - image: solid-color(107, 10, 0, 255, 8, 8)
+ bounds: 856 80 8 8
+ - image: solid-color(108, 10, 0, 255, 8, 8)
+ bounds: 864 80 8 8
+ - image: solid-color(109, 10, 0, 255, 8, 8)
+ bounds: 872 80 8 8
+ - image: solid-color(110, 10, 0, 255, 8, 8)
+ bounds: 880 80 8 8
+ - image: solid-color(111, 10, 0, 255, 8, 8)
+ bounds: 888 80 8 8
+ - image: solid-color(112, 10, 0, 255, 8, 8)
+ bounds: 896 80 8 8
+ - image: solid-color(113, 10, 0, 255, 8, 8)
+ bounds: 904 80 8 8
+ - image: solid-color(114, 10, 0, 255, 8, 8)
+ bounds: 912 80 8 8
+ - image: solid-color(115, 10, 0, 255, 8, 8)
+ bounds: 920 80 8 8
+ - image: solid-color(116, 10, 0, 255, 8, 8)
+ bounds: 928 80 8 8
+ - image: solid-color(117, 10, 0, 255, 8, 8)
+ bounds: 936 80 8 8
+ - image: solid-color(118, 10, 0, 255, 8, 8)
+ bounds: 944 80 8 8
+ - image: solid-color(119, 10, 0, 255, 8, 8)
+ bounds: 952 80 8 8
+ - image: solid-color(120, 10, 0, 255, 8, 8)
+ bounds: 960 80 8 8
+ - image: solid-color(121, 10, 0, 255, 8, 8)
+ bounds: 968 80 8 8
+ - image: solid-color(122, 10, 0, 255, 8, 8)
+ bounds: 976 80 8 8
+ - image: solid-color(123, 10, 0, 255, 8, 8)
+ bounds: 984 80 8 8
+ - image: solid-color(124, 10, 0, 255, 8, 8)
+ bounds: 992 80 8 8
+ - image: solid-color(125, 10, 0, 255, 8, 8)
+ bounds: 1000 80 8 8
+ - image: solid-color(126, 10, 0, 255, 8, 8)
+ bounds: 1008 80 8 8
+ - image: solid-color(127, 10, 0, 255, 8, 8)
+ bounds: 1016 80 8 8
+ - image: solid-color(0, 11, 0, 255, 8, 8)
+ bounds: 0 88 8 8
+ - image: solid-color(1, 11, 0, 255, 8, 8)
+ bounds: 8 88 8 8
+ - image: solid-color(2, 11, 0, 255, 8, 8)
+ bounds: 16 88 8 8
+ - image: solid-color(3, 11, 0, 255, 8, 8)
+ bounds: 24 88 8 8
+ - image: solid-color(4, 11, 0, 255, 8, 8)
+ bounds: 32 88 8 8
+ - image: solid-color(5, 11, 0, 255, 8, 8)
+ bounds: 40 88 8 8
+ - image: solid-color(6, 11, 0, 255, 8, 8)
+ bounds: 48 88 8 8
+ - image: solid-color(7, 11, 0, 255, 8, 8)
+ bounds: 56 88 8 8
+ - image: solid-color(8, 11, 0, 255, 8, 8)
+ bounds: 64 88 8 8
+ - image: solid-color(9, 11, 0, 255, 8, 8)
+ bounds: 72 88 8 8
+ - image: solid-color(10, 11, 0, 255, 8, 8)
+ bounds: 80 88 8 8
+ - image: solid-color(11, 11, 0, 255, 8, 8)
+ bounds: 88 88 8 8
+ - image: solid-color(12, 11, 0, 255, 8, 8)
+ bounds: 96 88 8 8
+ - image: solid-color(13, 11, 0, 255, 8, 8)
+ bounds: 104 88 8 8
+ - image: solid-color(14, 11, 0, 255, 8, 8)
+ bounds: 112 88 8 8
+ - image: solid-color(15, 11, 0, 255, 8, 8)
+ bounds: 120 88 8 8
+ - image: solid-color(16, 11, 0, 255, 8, 8)
+ bounds: 128 88 8 8
+ - image: solid-color(17, 11, 0, 255, 8, 8)
+ bounds: 136 88 8 8
+ - image: solid-color(18, 11, 0, 255, 8, 8)
+ bounds: 144 88 8 8
+ - image: solid-color(19, 11, 0, 255, 8, 8)
+ bounds: 152 88 8 8
+ - image: solid-color(20, 11, 0, 255, 8, 8)
+ bounds: 160 88 8 8
+ - image: solid-color(21, 11, 0, 255, 8, 8)
+ bounds: 168 88 8 8
+ - image: solid-color(22, 11, 0, 255, 8, 8)
+ bounds: 176 88 8 8
+ - image: solid-color(23, 11, 0, 255, 8, 8)
+ bounds: 184 88 8 8
+ - image: solid-color(24, 11, 0, 255, 8, 8)
+ bounds: 192 88 8 8
+ - image: solid-color(25, 11, 0, 255, 8, 8)
+ bounds: 200 88 8 8
+ - image: solid-color(26, 11, 0, 255, 8, 8)
+ bounds: 208 88 8 8
+ - image: solid-color(27, 11, 0, 255, 8, 8)
+ bounds: 216 88 8 8
+ - image: solid-color(28, 11, 0, 255, 8, 8)
+ bounds: 224 88 8 8
+ - image: solid-color(29, 11, 0, 255, 8, 8)
+ bounds: 232 88 8 8
+ - image: solid-color(30, 11, 0, 255, 8, 8)
+ bounds: 240 88 8 8
+ - image: solid-color(31, 11, 0, 255, 8, 8)
+ bounds: 248 88 8 8
+ - image: solid-color(32, 11, 0, 255, 8, 8)
+ bounds: 256 88 8 8
+ - image: solid-color(33, 11, 0, 255, 8, 8)
+ bounds: 264 88 8 8
+ - image: solid-color(34, 11, 0, 255, 8, 8)
+ bounds: 272 88 8 8
+ - image: solid-color(35, 11, 0, 255, 8, 8)
+ bounds: 280 88 8 8
+ - image: solid-color(36, 11, 0, 255, 8, 8)
+ bounds: 288 88 8 8
+ - image: solid-color(37, 11, 0, 255, 8, 8)
+ bounds: 296 88 8 8
+ - image: solid-color(38, 11, 0, 255, 8, 8)
+ bounds: 304 88 8 8
+ - image: solid-color(39, 11, 0, 255, 8, 8)
+ bounds: 312 88 8 8
+ - image: solid-color(40, 11, 0, 255, 8, 8)
+ bounds: 320 88 8 8
+ - image: solid-color(41, 11, 0, 255, 8, 8)
+ bounds: 328 88 8 8
+ - image: solid-color(42, 11, 0, 255, 8, 8)
+ bounds: 336 88 8 8
+ - image: solid-color(43, 11, 0, 255, 8, 8)
+ bounds: 344 88 8 8
+ - image: solid-color(44, 11, 0, 255, 8, 8)
+ bounds: 352 88 8 8
+ - image: solid-color(45, 11, 0, 255, 8, 8)
+ bounds: 360 88 8 8
+ - image: solid-color(46, 11, 0, 255, 8, 8)
+ bounds: 368 88 8 8
+ - image: solid-color(47, 11, 0, 255, 8, 8)
+ bounds: 376 88 8 8
+ - image: solid-color(48, 11, 0, 255, 8, 8)
+ bounds: 384 88 8 8
+ - image: solid-color(49, 11, 0, 255, 8, 8)
+ bounds: 392 88 8 8
+ - image: solid-color(50, 11, 0, 255, 8, 8)
+ bounds: 400 88 8 8
+ - image: solid-color(51, 11, 0, 255, 8, 8)
+ bounds: 408 88 8 8
+ - image: solid-color(52, 11, 0, 255, 8, 8)
+ bounds: 416 88 8 8
+ - image: solid-color(53, 11, 0, 255, 8, 8)
+ bounds: 424 88 8 8
+ - image: solid-color(54, 11, 0, 255, 8, 8)
+ bounds: 432 88 8 8
+ - image: solid-color(55, 11, 0, 255, 8, 8)
+ bounds: 440 88 8 8
+ - image: solid-color(56, 11, 0, 255, 8, 8)
+ bounds: 448 88 8 8
+ - image: solid-color(57, 11, 0, 255, 8, 8)
+ bounds: 456 88 8 8
+ - image: solid-color(58, 11, 0, 255, 8, 8)
+ bounds: 464 88 8 8
+ - image: solid-color(59, 11, 0, 255, 8, 8)
+ bounds: 472 88 8 8
+ - image: solid-color(60, 11, 0, 255, 8, 8)
+ bounds: 480 88 8 8
+ - image: solid-color(61, 11, 0, 255, 8, 8)
+ bounds: 488 88 8 8
+ - image: solid-color(62, 11, 0, 255, 8, 8)
+ bounds: 496 88 8 8
+ - image: solid-color(63, 11, 0, 255, 8, 8)
+ bounds: 504 88 8 8
+ - image: solid-color(64, 11, 0, 255, 8, 8)
+ bounds: 512 88 8 8
+ - image: solid-color(65, 11, 0, 255, 8, 8)
+ bounds: 520 88 8 8
+ - image: solid-color(66, 11, 0, 255, 8, 8)
+ bounds: 528 88 8 8
+ - image: solid-color(67, 11, 0, 255, 8, 8)
+ bounds: 536 88 8 8
+ - image: solid-color(68, 11, 0, 255, 8, 8)
+ bounds: 544 88 8 8
+ - image: solid-color(69, 11, 0, 255, 8, 8)
+ bounds: 552 88 8 8
+ - image: solid-color(70, 11, 0, 255, 8, 8)
+ bounds: 560 88 8 8
+ - image: solid-color(71, 11, 0, 255, 8, 8)
+ bounds: 568 88 8 8
+ - image: solid-color(72, 11, 0, 255, 8, 8)
+ bounds: 576 88 8 8
+ - image: solid-color(73, 11, 0, 255, 8, 8)
+ bounds: 584 88 8 8
+ - image: solid-color(74, 11, 0, 255, 8, 8)
+ bounds: 592 88 8 8
+ - image: solid-color(75, 11, 0, 255, 8, 8)
+ bounds: 600 88 8 8
+ - image: solid-color(76, 11, 0, 255, 8, 8)
+ bounds: 608 88 8 8
+ - image: solid-color(77, 11, 0, 255, 8, 8)
+ bounds: 616 88 8 8
+ - image: solid-color(78, 11, 0, 255, 8, 8)
+ bounds: 624 88 8 8
+ - image: solid-color(79, 11, 0, 255, 8, 8)
+ bounds: 632 88 8 8
+ - image: solid-color(80, 11, 0, 255, 8, 8)
+ bounds: 640 88 8 8
+ - image: solid-color(81, 11, 0, 255, 8, 8)
+ bounds: 648 88 8 8
+ - image: solid-color(82, 11, 0, 255, 8, 8)
+ bounds: 656 88 8 8
+ - image: solid-color(83, 11, 0, 255, 8, 8)
+ bounds: 664 88 8 8
+ - image: solid-color(84, 11, 0, 255, 8, 8)
+ bounds: 672 88 8 8
+ - image: solid-color(85, 11, 0, 255, 8, 8)
+ bounds: 680 88 8 8
+ - image: solid-color(86, 11, 0, 255, 8, 8)
+ bounds: 688 88 8 8
+ - image: solid-color(87, 11, 0, 255, 8, 8)
+ bounds: 696 88 8 8
+ - image: solid-color(88, 11, 0, 255, 8, 8)
+ bounds: 704 88 8 8
+ - image: solid-color(89, 11, 0, 255, 8, 8)
+ bounds: 712 88 8 8
+ - image: solid-color(90, 11, 0, 255, 8, 8)
+ bounds: 720 88 8 8
+ - image: solid-color(91, 11, 0, 255, 8, 8)
+ bounds: 728 88 8 8
+ - image: solid-color(92, 11, 0, 255, 8, 8)
+ bounds: 736 88 8 8
+ - image: solid-color(93, 11, 0, 255, 8, 8)
+ bounds: 744 88 8 8
+ - image: solid-color(94, 11, 0, 255, 8, 8)
+ bounds: 752 88 8 8
+ - image: solid-color(95, 11, 0, 255, 8, 8)
+ bounds: 760 88 8 8
+ - image: solid-color(96, 11, 0, 255, 8, 8)
+ bounds: 768 88 8 8
+ - image: solid-color(97, 11, 0, 255, 8, 8)
+ bounds: 776 88 8 8
+ - image: solid-color(98, 11, 0, 255, 8, 8)
+ bounds: 784 88 8 8
+ - image: solid-color(99, 11, 0, 255, 8, 8)
+ bounds: 792 88 8 8
+ - image: solid-color(100, 11, 0, 255, 8, 8)
+ bounds: 800 88 8 8
+ - image: solid-color(101, 11, 0, 255, 8, 8)
+ bounds: 808 88 8 8
+ - image: solid-color(102, 11, 0, 255, 8, 8)
+ bounds: 816 88 8 8
+ - image: solid-color(103, 11, 0, 255, 8, 8)
+ bounds: 824 88 8 8
+ - image: solid-color(104, 11, 0, 255, 8, 8)
+ bounds: 832 88 8 8
+ - image: solid-color(105, 11, 0, 255, 8, 8)
+ bounds: 840 88 8 8
+ - image: solid-color(106, 11, 0, 255, 8, 8)
+ bounds: 848 88 8 8
+ - image: solid-color(107, 11, 0, 255, 8, 8)
+ bounds: 856 88 8 8
+ - image: solid-color(108, 11, 0, 255, 8, 8)
+ bounds: 864 88 8 8
+ - image: solid-color(109, 11, 0, 255, 8, 8)
+ bounds: 872 88 8 8
+ - image: solid-color(110, 11, 0, 255, 8, 8)
+ bounds: 880 88 8 8
+ - image: solid-color(111, 11, 0, 255, 8, 8)
+ bounds: 888 88 8 8
+ - image: solid-color(112, 11, 0, 255, 8, 8)
+ bounds: 896 88 8 8
+ - image: solid-color(113, 11, 0, 255, 8, 8)
+ bounds: 904 88 8 8
+ - image: solid-color(114, 11, 0, 255, 8, 8)
+ bounds: 912 88 8 8
+ - image: solid-color(115, 11, 0, 255, 8, 8)
+ bounds: 920 88 8 8
+ - image: solid-color(116, 11, 0, 255, 8, 8)
+ bounds: 928 88 8 8
+ - image: solid-color(117, 11, 0, 255, 8, 8)
+ bounds: 936 88 8 8
+ - image: solid-color(118, 11, 0, 255, 8, 8)
+ bounds: 944 88 8 8
+ - image: solid-color(119, 11, 0, 255, 8, 8)
+ bounds: 952 88 8 8
+ - image: solid-color(120, 11, 0, 255, 8, 8)
+ bounds: 960 88 8 8
+ - image: solid-color(121, 11, 0, 255, 8, 8)
+ bounds: 968 88 8 8
+ - image: solid-color(122, 11, 0, 255, 8, 8)
+ bounds: 976 88 8 8
+ - image: solid-color(123, 11, 0, 255, 8, 8)
+ bounds: 984 88 8 8
+ - image: solid-color(124, 11, 0, 255, 8, 8)
+ bounds: 992 88 8 8
+ - image: solid-color(125, 11, 0, 255, 8, 8)
+ bounds: 1000 88 8 8
+ - image: solid-color(126, 11, 0, 255, 8, 8)
+ bounds: 1008 88 8 8
+ - image: solid-color(127, 11, 0, 255, 8, 8)
+ bounds: 1016 88 8 8
+ - image: solid-color(0, 12, 0, 255, 8, 8)
+ bounds: 0 96 8 8
+ - image: solid-color(1, 12, 0, 255, 8, 8)
+ bounds: 8 96 8 8
+ - image: solid-color(2, 12, 0, 255, 8, 8)
+ bounds: 16 96 8 8
+ - image: solid-color(3, 12, 0, 255, 8, 8)
+ bounds: 24 96 8 8
+ - image: solid-color(4, 12, 0, 255, 8, 8)
+ bounds: 32 96 8 8
+ - image: solid-color(5, 12, 0, 255, 8, 8)
+ bounds: 40 96 8 8
+ - image: solid-color(6, 12, 0, 255, 8, 8)
+ bounds: 48 96 8 8
+ - image: solid-color(7, 12, 0, 255, 8, 8)
+ bounds: 56 96 8 8
+ - image: solid-color(8, 12, 0, 255, 8, 8)
+ bounds: 64 96 8 8
+ - image: solid-color(9, 12, 0, 255, 8, 8)
+ bounds: 72 96 8 8
+ - image: solid-color(10, 12, 0, 255, 8, 8)
+ bounds: 80 96 8 8
+ - image: solid-color(11, 12, 0, 255, 8, 8)
+ bounds: 88 96 8 8
+ - image: solid-color(12, 12, 0, 255, 8, 8)
+ bounds: 96 96 8 8
+ - image: solid-color(13, 12, 0, 255, 8, 8)
+ bounds: 104 96 8 8
+ - image: solid-color(14, 12, 0, 255, 8, 8)
+ bounds: 112 96 8 8
+ - image: solid-color(15, 12, 0, 255, 8, 8)
+ bounds: 120 96 8 8
+ - image: solid-color(16, 12, 0, 255, 8, 8)
+ bounds: 128 96 8 8
+ - image: solid-color(17, 12, 0, 255, 8, 8)
+ bounds: 136 96 8 8
+ - image: solid-color(18, 12, 0, 255, 8, 8)
+ bounds: 144 96 8 8
+ - image: solid-color(19, 12, 0, 255, 8, 8)
+ bounds: 152 96 8 8
+ - image: solid-color(20, 12, 0, 255, 8, 8)
+ bounds: 160 96 8 8
+ - image: solid-color(21, 12, 0, 255, 8, 8)
+ bounds: 168 96 8 8
+ - image: solid-color(22, 12, 0, 255, 8, 8)
+ bounds: 176 96 8 8
+ - image: solid-color(23, 12, 0, 255, 8, 8)
+ bounds: 184 96 8 8
+ - image: solid-color(24, 12, 0, 255, 8, 8)
+ bounds: 192 96 8 8
+ - image: solid-color(25, 12, 0, 255, 8, 8)
+ bounds: 200 96 8 8
+ - image: solid-color(26, 12, 0, 255, 8, 8)
+ bounds: 208 96 8 8
+ - image: solid-color(27, 12, 0, 255, 8, 8)
+ bounds: 216 96 8 8
+ - image: solid-color(28, 12, 0, 255, 8, 8)
+ bounds: 224 96 8 8
+ - image: solid-color(29, 12, 0, 255, 8, 8)
+ bounds: 232 96 8 8
+ - image: solid-color(30, 12, 0, 255, 8, 8)
+ bounds: 240 96 8 8
+ - image: solid-color(31, 12, 0, 255, 8, 8)
+ bounds: 248 96 8 8
+ - image: solid-color(32, 12, 0, 255, 8, 8)
+ bounds: 256 96 8 8
+ - image: solid-color(33, 12, 0, 255, 8, 8)
+ bounds: 264 96 8 8
+ - image: solid-color(34, 12, 0, 255, 8, 8)
+ bounds: 272 96 8 8
+ - image: solid-color(35, 12, 0, 255, 8, 8)
+ bounds: 280 96 8 8
+ - image: solid-color(36, 12, 0, 255, 8, 8)
+ bounds: 288 96 8 8
+ - image: solid-color(37, 12, 0, 255, 8, 8)
+ bounds: 296 96 8 8
+ - image: solid-color(38, 12, 0, 255, 8, 8)
+ bounds: 304 96 8 8
+ - image: solid-color(39, 12, 0, 255, 8, 8)
+ bounds: 312 96 8 8
+ - image: solid-color(40, 12, 0, 255, 8, 8)
+ bounds: 320 96 8 8
+ - image: solid-color(41, 12, 0, 255, 8, 8)
+ bounds: 328 96 8 8
+ - image: solid-color(42, 12, 0, 255, 8, 8)
+ bounds: 336 96 8 8
+ - image: solid-color(43, 12, 0, 255, 8, 8)
+ bounds: 344 96 8 8
+ - image: solid-color(44, 12, 0, 255, 8, 8)
+ bounds: 352 96 8 8
+ - image: solid-color(45, 12, 0, 255, 8, 8)
+ bounds: 360 96 8 8
+ - image: solid-color(46, 12, 0, 255, 8, 8)
+ bounds: 368 96 8 8
+ - image: solid-color(47, 12, 0, 255, 8, 8)
+ bounds: 376 96 8 8
+ - image: solid-color(48, 12, 0, 255, 8, 8)
+ bounds: 384 96 8 8
+ - image: solid-color(49, 12, 0, 255, 8, 8)
+ bounds: 392 96 8 8
+ - image: solid-color(50, 12, 0, 255, 8, 8)
+ bounds: 400 96 8 8
+ - image: solid-color(51, 12, 0, 255, 8, 8)
+ bounds: 408 96 8 8
+ - image: solid-color(52, 12, 0, 255, 8, 8)
+ bounds: 416 96 8 8
+ - image: solid-color(53, 12, 0, 255, 8, 8)
+ bounds: 424 96 8 8
+ - image: solid-color(54, 12, 0, 255, 8, 8)
+ bounds: 432 96 8 8
+ - image: solid-color(55, 12, 0, 255, 8, 8)
+ bounds: 440 96 8 8
+ - image: solid-color(56, 12, 0, 255, 8, 8)
+ bounds: 448 96 8 8
+ - image: solid-color(57, 12, 0, 255, 8, 8)
+ bounds: 456 96 8 8
+ - image: solid-color(58, 12, 0, 255, 8, 8)
+ bounds: 464 96 8 8
+ - image: solid-color(59, 12, 0, 255, 8, 8)
+ bounds: 472 96 8 8
+ - image: solid-color(60, 12, 0, 255, 8, 8)
+ bounds: 480 96 8 8
+ - image: solid-color(61, 12, 0, 255, 8, 8)
+ bounds: 488 96 8 8
+ - image: solid-color(62, 12, 0, 255, 8, 8)
+ bounds: 496 96 8 8
+ - image: solid-color(63, 12, 0, 255, 8, 8)
+ bounds: 504 96 8 8
+ - image: solid-color(64, 12, 0, 255, 8, 8)
+ bounds: 512 96 8 8
+ - image: solid-color(65, 12, 0, 255, 8, 8)
+ bounds: 520 96 8 8
+ - image: solid-color(66, 12, 0, 255, 8, 8)
+ bounds: 528 96 8 8
+ - image: solid-color(67, 12, 0, 255, 8, 8)
+ bounds: 536 96 8 8
+ - image: solid-color(68, 12, 0, 255, 8, 8)
+ bounds: 544 96 8 8
+ - image: solid-color(69, 12, 0, 255, 8, 8)
+ bounds: 552 96 8 8
+ - image: solid-color(70, 12, 0, 255, 8, 8)
+ bounds: 560 96 8 8
+ - image: solid-color(71, 12, 0, 255, 8, 8)
+ bounds: 568 96 8 8
+ - image: solid-color(72, 12, 0, 255, 8, 8)
+ bounds: 576 96 8 8
+ - image: solid-color(73, 12, 0, 255, 8, 8)
+ bounds: 584 96 8 8
+ - image: solid-color(74, 12, 0, 255, 8, 8)
+ bounds: 592 96 8 8
+ - image: solid-color(75, 12, 0, 255, 8, 8)
+ bounds: 600 96 8 8
+ - image: solid-color(76, 12, 0, 255, 8, 8)
+ bounds: 608 96 8 8
+ - image: solid-color(77, 12, 0, 255, 8, 8)
+ bounds: 616 96 8 8
+ - image: solid-color(78, 12, 0, 255, 8, 8)
+ bounds: 624 96 8 8
+ - image: solid-color(79, 12, 0, 255, 8, 8)
+ bounds: 632 96 8 8
+ - image: solid-color(80, 12, 0, 255, 8, 8)
+ bounds: 640 96 8 8
+ - image: solid-color(81, 12, 0, 255, 8, 8)
+ bounds: 648 96 8 8
+ - image: solid-color(82, 12, 0, 255, 8, 8)
+ bounds: 656 96 8 8
+ - image: solid-color(83, 12, 0, 255, 8, 8)
+ bounds: 664 96 8 8
+ - image: solid-color(84, 12, 0, 255, 8, 8)
+ bounds: 672 96 8 8
+ - image: solid-color(85, 12, 0, 255, 8, 8)
+ bounds: 680 96 8 8
+ - image: solid-color(86, 12, 0, 255, 8, 8)
+ bounds: 688 96 8 8
+ - image: solid-color(87, 12, 0, 255, 8, 8)
+ bounds: 696 96 8 8
+ - image: solid-color(88, 12, 0, 255, 8, 8)
+ bounds: 704 96 8 8
+ - image: solid-color(89, 12, 0, 255, 8, 8)
+ bounds: 712 96 8 8
+ - image: solid-color(90, 12, 0, 255, 8, 8)
+ bounds: 720 96 8 8
+ - image: solid-color(91, 12, 0, 255, 8, 8)
+ bounds: 728 96 8 8
+ - image: solid-color(92, 12, 0, 255, 8, 8)
+ bounds: 736 96 8 8
+ - image: solid-color(93, 12, 0, 255, 8, 8)
+ bounds: 744 96 8 8
+ - image: solid-color(94, 12, 0, 255, 8, 8)
+ bounds: 752 96 8 8
+ - image: solid-color(95, 12, 0, 255, 8, 8)
+ bounds: 760 96 8 8
+ - image: solid-color(96, 12, 0, 255, 8, 8)
+ bounds: 768 96 8 8
+ - image: solid-color(97, 12, 0, 255, 8, 8)
+ bounds: 776 96 8 8
+ - image: solid-color(98, 12, 0, 255, 8, 8)
+ bounds: 784 96 8 8
+ - image: solid-color(99, 12, 0, 255, 8, 8)
+ bounds: 792 96 8 8
+ - image: solid-color(100, 12, 0, 255, 8, 8)
+ bounds: 800 96 8 8
+ - image: solid-color(101, 12, 0, 255, 8, 8)
+ bounds: 808 96 8 8
+ - image: solid-color(102, 12, 0, 255, 8, 8)
+ bounds: 816 96 8 8
+ - image: solid-color(103, 12, 0, 255, 8, 8)
+ bounds: 824 96 8 8
+ - image: solid-color(104, 12, 0, 255, 8, 8)
+ bounds: 832 96 8 8
+ - image: solid-color(105, 12, 0, 255, 8, 8)
+ bounds: 840 96 8 8
+ - image: solid-color(106, 12, 0, 255, 8, 8)
+ bounds: 848 96 8 8
+ - image: solid-color(107, 12, 0, 255, 8, 8)
+ bounds: 856 96 8 8
+ - image: solid-color(108, 12, 0, 255, 8, 8)
+ bounds: 864 96 8 8
+ - image: solid-color(109, 12, 0, 255, 8, 8)
+ bounds: 872 96 8 8
+ - image: solid-color(110, 12, 0, 255, 8, 8)
+ bounds: 880 96 8 8
+ - image: solid-color(111, 12, 0, 255, 8, 8)
+ bounds: 888 96 8 8
+ - image: solid-color(112, 12, 0, 255, 8, 8)
+ bounds: 896 96 8 8
+ - image: solid-color(113, 12, 0, 255, 8, 8)
+ bounds: 904 96 8 8
+ - image: solid-color(114, 12, 0, 255, 8, 8)
+ bounds: 912 96 8 8
+ - image: solid-color(115, 12, 0, 255, 8, 8)
+ bounds: 920 96 8 8
+ - image: solid-color(116, 12, 0, 255, 8, 8)
+ bounds: 928 96 8 8
+ - image: solid-color(117, 12, 0, 255, 8, 8)
+ bounds: 936 96 8 8
+ - image: solid-color(118, 12, 0, 255, 8, 8)
+ bounds: 944 96 8 8
+ - image: solid-color(119, 12, 0, 255, 8, 8)
+ bounds: 952 96 8 8
+ - image: solid-color(120, 12, 0, 255, 8, 8)
+ bounds: 960 96 8 8
+ - image: solid-color(121, 12, 0, 255, 8, 8)
+ bounds: 968 96 8 8
+ - image: solid-color(122, 12, 0, 255, 8, 8)
+ bounds: 976 96 8 8
+ - image: solid-color(123, 12, 0, 255, 8, 8)
+ bounds: 984 96 8 8
+ - image: solid-color(124, 12, 0, 255, 8, 8)
+ bounds: 992 96 8 8
+ - image: solid-color(125, 12, 0, 255, 8, 8)
+ bounds: 1000 96 8 8
+ - image: solid-color(126, 12, 0, 255, 8, 8)
+ bounds: 1008 96 8 8
+ - image: solid-color(127, 12, 0, 255, 8, 8)
+ bounds: 1016 96 8 8
+ - image: solid-color(0, 13, 0, 255, 8, 8)
+ bounds: 0 104 8 8
+ - image: solid-color(1, 13, 0, 255, 8, 8)
+ bounds: 8 104 8 8
+ - image: solid-color(2, 13, 0, 255, 8, 8)
+ bounds: 16 104 8 8
+ - image: solid-color(3, 13, 0, 255, 8, 8)
+ bounds: 24 104 8 8
+ - image: solid-color(4, 13, 0, 255, 8, 8)
+ bounds: 32 104 8 8
+ - image: solid-color(5, 13, 0, 255, 8, 8)
+ bounds: 40 104 8 8
+ - image: solid-color(6, 13, 0, 255, 8, 8)
+ bounds: 48 104 8 8
+ - image: solid-color(7, 13, 0, 255, 8, 8)
+ bounds: 56 104 8 8
+ - image: solid-color(8, 13, 0, 255, 8, 8)
+ bounds: 64 104 8 8
+ - image: solid-color(9, 13, 0, 255, 8, 8)
+ bounds: 72 104 8 8
+ - image: solid-color(10, 13, 0, 255, 8, 8)
+ bounds: 80 104 8 8
+ - image: solid-color(11, 13, 0, 255, 8, 8)
+ bounds: 88 104 8 8
+ - image: solid-color(12, 13, 0, 255, 8, 8)
+ bounds: 96 104 8 8
+ - image: solid-color(13, 13, 0, 255, 8, 8)
+ bounds: 104 104 8 8
+ - image: solid-color(14, 13, 0, 255, 8, 8)
+ bounds: 112 104 8 8
+ - image: solid-color(15, 13, 0, 255, 8, 8)
+ bounds: 120 104 8 8
+ - image: solid-color(16, 13, 0, 255, 8, 8)
+ bounds: 128 104 8 8
+ - image: solid-color(17, 13, 0, 255, 8, 8)
+ bounds: 136 104 8 8
+ - image: solid-color(18, 13, 0, 255, 8, 8)
+ bounds: 144 104 8 8
+ - image: solid-color(19, 13, 0, 255, 8, 8)
+ bounds: 152 104 8 8
+ - image: solid-color(20, 13, 0, 255, 8, 8)
+ bounds: 160 104 8 8
+ - image: solid-color(21, 13, 0, 255, 8, 8)
+ bounds: 168 104 8 8
+ - image: solid-color(22, 13, 0, 255, 8, 8)
+ bounds: 176 104 8 8
+ - image: solid-color(23, 13, 0, 255, 8, 8)
+ bounds: 184 104 8 8
+ - image: solid-color(24, 13, 0, 255, 8, 8)
+ bounds: 192 104 8 8
+ - image: solid-color(25, 13, 0, 255, 8, 8)
+ bounds: 200 104 8 8
+ - image: solid-color(26, 13, 0, 255, 8, 8)
+ bounds: 208 104 8 8
+ - image: solid-color(27, 13, 0, 255, 8, 8)
+ bounds: 216 104 8 8
+ - image: solid-color(28, 13, 0, 255, 8, 8)
+ bounds: 224 104 8 8
+ - image: solid-color(29, 13, 0, 255, 8, 8)
+ bounds: 232 104 8 8
+ - image: solid-color(30, 13, 0, 255, 8, 8)
+ bounds: 240 104 8 8
+ - image: solid-color(31, 13, 0, 255, 8, 8)
+ bounds: 248 104 8 8
+ - image: solid-color(32, 13, 0, 255, 8, 8)
+ bounds: 256 104 8 8
+ - image: solid-color(33, 13, 0, 255, 8, 8)
+ bounds: 264 104 8 8
+ - image: solid-color(34, 13, 0, 255, 8, 8)
+ bounds: 272 104 8 8
+ - image: solid-color(35, 13, 0, 255, 8, 8)
+ bounds: 280 104 8 8
+ - image: solid-color(36, 13, 0, 255, 8, 8)
+ bounds: 288 104 8 8
+ - image: solid-color(37, 13, 0, 255, 8, 8)
+ bounds: 296 104 8 8
+ - image: solid-color(38, 13, 0, 255, 8, 8)
+ bounds: 304 104 8 8
+ - image: solid-color(39, 13, 0, 255, 8, 8)
+ bounds: 312 104 8 8
+ - image: solid-color(40, 13, 0, 255, 8, 8)
+ bounds: 320 104 8 8
+ - image: solid-color(41, 13, 0, 255, 8, 8)
+ bounds: 328 104 8 8
+ - image: solid-color(42, 13, 0, 255, 8, 8)
+ bounds: 336 104 8 8
+ - image: solid-color(43, 13, 0, 255, 8, 8)
+ bounds: 344 104 8 8
+ - image: solid-color(44, 13, 0, 255, 8, 8)
+ bounds: 352 104 8 8
+ - image: solid-color(45, 13, 0, 255, 8, 8)
+ bounds: 360 104 8 8
+ - image: solid-color(46, 13, 0, 255, 8, 8)
+ bounds: 368 104 8 8
+ - image: solid-color(47, 13, 0, 255, 8, 8)
+ bounds: 376 104 8 8
+ - image: solid-color(48, 13, 0, 255, 8, 8)
+ bounds: 384 104 8 8
+ - image: solid-color(49, 13, 0, 255, 8, 8)
+ bounds: 392 104 8 8
+ - image: solid-color(50, 13, 0, 255, 8, 8)
+ bounds: 400 104 8 8
+ - image: solid-color(51, 13, 0, 255, 8, 8)
+ bounds: 408 104 8 8
+ - image: solid-color(52, 13, 0, 255, 8, 8)
+ bounds: 416 104 8 8
+ - image: solid-color(53, 13, 0, 255, 8, 8)
+ bounds: 424 104 8 8
+ - image: solid-color(54, 13, 0, 255, 8, 8)
+ bounds: 432 104 8 8
+ - image: solid-color(55, 13, 0, 255, 8, 8)
+ bounds: 440 104 8 8
+ - image: solid-color(56, 13, 0, 255, 8, 8)
+ bounds: 448 104 8 8
+ - image: solid-color(57, 13, 0, 255, 8, 8)
+ bounds: 456 104 8 8
+ - image: solid-color(58, 13, 0, 255, 8, 8)
+ bounds: 464 104 8 8
+ - image: solid-color(59, 13, 0, 255, 8, 8)
+ bounds: 472 104 8 8
+ - image: solid-color(60, 13, 0, 255, 8, 8)
+ bounds: 480 104 8 8
+ - image: solid-color(61, 13, 0, 255, 8, 8)
+ bounds: 488 104 8 8
+ - image: solid-color(62, 13, 0, 255, 8, 8)
+ bounds: 496 104 8 8
+ - image: solid-color(63, 13, 0, 255, 8, 8)
+ bounds: 504 104 8 8
+ - image: solid-color(64, 13, 0, 255, 8, 8)
+ bounds: 512 104 8 8
+ - image: solid-color(65, 13, 0, 255, 8, 8)
+ bounds: 520 104 8 8
+ - image: solid-color(66, 13, 0, 255, 8, 8)
+ bounds: 528 104 8 8
+ - image: solid-color(67, 13, 0, 255, 8, 8)
+ bounds: 536 104 8 8
+ - image: solid-color(68, 13, 0, 255, 8, 8)
+ bounds: 544 104 8 8
+ - image: solid-color(69, 13, 0, 255, 8, 8)
+ bounds: 552 104 8 8
+ - image: solid-color(70, 13, 0, 255, 8, 8)
+ bounds: 560 104 8 8
+ - image: solid-color(71, 13, 0, 255, 8, 8)
+ bounds: 568 104 8 8
+ - image: solid-color(72, 13, 0, 255, 8, 8)
+ bounds: 576 104 8 8
+ - image: solid-color(73, 13, 0, 255, 8, 8)
+ bounds: 584 104 8 8
+ - image: solid-color(74, 13, 0, 255, 8, 8)
+ bounds: 592 104 8 8
+ - image: solid-color(75, 13, 0, 255, 8, 8)
+ bounds: 600 104 8 8
+ - image: solid-color(76, 13, 0, 255, 8, 8)
+ bounds: 608 104 8 8
+ - image: solid-color(77, 13, 0, 255, 8, 8)
+ bounds: 616 104 8 8
+ - image: solid-color(78, 13, 0, 255, 8, 8)
+ bounds: 624 104 8 8
+ - image: solid-color(79, 13, 0, 255, 8, 8)
+ bounds: 632 104 8 8
+ - image: solid-color(80, 13, 0, 255, 8, 8)
+ bounds: 640 104 8 8
+ - image: solid-color(81, 13, 0, 255, 8, 8)
+ bounds: 648 104 8 8
+ - image: solid-color(82, 13, 0, 255, 8, 8)
+ bounds: 656 104 8 8
+ - image: solid-color(83, 13, 0, 255, 8, 8)
+ bounds: 664 104 8 8
+ - image: solid-color(84, 13, 0, 255, 8, 8)
+ bounds: 672 104 8 8
+ - image: solid-color(85, 13, 0, 255, 8, 8)
+ bounds: 680 104 8 8
+ - image: solid-color(86, 13, 0, 255, 8, 8)
+ bounds: 688 104 8 8
+ - image: solid-color(87, 13, 0, 255, 8, 8)
+ bounds: 696 104 8 8
+ - image: solid-color(88, 13, 0, 255, 8, 8)
+ bounds: 704 104 8 8
+ - image: solid-color(89, 13, 0, 255, 8, 8)
+ bounds: 712 104 8 8
+ - image: solid-color(90, 13, 0, 255, 8, 8)
+ bounds: 720 104 8 8
+ - image: solid-color(91, 13, 0, 255, 8, 8)
+ bounds: 728 104 8 8
+ - image: solid-color(92, 13, 0, 255, 8, 8)
+ bounds: 736 104 8 8
+ - image: solid-color(93, 13, 0, 255, 8, 8)
+ bounds: 744 104 8 8
+ - image: solid-color(94, 13, 0, 255, 8, 8)
+ bounds: 752 104 8 8
+ - image: solid-color(95, 13, 0, 255, 8, 8)
+ bounds: 760 104 8 8
+ - image: solid-color(96, 13, 0, 255, 8, 8)
+ bounds: 768 104 8 8
+ - image: solid-color(97, 13, 0, 255, 8, 8)
+ bounds: 776 104 8 8
+ - image: solid-color(98, 13, 0, 255, 8, 8)
+ bounds: 784 104 8 8
+ - image: solid-color(99, 13, 0, 255, 8, 8)
+ bounds: 792 104 8 8
+ - image: solid-color(100, 13, 0, 255, 8, 8)
+ bounds: 800 104 8 8
+ - image: solid-color(101, 13, 0, 255, 8, 8)
+ bounds: 808 104 8 8
+ - image: solid-color(102, 13, 0, 255, 8, 8)
+ bounds: 816 104 8 8
+ - image: solid-color(103, 13, 0, 255, 8, 8)
+ bounds: 824 104 8 8
+ - image: solid-color(104, 13, 0, 255, 8, 8)
+ bounds: 832 104 8 8
+ - image: solid-color(105, 13, 0, 255, 8, 8)
+ bounds: 840 104 8 8
+ - image: solid-color(106, 13, 0, 255, 8, 8)
+ bounds: 848 104 8 8
+ - image: solid-color(107, 13, 0, 255, 8, 8)
+ bounds: 856 104 8 8
+ - image: solid-color(108, 13, 0, 255, 8, 8)
+ bounds: 864 104 8 8
+ - image: solid-color(109, 13, 0, 255, 8, 8)
+ bounds: 872 104 8 8
+ - image: solid-color(110, 13, 0, 255, 8, 8)
+ bounds: 880 104 8 8
+ - image: solid-color(111, 13, 0, 255, 8, 8)
+ bounds: 888 104 8 8
+ - image: solid-color(112, 13, 0, 255, 8, 8)
+ bounds: 896 104 8 8
+ - image: solid-color(113, 13, 0, 255, 8, 8)
+ bounds: 904 104 8 8
+ - image: solid-color(114, 13, 0, 255, 8, 8)
+ bounds: 912 104 8 8
+ - image: solid-color(115, 13, 0, 255, 8, 8)
+ bounds: 920 104 8 8
+ - image: solid-color(116, 13, 0, 255, 8, 8)
+ bounds: 928 104 8 8
+ - image: solid-color(117, 13, 0, 255, 8, 8)
+ bounds: 936 104 8 8
+ - image: solid-color(118, 13, 0, 255, 8, 8)
+ bounds: 944 104 8 8
+ - image: solid-color(119, 13, 0, 255, 8, 8)
+ bounds: 952 104 8 8
+ - image: solid-color(120, 13, 0, 255, 8, 8)
+ bounds: 960 104 8 8
+ - image: solid-color(121, 13, 0, 255, 8, 8)
+ bounds: 968 104 8 8
+ - image: solid-color(122, 13, 0, 255, 8, 8)
+ bounds: 976 104 8 8
+ - image: solid-color(123, 13, 0, 255, 8, 8)
+ bounds: 984 104 8 8
+ - image: solid-color(124, 13, 0, 255, 8, 8)
+ bounds: 992 104 8 8
+ - image: solid-color(125, 13, 0, 255, 8, 8)
+ bounds: 1000 104 8 8
+ - image: solid-color(126, 13, 0, 255, 8, 8)
+ bounds: 1008 104 8 8
+ - image: solid-color(127, 13, 0, 255, 8, 8)
+ bounds: 1016 104 8 8
+ - image: solid-color(0, 14, 0, 255, 8, 8)
+ bounds: 0 112 8 8
+ - image: solid-color(1, 14, 0, 255, 8, 8)
+ bounds: 8 112 8 8
+ - image: solid-color(2, 14, 0, 255, 8, 8)
+ bounds: 16 112 8 8
+ - image: solid-color(3, 14, 0, 255, 8, 8)
+ bounds: 24 112 8 8
+ - image: solid-color(4, 14, 0, 255, 8, 8)
+ bounds: 32 112 8 8
+ - image: solid-color(5, 14, 0, 255, 8, 8)
+ bounds: 40 112 8 8
+ - image: solid-color(6, 14, 0, 255, 8, 8)
+ bounds: 48 112 8 8
+ - image: solid-color(7, 14, 0, 255, 8, 8)
+ bounds: 56 112 8 8
+ - image: solid-color(8, 14, 0, 255, 8, 8)
+ bounds: 64 112 8 8
+ - image: solid-color(9, 14, 0, 255, 8, 8)
+ bounds: 72 112 8 8
+ - image: solid-color(10, 14, 0, 255, 8, 8)
+ bounds: 80 112 8 8
+ - image: solid-color(11, 14, 0, 255, 8, 8)
+ bounds: 88 112 8 8
+ - image: solid-color(12, 14, 0, 255, 8, 8)
+ bounds: 96 112 8 8
+ - image: solid-color(13, 14, 0, 255, 8, 8)
+ bounds: 104 112 8 8
+ - image: solid-color(14, 14, 0, 255, 8, 8)
+ bounds: 112 112 8 8
+ - image: solid-color(15, 14, 0, 255, 8, 8)
+ bounds: 120 112 8 8
+ - image: solid-color(16, 14, 0, 255, 8, 8)
+ bounds: 128 112 8 8
+ - image: solid-color(17, 14, 0, 255, 8, 8)
+ bounds: 136 112 8 8
+ - image: solid-color(18, 14, 0, 255, 8, 8)
+ bounds: 144 112 8 8
+ - image: solid-color(19, 14, 0, 255, 8, 8)
+ bounds: 152 112 8 8
+ - image: solid-color(20, 14, 0, 255, 8, 8)
+ bounds: 160 112 8 8
+ - image: solid-color(21, 14, 0, 255, 8, 8)
+ bounds: 168 112 8 8
+ - image: solid-color(22, 14, 0, 255, 8, 8)
+ bounds: 176 112 8 8
+ - image: solid-color(23, 14, 0, 255, 8, 8)
+ bounds: 184 112 8 8
+ - image: solid-color(24, 14, 0, 255, 8, 8)
+ bounds: 192 112 8 8
+ - image: solid-color(25, 14, 0, 255, 8, 8)
+ bounds: 200 112 8 8
+ - image: solid-color(26, 14, 0, 255, 8, 8)
+ bounds: 208 112 8 8
+ - image: solid-color(27, 14, 0, 255, 8, 8)
+ bounds: 216 112 8 8
+ - image: solid-color(28, 14, 0, 255, 8, 8)
+ bounds: 224 112 8 8
+ - image: solid-color(29, 14, 0, 255, 8, 8)
+ bounds: 232 112 8 8
+ - image: solid-color(30, 14, 0, 255, 8, 8)
+ bounds: 240 112 8 8
+ - image: solid-color(31, 14, 0, 255, 8, 8)
+ bounds: 248 112 8 8
+ - image: solid-color(32, 14, 0, 255, 8, 8)
+ bounds: 256 112 8 8
+ - image: solid-color(33, 14, 0, 255, 8, 8)
+ bounds: 264 112 8 8
+ - image: solid-color(34, 14, 0, 255, 8, 8)
+ bounds: 272 112 8 8
+ - image: solid-color(35, 14, 0, 255, 8, 8)
+ bounds: 280 112 8 8
+ - image: solid-color(36, 14, 0, 255, 8, 8)
+ bounds: 288 112 8 8
+ - image: solid-color(37, 14, 0, 255, 8, 8)
+ bounds: 296 112 8 8
+ - image: solid-color(38, 14, 0, 255, 8, 8)
+ bounds: 304 112 8 8
+ - image: solid-color(39, 14, 0, 255, 8, 8)
+ bounds: 312 112 8 8
+ - image: solid-color(40, 14, 0, 255, 8, 8)
+ bounds: 320 112 8 8
+ - image: solid-color(41, 14, 0, 255, 8, 8)
+ bounds: 328 112 8 8
+ - image: solid-color(42, 14, 0, 255, 8, 8)
+ bounds: 336 112 8 8
+ - image: solid-color(43, 14, 0, 255, 8, 8)
+ bounds: 344 112 8 8
+ - image: solid-color(44, 14, 0, 255, 8, 8)
+ bounds: 352 112 8 8
+ - image: solid-color(45, 14, 0, 255, 8, 8)
+ bounds: 360 112 8 8
+ - image: solid-color(46, 14, 0, 255, 8, 8)
+ bounds: 368 112 8 8
+ - image: solid-color(47, 14, 0, 255, 8, 8)
+ bounds: 376 112 8 8
+ - image: solid-color(48, 14, 0, 255, 8, 8)
+ bounds: 384 112 8 8
+ - image: solid-color(49, 14, 0, 255, 8, 8)
+ bounds: 392 112 8 8
+ - image: solid-color(50, 14, 0, 255, 8, 8)
+ bounds: 400 112 8 8
+ - image: solid-color(51, 14, 0, 255, 8, 8)
+ bounds: 408 112 8 8
+ - image: solid-color(52, 14, 0, 255, 8, 8)
+ bounds: 416 112 8 8
+ - image: solid-color(53, 14, 0, 255, 8, 8)
+ bounds: 424 112 8 8
+ - image: solid-color(54, 14, 0, 255, 8, 8)
+ bounds: 432 112 8 8
+ - image: solid-color(55, 14, 0, 255, 8, 8)
+ bounds: 440 112 8 8
+ - image: solid-color(56, 14, 0, 255, 8, 8)
+ bounds: 448 112 8 8
+ - image: solid-color(57, 14, 0, 255, 8, 8)
+ bounds: 456 112 8 8
+ - image: solid-color(58, 14, 0, 255, 8, 8)
+ bounds: 464 112 8 8
+ - image: solid-color(59, 14, 0, 255, 8, 8)
+ bounds: 472 112 8 8
+ - image: solid-color(60, 14, 0, 255, 8, 8)
+ bounds: 480 112 8 8
+ - image: solid-color(61, 14, 0, 255, 8, 8)
+ bounds: 488 112 8 8
+ - image: solid-color(62, 14, 0, 255, 8, 8)
+ bounds: 496 112 8 8
+ - image: solid-color(63, 14, 0, 255, 8, 8)
+ bounds: 504 112 8 8
+ - image: solid-color(64, 14, 0, 255, 8, 8)
+ bounds: 512 112 8 8
+ - image: solid-color(65, 14, 0, 255, 8, 8)
+ bounds: 520 112 8 8
+ - image: solid-color(66, 14, 0, 255, 8, 8)
+ bounds: 528 112 8 8
+ - image: solid-color(67, 14, 0, 255, 8, 8)
+ bounds: 536 112 8 8
+ - image: solid-color(68, 14, 0, 255, 8, 8)
+ bounds: 544 112 8 8
+ - image: solid-color(69, 14, 0, 255, 8, 8)
+ bounds: 552 112 8 8
+ - image: solid-color(70, 14, 0, 255, 8, 8)
+ bounds: 560 112 8 8
+ - image: solid-color(71, 14, 0, 255, 8, 8)
+ bounds: 568 112 8 8
+ - image: solid-color(72, 14, 0, 255, 8, 8)
+ bounds: 576 112 8 8
+ - image: solid-color(73, 14, 0, 255, 8, 8)
+ bounds: 584 112 8 8
+ - image: solid-color(74, 14, 0, 255, 8, 8)
+ bounds: 592 112 8 8
+ - image: solid-color(75, 14, 0, 255, 8, 8)
+ bounds: 600 112 8 8
+ - image: solid-color(76, 14, 0, 255, 8, 8)
+ bounds: 608 112 8 8
+ - image: solid-color(77, 14, 0, 255, 8, 8)
+ bounds: 616 112 8 8
+ - image: solid-color(78, 14, 0, 255, 8, 8)
+ bounds: 624 112 8 8
+ - image: solid-color(79, 14, 0, 255, 8, 8)
+ bounds: 632 112 8 8
+ - image: solid-color(80, 14, 0, 255, 8, 8)
+ bounds: 640 112 8 8
+ - image: solid-color(81, 14, 0, 255, 8, 8)
+ bounds: 648 112 8 8
+ - image: solid-color(82, 14, 0, 255, 8, 8)
+ bounds: 656 112 8 8
+ - image: solid-color(83, 14, 0, 255, 8, 8)
+ bounds: 664 112 8 8
+ - image: solid-color(84, 14, 0, 255, 8, 8)
+ bounds: 672 112 8 8
+ - image: solid-color(85, 14, 0, 255, 8, 8)
+ bounds: 680 112 8 8
+ - image: solid-color(86, 14, 0, 255, 8, 8)
+ bounds: 688 112 8 8
+ - image: solid-color(87, 14, 0, 255, 8, 8)
+ bounds: 696 112 8 8
+ - image: solid-color(88, 14, 0, 255, 8, 8)
+ bounds: 704 112 8 8
+ - image: solid-color(89, 14, 0, 255, 8, 8)
+ bounds: 712 112 8 8
+ - image: solid-color(90, 14, 0, 255, 8, 8)
+ bounds: 720 112 8 8
+ - image: solid-color(91, 14, 0, 255, 8, 8)
+ bounds: 728 112 8 8
+ - image: solid-color(92, 14, 0, 255, 8, 8)
+ bounds: 736 112 8 8
+ - image: solid-color(93, 14, 0, 255, 8, 8)
+ bounds: 744 112 8 8
+ - image: solid-color(94, 14, 0, 255, 8, 8)
+ bounds: 752 112 8 8
+ - image: solid-color(95, 14, 0, 255, 8, 8)
+ bounds: 760 112 8 8
+ - image: solid-color(96, 14, 0, 255, 8, 8)
+ bounds: 768 112 8 8
+ - image: solid-color(97, 14, 0, 255, 8, 8)
+ bounds: 776 112 8 8
+ - image: solid-color(98, 14, 0, 255, 8, 8)
+ bounds: 784 112 8 8
+ - image: solid-color(99, 14, 0, 255, 8, 8)
+ bounds: 792 112 8 8
+ - image: solid-color(100, 14, 0, 255, 8, 8)
+ bounds: 800 112 8 8
+ - image: solid-color(101, 14, 0, 255, 8, 8)
+ bounds: 808 112 8 8
+ - image: solid-color(102, 14, 0, 255, 8, 8)
+ bounds: 816 112 8 8
+ - image: solid-color(103, 14, 0, 255, 8, 8)
+ bounds: 824 112 8 8
+ - image: solid-color(104, 14, 0, 255, 8, 8)
+ bounds: 832 112 8 8
+ - image: solid-color(105, 14, 0, 255, 8, 8)
+ bounds: 840 112 8 8
+ - image: solid-color(106, 14, 0, 255, 8, 8)
+ bounds: 848 112 8 8
+ - image: solid-color(107, 14, 0, 255, 8, 8)
+ bounds: 856 112 8 8
+ - image: solid-color(108, 14, 0, 255, 8, 8)
+ bounds: 864 112 8 8
+ - image: solid-color(109, 14, 0, 255, 8, 8)
+ bounds: 872 112 8 8
+ - image: solid-color(110, 14, 0, 255, 8, 8)
+ bounds: 880 112 8 8
+ - image: solid-color(111, 14, 0, 255, 8, 8)
+ bounds: 888 112 8 8
+ - image: solid-color(112, 14, 0, 255, 8, 8)
+ bounds: 896 112 8 8
+ - image: solid-color(113, 14, 0, 255, 8, 8)
+ bounds: 904 112 8 8
+ - image: solid-color(114, 14, 0, 255, 8, 8)
+ bounds: 912 112 8 8
+ - image: solid-color(115, 14, 0, 255, 8, 8)
+ bounds: 920 112 8 8
+ - image: solid-color(116, 14, 0, 255, 8, 8)
+ bounds: 928 112 8 8
+ - image: solid-color(117, 14, 0, 255, 8, 8)
+ bounds: 936 112 8 8
+ - image: solid-color(118, 14, 0, 255, 8, 8)
+ bounds: 944 112 8 8
+ - image: solid-color(119, 14, 0, 255, 8, 8)
+ bounds: 952 112 8 8
+ - image: solid-color(120, 14, 0, 255, 8, 8)
+ bounds: 960 112 8 8
+ - image: solid-color(121, 14, 0, 255, 8, 8)
+ bounds: 968 112 8 8
+ - image: solid-color(122, 14, 0, 255, 8, 8)
+ bounds: 976 112 8 8
+ - image: solid-color(123, 14, 0, 255, 8, 8)
+ bounds: 984 112 8 8
+ - image: solid-color(124, 14, 0, 255, 8, 8)
+ bounds: 992 112 8 8
+ - image: solid-color(125, 14, 0, 255, 8, 8)
+ bounds: 1000 112 8 8
+ - image: solid-color(126, 14, 0, 255, 8, 8)
+ bounds: 1008 112 8 8
+ - image: solid-color(127, 14, 0, 255, 8, 8)
+ bounds: 1016 112 8 8
+ - image: solid-color(0, 15, 0, 255, 8, 8)
+ bounds: 0 120 8 8
+ - image: solid-color(1, 15, 0, 255, 8, 8)
+ bounds: 8 120 8 8
+ - image: solid-color(2, 15, 0, 255, 8, 8)
+ bounds: 16 120 8 8
+ - image: solid-color(3, 15, 0, 255, 8, 8)
+ bounds: 24 120 8 8
+ - image: solid-color(4, 15, 0, 255, 8, 8)
+ bounds: 32 120 8 8
+ - image: solid-color(5, 15, 0, 255, 8, 8)
+ bounds: 40 120 8 8
+ - image: solid-color(6, 15, 0, 255, 8, 8)
+ bounds: 48 120 8 8
+ - image: solid-color(7, 15, 0, 255, 8, 8)
+ bounds: 56 120 8 8
+ - image: solid-color(8, 15, 0, 255, 8, 8)
+ bounds: 64 120 8 8
+ - image: solid-color(9, 15, 0, 255, 8, 8)
+ bounds: 72 120 8 8
+ - image: solid-color(10, 15, 0, 255, 8, 8)
+ bounds: 80 120 8 8
+ - image: solid-color(11, 15, 0, 255, 8, 8)
+ bounds: 88 120 8 8
+ - image: solid-color(12, 15, 0, 255, 8, 8)
+ bounds: 96 120 8 8
+ - image: solid-color(13, 15, 0, 255, 8, 8)
+ bounds: 104 120 8 8
+ - image: solid-color(14, 15, 0, 255, 8, 8)
+ bounds: 112 120 8 8
+ - image: solid-color(15, 15, 0, 255, 8, 8)
+ bounds: 120 120 8 8
+ - image: solid-color(16, 15, 0, 255, 8, 8)
+ bounds: 128 120 8 8
+ - image: solid-color(17, 15, 0, 255, 8, 8)
+ bounds: 136 120 8 8
+ - image: solid-color(18, 15, 0, 255, 8, 8)
+ bounds: 144 120 8 8
+ - image: solid-color(19, 15, 0, 255, 8, 8)
+ bounds: 152 120 8 8
+ - image: solid-color(20, 15, 0, 255, 8, 8)
+ bounds: 160 120 8 8
+ - image: solid-color(21, 15, 0, 255, 8, 8)
+ bounds: 168 120 8 8
+ - image: solid-color(22, 15, 0, 255, 8, 8)
+ bounds: 176 120 8 8
+ - image: solid-color(23, 15, 0, 255, 8, 8)
+ bounds: 184 120 8 8
+ - image: solid-color(24, 15, 0, 255, 8, 8)
+ bounds: 192 120 8 8
+ - image: solid-color(25, 15, 0, 255, 8, 8)
+ bounds: 200 120 8 8
+ - image: solid-color(26, 15, 0, 255, 8, 8)
+ bounds: 208 120 8 8
+ - image: solid-color(27, 15, 0, 255, 8, 8)
+ bounds: 216 120 8 8
+ - image: solid-color(28, 15, 0, 255, 8, 8)
+ bounds: 224 120 8 8
+ - image: solid-color(29, 15, 0, 255, 8, 8)
+ bounds: 232 120 8 8
+ - image: solid-color(30, 15, 0, 255, 8, 8)
+ bounds: 240 120 8 8
+ - image: solid-color(31, 15, 0, 255, 8, 8)
+ bounds: 248 120 8 8
+ - image: solid-color(32, 15, 0, 255, 8, 8)
+ bounds: 256 120 8 8
+ - image: solid-color(33, 15, 0, 255, 8, 8)
+ bounds: 264 120 8 8
+ - image: solid-color(34, 15, 0, 255, 8, 8)
+ bounds: 272 120 8 8
+ - image: solid-color(35, 15, 0, 255, 8, 8)
+ bounds: 280 120 8 8
+ - image: solid-color(36, 15, 0, 255, 8, 8)
+ bounds: 288 120 8 8
+ - image: solid-color(37, 15, 0, 255, 8, 8)
+ bounds: 296 120 8 8
+ - image: solid-color(38, 15, 0, 255, 8, 8)
+ bounds: 304 120 8 8
+ - image: solid-color(39, 15, 0, 255, 8, 8)
+ bounds: 312 120 8 8
+ - image: solid-color(40, 15, 0, 255, 8, 8)
+ bounds: 320 120 8 8
+ - image: solid-color(41, 15, 0, 255, 8, 8)
+ bounds: 328 120 8 8
+ - image: solid-color(42, 15, 0, 255, 8, 8)
+ bounds: 336 120 8 8
+ - image: solid-color(43, 15, 0, 255, 8, 8)
+ bounds: 344 120 8 8
+ - image: solid-color(44, 15, 0, 255, 8, 8)
+ bounds: 352 120 8 8
+ - image: solid-color(45, 15, 0, 255, 8, 8)
+ bounds: 360 120 8 8
+ - image: solid-color(46, 15, 0, 255, 8, 8)
+ bounds: 368 120 8 8
+ - image: solid-color(47, 15, 0, 255, 8, 8)
+ bounds: 376 120 8 8
+ - image: solid-color(48, 15, 0, 255, 8, 8)
+ bounds: 384 120 8 8
+ - image: solid-color(49, 15, 0, 255, 8, 8)
+ bounds: 392 120 8 8
+ - image: solid-color(50, 15, 0, 255, 8, 8)
+ bounds: 400 120 8 8
+ - image: solid-color(51, 15, 0, 255, 8, 8)
+ bounds: 408 120 8 8
+ - image: solid-color(52, 15, 0, 255, 8, 8)
+ bounds: 416 120 8 8
+ - image: solid-color(53, 15, 0, 255, 8, 8)
+ bounds: 424 120 8 8
+ - image: solid-color(54, 15, 0, 255, 8, 8)
+ bounds: 432 120 8 8
+ - image: solid-color(55, 15, 0, 255, 8, 8)
+ bounds: 440 120 8 8
+ - image: solid-color(56, 15, 0, 255, 8, 8)
+ bounds: 448 120 8 8
+ - image: solid-color(57, 15, 0, 255, 8, 8)
+ bounds: 456 120 8 8
+ - image: solid-color(58, 15, 0, 255, 8, 8)
+ bounds: 464 120 8 8
+ - image: solid-color(59, 15, 0, 255, 8, 8)
+ bounds: 472 120 8 8
+ - image: solid-color(60, 15, 0, 255, 8, 8)
+ bounds: 480 120 8 8
+ - image: solid-color(61, 15, 0, 255, 8, 8)
+ bounds: 488 120 8 8
+ - image: solid-color(62, 15, 0, 255, 8, 8)
+ bounds: 496 120 8 8
+ - image: solid-color(63, 15, 0, 255, 8, 8)
+ bounds: 504 120 8 8
+ - image: solid-color(64, 15, 0, 255, 8, 8)
+ bounds: 512 120 8 8
+ - image: solid-color(65, 15, 0, 255, 8, 8)
+ bounds: 520 120 8 8
+ - image: solid-color(66, 15, 0, 255, 8, 8)
+ bounds: 528 120 8 8
+ - image: solid-color(67, 15, 0, 255, 8, 8)
+ bounds: 536 120 8 8
+ - image: solid-color(68, 15, 0, 255, 8, 8)
+ bounds: 544 120 8 8
+ - image: solid-color(69, 15, 0, 255, 8, 8)
+ bounds: 552 120 8 8
+ - image: solid-color(70, 15, 0, 255, 8, 8)
+ bounds: 560 120 8 8
+ - image: solid-color(71, 15, 0, 255, 8, 8)
+ bounds: 568 120 8 8
+ - image: solid-color(72, 15, 0, 255, 8, 8)
+ bounds: 576 120 8 8
+ - image: solid-color(73, 15, 0, 255, 8, 8)
+ bounds: 584 120 8 8
+ - image: solid-color(74, 15, 0, 255, 8, 8)
+ bounds: 592 120 8 8
+ - image: solid-color(75, 15, 0, 255, 8, 8)
+ bounds: 600 120 8 8
+ - image: solid-color(76, 15, 0, 255, 8, 8)
+ bounds: 608 120 8 8
+ - image: solid-color(77, 15, 0, 255, 8, 8)
+ bounds: 616 120 8 8
+ - image: solid-color(78, 15, 0, 255, 8, 8)
+ bounds: 624 120 8 8
+ - image: solid-color(79, 15, 0, 255, 8, 8)
+ bounds: 632 120 8 8
+ - image: solid-color(80, 15, 0, 255, 8, 8)
+ bounds: 640 120 8 8
+ - image: solid-color(81, 15, 0, 255, 8, 8)
+ bounds: 648 120 8 8
+ - image: solid-color(82, 15, 0, 255, 8, 8)
+ bounds: 656 120 8 8
+ - image: solid-color(83, 15, 0, 255, 8, 8)
+ bounds: 664 120 8 8
+ - image: solid-color(84, 15, 0, 255, 8, 8)
+ bounds: 672 120 8 8
+ - image: solid-color(85, 15, 0, 255, 8, 8)
+ bounds: 680 120 8 8
+ - image: solid-color(86, 15, 0, 255, 8, 8)
+ bounds: 688 120 8 8
+ - image: solid-color(87, 15, 0, 255, 8, 8)
+ bounds: 696 120 8 8
+ - image: solid-color(88, 15, 0, 255, 8, 8)
+ bounds: 704 120 8 8
+ - image: solid-color(89, 15, 0, 255, 8, 8)
+ bounds: 712 120 8 8
+ - image: solid-color(90, 15, 0, 255, 8, 8)
+ bounds: 720 120 8 8
+ - image: solid-color(91, 15, 0, 255, 8, 8)
+ bounds: 728 120 8 8
+ - image: solid-color(92, 15, 0, 255, 8, 8)
+ bounds: 736 120 8 8
+ - image: solid-color(93, 15, 0, 255, 8, 8)
+ bounds: 744 120 8 8
+ - image: solid-color(94, 15, 0, 255, 8, 8)
+ bounds: 752 120 8 8
+ - image: solid-color(95, 15, 0, 255, 8, 8)
+ bounds: 760 120 8 8
+ - image: solid-color(96, 15, 0, 255, 8, 8)
+ bounds: 768 120 8 8
+ - image: solid-color(97, 15, 0, 255, 8, 8)
+ bounds: 776 120 8 8
+ - image: solid-color(98, 15, 0, 255, 8, 8)
+ bounds: 784 120 8 8
+ - image: solid-color(99, 15, 0, 255, 8, 8)
+ bounds: 792 120 8 8
+ - image: solid-color(100, 15, 0, 255, 8, 8)
+ bounds: 800 120 8 8
+ - image: solid-color(101, 15, 0, 255, 8, 8)
+ bounds: 808 120 8 8
+ - image: solid-color(102, 15, 0, 255, 8, 8)
+ bounds: 816 120 8 8
+ - image: solid-color(103, 15, 0, 255, 8, 8)
+ bounds: 824 120 8 8
+ - image: solid-color(104, 15, 0, 255, 8, 8)
+ bounds: 832 120 8 8
+ - image: solid-color(105, 15, 0, 255, 8, 8)
+ bounds: 840 120 8 8
+ - image: solid-color(106, 15, 0, 255, 8, 8)
+ bounds: 848 120 8 8
+ - image: solid-color(107, 15, 0, 255, 8, 8)
+ bounds: 856 120 8 8
+ - image: solid-color(108, 15, 0, 255, 8, 8)
+ bounds: 864 120 8 8
+ - image: solid-color(109, 15, 0, 255, 8, 8)
+ bounds: 872 120 8 8
+ - image: solid-color(110, 15, 0, 255, 8, 8)
+ bounds: 880 120 8 8
+ - image: solid-color(111, 15, 0, 255, 8, 8)
+ bounds: 888 120 8 8
+ - image: solid-color(112, 15, 0, 255, 8, 8)
+ bounds: 896 120 8 8
+ - image: solid-color(113, 15, 0, 255, 8, 8)
+ bounds: 904 120 8 8
+ - image: solid-color(114, 15, 0, 255, 8, 8)
+ bounds: 912 120 8 8
+ - image: solid-color(115, 15, 0, 255, 8, 8)
+ bounds: 920 120 8 8
+ - image: solid-color(116, 15, 0, 255, 8, 8)
+ bounds: 928 120 8 8
+ - image: solid-color(117, 15, 0, 255, 8, 8)
+ bounds: 936 120 8 8
+ - image: solid-color(118, 15, 0, 255, 8, 8)
+ bounds: 944 120 8 8
+ - image: solid-color(119, 15, 0, 255, 8, 8)
+ bounds: 952 120 8 8
+ - image: solid-color(120, 15, 0, 255, 8, 8)
+ bounds: 960 120 8 8
+ - image: solid-color(121, 15, 0, 255, 8, 8)
+ bounds: 968 120 8 8
+ - image: solid-color(122, 15, 0, 255, 8, 8)
+ bounds: 976 120 8 8
+ - image: solid-color(123, 15, 0, 255, 8, 8)
+ bounds: 984 120 8 8
+ - image: solid-color(124, 15, 0, 255, 8, 8)
+ bounds: 992 120 8 8
+ - image: solid-color(125, 15, 0, 255, 8, 8)
+ bounds: 1000 120 8 8
+ - image: solid-color(126, 15, 0, 255, 8, 8)
+ bounds: 1008 120 8 8
+ - image: solid-color(127, 15, 0, 255, 8, 8)
+ bounds: 1016 120 8 8
+ - image: solid-color(0, 16, 0, 255, 8, 8)
+ bounds: 0 128 8 8
+ - image: solid-color(1, 16, 0, 255, 8, 8)
+ bounds: 8 128 8 8
+ - image: solid-color(2, 16, 0, 255, 8, 8)
+ bounds: 16 128 8 8
+ - image: solid-color(3, 16, 0, 255, 8, 8)
+ bounds: 24 128 8 8
+ - image: solid-color(4, 16, 0, 255, 8, 8)
+ bounds: 32 128 8 8
+ - image: solid-color(5, 16, 0, 255, 8, 8)
+ bounds: 40 128 8 8
+ - image: solid-color(6, 16, 0, 255, 8, 8)
+ bounds: 48 128 8 8
+ - image: solid-color(7, 16, 0, 255, 8, 8)
+ bounds: 56 128 8 8
+ - image: solid-color(8, 16, 0, 255, 8, 8)
+ bounds: 64 128 8 8
+ - image: solid-color(9, 16, 0, 255, 8, 8)
+ bounds: 72 128 8 8
+ - image: solid-color(10, 16, 0, 255, 8, 8)
+ bounds: 80 128 8 8
+ - image: solid-color(11, 16, 0, 255, 8, 8)
+ bounds: 88 128 8 8
+ - image: solid-color(12, 16, 0, 255, 8, 8)
+ bounds: 96 128 8 8
+ - image: solid-color(13, 16, 0, 255, 8, 8)
+ bounds: 104 128 8 8
+ - image: solid-color(14, 16, 0, 255, 8, 8)
+ bounds: 112 128 8 8
+ - image: solid-color(15, 16, 0, 255, 8, 8)
+ bounds: 120 128 8 8
+ - image: solid-color(16, 16, 0, 255, 8, 8)
+ bounds: 128 128 8 8
+ - image: solid-color(17, 16, 0, 255, 8, 8)
+ bounds: 136 128 8 8
+ - image: solid-color(18, 16, 0, 255, 8, 8)
+ bounds: 144 128 8 8
+ - image: solid-color(19, 16, 0, 255, 8, 8)
+ bounds: 152 128 8 8
+ - image: solid-color(20, 16, 0, 255, 8, 8)
+ bounds: 160 128 8 8
+ - image: solid-color(21, 16, 0, 255, 8, 8)
+ bounds: 168 128 8 8
+ - image: solid-color(22, 16, 0, 255, 8, 8)
+ bounds: 176 128 8 8
+ - image: solid-color(23, 16, 0, 255, 8, 8)
+ bounds: 184 128 8 8
+ - image: solid-color(24, 16, 0, 255, 8, 8)
+ bounds: 192 128 8 8
+ - image: solid-color(25, 16, 0, 255, 8, 8)
+ bounds: 200 128 8 8
+ - image: solid-color(26, 16, 0, 255, 8, 8)
+ bounds: 208 128 8 8
+ - image: solid-color(27, 16, 0, 255, 8, 8)
+ bounds: 216 128 8 8
+ - image: solid-color(28, 16, 0, 255, 8, 8)
+ bounds: 224 128 8 8
+ - image: solid-color(29, 16, 0, 255, 8, 8)
+ bounds: 232 128 8 8
+ - image: solid-color(30, 16, 0, 255, 8, 8)
+ bounds: 240 128 8 8
+ - image: solid-color(31, 16, 0, 255, 8, 8)
+ bounds: 248 128 8 8
+ - image: solid-color(32, 16, 0, 255, 8, 8)
+ bounds: 256 128 8 8
+ - image: solid-color(33, 16, 0, 255, 8, 8)
+ bounds: 264 128 8 8
+ - image: solid-color(34, 16, 0, 255, 8, 8)
+ bounds: 272 128 8 8
+ - image: solid-color(35, 16, 0, 255, 8, 8)
+ bounds: 280 128 8 8
+ - image: solid-color(36, 16, 0, 255, 8, 8)
+ bounds: 288 128 8 8
+ - image: solid-color(37, 16, 0, 255, 8, 8)
+ bounds: 296 128 8 8
+ - image: solid-color(38, 16, 0, 255, 8, 8)
+ bounds: 304 128 8 8
+ - image: solid-color(39, 16, 0, 255, 8, 8)
+ bounds: 312 128 8 8
+ - image: solid-color(40, 16, 0, 255, 8, 8)
+ bounds: 320 128 8 8
+ - image: solid-color(41, 16, 0, 255, 8, 8)
+ bounds: 328 128 8 8
+ - image: solid-color(42, 16, 0, 255, 8, 8)
+ bounds: 336 128 8 8
+ - image: solid-color(43, 16, 0, 255, 8, 8)
+ bounds: 344 128 8 8
+ - image: solid-color(44, 16, 0, 255, 8, 8)
+ bounds: 352 128 8 8
+ - image: solid-color(45, 16, 0, 255, 8, 8)
+ bounds: 360 128 8 8
+ - image: solid-color(46, 16, 0, 255, 8, 8)
+ bounds: 368 128 8 8
+ - image: solid-color(47, 16, 0, 255, 8, 8)
+ bounds: 376 128 8 8
+ - image: solid-color(48, 16, 0, 255, 8, 8)
+ bounds: 384 128 8 8
+ - image: solid-color(49, 16, 0, 255, 8, 8)
+ bounds: 392 128 8 8
+ - image: solid-color(50, 16, 0, 255, 8, 8)
+ bounds: 400 128 8 8
+ - image: solid-color(51, 16, 0, 255, 8, 8)
+ bounds: 408 128 8 8
+ - image: solid-color(52, 16, 0, 255, 8, 8)
+ bounds: 416 128 8 8
+ - image: solid-color(53, 16, 0, 255, 8, 8)
+ bounds: 424 128 8 8
+ - image: solid-color(54, 16, 0, 255, 8, 8)
+ bounds: 432 128 8 8
+ - image: solid-color(55, 16, 0, 255, 8, 8)
+ bounds: 440 128 8 8
+ - image: solid-color(56, 16, 0, 255, 8, 8)
+ bounds: 448 128 8 8
+ - image: solid-color(57, 16, 0, 255, 8, 8)
+ bounds: 456 128 8 8
+ - image: solid-color(58, 16, 0, 255, 8, 8)
+ bounds: 464 128 8 8
+ - image: solid-color(59, 16, 0, 255, 8, 8)
+ bounds: 472 128 8 8
+ - image: solid-color(60, 16, 0, 255, 8, 8)
+ bounds: 480 128 8 8
+ - image: solid-color(61, 16, 0, 255, 8, 8)
+ bounds: 488 128 8 8
+ - image: solid-color(62, 16, 0, 255, 8, 8)
+ bounds: 496 128 8 8
+ - image: solid-color(63, 16, 0, 255, 8, 8)
+ bounds: 504 128 8 8
+ - image: solid-color(64, 16, 0, 255, 8, 8)
+ bounds: 512 128 8 8
+ - image: solid-color(65, 16, 0, 255, 8, 8)
+ bounds: 520 128 8 8
+ - image: solid-color(66, 16, 0, 255, 8, 8)
+ bounds: 528 128 8 8
+ - image: solid-color(67, 16, 0, 255, 8, 8)
+ bounds: 536 128 8 8
+ - image: solid-color(68, 16, 0, 255, 8, 8)
+ bounds: 544 128 8 8
+ - image: solid-color(69, 16, 0, 255, 8, 8)
+ bounds: 552 128 8 8
+ - image: solid-color(70, 16, 0, 255, 8, 8)
+ bounds: 560 128 8 8
+ - image: solid-color(71, 16, 0, 255, 8, 8)
+ bounds: 568 128 8 8
+ - image: solid-color(72, 16, 0, 255, 8, 8)
+ bounds: 576 128 8 8
+ - image: solid-color(73, 16, 0, 255, 8, 8)
+ bounds: 584 128 8 8
+ - image: solid-color(74, 16, 0, 255, 8, 8)
+ bounds: 592 128 8 8
+ - image: solid-color(75, 16, 0, 255, 8, 8)
+ bounds: 600 128 8 8
+ - image: solid-color(76, 16, 0, 255, 8, 8)
+ bounds: 608 128 8 8
+ - image: solid-color(77, 16, 0, 255, 8, 8)
+ bounds: 616 128 8 8
+ - image: solid-color(78, 16, 0, 255, 8, 8)
+ bounds: 624 128 8 8
+ - image: solid-color(79, 16, 0, 255, 8, 8)
+ bounds: 632 128 8 8
+ - image: solid-color(80, 16, 0, 255, 8, 8)
+ bounds: 640 128 8 8
+ - image: solid-color(81, 16, 0, 255, 8, 8)
+ bounds: 648 128 8 8
+ - image: solid-color(82, 16, 0, 255, 8, 8)
+ bounds: 656 128 8 8
+ - image: solid-color(83, 16, 0, 255, 8, 8)
+ bounds: 664 128 8 8
+ - image: solid-color(84, 16, 0, 255, 8, 8)
+ bounds: 672 128 8 8
+ - image: solid-color(85, 16, 0, 255, 8, 8)
+ bounds: 680 128 8 8
+ - image: solid-color(86, 16, 0, 255, 8, 8)
+ bounds: 688 128 8 8
+ - image: solid-color(87, 16, 0, 255, 8, 8)
+ bounds: 696 128 8 8
+ - image: solid-color(88, 16, 0, 255, 8, 8)
+ bounds: 704 128 8 8
+ - image: solid-color(89, 16, 0, 255, 8, 8)
+ bounds: 712 128 8 8
+ - image: solid-color(90, 16, 0, 255, 8, 8)
+ bounds: 720 128 8 8
+ - image: solid-color(91, 16, 0, 255, 8, 8)
+ bounds: 728 128 8 8
+ - image: solid-color(92, 16, 0, 255, 8, 8)
+ bounds: 736 128 8 8
+ - image: solid-color(93, 16, 0, 255, 8, 8)
+ bounds: 744 128 8 8
+ - image: solid-color(94, 16, 0, 255, 8, 8)
+ bounds: 752 128 8 8
+ - image: solid-color(95, 16, 0, 255, 8, 8)
+ bounds: 760 128 8 8
+ - image: solid-color(96, 16, 0, 255, 8, 8)
+ bounds: 768 128 8 8
+ - image: solid-color(97, 16, 0, 255, 8, 8)
+ bounds: 776 128 8 8
+ - image: solid-color(98, 16, 0, 255, 8, 8)
+ bounds: 784 128 8 8
+ - image: solid-color(99, 16, 0, 255, 8, 8)
+ bounds: 792 128 8 8
+ - image: solid-color(100, 16, 0, 255, 8, 8)
+ bounds: 800 128 8 8
+ - image: solid-color(101, 16, 0, 255, 8, 8)
+ bounds: 808 128 8 8
+ - image: solid-color(102, 16, 0, 255, 8, 8)
+ bounds: 816 128 8 8
+ - image: solid-color(103, 16, 0, 255, 8, 8)
+ bounds: 824 128 8 8
+ - image: solid-color(104, 16, 0, 255, 8, 8)
+ bounds: 832 128 8 8
+ - image: solid-color(105, 16, 0, 255, 8, 8)
+ bounds: 840 128 8 8
+ - image: solid-color(106, 16, 0, 255, 8, 8)
+ bounds: 848 128 8 8
+ - image: solid-color(107, 16, 0, 255, 8, 8)
+ bounds: 856 128 8 8
+ - image: solid-color(108, 16, 0, 255, 8, 8)
+ bounds: 864 128 8 8
+ - image: solid-color(109, 16, 0, 255, 8, 8)
+ bounds: 872 128 8 8
+ - image: solid-color(110, 16, 0, 255, 8, 8)
+ bounds: 880 128 8 8
+ - image: solid-color(111, 16, 0, 255, 8, 8)
+ bounds: 888 128 8 8
+ - image: solid-color(112, 16, 0, 255, 8, 8)
+ bounds: 896 128 8 8
+ - image: solid-color(113, 16, 0, 255, 8, 8)
+ bounds: 904 128 8 8
+ - image: solid-color(114, 16, 0, 255, 8, 8)
+ bounds: 912 128 8 8
+ - image: solid-color(115, 16, 0, 255, 8, 8)
+ bounds: 920 128 8 8
+ - image: solid-color(116, 16, 0, 255, 8, 8)
+ bounds: 928 128 8 8
+ - image: solid-color(117, 16, 0, 255, 8, 8)
+ bounds: 936 128 8 8
+ - image: solid-color(118, 16, 0, 255, 8, 8)
+ bounds: 944 128 8 8
+ - image: solid-color(119, 16, 0, 255, 8, 8)
+ bounds: 952 128 8 8
+ - image: solid-color(120, 16, 0, 255, 8, 8)
+ bounds: 960 128 8 8
+ - image: solid-color(121, 16, 0, 255, 8, 8)
+ bounds: 968 128 8 8
+ - image: solid-color(122, 16, 0, 255, 8, 8)
+ bounds: 976 128 8 8
+ - image: solid-color(123, 16, 0, 255, 8, 8)
+ bounds: 984 128 8 8
+ - image: solid-color(124, 16, 0, 255, 8, 8)
+ bounds: 992 128 8 8
+ - image: solid-color(125, 16, 0, 255, 8, 8)
+ bounds: 1000 128 8 8
+ - image: solid-color(126, 16, 0, 255, 8, 8)
+ bounds: 1008 128 8 8
+ - image: solid-color(127, 16, 0, 255, 8, 8)
+ bounds: 1016 128 8 8
+ - image: solid-color(0, 17, 0, 255, 8, 8)
+ bounds: 0 136 8 8
+ - image: solid-color(1, 17, 0, 255, 8, 8)
+ bounds: 8 136 8 8
+ - image: solid-color(2, 17, 0, 255, 8, 8)
+ bounds: 16 136 8 8
+ - image: solid-color(3, 17, 0, 255, 8, 8)
+ bounds: 24 136 8 8
+ - image: solid-color(4, 17, 0, 255, 8, 8)
+ bounds: 32 136 8 8
+ - image: solid-color(5, 17, 0, 255, 8, 8)
+ bounds: 40 136 8 8
+ - image: solid-color(6, 17, 0, 255, 8, 8)
+ bounds: 48 136 8 8
+ - image: solid-color(7, 17, 0, 255, 8, 8)
+ bounds: 56 136 8 8
+ - image: solid-color(8, 17, 0, 255, 8, 8)
+ bounds: 64 136 8 8
+ - image: solid-color(9, 17, 0, 255, 8, 8)
+ bounds: 72 136 8 8
+ - image: solid-color(10, 17, 0, 255, 8, 8)
+ bounds: 80 136 8 8
+ - image: solid-color(11, 17, 0, 255, 8, 8)
+ bounds: 88 136 8 8
+ - image: solid-color(12, 17, 0, 255, 8, 8)
+ bounds: 96 136 8 8
+ - image: solid-color(13, 17, 0, 255, 8, 8)
+ bounds: 104 136 8 8
+ - image: solid-color(14, 17, 0, 255, 8, 8)
+ bounds: 112 136 8 8
+ - image: solid-color(15, 17, 0, 255, 8, 8)
+ bounds: 120 136 8 8
+ - image: solid-color(16, 17, 0, 255, 8, 8)
+ bounds: 128 136 8 8
+ - image: solid-color(17, 17, 0, 255, 8, 8)
+ bounds: 136 136 8 8
+ - image: solid-color(18, 17, 0, 255, 8, 8)
+ bounds: 144 136 8 8
+ - image: solid-color(19, 17, 0, 255, 8, 8)
+ bounds: 152 136 8 8
+ - image: solid-color(20, 17, 0, 255, 8, 8)
+ bounds: 160 136 8 8
+ - image: solid-color(21, 17, 0, 255, 8, 8)
+ bounds: 168 136 8 8
+ - image: solid-color(22, 17, 0, 255, 8, 8)
+ bounds: 176 136 8 8
+ - image: solid-color(23, 17, 0, 255, 8, 8)
+ bounds: 184 136 8 8
+ - image: solid-color(24, 17, 0, 255, 8, 8)
+ bounds: 192 136 8 8
+ - image: solid-color(25, 17, 0, 255, 8, 8)
+ bounds: 200 136 8 8
+ - image: solid-color(26, 17, 0, 255, 8, 8)
+ bounds: 208 136 8 8
+ - image: solid-color(27, 17, 0, 255, 8, 8)
+ bounds: 216 136 8 8
+ - image: solid-color(28, 17, 0, 255, 8, 8)
+ bounds: 224 136 8 8
+ - image: solid-color(29, 17, 0, 255, 8, 8)
+ bounds: 232 136 8 8
+ - image: solid-color(30, 17, 0, 255, 8, 8)
+ bounds: 240 136 8 8
+ - image: solid-color(31, 17, 0, 255, 8, 8)
+ bounds: 248 136 8 8
+ - image: solid-color(32, 17, 0, 255, 8, 8)
+ bounds: 256 136 8 8
+ - image: solid-color(33, 17, 0, 255, 8, 8)
+ bounds: 264 136 8 8
+ - image: solid-color(34, 17, 0, 255, 8, 8)
+ bounds: 272 136 8 8
+ - image: solid-color(35, 17, 0, 255, 8, 8)
+ bounds: 280 136 8 8
+ - image: solid-color(36, 17, 0, 255, 8, 8)
+ bounds: 288 136 8 8
+ - image: solid-color(37, 17, 0, 255, 8, 8)
+ bounds: 296 136 8 8
+ - image: solid-color(38, 17, 0, 255, 8, 8)
+ bounds: 304 136 8 8
+ - image: solid-color(39, 17, 0, 255, 8, 8)
+ bounds: 312 136 8 8
+ - image: solid-color(40, 17, 0, 255, 8, 8)
+ bounds: 320 136 8 8
+ - image: solid-color(41, 17, 0, 255, 8, 8)
+ bounds: 328 136 8 8
+ - image: solid-color(42, 17, 0, 255, 8, 8)
+ bounds: 336 136 8 8
+ - image: solid-color(43, 17, 0, 255, 8, 8)
+ bounds: 344 136 8 8
+ - image: solid-color(44, 17, 0, 255, 8, 8)
+ bounds: 352 136 8 8
+ - image: solid-color(45, 17, 0, 255, 8, 8)
+ bounds: 360 136 8 8
+ - image: solid-color(46, 17, 0, 255, 8, 8)
+ bounds: 368 136 8 8
+ - image: solid-color(47, 17, 0, 255, 8, 8)
+ bounds: 376 136 8 8
+ - image: solid-color(48, 17, 0, 255, 8, 8)
+ bounds: 384 136 8 8
+ - image: solid-color(49, 17, 0, 255, 8, 8)
+ bounds: 392 136 8 8
+ - image: solid-color(50, 17, 0, 255, 8, 8)
+ bounds: 400 136 8 8
+ - image: solid-color(51, 17, 0, 255, 8, 8)
+ bounds: 408 136 8 8
+ - image: solid-color(52, 17, 0, 255, 8, 8)
+ bounds: 416 136 8 8
+ - image: solid-color(53, 17, 0, 255, 8, 8)
+ bounds: 424 136 8 8
+ - image: solid-color(54, 17, 0, 255, 8, 8)
+ bounds: 432 136 8 8
+ - image: solid-color(55, 17, 0, 255, 8, 8)
+ bounds: 440 136 8 8
+ - image: solid-color(56, 17, 0, 255, 8, 8)
+ bounds: 448 136 8 8
+ - image: solid-color(57, 17, 0, 255, 8, 8)
+ bounds: 456 136 8 8
+ - image: solid-color(58, 17, 0, 255, 8, 8)
+ bounds: 464 136 8 8
+ - image: solid-color(59, 17, 0, 255, 8, 8)
+ bounds: 472 136 8 8
+ - image: solid-color(60, 17, 0, 255, 8, 8)
+ bounds: 480 136 8 8
+ - image: solid-color(61, 17, 0, 255, 8, 8)
+ bounds: 488 136 8 8
+ - image: solid-color(62, 17, 0, 255, 8, 8)
+ bounds: 496 136 8 8
+ - image: solid-color(63, 17, 0, 255, 8, 8)
+ bounds: 504 136 8 8
+ - image: solid-color(64, 17, 0, 255, 8, 8)
+ bounds: 512 136 8 8
+ - image: solid-color(65, 17, 0, 255, 8, 8)
+ bounds: 520 136 8 8
+ - image: solid-color(66, 17, 0, 255, 8, 8)
+ bounds: 528 136 8 8
+ - image: solid-color(67, 17, 0, 255, 8, 8)
+ bounds: 536 136 8 8
+ - image: solid-color(68, 17, 0, 255, 8, 8)
+ bounds: 544 136 8 8
+ - image: solid-color(69, 17, 0, 255, 8, 8)
+ bounds: 552 136 8 8
+ - image: solid-color(70, 17, 0, 255, 8, 8)
+ bounds: 560 136 8 8
+ - image: solid-color(71, 17, 0, 255, 8, 8)
+ bounds: 568 136 8 8
+ - image: solid-color(72, 17, 0, 255, 8, 8)
+ bounds: 576 136 8 8
+ - image: solid-color(73, 17, 0, 255, 8, 8)
+ bounds: 584 136 8 8
+ - image: solid-color(74, 17, 0, 255, 8, 8)
+ bounds: 592 136 8 8
+ - image: solid-color(75, 17, 0, 255, 8, 8)
+ bounds: 600 136 8 8
+ - image: solid-color(76, 17, 0, 255, 8, 8)
+ bounds: 608 136 8 8
+ - image: solid-color(77, 17, 0, 255, 8, 8)
+ bounds: 616 136 8 8
+ - image: solid-color(78, 17, 0, 255, 8, 8)
+ bounds: 624 136 8 8
+ - image: solid-color(79, 17, 0, 255, 8, 8)
+ bounds: 632 136 8 8
+ - image: solid-color(80, 17, 0, 255, 8, 8)
+ bounds: 640 136 8 8
+ - image: solid-color(81, 17, 0, 255, 8, 8)
+ bounds: 648 136 8 8
+ - image: solid-color(82, 17, 0, 255, 8, 8)
+ bounds: 656 136 8 8
+ - image: solid-color(83, 17, 0, 255, 8, 8)
+ bounds: 664 136 8 8
+ - image: solid-color(84, 17, 0, 255, 8, 8)
+ bounds: 672 136 8 8
+ - image: solid-color(85, 17, 0, 255, 8, 8)
+ bounds: 680 136 8 8
+ - image: solid-color(86, 17, 0, 255, 8, 8)
+ bounds: 688 136 8 8
+ - image: solid-color(87, 17, 0, 255, 8, 8)
+ bounds: 696 136 8 8
+ - image: solid-color(88, 17, 0, 255, 8, 8)
+ bounds: 704 136 8 8
+ - image: solid-color(89, 17, 0, 255, 8, 8)
+ bounds: 712 136 8 8
+ - image: solid-color(90, 17, 0, 255, 8, 8)
+ bounds: 720 136 8 8
+ - image: solid-color(91, 17, 0, 255, 8, 8)
+ bounds: 728 136 8 8
+ - image: solid-color(92, 17, 0, 255, 8, 8)
+ bounds: 736 136 8 8
+ - image: solid-color(93, 17, 0, 255, 8, 8)
+ bounds: 744 136 8 8
+ - image: solid-color(94, 17, 0, 255, 8, 8)
+ bounds: 752 136 8 8
+ - image: solid-color(95, 17, 0, 255, 8, 8)
+ bounds: 760 136 8 8
+ - image: solid-color(96, 17, 0, 255, 8, 8)
+ bounds: 768 136 8 8
+ - image: solid-color(97, 17, 0, 255, 8, 8)
+ bounds: 776 136 8 8
+ - image: solid-color(98, 17, 0, 255, 8, 8)
+ bounds: 784 136 8 8
+ - image: solid-color(99, 17, 0, 255, 8, 8)
+ bounds: 792 136 8 8
+ - image: solid-color(100, 17, 0, 255, 8, 8)
+ bounds: 800 136 8 8
+ - image: solid-color(101, 17, 0, 255, 8, 8)
+ bounds: 808 136 8 8
+ - image: solid-color(102, 17, 0, 255, 8, 8)
+ bounds: 816 136 8 8
+ - image: solid-color(103, 17, 0, 255, 8, 8)
+ bounds: 824 136 8 8
+ - image: solid-color(104, 17, 0, 255, 8, 8)
+ bounds: 832 136 8 8
+ - image: solid-color(105, 17, 0, 255, 8, 8)
+ bounds: 840 136 8 8
+ - image: solid-color(106, 17, 0, 255, 8, 8)
+ bounds: 848 136 8 8
+ - image: solid-color(107, 17, 0, 255, 8, 8)
+ bounds: 856 136 8 8
+ - image: solid-color(108, 17, 0, 255, 8, 8)
+ bounds: 864 136 8 8
+ - image: solid-color(109, 17, 0, 255, 8, 8)
+ bounds: 872 136 8 8
+ - image: solid-color(110, 17, 0, 255, 8, 8)
+ bounds: 880 136 8 8
+ - image: solid-color(111, 17, 0, 255, 8, 8)
+ bounds: 888 136 8 8
+ - image: solid-color(112, 17, 0, 255, 8, 8)
+ bounds: 896 136 8 8
+ - image: solid-color(113, 17, 0, 255, 8, 8)
+ bounds: 904 136 8 8
+ - image: solid-color(114, 17, 0, 255, 8, 8)
+ bounds: 912 136 8 8
+ - image: solid-color(115, 17, 0, 255, 8, 8)
+ bounds: 920 136 8 8
+ - image: solid-color(116, 17, 0, 255, 8, 8)
+ bounds: 928 136 8 8
+ - image: solid-color(117, 17, 0, 255, 8, 8)
+ bounds: 936 136 8 8
+ - image: solid-color(118, 17, 0, 255, 8, 8)
+ bounds: 944 136 8 8
+ - image: solid-color(119, 17, 0, 255, 8, 8)
+ bounds: 952 136 8 8
+ - image: solid-color(120, 17, 0, 255, 8, 8)
+ bounds: 960 136 8 8
+ - image: solid-color(121, 17, 0, 255, 8, 8)
+ bounds: 968 136 8 8
+ - image: solid-color(122, 17, 0, 255, 8, 8)
+ bounds: 976 136 8 8
+ - image: solid-color(123, 17, 0, 255, 8, 8)
+ bounds: 984 136 8 8
+ - image: solid-color(124, 17, 0, 255, 8, 8)
+ bounds: 992 136 8 8
+ - image: solid-color(125, 17, 0, 255, 8, 8)
+ bounds: 1000 136 8 8
+ - image: solid-color(126, 17, 0, 255, 8, 8)
+ bounds: 1008 136 8 8
+ - image: solid-color(127, 17, 0, 255, 8, 8)
+ bounds: 1016 136 8 8
+ - image: solid-color(0, 18, 0, 255, 8, 8)
+ bounds: 0 144 8 8
+ - image: solid-color(1, 18, 0, 255, 8, 8)
+ bounds: 8 144 8 8
+ - image: solid-color(2, 18, 0, 255, 8, 8)
+ bounds: 16 144 8 8
+ - image: solid-color(3, 18, 0, 255, 8, 8)
+ bounds: 24 144 8 8
+ - image: solid-color(4, 18, 0, 255, 8, 8)
+ bounds: 32 144 8 8
+ - image: solid-color(5, 18, 0, 255, 8, 8)
+ bounds: 40 144 8 8
+ - image: solid-color(6, 18, 0, 255, 8, 8)
+ bounds: 48 144 8 8
+ - image: solid-color(7, 18, 0, 255, 8, 8)
+ bounds: 56 144 8 8
+ - image: solid-color(8, 18, 0, 255, 8, 8)
+ bounds: 64 144 8 8
+ - image: solid-color(9, 18, 0, 255, 8, 8)
+ bounds: 72 144 8 8
+ - image: solid-color(10, 18, 0, 255, 8, 8)
+ bounds: 80 144 8 8
+ - image: solid-color(11, 18, 0, 255, 8, 8)
+ bounds: 88 144 8 8
+ - image: solid-color(12, 18, 0, 255, 8, 8)
+ bounds: 96 144 8 8
+ - image: solid-color(13, 18, 0, 255, 8, 8)
+ bounds: 104 144 8 8
+ - image: solid-color(14, 18, 0, 255, 8, 8)
+ bounds: 112 144 8 8
+ - image: solid-color(15, 18, 0, 255, 8, 8)
+ bounds: 120 144 8 8
+ - image: solid-color(16, 18, 0, 255, 8, 8)
+ bounds: 128 144 8 8
+ - image: solid-color(17, 18, 0, 255, 8, 8)
+ bounds: 136 144 8 8
+ - image: solid-color(18, 18, 0, 255, 8, 8)
+ bounds: 144 144 8 8
+ - image: solid-color(19, 18, 0, 255, 8, 8)
+ bounds: 152 144 8 8
+ - image: solid-color(20, 18, 0, 255, 8, 8)
+ bounds: 160 144 8 8
+ - image: solid-color(21, 18, 0, 255, 8, 8)
+ bounds: 168 144 8 8
+ - image: solid-color(22, 18, 0, 255, 8, 8)
+ bounds: 176 144 8 8
+ - image: solid-color(23, 18, 0, 255, 8, 8)
+ bounds: 184 144 8 8
+ - image: solid-color(24, 18, 0, 255, 8, 8)
+ bounds: 192 144 8 8
+ - image: solid-color(25, 18, 0, 255, 8, 8)
+ bounds: 200 144 8 8
+ - image: solid-color(26, 18, 0, 255, 8, 8)
+ bounds: 208 144 8 8
+ - image: solid-color(27, 18, 0, 255, 8, 8)
+ bounds: 216 144 8 8
+ - image: solid-color(28, 18, 0, 255, 8, 8)
+ bounds: 224 144 8 8
+ - image: solid-color(29, 18, 0, 255, 8, 8)
+ bounds: 232 144 8 8
+ - image: solid-color(30, 18, 0, 255, 8, 8)
+ bounds: 240 144 8 8
+ - image: solid-color(31, 18, 0, 255, 8, 8)
+ bounds: 248 144 8 8
+ - image: solid-color(32, 18, 0, 255, 8, 8)
+ bounds: 256 144 8 8
+ - image: solid-color(33, 18, 0, 255, 8, 8)
+ bounds: 264 144 8 8
+ - image: solid-color(34, 18, 0, 255, 8, 8)
+ bounds: 272 144 8 8
+ - image: solid-color(35, 18, 0, 255, 8, 8)
+ bounds: 280 144 8 8
+ - image: solid-color(36, 18, 0, 255, 8, 8)
+ bounds: 288 144 8 8
+ - image: solid-color(37, 18, 0, 255, 8, 8)
+ bounds: 296 144 8 8
+ - image: solid-color(38, 18, 0, 255, 8, 8)
+ bounds: 304 144 8 8
+ - image: solid-color(39, 18, 0, 255, 8, 8)
+ bounds: 312 144 8 8
+ - image: solid-color(40, 18, 0, 255, 8, 8)
+ bounds: 320 144 8 8
+ - image: solid-color(41, 18, 0, 255, 8, 8)
+ bounds: 328 144 8 8
+ - image: solid-color(42, 18, 0, 255, 8, 8)
+ bounds: 336 144 8 8
+ - image: solid-color(43, 18, 0, 255, 8, 8)
+ bounds: 344 144 8 8
+ - image: solid-color(44, 18, 0, 255, 8, 8)
+ bounds: 352 144 8 8
+ - image: solid-color(45, 18, 0, 255, 8, 8)
+ bounds: 360 144 8 8
+ - image: solid-color(46, 18, 0, 255, 8, 8)
+ bounds: 368 144 8 8
+ - image: solid-color(47, 18, 0, 255, 8, 8)
+ bounds: 376 144 8 8
+ - image: solid-color(48, 18, 0, 255, 8, 8)
+ bounds: 384 144 8 8
+ - image: solid-color(49, 18, 0, 255, 8, 8)
+ bounds: 392 144 8 8
+ - image: solid-color(50, 18, 0, 255, 8, 8)
+ bounds: 400 144 8 8
+ - image: solid-color(51, 18, 0, 255, 8, 8)
+ bounds: 408 144 8 8
+ - image: solid-color(52, 18, 0, 255, 8, 8)
+ bounds: 416 144 8 8
+ - image: solid-color(53, 18, 0, 255, 8, 8)
+ bounds: 424 144 8 8
+ - image: solid-color(54, 18, 0, 255, 8, 8)
+ bounds: 432 144 8 8
+ - image: solid-color(55, 18, 0, 255, 8, 8)
+ bounds: 440 144 8 8
+ - image: solid-color(56, 18, 0, 255, 8, 8)
+ bounds: 448 144 8 8
+ - image: solid-color(57, 18, 0, 255, 8, 8)
+ bounds: 456 144 8 8
+ - image: solid-color(58, 18, 0, 255, 8, 8)
+ bounds: 464 144 8 8
+ - image: solid-color(59, 18, 0, 255, 8, 8)
+ bounds: 472 144 8 8
+ - image: solid-color(60, 18, 0, 255, 8, 8)
+ bounds: 480 144 8 8
+ - image: solid-color(61, 18, 0, 255, 8, 8)
+ bounds: 488 144 8 8
+ - image: solid-color(62, 18, 0, 255, 8, 8)
+ bounds: 496 144 8 8
+ - image: solid-color(63, 18, 0, 255, 8, 8)
+ bounds: 504 144 8 8
+ - image: solid-color(64, 18, 0, 255, 8, 8)
+ bounds: 512 144 8 8
+ - image: solid-color(65, 18, 0, 255, 8, 8)
+ bounds: 520 144 8 8
+ - image: solid-color(66, 18, 0, 255, 8, 8)
+ bounds: 528 144 8 8
+ - image: solid-color(67, 18, 0, 255, 8, 8)
+ bounds: 536 144 8 8
+ - image: solid-color(68, 18, 0, 255, 8, 8)
+ bounds: 544 144 8 8
+ - image: solid-color(69, 18, 0, 255, 8, 8)
+ bounds: 552 144 8 8
+ - image: solid-color(70, 18, 0, 255, 8, 8)
+ bounds: 560 144 8 8
+ - image: solid-color(71, 18, 0, 255, 8, 8)
+ bounds: 568 144 8 8
+ - image: solid-color(72, 18, 0, 255, 8, 8)
+ bounds: 576 144 8 8
+ - image: solid-color(73, 18, 0, 255, 8, 8)
+ bounds: 584 144 8 8
+ - image: solid-color(74, 18, 0, 255, 8, 8)
+ bounds: 592 144 8 8
+ - image: solid-color(75, 18, 0, 255, 8, 8)
+ bounds: 600 144 8 8
+ - image: solid-color(76, 18, 0, 255, 8, 8)
+ bounds: 608 144 8 8
+ - image: solid-color(77, 18, 0, 255, 8, 8)
+ bounds: 616 144 8 8
+ - image: solid-color(78, 18, 0, 255, 8, 8)
+ bounds: 624 144 8 8
+ - image: solid-color(79, 18, 0, 255, 8, 8)
+ bounds: 632 144 8 8
+ - image: solid-color(80, 18, 0, 255, 8, 8)
+ bounds: 640 144 8 8
+ - image: solid-color(81, 18, 0, 255, 8, 8)
+ bounds: 648 144 8 8
+ - image: solid-color(82, 18, 0, 255, 8, 8)
+ bounds: 656 144 8 8
+ - image: solid-color(83, 18, 0, 255, 8, 8)
+ bounds: 664 144 8 8
+ - image: solid-color(84, 18, 0, 255, 8, 8)
+ bounds: 672 144 8 8
+ - image: solid-color(85, 18, 0, 255, 8, 8)
+ bounds: 680 144 8 8
+ - image: solid-color(86, 18, 0, 255, 8, 8)
+ bounds: 688 144 8 8
+ - image: solid-color(87, 18, 0, 255, 8, 8)
+ bounds: 696 144 8 8
+ - image: solid-color(88, 18, 0, 255, 8, 8)
+ bounds: 704 144 8 8
+ - image: solid-color(89, 18, 0, 255, 8, 8)
+ bounds: 712 144 8 8
+ - image: solid-color(90, 18, 0, 255, 8, 8)
+ bounds: 720 144 8 8
+ - image: solid-color(91, 18, 0, 255, 8, 8)
+ bounds: 728 144 8 8
+ - image: solid-color(92, 18, 0, 255, 8, 8)
+ bounds: 736 144 8 8
+ - image: solid-color(93, 18, 0, 255, 8, 8)
+ bounds: 744 144 8 8
+ - image: solid-color(94, 18, 0, 255, 8, 8)
+ bounds: 752 144 8 8
+ - image: solid-color(95, 18, 0, 255, 8, 8)
+ bounds: 760 144 8 8
+ - image: solid-color(96, 18, 0, 255, 8, 8)
+ bounds: 768 144 8 8
+ - image: solid-color(97, 18, 0, 255, 8, 8)
+ bounds: 776 144 8 8
+ - image: solid-color(98, 18, 0, 255, 8, 8)
+ bounds: 784 144 8 8
+ - image: solid-color(99, 18, 0, 255, 8, 8)
+ bounds: 792 144 8 8
+ - image: solid-color(100, 18, 0, 255, 8, 8)
+ bounds: 800 144 8 8
+ - image: solid-color(101, 18, 0, 255, 8, 8)
+ bounds: 808 144 8 8
+ - image: solid-color(102, 18, 0, 255, 8, 8)
+ bounds: 816 144 8 8
+ - image: solid-color(103, 18, 0, 255, 8, 8)
+ bounds: 824 144 8 8
+ - image: solid-color(104, 18, 0, 255, 8, 8)
+ bounds: 832 144 8 8
+ - image: solid-color(105, 18, 0, 255, 8, 8)
+ bounds: 840 144 8 8
+ - image: solid-color(106, 18, 0, 255, 8, 8)
+ bounds: 848 144 8 8
+ - image: solid-color(107, 18, 0, 255, 8, 8)
+ bounds: 856 144 8 8
+ - image: solid-color(108, 18, 0, 255, 8, 8)
+ bounds: 864 144 8 8
+ - image: solid-color(109, 18, 0, 255, 8, 8)
+ bounds: 872 144 8 8
+ - image: solid-color(110, 18, 0, 255, 8, 8)
+ bounds: 880 144 8 8
+ - image: solid-color(111, 18, 0, 255, 8, 8)
+ bounds: 888 144 8 8
+ - image: solid-color(112, 18, 0, 255, 8, 8)
+ bounds: 896 144 8 8
+ - image: solid-color(113, 18, 0, 255, 8, 8)
+ bounds: 904 144 8 8
+ - image: solid-color(114, 18, 0, 255, 8, 8)
+ bounds: 912 144 8 8
+ - image: solid-color(115, 18, 0, 255, 8, 8)
+ bounds: 920 144 8 8
+ - image: solid-color(116, 18, 0, 255, 8, 8)
+ bounds: 928 144 8 8
+ - image: solid-color(117, 18, 0, 255, 8, 8)
+ bounds: 936 144 8 8
+ - image: solid-color(118, 18, 0, 255, 8, 8)
+ bounds: 944 144 8 8
+ - image: solid-color(119, 18, 0, 255, 8, 8)
+ bounds: 952 144 8 8
+ - image: solid-color(120, 18, 0, 255, 8, 8)
+ bounds: 960 144 8 8
+ - image: solid-color(121, 18, 0, 255, 8, 8)
+ bounds: 968 144 8 8
+ - image: solid-color(122, 18, 0, 255, 8, 8)
+ bounds: 976 144 8 8
+ - image: solid-color(123, 18, 0, 255, 8, 8)
+ bounds: 984 144 8 8
+ - image: solid-color(124, 18, 0, 255, 8, 8)
+ bounds: 992 144 8 8
+ - image: solid-color(125, 18, 0, 255, 8, 8)
+ bounds: 1000 144 8 8
+ - image: solid-color(126, 18, 0, 255, 8, 8)
+ bounds: 1008 144 8 8
+ - image: solid-color(127, 18, 0, 255, 8, 8)
+ bounds: 1016 144 8 8
+ - image: solid-color(0, 19, 0, 255, 8, 8)
+ bounds: 0 152 8 8
+ - image: solid-color(1, 19, 0, 255, 8, 8)
+ bounds: 8 152 8 8
+ - image: solid-color(2, 19, 0, 255, 8, 8)
+ bounds: 16 152 8 8
+ - image: solid-color(3, 19, 0, 255, 8, 8)
+ bounds: 24 152 8 8
+ - image: solid-color(4, 19, 0, 255, 8, 8)
+ bounds: 32 152 8 8
+ - image: solid-color(5, 19, 0, 255, 8, 8)
+ bounds: 40 152 8 8
+ - image: solid-color(6, 19, 0, 255, 8, 8)
+ bounds: 48 152 8 8
+ - image: solid-color(7, 19, 0, 255, 8, 8)
+ bounds: 56 152 8 8
+ - image: solid-color(8, 19, 0, 255, 8, 8)
+ bounds: 64 152 8 8
+ - image: solid-color(9, 19, 0, 255, 8, 8)
+ bounds: 72 152 8 8
+ - image: solid-color(10, 19, 0, 255, 8, 8)
+ bounds: 80 152 8 8
+ - image: solid-color(11, 19, 0, 255, 8, 8)
+ bounds: 88 152 8 8
+ - image: solid-color(12, 19, 0, 255, 8, 8)
+ bounds: 96 152 8 8
+ - image: solid-color(13, 19, 0, 255, 8, 8)
+ bounds: 104 152 8 8
+ - image: solid-color(14, 19, 0, 255, 8, 8)
+ bounds: 112 152 8 8
+ - image: solid-color(15, 19, 0, 255, 8, 8)
+ bounds: 120 152 8 8
+ - image: solid-color(16, 19, 0, 255, 8, 8)
+ bounds: 128 152 8 8
+ - image: solid-color(17, 19, 0, 255, 8, 8)
+ bounds: 136 152 8 8
+ - image: solid-color(18, 19, 0, 255, 8, 8)
+ bounds: 144 152 8 8
+ - image: solid-color(19, 19, 0, 255, 8, 8)
+ bounds: 152 152 8 8
+ - image: solid-color(20, 19, 0, 255, 8, 8)
+ bounds: 160 152 8 8
+ - image: solid-color(21, 19, 0, 255, 8, 8)
+ bounds: 168 152 8 8
+ - image: solid-color(22, 19, 0, 255, 8, 8)
+ bounds: 176 152 8 8
+ - image: solid-color(23, 19, 0, 255, 8, 8)
+ bounds: 184 152 8 8
+ - image: solid-color(24, 19, 0, 255, 8, 8)
+ bounds: 192 152 8 8
+ - image: solid-color(25, 19, 0, 255, 8, 8)
+ bounds: 200 152 8 8
+ - image: solid-color(26, 19, 0, 255, 8, 8)
+ bounds: 208 152 8 8
+ - image: solid-color(27, 19, 0, 255, 8, 8)
+ bounds: 216 152 8 8
+ - image: solid-color(28, 19, 0, 255, 8, 8)
+ bounds: 224 152 8 8
+ - image: solid-color(29, 19, 0, 255, 8, 8)
+ bounds: 232 152 8 8
+ - image: solid-color(30, 19, 0, 255, 8, 8)
+ bounds: 240 152 8 8
+ - image: solid-color(31, 19, 0, 255, 8, 8)
+ bounds: 248 152 8 8
+ - image: solid-color(32, 19, 0, 255, 8, 8)
+ bounds: 256 152 8 8
+ - image: solid-color(33, 19, 0, 255, 8, 8)
+ bounds: 264 152 8 8
+ - image: solid-color(34, 19, 0, 255, 8, 8)
+ bounds: 272 152 8 8
+ - image: solid-color(35, 19, 0, 255, 8, 8)
+ bounds: 280 152 8 8
+ - image: solid-color(36, 19, 0, 255, 8, 8)
+ bounds: 288 152 8 8
+ - image: solid-color(37, 19, 0, 255, 8, 8)
+ bounds: 296 152 8 8
+ - image: solid-color(38, 19, 0, 255, 8, 8)
+ bounds: 304 152 8 8
+ - image: solid-color(39, 19, 0, 255, 8, 8)
+ bounds: 312 152 8 8
+ - image: solid-color(40, 19, 0, 255, 8, 8)
+ bounds: 320 152 8 8
+ - image: solid-color(41, 19, 0, 255, 8, 8)
+ bounds: 328 152 8 8
+ - image: solid-color(42, 19, 0, 255, 8, 8)
+ bounds: 336 152 8 8
+ - image: solid-color(43, 19, 0, 255, 8, 8)
+ bounds: 344 152 8 8
+ - image: solid-color(44, 19, 0, 255, 8, 8)
+ bounds: 352 152 8 8
+ - image: solid-color(45, 19, 0, 255, 8, 8)
+ bounds: 360 152 8 8
+ - image: solid-color(46, 19, 0, 255, 8, 8)
+ bounds: 368 152 8 8
+ - image: solid-color(47, 19, 0, 255, 8, 8)
+ bounds: 376 152 8 8
+ - image: solid-color(48, 19, 0, 255, 8, 8)
+ bounds: 384 152 8 8
+ - image: solid-color(49, 19, 0, 255, 8, 8)
+ bounds: 392 152 8 8
+ - image: solid-color(50, 19, 0, 255, 8, 8)
+ bounds: 400 152 8 8
+ - image: solid-color(51, 19, 0, 255, 8, 8)
+ bounds: 408 152 8 8
+ - image: solid-color(52, 19, 0, 255, 8, 8)
+ bounds: 416 152 8 8
+ - image: solid-color(53, 19, 0, 255, 8, 8)
+ bounds: 424 152 8 8
+ - image: solid-color(54, 19, 0, 255, 8, 8)
+ bounds: 432 152 8 8
+ - image: solid-color(55, 19, 0, 255, 8, 8)
+ bounds: 440 152 8 8
+ - image: solid-color(56, 19, 0, 255, 8, 8)
+ bounds: 448 152 8 8
+ - image: solid-color(57, 19, 0, 255, 8, 8)
+ bounds: 456 152 8 8
+ - image: solid-color(58, 19, 0, 255, 8, 8)
+ bounds: 464 152 8 8
+ - image: solid-color(59, 19, 0, 255, 8, 8)
+ bounds: 472 152 8 8
+ - image: solid-color(60, 19, 0, 255, 8, 8)
+ bounds: 480 152 8 8
+ - image: solid-color(61, 19, 0, 255, 8, 8)
+ bounds: 488 152 8 8
+ - image: solid-color(62, 19, 0, 255, 8, 8)
+ bounds: 496 152 8 8
+ - image: solid-color(63, 19, 0, 255, 8, 8)
+ bounds: 504 152 8 8
+ - image: solid-color(64, 19, 0, 255, 8, 8)
+ bounds: 512 152 8 8
+ - image: solid-color(65, 19, 0, 255, 8, 8)
+ bounds: 520 152 8 8
+ - image: solid-color(66, 19, 0, 255, 8, 8)
+ bounds: 528 152 8 8
+ - image: solid-color(67, 19, 0, 255, 8, 8)
+ bounds: 536 152 8 8
+ - image: solid-color(68, 19, 0, 255, 8, 8)
+ bounds: 544 152 8 8
+ - image: solid-color(69, 19, 0, 255, 8, 8)
+ bounds: 552 152 8 8
+ - image: solid-color(70, 19, 0, 255, 8, 8)
+ bounds: 560 152 8 8
+ - image: solid-color(71, 19, 0, 255, 8, 8)
+ bounds: 568 152 8 8
+ - image: solid-color(72, 19, 0, 255, 8, 8)
+ bounds: 576 152 8 8
+ - image: solid-color(73, 19, 0, 255, 8, 8)
+ bounds: 584 152 8 8
+ - image: solid-color(74, 19, 0, 255, 8, 8)
+ bounds: 592 152 8 8
+ - image: solid-color(75, 19, 0, 255, 8, 8)
+ bounds: 600 152 8 8
+ - image: solid-color(76, 19, 0, 255, 8, 8)
+ bounds: 608 152 8 8
+ - image: solid-color(77, 19, 0, 255, 8, 8)
+ bounds: 616 152 8 8
+ - image: solid-color(78, 19, 0, 255, 8, 8)
+ bounds: 624 152 8 8
+ - image: solid-color(79, 19, 0, 255, 8, 8)
+ bounds: 632 152 8 8
+ - image: solid-color(80, 19, 0, 255, 8, 8)
+ bounds: 640 152 8 8
+ - image: solid-color(81, 19, 0, 255, 8, 8)
+ bounds: 648 152 8 8
+ - image: solid-color(82, 19, 0, 255, 8, 8)
+ bounds: 656 152 8 8
+ - image: solid-color(83, 19, 0, 255, 8, 8)
+ bounds: 664 152 8 8
+ - image: solid-color(84, 19, 0, 255, 8, 8)
+ bounds: 672 152 8 8
+ - image: solid-color(85, 19, 0, 255, 8, 8)
+ bounds: 680 152 8 8
+ - image: solid-color(86, 19, 0, 255, 8, 8)
+ bounds: 688 152 8 8
+ - image: solid-color(87, 19, 0, 255, 8, 8)
+ bounds: 696 152 8 8
+ - image: solid-color(88, 19, 0, 255, 8, 8)
+ bounds: 704 152 8 8
+ - image: solid-color(89, 19, 0, 255, 8, 8)
+ bounds: 712 152 8 8
+ - image: solid-color(90, 19, 0, 255, 8, 8)
+ bounds: 720 152 8 8
+ - image: solid-color(91, 19, 0, 255, 8, 8)
+ bounds: 728 152 8 8
+ - image: solid-color(92, 19, 0, 255, 8, 8)
+ bounds: 736 152 8 8
+ - image: solid-color(93, 19, 0, 255, 8, 8)
+ bounds: 744 152 8 8
+ - image: solid-color(94, 19, 0, 255, 8, 8)
+ bounds: 752 152 8 8
+ - image: solid-color(95, 19, 0, 255, 8, 8)
+ bounds: 760 152 8 8
+ - image: solid-color(96, 19, 0, 255, 8, 8)
+ bounds: 768 152 8 8
+ - image: solid-color(97, 19, 0, 255, 8, 8)
+ bounds: 776 152 8 8
+ - image: solid-color(98, 19, 0, 255, 8, 8)
+ bounds: 784 152 8 8
+ - image: solid-color(99, 19, 0, 255, 8, 8)
+ bounds: 792 152 8 8
+ - image: solid-color(100, 19, 0, 255, 8, 8)
+ bounds: 800 152 8 8
+ - image: solid-color(101, 19, 0, 255, 8, 8)
+ bounds: 808 152 8 8
+ - image: solid-color(102, 19, 0, 255, 8, 8)
+ bounds: 816 152 8 8
+ - image: solid-color(103, 19, 0, 255, 8, 8)
+ bounds: 824 152 8 8
+ - image: solid-color(104, 19, 0, 255, 8, 8)
+ bounds: 832 152 8 8
+ - image: solid-color(105, 19, 0, 255, 8, 8)
+ bounds: 840 152 8 8
+ - image: solid-color(106, 19, 0, 255, 8, 8)
+ bounds: 848 152 8 8
+ - image: solid-color(107, 19, 0, 255, 8, 8)
+ bounds: 856 152 8 8
+ - image: solid-color(108, 19, 0, 255, 8, 8)
+ bounds: 864 152 8 8
+ - image: solid-color(109, 19, 0, 255, 8, 8)
+ bounds: 872 152 8 8
+ - image: solid-color(110, 19, 0, 255, 8, 8)
+ bounds: 880 152 8 8
+ - image: solid-color(111, 19, 0, 255, 8, 8)
+ bounds: 888 152 8 8
+ - image: solid-color(112, 19, 0, 255, 8, 8)
+ bounds: 896 152 8 8
+ - image: solid-color(113, 19, 0, 255, 8, 8)
+ bounds: 904 152 8 8
+ - image: solid-color(114, 19, 0, 255, 8, 8)
+ bounds: 912 152 8 8
+ - image: solid-color(115, 19, 0, 255, 8, 8)
+ bounds: 920 152 8 8
+ - image: solid-color(116, 19, 0, 255, 8, 8)
+ bounds: 928 152 8 8
+ - image: solid-color(117, 19, 0, 255, 8, 8)
+ bounds: 936 152 8 8
+ - image: solid-color(118, 19, 0, 255, 8, 8)
+ bounds: 944 152 8 8
+ - image: solid-color(119, 19, 0, 255, 8, 8)
+ bounds: 952 152 8 8
+ - image: solid-color(120, 19, 0, 255, 8, 8)
+ bounds: 960 152 8 8
+ - image: solid-color(121, 19, 0, 255, 8, 8)
+ bounds: 968 152 8 8
+ - image: solid-color(122, 19, 0, 255, 8, 8)
+ bounds: 976 152 8 8
+ - image: solid-color(123, 19, 0, 255, 8, 8)
+ bounds: 984 152 8 8
+ - image: solid-color(124, 19, 0, 255, 8, 8)
+ bounds: 992 152 8 8
+ - image: solid-color(125, 19, 0, 255, 8, 8)
+ bounds: 1000 152 8 8
+ - image: solid-color(126, 19, 0, 255, 8, 8)
+ bounds: 1008 152 8 8
+ - image: solid-color(127, 19, 0, 255, 8, 8)
+ bounds: 1016 152 8 8
+ - image: solid-color(0, 20, 0, 255, 8, 8)
+ bounds: 0 160 8 8
+ - image: solid-color(1, 20, 0, 255, 8, 8)
+ bounds: 8 160 8 8
+ - image: solid-color(2, 20, 0, 255, 8, 8)
+ bounds: 16 160 8 8
+ - image: solid-color(3, 20, 0, 255, 8, 8)
+ bounds: 24 160 8 8
+ - image: solid-color(4, 20, 0, 255, 8, 8)
+ bounds: 32 160 8 8
+ - image: solid-color(5, 20, 0, 255, 8, 8)
+ bounds: 40 160 8 8
+ - image: solid-color(6, 20, 0, 255, 8, 8)
+ bounds: 48 160 8 8
+ - image: solid-color(7, 20, 0, 255, 8, 8)
+ bounds: 56 160 8 8
+ - image: solid-color(8, 20, 0, 255, 8, 8)
+ bounds: 64 160 8 8
+ - image: solid-color(9, 20, 0, 255, 8, 8)
+ bounds: 72 160 8 8
+ - image: solid-color(10, 20, 0, 255, 8, 8)
+ bounds: 80 160 8 8
+ - image: solid-color(11, 20, 0, 255, 8, 8)
+ bounds: 88 160 8 8
+ - image: solid-color(12, 20, 0, 255, 8, 8)
+ bounds: 96 160 8 8
+ - image: solid-color(13, 20, 0, 255, 8, 8)
+ bounds: 104 160 8 8
+ - image: solid-color(14, 20, 0, 255, 8, 8)
+ bounds: 112 160 8 8
+ - image: solid-color(15, 20, 0, 255, 8, 8)
+ bounds: 120 160 8 8
+ - image: solid-color(16, 20, 0, 255, 8, 8)
+ bounds: 128 160 8 8
+ - image: solid-color(17, 20, 0, 255, 8, 8)
+ bounds: 136 160 8 8
+ - image: solid-color(18, 20, 0, 255, 8, 8)
+ bounds: 144 160 8 8
+ - image: solid-color(19, 20, 0, 255, 8, 8)
+ bounds: 152 160 8 8
+ - image: solid-color(20, 20, 0, 255, 8, 8)
+ bounds: 160 160 8 8
+ - image: solid-color(21, 20, 0, 255, 8, 8)
+ bounds: 168 160 8 8
+ - image: solid-color(22, 20, 0, 255, 8, 8)
+ bounds: 176 160 8 8
+ - image: solid-color(23, 20, 0, 255, 8, 8)
+ bounds: 184 160 8 8
+ - image: solid-color(24, 20, 0, 255, 8, 8)
+ bounds: 192 160 8 8
+ - image: solid-color(25, 20, 0, 255, 8, 8)
+ bounds: 200 160 8 8
+ - image: solid-color(26, 20, 0, 255, 8, 8)
+ bounds: 208 160 8 8
+ - image: solid-color(27, 20, 0, 255, 8, 8)
+ bounds: 216 160 8 8
+ - image: solid-color(28, 20, 0, 255, 8, 8)
+ bounds: 224 160 8 8
+ - image: solid-color(29, 20, 0, 255, 8, 8)
+ bounds: 232 160 8 8
+ - image: solid-color(30, 20, 0, 255, 8, 8)
+ bounds: 240 160 8 8
+ - image: solid-color(31, 20, 0, 255, 8, 8)
+ bounds: 248 160 8 8
+ - image: solid-color(32, 20, 0, 255, 8, 8)
+ bounds: 256 160 8 8
+ - image: solid-color(33, 20, 0, 255, 8, 8)
+ bounds: 264 160 8 8
+ - image: solid-color(34, 20, 0, 255, 8, 8)
+ bounds: 272 160 8 8
+ - image: solid-color(35, 20, 0, 255, 8, 8)
+ bounds: 280 160 8 8
+ - image: solid-color(36, 20, 0, 255, 8, 8)
+ bounds: 288 160 8 8
+ - image: solid-color(37, 20, 0, 255, 8, 8)
+ bounds: 296 160 8 8
+ - image: solid-color(38, 20, 0, 255, 8, 8)
+ bounds: 304 160 8 8
+ - image: solid-color(39, 20, 0, 255, 8, 8)
+ bounds: 312 160 8 8
+ - image: solid-color(40, 20, 0, 255, 8, 8)
+ bounds: 320 160 8 8
+ - image: solid-color(41, 20, 0, 255, 8, 8)
+ bounds: 328 160 8 8
+ - image: solid-color(42, 20, 0, 255, 8, 8)
+ bounds: 336 160 8 8
+ - image: solid-color(43, 20, 0, 255, 8, 8)
+ bounds: 344 160 8 8
+ - image: solid-color(44, 20, 0, 255, 8, 8)
+ bounds: 352 160 8 8
+ - image: solid-color(45, 20, 0, 255, 8, 8)
+ bounds: 360 160 8 8
+ - image: solid-color(46, 20, 0, 255, 8, 8)
+ bounds: 368 160 8 8
+ - image: solid-color(47, 20, 0, 255, 8, 8)
+ bounds: 376 160 8 8
+ - image: solid-color(48, 20, 0, 255, 8, 8)
+ bounds: 384 160 8 8
+ - image: solid-color(49, 20, 0, 255, 8, 8)
+ bounds: 392 160 8 8
+ - image: solid-color(50, 20, 0, 255, 8, 8)
+ bounds: 400 160 8 8
+ - image: solid-color(51, 20, 0, 255, 8, 8)
+ bounds: 408 160 8 8
+ - image: solid-color(52, 20, 0, 255, 8, 8)
+ bounds: 416 160 8 8
+ - image: solid-color(53, 20, 0, 255, 8, 8)
+ bounds: 424 160 8 8
+ - image: solid-color(54, 20, 0, 255, 8, 8)
+ bounds: 432 160 8 8
+ - image: solid-color(55, 20, 0, 255, 8, 8)
+ bounds: 440 160 8 8
+ - image: solid-color(56, 20, 0, 255, 8, 8)
+ bounds: 448 160 8 8
+ - image: solid-color(57, 20, 0, 255, 8, 8)
+ bounds: 456 160 8 8
+ - image: solid-color(58, 20, 0, 255, 8, 8)
+ bounds: 464 160 8 8
+ - image: solid-color(59, 20, 0, 255, 8, 8)
+ bounds: 472 160 8 8
+ - image: solid-color(60, 20, 0, 255, 8, 8)
+ bounds: 480 160 8 8
+ - image: solid-color(61, 20, 0, 255, 8, 8)
+ bounds: 488 160 8 8
+ - image: solid-color(62, 20, 0, 255, 8, 8)
+ bounds: 496 160 8 8
+ - image: solid-color(63, 20, 0, 255, 8, 8)
+ bounds: 504 160 8 8
+ - image: solid-color(64, 20, 0, 255, 8, 8)
+ bounds: 512 160 8 8
+ - image: solid-color(65, 20, 0, 255, 8, 8)
+ bounds: 520 160 8 8
+ - image: solid-color(66, 20, 0, 255, 8, 8)
+ bounds: 528 160 8 8
+ - image: solid-color(67, 20, 0, 255, 8, 8)
+ bounds: 536 160 8 8
+ - image: solid-color(68, 20, 0, 255, 8, 8)
+ bounds: 544 160 8 8
+ - image: solid-color(69, 20, 0, 255, 8, 8)
+ bounds: 552 160 8 8
+ - image: solid-color(70, 20, 0, 255, 8, 8)
+ bounds: 560 160 8 8
+ - image: solid-color(71, 20, 0, 255, 8, 8)
+ bounds: 568 160 8 8
+ - image: solid-color(72, 20, 0, 255, 8, 8)
+ bounds: 576 160 8 8
+ - image: solid-color(73, 20, 0, 255, 8, 8)
+ bounds: 584 160 8 8
+ - image: solid-color(74, 20, 0, 255, 8, 8)
+ bounds: 592 160 8 8
+ - image: solid-color(75, 20, 0, 255, 8, 8)
+ bounds: 600 160 8 8
+ - image: solid-color(76, 20, 0, 255, 8, 8)
+ bounds: 608 160 8 8
+ - image: solid-color(77, 20, 0, 255, 8, 8)
+ bounds: 616 160 8 8
+ - image: solid-color(78, 20, 0, 255, 8, 8)
+ bounds: 624 160 8 8
+ - image: solid-color(79, 20, 0, 255, 8, 8)
+ bounds: 632 160 8 8
+ - image: solid-color(80, 20, 0, 255, 8, 8)
+ bounds: 640 160 8 8
+ - image: solid-color(81, 20, 0, 255, 8, 8)
+ bounds: 648 160 8 8
+ - image: solid-color(82, 20, 0, 255, 8, 8)
+ bounds: 656 160 8 8
+ - image: solid-color(83, 20, 0, 255, 8, 8)
+ bounds: 664 160 8 8
+ - image: solid-color(84, 20, 0, 255, 8, 8)
+ bounds: 672 160 8 8
+ - image: solid-color(85, 20, 0, 255, 8, 8)
+ bounds: 680 160 8 8
+ - image: solid-color(86, 20, 0, 255, 8, 8)
+ bounds: 688 160 8 8
+ - image: solid-color(87, 20, 0, 255, 8, 8)
+ bounds: 696 160 8 8
+ - image: solid-color(88, 20, 0, 255, 8, 8)
+ bounds: 704 160 8 8
+ - image: solid-color(89, 20, 0, 255, 8, 8)
+ bounds: 712 160 8 8
+ - image: solid-color(90, 20, 0, 255, 8, 8)
+ bounds: 720 160 8 8
+ - image: solid-color(91, 20, 0, 255, 8, 8)
+ bounds: 728 160 8 8
+ - image: solid-color(92, 20, 0, 255, 8, 8)
+ bounds: 736 160 8 8
+ - image: solid-color(93, 20, 0, 255, 8, 8)
+ bounds: 744 160 8 8
+ - image: solid-color(94, 20, 0, 255, 8, 8)
+ bounds: 752 160 8 8
+ - image: solid-color(95, 20, 0, 255, 8, 8)
+ bounds: 760 160 8 8
+ - image: solid-color(96, 20, 0, 255, 8, 8)
+ bounds: 768 160 8 8
+ - image: solid-color(97, 20, 0, 255, 8, 8)
+ bounds: 776 160 8 8
+ - image: solid-color(98, 20, 0, 255, 8, 8)
+ bounds: 784 160 8 8
+ - image: solid-color(99, 20, 0, 255, 8, 8)
+ bounds: 792 160 8 8
+ - image: solid-color(100, 20, 0, 255, 8, 8)
+ bounds: 800 160 8 8
+ - image: solid-color(101, 20, 0, 255, 8, 8)
+ bounds: 808 160 8 8
+ - image: solid-color(102, 20, 0, 255, 8, 8)
+ bounds: 816 160 8 8
+ - image: solid-color(103, 20, 0, 255, 8, 8)
+ bounds: 824 160 8 8
+ - image: solid-color(104, 20, 0, 255, 8, 8)
+ bounds: 832 160 8 8
+ - image: solid-color(105, 20, 0, 255, 8, 8)
+ bounds: 840 160 8 8
+ - image: solid-color(106, 20, 0, 255, 8, 8)
+ bounds: 848 160 8 8
+ - image: solid-color(107, 20, 0, 255, 8, 8)
+ bounds: 856 160 8 8
+ - image: solid-color(108, 20, 0, 255, 8, 8)
+ bounds: 864 160 8 8
+ - image: solid-color(109, 20, 0, 255, 8, 8)
+ bounds: 872 160 8 8
+ - image: solid-color(110, 20, 0, 255, 8, 8)
+ bounds: 880 160 8 8
+ - image: solid-color(111, 20, 0, 255, 8, 8)
+ bounds: 888 160 8 8
+ - image: solid-color(112, 20, 0, 255, 8, 8)
+ bounds: 896 160 8 8
+ - image: solid-color(113, 20, 0, 255, 8, 8)
+ bounds: 904 160 8 8
+ - image: solid-color(114, 20, 0, 255, 8, 8)
+ bounds: 912 160 8 8
+ - image: solid-color(115, 20, 0, 255, 8, 8)
+ bounds: 920 160 8 8
+ - image: solid-color(116, 20, 0, 255, 8, 8)
+ bounds: 928 160 8 8
+ - image: solid-color(117, 20, 0, 255, 8, 8)
+ bounds: 936 160 8 8
+ - image: solid-color(118, 20, 0, 255, 8, 8)
+ bounds: 944 160 8 8
+ - image: solid-color(119, 20, 0, 255, 8, 8)
+ bounds: 952 160 8 8
+ - image: solid-color(120, 20, 0, 255, 8, 8)
+ bounds: 960 160 8 8
+ - image: solid-color(121, 20, 0, 255, 8, 8)
+ bounds: 968 160 8 8
+ - image: solid-color(122, 20, 0, 255, 8, 8)
+ bounds: 976 160 8 8
+ - image: solid-color(123, 20, 0, 255, 8, 8)
+ bounds: 984 160 8 8
+ - image: solid-color(124, 20, 0, 255, 8, 8)
+ bounds: 992 160 8 8
+ - image: solid-color(125, 20, 0, 255, 8, 8)
+ bounds: 1000 160 8 8
+ - image: solid-color(126, 20, 0, 255, 8, 8)
+ bounds: 1008 160 8 8
+ - image: solid-color(127, 20, 0, 255, 8, 8)
+ bounds: 1016 160 8 8
+ - image: solid-color(0, 21, 0, 255, 8, 8)
+ bounds: 0 168 8 8
+ - image: solid-color(1, 21, 0, 255, 8, 8)
+ bounds: 8 168 8 8
+ - image: solid-color(2, 21, 0, 255, 8, 8)
+ bounds: 16 168 8 8
+ - image: solid-color(3, 21, 0, 255, 8, 8)
+ bounds: 24 168 8 8
+ - image: solid-color(4, 21, 0, 255, 8, 8)
+ bounds: 32 168 8 8
+ - image: solid-color(5, 21, 0, 255, 8, 8)
+ bounds: 40 168 8 8
+ - image: solid-color(6, 21, 0, 255, 8, 8)
+ bounds: 48 168 8 8
+ - image: solid-color(7, 21, 0, 255, 8, 8)
+ bounds: 56 168 8 8
+ - image: solid-color(8, 21, 0, 255, 8, 8)
+ bounds: 64 168 8 8
+ - image: solid-color(9, 21, 0, 255, 8, 8)
+ bounds: 72 168 8 8
+ - image: solid-color(10, 21, 0, 255, 8, 8)
+ bounds: 80 168 8 8
+ - image: solid-color(11, 21, 0, 255, 8, 8)
+ bounds: 88 168 8 8
+ - image: solid-color(12, 21, 0, 255, 8, 8)
+ bounds: 96 168 8 8
+ - image: solid-color(13, 21, 0, 255, 8, 8)
+ bounds: 104 168 8 8
+ - image: solid-color(14, 21, 0, 255, 8, 8)
+ bounds: 112 168 8 8
+ - image: solid-color(15, 21, 0, 255, 8, 8)
+ bounds: 120 168 8 8
+ - image: solid-color(16, 21, 0, 255, 8, 8)
+ bounds: 128 168 8 8
+ - image: solid-color(17, 21, 0, 255, 8, 8)
+ bounds: 136 168 8 8
+ - image: solid-color(18, 21, 0, 255, 8, 8)
+ bounds: 144 168 8 8
+ - image: solid-color(19, 21, 0, 255, 8, 8)
+ bounds: 152 168 8 8
+ - image: solid-color(20, 21, 0, 255, 8, 8)
+ bounds: 160 168 8 8
+ - image: solid-color(21, 21, 0, 255, 8, 8)
+ bounds: 168 168 8 8
+ - image: solid-color(22, 21, 0, 255, 8, 8)
+ bounds: 176 168 8 8
+ - image: solid-color(23, 21, 0, 255, 8, 8)
+ bounds: 184 168 8 8
+ - image: solid-color(24, 21, 0, 255, 8, 8)
+ bounds: 192 168 8 8
+ - image: solid-color(25, 21, 0, 255, 8, 8)
+ bounds: 200 168 8 8
+ - image: solid-color(26, 21, 0, 255, 8, 8)
+ bounds: 208 168 8 8
+ - image: solid-color(27, 21, 0, 255, 8, 8)
+ bounds: 216 168 8 8
+ - image: solid-color(28, 21, 0, 255, 8, 8)
+ bounds: 224 168 8 8
+ - image: solid-color(29, 21, 0, 255, 8, 8)
+ bounds: 232 168 8 8
+ - image: solid-color(30, 21, 0, 255, 8, 8)
+ bounds: 240 168 8 8
+ - image: solid-color(31, 21, 0, 255, 8, 8)
+ bounds: 248 168 8 8
+ - image: solid-color(32, 21, 0, 255, 8, 8)
+ bounds: 256 168 8 8
+ - image: solid-color(33, 21, 0, 255, 8, 8)
+ bounds: 264 168 8 8
+ - image: solid-color(34, 21, 0, 255, 8, 8)
+ bounds: 272 168 8 8
+ - image: solid-color(35, 21, 0, 255, 8, 8)
+ bounds: 280 168 8 8
+ - image: solid-color(36, 21, 0, 255, 8, 8)
+ bounds: 288 168 8 8
+ - image: solid-color(37, 21, 0, 255, 8, 8)
+ bounds: 296 168 8 8
+ - image: solid-color(38, 21, 0, 255, 8, 8)
+ bounds: 304 168 8 8
+ - image: solid-color(39, 21, 0, 255, 8, 8)
+ bounds: 312 168 8 8
+ - image: solid-color(40, 21, 0, 255, 8, 8)
+ bounds: 320 168 8 8
+ - image: solid-color(41, 21, 0, 255, 8, 8)
+ bounds: 328 168 8 8
+ - image: solid-color(42, 21, 0, 255, 8, 8)
+ bounds: 336 168 8 8
+ - image: solid-color(43, 21, 0, 255, 8, 8)
+ bounds: 344 168 8 8
+ - image: solid-color(44, 21, 0, 255, 8, 8)
+ bounds: 352 168 8 8
+ - image: solid-color(45, 21, 0, 255, 8, 8)
+ bounds: 360 168 8 8
+ - image: solid-color(46, 21, 0, 255, 8, 8)
+ bounds: 368 168 8 8
+ - image: solid-color(47, 21, 0, 255, 8, 8)
+ bounds: 376 168 8 8
+ - image: solid-color(48, 21, 0, 255, 8, 8)
+ bounds: 384 168 8 8
+ - image: solid-color(49, 21, 0, 255, 8, 8)
+ bounds: 392 168 8 8
+ - image: solid-color(50, 21, 0, 255, 8, 8)
+ bounds: 400 168 8 8
+ - image: solid-color(51, 21, 0, 255, 8, 8)
+ bounds: 408 168 8 8
+ - image: solid-color(52, 21, 0, 255, 8, 8)
+ bounds: 416 168 8 8
+ - image: solid-color(53, 21, 0, 255, 8, 8)
+ bounds: 424 168 8 8
+ - image: solid-color(54, 21, 0, 255, 8, 8)
+ bounds: 432 168 8 8
+ - image: solid-color(55, 21, 0, 255, 8, 8)
+ bounds: 440 168 8 8
+ - image: solid-color(56, 21, 0, 255, 8, 8)
+ bounds: 448 168 8 8
+ - image: solid-color(57, 21, 0, 255, 8, 8)
+ bounds: 456 168 8 8
+ - image: solid-color(58, 21, 0, 255, 8, 8)
+ bounds: 464 168 8 8
+ - image: solid-color(59, 21, 0, 255, 8, 8)
+ bounds: 472 168 8 8
+ - image: solid-color(60, 21, 0, 255, 8, 8)
+ bounds: 480 168 8 8
+ - image: solid-color(61, 21, 0, 255, 8, 8)
+ bounds: 488 168 8 8
+ - image: solid-color(62, 21, 0, 255, 8, 8)
+ bounds: 496 168 8 8
+ - image: solid-color(63, 21, 0, 255, 8, 8)
+ bounds: 504 168 8 8
+ - image: solid-color(64, 21, 0, 255, 8, 8)
+ bounds: 512 168 8 8
+ - image: solid-color(65, 21, 0, 255, 8, 8)
+ bounds: 520 168 8 8
+ - image: solid-color(66, 21, 0, 255, 8, 8)
+ bounds: 528 168 8 8
+ - image: solid-color(67, 21, 0, 255, 8, 8)
+ bounds: 536 168 8 8
+ - image: solid-color(68, 21, 0, 255, 8, 8)
+ bounds: 544 168 8 8
+ - image: solid-color(69, 21, 0, 255, 8, 8)
+ bounds: 552 168 8 8
+ - image: solid-color(70, 21, 0, 255, 8, 8)
+ bounds: 560 168 8 8
+ - image: solid-color(71, 21, 0, 255, 8, 8)
+ bounds: 568 168 8 8
+ - image: solid-color(72, 21, 0, 255, 8, 8)
+ bounds: 576 168 8 8
+ - image: solid-color(73, 21, 0, 255, 8, 8)
+ bounds: 584 168 8 8
+ - image: solid-color(74, 21, 0, 255, 8, 8)
+ bounds: 592 168 8 8
+ - image: solid-color(75, 21, 0, 255, 8, 8)
+ bounds: 600 168 8 8
+ - image: solid-color(76, 21, 0, 255, 8, 8)
+ bounds: 608 168 8 8
+ - image: solid-color(77, 21, 0, 255, 8, 8)
+ bounds: 616 168 8 8
+ - image: solid-color(78, 21, 0, 255, 8, 8)
+ bounds: 624 168 8 8
+ - image: solid-color(79, 21, 0, 255, 8, 8)
+ bounds: 632 168 8 8
+ - image: solid-color(80, 21, 0, 255, 8, 8)
+ bounds: 640 168 8 8
+ - image: solid-color(81, 21, 0, 255, 8, 8)
+ bounds: 648 168 8 8
+ - image: solid-color(82, 21, 0, 255, 8, 8)
+ bounds: 656 168 8 8
+ - image: solid-color(83, 21, 0, 255, 8, 8)
+ bounds: 664 168 8 8
+ - image: solid-color(84, 21, 0, 255, 8, 8)
+ bounds: 672 168 8 8
+ - image: solid-color(85, 21, 0, 255, 8, 8)
+ bounds: 680 168 8 8
+ - image: solid-color(86, 21, 0, 255, 8, 8)
+ bounds: 688 168 8 8
+ - image: solid-color(87, 21, 0, 255, 8, 8)
+ bounds: 696 168 8 8
+ - image: solid-color(88, 21, 0, 255, 8, 8)
+ bounds: 704 168 8 8
+ - image: solid-color(89, 21, 0, 255, 8, 8)
+ bounds: 712 168 8 8
+ - image: solid-color(90, 21, 0, 255, 8, 8)
+ bounds: 720 168 8 8
+ - image: solid-color(91, 21, 0, 255, 8, 8)
+ bounds: 728 168 8 8
+ - image: solid-color(92, 21, 0, 255, 8, 8)
+ bounds: 736 168 8 8
+ - image: solid-color(93, 21, 0, 255, 8, 8)
+ bounds: 744 168 8 8
+ - image: solid-color(94, 21, 0, 255, 8, 8)
+ bounds: 752 168 8 8
+ - image: solid-color(95, 21, 0, 255, 8, 8)
+ bounds: 760 168 8 8
+ - image: solid-color(96, 21, 0, 255, 8, 8)
+ bounds: 768 168 8 8
+ - image: solid-color(97, 21, 0, 255, 8, 8)
+ bounds: 776 168 8 8
+ - image: solid-color(98, 21, 0, 255, 8, 8)
+ bounds: 784 168 8 8
+ - image: solid-color(99, 21, 0, 255, 8, 8)
+ bounds: 792 168 8 8
+ - image: solid-color(100, 21, 0, 255, 8, 8)
+ bounds: 800 168 8 8
+ - image: solid-color(101, 21, 0, 255, 8, 8)
+ bounds: 808 168 8 8
+ - image: solid-color(102, 21, 0, 255, 8, 8)
+ bounds: 816 168 8 8
+ - image: solid-color(103, 21, 0, 255, 8, 8)
+ bounds: 824 168 8 8
+ - image: solid-color(104, 21, 0, 255, 8, 8)
+ bounds: 832 168 8 8
+ - image: solid-color(105, 21, 0, 255, 8, 8)
+ bounds: 840 168 8 8
+ - image: solid-color(106, 21, 0, 255, 8, 8)
+ bounds: 848 168 8 8
+ - image: solid-color(107, 21, 0, 255, 8, 8)
+ bounds: 856 168 8 8
+ - image: solid-color(108, 21, 0, 255, 8, 8)
+ bounds: 864 168 8 8
+ - image: solid-color(109, 21, 0, 255, 8, 8)
+ bounds: 872 168 8 8
+ - image: solid-color(110, 21, 0, 255, 8, 8)
+ bounds: 880 168 8 8
+ - image: solid-color(111, 21, 0, 255, 8, 8)
+ bounds: 888 168 8 8
+ - image: solid-color(112, 21, 0, 255, 8, 8)
+ bounds: 896 168 8 8
+ - image: solid-color(113, 21, 0, 255, 8, 8)
+ bounds: 904 168 8 8
+ - image: solid-color(114, 21, 0, 255, 8, 8)
+ bounds: 912 168 8 8
+ - image: solid-color(115, 21, 0, 255, 8, 8)
+ bounds: 920 168 8 8
+ - image: solid-color(116, 21, 0, 255, 8, 8)
+ bounds: 928 168 8 8
+ - image: solid-color(117, 21, 0, 255, 8, 8)
+ bounds: 936 168 8 8
+ - image: solid-color(118, 21, 0, 255, 8, 8)
+ bounds: 944 168 8 8
+ - image: solid-color(119, 21, 0, 255, 8, 8)
+ bounds: 952 168 8 8
+ - image: solid-color(120, 21, 0, 255, 8, 8)
+ bounds: 960 168 8 8
+ - image: solid-color(121, 21, 0, 255, 8, 8)
+ bounds: 968 168 8 8
+ - image: solid-color(122, 21, 0, 255, 8, 8)
+ bounds: 976 168 8 8
+ - image: solid-color(123, 21, 0, 255, 8, 8)
+ bounds: 984 168 8 8
+ - image: solid-color(124, 21, 0, 255, 8, 8)
+ bounds: 992 168 8 8
+ - image: solid-color(125, 21, 0, 255, 8, 8)
+ bounds: 1000 168 8 8
+ - image: solid-color(126, 21, 0, 255, 8, 8)
+ bounds: 1008 168 8 8
+ - image: solid-color(127, 21, 0, 255, 8, 8)
+ bounds: 1016 168 8 8
+ - image: solid-color(0, 22, 0, 255, 8, 8)
+ bounds: 0 176 8 8
+ - image: solid-color(1, 22, 0, 255, 8, 8)
+ bounds: 8 176 8 8
+ - image: solid-color(2, 22, 0, 255, 8, 8)
+ bounds: 16 176 8 8
+ - image: solid-color(3, 22, 0, 255, 8, 8)
+ bounds: 24 176 8 8
+ - image: solid-color(4, 22, 0, 255, 8, 8)
+ bounds: 32 176 8 8
+ - image: solid-color(5, 22, 0, 255, 8, 8)
+ bounds: 40 176 8 8
+ - image: solid-color(6, 22, 0, 255, 8, 8)
+ bounds: 48 176 8 8
+ - image: solid-color(7, 22, 0, 255, 8, 8)
+ bounds: 56 176 8 8
+ - image: solid-color(8, 22, 0, 255, 8, 8)
+ bounds: 64 176 8 8
+ - image: solid-color(9, 22, 0, 255, 8, 8)
+ bounds: 72 176 8 8
+ - image: solid-color(10, 22, 0, 255, 8, 8)
+ bounds: 80 176 8 8
+ - image: solid-color(11, 22, 0, 255, 8, 8)
+ bounds: 88 176 8 8
+ - image: solid-color(12, 22, 0, 255, 8, 8)
+ bounds: 96 176 8 8
+ - image: solid-color(13, 22, 0, 255, 8, 8)
+ bounds: 104 176 8 8
+ - image: solid-color(14, 22, 0, 255, 8, 8)
+ bounds: 112 176 8 8
+ - image: solid-color(15, 22, 0, 255, 8, 8)
+ bounds: 120 176 8 8
+ - image: solid-color(16, 22, 0, 255, 8, 8)
+ bounds: 128 176 8 8
+ - image: solid-color(17, 22, 0, 255, 8, 8)
+ bounds: 136 176 8 8
+ - image: solid-color(18, 22, 0, 255, 8, 8)
+ bounds: 144 176 8 8
+ - image: solid-color(19, 22, 0, 255, 8, 8)
+ bounds: 152 176 8 8
+ - image: solid-color(20, 22, 0, 255, 8, 8)
+ bounds: 160 176 8 8
+ - image: solid-color(21, 22, 0, 255, 8, 8)
+ bounds: 168 176 8 8
+ - image: solid-color(22, 22, 0, 255, 8, 8)
+ bounds: 176 176 8 8
+ - image: solid-color(23, 22, 0, 255, 8, 8)
+ bounds: 184 176 8 8
+ - image: solid-color(24, 22, 0, 255, 8, 8)
+ bounds: 192 176 8 8
+ - image: solid-color(25, 22, 0, 255, 8, 8)
+ bounds: 200 176 8 8
+ - image: solid-color(26, 22, 0, 255, 8, 8)
+ bounds: 208 176 8 8
+ - image: solid-color(27, 22, 0, 255, 8, 8)
+ bounds: 216 176 8 8
+ - image: solid-color(28, 22, 0, 255, 8, 8)
+ bounds: 224 176 8 8
+ - image: solid-color(29, 22, 0, 255, 8, 8)
+ bounds: 232 176 8 8
+ - image: solid-color(30, 22, 0, 255, 8, 8)
+ bounds: 240 176 8 8
+ - image: solid-color(31, 22, 0, 255, 8, 8)
+ bounds: 248 176 8 8
+ - image: solid-color(32, 22, 0, 255, 8, 8)
+ bounds: 256 176 8 8
+ - image: solid-color(33, 22, 0, 255, 8, 8)
+ bounds: 264 176 8 8
+ - image: solid-color(34, 22, 0, 255, 8, 8)
+ bounds: 272 176 8 8
+ - image: solid-color(35, 22, 0, 255, 8, 8)
+ bounds: 280 176 8 8
+ - image: solid-color(36, 22, 0, 255, 8, 8)
+ bounds: 288 176 8 8
+ - image: solid-color(37, 22, 0, 255, 8, 8)
+ bounds: 296 176 8 8
+ - image: solid-color(38, 22, 0, 255, 8, 8)
+ bounds: 304 176 8 8
+ - image: solid-color(39, 22, 0, 255, 8, 8)
+ bounds: 312 176 8 8
+ - image: solid-color(40, 22, 0, 255, 8, 8)
+ bounds: 320 176 8 8
+ - image: solid-color(41, 22, 0, 255, 8, 8)
+ bounds: 328 176 8 8
+ - image: solid-color(42, 22, 0, 255, 8, 8)
+ bounds: 336 176 8 8
+ - image: solid-color(43, 22, 0, 255, 8, 8)
+ bounds: 344 176 8 8
+ - image: solid-color(44, 22, 0, 255, 8, 8)
+ bounds: 352 176 8 8
+ - image: solid-color(45, 22, 0, 255, 8, 8)
+ bounds: 360 176 8 8
+ - image: solid-color(46, 22, 0, 255, 8, 8)
+ bounds: 368 176 8 8
+ - image: solid-color(47, 22, 0, 255, 8, 8)
+ bounds: 376 176 8 8
+ - image: solid-color(48, 22, 0, 255, 8, 8)
+ bounds: 384 176 8 8
+ - image: solid-color(49, 22, 0, 255, 8, 8)
+ bounds: 392 176 8 8
+ - image: solid-color(50, 22, 0, 255, 8, 8)
+ bounds: 400 176 8 8
+ - image: solid-color(51, 22, 0, 255, 8, 8)
+ bounds: 408 176 8 8
+ - image: solid-color(52, 22, 0, 255, 8, 8)
+ bounds: 416 176 8 8
+ - image: solid-color(53, 22, 0, 255, 8, 8)
+ bounds: 424 176 8 8
+ - image: solid-color(54, 22, 0, 255, 8, 8)
+ bounds: 432 176 8 8
+ - image: solid-color(55, 22, 0, 255, 8, 8)
+ bounds: 440 176 8 8
+ - image: solid-color(56, 22, 0, 255, 8, 8)
+ bounds: 448 176 8 8
+ - image: solid-color(57, 22, 0, 255, 8, 8)
+ bounds: 456 176 8 8
+ - image: solid-color(58, 22, 0, 255, 8, 8)
+ bounds: 464 176 8 8
+ - image: solid-color(59, 22, 0, 255, 8, 8)
+ bounds: 472 176 8 8
+ - image: solid-color(60, 22, 0, 255, 8, 8)
+ bounds: 480 176 8 8
+ - image: solid-color(61, 22, 0, 255, 8, 8)
+ bounds: 488 176 8 8
+ - image: solid-color(62, 22, 0, 255, 8, 8)
+ bounds: 496 176 8 8
+ - image: solid-color(63, 22, 0, 255, 8, 8)
+ bounds: 504 176 8 8
+ - image: solid-color(64, 22, 0, 255, 8, 8)
+ bounds: 512 176 8 8
+ - image: solid-color(65, 22, 0, 255, 8, 8)
+ bounds: 520 176 8 8
+ - image: solid-color(66, 22, 0, 255, 8, 8)
+ bounds: 528 176 8 8
+ - image: solid-color(67, 22, 0, 255, 8, 8)
+ bounds: 536 176 8 8
+ - image: solid-color(68, 22, 0, 255, 8, 8)
+ bounds: 544 176 8 8
+ - image: solid-color(69, 22, 0, 255, 8, 8)
+ bounds: 552 176 8 8
+ - image: solid-color(70, 22, 0, 255, 8, 8)
+ bounds: 560 176 8 8
+ - image: solid-color(71, 22, 0, 255, 8, 8)
+ bounds: 568 176 8 8
+ - image: solid-color(72, 22, 0, 255, 8, 8)
+ bounds: 576 176 8 8
+ - image: solid-color(73, 22, 0, 255, 8, 8)
+ bounds: 584 176 8 8
+ - image: solid-color(74, 22, 0, 255, 8, 8)
+ bounds: 592 176 8 8
+ - image: solid-color(75, 22, 0, 255, 8, 8)
+ bounds: 600 176 8 8
+ - image: solid-color(76, 22, 0, 255, 8, 8)
+ bounds: 608 176 8 8
+ - image: solid-color(77, 22, 0, 255, 8, 8)
+ bounds: 616 176 8 8
+ - image: solid-color(78, 22, 0, 255, 8, 8)
+ bounds: 624 176 8 8
+ - image: solid-color(79, 22, 0, 255, 8, 8)
+ bounds: 632 176 8 8
+ - image: solid-color(80, 22, 0, 255, 8, 8)
+ bounds: 640 176 8 8
+ - image: solid-color(81, 22, 0, 255, 8, 8)
+ bounds: 648 176 8 8
+ - image: solid-color(82, 22, 0, 255, 8, 8)
+ bounds: 656 176 8 8
+ - image: solid-color(83, 22, 0, 255, 8, 8)
+ bounds: 664 176 8 8
+ - image: solid-color(84, 22, 0, 255, 8, 8)
+ bounds: 672 176 8 8
+ - image: solid-color(85, 22, 0, 255, 8, 8)
+ bounds: 680 176 8 8
+ - image: solid-color(86, 22, 0, 255, 8, 8)
+ bounds: 688 176 8 8
+ - image: solid-color(87, 22, 0, 255, 8, 8)
+ bounds: 696 176 8 8
+ - image: solid-color(88, 22, 0, 255, 8, 8)
+ bounds: 704 176 8 8
+ - image: solid-color(89, 22, 0, 255, 8, 8)
+ bounds: 712 176 8 8
+ - image: solid-color(90, 22, 0, 255, 8, 8)
+ bounds: 720 176 8 8
+ - image: solid-color(91, 22, 0, 255, 8, 8)
+ bounds: 728 176 8 8
+ - image: solid-color(92, 22, 0, 255, 8, 8)
+ bounds: 736 176 8 8
+ - image: solid-color(93, 22, 0, 255, 8, 8)
+ bounds: 744 176 8 8
+ - image: solid-color(94, 22, 0, 255, 8, 8)
+ bounds: 752 176 8 8
+ - image: solid-color(95, 22, 0, 255, 8, 8)
+ bounds: 760 176 8 8
+ - image: solid-color(96, 22, 0, 255, 8, 8)
+ bounds: 768 176 8 8
+ - image: solid-color(97, 22, 0, 255, 8, 8)
+ bounds: 776 176 8 8
+ - image: solid-color(98, 22, 0, 255, 8, 8)
+ bounds: 784 176 8 8
+ - image: solid-color(99, 22, 0, 255, 8, 8)
+ bounds: 792 176 8 8
+ - image: solid-color(100, 22, 0, 255, 8, 8)
+ bounds: 800 176 8 8
+ - image: solid-color(101, 22, 0, 255, 8, 8)
+ bounds: 808 176 8 8
+ - image: solid-color(102, 22, 0, 255, 8, 8)
+ bounds: 816 176 8 8
+ - image: solid-color(103, 22, 0, 255, 8, 8)
+ bounds: 824 176 8 8
+ - image: solid-color(104, 22, 0, 255, 8, 8)
+ bounds: 832 176 8 8
+ - image: solid-color(105, 22, 0, 255, 8, 8)
+ bounds: 840 176 8 8
+ - image: solid-color(106, 22, 0, 255, 8, 8)
+ bounds: 848 176 8 8
+ - image: solid-color(107, 22, 0, 255, 8, 8)
+ bounds: 856 176 8 8
+ - image: solid-color(108, 22, 0, 255, 8, 8)
+ bounds: 864 176 8 8
+ - image: solid-color(109, 22, 0, 255, 8, 8)
+ bounds: 872 176 8 8
+ - image: solid-color(110, 22, 0, 255, 8, 8)
+ bounds: 880 176 8 8
+ - image: solid-color(111, 22, 0, 255, 8, 8)
+ bounds: 888 176 8 8
+ - image: solid-color(112, 22, 0, 255, 8, 8)
+ bounds: 896 176 8 8
+ - image: solid-color(113, 22, 0, 255, 8, 8)
+ bounds: 904 176 8 8
+ - image: solid-color(114, 22, 0, 255, 8, 8)
+ bounds: 912 176 8 8
+ - image: solid-color(115, 22, 0, 255, 8, 8)
+ bounds: 920 176 8 8
+ - image: solid-color(116, 22, 0, 255, 8, 8)
+ bounds: 928 176 8 8
+ - image: solid-color(117, 22, 0, 255, 8, 8)
+ bounds: 936 176 8 8
+ - image: solid-color(118, 22, 0, 255, 8, 8)
+ bounds: 944 176 8 8
+ - image: solid-color(119, 22, 0, 255, 8, 8)
+ bounds: 952 176 8 8
+ - image: solid-color(120, 22, 0, 255, 8, 8)
+ bounds: 960 176 8 8
+ - image: solid-color(121, 22, 0, 255, 8, 8)
+ bounds: 968 176 8 8
+ - image: solid-color(122, 22, 0, 255, 8, 8)
+ bounds: 976 176 8 8
+ - image: solid-color(123, 22, 0, 255, 8, 8)
+ bounds: 984 176 8 8
+ - image: solid-color(124, 22, 0, 255, 8, 8)
+ bounds: 992 176 8 8
+ - image: solid-color(125, 22, 0, 255, 8, 8)
+ bounds: 1000 176 8 8
+ - image: solid-color(126, 22, 0, 255, 8, 8)
+ bounds: 1008 176 8 8
+ - image: solid-color(127, 22, 0, 255, 8, 8)
+ bounds: 1016 176 8 8
+ - image: solid-color(0, 23, 0, 255, 8, 8)
+ bounds: 0 184 8 8
+ - image: solid-color(1, 23, 0, 255, 8, 8)
+ bounds: 8 184 8 8
+ - image: solid-color(2, 23, 0, 255, 8, 8)
+ bounds: 16 184 8 8
+ - image: solid-color(3, 23, 0, 255, 8, 8)
+ bounds: 24 184 8 8
+ - image: solid-color(4, 23, 0, 255, 8, 8)
+ bounds: 32 184 8 8
+ - image: solid-color(5, 23, 0, 255, 8, 8)
+ bounds: 40 184 8 8
+ - image: solid-color(6, 23, 0, 255, 8, 8)
+ bounds: 48 184 8 8
+ - image: solid-color(7, 23, 0, 255, 8, 8)
+ bounds: 56 184 8 8
+ - image: solid-color(8, 23, 0, 255, 8, 8)
+ bounds: 64 184 8 8
+ - image: solid-color(9, 23, 0, 255, 8, 8)
+ bounds: 72 184 8 8
+ - image: solid-color(10, 23, 0, 255, 8, 8)
+ bounds: 80 184 8 8
+ - image: solid-color(11, 23, 0, 255, 8, 8)
+ bounds: 88 184 8 8
+ - image: solid-color(12, 23, 0, 255, 8, 8)
+ bounds: 96 184 8 8
+ - image: solid-color(13, 23, 0, 255, 8, 8)
+ bounds: 104 184 8 8
+ - image: solid-color(14, 23, 0, 255, 8, 8)
+ bounds: 112 184 8 8
+ - image: solid-color(15, 23, 0, 255, 8, 8)
+ bounds: 120 184 8 8
+ - image: solid-color(16, 23, 0, 255, 8, 8)
+ bounds: 128 184 8 8
+ - image: solid-color(17, 23, 0, 255, 8, 8)
+ bounds: 136 184 8 8
+ - image: solid-color(18, 23, 0, 255, 8, 8)
+ bounds: 144 184 8 8
+ - image: solid-color(19, 23, 0, 255, 8, 8)
+ bounds: 152 184 8 8
+ - image: solid-color(20, 23, 0, 255, 8, 8)
+ bounds: 160 184 8 8
+ - image: solid-color(21, 23, 0, 255, 8, 8)
+ bounds: 168 184 8 8
+ - image: solid-color(22, 23, 0, 255, 8, 8)
+ bounds: 176 184 8 8
+ - image: solid-color(23, 23, 0, 255, 8, 8)
+ bounds: 184 184 8 8
+ - image: solid-color(24, 23, 0, 255, 8, 8)
+ bounds: 192 184 8 8
+ - image: solid-color(25, 23, 0, 255, 8, 8)
+ bounds: 200 184 8 8
+ - image: solid-color(26, 23, 0, 255, 8, 8)
+ bounds: 208 184 8 8
+ - image: solid-color(27, 23, 0, 255, 8, 8)
+ bounds: 216 184 8 8
+ - image: solid-color(28, 23, 0, 255, 8, 8)
+ bounds: 224 184 8 8
+ - image: solid-color(29, 23, 0, 255, 8, 8)
+ bounds: 232 184 8 8
+ - image: solid-color(30, 23, 0, 255, 8, 8)
+ bounds: 240 184 8 8
+ - image: solid-color(31, 23, 0, 255, 8, 8)
+ bounds: 248 184 8 8
+ - image: solid-color(32, 23, 0, 255, 8, 8)
+ bounds: 256 184 8 8
+ - image: solid-color(33, 23, 0, 255, 8, 8)
+ bounds: 264 184 8 8
+ - image: solid-color(34, 23, 0, 255, 8, 8)
+ bounds: 272 184 8 8
+ - image: solid-color(35, 23, 0, 255, 8, 8)
+ bounds: 280 184 8 8
+ - image: solid-color(36, 23, 0, 255, 8, 8)
+ bounds: 288 184 8 8
+ - image: solid-color(37, 23, 0, 255, 8, 8)
+ bounds: 296 184 8 8
+ - image: solid-color(38, 23, 0, 255, 8, 8)
+ bounds: 304 184 8 8
+ - image: solid-color(39, 23, 0, 255, 8, 8)
+ bounds: 312 184 8 8
+ - image: solid-color(40, 23, 0, 255, 8, 8)
+ bounds: 320 184 8 8
+ - image: solid-color(41, 23, 0, 255, 8, 8)
+ bounds: 328 184 8 8
+ - image: solid-color(42, 23, 0, 255, 8, 8)
+ bounds: 336 184 8 8
+ - image: solid-color(43, 23, 0, 255, 8, 8)
+ bounds: 344 184 8 8
+ - image: solid-color(44, 23, 0, 255, 8, 8)
+ bounds: 352 184 8 8
+ - image: solid-color(45, 23, 0, 255, 8, 8)
+ bounds: 360 184 8 8
+ - image: solid-color(46, 23, 0, 255, 8, 8)
+ bounds: 368 184 8 8
+ - image: solid-color(47, 23, 0, 255, 8, 8)
+ bounds: 376 184 8 8
+ - image: solid-color(48, 23, 0, 255, 8, 8)
+ bounds: 384 184 8 8
+ - image: solid-color(49, 23, 0, 255, 8, 8)
+ bounds: 392 184 8 8
+ - image: solid-color(50, 23, 0, 255, 8, 8)
+ bounds: 400 184 8 8
+ - image: solid-color(51, 23, 0, 255, 8, 8)
+ bounds: 408 184 8 8
+ - image: solid-color(52, 23, 0, 255, 8, 8)
+ bounds: 416 184 8 8
+ - image: solid-color(53, 23, 0, 255, 8, 8)
+ bounds: 424 184 8 8
+ - image: solid-color(54, 23, 0, 255, 8, 8)
+ bounds: 432 184 8 8
+ - image: solid-color(55, 23, 0, 255, 8, 8)
+ bounds: 440 184 8 8
+ - image: solid-color(56, 23, 0, 255, 8, 8)
+ bounds: 448 184 8 8
+ - image: solid-color(57, 23, 0, 255, 8, 8)
+ bounds: 456 184 8 8
+ - image: solid-color(58, 23, 0, 255, 8, 8)
+ bounds: 464 184 8 8
+ - image: solid-color(59, 23, 0, 255, 8, 8)
+ bounds: 472 184 8 8
+ - image: solid-color(60, 23, 0, 255, 8, 8)
+ bounds: 480 184 8 8
+ - image: solid-color(61, 23, 0, 255, 8, 8)
+ bounds: 488 184 8 8
+ - image: solid-color(62, 23, 0, 255, 8, 8)
+ bounds: 496 184 8 8
+ - image: solid-color(63, 23, 0, 255, 8, 8)
+ bounds: 504 184 8 8
+ - image: solid-color(64, 23, 0, 255, 8, 8)
+ bounds: 512 184 8 8
+ - image: solid-color(65, 23, 0, 255, 8, 8)
+ bounds: 520 184 8 8
+ - image: solid-color(66, 23, 0, 255, 8, 8)
+ bounds: 528 184 8 8
+ - image: solid-color(67, 23, 0, 255, 8, 8)
+ bounds: 536 184 8 8
+ - image: solid-color(68, 23, 0, 255, 8, 8)
+ bounds: 544 184 8 8
+ - image: solid-color(69, 23, 0, 255, 8, 8)
+ bounds: 552 184 8 8
+ - image: solid-color(70, 23, 0, 255, 8, 8)
+ bounds: 560 184 8 8
+ - image: solid-color(71, 23, 0, 255, 8, 8)
+ bounds: 568 184 8 8
+ - image: solid-color(72, 23, 0, 255, 8, 8)
+ bounds: 576 184 8 8
+ - image: solid-color(73, 23, 0, 255, 8, 8)
+ bounds: 584 184 8 8
+ - image: solid-color(74, 23, 0, 255, 8, 8)
+ bounds: 592 184 8 8
+ - image: solid-color(75, 23, 0, 255, 8, 8)
+ bounds: 600 184 8 8
+ - image: solid-color(76, 23, 0, 255, 8, 8)
+ bounds: 608 184 8 8
+ - image: solid-color(77, 23, 0, 255, 8, 8)
+ bounds: 616 184 8 8
+ - image: solid-color(78, 23, 0, 255, 8, 8)
+ bounds: 624 184 8 8
+ - image: solid-color(79, 23, 0, 255, 8, 8)
+ bounds: 632 184 8 8
+ - image: solid-color(80, 23, 0, 255, 8, 8)
+ bounds: 640 184 8 8
+ - image: solid-color(81, 23, 0, 255, 8, 8)
+ bounds: 648 184 8 8
+ - image: solid-color(82, 23, 0, 255, 8, 8)
+ bounds: 656 184 8 8
+ - image: solid-color(83, 23, 0, 255, 8, 8)
+ bounds: 664 184 8 8
+ - image: solid-color(84, 23, 0, 255, 8, 8)
+ bounds: 672 184 8 8
+ - image: solid-color(85, 23, 0, 255, 8, 8)
+ bounds: 680 184 8 8
+ - image: solid-color(86, 23, 0, 255, 8, 8)
+ bounds: 688 184 8 8
+ - image: solid-color(87, 23, 0, 255, 8, 8)
+ bounds: 696 184 8 8
+ - image: solid-color(88, 23, 0, 255, 8, 8)
+ bounds: 704 184 8 8
+ - image: solid-color(89, 23, 0, 255, 8, 8)
+ bounds: 712 184 8 8
+ - image: solid-color(90, 23, 0, 255, 8, 8)
+ bounds: 720 184 8 8
+ - image: solid-color(91, 23, 0, 255, 8, 8)
+ bounds: 728 184 8 8
+ - image: solid-color(92, 23, 0, 255, 8, 8)
+ bounds: 736 184 8 8
+ - image: solid-color(93, 23, 0, 255, 8, 8)
+ bounds: 744 184 8 8
+ - image: solid-color(94, 23, 0, 255, 8, 8)
+ bounds: 752 184 8 8
+ - image: solid-color(95, 23, 0, 255, 8, 8)
+ bounds: 760 184 8 8
+ - image: solid-color(96, 23, 0, 255, 8, 8)
+ bounds: 768 184 8 8
+ - image: solid-color(97, 23, 0, 255, 8, 8)
+ bounds: 776 184 8 8
+ - image: solid-color(98, 23, 0, 255, 8, 8)
+ bounds: 784 184 8 8
+ - image: solid-color(99, 23, 0, 255, 8, 8)
+ bounds: 792 184 8 8
+ - image: solid-color(100, 23, 0, 255, 8, 8)
+ bounds: 800 184 8 8
+ - image: solid-color(101, 23, 0, 255, 8, 8)
+ bounds: 808 184 8 8
+ - image: solid-color(102, 23, 0, 255, 8, 8)
+ bounds: 816 184 8 8
+ - image: solid-color(103, 23, 0, 255, 8, 8)
+ bounds: 824 184 8 8
+ - image: solid-color(104, 23, 0, 255, 8, 8)
+ bounds: 832 184 8 8
+ - image: solid-color(105, 23, 0, 255, 8, 8)
+ bounds: 840 184 8 8
+ - image: solid-color(106, 23, 0, 255, 8, 8)
+ bounds: 848 184 8 8
+ - image: solid-color(107, 23, 0, 255, 8, 8)
+ bounds: 856 184 8 8
+ - image: solid-color(108, 23, 0, 255, 8, 8)
+ bounds: 864 184 8 8
+ - image: solid-color(109, 23, 0, 255, 8, 8)
+ bounds: 872 184 8 8
+ - image: solid-color(110, 23, 0, 255, 8, 8)
+ bounds: 880 184 8 8
+ - image: solid-color(111, 23, 0, 255, 8, 8)
+ bounds: 888 184 8 8
+ - image: solid-color(112, 23, 0, 255, 8, 8)
+ bounds: 896 184 8 8
+ - image: solid-color(113, 23, 0, 255, 8, 8)
+ bounds: 904 184 8 8
+ - image: solid-color(114, 23, 0, 255, 8, 8)
+ bounds: 912 184 8 8
+ - image: solid-color(115, 23, 0, 255, 8, 8)
+ bounds: 920 184 8 8
+ - image: solid-color(116, 23, 0, 255, 8, 8)
+ bounds: 928 184 8 8
+ - image: solid-color(117, 23, 0, 255, 8, 8)
+ bounds: 936 184 8 8
+ - image: solid-color(118, 23, 0, 255, 8, 8)
+ bounds: 944 184 8 8
+ - image: solid-color(119, 23, 0, 255, 8, 8)
+ bounds: 952 184 8 8
+ - image: solid-color(120, 23, 0, 255, 8, 8)
+ bounds: 960 184 8 8
+ - image: solid-color(121, 23, 0, 255, 8, 8)
+ bounds: 968 184 8 8
+ - image: solid-color(122, 23, 0, 255, 8, 8)
+ bounds: 976 184 8 8
+ - image: solid-color(123, 23, 0, 255, 8, 8)
+ bounds: 984 184 8 8
+ - image: solid-color(124, 23, 0, 255, 8, 8)
+ bounds: 992 184 8 8
+ - image: solid-color(125, 23, 0, 255, 8, 8)
+ bounds: 1000 184 8 8
+ - image: solid-color(126, 23, 0, 255, 8, 8)
+ bounds: 1008 184 8 8
+ - image: solid-color(127, 23, 0, 255, 8, 8)
+ bounds: 1016 184 8 8
+ - image: solid-color(0, 24, 0, 255, 8, 8)
+ bounds: 0 192 8 8
+ - image: solid-color(1, 24, 0, 255, 8, 8)
+ bounds: 8 192 8 8
+ - image: solid-color(2, 24, 0, 255, 8, 8)
+ bounds: 16 192 8 8
+ - image: solid-color(3, 24, 0, 255, 8, 8)
+ bounds: 24 192 8 8
+ - image: solid-color(4, 24, 0, 255, 8, 8)
+ bounds: 32 192 8 8
+ - image: solid-color(5, 24, 0, 255, 8, 8)
+ bounds: 40 192 8 8
+ - image: solid-color(6, 24, 0, 255, 8, 8)
+ bounds: 48 192 8 8
+ - image: solid-color(7, 24, 0, 255, 8, 8)
+ bounds: 56 192 8 8
+ - image: solid-color(8, 24, 0, 255, 8, 8)
+ bounds: 64 192 8 8
+ - image: solid-color(9, 24, 0, 255, 8, 8)
+ bounds: 72 192 8 8
+ - image: solid-color(10, 24, 0, 255, 8, 8)
+ bounds: 80 192 8 8
+ - image: solid-color(11, 24, 0, 255, 8, 8)
+ bounds: 88 192 8 8
+ - image: solid-color(12, 24, 0, 255, 8, 8)
+ bounds: 96 192 8 8
+ - image: solid-color(13, 24, 0, 255, 8, 8)
+ bounds: 104 192 8 8
+ - image: solid-color(14, 24, 0, 255, 8, 8)
+ bounds: 112 192 8 8
+ - image: solid-color(15, 24, 0, 255, 8, 8)
+ bounds: 120 192 8 8
+ - image: solid-color(16, 24, 0, 255, 8, 8)
+ bounds: 128 192 8 8
+ - image: solid-color(17, 24, 0, 255, 8, 8)
+ bounds: 136 192 8 8
+ - image: solid-color(18, 24, 0, 255, 8, 8)
+ bounds: 144 192 8 8
+ - image: solid-color(19, 24, 0, 255, 8, 8)
+ bounds: 152 192 8 8
+ - image: solid-color(20, 24, 0, 255, 8, 8)
+ bounds: 160 192 8 8
+ - image: solid-color(21, 24, 0, 255, 8, 8)
+ bounds: 168 192 8 8
+ - image: solid-color(22, 24, 0, 255, 8, 8)
+ bounds: 176 192 8 8
+ - image: solid-color(23, 24, 0, 255, 8, 8)
+ bounds: 184 192 8 8
+ - image: solid-color(24, 24, 0, 255, 8, 8)
+ bounds: 192 192 8 8
+ - image: solid-color(25, 24, 0, 255, 8, 8)
+ bounds: 200 192 8 8
+ - image: solid-color(26, 24, 0, 255, 8, 8)
+ bounds: 208 192 8 8
+ - image: solid-color(27, 24, 0, 255, 8, 8)
+ bounds: 216 192 8 8
+ - image: solid-color(28, 24, 0, 255, 8, 8)
+ bounds: 224 192 8 8
+ - image: solid-color(29, 24, 0, 255, 8, 8)
+ bounds: 232 192 8 8
+ - image: solid-color(30, 24, 0, 255, 8, 8)
+ bounds: 240 192 8 8
+ - image: solid-color(31, 24, 0, 255, 8, 8)
+ bounds: 248 192 8 8
+ - image: solid-color(32, 24, 0, 255, 8, 8)
+ bounds: 256 192 8 8
+ - image: solid-color(33, 24, 0, 255, 8, 8)
+ bounds: 264 192 8 8
+ - image: solid-color(34, 24, 0, 255, 8, 8)
+ bounds: 272 192 8 8
+ - image: solid-color(35, 24, 0, 255, 8, 8)
+ bounds: 280 192 8 8
+ - image: solid-color(36, 24, 0, 255, 8, 8)
+ bounds: 288 192 8 8
+ - image: solid-color(37, 24, 0, 255, 8, 8)
+ bounds: 296 192 8 8
+ - image: solid-color(38, 24, 0, 255, 8, 8)
+ bounds: 304 192 8 8
+ - image: solid-color(39, 24, 0, 255, 8, 8)
+ bounds: 312 192 8 8
+ - image: solid-color(40, 24, 0, 255, 8, 8)
+ bounds: 320 192 8 8
+ - image: solid-color(41, 24, 0, 255, 8, 8)
+ bounds: 328 192 8 8
+ - image: solid-color(42, 24, 0, 255, 8, 8)
+ bounds: 336 192 8 8
+ - image: solid-color(43, 24, 0, 255, 8, 8)
+ bounds: 344 192 8 8
+ - image: solid-color(44, 24, 0, 255, 8, 8)
+ bounds: 352 192 8 8
+ - image: solid-color(45, 24, 0, 255, 8, 8)
+ bounds: 360 192 8 8
+ - image: solid-color(46, 24, 0, 255, 8, 8)
+ bounds: 368 192 8 8
+ - image: solid-color(47, 24, 0, 255, 8, 8)
+ bounds: 376 192 8 8
+ - image: solid-color(48, 24, 0, 255, 8, 8)
+ bounds: 384 192 8 8
+ - image: solid-color(49, 24, 0, 255, 8, 8)
+ bounds: 392 192 8 8
+ - image: solid-color(50, 24, 0, 255, 8, 8)
+ bounds: 400 192 8 8
+ - image: solid-color(51, 24, 0, 255, 8, 8)
+ bounds: 408 192 8 8
+ - image: solid-color(52, 24, 0, 255, 8, 8)
+ bounds: 416 192 8 8
+ - image: solid-color(53, 24, 0, 255, 8, 8)
+ bounds: 424 192 8 8
+ - image: solid-color(54, 24, 0, 255, 8, 8)
+ bounds: 432 192 8 8
+ - image: solid-color(55, 24, 0, 255, 8, 8)
+ bounds: 440 192 8 8
+ - image: solid-color(56, 24, 0, 255, 8, 8)
+ bounds: 448 192 8 8
+ - image: solid-color(57, 24, 0, 255, 8, 8)
+ bounds: 456 192 8 8
+ - image: solid-color(58, 24, 0, 255, 8, 8)
+ bounds: 464 192 8 8
+ - image: solid-color(59, 24, 0, 255, 8, 8)
+ bounds: 472 192 8 8
+ - image: solid-color(60, 24, 0, 255, 8, 8)
+ bounds: 480 192 8 8
+ - image: solid-color(61, 24, 0, 255, 8, 8)
+ bounds: 488 192 8 8
+ - image: solid-color(62, 24, 0, 255, 8, 8)
+ bounds: 496 192 8 8
+ - image: solid-color(63, 24, 0, 255, 8, 8)
+ bounds: 504 192 8 8
+ - image: solid-color(64, 24, 0, 255, 8, 8)
+ bounds: 512 192 8 8
+ - image: solid-color(65, 24, 0, 255, 8, 8)
+ bounds: 520 192 8 8
+ - image: solid-color(66, 24, 0, 255, 8, 8)
+ bounds: 528 192 8 8
+ - image: solid-color(67, 24, 0, 255, 8, 8)
+ bounds: 536 192 8 8
+ - image: solid-color(68, 24, 0, 255, 8, 8)
+ bounds: 544 192 8 8
+ - image: solid-color(69, 24, 0, 255, 8, 8)
+ bounds: 552 192 8 8
+ - image: solid-color(70, 24, 0, 255, 8, 8)
+ bounds: 560 192 8 8
+ - image: solid-color(71, 24, 0, 255, 8, 8)
+ bounds: 568 192 8 8
+ - image: solid-color(72, 24, 0, 255, 8, 8)
+ bounds: 576 192 8 8
+ - image: solid-color(73, 24, 0, 255, 8, 8)
+ bounds: 584 192 8 8
+ - image: solid-color(74, 24, 0, 255, 8, 8)
+ bounds: 592 192 8 8
+ - image: solid-color(75, 24, 0, 255, 8, 8)
+ bounds: 600 192 8 8
+ - image: solid-color(76, 24, 0, 255, 8, 8)
+ bounds: 608 192 8 8
+ - image: solid-color(77, 24, 0, 255, 8, 8)
+ bounds: 616 192 8 8
+ - image: solid-color(78, 24, 0, 255, 8, 8)
+ bounds: 624 192 8 8
+ - image: solid-color(79, 24, 0, 255, 8, 8)
+ bounds: 632 192 8 8
+ - image: solid-color(80, 24, 0, 255, 8, 8)
+ bounds: 640 192 8 8
+ - image: solid-color(81, 24, 0, 255, 8, 8)
+ bounds: 648 192 8 8
+ - image: solid-color(82, 24, 0, 255, 8, 8)
+ bounds: 656 192 8 8
+ - image: solid-color(83, 24, 0, 255, 8, 8)
+ bounds: 664 192 8 8
+ - image: solid-color(84, 24, 0, 255, 8, 8)
+ bounds: 672 192 8 8
+ - image: solid-color(85, 24, 0, 255, 8, 8)
+ bounds: 680 192 8 8
+ - image: solid-color(86, 24, 0, 255, 8, 8)
+ bounds: 688 192 8 8
+ - image: solid-color(87, 24, 0, 255, 8, 8)
+ bounds: 696 192 8 8
+ - image: solid-color(88, 24, 0, 255, 8, 8)
+ bounds: 704 192 8 8
+ - image: solid-color(89, 24, 0, 255, 8, 8)
+ bounds: 712 192 8 8
+ - image: solid-color(90, 24, 0, 255, 8, 8)
+ bounds: 720 192 8 8
+ - image: solid-color(91, 24, 0, 255, 8, 8)
+ bounds: 728 192 8 8
+ - image: solid-color(92, 24, 0, 255, 8, 8)
+ bounds: 736 192 8 8
+ - image: solid-color(93, 24, 0, 255, 8, 8)
+ bounds: 744 192 8 8
+ - image: solid-color(94, 24, 0, 255, 8, 8)
+ bounds: 752 192 8 8
+ - image: solid-color(95, 24, 0, 255, 8, 8)
+ bounds: 760 192 8 8
+ - image: solid-color(96, 24, 0, 255, 8, 8)
+ bounds: 768 192 8 8
+ - image: solid-color(97, 24, 0, 255, 8, 8)
+ bounds: 776 192 8 8
+ - image: solid-color(98, 24, 0, 255, 8, 8)
+ bounds: 784 192 8 8
+ - image: solid-color(99, 24, 0, 255, 8, 8)
+ bounds: 792 192 8 8
+ - image: solid-color(100, 24, 0, 255, 8, 8)
+ bounds: 800 192 8 8
+ - image: solid-color(101, 24, 0, 255, 8, 8)
+ bounds: 808 192 8 8
+ - image: solid-color(102, 24, 0, 255, 8, 8)
+ bounds: 816 192 8 8
+ - image: solid-color(103, 24, 0, 255, 8, 8)
+ bounds: 824 192 8 8
+ - image: solid-color(104, 24, 0, 255, 8, 8)
+ bounds: 832 192 8 8
+ - image: solid-color(105, 24, 0, 255, 8, 8)
+ bounds: 840 192 8 8
+ - image: solid-color(106, 24, 0, 255, 8, 8)
+ bounds: 848 192 8 8
+ - image: solid-color(107, 24, 0, 255, 8, 8)
+ bounds: 856 192 8 8
+ - image: solid-color(108, 24, 0, 255, 8, 8)
+ bounds: 864 192 8 8
+ - image: solid-color(109, 24, 0, 255, 8, 8)
+ bounds: 872 192 8 8
+ - image: solid-color(110, 24, 0, 255, 8, 8)
+ bounds: 880 192 8 8
+ - image: solid-color(111, 24, 0, 255, 8, 8)
+ bounds: 888 192 8 8
+ - image: solid-color(112, 24, 0, 255, 8, 8)
+ bounds: 896 192 8 8
+ - image: solid-color(113, 24, 0, 255, 8, 8)
+ bounds: 904 192 8 8
+ - image: solid-color(114, 24, 0, 255, 8, 8)
+ bounds: 912 192 8 8
+ - image: solid-color(115, 24, 0, 255, 8, 8)
+ bounds: 920 192 8 8
+ - image: solid-color(116, 24, 0, 255, 8, 8)
+ bounds: 928 192 8 8
+ - image: solid-color(117, 24, 0, 255, 8, 8)
+ bounds: 936 192 8 8
+ - image: solid-color(118, 24, 0, 255, 8, 8)
+ bounds: 944 192 8 8
+ - image: solid-color(119, 24, 0, 255, 8, 8)
+ bounds: 952 192 8 8
+ - image: solid-color(120, 24, 0, 255, 8, 8)
+ bounds: 960 192 8 8
+ - image: solid-color(121, 24, 0, 255, 8, 8)
+ bounds: 968 192 8 8
+ - image: solid-color(122, 24, 0, 255, 8, 8)
+ bounds: 976 192 8 8
+ - image: solid-color(123, 24, 0, 255, 8, 8)
+ bounds: 984 192 8 8
+ - image: solid-color(124, 24, 0, 255, 8, 8)
+ bounds: 992 192 8 8
+ - image: solid-color(125, 24, 0, 255, 8, 8)
+ bounds: 1000 192 8 8
+ - image: solid-color(126, 24, 0, 255, 8, 8)
+ bounds: 1008 192 8 8
+ - image: solid-color(127, 24, 0, 255, 8, 8)
+ bounds: 1016 192 8 8
+ - image: solid-color(0, 25, 0, 255, 8, 8)
+ bounds: 0 200 8 8
+ - image: solid-color(1, 25, 0, 255, 8, 8)
+ bounds: 8 200 8 8
+ - image: solid-color(2, 25, 0, 255, 8, 8)
+ bounds: 16 200 8 8
+ - image: solid-color(3, 25, 0, 255, 8, 8)
+ bounds: 24 200 8 8
+ - image: solid-color(4, 25, 0, 255, 8, 8)
+ bounds: 32 200 8 8
+ - image: solid-color(5, 25, 0, 255, 8, 8)
+ bounds: 40 200 8 8
+ - image: solid-color(6, 25, 0, 255, 8, 8)
+ bounds: 48 200 8 8
+ - image: solid-color(7, 25, 0, 255, 8, 8)
+ bounds: 56 200 8 8
+ - image: solid-color(8, 25, 0, 255, 8, 8)
+ bounds: 64 200 8 8
+ - image: solid-color(9, 25, 0, 255, 8, 8)
+ bounds: 72 200 8 8
+ - image: solid-color(10, 25, 0, 255, 8, 8)
+ bounds: 80 200 8 8
+ - image: solid-color(11, 25, 0, 255, 8, 8)
+ bounds: 88 200 8 8
+ - image: solid-color(12, 25, 0, 255, 8, 8)
+ bounds: 96 200 8 8
+ - image: solid-color(13, 25, 0, 255, 8, 8)
+ bounds: 104 200 8 8
+ - image: solid-color(14, 25, 0, 255, 8, 8)
+ bounds: 112 200 8 8
+ - image: solid-color(15, 25, 0, 255, 8, 8)
+ bounds: 120 200 8 8
+ - image: solid-color(16, 25, 0, 255, 8, 8)
+ bounds: 128 200 8 8
+ - image: solid-color(17, 25, 0, 255, 8, 8)
+ bounds: 136 200 8 8
+ - image: solid-color(18, 25, 0, 255, 8, 8)
+ bounds: 144 200 8 8
+ - image: solid-color(19, 25, 0, 255, 8, 8)
+ bounds: 152 200 8 8
+ - image: solid-color(20, 25, 0, 255, 8, 8)
+ bounds: 160 200 8 8
+ - image: solid-color(21, 25, 0, 255, 8, 8)
+ bounds: 168 200 8 8
+ - image: solid-color(22, 25, 0, 255, 8, 8)
+ bounds: 176 200 8 8
+ - image: solid-color(23, 25, 0, 255, 8, 8)
+ bounds: 184 200 8 8
+ - image: solid-color(24, 25, 0, 255, 8, 8)
+ bounds: 192 200 8 8
+ - image: solid-color(25, 25, 0, 255, 8, 8)
+ bounds: 200 200 8 8
+ - image: solid-color(26, 25, 0, 255, 8, 8)
+ bounds: 208 200 8 8
+ - image: solid-color(27, 25, 0, 255, 8, 8)
+ bounds: 216 200 8 8
+ - image: solid-color(28, 25, 0, 255, 8, 8)
+ bounds: 224 200 8 8
+ - image: solid-color(29, 25, 0, 255, 8, 8)
+ bounds: 232 200 8 8
+ - image: solid-color(30, 25, 0, 255, 8, 8)
+ bounds: 240 200 8 8
+ - image: solid-color(31, 25, 0, 255, 8, 8)
+ bounds: 248 200 8 8
+ - image: solid-color(32, 25, 0, 255, 8, 8)
+ bounds: 256 200 8 8
+ - image: solid-color(33, 25, 0, 255, 8, 8)
+ bounds: 264 200 8 8
+ - image: solid-color(34, 25, 0, 255, 8, 8)
+ bounds: 272 200 8 8
+ - image: solid-color(35, 25, 0, 255, 8, 8)
+ bounds: 280 200 8 8
+ - image: solid-color(36, 25, 0, 255, 8, 8)
+ bounds: 288 200 8 8
+ - image: solid-color(37, 25, 0, 255, 8, 8)
+ bounds: 296 200 8 8
+ - image: solid-color(38, 25, 0, 255, 8, 8)
+ bounds: 304 200 8 8
+ - image: solid-color(39, 25, 0, 255, 8, 8)
+ bounds: 312 200 8 8
+ - image: solid-color(40, 25, 0, 255, 8, 8)
+ bounds: 320 200 8 8
+ - image: solid-color(41, 25, 0, 255, 8, 8)
+ bounds: 328 200 8 8
+ - image: solid-color(42, 25, 0, 255, 8, 8)
+ bounds: 336 200 8 8
+ - image: solid-color(43, 25, 0, 255, 8, 8)
+ bounds: 344 200 8 8
+ - image: solid-color(44, 25, 0, 255, 8, 8)
+ bounds: 352 200 8 8
+ - image: solid-color(45, 25, 0, 255, 8, 8)
+ bounds: 360 200 8 8
+ - image: solid-color(46, 25, 0, 255, 8, 8)
+ bounds: 368 200 8 8
+ - image: solid-color(47, 25, 0, 255, 8, 8)
+ bounds: 376 200 8 8
+ - image: solid-color(48, 25, 0, 255, 8, 8)
+ bounds: 384 200 8 8
+ - image: solid-color(49, 25, 0, 255, 8, 8)
+ bounds: 392 200 8 8
+ - image: solid-color(50, 25, 0, 255, 8, 8)
+ bounds: 400 200 8 8
+ - image: solid-color(51, 25, 0, 255, 8, 8)
+ bounds: 408 200 8 8
+ - image: solid-color(52, 25, 0, 255, 8, 8)
+ bounds: 416 200 8 8
+ - image: solid-color(53, 25, 0, 255, 8, 8)
+ bounds: 424 200 8 8
+ - image: solid-color(54, 25, 0, 255, 8, 8)
+ bounds: 432 200 8 8
+ - image: solid-color(55, 25, 0, 255, 8, 8)
+ bounds: 440 200 8 8
+ - image: solid-color(56, 25, 0, 255, 8, 8)
+ bounds: 448 200 8 8
+ - image: solid-color(57, 25, 0, 255, 8, 8)
+ bounds: 456 200 8 8
+ - image: solid-color(58, 25, 0, 255, 8, 8)
+ bounds: 464 200 8 8
+ - image: solid-color(59, 25, 0, 255, 8, 8)
+ bounds: 472 200 8 8
+ - image: solid-color(60, 25, 0, 255, 8, 8)
+ bounds: 480 200 8 8
+ - image: solid-color(61, 25, 0, 255, 8, 8)
+ bounds: 488 200 8 8
+ - image: solid-color(62, 25, 0, 255, 8, 8)
+ bounds: 496 200 8 8
+ - image: solid-color(63, 25, 0, 255, 8, 8)
+ bounds: 504 200 8 8
+ - image: solid-color(64, 25, 0, 255, 8, 8)
+ bounds: 512 200 8 8
+ - image: solid-color(65, 25, 0, 255, 8, 8)
+ bounds: 520 200 8 8
+ - image: solid-color(66, 25, 0, 255, 8, 8)
+ bounds: 528 200 8 8
+ - image: solid-color(67, 25, 0, 255, 8, 8)
+ bounds: 536 200 8 8
+ - image: solid-color(68, 25, 0, 255, 8, 8)
+ bounds: 544 200 8 8
+ - image: solid-color(69, 25, 0, 255, 8, 8)
+ bounds: 552 200 8 8
+ - image: solid-color(70, 25, 0, 255, 8, 8)
+ bounds: 560 200 8 8
+ - image: solid-color(71, 25, 0, 255, 8, 8)
+ bounds: 568 200 8 8
+ - image: solid-color(72, 25, 0, 255, 8, 8)
+ bounds: 576 200 8 8
+ - image: solid-color(73, 25, 0, 255, 8, 8)
+ bounds: 584 200 8 8
+ - image: solid-color(74, 25, 0, 255, 8, 8)
+ bounds: 592 200 8 8
+ - image: solid-color(75, 25, 0, 255, 8, 8)
+ bounds: 600 200 8 8
+ - image: solid-color(76, 25, 0, 255, 8, 8)
+ bounds: 608 200 8 8
+ - image: solid-color(77, 25, 0, 255, 8, 8)
+ bounds: 616 200 8 8
+ - image: solid-color(78, 25, 0, 255, 8, 8)
+ bounds: 624 200 8 8
+ - image: solid-color(79, 25, 0, 255, 8, 8)
+ bounds: 632 200 8 8
+ - image: solid-color(80, 25, 0, 255, 8, 8)
+ bounds: 640 200 8 8
+ - image: solid-color(81, 25, 0, 255, 8, 8)
+ bounds: 648 200 8 8
+ - image: solid-color(82, 25, 0, 255, 8, 8)
+ bounds: 656 200 8 8
+ - image: solid-color(83, 25, 0, 255, 8, 8)
+ bounds: 664 200 8 8
+ - image: solid-color(84, 25, 0, 255, 8, 8)
+ bounds: 672 200 8 8
+ - image: solid-color(85, 25, 0, 255, 8, 8)
+ bounds: 680 200 8 8
+ - image: solid-color(86, 25, 0, 255, 8, 8)
+ bounds: 688 200 8 8
+ - image: solid-color(87, 25, 0, 255, 8, 8)
+ bounds: 696 200 8 8
+ - image: solid-color(88, 25, 0, 255, 8, 8)
+ bounds: 704 200 8 8
+ - image: solid-color(89, 25, 0, 255, 8, 8)
+ bounds: 712 200 8 8
+ - image: solid-color(90, 25, 0, 255, 8, 8)
+ bounds: 720 200 8 8
+ - image: solid-color(91, 25, 0, 255, 8, 8)
+ bounds: 728 200 8 8
+ - image: solid-color(92, 25, 0, 255, 8, 8)
+ bounds: 736 200 8 8
+ - image: solid-color(93, 25, 0, 255, 8, 8)
+ bounds: 744 200 8 8
+ - image: solid-color(94, 25, 0, 255, 8, 8)
+ bounds: 752 200 8 8
+ - image: solid-color(95, 25, 0, 255, 8, 8)
+ bounds: 760 200 8 8
+ - image: solid-color(96, 25, 0, 255, 8, 8)
+ bounds: 768 200 8 8
+ - image: solid-color(97, 25, 0, 255, 8, 8)
+ bounds: 776 200 8 8
+ - image: solid-color(98, 25, 0, 255, 8, 8)
+ bounds: 784 200 8 8
+ - image: solid-color(99, 25, 0, 255, 8, 8)
+ bounds: 792 200 8 8
+ - image: solid-color(100, 25, 0, 255, 8, 8)
+ bounds: 800 200 8 8
+ - image: solid-color(101, 25, 0, 255, 8, 8)
+ bounds: 808 200 8 8
+ - image: solid-color(102, 25, 0, 255, 8, 8)
+ bounds: 816 200 8 8
+ - image: solid-color(103, 25, 0, 255, 8, 8)
+ bounds: 824 200 8 8
+ - image: solid-color(104, 25, 0, 255, 8, 8)
+ bounds: 832 200 8 8
+ - image: solid-color(105, 25, 0, 255, 8, 8)
+ bounds: 840 200 8 8
+ - image: solid-color(106, 25, 0, 255, 8, 8)
+ bounds: 848 200 8 8
+ - image: solid-color(107, 25, 0, 255, 8, 8)
+ bounds: 856 200 8 8
+ - image: solid-color(108, 25, 0, 255, 8, 8)
+ bounds: 864 200 8 8
+ - image: solid-color(109, 25, 0, 255, 8, 8)
+ bounds: 872 200 8 8
+ - image: solid-color(110, 25, 0, 255, 8, 8)
+ bounds: 880 200 8 8
+ - image: solid-color(111, 25, 0, 255, 8, 8)
+ bounds: 888 200 8 8
+ - image: solid-color(112, 25, 0, 255, 8, 8)
+ bounds: 896 200 8 8
+ - image: solid-color(113, 25, 0, 255, 8, 8)
+ bounds: 904 200 8 8
+ - image: solid-color(114, 25, 0, 255, 8, 8)
+ bounds: 912 200 8 8
+ - image: solid-color(115, 25, 0, 255, 8, 8)
+ bounds: 920 200 8 8
+ - image: solid-color(116, 25, 0, 255, 8, 8)
+ bounds: 928 200 8 8
+ - image: solid-color(117, 25, 0, 255, 8, 8)
+ bounds: 936 200 8 8
+ - image: solid-color(118, 25, 0, 255, 8, 8)
+ bounds: 944 200 8 8
+ - image: solid-color(119, 25, 0, 255, 8, 8)
+ bounds: 952 200 8 8
+ - image: solid-color(120, 25, 0, 255, 8, 8)
+ bounds: 960 200 8 8
+ - image: solid-color(121, 25, 0, 255, 8, 8)
+ bounds: 968 200 8 8
+ - image: solid-color(122, 25, 0, 255, 8, 8)
+ bounds: 976 200 8 8
+ - image: solid-color(123, 25, 0, 255, 8, 8)
+ bounds: 984 200 8 8
+ - image: solid-color(124, 25, 0, 255, 8, 8)
+ bounds: 992 200 8 8
+ - image: solid-color(125, 25, 0, 255, 8, 8)
+ bounds: 1000 200 8 8
+ - image: solid-color(126, 25, 0, 255, 8, 8)
+ bounds: 1008 200 8 8
+ - image: solid-color(127, 25, 0, 255, 8, 8)
+ bounds: 1016 200 8 8
+ - image: solid-color(0, 26, 0, 255, 8, 8)
+ bounds: 0 208 8 8
+ - image: solid-color(1, 26, 0, 255, 8, 8)
+ bounds: 8 208 8 8
+ - image: solid-color(2, 26, 0, 255, 8, 8)
+ bounds: 16 208 8 8
+ - image: solid-color(3, 26, 0, 255, 8, 8)
+ bounds: 24 208 8 8
+ - image: solid-color(4, 26, 0, 255, 8, 8)
+ bounds: 32 208 8 8
+ - image: solid-color(5, 26, 0, 255, 8, 8)
+ bounds: 40 208 8 8
+ - image: solid-color(6, 26, 0, 255, 8, 8)
+ bounds: 48 208 8 8
+ - image: solid-color(7, 26, 0, 255, 8, 8)
+ bounds: 56 208 8 8
+ - image: solid-color(8, 26, 0, 255, 8, 8)
+ bounds: 64 208 8 8
+ - image: solid-color(9, 26, 0, 255, 8, 8)
+ bounds: 72 208 8 8
+ - image: solid-color(10, 26, 0, 255, 8, 8)
+ bounds: 80 208 8 8
+ - image: solid-color(11, 26, 0, 255, 8, 8)
+ bounds: 88 208 8 8
+ - image: solid-color(12, 26, 0, 255, 8, 8)
+ bounds: 96 208 8 8
+ - image: solid-color(13, 26, 0, 255, 8, 8)
+ bounds: 104 208 8 8
+ - image: solid-color(14, 26, 0, 255, 8, 8)
+ bounds: 112 208 8 8
+ - image: solid-color(15, 26, 0, 255, 8, 8)
+ bounds: 120 208 8 8
+ - image: solid-color(16, 26, 0, 255, 8, 8)
+ bounds: 128 208 8 8
+ - image: solid-color(17, 26, 0, 255, 8, 8)
+ bounds: 136 208 8 8
+ - image: solid-color(18, 26, 0, 255, 8, 8)
+ bounds: 144 208 8 8
+ - image: solid-color(19, 26, 0, 255, 8, 8)
+ bounds: 152 208 8 8
+ - image: solid-color(20, 26, 0, 255, 8, 8)
+ bounds: 160 208 8 8
+ - image: solid-color(21, 26, 0, 255, 8, 8)
+ bounds: 168 208 8 8
+ - image: solid-color(22, 26, 0, 255, 8, 8)
+ bounds: 176 208 8 8
+ - image: solid-color(23, 26, 0, 255, 8, 8)
+ bounds: 184 208 8 8
+ - image: solid-color(24, 26, 0, 255, 8, 8)
+ bounds: 192 208 8 8
+ - image: solid-color(25, 26, 0, 255, 8, 8)
+ bounds: 200 208 8 8
+ - image: solid-color(26, 26, 0, 255, 8, 8)
+ bounds: 208 208 8 8
+ - image: solid-color(27, 26, 0, 255, 8, 8)
+ bounds: 216 208 8 8
+ - image: solid-color(28, 26, 0, 255, 8, 8)
+ bounds: 224 208 8 8
+ - image: solid-color(29, 26, 0, 255, 8, 8)
+ bounds: 232 208 8 8
+ - image: solid-color(30, 26, 0, 255, 8, 8)
+ bounds: 240 208 8 8
+ - image: solid-color(31, 26, 0, 255, 8, 8)
+ bounds: 248 208 8 8
+ - image: solid-color(32, 26, 0, 255, 8, 8)
+ bounds: 256 208 8 8
+ - image: solid-color(33, 26, 0, 255, 8, 8)
+ bounds: 264 208 8 8
+ - image: solid-color(34, 26, 0, 255, 8, 8)
+ bounds: 272 208 8 8
+ - image: solid-color(35, 26, 0, 255, 8, 8)
+ bounds: 280 208 8 8
+ - image: solid-color(36, 26, 0, 255, 8, 8)
+ bounds: 288 208 8 8
+ - image: solid-color(37, 26, 0, 255, 8, 8)
+ bounds: 296 208 8 8
+ - image: solid-color(38, 26, 0, 255, 8, 8)
+ bounds: 304 208 8 8
+ - image: solid-color(39, 26, 0, 255, 8, 8)
+ bounds: 312 208 8 8
+ - image: solid-color(40, 26, 0, 255, 8, 8)
+ bounds: 320 208 8 8
+ - image: solid-color(41, 26, 0, 255, 8, 8)
+ bounds: 328 208 8 8
+ - image: solid-color(42, 26, 0, 255, 8, 8)
+ bounds: 336 208 8 8
+ - image: solid-color(43, 26, 0, 255, 8, 8)
+ bounds: 344 208 8 8
+ - image: solid-color(44, 26, 0, 255, 8, 8)
+ bounds: 352 208 8 8
+ - image: solid-color(45, 26, 0, 255, 8, 8)
+ bounds: 360 208 8 8
+ - image: solid-color(46, 26, 0, 255, 8, 8)
+ bounds: 368 208 8 8
+ - image: solid-color(47, 26, 0, 255, 8, 8)
+ bounds: 376 208 8 8
+ - image: solid-color(48, 26, 0, 255, 8, 8)
+ bounds: 384 208 8 8
+ - image: solid-color(49, 26, 0, 255, 8, 8)
+ bounds: 392 208 8 8
+ - image: solid-color(50, 26, 0, 255, 8, 8)
+ bounds: 400 208 8 8
+ - image: solid-color(51, 26, 0, 255, 8, 8)
+ bounds: 408 208 8 8
+ - image: solid-color(52, 26, 0, 255, 8, 8)
+ bounds: 416 208 8 8
+ - image: solid-color(53, 26, 0, 255, 8, 8)
+ bounds: 424 208 8 8
+ - image: solid-color(54, 26, 0, 255, 8, 8)
+ bounds: 432 208 8 8
+ - image: solid-color(55, 26, 0, 255, 8, 8)
+ bounds: 440 208 8 8
+ - image: solid-color(56, 26, 0, 255, 8, 8)
+ bounds: 448 208 8 8
+ - image: solid-color(57, 26, 0, 255, 8, 8)
+ bounds: 456 208 8 8
+ - image: solid-color(58, 26, 0, 255, 8, 8)
+ bounds: 464 208 8 8
+ - image: solid-color(59, 26, 0, 255, 8, 8)
+ bounds: 472 208 8 8
+ - image: solid-color(60, 26, 0, 255, 8, 8)
+ bounds: 480 208 8 8
+ - image: solid-color(61, 26, 0, 255, 8, 8)
+ bounds: 488 208 8 8
+ - image: solid-color(62, 26, 0, 255, 8, 8)
+ bounds: 496 208 8 8
+ - image: solid-color(63, 26, 0, 255, 8, 8)
+ bounds: 504 208 8 8
+ - image: solid-color(64, 26, 0, 255, 8, 8)
+ bounds: 512 208 8 8
+ - image: solid-color(65, 26, 0, 255, 8, 8)
+ bounds: 520 208 8 8
+ - image: solid-color(66, 26, 0, 255, 8, 8)
+ bounds: 528 208 8 8
+ - image: solid-color(67, 26, 0, 255, 8, 8)
+ bounds: 536 208 8 8
+ - image: solid-color(68, 26, 0, 255, 8, 8)
+ bounds: 544 208 8 8
+ - image: solid-color(69, 26, 0, 255, 8, 8)
+ bounds: 552 208 8 8
+ - image: solid-color(70, 26, 0, 255, 8, 8)
+ bounds: 560 208 8 8
+ - image: solid-color(71, 26, 0, 255, 8, 8)
+ bounds: 568 208 8 8
+ - image: solid-color(72, 26, 0, 255, 8, 8)
+ bounds: 576 208 8 8
+ - image: solid-color(73, 26, 0, 255, 8, 8)
+ bounds: 584 208 8 8
+ - image: solid-color(74, 26, 0, 255, 8, 8)
+ bounds: 592 208 8 8
+ - image: solid-color(75, 26, 0, 255, 8, 8)
+ bounds: 600 208 8 8
+ - image: solid-color(76, 26, 0, 255, 8, 8)
+ bounds: 608 208 8 8
+ - image: solid-color(77, 26, 0, 255, 8, 8)
+ bounds: 616 208 8 8
+ - image: solid-color(78, 26, 0, 255, 8, 8)
+ bounds: 624 208 8 8
+ - image: solid-color(79, 26, 0, 255, 8, 8)
+ bounds: 632 208 8 8
+ - image: solid-color(80, 26, 0, 255, 8, 8)
+ bounds: 640 208 8 8
+ - image: solid-color(81, 26, 0, 255, 8, 8)
+ bounds: 648 208 8 8
+ - image: solid-color(82, 26, 0, 255, 8, 8)
+ bounds: 656 208 8 8
+ - image: solid-color(83, 26, 0, 255, 8, 8)
+ bounds: 664 208 8 8
+ - image: solid-color(84, 26, 0, 255, 8, 8)
+ bounds: 672 208 8 8
+ - image: solid-color(85, 26, 0, 255, 8, 8)
+ bounds: 680 208 8 8
+ - image: solid-color(86, 26, 0, 255, 8, 8)
+ bounds: 688 208 8 8
+ - image: solid-color(87, 26, 0, 255, 8, 8)
+ bounds: 696 208 8 8
+ - image: solid-color(88, 26, 0, 255, 8, 8)
+ bounds: 704 208 8 8
+ - image: solid-color(89, 26, 0, 255, 8, 8)
+ bounds: 712 208 8 8
+ - image: solid-color(90, 26, 0, 255, 8, 8)
+ bounds: 720 208 8 8
+ - image: solid-color(91, 26, 0, 255, 8, 8)
+ bounds: 728 208 8 8
+ - image: solid-color(92, 26, 0, 255, 8, 8)
+ bounds: 736 208 8 8
+ - image: solid-color(93, 26, 0, 255, 8, 8)
+ bounds: 744 208 8 8
+ - image: solid-color(94, 26, 0, 255, 8, 8)
+ bounds: 752 208 8 8
+ - image: solid-color(95, 26, 0, 255, 8, 8)
+ bounds: 760 208 8 8
+ - image: solid-color(96, 26, 0, 255, 8, 8)
+ bounds: 768 208 8 8
+ - image: solid-color(97, 26, 0, 255, 8, 8)
+ bounds: 776 208 8 8
+ - image: solid-color(98, 26, 0, 255, 8, 8)
+ bounds: 784 208 8 8
+ - image: solid-color(99, 26, 0, 255, 8, 8)
+ bounds: 792 208 8 8
+ - image: solid-color(100, 26, 0, 255, 8, 8)
+ bounds: 800 208 8 8
+ - image: solid-color(101, 26, 0, 255, 8, 8)
+ bounds: 808 208 8 8
+ - image: solid-color(102, 26, 0, 255, 8, 8)
+ bounds: 816 208 8 8
+ - image: solid-color(103, 26, 0, 255, 8, 8)
+ bounds: 824 208 8 8
+ - image: solid-color(104, 26, 0, 255, 8, 8)
+ bounds: 832 208 8 8
+ - image: solid-color(105, 26, 0, 255, 8, 8)
+ bounds: 840 208 8 8
+ - image: solid-color(106, 26, 0, 255, 8, 8)
+ bounds: 848 208 8 8
+ - image: solid-color(107, 26, 0, 255, 8, 8)
+ bounds: 856 208 8 8
+ - image: solid-color(108, 26, 0, 255, 8, 8)
+ bounds: 864 208 8 8
+ - image: solid-color(109, 26, 0, 255, 8, 8)
+ bounds: 872 208 8 8
+ - image: solid-color(110, 26, 0, 255, 8, 8)
+ bounds: 880 208 8 8
+ - image: solid-color(111, 26, 0, 255, 8, 8)
+ bounds: 888 208 8 8
+ - image: solid-color(112, 26, 0, 255, 8, 8)
+ bounds: 896 208 8 8
+ - image: solid-color(113, 26, 0, 255, 8, 8)
+ bounds: 904 208 8 8
+ - image: solid-color(114, 26, 0, 255, 8, 8)
+ bounds: 912 208 8 8
+ - image: solid-color(115, 26, 0, 255, 8, 8)
+ bounds: 920 208 8 8
+ - image: solid-color(116, 26, 0, 255, 8, 8)
+ bounds: 928 208 8 8
+ - image: solid-color(117, 26, 0, 255, 8, 8)
+ bounds: 936 208 8 8
+ - image: solid-color(118, 26, 0, 255, 8, 8)
+ bounds: 944 208 8 8
+ - image: solid-color(119, 26, 0, 255, 8, 8)
+ bounds: 952 208 8 8
+ - image: solid-color(120, 26, 0, 255, 8, 8)
+ bounds: 960 208 8 8
+ - image: solid-color(121, 26, 0, 255, 8, 8)
+ bounds: 968 208 8 8
+ - image: solid-color(122, 26, 0, 255, 8, 8)
+ bounds: 976 208 8 8
+ - image: solid-color(123, 26, 0, 255, 8, 8)
+ bounds: 984 208 8 8
+ - image: solid-color(124, 26, 0, 255, 8, 8)
+ bounds: 992 208 8 8
+ - image: solid-color(125, 26, 0, 255, 8, 8)
+ bounds: 1000 208 8 8
+ - image: solid-color(126, 26, 0, 255, 8, 8)
+ bounds: 1008 208 8 8
+ - image: solid-color(127, 26, 0, 255, 8, 8)
+ bounds: 1016 208 8 8
+ - image: solid-color(0, 27, 0, 255, 8, 8)
+ bounds: 0 216 8 8
+ - image: solid-color(1, 27, 0, 255, 8, 8)
+ bounds: 8 216 8 8
+ - image: solid-color(2, 27, 0, 255, 8, 8)
+ bounds: 16 216 8 8
+ - image: solid-color(3, 27, 0, 255, 8, 8)
+ bounds: 24 216 8 8
+ - image: solid-color(4, 27, 0, 255, 8, 8)
+ bounds: 32 216 8 8
+ - image: solid-color(5, 27, 0, 255, 8, 8)
+ bounds: 40 216 8 8
+ - image: solid-color(6, 27, 0, 255, 8, 8)
+ bounds: 48 216 8 8
+ - image: solid-color(7, 27, 0, 255, 8, 8)
+ bounds: 56 216 8 8
+ - image: solid-color(8, 27, 0, 255, 8, 8)
+ bounds: 64 216 8 8
+ - image: solid-color(9, 27, 0, 255, 8, 8)
+ bounds: 72 216 8 8
+ - image: solid-color(10, 27, 0, 255, 8, 8)
+ bounds: 80 216 8 8
+ - image: solid-color(11, 27, 0, 255, 8, 8)
+ bounds: 88 216 8 8
+ - image: solid-color(12, 27, 0, 255, 8, 8)
+ bounds: 96 216 8 8
+ - image: solid-color(13, 27, 0, 255, 8, 8)
+ bounds: 104 216 8 8
+ - image: solid-color(14, 27, 0, 255, 8, 8)
+ bounds: 112 216 8 8
+ - image: solid-color(15, 27, 0, 255, 8, 8)
+ bounds: 120 216 8 8
+ - image: solid-color(16, 27, 0, 255, 8, 8)
+ bounds: 128 216 8 8
+ - image: solid-color(17, 27, 0, 255, 8, 8)
+ bounds: 136 216 8 8
+ - image: solid-color(18, 27, 0, 255, 8, 8)
+ bounds: 144 216 8 8
+ - image: solid-color(19, 27, 0, 255, 8, 8)
+ bounds: 152 216 8 8
+ - image: solid-color(20, 27, 0, 255, 8, 8)
+ bounds: 160 216 8 8
+ - image: solid-color(21, 27, 0, 255, 8, 8)
+ bounds: 168 216 8 8
+ - image: solid-color(22, 27, 0, 255, 8, 8)
+ bounds: 176 216 8 8
+ - image: solid-color(23, 27, 0, 255, 8, 8)
+ bounds: 184 216 8 8
+ - image: solid-color(24, 27, 0, 255, 8, 8)
+ bounds: 192 216 8 8
+ - image: solid-color(25, 27, 0, 255, 8, 8)
+ bounds: 200 216 8 8
+ - image: solid-color(26, 27, 0, 255, 8, 8)
+ bounds: 208 216 8 8
+ - image: solid-color(27, 27, 0, 255, 8, 8)
+ bounds: 216 216 8 8
+ - image: solid-color(28, 27, 0, 255, 8, 8)
+ bounds: 224 216 8 8
+ - image: solid-color(29, 27, 0, 255, 8, 8)
+ bounds: 232 216 8 8
+ - image: solid-color(30, 27, 0, 255, 8, 8)
+ bounds: 240 216 8 8
+ - image: solid-color(31, 27, 0, 255, 8, 8)
+ bounds: 248 216 8 8
+ - image: solid-color(32, 27, 0, 255, 8, 8)
+ bounds: 256 216 8 8
+ - image: solid-color(33, 27, 0, 255, 8, 8)
+ bounds: 264 216 8 8
+ - image: solid-color(34, 27, 0, 255, 8, 8)
+ bounds: 272 216 8 8
+ - image: solid-color(35, 27, 0, 255, 8, 8)
+ bounds: 280 216 8 8
+ - image: solid-color(36, 27, 0, 255, 8, 8)
+ bounds: 288 216 8 8
+ - image: solid-color(37, 27, 0, 255, 8, 8)
+ bounds: 296 216 8 8
+ - image: solid-color(38, 27, 0, 255, 8, 8)
+ bounds: 304 216 8 8
+ - image: solid-color(39, 27, 0, 255, 8, 8)
+ bounds: 312 216 8 8
+ - image: solid-color(40, 27, 0, 255, 8, 8)
+ bounds: 320 216 8 8
+ - image: solid-color(41, 27, 0, 255, 8, 8)
+ bounds: 328 216 8 8
+ - image: solid-color(42, 27, 0, 255, 8, 8)
+ bounds: 336 216 8 8
+ - image: solid-color(43, 27, 0, 255, 8, 8)
+ bounds: 344 216 8 8
+ - image: solid-color(44, 27, 0, 255, 8, 8)
+ bounds: 352 216 8 8
+ - image: solid-color(45, 27, 0, 255, 8, 8)
+ bounds: 360 216 8 8
+ - image: solid-color(46, 27, 0, 255, 8, 8)
+ bounds: 368 216 8 8
+ - image: solid-color(47, 27, 0, 255, 8, 8)
+ bounds: 376 216 8 8
+ - image: solid-color(48, 27, 0, 255, 8, 8)
+ bounds: 384 216 8 8
+ - image: solid-color(49, 27, 0, 255, 8, 8)
+ bounds: 392 216 8 8
+ - image: solid-color(50, 27, 0, 255, 8, 8)
+ bounds: 400 216 8 8
+ - image: solid-color(51, 27, 0, 255, 8, 8)
+ bounds: 408 216 8 8
+ - image: solid-color(52, 27, 0, 255, 8, 8)
+ bounds: 416 216 8 8
+ - image: solid-color(53, 27, 0, 255, 8, 8)
+ bounds: 424 216 8 8
+ - image: solid-color(54, 27, 0, 255, 8, 8)
+ bounds: 432 216 8 8
+ - image: solid-color(55, 27, 0, 255, 8, 8)
+ bounds: 440 216 8 8
+ - image: solid-color(56, 27, 0, 255, 8, 8)
+ bounds: 448 216 8 8
+ - image: solid-color(57, 27, 0, 255, 8, 8)
+ bounds: 456 216 8 8
+ - image: solid-color(58, 27, 0, 255, 8, 8)
+ bounds: 464 216 8 8
+ - image: solid-color(59, 27, 0, 255, 8, 8)
+ bounds: 472 216 8 8
+ - image: solid-color(60, 27, 0, 255, 8, 8)
+ bounds: 480 216 8 8
+ - image: solid-color(61, 27, 0, 255, 8, 8)
+ bounds: 488 216 8 8
+ - image: solid-color(62, 27, 0, 255, 8, 8)
+ bounds: 496 216 8 8
+ - image: solid-color(63, 27, 0, 255, 8, 8)
+ bounds: 504 216 8 8
+ - image: solid-color(64, 27, 0, 255, 8, 8)
+ bounds: 512 216 8 8
+ - image: solid-color(65, 27, 0, 255, 8, 8)
+ bounds: 520 216 8 8
+ - image: solid-color(66, 27, 0, 255, 8, 8)
+ bounds: 528 216 8 8
+ - image: solid-color(67, 27, 0, 255, 8, 8)
+ bounds: 536 216 8 8
+ - image: solid-color(68, 27, 0, 255, 8, 8)
+ bounds: 544 216 8 8
+ - image: solid-color(69, 27, 0, 255, 8, 8)
+ bounds: 552 216 8 8
+ - image: solid-color(70, 27, 0, 255, 8, 8)
+ bounds: 560 216 8 8
+ - image: solid-color(71, 27, 0, 255, 8, 8)
+ bounds: 568 216 8 8
+ - image: solid-color(72, 27, 0, 255, 8, 8)
+ bounds: 576 216 8 8
+ - image: solid-color(73, 27, 0, 255, 8, 8)
+ bounds: 584 216 8 8
+ - image: solid-color(74, 27, 0, 255, 8, 8)
+ bounds: 592 216 8 8
+ - image: solid-color(75, 27, 0, 255, 8, 8)
+ bounds: 600 216 8 8
+ - image: solid-color(76, 27, 0, 255, 8, 8)
+ bounds: 608 216 8 8
+ - image: solid-color(77, 27, 0, 255, 8, 8)
+ bounds: 616 216 8 8
+ - image: solid-color(78, 27, 0, 255, 8, 8)
+ bounds: 624 216 8 8
+ - image: solid-color(79, 27, 0, 255, 8, 8)
+ bounds: 632 216 8 8
+ - image: solid-color(80, 27, 0, 255, 8, 8)
+ bounds: 640 216 8 8
+ - image: solid-color(81, 27, 0, 255, 8, 8)
+ bounds: 648 216 8 8
+ - image: solid-color(82, 27, 0, 255, 8, 8)
+ bounds: 656 216 8 8
+ - image: solid-color(83, 27, 0, 255, 8, 8)
+ bounds: 664 216 8 8
+ - image: solid-color(84, 27, 0, 255, 8, 8)
+ bounds: 672 216 8 8
+ - image: solid-color(85, 27, 0, 255, 8, 8)
+ bounds: 680 216 8 8
+ - image: solid-color(86, 27, 0, 255, 8, 8)
+ bounds: 688 216 8 8
+ - image: solid-color(87, 27, 0, 255, 8, 8)
+ bounds: 696 216 8 8
+ - image: solid-color(88, 27, 0, 255, 8, 8)
+ bounds: 704 216 8 8
+ - image: solid-color(89, 27, 0, 255, 8, 8)
+ bounds: 712 216 8 8
+ - image: solid-color(90, 27, 0, 255, 8, 8)
+ bounds: 720 216 8 8
+ - image: solid-color(91, 27, 0, 255, 8, 8)
+ bounds: 728 216 8 8
+ - image: solid-color(92, 27, 0, 255, 8, 8)
+ bounds: 736 216 8 8
+ - image: solid-color(93, 27, 0, 255, 8, 8)
+ bounds: 744 216 8 8
+ - image: solid-color(94, 27, 0, 255, 8, 8)
+ bounds: 752 216 8 8
+ - image: solid-color(95, 27, 0, 255, 8, 8)
+ bounds: 760 216 8 8
+ - image: solid-color(96, 27, 0, 255, 8, 8)
+ bounds: 768 216 8 8
+ - image: solid-color(97, 27, 0, 255, 8, 8)
+ bounds: 776 216 8 8
+ - image: solid-color(98, 27, 0, 255, 8, 8)
+ bounds: 784 216 8 8
+ - image: solid-color(99, 27, 0, 255, 8, 8)
+ bounds: 792 216 8 8
+ - image: solid-color(100, 27, 0, 255, 8, 8)
+ bounds: 800 216 8 8
+ - image: solid-color(101, 27, 0, 255, 8, 8)
+ bounds: 808 216 8 8
+ - image: solid-color(102, 27, 0, 255, 8, 8)
+ bounds: 816 216 8 8
+ - image: solid-color(103, 27, 0, 255, 8, 8)
+ bounds: 824 216 8 8
+ - image: solid-color(104, 27, 0, 255, 8, 8)
+ bounds: 832 216 8 8
+ - image: solid-color(105, 27, 0, 255, 8, 8)
+ bounds: 840 216 8 8
+ - image: solid-color(106, 27, 0, 255, 8, 8)
+ bounds: 848 216 8 8
+ - image: solid-color(107, 27, 0, 255, 8, 8)
+ bounds: 856 216 8 8
+ - image: solid-color(108, 27, 0, 255, 8, 8)
+ bounds: 864 216 8 8
+ - image: solid-color(109, 27, 0, 255, 8, 8)
+ bounds: 872 216 8 8
+ - image: solid-color(110, 27, 0, 255, 8, 8)
+ bounds: 880 216 8 8
+ - image: solid-color(111, 27, 0, 255, 8, 8)
+ bounds: 888 216 8 8
+ - image: solid-color(112, 27, 0, 255, 8, 8)
+ bounds: 896 216 8 8
+ - image: solid-color(113, 27, 0, 255, 8, 8)
+ bounds: 904 216 8 8
+ - image: solid-color(114, 27, 0, 255, 8, 8)
+ bounds: 912 216 8 8
+ - image: solid-color(115, 27, 0, 255, 8, 8)
+ bounds: 920 216 8 8
+ - image: solid-color(116, 27, 0, 255, 8, 8)
+ bounds: 928 216 8 8
+ - image: solid-color(117, 27, 0, 255, 8, 8)
+ bounds: 936 216 8 8
+ - image: solid-color(118, 27, 0, 255, 8, 8)
+ bounds: 944 216 8 8
+ - image: solid-color(119, 27, 0, 255, 8, 8)
+ bounds: 952 216 8 8
+ - image: solid-color(120, 27, 0, 255, 8, 8)
+ bounds: 960 216 8 8
+ - image: solid-color(121, 27, 0, 255, 8, 8)
+ bounds: 968 216 8 8
+ - image: solid-color(122, 27, 0, 255, 8, 8)
+ bounds: 976 216 8 8
+ - image: solid-color(123, 27, 0, 255, 8, 8)
+ bounds: 984 216 8 8
+ - image: solid-color(124, 27, 0, 255, 8, 8)
+ bounds: 992 216 8 8
+ - image: solid-color(125, 27, 0, 255, 8, 8)
+ bounds: 1000 216 8 8
+ - image: solid-color(126, 27, 0, 255, 8, 8)
+ bounds: 1008 216 8 8
+ - image: solid-color(127, 27, 0, 255, 8, 8)
+ bounds: 1016 216 8 8
+ - image: solid-color(0, 28, 0, 255, 8, 8)
+ bounds: 0 224 8 8
+ - image: solid-color(1, 28, 0, 255, 8, 8)
+ bounds: 8 224 8 8
+ - image: solid-color(2, 28, 0, 255, 8, 8)
+ bounds: 16 224 8 8
+ - image: solid-color(3, 28, 0, 255, 8, 8)
+ bounds: 24 224 8 8
+ - image: solid-color(4, 28, 0, 255, 8, 8)
+ bounds: 32 224 8 8
+ - image: solid-color(5, 28, 0, 255, 8, 8)
+ bounds: 40 224 8 8
+ - image: solid-color(6, 28, 0, 255, 8, 8)
+ bounds: 48 224 8 8
+ - image: solid-color(7, 28, 0, 255, 8, 8)
+ bounds: 56 224 8 8
+ - image: solid-color(8, 28, 0, 255, 8, 8)
+ bounds: 64 224 8 8
+ - image: solid-color(9, 28, 0, 255, 8, 8)
+ bounds: 72 224 8 8
+ - image: solid-color(10, 28, 0, 255, 8, 8)
+ bounds: 80 224 8 8
+ - image: solid-color(11, 28, 0, 255, 8, 8)
+ bounds: 88 224 8 8
+ - image: solid-color(12, 28, 0, 255, 8, 8)
+ bounds: 96 224 8 8
+ - image: solid-color(13, 28, 0, 255, 8, 8)
+ bounds: 104 224 8 8
+ - image: solid-color(14, 28, 0, 255, 8, 8)
+ bounds: 112 224 8 8
+ - image: solid-color(15, 28, 0, 255, 8, 8)
+ bounds: 120 224 8 8
+ - image: solid-color(16, 28, 0, 255, 8, 8)
+ bounds: 128 224 8 8
+ - image: solid-color(17, 28, 0, 255, 8, 8)
+ bounds: 136 224 8 8
+ - image: solid-color(18, 28, 0, 255, 8, 8)
+ bounds: 144 224 8 8
+ - image: solid-color(19, 28, 0, 255, 8, 8)
+ bounds: 152 224 8 8
+ - image: solid-color(20, 28, 0, 255, 8, 8)
+ bounds: 160 224 8 8
+ - image: solid-color(21, 28, 0, 255, 8, 8)
+ bounds: 168 224 8 8
+ - image: solid-color(22, 28, 0, 255, 8, 8)
+ bounds: 176 224 8 8
+ - image: solid-color(23, 28, 0, 255, 8, 8)
+ bounds: 184 224 8 8
+ - image: solid-color(24, 28, 0, 255, 8, 8)
+ bounds: 192 224 8 8
+ - image: solid-color(25, 28, 0, 255, 8, 8)
+ bounds: 200 224 8 8
+ - image: solid-color(26, 28, 0, 255, 8, 8)
+ bounds: 208 224 8 8
+ - image: solid-color(27, 28, 0, 255, 8, 8)
+ bounds: 216 224 8 8
+ - image: solid-color(28, 28, 0, 255, 8, 8)
+ bounds: 224 224 8 8
+ - image: solid-color(29, 28, 0, 255, 8, 8)
+ bounds: 232 224 8 8
+ - image: solid-color(30, 28, 0, 255, 8, 8)
+ bounds: 240 224 8 8
+ - image: solid-color(31, 28, 0, 255, 8, 8)
+ bounds: 248 224 8 8
+ - image: solid-color(32, 28, 0, 255, 8, 8)
+ bounds: 256 224 8 8
+ - image: solid-color(33, 28, 0, 255, 8, 8)
+ bounds: 264 224 8 8
+ - image: solid-color(34, 28, 0, 255, 8, 8)
+ bounds: 272 224 8 8
+ - image: solid-color(35, 28, 0, 255, 8, 8)
+ bounds: 280 224 8 8
+ - image: solid-color(36, 28, 0, 255, 8, 8)
+ bounds: 288 224 8 8
+ - image: solid-color(37, 28, 0, 255, 8, 8)
+ bounds: 296 224 8 8
+ - image: solid-color(38, 28, 0, 255, 8, 8)
+ bounds: 304 224 8 8
+ - image: solid-color(39, 28, 0, 255, 8, 8)
+ bounds: 312 224 8 8
+ - image: solid-color(40, 28, 0, 255, 8, 8)
+ bounds: 320 224 8 8
+ - image: solid-color(41, 28, 0, 255, 8, 8)
+ bounds: 328 224 8 8
+ - image: solid-color(42, 28, 0, 255, 8, 8)
+ bounds: 336 224 8 8
+ - image: solid-color(43, 28, 0, 255, 8, 8)
+ bounds: 344 224 8 8
+ - image: solid-color(44, 28, 0, 255, 8, 8)
+ bounds: 352 224 8 8
+ - image: solid-color(45, 28, 0, 255, 8, 8)
+ bounds: 360 224 8 8
+ - image: solid-color(46, 28, 0, 255, 8, 8)
+ bounds: 368 224 8 8
+ - image: solid-color(47, 28, 0, 255, 8, 8)
+ bounds: 376 224 8 8
+ - image: solid-color(48, 28, 0, 255, 8, 8)
+ bounds: 384 224 8 8
+ - image: solid-color(49, 28, 0, 255, 8, 8)
+ bounds: 392 224 8 8
+ - image: solid-color(50, 28, 0, 255, 8, 8)
+ bounds: 400 224 8 8
+ - image: solid-color(51, 28, 0, 255, 8, 8)
+ bounds: 408 224 8 8
+ - image: solid-color(52, 28, 0, 255, 8, 8)
+ bounds: 416 224 8 8
+ - image: solid-color(53, 28, 0, 255, 8, 8)
+ bounds: 424 224 8 8
+ - image: solid-color(54, 28, 0, 255, 8, 8)
+ bounds: 432 224 8 8
+ - image: solid-color(55, 28, 0, 255, 8, 8)
+ bounds: 440 224 8 8
+ - image: solid-color(56, 28, 0, 255, 8, 8)
+ bounds: 448 224 8 8
+ - image: solid-color(57, 28, 0, 255, 8, 8)
+ bounds: 456 224 8 8
+ - image: solid-color(58, 28, 0, 255, 8, 8)
+ bounds: 464 224 8 8
+ - image: solid-color(59, 28, 0, 255, 8, 8)
+ bounds: 472 224 8 8
+ - image: solid-color(60, 28, 0, 255, 8, 8)
+ bounds: 480 224 8 8
+ - image: solid-color(61, 28, 0, 255, 8, 8)
+ bounds: 488 224 8 8
+ - image: solid-color(62, 28, 0, 255, 8, 8)
+ bounds: 496 224 8 8
+ - image: solid-color(63, 28, 0, 255, 8, 8)
+ bounds: 504 224 8 8
+ - image: solid-color(64, 28, 0, 255, 8, 8)
+ bounds: 512 224 8 8
+ - image: solid-color(65, 28, 0, 255, 8, 8)
+ bounds: 520 224 8 8
+ - image: solid-color(66, 28, 0, 255, 8, 8)
+ bounds: 528 224 8 8
+ - image: solid-color(67, 28, 0, 255, 8, 8)
+ bounds: 536 224 8 8
+ - image: solid-color(68, 28, 0, 255, 8, 8)
+ bounds: 544 224 8 8
+ - image: solid-color(69, 28, 0, 255, 8, 8)
+ bounds: 552 224 8 8
+ - image: solid-color(70, 28, 0, 255, 8, 8)
+ bounds: 560 224 8 8
+ - image: solid-color(71, 28, 0, 255, 8, 8)
+ bounds: 568 224 8 8
+ - image: solid-color(72, 28, 0, 255, 8, 8)
+ bounds: 576 224 8 8
+ - image: solid-color(73, 28, 0, 255, 8, 8)
+ bounds: 584 224 8 8
+ - image: solid-color(74, 28, 0, 255, 8, 8)
+ bounds: 592 224 8 8
+ - image: solid-color(75, 28, 0, 255, 8, 8)
+ bounds: 600 224 8 8
+ - image: solid-color(76, 28, 0, 255, 8, 8)
+ bounds: 608 224 8 8
+ - image: solid-color(77, 28, 0, 255, 8, 8)
+ bounds: 616 224 8 8
+ - image: solid-color(78, 28, 0, 255, 8, 8)
+ bounds: 624 224 8 8
+ - image: solid-color(79, 28, 0, 255, 8, 8)
+ bounds: 632 224 8 8
+ - image: solid-color(80, 28, 0, 255, 8, 8)
+ bounds: 640 224 8 8
+ - image: solid-color(81, 28, 0, 255, 8, 8)
+ bounds: 648 224 8 8
+ - image: solid-color(82, 28, 0, 255, 8, 8)
+ bounds: 656 224 8 8
+ - image: solid-color(83, 28, 0, 255, 8, 8)
+ bounds: 664 224 8 8
+ - image: solid-color(84, 28, 0, 255, 8, 8)
+ bounds: 672 224 8 8
+ - image: solid-color(85, 28, 0, 255, 8, 8)
+ bounds: 680 224 8 8
+ - image: solid-color(86, 28, 0, 255, 8, 8)
+ bounds: 688 224 8 8
+ - image: solid-color(87, 28, 0, 255, 8, 8)
+ bounds: 696 224 8 8
+ - image: solid-color(88, 28, 0, 255, 8, 8)
+ bounds: 704 224 8 8
+ - image: solid-color(89, 28, 0, 255, 8, 8)
+ bounds: 712 224 8 8
+ - image: solid-color(90, 28, 0, 255, 8, 8)
+ bounds: 720 224 8 8
+ - image: solid-color(91, 28, 0, 255, 8, 8)
+ bounds: 728 224 8 8
+ - image: solid-color(92, 28, 0, 255, 8, 8)
+ bounds: 736 224 8 8
+ - image: solid-color(93, 28, 0, 255, 8, 8)
+ bounds: 744 224 8 8
+ - image: solid-color(94, 28, 0, 255, 8, 8)
+ bounds: 752 224 8 8
+ - image: solid-color(95, 28, 0, 255, 8, 8)
+ bounds: 760 224 8 8
+ - image: solid-color(96, 28, 0, 255, 8, 8)
+ bounds: 768 224 8 8
+ - image: solid-color(97, 28, 0, 255, 8, 8)
+ bounds: 776 224 8 8
+ - image: solid-color(98, 28, 0, 255, 8, 8)
+ bounds: 784 224 8 8
+ - image: solid-color(99, 28, 0, 255, 8, 8)
+ bounds: 792 224 8 8
+ - image: solid-color(100, 28, 0, 255, 8, 8)
+ bounds: 800 224 8 8
+ - image: solid-color(101, 28, 0, 255, 8, 8)
+ bounds: 808 224 8 8
+ - image: solid-color(102, 28, 0, 255, 8, 8)
+ bounds: 816 224 8 8
+ - image: solid-color(103, 28, 0, 255, 8, 8)
+ bounds: 824 224 8 8
+ - image: solid-color(104, 28, 0, 255, 8, 8)
+ bounds: 832 224 8 8
+ - image: solid-color(105, 28, 0, 255, 8, 8)
+ bounds: 840 224 8 8
+ - image: solid-color(106, 28, 0, 255, 8, 8)
+ bounds: 848 224 8 8
+ - image: solid-color(107, 28, 0, 255, 8, 8)
+ bounds: 856 224 8 8
+ - image: solid-color(108, 28, 0, 255, 8, 8)
+ bounds: 864 224 8 8
+ - image: solid-color(109, 28, 0, 255, 8, 8)
+ bounds: 872 224 8 8
+ - image: solid-color(110, 28, 0, 255, 8, 8)
+ bounds: 880 224 8 8
+ - image: solid-color(111, 28, 0, 255, 8, 8)
+ bounds: 888 224 8 8
+ - image: solid-color(112, 28, 0, 255, 8, 8)
+ bounds: 896 224 8 8
+ - image: solid-color(113, 28, 0, 255, 8, 8)
+ bounds: 904 224 8 8
+ - image: solid-color(114, 28, 0, 255, 8, 8)
+ bounds: 912 224 8 8
+ - image: solid-color(115, 28, 0, 255, 8, 8)
+ bounds: 920 224 8 8
+ - image: solid-color(116, 28, 0, 255, 8, 8)
+ bounds: 928 224 8 8
+ - image: solid-color(117, 28, 0, 255, 8, 8)
+ bounds: 936 224 8 8
+ - image: solid-color(118, 28, 0, 255, 8, 8)
+ bounds: 944 224 8 8
+ - image: solid-color(119, 28, 0, 255, 8, 8)
+ bounds: 952 224 8 8
+ - image: solid-color(120, 28, 0, 255, 8, 8)
+ bounds: 960 224 8 8
+ - image: solid-color(121, 28, 0, 255, 8, 8)
+ bounds: 968 224 8 8
+ - image: solid-color(122, 28, 0, 255, 8, 8)
+ bounds: 976 224 8 8
+ - image: solid-color(123, 28, 0, 255, 8, 8)
+ bounds: 984 224 8 8
+ - image: solid-color(124, 28, 0, 255, 8, 8)
+ bounds: 992 224 8 8
+ - image: solid-color(125, 28, 0, 255, 8, 8)
+ bounds: 1000 224 8 8
+ - image: solid-color(126, 28, 0, 255, 8, 8)
+ bounds: 1008 224 8 8
+ - image: solid-color(127, 28, 0, 255, 8, 8)
+ bounds: 1016 224 8 8
+ - image: solid-color(0, 29, 0, 255, 8, 8)
+ bounds: 0 232 8 8
+ - image: solid-color(1, 29, 0, 255, 8, 8)
+ bounds: 8 232 8 8
+ - image: solid-color(2, 29, 0, 255, 8, 8)
+ bounds: 16 232 8 8
+ - image: solid-color(3, 29, 0, 255, 8, 8)
+ bounds: 24 232 8 8
+ - image: solid-color(4, 29, 0, 255, 8, 8)
+ bounds: 32 232 8 8
+ - image: solid-color(5, 29, 0, 255, 8, 8)
+ bounds: 40 232 8 8
+ - image: solid-color(6, 29, 0, 255, 8, 8)
+ bounds: 48 232 8 8
+ - image: solid-color(7, 29, 0, 255, 8, 8)
+ bounds: 56 232 8 8
+ - image: solid-color(8, 29, 0, 255, 8, 8)
+ bounds: 64 232 8 8
+ - image: solid-color(9, 29, 0, 255, 8, 8)
+ bounds: 72 232 8 8
+ - image: solid-color(10, 29, 0, 255, 8, 8)
+ bounds: 80 232 8 8
+ - image: solid-color(11, 29, 0, 255, 8, 8)
+ bounds: 88 232 8 8
+ - image: solid-color(12, 29, 0, 255, 8, 8)
+ bounds: 96 232 8 8
+ - image: solid-color(13, 29, 0, 255, 8, 8)
+ bounds: 104 232 8 8
+ - image: solid-color(14, 29, 0, 255, 8, 8)
+ bounds: 112 232 8 8
+ - image: solid-color(15, 29, 0, 255, 8, 8)
+ bounds: 120 232 8 8
+ - image: solid-color(16, 29, 0, 255, 8, 8)
+ bounds: 128 232 8 8
+ - image: solid-color(17, 29, 0, 255, 8, 8)
+ bounds: 136 232 8 8
+ - image: solid-color(18, 29, 0, 255, 8, 8)
+ bounds: 144 232 8 8
+ - image: solid-color(19, 29, 0, 255, 8, 8)
+ bounds: 152 232 8 8
+ - image: solid-color(20, 29, 0, 255, 8, 8)
+ bounds: 160 232 8 8
+ - image: solid-color(21, 29, 0, 255, 8, 8)
+ bounds: 168 232 8 8
+ - image: solid-color(22, 29, 0, 255, 8, 8)
+ bounds: 176 232 8 8
+ - image: solid-color(23, 29, 0, 255, 8, 8)
+ bounds: 184 232 8 8
+ - image: solid-color(24, 29, 0, 255, 8, 8)
+ bounds: 192 232 8 8
+ - image: solid-color(25, 29, 0, 255, 8, 8)
+ bounds: 200 232 8 8
+ - image: solid-color(26, 29, 0, 255, 8, 8)
+ bounds: 208 232 8 8
+ - image: solid-color(27, 29, 0, 255, 8, 8)
+ bounds: 216 232 8 8
+ - image: solid-color(28, 29, 0, 255, 8, 8)
+ bounds: 224 232 8 8
+ - image: solid-color(29, 29, 0, 255, 8, 8)
+ bounds: 232 232 8 8
+ - image: solid-color(30, 29, 0, 255, 8, 8)
+ bounds: 240 232 8 8
+ - image: solid-color(31, 29, 0, 255, 8, 8)
+ bounds: 248 232 8 8
+ - image: solid-color(32, 29, 0, 255, 8, 8)
+ bounds: 256 232 8 8
+ - image: solid-color(33, 29, 0, 255, 8, 8)
+ bounds: 264 232 8 8
+ - image: solid-color(34, 29, 0, 255, 8, 8)
+ bounds: 272 232 8 8
+ - image: solid-color(35, 29, 0, 255, 8, 8)
+ bounds: 280 232 8 8
+ - image: solid-color(36, 29, 0, 255, 8, 8)
+ bounds: 288 232 8 8
+ - image: solid-color(37, 29, 0, 255, 8, 8)
+ bounds: 296 232 8 8
+ - image: solid-color(38, 29, 0, 255, 8, 8)
+ bounds: 304 232 8 8
+ - image: solid-color(39, 29, 0, 255, 8, 8)
+ bounds: 312 232 8 8
+ - image: solid-color(40, 29, 0, 255, 8, 8)
+ bounds: 320 232 8 8
+ - image: solid-color(41, 29, 0, 255, 8, 8)
+ bounds: 328 232 8 8
+ - image: solid-color(42, 29, 0, 255, 8, 8)
+ bounds: 336 232 8 8
+ - image: solid-color(43, 29, 0, 255, 8, 8)
+ bounds: 344 232 8 8
+ - image: solid-color(44, 29, 0, 255, 8, 8)
+ bounds: 352 232 8 8
+ - image: solid-color(45, 29, 0, 255, 8, 8)
+ bounds: 360 232 8 8
+ - image: solid-color(46, 29, 0, 255, 8, 8)
+ bounds: 368 232 8 8
+ - image: solid-color(47, 29, 0, 255, 8, 8)
+ bounds: 376 232 8 8
+ - image: solid-color(48, 29, 0, 255, 8, 8)
+ bounds: 384 232 8 8
+ - image: solid-color(49, 29, 0, 255, 8, 8)
+ bounds: 392 232 8 8
+ - image: solid-color(50, 29, 0, 255, 8, 8)
+ bounds: 400 232 8 8
+ - image: solid-color(51, 29, 0, 255, 8, 8)
+ bounds: 408 232 8 8
+ - image: solid-color(52, 29, 0, 255, 8, 8)
+ bounds: 416 232 8 8
+ - image: solid-color(53, 29, 0, 255, 8, 8)
+ bounds: 424 232 8 8
+ - image: solid-color(54, 29, 0, 255, 8, 8)
+ bounds: 432 232 8 8
+ - image: solid-color(55, 29, 0, 255, 8, 8)
+ bounds: 440 232 8 8
+ - image: solid-color(56, 29, 0, 255, 8, 8)
+ bounds: 448 232 8 8
+ - image: solid-color(57, 29, 0, 255, 8, 8)
+ bounds: 456 232 8 8
+ - image: solid-color(58, 29, 0, 255, 8, 8)
+ bounds: 464 232 8 8
+ - image: solid-color(59, 29, 0, 255, 8, 8)
+ bounds: 472 232 8 8
+ - image: solid-color(60, 29, 0, 255, 8, 8)
+ bounds: 480 232 8 8
+ - image: solid-color(61, 29, 0, 255, 8, 8)
+ bounds: 488 232 8 8
+ - image: solid-color(62, 29, 0, 255, 8, 8)
+ bounds: 496 232 8 8
+ - image: solid-color(63, 29, 0, 255, 8, 8)
+ bounds: 504 232 8 8
+ - image: solid-color(64, 29, 0, 255, 8, 8)
+ bounds: 512 232 8 8
+ - image: solid-color(65, 29, 0, 255, 8, 8)
+ bounds: 520 232 8 8
+ - image: solid-color(66, 29, 0, 255, 8, 8)
+ bounds: 528 232 8 8
+ - image: solid-color(67, 29, 0, 255, 8, 8)
+ bounds: 536 232 8 8
+ - image: solid-color(68, 29, 0, 255, 8, 8)
+ bounds: 544 232 8 8
+ - image: solid-color(69, 29, 0, 255, 8, 8)
+ bounds: 552 232 8 8
+ - image: solid-color(70, 29, 0, 255, 8, 8)
+ bounds: 560 232 8 8
+ - image: solid-color(71, 29, 0, 255, 8, 8)
+ bounds: 568 232 8 8
+ - image: solid-color(72, 29, 0, 255, 8, 8)
+ bounds: 576 232 8 8
+ - image: solid-color(73, 29, 0, 255, 8, 8)
+ bounds: 584 232 8 8
+ - image: solid-color(74, 29, 0, 255, 8, 8)
+ bounds: 592 232 8 8
+ - image: solid-color(75, 29, 0, 255, 8, 8)
+ bounds: 600 232 8 8
+ - image: solid-color(76, 29, 0, 255, 8, 8)
+ bounds: 608 232 8 8
+ - image: solid-color(77, 29, 0, 255, 8, 8)
+ bounds: 616 232 8 8
+ - image: solid-color(78, 29, 0, 255, 8, 8)
+ bounds: 624 232 8 8
+ - image: solid-color(79, 29, 0, 255, 8, 8)
+ bounds: 632 232 8 8
+ - image: solid-color(80, 29, 0, 255, 8, 8)
+ bounds: 640 232 8 8
+ - image: solid-color(81, 29, 0, 255, 8, 8)
+ bounds: 648 232 8 8
+ - image: solid-color(82, 29, 0, 255, 8, 8)
+ bounds: 656 232 8 8
+ - image: solid-color(83, 29, 0, 255, 8, 8)
+ bounds: 664 232 8 8
+ - image: solid-color(84, 29, 0, 255, 8, 8)
+ bounds: 672 232 8 8
+ - image: solid-color(85, 29, 0, 255, 8, 8)
+ bounds: 680 232 8 8
+ - image: solid-color(86, 29, 0, 255, 8, 8)
+ bounds: 688 232 8 8
+ - image: solid-color(87, 29, 0, 255, 8, 8)
+ bounds: 696 232 8 8
+ - image: solid-color(88, 29, 0, 255, 8, 8)
+ bounds: 704 232 8 8
+ - image: solid-color(89, 29, 0, 255, 8, 8)
+ bounds: 712 232 8 8
+ - image: solid-color(90, 29, 0, 255, 8, 8)
+ bounds: 720 232 8 8
+ - image: solid-color(91, 29, 0, 255, 8, 8)
+ bounds: 728 232 8 8
+ - image: solid-color(92, 29, 0, 255, 8, 8)
+ bounds: 736 232 8 8
+ - image: solid-color(93, 29, 0, 255, 8, 8)
+ bounds: 744 232 8 8
+ - image: solid-color(94, 29, 0, 255, 8, 8)
+ bounds: 752 232 8 8
+ - image: solid-color(95, 29, 0, 255, 8, 8)
+ bounds: 760 232 8 8
+ - image: solid-color(96, 29, 0, 255, 8, 8)
+ bounds: 768 232 8 8
+ - image: solid-color(97, 29, 0, 255, 8, 8)
+ bounds: 776 232 8 8
+ - image: solid-color(98, 29, 0, 255, 8, 8)
+ bounds: 784 232 8 8
+ - image: solid-color(99, 29, 0, 255, 8, 8)
+ bounds: 792 232 8 8
+ - image: solid-color(100, 29, 0, 255, 8, 8)
+ bounds: 800 232 8 8
+ - image: solid-color(101, 29, 0, 255, 8, 8)
+ bounds: 808 232 8 8
+ - image: solid-color(102, 29, 0, 255, 8, 8)
+ bounds: 816 232 8 8
+ - image: solid-color(103, 29, 0, 255, 8, 8)
+ bounds: 824 232 8 8
+ - image: solid-color(104, 29, 0, 255, 8, 8)
+ bounds: 832 232 8 8
+ - image: solid-color(105, 29, 0, 255, 8, 8)
+ bounds: 840 232 8 8
+ - image: solid-color(106, 29, 0, 255, 8, 8)
+ bounds: 848 232 8 8
+ - image: solid-color(107, 29, 0, 255, 8, 8)
+ bounds: 856 232 8 8
+ - image: solid-color(108, 29, 0, 255, 8, 8)
+ bounds: 864 232 8 8
+ - image: solid-color(109, 29, 0, 255, 8, 8)
+ bounds: 872 232 8 8
+ - image: solid-color(110, 29, 0, 255, 8, 8)
+ bounds: 880 232 8 8
+ - image: solid-color(111, 29, 0, 255, 8, 8)
+ bounds: 888 232 8 8
+ - image: solid-color(112, 29, 0, 255, 8, 8)
+ bounds: 896 232 8 8
+ - image: solid-color(113, 29, 0, 255, 8, 8)
+ bounds: 904 232 8 8
+ - image: solid-color(114, 29, 0, 255, 8, 8)
+ bounds: 912 232 8 8
+ - image: solid-color(115, 29, 0, 255, 8, 8)
+ bounds: 920 232 8 8
+ - image: solid-color(116, 29, 0, 255, 8, 8)
+ bounds: 928 232 8 8
+ - image: solid-color(117, 29, 0, 255, 8, 8)
+ bounds: 936 232 8 8
+ - image: solid-color(118, 29, 0, 255, 8, 8)
+ bounds: 944 232 8 8
+ - image: solid-color(119, 29, 0, 255, 8, 8)
+ bounds: 952 232 8 8
+ - image: solid-color(120, 29, 0, 255, 8, 8)
+ bounds: 960 232 8 8
+ - image: solid-color(121, 29, 0, 255, 8, 8)
+ bounds: 968 232 8 8
+ - image: solid-color(122, 29, 0, 255, 8, 8)
+ bounds: 976 232 8 8
+ - image: solid-color(123, 29, 0, 255, 8, 8)
+ bounds: 984 232 8 8
+ - image: solid-color(124, 29, 0, 255, 8, 8)
+ bounds: 992 232 8 8
+ - image: solid-color(125, 29, 0, 255, 8, 8)
+ bounds: 1000 232 8 8
+ - image: solid-color(126, 29, 0, 255, 8, 8)
+ bounds: 1008 232 8 8
+ - image: solid-color(127, 29, 0, 255, 8, 8)
+ bounds: 1016 232 8 8
+ - image: solid-color(0, 30, 0, 255, 8, 8)
+ bounds: 0 240 8 8
+ - image: solid-color(1, 30, 0, 255, 8, 8)
+ bounds: 8 240 8 8
+ - image: solid-color(2, 30, 0, 255, 8, 8)
+ bounds: 16 240 8 8
+ - image: solid-color(3, 30, 0, 255, 8, 8)
+ bounds: 24 240 8 8
+ - image: solid-color(4, 30, 0, 255, 8, 8)
+ bounds: 32 240 8 8
+ - image: solid-color(5, 30, 0, 255, 8, 8)
+ bounds: 40 240 8 8
+ - image: solid-color(6, 30, 0, 255, 8, 8)
+ bounds: 48 240 8 8
+ - image: solid-color(7, 30, 0, 255, 8, 8)
+ bounds: 56 240 8 8
+ - image: solid-color(8, 30, 0, 255, 8, 8)
+ bounds: 64 240 8 8
+ - image: solid-color(9, 30, 0, 255, 8, 8)
+ bounds: 72 240 8 8
+ - image: solid-color(10, 30, 0, 255, 8, 8)
+ bounds: 80 240 8 8
+ - image: solid-color(11, 30, 0, 255, 8, 8)
+ bounds: 88 240 8 8
+ - image: solid-color(12, 30, 0, 255, 8, 8)
+ bounds: 96 240 8 8
+ - image: solid-color(13, 30, 0, 255, 8, 8)
+ bounds: 104 240 8 8
+ - image: solid-color(14, 30, 0, 255, 8, 8)
+ bounds: 112 240 8 8
+ - image: solid-color(15, 30, 0, 255, 8, 8)
+ bounds: 120 240 8 8
+ - image: solid-color(16, 30, 0, 255, 8, 8)
+ bounds: 128 240 8 8
+ - image: solid-color(17, 30, 0, 255, 8, 8)
+ bounds: 136 240 8 8
+ - image: solid-color(18, 30, 0, 255, 8, 8)
+ bounds: 144 240 8 8
+ - image: solid-color(19, 30, 0, 255, 8, 8)
+ bounds: 152 240 8 8
+ - image: solid-color(20, 30, 0, 255, 8, 8)
+ bounds: 160 240 8 8
+ - image: solid-color(21, 30, 0, 255, 8, 8)
+ bounds: 168 240 8 8
+ - image: solid-color(22, 30, 0, 255, 8, 8)
+ bounds: 176 240 8 8
+ - image: solid-color(23, 30, 0, 255, 8, 8)
+ bounds: 184 240 8 8
+ - image: solid-color(24, 30, 0, 255, 8, 8)
+ bounds: 192 240 8 8
+ - image: solid-color(25, 30, 0, 255, 8, 8)
+ bounds: 200 240 8 8
+ - image: solid-color(26, 30, 0, 255, 8, 8)
+ bounds: 208 240 8 8
+ - image: solid-color(27, 30, 0, 255, 8, 8)
+ bounds: 216 240 8 8
+ - image: solid-color(28, 30, 0, 255, 8, 8)
+ bounds: 224 240 8 8
+ - image: solid-color(29, 30, 0, 255, 8, 8)
+ bounds: 232 240 8 8
+ - image: solid-color(30, 30, 0, 255, 8, 8)
+ bounds: 240 240 8 8
+ - image: solid-color(31, 30, 0, 255, 8, 8)
+ bounds: 248 240 8 8
+ - image: solid-color(32, 30, 0, 255, 8, 8)
+ bounds: 256 240 8 8
+ - image: solid-color(33, 30, 0, 255, 8, 8)
+ bounds: 264 240 8 8
+ - image: solid-color(34, 30, 0, 255, 8, 8)
+ bounds: 272 240 8 8
+ - image: solid-color(35, 30, 0, 255, 8, 8)
+ bounds: 280 240 8 8
+ - image: solid-color(36, 30, 0, 255, 8, 8)
+ bounds: 288 240 8 8
+ - image: solid-color(37, 30, 0, 255, 8, 8)
+ bounds: 296 240 8 8
+ - image: solid-color(38, 30, 0, 255, 8, 8)
+ bounds: 304 240 8 8
+ - image: solid-color(39, 30, 0, 255, 8, 8)
+ bounds: 312 240 8 8
+ - image: solid-color(40, 30, 0, 255, 8, 8)
+ bounds: 320 240 8 8
+ - image: solid-color(41, 30, 0, 255, 8, 8)
+ bounds: 328 240 8 8
+ - image: solid-color(42, 30, 0, 255, 8, 8)
+ bounds: 336 240 8 8
+ - image: solid-color(43, 30, 0, 255, 8, 8)
+ bounds: 344 240 8 8
+ - image: solid-color(44, 30, 0, 255, 8, 8)
+ bounds: 352 240 8 8
+ - image: solid-color(45, 30, 0, 255, 8, 8)
+ bounds: 360 240 8 8
+ - image: solid-color(46, 30, 0, 255, 8, 8)
+ bounds: 368 240 8 8
+ - image: solid-color(47, 30, 0, 255, 8, 8)
+ bounds: 376 240 8 8
+ - image: solid-color(48, 30, 0, 255, 8, 8)
+ bounds: 384 240 8 8
+ - image: solid-color(49, 30, 0, 255, 8, 8)
+ bounds: 392 240 8 8
+ - image: solid-color(50, 30, 0, 255, 8, 8)
+ bounds: 400 240 8 8
+ - image: solid-color(51, 30, 0, 255, 8, 8)
+ bounds: 408 240 8 8
+ - image: solid-color(52, 30, 0, 255, 8, 8)
+ bounds: 416 240 8 8
+ - image: solid-color(53, 30, 0, 255, 8, 8)
+ bounds: 424 240 8 8
+ - image: solid-color(54, 30, 0, 255, 8, 8)
+ bounds: 432 240 8 8
+ - image: solid-color(55, 30, 0, 255, 8, 8)
+ bounds: 440 240 8 8
+ - image: solid-color(56, 30, 0, 255, 8, 8)
+ bounds: 448 240 8 8
+ - image: solid-color(57, 30, 0, 255, 8, 8)
+ bounds: 456 240 8 8
+ - image: solid-color(58, 30, 0, 255, 8, 8)
+ bounds: 464 240 8 8
+ - image: solid-color(59, 30, 0, 255, 8, 8)
+ bounds: 472 240 8 8
+ - image: solid-color(60, 30, 0, 255, 8, 8)
+ bounds: 480 240 8 8
+ - image: solid-color(61, 30, 0, 255, 8, 8)
+ bounds: 488 240 8 8
+ - image: solid-color(62, 30, 0, 255, 8, 8)
+ bounds: 496 240 8 8
+ - image: solid-color(63, 30, 0, 255, 8, 8)
+ bounds: 504 240 8 8
+ - image: solid-color(64, 30, 0, 255, 8, 8)
+ bounds: 512 240 8 8
+ - image: solid-color(65, 30, 0, 255, 8, 8)
+ bounds: 520 240 8 8
+ - image: solid-color(66, 30, 0, 255, 8, 8)
+ bounds: 528 240 8 8
+ - image: solid-color(67, 30, 0, 255, 8, 8)
+ bounds: 536 240 8 8
+ - image: solid-color(68, 30, 0, 255, 8, 8)
+ bounds: 544 240 8 8
+ - image: solid-color(69, 30, 0, 255, 8, 8)
+ bounds: 552 240 8 8
+ - image: solid-color(70, 30, 0, 255, 8, 8)
+ bounds: 560 240 8 8
+ - image: solid-color(71, 30, 0, 255, 8, 8)
+ bounds: 568 240 8 8
+ - image: solid-color(72, 30, 0, 255, 8, 8)
+ bounds: 576 240 8 8
+ - image: solid-color(73, 30, 0, 255, 8, 8)
+ bounds: 584 240 8 8
+ - image: solid-color(74, 30, 0, 255, 8, 8)
+ bounds: 592 240 8 8
+ - image: solid-color(75, 30, 0, 255, 8, 8)
+ bounds: 600 240 8 8
+ - image: solid-color(76, 30, 0, 255, 8, 8)
+ bounds: 608 240 8 8
+ - image: solid-color(77, 30, 0, 255, 8, 8)
+ bounds: 616 240 8 8
+ - image: solid-color(78, 30, 0, 255, 8, 8)
+ bounds: 624 240 8 8
+ - image: solid-color(79, 30, 0, 255, 8, 8)
+ bounds: 632 240 8 8
+ - image: solid-color(80, 30, 0, 255, 8, 8)
+ bounds: 640 240 8 8
+ - image: solid-color(81, 30, 0, 255, 8, 8)
+ bounds: 648 240 8 8
+ - image: solid-color(82, 30, 0, 255, 8, 8)
+ bounds: 656 240 8 8
+ - image: solid-color(83, 30, 0, 255, 8, 8)
+ bounds: 664 240 8 8
+ - image: solid-color(84, 30, 0, 255, 8, 8)
+ bounds: 672 240 8 8
+ - image: solid-color(85, 30, 0, 255, 8, 8)
+ bounds: 680 240 8 8
+ - image: solid-color(86, 30, 0, 255, 8, 8)
+ bounds: 688 240 8 8
+ - image: solid-color(87, 30, 0, 255, 8, 8)
+ bounds: 696 240 8 8
+ - image: solid-color(88, 30, 0, 255, 8, 8)
+ bounds: 704 240 8 8
+ - image: solid-color(89, 30, 0, 255, 8, 8)
+ bounds: 712 240 8 8
+ - image: solid-color(90, 30, 0, 255, 8, 8)
+ bounds: 720 240 8 8
+ - image: solid-color(91, 30, 0, 255, 8, 8)
+ bounds: 728 240 8 8
+ - image: solid-color(92, 30, 0, 255, 8, 8)
+ bounds: 736 240 8 8
+ - image: solid-color(93, 30, 0, 255, 8, 8)
+ bounds: 744 240 8 8
+ - image: solid-color(94, 30, 0, 255, 8, 8)
+ bounds: 752 240 8 8
+ - image: solid-color(95, 30, 0, 255, 8, 8)
+ bounds: 760 240 8 8
+ - image: solid-color(96, 30, 0, 255, 8, 8)
+ bounds: 768 240 8 8
+ - image: solid-color(97, 30, 0, 255, 8, 8)
+ bounds: 776 240 8 8
+ - image: solid-color(98, 30, 0, 255, 8, 8)
+ bounds: 784 240 8 8
+ - image: solid-color(99, 30, 0, 255, 8, 8)
+ bounds: 792 240 8 8
+ - image: solid-color(100, 30, 0, 255, 8, 8)
+ bounds: 800 240 8 8
+ - image: solid-color(101, 30, 0, 255, 8, 8)
+ bounds: 808 240 8 8
+ - image: solid-color(102, 30, 0, 255, 8, 8)
+ bounds: 816 240 8 8
+ - image: solid-color(103, 30, 0, 255, 8, 8)
+ bounds: 824 240 8 8
+ - image: solid-color(104, 30, 0, 255, 8, 8)
+ bounds: 832 240 8 8
+ - image: solid-color(105, 30, 0, 255, 8, 8)
+ bounds: 840 240 8 8
+ - image: solid-color(106, 30, 0, 255, 8, 8)
+ bounds: 848 240 8 8
+ - image: solid-color(107, 30, 0, 255, 8, 8)
+ bounds: 856 240 8 8
+ - image: solid-color(108, 30, 0, 255, 8, 8)
+ bounds: 864 240 8 8
+ - image: solid-color(109, 30, 0, 255, 8, 8)
+ bounds: 872 240 8 8
+ - image: solid-color(110, 30, 0, 255, 8, 8)
+ bounds: 880 240 8 8
+ - image: solid-color(111, 30, 0, 255, 8, 8)
+ bounds: 888 240 8 8
+ - image: solid-color(112, 30, 0, 255, 8, 8)
+ bounds: 896 240 8 8
+ - image: solid-color(113, 30, 0, 255, 8, 8)
+ bounds: 904 240 8 8
+ - image: solid-color(114, 30, 0, 255, 8, 8)
+ bounds: 912 240 8 8
+ - image: solid-color(115, 30, 0, 255, 8, 8)
+ bounds: 920 240 8 8
+ - image: solid-color(116, 30, 0, 255, 8, 8)
+ bounds: 928 240 8 8
+ - image: solid-color(117, 30, 0, 255, 8, 8)
+ bounds: 936 240 8 8
+ - image: solid-color(118, 30, 0, 255, 8, 8)
+ bounds: 944 240 8 8
+ - image: solid-color(119, 30, 0, 255, 8, 8)
+ bounds: 952 240 8 8
+ - image: solid-color(120, 30, 0, 255, 8, 8)
+ bounds: 960 240 8 8
+ - image: solid-color(121, 30, 0, 255, 8, 8)
+ bounds: 968 240 8 8
+ - image: solid-color(122, 30, 0, 255, 8, 8)
+ bounds: 976 240 8 8
+ - image: solid-color(123, 30, 0, 255, 8, 8)
+ bounds: 984 240 8 8
+ - image: solid-color(124, 30, 0, 255, 8, 8)
+ bounds: 992 240 8 8
+ - image: solid-color(125, 30, 0, 255, 8, 8)
+ bounds: 1000 240 8 8
+ - image: solid-color(126, 30, 0, 255, 8, 8)
+ bounds: 1008 240 8 8
+ - image: solid-color(127, 30, 0, 255, 8, 8)
+ bounds: 1016 240 8 8
+ - image: solid-color(0, 31, 0, 255, 8, 8)
+ bounds: 0 248 8 8
+ - image: solid-color(1, 31, 0, 255, 8, 8)
+ bounds: 8 248 8 8
+ - image: solid-color(2, 31, 0, 255, 8, 8)
+ bounds: 16 248 8 8
+ - image: solid-color(3, 31, 0, 255, 8, 8)
+ bounds: 24 248 8 8
+ - image: solid-color(4, 31, 0, 255, 8, 8)
+ bounds: 32 248 8 8
+ - image: solid-color(5, 31, 0, 255, 8, 8)
+ bounds: 40 248 8 8
+ - image: solid-color(6, 31, 0, 255, 8, 8)
+ bounds: 48 248 8 8
+ - image: solid-color(7, 31, 0, 255, 8, 8)
+ bounds: 56 248 8 8
+ - image: solid-color(8, 31, 0, 255, 8, 8)
+ bounds: 64 248 8 8
+ - image: solid-color(9, 31, 0, 255, 8, 8)
+ bounds: 72 248 8 8
+ - image: solid-color(10, 31, 0, 255, 8, 8)
+ bounds: 80 248 8 8
+ - image: solid-color(11, 31, 0, 255, 8, 8)
+ bounds: 88 248 8 8
+ - image: solid-color(12, 31, 0, 255, 8, 8)
+ bounds: 96 248 8 8
+ - image: solid-color(13, 31, 0, 255, 8, 8)
+ bounds: 104 248 8 8
+ - image: solid-color(14, 31, 0, 255, 8, 8)
+ bounds: 112 248 8 8
+ - image: solid-color(15, 31, 0, 255, 8, 8)
+ bounds: 120 248 8 8
+ - image: solid-color(16, 31, 0, 255, 8, 8)
+ bounds: 128 248 8 8
+ - image: solid-color(17, 31, 0, 255, 8, 8)
+ bounds: 136 248 8 8
+ - image: solid-color(18, 31, 0, 255, 8, 8)
+ bounds: 144 248 8 8
+ - image: solid-color(19, 31, 0, 255, 8, 8)
+ bounds: 152 248 8 8
+ - image: solid-color(20, 31, 0, 255, 8, 8)
+ bounds: 160 248 8 8
+ - image: solid-color(21, 31, 0, 255, 8, 8)
+ bounds: 168 248 8 8
+ - image: solid-color(22, 31, 0, 255, 8, 8)
+ bounds: 176 248 8 8
+ - image: solid-color(23, 31, 0, 255, 8, 8)
+ bounds: 184 248 8 8
+ - image: solid-color(24, 31, 0, 255, 8, 8)
+ bounds: 192 248 8 8
+ - image: solid-color(25, 31, 0, 255, 8, 8)
+ bounds: 200 248 8 8
+ - image: solid-color(26, 31, 0, 255, 8, 8)
+ bounds: 208 248 8 8
+ - image: solid-color(27, 31, 0, 255, 8, 8)
+ bounds: 216 248 8 8
+ - image: solid-color(28, 31, 0, 255, 8, 8)
+ bounds: 224 248 8 8
+ - image: solid-color(29, 31, 0, 255, 8, 8)
+ bounds: 232 248 8 8
+ - image: solid-color(30, 31, 0, 255, 8, 8)
+ bounds: 240 248 8 8
+ - image: solid-color(31, 31, 0, 255, 8, 8)
+ bounds: 248 248 8 8
+ - image: solid-color(32, 31, 0, 255, 8, 8)
+ bounds: 256 248 8 8
+ - image: solid-color(33, 31, 0, 255, 8, 8)
+ bounds: 264 248 8 8
+ - image: solid-color(34, 31, 0, 255, 8, 8)
+ bounds: 272 248 8 8
+ - image: solid-color(35, 31, 0, 255, 8, 8)
+ bounds: 280 248 8 8
+ - image: solid-color(36, 31, 0, 255, 8, 8)
+ bounds: 288 248 8 8
+ - image: solid-color(37, 31, 0, 255, 8, 8)
+ bounds: 296 248 8 8
+ - image: solid-color(38, 31, 0, 255, 8, 8)
+ bounds: 304 248 8 8
+ - image: solid-color(39, 31, 0, 255, 8, 8)
+ bounds: 312 248 8 8
+ - image: solid-color(40, 31, 0, 255, 8, 8)
+ bounds: 320 248 8 8
+ - image: solid-color(41, 31, 0, 255, 8, 8)
+ bounds: 328 248 8 8
+ - image: solid-color(42, 31, 0, 255, 8, 8)
+ bounds: 336 248 8 8
+ - image: solid-color(43, 31, 0, 255, 8, 8)
+ bounds: 344 248 8 8
+ - image: solid-color(44, 31, 0, 255, 8, 8)
+ bounds: 352 248 8 8
+ - image: solid-color(45, 31, 0, 255, 8, 8)
+ bounds: 360 248 8 8
+ - image: solid-color(46, 31, 0, 255, 8, 8)
+ bounds: 368 248 8 8
+ - image: solid-color(47, 31, 0, 255, 8, 8)
+ bounds: 376 248 8 8
+ - image: solid-color(48, 31, 0, 255, 8, 8)
+ bounds: 384 248 8 8
+ - image: solid-color(49, 31, 0, 255, 8, 8)
+ bounds: 392 248 8 8
+ - image: solid-color(50, 31, 0, 255, 8, 8)
+ bounds: 400 248 8 8
+ - image: solid-color(51, 31, 0, 255, 8, 8)
+ bounds: 408 248 8 8
+ - image: solid-color(52, 31, 0, 255, 8, 8)
+ bounds: 416 248 8 8
+ - image: solid-color(53, 31, 0, 255, 8, 8)
+ bounds: 424 248 8 8
+ - image: solid-color(54, 31, 0, 255, 8, 8)
+ bounds: 432 248 8 8
+ - image: solid-color(55, 31, 0, 255, 8, 8)
+ bounds: 440 248 8 8
+ - image: solid-color(56, 31, 0, 255, 8, 8)
+ bounds: 448 248 8 8
+ - image: solid-color(57, 31, 0, 255, 8, 8)
+ bounds: 456 248 8 8
+ - image: solid-color(58, 31, 0, 255, 8, 8)
+ bounds: 464 248 8 8
+ - image: solid-color(59, 31, 0, 255, 8, 8)
+ bounds: 472 248 8 8
+ - image: solid-color(60, 31, 0, 255, 8, 8)
+ bounds: 480 248 8 8
+ - image: solid-color(61, 31, 0, 255, 8, 8)
+ bounds: 488 248 8 8
+ - image: solid-color(62, 31, 0, 255, 8, 8)
+ bounds: 496 248 8 8
+ - image: solid-color(63, 31, 0, 255, 8, 8)
+ bounds: 504 248 8 8
+ - image: solid-color(64, 31, 0, 255, 8, 8)
+ bounds: 512 248 8 8
+ - image: solid-color(65, 31, 0, 255, 8, 8)
+ bounds: 520 248 8 8
+ - image: solid-color(66, 31, 0, 255, 8, 8)
+ bounds: 528 248 8 8
+ - image: solid-color(67, 31, 0, 255, 8, 8)
+ bounds: 536 248 8 8
+ - image: solid-color(68, 31, 0, 255, 8, 8)
+ bounds: 544 248 8 8
+ - image: solid-color(69, 31, 0, 255, 8, 8)
+ bounds: 552 248 8 8
+ - image: solid-color(70, 31, 0, 255, 8, 8)
+ bounds: 560 248 8 8
+ - image: solid-color(71, 31, 0, 255, 8, 8)
+ bounds: 568 248 8 8
+ - image: solid-color(72, 31, 0, 255, 8, 8)
+ bounds: 576 248 8 8
+ - image: solid-color(73, 31, 0, 255, 8, 8)
+ bounds: 584 248 8 8
+ - image: solid-color(74, 31, 0, 255, 8, 8)
+ bounds: 592 248 8 8
+ - image: solid-color(75, 31, 0, 255, 8, 8)
+ bounds: 600 248 8 8
+ - image: solid-color(76, 31, 0, 255, 8, 8)
+ bounds: 608 248 8 8
+ - image: solid-color(77, 31, 0, 255, 8, 8)
+ bounds: 616 248 8 8
+ - image: solid-color(78, 31, 0, 255, 8, 8)
+ bounds: 624 248 8 8
+ - image: solid-color(79, 31, 0, 255, 8, 8)
+ bounds: 632 248 8 8
+ - image: solid-color(80, 31, 0, 255, 8, 8)
+ bounds: 640 248 8 8
+ - image: solid-color(81, 31, 0, 255, 8, 8)
+ bounds: 648 248 8 8
+ - image: solid-color(82, 31, 0, 255, 8, 8)
+ bounds: 656 248 8 8
+ - image: solid-color(83, 31, 0, 255, 8, 8)
+ bounds: 664 248 8 8
+ - image: solid-color(84, 31, 0, 255, 8, 8)
+ bounds: 672 248 8 8
+ - image: solid-color(85, 31, 0, 255, 8, 8)
+ bounds: 680 248 8 8
+ - image: solid-color(86, 31, 0, 255, 8, 8)
+ bounds: 688 248 8 8
+ - image: solid-color(87, 31, 0, 255, 8, 8)
+ bounds: 696 248 8 8
+ - image: solid-color(88, 31, 0, 255, 8, 8)
+ bounds: 704 248 8 8
+ - image: solid-color(89, 31, 0, 255, 8, 8)
+ bounds: 712 248 8 8
+ - image: solid-color(90, 31, 0, 255, 8, 8)
+ bounds: 720 248 8 8
+ - image: solid-color(91, 31, 0, 255, 8, 8)
+ bounds: 728 248 8 8
+ - image: solid-color(92, 31, 0, 255, 8, 8)
+ bounds: 736 248 8 8
+ - image: solid-color(93, 31, 0, 255, 8, 8)
+ bounds: 744 248 8 8
+ - image: solid-color(94, 31, 0, 255, 8, 8)
+ bounds: 752 248 8 8
+ - image: solid-color(95, 31, 0, 255, 8, 8)
+ bounds: 760 248 8 8
+ - image: solid-color(96, 31, 0, 255, 8, 8)
+ bounds: 768 248 8 8
+ - image: solid-color(97, 31, 0, 255, 8, 8)
+ bounds: 776 248 8 8
+ - image: solid-color(98, 31, 0, 255, 8, 8)
+ bounds: 784 248 8 8
+ - image: solid-color(99, 31, 0, 255, 8, 8)
+ bounds: 792 248 8 8
+ - image: solid-color(100, 31, 0, 255, 8, 8)
+ bounds: 800 248 8 8
+ - image: solid-color(101, 31, 0, 255, 8, 8)
+ bounds: 808 248 8 8
+ - image: solid-color(102, 31, 0, 255, 8, 8)
+ bounds: 816 248 8 8
+ - image: solid-color(103, 31, 0, 255, 8, 8)
+ bounds: 824 248 8 8
+ - image: solid-color(104, 31, 0, 255, 8, 8)
+ bounds: 832 248 8 8
+ - image: solid-color(105, 31, 0, 255, 8, 8)
+ bounds: 840 248 8 8
+ - image: solid-color(106, 31, 0, 255, 8, 8)
+ bounds: 848 248 8 8
+ - image: solid-color(107, 31, 0, 255, 8, 8)
+ bounds: 856 248 8 8
+ - image: solid-color(108, 31, 0, 255, 8, 8)
+ bounds: 864 248 8 8
+ - image: solid-color(109, 31, 0, 255, 8, 8)
+ bounds: 872 248 8 8
+ - image: solid-color(110, 31, 0, 255, 8, 8)
+ bounds: 880 248 8 8
+ - image: solid-color(111, 31, 0, 255, 8, 8)
+ bounds: 888 248 8 8
+ - image: solid-color(112, 31, 0, 255, 8, 8)
+ bounds: 896 248 8 8
+ - image: solid-color(113, 31, 0, 255, 8, 8)
+ bounds: 904 248 8 8
+ - image: solid-color(114, 31, 0, 255, 8, 8)
+ bounds: 912 248 8 8
+ - image: solid-color(115, 31, 0, 255, 8, 8)
+ bounds: 920 248 8 8
+ - image: solid-color(116, 31, 0, 255, 8, 8)
+ bounds: 928 248 8 8
+ - image: solid-color(117, 31, 0, 255, 8, 8)
+ bounds: 936 248 8 8
+ - image: solid-color(118, 31, 0, 255, 8, 8)
+ bounds: 944 248 8 8
+ - image: solid-color(119, 31, 0, 255, 8, 8)
+ bounds: 952 248 8 8
+ - image: solid-color(120, 31, 0, 255, 8, 8)
+ bounds: 960 248 8 8
+ - image: solid-color(121, 31, 0, 255, 8, 8)
+ bounds: 968 248 8 8
+ - image: solid-color(122, 31, 0, 255, 8, 8)
+ bounds: 976 248 8 8
+ - image: solid-color(123, 31, 0, 255, 8, 8)
+ bounds: 984 248 8 8
+ - image: solid-color(124, 31, 0, 255, 8, 8)
+ bounds: 992 248 8 8
+ - image: solid-color(125, 31, 0, 255, 8, 8)
+ bounds: 1000 248 8 8
+ - image: solid-color(126, 31, 0, 255, 8, 8)
+ bounds: 1008 248 8 8
+ - image: solid-color(127, 31, 0, 255, 8, 8)
+ bounds: 1016 248 8 8
+ - image: solid-color(0, 32, 0, 255, 8, 8)
+ bounds: 0 256 8 8
+ - image: solid-color(1, 32, 0, 255, 8, 8)
+ bounds: 8 256 8 8
+ - image: solid-color(2, 32, 0, 255, 8, 8)
+ bounds: 16 256 8 8
+ - image: solid-color(3, 32, 0, 255, 8, 8)
+ bounds: 24 256 8 8
+ - image: solid-color(4, 32, 0, 255, 8, 8)
+ bounds: 32 256 8 8
+ - image: solid-color(5, 32, 0, 255, 8, 8)
+ bounds: 40 256 8 8
+ - image: solid-color(6, 32, 0, 255, 8, 8)
+ bounds: 48 256 8 8
+ - image: solid-color(7, 32, 0, 255, 8, 8)
+ bounds: 56 256 8 8
+ - image: solid-color(8, 32, 0, 255, 8, 8)
+ bounds: 64 256 8 8
+ - image: solid-color(9, 32, 0, 255, 8, 8)
+ bounds: 72 256 8 8
+ - image: solid-color(10, 32, 0, 255, 8, 8)
+ bounds: 80 256 8 8
+ - image: solid-color(11, 32, 0, 255, 8, 8)
+ bounds: 88 256 8 8
+ - image: solid-color(12, 32, 0, 255, 8, 8)
+ bounds: 96 256 8 8
+ - image: solid-color(13, 32, 0, 255, 8, 8)
+ bounds: 104 256 8 8
+ - image: solid-color(14, 32, 0, 255, 8, 8)
+ bounds: 112 256 8 8
+ - image: solid-color(15, 32, 0, 255, 8, 8)
+ bounds: 120 256 8 8
+ - image: solid-color(16, 32, 0, 255, 8, 8)
+ bounds: 128 256 8 8
+ - image: solid-color(17, 32, 0, 255, 8, 8)
+ bounds: 136 256 8 8
+ - image: solid-color(18, 32, 0, 255, 8, 8)
+ bounds: 144 256 8 8
+ - image: solid-color(19, 32, 0, 255, 8, 8)
+ bounds: 152 256 8 8
+ - image: solid-color(20, 32, 0, 255, 8, 8)
+ bounds: 160 256 8 8
+ - image: solid-color(21, 32, 0, 255, 8, 8)
+ bounds: 168 256 8 8
+ - image: solid-color(22, 32, 0, 255, 8, 8)
+ bounds: 176 256 8 8
+ - image: solid-color(23, 32, 0, 255, 8, 8)
+ bounds: 184 256 8 8
+ - image: solid-color(24, 32, 0, 255, 8, 8)
+ bounds: 192 256 8 8
+ - image: solid-color(25, 32, 0, 255, 8, 8)
+ bounds: 200 256 8 8
+ - image: solid-color(26, 32, 0, 255, 8, 8)
+ bounds: 208 256 8 8
+ - image: solid-color(27, 32, 0, 255, 8, 8)
+ bounds: 216 256 8 8
+ - image: solid-color(28, 32, 0, 255, 8, 8)
+ bounds: 224 256 8 8
+ - image: solid-color(29, 32, 0, 255, 8, 8)
+ bounds: 232 256 8 8
+ - image: solid-color(30, 32, 0, 255, 8, 8)
+ bounds: 240 256 8 8
+ - image: solid-color(31, 32, 0, 255, 8, 8)
+ bounds: 248 256 8 8
+ - image: solid-color(32, 32, 0, 255, 8, 8)
+ bounds: 256 256 8 8
+ - image: solid-color(33, 32, 0, 255, 8, 8)
+ bounds: 264 256 8 8
+ - image: solid-color(34, 32, 0, 255, 8, 8)
+ bounds: 272 256 8 8
+ - image: solid-color(35, 32, 0, 255, 8, 8)
+ bounds: 280 256 8 8
+ - image: solid-color(36, 32, 0, 255, 8, 8)
+ bounds: 288 256 8 8
+ - image: solid-color(37, 32, 0, 255, 8, 8)
+ bounds: 296 256 8 8
+ - image: solid-color(38, 32, 0, 255, 8, 8)
+ bounds: 304 256 8 8
+ - image: solid-color(39, 32, 0, 255, 8, 8)
+ bounds: 312 256 8 8
+ - image: solid-color(40, 32, 0, 255, 8, 8)
+ bounds: 320 256 8 8
+ - image: solid-color(41, 32, 0, 255, 8, 8)
+ bounds: 328 256 8 8
+ - image: solid-color(42, 32, 0, 255, 8, 8)
+ bounds: 336 256 8 8
+ - image: solid-color(43, 32, 0, 255, 8, 8)
+ bounds: 344 256 8 8
+ - image: solid-color(44, 32, 0, 255, 8, 8)
+ bounds: 352 256 8 8
+ - image: solid-color(45, 32, 0, 255, 8, 8)
+ bounds: 360 256 8 8
+ - image: solid-color(46, 32, 0, 255, 8, 8)
+ bounds: 368 256 8 8
+ - image: solid-color(47, 32, 0, 255, 8, 8)
+ bounds: 376 256 8 8
+ - image: solid-color(48, 32, 0, 255, 8, 8)
+ bounds: 384 256 8 8
+ - image: solid-color(49, 32, 0, 255, 8, 8)
+ bounds: 392 256 8 8
+ - image: solid-color(50, 32, 0, 255, 8, 8)
+ bounds: 400 256 8 8
+ - image: solid-color(51, 32, 0, 255, 8, 8)
+ bounds: 408 256 8 8
+ - image: solid-color(52, 32, 0, 255, 8, 8)
+ bounds: 416 256 8 8
+ - image: solid-color(53, 32, 0, 255, 8, 8)
+ bounds: 424 256 8 8
+ - image: solid-color(54, 32, 0, 255, 8, 8)
+ bounds: 432 256 8 8
+ - image: solid-color(55, 32, 0, 255, 8, 8)
+ bounds: 440 256 8 8
+ - image: solid-color(56, 32, 0, 255, 8, 8)
+ bounds: 448 256 8 8
+ - image: solid-color(57, 32, 0, 255, 8, 8)
+ bounds: 456 256 8 8
+ - image: solid-color(58, 32, 0, 255, 8, 8)
+ bounds: 464 256 8 8
+ - image: solid-color(59, 32, 0, 255, 8, 8)
+ bounds: 472 256 8 8
+ - image: solid-color(60, 32, 0, 255, 8, 8)
+ bounds: 480 256 8 8
+ - image: solid-color(61, 32, 0, 255, 8, 8)
+ bounds: 488 256 8 8
+ - image: solid-color(62, 32, 0, 255, 8, 8)
+ bounds: 496 256 8 8
+ - image: solid-color(63, 32, 0, 255, 8, 8)
+ bounds: 504 256 8 8
+ - image: solid-color(64, 32, 0, 255, 8, 8)
+ bounds: 512 256 8 8
+ - image: solid-color(65, 32, 0, 255, 8, 8)
+ bounds: 520 256 8 8
+ - image: solid-color(66, 32, 0, 255, 8, 8)
+ bounds: 528 256 8 8
+ - image: solid-color(67, 32, 0, 255, 8, 8)
+ bounds: 536 256 8 8
+ - image: solid-color(68, 32, 0, 255, 8, 8)
+ bounds: 544 256 8 8
+ - image: solid-color(69, 32, 0, 255, 8, 8)
+ bounds: 552 256 8 8
+ - image: solid-color(70, 32, 0, 255, 8, 8)
+ bounds: 560 256 8 8
+ - image: solid-color(71, 32, 0, 255, 8, 8)
+ bounds: 568 256 8 8
+ - image: solid-color(72, 32, 0, 255, 8, 8)
+ bounds: 576 256 8 8
+ - image: solid-color(73, 32, 0, 255, 8, 8)
+ bounds: 584 256 8 8
+ - image: solid-color(74, 32, 0, 255, 8, 8)
+ bounds: 592 256 8 8
+ - image: solid-color(75, 32, 0, 255, 8, 8)
+ bounds: 600 256 8 8
+ - image: solid-color(76, 32, 0, 255, 8, 8)
+ bounds: 608 256 8 8
+ - image: solid-color(77, 32, 0, 255, 8, 8)
+ bounds: 616 256 8 8
+ - image: solid-color(78, 32, 0, 255, 8, 8)
+ bounds: 624 256 8 8
+ - image: solid-color(79, 32, 0, 255, 8, 8)
+ bounds: 632 256 8 8
+ - image: solid-color(80, 32, 0, 255, 8, 8)
+ bounds: 640 256 8 8
+ - image: solid-color(81, 32, 0, 255, 8, 8)
+ bounds: 648 256 8 8
+ - image: solid-color(82, 32, 0, 255, 8, 8)
+ bounds: 656 256 8 8
+ - image: solid-color(83, 32, 0, 255, 8, 8)
+ bounds: 664 256 8 8
+ - image: solid-color(84, 32, 0, 255, 8, 8)
+ bounds: 672 256 8 8
+ - image: solid-color(85, 32, 0, 255, 8, 8)
+ bounds: 680 256 8 8
+ - image: solid-color(86, 32, 0, 255, 8, 8)
+ bounds: 688 256 8 8
+ - image: solid-color(87, 32, 0, 255, 8, 8)
+ bounds: 696 256 8 8
+ - image: solid-color(88, 32, 0, 255, 8, 8)
+ bounds: 704 256 8 8
+ - image: solid-color(89, 32, 0, 255, 8, 8)
+ bounds: 712 256 8 8
+ - image: solid-color(90, 32, 0, 255, 8, 8)
+ bounds: 720 256 8 8
+ - image: solid-color(91, 32, 0, 255, 8, 8)
+ bounds: 728 256 8 8
+ - image: solid-color(92, 32, 0, 255, 8, 8)
+ bounds: 736 256 8 8
+ - image: solid-color(93, 32, 0, 255, 8, 8)
+ bounds: 744 256 8 8
+ - image: solid-color(94, 32, 0, 255, 8, 8)
+ bounds: 752 256 8 8
+ - image: solid-color(95, 32, 0, 255, 8, 8)
+ bounds: 760 256 8 8
+ - image: solid-color(96, 32, 0, 255, 8, 8)
+ bounds: 768 256 8 8
+ - image: solid-color(97, 32, 0, 255, 8, 8)
+ bounds: 776 256 8 8
+ - image: solid-color(98, 32, 0, 255, 8, 8)
+ bounds: 784 256 8 8
+ - image: solid-color(99, 32, 0, 255, 8, 8)
+ bounds: 792 256 8 8
+ - image: solid-color(100, 32, 0, 255, 8, 8)
+ bounds: 800 256 8 8
+ - image: solid-color(101, 32, 0, 255, 8, 8)
+ bounds: 808 256 8 8
+ - image: solid-color(102, 32, 0, 255, 8, 8)
+ bounds: 816 256 8 8
+ - image: solid-color(103, 32, 0, 255, 8, 8)
+ bounds: 824 256 8 8
+ - image: solid-color(104, 32, 0, 255, 8, 8)
+ bounds: 832 256 8 8
+ - image: solid-color(105, 32, 0, 255, 8, 8)
+ bounds: 840 256 8 8
+ - image: solid-color(106, 32, 0, 255, 8, 8)
+ bounds: 848 256 8 8
+ - image: solid-color(107, 32, 0, 255, 8, 8)
+ bounds: 856 256 8 8
+ - image: solid-color(108, 32, 0, 255, 8, 8)
+ bounds: 864 256 8 8
+ - image: solid-color(109, 32, 0, 255, 8, 8)
+ bounds: 872 256 8 8
+ - image: solid-color(110, 32, 0, 255, 8, 8)
+ bounds: 880 256 8 8
+ - image: solid-color(111, 32, 0, 255, 8, 8)
+ bounds: 888 256 8 8
+ - image: solid-color(112, 32, 0, 255, 8, 8)
+ bounds: 896 256 8 8
+ - image: solid-color(113, 32, 0, 255, 8, 8)
+ bounds: 904 256 8 8
+ - image: solid-color(114, 32, 0, 255, 8, 8)
+ bounds: 912 256 8 8
+ - image: solid-color(115, 32, 0, 255, 8, 8)
+ bounds: 920 256 8 8
+ - image: solid-color(116, 32, 0, 255, 8, 8)
+ bounds: 928 256 8 8
+ - image: solid-color(117, 32, 0, 255, 8, 8)
+ bounds: 936 256 8 8
+ - image: solid-color(118, 32, 0, 255, 8, 8)
+ bounds: 944 256 8 8
+ - image: solid-color(119, 32, 0, 255, 8, 8)
+ bounds: 952 256 8 8
+ - image: solid-color(120, 32, 0, 255, 8, 8)
+ bounds: 960 256 8 8
+ - image: solid-color(121, 32, 0, 255, 8, 8)
+ bounds: 968 256 8 8
+ - image: solid-color(122, 32, 0, 255, 8, 8)
+ bounds: 976 256 8 8
+ - image: solid-color(123, 32, 0, 255, 8, 8)
+ bounds: 984 256 8 8
+ - image: solid-color(124, 32, 0, 255, 8, 8)
+ bounds: 992 256 8 8
+ - image: solid-color(125, 32, 0, 255, 8, 8)
+ bounds: 1000 256 8 8
+ - image: solid-color(126, 32, 0, 255, 8, 8)
+ bounds: 1008 256 8 8
+ - image: solid-color(127, 32, 0, 255, 8, 8)
+ bounds: 1016 256 8 8
+ - image: solid-color(0, 33, 0, 255, 8, 8)
+ bounds: 0 264 8 8
+ - image: solid-color(1, 33, 0, 255, 8, 8)
+ bounds: 8 264 8 8
+ - image: solid-color(2, 33, 0, 255, 8, 8)
+ bounds: 16 264 8 8
+ - image: solid-color(3, 33, 0, 255, 8, 8)
+ bounds: 24 264 8 8
+ - image: solid-color(4, 33, 0, 255, 8, 8)
+ bounds: 32 264 8 8
+ - image: solid-color(5, 33, 0, 255, 8, 8)
+ bounds: 40 264 8 8
+ - image: solid-color(6, 33, 0, 255, 8, 8)
+ bounds: 48 264 8 8
+ - image: solid-color(7, 33, 0, 255, 8, 8)
+ bounds: 56 264 8 8
+ - image: solid-color(8, 33, 0, 255, 8, 8)
+ bounds: 64 264 8 8
+ - image: solid-color(9, 33, 0, 255, 8, 8)
+ bounds: 72 264 8 8
+ - image: solid-color(10, 33, 0, 255, 8, 8)
+ bounds: 80 264 8 8
+ - image: solid-color(11, 33, 0, 255, 8, 8)
+ bounds: 88 264 8 8
+ - image: solid-color(12, 33, 0, 255, 8, 8)
+ bounds: 96 264 8 8
+ - image: solid-color(13, 33, 0, 255, 8, 8)
+ bounds: 104 264 8 8
+ - image: solid-color(14, 33, 0, 255, 8, 8)
+ bounds: 112 264 8 8
+ - image: solid-color(15, 33, 0, 255, 8, 8)
+ bounds: 120 264 8 8
+ - image: solid-color(16, 33, 0, 255, 8, 8)
+ bounds: 128 264 8 8
+ - image: solid-color(17, 33, 0, 255, 8, 8)
+ bounds: 136 264 8 8
+ - image: solid-color(18, 33, 0, 255, 8, 8)
+ bounds: 144 264 8 8
+ - image: solid-color(19, 33, 0, 255, 8, 8)
+ bounds: 152 264 8 8
+ - image: solid-color(20, 33, 0, 255, 8, 8)
+ bounds: 160 264 8 8
+ - image: solid-color(21, 33, 0, 255, 8, 8)
+ bounds: 168 264 8 8
+ - image: solid-color(22, 33, 0, 255, 8, 8)
+ bounds: 176 264 8 8
+ - image: solid-color(23, 33, 0, 255, 8, 8)
+ bounds: 184 264 8 8
+ - image: solid-color(24, 33, 0, 255, 8, 8)
+ bounds: 192 264 8 8
+ - image: solid-color(25, 33, 0, 255, 8, 8)
+ bounds: 200 264 8 8
+ - image: solid-color(26, 33, 0, 255, 8, 8)
+ bounds: 208 264 8 8
+ - image: solid-color(27, 33, 0, 255, 8, 8)
+ bounds: 216 264 8 8
+ - image: solid-color(28, 33, 0, 255, 8, 8)
+ bounds: 224 264 8 8
+ - image: solid-color(29, 33, 0, 255, 8, 8)
+ bounds: 232 264 8 8
+ - image: solid-color(30, 33, 0, 255, 8, 8)
+ bounds: 240 264 8 8
+ - image: solid-color(31, 33, 0, 255, 8, 8)
+ bounds: 248 264 8 8
+ - image: solid-color(32, 33, 0, 255, 8, 8)
+ bounds: 256 264 8 8
+ - image: solid-color(33, 33, 0, 255, 8, 8)
+ bounds: 264 264 8 8
+ - image: solid-color(34, 33, 0, 255, 8, 8)
+ bounds: 272 264 8 8
+ - image: solid-color(35, 33, 0, 255, 8, 8)
+ bounds: 280 264 8 8
+ - image: solid-color(36, 33, 0, 255, 8, 8)
+ bounds: 288 264 8 8
+ - image: solid-color(37, 33, 0, 255, 8, 8)
+ bounds: 296 264 8 8
+ - image: solid-color(38, 33, 0, 255, 8, 8)
+ bounds: 304 264 8 8
+ - image: solid-color(39, 33, 0, 255, 8, 8)
+ bounds: 312 264 8 8
+ - image: solid-color(40, 33, 0, 255, 8, 8)
+ bounds: 320 264 8 8
+ - image: solid-color(41, 33, 0, 255, 8, 8)
+ bounds: 328 264 8 8
+ - image: solid-color(42, 33, 0, 255, 8, 8)
+ bounds: 336 264 8 8
+ - image: solid-color(43, 33, 0, 255, 8, 8)
+ bounds: 344 264 8 8
+ - image: solid-color(44, 33, 0, 255, 8, 8)
+ bounds: 352 264 8 8
+ - image: solid-color(45, 33, 0, 255, 8, 8)
+ bounds: 360 264 8 8
+ - image: solid-color(46, 33, 0, 255, 8, 8)
+ bounds: 368 264 8 8
+ - image: solid-color(47, 33, 0, 255, 8, 8)
+ bounds: 376 264 8 8
+ - image: solid-color(48, 33, 0, 255, 8, 8)
+ bounds: 384 264 8 8
+ - image: solid-color(49, 33, 0, 255, 8, 8)
+ bounds: 392 264 8 8
+ - image: solid-color(50, 33, 0, 255, 8, 8)
+ bounds: 400 264 8 8
+ - image: solid-color(51, 33, 0, 255, 8, 8)
+ bounds: 408 264 8 8
+ - image: solid-color(52, 33, 0, 255, 8, 8)
+ bounds: 416 264 8 8
+ - image: solid-color(53, 33, 0, 255, 8, 8)
+ bounds: 424 264 8 8
+ - image: solid-color(54, 33, 0, 255, 8, 8)
+ bounds: 432 264 8 8
+ - image: solid-color(55, 33, 0, 255, 8, 8)
+ bounds: 440 264 8 8
+ - image: solid-color(56, 33, 0, 255, 8, 8)
+ bounds: 448 264 8 8
+ - image: solid-color(57, 33, 0, 255, 8, 8)
+ bounds: 456 264 8 8
+ - image: solid-color(58, 33, 0, 255, 8, 8)
+ bounds: 464 264 8 8
+ - image: solid-color(59, 33, 0, 255, 8, 8)
+ bounds: 472 264 8 8
+ - image: solid-color(60, 33, 0, 255, 8, 8)
+ bounds: 480 264 8 8
+ - image: solid-color(61, 33, 0, 255, 8, 8)
+ bounds: 488 264 8 8
+ - image: solid-color(62, 33, 0, 255, 8, 8)
+ bounds: 496 264 8 8
+ - image: solid-color(63, 33, 0, 255, 8, 8)
+ bounds: 504 264 8 8
+ - image: solid-color(64, 33, 0, 255, 8, 8)
+ bounds: 512 264 8 8
+ - image: solid-color(65, 33, 0, 255, 8, 8)
+ bounds: 520 264 8 8
+ - image: solid-color(66, 33, 0, 255, 8, 8)
+ bounds: 528 264 8 8
+ - image: solid-color(67, 33, 0, 255, 8, 8)
+ bounds: 536 264 8 8
+ - image: solid-color(68, 33, 0, 255, 8, 8)
+ bounds: 544 264 8 8
+ - image: solid-color(69, 33, 0, 255, 8, 8)
+ bounds: 552 264 8 8
+ - image: solid-color(70, 33, 0, 255, 8, 8)
+ bounds: 560 264 8 8
+ - image: solid-color(71, 33, 0, 255, 8, 8)
+ bounds: 568 264 8 8
+ - image: solid-color(72, 33, 0, 255, 8, 8)
+ bounds: 576 264 8 8
+ - image: solid-color(73, 33, 0, 255, 8, 8)
+ bounds: 584 264 8 8
+ - image: solid-color(74, 33, 0, 255, 8, 8)
+ bounds: 592 264 8 8
+ - image: solid-color(75, 33, 0, 255, 8, 8)
+ bounds: 600 264 8 8
+ - image: solid-color(76, 33, 0, 255, 8, 8)
+ bounds: 608 264 8 8
+ - image: solid-color(77, 33, 0, 255, 8, 8)
+ bounds: 616 264 8 8
+ - image: solid-color(78, 33, 0, 255, 8, 8)
+ bounds: 624 264 8 8
+ - image: solid-color(79, 33, 0, 255, 8, 8)
+ bounds: 632 264 8 8
+ - image: solid-color(80, 33, 0, 255, 8, 8)
+ bounds: 640 264 8 8
+ - image: solid-color(81, 33, 0, 255, 8, 8)
+ bounds: 648 264 8 8
+ - image: solid-color(82, 33, 0, 255, 8, 8)
+ bounds: 656 264 8 8
+ - image: solid-color(83, 33, 0, 255, 8, 8)
+ bounds: 664 264 8 8
+ - image: solid-color(84, 33, 0, 255, 8, 8)
+ bounds: 672 264 8 8
+ - image: solid-color(85, 33, 0, 255, 8, 8)
+ bounds: 680 264 8 8
+ - image: solid-color(86, 33, 0, 255, 8, 8)
+ bounds: 688 264 8 8
+ - image: solid-color(87, 33, 0, 255, 8, 8)
+ bounds: 696 264 8 8
+ - image: solid-color(88, 33, 0, 255, 8, 8)
+ bounds: 704 264 8 8
+ - image: solid-color(89, 33, 0, 255, 8, 8)
+ bounds: 712 264 8 8
+ - image: solid-color(90, 33, 0, 255, 8, 8)
+ bounds: 720 264 8 8
+ - image: solid-color(91, 33, 0, 255, 8, 8)
+ bounds: 728 264 8 8
+ - image: solid-color(92, 33, 0, 255, 8, 8)
+ bounds: 736 264 8 8
+ - image: solid-color(93, 33, 0, 255, 8, 8)
+ bounds: 744 264 8 8
+ - image: solid-color(94, 33, 0, 255, 8, 8)
+ bounds: 752 264 8 8
+ - image: solid-color(95, 33, 0, 255, 8, 8)
+ bounds: 760 264 8 8
+ - image: solid-color(96, 33, 0, 255, 8, 8)
+ bounds: 768 264 8 8
+ - image: solid-color(97, 33, 0, 255, 8, 8)
+ bounds: 776 264 8 8
+ - image: solid-color(98, 33, 0, 255, 8, 8)
+ bounds: 784 264 8 8
+ - image: solid-color(99, 33, 0, 255, 8, 8)
+ bounds: 792 264 8 8
+ - image: solid-color(100, 33, 0, 255, 8, 8)
+ bounds: 800 264 8 8
+ - image: solid-color(101, 33, 0, 255, 8, 8)
+ bounds: 808 264 8 8
+ - image: solid-color(102, 33, 0, 255, 8, 8)
+ bounds: 816 264 8 8
+ - image: solid-color(103, 33, 0, 255, 8, 8)
+ bounds: 824 264 8 8
+ - image: solid-color(104, 33, 0, 255, 8, 8)
+ bounds: 832 264 8 8
+ - image: solid-color(105, 33, 0, 255, 8, 8)
+ bounds: 840 264 8 8
+ - image: solid-color(106, 33, 0, 255, 8, 8)
+ bounds: 848 264 8 8
+ - image: solid-color(107, 33, 0, 255, 8, 8)
+ bounds: 856 264 8 8
+ - image: solid-color(108, 33, 0, 255, 8, 8)
+ bounds: 864 264 8 8
+ - image: solid-color(109, 33, 0, 255, 8, 8)
+ bounds: 872 264 8 8
+ - image: solid-color(110, 33, 0, 255, 8, 8)
+ bounds: 880 264 8 8
+ - image: solid-color(111, 33, 0, 255, 8, 8)
+ bounds: 888 264 8 8
+ - image: solid-color(112, 33, 0, 255, 8, 8)
+ bounds: 896 264 8 8
+ - image: solid-color(113, 33, 0, 255, 8, 8)
+ bounds: 904 264 8 8
+ - image: solid-color(114, 33, 0, 255, 8, 8)
+ bounds: 912 264 8 8
+ - image: solid-color(115, 33, 0, 255, 8, 8)
+ bounds: 920 264 8 8
+ - image: solid-color(116, 33, 0, 255, 8, 8)
+ bounds: 928 264 8 8
+ - image: solid-color(117, 33, 0, 255, 8, 8)
+ bounds: 936 264 8 8
+ - image: solid-color(118, 33, 0, 255, 8, 8)
+ bounds: 944 264 8 8
+ - image: solid-color(119, 33, 0, 255, 8, 8)
+ bounds: 952 264 8 8
+ - image: solid-color(120, 33, 0, 255, 8, 8)
+ bounds: 960 264 8 8
+ - image: solid-color(121, 33, 0, 255, 8, 8)
+ bounds: 968 264 8 8
+ - image: solid-color(122, 33, 0, 255, 8, 8)
+ bounds: 976 264 8 8
+ - image: solid-color(123, 33, 0, 255, 8, 8)
+ bounds: 984 264 8 8
+ - image: solid-color(124, 33, 0, 255, 8, 8)
+ bounds: 992 264 8 8
+ - image: solid-color(125, 33, 0, 255, 8, 8)
+ bounds: 1000 264 8 8
+ - image: solid-color(126, 33, 0, 255, 8, 8)
+ bounds: 1008 264 8 8
+ - image: solid-color(127, 33, 0, 255, 8, 8)
+ bounds: 1016 264 8 8
+ - image: solid-color(0, 34, 0, 255, 8, 8)
+ bounds: 0 272 8 8
+ - image: solid-color(1, 34, 0, 255, 8, 8)
+ bounds: 8 272 8 8
+ - image: solid-color(2, 34, 0, 255, 8, 8)
+ bounds: 16 272 8 8
+ - image: solid-color(3, 34, 0, 255, 8, 8)
+ bounds: 24 272 8 8
+ - image: solid-color(4, 34, 0, 255, 8, 8)
+ bounds: 32 272 8 8
+ - image: solid-color(5, 34, 0, 255, 8, 8)
+ bounds: 40 272 8 8
+ - image: solid-color(6, 34, 0, 255, 8, 8)
+ bounds: 48 272 8 8
+ - image: solid-color(7, 34, 0, 255, 8, 8)
+ bounds: 56 272 8 8
+ - image: solid-color(8, 34, 0, 255, 8, 8)
+ bounds: 64 272 8 8
+ - image: solid-color(9, 34, 0, 255, 8, 8)
+ bounds: 72 272 8 8
+ - image: solid-color(10, 34, 0, 255, 8, 8)
+ bounds: 80 272 8 8
+ - image: solid-color(11, 34, 0, 255, 8, 8)
+ bounds: 88 272 8 8
+ - image: solid-color(12, 34, 0, 255, 8, 8)
+ bounds: 96 272 8 8
+ - image: solid-color(13, 34, 0, 255, 8, 8)
+ bounds: 104 272 8 8
+ - image: solid-color(14, 34, 0, 255, 8, 8)
+ bounds: 112 272 8 8
+ - image: solid-color(15, 34, 0, 255, 8, 8)
+ bounds: 120 272 8 8
+ - image: solid-color(16, 34, 0, 255, 8, 8)
+ bounds: 128 272 8 8
+ - image: solid-color(17, 34, 0, 255, 8, 8)
+ bounds: 136 272 8 8
+ - image: solid-color(18, 34, 0, 255, 8, 8)
+ bounds: 144 272 8 8
+ - image: solid-color(19, 34, 0, 255, 8, 8)
+ bounds: 152 272 8 8
+ - image: solid-color(20, 34, 0, 255, 8, 8)
+ bounds: 160 272 8 8
+ - image: solid-color(21, 34, 0, 255, 8, 8)
+ bounds: 168 272 8 8
+ - image: solid-color(22, 34, 0, 255, 8, 8)
+ bounds: 176 272 8 8
+ - image: solid-color(23, 34, 0, 255, 8, 8)
+ bounds: 184 272 8 8
+ - image: solid-color(24, 34, 0, 255, 8, 8)
+ bounds: 192 272 8 8
+ - image: solid-color(25, 34, 0, 255, 8, 8)
+ bounds: 200 272 8 8
+ - image: solid-color(26, 34, 0, 255, 8, 8)
+ bounds: 208 272 8 8
+ - image: solid-color(27, 34, 0, 255, 8, 8)
+ bounds: 216 272 8 8
+ - image: solid-color(28, 34, 0, 255, 8, 8)
+ bounds: 224 272 8 8
+ - image: solid-color(29, 34, 0, 255, 8, 8)
+ bounds: 232 272 8 8
+ - image: solid-color(30, 34, 0, 255, 8, 8)
+ bounds: 240 272 8 8
+ - image: solid-color(31, 34, 0, 255, 8, 8)
+ bounds: 248 272 8 8
+ - image: solid-color(32, 34, 0, 255, 8, 8)
+ bounds: 256 272 8 8
+ - image: solid-color(33, 34, 0, 255, 8, 8)
+ bounds: 264 272 8 8
+ - image: solid-color(34, 34, 0, 255, 8, 8)
+ bounds: 272 272 8 8
+ - image: solid-color(35, 34, 0, 255, 8, 8)
+ bounds: 280 272 8 8
+ - image: solid-color(36, 34, 0, 255, 8, 8)
+ bounds: 288 272 8 8
+ - image: solid-color(37, 34, 0, 255, 8, 8)
+ bounds: 296 272 8 8
+ - image: solid-color(38, 34, 0, 255, 8, 8)
+ bounds: 304 272 8 8
+ - image: solid-color(39, 34, 0, 255, 8, 8)
+ bounds: 312 272 8 8
+ - image: solid-color(40, 34, 0, 255, 8, 8)
+ bounds: 320 272 8 8
+ - image: solid-color(41, 34, 0, 255, 8, 8)
+ bounds: 328 272 8 8
+ - image: solid-color(42, 34, 0, 255, 8, 8)
+ bounds: 336 272 8 8
+ - image: solid-color(43, 34, 0, 255, 8, 8)
+ bounds: 344 272 8 8
+ - image: solid-color(44, 34, 0, 255, 8, 8)
+ bounds: 352 272 8 8
+ - image: solid-color(45, 34, 0, 255, 8, 8)
+ bounds: 360 272 8 8
+ - image: solid-color(46, 34, 0, 255, 8, 8)
+ bounds: 368 272 8 8
+ - image: solid-color(47, 34, 0, 255, 8, 8)
+ bounds: 376 272 8 8
+ - image: solid-color(48, 34, 0, 255, 8, 8)
+ bounds: 384 272 8 8
+ - image: solid-color(49, 34, 0, 255, 8, 8)
+ bounds: 392 272 8 8
+ - image: solid-color(50, 34, 0, 255, 8, 8)
+ bounds: 400 272 8 8
+ - image: solid-color(51, 34, 0, 255, 8, 8)
+ bounds: 408 272 8 8
+ - image: solid-color(52, 34, 0, 255, 8, 8)
+ bounds: 416 272 8 8
+ - image: solid-color(53, 34, 0, 255, 8, 8)
+ bounds: 424 272 8 8
+ - image: solid-color(54, 34, 0, 255, 8, 8)
+ bounds: 432 272 8 8
+ - image: solid-color(55, 34, 0, 255, 8, 8)
+ bounds: 440 272 8 8
+ - image: solid-color(56, 34, 0, 255, 8, 8)
+ bounds: 448 272 8 8
+ - image: solid-color(57, 34, 0, 255, 8, 8)
+ bounds: 456 272 8 8
+ - image: solid-color(58, 34, 0, 255, 8, 8)
+ bounds: 464 272 8 8
+ - image: solid-color(59, 34, 0, 255, 8, 8)
+ bounds: 472 272 8 8
+ - image: solid-color(60, 34, 0, 255, 8, 8)
+ bounds: 480 272 8 8
+ - image: solid-color(61, 34, 0, 255, 8, 8)
+ bounds: 488 272 8 8
+ - image: solid-color(62, 34, 0, 255, 8, 8)
+ bounds: 496 272 8 8
+ - image: solid-color(63, 34, 0, 255, 8, 8)
+ bounds: 504 272 8 8
+ - image: solid-color(64, 34, 0, 255, 8, 8)
+ bounds: 512 272 8 8
+ - image: solid-color(65, 34, 0, 255, 8, 8)
+ bounds: 520 272 8 8
+ - image: solid-color(66, 34, 0, 255, 8, 8)
+ bounds: 528 272 8 8
+ - image: solid-color(67, 34, 0, 255, 8, 8)
+ bounds: 536 272 8 8
+ - image: solid-color(68, 34, 0, 255, 8, 8)
+ bounds: 544 272 8 8
+ - image: solid-color(69, 34, 0, 255, 8, 8)
+ bounds: 552 272 8 8
+ - image: solid-color(70, 34, 0, 255, 8, 8)
+ bounds: 560 272 8 8
+ - image: solid-color(71, 34, 0, 255, 8, 8)
+ bounds: 568 272 8 8
+ - image: solid-color(72, 34, 0, 255, 8, 8)
+ bounds: 576 272 8 8
+ - image: solid-color(73, 34, 0, 255, 8, 8)
+ bounds: 584 272 8 8
+ - image: solid-color(74, 34, 0, 255, 8, 8)
+ bounds: 592 272 8 8
+ - image: solid-color(75, 34, 0, 255, 8, 8)
+ bounds: 600 272 8 8
+ - image: solid-color(76, 34, 0, 255, 8, 8)
+ bounds: 608 272 8 8
+ - image: solid-color(77, 34, 0, 255, 8, 8)
+ bounds: 616 272 8 8
+ - image: solid-color(78, 34, 0, 255, 8, 8)
+ bounds: 624 272 8 8
+ - image: solid-color(79, 34, 0, 255, 8, 8)
+ bounds: 632 272 8 8
+ - image: solid-color(80, 34, 0, 255, 8, 8)
+ bounds: 640 272 8 8
+ - image: solid-color(81, 34, 0, 255, 8, 8)
+ bounds: 648 272 8 8
+ - image: solid-color(82, 34, 0, 255, 8, 8)
+ bounds: 656 272 8 8
+ - image: solid-color(83, 34, 0, 255, 8, 8)
+ bounds: 664 272 8 8
+ - image: solid-color(84, 34, 0, 255, 8, 8)
+ bounds: 672 272 8 8
+ - image: solid-color(85, 34, 0, 255, 8, 8)
+ bounds: 680 272 8 8
+ - image: solid-color(86, 34, 0, 255, 8, 8)
+ bounds: 688 272 8 8
+ - image: solid-color(87, 34, 0, 255, 8, 8)
+ bounds: 696 272 8 8
+ - image: solid-color(88, 34, 0, 255, 8, 8)
+ bounds: 704 272 8 8
+ - image: solid-color(89, 34, 0, 255, 8, 8)
+ bounds: 712 272 8 8
+ - image: solid-color(90, 34, 0, 255, 8, 8)
+ bounds: 720 272 8 8
+ - image: solid-color(91, 34, 0, 255, 8, 8)
+ bounds: 728 272 8 8
+ - image: solid-color(92, 34, 0, 255, 8, 8)
+ bounds: 736 272 8 8
+ - image: solid-color(93, 34, 0, 255, 8, 8)
+ bounds: 744 272 8 8
+ - image: solid-color(94, 34, 0, 255, 8, 8)
+ bounds: 752 272 8 8
+ - image: solid-color(95, 34, 0, 255, 8, 8)
+ bounds: 760 272 8 8
+ - image: solid-color(96, 34, 0, 255, 8, 8)
+ bounds: 768 272 8 8
+ - image: solid-color(97, 34, 0, 255, 8, 8)
+ bounds: 776 272 8 8
+ - image: solid-color(98, 34, 0, 255, 8, 8)
+ bounds: 784 272 8 8
+ - image: solid-color(99, 34, 0, 255, 8, 8)
+ bounds: 792 272 8 8
+ - image: solid-color(100, 34, 0, 255, 8, 8)
+ bounds: 800 272 8 8
+ - image: solid-color(101, 34, 0, 255, 8, 8)
+ bounds: 808 272 8 8
+ - image: solid-color(102, 34, 0, 255, 8, 8)
+ bounds: 816 272 8 8
+ - image: solid-color(103, 34, 0, 255, 8, 8)
+ bounds: 824 272 8 8
+ - image: solid-color(104, 34, 0, 255, 8, 8)
+ bounds: 832 272 8 8
+ - image: solid-color(105, 34, 0, 255, 8, 8)
+ bounds: 840 272 8 8
+ - image: solid-color(106, 34, 0, 255, 8, 8)
+ bounds: 848 272 8 8
+ - image: solid-color(107, 34, 0, 255, 8, 8)
+ bounds: 856 272 8 8
+ - image: solid-color(108, 34, 0, 255, 8, 8)
+ bounds: 864 272 8 8
+ - image: solid-color(109, 34, 0, 255, 8, 8)
+ bounds: 872 272 8 8
+ - image: solid-color(110, 34, 0, 255, 8, 8)
+ bounds: 880 272 8 8
+ - image: solid-color(111, 34, 0, 255, 8, 8)
+ bounds: 888 272 8 8
+ - image: solid-color(112, 34, 0, 255, 8, 8)
+ bounds: 896 272 8 8
+ - image: solid-color(113, 34, 0, 255, 8, 8)
+ bounds: 904 272 8 8
+ - image: solid-color(114, 34, 0, 255, 8, 8)
+ bounds: 912 272 8 8
+ - image: solid-color(115, 34, 0, 255, 8, 8)
+ bounds: 920 272 8 8
+ - image: solid-color(116, 34, 0, 255, 8, 8)
+ bounds: 928 272 8 8
+ - image: solid-color(117, 34, 0, 255, 8, 8)
+ bounds: 936 272 8 8
+ - image: solid-color(118, 34, 0, 255, 8, 8)
+ bounds: 944 272 8 8
+ - image: solid-color(119, 34, 0, 255, 8, 8)
+ bounds: 952 272 8 8
+ - image: solid-color(120, 34, 0, 255, 8, 8)
+ bounds: 960 272 8 8
+ - image: solid-color(121, 34, 0, 255, 8, 8)
+ bounds: 968 272 8 8
+ - image: solid-color(122, 34, 0, 255, 8, 8)
+ bounds: 976 272 8 8
+ - image: solid-color(123, 34, 0, 255, 8, 8)
+ bounds: 984 272 8 8
+ - image: solid-color(124, 34, 0, 255, 8, 8)
+ bounds: 992 272 8 8
+ - image: solid-color(125, 34, 0, 255, 8, 8)
+ bounds: 1000 272 8 8
+ - image: solid-color(126, 34, 0, 255, 8, 8)
+ bounds: 1008 272 8 8
+ - image: solid-color(127, 34, 0, 255, 8, 8)
+ bounds: 1016 272 8 8
+ - image: solid-color(0, 35, 0, 255, 8, 8)
+ bounds: 0 280 8 8
+ - image: solid-color(1, 35, 0, 255, 8, 8)
+ bounds: 8 280 8 8
+ - image: solid-color(2, 35, 0, 255, 8, 8)
+ bounds: 16 280 8 8
+ - image: solid-color(3, 35, 0, 255, 8, 8)
+ bounds: 24 280 8 8
+ - image: solid-color(4, 35, 0, 255, 8, 8)
+ bounds: 32 280 8 8
+ - image: solid-color(5, 35, 0, 255, 8, 8)
+ bounds: 40 280 8 8
+ - image: solid-color(6, 35, 0, 255, 8, 8)
+ bounds: 48 280 8 8
+ - image: solid-color(7, 35, 0, 255, 8, 8)
+ bounds: 56 280 8 8
+ - image: solid-color(8, 35, 0, 255, 8, 8)
+ bounds: 64 280 8 8
+ - image: solid-color(9, 35, 0, 255, 8, 8)
+ bounds: 72 280 8 8
+ - image: solid-color(10, 35, 0, 255, 8, 8)
+ bounds: 80 280 8 8
+ - image: solid-color(11, 35, 0, 255, 8, 8)
+ bounds: 88 280 8 8
+ - image: solid-color(12, 35, 0, 255, 8, 8)
+ bounds: 96 280 8 8
+ - image: solid-color(13, 35, 0, 255, 8, 8)
+ bounds: 104 280 8 8
+ - image: solid-color(14, 35, 0, 255, 8, 8)
+ bounds: 112 280 8 8
+ - image: solid-color(15, 35, 0, 255, 8, 8)
+ bounds: 120 280 8 8
+ - image: solid-color(16, 35, 0, 255, 8, 8)
+ bounds: 128 280 8 8
+ - image: solid-color(17, 35, 0, 255, 8, 8)
+ bounds: 136 280 8 8
+ - image: solid-color(18, 35, 0, 255, 8, 8)
+ bounds: 144 280 8 8
+ - image: solid-color(19, 35, 0, 255, 8, 8)
+ bounds: 152 280 8 8
+ - image: solid-color(20, 35, 0, 255, 8, 8)
+ bounds: 160 280 8 8
+ - image: solid-color(21, 35, 0, 255, 8, 8)
+ bounds: 168 280 8 8
+ - image: solid-color(22, 35, 0, 255, 8, 8)
+ bounds: 176 280 8 8
+ - image: solid-color(23, 35, 0, 255, 8, 8)
+ bounds: 184 280 8 8
+ - image: solid-color(24, 35, 0, 255, 8, 8)
+ bounds: 192 280 8 8
+ - image: solid-color(25, 35, 0, 255, 8, 8)
+ bounds: 200 280 8 8
+ - image: solid-color(26, 35, 0, 255, 8, 8)
+ bounds: 208 280 8 8
+ - image: solid-color(27, 35, 0, 255, 8, 8)
+ bounds: 216 280 8 8
+ - image: solid-color(28, 35, 0, 255, 8, 8)
+ bounds: 224 280 8 8
+ - image: solid-color(29, 35, 0, 255, 8, 8)
+ bounds: 232 280 8 8
+ - image: solid-color(30, 35, 0, 255, 8, 8)
+ bounds: 240 280 8 8
+ - image: solid-color(31, 35, 0, 255, 8, 8)
+ bounds: 248 280 8 8
+ - image: solid-color(32, 35, 0, 255, 8, 8)
+ bounds: 256 280 8 8
+ - image: solid-color(33, 35, 0, 255, 8, 8)
+ bounds: 264 280 8 8
+ - image: solid-color(34, 35, 0, 255, 8, 8)
+ bounds: 272 280 8 8
+ - image: solid-color(35, 35, 0, 255, 8, 8)
+ bounds: 280 280 8 8
+ - image: solid-color(36, 35, 0, 255, 8, 8)
+ bounds: 288 280 8 8
+ - image: solid-color(37, 35, 0, 255, 8, 8)
+ bounds: 296 280 8 8
+ - image: solid-color(38, 35, 0, 255, 8, 8)
+ bounds: 304 280 8 8
+ - image: solid-color(39, 35, 0, 255, 8, 8)
+ bounds: 312 280 8 8
+ - image: solid-color(40, 35, 0, 255, 8, 8)
+ bounds: 320 280 8 8
+ - image: solid-color(41, 35, 0, 255, 8, 8)
+ bounds: 328 280 8 8
+ - image: solid-color(42, 35, 0, 255, 8, 8)
+ bounds: 336 280 8 8
+ - image: solid-color(43, 35, 0, 255, 8, 8)
+ bounds: 344 280 8 8
+ - image: solid-color(44, 35, 0, 255, 8, 8)
+ bounds: 352 280 8 8
+ - image: solid-color(45, 35, 0, 255, 8, 8)
+ bounds: 360 280 8 8
+ - image: solid-color(46, 35, 0, 255, 8, 8)
+ bounds: 368 280 8 8
+ - image: solid-color(47, 35, 0, 255, 8, 8)
+ bounds: 376 280 8 8
+ - image: solid-color(48, 35, 0, 255, 8, 8)
+ bounds: 384 280 8 8
+ - image: solid-color(49, 35, 0, 255, 8, 8)
+ bounds: 392 280 8 8
+ - image: solid-color(50, 35, 0, 255, 8, 8)
+ bounds: 400 280 8 8
+ - image: solid-color(51, 35, 0, 255, 8, 8)
+ bounds: 408 280 8 8
+ - image: solid-color(52, 35, 0, 255, 8, 8)
+ bounds: 416 280 8 8
+ - image: solid-color(53, 35, 0, 255, 8, 8)
+ bounds: 424 280 8 8
+ - image: solid-color(54, 35, 0, 255, 8, 8)
+ bounds: 432 280 8 8
+ - image: solid-color(55, 35, 0, 255, 8, 8)
+ bounds: 440 280 8 8
+ - image: solid-color(56, 35, 0, 255, 8, 8)
+ bounds: 448 280 8 8
+ - image: solid-color(57, 35, 0, 255, 8, 8)
+ bounds: 456 280 8 8
+ - image: solid-color(58, 35, 0, 255, 8, 8)
+ bounds: 464 280 8 8
+ - image: solid-color(59, 35, 0, 255, 8, 8)
+ bounds: 472 280 8 8
+ - image: solid-color(60, 35, 0, 255, 8, 8)
+ bounds: 480 280 8 8
+ - image: solid-color(61, 35, 0, 255, 8, 8)
+ bounds: 488 280 8 8
+ - image: solid-color(62, 35, 0, 255, 8, 8)
+ bounds: 496 280 8 8
+ - image: solid-color(63, 35, 0, 255, 8, 8)
+ bounds: 504 280 8 8
+ - image: solid-color(64, 35, 0, 255, 8, 8)
+ bounds: 512 280 8 8
+ - image: solid-color(65, 35, 0, 255, 8, 8)
+ bounds: 520 280 8 8
+ - image: solid-color(66, 35, 0, 255, 8, 8)
+ bounds: 528 280 8 8
+ - image: solid-color(67, 35, 0, 255, 8, 8)
+ bounds: 536 280 8 8
+ - image: solid-color(68, 35, 0, 255, 8, 8)
+ bounds: 544 280 8 8
+ - image: solid-color(69, 35, 0, 255, 8, 8)
+ bounds: 552 280 8 8
+ - image: solid-color(70, 35, 0, 255, 8, 8)
+ bounds: 560 280 8 8
+ - image: solid-color(71, 35, 0, 255, 8, 8)
+ bounds: 568 280 8 8
+ - image: solid-color(72, 35, 0, 255, 8, 8)
+ bounds: 576 280 8 8
+ - image: solid-color(73, 35, 0, 255, 8, 8)
+ bounds: 584 280 8 8
+ - image: solid-color(74, 35, 0, 255, 8, 8)
+ bounds: 592 280 8 8
+ - image: solid-color(75, 35, 0, 255, 8, 8)
+ bounds: 600 280 8 8
+ - image: solid-color(76, 35, 0, 255, 8, 8)
+ bounds: 608 280 8 8
+ - image: solid-color(77, 35, 0, 255, 8, 8)
+ bounds: 616 280 8 8
+ - image: solid-color(78, 35, 0, 255, 8, 8)
+ bounds: 624 280 8 8
+ - image: solid-color(79, 35, 0, 255, 8, 8)
+ bounds: 632 280 8 8
+ - image: solid-color(80, 35, 0, 255, 8, 8)
+ bounds: 640 280 8 8
+ - image: solid-color(81, 35, 0, 255, 8, 8)
+ bounds: 648 280 8 8
+ - image: solid-color(82, 35, 0, 255, 8, 8)
+ bounds: 656 280 8 8
+ - image: solid-color(83, 35, 0, 255, 8, 8)
+ bounds: 664 280 8 8
+ - image: solid-color(84, 35, 0, 255, 8, 8)
+ bounds: 672 280 8 8
+ - image: solid-color(85, 35, 0, 255, 8, 8)
+ bounds: 680 280 8 8
+ - image: solid-color(86, 35, 0, 255, 8, 8)
+ bounds: 688 280 8 8
+ - image: solid-color(87, 35, 0, 255, 8, 8)
+ bounds: 696 280 8 8
+ - image: solid-color(88, 35, 0, 255, 8, 8)
+ bounds: 704 280 8 8
+ - image: solid-color(89, 35, 0, 255, 8, 8)
+ bounds: 712 280 8 8
+ - image: solid-color(90, 35, 0, 255, 8, 8)
+ bounds: 720 280 8 8
+ - image: solid-color(91, 35, 0, 255, 8, 8)
+ bounds: 728 280 8 8
+ - image: solid-color(92, 35, 0, 255, 8, 8)
+ bounds: 736 280 8 8
+ - image: solid-color(93, 35, 0, 255, 8, 8)
+ bounds: 744 280 8 8
+ - image: solid-color(94, 35, 0, 255, 8, 8)
+ bounds: 752 280 8 8
+ - image: solid-color(95, 35, 0, 255, 8, 8)
+ bounds: 760 280 8 8
+ - image: solid-color(96, 35, 0, 255, 8, 8)
+ bounds: 768 280 8 8
+ - image: solid-color(97, 35, 0, 255, 8, 8)
+ bounds: 776 280 8 8
+ - image: solid-color(98, 35, 0, 255, 8, 8)
+ bounds: 784 280 8 8
+ - image: solid-color(99, 35, 0, 255, 8, 8)
+ bounds: 792 280 8 8
+ - image: solid-color(100, 35, 0, 255, 8, 8)
+ bounds: 800 280 8 8
+ - image: solid-color(101, 35, 0, 255, 8, 8)
+ bounds: 808 280 8 8
+ - image: solid-color(102, 35, 0, 255, 8, 8)
+ bounds: 816 280 8 8
+ - image: solid-color(103, 35, 0, 255, 8, 8)
+ bounds: 824 280 8 8
+ - image: solid-color(104, 35, 0, 255, 8, 8)
+ bounds: 832 280 8 8
+ - image: solid-color(105, 35, 0, 255, 8, 8)
+ bounds: 840 280 8 8
+ - image: solid-color(106, 35, 0, 255, 8, 8)
+ bounds: 848 280 8 8
+ - image: solid-color(107, 35, 0, 255, 8, 8)
+ bounds: 856 280 8 8
+ - image: solid-color(108, 35, 0, 255, 8, 8)
+ bounds: 864 280 8 8
+ - image: solid-color(109, 35, 0, 255, 8, 8)
+ bounds: 872 280 8 8
+ - image: solid-color(110, 35, 0, 255, 8, 8)
+ bounds: 880 280 8 8
+ - image: solid-color(111, 35, 0, 255, 8, 8)
+ bounds: 888 280 8 8
+ - image: solid-color(112, 35, 0, 255, 8, 8)
+ bounds: 896 280 8 8
+ - image: solid-color(113, 35, 0, 255, 8, 8)
+ bounds: 904 280 8 8
+ - image: solid-color(114, 35, 0, 255, 8, 8)
+ bounds: 912 280 8 8
+ - image: solid-color(115, 35, 0, 255, 8, 8)
+ bounds: 920 280 8 8
+ - image: solid-color(116, 35, 0, 255, 8, 8)
+ bounds: 928 280 8 8
+ - image: solid-color(117, 35, 0, 255, 8, 8)
+ bounds: 936 280 8 8
+ - image: solid-color(118, 35, 0, 255, 8, 8)
+ bounds: 944 280 8 8
+ - image: solid-color(119, 35, 0, 255, 8, 8)
+ bounds: 952 280 8 8
+ - image: solid-color(120, 35, 0, 255, 8, 8)
+ bounds: 960 280 8 8
+ - image: solid-color(121, 35, 0, 255, 8, 8)
+ bounds: 968 280 8 8
+ - image: solid-color(122, 35, 0, 255, 8, 8)
+ bounds: 976 280 8 8
+ - image: solid-color(123, 35, 0, 255, 8, 8)
+ bounds: 984 280 8 8
+ - image: solid-color(124, 35, 0, 255, 8, 8)
+ bounds: 992 280 8 8
+ - image: solid-color(125, 35, 0, 255, 8, 8)
+ bounds: 1000 280 8 8
+ - image: solid-color(126, 35, 0, 255, 8, 8)
+ bounds: 1008 280 8 8
+ - image: solid-color(127, 35, 0, 255, 8, 8)
+ bounds: 1016 280 8 8
+ - image: solid-color(0, 36, 0, 255, 8, 8)
+ bounds: 0 288 8 8
+ - image: solid-color(1, 36, 0, 255, 8, 8)
+ bounds: 8 288 8 8
+ - image: solid-color(2, 36, 0, 255, 8, 8)
+ bounds: 16 288 8 8
+ - image: solid-color(3, 36, 0, 255, 8, 8)
+ bounds: 24 288 8 8
+ - image: solid-color(4, 36, 0, 255, 8, 8)
+ bounds: 32 288 8 8
+ - image: solid-color(5, 36, 0, 255, 8, 8)
+ bounds: 40 288 8 8
+ - image: solid-color(6, 36, 0, 255, 8, 8)
+ bounds: 48 288 8 8
+ - image: solid-color(7, 36, 0, 255, 8, 8)
+ bounds: 56 288 8 8
+ - image: solid-color(8, 36, 0, 255, 8, 8)
+ bounds: 64 288 8 8
+ - image: solid-color(9, 36, 0, 255, 8, 8)
+ bounds: 72 288 8 8
+ - image: solid-color(10, 36, 0, 255, 8, 8)
+ bounds: 80 288 8 8
+ - image: solid-color(11, 36, 0, 255, 8, 8)
+ bounds: 88 288 8 8
+ - image: solid-color(12, 36, 0, 255, 8, 8)
+ bounds: 96 288 8 8
+ - image: solid-color(13, 36, 0, 255, 8, 8)
+ bounds: 104 288 8 8
+ - image: solid-color(14, 36, 0, 255, 8, 8)
+ bounds: 112 288 8 8
+ - image: solid-color(15, 36, 0, 255, 8, 8)
+ bounds: 120 288 8 8
+ - image: solid-color(16, 36, 0, 255, 8, 8)
+ bounds: 128 288 8 8
+ - image: solid-color(17, 36, 0, 255, 8, 8)
+ bounds: 136 288 8 8
+ - image: solid-color(18, 36, 0, 255, 8, 8)
+ bounds: 144 288 8 8
+ - image: solid-color(19, 36, 0, 255, 8, 8)
+ bounds: 152 288 8 8
+ - image: solid-color(20, 36, 0, 255, 8, 8)
+ bounds: 160 288 8 8
+ - image: solid-color(21, 36, 0, 255, 8, 8)
+ bounds: 168 288 8 8
+ - image: solid-color(22, 36, 0, 255, 8, 8)
+ bounds: 176 288 8 8
+ - image: solid-color(23, 36, 0, 255, 8, 8)
+ bounds: 184 288 8 8
+ - image: solid-color(24, 36, 0, 255, 8, 8)
+ bounds: 192 288 8 8
+ - image: solid-color(25, 36, 0, 255, 8, 8)
+ bounds: 200 288 8 8
+ - image: solid-color(26, 36, 0, 255, 8, 8)
+ bounds: 208 288 8 8
+ - image: solid-color(27, 36, 0, 255, 8, 8)
+ bounds: 216 288 8 8
+ - image: solid-color(28, 36, 0, 255, 8, 8)
+ bounds: 224 288 8 8
+ - image: solid-color(29, 36, 0, 255, 8, 8)
+ bounds: 232 288 8 8
+ - image: solid-color(30, 36, 0, 255, 8, 8)
+ bounds: 240 288 8 8
+ - image: solid-color(31, 36, 0, 255, 8, 8)
+ bounds: 248 288 8 8
+ - image: solid-color(32, 36, 0, 255, 8, 8)
+ bounds: 256 288 8 8
+ - image: solid-color(33, 36, 0, 255, 8, 8)
+ bounds: 264 288 8 8
+ - image: solid-color(34, 36, 0, 255, 8, 8)
+ bounds: 272 288 8 8
+ - image: solid-color(35, 36, 0, 255, 8, 8)
+ bounds: 280 288 8 8
+ - image: solid-color(36, 36, 0, 255, 8, 8)
+ bounds: 288 288 8 8
+ - image: solid-color(37, 36, 0, 255, 8, 8)
+ bounds: 296 288 8 8
+ - image: solid-color(38, 36, 0, 255, 8, 8)
+ bounds: 304 288 8 8
+ - image: solid-color(39, 36, 0, 255, 8, 8)
+ bounds: 312 288 8 8
+ - image: solid-color(40, 36, 0, 255, 8, 8)
+ bounds: 320 288 8 8
+ - image: solid-color(41, 36, 0, 255, 8, 8)
+ bounds: 328 288 8 8
+ - image: solid-color(42, 36, 0, 255, 8, 8)
+ bounds: 336 288 8 8
+ - image: solid-color(43, 36, 0, 255, 8, 8)
+ bounds: 344 288 8 8
+ - image: solid-color(44, 36, 0, 255, 8, 8)
+ bounds: 352 288 8 8
+ - image: solid-color(45, 36, 0, 255, 8, 8)
+ bounds: 360 288 8 8
+ - image: solid-color(46, 36, 0, 255, 8, 8)
+ bounds: 368 288 8 8
+ - image: solid-color(47, 36, 0, 255, 8, 8)
+ bounds: 376 288 8 8
+ - image: solid-color(48, 36, 0, 255, 8, 8)
+ bounds: 384 288 8 8
+ - image: solid-color(49, 36, 0, 255, 8, 8)
+ bounds: 392 288 8 8
+ - image: solid-color(50, 36, 0, 255, 8, 8)
+ bounds: 400 288 8 8
+ - image: solid-color(51, 36, 0, 255, 8, 8)
+ bounds: 408 288 8 8
+ - image: solid-color(52, 36, 0, 255, 8, 8)
+ bounds: 416 288 8 8
+ - image: solid-color(53, 36, 0, 255, 8, 8)
+ bounds: 424 288 8 8
+ - image: solid-color(54, 36, 0, 255, 8, 8)
+ bounds: 432 288 8 8
+ - image: solid-color(55, 36, 0, 255, 8, 8)
+ bounds: 440 288 8 8
+ - image: solid-color(56, 36, 0, 255, 8, 8)
+ bounds: 448 288 8 8
+ - image: solid-color(57, 36, 0, 255, 8, 8)
+ bounds: 456 288 8 8
+ - image: solid-color(58, 36, 0, 255, 8, 8)
+ bounds: 464 288 8 8
+ - image: solid-color(59, 36, 0, 255, 8, 8)
+ bounds: 472 288 8 8
+ - image: solid-color(60, 36, 0, 255, 8, 8)
+ bounds: 480 288 8 8
+ - image: solid-color(61, 36, 0, 255, 8, 8)
+ bounds: 488 288 8 8
+ - image: solid-color(62, 36, 0, 255, 8, 8)
+ bounds: 496 288 8 8
+ - image: solid-color(63, 36, 0, 255, 8, 8)
+ bounds: 504 288 8 8
+ - image: solid-color(64, 36, 0, 255, 8, 8)
+ bounds: 512 288 8 8
+ - image: solid-color(65, 36, 0, 255, 8, 8)
+ bounds: 520 288 8 8
+ - image: solid-color(66, 36, 0, 255, 8, 8)
+ bounds: 528 288 8 8
+ - image: solid-color(67, 36, 0, 255, 8, 8)
+ bounds: 536 288 8 8
+ - image: solid-color(68, 36, 0, 255, 8, 8)
+ bounds: 544 288 8 8
+ - image: solid-color(69, 36, 0, 255, 8, 8)
+ bounds: 552 288 8 8
+ - image: solid-color(70, 36, 0, 255, 8, 8)
+ bounds: 560 288 8 8
+ - image: solid-color(71, 36, 0, 255, 8, 8)
+ bounds: 568 288 8 8
+ - image: solid-color(72, 36, 0, 255, 8, 8)
+ bounds: 576 288 8 8
+ - image: solid-color(73, 36, 0, 255, 8, 8)
+ bounds: 584 288 8 8
+ - image: solid-color(74, 36, 0, 255, 8, 8)
+ bounds: 592 288 8 8
+ - image: solid-color(75, 36, 0, 255, 8, 8)
+ bounds: 600 288 8 8
+ - image: solid-color(76, 36, 0, 255, 8, 8)
+ bounds: 608 288 8 8
+ - image: solid-color(77, 36, 0, 255, 8, 8)
+ bounds: 616 288 8 8
+ - image: solid-color(78, 36, 0, 255, 8, 8)
+ bounds: 624 288 8 8
+ - image: solid-color(79, 36, 0, 255, 8, 8)
+ bounds: 632 288 8 8
+ - image: solid-color(80, 36, 0, 255, 8, 8)
+ bounds: 640 288 8 8
+ - image: solid-color(81, 36, 0, 255, 8, 8)
+ bounds: 648 288 8 8
+ - image: solid-color(82, 36, 0, 255, 8, 8)
+ bounds: 656 288 8 8
+ - image: solid-color(83, 36, 0, 255, 8, 8)
+ bounds: 664 288 8 8
+ - image: solid-color(84, 36, 0, 255, 8, 8)
+ bounds: 672 288 8 8
+ - image: solid-color(85, 36, 0, 255, 8, 8)
+ bounds: 680 288 8 8
+ - image: solid-color(86, 36, 0, 255, 8, 8)
+ bounds: 688 288 8 8
+ - image: solid-color(87, 36, 0, 255, 8, 8)
+ bounds: 696 288 8 8
+ - image: solid-color(88, 36, 0, 255, 8, 8)
+ bounds: 704 288 8 8
+ - image: solid-color(89, 36, 0, 255, 8, 8)
+ bounds: 712 288 8 8
+ - image: solid-color(90, 36, 0, 255, 8, 8)
+ bounds: 720 288 8 8
+ - image: solid-color(91, 36, 0, 255, 8, 8)
+ bounds: 728 288 8 8
+ - image: solid-color(92, 36, 0, 255, 8, 8)
+ bounds: 736 288 8 8
+ - image: solid-color(93, 36, 0, 255, 8, 8)
+ bounds: 744 288 8 8
+ - image: solid-color(94, 36, 0, 255, 8, 8)
+ bounds: 752 288 8 8
+ - image: solid-color(95, 36, 0, 255, 8, 8)
+ bounds: 760 288 8 8
+ - image: solid-color(96, 36, 0, 255, 8, 8)
+ bounds: 768 288 8 8
+ - image: solid-color(97, 36, 0, 255, 8, 8)
+ bounds: 776 288 8 8
+ - image: solid-color(98, 36, 0, 255, 8, 8)
+ bounds: 784 288 8 8
+ - image: solid-color(99, 36, 0, 255, 8, 8)
+ bounds: 792 288 8 8
+ - image: solid-color(100, 36, 0, 255, 8, 8)
+ bounds: 800 288 8 8
+ - image: solid-color(101, 36, 0, 255, 8, 8)
+ bounds: 808 288 8 8
+ - image: solid-color(102, 36, 0, 255, 8, 8)
+ bounds: 816 288 8 8
+ - image: solid-color(103, 36, 0, 255, 8, 8)
+ bounds: 824 288 8 8
+ - image: solid-color(104, 36, 0, 255, 8, 8)
+ bounds: 832 288 8 8
+ - image: solid-color(105, 36, 0, 255, 8, 8)
+ bounds: 840 288 8 8
+ - image: solid-color(106, 36, 0, 255, 8, 8)
+ bounds: 848 288 8 8
+ - image: solid-color(107, 36, 0, 255, 8, 8)
+ bounds: 856 288 8 8
+ - image: solid-color(108, 36, 0, 255, 8, 8)
+ bounds: 864 288 8 8
+ - image: solid-color(109, 36, 0, 255, 8, 8)
+ bounds: 872 288 8 8
+ - image: solid-color(110, 36, 0, 255, 8, 8)
+ bounds: 880 288 8 8
+ - image: solid-color(111, 36, 0, 255, 8, 8)
+ bounds: 888 288 8 8
+ - image: solid-color(112, 36, 0, 255, 8, 8)
+ bounds: 896 288 8 8
+ - image: solid-color(113, 36, 0, 255, 8, 8)
+ bounds: 904 288 8 8
+ - image: solid-color(114, 36, 0, 255, 8, 8)
+ bounds: 912 288 8 8
+ - image: solid-color(115, 36, 0, 255, 8, 8)
+ bounds: 920 288 8 8
+ - image: solid-color(116, 36, 0, 255, 8, 8)
+ bounds: 928 288 8 8
+ - image: solid-color(117, 36, 0, 255, 8, 8)
+ bounds: 936 288 8 8
+ - image: solid-color(118, 36, 0, 255, 8, 8)
+ bounds: 944 288 8 8
+ - image: solid-color(119, 36, 0, 255, 8, 8)
+ bounds: 952 288 8 8
+ - image: solid-color(120, 36, 0, 255, 8, 8)
+ bounds: 960 288 8 8
+ - image: solid-color(121, 36, 0, 255, 8, 8)
+ bounds: 968 288 8 8
+ - image: solid-color(122, 36, 0, 255, 8, 8)
+ bounds: 976 288 8 8
+ - image: solid-color(123, 36, 0, 255, 8, 8)
+ bounds: 984 288 8 8
+ - image: solid-color(124, 36, 0, 255, 8, 8)
+ bounds: 992 288 8 8
+ - image: solid-color(125, 36, 0, 255, 8, 8)
+ bounds: 1000 288 8 8
+ - image: solid-color(126, 36, 0, 255, 8, 8)
+ bounds: 1008 288 8 8
+ - image: solid-color(127, 36, 0, 255, 8, 8)
+ bounds: 1016 288 8 8
+ - image: solid-color(0, 37, 0, 255, 8, 8)
+ bounds: 0 296 8 8
+ - image: solid-color(1, 37, 0, 255, 8, 8)
+ bounds: 8 296 8 8
+ - image: solid-color(2, 37, 0, 255, 8, 8)
+ bounds: 16 296 8 8
+ - image: solid-color(3, 37, 0, 255, 8, 8)
+ bounds: 24 296 8 8
+ - image: solid-color(4, 37, 0, 255, 8, 8)
+ bounds: 32 296 8 8
+ - image: solid-color(5, 37, 0, 255, 8, 8)
+ bounds: 40 296 8 8
+ - image: solid-color(6, 37, 0, 255, 8, 8)
+ bounds: 48 296 8 8
+ - image: solid-color(7, 37, 0, 255, 8, 8)
+ bounds: 56 296 8 8
+ - image: solid-color(8, 37, 0, 255, 8, 8)
+ bounds: 64 296 8 8
+ - image: solid-color(9, 37, 0, 255, 8, 8)
+ bounds: 72 296 8 8
+ - image: solid-color(10, 37, 0, 255, 8, 8)
+ bounds: 80 296 8 8
+ - image: solid-color(11, 37, 0, 255, 8, 8)
+ bounds: 88 296 8 8
+ - image: solid-color(12, 37, 0, 255, 8, 8)
+ bounds: 96 296 8 8
+ - image: solid-color(13, 37, 0, 255, 8, 8)
+ bounds: 104 296 8 8
+ - image: solid-color(14, 37, 0, 255, 8, 8)
+ bounds: 112 296 8 8
+ - image: solid-color(15, 37, 0, 255, 8, 8)
+ bounds: 120 296 8 8
+ - image: solid-color(16, 37, 0, 255, 8, 8)
+ bounds: 128 296 8 8
+ - image: solid-color(17, 37, 0, 255, 8, 8)
+ bounds: 136 296 8 8
+ - image: solid-color(18, 37, 0, 255, 8, 8)
+ bounds: 144 296 8 8
+ - image: solid-color(19, 37, 0, 255, 8, 8)
+ bounds: 152 296 8 8
+ - image: solid-color(20, 37, 0, 255, 8, 8)
+ bounds: 160 296 8 8
+ - image: solid-color(21, 37, 0, 255, 8, 8)
+ bounds: 168 296 8 8
+ - image: solid-color(22, 37, 0, 255, 8, 8)
+ bounds: 176 296 8 8
+ - image: solid-color(23, 37, 0, 255, 8, 8)
+ bounds: 184 296 8 8
+ - image: solid-color(24, 37, 0, 255, 8, 8)
+ bounds: 192 296 8 8
+ - image: solid-color(25, 37, 0, 255, 8, 8)
+ bounds: 200 296 8 8
+ - image: solid-color(26, 37, 0, 255, 8, 8)
+ bounds: 208 296 8 8
+ - image: solid-color(27, 37, 0, 255, 8, 8)
+ bounds: 216 296 8 8
+ - image: solid-color(28, 37, 0, 255, 8, 8)
+ bounds: 224 296 8 8
+ - image: solid-color(29, 37, 0, 255, 8, 8)
+ bounds: 232 296 8 8
+ - image: solid-color(30, 37, 0, 255, 8, 8)
+ bounds: 240 296 8 8
+ - image: solid-color(31, 37, 0, 255, 8, 8)
+ bounds: 248 296 8 8
+ - image: solid-color(32, 37, 0, 255, 8, 8)
+ bounds: 256 296 8 8
+ - image: solid-color(33, 37, 0, 255, 8, 8)
+ bounds: 264 296 8 8
+ - image: solid-color(34, 37, 0, 255, 8, 8)
+ bounds: 272 296 8 8
+ - image: solid-color(35, 37, 0, 255, 8, 8)
+ bounds: 280 296 8 8
+ - image: solid-color(36, 37, 0, 255, 8, 8)
+ bounds: 288 296 8 8
+ - image: solid-color(37, 37, 0, 255, 8, 8)
+ bounds: 296 296 8 8
+ - image: solid-color(38, 37, 0, 255, 8, 8)
+ bounds: 304 296 8 8
+ - image: solid-color(39, 37, 0, 255, 8, 8)
+ bounds: 312 296 8 8
+ - image: solid-color(40, 37, 0, 255, 8, 8)
+ bounds: 320 296 8 8
+ - image: solid-color(41, 37, 0, 255, 8, 8)
+ bounds: 328 296 8 8
+ - image: solid-color(42, 37, 0, 255, 8, 8)
+ bounds: 336 296 8 8
+ - image: solid-color(43, 37, 0, 255, 8, 8)
+ bounds: 344 296 8 8
+ - image: solid-color(44, 37, 0, 255, 8, 8)
+ bounds: 352 296 8 8
+ - image: solid-color(45, 37, 0, 255, 8, 8)
+ bounds: 360 296 8 8
+ - image: solid-color(46, 37, 0, 255, 8, 8)
+ bounds: 368 296 8 8
+ - image: solid-color(47, 37, 0, 255, 8, 8)
+ bounds: 376 296 8 8
+ - image: solid-color(48, 37, 0, 255, 8, 8)
+ bounds: 384 296 8 8
+ - image: solid-color(49, 37, 0, 255, 8, 8)
+ bounds: 392 296 8 8
+ - image: solid-color(50, 37, 0, 255, 8, 8)
+ bounds: 400 296 8 8
+ - image: solid-color(51, 37, 0, 255, 8, 8)
+ bounds: 408 296 8 8
+ - image: solid-color(52, 37, 0, 255, 8, 8)
+ bounds: 416 296 8 8
+ - image: solid-color(53, 37, 0, 255, 8, 8)
+ bounds: 424 296 8 8
+ - image: solid-color(54, 37, 0, 255, 8, 8)
+ bounds: 432 296 8 8
+ - image: solid-color(55, 37, 0, 255, 8, 8)
+ bounds: 440 296 8 8
+ - image: solid-color(56, 37, 0, 255, 8, 8)
+ bounds: 448 296 8 8
+ - image: solid-color(57, 37, 0, 255, 8, 8)
+ bounds: 456 296 8 8
+ - image: solid-color(58, 37, 0, 255, 8, 8)
+ bounds: 464 296 8 8
+ - image: solid-color(59, 37, 0, 255, 8, 8)
+ bounds: 472 296 8 8
+ - image: solid-color(60, 37, 0, 255, 8, 8)
+ bounds: 480 296 8 8
+ - image: solid-color(61, 37, 0, 255, 8, 8)
+ bounds: 488 296 8 8
+ - image: solid-color(62, 37, 0, 255, 8, 8)
+ bounds: 496 296 8 8
+ - image: solid-color(63, 37, 0, 255, 8, 8)
+ bounds: 504 296 8 8
+ - image: solid-color(64, 37, 0, 255, 8, 8)
+ bounds: 512 296 8 8
+ - image: solid-color(65, 37, 0, 255, 8, 8)
+ bounds: 520 296 8 8
+ - image: solid-color(66, 37, 0, 255, 8, 8)
+ bounds: 528 296 8 8
+ - image: solid-color(67, 37, 0, 255, 8, 8)
+ bounds: 536 296 8 8
+ - image: solid-color(68, 37, 0, 255, 8, 8)
+ bounds: 544 296 8 8
+ - image: solid-color(69, 37, 0, 255, 8, 8)
+ bounds: 552 296 8 8
+ - image: solid-color(70, 37, 0, 255, 8, 8)
+ bounds: 560 296 8 8
+ - image: solid-color(71, 37, 0, 255, 8, 8)
+ bounds: 568 296 8 8
+ - image: solid-color(72, 37, 0, 255, 8, 8)
+ bounds: 576 296 8 8
+ - image: solid-color(73, 37, 0, 255, 8, 8)
+ bounds: 584 296 8 8
+ - image: solid-color(74, 37, 0, 255, 8, 8)
+ bounds: 592 296 8 8
+ - image: solid-color(75, 37, 0, 255, 8, 8)
+ bounds: 600 296 8 8
+ - image: solid-color(76, 37, 0, 255, 8, 8)
+ bounds: 608 296 8 8
+ - image: solid-color(77, 37, 0, 255, 8, 8)
+ bounds: 616 296 8 8
+ - image: solid-color(78, 37, 0, 255, 8, 8)
+ bounds: 624 296 8 8
+ - image: solid-color(79, 37, 0, 255, 8, 8)
+ bounds: 632 296 8 8
+ - image: solid-color(80, 37, 0, 255, 8, 8)
+ bounds: 640 296 8 8
+ - image: solid-color(81, 37, 0, 255, 8, 8)
+ bounds: 648 296 8 8
+ - image: solid-color(82, 37, 0, 255, 8, 8)
+ bounds: 656 296 8 8
+ - image: solid-color(83, 37, 0, 255, 8, 8)
+ bounds: 664 296 8 8
+ - image: solid-color(84, 37, 0, 255, 8, 8)
+ bounds: 672 296 8 8
+ - image: solid-color(85, 37, 0, 255, 8, 8)
+ bounds: 680 296 8 8
+ - image: solid-color(86, 37, 0, 255, 8, 8)
+ bounds: 688 296 8 8
+ - image: solid-color(87, 37, 0, 255, 8, 8)
+ bounds: 696 296 8 8
+ - image: solid-color(88, 37, 0, 255, 8, 8)
+ bounds: 704 296 8 8
+ - image: solid-color(89, 37, 0, 255, 8, 8)
+ bounds: 712 296 8 8
+ - image: solid-color(90, 37, 0, 255, 8, 8)
+ bounds: 720 296 8 8
+ - image: solid-color(91, 37, 0, 255, 8, 8)
+ bounds: 728 296 8 8
+ - image: solid-color(92, 37, 0, 255, 8, 8)
+ bounds: 736 296 8 8
+ - image: solid-color(93, 37, 0, 255, 8, 8)
+ bounds: 744 296 8 8
+ - image: solid-color(94, 37, 0, 255, 8, 8)
+ bounds: 752 296 8 8
+ - image: solid-color(95, 37, 0, 255, 8, 8)
+ bounds: 760 296 8 8
+ - image: solid-color(96, 37, 0, 255, 8, 8)
+ bounds: 768 296 8 8
+ - image: solid-color(97, 37, 0, 255, 8, 8)
+ bounds: 776 296 8 8
+ - image: solid-color(98, 37, 0, 255, 8, 8)
+ bounds: 784 296 8 8
+ - image: solid-color(99, 37, 0, 255, 8, 8)
+ bounds: 792 296 8 8
+ - image: solid-color(100, 37, 0, 255, 8, 8)
+ bounds: 800 296 8 8
+ - image: solid-color(101, 37, 0, 255, 8, 8)
+ bounds: 808 296 8 8
+ - image: solid-color(102, 37, 0, 255, 8, 8)
+ bounds: 816 296 8 8
+ - image: solid-color(103, 37, 0, 255, 8, 8)
+ bounds: 824 296 8 8
+ - image: solid-color(104, 37, 0, 255, 8, 8)
+ bounds: 832 296 8 8
+ - image: solid-color(105, 37, 0, 255, 8, 8)
+ bounds: 840 296 8 8
+ - image: solid-color(106, 37, 0, 255, 8, 8)
+ bounds: 848 296 8 8
+ - image: solid-color(107, 37, 0, 255, 8, 8)
+ bounds: 856 296 8 8
+ - image: solid-color(108, 37, 0, 255, 8, 8)
+ bounds: 864 296 8 8
+ - image: solid-color(109, 37, 0, 255, 8, 8)
+ bounds: 872 296 8 8
+ - image: solid-color(110, 37, 0, 255, 8, 8)
+ bounds: 880 296 8 8
+ - image: solid-color(111, 37, 0, 255, 8, 8)
+ bounds: 888 296 8 8
+ - image: solid-color(112, 37, 0, 255, 8, 8)
+ bounds: 896 296 8 8
+ - image: solid-color(113, 37, 0, 255, 8, 8)
+ bounds: 904 296 8 8
+ - image: solid-color(114, 37, 0, 255, 8, 8)
+ bounds: 912 296 8 8
+ - image: solid-color(115, 37, 0, 255, 8, 8)
+ bounds: 920 296 8 8
+ - image: solid-color(116, 37, 0, 255, 8, 8)
+ bounds: 928 296 8 8
+ - image: solid-color(117, 37, 0, 255, 8, 8)
+ bounds: 936 296 8 8
+ - image: solid-color(118, 37, 0, 255, 8, 8)
+ bounds: 944 296 8 8
+ - image: solid-color(119, 37, 0, 255, 8, 8)
+ bounds: 952 296 8 8
+ - image: solid-color(120, 37, 0, 255, 8, 8)
+ bounds: 960 296 8 8
+ - image: solid-color(121, 37, 0, 255, 8, 8)
+ bounds: 968 296 8 8
+ - image: solid-color(122, 37, 0, 255, 8, 8)
+ bounds: 976 296 8 8
+ - image: solid-color(123, 37, 0, 255, 8, 8)
+ bounds: 984 296 8 8
+ - image: solid-color(124, 37, 0, 255, 8, 8)
+ bounds: 992 296 8 8
+ - image: solid-color(125, 37, 0, 255, 8, 8)
+ bounds: 1000 296 8 8
+ - image: solid-color(126, 37, 0, 255, 8, 8)
+ bounds: 1008 296 8 8
+ - image: solid-color(127, 37, 0, 255, 8, 8)
+ bounds: 1016 296 8 8
+ - image: solid-color(0, 38, 0, 255, 8, 8)
+ bounds: 0 304 8 8
+ - image: solid-color(1, 38, 0, 255, 8, 8)
+ bounds: 8 304 8 8
+ - image: solid-color(2, 38, 0, 255, 8, 8)
+ bounds: 16 304 8 8
+ - image: solid-color(3, 38, 0, 255, 8, 8)
+ bounds: 24 304 8 8
+ - image: solid-color(4, 38, 0, 255, 8, 8)
+ bounds: 32 304 8 8
+ - image: solid-color(5, 38, 0, 255, 8, 8)
+ bounds: 40 304 8 8
+ - image: solid-color(6, 38, 0, 255, 8, 8)
+ bounds: 48 304 8 8
+ - image: solid-color(7, 38, 0, 255, 8, 8)
+ bounds: 56 304 8 8
+ - image: solid-color(8, 38, 0, 255, 8, 8)
+ bounds: 64 304 8 8
+ - image: solid-color(9, 38, 0, 255, 8, 8)
+ bounds: 72 304 8 8
+ - image: solid-color(10, 38, 0, 255, 8, 8)
+ bounds: 80 304 8 8
+ - image: solid-color(11, 38, 0, 255, 8, 8)
+ bounds: 88 304 8 8
+ - image: solid-color(12, 38, 0, 255, 8, 8)
+ bounds: 96 304 8 8
+ - image: solid-color(13, 38, 0, 255, 8, 8)
+ bounds: 104 304 8 8
+ - image: solid-color(14, 38, 0, 255, 8, 8)
+ bounds: 112 304 8 8
+ - image: solid-color(15, 38, 0, 255, 8, 8)
+ bounds: 120 304 8 8
+ - image: solid-color(16, 38, 0, 255, 8, 8)
+ bounds: 128 304 8 8
+ - image: solid-color(17, 38, 0, 255, 8, 8)
+ bounds: 136 304 8 8
+ - image: solid-color(18, 38, 0, 255, 8, 8)
+ bounds: 144 304 8 8
+ - image: solid-color(19, 38, 0, 255, 8, 8)
+ bounds: 152 304 8 8
+ - image: solid-color(20, 38, 0, 255, 8, 8)
+ bounds: 160 304 8 8
+ - image: solid-color(21, 38, 0, 255, 8, 8)
+ bounds: 168 304 8 8
+ - image: solid-color(22, 38, 0, 255, 8, 8)
+ bounds: 176 304 8 8
+ - image: solid-color(23, 38, 0, 255, 8, 8)
+ bounds: 184 304 8 8
+ - image: solid-color(24, 38, 0, 255, 8, 8)
+ bounds: 192 304 8 8
+ - image: solid-color(25, 38, 0, 255, 8, 8)
+ bounds: 200 304 8 8
+ - image: solid-color(26, 38, 0, 255, 8, 8)
+ bounds: 208 304 8 8
+ - image: solid-color(27, 38, 0, 255, 8, 8)
+ bounds: 216 304 8 8
+ - image: solid-color(28, 38, 0, 255, 8, 8)
+ bounds: 224 304 8 8
+ - image: solid-color(29, 38, 0, 255, 8, 8)
+ bounds: 232 304 8 8
+ - image: solid-color(30, 38, 0, 255, 8, 8)
+ bounds: 240 304 8 8
+ - image: solid-color(31, 38, 0, 255, 8, 8)
+ bounds: 248 304 8 8
+ - image: solid-color(32, 38, 0, 255, 8, 8)
+ bounds: 256 304 8 8
+ - image: solid-color(33, 38, 0, 255, 8, 8)
+ bounds: 264 304 8 8
+ - image: solid-color(34, 38, 0, 255, 8, 8)
+ bounds: 272 304 8 8
+ - image: solid-color(35, 38, 0, 255, 8, 8)
+ bounds: 280 304 8 8
+ - image: solid-color(36, 38, 0, 255, 8, 8)
+ bounds: 288 304 8 8
+ - image: solid-color(37, 38, 0, 255, 8, 8)
+ bounds: 296 304 8 8
+ - image: solid-color(38, 38, 0, 255, 8, 8)
+ bounds: 304 304 8 8
+ - image: solid-color(39, 38, 0, 255, 8, 8)
+ bounds: 312 304 8 8
+ - image: solid-color(40, 38, 0, 255, 8, 8)
+ bounds: 320 304 8 8
+ - image: solid-color(41, 38, 0, 255, 8, 8)
+ bounds: 328 304 8 8
+ - image: solid-color(42, 38, 0, 255, 8, 8)
+ bounds: 336 304 8 8
+ - image: solid-color(43, 38, 0, 255, 8, 8)
+ bounds: 344 304 8 8
+ - image: solid-color(44, 38, 0, 255, 8, 8)
+ bounds: 352 304 8 8
+ - image: solid-color(45, 38, 0, 255, 8, 8)
+ bounds: 360 304 8 8
+ - image: solid-color(46, 38, 0, 255, 8, 8)
+ bounds: 368 304 8 8
+ - image: solid-color(47, 38, 0, 255, 8, 8)
+ bounds: 376 304 8 8
+ - image: solid-color(48, 38, 0, 255, 8, 8)
+ bounds: 384 304 8 8
+ - image: solid-color(49, 38, 0, 255, 8, 8)
+ bounds: 392 304 8 8
+ - image: solid-color(50, 38, 0, 255, 8, 8)
+ bounds: 400 304 8 8
+ - image: solid-color(51, 38, 0, 255, 8, 8)
+ bounds: 408 304 8 8
+ - image: solid-color(52, 38, 0, 255, 8, 8)
+ bounds: 416 304 8 8
+ - image: solid-color(53, 38, 0, 255, 8, 8)
+ bounds: 424 304 8 8
+ - image: solid-color(54, 38, 0, 255, 8, 8)
+ bounds: 432 304 8 8
+ - image: solid-color(55, 38, 0, 255, 8, 8)
+ bounds: 440 304 8 8
+ - image: solid-color(56, 38, 0, 255, 8, 8)
+ bounds: 448 304 8 8
+ - image: solid-color(57, 38, 0, 255, 8, 8)
+ bounds: 456 304 8 8
+ - image: solid-color(58, 38, 0, 255, 8, 8)
+ bounds: 464 304 8 8
+ - image: solid-color(59, 38, 0, 255, 8, 8)
+ bounds: 472 304 8 8
+ - image: solid-color(60, 38, 0, 255, 8, 8)
+ bounds: 480 304 8 8
+ - image: solid-color(61, 38, 0, 255, 8, 8)
+ bounds: 488 304 8 8
+ - image: solid-color(62, 38, 0, 255, 8, 8)
+ bounds: 496 304 8 8
+ - image: solid-color(63, 38, 0, 255, 8, 8)
+ bounds: 504 304 8 8
+ - image: solid-color(64, 38, 0, 255, 8, 8)
+ bounds: 512 304 8 8
+ - image: solid-color(65, 38, 0, 255, 8, 8)
+ bounds: 520 304 8 8
+ - image: solid-color(66, 38, 0, 255, 8, 8)
+ bounds: 528 304 8 8
+ - image: solid-color(67, 38, 0, 255, 8, 8)
+ bounds: 536 304 8 8
+ - image: solid-color(68, 38, 0, 255, 8, 8)
+ bounds: 544 304 8 8
+ - image: solid-color(69, 38, 0, 255, 8, 8)
+ bounds: 552 304 8 8
+ - image: solid-color(70, 38, 0, 255, 8, 8)
+ bounds: 560 304 8 8
+ - image: solid-color(71, 38, 0, 255, 8, 8)
+ bounds: 568 304 8 8
+ - image: solid-color(72, 38, 0, 255, 8, 8)
+ bounds: 576 304 8 8
+ - image: solid-color(73, 38, 0, 255, 8, 8)
+ bounds: 584 304 8 8
+ - image: solid-color(74, 38, 0, 255, 8, 8)
+ bounds: 592 304 8 8
+ - image: solid-color(75, 38, 0, 255, 8, 8)
+ bounds: 600 304 8 8
+ - image: solid-color(76, 38, 0, 255, 8, 8)
+ bounds: 608 304 8 8
+ - image: solid-color(77, 38, 0, 255, 8, 8)
+ bounds: 616 304 8 8
+ - image: solid-color(78, 38, 0, 255, 8, 8)
+ bounds: 624 304 8 8
+ - image: solid-color(79, 38, 0, 255, 8, 8)
+ bounds: 632 304 8 8
+ - image: solid-color(80, 38, 0, 255, 8, 8)
+ bounds: 640 304 8 8
+ - image: solid-color(81, 38, 0, 255, 8, 8)
+ bounds: 648 304 8 8
+ - image: solid-color(82, 38, 0, 255, 8, 8)
+ bounds: 656 304 8 8
+ - image: solid-color(83, 38, 0, 255, 8, 8)
+ bounds: 664 304 8 8
+ - image: solid-color(84, 38, 0, 255, 8, 8)
+ bounds: 672 304 8 8
+ - image: solid-color(85, 38, 0, 255, 8, 8)
+ bounds: 680 304 8 8
+ - image: solid-color(86, 38, 0, 255, 8, 8)
+ bounds: 688 304 8 8
+ - image: solid-color(87, 38, 0, 255, 8, 8)
+ bounds: 696 304 8 8
+ - image: solid-color(88, 38, 0, 255, 8, 8)
+ bounds: 704 304 8 8
+ - image: solid-color(89, 38, 0, 255, 8, 8)
+ bounds: 712 304 8 8
+ - image: solid-color(90, 38, 0, 255, 8, 8)
+ bounds: 720 304 8 8
+ - image: solid-color(91, 38, 0, 255, 8, 8)
+ bounds: 728 304 8 8
+ - image: solid-color(92, 38, 0, 255, 8, 8)
+ bounds: 736 304 8 8
+ - image: solid-color(93, 38, 0, 255, 8, 8)
+ bounds: 744 304 8 8
+ - image: solid-color(94, 38, 0, 255, 8, 8)
+ bounds: 752 304 8 8
+ - image: solid-color(95, 38, 0, 255, 8, 8)
+ bounds: 760 304 8 8
+ - image: solid-color(96, 38, 0, 255, 8, 8)
+ bounds: 768 304 8 8
+ - image: solid-color(97, 38, 0, 255, 8, 8)
+ bounds: 776 304 8 8
+ - image: solid-color(98, 38, 0, 255, 8, 8)
+ bounds: 784 304 8 8
+ - image: solid-color(99, 38, 0, 255, 8, 8)
+ bounds: 792 304 8 8
+ - image: solid-color(100, 38, 0, 255, 8, 8)
+ bounds: 800 304 8 8
+ - image: solid-color(101, 38, 0, 255, 8, 8)
+ bounds: 808 304 8 8
+ - image: solid-color(102, 38, 0, 255, 8, 8)
+ bounds: 816 304 8 8
+ - image: solid-color(103, 38, 0, 255, 8, 8)
+ bounds: 824 304 8 8
+ - image: solid-color(104, 38, 0, 255, 8, 8)
+ bounds: 832 304 8 8
+ - image: solid-color(105, 38, 0, 255, 8, 8)
+ bounds: 840 304 8 8
+ - image: solid-color(106, 38, 0, 255, 8, 8)
+ bounds: 848 304 8 8
+ - image: solid-color(107, 38, 0, 255, 8, 8)
+ bounds: 856 304 8 8
+ - image: solid-color(108, 38, 0, 255, 8, 8)
+ bounds: 864 304 8 8
+ - image: solid-color(109, 38, 0, 255, 8, 8)
+ bounds: 872 304 8 8
+ - image: solid-color(110, 38, 0, 255, 8, 8)
+ bounds: 880 304 8 8
+ - image: solid-color(111, 38, 0, 255, 8, 8)
+ bounds: 888 304 8 8
+ - image: solid-color(112, 38, 0, 255, 8, 8)
+ bounds: 896 304 8 8
+ - image: solid-color(113, 38, 0, 255, 8, 8)
+ bounds: 904 304 8 8
+ - image: solid-color(114, 38, 0, 255, 8, 8)
+ bounds: 912 304 8 8
+ - image: solid-color(115, 38, 0, 255, 8, 8)
+ bounds: 920 304 8 8
+ - image: solid-color(116, 38, 0, 255, 8, 8)
+ bounds: 928 304 8 8
+ - image: solid-color(117, 38, 0, 255, 8, 8)
+ bounds: 936 304 8 8
+ - image: solid-color(118, 38, 0, 255, 8, 8)
+ bounds: 944 304 8 8
+ - image: solid-color(119, 38, 0, 255, 8, 8)
+ bounds: 952 304 8 8
+ - image: solid-color(120, 38, 0, 255, 8, 8)
+ bounds: 960 304 8 8
+ - image: solid-color(121, 38, 0, 255, 8, 8)
+ bounds: 968 304 8 8
+ - image: solid-color(122, 38, 0, 255, 8, 8)
+ bounds: 976 304 8 8
+ - image: solid-color(123, 38, 0, 255, 8, 8)
+ bounds: 984 304 8 8
+ - image: solid-color(124, 38, 0, 255, 8, 8)
+ bounds: 992 304 8 8
+ - image: solid-color(125, 38, 0, 255, 8, 8)
+ bounds: 1000 304 8 8
+ - image: solid-color(126, 38, 0, 255, 8, 8)
+ bounds: 1008 304 8 8
+ - image: solid-color(127, 38, 0, 255, 8, 8)
+ bounds: 1016 304 8 8
+ - image: solid-color(0, 39, 0, 255, 8, 8)
+ bounds: 0 312 8 8
+ - image: solid-color(1, 39, 0, 255, 8, 8)
+ bounds: 8 312 8 8
+ - image: solid-color(2, 39, 0, 255, 8, 8)
+ bounds: 16 312 8 8
+ - image: solid-color(3, 39, 0, 255, 8, 8)
+ bounds: 24 312 8 8
+ - image: solid-color(4, 39, 0, 255, 8, 8)
+ bounds: 32 312 8 8
+ - image: solid-color(5, 39, 0, 255, 8, 8)
+ bounds: 40 312 8 8
+ - image: solid-color(6, 39, 0, 255, 8, 8)
+ bounds: 48 312 8 8
+ - image: solid-color(7, 39, 0, 255, 8, 8)
+ bounds: 56 312 8 8
+ - image: solid-color(8, 39, 0, 255, 8, 8)
+ bounds: 64 312 8 8
+ - image: solid-color(9, 39, 0, 255, 8, 8)
+ bounds: 72 312 8 8
+ - image: solid-color(10, 39, 0, 255, 8, 8)
+ bounds: 80 312 8 8
+ - image: solid-color(11, 39, 0, 255, 8, 8)
+ bounds: 88 312 8 8
+ - image: solid-color(12, 39, 0, 255, 8, 8)
+ bounds: 96 312 8 8
+ - image: solid-color(13, 39, 0, 255, 8, 8)
+ bounds: 104 312 8 8
+ - image: solid-color(14, 39, 0, 255, 8, 8)
+ bounds: 112 312 8 8
+ - image: solid-color(15, 39, 0, 255, 8, 8)
+ bounds: 120 312 8 8
+ - image: solid-color(16, 39, 0, 255, 8, 8)
+ bounds: 128 312 8 8
+ - image: solid-color(17, 39, 0, 255, 8, 8)
+ bounds: 136 312 8 8
+ - image: solid-color(18, 39, 0, 255, 8, 8)
+ bounds: 144 312 8 8
+ - image: solid-color(19, 39, 0, 255, 8, 8)
+ bounds: 152 312 8 8
+ - image: solid-color(20, 39, 0, 255, 8, 8)
+ bounds: 160 312 8 8
+ - image: solid-color(21, 39, 0, 255, 8, 8)
+ bounds: 168 312 8 8
+ - image: solid-color(22, 39, 0, 255, 8, 8)
+ bounds: 176 312 8 8
+ - image: solid-color(23, 39, 0, 255, 8, 8)
+ bounds: 184 312 8 8
+ - image: solid-color(24, 39, 0, 255, 8, 8)
+ bounds: 192 312 8 8
+ - image: solid-color(25, 39, 0, 255, 8, 8)
+ bounds: 200 312 8 8
+ - image: solid-color(26, 39, 0, 255, 8, 8)
+ bounds: 208 312 8 8
+ - image: solid-color(27, 39, 0, 255, 8, 8)
+ bounds: 216 312 8 8
+ - image: solid-color(28, 39, 0, 255, 8, 8)
+ bounds: 224 312 8 8
+ - image: solid-color(29, 39, 0, 255, 8, 8)
+ bounds: 232 312 8 8
+ - image: solid-color(30, 39, 0, 255, 8, 8)
+ bounds: 240 312 8 8
+ - image: solid-color(31, 39, 0, 255, 8, 8)
+ bounds: 248 312 8 8
+ - image: solid-color(32, 39, 0, 255, 8, 8)
+ bounds: 256 312 8 8
+ - image: solid-color(33, 39, 0, 255, 8, 8)
+ bounds: 264 312 8 8
+ - image: solid-color(34, 39, 0, 255, 8, 8)
+ bounds: 272 312 8 8
+ - image: solid-color(35, 39, 0, 255, 8, 8)
+ bounds: 280 312 8 8
+ - image: solid-color(36, 39, 0, 255, 8, 8)
+ bounds: 288 312 8 8
+ - image: solid-color(37, 39, 0, 255, 8, 8)
+ bounds: 296 312 8 8
+ - image: solid-color(38, 39, 0, 255, 8, 8)
+ bounds: 304 312 8 8
+ - image: solid-color(39, 39, 0, 255, 8, 8)
+ bounds: 312 312 8 8
+ - image: solid-color(40, 39, 0, 255, 8, 8)
+ bounds: 320 312 8 8
+ - image: solid-color(41, 39, 0, 255, 8, 8)
+ bounds: 328 312 8 8
+ - image: solid-color(42, 39, 0, 255, 8, 8)
+ bounds: 336 312 8 8
+ - image: solid-color(43, 39, 0, 255, 8, 8)
+ bounds: 344 312 8 8
+ - image: solid-color(44, 39, 0, 255, 8, 8)
+ bounds: 352 312 8 8
+ - image: solid-color(45, 39, 0, 255, 8, 8)
+ bounds: 360 312 8 8
+ - image: solid-color(46, 39, 0, 255, 8, 8)
+ bounds: 368 312 8 8
+ - image: solid-color(47, 39, 0, 255, 8, 8)
+ bounds: 376 312 8 8
+ - image: solid-color(48, 39, 0, 255, 8, 8)
+ bounds: 384 312 8 8
+ - image: solid-color(49, 39, 0, 255, 8, 8)
+ bounds: 392 312 8 8
+ - image: solid-color(50, 39, 0, 255, 8, 8)
+ bounds: 400 312 8 8
+ - image: solid-color(51, 39, 0, 255, 8, 8)
+ bounds: 408 312 8 8
+ - image: solid-color(52, 39, 0, 255, 8, 8)
+ bounds: 416 312 8 8
+ - image: solid-color(53, 39, 0, 255, 8, 8)
+ bounds: 424 312 8 8
+ - image: solid-color(54, 39, 0, 255, 8, 8)
+ bounds: 432 312 8 8
+ - image: solid-color(55, 39, 0, 255, 8, 8)
+ bounds: 440 312 8 8
+ - image: solid-color(56, 39, 0, 255, 8, 8)
+ bounds: 448 312 8 8
+ - image: solid-color(57, 39, 0, 255, 8, 8)
+ bounds: 456 312 8 8
+ - image: solid-color(58, 39, 0, 255, 8, 8)
+ bounds: 464 312 8 8
+ - image: solid-color(59, 39, 0, 255, 8, 8)
+ bounds: 472 312 8 8
+ - image: solid-color(60, 39, 0, 255, 8, 8)
+ bounds: 480 312 8 8
+ - image: solid-color(61, 39, 0, 255, 8, 8)
+ bounds: 488 312 8 8
+ - image: solid-color(62, 39, 0, 255, 8, 8)
+ bounds: 496 312 8 8
+ - image: solid-color(63, 39, 0, 255, 8, 8)
+ bounds: 504 312 8 8
+ - image: solid-color(64, 39, 0, 255, 8, 8)
+ bounds: 512 312 8 8
+ - image: solid-color(65, 39, 0, 255, 8, 8)
+ bounds: 520 312 8 8
+ - image: solid-color(66, 39, 0, 255, 8, 8)
+ bounds: 528 312 8 8
+ - image: solid-color(67, 39, 0, 255, 8, 8)
+ bounds: 536 312 8 8
+ - image: solid-color(68, 39, 0, 255, 8, 8)
+ bounds: 544 312 8 8
+ - image: solid-color(69, 39, 0, 255, 8, 8)
+ bounds: 552 312 8 8
+ - image: solid-color(70, 39, 0, 255, 8, 8)
+ bounds: 560 312 8 8
+ - image: solid-color(71, 39, 0, 255, 8, 8)
+ bounds: 568 312 8 8
+ - image: solid-color(72, 39, 0, 255, 8, 8)
+ bounds: 576 312 8 8
+ - image: solid-color(73, 39, 0, 255, 8, 8)
+ bounds: 584 312 8 8
+ - image: solid-color(74, 39, 0, 255, 8, 8)
+ bounds: 592 312 8 8
+ - image: solid-color(75, 39, 0, 255, 8, 8)
+ bounds: 600 312 8 8
+ - image: solid-color(76, 39, 0, 255, 8, 8)
+ bounds: 608 312 8 8
+ - image: solid-color(77, 39, 0, 255, 8, 8)
+ bounds: 616 312 8 8
+ - image: solid-color(78, 39, 0, 255, 8, 8)
+ bounds: 624 312 8 8
+ - image: solid-color(79, 39, 0, 255, 8, 8)
+ bounds: 632 312 8 8
+ - image: solid-color(80, 39, 0, 255, 8, 8)
+ bounds: 640 312 8 8
+ - image: solid-color(81, 39, 0, 255, 8, 8)
+ bounds: 648 312 8 8
+ - image: solid-color(82, 39, 0, 255, 8, 8)
+ bounds: 656 312 8 8
+ - image: solid-color(83, 39, 0, 255, 8, 8)
+ bounds: 664 312 8 8
+ - image: solid-color(84, 39, 0, 255, 8, 8)
+ bounds: 672 312 8 8
+ - image: solid-color(85, 39, 0, 255, 8, 8)
+ bounds: 680 312 8 8
+ - image: solid-color(86, 39, 0, 255, 8, 8)
+ bounds: 688 312 8 8
+ - image: solid-color(87, 39, 0, 255, 8, 8)
+ bounds: 696 312 8 8
+ - image: solid-color(88, 39, 0, 255, 8, 8)
+ bounds: 704 312 8 8
+ - image: solid-color(89, 39, 0, 255, 8, 8)
+ bounds: 712 312 8 8
+ - image: solid-color(90, 39, 0, 255, 8, 8)
+ bounds: 720 312 8 8
+ - image: solid-color(91, 39, 0, 255, 8, 8)
+ bounds: 728 312 8 8
+ - image: solid-color(92, 39, 0, 255, 8, 8)
+ bounds: 736 312 8 8
+ - image: solid-color(93, 39, 0, 255, 8, 8)
+ bounds: 744 312 8 8
+ - image: solid-color(94, 39, 0, 255, 8, 8)
+ bounds: 752 312 8 8
+ - image: solid-color(95, 39, 0, 255, 8, 8)
+ bounds: 760 312 8 8
+ - image: solid-color(96, 39, 0, 255, 8, 8)
+ bounds: 768 312 8 8
+ - image: solid-color(97, 39, 0, 255, 8, 8)
+ bounds: 776 312 8 8
+ - image: solid-color(98, 39, 0, 255, 8, 8)
+ bounds: 784 312 8 8
+ - image: solid-color(99, 39, 0, 255, 8, 8)
+ bounds: 792 312 8 8
+ - image: solid-color(100, 39, 0, 255, 8, 8)
+ bounds: 800 312 8 8
+ - image: solid-color(101, 39, 0, 255, 8, 8)
+ bounds: 808 312 8 8
+ - image: solid-color(102, 39, 0, 255, 8, 8)
+ bounds: 816 312 8 8
+ - image: solid-color(103, 39, 0, 255, 8, 8)
+ bounds: 824 312 8 8
+ - image: solid-color(104, 39, 0, 255, 8, 8)
+ bounds: 832 312 8 8
+ - image: solid-color(105, 39, 0, 255, 8, 8)
+ bounds: 840 312 8 8
+ - image: solid-color(106, 39, 0, 255, 8, 8)
+ bounds: 848 312 8 8
+ - image: solid-color(107, 39, 0, 255, 8, 8)
+ bounds: 856 312 8 8
+ - image: solid-color(108, 39, 0, 255, 8, 8)
+ bounds: 864 312 8 8
+ - image: solid-color(109, 39, 0, 255, 8, 8)
+ bounds: 872 312 8 8
+ - image: solid-color(110, 39, 0, 255, 8, 8)
+ bounds: 880 312 8 8
+ - image: solid-color(111, 39, 0, 255, 8, 8)
+ bounds: 888 312 8 8
+ - image: solid-color(112, 39, 0, 255, 8, 8)
+ bounds: 896 312 8 8
+ - image: solid-color(113, 39, 0, 255, 8, 8)
+ bounds: 904 312 8 8
+ - image: solid-color(114, 39, 0, 255, 8, 8)
+ bounds: 912 312 8 8
+ - image: solid-color(115, 39, 0, 255, 8, 8)
+ bounds: 920 312 8 8
+ - image: solid-color(116, 39, 0, 255, 8, 8)
+ bounds: 928 312 8 8
+ - image: solid-color(117, 39, 0, 255, 8, 8)
+ bounds: 936 312 8 8
+ - image: solid-color(118, 39, 0, 255, 8, 8)
+ bounds: 944 312 8 8
+ - image: solid-color(119, 39, 0, 255, 8, 8)
+ bounds: 952 312 8 8
+ - image: solid-color(120, 39, 0, 255, 8, 8)
+ bounds: 960 312 8 8
+ - image: solid-color(121, 39, 0, 255, 8, 8)
+ bounds: 968 312 8 8
+ - image: solid-color(122, 39, 0, 255, 8, 8)
+ bounds: 976 312 8 8
+ - image: solid-color(123, 39, 0, 255, 8, 8)
+ bounds: 984 312 8 8
+ - image: solid-color(124, 39, 0, 255, 8, 8)
+ bounds: 992 312 8 8
+ - image: solid-color(125, 39, 0, 255, 8, 8)
+ bounds: 1000 312 8 8
+ - image: solid-color(126, 39, 0, 255, 8, 8)
+ bounds: 1008 312 8 8
+ - image: solid-color(127, 39, 0, 255, 8, 8)
+ bounds: 1016 312 8 8
+ - image: solid-color(0, 40, 0, 255, 8, 8)
+ bounds: 0 320 8 8
+ - image: solid-color(1, 40, 0, 255, 8, 8)
+ bounds: 8 320 8 8
+ - image: solid-color(2, 40, 0, 255, 8, 8)
+ bounds: 16 320 8 8
+ - image: solid-color(3, 40, 0, 255, 8, 8)
+ bounds: 24 320 8 8
+ - image: solid-color(4, 40, 0, 255, 8, 8)
+ bounds: 32 320 8 8
+ - image: solid-color(5, 40, 0, 255, 8, 8)
+ bounds: 40 320 8 8
+ - image: solid-color(6, 40, 0, 255, 8, 8)
+ bounds: 48 320 8 8
+ - image: solid-color(7, 40, 0, 255, 8, 8)
+ bounds: 56 320 8 8
+ - image: solid-color(8, 40, 0, 255, 8, 8)
+ bounds: 64 320 8 8
+ - image: solid-color(9, 40, 0, 255, 8, 8)
+ bounds: 72 320 8 8
+ - image: solid-color(10, 40, 0, 255, 8, 8)
+ bounds: 80 320 8 8
+ - image: solid-color(11, 40, 0, 255, 8, 8)
+ bounds: 88 320 8 8
+ - image: solid-color(12, 40, 0, 255, 8, 8)
+ bounds: 96 320 8 8
+ - image: solid-color(13, 40, 0, 255, 8, 8)
+ bounds: 104 320 8 8
+ - image: solid-color(14, 40, 0, 255, 8, 8)
+ bounds: 112 320 8 8
+ - image: solid-color(15, 40, 0, 255, 8, 8)
+ bounds: 120 320 8 8
+ - image: solid-color(16, 40, 0, 255, 8, 8)
+ bounds: 128 320 8 8
+ - image: solid-color(17, 40, 0, 255, 8, 8)
+ bounds: 136 320 8 8
+ - image: solid-color(18, 40, 0, 255, 8, 8)
+ bounds: 144 320 8 8
+ - image: solid-color(19, 40, 0, 255, 8, 8)
+ bounds: 152 320 8 8
+ - image: solid-color(20, 40, 0, 255, 8, 8)
+ bounds: 160 320 8 8
+ - image: solid-color(21, 40, 0, 255, 8, 8)
+ bounds: 168 320 8 8
+ - image: solid-color(22, 40, 0, 255, 8, 8)
+ bounds: 176 320 8 8
+ - image: solid-color(23, 40, 0, 255, 8, 8)
+ bounds: 184 320 8 8
+ - image: solid-color(24, 40, 0, 255, 8, 8)
+ bounds: 192 320 8 8
+ - image: solid-color(25, 40, 0, 255, 8, 8)
+ bounds: 200 320 8 8
+ - image: solid-color(26, 40, 0, 255, 8, 8)
+ bounds: 208 320 8 8
+ - image: solid-color(27, 40, 0, 255, 8, 8)
+ bounds: 216 320 8 8
+ - image: solid-color(28, 40, 0, 255, 8, 8)
+ bounds: 224 320 8 8
+ - image: solid-color(29, 40, 0, 255, 8, 8)
+ bounds: 232 320 8 8
+ - image: solid-color(30, 40, 0, 255, 8, 8)
+ bounds: 240 320 8 8
+ - image: solid-color(31, 40, 0, 255, 8, 8)
+ bounds: 248 320 8 8
+ - image: solid-color(32, 40, 0, 255, 8, 8)
+ bounds: 256 320 8 8
+ - image: solid-color(33, 40, 0, 255, 8, 8)
+ bounds: 264 320 8 8
+ - image: solid-color(34, 40, 0, 255, 8, 8)
+ bounds: 272 320 8 8
+ - image: solid-color(35, 40, 0, 255, 8, 8)
+ bounds: 280 320 8 8
+ - image: solid-color(36, 40, 0, 255, 8, 8)
+ bounds: 288 320 8 8
+ - image: solid-color(37, 40, 0, 255, 8, 8)
+ bounds: 296 320 8 8
+ - image: solid-color(38, 40, 0, 255, 8, 8)
+ bounds: 304 320 8 8
+ - image: solid-color(39, 40, 0, 255, 8, 8)
+ bounds: 312 320 8 8
+ - image: solid-color(40, 40, 0, 255, 8, 8)
+ bounds: 320 320 8 8
+ - image: solid-color(41, 40, 0, 255, 8, 8)
+ bounds: 328 320 8 8
+ - image: solid-color(42, 40, 0, 255, 8, 8)
+ bounds: 336 320 8 8
+ - image: solid-color(43, 40, 0, 255, 8, 8)
+ bounds: 344 320 8 8
+ - image: solid-color(44, 40, 0, 255, 8, 8)
+ bounds: 352 320 8 8
+ - image: solid-color(45, 40, 0, 255, 8, 8)
+ bounds: 360 320 8 8
+ - image: solid-color(46, 40, 0, 255, 8, 8)
+ bounds: 368 320 8 8
+ - image: solid-color(47, 40, 0, 255, 8, 8)
+ bounds: 376 320 8 8
+ - image: solid-color(48, 40, 0, 255, 8, 8)
+ bounds: 384 320 8 8
+ - image: solid-color(49, 40, 0, 255, 8, 8)
+ bounds: 392 320 8 8
+ - image: solid-color(50, 40, 0, 255, 8, 8)
+ bounds: 400 320 8 8
+ - image: solid-color(51, 40, 0, 255, 8, 8)
+ bounds: 408 320 8 8
+ - image: solid-color(52, 40, 0, 255, 8, 8)
+ bounds: 416 320 8 8
+ - image: solid-color(53, 40, 0, 255, 8, 8)
+ bounds: 424 320 8 8
+ - image: solid-color(54, 40, 0, 255, 8, 8)
+ bounds: 432 320 8 8
+ - image: solid-color(55, 40, 0, 255, 8, 8)
+ bounds: 440 320 8 8
+ - image: solid-color(56, 40, 0, 255, 8, 8)
+ bounds: 448 320 8 8
+ - image: solid-color(57, 40, 0, 255, 8, 8)
+ bounds: 456 320 8 8
+ - image: solid-color(58, 40, 0, 255, 8, 8)
+ bounds: 464 320 8 8
+ - image: solid-color(59, 40, 0, 255, 8, 8)
+ bounds: 472 320 8 8
+ - image: solid-color(60, 40, 0, 255, 8, 8)
+ bounds: 480 320 8 8
+ - image: solid-color(61, 40, 0, 255, 8, 8)
+ bounds: 488 320 8 8
+ - image: solid-color(62, 40, 0, 255, 8, 8)
+ bounds: 496 320 8 8
+ - image: solid-color(63, 40, 0, 255, 8, 8)
+ bounds: 504 320 8 8
+ - image: solid-color(64, 40, 0, 255, 8, 8)
+ bounds: 512 320 8 8
+ - image: solid-color(65, 40, 0, 255, 8, 8)
+ bounds: 520 320 8 8
+ - image: solid-color(66, 40, 0, 255, 8, 8)
+ bounds: 528 320 8 8
+ - image: solid-color(67, 40, 0, 255, 8, 8)
+ bounds: 536 320 8 8
+ - image: solid-color(68, 40, 0, 255, 8, 8)
+ bounds: 544 320 8 8
+ - image: solid-color(69, 40, 0, 255, 8, 8)
+ bounds: 552 320 8 8
+ - image: solid-color(70, 40, 0, 255, 8, 8)
+ bounds: 560 320 8 8
+ - image: solid-color(71, 40, 0, 255, 8, 8)
+ bounds: 568 320 8 8
+ - image: solid-color(72, 40, 0, 255, 8, 8)
+ bounds: 576 320 8 8
+ - image: solid-color(73, 40, 0, 255, 8, 8)
+ bounds: 584 320 8 8
+ - image: solid-color(74, 40, 0, 255, 8, 8)
+ bounds: 592 320 8 8
+ - image: solid-color(75, 40, 0, 255, 8, 8)
+ bounds: 600 320 8 8
+ - image: solid-color(76, 40, 0, 255, 8, 8)
+ bounds: 608 320 8 8
+ - image: solid-color(77, 40, 0, 255, 8, 8)
+ bounds: 616 320 8 8
+ - image: solid-color(78, 40, 0, 255, 8, 8)
+ bounds: 624 320 8 8
+ - image: solid-color(79, 40, 0, 255, 8, 8)
+ bounds: 632 320 8 8
+ - image: solid-color(80, 40, 0, 255, 8, 8)
+ bounds: 640 320 8 8
+ - image: solid-color(81, 40, 0, 255, 8, 8)
+ bounds: 648 320 8 8
+ - image: solid-color(82, 40, 0, 255, 8, 8)
+ bounds: 656 320 8 8
+ - image: solid-color(83, 40, 0, 255, 8, 8)
+ bounds: 664 320 8 8
+ - image: solid-color(84, 40, 0, 255, 8, 8)
+ bounds: 672 320 8 8
+ - image: solid-color(85, 40, 0, 255, 8, 8)
+ bounds: 680 320 8 8
+ - image: solid-color(86, 40, 0, 255, 8, 8)
+ bounds: 688 320 8 8
+ - image: solid-color(87, 40, 0, 255, 8, 8)
+ bounds: 696 320 8 8
+ - image: solid-color(88, 40, 0, 255, 8, 8)
+ bounds: 704 320 8 8
+ - image: solid-color(89, 40, 0, 255, 8, 8)
+ bounds: 712 320 8 8
+ - image: solid-color(90, 40, 0, 255, 8, 8)
+ bounds: 720 320 8 8
+ - image: solid-color(91, 40, 0, 255, 8, 8)
+ bounds: 728 320 8 8
+ - image: solid-color(92, 40, 0, 255, 8, 8)
+ bounds: 736 320 8 8
+ - image: solid-color(93, 40, 0, 255, 8, 8)
+ bounds: 744 320 8 8
+ - image: solid-color(94, 40, 0, 255, 8, 8)
+ bounds: 752 320 8 8
+ - image: solid-color(95, 40, 0, 255, 8, 8)
+ bounds: 760 320 8 8
+ - image: solid-color(96, 40, 0, 255, 8, 8)
+ bounds: 768 320 8 8
+ - image: solid-color(97, 40, 0, 255, 8, 8)
+ bounds: 776 320 8 8
+ - image: solid-color(98, 40, 0, 255, 8, 8)
+ bounds: 784 320 8 8
+ - image: solid-color(99, 40, 0, 255, 8, 8)
+ bounds: 792 320 8 8
+ - image: solid-color(100, 40, 0, 255, 8, 8)
+ bounds: 800 320 8 8
+ - image: solid-color(101, 40, 0, 255, 8, 8)
+ bounds: 808 320 8 8
+ - image: solid-color(102, 40, 0, 255, 8, 8)
+ bounds: 816 320 8 8
+ - image: solid-color(103, 40, 0, 255, 8, 8)
+ bounds: 824 320 8 8
+ - image: solid-color(104, 40, 0, 255, 8, 8)
+ bounds: 832 320 8 8
+ - image: solid-color(105, 40, 0, 255, 8, 8)
+ bounds: 840 320 8 8
+ - image: solid-color(106, 40, 0, 255, 8, 8)
+ bounds: 848 320 8 8
+ - image: solid-color(107, 40, 0, 255, 8, 8)
+ bounds: 856 320 8 8
+ - image: solid-color(108, 40, 0, 255, 8, 8)
+ bounds: 864 320 8 8
+ - image: solid-color(109, 40, 0, 255, 8, 8)
+ bounds: 872 320 8 8
+ - image: solid-color(110, 40, 0, 255, 8, 8)
+ bounds: 880 320 8 8
+ - image: solid-color(111, 40, 0, 255, 8, 8)
+ bounds: 888 320 8 8
+ - image: solid-color(112, 40, 0, 255, 8, 8)
+ bounds: 896 320 8 8
+ - image: solid-color(113, 40, 0, 255, 8, 8)
+ bounds: 904 320 8 8
+ - image: solid-color(114, 40, 0, 255, 8, 8)
+ bounds: 912 320 8 8
+ - image: solid-color(115, 40, 0, 255, 8, 8)
+ bounds: 920 320 8 8
+ - image: solid-color(116, 40, 0, 255, 8, 8)
+ bounds: 928 320 8 8
+ - image: solid-color(117, 40, 0, 255, 8, 8)
+ bounds: 936 320 8 8
+ - image: solid-color(118, 40, 0, 255, 8, 8)
+ bounds: 944 320 8 8
+ - image: solid-color(119, 40, 0, 255, 8, 8)
+ bounds: 952 320 8 8
+ - image: solid-color(120, 40, 0, 255, 8, 8)
+ bounds: 960 320 8 8
+ - image: solid-color(121, 40, 0, 255, 8, 8)
+ bounds: 968 320 8 8
+ - image: solid-color(122, 40, 0, 255, 8, 8)
+ bounds: 976 320 8 8
+ - image: solid-color(123, 40, 0, 255, 8, 8)
+ bounds: 984 320 8 8
+ - image: solid-color(124, 40, 0, 255, 8, 8)
+ bounds: 992 320 8 8
+ - image: solid-color(125, 40, 0, 255, 8, 8)
+ bounds: 1000 320 8 8
+ - image: solid-color(126, 40, 0, 255, 8, 8)
+ bounds: 1008 320 8 8
+ - image: solid-color(127, 40, 0, 255, 8, 8)
+ bounds: 1016 320 8 8
+ - image: solid-color(0, 41, 0, 255, 8, 8)
+ bounds: 0 328 8 8
+ - image: solid-color(1, 41, 0, 255, 8, 8)
+ bounds: 8 328 8 8
+ - image: solid-color(2, 41, 0, 255, 8, 8)
+ bounds: 16 328 8 8
+ - image: solid-color(3, 41, 0, 255, 8, 8)
+ bounds: 24 328 8 8
+ - image: solid-color(4, 41, 0, 255, 8, 8)
+ bounds: 32 328 8 8
+ - image: solid-color(5, 41, 0, 255, 8, 8)
+ bounds: 40 328 8 8
+ - image: solid-color(6, 41, 0, 255, 8, 8)
+ bounds: 48 328 8 8
+ - image: solid-color(7, 41, 0, 255, 8, 8)
+ bounds: 56 328 8 8
+ - image: solid-color(8, 41, 0, 255, 8, 8)
+ bounds: 64 328 8 8
+ - image: solid-color(9, 41, 0, 255, 8, 8)
+ bounds: 72 328 8 8
+ - image: solid-color(10, 41, 0, 255, 8, 8)
+ bounds: 80 328 8 8
+ - image: solid-color(11, 41, 0, 255, 8, 8)
+ bounds: 88 328 8 8
+ - image: solid-color(12, 41, 0, 255, 8, 8)
+ bounds: 96 328 8 8
+ - image: solid-color(13, 41, 0, 255, 8, 8)
+ bounds: 104 328 8 8
+ - image: solid-color(14, 41, 0, 255, 8, 8)
+ bounds: 112 328 8 8
+ - image: solid-color(15, 41, 0, 255, 8, 8)
+ bounds: 120 328 8 8
+ - image: solid-color(16, 41, 0, 255, 8, 8)
+ bounds: 128 328 8 8
+ - image: solid-color(17, 41, 0, 255, 8, 8)
+ bounds: 136 328 8 8
+ - image: solid-color(18, 41, 0, 255, 8, 8)
+ bounds: 144 328 8 8
+ - image: solid-color(19, 41, 0, 255, 8, 8)
+ bounds: 152 328 8 8
+ - image: solid-color(20, 41, 0, 255, 8, 8)
+ bounds: 160 328 8 8
+ - image: solid-color(21, 41, 0, 255, 8, 8)
+ bounds: 168 328 8 8
+ - image: solid-color(22, 41, 0, 255, 8, 8)
+ bounds: 176 328 8 8
+ - image: solid-color(23, 41, 0, 255, 8, 8)
+ bounds: 184 328 8 8
+ - image: solid-color(24, 41, 0, 255, 8, 8)
+ bounds: 192 328 8 8
+ - image: solid-color(25, 41, 0, 255, 8, 8)
+ bounds: 200 328 8 8
+ - image: solid-color(26, 41, 0, 255, 8, 8)
+ bounds: 208 328 8 8
+ - image: solid-color(27, 41, 0, 255, 8, 8)
+ bounds: 216 328 8 8
+ - image: solid-color(28, 41, 0, 255, 8, 8)
+ bounds: 224 328 8 8
+ - image: solid-color(29, 41, 0, 255, 8, 8)
+ bounds: 232 328 8 8
+ - image: solid-color(30, 41, 0, 255, 8, 8)
+ bounds: 240 328 8 8
+ - image: solid-color(31, 41, 0, 255, 8, 8)
+ bounds: 248 328 8 8
+ - image: solid-color(32, 41, 0, 255, 8, 8)
+ bounds: 256 328 8 8
+ - image: solid-color(33, 41, 0, 255, 8, 8)
+ bounds: 264 328 8 8
+ - image: solid-color(34, 41, 0, 255, 8, 8)
+ bounds: 272 328 8 8
+ - image: solid-color(35, 41, 0, 255, 8, 8)
+ bounds: 280 328 8 8
+ - image: solid-color(36, 41, 0, 255, 8, 8)
+ bounds: 288 328 8 8
+ - image: solid-color(37, 41, 0, 255, 8, 8)
+ bounds: 296 328 8 8
+ - image: solid-color(38, 41, 0, 255, 8, 8)
+ bounds: 304 328 8 8
+ - image: solid-color(39, 41, 0, 255, 8, 8)
+ bounds: 312 328 8 8
+ - image: solid-color(40, 41, 0, 255, 8, 8)
+ bounds: 320 328 8 8
+ - image: solid-color(41, 41, 0, 255, 8, 8)
+ bounds: 328 328 8 8
+ - image: solid-color(42, 41, 0, 255, 8, 8)
+ bounds: 336 328 8 8
+ - image: solid-color(43, 41, 0, 255, 8, 8)
+ bounds: 344 328 8 8
+ - image: solid-color(44, 41, 0, 255, 8, 8)
+ bounds: 352 328 8 8
+ - image: solid-color(45, 41, 0, 255, 8, 8)
+ bounds: 360 328 8 8
+ - image: solid-color(46, 41, 0, 255, 8, 8)
+ bounds: 368 328 8 8
+ - image: solid-color(47, 41, 0, 255, 8, 8)
+ bounds: 376 328 8 8
+ - image: solid-color(48, 41, 0, 255, 8, 8)
+ bounds: 384 328 8 8
+ - image: solid-color(49, 41, 0, 255, 8, 8)
+ bounds: 392 328 8 8
+ - image: solid-color(50, 41, 0, 255, 8, 8)
+ bounds: 400 328 8 8
+ - image: solid-color(51, 41, 0, 255, 8, 8)
+ bounds: 408 328 8 8
+ - image: solid-color(52, 41, 0, 255, 8, 8)
+ bounds: 416 328 8 8
+ - image: solid-color(53, 41, 0, 255, 8, 8)
+ bounds: 424 328 8 8
+ - image: solid-color(54, 41, 0, 255, 8, 8)
+ bounds: 432 328 8 8
+ - image: solid-color(55, 41, 0, 255, 8, 8)
+ bounds: 440 328 8 8
+ - image: solid-color(56, 41, 0, 255, 8, 8)
+ bounds: 448 328 8 8
+ - image: solid-color(57, 41, 0, 255, 8, 8)
+ bounds: 456 328 8 8
+ - image: solid-color(58, 41, 0, 255, 8, 8)
+ bounds: 464 328 8 8
+ - image: solid-color(59, 41, 0, 255, 8, 8)
+ bounds: 472 328 8 8
+ - image: solid-color(60, 41, 0, 255, 8, 8)
+ bounds: 480 328 8 8
+ - image: solid-color(61, 41, 0, 255, 8, 8)
+ bounds: 488 328 8 8
+ - image: solid-color(62, 41, 0, 255, 8, 8)
+ bounds: 496 328 8 8
+ - image: solid-color(63, 41, 0, 255, 8, 8)
+ bounds: 504 328 8 8
+ - image: solid-color(64, 41, 0, 255, 8, 8)
+ bounds: 512 328 8 8
+ - image: solid-color(65, 41, 0, 255, 8, 8)
+ bounds: 520 328 8 8
+ - image: solid-color(66, 41, 0, 255, 8, 8)
+ bounds: 528 328 8 8
+ - image: solid-color(67, 41, 0, 255, 8, 8)
+ bounds: 536 328 8 8
+ - image: solid-color(68, 41, 0, 255, 8, 8)
+ bounds: 544 328 8 8
+ - image: solid-color(69, 41, 0, 255, 8, 8)
+ bounds: 552 328 8 8
+ - image: solid-color(70, 41, 0, 255, 8, 8)
+ bounds: 560 328 8 8
+ - image: solid-color(71, 41, 0, 255, 8, 8)
+ bounds: 568 328 8 8
+ - image: solid-color(72, 41, 0, 255, 8, 8)
+ bounds: 576 328 8 8
+ - image: solid-color(73, 41, 0, 255, 8, 8)
+ bounds: 584 328 8 8
+ - image: solid-color(74, 41, 0, 255, 8, 8)
+ bounds: 592 328 8 8
+ - image: solid-color(75, 41, 0, 255, 8, 8)
+ bounds: 600 328 8 8
+ - image: solid-color(76, 41, 0, 255, 8, 8)
+ bounds: 608 328 8 8
+ - image: solid-color(77, 41, 0, 255, 8, 8)
+ bounds: 616 328 8 8
+ - image: solid-color(78, 41, 0, 255, 8, 8)
+ bounds: 624 328 8 8
+ - image: solid-color(79, 41, 0, 255, 8, 8)
+ bounds: 632 328 8 8
+ - image: solid-color(80, 41, 0, 255, 8, 8)
+ bounds: 640 328 8 8
+ - image: solid-color(81, 41, 0, 255, 8, 8)
+ bounds: 648 328 8 8
+ - image: solid-color(82, 41, 0, 255, 8, 8)
+ bounds: 656 328 8 8
+ - image: solid-color(83, 41, 0, 255, 8, 8)
+ bounds: 664 328 8 8
+ - image: solid-color(84, 41, 0, 255, 8, 8)
+ bounds: 672 328 8 8
+ - image: solid-color(85, 41, 0, 255, 8, 8)
+ bounds: 680 328 8 8
+ - image: solid-color(86, 41, 0, 255, 8, 8)
+ bounds: 688 328 8 8
+ - image: solid-color(87, 41, 0, 255, 8, 8)
+ bounds: 696 328 8 8
+ - image: solid-color(88, 41, 0, 255, 8, 8)
+ bounds: 704 328 8 8
+ - image: solid-color(89, 41, 0, 255, 8, 8)
+ bounds: 712 328 8 8
+ - image: solid-color(90, 41, 0, 255, 8, 8)
+ bounds: 720 328 8 8
+ - image: solid-color(91, 41, 0, 255, 8, 8)
+ bounds: 728 328 8 8
+ - image: solid-color(92, 41, 0, 255, 8, 8)
+ bounds: 736 328 8 8
+ - image: solid-color(93, 41, 0, 255, 8, 8)
+ bounds: 744 328 8 8
+ - image: solid-color(94, 41, 0, 255, 8, 8)
+ bounds: 752 328 8 8
+ - image: solid-color(95, 41, 0, 255, 8, 8)
+ bounds: 760 328 8 8
+ - image: solid-color(96, 41, 0, 255, 8, 8)
+ bounds: 768 328 8 8
+ - image: solid-color(97, 41, 0, 255, 8, 8)
+ bounds: 776 328 8 8
+ - image: solid-color(98, 41, 0, 255, 8, 8)
+ bounds: 784 328 8 8
+ - image: solid-color(99, 41, 0, 255, 8, 8)
+ bounds: 792 328 8 8
+ - image: solid-color(100, 41, 0, 255, 8, 8)
+ bounds: 800 328 8 8
+ - image: solid-color(101, 41, 0, 255, 8, 8)
+ bounds: 808 328 8 8
+ - image: solid-color(102, 41, 0, 255, 8, 8)
+ bounds: 816 328 8 8
+ - image: solid-color(103, 41, 0, 255, 8, 8)
+ bounds: 824 328 8 8
+ - image: solid-color(104, 41, 0, 255, 8, 8)
+ bounds: 832 328 8 8
+ - image: solid-color(105, 41, 0, 255, 8, 8)
+ bounds: 840 328 8 8
+ - image: solid-color(106, 41, 0, 255, 8, 8)
+ bounds: 848 328 8 8
+ - image: solid-color(107, 41, 0, 255, 8, 8)
+ bounds: 856 328 8 8
+ - image: solid-color(108, 41, 0, 255, 8, 8)
+ bounds: 864 328 8 8
+ - image: solid-color(109, 41, 0, 255, 8, 8)
+ bounds: 872 328 8 8
+ - image: solid-color(110, 41, 0, 255, 8, 8)
+ bounds: 880 328 8 8
+ - image: solid-color(111, 41, 0, 255, 8, 8)
+ bounds: 888 328 8 8
+ - image: solid-color(112, 41, 0, 255, 8, 8)
+ bounds: 896 328 8 8
+ - image: solid-color(113, 41, 0, 255, 8, 8)
+ bounds: 904 328 8 8
+ - image: solid-color(114, 41, 0, 255, 8, 8)
+ bounds: 912 328 8 8
+ - image: solid-color(115, 41, 0, 255, 8, 8)
+ bounds: 920 328 8 8
+ - image: solid-color(116, 41, 0, 255, 8, 8)
+ bounds: 928 328 8 8
+ - image: solid-color(117, 41, 0, 255, 8, 8)
+ bounds: 936 328 8 8
+ - image: solid-color(118, 41, 0, 255, 8, 8)
+ bounds: 944 328 8 8
+ - image: solid-color(119, 41, 0, 255, 8, 8)
+ bounds: 952 328 8 8
+ - image: solid-color(120, 41, 0, 255, 8, 8)
+ bounds: 960 328 8 8
+ - image: solid-color(121, 41, 0, 255, 8, 8)
+ bounds: 968 328 8 8
+ - image: solid-color(122, 41, 0, 255, 8, 8)
+ bounds: 976 328 8 8
+ - image: solid-color(123, 41, 0, 255, 8, 8)
+ bounds: 984 328 8 8
+ - image: solid-color(124, 41, 0, 255, 8, 8)
+ bounds: 992 328 8 8
+ - image: solid-color(125, 41, 0, 255, 8, 8)
+ bounds: 1000 328 8 8
+ - image: solid-color(126, 41, 0, 255, 8, 8)
+ bounds: 1008 328 8 8
+ - image: solid-color(127, 41, 0, 255, 8, 8)
+ bounds: 1016 328 8 8
+ - image: solid-color(0, 42, 0, 255, 8, 8)
+ bounds: 0 336 8 8
+ - image: solid-color(1, 42, 0, 255, 8, 8)
+ bounds: 8 336 8 8
+ - image: solid-color(2, 42, 0, 255, 8, 8)
+ bounds: 16 336 8 8
+ - image: solid-color(3, 42, 0, 255, 8, 8)
+ bounds: 24 336 8 8
+ - image: solid-color(4, 42, 0, 255, 8, 8)
+ bounds: 32 336 8 8
+ - image: solid-color(5, 42, 0, 255, 8, 8)
+ bounds: 40 336 8 8
+ - image: solid-color(6, 42, 0, 255, 8, 8)
+ bounds: 48 336 8 8
+ - image: solid-color(7, 42, 0, 255, 8, 8)
+ bounds: 56 336 8 8
+ - image: solid-color(8, 42, 0, 255, 8, 8)
+ bounds: 64 336 8 8
+ - image: solid-color(9, 42, 0, 255, 8, 8)
+ bounds: 72 336 8 8
+ - image: solid-color(10, 42, 0, 255, 8, 8)
+ bounds: 80 336 8 8
+ - image: solid-color(11, 42, 0, 255, 8, 8)
+ bounds: 88 336 8 8
+ - image: solid-color(12, 42, 0, 255, 8, 8)
+ bounds: 96 336 8 8
+ - image: solid-color(13, 42, 0, 255, 8, 8)
+ bounds: 104 336 8 8
+ - image: solid-color(14, 42, 0, 255, 8, 8)
+ bounds: 112 336 8 8
+ - image: solid-color(15, 42, 0, 255, 8, 8)
+ bounds: 120 336 8 8
+ - image: solid-color(16, 42, 0, 255, 8, 8)
+ bounds: 128 336 8 8
+ - image: solid-color(17, 42, 0, 255, 8, 8)
+ bounds: 136 336 8 8
+ - image: solid-color(18, 42, 0, 255, 8, 8)
+ bounds: 144 336 8 8
+ - image: solid-color(19, 42, 0, 255, 8, 8)
+ bounds: 152 336 8 8
+ - image: solid-color(20, 42, 0, 255, 8, 8)
+ bounds: 160 336 8 8
+ - image: solid-color(21, 42, 0, 255, 8, 8)
+ bounds: 168 336 8 8
+ - image: solid-color(22, 42, 0, 255, 8, 8)
+ bounds: 176 336 8 8
+ - image: solid-color(23, 42, 0, 255, 8, 8)
+ bounds: 184 336 8 8
+ - image: solid-color(24, 42, 0, 255, 8, 8)
+ bounds: 192 336 8 8
+ - image: solid-color(25, 42, 0, 255, 8, 8)
+ bounds: 200 336 8 8
+ - image: solid-color(26, 42, 0, 255, 8, 8)
+ bounds: 208 336 8 8
+ - image: solid-color(27, 42, 0, 255, 8, 8)
+ bounds: 216 336 8 8
+ - image: solid-color(28, 42, 0, 255, 8, 8)
+ bounds: 224 336 8 8
+ - image: solid-color(29, 42, 0, 255, 8, 8)
+ bounds: 232 336 8 8
+ - image: solid-color(30, 42, 0, 255, 8, 8)
+ bounds: 240 336 8 8
+ - image: solid-color(31, 42, 0, 255, 8, 8)
+ bounds: 248 336 8 8
+ - image: solid-color(32, 42, 0, 255, 8, 8)
+ bounds: 256 336 8 8
+ - image: solid-color(33, 42, 0, 255, 8, 8)
+ bounds: 264 336 8 8
+ - image: solid-color(34, 42, 0, 255, 8, 8)
+ bounds: 272 336 8 8
+ - image: solid-color(35, 42, 0, 255, 8, 8)
+ bounds: 280 336 8 8
+ - image: solid-color(36, 42, 0, 255, 8, 8)
+ bounds: 288 336 8 8
+ - image: solid-color(37, 42, 0, 255, 8, 8)
+ bounds: 296 336 8 8
+ - image: solid-color(38, 42, 0, 255, 8, 8)
+ bounds: 304 336 8 8
+ - image: solid-color(39, 42, 0, 255, 8, 8)
+ bounds: 312 336 8 8
+ - image: solid-color(40, 42, 0, 255, 8, 8)
+ bounds: 320 336 8 8
+ - image: solid-color(41, 42, 0, 255, 8, 8)
+ bounds: 328 336 8 8
+ - image: solid-color(42, 42, 0, 255, 8, 8)
+ bounds: 336 336 8 8
+ - image: solid-color(43, 42, 0, 255, 8, 8)
+ bounds: 344 336 8 8
+ - image: solid-color(44, 42, 0, 255, 8, 8)
+ bounds: 352 336 8 8
+ - image: solid-color(45, 42, 0, 255, 8, 8)
+ bounds: 360 336 8 8
+ - image: solid-color(46, 42, 0, 255, 8, 8)
+ bounds: 368 336 8 8
+ - image: solid-color(47, 42, 0, 255, 8, 8)
+ bounds: 376 336 8 8
+ - image: solid-color(48, 42, 0, 255, 8, 8)
+ bounds: 384 336 8 8
+ - image: solid-color(49, 42, 0, 255, 8, 8)
+ bounds: 392 336 8 8
+ - image: solid-color(50, 42, 0, 255, 8, 8)
+ bounds: 400 336 8 8
+ - image: solid-color(51, 42, 0, 255, 8, 8)
+ bounds: 408 336 8 8
+ - image: solid-color(52, 42, 0, 255, 8, 8)
+ bounds: 416 336 8 8
+ - image: solid-color(53, 42, 0, 255, 8, 8)
+ bounds: 424 336 8 8
+ - image: solid-color(54, 42, 0, 255, 8, 8)
+ bounds: 432 336 8 8
+ - image: solid-color(55, 42, 0, 255, 8, 8)
+ bounds: 440 336 8 8
+ - image: solid-color(56, 42, 0, 255, 8, 8)
+ bounds: 448 336 8 8
+ - image: solid-color(57, 42, 0, 255, 8, 8)
+ bounds: 456 336 8 8
+ - image: solid-color(58, 42, 0, 255, 8, 8)
+ bounds: 464 336 8 8
+ - image: solid-color(59, 42, 0, 255, 8, 8)
+ bounds: 472 336 8 8
+ - image: solid-color(60, 42, 0, 255, 8, 8)
+ bounds: 480 336 8 8
+ - image: solid-color(61, 42, 0, 255, 8, 8)
+ bounds: 488 336 8 8
+ - image: solid-color(62, 42, 0, 255, 8, 8)
+ bounds: 496 336 8 8
+ - image: solid-color(63, 42, 0, 255, 8, 8)
+ bounds: 504 336 8 8
+ - image: solid-color(64, 42, 0, 255, 8, 8)
+ bounds: 512 336 8 8
+ - image: solid-color(65, 42, 0, 255, 8, 8)
+ bounds: 520 336 8 8
+ - image: solid-color(66, 42, 0, 255, 8, 8)
+ bounds: 528 336 8 8
+ - image: solid-color(67, 42, 0, 255, 8, 8)
+ bounds: 536 336 8 8
+ - image: solid-color(68, 42, 0, 255, 8, 8)
+ bounds: 544 336 8 8
+ - image: solid-color(69, 42, 0, 255, 8, 8)
+ bounds: 552 336 8 8
+ - image: solid-color(70, 42, 0, 255, 8, 8)
+ bounds: 560 336 8 8
+ - image: solid-color(71, 42, 0, 255, 8, 8)
+ bounds: 568 336 8 8
+ - image: solid-color(72, 42, 0, 255, 8, 8)
+ bounds: 576 336 8 8
+ - image: solid-color(73, 42, 0, 255, 8, 8)
+ bounds: 584 336 8 8
+ - image: solid-color(74, 42, 0, 255, 8, 8)
+ bounds: 592 336 8 8
+ - image: solid-color(75, 42, 0, 255, 8, 8)
+ bounds: 600 336 8 8
+ - image: solid-color(76, 42, 0, 255, 8, 8)
+ bounds: 608 336 8 8
+ - image: solid-color(77, 42, 0, 255, 8, 8)
+ bounds: 616 336 8 8
+ - image: solid-color(78, 42, 0, 255, 8, 8)
+ bounds: 624 336 8 8
+ - image: solid-color(79, 42, 0, 255, 8, 8)
+ bounds: 632 336 8 8
+ - image: solid-color(80, 42, 0, 255, 8, 8)
+ bounds: 640 336 8 8
+ - image: solid-color(81, 42, 0, 255, 8, 8)
+ bounds: 648 336 8 8
+ - image: solid-color(82, 42, 0, 255, 8, 8)
+ bounds: 656 336 8 8
+ - image: solid-color(83, 42, 0, 255, 8, 8)
+ bounds: 664 336 8 8
+ - image: solid-color(84, 42, 0, 255, 8, 8)
+ bounds: 672 336 8 8
+ - image: solid-color(85, 42, 0, 255, 8, 8)
+ bounds: 680 336 8 8
+ - image: solid-color(86, 42, 0, 255, 8, 8)
+ bounds: 688 336 8 8
+ - image: solid-color(87, 42, 0, 255, 8, 8)
+ bounds: 696 336 8 8
+ - image: solid-color(88, 42, 0, 255, 8, 8)
+ bounds: 704 336 8 8
+ - image: solid-color(89, 42, 0, 255, 8, 8)
+ bounds: 712 336 8 8
+ - image: solid-color(90, 42, 0, 255, 8, 8)
+ bounds: 720 336 8 8
+ - image: solid-color(91, 42, 0, 255, 8, 8)
+ bounds: 728 336 8 8
+ - image: solid-color(92, 42, 0, 255, 8, 8)
+ bounds: 736 336 8 8
+ - image: solid-color(93, 42, 0, 255, 8, 8)
+ bounds: 744 336 8 8
+ - image: solid-color(94, 42, 0, 255, 8, 8)
+ bounds: 752 336 8 8
+ - image: solid-color(95, 42, 0, 255, 8, 8)
+ bounds: 760 336 8 8
+ - image: solid-color(96, 42, 0, 255, 8, 8)
+ bounds: 768 336 8 8
+ - image: solid-color(97, 42, 0, 255, 8, 8)
+ bounds: 776 336 8 8
+ - image: solid-color(98, 42, 0, 255, 8, 8)
+ bounds: 784 336 8 8
+ - image: solid-color(99, 42, 0, 255, 8, 8)
+ bounds: 792 336 8 8
+ - image: solid-color(100, 42, 0, 255, 8, 8)
+ bounds: 800 336 8 8
+ - image: solid-color(101, 42, 0, 255, 8, 8)
+ bounds: 808 336 8 8
+ - image: solid-color(102, 42, 0, 255, 8, 8)
+ bounds: 816 336 8 8
+ - image: solid-color(103, 42, 0, 255, 8, 8)
+ bounds: 824 336 8 8
+ - image: solid-color(104, 42, 0, 255, 8, 8)
+ bounds: 832 336 8 8
+ - image: solid-color(105, 42, 0, 255, 8, 8)
+ bounds: 840 336 8 8
+ - image: solid-color(106, 42, 0, 255, 8, 8)
+ bounds: 848 336 8 8
+ - image: solid-color(107, 42, 0, 255, 8, 8)
+ bounds: 856 336 8 8
+ - image: solid-color(108, 42, 0, 255, 8, 8)
+ bounds: 864 336 8 8
+ - image: solid-color(109, 42, 0, 255, 8, 8)
+ bounds: 872 336 8 8
+ - image: solid-color(110, 42, 0, 255, 8, 8)
+ bounds: 880 336 8 8
+ - image: solid-color(111, 42, 0, 255, 8, 8)
+ bounds: 888 336 8 8
+ - image: solid-color(112, 42, 0, 255, 8, 8)
+ bounds: 896 336 8 8
+ - image: solid-color(113, 42, 0, 255, 8, 8)
+ bounds: 904 336 8 8
+ - image: solid-color(114, 42, 0, 255, 8, 8)
+ bounds: 912 336 8 8
+ - image: solid-color(115, 42, 0, 255, 8, 8)
+ bounds: 920 336 8 8
+ - image: solid-color(116, 42, 0, 255, 8, 8)
+ bounds: 928 336 8 8
+ - image: solid-color(117, 42, 0, 255, 8, 8)
+ bounds: 936 336 8 8
+ - image: solid-color(118, 42, 0, 255, 8, 8)
+ bounds: 944 336 8 8
+ - image: solid-color(119, 42, 0, 255, 8, 8)
+ bounds: 952 336 8 8
+ - image: solid-color(120, 42, 0, 255, 8, 8)
+ bounds: 960 336 8 8
+ - image: solid-color(121, 42, 0, 255, 8, 8)
+ bounds: 968 336 8 8
+ - image: solid-color(122, 42, 0, 255, 8, 8)
+ bounds: 976 336 8 8
+ - image: solid-color(123, 42, 0, 255, 8, 8)
+ bounds: 984 336 8 8
+ - image: solid-color(124, 42, 0, 255, 8, 8)
+ bounds: 992 336 8 8
+ - image: solid-color(125, 42, 0, 255, 8, 8)
+ bounds: 1000 336 8 8
+ - image: solid-color(126, 42, 0, 255, 8, 8)
+ bounds: 1008 336 8 8
+ - image: solid-color(127, 42, 0, 255, 8, 8)
+ bounds: 1016 336 8 8
+ - image: solid-color(0, 43, 0, 255, 8, 8)
+ bounds: 0 344 8 8
+ - image: solid-color(1, 43, 0, 255, 8, 8)
+ bounds: 8 344 8 8
+ - image: solid-color(2, 43, 0, 255, 8, 8)
+ bounds: 16 344 8 8
+ - image: solid-color(3, 43, 0, 255, 8, 8)
+ bounds: 24 344 8 8
+ - image: solid-color(4, 43, 0, 255, 8, 8)
+ bounds: 32 344 8 8
+ - image: solid-color(5, 43, 0, 255, 8, 8)
+ bounds: 40 344 8 8
+ - image: solid-color(6, 43, 0, 255, 8, 8)
+ bounds: 48 344 8 8
+ - image: solid-color(7, 43, 0, 255, 8, 8)
+ bounds: 56 344 8 8
+ - image: solid-color(8, 43, 0, 255, 8, 8)
+ bounds: 64 344 8 8
+ - image: solid-color(9, 43, 0, 255, 8, 8)
+ bounds: 72 344 8 8
+ - image: solid-color(10, 43, 0, 255, 8, 8)
+ bounds: 80 344 8 8
+ - image: solid-color(11, 43, 0, 255, 8, 8)
+ bounds: 88 344 8 8
+ - image: solid-color(12, 43, 0, 255, 8, 8)
+ bounds: 96 344 8 8
+ - image: solid-color(13, 43, 0, 255, 8, 8)
+ bounds: 104 344 8 8
+ - image: solid-color(14, 43, 0, 255, 8, 8)
+ bounds: 112 344 8 8
+ - image: solid-color(15, 43, 0, 255, 8, 8)
+ bounds: 120 344 8 8
+ - image: solid-color(16, 43, 0, 255, 8, 8)
+ bounds: 128 344 8 8
+ - image: solid-color(17, 43, 0, 255, 8, 8)
+ bounds: 136 344 8 8
+ - image: solid-color(18, 43, 0, 255, 8, 8)
+ bounds: 144 344 8 8
+ - image: solid-color(19, 43, 0, 255, 8, 8)
+ bounds: 152 344 8 8
+ - image: solid-color(20, 43, 0, 255, 8, 8)
+ bounds: 160 344 8 8
+ - image: solid-color(21, 43, 0, 255, 8, 8)
+ bounds: 168 344 8 8
+ - image: solid-color(22, 43, 0, 255, 8, 8)
+ bounds: 176 344 8 8
+ - image: solid-color(23, 43, 0, 255, 8, 8)
+ bounds: 184 344 8 8
+ - image: solid-color(24, 43, 0, 255, 8, 8)
+ bounds: 192 344 8 8
+ - image: solid-color(25, 43, 0, 255, 8, 8)
+ bounds: 200 344 8 8
+ - image: solid-color(26, 43, 0, 255, 8, 8)
+ bounds: 208 344 8 8
+ - image: solid-color(27, 43, 0, 255, 8, 8)
+ bounds: 216 344 8 8
+ - image: solid-color(28, 43, 0, 255, 8, 8)
+ bounds: 224 344 8 8
+ - image: solid-color(29, 43, 0, 255, 8, 8)
+ bounds: 232 344 8 8
+ - image: solid-color(30, 43, 0, 255, 8, 8)
+ bounds: 240 344 8 8
+ - image: solid-color(31, 43, 0, 255, 8, 8)
+ bounds: 248 344 8 8
+ - image: solid-color(32, 43, 0, 255, 8, 8)
+ bounds: 256 344 8 8
+ - image: solid-color(33, 43, 0, 255, 8, 8)
+ bounds: 264 344 8 8
+ - image: solid-color(34, 43, 0, 255, 8, 8)
+ bounds: 272 344 8 8
+ - image: solid-color(35, 43, 0, 255, 8, 8)
+ bounds: 280 344 8 8
+ - image: solid-color(36, 43, 0, 255, 8, 8)
+ bounds: 288 344 8 8
+ - image: solid-color(37, 43, 0, 255, 8, 8)
+ bounds: 296 344 8 8
+ - image: solid-color(38, 43, 0, 255, 8, 8)
+ bounds: 304 344 8 8
+ - image: solid-color(39, 43, 0, 255, 8, 8)
+ bounds: 312 344 8 8
+ - image: solid-color(40, 43, 0, 255, 8, 8)
+ bounds: 320 344 8 8
+ - image: solid-color(41, 43, 0, 255, 8, 8)
+ bounds: 328 344 8 8
+ - image: solid-color(42, 43, 0, 255, 8, 8)
+ bounds: 336 344 8 8
+ - image: solid-color(43, 43, 0, 255, 8, 8)
+ bounds: 344 344 8 8
+ - image: solid-color(44, 43, 0, 255, 8, 8)
+ bounds: 352 344 8 8
+ - image: solid-color(45, 43, 0, 255, 8, 8)
+ bounds: 360 344 8 8
+ - image: solid-color(46, 43, 0, 255, 8, 8)
+ bounds: 368 344 8 8
+ - image: solid-color(47, 43, 0, 255, 8, 8)
+ bounds: 376 344 8 8
+ - image: solid-color(48, 43, 0, 255, 8, 8)
+ bounds: 384 344 8 8
+ - image: solid-color(49, 43, 0, 255, 8, 8)
+ bounds: 392 344 8 8
+ - image: solid-color(50, 43, 0, 255, 8, 8)
+ bounds: 400 344 8 8
+ - image: solid-color(51, 43, 0, 255, 8, 8)
+ bounds: 408 344 8 8
+ - image: solid-color(52, 43, 0, 255, 8, 8)
+ bounds: 416 344 8 8
+ - image: solid-color(53, 43, 0, 255, 8, 8)
+ bounds: 424 344 8 8
+ - image: solid-color(54, 43, 0, 255, 8, 8)
+ bounds: 432 344 8 8
+ - image: solid-color(55, 43, 0, 255, 8, 8)
+ bounds: 440 344 8 8
+ - image: solid-color(56, 43, 0, 255, 8, 8)
+ bounds: 448 344 8 8
+ - image: solid-color(57, 43, 0, 255, 8, 8)
+ bounds: 456 344 8 8
+ - image: solid-color(58, 43, 0, 255, 8, 8)
+ bounds: 464 344 8 8
+ - image: solid-color(59, 43, 0, 255, 8, 8)
+ bounds: 472 344 8 8
+ - image: solid-color(60, 43, 0, 255, 8, 8)
+ bounds: 480 344 8 8
+ - image: solid-color(61, 43, 0, 255, 8, 8)
+ bounds: 488 344 8 8
+ - image: solid-color(62, 43, 0, 255, 8, 8)
+ bounds: 496 344 8 8
+ - image: solid-color(63, 43, 0, 255, 8, 8)
+ bounds: 504 344 8 8
+ - image: solid-color(64, 43, 0, 255, 8, 8)
+ bounds: 512 344 8 8
+ - image: solid-color(65, 43, 0, 255, 8, 8)
+ bounds: 520 344 8 8
+ - image: solid-color(66, 43, 0, 255, 8, 8)
+ bounds: 528 344 8 8
+ - image: solid-color(67, 43, 0, 255, 8, 8)
+ bounds: 536 344 8 8
+ - image: solid-color(68, 43, 0, 255, 8, 8)
+ bounds: 544 344 8 8
+ - image: solid-color(69, 43, 0, 255, 8, 8)
+ bounds: 552 344 8 8
+ - image: solid-color(70, 43, 0, 255, 8, 8)
+ bounds: 560 344 8 8
+ - image: solid-color(71, 43, 0, 255, 8, 8)
+ bounds: 568 344 8 8
+ - image: solid-color(72, 43, 0, 255, 8, 8)
+ bounds: 576 344 8 8
+ - image: solid-color(73, 43, 0, 255, 8, 8)
+ bounds: 584 344 8 8
+ - image: solid-color(74, 43, 0, 255, 8, 8)
+ bounds: 592 344 8 8
+ - image: solid-color(75, 43, 0, 255, 8, 8)
+ bounds: 600 344 8 8
+ - image: solid-color(76, 43, 0, 255, 8, 8)
+ bounds: 608 344 8 8
+ - image: solid-color(77, 43, 0, 255, 8, 8)
+ bounds: 616 344 8 8
+ - image: solid-color(78, 43, 0, 255, 8, 8)
+ bounds: 624 344 8 8
+ - image: solid-color(79, 43, 0, 255, 8, 8)
+ bounds: 632 344 8 8
+ - image: solid-color(80, 43, 0, 255, 8, 8)
+ bounds: 640 344 8 8
+ - image: solid-color(81, 43, 0, 255, 8, 8)
+ bounds: 648 344 8 8
+ - image: solid-color(82, 43, 0, 255, 8, 8)
+ bounds: 656 344 8 8
+ - image: solid-color(83, 43, 0, 255, 8, 8)
+ bounds: 664 344 8 8
+ - image: solid-color(84, 43, 0, 255, 8, 8)
+ bounds: 672 344 8 8
+ - image: solid-color(85, 43, 0, 255, 8, 8)
+ bounds: 680 344 8 8
+ - image: solid-color(86, 43, 0, 255, 8, 8)
+ bounds: 688 344 8 8
+ - image: solid-color(87, 43, 0, 255, 8, 8)
+ bounds: 696 344 8 8
+ - image: solid-color(88, 43, 0, 255, 8, 8)
+ bounds: 704 344 8 8
+ - image: solid-color(89, 43, 0, 255, 8, 8)
+ bounds: 712 344 8 8
+ - image: solid-color(90, 43, 0, 255, 8, 8)
+ bounds: 720 344 8 8
+ - image: solid-color(91, 43, 0, 255, 8, 8)
+ bounds: 728 344 8 8
+ - image: solid-color(92, 43, 0, 255, 8, 8)
+ bounds: 736 344 8 8
+ - image: solid-color(93, 43, 0, 255, 8, 8)
+ bounds: 744 344 8 8
+ - image: solid-color(94, 43, 0, 255, 8, 8)
+ bounds: 752 344 8 8
+ - image: solid-color(95, 43, 0, 255, 8, 8)
+ bounds: 760 344 8 8
+ - image: solid-color(96, 43, 0, 255, 8, 8)
+ bounds: 768 344 8 8
+ - image: solid-color(97, 43, 0, 255, 8, 8)
+ bounds: 776 344 8 8
+ - image: solid-color(98, 43, 0, 255, 8, 8)
+ bounds: 784 344 8 8
+ - image: solid-color(99, 43, 0, 255, 8, 8)
+ bounds: 792 344 8 8
+ - image: solid-color(100, 43, 0, 255, 8, 8)
+ bounds: 800 344 8 8
+ - image: solid-color(101, 43, 0, 255, 8, 8)
+ bounds: 808 344 8 8
+ - image: solid-color(102, 43, 0, 255, 8, 8)
+ bounds: 816 344 8 8
+ - image: solid-color(103, 43, 0, 255, 8, 8)
+ bounds: 824 344 8 8
+ - image: solid-color(104, 43, 0, 255, 8, 8)
+ bounds: 832 344 8 8
+ - image: solid-color(105, 43, 0, 255, 8, 8)
+ bounds: 840 344 8 8
+ - image: solid-color(106, 43, 0, 255, 8, 8)
+ bounds: 848 344 8 8
+ - image: solid-color(107, 43, 0, 255, 8, 8)
+ bounds: 856 344 8 8
+ - image: solid-color(108, 43, 0, 255, 8, 8)
+ bounds: 864 344 8 8
+ - image: solid-color(109, 43, 0, 255, 8, 8)
+ bounds: 872 344 8 8
+ - image: solid-color(110, 43, 0, 255, 8, 8)
+ bounds: 880 344 8 8
+ - image: solid-color(111, 43, 0, 255, 8, 8)
+ bounds: 888 344 8 8
+ - image: solid-color(112, 43, 0, 255, 8, 8)
+ bounds: 896 344 8 8
+ - image: solid-color(113, 43, 0, 255, 8, 8)
+ bounds: 904 344 8 8
+ - image: solid-color(114, 43, 0, 255, 8, 8)
+ bounds: 912 344 8 8
+ - image: solid-color(115, 43, 0, 255, 8, 8)
+ bounds: 920 344 8 8
+ - image: solid-color(116, 43, 0, 255, 8, 8)
+ bounds: 928 344 8 8
+ - image: solid-color(117, 43, 0, 255, 8, 8)
+ bounds: 936 344 8 8
+ - image: solid-color(118, 43, 0, 255, 8, 8)
+ bounds: 944 344 8 8
+ - image: solid-color(119, 43, 0, 255, 8, 8)
+ bounds: 952 344 8 8
+ - image: solid-color(120, 43, 0, 255, 8, 8)
+ bounds: 960 344 8 8
+ - image: solid-color(121, 43, 0, 255, 8, 8)
+ bounds: 968 344 8 8
+ - image: solid-color(122, 43, 0, 255, 8, 8)
+ bounds: 976 344 8 8
+ - image: solid-color(123, 43, 0, 255, 8, 8)
+ bounds: 984 344 8 8
+ - image: solid-color(124, 43, 0, 255, 8, 8)
+ bounds: 992 344 8 8
+ - image: solid-color(125, 43, 0, 255, 8, 8)
+ bounds: 1000 344 8 8
+ - image: solid-color(126, 43, 0, 255, 8, 8)
+ bounds: 1008 344 8 8
+ - image: solid-color(127, 43, 0, 255, 8, 8)
+ bounds: 1016 344 8 8
+ - image: solid-color(0, 44, 0, 255, 8, 8)
+ bounds: 0 352 8 8
+ - image: solid-color(1, 44, 0, 255, 8, 8)
+ bounds: 8 352 8 8
+ - image: solid-color(2, 44, 0, 255, 8, 8)
+ bounds: 16 352 8 8
+ - image: solid-color(3, 44, 0, 255, 8, 8)
+ bounds: 24 352 8 8
+ - image: solid-color(4, 44, 0, 255, 8, 8)
+ bounds: 32 352 8 8
+ - image: solid-color(5, 44, 0, 255, 8, 8)
+ bounds: 40 352 8 8
+ - image: solid-color(6, 44, 0, 255, 8, 8)
+ bounds: 48 352 8 8
+ - image: solid-color(7, 44, 0, 255, 8, 8)
+ bounds: 56 352 8 8
+ - image: solid-color(8, 44, 0, 255, 8, 8)
+ bounds: 64 352 8 8
+ - image: solid-color(9, 44, 0, 255, 8, 8)
+ bounds: 72 352 8 8
+ - image: solid-color(10, 44, 0, 255, 8, 8)
+ bounds: 80 352 8 8
+ - image: solid-color(11, 44, 0, 255, 8, 8)
+ bounds: 88 352 8 8
+ - image: solid-color(12, 44, 0, 255, 8, 8)
+ bounds: 96 352 8 8
+ - image: solid-color(13, 44, 0, 255, 8, 8)
+ bounds: 104 352 8 8
+ - image: solid-color(14, 44, 0, 255, 8, 8)
+ bounds: 112 352 8 8
+ - image: solid-color(15, 44, 0, 255, 8, 8)
+ bounds: 120 352 8 8
+ - image: solid-color(16, 44, 0, 255, 8, 8)
+ bounds: 128 352 8 8
+ - image: solid-color(17, 44, 0, 255, 8, 8)
+ bounds: 136 352 8 8
+ - image: solid-color(18, 44, 0, 255, 8, 8)
+ bounds: 144 352 8 8
+ - image: solid-color(19, 44, 0, 255, 8, 8)
+ bounds: 152 352 8 8
+ - image: solid-color(20, 44, 0, 255, 8, 8)
+ bounds: 160 352 8 8
+ - image: solid-color(21, 44, 0, 255, 8, 8)
+ bounds: 168 352 8 8
+ - image: solid-color(22, 44, 0, 255, 8, 8)
+ bounds: 176 352 8 8
+ - image: solid-color(23, 44, 0, 255, 8, 8)
+ bounds: 184 352 8 8
+ - image: solid-color(24, 44, 0, 255, 8, 8)
+ bounds: 192 352 8 8
+ - image: solid-color(25, 44, 0, 255, 8, 8)
+ bounds: 200 352 8 8
+ - image: solid-color(26, 44, 0, 255, 8, 8)
+ bounds: 208 352 8 8
+ - image: solid-color(27, 44, 0, 255, 8, 8)
+ bounds: 216 352 8 8
+ - image: solid-color(28, 44, 0, 255, 8, 8)
+ bounds: 224 352 8 8
+ - image: solid-color(29, 44, 0, 255, 8, 8)
+ bounds: 232 352 8 8
+ - image: solid-color(30, 44, 0, 255, 8, 8)
+ bounds: 240 352 8 8
+ - image: solid-color(31, 44, 0, 255, 8, 8)
+ bounds: 248 352 8 8
+ - image: solid-color(32, 44, 0, 255, 8, 8)
+ bounds: 256 352 8 8
+ - image: solid-color(33, 44, 0, 255, 8, 8)
+ bounds: 264 352 8 8
+ - image: solid-color(34, 44, 0, 255, 8, 8)
+ bounds: 272 352 8 8
+ - image: solid-color(35, 44, 0, 255, 8, 8)
+ bounds: 280 352 8 8
+ - image: solid-color(36, 44, 0, 255, 8, 8)
+ bounds: 288 352 8 8
+ - image: solid-color(37, 44, 0, 255, 8, 8)
+ bounds: 296 352 8 8
+ - image: solid-color(38, 44, 0, 255, 8, 8)
+ bounds: 304 352 8 8
+ - image: solid-color(39, 44, 0, 255, 8, 8)
+ bounds: 312 352 8 8
+ - image: solid-color(40, 44, 0, 255, 8, 8)
+ bounds: 320 352 8 8
+ - image: solid-color(41, 44, 0, 255, 8, 8)
+ bounds: 328 352 8 8
+ - image: solid-color(42, 44, 0, 255, 8, 8)
+ bounds: 336 352 8 8
+ - image: solid-color(43, 44, 0, 255, 8, 8)
+ bounds: 344 352 8 8
+ - image: solid-color(44, 44, 0, 255, 8, 8)
+ bounds: 352 352 8 8
+ - image: solid-color(45, 44, 0, 255, 8, 8)
+ bounds: 360 352 8 8
+ - image: solid-color(46, 44, 0, 255, 8, 8)
+ bounds: 368 352 8 8
+ - image: solid-color(47, 44, 0, 255, 8, 8)
+ bounds: 376 352 8 8
+ - image: solid-color(48, 44, 0, 255, 8, 8)
+ bounds: 384 352 8 8
+ - image: solid-color(49, 44, 0, 255, 8, 8)
+ bounds: 392 352 8 8
+ - image: solid-color(50, 44, 0, 255, 8, 8)
+ bounds: 400 352 8 8
+ - image: solid-color(51, 44, 0, 255, 8, 8)
+ bounds: 408 352 8 8
+ - image: solid-color(52, 44, 0, 255, 8, 8)
+ bounds: 416 352 8 8
+ - image: solid-color(53, 44, 0, 255, 8, 8)
+ bounds: 424 352 8 8
+ - image: solid-color(54, 44, 0, 255, 8, 8)
+ bounds: 432 352 8 8
+ - image: solid-color(55, 44, 0, 255, 8, 8)
+ bounds: 440 352 8 8
+ - image: solid-color(56, 44, 0, 255, 8, 8)
+ bounds: 448 352 8 8
+ - image: solid-color(57, 44, 0, 255, 8, 8)
+ bounds: 456 352 8 8
+ - image: solid-color(58, 44, 0, 255, 8, 8)
+ bounds: 464 352 8 8
+ - image: solid-color(59, 44, 0, 255, 8, 8)
+ bounds: 472 352 8 8
+ - image: solid-color(60, 44, 0, 255, 8, 8)
+ bounds: 480 352 8 8
+ - image: solid-color(61, 44, 0, 255, 8, 8)
+ bounds: 488 352 8 8
+ - image: solid-color(62, 44, 0, 255, 8, 8)
+ bounds: 496 352 8 8
+ - image: solid-color(63, 44, 0, 255, 8, 8)
+ bounds: 504 352 8 8
+ - image: solid-color(64, 44, 0, 255, 8, 8)
+ bounds: 512 352 8 8
+ - image: solid-color(65, 44, 0, 255, 8, 8)
+ bounds: 520 352 8 8
+ - image: solid-color(66, 44, 0, 255, 8, 8)
+ bounds: 528 352 8 8
+ - image: solid-color(67, 44, 0, 255, 8, 8)
+ bounds: 536 352 8 8
+ - image: solid-color(68, 44, 0, 255, 8, 8)
+ bounds: 544 352 8 8
+ - image: solid-color(69, 44, 0, 255, 8, 8)
+ bounds: 552 352 8 8
+ - image: solid-color(70, 44, 0, 255, 8, 8)
+ bounds: 560 352 8 8
+ - image: solid-color(71, 44, 0, 255, 8, 8)
+ bounds: 568 352 8 8
+ - image: solid-color(72, 44, 0, 255, 8, 8)
+ bounds: 576 352 8 8
+ - image: solid-color(73, 44, 0, 255, 8, 8)
+ bounds: 584 352 8 8
+ - image: solid-color(74, 44, 0, 255, 8, 8)
+ bounds: 592 352 8 8
+ - image: solid-color(75, 44, 0, 255, 8, 8)
+ bounds: 600 352 8 8
+ - image: solid-color(76, 44, 0, 255, 8, 8)
+ bounds: 608 352 8 8
+ - image: solid-color(77, 44, 0, 255, 8, 8)
+ bounds: 616 352 8 8
+ - image: solid-color(78, 44, 0, 255, 8, 8)
+ bounds: 624 352 8 8
+ - image: solid-color(79, 44, 0, 255, 8, 8)
+ bounds: 632 352 8 8
+ - image: solid-color(80, 44, 0, 255, 8, 8)
+ bounds: 640 352 8 8
+ - image: solid-color(81, 44, 0, 255, 8, 8)
+ bounds: 648 352 8 8
+ - image: solid-color(82, 44, 0, 255, 8, 8)
+ bounds: 656 352 8 8
+ - image: solid-color(83, 44, 0, 255, 8, 8)
+ bounds: 664 352 8 8
+ - image: solid-color(84, 44, 0, 255, 8, 8)
+ bounds: 672 352 8 8
+ - image: solid-color(85, 44, 0, 255, 8, 8)
+ bounds: 680 352 8 8
+ - image: solid-color(86, 44, 0, 255, 8, 8)
+ bounds: 688 352 8 8
+ - image: solid-color(87, 44, 0, 255, 8, 8)
+ bounds: 696 352 8 8
+ - image: solid-color(88, 44, 0, 255, 8, 8)
+ bounds: 704 352 8 8
+ - image: solid-color(89, 44, 0, 255, 8, 8)
+ bounds: 712 352 8 8
+ - image: solid-color(90, 44, 0, 255, 8, 8)
+ bounds: 720 352 8 8
+ - image: solid-color(91, 44, 0, 255, 8, 8)
+ bounds: 728 352 8 8
+ - image: solid-color(92, 44, 0, 255, 8, 8)
+ bounds: 736 352 8 8
+ - image: solid-color(93, 44, 0, 255, 8, 8)
+ bounds: 744 352 8 8
+ - image: solid-color(94, 44, 0, 255, 8, 8)
+ bounds: 752 352 8 8
+ - image: solid-color(95, 44, 0, 255, 8, 8)
+ bounds: 760 352 8 8
+ - image: solid-color(96, 44, 0, 255, 8, 8)
+ bounds: 768 352 8 8
+ - image: solid-color(97, 44, 0, 255, 8, 8)
+ bounds: 776 352 8 8
+ - image: solid-color(98, 44, 0, 255, 8, 8)
+ bounds: 784 352 8 8
+ - image: solid-color(99, 44, 0, 255, 8, 8)
+ bounds: 792 352 8 8
+ - image: solid-color(100, 44, 0, 255, 8, 8)
+ bounds: 800 352 8 8
+ - image: solid-color(101, 44, 0, 255, 8, 8)
+ bounds: 808 352 8 8
+ - image: solid-color(102, 44, 0, 255, 8, 8)
+ bounds: 816 352 8 8
+ - image: solid-color(103, 44, 0, 255, 8, 8)
+ bounds: 824 352 8 8
+ - image: solid-color(104, 44, 0, 255, 8, 8)
+ bounds: 832 352 8 8
+ - image: solid-color(105, 44, 0, 255, 8, 8)
+ bounds: 840 352 8 8
+ - image: solid-color(106, 44, 0, 255, 8, 8)
+ bounds: 848 352 8 8
+ - image: solid-color(107, 44, 0, 255, 8, 8)
+ bounds: 856 352 8 8
+ - image: solid-color(108, 44, 0, 255, 8, 8)
+ bounds: 864 352 8 8
+ - image: solid-color(109, 44, 0, 255, 8, 8)
+ bounds: 872 352 8 8
+ - image: solid-color(110, 44, 0, 255, 8, 8)
+ bounds: 880 352 8 8
+ - image: solid-color(111, 44, 0, 255, 8, 8)
+ bounds: 888 352 8 8
+ - image: solid-color(112, 44, 0, 255, 8, 8)
+ bounds: 896 352 8 8
+ - image: solid-color(113, 44, 0, 255, 8, 8)
+ bounds: 904 352 8 8
+ - image: solid-color(114, 44, 0, 255, 8, 8)
+ bounds: 912 352 8 8
+ - image: solid-color(115, 44, 0, 255, 8, 8)
+ bounds: 920 352 8 8
+ - image: solid-color(116, 44, 0, 255, 8, 8)
+ bounds: 928 352 8 8
+ - image: solid-color(117, 44, 0, 255, 8, 8)
+ bounds: 936 352 8 8
+ - image: solid-color(118, 44, 0, 255, 8, 8)
+ bounds: 944 352 8 8
+ - image: solid-color(119, 44, 0, 255, 8, 8)
+ bounds: 952 352 8 8
+ - image: solid-color(120, 44, 0, 255, 8, 8)
+ bounds: 960 352 8 8
+ - image: solid-color(121, 44, 0, 255, 8, 8)
+ bounds: 968 352 8 8
+ - image: solid-color(122, 44, 0, 255, 8, 8)
+ bounds: 976 352 8 8
+ - image: solid-color(123, 44, 0, 255, 8, 8)
+ bounds: 984 352 8 8
+ - image: solid-color(124, 44, 0, 255, 8, 8)
+ bounds: 992 352 8 8
+ - image: solid-color(125, 44, 0, 255, 8, 8)
+ bounds: 1000 352 8 8
+ - image: solid-color(126, 44, 0, 255, 8, 8)
+ bounds: 1008 352 8 8
+ - image: solid-color(127, 44, 0, 255, 8, 8)
+ bounds: 1016 352 8 8
+ - image: solid-color(0, 45, 0, 255, 8, 8)
+ bounds: 0 360 8 8
+ - image: solid-color(1, 45, 0, 255, 8, 8)
+ bounds: 8 360 8 8
+ - image: solid-color(2, 45, 0, 255, 8, 8)
+ bounds: 16 360 8 8
+ - image: solid-color(3, 45, 0, 255, 8, 8)
+ bounds: 24 360 8 8
+ - image: solid-color(4, 45, 0, 255, 8, 8)
+ bounds: 32 360 8 8
+ - image: solid-color(5, 45, 0, 255, 8, 8)
+ bounds: 40 360 8 8
+ - image: solid-color(6, 45, 0, 255, 8, 8)
+ bounds: 48 360 8 8
+ - image: solid-color(7, 45, 0, 255, 8, 8)
+ bounds: 56 360 8 8
+ - image: solid-color(8, 45, 0, 255, 8, 8)
+ bounds: 64 360 8 8
+ - image: solid-color(9, 45, 0, 255, 8, 8)
+ bounds: 72 360 8 8
+ - image: solid-color(10, 45, 0, 255, 8, 8)
+ bounds: 80 360 8 8
+ - image: solid-color(11, 45, 0, 255, 8, 8)
+ bounds: 88 360 8 8
+ - image: solid-color(12, 45, 0, 255, 8, 8)
+ bounds: 96 360 8 8
+ - image: solid-color(13, 45, 0, 255, 8, 8)
+ bounds: 104 360 8 8
+ - image: solid-color(14, 45, 0, 255, 8, 8)
+ bounds: 112 360 8 8
+ - image: solid-color(15, 45, 0, 255, 8, 8)
+ bounds: 120 360 8 8
+ - image: solid-color(16, 45, 0, 255, 8, 8)
+ bounds: 128 360 8 8
+ - image: solid-color(17, 45, 0, 255, 8, 8)
+ bounds: 136 360 8 8
+ - image: solid-color(18, 45, 0, 255, 8, 8)
+ bounds: 144 360 8 8
+ - image: solid-color(19, 45, 0, 255, 8, 8)
+ bounds: 152 360 8 8
+ - image: solid-color(20, 45, 0, 255, 8, 8)
+ bounds: 160 360 8 8
+ - image: solid-color(21, 45, 0, 255, 8, 8)
+ bounds: 168 360 8 8
+ - image: solid-color(22, 45, 0, 255, 8, 8)
+ bounds: 176 360 8 8
+ - image: solid-color(23, 45, 0, 255, 8, 8)
+ bounds: 184 360 8 8
+ - image: solid-color(24, 45, 0, 255, 8, 8)
+ bounds: 192 360 8 8
+ - image: solid-color(25, 45, 0, 255, 8, 8)
+ bounds: 200 360 8 8
+ - image: solid-color(26, 45, 0, 255, 8, 8)
+ bounds: 208 360 8 8
+ - image: solid-color(27, 45, 0, 255, 8, 8)
+ bounds: 216 360 8 8
+ - image: solid-color(28, 45, 0, 255, 8, 8)
+ bounds: 224 360 8 8
+ - image: solid-color(29, 45, 0, 255, 8, 8)
+ bounds: 232 360 8 8
+ - image: solid-color(30, 45, 0, 255, 8, 8)
+ bounds: 240 360 8 8
+ - image: solid-color(31, 45, 0, 255, 8, 8)
+ bounds: 248 360 8 8
+ - image: solid-color(32, 45, 0, 255, 8, 8)
+ bounds: 256 360 8 8
+ - image: solid-color(33, 45, 0, 255, 8, 8)
+ bounds: 264 360 8 8
+ - image: solid-color(34, 45, 0, 255, 8, 8)
+ bounds: 272 360 8 8
+ - image: solid-color(35, 45, 0, 255, 8, 8)
+ bounds: 280 360 8 8
+ - image: solid-color(36, 45, 0, 255, 8, 8)
+ bounds: 288 360 8 8
+ - image: solid-color(37, 45, 0, 255, 8, 8)
+ bounds: 296 360 8 8
+ - image: solid-color(38, 45, 0, 255, 8, 8)
+ bounds: 304 360 8 8
+ - image: solid-color(39, 45, 0, 255, 8, 8)
+ bounds: 312 360 8 8
+ - image: solid-color(40, 45, 0, 255, 8, 8)
+ bounds: 320 360 8 8
+ - image: solid-color(41, 45, 0, 255, 8, 8)
+ bounds: 328 360 8 8
+ - image: solid-color(42, 45, 0, 255, 8, 8)
+ bounds: 336 360 8 8
+ - image: solid-color(43, 45, 0, 255, 8, 8)
+ bounds: 344 360 8 8
+ - image: solid-color(44, 45, 0, 255, 8, 8)
+ bounds: 352 360 8 8
+ - image: solid-color(45, 45, 0, 255, 8, 8)
+ bounds: 360 360 8 8
+ - image: solid-color(46, 45, 0, 255, 8, 8)
+ bounds: 368 360 8 8
+ - image: solid-color(47, 45, 0, 255, 8, 8)
+ bounds: 376 360 8 8
+ - image: solid-color(48, 45, 0, 255, 8, 8)
+ bounds: 384 360 8 8
+ - image: solid-color(49, 45, 0, 255, 8, 8)
+ bounds: 392 360 8 8
+ - image: solid-color(50, 45, 0, 255, 8, 8)
+ bounds: 400 360 8 8
+ - image: solid-color(51, 45, 0, 255, 8, 8)
+ bounds: 408 360 8 8
+ - image: solid-color(52, 45, 0, 255, 8, 8)
+ bounds: 416 360 8 8
+ - image: solid-color(53, 45, 0, 255, 8, 8)
+ bounds: 424 360 8 8
+ - image: solid-color(54, 45, 0, 255, 8, 8)
+ bounds: 432 360 8 8
+ - image: solid-color(55, 45, 0, 255, 8, 8)
+ bounds: 440 360 8 8
+ - image: solid-color(56, 45, 0, 255, 8, 8)
+ bounds: 448 360 8 8
+ - image: solid-color(57, 45, 0, 255, 8, 8)
+ bounds: 456 360 8 8
+ - image: solid-color(58, 45, 0, 255, 8, 8)
+ bounds: 464 360 8 8
+ - image: solid-color(59, 45, 0, 255, 8, 8)
+ bounds: 472 360 8 8
+ - image: solid-color(60, 45, 0, 255, 8, 8)
+ bounds: 480 360 8 8
+ - image: solid-color(61, 45, 0, 255, 8, 8)
+ bounds: 488 360 8 8
+ - image: solid-color(62, 45, 0, 255, 8, 8)
+ bounds: 496 360 8 8
+ - image: solid-color(63, 45, 0, 255, 8, 8)
+ bounds: 504 360 8 8
+ - image: solid-color(64, 45, 0, 255, 8, 8)
+ bounds: 512 360 8 8
+ - image: solid-color(65, 45, 0, 255, 8, 8)
+ bounds: 520 360 8 8
+ - image: solid-color(66, 45, 0, 255, 8, 8)
+ bounds: 528 360 8 8
+ - image: solid-color(67, 45, 0, 255, 8, 8)
+ bounds: 536 360 8 8
+ - image: solid-color(68, 45, 0, 255, 8, 8)
+ bounds: 544 360 8 8
+ - image: solid-color(69, 45, 0, 255, 8, 8)
+ bounds: 552 360 8 8
+ - image: solid-color(70, 45, 0, 255, 8, 8)
+ bounds: 560 360 8 8
+ - image: solid-color(71, 45, 0, 255, 8, 8)
+ bounds: 568 360 8 8
+ - image: solid-color(72, 45, 0, 255, 8, 8)
+ bounds: 576 360 8 8
+ - image: solid-color(73, 45, 0, 255, 8, 8)
+ bounds: 584 360 8 8
+ - image: solid-color(74, 45, 0, 255, 8, 8)
+ bounds: 592 360 8 8
+ - image: solid-color(75, 45, 0, 255, 8, 8)
+ bounds: 600 360 8 8
+ - image: solid-color(76, 45, 0, 255, 8, 8)
+ bounds: 608 360 8 8
+ - image: solid-color(77, 45, 0, 255, 8, 8)
+ bounds: 616 360 8 8
+ - image: solid-color(78, 45, 0, 255, 8, 8)
+ bounds: 624 360 8 8
+ - image: solid-color(79, 45, 0, 255, 8, 8)
+ bounds: 632 360 8 8
+ - image: solid-color(80, 45, 0, 255, 8, 8)
+ bounds: 640 360 8 8
+ - image: solid-color(81, 45, 0, 255, 8, 8)
+ bounds: 648 360 8 8
+ - image: solid-color(82, 45, 0, 255, 8, 8)
+ bounds: 656 360 8 8
+ - image: solid-color(83, 45, 0, 255, 8, 8)
+ bounds: 664 360 8 8
+ - image: solid-color(84, 45, 0, 255, 8, 8)
+ bounds: 672 360 8 8
+ - image: solid-color(85, 45, 0, 255, 8, 8)
+ bounds: 680 360 8 8
+ - image: solid-color(86, 45, 0, 255, 8, 8)
+ bounds: 688 360 8 8
+ - image: solid-color(87, 45, 0, 255, 8, 8)
+ bounds: 696 360 8 8
+ - image: solid-color(88, 45, 0, 255, 8, 8)
+ bounds: 704 360 8 8
+ - image: solid-color(89, 45, 0, 255, 8, 8)
+ bounds: 712 360 8 8
+ - image: solid-color(90, 45, 0, 255, 8, 8)
+ bounds: 720 360 8 8
+ - image: solid-color(91, 45, 0, 255, 8, 8)
+ bounds: 728 360 8 8
+ - image: solid-color(92, 45, 0, 255, 8, 8)
+ bounds: 736 360 8 8
+ - image: solid-color(93, 45, 0, 255, 8, 8)
+ bounds: 744 360 8 8
+ - image: solid-color(94, 45, 0, 255, 8, 8)
+ bounds: 752 360 8 8
+ - image: solid-color(95, 45, 0, 255, 8, 8)
+ bounds: 760 360 8 8
+ - image: solid-color(96, 45, 0, 255, 8, 8)
+ bounds: 768 360 8 8
+ - image: solid-color(97, 45, 0, 255, 8, 8)
+ bounds: 776 360 8 8
+ - image: solid-color(98, 45, 0, 255, 8, 8)
+ bounds: 784 360 8 8
+ - image: solid-color(99, 45, 0, 255, 8, 8)
+ bounds: 792 360 8 8
+ - image: solid-color(100, 45, 0, 255, 8, 8)
+ bounds: 800 360 8 8
+ - image: solid-color(101, 45, 0, 255, 8, 8)
+ bounds: 808 360 8 8
+ - image: solid-color(102, 45, 0, 255, 8, 8)
+ bounds: 816 360 8 8
+ - image: solid-color(103, 45, 0, 255, 8, 8)
+ bounds: 824 360 8 8
+ - image: solid-color(104, 45, 0, 255, 8, 8)
+ bounds: 832 360 8 8
+ - image: solid-color(105, 45, 0, 255, 8, 8)
+ bounds: 840 360 8 8
+ - image: solid-color(106, 45, 0, 255, 8, 8)
+ bounds: 848 360 8 8
+ - image: solid-color(107, 45, 0, 255, 8, 8)
+ bounds: 856 360 8 8
+ - image: solid-color(108, 45, 0, 255, 8, 8)
+ bounds: 864 360 8 8
+ - image: solid-color(109, 45, 0, 255, 8, 8)
+ bounds: 872 360 8 8
+ - image: solid-color(110, 45, 0, 255, 8, 8)
+ bounds: 880 360 8 8
+ - image: solid-color(111, 45, 0, 255, 8, 8)
+ bounds: 888 360 8 8
+ - image: solid-color(112, 45, 0, 255, 8, 8)
+ bounds: 896 360 8 8
+ - image: solid-color(113, 45, 0, 255, 8, 8)
+ bounds: 904 360 8 8
+ - image: solid-color(114, 45, 0, 255, 8, 8)
+ bounds: 912 360 8 8
+ - image: solid-color(115, 45, 0, 255, 8, 8)
+ bounds: 920 360 8 8
+ - image: solid-color(116, 45, 0, 255, 8, 8)
+ bounds: 928 360 8 8
+ - image: solid-color(117, 45, 0, 255, 8, 8)
+ bounds: 936 360 8 8
+ - image: solid-color(118, 45, 0, 255, 8, 8)
+ bounds: 944 360 8 8
+ - image: solid-color(119, 45, 0, 255, 8, 8)
+ bounds: 952 360 8 8
+ - image: solid-color(120, 45, 0, 255, 8, 8)
+ bounds: 960 360 8 8
+ - image: solid-color(121, 45, 0, 255, 8, 8)
+ bounds: 968 360 8 8
+ - image: solid-color(122, 45, 0, 255, 8, 8)
+ bounds: 976 360 8 8
+ - image: solid-color(123, 45, 0, 255, 8, 8)
+ bounds: 984 360 8 8
+ - image: solid-color(124, 45, 0, 255, 8, 8)
+ bounds: 992 360 8 8
+ - image: solid-color(125, 45, 0, 255, 8, 8)
+ bounds: 1000 360 8 8
+ - image: solid-color(126, 45, 0, 255, 8, 8)
+ bounds: 1008 360 8 8
+ - image: solid-color(127, 45, 0, 255, 8, 8)
+ bounds: 1016 360 8 8
+ - image: solid-color(0, 46, 0, 255, 8, 8)
+ bounds: 0 368 8 8
+ - image: solid-color(1, 46, 0, 255, 8, 8)
+ bounds: 8 368 8 8
+ - image: solid-color(2, 46, 0, 255, 8, 8)
+ bounds: 16 368 8 8
+ - image: solid-color(3, 46, 0, 255, 8, 8)
+ bounds: 24 368 8 8
+ - image: solid-color(4, 46, 0, 255, 8, 8)
+ bounds: 32 368 8 8
+ - image: solid-color(5, 46, 0, 255, 8, 8)
+ bounds: 40 368 8 8
+ - image: solid-color(6, 46, 0, 255, 8, 8)
+ bounds: 48 368 8 8
+ - image: solid-color(7, 46, 0, 255, 8, 8)
+ bounds: 56 368 8 8
+ - image: solid-color(8, 46, 0, 255, 8, 8)
+ bounds: 64 368 8 8
+ - image: solid-color(9, 46, 0, 255, 8, 8)
+ bounds: 72 368 8 8
+ - image: solid-color(10, 46, 0, 255, 8, 8)
+ bounds: 80 368 8 8
+ - image: solid-color(11, 46, 0, 255, 8, 8)
+ bounds: 88 368 8 8
+ - image: solid-color(12, 46, 0, 255, 8, 8)
+ bounds: 96 368 8 8
+ - image: solid-color(13, 46, 0, 255, 8, 8)
+ bounds: 104 368 8 8
+ - image: solid-color(14, 46, 0, 255, 8, 8)
+ bounds: 112 368 8 8
+ - image: solid-color(15, 46, 0, 255, 8, 8)
+ bounds: 120 368 8 8
+ - image: solid-color(16, 46, 0, 255, 8, 8)
+ bounds: 128 368 8 8
+ - image: solid-color(17, 46, 0, 255, 8, 8)
+ bounds: 136 368 8 8
+ - image: solid-color(18, 46, 0, 255, 8, 8)
+ bounds: 144 368 8 8
+ - image: solid-color(19, 46, 0, 255, 8, 8)
+ bounds: 152 368 8 8
+ - image: solid-color(20, 46, 0, 255, 8, 8)
+ bounds: 160 368 8 8
+ - image: solid-color(21, 46, 0, 255, 8, 8)
+ bounds: 168 368 8 8
+ - image: solid-color(22, 46, 0, 255, 8, 8)
+ bounds: 176 368 8 8
+ - image: solid-color(23, 46, 0, 255, 8, 8)
+ bounds: 184 368 8 8
+ - image: solid-color(24, 46, 0, 255, 8, 8)
+ bounds: 192 368 8 8
+ - image: solid-color(25, 46, 0, 255, 8, 8)
+ bounds: 200 368 8 8
+ - image: solid-color(26, 46, 0, 255, 8, 8)
+ bounds: 208 368 8 8
+ - image: solid-color(27, 46, 0, 255, 8, 8)
+ bounds: 216 368 8 8
+ - image: solid-color(28, 46, 0, 255, 8, 8)
+ bounds: 224 368 8 8
+ - image: solid-color(29, 46, 0, 255, 8, 8)
+ bounds: 232 368 8 8
+ - image: solid-color(30, 46, 0, 255, 8, 8)
+ bounds: 240 368 8 8
+ - image: solid-color(31, 46, 0, 255, 8, 8)
+ bounds: 248 368 8 8
+ - image: solid-color(32, 46, 0, 255, 8, 8)
+ bounds: 256 368 8 8
+ - image: solid-color(33, 46, 0, 255, 8, 8)
+ bounds: 264 368 8 8
+ - image: solid-color(34, 46, 0, 255, 8, 8)
+ bounds: 272 368 8 8
+ - image: solid-color(35, 46, 0, 255, 8, 8)
+ bounds: 280 368 8 8
+ - image: solid-color(36, 46, 0, 255, 8, 8)
+ bounds: 288 368 8 8
+ - image: solid-color(37, 46, 0, 255, 8, 8)
+ bounds: 296 368 8 8
+ - image: solid-color(38, 46, 0, 255, 8, 8)
+ bounds: 304 368 8 8
+ - image: solid-color(39, 46, 0, 255, 8, 8)
+ bounds: 312 368 8 8
+ - image: solid-color(40, 46, 0, 255, 8, 8)
+ bounds: 320 368 8 8
+ - image: solid-color(41, 46, 0, 255, 8, 8)
+ bounds: 328 368 8 8
+ - image: solid-color(42, 46, 0, 255, 8, 8)
+ bounds: 336 368 8 8
+ - image: solid-color(43, 46, 0, 255, 8, 8)
+ bounds: 344 368 8 8
+ - image: solid-color(44, 46, 0, 255, 8, 8)
+ bounds: 352 368 8 8
+ - image: solid-color(45, 46, 0, 255, 8, 8)
+ bounds: 360 368 8 8
+ - image: solid-color(46, 46, 0, 255, 8, 8)
+ bounds: 368 368 8 8
+ - image: solid-color(47, 46, 0, 255, 8, 8)
+ bounds: 376 368 8 8
+ - image: solid-color(48, 46, 0, 255, 8, 8)
+ bounds: 384 368 8 8
+ - image: solid-color(49, 46, 0, 255, 8, 8)
+ bounds: 392 368 8 8
+ - image: solid-color(50, 46, 0, 255, 8, 8)
+ bounds: 400 368 8 8
+ - image: solid-color(51, 46, 0, 255, 8, 8)
+ bounds: 408 368 8 8
+ - image: solid-color(52, 46, 0, 255, 8, 8)
+ bounds: 416 368 8 8
+ - image: solid-color(53, 46, 0, 255, 8, 8)
+ bounds: 424 368 8 8
+ - image: solid-color(54, 46, 0, 255, 8, 8)
+ bounds: 432 368 8 8
+ - image: solid-color(55, 46, 0, 255, 8, 8)
+ bounds: 440 368 8 8
+ - image: solid-color(56, 46, 0, 255, 8, 8)
+ bounds: 448 368 8 8
+ - image: solid-color(57, 46, 0, 255, 8, 8)
+ bounds: 456 368 8 8
+ - image: solid-color(58, 46, 0, 255, 8, 8)
+ bounds: 464 368 8 8
+ - image: solid-color(59, 46, 0, 255, 8, 8)
+ bounds: 472 368 8 8
+ - image: solid-color(60, 46, 0, 255, 8, 8)
+ bounds: 480 368 8 8
+ - image: solid-color(61, 46, 0, 255, 8, 8)
+ bounds: 488 368 8 8
+ - image: solid-color(62, 46, 0, 255, 8, 8)
+ bounds: 496 368 8 8
+ - image: solid-color(63, 46, 0, 255, 8, 8)
+ bounds: 504 368 8 8
+ - image: solid-color(64, 46, 0, 255, 8, 8)
+ bounds: 512 368 8 8
+ - image: solid-color(65, 46, 0, 255, 8, 8)
+ bounds: 520 368 8 8
+ - image: solid-color(66, 46, 0, 255, 8, 8)
+ bounds: 528 368 8 8
+ - image: solid-color(67, 46, 0, 255, 8, 8)
+ bounds: 536 368 8 8
+ - image: solid-color(68, 46, 0, 255, 8, 8)
+ bounds: 544 368 8 8
+ - image: solid-color(69, 46, 0, 255, 8, 8)
+ bounds: 552 368 8 8
+ - image: solid-color(70, 46, 0, 255, 8, 8)
+ bounds: 560 368 8 8
+ - image: solid-color(71, 46, 0, 255, 8, 8)
+ bounds: 568 368 8 8
+ - image: solid-color(72, 46, 0, 255, 8, 8)
+ bounds: 576 368 8 8
+ - image: solid-color(73, 46, 0, 255, 8, 8)
+ bounds: 584 368 8 8
+ - image: solid-color(74, 46, 0, 255, 8, 8)
+ bounds: 592 368 8 8
+ - image: solid-color(75, 46, 0, 255, 8, 8)
+ bounds: 600 368 8 8
+ - image: solid-color(76, 46, 0, 255, 8, 8)
+ bounds: 608 368 8 8
+ - image: solid-color(77, 46, 0, 255, 8, 8)
+ bounds: 616 368 8 8
+ - image: solid-color(78, 46, 0, 255, 8, 8)
+ bounds: 624 368 8 8
+ - image: solid-color(79, 46, 0, 255, 8, 8)
+ bounds: 632 368 8 8
+ - image: solid-color(80, 46, 0, 255, 8, 8)
+ bounds: 640 368 8 8
+ - image: solid-color(81, 46, 0, 255, 8, 8)
+ bounds: 648 368 8 8
+ - image: solid-color(82, 46, 0, 255, 8, 8)
+ bounds: 656 368 8 8
+ - image: solid-color(83, 46, 0, 255, 8, 8)
+ bounds: 664 368 8 8
+ - image: solid-color(84, 46, 0, 255, 8, 8)
+ bounds: 672 368 8 8
+ - image: solid-color(85, 46, 0, 255, 8, 8)
+ bounds: 680 368 8 8
+ - image: solid-color(86, 46, 0, 255, 8, 8)
+ bounds: 688 368 8 8
+ - image: solid-color(87, 46, 0, 255, 8, 8)
+ bounds: 696 368 8 8
+ - image: solid-color(88, 46, 0, 255, 8, 8)
+ bounds: 704 368 8 8
+ - image: solid-color(89, 46, 0, 255, 8, 8)
+ bounds: 712 368 8 8
+ - image: solid-color(90, 46, 0, 255, 8, 8)
+ bounds: 720 368 8 8
+ - image: solid-color(91, 46, 0, 255, 8, 8)
+ bounds: 728 368 8 8
+ - image: solid-color(92, 46, 0, 255, 8, 8)
+ bounds: 736 368 8 8
+ - image: solid-color(93, 46, 0, 255, 8, 8)
+ bounds: 744 368 8 8
+ - image: solid-color(94, 46, 0, 255, 8, 8)
+ bounds: 752 368 8 8
+ - image: solid-color(95, 46, 0, 255, 8, 8)
+ bounds: 760 368 8 8
+ - image: solid-color(96, 46, 0, 255, 8, 8)
+ bounds: 768 368 8 8
+ - image: solid-color(97, 46, 0, 255, 8, 8)
+ bounds: 776 368 8 8
+ - image: solid-color(98, 46, 0, 255, 8, 8)
+ bounds: 784 368 8 8
+ - image: solid-color(99, 46, 0, 255, 8, 8)
+ bounds: 792 368 8 8
+ - image: solid-color(100, 46, 0, 255, 8, 8)
+ bounds: 800 368 8 8
+ - image: solid-color(101, 46, 0, 255, 8, 8)
+ bounds: 808 368 8 8
+ - image: solid-color(102, 46, 0, 255, 8, 8)
+ bounds: 816 368 8 8
+ - image: solid-color(103, 46, 0, 255, 8, 8)
+ bounds: 824 368 8 8
+ - image: solid-color(104, 46, 0, 255, 8, 8)
+ bounds: 832 368 8 8
+ - image: solid-color(105, 46, 0, 255, 8, 8)
+ bounds: 840 368 8 8
+ - image: solid-color(106, 46, 0, 255, 8, 8)
+ bounds: 848 368 8 8
+ - image: solid-color(107, 46, 0, 255, 8, 8)
+ bounds: 856 368 8 8
+ - image: solid-color(108, 46, 0, 255, 8, 8)
+ bounds: 864 368 8 8
+ - image: solid-color(109, 46, 0, 255, 8, 8)
+ bounds: 872 368 8 8
+ - image: solid-color(110, 46, 0, 255, 8, 8)
+ bounds: 880 368 8 8
+ - image: solid-color(111, 46, 0, 255, 8, 8)
+ bounds: 888 368 8 8
+ - image: solid-color(112, 46, 0, 255, 8, 8)
+ bounds: 896 368 8 8
+ - image: solid-color(113, 46, 0, 255, 8, 8)
+ bounds: 904 368 8 8
+ - image: solid-color(114, 46, 0, 255, 8, 8)
+ bounds: 912 368 8 8
+ - image: solid-color(115, 46, 0, 255, 8, 8)
+ bounds: 920 368 8 8
+ - image: solid-color(116, 46, 0, 255, 8, 8)
+ bounds: 928 368 8 8
+ - image: solid-color(117, 46, 0, 255, 8, 8)
+ bounds: 936 368 8 8
+ - image: solid-color(118, 46, 0, 255, 8, 8)
+ bounds: 944 368 8 8
+ - image: solid-color(119, 46, 0, 255, 8, 8)
+ bounds: 952 368 8 8
+ - image: solid-color(120, 46, 0, 255, 8, 8)
+ bounds: 960 368 8 8
+ - image: solid-color(121, 46, 0, 255, 8, 8)
+ bounds: 968 368 8 8
+ - image: solid-color(122, 46, 0, 255, 8, 8)
+ bounds: 976 368 8 8
+ - image: solid-color(123, 46, 0, 255, 8, 8)
+ bounds: 984 368 8 8
+ - image: solid-color(124, 46, 0, 255, 8, 8)
+ bounds: 992 368 8 8
+ - image: solid-color(125, 46, 0, 255, 8, 8)
+ bounds: 1000 368 8 8
+ - image: solid-color(126, 46, 0, 255, 8, 8)
+ bounds: 1008 368 8 8
+ - image: solid-color(127, 46, 0, 255, 8, 8)
+ bounds: 1016 368 8 8
+ - image: solid-color(0, 47, 0, 255, 8, 8)
+ bounds: 0 376 8 8
+ - image: solid-color(1, 47, 0, 255, 8, 8)
+ bounds: 8 376 8 8
+ - image: solid-color(2, 47, 0, 255, 8, 8)
+ bounds: 16 376 8 8
+ - image: solid-color(3, 47, 0, 255, 8, 8)
+ bounds: 24 376 8 8
+ - image: solid-color(4, 47, 0, 255, 8, 8)
+ bounds: 32 376 8 8
+ - image: solid-color(5, 47, 0, 255, 8, 8)
+ bounds: 40 376 8 8
+ - image: solid-color(6, 47, 0, 255, 8, 8)
+ bounds: 48 376 8 8
+ - image: solid-color(7, 47, 0, 255, 8, 8)
+ bounds: 56 376 8 8
+ - image: solid-color(8, 47, 0, 255, 8, 8)
+ bounds: 64 376 8 8
+ - image: solid-color(9, 47, 0, 255, 8, 8)
+ bounds: 72 376 8 8
+ - image: solid-color(10, 47, 0, 255, 8, 8)
+ bounds: 80 376 8 8
+ - image: solid-color(11, 47, 0, 255, 8, 8)
+ bounds: 88 376 8 8
+ - image: solid-color(12, 47, 0, 255, 8, 8)
+ bounds: 96 376 8 8
+ - image: solid-color(13, 47, 0, 255, 8, 8)
+ bounds: 104 376 8 8
+ - image: solid-color(14, 47, 0, 255, 8, 8)
+ bounds: 112 376 8 8
+ - image: solid-color(15, 47, 0, 255, 8, 8)
+ bounds: 120 376 8 8
+ - image: solid-color(16, 47, 0, 255, 8, 8)
+ bounds: 128 376 8 8
+ - image: solid-color(17, 47, 0, 255, 8, 8)
+ bounds: 136 376 8 8
+ - image: solid-color(18, 47, 0, 255, 8, 8)
+ bounds: 144 376 8 8
+ - image: solid-color(19, 47, 0, 255, 8, 8)
+ bounds: 152 376 8 8
+ - image: solid-color(20, 47, 0, 255, 8, 8)
+ bounds: 160 376 8 8
+ - image: solid-color(21, 47, 0, 255, 8, 8)
+ bounds: 168 376 8 8
+ - image: solid-color(22, 47, 0, 255, 8, 8)
+ bounds: 176 376 8 8
+ - image: solid-color(23, 47, 0, 255, 8, 8)
+ bounds: 184 376 8 8
+ - image: solid-color(24, 47, 0, 255, 8, 8)
+ bounds: 192 376 8 8
+ - image: solid-color(25, 47, 0, 255, 8, 8)
+ bounds: 200 376 8 8
+ - image: solid-color(26, 47, 0, 255, 8, 8)
+ bounds: 208 376 8 8
+ - image: solid-color(27, 47, 0, 255, 8, 8)
+ bounds: 216 376 8 8
+ - image: solid-color(28, 47, 0, 255, 8, 8)
+ bounds: 224 376 8 8
+ - image: solid-color(29, 47, 0, 255, 8, 8)
+ bounds: 232 376 8 8
+ - image: solid-color(30, 47, 0, 255, 8, 8)
+ bounds: 240 376 8 8
+ - image: solid-color(31, 47, 0, 255, 8, 8)
+ bounds: 248 376 8 8
+ - image: solid-color(32, 47, 0, 255, 8, 8)
+ bounds: 256 376 8 8
+ - image: solid-color(33, 47, 0, 255, 8, 8)
+ bounds: 264 376 8 8
+ - image: solid-color(34, 47, 0, 255, 8, 8)
+ bounds: 272 376 8 8
+ - image: solid-color(35, 47, 0, 255, 8, 8)
+ bounds: 280 376 8 8
+ - image: solid-color(36, 47, 0, 255, 8, 8)
+ bounds: 288 376 8 8
+ - image: solid-color(37, 47, 0, 255, 8, 8)
+ bounds: 296 376 8 8
+ - image: solid-color(38, 47, 0, 255, 8, 8)
+ bounds: 304 376 8 8
+ - image: solid-color(39, 47, 0, 255, 8, 8)
+ bounds: 312 376 8 8
+ - image: solid-color(40, 47, 0, 255, 8, 8)
+ bounds: 320 376 8 8
+ - image: solid-color(41, 47, 0, 255, 8, 8)
+ bounds: 328 376 8 8
+ - image: solid-color(42, 47, 0, 255, 8, 8)
+ bounds: 336 376 8 8
+ - image: solid-color(43, 47, 0, 255, 8, 8)
+ bounds: 344 376 8 8
+ - image: solid-color(44, 47, 0, 255, 8, 8)
+ bounds: 352 376 8 8
+ - image: solid-color(45, 47, 0, 255, 8, 8)
+ bounds: 360 376 8 8
+ - image: solid-color(46, 47, 0, 255, 8, 8)
+ bounds: 368 376 8 8
+ - image: solid-color(47, 47, 0, 255, 8, 8)
+ bounds: 376 376 8 8
+ - image: solid-color(48, 47, 0, 255, 8, 8)
+ bounds: 384 376 8 8
+ - image: solid-color(49, 47, 0, 255, 8, 8)
+ bounds: 392 376 8 8
+ - image: solid-color(50, 47, 0, 255, 8, 8)
+ bounds: 400 376 8 8
+ - image: solid-color(51, 47, 0, 255, 8, 8)
+ bounds: 408 376 8 8
+ - image: solid-color(52, 47, 0, 255, 8, 8)
+ bounds: 416 376 8 8
+ - image: solid-color(53, 47, 0, 255, 8, 8)
+ bounds: 424 376 8 8
+ - image: solid-color(54, 47, 0, 255, 8, 8)
+ bounds: 432 376 8 8
+ - image: solid-color(55, 47, 0, 255, 8, 8)
+ bounds: 440 376 8 8
+ - image: solid-color(56, 47, 0, 255, 8, 8)
+ bounds: 448 376 8 8
+ - image: solid-color(57, 47, 0, 255, 8, 8)
+ bounds: 456 376 8 8
+ - image: solid-color(58, 47, 0, 255, 8, 8)
+ bounds: 464 376 8 8
+ - image: solid-color(59, 47, 0, 255, 8, 8)
+ bounds: 472 376 8 8
+ - image: solid-color(60, 47, 0, 255, 8, 8)
+ bounds: 480 376 8 8
+ - image: solid-color(61, 47, 0, 255, 8, 8)
+ bounds: 488 376 8 8
+ - image: solid-color(62, 47, 0, 255, 8, 8)
+ bounds: 496 376 8 8
+ - image: solid-color(63, 47, 0, 255, 8, 8)
+ bounds: 504 376 8 8
+ - image: solid-color(64, 47, 0, 255, 8, 8)
+ bounds: 512 376 8 8
+ - image: solid-color(65, 47, 0, 255, 8, 8)
+ bounds: 520 376 8 8
+ - image: solid-color(66, 47, 0, 255, 8, 8)
+ bounds: 528 376 8 8
+ - image: solid-color(67, 47, 0, 255, 8, 8)
+ bounds: 536 376 8 8
+ - image: solid-color(68, 47, 0, 255, 8, 8)
+ bounds: 544 376 8 8
+ - image: solid-color(69, 47, 0, 255, 8, 8)
+ bounds: 552 376 8 8
+ - image: solid-color(70, 47, 0, 255, 8, 8)
+ bounds: 560 376 8 8
+ - image: solid-color(71, 47, 0, 255, 8, 8)
+ bounds: 568 376 8 8
+ - image: solid-color(72, 47, 0, 255, 8, 8)
+ bounds: 576 376 8 8
+ - image: solid-color(73, 47, 0, 255, 8, 8)
+ bounds: 584 376 8 8
+ - image: solid-color(74, 47, 0, 255, 8, 8)
+ bounds: 592 376 8 8
+ - image: solid-color(75, 47, 0, 255, 8, 8)
+ bounds: 600 376 8 8
+ - image: solid-color(76, 47, 0, 255, 8, 8)
+ bounds: 608 376 8 8
+ - image: solid-color(77, 47, 0, 255, 8, 8)
+ bounds: 616 376 8 8
+ - image: solid-color(78, 47, 0, 255, 8, 8)
+ bounds: 624 376 8 8
+ - image: solid-color(79, 47, 0, 255, 8, 8)
+ bounds: 632 376 8 8
+ - image: solid-color(80, 47, 0, 255, 8, 8)
+ bounds: 640 376 8 8
+ - image: solid-color(81, 47, 0, 255, 8, 8)
+ bounds: 648 376 8 8
+ - image: solid-color(82, 47, 0, 255, 8, 8)
+ bounds: 656 376 8 8
+ - image: solid-color(83, 47, 0, 255, 8, 8)
+ bounds: 664 376 8 8
+ - image: solid-color(84, 47, 0, 255, 8, 8)
+ bounds: 672 376 8 8
+ - image: solid-color(85, 47, 0, 255, 8, 8)
+ bounds: 680 376 8 8
+ - image: solid-color(86, 47, 0, 255, 8, 8)
+ bounds: 688 376 8 8
+ - image: solid-color(87, 47, 0, 255, 8, 8)
+ bounds: 696 376 8 8
+ - image: solid-color(88, 47, 0, 255, 8, 8)
+ bounds: 704 376 8 8
+ - image: solid-color(89, 47, 0, 255, 8, 8)
+ bounds: 712 376 8 8
+ - image: solid-color(90, 47, 0, 255, 8, 8)
+ bounds: 720 376 8 8
+ - image: solid-color(91, 47, 0, 255, 8, 8)
+ bounds: 728 376 8 8
+ - image: solid-color(92, 47, 0, 255, 8, 8)
+ bounds: 736 376 8 8
+ - image: solid-color(93, 47, 0, 255, 8, 8)
+ bounds: 744 376 8 8
+ - image: solid-color(94, 47, 0, 255, 8, 8)
+ bounds: 752 376 8 8
+ - image: solid-color(95, 47, 0, 255, 8, 8)
+ bounds: 760 376 8 8
+ - image: solid-color(96, 47, 0, 255, 8, 8)
+ bounds: 768 376 8 8
+ - image: solid-color(97, 47, 0, 255, 8, 8)
+ bounds: 776 376 8 8
+ - image: solid-color(98, 47, 0, 255, 8, 8)
+ bounds: 784 376 8 8
+ - image: solid-color(99, 47, 0, 255, 8, 8)
+ bounds: 792 376 8 8
+ - image: solid-color(100, 47, 0, 255, 8, 8)
+ bounds: 800 376 8 8
+ - image: solid-color(101, 47, 0, 255, 8, 8)
+ bounds: 808 376 8 8
+ - image: solid-color(102, 47, 0, 255, 8, 8)
+ bounds: 816 376 8 8
+ - image: solid-color(103, 47, 0, 255, 8, 8)
+ bounds: 824 376 8 8
+ - image: solid-color(104, 47, 0, 255, 8, 8)
+ bounds: 832 376 8 8
+ - image: solid-color(105, 47, 0, 255, 8, 8)
+ bounds: 840 376 8 8
+ - image: solid-color(106, 47, 0, 255, 8, 8)
+ bounds: 848 376 8 8
+ - image: solid-color(107, 47, 0, 255, 8, 8)
+ bounds: 856 376 8 8
+ - image: solid-color(108, 47, 0, 255, 8, 8)
+ bounds: 864 376 8 8
+ - image: solid-color(109, 47, 0, 255, 8, 8)
+ bounds: 872 376 8 8
+ - image: solid-color(110, 47, 0, 255, 8, 8)
+ bounds: 880 376 8 8
+ - image: solid-color(111, 47, 0, 255, 8, 8)
+ bounds: 888 376 8 8
+ - image: solid-color(112, 47, 0, 255, 8, 8)
+ bounds: 896 376 8 8
+ - image: solid-color(113, 47, 0, 255, 8, 8)
+ bounds: 904 376 8 8
+ - image: solid-color(114, 47, 0, 255, 8, 8)
+ bounds: 912 376 8 8
+ - image: solid-color(115, 47, 0, 255, 8, 8)
+ bounds: 920 376 8 8
+ - image: solid-color(116, 47, 0, 255, 8, 8)
+ bounds: 928 376 8 8
+ - image: solid-color(117, 47, 0, 255, 8, 8)
+ bounds: 936 376 8 8
+ - image: solid-color(118, 47, 0, 255, 8, 8)
+ bounds: 944 376 8 8
+ - image: solid-color(119, 47, 0, 255, 8, 8)
+ bounds: 952 376 8 8
+ - image: solid-color(120, 47, 0, 255, 8, 8)
+ bounds: 960 376 8 8
+ - image: solid-color(121, 47, 0, 255, 8, 8)
+ bounds: 968 376 8 8
+ - image: solid-color(122, 47, 0, 255, 8, 8)
+ bounds: 976 376 8 8
+ - image: solid-color(123, 47, 0, 255, 8, 8)
+ bounds: 984 376 8 8
+ - image: solid-color(124, 47, 0, 255, 8, 8)
+ bounds: 992 376 8 8
+ - image: solid-color(125, 47, 0, 255, 8, 8)
+ bounds: 1000 376 8 8
+ - image: solid-color(126, 47, 0, 255, 8, 8)
+ bounds: 1008 376 8 8
+ - image: solid-color(127, 47, 0, 255, 8, 8)
+ bounds: 1016 376 8 8
+ - image: solid-color(0, 48, 0, 255, 8, 8)
+ bounds: 0 384 8 8
+ - image: solid-color(1, 48, 0, 255, 8, 8)
+ bounds: 8 384 8 8
+ - image: solid-color(2, 48, 0, 255, 8, 8)
+ bounds: 16 384 8 8
+ - image: solid-color(3, 48, 0, 255, 8, 8)
+ bounds: 24 384 8 8
+ - image: solid-color(4, 48, 0, 255, 8, 8)
+ bounds: 32 384 8 8
+ - image: solid-color(5, 48, 0, 255, 8, 8)
+ bounds: 40 384 8 8
+ - image: solid-color(6, 48, 0, 255, 8, 8)
+ bounds: 48 384 8 8
+ - image: solid-color(7, 48, 0, 255, 8, 8)
+ bounds: 56 384 8 8
+ - image: solid-color(8, 48, 0, 255, 8, 8)
+ bounds: 64 384 8 8
+ - image: solid-color(9, 48, 0, 255, 8, 8)
+ bounds: 72 384 8 8
+ - image: solid-color(10, 48, 0, 255, 8, 8)
+ bounds: 80 384 8 8
+ - image: solid-color(11, 48, 0, 255, 8, 8)
+ bounds: 88 384 8 8
+ - image: solid-color(12, 48, 0, 255, 8, 8)
+ bounds: 96 384 8 8
+ - image: solid-color(13, 48, 0, 255, 8, 8)
+ bounds: 104 384 8 8
+ - image: solid-color(14, 48, 0, 255, 8, 8)
+ bounds: 112 384 8 8
+ - image: solid-color(15, 48, 0, 255, 8, 8)
+ bounds: 120 384 8 8
+ - image: solid-color(16, 48, 0, 255, 8, 8)
+ bounds: 128 384 8 8
+ - image: solid-color(17, 48, 0, 255, 8, 8)
+ bounds: 136 384 8 8
+ - image: solid-color(18, 48, 0, 255, 8, 8)
+ bounds: 144 384 8 8
+ - image: solid-color(19, 48, 0, 255, 8, 8)
+ bounds: 152 384 8 8
+ - image: solid-color(20, 48, 0, 255, 8, 8)
+ bounds: 160 384 8 8
+ - image: solid-color(21, 48, 0, 255, 8, 8)
+ bounds: 168 384 8 8
+ - image: solid-color(22, 48, 0, 255, 8, 8)
+ bounds: 176 384 8 8
+ - image: solid-color(23, 48, 0, 255, 8, 8)
+ bounds: 184 384 8 8
+ - image: solid-color(24, 48, 0, 255, 8, 8)
+ bounds: 192 384 8 8
+ - image: solid-color(25, 48, 0, 255, 8, 8)
+ bounds: 200 384 8 8
+ - image: solid-color(26, 48, 0, 255, 8, 8)
+ bounds: 208 384 8 8
+ - image: solid-color(27, 48, 0, 255, 8, 8)
+ bounds: 216 384 8 8
+ - image: solid-color(28, 48, 0, 255, 8, 8)
+ bounds: 224 384 8 8
+ - image: solid-color(29, 48, 0, 255, 8, 8)
+ bounds: 232 384 8 8
+ - image: solid-color(30, 48, 0, 255, 8, 8)
+ bounds: 240 384 8 8
+ - image: solid-color(31, 48, 0, 255, 8, 8)
+ bounds: 248 384 8 8
+ - image: solid-color(32, 48, 0, 255, 8, 8)
+ bounds: 256 384 8 8
+ - image: solid-color(33, 48, 0, 255, 8, 8)
+ bounds: 264 384 8 8
+ - image: solid-color(34, 48, 0, 255, 8, 8)
+ bounds: 272 384 8 8
+ - image: solid-color(35, 48, 0, 255, 8, 8)
+ bounds: 280 384 8 8
+ - image: solid-color(36, 48, 0, 255, 8, 8)
+ bounds: 288 384 8 8
+ - image: solid-color(37, 48, 0, 255, 8, 8)
+ bounds: 296 384 8 8
+ - image: solid-color(38, 48, 0, 255, 8, 8)
+ bounds: 304 384 8 8
+ - image: solid-color(39, 48, 0, 255, 8, 8)
+ bounds: 312 384 8 8
+ - image: solid-color(40, 48, 0, 255, 8, 8)
+ bounds: 320 384 8 8
+ - image: solid-color(41, 48, 0, 255, 8, 8)
+ bounds: 328 384 8 8
+ - image: solid-color(42, 48, 0, 255, 8, 8)
+ bounds: 336 384 8 8
+ - image: solid-color(43, 48, 0, 255, 8, 8)
+ bounds: 344 384 8 8
+ - image: solid-color(44, 48, 0, 255, 8, 8)
+ bounds: 352 384 8 8
+ - image: solid-color(45, 48, 0, 255, 8, 8)
+ bounds: 360 384 8 8
+ - image: solid-color(46, 48, 0, 255, 8, 8)
+ bounds: 368 384 8 8
+ - image: solid-color(47, 48, 0, 255, 8, 8)
+ bounds: 376 384 8 8
+ - image: solid-color(48, 48, 0, 255, 8, 8)
+ bounds: 384 384 8 8
+ - image: solid-color(49, 48, 0, 255, 8, 8)
+ bounds: 392 384 8 8
+ - image: solid-color(50, 48, 0, 255, 8, 8)
+ bounds: 400 384 8 8
+ - image: solid-color(51, 48, 0, 255, 8, 8)
+ bounds: 408 384 8 8
+ - image: solid-color(52, 48, 0, 255, 8, 8)
+ bounds: 416 384 8 8
+ - image: solid-color(53, 48, 0, 255, 8, 8)
+ bounds: 424 384 8 8
+ - image: solid-color(54, 48, 0, 255, 8, 8)
+ bounds: 432 384 8 8
+ - image: solid-color(55, 48, 0, 255, 8, 8)
+ bounds: 440 384 8 8
+ - image: solid-color(56, 48, 0, 255, 8, 8)
+ bounds: 448 384 8 8
+ - image: solid-color(57, 48, 0, 255, 8, 8)
+ bounds: 456 384 8 8
+ - image: solid-color(58, 48, 0, 255, 8, 8)
+ bounds: 464 384 8 8
+ - image: solid-color(59, 48, 0, 255, 8, 8)
+ bounds: 472 384 8 8
+ - image: solid-color(60, 48, 0, 255, 8, 8)
+ bounds: 480 384 8 8
+ - image: solid-color(61, 48, 0, 255, 8, 8)
+ bounds: 488 384 8 8
+ - image: solid-color(62, 48, 0, 255, 8, 8)
+ bounds: 496 384 8 8
+ - image: solid-color(63, 48, 0, 255, 8, 8)
+ bounds: 504 384 8 8
+ - image: solid-color(64, 48, 0, 255, 8, 8)
+ bounds: 512 384 8 8
+ - image: solid-color(65, 48, 0, 255, 8, 8)
+ bounds: 520 384 8 8
+ - image: solid-color(66, 48, 0, 255, 8, 8)
+ bounds: 528 384 8 8
+ - image: solid-color(67, 48, 0, 255, 8, 8)
+ bounds: 536 384 8 8
+ - image: solid-color(68, 48, 0, 255, 8, 8)
+ bounds: 544 384 8 8
+ - image: solid-color(69, 48, 0, 255, 8, 8)
+ bounds: 552 384 8 8
+ - image: solid-color(70, 48, 0, 255, 8, 8)
+ bounds: 560 384 8 8
+ - image: solid-color(71, 48, 0, 255, 8, 8)
+ bounds: 568 384 8 8
+ - image: solid-color(72, 48, 0, 255, 8, 8)
+ bounds: 576 384 8 8
+ - image: solid-color(73, 48, 0, 255, 8, 8)
+ bounds: 584 384 8 8
+ - image: solid-color(74, 48, 0, 255, 8, 8)
+ bounds: 592 384 8 8
+ - image: solid-color(75, 48, 0, 255, 8, 8)
+ bounds: 600 384 8 8
+ - image: solid-color(76, 48, 0, 255, 8, 8)
+ bounds: 608 384 8 8
+ - image: solid-color(77, 48, 0, 255, 8, 8)
+ bounds: 616 384 8 8
+ - image: solid-color(78, 48, 0, 255, 8, 8)
+ bounds: 624 384 8 8
+ - image: solid-color(79, 48, 0, 255, 8, 8)
+ bounds: 632 384 8 8
+ - image: solid-color(80, 48, 0, 255, 8, 8)
+ bounds: 640 384 8 8
+ - image: solid-color(81, 48, 0, 255, 8, 8)
+ bounds: 648 384 8 8
+ - image: solid-color(82, 48, 0, 255, 8, 8)
+ bounds: 656 384 8 8
+ - image: solid-color(83, 48, 0, 255, 8, 8)
+ bounds: 664 384 8 8
+ - image: solid-color(84, 48, 0, 255, 8, 8)
+ bounds: 672 384 8 8
+ - image: solid-color(85, 48, 0, 255, 8, 8)
+ bounds: 680 384 8 8
+ - image: solid-color(86, 48, 0, 255, 8, 8)
+ bounds: 688 384 8 8
+ - image: solid-color(87, 48, 0, 255, 8, 8)
+ bounds: 696 384 8 8
+ - image: solid-color(88, 48, 0, 255, 8, 8)
+ bounds: 704 384 8 8
+ - image: solid-color(89, 48, 0, 255, 8, 8)
+ bounds: 712 384 8 8
+ - image: solid-color(90, 48, 0, 255, 8, 8)
+ bounds: 720 384 8 8
+ - image: solid-color(91, 48, 0, 255, 8, 8)
+ bounds: 728 384 8 8
+ - image: solid-color(92, 48, 0, 255, 8, 8)
+ bounds: 736 384 8 8
+ - image: solid-color(93, 48, 0, 255, 8, 8)
+ bounds: 744 384 8 8
+ - image: solid-color(94, 48, 0, 255, 8, 8)
+ bounds: 752 384 8 8
+ - image: solid-color(95, 48, 0, 255, 8, 8)
+ bounds: 760 384 8 8
+ - image: solid-color(96, 48, 0, 255, 8, 8)
+ bounds: 768 384 8 8
+ - image: solid-color(97, 48, 0, 255, 8, 8)
+ bounds: 776 384 8 8
+ - image: solid-color(98, 48, 0, 255, 8, 8)
+ bounds: 784 384 8 8
+ - image: solid-color(99, 48, 0, 255, 8, 8)
+ bounds: 792 384 8 8
+ - image: solid-color(100, 48, 0, 255, 8, 8)
+ bounds: 800 384 8 8
+ - image: solid-color(101, 48, 0, 255, 8, 8)
+ bounds: 808 384 8 8
+ - image: solid-color(102, 48, 0, 255, 8, 8)
+ bounds: 816 384 8 8
+ - image: solid-color(103, 48, 0, 255, 8, 8)
+ bounds: 824 384 8 8
+ - image: solid-color(104, 48, 0, 255, 8, 8)
+ bounds: 832 384 8 8
+ - image: solid-color(105, 48, 0, 255, 8, 8)
+ bounds: 840 384 8 8
+ - image: solid-color(106, 48, 0, 255, 8, 8)
+ bounds: 848 384 8 8
+ - image: solid-color(107, 48, 0, 255, 8, 8)
+ bounds: 856 384 8 8
+ - image: solid-color(108, 48, 0, 255, 8, 8)
+ bounds: 864 384 8 8
+ - image: solid-color(109, 48, 0, 255, 8, 8)
+ bounds: 872 384 8 8
+ - image: solid-color(110, 48, 0, 255, 8, 8)
+ bounds: 880 384 8 8
+ - image: solid-color(111, 48, 0, 255, 8, 8)
+ bounds: 888 384 8 8
+ - image: solid-color(112, 48, 0, 255, 8, 8)
+ bounds: 896 384 8 8
+ - image: solid-color(113, 48, 0, 255, 8, 8)
+ bounds: 904 384 8 8
+ - image: solid-color(114, 48, 0, 255, 8, 8)
+ bounds: 912 384 8 8
+ - image: solid-color(115, 48, 0, 255, 8, 8)
+ bounds: 920 384 8 8
+ - image: solid-color(116, 48, 0, 255, 8, 8)
+ bounds: 928 384 8 8
+ - image: solid-color(117, 48, 0, 255, 8, 8)
+ bounds: 936 384 8 8
+ - image: solid-color(118, 48, 0, 255, 8, 8)
+ bounds: 944 384 8 8
+ - image: solid-color(119, 48, 0, 255, 8, 8)
+ bounds: 952 384 8 8
+ - image: solid-color(120, 48, 0, 255, 8, 8)
+ bounds: 960 384 8 8
+ - image: solid-color(121, 48, 0, 255, 8, 8)
+ bounds: 968 384 8 8
+ - image: solid-color(122, 48, 0, 255, 8, 8)
+ bounds: 976 384 8 8
+ - image: solid-color(123, 48, 0, 255, 8, 8)
+ bounds: 984 384 8 8
+ - image: solid-color(124, 48, 0, 255, 8, 8)
+ bounds: 992 384 8 8
+ - image: solid-color(125, 48, 0, 255, 8, 8)
+ bounds: 1000 384 8 8
+ - image: solid-color(126, 48, 0, 255, 8, 8)
+ bounds: 1008 384 8 8
+ - image: solid-color(127, 48, 0, 255, 8, 8)
+ bounds: 1016 384 8 8
+ - image: solid-color(0, 49, 0, 255, 8, 8)
+ bounds: 0 392 8 8
+ - image: solid-color(1, 49, 0, 255, 8, 8)
+ bounds: 8 392 8 8
+ - image: solid-color(2, 49, 0, 255, 8, 8)
+ bounds: 16 392 8 8
+ - image: solid-color(3, 49, 0, 255, 8, 8)
+ bounds: 24 392 8 8
+ - image: solid-color(4, 49, 0, 255, 8, 8)
+ bounds: 32 392 8 8
+ - image: solid-color(5, 49, 0, 255, 8, 8)
+ bounds: 40 392 8 8
+ - image: solid-color(6, 49, 0, 255, 8, 8)
+ bounds: 48 392 8 8
+ - image: solid-color(7, 49, 0, 255, 8, 8)
+ bounds: 56 392 8 8
+ - image: solid-color(8, 49, 0, 255, 8, 8)
+ bounds: 64 392 8 8
+ - image: solid-color(9, 49, 0, 255, 8, 8)
+ bounds: 72 392 8 8
+ - image: solid-color(10, 49, 0, 255, 8, 8)
+ bounds: 80 392 8 8
+ - image: solid-color(11, 49, 0, 255, 8, 8)
+ bounds: 88 392 8 8
+ - image: solid-color(12, 49, 0, 255, 8, 8)
+ bounds: 96 392 8 8
+ - image: solid-color(13, 49, 0, 255, 8, 8)
+ bounds: 104 392 8 8
+ - image: solid-color(14, 49, 0, 255, 8, 8)
+ bounds: 112 392 8 8
+ - image: solid-color(15, 49, 0, 255, 8, 8)
+ bounds: 120 392 8 8
+ - image: solid-color(16, 49, 0, 255, 8, 8)
+ bounds: 128 392 8 8
+ - image: solid-color(17, 49, 0, 255, 8, 8)
+ bounds: 136 392 8 8
+ - image: solid-color(18, 49, 0, 255, 8, 8)
+ bounds: 144 392 8 8
+ - image: solid-color(19, 49, 0, 255, 8, 8)
+ bounds: 152 392 8 8
+ - image: solid-color(20, 49, 0, 255, 8, 8)
+ bounds: 160 392 8 8
+ - image: solid-color(21, 49, 0, 255, 8, 8)
+ bounds: 168 392 8 8
+ - image: solid-color(22, 49, 0, 255, 8, 8)
+ bounds: 176 392 8 8
+ - image: solid-color(23, 49, 0, 255, 8, 8)
+ bounds: 184 392 8 8
+ - image: solid-color(24, 49, 0, 255, 8, 8)
+ bounds: 192 392 8 8
+ - image: solid-color(25, 49, 0, 255, 8, 8)
+ bounds: 200 392 8 8
+ - image: solid-color(26, 49, 0, 255, 8, 8)
+ bounds: 208 392 8 8
+ - image: solid-color(27, 49, 0, 255, 8, 8)
+ bounds: 216 392 8 8
+ - image: solid-color(28, 49, 0, 255, 8, 8)
+ bounds: 224 392 8 8
+ - image: solid-color(29, 49, 0, 255, 8, 8)
+ bounds: 232 392 8 8
+ - image: solid-color(30, 49, 0, 255, 8, 8)
+ bounds: 240 392 8 8
+ - image: solid-color(31, 49, 0, 255, 8, 8)
+ bounds: 248 392 8 8
+ - image: solid-color(32, 49, 0, 255, 8, 8)
+ bounds: 256 392 8 8
+ - image: solid-color(33, 49, 0, 255, 8, 8)
+ bounds: 264 392 8 8
+ - image: solid-color(34, 49, 0, 255, 8, 8)
+ bounds: 272 392 8 8
+ - image: solid-color(35, 49, 0, 255, 8, 8)
+ bounds: 280 392 8 8
+ - image: solid-color(36, 49, 0, 255, 8, 8)
+ bounds: 288 392 8 8
+ - image: solid-color(37, 49, 0, 255, 8, 8)
+ bounds: 296 392 8 8
+ - image: solid-color(38, 49, 0, 255, 8, 8)
+ bounds: 304 392 8 8
+ - image: solid-color(39, 49, 0, 255, 8, 8)
+ bounds: 312 392 8 8
+ - image: solid-color(40, 49, 0, 255, 8, 8)
+ bounds: 320 392 8 8
+ - image: solid-color(41, 49, 0, 255, 8, 8)
+ bounds: 328 392 8 8
+ - image: solid-color(42, 49, 0, 255, 8, 8)
+ bounds: 336 392 8 8
+ - image: solid-color(43, 49, 0, 255, 8, 8)
+ bounds: 344 392 8 8
+ - image: solid-color(44, 49, 0, 255, 8, 8)
+ bounds: 352 392 8 8
+ - image: solid-color(45, 49, 0, 255, 8, 8)
+ bounds: 360 392 8 8
+ - image: solid-color(46, 49, 0, 255, 8, 8)
+ bounds: 368 392 8 8
+ - image: solid-color(47, 49, 0, 255, 8, 8)
+ bounds: 376 392 8 8
+ - image: solid-color(48, 49, 0, 255, 8, 8)
+ bounds: 384 392 8 8
+ - image: solid-color(49, 49, 0, 255, 8, 8)
+ bounds: 392 392 8 8
+ - image: solid-color(50, 49, 0, 255, 8, 8)
+ bounds: 400 392 8 8
+ - image: solid-color(51, 49, 0, 255, 8, 8)
+ bounds: 408 392 8 8
+ - image: solid-color(52, 49, 0, 255, 8, 8)
+ bounds: 416 392 8 8
+ - image: solid-color(53, 49, 0, 255, 8, 8)
+ bounds: 424 392 8 8
+ - image: solid-color(54, 49, 0, 255, 8, 8)
+ bounds: 432 392 8 8
+ - image: solid-color(55, 49, 0, 255, 8, 8)
+ bounds: 440 392 8 8
+ - image: solid-color(56, 49, 0, 255, 8, 8)
+ bounds: 448 392 8 8
+ - image: solid-color(57, 49, 0, 255, 8, 8)
+ bounds: 456 392 8 8
+ - image: solid-color(58, 49, 0, 255, 8, 8)
+ bounds: 464 392 8 8
+ - image: solid-color(59, 49, 0, 255, 8, 8)
+ bounds: 472 392 8 8
+ - image: solid-color(60, 49, 0, 255, 8, 8)
+ bounds: 480 392 8 8
+ - image: solid-color(61, 49, 0, 255, 8, 8)
+ bounds: 488 392 8 8
+ - image: solid-color(62, 49, 0, 255, 8, 8)
+ bounds: 496 392 8 8
+ - image: solid-color(63, 49, 0, 255, 8, 8)
+ bounds: 504 392 8 8
+ - image: solid-color(64, 49, 0, 255, 8, 8)
+ bounds: 512 392 8 8
+ - image: solid-color(65, 49, 0, 255, 8, 8)
+ bounds: 520 392 8 8
+ - image: solid-color(66, 49, 0, 255, 8, 8)
+ bounds: 528 392 8 8
+ - image: solid-color(67, 49, 0, 255, 8, 8)
+ bounds: 536 392 8 8
+ - image: solid-color(68, 49, 0, 255, 8, 8)
+ bounds: 544 392 8 8
+ - image: solid-color(69, 49, 0, 255, 8, 8)
+ bounds: 552 392 8 8
+ - image: solid-color(70, 49, 0, 255, 8, 8)
+ bounds: 560 392 8 8
+ - image: solid-color(71, 49, 0, 255, 8, 8)
+ bounds: 568 392 8 8
+ - image: solid-color(72, 49, 0, 255, 8, 8)
+ bounds: 576 392 8 8
+ - image: solid-color(73, 49, 0, 255, 8, 8)
+ bounds: 584 392 8 8
+ - image: solid-color(74, 49, 0, 255, 8, 8)
+ bounds: 592 392 8 8
+ - image: solid-color(75, 49, 0, 255, 8, 8)
+ bounds: 600 392 8 8
+ - image: solid-color(76, 49, 0, 255, 8, 8)
+ bounds: 608 392 8 8
+ - image: solid-color(77, 49, 0, 255, 8, 8)
+ bounds: 616 392 8 8
+ - image: solid-color(78, 49, 0, 255, 8, 8)
+ bounds: 624 392 8 8
+ - image: solid-color(79, 49, 0, 255, 8, 8)
+ bounds: 632 392 8 8
+ - image: solid-color(80, 49, 0, 255, 8, 8)
+ bounds: 640 392 8 8
+ - image: solid-color(81, 49, 0, 255, 8, 8)
+ bounds: 648 392 8 8
+ - image: solid-color(82, 49, 0, 255, 8, 8)
+ bounds: 656 392 8 8
+ - image: solid-color(83, 49, 0, 255, 8, 8)
+ bounds: 664 392 8 8
+ - image: solid-color(84, 49, 0, 255, 8, 8)
+ bounds: 672 392 8 8
+ - image: solid-color(85, 49, 0, 255, 8, 8)
+ bounds: 680 392 8 8
+ - image: solid-color(86, 49, 0, 255, 8, 8)
+ bounds: 688 392 8 8
+ - image: solid-color(87, 49, 0, 255, 8, 8)
+ bounds: 696 392 8 8
+ - image: solid-color(88, 49, 0, 255, 8, 8)
+ bounds: 704 392 8 8
+ - image: solid-color(89, 49, 0, 255, 8, 8)
+ bounds: 712 392 8 8
+ - image: solid-color(90, 49, 0, 255, 8, 8)
+ bounds: 720 392 8 8
+ - image: solid-color(91, 49, 0, 255, 8, 8)
+ bounds: 728 392 8 8
+ - image: solid-color(92, 49, 0, 255, 8, 8)
+ bounds: 736 392 8 8
+ - image: solid-color(93, 49, 0, 255, 8, 8)
+ bounds: 744 392 8 8
+ - image: solid-color(94, 49, 0, 255, 8, 8)
+ bounds: 752 392 8 8
+ - image: solid-color(95, 49, 0, 255, 8, 8)
+ bounds: 760 392 8 8
+ - image: solid-color(96, 49, 0, 255, 8, 8)
+ bounds: 768 392 8 8
+ - image: solid-color(97, 49, 0, 255, 8, 8)
+ bounds: 776 392 8 8
+ - image: solid-color(98, 49, 0, 255, 8, 8)
+ bounds: 784 392 8 8
+ - image: solid-color(99, 49, 0, 255, 8, 8)
+ bounds: 792 392 8 8
+ - image: solid-color(100, 49, 0, 255, 8, 8)
+ bounds: 800 392 8 8
+ - image: solid-color(101, 49, 0, 255, 8, 8)
+ bounds: 808 392 8 8
+ - image: solid-color(102, 49, 0, 255, 8, 8)
+ bounds: 816 392 8 8
+ - image: solid-color(103, 49, 0, 255, 8, 8)
+ bounds: 824 392 8 8
+ - image: solid-color(104, 49, 0, 255, 8, 8)
+ bounds: 832 392 8 8
+ - image: solid-color(105, 49, 0, 255, 8, 8)
+ bounds: 840 392 8 8
+ - image: solid-color(106, 49, 0, 255, 8, 8)
+ bounds: 848 392 8 8
+ - image: solid-color(107, 49, 0, 255, 8, 8)
+ bounds: 856 392 8 8
+ - image: solid-color(108, 49, 0, 255, 8, 8)
+ bounds: 864 392 8 8
+ - image: solid-color(109, 49, 0, 255, 8, 8)
+ bounds: 872 392 8 8
+ - image: solid-color(110, 49, 0, 255, 8, 8)
+ bounds: 880 392 8 8
+ - image: solid-color(111, 49, 0, 255, 8, 8)
+ bounds: 888 392 8 8
+ - image: solid-color(112, 49, 0, 255, 8, 8)
+ bounds: 896 392 8 8
+ - image: solid-color(113, 49, 0, 255, 8, 8)
+ bounds: 904 392 8 8
+ - image: solid-color(114, 49, 0, 255, 8, 8)
+ bounds: 912 392 8 8
+ - image: solid-color(115, 49, 0, 255, 8, 8)
+ bounds: 920 392 8 8
+ - image: solid-color(116, 49, 0, 255, 8, 8)
+ bounds: 928 392 8 8
+ - image: solid-color(117, 49, 0, 255, 8, 8)
+ bounds: 936 392 8 8
+ - image: solid-color(118, 49, 0, 255, 8, 8)
+ bounds: 944 392 8 8
+ - image: solid-color(119, 49, 0, 255, 8, 8)
+ bounds: 952 392 8 8
+ - image: solid-color(120, 49, 0, 255, 8, 8)
+ bounds: 960 392 8 8
+ - image: solid-color(121, 49, 0, 255, 8, 8)
+ bounds: 968 392 8 8
+ - image: solid-color(122, 49, 0, 255, 8, 8)
+ bounds: 976 392 8 8
+ - image: solid-color(123, 49, 0, 255, 8, 8)
+ bounds: 984 392 8 8
+ - image: solid-color(124, 49, 0, 255, 8, 8)
+ bounds: 992 392 8 8
+ - image: solid-color(125, 49, 0, 255, 8, 8)
+ bounds: 1000 392 8 8
+ - image: solid-color(126, 49, 0, 255, 8, 8)
+ bounds: 1008 392 8 8
+ - image: solid-color(127, 49, 0, 255, 8, 8)
+ bounds: 1016 392 8 8
+ - image: solid-color(0, 50, 0, 255, 8, 8)
+ bounds: 0 400 8 8
+ - image: solid-color(1, 50, 0, 255, 8, 8)
+ bounds: 8 400 8 8
+ - image: solid-color(2, 50, 0, 255, 8, 8)
+ bounds: 16 400 8 8
+ - image: solid-color(3, 50, 0, 255, 8, 8)
+ bounds: 24 400 8 8
+ - image: solid-color(4, 50, 0, 255, 8, 8)
+ bounds: 32 400 8 8
+ - image: solid-color(5, 50, 0, 255, 8, 8)
+ bounds: 40 400 8 8
+ - image: solid-color(6, 50, 0, 255, 8, 8)
+ bounds: 48 400 8 8
+ - image: solid-color(7, 50, 0, 255, 8, 8)
+ bounds: 56 400 8 8
+ - image: solid-color(8, 50, 0, 255, 8, 8)
+ bounds: 64 400 8 8
+ - image: solid-color(9, 50, 0, 255, 8, 8)
+ bounds: 72 400 8 8
+ - image: solid-color(10, 50, 0, 255, 8, 8)
+ bounds: 80 400 8 8
+ - image: solid-color(11, 50, 0, 255, 8, 8)
+ bounds: 88 400 8 8
+ - image: solid-color(12, 50, 0, 255, 8, 8)
+ bounds: 96 400 8 8
+ - image: solid-color(13, 50, 0, 255, 8, 8)
+ bounds: 104 400 8 8
+ - image: solid-color(14, 50, 0, 255, 8, 8)
+ bounds: 112 400 8 8
+ - image: solid-color(15, 50, 0, 255, 8, 8)
+ bounds: 120 400 8 8
+ - image: solid-color(16, 50, 0, 255, 8, 8)
+ bounds: 128 400 8 8
+ - image: solid-color(17, 50, 0, 255, 8, 8)
+ bounds: 136 400 8 8
+ - image: solid-color(18, 50, 0, 255, 8, 8)
+ bounds: 144 400 8 8
+ - image: solid-color(19, 50, 0, 255, 8, 8)
+ bounds: 152 400 8 8
+ - image: solid-color(20, 50, 0, 255, 8, 8)
+ bounds: 160 400 8 8
+ - image: solid-color(21, 50, 0, 255, 8, 8)
+ bounds: 168 400 8 8
+ - image: solid-color(22, 50, 0, 255, 8, 8)
+ bounds: 176 400 8 8
+ - image: solid-color(23, 50, 0, 255, 8, 8)
+ bounds: 184 400 8 8
+ - image: solid-color(24, 50, 0, 255, 8, 8)
+ bounds: 192 400 8 8
+ - image: solid-color(25, 50, 0, 255, 8, 8)
+ bounds: 200 400 8 8
+ - image: solid-color(26, 50, 0, 255, 8, 8)
+ bounds: 208 400 8 8
+ - image: solid-color(27, 50, 0, 255, 8, 8)
+ bounds: 216 400 8 8
+ - image: solid-color(28, 50, 0, 255, 8, 8)
+ bounds: 224 400 8 8
+ - image: solid-color(29, 50, 0, 255, 8, 8)
+ bounds: 232 400 8 8
+ - image: solid-color(30, 50, 0, 255, 8, 8)
+ bounds: 240 400 8 8
+ - image: solid-color(31, 50, 0, 255, 8, 8)
+ bounds: 248 400 8 8
+ - image: solid-color(32, 50, 0, 255, 8, 8)
+ bounds: 256 400 8 8
+ - image: solid-color(33, 50, 0, 255, 8, 8)
+ bounds: 264 400 8 8
+ - image: solid-color(34, 50, 0, 255, 8, 8)
+ bounds: 272 400 8 8
+ - image: solid-color(35, 50, 0, 255, 8, 8)
+ bounds: 280 400 8 8
+ - image: solid-color(36, 50, 0, 255, 8, 8)
+ bounds: 288 400 8 8
+ - image: solid-color(37, 50, 0, 255, 8, 8)
+ bounds: 296 400 8 8
+ - image: solid-color(38, 50, 0, 255, 8, 8)
+ bounds: 304 400 8 8
+ - image: solid-color(39, 50, 0, 255, 8, 8)
+ bounds: 312 400 8 8
+ - image: solid-color(40, 50, 0, 255, 8, 8)
+ bounds: 320 400 8 8
+ - image: solid-color(41, 50, 0, 255, 8, 8)
+ bounds: 328 400 8 8
+ - image: solid-color(42, 50, 0, 255, 8, 8)
+ bounds: 336 400 8 8
+ - image: solid-color(43, 50, 0, 255, 8, 8)
+ bounds: 344 400 8 8
+ - image: solid-color(44, 50, 0, 255, 8, 8)
+ bounds: 352 400 8 8
+ - image: solid-color(45, 50, 0, 255, 8, 8)
+ bounds: 360 400 8 8
+ - image: solid-color(46, 50, 0, 255, 8, 8)
+ bounds: 368 400 8 8
+ - image: solid-color(47, 50, 0, 255, 8, 8)
+ bounds: 376 400 8 8
+ - image: solid-color(48, 50, 0, 255, 8, 8)
+ bounds: 384 400 8 8
+ - image: solid-color(49, 50, 0, 255, 8, 8)
+ bounds: 392 400 8 8
+ - image: solid-color(50, 50, 0, 255, 8, 8)
+ bounds: 400 400 8 8
+ - image: solid-color(51, 50, 0, 255, 8, 8)
+ bounds: 408 400 8 8
+ - image: solid-color(52, 50, 0, 255, 8, 8)
+ bounds: 416 400 8 8
+ - image: solid-color(53, 50, 0, 255, 8, 8)
+ bounds: 424 400 8 8
+ - image: solid-color(54, 50, 0, 255, 8, 8)
+ bounds: 432 400 8 8
+ - image: solid-color(55, 50, 0, 255, 8, 8)
+ bounds: 440 400 8 8
+ - image: solid-color(56, 50, 0, 255, 8, 8)
+ bounds: 448 400 8 8
+ - image: solid-color(57, 50, 0, 255, 8, 8)
+ bounds: 456 400 8 8
+ - image: solid-color(58, 50, 0, 255, 8, 8)
+ bounds: 464 400 8 8
+ - image: solid-color(59, 50, 0, 255, 8, 8)
+ bounds: 472 400 8 8
+ - image: solid-color(60, 50, 0, 255, 8, 8)
+ bounds: 480 400 8 8
+ - image: solid-color(61, 50, 0, 255, 8, 8)
+ bounds: 488 400 8 8
+ - image: solid-color(62, 50, 0, 255, 8, 8)
+ bounds: 496 400 8 8
+ - image: solid-color(63, 50, 0, 255, 8, 8)
+ bounds: 504 400 8 8
+ - image: solid-color(64, 50, 0, 255, 8, 8)
+ bounds: 512 400 8 8
+ - image: solid-color(65, 50, 0, 255, 8, 8)
+ bounds: 520 400 8 8
+ - image: solid-color(66, 50, 0, 255, 8, 8)
+ bounds: 528 400 8 8
+ - image: solid-color(67, 50, 0, 255, 8, 8)
+ bounds: 536 400 8 8
+ - image: solid-color(68, 50, 0, 255, 8, 8)
+ bounds: 544 400 8 8
+ - image: solid-color(69, 50, 0, 255, 8, 8)
+ bounds: 552 400 8 8
+ - image: solid-color(70, 50, 0, 255, 8, 8)
+ bounds: 560 400 8 8
+ - image: solid-color(71, 50, 0, 255, 8, 8)
+ bounds: 568 400 8 8
+ - image: solid-color(72, 50, 0, 255, 8, 8)
+ bounds: 576 400 8 8
+ - image: solid-color(73, 50, 0, 255, 8, 8)
+ bounds: 584 400 8 8
+ - image: solid-color(74, 50, 0, 255, 8, 8)
+ bounds: 592 400 8 8
+ - image: solid-color(75, 50, 0, 255, 8, 8)
+ bounds: 600 400 8 8
+ - image: solid-color(76, 50, 0, 255, 8, 8)
+ bounds: 608 400 8 8
+ - image: solid-color(77, 50, 0, 255, 8, 8)
+ bounds: 616 400 8 8
+ - image: solid-color(78, 50, 0, 255, 8, 8)
+ bounds: 624 400 8 8
+ - image: solid-color(79, 50, 0, 255, 8, 8)
+ bounds: 632 400 8 8
+ - image: solid-color(80, 50, 0, 255, 8, 8)
+ bounds: 640 400 8 8
+ - image: solid-color(81, 50, 0, 255, 8, 8)
+ bounds: 648 400 8 8
+ - image: solid-color(82, 50, 0, 255, 8, 8)
+ bounds: 656 400 8 8
+ - image: solid-color(83, 50, 0, 255, 8, 8)
+ bounds: 664 400 8 8
+ - image: solid-color(84, 50, 0, 255, 8, 8)
+ bounds: 672 400 8 8
+ - image: solid-color(85, 50, 0, 255, 8, 8)
+ bounds: 680 400 8 8
+ - image: solid-color(86, 50, 0, 255, 8, 8)
+ bounds: 688 400 8 8
+ - image: solid-color(87, 50, 0, 255, 8, 8)
+ bounds: 696 400 8 8
+ - image: solid-color(88, 50, 0, 255, 8, 8)
+ bounds: 704 400 8 8
+ - image: solid-color(89, 50, 0, 255, 8, 8)
+ bounds: 712 400 8 8
+ - image: solid-color(90, 50, 0, 255, 8, 8)
+ bounds: 720 400 8 8
+ - image: solid-color(91, 50, 0, 255, 8, 8)
+ bounds: 728 400 8 8
+ - image: solid-color(92, 50, 0, 255, 8, 8)
+ bounds: 736 400 8 8
+ - image: solid-color(93, 50, 0, 255, 8, 8)
+ bounds: 744 400 8 8
+ - image: solid-color(94, 50, 0, 255, 8, 8)
+ bounds: 752 400 8 8
+ - image: solid-color(95, 50, 0, 255, 8, 8)
+ bounds: 760 400 8 8
+ - image: solid-color(96, 50, 0, 255, 8, 8)
+ bounds: 768 400 8 8
+ - image: solid-color(97, 50, 0, 255, 8, 8)
+ bounds: 776 400 8 8
+ - image: solid-color(98, 50, 0, 255, 8, 8)
+ bounds: 784 400 8 8
+ - image: solid-color(99, 50, 0, 255, 8, 8)
+ bounds: 792 400 8 8
+ - image: solid-color(100, 50, 0, 255, 8, 8)
+ bounds: 800 400 8 8
+ - image: solid-color(101, 50, 0, 255, 8, 8)
+ bounds: 808 400 8 8
+ - image: solid-color(102, 50, 0, 255, 8, 8)
+ bounds: 816 400 8 8
+ - image: solid-color(103, 50, 0, 255, 8, 8)
+ bounds: 824 400 8 8
+ - image: solid-color(104, 50, 0, 255, 8, 8)
+ bounds: 832 400 8 8
+ - image: solid-color(105, 50, 0, 255, 8, 8)
+ bounds: 840 400 8 8
+ - image: solid-color(106, 50, 0, 255, 8, 8)
+ bounds: 848 400 8 8
+ - image: solid-color(107, 50, 0, 255, 8, 8)
+ bounds: 856 400 8 8
+ - image: solid-color(108, 50, 0, 255, 8, 8)
+ bounds: 864 400 8 8
+ - image: solid-color(109, 50, 0, 255, 8, 8)
+ bounds: 872 400 8 8
+ - image: solid-color(110, 50, 0, 255, 8, 8)
+ bounds: 880 400 8 8
+ - image: solid-color(111, 50, 0, 255, 8, 8)
+ bounds: 888 400 8 8
+ - image: solid-color(112, 50, 0, 255, 8, 8)
+ bounds: 896 400 8 8
+ - image: solid-color(113, 50, 0, 255, 8, 8)
+ bounds: 904 400 8 8
+ - image: solid-color(114, 50, 0, 255, 8, 8)
+ bounds: 912 400 8 8
+ - image: solid-color(115, 50, 0, 255, 8, 8)
+ bounds: 920 400 8 8
+ - image: solid-color(116, 50, 0, 255, 8, 8)
+ bounds: 928 400 8 8
+ - image: solid-color(117, 50, 0, 255, 8, 8)
+ bounds: 936 400 8 8
+ - image: solid-color(118, 50, 0, 255, 8, 8)
+ bounds: 944 400 8 8
+ - image: solid-color(119, 50, 0, 255, 8, 8)
+ bounds: 952 400 8 8
+ - image: solid-color(120, 50, 0, 255, 8, 8)
+ bounds: 960 400 8 8
+ - image: solid-color(121, 50, 0, 255, 8, 8)
+ bounds: 968 400 8 8
+ - image: solid-color(122, 50, 0, 255, 8, 8)
+ bounds: 976 400 8 8
+ - image: solid-color(123, 50, 0, 255, 8, 8)
+ bounds: 984 400 8 8
+ - image: solid-color(124, 50, 0, 255, 8, 8)
+ bounds: 992 400 8 8
+ - image: solid-color(125, 50, 0, 255, 8, 8)
+ bounds: 1000 400 8 8
+ - image: solid-color(126, 50, 0, 255, 8, 8)
+ bounds: 1008 400 8 8
+ - image: solid-color(127, 50, 0, 255, 8, 8)
+ bounds: 1016 400 8 8
+ - image: solid-color(0, 51, 0, 255, 8, 8)
+ bounds: 0 408 8 8
+ - image: solid-color(1, 51, 0, 255, 8, 8)
+ bounds: 8 408 8 8
+ - image: solid-color(2, 51, 0, 255, 8, 8)
+ bounds: 16 408 8 8
+ - image: solid-color(3, 51, 0, 255, 8, 8)
+ bounds: 24 408 8 8
+ - image: solid-color(4, 51, 0, 255, 8, 8)
+ bounds: 32 408 8 8
+ - image: solid-color(5, 51, 0, 255, 8, 8)
+ bounds: 40 408 8 8
+ - image: solid-color(6, 51, 0, 255, 8, 8)
+ bounds: 48 408 8 8
+ - image: solid-color(7, 51, 0, 255, 8, 8)
+ bounds: 56 408 8 8
+ - image: solid-color(8, 51, 0, 255, 8, 8)
+ bounds: 64 408 8 8
+ - image: solid-color(9, 51, 0, 255, 8, 8)
+ bounds: 72 408 8 8
+ - image: solid-color(10, 51, 0, 255, 8, 8)
+ bounds: 80 408 8 8
+ - image: solid-color(11, 51, 0, 255, 8, 8)
+ bounds: 88 408 8 8
+ - image: solid-color(12, 51, 0, 255, 8, 8)
+ bounds: 96 408 8 8
+ - image: solid-color(13, 51, 0, 255, 8, 8)
+ bounds: 104 408 8 8
+ - image: solid-color(14, 51, 0, 255, 8, 8)
+ bounds: 112 408 8 8
+ - image: solid-color(15, 51, 0, 255, 8, 8)
+ bounds: 120 408 8 8
+ - image: solid-color(16, 51, 0, 255, 8, 8)
+ bounds: 128 408 8 8
+ - image: solid-color(17, 51, 0, 255, 8, 8)
+ bounds: 136 408 8 8
+ - image: solid-color(18, 51, 0, 255, 8, 8)
+ bounds: 144 408 8 8
+ - image: solid-color(19, 51, 0, 255, 8, 8)
+ bounds: 152 408 8 8
+ - image: solid-color(20, 51, 0, 255, 8, 8)
+ bounds: 160 408 8 8
+ - image: solid-color(21, 51, 0, 255, 8, 8)
+ bounds: 168 408 8 8
+ - image: solid-color(22, 51, 0, 255, 8, 8)
+ bounds: 176 408 8 8
+ - image: solid-color(23, 51, 0, 255, 8, 8)
+ bounds: 184 408 8 8
+ - image: solid-color(24, 51, 0, 255, 8, 8)
+ bounds: 192 408 8 8
+ - image: solid-color(25, 51, 0, 255, 8, 8)
+ bounds: 200 408 8 8
+ - image: solid-color(26, 51, 0, 255, 8, 8)
+ bounds: 208 408 8 8
+ - image: solid-color(27, 51, 0, 255, 8, 8)
+ bounds: 216 408 8 8
+ - image: solid-color(28, 51, 0, 255, 8, 8)
+ bounds: 224 408 8 8
+ - image: solid-color(29, 51, 0, 255, 8, 8)
+ bounds: 232 408 8 8
+ - image: solid-color(30, 51, 0, 255, 8, 8)
+ bounds: 240 408 8 8
+ - image: solid-color(31, 51, 0, 255, 8, 8)
+ bounds: 248 408 8 8
+ - image: solid-color(32, 51, 0, 255, 8, 8)
+ bounds: 256 408 8 8
+ - image: solid-color(33, 51, 0, 255, 8, 8)
+ bounds: 264 408 8 8
+ - image: solid-color(34, 51, 0, 255, 8, 8)
+ bounds: 272 408 8 8
+ - image: solid-color(35, 51, 0, 255, 8, 8)
+ bounds: 280 408 8 8
+ - image: solid-color(36, 51, 0, 255, 8, 8)
+ bounds: 288 408 8 8
+ - image: solid-color(37, 51, 0, 255, 8, 8)
+ bounds: 296 408 8 8
+ - image: solid-color(38, 51, 0, 255, 8, 8)
+ bounds: 304 408 8 8
+ - image: solid-color(39, 51, 0, 255, 8, 8)
+ bounds: 312 408 8 8
+ - image: solid-color(40, 51, 0, 255, 8, 8)
+ bounds: 320 408 8 8
+ - image: solid-color(41, 51, 0, 255, 8, 8)
+ bounds: 328 408 8 8
+ - image: solid-color(42, 51, 0, 255, 8, 8)
+ bounds: 336 408 8 8
+ - image: solid-color(43, 51, 0, 255, 8, 8)
+ bounds: 344 408 8 8
+ - image: solid-color(44, 51, 0, 255, 8, 8)
+ bounds: 352 408 8 8
+ - image: solid-color(45, 51, 0, 255, 8, 8)
+ bounds: 360 408 8 8
+ - image: solid-color(46, 51, 0, 255, 8, 8)
+ bounds: 368 408 8 8
+ - image: solid-color(47, 51, 0, 255, 8, 8)
+ bounds: 376 408 8 8
+ - image: solid-color(48, 51, 0, 255, 8, 8)
+ bounds: 384 408 8 8
+ - image: solid-color(49, 51, 0, 255, 8, 8)
+ bounds: 392 408 8 8
+ - image: solid-color(50, 51, 0, 255, 8, 8)
+ bounds: 400 408 8 8
+ - image: solid-color(51, 51, 0, 255, 8, 8)
+ bounds: 408 408 8 8
+ - image: solid-color(52, 51, 0, 255, 8, 8)
+ bounds: 416 408 8 8
+ - image: solid-color(53, 51, 0, 255, 8, 8)
+ bounds: 424 408 8 8
+ - image: solid-color(54, 51, 0, 255, 8, 8)
+ bounds: 432 408 8 8
+ - image: solid-color(55, 51, 0, 255, 8, 8)
+ bounds: 440 408 8 8
+ - image: solid-color(56, 51, 0, 255, 8, 8)
+ bounds: 448 408 8 8
+ - image: solid-color(57, 51, 0, 255, 8, 8)
+ bounds: 456 408 8 8
+ - image: solid-color(58, 51, 0, 255, 8, 8)
+ bounds: 464 408 8 8
+ - image: solid-color(59, 51, 0, 255, 8, 8)
+ bounds: 472 408 8 8
+ - image: solid-color(60, 51, 0, 255, 8, 8)
+ bounds: 480 408 8 8
+ - image: solid-color(61, 51, 0, 255, 8, 8)
+ bounds: 488 408 8 8
+ - image: solid-color(62, 51, 0, 255, 8, 8)
+ bounds: 496 408 8 8
+ - image: solid-color(63, 51, 0, 255, 8, 8)
+ bounds: 504 408 8 8
+ - image: solid-color(64, 51, 0, 255, 8, 8)
+ bounds: 512 408 8 8
+ - image: solid-color(65, 51, 0, 255, 8, 8)
+ bounds: 520 408 8 8
+ - image: solid-color(66, 51, 0, 255, 8, 8)
+ bounds: 528 408 8 8
+ - image: solid-color(67, 51, 0, 255, 8, 8)
+ bounds: 536 408 8 8
+ - image: solid-color(68, 51, 0, 255, 8, 8)
+ bounds: 544 408 8 8
+ - image: solid-color(69, 51, 0, 255, 8, 8)
+ bounds: 552 408 8 8
+ - image: solid-color(70, 51, 0, 255, 8, 8)
+ bounds: 560 408 8 8
+ - image: solid-color(71, 51, 0, 255, 8, 8)
+ bounds: 568 408 8 8
+ - image: solid-color(72, 51, 0, 255, 8, 8)
+ bounds: 576 408 8 8
+ - image: solid-color(73, 51, 0, 255, 8, 8)
+ bounds: 584 408 8 8
+ - image: solid-color(74, 51, 0, 255, 8, 8)
+ bounds: 592 408 8 8
+ - image: solid-color(75, 51, 0, 255, 8, 8)
+ bounds: 600 408 8 8
+ - image: solid-color(76, 51, 0, 255, 8, 8)
+ bounds: 608 408 8 8
+ - image: solid-color(77, 51, 0, 255, 8, 8)
+ bounds: 616 408 8 8
+ - image: solid-color(78, 51, 0, 255, 8, 8)
+ bounds: 624 408 8 8
+ - image: solid-color(79, 51, 0, 255, 8, 8)
+ bounds: 632 408 8 8
+ - image: solid-color(80, 51, 0, 255, 8, 8)
+ bounds: 640 408 8 8
+ - image: solid-color(81, 51, 0, 255, 8, 8)
+ bounds: 648 408 8 8
+ - image: solid-color(82, 51, 0, 255, 8, 8)
+ bounds: 656 408 8 8
+ - image: solid-color(83, 51, 0, 255, 8, 8)
+ bounds: 664 408 8 8
+ - image: solid-color(84, 51, 0, 255, 8, 8)
+ bounds: 672 408 8 8
+ - image: solid-color(85, 51, 0, 255, 8, 8)
+ bounds: 680 408 8 8
+ - image: solid-color(86, 51, 0, 255, 8, 8)
+ bounds: 688 408 8 8
+ - image: solid-color(87, 51, 0, 255, 8, 8)
+ bounds: 696 408 8 8
+ - image: solid-color(88, 51, 0, 255, 8, 8)
+ bounds: 704 408 8 8
+ - image: solid-color(89, 51, 0, 255, 8, 8)
+ bounds: 712 408 8 8
+ - image: solid-color(90, 51, 0, 255, 8, 8)
+ bounds: 720 408 8 8
+ - image: solid-color(91, 51, 0, 255, 8, 8)
+ bounds: 728 408 8 8
+ - image: solid-color(92, 51, 0, 255, 8, 8)
+ bounds: 736 408 8 8
+ - image: solid-color(93, 51, 0, 255, 8, 8)
+ bounds: 744 408 8 8
+ - image: solid-color(94, 51, 0, 255, 8, 8)
+ bounds: 752 408 8 8
+ - image: solid-color(95, 51, 0, 255, 8, 8)
+ bounds: 760 408 8 8
+ - image: solid-color(96, 51, 0, 255, 8, 8)
+ bounds: 768 408 8 8
+ - image: solid-color(97, 51, 0, 255, 8, 8)
+ bounds: 776 408 8 8
+ - image: solid-color(98, 51, 0, 255, 8, 8)
+ bounds: 784 408 8 8
+ - image: solid-color(99, 51, 0, 255, 8, 8)
+ bounds: 792 408 8 8
+ - image: solid-color(100, 51, 0, 255, 8, 8)
+ bounds: 800 408 8 8
+ - image: solid-color(101, 51, 0, 255, 8, 8)
+ bounds: 808 408 8 8
+ - image: solid-color(102, 51, 0, 255, 8, 8)
+ bounds: 816 408 8 8
+ - image: solid-color(103, 51, 0, 255, 8, 8)
+ bounds: 824 408 8 8
+ - image: solid-color(104, 51, 0, 255, 8, 8)
+ bounds: 832 408 8 8
+ - image: solid-color(105, 51, 0, 255, 8, 8)
+ bounds: 840 408 8 8
+ - image: solid-color(106, 51, 0, 255, 8, 8)
+ bounds: 848 408 8 8
+ - image: solid-color(107, 51, 0, 255, 8, 8)
+ bounds: 856 408 8 8
+ - image: solid-color(108, 51, 0, 255, 8, 8)
+ bounds: 864 408 8 8
+ - image: solid-color(109, 51, 0, 255, 8, 8)
+ bounds: 872 408 8 8
+ - image: solid-color(110, 51, 0, 255, 8, 8)
+ bounds: 880 408 8 8
+ - image: solid-color(111, 51, 0, 255, 8, 8)
+ bounds: 888 408 8 8
+ - image: solid-color(112, 51, 0, 255, 8, 8)
+ bounds: 896 408 8 8
+ - image: solid-color(113, 51, 0, 255, 8, 8)
+ bounds: 904 408 8 8
+ - image: solid-color(114, 51, 0, 255, 8, 8)
+ bounds: 912 408 8 8
+ - image: solid-color(115, 51, 0, 255, 8, 8)
+ bounds: 920 408 8 8
+ - image: solid-color(116, 51, 0, 255, 8, 8)
+ bounds: 928 408 8 8
+ - image: solid-color(117, 51, 0, 255, 8, 8)
+ bounds: 936 408 8 8
+ - image: solid-color(118, 51, 0, 255, 8, 8)
+ bounds: 944 408 8 8
+ - image: solid-color(119, 51, 0, 255, 8, 8)
+ bounds: 952 408 8 8
+ - image: solid-color(120, 51, 0, 255, 8, 8)
+ bounds: 960 408 8 8
+ - image: solid-color(121, 51, 0, 255, 8, 8)
+ bounds: 968 408 8 8
+ - image: solid-color(122, 51, 0, 255, 8, 8)
+ bounds: 976 408 8 8
+ - image: solid-color(123, 51, 0, 255, 8, 8)
+ bounds: 984 408 8 8
+ - image: solid-color(124, 51, 0, 255, 8, 8)
+ bounds: 992 408 8 8
+ - image: solid-color(125, 51, 0, 255, 8, 8)
+ bounds: 1000 408 8 8
+ - image: solid-color(126, 51, 0, 255, 8, 8)
+ bounds: 1008 408 8 8
+ - image: solid-color(127, 51, 0, 255, 8, 8)
+ bounds: 1016 408 8 8
+ - image: solid-color(0, 52, 0, 255, 8, 8)
+ bounds: 0 416 8 8
+ - image: solid-color(1, 52, 0, 255, 8, 8)
+ bounds: 8 416 8 8
+ - image: solid-color(2, 52, 0, 255, 8, 8)
+ bounds: 16 416 8 8
+ - image: solid-color(3, 52, 0, 255, 8, 8)
+ bounds: 24 416 8 8
+ - image: solid-color(4, 52, 0, 255, 8, 8)
+ bounds: 32 416 8 8
+ - image: solid-color(5, 52, 0, 255, 8, 8)
+ bounds: 40 416 8 8
+ - image: solid-color(6, 52, 0, 255, 8, 8)
+ bounds: 48 416 8 8
+ - image: solid-color(7, 52, 0, 255, 8, 8)
+ bounds: 56 416 8 8
+ - image: solid-color(8, 52, 0, 255, 8, 8)
+ bounds: 64 416 8 8
+ - image: solid-color(9, 52, 0, 255, 8, 8)
+ bounds: 72 416 8 8
+ - image: solid-color(10, 52, 0, 255, 8, 8)
+ bounds: 80 416 8 8
+ - image: solid-color(11, 52, 0, 255, 8, 8)
+ bounds: 88 416 8 8
+ - image: solid-color(12, 52, 0, 255, 8, 8)
+ bounds: 96 416 8 8
+ - image: solid-color(13, 52, 0, 255, 8, 8)
+ bounds: 104 416 8 8
+ - image: solid-color(14, 52, 0, 255, 8, 8)
+ bounds: 112 416 8 8
+ - image: solid-color(15, 52, 0, 255, 8, 8)
+ bounds: 120 416 8 8
+ - image: solid-color(16, 52, 0, 255, 8, 8)
+ bounds: 128 416 8 8
+ - image: solid-color(17, 52, 0, 255, 8, 8)
+ bounds: 136 416 8 8
+ - image: solid-color(18, 52, 0, 255, 8, 8)
+ bounds: 144 416 8 8
+ - image: solid-color(19, 52, 0, 255, 8, 8)
+ bounds: 152 416 8 8
+ - image: solid-color(20, 52, 0, 255, 8, 8)
+ bounds: 160 416 8 8
+ - image: solid-color(21, 52, 0, 255, 8, 8)
+ bounds: 168 416 8 8
+ - image: solid-color(22, 52, 0, 255, 8, 8)
+ bounds: 176 416 8 8
+ - image: solid-color(23, 52, 0, 255, 8, 8)
+ bounds: 184 416 8 8
+ - image: solid-color(24, 52, 0, 255, 8, 8)
+ bounds: 192 416 8 8
+ - image: solid-color(25, 52, 0, 255, 8, 8)
+ bounds: 200 416 8 8
+ - image: solid-color(26, 52, 0, 255, 8, 8)
+ bounds: 208 416 8 8
+ - image: solid-color(27, 52, 0, 255, 8, 8)
+ bounds: 216 416 8 8
+ - image: solid-color(28, 52, 0, 255, 8, 8)
+ bounds: 224 416 8 8
+ - image: solid-color(29, 52, 0, 255, 8, 8)
+ bounds: 232 416 8 8
+ - image: solid-color(30, 52, 0, 255, 8, 8)
+ bounds: 240 416 8 8
+ - image: solid-color(31, 52, 0, 255, 8, 8)
+ bounds: 248 416 8 8
+ - image: solid-color(32, 52, 0, 255, 8, 8)
+ bounds: 256 416 8 8
+ - image: solid-color(33, 52, 0, 255, 8, 8)
+ bounds: 264 416 8 8
+ - image: solid-color(34, 52, 0, 255, 8, 8)
+ bounds: 272 416 8 8
+ - image: solid-color(35, 52, 0, 255, 8, 8)
+ bounds: 280 416 8 8
+ - image: solid-color(36, 52, 0, 255, 8, 8)
+ bounds: 288 416 8 8
+ - image: solid-color(37, 52, 0, 255, 8, 8)
+ bounds: 296 416 8 8
+ - image: solid-color(38, 52, 0, 255, 8, 8)
+ bounds: 304 416 8 8
+ - image: solid-color(39, 52, 0, 255, 8, 8)
+ bounds: 312 416 8 8
+ - image: solid-color(40, 52, 0, 255, 8, 8)
+ bounds: 320 416 8 8
+ - image: solid-color(41, 52, 0, 255, 8, 8)
+ bounds: 328 416 8 8
+ - image: solid-color(42, 52, 0, 255, 8, 8)
+ bounds: 336 416 8 8
+ - image: solid-color(43, 52, 0, 255, 8, 8)
+ bounds: 344 416 8 8
+ - image: solid-color(44, 52, 0, 255, 8, 8)
+ bounds: 352 416 8 8
+ - image: solid-color(45, 52, 0, 255, 8, 8)
+ bounds: 360 416 8 8
+ - image: solid-color(46, 52, 0, 255, 8, 8)
+ bounds: 368 416 8 8
+ - image: solid-color(47, 52, 0, 255, 8, 8)
+ bounds: 376 416 8 8
+ - image: solid-color(48, 52, 0, 255, 8, 8)
+ bounds: 384 416 8 8
+ - image: solid-color(49, 52, 0, 255, 8, 8)
+ bounds: 392 416 8 8
+ - image: solid-color(50, 52, 0, 255, 8, 8)
+ bounds: 400 416 8 8
+ - image: solid-color(51, 52, 0, 255, 8, 8)
+ bounds: 408 416 8 8
+ - image: solid-color(52, 52, 0, 255, 8, 8)
+ bounds: 416 416 8 8
+ - image: solid-color(53, 52, 0, 255, 8, 8)
+ bounds: 424 416 8 8
+ - image: solid-color(54, 52, 0, 255, 8, 8)
+ bounds: 432 416 8 8
+ - image: solid-color(55, 52, 0, 255, 8, 8)
+ bounds: 440 416 8 8
+ - image: solid-color(56, 52, 0, 255, 8, 8)
+ bounds: 448 416 8 8
+ - image: solid-color(57, 52, 0, 255, 8, 8)
+ bounds: 456 416 8 8
+ - image: solid-color(58, 52, 0, 255, 8, 8)
+ bounds: 464 416 8 8
+ - image: solid-color(59, 52, 0, 255, 8, 8)
+ bounds: 472 416 8 8
+ - image: solid-color(60, 52, 0, 255, 8, 8)
+ bounds: 480 416 8 8
+ - image: solid-color(61, 52, 0, 255, 8, 8)
+ bounds: 488 416 8 8
+ - image: solid-color(62, 52, 0, 255, 8, 8)
+ bounds: 496 416 8 8
+ - image: solid-color(63, 52, 0, 255, 8, 8)
+ bounds: 504 416 8 8
+ - image: solid-color(64, 52, 0, 255, 8, 8)
+ bounds: 512 416 8 8
+ - image: solid-color(65, 52, 0, 255, 8, 8)
+ bounds: 520 416 8 8
+ - image: solid-color(66, 52, 0, 255, 8, 8)
+ bounds: 528 416 8 8
+ - image: solid-color(67, 52, 0, 255, 8, 8)
+ bounds: 536 416 8 8
+ - image: solid-color(68, 52, 0, 255, 8, 8)
+ bounds: 544 416 8 8
+ - image: solid-color(69, 52, 0, 255, 8, 8)
+ bounds: 552 416 8 8
+ - image: solid-color(70, 52, 0, 255, 8, 8)
+ bounds: 560 416 8 8
+ - image: solid-color(71, 52, 0, 255, 8, 8)
+ bounds: 568 416 8 8
+ - image: solid-color(72, 52, 0, 255, 8, 8)
+ bounds: 576 416 8 8
+ - image: solid-color(73, 52, 0, 255, 8, 8)
+ bounds: 584 416 8 8
+ - image: solid-color(74, 52, 0, 255, 8, 8)
+ bounds: 592 416 8 8
+ - image: solid-color(75, 52, 0, 255, 8, 8)
+ bounds: 600 416 8 8
+ - image: solid-color(76, 52, 0, 255, 8, 8)
+ bounds: 608 416 8 8
+ - image: solid-color(77, 52, 0, 255, 8, 8)
+ bounds: 616 416 8 8
+ - image: solid-color(78, 52, 0, 255, 8, 8)
+ bounds: 624 416 8 8
+ - image: solid-color(79, 52, 0, 255, 8, 8)
+ bounds: 632 416 8 8
+ - image: solid-color(80, 52, 0, 255, 8, 8)
+ bounds: 640 416 8 8
+ - image: solid-color(81, 52, 0, 255, 8, 8)
+ bounds: 648 416 8 8
+ - image: solid-color(82, 52, 0, 255, 8, 8)
+ bounds: 656 416 8 8
+ - image: solid-color(83, 52, 0, 255, 8, 8)
+ bounds: 664 416 8 8
+ - image: solid-color(84, 52, 0, 255, 8, 8)
+ bounds: 672 416 8 8
+ - image: solid-color(85, 52, 0, 255, 8, 8)
+ bounds: 680 416 8 8
+ - image: solid-color(86, 52, 0, 255, 8, 8)
+ bounds: 688 416 8 8
+ - image: solid-color(87, 52, 0, 255, 8, 8)
+ bounds: 696 416 8 8
+ - image: solid-color(88, 52, 0, 255, 8, 8)
+ bounds: 704 416 8 8
+ - image: solid-color(89, 52, 0, 255, 8, 8)
+ bounds: 712 416 8 8
+ - image: solid-color(90, 52, 0, 255, 8, 8)
+ bounds: 720 416 8 8
+ - image: solid-color(91, 52, 0, 255, 8, 8)
+ bounds: 728 416 8 8
+ - image: solid-color(92, 52, 0, 255, 8, 8)
+ bounds: 736 416 8 8
+ - image: solid-color(93, 52, 0, 255, 8, 8)
+ bounds: 744 416 8 8
+ - image: solid-color(94, 52, 0, 255, 8, 8)
+ bounds: 752 416 8 8
+ - image: solid-color(95, 52, 0, 255, 8, 8)
+ bounds: 760 416 8 8
+ - image: solid-color(96, 52, 0, 255, 8, 8)
+ bounds: 768 416 8 8
+ - image: solid-color(97, 52, 0, 255, 8, 8)
+ bounds: 776 416 8 8
+ - image: solid-color(98, 52, 0, 255, 8, 8)
+ bounds: 784 416 8 8
+ - image: solid-color(99, 52, 0, 255, 8, 8)
+ bounds: 792 416 8 8
+ - image: solid-color(100, 52, 0, 255, 8, 8)
+ bounds: 800 416 8 8
+ - image: solid-color(101, 52, 0, 255, 8, 8)
+ bounds: 808 416 8 8
+ - image: solid-color(102, 52, 0, 255, 8, 8)
+ bounds: 816 416 8 8
+ - image: solid-color(103, 52, 0, 255, 8, 8)
+ bounds: 824 416 8 8
+ - image: solid-color(104, 52, 0, 255, 8, 8)
+ bounds: 832 416 8 8
+ - image: solid-color(105, 52, 0, 255, 8, 8)
+ bounds: 840 416 8 8
+ - image: solid-color(106, 52, 0, 255, 8, 8)
+ bounds: 848 416 8 8
+ - image: solid-color(107, 52, 0, 255, 8, 8)
+ bounds: 856 416 8 8
+ - image: solid-color(108, 52, 0, 255, 8, 8)
+ bounds: 864 416 8 8
+ - image: solid-color(109, 52, 0, 255, 8, 8)
+ bounds: 872 416 8 8
+ - image: solid-color(110, 52, 0, 255, 8, 8)
+ bounds: 880 416 8 8
+ - image: solid-color(111, 52, 0, 255, 8, 8)
+ bounds: 888 416 8 8
+ - image: solid-color(112, 52, 0, 255, 8, 8)
+ bounds: 896 416 8 8
+ - image: solid-color(113, 52, 0, 255, 8, 8)
+ bounds: 904 416 8 8
+ - image: solid-color(114, 52, 0, 255, 8, 8)
+ bounds: 912 416 8 8
+ - image: solid-color(115, 52, 0, 255, 8, 8)
+ bounds: 920 416 8 8
+ - image: solid-color(116, 52, 0, 255, 8, 8)
+ bounds: 928 416 8 8
+ - image: solid-color(117, 52, 0, 255, 8, 8)
+ bounds: 936 416 8 8
+ - image: solid-color(118, 52, 0, 255, 8, 8)
+ bounds: 944 416 8 8
+ - image: solid-color(119, 52, 0, 255, 8, 8)
+ bounds: 952 416 8 8
+ - image: solid-color(120, 52, 0, 255, 8, 8)
+ bounds: 960 416 8 8
+ - image: solid-color(121, 52, 0, 255, 8, 8)
+ bounds: 968 416 8 8
+ - image: solid-color(122, 52, 0, 255, 8, 8)
+ bounds: 976 416 8 8
+ - image: solid-color(123, 52, 0, 255, 8, 8)
+ bounds: 984 416 8 8
+ - image: solid-color(124, 52, 0, 255, 8, 8)
+ bounds: 992 416 8 8
+ - image: solid-color(125, 52, 0, 255, 8, 8)
+ bounds: 1000 416 8 8
+ - image: solid-color(126, 52, 0, 255, 8, 8)
+ bounds: 1008 416 8 8
+ - image: solid-color(127, 52, 0, 255, 8, 8)
+ bounds: 1016 416 8 8
+ - image: solid-color(0, 53, 0, 255, 8, 8)
+ bounds: 0 424 8 8
+ - image: solid-color(1, 53, 0, 255, 8, 8)
+ bounds: 8 424 8 8
+ - image: solid-color(2, 53, 0, 255, 8, 8)
+ bounds: 16 424 8 8
+ - image: solid-color(3, 53, 0, 255, 8, 8)
+ bounds: 24 424 8 8
+ - image: solid-color(4, 53, 0, 255, 8, 8)
+ bounds: 32 424 8 8
+ - image: solid-color(5, 53, 0, 255, 8, 8)
+ bounds: 40 424 8 8
+ - image: solid-color(6, 53, 0, 255, 8, 8)
+ bounds: 48 424 8 8
+ - image: solid-color(7, 53, 0, 255, 8, 8)
+ bounds: 56 424 8 8
+ - image: solid-color(8, 53, 0, 255, 8, 8)
+ bounds: 64 424 8 8
+ - image: solid-color(9, 53, 0, 255, 8, 8)
+ bounds: 72 424 8 8
+ - image: solid-color(10, 53, 0, 255, 8, 8)
+ bounds: 80 424 8 8
+ - image: solid-color(11, 53, 0, 255, 8, 8)
+ bounds: 88 424 8 8
+ - image: solid-color(12, 53, 0, 255, 8, 8)
+ bounds: 96 424 8 8
+ - image: solid-color(13, 53, 0, 255, 8, 8)
+ bounds: 104 424 8 8
+ - image: solid-color(14, 53, 0, 255, 8, 8)
+ bounds: 112 424 8 8
+ - image: solid-color(15, 53, 0, 255, 8, 8)
+ bounds: 120 424 8 8
+ - image: solid-color(16, 53, 0, 255, 8, 8)
+ bounds: 128 424 8 8
+ - image: solid-color(17, 53, 0, 255, 8, 8)
+ bounds: 136 424 8 8
+ - image: solid-color(18, 53, 0, 255, 8, 8)
+ bounds: 144 424 8 8
+ - image: solid-color(19, 53, 0, 255, 8, 8)
+ bounds: 152 424 8 8
+ - image: solid-color(20, 53, 0, 255, 8, 8)
+ bounds: 160 424 8 8
+ - image: solid-color(21, 53, 0, 255, 8, 8)
+ bounds: 168 424 8 8
+ - image: solid-color(22, 53, 0, 255, 8, 8)
+ bounds: 176 424 8 8
+ - image: solid-color(23, 53, 0, 255, 8, 8)
+ bounds: 184 424 8 8
+ - image: solid-color(24, 53, 0, 255, 8, 8)
+ bounds: 192 424 8 8
+ - image: solid-color(25, 53, 0, 255, 8, 8)
+ bounds: 200 424 8 8
+ - image: solid-color(26, 53, 0, 255, 8, 8)
+ bounds: 208 424 8 8
+ - image: solid-color(27, 53, 0, 255, 8, 8)
+ bounds: 216 424 8 8
+ - image: solid-color(28, 53, 0, 255, 8, 8)
+ bounds: 224 424 8 8
+ - image: solid-color(29, 53, 0, 255, 8, 8)
+ bounds: 232 424 8 8
+ - image: solid-color(30, 53, 0, 255, 8, 8)
+ bounds: 240 424 8 8
+ - image: solid-color(31, 53, 0, 255, 8, 8)
+ bounds: 248 424 8 8
+ - image: solid-color(32, 53, 0, 255, 8, 8)
+ bounds: 256 424 8 8
+ - image: solid-color(33, 53, 0, 255, 8, 8)
+ bounds: 264 424 8 8
+ - image: solid-color(34, 53, 0, 255, 8, 8)
+ bounds: 272 424 8 8
+ - image: solid-color(35, 53, 0, 255, 8, 8)
+ bounds: 280 424 8 8
+ - image: solid-color(36, 53, 0, 255, 8, 8)
+ bounds: 288 424 8 8
+ - image: solid-color(37, 53, 0, 255, 8, 8)
+ bounds: 296 424 8 8
+ - image: solid-color(38, 53, 0, 255, 8, 8)
+ bounds: 304 424 8 8
+ - image: solid-color(39, 53, 0, 255, 8, 8)
+ bounds: 312 424 8 8
+ - image: solid-color(40, 53, 0, 255, 8, 8)
+ bounds: 320 424 8 8
+ - image: solid-color(41, 53, 0, 255, 8, 8)
+ bounds: 328 424 8 8
+ - image: solid-color(42, 53, 0, 255, 8, 8)
+ bounds: 336 424 8 8
+ - image: solid-color(43, 53, 0, 255, 8, 8)
+ bounds: 344 424 8 8
+ - image: solid-color(44, 53, 0, 255, 8, 8)
+ bounds: 352 424 8 8
+ - image: solid-color(45, 53, 0, 255, 8, 8)
+ bounds: 360 424 8 8
+ - image: solid-color(46, 53, 0, 255, 8, 8)
+ bounds: 368 424 8 8
+ - image: solid-color(47, 53, 0, 255, 8, 8)
+ bounds: 376 424 8 8
+ - image: solid-color(48, 53, 0, 255, 8, 8)
+ bounds: 384 424 8 8
+ - image: solid-color(49, 53, 0, 255, 8, 8)
+ bounds: 392 424 8 8
+ - image: solid-color(50, 53, 0, 255, 8, 8)
+ bounds: 400 424 8 8
+ - image: solid-color(51, 53, 0, 255, 8, 8)
+ bounds: 408 424 8 8
+ - image: solid-color(52, 53, 0, 255, 8, 8)
+ bounds: 416 424 8 8
+ - image: solid-color(53, 53, 0, 255, 8, 8)
+ bounds: 424 424 8 8
+ - image: solid-color(54, 53, 0, 255, 8, 8)
+ bounds: 432 424 8 8
+ - image: solid-color(55, 53, 0, 255, 8, 8)
+ bounds: 440 424 8 8
+ - image: solid-color(56, 53, 0, 255, 8, 8)
+ bounds: 448 424 8 8
+ - image: solid-color(57, 53, 0, 255, 8, 8)
+ bounds: 456 424 8 8
+ - image: solid-color(58, 53, 0, 255, 8, 8)
+ bounds: 464 424 8 8
+ - image: solid-color(59, 53, 0, 255, 8, 8)
+ bounds: 472 424 8 8
+ - image: solid-color(60, 53, 0, 255, 8, 8)
+ bounds: 480 424 8 8
+ - image: solid-color(61, 53, 0, 255, 8, 8)
+ bounds: 488 424 8 8
+ - image: solid-color(62, 53, 0, 255, 8, 8)
+ bounds: 496 424 8 8
+ - image: solid-color(63, 53, 0, 255, 8, 8)
+ bounds: 504 424 8 8
+ - image: solid-color(64, 53, 0, 255, 8, 8)
+ bounds: 512 424 8 8
+ - image: solid-color(65, 53, 0, 255, 8, 8)
+ bounds: 520 424 8 8
+ - image: solid-color(66, 53, 0, 255, 8, 8)
+ bounds: 528 424 8 8
+ - image: solid-color(67, 53, 0, 255, 8, 8)
+ bounds: 536 424 8 8
+ - image: solid-color(68, 53, 0, 255, 8, 8)
+ bounds: 544 424 8 8
+ - image: solid-color(69, 53, 0, 255, 8, 8)
+ bounds: 552 424 8 8
+ - image: solid-color(70, 53, 0, 255, 8, 8)
+ bounds: 560 424 8 8
+ - image: solid-color(71, 53, 0, 255, 8, 8)
+ bounds: 568 424 8 8
+ - image: solid-color(72, 53, 0, 255, 8, 8)
+ bounds: 576 424 8 8
+ - image: solid-color(73, 53, 0, 255, 8, 8)
+ bounds: 584 424 8 8
+ - image: solid-color(74, 53, 0, 255, 8, 8)
+ bounds: 592 424 8 8
+ - image: solid-color(75, 53, 0, 255, 8, 8)
+ bounds: 600 424 8 8
+ - image: solid-color(76, 53, 0, 255, 8, 8)
+ bounds: 608 424 8 8
+ - image: solid-color(77, 53, 0, 255, 8, 8)
+ bounds: 616 424 8 8
+ - image: solid-color(78, 53, 0, 255, 8, 8)
+ bounds: 624 424 8 8
+ - image: solid-color(79, 53, 0, 255, 8, 8)
+ bounds: 632 424 8 8
+ - image: solid-color(80, 53, 0, 255, 8, 8)
+ bounds: 640 424 8 8
+ - image: solid-color(81, 53, 0, 255, 8, 8)
+ bounds: 648 424 8 8
+ - image: solid-color(82, 53, 0, 255, 8, 8)
+ bounds: 656 424 8 8
+ - image: solid-color(83, 53, 0, 255, 8, 8)
+ bounds: 664 424 8 8
+ - image: solid-color(84, 53, 0, 255, 8, 8)
+ bounds: 672 424 8 8
+ - image: solid-color(85, 53, 0, 255, 8, 8)
+ bounds: 680 424 8 8
+ - image: solid-color(86, 53, 0, 255, 8, 8)
+ bounds: 688 424 8 8
+ - image: solid-color(87, 53, 0, 255, 8, 8)
+ bounds: 696 424 8 8
+ - image: solid-color(88, 53, 0, 255, 8, 8)
+ bounds: 704 424 8 8
+ - image: solid-color(89, 53, 0, 255, 8, 8)
+ bounds: 712 424 8 8
+ - image: solid-color(90, 53, 0, 255, 8, 8)
+ bounds: 720 424 8 8
+ - image: solid-color(91, 53, 0, 255, 8, 8)
+ bounds: 728 424 8 8
+ - image: solid-color(92, 53, 0, 255, 8, 8)
+ bounds: 736 424 8 8
+ - image: solid-color(93, 53, 0, 255, 8, 8)
+ bounds: 744 424 8 8
+ - image: solid-color(94, 53, 0, 255, 8, 8)
+ bounds: 752 424 8 8
+ - image: solid-color(95, 53, 0, 255, 8, 8)
+ bounds: 760 424 8 8
+ - image: solid-color(96, 53, 0, 255, 8, 8)
+ bounds: 768 424 8 8
+ - image: solid-color(97, 53, 0, 255, 8, 8)
+ bounds: 776 424 8 8
+ - image: solid-color(98, 53, 0, 255, 8, 8)
+ bounds: 784 424 8 8
+ - image: solid-color(99, 53, 0, 255, 8, 8)
+ bounds: 792 424 8 8
+ - image: solid-color(100, 53, 0, 255, 8, 8)
+ bounds: 800 424 8 8
+ - image: solid-color(101, 53, 0, 255, 8, 8)
+ bounds: 808 424 8 8
+ - image: solid-color(102, 53, 0, 255, 8, 8)
+ bounds: 816 424 8 8
+ - image: solid-color(103, 53, 0, 255, 8, 8)
+ bounds: 824 424 8 8
+ - image: solid-color(104, 53, 0, 255, 8, 8)
+ bounds: 832 424 8 8
+ - image: solid-color(105, 53, 0, 255, 8, 8)
+ bounds: 840 424 8 8
+ - image: solid-color(106, 53, 0, 255, 8, 8)
+ bounds: 848 424 8 8
+ - image: solid-color(107, 53, 0, 255, 8, 8)
+ bounds: 856 424 8 8
+ - image: solid-color(108, 53, 0, 255, 8, 8)
+ bounds: 864 424 8 8
+ - image: solid-color(109, 53, 0, 255, 8, 8)
+ bounds: 872 424 8 8
+ - image: solid-color(110, 53, 0, 255, 8, 8)
+ bounds: 880 424 8 8
+ - image: solid-color(111, 53, 0, 255, 8, 8)
+ bounds: 888 424 8 8
+ - image: solid-color(112, 53, 0, 255, 8, 8)
+ bounds: 896 424 8 8
+ - image: solid-color(113, 53, 0, 255, 8, 8)
+ bounds: 904 424 8 8
+ - image: solid-color(114, 53, 0, 255, 8, 8)
+ bounds: 912 424 8 8
+ - image: solid-color(115, 53, 0, 255, 8, 8)
+ bounds: 920 424 8 8
+ - image: solid-color(116, 53, 0, 255, 8, 8)
+ bounds: 928 424 8 8
+ - image: solid-color(117, 53, 0, 255, 8, 8)
+ bounds: 936 424 8 8
+ - image: solid-color(118, 53, 0, 255, 8, 8)
+ bounds: 944 424 8 8
+ - image: solid-color(119, 53, 0, 255, 8, 8)
+ bounds: 952 424 8 8
+ - image: solid-color(120, 53, 0, 255, 8, 8)
+ bounds: 960 424 8 8
+ - image: solid-color(121, 53, 0, 255, 8, 8)
+ bounds: 968 424 8 8
+ - image: solid-color(122, 53, 0, 255, 8, 8)
+ bounds: 976 424 8 8
+ - image: solid-color(123, 53, 0, 255, 8, 8)
+ bounds: 984 424 8 8
+ - image: solid-color(124, 53, 0, 255, 8, 8)
+ bounds: 992 424 8 8
+ - image: solid-color(125, 53, 0, 255, 8, 8)
+ bounds: 1000 424 8 8
+ - image: solid-color(126, 53, 0, 255, 8, 8)
+ bounds: 1008 424 8 8
+ - image: solid-color(127, 53, 0, 255, 8, 8)
+ bounds: 1016 424 8 8
+ - image: solid-color(0, 54, 0, 255, 8, 8)
+ bounds: 0 432 8 8
+ - image: solid-color(1, 54, 0, 255, 8, 8)
+ bounds: 8 432 8 8
+ - image: solid-color(2, 54, 0, 255, 8, 8)
+ bounds: 16 432 8 8
+ - image: solid-color(3, 54, 0, 255, 8, 8)
+ bounds: 24 432 8 8
+ - image: solid-color(4, 54, 0, 255, 8, 8)
+ bounds: 32 432 8 8
+ - image: solid-color(5, 54, 0, 255, 8, 8)
+ bounds: 40 432 8 8
+ - image: solid-color(6, 54, 0, 255, 8, 8)
+ bounds: 48 432 8 8
+ - image: solid-color(7, 54, 0, 255, 8, 8)
+ bounds: 56 432 8 8
+ - image: solid-color(8, 54, 0, 255, 8, 8)
+ bounds: 64 432 8 8
+ - image: solid-color(9, 54, 0, 255, 8, 8)
+ bounds: 72 432 8 8
+ - image: solid-color(10, 54, 0, 255, 8, 8)
+ bounds: 80 432 8 8
+ - image: solid-color(11, 54, 0, 255, 8, 8)
+ bounds: 88 432 8 8
+ - image: solid-color(12, 54, 0, 255, 8, 8)
+ bounds: 96 432 8 8
+ - image: solid-color(13, 54, 0, 255, 8, 8)
+ bounds: 104 432 8 8
+ - image: solid-color(14, 54, 0, 255, 8, 8)
+ bounds: 112 432 8 8
+ - image: solid-color(15, 54, 0, 255, 8, 8)
+ bounds: 120 432 8 8
+ - image: solid-color(16, 54, 0, 255, 8, 8)
+ bounds: 128 432 8 8
+ - image: solid-color(17, 54, 0, 255, 8, 8)
+ bounds: 136 432 8 8
+ - image: solid-color(18, 54, 0, 255, 8, 8)
+ bounds: 144 432 8 8
+ - image: solid-color(19, 54, 0, 255, 8, 8)
+ bounds: 152 432 8 8
+ - image: solid-color(20, 54, 0, 255, 8, 8)
+ bounds: 160 432 8 8
+ - image: solid-color(21, 54, 0, 255, 8, 8)
+ bounds: 168 432 8 8
+ - image: solid-color(22, 54, 0, 255, 8, 8)
+ bounds: 176 432 8 8
+ - image: solid-color(23, 54, 0, 255, 8, 8)
+ bounds: 184 432 8 8
+ - image: solid-color(24, 54, 0, 255, 8, 8)
+ bounds: 192 432 8 8
+ - image: solid-color(25, 54, 0, 255, 8, 8)
+ bounds: 200 432 8 8
+ - image: solid-color(26, 54, 0, 255, 8, 8)
+ bounds: 208 432 8 8
+ - image: solid-color(27, 54, 0, 255, 8, 8)
+ bounds: 216 432 8 8
+ - image: solid-color(28, 54, 0, 255, 8, 8)
+ bounds: 224 432 8 8
+ - image: solid-color(29, 54, 0, 255, 8, 8)
+ bounds: 232 432 8 8
+ - image: solid-color(30, 54, 0, 255, 8, 8)
+ bounds: 240 432 8 8
+ - image: solid-color(31, 54, 0, 255, 8, 8)
+ bounds: 248 432 8 8
+ - image: solid-color(32, 54, 0, 255, 8, 8)
+ bounds: 256 432 8 8
+ - image: solid-color(33, 54, 0, 255, 8, 8)
+ bounds: 264 432 8 8
+ - image: solid-color(34, 54, 0, 255, 8, 8)
+ bounds: 272 432 8 8
+ - image: solid-color(35, 54, 0, 255, 8, 8)
+ bounds: 280 432 8 8
+ - image: solid-color(36, 54, 0, 255, 8, 8)
+ bounds: 288 432 8 8
+ - image: solid-color(37, 54, 0, 255, 8, 8)
+ bounds: 296 432 8 8
+ - image: solid-color(38, 54, 0, 255, 8, 8)
+ bounds: 304 432 8 8
+ - image: solid-color(39, 54, 0, 255, 8, 8)
+ bounds: 312 432 8 8
+ - image: solid-color(40, 54, 0, 255, 8, 8)
+ bounds: 320 432 8 8
+ - image: solid-color(41, 54, 0, 255, 8, 8)
+ bounds: 328 432 8 8
+ - image: solid-color(42, 54, 0, 255, 8, 8)
+ bounds: 336 432 8 8
+ - image: solid-color(43, 54, 0, 255, 8, 8)
+ bounds: 344 432 8 8
+ - image: solid-color(44, 54, 0, 255, 8, 8)
+ bounds: 352 432 8 8
+ - image: solid-color(45, 54, 0, 255, 8, 8)
+ bounds: 360 432 8 8
+ - image: solid-color(46, 54, 0, 255, 8, 8)
+ bounds: 368 432 8 8
+ - image: solid-color(47, 54, 0, 255, 8, 8)
+ bounds: 376 432 8 8
+ - image: solid-color(48, 54, 0, 255, 8, 8)
+ bounds: 384 432 8 8
+ - image: solid-color(49, 54, 0, 255, 8, 8)
+ bounds: 392 432 8 8
+ - image: solid-color(50, 54, 0, 255, 8, 8)
+ bounds: 400 432 8 8
+ - image: solid-color(51, 54, 0, 255, 8, 8)
+ bounds: 408 432 8 8
+ - image: solid-color(52, 54, 0, 255, 8, 8)
+ bounds: 416 432 8 8
+ - image: solid-color(53, 54, 0, 255, 8, 8)
+ bounds: 424 432 8 8
+ - image: solid-color(54, 54, 0, 255, 8, 8)
+ bounds: 432 432 8 8
+ - image: solid-color(55, 54, 0, 255, 8, 8)
+ bounds: 440 432 8 8
+ - image: solid-color(56, 54, 0, 255, 8, 8)
+ bounds: 448 432 8 8
+ - image: solid-color(57, 54, 0, 255, 8, 8)
+ bounds: 456 432 8 8
+ - image: solid-color(58, 54, 0, 255, 8, 8)
+ bounds: 464 432 8 8
+ - image: solid-color(59, 54, 0, 255, 8, 8)
+ bounds: 472 432 8 8
+ - image: solid-color(60, 54, 0, 255, 8, 8)
+ bounds: 480 432 8 8
+ - image: solid-color(61, 54, 0, 255, 8, 8)
+ bounds: 488 432 8 8
+ - image: solid-color(62, 54, 0, 255, 8, 8)
+ bounds: 496 432 8 8
+ - image: solid-color(63, 54, 0, 255, 8, 8)
+ bounds: 504 432 8 8
+ - image: solid-color(64, 54, 0, 255, 8, 8)
+ bounds: 512 432 8 8
+ - image: solid-color(65, 54, 0, 255, 8, 8)
+ bounds: 520 432 8 8
+ - image: solid-color(66, 54, 0, 255, 8, 8)
+ bounds: 528 432 8 8
+ - image: solid-color(67, 54, 0, 255, 8, 8)
+ bounds: 536 432 8 8
+ - image: solid-color(68, 54, 0, 255, 8, 8)
+ bounds: 544 432 8 8
+ - image: solid-color(69, 54, 0, 255, 8, 8)
+ bounds: 552 432 8 8
+ - image: solid-color(70, 54, 0, 255, 8, 8)
+ bounds: 560 432 8 8
+ - image: solid-color(71, 54, 0, 255, 8, 8)
+ bounds: 568 432 8 8
+ - image: solid-color(72, 54, 0, 255, 8, 8)
+ bounds: 576 432 8 8
+ - image: solid-color(73, 54, 0, 255, 8, 8)
+ bounds: 584 432 8 8
+ - image: solid-color(74, 54, 0, 255, 8, 8)
+ bounds: 592 432 8 8
+ - image: solid-color(75, 54, 0, 255, 8, 8)
+ bounds: 600 432 8 8
+ - image: solid-color(76, 54, 0, 255, 8, 8)
+ bounds: 608 432 8 8
+ - image: solid-color(77, 54, 0, 255, 8, 8)
+ bounds: 616 432 8 8
+ - image: solid-color(78, 54, 0, 255, 8, 8)
+ bounds: 624 432 8 8
+ - image: solid-color(79, 54, 0, 255, 8, 8)
+ bounds: 632 432 8 8
+ - image: solid-color(80, 54, 0, 255, 8, 8)
+ bounds: 640 432 8 8
+ - image: solid-color(81, 54, 0, 255, 8, 8)
+ bounds: 648 432 8 8
+ - image: solid-color(82, 54, 0, 255, 8, 8)
+ bounds: 656 432 8 8
+ - image: solid-color(83, 54, 0, 255, 8, 8)
+ bounds: 664 432 8 8
+ - image: solid-color(84, 54, 0, 255, 8, 8)
+ bounds: 672 432 8 8
+ - image: solid-color(85, 54, 0, 255, 8, 8)
+ bounds: 680 432 8 8
+ - image: solid-color(86, 54, 0, 255, 8, 8)
+ bounds: 688 432 8 8
+ - image: solid-color(87, 54, 0, 255, 8, 8)
+ bounds: 696 432 8 8
+ - image: solid-color(88, 54, 0, 255, 8, 8)
+ bounds: 704 432 8 8
+ - image: solid-color(89, 54, 0, 255, 8, 8)
+ bounds: 712 432 8 8
+ - image: solid-color(90, 54, 0, 255, 8, 8)
+ bounds: 720 432 8 8
+ - image: solid-color(91, 54, 0, 255, 8, 8)
+ bounds: 728 432 8 8
+ - image: solid-color(92, 54, 0, 255, 8, 8)
+ bounds: 736 432 8 8
+ - image: solid-color(93, 54, 0, 255, 8, 8)
+ bounds: 744 432 8 8
+ - image: solid-color(94, 54, 0, 255, 8, 8)
+ bounds: 752 432 8 8
+ - image: solid-color(95, 54, 0, 255, 8, 8)
+ bounds: 760 432 8 8
+ - image: solid-color(96, 54, 0, 255, 8, 8)
+ bounds: 768 432 8 8
+ - image: solid-color(97, 54, 0, 255, 8, 8)
+ bounds: 776 432 8 8
+ - image: solid-color(98, 54, 0, 255, 8, 8)
+ bounds: 784 432 8 8
+ - image: solid-color(99, 54, 0, 255, 8, 8)
+ bounds: 792 432 8 8
+ - image: solid-color(100, 54, 0, 255, 8, 8)
+ bounds: 800 432 8 8
+ - image: solid-color(101, 54, 0, 255, 8, 8)
+ bounds: 808 432 8 8
+ - image: solid-color(102, 54, 0, 255, 8, 8)
+ bounds: 816 432 8 8
+ - image: solid-color(103, 54, 0, 255, 8, 8)
+ bounds: 824 432 8 8
+ - image: solid-color(104, 54, 0, 255, 8, 8)
+ bounds: 832 432 8 8
+ - image: solid-color(105, 54, 0, 255, 8, 8)
+ bounds: 840 432 8 8
+ - image: solid-color(106, 54, 0, 255, 8, 8)
+ bounds: 848 432 8 8
+ - image: solid-color(107, 54, 0, 255, 8, 8)
+ bounds: 856 432 8 8
+ - image: solid-color(108, 54, 0, 255, 8, 8)
+ bounds: 864 432 8 8
+ - image: solid-color(109, 54, 0, 255, 8, 8)
+ bounds: 872 432 8 8
+ - image: solid-color(110, 54, 0, 255, 8, 8)
+ bounds: 880 432 8 8
+ - image: solid-color(111, 54, 0, 255, 8, 8)
+ bounds: 888 432 8 8
+ - image: solid-color(112, 54, 0, 255, 8, 8)
+ bounds: 896 432 8 8
+ - image: solid-color(113, 54, 0, 255, 8, 8)
+ bounds: 904 432 8 8
+ - image: solid-color(114, 54, 0, 255, 8, 8)
+ bounds: 912 432 8 8
+ - image: solid-color(115, 54, 0, 255, 8, 8)
+ bounds: 920 432 8 8
+ - image: solid-color(116, 54, 0, 255, 8, 8)
+ bounds: 928 432 8 8
+ - image: solid-color(117, 54, 0, 255, 8, 8)
+ bounds: 936 432 8 8
+ - image: solid-color(118, 54, 0, 255, 8, 8)
+ bounds: 944 432 8 8
+ - image: solid-color(119, 54, 0, 255, 8, 8)
+ bounds: 952 432 8 8
+ - image: solid-color(120, 54, 0, 255, 8, 8)
+ bounds: 960 432 8 8
+ - image: solid-color(121, 54, 0, 255, 8, 8)
+ bounds: 968 432 8 8
+ - image: solid-color(122, 54, 0, 255, 8, 8)
+ bounds: 976 432 8 8
+ - image: solid-color(123, 54, 0, 255, 8, 8)
+ bounds: 984 432 8 8
+ - image: solid-color(124, 54, 0, 255, 8, 8)
+ bounds: 992 432 8 8
+ - image: solid-color(125, 54, 0, 255, 8, 8)
+ bounds: 1000 432 8 8
+ - image: solid-color(126, 54, 0, 255, 8, 8)
+ bounds: 1008 432 8 8
+ - image: solid-color(127, 54, 0, 255, 8, 8)
+ bounds: 1016 432 8 8
+ - image: solid-color(0, 55, 0, 255, 8, 8)
+ bounds: 0 440 8 8
+ - image: solid-color(1, 55, 0, 255, 8, 8)
+ bounds: 8 440 8 8
+ - image: solid-color(2, 55, 0, 255, 8, 8)
+ bounds: 16 440 8 8
+ - image: solid-color(3, 55, 0, 255, 8, 8)
+ bounds: 24 440 8 8
+ - image: solid-color(4, 55, 0, 255, 8, 8)
+ bounds: 32 440 8 8
+ - image: solid-color(5, 55, 0, 255, 8, 8)
+ bounds: 40 440 8 8
+ - image: solid-color(6, 55, 0, 255, 8, 8)
+ bounds: 48 440 8 8
+ - image: solid-color(7, 55, 0, 255, 8, 8)
+ bounds: 56 440 8 8
+ - image: solid-color(8, 55, 0, 255, 8, 8)
+ bounds: 64 440 8 8
+ - image: solid-color(9, 55, 0, 255, 8, 8)
+ bounds: 72 440 8 8
+ - image: solid-color(10, 55, 0, 255, 8, 8)
+ bounds: 80 440 8 8
+ - image: solid-color(11, 55, 0, 255, 8, 8)
+ bounds: 88 440 8 8
+ - image: solid-color(12, 55, 0, 255, 8, 8)
+ bounds: 96 440 8 8
+ - image: solid-color(13, 55, 0, 255, 8, 8)
+ bounds: 104 440 8 8
+ - image: solid-color(14, 55, 0, 255, 8, 8)
+ bounds: 112 440 8 8
+ - image: solid-color(15, 55, 0, 255, 8, 8)
+ bounds: 120 440 8 8
+ - image: solid-color(16, 55, 0, 255, 8, 8)
+ bounds: 128 440 8 8
+ - image: solid-color(17, 55, 0, 255, 8, 8)
+ bounds: 136 440 8 8
+ - image: solid-color(18, 55, 0, 255, 8, 8)
+ bounds: 144 440 8 8
+ - image: solid-color(19, 55, 0, 255, 8, 8)
+ bounds: 152 440 8 8
+ - image: solid-color(20, 55, 0, 255, 8, 8)
+ bounds: 160 440 8 8
+ - image: solid-color(21, 55, 0, 255, 8, 8)
+ bounds: 168 440 8 8
+ - image: solid-color(22, 55, 0, 255, 8, 8)
+ bounds: 176 440 8 8
+ - image: solid-color(23, 55, 0, 255, 8, 8)
+ bounds: 184 440 8 8
+ - image: solid-color(24, 55, 0, 255, 8, 8)
+ bounds: 192 440 8 8
+ - image: solid-color(25, 55, 0, 255, 8, 8)
+ bounds: 200 440 8 8
+ - image: solid-color(26, 55, 0, 255, 8, 8)
+ bounds: 208 440 8 8
+ - image: solid-color(27, 55, 0, 255, 8, 8)
+ bounds: 216 440 8 8
+ - image: solid-color(28, 55, 0, 255, 8, 8)
+ bounds: 224 440 8 8
+ - image: solid-color(29, 55, 0, 255, 8, 8)
+ bounds: 232 440 8 8
+ - image: solid-color(30, 55, 0, 255, 8, 8)
+ bounds: 240 440 8 8
+ - image: solid-color(31, 55, 0, 255, 8, 8)
+ bounds: 248 440 8 8
+ - image: solid-color(32, 55, 0, 255, 8, 8)
+ bounds: 256 440 8 8
+ - image: solid-color(33, 55, 0, 255, 8, 8)
+ bounds: 264 440 8 8
+ - image: solid-color(34, 55, 0, 255, 8, 8)
+ bounds: 272 440 8 8
+ - image: solid-color(35, 55, 0, 255, 8, 8)
+ bounds: 280 440 8 8
+ - image: solid-color(36, 55, 0, 255, 8, 8)
+ bounds: 288 440 8 8
+ - image: solid-color(37, 55, 0, 255, 8, 8)
+ bounds: 296 440 8 8
+ - image: solid-color(38, 55, 0, 255, 8, 8)
+ bounds: 304 440 8 8
+ - image: solid-color(39, 55, 0, 255, 8, 8)
+ bounds: 312 440 8 8
+ - image: solid-color(40, 55, 0, 255, 8, 8)
+ bounds: 320 440 8 8
+ - image: solid-color(41, 55, 0, 255, 8, 8)
+ bounds: 328 440 8 8
+ - image: solid-color(42, 55, 0, 255, 8, 8)
+ bounds: 336 440 8 8
+ - image: solid-color(43, 55, 0, 255, 8, 8)
+ bounds: 344 440 8 8
+ - image: solid-color(44, 55, 0, 255, 8, 8)
+ bounds: 352 440 8 8
+ - image: solid-color(45, 55, 0, 255, 8, 8)
+ bounds: 360 440 8 8
+ - image: solid-color(46, 55, 0, 255, 8, 8)
+ bounds: 368 440 8 8
+ - image: solid-color(47, 55, 0, 255, 8, 8)
+ bounds: 376 440 8 8
+ - image: solid-color(48, 55, 0, 255, 8, 8)
+ bounds: 384 440 8 8
+ - image: solid-color(49, 55, 0, 255, 8, 8)
+ bounds: 392 440 8 8
+ - image: solid-color(50, 55, 0, 255, 8, 8)
+ bounds: 400 440 8 8
+ - image: solid-color(51, 55, 0, 255, 8, 8)
+ bounds: 408 440 8 8
+ - image: solid-color(52, 55, 0, 255, 8, 8)
+ bounds: 416 440 8 8
+ - image: solid-color(53, 55, 0, 255, 8, 8)
+ bounds: 424 440 8 8
+ - image: solid-color(54, 55, 0, 255, 8, 8)
+ bounds: 432 440 8 8
+ - image: solid-color(55, 55, 0, 255, 8, 8)
+ bounds: 440 440 8 8
+ - image: solid-color(56, 55, 0, 255, 8, 8)
+ bounds: 448 440 8 8
+ - image: solid-color(57, 55, 0, 255, 8, 8)
+ bounds: 456 440 8 8
+ - image: solid-color(58, 55, 0, 255, 8, 8)
+ bounds: 464 440 8 8
+ - image: solid-color(59, 55, 0, 255, 8, 8)
+ bounds: 472 440 8 8
+ - image: solid-color(60, 55, 0, 255, 8, 8)
+ bounds: 480 440 8 8
+ - image: solid-color(61, 55, 0, 255, 8, 8)
+ bounds: 488 440 8 8
+ - image: solid-color(62, 55, 0, 255, 8, 8)
+ bounds: 496 440 8 8
+ - image: solid-color(63, 55, 0, 255, 8, 8)
+ bounds: 504 440 8 8
+ - image: solid-color(64, 55, 0, 255, 8, 8)
+ bounds: 512 440 8 8
+ - image: solid-color(65, 55, 0, 255, 8, 8)
+ bounds: 520 440 8 8
+ - image: solid-color(66, 55, 0, 255, 8, 8)
+ bounds: 528 440 8 8
+ - image: solid-color(67, 55, 0, 255, 8, 8)
+ bounds: 536 440 8 8
+ - image: solid-color(68, 55, 0, 255, 8, 8)
+ bounds: 544 440 8 8
+ - image: solid-color(69, 55, 0, 255, 8, 8)
+ bounds: 552 440 8 8
+ - image: solid-color(70, 55, 0, 255, 8, 8)
+ bounds: 560 440 8 8
+ - image: solid-color(71, 55, 0, 255, 8, 8)
+ bounds: 568 440 8 8
+ - image: solid-color(72, 55, 0, 255, 8, 8)
+ bounds: 576 440 8 8
+ - image: solid-color(73, 55, 0, 255, 8, 8)
+ bounds: 584 440 8 8
+ - image: solid-color(74, 55, 0, 255, 8, 8)
+ bounds: 592 440 8 8
+ - image: solid-color(75, 55, 0, 255, 8, 8)
+ bounds: 600 440 8 8
+ - image: solid-color(76, 55, 0, 255, 8, 8)
+ bounds: 608 440 8 8
+ - image: solid-color(77, 55, 0, 255, 8, 8)
+ bounds: 616 440 8 8
+ - image: solid-color(78, 55, 0, 255, 8, 8)
+ bounds: 624 440 8 8
+ - image: solid-color(79, 55, 0, 255, 8, 8)
+ bounds: 632 440 8 8
+ - image: solid-color(80, 55, 0, 255, 8, 8)
+ bounds: 640 440 8 8
+ - image: solid-color(81, 55, 0, 255, 8, 8)
+ bounds: 648 440 8 8
+ - image: solid-color(82, 55, 0, 255, 8, 8)
+ bounds: 656 440 8 8
+ - image: solid-color(83, 55, 0, 255, 8, 8)
+ bounds: 664 440 8 8
+ - image: solid-color(84, 55, 0, 255, 8, 8)
+ bounds: 672 440 8 8
+ - image: solid-color(85, 55, 0, 255, 8, 8)
+ bounds: 680 440 8 8
+ - image: solid-color(86, 55, 0, 255, 8, 8)
+ bounds: 688 440 8 8
+ - image: solid-color(87, 55, 0, 255, 8, 8)
+ bounds: 696 440 8 8
+ - image: solid-color(88, 55, 0, 255, 8, 8)
+ bounds: 704 440 8 8
+ - image: solid-color(89, 55, 0, 255, 8, 8)
+ bounds: 712 440 8 8
+ - image: solid-color(90, 55, 0, 255, 8, 8)
+ bounds: 720 440 8 8
+ - image: solid-color(91, 55, 0, 255, 8, 8)
+ bounds: 728 440 8 8
+ - image: solid-color(92, 55, 0, 255, 8, 8)
+ bounds: 736 440 8 8
+ - image: solid-color(93, 55, 0, 255, 8, 8)
+ bounds: 744 440 8 8
+ - image: solid-color(94, 55, 0, 255, 8, 8)
+ bounds: 752 440 8 8
+ - image: solid-color(95, 55, 0, 255, 8, 8)
+ bounds: 760 440 8 8
+ - image: solid-color(96, 55, 0, 255, 8, 8)
+ bounds: 768 440 8 8
+ - image: solid-color(97, 55, 0, 255, 8, 8)
+ bounds: 776 440 8 8
+ - image: solid-color(98, 55, 0, 255, 8, 8)
+ bounds: 784 440 8 8
+ - image: solid-color(99, 55, 0, 255, 8, 8)
+ bounds: 792 440 8 8
+ - image: solid-color(100, 55, 0, 255, 8, 8)
+ bounds: 800 440 8 8
+ - image: solid-color(101, 55, 0, 255, 8, 8)
+ bounds: 808 440 8 8
+ - image: solid-color(102, 55, 0, 255, 8, 8)
+ bounds: 816 440 8 8
+ - image: solid-color(103, 55, 0, 255, 8, 8)
+ bounds: 824 440 8 8
+ - image: solid-color(104, 55, 0, 255, 8, 8)
+ bounds: 832 440 8 8
+ - image: solid-color(105, 55, 0, 255, 8, 8)
+ bounds: 840 440 8 8
+ - image: solid-color(106, 55, 0, 255, 8, 8)
+ bounds: 848 440 8 8
+ - image: solid-color(107, 55, 0, 255, 8, 8)
+ bounds: 856 440 8 8
+ - image: solid-color(108, 55, 0, 255, 8, 8)
+ bounds: 864 440 8 8
+ - image: solid-color(109, 55, 0, 255, 8, 8)
+ bounds: 872 440 8 8
+ - image: solid-color(110, 55, 0, 255, 8, 8)
+ bounds: 880 440 8 8
+ - image: solid-color(111, 55, 0, 255, 8, 8)
+ bounds: 888 440 8 8
+ - image: solid-color(112, 55, 0, 255, 8, 8)
+ bounds: 896 440 8 8
+ - image: solid-color(113, 55, 0, 255, 8, 8)
+ bounds: 904 440 8 8
+ - image: solid-color(114, 55, 0, 255, 8, 8)
+ bounds: 912 440 8 8
+ - image: solid-color(115, 55, 0, 255, 8, 8)
+ bounds: 920 440 8 8
+ - image: solid-color(116, 55, 0, 255, 8, 8)
+ bounds: 928 440 8 8
+ - image: solid-color(117, 55, 0, 255, 8, 8)
+ bounds: 936 440 8 8
+ - image: solid-color(118, 55, 0, 255, 8, 8)
+ bounds: 944 440 8 8
+ - image: solid-color(119, 55, 0, 255, 8, 8)
+ bounds: 952 440 8 8
+ - image: solid-color(120, 55, 0, 255, 8, 8)
+ bounds: 960 440 8 8
+ - image: solid-color(121, 55, 0, 255, 8, 8)
+ bounds: 968 440 8 8
+ - image: solid-color(122, 55, 0, 255, 8, 8)
+ bounds: 976 440 8 8
+ - image: solid-color(123, 55, 0, 255, 8, 8)
+ bounds: 984 440 8 8
+ - image: solid-color(124, 55, 0, 255, 8, 8)
+ bounds: 992 440 8 8
+ - image: solid-color(125, 55, 0, 255, 8, 8)
+ bounds: 1000 440 8 8
+ - image: solid-color(126, 55, 0, 255, 8, 8)
+ bounds: 1008 440 8 8
+ - image: solid-color(127, 55, 0, 255, 8, 8)
+ bounds: 1016 440 8 8
+ - image: solid-color(0, 56, 0, 255, 8, 8)
+ bounds: 0 448 8 8
+ - image: solid-color(1, 56, 0, 255, 8, 8)
+ bounds: 8 448 8 8
+ - image: solid-color(2, 56, 0, 255, 8, 8)
+ bounds: 16 448 8 8
+ - image: solid-color(3, 56, 0, 255, 8, 8)
+ bounds: 24 448 8 8
+ - image: solid-color(4, 56, 0, 255, 8, 8)
+ bounds: 32 448 8 8
+ - image: solid-color(5, 56, 0, 255, 8, 8)
+ bounds: 40 448 8 8
+ - image: solid-color(6, 56, 0, 255, 8, 8)
+ bounds: 48 448 8 8
+ - image: solid-color(7, 56, 0, 255, 8, 8)
+ bounds: 56 448 8 8
+ - image: solid-color(8, 56, 0, 255, 8, 8)
+ bounds: 64 448 8 8
+ - image: solid-color(9, 56, 0, 255, 8, 8)
+ bounds: 72 448 8 8
+ - image: solid-color(10, 56, 0, 255, 8, 8)
+ bounds: 80 448 8 8
+ - image: solid-color(11, 56, 0, 255, 8, 8)
+ bounds: 88 448 8 8
+ - image: solid-color(12, 56, 0, 255, 8, 8)
+ bounds: 96 448 8 8
+ - image: solid-color(13, 56, 0, 255, 8, 8)
+ bounds: 104 448 8 8
+ - image: solid-color(14, 56, 0, 255, 8, 8)
+ bounds: 112 448 8 8
+ - image: solid-color(15, 56, 0, 255, 8, 8)
+ bounds: 120 448 8 8
+ - image: solid-color(16, 56, 0, 255, 8, 8)
+ bounds: 128 448 8 8
+ - image: solid-color(17, 56, 0, 255, 8, 8)
+ bounds: 136 448 8 8
+ - image: solid-color(18, 56, 0, 255, 8, 8)
+ bounds: 144 448 8 8
+ - image: solid-color(19, 56, 0, 255, 8, 8)
+ bounds: 152 448 8 8
+ - image: solid-color(20, 56, 0, 255, 8, 8)
+ bounds: 160 448 8 8
+ - image: solid-color(21, 56, 0, 255, 8, 8)
+ bounds: 168 448 8 8
+ - image: solid-color(22, 56, 0, 255, 8, 8)
+ bounds: 176 448 8 8
+ - image: solid-color(23, 56, 0, 255, 8, 8)
+ bounds: 184 448 8 8
+ - image: solid-color(24, 56, 0, 255, 8, 8)
+ bounds: 192 448 8 8
+ - image: solid-color(25, 56, 0, 255, 8, 8)
+ bounds: 200 448 8 8
+ - image: solid-color(26, 56, 0, 255, 8, 8)
+ bounds: 208 448 8 8
+ - image: solid-color(27, 56, 0, 255, 8, 8)
+ bounds: 216 448 8 8
+ - image: solid-color(28, 56, 0, 255, 8, 8)
+ bounds: 224 448 8 8
+ - image: solid-color(29, 56, 0, 255, 8, 8)
+ bounds: 232 448 8 8
+ - image: solid-color(30, 56, 0, 255, 8, 8)
+ bounds: 240 448 8 8
+ - image: solid-color(31, 56, 0, 255, 8, 8)
+ bounds: 248 448 8 8
+ - image: solid-color(32, 56, 0, 255, 8, 8)
+ bounds: 256 448 8 8
+ - image: solid-color(33, 56, 0, 255, 8, 8)
+ bounds: 264 448 8 8
+ - image: solid-color(34, 56, 0, 255, 8, 8)
+ bounds: 272 448 8 8
+ - image: solid-color(35, 56, 0, 255, 8, 8)
+ bounds: 280 448 8 8
+ - image: solid-color(36, 56, 0, 255, 8, 8)
+ bounds: 288 448 8 8
+ - image: solid-color(37, 56, 0, 255, 8, 8)
+ bounds: 296 448 8 8
+ - image: solid-color(38, 56, 0, 255, 8, 8)
+ bounds: 304 448 8 8
+ - image: solid-color(39, 56, 0, 255, 8, 8)
+ bounds: 312 448 8 8
+ - image: solid-color(40, 56, 0, 255, 8, 8)
+ bounds: 320 448 8 8
+ - image: solid-color(41, 56, 0, 255, 8, 8)
+ bounds: 328 448 8 8
+ - image: solid-color(42, 56, 0, 255, 8, 8)
+ bounds: 336 448 8 8
+ - image: solid-color(43, 56, 0, 255, 8, 8)
+ bounds: 344 448 8 8
+ - image: solid-color(44, 56, 0, 255, 8, 8)
+ bounds: 352 448 8 8
+ - image: solid-color(45, 56, 0, 255, 8, 8)
+ bounds: 360 448 8 8
+ - image: solid-color(46, 56, 0, 255, 8, 8)
+ bounds: 368 448 8 8
+ - image: solid-color(47, 56, 0, 255, 8, 8)
+ bounds: 376 448 8 8
+ - image: solid-color(48, 56, 0, 255, 8, 8)
+ bounds: 384 448 8 8
+ - image: solid-color(49, 56, 0, 255, 8, 8)
+ bounds: 392 448 8 8
+ - image: solid-color(50, 56, 0, 255, 8, 8)
+ bounds: 400 448 8 8
+ - image: solid-color(51, 56, 0, 255, 8, 8)
+ bounds: 408 448 8 8
+ - image: solid-color(52, 56, 0, 255, 8, 8)
+ bounds: 416 448 8 8
+ - image: solid-color(53, 56, 0, 255, 8, 8)
+ bounds: 424 448 8 8
+ - image: solid-color(54, 56, 0, 255, 8, 8)
+ bounds: 432 448 8 8
+ - image: solid-color(55, 56, 0, 255, 8, 8)
+ bounds: 440 448 8 8
+ - image: solid-color(56, 56, 0, 255, 8, 8)
+ bounds: 448 448 8 8
+ - image: solid-color(57, 56, 0, 255, 8, 8)
+ bounds: 456 448 8 8
+ - image: solid-color(58, 56, 0, 255, 8, 8)
+ bounds: 464 448 8 8
+ - image: solid-color(59, 56, 0, 255, 8, 8)
+ bounds: 472 448 8 8
+ - image: solid-color(60, 56, 0, 255, 8, 8)
+ bounds: 480 448 8 8
+ - image: solid-color(61, 56, 0, 255, 8, 8)
+ bounds: 488 448 8 8
+ - image: solid-color(62, 56, 0, 255, 8, 8)
+ bounds: 496 448 8 8
+ - image: solid-color(63, 56, 0, 255, 8, 8)
+ bounds: 504 448 8 8
+ - image: solid-color(64, 56, 0, 255, 8, 8)
+ bounds: 512 448 8 8
+ - image: solid-color(65, 56, 0, 255, 8, 8)
+ bounds: 520 448 8 8
+ - image: solid-color(66, 56, 0, 255, 8, 8)
+ bounds: 528 448 8 8
+ - image: solid-color(67, 56, 0, 255, 8, 8)
+ bounds: 536 448 8 8
+ - image: solid-color(68, 56, 0, 255, 8, 8)
+ bounds: 544 448 8 8
+ - image: solid-color(69, 56, 0, 255, 8, 8)
+ bounds: 552 448 8 8
+ - image: solid-color(70, 56, 0, 255, 8, 8)
+ bounds: 560 448 8 8
+ - image: solid-color(71, 56, 0, 255, 8, 8)
+ bounds: 568 448 8 8
+ - image: solid-color(72, 56, 0, 255, 8, 8)
+ bounds: 576 448 8 8
+ - image: solid-color(73, 56, 0, 255, 8, 8)
+ bounds: 584 448 8 8
+ - image: solid-color(74, 56, 0, 255, 8, 8)
+ bounds: 592 448 8 8
+ - image: solid-color(75, 56, 0, 255, 8, 8)
+ bounds: 600 448 8 8
+ - image: solid-color(76, 56, 0, 255, 8, 8)
+ bounds: 608 448 8 8
+ - image: solid-color(77, 56, 0, 255, 8, 8)
+ bounds: 616 448 8 8
+ - image: solid-color(78, 56, 0, 255, 8, 8)
+ bounds: 624 448 8 8
+ - image: solid-color(79, 56, 0, 255, 8, 8)
+ bounds: 632 448 8 8
+ - image: solid-color(80, 56, 0, 255, 8, 8)
+ bounds: 640 448 8 8
+ - image: solid-color(81, 56, 0, 255, 8, 8)
+ bounds: 648 448 8 8
+ - image: solid-color(82, 56, 0, 255, 8, 8)
+ bounds: 656 448 8 8
+ - image: solid-color(83, 56, 0, 255, 8, 8)
+ bounds: 664 448 8 8
+ - image: solid-color(84, 56, 0, 255, 8, 8)
+ bounds: 672 448 8 8
+ - image: solid-color(85, 56, 0, 255, 8, 8)
+ bounds: 680 448 8 8
+ - image: solid-color(86, 56, 0, 255, 8, 8)
+ bounds: 688 448 8 8
+ - image: solid-color(87, 56, 0, 255, 8, 8)
+ bounds: 696 448 8 8
+ - image: solid-color(88, 56, 0, 255, 8, 8)
+ bounds: 704 448 8 8
+ - image: solid-color(89, 56, 0, 255, 8, 8)
+ bounds: 712 448 8 8
+ - image: solid-color(90, 56, 0, 255, 8, 8)
+ bounds: 720 448 8 8
+ - image: solid-color(91, 56, 0, 255, 8, 8)
+ bounds: 728 448 8 8
+ - image: solid-color(92, 56, 0, 255, 8, 8)
+ bounds: 736 448 8 8
+ - image: solid-color(93, 56, 0, 255, 8, 8)
+ bounds: 744 448 8 8
+ - image: solid-color(94, 56, 0, 255, 8, 8)
+ bounds: 752 448 8 8
+ - image: solid-color(95, 56, 0, 255, 8, 8)
+ bounds: 760 448 8 8
+ - image: solid-color(96, 56, 0, 255, 8, 8)
+ bounds: 768 448 8 8
+ - image: solid-color(97, 56, 0, 255, 8, 8)
+ bounds: 776 448 8 8
+ - image: solid-color(98, 56, 0, 255, 8, 8)
+ bounds: 784 448 8 8
+ - image: solid-color(99, 56, 0, 255, 8, 8)
+ bounds: 792 448 8 8
+ - image: solid-color(100, 56, 0, 255, 8, 8)
+ bounds: 800 448 8 8
+ - image: solid-color(101, 56, 0, 255, 8, 8)
+ bounds: 808 448 8 8
+ - image: solid-color(102, 56, 0, 255, 8, 8)
+ bounds: 816 448 8 8
+ - image: solid-color(103, 56, 0, 255, 8, 8)
+ bounds: 824 448 8 8
+ - image: solid-color(104, 56, 0, 255, 8, 8)
+ bounds: 832 448 8 8
+ - image: solid-color(105, 56, 0, 255, 8, 8)
+ bounds: 840 448 8 8
+ - image: solid-color(106, 56, 0, 255, 8, 8)
+ bounds: 848 448 8 8
+ - image: solid-color(107, 56, 0, 255, 8, 8)
+ bounds: 856 448 8 8
+ - image: solid-color(108, 56, 0, 255, 8, 8)
+ bounds: 864 448 8 8
+ - image: solid-color(109, 56, 0, 255, 8, 8)
+ bounds: 872 448 8 8
+ - image: solid-color(110, 56, 0, 255, 8, 8)
+ bounds: 880 448 8 8
+ - image: solid-color(111, 56, 0, 255, 8, 8)
+ bounds: 888 448 8 8
+ - image: solid-color(112, 56, 0, 255, 8, 8)
+ bounds: 896 448 8 8
+ - image: solid-color(113, 56, 0, 255, 8, 8)
+ bounds: 904 448 8 8
+ - image: solid-color(114, 56, 0, 255, 8, 8)
+ bounds: 912 448 8 8
+ - image: solid-color(115, 56, 0, 255, 8, 8)
+ bounds: 920 448 8 8
+ - image: solid-color(116, 56, 0, 255, 8, 8)
+ bounds: 928 448 8 8
+ - image: solid-color(117, 56, 0, 255, 8, 8)
+ bounds: 936 448 8 8
+ - image: solid-color(118, 56, 0, 255, 8, 8)
+ bounds: 944 448 8 8
+ - image: solid-color(119, 56, 0, 255, 8, 8)
+ bounds: 952 448 8 8
+ - image: solid-color(120, 56, 0, 255, 8, 8)
+ bounds: 960 448 8 8
+ - image: solid-color(121, 56, 0, 255, 8, 8)
+ bounds: 968 448 8 8
+ - image: solid-color(122, 56, 0, 255, 8, 8)
+ bounds: 976 448 8 8
+ - image: solid-color(123, 56, 0, 255, 8, 8)
+ bounds: 984 448 8 8
+ - image: solid-color(124, 56, 0, 255, 8, 8)
+ bounds: 992 448 8 8
+ - image: solid-color(125, 56, 0, 255, 8, 8)
+ bounds: 1000 448 8 8
+ - image: solid-color(126, 56, 0, 255, 8, 8)
+ bounds: 1008 448 8 8
+ - image: solid-color(127, 56, 0, 255, 8, 8)
+ bounds: 1016 448 8 8
+ - image: solid-color(0, 57, 0, 255, 8, 8)
+ bounds: 0 456 8 8
+ - image: solid-color(1, 57, 0, 255, 8, 8)
+ bounds: 8 456 8 8
+ - image: solid-color(2, 57, 0, 255, 8, 8)
+ bounds: 16 456 8 8
+ - image: solid-color(3, 57, 0, 255, 8, 8)
+ bounds: 24 456 8 8
+ - image: solid-color(4, 57, 0, 255, 8, 8)
+ bounds: 32 456 8 8
+ - image: solid-color(5, 57, 0, 255, 8, 8)
+ bounds: 40 456 8 8
+ - image: solid-color(6, 57, 0, 255, 8, 8)
+ bounds: 48 456 8 8
+ - image: solid-color(7, 57, 0, 255, 8, 8)
+ bounds: 56 456 8 8
+ - image: solid-color(8, 57, 0, 255, 8, 8)
+ bounds: 64 456 8 8
+ - image: solid-color(9, 57, 0, 255, 8, 8)
+ bounds: 72 456 8 8
+ - image: solid-color(10, 57, 0, 255, 8, 8)
+ bounds: 80 456 8 8
+ - image: solid-color(11, 57, 0, 255, 8, 8)
+ bounds: 88 456 8 8
+ - image: solid-color(12, 57, 0, 255, 8, 8)
+ bounds: 96 456 8 8
+ - image: solid-color(13, 57, 0, 255, 8, 8)
+ bounds: 104 456 8 8
+ - image: solid-color(14, 57, 0, 255, 8, 8)
+ bounds: 112 456 8 8
+ - image: solid-color(15, 57, 0, 255, 8, 8)
+ bounds: 120 456 8 8
+ - image: solid-color(16, 57, 0, 255, 8, 8)
+ bounds: 128 456 8 8
+ - image: solid-color(17, 57, 0, 255, 8, 8)
+ bounds: 136 456 8 8
+ - image: solid-color(18, 57, 0, 255, 8, 8)
+ bounds: 144 456 8 8
+ - image: solid-color(19, 57, 0, 255, 8, 8)
+ bounds: 152 456 8 8
+ - image: solid-color(20, 57, 0, 255, 8, 8)
+ bounds: 160 456 8 8
+ - image: solid-color(21, 57, 0, 255, 8, 8)
+ bounds: 168 456 8 8
+ - image: solid-color(22, 57, 0, 255, 8, 8)
+ bounds: 176 456 8 8
+ - image: solid-color(23, 57, 0, 255, 8, 8)
+ bounds: 184 456 8 8
+ - image: solid-color(24, 57, 0, 255, 8, 8)
+ bounds: 192 456 8 8
+ - image: solid-color(25, 57, 0, 255, 8, 8)
+ bounds: 200 456 8 8
+ - image: solid-color(26, 57, 0, 255, 8, 8)
+ bounds: 208 456 8 8
+ - image: solid-color(27, 57, 0, 255, 8, 8)
+ bounds: 216 456 8 8
+ - image: solid-color(28, 57, 0, 255, 8, 8)
+ bounds: 224 456 8 8
+ - image: solid-color(29, 57, 0, 255, 8, 8)
+ bounds: 232 456 8 8
+ - image: solid-color(30, 57, 0, 255, 8, 8)
+ bounds: 240 456 8 8
+ - image: solid-color(31, 57, 0, 255, 8, 8)
+ bounds: 248 456 8 8
+ - image: solid-color(32, 57, 0, 255, 8, 8)
+ bounds: 256 456 8 8
+ - image: solid-color(33, 57, 0, 255, 8, 8)
+ bounds: 264 456 8 8
+ - image: solid-color(34, 57, 0, 255, 8, 8)
+ bounds: 272 456 8 8
+ - image: solid-color(35, 57, 0, 255, 8, 8)
+ bounds: 280 456 8 8
+ - image: solid-color(36, 57, 0, 255, 8, 8)
+ bounds: 288 456 8 8
+ - image: solid-color(37, 57, 0, 255, 8, 8)
+ bounds: 296 456 8 8
+ - image: solid-color(38, 57, 0, 255, 8, 8)
+ bounds: 304 456 8 8
+ - image: solid-color(39, 57, 0, 255, 8, 8)
+ bounds: 312 456 8 8
+ - image: solid-color(40, 57, 0, 255, 8, 8)
+ bounds: 320 456 8 8
+ - image: solid-color(41, 57, 0, 255, 8, 8)
+ bounds: 328 456 8 8
+ - image: solid-color(42, 57, 0, 255, 8, 8)
+ bounds: 336 456 8 8
+ - image: solid-color(43, 57, 0, 255, 8, 8)
+ bounds: 344 456 8 8
+ - image: solid-color(44, 57, 0, 255, 8, 8)
+ bounds: 352 456 8 8
+ - image: solid-color(45, 57, 0, 255, 8, 8)
+ bounds: 360 456 8 8
+ - image: solid-color(46, 57, 0, 255, 8, 8)
+ bounds: 368 456 8 8
+ - image: solid-color(47, 57, 0, 255, 8, 8)
+ bounds: 376 456 8 8
+ - image: solid-color(48, 57, 0, 255, 8, 8)
+ bounds: 384 456 8 8
+ - image: solid-color(49, 57, 0, 255, 8, 8)
+ bounds: 392 456 8 8
+ - image: solid-color(50, 57, 0, 255, 8, 8)
+ bounds: 400 456 8 8
+ - image: solid-color(51, 57, 0, 255, 8, 8)
+ bounds: 408 456 8 8
+ - image: solid-color(52, 57, 0, 255, 8, 8)
+ bounds: 416 456 8 8
+ - image: solid-color(53, 57, 0, 255, 8, 8)
+ bounds: 424 456 8 8
+ - image: solid-color(54, 57, 0, 255, 8, 8)
+ bounds: 432 456 8 8
+ - image: solid-color(55, 57, 0, 255, 8, 8)
+ bounds: 440 456 8 8
+ - image: solid-color(56, 57, 0, 255, 8, 8)
+ bounds: 448 456 8 8
+ - image: solid-color(57, 57, 0, 255, 8, 8)
+ bounds: 456 456 8 8
+ - image: solid-color(58, 57, 0, 255, 8, 8)
+ bounds: 464 456 8 8
+ - image: solid-color(59, 57, 0, 255, 8, 8)
+ bounds: 472 456 8 8
+ - image: solid-color(60, 57, 0, 255, 8, 8)
+ bounds: 480 456 8 8
+ - image: solid-color(61, 57, 0, 255, 8, 8)
+ bounds: 488 456 8 8
+ - image: solid-color(62, 57, 0, 255, 8, 8)
+ bounds: 496 456 8 8
+ - image: solid-color(63, 57, 0, 255, 8, 8)
+ bounds: 504 456 8 8
+ - image: solid-color(64, 57, 0, 255, 8, 8)
+ bounds: 512 456 8 8
+ - image: solid-color(65, 57, 0, 255, 8, 8)
+ bounds: 520 456 8 8
+ - image: solid-color(66, 57, 0, 255, 8, 8)
+ bounds: 528 456 8 8
+ - image: solid-color(67, 57, 0, 255, 8, 8)
+ bounds: 536 456 8 8
+ - image: solid-color(68, 57, 0, 255, 8, 8)
+ bounds: 544 456 8 8
+ - image: solid-color(69, 57, 0, 255, 8, 8)
+ bounds: 552 456 8 8
+ - image: solid-color(70, 57, 0, 255, 8, 8)
+ bounds: 560 456 8 8
+ - image: solid-color(71, 57, 0, 255, 8, 8)
+ bounds: 568 456 8 8
+ - image: solid-color(72, 57, 0, 255, 8, 8)
+ bounds: 576 456 8 8
+ - image: solid-color(73, 57, 0, 255, 8, 8)
+ bounds: 584 456 8 8
+ - image: solid-color(74, 57, 0, 255, 8, 8)
+ bounds: 592 456 8 8
+ - image: solid-color(75, 57, 0, 255, 8, 8)
+ bounds: 600 456 8 8
+ - image: solid-color(76, 57, 0, 255, 8, 8)
+ bounds: 608 456 8 8
+ - image: solid-color(77, 57, 0, 255, 8, 8)
+ bounds: 616 456 8 8
+ - image: solid-color(78, 57, 0, 255, 8, 8)
+ bounds: 624 456 8 8
+ - image: solid-color(79, 57, 0, 255, 8, 8)
+ bounds: 632 456 8 8
+ - image: solid-color(80, 57, 0, 255, 8, 8)
+ bounds: 640 456 8 8
+ - image: solid-color(81, 57, 0, 255, 8, 8)
+ bounds: 648 456 8 8
+ - image: solid-color(82, 57, 0, 255, 8, 8)
+ bounds: 656 456 8 8
+ - image: solid-color(83, 57, 0, 255, 8, 8)
+ bounds: 664 456 8 8
+ - image: solid-color(84, 57, 0, 255, 8, 8)
+ bounds: 672 456 8 8
+ - image: solid-color(85, 57, 0, 255, 8, 8)
+ bounds: 680 456 8 8
+ - image: solid-color(86, 57, 0, 255, 8, 8)
+ bounds: 688 456 8 8
+ - image: solid-color(87, 57, 0, 255, 8, 8)
+ bounds: 696 456 8 8
+ - image: solid-color(88, 57, 0, 255, 8, 8)
+ bounds: 704 456 8 8
+ - image: solid-color(89, 57, 0, 255, 8, 8)
+ bounds: 712 456 8 8
+ - image: solid-color(90, 57, 0, 255, 8, 8)
+ bounds: 720 456 8 8
+ - image: solid-color(91, 57, 0, 255, 8, 8)
+ bounds: 728 456 8 8
+ - image: solid-color(92, 57, 0, 255, 8, 8)
+ bounds: 736 456 8 8
+ - image: solid-color(93, 57, 0, 255, 8, 8)
+ bounds: 744 456 8 8
+ - image: solid-color(94, 57, 0, 255, 8, 8)
+ bounds: 752 456 8 8
+ - image: solid-color(95, 57, 0, 255, 8, 8)
+ bounds: 760 456 8 8
+ - image: solid-color(96, 57, 0, 255, 8, 8)
+ bounds: 768 456 8 8
+ - image: solid-color(97, 57, 0, 255, 8, 8)
+ bounds: 776 456 8 8
+ - image: solid-color(98, 57, 0, 255, 8, 8)
+ bounds: 784 456 8 8
+ - image: solid-color(99, 57, 0, 255, 8, 8)
+ bounds: 792 456 8 8
+ - image: solid-color(100, 57, 0, 255, 8, 8)
+ bounds: 800 456 8 8
+ - image: solid-color(101, 57, 0, 255, 8, 8)
+ bounds: 808 456 8 8
+ - image: solid-color(102, 57, 0, 255, 8, 8)
+ bounds: 816 456 8 8
+ - image: solid-color(103, 57, 0, 255, 8, 8)
+ bounds: 824 456 8 8
+ - image: solid-color(104, 57, 0, 255, 8, 8)
+ bounds: 832 456 8 8
+ - image: solid-color(105, 57, 0, 255, 8, 8)
+ bounds: 840 456 8 8
+ - image: solid-color(106, 57, 0, 255, 8, 8)
+ bounds: 848 456 8 8
+ - image: solid-color(107, 57, 0, 255, 8, 8)
+ bounds: 856 456 8 8
+ - image: solid-color(108, 57, 0, 255, 8, 8)
+ bounds: 864 456 8 8
+ - image: solid-color(109, 57, 0, 255, 8, 8)
+ bounds: 872 456 8 8
+ - image: solid-color(110, 57, 0, 255, 8, 8)
+ bounds: 880 456 8 8
+ - image: solid-color(111, 57, 0, 255, 8, 8)
+ bounds: 888 456 8 8
+ - image: solid-color(112, 57, 0, 255, 8, 8)
+ bounds: 896 456 8 8
+ - image: solid-color(113, 57, 0, 255, 8, 8)
+ bounds: 904 456 8 8
+ - image: solid-color(114, 57, 0, 255, 8, 8)
+ bounds: 912 456 8 8
+ - image: solid-color(115, 57, 0, 255, 8, 8)
+ bounds: 920 456 8 8
+ - image: solid-color(116, 57, 0, 255, 8, 8)
+ bounds: 928 456 8 8
+ - image: solid-color(117, 57, 0, 255, 8, 8)
+ bounds: 936 456 8 8
+ - image: solid-color(118, 57, 0, 255, 8, 8)
+ bounds: 944 456 8 8
+ - image: solid-color(119, 57, 0, 255, 8, 8)
+ bounds: 952 456 8 8
+ - image: solid-color(120, 57, 0, 255, 8, 8)
+ bounds: 960 456 8 8
+ - image: solid-color(121, 57, 0, 255, 8, 8)
+ bounds: 968 456 8 8
+ - image: solid-color(122, 57, 0, 255, 8, 8)
+ bounds: 976 456 8 8
+ - image: solid-color(123, 57, 0, 255, 8, 8)
+ bounds: 984 456 8 8
+ - image: solid-color(124, 57, 0, 255, 8, 8)
+ bounds: 992 456 8 8
+ - image: solid-color(125, 57, 0, 255, 8, 8)
+ bounds: 1000 456 8 8
+ - image: solid-color(126, 57, 0, 255, 8, 8)
+ bounds: 1008 456 8 8
+ - image: solid-color(127, 57, 0, 255, 8, 8)
+ bounds: 1016 456 8 8
+ - image: solid-color(0, 58, 0, 255, 8, 8)
+ bounds: 0 464 8 8
+ - image: solid-color(1, 58, 0, 255, 8, 8)
+ bounds: 8 464 8 8
+ - image: solid-color(2, 58, 0, 255, 8, 8)
+ bounds: 16 464 8 8
+ - image: solid-color(3, 58, 0, 255, 8, 8)
+ bounds: 24 464 8 8
+ - image: solid-color(4, 58, 0, 255, 8, 8)
+ bounds: 32 464 8 8
+ - image: solid-color(5, 58, 0, 255, 8, 8)
+ bounds: 40 464 8 8
+ - image: solid-color(6, 58, 0, 255, 8, 8)
+ bounds: 48 464 8 8
+ - image: solid-color(7, 58, 0, 255, 8, 8)
+ bounds: 56 464 8 8
+ - image: solid-color(8, 58, 0, 255, 8, 8)
+ bounds: 64 464 8 8
+ - image: solid-color(9, 58, 0, 255, 8, 8)
+ bounds: 72 464 8 8
+ - image: solid-color(10, 58, 0, 255, 8, 8)
+ bounds: 80 464 8 8
+ - image: solid-color(11, 58, 0, 255, 8, 8)
+ bounds: 88 464 8 8
+ - image: solid-color(12, 58, 0, 255, 8, 8)
+ bounds: 96 464 8 8
+ - image: solid-color(13, 58, 0, 255, 8, 8)
+ bounds: 104 464 8 8
+ - image: solid-color(14, 58, 0, 255, 8, 8)
+ bounds: 112 464 8 8
+ - image: solid-color(15, 58, 0, 255, 8, 8)
+ bounds: 120 464 8 8
+ - image: solid-color(16, 58, 0, 255, 8, 8)
+ bounds: 128 464 8 8
+ - image: solid-color(17, 58, 0, 255, 8, 8)
+ bounds: 136 464 8 8
+ - image: solid-color(18, 58, 0, 255, 8, 8)
+ bounds: 144 464 8 8
+ - image: solid-color(19, 58, 0, 255, 8, 8)
+ bounds: 152 464 8 8
+ - image: solid-color(20, 58, 0, 255, 8, 8)
+ bounds: 160 464 8 8
+ - image: solid-color(21, 58, 0, 255, 8, 8)
+ bounds: 168 464 8 8
+ - image: solid-color(22, 58, 0, 255, 8, 8)
+ bounds: 176 464 8 8
+ - image: solid-color(23, 58, 0, 255, 8, 8)
+ bounds: 184 464 8 8
+ - image: solid-color(24, 58, 0, 255, 8, 8)
+ bounds: 192 464 8 8
+ - image: solid-color(25, 58, 0, 255, 8, 8)
+ bounds: 200 464 8 8
+ - image: solid-color(26, 58, 0, 255, 8, 8)
+ bounds: 208 464 8 8
+ - image: solid-color(27, 58, 0, 255, 8, 8)
+ bounds: 216 464 8 8
+ - image: solid-color(28, 58, 0, 255, 8, 8)
+ bounds: 224 464 8 8
+ - image: solid-color(29, 58, 0, 255, 8, 8)
+ bounds: 232 464 8 8
+ - image: solid-color(30, 58, 0, 255, 8, 8)
+ bounds: 240 464 8 8
+ - image: solid-color(31, 58, 0, 255, 8, 8)
+ bounds: 248 464 8 8
+ - image: solid-color(32, 58, 0, 255, 8, 8)
+ bounds: 256 464 8 8
+ - image: solid-color(33, 58, 0, 255, 8, 8)
+ bounds: 264 464 8 8
+ - image: solid-color(34, 58, 0, 255, 8, 8)
+ bounds: 272 464 8 8
+ - image: solid-color(35, 58, 0, 255, 8, 8)
+ bounds: 280 464 8 8
+ - image: solid-color(36, 58, 0, 255, 8, 8)
+ bounds: 288 464 8 8
+ - image: solid-color(37, 58, 0, 255, 8, 8)
+ bounds: 296 464 8 8
+ - image: solid-color(38, 58, 0, 255, 8, 8)
+ bounds: 304 464 8 8
+ - image: solid-color(39, 58, 0, 255, 8, 8)
+ bounds: 312 464 8 8
+ - image: solid-color(40, 58, 0, 255, 8, 8)
+ bounds: 320 464 8 8
+ - image: solid-color(41, 58, 0, 255, 8, 8)
+ bounds: 328 464 8 8
+ - image: solid-color(42, 58, 0, 255, 8, 8)
+ bounds: 336 464 8 8
+ - image: solid-color(43, 58, 0, 255, 8, 8)
+ bounds: 344 464 8 8
+ - image: solid-color(44, 58, 0, 255, 8, 8)
+ bounds: 352 464 8 8
+ - image: solid-color(45, 58, 0, 255, 8, 8)
+ bounds: 360 464 8 8
+ - image: solid-color(46, 58, 0, 255, 8, 8)
+ bounds: 368 464 8 8
+ - image: solid-color(47, 58, 0, 255, 8, 8)
+ bounds: 376 464 8 8
+ - image: solid-color(48, 58, 0, 255, 8, 8)
+ bounds: 384 464 8 8
+ - image: solid-color(49, 58, 0, 255, 8, 8)
+ bounds: 392 464 8 8
+ - image: solid-color(50, 58, 0, 255, 8, 8)
+ bounds: 400 464 8 8
+ - image: solid-color(51, 58, 0, 255, 8, 8)
+ bounds: 408 464 8 8
+ - image: solid-color(52, 58, 0, 255, 8, 8)
+ bounds: 416 464 8 8
+ - image: solid-color(53, 58, 0, 255, 8, 8)
+ bounds: 424 464 8 8
+ - image: solid-color(54, 58, 0, 255, 8, 8)
+ bounds: 432 464 8 8
+ - image: solid-color(55, 58, 0, 255, 8, 8)
+ bounds: 440 464 8 8
+ - image: solid-color(56, 58, 0, 255, 8, 8)
+ bounds: 448 464 8 8
+ - image: solid-color(57, 58, 0, 255, 8, 8)
+ bounds: 456 464 8 8
+ - image: solid-color(58, 58, 0, 255, 8, 8)
+ bounds: 464 464 8 8
+ - image: solid-color(59, 58, 0, 255, 8, 8)
+ bounds: 472 464 8 8
+ - image: solid-color(60, 58, 0, 255, 8, 8)
+ bounds: 480 464 8 8
+ - image: solid-color(61, 58, 0, 255, 8, 8)
+ bounds: 488 464 8 8
+ - image: solid-color(62, 58, 0, 255, 8, 8)
+ bounds: 496 464 8 8
+ - image: solid-color(63, 58, 0, 255, 8, 8)
+ bounds: 504 464 8 8
+ - image: solid-color(64, 58, 0, 255, 8, 8)
+ bounds: 512 464 8 8
+ - image: solid-color(65, 58, 0, 255, 8, 8)
+ bounds: 520 464 8 8
+ - image: solid-color(66, 58, 0, 255, 8, 8)
+ bounds: 528 464 8 8
+ - image: solid-color(67, 58, 0, 255, 8, 8)
+ bounds: 536 464 8 8
+ - image: solid-color(68, 58, 0, 255, 8, 8)
+ bounds: 544 464 8 8
+ - image: solid-color(69, 58, 0, 255, 8, 8)
+ bounds: 552 464 8 8
+ - image: solid-color(70, 58, 0, 255, 8, 8)
+ bounds: 560 464 8 8
+ - image: solid-color(71, 58, 0, 255, 8, 8)
+ bounds: 568 464 8 8
+ - image: solid-color(72, 58, 0, 255, 8, 8)
+ bounds: 576 464 8 8
+ - image: solid-color(73, 58, 0, 255, 8, 8)
+ bounds: 584 464 8 8
+ - image: solid-color(74, 58, 0, 255, 8, 8)
+ bounds: 592 464 8 8
+ - image: solid-color(75, 58, 0, 255, 8, 8)
+ bounds: 600 464 8 8
+ - image: solid-color(76, 58, 0, 255, 8, 8)
+ bounds: 608 464 8 8
+ - image: solid-color(77, 58, 0, 255, 8, 8)
+ bounds: 616 464 8 8
+ - image: solid-color(78, 58, 0, 255, 8, 8)
+ bounds: 624 464 8 8
+ - image: solid-color(79, 58, 0, 255, 8, 8)
+ bounds: 632 464 8 8
+ - image: solid-color(80, 58, 0, 255, 8, 8)
+ bounds: 640 464 8 8
+ - image: solid-color(81, 58, 0, 255, 8, 8)
+ bounds: 648 464 8 8
+ - image: solid-color(82, 58, 0, 255, 8, 8)
+ bounds: 656 464 8 8
+ - image: solid-color(83, 58, 0, 255, 8, 8)
+ bounds: 664 464 8 8
+ - image: solid-color(84, 58, 0, 255, 8, 8)
+ bounds: 672 464 8 8
+ - image: solid-color(85, 58, 0, 255, 8, 8)
+ bounds: 680 464 8 8
+ - image: solid-color(86, 58, 0, 255, 8, 8)
+ bounds: 688 464 8 8
+ - image: solid-color(87, 58, 0, 255, 8, 8)
+ bounds: 696 464 8 8
+ - image: solid-color(88, 58, 0, 255, 8, 8)
+ bounds: 704 464 8 8
+ - image: solid-color(89, 58, 0, 255, 8, 8)
+ bounds: 712 464 8 8
+ - image: solid-color(90, 58, 0, 255, 8, 8)
+ bounds: 720 464 8 8
+ - image: solid-color(91, 58, 0, 255, 8, 8)
+ bounds: 728 464 8 8
+ - image: solid-color(92, 58, 0, 255, 8, 8)
+ bounds: 736 464 8 8
+ - image: solid-color(93, 58, 0, 255, 8, 8)
+ bounds: 744 464 8 8
+ - image: solid-color(94, 58, 0, 255, 8, 8)
+ bounds: 752 464 8 8
+ - image: solid-color(95, 58, 0, 255, 8, 8)
+ bounds: 760 464 8 8
+ - image: solid-color(96, 58, 0, 255, 8, 8)
+ bounds: 768 464 8 8
+ - image: solid-color(97, 58, 0, 255, 8, 8)
+ bounds: 776 464 8 8
+ - image: solid-color(98, 58, 0, 255, 8, 8)
+ bounds: 784 464 8 8
+ - image: solid-color(99, 58, 0, 255, 8, 8)
+ bounds: 792 464 8 8
+ - image: solid-color(100, 58, 0, 255, 8, 8)
+ bounds: 800 464 8 8
+ - image: solid-color(101, 58, 0, 255, 8, 8)
+ bounds: 808 464 8 8
+ - image: solid-color(102, 58, 0, 255, 8, 8)
+ bounds: 816 464 8 8
+ - image: solid-color(103, 58, 0, 255, 8, 8)
+ bounds: 824 464 8 8
+ - image: solid-color(104, 58, 0, 255, 8, 8)
+ bounds: 832 464 8 8
+ - image: solid-color(105, 58, 0, 255, 8, 8)
+ bounds: 840 464 8 8
+ - image: solid-color(106, 58, 0, 255, 8, 8)
+ bounds: 848 464 8 8
+ - image: solid-color(107, 58, 0, 255, 8, 8)
+ bounds: 856 464 8 8
+ - image: solid-color(108, 58, 0, 255, 8, 8)
+ bounds: 864 464 8 8
+ - image: solid-color(109, 58, 0, 255, 8, 8)
+ bounds: 872 464 8 8
+ - image: solid-color(110, 58, 0, 255, 8, 8)
+ bounds: 880 464 8 8
+ - image: solid-color(111, 58, 0, 255, 8, 8)
+ bounds: 888 464 8 8
+ - image: solid-color(112, 58, 0, 255, 8, 8)
+ bounds: 896 464 8 8
+ - image: solid-color(113, 58, 0, 255, 8, 8)
+ bounds: 904 464 8 8
+ - image: solid-color(114, 58, 0, 255, 8, 8)
+ bounds: 912 464 8 8
+ - image: solid-color(115, 58, 0, 255, 8, 8)
+ bounds: 920 464 8 8
+ - image: solid-color(116, 58, 0, 255, 8, 8)
+ bounds: 928 464 8 8
+ - image: solid-color(117, 58, 0, 255, 8, 8)
+ bounds: 936 464 8 8
+ - image: solid-color(118, 58, 0, 255, 8, 8)
+ bounds: 944 464 8 8
+ - image: solid-color(119, 58, 0, 255, 8, 8)
+ bounds: 952 464 8 8
+ - image: solid-color(120, 58, 0, 255, 8, 8)
+ bounds: 960 464 8 8
+ - image: solid-color(121, 58, 0, 255, 8, 8)
+ bounds: 968 464 8 8
+ - image: solid-color(122, 58, 0, 255, 8, 8)
+ bounds: 976 464 8 8
+ - image: solid-color(123, 58, 0, 255, 8, 8)
+ bounds: 984 464 8 8
+ - image: solid-color(124, 58, 0, 255, 8, 8)
+ bounds: 992 464 8 8
+ - image: solid-color(125, 58, 0, 255, 8, 8)
+ bounds: 1000 464 8 8
+ - image: solid-color(126, 58, 0, 255, 8, 8)
+ bounds: 1008 464 8 8
+ - image: solid-color(127, 58, 0, 255, 8, 8)
+ bounds: 1016 464 8 8
+ - image: solid-color(0, 59, 0, 255, 8, 8)
+ bounds: 0 472 8 8
+ - image: solid-color(1, 59, 0, 255, 8, 8)
+ bounds: 8 472 8 8
+ - image: solid-color(2, 59, 0, 255, 8, 8)
+ bounds: 16 472 8 8
+ - image: solid-color(3, 59, 0, 255, 8, 8)
+ bounds: 24 472 8 8
+ - image: solid-color(4, 59, 0, 255, 8, 8)
+ bounds: 32 472 8 8
+ - image: solid-color(5, 59, 0, 255, 8, 8)
+ bounds: 40 472 8 8
+ - image: solid-color(6, 59, 0, 255, 8, 8)
+ bounds: 48 472 8 8
+ - image: solid-color(7, 59, 0, 255, 8, 8)
+ bounds: 56 472 8 8
+ - image: solid-color(8, 59, 0, 255, 8, 8)
+ bounds: 64 472 8 8
+ - image: solid-color(9, 59, 0, 255, 8, 8)
+ bounds: 72 472 8 8
+ - image: solid-color(10, 59, 0, 255, 8, 8)
+ bounds: 80 472 8 8
+ - image: solid-color(11, 59, 0, 255, 8, 8)
+ bounds: 88 472 8 8
+ - image: solid-color(12, 59, 0, 255, 8, 8)
+ bounds: 96 472 8 8
+ - image: solid-color(13, 59, 0, 255, 8, 8)
+ bounds: 104 472 8 8
+ - image: solid-color(14, 59, 0, 255, 8, 8)
+ bounds: 112 472 8 8
+ - image: solid-color(15, 59, 0, 255, 8, 8)
+ bounds: 120 472 8 8
+ - image: solid-color(16, 59, 0, 255, 8, 8)
+ bounds: 128 472 8 8
+ - image: solid-color(17, 59, 0, 255, 8, 8)
+ bounds: 136 472 8 8
+ - image: solid-color(18, 59, 0, 255, 8, 8)
+ bounds: 144 472 8 8
+ - image: solid-color(19, 59, 0, 255, 8, 8)
+ bounds: 152 472 8 8
+ - image: solid-color(20, 59, 0, 255, 8, 8)
+ bounds: 160 472 8 8
+ - image: solid-color(21, 59, 0, 255, 8, 8)
+ bounds: 168 472 8 8
+ - image: solid-color(22, 59, 0, 255, 8, 8)
+ bounds: 176 472 8 8
+ - image: solid-color(23, 59, 0, 255, 8, 8)
+ bounds: 184 472 8 8
+ - image: solid-color(24, 59, 0, 255, 8, 8)
+ bounds: 192 472 8 8
+ - image: solid-color(25, 59, 0, 255, 8, 8)
+ bounds: 200 472 8 8
+ - image: solid-color(26, 59, 0, 255, 8, 8)
+ bounds: 208 472 8 8
+ - image: solid-color(27, 59, 0, 255, 8, 8)
+ bounds: 216 472 8 8
+ - image: solid-color(28, 59, 0, 255, 8, 8)
+ bounds: 224 472 8 8
+ - image: solid-color(29, 59, 0, 255, 8, 8)
+ bounds: 232 472 8 8
+ - image: solid-color(30, 59, 0, 255, 8, 8)
+ bounds: 240 472 8 8
+ - image: solid-color(31, 59, 0, 255, 8, 8)
+ bounds: 248 472 8 8
+ - image: solid-color(32, 59, 0, 255, 8, 8)
+ bounds: 256 472 8 8
+ - image: solid-color(33, 59, 0, 255, 8, 8)
+ bounds: 264 472 8 8
+ - image: solid-color(34, 59, 0, 255, 8, 8)
+ bounds: 272 472 8 8
+ - image: solid-color(35, 59, 0, 255, 8, 8)
+ bounds: 280 472 8 8
+ - image: solid-color(36, 59, 0, 255, 8, 8)
+ bounds: 288 472 8 8
+ - image: solid-color(37, 59, 0, 255, 8, 8)
+ bounds: 296 472 8 8
+ - image: solid-color(38, 59, 0, 255, 8, 8)
+ bounds: 304 472 8 8
+ - image: solid-color(39, 59, 0, 255, 8, 8)
+ bounds: 312 472 8 8
+ - image: solid-color(40, 59, 0, 255, 8, 8)
+ bounds: 320 472 8 8
+ - image: solid-color(41, 59, 0, 255, 8, 8)
+ bounds: 328 472 8 8
+ - image: solid-color(42, 59, 0, 255, 8, 8)
+ bounds: 336 472 8 8
+ - image: solid-color(43, 59, 0, 255, 8, 8)
+ bounds: 344 472 8 8
+ - image: solid-color(44, 59, 0, 255, 8, 8)
+ bounds: 352 472 8 8
+ - image: solid-color(45, 59, 0, 255, 8, 8)
+ bounds: 360 472 8 8
+ - image: solid-color(46, 59, 0, 255, 8, 8)
+ bounds: 368 472 8 8
+ - image: solid-color(47, 59, 0, 255, 8, 8)
+ bounds: 376 472 8 8
+ - image: solid-color(48, 59, 0, 255, 8, 8)
+ bounds: 384 472 8 8
+ - image: solid-color(49, 59, 0, 255, 8, 8)
+ bounds: 392 472 8 8
+ - image: solid-color(50, 59, 0, 255, 8, 8)
+ bounds: 400 472 8 8
+ - image: solid-color(51, 59, 0, 255, 8, 8)
+ bounds: 408 472 8 8
+ - image: solid-color(52, 59, 0, 255, 8, 8)
+ bounds: 416 472 8 8
+ - image: solid-color(53, 59, 0, 255, 8, 8)
+ bounds: 424 472 8 8
+ - image: solid-color(54, 59, 0, 255, 8, 8)
+ bounds: 432 472 8 8
+ - image: solid-color(55, 59, 0, 255, 8, 8)
+ bounds: 440 472 8 8
+ - image: solid-color(56, 59, 0, 255, 8, 8)
+ bounds: 448 472 8 8
+ - image: solid-color(57, 59, 0, 255, 8, 8)
+ bounds: 456 472 8 8
+ - image: solid-color(58, 59, 0, 255, 8, 8)
+ bounds: 464 472 8 8
+ - image: solid-color(59, 59, 0, 255, 8, 8)
+ bounds: 472 472 8 8
+ - image: solid-color(60, 59, 0, 255, 8, 8)
+ bounds: 480 472 8 8
+ - image: solid-color(61, 59, 0, 255, 8, 8)
+ bounds: 488 472 8 8
+ - image: solid-color(62, 59, 0, 255, 8, 8)
+ bounds: 496 472 8 8
+ - image: solid-color(63, 59, 0, 255, 8, 8)
+ bounds: 504 472 8 8
+ - image: solid-color(64, 59, 0, 255, 8, 8)
+ bounds: 512 472 8 8
+ - image: solid-color(65, 59, 0, 255, 8, 8)
+ bounds: 520 472 8 8
+ - image: solid-color(66, 59, 0, 255, 8, 8)
+ bounds: 528 472 8 8
+ - image: solid-color(67, 59, 0, 255, 8, 8)
+ bounds: 536 472 8 8
+ - image: solid-color(68, 59, 0, 255, 8, 8)
+ bounds: 544 472 8 8
+ - image: solid-color(69, 59, 0, 255, 8, 8)
+ bounds: 552 472 8 8
+ - image: solid-color(70, 59, 0, 255, 8, 8)
+ bounds: 560 472 8 8
+ - image: solid-color(71, 59, 0, 255, 8, 8)
+ bounds: 568 472 8 8
+ - image: solid-color(72, 59, 0, 255, 8, 8)
+ bounds: 576 472 8 8
+ - image: solid-color(73, 59, 0, 255, 8, 8)
+ bounds: 584 472 8 8
+ - image: solid-color(74, 59, 0, 255, 8, 8)
+ bounds: 592 472 8 8
+ - image: solid-color(75, 59, 0, 255, 8, 8)
+ bounds: 600 472 8 8
+ - image: solid-color(76, 59, 0, 255, 8, 8)
+ bounds: 608 472 8 8
+ - image: solid-color(77, 59, 0, 255, 8, 8)
+ bounds: 616 472 8 8
+ - image: solid-color(78, 59, 0, 255, 8, 8)
+ bounds: 624 472 8 8
+ - image: solid-color(79, 59, 0, 255, 8, 8)
+ bounds: 632 472 8 8
+ - image: solid-color(80, 59, 0, 255, 8, 8)
+ bounds: 640 472 8 8
+ - image: solid-color(81, 59, 0, 255, 8, 8)
+ bounds: 648 472 8 8
+ - image: solid-color(82, 59, 0, 255, 8, 8)
+ bounds: 656 472 8 8
+ - image: solid-color(83, 59, 0, 255, 8, 8)
+ bounds: 664 472 8 8
+ - image: solid-color(84, 59, 0, 255, 8, 8)
+ bounds: 672 472 8 8
+ - image: solid-color(85, 59, 0, 255, 8, 8)
+ bounds: 680 472 8 8
+ - image: solid-color(86, 59, 0, 255, 8, 8)
+ bounds: 688 472 8 8
+ - image: solid-color(87, 59, 0, 255, 8, 8)
+ bounds: 696 472 8 8
+ - image: solid-color(88, 59, 0, 255, 8, 8)
+ bounds: 704 472 8 8
+ - image: solid-color(89, 59, 0, 255, 8, 8)
+ bounds: 712 472 8 8
+ - image: solid-color(90, 59, 0, 255, 8, 8)
+ bounds: 720 472 8 8
+ - image: solid-color(91, 59, 0, 255, 8, 8)
+ bounds: 728 472 8 8
+ - image: solid-color(92, 59, 0, 255, 8, 8)
+ bounds: 736 472 8 8
+ - image: solid-color(93, 59, 0, 255, 8, 8)
+ bounds: 744 472 8 8
+ - image: solid-color(94, 59, 0, 255, 8, 8)
+ bounds: 752 472 8 8
+ - image: solid-color(95, 59, 0, 255, 8, 8)
+ bounds: 760 472 8 8
+ - image: solid-color(96, 59, 0, 255, 8, 8)
+ bounds: 768 472 8 8
+ - image: solid-color(97, 59, 0, 255, 8, 8)
+ bounds: 776 472 8 8
+ - image: solid-color(98, 59, 0, 255, 8, 8)
+ bounds: 784 472 8 8
+ - image: solid-color(99, 59, 0, 255, 8, 8)
+ bounds: 792 472 8 8
+ - image: solid-color(100, 59, 0, 255, 8, 8)
+ bounds: 800 472 8 8
+ - image: solid-color(101, 59, 0, 255, 8, 8)
+ bounds: 808 472 8 8
+ - image: solid-color(102, 59, 0, 255, 8, 8)
+ bounds: 816 472 8 8
+ - image: solid-color(103, 59, 0, 255, 8, 8)
+ bounds: 824 472 8 8
+ - image: solid-color(104, 59, 0, 255, 8, 8)
+ bounds: 832 472 8 8
+ - image: solid-color(105, 59, 0, 255, 8, 8)
+ bounds: 840 472 8 8
+ - image: solid-color(106, 59, 0, 255, 8, 8)
+ bounds: 848 472 8 8
+ - image: solid-color(107, 59, 0, 255, 8, 8)
+ bounds: 856 472 8 8
+ - image: solid-color(108, 59, 0, 255, 8, 8)
+ bounds: 864 472 8 8
+ - image: solid-color(109, 59, 0, 255, 8, 8)
+ bounds: 872 472 8 8
+ - image: solid-color(110, 59, 0, 255, 8, 8)
+ bounds: 880 472 8 8
+ - image: solid-color(111, 59, 0, 255, 8, 8)
+ bounds: 888 472 8 8
+ - image: solid-color(112, 59, 0, 255, 8, 8)
+ bounds: 896 472 8 8
+ - image: solid-color(113, 59, 0, 255, 8, 8)
+ bounds: 904 472 8 8
+ - image: solid-color(114, 59, 0, 255, 8, 8)
+ bounds: 912 472 8 8
+ - image: solid-color(115, 59, 0, 255, 8, 8)
+ bounds: 920 472 8 8
+ - image: solid-color(116, 59, 0, 255, 8, 8)
+ bounds: 928 472 8 8
+ - image: solid-color(117, 59, 0, 255, 8, 8)
+ bounds: 936 472 8 8
+ - image: solid-color(118, 59, 0, 255, 8, 8)
+ bounds: 944 472 8 8
+ - image: solid-color(119, 59, 0, 255, 8, 8)
+ bounds: 952 472 8 8
+ - image: solid-color(120, 59, 0, 255, 8, 8)
+ bounds: 960 472 8 8
+ - image: solid-color(121, 59, 0, 255, 8, 8)
+ bounds: 968 472 8 8
+ - image: solid-color(122, 59, 0, 255, 8, 8)
+ bounds: 976 472 8 8
+ - image: solid-color(123, 59, 0, 255, 8, 8)
+ bounds: 984 472 8 8
+ - image: solid-color(124, 59, 0, 255, 8, 8)
+ bounds: 992 472 8 8
+ - image: solid-color(125, 59, 0, 255, 8, 8)
+ bounds: 1000 472 8 8
+ - image: solid-color(126, 59, 0, 255, 8, 8)
+ bounds: 1008 472 8 8
+ - image: solid-color(127, 59, 0, 255, 8, 8)
+ bounds: 1016 472 8 8
+ - image: solid-color(0, 60, 0, 255, 8, 8)
+ bounds: 0 480 8 8
+ - image: solid-color(1, 60, 0, 255, 8, 8)
+ bounds: 8 480 8 8
+ - image: solid-color(2, 60, 0, 255, 8, 8)
+ bounds: 16 480 8 8
+ - image: solid-color(3, 60, 0, 255, 8, 8)
+ bounds: 24 480 8 8
+ - image: solid-color(4, 60, 0, 255, 8, 8)
+ bounds: 32 480 8 8
+ - image: solid-color(5, 60, 0, 255, 8, 8)
+ bounds: 40 480 8 8
+ - image: solid-color(6, 60, 0, 255, 8, 8)
+ bounds: 48 480 8 8
+ - image: solid-color(7, 60, 0, 255, 8, 8)
+ bounds: 56 480 8 8
+ - image: solid-color(8, 60, 0, 255, 8, 8)
+ bounds: 64 480 8 8
+ - image: solid-color(9, 60, 0, 255, 8, 8)
+ bounds: 72 480 8 8
+ - image: solid-color(10, 60, 0, 255, 8, 8)
+ bounds: 80 480 8 8
+ - image: solid-color(11, 60, 0, 255, 8, 8)
+ bounds: 88 480 8 8
+ - image: solid-color(12, 60, 0, 255, 8, 8)
+ bounds: 96 480 8 8
+ - image: solid-color(13, 60, 0, 255, 8, 8)
+ bounds: 104 480 8 8
+ - image: solid-color(14, 60, 0, 255, 8, 8)
+ bounds: 112 480 8 8
+ - image: solid-color(15, 60, 0, 255, 8, 8)
+ bounds: 120 480 8 8
+ - image: solid-color(16, 60, 0, 255, 8, 8)
+ bounds: 128 480 8 8
+ - image: solid-color(17, 60, 0, 255, 8, 8)
+ bounds: 136 480 8 8
+ - image: solid-color(18, 60, 0, 255, 8, 8)
+ bounds: 144 480 8 8
+ - image: solid-color(19, 60, 0, 255, 8, 8)
+ bounds: 152 480 8 8
+ - image: solid-color(20, 60, 0, 255, 8, 8)
+ bounds: 160 480 8 8
+ - image: solid-color(21, 60, 0, 255, 8, 8)
+ bounds: 168 480 8 8
+ - image: solid-color(22, 60, 0, 255, 8, 8)
+ bounds: 176 480 8 8
+ - image: solid-color(23, 60, 0, 255, 8, 8)
+ bounds: 184 480 8 8
+ - image: solid-color(24, 60, 0, 255, 8, 8)
+ bounds: 192 480 8 8
+ - image: solid-color(25, 60, 0, 255, 8, 8)
+ bounds: 200 480 8 8
+ - image: solid-color(26, 60, 0, 255, 8, 8)
+ bounds: 208 480 8 8
+ - image: solid-color(27, 60, 0, 255, 8, 8)
+ bounds: 216 480 8 8
+ - image: solid-color(28, 60, 0, 255, 8, 8)
+ bounds: 224 480 8 8
+ - image: solid-color(29, 60, 0, 255, 8, 8)
+ bounds: 232 480 8 8
+ - image: solid-color(30, 60, 0, 255, 8, 8)
+ bounds: 240 480 8 8
+ - image: solid-color(31, 60, 0, 255, 8, 8)
+ bounds: 248 480 8 8
+ - image: solid-color(32, 60, 0, 255, 8, 8)
+ bounds: 256 480 8 8
+ - image: solid-color(33, 60, 0, 255, 8, 8)
+ bounds: 264 480 8 8
+ - image: solid-color(34, 60, 0, 255, 8, 8)
+ bounds: 272 480 8 8
+ - image: solid-color(35, 60, 0, 255, 8, 8)
+ bounds: 280 480 8 8
+ - image: solid-color(36, 60, 0, 255, 8, 8)
+ bounds: 288 480 8 8
+ - image: solid-color(37, 60, 0, 255, 8, 8)
+ bounds: 296 480 8 8
+ - image: solid-color(38, 60, 0, 255, 8, 8)
+ bounds: 304 480 8 8
+ - image: solid-color(39, 60, 0, 255, 8, 8)
+ bounds: 312 480 8 8
+ - image: solid-color(40, 60, 0, 255, 8, 8)
+ bounds: 320 480 8 8
+ - image: solid-color(41, 60, 0, 255, 8, 8)
+ bounds: 328 480 8 8
+ - image: solid-color(42, 60, 0, 255, 8, 8)
+ bounds: 336 480 8 8
+ - image: solid-color(43, 60, 0, 255, 8, 8)
+ bounds: 344 480 8 8
+ - image: solid-color(44, 60, 0, 255, 8, 8)
+ bounds: 352 480 8 8
+ - image: solid-color(45, 60, 0, 255, 8, 8)
+ bounds: 360 480 8 8
+ - image: solid-color(46, 60, 0, 255, 8, 8)
+ bounds: 368 480 8 8
+ - image: solid-color(47, 60, 0, 255, 8, 8)
+ bounds: 376 480 8 8
+ - image: solid-color(48, 60, 0, 255, 8, 8)
+ bounds: 384 480 8 8
+ - image: solid-color(49, 60, 0, 255, 8, 8)
+ bounds: 392 480 8 8
+ - image: solid-color(50, 60, 0, 255, 8, 8)
+ bounds: 400 480 8 8
+ - image: solid-color(51, 60, 0, 255, 8, 8)
+ bounds: 408 480 8 8
+ - image: solid-color(52, 60, 0, 255, 8, 8)
+ bounds: 416 480 8 8
+ - image: solid-color(53, 60, 0, 255, 8, 8)
+ bounds: 424 480 8 8
+ - image: solid-color(54, 60, 0, 255, 8, 8)
+ bounds: 432 480 8 8
+ - image: solid-color(55, 60, 0, 255, 8, 8)
+ bounds: 440 480 8 8
+ - image: solid-color(56, 60, 0, 255, 8, 8)
+ bounds: 448 480 8 8
+ - image: solid-color(57, 60, 0, 255, 8, 8)
+ bounds: 456 480 8 8
+ - image: solid-color(58, 60, 0, 255, 8, 8)
+ bounds: 464 480 8 8
+ - image: solid-color(59, 60, 0, 255, 8, 8)
+ bounds: 472 480 8 8
+ - image: solid-color(60, 60, 0, 255, 8, 8)
+ bounds: 480 480 8 8
+ - image: solid-color(61, 60, 0, 255, 8, 8)
+ bounds: 488 480 8 8
+ - image: solid-color(62, 60, 0, 255, 8, 8)
+ bounds: 496 480 8 8
+ - image: solid-color(63, 60, 0, 255, 8, 8)
+ bounds: 504 480 8 8
+ - image: solid-color(64, 60, 0, 255, 8, 8)
+ bounds: 512 480 8 8
+ - image: solid-color(65, 60, 0, 255, 8, 8)
+ bounds: 520 480 8 8
+ - image: solid-color(66, 60, 0, 255, 8, 8)
+ bounds: 528 480 8 8
+ - image: solid-color(67, 60, 0, 255, 8, 8)
+ bounds: 536 480 8 8
+ - image: solid-color(68, 60, 0, 255, 8, 8)
+ bounds: 544 480 8 8
+ - image: solid-color(69, 60, 0, 255, 8, 8)
+ bounds: 552 480 8 8
+ - image: solid-color(70, 60, 0, 255, 8, 8)
+ bounds: 560 480 8 8
+ - image: solid-color(71, 60, 0, 255, 8, 8)
+ bounds: 568 480 8 8
+ - image: solid-color(72, 60, 0, 255, 8, 8)
+ bounds: 576 480 8 8
+ - image: solid-color(73, 60, 0, 255, 8, 8)
+ bounds: 584 480 8 8
+ - image: solid-color(74, 60, 0, 255, 8, 8)
+ bounds: 592 480 8 8
+ - image: solid-color(75, 60, 0, 255, 8, 8)
+ bounds: 600 480 8 8
+ - image: solid-color(76, 60, 0, 255, 8, 8)
+ bounds: 608 480 8 8
+ - image: solid-color(77, 60, 0, 255, 8, 8)
+ bounds: 616 480 8 8
+ - image: solid-color(78, 60, 0, 255, 8, 8)
+ bounds: 624 480 8 8
+ - image: solid-color(79, 60, 0, 255, 8, 8)
+ bounds: 632 480 8 8
+ - image: solid-color(80, 60, 0, 255, 8, 8)
+ bounds: 640 480 8 8
+ - image: solid-color(81, 60, 0, 255, 8, 8)
+ bounds: 648 480 8 8
+ - image: solid-color(82, 60, 0, 255, 8, 8)
+ bounds: 656 480 8 8
+ - image: solid-color(83, 60, 0, 255, 8, 8)
+ bounds: 664 480 8 8
+ - image: solid-color(84, 60, 0, 255, 8, 8)
+ bounds: 672 480 8 8
+ - image: solid-color(85, 60, 0, 255, 8, 8)
+ bounds: 680 480 8 8
+ - image: solid-color(86, 60, 0, 255, 8, 8)
+ bounds: 688 480 8 8
+ - image: solid-color(87, 60, 0, 255, 8, 8)
+ bounds: 696 480 8 8
+ - image: solid-color(88, 60, 0, 255, 8, 8)
+ bounds: 704 480 8 8
+ - image: solid-color(89, 60, 0, 255, 8, 8)
+ bounds: 712 480 8 8
+ - image: solid-color(90, 60, 0, 255, 8, 8)
+ bounds: 720 480 8 8
+ - image: solid-color(91, 60, 0, 255, 8, 8)
+ bounds: 728 480 8 8
+ - image: solid-color(92, 60, 0, 255, 8, 8)
+ bounds: 736 480 8 8
+ - image: solid-color(93, 60, 0, 255, 8, 8)
+ bounds: 744 480 8 8
+ - image: solid-color(94, 60, 0, 255, 8, 8)
+ bounds: 752 480 8 8
+ - image: solid-color(95, 60, 0, 255, 8, 8)
+ bounds: 760 480 8 8
+ - image: solid-color(96, 60, 0, 255, 8, 8)
+ bounds: 768 480 8 8
+ - image: solid-color(97, 60, 0, 255, 8, 8)
+ bounds: 776 480 8 8
+ - image: solid-color(98, 60, 0, 255, 8, 8)
+ bounds: 784 480 8 8
+ - image: solid-color(99, 60, 0, 255, 8, 8)
+ bounds: 792 480 8 8
+ - image: solid-color(100, 60, 0, 255, 8, 8)
+ bounds: 800 480 8 8
+ - image: solid-color(101, 60, 0, 255, 8, 8)
+ bounds: 808 480 8 8
+ - image: solid-color(102, 60, 0, 255, 8, 8)
+ bounds: 816 480 8 8
+ - image: solid-color(103, 60, 0, 255, 8, 8)
+ bounds: 824 480 8 8
+ - image: solid-color(104, 60, 0, 255, 8, 8)
+ bounds: 832 480 8 8
+ - image: solid-color(105, 60, 0, 255, 8, 8)
+ bounds: 840 480 8 8
+ - image: solid-color(106, 60, 0, 255, 8, 8)
+ bounds: 848 480 8 8
+ - image: solid-color(107, 60, 0, 255, 8, 8)
+ bounds: 856 480 8 8
+ - image: solid-color(108, 60, 0, 255, 8, 8)
+ bounds: 864 480 8 8
+ - image: solid-color(109, 60, 0, 255, 8, 8)
+ bounds: 872 480 8 8
+ - image: solid-color(110, 60, 0, 255, 8, 8)
+ bounds: 880 480 8 8
+ - image: solid-color(111, 60, 0, 255, 8, 8)
+ bounds: 888 480 8 8
+ - image: solid-color(112, 60, 0, 255, 8, 8)
+ bounds: 896 480 8 8
+ - image: solid-color(113, 60, 0, 255, 8, 8)
+ bounds: 904 480 8 8
+ - image: solid-color(114, 60, 0, 255, 8, 8)
+ bounds: 912 480 8 8
+ - image: solid-color(115, 60, 0, 255, 8, 8)
+ bounds: 920 480 8 8
+ - image: solid-color(116, 60, 0, 255, 8, 8)
+ bounds: 928 480 8 8
+ - image: solid-color(117, 60, 0, 255, 8, 8)
+ bounds: 936 480 8 8
+ - image: solid-color(118, 60, 0, 255, 8, 8)
+ bounds: 944 480 8 8
+ - image: solid-color(119, 60, 0, 255, 8, 8)
+ bounds: 952 480 8 8
+ - image: solid-color(120, 60, 0, 255, 8, 8)
+ bounds: 960 480 8 8
+ - image: solid-color(121, 60, 0, 255, 8, 8)
+ bounds: 968 480 8 8
+ - image: solid-color(122, 60, 0, 255, 8, 8)
+ bounds: 976 480 8 8
+ - image: solid-color(123, 60, 0, 255, 8, 8)
+ bounds: 984 480 8 8
+ - image: solid-color(124, 60, 0, 255, 8, 8)
+ bounds: 992 480 8 8
+ - image: solid-color(125, 60, 0, 255, 8, 8)
+ bounds: 1000 480 8 8
+ - image: solid-color(126, 60, 0, 255, 8, 8)
+ bounds: 1008 480 8 8
+ - image: solid-color(127, 60, 0, 255, 8, 8)
+ bounds: 1016 480 8 8
+ - image: solid-color(0, 61, 0, 255, 8, 8)
+ bounds: 0 488 8 8
+ - image: solid-color(1, 61, 0, 255, 8, 8)
+ bounds: 8 488 8 8
+ - image: solid-color(2, 61, 0, 255, 8, 8)
+ bounds: 16 488 8 8
+ - image: solid-color(3, 61, 0, 255, 8, 8)
+ bounds: 24 488 8 8
+ - image: solid-color(4, 61, 0, 255, 8, 8)
+ bounds: 32 488 8 8
+ - image: solid-color(5, 61, 0, 255, 8, 8)
+ bounds: 40 488 8 8
+ - image: solid-color(6, 61, 0, 255, 8, 8)
+ bounds: 48 488 8 8
+ - image: solid-color(7, 61, 0, 255, 8, 8)
+ bounds: 56 488 8 8
+ - image: solid-color(8, 61, 0, 255, 8, 8)
+ bounds: 64 488 8 8
+ - image: solid-color(9, 61, 0, 255, 8, 8)
+ bounds: 72 488 8 8
+ - image: solid-color(10, 61, 0, 255, 8, 8)
+ bounds: 80 488 8 8
+ - image: solid-color(11, 61, 0, 255, 8, 8)
+ bounds: 88 488 8 8
+ - image: solid-color(12, 61, 0, 255, 8, 8)
+ bounds: 96 488 8 8
+ - image: solid-color(13, 61, 0, 255, 8, 8)
+ bounds: 104 488 8 8
+ - image: solid-color(14, 61, 0, 255, 8, 8)
+ bounds: 112 488 8 8
+ - image: solid-color(15, 61, 0, 255, 8, 8)
+ bounds: 120 488 8 8
+ - image: solid-color(16, 61, 0, 255, 8, 8)
+ bounds: 128 488 8 8
+ - image: solid-color(17, 61, 0, 255, 8, 8)
+ bounds: 136 488 8 8
+ - image: solid-color(18, 61, 0, 255, 8, 8)
+ bounds: 144 488 8 8
+ - image: solid-color(19, 61, 0, 255, 8, 8)
+ bounds: 152 488 8 8
+ - image: solid-color(20, 61, 0, 255, 8, 8)
+ bounds: 160 488 8 8
+ - image: solid-color(21, 61, 0, 255, 8, 8)
+ bounds: 168 488 8 8
+ - image: solid-color(22, 61, 0, 255, 8, 8)
+ bounds: 176 488 8 8
+ - image: solid-color(23, 61, 0, 255, 8, 8)
+ bounds: 184 488 8 8
+ - image: solid-color(24, 61, 0, 255, 8, 8)
+ bounds: 192 488 8 8
+ - image: solid-color(25, 61, 0, 255, 8, 8)
+ bounds: 200 488 8 8
+ - image: solid-color(26, 61, 0, 255, 8, 8)
+ bounds: 208 488 8 8
+ - image: solid-color(27, 61, 0, 255, 8, 8)
+ bounds: 216 488 8 8
+ - image: solid-color(28, 61, 0, 255, 8, 8)
+ bounds: 224 488 8 8
+ - image: solid-color(29, 61, 0, 255, 8, 8)
+ bounds: 232 488 8 8
+ - image: solid-color(30, 61, 0, 255, 8, 8)
+ bounds: 240 488 8 8
+ - image: solid-color(31, 61, 0, 255, 8, 8)
+ bounds: 248 488 8 8
+ - image: solid-color(32, 61, 0, 255, 8, 8)
+ bounds: 256 488 8 8
+ - image: solid-color(33, 61, 0, 255, 8, 8)
+ bounds: 264 488 8 8
+ - image: solid-color(34, 61, 0, 255, 8, 8)
+ bounds: 272 488 8 8
+ - image: solid-color(35, 61, 0, 255, 8, 8)
+ bounds: 280 488 8 8
+ - image: solid-color(36, 61, 0, 255, 8, 8)
+ bounds: 288 488 8 8
+ - image: solid-color(37, 61, 0, 255, 8, 8)
+ bounds: 296 488 8 8
+ - image: solid-color(38, 61, 0, 255, 8, 8)
+ bounds: 304 488 8 8
+ - image: solid-color(39, 61, 0, 255, 8, 8)
+ bounds: 312 488 8 8
+ - image: solid-color(40, 61, 0, 255, 8, 8)
+ bounds: 320 488 8 8
+ - image: solid-color(41, 61, 0, 255, 8, 8)
+ bounds: 328 488 8 8
+ - image: solid-color(42, 61, 0, 255, 8, 8)
+ bounds: 336 488 8 8
+ - image: solid-color(43, 61, 0, 255, 8, 8)
+ bounds: 344 488 8 8
+ - image: solid-color(44, 61, 0, 255, 8, 8)
+ bounds: 352 488 8 8
+ - image: solid-color(45, 61, 0, 255, 8, 8)
+ bounds: 360 488 8 8
+ - image: solid-color(46, 61, 0, 255, 8, 8)
+ bounds: 368 488 8 8
+ - image: solid-color(47, 61, 0, 255, 8, 8)
+ bounds: 376 488 8 8
+ - image: solid-color(48, 61, 0, 255, 8, 8)
+ bounds: 384 488 8 8
+ - image: solid-color(49, 61, 0, 255, 8, 8)
+ bounds: 392 488 8 8
+ - image: solid-color(50, 61, 0, 255, 8, 8)
+ bounds: 400 488 8 8
+ - image: solid-color(51, 61, 0, 255, 8, 8)
+ bounds: 408 488 8 8
+ - image: solid-color(52, 61, 0, 255, 8, 8)
+ bounds: 416 488 8 8
+ - image: solid-color(53, 61, 0, 255, 8, 8)
+ bounds: 424 488 8 8
+ - image: solid-color(54, 61, 0, 255, 8, 8)
+ bounds: 432 488 8 8
+ - image: solid-color(55, 61, 0, 255, 8, 8)
+ bounds: 440 488 8 8
+ - image: solid-color(56, 61, 0, 255, 8, 8)
+ bounds: 448 488 8 8
+ - image: solid-color(57, 61, 0, 255, 8, 8)
+ bounds: 456 488 8 8
+ - image: solid-color(58, 61, 0, 255, 8, 8)
+ bounds: 464 488 8 8
+ - image: solid-color(59, 61, 0, 255, 8, 8)
+ bounds: 472 488 8 8
+ - image: solid-color(60, 61, 0, 255, 8, 8)
+ bounds: 480 488 8 8
+ - image: solid-color(61, 61, 0, 255, 8, 8)
+ bounds: 488 488 8 8
+ - image: solid-color(62, 61, 0, 255, 8, 8)
+ bounds: 496 488 8 8
+ - image: solid-color(63, 61, 0, 255, 8, 8)
+ bounds: 504 488 8 8
+ - image: solid-color(64, 61, 0, 255, 8, 8)
+ bounds: 512 488 8 8
+ - image: solid-color(65, 61, 0, 255, 8, 8)
+ bounds: 520 488 8 8
+ - image: solid-color(66, 61, 0, 255, 8, 8)
+ bounds: 528 488 8 8
+ - image: solid-color(67, 61, 0, 255, 8, 8)
+ bounds: 536 488 8 8
+ - image: solid-color(68, 61, 0, 255, 8, 8)
+ bounds: 544 488 8 8
+ - image: solid-color(69, 61, 0, 255, 8, 8)
+ bounds: 552 488 8 8
+ - image: solid-color(70, 61, 0, 255, 8, 8)
+ bounds: 560 488 8 8
+ - image: solid-color(71, 61, 0, 255, 8, 8)
+ bounds: 568 488 8 8
+ - image: solid-color(72, 61, 0, 255, 8, 8)
+ bounds: 576 488 8 8
+ - image: solid-color(73, 61, 0, 255, 8, 8)
+ bounds: 584 488 8 8
+ - image: solid-color(74, 61, 0, 255, 8, 8)
+ bounds: 592 488 8 8
+ - image: solid-color(75, 61, 0, 255, 8, 8)
+ bounds: 600 488 8 8
+ - image: solid-color(76, 61, 0, 255, 8, 8)
+ bounds: 608 488 8 8
+ - image: solid-color(77, 61, 0, 255, 8, 8)
+ bounds: 616 488 8 8
+ - image: solid-color(78, 61, 0, 255, 8, 8)
+ bounds: 624 488 8 8
+ - image: solid-color(79, 61, 0, 255, 8, 8)
+ bounds: 632 488 8 8
+ - image: solid-color(80, 61, 0, 255, 8, 8)
+ bounds: 640 488 8 8
+ - image: solid-color(81, 61, 0, 255, 8, 8)
+ bounds: 648 488 8 8
+ - image: solid-color(82, 61, 0, 255, 8, 8)
+ bounds: 656 488 8 8
+ - image: solid-color(83, 61, 0, 255, 8, 8)
+ bounds: 664 488 8 8
+ - image: solid-color(84, 61, 0, 255, 8, 8)
+ bounds: 672 488 8 8
+ - image: solid-color(85, 61, 0, 255, 8, 8)
+ bounds: 680 488 8 8
+ - image: solid-color(86, 61, 0, 255, 8, 8)
+ bounds: 688 488 8 8
+ - image: solid-color(87, 61, 0, 255, 8, 8)
+ bounds: 696 488 8 8
+ - image: solid-color(88, 61, 0, 255, 8, 8)
+ bounds: 704 488 8 8
+ - image: solid-color(89, 61, 0, 255, 8, 8)
+ bounds: 712 488 8 8
+ - image: solid-color(90, 61, 0, 255, 8, 8)
+ bounds: 720 488 8 8
+ - image: solid-color(91, 61, 0, 255, 8, 8)
+ bounds: 728 488 8 8
+ - image: solid-color(92, 61, 0, 255, 8, 8)
+ bounds: 736 488 8 8
+ - image: solid-color(93, 61, 0, 255, 8, 8)
+ bounds: 744 488 8 8
+ - image: solid-color(94, 61, 0, 255, 8, 8)
+ bounds: 752 488 8 8
+ - image: solid-color(95, 61, 0, 255, 8, 8)
+ bounds: 760 488 8 8
+ - image: solid-color(96, 61, 0, 255, 8, 8)
+ bounds: 768 488 8 8
+ - image: solid-color(97, 61, 0, 255, 8, 8)
+ bounds: 776 488 8 8
+ - image: solid-color(98, 61, 0, 255, 8, 8)
+ bounds: 784 488 8 8
+ - image: solid-color(99, 61, 0, 255, 8, 8)
+ bounds: 792 488 8 8
+ - image: solid-color(100, 61, 0, 255, 8, 8)
+ bounds: 800 488 8 8
+ - image: solid-color(101, 61, 0, 255, 8, 8)
+ bounds: 808 488 8 8
+ - image: solid-color(102, 61, 0, 255, 8, 8)
+ bounds: 816 488 8 8
+ - image: solid-color(103, 61, 0, 255, 8, 8)
+ bounds: 824 488 8 8
+ - image: solid-color(104, 61, 0, 255, 8, 8)
+ bounds: 832 488 8 8
+ - image: solid-color(105, 61, 0, 255, 8, 8)
+ bounds: 840 488 8 8
+ - image: solid-color(106, 61, 0, 255, 8, 8)
+ bounds: 848 488 8 8
+ - image: solid-color(107, 61, 0, 255, 8, 8)
+ bounds: 856 488 8 8
+ - image: solid-color(108, 61, 0, 255, 8, 8)
+ bounds: 864 488 8 8
+ - image: solid-color(109, 61, 0, 255, 8, 8)
+ bounds: 872 488 8 8
+ - image: solid-color(110, 61, 0, 255, 8, 8)
+ bounds: 880 488 8 8
+ - image: solid-color(111, 61, 0, 255, 8, 8)
+ bounds: 888 488 8 8
+ - image: solid-color(112, 61, 0, 255, 8, 8)
+ bounds: 896 488 8 8
+ - image: solid-color(113, 61, 0, 255, 8, 8)
+ bounds: 904 488 8 8
+ - image: solid-color(114, 61, 0, 255, 8, 8)
+ bounds: 912 488 8 8
+ - image: solid-color(115, 61, 0, 255, 8, 8)
+ bounds: 920 488 8 8
+ - image: solid-color(116, 61, 0, 255, 8, 8)
+ bounds: 928 488 8 8
+ - image: solid-color(117, 61, 0, 255, 8, 8)
+ bounds: 936 488 8 8
+ - image: solid-color(118, 61, 0, 255, 8, 8)
+ bounds: 944 488 8 8
+ - image: solid-color(119, 61, 0, 255, 8, 8)
+ bounds: 952 488 8 8
+ - image: solid-color(120, 61, 0, 255, 8, 8)
+ bounds: 960 488 8 8
+ - image: solid-color(121, 61, 0, 255, 8, 8)
+ bounds: 968 488 8 8
+ - image: solid-color(122, 61, 0, 255, 8, 8)
+ bounds: 976 488 8 8
+ - image: solid-color(123, 61, 0, 255, 8, 8)
+ bounds: 984 488 8 8
+ - image: solid-color(124, 61, 0, 255, 8, 8)
+ bounds: 992 488 8 8
+ - image: solid-color(125, 61, 0, 255, 8, 8)
+ bounds: 1000 488 8 8
+ - image: solid-color(126, 61, 0, 255, 8, 8)
+ bounds: 1008 488 8 8
+ - image: solid-color(127, 61, 0, 255, 8, 8)
+ bounds: 1016 488 8 8
+ - image: solid-color(0, 62, 0, 255, 8, 8)
+ bounds: 0 496 8 8
+ - image: solid-color(1, 62, 0, 255, 8, 8)
+ bounds: 8 496 8 8
+ - image: solid-color(2, 62, 0, 255, 8, 8)
+ bounds: 16 496 8 8
+ - image: solid-color(3, 62, 0, 255, 8, 8)
+ bounds: 24 496 8 8
+ - image: solid-color(4, 62, 0, 255, 8, 8)
+ bounds: 32 496 8 8
+ - image: solid-color(5, 62, 0, 255, 8, 8)
+ bounds: 40 496 8 8
+ - image: solid-color(6, 62, 0, 255, 8, 8)
+ bounds: 48 496 8 8
+ - image: solid-color(7, 62, 0, 255, 8, 8)
+ bounds: 56 496 8 8
+ - image: solid-color(8, 62, 0, 255, 8, 8)
+ bounds: 64 496 8 8
+ - image: solid-color(9, 62, 0, 255, 8, 8)
+ bounds: 72 496 8 8
+ - image: solid-color(10, 62, 0, 255, 8, 8)
+ bounds: 80 496 8 8
+ - image: solid-color(11, 62, 0, 255, 8, 8)
+ bounds: 88 496 8 8
+ - image: solid-color(12, 62, 0, 255, 8, 8)
+ bounds: 96 496 8 8
+ - image: solid-color(13, 62, 0, 255, 8, 8)
+ bounds: 104 496 8 8
+ - image: solid-color(14, 62, 0, 255, 8, 8)
+ bounds: 112 496 8 8
+ - image: solid-color(15, 62, 0, 255, 8, 8)
+ bounds: 120 496 8 8
+ - image: solid-color(16, 62, 0, 255, 8, 8)
+ bounds: 128 496 8 8
+ - image: solid-color(17, 62, 0, 255, 8, 8)
+ bounds: 136 496 8 8
+ - image: solid-color(18, 62, 0, 255, 8, 8)
+ bounds: 144 496 8 8
+ - image: solid-color(19, 62, 0, 255, 8, 8)
+ bounds: 152 496 8 8
+ - image: solid-color(20, 62, 0, 255, 8, 8)
+ bounds: 160 496 8 8
+ - image: solid-color(21, 62, 0, 255, 8, 8)
+ bounds: 168 496 8 8
+ - image: solid-color(22, 62, 0, 255, 8, 8)
+ bounds: 176 496 8 8
+ - image: solid-color(23, 62, 0, 255, 8, 8)
+ bounds: 184 496 8 8
+ - image: solid-color(24, 62, 0, 255, 8, 8)
+ bounds: 192 496 8 8
+ - image: solid-color(25, 62, 0, 255, 8, 8)
+ bounds: 200 496 8 8
+ - image: solid-color(26, 62, 0, 255, 8, 8)
+ bounds: 208 496 8 8
+ - image: solid-color(27, 62, 0, 255, 8, 8)
+ bounds: 216 496 8 8
+ - image: solid-color(28, 62, 0, 255, 8, 8)
+ bounds: 224 496 8 8
+ - image: solid-color(29, 62, 0, 255, 8, 8)
+ bounds: 232 496 8 8
+ - image: solid-color(30, 62, 0, 255, 8, 8)
+ bounds: 240 496 8 8
+ - image: solid-color(31, 62, 0, 255, 8, 8)
+ bounds: 248 496 8 8
+ - image: solid-color(32, 62, 0, 255, 8, 8)
+ bounds: 256 496 8 8
+ - image: solid-color(33, 62, 0, 255, 8, 8)
+ bounds: 264 496 8 8
+ - image: solid-color(34, 62, 0, 255, 8, 8)
+ bounds: 272 496 8 8
+ - image: solid-color(35, 62, 0, 255, 8, 8)
+ bounds: 280 496 8 8
+ - image: solid-color(36, 62, 0, 255, 8, 8)
+ bounds: 288 496 8 8
+ - image: solid-color(37, 62, 0, 255, 8, 8)
+ bounds: 296 496 8 8
+ - image: solid-color(38, 62, 0, 255, 8, 8)
+ bounds: 304 496 8 8
+ - image: solid-color(39, 62, 0, 255, 8, 8)
+ bounds: 312 496 8 8
+ - image: solid-color(40, 62, 0, 255, 8, 8)
+ bounds: 320 496 8 8
+ - image: solid-color(41, 62, 0, 255, 8, 8)
+ bounds: 328 496 8 8
+ - image: solid-color(42, 62, 0, 255, 8, 8)
+ bounds: 336 496 8 8
+ - image: solid-color(43, 62, 0, 255, 8, 8)
+ bounds: 344 496 8 8
+ - image: solid-color(44, 62, 0, 255, 8, 8)
+ bounds: 352 496 8 8
+ - image: solid-color(45, 62, 0, 255, 8, 8)
+ bounds: 360 496 8 8
+ - image: solid-color(46, 62, 0, 255, 8, 8)
+ bounds: 368 496 8 8
+ - image: solid-color(47, 62, 0, 255, 8, 8)
+ bounds: 376 496 8 8
+ - image: solid-color(48, 62, 0, 255, 8, 8)
+ bounds: 384 496 8 8
+ - image: solid-color(49, 62, 0, 255, 8, 8)
+ bounds: 392 496 8 8
+ - image: solid-color(50, 62, 0, 255, 8, 8)
+ bounds: 400 496 8 8
+ - image: solid-color(51, 62, 0, 255, 8, 8)
+ bounds: 408 496 8 8
+ - image: solid-color(52, 62, 0, 255, 8, 8)
+ bounds: 416 496 8 8
+ - image: solid-color(53, 62, 0, 255, 8, 8)
+ bounds: 424 496 8 8
+ - image: solid-color(54, 62, 0, 255, 8, 8)
+ bounds: 432 496 8 8
+ - image: solid-color(55, 62, 0, 255, 8, 8)
+ bounds: 440 496 8 8
+ - image: solid-color(56, 62, 0, 255, 8, 8)
+ bounds: 448 496 8 8
+ - image: solid-color(57, 62, 0, 255, 8, 8)
+ bounds: 456 496 8 8
+ - image: solid-color(58, 62, 0, 255, 8, 8)
+ bounds: 464 496 8 8
+ - image: solid-color(59, 62, 0, 255, 8, 8)
+ bounds: 472 496 8 8
+ - image: solid-color(60, 62, 0, 255, 8, 8)
+ bounds: 480 496 8 8
+ - image: solid-color(61, 62, 0, 255, 8, 8)
+ bounds: 488 496 8 8
+ - image: solid-color(62, 62, 0, 255, 8, 8)
+ bounds: 496 496 8 8
+ - image: solid-color(63, 62, 0, 255, 8, 8)
+ bounds: 504 496 8 8
+ - image: solid-color(64, 62, 0, 255, 8, 8)
+ bounds: 512 496 8 8
+ - image: solid-color(65, 62, 0, 255, 8, 8)
+ bounds: 520 496 8 8
+ - image: solid-color(66, 62, 0, 255, 8, 8)
+ bounds: 528 496 8 8
+ - image: solid-color(67, 62, 0, 255, 8, 8)
+ bounds: 536 496 8 8
+ - image: solid-color(68, 62, 0, 255, 8, 8)
+ bounds: 544 496 8 8
+ - image: solid-color(69, 62, 0, 255, 8, 8)
+ bounds: 552 496 8 8
+ - image: solid-color(70, 62, 0, 255, 8, 8)
+ bounds: 560 496 8 8
+ - image: solid-color(71, 62, 0, 255, 8, 8)
+ bounds: 568 496 8 8
+ - image: solid-color(72, 62, 0, 255, 8, 8)
+ bounds: 576 496 8 8
+ - image: solid-color(73, 62, 0, 255, 8, 8)
+ bounds: 584 496 8 8
+ - image: solid-color(74, 62, 0, 255, 8, 8)
+ bounds: 592 496 8 8
+ - image: solid-color(75, 62, 0, 255, 8, 8)
+ bounds: 600 496 8 8
+ - image: solid-color(76, 62, 0, 255, 8, 8)
+ bounds: 608 496 8 8
+ - image: solid-color(77, 62, 0, 255, 8, 8)
+ bounds: 616 496 8 8
+ - image: solid-color(78, 62, 0, 255, 8, 8)
+ bounds: 624 496 8 8
+ - image: solid-color(79, 62, 0, 255, 8, 8)
+ bounds: 632 496 8 8
+ - image: solid-color(80, 62, 0, 255, 8, 8)
+ bounds: 640 496 8 8
+ - image: solid-color(81, 62, 0, 255, 8, 8)
+ bounds: 648 496 8 8
+ - image: solid-color(82, 62, 0, 255, 8, 8)
+ bounds: 656 496 8 8
+ - image: solid-color(83, 62, 0, 255, 8, 8)
+ bounds: 664 496 8 8
+ - image: solid-color(84, 62, 0, 255, 8, 8)
+ bounds: 672 496 8 8
+ - image: solid-color(85, 62, 0, 255, 8, 8)
+ bounds: 680 496 8 8
+ - image: solid-color(86, 62, 0, 255, 8, 8)
+ bounds: 688 496 8 8
+ - image: solid-color(87, 62, 0, 255, 8, 8)
+ bounds: 696 496 8 8
+ - image: solid-color(88, 62, 0, 255, 8, 8)
+ bounds: 704 496 8 8
+ - image: solid-color(89, 62, 0, 255, 8, 8)
+ bounds: 712 496 8 8
+ - image: solid-color(90, 62, 0, 255, 8, 8)
+ bounds: 720 496 8 8
+ - image: solid-color(91, 62, 0, 255, 8, 8)
+ bounds: 728 496 8 8
+ - image: solid-color(92, 62, 0, 255, 8, 8)
+ bounds: 736 496 8 8
+ - image: solid-color(93, 62, 0, 255, 8, 8)
+ bounds: 744 496 8 8
+ - image: solid-color(94, 62, 0, 255, 8, 8)
+ bounds: 752 496 8 8
+ - image: solid-color(95, 62, 0, 255, 8, 8)
+ bounds: 760 496 8 8
+ - image: solid-color(96, 62, 0, 255, 8, 8)
+ bounds: 768 496 8 8
+ - image: solid-color(97, 62, 0, 255, 8, 8)
+ bounds: 776 496 8 8
+ - image: solid-color(98, 62, 0, 255, 8, 8)
+ bounds: 784 496 8 8
+ - image: solid-color(99, 62, 0, 255, 8, 8)
+ bounds: 792 496 8 8
+ - image: solid-color(100, 62, 0, 255, 8, 8)
+ bounds: 800 496 8 8
+ - image: solid-color(101, 62, 0, 255, 8, 8)
+ bounds: 808 496 8 8
+ - image: solid-color(102, 62, 0, 255, 8, 8)
+ bounds: 816 496 8 8
+ - image: solid-color(103, 62, 0, 255, 8, 8)
+ bounds: 824 496 8 8
+ - image: solid-color(104, 62, 0, 255, 8, 8)
+ bounds: 832 496 8 8
+ - image: solid-color(105, 62, 0, 255, 8, 8)
+ bounds: 840 496 8 8
+ - image: solid-color(106, 62, 0, 255, 8, 8)
+ bounds: 848 496 8 8
+ - image: solid-color(107, 62, 0, 255, 8, 8)
+ bounds: 856 496 8 8
+ - image: solid-color(108, 62, 0, 255, 8, 8)
+ bounds: 864 496 8 8
+ - image: solid-color(109, 62, 0, 255, 8, 8)
+ bounds: 872 496 8 8
+ - image: solid-color(110, 62, 0, 255, 8, 8)
+ bounds: 880 496 8 8
+ - image: solid-color(111, 62, 0, 255, 8, 8)
+ bounds: 888 496 8 8
+ - image: solid-color(112, 62, 0, 255, 8, 8)
+ bounds: 896 496 8 8
+ - image: solid-color(113, 62, 0, 255, 8, 8)
+ bounds: 904 496 8 8
+ - image: solid-color(114, 62, 0, 255, 8, 8)
+ bounds: 912 496 8 8
+ - image: solid-color(115, 62, 0, 255, 8, 8)
+ bounds: 920 496 8 8
+ - image: solid-color(116, 62, 0, 255, 8, 8)
+ bounds: 928 496 8 8
+ - image: solid-color(117, 62, 0, 255, 8, 8)
+ bounds: 936 496 8 8
+ - image: solid-color(118, 62, 0, 255, 8, 8)
+ bounds: 944 496 8 8
+ - image: solid-color(119, 62, 0, 255, 8, 8)
+ bounds: 952 496 8 8
+ - image: solid-color(120, 62, 0, 255, 8, 8)
+ bounds: 960 496 8 8
+ - image: solid-color(121, 62, 0, 255, 8, 8)
+ bounds: 968 496 8 8
+ - image: solid-color(122, 62, 0, 255, 8, 8)
+ bounds: 976 496 8 8
+ - image: solid-color(123, 62, 0, 255, 8, 8)
+ bounds: 984 496 8 8
+ - image: solid-color(124, 62, 0, 255, 8, 8)
+ bounds: 992 496 8 8
+ - image: solid-color(125, 62, 0, 255, 8, 8)
+ bounds: 1000 496 8 8
+ - image: solid-color(126, 62, 0, 255, 8, 8)
+ bounds: 1008 496 8 8
+ - image: solid-color(127, 62, 0, 255, 8, 8)
+ bounds: 1016 496 8 8
+ - image: solid-color(0, 63, 0, 255, 8, 8)
+ bounds: 0 504 8 8
+ - image: solid-color(1, 63, 0, 255, 8, 8)
+ bounds: 8 504 8 8
+ - image: solid-color(2, 63, 0, 255, 8, 8)
+ bounds: 16 504 8 8
+ - image: solid-color(3, 63, 0, 255, 8, 8)
+ bounds: 24 504 8 8
+ - image: solid-color(4, 63, 0, 255, 8, 8)
+ bounds: 32 504 8 8
+ - image: solid-color(5, 63, 0, 255, 8, 8)
+ bounds: 40 504 8 8
+ - image: solid-color(6, 63, 0, 255, 8, 8)
+ bounds: 48 504 8 8
+ - image: solid-color(7, 63, 0, 255, 8, 8)
+ bounds: 56 504 8 8
+ - image: solid-color(8, 63, 0, 255, 8, 8)
+ bounds: 64 504 8 8
+ - image: solid-color(9, 63, 0, 255, 8, 8)
+ bounds: 72 504 8 8
+ - image: solid-color(10, 63, 0, 255, 8, 8)
+ bounds: 80 504 8 8
+ - image: solid-color(11, 63, 0, 255, 8, 8)
+ bounds: 88 504 8 8
+ - image: solid-color(12, 63, 0, 255, 8, 8)
+ bounds: 96 504 8 8
+ - image: solid-color(13, 63, 0, 255, 8, 8)
+ bounds: 104 504 8 8
+ - image: solid-color(14, 63, 0, 255, 8, 8)
+ bounds: 112 504 8 8
+ - image: solid-color(15, 63, 0, 255, 8, 8)
+ bounds: 120 504 8 8
+ - image: solid-color(16, 63, 0, 255, 8, 8)
+ bounds: 128 504 8 8
+ - image: solid-color(17, 63, 0, 255, 8, 8)
+ bounds: 136 504 8 8
+ - image: solid-color(18, 63, 0, 255, 8, 8)
+ bounds: 144 504 8 8
+ - image: solid-color(19, 63, 0, 255, 8, 8)
+ bounds: 152 504 8 8
+ - image: solid-color(20, 63, 0, 255, 8, 8)
+ bounds: 160 504 8 8
+ - image: solid-color(21, 63, 0, 255, 8, 8)
+ bounds: 168 504 8 8
+ - image: solid-color(22, 63, 0, 255, 8, 8)
+ bounds: 176 504 8 8
+ - image: solid-color(23, 63, 0, 255, 8, 8)
+ bounds: 184 504 8 8
+ - image: solid-color(24, 63, 0, 255, 8, 8)
+ bounds: 192 504 8 8
+ - image: solid-color(25, 63, 0, 255, 8, 8)
+ bounds: 200 504 8 8
+ - image: solid-color(26, 63, 0, 255, 8, 8)
+ bounds: 208 504 8 8
+ - image: solid-color(27, 63, 0, 255, 8, 8)
+ bounds: 216 504 8 8
+ - image: solid-color(28, 63, 0, 255, 8, 8)
+ bounds: 224 504 8 8
+ - image: solid-color(29, 63, 0, 255, 8, 8)
+ bounds: 232 504 8 8
+ - image: solid-color(30, 63, 0, 255, 8, 8)
+ bounds: 240 504 8 8
+ - image: solid-color(31, 63, 0, 255, 8, 8)
+ bounds: 248 504 8 8
+ - image: solid-color(32, 63, 0, 255, 8, 8)
+ bounds: 256 504 8 8
+ - image: solid-color(33, 63, 0, 255, 8, 8)
+ bounds: 264 504 8 8
+ - image: solid-color(34, 63, 0, 255, 8, 8)
+ bounds: 272 504 8 8
+ - image: solid-color(35, 63, 0, 255, 8, 8)
+ bounds: 280 504 8 8
+ - image: solid-color(36, 63, 0, 255, 8, 8)
+ bounds: 288 504 8 8
+ - image: solid-color(37, 63, 0, 255, 8, 8)
+ bounds: 296 504 8 8
+ - image: solid-color(38, 63, 0, 255, 8, 8)
+ bounds: 304 504 8 8
+ - image: solid-color(39, 63, 0, 255, 8, 8)
+ bounds: 312 504 8 8
+ - image: solid-color(40, 63, 0, 255, 8, 8)
+ bounds: 320 504 8 8
+ - image: solid-color(41, 63, 0, 255, 8, 8)
+ bounds: 328 504 8 8
+ - image: solid-color(42, 63, 0, 255, 8, 8)
+ bounds: 336 504 8 8
+ - image: solid-color(43, 63, 0, 255, 8, 8)
+ bounds: 344 504 8 8
+ - image: solid-color(44, 63, 0, 255, 8, 8)
+ bounds: 352 504 8 8
+ - image: solid-color(45, 63, 0, 255, 8, 8)
+ bounds: 360 504 8 8
+ - image: solid-color(46, 63, 0, 255, 8, 8)
+ bounds: 368 504 8 8
+ - image: solid-color(47, 63, 0, 255, 8, 8)
+ bounds: 376 504 8 8
+ - image: solid-color(48, 63, 0, 255, 8, 8)
+ bounds: 384 504 8 8
+ - image: solid-color(49, 63, 0, 255, 8, 8)
+ bounds: 392 504 8 8
+ - image: solid-color(50, 63, 0, 255, 8, 8)
+ bounds: 400 504 8 8
+ - image: solid-color(51, 63, 0, 255, 8, 8)
+ bounds: 408 504 8 8
+ - image: solid-color(52, 63, 0, 255, 8, 8)
+ bounds: 416 504 8 8
+ - image: solid-color(53, 63, 0, 255, 8, 8)
+ bounds: 424 504 8 8
+ - image: solid-color(54, 63, 0, 255, 8, 8)
+ bounds: 432 504 8 8
+ - image: solid-color(55, 63, 0, 255, 8, 8)
+ bounds: 440 504 8 8
+ - image: solid-color(56, 63, 0, 255, 8, 8)
+ bounds: 448 504 8 8
+ - image: solid-color(57, 63, 0, 255, 8, 8)
+ bounds: 456 504 8 8
+ - image: solid-color(58, 63, 0, 255, 8, 8)
+ bounds: 464 504 8 8
+ - image: solid-color(59, 63, 0, 255, 8, 8)
+ bounds: 472 504 8 8
+ - image: solid-color(60, 63, 0, 255, 8, 8)
+ bounds: 480 504 8 8
+ - image: solid-color(61, 63, 0, 255, 8, 8)
+ bounds: 488 504 8 8
+ - image: solid-color(62, 63, 0, 255, 8, 8)
+ bounds: 496 504 8 8
+ - image: solid-color(63, 63, 0, 255, 8, 8)
+ bounds: 504 504 8 8
+ - image: solid-color(64, 63, 0, 255, 8, 8)
+ bounds: 512 504 8 8
+ - image: solid-color(65, 63, 0, 255, 8, 8)
+ bounds: 520 504 8 8
+ - image: solid-color(66, 63, 0, 255, 8, 8)
+ bounds: 528 504 8 8
+ - image: solid-color(67, 63, 0, 255, 8, 8)
+ bounds: 536 504 8 8
+ - image: solid-color(68, 63, 0, 255, 8, 8)
+ bounds: 544 504 8 8
+ - image: solid-color(69, 63, 0, 255, 8, 8)
+ bounds: 552 504 8 8
+ - image: solid-color(70, 63, 0, 255, 8, 8)
+ bounds: 560 504 8 8
+ - image: solid-color(71, 63, 0, 255, 8, 8)
+ bounds: 568 504 8 8
+ - image: solid-color(72, 63, 0, 255, 8, 8)
+ bounds: 576 504 8 8
+ - image: solid-color(73, 63, 0, 255, 8, 8)
+ bounds: 584 504 8 8
+ - image: solid-color(74, 63, 0, 255, 8, 8)
+ bounds: 592 504 8 8
+ - image: solid-color(75, 63, 0, 255, 8, 8)
+ bounds: 600 504 8 8
+ - image: solid-color(76, 63, 0, 255, 8, 8)
+ bounds: 608 504 8 8
+ - image: solid-color(77, 63, 0, 255, 8, 8)
+ bounds: 616 504 8 8
+ - image: solid-color(78, 63, 0, 255, 8, 8)
+ bounds: 624 504 8 8
+ - image: solid-color(79, 63, 0, 255, 8, 8)
+ bounds: 632 504 8 8
+ - image: solid-color(80, 63, 0, 255, 8, 8)
+ bounds: 640 504 8 8
+ - image: solid-color(81, 63, 0, 255, 8, 8)
+ bounds: 648 504 8 8
+ - image: solid-color(82, 63, 0, 255, 8, 8)
+ bounds: 656 504 8 8
+ - image: solid-color(83, 63, 0, 255, 8, 8)
+ bounds: 664 504 8 8
+ - image: solid-color(84, 63, 0, 255, 8, 8)
+ bounds: 672 504 8 8
+ - image: solid-color(85, 63, 0, 255, 8, 8)
+ bounds: 680 504 8 8
+ - image: solid-color(86, 63, 0, 255, 8, 8)
+ bounds: 688 504 8 8
+ - image: solid-color(87, 63, 0, 255, 8, 8)
+ bounds: 696 504 8 8
+ - image: solid-color(88, 63, 0, 255, 8, 8)
+ bounds: 704 504 8 8
+ - image: solid-color(89, 63, 0, 255, 8, 8)
+ bounds: 712 504 8 8
+ - image: solid-color(90, 63, 0, 255, 8, 8)
+ bounds: 720 504 8 8
+ - image: solid-color(91, 63, 0, 255, 8, 8)
+ bounds: 728 504 8 8
+ - image: solid-color(92, 63, 0, 255, 8, 8)
+ bounds: 736 504 8 8
+ - image: solid-color(93, 63, 0, 255, 8, 8)
+ bounds: 744 504 8 8
+ - image: solid-color(94, 63, 0, 255, 8, 8)
+ bounds: 752 504 8 8
+ - image: solid-color(95, 63, 0, 255, 8, 8)
+ bounds: 760 504 8 8
+ - image: solid-color(96, 63, 0, 255, 8, 8)
+ bounds: 768 504 8 8
+ - image: solid-color(97, 63, 0, 255, 8, 8)
+ bounds: 776 504 8 8
+ - image: solid-color(98, 63, 0, 255, 8, 8)
+ bounds: 784 504 8 8
+ - image: solid-color(99, 63, 0, 255, 8, 8)
+ bounds: 792 504 8 8
+ - image: solid-color(100, 63, 0, 255, 8, 8)
+ bounds: 800 504 8 8
+ - image: solid-color(101, 63, 0, 255, 8, 8)
+ bounds: 808 504 8 8
+ - image: solid-color(102, 63, 0, 255, 8, 8)
+ bounds: 816 504 8 8
+ - image: solid-color(103, 63, 0, 255, 8, 8)
+ bounds: 824 504 8 8
+ - image: solid-color(104, 63, 0, 255, 8, 8)
+ bounds: 832 504 8 8
+ - image: solid-color(105, 63, 0, 255, 8, 8)
+ bounds: 840 504 8 8
+ - image: solid-color(106, 63, 0, 255, 8, 8)
+ bounds: 848 504 8 8
+ - image: solid-color(107, 63, 0, 255, 8, 8)
+ bounds: 856 504 8 8
+ - image: solid-color(108, 63, 0, 255, 8, 8)
+ bounds: 864 504 8 8
+ - image: solid-color(109, 63, 0, 255, 8, 8)
+ bounds: 872 504 8 8
+ - image: solid-color(110, 63, 0, 255, 8, 8)
+ bounds: 880 504 8 8
+ - image: solid-color(111, 63, 0, 255, 8, 8)
+ bounds: 888 504 8 8
+ - image: solid-color(112, 63, 0, 255, 8, 8)
+ bounds: 896 504 8 8
+ - image: solid-color(113, 63, 0, 255, 8, 8)
+ bounds: 904 504 8 8
+ - image: solid-color(114, 63, 0, 255, 8, 8)
+ bounds: 912 504 8 8
+ - image: solid-color(115, 63, 0, 255, 8, 8)
+ bounds: 920 504 8 8
+ - image: solid-color(116, 63, 0, 255, 8, 8)
+ bounds: 928 504 8 8
+ - image: solid-color(117, 63, 0, 255, 8, 8)
+ bounds: 936 504 8 8
+ - image: solid-color(118, 63, 0, 255, 8, 8)
+ bounds: 944 504 8 8
+ - image: solid-color(119, 63, 0, 255, 8, 8)
+ bounds: 952 504 8 8
+ - image: solid-color(120, 63, 0, 255, 8, 8)
+ bounds: 960 504 8 8
+ - image: solid-color(121, 63, 0, 255, 8, 8)
+ bounds: 968 504 8 8
+ - image: solid-color(122, 63, 0, 255, 8, 8)
+ bounds: 976 504 8 8
+ - image: solid-color(123, 63, 0, 255, 8, 8)
+ bounds: 984 504 8 8
+ - image: solid-color(124, 63, 0, 255, 8, 8)
+ bounds: 992 504 8 8
+ - image: solid-color(125, 63, 0, 255, 8, 8)
+ bounds: 1000 504 8 8
+ - image: solid-color(126, 63, 0, 255, 8, 8)
+ bounds: 1008 504 8 8
+ - image: solid-color(127, 63, 0, 255, 8, 8)
+ bounds: 1016 504 8 8
diff --git a/gfx/wr/wrench/benchmarks/overlapping-text-shadows.yaml b/gfx/wr/wrench/benchmarks/overlapping-text-shadows.yaml
new file mode 100644
index 0000000000..d32d4aa45c
--- /dev/null
+++ b/gfx/wr/wrench/benchmarks/overlapping-text-shadows.yaml
@@ -0,0 +1,807 @@
+root:
+ items:
+ -
+ type: "shadow"
+ offset: [0, 0]
+ color: red
+ -
+ type: "shadow"
+ offset: [1, 1]
+ color: red
+ -
+ type: "shadow"
+ offset: [2, 2]
+ color: red
+ -
+ type: "shadow"
+ offset: [3, 3]
+ color: red
+ -
+ type: "shadow"
+ offset: [4, 4]
+ color: red
+ -
+ type: "shadow"
+ offset: [5, 5]
+ color: red
+ -
+ type: "shadow"
+ offset: [6, 6]
+ color: red
+ -
+ type: "shadow"
+ offset: [7, 7]
+ color: red
+ -
+ type: "shadow"
+ offset: [8, 8]
+ color: red
+ -
+ type: "shadow"
+ offset: [9, 9]
+ color: red
+ -
+ type: "shadow"
+ offset: [10, 10]
+ color: red
+ -
+ type: "shadow"
+ offset: [11, 11]
+ color: red
+ -
+ type: "shadow"
+ offset: [12, 12]
+ color: red
+ -
+ type: "shadow"
+ offset: [13, 13]
+ color: red
+ -
+ type: "shadow"
+ offset: [14, 14]
+ color: red
+ -
+ type: "shadow"
+ offset: [15, 15]
+ color: red
+ -
+ type: "shadow"
+ offset: [16, 16]
+ color: red
+ -
+ type: "shadow"
+ offset: [17, 17]
+ color: red
+ -
+ type: "shadow"
+ offset: [18, 18]
+ color: red
+ -
+ type: "shadow"
+ offset: [19, 19]
+ color: red
+ -
+ type: "shadow"
+ offset: [20, 20]
+ color: red
+ -
+ type: "shadow"
+ offset: [21, 21]
+ color: red
+ -
+ type: "shadow"
+ offset: [22, 22]
+ color: red
+ -
+ type: "shadow"
+ offset: [23, 23]
+ color: red
+ -
+ type: "shadow"
+ offset: [24, 24]
+ color: red
+ -
+ type: "shadow"
+ offset: [25, 25]
+ color: red
+ -
+ type: "shadow"
+ offset: [26, 26]
+ color: red
+ -
+ type: "shadow"
+ offset: [27, 27]
+ color: red
+ -
+ type: "shadow"
+ offset: [28, 28]
+ color: red
+ -
+ type: "shadow"
+ offset: [29, 29]
+ color: red
+ -
+ type: "shadow"
+ offset: [30, 30]
+ color: red
+ -
+ type: "shadow"
+ offset: [31, 31]
+ color: red
+ -
+ type: "shadow"
+ offset: [32, 32]
+ color: red
+ -
+ type: "shadow"
+ offset: [33, 33]
+ color: red
+ -
+ type: "shadow"
+ offset: [34, 34]
+ color: red
+ -
+ type: "shadow"
+ offset: [35, 35]
+ color: red
+ -
+ type: "shadow"
+ offset: [36, 36]
+ color: red
+ -
+ type: "shadow"
+ offset: [37, 37]
+ color: red
+ -
+ type: "shadow"
+ offset: [38, 38]
+ color: red
+ -
+ type: "shadow"
+ offset: [39, 39]
+ color: red
+ -
+ type: "shadow"
+ offset: [40, 40]
+ color: red
+ -
+ type: "shadow"
+ offset: [41, 41]
+ color: red
+ -
+ type: "shadow"
+ offset: [42, 42]
+ color: red
+ -
+ type: "shadow"
+ offset: [43, 43]
+ color: red
+ -
+ type: "shadow"
+ offset: [44, 44]
+ color: red
+ -
+ type: "shadow"
+ offset: [45, 45]
+ color: red
+ -
+ type: "shadow"
+ offset: [46, 46]
+ color: red
+ -
+ type: "shadow"
+ offset: [47, 47]
+ color: red
+ -
+ type: "shadow"
+ offset: [48, 48]
+ color: red
+ -
+ type: "shadow"
+ offset: [49, 49]
+ color: red
+ -
+ type: "shadow"
+ offset: [50, 50]
+ color: red
+ -
+ type: "shadow"
+ offset: [51, 51]
+ color: red
+ -
+ type: "shadow"
+ offset: [52, 52]
+ color: red
+ -
+ type: "shadow"
+ offset: [53, 53]
+ color: red
+ -
+ type: "shadow"
+ offset: [54, 54]
+ color: red
+ -
+ type: "shadow"
+ offset: [55, 55]
+ color: red
+ -
+ type: "shadow"
+ offset: [56, 56]
+ color: red
+ -
+ type: "shadow"
+ offset: [57, 57]
+ color: red
+ -
+ type: "shadow"
+ offset: [58, 58]
+ color: red
+ -
+ type: "shadow"
+ offset: [59, 59]
+ color: red
+ -
+ type: "shadow"
+ offset: [60, 60]
+ color: red
+ -
+ type: "shadow"
+ offset: [61, 61]
+ color: red
+ -
+ type: "shadow"
+ offset: [62, 62]
+ color: red
+ -
+ type: "shadow"
+ offset: [63, 63]
+ color: red
+ -
+ type: "shadow"
+ offset: [64, 64]
+ color: red
+ -
+ type: "shadow"
+ offset: [65, 65]
+ color: red
+ -
+ type: "shadow"
+ offset: [66, 66]
+ color: red
+ -
+ type: "shadow"
+ offset: [67, 67]
+ color: red
+ -
+ type: "shadow"
+ offset: [68, 68]
+ color: red
+ -
+ type: "shadow"
+ offset: [69, 69]
+ color: red
+ -
+ type: "shadow"
+ offset: [70, 70]
+ color: red
+ -
+ type: "shadow"
+ offset: [71, 71]
+ color: red
+ -
+ type: "shadow"
+ offset: [72, 72]
+ color: red
+ -
+ type: "shadow"
+ offset: [73, 73]
+ color: red
+ -
+ type: "shadow"
+ offset: [74, 74]
+ color: red
+ -
+ type: "shadow"
+ offset: [75, 75]
+ color: red
+ -
+ type: "shadow"
+ offset: [76, 76]
+ color: red
+ -
+ type: "shadow"
+ offset: [77, 77]
+ color: red
+ -
+ type: "shadow"
+ offset: [78, 78]
+ color: red
+ -
+ type: "shadow"
+ offset: [79, 79]
+ color: red
+ -
+ type: "shadow"
+ offset: [80, 80]
+ color: red
+ -
+ type: "shadow"
+ offset: [81, 81]
+ color: red
+ -
+ type: "shadow"
+ offset: [82, 82]
+ color: red
+ -
+ type: "shadow"
+ offset: [83, 83]
+ color: red
+ -
+ type: "shadow"
+ offset: [84, 84]
+ color: red
+ -
+ type: "shadow"
+ offset: [85, 85]
+ color: red
+ -
+ type: "shadow"
+ offset: [86, 86]
+ color: red
+ -
+ type: "shadow"
+ offset: [87, 87]
+ color: red
+ -
+ type: "shadow"
+ offset: [88, 88]
+ color: red
+ -
+ type: "shadow"
+ offset: [89, 89]
+ color: red
+ -
+ type: "shadow"
+ offset: [90, 90]
+ color: red
+ -
+ type: "shadow"
+ offset: [91, 91]
+ color: red
+ -
+ type: "shadow"
+ offset: [92, 92]
+ color: red
+ -
+ type: "shadow"
+ offset: [93, 93]
+ color: red
+ -
+ type: "shadow"
+ offset: [94, 94]
+ color: red
+ -
+ type: "shadow"
+ offset: [95, 95]
+ color: red
+ -
+ type: "shadow"
+ offset: [96, 96]
+ color: red
+ -
+ type: "shadow"
+ offset: [97, 97]
+ color: red
+ -
+ type: "shadow"
+ offset: [98, 98]
+ color: red
+ -
+ type: "shadow"
+ offset: [99, 99]
+ color: red
+ -
+ type: "shadow"
+ offset: [100, 100]
+ color: red
+ -
+ type: "shadow"
+ offset: [101, 101]
+ color: red
+ -
+ type: "shadow"
+ offset: [102, 102]
+ color: red
+ -
+ type: "shadow"
+ offset: [103, 103]
+ color: red
+ -
+ type: "shadow"
+ offset: [104, 104]
+ color: red
+ -
+ type: "shadow"
+ offset: [105, 105]
+ color: red
+ -
+ type: "shadow"
+ offset: [106, 106]
+ color: red
+ -
+ type: "shadow"
+ offset: [107, 107]
+ color: red
+ -
+ type: "shadow"
+ offset: [108, 108]
+ color: red
+ -
+ type: "shadow"
+ offset: [109, 109]
+ color: red
+ -
+ type: "shadow"
+ offset: [110, 110]
+ color: red
+ -
+ type: "shadow"
+ offset: [111, 111]
+ color: red
+ -
+ type: "shadow"
+ offset: [112, 112]
+ color: red
+ -
+ type: "shadow"
+ offset: [113, 113]
+ color: red
+ -
+ type: "shadow"
+ offset: [114, 114]
+ color: red
+ -
+ type: "shadow"
+ offset: [115, 115]
+ color: red
+ -
+ type: "shadow"
+ offset: [116, 116]
+ color: red
+ -
+ type: "shadow"
+ offset: [117, 117]
+ color: red
+ -
+ type: "shadow"
+ offset: [118, 118]
+ color: red
+ -
+ type: "shadow"
+ offset: [119, 119]
+ color: red
+ -
+ type: "shadow"
+ offset: [120, 120]
+ color: red
+ -
+ type: "shadow"
+ offset: [121, 121]
+ color: red
+ -
+ type: "shadow"
+ offset: [122, 122]
+ color: red
+ -
+ type: "shadow"
+ offset: [123, 123]
+ color: red
+ -
+ type: "shadow"
+ offset: [124, 124]
+ color: red
+ -
+ type: "shadow"
+ offset: [125, 125]
+ color: red
+ -
+ type: "shadow"
+ offset: [126, 126]
+ color: red
+ -
+ type: "shadow"
+ offset: [127, 127]
+ color: red
+ -
+ type: "shadow"
+ offset: [128, 128]
+ color: red
+ -
+ type: "shadow"
+ offset: [129, 129]
+ color: red
+ -
+ type: "shadow"
+ offset: [130, 130]
+ color: red
+ -
+ type: "shadow"
+ offset: [131, 131]
+ color: red
+ -
+ type: "shadow"
+ offset: [132, 132]
+ color: red
+ -
+ type: "shadow"
+ offset: [133, 133]
+ color: red
+ -
+ type: "shadow"
+ offset: [134, 134]
+ color: red
+ -
+ type: "shadow"
+ offset: [135, 135]
+ color: red
+ -
+ type: "shadow"
+ offset: [136, 136]
+ color: red
+ -
+ type: "shadow"
+ offset: [137, 137]
+ color: red
+ -
+ type: "shadow"
+ offset: [138, 138]
+ color: red
+ -
+ type: "shadow"
+ offset: [139, 139]
+ color: red
+ -
+ type: "shadow"
+ offset: [140, 140]
+ color: red
+ -
+ type: "shadow"
+ offset: [141, 141]
+ color: red
+ -
+ type: "shadow"
+ offset: [142, 142]
+ color: red
+ -
+ type: "shadow"
+ offset: [143, 143]
+ color: red
+ -
+ type: "shadow"
+ offset: [144, 144]
+ color: red
+ -
+ type: "shadow"
+ offset: [145, 145]
+ color: red
+ -
+ type: "shadow"
+ offset: [146, 146]
+ color: red
+ -
+ type: "shadow"
+ offset: [147, 147]
+ color: red
+ -
+ type: "shadow"
+ offset: [148, 148]
+ color: red
+ -
+ type: "shadow"
+ offset: [149, 149]
+ color: red
+ -
+ type: "shadow"
+ offset: [150, 150]
+ color: red
+ -
+ type: "shadow"
+ offset: [151, 151]
+ color: red
+ -
+ type: "shadow"
+ offset: [152, 152]
+ color: red
+ -
+ type: "shadow"
+ offset: [153, 153]
+ color: red
+ -
+ type: "shadow"
+ offset: [154, 154]
+ color: red
+ -
+ type: "shadow"
+ offset: [155, 155]
+ color: red
+ -
+ type: "shadow"
+ offset: [156, 156]
+ color: red
+ -
+ type: "shadow"
+ offset: [157, 157]
+ color: red
+ -
+ type: "shadow"
+ offset: [158, 158]
+ color: red
+ -
+ type: "shadow"
+ offset: [159, 159]
+ color: red
+ -
+ type: "shadow"
+ offset: [160, 160]
+ color: red
+ -
+ type: "shadow"
+ offset: [161, 161]
+ color: red
+ -
+ type: "shadow"
+ offset: [162, 162]
+ color: red
+ -
+ type: "shadow"
+ offset: [163, 163]
+ color: red
+ -
+ type: "shadow"
+ offset: [164, 164]
+ color: red
+ -
+ type: "shadow"
+ offset: [165, 165]
+ color: red
+ -
+ type: "shadow"
+ offset: [166, 166]
+ color: red
+ -
+ type: "shadow"
+ offset: [167, 167]
+ color: red
+ -
+ type: "shadow"
+ offset: [168, 168]
+ color: red
+ -
+ type: "shadow"
+ offset: [169, 169]
+ color: red
+ -
+ type: "shadow"
+ offset: [170, 170]
+ color: red
+ -
+ type: "shadow"
+ offset: [171, 171]
+ color: red
+ -
+ type: "shadow"
+ offset: [172, 172]
+ color: red
+ -
+ type: "shadow"
+ offset: [173, 173]
+ color: red
+ -
+ type: "shadow"
+ offset: [174, 174]
+ color: red
+ -
+ type: "shadow"
+ offset: [175, 175]
+ color: red
+ -
+ type: "shadow"
+ offset: [176, 176]
+ color: red
+ -
+ type: "shadow"
+ offset: [177, 177]
+ color: red
+ -
+ type: "shadow"
+ offset: [178, 178]
+ color: red
+ -
+ type: "shadow"
+ offset: [179, 179]
+ color: red
+ -
+ type: "shadow"
+ offset: [180, 180]
+ color: red
+ -
+ type: "shadow"
+ offset: [181, 181]
+ color: red
+ -
+ type: "shadow"
+ offset: [182, 182]
+ color: red
+ -
+ type: "shadow"
+ offset: [183, 183]
+ color: red
+ -
+ type: "shadow"
+ offset: [184, 184]
+ color: red
+ -
+ type: "shadow"
+ offset: [185, 185]
+ color: red
+ -
+ type: "shadow"
+ offset: [186, 186]
+ color: red
+ -
+ type: "shadow"
+ offset: [187, 187]
+ color: red
+ -
+ type: "shadow"
+ offset: [188, 188]
+ color: red
+ -
+ type: "shadow"
+ offset: [189, 189]
+ color: red
+ -
+ type: "shadow"
+ offset: [190, 190]
+ color: red
+ -
+ type: "shadow"
+ offset: [191, 191]
+ color: red
+ -
+ type: "shadow"
+ offset: [192, 192]
+ color: red
+ -
+ type: "shadow"
+ offset: [193, 193]
+ color: red
+ -
+ type: "shadow"
+ offset: [194, 194]
+ color: red
+ -
+ type: "shadow"
+ offset: [195, 195]
+ color: red
+ -
+ type: "shadow"
+ offset: [196, 196]
+ color: red
+ -
+ type: "shadow"
+ offset: [197, 197]
+ color: red
+ -
+ type: "shadow"
+ offset: [198, 198]
+ color: red
+ -
+ type: "shadow"
+ offset: [199, 199]
+ color: red
+ - text: "Much overdraw many pixels"
+ origin: 20 70
+ size: 60
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/benchmarks/radial-gradient.yaml b/gfx/wr/wrench/benchmarks/radial-gradient.yaml
new file mode 100644
index 0000000000..e71d301371
--- /dev/null
+++ b/gfx/wr/wrench/benchmarks/radial-gradient.yaml
@@ -0,0 +1,52 @@
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: [ 0, 0, 1980, 1080]
+ center: [ 990, 540 ]
+ start-radius: 5
+ end-radius: 8000
+ stops: [ 0.0, black, 1.0, white ]
+ repeat: false
+ - type: radial-gradient
+ bounds: [ 0, 0, 1980, 1080]
+ center: [ 990, 540 ]
+ start-radius: 5
+ end-radius: 8000
+ stops: [ 0.0, black, 1.0, white ]
+ repeat: false
+ - type: radial-gradient
+ bounds: [ 0, 0, 1980, 1080]
+ center: [ 990, 540 ]
+ start-radius: 5
+ end-radius: 8000
+ stops: [ 0.0, black, 1.0, white ]
+ repeat: false
+ - type: radial-gradient
+ bounds: [ 0, 0, 1980, 1080]
+ center: [ 990, 540 ]
+ start-radius: 5
+ end-radius: 8000
+ stops: [ 0.0, black, 1.0, white ]
+ repeat: false
+ - type: radial-gradient
+ bounds: [ 0, 0, 1980, 1080]
+ center: [ 990, 540 ]
+ start-radius: 5
+ end-radius: 8000
+ stops: [ 0.0, black, 1.0, white ]
+ repeat: false
+ - type: radial-gradient
+ bounds: [ 0, 0, 1980, 1080]
+ center: [ 990, 540 ]
+ start-radius: 5
+ end-radius: 8000
+ stops: [ 0.0, black, 1.0, white ]
+ repeat: false
+ - type: radial-gradient
+ bounds: [ 0, 0, 1980, 1080]
+ center: [ 990, 540 ]
+ start-radius: 5
+ end-radius: 8000
+ stops: [ 0.0, black, 1.0, white ]
+ repeat: false
diff --git a/gfx/wr/wrench/benchmarks/simple-batching.yaml b/gfx/wr/wrench/benchmarks/simple-batching.yaml
new file mode 100644
index 0000000000..4f2cf019b2
--- /dev/null
+++ b/gfx/wr/wrench/benchmarks/simple-batching.yaml
@@ -0,0 +1,45 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 512, 512]
+ color: green
+ - type: rect
+ bounds: [0, 0, 512, 512]
+ color: green
+ - type: rect
+ bounds: [0, 0, 512, 512]
+ color: green
+ - type: rect
+ bounds: [0, 0, 512, 512]
+ color: green
+ - type: rect
+ bounds: [0, 0, 512, 512]
+ color: green
+ - type: rect
+ bounds: [0, 0, 512, 512]
+ color: green
+ - type: rect
+ bounds: [0, 0, 512, 512]
+ color: green
+ - type: rect
+ bounds: [0, 0, 512, 512]
+ color: green
+ - type: rect
+ bounds: [0, 0, 512, 512]
+ color: green
+ - type: rect
+ bounds: [0, 0, 512, 512]
+ color: green
+ - type: rect
+ bounds: [0, 0, 512, 512]
+ color: green
+ - type: rect
+ bounds: [0, 0, 512, 512]
+ color: green
+ - type: rect
+ bounds: [0, 0, 512, 512]
+ color: green
+ - type: rect
+ bounds: [0, 0, 512, 512]
+ color: green
diff --git a/gfx/wr/wrench/benchmarks/text-rendering.yaml b/gfx/wr/wrench/benchmarks/text-rendering.yaml
new file mode 100644
index 0000000000..6b7ac3fb3a
--- /dev/null
+++ b/gfx/wr/wrench/benchmarks/text-rendering.yaml
@@ -0,0 +1,260 @@
+root:
+ items:
+ - text: "Cats making all the muffins knock over christmas tree"
+ origin: 20 30
+ size: 20
+ - text: "Cat ipsum dolor sit amet, poop in the plant pot and demand to be let outside at once, and expect owner to wait for me as i think about it."
+ origin: 20 50
+ size: 9
+ - text: "Slap owner's face at 5am until human fills food dish licks your face."
+ origin: 20 80
+ size: 18
+ - text: "Play riveting piece on synthesizer keyboard cat snacks, yet meow go back to sleep owner brings food and water tries to"
+ origin: 20 100
+ size: 10
+ - text: "pet on head, so scratch get sprayed by water because bad cat nap all day, kitty scratches couch bad kitty."
+ origin: 20 112
+ size: 10
+ - text: "Touch water with paw then recoil in horror climb a tree, wait for a fireman jump to fireman then"
+ origin: 20 130
+ size: 12
+ - text: "scratch his face and mark territory when in doubt, wash."
+ origin: 20 150
+ size: 12
+ - text: "Friends are not food warm up laptop with butt lick butt fart rainbows until owner yells pee in litter box hiss at cats if it fits,"
+ origin: 20 170
+ size: 10
+ - text: "i sits swat at dog, or chase the pig around the house yet thug cat but hiss and stare at nothing then run suddenly away."
+ origin: 20 180
+ size: 10
+ - text: "Howl uncontrollably for no reason pushes butt to face eat a plant, kill a hand for cough hairball on conveniently placed pants."
+ origin: 20 200
+ size: 10
+ - text: "Wake up human for food at 4am. Decide to want nothing to do with my owner today pelt around the house and up and down stairs chasing phantoms."
+ origin: 20 220
+ size: 8
+ - text: "Poop on grasses."
+ origin: 20 240
+ size: 16
+ - text: "Hunt by meowing loudly at 5am next to human slave food dispenser bleghbleghvomit my"
+ origin: 20 260
+ size: 10
+ - text: "furball really tie the room together and throwup on your pillow, so scamper."
+ origin: 20 270
+ size: 10
+ - text: "Spread kitty litter all over house."
+ origin: 20 290
+ size: 18
+ - text: "Friends are not food curl into a furry donut or going to catch the red"
+ origin: 20 320
+ size: 16
+ - text: "dot today going to catch the red dot today or destroy the blinds or purr."
+ origin: 20 340
+ size: 16
+
+ - text: "Cats making all the muffins knock over christmas tree"
+ origin: 850 30
+ size: 20
+ color: red
+ - text: "Cat ipsum dolor sit amet, poop in the plant pot and demand to be let outside at once, and expect owner to wait for me as i think about it."
+ origin: 850 50
+ size: 9
+ color: red
+ - text: "Slap owner's face at 5am until human fills food dish licks your face."
+ origin: 850 80
+ size: 18
+ color: red
+ - text: "Play riveting piece on synthesizer keyboard cat snacks, yet meow go back to sleep owner brings food and water tries to"
+ origin: 850 100
+ size: 10
+ color: red
+ - text: "pet on head, so scratch get sprayed by water because bad cat nap all day, kitty scratches couch bad kitty."
+ origin: 850 112
+ size: 10
+ color: red
+ - text: "Touch water with paw then recoil in horror climb a tree, wait for a fireman jump to fireman then"
+ origin: 850 130
+ size: 12
+ color: red
+ - text: "scratch his face and mark territory when in doubt, wash."
+ origin: 850 150
+ size: 12
+ color: red
+ - text: "Friends are not food warm up laptop with butt lick butt fart rainbows until owner yells pee in litter box hiss at cats if it fits,"
+ origin: 850 170
+ size: 10
+ color: red
+ - text: "i sits swat at dog, or chase the pig around the house yet thug cat but hiss and stare at nothing then run suddenly away."
+ origin: 850 180
+ size: 10
+ color: red
+ - text: "Howl uncontrollably for no reason pushes butt to face eat a plant, kill a hand for cough hairball on conveniently placed pants."
+ origin: 850 200
+ size: 10
+ color: red
+ - text: "Wake up human for food at 4am. Decide to want nothing to do with my owner today pelt around the house and up and down stairs chasing phantoms."
+ origin: 850 220
+ size: 8
+ color: red
+ - text: "Poop on grasses."
+ origin: 850 240
+ size: 16
+ color: red
+ - text: "Hunt by meowing loudly at 5am next to human slave food dispenser bleghbleghvomit my"
+ origin: 850 260
+ size: 10
+ color: red
+ - text: "furball really tie the room together and throwup on your pillow, so scamper."
+ origin: 850 270
+ size: 10
+ color: red
+ - text: "Spread kitty litter all over house."
+ origin: 850 290
+ size: 18
+ color: red
+ - text: "Friends are not food curl into a furry donut or going to catch the red"
+ origin: 850 320
+ size: 16
+ color: red
+ - text: "dot today going to catch the red dot today or destroy the blinds or purr."
+ origin: 850 340
+ size: 16
+ color: red
+
+ - text: "Cats making all the muffins knock over christmas tree"
+ origin: 20 430
+ size: 20
+ color: green
+ - text: "Cat ipsum dolor sit amet, poop in the plant pot and demand to be let outside at once, and expect owner to wait for me as i think about it."
+ origin: 20 450
+ size: 9
+ color: green
+ - text: "Slap owner's face at 5am until human fills food dish licks your face."
+ origin: 20 480
+ size: 18
+ color: green
+ - text: "Play riveting piece on synthesizer keyboard cat snacks, yet meow go back to sleep owner brings food and water tries to"
+ origin: 20 500
+ size: 10
+ color: green
+ - text: "pet on head, so scratch get sprayed by water because bad cat nap all day, kitty scratches couch bad kitty."
+ origin: 20 512
+ size: 10
+ color: green
+ - text: "Touch water with paw then recoil in horror climb a tree, wait for a fireman jump to fireman then"
+ origin: 20 530
+ size: 12
+ color: green
+ - text: "scratch his face and mark territory when in doubt, wash."
+ origin: 20 550
+ size: 12
+ color: green
+ - text: "Friends are not food warm up laptop with butt lick butt fart rainbows until owner yells pee in litter box hiss at cats if it fits,"
+ origin: 20 570
+ size: 10
+ color: green
+ - text: "i sits swat at dog, or chase the pig around the house yet thug cat but hiss and stare at nothing then run suddenly away."
+ origin: 20 580
+ size: 10
+ color: green
+ - text: "Howl uncontrollably for no reason pushes butt to face eat a plant, kill a hand for cough hairball on conveniently placed pants."
+ origin: 20 600
+ size: 10
+ color: green
+ - text: "Wake up human for food at 4am. Decide to want nothing to do with my owner today pelt around the house and up and down stairs chasing phantoms."
+ origin: 20 620
+ size: 8
+ color: green
+ - text: "Poop on grasses."
+ origin: 20 640
+ size: 16
+ color: green
+ - text: "Hunt by meowing loudly at 5am next to human slave food dispenser bleghbleghvomit my"
+ origin: 20 660
+ size: 10
+ color: green
+ - text: "furball really tie the room together and throwup on your pillow, so scamper."
+ origin: 20 670
+ size: 10
+ color: green
+ - text: "Spread kitty litter all over house."
+ origin: 20 690
+ size: 18
+ color: green
+ - text: "Friends are not food curl into a furry donut or going to catch the red"
+ origin: 20 720
+ size: 16
+ color: green
+ - text: "dot today going to catch the red dot today or destroy the blinds or purr."
+ origin: 20 740
+ size: 16
+ color: green
+
+ - text: "Cats making all the muffins knock over christmas tree"
+ origin: 850 430
+ size: 20
+ color: blue
+ - text: "Cat ipsum dolor sit amet, poop in the plant pot and demand to be let outside at once, and expect owner to wait for me as i think about it."
+ origin: 850 450
+ size: 9
+ color: blue
+ - text: "Slap owner's face at 5am until human fills food dish licks your face."
+ origin: 850 480
+ size: 18
+ color: blue
+ - text: "Play riveting piece on synthesizer keyboard cat snacks, yet meow go back to sleep owner brings food and water tries to"
+ origin: 850 500
+ size: 10
+ color: blue
+ - text: "pet on head, so scratch get sprayed by water because bad cat nap all day, kitty scratches couch bad kitty."
+ origin: 850 512
+ size: 10
+ color: blue
+ - text: "Touch water with paw then recoil in horror climb a tree, wait for a fireman jump to fireman then"
+ origin: 850 530
+ size: 12
+ color: blue
+ - text: "scratch his face and mark territory when in doubt, wash."
+ origin: 850 550
+ size: 12
+ color: blue
+ - text: "Friends are not food warm up laptop with butt lick butt fart rainbows until owner yells pee in litter box hiss at cats if it fits,"
+ origin: 850 570
+ size: 10
+ color: blue
+ - text: "i sits swat at dog, or chase the pig around the house yet thug cat but hiss and stare at nothing then run suddenly away."
+ origin: 850 580
+ size: 10
+ color: blue
+ - text: "Howl uncontrollably for no reason pushes butt to face eat a plant, kill a hand for cough hairball on conveniently placed pants."
+ origin: 850 600
+ size: 10
+ color: blue
+ - text: "Wake up human for food at 4am. Decide to want nothing to do with my owner today pelt around the house and up and down stairs chasing phantoms."
+ origin: 850 620
+ size: 8
+ color: blue
+ - text: "Poop on grasses."
+ origin: 850 640
+ size: 16
+ color: blue
+ - text: "Hunt by meowing loudly at 5am next to human slave food dispenser bleghbleghvomit my"
+ origin: 850 660
+ size: 10
+ color: blue
+ - text: "furball really tie the room together and throwup on your pillow, so scamper."
+ origin: 850 670
+ size: 10
+ color: blue
+ - text: "Spread kitty litter all over house."
+ origin: 850 690
+ size: 18
+ color: blue
+ - text: "Friends are not food curl into a furry donut or going to catch the red"
+ origin: 850 720
+ size: 16
+ color: blue
+ - text: "dot today going to catch the red dot today or destroy the blinds or purr."
+ origin: 850 740
+ size: 16
+ color: blue
diff --git a/gfx/wr/wrench/benchmarks/transforms-simple.yaml b/gfx/wr/wrench/benchmarks/transforms-simple.yaml
new file mode 100644
index 0000000000..53956da401
--- /dev/null
+++ b/gfx/wr/wrench/benchmarks/transforms-simple.yaml
@@ -0,0 +1,44 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1024, 1024]
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1024, 1024]
+ transform: rotate(45)
+ items:
+ - type: rect
+ bounds: [0, 0, 1024, 1024]
+ color: [255, 0, 0, 0.5]
+ - type: rect
+ bounds: [0, 0, 1024, 1024]
+ color: [255, 0, 0, 0.5]
+ - type: rect
+ bounds: [0, 0, 1024, 1024]
+ color: [255, 0, 0, 0.5]
+ - type: rect
+ bounds: [0, 0, 1024, 1024]
+ color: [255, 0, 0, 0.5]
+ - type: rect
+ bounds: [0, 0, 1024, 1024]
+ color: [255, 0, 0, 0.5]
+ - type: rect
+ bounds: [0, 0, 1024, 1024]
+ color: [255, 0, 0, 0.5]
+ - type: rect
+ bounds: [0, 0, 1024, 1024]
+ color: [255, 0, 0, 0.5]
+ - type: rect
+ bounds: [0, 0, 1024, 1024]
+ color: [255, 0, 0, 0.5]
+ - type: rect
+ bounds: [0, 0, 1024, 1024]
+ color: [255, 0, 0, 0.5]
+ - type: rect
+ bounds: [0, 0, 1024, 1024]
+ color: [255, 0, 0, 0.5]
+ - type: rect
+ bounds: [0, 0, 1024, 1024]
+ color: [255, 0, 0, 0.5]
+
diff --git a/gfx/wr/wrench/benchmarks/unaligned-gradient.yaml b/gfx/wr/wrench/benchmarks/unaligned-gradient.yaml
new file mode 100644
index 0000000000..ea7738202d
--- /dev/null
+++ b/gfx/wr/wrench/benchmarks/unaligned-gradient.yaml
@@ -0,0 +1,62 @@
+root:
+ items:
+ - type: gradient
+ bounds: [ 0, 0, 1980, 1080]
+ start: [ 0, -2000 ]
+ end: [ 1, 4000 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: gradient
+ bounds: [ 0, 0, 1980, 1080]
+ start: [ 0, -2000 ]
+ end: [ 1, 4000 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: gradient
+ bounds: [ 0, 0, 1980, 1080]
+ start: [ 0, -2000 ]
+ end: [ 1, 4000 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: gradient
+ bounds: [ 0, 0, 1980, 1080]
+ start: [ 0, -2000 ]
+ end: [ 1, 4000 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: gradient
+ bounds: [ 0, 0, 1980, 1080]
+ start: [ 0, -2000 ]
+ end: [ 1, 4000 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: gradient
+ bounds: [ 0, 0, 1980, 1080]
+ start: [ 0, -2000 ]
+ end: [ 1, 4000 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: gradient
+ bounds: [ 0, 0, 1980, 1080]
+ start: [ 0, -2000 ]
+ end: [ 1, 4000 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: gradient
+ bounds: [ 0, 0, 1980, 1080]
+ start: [ 0, -2000 ]
+ end: [ 1, 4000 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: gradient
+ bounds: [ 0, 0, 1980, 1080]
+ start: [ 0, -2000 ]
+ end: [ 1, 4000 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: gradient
+ bounds: [ 0, 0, 1980, 1080]
+ start: [ 0, -2000 ]
+ end: [ 1, 4000 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
diff --git a/gfx/wr/wrench/build.rs b/gfx/wr/wrench/build.rs
new file mode 100644
index 0000000000..974ef3180c
--- /dev/null
+++ b/gfx/wr/wrench/build.rs
@@ -0,0 +1,28 @@
+/* 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::env;
+use std::fs;
+use std::path::PathBuf;
+
+fn main() {
+ let target = env::var("TARGET").unwrap();
+ let out_dir = env::var_os("OUT_DIR").unwrap();
+ let out_dir = PathBuf::from(out_dir);
+
+ println!("cargo:rerun-if-changed=res/wrench.exe.manifest");
+ if target.contains("windows") {
+ let src = PathBuf::from("res/wrench.exe.manifest");
+ let mut dst = out_dir
+ .parent()
+ .unwrap()
+ .parent()
+ .unwrap()
+ .parent()
+ .unwrap()
+ .to_owned();
+ dst.push("wrench.exe.manifest");
+ fs::copy(&src, &dst).unwrap();
+ }
+}
diff --git a/gfx/wr/wrench/examples/animated.anim b/gfx/wr/wrench/examples/animated.anim
new file mode 100644
index 0000000000..ee834e6cc2
--- /dev/null
+++ b/gfx/wr/wrench/examples/animated.anim
@@ -0,0 +1,13 @@
+---
+ anim.color:
+ - red
+ - green
+ - blue
+ rect.pos:
+ - [100, 100, 100, 100]
+ - [100, 100, 120, 100]
+ - [100, 100, 140, 100]
+ bs.offset:
+ - [0, 0]
+ - [2, 0]
+ - [4, 0]
diff --git a/gfx/wr/wrench/examples/animated.yaml b/gfx/wr/wrench/examples/animated.yaml
new file mode 100644
index 0000000000..7f9026ad6e
--- /dev/null
+++ b/gfx/wr/wrench/examples/animated.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: key(rect.pos)
+ color: key(anim.color)
+ - type: box-shadow
+ bounds: [ 100, 300, 100, 100 ]
+ blur-radius: 3
+ border-radius: 10
+ color: blue
+ clip-mode: outset
+ offset: key(bs.offset)
diff --git a/gfx/wr/wrench/invalidation/basic.yaml b/gfx/wr/wrench/invalidation/basic.yaml
new file mode 100644
index 0000000000..c7d1b6f450
--- /dev/null
+++ b/gfx/wr/wrench/invalidation/basic.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ color: red
+ bounds: 100 100 500 100
diff --git a/gfx/wr/wrench/invalidation/composite_nop_1.yaml b/gfx/wr/wrench/invalidation/composite_nop_1.yaml
new file mode 100644
index 0000000000..ce6cc46977
--- /dev/null
+++ b/gfx/wr/wrench/invalidation/composite_nop_1.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ color: red
+ bounds: 100 100 100 100
diff --git a/gfx/wr/wrench/invalidation/composite_nop_2.yaml b/gfx/wr/wrench/invalidation/composite_nop_2.yaml
new file mode 100644
index 0000000000..a99663ed54
--- /dev/null
+++ b/gfx/wr/wrench/invalidation/composite_nop_2.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ color: red
+ bounds: 100 120 100 100
diff --git a/gfx/wr/wrench/reftests/aa/aa-dist-bug-ref.yaml b/gfx/wr/wrench/reftests/aa/aa-dist-bug-ref.yaml
new file mode 100644
index 0000000000..c83fff2a0b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/aa/aa-dist-bug-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ color: red
+ bounds: [340, 184, 50, 20]
diff --git a/gfx/wr/wrench/reftests/aa/aa-dist-bug.yaml b/gfx/wr/wrench/reftests/aa/aa-dist-bug.yaml
new file mode 100644
index 0000000000..bac51368e9
--- /dev/null
+++ b/gfx/wr/wrench/reftests/aa/aa-dist-bug.yaml
@@ -0,0 +1,48 @@
+# Test that when the AA range is > 1, the AA is correctly applied to
+# an ellipse clip. This is a regression test for issue #2576.
+# The rectangles below mask out most of the box shadow and rounded
+# corners, which are not relevant to what is being tested here.
+---
+root:
+ items:
+ - type: stacking-context
+ transform: rotate-z(-45) rotate-x(-60)
+ transform-origin: 300 300
+ items:
+ - type: box-shadow
+ bounds: [0, 0, 150, 150]
+ color: black
+ offset: [150, 50]
+ border-radius: 8
+
+ - type: clip
+ id: 2
+ complex:
+ - rect: [90, 0, 150, 150]
+ radius: 8
+
+ - type: clip-chain
+ id: 10
+ clips: [2]
+
+ - type: rect
+ color: red
+ border-radius: 8
+ bounds: [90, 0, 150, 150]
+ clip-chain: 10
+
+ - type: rect
+ color: white
+ bounds: [250, 100, 240, 84]
+
+ - type: rect
+ color: white
+ bounds: [250, 204, 240, 70]
+
+ - type: rect
+ color: white
+ bounds: [240, 100, 100, 150]
+
+ - type: rect
+ color: white
+ bounds: [390, 100, 100, 150]
diff --git a/gfx/wr/wrench/reftests/aa/fractional-radii-ref.yaml b/gfx/wr/wrench/reftests/aa/fractional-radii-ref.yaml
new file mode 100644
index 0000000000..364d6e7529
--- /dev/null
+++ b/gfx/wr/wrench/reftests/aa/fractional-radii-ref.yaml
@@ -0,0 +1,18 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: 20 50 100 100
+ color: blue
+
+ - type: rect
+ bounds: 140 50 100 100
+ color: blue
+
+ - type: rect
+ bounds: 260 50 100 100
+ color: blue
+
+ - type: rect
+ bounds: 380 50 100 100
+ color: blue
diff --git a/gfx/wr/wrench/reftests/aa/fractional-radii.yaml b/gfx/wr/wrench/reftests/aa/fractional-radii.yaml
new file mode 100644
index 0000000000..cf61cd27b6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/aa/fractional-radii.yaml
@@ -0,0 +1,54 @@
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [20, 50, 100, 100]
+ radius: 0
+ - type: clip-chain
+ id: 10
+ clips: [2]
+ - type: rect
+ bounds: 20 50 100 100
+ color: blue
+ clip-chain: 10
+
+ - type: clip
+ id: 3
+ complex:
+ - rect: [140, 50, 100, 100]
+ radius: 0.001
+ - type: clip-chain
+ id: 11
+ clips: [3]
+ - type: rect
+ bounds: 140 50 100 100
+ color: blue
+ clip-chain: 11
+
+ - type: clip
+ id: 4
+ complex:
+ - rect: [260, 50, 100, 100]
+ radius: 0.01
+ - type: clip-chain
+ id: 12
+ clips: [4]
+ - type: rect
+ bounds: 260 50 100 100
+ color: blue
+ clip-chain: 12
+
+ - type: clip
+ id: 5
+ complex:
+ - rect: [380, 50, 100, 100]
+ radius: 0.1
+ - type: clip-chain
+ id: 13
+ clips: [5]
+ - type: rect
+ bounds: 380 50 100 100
+ color: blue
+ clip-chain: 13
diff --git a/gfx/wr/wrench/reftests/aa/reftest.list b/gfx/wr/wrench/reftests/aa/reftest.list
new file mode 100644
index 0000000000..a998efef25
--- /dev/null
+++ b/gfx/wr/wrench/reftests/aa/reftest.list
@@ -0,0 +1,3 @@
+skip_on(android) fuzzy(1,1) fuzzy-if(platform(swgl),4,27) == rounded-rects.yaml rounded-rects-ref.png # Too wide for Android
+== aa-dist-bug.yaml aa-dist-bug-ref.yaml
+fuzzy-if(env(android,device),6,792) == fractional-radii.yaml fractional-radii-ref.yaml
diff --git a/gfx/wr/wrench/reftests/aa/rounded-rects-ref.png b/gfx/wr/wrench/reftests/aa/rounded-rects-ref.png
new file mode 100644
index 0000000000..73fb98446b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/aa/rounded-rects-ref.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/aa/rounded-rects.yaml b/gfx/wr/wrench/reftests/aa/rounded-rects.yaml
new file mode 100644
index 0000000000..9554707e31
--- /dev/null
+++ b/gfx/wr/wrench/reftests/aa/rounded-rects.yaml
@@ -0,0 +1,56 @@
+---
+root:
+ items:
+ -
+ bounds: 0 0 1000 1000
+ type: stacking-context
+ items:
+ - type: clip
+ id: 3
+ complex:
+ - rect: [50, 50, 200, 200]
+ radius: 8
+ -
+ type: "clip-chain"
+ id: 10
+ clips: [3]
+
+ - type: rect
+ bounds: 50 50 200 200
+ color: red
+ clip-chain: 10
+
+ - type: clip
+ id: 4
+ complex:
+ - rect: [270, 50, 200, 200]
+ radius: [16, 32, 48, 64]
+ -
+ type: "clip-chain"
+ id: 11
+ clips: [4]
+
+ - type: rect
+ bounds: 270 50 200 200
+ color: green
+ clip-chain: 11
+
+ - type: clip
+ id: 5
+ complex:
+ - rect: [490, 50, 500, 500]
+ radius: {
+ top-left: [32, 16],
+ top-right: [40, 24],
+ bottom-left: [48, 64],
+ bottom-right: [52, 80],
+ }
+ -
+ type: "clip-chain"
+ id: 12
+ clips: [5]
+
+ - type: rect
+ bounds: 490 50 500 500
+ color: blue
+ clip-chain: 12 \ No newline at end of file
diff --git a/gfx/wr/wrench/reftests/backface/backface-3d-leaf.yaml b/gfx/wr/wrench/reftests/backface/backface-3d-leaf.yaml
new file mode 100644
index 0000000000..1a7be55eae
--- /dev/null
+++ b/gfx/wr/wrench/reftests/backface/backface-3d-leaf.yaml
@@ -0,0 +1,18 @@
+# In this test, the leaf green rectangle has only its back visible
+# due to rotate-x(180) transformation, preserve-3d style,
+# and "backface-visible = false", so it's completely hidden.
+---
+root:
+ items:
+ - type: rect
+ color: red
+ bounds: 0 0 1024 768
+ - type: stacking-context
+ bounds: 0 0 1024 768
+ transform-style: preserve-3d
+ transform: rotate-x(180)
+ items:
+ - type: rect
+ bounds: 0 0 200 200
+ color: green
+ backface-visible: false
diff --git a/gfx/wr/wrench/reftests/backface/backface-both-sides-ref.yaml b/gfx/wr/wrench/reftests/backface/backface-both-sides-ref.yaml
new file mode 100644
index 0000000000..aa0bd8e57c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/backface/backface-both-sides-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: stacking-context
+ items:
+ - type: rect
+ color: red
+ bounds: 100 0 100 100
diff --git a/gfx/wr/wrench/reftests/backface/backface-both-sides.yaml b/gfx/wr/wrench/reftests/backface/backface-both-sides.yaml
new file mode 100644
index 0000000000..0212c3002d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/backface/backface-both-sides.yaml
@@ -0,0 +1,25 @@
+# In this test, both rectangles have invisible back faces and are
+# parented to a preserve-3d context that is rotated by 180 degrees.
+# The red one is also rotated 180 degrees, and should be visible.
+---
+root:
+ items:
+ - type: stacking-context
+ items:
+ - type: stacking-context
+ transform: rotate-y(180)
+ transform-style: preserve-3d
+ transform-origin: 50 50
+ items:
+ - type: rect
+ color: green
+ bounds: 0 0 100 100
+ backface-visible: false
+ - type: stacking-context
+ transform: rotate-y(180)
+ transform-origin: 0 0
+ backface-visible: false
+ items:
+ - type: rect
+ color: red
+ bounds: 0 0 100 100
diff --git a/gfx/wr/wrench/reftests/backface/backface-double-flip.yaml b/gfx/wr/wrench/reftests/backface/backface-double-flip.yaml
new file mode 100644
index 0000000000..b242e95627
--- /dev/null
+++ b/gfx/wr/wrench/reftests/backface/backface-double-flip.yaml
@@ -0,0 +1,21 @@
+# In this test, the red rectangle has backface visibility turned off.
+# However its world transformation makes it front-facing.
+# However it's still invisible because it's back-facing in the picture space
+# of the transformed stacking context.
+---
+root:
+ items:
+ - type: stacking-context
+ items:
+ - type: stacking-context
+ transform: rotate-y(180)
+ transform-style: preserve-3d
+ transform-origin: 50 50
+ items:
+ - type: stacking-context
+ transform: rotate-y(180)
+ items:
+ - type: rect
+ color: red
+ backface-visible: false
+ bounds: 0 0 100 100
diff --git a/gfx/wr/wrench/reftests/backface/backface-flatten.yaml b/gfx/wr/wrench/reftests/backface/backface-flatten.yaml
new file mode 100644
index 0000000000..e754d45bc9
--- /dev/null
+++ b/gfx/wr/wrench/reftests/backface/backface-flatten.yaml
@@ -0,0 +1,31 @@
+# In this test, there is a flattened ref frame between an element
+# and the picture it's rendered into. The element should be considered
+# invisible as it's back face oriented at this flattening step.
+# If WR only checks the "final" transform, it will consider it visible.
+
+---
+root:
+ transform: rotate-x(1)
+ items:
+ - type: stacking-context
+ transform-style: preserve-3d
+ #transform: rotate-x(1)
+ items:
+ - type: stacking-context
+ bounds: 0 0 200 200
+ transform: rotate-y(30)
+ transform-style: flat
+ items:
+ - type: stacking-context
+ bounds: 0 0 200 200
+ transform: rotate-y(-100)
+ transform-style: preserve-3d
+ items:
+ - type: stacking-context
+ #transform: rotate-x(1)
+ transform-style: flat
+ items:
+ - type: rect
+ bounds: 0 0 200 200
+ color: green
+ backface-visible: false
diff --git a/gfx/wr/wrench/reftests/backface/backface-hidden.yaml b/gfx/wr/wrench/reftests/backface/backface-hidden.yaml
new file mode 100644
index 0000000000..afa9d653b8
--- /dev/null
+++ b/gfx/wr/wrench/reftests/backface/backface-hidden.yaml
@@ -0,0 +1,18 @@
+# In this test, the leaf green rectangle has only its back visible
+# due to rotate-x(180) transformation,
+# and "backface-visible = false", so it's completely hidden.
+---
+root:
+ items:
+ - type: rect
+ color: red
+ bounds: 0 0 1024 768
+ - type: stacking-context
+ bounds: 0 0 1024 768
+ transform: rotate-x(180)
+ transform-style: preserve-3d
+ items:
+ - type: rect
+ bounds: 0 0 1024 768
+ color: green
+ backface-visible: false
diff --git a/gfx/wr/wrench/reftests/backface/backface-leaf-ref.yaml b/gfx/wr/wrench/reftests/backface/backface-leaf-ref.yaml
new file mode 100644
index 0000000000..17d39b4d61
--- /dev/null
+++ b/gfx/wr/wrench/reftests/backface/backface-leaf-ref.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: rect
+ color: red
+ bounds: 0 0 1024 768
+ - type: stacking-context
+ bounds: 0 0 1024 768
+ items:
+ - type: rect
+ bounds: 0 568 200 200
+ color: green
diff --git a/gfx/wr/wrench/reftests/backface/backface-leaf.yaml b/gfx/wr/wrench/reftests/backface/backface-leaf.yaml
new file mode 100644
index 0000000000..d1c1b893be
--- /dev/null
+++ b/gfx/wr/wrench/reftests/backface/backface-leaf.yaml
@@ -0,0 +1,16 @@
+# In this test, the leaf green rectangle "backface-visible = false" which is ignored
+# because it doesn't have any transform, and it's not in preserve-3d context.
+---
+root:
+ items:
+ - type: rect
+ color: red
+ bounds: 0 0 1024 768
+ - type: stacking-context
+ bounds: 0 0 1024 768
+ transform: rotate-x(180)
+ items:
+ - type: rect
+ bounds: 0 0 200 200
+ color: green
+ backface-visible: false
diff --git a/gfx/wr/wrench/reftests/backface/backface-perspective-ref.yaml b/gfx/wr/wrench/reftests/backface/backface-perspective-ref.yaml
new file mode 100644
index 0000000000..1427b57dd9
--- /dev/null
+++ b/gfx/wr/wrench/reftests/backface/backface-perspective-ref.yaml
@@ -0,0 +1,16 @@
+root:
+ items:
+ - type: stacking-context
+ bounds: 0 0 300 100
+ transform-style: flat
+ perspective: 100
+ items:
+ - type: stacking-context
+ bounds: 0 0 100 100
+ transform-style: flat
+ transform: rotate-y(-120)
+ backface-visible: true
+ items:
+ - type: rect
+ bounds: 0 0 100 100
+ color: red
diff --git a/gfx/wr/wrench/reftests/backface/backface-perspective.yaml b/gfx/wr/wrench/reftests/backface/backface-perspective.yaml
new file mode 100644
index 0000000000..cb52c9a584
--- /dev/null
+++ b/gfx/wr/wrench/reftests/backface/backface-perspective.yaml
@@ -0,0 +1,20 @@
+# Checks a special case where CSS has both perspective and a transform.
+# In this case, the backface visibility should be computed based on the perspective
+# parent, not the immediate parent.
+# The immediate parent is `rotate-y(-120)`, which is obviously back facing.
+# But perspective transform makes it front-facing (since the frustum side panels are rotated even more).
+root:
+ items:
+ - type: stacking-context
+ bounds: 0 0 300 100
+ perspective: 100
+ items:
+ - type: stacking-context
+ bounds: 0 0 100 100
+ transform: rotate-y(-120)
+ paired-with-perspective: true
+ backface-visible: false
+ items:
+ - type: rect
+ bounds: 0 0 100 100
+ color: red
diff --git a/gfx/wr/wrench/reftests/backface/backface-picture-ref.yaml b/gfx/wr/wrench/reftests/backface/backface-picture-ref.yaml
new file mode 100644
index 0000000000..3013f72f20
--- /dev/null
+++ b/gfx/wr/wrench/reftests/backface/backface-picture-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ color: red
+ bounds: 0 0 100 100
diff --git a/gfx/wr/wrench/reftests/backface/backface-picture.yaml b/gfx/wr/wrench/reftests/backface/backface-picture.yaml
new file mode 100644
index 0000000000..189c23d5b5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/backface/backface-picture.yaml
@@ -0,0 +1,21 @@
+# In this test we ensure that "backface-visiblity" property
+# is not inherited, and the red rect rotated by 180 degrees
+# is still rendered.
+---
+root:
+ items:
+ - type: stacking-context
+ items:
+ - type: stacking-context
+ transform-style: preserve-3d
+ backface-visible: false
+ items:
+ - type: stacking-context
+ transform: rotate-y(180)
+ transform-origin: 50 50
+ items:
+ - type: stacking-context
+ items:
+ - type: rect
+ color: red
+ bounds: 0 0 100 100
diff --git a/gfx/wr/wrench/reftests/backface/backface-ref.yaml b/gfx/wr/wrench/reftests/backface/backface-ref.yaml
new file mode 100644
index 0000000000..614cf5f465
--- /dev/null
+++ b/gfx/wr/wrench/reftests/backface/backface-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ color: red
+ bounds: 0 0 1024 768
diff --git a/gfx/wr/wrench/reftests/backface/backface-sc.yaml b/gfx/wr/wrench/reftests/backface/backface-sc.yaml
new file mode 100644
index 0000000000..350baca592
--- /dev/null
+++ b/gfx/wr/wrench/reftests/backface/backface-sc.yaml
@@ -0,0 +1,17 @@
+# This test is similar to "backface-leaf.yaml" but with the whole
+# stacking context (containing a green rect) turning invisible
+# because of the "rotate-x(180)" transform.
+---
+root:
+ items:
+ - type: rect
+ color: red
+ bounds: 0 0 1024 768
+ - type: stacking-context
+ bounds: 0 0 1024 768
+ transform: rotate-x(180)
+ backface-visible: false
+ items:
+ - type: rect
+ bounds: 0 0 200 200
+ color: green
diff --git a/gfx/wr/wrench/reftests/backface/backface-vis-3d.yaml b/gfx/wr/wrench/reftests/backface/backface-vis-3d.yaml
new file mode 100644
index 0000000000..c58026a5cc
--- /dev/null
+++ b/gfx/wr/wrench/reftests/backface/backface-vis-3d.yaml
@@ -0,0 +1,16 @@
+# Verifies that a 3d context selects the correct reference (containing block) spatial node.
+root:
+ items:
+ - type: stacking-context
+ bounds: 100 0 100 100
+ transform: scale-x(-1)
+ items:
+ - type: stacking-context
+ bounds: 0 0 100 100
+ transform: rotate-y(180)
+ transform-style: preserve-3d
+ items:
+ - type: rect
+ bounds: 0 0 100 100
+ color: red
+ backface-visible: false
diff --git a/gfx/wr/wrench/reftests/backface/blank.yaml b/gfx/wr/wrench/reftests/backface/blank.yaml
new file mode 100644
index 0000000000..c4eb3ab673
--- /dev/null
+++ b/gfx/wr/wrench/reftests/backface/blank.yaml
@@ -0,0 +1,2 @@
+---
+root:
diff --git a/gfx/wr/wrench/reftests/backface/reftest.list b/gfx/wr/wrench/reftests/backface/reftest.list
new file mode 100644
index 0000000000..21a5e53d0c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/backface/reftest.list
@@ -0,0 +1,10 @@
+== backface-leaf.yaml backface-leaf-ref.yaml
+== backface-3d-leaf.yaml backface-ref.yaml
+== backface-hidden.yaml backface-ref.yaml
+== backface-sc.yaml backface-ref.yaml
+== backface-picture.yaml backface-picture-ref.yaml
+== backface-double-flip.yaml blank.yaml
+== backface-both-sides.yaml backface-both-sides-ref.yaml
+== backface-vis-3d.yaml blank.yaml
+== backface-flatten.yaml blank.yaml
+fuzzy(1,19) == backface-perspective.yaml backface-perspective-ref.yaml
diff --git a/gfx/wr/wrench/reftests/blend/backdrop-filter-blend-container-ref.yaml b/gfx/wr/wrench/reftests/blend/backdrop-filter-blend-container-ref.yaml
new file mode 100644
index 0000000000..108bdec453
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/backdrop-filter-blend-container-ref.yaml
@@ -0,0 +1,8 @@
+# verify that the results of a root-level mix-blend are available as
+# input to a backdrop-filter following that follows the blend container
+---
+root:
+ items:
+ - type: rect
+ bounds: 0 0 100 100
+ color: cyan
diff --git a/gfx/wr/wrench/reftests/blend/backdrop-filter-blend-container.yaml b/gfx/wr/wrench/reftests/blend/backdrop-filter-blend-container.yaml
new file mode 100644
index 0000000000..899e39ab2e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/backdrop-filter-blend-container.yaml
@@ -0,0 +1,21 @@
+# verify that the results of a root-level mix-blend are available as
+# input to a backdrop-filter following that follows the blend container
+---
+root:
+ items:
+ - type: stacking-context
+ blend-container: true
+ items:
+ - type: rect
+ bounds: 0 0 100 100
+ color: red
+ - type: stacking-context
+ bounds: 0 0 100 100
+ mix-blend-mode: multiply
+ items:
+ - type: rect
+ bounds: 0 0 100 100
+ color: red
+ - type: backdrop-filter
+ bounds: 0 0 100 100
+ filters: invert(1)
diff --git a/gfx/wr/wrench/reftests/blend/backdrop-filter-preceding-ref.yaml b/gfx/wr/wrench/reftests/blend/backdrop-filter-preceding-ref.yaml
new file mode 100644
index 0000000000..25069e8bb0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/backdrop-filter-preceding-ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: 0 0 100 100
+ color: cyan
+ - type: rect
+ bounds: 100 0 100 100
+ color: magenta
diff --git a/gfx/wr/wrench/reftests/blend/backdrop-filter-preceding.yaml b/gfx/wr/wrench/reftests/blend/backdrop-filter-preceding.yaml
new file mode 100644
index 0000000000..74753c2f2d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/backdrop-filter-preceding.yaml
@@ -0,0 +1,24 @@
+# verify that content preceding a root level mix-blend container is still
+# available as part of the backdrop root for subsequent backdrop-filters
+---
+root:
+ items:
+ - type: rect
+ bounds: 0 0 100 100
+ color: red
+ - type: stacking-context
+ blend-container: true
+ items:
+ - type: rect
+ bounds: 100 0 100 100
+ color: green
+ - type: stacking-context
+ bounds: 100 0 100 100
+ mix-blend-mode: multiply
+ items:
+ - type: rect
+ bounds: 0 0 100 100
+ color: green
+ - type: backdrop-filter
+ bounds: 0 0 200 100
+ filters: invert(1)
diff --git a/gfx/wr/wrench/reftests/blend/blank.yaml b/gfx/wr/wrench/reftests/blend/blank.yaml
new file mode 100644
index 0000000000..c4eb3ab673
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/blank.yaml
@@ -0,0 +1,2 @@
+---
+root:
diff --git a/gfx/wr/wrench/reftests/blend/blend-overflow-ref.yaml b/gfx/wr/wrench/reftests/blend/blend-overflow-ref.yaml
new file mode 100644
index 0000000000..6c0c71f53f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/blend-overflow-ref.yaml
@@ -0,0 +1,14 @@
+# Ensure that we correctly calculate the UV sampling rect for the backdrop
+# Similar to the mix-blend-mode-overflowing-child.html test in Gecko
+---
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: green
+ - type: rect
+ bounds: [50, 50, 100, 100]
+ color: green
+ - type: rect
+ bounds: [50, 50, 50, 50]
+ color: black
diff --git a/gfx/wr/wrench/reftests/blend/blend-overflow.yaml b/gfx/wr/wrench/reftests/blend/blend-overflow.yaml
new file mode 100644
index 0000000000..a0a0aa53b1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/blend-overflow.yaml
@@ -0,0 +1,18 @@
+# Ensure that we correctly calculate the UV sampling rect for the backdrop
+# Similar to the mix-blend-mode-overflowing-child.html test in Gecko
+---
+root:
+ items:
+ - type: stacking-context
+ blend-container: true
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: green
+ - type: stacking-context
+ bounds: [50, 50, 100, 100]
+ mix-blend-mode: difference
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: green
diff --git a/gfx/wr/wrench/reftests/blend/child-surface-ref.yaml b/gfx/wr/wrench/reftests/blend/child-surface-ref.yaml
new file mode 100644
index 0000000000..28e8c042d9
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/child-surface-ref.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: stacking-context
+ blend-container: true
+ bounds: [0, 0, 100, 100]
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [255, 0, 0]
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ mix-blend-mode: hue
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [255, 255, 0]
diff --git a/gfx/wr/wrench/reftests/blend/child-surface.yaml b/gfx/wr/wrench/reftests/blend/child-surface.yaml
new file mode 100644
index 0000000000..983ecc17c1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/child-surface.yaml
@@ -0,0 +1,22 @@
+# verify that the clipping_rect of a child surface (caused by the identity filter)
+# is corrected used to select the backdrop for a mix-blend child surface
+---
+root:
+ items:
+ - type: stacking-context
+ filters: [identity]
+ items:
+ - type: stacking-context
+ blend-container: true
+ bounds: [0, 0, 100, 100]
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [255, 0, 0]
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ mix-blend-mode: hue
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [255, 255, 0]
diff --git a/gfx/wr/wrench/reftests/blend/darken-ref.yaml b/gfx/wr/wrench/reftests/blend/darken-ref.yaml
new file mode 100644
index 0000000000..a83cf68244
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/darken-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [10, 20, 10]
diff --git a/gfx/wr/wrench/reftests/blend/darken.yaml b/gfx/wr/wrench/reftests/blend/darken.yaml
new file mode 100644
index 0000000000..2de68d8724
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/darken.yaml
@@ -0,0 +1,16 @@
+---
+root:
+ items:
+ - type: stacking-context
+ blend-container: true
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [10, 20, 30]
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ mix-blend-mode: darken
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [30, 20, 10]
diff --git a/gfx/wr/wrench/reftests/blend/difference-ref.yaml b/gfx/wr/wrench/reftests/blend/difference-ref.yaml
new file mode 100644
index 0000000000..b913384f24
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/difference-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [0, 0, 0]
diff --git a/gfx/wr/wrench/reftests/blend/difference-transparent-ref.yaml b/gfx/wr/wrench/reftests/blend/difference-transparent-ref.yaml
new file mode 100644
index 0000000000..8ed801fb15
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/difference-transparent-ref.yaml
@@ -0,0 +1,23 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+
+ # First blend black rect with green in place
+ # Cs = (1 - αb) x Cs + αb x B(Cb, Cs)
+ # B(Cb, Cs) = | Cb - Cs | = (0, 0, 0)
+ # 0.5 * (0, 255, 0) + 0.5 * (0, 0, 0) = (0, 127.5, 0)
+ #
+ # Now, composite the resulting color with src-over; the alpha is the original alpha for the top layer
+ # while the color is the blending result
+ # co = αs x Fa x Cs + αb x Fb x Cb - this is premultiplied
+ # αo = αs + αb x (1 – αs)
+ # Source over: Fa = 1; Fb = 1 – αs
+ # co = 0.5 * 1 * (0, 127.5, 0) + 0.5 * 0.5 * (0, 255, 0) = (0, 63.5, 0) + 0.25 * (0, 255, 0) = (0, 127.5, 0)
+ # ao = 0.5 + 0.5 * 0.5 = 0.75
+ # Co = co/ao = (0, 127.5, 0) / 0.75
+ #
+ # Now alpha composite on white background
+ # co = 0.75 * 1 * (0, 127.5, 0) / 0.75 + 1 * 0.25 * (255, 255, 255) = (0, 127.5, 0) + (63.75, 63.75, 63.75) = (63.75, 159, 63.75) = (64, 191, 64)
+ color: [64, 191, 64]
diff --git a/gfx/wr/wrench/reftests/blend/difference-transparent.yaml b/gfx/wr/wrench/reftests/blend/difference-transparent.yaml
new file mode 100644
index 0000000000..bd828d454a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/difference-transparent.yaml
@@ -0,0 +1,22 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: white
+ # this stacking context should create an isolated group for its children
+ # causing the yellow rect to not blend with the green backdrop
+ - type: stacking-context
+ blend-container: true
+ bounds: [0, 0, 100, 100]
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [0, 255, 0, 0.5]
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ mix-blend-mode: difference
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [0, 255, 0, 0.5]
diff --git a/gfx/wr/wrench/reftests/blend/difference.yaml b/gfx/wr/wrench/reftests/blend/difference.yaml
new file mode 100644
index 0000000000..0c8d0fcd02
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/difference.yaml
@@ -0,0 +1,16 @@
+---
+root:
+ items:
+ - type: stacking-context
+ blend-container: true
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: green
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ mix-blend-mode: difference
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: green
diff --git a/gfx/wr/wrench/reftests/blend/isolated-2-ref.yaml b/gfx/wr/wrench/reftests/blend/isolated-2-ref.yaml
new file mode 100644
index 0000000000..3226b6edda
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/isolated-2-ref.yaml
@@ -0,0 +1,21 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [10, 10, 130, 130]
+ color: [255, 255, 0]
+ - type: stacking-context
+ bounds: [10, 10, 130, 130]
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [0, 255, 0]
+ - type: stacking-context
+ bounds: [20, 20, 100, 100]
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [255, 0, 0]
+ - type: rect
+ bounds: [0, 0, 80, 80]
+ color: [0, 0, 0]
diff --git a/gfx/wr/wrench/reftests/blend/isolated-2.yaml b/gfx/wr/wrench/reftests/blend/isolated-2.yaml
new file mode 100644
index 0000000000..87bbac3caa
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/isolated-2.yaml
@@ -0,0 +1,25 @@
+# translation of wpt/css-tests/compositing-1_dev/html/mix-blend-mode-stacking-context-creates-isolation.htm
+---
+root:
+ items:
+ - type: rect
+ bounds: [10, 10, 130, 130]
+ color: [255, 255, 0]
+ # this stacking context should create an isolated group for its children
+ # inside there should be overlapping red and green rects
+ # where they intersect should be a black rect
+ # the rects should not blend with the yellow backdrop
+ - type: stacking-context
+ blend-container: true
+ bounds: [10, 10, 130, 130]
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [0, 255, 0]
+ - type: stacking-context
+ bounds: [20, 20, 100, 100]
+ mix-blend-mode: multiply
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [255, 0, 0]
diff --git a/gfx/wr/wrench/reftests/blend/isolated-premultiplied-2-ref.yaml b/gfx/wr/wrench/reftests/blend/isolated-premultiplied-2-ref.yaml
new file mode 100644
index 0000000000..f5de6dc33d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/isolated-premultiplied-2-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [229, 239, 229]
diff --git a/gfx/wr/wrench/reftests/blend/isolated-premultiplied-2.yaml b/gfx/wr/wrench/reftests/blend/isolated-premultiplied-2.yaml
new file mode 100644
index 0000000000..6836fedd84
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/isolated-premultiplied-2.yaml
@@ -0,0 +1,14 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ blend-container: true
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ mix-blend-mode: lighten
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [0, 100, 0, 0.1]
diff --git a/gfx/wr/wrench/reftests/blend/isolated-premultiplied.yaml b/gfx/wr/wrench/reftests/blend/isolated-premultiplied.yaml
new file mode 100644
index 0000000000..a8d738a389
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/isolated-premultiplied.yaml
@@ -0,0 +1,24 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ items:
+ # this stacking context should force its parent to be an isolated group
+ # we don't want it to actually draw anything so just make it draw a white rect
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ mix-blend-mode: lighten
+ items:
+ - type: rect
+ bounds: [500, 500, 100, 100]
+ color: [255, 255, 255]
+
+ # transparent white, should be invisible
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [255, 255, 255, 0.5]
+ # transparent white, should be invisible
+ - type: image
+ bounds: [200, 0, 100, 100]
+ src: "transparent-white.png"
diff --git a/gfx/wr/wrench/reftests/blend/isolated-ref.yaml b/gfx/wr/wrench/reftests/blend/isolated-ref.yaml
new file mode 100644
index 0000000000..0f9061997a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/isolated-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [255, 255, 0]
diff --git a/gfx/wr/wrench/reftests/blend/isolated-with-filter.yaml b/gfx/wr/wrench/reftests/blend/isolated-with-filter.yaml
new file mode 100644
index 0000000000..0635b15c5e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/isolated-with-filter.yaml
@@ -0,0 +1,19 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [255, 0, 0]
+ # the presence of this filter shouldn't break isolated groups
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ filters: opacity(1.0)
+ blend-container: true
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ mix-blend-mode: difference
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [255, 255, 0]
diff --git a/gfx/wr/wrench/reftests/blend/isolated.yaml b/gfx/wr/wrench/reftests/blend/isolated.yaml
new file mode 100644
index 0000000000..fcfd0676d7
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/isolated.yaml
@@ -0,0 +1,19 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [255, 0, 0]
+ # this stacking context should create an isolated group for its children
+ # causing the yellow rect to not blend with the green backdrop
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ blend-container: true
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ mix-blend-mode: difference
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [255, 255, 0]
diff --git a/gfx/wr/wrench/reftests/blend/large-ref.yaml b/gfx/wr/wrench/reftests/blend/large-ref.yaml
new file mode 100644
index 0000000000..c7e8433414
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/large-ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: 0 0 2000 2000
+ items:
+ - type: rect
+ bounds: 0 0 2000 2000
+ color: [0, 128, 0, 1]
diff --git a/gfx/wr/wrench/reftests/blend/large.yaml b/gfx/wr/wrench/reftests/blend/large.yaml
new file mode 100644
index 0000000000..5f15cfe332
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/large.yaml
@@ -0,0 +1,14 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: 0 0 2000 2000
+ blend-container: true
+ items:
+ - type: stacking-context
+ bounds: 0 0 2000 2000
+ mix-blend-mode: screen
+ items:
+ - type: rect
+ bounds: 0 0 2000 2000
+ color: [0, 128, 0, 1]
diff --git a/gfx/wr/wrench/reftests/blend/lighten-ref.yaml b/gfx/wr/wrench/reftests/blend/lighten-ref.yaml
new file mode 100644
index 0000000000..5720f140a6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/lighten-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [30, 20, 30]
diff --git a/gfx/wr/wrench/reftests/blend/lighten.yaml b/gfx/wr/wrench/reftests/blend/lighten.yaml
new file mode 100644
index 0000000000..426e2323b9
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/lighten.yaml
@@ -0,0 +1,16 @@
+---
+root:
+ items:
+ - type: stacking-context
+ blend-container: true
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [10, 20, 30]
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ mix-blend-mode: lighten
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [30, 20, 10]
diff --git a/gfx/wr/wrench/reftests/blend/mix-blend-complex-transform.png b/gfx/wr/wrench/reftests/blend/mix-blend-complex-transform.png
new file mode 100644
index 0000000000..7929f27892
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/mix-blend-complex-transform.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/blend/mix-blend-complex-transform.yaml b/gfx/wr/wrench/reftests/blend/mix-blend-complex-transform.yaml
new file mode 100644
index 0000000000..b713e96c75
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/mix-blend-complex-transform.yaml
@@ -0,0 +1,56 @@
+---
+root:
+ items:
+ - type: stacking-context
+ transform: [
+ 1, 0, 0, 0,
+ 0.3443276, 1, 0, 0,
+ 0, 0, 1, 0,
+ -1822.09131, 0, 0, 1
+ ]
+ bounds: 12 12 0 0
+ items:
+ - type: clip
+ bounds: 0 0 1884 1290
+ id: 2
+ -
+ type: "clip-chain"
+ id: 3
+ clips: [2]
+ - type: stacking-context
+ transform: [
+ 1, 0, 0, 0,
+ -0.3443276, 1, 0, 0,
+ 0, 0, 1, 0,
+ 444.18262, 0, 0, 1
+ ]
+ items:
+ - type: stacking-context
+ clip-chain: 3
+ transform: [
+ 1, 0, 0, 0,
+ 0.3443276, 1, 0, 0,
+ 0, 0, 1, 0,
+ -444.18262, 0, 0, 1
+ ]
+ items:
+ - type: stacking-context
+ items:
+ - type: stacking-context
+ blend-container: true
+ items:
+ - type: stacking-context
+ mix-blend-mode: multiply
+ items:
+ - type: stacking-context
+ bounds: 1436 0 0 0
+ transform: [
+ 1, 0, 0, 0,
+ -0.3443276, 1, 0, 0,
+ 0, 0, 1, 0,
+ 444.18262, 0, 0, 1
+ ]
+ items:
+ - type: rect
+ color: red
+ bounds: 0 0 113.025 1290
diff --git a/gfx/wr/wrench/reftests/blend/mix-blend-invalid-backdrop-ref.yaml b/gfx/wr/wrench/reftests/blend/mix-blend-invalid-backdrop-ref.yaml
new file mode 100644
index 0000000000..1e204cb338
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/mix-blend-invalid-backdrop-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: red
diff --git a/gfx/wr/wrench/reftests/blend/mix-blend-invalid-backdrop.yaml b/gfx/wr/wrench/reftests/blend/mix-blend-invalid-backdrop.yaml
new file mode 100644
index 0000000000..4a5f97f381
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/mix-blend-invalid-backdrop.yaml
@@ -0,0 +1,33 @@
+# Test that if the parent surface is clipped such that there
+# is no backdrop rect available, no crash occurs and output
+# is as expected (a no-op mix-blend)
+---
+root:
+ items:
+ # Ensure a filter is placed here to force this mix-blend to isolate from
+ # the tile cache backdrop - otherwise the surface won't get clipped, which
+ # is what we're trying to test.
+ - type: stacking-context
+ filters: [identity]
+ items:
+ - type: clip
+ id: 2
+ bounds: [0, 0, 100, 100]
+ -
+ type: "clip-chain"
+ id: 3
+ clips: [2]
+ - type: stacking-context
+ blend-container: true
+ clip-chain: 3
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: red
+ - type: stacking-context
+ bounds: [100, 0, 100, 100]
+ mix-blend-mode: multiply
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: green
diff --git a/gfx/wr/wrench/reftests/blend/multi-mix-blend-mode-ref.yaml b/gfx/wr/wrench/reftests/blend/multi-mix-blend-mode-ref.yaml
new file mode 100644
index 0000000000..ae366a09e0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/multi-mix-blend-mode-ref.yaml
@@ -0,0 +1,14 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: 0 0 800 200
+ color: [255, 0, 0, 1]
+
+ - type: rect
+ bounds: 50 50 100 100
+ color: [255, 255, 0, 1]
+
+ - type: rect
+ bounds: 300 50 400 100
+ color: [255, 0, 255, 1]
diff --git a/gfx/wr/wrench/reftests/blend/multi-mix-blend-mode.yaml b/gfx/wr/wrench/reftests/blend/multi-mix-blend-mode.yaml
new file mode 100644
index 0000000000..d3ba8fcaf0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/multi-mix-blend-mode.yaml
@@ -0,0 +1,25 @@
+---
+root:
+ items:
+ - type: stacking-context
+ blend-container: true
+ items:
+ - type: rect
+ bounds: 0 0 800 200
+ color: [255, 0, 0, 1]
+
+ - type: stacking-context
+ bounds: 50 50 100 100
+ mix-blend-mode: difference
+ items:
+ - type: rect
+ bounds: 0 0 100 100
+ color: [0, 255, 0, 1]
+
+ - type: stacking-context
+ bounds: 300 50 100 100
+ mix-blend-mode: difference
+ items:
+ - type: rect
+ bounds: 0 0 400 100
+ color: [0, 0, 255, 1]
diff --git a/gfx/wr/wrench/reftests/blend/multiply-2-ref.yaml b/gfx/wr/wrench/reftests/blend/multiply-2-ref.yaml
new file mode 100644
index 0000000000..6d4679a79d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/multiply-2-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [0, 128, 0]
diff --git a/gfx/wr/wrench/reftests/blend/multiply-2.yaml b/gfx/wr/wrench/reftests/blend/multiply-2.yaml
new file mode 100644
index 0000000000..0ad1ef35d6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/multiply-2.yaml
@@ -0,0 +1,16 @@
+---
+root:
+ items:
+ - type: stacking-context
+ blend-container: true
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [0, 255, 0]
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ mix-blend-mode: multiply
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [255, 128, 0]
diff --git a/gfx/wr/wrench/reftests/blend/multiply-3.yaml b/gfx/wr/wrench/reftests/blend/multiply-3.yaml
new file mode 100644
index 0000000000..05e1aa20f6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/multiply-3.yaml
@@ -0,0 +1,20 @@
+---
+root:
+ items:
+ - type: stacking-context
+ blend-container: true
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [0, 255, 0]
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ mix-blend-mode: multiply
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ mix-blend-mode: multiply
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [255, 128, 0]
diff --git a/gfx/wr/wrench/reftests/blend/multiply-ref.yaml b/gfx/wr/wrench/reftests/blend/multiply-ref.yaml
new file mode 100644
index 0000000000..0b9aeed906
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/multiply-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: green
diff --git a/gfx/wr/wrench/reftests/blend/multiply.yaml b/gfx/wr/wrench/reftests/blend/multiply.yaml
new file mode 100644
index 0000000000..f9fa6cbb75
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/multiply.yaml
@@ -0,0 +1,16 @@
+---
+root:
+ items:
+ - type: stacking-context
+ blend-container: true
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: green
+ - type: stacking-context
+ bounds: [25, 25, 50, 50]
+ mix-blend-mode: multiply
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: green
diff --git a/gfx/wr/wrench/reftests/blend/raster-roots-1-ref.yaml b/gfx/wr/wrench/reftests/blend/raster-roots-1-ref.yaml
new file mode 100644
index 0000000000..1b0727ccee
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/raster-roots-1-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ color: black
+ bounds: [0, 100, 100, 100]
diff --git a/gfx/wr/wrench/reftests/blend/raster-roots-1.yaml b/gfx/wr/wrench/reftests/blend/raster-roots-1.yaml
new file mode 100644
index 0000000000..fabb7c5471
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/raster-roots-1.yaml
@@ -0,0 +1,32 @@
+# Verify that when a picture cache is both a raster root and also a blend container,
+# the readback correctly maps to local raster-space, not final device-space.
+---
+root:
+ items:
+ - type: iframe
+ id: [1, 2]
+ bounds: [0, 100, 1000, 1000]
+
+pipelines:
+ - id: [1, 2]
+ items:
+ - type: stacking-context
+ blend-container: true
+ items:
+ - type: scroll-frame
+ bounds: [0, 0, 1000, 1000]
+ content-size: [1000, 10000]
+ items:
+ - type: gradient
+ bounds: [0, 0, 100, 100]
+ start: [0, 0]
+ end: [100, 0]
+ stops: [0.0, [255, 0, 0, 1], 1.0, [0, 255, 0, 1]]
+ - type: stacking-context
+ mix-blend-mode: difference
+ items:
+ - type: gradient
+ bounds: [0, 0, 100, 100]
+ start: [0, 0]
+ end: [100, 0]
+ stops: [0.0, [255, 0, 0, 1], 1.0, [0, 255, 0, 1]]
diff --git a/gfx/wr/wrench/reftests/blend/reftest.list b/gfx/wr/wrench/reftests/blend/reftest.list
new file mode 100644
index 0000000000..eee8cc3a01
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/reftest.list
@@ -0,0 +1,32 @@
+== multiply.yaml multiply-ref.yaml
+fuzzy(1,32) == multiply-2.yaml multiply-2-ref.yaml
+fuzzy(1,32) == color_targets(3) alpha_targets(0) multiply-3.yaml multiply-2-ref.yaml
+== difference.yaml difference-ref.yaml
+fuzzy(1,30000) == difference-transparent.yaml difference-transparent-ref.yaml
+fuzzy-if(platform(swgl),1,10000) == darken.yaml darken-ref.yaml
+fuzzy-if(platform(swgl),1,10000) == lighten.yaml lighten-ref.yaml
+
+fuzzy(1,32) == repeated-difference.yaml repeated-difference-ref.yaml
+
+== isolated.yaml isolated-ref.yaml
+fuzzy(3,397) == isolated-2.yaml isolated-2-ref.yaml
+== isolated-with-filter.yaml isolated-ref.yaml
+== isolated-premultiplied.yaml blank.yaml
+== isolated-premultiplied-2.yaml isolated-premultiplied-2-ref.yaml
+
+== large.yaml large-ref.yaml
+
+# fuzzy because dithering is different for gradients
+# drawn in different render targets
+fuzzy(1,2502) == transparent-composite-1.yaml transparent-composite-1-ref.yaml
+fuzzy(1,2502) == transparent-composite-2.yaml transparent-composite-2-ref.yaml
+
+fuzzy(2,420) == multi-mix-blend-mode.yaml multi-mix-blend-mode-ref.yaml
+== mix-blend-invalid-backdrop.yaml mix-blend-invalid-backdrop-ref.yaml
+platform(linux) == mix-blend-complex-transform.yaml mix-blend-complex-transform.png
+== raster-roots-1.yaml raster-roots-1-ref.yaml
+== child-surface.yaml child-surface-ref.yaml
+== blend-overflow.yaml blend-overflow-ref.yaml
+
+== backdrop-filter-blend-container.yaml backdrop-filter-blend-container-ref.yaml
+== backdrop-filter-preceding.yaml backdrop-filter-preceding-ref.yaml
diff --git a/gfx/wr/wrench/reftests/blend/repeated-difference-ref.yaml b/gfx/wr/wrench/reftests/blend/repeated-difference-ref.yaml
new file mode 100644
index 0000000000..30d53325ba
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/repeated-difference-ref.yaml
@@ -0,0 +1,7 @@
+---
+root:
+ items:
+ -
+ bounds: [0, 0, 100, 100]
+ type: rect
+ color: [0, 255, 0]
diff --git a/gfx/wr/wrench/reftests/blend/repeated-difference.yaml b/gfx/wr/wrench/reftests/blend/repeated-difference.yaml
new file mode 100644
index 0000000000..a04e6cb274
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/repeated-difference.yaml
@@ -0,0 +1,31 @@
+---
+root:
+ items:
+ -
+ bounds: [0, 0, 100, 100]
+ type: rect
+ color: [255, 255, 255]
+ - type: stacking-context
+ blend-container: true
+ items:
+ -
+ bounds: [0, 0, 100, 100]
+ type: stacking-context
+ mix-blend-mode: difference
+ items:
+ -
+ bounds: [0, 0, 100, 100]
+ type: rect
+ color: [255, 255, 255]
+ - type: stacking-context
+ blend-container: true
+ items:
+ -
+ bounds: [0, 0, 100, 100]
+ type: stacking-context
+ mix-blend-mode: difference
+ items:
+ -
+ bounds: [0, 0, 100, 100]
+ type: rect
+ color: [0, 255, 0]
diff --git a/gfx/wr/wrench/reftests/blend/transparent-composite-1-ref.yaml b/gfx/wr/wrench/reftests/blend/transparent-composite-1-ref.yaml
new file mode 100644
index 0000000000..235801c5e1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/transparent-composite-1-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: [0, 0, 100, 100]
+ start: [0, 0]
+ end: [0, 100]
+ stops: [0.0, [0,0,0,0], 1.0, green]
diff --git a/gfx/wr/wrench/reftests/blend/transparent-composite-1.yaml b/gfx/wr/wrench/reftests/blend/transparent-composite-1.yaml
new file mode 100644
index 0000000000..319b636843
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/transparent-composite-1.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ mix-blend-mode: darken
+ items:
+ - type: gradient
+ bounds: [0, 0, 100, 100]
+ start: [0, 0]
+ end: [0, 100]
+ stops: [0.0, [0,0,0,0], 1.0, green]
diff --git a/gfx/wr/wrench/reftests/blend/transparent-composite-2-ref.yaml b/gfx/wr/wrench/reftests/blend/transparent-composite-2-ref.yaml
new file mode 100644
index 0000000000..235801c5e1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/transparent-composite-2-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: [0, 0, 100, 100]
+ start: [0, 0]
+ end: [0, 100]
+ stops: [0.0, [0,0,0,0], 1.0, green]
diff --git a/gfx/wr/wrench/reftests/blend/transparent-composite-2.yaml b/gfx/wr/wrench/reftests/blend/transparent-composite-2.yaml
new file mode 100644
index 0000000000..e064f68f95
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/transparent-composite-2.yaml
@@ -0,0 +1,16 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ mix-blend-mode: darken
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ mix-blend-mode: darken
+ items:
+ - type: gradient
+ bounds: [0, 0, 100, 100]
+ start: [0, 0]
+ end: [0, 100]
+ stops: [0.0, [0,0,0,0], 1.0, green]
diff --git a/gfx/wr/wrench/reftests/blend/transparent-white.png b/gfx/wr/wrench/reftests/blend/transparent-white.png
new file mode 100644
index 0000000000..6019b2b817
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/transparent-white.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/border/blank.yaml b/gfx/wr/wrench/reftests/border/blank.yaml
new file mode 100644
index 0000000000..e3b000bcb2
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/blank.yaml
@@ -0,0 +1,3 @@
+---
+root:
+
diff --git a/gfx/wr/wrench/reftests/border/border-clamp-corner-radius.png b/gfx/wr/wrench/reftests/border/border-clamp-corner-radius.png
new file mode 100644
index 0000000000..cf20fb5990
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-clamp-corner-radius.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/border/border-clamp-corner-radius.yaml b/gfx/wr/wrench/reftests/border/border-clamp-corner-radius.yaml
new file mode 100644
index 0000000000..10859274c7
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-clamp-corner-radius.yaml
@@ -0,0 +1,24 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 420, 250]
+ items:
+ - type: border
+ bounds: [ 0, 0, 200, 200 ]
+ width: 10
+ border-type: normal
+ style: solid
+ radius: 180
+ color: blue
+ - type: border
+ bounds: [ 200, 0, 200, 200 ]
+ width: 10
+ border-type: normal
+ style: solid
+ radius:
+ top-left: [180, 180]
+ top-right: [0, 0]
+ bottom-left: [0, 0]
+ bottom-right: [180, 180]
+ color: blue \ No newline at end of file
diff --git a/gfx/wr/wrench/reftests/border/border-dashed-dotted-caching.png b/gfx/wr/wrench/reftests/border/border-dashed-dotted-caching.png
new file mode 100644
index 0000000000..14a79cd475
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-dashed-dotted-caching.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/border/border-dashed-dotted-caching.yaml b/gfx/wr/wrench/reftests/border/border-dashed-dotted-caching.yaml
new file mode 100644
index 0000000000..ca224852fd
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-dashed-dotted-caching.yaml
@@ -0,0 +1,76 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ items:
+
+ - type: border
+ bounds: [ 0, 10, 25, 30 ]
+ width: [ 10 ]
+ border-type: normal
+ style: [ dashed ]
+ color: [ blue ]
+
+ - type: border
+ bounds: [ 0, 60, 200, 30 ]
+ width: [ 10 ]
+ border-type: normal
+ style: [ dashed ]
+ color: [ blue ]
+
+ - type: border
+ bounds: [ 0, 110, 300, 30 ]
+ width: [ 10 ]
+ border-type: normal
+ style: [ dashed ]
+ color: [ blue ]
+
+ - type: border
+ bounds: [ 0, 160, 400, 30 ]
+ width: [ 10 ]
+ border-type: normal
+ style: [ dashed ]
+ color: [ blue ]
+
+ - type: border
+ bounds: [ 0, 210, 500, 30 ]
+ width: [ 10 ]
+ border-type: normal
+ style: [ dashed ]
+ color: [ blue ]
+
+ - type: border
+ bounds: [ 0, 260, 25, 30 ]
+ width: [ 10 ]
+ border-type: normal
+ style: [ dotted ]
+ color: [ blue ]
+
+ - type: border
+ bounds: [ 0, 310, 200, 30 ]
+ width: [ 10 ]
+ border-type: normal
+ style: [ dotted ]
+ color: [ blue ]
+
+ - type: border
+ bounds: [ 0, 360, 300, 30 ]
+ width: [ 10 ]
+ border-type: normal
+ style: [ dotted ]
+ color: [ blue ]
+
+ - type: border
+ bounds: [ 0, 410, 400, 30 ]
+ width: [ 10 ]
+ border-type: normal
+ style: [ dotted ]
+ color: [ blue ]
+
+ - type: border
+ bounds: [ 0, 460, 500, 30 ]
+ width: [ 10 ]
+ border-type: normal
+ style: [ dotted ]
+ color: [ blue ]
diff --git a/gfx/wr/wrench/reftests/border/border-double-1px-ref.yaml b/gfx/wr/wrench/reftests/border/border-double-1px-ref.yaml
new file mode 100644
index 0000000000..f0c9eca1ca
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-double-1px-ref.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: stacking-context
+ items:
+ - type: border
+ bounds: [ 10, 10, 100, 20 ]
+ width: 1
+ border-type: normal
+ style: solid
+ color: red
+ - type: border
+ bounds: [ 12, 12, 96, 16 ]
+ width: 1
+ border-type: normal
+ style: solid
+ color: red
diff --git a/gfx/wr/wrench/reftests/border/border-double-1px.yaml b/gfx/wr/wrench/reftests/border/border-double-1px.yaml
new file mode 100644
index 0000000000..3a5f084108
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-double-1px.yaml
@@ -0,0 +1,11 @@
+---
+root:
+ items:
+ - type: stacking-context
+ items:
+ - type: border
+ bounds: [ 10, 10, 100, 20 ]
+ width: 3
+ border-type: normal
+ style: double
+ color: red
diff --git a/gfx/wr/wrench/reftests/border/border-double-simple-2-ref.yaml b/gfx/wr/wrench/reftests/border/border-double-simple-2-ref.yaml
new file mode 100644
index 0000000000..e7fa39f125
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-double-simple-2-ref.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ items:
+ - type: border
+ bounds: [ 0, 0, 500, 500 ]
+ width: [ 1 ]
+ border-type: normal
+ style: [ solid ]
+ color: [ black ]
diff --git a/gfx/wr/wrench/reftests/border/border-double-simple-2.yaml b/gfx/wr/wrench/reftests/border/border-double-simple-2.yaml
new file mode 100644
index 0000000000..6fa6d634cd
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-double-simple-2.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ items:
+ - type: border
+ bounds: [ 0, 0, 500, 500 ]
+ width: [ 1 ]
+ border-type: normal
+ style: [ double ]
+ color: [ black ]
diff --git a/gfx/wr/wrench/reftests/border/border-double-simple-ref.yaml b/gfx/wr/wrench/reftests/border/border-double-simple-ref.yaml
new file mode 100644
index 0000000000..564e4df163
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-double-simple-ref.yaml
@@ -0,0 +1,21 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 50, 50]
+ items:
+ - type: border
+ bounds: [ 0, 0, 50, 50 ]
+ width: [ 4, 4, 4, 4 ]
+ border-type: normal
+ style: [ solid, solid, solid, solid ]
+ color: [ blue, blue, blue, blue ]
+ - type: stacking-context
+ bounds: [8, 8, 34, 34]
+ items:
+ - type: border
+ bounds: [ 0, 0, 34, 34 ]
+ width: [ 4, 4, 4, 4 ]
+ border-type: normal
+ style: [ solid, solid, solid, solid ]
+ color: [ blue, blue, blue, blue ]
diff --git a/gfx/wr/wrench/reftests/border/border-double-simple.yaml b/gfx/wr/wrench/reftests/border/border-double-simple.yaml
new file mode 100644
index 0000000000..4830485a8f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-double-simple.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ items:
+ - type: border
+ bounds: [ 0, 0, 50, 50 ]
+ width: [ 12, 12, 12, 12 ]
+ border-type: normal
+ style: [ double, double, double, double ]
+ color: [ blue, blue, blue, blue ]
diff --git a/gfx/wr/wrench/reftests/border/border-gradient-nine-patch.png b/gfx/wr/wrench/reftests/border/border-gradient-nine-patch.png
new file mode 100644
index 0000000000..c984cf9015
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-gradient-nine-patch.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/border/border-gradient-nine-patch.yaml b/gfx/wr/wrench/reftests/border/border-gradient-nine-patch.yaml
new file mode 100644
index 0000000000..667fb701e3
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-gradient-nine-patch.yaml
@@ -0,0 +1,11 @@
+---
+root:
+ items:
+ - type: border
+ bounds: [ 0, 0, 200, 200 ]
+ width: 30
+ border-type: gradient
+ start: [ 0, 200 ]
+ end: [ 200, 0 ]
+ stops: [ 0.0, red, 0.177, red, 0.177, yellow, 0.50, yellow, 0.50, red ]
+ slice: [ 50 ]
diff --git a/gfx/wr/wrench/reftests/border/border-gradient-simple-ref.yaml b/gfx/wr/wrench/reftests/border/border-gradient-simple-ref.yaml
new file mode 100644
index 0000000000..d961c12041
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-gradient-simple-ref.yaml
@@ -0,0 +1,55 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 50, 50]
+ items:
+ - type: gradient # top left
+ bounds: [ 0, 0, 10, 10]
+ start: [ 25, 0 ]
+ end: [ 25, 50 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: gradient # top right
+ bounds: [ 40, 0, 10, 10]
+ start: [ -15, 0 ]
+ end: [ -15, 50 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: gradient # bottom left
+ bounds: [ 0, 40, 10, 10]
+ start: [ 25, -40 ]
+ end: [ 25, 10 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: gradient # bottom right
+ bounds: [ 40, 40, 10, 10]
+ start: [ -15, -40 ]
+ end: [ -15, 10 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: gradient # top
+ bounds: [ 10, 0, 30, 10]
+ start: [ 15, 0 ]
+ end: [ 15, 50 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: gradient # right
+ bounds: [ 40, 10, 10, 30]
+ start: [ -15, -10 ]
+ end: [ -15, 40 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: gradient # bottom
+ bounds: [ 10, 40, 30, 10]
+ start: [ 15, -40 ]
+ end: [ 15, 10 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: gradient # left
+ bounds: [ 0, 10, 10, 30]
+ start: [ 25, -10 ]
+ end: [ 25, 40 ]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+
diff --git a/gfx/wr/wrench/reftests/border/border-gradient-simple.yaml b/gfx/wr/wrench/reftests/border/border-gradient-simple.yaml
new file mode 100644
index 0000000000..6ff94e8a89
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-gradient-simple.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 50, 50]
+ items:
+ - type: border
+ bounds: [ 0, 0, 50, 50 ]
+ width: [ 10, 10, 10, 10 ]
+ border-type: gradient
+ start: [ 25, 0 ]
+ end: [ 25, 50 ]
+ stops: [ 0.0, red, 1.0, green ]
diff --git a/gfx/wr/wrench/reftests/border/border-groove-simple-ref.yaml b/gfx/wr/wrench/reftests/border/border-groove-simple-ref.yaml
new file mode 100644
index 0000000000..9dfe5f3358
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-groove-simple-ref.yaml
@@ -0,0 +1,21 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 50, 50]
+ items:
+ - type: border
+ bounds: [ 0, 0, 50, 50 ]
+ width: [ 6, 6, 6, 6 ]
+ border-type: normal
+ style: [ solid, solid, solid, solid ]
+ color: [ 0 0 170 1.0, 0 0 255 1.0, 0 0 255 1.0, 0 0 170 1.0 ]
+ - type: stacking-context
+ bounds: [6, 6, 38, 38]
+ items:
+ - type: border
+ bounds: [ 0, 0, 38, 38 ]
+ width: [ 6, 6, 6, 6 ]
+ border-type: normal
+ style: [ solid, solid, solid, solid ]
+ color: [ 0 0 255 1.0, 0 0 170 1.0, 0 0 170 1.0, 0 0 255 1.0 ]
diff --git a/gfx/wr/wrench/reftests/border/border-groove-simple.yaml b/gfx/wr/wrench/reftests/border/border-groove-simple.yaml
new file mode 100644
index 0000000000..0d992d5dc6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-groove-simple.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ items:
+ - type: border
+ bounds: [ 0, 0, 50, 50 ]
+ width: [ 12, 12, 12, 12 ]
+ border-type: normal
+ style: [ groove, groove, groove, groove ]
+ color: [ blue, blue, blue, blue ]
diff --git a/gfx/wr/wrench/reftests/border/border-image-crash-ref.yaml b/gfx/wr/wrench/reftests/border/border-image-crash-ref.yaml
new file mode 100644
index 0000000000..b561863130
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-image-crash-ref.yaml
@@ -0,0 +1,20 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ items:
+ - type: border
+ bounds: [ 100, 100, 192, 192 ]
+ width: 32
+ border-type: image
+ image-source: "border-image-src.png"
+ image-width: 32
+ image-height: 32
+ slice: [ 3, 0, 1, 36 ]
+ repeat-vertical: stretch
+ repeat-horizontal: stretch
+ fill: true
+ - type: rect
+ bounds: [ 100, 100, 192, 192 ]
+ color: white
diff --git a/gfx/wr/wrench/reftests/border/border-image-crash.yaml b/gfx/wr/wrench/reftests/border/border-image-crash.yaml
new file mode 100644
index 0000000000..b561863130
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-image-crash.yaml
@@ -0,0 +1,20 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ items:
+ - type: border
+ bounds: [ 100, 100, 192, 192 ]
+ width: 32
+ border-type: image
+ image-source: "border-image-src.png"
+ image-width: 32
+ image-height: 32
+ slice: [ 3, 0, 1, 36 ]
+ repeat-vertical: stretch
+ repeat-horizontal: stretch
+ fill: true
+ - type: rect
+ bounds: [ 100, 100, 192, 192 ]
+ color: white
diff --git a/gfx/wr/wrench/reftests/border/border-image-empty-slice-ref.png b/gfx/wr/wrench/reftests/border/border-image-empty-slice-ref.png
new file mode 100644
index 0000000000..8ba6808ac9
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-image-empty-slice-ref.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/border/border-image-empty-slice.yaml b/gfx/wr/wrench/reftests/border/border-image-empty-slice.yaml
new file mode 100644
index 0000000000..a001038878
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-image-empty-slice.yaml
@@ -0,0 +1,46 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ items:
+ - type: border
+ bounds: [ 20, 20, 192, 192 ]
+ width: 32
+ border-type: image
+ image-source: "border-image-src.png"
+ image-width: 96
+ image-height: 96
+ slice: [ 32, 0, 32, 0 ]
+ repeat-vertical: round
+ repeat-horizontal: round
+ - type: border
+ bounds: [ 240, 20, 192, 192 ]
+ width: 32
+ border-type: image
+ image-source: "border-image-src.png"
+ image-width: 96
+ image-height: 96
+ slice: [ 0, 32, 0, 32 ]
+ repeat-vertical: round
+ repeat-horizontal: round
+ - type: border
+ bounds: [ 20, 240, 192, 192 ]
+ width: 32
+ border-type: image
+ image-source: "border-image-src.png"
+ image-width: 96
+ image-height: 96
+ slice: [ 32, 0, 0, 32 ]
+ repeat-vertical: round
+ repeat-horizontal: round
+ - type: border
+ bounds: [ 240, 240, 192, 192 ]
+ width: 32
+ border-type: image
+ image-source: "border-image-src.png"
+ image-width: 96
+ image-height: 96
+ slice: [ 0, 32, 32, 0 ]
+ repeat-vertical: round
+ repeat-horizontal: round
diff --git a/gfx/wr/wrench/reftests/border/border-image-fill-2-ref.png b/gfx/wr/wrench/reftests/border/border-image-fill-2-ref.png
new file mode 100644
index 0000000000..5bb7dc8fb2
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-image-fill-2-ref.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/border/border-image-fill-2.yaml b/gfx/wr/wrench/reftests/border/border-image-fill-2.yaml
new file mode 100644
index 0000000000..be7081fb52
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-image-fill-2.yaml
@@ -0,0 +1,51 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ items:
+ - type: border
+ bounds: [ 20, 20, 200, 200 ]
+ width: [20, 40]
+ border-type: image
+ image-source: "border-image-src-2.png"
+ image-width: 100
+ image-height: 50
+ slice: [ 20, 40 ]
+ repeat-vertical: repeat
+ repeat-horizontal: repeat
+ fill: true
+ - type: border
+ bounds: [ 240, 20, 200, 200 ]
+ width: [20, 40]
+ slice: [ 20, 40 ]
+ border-type: image
+ image-source: "border-image-src-2.png"
+ image-width: 100
+ image-height: 50
+ slice: [ 20, 40 ]
+ repeat-vertical: stretch
+ repeat-horizontal: repeat
+ fill: true
+ - type: border
+ bounds: [ 20, 240, 200, 200 ]
+ width: [20, 40]
+ border-type: image
+ image-source: "border-image-src-2.png"
+ image-width: 100
+ image-height: 50
+ slice: [ 20, 40 ]
+ repeat-vertical: repeat
+ repeat-horizontal: stretch
+ fill: true
+ - type: border
+ bounds: [ 240, 240, 200, 200 ]
+ width: [20, 40]
+ border-type: image
+ image-source: "border-image-src-2.png"
+ image-width: 100
+ image-height: 50
+ slice: [ 20, 40 ]
+ repeat-vertical: stretch
+ repeat-horizontal: stretch
+ fill: true
diff --git a/gfx/wr/wrench/reftests/border/border-image-fill-ref.png b/gfx/wr/wrench/reftests/border/border-image-fill-ref.png
new file mode 100644
index 0000000000..9d618bc76e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-image-fill-ref.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/border/border-image-fill.yaml b/gfx/wr/wrench/reftests/border/border-image-fill.yaml
new file mode 100644
index 0000000000..62ea0493f5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-image-fill.yaml
@@ -0,0 +1,50 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ items:
+ - type: border
+ bounds: [ 100, 100, 192, 192 ]
+ width: 32
+ border-type: image
+ image-source: "border-image-src.png"
+ image-width: 96
+ image-height: 96
+ slice: [ 32 ]
+ repeat-vertical: repeat
+ repeat-horizontal: repeat
+ fill: true
+ - type: border
+ bounds: [ 300, 100, 192, 192 ]
+ width: 32
+ border-type: image
+ image-source: "border-image-src.png"
+ image-width: 96
+ image-height: 96
+ slice: [ 32 ]
+ repeat-vertical: stretch
+ repeat-horizontal: repeat
+ fill: true
+ - type: border
+ bounds: [ 100, 300, 192, 192 ]
+ width: 32
+ border-type: image
+ image-source: "border-image-src.png"
+ image-width: 96
+ image-height: 96
+ slice: [ 32 ]
+ repeat-vertical: repeat
+ repeat-horizontal: stretch
+ fill: true
+ - type: border
+ bounds: [ 300, 300, 192, 192 ]
+ width: 32
+ border-type: image
+ image-source: "border-image-src.png"
+ image-width: 96
+ image-height: 96
+ slice: [ 32 ]
+ repeat-vertical: stretch
+ repeat-horizontal: stretch
+ fill: true
diff --git a/gfx/wr/wrench/reftests/border/border-image-ref.png b/gfx/wr/wrench/reftests/border/border-image-ref.png
new file mode 100644
index 0000000000..73e4eb5256
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-image-ref.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/border/border-image-round-ref.png b/gfx/wr/wrench/reftests/border/border-image-round-ref.png
new file mode 100644
index 0000000000..62a18fbba9
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-image-round-ref.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/border/border-image-round.yaml b/gfx/wr/wrench/reftests/border/border-image-round.yaml
new file mode 100644
index 0000000000..7bd9b4b7c7
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-image-round.yaml
@@ -0,0 +1,39 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ items:
+ # The pattern fits exactly a whole number of times in the border.
+ - type: border
+ bounds: [ 50, 50, 192, 192 ]
+ width: 32
+ border-type: image
+ image-source: "border-image-src.png"
+ image-width: 96
+ image-height: 96
+ slice: [ 32 ]
+ repeat-vertical: round
+ repeat-horizontal: round
+ # The pattern has to be stretched to fit.
+ - type: border
+ bounds: [ 300, 50, 200, 200 ]
+ width: 32
+ border-type: image
+ image-source: "border-image-src.png"
+ image-width: 96
+ image-height: 96
+ slice: [ 32 ]
+ repeat-vertical: round
+ repeat-horizontal: round
+ # The pattern has to be shrunk to fit.
+ - type: border
+ bounds: [ 550, 50, 180, 180 ]
+ width: 32
+ border-type: image
+ image-source: "border-image-src.png"
+ image-width: 96
+ image-height: 96
+ slice: [ 32 ]
+ repeat-vertical: round
+ repeat-horizontal: round
diff --git a/gfx/wr/wrench/reftests/border/border-image-src-2.png b/gfx/wr/wrench/reftests/border/border-image-src-2.png
new file mode 100644
index 0000000000..5ebebaf21f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-image-src-2.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/border/border-image-src.png b/gfx/wr/wrench/reftests/border/border-image-src.png
new file mode 100644
index 0000000000..128e560d90
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-image-src.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/border/border-image.yaml b/gfx/wr/wrench/reftests/border/border-image.yaml
new file mode 100644
index 0000000000..3ed969f7c5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-image.yaml
@@ -0,0 +1,16 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ items:
+ - type: border
+ bounds: [ 100, 100, 192, 192 ]
+ width: 32
+ border-type: image
+ image-source: "border-image-src.png"
+ image-width: 96
+ image-height: 96
+ slice: [ 32 ]
+ repeat-vertical: stretch
+ repeat-horizontal: stretch
diff --git a/gfx/wr/wrench/reftests/border/border-invisible-ref.yaml b/gfx/wr/wrench/reftests/border/border-invisible-ref.yaml
new file mode 100644
index 0000000000..949868c9b2
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-invisible-ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ items:
+ - type: rect
+ bounds: [99, 10, 1, 90]
+ color: black
diff --git a/gfx/wr/wrench/reftests/border/border-invisible.yaml b/gfx/wr/wrench/reftests/border/border-invisible.yaml
new file mode 100644
index 0000000000..4838786299
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-invisible.yaml
@@ -0,0 +1,17 @@
+--- # checks that invisible border corners don't render
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ items:
+ - type: border
+ bounds: [ 10, 10, 90, 90 ]
+ width: [0, 0, 0, 1]
+ border-type: normal
+ style: solid
+ radius:
+ top-left: 10
+ bottom-right: 0
+ top-right: 0
+ bottom-left: 3
+ color: [red, black, red, red]
diff --git a/gfx/wr/wrench/reftests/border/border-no-bogus-line-ref.png b/gfx/wr/wrench/reftests/border/border-no-bogus-line-ref.png
new file mode 100644
index 0000000000..1310e99f79
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-no-bogus-line-ref.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/border/border-no-bogus-line.yaml b/gfx/wr/wrench/reftests/border/border-no-bogus-line.yaml
new file mode 100644
index 0000000000..a030211da9
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-no-bogus-line.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ items:
+ - type: border
+ bounds: [ 10, 10, 90, 80 ]
+ width: 3
+ border-type: normal
+ style: solid
+ color: [ black, black, black, black ]
+ radius: 40.5
diff --git a/gfx/wr/wrench/reftests/border/border-none-ref.yaml b/gfx/wr/wrench/reftests/border/border-none-ref.yaml
new file mode 100644
index 0000000000..e011c78eb5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-none-ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ items:
+ - type: rect
+ bounds: [ 0, 0, 500, 12 ]
+ color: black
diff --git a/gfx/wr/wrench/reftests/border/border-none.yaml b/gfx/wr/wrench/reftests/border/border-none.yaml
new file mode 100644
index 0000000000..4b423c7da5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-none.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ items:
+ - type: border
+ bounds: [ 0, 0, 500, 500 ]
+ width: [ 12, 12, 12, 12 ]
+ border-type: normal
+ style: [ solid, none, none, none ]
+ color: [ black ]
diff --git a/gfx/wr/wrench/reftests/border/border-overlapping-corner-ref.yaml b/gfx/wr/wrench/reftests/border/border-overlapping-corner-ref.yaml
new file mode 100644
index 0000000000..b985704376
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-overlapping-corner-ref.yaml
@@ -0,0 +1,22 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 200, 200]
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [ 10, 10, 180, 180 ]
+ radius:
+ top-left: [180, 180]
+ top-right: [0, 0]
+ bottom-left: [0, 0]
+ bottom-right: [180, 180]
+ - type: clip-chain
+ id: 3
+ clips: [2]
+ - type: rect
+ bounds: [ 0, 0, 200, 200 ]
+ color: [ 0, 0, 255, 0.5 ]
+ clip-chain: 3
diff --git a/gfx/wr/wrench/reftests/border/border-overlapping-corner.yaml b/gfx/wr/wrench/reftests/border/border-overlapping-corner.yaml
new file mode 100644
index 0000000000..08e71fabef
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-overlapping-corner.yaml
@@ -0,0 +1,17 @@
+--- # Checks that corners are clipped correctly when they overlap with an adjacent corner
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 200, 200]
+ items:
+ - type: border
+ bounds: [ 10, 10, 180, 180 ]
+ width: 90
+ border-type: normal
+ style: solid
+ radius:
+ top-left: 180
+ bottom-right: 180
+ top-right: 0
+ bottom-left: 0
+ color: [ [0, 0, 255, 0.5] ]
diff --git a/gfx/wr/wrench/reftests/border/border-overlapping-edge-ref.yaml b/gfx/wr/wrench/reftests/border/border-overlapping-edge-ref.yaml
new file mode 100644
index 0000000000..5cb34e5ae2
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-overlapping-edge-ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 40]
+ items:
+ - type: rect
+ bounds: [ 10, 10, 100, 20 ]
+ color: [ 0, 0, 255, 0.5 ]
diff --git a/gfx/wr/wrench/reftests/border/border-overlapping-edge.yaml b/gfx/wr/wrench/reftests/border/border-overlapping-edge.yaml
new file mode 100644
index 0000000000..e39e06fddc
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-overlapping-edge.yaml
@@ -0,0 +1,12 @@
+--- # Checks that segments are clipped correctly when opposite edges of the border overlap
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 40]
+ items:
+ - type: border
+ bounds: [ 10, 10, 100, 20 ]
+ width: 15
+ border-type: normal
+ style: solid
+ color: [ [0, 0, 255, 0.5] ]
diff --git a/gfx/wr/wrench/reftests/border/border-radial-gradient-nine-patch.png b/gfx/wr/wrench/reftests/border/border-radial-gradient-nine-patch.png
new file mode 100644
index 0000000000..9fa19d215b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-radial-gradient-nine-patch.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/border/border-radial-gradient-nine-patch.yaml b/gfx/wr/wrench/reftests/border/border-radial-gradient-nine-patch.yaml
new file mode 100644
index 0000000000..06ba64e3ab
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-radial-gradient-nine-patch.yaml
@@ -0,0 +1,11 @@
+---
+root:
+ items:
+ - type: border
+ bounds: [ 0, 0, 200, 200 ]
+ width: 20
+ border-type: radial-gradient
+ center: [ 100, 100 ]
+ radius: [ 200, 200 ]
+ stops: [ 0.0, red, 0.5, red, 0.5, green ]
+ slice: [ 50 ]
diff --git a/gfx/wr/wrench/reftests/border/border-radial-gradient-simple-ref.yaml b/gfx/wr/wrench/reftests/border/border-radial-gradient-simple-ref.yaml
new file mode 100644
index 0000000000..7c3e93fd76
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-radial-gradient-simple-ref.yaml
@@ -0,0 +1,54 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 50, 50]
+ items:
+ - type: radial-gradient # top left
+ bounds: [ 0, 0, 10, 10]
+ center: [ 25, 25 ]
+ radius: [50, 50]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: radial-gradient # top right
+ bounds: [ 40, 0, 10, 10]
+ center: [ -15, 25 ]
+ radius: [50, 50]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: radial-gradient # bottom left
+ bounds: [ 0, 40, 10, 10]
+ center: [ 25, -15 ]
+ radius: [50, 50]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: radial-gradient # bottom right
+ bounds: [ 40, 40, 10, 10]
+ center: [ -15, -15 ]
+ radius: [50, 50]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: radial-gradient # top
+ bounds: [ 10, 0, 30, 10]
+ center: [ 15, 25 ]
+ radius: [50, 50]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: radial-gradient # right
+ bounds: [ 40, 10, 10, 30]
+ center: [ -15, 15 ]
+ radius: [50, 50]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: radial-gradient # bottom
+ bounds: [ 10, 40, 30, 10]
+ center: [ 15, -15 ]
+ radius: [50, 50]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
+ - type: radial-gradient # left
+ bounds: [ 0, 10, 10, 30]
+ center: [ 25, 15 ]
+ radius: [50, 50]
+ stops: [ 0.0, red, 1.0, green ]
+ repeat: false
diff --git a/gfx/wr/wrench/reftests/border/border-radial-gradient-simple.yaml b/gfx/wr/wrench/reftests/border/border-radial-gradient-simple.yaml
new file mode 100644
index 0000000000..11dded8a04
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-radial-gradient-simple.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 50, 50]
+ items:
+ - type: border
+ bounds: [ 0, 0, 50, 50 ]
+ width: [ 10, 10, 10, 10 ]
+ border-type: radial-gradient
+ center: [ 25, 25 ]
+ radius: [ 50, 50 ]
+ stops: [ 0.0, red, 1.0, green ]
diff --git a/gfx/wr/wrench/reftests/border/border-radii.png b/gfx/wr/wrench/reftests/border/border-radii.png
new file mode 100644
index 0000000000..7a4bc340eb
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-radii.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/border/border-radii.yaml b/gfx/wr/wrench/reftests/border/border-radii.yaml
new file mode 100644
index 0000000000..c0b2fda968
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-radii.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ items:
+ - type: border
+ bounds: [ 10, 10, 90, 90 ]
+ width: 10
+ border-type: normal
+ style: solid
+ radius:
+ top-left: 16
+ bottom-right: 16
+ top-right: 8
+ bottom-left: 8
+ color: [ blue, blue, blue, blue ]
diff --git a/gfx/wr/wrench/reftests/border/border-ridge-simple-ref.yaml b/gfx/wr/wrench/reftests/border/border-ridge-simple-ref.yaml
new file mode 100644
index 0000000000..b940962d45
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-ridge-simple-ref.yaml
@@ -0,0 +1,21 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 50, 50]
+ items:
+ - type: border
+ bounds: [ 0, 0, 50, 50 ]
+ width: [ 6, 6, 6, 6 ]
+ border-type: normal
+ style: [ solid, solid, solid, solid ]
+ color: [ 0 0 255 1.0, 0 0 170 1.0, 0 0 170 1.0, 0 0 255 1.0 ]
+ - type: stacking-context
+ bounds: [6, 6, 38, 38]
+ items:
+ - type: border
+ bounds: [ 0, 0, 38, 38 ]
+ width: [ 6, 6, 6, 6 ]
+ border-type: normal
+ style: [ solid, solid, solid, solid ]
+ color: [ 0 0 170 1.0, 0 0 255 1.0, 0 0 255 1.0, 0 0 170 1.0 ]
diff --git a/gfx/wr/wrench/reftests/border/border-ridge-simple.yaml b/gfx/wr/wrench/reftests/border/border-ridge-simple.yaml
new file mode 100644
index 0000000000..906b40fa42
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-ridge-simple.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ items:
+ - type: border
+ bounds: [ 0, 0, 50, 50 ]
+ width: [ 12, 12, 12, 12 ]
+ border-type: normal
+ style: [ ridge, ridge, ridge, ridge ]
+ color: [ blue, blue, blue, blue ]
diff --git a/gfx/wr/wrench/reftests/border/border-suite-2.png b/gfx/wr/wrench/reftests/border/border-suite-2.png
new file mode 100644
index 0000000000..cb43146c82
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-suite-2.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/border/border-suite-2.yaml b/gfx/wr/wrench/reftests/border/border-suite-2.yaml
new file mode 100644
index 0000000000..f86b20da30
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-suite-2.yaml
@@ -0,0 +1,171 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: border
+ bounds: [ 10, 10, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: groove
+ color: [ red, green, blue, yellow ]
+ radius: 0
+ - type: border
+ bounds: [ 120, 10, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: groove
+ color: [ red, green, blue, yellow ]
+ radius: 10
+ - type: border
+ bounds: [ 230, 10, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: groove
+ color: [ red, green, blue, yellow ]
+ radius: 20
+ - type: border
+ bounds: [ 340, 10, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: groove
+ color: [ red, green, blue, yellow ]
+ radius: 30
+ - type: border
+ bounds: [ 450, 10, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: groove
+ color: [ red, green, blue, yellow ]
+ radius: 40
+ - type: border
+ bounds: [ 560, 10, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: groove
+ color: [ red, green, blue, yellow ]
+ radius: 50
+
+ - type: border
+ bounds: [ 10, 120, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: ridge
+ color: [ red, green, blue, yellow ]
+ radius: 0
+ - type: border
+ bounds: [ 120, 120, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: ridge
+ color: [ red, green, blue, yellow ]
+ radius: 10
+ - type: border
+ bounds: [ 230, 120, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: ridge
+ color: [ red, green, blue, yellow ]
+ radius: 20
+ - type: border
+ bounds: [ 340, 120, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: ridge
+ color: [ red, green, blue, yellow ]
+ radius: 30
+ - type: border
+ bounds: [ 450, 120, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: ridge
+ color: [ red, green, blue, yellow ]
+ radius: 40
+ - type: border
+ bounds: [ 560, 120, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: ridge
+ color: [ red, green, blue, yellow ]
+ radius: 50
+
+ - type: border
+ bounds: [ 10, 230, 100, 100 ]
+ width: 1
+ border-type: normal
+ style: dashed
+ color: [ red, green, blue, black ]
+ radius: 16
+ - type: border
+ bounds: [ 120, 230, 100, 100 ]
+ width: 2
+ border-type: normal
+ style: dashed
+ color: [ red, green, blue, black ]
+ radius: 32
+ - type: border
+ bounds: [ 230, 230, 100, 100 ]
+ width: 3
+ border-type: normal
+ style: dashed
+ color: [ red, green, blue, black ]
+ radius: 32
+ - type: border
+ bounds: [ 340, 230, 100, 100 ]
+ width: 8
+ border-type: normal
+ style: dashed
+ color: [ red, green, blue, black ]
+ radius: 32
+ - type: border
+ bounds: [ 450, 230, 200, 100 ]
+ width: 4
+ border-type: normal
+ style: dotted
+ color: [ red, green, blue, black ]
+ radius: {
+ top-left: [32, 48],
+ top-right: [64, 32],
+ bottom-left: [10, 40],
+ bottom-right: [48, 48],
+ }
+
+ - type: border
+ bounds: [ 10, 340, 200, 200 ]
+ width: [4, 8, 16, 8]
+ border-type: normal
+ style: dashed
+ color: [ red, green, blue, black ]
+ radius: {
+ top-left: [32, 64],
+ top-right: [32, 32],
+ bottom-left: [64, 32],
+ bottom-right: [32, 32],
+ }
+ - type: border
+ bounds: [ 230, 340, 200, 200 ]
+ width: 4
+ border-type: normal
+ style: dashed
+ color: [ red, green, blue, black ]
+ radius: {
+ top-left: [64, 128],
+ top-right: [16, 32],
+ bottom-left: [40, 18],
+ bottom-right: [100, 50],
+ }
+ - type: border
+ bounds: [ 450, 340, 200, 100 ]
+ width: 8
+ border-type: normal
+ style: dotted
+ color: [ red, green, blue, black ]
+ radius: 32
+ - type: border
+ bounds: [ 450, 450, 200, 100 ]
+ width: [8, 8, 12, 12]
+ border-type: normal
+ style: dotted
+ color: [ red, green, blue, black ]
+ radius: 40
diff --git a/gfx/wr/wrench/reftests/border/border-suite-3.png b/gfx/wr/wrench/reftests/border/border-suite-3.png
new file mode 100644
index 0000000000..b2b7d7347c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-suite-3.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/border/border-suite-3.yaml b/gfx/wr/wrench/reftests/border/border-suite-3.yaml
new file mode 100644
index 0000000000..2deb821071
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-suite-3.yaml
@@ -0,0 +1,57 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: border
+ bounds: [ 10, 10, 200, 200 ]
+ width: 10
+ border-type: normal
+ style: [ solid, double, solid, double ]
+ color: [ red, green, blue, black ]
+ radius: 16
+ - type: border
+ bounds: [ 230, 10, 200, 200 ]
+ width: 16
+ border-type: normal
+ style: [ solid, double, solid, double ]
+ color: [ red, green, blue, black ]
+ - type: border
+ bounds: [ 450, 10, 200, 200 ]
+ width: 10
+ border-type: normal
+ style: [ solid, double, solid, double ]
+ color: [ red, green, blue, black ]
+ radius: {
+ top-left: [32, 48],
+ top-right: [64, 32],
+ bottom-left: [10, 40],
+ bottom-right: [48, 48],
+ }
+
+ - type: border
+ bounds: [ 10, 230, 200, 200 ]
+ width: 24
+ border-type: normal
+ style: [ inset, double, ridge, groove ]
+ color: [ red, green, blue, black ]
+ radius: 16
+ - type: border
+ bounds: [ 230, 230, 200, 200 ]
+ width: 32
+ border-type: normal
+ style: [ outset, double, ridge, groove ]
+ color: [ red, green, blue, black ]
+ - type: border
+ bounds: [ 450, 230, 200, 200 ]
+ width: 18
+ border-type: normal
+ style: [ outset, double, ridge, groove ]
+ color: [ red, green, blue, black ]
+ radius: {
+ top-left: [32, 48],
+ top-right: [64, 32],
+ bottom-left: [10, 40],
+ bottom-right: [48, 48],
+ }
diff --git a/gfx/wr/wrench/reftests/border/border-suite.png b/gfx/wr/wrench/reftests/border/border-suite.png
new file mode 100644
index 0000000000..8e8870afdf
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-suite.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/border/border-suite.yaml b/gfx/wr/wrench/reftests/border/border-suite.yaml
new file mode 100644
index 0000000000..5c74679a0a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/border-suite.yaml
@@ -0,0 +1,351 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: border
+ bounds: [ 10, 10, 100, 100 ]
+ width: [ 1, 1, 1, 1 ]
+ 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],
+ }
+ - type: border
+ bounds: [ 120, 10, 100, 100 ]
+ width: [ 2, 2, 2, 2 ]
+ 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],
+ }
+ - type: border
+ bounds: [ 230, 10, 100, 100 ]
+ width: [ 3, 3, 3, 3 ]
+ 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],
+ }
+ - type: border
+ bounds: [ 340, 10, 100, 100 ]
+ width: [ 5, 5, 5, 5 ]
+ 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],
+ }
+ - type: border
+ bounds: [ 450, 10, 100, 100 ]
+ width: [ 5, 5, 5, 5 ]
+ border-type: normal
+ style: double
+ color: [ red, green, blue, black ]
+ radius: {
+ top-left: [20, 20],
+ top-right: [10, 10],
+ bottom-left: [25, 25],
+ bottom-right: [0, 0],
+ }
+ - type: border
+ bounds: [ 560, 10, 100, 100 ]
+ width: [ 10, 10, 10, 10 ]
+ border-type: normal
+ style: double
+ color: [ red, green, blue, black ]
+ radius: {
+ top-left: [20, 20],
+ top-right: [10, 10],
+ bottom-left: [25, 25],
+ bottom-right: [0, 0],
+ }
+
+ - type: border
+ bounds: [ 10, 120, 100, 100 ]
+ width: 10
+ border-type: normal
+ style: inset
+ color: [ red, green, blue, black ]
+ - type: border
+ bounds: [ 120, 120, 100, 100 ]
+ width: 10
+ border-type: normal
+ style: outset
+ color: [ red, green, blue, black ]
+ - type: border
+ bounds: [ 230, 120, 100, 100 ]
+ width: 10
+ border-type: normal
+ style: inset
+ color: [ red, green, blue, black ]
+ radius: 10
+ - type: border
+ bounds: [ 340, 120, 100, 100 ]
+ width: 10
+ border-type: normal
+ style: outset
+ color: [ red, green, blue, black ]
+ radius: 20
+ - type: border
+ bounds: [ 450, 120, 100, 100 ]
+ width: 10
+ border-type: normal
+ style: outset
+ color: [ red, green, blue, black ]
+ radius: 35
+ - type: border
+ bounds: [ 560, 120, 100, 100 ]
+ width: 10
+ border-type: normal
+ style: outset
+ color: [ red, green, blue, black ]
+ radius: 50
+
+ - type: border
+ bounds: [ 10, 230, 100, 100 ]
+ width: [ 1, 1, 1, 1 ]
+ border-type: normal
+ style: solid
+ color: [ red, green, blue, black ]
+ radius: {
+ top-left: [20, 40],
+ top-right: [30, 10],
+ bottom-left: [21, 7],
+ bottom-right: [0, 5],
+ }
+ - type: border
+ bounds: [ 120, 230, 100, 100 ]
+ width: [ 2, 2, 2, 2 ]
+ border-type: normal
+ style: solid
+ color: [ red, green, blue, black ]
+ radius: {
+ top-left: [20, 40],
+ top-right: [30, 10],
+ bottom-left: [21, 7],
+ bottom-right: [0, 5],
+ }
+ - type: border
+ bounds: [ 230, 230, 100, 100 ]
+ width: [ 3, 3, 3, 3 ]
+ border-type: normal
+ style: solid
+ color: [ red, green, blue, black ]
+ radius: {
+ top-left: [20, 40],
+ top-right: [30, 10],
+ bottom-left: [21, 7],
+ bottom-right: [0, 5],
+ }
+ - type: border
+ bounds: [ 340, 230, 100, 100 ]
+ width: [ 5, 5, 5, 5 ]
+ border-type: normal
+ style: solid
+ color: [ red, green, blue, black ]
+ radius: {
+ top-left: [20, 40],
+ top-right: [30, 10],
+ bottom-left: [21, 7],
+ bottom-right: [0, 5],
+ }
+ - type: border
+ bounds: [ 450, 230, 100, 100 ]
+ width: [ 5, 5, 5, 5 ]
+ border-type: normal
+ style: double
+ color: [ red, green, blue, black ]
+ radius: {
+ top-left: [20, 40],
+ top-right: [30, 10],
+ bottom-left: [21, 7],
+ bottom-right: [0, 5],
+ }
+ - type: border
+ bounds: [ 560, 230, 100, 100 ]
+ width: [ 10, 10, 10, 10 ]
+ border-type: normal
+ style: double
+ color: [ red, green, blue, black ]
+ radius: {
+ top-left: [20, 40],
+ top-right: [30, 10],
+ bottom-left: [21, 7],
+ bottom-right: [0, 5],
+ }
+
+ - type: border
+ bounds: [ 10, 340, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: solid
+ color: [ red, green, blue, black ]
+ radius: {
+ top-left: [20, 40],
+ top-right: [30, 10],
+ bottom-left: [21, 7],
+ bottom-right: [0, 5],
+ }
+ - type: border
+ bounds: [ 120, 340, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: solid
+ color: [ red, green, blue, black ]
+ radius: {
+ top-left: [20, 40],
+ top-right: [30, 10],
+ bottom-left: [21, 7],
+ bottom-right: [0, 5],
+ }
+ - type: border
+ bounds: [ 230, 340, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: solid
+ color: [ red, green, blue, black ]
+ radius: {
+ top-left: [20, 40],
+ top-right: [30, 10],
+ bottom-left: [21, 7],
+ bottom-right: [0, 5],
+ }
+ - type: border
+ bounds: [ 340, 340, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: solid
+ color: [ red, green, blue, black ]
+ radius: {
+ top-left: [20, 40],
+ top-right: [30, 10],
+ bottom-left: [21, 7],
+ bottom-right: [0, 5],
+ }
+ - type: border
+ bounds: [ 450, 340, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: double
+ color: [ red, green, blue, black ]
+ radius: {
+ top-left: [20, 40],
+ top-right: [30, 10],
+ bottom-left: [21, 7],
+ bottom-right: [0, 5],
+ }
+ - type: border
+ bounds: [ 560, 340, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: double
+ color: [ red, green, blue, black ]
+ radius: {
+ top-left: [20, 40],
+ top-right: [30, 10],
+ bottom-left: [21, 7],
+ bottom-right: [0, 5],
+ }
+
+ - type: border
+ bounds: [ 10, 450, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: solid
+ color: [ red, green, blue, black ]
+ radius: 0
+ - type: border
+ bounds: [ 120, 450, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: solid
+ color: [ red, green, blue, black ]
+ radius: 10
+ - type: border
+ bounds: [ 230, 450, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: solid
+ color: [ red, green, blue, black ]
+ radius: 20
+ - type: border
+ bounds: [ 340, 450, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: solid
+ color: [ red, green, blue, black ]
+ radius: 30
+ - type: border
+ bounds: [ 450, 450, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: solid
+ color: [ red, green, blue, black ]
+ radius: 40
+ - type: border
+ bounds: [ 560, 450, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: solid
+ color: [ red, green, blue, black ]
+ radius: 50
+
+ - type: border
+ bounds: [ 10, 560, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: double
+ color: [ red, green, blue, black ]
+ radius: 0
+ - type: border
+ bounds: [ 120, 560, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: double
+ color: [ red, green, blue, black ]
+ radius: 10
+ - type: border
+ bounds: [ 230, 560, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: double
+ color: [ red, green, blue, black ]
+ radius: 20
+ - type: border
+ bounds: [ 340, 560, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: double
+ color: [ red, green, blue, black ]
+ radius: 30
+ - type: border
+ bounds: [ 450, 560, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: double
+ color: [ red, green, blue, black ]
+ radius: 40
+ - type: border
+ bounds: [ 560, 560, 100, 100 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: double
+ color: [ red, green, blue, black ]
+ radius: 50
diff --git a/gfx/wr/wrench/reftests/border/degenerate-curve.png b/gfx/wr/wrench/reftests/border/degenerate-curve.png
new file mode 100644
index 0000000000..9a4eb6736f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/degenerate-curve.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/border/degenerate-curve.yaml b/gfx/wr/wrench/reftests/border/degenerate-curve.yaml
new file mode 100644
index 0000000000..d5ee02ba4f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/degenerate-curve.yaml
@@ -0,0 +1,293 @@
+---
+root:
+ items:
+ -
+ bounds: [0, 0, 1024, 740]
+ type: "stacking-context"
+ "scroll-policy": scrollable
+ "transform-style": flat
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [28, 18, 144, 122]
+ radius:
+ "top-left": [61, 61]
+ "top-right": [61, 61]
+ "bottom-left": [0, 0]
+ "bottom-right": [61, 61]
+ - type: clip-chain
+ id: 20
+ clips: [2]
+ - type: rect
+ color: 255 0 0 1.0000
+ bounds: [28, 18, 144, 122]
+ clip-chain: 20
+ -
+ bounds: [28, 18, 144, 122]
+ type: border
+ width: [18, 0, 18, 0]
+ "border-type": normal
+ color: 0 0 255 1.0000
+ style:
+ - solid
+ - none
+ - solid
+ - none
+ radius:
+ "top-left": [61, 61]
+ "top-right": [61, 61]
+ "bottom-left": [0, 0]
+ "bottom-right": [61, 61]
+ - type: clip
+ id: 3
+ complex:
+ - rect: [28, 160, 144, 122]
+ radius: [61, 61]
+ - type: clip-chain
+ id: 21
+ clips: [3]
+ - type: rect
+ bounds: [28, 160, 144, 122]
+ color: 255 0 0 1.0000
+ clip-chain: 21
+
+ -
+ bounds: [28, 160, 144, 122]
+ type: border
+ width: [18, 0, 18, 0]
+ "border-type": normal
+ color: 0 0 255 1.0000
+ style:
+ - solid
+ - none
+ - solid
+ - none
+ radius: [61, 61]
+ - type: clip
+ id: 4
+ complex:
+ - rect: [28, 302, 154, 122]
+ radius:
+ "top-left": [0, 0]
+ "top-right": [61, 61]
+ "bottom-left": [0, 0]
+ "bottom-right": [61, 61]
+ - type: clip-chain
+ id: 22
+ clips: [4]
+ - type: rect
+ bounds: [28, 302, 154, 122]
+ color: 255 0 0 1.0000
+ clip-chain: 22
+
+ - type: border
+ bounds: [28, 302, 154, 122]
+ width: [18, 0, 18, 10]
+ "border-type": normal
+ color:
+ - 0 0 255 1.0000
+ - 0 0 255 1.0000
+ - 0 0 255 1.0000
+ - 0 0 255 1.0000
+ style:
+ - solid
+ - none
+ - solid
+ - solid
+ radius:
+ "top-left": [0, 0]
+ "top-right": [61, 61]
+ "bottom-left": [0, 0]
+ "bottom-right": [61, 61]
+ - type: clip
+ id: 5
+ complex:
+ - rect: [202, 18, 144.03334, 122]
+ radius:
+ "top-left": [61, 61]
+ "top-right": [61, 61]
+ "bottom-left": [0, 0]
+ "bottom-right": [61, 61]
+ - type: clip-chain
+ id: 23
+ clips: [5]
+ - type: rect
+ bounds: [202, 18, 144.03334, 122]
+ color: 255 0 0 1.0000
+ clip-chain: 23
+
+ - type: border
+ bounds: [202, 18, 144.03334, 122]
+ width: [18, 0.016666668, 18, 0.016666668]
+ "border-type": normal
+ color:
+ - 0 0 255 1.0000
+ - 255 0 0 1.0000
+ - 0 0 255 1.0000
+ - 255 0 0 1.0000
+ style: solid
+ radius:
+ "top-left": [61, 61]
+ "top-right": [61, 61]
+ "bottom-left": [0, 0]
+ "bottom-right": [61, 61]
+ - type: clip
+ id: 6
+ complex:
+ - rect: [202, 160, 144, 122]
+ radius: [61, 61]
+ - type: clip-chain
+ id: 24
+ clips: [6]
+ - type: rect
+ bounds: [202, 160, 144, 122]
+ color: 255 0 0 1.0000
+ clip-chain: 24
+
+ - type: border
+ bounds: [202, 160, 144, 122]
+ width: [18, 0, 18, 0]
+ "border-type": normal
+ color:
+ - 0 0 255 1.0000
+ - 255 0 0 1.0000
+ - 0 0 255 1.0000
+ - 255 0 0 1.0000
+ style: solid
+ radius: [61, 61]
+ - type: clip
+ id: 7
+ complex:
+ - rect: [202, 302, 154, 122]
+ radius:
+ "top-left": [0, 0]
+ "top-right": [72, 72]
+ "bottom-left": [0, 0]
+ "bottom-right": [0, 0]
+ - type: clip-chain
+ id: 25
+ clips: [7]
+ - type: rect
+ bounds: [202, 302, 154, 122]
+ color: 255 0 0 1.0000
+ clip-chain: 25
+
+ -
+ bounds: [202, 302, 154, 122]
+ type: border
+ width: [18, 0, 18, 10]
+ "border-type": normal
+ color:
+ - 0 0 255 1.0000
+ - 0 0 255 1.0000
+ - 0 0 255 1.0000
+ - 0 0 255 1.0000
+ style:
+ - solid
+ - none
+ - solid
+ - solid
+ radius:
+ "top-left": [0, 0]
+ "top-right": [72, 72]
+ "bottom-left": [0, 0]
+ "bottom-right": [0, 0]
+ - type: clip
+ id: 8
+ complex:
+ - rect: [376, 18, 144, 122]
+ radius: [61, 61]
+ - type: clip-chain
+ id: 26
+ clips: [8]
+ - type: rect
+ bounds: [376, 18, 144, 122]
+ color: 255 0 0 1.0000
+ clip-chain: 26
+
+ -
+ bounds: [376, 18, 144, 122]
+ type: border
+ width: [18, 0, 18, 0]
+ "border-type": normal
+ color:
+ - 0 0 255 1.0000
+ - 255 0 0 1.0000
+ - 0 0 255 1.0000
+ - 255 0 0 1.0000
+ style:
+ - solid
+ - hidden
+ - solid
+ - hidden
+ radius: [61, 61]
+ - type: clip
+ id: 9
+ complex:
+ - rect: [376, 160, 144, 122]
+ radius: [61, 61]
+ - type: clip-chain
+ id: 27
+ clips: [9]
+ - type: rect
+ bounds: [376, 160, 144, 122]
+ color: 255 0 0 1.0000
+ clip-chain: 27
+
+ -
+ bounds: [376, 160, 144, 122]
+ clip: [-17895698, -17895698, 35791396, 35791396]
+ type: border
+ width: [18, 0, 18, 0]
+ "border-type": normal
+ color:
+ - 0 0 255 1.0000
+ - 255 0 0 1.0000
+ - 0 0 255 1.0000
+ - 255 0 0 1.0000
+ style:
+ - solid
+ - none
+ - solid
+ - none
+ radius: [61, 61]
+ - type: clip
+ id: 10
+ complex:
+ - rect: [376, 302, 144, 122]
+ radius:
+ "top-left": [0, 0]
+ "top-right": [72, 72]
+ "bottom-left": [72, 72]
+ "bottom-right": [0, 0]
+ - type: clip-chain
+ id: 28
+ clips: [10]
+ - type: rect
+ bounds: [376, 302, 144, 122]
+ color: 255 0 0 1.0000
+ clip-chain: 28
+
+ -
+ bounds: [376, 302, 144, 122]
+ type: border
+ width: [18, 0, 18, 0]
+ "border-type": normal
+ color:
+ - 0 0 255 1.0000
+ - 255 0 0 1.0000
+ - 0 0 255 1.0000
+ - 0 0 255 1.0000
+ style:
+ - solid
+ - none
+ - solid
+ - hidden
+ radius:
+ "top-left": [0, 0]
+ "top-right": [72, 72]
+ "bottom-left": [72, 72]
+ "bottom-right": [0, 0]
+ id: [0, 1]
+pipelines: []
diff --git a/gfx/wr/wrench/reftests/border/discontinued-dash.png b/gfx/wr/wrench/reftests/border/discontinued-dash.png
new file mode 100644
index 0000000000..67625e1928
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/discontinued-dash.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/border/discontinued-dash.yaml b/gfx/wr/wrench/reftests/border/discontinued-dash.yaml
new file mode 100644
index 0000000000..886f9cfe59
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/discontinued-dash.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ items:
+ - type: border
+ bounds: [ 10, 10, 300, 300]
+ width: [ 10, 10, 10, 10 ]
+ border-type: normal
+ style: [ dashed, solid, solid, solid ]
+ color: [ black, black, black, black ]
diff --git a/gfx/wr/wrench/reftests/border/dotted-corner-small-radius.png b/gfx/wr/wrench/reftests/border/dotted-corner-small-radius.png
new file mode 100644
index 0000000000..99405da1a3
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/dotted-corner-small-radius.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/border/dotted-corner-small-radius.yaml b/gfx/wr/wrench/reftests/border/dotted-corner-small-radius.yaml
new file mode 100644
index 0000000000..4e40c251e4
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/dotted-corner-small-radius.yaml
@@ -0,0 +1,11 @@
+---
+root:
+ items:
+ - bounds: [5, 5, 490, 490]
+ "clip-rect": [5, 5, 490, 490]
+ "backface-visible": true
+ type: border
+ width: 25
+ "border-type": normal
+ color: black
+ style: dotted
diff --git a/gfx/wr/wrench/reftests/border/green-square.yaml b/gfx/wr/wrench/reftests/border/green-square.yaml
new file mode 100644
index 0000000000..2f13625928
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/green-square.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ items:
+ - type: rect
+ bounds: [ 0, 0, 400, 400 ]
+ color: green
diff --git a/gfx/wr/wrench/reftests/border/max-scale-ref.yaml b/gfx/wr/wrench/reftests/border/max-scale-ref.yaml
new file mode 100644
index 0000000000..0647a7a864
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/max-scale-ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: border
+ bounds: [ 0, 0, 4000, 1 ]
+ width: [ 1, 0, 0, 0 ]
+ border-type: normal
+ style: solid
+ color: red
diff --git a/gfx/wr/wrench/reftests/border/max-scale.yaml b/gfx/wr/wrench/reftests/border/max-scale.yaml
new file mode 100644
index 0000000000..cf292d5798
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/max-scale.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: stacking-context
+ transform: scale(3000, 1)
+ items:
+ - type: border
+ bounds: [ 0, 0, 100, 100 ]
+ width: [ 1, 0, 0, 0 ]
+ border-type: normal
+ style: solid
+ color: red
diff --git a/gfx/wr/wrench/reftests/border/no-aa.yaml b/gfx/wr/wrench/reftests/border/no-aa.yaml
new file mode 100644
index 0000000000..f937a865f4
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/no-aa.yaml
@@ -0,0 +1,20 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ items:
+ - type: border
+ bounds: [ 0, 0, 400, 400 ]
+ width: [ 200, 200, 200, 200 ]
+ border-type: normal
+ style: solid
+ color: [ green, green, transparent, transparent ]
+ do_aa: false
+ - type: border
+ bounds: [ 0, 0, 400, 400 ]
+ width: [ 200, 200, 200, 200 ]
+ border-type: normal
+ style: solid
+ color: [ transparent, transparent, green, green ]
+ do_aa: false
diff --git a/gfx/wr/wrench/reftests/border/overlapping.png b/gfx/wr/wrench/reftests/border/overlapping.png
new file mode 100644
index 0000000000..bf12c8d81a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/overlapping.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/border/overlapping.yaml b/gfx/wr/wrench/reftests/border/overlapping.yaml
new file mode 100644
index 0000000000..2a2b3dfb30
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/overlapping.yaml
@@ -0,0 +1,22 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [ 0, 0, 200, 200 ]
+ radius:
+ top-left: [180, 180]
+ top-right: [0, 0]
+ bottom-left: [0, 0]
+ bottom-right: [180, 180]
+ - type: clip-chain
+ id: 10
+ clips: [2]
+ - type: rect
+ bounds: [ 0, 0, 200, 200 ]
+ color: blue
+ clip-chain: 10
diff --git a/gfx/wr/wrench/reftests/border/reftest.list b/gfx/wr/wrench/reftests/border/reftest.list
new file mode 100644
index 0000000000..a5551e8270
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/reftest.list
@@ -0,0 +1,35 @@
+platform(linux,mac) == border-clamp-corner-radius.yaml border-clamp-corner-radius.png
+fuzzy(1,840) == border-gradient-simple.yaml border-gradient-simple-ref.yaml
+platform(linux,mac) == border-gradient-nine-patch.yaml border-gradient-nine-patch.png
+fuzzy-if(platform(swgl),1,4) == border-radial-gradient-simple.yaml border-radial-gradient-simple-ref.yaml
+platform(linux,mac) == border-radial-gradient-nine-patch.yaml border-radial-gradient-nine-patch.png
+== fuzzy(1,10) border-radii.yaml border-radii.png
+== border-none.yaml border-none-ref.yaml
+fuzzy(128,69) fuzzy-if(platform(swgl),2,118) == border-overlapping-corner.yaml border-overlapping-corner-ref.yaml
+== border-overlapping-edge.yaml border-overlapping-edge-ref.yaml
+== border-invisible.yaml border-invisible-ref.yaml
+platform(linux,mac) fuzzy(3,80) == border-suite.yaml border-suite.png
+platform(linux,mac) fuzzy(8,105) == border-suite-2.yaml border-suite-2.png
+platform(linux,mac) fuzzy(1,10) == border-suite-3.yaml border-suite-3.png
+skip_on(android,device) == border-double-simple.yaml border-double-simple-ref.yaml # Fails on Pixel2
+== border-double-simple-2.yaml border-double-simple-2-ref.yaml
+skip_on(android,device) fuzzy(64,24) == border-groove-simple.yaml border-groove-simple-ref.yaml # Fails on Pixel2
+skip_on(android,device) fuzzy(64,24) == border-ridge-simple.yaml border-ridge-simple-ref.yaml # Fails on Pixel2
+platform(linux,mac) fuzzy(1,26) == degenerate-curve.yaml degenerate-curve.png
+platform(linux,mac) == border-image.yaml border-image-ref.png
+platform(linux,mac) == border-image-empty-slice.yaml border-image-empty-slice-ref.png
+platform(linux,mac) == border-image-round.yaml border-image-round-ref.png
+== border-image-crash.yaml border-image-crash-ref.yaml
+fuzzy(1,5000) == border-image-fill.yaml border-image-fill-ref.png
+fuzzy(2,25360) == border-image-fill-2.yaml border-image-fill-2-ref.png
+fuzzy-if(platform(swgl),1,8) == border-no-bogus-line.yaml border-no-bogus-line-ref.png
+platform(linux,mac) fuzzy(1,130) == dotted-corner-small-radius.yaml dotted-corner-small-radius.png
+fuzzy-if(platform(swgl),1,20) == overlapping.yaml overlapping.png
+== zero-width.yaml blank.yaml
+platform(linux,mac) == small-dotted-border.yaml small-dotted-border.png
+fuzzy(1,30) == discontinued-dash.yaml discontinued-dash.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,90) == no-aa.yaml green-square.yaml
+skip_on(android,device) == border-double-1px.yaml border-double-1px-ref.yaml # Fails on Pixel2
+== max-scale.yaml max-scale-ref.yaml
diff --git a/gfx/wr/wrench/reftests/border/small-dotted-border.png b/gfx/wr/wrench/reftests/border/small-dotted-border.png
new file mode 100644
index 0000000000..b867b04c2b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/small-dotted-border.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/border/small-dotted-border.yaml b/gfx/wr/wrench/reftests/border/small-dotted-border.yaml
new file mode 100644
index 0000000000..2fb189bce2
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/small-dotted-border.yaml
@@ -0,0 +1,19 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ items:
+ - type: border
+ bounds: [ 0, 0, 100, 20 ]
+ width: [ 1, 1, 1, 1 ]
+ border-type: normal
+ style: [ dotted, dotted, dotted, dotted ]
+ color: [ black, black, black, black ]
+
+ - type: border
+ bounds: [ 0, 30, 100, 20 ]
+ width: [ 2, 2, 2, 2 ]
+ border-type: normal
+ style: [ dotted, dotted, dotted, dotted ]
+ color: [ black, black, black, black ]
diff --git a/gfx/wr/wrench/reftests/border/small-inset-outset-notref.yaml b/gfx/wr/wrench/reftests/border/small-inset-outset-notref.yaml
new file mode 100644
index 0000000000..e55bdc84c6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/small-inset-outset-notref.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ items:
+ - type: border
+ bounds: [ 0, 0, 200, 200 ]
+ width: 1
+ border-type: normal
+ style: solid
+ color: black
diff --git a/gfx/wr/wrench/reftests/border/small-inset-outset.yaml b/gfx/wr/wrench/reftests/border/small-inset-outset.yaml
new file mode 100644
index 0000000000..f7bc42807b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/small-inset-outset.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ items:
+ - type: border
+ bounds: [ 0, 0, 200, 200 ]
+ width: 1
+ border-type: normal
+ style: inset
+ color: black
diff --git a/gfx/wr/wrench/reftests/border/zero-width.yaml b/gfx/wr/wrench/reftests/border/zero-width.yaml
new file mode 100644
index 0000000000..6d12d05bf8
--- /dev/null
+++ b/gfx/wr/wrench/reftests/border/zero-width.yaml
@@ -0,0 +1,12 @@
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ items:
+ - type: border
+ bounds: [ 0, 0, 50, 50 ]
+ width: [ 0, 0, 0, 0 ]
+ border-type: normal
+ style: [ solid, solid, solid, solid ]
+ color: [ blue, blue, blue, blue ]
+
diff --git a/gfx/wr/wrench/reftests/boxshadow/blank.yaml b/gfx/wr/wrench/reftests/boxshadow/blank.yaml
new file mode 100644
index 0000000000..c4eb3ab673
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/blank.yaml
@@ -0,0 +1,2 @@
+---
+root:
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-blurred-overlapping-radii-ref.yaml b/gfx/wr/wrench/reftests/boxshadow/box-shadow-blurred-overlapping-radii-ref.yaml
new file mode 100644
index 0000000000..5e2da4fa8a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-blurred-overlapping-radii-ref.yaml
@@ -0,0 +1,20 @@
+
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: box-shadow
+ bounds: [ 0, 1, 265, 265 ]
+ color: red
+ clip-mode: outset
+ offset: 350 -1
+ blur-radius: 1
+ spread-radius: -80
+ border-radius:
+ top-left: [185, 185]
+ top-right: [0, 0]
+ bottom-left: [0, 0]
+ bottom-right: [185, 185]
+
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-blurred-overlapping-radii.yaml b/gfx/wr/wrench/reftests/boxshadow/box-shadow-blurred-overlapping-radii.yaml
new file mode 100644
index 0000000000..ebc4b263a1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-blurred-overlapping-radii.yaml
@@ -0,0 +1,19 @@
+
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: box-shadow
+ bounds: [ 0, 1, 265, 265 ]
+ color: red
+ clip-mode: outset
+ offset: 350 -1
+ blur-radius: 1
+ spread-radius: -80
+ border-radius:
+ top-left: [250, 250]
+ top-right: [0, 0]
+ bottom-left: [0, 0]
+ bottom-right: [250, 250]
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-border-radii.png b/gfx/wr/wrench/reftests/boxshadow/box-shadow-border-radii.png
new file mode 100644
index 0000000000..0741826160
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-border-radii.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-border-radii.yaml b/gfx/wr/wrench/reftests/boxshadow/box-shadow-border-radii.yaml
new file mode 100644
index 0000000000..d52601e019
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-border-radii.yaml
@@ -0,0 +1,36 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: box-shadow
+ bounds: [ 50, 50, 100, 100 ]
+ color: red
+ clip-mode: outset
+ blur-radius: 10
+ border-radius: {
+ top-left: [20, 30],
+ top-right: [10, 30],
+ bottom-left: [50, 25],
+ bottom-right: [0, 0],
+ }
+
+ - type: box-shadow
+ bounds: [ 200, 50, 100, 100 ]
+ color: green
+ clip-mode: inset
+ blur-radius: 10
+ border-radius: {
+ top-left: [20, 30],
+ top-right: [10, 30],
+ bottom-left: [50, 25],
+ bottom-right: [0, 0],
+ }
+
+ - type: box-shadow
+ bounds: [ 350, 50, 100, 100 ]
+ color: blue
+ clip-mode: outset
+ blur-radius: 10
+ border-radius: [ 10, 20, 30, 40 ]
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-cache.png b/gfx/wr/wrench/reftests/boxshadow/box-shadow-cache.png
new file mode 100644
index 0000000000..96d2c7fed3
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-cache.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-cache.yaml b/gfx/wr/wrench/reftests/boxshadow/box-shadow-cache.yaml
new file mode 100644
index 0000000000..721c6fe4e0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-cache.yaml
@@ -0,0 +1,98 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: box-shadow
+ bounds: [ 50, 50, 100, 100 ]
+ color: red
+ clip-mode: outset
+ blur-radius: 10
+ border-radius: {
+ top-left: [20, 30],
+ top-right: [10, 30],
+ bottom-left: [50, 25],
+ bottom-right: [0, 0],
+ }
+
+ - type: box-shadow
+ bounds: [ 50, 200, 100, 100 ]
+ color: red
+ clip-mode: outset
+ blur-radius: 10
+ border-radius: {
+ top-left: [20, 30],
+ top-right: [10, 30],
+ bottom-left: [50, 25],
+ bottom-right: [0, 0],
+ }
+
+ - type: box-shadow
+ bounds: [ 50, 350, 100, 100 ]
+ color: red
+ clip-mode: outset
+ blur-radius: 10
+ border-radius: {
+ top-left: [20, 30],
+ top-right: [10, 30],
+ bottom-left: [50, 25],
+ bottom-right: [0, 0],
+ }
+
+ - type: box-shadow
+ bounds: [ 200, 50, 100, 100 ]
+ color: green
+ clip-mode: inset
+ blur-radius: 10
+ border-radius: {
+ top-left: [20, 30],
+ top-right: [10, 30],
+ bottom-left: [50, 25],
+ bottom-right: [0, 0],
+ }
+
+ - type: box-shadow
+ bounds: [ 200, 200, 100, 100 ]
+ color: green
+ clip-mode: inset
+ blur-radius: 10
+ border-radius: {
+ top-left: [20, 30],
+ top-right: [10, 30],
+ bottom-left: [50, 25],
+ bottom-right: [0, 0],
+ }
+
+ - type: box-shadow
+ bounds: [ 200, 350, 100, 100 ]
+ color: green
+ clip-mode: inset
+ blur-radius: 10
+ border-radius: {
+ top-left: [20, 30],
+ top-right: [10, 30],
+ bottom-left: [50, 25],
+ bottom-right: [0, 0],
+ }
+
+ - type: box-shadow
+ bounds: [ 350, 50, 100, 100 ]
+ color: blue
+ clip-mode: outset
+ blur-radius: 10
+ border-radius: [ 10, 20, 30, 40 ]
+
+ - type: box-shadow
+ bounds: [ 350, 200, 100, 100 ]
+ color: blue
+ clip-mode: outset
+ blur-radius: 10
+ border-radius: [ 10, 20, 30, 40 ]
+
+ - type: box-shadow
+ bounds: [ 350, 350, 100, 100 ]
+ color: blue
+ clip-mode: outset
+ blur-radius: 10
+ border-radius: [ 10, 20, 30, 40 ]
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-clip-ref.yaml b/gfx/wr/wrench/reftests/boxshadow/box-shadow-clip-ref.yaml
new file mode 100644
index 0000000000..cda90245c9
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-clip-ref.yaml
@@ -0,0 +1,59 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: clip
+ id: 2
+ bounds: [0, 0, 100, 200]
+ - type: clip-chain
+ id: 10
+ clips: [2]
+ - type: box-shadow
+ bounds: [ 20, 20, 200, 80 ]
+ color: blue
+ offset: [10, 20]
+ clip-mode: outset
+ clip-chain: 10
+
+ - type: clip
+ id: 3
+ bounds: [200, 0, 100, 200]
+ - type: clip-chain
+ id: 11
+ clips: [3]
+ - type: box-shadow
+ bounds: [ 220, 20, 200, 80 ]
+ color: red
+ offset: [10, 20]
+ blur-radius: 10
+ clip-mode: outset
+ clip-chain: 11
+
+ - type: clip
+ id: 4
+ bounds: [0, 200, 100, 200]
+ - type: clip-chain
+ id: 12
+ clips: [4]
+ - type: box-shadow
+ bounds: [ 20, 220, 200, 80 ]
+ color: green
+ offset: [10, 20]
+ clip-mode: inset
+ clip-chain: 12
+
+ - type: clip
+ id: 5
+ bounds: [200, 200, 100, 200]
+ - type: clip-chain
+ id: 13
+ clips: [5]
+ - type: box-shadow
+ bounds: [ 220, 220, 200, 80 ]
+ color: yellow
+ offset: [10, 20]
+ blur-radius: 10
+ clip-mode: inset
+ clip-chain: 13
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-clip.yaml b/gfx/wr/wrench/reftests/boxshadow/box-shadow-clip.yaml
new file mode 100644
index 0000000000..c027b54379
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-clip.yaml
@@ -0,0 +1,32 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: box-shadow
+ bounds: [ 20, 20, 200, 80 ]
+ clip-rect: [0, 0, 100, 200]
+ color: blue
+ offset: [10, 20]
+ clip-mode: outset
+ - type: box-shadow
+ bounds: [ 220, 20, 200, 80 ]
+ clip-rect: [200, 0, 100, 200]
+ color: red
+ offset: [10, 20]
+ blur-radius: 10
+ clip-mode: outset
+ - type: box-shadow
+ bounds: [ 20, 220, 200, 80 ]
+ clip-rect: [0, 200, 100, 200]
+ color: green
+ offset: [10, 20]
+ clip-mode: inset
+ - type: box-shadow
+ bounds: [ 220, 220, 200, 80 ]
+ clip-rect: [200, 200, 100, 200]
+ color: yellow
+ offset: [10, 20]
+ blur-radius: 10
+ clip-mode: inset
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-empty.yaml b/gfx/wr/wrench/reftests/boxshadow/box-shadow-empty.yaml
new file mode 100644
index 0000000000..d3640d53f5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-empty.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [50, 50, 500, 500]
+ items:
+ - type: box-shadow
+ bounds: [ 10, 10, 400, 400 ]
+ blur-radius: 0
+ clip-mode: outset
+ offset: [0, 0]
+ spread-radius: 0
+ color: [255, 0, 0]
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-huge-radius.png b/gfx/wr/wrench/reftests/boxshadow/box-shadow-huge-radius.png
new file mode 100644
index 0000000000..f2be685139
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-huge-radius.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-huge-radius.yaml b/gfx/wr/wrench/reftests/boxshadow/box-shadow-huge-radius.yaml
new file mode 100644
index 0000000000..28ac4a62a3
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-huge-radius.yaml
@@ -0,0 +1,120 @@
+---
+root:
+ items:
+ -
+ bounds: [0, 0, 0, 0]
+ "clip-rect": [0, 0, 0, 0]
+ "backface-visible": true
+ type: "stacking-context"
+ "transform-style": flat
+ "raster-space": screen
+ filters: []
+ items:
+ -
+ bounds: [0, 148, 1368, 588]
+ "clip-rect": [0, 148, 1368, 588]
+ "backface-visible": true
+ type: clip
+ id: 2
+ -
+ bounds: [0, 0, 0, 0]
+ "clip-rect": [0, 0, 0, 0]
+ "backface-visible": true
+ type: "clip-chain"
+ id: 3
+ clips: [2]
+ -
+ bounds: [16, 164, 16200, 200]
+ "clip-rect": [40, 188, 1328, 216]
+ clip-chain: 3
+ "backface-visible": true
+ type: "box-shadow"
+ "box-bounds": [16, 164, 16200, 200]
+ offset: [32, 32]
+ color: 0 0 255 1.0000
+ "blur-radius": 6
+ "spread-radius": 0
+ "border-radius":
+ "top-left": [16200, 200]
+ "top-right": [0, 0]
+ "bottom-left": [0, 0]
+ "bottom-right": [0, 0]
+ "clip-mode": outset
+ -
+ "clip-rect": [16, 164, 1352, 200]
+ "backface-visible": true
+ type: clip
+ id: 4
+ complex:
+ -
+ rect: [16, 164, 16200, 200]
+ radius:
+ "top-left": [16200, 200]
+ "top-right": [0, 0]
+ "bottom-left": [0, 0]
+ "bottom-right": [0, 0]
+ "clip-mode": clip
+ -
+ bounds: [0, 0, 0, 0]
+ "clip-rect": [0, 0, 0, 0]
+ clip-chain: 3
+ "backface-visible": true
+ type: "clip-chain"
+ id: 5
+ clips: [4]
+ parent: 3
+ -
+ bounds: [16, 164, 16200, 200]
+ "clip-rect": [16, 164, 16200, 200]
+ clip-chain: 5
+ "backface-visible": true
+ type: rect
+ color: 255 0 255 1.0000
+ -
+ bounds: [16, 402, 200, 200]
+ "clip-rect": [40, 426, 216, 216]
+ clip-chain: 3
+ "backface-visible": true
+ type: "box-shadow"
+ "box-bounds": [16, 402, 200, 200]
+ offset: [32, 32]
+ color: 0 255 255 1.0000
+ "blur-radius": 6
+ "spread-radius": 0
+ "border-radius":
+ "top-left": [0, 0]
+ "top-right": [200, 200]
+ "bottom-left": [0, 0]
+ "bottom-right": [0, 0]
+ "clip-mode": outset
+ -
+ "clip-rect": [16, 402, 200, 200]
+ "backface-visible": true
+ type: clip
+ id: 6
+ complex:
+ -
+ rect: [16, 402, 200, 200]
+ radius:
+ "top-left": [0, 0]
+ "top-right": [200, 200]
+ "bottom-left": [0, 0]
+ "bottom-right": [0, 0]
+ "clip-mode": clip
+ -
+ bounds: [0, 0, 0, 0]
+ "clip-rect": [0, 0, 0, 0]
+ "backface-visible": true
+ type: "clip-chain"
+ id: 7
+ clips: [6]
+ parent: 3
+ -
+ bounds: [16, 402, 200, 200]
+ "clip-rect": [16, 402, 200, 200]
+ clip-chain: 7
+ "backface-visible": true
+ type: rect
+ color: 0 128 0 1.0000
+ id: [0, 0]
+pipelines: []
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius-2.png b/gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius-2.png
new file mode 100644
index 0000000000..9111ca4413
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius-2.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius-2.yaml b/gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius-2.yaml
new file mode 100644
index 0000000000..0a82cb5bee
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius-2.yaml
@@ -0,0 +1,14 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [50, 50, 1500, 200]
+ items:
+ - type: box-shadow
+ bounds: [ 10, 10, 900, 900 ]
+ blur-radius: 5000
+ clip-mode: outset
+ offset: [10, 10]
+ spread-radius: 10
+ color: [255, 0, 0]
+ border-radius: 10
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius-3.png b/gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius-3.png
new file mode 100644
index 0000000000..2b95442280
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius-3.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius-3.yaml b/gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius-3.yaml
new file mode 100644
index 0000000000..49d53e721f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius-3.yaml
@@ -0,0 +1,14 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [50, 50, 1500, 200]
+ items:
+ - type: box-shadow
+ bounds: [ 10, 10, 1500, 100 ]
+ blur-radius: 200
+ clip-mode: outset
+ offset: [10, 100]
+ spread-radius: 10
+ color: [255, 0, 0]
+ border-radius: 10
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius-ref.yaml b/gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius-ref.yaml
new file mode 100644
index 0000000000..29555c8f1d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius-ref.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [50, 50, 500, 500]
+ items:
+ - type: box-shadow
+ bounds: [ 10, 10, 400, 400 ]
+ blur-radius: 300
+ clip-mode: outset
+ offset: [0, 0]
+ spread-radius: 0
+ color: [255, 0, 0]
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius.yaml b/gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius.yaml
new file mode 100644
index 0000000000..13b2239658
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [50, 50, 500, 500]
+ items:
+ - type: box-shadow
+ bounds: [ 10, 10, 400, 400 ]
+ blur-radius: 2000
+ clip-mode: outset
+ offset: [0, 0]
+ spread-radius: 0
+ color: [255, 0, 0]
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-non-uniform-corner.png b/gfx/wr/wrench/reftests/boxshadow/box-shadow-non-uniform-corner.png
new file mode 100644
index 0000000000..84d7af5164
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-non-uniform-corner.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-non-uniform-corner.yaml b/gfx/wr/wrench/reftests/boxshadow/box-shadow-non-uniform-corner.yaml
new file mode 100644
index 0000000000..2dd7175d17
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-non-uniform-corner.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: box-shadow
+ bounds: [ 50, 50, 500, 200 ]
+ color: red
+ clip-mode: outset
+ blur-radius: 0
+ spread-radius: 10
+ border-radius: {
+ top-left: 32,
+ }
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-spread-radii-ref.yaml b/gfx/wr/wrench/reftests/boxshadow/box-shadow-spread-radii-ref.yaml
new file mode 100644
index 0000000000..e44008fcb5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-spread-radii-ref.yaml
@@ -0,0 +1,21 @@
+# This emulates the result of box-shadow-spread.yaml without with a spread
+# amount of 0 compensated by manually inflating and offsetting the rectangle
+# and border radii.
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: box-shadow
+ bounds: [ 0, 0, 280, 280 ]
+ color: blue
+ clip-mode: outset
+ offset: 300 20
+ blur-radius: 4
+ spread-radius: 0
+ border-radius:
+ top-left: [0, 0]
+ top-right: [140, 140]
+ bottom-left: [140, 140]
+ bottom-right: [0, 0]
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-spread-radii.yaml b/gfx/wr/wrench/reftests/boxshadow/box-shadow-spread-radii.yaml
new file mode 100644
index 0000000000..902c7d089b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-spread-radii.yaml
@@ -0,0 +1,18 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: box-shadow
+ bounds: [ 0, 0, 200, 200 ]
+ color: blue
+ clip-mode: outset
+ offset: 340 60
+ blur-radius: 4
+ spread-radius: 40
+ border-radius:
+ top-left: [0, 0]
+ top-right: [100, 100]
+ bottom-left: [100, 100]
+ bottom-right: [0, 0]
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-spread.png b/gfx/wr/wrench/reftests/boxshadow/box-shadow-spread.png
new file mode 100644
index 0000000000..7c1910aba4
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-spread.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-spread.yaml b/gfx/wr/wrench/reftests/boxshadow/box-shadow-spread.yaml
new file mode 100644
index 0000000000..0add751910
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-spread.yaml
@@ -0,0 +1,68 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: box-shadow
+ bounds: [ 20, 20, 80, 80 ]
+ color: blue
+ clip-mode: inset
+ spread-radius: 10
+ border-radius: 20
+
+ - type: box-shadow
+ bounds: [ 120, 20, 80, 80 ]
+ color: blue
+ clip-mode: inset
+ spread-radius: 10
+ border-radius: 25
+
+ - type: box-shadow
+ bounds: [ 220, 20, 80, 80 ]
+ color: blue
+ clip-mode: inset
+ spread-radius: 10
+ border-radius: 10
+
+ - type: box-shadow
+ bounds: [ 320, 20, 80, 80 ]
+ color: blue
+ clip-mode: inset
+ spread-radius: 10
+ border-radius: 9
+
+ - type: box-shadow
+ bounds: [ 420, 20, 80, 80 ]
+ color: blue
+ clip-mode: inset
+ spread-radius: 10
+ border-radius: 8
+
+ - type: box-shadow
+ bounds: [ 520, 20, 80, 80 ]
+ color: blue
+ clip-mode: inset
+ spread-radius: 10
+ border-radius: 7
+
+ - type: box-shadow
+ bounds: [ 620, 20, 80, 80 ]
+ color: blue
+ clip-mode: inset
+ spread-radius: 10
+ border-radius: 6
+
+ - type: box-shadow
+ bounds: [ 720, 20, 80, 80 ]
+ color: blue
+ clip-mode: inset
+ spread-radius: 10
+ border-radius: 5
+
+ - type: box-shadow
+ bounds: [ 820, 20, 80, 80 ]
+ color: blue
+ clip-mode: inset
+ spread-radius: 10
+ border-radius: 4
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-stretch-mode-x.png b/gfx/wr/wrench/reftests/boxshadow/box-shadow-stretch-mode-x.png
new file mode 100644
index 0000000000..5d631c182b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-stretch-mode-x.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-stretch-mode-x.yaml b/gfx/wr/wrench/reftests/boxshadow/box-shadow-stretch-mode-x.yaml
new file mode 100644
index 0000000000..225433e4fe
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-stretch-mode-x.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: box-shadow
+ bounds: [ 50, 50, 100, 800 ]
+ color: red
+ blur-radius: 16
+ border-radius: 32
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-stretch-mode-y.png b/gfx/wr/wrench/reftests/boxshadow/box-shadow-stretch-mode-y.png
new file mode 100644
index 0000000000..d76f58e170
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-stretch-mode-y.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-stretch-mode-y.yaml b/gfx/wr/wrench/reftests/boxshadow/box-shadow-stretch-mode-y.yaml
new file mode 100644
index 0000000000..f90366b79f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-stretch-mode-y.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: box-shadow
+ bounds: [ 50, 50, 800, 100 ]
+ color: green
+ clip-mode: inset
+ blur-radius: 16
+ border-radius: 32
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-suite-blur.png b/gfx/wr/wrench/reftests/boxshadow/box-shadow-suite-blur.png
new file mode 100644
index 0000000000..67b46a609e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-suite-blur.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-suite-blur.yaml b/gfx/wr/wrench/reftests/boxshadow/box-shadow-suite-blur.yaml
new file mode 100644
index 0000000000..29ab5a2a43
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-suite-blur.yaml
@@ -0,0 +1,154 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ # 1st row
+ - type: box-shadow
+ bounds: [ 50, 50, 100, 100 ]
+ color: red
+ clip-mode: outset
+ blur-radius: 5
+
+ - type: box-shadow
+ bounds: [ 200, 50, 100, 100 ]
+ color: red
+ clip-mode: outset
+ offset: 20 0
+ blur-radius: 5
+
+ - type: box-shadow
+ bounds: [ 350, 50, 100, 100 ]
+ color: red
+ clip-mode: outset
+ offset: 0 -40
+ blur-radius: 5
+
+ - type: box-shadow
+ bounds: [ 500, 50, 100, 100 ]
+ color: red
+ clip-mode: outset
+ spread-radius: 30
+ blur-radius: 5
+
+ - type: box-shadow
+ bounds: [ 650, 50, 100, 100 ]
+ color: red
+ clip-mode: outset
+ spread-radius: 30
+ offset: 50 -10
+ blur-radius: 5
+
+ # 2nd row
+ - type: box-shadow
+ bounds: [ 50, 250, 100, 100 ]
+ color: green
+ border-radius: 32
+ blur-radius: 5
+
+ - type: box-shadow
+ bounds: [ 200, 250, 100, 100 ]
+ color: green
+ offset: 20 0
+ border-radius: 32
+ blur-radius: 5
+
+ - type: box-shadow
+ bounds: [ 350, 250, 100, 100 ]
+ color: green
+ offset: 0 -40
+ border-radius: 32
+ blur-radius: 5
+
+ - type: box-shadow
+ bounds: [ 500, 250, 100, 100 ]
+ color: green
+ spread-radius: 30
+ border-radius: 32
+ blur-radius: 5
+
+ - type: box-shadow
+ bounds: [ 650, 250, 100, 100 ]
+ color: green
+ spread-radius: 30
+ offset: 50 -10
+ border-radius: 32
+ blur-radius: 5
+
+ # 3rd row
+ - type: box-shadow
+ bounds: [ 50, 450, 100, 100 ]
+ color: red
+ clip-mode: inset
+ blur-radius: 5
+
+ - type: box-shadow
+ bounds: [ 200, 450, 100, 100 ]
+ color: red
+ clip-mode: inset
+ offset: 20 0
+ blur-radius: 5
+
+ - type: box-shadow
+ bounds: [ 350, 450, 100, 100 ]
+ color: red
+ clip-mode: inset
+ offset: 0 -40
+ blur-radius: 5
+
+ - type: box-shadow
+ bounds: [ 500, 450, 100, 100 ]
+ color: red
+ clip-mode: inset
+ spread-radius: 30
+ blur-radius: 5
+
+ - type: box-shadow
+ bounds: [ 650, 450, 100, 100 ]
+ color: red
+ clip-mode: inset
+ spread-radius: 30
+ offset: 50 -10
+ blur-radius: 5
+
+ # 4th row
+ - type: box-shadow
+ bounds: [ 50, 650, 100, 100 ]
+ color: red
+ clip-mode: inset
+ border-radius: 32
+ blur-radius: 5
+
+ - type: box-shadow
+ bounds: [ 200, 650, 100, 100 ]
+ color: red
+ clip-mode: inset
+ offset: 20 0
+ border-radius: 32
+ blur-radius: 5
+
+ - type: box-shadow
+ bounds: [ 350, 650, 100, 100 ]
+ color: red
+ clip-mode: inset
+ offset: 0 -40
+ border-radius: 32
+ blur-radius: 5
+
+ - type: box-shadow
+ bounds: [ 500, 650, 100, 100 ]
+ color: red
+ clip-mode: inset
+ spread-radius: 30
+ border-radius: 32
+ blur-radius: 5
+
+ - type: box-shadow
+ bounds: [ 650, 650, 100, 100 ]
+ color: red
+ clip-mode: inset
+ spread-radius: 30
+ offset: 50 -10
+ border-radius: 32
+ blur-radius: 5
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-suite-no-blur.png b/gfx/wr/wrench/reftests/boxshadow/box-shadow-suite-no-blur.png
new file mode 100644
index 0000000000..66e6dd9c38
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-suite-no-blur.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/boxshadow/box-shadow-suite-no-blur.yaml b/gfx/wr/wrench/reftests/boxshadow/box-shadow-suite-no-blur.yaml
new file mode 100644
index 0000000000..8544da8ccc
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/box-shadow-suite-no-blur.yaml
@@ -0,0 +1,134 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ # 1st row
+ - type: box-shadow
+ bounds: [ 50, 50, 100, 100 ]
+ color: red
+ clip-mode: outset
+
+ - type: box-shadow
+ bounds: [ 200, 50, 100, 100 ]
+ color: red
+ clip-mode: outset
+ offset: 20 0
+
+ - type: box-shadow
+ bounds: [ 350, 50, 100, 100 ]
+ color: red
+ clip-mode: outset
+ offset: 0 -40
+
+ - type: box-shadow
+ bounds: [ 500, 50, 100, 100 ]
+ color: red
+ clip-mode: outset
+ spread-radius: 30
+
+ - type: box-shadow
+ bounds: [ 650, 50, 100, 100 ]
+ color: red
+ clip-mode: outset
+ spread-radius: 30
+ offset: 50 -10
+
+ # 2nd row
+ - type: box-shadow
+ bounds: [ 50, 250, 100, 100 ]
+ color: green
+ border-radius: 32
+
+ - type: box-shadow
+ bounds: [ 200, 250, 100, 100 ]
+ color: green
+ offset: 20 0
+ border-radius: 32
+
+ - type: box-shadow
+ bounds: [ 350, 250, 100, 100 ]
+ color: green
+ offset: 0 -40
+ border-radius: 32
+
+ - type: box-shadow
+ bounds: [ 500, 250, 100, 100 ]
+ color: green
+ spread-radius: 30
+ border-radius: 32
+
+ - type: box-shadow
+ bounds: [ 650, 250, 100, 100 ]
+ color: green
+ spread-radius: 30
+ offset: 50 -10
+ border-radius: 32
+
+ # 3rd row
+ - type: box-shadow
+ bounds: [ 50, 450, 100, 100 ]
+ color: red
+ clip-mode: inset
+
+ - type: box-shadow
+ bounds: [ 200, 450, 100, 100 ]
+ color: red
+ clip-mode: inset
+ offset: 20 0
+
+ - type: box-shadow
+ bounds: [ 350, 450, 100, 100 ]
+ color: red
+ clip-mode: inset
+ offset: 0 -40
+
+ - type: box-shadow
+ bounds: [ 500, 450, 100, 100 ]
+ color: red
+ clip-mode: inset
+ spread-radius: 30
+
+ - type: box-shadow
+ bounds: [ 650, 450, 100, 100 ]
+ color: red
+ clip-mode: inset
+ spread-radius: 30
+ offset: 50 -10
+
+ # 4th row
+ - type: box-shadow
+ bounds: [ 50, 650, 100, 100 ]
+ color: red
+ clip-mode: inset
+ border-radius: 32
+
+ - type: box-shadow
+ bounds: [ 200, 650, 100, 100 ]
+ color: red
+ clip-mode: inset
+ offset: 20 0
+ border-radius: 32
+
+ - type: box-shadow
+ bounds: [ 350, 650, 100, 100 ]
+ color: red
+ clip-mode: inset
+ offset: 0 -40
+ border-radius: 32
+
+ - type: box-shadow
+ bounds: [ 500, 650, 100, 100 ]
+ color: red
+ clip-mode: inset
+ spread-radius: 30
+ border-radius: 32
+
+ - type: box-shadow
+ bounds: [ 650, 650, 100, 100 ]
+ color: red
+ clip-mode: inset
+ spread-radius: 30
+ offset: 50 -10
+ border-radius: 32
diff --git a/gfx/wr/wrench/reftests/boxshadow/boxshadow-spread-only-ref.png b/gfx/wr/wrench/reftests/boxshadow/boxshadow-spread-only-ref.png
new file mode 100644
index 0000000000..314b17f963
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/boxshadow-spread-only-ref.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/boxshadow/boxshadow-spread-only.yaml b/gfx/wr/wrench/reftests/boxshadow/boxshadow-spread-only.yaml
new file mode 100644
index 0000000000..d8dabde293
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/boxshadow-spread-only.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 600, 600]
+ items:
+ - type: box-shadow
+ bounds: [ 40, 40, 400, 400 ]
+ blur-radius: 0
+ spread-radius: 20
+ clip-mode: outset
+ border-radius: 200
+ color: black
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-alpha.png b/gfx/wr/wrench/reftests/boxshadow/inset-alpha.png
new file mode 100644
index 0000000000..bc4c0a1f37
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-alpha.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-alpha.yaml b/gfx/wr/wrench/reftests/boxshadow/inset-alpha.yaml
new file mode 100644
index 0000000000..7764f0a276
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-alpha.yaml
@@ -0,0 +1,14 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ items:
+ - type: rect
+ bounds: [ 0, 0, 100, 100 ]
+ color: [0, 255, 0]
+ - type: box-shadow
+ bounds: [ 0, 0, 100, 100 ]
+ clip-mode: inset
+ color: [255, 255, 255, 0.1]
+ blur-radius: 100
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-border-radius-ref.yaml b/gfx/wr/wrench/reftests/boxshadow/inset-border-radius-ref.yaml
new file mode 100644
index 0000000000..62d5dd10c9
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-border-radius-ref.yaml
@@ -0,0 +1,16 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ items:
+ - type: clip
+ bounds: [0, 0, 100, 100]
+ complex:
+ - rect: [ 10, 10, 80, 80 ]
+ radius: 10
+ items:
+ - type: box-shadow
+ bounds: [ 10, 10, 80, 80 ]
+ blur-radius: 25
+ clip-mode: inset
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-border-radius.png b/gfx/wr/wrench/reftests/boxshadow/inset-border-radius.png
new file mode 100644
index 0000000000..1572de5b77
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-border-radius.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-border-radius.yaml b/gfx/wr/wrench/reftests/boxshadow/inset-border-radius.yaml
new file mode 100644
index 0000000000..cb5c274f5a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-border-radius.yaml
@@ -0,0 +1,11 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ items:
+ - type: box-shadow
+ bounds: [ 10, 10, 80, 80 ]
+ blur-radius: 25
+ clip-mode: inset
+ border-radius: 10
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-downscale.png b/gfx/wr/wrench/reftests/boxshadow/inset-downscale.png
new file mode 100644
index 0000000000..aef7968600
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-downscale.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-downscale.yaml b/gfx/wr/wrench/reftests/boxshadow/inset-downscale.yaml
new file mode 100644
index 0000000000..dd0c3abc2d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-downscale.yaml
@@ -0,0 +1,10 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [50, 50, 400, 200]
+ items:
+ - type: box-shadow
+ bounds: [ 10, 10, 400, 200 ]
+ blur-radius: 50
+ clip-mode: inset
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-empty.yaml b/gfx/wr/wrench/reftests/boxshadow/inset-empty.yaml
new file mode 100644
index 0000000000..85d2840546
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-empty.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [50, 50, 500, 500]
+ items:
+ - type: box-shadow
+ bounds: [ 10, 10, 400, 400 ]
+ blur-radius: 0
+ clip-mode: inset
+ offset: [0, 0]
+ spread-radius: 0
+ color: [255, 0, 0]
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-large-offset-ref.png b/gfx/wr/wrench/reftests/boxshadow/inset-large-offset-ref.png
new file mode 100644
index 0000000000..e1d189eaa0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-large-offset-ref.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-large-offset.yaml b/gfx/wr/wrench/reftests/boxshadow/inset-large-offset.yaml
new file mode 100644
index 0000000000..1cc2355d02
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-large-offset.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [50, 50, 500, 500]
+ items:
+ - type: box-shadow
+ bounds: [ 10, 10, 400, 400 ]
+ blur-radius: 1
+ clip-mode: inset
+ offset: [0, 200]
+ spread-radius: 0
+ color: [255, 0, 0]
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-mask-region.png b/gfx/wr/wrench/reftests/boxshadow/inset-mask-region.png
new file mode 100644
index 0000000000..b696cbfd41
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-mask-region.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-mask-region.yaml b/gfx/wr/wrench/reftests/boxshadow/inset-mask-region.yaml
new file mode 100644
index 0000000000..08f52af181
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-mask-region.yaml
@@ -0,0 +1,11 @@
+---
+root:
+ items:
+ - type: stacking-context
+ items:
+ - type: box-shadow
+ bounds: [ 50, 50, 400.1, 400.1 ]
+ color: red
+ blur-radius: 10
+ clip-mode: inset
+ border-radius: 185
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-neg-offset.png b/gfx/wr/wrench/reftests/boxshadow/inset-neg-offset.png
new file mode 100644
index 0000000000..b6b232a1d0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-neg-offset.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-neg-offset.yaml b/gfx/wr/wrench/reftests/boxshadow/inset-neg-offset.yaml
new file mode 100644
index 0000000000..9fe63176c0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-neg-offset.yaml
@@ -0,0 +1,18 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: box-shadow
+ bounds: [ 22, 22, 200, 80 ]
+ color: blue
+ clip-mode: inset
+ offset: -2 -2
+ blur-radius: 20
+ spread-radius: 0
+ border-radius:
+ top-left: [0, 0]
+ top-right: [0, 0]
+ bottom-left: [0, 0]
+ bottom-right: [0, 0]
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-no-blur-radius-ref.png b/gfx/wr/wrench/reftests/boxshadow/inset-no-blur-radius-ref.png
new file mode 100644
index 0000000000..9c6a8d4bcf
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-no-blur-radius-ref.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-no-blur-radius.yaml b/gfx/wr/wrench/reftests/boxshadow/inset-no-blur-radius.yaml
new file mode 100644
index 0000000000..eb9c783718
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-no-blur-radius.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ items:
+ - type: box-shadow
+ bounds: [ 10, 10, 80, 80 ]
+ blur-radius: 0
+ clip-mode: inset
+ border-radius: 10
+ offset: 10 10
+ color: [255, 0, 0]
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-offset.png b/gfx/wr/wrench/reftests/boxshadow/inset-offset.png
new file mode 100644
index 0000000000..d5f41456fe
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-offset.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-offset.yaml b/gfx/wr/wrench/reftests/boxshadow/inset-offset.yaml
new file mode 100644
index 0000000000..17fa232f3a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-offset.yaml
@@ -0,0 +1,18 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: box-shadow
+ bounds: [ 22, 22, 200, 80 ]
+ color: blue
+ clip-mode: inset
+ offset: 5 5
+ blur-radius: 20
+ spread-radius: 0
+ border-radius:
+ top-left: [0, 0]
+ top-right: [0, 0]
+ bottom-left: [0, 0]
+ bottom-right: [0, 0]
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-simple-ref.yaml b/gfx/wr/wrench/reftests/boxshadow/inset-simple-ref.yaml
new file mode 100644
index 0000000000..526a1fa4e0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-simple-ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [50, 50, 100, 100]
+ items:
+ - type: rect
+ bounds: [ 10, 10, 80, 80 ]
+ color: [0, 255, 0]
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-simple.yaml b/gfx/wr/wrench/reftests/boxshadow/inset-simple.yaml
new file mode 100644
index 0000000000..78f6ef18c1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-simple.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [50, 50, 100, 100]
+ 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
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-spread-large-ref.yaml b/gfx/wr/wrench/reftests/boxshadow/inset-spread-large-ref.yaml
new file mode 100644
index 0000000000..bdeec79e35
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-spread-large-ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [50, 50, 100, 100]
+ items:
+ - type: rect
+ bounds: [ 10, 10, 80, 80 ]
+ color: [255, 0, 0]
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-spread-large.yaml b/gfx/wr/wrench/reftests/boxshadow/inset-spread-large.yaml
new file mode 100644
index 0000000000..05cc5e40f2
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-spread-large.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [50, 50, 100, 100]
+ items:
+ - type: box-shadow
+ bounds: [ 10, 10, 80, 80 ]
+ blur-radius: 5
+ clip-mode: inset
+ spread-radius: 200
+ color: [255, 0, 0]
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-spread-ref.yaml b/gfx/wr/wrench/reftests/boxshadow/inset-spread-ref.yaml
new file mode 100644
index 0000000000..76802f9db0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-spread-ref.yaml
@@ -0,0 +1,10 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [50, 50, 100, 100]
+ items:
+ - type: rect
+ bounds: [ 10, 10, 80, 80 ]
+ color: [255, 255, 255]
+
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-spread.yaml b/gfx/wr/wrench/reftests/boxshadow/inset-spread.yaml
new file mode 100644
index 0000000000..de09797dcf
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-spread.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [50, 50, 100, 100]
+ items:
+ - type: box-shadow
+ bounds: [ 10, 10, 80, 80 ]
+ blur-radius: 5
+ clip-mode: inset
+ spread-radius: 20
+ color: [0, 0, 0]
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-subpx.png b/gfx/wr/wrench/reftests/boxshadow/inset-subpx.png
new file mode 100644
index 0000000000..89ca1e44b1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-subpx.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/boxshadow/inset-subpx.yaml b/gfx/wr/wrench/reftests/boxshadow/inset-subpx.yaml
new file mode 100644
index 0000000000..22dc3f0959
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/inset-subpx.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: box-shadow
+ bounds: [ 76, 67.5, 59.016666, 39 ]
+ offset: [30, 9]
+ blur-radius: 7
+ color: blue
+ clip-mode: inset
diff --git a/gfx/wr/wrench/reftests/boxshadow/invalid-ref.yaml b/gfx/wr/wrench/reftests/boxshadow/invalid-ref.yaml
new file mode 100644
index 0000000000..4f2245ccaa
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/invalid-ref.yaml
@@ -0,0 +1,5 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
diff --git a/gfx/wr/wrench/reftests/boxshadow/invalid.yaml b/gfx/wr/wrench/reftests/boxshadow/invalid.yaml
new file mode 100644
index 0000000000..6c1fecb995
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/invalid.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: box-shadow
+ bounds: [ 100, 100, 200, 10 ]
+ blur-radius: 10.5
+ offset: [15, 15]
+ spread-radius: -15
+ color: red
diff --git a/gfx/wr/wrench/reftests/boxshadow/no-stretch.png b/gfx/wr/wrench/reftests/boxshadow/no-stretch.png
new file mode 100644
index 0000000000..e916a7d382
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/no-stretch.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/boxshadow/no-stretch.yaml b/gfx/wr/wrench/reftests/boxshadow/no-stretch.yaml
new file mode 100644
index 0000000000..4276ffa929
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/no-stretch.yaml
@@ -0,0 +1,21 @@
+# Test that box-shadows with overlapping corners
+# are drawn with simple mode (not stretched).
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: box-shadow
+ bounds: [ 50, 50, 150, 150 ]
+ color: red
+ clip-mode: outset
+ blur-radius: 2.4
+ offset: [100, 0]
+ spread-radius: -40
+ border-radius: {
+ top-left: 112,
+ top-right: 48,
+ bottom-left: 48,
+ bottom-right: 112,
+ }
diff --git a/gfx/wr/wrench/reftests/boxshadow/overlap1.png b/gfx/wr/wrench/reftests/boxshadow/overlap1.png
new file mode 100644
index 0000000000..a47c3ad696
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/overlap1.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/boxshadow/overlap1.yaml b/gfx/wr/wrench/reftests/boxshadow/overlap1.yaml
new file mode 100644
index 0000000000..1744983da6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/overlap1.yaml
@@ -0,0 +1,18 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: box-shadow
+ bounds: [ 50, 10, 200, 80 ]
+ color: black
+ clip-mode: outset
+ offset: 0 150
+ blur-radius: 20
+ border-radius:
+ top-left: [100, 40]
+ top-right: [0, 0]
+ bottom-left: [100, 40]
+ bottom-right: [0, 0]
+
diff --git a/gfx/wr/wrench/reftests/boxshadow/overlap2.png b/gfx/wr/wrench/reftests/boxshadow/overlap2.png
new file mode 100644
index 0000000000..6b7b176011
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/overlap2.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/boxshadow/overlap2.yaml b/gfx/wr/wrench/reftests/boxshadow/overlap2.yaml
new file mode 100644
index 0000000000..59ffb2c438
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/overlap2.yaml
@@ -0,0 +1,19 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: box-shadow
+ bounds: [ 0, 0, 200, 200 ]
+ color: red
+ clip-mode: outset
+ offset: 200 0
+ blur-radius: 1
+ spread-radius: 0
+ border-radius:
+ top-left: [100, 100]
+ top-right: [0, 0]
+ bottom-left: [0, 0]
+ bottom-right: [150, 150]
+
diff --git a/gfx/wr/wrench/reftests/boxshadow/reftest.list b/gfx/wr/wrench/reftests/boxshadow/reftest.list
new file mode 100644
index 0000000000..7efc35e783
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/reftest.list
@@ -0,0 +1,38 @@
+!= inset-simple.yaml inset-simple-ref.yaml
+!= inset-spread.yaml inset-spread-ref.yaml
+fuzzy-if(platform(swgl),3,2) == inset-no-blur-radius.yaml inset-no-blur-radius-ref.png
+== inset-spread-large.yaml inset-spread-large-ref.yaml
+platform(linux,mac) == inset-alpha.yaml inset-alpha.png
+platform(linux,mac) == boxshadow-spread-only.yaml boxshadow-spread-only-ref.png
+== box-shadow-clip.yaml box-shadow-clip-ref.yaml
+fuzzy(1,402) fuzzy-if(platform(swgl),2,1208) fuzzy-if(env(android,device),6,399) == inset-large-offset.yaml inset-large-offset-ref.png
+platform(linux,mac) == inset-border-radius.yaml inset-border-radius.png
+platform(linux,mac) == inset-offset.yaml inset-offset.png
+platform(linux,mac) == inset-neg-offset.yaml inset-neg-offset.png
+== box-shadow-empty.yaml blank.yaml
+platform(linux,mac) == box-shadow-suite-no-blur.yaml box-shadow-suite-no-blur.png
+platform(linux,mac) fuzzy(1,84) == box-shadow-suite-blur.yaml box-shadow-suite-blur.png
+fuzzy(1,8) == box-shadow-large-blur-radius.yaml box-shadow-large-blur-radius-ref.yaml
+fuzzy(1,6388) == rounding.yaml rounding-ref.yaml
+platform(linux,mac) == box-shadow-border-radii.yaml box-shadow-border-radii.png
+skip_on(android) fuzzy-if(platform(swgl),9,34) == box-shadow-spread.yaml box-shadow-spread.png # Too wide for Android
+== box-shadow-spread-radii.yaml box-shadow-spread-radii-ref.yaml
+== invalid.yaml invalid-ref.yaml
+== inset-empty.yaml blank.yaml
+platform(linux,mac) == inset-subpx.yaml inset-subpx.png
+platform(linux,mac) fuzzy(1,4) == inset-downscale.yaml inset-downscale.png
+platform(linux,mac) fuzzy(1,979) == box-shadow-cache.yaml box-shadow-cache.png
+platform(linux,mac) fuzzy(1,685) == overlap1.yaml overlap1.png
+fuzzy(2,757) fuzzy-if(platform(swgl),2,2414) == overlap2.yaml overlap2.png
+platform(linux,mac) fuzzy(1,48) == no-stretch.yaml no-stretch.png
+platform(linux,mac) fuzzy(1,9) == box-shadow-stretch-mode-x.yaml box-shadow-stretch-mode-x.png
+platform(linux,mac) fuzzy(1,41) == box-shadow-stretch-mode-y.yaml box-shadow-stretch-mode-y.png
+platform(linux,mac) fuzzy(1,14) == inset-mask-region.yaml inset-mask-region.png
+== box-shadow-blurred-overlapping-radii.yaml box-shadow-blurred-overlapping-radii-ref.yaml
+
+fuzzy(1,5) platform(linux,mac) == box-shadow-huge-radius.yaml box-shadow-huge-radius.png
+
+platform(linux,mac) == box-shadow-large-blur-radius-2.yaml box-shadow-large-blur-radius-2.png
+platform(linux,mac) fuzzy(1,8) == box-shadow-large-blur-radius-3.yaml box-shadow-large-blur-radius-3.png
+platform(linux,mac) fuzzy(1,79) == scale.yaml scale.png
+platform(linux,mac) fuzzy(1,4) == box-shadow-non-uniform-corner.yaml box-shadow-non-uniform-corner.png
diff --git a/gfx/wr/wrench/reftests/boxshadow/rounding-ref.yaml b/gfx/wr/wrench/reftests/boxshadow/rounding-ref.yaml
new file mode 100644
index 0000000000..e6e2fb8464
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/rounding-ref.yaml
@@ -0,0 +1,18 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: rect
+ bounds: [100, 100, 500, 500]
+ color: green
+ - type: rect
+ bounds: [200, 200, 300, 300]
+ color: black
+ - type: box-shadow
+ bounds: [200, 200, 300, 300]
+ blur-radius: 20
+ clip-mode: inset
+ spread-radius: 100
+ color: green
diff --git a/gfx/wr/wrench/reftests/boxshadow/rounding.yaml b/gfx/wr/wrench/reftests/boxshadow/rounding.yaml
new file mode 100644
index 0000000000..8078b2e1d6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/rounding.yaml
@@ -0,0 +1,15 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: rect
+ bounds: [100, 100, 500, 500]
+ color: black
+ - type: box-shadow
+ bounds: [100, 100, 500, 500]
+ blur-radius: 20
+ clip-mode: inset
+ spread-radius: 200
+ color: green
diff --git a/gfx/wr/wrench/reftests/boxshadow/scale.png b/gfx/wr/wrench/reftests/boxshadow/scale.png
new file mode 100644
index 0000000000..58505f2845
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/scale.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/boxshadow/scale.yaml b/gfx/wr/wrench/reftests/boxshadow/scale.yaml
new file mode 100644
index 0000000000..12263b4eb9
--- /dev/null
+++ b/gfx/wr/wrench/reftests/boxshadow/scale.yaml
@@ -0,0 +1,183 @@
+# Test that box shadows are drawn with correct symmetry in
+# a variety of cases. This test checks for various scale
+# factors, in combination with box shadows clipped by the
+# screen rect. It also has come large box shadows that check
+# the symmetry still works with segmentation enabled.
+---
+root:
+ items:
+ - type: stacking-context
+ transform: scale(0.3)
+ items:
+ - type: box-shadow
+ bounds: [ -70, 50, 100, 100 ]
+ color: black
+ blur-radius: 1
+ clip-mode: inset
+ - type: stacking-context
+ transform: scale(0.4)
+ items:
+ - type: box-shadow
+ bounds: [ -70, 150, 100, 100 ]
+ color: black
+ blur-radius: 1
+ clip-mode: inset
+ - type: stacking-context
+ transform: scale(0.5)
+ items:
+ - type: box-shadow
+ bounds: [ -70, 250, 100, 100 ]
+ color: black
+ blur-radius: 1
+ clip-mode: inset
+ - type: stacking-context
+ transform: scale(0.6)
+ items:
+ - type: box-shadow
+ bounds: [ -70, 350, 100, 100 ]
+ color: black
+ blur-radius: 1
+ clip-mode: inset
+ - type: stacking-context
+ transform: scale(0.7)
+ items:
+ - type: box-shadow
+ bounds: [ -70, 450, 100, 100 ]
+ color: black
+ blur-radius: 1
+ clip-mode: inset
+ - type: stacking-context
+ transform: scale(0.73)
+ items:
+ - type: box-shadow
+ bounds: [ -70, 550, 100, 100 ]
+ color: black
+ blur-radius: 1
+ clip-mode: inset
+ - type: stacking-context
+ transform: scale(0.75)
+ items:
+ - type: box-shadow
+ bounds: [ -70, 650, 100, 100 ]
+ color: black
+ blur-radius: 1
+ clip-mode: inset
+ - type: stacking-context
+ transform: scale(0.77)
+ items:
+ - type: box-shadow
+ bounds: [ -70, 750, 100, 100 ]
+ color: black
+ blur-radius: 1
+ clip-mode: inset
+ - type: stacking-context
+ transform: scale(0.8)
+ items:
+ - type: box-shadow
+ bounds: [ -70, 850, 100, 100 ]
+ color: black
+ blur-radius: 1
+ clip-mode: inset
+ - type: stacking-context
+ items:
+ - type: box-shadow
+ bounds: [ -70, 800, 100, 100 ]
+ color: black
+ blur-radius: 1
+ clip-mode: inset
+
+ - type: stacking-context
+ transform: scale(0.3)
+ items:
+ - type: box-shadow
+ bounds: [ 50, 50, 100, 100 ]
+ color: black
+ blur-radius: 1
+ clip-mode: inset
+ - type: stacking-context
+ transform: scale(0.4)
+ items:
+ - type: box-shadow
+ bounds: [ 50, 150, 100, 100 ]
+ color: black
+ blur-radius: 1
+ clip-mode: inset
+ - type: stacking-context
+ transform: scale(0.5)
+ items:
+ - type: box-shadow
+ bounds: [ 50, 250, 100, 100 ]
+ color: black
+ blur-radius: 1
+ clip-mode: inset
+ - type: stacking-context
+ transform: scale(0.6)
+ items:
+ - type: box-shadow
+ bounds: [ 50, 350, 100, 100 ]
+ color: black
+ blur-radius: 1
+ clip-mode: inset
+ - type: stacking-context
+ transform: scale(0.7)
+ items:
+ - type: box-shadow
+ bounds: [ 50, 450, 100, 100 ]
+ color: black
+ blur-radius: 1
+ clip-mode: inset
+ - type: stacking-context
+ transform: scale(0.73)
+ items:
+ - type: box-shadow
+ bounds: [ 50, 550, 100, 100 ]
+ color: black
+ blur-radius: 1
+ clip-mode: inset
+ - type: stacking-context
+ transform: scale(0.75)
+ items:
+ - type: box-shadow
+ bounds: [ 50, 650, 100, 100 ]
+ color: black
+ blur-radius: 1
+ clip-mode: inset
+ - type: stacking-context
+ transform: scale(0.77)
+ items:
+ - type: box-shadow
+ bounds: [ 50, 750, 100, 100 ]
+ color: black
+ blur-radius: 1
+ clip-mode: inset
+ - type: stacking-context
+ transform: scale(0.8)
+ items:
+ - type: box-shadow
+ bounds: [ 50, 850, 100, 100 ]
+ color: black
+ blur-radius: 1
+ clip-mode: inset
+ - type: stacking-context
+ items:
+ - type: box-shadow
+ bounds: [ 50, 800, 100, 100 ]
+ color: black
+ blur-radius: 1
+ clip-mode: inset
+
+ - type: stacking-context
+ items:
+ - type: box-shadow
+ bounds: [ 200, -100, 600, 200 ]
+ color: black
+ blur-radius: 5
+ clip-mode: inset
+
+ - type: stacking-context
+ items:
+ - type: box-shadow
+ bounds: [ 200, 200, 600, 200 ]
+ color: black
+ blur-radius: 5
+ clip-mode: outset
diff --git a/gfx/wr/wrench/reftests/clip/blank.yaml b/gfx/wr/wrench/reftests/clip/blank.yaml
new file mode 100644
index 0000000000..c4eb3ab673
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/blank.yaml
@@ -0,0 +1,2 @@
+---
+root:
diff --git a/gfx/wr/wrench/reftests/clip/blend-container-ref.yaml b/gfx/wr/wrench/reftests/clip/blend-container-ref.yaml
new file mode 100644
index 0000000000..2d3464959d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/blend-container-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [ 50, 50, 100, 100 ]
+ color: blue
diff --git a/gfx/wr/wrench/reftests/clip/blend-container.yaml b/gfx/wr/wrench/reftests/clip/blend-container.yaml
new file mode 100644
index 0000000000..5506f2d25b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/blend-container.yaml
@@ -0,0 +1,19 @@
+# Verify that clip-chains are correctly applied to root level blend containers (which get
+# optimized to be tile caches).
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ bounds: [ 50, 50, 100, 100 ]
+ -
+ type: "clip-chain"
+ id: 3
+ clips: [2]
+ - type: stacking-context
+ blend-container: true
+ clip-chain: 3
+ items:
+ - type: rect
+ bounds: [ 0, 0, 200, 200 ]
+ color: blue
diff --git a/gfx/wr/wrench/reftests/clip/border-with-rounded-clip.png b/gfx/wr/wrench/reftests/clip/border-with-rounded-clip.png
new file mode 100644
index 0000000000..120b21b6cc
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/border-with-rounded-clip.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/clip/border-with-rounded-clip.yaml b/gfx/wr/wrench/reftests/clip/border-with-rounded-clip.yaml
new file mode 100644
index 0000000000..a41f9aeea7
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/border-with-rounded-clip.yaml
@@ -0,0 +1,23 @@
+---
+root:
+ items:
+ -
+ clip-rect: [10, 10, 938, 200]
+ type: clip
+ id: 2
+ complex:
+ -
+ rect: [10, 10, 938, 200]
+ radius: [97, 97]
+ "clip-mode": clip
+ - type: clip-chain
+ id: 12
+ clips: [2]
+ -
+ bounds: [10, 10, 100, 200]
+ type: border
+ width: [100, 50, 100, 50]
+ border-type: normal
+ color: green
+ style: solid
+ clip-chain: 12
diff --git a/gfx/wr/wrench/reftests/clip/clip-3d-transform-ref.yaml b/gfx/wr/wrench/reftests/clip/clip-3d-transform-ref.yaml
new file mode 100644
index 0000000000..58d66bec5a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/clip-3d-transform-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ -
+ bounds: [0, 0, 200, 200]
+ "clip-rect": [0, 0, 200, 200]
+ type: rect
+ color: green
diff --git a/gfx/wr/wrench/reftests/clip/clip-3d-transform.yaml b/gfx/wr/wrench/reftests/clip/clip-3d-transform.yaml
new file mode 100644
index 0000000000..c40780ca22
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/clip-3d-transform.yaml
@@ -0,0 +1,33 @@
+# This tests that clipping works well inside of a transformation with a 3d component that
+# is still axis-aligned and lacking a perspective component. These two spaces should exist
+# within a compatible coordinate system for local clipping (no masking).
+---
+root:
+ items:
+ -
+ bounds: [0, 0, 0, 0]
+ "clip-rect": [0, 0, 0, 0]
+ "backface-visible": true
+ type: "stacking-context"
+ "scroll-policy": scrollable
+ transform: [0.5, 0, -0.8660254, 0, 0, 1, 0, 0, 0.8660254, 0, 0.5, 0, 0, 0, 0, 1]
+ "transform-style": flat
+ filters: []
+ items:
+ -
+ bounds: [0, 0, 800, 400]
+ "clip-rect": [0, 0, 800, 400]
+ "backface-visible": true
+ type: clip
+ id: 2
+ "content-size": [800, 400]
+ - type: clip-chain
+ id: 3
+ clips: [2]
+ -
+ bounds: [0, 0, 400, 200]
+ "clip-rect": [0, 0, 400, 200]
+ "backface-visible": true
+ type: rect
+ clip-chain: 3
+ color: green
diff --git a/gfx/wr/wrench/reftests/clip/clip-45-degree-rotation-ref.png b/gfx/wr/wrench/reftests/clip/clip-45-degree-rotation-ref.png
new file mode 100644
index 0000000000..7b27e84239
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/clip-45-degree-rotation-ref.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/clip/clip-45-degree-rotation.yaml b/gfx/wr/wrench/reftests/clip/clip-45-degree-rotation.yaml
new file mode 100644
index 0000000000..018a8a1d78
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/clip-45-degree-rotation.yaml
@@ -0,0 +1,35 @@
+# Test that transformed content is clipped properly by clips with a different transform.
+---
+root:
+ items:
+ -
+ bounds: [0, 0, 0, 0]
+ "clip-rect": [0, 0, 0, 0]
+ type: "stacking-context"
+ transform: rotate(-45) translate(200, 0)
+ items:
+ -
+ bounds: [0, 0, 300, 300]
+ "clip-rect": [0, 0, 300, 300]
+ type: rect
+ color: red
+ -
+ bounds: [0, 0, 300, 300]
+ "clip-rect": [0, 0, 300, 300]
+ type: clip
+ id: 5
+ - type: clip-chain
+ id: 10
+ clips: [5]
+ -
+ bounds: [0, 0, 0, 0]
+ "clip-rect": [0, 0, 0, 0]
+ type: "stacking-context"
+ transform: rotate(45) translate(-300, 0)
+ clip-chain: 10
+ items:
+ -
+ bounds: [0, 0, 1598, 1200]
+ "clip-rect": [0, 0, 1598, 1200]
+ type: rect
+ color: green
diff --git a/gfx/wr/wrench/reftests/clip/clip-and-filter-with-rotation-ref.yaml b/gfx/wr/wrench/reftests/clip/clip-and-filter-with-rotation-ref.yaml
new file mode 100644
index 0000000000..c092030714
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/clip-and-filter-with-rotation-ref.yaml
@@ -0,0 +1,18 @@
+# Test that filtered content is clipped properly in a rotated context
+---
+root:
+ items:
+ -
+ bounds: [16, 16, 0, 0]
+ type: "reference-frame"
+ transform: rotate(10)
+ id: 7
+ items:
+ -
+ bounds: [0, 0, 0, 0]
+ type: "stacking-context"
+ items:
+ -
+ bounds: [0, 0, 324, 295]
+ type: rect
+ color: red
diff --git a/gfx/wr/wrench/reftests/clip/clip-and-filter-with-rotation.yaml b/gfx/wr/wrench/reftests/clip/clip-and-filter-with-rotation.yaml
new file mode 100644
index 0000000000..dcd2668220
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/clip-and-filter-with-rotation.yaml
@@ -0,0 +1,36 @@
+# Test that filtered content is clipped properly in a rotated context
+---
+root:
+ items:
+ -
+ bounds: [16, 16, 0, 0]
+ type: "reference-frame"
+ transform: rotate(10)
+ id: 7
+ items:
+ -
+ bounds: [0, 0, 0, 0]
+ type: "stacking-context"
+ items:
+ -
+ bounds: [0, 0, 324, 295]
+ type: clip
+ id: 2
+ -
+ type: "clip-chain"
+ id: 3
+ clips: [2]
+ -
+ bounds: [0, 0, 0, 0]
+ clip-chain: 3
+ type: "stacking-context"
+ filters: ["contrast(1.1)"] # any blend-style filter will do
+ items:
+ -
+ bounds: [-150, -150, 624, 624]
+ type: rect
+ color: blue
+ -
+ bounds: [-2, -2, 328, 299]
+ type: rect
+ color: red
diff --git a/gfx/wr/wrench/reftests/clip/clip-corner-overlap-ref.yaml b/gfx/wr/wrench/reftests/clip/clip-corner-overlap-ref.yaml
new file mode 100644
index 0000000000..9f395a9836
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/clip-corner-overlap-ref.yaml
@@ -0,0 +1,117 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [ 50, 50, 200, 100 ]
+ radius:
+ top-left: [200, 100]
+ top-right: [0, 0]
+ bottom-left: [0, 0]
+ bottom-right: [0, 0]
+ - type: rect
+ bounds: [ 50, 50, 200, 100 ]
+ color: blue
+ clip-chain: [2]
+
+ - type: clip
+ id: 3
+ complex:
+ - rect: [ 50, 150, 200, 100 ]
+ radius:
+ top-left: [0, 0]
+ top-right: [0, 0]
+ bottom-left: [200, 100]
+ bottom-right: [0, 0]
+ - type: rect
+ bounds: [ 50, 150, 200, 100 ]
+ color: blue
+ clip-chain: [3]
+
+ - type: clip
+ id: 4
+ complex:
+ - rect: [ 300, 50, 100, 200 ]
+ radius:
+ top-left: [100, 200]
+ top-right: [0, 0]
+ bottom-left: [0, 0]
+ bottom-right: [0, 0]
+ - type: rect
+ bounds: [ 300, 50, 100, 200 ]
+ color: green
+ clip-chain: [4]
+
+ - type: clip
+ id: 5
+ complex:
+ - rect: [ 400, 50, 100, 200 ]
+ radius:
+ top-left: [0, 0]
+ top-right: [100, 200]
+ bottom-left: [0, 0]
+ bottom-right: [0, 0]
+ - type: rect
+ bounds: [ 400, 50, 100, 200 ]
+ color: green
+ clip-chain: [5]
+
+ - type: clip
+ id: 6
+ complex:
+ - rect: [ 50, 300, 200, 100 ]
+ radius:
+ top-left: [0, 0]
+ top-right: [200, 100]
+ bottom-left: [0, 0]
+ bottom-right: [0, 0]
+ - type: rect
+ bounds: [ 50, 300, 200, 100 ]
+ color: red
+ clip-chain: [6]
+
+ - type: clip
+ id: 7
+ complex:
+ - rect: [ 50, 400, 200, 100 ]
+ radius:
+ top-left: [0, 0]
+ top-right: [0, 0]
+ bottom-left: [0, 0]
+ bottom-right: [200, 100]
+ - type: rect
+ bounds: [ 50, 400, 200, 100 ]
+ color: red
+ clip-chain: [7]
+
+ - type: clip
+ id: 8
+ complex:
+ - rect: [ 300, 300, 100, 200 ]
+ radius:
+ top-left: [0, 0]
+ top-right: [0, 0]
+ bottom-left: [100, 200]
+ bottom-right: [0, 0]
+ - type: rect
+ bounds: [ 300, 300, 100, 200 ]
+ color: yellow
+ clip-chain: [8]
+
+ - type: clip
+ id: 9
+ complex:
+ - rect: [ 400, 300, 100, 200 ]
+ radius:
+ top-left: [0, 0]
+ top-right: [0, 0]
+ bottom-left: [0, 0]
+ bottom-right: [100, 200]
+ - type: rect
+ bounds: [ 400, 300, 100, 200 ]
+ color: yellow
+ clip-chain: [9]
diff --git a/gfx/wr/wrench/reftests/clip/clip-corner-overlap.yaml b/gfx/wr/wrench/reftests/clip/clip-corner-overlap.yaml
new file mode 100644
index 0000000000..46b562eba5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/clip-corner-overlap.yaml
@@ -0,0 +1,61 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [ 50, 50, 200, 200 ]
+ radius:
+ top-left: [200, 100]
+ top-right: [0, 0]
+ bottom-left: [200, 100]
+ bottom-right: [0, 0]
+ - type: rect
+ bounds: [ 50, 50, 200, 200 ]
+ color: blue
+ clip-chain: [2]
+
+ - type: clip
+ id: 3
+ complex:
+ - rect: [ 300, 50, 200, 200 ]
+ radius:
+ top-left: [100, 200]
+ top-right: [100, 200]
+ bottom-left: [0, 0]
+ bottom-right: [0, 0]
+ - type: rect
+ bounds: [ 300, 50, 200, 200 ]
+ color: green
+ clip-chain: [3]
+
+ - type: clip
+ id: 4
+ complex:
+ - rect: [ 50, 300, 200, 200 ]
+ radius:
+ top-left: [0, 0]
+ top-right: [200, 100]
+ bottom-left: [0, 0]
+ bottom-right: [200, 100]
+ - type: rect
+ bounds: [ 50, 300, 200, 200 ]
+ color: red
+ clip-chain: [4]
+
+ - type: clip
+ id: 5
+ complex:
+ - rect: [ 300, 300, 200, 200 ]
+ radius:
+ top-left: [0, 0]
+ top-right: [0, 0]
+ bottom-left: [100, 200]
+ bottom-right: [100, 200]
+ - type: rect
+ bounds: [ 300, 300, 200, 200 ]
+ color: yellow
+ clip-chain: [5]
diff --git a/gfx/wr/wrench/reftests/clip/clip-ellipse.png b/gfx/wr/wrench/reftests/clip/clip-ellipse.png
new file mode 100644
index 0000000000..49570d0359
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/clip-ellipse.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/clip/clip-ellipse.yaml b/gfx/wr/wrench/reftests/clip/clip-ellipse.yaml
new file mode 100644
index 0000000000..846fefae89
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/clip-ellipse.yaml
@@ -0,0 +1,110 @@
+---
+root:
+ items:
+ - type: clip
+ id: 100
+ complex:
+ - rect: [20, 20, 100, 100]
+ radius: [32, 16]
+ - type: clip-chain
+ id: 200
+ clips: [100]
+ - type: rect
+ bounds: [20, 20, 100, 100]
+ color: red
+ clip-chain: 200
+
+ - type: clip
+ id: 101
+ complex:
+ - rect: [130, 20, 100, 100]
+ radius: [32, 16]
+ clip-mode: clip-out
+ - type: clip-chain
+ id: 201
+ clips: [101]
+ - type: rect
+ bounds: [130, 20, 100, 100]
+ color: green
+ clip-chain: 201
+
+ - type: clip
+ id: 102
+ complex:
+ - rect: [20, 130, 100, 100]
+ radius: [16, 32]
+ - type: clip-chain
+ id: 202
+ clips: [102]
+ - type: rect
+ bounds: [20, 130, 100, 100]
+ color: red
+ clip-chain: 202
+
+ - type: clip
+ id: 103
+ complex:
+ - rect: [130, 130, 100, 100]
+ radius: [16, 32]
+ clip-mode: clip-out
+ - type: clip-chain
+ id: 203
+ clips: [103]
+ - type: rect
+ bounds: [130, 130, 100, 100]
+ color: green
+ clip-chain: 203
+
+ - type: clip
+ id: 104
+ complex:
+ - rect: [20, 240, 100, 100]
+ radius: [128, 32]
+ - type: clip-chain
+ id: 204
+ clips: [104]
+ - type: rect
+ bounds: [20, 240, 100, 100]
+ color: red
+ clip-chain: 204
+
+ - type: clip
+ id: 105
+ complex:
+ - rect: [130, 240, 100, 100]
+ radius: [128, 32]
+ clip-mode: clip-out
+ - type: clip-chain
+ id: 205
+ clips: [105]
+ - type: rect
+ bounds: [130, 240, 100, 100]
+ color: green
+ clip-chain: 205
+
+ - type: clip
+ id: 106
+ complex:
+ - rect: [20, 350, 100, 100]
+ radius: [32, 128]
+ - type: clip-chain
+ id: 206
+ clips: [106]
+ - type: rect
+ bounds: [20, 350, 100, 100]
+ color: red
+ clip-chain: 206
+
+ - type: clip
+ id: 107
+ complex:
+ - rect: [130, 350, 100, 100]
+ radius: [32, 128]
+ clip-mode: clip-out
+ - type: clip-chain
+ id: 207
+ clips: [107]
+ - type: rect
+ bounds: [130, 350, 100, 100]
+ color: green
+ clip-chain: 207
diff --git a/gfx/wr/wrench/reftests/clip/clip-empty-inner-rect-ref.yaml b/gfx/wr/wrench/reftests/clip/clip-empty-inner-rect-ref.yaml
new file mode 100644
index 0000000000..c41bcee382
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/clip-empty-inner-rect-ref.yaml
@@ -0,0 +1,20 @@
+# In this case, the inner rectangle of the clip is empty, because
+# the size is 200 and each corner radius is 100.
+# The blue rect should be fully invisible
+---
+root:
+ items:
+ -
+ type: "stacking-context"
+ bounds: [0, 0, 0, 0]
+ items:
+ -
+ type: clip
+ id: 2
+ complex:
+ - rect: [ 0, 0, 200, 200 ]
+ radius: 100
+ - type: rect
+ bounds: [ 0, 0, 200, 200 ]
+ color: red
+ clip-chain: [2]
diff --git a/gfx/wr/wrench/reftests/clip/clip-empty-inner-rect.yaml b/gfx/wr/wrench/reftests/clip/clip-empty-inner-rect.yaml
new file mode 100644
index 0000000000..a57c816223
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/clip-empty-inner-rect.yaml
@@ -0,0 +1,29 @@
+# In this case, the inner rectangle of the clip is empty, because
+# the size is 200 and each corner radius is 100.
+# The blue rect should be fully invisible
+---
+root:
+ items:
+ -
+ type: "stacking-context"
+ bounds: [0, 0, 0, 0]
+ items:
+ -
+ type: clip
+ id: 2
+ complex:
+ - rect: [ 0, 0, 200, 200 ]
+ radius: 100
+ - type: rect
+ bounds: [ 0, 0, 200, 200 ]
+ color: red
+ clip-chain: [2]
+ - type: "stacking-context"
+ bounds: [0, 0, 0, 0]
+ transform: translate(0, 0, 1)
+ items:
+ -
+ type: rect
+ bounds: [0, 0, 25, 25]
+ color: blue
+ clip-chain: [2]
diff --git a/gfx/wr/wrench/reftests/clip/clip-filter-raster-root-ref.yaml b/gfx/wr/wrench/reftests/clip/clip-filter-raster-root-ref.yaml
new file mode 100644
index 0000000000..7fde17f184
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/clip-filter-raster-root-ref.yaml
@@ -0,0 +1,16 @@
+---
+root:
+ items:
+ -
+ type: "reference-frame"
+ transform: [perspective(10), rotate(45)]
+ transform-origin: 500 100
+ items:
+ -
+ type: "stacking-context"
+ filters: [identity]
+ items:
+ -
+ bounds: [100, 100, 200, 200]
+ type: rect
+ color: red
diff --git a/gfx/wr/wrench/reftests/clip/clip-filter-raster-root.yaml b/gfx/wr/wrench/reftests/clip/clip-filter-raster-root.yaml
new file mode 100644
index 0000000000..688b832016
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/clip-filter-raster-root.yaml
@@ -0,0 +1,27 @@
+# Test that a local-space clip when applied to a surface (due to the filter) that is also
+# a raster root (due to the perspective) is correctly applied.
+---
+root:
+ items:
+ -
+ type: "reference-frame"
+ transform: [perspective(10), rotate(45)]
+ transform-origin: 500 100
+ items:
+ -
+ bounds: [100, 100, 200, 200]
+ type: clip
+ id: 2
+ -
+ type: "clip-chain"
+ id: 3
+ clips: [2]
+ -
+ clip-chain: 3
+ type: "stacking-context"
+ filters: [identity]
+ items:
+ -
+ bounds: [0, 0, 400, 400]
+ type: rect
+ color: red
diff --git a/gfx/wr/wrench/reftests/clip/clip-mode.png b/gfx/wr/wrench/reftests/clip/clip-mode.png
new file mode 100644
index 0000000000..e40a9db215
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/clip-mode.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/clip/clip-mode.yaml b/gfx/wr/wrench/reftests/clip/clip-mode.yaml
new file mode 100644
index 0000000000..d20e53668f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/clip-mode.yaml
@@ -0,0 +1,29 @@
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [20, 20, 100, 100]
+ radius: 32
+ - type: clip-chain
+ id: 12
+ clips: [2]
+ - type: rect
+ bounds: [20, 20, 100, 100]
+ color: red
+ clip-chain: 12
+
+ - type: clip
+ id: 3
+ complex:
+ - rect: [130, 20, 100, 100]
+ radius: 32
+ clip-mode: clip-out
+ - type: clip-chain
+ id: 13
+ clips: [3]
+ - type: rect
+ bounds: [130, 20, 100, 100]
+ color: green
+ clip-chain: 13
diff --git a/gfx/wr/wrench/reftests/clip/clip-rectangle-redundant-sc-ref.yaml b/gfx/wr/wrench/reftests/clip/clip-rectangle-redundant-sc-ref.yaml
new file mode 100644
index 0000000000..5946aeedae
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/clip-rectangle-redundant-sc-ref.yaml
@@ -0,0 +1,7 @@
+---
+root:
+ items:
+ -
+ bounds: [0, 0, 100, 100]
+ type: rect
+ color: blue
diff --git a/gfx/wr/wrench/reftests/clip/clip-rectangle-redundant-sc.yaml b/gfx/wr/wrench/reftests/clip/clip-rectangle-redundant-sc.yaml
new file mode 100644
index 0000000000..eb0493a81d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/clip-rectangle-redundant-sc.yaml
@@ -0,0 +1,20 @@
+# Test that rect clips on redundant stacking contexts are applied to child prims
+---
+root:
+ items:
+ -
+ bounds: [0, 0, 100, 100]
+ type: clip
+ id: 2
+ -
+ type: "clip-chain"
+ id: 3
+ clips: [2]
+ -
+ clip-chain: 3
+ type: "stacking-context"
+ items:
+ -
+ bounds: [0, 0, 200, 200]
+ type: rect
+ color: blue
diff --git a/gfx/wr/wrench/reftests/clip/clip-thin-rotated-ref.yaml b/gfx/wr/wrench/reftests/clip/clip-thin-rotated-ref.yaml
new file mode 100644
index 0000000000..e09079424d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/clip-thin-rotated-ref.yaml
@@ -0,0 +1,13 @@
+# Test checks if a rotated clip with a long and thin rectangle would still
+# correctly affect the primitive with regards to the inner bounds.
+---
+root:
+ items:
+ -
+ bounds: [0, 0, 0, 0]
+ type: "stacking-context"
+ items:
+ -
+ bounds: [100, 100, 14, 14]
+ type: rect
+ color: blue
diff --git a/gfx/wr/wrench/reftests/clip/clip-thin-rotated.yaml b/gfx/wr/wrench/reftests/clip/clip-thin-rotated.yaml
new file mode 100644
index 0000000000..d0e626af91
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/clip-thin-rotated.yaml
@@ -0,0 +1,41 @@
+# Test checks if a rotated clip with a long and thin rectangle would still
+# correctly affect the primitive with regards to the inner bounds.
+---
+root:
+ items:
+ -
+ bounds: [0, 0, 0, 0]
+ type: "reference-frame"
+ id: 2
+ -
+ bounds: [0, 0, 0, 0]
+ type: "stacking-context"
+ transform: rotate(45) translate(200, 0)
+ items:
+ -
+ bounds: [0, 0, 20, 1000]
+ type: clip
+ id: 5
+ # uncomment this to see the clip area
+ #-
+ # bounds: [0, 0, 20, 1000]
+ # type: rect
+ # color: green
+ - # we aren't supposed to see this one
+ bounds: [0, 0, 0, 0]
+ type: "stacking-context"
+ clip-and-scroll: [2, 5]
+ items:
+ -
+ bounds: [120, 120, 10, 10]
+ type: rect
+ color: red
+ -
+ bounds: [0, 0, 0, 0]
+ type: "stacking-context"
+ clip-and-scroll: [2, 5]
+ items:
+ -
+ bounds: [100, 100, 14, 14]
+ type: rect
+ color: blue
diff --git a/gfx/wr/wrench/reftests/clip/clipped-occlusion-ref.yaml b/gfx/wr/wrench/reftests/clip/clipped-occlusion-ref.yaml
new file mode 100644
index 0000000000..69a5fb0624
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/clipped-occlusion-ref.yaml
@@ -0,0 +1,7 @@
+---
+root:
+ items:
+ -
+ type: rect
+ bounds: [0, 0, 500, 500]
+ color: red
diff --git a/gfx/wr/wrench/reftests/clip/clipped-occlusion.yaml b/gfx/wr/wrench/reftests/clip/clipped-occlusion.yaml
new file mode 100644
index 0000000000..8f47d2837e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/clipped-occlusion.yaml
@@ -0,0 +1,25 @@
+# This is a regression test for https://bugzilla.mozilla.org/show_bug.cgi?id=1594567
+# The single clip node from the primitive inside the scroll frame will be promoted
+# to a 'shared clip' in the picture cache for the scroll frame. Ensure that this clip
+# (zero sized in this test) is included in the tile occlusion culling.
+---
+root:
+ items:
+ -
+ type: rect
+ bounds: [0, 0, 500, 500]
+ color: red
+ -
+ type: scroll-frame
+ content-size: [1000, 10000]
+ bounds: [0, -5000, 1000, 10000]
+ items:
+ - type: clip
+ id: 2
+ bounds: [0, 0, 0, 0]
+ -
+ bounds: [0, -5000, 1000, 10000]
+ type: rect
+ color: green
+ clip-chain: [2]
+ \ No newline at end of file
diff --git a/gfx/wr/wrench/reftests/clip/custom-clip-chain-node-ancestors-ref.yaml b/gfx/wr/wrench/reftests/clip/custom-clip-chain-node-ancestors-ref.yaml
new file mode 100644
index 0000000000..c84fe7a612
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/custom-clip-chain-node-ancestors-ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ -
+ bounds: [10, 10, 100, 100]
+ "clip-rect": [10, 10, 100, 100]
+ "backface-visible": true
+ type: rect
+ color: 0 255 0 1
diff --git a/gfx/wr/wrench/reftests/clip/custom-clip-chain-node-ancestors.yaml b/gfx/wr/wrench/reftests/clip/custom-clip-chain-node-ancestors.yaml
new file mode 100644
index 0000000000..e2cf3c9177
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/custom-clip-chain-node-ancestors.yaml
@@ -0,0 +1,31 @@
+# This test ensures that custom clip chains are not affected by the ancestors
+# of their clipping nodes. In this case the node, 3, will probably be optimized
+# away since its ancestor 2 has a tighter bounding rect. On the other hand, a
+# clip chain which includes 3 should only get the rectangle specified by that
+# node and not be affected by 2 at all.
+---
+root:
+ items:
+ -
+ bounds: [25, 25, 50, 50]
+ "clip-rect": [25, 25, 50, 50]
+ type: clip
+ id: 2
+ -
+ bounds: [10, 10, 100, 100]
+ clip-rect: [10, 10, 100, 100]
+ type: clip
+ id: 3
+ "content-size": [800, 1000]
+ -
+ bounds: [0, 0, 0, 0]
+ clip-rect: [0, 0, 0, 0]
+ type: "clip-chain"
+ id: 10
+ clips: [3]
+ -
+ bounds: [0, 0, 200, 200]
+ clip-rect: [0, 0, 200, 200]
+ clip-chain: 10
+ type: rect
+ color: 0 255 0 1
diff --git a/gfx/wr/wrench/reftests/clip/fixed-position-clipping-ref.yaml b/gfx/wr/wrench/reftests/clip/fixed-position-clipping-ref.yaml
new file mode 100644
index 0000000000..abbc91f897
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/fixed-position-clipping-ref.yaml
@@ -0,0 +1,15 @@
+---
+root:
+ items:
+ -
+ bounds: [10, 10, 100, 100]
+ clip-rect: [10, 10, 100, 100]
+ type: rect
+ color: 0 255 0 1.0
+ -
+ bounds: [110, 10, 100, 100]
+ clip-rect: [110, 10, 100, 100]
+ type: rect
+ color: 0 255 0 1.0
+ id: [0, 1]
+pipelines: []
diff --git a/gfx/wr/wrench/reftests/clip/fixed-position-clipping.yaml b/gfx/wr/wrench/reftests/clip/fixed-position-clipping.yaml
new file mode 100644
index 0000000000..e6e27ea857
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/fixed-position-clipping.yaml
@@ -0,0 +1,45 @@
+# This test ensures that children of fixed position stacking contexts are not
+# clipped by parent clipping nodes. The contents of the fixed position stacking
+# contexts below should not be clipped by their parent clipping nodes, but
+# instead should be promoted to be children of the top-level reference frame.
+---
+root:
+ items:
+ -
+ clip-rect: [15, 15, 30, 30]
+ type: scroll-frame
+ content-size: [60, 60]
+ bounds: [15, 15, 30, 30]
+ items:
+ -
+ bounds: [10, 10, 100, 100]
+ clip-rect: [10, 10, 100, 100]
+ type: stacking-context
+ items:
+ -
+ bounds: [0, 0, 100, 100]
+ clip-rect: [0, 0, 100, 100]
+ type: rect
+ color: 0 255 0 1.0
+ # The same test as above, except this time the stacking context also starts its
+ # own reference frame.
+ -
+ clip-rect: [115, 15, 30, 30]
+ type: scroll-frame
+ content-size: [60, 60]
+ bounds: [115, 15, 30, 30]
+ items:
+ -
+ bounds: [110, 10, 100, 100]
+ clip-rect: [110, 10, 100, 100]
+ id: 4
+ type: stacking-context
+ transform: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
+ items:
+ -
+ bounds: [0, 0, 100, 100]
+ clip-rect: [0, 0, 100, 100]
+ type: rect
+ color: 0 255 0 1.0
+ id: [0, 1]
+pipelines: []
diff --git a/gfx/wr/wrench/reftests/clip/iframe-nested-in-stacking-context-ref.yaml b/gfx/wr/wrench/reftests/clip/iframe-nested-in-stacking-context-ref.yaml
new file mode 100644
index 0000000000..c8f3148ea5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/iframe-nested-in-stacking-context-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [50, 50, 50, 100]
+ color: red
diff --git a/gfx/wr/wrench/reftests/clip/iframe-nested-in-stacking-context.yaml b/gfx/wr/wrench/reftests/clip/iframe-nested-in-stacking-context.yaml
new file mode 100644
index 0000000000..7c1dd4336b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/iframe-nested-in-stacking-context.yaml
@@ -0,0 +1,27 @@
+# Ensure that a stacking context with a simple clip that encloses
+# an iframe correctly propagates the stacking context clip to the
+# clip on the iframe.
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ bounds: [50, 50, 50, 100]
+ -
+ type: "clip-chain"
+ id: 3
+ clips: [2]
+ - type: stacking-context
+ bounds: [50, 50, 100, 100]
+ clip-chain: 3
+ items:
+ - type: iframe
+ id: [1, 3]
+ bounds: [0, 0, 100, 100]
+pipelines:
+ -
+ id: [1, 3]
+ items:
+ - type: rect
+ color: red
+ bounds: [0, 0, 100, 100]
diff --git a/gfx/wr/wrench/reftests/clip/raster-roots-tiled-mask-ref.yaml b/gfx/wr/wrench/reftests/clip/raster-roots-tiled-mask-ref.yaml
new file mode 100644
index 0000000000..6fdde50513
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/raster-roots-tiled-mask-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 8, 128]
+ color: [255, 0, 0, 0.5]
diff --git a/gfx/wr/wrench/reftests/clip/raster-roots-tiled-mask.yaml b/gfx/wr/wrench/reftests/clip/raster-roots-tiled-mask.yaml
new file mode 100644
index 0000000000..b7f513719b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/raster-roots-tiled-mask.yaml
@@ -0,0 +1,22 @@
+# Verify that tiled image clip masks are correctly positioned when the
+# underlying surface has established a raster root
+---
+root:
+ items:
+ - type: scroll-frame
+ bounds: [0, 0, 8, 1024]
+ content-size: [8, 2048]
+ scroll-offset: [0, 1024]
+ items:
+ - type: clip
+ id: 2
+ image-mask:
+ image: solid-color(128,0,0,0,8,1024)
+ rect: [0, 0, 8, 2048]
+ repeat: false
+ - type: stacking-context
+ clip-chain: [2]
+ items:
+ - type: rect
+ bounds: [0, 1024, 8, 128]
+ color: red
diff --git a/gfx/wr/wrench/reftests/clip/reftest.list b/gfx/wr/wrench/reftests/clip/reftest.list
new file mode 100644
index 0000000000..2284c12cda
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/reftest.list
@@ -0,0 +1,20 @@
+ platform(linux,mac) == border-with-rounded-clip.yaml border-with-rounded-clip.png
+fuzzy-if(platform(swgl),1,4) == clip-mode.yaml clip-mode.png
+fuzzy-if(platform(swgl),1,80) == clip-ellipse.yaml clip-ellipse.png
+platform(linux,mac) == clip-45-degree-rotation.yaml clip-45-degree-rotation-ref.png
+== clip-3d-transform.yaml clip-3d-transform-ref.yaml
+fuzzy(1,4) == clip-corner-overlap.yaml clip-corner-overlap-ref.yaml
+== custom-clip-chain-node-ancestors.yaml custom-clip-chain-node-ancestors-ref.yaml
+== fixed-position-clipping.yaml fixed-position-clipping-ref.yaml
+platform(linux,mac) == segmentation-with-other-coordinate-system-clip.yaml segmentation-with-other-coordinate-system-clip.png
+== segmentation-across-rotation.yaml segmentation-across-rotation-ref.yaml
+skip_on(android,device) fuzzy(1,2) == color_targets(3) alpha_targets(1) stacking-context-clip.yaml stacking-context-clip-ref.yaml
+== snapping.yaml snapping-ref.yaml
+fuzzy(160,1055) == clip-and-filter-with-rotation.yaml clip-and-filter-with-rotation-ref.yaml
+== clipped-occlusion.yaml clipped-occlusion-ref.yaml
+== clip-empty-inner-rect.yaml clip-empty-inner-rect-ref.yaml
+== iframe-nested-in-stacking-context.yaml iframe-nested-in-stacking-context-ref.yaml
+== clip-rectangle-redundant-sc.yaml clip-rectangle-redundant-sc-ref.yaml
+fuzzy(128,707) == clip-filter-raster-root.yaml clip-filter-raster-root-ref.yaml
+== blend-container.yaml blend-container-ref.yaml
+== raster-roots-tiled-mask.yaml raster-roots-tiled-mask-ref.yaml
diff --git a/gfx/wr/wrench/reftests/clip/segmentation-across-rotation-ref.yaml b/gfx/wr/wrench/reftests/clip/segmentation-across-rotation-ref.yaml
new file mode 100644
index 0000000000..6d14bab91c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/segmentation-across-rotation-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ color: [0, 255, 0, 1]
+ bounds: [100, 100, 100, 100]
diff --git a/gfx/wr/wrench/reftests/clip/segmentation-across-rotation.yaml b/gfx/wr/wrench/reftests/clip/segmentation-across-rotation.yaml
new file mode 100644
index 0000000000..6d9ea31708
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/segmentation-across-rotation.yaml
@@ -0,0 +1,31 @@
+# This test ensures that a clip that is segmented, with segments
+# that have no intersection with the world-space outer bounds of
+# the clip rectangle render correctly. In this case the first clip
+# defines the outer bounds of the clip rectangle and the rotation
+# ensures that the inner clip isn't optimized away completely. The
+# segments of the rounded corner clip don't have any intersection at
+# all with the clip in area from the outer clip, so they shouldn't
+# affect the rendering of the green square.
+---
+root:
+ items:
+ - type: clip
+ bounds: [100, 100, 100, 100]
+ id: 2
+ -
+ type: stacking-context
+ transform: rotate(0.25)
+ items:
+ - type: clip
+ id: 3
+ complex:
+ - rect: [ 0, 0, 2400, 900 ]
+ radius: 50
+ - type: clip-chain
+ id: 4
+ clips: [2, 3]
+ - type: rect
+ color: [0, 255, 0, 1]
+ bounds: [0, 0, 2400, 900]
+ clip-chain: 4
+
diff --git a/gfx/wr/wrench/reftests/clip/segmentation-with-other-coordinate-system-clip-ref.yaml b/gfx/wr/wrench/reftests/clip/segmentation-with-other-coordinate-system-clip-ref.yaml
new file mode 100644
index 0000000000..8627d2ed74
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/segmentation-with-other-coordinate-system-clip-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ -
+ type: rect
+ color: green
+ bounds: [0, 0, 100, 100]
+ "clip-rect": [0, 0, 100, 100]
diff --git a/gfx/wr/wrench/reftests/clip/segmentation-with-other-coordinate-system-clip.png b/gfx/wr/wrench/reftests/clip/segmentation-with-other-coordinate-system-clip.png
new file mode 100644
index 0000000000..67507a2573
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/segmentation-with-other-coordinate-system-clip.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/clip/segmentation-with-other-coordinate-system-clip.yaml b/gfx/wr/wrench/reftests/clip/segmentation-with-other-coordinate-system-clip.yaml
new file mode 100644
index 0000000000..9cf73988a8
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/segmentation-with-other-coordinate-system-clip.yaml
@@ -0,0 +1,48 @@
+# This is testing whether a clip properly clips a primitive in another
+# coordinate system that is segmented.
+# See https://github.com/servo/webrender/issues/2294
+---
+root:
+ items:
+ -
+ type: "stacking-context"
+ items:
+ -
+ id: 2
+ bounds: [0, 0, 100, 100]
+ "clip-rect": [0, 0, 100, 100]
+ type: clip
+ -
+ type: "stacking-context"
+ transform: [0.98883086, 0.14904226, 0, 0, -0.14904226, 0.98883086, 0, 0, 0, 0, 1, 0, 1533.3134, 109.21605, 0, 1]
+ items:
+ -
+ type: "stacking-context"
+ transform: [0.7818315, 0.6234898, 0, 0, -0.6234898, 0.7818315, 0, 0, 0, 0, 1, 0, 132.98201, -64.04077, 0, 1]
+ items:
+ -
+ type: "stacking-context"
+ transform: [0.93087375, 0.36534107, 0, 0, -0.36534107, 0.93087375, 0, 0, 0, 0, 1, 0, 68.64584, -46.80194, 0, 1]
+ items:
+ -
+ type: "stacking-context"
+ transform: [0.8262389, 0.56332004, 0, 0, -0.56332004, 0.8262389, 0, 0, 0, 0, 1, 0, 116.458824, -61.550323, 0, 1]
+ items:
+ -
+ type: "stacking-context"
+ transform: [0.90096885, 0.43388373, 0, 0, -0.43388373, 0.90096885, 0, 0, 0, 0, 1, 0, 84.200554, -52.906708, 0, 1]
+ items:
+ -
+ type: "stacking-context"
+ transform: [0.98883086, 0.14904226, 0, 0, -0.14904226, 0.98883086, 0, 0, 0, 0, 1, 0, 25.3134, -21.78395, 0, 1]
+ items:
+ -
+ type: "stacking-context"
+ transform: [0.73305184, 0.68017274, 0, 0, -0.68017274, 0.73305184, 0, 0, 0, 0, 1, 0, 149.64511, -65.28949, 0, 1]
+ items:
+ -
+ bounds: [1000, 0, 1000, 1000]
+ "clip-rect": [1000, 0, 1000, 1000]
+ type: rect
+ color: green
+ clip-chain: [2]
diff --git a/gfx/wr/wrench/reftests/clip/snapping-ref.yaml b/gfx/wr/wrench/reftests/clip/snapping-ref.yaml
new file mode 100644
index 0000000000..003f7b3e6f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/snapping-ref.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [50, 50, 100, 100]
+ radius: 16
+ - type: rect
+ bounds: 50 50 100 100
+ color: red
+ clip-chain: [2]
+
+ - type: rect
+ bounds: 200 50 100 100
+ color: green
+
diff --git a/gfx/wr/wrench/reftests/clip/snapping.yaml b/gfx/wr/wrench/reftests/clip/snapping.yaml
new file mode 100644
index 0000000000..0658885a2e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/snapping.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [50.3, 50.3, 100, 100]
+ radius: 16
+ - type: rect
+ bounds: 50.3 50.3 100 100
+ color: red
+ clip-chain: [2]
+
+ - type: rect
+ bounds: 200.3 50.3 100 100
+ color: green
+
diff --git a/gfx/wr/wrench/reftests/clip/stacking-context-clip-ref.yaml b/gfx/wr/wrench/reftests/clip/stacking-context-clip-ref.yaml
new file mode 100644
index 0000000000..412309ccda
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/stacking-context-clip-ref.yaml
@@ -0,0 +1,37 @@
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [0, 0, 100, 100]
+ radius: {
+ top-left: 50,
+ top-right: 50,
+ bottom-left: 50,
+ bottom-right: 50,
+ }
+ - type: clip-chain
+ id: 12
+ clips: [2]
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ items:
+ - type: rect
+ bounds: [ 0, 0, 100, 100 ]
+ color: [0, 255, 0]
+ clip-chain: 12
+ - type: clip
+ id: 3
+ bounds: [120, 0, 50, 50]
+ - type: clip-chain
+ id: 13
+ clips: [3]
+ - type: stacking-context
+ bounds: [100, 0, 100, 100]
+ filters: hue-rotate(90)
+ items:
+ - type: rect
+ bounds: [ 0, 0, 100, 100 ]
+ color: [0, 255, 0]
+ clip-chain: 13
diff --git a/gfx/wr/wrench/reftests/clip/stacking-context-clip.yaml b/gfx/wr/wrench/reftests/clip/stacking-context-clip.yaml
new file mode 100644
index 0000000000..e4a9618204
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/stacking-context-clip.yaml
@@ -0,0 +1,43 @@
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [0, 0, 100, 100]
+ radius: {
+ top-left: 50,
+ top-right: 50,
+ bottom-left: 50,
+ bottom-right: 50,
+ }
+ -
+ type: "clip-chain"
+ id: 4
+ clips: [2]
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ clip-chain: 4
+ items:
+ - type: rect
+ bounds: [ 0, 0, 100, 100 ]
+ color: [0, 255, 0]
+ # The same test, but this time with hue rotation, which means that the stacking
+ # context is rendered to an intermediate surface first. Unfortunately, we cannot
+ # use a rounded clip here because we want to avoid subpixel differences and avoid
+ # relying on a PNG reference image.
+ - type: clip
+ id: 3
+ bounds: [120, 0, 50, 50]
+ -
+ type: "clip-chain"
+ id: 5
+ clips: [3]
+ - type: stacking-context
+ bounds: [100, 0, 100, 100]
+ filters: hue-rotate(90)
+ clip-chain: 5
+ items:
+ - type: rect
+ bounds: [ 0, 0, 100, 100 ]
+ color: [0, 255, 0]
diff --git a/gfx/wr/wrench/reftests/compositor-surface/basic-ref.yaml b/gfx/wr/wrench/reftests/compositor-surface/basic-ref.yaml
new file mode 100644
index 0000000000..1a567dd9e6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/compositor-surface/basic-ref.yaml
@@ -0,0 +1,18 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [50, 50, 462, 462]
+ color: green
+ - type: rect
+ bounds: [125, 125, 312, 312]
+ color: [128, 128, 128, 0.5]
+ - image: transparent-checkerboard(2,16,16)
+ bounds: [150, 150, 262, 262]
+ - type: rect
+ bounds: [200, 200, 162, 162]
+ color: [0, 0, 255, 0.5]
+ - text: "Subpixel AA Text"
+ origin: 80 100
+ size: 20
+ color: black
diff --git a/gfx/wr/wrench/reftests/compositor-surface/basic.yaml b/gfx/wr/wrench/reftests/compositor-surface/basic.yaml
new file mode 100644
index 0000000000..0ea9b8a394
--- /dev/null
+++ b/gfx/wr/wrench/reftests/compositor-surface/basic.yaml
@@ -0,0 +1,22 @@
+# Test that basic functionality of non-opaque compositor surfaces
+# is blending correctly, and that subpixel AA is used outside the
+# compositor surface region.
+---
+root:
+ items:
+ - type: rect
+ bounds: [50, 50, 462, 462]
+ color: green
+ - type: rect
+ bounds: [125, 125, 312, 312]
+ color: [128, 128, 128, 0.5]
+ - image: transparent-checkerboard(2,16,16)
+ bounds: [150, 150, 262, 262]
+ prefer-compositor-surface: true
+ - type: rect
+ bounds: [200, 200, 162, 162]
+ color: [0, 0, 255, 0.5]
+ - text: "Subpixel AA Text"
+ origin: 80 100
+ size: 20
+ color: black
diff --git a/gfx/wr/wrench/reftests/compositor-surface/blank.yaml b/gfx/wr/wrench/reftests/compositor-surface/blank.yaml
new file mode 100644
index 0000000000..c4eb3ab673
--- /dev/null
+++ b/gfx/wr/wrench/reftests/compositor-surface/blank.yaml
@@ -0,0 +1,2 @@
+---
+root:
diff --git a/gfx/wr/wrench/reftests/compositor-surface/coord-systems.yaml b/gfx/wr/wrench/reftests/compositor-surface/coord-systems.yaml
new file mode 100644
index 0000000000..7cc2b77710
--- /dev/null
+++ b/gfx/wr/wrench/reftests/compositor-surface/coord-systems.yaml
@@ -0,0 +1,17 @@
+# Regression test - ensure we don't crash when we encounter a
+# transform hierarchy that has different coordinate systems but
+# results in a final transform that is 2d-axis-aligned
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [50, 50, 262, 262]
+ transform: rotate(45)
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 262, 262]
+ transform: rotate(-45)
+ items:
+ - image: checkerboard(2,16,16)
+ bounds: [0, 0, 262, 262]
+ prefer-compositor-surface: true
diff --git a/gfx/wr/wrench/reftests/compositor-surface/filter-overlay-ref.yaml b/gfx/wr/wrench/reftests/compositor-surface/filter-overlay-ref.yaml
new file mode 100644
index 0000000000..c5475c9814
--- /dev/null
+++ b/gfx/wr/wrench/reftests/compositor-surface/filter-overlay-ref.yaml
@@ -0,0 +1,11 @@
+---
+root:
+ items:
+ - image: checkerboard(2,16,16)
+ bounds: [50, 50, 262, 262]
+ - type: stacking-context
+ filters: [opacity(0.5)]
+ items:
+ - type: rect
+ bounds: [100, 100, 162, 162]
+ color: green
diff --git a/gfx/wr/wrench/reftests/compositor-surface/filter-overlay.yaml b/gfx/wr/wrench/reftests/compositor-surface/filter-overlay.yaml
new file mode 100644
index 0000000000..514225363a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/compositor-surface/filter-overlay.yaml
@@ -0,0 +1,15 @@
+# Verify that an off-screen surface that is placed over top
+# of a compositor surface selects the correct sub-slice when
+# adding to command buffers.
+---
+root:
+ items:
+ - image: checkerboard(2,16,16)
+ bounds: [50, 50, 262, 262]
+ prefer-compositor-surface: true
+ - type: stacking-context
+ filters: [opacity(0.5)]
+ items:
+ - type: rect
+ bounds: [100, 100, 162, 162]
+ color: green
diff --git a/gfx/wr/wrench/reftests/compositor-surface/mix-blend-ref.yaml b/gfx/wr/wrench/reftests/compositor-surface/mix-blend-ref.yaml
new file mode 100644
index 0000000000..c8ce2fb444
--- /dev/null
+++ b/gfx/wr/wrench/reftests/compositor-surface/mix-blend-ref.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: stacking-context
+ blend-container: true
+ items:
+ - type: rect
+ bounds: [50, 50, 362, 362]
+ color: green
+ - image: transparent-checkerboard(2,16,16)
+ bounds: [100, 100, 262, 262]
+ - type: stacking-context
+ mix-blend-mode: multiply
+ items:
+ - type: rect
+ bounds: [150, 150, 162, 162]
+ color: red
diff --git a/gfx/wr/wrench/reftests/compositor-surface/mix-blend.yaml b/gfx/wr/wrench/reftests/compositor-surface/mix-blend.yaml
new file mode 100644
index 0000000000..fb8afe312d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/compositor-surface/mix-blend.yaml
@@ -0,0 +1,20 @@
+# Verify that we don't promote compositor surfaces within a root level blend container,
+# as that can prevent correct blending with the compositor surface.
+---
+root:
+ items:
+ - type: stacking-context
+ blend-container: true
+ items:
+ - type: rect
+ bounds: [50, 50, 362, 362]
+ color: green
+ - image: transparent-checkerboard(2,16,16)
+ bounds: [100, 100, 262, 262]
+ prefer-compositor-surface: true
+ - type: stacking-context
+ mix-blend-mode: multiply
+ items:
+ - type: rect
+ bounds: [150, 150, 162, 162]
+ color: red
diff --git a/gfx/wr/wrench/reftests/compositor-surface/picture-passthrough.yaml b/gfx/wr/wrench/reftests/compositor-surface/picture-passthrough.yaml
new file mode 100644
index 0000000000..cfe73293d2
--- /dev/null
+++ b/gfx/wr/wrench/reftests/compositor-surface/picture-passthrough.yaml
@@ -0,0 +1,15 @@
+# Ensure that compositor surfaces which exist in child pictures with
+# None for composite mode don't get promoted to a compositor surface,
+# as we don't currently account for these in `compositor_surface_count`.
+---
+root:
+ items:
+ - type: stacking-context
+ backdrop-root: true
+ items:
+ - type: backdrop-filter
+ bounds: 0 0 256 256
+ filters: invert(1)
+ - image: checkerboard(2,16,16)
+ bounds: [150, 150, 262, 262]
+ prefer-compositor-surface: true
diff --git a/gfx/wr/wrench/reftests/compositor-surface/reftest.list b/gfx/wr/wrench/reftests/compositor-surface/reftest.list
new file mode 100644
index 0000000000..a026a26b18
--- /dev/null
+++ b/gfx/wr/wrench/reftests/compositor-surface/reftest.list
@@ -0,0 +1,6 @@
+skip_on(android) fuzzy(2,500) == basic.yaml basic-ref.yaml
+== too-many-surfaces.yaml too-many-surfaces-ref.yaml
+!= picture-passthrough.yaml blank.yaml
+fuzzy(2,1000) == mix-blend.yaml mix-blend-ref.yaml
+!= coord-systems.yaml blank.yaml
+fuzzy(2,2500) == filter-overlay.yaml filter-overlay-ref.yaml
diff --git a/gfx/wr/wrench/reftests/compositor-surface/too-many-surfaces-ref.yaml b/gfx/wr/wrench/reftests/compositor-surface/too-many-surfaces-ref.yaml
new file mode 100644
index 0000000000..00f9f7da6b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/compositor-surface/too-many-surfaces-ref.yaml
@@ -0,0 +1,31 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [50, 50, 400, 88]
+ color: green
+ - image: transparent-checkerboard(2,8,8)
+ bounds: [60, 60, 68, 68]
+ - type: rect
+ bounds: [55, 70, 390, 5]
+ color: [255, 0, 0, 0.9]
+ - image: transparent-checkerboard(2,8,8)
+ bounds: [138, 60, 68, 68]
+ - type: rect
+ bounds: [55, 80, 390, 5]
+ color: [255, 0, 0, 0.9]
+ - image: transparent-checkerboard(2,8,8)
+ bounds: [216, 60, 68, 68]
+ - type: rect
+ bounds: [55, 90, 390, 5]
+ color: [255, 0, 0, 0.9]
+ - image: transparent-checkerboard(2,8,8)
+ bounds: [294, 60, 68, 68]
+ - type: rect
+ bounds: [55, 100, 390, 5]
+ color: [255, 0, 0, 0.9]
+ - image: transparent-checkerboard(2,8,8)
+ bounds: [372, 60, 68, 68]
+ - type: rect
+ bounds: [55, 110, 390, 5]
+ color: [255, 0, 0, 0.9]
diff --git a/gfx/wr/wrench/reftests/compositor-surface/too-many-surfaces.yaml b/gfx/wr/wrench/reftests/compositor-surface/too-many-surfaces.yaml
new file mode 100644
index 0000000000..e5744ddedc
--- /dev/null
+++ b/gfx/wr/wrench/reftests/compositor-surface/too-many-surfaces.yaml
@@ -0,0 +1,39 @@
+# Test that when there are too many compositor surfaces present that
+# the additional compositor surfaces are correctly drawn as regular
+# primitives into picture cache content tiles.
+---
+root:
+ items:
+ - type: rect
+ bounds: [50, 50, 400, 88]
+ color: green
+ - image: transparent-checkerboard(2,8,8)
+ bounds: [60, 60, 68, 68]
+ prefer-compositor-surface: true
+ - type: rect
+ bounds: [55, 70, 390, 5]
+ color: [255, 0, 0, 0.9]
+ - image: transparent-checkerboard(2,8,8)
+ bounds: [138, 60, 68, 68]
+ prefer-compositor-surface: true
+ - type: rect
+ bounds: [55, 80, 390, 5]
+ color: [255, 0, 0, 0.9]
+ - image: transparent-checkerboard(2,8,8)
+ bounds: [216, 60, 68, 68]
+ prefer-compositor-surface: true
+ - type: rect
+ bounds: [55, 90, 390, 5]
+ color: [255, 0, 0, 0.9]
+ - image: transparent-checkerboard(2,8,8)
+ bounds: [294, 60, 68, 68]
+ prefer-compositor-surface: true
+ - type: rect
+ bounds: [55, 100, 390, 5]
+ color: [255, 0, 0, 0.9]
+ - image: transparent-checkerboard(2,8,8)
+ bounds: [372, 60, 68, 68]
+ prefer-compositor-surface: true
+ - type: rect
+ bounds: [55, 110, 390, 5]
+ color: [255, 0, 0, 0.9]
diff --git a/gfx/wr/wrench/reftests/crash/blank.yaml b/gfx/wr/wrench/reftests/crash/blank.yaml
new file mode 100644
index 0000000000..c4eb3ab673
--- /dev/null
+++ b/gfx/wr/wrench/reftests/crash/blank.yaml
@@ -0,0 +1,2 @@
+---
+root:
diff --git a/gfx/wr/wrench/reftests/crash/iframe-dup.yaml b/gfx/wr/wrench/reftests/crash/iframe-dup.yaml
new file mode 100644
index 0000000000..792caba3c8
--- /dev/null
+++ b/gfx/wr/wrench/reftests/crash/iframe-dup.yaml
@@ -0,0 +1,21 @@
+# Ensure that a duplicated iframe reference doesn't cause a panic in WR
+---
+root:
+ items:
+ -
+ type: iframe
+ id: [1, 2]
+ bounds: [100, 100, 100, 100]
+ -
+ type: iframe
+ id: [1, 2]
+ bounds: [100, 100, 100, 100]
+
+pipelines:
+ -
+ id: [1, 2]
+ items:
+ -
+ type: "rect"
+ color: [255, 0, 0, 1]
+ bounds: [100, 100, 100, 100]
diff --git a/gfx/wr/wrench/reftests/crash/many-segments.yaml b/gfx/wr/wrench/reftests/crash/many-segments.yaml
new file mode 100644
index 0000000000..f7301ac449
--- /dev/null
+++ b/gfx/wr/wrench/reftests/crash/many-segments.yaml
@@ -0,0 +1,112 @@
+# Ensure that in the case of a huge number of overlapping and intersecting rounded
+# clips we don't generate too many segments, causing the GPU cache to assert.
+---
+root:
+ items:
+ -
+ type: clip
+ complex:
+ - rect: [128, 128, 632, 632]
+ radius: 300
+ id: 2
+ -
+ type: clip
+ complex:
+ - rect: [126, 126, 628, 628]
+ radius: 300
+ id: 3
+ -
+ type: clip
+ complex:
+ - rect: [123, 123, 624, 624]
+ radius: 300
+ id: 4
+ -
+ type: clip
+ complex:
+ - rect: [120, 120, 626, 626]
+ radius: 300
+ id: 5
+ -
+ type: clip
+ complex:
+ - rect: [117, 117, 622, 622]
+ radius: 300
+ id: 6
+ -
+ type: clip
+ complex:
+ - rect: [114, 114, 619, 619]
+ radius: 300
+ id: 7
+ -
+ type: clip
+ complex:
+ - rect: [111, 111, 615, 615]
+ radius: 300
+ id: 8
+ -
+ type: clip
+ complex:
+ - rect: [108, 108, 612, 612]
+ radius: 300
+ id: 9
+ -
+ type: clip
+ complex:
+ - rect: [105, 105, 609, 609]
+ radius: 300
+ id: 10
+ -
+ type: clip
+ complex:
+ - rect: [102, 102, 606, 606]
+ radius: 300
+ id: 11
+ -
+ type: clip
+ complex:
+ - rect: [99, 99, 603, 603]
+ radius: 300
+ id: 12
+ -
+ type: clip
+ complex:
+ - rect: [96, 96, 600, 600]
+ radius: 300
+ id: 13
+ -
+ type: clip
+ complex:
+ - rect: [93, 93, 612, 612]
+ radius: 300
+ id: 14
+ -
+ type: clip
+ complex:
+ - rect: [90, 90, 609, 609]
+ radius: 300
+ id: 15
+ -
+ type: clip
+ complex:
+ - rect: [87, 87, 606, 606]
+ radius: 300
+ id: 16
+ -
+ type: clip
+ complex:
+ - rect: [84, 84, 603, 603]
+ radius: 300
+ id: 17
+
+ -
+ type: "clip-chain"
+ id: 1024
+ clips: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
+ -
+ bounds: [128, 128, 632, 632]
+ clip-rect: [128, 128, 632, 632]
+ clip-chain: 1024
+ type: rect
+ color: 0 255 0 1
diff --git a/gfx/wr/wrench/reftests/crash/reftest.list b/gfx/wr/wrench/reftests/crash/reftest.list
new file mode 100644
index 0000000000..63e815a80b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/crash/reftest.list
@@ -0,0 +1,2 @@
+!= iframe-dup.yaml blank.yaml
+!= many-segments.yaml blank.yaml
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-across-tiles.png b/gfx/wr/wrench/reftests/filters/backdrop-filter-across-tiles.png
new file mode 100644
index 0000000000..39371a08c7
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-across-tiles.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-across-tiles.yaml b/gfx/wr/wrench/reftests/filters/backdrop-filter-across-tiles.yaml
new file mode 100644
index 0000000000..9b8145a545
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-across-tiles.yaml
@@ -0,0 +1,12 @@
+# Verify that resolves across tile boundaries do correct readbacks
+---
+root:
+ items:
+ - type: stacking-context
+ backdrop-root: true
+ items:
+ - image: xy-gradient(640,640)
+ bounds: 0 0 640 640
+ - type: backdrop-filter
+ bounds: 20 20 600 600
+ filters: [invert(1)]
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-basic-ref.yaml b/gfx/wr/wrench/reftests/filters/backdrop-filter-basic-ref.yaml
new file mode 100644
index 0000000000..aec471df21
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-basic-ref.yaml
@@ -0,0 +1,11 @@
+
+# Tests that a basic invert backdrop-filter works
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: 0 0 0 0
+ items:
+ - type: rect
+ color: [0, 255, 255, 1]
+ bounds: 0 0 256 256
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-basic.yaml b/gfx/wr/wrench/reftests/filters/backdrop-filter-basic.yaml
new file mode 100644
index 0000000000..95521d2fab
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-basic.yaml
@@ -0,0 +1,21 @@
+# Tests that a basic invert backdrop-filter works
+---
+root:
+ items:
+ - type: stacking-context
+ backdrop-root: true
+ bounds: 0 0 0 0
+ items:
+ - type: rect
+ color: [255, 0, 0, 1]
+ bounds: 0 0 256 256
+ - type: clip
+ id: 2
+ bounds: 0 0 256 256
+ clip-rect: 0 0 256 256
+ - type: stacking-context
+ bounds: 0 0 0 0
+ items:
+ - type: backdrop-filter
+ bounds: 0 0 256 256
+ filters: invert(1)
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-blur-across-tiles.png b/gfx/wr/wrench/reftests/filters/backdrop-filter-blur-across-tiles.png
new file mode 100644
index 0000000000..9164c78598
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-blur-across-tiles.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-blur-across-tiles.yaml b/gfx/wr/wrench/reftests/filters/backdrop-filter-blur-across-tiles.yaml
new file mode 100644
index 0000000000..80be3ab493
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-blur-across-tiles.yaml
@@ -0,0 +1,19 @@
+# Verify that blurs across tile boundaries do correct readbacks
+---
+root:
+ items:
+ - type: stacking-context
+ backdrop-root: true
+ bounds: 20 20 0 0
+ items:
+ - image: checkerboard(2,32,32,16,22)
+ bounds: 0 0 516 644
+ - type: backdrop-filter
+ bounds: 50 50 416 544
+ filters: ["blur(5,5)"]
+ - type: border
+ bounds: [ 50, 50, 416, 544 ]
+ width: [ 1, 1, 1, 1 ]
+ border-type: normal
+ style: solid
+ color: black
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-blur-edge-mode-ref.yaml b/gfx/wr/wrench/reftests/filters/backdrop-filter-blur-edge-mode-ref.yaml
new file mode 100644
index 0000000000..99238dcc85
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-blur-edge-mode-ref.yaml
@@ -0,0 +1,10 @@
+# Verify that blurs on backdrop-filters use edgeMode=duplicate
+---
+root:
+ items:
+ - type: rect
+ bounds: 0 0 400 400
+ color: red
+ - type: rect
+ bounds: 100 100 200 200
+ color: blue
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-blur-edge-mode.yaml b/gfx/wr/wrench/reftests/filters/backdrop-filter-blur-edge-mode.yaml
new file mode 100644
index 0000000000..53681976c6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-blur-edge-mode.yaml
@@ -0,0 +1,17 @@
+# Verify that blurs on backdrop-filters use edgeMode=duplicate
+---
+root:
+ items:
+ - type: stacking-context
+ backdrop-root: true
+ bounds: 0 0 0 0
+ items:
+ - type: rect
+ bounds: 0 0 400 400
+ color: red
+ - type: rect
+ bounds: 100 100 200 200
+ color: blue
+ - type: backdrop-filter
+ bounds: 100 100 200 200
+ filters: ["blur(10,10)"]
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-chain.png b/gfx/wr/wrench/reftests/filters/backdrop-filter-chain.png
new file mode 100644
index 0000000000..1d5c146711
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-chain.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-chain.yaml b/gfx/wr/wrench/reftests/filters/backdrop-filter-chain.yaml
new file mode 100644
index 0000000000..16f081ae73
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-chain.yaml
@@ -0,0 +1,12 @@
+# Verify that backdrop filters with complex filter chains draw correctly
+---
+root:
+ items:
+ - type: stacking-context
+ backdrop-root: true
+ items:
+ - image: "firefox.png"
+ bounds: 0 0 256 256
+ - type: backdrop-filter
+ bounds: 50 50 100 100
+ filters: ["blur(5,5)", sepia(0.5), brightness(0.8)]
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-clip-mask-ref.yaml b/gfx/wr/wrench/reftests/filters/backdrop-filter-clip-mask-ref.yaml
new file mode 100644
index 0000000000..e9683fc373
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-clip-mask-ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: 0 0 256 256
+ color: red
+ - type: rect
+ bounds: 64 64 128 128
+ color: cyan \ No newline at end of file
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-clip-mask.yaml b/gfx/wr/wrench/reftests/filters/backdrop-filter-clip-mask.yaml
new file mode 100644
index 0000000000..ee3bba5ab3
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-clip-mask.yaml
@@ -0,0 +1,28 @@
+# Ensure that a backdrop-filter enclosed by a stacking context with
+# a clip-mask can select the correct backdrop root
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ image-mask:
+ image: solid-color(255,255,255,255,128,128)
+ rect: [64, 64, 128, 128]
+ repeat: false
+ -
+ type: "clip-chain"
+ id: 3
+ clips: [2]
+ - type: stacking-context
+ backdrop-root: true
+ items:
+ - type: rect
+ bounds: 0 0 256 256
+ color: red
+ - type: stacking-context
+ clip-chain: 3
+ wraps-backdrop-filter: true
+ items:
+ - type: backdrop-filter
+ bounds: 0 0 256 256
+ filters: invert(1) \ No newline at end of file
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-culled-filter-ref.yaml b/gfx/wr/wrench/reftests/filters/backdrop-filter-culled-filter-ref.yaml
new file mode 100644
index 0000000000..bbcf0f820d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-culled-filter-ref.yaml
@@ -0,0 +1,5 @@
+---
+root:
+ items:
+ - image: checkerboard(2,14,14,14,14)
+ bounds: 0 0 200 200
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-culled-filter.yaml b/gfx/wr/wrench/reftests/filters/backdrop-filter-culled-filter.yaml
new file mode 100644
index 0000000000..90edbcba16
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-culled-filter.yaml
@@ -0,0 +1,25 @@
+# Ensure that the backdrop capture primitive is culled in the
+# same way the backdrop render primitive is.
+---
+root:
+ items:
+ - type: clip
+ bounds: [0, 0, 0, 0]
+ clip-rect: [0, 0, 0, 0]
+ id: 2
+ -
+ type: "clip-chain"
+ id: 3
+ clips: [2]
+ - image: checkerboard(2,14,14,14,14)
+ bounds: 0 0 200 200
+ - type: stacking-context
+ transform: rotate(45)
+ transform-origin: 100 100
+ filters: identity
+ wraps-backdrop-filter: true
+ clip-chain: 3
+ items:
+ - type: backdrop-filter
+ bounds: 50 50 100 100
+ filters: invert(1)
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-drop-shadow.png b/gfx/wr/wrench/reftests/filters/backdrop-filter-drop-shadow.png
new file mode 100644
index 0000000000..dd72042a69
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-drop-shadow.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-drop-shadow.yaml b/gfx/wr/wrench/reftests/filters/backdrop-filter-drop-shadow.yaml
new file mode 100644
index 0000000000..e94b94f14a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-drop-shadow.yaml
@@ -0,0 +1,13 @@
+# Tests that a drop-shadow backdrop-filter works (involves very complex dependency graph due to picture + shadow content)
+---
+root:
+ items:
+ - type: stacking-context
+ backdrop-root: true
+ items:
+ - type: rect
+ color: red
+ bounds: 0 0 128 128
+ - type: backdrop-filter
+ bounds: 0 0 256 256
+ filters: drop-shadow([20, 20], 10, [255, 0, 0, 1])
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-half-opacity-ref.yaml b/gfx/wr/wrench/reftests/filters/backdrop-filter-half-opacity-ref.yaml
new file mode 100644
index 0000000000..e26ee0de4a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-half-opacity-ref.yaml
@@ -0,0 +1,5 @@
+root:
+ items:
+ - type: rect
+ bounds: [20, 20, 100, 100]
+ color: [128, 128, 128, 1]
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-half-opacity.yaml b/gfx/wr/wrench/reftests/filters/backdrop-filter-half-opacity.yaml
new file mode 100644
index 0000000000..33fac807a9
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-half-opacity.yaml
@@ -0,0 +1,13 @@
+root:
+ items:
+ - type: rect
+ bounds: [20, 20, 100, 100]
+ color: [0, 128, 0, 1]
+ - type: stacking-context
+ bounds: [20, 20, 0, 0]
+ filters: [opacity(0.5)]
+ wraps-backdrop-filter: true
+ items:
+ - type: backdrop-filter
+ bounds: [0, 0, 100, 100]
+ filters: [invert(1)]
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-isolation-ref.yaml b/gfx/wr/wrench/reftests/filters/backdrop-filter-isolation-ref.yaml
new file mode 100644
index 0000000000..7bdc84f019
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-isolation-ref.yaml
@@ -0,0 +1,20 @@
+# Ensure that stacking context isolation correctly works with backdrop-filter
+# (doesn't apply the filter outside the bounds of the isolated stacking context bounds)
+root:
+ items:
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: [0, 128, 0, 1]
+ - type: stacking-context
+ bounds: [130, 10, 0, 0]
+ filters: [identity]
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [0, 128, 0, 1]
+ - type: rect
+ bounds: [-90, 30, 160, 160]
+ color: [255, 255, 0, 0.5]
+ - type: rect
+ bounds: [0, 30, 70, 70]
+ color: [255, 191, 127, 1]
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-isolation.yaml b/gfx/wr/wrench/reftests/filters/backdrop-filter-isolation.yaml
new file mode 100644
index 0000000000..1ab896a234
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-isolation.yaml
@@ -0,0 +1,20 @@
+# Ensure that stacking context isolation correctly works with backdrop-filter
+# (doesn't apply the filter outside the bounds of the isolated stacking context bounds)
+root:
+ items:
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: [0, 128, 0, 1]
+ - type: stacking-context
+ bounds: [130, 10, 0, 0]
+ filters: [identity]
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [0, 128, 0, 1]
+ - type: backdrop-filter
+ bounds: [-90, 30, 160, 160]
+ filters: [invert(1)]
+ - type: rect
+ bounds: [-90, 30, 160, 160]
+ color: [255, 255, 0, 0.5]
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-nested.yaml b/gfx/wr/wrench/reftests/filters/backdrop-filter-nested.yaml
new file mode 100644
index 0000000000..1f9e5286c9
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-nested.yaml
@@ -0,0 +1,31 @@
+# Verify that when there are sibling backdrop-filters on the same backdrop
+# root, but they are separated by a filter / clip chain, the dependencies
+# are correctly set to ensure that the constrints for Existing render task
+# locations are met (that the existing task is scheduled prior to the
+# existing task, even if it's not needed for rendering order).
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ image-mask:
+ image: solid-color(255,255,255,255,100,100)
+ rect: [0, 0, 100, 100]
+ repeat: false
+ - type: clip-chain
+ id: 12
+ clips: [2]
+ - type: stacking-context
+ filters: [identity]
+ wraps-backdrop-filter: true
+ items:
+ - type: backdrop-filter
+ bounds: 0 0 100 100
+ filters: identity
+ - type: stacking-context
+ wraps-backdrop-filter: true
+ clip-chain: 12
+ items:
+ - type: backdrop-filter
+ bounds: 0 0 100 100
+ filters: identity
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-on-child-surface-ref.yaml b/gfx/wr/wrench/reftests/filters/backdrop-filter-on-child-surface-ref.yaml
new file mode 100644
index 0000000000..214bb87ad0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-on-child-surface-ref.yaml
@@ -0,0 +1,7 @@
+# Tests that a basic invert backdrop-filter works on a child surface
+---
+root:
+ items:
+ - type: rect
+ bounds: 0 0 256 256
+ color: cyan
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-on-child-surface.yaml b/gfx/wr/wrench/reftests/filters/backdrop-filter-on-child-surface.yaml
new file mode 100644
index 0000000000..3792556ade
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-on-child-surface.yaml
@@ -0,0 +1,14 @@
+# Tests that a basic invert backdrop-filter works on a child surface
+---
+root:
+ items:
+ - type: stacking-context
+ backdrop-root: true
+ filters: [identity]
+ items:
+ - type: rect
+ bounds: 0 0 256 256
+ color: red
+ - type: backdrop-filter
+ bounds: 0 0 256 256
+ filters: invert(1)
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-overlap.png b/gfx/wr/wrench/reftests/filters/backdrop-filter-overlap.png
new file mode 100644
index 0000000000..cebb57f862
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-overlap.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-overlap.yaml b/gfx/wr/wrench/reftests/filters/backdrop-filter-overlap.yaml
new file mode 100644
index 0000000000..68c5c0aba1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-overlap.yaml
@@ -0,0 +1,15 @@
+# Verify that sibling backdrop filters correctly read back intermediate results
+---
+root:
+ items:
+ - type: stacking-context
+ backdrop-root: true
+ items:
+ - image: checkerboard(0,16,16)
+ bounds: 0 0 256 256
+ - type: backdrop-filter
+ bounds: 32 32 192 192
+ filters: [invert(1)]
+ - type: backdrop-filter
+ bounds: 64 64 128 128
+ filters: [invert(1)]
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-perspective.png b/gfx/wr/wrench/reftests/filters/backdrop-filter-perspective.png
new file mode 100644
index 0000000000..d9d49726bb
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-perspective.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-perspective.yaml b/gfx/wr/wrench/reftests/filters/backdrop-filter-perspective.yaml
new file mode 100644
index 0000000000..f5b9a1da64
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-perspective.yaml
@@ -0,0 +1,32 @@
+# Tests that backdrop filter works with a perspective transform
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: 0 0 0 0
+ backdrop-root: true
+ perspective: 500
+ items:
+ - type: rect
+ color: [255, 255, 255, 1]
+ bounds: 0 0 1024 1024
+ - image: "firefox.png"
+ bounds: 0 0 256 256
+ - type: stacking-context
+ bounds: 50 50 0 0
+ transform: ["rotate-y(-50)", "rotate-z(-45)"]
+ items:
+ - type: clip
+ id: 2
+ bounds: 0 0 100 100
+ clip-rect: 0 0 100 100
+ - type: clip-chain
+ id: 12
+ clips: [2]
+ - type: stacking-context
+ bounds: 0 0 0 0
+ items:
+ - type: backdrop-filter
+ bounds: 0 0 100 100
+ clip-chain: 12
+ filters: invert(1)
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-transformed-filter.png b/gfx/wr/wrench/reftests/filters/backdrop-filter-transformed-filter.png
new file mode 100644
index 0000000000..743bd03425
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-transformed-filter.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/backdrop-filter-transformed-filter.yaml b/gfx/wr/wrench/reftests/filters/backdrop-filter-transformed-filter.yaml
new file mode 100644
index 0000000000..dcde064917
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/backdrop-filter-transformed-filter.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: rect
+ color: red
+ bounds: 0 0 200 200
+ - image: checkerboard(2,14,14,14,14)
+ bounds: 0 0 200 200
+ - type: stacking-context
+ transform: rotate(45)
+ transform-origin: 100 100
+ filters: identity
+ wraps-backdrop-filter: true
+ items:
+ - type: backdrop-filter
+ bounds: 50 50 100 100
+ filters: invert(1)
diff --git a/gfx/wr/wrench/reftests/filters/blank.yaml b/gfx/wr/wrench/reftests/filters/blank.yaml
new file mode 100644
index 0000000000..c4eb3ab673
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/blank.yaml
@@ -0,0 +1,2 @@
+---
+root:
diff --git a/gfx/wr/wrench/reftests/filters/blend-clipped-raster-root.png b/gfx/wr/wrench/reftests/filters/blend-clipped-raster-root.png
new file mode 100644
index 0000000000..ce3f7e229f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/blend-clipped-raster-root.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/blend-clipped-raster-root.yaml b/gfx/wr/wrench/reftests/filters/blend-clipped-raster-root.yaml
new file mode 100644
index 0000000000..96e55f6a32
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/blend-clipped-raster-root.yaml
@@ -0,0 +1,26 @@
+---
+root:
+ items:
+ -
+ bounds: [0, 100, 1000, 1000]
+ type: clip
+ id: 3
+ -
+ type: "clip-chain"
+ id: 4
+ clips: [3]
+ -
+ bounds: [200, 200, 0, 0]
+ clip-chain: 4
+ type: "stacking-context"
+ transform: rotate(10)
+ items:
+ -
+ type: "stacking-context"
+ filters:
+ - opacity(0.5)
+ items:
+ -
+ bounds: [0, 0, 1000, 500]
+ type: rect
+ color: green
diff --git a/gfx/wr/wrench/reftests/filters/blend-clipped.png b/gfx/wr/wrench/reftests/filters/blend-clipped.png
new file mode 100644
index 0000000000..e500a538e8
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/blend-clipped.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/blend-clipped.yaml b/gfx/wr/wrench/reftests/filters/blend-clipped.yaml
new file mode 100644
index 0000000000..8162a96c09
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/blend-clipped.yaml
@@ -0,0 +1,103 @@
+---
+root:
+ items:
+ -
+ type: "stacking-context"
+ items:
+ -
+ bounds: [0, 0, 1887, 2081]
+ "clip-rect": [0, 0, 1887, 2081]
+ "backface-visible": true
+ type: clip
+ id: 2
+ "content-size": [1887, 2081]
+ -
+ bounds: [0, 111, 1887, 1970]
+ "clip-rect": [0, 111, 1887, 1970]
+ "backface-visible": true
+ type: clip
+ id: 3
+ "content-size": [1887, 1970]
+ -
+ bounds: [0, 111, 1887, 1971]
+ "clip-rect": [0, 111, 1887, 1971]
+ "backface-visible": true
+ type: iframe
+ id: [1, 3]
+ id: [1, 1]
+pipelines:
+ -
+ id: [1, 3]
+ items:
+ -
+ type: "stacking-context"
+ items:
+ -
+ bounds: [0, 0, 1887, 1971]
+ "clip-rect": [0, 0, 1887, 1971]
+ type: clip
+ id: 10
+ "content-size": [1887, 1971]
+ -
+ "clip-rect": [0, 0, 1887, 1971]
+ type: "scroll-frame"
+ id: 2
+ "content-size": [1887, 1971]
+ bounds: [0, 0, 1887, 1971]
+ -
+ bounds: [0, 0, 1887, 1971]
+ "clip-rect": [0, 0, 1887, 1971]
+ type: clip
+ id: 3
+ "content-size": [1887, 1971]
+ -
+ type: "stacking-context"
+ items:
+ -
+ bounds: [0, -186, 1887, 239]
+ "clip-rect": [0, -186, 1887, 239]
+ "backface-visible": true
+ type: clip
+ id: 4
+ "content-size": [1887, 239]
+ -
+ bounds: [-660.45, -186, 0, 0]
+ "clip-rect": [-660.45, -186, 0, 0]
+ spatial-id: 2
+ type: "stacking-context"
+ transform: [1, 0, 0, 0, -0.57735026, 1, 0, 0, 0, 0, 1, 0, 68.849, 0, 0, 1]
+ items:
+ -
+ type: "stacking-context"
+ filters:
+ - opacity(0.8)
+ items:
+ -
+ bounds: [0, 0, 1887, 239]
+ "clip-rect": [0, 0, 1887, 239]
+ type: clip
+ id: 5
+ "content-size": [1887, 239]
+ - type: clip-chain
+ id: 15
+ clips: [3, 4, 5]
+ -
+ bounds: [0, 0, 1887, 239]
+ "clip-rect": [0, 0, 1887, 239]
+ clip-chain: 15
+ "backface-visible": true
+ type: gradient
+ start: [943.5, -0.00000000000005684342]
+ end: [943.5, 238.5]
+ "tile-size": [1887, 238.5]
+ "tile-spacing": [0, 0]
+ stops:
+ - 0
+ - 0 153 204.00002 1.0000
+ - 0.3
+ - 0 153 204.00002 1.0000
+ - 0.6
+ - 7.0000005 137 194.00002 1.0000
+ - 1
+ - 0 91 137 1.0000
+ repeat: false
diff --git a/gfx/wr/wrench/reftests/filters/drop-shadow-inverse-scale.yaml b/gfx/wr/wrench/reftests/filters/drop-shadow-inverse-scale.yaml
new file mode 100644
index 0000000000..229bdaa4b6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/drop-shadow-inverse-scale.yaml
@@ -0,0 +1,14 @@
+# Ensure that zero sized drop shadows handle reflection scale transforms without crashing
+---
+root:
+ items:
+ -
+ type: "stacking-context"
+ bounds: [100, 100, 100, 100]
+ filters: drop-shadow([0, 1], 1, red)
+ transform: scale(-1, 1)
+ items:
+ -
+ type: rect
+ color: green
+ bounds: [0, 0, 1, 0]
diff --git a/gfx/wr/wrench/reftests/filters/filter-blur-clamping-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-blur-clamping-ref.yaml
new file mode 100644
index 0000000000..a06489467a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-blur-clamping-ref.yaml
@@ -0,0 +1,18 @@
+# Ensures that blur clamping happens after scale factors are applied
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [100, 100, 300, 300]
+ filters: blur(100, 100)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: 0 255 0 1.0
+ - type: stacking-context
+ bounds: [400, 100, 300, 300]
+ filters: blur(50, 50)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: 255 0 0 1.0
diff --git a/gfx/wr/wrench/reftests/filters/filter-blur-clamping.yaml b/gfx/wr/wrench/reftests/filters/filter-blur-clamping.yaml
new file mode 100644
index 0000000000..0d81a616ee
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-blur-clamping.yaml
@@ -0,0 +1,30 @@
+# Ensures that blur clamping happens after scale factors are applied
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [100, 100, 300, 300]
+ transform: scale(10)
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 300, 300]
+ # Blur will be 20 * 10(scale) = 200 and it should then be clamped to 100
+ filters: blur(20, 20)
+ items:
+ - type: rect
+ bounds: [0, 0, 10, 10]
+ color: 0 255 0 1.0
+ - type: stacking-context
+ bounds: [400, 100, 300, 300]
+ transform: scale(0.1)
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 300, 300]
+ # Blur should be 500 * 0.1(scale) = 50. This tests to make sure clamping
+ # does not occur before applying scale factors, otherwise 500 would be
+ # clamped to 100.
+ filters: blur(500, 500)
+ items:
+ - type: rect
+ bounds: [0, 0, 1000, 1000]
+ color: 255 0 0 1.0
diff --git a/gfx/wr/wrench/reftests/filters/filter-blur-downscale-fractional.png b/gfx/wr/wrench/reftests/filters/filter-blur-downscale-fractional.png
new file mode 100644
index 0000000000..36da2d2620
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-blur-downscale-fractional.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/filter-blur-downscale-fractional.yaml b/gfx/wr/wrench/reftests/filters/filter-blur-downscale-fractional.yaml
new file mode 100644
index 0000000000..56f843d5ec
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-blur-downscale-fractional.yaml
@@ -0,0 +1,11 @@
+# verify that we correctly size an image to a power of two when
+# using downscale passes to avoid blur artifacts
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [100, 100, 300, 300]
+ filters: blur(16.4, 16.4)
+ items:
+ - image: "firefox.png"
+ bounds: 20 20 200 200
diff --git a/gfx/wr/wrench/reftests/filters/filter-blur-huge.yaml b/gfx/wr/wrench/reftests/filters/filter-blur-huge.yaml
new file mode 100644
index 0000000000..13a4c72fd8
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-blur-huge.yaml
@@ -0,0 +1,10 @@
+# Don't crash on very large blur radius!
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [10, 10, 260, 260]
+ filters: blur(1000000, 1000000)
+ items:
+ - image: checkerboard(2, 16, 16)
+ bounds: [10, 10, 260, 260]
diff --git a/gfx/wr/wrench/reftests/filters/filter-blur-scaled-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-blur-scaled-ref.yaml
new file mode 100644
index 0000000000..6a5f314df0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-blur-scaled-ref.yaml
@@ -0,0 +1,11 @@
+# Ensure scales from enclosing SCs get applied to blurs
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ filters: blur(10, 10)
+ items:
+ - type: rect
+ bounds: [50, 50, 250, 250]
+ color: 0 255 0 1.0
diff --git a/gfx/wr/wrench/reftests/filters/filter-blur-scaled-xonly.png b/gfx/wr/wrench/reftests/filters/filter-blur-scaled-xonly.png
new file mode 100644
index 0000000000..48f8913752
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-blur-scaled-xonly.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/filter-blur-scaled-xonly.yaml b/gfx/wr/wrench/reftests/filters/filter-blur-scaled-xonly.yaml
new file mode 100644
index 0000000000..18a897131d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-blur-scaled-xonly.yaml
@@ -0,0 +1,16 @@
+# Ensure scales from enclosing SCs get applied to blurs. This one
+# applies a mixed-dimension scale by scaling the x-axis only.
+---
+root:
+ items:
+ - type: reference-frame
+ bounds: [0, 0, 100, 100]
+ transform: [5, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ filters: blur(2, 2)
+ items:
+ - type: rect
+ bounds: [10, 10, 50, 50]
+ color: 0 255 0 1.0
diff --git a/gfx/wr/wrench/reftests/filters/filter-blur-scaled.yaml b/gfx/wr/wrench/reftests/filters/filter-blur-scaled.yaml
new file mode 100644
index 0000000000..0e8c9cbff9
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-blur-scaled.yaml
@@ -0,0 +1,15 @@
+# Ensure scales from enclosing SCs get applied to blurs
+---
+root:
+ items:
+ - type: reference-frame
+ bounds: [0, 0, 100, 100]
+ transform: [5, 0, 0, 0, 0, 5, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ filters: blur(2, 2)
+ items:
+ - type: rect
+ bounds: [10, 10, 50, 50]
+ color: 0 255 0 1.0
diff --git a/gfx/wr/wrench/reftests/filters/filter-blur.png b/gfx/wr/wrench/reftests/filters/filter-blur.png
new file mode 100644
index 0000000000..695db74925
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-blur.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/filter-blur.yaml b/gfx/wr/wrench/reftests/filters/filter-blur.yaml
new file mode 100644
index 0000000000..ee915d7bb1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-blur.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [100, 100, 300, 300]
+ filters: blur(10, 10)
+ items:
+ - image: "firefox.png"
+ bounds: 20 20 256 256
diff --git a/gfx/wr/wrench/reftests/filters/filter-brightness-2-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-brightness-2-ref.yaml
new file mode 100644
index 0000000000..209dfb3cc3
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-brightness-2-ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: [0, 0, 0, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-brightness-2.yaml b/gfx/wr/wrench/reftests/filters/filter-brightness-2.yaml
new file mode 100644
index 0000000000..2bdb20aa1a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-brightness-2.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: stacking-context
+ bounds: [10, 10, 100, 100]
+ filters: brightness(0)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [255, 0, 0, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-brightness-3-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-brightness-3-ref.yaml
new file mode 100644
index 0000000000..3201170182
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-brightness-3-ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: [0, 128, 0, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-brightness-3.yaml b/gfx/wr/wrench/reftests/filters/filter-brightness-3.yaml
new file mode 100644
index 0000000000..8ea1eef5a8
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-brightness-3.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: stacking-context
+ bounds: [10, 10, 100, 100]
+ filters: brightness(4)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [0, 32, 0, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-brightness-4-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-brightness-4-ref.yaml
new file mode 100644
index 0000000000..b300d2a026
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-brightness-4-ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: [0, 64, 0, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-brightness-4.yaml b/gfx/wr/wrench/reftests/filters/filter-brightness-4.yaml
new file mode 100644
index 0000000000..9a15b15895
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-brightness-4.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: stacking-context
+ bounds: [10, 10, 100, 100]
+ filters: brightness(0.25)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [0, 255, 0, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-brightness-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-brightness-ref.yaml
new file mode 100644
index 0000000000..98e1bf419a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-brightness-ref.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: [64, 64, 64, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-brightness.yaml b/gfx/wr/wrench/reftests/filters/filter-brightness.yaml
new file mode 100644
index 0000000000..65da0e0b25
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-brightness.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+
+ - type: stacking-context
+ bounds: [10, 10, 100, 100]
+ filters: brightness(2)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [255, 255, 255, 0.25]
diff --git a/gfx/wr/wrench/reftests/filters/filter-color-matrix-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-color-matrix-ref.yaml
new file mode 100644
index 0000000000..2d32e5307b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-color-matrix-ref.yaml
@@ -0,0 +1,21 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 170]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 170]
+ color: [0, 0, 0, 1]
+ - type: rect
+ bounds: [10, 10, 50, 50]
+ color: [100, 175, 136, 1]
+ - type: rect
+ bounds: [10, 60, 50, 50]
+ color: [255, 0, 255, 1]
+ - type: rect
+ bounds: [60, 10, 50, 50]
+ color: [255, 0, 0, 1]
+ - type: rect
+ bounds: [10, 110, 50, 50]
+ color: [0, 0, 128, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-color-matrix.yaml b/gfx/wr/wrench/reftests/filters/filter-color-matrix.yaml
new file mode 100644
index 0000000000..4deb19b779
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-color-matrix.yaml
@@ -0,0 +1,53 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 170]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 170]
+ color: [0, 0, 0, 1]
+ - type: stacking-context
+ bounds: [10, 10, 50, 50]
+ filters: color-matrix( 0.393, 0.686, 0.534, 0,
+ 0.189, 0.168, 0.131, 0,
+ 0.349, 0.272, 0, 0,
+ 0, 0, 0, 1,
+ 0, 0, 0, 0 )
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [255, 0, 0, 1]
+ - type: stacking-context
+ bounds: [10, 60, 50, 50]
+ filters: color-matrix( -1, 0, 0, 0,
+ 0, -1, 0, 0,
+ 0, 0, -1, 0,
+ 0, 0, 0, 1,
+ 1, 1, 1, 0 )
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [0, 255, 0, 1]
+ - type: stacking-context
+ bounds: [60, 10, 50, 50]
+ filters: color-matrix( 0, 0, 1, 0,
+ 0, 1, 0, 0,
+ 1, 0, 0, 0,
+ 0, 0, 0, 1,
+ 0, 0, 0, 0 )
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [0, 0, 255, 1]
+ - type: stacking-context
+ bounds: [10, 110, 50, 50]
+ filters: color-matrix( 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0.5,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0 )
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [0, 0, 255, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-component-transfer-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-component-transfer-ref.yaml
new file mode 100644
index 0000000000..e19e1ff0b0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-component-transfer-ref.yaml
@@ -0,0 +1,51 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 170, 250]
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [255, 0, 255, 1]
+ - type: rect
+ bounds: [0, 50, 50, 50]
+ color: [0, 255, 141, 1]
+ - type: rect
+ bounds: [0, 100, 50, 50]
+ color: [255, 255, 0, 1]
+ - type: rect
+ bounds: [0, 150, 50, 50]
+ color: [191, 128, 128, 1]
+ - type: rect
+ bounds: [0, 200, 50, 50]
+ color: [0, 255, 24, 1]
+ - type: rect
+ bounds: [60, 0, 50, 50]
+ color: [255, 0, 255, 1]
+ - type: rect
+ bounds: [60, 50, 50, 50]
+ color: [255, 255, 255, 1]
+ - type: rect
+ bounds: [60, 100, 50, 50]
+ color: [255, 255, 255, 1]
+ - type: rect
+ bounds: [60, 150, 50, 50]
+ color: [223, 191, 191, 1]
+ - type: rect
+ bounds: [60, 200, 50, 50]
+ color: [191, 255, 197, 1]
+ - type: rect
+ bounds: [120, 0, 50, 50]
+ color: [255, 127, 255, 1]
+ - type: rect
+ bounds: [120, 50, 50, 50]
+ color: [128, 255, 200, 1]
+ - type: rect
+ bounds: [120, 100, 50, 50]
+ color: [255, 255, 255, 1]
+ - type: rect
+ bounds: [120, 150, 50, 50]
+ color: [239, 223, 223, 1]
+ - type: rect
+ bounds: [120, 200, 50, 50]
+ color: [63, 255, 81, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-component-transfer.yaml b/gfx/wr/wrench/reftests/filters/filter-component-transfer.yaml
new file mode 100644
index 0000000000..a573b5587e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-component-transfer.yaml
@@ -0,0 +1,352 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 170, 250]
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 50, 50]
+ filters:
+ - component-transfer
+ filter-datas:
+ - - - Identity
+ - Identity
+ - Identity
+ - Identity
+ - []
+ - []
+ - []
+ - []
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [255, 0, 255, 1]
+ - type: stacking-context
+ bounds: [0, 50, 50, 50]
+ filters:
+ - component-transfer
+ filter-datas:
+ - - - Table
+ - Table
+ - Table
+ - Identity
+ - - "1"
+ - "1"
+ - "0"
+ - "0"
+ - - "0"
+ - "0"
+ - "1"
+ - "1"
+ - - "0"
+ - "1"
+ - "1"
+ - "0"
+ - []
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [173, 255, 47, 1]
+ - type: stacking-context
+ bounds: [0, 100, 50, 50]
+ filters:
+ - component-transfer
+ filter-datas:
+ - - - Discrete
+ - Discrete
+ - Discrete
+ - Identity
+ - - "1"
+ - "1"
+ - "0"
+ - "0"
+ - - "0"
+ - "0"
+ - "1"
+ - "1"
+ - - "0"
+ - "1"
+ - "1"
+ - "0"
+ - []
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [0, 255, 255, 1]
+ - type: stacking-context
+ bounds: [0, 150, 50, 50]
+ filters:
+ - component-transfer
+ filter-datas:
+ - - - Linear
+ - Linear
+ - Linear
+ - Identity
+ - - "0.5"
+ - "0.25"
+ - - "0.5"
+ - "0"
+ - - "0.5"
+ - "0.5"
+ - []
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [255, 255, 0, 1]
+ - type: stacking-context
+ bounds: [0, 200, 50, 50]
+ filters:
+ - component-transfer
+ filter-datas:
+ - - - Gamma
+ - Gamma
+ - Gamma
+ - Identity
+ - - "2"
+ - "5"
+ - "-1"
+ - - "2"
+ - "3"
+ - "0"
+ - - "2"
+ - "1"
+ - "-1.75"
+ - []
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [135, 206, 235, 1]
+ - type: stacking-context
+ bounds: [60, 0, 50, 50]
+ filters:
+ - component-transfer
+ filter-datas:
+ - - - Identity
+ - Identity
+ - Identity
+ - Identity
+ - []
+ - []
+ - []
+ - []
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [255, 0, 255, 1]
+ - type: stacking-context
+ bounds: [60, 50, 50, 50]
+ filters:
+ - component-transfer
+ filter-datas:
+ - - - Table
+ - Table
+ - Table
+ - Table
+ - - "1"
+ - "1"
+ - "0"
+ - "0"
+ - - "0"
+ - "0"
+ - "1"
+ - "1"
+ - - "0"
+ - "1"
+ - "1"
+ - "0"
+ - - "1"
+ - "0"
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [173, 255, 47, 1]
+ - type: stacking-context
+ bounds: [60, 100, 50, 50]
+ filters:
+ - component-transfer
+ filter-datas:
+ - - - Discrete
+ - Discrete
+ - Discrete
+ - Discrete
+ - - "1"
+ - "1"
+ - "0"
+ - "0"
+ - - "0"
+ - "0"
+ - "1"
+ - "1"
+ - - "0"
+ - "1"
+ - "1"
+ - "0"
+ - - "1"
+ - "0"
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [0, 255, 255, 1]
+ - type: stacking-context
+ bounds: [60, 150, 50, 50]
+ filters:
+ - component-transfer
+ filter-datas:
+ - - - Linear
+ - Linear
+ - Linear
+ - Linear
+ - - "0.5"
+ - "0.25"
+ - - "0.5"
+ - "0"
+ - - "0.5"
+ - "0.5"
+ - - "0.5"
+ - "0"
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [255, 255, 0, 1]
+ - type: stacking-context
+ bounds: [60, 200, 50, 50]
+ filters:
+ - component-transfer
+ filter-datas:
+ - - - Gamma
+ - Gamma
+ - Gamma
+ - Gamma
+ - - "2"
+ - "5"
+ - "-1"
+ - - "2"
+ - "3"
+ - "0"
+ - - "2"
+ - "1"
+ - "-1.75"
+ - - "2"
+ - "1"
+ - "-1.75"
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [135, 206, 235, 1]
+ - type: stacking-context
+ bounds: [120, 0, 50, 50]
+ filters:
+ - component-transfer
+ filter-datas:
+ - - - Identity
+ - Identity
+ - Identity
+ - Identity
+ - []
+ - []
+ - []
+ - []
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [255, 0, 255, 0.5]
+ - type: stacking-context
+ bounds: [120, 50, 50, 50]
+ filters:
+ - component-transfer
+ filter-datas:
+ - - - Table
+ - Table
+ - Table
+ - Table
+ - - "1"
+ - "1"
+ - "0"
+ - "0"
+ - - "0"
+ - "0"
+ - "1"
+ - "1"
+ - - "0"
+ - "1"
+ - "1"
+ - "0"
+ - - "1"
+ - "0"
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [173, 255, 47, 0.5]
+ - type: stacking-context
+ bounds: [120, 100, 50, 50]
+ filters:
+ - component-transfer
+ filter-datas:
+ - - - Discrete
+ - Discrete
+ - Discrete
+ - Discrete
+ - - "1"
+ - "1"
+ - "0"
+ - "0"
+ - - "0"
+ - "0"
+ - "1"
+ - "1"
+ - - "0"
+ - "1"
+ - "1"
+ - "0"
+ - - "1"
+ - "0"
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [0, 255, 255, 0.5]
+ - type: stacking-context
+ bounds: [120, 150, 50, 50]
+ filters:
+ - component-transfer
+ filter-datas:
+ - - - Linear
+ - Linear
+ - Linear
+ - Linear
+ - - "0.5"
+ - "0.25"
+ - - "0.5"
+ - "0"
+ - - "0.5"
+ - "0.5"
+ - - "0.5"
+ - "0"
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [255, 255, 0, 0.5]
+ - type: stacking-context
+ bounds: [120, 200, 50, 50]
+ filters:
+ - component-transfer
+ filter-datas:
+ - - - Gamma
+ - Gamma
+ - Gamma
+ - Gamma
+ - - "2"
+ - "5"
+ - "-1"
+ - - "2"
+ - "3"
+ - "0"
+ - - "2"
+ - "1"
+ - "-1.75"
+ - - "2"
+ - "1"
+ - "-0.25"
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [135, 206, 235, 0.5]
diff --git a/gfx/wr/wrench/reftests/filters/filter-contrast-gray-alpha-1-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-contrast-gray-alpha-1-ref.yaml
new file mode 100644
index 0000000000..6b176d9570
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-contrast-gray-alpha-1-ref.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [255, 255, 255, 1]
+
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: [223, 223, 223, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-contrast-gray-alpha-1.yaml b/gfx/wr/wrench/reftests/filters/filter-contrast-gray-alpha-1.yaml
new file mode 100644
index 0000000000..87758d3f4a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-contrast-gray-alpha-1.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: stacking-context
+ bounds: [10, 10, 100, 100]
+ filters: contrast(0)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [128, 128, 128, 0.25]
diff --git a/gfx/wr/wrench/reftests/filters/filter-drop-shadow-blur-clamping-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-blur-clamping-ref.yaml
new file mode 100644
index 0000000000..6ffcde5f5b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-blur-clamping-ref.yaml
@@ -0,0 +1,18 @@
+# Ensures that blur clamping happens after scale factors are applied
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [100, 100, 300, 300]
+ filters: drop-shadow([0, 0], 100, blue)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: 0 255 0 1.0
+ - type: stacking-context
+ bounds: [400, 100, 300, 300]
+ filters: drop-shadow([0, 0], 50, green)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: 255 0 0 1.0
diff --git a/gfx/wr/wrench/reftests/filters/filter-drop-shadow-blur-clamping.yaml b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-blur-clamping.yaml
new file mode 100644
index 0000000000..1d7157a8e6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-blur-clamping.yaml
@@ -0,0 +1,30 @@
+# Ensures that blur clamping happens after scale factors are applied
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [100, 100, 300, 300]
+ transform: scale(10)
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 300, 300]
+ # Blur will be 20 * 10(scale) = 200 and it should then be clamped to 100
+ filters: drop-shadow([0, 0], 20, blue)
+ items:
+ - type: rect
+ bounds: [0, 0, 10, 10]
+ color: 0 255 0 1.0
+ - type: stacking-context
+ bounds: [400, 100, 300, 300]
+ transform: scale(0.1)
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 300, 300]
+ # Blur should be 500 * 0.1(scale) = 50. This tests to make sure clamping
+ # does not occur before applying scale factors, otherwise 500 would be
+ # clamped to 100.
+ filters: drop-shadow([0, 0], 500, green)
+ items:
+ - type: rect
+ bounds: [0, 0, 1000, 1000]
+ color: 255 0 0 1.0
diff --git a/gfx/wr/wrench/reftests/filters/filter-drop-shadow-clip-2.png b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-clip-2.png
new file mode 100644
index 0000000000..7b3d1d87ce
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-clip-2.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/filter-drop-shadow-clip-2.yaml b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-clip-2.yaml
new file mode 100644
index 0000000000..4616d5e290
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-clip-2.yaml
@@ -0,0 +1,21 @@
+# Bug 1561447: If a clip task is created for a picture, it should take into account the rect of the drop shadow
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ bounds: 10 0 300 300
+ clip-rect: 10 0 300 300
+ -
+ type: "clip-chain"
+ id: 3
+ clips: [2]
+ - type: stacking-context
+ bounds: 30 30 0 0
+ transform: rotate-z(-45)
+ filters: drop-shadow([15, 0], 0, red)
+ clip-chain: 3
+ items:
+ - type: rect
+ bounds: 0 0 100 100
+ color: blue
diff --git a/gfx/wr/wrench/reftests/filters/filter-drop-shadow-clip-3.png b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-clip-3.png
new file mode 100644
index 0000000000..40eded52ac
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-clip-3.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/filter-drop-shadow-clip-3.yaml b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-clip-3.yaml
new file mode 100644
index 0000000000..4b9ad3d36e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-clip-3.yaml
@@ -0,0 +1,37 @@
+---
+# WebRender internally clips the stacking context to avoid
+# rendering parts that are off-screen. It has to take shadow
+# offsets into account.
+# This test introduces a 90 degrees rotation to ensure that
+# the clip inflation isn't done in the wrong space.
+root:
+ items:
+ # Rotation inside of the shadowed stacking context.
+ - type: stacking-context
+ bounds: [0, -10, 200, 100]
+ filters: drop-shadow([-10, 200], 5, red)
+ items:
+ - type: stacking-context
+ bounds: [50, -10, 200, 100]
+ transform: rotate-z(-90)
+ items:
+ -
+ bounds: [0, 0, 500, 150]
+ glyphs: [55, 75, 76, 86]
+ offsets: [0, 100, 75, 100, 150, 100, 180, 100]
+ size: 90
+ color: blue
+ font: "../text/VeraBd.ttf"
+ # Rotation applied to the shadowed stacking context directly.
+ - type: stacking-context
+ bounds: [150, 35, 200, 100]
+ filters: drop-shadow([200, 10], 5, red)
+ transform: rotate-z(-90)
+ items:
+ -
+ bounds: [0, 0, 500, 150]
+ glyphs: [55, 75, 76, 86]
+ offsets: [0, 100, 75, 100, 150, 100, 180, 100]
+ size: 90
+ color: blue
+ font: "../text/VeraBd.ttf"
diff --git a/gfx/wr/wrench/reftests/filters/filter-drop-shadow-clip.png b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-clip.png
new file mode 100644
index 0000000000..6b0624c8c1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-clip.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/filter-drop-shadow-clip.yaml b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-clip.yaml
new file mode 100644
index 0000000000..33f066328d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-clip.yaml
@@ -0,0 +1,27 @@
+---
+root:
+ items:
+ - type: clip
+ id: 3
+ complex:
+ - rect: [0, 0, 256, 256]
+ radius: 16
+ - type: clip-chain
+ id: 5
+ clips: [3]
+ - type: clip
+ id: 4
+ bounds: [100, 100, 2000, 2000]
+ -
+ type: "clip-chain"
+ id: 6
+ clips: [4]
+ - type: stacking-context
+ bounds: [10, 10, 0, 0]
+ filters: drop-shadow([50, 50], 25, red)
+ clip-chain: 6
+ items:
+ - type: rect
+ bounds: 0 0 256 256
+ color: green
+ clip-chain: 5
diff --git a/gfx/wr/wrench/reftests/filters/filter-drop-shadow-fractional.png b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-fractional.png
new file mode 100644
index 0000000000..f75e2c5c9e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-fractional.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/filter-drop-shadow-fractional.yaml b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-fractional.yaml
new file mode 100644
index 0000000000..e61ae8ec45
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-fractional.yaml
@@ -0,0 +1,11 @@
+# Verify that drop shadows with fractional blur radii are correctly centered
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [20, 20, 0, 0]
+ filters: drop-shadow([0, 0], 5.01, [255, 0, 0, 1])
+ items:
+ - type: rect
+ bounds: 0 0 100 100
+ color: black
diff --git a/gfx/wr/wrench/reftests/filters/filter-drop-shadow-huge.yaml b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-huge.yaml
new file mode 100644
index 0000000000..dc971a7d3a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-huge.yaml
@@ -0,0 +1,10 @@
+# Don't crash on very large blur radius!
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [10, 10, 260, 260]
+ filters: drop-shadow([73, 73], 10000000, [255, 0, 0, 1])
+ items:
+ - image: checkerboard(2, 16, 16)
+ bounds: [10, 10, 260, 260]
diff --git a/gfx/wr/wrench/reftests/filters/filter-drop-shadow-on-viewport-edge.png b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-on-viewport-edge.png
new file mode 100644
index 0000000000..a63372edfb
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-on-viewport-edge.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/filter-drop-shadow-on-viewport-edge.yaml b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-on-viewport-edge.yaml
new file mode 100644
index 0000000000..663c03a0de
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-on-viewport-edge.yaml
@@ -0,0 +1,10 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [10, 10, 400, 400]
+ filters: drop-shadow([10, 10], 20, [255, 0, 0, 1])
+ items:
+ - type: rect
+ bounds: 0 0 256 256
+ color: green
diff --git a/gfx/wr/wrench/reftests/filters/filter-drop-shadow-scaled-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-scaled-ref.yaml
new file mode 100644
index 0000000000..9173528be4
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-scaled-ref.yaml
@@ -0,0 +1,11 @@
+# Ensure scales from enclosing SCs get applied to drop-shadows
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ filters: drop-shadow([0, 0], 10, [255, 0, 0, 1])
+ items:
+ - type: rect
+ bounds: [50, 50, 250, 250]
+ color: 0 255 0 1.0
diff --git a/gfx/wr/wrench/reftests/filters/filter-drop-shadow-scaled.yaml b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-scaled.yaml
new file mode 100644
index 0000000000..87fac1ee6d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-scaled.yaml
@@ -0,0 +1,15 @@
+# Ensure scales from enclosing SCs get applied to drop-shadows
+---
+root:
+ items:
+ - type: reference-frame
+ bounds: [0, 0, 100, 100]
+ transform: [5, 0, 0, 0, 0, 5, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 400, 400]
+ filters: drop-shadow([0, 0], 2, [255, 0, 0, 1])
+ items:
+ - type: rect
+ bounds: [10, 10, 50, 50]
+ color: 0 255 0 1.0
diff --git a/gfx/wr/wrench/reftests/filters/filter-drop-shadow-transform-huge.yaml b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-transform-huge.yaml
new file mode 100644
index 0000000000..e46af9dfa4
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-drop-shadow-transform-huge.yaml
@@ -0,0 +1,17 @@
+# Don't crash on large blur radius with large transform!
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ transform: scale-y(100)
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 1000]
+ filters: drop-shadow([0, 0], 999999, [255, 0, 0, 1])
+ items:
+ - image: checkerboard(2, 16, 16)
+ bounds: [0, 0, 1000, 1000]
diff --git a/gfx/wr/wrench/reftests/filters/filter-drop-shadow.png b/gfx/wr/wrench/reftests/filters/filter-drop-shadow.png
new file mode 100644
index 0000000000..cf9355e63c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-drop-shadow.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/filter-drop-shadow.yaml b/gfx/wr/wrench/reftests/filters/filter-drop-shadow.yaml
new file mode 100644
index 0000000000..59f90b42da
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-drop-shadow.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [100, 100, 400, 400]
+ filters: drop-shadow([73, 73], 20, [255, 0, 0, 1])
+ items:
+ - image: "firefox.png"
+ bounds: 0 0 256 256
diff --git a/gfx/wr/wrench/reftests/filters/filter-grayscale-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-grayscale-ref.yaml
new file mode 100644
index 0000000000..6a0baae7d9
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-grayscale-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [10, 10, 200, 200]
+ color: [182, 182, 182]
diff --git a/gfx/wr/wrench/reftests/filters/filter-grayscale.yaml b/gfx/wr/wrench/reftests/filters/filter-grayscale.yaml
new file mode 100644
index 0000000000..efa299cc07
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-grayscale.yaml
@@ -0,0 +1,10 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [10, 10, 200, 200]
+ filters: grayscale(1)
+ items:
+ - type: rect
+ bounds: [0, 0, 200, 200]
+ color: [0, 255, 0]
diff --git a/gfx/wr/wrench/reftests/filters/filter-hue-rotate-1-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-hue-rotate-1-ref.yaml
new file mode 100644
index 0000000000..a1c4039010
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-hue-rotate-1-ref.yaml
@@ -0,0 +1,21 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+ - type: rect
+ bounds: [10, 10, 50, 50]
+ color: [0, 91, 0, 1]
+ - type: rect
+ bounds: [10, 60, 50, 50]
+ color: [0, 218, 255, 1]
+ - type: rect
+ bounds: [60, 10, 50, 50]
+ color: [255, 0, 37, 1]
+ - type: rect
+ bounds: [60, 60, 50, 50]
+ color: [128, 128, 128, 1] \ No newline at end of file
diff --git a/gfx/wr/wrench/reftests/filters/filter-hue-rotate-1.yaml b/gfx/wr/wrench/reftests/filters/filter-hue-rotate-1.yaml
new file mode 100644
index 0000000000..9934661bbd
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-hue-rotate-1.yaml
@@ -0,0 +1,37 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+ - type: stacking-context
+ bounds: [10, 10, 50, 50]
+ filters: hue-rotate(90)
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [255, 0, 0, 1]
+ - type: stacking-context
+ bounds: [10, 60, 50, 50]
+ filters: hue-rotate(90)
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [0, 255, 0, 1]
+ - type: stacking-context
+ bounds: [60, 10, 50, 50]
+ filters: hue-rotate(90)
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [0, 0, 255, 1]
+ - type: stacking-context
+ bounds: [60, 60, 50, 50]
+ filters: hue-rotate(90)
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [128, 128, 128, 1] \ No newline at end of file
diff --git a/gfx/wr/wrench/reftests/filters/filter-hue-rotate-alpha-1-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-hue-rotate-alpha-1-ref.yaml
new file mode 100644
index 0000000000..436f30937a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-hue-rotate-alpha-1-ref.yaml
@@ -0,0 +1,21 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+ - type: rect
+ bounds: [10, 10, 50, 50]
+ color: [0, 46, 0, 1]
+ - type: rect
+ bounds: [10, 60, 50, 50]
+ color: [0, 109, 128, 1]
+ - type: rect
+ bounds: [60, 10, 50, 50]
+ color: [128, 0, 18, 1]
+ - type: rect
+ bounds: [60, 60, 50, 50]
+ color: [64, 64, 64, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-hue-rotate-alpha-1.yaml b/gfx/wr/wrench/reftests/filters/filter-hue-rotate-alpha-1.yaml
new file mode 100644
index 0000000000..b38cb218ff
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-hue-rotate-alpha-1.yaml
@@ -0,0 +1,37 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+ - type: stacking-context
+ bounds: [10, 10, 50, 50]
+ filters: hue-rotate(90)
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [255, 0, 0, 0.5]
+ - type: stacking-context
+ bounds: [10, 60, 50, 50]
+ filters: hue-rotate(90)
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [0, 255, 0, 0.5]
+ - type: stacking-context
+ bounds: [60, 10, 50, 50]
+ filters: hue-rotate(90)
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [0, 0, 255, 0.5]
+ - type: stacking-context
+ bounds: [60, 60, 50, 50]
+ filters: hue-rotate(90)
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [128, 128, 128, 0.5] \ No newline at end of file
diff --git a/gfx/wr/wrench/reftests/filters/filter-invert-2-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-invert-2-ref.yaml
new file mode 100644
index 0000000000..08aa94333d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-invert-2-ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 255, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-invert-2.yaml b/gfx/wr/wrench/reftests/filters/filter-invert-2.yaml
new file mode 100644
index 0000000000..f254816dc5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-invert-2.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 255, 1]
+
+ - type: stacking-context
+ bounds: [10, 10, 100, 100]
+ filters: invert(1)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [255, 255, 0, 0.5]
diff --git a/gfx/wr/wrench/reftests/filters/filter-invert-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-invert-ref.yaml
new file mode 100644
index 0000000000..674ecee969
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-invert-ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: [0, 0, 0, 0.25]
diff --git a/gfx/wr/wrench/reftests/filters/filter-invert.yaml b/gfx/wr/wrench/reftests/filters/filter-invert.yaml
new file mode 100644
index 0000000000..57ebcdb43c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-invert.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: stacking-context
+ bounds: [10, 10, 100, 100]
+ filters: invert(1)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [255, 255, 255, 0.25]
diff --git a/gfx/wr/wrench/reftests/filters/filter-large-blur-radius.png b/gfx/wr/wrench/reftests/filters/filter-large-blur-radius.png
new file mode 100644
index 0000000000..e48a567c38
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-large-blur-radius.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/filter-large-blur-radius.yaml b/gfx/wr/wrench/reftests/filters/filter-large-blur-radius.yaml
new file mode 100644
index 0000000000..6ce5f4cb22
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-large-blur-radius.yaml
@@ -0,0 +1,10 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: 100 100 512 512
+ filters: blur(100, 100)
+ items:
+ - type: rect
+ bounds: 0 0 512 512
+ color: red
diff --git a/gfx/wr/wrench/reftests/filters/filter-long-chain.png b/gfx/wr/wrench/reftests/filters/filter-long-chain.png
new file mode 100644
index 0000000000..6e58dd6300
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-long-chain.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/filter-long-chain.yaml b/gfx/wr/wrench/reftests/filters/filter-long-chain.yaml
new file mode 100644
index 0000000000..a0f9d698bd
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-long-chain.yaml
@@ -0,0 +1,19 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [100, 100, 400, 400]
+ filters: [brightness(2),
+ contrast(0.4),
+ grayscale(0.2),
+ hue-rotate(270),
+ invert(0.6),
+ opacity(0.8),
+ saturate(10),
+ sepia(0.4),
+ "blur(3,3)",
+ "drop-shadow([73, 73], 3, [0, 255, 0, 1])"]
+ items:
+ - type: rect
+ color: red
+ bounds: 0 0 256 256
diff --git a/gfx/wr/wrench/reftests/filters/filter-mix-blend-mode-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-mix-blend-mode-ref.yaml
new file mode 100644
index 0000000000..0a43095403
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-mix-blend-mode-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ color: [128, 128, 127, 1]
+ bounds: 100 100 100 100
diff --git a/gfx/wr/wrench/reftests/filters/filter-mix-blend-mode.yaml b/gfx/wr/wrench/reftests/filters/filter-mix-blend-mode.yaml
new file mode 100644
index 0000000000..0b6e3eb468
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-mix-blend-mode.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: stacking-context
+ blend-container: true
+ items:
+ - type: rect
+ color: [128, 128, 128, 1]
+ bounds: 100 100 100 100
+ - type: stacking-context
+ bounds: [100, 100, 100, 100]
+ filters: [invert(1)]
+ mix-blend-mode: exclusion
+ items:
+ - type: rect
+ color: yellow
+ bounds: 0 0 100 100
diff --git a/gfx/wr/wrench/reftests/filters/filter-mix-blend-scaling-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-mix-blend-scaling-ref.yaml
new file mode 100644
index 0000000000..949da2b16b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-mix-blend-scaling-ref.yaml
@@ -0,0 +1,11 @@
+
+# Tests that mix-blend mode content renders correctly when in a scaled surface.
+# See bug 1642549
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: 0 0 0 0
+ items:
+ - rect: 0 0 150 150
+ color: green
diff --git a/gfx/wr/wrench/reftests/filters/filter-mix-blend-scaling.yaml b/gfx/wr/wrench/reftests/filters/filter-mix-blend-scaling.yaml
new file mode 100644
index 0000000000..577f4edebf
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-mix-blend-scaling.yaml
@@ -0,0 +1,23 @@
+# Tests that mix-blend mode content renders correctly when in a scaled surface.
+# See bug 1642549
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: 0 0 0 0
+ # Force WebRender to form a raster root
+ transform: perspective(1000)
+ items:
+ - type: stacking-context
+ bounds: -50 -50 0 0
+ # The raster root will have a 2x scaling factor
+ transform: scale(2)
+ blend-container: true
+ items:
+ - rect: 0 0 100 100
+ color: green
+ - type: stacking-context
+ mix-blend-mode: multiply
+ items:
+ - rect: 0 0 100 100
+ color: white
diff --git a/gfx/wr/wrench/reftests/filters/filter-saturate-blue-1-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-saturate-blue-1-ref.yaml
new file mode 100644
index 0000000000..9b5a96e542
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-saturate-blue-1-ref.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: [18, 18, 18, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-saturate-blue-1.yaml b/gfx/wr/wrench/reftests/filters/filter-saturate-blue-1.yaml
new file mode 100644
index 0000000000..22d47f10e0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-saturate-blue-1.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+
+ - type: stacking-context
+ bounds: [10, 10, 100, 100]
+ filters: saturate(0)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [0, 0, 255, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-saturate-blue-2-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-saturate-blue-2-ref.yaml
new file mode 100644
index 0000000000..a83166358d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-saturate-blue-2-ref.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: [9, 9, 137, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-saturate-blue-2.yaml b/gfx/wr/wrench/reftests/filters/filter-saturate-blue-2.yaml
new file mode 100644
index 0000000000..0b8cac5609
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-saturate-blue-2.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+
+ - type: stacking-context
+ bounds: [10, 10, 100, 100]
+ filters: saturate(0.5)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [0, 0, 255, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-saturate-blue-3-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-saturate-blue-3-ref.yaml
new file mode 100644
index 0000000000..ebb3450e02
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-saturate-blue-3-ref.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: [0, 0, 255, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-saturate-blue-3.yaml b/gfx/wr/wrench/reftests/filters/filter-saturate-blue-3.yaml
new file mode 100644
index 0000000000..060d0294ff
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-saturate-blue-3.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+
+ - type: stacking-context
+ bounds: [10, 10, 100, 100]
+ filters: saturate(1)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [0, 0, 255, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-saturate-blue-alpha-1-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-saturate-blue-alpha-1-ref.yaml
new file mode 100644
index 0000000000..321bea6876
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-saturate-blue-alpha-1-ref.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: [5, 5, 69, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-saturate-blue-alpha-1.yaml b/gfx/wr/wrench/reftests/filters/filter-saturate-blue-alpha-1.yaml
new file mode 100644
index 0000000000..1a264cc458
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-saturate-blue-alpha-1.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+
+ - type: stacking-context
+ bounds: [10, 10, 100, 100]
+ filters: saturate(0.5)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [0, 0, 255, 0.5]
diff --git a/gfx/wr/wrench/reftests/filters/filter-saturate-green-1-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-saturate-green-1-ref.yaml
new file mode 100644
index 0000000000..2a710fae15
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-saturate-green-1-ref.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: [182, 182, 182, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-saturate-green-1.yaml b/gfx/wr/wrench/reftests/filters/filter-saturate-green-1.yaml
new file mode 100644
index 0000000000..4fcfa8174b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-saturate-green-1.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+
+ - type: stacking-context
+ bounds: [10, 10, 100, 100]
+ filters: saturate(0)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [0, 255, 0, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-saturate-green-2-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-saturate-green-2-ref.yaml
new file mode 100644
index 0000000000..f13c6e225d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-saturate-green-2-ref.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: [91, 219, 91, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-saturate-green-2.yaml b/gfx/wr/wrench/reftests/filters/filter-saturate-green-2.yaml
new file mode 100644
index 0000000000..98f033bb76
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-saturate-green-2.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+
+ - type: stacking-context
+ bounds: [10, 10, 100, 100]
+ filters: saturate(0.5)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [0, 255, 0, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-saturate-green-3-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-saturate-green-3-ref.yaml
new file mode 100644
index 0000000000..00aae63bdc
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-saturate-green-3-ref.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: [0, 255, 0, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-saturate-green-3.yaml b/gfx/wr/wrench/reftests/filters/filter-saturate-green-3.yaml
new file mode 100644
index 0000000000..c7f3561171
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-saturate-green-3.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+
+ - type: stacking-context
+ bounds: [10, 10, 100, 100]
+ filters: saturate(1)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [0, 255, 0, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-saturate-green-alpha-1-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-saturate-green-alpha-1-ref.yaml
new file mode 100644
index 0000000000..e4c5643f29
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-saturate-green-alpha-1-ref.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: [46, 110, 46, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-saturate-green-alpha-1.yaml b/gfx/wr/wrench/reftests/filters/filter-saturate-green-alpha-1.yaml
new file mode 100644
index 0000000000..042d19d6f5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-saturate-green-alpha-1.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+
+ - type: stacking-context
+ bounds: [10, 10, 100, 100]
+ filters: saturate(0.5)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [0, 255, 0, 0.5]
diff --git a/gfx/wr/wrench/reftests/filters/filter-saturate-red-1-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-saturate-red-1-ref.yaml
new file mode 100644
index 0000000000..3b95adb53a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-saturate-red-1-ref.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: [54, 54, 54, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-saturate-red-1.yaml b/gfx/wr/wrench/reftests/filters/filter-saturate-red-1.yaml
new file mode 100644
index 0000000000..9dab61a552
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-saturate-red-1.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+
+ - type: stacking-context
+ bounds: [10, 10, 100, 100]
+ filters: saturate(0)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [255, 0, 0, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-saturate-red-2-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-saturate-red-2-ref.yaml
new file mode 100644
index 0000000000..f6707424d0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-saturate-red-2-ref.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: [155, 27, 27, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-saturate-red-2.yaml b/gfx/wr/wrench/reftests/filters/filter-saturate-red-2.yaml
new file mode 100644
index 0000000000..5810894a2c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-saturate-red-2.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+
+ - type: stacking-context
+ bounds: [10, 10, 100, 100]
+ filters: saturate(0.5)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [255, 0, 0, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-saturate-red-3-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-saturate-red-3-ref.yaml
new file mode 100644
index 0000000000..0c9e4f4ac2
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-saturate-red-3-ref.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: [255, 0, 0, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-saturate-red-3.yaml b/gfx/wr/wrench/reftests/filters/filter-saturate-red-3.yaml
new file mode 100644
index 0000000000..ce9ed012d3
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-saturate-red-3.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+
+ - type: stacking-context
+ bounds: [10, 10, 100, 100]
+ filters: saturate(1)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [255, 0, 0, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-saturate-red-alpha-1-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-saturate-red-alpha-1-ref.yaml
new file mode 100644
index 0000000000..bdcd52d339
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-saturate-red-alpha-1-ref.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: [78, 14, 14, 1]
diff --git a/gfx/wr/wrench/reftests/filters/filter-saturate-red-alpha-1.yaml b/gfx/wr/wrench/reftests/filters/filter-saturate-red-alpha-1.yaml
new file mode 100644
index 0000000000..11218fcf85
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-saturate-red-alpha-1.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 120]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 120]
+ color: [0, 0, 0, 1]
+
+ - type: stacking-context
+ bounds: [10, 10, 100, 100]
+ filters: saturate(0.5)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [255, 0, 0, 0.5]
diff --git a/gfx/wr/wrench/reftests/filters/filter-segments-ref.yaml b/gfx/wr/wrench/reftests/filters/filter-segments-ref.yaml
new file mode 100644
index 0000000000..1047323d23
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-segments-ref.yaml
@@ -0,0 +1,18 @@
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [10, 10, 256, 256]
+ radius: 16
+ - type: rect
+ color: [54, 54, 54]
+ bounds: [10, 10, 512, 512]
+ clip-chain: [2]
+ - type: rect
+ color: red
+ bounds: [0, 0, 300, 32]
+ - type: rect
+ color: red
+ bounds: [0, 250, 300, 32]
diff --git a/gfx/wr/wrench/reftests/filters/filter-segments.yaml b/gfx/wr/wrench/reftests/filters/filter-segments.yaml
new file mode 100644
index 0000000000..dbb62ed7d3
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-segments.yaml
@@ -0,0 +1,24 @@
+# Ensure that picture / filter primitives draw the entire primitive
+# when they are eligible to be segmented. The red rects are used to
+# mask out the corners, since the AA varies between the tests.
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [10, 10, 256, 256]
+ radius: 16
+ - type: stacking-context
+ filters: grayscale(1)
+ items:
+ - type: rect
+ color: red
+ bounds: [10, 10, 512, 512]
+ clip-chain: [2]
+ - type: rect
+ color: red
+ bounds: [0, 0, 300, 32]
+ - type: rect
+ color: red
+ bounds: [0, 250, 300, 32]
diff --git a/gfx/wr/wrench/reftests/filters/filter-small-blur-radius.png b/gfx/wr/wrench/reftests/filters/filter-small-blur-radius.png
new file mode 100644
index 0000000000..e755e2bb6c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-small-blur-radius.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/filter-small-blur-radius.yaml b/gfx/wr/wrench/reftests/filters/filter-small-blur-radius.yaml
new file mode 100644
index 0000000000..d5b550d0ab
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/filter-small-blur-radius.yaml
@@ -0,0 +1,10 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: 100 100 512 512
+ filters: blur(2, 2)
+ items:
+ - type: rect
+ bounds: 0 0 512 512
+ color: red
diff --git a/gfx/wr/wrench/reftests/filters/firefox.png b/gfx/wr/wrench/reftests/filters/firefox.png
new file mode 100644
index 0000000000..696ba9c9b5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/firefox.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/iframe-dropshadow-ref.yaml b/gfx/wr/wrench/reftests/filters/iframe-dropshadow-ref.yaml
new file mode 100644
index 0000000000..47c7140163
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/iframe-dropshadow-ref.yaml
@@ -0,0 +1,22 @@
+# a drop-shadow in an iframe causes the content to not render
+---
+root:
+ items:
+ -
+ type: "stacking-context"
+ bounds: [0, 148, 1920, 1606]
+ items:
+ -
+ type: "rect"
+ color: [255, 0, 0, 1]
+ bounds: [387, 12, 34, 38]
+ clip-rect: [386, 11, 36, 40]
+ -
+ type: "stacking-context"
+ filters: drop-shadow([0, 4], 2, [0, 0, 0, 0.3294117748737335])
+ items:
+ -
+ type: "rect"
+ color: [255, 0, 0, 1]
+ bounds: [345, 12, 34, 38]
+ clip-rect: [344, 11, 36, 40]
diff --git a/gfx/wr/wrench/reftests/filters/iframe-dropshadow.yaml b/gfx/wr/wrench/reftests/filters/iframe-dropshadow.yaml
new file mode 100644
index 0000000000..440e757e04
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/iframe-dropshadow.yaml
@@ -0,0 +1,34 @@
+# a drop-shadow in an iframe causes the content to not render
+---
+root:
+ items:
+ -
+ type: "stacking-context"
+ items:
+ -
+ type: iframe
+ id: [1, 2]
+ bounds: [0, 148, 1920, 1606]
+ clip-rect: [0, 148, 1920, 1606]
+
+pipelines:
+ -
+ id: [1, 2]
+ items:
+ -
+ type: "stacking-context"
+ items:
+ -
+ type: "rect"
+ color: [255, 0, 0, 1]
+ bounds: [387, 12, 34, 38]
+ clip-rect: [386, 11, 36, 40]
+ -
+ type: "stacking-context"
+ filters: drop-shadow([0, 4], 2, [0, 0, 0, 0.3294117748737335])
+ items:
+ -
+ type: "rect"
+ color: [255, 0, 0, 1]
+ bounds: [345, 12, 34, 38]
+ clip-rect: [344, 11, 36, 40]
diff --git a/gfx/wr/wrench/reftests/filters/invisible-ref.yaml b/gfx/wr/wrench/reftests/filters/invisible-ref.yaml
new file mode 100644
index 0000000000..ec31525d31
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/invisible-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [10, 10, 50, 50]
+ color: green
diff --git a/gfx/wr/wrench/reftests/filters/invisible.yaml b/gfx/wr/wrench/reftests/filters/invisible.yaml
new file mode 100644
index 0000000000..c4a99e64a8
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/invisible.yaml
@@ -0,0 +1,27 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ filters: opacity(0.0),
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ items:
+ - type: rect
+ bounds: [70, 70, 50, 50]
+ color: green
+ # This display item ensures that the stacking context is skipped, but
+ # later items are not.
+ - type: rect
+ bounds: [10, 10, 50, 50]
+ color: green
diff --git a/gfx/wr/wrench/reftests/filters/isolated-ref.yaml b/gfx/wr/wrench/reftests/filters/isolated-ref.yaml
new file mode 100644
index 0000000000..28c6912a97
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/isolated-ref.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [0, 0, 0]
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ filters: opacity(0.5)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [0, 255, 0]
diff --git a/gfx/wr/wrench/reftests/filters/isolated.yaml b/gfx/wr/wrench/reftests/filters/isolated.yaml
new file mode 100644
index 0000000000..5f61ba2dfa
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/isolated.yaml
@@ -0,0 +1,17 @@
+# this tests that filters don't create isolated groups
+---
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [0, 0, 0]
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ filters: opacity(0.5)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: [0, 255, 0]
diff --git a/gfx/wr/wrench/reftests/filters/opacity-combined-ref.yaml b/gfx/wr/wrench/reftests/filters/opacity-combined-ref.yaml
new file mode 100644
index 0000000000..2b008f0a2f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/opacity-combined-ref.yaml
@@ -0,0 +1,10 @@
+# this tests that opacity combination respets pre-multiplied alpha
+root:
+ items:
+ - type: stacking-context
+ bounds: [10, 10, 500, 500]
+ filters: [opacity(0.25)]
+ items:
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: green
diff --git a/gfx/wr/wrench/reftests/filters/opacity-combined.yaml b/gfx/wr/wrench/reftests/filters/opacity-combined.yaml
new file mode 100644
index 0000000000..7d9bfa4ac5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/opacity-combined.yaml
@@ -0,0 +1,10 @@
+# this tests that opacity combination respects pre-multiplied alpha
+root:
+ items:
+ - type: stacking-context
+ bounds: [10, 10, 500, 500]
+ filters: [opacity(0.50), opacity(0.50)]
+ items:
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: green
diff --git a/gfx/wr/wrench/reftests/filters/opacity-overlap-ref.yaml b/gfx/wr/wrench/reftests/filters/opacity-overlap-ref.yaml
new file mode 100644
index 0000000000..d3ad75812e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/opacity-overlap-ref.yaml
@@ -0,0 +1,9 @@
+# this tests opacity rectangle overlapping with opaque one
+root:
+ items:
+ - type: stacking-context
+ bounds: [10, 10, 500, 500]
+ items:
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: [32, 0, 96]
diff --git a/gfx/wr/wrench/reftests/filters/opacity-overlap.yaml b/gfx/wr/wrench/reftests/filters/opacity-overlap.yaml
new file mode 100644
index 0000000000..94832004a3
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/opacity-overlap.yaml
@@ -0,0 +1,16 @@
+# this tests opacity rectangle overlapping with opaque one
+root:
+ items:
+ - type: stacking-context
+ bounds: [10, 10, 500, 500]
+ items:
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: [128, 0, 0]
+ - type: stacking-context
+ bounds: [10, 10, 500, 500]
+ filters: [opacity(0.75)]
+ items:
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: [0, 0, 128]
diff --git a/gfx/wr/wrench/reftests/filters/opacity-ref.yaml b/gfx/wr/wrench/reftests/filters/opacity-ref.yaml
new file mode 100644
index 0000000000..ffe06bc5fd
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/opacity-ref.yaml
@@ -0,0 +1,8 @@
+root:
+ items:
+ - type: stacking-context
+ bounds: [10, 10, 500, 500]
+ items:
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: [255, 255, 209, 1.0]
diff --git a/gfx/wr/wrench/reftests/filters/opacity.yaml b/gfx/wr/wrench/reftests/filters/opacity.yaml
new file mode 100644
index 0000000000..838937de2a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/opacity.yaml
@@ -0,0 +1,10 @@
+# this tests opacity pre-multiplied color
+root:
+ items:
+ - type: stacking-context
+ bounds: [10, 10, 500, 500]
+ filters: [opacity(0.9)]
+ items:
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: [255, 255, 0, 0.2]
diff --git a/gfx/wr/wrench/reftests/filters/reftest.list b/gfx/wr/wrench/reftests/filters/reftest.list
new file mode 100644
index 0000000000..493cb6ff8c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/reftest.list
@@ -0,0 +1,85 @@
+== filter-grayscale.yaml filter-grayscale-ref.yaml
+platform(linux,mac) == draw_calls(7) color_targets(7) alpha_targets(0) filter-blur.yaml filter-blur.png
+platform(linux,mac) == filter-blur-downscale-fractional.yaml filter-blur-downscale-fractional.png
+== isolated.yaml isolated-ref.yaml
+== invisible.yaml invisible-ref.yaml
+fuzzy-if(platform(swgl),1,10000) == opacity.yaml opacity-ref.yaml
+fuzzy-range(<=1,*10000) == opacity-combined.yaml opacity-combined-ref.yaml
+fuzzy-if(platform(swgl),1,10000) == opacity-overlap.yaml opacity-overlap-ref.yaml
+== filter-brightness.yaml filter-brightness-ref.yaml
+== filter-brightness-2.yaml filter-brightness-2-ref.yaml
+== filter-brightness-3.yaml filter-brightness-3-ref.yaml
+fuzzy-if(platform(swgl),1,10000) == filter-brightness-4.yaml filter-brightness-4-ref.yaml
+== filter-component-transfer.yaml filter-component-transfer-ref.yaml
+skip_on(android,device) == filter-color-matrix.yaml filter-color-matrix-ref.yaml # fails on Pixel2
+== filter-contrast-gray-alpha-1.yaml filter-contrast-gray-alpha-1-ref.yaml
+== filter-invert.yaml filter-invert-ref.yaml
+== filter-invert-2.yaml filter-invert-2-ref.yaml
+platform(linux,mac) fuzzy(1,133) == filter-large-blur-radius.yaml filter-large-blur-radius.png
+skip_on(android,device) fuzzy(1,12) fuzzy-if(platform(swgl),2,12276) == draw_calls(7) color_targets(6) alpha_targets(0) filter-small-blur-radius.yaml filter-small-blur-radius.png # fails on Pixel2
+== filter-saturate-red-1.yaml filter-saturate-red-1-ref.yaml
+== filter-saturate-red-2.yaml filter-saturate-red-2-ref.yaml
+== filter-saturate-red-3.yaml filter-saturate-red-3-ref.yaml
+== filter-saturate-green-1.yaml filter-saturate-green-1-ref.yaml
+== filter-saturate-green-2.yaml filter-saturate-green-2-ref.yaml
+== filter-saturate-green-3.yaml filter-saturate-green-3-ref.yaml
+== filter-saturate-blue-1.yaml filter-saturate-blue-1-ref.yaml
+== filter-saturate-blue-2.yaml filter-saturate-blue-2-ref.yaml
+== filter-saturate-blue-3.yaml filter-saturate-blue-3-ref.yaml
+== filter-saturate-red-alpha-1.yaml filter-saturate-red-alpha-1-ref.yaml
+== filter-saturate-green-alpha-1.yaml filter-saturate-green-alpha-1-ref.yaml
+== filter-saturate-blue-alpha-1.yaml filter-saturate-blue-alpha-1-ref.yaml
+fuzzy(1,14) == filter-hue-rotate-1.yaml filter-hue-rotate-1-ref.yaml
+skip_on(android,device) == filter-hue-rotate-alpha-1.yaml filter-hue-rotate-alpha-1-ref.yaml # Fails on Pixel2
+skip_on(android,device) fuzzy(2,9072) fuzzy-if(platform(swgl),9,109897) == filter-long-chain.yaml filter-long-chain.png # fails on Pixel2
+platform(linux,mac) == filter-drop-shadow.yaml filter-drop-shadow.png
+platform(linux,mac) == filter-drop-shadow-on-viewport-edge.yaml filter-drop-shadow-on-viewport-edge.png
+platform(linux,mac) == blend-clipped.yaml blend-clipped.png
+platform(linux,mac) == filter-drop-shadow-clip.yaml filter-drop-shadow-clip.png
+fuzzy(2,10) platform(linux,mac) == filter-drop-shadow-clip-2.yaml filter-drop-shadow-clip-2.png
+fuzzy(1,58) platform(linux) == filter-drop-shadow-clip-3.yaml filter-drop-shadow-clip-3.png
+fuzzy(5,100000) == filter-drop-shadow-scaled.yaml filter-drop-shadow-scaled-ref.yaml
+== filter-segments.yaml filter-segments-ref.yaml
+== iframe-dropshadow.yaml iframe-dropshadow-ref.yaml
+skip_on(android,device) == filter-mix-blend-mode.yaml filter-mix-blend-mode-ref.yaml # fails on Pixel2
+== fuzzy(3,20000) srgb-to-linear.yaml srgb-to-linear-ref.yaml
+!= srgb-to-linear-2.yaml srgb-to-linear-ref.yaml
+!= filter-blur-huge.yaml blank.yaml
+!= filter-drop-shadow-huge.yaml blank.yaml
+!= filter-drop-shadow-transform-huge.yaml blank.yaml
+fuzzy(4,62000) == filter-drop-shadow-blur-clamping.yaml filter-drop-shadow-blur-clamping-ref.yaml
+== filter-blur-scaled.yaml filter-blur-scaled-ref.yaml
+fuzzy(5,72000) == filter-blur-clamping.yaml filter-blur-clamping-ref.yaml
+skip_on(android,device) skip_on(win) fuzzy(1,104) fuzzy-if(platform(swgl),4,18484) == filter-blur-scaled-xonly.yaml filter-blur-scaled-xonly.png # fails on Pixel2
+== svg-filter-component-transfer.yaml filter-component-transfer-ref.yaml
+== svg-filter-flood.yaml svg-filter-flood-ref.yaml
+skip_on(android,device) == svg-filter-blend.yaml svg-filter-blend-ref.yaml
+skip_on(android,device) == svg-filter-color-matrix.yaml filter-color-matrix-ref.yaml # fails on Pixel2
+platform(linux,mac) == draw_calls(8) color_targets(8) alpha_targets(0) svg-filter-blur.yaml filter-blur.png # Extra draw call is due to render task graph workaround
+platform(linux,mac) == svg-filter-drop-shadow.yaml svg-filter-drop-shadow.png
+== fuzzy(1,10000) svg-srgb-to-linear.yaml srgb-to-linear-ref.yaml
+platform(linux,mac) == fuzzy(6,36790) svg-filter-drop-shadow-rotate.yaml svg-filter-drop-shadow-rotate-ref.yaml
+platform(linux,mac) fuzzy(3,3550) == svg-filter-blur-transforms.yaml svg-filter-blur-transforms.png
+platform(linux,mac) == svg-filter-drop-shadow-on-viewport-edge.yaml svg-filter-drop-shadow-on-viewport-edge.png
+fuzzy(1,1) platform(linux,mac) == svg-filter-drop-shadow-perspective.yaml svg-filter-drop-shadow-perspective.png
+== backdrop-filter-basic.yaml backdrop-filter-basic-ref.yaml
+platform(linux,mac) == backdrop-filter-perspective.yaml backdrop-filter-perspective.png
+== backdrop-filter-on-child-surface.yaml backdrop-filter-on-child-surface-ref.yaml
+== backdrop-filter-clip-mask.yaml backdrop-filter-clip-mask-ref.yaml
+platform(linux,mac) == backdrop-filter-across-tiles.yaml backdrop-filter-across-tiles.png
+platform(linux,mac) == backdrop-filter-chain.yaml backdrop-filter-chain.png
+platform(linux,mac) == backdrop-filter-overlap.yaml backdrop-filter-overlap.png
+platform(linux,mac) == backdrop-filter-blur-across-tiles.yaml backdrop-filter-blur-across-tiles.png
+== fuzzy(1,4900) backdrop-filter-isolation.yaml backdrop-filter-isolation-ref.yaml
+#platform(linux,mac) == backdrop-filter-drop-shadow.yaml backdrop-filter-drop-shadow.png
+== backdrop-filter-blur-edge-mode.yaml backdrop-filter-blur-edge-mode-ref.yaml
+== backdrop-filter-half-opacity.yaml backdrop-filter-half-opacity-ref.yaml
+== backdrop-filter-culled-filter.yaml backdrop-filter-culled-filter-ref.yaml
+== backdrop-filter-nested.yaml blank.yaml # just don't crash
+platform(linux,mac) == backdrop-filter-transformed-filter.yaml backdrop-filter-transformed-filter.png
+platform(linux,max) == svg-filter-offset.yaml svg-filter-offset-ref.yaml
+skip_on(android,device) == fuzzy(1,100) svg-filter-composite.yaml svg-filter-composite-ref.yaml
+skip_on(android,device) == filter-mix-blend-scaling.yaml filter-mix-blend-scaling-ref.yaml
+platform(linux) == blend-clipped-raster-root.yaml blend-clipped-raster-root.png
+== drop-shadow-inverse-scale.yaml blank.yaml
+platform(linux,mac) == filter-drop-shadow-fractional.yaml filter-drop-shadow-fractional.png
diff --git a/gfx/wr/wrench/reftests/filters/srgb-to-linear-2.yaml b/gfx/wr/wrench/reftests/filters/srgb-to-linear-2.yaml
new file mode 100644
index 0000000000..333ef502ae
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/srgb-to-linear-2.yaml
@@ -0,0 +1,16 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 300, 100]
+ filters: [srgb-to-linear]
+ items:
+ - type: rect
+ bounds: [100, 0, 100, 100]
+ color: [200, 200, 200, 1.0]
+ - type: rect
+ bounds: [100, 0, 100, 100]
+ color: [100, 100, 100, 1.0]
+ - type: rect
+ bounds: [200, 0, 100, 100]
+ color: [50, 50, 50, 1.0]
diff --git a/gfx/wr/wrench/reftests/filters/srgb-to-linear-ref.yaml b/gfx/wr/wrench/reftests/filters/srgb-to-linear-ref.yaml
new file mode 100644
index 0000000000..d984523da6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/srgb-to-linear-ref.yaml
@@ -0,0 +1,15 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 300, 100]
+ items:
+ - type: rect
+ bounds: [100, 0, 100, 100]
+ color: [200, 200, 200, 1.0]
+ - type: rect
+ bounds: [100, 0, 100, 100]
+ color: [100, 100, 100, 1.0]
+ - type: rect
+ bounds: [200, 0, 100, 100]
+ color: [50, 50, 50, 1.0]
diff --git a/gfx/wr/wrench/reftests/filters/srgb-to-linear.yaml b/gfx/wr/wrench/reftests/filters/srgb-to-linear.yaml
new file mode 100644
index 0000000000..67eb35119f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/srgb-to-linear.yaml
@@ -0,0 +1,16 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 300, 100]
+ filters: [srgb-to-linear, linear-to-srgb]
+ items:
+ - type: rect
+ bounds: [100, 0, 100, 100]
+ color: [200, 200, 200, 1.0]
+ - type: rect
+ bounds: [100, 0, 100, 100]
+ color: [100, 100, 100, 1.0]
+ - type: rect
+ bounds: [200, 0, 100, 100]
+ color: [50, 50, 50, 1.0]
diff --git a/gfx/wr/wrench/reftests/filters/svg-filter-blend-ref.yaml b/gfx/wr/wrench/reftests/filters/svg-filter-blend-ref.yaml
new file mode 100644
index 0000000000..d796f0b85e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/svg-filter-blend-ref.yaml
@@ -0,0 +1,21 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 50, 250]
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [0, 255, 0, 1]
+ - type: rect
+ bounds: [0, 50, 50, 50]
+ color: [40, 20, 2, 1]
+ - type: rect
+ bounds: [0, 100, 50, 50]
+ color: [20, 10, 155, 1]
+ - type: rect
+ bounds: [0, 150, 50, 50]
+ color: [255, 222, 156, 1]
+ - type: rect
+ bounds: [0, 200, 50, 50]
+ color: [255, 245, 151, 1]
diff --git a/gfx/wr/wrench/reftests/filters/svg-filter-blend.yaml b/gfx/wr/wrench/reftests/filters/svg-filter-blend.yaml
new file mode 100644
index 0000000000..2167013d3c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/svg-filter-blend.yaml
@@ -0,0 +1,83 @@
+# Tests various blend modes using the blend filter primitive.
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 50, 250]
+ filter-primitives:
+ - type: flood
+ color: [255, 255, 255, 1]
+ color-space: srgb
+ - type: blend
+ in1: original
+ in2: 0
+ color-space: srgb
+ blend-mode: difference
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [255, 0, 255, 1]
+
+ - type: stacking-context
+ bounds: [0, 0, 50, 250]
+ filter-primitives:
+ - type: flood
+ color: [51, 51, 51, 1]
+ color-space: srgb
+ - type: blend
+ in1: original
+ in2: 0
+ color-space: srgb
+ blend-mode: multiply
+ items:
+ - type: rect
+ bounds: [0, 50, 50, 50]
+ color: [200, 100, 10, 1]
+
+ - type: stacking-context
+ bounds: [0, 0, 50, 250]
+ filter-primitives:
+ - type: flood
+ color: [255, 10, 156, 1]
+ color-space: srgb
+ - type: blend
+ in1: original
+ in2: 0
+ color-space: srgb
+ blend-mode: darken
+ items:
+ - type: rect
+ bounds: [0, 100, 50, 50]
+ color: [20, 222, 155, 1]
+
+ - type: stacking-context
+ bounds: [0, 0, 50, 250]
+ filter-primitives:
+ - type: flood
+ color: [255, 10, 156, 1]
+ color-space: srgb
+ - type: blend
+ in1: original
+ in2: 0
+ color-space: srgb
+ blend-mode: lighten
+ items:
+ - type: rect
+ bounds: [0, 150, 50, 50]
+ color: [20, 222, 155, 1]
+
+ - type: stacking-context
+ bounds: [0, 0, 50, 250]
+ filter-primitives:
+ - type: flood
+ color: [255, 10, 156, 1]
+ color-space: srgb
+ - type: blend
+ in1: original
+ in2: 0
+ color-space: srgb
+ blend-mode: exclusion
+ items:
+ - type: rect
+ bounds: [0, 200, 50, 50]
+ color: [0, 255, 24, 1]
diff --git a/gfx/wr/wrench/reftests/filters/svg-filter-blur-transforms.png b/gfx/wr/wrench/reftests/filters/svg-filter-blur-transforms.png
new file mode 100644
index 0000000000..890c815bef
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/svg-filter-blur-transforms.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/svg-filter-blur-transforms.yaml b/gfx/wr/wrench/reftests/filters/svg-filter-blur-transforms.yaml
new file mode 100644
index 0000000000..d78f3ae806
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/svg-filter-blur-transforms.yaml
@@ -0,0 +1,16 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 100, 300, 300]
+ transform: scale-x(0.1) rotate-z(-45)
+ filter-primitives:
+ - type: blur
+ width: 10
+ height: 10
+ in: previous
+ color-space: srgb
+ items:
+ - type: rect
+ color: red
+ bounds: 0 0 256 256
diff --git a/gfx/wr/wrench/reftests/filters/svg-filter-blur.yaml b/gfx/wr/wrench/reftests/filters/svg-filter-blur.yaml
new file mode 100644
index 0000000000..5554688500
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/svg-filter-blur.yaml
@@ -0,0 +1,14 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [100, 100, 300, 300]
+ filter-primitives:
+ - type: blur
+ width: 10
+ height: 10
+ in: previous
+ color-space: srgb
+ items:
+ - image: "firefox.png"
+ bounds: 20 20 256 256
diff --git a/gfx/wr/wrench/reftests/filters/svg-filter-color-matrix.yaml b/gfx/wr/wrench/reftests/filters/svg-filter-color-matrix.yaml
new file mode 100644
index 0000000000..60166b7514
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/svg-filter-color-matrix.yaml
@@ -0,0 +1,69 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 120, 170]
+ items:
+ - type: rect
+ bounds: [0, 0, 120, 170]
+ color: [0, 0, 0, 1]
+ - type: stacking-context
+ bounds: [10, 10, 50, 50]
+ filter-primitives:
+ - type: color-matrix
+ in: previous
+ color-space: srgb
+ matrix: [0.393, 0.686, 0.534, 0,
+ 0.189, 0.168, 0.131, 0,
+ 0.349, 0.272, 0, 0,
+ 0, 0, 0, 1,
+ 0, 0, 0, 0]
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [255, 0, 0, 1]
+ - type: stacking-context
+ bounds: [10, 60, 50, 50]
+ filter-primitives:
+ - type: color-matrix
+ in: previous
+ color-space: srgb
+ matrix: [-1, 0, 0, 0,
+ 0, -1, 0, 0,
+ 0, 0, -1, 0,
+ 0, 0, 0, 1,
+ 1, 1, 1, 0]
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [0, 255, 0, 1]
+ - type: stacking-context
+ bounds: [60, 10, 50, 50]
+ filter-primitives:
+ - type: color-matrix
+ in: previous
+ color-space: srgb
+ matrix: [0, 0, 1, 0,
+ 0, 1, 0, 0,
+ 1, 0, 0, 0,
+ 0, 0, 0, 1,
+ 0, 0, 0, 0]
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [0, 0, 255, 1]
+ - type: stacking-context
+ bounds: [10, 110, 50, 50]
+ filter-primitives:
+ - type: color-matrix
+ in: previous
+ color-space: srgb
+ matrix: [1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0.5,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0]
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [0, 0, 255, 1]
diff --git a/gfx/wr/wrench/reftests/filters/svg-filter-component-transfer.yaml b/gfx/wr/wrench/reftests/filters/svg-filter-component-transfer.yaml
new file mode 100644
index 0000000000..88b56a4a25
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/svg-filter-component-transfer.yaml
@@ -0,0 +1,382 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 50, 250]
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 50, 50]
+ filter-primitives:
+ - type: component-transfer
+ color-space: srgb
+ in: previous
+ filter-datas:
+ - - - Identity
+ - Identity
+ - Identity
+ - Identity
+ - []
+ - []
+ - []
+ - []
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [255, 0, 255, 1]
+ - type: stacking-context
+ bounds: [0, 50, 50, 50]
+ filter-primitives:
+ - type: component-transfer
+ color-space: srgb
+ in: previous
+ filter-datas:
+ - - - Table
+ - Table
+ - Table
+ - Identity
+ - - "1"
+ - "1"
+ - "0"
+ - "0"
+ - - "0"
+ - "0"
+ - "1"
+ - "1"
+ - - "0"
+ - "1"
+ - "1"
+ - "0"
+ - []
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [173, 255, 47, 1]
+ - type: stacking-context
+ bounds: [0, 100, 50, 50]
+ filter-primitives:
+ - type: component-transfer
+ color-space: srgb
+ in: previous
+ filter-datas:
+ - - - Discrete
+ - Discrete
+ - Discrete
+ - Identity
+ - - "1"
+ - "1"
+ - "0"
+ - "0"
+ - - "0"
+ - "0"
+ - "1"
+ - "1"
+ - - "0"
+ - "1"
+ - "1"
+ - "0"
+ - []
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [0, 255, 255, 1]
+ - type: stacking-context
+ bounds: [0, 150, 50, 50]
+ filter-primitives:
+ - type: component-transfer
+ color-space: srgb
+ in: previous
+ filter-datas:
+ - - - Linear
+ - Linear
+ - Linear
+ - Identity
+ - - "0.5"
+ - "0.25"
+ - - "0.5"
+ - "0"
+ - - "0.5"
+ - "0.5"
+ - []
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [255, 255, 0, 1]
+ - type: stacking-context
+ bounds: [0, 200, 50, 50]
+ filter-primitives:
+ - type: component-transfer
+ color-space: srgb
+ in: previous
+ filter-datas:
+ - - - Gamma
+ - Gamma
+ - Gamma
+ - Identity
+ - - "2"
+ - "5"
+ - "-1"
+ - - "2"
+ - "3"
+ - "0"
+ - - "2"
+ - "1"
+ - "-1.75"
+ - []
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [135, 206, 235, 1]
+ - type: stacking-context
+ bounds: [60, 0, 50, 50]
+ filter-primitives:
+ - type: component-transfer
+ color-space: srgb
+ in: previous
+ filter-datas:
+ - - - Identity
+ - Identity
+ - Identity
+ - Identity
+ - []
+ - []
+ - []
+ - []
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [255, 0, 255, 1]
+ - type: stacking-context
+ bounds: [60, 50, 50, 50]
+ filter-primitives:
+ - type: component-transfer
+ color-space: srgb
+ in: previous
+ filter-datas:
+ - - - Table
+ - Table
+ - Table
+ - Table
+ - - "1"
+ - "1"
+ - "0"
+ - "0"
+ - - "0"
+ - "0"
+ - "1"
+ - "1"
+ - - "0"
+ - "1"
+ - "1"
+ - "0"
+ - - "1"
+ - "0"
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [173, 255, 47, 1]
+ - type: stacking-context
+ bounds: [60, 100, 50, 50]
+ filter-primitives:
+ - type: component-transfer
+ color-space: srgb
+ in: previous
+ filter-datas:
+ - - - Discrete
+ - Discrete
+ - Discrete
+ - Discrete
+ - - "1"
+ - "1"
+ - "0"
+ - "0"
+ - - "0"
+ - "0"
+ - "1"
+ - "1"
+ - - "0"
+ - "1"
+ - "1"
+ - "0"
+ - - "1"
+ - "0"
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [0, 255, 255, 1]
+ - type: stacking-context
+ bounds: [60, 150, 50, 50]
+ filter-primitives:
+ - type: component-transfer
+ color-space: srgb
+ in: previous
+ filter-datas:
+ - - - Linear
+ - Linear
+ - Linear
+ - Linear
+ - - "0.5"
+ - "0.25"
+ - - "0.5"
+ - "0"
+ - - "0.5"
+ - "0.5"
+ - - "0.5"
+ - "0"
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [255, 255, 0, 1]
+ - type: stacking-context
+ bounds: [60, 200, 50, 50]
+ filter-primitives:
+ - type: component-transfer
+ color-space: srgb
+ in: previous
+ filter-datas:
+ - - - Gamma
+ - Gamma
+ - Gamma
+ - Gamma
+ - - "2"
+ - "5"
+ - "-1"
+ - - "2"
+ - "3"
+ - "0"
+ - - "2"
+ - "1"
+ - "-1.75"
+ - - "2"
+ - "1"
+ - "-1.75"
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [135, 206, 235, 1]
+ - type: stacking-context
+ bounds: [120, 0, 50, 50]
+ filter-primitives:
+ - type: component-transfer
+ color-space: srgb
+ in: previous
+ filter-datas:
+ - - - Identity
+ - Identity
+ - Identity
+ - Identity
+ - []
+ - []
+ - []
+ - []
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [255, 0, 255, 0.5]
+ - type: stacking-context
+ bounds: [120, 50, 50, 50]
+ filter-primitives:
+ - type: component-transfer
+ color-space: srgb
+ in: previous
+ filter-datas:
+ - - - Table
+ - Table
+ - Table
+ - Table
+ - - "1"
+ - "1"
+ - "0"
+ - "0"
+ - - "0"
+ - "0"
+ - "1"
+ - "1"
+ - - "0"
+ - "1"
+ - "1"
+ - "0"
+ - - "1"
+ - "0"
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [173, 255, 47, 0.5]
+ - type: stacking-context
+ bounds: [120, 100, 50, 50]
+ filter-primitives:
+ - type: component-transfer
+ color-space: srgb
+ in: previous
+ filter-datas:
+ - - - Discrete
+ - Discrete
+ - Discrete
+ - Discrete
+ - - "1"
+ - "1"
+ - "0"
+ - "0"
+ - - "0"
+ - "0"
+ - "1"
+ - "1"
+ - - "0"
+ - "1"
+ - "1"
+ - "0"
+ - - "1"
+ - "0"
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [0, 255, 255, 0.5]
+ - type: stacking-context
+ bounds: [120, 150, 50, 50]
+ filter-primitives:
+ - type: component-transfer
+ color-space: srgb
+ in: previous
+ filter-datas:
+ - - - Linear
+ - Linear
+ - Linear
+ - Linear
+ - - "0.5"
+ - "0.25"
+ - - "0.5"
+ - "0"
+ - - "0.5"
+ - "0.5"
+ - - "0.5"
+ - "0"
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [255, 255, 0, 0.5]
+ - type: stacking-context
+ bounds: [120, 200, 50, 50]
+ filter-primitives:
+ - type: component-transfer
+ color-space: srgb
+ in: previous
+ filter-datas:
+ - - - Gamma
+ - Gamma
+ - Gamma
+ - Gamma
+ - - "2"
+ - "5"
+ - "-1"
+ - - "2"
+ - "3"
+ - "0"
+ - - "2"
+ - "1"
+ - "-1.75"
+ - - "2"
+ - "1"
+ - "-0.25"
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: [135, 206, 235, 0.5]
diff --git a/gfx/wr/wrench/reftests/filters/svg-filter-composite-ref.yaml b/gfx/wr/wrench/reftests/filters/svg-filter-composite-ref.yaml
new file mode 100644
index 0000000000..25d9e7db6a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/svg-filter-composite-ref.yaml
@@ -0,0 +1,73 @@
+# Tests the composite SVG filter primitive
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: 0 0 0 0
+ items:
+ - type: rect
+ color: yellow
+ bounds: 10 10 100 100
+ - type: rect
+ color: blue
+ bounds: 60 60 100 100
+ - type: stacking-context
+ bounds: 200 0 0 0
+ items:
+ - type: rect
+ color: blue
+ bounds: 60 60 50 50
+ - type: stacking-context
+ bounds: 400 0 0 0
+ items:
+ - type: rect
+ color: yellow
+ bounds: 10 10 100 100
+ - type: rect
+ color: blue
+ bounds: 60 60 50 50
+ - type: stacking-context
+ bounds: 600 0 0 0
+ items:
+ - type: rect
+ color: blue
+ bounds: 60 60 100 100
+ - type: rect
+ color: white
+ bounds: 60 60 50 50
+ - type: stacking-context
+ bounds: 0 200 0 0
+ items:
+ - type: rect
+ color: yellow
+ bounds: 10 10 100 100
+ - type: rect
+ color: blue
+ bounds: 60 60 100 100
+ - type: rect
+ color: white
+ bounds: 60 60 50 50
+ - type: stacking-context
+ bounds: 200 200 0 0
+ items:
+ - type: rect
+ color: [255, 0, 0, 1.0]
+ bounds: 10 10 100 100
+ - type: rect
+ color: [0, 0, 255, 1.0]
+ bounds: 60 60 100 100
+ - type: rect
+ color: [255, 0, 255, 1.0]
+ bounds: 60 60 50 50
+ - type: stacking-context
+ bounds: 400 200 0 0
+ items:
+ - type: rect
+ color: [255, 255, 127, 1.0]
+ bounds: 10 10 100 100
+ - type: rect
+ color: [127, 127, 255, 1.0]
+ bounds: 60 60 100 100
+ - type: rect
+ color: [188, 188, 188, 1.0]
+ bounds: 60 60 50 50
diff --git a/gfx/wr/wrench/reftests/filters/svg-filter-composite.yaml b/gfx/wr/wrench/reftests/filters/svg-filter-composite.yaml
new file mode 100644
index 0000000000..68f3cc0744
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/svg-filter-composite.yaml
@@ -0,0 +1,124 @@
+# Tests the composite SVG filter primitive
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: 0 0 0 0
+ filter-primitives:
+ - type: flood
+ color: yellow
+ in: previous
+ - type: offset
+ offset: -50 -50
+ in: previous
+ - type: composite
+ in1: original
+ in2: 1
+ operator: over
+ items:
+ - type: rect
+ color: blue
+ bounds: 60 60 100 100
+ - type: stacking-context
+ bounds: 200 0 0 0
+ filter-primitives:
+ - type: flood
+ color: yellow
+ in: previous
+ - type: offset
+ offset: -50 -50
+ in: previous
+ - type: composite
+ in1: original
+ in2: 1
+ operator: in
+ items:
+ - type: rect
+ color: blue
+ bounds: 60 60 100 100
+ - type: stacking-context
+ bounds: 400 0 0 0
+ filter-primitives:
+ - type: flood
+ color: yellow
+ in: previous
+ - type: offset
+ offset: -50 -50
+ in: previous
+ - type: composite
+ in1: original
+ in2: 1
+ operator: atop
+ items:
+ - type: rect
+ color: blue
+ bounds: 60 60 100 100
+ - type: stacking-context
+ bounds: 600 0 0 0
+ filter-primitives:
+ - type: flood
+ color: yellow
+ in: previous
+ - type: offset
+ offset: -50 -50
+ in: previous
+ - type: composite
+ in1: original
+ in2: 1
+ operator: out
+ items:
+ - type: rect
+ color: blue
+ bounds: 60 60 100 100
+ - type: stacking-context
+ bounds: 0 200 0 0
+ filter-primitives:
+ - type: flood
+ color: yellow
+ in: previous
+ - type: offset
+ offset: -50 -50
+ in: previous
+ - type: composite
+ in1: original
+ in2: 1
+ operator: xor
+ items:
+ - type: rect
+ color: blue
+ bounds: 60 60 100 100
+ - type: stacking-context
+ bounds: 200 200 0 0
+ filter-primitives:
+ - type: flood
+ color: [255, 0, 0, 1.0]
+ in: previous
+ - type: offset
+ offset: -50 -50
+ in: previous
+ - type: composite
+ in1: original
+ in2: 1
+ operator: lighter
+ items:
+ - type: rect
+ color: [0, 0, 255, 1.0]
+ bounds: 60 60 100 100
+ - type: stacking-context
+ bounds: 400 200 0 0
+ filter-primitives:
+ - type: flood
+ color: yellow
+ in: previous
+ - type: offset
+ offset: -50 -50
+ in: previous
+ - type: composite
+ in1: original
+ in2: 1
+ operator: arithmetic
+ k-values: [0.5, 0.5, 0.5, 0]
+ items:
+ - type: rect
+ color: blue
+ bounds: 60 60 100 100
diff --git a/gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow-on-viewport-edge.png b/gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow-on-viewport-edge.png
new file mode 100644
index 0000000000..a63372edfb
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow-on-viewport-edge.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow-on-viewport-edge.yaml b/gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow-on-viewport-edge.yaml
new file mode 100644
index 0000000000..3b1a4da213
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow-on-viewport-edge.yaml
@@ -0,0 +1,16 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [10, 10, 400, 400]
+ filter-primitives:
+ - type: drop-shadow
+ offset: [10, 10]
+ radius: 20
+ color: [255, 0, 0, 1]
+ in: previous
+ color-space: srgb
+ items:
+ - type: rect
+ bounds: 0 0 256 256
+ color: green
diff --git a/gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow-perspective.png b/gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow-perspective.png
new file mode 100644
index 0000000000..1015e7f4c0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow-perspective.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow-perspective.yaml b/gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow-perspective.yaml
new file mode 100644
index 0000000000..744e2f655e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow-perspective.yaml
@@ -0,0 +1,22 @@
+# Tests SVG drop shadows with perspective transforms
+---
+root:
+ items:
+ - type: stacking-context
+ perspective: 100
+ perspective-origin: 100 50
+ items:
+ - type: "stacking-context"
+ transform-origin: 0 250
+ transform: rotate-x(-15)
+ filter-primitives:
+ - type: drop-shadow
+ color: red
+ offset: [20, 20]
+ radius: 10
+ in: previous
+ color-space: srgb
+ items:
+ - type: rect
+ color: blue
+ bounds: 0 0 200 200
diff --git a/gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow-rotate-ref.yaml b/gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow-rotate-ref.yaml
new file mode 100644
index 0000000000..69501250a0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow-rotate-ref.yaml
@@ -0,0 +1,11 @@
+# Tests SVG drop shadows with transforms
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [100, 100, 400, 400]
+ filters: drop-shadow([73, 73], 20, [255, 0, 0, 1])
+ transform: rotate-z(45)
+ items:
+ - image: "firefox.png"
+ bounds: 0 0 256 256
diff --git a/gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow-rotate.yaml b/gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow-rotate.yaml
new file mode 100644
index 0000000000..71acca0f52
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow-rotate.yaml
@@ -0,0 +1,17 @@
+# Tests SVG drop shadows with transforms
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [100, 100, 400, 400]
+ transform: rotate-z(45)
+ filter-primitives:
+ - type: drop-shadow
+ in: previous
+ offset: [73, 73]
+ radius: 20
+ color: [255, 0, 0, 1]
+ color-space: srgb
+ items:
+ - image: "firefox.png"
+ bounds: 0 0 256 256
diff --git a/gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow.png b/gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow.png
new file mode 100644
index 0000000000..eee62c79d2
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow.yaml b/gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow.yaml
new file mode 100644
index 0000000000..25cdfaf61e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/svg-filter-drop-shadow.yaml
@@ -0,0 +1,16 @@
+# Tests that SVG drop shadows are working properly
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [100, 100, 400, 400]
+ filter-primitives:
+ - type: drop-shadow
+ in: previous
+ offset: [73, 73]
+ radius: 20
+ color: [255, 0, 0, 1]
+ color-space: srgb
+ items:
+ - image: "firefox.png"
+ bounds: 0 0 256 256
diff --git a/gfx/wr/wrench/reftests/filters/svg-filter-flood-ref.yaml b/gfx/wr/wrench/reftests/filters/svg-filter-flood-ref.yaml
new file mode 100644
index 0000000000..ae4eb6a0d1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/svg-filter-flood-ref.yaml
@@ -0,0 +1,10 @@
+# Test that flood filter is equivalent to drawing a rect with the same size and color
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [100, 100, 400, 400]
+ items:
+ - type: rect
+ bounds: [20, 20, 256, 256]
+ color: [0, 255.0, 0, 0.4]
diff --git a/gfx/wr/wrench/reftests/filters/svg-filter-flood.yaml b/gfx/wr/wrench/reftests/filters/svg-filter-flood.yaml
new file mode 100644
index 0000000000..bf896e266f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/svg-filter-flood.yaml
@@ -0,0 +1,10 @@
+# Test that flood filter is equivalent to drawing a rect with the same size and color
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [100, 100, 400, 400]
+ filters: flood([0, 255.0, 0, 0.4])
+ items:
+ - image: "firefox.png"
+ bounds: 20 20 256 256
diff --git a/gfx/wr/wrench/reftests/filters/svg-filter-offset-ref.yaml b/gfx/wr/wrench/reftests/filters/svg-filter-offset-ref.yaml
new file mode 100644
index 0000000000..f6326b5134
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/svg-filter-offset-ref.yaml
@@ -0,0 +1,11 @@
+# Tests the SVG offset filter primitive
+# An offset filter should have the same effect as changing the origin of the rectangle.
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: 0 0 0 0
+ items:
+ - type: rect
+ bounds: 20 20 100 100
+ color: red
diff --git a/gfx/wr/wrench/reftests/filters/svg-filter-offset.yaml b/gfx/wr/wrench/reftests/filters/svg-filter-offset.yaml
new file mode 100644
index 0000000000..f48fb5104e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/svg-filter-offset.yaml
@@ -0,0 +1,15 @@
+# Tests the SVG offset filter primitive
+# An offset filter should have the same effect as changing the origin of the rectangle.
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: 0 0 0 0
+ filter-primitives:
+ - type: offset
+ offset: 10 10
+ in: original
+ items:
+ - type: rect
+ bounds: 10 10 100 100
+ color: red
diff --git a/gfx/wr/wrench/reftests/filters/svg-srgb-to-linear.yaml b/gfx/wr/wrench/reftests/filters/svg-srgb-to-linear.yaml
new file mode 100644
index 0000000000..f7f33165f5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/filters/svg-srgb-to-linear.yaml
@@ -0,0 +1,20 @@
+# this test ensures that a sRGB -> linear-RGB -> sRGB results in no change (with exception to rounding error)
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 300, 100]
+ filter-primitives:
+ - type: identity
+ in: previous
+ color-space: linear-rgb
+ items:
+ - type: rect
+ bounds: [100, 0, 100, 100]
+ color: [200, 200, 200, 1.0]
+ - type: rect
+ bounds: [100, 0, 100, 100]
+ color: [100, 100, 100, 1.0]
+ - type: rect
+ bounds: [200, 0, 100, 100]
+ color: [50, 50, 50, 1.0]
diff --git a/gfx/wr/wrench/reftests/gradient/conic-angle-wraparound-negative.yaml b/gfx/wr/wrench/reftests/gradient/conic-angle-wraparound-negative.yaml
new file mode 100644
index 0000000000..f2053c42b6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/conic-angle-wraparound-negative.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: conic-gradient
+ bounds: 50 50 300 300
+ center: 150 150
+ angle: -5.497787143782138
+ stops: [0.0, red, 1.0, yellow] \ No newline at end of file
diff --git a/gfx/wr/wrench/reftests/gradient/conic-angle-wraparound.yaml b/gfx/wr/wrench/reftests/gradient/conic-angle-wraparound.yaml
new file mode 100644
index 0000000000..67a1370eac
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/conic-angle-wraparound.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: conic-gradient
+ bounds: 50 50 300 300
+ center: 150 150
+ angle: 7.0685834705770345
+ stops: [0.0, red, 1.0, yellow] \ No newline at end of file
diff --git a/gfx/wr/wrench/reftests/gradient/conic-angle.png b/gfx/wr/wrench/reftests/gradient/conic-angle.png
new file mode 100644
index 0000000000..81ec931cec
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/conic-angle.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/gradient/conic-angle.yaml b/gfx/wr/wrench/reftests/gradient/conic-angle.yaml
new file mode 100644
index 0000000000..11a068c9e6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/conic-angle.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: conic-gradient
+ bounds: 50 50 300 300
+ center: 150 150
+ angle: 0.7853981633974483
+ stops: [0.0, red, 1.0, yellow] \ No newline at end of file
diff --git a/gfx/wr/wrench/reftests/gradient/conic-backdrop-ref.yaml b/gfx/wr/wrench/reftests/gradient/conic-backdrop-ref.yaml
new file mode 100644
index 0000000000..e4d55171d3
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/conic-backdrop-ref.yaml
@@ -0,0 +1,11 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: 0 0 800 450
+ color: red
+ - type: conic-gradient
+ bounds: 0 0 800 450
+ center: 400 225
+ angle: 0.0
+ stops: [ 0.0, [255, 255, 255, 1], 1.0, [0,0,0,1] ]
diff --git a/gfx/wr/wrench/reftests/gradient/conic-backdrop-with-spacing-ref.yaml b/gfx/wr/wrench/reftests/gradient/conic-backdrop-with-spacing-ref.yaml
new file mode 100644
index 0000000000..2f9bd2225b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/conic-backdrop-with-spacing-ref.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: 0 0 800 450
+ color: red
+ - type: conic-gradient
+ bounds: 0 0 800 450
+ center: 100 100
+ angle: 0.0
+ stops: [ 0.0, [255, 255, 255, 1], 1.0, [0,0,0,1] ]
+ tile-size: 100 100
+ tile-spacing: 20 20
diff --git a/gfx/wr/wrench/reftests/gradient/conic-backdrop-with-spacing.yaml b/gfx/wr/wrench/reftests/gradient/conic-backdrop-with-spacing.yaml
new file mode 100644
index 0000000000..695e600dff
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/conic-backdrop-with-spacing.yaml
@@ -0,0 +1,18 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: 0 0 800 450
+ color: red
+ - type: "scroll-frame"
+ bounds: 0 0 800 450
+ clip-rect: 0 0 800 450
+ id: 2
+ - type: conic-gradient
+ bounds: 0 0 800 450
+ spatial-id: 2
+ center: 100 100
+ angle: 0.0
+ stops: [ 0.0, [255, 255, 255, 1], 1.0, [0,0,0,1] ]
+ tile-size: 100 100
+ tile-spacing: 20 20
diff --git a/gfx/wr/wrench/reftests/gradient/conic-backdrop.yaml b/gfx/wr/wrench/reftests/gradient/conic-backdrop.yaml
new file mode 100644
index 0000000000..bc654a50ac
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/conic-backdrop.yaml
@@ -0,0 +1,16 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: 0 0 800 450
+ color: red
+ - type: "scroll-frame"
+ bounds: 0 0 800 450
+ clip-rect: 0 0 800 450
+ id: 2
+ - type: conic-gradient
+ bounds: 0 0 800 450
+ spatial-id: 2
+ center: 400 225
+ angle: 0.0
+ stops: [ 0.0, [255, 255, 255, 1], 1.0, [0,0,0,1] ]
diff --git a/gfx/wr/wrench/reftests/gradient/conic-center.png b/gfx/wr/wrench/reftests/gradient/conic-center.png
new file mode 100644
index 0000000000..9843a2efb5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/conic-center.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/gradient/conic-center.yaml b/gfx/wr/wrench/reftests/gradient/conic-center.yaml
new file mode 100644
index 0000000000..d01ebc9c8e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/conic-center.yaml
@@ -0,0 +1,11 @@
+---
+root:
+ items:
+ - type: conic-gradient
+ bounds: 50 50 200 200
+ center: 50 50
+ angle: 0.0
+ stops: [0.0, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ 0.5, blue, 0.75, blue,
+ 0.75, black, 1.0, black] \ No newline at end of file
diff --git a/gfx/wr/wrench/reftests/gradient/conic-color-wheel.png b/gfx/wr/wrench/reftests/gradient/conic-color-wheel.png
new file mode 100644
index 0000000000..3fff3c32d4
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/conic-color-wheel.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/gradient/conic-color-wheel.yaml b/gfx/wr/wrench/reftests/gradient/conic-color-wheel.yaml
new file mode 100644
index 0000000000..60e6bad865
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/conic-color-wheel.yaml
@@ -0,0 +1,14 @@
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [50, 50, 300, 300]
+ radius: 300
+ - type: conic-gradient
+ bounds: 50 50 300 300
+ center: 150 150
+ angle: 0.0
+ stops: [0.0, red, 0.16666, yellow, 0.33333, green, 0.5, [0,255,255,1], 0.66666, blue, 0.83333, [255,0,255,1], 1.0, red]
+ clip-chain: [2]
diff --git a/gfx/wr/wrench/reftests/gradient/conic-large-hard-stop-ref.yaml b/gfx/wr/wrench/reftests/gradient/conic-large-hard-stop-ref.yaml
new file mode 100644
index 0000000000..95c4daf636
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/conic-large-hard-stop-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: conic-gradient
+ bounds: 50 50 250 250
+ center: 100 100
+ angle: 0
+ stops: [0.0, red, 0.2, red, 0.2, yellow, 1.0, yellow]
diff --git a/gfx/wr/wrench/reftests/gradient/conic-large-hard-stop.yaml b/gfx/wr/wrench/reftests/gradient/conic-large-hard-stop.yaml
new file mode 100644
index 0000000000..5656035f5b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/conic-large-hard-stop.yaml
@@ -0,0 +1,14 @@
+---
+root:
+ items:
+ - type: conic-gradient
+ bounds: 50 50 2048 2048
+ center: 100 100
+ angle: 0
+ stops: [0.0, red, 0.2, red, 0.2, yellow, 1.0, yellow]
+ - type: rect
+ bounds: 0 300 2048 2048
+ color: white
+ - type: rect
+ bounds: 300 0 2048 2048
+ color: white
diff --git a/gfx/wr/wrench/reftests/gradient/conic-large-ref.yaml b/gfx/wr/wrench/reftests/gradient/conic-large-ref.yaml
new file mode 100644
index 0000000000..9441175cf1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/conic-large-ref.yaml
@@ -0,0 +1,16 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: 50 50 2000 300
+ color: blue
+
+ - type: conic-gradient
+ bounds: 50 50 2000 300
+ center: 150 150
+ angle: 0.0
+ stops: [0.0, red,
+ 0.125, blue,
+ 0.375, blue,
+ 0.5, yellow,
+ 1.0, red] \ No newline at end of file
diff --git a/gfx/wr/wrench/reftests/gradient/conic-large.yaml b/gfx/wr/wrench/reftests/gradient/conic-large.yaml
new file mode 100644
index 0000000000..78bf305f54
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/conic-large.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: conic-gradient
+ bounds: 50 50 2000 300
+ center: 150 150
+ angle: 0.0
+ stops: [0.0, red,
+ 0.125, blue,
+ 0.375, blue,
+ 0.5, yellow,
+ 1.0, red] \ No newline at end of file
diff --git a/gfx/wr/wrench/reftests/gradient/conic-nan.yaml b/gfx/wr/wrench/reftests/gradient/conic-nan.yaml
new file mode 100644
index 0000000000..6cc6234611
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/conic-nan.yaml
@@ -0,0 +1,41 @@
+---
+root:
+ items:
+ - type: conic-gradient
+ bounds: 50 50 200 NaN
+ center: 100 100
+ angle: 0.0
+ stops: [0.0, red, 0.25, green, 0.5, blue, 0.75, black]
+ - type: conic-gradient
+ bounds: 50 50 200 200
+ center: NaN 100
+ angle: 0.0
+ stops: [0.0, red, 0.25, green, 0.5, blue, 0.75, black]
+ - type: conic-gradient
+ bounds: 50 50 200 200
+ center: 100 100
+ angle: NaN
+ stops: [0.0, red, 0.25, green, 0.5, blue, 0.75, black]
+ - type: conic-gradient
+ bounds: 50 50 200 200
+ center: 100 100
+ angle: 0.0
+ stops: [0.0, red, NaN, green, 0.5, blue, 0.75, black]
+ - type: conic-gradient
+ bounds: 50 50 200 200
+ tile-size: NaN 200
+ center: 100 100
+ angle: 0.0
+ stops: [0.0, red, 0.25, green, 0.5, blue, 0.75, black]
+ - type: conic-gradient
+ bounds: 50 50 200 200
+ clip-rect: NaN 0 100 100
+ center: 100 100
+ angle: 0.0
+ stops: [0.0, red, 0.25, green, 0.5, blue, 0.75, black]
+ - type: conic-gradient
+ bounds: NaN NaN NaN NaN
+ clip-rect: NaN NaN NaN NaN
+ center: NaN NaN
+ angle: NaN
+ stops: [NaN, red, NaN, green, NaN, blue, NaN, black]
diff --git a/gfx/wr/wrench/reftests/gradient/conic-ref.yaml b/gfx/wr/wrench/reftests/gradient/conic-ref.yaml
new file mode 100644
index 0000000000..c29484ae9c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/conic-ref.yaml
@@ -0,0 +1,18 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: 50 50 100 100
+ items:
+ - type: rect
+ bounds: 0 0 100 100
+ color: black
+ - type: rect
+ bounds: 100 0 100 100
+ color: red
+ - type: rect
+ bounds: 100 100 100 100
+ color: green
+ - type: rect
+ bounds: 0 100 100 100
+ color: blue
diff --git a/gfx/wr/wrench/reftests/gradient/conic-simple.png b/gfx/wr/wrench/reftests/gradient/conic-simple.png
new file mode 100644
index 0000000000..79d0885c40
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/conic-simple.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/gradient/conic-simple.yaml b/gfx/wr/wrench/reftests/gradient/conic-simple.yaml
new file mode 100644
index 0000000000..c40c878403
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/conic-simple.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: conic-gradient
+ bounds: 50 50 300 300
+ center: 150 150
+ angle: 0.0
+ stops: [0.0, red, 1.0, yellow] \ No newline at end of file
diff --git a/gfx/wr/wrench/reftests/gradient/conic.yaml b/gfx/wr/wrench/reftests/gradient/conic.yaml
new file mode 100644
index 0000000000..ad034d6a9b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/conic.yaml
@@ -0,0 +1,11 @@
+---
+root:
+ items:
+ - type: conic-gradient
+ bounds: 50 50 200 200
+ center: 100 100
+ angle: 0.0
+ stops: [0.0, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ 0.5, blue, 0.75, blue,
+ 0.75, black, 1.0, black] \ No newline at end of file
diff --git a/gfx/wr/wrench/reftests/gradient/gradient_cache_5stops.yaml b/gfx/wr/wrench/reftests/gradient/gradient_cache_5stops.yaml
new file mode 100644
index 0000000000..d448723002
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/gradient_cache_5stops.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 0 0 960 540
+ start: 0 0
+ end: 960 0
+ stops: [0.0, red,
+ 0.25, green,
+ 0.5, blue,
+ 0.75, [40,40,40,1],
+ 1.0, [100,200,50,1]]
+
diff --git a/gfx/wr/wrench/reftests/gradient/gradient_cache_5stops_ref.yaml b/gfx/wr/wrench/reftests/gradient/gradient_cache_5stops_ref.yaml
new file mode 100644
index 0000000000..34b6b0e01c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/gradient_cache_5stops_ref.yaml
@@ -0,0 +1,18 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 0 0 480 540
+ start: 0 0
+ end: 480 0
+ stops: [0.0, red,
+ 0.5, green,
+ 1.0, blue]
+ - type: gradient
+ bounds: 480 0 480 540
+ start: 0 0
+ end: 480 0
+ stops: [ 0.0, blue,
+ 0.5, [40,40,40,1],
+ 1.0, [100,200,50,1]]
+
diff --git a/gfx/wr/wrench/reftests/gradient/gradient_cache_5stops_vertical.yaml b/gfx/wr/wrench/reftests/gradient/gradient_cache_5stops_vertical.yaml
new file mode 100644
index 0000000000..dd2c8b7c9d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/gradient_cache_5stops_vertical.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 0 0 960 540
+ start: 0 0
+ end: 0 540
+ stops: [0.0, red,
+ 0.25, green,
+ 0.5, blue,
+ 0.75, [40,40,40,1],
+ 1.0, [100,200,50,1]]
+
diff --git a/gfx/wr/wrench/reftests/gradient/gradient_cache_5stops_vertical_ref.yaml b/gfx/wr/wrench/reftests/gradient/gradient_cache_5stops_vertical_ref.yaml
new file mode 100644
index 0000000000..704b5be2f6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/gradient_cache_5stops_vertical_ref.yaml
@@ -0,0 +1,18 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 0 0 960 270
+ start: 0 0
+ end: 0 270
+ stops: [0.0, red,
+ 0.5, green,
+ 1.0, blue]
+ - type: gradient
+ bounds: 0 270 960 270
+ start: 0 0
+ end: 0 270
+ stops: [ 0.0, blue,
+ 0.5, [40,40,40,1],
+ 1.0, [100,200,50,1]]
+
diff --git a/gfx/wr/wrench/reftests/gradient/gradient_cache_clamp.yaml b/gfx/wr/wrench/reftests/gradient/gradient_cache_clamp.yaml
new file mode 100644
index 0000000000..1c55a269a1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/gradient_cache_clamp.yaml
@@ -0,0 +1,20 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 0 0 400 200
+ start: 0 100
+ end: 100 100
+ stops: [0.0, blue, 1.0, blue, 1.0, red]
+ - type: gradient
+ bounds: 0 300 400 200
+ start: 100 100
+ end: 200 100
+ stops: [0.0, blue, 1.0, blue, 1.0, red]
+ - type: gradient
+ bounds: 0 600 200 400
+ start: 0 100
+ end: 0 300
+ stops: [
+ 0.0, blue,
+ 1.0, red]
diff --git a/gfx/wr/wrench/reftests/gradient/gradient_cache_clamp_ref.yaml b/gfx/wr/wrench/reftests/gradient/gradient_cache_clamp_ref.yaml
new file mode 100644
index 0000000000..4631192cd8
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/gradient_cache_clamp_ref.yaml
@@ -0,0 +1,30 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 0 0 400 200
+ start: 0 100
+ end: 400 100
+ stops: [
+ 0.0, blue,
+ 0.25, blue,
+ 0.25, red,
+ 1.0, red]
+ - type: gradient
+ bounds: 0 300 400 200
+ start: 0 100
+ end: 400 100
+ stops: [
+ 0.0, blue,
+ 0.5, blue,
+ 0.5, red,
+ 1.0, red]
+ - type: gradient
+ bounds: 0 600 200 400
+ start: 0 0
+ end: 0 400
+ stops: [
+ 0.0, blue,
+ 0.25, blue,
+ 0.75, red,
+ 1.0, red]
diff --git a/gfx/wr/wrench/reftests/gradient/gradient_cache_hardstop.yaml b/gfx/wr/wrench/reftests/gradient/gradient_cache_hardstop.yaml
new file mode 100644
index 0000000000..53c908fb22
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/gradient_cache_hardstop.yaml
@@ -0,0 +1,19 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 0 0 960 540
+ start: 0 0
+ end: 960 0
+ stops: [0.0, red,
+ 0.125, yellow,
+ 0.25, red,
+ 0.25, green,
+ 0.375, yellow,
+ 0.5, green,
+ 0.5, blue,
+ 0.625, yellow,
+ 0.75, blue,
+ 0.75, white,
+ 1.0, [100,200,50,1]]
+
diff --git a/gfx/wr/wrench/reftests/gradient/gradient_cache_hardstop_clip.yaml b/gfx/wr/wrench/reftests/gradient/gradient_cache_hardstop_clip.yaml
new file mode 100644
index 0000000000..1af3894406
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/gradient_cache_hardstop_clip.yaml
@@ -0,0 +1,27 @@
+---
+root:
+ items:
+ - type: clip
+ id: 101
+ complex:
+ - rect: [100, 100, 760, 340]
+ radius: [32, 32]
+ - type: clip-chain
+ id: 201
+ clips: [101]
+ - type: gradient
+ bounds: 0 0 960 540
+ start: 0 0
+ end: 960 0
+ stops: [0.0, red,
+ 0.125, yellow,
+ 0.25, red,
+ 0.25, green,
+ 0.375, yellow,
+ 0.5, green,
+ 0.5, blue,
+ 0.625, yellow,
+ 0.75, blue,
+ 0.75, white,
+ 1.0, [100,200,50,1]]
+ clip-chain: 201
diff --git a/gfx/wr/wrench/reftests/gradient/gradient_cache_hardstop_clip_ref.yaml b/gfx/wr/wrench/reftests/gradient/gradient_cache_hardstop_clip_ref.yaml
new file mode 100644
index 0000000000..ac13881be8
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/gradient_cache_hardstop_clip_ref.yaml
@@ -0,0 +1,41 @@
+---
+root:
+ items:
+ - type: clip
+ id: 101
+ complex:
+ - rect: [100, 100, 760, 340]
+ radius: [32, 32]
+ - type: clip-chain
+ id: 201
+ clips: [101]
+ - type: gradient
+ bounds: 0 0 480 540
+ start: 0 0
+ end: 480 0
+ stops: [0.0, red,
+ 0.25, yellow,
+ 0.5, red,
+ 0.5, green,
+ 0.75, yellow,
+ 1.0, green]
+ clip-chain: 201
+
+ - type: clip
+ id: 102
+ complex:
+ - rect: [100, 100, 760, 340]
+ radius: [32, 32]
+ - type: clip-chain
+ id: 202
+ clips: [102]
+ - type: gradient
+ bounds: 480 0 480 540
+ start: 0 0
+ end: 480 0
+ stops: [0.0, blue,
+ 0.25, yellow,
+ 0.5, blue,
+ 0.5, white,
+ 1.0, [100,200,50,1]]
+ clip-chain: 202
diff --git a/gfx/wr/wrench/reftests/gradient/gradient_cache_hardstop_ref.yaml b/gfx/wr/wrench/reftests/gradient/gradient_cache_hardstop_ref.yaml
new file mode 100644
index 0000000000..e4b3928046
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/gradient_cache_hardstop_ref.yaml
@@ -0,0 +1,24 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 0 0 480 540
+ start: 0 0
+ end: 480 0
+ stops: [0.0, red,
+ 0.25, yellow,
+ 0.5, red,
+ 0.5, green,
+ 0.75, yellow,
+ 1.0, green]
+ - type: gradient
+ bounds: 480 0 480 540
+ start: 0 0
+ end: 480 0
+ stops: [0.0, blue,
+ 0.25, yellow,
+ 0.5, blue,
+ 0.5, white,
+ 1.0, [100,200,50,1]]
+
+
diff --git a/gfx/wr/wrench/reftests/gradient/gradient_cache_repeat.yaml b/gfx/wr/wrench/reftests/gradient/gradient_cache_repeat.yaml
new file mode 100644
index 0000000000..20a07a72a6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/gradient_cache_repeat.yaml
@@ -0,0 +1,119 @@
+---
+root:
+ items:
+ # non-repeating
+ - type: gradient
+ bounds: 100 50 500 10
+ start: 100 0
+ end: 200 0
+ repeat: false
+ stops: [0.0, green,
+ 0.5, green,
+ 0.5, blue,
+ 1.0, blue ]
+
+ # repeat 4 times
+ - type: gradient
+ bounds: 100 100 500 10
+ start: 100 0
+ end: 200 0
+ repeat: true
+ stops: [0.0, green,
+ 0.5, green,
+ 0.5, blue,
+ 1.0, blue ]
+
+ # same but start doesn't line up with 0
+ - type: gradient
+ bounds: 100 150 500 10
+ start: 125 0
+ end: 225 0
+ repeat: true
+ stops: [0.0, green,
+ 0.5, green,
+ 0.5, blue,
+ 1.0, blue ]
+
+ # more hard stops, non-uniform distribution
+ - type: gradient
+ bounds: 100 250 500 10
+ start: 200 0
+ end: 300 0
+ repeat: false
+ stops: [0.0, green,
+ 0.25, green,
+ 0.25, red,
+ 0.75, red,
+ 0.75, blue,
+ 1.0, blue ]
+
+ # repeat the hard stops
+ - type: gradient
+ bounds: 100 300 500 10
+ start: 200 0
+ end: 300 0
+ repeat: true
+ stops: [0.0, green,
+ 0.25, green,
+ 0.25, red,
+ 0.75, red,
+ 0.75, blue,
+ 1.0, blue ]
+
+ # same but start doesn't line up with 0
+ - type: gradient
+ bounds: 100 350 500 10
+ start: 175 0
+ end: 275 0
+ repeat: true
+ stops: [0.0, green,
+ 0.25, green,
+ 0.25, red,
+ 0.75, red,
+ 0.75, blue,
+ 1.0, blue ]
+
+ # the entire gradient from 0 to 1 is
+ # "offscreen", we're only seeing its
+ # repeats. the gradient is 100 wide
+ # and ends at -75, so the first
+ # three-quarters of it would be hidden,
+ # that is, it should start with blue.
+ - type: gradient
+ bounds: 100 400 500 10
+ start: -175 0
+ end: -75 0
+ repeat: true
+ stops: [0.0, green,
+ 0.25, green,
+ 0.25, red,
+ 0.75, red,
+ 0.75, blue,
+ 1.0, blue ]
+
+ # same but over on the right
+ - type: gradient
+ bounds: 100 450 500 10
+ start: 575 0
+ end: 675 0
+ repeat: true
+ stops: [0.0, green,
+ 0.25, green,
+ 0.25, red,
+ 0.75, red,
+ 0.75, blue,
+ 1.0, blue ]
+
+ # a repeat, but not really because only part
+ # of the gradient is visible
+ - type: gradient
+ bounds: 100 500 500 10
+ start: -50 0
+ end: 550 0
+ repeat: true
+ stops: [0.0, green,
+ 0.25, green,
+ 0.25, red,
+ 0.75, red,
+ 0.75, blue,
+ 1.0, blue ]
diff --git a/gfx/wr/wrench/reftests/gradient/gradient_cache_repeat_ref.yaml b/gfx/wr/wrench/reftests/gradient/gradient_cache_repeat_ref.yaml
new file mode 100644
index 0000000000..e1682622f8
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/gradient_cache_repeat_ref.yaml
@@ -0,0 +1,119 @@
+---
+root:
+ items:
+ # non-repeating
+ - type: gradient
+ bounds: 100 50 500 10
+ start: 100 0
+ end: 200 0.001
+ repeat: false
+ stops: [0.0, green,
+ 0.5, green,
+ 0.5, blue,
+ 1.0, blue ]
+
+ # repeat 4 times
+ - type: gradient
+ bounds: 100 100 500 10
+ start: 100 0
+ end: 200 0.001
+ repeat: true
+ stops: [0.0, green,
+ 0.5, green,
+ 0.5, blue,
+ 1.0, blue ]
+
+ # same but start doesn't line up with 0
+ - type: gradient
+ bounds: 100 150 500 10
+ start: 125 0
+ end: 225 0.001
+ repeat: true
+ stops: [0.0, green,
+ 0.5, green,
+ 0.5, blue,
+ 1.0, blue ]
+
+ # more hard stops, non-uniform distribution
+ - type: gradient
+ bounds: 100 250 500 10
+ start: 200 0
+ end: 300 0.001
+ repeat: false
+ stops: [0.0, green,
+ 0.25, green,
+ 0.25, red,
+ 0.75, red,
+ 0.75, blue,
+ 1.0, blue ]
+
+ # repeat the hard stops
+ - type: gradient
+ bounds: 100 300 500 10
+ start: 200 0
+ end: 300 0.001
+ repeat: true
+ stops: [0.0, green,
+ 0.25, green,
+ 0.25, red,
+ 0.75, red,
+ 0.75, blue,
+ 1.0, blue ]
+
+ # same but start doesn't line up with 0
+ - type: gradient
+ bounds: 100 350 500 10
+ start: 175 0
+ end: 275 0.001
+ repeat: true
+ stops: [0.0, green,
+ 0.25, green,
+ 0.25, red,
+ 0.75, red,
+ 0.75, blue,
+ 1.0, blue ]
+
+ # the entire gradient from 0 to 1 is
+ # "offscreen", we're only seeing its
+ # repeats. the gradient is 100 wide
+ # and ends at -75, so the first
+ # three-quarters of it would be hidden,
+ # that is, it should start with blue.
+ - type: gradient
+ bounds: 100 400 500 10
+ start: -175 0
+ end: -75 0.001
+ repeat: true
+ stops: [0.0, green,
+ 0.25, green,
+ 0.25, red,
+ 0.75, red,
+ 0.75, blue,
+ 1.0, blue ]
+
+ # same but over on the right
+ - type: gradient
+ bounds: 100 450 500 10
+ start: 575 0
+ end: 675 0.001
+ repeat: true
+ stops: [0.0, green,
+ 0.25, green,
+ 0.25, red,
+ 0.75, red,
+ 0.75, blue,
+ 1.0, blue ]
+
+ # a repeat, but not really because only part
+ # of the gradient is visible
+ - type: gradient
+ bounds: 100 500 500 10
+ start: -50 0
+ end: 550 0.001
+ repeat: true
+ stops: [0.0, green,
+ 0.25, green,
+ 0.25, red,
+ 0.75, red,
+ 0.75, blue,
+ 1.0, blue ]
diff --git a/gfx/wr/wrench/reftests/gradient/linear-adjust-tile-size-ref.yaml b/gfx/wr/wrench/reftests/gradient/linear-adjust-tile-size-ref.yaml
new file mode 100644
index 0000000000..c9145fc5e6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-adjust-tile-size-ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 15.47998046875 18 684.39990234375 643.199951171875
+ start: 10.286011695861816 653.47998046875
+ end: 143.13165283203125 520.7279663085938
+ stops: [0.0, red, 1.0, blue]
+ repeat: true
diff --git a/gfx/wr/wrench/reftests/gradient/linear-adjust-tile-size.yaml b/gfx/wr/wrench/reftests/gradient/linear-adjust-tile-size.yaml
new file mode 100644
index 0000000000..4d78b9b99e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-adjust-tile-size.yaml
@@ -0,0 +1,10 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 15.47998046875 18 684.39990234375 643.199951171875
+ tile-size: 684.4000244140625 643.2000122070313
+ start: 10.286011695861816 653.47998046875
+ end: 143.13165283203125 520.7279663085938
+ stops: [0.0, red, 1.0, blue]
+ repeat: true
diff --git a/gfx/wr/wrench/reftests/gradient/linear-aligned-border-radius.png b/gfx/wr/wrench/reftests/gradient/linear-aligned-border-radius.png
new file mode 100644
index 0000000000..450e0ac56a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-aligned-border-radius.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/gradient/linear-aligned-border-radius.yaml b/gfx/wr/wrench/reftests/gradient/linear-aligned-border-radius.yaml
new file mode 100644
index 0000000000..13e2b324a8
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-aligned-border-radius.yaml
@@ -0,0 +1,46 @@
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [20, 20, 100, 100]
+ radius: 32
+ - type: gradient
+ bounds: 20 20 100 100
+ start: 50 0
+ end: 50 100
+ stops: [0.0, red, 1.0, yellow]
+ clip-chain: [2]
+
+ - type: rect
+ bounds: [130, 10, 120, 120]
+ color: blue
+
+ - type: clip
+ id: 3
+ complex:
+ - rect: [140, 20, 100, 100]
+ radius: 32
+ - type: gradient
+ bounds: 140 20 100 100
+ start: 50 0
+ end: 50 100
+ stops: [0.0, red, 1.0, yellow]
+ clip-chain: [3]
+
+ - type: rect
+ bounds: [260, 10, 120, 120]
+ color: black
+
+ - type: clip
+ id: 4
+ complex:
+ - rect: [270, 20, 100, 100]
+ radius: 32
+ - type: gradient
+ bounds: 270 20 100 100
+ start: 50 0
+ end: 50 100
+ stops: [0.0, red, 1.0, yellow]
+ clip-chain: [4]
diff --git a/gfx/wr/wrench/reftests/gradient/linear-aligned-clip-ref.yaml b/gfx/wr/wrench/reftests/gradient/linear-aligned-clip-ref.yaml
new file mode 100644
index 0000000000..08a395dc71
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-aligned-clip-ref.yaml
@@ -0,0 +1,16 @@
+---
+root:
+ items:
+ # an aligned gradient from [0, 400]
+ - type: gradient
+ bounds: 0 0 200 400
+ start: 100 0
+ end: 100 400
+ stops: [0.0, green, 1.0, blue]
+ # manual clipping
+ - type: rect
+ bounds: 0 0 200 100
+ color: white
+ - type: rect
+ bounds: 0 300 200 100
+ color: white
diff --git a/gfx/wr/wrench/reftests/gradient/linear-aligned-clip.yaml b/gfx/wr/wrench/reftests/gradient/linear-aligned-clip.yaml
new file mode 100644
index 0000000000..fad030724f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-aligned-clip.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ # an aligned gradient from [0, 400] and clipped to [100, 300]
+ - type: gradient
+ bounds: 0 100 200 200
+ start: 100 -100
+ end: 100 300
+ stops: [0.0, green, 1.0, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/linear-backdrop-ref.yaml b/gfx/wr/wrench/reftests/gradient/linear-backdrop-ref.yaml
new file mode 100644
index 0000000000..7cc02b573c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-backdrop-ref.yaml
@@ -0,0 +1,11 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: 0 0 800 450
+ color: red
+ - type: gradient
+ bounds: 0 0 800 450
+ start: 100 100
+ end: 700 350
+ stops: [ 0.0, [255, 255, 255, 1], 1.0, [0,0,0,1] ]
diff --git a/gfx/wr/wrench/reftests/gradient/linear-backdrop-with-spacing-ref.yaml b/gfx/wr/wrench/reftests/gradient/linear-backdrop-with-spacing-ref.yaml
new file mode 100644
index 0000000000..e72d156433
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-backdrop-with-spacing-ref.yaml
@@ -0,0 +1,14 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: 0 0 800 450
+ color: red
+ - type: gradient
+ bounds: 0 0 800 450
+ start: 20 20
+ end: 80 50
+ stops: [ 0.0, [255, 255, 255, 1], 1.0, [0,0,0,1] ]
+ tile-size: 100 100
+ tile-spacing: 20 20
+
diff --git a/gfx/wr/wrench/reftests/gradient/linear-backdrop-with-spacing.yaml b/gfx/wr/wrench/reftests/gradient/linear-backdrop-with-spacing.yaml
new file mode 100644
index 0000000000..dd85ffdae5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-backdrop-with-spacing.yaml
@@ -0,0 +1,18 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: 0 0 800 450
+ color: red
+ - type: "scroll-frame"
+ bounds: 0 0 800 450
+ clip-rect: 0 0 800 450
+ id: 2
+ - type: gradient
+ bounds: 0 0 800 450
+ spatial-id: 2
+ start: 20 20
+ end: 80 50
+ stops: [ 0.0, [255, 255, 255, 1], 1.0, [0,0,0,1] ]
+ tile-size: 100 100
+ tile-spacing: 20 20
diff --git a/gfx/wr/wrench/reftests/gradient/linear-backdrop.yaml b/gfx/wr/wrench/reftests/gradient/linear-backdrop.yaml
new file mode 100644
index 0000000000..7b351788d4
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-backdrop.yaml
@@ -0,0 +1,16 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: 0 0 800 450
+ color: red
+ - type: "scroll-frame"
+ bounds: 0 0 800 450
+ clip-rect: 0 0 800 450
+ id: 2
+ - type: gradient
+ bounds: 0 0 800 450
+ spatial-id: 2
+ start: 100 100
+ end: 700 350
+ stops: [ 0.0, [255, 255, 255, 1], 1.0, [0,0,0,1] ]
diff --git a/gfx/wr/wrench/reftests/gradient/linear-bug-1703141.yaml b/gfx/wr/wrench/reftests/gradient/linear-bug-1703141.yaml
new file mode 100644
index 0000000000..9db333277e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-bug-1703141.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: -8396 465 9136 3
+ tile-size: 10498.667 3
+ start: 9448.5 1.5
+ end: 10498.333 1.5
+ repeat: true
+ stops: [0.0, [0,0,0,0], 0.5, [0,0,0,0],
+ 0.5, red, 0.75, red,
+ 0.75, [0,0,0,0], 1.0, [0,0,0,0]]
diff --git a/gfx/wr/wrench/reftests/gradient/linear-clamp-1-ref.yaml b/gfx/wr/wrench/reftests/gradient/linear-clamp-1-ref.yaml
new file mode 100644
index 0000000000..81c366d858
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-clamp-1-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 0 100
+ end: 200 100
+ stops: [0.0, blue, 0.5, blue, 0.5, red, 1.0, red]
diff --git a/gfx/wr/wrench/reftests/gradient/linear-clamp-1a.yaml b/gfx/wr/wrench/reftests/gradient/linear-clamp-1a.yaml
new file mode 100644
index 0000000000..b83963a37a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-clamp-1a.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 0 100
+ end: 100 100
+ stops: [0.0, blue, 1.0, blue, 1.0, red]
diff --git a/gfx/wr/wrench/reftests/gradient/linear-clamp-1b.yaml b/gfx/wr/wrench/reftests/gradient/linear-clamp-1b.yaml
new file mode 100644
index 0000000000..ffe3391999
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-clamp-1b.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 100 100
+ end: 200 100
+ stops: [0.0, blue, 0.0, red, 1.0, red]
diff --git a/gfx/wr/wrench/reftests/gradient/linear-clamp-2-ref.yaml b/gfx/wr/wrench/reftests/gradient/linear-clamp-2-ref.yaml
new file mode 100644
index 0000000000..8eb475d0a5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-clamp-2-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 0 100
+ end: 200 100
+ stops: [0.0, blue, 0.25, blue, 0.25, green, 0.75, green, 0.75, red, 1.0, red]
diff --git a/gfx/wr/wrench/reftests/gradient/linear-clamp-2.yaml b/gfx/wr/wrench/reftests/gradient/linear-clamp-2.yaml
new file mode 100644
index 0000000000..48428b974a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-clamp-2.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 50 100
+ end: 150 100
+ stops: [0.0, blue, 0.0, green, 1.0, green, 1.0, red]
diff --git a/gfx/wr/wrench/reftests/gradient/linear-double.yaml b/gfx/wr/wrench/reftests/gradient/linear-double.yaml
new file mode 100644
index 0000000000..c9e4a44d84
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-double.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 300 50 200 200
+ start: 0 100
+ end: 200 100
+ stops: [0.0, blue, 1.0, red]
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 0 100
+ end: 200 100
+ stops: [0.0, green, 1.0, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/linear-far-endpoints.yaml b/gfx/wr/wrench/reftests/gradient/linear-far-endpoints.yaml
new file mode 100644
index 0000000000..45bb52debc
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-far-endpoints.yaml
@@ -0,0 +1,10 @@
+# Axis-aligned linear gradient with very far endpoints. It goes through the gradient
+# decomposition path which should not choke on overflow or casting failure.
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 500 500
+ start: -19958788096 0
+ end: 19958788096 0
+ stops: [0.0, red, 1.0, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/linear-hard-stop-ref.png b/gfx/wr/wrench/reftests/gradient/linear-hard-stop-ref.png
new file mode 100644
index 0000000000..4feb5e4993
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-hard-stop-ref.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/gradient/linear-hard-stop-repeat-large-ref.yaml b/gfx/wr/wrench/reftests/gradient/linear-hard-stop-repeat-large-ref.yaml
new file mode 100644
index 0000000000..9287f91dd4
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-hard-stop-repeat-large-ref.yaml
@@ -0,0 +1,11 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 0 0 800 200
+ tile-size: 200 200
+ clip-rect: 0 0 800 200
+ start: 0 0
+ end: 20 0
+ stops: [0.0, black, 0.1, black, 0.2, white, 1.0, white]
+ repeat: true
diff --git a/gfx/wr/wrench/reftests/gradient/linear-hard-stop-repeat-large.yaml b/gfx/wr/wrench/reftests/gradient/linear-hard-stop-repeat-large.yaml
new file mode 100644
index 0000000000..1abf946263
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-hard-stop-repeat-large.yaml
@@ -0,0 +1,14 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 0 0 4000 200
+ tile-size: 200 200
+ clip-rect: 0 0 4000 200
+ start: 0 0
+ end: 20 0
+ stops: [0.0, black, 0.1, black, 0.2, white, 1.0, white]
+ repeat: true
+ - type: rect
+ bounds: 800 0 4000 200
+ color: white
diff --git a/gfx/wr/wrench/reftests/gradient/linear-hard-stop.yaml b/gfx/wr/wrench/reftests/gradient/linear-hard-stop.yaml
new file mode 100644
index 0000000000..b9249e7f2a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-hard-stop.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 0 0
+ end: 0 100
+ stops: [0.0, blue, 0.5 , red, 0.5, green]
diff --git a/gfx/wr/wrench/reftests/gradient/linear-large-ref.yaml b/gfx/wr/wrench/reftests/gradient/linear-large-ref.yaml
new file mode 100644
index 0000000000..472f04fd17
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-large-ref.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: 50 50 2000 300
+ color: blue
+
+ - type: gradient
+ bounds: 50 50 400 300
+ start: 0 0
+ end: 100 20
+ stops: [0.0, red, 1.0, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/linear-large.yaml b/gfx/wr/wrench/reftests/gradient/linear-large.yaml
new file mode 100644
index 0000000000..cf9c50edd1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-large.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 2000 300
+ start: 0 0
+ end: 100 20
+ stops: [0.0, red, 1.0, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/linear-nan.yaml b/gfx/wr/wrench/reftests/gradient/linear-nan.yaml
new file mode 100644
index 0000000000..687a06d372
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-nan.yaml
@@ -0,0 +1,213 @@
+---
+root:
+ items:
+ # Small-ish gradients
+ - type: gradient
+ bounds: 50 50 NaN 200
+ start: 0 100
+ end: 200 100
+ stops: [0.0, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ 0.5, blue, 0.75, blue,
+ 0.75, black, 1.0, black]
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 0 100
+ end: NaN 100
+ stops: [0.0, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ 0.5, blue, 0.75, blue,
+ 0.75, black, 1.0, black]
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 0 NaN
+ end: 200 100
+ stops: [0.0, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ 0.5, blue, 0.75, blue,
+ 0.75, black, 1.0, black]
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 0 100
+ end: 200 100
+ stops: [0.0, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ NaN, blue, 0.75, blue,
+ 0.75, black, 1.0, black]
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 0 100
+ end: 200 100
+ stops: [0.0, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ 0.0, blue, 0.75, blue,
+ 0.75, black, NaN, black]
+ - type: gradient
+ bounds: 50 50 200 200
+ clip-rect: 50 50 150 NaN
+ start: 0 100
+ end: 200 100
+ stops: [0.0, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ 0.0, blue, 0.75, blue,
+ 0.75, black, 1.0, black]
+ - type: gradient
+ bounds: 50 50 200 200
+ tile-size: NaN 200
+ start: 0 100
+ end: 200 100
+ stops: [0.0, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ 0.0, blue, 0.75, blue,
+ 0.75, black, 1.0, black]
+
+ # Large-ish gradients
+ - type: gradient
+ bounds: 50 50 NaN 500
+ start: 0 100
+ end: 500 100
+ stops: [0.0, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ 0.5, blue, 0.75, blue,
+ 0.75, black, 1.0, black]
+ - type: gradient
+ bounds: 50 50 500 500
+ start: 0 100
+ end: NaN 100
+ stops: [0.0, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ 0.5, blue, 0.75, blue,
+ 0.75, black, 1.0, black]
+ - type: gradient
+ bounds: 50 50 500 500
+ start: 0 NaN
+ end: 500 100
+ stops: [0.0, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ 0.5, blue, 0.75, blue,
+ 0.75, black, 1.0, black]
+ - type: gradient
+ bounds: 50 50 500 500
+ start: 0 100
+ end: 500 100
+ stops: [0.0, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ NaN, blue, 0.75, blue,
+ 0.75, black, 1.0, black]
+ - type: gradient
+ bounds: 50 50 500 500
+ start: 0 100
+ end: 500 100
+ stops: [0.0, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ 0.0, blue, 0.75, blue,
+ 0.75, black, NaN, black]
+ - type: gradient
+ bounds: 50 50 500 500
+ clip-rect: 50 50 150 NaN
+ start: 0 100
+ end: 500 100
+ stops: [0.0, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ 0.0, blue, 0.75, blue,
+ 0.75, black, 1.0, black]
+ - type: gradient
+ bounds: 50 50 500 500
+ tile-size: NaN 500
+ start: 0 100
+ end: 500 100
+ stops: [0.0, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ 0.0, blue, 0.75, blue,
+ 0.75, black, 1.0, black]
+
+ # Very large gradients
+ - type: gradient
+ bounds: 50 50 10000 10000
+ start: 0 100
+ end: NaN 100
+ stops: [0.0, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ 0.5, blue, 0.75, blue,
+ 0.75, black, 1.0, black]
+ - type: gradient
+ bounds: 50 50 10000 10000
+ start: 0 NaN
+ end: 10000 100
+ stops: [0.0, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ 0.5, blue, 0.75, blue,
+ 0.75, black, 1.0, black]
+ - type: gradient
+ bounds: 50 50 10000 10000
+ start: 0 100
+ end: 10000 100
+ stops: [0.0, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ NaN, blue, 0.75, blue,
+ 0.75, black, 1.0, black]
+ - type: gradient
+ bounds: 50 50 10000 10000
+ start: 0 100
+ end: 10000 100
+ stops: [0.0, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ 0.0, blue, 0.75, blue,
+ 0.75, black, NaN, black]
+ - type: gradient
+ bounds: 50 50 10000 10000
+ clip-rect: 50 50 150 NaN
+ start: 0 100
+ end: 10000 100
+ stops: [0.0, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ 0.0, blue, 0.75, blue,
+ 0.75, black, 1.0, black]
+ - type: gradient
+ bounds: 50 50 10000 10000
+ tile-size: NaN 10000
+ start: 0 100
+ end: 10000 100
+ stops: [0.0, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ 0.0, blue, 0.75, blue,
+ 0.75, black, 1.0, black]
+
+ # Not axis-aligned
+ - type: gradient
+ bounds: 50 50 200 200
+ tile-size: NaN 200
+ start: 0 0
+ end: 200 100
+ stops: [0.0, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ 0.0, blue, 0.75, blue,
+ 0.75, black, 1.0, black]
+ - type: gradient
+ bounds: NaN 50 200 200
+ tile-size: 100 100
+ start: 0 0
+ end: 200 100
+ stops: [0.0, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ 0.0, blue, 0.75, blue,
+ 0.75, black, 1.0, black]
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 0.0 0
+ end: 200 100
+ stops: [NaN, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ 0.0, blue, NaN, blue,
+ 0.75, black, 1.0, black]
+
+ # Post-apocalyptic gradient
+ - type: gradient
+ bounds: NaN NaN NaN NaN
+ tile-size: NaN NaN
+ start: NaN NaN
+ end: NaN NaN
+ stops: [NaN, red, NaN, red,
+ NaN, green, NaN, green,
+ NaN, blue, NaN, blue,
+ NaN, black, NaN, black]
diff --git a/gfx/wr/wrench/reftests/gradient/linear-ref.png b/gfx/wr/wrench/reftests/gradient/linear-ref.png
new file mode 100644
index 0000000000..b157ee4bef
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-ref.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/gradient/linear-ref.yaml b/gfx/wr/wrench/reftests/gradient/linear-ref.yaml
new file mode 100644
index 0000000000..83d7248166
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-ref.yaml
@@ -0,0 +1,18 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: 50 50 200 200
+ items:
+ - type: rect
+ bounds: 0 0 50 200
+ color: red
+ - type: rect
+ bounds: 50 0 50 200
+ color: green
+ - type: rect
+ bounds: 100 0 50 200
+ color: blue
+ - type: rect
+ bounds: 150 0 50 200
+ color: black
diff --git a/gfx/wr/wrench/reftests/gradient/linear-repeat-clip-ref.yaml b/gfx/wr/wrench/reftests/gradient/linear-repeat-clip-ref.yaml
new file mode 100644
index 0000000000..d8fd0b3b32
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-repeat-clip-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 0 0 500 500
+ start: 0 -20
+ end: 0 520
+ stops: [0.0, green, 0.5, blue, 1.0, red]
diff --git a/gfx/wr/wrench/reftests/gradient/linear-repeat-clip.yaml b/gfx/wr/wrench/reftests/gradient/linear-repeat-clip.yaml
new file mode 100644
index 0000000000..9c84edd714
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-repeat-clip.yaml
@@ -0,0 +1,13 @@
+# This test has a gradient primitive that is much larger than its local clip
+# and some tiling that can be optimized away. The combination of clipping
+# and stretching optimizations used to cause produce the wrong clip.
+---
+root:
+ items:
+ - type: gradient
+ bounds: -500 0 2000 500
+ tile-size: 100 500
+ clip-rect: 0 0 500 500
+ start: 0 -20
+ end: 0 520
+ stops: [0.0, green, 0.5, blue, 1.0, red]
diff --git a/gfx/wr/wrench/reftests/gradient/linear-reverse-2-ref.yaml b/gfx/wr/wrench/reftests/gradient/linear-reverse-2-ref.yaml
new file mode 100644
index 0000000000..b85d303503
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-reverse-2-ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 50 100
+ end: 150 100
+ stops: [0.0, green, 0.5, blue,
+ 0.5, blue, 1.0, red]
diff --git a/gfx/wr/wrench/reftests/gradient/linear-reverse-2.yaml b/gfx/wr/wrench/reftests/gradient/linear-reverse-2.yaml
new file mode 100644
index 0000000000..c74c6b88f9
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-reverse-2.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 150 100
+ end: 50 100
+ stops: [0.0, red, 0.5, blue,
+ 0.5, blue, 1.0, green]
diff --git a/gfx/wr/wrench/reftests/gradient/linear-reverse-3-ref.yaml b/gfx/wr/wrench/reftests/gradient/linear-reverse-3-ref.yaml
new file mode 100644
index 0000000000..088977f50f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-reverse-3-ref.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 100 100
+ start: 0 0
+ end: 110 0
+ stops: [0.0, white, 1.0, black]
+ - type: gradient
+ bounds: 150 50 100 100
+ start: 0 0
+ end: 110 0
+ stops: [0.0, black, 1.0, white]
diff --git a/gfx/wr/wrench/reftests/gradient/linear-reverse-3.yaml b/gfx/wr/wrench/reftests/gradient/linear-reverse-3.yaml
new file mode 100644
index 0000000000..87b675c8ad
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-reverse-3.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 100 100
+ start: 0 0
+ end: 110 0
+ stops: [0.0, white, 1.0, black]
+ - type: gradient
+ bounds: 150 50 100 100
+ start: 110 0
+ end: 0 0
+ stops: [0.0, white, 1.0, black]
diff --git a/gfx/wr/wrench/reftests/gradient/linear-reverse.yaml b/gfx/wr/wrench/reftests/gradient/linear-reverse.yaml
new file mode 100644
index 0000000000..ecae199e88
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-reverse.yaml
@@ -0,0 +1,11 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 200 100
+ end: 000 100
+ stops: [0.0, black, 0.25, black,
+ 0.25, blue, 0.5, blue,
+ 0.5, green, 0.75, green,
+ 0.75, red, 1.0, red]
diff --git a/gfx/wr/wrench/reftests/gradient/linear-stops-ref.png b/gfx/wr/wrench/reftests/gradient/linear-stops-ref.png
new file mode 100644
index 0000000000..844b244002
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-stops-ref.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/gradient/linear-stops.yaml b/gfx/wr/wrench/reftests/gradient/linear-stops.yaml
new file mode 100644
index 0000000000..f5b8bfbc99
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear-stops.yaml
@@ -0,0 +1,7 @@
+root:
+ items:
+ - type: gradient
+ bounds: [0, 0, 200, 200]
+ start: 0 100
+ end: 200 100
+ stops: [0.0, red, 0.5, green, 1.0, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/linear.yaml b/gfx/wr/wrench/reftests/gradient/linear.yaml
new file mode 100644
index 0000000000..53d8d512cd
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/linear.yaml
@@ -0,0 +1,11 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 0 100
+ end: 200 100
+ stops: [0.0, red, 0.25, red,
+ 0.25, green, 0.5, green,
+ 0.5, blue, 0.75, blue,
+ 0.75, black, 1.0, black]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-conic-1-ref.yaml b/gfx/wr/wrench/reftests/gradient/norm-conic-1-ref.yaml
new file mode 100644
index 0000000000..a3bb761463
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-conic-1-ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: conic-gradient
+ bounds: 50 50 200 200
+ angle: 0.0
+ center: 100 100
+ stops: [0.0, green, 0.5, green,
+ 0.5, blue, 1.0, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-conic-1.yaml b/gfx/wr/wrench/reftests/gradient/norm-conic-1.yaml
new file mode 100644
index 0000000000..8bf6b734c2
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-conic-1.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: conic-gradient
+ bounds: 50 50 200 200
+ angle: 0.0
+ center: 100 100
+ stops: [0.25, green, 0.5, green,
+ 0.5, blue, 0.75, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-conic-2-ref.yaml b/gfx/wr/wrench/reftests/gradient/norm-conic-2-ref.yaml
new file mode 100644
index 0000000000..a3bb761463
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-conic-2-ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: conic-gradient
+ bounds: 50 50 200 200
+ angle: 0.0
+ center: 100 100
+ stops: [0.0, green, 0.5, green,
+ 0.5, blue, 1.0, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-conic-2.yaml b/gfx/wr/wrench/reftests/gradient/norm-conic-2.yaml
new file mode 100644
index 0000000000..ba9b174b51
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-conic-2.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: conic-gradient
+ bounds: 50 50 200 200
+ angle: 0.0
+ center: 100 100
+ stops: [0.5, green,
+ 0.5, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-conic-3-ref.yaml b/gfx/wr/wrench/reftests/gradient/norm-conic-3-ref.yaml
new file mode 100644
index 0000000000..d5403c498f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-conic-3-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: conic-gradient
+ bounds: 50 50 200 200
+ angle: 0.0
+ center: 100 100
+ stops: [0.0, blue, 1.0, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-conic-3.yaml b/gfx/wr/wrench/reftests/gradient/norm-conic-3.yaml
new file mode 100644
index 0000000000..962ff4e7aa
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-conic-3.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: conic-gradient
+ bounds: 50 50 200 200
+ angle: 0.0
+ center: 100 100
+ stops: [-0.5, green,
+ -0.5, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-conic-4-ref.yaml b/gfx/wr/wrench/reftests/gradient/norm-conic-4-ref.yaml
new file mode 100644
index 0000000000..6c0b6e508f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-conic-4-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: conic-gradient
+ bounds: 50 50 200 200
+ angle: 0.0
+ center: 100 100
+ stops: [0.0, green, 1.0, green]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-conic-4.yaml b/gfx/wr/wrench/reftests/gradient/norm-conic-4.yaml
new file mode 100644
index 0000000000..6564d84721
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-conic-4.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: conic-gradient
+ bounds: 50 50 200 200
+ angle: 0.0
+ center: 100 100
+ stops: [1.5, green,
+ 1.5, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-conic-degenerate-ref.yaml b/gfx/wr/wrench/reftests/gradient/norm-conic-degenerate-ref.yaml
new file mode 100644
index 0000000000..1ba6bd2f9e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-conic-degenerate-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: conic-gradient
+ bounds: 50 50 300 300
+ angle: 0.0
+ center: 150 150
+ stops: [0.0, red, 1.0, red]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-conic-degenerate.yaml b/gfx/wr/wrench/reftests/gradient/norm-conic-degenerate.yaml
new file mode 100644
index 0000000000..be96200722
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-conic-degenerate.yaml
@@ -0,0 +1,14 @@
+# see: https://www.w3.org/TR/2012/CR-css3-images-20120417/#repeating-gradients
+# the spec says that repeating gradients with color stops in the same offset
+# must render as a solid rect with color equal to the average color of the
+# gradient. Gecko and Blink seem to draw it with color equal to the last stop
+# so that is the behavior tested here
+---
+root:
+ items:
+ - type: conic-gradient
+ bounds: 50 50 300 300
+ angle: 0.0
+ center: 150 150
+ stops: [0.5, blue, 0.5, red]
+ repeat: true
diff --git a/gfx/wr/wrench/reftests/gradient/norm-linear-1-ref.yaml b/gfx/wr/wrench/reftests/gradient/norm-linear-1-ref.yaml
new file mode 100644
index 0000000000..5b8a0b317a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-linear-1-ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 0 100
+ end: 200 100
+ stops: [0.0, green, 0.5, green,
+ 0.5, blue, 1.0, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-linear-1.yaml b/gfx/wr/wrench/reftests/gradient/norm-linear-1.yaml
new file mode 100644
index 0000000000..d79b8608b1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-linear-1.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 0 100
+ end: 200 100
+ stops: [0.25, green, 0.5, green,
+ 0.5, blue, 0.75, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-linear-2-ref.yaml b/gfx/wr/wrench/reftests/gradient/norm-linear-2-ref.yaml
new file mode 100644
index 0000000000..5b8a0b317a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-linear-2-ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 0 100
+ end: 200 100
+ stops: [0.0, green, 0.5, green,
+ 0.5, blue, 1.0, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-linear-2.yaml b/gfx/wr/wrench/reftests/gradient/norm-linear-2.yaml
new file mode 100644
index 0000000000..aa3aa2afe1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-linear-2.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 0 100
+ end: 200 100
+ stops: [0.5, green,
+ 0.5, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-linear-3-ref.yaml b/gfx/wr/wrench/reftests/gradient/norm-linear-3-ref.yaml
new file mode 100644
index 0000000000..62bfda97e6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-linear-3-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 0 100
+ end: 200 100
+ stops: [0.0, blue, 1.0, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-linear-3.yaml b/gfx/wr/wrench/reftests/gradient/norm-linear-3.yaml
new file mode 100644
index 0000000000..cc18371592
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-linear-3.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 0 100
+ end: 200 100
+ stops: [-0.5, green,
+ -0.5, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-linear-4-ref.yaml b/gfx/wr/wrench/reftests/gradient/norm-linear-4-ref.yaml
new file mode 100644
index 0000000000..bcd84d8294
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-linear-4-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 0 100
+ end: 200 100
+ stops: [0.0, green, 1.0, green]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-linear-4.yaml b/gfx/wr/wrench/reftests/gradient/norm-linear-4.yaml
new file mode 100644
index 0000000000..df622bf299
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-linear-4.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 0 100
+ end: 200 100
+ stops: [1.5, green,
+ 1.5, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-linear-degenerate-ref.yaml b/gfx/wr/wrench/reftests/gradient/norm-linear-degenerate-ref.yaml
new file mode 100644
index 0000000000..ae61c486cb
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-linear-degenerate-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 300 300
+ start: 0 150
+ end: 300 150
+ stops: [0.0, red, 1.0, red]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-linear-degenerate.yaml b/gfx/wr/wrench/reftests/gradient/norm-linear-degenerate.yaml
new file mode 100644
index 0000000000..89b431df32
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-linear-degenerate.yaml
@@ -0,0 +1,14 @@
+# see: https://www.w3.org/TR/2012/CR-css3-images-20120417/#repeating-gradients
+# the spec says that repeating gradients with color stops in the same offset
+# must render as a solid rect with color equal to the average color of the
+# gradient. Gecko and Blink seem to draw it with color equal to the last stop
+# so that is the behavior tested here
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 300 300
+ start: 0 150
+ end: 300 150
+ stops: [0.5, blue, 0.5, red]
+ repeat: true
diff --git a/gfx/wr/wrench/reftests/gradient/norm-radial-1-ref.yaml b/gfx/wr/wrench/reftests/gradient/norm-radial-1-ref.yaml
new file mode 100644
index 0000000000..a67b51ecf8
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-radial-1-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 50 50 300 300
+ center: 150 150
+ radius: 200 200
+ stops: [0.0, red, 0.5, red, 0.5, blue, 1.0, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-radial-1.yaml b/gfx/wr/wrench/reftests/gradient/norm-radial-1.yaml
new file mode 100644
index 0000000000..7ee6caeafa
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-radial-1.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 50 50 300 300
+ center: 150 150
+ radius: 200 200
+ stops: [0.5, red, 0.5, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-radial-2-ref.yaml b/gfx/wr/wrench/reftests/gradient/norm-radial-2-ref.yaml
new file mode 100644
index 0000000000..adfc8d70d4
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-radial-2-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 50 50 300 300
+ center: 150 150
+ radius: 200 200
+ stops: [0.0, blue, 1.0, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-radial-2.yaml b/gfx/wr/wrench/reftests/gradient/norm-radial-2.yaml
new file mode 100644
index 0000000000..dac1b78361
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-radial-2.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 50 50 300 300
+ center: 150 150
+ radius: 200 200
+ stops: [-0.5, red, -0.5, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-radial-3-ref.yaml b/gfx/wr/wrench/reftests/gradient/norm-radial-3-ref.yaml
new file mode 100644
index 0000000000..d90d1ee4f8
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-radial-3-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 50 50 300 300
+ center: 150 150
+ radius: 200 200
+ stops: [0.0, red, 1.0, red]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-radial-3.yaml b/gfx/wr/wrench/reftests/gradient/norm-radial-3.yaml
new file mode 100644
index 0000000000..fbd5dc929f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-radial-3.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 50 50 300 300
+ center: 150 150
+ radius: 200 200
+ stops: [1.5, red, 1.5, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-radial-degenerate-ref.yaml b/gfx/wr/wrench/reftests/gradient/norm-radial-degenerate-ref.yaml
new file mode 100644
index 0000000000..afe59a770b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-radial-degenerate-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 50 50 300 300
+ center: 150 150
+ radius: 150 150
+ stops: [0.0, red, 1.0, red]
diff --git a/gfx/wr/wrench/reftests/gradient/norm-radial-degenerate.yaml b/gfx/wr/wrench/reftests/gradient/norm-radial-degenerate.yaml
new file mode 100644
index 0000000000..26d9935475
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/norm-radial-degenerate.yaml
@@ -0,0 +1,14 @@
+# see: https://www.w3.org/TR/2012/CR-css3-images-20120417/#repeating-gradients
+# the spec says that repeating gradients with color stops in the same offset
+# must render as a solid rect with color equal to the average color of the
+# gradient. Gecko and Blink seem to draw it with color equal to the last stop
+# so that is the behavior tested here
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 50 50 300 300
+ center: 150 150
+ radius: 150 150
+ stops: [0.5, blue, 0.5, red]
+ repeat: true
diff --git a/gfx/wr/wrench/reftests/gradient/premultiplied-aligned-2.png b/gfx/wr/wrench/reftests/gradient/premultiplied-aligned-2.png
new file mode 100644
index 0000000000..b27b9df586
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/premultiplied-aligned-2.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/gradient/premultiplied-aligned-2.yaml b/gfx/wr/wrench/reftests/gradient/premultiplied-aligned-2.yaml
new file mode 100644
index 0000000000..ea8acd5f26
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/premultiplied-aligned-2.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 0 100
+ end: 200 100
+ stops: [0.0, [255.0, 0.0, 0.0, 0.5], 0.5, [0.0, 255.0, 0.0, 0.5], 1.0, [0.0, 0.0, 255.0, 0.5]]
diff --git a/gfx/wr/wrench/reftests/gradient/premultiplied-aligned.png b/gfx/wr/wrench/reftests/gradient/premultiplied-aligned.png
new file mode 100644
index 0000000000..35fad64b1a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/premultiplied-aligned.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/gradient/premultiplied-aligned.yaml b/gfx/wr/wrench/reftests/gradient/premultiplied-aligned.yaml
new file mode 100644
index 0000000000..a709f845ea
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/premultiplied-aligned.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 0 100
+ end: 200 100
+ stops: [0.0, red, 0.5, [0.0, 0.0, 0.0, 0.0], 1.0, green]
diff --git a/gfx/wr/wrench/reftests/gradient/premultiplied-angle-2.png b/gfx/wr/wrench/reftests/gradient/premultiplied-angle-2.png
new file mode 100644
index 0000000000..ea7a5cf7ef
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/premultiplied-angle-2.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/gradient/premultiplied-angle-2.yaml b/gfx/wr/wrench/reftests/gradient/premultiplied-angle-2.yaml
new file mode 100644
index 0000000000..35546b0cd2
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/premultiplied-angle-2.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 0 0
+ end: 200 200
+ stops: [0.0, [255.0, 0.0, 0.0, 0.5], 0.5, [0.0, 255.0, 0.0, 0.5], 1.0, [0.0, 0.0, 255.0, 0.5]]
diff --git a/gfx/wr/wrench/reftests/gradient/premultiplied-angle.png b/gfx/wr/wrench/reftests/gradient/premultiplied-angle.png
new file mode 100644
index 0000000000..a37120ad7e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/premultiplied-angle.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/gradient/premultiplied-angle.yaml b/gfx/wr/wrench/reftests/gradient/premultiplied-angle.yaml
new file mode 100644
index 0000000000..b665e47150
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/premultiplied-angle.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 0 0
+ end: 200 200
+ stops: [0.0, red, 0.5, [0.0, 0.0, 0.0, 0.0], 1.0, green]
diff --git a/gfx/wr/wrench/reftests/gradient/premultiplied-conic-2.png b/gfx/wr/wrench/reftests/gradient/premultiplied-conic-2.png
new file mode 100644
index 0000000000..e589168621
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/premultiplied-conic-2.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/gradient/premultiplied-conic-2.yaml b/gfx/wr/wrench/reftests/gradient/premultiplied-conic-2.yaml
new file mode 100644
index 0000000000..dee4cec03b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/premultiplied-conic-2.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: conic-gradient
+ bounds: 50 50 200 200
+ angle: 0.0
+ center: 100 100
+ stops: [0.0, [255.0, 0.0, 0.0, 0.5], 0.5, [0.0, 255.0, 0.0, 0.5], 1.0, [0.0, 0.0, 255.0, 0.5]]
diff --git a/gfx/wr/wrench/reftests/gradient/premultiplied-conic.png b/gfx/wr/wrench/reftests/gradient/premultiplied-conic.png
new file mode 100644
index 0000000000..678dc9c87f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/premultiplied-conic.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/gradient/premultiplied-conic.yaml b/gfx/wr/wrench/reftests/gradient/premultiplied-conic.yaml
new file mode 100644
index 0000000000..d4b442bca7
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/premultiplied-conic.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: conic-gradient
+ bounds: 50 50 200 200
+ angle: 0.0
+ center: 100 100
+ stops: [0.0, red, 0.5, [0.0, 0.0, 0.0, 0.0], 1.0, green]
diff --git a/gfx/wr/wrench/reftests/gradient/premultiplied-radial-2.png b/gfx/wr/wrench/reftests/gradient/premultiplied-radial-2.png
new file mode 100644
index 0000000000..cfd7ca2aaa
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/premultiplied-radial-2.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/gradient/premultiplied-radial-2.yaml b/gfx/wr/wrench/reftests/gradient/premultiplied-radial-2.yaml
new file mode 100644
index 0000000000..3e682328d1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/premultiplied-radial-2.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 50 50 200 200
+ center: 100 100
+ radius: 100 100
+ stops: [0.0, [255.0, 0.0, 0.0, 0.5], 0.5, [0.0, 255.0, 0.0, 0.5], 1.0, [0.0, 0.0, 255.0, 0.5]]
diff --git a/gfx/wr/wrench/reftests/gradient/premultiplied-radial.png b/gfx/wr/wrench/reftests/gradient/premultiplied-radial.png
new file mode 100644
index 0000000000..0b2b2c2f21
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/premultiplied-radial.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/gradient/premultiplied-radial.yaml b/gfx/wr/wrench/reftests/gradient/premultiplied-radial.yaml
new file mode 100644
index 0000000000..dc3750abd4
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/premultiplied-radial.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 50 50 200 200
+ center: 100 100
+ radius: 100 100
+ stops: [0.0, red, 0.5, [0.0, 0.0, 0.0, 0.0], 1.0, green]
diff --git a/gfx/wr/wrench/reftests/gradient/radial-backdrop-ref.yaml b/gfx/wr/wrench/reftests/gradient/radial-backdrop-ref.yaml
new file mode 100644
index 0000000000..a878e2ac0a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/radial-backdrop-ref.yaml
@@ -0,0 +1,11 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: 0 0 800 450
+ color: red
+ - type: radial-gradient
+ bounds: 0 0 800 450
+ center: 400 225
+ radius: 200 200
+ stops: [ 0.0, [255, 255, 255, 1], 1.0, [0,0,0,1] ]
diff --git a/gfx/wr/wrench/reftests/gradient/radial-backdrop-with-spacing-ref.yaml b/gfx/wr/wrench/reftests/gradient/radial-backdrop-with-spacing-ref.yaml
new file mode 100644
index 0000000000..2aea254d3a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/radial-backdrop-with-spacing-ref.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: 0 0 800 450
+ color: red
+ - type: radial-gradient
+ bounds: 0 0 800 450
+ center: 50 50
+ radius: 60 60
+ stops: [ 0.0, [255, 255, 255, 1], 1.0, [0,0,0,1] ]
+ tile-size: 100 100
+ tile-spacing: 20 20
diff --git a/gfx/wr/wrench/reftests/gradient/radial-backdrop-with-spacing.yaml b/gfx/wr/wrench/reftests/gradient/radial-backdrop-with-spacing.yaml
new file mode 100644
index 0000000000..f8e2c14b1c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/radial-backdrop-with-spacing.yaml
@@ -0,0 +1,18 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: 0 0 800 450
+ color: red
+ - type: "scroll-frame"
+ bounds: 0 0 800 450
+ clip-rect: 0 0 800 450
+ id: 2
+ - type: radial-gradient
+ bounds: 0 0 800 450
+ spatial-id: 2
+ center: 50 50
+ radius: 60 60
+ stops: [ 0.0, [255, 255, 255, 1], 1.0, [0,0,0,1] ]
+ tile-size: 100 100
+ tile-spacing: 20 20
diff --git a/gfx/wr/wrench/reftests/gradient/radial-backdrop.yaml b/gfx/wr/wrench/reftests/gradient/radial-backdrop.yaml
new file mode 100644
index 0000000000..91ee3a6ba1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/radial-backdrop.yaml
@@ -0,0 +1,16 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: 0 0 800 450
+ color: red
+ - type: "scroll-frame"
+ bounds: 0 0 800 450
+ clip-rect: 0 0 800 450
+ id: 2
+ - type: radial-gradient
+ bounds: 0 0 800 450
+ spatial-id: 2
+ center: 400 225
+ radius: 200 200
+ stops: [ 0.0, [255, 255, 255, 1], 1.0, [0,0,0,1] ]
diff --git a/gfx/wr/wrench/reftests/gradient/radial-circle-ref.png b/gfx/wr/wrench/reftests/gradient/radial-circle-ref.png
new file mode 100644
index 0000000000..3f9a748a7e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/radial-circle-ref.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/gradient/radial-circle.yaml b/gfx/wr/wrench/reftests/gradient/radial-circle.yaml
new file mode 100644
index 0000000000..037da7eddc
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/radial-circle.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 50 50 300 300
+ center: 150 150
+ radius: 200 200
+ stops: [0, red, 1, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/radial-ellipse-ref.png b/gfx/wr/wrench/reftests/gradient/radial-ellipse-ref.png
new file mode 100644
index 0000000000..b4786cd387
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/radial-ellipse-ref.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/gradient/radial-ellipse.yaml b/gfx/wr/wrench/reftests/gradient/radial-ellipse.yaml
new file mode 100644
index 0000000000..7c733f7223
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/radial-ellipse.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 50 50 300 300
+ center: 150 150
+ radius: 100 200
+ stops: [0, red, 1, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/radial-large-ref.png b/gfx/wr/wrench/reftests/gradient/radial-large-ref.png
new file mode 100644
index 0000000000..444b876094
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/radial-large-ref.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/gradient/radial-large.yaml b/gfx/wr/wrench/reftests/gradient/radial-large.yaml
new file mode 100644
index 0000000000..c03adec6c1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/radial-large.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 50 50 2000 300
+ center: 1000 150
+ radius: 900 200
+ stops: [0, red, 1, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/radial-nan.yaml b/gfx/wr/wrench/reftests/gradient/radial-nan.yaml
new file mode 100644
index 0000000000..f8127a8c7a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/radial-nan.yaml
@@ -0,0 +1,35 @@
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 50 50 NaN 300
+ center: 150 150
+ radius: 200 200
+ stops: [0, red, 1, blue]
+ - type: radial-gradient
+ bounds: 50 50 300 300
+ center: 150 NaN
+ radius: 200 200
+ stops: [0, red, 1, blue]
+ - type: radial-gradient
+ bounds: 50 50 300 300
+ center: 150 150
+ radius: NaN 200
+ stops: [0, red, 1, blue]
+ - type: radial-gradient
+ bounds: 50 50 300 300
+ center: 150 150
+ radius: 200 200
+ stops: [0, red, NaN, blue]
+ - type: radial-gradient
+ bounds: 50 50 300 300
+ tile-size: 50 NaN
+ center: 150 150
+ radius: 200 200
+ stops: [0, red, 1, blue]
+ - type: radial-gradient
+ bounds: 50 50 300 300
+ clip-rect: 50 10 NaN 300
+ center: 150 150
+ radius: 200 200
+ stops: [0, red, 1, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/radial-optimized-2-ref.yaml b/gfx/wr/wrench/reftests/gradient/radial-optimized-2-ref.yaml
new file mode 100644
index 0000000000..4b900d93fa
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/radial-optimized-2-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 50 50 500 500
+ center: 150 150
+ radius: 80 160
+ stops: [0, red, 1, [0,0,0,0]]
diff --git a/gfx/wr/wrench/reftests/gradient/radial-optimized-2.yaml b/gfx/wr/wrench/reftests/gradient/radial-optimized-2.yaml
new file mode 100644
index 0000000000..3fac9042e2
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/radial-optimized-2.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 50 50 500 500
+ center: 150 150
+ radius: 20 40
+ stops: [0, red, 4, [0,0,0,0]]
diff --git a/gfx/wr/wrench/reftests/gradient/radial-optimized-ref.yaml b/gfx/wr/wrench/reftests/gradient/radial-optimized-ref.yaml
new file mode 100644
index 0000000000..380384ffe6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/radial-optimized-ref.yaml
@@ -0,0 +1,33 @@
+# Hand-roll decomposition that webrender would do for radial-optimized.yaml
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 160 160 20 30
+ center: 10 15
+ radius: 10 15
+ stops: [0, [255.0,0,0,0.7], 1, [0,0,255.0,0.7]]
+ - type: rect
+ bounds: 10 20 150 140
+ color: [0,0,255.0,0.7]
+ - type: rect
+ bounds: 160 20 20 140
+ color: [0,0,255.0,0.7]
+ - type: rect
+ bounds: 180 20 130 140
+ color: [0,0,255.0,0.7]
+ - type: rect
+ bounds: 10 160 150 30
+ color: [0,0,255.0,0.7]
+ - type: rect
+ bounds: 180 160 130 30
+ color: [0,0,255.0,0.7]
+ - type: rect
+ bounds: 10 190 150 160
+ color: [0,0,255.0,0.7]
+ - type: rect
+ bounds: 160 190 20 160
+ color: [0,0,255.0,0.7]
+ - type: rect
+ bounds: 180 190 130 160
+ color: [0,0,255.0,0.7]
diff --git a/gfx/wr/wrench/reftests/gradient/radial-optimized.yaml b/gfx/wr/wrench/reftests/gradient/radial-optimized.yaml
new file mode 100644
index 0000000000..cfc668bc66
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/radial-optimized.yaml
@@ -0,0 +1,12 @@
+# A small radial gradient in a large primitive.
+# Most of the primitive is the constant color of the last gradient stop,
+# and webrender will try to optimize these parts by drawing them with
+# solid color primitives.
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 10 20 300 330
+ center: 160 155
+ radius: 10 15
+ stops: [0, [255.0,0,0,0.7], 1, [0,0,255.0,0.7]]
diff --git a/gfx/wr/wrench/reftests/gradient/radial-tiling-optimized-ref.yaml b/gfx/wr/wrench/reftests/gradient/radial-tiling-optimized-ref.yaml
new file mode 100644
index 0000000000..b5b4957f9b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/radial-tiling-optimized-ref.yaml
@@ -0,0 +1,24 @@
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 50 40 20 20
+ center: 10 10
+ radius: 10 10
+ stops: [0, red, 1, blue, 1, [0,0,0,0]]
+ - type: radial-gradient
+ bounds: 450 40 20 20
+ center: 10 10
+ radius: 10 10
+ stops: [0, red, 1, blue, 1, [0,0,0,0]]
+ - type: radial-gradient
+ bounds: 450 340 20 20
+ center: 10 10
+ radius: 10 10
+ stops: [0, red, 1, blue, 1, [0,0,0,0]]
+ - type: radial-gradient
+ bounds: 50 340 20 20
+ center: 10 10
+ radius: 10 10
+ stops: [0, red, 1, blue, 1, [0,0,0,0]]
+
diff --git a/gfx/wr/wrench/reftests/gradient/radial-tiling-optimized.yaml b/gfx/wr/wrench/reftests/gradient/radial-tiling-optimized.yaml
new file mode 100644
index 0000000000..a92fd24479
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/radial-tiling-optimized.yaml
@@ -0,0 +1,13 @@
+# A small repeated radial gradient in a large primitive.
+# There is a lot of fully transparent space that webrender
+# will optimize out.
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 10 10 800 600
+ center: 50 40
+ radius: 10 10
+ stops: [0, red, 1, blue, 1, [0,0,0,0]]
+ tile-size: 300 200
+ tile-spacing: 100 100
diff --git a/gfx/wr/wrench/reftests/gradient/radial-zero-size-1.yaml b/gfx/wr/wrench/reftests/gradient/radial-zero-size-1.yaml
new file mode 100644
index 0000000000..43e4ef323d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/radial-zero-size-1.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 50 50 200 200
+ center: 100 100
+ radius: 100 0
+ stops: [0.0, blue, 1.0, red]
diff --git a/gfx/wr/wrench/reftests/gradient/radial-zero-size-2.yaml b/gfx/wr/wrench/reftests/gradient/radial-zero-size-2.yaml
new file mode 100644
index 0000000000..94bf6eae73
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/radial-zero-size-2.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 50 50 200 200
+ center: 100 100
+ radius: 0 100
+ stops: [0.0, blue, 1.0, red]
diff --git a/gfx/wr/wrench/reftests/gradient/radial-zero-size-3.yaml b/gfx/wr/wrench/reftests/gradient/radial-zero-size-3.yaml
new file mode 100644
index 0000000000..3efa1ec307
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/radial-zero-size-3.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: 50 50 200 200
+ color: red
diff --git a/gfx/wr/wrench/reftests/gradient/radial-zero-size-ref.yaml b/gfx/wr/wrench/reftests/gradient/radial-zero-size-ref.yaml
new file mode 100644
index 0000000000..b3770b752e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/radial-zero-size-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 50 50 200 200
+ center: 100 100
+ radius: 100 100
+ stops: [0.0, red, 1.0, red]
diff --git a/gfx/wr/wrench/reftests/gradient/reftest.list b/gfx/wr/wrench/reftests/gradient/reftest.list
new file mode 100644
index 0000000000..5c55c29b06
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/reftest.list
@@ -0,0 +1,128 @@
+platform(linux,mac) == premultiplied-aligned.yaml premultiplied-aligned.png
+fuzzy(1,500) platform(linux,mac) == premultiplied-angle.yaml premultiplied-angle.png
+platform(linux,mac) == premultiplied-radial.yaml premultiplied-radial.png
+platform(linux,mac) == premultiplied-conic.yaml premultiplied-conic.png
+
+platform(linux,mac) == premultiplied-aligned-2.yaml premultiplied-aligned-2.png
+platform(linux,mac) == premultiplied-angle-2.yaml premultiplied-angle-2.png
+platform(linux,mac) == premultiplied-radial-2.yaml premultiplied-radial-2.png
+platform(linux,mac) == premultiplied-conic-2.yaml premultiplied-conic-2.png
+
+== linear.yaml linear-ref.png
+== linear-reverse.yaml linear-ref.png
+fuzzy(255,1200) == linear-reverse-2.yaml linear-reverse-2-ref.yaml
+== linear-reverse-3.yaml linear-reverse-3-ref.yaml
+platform(linux,mac) fuzzy(1,35000) == linear-stops.yaml linear-stops-ref.png
+
+== linear-clamp-1a.yaml linear-clamp-1-ref.yaml
+== linear-clamp-1b.yaml linear-clamp-1-ref.yaml
+== linear-clamp-2.yaml linear-clamp-2-ref.yaml
+
+fuzzy-range(<=1,*4800) == linear-hard-stop.yaml linear-hard-stop-ref.png
+fuzzy-if(env(android,device),15,17000) == linear-hard-stop-repeat-large.yaml linear-hard-stop-repeat-large-ref.yaml # Android device is Samsung Galaxy A51
+
+# dithering requires us to fuzz here
+fuzzy(1,20000) == linear.yaml linear-ref.yaml
+fuzzy(1,20000) == linear-reverse.yaml linear-ref.yaml
+
+fuzzy(1,40000) == linear-aligned-clip.yaml linear-aligned-clip-ref.yaml
+
+platform(linux,mac) fuzzy(1,80000) == radial-circle.yaml radial-circle-ref.png
+platform(linux,mac) fuzzy(1,80000) == radial-ellipse.yaml radial-ellipse-ref.png
+
+!= radial-circle.yaml radial-ellipse.yaml
+
+== norm-linear-1.yaml norm-linear-1-ref.yaml
+== norm-linear-2.yaml norm-linear-2-ref.yaml
+== norm-linear-3.yaml norm-linear-3-ref.yaml
+== norm-linear-4.yaml norm-linear-4-ref.yaml
+== norm-linear-degenerate.yaml norm-radial-degenerate-ref.yaml
+
+== norm-radial-1.yaml norm-radial-1-ref.yaml
+== norm-radial-2.yaml norm-radial-2-ref.yaml
+== norm-radial-3.yaml norm-radial-3-ref.yaml
+== norm-radial-degenerate.yaml norm-radial-degenerate-ref.yaml
+
+== norm-conic-1.yaml norm-conic-1-ref.yaml
+== norm-conic-2.yaml norm-conic-2-ref.yaml
+== norm-conic-3.yaml norm-conic-3-ref.yaml
+== norm-conic-4.yaml norm-conic-4-ref.yaml
+== norm-conic-degenerate.yaml norm-conic-degenerate-ref.yaml
+
+# fuzzy because of differences from normalization
+# this might be able to be improved
+fuzzy(255,1200) == repeat-linear.yaml repeat-linear-ref.yaml
+fuzzy(255,1200) == repeat-linear-reverse.yaml repeat-linear-ref.yaml
+fuzzy(255,2666) == repeat-radial.yaml repeat-radial-ref.yaml
+fuzzy(255,2666) == repeat-radial-negative.yaml repeat-radial-ref.yaml
+fuzzy(255,1652) == repeat-conic.yaml repeat-conic-ref.yaml
+fuzzy(255,1652) == repeat-conic-negative.yaml repeat-conic-ref.yaml
+
+# fuzzy because of thin spaced out column of pixels that are 1 off
+fuzzy(1,83164) == tiling-linear-1.yaml tiling-linear-1-ref.yaml
+fuzzy(1,46293) == tiling-linear-2.yaml tiling-linear-2-ref.yaml
+fuzzy(1,62154) == tiling-linear-3.yaml tiling-linear-3-ref.yaml
+
+fuzzy(1,17) == tiling-radial-1.yaml tiling-radial-1-ref.yaml
+fuzzy(1,1) == tiling-radial-2.yaml tiling-radial-2-ref.yaml
+fuzzy(1,3) fuzzy-if(platform(swgl),1,1318) == tiling-radial-3.yaml tiling-radial-3-ref.yaml
+fuzzy(1,17) == tiling-radial-4.yaml tiling-radial-4-ref.yaml
+
+fuzzy(1,17) == tiling-conic-1.yaml tiling-conic-1-ref.yaml
+fuzzy(1,1) == tiling-conic-2.yaml tiling-conic-2-ref.yaml
+fuzzy(1,7) == tiling-conic-3.yaml tiling-conic-3-ref.yaml
+
+== radial-zero-size-1.yaml radial-zero-size-ref.yaml
+== radial-zero-size-2.yaml radial-zero-size-ref.yaml
+== radial-zero-size-3.yaml radial-zero-size-ref.yaml
+
+== linear-adjust-tile-size.yaml linear-adjust-tile-size-ref.yaml
+== linear-repeat-clip.yaml linear-repeat-clip-ref.yaml
+
+platform(linux,mac) == linear-aligned-border-radius.yaml linear-aligned-border-radius.png
+# interpolation fuzz from sampling texture-baked gradient ramps
+platform(linux,mac) fuzzy-range(<=1,*1404) == repeat-border-radius.yaml repeat-border-radius.png
+
+== conic.yaml conic-ref.yaml
+fuzzy(1,57) == conic-simple.yaml conic-simple.png
+fuzzy(255,302) == conic-angle.yaml conic-angle.png
+== conic-center.yaml conic-center.png
+fuzzy(1,2) == conic-angle-wraparound.yaml conic-angle.yaml
+fuzzy-if(env(android,device),254,146) fuzzy-if(not(env(android,device)),1,1) == conic-angle-wraparound-negative.yaml conic-angle.yaml # Android device is Samsung Galaxy A51
+fuzzy(1,333) == conic-color-wheel.yaml conic-color-wheel.png
+
+# gradient caching tests
+# replaces a computed gradient by a sampled texture, so a lot of off-by-one
+# variation from interpolation, which is fine:
+fuzzy-range(<=1,*195000) == gradient_cache_5stops.yaml gradient_cache_5stops_ref.yaml
+fuzzy-range(<=1,*171840) == gradient_cache_5stops_vertical.yaml gradient_cache_5stops_vertical_ref.yaml
+== gradient_cache_hardstop.yaml gradient_cache_hardstop_ref.yaml
+fuzzy-range(<=1,1) == gradient_cache_hardstop_clip.yaml gradient_cache_hardstop_clip_ref.yaml
+== gradient_cache_clamp.yaml gradient_cache_clamp_ref.yaml
+== gradient_cache_repeat.yaml gradient_cache_repeat_ref.yaml
+== linear-bug-1703141.yaml linear-bug-1703141.yaml
+
+# Recognize opaque tiles with gradient backgrounds
+== conic-backdrop-with-spacing.yaml conic-backdrop-with-spacing-ref.yaml
+== conic-backdrop.yaml conic-backdrop-ref.yaml
+== linear-backdrop-with-spacing.yaml linear-backdrop-with-spacing-ref.yaml
+== linear-backdrop.yaml linear-backdrop-ref.yaml
+== radial-backdrop-with-spacing.yaml radial-backdrop-with-spacing-ref.yaml
+== radial-backdrop.yaml radial-backdrop-ref.yaml
+
+# Exercise the radial gradient optimization code path
+== radial-optimized.yaml radial-optimized-ref.yaml
+== radial-optimized-2.yaml radial-optimized-2-ref.yaml
+== radial-tiling-optimized.yaml radial-tiling-optimized-ref.yaml
+
+# Exercise the cached gradient scaling code path
+fuzzy(2,23000) == linear-large.yaml linear-large-ref.yaml
+== conic-large.yaml conic-large-ref.yaml
+fuzzy-if(env(android,device),254,1) == conic-large-hard-stop.yaml conic-large-hard-stop-ref.yaml # Android device is Samsung Galaxy A51
+fuzzy(1,7000) == radial-large.yaml radial-large-ref.png
+
+# crash tests
+== linear-far-endpoints.yaml linear-far-endpoints.yaml
+== linear-nan.yaml linear-nan.yaml
+== radial-nan.yaml radial-nan.yaml
+== conic-nan.yaml conic-nan.yaml
diff --git a/gfx/wr/wrench/reftests/gradient/repeat-border-radius.png b/gfx/wr/wrench/reftests/gradient/repeat-border-radius.png
new file mode 100644
index 0000000000..cc0fa947ae
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/repeat-border-radius.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/gradient/repeat-border-radius.yaml b/gfx/wr/wrench/reftests/gradient/repeat-border-radius.yaml
new file mode 100644
index 0000000000..08904e082f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/repeat-border-radius.yaml
@@ -0,0 +1,136 @@
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [20, 20, 100, 100]
+ radius: 32
+ - type: gradient
+ bounds: 20 20 100 100
+ start: 50 0
+ end: 50 100
+ stops: [0.0, red, 1.0, yellow]
+ repeat: true
+ clip-chain: [2]
+
+ - type: rect
+ bounds: [130, 10, 120, 120]
+ color: blue
+
+ - type: clip
+ id: 3
+ complex:
+ - rect: [140, 20, 100, 100]
+ radius: 32
+ - type: gradient
+ bounds: 140 20 100 100
+ start: 50 0
+ end: 50 100
+ stops: [0.2, red, 1.0, yellow]
+ clip-chain: [3]
+
+ - type: rect
+ bounds: [260, 10, 120, 120]
+ color: black
+
+ - type: clip
+ id: 4
+ complex:
+ - rect: [270, 20, 100, 100]
+ radius: 32
+ - type: gradient
+ bounds: 270 20 100 100
+ start: 50 0
+ end: 51 100
+ stops: [0.0, red, 1.0, yellow]
+ clip-chain: [4]
+
+ - type: clip
+ id: 5
+ complex:
+ - rect: [20, 160, 100, 100]
+ radius: 32
+ - type: radial-gradient
+ bounds: 20 160 100 100
+ center: 50 50
+ radius: 25 25
+ stops: [0.0, red, 1.0, yellow]
+ clip-chain: [5]
+
+ - type: rect
+ bounds: [130, 150, 120, 120]
+ color: blue
+
+ - type: clip
+ id: 6
+ complex:
+ - rect: [140, 160, 100, 100]
+ radius: 32
+ - type: radial-gradient
+ bounds: 140 160 100 100
+ center: 50 50
+ radius: 25 25
+ stops: [0.0, red, 1.0, yellow]
+ clip-chain: [6]
+
+ - type: rect
+ bounds: [260, 150, 120, 120]
+ color: black
+
+ - type: clip
+ id: 7
+ complex:
+ - rect: [270, 160, 100, 100]
+ radius: 32
+ - type: radial-gradient
+ bounds: 270 160 100 100
+ center: 50 50
+ radius: 25 25
+ stops: [0.0, red, 1.0, yellow]
+ clip-chain: [7]
+
+ - type: clip
+ id: 8
+ complex:
+ - rect: [20, 300, 100, 100]
+ radius: 32
+ - type: conic-gradient
+ bounds: 20 300 100 100
+ angle: 0.0
+ center: 50 50
+ stops: [0.0, red, 1.0, yellow]
+ repeat: true
+ clip-chain: [8]
+
+ - type: rect
+ bounds: [130, 290, 120, 120]
+ color: blue
+
+ - type: clip
+ id: 9
+ complex:
+ - rect: [140, 300, 100, 100]
+ radius: 32
+ - type: conic-gradient
+ bounds: 140 300 100 100
+ angle: 0.0
+ center: 50 50
+ stops: [0.0, red, 1.0, yellow]
+ clip-chain: [9]
+
+ - type: rect
+ bounds: [260, 290, 120, 120]
+ color: black
+
+ - type: clip
+ id: 10
+ complex:
+ - rect: [270, 300, 100, 100]
+ radius: 32
+ - type: conic-gradient
+ bounds: 270 300 100 100
+ angle: 0.0
+ center: 50 50
+ stops: [0.0, red, 1.0, yellow]
+ clip-chain: [10]
diff --git a/gfx/wr/wrench/reftests/gradient/repeat-conic-negative.yaml b/gfx/wr/wrench/reftests/gradient/repeat-conic-negative.yaml
new file mode 100644
index 0000000000..e1043562da
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/repeat-conic-negative.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: conic-gradient
+ bounds: 50 50 300 300
+ angle: -0.62831853
+ center: 150 150
+ stops: [0.1, red, 0.2, red, 0.2, blue, 0.3, blue]
+ repeat: true
diff --git a/gfx/wr/wrench/reftests/gradient/repeat-conic-ref.yaml b/gfx/wr/wrench/reftests/gradient/repeat-conic-ref.yaml
new file mode 100644
index 0000000000..b98cc378a7
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/repeat-conic-ref.yaml
@@ -0,0 +1,27 @@
+---
+root:
+ items:
+ - type: conic-gradient
+ bounds: 50 50 300 300
+ angle: 0.0
+ center: 150 150
+ stops: [0.0, red,
+ 0.1, red,
+ 0.1, blue,
+ 0.2, blue,
+ 0.2, red,
+ 0.3, red,
+ 0.3, blue,
+ 0.4, blue,
+ 0.4, red,
+ 0.5, red,
+ 0.5, blue,
+ 0.6, blue,
+ 0.6, red,
+ 0.7, red,
+ 0.7, blue,
+ 0.8, blue,
+ 0.8, red,
+ 0.9, red,
+ 0.9, blue,
+ 1.0, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/repeat-conic.yaml b/gfx/wr/wrench/reftests/gradient/repeat-conic.yaml
new file mode 100644
index 0000000000..bbc4202aec
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/repeat-conic.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: conic-gradient
+ bounds: 50 50 300 300
+ angle: 0.0
+ center: 150 150
+ stops: [0.1, blue, 0.2, blue, 0.2, red, 0.3, red]
+ repeat: true
diff --git a/gfx/wr/wrench/reftests/gradient/repeat-linear-ref.yaml b/gfx/wr/wrench/reftests/gradient/repeat-linear-ref.yaml
new file mode 100644
index 0000000000..420a08db01
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/repeat-linear-ref.yaml
@@ -0,0 +1,27 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 300 300
+ start: 0 150
+ end: 300 150
+ stops: [0.0, red,
+ 0.1, red,
+ 0.1, blue,
+ 0.2, blue,
+ 0.2, red,
+ 0.3, red,
+ 0.3, blue,
+ 0.4, blue,
+ 0.4, red,
+ 0.5, red,
+ 0.5, blue,
+ 0.6, blue,
+ 0.6, red,
+ 0.7, red,
+ 0.7, blue,
+ 0.8, blue,
+ 0.8, red,
+ 0.9, red,
+ 0.9, blue,
+ 1.0, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/repeat-linear-reverse.yaml b/gfx/wr/wrench/reftests/gradient/repeat-linear-reverse.yaml
new file mode 100644
index 0000000000..a81bafe1bd
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/repeat-linear-reverse.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 300 300
+ start: 300 150
+ end: 0 150
+ stops: [0.1, red, 0.2, red, 0.2, blue, 0.3, blue]
+ repeat: true
diff --git a/gfx/wr/wrench/reftests/gradient/repeat-linear.yaml b/gfx/wr/wrench/reftests/gradient/repeat-linear.yaml
new file mode 100644
index 0000000000..63e136bc8e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/repeat-linear.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: gradient
+ bounds: 50 50 300 300
+ start: 0 150
+ end: 300 150
+ stops: [0.1, blue, 0.2, blue, 0.2, red, 0.3, red]
+ repeat: true
diff --git a/gfx/wr/wrench/reftests/gradient/repeat-radial-negative.yaml b/gfx/wr/wrench/reftests/gradient/repeat-radial-negative.yaml
new file mode 100644
index 0000000000..949455f893
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/repeat-radial-negative.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 50 50 300 300
+ center: 150 150
+ radius: 150 150
+ stops: [-0.3, blue, -0.2, blue, -0.2, red, -0.1, red]
+ repeat: true
diff --git a/gfx/wr/wrench/reftests/gradient/repeat-radial-ref.yaml b/gfx/wr/wrench/reftests/gradient/repeat-radial-ref.yaml
new file mode 100644
index 0000000000..d2c0292e6e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/repeat-radial-ref.yaml
@@ -0,0 +1,38 @@
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 50 50 300 300
+ center: 150 150
+ radius: 150 150
+ # note: we need stops up to 1.4 because a repeating radial gradient
+ # will fill the whole rect beyond 1.0. So the furthest radius we have
+ # to fill in is the diagonal of the unit square
+ stops: [0.0, red,
+ 0.1, red,
+ 0.1, blue,
+ 0.2, blue,
+ 0.2, red,
+ 0.3, red,
+ 0.3, blue,
+ 0.4, blue,
+ 0.4, red,
+ 0.5, red,
+ 0.5, blue,
+ 0.6, blue,
+ 0.6, red,
+ 0.7, red,
+ 0.7, blue,
+ 0.8, blue,
+ 0.8, red,
+ 0.9, red,
+ 0.9, blue,
+ 1.0, blue,
+ 1.0, red,
+ 1.1, red,
+ 1.1, blue,
+ 1.2, blue,
+ 1.2, red,
+ 1.3, red,
+ 1.3, blue,
+ 1.4, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/repeat-radial.yaml b/gfx/wr/wrench/reftests/gradient/repeat-radial.yaml
new file mode 100644
index 0000000000..04c676c25b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/repeat-radial.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: radial-gradient
+ bounds: 50 50 300 300
+ center: 150 150
+ radius: 150 150
+ stops: [0.1, blue, 0.2, blue, 0.2, red, 0.3, red]
+ repeat: true
diff --git a/gfx/wr/wrench/reftests/gradient/tiling-conic-1-ref.yaml b/gfx/wr/wrench/reftests/gradient/tiling-conic-1-ref.yaml
new file mode 100644
index 0000000000..6b93e28fa1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/tiling-conic-1-ref.yaml
@@ -0,0 +1,27 @@
+---
+root:
+ items:
+ # top left
+ - type: conic-gradient
+ bounds: 50 50 200 200
+ angle: 0.0
+ center: 100 100
+ stops: [0, red, 1, blue]
+ # top right
+ - type: conic-gradient
+ bounds: 350 50 200 200
+ angle: 0.0
+ center: 100 100
+ stops: [0, red, 1, blue]
+ # bottom left
+ - type: conic-gradient
+ bounds: 50 350 200 200
+ angle: 0.0
+ center: 100 100
+ stops: [0, red, 1, blue]
+ # bottom right
+ - type: conic-gradient
+ bounds: 350 350 200 200
+ angle: 0.0
+ center: 100 100
+ stops: [0, red, 1, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/tiling-conic-1.yaml b/gfx/wr/wrench/reftests/gradient/tiling-conic-1.yaml
new file mode 100644
index 0000000000..71ffe80b22
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/tiling-conic-1.yaml
@@ -0,0 +1,11 @@
+---
+root:
+ items:
+ # basic - 4 tiles spaced out with no clipping
+ - type: conic-gradient
+ bounds: 50 50 500 500
+ angle: 0.0
+ center: 100 100
+ stops: [0, red, 1, blue]
+ tile-size: 200 200
+ tile-spacing: 100 100
diff --git a/gfx/wr/wrench/reftests/gradient/tiling-conic-2-ref.yaml b/gfx/wr/wrench/reftests/gradient/tiling-conic-2-ref.yaml
new file mode 100644
index 0000000000..4360aa204c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/tiling-conic-2-ref.yaml
@@ -0,0 +1,27 @@
+---
+root:
+ items:
+ # top left
+ - type: conic-gradient
+ bounds: 50 50 200 200
+ angle: 0.0
+ center: 100 100
+ stops: [0, red, 1, blue]
+ # top right
+ - type: conic-gradient
+ bounds: 350 50 100 200
+ angle: 0.0
+ center: 100 100
+ stops: [0, red, 1, blue]
+ # bottom left
+ - type: conic-gradient
+ bounds: 50 350 200 100
+ angle: 0.0
+ center: 100 100
+ stops: [0, red, 1, blue]
+ # bottom right
+ - type: conic-gradient
+ bounds: 350 350 100 100
+ angle: 0.0
+ center: 100 100
+ stops: [0, red, 1, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/tiling-conic-2.yaml b/gfx/wr/wrench/reftests/gradient/tiling-conic-2.yaml
new file mode 100644
index 0000000000..ba9522a68c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/tiling-conic-2.yaml
@@ -0,0 +1,11 @@
+---
+root:
+ items:
+ # slightly clip the last tile
+ - type: conic-gradient
+ bounds: 50 50 400 400
+ angle: 0.0
+ center: 100 100
+ stops: [0, red, 1, blue]
+ tile-size: 200 200
+ tile-spacing: 100 100
diff --git a/gfx/wr/wrench/reftests/gradient/tiling-conic-3-ref.yaml b/gfx/wr/wrench/reftests/gradient/tiling-conic-3-ref.yaml
new file mode 100644
index 0000000000..d7b9541ef4
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/tiling-conic-3-ref.yaml
@@ -0,0 +1,27 @@
+---
+root:
+ items:
+ # top left
+ - type: conic-gradient
+ bounds: 50 50 200 200
+ angle: 0.0
+ center: 100 100
+ stops: [0, red, 1, blue]
+ # top right
+ - type: conic-gradient
+ bounds: 250 50 200 200
+ angle: 0.0
+ center: 100 100
+ stops: [0, red, 1, blue]
+ # bottom left
+ - type: conic-gradient
+ bounds: 50 250 200 100
+ angle: 0.0
+ center: 100 100
+ stops: [0, red, 1, blue]
+ # bottom right
+ - type: conic-gradient
+ bounds: 250 250 200 100
+ angle: 0.0
+ center: 100 100
+ stops: [0, red, 1, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/tiling-conic-3.yaml b/gfx/wr/wrench/reftests/gradient/tiling-conic-3.yaml
new file mode 100644
index 0000000000..77b28a9e22
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/tiling-conic-3.yaml
@@ -0,0 +1,11 @@
+---
+root:
+ items:
+ # no spacing with a clip
+ - type: conic-gradient
+ bounds: 50 50 400 300
+ angle: 0.0
+ center: 100 100
+ stops: [0, red, 1, blue]
+ tile-size: 200 200
+ tile-spacing: 0 0
diff --git a/gfx/wr/wrench/reftests/gradient/tiling-linear-1-ref.yaml b/gfx/wr/wrench/reftests/gradient/tiling-linear-1-ref.yaml
new file mode 100644
index 0000000000..c06e05fb53
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/tiling-linear-1-ref.yaml
@@ -0,0 +1,27 @@
+---
+root:
+ items:
+ # top left
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 0 100
+ end: 200 100
+ stops: [0, red, 1, blue]
+ # top right
+ - type: gradient
+ bounds: 350 50 200 200
+ start: 0 100
+ end: 200 100
+ stops: [0, red, 1, blue]
+ # bottom left
+ - type: gradient
+ bounds: 50 350 200 200
+ start: 0 100
+ end: 200 100
+ stops: [0, red, 1, blue]
+ # bottom right
+ - type: gradient
+ bounds: 350 350 200 200
+ start: 0 100
+ end: 200 100
+ stops: [0, red, 1, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/tiling-linear-1.yaml b/gfx/wr/wrench/reftests/gradient/tiling-linear-1.yaml
new file mode 100644
index 0000000000..f388e7c593
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/tiling-linear-1.yaml
@@ -0,0 +1,11 @@
+---
+root:
+ items:
+ # basic - 4 tiles spaced out with no clipping
+ - type: gradient
+ bounds: 50 50 500 500
+ start: 0 100
+ end: 200 100
+ stops: [0, red, 1, blue]
+ tile-size: 200 200
+ tile-spacing: 100 100
diff --git a/gfx/wr/wrench/reftests/gradient/tiling-linear-2-ref.yaml b/gfx/wr/wrench/reftests/gradient/tiling-linear-2-ref.yaml
new file mode 100644
index 0000000000..be7dc77463
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/tiling-linear-2-ref.yaml
@@ -0,0 +1,27 @@
+---
+root:
+ items:
+ # top left
+ - type: gradient
+ bounds: 50 50 200 200
+ start: 0 100
+ end: 200 100
+ stops: [0, red, 1, blue]
+ # top right
+ - type: gradient
+ bounds: 350 50 100 200
+ start: 0 100
+ end: 200 100
+ stops: [0, red, 1, blue]
+ # bottom left
+ - type: gradient
+ bounds: 50 350 200 100
+ start: 0 100
+ end: 200 100
+ stops: [0, red, 1, blue]
+ # bottom right
+ - type: gradient
+ bounds: 350 350 100 100
+ start: 0 100
+ end: 200 100
+ stops: [0, red, 1, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/tiling-linear-2.yaml b/gfx/wr/wrench/reftests/gradient/tiling-linear-2.yaml
new file mode 100644
index 0000000000..7a01c062d1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/tiling-linear-2.yaml
@@ -0,0 +1,11 @@
+---
+root:
+ items:
+ # slightly clip the last tile
+ - type: gradient
+ bounds: 50 50 400 400
+ start: 0 100
+ end: 200 100
+ stops: [0, red, 1, blue]
+ tile-size: 200 200
+ tile-spacing: 100 100
diff --git a/gfx/wr/wrench/reftests/gradient/tiling-linear-3-ref.yaml b/gfx/wr/wrench/reftests/gradient/tiling-linear-3-ref.yaml
new file mode 100644
index 0000000000..90b110c8d0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/tiling-linear-3-ref.yaml
@@ -0,0 +1,15 @@
+---
+root:
+ items:
+ # top left and bottom left
+ - type: gradient
+ bounds: 50 50 200 300
+ start: 0 100
+ end: 200 100
+ stops: [0, red, 1, blue]
+ # top right and bottom right
+ - type: gradient
+ bounds: 250 50 200 300
+ start: 0 100
+ end: 200 100
+ stops: [0, red, 1, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/tiling-linear-3.yaml b/gfx/wr/wrench/reftests/gradient/tiling-linear-3.yaml
new file mode 100644
index 0000000000..a595f3572f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/tiling-linear-3.yaml
@@ -0,0 +1,11 @@
+---
+root:
+ items:
+ # no spacing with a clip
+ - type: gradient
+ bounds: 50 50 400 300
+ start: 0 100
+ end: 200 100
+ stops: [0, red, 1, blue]
+ tile-size: 200 200
+ tile-spacing: 0 0
diff --git a/gfx/wr/wrench/reftests/gradient/tiling-radial-1-ref.yaml b/gfx/wr/wrench/reftests/gradient/tiling-radial-1-ref.yaml
new file mode 100644
index 0000000000..66f8c03592
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/tiling-radial-1-ref.yaml
@@ -0,0 +1,27 @@
+---
+root:
+ items:
+ # top left
+ - type: radial-gradient
+ bounds: 50 50 200 200
+ center: 100 100
+ radius: 100 100
+ stops: [0, red, 1, blue]
+ # top right
+ - type: radial-gradient
+ bounds: 350 50 200 200
+ center: 100 100
+ radius: 100 100
+ stops: [0, red, 1, blue]
+ # bottom left
+ - type: radial-gradient
+ bounds: 50 350 200 200
+ center: 100 100
+ radius: 100 100
+ stops: [0, red, 1, blue]
+ # bottom right
+ - type: radial-gradient
+ bounds: 350 350 200 200
+ center: 100 100
+ radius: 100 100
+ stops: [0, red, 1, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/tiling-radial-1.yaml b/gfx/wr/wrench/reftests/gradient/tiling-radial-1.yaml
new file mode 100644
index 0000000000..b832a72f96
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/tiling-radial-1.yaml
@@ -0,0 +1,11 @@
+---
+root:
+ items:
+ # basic - 4 tiles spaced out with no clipping
+ - type: radial-gradient
+ bounds: 50 50 500 500
+ center: 100 100
+ radius: 100 100
+ stops: [0, red, 1, blue]
+ tile-size: 200 200
+ tile-spacing: 100 100
diff --git a/gfx/wr/wrench/reftests/gradient/tiling-radial-2-ref.yaml b/gfx/wr/wrench/reftests/gradient/tiling-radial-2-ref.yaml
new file mode 100644
index 0000000000..da9bcd1bff
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/tiling-radial-2-ref.yaml
@@ -0,0 +1,27 @@
+---
+root:
+ items:
+ # top left
+ - type: radial-gradient
+ bounds: 50 50 200 200
+ center: 100 100
+ radius: 100 100
+ stops: [0, red, 1, blue]
+ # top right
+ - type: radial-gradient
+ bounds: 350 50 100 200
+ center: 100 100
+ radius: 100 100
+ stops: [0, red, 1, blue]
+ # bottom left
+ - type: radial-gradient
+ bounds: 50 350 200 100
+ center: 100 100
+ radius: 100 100
+ stops: [0, red, 1, blue]
+ # bottom right
+ - type: radial-gradient
+ bounds: 350 350 100 100
+ center: 100 100
+ radius: 100 100
+ stops: [0, red, 1, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/tiling-radial-2.yaml b/gfx/wr/wrench/reftests/gradient/tiling-radial-2.yaml
new file mode 100644
index 0000000000..8b7feb205e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/tiling-radial-2.yaml
@@ -0,0 +1,11 @@
+---
+root:
+ items:
+ # slightly clip the last tile
+ - type: radial-gradient
+ bounds: 50 50 400 400
+ center: 100 100
+ radius: 100 100
+ stops: [0, red, 1, blue]
+ tile-size: 200 200
+ tile-spacing: 100 100
diff --git a/gfx/wr/wrench/reftests/gradient/tiling-radial-3-ref.yaml b/gfx/wr/wrench/reftests/gradient/tiling-radial-3-ref.yaml
new file mode 100644
index 0000000000..d8564eed64
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/tiling-radial-3-ref.yaml
@@ -0,0 +1,27 @@
+---
+root:
+ items:
+ # top left
+ - type: radial-gradient
+ bounds: 50 50 200 200
+ center: 100 100
+ radius: 200 100
+ stops: [0, red, 1, blue]
+ # top right
+ - type: radial-gradient
+ bounds: 250 50 200 200
+ center: 100 100
+ radius: 200 100
+ stops: [0, red, 1, blue]
+ # bottom left
+ - type: radial-gradient
+ bounds: 50 250 200 100
+ center: 100 100
+ radius: 200 100
+ stops: [0, red, 1, blue]
+ # bottom right
+ - type: radial-gradient
+ bounds: 250 250 200 100
+ center: 100 100
+ radius: 200 100
+ stops: [0, red, 1, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/tiling-radial-3.yaml b/gfx/wr/wrench/reftests/gradient/tiling-radial-3.yaml
new file mode 100644
index 0000000000..4ca0886a0d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/tiling-radial-3.yaml
@@ -0,0 +1,11 @@
+---
+root:
+ items:
+ # no spacing with a clip
+ - type: radial-gradient
+ bounds: 50 50 400 300
+ center: 100 100
+ radius: 200 100
+ stops: [0, red, 1, blue]
+ tile-size: 200 200
+ tile-spacing: 0 0
diff --git a/gfx/wr/wrench/reftests/gradient/tiling-radial-4-ref.yaml b/gfx/wr/wrench/reftests/gradient/tiling-radial-4-ref.yaml
new file mode 100644
index 0000000000..2ba2a96322
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/tiling-radial-4-ref.yaml
@@ -0,0 +1,27 @@
+---
+root:
+ items:
+ # top left
+ - type: radial-gradient
+ bounds: 50 50 200 200
+ center: 100 100
+ radius: 200 100
+ stops: [0, red, 1, blue]
+ # top right
+ - type: radial-gradient
+ bounds: 350 50 200 200
+ center: 100 100
+ radius: 200 100
+ stops: [0, red, 1, blue]
+ # bottom left
+ - type: radial-gradient
+ bounds: 50 350 200 200
+ center: 100 100
+ radius: 200 100
+ stops: [0, red, 1, blue]
+ # bottom right
+ - type: radial-gradient
+ bounds: 350 350 200 200
+ center: 100 100
+ radius: 200 100
+ stops: [0, red, 1, blue]
diff --git a/gfx/wr/wrench/reftests/gradient/tiling-radial-4.yaml b/gfx/wr/wrench/reftests/gradient/tiling-radial-4.yaml
new file mode 100644
index 0000000000..724019441f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/gradient/tiling-radial-4.yaml
@@ -0,0 +1,11 @@
+---
+root:
+ items:
+ # make sure the ellipse transformation retains square tiles
+ - type: radial-gradient
+ bounds: 50 50 500 500
+ center: 100 100
+ radius: 200 100
+ stops: [0, red, 1, blue]
+ tile-size: 200 200
+ tile-spacing: 100 100
diff --git a/gfx/wr/wrench/reftests/image/colorrect.png b/gfx/wr/wrench/reftests/image/colorrect.png
new file mode 100644
index 0000000000..75283ee1f1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/colorrect.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/image/downscale.png b/gfx/wr/wrench/reftests/image/downscale.png
new file mode 100644
index 0000000000..460a29df19
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/downscale.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/image/downscale.yaml b/gfx/wr/wrench/reftests/image/downscale.yaml
new file mode 100644
index 0000000000..a848af7ad4
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/downscale.yaml
@@ -0,0 +1,18 @@
+# Tests the image quality of using a very large A8 image
+# as a clip mask. It's currently not very good!
+# https://github.com/servo/webrender/issues/2023
+root:
+ items:
+ - type: clip
+ id: 2
+ image-mask:
+ image: "firefox.png"
+ rect: [0, 0, 350, 90]
+ repeat: false
+ - type: stacking-context
+ clip-chain: [2]
+ items:
+ - type: rect
+ bounds: [0, 0, 350, 90]
+ color: blue
+
diff --git a/gfx/wr/wrench/reftests/image/firefox.png b/gfx/wr/wrench/reftests/image/firefox.png
new file mode 100644
index 0000000000..f00696789a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/firefox.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/image/green-alpha-ref.yaml b/gfx/wr/wrench/reftests/image/green-alpha-ref.yaml
new file mode 100644
index 0000000000..62811ac9e2
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/green-alpha-ref.yaml
@@ -0,0 +1,4 @@
+root:
+ items:
+ - image: solid-color(0, 255, 0, 127, 400, 400)
+ bounds: 0 0 400 400
diff --git a/gfx/wr/wrench/reftests/image/image-alpha-stretch-tile.yaml b/gfx/wr/wrench/reftests/image/image-alpha-stretch-tile.yaml
new file mode 100644
index 0000000000..8579d03720
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/image-alpha-stretch-tile.yaml
@@ -0,0 +1,8 @@
+root:
+ items:
+ - image: solid-color(0, 255, 0, 255, 40, 4097)
+ bounds: 0 0 400 400
+ alpha: premultiplied-alpha
+ color: 255 255 255 0.5
+ stretch-size: 200 200
+ tile-spacing: 0 0
diff --git a/gfx/wr/wrench/reftests/image/image-alpha-stretch.yaml b/gfx/wr/wrench/reftests/image/image-alpha-stretch.yaml
new file mode 100644
index 0000000000..776d750745
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/image-alpha-stretch.yaml
@@ -0,0 +1,6 @@
+root:
+ items:
+ - image: solid-color(0, 255, 0, 255, 40, 4097)
+ bounds: 0 0 400 400
+ alpha: premultiplied-alpha
+ color: 255 255 255 0.5
diff --git a/gfx/wr/wrench/reftests/image/image-filter-stretch-tile.yaml b/gfx/wr/wrench/reftests/image/image-filter-stretch-tile.yaml
new file mode 100644
index 0000000000..facb98d7da
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/image-filter-stretch-tile.yaml
@@ -0,0 +1,10 @@
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 400, 400]
+ filters: [opacity(0.5)]
+ items:
+ - image: solid-color(0, 255, 0, 255, 40, 4097)
+ bounds: 0 0 400 400
+ stretch-size: 200 200
+ tile-spacing: 0 0
diff --git a/gfx/wr/wrench/reftests/image/image-filter-stretch.yaml b/gfx/wr/wrench/reftests/image/image-filter-stretch.yaml
new file mode 100644
index 0000000000..c988f174ea
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/image-filter-stretch.yaml
@@ -0,0 +1,8 @@
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 400, 400]
+ filters: [opacity(0.5)]
+ items:
+ - image: solid-color(0, 255, 0, 255, 40, 4097)
+ bounds: 0 0 400 400
diff --git a/gfx/wr/wrench/reftests/image/occlusion.png b/gfx/wr/wrench/reftests/image/occlusion.png
new file mode 100644
index 0000000000..56332f2ef5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/occlusion.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/image/occlusion.yaml b/gfx/wr/wrench/reftests/image/occlusion.yaml
new file mode 100644
index 0000000000..4e89a7765b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/occlusion.yaml
@@ -0,0 +1,15 @@
+# Ensure that the clip rect of a primitive that is promoted to a compositor
+# surface is correctly applied when registering it as an occlusion plane.
+
+root:
+ items:
+ - type: rect
+ color: red
+ bounds: [50, 50, 200, 200]
+
+ - type: yuv-image
+ format: interleaved
+ src: spacex-yuv.png
+ bounds: [50, 50, 200, 200]
+ prefer-compositor-surface: true
+ clip-rect: [75, 75, 150, 150]
diff --git a/gfx/wr/wrench/reftests/image/reftest.list b/gfx/wr/wrench/reftests/image/reftest.list
new file mode 100644
index 0000000000..a02e322105
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/reftest.list
@@ -0,0 +1,19 @@
+== tile-size.yaml tile-size-ref.yaml
+== very-big.yaml very-big-ref.yaml
+== very-big-tile-size.yaml very-big-tile-size-ref.yaml
+== tile-with-spacing.yaml tile-with-spacing-ref.yaml
+skip_on(android,device) fuzzy(1,331264) == tile-repeat-prim-or-decompose.yaml tile-repeat-prim-or-decompose-ref.yaml
+platform(linux,mac) options(allow-mipmaps) == downscale.yaml downscale.png
+skip_on(android,device) fuzzy-if(platform(swgl),1,20) == segments.yaml segments.png
+platform(linux,mac) fuzzy(1,6105) == yuv.yaml yuv.png
+platform(linux,mac) fuzzy(1,6105) fuzzy-if(platform(swgl),1,205000) == yuv-clip.yaml yuv.png
+skip_on(android,device) == tiled-clip-chain.yaml tiled-clip-chain-ref.yaml
+skip_on(android,device) == tiled-complex-clip.yaml tiled-complex-clip-ref.yaml
+platform(linux,mac) == texture-rect.yaml texture-rect-ref.yaml
+platform(linux) fuzzy(1,161) == occlusion.yaml occlusion.png
+# allow slight lerp change where the squares meet, but catch lerping problems on the boundary (should clamp)
+fuzzy-range(<=2,*450) == rgb_composite.yaml rgb_composite_ref.yaml
+fuzzy(1,160000) == image-alpha-stretch.yaml green-alpha-ref.yaml
+fuzzy(1,160000) == image-alpha-stretch-tile.yaml green-alpha-ref.yaml
+fuzzy(1,160000) == image-filter-stretch.yaml green-alpha-ref.yaml
+fuzzy(1,160000) == image-filter-stretch-tile.yaml green-alpha-ref.yaml
diff --git a/gfx/wr/wrench/reftests/image/rgb_composite.yaml b/gfx/wr/wrench/reftests/image/rgb_composite.yaml
new file mode 100644
index 0000000000..74db512eed
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/rgb_composite.yaml
@@ -0,0 +1,10 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [50, 50, 100, 100]
+ transform: scale(1.5)
+ items:
+ - image: colorrect.png
+ bounds: [0, 0, 100, 100]
+ prefer-compositor-surface: true
diff --git a/gfx/wr/wrench/reftests/image/rgb_composite_ref.yaml b/gfx/wr/wrench/reftests/image/rgb_composite_ref.yaml
new file mode 100644
index 0000000000..0bc4a6e436
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/rgb_composite_ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [50, 50, 100, 100]
+ transform: scale(1.5)
+ items:
+ - image: colorrect.png
+ bounds: [0, 0, 100, 100]
diff --git a/gfx/wr/wrench/reftests/image/segments.png b/gfx/wr/wrench/reftests/image/segments.png
new file mode 100644
index 0000000000..45e4b544d4
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/segments.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/image/segments.yaml b/gfx/wr/wrench/reftests/image/segments.yaml
new file mode 100644
index 0000000000..840fc60398
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/segments.yaml
@@ -0,0 +1,12 @@
+root:
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [10, 10, 260, 260]
+ radius: 32
+ - image: checkerboard(2, 16, 16)
+ clip-chain: [2]
+ bounds: [10, 10, 260, 260]
+ - image: checkerboard(2, 16, 16)
+ bounds: [10, 290, 260, 260]
diff --git a/gfx/wr/wrench/reftests/image/spacex-u.png b/gfx/wr/wrench/reftests/image/spacex-u.png
new file mode 100644
index 0000000000..1de9b6ff66
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/spacex-u.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/image/spacex-uv.png b/gfx/wr/wrench/reftests/image/spacex-uv.png
new file mode 100644
index 0000000000..4b0259900e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/spacex-uv.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/image/spacex-v.png b/gfx/wr/wrench/reftests/image/spacex-v.png
new file mode 100644
index 0000000000..1d1a1c0118
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/spacex-v.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/image/spacex-y.png b/gfx/wr/wrench/reftests/image/spacex-y.png
new file mode 100644
index 0000000000..2256fabecd
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/spacex-y.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/image/spacex-yuv.png b/gfx/wr/wrench/reftests/image/spacex-yuv.png
new file mode 100644
index 0000000000..6073e71735
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/spacex-yuv.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/image/texture-rect-ref.yaml b/gfx/wr/wrench/reftests/image/texture-rect-ref.yaml
new file mode 100644
index 0000000000..42d8907434
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/texture-rect-ref.yaml
@@ -0,0 +1,15 @@
+# Reference for test to ensure external images using the textureRect sampler mode work correctly.
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: red
+ - type: rect
+ bounds: [50, 0, 50, 50]
+ color: blue
+ - type: rect
+ bounds: [0, 50, 50, 50]
+ color: green
+ - type: rect
+ bounds: [50, 50, 50, 50]
+ color: white \ No newline at end of file
diff --git a/gfx/wr/wrench/reftests/image/texture-rect.yaml b/gfx/wr/wrench/reftests/image/texture-rect.yaml
new file mode 100644
index 0000000000..78e7ce6e86
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/texture-rect.yaml
@@ -0,0 +1,7 @@
+# Test to ensure external images using the textureRect sampler mode work correctly.
+root:
+ items:
+ - image: colorrect.png
+ bounds: [0, 0, 100, 100]
+ external: true
+ external-target: rect \ No newline at end of file
diff --git a/gfx/wr/wrench/reftests/image/tile-repeat-prim-or-decompose-ref.yaml b/gfx/wr/wrench/reftests/image/tile-repeat-prim-or-decompose-ref.yaml
new file mode 100644
index 0000000000..8b7e801d56
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/tile-repeat-prim-or-decompose-ref.yaml
@@ -0,0 +1,8 @@
+root:
+ items:
+ - image: xy-gradient(500, 50)
+ bounds: 0 0 800 800
+ stretch-size: 50 50
+ - image: xy-gradient(50, 500)
+ bounds: 800 0 800 800
+ stretch-size: 50 50
diff --git a/gfx/wr/wrench/reftests/image/tile-repeat-prim-or-decompose.yaml b/gfx/wr/wrench/reftests/image/tile-repeat-prim-or-decompose.yaml
new file mode 100644
index 0000000000..43b12bbd94
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/tile-repeat-prim-or-decompose.yaml
@@ -0,0 +1,17 @@
+# This test aims at exercising the different ways we handle repetition of tiled images.
+root:
+ items:
+ # This should cause the primitive repetition to be decomposed on the cpu along the x axis
+ # but perform the repetition along the y axis on the image shader because the image width
+ # fits within the tile size.
+ - image: xy-gradient(500, 50)
+ bounds: 0 0 800 800
+ stretch-size: 50 50
+ tile-size: 50
+ # This should cause the primitive repetition to be decomposed on the cpu along the y axis
+ # but perform the repetition along the x axis in the image shader because the image height
+ # fits within the tile size.
+ - image: xy-gradient(50, 500)
+ bounds: 800 0 800 800
+ stretch-size: 50 50
+ tile-size: 50
diff --git a/gfx/wr/wrench/reftests/image/tile-size-ref.yaml b/gfx/wr/wrench/reftests/image/tile-size-ref.yaml
new file mode 100644
index 0000000000..c90ea8a341
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/tile-size-ref.yaml
@@ -0,0 +1,14 @@
+root:
+ items:
+ - image: xy-gradient(512, 512)
+ bounds: 0 0 512 512
+ stretch-size: 512 512
+ - image: xy-gradient(512, 512)
+ bounds: 512 0 512 512
+ stretch-size: 512 512
+ - image: xy-gradient(512, 512)
+ bounds: 0 512 512 512
+ stretch-size: 512 512
+ - image: xy-gradient(512, 512)
+ bounds: 512 512 512 512
+ stretch-size: 512 512
diff --git a/gfx/wr/wrench/reftests/image/tile-size.yaml b/gfx/wr/wrench/reftests/image/tile-size.yaml
new file mode 100644
index 0000000000..e17a7b53a4
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/tile-size.yaml
@@ -0,0 +1,19 @@
+root:
+ items:
+ - image: xy-gradient(512, 512)
+ bounds: 0 0 512 512
+ stretch-size: 512 512
+ tile-size: 64
+ - image: xy-gradient(512, 512)
+ bounds: 512 0 512 512
+ stretch-size: 512 512
+ tile-size: 128
+ - image: xy-gradient(512, 512)
+ bounds: 0 512 512 512
+ stretch-size: 512 512
+ tile-size: 256
+ # tile size bigger than the image itself
+ - image: xy-gradient(512, 512)
+ bounds: 512 512 512 512
+ stretch-size: 512 512
+ tile-size: 4096
diff --git a/gfx/wr/wrench/reftests/image/tile-with-spacing-ref.yaml b/gfx/wr/wrench/reftests/image/tile-with-spacing-ref.yaml
new file mode 100644
index 0000000000..63e1315f0b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/tile-with-spacing-ref.yaml
@@ -0,0 +1,6 @@
+root:
+ items:
+ - image: solid-color(255, 0, 0, 255, 300, 300)
+ bounds: 0 0 800 800
+ stretch-size: 200 200
+ tile-spacing: 10 10
diff --git a/gfx/wr/wrench/reftests/image/tile-with-spacing.yaml b/gfx/wr/wrench/reftests/image/tile-with-spacing.yaml
new file mode 100644
index 0000000000..40bc5802d4
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/tile-with-spacing.yaml
@@ -0,0 +1,12 @@
+root:
+ items:
+ # TODO: This test would be more useful if we used an image with variations instead
+ # of a solid color. To do this we first need to change the way pixel snapping is
+ # applied so that when an image is split into several primitives, so that the latter
+ # all get snapped the same way rather than independently.
+ #- image: xy-gradient(300, 300)
+ - image: solid-color(255, 0, 0, 255, 300, 300)
+ bounds: 0 0 800 800
+ stretch-size: 200 200
+ tile-spacing: 10 10
+ tile-size: 64
diff --git a/gfx/wr/wrench/reftests/image/tiled-clip-chain-ref.yaml b/gfx/wr/wrench/reftests/image/tiled-clip-chain-ref.yaml
new file mode 100644
index 0000000000..83b4494269
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/tiled-clip-chain-ref.yaml
@@ -0,0 +1,10 @@
+root:
+ items:
+ - image: checkerboard(2, 16, 16)
+ bounds: [10, 10, 260, 260]
+ - type: rect
+ bounds: [10, 110, 200, 200]
+ color: white
+ - type: rect
+ bounds: [110, 10, 200, 270]
+ color: white \ No newline at end of file
diff --git a/gfx/wr/wrench/reftests/image/tiled-clip-chain.yaml b/gfx/wr/wrench/reftests/image/tiled-clip-chain.yaml
new file mode 100644
index 0000000000..9647e319dc
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/tiled-clip-chain.yaml
@@ -0,0 +1,11 @@
+# Test that local clip rects from clip-chains are correctly
+# propagated into the local clip rect for tiled images.
+root:
+ items:
+ - type: clip
+ id: 2
+ bounds: [10, 10, 100, 100]
+ - image: checkerboard(2, 16, 16)
+ clip-chain: [2]
+ bounds: [10, 10, 260, 260]
+ tile-size: 64 \ No newline at end of file
diff --git a/gfx/wr/wrench/reftests/image/tiled-complex-clip-ref.yaml b/gfx/wr/wrench/reftests/image/tiled-complex-clip-ref.yaml
new file mode 100644
index 0000000000..871807336a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/tiled-complex-clip-ref.yaml
@@ -0,0 +1,10 @@
+root:
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [10, 10, 100, 100]
+ radius: 32
+ - image: checkerboard(2, 16, 16)
+ clip-chain: [2]
+ bounds: [10, 10, 260, 260]
diff --git a/gfx/wr/wrench/reftests/image/tiled-complex-clip.yaml b/gfx/wr/wrench/reftests/image/tiled-complex-clip.yaml
new file mode 100644
index 0000000000..00cc8124f5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/tiled-complex-clip.yaml
@@ -0,0 +1,13 @@
+# Test that complex clips from clip-chains are correctly
+# taken into account for tiled images.
+root:
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [10, 10, 100, 100]
+ radius: 32
+ - image: checkerboard(2, 16, 16)
+ clip-chain: [2]
+ bounds: [10, 10, 260, 260]
+ tile-size: 64
diff --git a/gfx/wr/wrench/reftests/image/very-big-ref.yaml b/gfx/wr/wrench/reftests/image/very-big-ref.yaml
new file mode 100644
index 0000000000..6d7ea1b4b1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/very-big-ref.yaml
@@ -0,0 +1,5 @@
+root:
+ items:
+ - type: rect
+ bounds: 0 0 500 500
+ color: red
diff --git a/gfx/wr/wrench/reftests/image/very-big-tile-size-ref.yaml b/gfx/wr/wrench/reftests/image/very-big-tile-size-ref.yaml
new file mode 100644
index 0000000000..6d7ea1b4b1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/very-big-tile-size-ref.yaml
@@ -0,0 +1,5 @@
+root:
+ items:
+ - type: rect
+ bounds: 0 0 500 500
+ color: red
diff --git a/gfx/wr/wrench/reftests/image/very-big-tile-size.yaml b/gfx/wr/wrench/reftests/image/very-big-tile-size.yaml
new file mode 100644
index 0000000000..d3bb3b8ec7
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/very-big-tile-size.yaml
@@ -0,0 +1,9 @@
+root:
+ items:
+ - type: rect
+ bounds: 0 0 500 500
+ color: green
+ - image: solid-color(255, 0, 0, 255, 100000, 1000)
+ bounds: 0 0 500 500
+ stretch-size: 1000000 1000
+ tile-size: 60000
diff --git a/gfx/wr/wrench/reftests/image/very-big.yaml b/gfx/wr/wrench/reftests/image/very-big.yaml
new file mode 100644
index 0000000000..dc0981d3a4
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/very-big.yaml
@@ -0,0 +1,5 @@
+root:
+ items:
+ - image: solid-color(255, 0, 0, 255, 100000, 1000)
+ bounds: 0 0 500 500
+ stretch-size: 1000000 1000
diff --git a/gfx/wr/wrench/reftests/image/yuv-clip.yaml b/gfx/wr/wrench/reftests/image/yuv-clip.yaml
new file mode 100644
index 0000000000..03fe55c3cd
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/yuv-clip.yaml
@@ -0,0 +1,26 @@
+root:
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [0, 0, 1314, 650]
+ - type: yuv-image
+ clip-chain: [2]
+ format: planar
+ src-y: spacex-y.png
+ src-u: spacex-u.png
+ src-v: spacex-v.png
+ bounds: [10, 10, 427, 640]
+
+ - type: yuv-image
+ clip-chain: [2]
+ format: interleaved
+ src: spacex-yuv.png
+ bounds: [447, 10, 427, 640]
+
+ - type: yuv-image
+ clip-chain: [2]
+ format: nv12
+ src-y: spacex-y.png
+ src-uv: spacex-uv.png
+ bounds: [887, 10, 427, 640]
diff --git a/gfx/wr/wrench/reftests/image/yuv.png b/gfx/wr/wrench/reftests/image/yuv.png
new file mode 100644
index 0000000000..66519bea8f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/yuv.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/image/yuv.yaml b/gfx/wr/wrench/reftests/image/yuv.yaml
new file mode 100644
index 0000000000..8120d93f8c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/image/yuv.yaml
@@ -0,0 +1,19 @@
+root:
+ items:
+ - type: yuv-image
+ format: planar
+ src-y: spacex-y.png
+ src-u: spacex-u.png
+ src-v: spacex-v.png
+ bounds: [10, 10, 427, 640]
+
+ - type: yuv-image
+ format: interleaved
+ src: spacex-yuv.png
+ bounds: [447, 10, 427, 640]
+
+ - type: yuv-image
+ format: nv12
+ src-y: spacex-y.png
+ src-uv: spacex-uv.png
+ bounds: [887, 10, 427, 640]
diff --git a/gfx/wr/wrench/reftests/mask/aligned-layer-rect-ref.yaml b/gfx/wr/wrench/reftests/mask/aligned-layer-rect-ref.yaml
new file mode 100644
index 0000000000..d4ef946d7c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/aligned-layer-rect-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: stacking-context
+ items:
+ - type: rect
+ bounds: [9, 9, 10, 10]
+ color: blue
diff --git a/gfx/wr/wrench/reftests/mask/aligned-layer-rect.yaml b/gfx/wr/wrench/reftests/mask/aligned-layer-rect.yaml
new file mode 100644
index 0000000000..7c2628b008
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/aligned-layer-rect.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: scroll-frame
+ id: 2
+ bounds: [9, 9, 10, 10]
+ content-size: [95, 88]
+ clip-to-frame: true
+ items:
+ - type: scroll-frame
+ bounds: [0, 0, 100, 100]
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: blue
+ clip-chain: [2]
+ \ No newline at end of file
diff --git a/gfx/wr/wrench/reftests/mask/checkerboard-tiling.yaml b/gfx/wr/wrench/reftests/mask/checkerboard-tiling.yaml
new file mode 100644
index 0000000000..db9f115d92
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/checkerboard-tiling.yaml
@@ -0,0 +1,18 @@
+# Tests a tiled image mask with leftover tile offsets.
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ image-mask:
+ image: transparent-checkerboard(2, 16, 16)
+ rect: [0, 0, 200, 200]
+ repeat: false
+ # This is the point of the test, can't be a divisor of 200.
+ tile-size: 37
+ - type: stacking-context
+ clip-chain: [2]
+ items:
+ - type: rect
+ bounds: [0, 0, 200, 200]
+ color: blue
diff --git a/gfx/wr/wrench/reftests/mask/checkerboard.png b/gfx/wr/wrench/reftests/mask/checkerboard.png
new file mode 100644
index 0000000000..f2c45c3194
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/checkerboard.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/mask/checkerboard.yaml b/gfx/wr/wrench/reftests/mask/checkerboard.yaml
new file mode 100644
index 0000000000..755b4f8556
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/checkerboard.yaml
@@ -0,0 +1,15 @@
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ image-mask:
+ image: transparent-checkerboard(2, 16, 16)
+ rect: [0, 0, 200, 200]
+ repeat: false
+ - type: stacking-context
+ clip-chain: [2]
+ items:
+ - type: rect
+ bounds: [0, 0, 200, 200]
+ color: blue
diff --git a/gfx/wr/wrench/reftests/mask/circle.png b/gfx/wr/wrench/reftests/mask/circle.png
new file mode 100644
index 0000000000..ea5789e44f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/circle.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/mask/green.yaml b/gfx/wr/wrench/reftests/mask/green.yaml
new file mode 100644
index 0000000000..0d3df60582
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/green.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 95, 88]
+ color: green
diff --git a/gfx/wr/wrench/reftests/mask/mask-atomicity-ref.yaml b/gfx/wr/wrench/reftests/mask/mask-atomicity-ref.yaml
new file mode 100644
index 0000000000..81480856c0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/mask-atomicity-ref.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [25, 25, 100, 100]
+ color: red
+ - type: stacking-context
+ bounds: [0, 0, 200, 200]
+ filters:
+ - "opacity(0.5)"
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: blue
+ - type: rect
+ bounds: [50, 50, 100, 100]
+ color: green
diff --git a/gfx/wr/wrench/reftests/mask/mask-atomicity-tiling.yaml b/gfx/wr/wrench/reftests/mask/mask-atomicity-tiling.yaml
new file mode 100644
index 0000000000..17d779d779
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/mask-atomicity-tiling.yaml
@@ -0,0 +1,28 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [25, 25, 100, 100]
+ color: red
+ - type: clip
+ id: 2
+ image-mask:
+ # premultiplied 0.5 alpha white(??)
+ image: solid-color(127,127,127,127,200,200)
+ rect: [0, 0, 200, 200]
+ tile-size: 10
+ repeat: false
+ -
+ type: "clip-chain"
+ id: 3
+ clips: [2]
+ - type: stacking-context
+ bounds: [0, 0, 200, 200]
+ clip-chain: 3
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: blue
+ - type: rect
+ bounds: [50, 50, 100, 100]
+ color: green
diff --git a/gfx/wr/wrench/reftests/mask/mask-atomicity.yaml b/gfx/wr/wrench/reftests/mask/mask-atomicity.yaml
new file mode 100644
index 0000000000..26a3e9cede
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/mask-atomicity.yaml
@@ -0,0 +1,27 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [25, 25, 100, 100]
+ color: red
+ - type: clip
+ id: 2
+ image-mask:
+ # premultiplied 0.5 alpha white(??)
+ image: solid-color(127,127,127,127,200,200)
+ rect: [0, 0, 200, 200]
+ repeat: false
+ -
+ type: "clip-chain"
+ id: 3
+ clips: [2]
+ - type: stacking-context
+ bounds: [0, 0, 200, 200]
+ clip-chain: 3
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: blue
+ - type: rect
+ bounds: [50, 50, 100, 100]
+ color: green
diff --git a/gfx/wr/wrench/reftests/mask/mask-perspective-tiling.yaml b/gfx/wr/wrench/reftests/mask/mask-perspective-tiling.yaml
new file mode 100644
index 0000000000..fd297c5dfa
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/mask-perspective-tiling.yaml
@@ -0,0 +1,25 @@
+---
+root:
+ items:
+ -
+ type: stacking-context
+ perspective: 1
+ perspective-origin: 0 0
+ items:
+ -
+ type: stacking-context
+ transform: 10 0 0 0 0 10 0 0 0 0 10 0 0 0 -9 1
+ items:
+ - type: clip
+ id: 2
+ image-mask:
+ image: "mask.png"
+ rect: [0, 0, 35, 35]
+ repeat: false
+ tile-size: 5
+ - type: stacking-context
+ clip-chain: [2]
+ items:
+ - type: rect
+ bounds: [0, 0, 95, 88]
+ color: blue
diff --git a/gfx/wr/wrench/reftests/mask/mask-perspective.png b/gfx/wr/wrench/reftests/mask/mask-perspective.png
new file mode 100644
index 0000000000..425872c290
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/mask-perspective.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/mask/mask-perspective.yaml b/gfx/wr/wrench/reftests/mask/mask-perspective.yaml
new file mode 100644
index 0000000000..24f7ba417e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/mask-perspective.yaml
@@ -0,0 +1,24 @@
+---
+root:
+ items:
+ -
+ type: stacking-context
+ perspective: 1
+ perspective-origin: 0 0
+ items:
+ -
+ type: stacking-context
+ transform: 10 0 0 0 0 10 0 0 0 0 10 0 0 0 -9 1
+ items:
+ - type: clip
+ id: 2
+ image-mask:
+ image: "mask.png"
+ rect: [0, 0, 35, 35]
+ repeat: false
+ - type: stacking-context
+ clip-chain: [2]
+ items:
+ - type: rect
+ bounds: [0, 0, 95, 88]
+ color: blue
diff --git a/gfx/wr/wrench/reftests/mask/mask-ref.yaml b/gfx/wr/wrench/reftests/mask/mask-ref.yaml
new file mode 100644
index 0000000000..01c4e60946
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/mask-ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 95, 88]
+ items:
+ - type: rect
+ bounds: [9, 9, 10, 10]
+ color: blue
diff --git a/gfx/wr/wrench/reftests/mask/mask-tiling.yaml b/gfx/wr/wrench/reftests/mask/mask-tiling.yaml
new file mode 100644
index 0000000000..711095b924
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/mask-tiling.yaml
@@ -0,0 +1,16 @@
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ image-mask:
+ image: "mask.png"
+ rect: [0, 0, 35, 35]
+ repeat: false
+ tile-size: 5
+ - type: stacking-context
+ clip-chain: [2]
+ items:
+ - type: rect
+ bounds: [0, 0, 95, 88]
+ color: blue
diff --git a/gfx/wr/wrench/reftests/mask/mask-transformed-to-empty-rect-ref.yaml b/gfx/wr/wrench/reftests/mask/mask-transformed-to-empty-rect-ref.yaml
new file mode 100644
index 0000000000..1df66453d6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/mask-transformed-to-empty-rect-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: 0 255 0 1.0
diff --git a/gfx/wr/wrench/reftests/mask/mask-transformed-to-empty-rect.yaml b/gfx/wr/wrench/reftests/mask/mask-transformed-to-empty-rect.yaml
new file mode 100644
index 0000000000..739e54fa5b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/mask-transformed-to-empty-rect.yaml
@@ -0,0 +1,25 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 300, 300]
+ "scroll-policy": scrollable
+ z-index: 4
+ transform: [1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 150, -150, 1]
+ items:
+ - type: clip
+ # This image mask here assures that we will be forced to try to
+ # mask instead of skipping it due to the mask rect becoming a
+ # zero rect.
+ image-mask:
+ image: "tiny-check-mask.png"
+ rect: [0, 0, 300, 300]
+ repeat: false
+ id: 2
+ - type: rect
+ clip-chain: [2]
+ bounds: [0, 0, 300, 300]
+ color: 0 128 0 1.0000
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: 0 255 0 1.0
diff --git a/gfx/wr/wrench/reftests/mask/mask.png b/gfx/wr/wrench/reftests/mask/mask.png
new file mode 100644
index 0000000000..ab1bdb9b50
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/mask.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/mask/mask.yaml b/gfx/wr/wrench/reftests/mask/mask.yaml
new file mode 100644
index 0000000000..65b3127621
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/mask.yaml
@@ -0,0 +1,15 @@
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ image-mask:
+ image: "mask.png"
+ rect: [0, 0, 35, 35]
+ repeat: false
+ - type: stacking-context
+ clip-chain: [2]
+ items:
+ - type: rect
+ bounds: [0, 0, 95, 88]
+ color: blue
diff --git a/gfx/wr/wrench/reftests/mask/missing-mask-ref.yaml b/gfx/wr/wrench/reftests/mask/missing-mask-ref.yaml
new file mode 100644
index 0000000000..112da9c9d1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/missing-mask-ref.yaml
@@ -0,0 +1,7 @@
+# Don't crash when supplied an invalid image key for the mask!
+---
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 35, 35]
+ color: blue
diff --git a/gfx/wr/wrench/reftests/mask/missing-mask.yaml b/gfx/wr/wrench/reftests/mask/missing-mask.yaml
new file mode 100644
index 0000000000..69a1c72774
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/missing-mask.yaml
@@ -0,0 +1,14 @@
+# Don't crash when supplied an invalid image key for the mask!
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ image-mask:
+ image: invalid
+ rect: [0, 0, 35, 35]
+ repeat: false
+ - type: rect
+ bounds: [0, 0, 95, 88]
+ color: blue
+ clip-chain: [2]
diff --git a/gfx/wr/wrench/reftests/mask/nested-mask-ref.yaml b/gfx/wr/wrench/reftests/mask/nested-mask-ref.yaml
new file mode 100644
index 0000000000..c16df79e4c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/nested-mask-ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 95, 88]
+ items:
+ - type: rect
+ bounds: [13, 13, 6, 6]
+ color: blue
diff --git a/gfx/wr/wrench/reftests/mask/nested-mask-tiling.yaml b/gfx/wr/wrench/reftests/mask/nested-mask-tiling.yaml
new file mode 100644
index 0000000000..b096021380
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/nested-mask-tiling.yaml
@@ -0,0 +1,23 @@
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ image-mask:
+ image: "mask.png"
+ rect: [0, 0, 35, 35]
+ repeat: false
+ tile-size: 5
+ - type: clip
+ id: 3
+ image-mask:
+ image: "mask.png"
+ rect: [4, 4, 35, 35]
+ repeat: false
+ tile-size: 5
+ - type: stacking-context
+ clip-chain: [2, 3]
+ items:
+ - type: rect
+ bounds: [0, 0, 95, 88]
+ color: blue
diff --git a/gfx/wr/wrench/reftests/mask/nested-mask.yaml b/gfx/wr/wrench/reftests/mask/nested-mask.yaml
new file mode 100644
index 0000000000..3a836f7bf1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/nested-mask.yaml
@@ -0,0 +1,21 @@
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ image-mask:
+ image: "mask.png"
+ rect: [0, 0, 35, 35]
+ repeat: false
+ - type: clip
+ id: 3
+ image-mask:
+ image: "mask.png"
+ rect: [4, 4, 35, 35]
+ repeat: false
+ - type: stacking-context
+ clip-chain: [2, 3]
+ items:
+ - type: rect
+ bounds: [0, 0, 95, 88]
+ color: blue
diff --git a/gfx/wr/wrench/reftests/mask/out-of-bounds.yaml b/gfx/wr/wrench/reftests/mask/out-of-bounds.yaml
new file mode 100644
index 0000000000..df478957df
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/out-of-bounds.yaml
@@ -0,0 +1,16 @@
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [0, 0, 10000, 10000]
+ radius:
+ top-left: [30, 30]
+ top-right: [30, 30]
+ bottom-right: [30, 30]
+ bottom-left: [30, 30]
+ - type: rect
+ bounds: [0, 0, 10000, 10000]
+ color: green
+ clip-chain: [2]
diff --git a/gfx/wr/wrench/reftests/mask/reftest.list b/gfx/wr/wrench/reftests/mask/reftest.list
new file mode 100644
index 0000000000..8119f4a830
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/reftest.list
@@ -0,0 +1,17 @@
+fuzzy(1,10) == mask.yaml mask-ref.yaml
+fuzzy(1,10) == mask-tiling.yaml mask-ref.yaml
+fuzzy(1,10) == nested-mask.yaml nested-mask-ref.yaml
+== nested-mask-tiling.yaml nested-mask-ref.yaml
+!= mask.yaml green.yaml
+== aligned-layer-rect.yaml aligned-layer-rect-ref.yaml
+== mask-transformed-to-empty-rect.yaml mask-transformed-to-empty-rect-ref.yaml
+platform(linux,mac) == rounded-corners.yaml rounded-corners.png
+!= mask.yaml out-of-bounds.yaml
+platform(linux,mac) fuzzy(1,17500) color_targets(3) alpha_targets(1) == mask-atomicity.yaml mask-atomicity-ref.yaml
+platform(linux,mac) fuzzy(1,17500) == mask-atomicity-tiling.yaml mask-atomicity-ref.yaml
+platform(linux,mac) == mask-perspective.yaml mask-perspective.png
+== fuzzy(1,11) mask-perspective-tiling.yaml mask-perspective.yaml
+platform(linux,mac) == checkerboard.yaml checkerboard.png
+skip_on(android,device) fuzzy(2,1900) == checkerboard.yaml checkerboard-tiling.yaml # Fails on a Pixel2
+== missing-mask.yaml missing-mask-ref.yaml
+platform(linux) == scaled-filter-raster-root.yaml scaled-filter-raster-root.png
diff --git a/gfx/wr/wrench/reftests/mask/rounded-corners.png b/gfx/wr/wrench/reftests/mask/rounded-corners.png
new file mode 100644
index 0000000000..4f8d1d0fa0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/rounded-corners.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/mask/rounded-corners.yaml b/gfx/wr/wrench/reftests/mask/rounded-corners.yaml
new file mode 100644
index 0000000000..1cedea53bc
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/rounded-corners.yaml
@@ -0,0 +1,24 @@
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [0, 0, 100, 100]
+ radius:
+ top-left: [30, 30]
+ top-right: [30, 30]
+ bottom-right: [30, 30]
+ bottom-left: [30, 30]
+ - type: border
+ clip-chain: [2]
+ bounds: [0, 0, 100, 100]
+ width: [5, 5, 5, 5]
+ border-type: normal
+ style: [solid, solid, solid, solid]
+ color: [blue, blue, blue, blue]
+ radius:
+ top-left: [30, 30]
+ top-right: [30, 30]
+ bottom-right: [0, 0]
+ bottom-left: [0, 0]
diff --git a/gfx/wr/wrench/reftests/mask/scaled-filter-raster-root.png b/gfx/wr/wrench/reftests/mask/scaled-filter-raster-root.png
new file mode 100644
index 0000000000..e9cf49553e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/scaled-filter-raster-root.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/mask/scaled-filter-raster-root.yaml b/gfx/wr/wrench/reftests/mask/scaled-filter-raster-root.yaml
new file mode 100644
index 0000000000..77925dacff
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/scaled-filter-raster-root.yaml
@@ -0,0 +1,30 @@
+# Verify that a tiled clip mask is correctly applied to a surface that
+# is both a raster root and has a device-pixel ratio different than the
+# primary surface (due to the scale transform).
+---
+root:
+ items:
+ - type: stacking-context
+ transform-origin: [0, 0]
+ transform: scale(20)
+ items:
+ - type: clip
+ id: 2
+ image-mask:
+ image: "circle.png"
+ rect: [10, 10, 10, 10]
+ repeat: false
+ tile-size: 200
+ -
+ type: "clip-chain"
+ id: 3
+ clips: [2]
+ - type: stacking-context
+ filters: [opacity(0.5)]
+ items:
+ - type: stacking-context
+ clip-chain: 3
+ items:
+ - type: rect
+ bounds: [10, 10, 10, 10]
+ color: blue
diff --git a/gfx/wr/wrench/reftests/mask/tiny-check-mask.png b/gfx/wr/wrench/reftests/mask/tiny-check-mask.png
new file mode 100644
index 0000000000..d6f6e3548d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/tiny-check-mask.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/performance/compositor-surface-opaque-slice-ref.yaml b/gfx/wr/wrench/reftests/performance/compositor-surface-opaque-slice-ref.yaml
new file mode 100644
index 0000000000..7c538488bc
--- /dev/null
+++ b/gfx/wr/wrench/reftests/performance/compositor-surface-opaque-slice-ref.yaml
@@ -0,0 +1,9 @@
+root:
+ items:
+ -
+ bounds: [0, 0, 200, 200]
+ image: checkerboard(2, 16, 12)
+ stretch-size: 200 200
+ - type: rect
+ bounds: [0, 0, 200, 200]
+ color: [255, 0, 0, 0.5]
diff --git a/gfx/wr/wrench/reftests/performance/compositor-surface-opaque-slice.yaml b/gfx/wr/wrench/reftests/performance/compositor-surface-opaque-slice.yaml
new file mode 100644
index 0000000000..a5821973e9
--- /dev/null
+++ b/gfx/wr/wrench/reftests/performance/compositor-surface-opaque-slice.yaml
@@ -0,0 +1,10 @@
+root:
+ items:
+ -
+ bounds: [0, 0, 200, 200]
+ image: checkerboard(2, 16, 12)
+ stretch-size: 200 200
+ prefer-compositor-surface: true
+ - type: rect
+ bounds: [0, 0, 200, 200]
+ color: [255, 0, 0, 0.5]
diff --git a/gfx/wr/wrench/reftests/performance/no-clip-mask.png b/gfx/wr/wrench/reftests/performance/no-clip-mask.png
new file mode 100644
index 0000000000..0b1bfca755
--- /dev/null
+++ b/gfx/wr/wrench/reftests/performance/no-clip-mask.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/performance/no-clip-mask.yaml b/gfx/wr/wrench/reftests/performance/no-clip-mask.yaml
new file mode 100644
index 0000000000..51d981e2a0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/performance/no-clip-mask.yaml
@@ -0,0 +1,24 @@
+# In this case, there is no incompatible transform.
+# Therefore, the clip condition should be handled
+# by the vertex shader via the local_clip_rect, and
+# there should be no clip mask generated.
+---
+root:
+ items:
+ -
+ type: "stacking-context"
+ items:
+ -
+ bounds: [0, 111, 1087, 565]
+ "clip-rect": [0, 111, 1087, 565]
+ type: iframe
+ id: [1, 2]
+pipelines:
+ -
+ id: [1, 2]
+ items:
+ -
+ bounds: [1075, -1, 12, 199]
+ "clip-rect": [1075, -1, 12, 199]
+ image: checkerboard(4, 8, 8)
+ stretch-size: 72 72
diff --git a/gfx/wr/wrench/reftests/performance/reftest.list b/gfx/wr/wrench/reftests/performance/reftest.list
new file mode 100644
index 0000000000..5ca69146dc
--- /dev/null
+++ b/gfx/wr/wrench/reftests/performance/reftest.list
@@ -0,0 +1,2 @@
+skip_on(android) == color_targets(2) alpha_targets(0) no-clip-mask.yaml no-clip-mask.png # Too wide for Android
+platform(linux,mac) == compositor-surface-opaque-slice.yaml compositor-surface-opaque-slice-ref.yaml
diff --git a/gfx/wr/wrench/reftests/reftest.list b/gfx/wr/wrench/reftests/reftest.list
new file mode 100644
index 0000000000..99d20f3beb
--- /dev/null
+++ b/gfx/wr/wrench/reftests/reftest.list
@@ -0,0 +1,19 @@
+include aa/reftest.list
+include backface/reftest.list
+include blend/reftest.list
+include border/reftest.list
+include boxshadow/reftest.list
+include clip/reftest.list
+include compositor-surface/reftest.list
+include crash/reftest.list
+include filters/reftest.list
+include gradient/reftest.list
+include image/reftest.list
+include mask/reftest.list
+include performance/reftest.list
+include scrolling/reftest.list
+include snap/reftest.list
+include split/reftest.list
+include text/reftest.list
+include transforms/reftest.list
+include tiles/reftest.list
diff --git a/gfx/wr/wrench/reftests/scrolling/ancestor-scroll-frames.yaml b/gfx/wr/wrench/reftests/scrolling/ancestor-scroll-frames.yaml
new file mode 100644
index 0000000000..9d5c1d006e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/ancestor-scroll-frames.yaml
@@ -0,0 +1,37 @@
+# Test that we don't select a nested scroll frame as a scroll root and include
+# primitives that are positioned by ancestors of that scroll root, when creating
+# a tile cache for a blend container. This test will cause a panic in
+# `get_relative_transform_with_face` otherwise.
+---
+root:
+ items:
+ - type: stacking-context
+ blend-container: true
+ items:
+ - type: scroll-frame
+ bounds: [0, 0, 200, 200]
+ content-size: [200, 400]
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 200, 200]
+ transform: rotate(45)
+ items:
+ - type: rect
+ bounds: [0, 0, 200, 200]
+ color: red
+ - type: scroll-frame
+ bounds: [200, 0, 400, 200]
+ content-size: [400, 200]
+ items:
+ - type: scroll-frame
+ bounds: [200, 0, 400, 200]
+ content-size: [400, 400]
+ items:
+ - type: rect
+ bounds: [200, 0, 100, 200]
+ color: green
+ backface-visible: true
+ - type: rect
+ bounds: [300, 0, 100, 200]
+ color: blue
+ backface-visible: false
diff --git a/gfx/wr/wrench/reftests/scrolling/blank.yaml b/gfx/wr/wrench/reftests/scrolling/blank.yaml
new file mode 100644
index 0000000000..c4eb3ab673
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/blank.yaml
@@ -0,0 +1,2 @@
+---
+root:
diff --git a/gfx/wr/wrench/reftests/scrolling/clip-and-scroll-property-ref.yaml b/gfx/wr/wrench/reftests/scrolling/clip-and-scroll-property-ref.yaml
new file mode 100644
index 0000000000..a465f412df
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/clip-and-scroll-property-ref.yaml
@@ -0,0 +1,5 @@
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 200, 200]
+ color: green
diff --git a/gfx/wr/wrench/reftests/scrolling/clip-and-scroll-property.yaml b/gfx/wr/wrench/reftests/scrolling/clip-and-scroll-property.yaml
new file mode 100644
index 0000000000..43352236f5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/clip-and-scroll-property.yaml
@@ -0,0 +1,33 @@
+---
+root:
+ items:
+ -
+ bounds: [0, 0, 200, 200]
+ type: "stacking-context"
+ "scroll-policy": scrollable
+ items:
+ -
+ bounds: [0, 0, 200, 200]
+ type: clip
+ id: 2
+ - type: clip-chain
+ id: 12
+ clips: [2]
+ # Here we are testing that the clip-chain property applies to
+ # both stacking contexts and items.
+ -
+ bounds: [0, 0, 0, 0]
+ content-size: [200, 200]
+ clip-chain: 12
+ type: "stacking-context"
+ "scroll-policy": scrollable
+ transform: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -100, 0, 0, 1]
+ "transform-style": flat
+ items:
+ -
+ bounds: [100, 0, 200, 200]
+ clip: [-8947849, -8947849, 17895698, 17895698]
+ type: rect
+ color: green
+ id: [0, 0]
+pipelines: []
diff --git a/gfx/wr/wrench/reftests/scrolling/empty-mask-ref.yaml b/gfx/wr/wrench/reftests/scrolling/empty-mask-ref.yaml
new file mode 100644
index 0000000000..aca8ccd66c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/empty-mask-ref.yaml
@@ -0,0 +1,5 @@
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: green
diff --git a/gfx/wr/wrench/reftests/scrolling/empty-mask.yaml b/gfx/wr/wrench/reftests/scrolling/empty-mask.yaml
new file mode 100644
index 0000000000..2fe90d6338
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/empty-mask.yaml
@@ -0,0 +1,20 @@
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: green
+ - type: scroll-frame
+ bounds: [0, 0, 100, 100]
+ items:
+ - type: clip
+ id: 2
+ bounds: [0, 0, 0, 0]
+ - type: clip
+ id: 3
+ complex:
+ - rect: [0, 0, 100, 100]
+ radius: 20
+ - type: rect
+ bounds: [0, 0, 500, 500]
+ color: red
+ clip-chain: [2, 3]
diff --git a/gfx/wr/wrench/reftests/scrolling/ext-scroll-offset-1-ref.yaml b/gfx/wr/wrench/reftests/scrolling/ext-scroll-offset-1-ref.yaml
new file mode 100644
index 0000000000..3b8d47e94c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/ext-scroll-offset-1-ref.yaml
@@ -0,0 +1,10 @@
+root:
+ items:
+ - type: scroll-frame
+ bounds: [0, 0, 100, 100]
+ content-size: [100, 1000]
+ scroll-offset: [0, -50]
+ items:
+ - type: rect
+ color: green
+ bounds: [0, 0, 100, 100]
diff --git a/gfx/wr/wrench/reftests/scrolling/ext-scroll-offset-1.yaml b/gfx/wr/wrench/reftests/scrolling/ext-scroll-offset-1.yaml
new file mode 100644
index 0000000000..7803574bed
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/ext-scroll-offset-1.yaml
@@ -0,0 +1,14 @@
+# Test that items with an external scroll offset in both the
+# scroll node and primitive match up with items that have
+# been scrolled without an external scroll offset.
+root:
+ items:
+ - type: scroll-frame
+ bounds: [0, 0, 100, 100]
+ content-size: [100, 1000]
+ scroll-offset: [0, 0]
+ external-scroll-offset: [0, 50]
+ items:
+ - type: rect
+ color: green
+ bounds: [0, 50, 100, 100]
diff --git a/gfx/wr/wrench/reftests/scrolling/fixed-position-ref.yaml b/gfx/wr/wrench/reftests/scrolling/fixed-position-ref.yaml
new file mode 100644
index 0000000000..aa26387828
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/fixed-position-ref.yaml
@@ -0,0 +1,14 @@
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: green
+ - type: rect
+ bounds: [60, 0, 50, 50]
+ color: green
+ - type: rect
+ bounds: [120, 0, 50, 50]
+ color: green
+ - type: rect
+ bounds: [180, 0, 50, 50]
+ color: green
diff --git a/gfx/wr/wrench/reftests/scrolling/fixed-position-scrolling-clip-ref.yaml b/gfx/wr/wrench/reftests/scrolling/fixed-position-scrolling-clip-ref.yaml
new file mode 100644
index 0000000000..eb50353314
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/fixed-position-scrolling-clip-ref.yaml
@@ -0,0 +1,5 @@
+root:
+ items:
+ - type: rect
+ bounds: [10, 10, 50, 50]
+ color: green
diff --git a/gfx/wr/wrench/reftests/scrolling/fixed-position-scrolling-clip.yaml b/gfx/wr/wrench/reftests/scrolling/fixed-position-scrolling-clip.yaml
new file mode 100644
index 0000000000..bdf537409e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/fixed-position-scrolling-clip.yaml
@@ -0,0 +1,33 @@
+root:
+ items:
+ - type: scroll-frame
+ bounds: [10, 10, 100, 300]
+ content-size: [100, 700]
+ id: 41
+ scroll-offset: [0, 50]
+ items:
+ # The rectangles below should stay in place even when the parent scroll area scrolls,
+ # because they use the root reference frame as their scroll node (fixed position).
+ # On the other hand, the clip item here will scroll with its parent scroll area. Normally
+ # fixed position items would only be clipped by their reference frame (in this case the
+ # root), but since these items specify an auxiliary clip, they will be clipped by their
+ # sibling clip (42).
+ - type: clip
+ bounds: [10, 60, 50, 50]
+ id: 42
+ - type: clip-chain
+ id: 142
+ clips: [42]
+ - type: stacking-context
+ bounds: [10, 10, 100, 100]
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 50]
+ color: green
+ clip-chain: 142
+ spatial-id: root-reference-frame
+ - type: rect
+ bounds: [0, 50, 100, 50]
+ color: red
+ clip-chain: 142
+ spatial-id: root-reference-frame
diff --git a/gfx/wr/wrench/reftests/scrolling/fixed-position.yaml b/gfx/wr/wrench/reftests/scrolling/fixed-position.yaml
new file mode 100644
index 0000000000..c56f1378cd
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/fixed-position.yaml
@@ -0,0 +1,52 @@
+root:
+ bounds: [0, 0, 1024, 10000]
+ scroll-offset: [0, 100]
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 50, 50]
+ items:
+ # This item should not scroll out of view because it is fixed position.
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: green
+ spatial-id: root-reference-frame
+ - type: stacking-context
+ bounds: [0, 0, 50, 50]
+ transform: translate(60, 100)
+ id: 100
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 50, 50]
+ items:
+ # Even though there is a custom clip-scroll ID, it should scroll,
+ # because it is fixed relative to its reference frame. The reference frame
+ # of this stacking context is the stacking context parent because it has
+ # a transformation.
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: green
+ - type: stacking-context
+ bounds: [120, 0, 50, 200]
+ transform: translate(0, 0)
+ id: 101
+ items:
+ # This is similar to the previous case, but ensures that this still works
+ # even with an identity transform.
+ - type: stacking-context
+ bounds: [0, 0, 50, 200]
+ items:
+ - type: rect
+ bounds: [0, 100, 50, 50]
+ color: green
+ - type: stacking-context
+ bounds: [180, 0, 50, 200]
+ perspective: 1
+ id: 102
+ items:
+ # This is similar to the previous case, but for perspective.
+ - type: stacking-context
+ bounds: [0, 0, 50, 200]
+ items:
+ - type: rect
+ bounds: [0, 100, 50, 50]
+ color: green
diff --git a/gfx/wr/wrench/reftests/scrolling/mask.png b/gfx/wr/wrench/reftests/scrolling/mask.png
new file mode 100644
index 0000000000..d3cbfe6a63
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/mask.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/scrolling/nested-scroll-offset-ref.yaml b/gfx/wr/wrench/reftests/scrolling/nested-scroll-offset-ref.yaml
new file mode 100644
index 0000000000..c2f0a30710
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/nested-scroll-offset-ref.yaml
@@ -0,0 +1,5 @@
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: green
diff --git a/gfx/wr/wrench/reftests/scrolling/nested-scroll-offset.yaml b/gfx/wr/wrench/reftests/scrolling/nested-scroll-offset.yaml
new file mode 100644
index 0000000000..850468f180
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/nested-scroll-offset.yaml
@@ -0,0 +1,16 @@
+root:
+ items:
+ - type: scroll-frame
+ bounds: [0, 0, 500, 500]
+ content-size: [1000, 1000]
+ scroll-offset: [0, 300]
+ items:
+ - type: scroll-frame
+ bounds: [0, 300, 50, 50]
+ items:
+ - type: scroll-frame
+ bounds: [0, 300, 50, 50]
+ items:
+ - type: rect
+ bounds: [0, 300, 50, 50]
+ color: green
diff --git a/gfx/wr/wrench/reftests/scrolling/nested-stickys-ref.yaml b/gfx/wr/wrench/reftests/scrolling/nested-stickys-ref.yaml
new file mode 100644
index 0000000000..869ad7e9e1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/nested-stickys-ref.yaml
@@ -0,0 +1,11 @@
+root:
+ items:
+ # This is a scroll frame with an out-of-viewport rect that should be pushed into the
+ # viewport by its "bottom" sticky constraint.
+ - type: scroll-frame
+ bounds: [10, 10, 50, 50]
+ content-size: [200, 200]
+ items:
+ - type: rect
+ bounds: [10, 20, 50, 50]
+ color: green
diff --git a/gfx/wr/wrench/reftests/scrolling/nested-stickys.yaml b/gfx/wr/wrench/reftests/scrolling/nested-stickys.yaml
new file mode 100644
index 0000000000..2082bb2576
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/nested-stickys.yaml
@@ -0,0 +1,20 @@
+root:
+ items:
+ - type: scroll-frame
+ bounds: [10, 10, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [0, 30]
+ items:
+ - type: sticky-frame
+ bounds: [10, 30, 50, 50]
+ margin-top: 5
+ vertical-offset-bounds: [0, 500]
+ items:
+ - type: sticky-frame
+ bounds: [10, 30, 50, 50]
+ margin-top: 10
+ vertical-offset-bounds: [0, 500]
+ items:
+ - type: rect
+ bounds: [10, 30, 50, 50]
+ color: green
diff --git a/gfx/wr/wrench/reftests/scrolling/out-of-bounds-scroll-ref.yaml b/gfx/wr/wrench/reftests/scrolling/out-of-bounds-scroll-ref.yaml
new file mode 100644
index 0000000000..eb50353314
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/out-of-bounds-scroll-ref.yaml
@@ -0,0 +1,5 @@
+root:
+ items:
+ - type: rect
+ bounds: [10, 10, 50, 50]
+ color: green
diff --git a/gfx/wr/wrench/reftests/scrolling/out-of-bounds-scroll.yaml b/gfx/wr/wrench/reftests/scrolling/out-of-bounds-scroll.yaml
new file mode 100644
index 0000000000..8c87bd6f5c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/out-of-bounds-scroll.yaml
@@ -0,0 +1,10 @@
+root:
+ items:
+ - type: scroll-frame
+ bounds: [10, 10, 50, 100]
+ content-size: [50, 200]
+ scroll-offset: [0, 50]
+ items:
+ - type: rect
+ bounds: [10, 60, 50, 50]
+ color: green
diff --git a/gfx/wr/wrench/reftests/scrolling/reftest.list b/gfx/wr/wrench/reftests/scrolling/reftest.list
new file mode 100644
index 0000000000..4a8b074e58
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/reftest.list
@@ -0,0 +1,25 @@
+== empty-mask.yaml empty-mask-ref.yaml
+== fixed-position-scrolling-clip.yaml fixed-position-scrolling-clip-ref.yaml
+== fixed-position.yaml fixed-position-ref.yaml
+== nested-scroll-offset.yaml nested-scroll-offset-ref.yaml
+== out-of-bounds-scroll.yaml out-of-bounds-scroll-ref.yaml
+== root-scroll.yaml root-scroll-ref.yaml
+== scroll-layer.yaml scroll-layer-ref.yaml
+== simple.yaml simple-ref.yaml
+== clip-and-scroll-property.yaml clip-and-scroll-property-ref.yaml
+== translate-nested.yaml translate-nested-ref.yaml
+== sticky.yaml sticky-ref.yaml
+== sticky-nested.yaml sticky-ref.yaml
+== sticky-applied.yaml sticky-applied-ref.yaml
+== sticky-transformed.yaml sticky-transformed-ref.yaml
+== sibling-hidden-clip.yaml sibling-hidden-clip-ref.yaml
+== scale-offsets.yaml scale-offsets-ref.yaml
+== nested-stickys.yaml nested-stickys-ref.yaml
+== viewport-offset.yaml viewport-offset-ref.yaml
+== ext-scroll-offset-1.yaml ext-scroll-offset-1-ref.yaml
+== scroll-frame-order.yaml scroll-frame-order-ref.yaml
+!= ancestor-scroll-frames.yaml blank.yaml
+== scroll-generation-1.yaml scroll-generation-ref.yaml
+== scroll-generation-2.yaml scroll-generation-ref.yaml
+== scroll-generation-3.yaml scroll-generation-ref.yaml
+== scroll-generation-4.yaml scroll-generation-ref.yaml
diff --git a/gfx/wr/wrench/reftests/scrolling/root-scroll-ref.yaml b/gfx/wr/wrench/reftests/scrolling/root-scroll-ref.yaml
new file mode 100644
index 0000000000..d49f063d3a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/root-scroll-ref.yaml
@@ -0,0 +1,6 @@
+root:
+ bounds: [0, 0, 1024, 10000]
+ items:
+ - type: rect
+ bounds: [10, 10, 50, 50]
+ color: green
diff --git a/gfx/wr/wrench/reftests/scrolling/root-scroll.yaml b/gfx/wr/wrench/reftests/scrolling/root-scroll.yaml
new file mode 100644
index 0000000000..d95c7debe1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/root-scroll.yaml
@@ -0,0 +1,7 @@
+root:
+ bounds: [0, 0, 1024, 10000]
+ scroll-offset: [0, 100]
+ items:
+ - type: rect
+ bounds: [10, 110, 50, 50]
+ color: green
diff --git a/gfx/wr/wrench/reftests/scrolling/scale-offsets-ref.yaml b/gfx/wr/wrench/reftests/scrolling/scale-offsets-ref.yaml
new file mode 100644
index 0000000000..9276ded8f8
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/scale-offsets-ref.yaml
@@ -0,0 +1,5 @@
+root:
+ items:
+ - type: rect
+ color: green
+ bounds: [50, 50, 50, 50]
diff --git a/gfx/wr/wrench/reftests/scrolling/scale-offsets.yaml b/gfx/wr/wrench/reftests/scrolling/scale-offsets.yaml
new file mode 100644
index 0000000000..b4a93b8c62
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/scale-offsets.yaml
@@ -0,0 +1,18 @@
+# Test that a scroll-frame nested within a scale
+# transform correctly calculates local offsets.
+root:
+ items:
+ - type: stacking-context
+ transform: scale(0.5)
+ items:
+ - type: scroll-frame
+ id: 2
+ bounds: [100, 100, 100, 100]
+ content-size: [260, 260]
+ scroll-offset: [0, 60]
+ clip-to-frame: true
+ items:
+ - type: rect
+ color: green
+ bounds: [100, 100, 260, 260]
+ clip-chain: [2]
diff --git a/gfx/wr/wrench/reftests/scrolling/scroll-frame-order-ref.yaml b/gfx/wr/wrench/reftests/scrolling/scroll-frame-order-ref.yaml
new file mode 100644
index 0000000000..b29d3ba2b8
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/scroll-frame-order-ref.yaml
@@ -0,0 +1,34 @@
+# Tests that scroll frames can be defined and used in any order.
+
+---
+root:
+ items:
+ - type: scroll-frame
+ bounds: [0, 0, 50, 50]
+ content-size: [50, 50]
+ clip-rect: [0, 0, 50, 50]
+ id: 123
+ - type: scroll-frame
+ bounds: [50, 0, 50, 50]
+ content-size: [50, 50]
+ clip-rect: [50, 0, 50, 50]
+ scroll-offset: [0, -25]
+ id: 456
+ - type: scroll-frame
+ bounds: [100, 0, 50, 50]
+ content-size: [50, 50]
+ clip-rect: [100, 0, 50, 50]
+ scroll-offset: [0, -15]
+ id: 789
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: red
+ spatial-id: 123
+ - type: rect
+ bounds: [50, 0, 50, 50]
+ color: green
+ spatial-id: 456
+ - type: rect
+ bounds: [100, 0, 50, 50]
+ color: blue
+ spatial-id: 789
diff --git a/gfx/wr/wrench/reftests/scrolling/scroll-frame-order.yaml b/gfx/wr/wrench/reftests/scrolling/scroll-frame-order.yaml
new file mode 100644
index 0000000000..2977c04d95
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/scroll-frame-order.yaml
@@ -0,0 +1,35 @@
+# Tests that scroll frames can be defined and used in any order.
+
+---
+root:
+ items:
+ - type: scroll-frame
+ bounds: [0, 0, 50, 50]
+ content-size: [50, 50]
+ clip-rect: [0, 0, 50, 50]
+ id: 123
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: red
+ spatial-id: 123
+ - type: scroll-frame
+ bounds: [50, 0, 50, 50]
+ content-size: [50, 50]
+ clip-rect: [50, 0, 50, 50]
+ scroll-offset: [0, -25]
+ id: 456
+ - type: rect
+ bounds: [50, 0, 50, 50]
+ color: green
+ spatial-id: 456
+ - type: scroll-frame
+ bounds: [100, 0, 50, 50]
+ content-size: [50, 50]
+ clip-rect: [100, 0, 50, 50]
+ scroll-offset: [0, -15]
+ id: 789
+ - type: rect
+ bounds: [100, 0, 50, 50]
+ color: blue
+ spatial-id: 789
+
diff --git a/gfx/wr/wrench/reftests/scrolling/scroll-generation-1.yaml b/gfx/wr/wrench/reftests/scrolling/scroll-generation-1.yaml
new file mode 100644
index 0000000000..8caf080d09
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/scroll-generation-1.yaml
@@ -0,0 +1,18 @@
+# Test that an scroll frame having two different generations' scroll-offsets.
+root:
+ items:
+ - type: scroll-frame
+ bounds: [0, 0, 100, 100]
+ content-size: [100, 1000]
+ scroll-offsets:
+ - offset: [0, 0]
+ generation: 1
+ - offset: [0, -10]
+ generation: 2
+ external-scroll-offset: [0, 50]
+ scroll-generation: 2
+ has-scroll-linked-effect: true
+ items:
+ - type: rect
+ color: green
+ bounds: [0, 50, 100, 100]
diff --git a/gfx/wr/wrench/reftests/scrolling/scroll-generation-2.yaml b/gfx/wr/wrench/reftests/scrolling/scroll-generation-2.yaml
new file mode 100644
index 0000000000..7b48e4f491
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/scroll-generation-2.yaml
@@ -0,0 +1,19 @@
+# Test that an scroll frame having scroll-offsets which don't match the scroll
+# generation on the main-thread.
+root:
+ items:
+ - type: scroll-frame
+ bounds: [0, 0, 100, 100]
+ content-size: [100, 1000]
+ scroll-offsets:
+ - offset: [0, -10]
+ generation: 10
+ - offset: [0, -100]
+ generation: 11
+ external-scroll-offset: [0, 50]
+ scroll-generation: 2
+ has-scroll-linked-effect: true
+ items:
+ - type: rect
+ color: green
+ bounds: [0, 50, 100, 100]
diff --git a/gfx/wr/wrench/reftests/scrolling/scroll-generation-3.yaml b/gfx/wr/wrench/reftests/scrolling/scroll-generation-3.yaml
new file mode 100644
index 0000000000..a52beb8c9c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/scroll-generation-3.yaml
@@ -0,0 +1,20 @@
+# Test that an scroll frame having an invalid scroll generation on the
+# main-thread. The least scroll generation offset should be used, in this case
+# it's 10th generation.
+root:
+ items:
+ - type: scroll-frame
+ bounds: [0, 0, 100, 100]
+ content-size: [100, 1000]
+ scroll-offsets:
+ - offset: [0, -10]
+ generation: 10
+ - offset: [0, -100]
+ generation: 11
+ external-scroll-offset: [0, 50]
+ scroll-generation: 0
+ has-scroll-linked-effect: true
+ items:
+ - type: rect
+ color: green
+ bounds: [0, 50, 100, 100]
diff --git a/gfx/wr/wrench/reftests/scrolling/scroll-generation-4.yaml b/gfx/wr/wrench/reftests/scrolling/scroll-generation-4.yaml
new file mode 100644
index 0000000000..3c6c1c627b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/scroll-generation-4.yaml
@@ -0,0 +1,13 @@
+# Test that an scroll frame having no scroll-offsets, this is kinda crash test.
+root:
+ items:
+ - type: scroll-frame
+ bounds: [0, 0, 100, 100]
+ content-size: [100, 1000]
+ external-scroll-offset: [0, 60]
+ scroll-generation: 2
+ has-scroll-linked-effect: true
+ items:
+ - type: rect
+ color: green
+ bounds: [0, 60, 100, 100]
diff --git a/gfx/wr/wrench/reftests/scrolling/scroll-generation-ref.yaml b/gfx/wr/wrench/reftests/scrolling/scroll-generation-ref.yaml
new file mode 100644
index 0000000000..3b2302992a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/scroll-generation-ref.yaml
@@ -0,0 +1,10 @@
+root:
+ items:
+ - type: scroll-frame
+ bounds: [0, 0, 100, 100]
+ content-size: [100, 1000]
+ external-scroll-offset: [0, 60]
+ items:
+ - type: rect
+ color: green
+ bounds: [0, 60, 100, 100]
diff --git a/gfx/wr/wrench/reftests/scrolling/scroll-layer-ref.yaml b/gfx/wr/wrench/reftests/scrolling/scroll-layer-ref.yaml
new file mode 100644
index 0000000000..c2f0a30710
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/scroll-layer-ref.yaml
@@ -0,0 +1,5 @@
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 50]
+ color: green
diff --git a/gfx/wr/wrench/reftests/scrolling/scroll-layer.yaml b/gfx/wr/wrench/reftests/scrolling/scroll-layer.yaml
new file mode 100644
index 0000000000..4956672a77
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/scroll-layer.yaml
@@ -0,0 +1,10 @@
+root:
+ items:
+ - type: scroll-frame
+ bounds: [0, 0, 100, 100]
+ content-size: [1000, 1000]
+ scroll-offset: [50, 50]
+ items:
+ - type: rect
+ bounds: [50, 50, 50, 50]
+ color: green
diff --git a/gfx/wr/wrench/reftests/scrolling/sibling-hidden-clip-ref.yaml b/gfx/wr/wrench/reftests/scrolling/sibling-hidden-clip-ref.yaml
new file mode 100644
index 0000000000..f722f6f815
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/sibling-hidden-clip-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [10, 10, 40, 70]
+ color: red
+ id: [0, 0]
+pipelines: []
diff --git a/gfx/wr/wrench/reftests/scrolling/sibling-hidden-clip.yaml b/gfx/wr/wrench/reftests/scrolling/sibling-hidden-clip.yaml
new file mode 100644
index 0000000000..9039a4af0d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/sibling-hidden-clip.yaml
@@ -0,0 +1,23 @@
+---
+root:
+ items:
+ -
+ bounds: [0, 0, 200, 200]
+ type: "stacking-context"
+ "scroll-policy": scrollable
+ filters: [opacity(0.0)]
+ items:
+ -
+ bounds: [0, 0, 50, 80]
+ type: clip
+ id: 2
+ - type: clip-chain
+ id: 12
+ clips: [2]
+ - type: rect
+ bounds: [10, 10, 100, 100]
+ color: red
+ clip-chain: 12
+
+ id: [0, 0]
+pipelines: []
diff --git a/gfx/wr/wrench/reftests/scrolling/simple-ref.yaml b/gfx/wr/wrench/reftests/scrolling/simple-ref.yaml
new file mode 100644
index 0000000000..cb9ddda07f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/simple-ref.yaml
@@ -0,0 +1,8 @@
+root:
+ items:
+ - type: rect
+ bounds: [10, 10, 50, 50]
+ color: green
+ - type: rect
+ bounds: [70, 10, 50, 50]
+ color: green
diff --git a/gfx/wr/wrench/reftests/scrolling/simple.yaml b/gfx/wr/wrench/reftests/scrolling/simple.yaml
new file mode 100644
index 0000000000..64a433d83c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/simple.yaml
@@ -0,0 +1,22 @@
+root:
+ items:
+ - type: scroll-frame
+ id: 2
+ bounds: [10, 10, 50, 50]
+ content-size: [100, 100]
+ clip-to-frame: true
+ items:
+ - type: rect
+ bounds: [10, 10, 500, 500]
+ color: green
+ clip-chain: [2]
+ - type: scroll-frame
+ id: 3
+ bounds: [70, 10, 50, 50]
+ content-size: [100, 100]
+ clip-to-frame: true
+ items:
+ - type: rect
+ bounds: [70, 10, 100, 100]
+ color: green
+ clip-chain: [3]
diff --git a/gfx/wr/wrench/reftests/scrolling/sticky-applied-ref.yaml b/gfx/wr/wrench/reftests/scrolling/sticky-applied-ref.yaml
new file mode 100644
index 0000000000..1dd3973c31
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/sticky-applied-ref.yaml
@@ -0,0 +1,70 @@
+root:
+ items:
+ # Reference images are listed in the same order as the test images -
+ # all the "bottom-sticky" ones are first, grouped together.
+ - type: rect
+ bounds: [10, 10, 50, 50]
+ color: green
+ - type: rect
+ bounds: [10, 70, 50, 40]
+ color: green
+ - type: rect
+ bounds: [10, 130, 50, 50]
+ color: green
+ - type: rect
+ bounds: [10, 190, 50, 50]
+ color: green
+ - type: rect
+ bounds: [10, 255, 50, 45]
+ color: green
+
+ # Top sticky
+ - type: rect
+ bounds: [70, 10, 50, 50]
+ color: green
+ - type: rect
+ bounds: [70, 80, 50, 40]
+ color: green
+ - type: rect
+ bounds: [70, 130, 50, 50]
+ color: green
+ - type: rect
+ bounds: [70, 190, 50, 50]
+ color: green
+ - type: rect
+ bounds: [70, 250, 50, 45]
+ color: green
+
+ # Right sticky
+ - type: rect
+ bounds: [130, 10, 50, 50]
+ color: green
+ - type: rect
+ bounds: [130, 70, 40, 50]
+ color: green
+ - type: rect
+ bounds: [130, 130, 50, 50]
+ color: green
+ - type: rect
+ bounds: [130, 190, 50, 50]
+ color: green
+ - type: rect
+ bounds: [135, 250, 45, 50]
+ color: green
+
+ # Left sticky
+ - type: rect
+ bounds: [190, 10, 50, 50]
+ color: green
+ - type: rect
+ bounds: [200, 70, 40, 50]
+ color: green
+ - type: rect
+ bounds: [190, 130, 50, 50]
+ color: green
+ - type: rect
+ bounds: [190, 190, 50, 50]
+ color: green
+ - type: rect
+ bounds: [190, 250, 45, 50]
+ color: green
diff --git a/gfx/wr/wrench/reftests/scrolling/sticky-applied.yaml b/gfx/wr/wrench/reftests/scrolling/sticky-applied.yaml
new file mode 100644
index 0000000000..21a9dc4743
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/sticky-applied.yaml
@@ -0,0 +1,367 @@
+root:
+ items:
+ # Tests sticky-positioned items with previously-applied-offsets.
+
+ # This item is bottom-sticky; we indicate to WR that it already has had
+ # a sticky offset of y=-50 applied, and then scroll down by 50 pixels. WR
+ # should unapply the entire 50px that was applied, and leave the rect
+ # visually in the same spot.
+ - type: scroll-frame
+ id: 2
+ bounds: [10, 10, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [0, 50]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [10, 10, 50, 50]
+ margin-bottom: 0
+ vertical-offset-bounds: [-500, 0]
+ previously-applied-offset: [0, -50]
+ items:
+ - type: rect
+ bounds: [10, 10, 50, 50]
+ color: green
+ clip-chain: [2]
+ # Same as above, but this time we only indicate a previous-applied-offset
+ # of 40 pixels, so that's the maximum WR will unapply
+ - type: scroll-frame
+ id: 3
+ bounds: [10, 70, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [0, 50]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [10, 70, 50, 50]
+ margin-bottom: 0
+ vertical-offset-bounds: [-500, 0]
+ previously-applied-offset: [0, -40]
+ items:
+ - type: rect
+ bounds: [10, 70, 50, 50]
+ color: green
+ clip-chain: [3]
+ # This time we indicate a previously-applied-offset of 50 pixels, but only
+ # scroll by 40 pixels. In this case 40 of the 50 applied-offset pixels will
+ # be unapplied
+ - type: scroll-frame
+ id: 4
+ bounds: [10, 130, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [0, 40]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [10, 130, 50, 50]
+ margin-bottom: 0
+ vertical-offset-bounds: [-500, 0]
+ previously-applied-offset: [0, -50]
+ items:
+ - type: rect
+ bounds: [10, 130, 50, 50]
+ color: green
+ clip-chain: [4]
+ # Here we indicate a previously-applied-offset of 50 pixels, but continue
+ # scrolling in WR in the same direction by another 10 pixels. This effectively
+ # increases the applied offset to 60 pixels.
+ - type: scroll-frame
+ id: 5
+ bounds: [10, 190, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [0, -10]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [10, 190, 50, 50]
+ margin-bottom: 0
+ vertical-offset-bounds: [-500, 0]
+ previously-applied-offset: [0, -50]
+ items:
+ - type: rect
+ bounds: [10, 190, 50, 50]
+ color: green
+ clip-chain: [5]
+ # Same as previous case, but this time with the vertical-offset-bounds
+ # adjusted to limit the applied offset to only 55 pixels.
+ - type: scroll-frame
+ id: 6
+ bounds: [10, 250, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [0, -10]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [10, 250, 50, 50]
+ margin-bottom: 0
+ vertical-offset-bounds: [-55, 0]
+ previously-applied-offset: [0, -50]
+ items:
+ - type: rect
+ bounds: [10, 250, 50, 50]
+ color: green
+ clip-chain: [6]
+
+ # Repeat all the above cases, for top-sticky
+
+ - type: scroll-frame
+ id: 7
+ bounds: [70, 10, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [0, -50]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [70, 10, 50, 50]
+ margin-top: 0
+ vertical-offset-bounds: [0, 500]
+ previously-applied-offset: [0, 50]
+ items:
+ - type: rect
+ bounds: [70, 10, 50, 50]
+ color: green
+ clip-chain: [7]
+ - type: scroll-frame
+ id: 8
+ bounds: [70, 70, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [0, -50]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [70, 70, 50, 50]
+ margin-top: 0
+ vertical-offset-bounds: [0, 500]
+ previously-applied-offset: [0, 40]
+ items:
+ - type: rect
+ bounds: [70, 70, 50, 50]
+ color: green
+ clip-chain: [8]
+ - type: scroll-frame
+ id: 9
+ bounds: [70, 130, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [0, -40]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [70, 130, 50, 50]
+ margin-top: 0
+ vertical-offset-bounds: [0, 500]
+ previously-applied-offset: [0, 50]
+ items:
+ - type: rect
+ bounds: [70, 130, 50, 50]
+ color: green
+ clip-chain: [9]
+ - type: scroll-frame
+ id: 10
+ bounds: [70, 190, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [0, 10]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [70, 190, 50, 50]
+ margin-top: 0
+ vertical-offset-bounds: [0, 500]
+ previously-applied-offset: [0, 50]
+ items:
+ - type: rect
+ bounds: [70, 190, 50, 50]
+ color: green
+ clip-chain: [10]
+ - type: scroll-frame
+ id: 11
+ bounds: [70, 250, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [0, 10]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [70, 250, 50, 50]
+ margin-top: 0
+ vertical-offset-bounds: [0, 55]
+ previously-applied-offset: [0, 50]
+ items:
+ - type: rect
+ bounds: [70, 250, 50, 50]
+ color: green
+ clip-chain: [11]
+
+ # Repeat all the above cases, for right-sticky
+
+ - type: scroll-frame
+ id: 12
+ bounds: [130, 10, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [50, 0]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [130, 10, 50, 50]
+ margin-right: 0
+ horizontal-offset-bounds: [-500, 0]
+ previously-applied-offset: [-50, 0]
+ items:
+ - type: rect
+ bounds: [130, 10, 50, 50]
+ color: green
+ clip-chain: [12]
+ - type: scroll-frame
+ id: 13
+ bounds: [130, 70, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [50, 0]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [130, 70, 50, 50]
+ margin-right: 0
+ horizontal-offset-bounds: [-500, 0]
+ previously-applied-offset: [-40, 0]
+ items:
+ - type: rect
+ bounds: [130, 70, 50, 50]
+ color: green
+ clip-chain: [13]
+ - type: scroll-frame
+ id: 14
+ bounds: [130, 130, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [40, 0]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [130, 130, 50, 50]
+ margin-right: 0
+ horizontal-offset-bounds: [-500, 0]
+ previously-applied-offset: [-50, 0]
+ items:
+ - type: rect
+ bounds: [130, 130, 50, 50]
+ color: green
+ clip-chain: [14]
+ - type: scroll-frame
+ id: 15
+ bounds: [130, 190, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [-10, 0]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [130, 190, 50, 50]
+ margin-right: 0
+ horizontal-offset-bounds: [-500, 0]
+ previously-applied-offset: [-50, 0]
+ items:
+ - type: rect
+ bounds: [130, 190, 50, 50]
+ color: green
+ clip-chain: [15]
+ - type: scroll-frame
+ id: 16
+ bounds: [130, 250, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [-10, 0]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [130, 250, 50, 50]
+ margin-right: 0
+ horizontal-offset-bounds: [-55, 0]
+ previously-applied-offset: [-50, 0]
+ items:
+ - type: rect
+ bounds: [130, 250, 50, 50]
+ color: green
+ clip-chain: [16]
+
+ # Repeat all the above cases, for left-sticky
+
+ - type: scroll-frame
+ id: 17
+ bounds: [190, 10, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [-50, 0]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [190, 10, 50, 50]
+ margin-left: 0
+ horizontal-offset-bounds: [0, 500]
+ previously-applied-offset: [50, 0]
+ items:
+ - type: rect
+ bounds: [190, 10, 50, 50]
+ color: green
+ clip-chain: [17]
+ - type: scroll-frame
+ id: 18
+ bounds: [190, 70, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [-50, 0]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [190, 70, 50, 50]
+ margin-left: 0
+ horizontal-offset-bounds: [0, 500]
+ previously-applied-offset: [40, 0]
+ items:
+ - type: rect
+ bounds: [190, 70, 50, 50]
+ color: green
+ clip-chain: [18]
+ - type: scroll-frame
+ id: 19
+ bounds: [190, 130, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [-40, 0]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [190, 130, 50, 50]
+ margin-left: 0
+ horizontal-offset-bounds: [0, 500]
+ previously-applied-offset: [50, 0]
+ items:
+ - type: rect
+ bounds: [190, 130, 50, 50]
+ color: green
+ clip-chain: [19]
+ - type: scroll-frame
+ id: 20
+ bounds: [190, 190, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [10, 0]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [190, 190, 50, 50]
+ margin-left: 0
+ horizontal-offset-bounds: [0, 500]
+ previously-applied-offset: [50, 0]
+ items:
+ - type: rect
+ bounds: [190, 190, 50, 50]
+ color: green
+ clip-chain: [20]
+ - type: scroll-frame
+ id: 21
+ bounds: [190, 250, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [10, 0]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [190, 250, 50, 50]
+ margin-left: 0
+ horizontal-offset-bounds: [0, 55]
+ previously-applied-offset: [50, 0]
+ items:
+ - type: rect
+ bounds: [190, 250, 50, 50]
+ color: green
+ clip-chain: [21]
diff --git a/gfx/wr/wrench/reftests/scrolling/sticky-nested.yaml b/gfx/wr/wrench/reftests/scrolling/sticky-nested.yaml
new file mode 100644
index 0000000000..aa4b655e58
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/sticky-nested.yaml
@@ -0,0 +1,257 @@
+root:
+ items:
+ # This is a scroll frame with an out-of-viewport rect that should be pushed into the
+ # viewport by its "bottom" sticky constraint.
+ - type: scroll-frame
+ id: 2
+ bounds: [10, 10, 50, 50]
+ content-size: [200, 200]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [10, 60, 50, 50]
+ margin-bottom: 0
+ vertical-offset-bounds: [-500, 0]
+ items:
+ - type: clip
+ id: 22
+ bounds: [10, 60, 50, 50]
+ content-size: [100, 100]
+ - type: rect
+ bounds: [10, 60, 50, 50]
+ color: green
+ clip-chain: [2, 22]
+ # Do the same thing, but now for the "top" constraint.
+ - type: scroll-frame
+ id: 3
+ bounds: [70, 10, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [0, 50]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [70, 10, 50, 50]
+ margin-top: 0
+ vertical-offset-bounds: [0, 500]
+ items:
+ - type: clip
+ id: 23
+ bounds: [70, 10, 50, 50]
+ content-size: [100, 100]
+ - type: rect
+ bounds: [70, 10, 50, 50]
+ color: green
+ clip-chain: [3, 23]
+ # Do the same thing, but now for the "right" constraint.
+ - type: scroll-frame
+ id: 4
+ bounds: [10, 70, 50, 50]
+ content-size: [200, 200]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [60, 70, 50, 50]
+ margin-right: 0
+ horizontal-offset-bounds: [-500, 0]
+ items:
+ - type: clip
+ id: 24
+ bounds: [60, 70, 50, 50]
+ content-size: [100, 100]
+ - type: rect
+ bounds: [60, 70, 50, 50]
+ color: green
+ clip-chain: [4, 24]
+ # Do the same thing, but now for the "left" constraint.
+ - type: scroll-frame
+ id: 5
+ bounds: [70, 70, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [50, 0]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [70, 70, 50, 50]
+ margin-left: 0
+ horizontal-offset-bounds: [0, 500]
+ items:
+ - type: clip
+ id: 25
+ bounds: [70, 70, 50, 50]
+ content-size: [100, 100]
+ - type: rect
+ bounds: [70, 70, 50, 50]
+ color: green
+ clip-chain: [5, 25]
+
+ # The same tests, but this time with a margin.
+ - type: scroll-frame
+ id: 6
+ bounds: [130, 10, 50, 50]
+ content-size: [200, 200]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [130, 60, 50, 50]
+ margin-bottom: 10
+ vertical-offset-bounds: [-500, 0]
+ items:
+ - type: clip
+ id: 26
+ bounds: [130, 60, 50, 50]
+ content-size: [100, 100]
+ - type: rect
+ bounds: [130, 60, 50, 50]
+ color: green
+ clip-chain: [6, 26]
+
+ # Do the same thing, but now for the "top" constraint.
+ - type: scroll-frame
+ id: 7
+ bounds: [190, 10, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [0, 50]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [190, 10, 50, 50]
+ margin-top: 10
+ vertical-offset-bounds: [0, 500]
+ items:
+ - type: clip
+ id: 27
+ bounds: [190, 10, 50, 50]
+ content-size: [100, 100]
+ - type: rect
+ bounds: [190, 10, 50, 50]
+ color: green
+ clip-chain: [7, 27]
+
+ # Do the same thing, but now for the "right" constraint.
+ - type: scroll-frame
+ id: 8
+ bounds: [130, 70, 50, 50]
+ content-size: [200, 200]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [180, 70, 50, 50]
+ margin-right: 10
+ horizontal-offset-bounds: [-500, 0]
+ items:
+ - type: clip
+ id: 28
+ bounds: [180, 70, 50, 50]
+ content-size: [100, 100]
+ - type: rect
+ bounds: [180, 70, 50, 50]
+ color: green
+ clip-chain: [8, 28]
+
+ # Do the same thing, but now for the "left" constraint.
+ - type: scroll-frame
+ id: 9
+ bounds: [190, 70, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [50, 0]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [190, 70, 50, 50]
+ margin-left: 10
+ horizontal-offset-bounds: [0, 500]
+ items:
+ - type: clip
+ id: 29
+ bounds: [190, 70, 50, 50]
+ content-size: [100, 100]
+ - type: rect
+ bounds: [190, 70, 50, 50]
+ color: green
+ clip-chain: [9, 29]
+
+ # The same tests, but this time with a limit.
+ - type: scroll-frame
+ id: 10
+ bounds: [250, 10, 50, 50]
+ content-size: [200, 200]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [250, 60, 50, 50]
+ margin-bottom: 0
+ vertical-offset-bounds: [-25, 0]
+ items:
+ - type: clip
+ id: 30
+ bounds: [250, 60, 50, 50]
+ content-size: [100, 100]
+ - type: rect
+ bounds: [250, 60, 50, 50]
+ color: green
+ clip-chain: [10, 30]
+
+ # Do the same thing, but now for the "top" constraint.
+ - type: scroll-frame
+ id: 11
+ bounds: [310, 10, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [0, 50]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [310, 10, 50, 50]
+ margin-top: 0
+ vertical-offset-bounds: [0, 25]
+ items:
+ - type: clip
+ bounds: [310, 10, 50, 50]
+ content-size: [100, 100]
+ id: 31
+ - type: rect
+ bounds: [310, 10, 50, 50]
+ color: green
+ clip-chain: [11, 31]
+
+ # Do the same thing, but now for the "right" constraint.
+ - type: scroll-frame
+ id: 12
+ bounds: [250, 70, 50, 50]
+ content-size: [200, 200]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [300, 70, 50, 50]
+ margin-right: 0
+ horizontal-offset-bounds: [-25, 0]
+ items:
+ - type: clip
+ id: 32
+ bounds: [300, 70, 50, 50]
+ content-size: [100, 100]
+ - type: rect
+ bounds: [300, 70, 50, 50]
+ color: green
+ clip-chain: [12, 32]
+
+ # Do the same thing, but now for the "left" constraint.
+ - type: scroll-frame
+ id: 13
+ bounds: [310, 70, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [50, 0]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [310, 70, 50, 50]
+ margin-left: 0
+ horizontal-offset-bounds: [0, 25]
+ items:
+ - type: clip
+ id: 33
+ bounds: [310, 70, 50, 50]
+ content-size: [100, 100]
+ - type: rect
+ bounds: [310, 70, 50, 50]
+ color: green
+ clip-chain: [13, 33]
diff --git a/gfx/wr/wrench/reftests/scrolling/sticky-ref.yaml b/gfx/wr/wrench/reftests/scrolling/sticky-ref.yaml
new file mode 100644
index 0000000000..0f7b7b79a6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/sticky-ref.yaml
@@ -0,0 +1,40 @@
+root:
+ items:
+ - type: rect
+ bounds: [10, 10, 50, 50]
+ color: green
+ - type: rect
+ bounds: [10, 70, 50, 50]
+ color: green
+ - type: rect
+ bounds: [70, 10, 50, 50]
+ color: green
+ - type: rect
+ bounds: [70, 70, 50, 50]
+ color: green
+
+ - type: rect
+ bounds: [130, 10, 50, 40]
+ color: green
+ - type: rect
+ bounds: [130, 70, 40, 50]
+ color: green
+ - type: rect
+ bounds: [190, 20, 50, 40]
+ color: green
+ - type: rect
+ bounds: [200, 70, 40, 50]
+ color: green
+
+ - type: rect
+ bounds: [250, 35, 50, 25]
+ color: green
+ - type: rect
+ bounds: [275, 70, 25, 50]
+ color: green
+ - type: rect
+ bounds: [310, 10, 50, 25]
+ color: green
+ - type: rect
+ bounds: [310, 70, 25, 50]
+ color: green
diff --git a/gfx/wr/wrench/reftests/scrolling/sticky-transformed-ref.yaml b/gfx/wr/wrench/reftests/scrolling/sticky-transformed-ref.yaml
new file mode 100644
index 0000000000..9849a3f3d8
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/sticky-transformed-ref.yaml
@@ -0,0 +1,5 @@
+root:
+ items:
+ - type: rect
+ bounds: [10, 30, 10, 10]
+ color: green
diff --git a/gfx/wr/wrench/reftests/scrolling/sticky-transformed.yaml b/gfx/wr/wrench/reftests/scrolling/sticky-transformed.yaml
new file mode 100644
index 0000000000..0144a7c1c3
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/sticky-transformed.yaml
@@ -0,0 +1,21 @@
+root:
+ items:
+ # There is a new reference frame introduced between the scrollframe and
+ # the sticky item. This tests that the sticky item is still positioned
+ # correctly.
+ - type: scroll-frame
+ bounds: [10, 10, 50, 50]
+ content-size: [200, 200]
+ items:
+ - type: stacking-context
+ bounds: [10, 20, 10, 10]
+ transform: translate(0, 10)
+ items:
+ - type: sticky-frame
+ bounds: [0, 0, 10, 10]
+ margin-top: 10
+ vertical-offset-bounds: [0, 200]
+ items:
+ - type: rect
+ bounds: [0, 0, 10, 10]
+ color: green
diff --git a/gfx/wr/wrench/reftests/scrolling/sticky.yaml b/gfx/wr/wrench/reftests/scrolling/sticky.yaml
new file mode 100644
index 0000000000..87480e0751
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/sticky.yaml
@@ -0,0 +1,203 @@
+root:
+ items:
+ # This is a scroll frame with an out-of-viewport rect that should be pushed into the
+ # viewport by its "bottom" sticky constraint.
+ - type: scroll-frame
+ id: 2
+ bounds: [10, 10, 50, 50]
+ content-size: [200, 200]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [10, 60, 50, 50]
+ margin-bottom: 0
+ vertical-offset-bounds: [-500, 0]
+ items:
+ - type: rect
+ bounds: [10, 60, 50, 50]
+ color: green
+ clip-chain: [2]
+ # Do the same thing, but now for the "top" constraint.
+ - type: scroll-frame
+ id: 3
+ bounds: [70, 10, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [0, 50]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [70, 10, 50, 50]
+ margin-top: 0
+ vertical-offset-bounds: [0, 500]
+ items:
+ - type: rect
+ bounds: [70, 10, 50, 50]
+ color: green
+ clip-chain: [3]
+ # Do the same thing, but now for the "right" constraint.
+ - type: scroll-frame
+ id: 4
+ bounds: [10, 70, 50, 50]
+ content-size: [200, 200]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [60, 70, 50, 50]
+ margin-right: 0
+ horizontal-offset-bounds: [-500, 0]
+ items:
+ - type: rect
+ bounds: [60, 70, 50, 50]
+ color: green
+ clip-chain: [4]
+ # Do the same thing, but now for the "left" constraint.
+ - type: scroll-frame
+ id: 5
+ bounds: [70, 70, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [50, 0]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [70, 70, 50, 50]
+ margin-left: 0
+ horizontal-offset-bounds: [0, 500]
+ items:
+ - type: rect
+ bounds: [70, 70, 50, 50]
+ color: green
+ clip-chain: [5]
+
+ # The same tests, but this time with a margin.
+ - type: scroll-frame
+ id: 6
+ bounds: [130, 10, 50, 50]
+ content-size: [200, 200]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [130, 60, 50, 50]
+ margin-bottom: 10
+ vertical-offset-bounds: [-500, 0]
+ items:
+ - type: rect
+ bounds: [130, 60, 50, 50]
+ color: green
+ clip-chain: [6]
+ # Do the same thing, but now for the "top" constraint.
+ - type: scroll-frame
+ id: 7
+ bounds: [190, 10, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [0, 50]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [190, 10, 50, 50]
+ margin-top: 10
+ vertical-offset-bounds: [0, 500]
+ items:
+ - type: rect
+ bounds: [190, 10, 50, 50]
+ color: green
+ clip-chain: [7]
+ # Do the same thing, but now for the "right" constraint.
+ - type: scroll-frame
+ id: 8
+ bounds: [130, 70, 50, 50]
+ content-size: [200, 200]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [180, 70, 50, 50]
+ margin-right: 10
+ horizontal-offset-bounds: [-500, 0]
+ items:
+ - type: rect
+ bounds: [180, 70, 50, 50]
+ color: green
+ clip-chain: [8]
+ # Do the same thing, but now for the "left" constraint.
+ - type: scroll-frame
+ id: 9
+ bounds: [190, 70, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [50, 0]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [190, 70, 50, 50]
+ margin-left: 10
+ horizontal-offset-bounds: [0, 500]
+ items:
+ - type: rect
+ bounds: [190, 70, 50, 50]
+ color: green
+ clip-chain: [9]
+
+ # The same tests, but this time with a limit.
+ - type: scroll-frame
+ id: 10
+ bounds: [250, 10, 50, 50]
+ content-size: [200, 200]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [250, 60, 50, 50]
+ margin-bottom: 0
+ vertical-offset-bounds: [-25, 0]
+ items:
+ - type: rect
+ bounds: [250, 60, 50, 50]
+ color: green
+ clip-chain: [10]
+ # Do the same thing, but now for the "top" constraint.
+ - type: scroll-frame
+ id: 11
+ bounds: [310, 10, 50, 50]
+ content-size: [200, 200]
+ scroll-offset: [0, 50]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [310, 10, 50, 50]
+ margin-top: 0
+ vertical-offset-bounds: [0, 25]
+ items:
+ - type: rect
+ bounds: [310, 10, 50, 50]
+ color: green
+ clip-chain: [11]
+ # Do the same thing, but now for the "right" constraint.
+ - type: scroll-frame
+ id: 12
+ bounds: [250, 70, 50, 50]
+ content-size: [200, 200]
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: [300, 70, 50, 50]
+ margin-right: 0
+ horizontal-offset-bounds: [-25, 0]
+ items:
+ - type: rect
+ bounds: [300, 70, 50, 50]
+ color: green
+ clip-chain: [12]
+ # Do the same thing, but now for the "left" constraint.
+ - type: scroll-frame
+ id: 13
+ bounds: [310, 70, 50, 50]
+ content-size: [200, 200]
+ clip-to-frame: true
+ scroll-offset: [50, 0]
+ items:
+ - type: sticky-frame
+ bounds: [310, 70, 50, 50]
+ margin-left: 0
+ horizontal-offset-bounds: [0, 25]
+ items:
+ - type: rect
+ bounds: [310, 70, 50, 50]
+ color: green
+ clip-chain: [13]
diff --git a/gfx/wr/wrench/reftests/scrolling/translate-nested-ref.yaml b/gfx/wr/wrench/reftests/scrolling/translate-nested-ref.yaml
new file mode 100644
index 0000000000..f9f6c68209
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/translate-nested-ref.yaml
@@ -0,0 +1,14 @@
+---
+root:
+ items:
+ -
+ bounds: [8, 8, 500, 500]
+ clip: [0, 0, 0, 0]
+ type: "stacking-context"
+ items:
+ -
+ bounds: [0, 0, 200, 200]
+ type: rect
+ color: green
+ id: [0, 0]
+pipelines: []
diff --git a/gfx/wr/wrench/reftests/scrolling/translate-nested.yaml b/gfx/wr/wrench/reftests/scrolling/translate-nested.yaml
new file mode 100644
index 0000000000..3920dda414
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/translate-nested.yaml
@@ -0,0 +1,29 @@
+---
+root:
+ items:
+ -
+ bounds: [8, 8, 500, 500]
+ type: "stacking-context"
+ items:
+ -
+ bounds: [0, 0, 200, 200]
+ type: clip
+ id: 2
+ -
+ bounds: [0, 0, 200, 200]
+ type: rect
+ color: red
+ clip-chain: [2]
+ -
+ bounds: [0, 0, 200, 200]
+ type: "stacking-context"
+ transform: translate(100, 0)
+ items:
+ -
+ bounds: [-100, 0, 200, 200]
+ clip-rect: [-300, -300, 900, 900]
+ type: rect
+ color: green
+ clip-chain: [2]
+ id: [0, 0]
+pipelines: []
diff --git a/gfx/wr/wrench/reftests/scrolling/viewport-offset-ref.yaml b/gfx/wr/wrench/reftests/scrolling/viewport-offset-ref.yaml
new file mode 100644
index 0000000000..b7d697e4e0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/viewport-offset-ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: 0 0 0 0
+ items:
+ - type: rect
+ color: red
+ bounds: 0 100 1000 50
diff --git a/gfx/wr/wrench/reftests/scrolling/viewport-offset.yaml b/gfx/wr/wrench/reftests/scrolling/viewport-offset.yaml
new file mode 100644
index 0000000000..171e69a63d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/scrolling/viewport-offset.yaml
@@ -0,0 +1,23 @@
+# The test assures that the viewport offset is correctly getting
+# the reference_frame_relative_offset applied when flattening
+# a scroll frame.
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: 0 100 0 0
+ items:
+ - type: scroll-frame
+ id: 2
+ bounds: 0 0 1000 1000
+ clip-to-frame: true
+ items:
+ - type: sticky-frame
+ bounds: 0 -50 1000 50
+ margin-top: -50
+ vertical-offset-bounds: [0, 2000]
+ items:
+ - type: rect
+ bounds: 0 -50 1000 100
+ color: red
+ clip-chain: [2]
diff --git a/gfx/wr/wrench/reftests/snap/1761299-ref.yaml b/gfx/wr/wrench/reftests/snap/1761299-ref.yaml
new file mode 100644
index 0000000000..5102c7331e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/snap/1761299-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 512, 128]
+ color: red
diff --git a/gfx/wr/wrench/reftests/snap/1761299.yaml b/gfx/wr/wrench/reftests/snap/1761299.yaml
new file mode 100644
index 0000000000..f36f0c4e50
--- /dev/null
+++ b/gfx/wr/wrench/reftests/snap/1761299.yaml
@@ -0,0 +1,12 @@
+# verify that the raster spatial node selected when a surface is too large
+# is correct propagated to the picture render task
+---
+root:
+ items:
+ - type: stacking-context
+ filters: [identity]
+ transform: scale(0.5,1,1)
+ items:
+ - type: rect
+ bounds: [0, 0, 1024, 128]
+ color: red
diff --git a/gfx/wr/wrench/reftests/snap/fractional-filter-ref.yaml b/gfx/wr/wrench/reftests/snap/fractional-filter-ref.yaml
new file mode 100644
index 0000000000..c8985a7d81
--- /dev/null
+++ b/gfx/wr/wrench/reftests/snap/fractional-filter-ref.yaml
@@ -0,0 +1,13 @@
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1920, 1200]
+ transform: translate(-45, 7)
+ items:
+ - type: stacking-context
+ filters: [opacity(0.9)]
+ items:
+ - text: "This should be pixel aligned!"
+ origin: 416.543499 160.008325
+ size: 16
+
diff --git a/gfx/wr/wrench/reftests/snap/fractional-filter.yaml b/gfx/wr/wrench/reftests/snap/fractional-filter.yaml
new file mode 100644
index 0000000000..6b45682ad2
--- /dev/null
+++ b/gfx/wr/wrench/reftests/snap/fractional-filter.yaml
@@ -0,0 +1,14 @@
+# Verify that a fractional transform that is attached to an off-screen
+# surface is able to snap correctly
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1920, 1200]
+ transform: translate(-45, 7.491675)
+ items:
+ - type: stacking-context
+ filters: [opacity(0.9)]
+ items:
+ - text: "This should be pixel aligned!"
+ origin: 416.543499 160.008325
+ size: 16
diff --git a/gfx/wr/wrench/reftests/snap/preserve-3d.png b/gfx/wr/wrench/reftests/snap/preserve-3d.png
new file mode 100644
index 0000000000..e02c30654c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/snap/preserve-3d.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/snap/preserve-3d.yaml b/gfx/wr/wrench/reftests/snap/preserve-3d.yaml
new file mode 100644
index 0000000000..133233d58b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/snap/preserve-3d.yaml
@@ -0,0 +1,55 @@
+---
+root:
+ items:
+ -
+ bounds: [293, 139, 500, 500]
+ type: reference-frame
+ transform: scale(0.7)
+ items:
+ -
+ bounds: [0, 0, 500, 500]
+ type: stacking-context
+ perspective: [1, 0, 0, 0, 0, 1, 0, 0, -0.25416666, -0.23866667, 1, -0.00083333335, 0, 0, 0, 1]
+ "transform-style": preserve-3d
+ items:
+ -
+ bounds: [0, 0, 0, 0]
+ type: stacking-context
+ transform: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -110, 0, 0, 1]
+ items:
+ -
+ type: clip
+ id: 2
+ complex:
+ -
+ rect: [24, 539, 24, 24]
+ radius: [12, 12]
+ -
+ bounds: [24, 539, 24, 24]
+ type: rect
+ color: black
+ clip-chain: [2]
+ -
+ type: clip
+ id: 3
+ complex:
+ -
+ rect: [24, 770, 24, 24]
+ radius: [12, 12]
+ -
+ bounds: [24, 770, 24, 24]
+ type: rect
+ color: black
+ clip-chain: [3]
+ -
+ type: clip
+ id: 4
+ complex:
+ -
+ rect: [24, 847, 24, 24]
+ radius: [12, 12]
+ -
+ bounds: [24, 847, 24, 24]
+ type: rect
+ color: black
+ clip-chain: [4]
diff --git a/gfx/wr/wrench/reftests/snap/reftest.list b/gfx/wr/wrench/reftests/snap/reftest.list
new file mode 100644
index 0000000000..3464829f9b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/snap/reftest.list
@@ -0,0 +1,6 @@
+platform(linux,mac) == snap.yaml snap.png
+== transform.yaml transform.png
+platform(linux,mac) == preserve-3d.yaml preserve-3d.png
+fuzzy(128,200) == subpixel-raster-root.yaml subpixel-raster-root-ref.yaml
+platform(linux,mac) == fractional-filter.yaml fractional-filter-ref.yaml
+max_surface_size(256) == 1761299.yaml 1761299.yaml
diff --git a/gfx/wr/wrench/reftests/snap/snap.png b/gfx/wr/wrench/reftests/snap/snap.png
new file mode 100644
index 0000000000..38f7cf33e5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/snap/snap.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/snap/snap.yaml b/gfx/wr/wrench/reftests/snap/snap.yaml
new file mode 100644
index 0000000000..f292a728a9
--- /dev/null
+++ b/gfx/wr/wrench/reftests/snap/snap.yaml
@@ -0,0 +1,15 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [ 1, 1, 5, 5 ]
+ color: green
+ - type: rect
+ bounds: [ 10.4, 1.8, 5, 5 ]
+ color: green
+ - type: rect
+ bounds: [ 21.5, 1.5, 5, 5 ]
+ color: green
+ - type: rect
+ bounds: [ 31.2, 1.6, 4.3, 5.5 ]
+ color: green
diff --git a/gfx/wr/wrench/reftests/snap/subpixel-raster-root-ref.yaml b/gfx/wr/wrench/reftests/snap/subpixel-raster-root-ref.yaml
new file mode 100644
index 0000000000..8afe567788
--- /dev/null
+++ b/gfx/wr/wrench/reftests/snap/subpixel-raster-root-ref.yaml
@@ -0,0 +1,7 @@
+---
+root:
+ items:
+ -
+ bounds: [0, 111, 200, 1]
+ type: rect
+ color: green
diff --git a/gfx/wr/wrench/reftests/snap/subpixel-raster-root.yaml b/gfx/wr/wrench/reftests/snap/subpixel-raster-root.yaml
new file mode 100644
index 0000000000..494ec42334
--- /dev/null
+++ b/gfx/wr/wrench/reftests/snap/subpixel-raster-root.yaml
@@ -0,0 +1,13 @@
+# Verify that we don't incorrectly snap surface rects with fractional pixel offsets
+---
+root:
+ items:
+ -
+ type: "stacking-context"
+ transform: translate(0, 100.5, 0)
+ transform-style: preserve-3d
+ items:
+ -
+ bounds: [0, 10.5, 200, 1]
+ type: rect
+ color: green
diff --git a/gfx/wr/wrench/reftests/snap/transform.png b/gfx/wr/wrench/reftests/snap/transform.png
new file mode 100644
index 0000000000..73c8feacdf
--- /dev/null
+++ b/gfx/wr/wrench/reftests/snap/transform.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/snap/transform.yaml b/gfx/wr/wrench/reftests/snap/transform.yaml
new file mode 100644
index 0000000000..d1b0579eda
--- /dev/null
+++ b/gfx/wr/wrench/reftests/snap/transform.yaml
@@ -0,0 +1,19 @@
+---
+root:
+ items:
+ # 45 degrees result in middle -+ 14.142, rounded to 14
+ - type: stacking-context
+ bounds: [0, 0, 60, 60]
+ transform: rotate-y(45)
+ items:
+ - type: rect
+ bounds: [ 10, 10, 40, 40 ]
+ color: blue
+ # 50 degrees result in middle -+ 12.856, rounded to 13
+ - type: stacking-context
+ bounds: [50, 0, 60, 60]
+ transform: rotate-x(50)
+ items:
+ - type: rect
+ bounds: [ 10, 10, 40, 40 ]
+ color: blue
diff --git a/gfx/wr/wrench/reftests/split/cross-ref.yaml b/gfx/wr/wrench/reftests/split/cross-ref.yaml
new file mode 100644
index 0000000000..c784e62fce
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/cross-ref.yaml
@@ -0,0 +1,36 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 200]
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 200]
+ transform: rotate-y(-45) rotate-x(45)
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 100]
+ color: green
+ - type: stacking-context
+ bounds: [0, 0, 100, 200]
+ transform: rotate-y(45) rotate-x(45)
+ items:
+ - type: rect
+ bounds: [0, 0, 50, 100]
+ color: red
+ - type: stacking-context
+ bounds: [0, 0, 100, 200]
+ transform: rotate-y(45) rotate-x(45)
+ items:
+ - type: rect
+ bounds: [50, 0, 50, 100]
+ color: red
+ - type: stacking-context
+ bounds: [0, 0, 100, 200]
+ transform: rotate-y(-45) rotate-x(45)
+ items:
+ - type: rect
+ bounds: [50, 0, 50, 100]
+ color: green
+
+
diff --git a/gfx/wr/wrench/reftests/split/cross.yaml b/gfx/wr/wrench/reftests/split/cross.yaml
new file mode 100644
index 0000000000..e306dbad33
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/cross.yaml
@@ -0,0 +1,24 @@
+# Crossed planes test. Similar to "simple" but has more complex transformations,
+# which puts higher requirements on the precision of arithmetics,
+# and all 4 pieces are visible on screen.
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 200]
+ transform-style: preserve-3d
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 200]
+ transform: rotate-y(45) rotate-x(45)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: red
+ - type: stacking-context
+ bounds: [0, 0, 100, 200]
+ transform: rotate-y(-45) rotate-x(45)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: green
diff --git a/gfx/wr/wrench/reftests/split/filter-ref.yaml b/gfx/wr/wrench/reftests/split/filter-ref.yaml
new file mode 100644
index 0000000000..01e16b6e8f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/filter-ref.yaml
@@ -0,0 +1,10 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 200, 200]
+ transform-style: preserve-3d
+ items:
+ - type: rect
+ bounds: [0, 0, 200, 200]
+ color: [255, 0, 0, 0.5]
diff --git a/gfx/wr/wrench/reftests/split/filter.yaml b/gfx/wr/wrench/reftests/split/filter.yaml
new file mode 100644
index 0000000000..54667c0652
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/filter.yaml
@@ -0,0 +1,21 @@
+# This test ensures that an opacity filter forces the flattening of preserve-3D context.
+# The innermost rectangle has backface visibility disabled. If the parent stacking context
+# is a part of preserve-3D hierarchy, then the item would be backward-facing, thus invisible.
+# With the opacity filter, it's no longer a part of the preserve-3D, thus visible.
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 200, 200]
+ transform-style: preserve-3d
+ transform: rotate-y(180)
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 200, 200]
+ transform-style: preserve-3d
+ filters: [opacity(0.5)]
+ items:
+ - type: rect
+ bounds: [0, 0, 200, 200]
+ color: red
+ backface-visible: false
diff --git a/gfx/wr/wrench/reftests/split/gradient-ref.yaml b/gfx/wr/wrench/reftests/split/gradient-ref.yaml
new file mode 100644
index 0000000000..5a461c737d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/gradient-ref.yaml
@@ -0,0 +1,39 @@
+# Checks the UV interpolation of a preserved 3d stacking context matches
+# a flat stacking context. There are four black rectangles that overlap
+# the edges of the transformed gradient to minimize fuzz.
+---
+root:
+ items:
+ - type: "reference-frame"
+ bounds: [0, 0, 2746, 1408]
+ transform-style: flat
+ perspective: [1, 0, 0, 0, 0, 1, 0, 0, -0.4699999988079071, -0.44999998807907104, 1, -0.0012499999720603228, 176, 160, 0, 1]
+ items:
+ - type: "reference-frame"
+ bounds: [0, 0, 2746, 1408]
+ transform-style: flat
+ transform: rotate-y(-53)
+ items:
+ - type: "stacking-context"
+ transform-style: flat
+ items:
+ - type: rect
+ bounds: [0, 0, 300, 300]
+ color: 255 255 255 1.0000
+ - type: gradient
+ bounds: [0, 0, 300, 150]
+ start: [150, 0]
+ end: [150, 150]
+ stops: [0.0, [255, 0, 0, 1], 1.0, [254, 0, 0, 1]]
+ - type: rect
+ bounds: [520, 260, 120, 20]
+ color: 0 0 0 1.0000
+ - type: rect
+ bounds: [520, 336, 120, 3]
+ color: 0 0 0 1.0000
+ - type: rect
+ bounds: [520, 260, 10, 80]
+ color: 0 0 0 1.0000
+ - type: rect
+ bounds: [622, 260, 10, 80]
+ color: 0 0 0 1.0000
diff --git a/gfx/wr/wrench/reftests/split/gradient.yaml b/gfx/wr/wrench/reftests/split/gradient.yaml
new file mode 100644
index 0000000000..8da777df03
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/gradient.yaml
@@ -0,0 +1,42 @@
+# Checks the UV interpolation of a preserved 3d stacking context matches
+# a flat stacking context. There are four black rectangles that overlap
+# the edges of the transformed gradient to minimize fuzz.
+---
+root:
+ items:
+ - type: "reference-frame"
+ bounds: [0, 0, 2746, 1408]
+ transform-style: preserve-3d
+ perspective: [1, 0, 0, 0, 0, 1, 0, 0, -0.4699999988079071, -0.44999998807907104, 1, -0.0012499999720603228, 176, 160, 0, 1]
+ items:
+ - type: "reference-frame"
+ bounds: [0, 0, 2746, 1408]
+ transform-style: preserve-3d
+ transform: rotate-y(-53)
+ items:
+ - type: "stacking-context"
+ transform-style: preserve-3d
+ items:
+ - type: "stacking-context"
+ transform-style: flat
+ items:
+ - type: rect
+ bounds: [0, 0, 300, 300]
+ color: 255 255 255 1.0000
+ - type: gradient
+ bounds: [0, 0, 300, 150]
+ start: [150, 0]
+ end: [150, 150]
+ stops: [0.0, [255, 0, 0, 1], 1.0, [254, 0, 0, 1]]
+ - type: rect
+ bounds: [520, 260, 120, 20]
+ color: 0 0 0 1.0000
+ - type: rect
+ bounds: [520, 336, 120, 3]
+ color: 0 0 0 1.0000
+ - type: rect
+ bounds: [520, 260, 10, 80]
+ color: 0 0 0 1.0000
+ - type: rect
+ bounds: [622, 260, 10, 80]
+ color: 0 0 0 1.0000
diff --git a/gfx/wr/wrench/reftests/split/intermediate-1-ref.yaml b/gfx/wr/wrench/reftests/split/intermediate-1-ref.yaml
new file mode 100644
index 0000000000..2d67df4ead
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/intermediate-1-ref.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: 8 36 100 100
+ color: green
+ - type: rect
+ bounds: 108 36 100 100
+ color: blue
+ - type: rect
+ bounds: 208 36 100 100
+ color: yellow
diff --git a/gfx/wr/wrench/reftests/split/intermediate-1.yaml b/gfx/wr/wrench/reftests/split/intermediate-1.yaml
new file mode 100644
index 0000000000..0c02fe8e3c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/intermediate-1.yaml
@@ -0,0 +1,38 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: 8 36 300 100
+ color: red
+ - type: stacking-context
+ transform-style: preserve-3d
+ transform: translate(8, 36, 0)
+ items:
+ - type: stacking-context
+ transform-style: preserve-3d
+ items:
+ - type: rect
+ bounds: 0 0 100 100
+ color: green
+ - type: stacking-context # flat, intermediate surface
+ items:
+ - type: stacking-context
+ transform: translate(100, 0, 0)
+ transform-style: preserve-3d
+ items:
+ - type: stacking-context
+ transform-style: preserve-3d
+ items:
+ - type: rect
+ bounds: 0 0 100 100
+ color: blue
+ - type: stacking-context
+ transform: translate(100, 0, 0)
+ transform-style: preserve-3d
+ items:
+ - type: stacking-context
+ transform-style: preserve-3d
+ items:
+ - type: rect
+ bounds: 0 0 100 100
+ color: yellow
diff --git a/gfx/wr/wrench/reftests/split/intermediate-2.yaml b/gfx/wr/wrench/reftests/split/intermediate-2.yaml
new file mode 100644
index 0000000000..7d926ade4a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/intermediate-2.yaml
@@ -0,0 +1,34 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: 8 36 300 100
+ color: red
+ - type: stacking-context
+ transform-style: preserve-3d
+ transform: translate(8, 36, 0)
+ items:
+ - type: stacking-context
+ transform-style: preserve-3d
+ items:
+ - type: rect
+ bounds: 0 0 100 100
+ color: green
+ - type: stacking-context
+ items:
+ - type: stacking-context
+ transform: translate(100, 0, 0)
+ items:
+ - type: rect
+ bounds: 0 0 100 100
+ color: blue
+ - type: stacking-context
+ transform: translate(100, 0, 0)
+ transform-style: preserve-3d
+ items:
+ - type: stacking-context
+ transform-style: preserve-3d
+ items:
+ - type: rect
+ bounds: 0 0 100 100
+ color: yellow
diff --git a/gfx/wr/wrench/reftests/split/mixed-order-ref.yaml b/gfx/wr/wrench/reftests/split/mixed-order-ref.yaml
new file mode 100644
index 0000000000..e3632665c0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/mixed-order-ref.yaml
@@ -0,0 +1,22 @@
+# The result should be matching the flat ordering, since there
+# is no variation in Z of the preserve-3D children.
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 200, 200]
+ transform: rotate-x(30)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: green
+ - type: stacking-context
+ bounds: [25, 25, 100, 100]
+ #transform: rotate-z(45)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: red
+ - type: rect
+ bounds: [100, 0, 100, 100]
+ color: blue
diff --git a/gfx/wr/wrench/reftests/split/mixed-order.yaml b/gfx/wr/wrench/reftests/split/mixed-order.yaml
new file mode 100644
index 0000000000..789621dc5b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/mixed-order.yaml
@@ -0,0 +1,25 @@
+# The "preserve-3d" context has a mix of children being other stacking contexts and
+# just primitives. The other stacking contexts are also "preserve-3d" but on the same plane,
+# so the result ordering should be exactly matching the way we specify the children.
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 200, 200]
+ transform-style: preserve-3d
+ transform: rotate-x(30)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: green
+ - type: stacking-context
+ bounds: [25, 25, 100, 100]
+ #TODO: https://github.com/servo/webrender/issues/2946
+ #transform: rotate-z(45)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: red
+ - type: rect
+ bounds: [100, 0, 100, 100]
+ color: blue
diff --git a/gfx/wr/wrench/reftests/split/near-plane.png b/gfx/wr/wrench/reftests/split/near-plane.png
new file mode 100644
index 0000000000..c6e81c9983
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/near-plane.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/split/near-plane.yaml b/gfx/wr/wrench/reftests/split/near-plane.yaml
new file mode 100644
index 0000000000..f1911674ca
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/near-plane.yaml
@@ -0,0 +1,16 @@
+# In this test, there is a single polygon intersecting the near plane.
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 600, 600]
+ transform-style: preserve-3d
+ perspective: 200
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 600, 600]
+ transform: rotate-x(-60.0)
+ items:
+ - type: rect
+ bounds: [000, 0, 600, 600]
+ color: [255, 0, 0, 0.5]
diff --git a/gfx/wr/wrench/reftests/split/nested-coord-systems-ref.yaml b/gfx/wr/wrench/reftests/split/nested-coord-systems-ref.yaml
new file mode 100644
index 0000000000..97e2e82fc5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/nested-coord-systems-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: 0 0 150 200
+ color: green
diff --git a/gfx/wr/wrench/reftests/split/nested-coord-systems.yaml b/gfx/wr/wrench/reftests/split/nested-coord-systems.yaml
new file mode 100644
index 0000000000..5cdc4ab634
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/nested-coord-systems.yaml
@@ -0,0 +1,28 @@
+# This test checks our ability to have the plane splitting root itself to be rasterized
+# in some local space. Perspective transform forces WR to use the local space.
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: 0 0 400 400
+ transform-style: preserve-3d
+ items:
+ - type: stacking-context
+ bounds: 0 0 400 400
+ transform-style: flat
+ transform: perspective(100) rotate-y(30)
+ transform-origin: 0 0
+ items:
+ - type: stacking-context
+ bounds: 0 0 400 400
+ transform-style: preserve-3d
+ items:
+ - type: stacking-context
+ bounds: 0 0 400 400
+ transform-style: flat
+ transform: perspective(100) rotate-y(30)
+ transform-origin: 0 0
+ items:
+ - type: rect
+ bounds: 0 0 200 200
+ color: green
diff --git a/gfx/wr/wrench/reftests/split/nested-preserve3d-crash.yaml b/gfx/wr/wrench/reftests/split/nested-preserve3d-crash.yaml
new file mode 100644
index 0000000000..68a01ebeb1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/nested-preserve3d-crash.yaml
@@ -0,0 +1,37 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: 0 0 1024 768
+ items:
+ - type: stacking-context
+ bounds: 0 0 1024 768
+ transform-style: preserve-3d
+ transform: rotate-x(180)
+ items:
+ - type: stacking-context
+ bounds: 0 0 1024 768
+ items:
+ - type: rect
+ bounds: 0 0 200 200
+ color: green
+ - type: stacking-context
+ bounds: 0 0 1024 768
+ items:
+ - type: rect
+ bounds: 0 0 200 200
+ color: green
+ - type: stacking-context
+ bounds: 0 0 1024 768
+ transform-style: preserve-3d
+ transform: rotate-x(180)
+ items:
+ - type: rect
+ bounds: 0 0 200 200
+ color: green
+ - type: stacking-context
+ bounds: 0 0 1024 768
+ items:
+ - type: rect
+ bounds: 0 0 200 200
+ color: green
diff --git a/gfx/wr/wrench/reftests/split/nested-ref.yaml b/gfx/wr/wrench/reftests/split/nested-ref.yaml
new file mode 100644
index 0000000000..798b624356
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/nested-ref.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1024, 1024]
+ items:
+ - type: rect
+ bounds: [150, 0, 300, 600]
+ color: red
+ - type: rect
+ bounds: [150, 0, 300, 200]
+ color: green
diff --git a/gfx/wr/wrench/reftests/split/nested.yaml b/gfx/wr/wrench/reftests/split/nested.yaml
new file mode 100644
index 0000000000..5b93224fda
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/nested.yaml
@@ -0,0 +1,25 @@
+# This tests have a non-preserve3d stacking context nested within
+# preserve-3d sub-tree. This nested context is still getting baked in
+# and participates (as a whole) in plane splitting.
+# It is layed out in the same plane as the parent, so should be ordered
+# last, given that it's coming later than the parent.
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1024, 1024]
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 600, 600]
+ transform-style: preserve-3d
+ transform: rotate-y(60.0)
+ items:
+ - type: rect
+ bounds: [0, 0, 600, 600]
+ color: red
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ items:
+ - type: rect
+ bounds: [0, 0, 600, 200]
+ color: green
diff --git a/gfx/wr/wrench/reftests/split/order-1-ref.yaml b/gfx/wr/wrench/reftests/split/order-1-ref.yaml
new file mode 100644
index 0000000000..930ac5c8b9
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/order-1-ref.yaml
@@ -0,0 +1,8 @@
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: green
diff --git a/gfx/wr/wrench/reftests/split/order-1.yaml b/gfx/wr/wrench/reftests/split/order-1.yaml
new file mode 100644
index 0000000000..7b5613f6d3
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/order-1.yaml
@@ -0,0 +1,20 @@
+# The "preserve-3d" context has a "flat" child. Rotation by 180 degrees and positive Z offset
+# should compensate each other, resulting in a green rectangle.
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ transform-style: preserve-3d
+ transform: rotate-x(180)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: green
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ transform: translate(0, 0, 10)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: red
diff --git a/gfx/wr/wrench/reftests/split/order-2-ref.yaml b/gfx/wr/wrench/reftests/split/order-2-ref.yaml
new file mode 100644
index 0000000000..930ac5c8b9
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/order-2-ref.yaml
@@ -0,0 +1,8 @@
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: green
diff --git a/gfx/wr/wrench/reftests/split/order-2.yaml b/gfx/wr/wrench/reftests/split/order-2.yaml
new file mode 100644
index 0000000000..a94d25b8f1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/order-2.yaml
@@ -0,0 +1,27 @@
+# The "preserve-3d" context has a "flat" child, which contains two other stacking contexts
+# that have their Z modified. The Z should not affect their drawing order, since they
+# are hidden from the 3D hierarchy by the flat parent.
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ transform-style: preserve-3d
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ transform: translate(0, 0, 10)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: red
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ transform: translate(0, 0, -10)
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: green
diff --git a/gfx/wr/wrench/reftests/split/order-3-ref.yaml b/gfx/wr/wrench/reftests/split/order-3-ref.yaml
new file mode 100644
index 0000000000..9d896d9699
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/order-3-ref.yaml
@@ -0,0 +1,15 @@
+---
+root:
+ items:
+ -
+ type: rect
+ bounds: [8, 8, 200, 200]
+ color: blue
+ -
+ type: rect
+ bounds: [8, 208, 200, 200]
+ color: blue
+ -
+ type: rect
+ bounds: [8, 408, 200, 200]
+ color: blue
diff --git a/gfx/wr/wrench/reftests/split/order-3.yaml b/gfx/wr/wrench/reftests/split/order-3.yaml
new file mode 100644
index 0000000000..7f80652af6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/order-3.yaml
@@ -0,0 +1,31 @@
+# Ensure that preserve-3d items are correctly batched with transparent siblings.
+---
+root:
+ items:
+ -
+ type: stacking-context
+ transform-style: "preserve-3d"
+ items:
+ -
+ type: rect
+ bounds: [8, 8, 200, 200]
+ color: blue
+ -
+ type: stacking-context
+ transform-style: "preserve-3d"
+ items:
+ -
+ type: rect
+ bounds: [8, 208, 200, 200]
+ color: blue
+ - type: rect
+ color: [255, 0, 0, 0.5]
+ bounds: [8, 408, 200, 200]
+ -
+ type: stacking-context
+ transform-style: "preserve-3d"
+ items:
+ -
+ type: rect
+ bounds: [8, 408, 200, 200]
+ color: blue
diff --git a/gfx/wr/wrench/reftests/split/ordering-ref.yaml b/gfx/wr/wrench/reftests/split/ordering-ref.yaml
new file mode 100644
index 0000000000..33ad571660
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/ordering-ref.yaml
@@ -0,0 +1,15 @@
+---
+root:
+ items:
+ -
+ bounds: [15, 15, 150, 300]
+ type: rect
+ color: 255 0 0 1.0000
+ -
+ bounds: [203, 15, 150, 300]
+ type: rect
+ color: 255 0 0 1.0000
+ -
+ bounds: [0, 0, 450, 150]
+ type: rect
+ color: 0 0 0 0.6667
diff --git a/gfx/wr/wrench/reftests/split/ordering.yaml b/gfx/wr/wrench/reftests/split/ordering.yaml
new file mode 100644
index 0000000000..2809166454
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/ordering.yaml
@@ -0,0 +1,25 @@
+---
+root:
+ items:
+ -
+ type: "stacking-context"
+ transform: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 15, 15, 0, 1]
+ "transform-style": "preserve-3d"
+ items:
+ -
+ perspective: [1, 0, 0, 0, 0, 1, 0, 0, -0.05, -0.1, 1, -0.00066666666, 0, 0, 0, 1]
+ type: "stacking-context"
+ "transform-style": "preserve-3d"
+ items:
+ -
+ bounds: [0, 0, 150, 300]
+ type: rect
+ color: 255 0 0 1.0000
+ -
+ bounds: [203, 15, 150, 300]
+ type: rect
+ color: 255 0 0 1.0000
+ -
+ bounds: [0, 0, 450, 150]
+ type: rect
+ color: 0 0 0 0.6667
diff --git a/gfx/wr/wrench/reftests/split/perspective-clipping-ref.yaml b/gfx/wr/wrench/reftests/split/perspective-clipping-ref.yaml
new file mode 100644
index 0000000000..8f6d4a1566
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/perspective-clipping-ref.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: 0 0 1024 768
+ items:
+ - type: rect
+ bounds: 0 0 1024 768
+ color: green
diff --git a/gfx/wr/wrench/reftests/split/perspective-clipping.yaml b/gfx/wr/wrench/reftests/split/perspective-clipping.yaml
new file mode 100644
index 0000000000..7135f27c37
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/perspective-clipping.yaml
@@ -0,0 +1,23 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: 0 0 1024 768
+ items:
+ - type: rect
+ bounds: 0 0 1024 768
+ color: red
+ - type: stacking-context
+ bounds: 0 0 1024 768
+ transform-style: preserve-3d
+ perspective: 300
+ perspective-origin: 0 0
+ items:
+ - type: stacking-context
+ bounds: 0 0 1024 768
+ # translate-Z(-300) scale(2)
+ transform: 2 0 0 0 0 2 0 0 0 0 2 0 0 0 -300 1
+ items:
+ - type: rect
+ bounds: 0 0 1024 768
+ color: green
diff --git a/gfx/wr/wrench/reftests/split/reftest.list b/gfx/wr/wrench/reftests/split/reftest.list
new file mode 100644
index 0000000000..cd9b33c02b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/reftest.list
@@ -0,0 +1,22 @@
+fuzzy-if(platform(swgl),1,180000) == simple.yaml simple-ref.yaml
+== order-1.yaml order-1-ref.yaml
+== order-2.yaml order-2-ref.yaml
+== nested.yaml nested-ref.yaml
+# fuzziness is needed due to perspective for the edge
+fuzzy(35,200) == nested-coord-systems.yaml nested-coord-systems-ref.yaml
+== nested-preserve3d-crash.yaml nested-preserve3d-crash.yaml
+== perspective-clipping.yaml perspective-clipping-ref.yaml
+== intermediate-1.yaml intermediate-1-ref.yaml
+== intermediate-2.yaml intermediate-1-ref.yaml
+== split-intersect1.yaml split-intersect1-ref.yaml
+== ordering.yaml ordering-ref.yaml
+fuzzy(1,20) fuzzy-if(platform(swgl),128,39) == near-plane.yaml near-plane.png
+# Note: on windows the image is rendered at a slightly different spot.
+# similarly, a lot of tests in "transform" are non-windows. TODO: investigate
+platform(linux,mac) fuzzy(1,20) == same-plane.yaml same-plane.png
+#TODO: https://github.com/servo/webrender/issues/2946
+#== cross.yaml cross-ref.yaml
+== mixed-order.yaml mixed-order-ref.yaml
+fuzzy(1,40000) == filter.yaml filter-ref.yaml
+fuzzy(1,10000) == gradient.yaml gradient-ref.yaml
+== order-3.yaml order-3-ref.yaml
diff --git a/gfx/wr/wrench/reftests/split/same-plane.png b/gfx/wr/wrench/reftests/split/same-plane.png
new file mode 100644
index 0000000000..3d5baf998f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/same-plane.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/split/same-plane.yaml b/gfx/wr/wrench/reftests/split/same-plane.yaml
new file mode 100644
index 0000000000..277709bad1
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/same-plane.yaml
@@ -0,0 +1,33 @@
+# This test has a number of rectangles placed on the same transformed preserve3d plane.
+# The expected result is that the rectangles are rendered in the order they are listed.
+# This can fail if the primitives are considered outside of the main plane due
+# to floating point precision issues in plane-splitting and transformation.
+---
+root:
+ items:
+ - type: "stacking-context"
+ transform-style: preserve-3d
+ transform: rotate-y(-30) rotate-x(-75) translate(-100, 100, 0)
+ items:
+ - type: "stacking-context"
+ perspective: 400
+ transform-style: preserve-3d
+ items:
+ - type: rect
+ bounds: [0, 0, 450, 300]
+ color: 0 255 0 1.0000
+ - type: stacking-context
+ bounds: [0, 0, 600, 600]
+ transform: translate(200, 0, 0)
+ items:
+ - type: rect
+ bounds: [-200, 0, 150, 300]
+ color: 255 0 0 1.0000
+ - type: stacking-context
+ bounds: [0, 0, 600, 600]
+ transform: rotate-z(-90)
+ items:
+ -
+ bounds: [0, 200, 150, 200]
+ type: rect
+ color: 0 0 255 1.0000
diff --git a/gfx/wr/wrench/reftests/split/simple-ref.yaml b/gfx/wr/wrench/reftests/split/simple-ref.yaml
new file mode 100644
index 0000000000..de1fad9440
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/simple-ref.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1024, 1024]
+ items:
+ - type: rect
+ bounds: [150, 0, 150, 600]
+ color: [191, 127, 63]
+ - type: rect
+ bounds: [300, 0, 150, 600]
+ color: [127, 191, 63]
diff --git a/gfx/wr/wrench/reftests/split/simple.yaml b/gfx/wr/wrench/reftests/split/simple.yaml
new file mode 100644
index 0000000000..b6445780f5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/simple.yaml
@@ -0,0 +1,24 @@
+# This tests a simple intersection of 2 rotated planes to split properly.
+# If the split doesn't happen, we see a single-colored rectangle.
+# If it does, the rectangle gets split vertically in 2 parts of different colors.
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1024, 1024]
+ transform-style: preserve-3d
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 600, 600]
+ transform: rotate-y(-60.0)
+ items:
+ - type: rect
+ bounds: [0, 0, 600, 600]
+ color: [255, 0, 0, 0.5]
+ - type: stacking-context
+ bounds: [0, 0, 600, 600]
+ transform: rotate-y(60.0)
+ items:
+ - type: rect
+ bounds: [0, 0, 600, 600]
+ color: [0, 255, 0, 0.5]
diff --git a/gfx/wr/wrench/reftests/split/split-intersect1-ref.yaml b/gfx/wr/wrench/reftests/split/split-intersect1-ref.yaml
new file mode 100644
index 0000000000..134698571c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/split-intersect1-ref.yaml
@@ -0,0 +1,15 @@
+---
+root:
+ items:
+ - type: stacking-context
+ items:
+ - type: stacking-context
+ items:
+ - type: rect
+ bounds: 50 0 50 100
+ color: red
+ - type: stacking-context
+ items:
+ - type: rect
+ bounds: 0 0 50 100
+ color: green
diff --git a/gfx/wr/wrench/reftests/split/split-intersect1.yaml b/gfx/wr/wrench/reftests/split/split-intersect1.yaml
new file mode 100644
index 0000000000..dff7638707
--- /dev/null
+++ b/gfx/wr/wrench/reftests/split/split-intersect1.yaml
@@ -0,0 +1,18 @@
+---
+root:
+ items:
+ - type: stacking-context
+ transform-style: preserve-3d
+ items:
+ - type: stacking-context
+ items:
+ - type: rect
+ bounds: 0 0 100 100
+ color: red
+ - type: stacking-context
+ transform: rotate-y(-0.1)
+ bounds: 0 0 100 100
+ items:
+ - type: rect
+ bounds: 0 0 100 100
+ color: green
diff --git a/gfx/wr/wrench/reftests/text/1658-ref.yaml b/gfx/wr/wrench/reftests/text/1658-ref.yaml
new file mode 100644
index 0000000000..4f5fbe15c5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/1658-ref.yaml
@@ -0,0 +1,26 @@
+---
+root:
+ items:
+ -
+ type: "shadow"
+ blur-radius: 5
+ bounds: [14, 18, 205, 35]
+ offset: [0, 0]
+ color: black
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55]
+ offsets: [16, 43]
+ size: 18
+ color: [0, 0, 0, 0.0]
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
+ -
+ type: text
+ bounds: [14, 18, 205, 35]
+ glyphs: [55]
+ offsets: [16, 43]
+ size: 18
+ color: [0, 0, 0, 1.0]
+ font: "VeraBd.ttf"
diff --git a/gfx/wr/wrench/reftests/text/1658.yaml b/gfx/wr/wrench/reftests/text/1658.yaml
new file mode 100644
index 0000000000..523e7b1143
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/1658.yaml
@@ -0,0 +1,19 @@
+---
+root:
+ items:
+ -
+ type: "shadow"
+ blur-radius: 5
+ bounds: [14, 18, 205, 35]
+ offset: [0, 0]
+ color: black
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55]
+ offsets: [16, 43]
+ size: 18
+ color: [0, 0, 0, 1.0]
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
+
diff --git a/gfx/wr/wrench/reftests/text/Ahem.ttf b/gfx/wr/wrench/reftests/text/Ahem.ttf
new file mode 100644
index 0000000000..4d4785a412
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/Ahem.ttf
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/FreeSans.ttf b/gfx/wr/wrench/reftests/text/FreeSans.ttf
new file mode 100644
index 0000000000..9db958532c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/FreeSans.ttf
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/Proggy-License.txt b/gfx/wr/wrench/reftests/text/Proggy-License.txt
new file mode 100644
index 0000000000..f45a0d4bed
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/Proggy-License.txt
@@ -0,0 +1,10 @@
+The "Proggy.ttf" font was downloaded from https://proggyfonts.net/download/ on Dec 5, 2017.
+The following license applies to "Proggy.ttf":
+
+Copyright (c) 2004, 2005 Tristan Grimmer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/gfx/wr/wrench/reftests/text/Proggy.ttf b/gfx/wr/wrench/reftests/text/Proggy.ttf
new file mode 100644
index 0000000000..308d3e1ac9
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/Proggy.ttf
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/VeraBd.ttf b/gfx/wr/wrench/reftests/text/VeraBd.ttf
new file mode 100644
index 0000000000..51d6111d72
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/VeraBd.ttf
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/ahem-ref.yaml b/gfx/wr/wrench/reftests/text/ahem-ref.yaml
new file mode 100644
index 0000000000..061e62abbf
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/ahem-ref.yaml
@@ -0,0 +1,18 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [ 16, 16, 520, 280 ]
+ color: 128 128 128 1.0
+ - type: rect
+ bounds: [ 56, 56, 440, 200 ]
+ color: white
+ - type: rect
+ bounds: [ 56, 56, 80, 160 ]
+ color: blue
+ - type: rect
+ bounds: [ 56, 216, 120, 40 ]
+ color: black
+ - type: rect
+ bounds: [ 176, 56, 80, 160 ]
+ color: 255 165 0 1.0000
diff --git a/gfx/wr/wrench/reftests/text/ahem.yaml b/gfx/wr/wrench/reftests/text/ahem.yaml
new file mode 100644
index 0000000000..60f2dbc6a2
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/ahem.yaml
@@ -0,0 +1,219 @@
+---
+root:
+ items:
+ -
+ bounds: [0, 0, 0, 0]
+ "clip-rect": [0, 0, 0, 0]
+ "backface-visible": true
+ -
+ bounds: [0, 0, 2560, 1294]
+ "clip-rect": [0, 0, 2560, 1294]
+ "backface-visible": true
+ type: rect
+ color: white
+ -
+ bounds: [0, 0, 2560, 1294]
+ "clip-rect": [0, 0, 2560, 1294]
+ "backface-visible": true
+ type: clip
+ id: 10
+ "content-size": [2560, 1294]
+ -
+ "clip-rect": [0, 0, 2560, 1294]
+ "backface-visible": true
+ type: "scroll-frame"
+ id: 2
+ "content-size": [2560, 1294]
+ bounds: [0, 0, 2560, 1294]
+ -
+ bounds: [0, 0, 2560, 1294]
+ "clip-rect": [0, 0, 2560, 1294]
+ "backface-visible": true
+ type: clip
+ id: 3
+ "content-size": [2560, 1294]
+ -
+ bounds: [0, 0, 2560, 1294]
+ "clip-rect": [0, 0, 2560, 1294]
+ "backface-visible": true
+ type: rect
+ color: white
+ -
+ bounds: [16, 16, 520, 280]
+ "clip-rect": [16, 16, 520, 280]
+ "backface-visible": true
+ type: border
+ width: 40
+ "border-type": normal
+ color: 128 128 128 1.0000
+ style: solid
+ -
+ bounds: [56, 216, 80, 40]
+ "clip-rect": [56, 216, 80, 40]
+ "backface-visible": true
+ type: clip
+ id: 4
+ "content-size": [80, 40]
+ -
+ bounds: [56, 216, 80, 40]
+ "clip-rect": [56, 216, 80, 40]
+ "backface-visible": true
+ type: rect
+ color: black
+ -
+ bounds: [54, 55, 124, 42]
+ "clip-rect": [0, 0, 2560, 1294]
+ "backface-visible": true
+ glyphs: [67, 68]
+ offsets: [56, 88, 96, 88]
+ size: 30
+ color: 0 0 255 1.0000
+ font: "Ahem.ttf"
+ -
+ bounds: [54, 95, 124, 42]
+ "clip-rect": [0, 0, 2560, 1294]
+ "backface-visible": true
+ glyphs: [69, 70]
+ offsets: [56, 128, 96, 128]
+ size: 30
+ color: 0 0 255 1.0000
+ font: "Ahem.ttf"
+ -
+ bounds: [54, 135, 124, 42]
+ "clip-rect": [0, 0, 2560, 1294]
+ "backface-visible": true
+ glyphs: [71, 72]
+ offsets: [56, 168, 96, 168]
+ size: 30
+ color: 0 0 255 1.0000
+ font: "Ahem.ttf"
+ -
+ bounds: [54, 175, 124, 42]
+ "clip-rect": [0, 0, 2560, 1294]
+ "backface-visible": true
+ glyphs: [73, 74]
+ offsets: [56, 208, 96, 208]
+ size: 30
+ color: 0 0 255 1.0000
+ font: "Ahem.ttf"
+ -
+ bounds: [54, 215, 124, 42]
+ "clip-rect": [0, 0, 2560, 1294]
+ "backface-visible": true
+ glyphs: [19, 20, 21]
+ offsets: [56, 248, 96, 248, 136, 248]
+ size: 30
+ color: black
+ font: "Ahem.ttf"
+ -
+ bounds: [174, 55, 124, 42]
+ "clip-rect": [0, 0, 2560, 1294]
+ "backface-visible": true
+ glyphs: [75, 76]
+ offsets: [176, 88, 216, 88]
+ size: 30
+ color: 255 165 0 1.0000
+ font: "Ahem.ttf"
+ -
+ bounds: [174, 95, 124, 42]
+ "clip-rect": [0, 0, 2560, 1294]
+ "backface-visible": true
+ glyphs: [77, 78]
+ offsets: [176, 128, 216, 128]
+ size: 30
+ color: 255 165 0 1.0000
+ font: "Ahem.ttf"
+ -
+ bounds: [174, 135, 124, 42]
+ "clip-rect": [0, 0, 2560, 1294]
+ "backface-visible": true
+ glyphs: [79, 80]
+ offsets: [176, 168, 216, 168]
+ size: 30
+ color: 255 165 0 1.0000
+ font: "Ahem.ttf"
+ -
+ bounds: [174, 175, 124, 42]
+ "clip-rect": [0, 0, 2560, 1294]
+ "backface-visible": true
+ glyphs: [81, 83]
+ offsets: [176, 208, 216, 208]
+ size: 30
+ color: 255 165 0 1.0000
+ font: "Ahem.ttf"
+ -
+ bounds: [174, 215, 124, 42]
+ "clip-rect": [0, 0, 2560, 1294]
+ "backface-visible": true
+ glyphs: [67, 68]
+ offsets: [176, 248, 216, 248]
+ size: 30
+ color: white
+ font: "Ahem.ttf"
+ -
+ bounds: [294, 55, 124, 42]
+ "clip-rect": [0, 0, 2560, 1294]
+ "backface-visible": true
+ glyphs: [69, 70]
+ offsets: [296, 88, 336, 88]
+ size: 30
+ color: white
+ font: "Ahem.ttf"
+ -
+ bounds: [294, 95, 124, 42]
+ "clip-rect": [0, 0, 2560, 1294]
+ "backface-visible": true
+ glyphs: [71, 72]
+ offsets: [296, 128, 336, 128]
+ size: 30
+ color: white
+ font: "Ahem.ttf"
+ -
+ bounds: [294, 135, 124, 42]
+ "clip-rect": [0, 0, 2560, 1294]
+ "backface-visible": true
+ glyphs: [73, 74]
+ offsets: [296, 168, 336, 168]
+ size: 30
+ color: white
+ font: "Ahem.ttf"
+ -
+ bounds: [294, 175, 124, 42]
+ "clip-rect": [0, 0, 2560, 1294]
+ "backface-visible": true
+ glyphs: [84, 85]
+ offsets: [296, 208, 336, 208]
+ size: 30
+ color: white
+ font: "Ahem.ttf"
+ -
+ bounds: [294, 215, 124, 42]
+ "clip-rect": [0, 0, 2560, 1294]
+ "backface-visible": true
+ glyphs: [86, 87]
+ offsets: [296, 248, 336, 248]
+ size: 30
+ color: white
+ font: "Ahem.ttf"
+ -
+ bounds: [414, 55, 124, 42]
+ "clip-rect": [0, 0, 2560, 1294]
+ "backface-visible": true
+ glyphs: [88, 89]
+ offsets: [416, 88, 456, 88]
+ size: 30
+ color: white
+ font: "Ahem.ttf"
+ -
+ bounds: [414, 95, 124, 42]
+ "clip-rect": [0, 0, 2560, 1294]
+ "backface-visible": true
+ glyphs: [90, 91]
+ offsets: [416, 128, 456, 128]
+ size: 30
+ color: white
+ font: "Ahem.ttf"
+ -
+ bounds: [0, 0, 0, 0]
+ "clip-rect": [0, 0, 0, 0]
+ "backface-visible": true
diff --git a/gfx/wr/wrench/reftests/text/allow-subpixel-ref.yaml b/gfx/wr/wrench/reftests/text/allow-subpixel-ref.yaml
new file mode 100644
index 0000000000..d99c91b77f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/allow-subpixel-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - text: "This should not be subpixel text"
+ origin: 20 120
+ size: 18
+ color: black
+ font: "VeraBd.ttf"
diff --git a/gfx/wr/wrench/reftests/text/allow-subpixel.yaml b/gfx/wr/wrench/reftests/text/allow-subpixel.yaml
new file mode 100644
index 0000000000..d7bd98a287
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/allow-subpixel.yaml
@@ -0,0 +1,19 @@
+--- # Verify that subpixel AA is disabled if the text's enclosing stacking context requires an intermediate surface
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 200]
+ blend-container: true
+ items:
+ - text: "This should not be subpixel text"
+ origin: 20 120
+ size: 18
+ color: black
+ font: "VeraBd.ttf"
+ - type: stacking-context
+ bounds: [0, 0, 100, 100]
+ mix-blend-mode: multiply
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: white
diff --git a/gfx/wr/wrench/reftests/text/alpha-transform.png b/gfx/wr/wrench/reftests/text/alpha-transform.png
new file mode 100644
index 0000000000..e22066b8c6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/alpha-transform.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/alpha-transform.yaml b/gfx/wr/wrench/reftests/text/alpha-transform.yaml
new file mode 100644
index 0000000000..8a623b8067
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/alpha-transform.yaml
@@ -0,0 +1,10 @@
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 660, 210]
+ transform: scale(1.5, 2.5) rotate(-10)
+ items:
+ - text: "a Bcd Efgh Ijklm Nopqrs Tuvwxyz"
+ origin: 20 50
+ size: 20
+ font: "FreeSans.ttf"
diff --git a/gfx/wr/wrench/reftests/text/bg-color-ref.yaml b/gfx/wr/wrench/reftests/text/bg-color-ref.yaml
new file mode 100644
index 0000000000..01af0e93b6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/bg-color-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - text: "A"
+ origin: 30 220
+ size: 200
+ color: black
+ font: "FreeSans.ttf"
diff --git a/gfx/wr/wrench/reftests/text/bg-color.yaml b/gfx/wr/wrench/reftests/text/bg-color.yaml
new file mode 100644
index 0000000000..14f53d79be
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/bg-color.yaml
@@ -0,0 +1,15 @@
+# verify that drawing a text run on an off-screen surface with a
+# specified background color gives the same result as drawing a
+# subpixel text run directly on the background.
+---
+root:
+ items:
+ - type: stacking-context
+ transform-style: preserve-3d
+ items:
+ - text: "A"
+ origin: 30 220
+ size: 200
+ color: black
+ font: "FreeSans.ttf"
+ bg-color: white
diff --git a/gfx/wr/wrench/reftests/text/blank.yaml b/gfx/wr/wrench/reftests/text/blank.yaml
new file mode 100644
index 0000000000..c4eb3ab673
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/blank.yaml
@@ -0,0 +1,2 @@
+---
+root:
diff --git a/gfx/wr/wrench/reftests/text/blurred-shadow-local-clip-rect-ref.png b/gfx/wr/wrench/reftests/text/blurred-shadow-local-clip-rect-ref.png
new file mode 100644
index 0000000000..944d0b7847
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/blurred-shadow-local-clip-rect-ref.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/blurred-shadow-local-clip-rect.yaml b/gfx/wr/wrench/reftests/text/blurred-shadow-local-clip-rect.yaml
new file mode 100644
index 0000000000..bbfc2f1f9f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/blurred-shadow-local-clip-rect.yaml
@@ -0,0 +1,48 @@
+# This test ensures that for text that has a shadow and blurred shadow, the
+# shadows are clipped in the same way (via the local clip rect) as the shadowed
+# element.
+---
+root:
+ items:
+ -
+ type: "shadow"
+ bounds: [10, 14, 220, 200]
+ blur-radius: 0
+ offset: [0, 50]
+ color: black
+ -
+ type: "shadow"
+ bounds: [10, 14, 220, 100]
+ blur-radius: 3
+ offset: [0, 25]
+ color: black
+ -
+ bounds: [10, 14, 215, 45]
+ clip-rect: [10, 14, 122, 40]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [16, 43, 35.533333, 43, 51.533333, 43, 60.4, 43, 72.833336, 43, 80.833336, 43, 89.7, 43, 102.13333, 43, 110.13333, 43, 119, 43, 135, 43, 149.2, 43, 157.2, 43, 173.2, 43, 187.4, 43, 196.26666, 43]
+ size: 18
+ color: green
+ font: "VeraBd.ttf"
+ -
+ type: line
+ clip-rect: [10, 14, 122, 40]
+ baseline: 45
+ start: 14
+ end: 210
+ width: 3
+ orientation: horizontal
+ color: green
+ style: solid
+ -
+ type: line
+ clip-rect: [10, 14, 122, 40]
+ baseline: 32
+ start: 14
+ end: 210
+ width: 3
+ orientation: horizontal
+ color: green
+ style: solid
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/border-radius-alpha.png b/gfx/wr/wrench/reftests/text/border-radius-alpha.png
new file mode 100644
index 0000000000..e20302a562
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/border-radius-alpha.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/border-radius-subpx.png b/gfx/wr/wrench/reftests/text/border-radius-subpx.png
new file mode 100644
index 0000000000..4361f563db
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/border-radius-subpx.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/border-radius.yaml b/gfx/wr/wrench/reftests/text/border-radius.yaml
new file mode 100644
index 0000000000..e61622b957
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/border-radius.yaml
@@ -0,0 +1,18 @@
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [20, 20, 100, 100]
+ radius: 32
+ - type: rect
+ bounds: [20, 20, 100, 100]
+ color: blue
+ clip-chain: [2]
+ - text: "A"
+ origin: 30 120
+ size: 200
+ color: red
+ font: "FreeSans.ttf"
+ clip-chain: [2]
diff --git a/gfx/wr/wrench/reftests/text/clipped-transform.png b/gfx/wr/wrench/reftests/text/clipped-transform.png
new file mode 100644
index 0000000000..24459d2d3c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/clipped-transform.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/clipped-transform.yaml b/gfx/wr/wrench/reftests/text/clipped-transform.yaml
new file mode 100644
index 0000000000..7f9c9b8661
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/clipped-transform.yaml
@@ -0,0 +1,12 @@
+--- # checks that local clip rects don't inappropriately shear transformed glyphs
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 65, 70]
+ transform: [0.7086478, 0.7055624, 0, 0, -0.7055624, 0.7086478, 0, 0, 0, 0, 1, 0, 40, 10, 0, 1]
+ items:
+ - text: "O"
+ clip-rect: [0, 0, 44, 44]
+ origin: 0 38
+ size: 30
+ font: "FreeSans.ttf"
diff --git a/gfx/wr/wrench/reftests/text/color-bitmap-shadow-ref.yaml b/gfx/wr/wrench/reftests/text/color-bitmap-shadow-ref.yaml
new file mode 100644
index 0000000000..039f3c6edf
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/color-bitmap-shadow-ref.yaml
@@ -0,0 +1,30 @@
+---
+root:
+ items:
+ -
+ text: "\u263A"
+ origin: [8, 56]
+ size: 36
+ color: white
+ family: "Apple Color Emoji"
+ embedded-bitmaps: true
+ -
+ type: rect
+ bounds: [56, 56, 48, 48]
+ color: yellow
+ -
+ type: rect
+ bounds: [56, 56, 48, 12]
+ color: black
+ -
+ type: rect
+ bounds: [56, 92, 48, 12]
+ color: black
+ -
+ type: rect
+ bounds: [56, 56, 12, 48]
+ color: black
+ -
+ type: rect
+ bounds: [92, 56, 12, 48]
+ color: black
diff --git a/gfx/wr/wrench/reftests/text/color-bitmap-shadow.yaml b/gfx/wr/wrench/reftests/text/color-bitmap-shadow.yaml
new file mode 100644
index 0000000000..4ead9270cd
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/color-bitmap-shadow.yaml
@@ -0,0 +1,34 @@
+--- # checks that color emoji fast shadows use the shadow color
+root:
+ items:
+ -
+ type: "shadow"
+ bounds: [0, 0, 115, 115]
+ offset: [48, 48]
+ blur-radius: 0
+ color: yellow
+ -
+ text: "\u263A"
+ origin: [8, 56]
+ size: 36
+ color: blue
+ family: "Apple Color Emoji"
+ embedded-bitmaps: true
+ -
+ type: "pop-all-shadows"
+ -
+ type: rect
+ bounds: [56, 56, 48, 12]
+ color: black
+ -
+ type: rect
+ bounds: [56, 92, 48, 12]
+ color: black
+ -
+ type: rect
+ bounds: [56, 56, 12, 48]
+ color: black
+ -
+ type: rect
+ bounds: [92, 56, 12, 48]
+ color: black
diff --git a/gfx/wr/wrench/reftests/text/colors-alpha.png b/gfx/wr/wrench/reftests/text/colors-alpha.png
new file mode 100644
index 0000000000..3e8d985025
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/colors-alpha.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/colors-subpx.png b/gfx/wr/wrench/reftests/text/colors-subpx.png
new file mode 100644
index 0000000000..50d5c36be3
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/colors-subpx.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/colors.yaml b/gfx/wr/wrench/reftests/text/colors.yaml
new file mode 100644
index 0000000000..aceb032898
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/colors.yaml
@@ -0,0 +1,211 @@
+root:
+ items:
+ - type: rect
+ bounds: [10, 10, 250, 50]
+ color: white
+ - text: "A B C a b c"
+ origin: 20 50
+ size: 32
+ font: "FreeSans.ttf"
+
+ - type: rect
+ bounds: [270, 10, 250, 50]
+ color: red
+ - text: "A B C a b c"
+ origin: 280 50
+ size: 32
+ font: "FreeSans.ttf"
+
+ - type: rect
+ bounds: [530, 10, 250, 50]
+ color: green
+ - text: "A B C a b c"
+ origin: 540 50
+ size: 32
+ font: "FreeSans.ttf"
+
+ - type: rect
+ bounds: [10, 70, 250, 50]
+ color: blue
+ - text: "A B C a b c"
+ origin: 20 110
+ size: 32
+ font: "FreeSans.ttf"
+
+ - type: rect
+ bounds: [270, 70, 250, 50]
+ color: [100, 100, 100]
+ - text: "A B C a b c"
+ origin: 280 110
+ size: 32
+ font: "FreeSans.ttf"
+
+ - type: rect
+ bounds: [530, 70, 250, 50]
+ color: [200, 100, 150]
+ - text: "A B C a b c"
+ origin: 540 110
+ size: 32
+ font: "FreeSans.ttf"
+
+ - type: rect
+ bounds: [10, 130, 250, 50]
+ color: white
+ - text: "A B C a b c"
+ origin: 20 170
+ size: 32
+ font: "FreeSans.ttf"
+ color: white
+
+ - type: rect
+ bounds: [270, 130, 250, 50]
+ color: red
+ - text: "A B C a b c"
+ origin: 280 170
+ size: 32
+ font: "FreeSans.ttf"
+ color: white
+
+ - type: rect
+ bounds: [530, 130, 250, 50]
+ color: green
+ - text: "A B C a b c"
+ origin: 540 170
+ size: 32
+ font: "FreeSans.ttf"
+ color: white
+
+ - type: rect
+ bounds: [10, 190, 250, 50]
+ color: blue
+ - text: "A B C a b c"
+ origin: 20 230
+ size: 32
+ font: "FreeSans.ttf"
+ color: white
+
+ - type: rect
+ bounds: [270, 190, 250, 50]
+ color: [100, 100, 100]
+ - text: "A B C a b c"
+ origin: 280 230
+ size: 32
+ font: "FreeSans.ttf"
+ color: white
+
+ - type: rect
+ bounds: [530, 190, 250, 50]
+ color: [200, 100, 150]
+ - text: "A B C a b c"
+ origin: 540 230
+ size: 32
+ font: "FreeSans.ttf"
+ color: white
+
+ - type: rect
+ bounds: [10, 250, 250, 50]
+ color: white
+ - text: "A B C a b c"
+ origin: 20 290
+ size: 32
+ font: "FreeSans.ttf"
+ color: [200, 180, 200]
+
+ - type: rect
+ bounds: [270, 250, 250, 50]
+ color: red
+ - text: "A B C a b c"
+ origin: 280 290
+ size: 32
+ font: "FreeSans.ttf"
+ color: [200, 180, 200]
+
+ - type: rect
+ bounds: [530, 250, 250, 50]
+ color: green
+ - text: "A B C a b c"
+ origin: 540 290
+ size: 32
+ font: "FreeSans.ttf"
+ color: [200, 180, 200]
+
+ - type: rect
+ bounds: [10, 310, 250, 50]
+ color: blue
+ - text: "A B C a b c"
+ origin: 20 350
+ size: 32
+ font: "FreeSans.ttf"
+ color: [200, 180, 200]
+
+ - type: rect
+ bounds: [270, 310, 250, 50]
+ color: [100, 100, 100]
+ - text: "A B C a b c"
+ origin: 280 350
+ size: 32
+ font: "FreeSans.ttf"
+ color: [200, 180, 200]
+
+ - type: rect
+ bounds: [530, 310, 250, 50]
+ color: [200, 100, 150]
+ - text: "A B C a b c"
+ origin: 540 350
+ size: 32
+ font: "FreeSans.ttf"
+ color: [200, 180, 200]
+
+ - type: rect
+ bounds: [10, 370, 250, 50]
+ color: white
+ - text: "A B C a b c"
+ origin: 20 410
+ size: 32
+ font: "FreeSans.ttf"
+ color: [50, 50, 50, 0.5]
+
+ - type: rect
+ bounds: [270, 370, 250, 50]
+ color: red
+ - text: "A B C a b c"
+ origin: 280 410
+ size: 32
+ font: "FreeSans.ttf"
+ color: [50, 50, 50, 0.5]
+
+ - type: rect
+ bounds: [530, 370, 250, 50]
+ color: green
+ - text: "A B C a b c"
+ origin: 540 410
+ size: 32
+ font: "FreeSans.ttf"
+ color: [50, 50, 50, 0.5]
+
+ - type: rect
+ bounds: [10, 430, 250, 50]
+ color: blue
+ - text: "A B C a b c"
+ origin: 20 470
+ size: 32
+ font: "FreeSans.ttf"
+ color: [50, 50, 50, 0.5]
+
+ - type: rect
+ bounds: [270, 430, 250, 50]
+ color: [100, 100, 100]
+ - text: "A B C a b c"
+ origin: 280 470
+ size: 32
+ font: "FreeSans.ttf"
+ color: [50, 50, 50, 0.5]
+
+ - type: rect
+ bounds: [530, 430, 250, 50]
+ color: [200, 100, 150]
+ - text: "A B C a b c"
+ origin: 540 470
+ size: 32
+ font: "FreeSans.ttf"
+ color: [50, 50, 50, 0.5]
diff --git a/gfx/wr/wrench/reftests/text/decorations-ref.yaml b/gfx/wr/wrench/reftests/text/decorations-ref.yaml
new file mode 100644
index 0000000000..5ad75109fa
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/decorations-ref.yaml
@@ -0,0 +1,19 @@
+---
+root:
+ items:
+ - type: rect # short, horizontal
+ bounds: [ 4, 2, 5, 1 ]
+ color: green
+ - type: rect # short, vertical
+ bounds: [ 12, 14, 1, 5 ]
+ color: red
+ style: solid
+ - type: rect # long, horizontal
+ bounds: [ 34, 32, 200, 3 ]
+ color: blue
+ style: solid
+ - type: rect # long, vertical
+ bounds: [ 52, 54, 3, 200 ]
+ color: black
+ style: solid
+
diff --git a/gfx/wr/wrench/reftests/text/decorations-suite.png b/gfx/wr/wrench/reftests/text/decorations-suite.png
new file mode 100644
index 0000000000..c596ededeb
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/decorations-suite.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/decorations-suite.yaml b/gfx/wr/wrench/reftests/text/decorations-suite.yaml
new file mode 100644
index 0000000000..bbe02dcc95
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/decorations-suite.yaml
@@ -0,0 +1,348 @@
+---
+root:
+ items:
+ - type: line
+ baseline: 10
+ start: 10
+ end: 210
+ width: 1
+ orientation: horizontal
+ color: black
+ style: solid
+ - type: line
+ baseline: 20
+ start: 10
+ end: 210
+ width: 1
+ orientation: horizontal
+ color: blue
+ style: dashed
+ - type: line
+ baseline: 30
+ start: 10
+ end: 210
+ width: 1
+ orientation: horizontal
+ color: green
+ style: dotted
+ - type: line
+ baseline: 40
+ start: 10
+ end: 210
+ width: 3
+ thickness: 1
+ orientation: horizontal
+ color: red
+ style: wavy
+
+ - type: line
+ baseline: 50
+ start: 10
+ end: 210
+ width: 2
+ orientation: horizontal
+ color: black
+ style: solid
+ - type: line
+ baseline: 65
+ start: 10
+ end: 210
+ width: 2
+ orientation: horizontal
+ color: blue
+ style: dashed
+ - type: line
+ baseline: 80
+ start: 10
+ end: 207 # pruposefully cut off
+ width: 2
+ orientation: horizontal
+ color: green
+ style: dotted
+ - type: line
+ baseline: 95
+ start: 10
+ end: 210
+ width: 6
+ thickness: 2
+ orientation: horizontal
+ color: red
+ style: wavy
+
+ -
+ type: "shadow"
+ bounds: [8, 100, 225, 50]
+ blur-radius: 0
+ offset: [2, 2]
+ color: red
+ - type: line
+ baseline: 110
+ start: 10
+ end: 210
+ width: 1
+ orientation: horizontal
+ color: [0,0,0,0]
+ style: solid
+ - type: line
+ baseline: 120
+ start: 10
+ end: 210
+ width: 1
+ orientation: horizontal
+ color: [0,0,0,0]
+ style: dashed
+ - type: line
+ baseline: 130
+ start: 10
+ end: 209
+ width: 1
+ orientation: horizontal
+ color: [0,0,0,0]
+ style: dotted
+ - type: line
+ baseline: 140
+ start: 10
+ end: 210
+ width: 3
+ thickness: 1
+ orientation: horizontal
+ color: [0,0,0,0]
+ style: wavy
+ -
+ type: pop-all-shadows
+
+ -
+ type: "shadow"
+ bounds: [8, 145, 225, 65]
+ blur-radius: 1
+ offset: [2, 3]
+ color: red
+ - type: line
+ baseline: 150
+ start: 10
+ end: 210
+ width: 2
+ orientation: horizontal
+ color: black
+ style: solid
+ - type: line
+ baseline: 165
+ start: 10
+ end: 210
+ width: 2
+ orientation: horizontal
+ color: blue
+ style: dashed
+ - type: line
+ baseline: 180
+ start: 10
+ end: 207 # purposefully cut off
+ width: 2
+ orientation: horizontal
+ color: green
+ style: dotted
+ - type: line
+ baseline: 195
+ start: 10
+ end: 210
+ width: 6
+ thickness: 2
+ orientation: horizontal
+ color: red
+ style: wavy
+ -
+ type: pop-all-shadows
+
+ -
+ type: "shadow"
+ bounds: [8, 220, 225, 40]
+ blur-radius: 0
+ offset: [5, 7]
+ color: red
+ - type: line
+ baseline: 230
+ start: 10
+ end: 210
+ width: 8
+ orientation: horizontal
+ color: black
+ style: solid
+ - type: line
+ baseline: 250
+ start: 10
+ end: 210
+ width: 8
+ orientation: horizontal
+ color: blue
+ style: dashed
+ - type: line
+ baseline: 270
+ start: 10
+ end: 205 # purposefully cut off
+ width: 8
+ orientation: horizontal
+ color: green
+ style: dotted
+ - type: line
+ baseline: 290
+ start: 10
+ end: 210
+ width: 12
+ thickness: 3
+ orientation: horizontal
+ color: black
+ style: wavy
+ -
+ type: "pop-all-shadows"
+
+ -
+ type: "shadow"
+ bounds: [0, 320, 240, 140]
+ blur-radius: 3
+ offset: [5, 7]
+ color: red
+ - type: line
+ baseline: 330
+ start: 10
+ end: 210
+ width: 8
+ orientation: horizontal
+ color: black
+ style: solid
+ - type: line
+ baseline: 350
+ start: 10
+ end: 210
+ width: 8
+ orientation: horizontal
+ color: blue
+ style: dashed
+ - type: line
+ baseline: 370
+ start: 10
+ end: 205 # purposefully cut off
+ width: 8
+ orientation: horizontal
+ color: green
+ style: dotted
+ - type: line
+ baseline: 390
+ start: 10
+ end: 210
+ width: 16
+ thickness: 4
+ orientation: horizontal
+ color: black
+ style: wavy
+ -
+ type: "pop-all-shadows"
+
+ - type: line
+ baseline: 220
+ start: 10
+ end: 210
+ width: 1
+ orientation: vertical
+ color: black
+ style: solid
+ - type: line
+ baseline: 230
+ start: 10
+ end: 210
+ width: 1
+ orientation: vertical
+ color: blue
+ style: dashed
+ - type: line
+ baseline: 240
+ start: 10
+ end: 210
+ width: 1
+ orientation: vertical
+ color: green
+ style: dotted
+ - type: line
+ baseline: 250
+ start: 10
+ end: 210
+ thickness: 1
+ width: 3
+ orientation: vertical
+ color: red
+ style: wavy
+
+ - type: line
+ baseline: 270
+ start: 10
+ end: 210
+ width: 2
+ orientation: vertical
+ color: black
+ style: solid
+ - type: line
+ baseline: 290
+ start: 10
+ end: 210
+ width: 2
+ orientation: vertical
+ color: blue
+ style: dashed
+ - type: line
+ baseline: 310
+ start: 10
+ end: 207 # purposefully cut off
+ width: 2
+ orientation: vertical
+ color: green
+ style: dotted
+ - type: line
+ baseline: 330
+ start: 10
+ end: 210
+ thickness: 2
+ width: 6
+ orientation: vertical
+ color: red
+ style: wavy
+
+ -
+ type: "shadow"
+ bounds: [350, 0, 120, 240]
+ blur-radius: 3
+ offset: [5, 2]
+ color: black
+ - type: line
+ baseline: 380
+ start: 10
+ end: 210
+ width: 8
+ orientation: vertical
+ color: yellow
+ style: solid
+ - type: line
+ baseline: 400
+ start: 10
+ end: 210
+ width: 8
+ orientation: vertical
+ color: blue
+ style: dashed
+ - type: line
+ baseline: 420
+ start: 10
+ end: 205 # purposefully cut off
+ width: 8
+ orientation: vertical
+ color: green
+ style: dotted
+ - type: line
+ baseline: 440
+ start: 10
+ end: 210
+ thickness: 4
+ width: 16
+ orientation: vertical
+ color: red
+ style: wavy
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/decorations.yaml b/gfx/wr/wrench/reftests/text/decorations.yaml
new file mode 100644
index 0000000000..db15551a74
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/decorations.yaml
@@ -0,0 +1,36 @@
+---
+root:
+ items:
+ - type: line # short, horizontal
+ baseline: 2
+ start: 4
+ end: 9
+ width: 1
+ orientation: horizontal
+ color: green
+ style: solid
+ - type: line # short, vertical
+ baseline: 12
+ start: 14
+ end: 19
+ width: 1
+ orientation: vertical
+ color: red
+ style: solid
+ - type: line # long, horizontal
+ baseline: 32
+ start: 34
+ end: 234
+ width: 3
+ orientation: horizontal
+ color: blue
+ style: solid
+ - type: line # long, vertical
+ baseline: 52
+ start: 54
+ end: 254
+ width: 3
+ orientation: vertical
+ color: black
+ style: solid
+
diff --git a/gfx/wr/wrench/reftests/text/diacritics-ref.yaml b/gfx/wr/wrench/reftests/text/diacritics-ref.yaml
new file mode 100644
index 0000000000..334e1899ee
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/diacritics-ref.yaml
@@ -0,0 +1,6 @@
+root:
+ items:
+ - text: "x"
+ origin: 20 30
+ size: 20
+ font: "FreeSans.ttf"
diff --git a/gfx/wr/wrench/reftests/text/diacritics.yaml b/gfx/wr/wrench/reftests/text/diacritics.yaml
new file mode 100644
index 0000000000..7c613c3aa6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/diacritics.yaml
@@ -0,0 +1,6 @@
+root:
+ items:
+ - text: "x̂"
+ origin: 20 30
+ size: 20
+ font: "FreeSans.ttf"
diff --git a/gfx/wr/wrench/reftests/text/embedded-bitmaps.png b/gfx/wr/wrench/reftests/text/embedded-bitmaps.png
new file mode 100644
index 0000000000..a8039015a5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/embedded-bitmaps.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/embedded-bitmaps.yaml b/gfx/wr/wrench/reftests/text/embedded-bitmaps.yaml
new file mode 100644
index 0000000000..f22b11d95f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/embedded-bitmaps.yaml
@@ -0,0 +1,9 @@
+--- # checks that embedded bitmaps are rasterized and use a proper layout
+root:
+ items:
+ - text: "ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 {}[]()<>$*-+=/#_%^@\\&|~?'\"!,.;:"
+ origin: 20 30
+ bounds: [0, 0, 710, 50]
+ size: 8.25
+ font: "Proggy.ttf"
+ embedded-bitmaps: true
diff --git a/gfx/wr/wrench/reftests/text/intermediate-transform.yaml b/gfx/wr/wrench/reftests/text/intermediate-transform.yaml
new file mode 100644
index 0000000000..ab2e51804c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/intermediate-transform.yaml
@@ -0,0 +1,54 @@
+# This test case makes the text flipped relative to the surface it renders to, but not to the world.
+---
+root:
+ items:
+ -
+ type: "stacking-context"
+ items:
+ -
+ type: "reference-frame"
+ transform: [0.7753850221633911, 0, 0, 0, 0, 0.7753850221633911, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
+ items:
+ -
+ type: "stacking-context"
+ items:
+ -
+ type: "stacking-context"
+ transform-style: "preserve-3d"
+ origin: [237, 246]
+ items:
+ -
+ type: "reference-frame"
+ transform-style: "preserve-3d"
+ transform: [1, 0, 0, 0, 0, 1, 0, 0, -0.09215625375509262, -0.05100416764616966, 1, -0.0001250000059371814, 0, -3, 0, 1]
+ items:
+ -
+ type: "stacking-context"
+ transform-style: "preserve-3d"
+ items:
+ -
+ type: "reference-frame"
+ transform: [-1, 0, 0, 0, 0, -0.9659258127212524, 0.258819043636322, 0, 0, 0.258819043636322, 0.9659258127212524, 0, 1474.5, 802.0977172851563, -105.5981674194336, 1]
+ items:
+ -
+ type: "stacking-context"
+ items:
+ -
+ rect: [0, 0, 1475, 408]
+ color: red
+ -
+ type: "reference-frame"
+ transform: [-1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 1474.5, 408, 0, 1]
+ items:
+ -
+ type: "stacking-context"
+ items:
+ -
+ glyphs: [55,43,40,3,44,54,36,37,40,47,47,36]
+ offsets: [511, 290, 551.2000122070313, 290, 597.066650390625, 290, 637.2666625976563, 290, 656.1333618164063, 290, 682.0999755859375, 290, 716.6333618164063, 290, 759.6666870117188, 290, 799.8666381835938, 290, 840.066650390625, 290, 880.2666625976563, 290, 920.4666748046875, 290]
+ size: 22
+ color: black
+ font: "../text/VeraBd.ttf"
+ bounds: [509, 250, 457, 54]
+ clip-rect: [508, 249, 459, 56]
+
diff --git a/gfx/wr/wrench/reftests/text/isolated-text.png b/gfx/wr/wrench/reftests/text/isolated-text.png
new file mode 100644
index 0000000000..a0c8ed3650
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/isolated-text.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/isolated-text.yaml b/gfx/wr/wrench/reftests/text/isolated-text.yaml
new file mode 100644
index 0000000000..897d410a84
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/isolated-text.yaml
@@ -0,0 +1,14 @@
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 100]
+ transform: perspective(1000)
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 1000, 100]
+ transform-style: preserve-3d
+ items:
+ - text: "Cats making all the muffins knock over christmas tree"
+ origin: 20 30
+ size: 20
+ font: "FreeSans.ttf"
diff --git a/gfx/wr/wrench/reftests/text/large-glyphs.yaml b/gfx/wr/wrench/reftests/text/large-glyphs.yaml
new file mode 100644
index 0000000000..4bbd3ee10c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/large-glyphs.yaml
@@ -0,0 +1,8 @@
+--- # Verify that large glyphs actually render at all.
+root:
+ items:
+ - text: "HI"
+ origin: 0 1234
+ size: 1234
+ color: black
+ font: "VeraBd.ttf"
diff --git a/gfx/wr/wrench/reftests/text/large-line-decoration.yaml b/gfx/wr/wrench/reftests/text/large-line-decoration.yaml
new file mode 100644
index 0000000000..ec12a5dc31
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/large-line-decoration.yaml
@@ -0,0 +1,43 @@
+---
+root:
+ items:
+ - type: line
+ baseline: 0
+ start: 0
+ end: 50
+ width: 5000
+ thickness: 5000
+ orientation: horizontal
+ color: red
+ style: solid
+
+ - type: line
+ baseline: 0
+ start: 100
+ end: 150
+ width: 5000
+ thickness: 5000
+ orientation: horizontal
+ color: green
+ style: dashed
+
+ - type: line
+ baseline: 0
+ start: 200
+ end: 250
+ width: 5000
+ thickness: 5000
+ orientation: horizontal
+ color: blue
+ style: dotted
+
+ - type: line
+ baseline: 0
+ start: 300
+ end: 350
+ width: 5000
+ thickness: 5000
+ orientation: horizontal
+ color: yellow
+ style: wavy
+
diff --git a/gfx/wr/wrench/reftests/text/long-text.yaml b/gfx/wr/wrench/reftests/text/long-text.yaml
new file mode 100644
index 0000000000..e44658ca99
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/long-text.yaml
@@ -0,0 +1,610 @@
+---
+root:
+ items:
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55,
+ ]
+ offsets: [16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43, 16, 43,
+ ]
+ size: 18
+ color: black
+ font: "VeraBd.ttf"
diff --git a/gfx/wr/wrench/reftests/text/mix-blend-layers-ref.yaml b/gfx/wr/wrench/reftests/text/mix-blend-layers-ref.yaml
new file mode 100644
index 0000000000..0d9f6b8864
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/mix-blend-layers-ref.yaml
@@ -0,0 +1,50 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [100, 100, 0, 0]
+ blend-container: true
+ items:
+ - type: "shadow"
+ blur-radius: 0
+ offset: [-10, -4]
+ color: [255, 255, 0, 1]
+ - text: "Fault"
+ origin: 200 300
+ size: 200
+ color: [0, 0, 0, 0]
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
+
+ - type: stacking-context
+ blend-container: true
+ mix-blend-mode: darken
+ items:
+ - type: "shadow"
+ blur-radius: 0
+ offset: [6, -8]
+ color: [0, 255, 255, 1]
+ - text: "Fault"
+ origin: 200 300
+ size: 200
+ color: [0, 0, 0, 0]
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
+
+ - type: stacking-context
+ blend-container: true
+ mix-blend-mode: darken
+ items:
+ - type: "shadow"
+ blur-radius: 0
+ offset: [10, 4]
+ color: [255, 0, 255, 1]
+ - text: "Fault"
+ origin: 200 300
+ size: 200
+ color: [0, 0, 0, 0]
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/mix-blend-layers.yaml b/gfx/wr/wrench/reftests/text/mix-blend-layers.yaml
new file mode 100644
index 0000000000..5c21935100
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/mix-blend-layers.yaml
@@ -0,0 +1,53 @@
+# Verify that surfaces with differing surface/raster spatial nodes correctly sample from
+# parent mix-blend container surfaces
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [100, 100, 0, 0]
+ transform: scale(2)
+ blend-container: true
+ items:
+ - type: "shadow"
+ blur-radius: 0
+ offset: [-5, -2]
+ color: [255, 255, 0, 1]
+ - text: "Fault"
+ origin: 100 150
+ size: 100
+ color: [0, 0, 0, 0]
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
+
+ - type: stacking-context
+ blend-container: true
+ mix-blend-mode: darken
+ items:
+ - type: "shadow"
+ blur-radius: 0
+ offset: [3, -4]
+ color: [0, 255, 255, 1]
+ - text: "Fault"
+ origin: 100 150
+ size: 100
+ color: [0, 0, 0, 0]
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
+
+ - type: stacking-context
+ blend-container: true
+ mix-blend-mode: darken
+ items:
+ - type: "shadow"
+ blur-radius: 0
+ offset: [5, 2]
+ color: [255, 0, 255, 1]
+ - text: "Fault"
+ origin: 100 150
+ size: 100
+ color: [0, 0, 0, 0]
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/negative-pos.yaml b/gfx/wr/wrench/reftests/text/negative-pos.yaml
new file mode 100644
index 0000000000..7dac5c4961
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/negative-pos.yaml
@@ -0,0 +1,10 @@
+---
+root:
+ items:
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55]
+ offsets: [-2, 43]
+ size: 24
+ color: black
+ font: "VeraBd.ttf"
diff --git a/gfx/wr/wrench/reftests/text/non-opaque-notref.yaml b/gfx/wr/wrench/reftests/text/non-opaque-notref.yaml
new file mode 100644
index 0000000000..53d878b958
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/non-opaque-notref.yaml
@@ -0,0 +1,10 @@
+---
+root:
+ items:
+ -
+ bounds: [0, 0, 500, 500]
+ glyphs: [55]
+ offsets: [200, 200]
+ size: 100
+ color: [0, 0, 0]
+ font: "VeraBd.ttf"
diff --git a/gfx/wr/wrench/reftests/text/non-opaque.yaml b/gfx/wr/wrench/reftests/text/non-opaque.yaml
new file mode 100644
index 0000000000..ff2573dbfa
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/non-opaque.yaml
@@ -0,0 +1,10 @@
+---
+root:
+ items:
+ -
+ bounds: [0, 0, 500, 500]
+ glyphs: [55]
+ offsets: [200, 200]
+ size: 100
+ color: [0, 0, 0, 0.5]
+ font: "VeraBd.ttf"
diff --git a/gfx/wr/wrench/reftests/text/perspective-clip.png b/gfx/wr/wrench/reftests/text/perspective-clip.png
new file mode 100644
index 0000000000..7d19eff293
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/perspective-clip.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/perspective-clip.yaml b/gfx/wr/wrench/reftests/text/perspective-clip.yaml
new file mode 100644
index 0000000000..0fab8b3bf6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/perspective-clip.yaml
@@ -0,0 +1,28 @@
+---
+root:
+ items:
+ -
+ type: stacking-context
+ perspective: 1
+ perspective-origin: 0 0
+ items:
+ -
+ type: stacking-context
+ transform: 10 0 0 0 0 10 0 0 0 0 10 0 0 0 -9 1
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [50, 50, 200, 200]
+ radius: 8
+ -
+ type: rect
+ bounds: [50, 50, 200, 200]
+ color: green
+ clip-chain: [2]
+ -
+ text: "X"
+ origin: 100 150
+ size: 100
+ font: "Ahem.ttf"
+ clip-chain: [2]
diff --git a/gfx/wr/wrench/reftests/text/raster-space-snap-ref.yaml b/gfx/wr/wrench/reftests/text/raster-space-snap-ref.yaml
new file mode 100644
index 0000000000..43e4ccdf46
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/raster-space-snap-ref.yaml
@@ -0,0 +1,10 @@
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 480, 80]
+ raster-space: screen
+ items:
+ - text: "a Bcd Efgh Ijklm Nopqrs Tuvwxyz"
+ origin: 20.5 50
+ size: 20
+ font: "FreeSans.ttf"
diff --git a/gfx/wr/wrench/reftests/text/raster-space-snap.yaml b/gfx/wr/wrench/reftests/text/raster-space-snap.yaml
new file mode 100644
index 0000000000..4ae3f8c99f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/raster-space-snap.yaml
@@ -0,0 +1,10 @@
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 480, 80]
+ raster-space: local(1.0)
+ items:
+ - text: "a Bcd Efgh Ijklm Nopqrs Tuvwxyz"
+ origin: 20.5 50
+ size: 20
+ font: "FreeSans.ttf"
diff --git a/gfx/wr/wrench/reftests/text/raster-space.png b/gfx/wr/wrench/reftests/text/raster-space.png
new file mode 100644
index 0000000000..8289e428d5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/raster-space.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/raster-space.yaml b/gfx/wr/wrench/reftests/text/raster-space.yaml
new file mode 100644
index 0000000000..a4efba37fd
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/raster-space.yaml
@@ -0,0 +1,29 @@
+root:
+ items:
+ - type: stacking-context
+ transform: scale(5.0) rotate(-45)
+ transform-origin: 300 300
+ raster-space: local(1.0)
+ filters: [opacity(0.5)]
+ items:
+ - text: "Local"
+ origin: 20 50
+ size: 20
+ font: "FreeSans.ttf"
+ - type: stacking-context
+ transform: scale(5.0) rotate(-45)
+ transform-origin: 0 400
+ items:
+ - text: "Screen"
+ origin: 20 50
+ size: 20
+ font: "FreeSans.ttf"
+ - type: stacking-context
+ transform: scale(5.0) rotate(-45)
+ transform-origin: -80 240
+ raster-space: local(5.0)
+ items:
+ - text: "Local (scaled)"
+ origin: 20 50
+ size: 10
+ font: "FreeSans.ttf"
diff --git a/gfx/wr/wrench/reftests/text/raster_root_C_8192.yaml b/gfx/wr/wrench/reftests/text/raster_root_C_8192.yaml
new file mode 100644
index 0000000000..8bb175d211
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/raster_root_C_8192.yaml
@@ -0,0 +1,391 @@
+root:
+ items:
+ - type: "stacking-context"
+ transform: scale(0.125)
+ items:
+ - type: "stacking-context"
+ perspective: 100
+ perspective-origin: 100 50
+ blend-container: true
+ items:
+ - image: checkerboard(0, 512, 16);
+ bounds: [1600, 1600, 8192, 8192]
+ - type: rect
+ color: [180, 140, 120, 0.4]
+ bounds: 2400 2400 8192 8192
+ - type: clip
+ id: 101
+ complex:
+ - rect: [1920, 1920, 4096, 4096]
+ radius: [2048, 2048]
+ - type: clip-chain
+ id: 201
+ clips: [101]
+ - type: "stacking-context"
+ bounds: [0, 0, 8192, 8192]
+ filters: [invert(1)]
+ mix-blend-mode: exclusion
+ clip-chain: 201
+ items:
+ - type: "stacking-context"
+ transform: scale(24)
+ items:
+ - type: line
+ baseline: 16
+ start: 16
+ end: 208
+ width: 1
+ orientation: horizontal
+ color: black
+ style: solid
+ - type: line
+ baseline: 24
+ start: 16
+ end: 208
+ width: 1
+ orientation: horizontal
+ color: blue
+ style: dashed
+ - type: line
+ baseline: 32
+ start: 16
+ end: 208
+ width: 1
+ orientation: horizontal
+ color: green
+ style: dotted
+ - type: line
+ baseline: 40
+ start: 16
+ end: 208
+ width: 4
+ thickness: 1
+ orientation: horizontal
+ color: red
+ style: wavy
+ - type: line
+ baseline: 48
+ start: 16
+ end: 208
+ width: 2
+ orientation: horizontal
+ color: black
+ style: solid
+ - type: line
+ baseline: 64
+ start: 16
+ end: 208
+ width: 2
+ orientation: horizontal
+ color: blue
+ style: dashed
+ - type: line
+ baseline: 80
+ start: 16
+ end: 207 # pruposefully cut off
+ width: 2
+ orientation: horizontal
+ color: green
+ style: dotted
+ - type: line
+ baseline: 96
+ start: 16
+ end: 208
+ width: 6
+ thickness: 2
+ orientation: horizontal
+ color: red
+ style: wavy
+
+ -
+ type: "shadow"
+ bounds: [8, 100, 225, 50]
+ blur-radius: 0
+ offset: [2, 2]
+ color: red
+ - type: line
+ baseline: 112
+ start: 16
+ end: 208
+ width: 1
+ orientation: horizontal
+ color: [0,0,0,0]
+ style: solid
+ - type: line
+ baseline: 120
+ start: 16
+ end: 208
+ width: 1
+ orientation: horizontal
+ color: [0,0,0,0]
+ style: dashed
+ - type: line
+ baseline: 128
+ start: 16
+ end: 209
+ width: 1
+ orientation: horizontal
+ color: [0,0,0,0]
+ style: dotted
+ - type: line
+ baseline: 136
+ start: 16
+ end: 208
+ width: 4
+ thickness: 1
+ orientation: horizontal
+ color: [0,0,0,0]
+ style: wavy
+ -
+ type: pop-all-shadows
+
+ -
+ type: "shadow"
+ bounds: [8, 145, 225, 65]
+ blur-radius: 1
+ offset: [2, 3]
+ color: red
+ - type: line
+ baseline: 160
+ start: 16
+ end: 208
+ width: 2
+ orientation: horizontal
+ color: black
+ style: solid
+ - type: line
+ baseline: 168
+ start: 16
+ end: 208
+ width: 2
+ orientation: horizontal
+ color: blue
+ style: dashed
+ - type: line
+ baseline: 184
+ start: 16
+ end: 207 # purposefully cut off
+ width: 2
+ orientation: horizontal
+ color: green
+ style: dotted
+ - type: line
+ baseline: 192
+ start: 16
+ end: 208
+ width: 6
+ thickness: 2
+ orientation: horizontal
+ color: red
+ style: wavy
+ -
+ type: pop-all-shadows
+
+ -
+ type: "shadow"
+ bounds: [8, 220, 225, 40]
+ blur-radius: 0
+ offset: [5, 7]
+ color: red
+ - type: line
+ baseline: 232
+ start: 16
+ end: 208
+ width: 8
+ orientation: horizontal
+ color: black
+ style: solid
+ - type: line
+ baseline: 248
+ start: 16
+ end: 208
+ width: 8
+ orientation: horizontal
+ color: blue
+ style: dashed
+ - type: line
+ baseline: 272
+ start: 16
+ end: 205 # purposefully cut off
+ width: 8
+ orientation: horizontal
+ color: green
+ style: dotted
+ - type: line
+ baseline: 296
+ start: 16
+ end: 208
+ width: 12
+ thickness: 3
+ orientation: horizontal
+ color: black
+ style: wavy
+ -
+ type: "pop-all-shadows"
+
+ -
+ type: "shadow"
+ bounds: [0, 320, 240, 140]
+ blur-radius: 3
+ offset: [5, 7]
+ color: red
+ - type: line
+ baseline: 320
+ start: 16
+ end: 208
+ width: 8
+ orientation: horizontal
+ color: black
+ style: solid
+ - type: line
+ baseline: 352
+ start: 16
+ end: 208
+ width: 8
+ orientation: horizontal
+ color: blue
+ style: dashed
+ - type: line
+ baseline: 368
+ start: 16
+ end: 205 # purposefully cut off
+ width: 8
+ orientation: horizontal
+ color: green
+ style: dotted
+ - type: line
+ baseline: 392
+ start: 16
+ end: 208
+ width: 16
+ thickness: 4
+ orientation: horizontal
+ color: black
+ style: wavy
+ -
+ type: "pop-all-shadows"
+
+ - type: line
+ baseline: 224
+ start: 16
+ end: 208
+ width: 1
+ orientation: vertical
+ color: black
+ style: solid
+ - type: line
+ baseline: 232
+ start: 16
+ end: 208
+ width: 1
+ orientation: vertical
+ color: blue
+ style: dashed
+ - type: line
+ baseline: 240
+ start: 16
+ end: 208
+ width: 1
+ orientation: vertical
+ color: green
+ style: dotted
+ - type: line
+ baseline: 256
+ start: 16
+ end: 208
+ thickness: 1
+ width: 4
+ orientation: vertical
+ color: red
+ style: wavy
+
+ - type: line
+ baseline: 272
+ start: 16
+ end: 208
+ width: 2
+ orientation: vertical
+ color: black
+ style: solid
+ - type: line
+ baseline: 296
+ start: 16
+ end: 208
+ width: 2
+ orientation: vertical
+ color: blue
+ style: dashed
+ - type: line
+ baseline: 320
+ start: 16
+ end: 207 # purposefully cut off
+ width: 2
+ orientation: vertical
+ color: green
+ style: dotted
+ - type: line
+ baseline: 336
+ start: 16
+ end: 208
+ thickness: 2
+ width: 6
+ orientation: vertical
+ color: red
+ style: wavy
+
+ -
+ type: "shadow"
+ bounds: [350, 0, 120, 240]
+ blur-radius: 3
+ offset: [5, 2]
+ color: black
+ - type: line
+ baseline: 384
+ start: 16
+ end: 208
+ width: 8
+ orientation: vertical
+ color: yellow
+ style: solid
+ - type: line
+ baseline: 400
+ start: 16
+ end: 208
+ width: 8
+ orientation: vertical
+ color: blue
+ style: dashed
+ - type: line
+ baseline: 416
+ start: 16
+ end: 205 # purposefully cut off
+ width: 8
+ orientation: vertical
+ color: green
+ style: dotted
+ - type: line
+ baseline: 440
+ start: 16
+ end: 208
+ thickness: 4
+ width: 16
+ orientation: vertical
+ color: red
+ style: wavy
+ -
+ type: "pop-all-shadows"
+ - text: "side-left"
+ origin: 80 120
+ size: 32
+ transpose: true
+ flip-x: true
+ font: "VeraBd.ttf"
+ color: [40,40,40,1.0]
+ - text: "side-right"
+ origin: 240 240
+ size: 32
+ color: [190,180,200,1.0]
+ transpose: true
+ flip-y: true
+ font: "FreeSans.ttf"
+
diff --git a/gfx/wr/wrench/reftests/text/raster_root_C_ref.yaml b/gfx/wr/wrench/reftests/text/raster_root_C_ref.yaml
new file mode 100644
index 0000000000..4c28f76ced
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/raster_root_C_ref.yaml
@@ -0,0 +1,391 @@
+root:
+ items:
+ - type: "stacking-context"
+ transform: scale(0.5)
+ items:
+ - type: "stacking-context"
+ perspective: 100
+ perspective-origin: 100 50
+ blend-container: true
+ items:
+ - image: checkerboard(0, 128, 16);
+ bounds: [400, 400, 2048, 2048]
+ - type: rect
+ color: [180, 140, 120, 0.4]
+ bounds: 600 600 2048 2048
+ - type: clip
+ id: 101
+ complex:
+ - rect: [480, 480, 1024, 1024]
+ radius: [512, 512]
+ - type: clip-chain
+ id: 201
+ clips: [101]
+ - type: "stacking-context"
+ bounds: [0, 0, 2048, 2048]
+ filters: [invert(1)]
+ mix-blend-mode: exclusion
+ clip-chain: 201
+ items:
+ - type: "stacking-context"
+ transform: scale(6)
+ items:
+ - type: line
+ baseline: 16
+ start: 16
+ end: 208
+ width: 1
+ orientation: horizontal
+ color: black
+ style: solid
+ - type: line
+ baseline: 24
+ start: 16
+ end: 208
+ width: 1
+ orientation: horizontal
+ color: blue
+ style: dashed
+ - type: line
+ baseline: 32
+ start: 16
+ end: 208
+ width: 1
+ orientation: horizontal
+ color: green
+ style: dotted
+ - type: line
+ baseline: 40
+ start: 16
+ end: 208
+ width: 4
+ thickness: 1
+ orientation: horizontal
+ color: red
+ style: wavy
+ - type: line
+ baseline: 48
+ start: 16
+ end: 208
+ width: 2
+ orientation: horizontal
+ color: black
+ style: solid
+ - type: line
+ baseline: 64
+ start: 16
+ end: 208
+ width: 2
+ orientation: horizontal
+ color: blue
+ style: dashed
+ - type: line
+ baseline: 80
+ start: 16
+ end: 207 # pruposefully cut off
+ width: 2
+ orientation: horizontal
+ color: green
+ style: dotted
+ - type: line
+ baseline: 96
+ start: 16
+ end: 208
+ width: 6
+ thickness: 2
+ orientation: horizontal
+ color: red
+ style: wavy
+
+ -
+ type: "shadow"
+ bounds: [8, 100, 225, 50]
+ blur-radius: 0
+ offset: [2, 2]
+ color: red
+ - type: line
+ baseline: 112
+ start: 16
+ end: 208
+ width: 1
+ orientation: horizontal
+ color: [0,0,0,0]
+ style: solid
+ - type: line
+ baseline: 120
+ start: 16
+ end: 208
+ width: 1
+ orientation: horizontal
+ color: [0,0,0,0]
+ style: dashed
+ - type: line
+ baseline: 128
+ start: 16
+ end: 209
+ width: 1
+ orientation: horizontal
+ color: [0,0,0,0]
+ style: dotted
+ - type: line
+ baseline: 136
+ start: 16
+ end: 208
+ width: 4
+ thickness: 1
+ orientation: horizontal
+ color: [0,0,0,0]
+ style: wavy
+ -
+ type: pop-all-shadows
+
+ -
+ type: "shadow"
+ bounds: [8, 145, 225, 65]
+ blur-radius: 1
+ offset: [2, 3]
+ color: red
+ - type: line
+ baseline: 160
+ start: 16
+ end: 208
+ width: 2
+ orientation: horizontal
+ color: black
+ style: solid
+ - type: line
+ baseline: 168
+ start: 16
+ end: 208
+ width: 2
+ orientation: horizontal
+ color: blue
+ style: dashed
+ - type: line
+ baseline: 184
+ start: 16
+ end: 207 # purposefully cut off
+ width: 2
+ orientation: horizontal
+ color: green
+ style: dotted
+ - type: line
+ baseline: 192
+ start: 16
+ end: 208
+ width: 6
+ thickness: 2
+ orientation: horizontal
+ color: red
+ style: wavy
+ -
+ type: pop-all-shadows
+
+ -
+ type: "shadow"
+ bounds: [8, 220, 225, 40]
+ blur-radius: 0
+ offset: [5, 7]
+ color: red
+ - type: line
+ baseline: 232
+ start: 16
+ end: 208
+ width: 8
+ orientation: horizontal
+ color: black
+ style: solid
+ - type: line
+ baseline: 248
+ start: 16
+ end: 208
+ width: 8
+ orientation: horizontal
+ color: blue
+ style: dashed
+ - type: line
+ baseline: 272
+ start: 16
+ end: 205 # purposefully cut off
+ width: 8
+ orientation: horizontal
+ color: green
+ style: dotted
+ - type: line
+ baseline: 296
+ start: 16
+ end: 208
+ width: 12
+ thickness: 3
+ orientation: horizontal
+ color: black
+ style: wavy
+ -
+ type: "pop-all-shadows"
+
+ -
+ type: "shadow"
+ bounds: [0, 320, 240, 140]
+ blur-radius: 3
+ offset: [5, 7]
+ color: red
+ - type: line
+ baseline: 320
+ start: 16
+ end: 208
+ width: 8
+ orientation: horizontal
+ color: black
+ style: solid
+ - type: line
+ baseline: 352
+ start: 16
+ end: 208
+ width: 8
+ orientation: horizontal
+ color: blue
+ style: dashed
+ - type: line
+ baseline: 368
+ start: 16
+ end: 205 # purposefully cut off
+ width: 8
+ orientation: horizontal
+ color: green
+ style: dotted
+ - type: line
+ baseline: 392
+ start: 16
+ end: 208
+ width: 16
+ thickness: 4
+ orientation: horizontal
+ color: black
+ style: wavy
+ -
+ type: "pop-all-shadows"
+
+ - type: line
+ baseline: 224
+ start: 16
+ end: 208
+ width: 1
+ orientation: vertical
+ color: black
+ style: solid
+ - type: line
+ baseline: 232
+ start: 16
+ end: 208
+ width: 1
+ orientation: vertical
+ color: blue
+ style: dashed
+ - type: line
+ baseline: 240
+ start: 16
+ end: 208
+ width: 1
+ orientation: vertical
+ color: green
+ style: dotted
+ - type: line
+ baseline: 256
+ start: 16
+ end: 208
+ thickness: 1
+ width: 4
+ orientation: vertical
+ color: red
+ style: wavy
+
+ - type: line
+ baseline: 272
+ start: 16
+ end: 208
+ width: 2
+ orientation: vertical
+ color: black
+ style: solid
+ - type: line
+ baseline: 296
+ start: 16
+ end: 208
+ width: 2
+ orientation: vertical
+ color: blue
+ style: dashed
+ - type: line
+ baseline: 320
+ start: 16
+ end: 207 # purposefully cut off
+ width: 2
+ orientation: vertical
+ color: green
+ style: dotted
+ - type: line
+ baseline: 336
+ start: 16
+ end: 208
+ thickness: 2
+ width: 6
+ orientation: vertical
+ color: red
+ style: wavy
+
+ -
+ type: "shadow"
+ bounds: [350, 0, 120, 240]
+ blur-radius: 3
+ offset: [5, 2]
+ color: black
+ - type: line
+ baseline: 384
+ start: 16
+ end: 208
+ width: 8
+ orientation: vertical
+ color: yellow
+ style: solid
+ - type: line
+ baseline: 400
+ start: 16
+ end: 208
+ width: 8
+ orientation: vertical
+ color: blue
+ style: dashed
+ - type: line
+ baseline: 416
+ start: 16
+ end: 205 # purposefully cut off
+ width: 8
+ orientation: vertical
+ color: green
+ style: dotted
+ - type: line
+ baseline: 440
+ start: 16
+ end: 208
+ thickness: 4
+ width: 16
+ orientation: vertical
+ color: red
+ style: wavy
+ -
+ type: "pop-all-shadows"
+ - text: "side-left"
+ origin: 80 120
+ size: 32
+ transpose: true
+ flip-x: true
+ font: "VeraBd.ttf"
+ color: [40,40,40,1.0]
+ - text: "side-right"
+ origin: 240 240
+ size: 32
+ color: [190,180,200,1.0]
+ transpose: true
+ flip-y: true
+ font: "FreeSans.ttf"
+
diff --git a/gfx/wr/wrench/reftests/text/reftest.list b/gfx/wr/wrench/reftests/text/reftest.list
new file mode 100644
index 0000000000..3cd124085a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/reftest.list
@@ -0,0 +1,85 @@
+!= text.yaml blank.yaml
+!= long-text.yaml blank.yaml
+!= negative-pos.yaml blank.yaml
+!= shadow.yaml text.yaml
+!= shadow-single.yaml blank.yaml
+!= shadow-cover-1.yaml blank.yaml
+!= shadow-cover-2.yaml blank.yaml
+
+skip_on(android,device) fuzzy(1,3) == shadow.yaml shadow-ref.yaml # Fails on Pixel2
+fuzzy(1,1) == shadow-huge.yaml shadow-huge-ref.yaml
+!= shadow-cover-1.yaml shadow-cover-2.yaml
+!= shadow-many.yaml shadow.yaml
+!= shadow-complex.yaml shadow-many.yaml
+!= shadow-clipped-text.yaml blank.yaml
+!= non-opaque.yaml non-opaque-notref.yaml
+== decorations.yaml decorations-ref.yaml
+skip_on(android,device) fuzzy(1,3692) fuzzy-if(platform(swgl),3,13540) == decorations-suite.yaml decorations-suite.png # Fails on Pixel2
+== 1658.yaml 1658-ref.yaml
+fuzzy(2,405) fuzzy-if(platform(swgl),2,1508) == split-batch.yaml split-batch-ref.yaml
+# Next 3 tests affected by bug 1548099 on Android
+skip_on(android) == shadow-red.yaml shadow-red-ref.yaml
+skip_on(android) fuzzy(1,999) fuzzy-if(platform(swgl),2,1324) == shadow-grey.yaml shadow-grey-ref.yaml
+skip_on(android) fuzzy(1,828) fuzzy-if(platform(swgl),2,1538) == shadow-grey-transparent.yaml shadow-grey-ref.yaml
+== subtle-shadow.yaml subtle-shadow-ref.yaml
+fuzzy(1,64) == shadow-atomic.yaml shadow-atomic-ref.yaml
+fuzzy(1,64) == shadow-clip-rect.yaml shadow-atomic-ref.yaml
+fuzzy(1,1) platform(linux) == shadow-ordering.yaml shadow-ordering-ref.yaml
+# Next 6 tests affected by bug 1548099 on Android
+skip_on(android) != synthetic-bold.yaml synthetic-bold-not-ref.yaml
+skip_on(android) fuzzy(1,1786) options(disable-subpixel) == synthetic-bold-transparent.yaml synthetic-bold-transparent-ref.yaml
+skip_on(android) != synthetic-bold-transparent.yaml synthetic-bold.yaml
+skip_on(android) != synthetic-italics.yaml synthetic-italics-ref.yaml
+skip_on(android) != synthetic-italics-custom.yaml synthetic-italics-ref.yaml
+skip_on(android) != synthetic-italics-custom.yaml synthetic-italics.yaml
+options(disable-aa) == ahem.yaml ahem-ref.yaml
+platform(linux) == isolated-text.yaml isolated-text.png
+platform(mac) skip_on(mac,>=10.14) fuzzy(3,67) == white-opacity.yaml white-opacity.png
+fuzzy(1,113) platform(linux) options(disable-subpixel) == colors.yaml colors-alpha.png
+fuzzy(1,774) platform(linux) draw_calls(3) == colors.yaml colors-subpx.png
+platform(linux) options(disable-subpixel) == border-radius.yaml border-radius-alpha.png
+platform(linux) == border-radius.yaml border-radius-subpx.png
+options(disable-aa) == transparent-no-aa.yaml transparent-no-aa-ref.yaml
+!= diacritics.yaml diacritics-ref.yaml
+fuzzy(1,1) platform(linux) options(disable-subpixel) == text-masking.yaml text-masking-alpha.png
+fuzzy(1,44) platform(linux) == text-masking.yaml text-masking-subpx.png
+fuzzy(1,30) platform(linux) options(disable-subpixel) == alpha-transform.yaml alpha-transform.png
+fuzzy(1,20) platform(linux) == subpixel-rotate.yaml subpixel-rotate.png
+fuzzy(1,72) platform(linux) == subpixel-scale.yaml subpixel-scale.png
+fuzzy(1,26) platform(linux) == subpixel-skew.yaml subpixel-skew.png
+fuzzy(1,381) platform(linux) == subpixel-translate.yaml subpixel-translate-ref.yaml
+!= shadow-rotate.yaml blank.yaml
+platform(linux) == embedded-bitmaps.yaml embedded-bitmaps.png
+fuzzy(1,13) platform(linux) == clipped-transform.yaml clipped-transform.png
+platform(mac) fuzzy(195,30) == color-bitmap-shadow.yaml color-bitmap-shadow-ref.yaml
+platform(linux) == writing-modes.yaml writing-modes-ref.yaml
+fuzzy(1,5) platform(linux) == blurred-shadow-local-clip-rect.yaml blurred-shadow-local-clip-rect-ref.png
+fuzzy(1,1) platform(linux) == two-shadows.yaml two-shadows.png
+== shadow-clip.yaml shadow-clip-ref.yaml
+== shadow-fast-clip.yaml shadow-fast-clip-ref.yaml
+skip_on(android,device) == shadow-partial-glyph.yaml shadow-partial-glyph-ref.yaml # Fails on Pixel2
+fuzzy(2,212) platform(linux) == shadow-transforms.yaml shadow-transforms.png
+fuzzy(2,370) platform(linux) == raster-space.yaml raster-space.png
+skip_on(android) skip_on(mac,>=10.14) != allow-subpixel.yaml allow-subpixel-ref.yaml # Android: we don't enable sub-px aa on this platform.
+skip_on(android,device) fuzzy-if(platform(swgl),1,1085) == bg-color.yaml bg-color-ref.yaml # Fails on Pixel2
+!= large-glyphs.yaml blank.yaml
+!= large-line-decoration.yaml blank.yaml
+skip_on(android,device) == snap-text-offset.yaml snap-text-offset-ref.yaml
+fuzzy(5,4435) == shadow-border.yaml shadow-solid-ref.yaml
+fuzzy(5,4435) == shadow-image.yaml shadow-solid-ref.yaml
+options(disable-aa) == snap-clip.yaml snap-clip-ref.yaml
+platform(linux) == perspective-clip.yaml perspective-clip.png
+fuzzy(1,150) 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
+fuzzy(1,15) platform(linux) force_subpixel_aa_where_possible(true) == text-fixed-slice.yaml text-fixed-slice-slow.png
+fuzzy(1,15) platform(linux) force_subpixel_aa_where_possible(false) == text-fixed-slice.yaml text-fixed-slice-fast.png
+
+# a 8544x8544 raster root vs. 2136x2136
+# most pixels are off by a small amount, but a few pixels on the edge vary by a lot, pushing up the fuzzy max-diff;
+# the main goal of the test is that everything is in the same place, at the same scale, clipped the same way,
+# despite 4x on-the-fly scale change.
+skip_on(android) fuzzy(120,20800) == raster_root_C_8192.yaml raster_root_C_ref.yaml
+== subpx-bg-mask.yaml subpx-bg-mask-ref.yaml
+platform(linux,mac) == rotate-snap-clip.yaml rotate-snap-clip-ref.yaml
+platform(linux,mac) == rotate-snap-filter.yaml rotate-snap-filter-ref.yaml
+fuzzy-if(env(android,device),47,6860) fuzzy-if(not(env(android,device)),4,6600) == mix-blend-layers.yaml mix-blend-layers-ref.yaml # Android device is Samsung Galaxy A51
diff --git a/gfx/wr/wrench/reftests/text/rotate-snap-clip-ref.yaml b/gfx/wr/wrench/reftests/text/rotate-snap-clip-ref.yaml
new file mode 100644
index 0000000000..b0212e9929
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/rotate-snap-clip-ref.yaml
@@ -0,0 +1,26 @@
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [0, 0, 400, 30]
+ radius: {
+ top-left: 2,
+ top-right: 2,
+ bottom-left: 2,
+ bottom-right: 2,
+ }
+ - type: clip-chain
+ id: 12
+ clips: [2]
+ - type: stacking-context
+ clip-chain: 12
+ items:
+ - type: stacking-context
+ items:
+ - text: "Should be upright"
+ origin: 20 24
+ size: 18
+ color: black
+ font: "VeraBd.ttf"
diff --git a/gfx/wr/wrench/reftests/text/rotate-snap-clip.yaml b/gfx/wr/wrench/reftests/text/rotate-snap-clip.yaml
new file mode 100644
index 0000000000..3861bf0e21
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/rotate-snap-clip.yaml
@@ -0,0 +1,29 @@
+# Verify that snapping on a clipped surface with a 180 deg rotation + a local 180 deg rotation works correctly
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [0, 0, 400, 30]
+ radius: {
+ top-left: 2,
+ top-right: 2,
+ bottom-left: 2,
+ bottom-right: 2,
+ }
+ - type: clip-chain
+ id: 12
+ clips: [2]
+ - type: stacking-context
+ clip-chain: 12
+ transform: rotate(180)
+ items:
+ - type: stacking-context
+ transform: rotate(180)
+ items:
+ - text: "Should be upright"
+ origin: 20 24
+ size: 18
+ color: black
+ font: "VeraBd.ttf"
diff --git a/gfx/wr/wrench/reftests/text/rotate-snap-filter-ref.yaml b/gfx/wr/wrench/reftests/text/rotate-snap-filter-ref.yaml
new file mode 100644
index 0000000000..ae230425ac
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/rotate-snap-filter-ref.yaml
@@ -0,0 +1,14 @@
+# Verify that snapping on a filtered surface with a 180 deg rotation + a local 180 deg rotation works correctly
+---
+root:
+ items:
+ - type: stacking-context
+ filters: [opacity(0.5)]
+ items:
+ - type: stacking-context
+ items:
+ - text: "Should be upright"
+ origin: 20 24
+ size: 18
+ color: black
+ font: "VeraBd.ttf"
diff --git a/gfx/wr/wrench/reftests/text/rotate-snap-filter.yaml b/gfx/wr/wrench/reftests/text/rotate-snap-filter.yaml
new file mode 100644
index 0000000000..48be649aa2
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/rotate-snap-filter.yaml
@@ -0,0 +1,16 @@
+# Verify that snapping on a filtered surface with a 180 deg rotation + a local 180 deg rotation works correctly
+---
+root:
+ items:
+ - type: stacking-context
+ filters: [opacity(0.5)]
+ transform: rotate(180)
+ items:
+ - type: stacking-context
+ transform: rotate(180)
+ items:
+ - text: "Should be upright"
+ origin: 20 24
+ size: 18
+ color: black
+ font: "VeraBd.ttf"
diff --git a/gfx/wr/wrench/reftests/text/shadow-atomic-ref.yaml b/gfx/wr/wrench/reftests/text/shadow-atomic-ref.yaml
new file mode 100644
index 0000000000..ccf159b5a7
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-atomic-ref.yaml
@@ -0,0 +1,81 @@
+---
+root:
+ items:
+# equivalent of shadow with [20, 16] offset
+ -
+ type: line
+ baseline: 61
+ start: 34
+ end: 230
+ width: 3
+ orientation: horizontal
+ color: green
+ style: solid
+ -
+ bounds: [34, 34, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [36, 59, 55.533333, 59, 71.533333, 59, 80.4, 59, 92.833336, 59, 100.833336, 59, 109.7, 59, 122.13333, 59, 130.13333, 59, 139, 59, 155, 59, 169.2, 59, 177.2, 59, 193.2, 59, 207.4, 59, 216.26666, 59]
+ size: 18
+ color: green
+ font: "VeraBd.ttf"
+ -
+ type: line
+ baseline: 48
+ start: 34
+ end: 230
+ width: 3
+ orientation: horizontal
+ color: green
+ style: solid
+# equivalent of shadow with [10, 8] offset
+ -
+ type: line
+ baseline: 53
+ start: 24
+ end: 220
+ width: 3
+ orientation: horizontal
+ color: black
+ style: solid
+ -
+ bounds: [24, 26, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [26, 51, 45.533333, 51, 61.533333, 51, 70.4, 51, 82.833336, 51, 90.833336, 51, 99.7, 51, 112.13333, 51, 120.13333, 51, 129, 51, 145, 51, 159.2, 51, 167.2, 51, 183.2, 51, 197.4, 51, 206.26666, 51]
+ size: 18
+ color: black
+ font: "VeraBd.ttf"
+ -
+ type: line
+ baseline: 40
+ start: 24
+ end: 220
+ width: 3
+ orientation: horizontal
+ color: black
+ style: solid
+# same as shadow-atomic.yaml
+ -
+ type: line
+ baseline: 45
+ start: 14
+ end: 210
+ width: 3
+ orientation: horizontal
+ color: red
+ style: solid
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [16, 43, 35.533333, 43, 51.533333, 43, 60.4, 43, 72.833336, 43, 80.833336, 43, 89.7, 43, 102.13333, 43, 110.13333, 43, 119, 43, 135, 43, 149.2, 43, 157.2, 43, 173.2, 43, 187.4, 43, 196.26666, 43]
+ size: 18
+ color: red
+ font: "VeraBd.ttf"
+ -
+ type: line
+ baseline: 32
+ start: 14
+ end: 210
+ width: 3
+ orientation: horizontal
+ color: red
+ style: solid
diff --git a/gfx/wr/wrench/reftests/text/shadow-atomic.yaml b/gfx/wr/wrench/reftests/text/shadow-atomic.yaml
new file mode 100644
index 0000000000..21b9eac876
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-atomic.yaml
@@ -0,0 +1,42 @@
+--- # checks that decorations on "real" content and "shadow" content are on seperate, atomic, layers
+root:
+ items:
+ -
+ type: "shadow"
+ bounds: [14, 18, 205, 35]
+ blur-radius: 0
+ offset: [20, 16]
+ color: green
+ -
+ type: "shadow"
+ bounds: [14, 18, 205, 35]
+ blur-radius: 0
+ offset: [10, 8]
+ color: black
+ -
+ type: line
+ baseline: 45
+ start: 14
+ end: 210
+ width: 3
+ orientation: horizontal
+ color: red
+ style: solid
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [16, 43, 35.533333, 43, 51.533333, 43, 60.4, 43, 72.833336, 43, 80.833336, 43, 89.7, 43, 102.13333, 43, 110.13333, 43, 119, 43, 135, 43, 149.2, 43, 157.2, 43, 173.2, 43, 187.4, 43, 196.26666, 43]
+ size: 18
+ color: red
+ font: "VeraBd.ttf"
+ -
+ type: line
+ baseline: 32
+ start: 14
+ end: 210
+ width: 3
+ orientation: horizontal
+ color: red
+ style: solid
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/shadow-border.yaml b/gfx/wr/wrench/reftests/text/shadow-border.yaml
new file mode 100644
index 0000000000..b05fb88cb5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-border.yaml
@@ -0,0 +1,19 @@
+---
+root:
+ items:
+ -
+ type: "shadow"
+ blur-radius: 25
+ bounds: [0, 0, 90, 90]
+ offset: [0, 0]
+ color: [0, 0, 0, 0.8]
+ -
+ type: border
+ bounds: [ 30, 30, 30, 30 ]
+ width: [ 15, 15, 15, 15 ]
+ border-type: normal
+ style: solid
+ color: blue
+ radius: 0
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/shadow-clip-rect.yaml b/gfx/wr/wrench/reftests/text/shadow-clip-rect.yaml
new file mode 100644
index 0000000000..6edf6ccb5a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-clip-rect.yaml
@@ -0,0 +1,45 @@
+--- # checks that decorations on "real" content and "shadow" content are on seperate, atomic, layers
+root:
+ items:
+ -
+ type: "shadow"
+ bounds: [14, 18, 205, 35]
+ blur-radius: 0
+ offset: [20, 16]
+ color: green
+ -
+ type: "shadow"
+ bounds: [14, 18, 205, 35]
+ blur-radius: 0
+ offset: [10, 8]
+ color: black
+ -
+ type: line
+ clip-rect: [14, 18, 205, 35]
+ baseline: 45
+ start: 14
+ end: 210
+ width: 3
+ orientation: horizontal
+ color: red
+ style: solid
+ -
+ bounds: [14, 18, 205, 35]
+ clip-rect: [14, 18, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [16, 43, 35.533333, 43, 51.533333, 43, 60.4, 43, 72.833336, 43, 80.833336, 43, 89.7, 43, 102.13333, 43, 110.13333, 43, 119, 43, 135, 43, 149.2, 43, 157.2, 43, 173.2, 43, 187.4, 43, 196.26666, 43]
+ size: 18
+ color: red
+ font: "VeraBd.ttf"
+ -
+ type: line
+ clip-rect: [14, 18, 205, 35]
+ baseline: 32
+ start: 14
+ end: 210
+ width: 3
+ orientation: horizontal
+ color: red
+ style: solid
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/shadow-clip-ref.yaml b/gfx/wr/wrench/reftests/text/shadow-clip-ref.yaml
new file mode 100644
index 0000000000..ff57bfde63
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-clip-ref.yaml
@@ -0,0 +1,11 @@
+# Test that the clip-chain local rect is not applied
+# to the text run being drawn in the shadow. If it was
+# applied, the edges of the quad will be a slightly
+# different color, since the blur will include some
+# clipped out pixels.
+---
+root:
+ items:
+ - type: rect
+ bounds: [28, 28, 80, 80]
+ color: [255, 127, 127, 1]
diff --git a/gfx/wr/wrench/reftests/text/shadow-clip.yaml b/gfx/wr/wrench/reftests/text/shadow-clip.yaml
new file mode 100644
index 0000000000..92b5886acf
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-clip.yaml
@@ -0,0 +1,26 @@
+# Test that the clip-chain local rect is not applied
+# to the text run being drawn in the shadow. If it was
+# applied, the edges of the quad will be a slightly
+# different color, since the blur will include some
+# clipped out pixels.
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ bounds: [28, 28, 80, 80]
+ -
+ type: "shadow"
+ bounds: [0, 0, 200, 200]
+ blur-radius: 2
+ color: [255, 0, 0, 0.5]
+ clip-chain: [2]
+ -
+ bounds: [6, 6, 132, 133]
+ glyphs: [67]
+ offsets: [8, 111]
+ size: 128
+ color: [0, 0, 0, 0]
+ font: "Ahem.ttf"
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/shadow-clipped-text.yaml b/gfx/wr/wrench/reftests/text/shadow-clipped-text.yaml
new file mode 100644
index 0000000000..9ab01d0a3f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-clipped-text.yaml
@@ -0,0 +1,24 @@
+# Tests that the unclipped parts of a shadow still render even if the associated
+# text is clipped out.
+---
+root:
+ items:
+ - type: scroll-frame
+ bounds: [14, 18, 10, 5]
+ content-size: [10, 5]
+ items:
+ -
+ type: "shadow"
+ bounds: [11, 20, 100, 100]
+ blur-radius: 3
+ offset: [0, 0]
+ color: black
+ -
+ bounds: [14, 23, 100, 100]
+ glyphs: [55]
+ offsets: [16, 43]
+ size: 18
+ color: black
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/shadow-complex.yaml b/gfx/wr/wrench/reftests/text/shadow-complex.yaml
new file mode 100644
index 0000000000..e1bfbc1b3a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-complex.yaml
@@ -0,0 +1,46 @@
+--- # The same as shadow-many.yaml, except the shadows only apply to parts of the text
+root:
+ items:
+ -
+ type: "shadow"
+ bounds: [14, 18, 205, 35]
+ blur-radius: 5
+ offset: [0, 0]
+ color: black
+ -
+ type: "shadow"
+ bounds: [14, 18, 205, 35]
+ blur-radius: 0
+ offset: [2, 3]
+ color: red
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55, 75, 76, 86]
+ offsets: [16, 43, 35.533333, 43, 51.533333, 43, 60.4, 43]
+ size: 18
+ color: [0, 0, 0, 0] # actual text is transparent
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
+ -
+ type: "shadow"
+ bounds: [14, 18, 205, 35]
+ blur-radius: 3
+ offset: [-2, 3.5]
+ color: blue
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [3, 76, 86, 3, 87, 75]
+ offsets: [72.833336, 43, 80.833336, 43, 89.7, 43, 102.13333, 43, 110.13333, 43, 119, 43]
+ size: 18
+ color: [0, 0, 0, 0] # actual text is transparent
+ font: "VeraBd.ttf"
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [72, 3, 69, 72, 86, 87]
+ offsets: [135, 43, 149.2, 43, 157.2, 43, 173.2, 43, 187.4, 43, 196.26666, 43]
+ size: 18
+ color: [0, 0, 0, 0] # actual text is transparent
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/shadow-cover-1.yaml b/gfx/wr/wrench/reftests/text/shadow-cover-1.yaml
new file mode 100644
index 0000000000..088c26f1c5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-cover-1.yaml
@@ -0,0 +1,18 @@
+--- # text covering a shadow
+root:
+ items:
+ -
+ type: "shadow"
+ bounds: [14, 18, 205, 35]
+ blur-radius: 3
+ offset: [0, 0]
+ color: red
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [16, 43, 35.533333, 43, 51.533333, 43, 60.4, 43, 72.833336, 43, 80.833336, 43, 89.7, 43, 102.13333, 43, 110.13333, 43, 119, 43, 135, 43, 149.2, 43, 157.2, 43, 173.2, 43, 187.4, 43, 196.26666, 43]
+ size: 18
+ color: green
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/shadow-cover-2.yaml b/gfx/wr/wrench/reftests/text/shadow-cover-2.yaml
new file mode 100644
index 0000000000..c18e277340
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-cover-2.yaml
@@ -0,0 +1,26 @@
+--- # shadow covering text
+root:
+ items:
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [16, 43, 35.533333, 43, 51.533333, 43, 60.4, 43, 72.833336, 43, 80.833336, 43, 89.7, 43, 102.13333, 43, 110.13333, 43, 119, 43, 135, 43, 149.2, 43, 157.2, 43, 173.2, 43, 187.4, 43, 196.26666, 43]
+ size: 18
+ color: green
+ font: "VeraBd.ttf"
+ -
+ type: "shadow"
+ bounds: [14, 18, 205, 35]
+ blur-radius: 3
+ offset: [0, 0]
+ color: red
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [16, 43, 35.533333, 43, 51.533333, 43, 60.4, 43, 72.833336, 43, 80.833336, 43, 89.7, 43, 102.13333, 43, 110.13333, 43, 119, 43, 135, 43, 149.2, 43, 157.2, 43, 173.2, 43, 187.4, 43, 196.26666, 43]
+ size: 18
+ color: [0, 0, 0, 0] # text transparent
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
+
diff --git a/gfx/wr/wrench/reftests/text/shadow-fast-clip-ref.yaml b/gfx/wr/wrench/reftests/text/shadow-fast-clip-ref.yaml
new file mode 100644
index 0000000000..490b7d7b51
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-fast-clip-ref.yaml
@@ -0,0 +1,23 @@
+# Test that fast shadows actually apply clips
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ bounds: [28, 28, 80, 80]
+ -
+ bounds: [6, 6, 132, 133]
+ text: "overflow text"
+ origin: [9, 101]
+ size: 12
+ color: [0, 0, 0, 1]
+ font: "VeraBd.ttf"
+ clip-chain: [2]
+ -
+ bounds: [6, 6, 132, 133]
+ text: "overflow text"
+ origin: [8, 100]
+ size: 12
+ color: [255, 0, 0, 1]
+ font: "VeraBd.ttf"
+ clip-chain: [2]
diff --git a/gfx/wr/wrench/reftests/text/shadow-fast-clip.yaml b/gfx/wr/wrench/reftests/text/shadow-fast-clip.yaml
new file mode 100644
index 0000000000..2a33267c96
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-fast-clip.yaml
@@ -0,0 +1,24 @@
+# Test that fast shadows actually apply clips
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ bounds: [28, 28, 80, 80]
+ -
+ type: "shadow"
+ bounds: [0, 0, 200, 200]
+ blur-radius: 0
+ offset: [1, 1]
+ color: [0, 0, 0, 1]
+ clip-chain: [2]
+ -
+ bounds: [6, 6, 132, 133]
+ text: "overflow text"
+ origin: [8, 100]
+ size: 12
+ color: [255, 0, 0, 1]
+ font: "VeraBd.ttf"
+ clip-chain: [2]
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/shadow-grey-ref.yaml b/gfx/wr/wrench/reftests/text/shadow-grey-ref.yaml
new file mode 100644
index 0000000000..d40d997161
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-grey-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - rect: [14, 10, 205, 38]
+ color: [100, 100, 100, 1.0]
+
diff --git a/gfx/wr/wrench/reftests/text/shadow-grey-transparent.yaml b/gfx/wr/wrench/reftests/text/shadow-grey-transparent.yaml
new file mode 100644
index 0000000000..ba54ede1e2
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-grey-transparent.yaml
@@ -0,0 +1,19 @@
+---
+root:
+ items:
+ - rect: [14, 10, 205, 38]
+ color: [100, 100, 100, 1.0]
+ -
+ type: "shadow"
+ blur-radius: 2
+ color: [100, 100, 100, 0.2]
+ bounds: [14, 10, 205, 38]
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [16, 43, 35.533333, 43, 51.533333, 43, 60.4, 43, 72.833336, 43, 80.833336, 43, 89.7, 43, 102.13333, 43, 110.13333, 43, 119, 43, 135, 43, 149.2, 43, 157.2, 43, 173.2, 43, 187.4, 43, 196.26666, 43]
+ size: 24
+ color: [0,0,0,0]
+ -
+ type: "pop-all-shadows"
+
diff --git a/gfx/wr/wrench/reftests/text/shadow-grey.yaml b/gfx/wr/wrench/reftests/text/shadow-grey.yaml
new file mode 100644
index 0000000000..a728518ce3
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-grey.yaml
@@ -0,0 +1,19 @@
+---
+root:
+ items:
+ - rect: [14, 10, 205, 38]
+ color: [100, 100, 100, 1.0]
+ -
+ type: "shadow"
+ blur-radius: 2
+ color: [100, 100, 100, 1.0]
+ bounds: [14, 10, 205, 38]
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [16, 43, 35.533333, 43, 51.533333, 43, 60.4, 43, 72.833336, 43, 80.833336, 43, 89.7, 43, 102.13333, 43, 110.13333, 43, 119, 43, 135, 43, 149.2, 43, 157.2, 43, 173.2, 43, 187.4, 43, 196.26666, 43]
+ size: 24
+ color: [0,0,0,0]
+ -
+ type: "pop-all-shadows"
+
diff --git a/gfx/wr/wrench/reftests/text/shadow-huge-ref.yaml b/gfx/wr/wrench/reftests/text/shadow-huge-ref.yaml
new file mode 100644
index 0000000000..c0f8332a9e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-huge-ref.yaml
@@ -0,0 +1,18 @@
+---
+root:
+ items:
+ -
+ type: "shadow"
+ bounds: [14, 18, 205, 35]
+ blur-radius: 600
+ offset: [10, 10]
+ color: black
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [16, 43, 35.533333, 43, 51.533333, 43, 60.4, 43, 72.833336, 43, 80.833336, 43, 89.7, 43, 102.13333, 43, 110.13333, 43, 119, 43, 135, 43, 149.2, 43, 157.2, 43, 173.2, 43, 187.4, 43, 196.26666, 43]
+ size: 18
+ color: [0, 0, 0, 1]
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/shadow-huge.yaml b/gfx/wr/wrench/reftests/text/shadow-huge.yaml
new file mode 100644
index 0000000000..1814dcf1df
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-huge.yaml
@@ -0,0 +1,18 @@
+---
+root:
+ items:
+ -
+ type: "shadow"
+ bounds: [14, 18, 205, 35]
+ blur-radius: 100000000
+ offset: [10, 10]
+ color: black
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [16, 43, 35.533333, 43, 51.533333, 43, 60.4, 43, 72.833336, 43, 80.833336, 43, 89.7, 43, 102.13333, 43, 110.13333, 43, 119, 43, 135, 43, 149.2, 43, 157.2, 43, 173.2, 43, 187.4, 43, 196.26666, 43]
+ size: 18
+ color: [0, 0, 0, 1]
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/shadow-image.yaml b/gfx/wr/wrench/reftests/text/shadow-image.yaml
new file mode 100644
index 0000000000..4d472aadc4
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-image.yaml
@@ -0,0 +1,14 @@
+---
+root:
+ items:
+ -
+ type: "shadow"
+ blur-radius: 25
+ bounds: [0, 0, 90, 90]
+ offset: [0, 0]
+ color: [0, 0, 0, 0.8]
+ -
+ bounds: [ 30, 30, 30, 30 ]
+ image: solid-color(0, 0, 255, 255, 30, 30)
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/shadow-many.yaml b/gfx/wr/wrench/reftests/text/shadow-many.yaml
new file mode 100644
index 0000000000..b5daabe669
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-many.yaml
@@ -0,0 +1,30 @@
+--- # the same as shadow.yaml, except there are many shadows with different offsets and colors
+root:
+ items:
+ -
+ type: "shadow"
+ bounds: [14, 18, 205, 35]
+ blur-radius: 5
+ offset: [0, 0]
+ color: black
+ -
+ type: "shadow"
+ bounds: [14, 18, 205, 35]
+ blur-radius: 0
+ offset: [2, 3]
+ color: red
+ -
+ type: "shadow"
+ bounds: [14, 18, 205, 35]
+ blur-radius: 3
+ offset: [-2, 3.5]
+ color: blue
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [16, 43, 35.533333, 43, 51.533333, 43, 60.4, 43, 72.833336, 43, 80.833336, 43, 89.7, 43, 102.13333, 43, 110.13333, 43, 119, 43, 135, 43, 149.2, 43, 157.2, 43, 173.2, 43, 187.4, 43, 196.26666, 43]
+ size: 18
+ color: [0, 0, 0, 0] # actual text is transparent
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/shadow-ordering-ref.yaml b/gfx/wr/wrench/reftests/text/shadow-ordering-ref.yaml
new file mode 100644
index 0000000000..cc0b378cb0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-ordering-ref.yaml
@@ -0,0 +1,131 @@
+--- # checks that decorations on "real" content and "shadow" content are on seperate, atomic, layers
+root:
+ items:
+ -
+ type: "shadow"
+ bounds: [44, 42, 205, 35]
+ blur-radius: 1.0
+ offset: [30, 24]
+ color: blue
+ -
+ type: line
+ baseline: 45
+ start: 14
+ end: 210
+ width: 3
+ orientation: horizontal
+ color: [0,0,0,0] # transparent
+ style: solid
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [16, 43, 35.533333, 43, 51.533333, 43, 60.4, 43, 72.833336, 43, 80.833336, 43, 89.7, 43, 102.13333, 43, 110.13333, 43, 119, 43, 135, 43, 149.2, 43, 157.2, 43, 173.2, 43, 187.4, 43, 196.26666, 43]
+ size: 18
+ color: [0,0,0,0] # transparent
+ font: "VeraBd.ttf"
+ -
+ type: line
+ baseline: 32
+ start: 14
+ end: 210
+ width: 3
+ orientation: horizontal
+ color: [0,0,0,0] # transparent
+ style: solid
+ -
+ type: "pop-all-shadows"
+
+ -
+ type: "shadow"
+ bounds: [34, 34, 205, 35]
+ blur-radius: 0.0 # no blur to tigger fast shadows
+ offset: [20, 16]
+ color: green
+ -
+ type: line
+ baseline: 45
+ start: 14
+ end: 210
+ width: 3
+ orientation: horizontal
+ color: [0,0,0,0] # transparent
+ style: solid
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [16, 43, 35.533333, 43, 51.533333, 43, 60.4, 43, 72.833336, 43, 80.833336, 43, 89.7, 43, 102.13333, 43, 110.13333, 43, 119, 43, 135, 43, 149.2, 43, 157.2, 43, 173.2, 43, 187.4, 43, 196.26666, 43]
+ size: 18
+ color: [0,0,0,0] # transparent
+ font: "VeraBd.ttf"
+ -
+ type: line
+ baseline: 32
+ start: 14
+ end: 210
+ width: 3
+ orientation: horizontal
+ color: [0,0,0,0] # transparent
+ style: solid
+ -
+ type: "pop-all-shadows"
+
+ -
+ type: "shadow"
+ bounds: [24, 26, 205, 35]
+ blur-radius: 1.0
+ offset: [10, 8]
+ color: black
+ -
+ type: line
+ baseline: 45
+ start: 14
+ end: 210
+ width: 3
+ orientation: horizontal
+ color: [0,0,0,0] # transparent
+ style: solid
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [16, 43, 35.533333, 43, 51.533333, 43, 60.4, 43, 72.833336, 43, 80.833336, 43, 89.7, 43, 102.13333, 43, 110.13333, 43, 119, 43, 135, 43, 149.2, 43, 157.2, 43, 173.2, 43, 187.4, 43, 196.26666, 43]
+ size: 18
+ color: [0,0,0,0] # transparent
+ font: "VeraBd.ttf"
+ -
+ type: line
+ baseline: 32
+ start: 14
+ end: 210
+ width: 3
+ orientation: horizontal
+ color: [0,0,0,0] # transparent
+ style: solid
+ -
+ type: "pop-all-shadows"
+
+ # real text
+ -
+ type: line
+ baseline: 45
+ start: 14
+ end: 210
+ width: 3
+ orientation: horizontal
+ color: red
+ style: solid
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [16, 43, 35.533333, 43, 51.533333, 43, 60.4, 43, 72.833336, 43, 80.833336, 43, 89.7, 43, 102.13333, 43, 110.13333, 43, 119, 43, 135, 43, 149.2, 43, 157.2, 43, 173.2, 43, 187.4, 43, 196.26666, 43]
+ size: 18
+ color: red
+ font: "VeraBd.ttf"
+ -
+ type: line
+ baseline: 32
+ start: 14
+ end: 210
+ width: 3
+ orientation: horizontal
+ color: red
+ style: solid
diff --git a/gfx/wr/wrench/reftests/text/shadow-ordering.yaml b/gfx/wr/wrench/reftests/text/shadow-ordering.yaml
new file mode 100644
index 0000000000..d1969ef09b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-ordering.yaml
@@ -0,0 +1,48 @@
+--- # checks that decorations on "real" content and "shadow" content are on seperate, atomic, layers
+root:
+ items:
+ -
+ type: "shadow"
+ bounds: [44, 42, 205, 35]
+ blur-radius: 1.0
+ offset: [30, 24]
+ color: blue
+ -
+ type: "shadow"
+ bounds: [34, 34, 205, 35]
+ blur-radius: 0.0 # no blur to tigger fast shadows
+ offset: [20, 16]
+ color: green
+ -
+ type: "shadow"
+ bounds: [24, 26, 205, 35]
+ blur-radius: 1.0
+ offset: [10, 8]
+ color: black
+ -
+ type: line
+ baseline: 45
+ start: 14
+ end: 210
+ width: 3
+ orientation: horizontal
+ color: red
+ style: solid
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [16, 43, 35.533333, 43, 51.533333, 43, 60.4, 43, 72.833336, 43, 80.833336, 43, 89.7, 43, 102.13333, 43, 110.13333, 43, 119, 43, 135, 43, 149.2, 43, 157.2, 43, 173.2, 43, 187.4, 43, 196.26666, 43]
+ size: 18
+ color: red
+ font: "VeraBd.ttf"
+ -
+ type: line
+ baseline: 32
+ start: 14
+ end: 210
+ width: 3
+ orientation: horizontal
+ color: red
+ style: solid
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/shadow-partial-glyph-ref.yaml b/gfx/wr/wrench/reftests/text/shadow-partial-glyph-ref.yaml
new file mode 100644
index 0000000000..e49ec68e97
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-partial-glyph-ref.yaml
@@ -0,0 +1,50 @@
+--- # taking the shadow of multiple copies of a glyph with different clips should look the same as the unclipped glyph
+root:
+ items:
+ -
+ type: "shadow"
+ blur-radius: 2
+ bounds: [14, 18, 205, 35]
+ offset: [40, 0]
+ color: black
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [58]
+ offsets: [16, 43]
+ size: 18
+ color: [0, 0, 0, 0]
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
+ -
+ bounds: [14, 18, 205, 35]
+ clip-rect: [16, 18, 14, 14]
+ glyphs: [58]
+ offsets: [16, 43]
+ size: 18
+ color: [255, 0, 0, 1]
+ font: "VeraBd.ttf"
+ -
+ bounds: [14, 18, 205, 35]
+ clip-rect: [30, 18, 14, 14]
+ glyphs: [58]
+ offsets: [16, 43]
+ size: 18
+ color: [0, 255, 0, 1]
+ font: "VeraBd.ttf"
+ -
+ bounds: [14, 18, 205, 35]
+ clip-rect: [16, 32, 14, 14]
+ glyphs: [58]
+ offsets: [16, 43]
+ size: 18
+ color: [0, 0, 255, 1]
+ font: "VeraBd.ttf"
+ -
+ bounds: [14, 18, 205, 35]
+ clip-rect: [30, 32, 14, 14]
+ glyphs: [58]
+ offsets: [16, 43]
+ size: 18
+ color: [255, 0, 255, 1]
+ font: "VeraBd.ttf" \ No newline at end of file
diff --git a/gfx/wr/wrench/reftests/text/shadow-partial-glyph.yaml b/gfx/wr/wrench/reftests/text/shadow-partial-glyph.yaml
new file mode 100644
index 0000000000..ccf455d0f6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-partial-glyph.yaml
@@ -0,0 +1,43 @@
+--- # taking the shadow of multiple copies of a glyph with different clips should look the same as the unclipped glyph
+root:
+ items:
+ -
+ type: "shadow"
+ blur-radius: 2
+ bounds: [14, 18, 205, 35]
+ offset: [40, 0]
+ color: black
+ -
+ bounds: [14, 18, 205, 35]
+ clip-rect: [16, 18, 14, 14]
+ glyphs: [58]
+ offsets: [16, 43]
+ size: 18
+ color: [255, 0, 0, 1]
+ font: "VeraBd.ttf"
+ -
+ bounds: [14, 18, 205, 35]
+ clip-rect: [30, 18, 14, 14]
+ glyphs: [58]
+ offsets: [16, 43]
+ size: 18
+ color: [0, 255, 0, 1]
+ font: "VeraBd.ttf"
+ -
+ bounds: [14, 18, 205, 35]
+ clip-rect: [16, 32, 14, 14]
+ glyphs: [58]
+ offsets: [16, 43]
+ size: 18
+ color: [0, 0, 255, 1]
+ font: "VeraBd.ttf"
+ -
+ bounds: [14, 18, 205, 35]
+ clip-rect: [30, 32, 14, 14]
+ glyphs: [58]
+ offsets: [16, 43]
+ size: 18
+ color: [255, 0, 255, 1]
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows" \ No newline at end of file
diff --git a/gfx/wr/wrench/reftests/text/shadow-red-ref.yaml b/gfx/wr/wrench/reftests/text/shadow-red-ref.yaml
new file mode 100644
index 0000000000..6c2f50fba0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-red-ref.yaml
@@ -0,0 +1,5 @@
+---
+root:
+ items:
+ - rect: [14, 10, 205, 38]
+ color: red
diff --git a/gfx/wr/wrench/reftests/text/shadow-red.yaml b/gfx/wr/wrench/reftests/text/shadow-red.yaml
new file mode 100644
index 0000000000..62df9fc39c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-red.yaml
@@ -0,0 +1,19 @@
+---
+root:
+ items:
+ - rect: [14, 10, 205, 38]
+ color: red
+ -
+ type: "shadow"
+ blur-radius: 1
+ color: red
+ bounds: [14, 10, 205, 38]
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [16, 43, 35.533333, 43, 51.533333, 43, 60.4, 43, 72.833336, 43, 80.833336, 43, 89.7, 43, 102.13333, 43, 110.13333, 43, 119, 43, 135, 43, 149.2, 43, 157.2, 43, 173.2, 43, 187.4, 43, 196.26666, 43]
+ size: 24
+ color: [0,0,0,0]
+ -
+ type: "pop-all-shadows"
+
diff --git a/gfx/wr/wrench/reftests/text/shadow-ref.yaml b/gfx/wr/wrench/reftests/text/shadow-ref.yaml
new file mode 100644
index 0000000000..efc8d2d9f0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-ref.yaml
@@ -0,0 +1,25 @@
+---
+root:
+ items:
+ - # Compare non-blurred offset shadow to text with all glyphs offset that much
+ bounds: [16, 21, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [18, 46, 37.533333, 46, 53.533333, 46, 62.4, 46, 74.833336, 46, 82.833336, 46, 91.7, 46, 104.13333, 46, 112.13333, 46, 121, 46, 137, 46, 151.2, 46, 159.2, 46, 175.2, 46, 189.4, 46, 198.26666, 46]
+ size: 18
+ color: black
+ font: "VeraBd.ttf"
+ - # Compare blurred offset shadow to shadow
+ type: "shadow"
+ bounds: [12, 314, 205, 35]
+ blur-radius: 5
+ offset: [0, 0]
+ color: red
+ -
+ bounds: [14, 318, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [14, 339, 33.533333, 339, 49.533333, 339, 58.4, 339, 70.833336, 339, 78.833336, 339, 87.7, 339, 100.13333, 339, 108.13333, 339, 117, 339, 133, 339, 147.2, 339, 155.2, 339, 171.2, 339, 185.4, 339, 194.26666, 339]
+ size: 18
+ color: [0, 0, 0, 0] # actual text is transparent
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/shadow-rotate.yaml b/gfx/wr/wrench/reftests/text/shadow-rotate.yaml
new file mode 100644
index 0000000000..e0ffc9ac03
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-rotate.yaml
@@ -0,0 +1,18 @@
+--- #checks that transformed text shadows can locate glyphs in the glyph cache
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 430, 330]
+ transform: rotate(30)
+ items:
+ - type: "shadow"
+ bounds: [0, 0, 430, 330]
+ blur-radius: 1
+ offset: [0, 1]
+ color: blue
+ - text: "a Bcd Efgh Ijklm Nopqrs Tuvwxyz"
+ origin: 50 200
+ size: 20
+ font: "FreeSans.ttf"
+ - type: "pop-all-shadows"
+
diff --git a/gfx/wr/wrench/reftests/text/shadow-single.yaml b/gfx/wr/wrench/reftests/text/shadow-single.yaml
new file mode 100644
index 0000000000..1804a073ff
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-single.yaml
@@ -0,0 +1,18 @@
+---
+root:
+ items:
+ -
+ type: "shadow"
+ blur-radius: 5
+ bounds: [14, 18, 205, 35]
+ offset: [0, 0]
+ color: black
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55]
+ offsets: [16, 43]
+ size: 18
+ color: [0, 0, 0, 0] # actual text is transparent
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/shadow-solid-ref.yaml b/gfx/wr/wrench/reftests/text/shadow-solid-ref.yaml
new file mode 100644
index 0000000000..264b1e1096
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-solid-ref.yaml
@@ -0,0 +1,16 @@
+---
+root:
+ items:
+ -
+ type: "shadow"
+ blur-radius: 25
+ bounds: [0, 0, 90, 90]
+ offset: [0, 0]
+ color: [0, 0, 0, 0.8]
+ -
+ bounds: [30, 30, 30, 30]
+ type: rect
+ style: solid
+ color: blue
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/shadow-transforms.png b/gfx/wr/wrench/reftests/text/shadow-transforms.png
new file mode 100644
index 0000000000..61e50a5547
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-transforms.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/shadow-transforms.yaml b/gfx/wr/wrench/reftests/text/shadow-transforms.yaml
new file mode 100644
index 0000000000..7a75133dab
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow-transforms.yaml
@@ -0,0 +1,58 @@
+# Various tests for rotated text shadows, such as
+# blur radius, offset, shadow color.
+root:
+ items:
+ - type: stacking-context
+ transform: rotate(-30)
+ transform-origin: 80 80
+ items:
+ -
+ type: "shadow"
+ bounds: [0, 0, 350, 100]
+ blur-radius: 5
+ color: [255, 0, 0, 1]
+ - text: "A red shadow"
+ origin: 50 40
+ size: 20
+ font: "FreeSans.ttf"
+ -
+ type: "pop-all-shadows"
+
+ -
+ type: "shadow"
+ bounds: [100, 100, 350, 100]
+ blur-radius: 2
+ offset: 10 10
+ color: [0, 255, 0, 0.5]
+ - text: "Red text, green shadow"
+ origin: 150 140
+ size: 20
+ font: "FreeSans.ttf"
+ color: red
+ -
+ type: "pop-all-shadows"
+
+ - type: stacking-context
+ perspective: 100
+ perspective-origin: 650 100
+ items:
+ - type: "stacking-context"
+ transform-origin: 235 235
+ transform: rotate-x(-15)
+ items:
+ -
+ type: "shadow"
+ blur-radius: 5
+ color: [255, 0, 0, 1]
+ offset: [0, 20]
+ -
+ type: "shadow"
+ blur-radius: 0
+ color: [0, 0, 255, 1]
+ offset: [0, -20]
+ - text: "PERSPECTIVE!!!"
+ origin: 500 100
+ size: 100
+ font: "FreeSans.ttf"
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/shadow.yaml b/gfx/wr/wrench/reftests/text/shadow.yaml
new file mode 100644
index 0000000000..56ffdde93b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/shadow.yaml
@@ -0,0 +1,33 @@
+---
+root:
+ items:
+ -
+ type: "shadow"
+ bounds: [14, 18, 205, 35]
+ blur-radius: 0
+ offset: [2, 3]
+ color: black
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [16, 43, 35.533333, 43, 51.533333, 43, 60.4, 43, 72.833336, 43, 80.833336, 43, 89.7, 43, 102.13333, 43, 110.13333, 43, 119, 43, 135, 43, 149.2, 43, 157.2, 43, 173.2, 43, 187.4, 43, 196.26666, 43]
+ size: 18
+ color: [0, 0, 0, 0] # actual text is transparent
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
+ -
+ type: "shadow"
+ bounds: [12, 314, 205, 35]
+ blur-radius: 5
+ offset: [-2, -4]
+ color: red
+ -
+ bounds: [14, 318, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [16, 343, 35.533333, 343, 51.533333, 343, 60.4, 343, 72.833336, 343, 80.833336, 343, 89.7, 343, 102.13333, 343, 110.13333, 343, 119, 343, 135, 343, 149.2, 343, 157.2, 343, 173.2, 343, 187.4, 343, 196.26666, 343]
+ size: 18
+ color: [0, 0, 0, 0] # actual text is transparent
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/snap-clip-ref.yaml b/gfx/wr/wrench/reftests/text/snap-clip-ref.yaml
new file mode 100644
index 0000000000..676b7c80c5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/snap-clip-ref.yaml
@@ -0,0 +1,7 @@
+root:
+ items:
+ - bounds: [0, 0, 35, 35]
+ glyphs: [50]
+ offsets: [10, 30]
+ size: 20
+ font: "FreeSans.ttf"
diff --git a/gfx/wr/wrench/reftests/text/snap-clip.yaml b/gfx/wr/wrench/reftests/text/snap-clip.yaml
new file mode 100644
index 0000000000..6ee30aa09b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/snap-clip.yaml
@@ -0,0 +1,8 @@
+root:
+ items:
+ - bounds: [0, 0, 35, 35]
+ glyphs: [50]
+ offsets: [10.3, 30]
+ clip-rect: [0, 0, 29.7, 35]
+ size: 20
+ font: "FreeSans.ttf"
diff --git a/gfx/wr/wrench/reftests/text/snap-text-offset-ref.yaml b/gfx/wr/wrench/reftests/text/snap-text-offset-ref.yaml
new file mode 100644
index 0000000000..51b6a8c7c9
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/snap-text-offset-ref.yaml
@@ -0,0 +1,11 @@
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 200, 100]
+ transform: translate(0.5, 0.5)
+ items:
+ - bounds: [0, 0, 200, 100]
+ glyphs: [68, 3, 37, 70, 71, 3, 40, 73, 74, 75]
+ offsets: [20.3, 50, 35, 50, 43, 50.49, 61.2, 50, 75.4, 50, 90.6, 50, 98.8, 50, 116.7, 50, 124.1, 50, 139.5, 50]
+ size: 20
+ font: "FreeSans.ttf"
diff --git a/gfx/wr/wrench/reftests/text/snap-text-offset.yaml b/gfx/wr/wrench/reftests/text/snap-text-offset.yaml
new file mode 100644
index 0000000000..df8c3680d8
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/snap-text-offset.yaml
@@ -0,0 +1,10 @@
+root:
+ items:
+ - type: stacking-context
+ bounds: [0.5, 0.5, 200, 100]
+ items:
+ - bounds: [0, 0, 200, 100]
+ glyphs: [68, 3, 37, 70, 71, 3, 40, 73, 74, 75]
+ offsets: [20.3, 50, 35, 50, 43, 50.49, 61.2, 50, 75.4, 50, 90.6, 50, 98.8, 50, 116.7, 50, 124.1, 50, 139.5, 50]
+ size: 20
+ font: "FreeSans.ttf"
diff --git a/gfx/wr/wrench/reftests/text/split-batch-ref.yaml b/gfx/wr/wrench/reftests/text/split-batch-ref.yaml
new file mode 100644
index 0000000000..687b051607
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/split-batch-ref.yaml
@@ -0,0 +1,18 @@
+---
+root:
+ items:
+ -
+ type: "shadow"
+ bounds: [0, 0, 650, 670]
+ blur-radius: 2
+ offset: [10, 10]
+ color: black
+ -
+ bounds: [0, 0, 650, 670]
+ glyphs: [55, 45]
+ offsets: [20, 500, 400, 500]
+ size: 500
+ color: [255, 0, 0, 1]
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/split-batch.yaml b/gfx/wr/wrench/reftests/text/split-batch.yaml
new file mode 100644
index 0000000000..687b051607
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/split-batch.yaml
@@ -0,0 +1,18 @@
+---
+root:
+ items:
+ -
+ type: "shadow"
+ bounds: [0, 0, 650, 670]
+ blur-radius: 2
+ offset: [10, 10]
+ color: black
+ -
+ bounds: [0, 0, 650, 670]
+ glyphs: [55, 45]
+ offsets: [20, 500, 400, 500]
+ size: 500
+ color: [255, 0, 0, 1]
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/subpixel-rotate.png b/gfx/wr/wrench/reftests/text/subpixel-rotate.png
new file mode 100644
index 0000000000..1eaf89db1f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/subpixel-rotate.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/subpixel-rotate.yaml b/gfx/wr/wrench/reftests/text/subpixel-rotate.yaml
new file mode 100644
index 0000000000..296afb0d28
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/subpixel-rotate.yaml
@@ -0,0 +1,10 @@
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 430, 330]
+ transform: rotate(-30)
+ items:
+ - text: "a Bcd Efgh Ijklm Nopqrs Tuvwxyz"
+ origin: 50 200
+ size: 20
+ font: "FreeSans.ttf"
diff --git a/gfx/wr/wrench/reftests/text/subpixel-scale.png b/gfx/wr/wrench/reftests/text/subpixel-scale.png
new file mode 100644
index 0000000000..5999d2d5dd
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/subpixel-scale.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/subpixel-scale.yaml b/gfx/wr/wrench/reftests/text/subpixel-scale.yaml
new file mode 100644
index 0000000000..84e7b72bfd
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/subpixel-scale.yaml
@@ -0,0 +1,10 @@
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 680, 80]
+ transform: scale-x(1.5)
+ items:
+ - text: "a Bcd Efgh Ijklm Nopqrs Tuvwxyz"
+ origin: 20 50
+ size: 20
+ font: "FreeSans.ttf"
diff --git a/gfx/wr/wrench/reftests/text/subpixel-skew.png b/gfx/wr/wrench/reftests/text/subpixel-skew.png
new file mode 100644
index 0000000000..9dcfde0088
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/subpixel-skew.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/subpixel-skew.yaml b/gfx/wr/wrench/reftests/text/subpixel-skew.yaml
new file mode 100644
index 0000000000..7d06f722b2
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/subpixel-skew.yaml
@@ -0,0 +1,10 @@
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 480, 80]
+ transform: skew-x(30)
+ items:
+ - text: "a Bcd Efgh Ijklm Nopqrs Tuvwxyz"
+ origin: 20 50
+ size: 20
+ font: "FreeSans.ttf"
diff --git a/gfx/wr/wrench/reftests/text/subpixel-translate-ref.yaml b/gfx/wr/wrench/reftests/text/subpixel-translate-ref.yaml
new file mode 100644
index 0000000000..a4377ee76d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/subpixel-translate-ref.yaml
@@ -0,0 +1,12 @@
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ items:
+ -
+ bounds: [0, 0, 500, 500]
+ glyphs: [80, 80, 80, 80, 80]
+ offsets: [50, 100, 71.111, 100, 92.222, 100, 113.333, 100, 134.444, 100]
+ origin: 0 0
+ size: 20
+ font: "FreeSans.ttf"
diff --git a/gfx/wr/wrench/reftests/text/subpixel-translate.yaml b/gfx/wr/wrench/reftests/text/subpixel-translate.yaml
new file mode 100644
index 0000000000..940dc6fef7
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/subpixel-translate.yaml
@@ -0,0 +1,13 @@
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 500, 500]
+ transform: translate(0.444, 0)
+ items:
+ -
+ bounds: [0, 0, 500, 500]
+ glyphs: [80, 80, 80, 80, 80]
+ offsets: [50, 100, 71.111, 100, 92.222, 100, 113.333, 100, 134.444, 100]
+ origin: 0 0
+ size: 20
+ font: "FreeSans.ttf"
diff --git a/gfx/wr/wrench/reftests/text/subpx-bg-mask-ref.yaml b/gfx/wr/wrench/reftests/text/subpx-bg-mask-ref.yaml
new file mode 100644
index 0000000000..782907b5c3
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/subpx-bg-mask-ref.yaml
@@ -0,0 +1,8 @@
+---
+root:
+ items:
+ - text: "A"
+ origin: 20 190
+ size: 180
+ color: black
+ font: "FreeSans.ttf"
diff --git a/gfx/wr/wrench/reftests/text/subpx-bg-mask.yaml b/gfx/wr/wrench/reftests/text/subpx-bg-mask.yaml
new file mode 100644
index 0000000000..4d375d3884
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/subpx-bg-mask.yaml
@@ -0,0 +1,28 @@
+# Verify that text on a picture cache slice where the background
+# has a rounded-rect clip correct enables subpixel AA
+---
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 100, 100]
+ color: white
+ - type: scroll-frame
+ bounds: [0, 0, 200, 200]
+ content-size: [200, 200]
+ scroll-offset: [0, 0]
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [0, 0, 200, 200]
+ radius: 4
+ - type: rect
+ bounds: [0, 0, 200, 200]
+ color: white
+ clip-chain: [2]
+ - text: "A"
+ origin: 20 190
+ size: 180
+ color: black
+ font: "FreeSans.ttf"
+ clip-chain: [2]
diff --git a/gfx/wr/wrench/reftests/text/subtle-shadow-ref.yaml b/gfx/wr/wrench/reftests/text/subtle-shadow-ref.yaml
new file mode 100644
index 0000000000..54374e1191
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/subtle-shadow-ref.yaml
@@ -0,0 +1,27 @@
+--- # incredibly faint shadows showing up as opaque black??
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 255, 85]
+ color: [245, 100, 100, 1.0]
+ -
+ type: "shadow"
+ bounds: [14, 18, 205, 35]
+ blur-radius: 0
+ offset: [0, 1]
+ color: [0.0,0.0,0.0, 0.101961]
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [16, 43, 35.533333, 43, 51.533333, 43, 60.4, 43, 72.833336, 43, 80.833336, 43, 89.7, 43, 102.13333, 43, 110.13333, 43, 119, 43, 135, 43, 149.2, 43, 157.2, 43, 173.2, 43, 187.4, 43, 196.26666, 43]
+ size: 12
+ color: [168, 168, 168, 0.0]
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
+ - bounds: [14, 18, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [16, 43, 35.533333, 43, 51.533333, 43, 60.4, 43, 72.833336, 43, 80.833336, 43, 89.7, 43, 102.13333, 43, 110.13333, 43, 119, 43, 135, 43, 149.2, 43, 157.2, 43, 173.2, 43, 187.4, 43, 196.26666, 43]
+ size: 12
+ color: [168, 168, 168, 1.0]
+ font: "VeraBd.ttf"
diff --git a/gfx/wr/wrench/reftests/text/subtle-shadow.yaml b/gfx/wr/wrench/reftests/text/subtle-shadow.yaml
new file mode 100644
index 0000000000..67d19e81fd
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/subtle-shadow.yaml
@@ -0,0 +1,21 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 255, 85]
+ color: [245, 100, 100, 1.0]
+ -
+ type: "shadow"
+ bounds: [14, 18, 205, 35]
+ blur-radius: 0
+ offset: [0, 1]
+ color: [0.0,0.0,0.0, 0.101961]
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [16, 43, 35.533333, 43, 51.533333, 43, 60.4, 43, 72.833336, 43, 80.833336, 43, 89.7, 43, 102.13333, 43, 110.13333, 43, 119, 43, 135, 43, 149.2, 43, 157.2, 43, 173.2, 43, 187.4, 43, 196.26666, 43]
+ size: 12
+ color: [168, 168, 168, 1.0]
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/synthetic-bold-not-ref.yaml b/gfx/wr/wrench/reftests/text/synthetic-bold-not-ref.yaml
new file mode 100644
index 0000000000..b947bf4597
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/synthetic-bold-not-ref.yaml
@@ -0,0 +1,5 @@
+root:
+ items:
+ - text: "Fake bold is great"
+ origin: 20 40
+ size: 20
diff --git a/gfx/wr/wrench/reftests/text/synthetic-bold-transparent-ref.yaml b/gfx/wr/wrench/reftests/text/synthetic-bold-transparent-ref.yaml
new file mode 100644
index 0000000000..c7db0a3426
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/synthetic-bold-transparent-ref.yaml
@@ -0,0 +1,10 @@
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 660, 210]
+ filters: opacity(0.5)
+ items:
+ - text: "Fake bold is great"
+ origin: 20 40
+ size: 20
+ synthetic-bold: true
diff --git a/gfx/wr/wrench/reftests/text/synthetic-bold-transparent.yaml b/gfx/wr/wrench/reftests/text/synthetic-bold-transparent.yaml
new file mode 100644
index 0000000000..043a6da781
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/synthetic-bold-transparent.yaml
@@ -0,0 +1,7 @@
+root:
+ items:
+ - text: "Fake bold is great"
+ origin: 20 40
+ size: 20
+ color: [0, 0, 0, 0.5]
+ synthetic-bold: true
diff --git a/gfx/wr/wrench/reftests/text/synthetic-bold.yaml b/gfx/wr/wrench/reftests/text/synthetic-bold.yaml
new file mode 100644
index 0000000000..8181dbb84e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/synthetic-bold.yaml
@@ -0,0 +1,6 @@
+root:
+ items:
+ - text: "Fake bold is great"
+ origin: 20 40
+ size: 20
+ synthetic-bold: true
diff --git a/gfx/wr/wrench/reftests/text/synthetic-italics-custom.yaml b/gfx/wr/wrench/reftests/text/synthetic-italics-custom.yaml
new file mode 100644
index 0000000000..00f30a776f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/synthetic-italics-custom.yaml
@@ -0,0 +1,7 @@
+root:
+ items:
+ - text: "Fake italics are great"
+ origin: 20 40
+ size: 20
+ synthetic-italics: 45
+
diff --git a/gfx/wr/wrench/reftests/text/synthetic-italics-ref.yaml b/gfx/wr/wrench/reftests/text/synthetic-italics-ref.yaml
new file mode 100644
index 0000000000..5c5be707f7
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/synthetic-italics-ref.yaml
@@ -0,0 +1,5 @@
+root:
+ items:
+ - text: "Fake italics are great"
+ origin: 20 40
+ size: 20
diff --git a/gfx/wr/wrench/reftests/text/synthetic-italics.yaml b/gfx/wr/wrench/reftests/text/synthetic-italics.yaml
new file mode 100644
index 0000000000..fa9b78a3f0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/synthetic-italics.yaml
@@ -0,0 +1,7 @@
+root:
+ items:
+ - text: "Fake italics are great"
+ origin: 20 40
+ size: 20
+ synthetic-italics: true
+
diff --git a/gfx/wr/wrench/reftests/text/text-fixed-slice-fast.png b/gfx/wr/wrench/reftests/text/text-fixed-slice-fast.png
new file mode 100644
index 0000000000..e6d6147219
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/text-fixed-slice-fast.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/text-fixed-slice-slow.png b/gfx/wr/wrench/reftests/text/text-fixed-slice-slow.png
new file mode 100644
index 0000000000..4ccfff39b2
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/text-fixed-slice-slow.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/text-fixed-slice.yaml b/gfx/wr/wrench/reftests/text/text-fixed-slice.yaml
new file mode 100644
index 0000000000..9ee4dfcc56
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/text-fixed-slice.yaml
@@ -0,0 +1,27 @@
+# Verify that the option to configure performance / quality settings for
+# subpixel AA with picture caching is respected.
+root:
+ items:
+ - type: scroll-frame
+ bounds: [0, 0, 500, 200]
+ content-size: [500, 500]
+ clip-to-frame: true
+ id: 2
+ items:
+ - type: rect
+ bounds: [0, 0, 500, 200]
+ color: white
+ clip-chain: [2]
+ - type: clip
+ id: 3
+ bounds: [0, 0, 500, 200]
+ - type: rect
+ bounds: [0, 0, 500, 200]
+ color: white
+ clip-chain: [3]
+ - text: "The sun has frightened off the night!"
+ origin: 20 40
+ size: 20
+ font: "FreeSans.ttf"
+ spatial-id: 2
+ clip-chain: [2]
diff --git a/gfx/wr/wrench/reftests/text/text-masking-alpha.png b/gfx/wr/wrench/reftests/text/text-masking-alpha.png
new file mode 100644
index 0000000000..2b816eb288
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/text-masking-alpha.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/text-masking-mask.png b/gfx/wr/wrench/reftests/text/text-masking-mask.png
new file mode 100644
index 0000000000..a27a494849
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/text-masking-mask.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/text-masking-subpx.png b/gfx/wr/wrench/reftests/text/text-masking-subpx.png
new file mode 100644
index 0000000000..791a4c6347
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/text-masking-subpx.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/text-masking.yaml b/gfx/wr/wrench/reftests/text/text-masking.yaml
new file mode 100644
index 0000000000..5e3e4642c6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/text-masking.yaml
@@ -0,0 +1,26 @@
+---
+root:
+ items:
+ - bounds: [0, 0, 750, 100]
+ "clip-rect": [0, 0, 750, 100]
+ type: rect
+ color: [180, 180, 180]
+ - type: clip
+ "clip-rect": [0, 0, 750, 100]
+ id: 2
+ "content-size": [750, 100]
+ "image-mask":
+ image: "text-masking-mask.png"
+ rect: [0, 0, 750, 100]
+ repeat: false
+ - text: "Cats making all the muffins knock over christmas tree"
+ origin: 40 40
+ size: 20
+ clip-chain: [2]
+ font: "FreeSans.ttf"
+ - text: "Cats making all the muffins knock over christmas tree"
+ origin: 40 80
+ size: 20
+ color: white
+ clip-chain: [2]
+ font: "FreeSans.ttf"
diff --git a/gfx/wr/wrench/reftests/text/text.yaml b/gfx/wr/wrench/reftests/text/text.yaml
new file mode 100644
index 0000000000..35e51fa92f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/text.yaml
@@ -0,0 +1,10 @@
+---
+root:
+ items:
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [16, 43, 35.533333, 43, 51.533333, 43, 60.4, 43, 72.833336, 43, 80.833336, 43, 89.7, 43, 102.13333, 43, 110.13333, 43, 119, 43, 135, 43, 149.2, 43, 157.2, 43, 173.2, 43, 187.4, 43, 196.26666, 43]
+ size: 18
+ color: black
+ font: "VeraBd.ttf"
diff --git a/gfx/wr/wrench/reftests/text/transparent-no-aa-ref.yaml b/gfx/wr/wrench/reftests/text/transparent-no-aa-ref.yaml
new file mode 100644
index 0000000000..800b7a9f2a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/transparent-no-aa-ref.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ -
+ type: "shadow"
+ bounds: [0, 0, 200, 200]
+ offset: [0, 0]
+ blur-radius: 0
+ color: [0, 0, 0, 0.5]
+ -
+ text: "hello everybody"
+ origin: [10, 20]
+ size: 18
+ color: [0, 0, 0, 0]
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/transparent-no-aa.yaml b/gfx/wr/wrench/reftests/text/transparent-no-aa.yaml
new file mode 100644
index 0000000000..3a5376f7a2
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/transparent-no-aa.yaml
@@ -0,0 +1,9 @@
+---
+root:
+ items:
+ -
+ text: "hello everybody"
+ origin: [10, 20]
+ size: 18
+ color: [0, 0, 0, 0.5]
+ font: "VeraBd.ttf"
diff --git a/gfx/wr/wrench/reftests/text/two-shadows.png b/gfx/wr/wrench/reftests/text/two-shadows.png
new file mode 100644
index 0000000000..3907f1be74
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/two-shadows.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/two-shadows.yaml b/gfx/wr/wrench/reftests/text/two-shadows.yaml
new file mode 100644
index 0000000000..cd16bd1fcf
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/two-shadows.yaml
@@ -0,0 +1,24 @@
+--- # A simple example of two shadows
+root:
+ items:
+ -
+ type: "shadow"
+ bounds: [14, 18, 205, 35]
+ blur-radius: 2
+ offset: [-2, -2]
+ color: [255, 0, 0, 1]
+ -
+ type: "shadow"
+ bounds: [14, 18, 205, 35]
+ blur-radius: 2
+ offset: [2, 2]
+ color: [0, 0, 255, 1]
+ -
+ bounds: [14, 18, 205, 35]
+ glyphs: [55, 75, 76, 86, 3, 76, 86, 3, 87, 75, 72, 3, 69, 72, 86, 87]
+ offsets: [16, 43, 35.533333, 43, 51.533333, 43, 60.4, 43, 72.833336, 43, 80.833336, 43, 89.7, 43, 102.13333, 43, 110.13333, 43, 119, 43, 135, 43, 149.2, 43, 157.2, 43, 173.2, 43, 187.4, 43, 196.26666, 43]
+ size: 18
+ color: [0, 0, 0, 0]
+ font: "VeraBd.ttf"
+ -
+ type: "pop-all-shadows"
diff --git a/gfx/wr/wrench/reftests/text/white-opacity.png b/gfx/wr/wrench/reftests/text/white-opacity.png
new file mode 100644
index 0000000000..69c76b28ae
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/white-opacity.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/text/white-opacity.yaml b/gfx/wr/wrench/reftests/text/white-opacity.yaml
new file mode 100644
index 0000000000..0e16856c14
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/white-opacity.yaml
@@ -0,0 +1,17 @@
+root:
+ items:
+ - type: stacking-context
+ bounds: [10, 10, 1000, 100]
+ items:
+ - type: rect
+ bounds: [0, 0, 720, 100]
+ color: [211, 211, 211, 1]
+ - type: stacking-context
+ bounds: [0, 0, 500, 100]
+ filters: [opacity(0.4)]
+ items:
+ - text: "Look, no white edges!!"
+ origin: 20 50
+ size: 40
+ color: white
+ font: "VeraBd.ttf"
diff --git a/gfx/wr/wrench/reftests/text/writing-modes-ref.yaml b/gfx/wr/wrench/reftests/text/writing-modes-ref.yaml
new file mode 100644
index 0000000000..c392822e60
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/writing-modes-ref.yaml
@@ -0,0 +1,19 @@
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 300, 60]
+ transform: rotate(-90) translate(-120, 160)
+ items:
+ - text: "This is sideways-left"
+ origin: 0 40
+ size: 20
+ font: "FreeSans.ttf"
+ - type: stacking-context
+ bounds: [0, 0, 300, 60]
+ transform: rotate(90) translate(-90, 120)
+ items:
+ - text: "This is sideways-right"
+ origin: 0 40
+ size: 20
+ font: "FreeSans.ttf"
+
diff --git a/gfx/wr/wrench/reftests/text/writing-modes.yaml b/gfx/wr/wrench/reftests/text/writing-modes.yaml
new file mode 100644
index 0000000000..2c55e209e0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/text/writing-modes.yaml
@@ -0,0 +1,15 @@
+root:
+ items:
+ - text: "This is sideways-left"
+ origin: 20 40
+ size: 20
+ transpose: true
+ flip-x: true
+ font: "FreeSans.ttf"
+ - text: "This is sideways-right"
+ origin: 70 300
+ size: 20
+ transpose: true
+ flip-y: true
+ font: "FreeSans.ttf"
+
diff --git a/gfx/wr/wrench/reftests/tiles/complex-shared-clip-root-ref.yaml b/gfx/wr/wrench/reftests/tiles/complex-shared-clip-root-ref.yaml
new file mode 100644
index 0000000000..6a76d67f33
--- /dev/null
+++ b/gfx/wr/wrench/reftests/tiles/complex-shared-clip-root-ref.yaml
@@ -0,0 +1,13 @@
+---
+root:
+ items:
+ -
+ type: clip
+ id: 2
+ complex:
+ - rect: [50, 50, 200, 200]
+ radius: 16
+ - type: rect
+ bounds: [50, 50, 200, 200]
+ color: red
+ clip-chain: [2]
diff --git a/gfx/wr/wrench/reftests/tiles/complex-shared-clip-root.yaml b/gfx/wr/wrench/reftests/tiles/complex-shared-clip-root.yaml
new file mode 100644
index 0000000000..3d32a7f006
--- /dev/null
+++ b/gfx/wr/wrench/reftests/tiles/complex-shared-clip-root.yaml
@@ -0,0 +1,25 @@
+# verify that we don't select a shared clip root for a tile cache that has an ancestor complex clip
+---
+root:
+ items:
+ -
+ type: clip
+ id: 2
+ complex:
+ - rect: [50, 50, 200, 200]
+ radius: 16
+ -
+ type: clip
+ id: 3
+ bounds: [50, 50, 200, 200]
+
+ - type: scroll-frame
+ bounds: 50 50 200 200
+ scroll-offset: [0, 0]
+ id: 10
+ items:
+ - type: rect
+ bounds: [50, 50, 200, 200]
+ color: red
+ spatial-id: 10
+ clip-chain: [3, 2]
diff --git a/gfx/wr/wrench/reftests/tiles/mix-blend-clip-ref.yaml b/gfx/wr/wrench/reftests/tiles/mix-blend-clip-ref.yaml
new file mode 100644
index 0000000000..e31ead5b87
--- /dev/null
+++ b/gfx/wr/wrench/reftests/tiles/mix-blend-clip-ref.yaml
@@ -0,0 +1,14 @@
+---
+root:
+ items:
+ - type: stacking-context
+ blend-container: true
+ items:
+ - type: rect
+ bounds: [0, 0, 128, 128]
+ color: magenta
+ - type: stacking-context
+ mix-blend-mode: lighten
+ items:
+ - image: checkerboard(0, 16, 16, 8, 8)
+ bounds: [0, 0, 128, 128]
diff --git a/gfx/wr/wrench/reftests/tiles/mix-blend-clip.yaml b/gfx/wr/wrench/reftests/tiles/mix-blend-clip.yaml
new file mode 100644
index 0000000000..43b68f1787
--- /dev/null
+++ b/gfx/wr/wrench/reftests/tiles/mix-blend-clip.yaml
@@ -0,0 +1,31 @@
+# Test that a backdrop which has both a shared clip (on the tile cache) and is also scrolled
+# correctly calculates the available backdrop rect for a mix-blend on a child.
+---
+root:
+ items:
+ -
+ bounds: [0, 0, 128, 128]
+ type: clip
+ id: 2
+ - type: clip-chain
+ id: 12
+ clips: [2]
+ - type: stacking-context
+ blend-container: true
+ items:
+ - type: scroll-frame
+ bounds: 0 0 128 512
+ scroll-offset: [0, 128]
+ id: 3
+ items:
+ - type: rect
+ bounds: [0, 0, 128, 512]
+ color: magenta
+ spatial-id: 3
+ clip-chain: 12
+ - type: stacking-context
+ mix-blend-mode: lighten
+ clip-chain: 12
+ items:
+ - image: checkerboard(0, 16, 16, 8, 16)
+ bounds: [0, 0, 128, 256]
diff --git a/gfx/wr/wrench/reftests/tiles/prim-suite.yaml b/gfx/wr/wrench/reftests/tiles/prim-suite.yaml
new file mode 100644
index 0000000000..2a170f8fc9
--- /dev/null
+++ b/gfx/wr/wrench/reftests/tiles/prim-suite.yaml
@@ -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]
diff --git a/gfx/wr/wrench/reftests/tiles/rect.yaml b/gfx/wr/wrench/reftests/tiles/rect.yaml
new file mode 100644
index 0000000000..e3f71ac9dd
--- /dev/null
+++ b/gfx/wr/wrench/reftests/tiles/rect.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: 50 50 200 200
+ color: red
diff --git a/gfx/wr/wrench/reftests/tiles/reftest.list b/gfx/wr/wrench/reftests/tiles/reftest.list
new file mode 100644
index 0000000000..800563b87c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/tiles/reftest.list
@@ -0,0 +1,7 @@
+** rect.yaml
+fuzzy(1,2000) ** simple-gradient.yaml
+# TODO: Fix rasterizer inaccuracies so this is the same regardless of tile size!
+!* prim-suite.yaml
+== mix-blend-clip.yaml mix-blend-clip-ref.yaml
+platform(linux) == tile-cache-raster-root.yaml tile-cache-raster-root.png
+== complex-shared-clip-root.yaml complex-shared-clip-root-ref.yaml
diff --git a/gfx/wr/wrench/reftests/tiles/simple-gradient.yaml b/gfx/wr/wrench/reftests/tiles/simple-gradient.yaml
new file mode 100644
index 0000000000..0879b2442f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/tiles/simple-gradient.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
diff --git a/gfx/wr/wrench/reftests/tiles/tile-cache-raster-root.png b/gfx/wr/wrench/reftests/tiles/tile-cache-raster-root.png
new file mode 100644
index 0000000000..2615b4c9cd
--- /dev/null
+++ b/gfx/wr/wrench/reftests/tiles/tile-cache-raster-root.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/tiles/tile-cache-raster-root.yaml b/gfx/wr/wrench/reftests/tiles/tile-cache-raster-root.yaml
new file mode 100644
index 0000000000..9fc06b7bd5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/tiles/tile-cache-raster-root.yaml
@@ -0,0 +1,43 @@
+# Based on a reduced case from blend-clipped.yaml, ensure that if tile caches don't create
+# raster roots (which messes with the special case handling and positioning of them).
+---
+root:
+ items:
+ -
+ type: "stacking-context"
+ items:
+ -
+ bounds: [0, 111, 1887, 1971]
+ type: iframe
+ id: [1, 3]
+ id: [1, 1]
+pipelines:
+ -
+ id: [1, 3]
+ items:
+ -
+ type: "scroll-frame"
+ id: 2
+ bounds: [0, 0, 1887, 1971]
+ clip-to-frame: true
+ -
+ spatial-id: 2
+ type: "stacking-context"
+ items:
+ -
+ bounds: [0, -186, 1887, 239]
+ spatial-id: 2
+ type: clip
+ id: 4
+ -
+ bounds: [-660.45, -186, 0, 0]
+ spatial-id: 2
+ clip-chain: [4]
+ type: "stacking-context"
+ transform: [1, 0, 0, 0, -0.57735026, 1, 0, 0, 0, 0, 1, 0, 68.849, 0, 0, 1]
+ items:
+ -
+ bounds: [0, 0, 1887, 239]
+ type: rect
+ color: blue
+ clip-chain: [2, 4]
diff --git a/gfx/wr/wrench/reftests/transforms/big-axis-aligned-scale-ref.yaml b/gfx/wr/wrench/reftests/transforms/big-axis-aligned-scale-ref.yaml
new file mode 100644
index 0000000000..357981c103
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/big-axis-aligned-scale-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [ 0, 0, 200, 44 ]
+ color: green
diff --git a/gfx/wr/wrench/reftests/transforms/big-axis-aligned-scale.yaml b/gfx/wr/wrench/reftests/transforms/big-axis-aligned-scale.yaml
new file mode 100644
index 0000000000..10db1ae2b2
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/big-axis-aligned-scale.yaml
@@ -0,0 +1,10 @@
+---
+root:
+ items:
+ - type: stacking-context
+ transform-style: preserve-3d
+ transform: scale(-2, 44, 1)
+ items:
+ - type: rect
+ bounds: [ -100, -100, 200, 101 ]
+ color: green
diff --git a/gfx/wr/wrench/reftests/transforms/blank.yaml b/gfx/wr/wrench/reftests/transforms/blank.yaml
new file mode 100644
index 0000000000..c4eb3ab673
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/blank.yaml
@@ -0,0 +1,2 @@
+---
+root:
diff --git a/gfx/wr/wrench/reftests/transforms/border-scale-2.png b/gfx/wr/wrench/reftests/transforms/border-scale-2.png
new file mode 100644
index 0000000000..b848abfbff
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/border-scale-2.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/border-scale-2.yaml b/gfx/wr/wrench/reftests/transforms/border-scale-2.yaml
new file mode 100644
index 0000000000..f841033766
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/border-scale-2.yaml
@@ -0,0 +1,19 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [50, 50, 100, 100]
+ transform: scale(0.5)
+ items:
+ - type: border
+ bounds: [ 100, 100, 400, 200 ]
+ width: [ 20, 20, 20, 20 ]
+ border-type: normal
+ style: solid
+ color: [ blue, blue, blue, blue ]
+ radius: {
+ top-left: [100, 100],
+ top-right: [100, 100],
+ bottom-left: [100, 100],
+ bottom-right: [100, 100],
+ }
diff --git a/gfx/wr/wrench/reftests/transforms/border-scale-3.png b/gfx/wr/wrench/reftests/transforms/border-scale-3.png
new file mode 100644
index 0000000000..f51553e04f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/border-scale-3.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/border-scale-3.yaml b/gfx/wr/wrench/reftests/transforms/border-scale-3.yaml
new file mode 100644
index 0000000000..9308f2471d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/border-scale-3.yaml
@@ -0,0 +1,19 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [50, 50, 100, 100]
+ transform: [10,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]
+ items:
+ - type: border
+ bounds: [ 5, 50, 20, 100 ]
+ width: [ 10, 1, 10, 1 ]
+ border-type: normal
+ style: solid
+ color: blue
+ radius: {
+ top-left: [5, 50],
+ top-right: [5, 50],
+ bottom-left: [5, 50],
+ bottom-right: [5, 50],
+ }
diff --git a/gfx/wr/wrench/reftests/transforms/border-scale-4.png b/gfx/wr/wrench/reftests/transforms/border-scale-4.png
new file mode 100644
index 0000000000..6e1d664f9a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/border-scale-4.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/border-scale-4.yaml b/gfx/wr/wrench/reftests/transforms/border-scale-4.yaml
new file mode 100644
index 0000000000..b2e50bbd2c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/border-scale-4.yaml
@@ -0,0 +1,19 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [50, 50, 100, 100]
+ transform: [1,0,0,0, 0,10,0,0, 0,0,1,0, 0,0,0,1]
+ items:
+ - type: border
+ bounds: [ 50, 5, 200, 10 ]
+ width: [ 1, 10, 1, 10 ]
+ border-type: normal
+ style: solid
+ color: [ blue, blue, blue, blue ]
+ radius: {
+ top-left: [50, 5],
+ top-right: [50, 5],
+ bottom-left: [50, 5],
+ bottom-right: [50, 5],
+ }
diff --git a/gfx/wr/wrench/reftests/transforms/border-scale.png b/gfx/wr/wrench/reftests/transforms/border-scale.png
new file mode 100644
index 0000000000..6577268663
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/border-scale.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/border-scale.yaml b/gfx/wr/wrench/reftests/transforms/border-scale.yaml
new file mode 100644
index 0000000000..4bf10c3029
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/border-scale.yaml
@@ -0,0 +1,19 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [50, 50, 100, 100]
+ transform: scale(10)
+ items:
+ - type: border
+ bounds: [ 5, 5, 20, 10 ]
+ width: [ 1, 1, 1, 1 ]
+ border-type: normal
+ style: solid
+ color: [ blue, blue, blue, blue ]
+ radius: {
+ top-left: [5, 5],
+ top-right: [5, 5],
+ bottom-left: [5, 5],
+ bottom-right: [5, 5],
+ }
diff --git a/gfx/wr/wrench/reftests/transforms/border-zoom.png b/gfx/wr/wrench/reftests/transforms/border-zoom.png
new file mode 100644
index 0000000000..0bd7cc21ab
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/border-zoom.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/border-zoom.yaml b/gfx/wr/wrench/reftests/transforms/border-zoom.yaml
new file mode 100644
index 0000000000..d36d66962b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/border-zoom.yaml
@@ -0,0 +1,22 @@
+---
+root:
+ items:
+ - type: reference-frame
+ transform: scale(4)
+ items:
+ - type: stacking-context
+ bounds: [50, 50, 100, 100]
+ transform: rotate(-30)
+ items:
+ - type: border
+ bounds: [ 10, 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],
+ }
diff --git a/gfx/wr/wrench/reftests/transforms/clip-translate-ref.yaml b/gfx/wr/wrench/reftests/transforms/clip-translate-ref.yaml
new file mode 100644
index 0000000000..412397d90f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/clip-translate-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ color: red
+ bounds: [6, 168, 18, 18]
diff --git a/gfx/wr/wrench/reftests/transforms/clip-translate.yaml b/gfx/wr/wrench/reftests/transforms/clip-translate.yaml
new file mode 100644
index 0000000000..5d4d9a049f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/clip-translate.yaml
@@ -0,0 +1,23 @@
+---
+root:
+ items:
+ -
+ type: "stacking-context"
+ transform: [0.75, 0, 0, 0, 0, 0.75, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
+ items:
+ -
+ type: "stacking-context"
+ transform: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 8, 224, 0, 1]
+ items:
+ - type: clip
+ id: 2
+ image-mask:
+ image: solid-color(255, 0, 0, 255, 24, 24)
+ rect: [0, 0, 24, 24]
+ repeat: false
+ - type: stacking-context
+ clip-chain: [2]
+ items:
+ - type: rect
+ color: red
+ bounds: [0, 0, 24, 24]
diff --git a/gfx/wr/wrench/reftests/transforms/complex-preserve-3d.yaml b/gfx/wr/wrench/reftests/transforms/complex-preserve-3d.yaml
new file mode 100644
index 0000000000..9aa0884f62
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/complex-preserve-3d.yaml
@@ -0,0 +1,24 @@
+# the root sc should dominate backface-visibility and hide the rect
+
+---
+root:
+ items:
+ -
+ bounds: [300, 300, 300, 300]
+ clip-rect: [300, 300, 300, 300]
+ type: "stacking-context"
+ transform: rotate-y(180)
+ transform-style: flat
+ backface-visible: false
+ items:
+ -
+ type: "stacking-context"
+ transform-style: preserve-3d
+ backface-visible: true
+ items:
+ -
+ bounds: [350, 350, 150, 150]
+ clip-rect: [350, 350, 150, 150]
+ type: rect
+ color: 255 255 0 0.4000
+ backface-visible: true
diff --git a/gfx/wr/wrench/reftests/transforms/computed-rotation-yflip.png b/gfx/wr/wrench/reftests/transforms/computed-rotation-yflip.png
new file mode 100644
index 0000000000..5e1b852f49
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/computed-rotation-yflip.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/computed-rotation-yflip.yaml b/gfx/wr/wrench/reftests/transforms/computed-rotation-yflip.yaml
new file mode 100644
index 0000000000..7cbea35433
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/computed-rotation-yflip.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ - type: iframe
+ id: [1, 3]
+ bounds: [100, 100, 128, 256]
+pipelines:
+ -
+ id: [1, 3]
+ items:
+ - type: computed-frame
+ scale-from: [256, 256]
+ vertical-flip: true
+ rotation: 90
+ items:
+ - image: firefox.png
+ bounds: [0, 0, 256, 256]
diff --git a/gfx/wr/wrench/reftests/transforms/content-offset.png b/gfx/wr/wrench/reftests/transforms/content-offset.png
new file mode 100644
index 0000000000..16a39791b2
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/content-offset.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/content-offset.yaml b/gfx/wr/wrench/reftests/transforms/content-offset.yaml
new file mode 100644
index 0000000000..379b269fe6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/content-offset.yaml
@@ -0,0 +1,18 @@
+---
+root:
+ items:
+ -
+ type: "stacking-context"
+ perspective: 1000
+ perspective-origin: 0 0
+ "transform-style": "preserve-3d"
+ items:
+ -
+ type: "stacking-context"
+ transform: rotate-x(-45) translate(100, 100, 0)
+ "transform-style": "preserve-3d"
+ items:
+ -
+ bounds: [0, 0, 200, 200]
+ type: rect
+ color: red
diff --git a/gfx/wr/wrench/reftests/transforms/coord-system.png b/gfx/wr/wrench/reftests/transforms/coord-system.png
new file mode 100644
index 0000000000..49890ebccf
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/coord-system.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/coord-system.yaml b/gfx/wr/wrench/reftests/transforms/coord-system.yaml
new file mode 100644
index 0000000000..236ebb6d8f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/coord-system.yaml
@@ -0,0 +1,24 @@
+---
+root:
+ items:
+ -
+ type: "stacking-context"
+ perspective: [1, 0, 0, 0, 0, 1, 0, 0, -0.102400005, -0.185, 1, -0.0005, 0, 0, 0, 1]
+ items:
+ -
+ type: "scroll-frame"
+ id: 2
+ "content-size": [1024, 740]
+ bounds: [0, 0, 1024, 740]
+ -
+ bounds: [0, 0, 1024, 200]
+ "clip-rect": [0, 0, 1024, 200]
+ spatial-id: 2
+ type: "stacking-context"
+ transform: [0.70710677, 0, -0.70710677, 0, 0, 1, 0, 0, 0.70710677, 0, 0.70710677, 0, 149.96133, 0, -937.9613, 1]
+ items:
+ -
+ bounds: [0, 0, 1024, 200]
+ "clip-rect": [0, 0, 1024, 200]
+ type: rect
+ color: 0 128 0 1.0000
diff --git a/gfx/wr/wrench/reftests/transforms/firefox.png b/gfx/wr/wrench/reftests/transforms/firefox.png
new file mode 100644
index 0000000000..696ba9c9b5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/firefox.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/flatten-all-flat-ref.yaml b/gfx/wr/wrench/reftests/transforms/flatten-all-flat-ref.yaml
new file mode 100644
index 0000000000..fd60bccb4b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/flatten-all-flat-ref.yaml
@@ -0,0 +1,18 @@
+---
+root:
+ items:
+ - type: reference-frame
+ transform-style: flat
+ items:
+ - type: reference-frame
+ perspective: 1000
+ transform-style: flat
+ items:
+ - type: reference-frame
+ transform: rotate-z(-45) rotate-x(-75)
+ bounds: 50 0 100 100
+ transform-style: flat
+ items:
+ - type: rect
+ bounds: 0 0 100 100
+ color: green
diff --git a/gfx/wr/wrench/reftests/transforms/flatten-all-flat.yaml b/gfx/wr/wrench/reftests/transforms/flatten-all-flat.yaml
new file mode 100644
index 0000000000..d471b052cc
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/flatten-all-flat.yaml
@@ -0,0 +1,20 @@
+# This test has a series of flat transforms that affect Z.
+---
+root:
+ items:
+ - type: reference-frame
+ transform: rotate-y(0.0001) # this transform used to affect the Z coordinates during flattening
+ transform-style: flat
+ items:
+ - type: reference-frame
+ perspective: 1000
+ transform-style: flat
+ items:
+ - type: reference-frame
+ transform: rotate-z(-45) rotate-x(-75)
+ bounds: 50 0 100 100
+ transform-style: flat
+ items:
+ - type: rect
+ bounds: 0 0 100 100
+ color: green
diff --git a/gfx/wr/wrench/reftests/transforms/flatten-preserve-3d-root-ref.yaml b/gfx/wr/wrench/reftests/transforms/flatten-preserve-3d-root-ref.yaml
new file mode 100644
index 0000000000..5dbf829e02
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/flatten-preserve-3d-root-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - bounds: [100, 150, 150, 75]
+ type: rect
+ color: green
diff --git a/gfx/wr/wrench/reftests/transforms/flatten-preserve-3d-root.yaml b/gfx/wr/wrench/reftests/transforms/flatten-preserve-3d-root.yaml
new file mode 100644
index 0000000000..b3507a985a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/flatten-preserve-3d-root.yaml
@@ -0,0 +1,23 @@
+# This test ensures that we flatten the trasformations (i.e. zero out Z coordinates)
+# at the boundaries of preserve-3d hierarchies.
+# If the stacking context isn't flattened at the preserve-3d boundary here,
+# it's non-zero Z component starts affecting the screen space position
+# due to the "rotate-x" transform at the top level.
+---
+root:
+ items:
+ -
+ bounds: [100, 100, 0, 0]
+ type: stacking-context
+ transform: rotate-x(60)
+ transform-style: flat
+ items:
+ -
+ type: "stacking-context"
+ transform: translate(0, 0, 200)
+ transform-style: preserve-3d
+ items:
+ -
+ bounds: [0, 0, 150, 150]
+ type: rect
+ color: green
diff --git a/gfx/wr/wrench/reftests/transforms/flatten-twice-ref.yaml b/gfx/wr/wrench/reftests/transforms/flatten-twice-ref.yaml
new file mode 100644
index 0000000000..27e4183555
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/flatten-twice-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+ items:
+ - type: rect
+ bounds: [100, 100, 200, 100]
+ color: green
diff --git a/gfx/wr/wrench/reftests/transforms/flatten-twice.yaml b/gfx/wr/wrench/reftests/transforms/flatten-twice.yaml
new file mode 100644
index 0000000000..9af750a7c0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/flatten-twice.yaml
@@ -0,0 +1,21 @@
+# This test ensures that we flatten the "flat" style trasformations.
+# If the flattening doesn't happen here, the rect gets rotated back
+# to the original position.
+---
+root:
+ items:
+ -
+ bounds: [100, 100, 0, 0]
+ type: stacking-context
+ transform: rotate-x(45)
+ transform-origin: 0 0
+ items:
+ -
+ type: "stacking-context"
+ transform: rotate-x(-45)
+ transform-origin: 0 0
+ items:
+ -
+ bounds: [0, 0, 200, 200]
+ type: rect
+ color: green
diff --git a/gfx/wr/wrench/reftests/transforms/image-rotated-clip.png b/gfx/wr/wrench/reftests/transforms/image-rotated-clip.png
new file mode 100644
index 0000000000..96439dbc6e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/image-rotated-clip.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/image-rotated-clip.yaml b/gfx/wr/wrench/reftests/transforms/image-rotated-clip.yaml
new file mode 100644
index 0000000000..71f7300a5f
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/image-rotated-clip.yaml
@@ -0,0 +1,19 @@
+---
+root:
+ items:
+ -
+ bounds: [18, 18, 400, 400]
+ "clip-rect": [18, 18, 400, 400]
+ type: clip
+ id: 2
+ "content-size": [400, 400]
+ -
+ bounds: [0, 0, 0, 0]
+ clip-chain: [2]
+ type: "stacking-context"
+ transform: [0.70710677, 0.70710677, 0, 0, -0.70710677, 0.70710677, 0, 0, 0, 0, 1, 0, 218, -64.84271, 0, 1]
+ items:
+ -
+ bounds: [0, 0, 400, 400]
+ "clip-rect": [0, 0, 400, 400]
+ image: solid-color(255, 0, 0, 255, 400, 400)
diff --git a/gfx/wr/wrench/reftests/transforms/image.png b/gfx/wr/wrench/reftests/transforms/image.png
new file mode 100644
index 0000000000..c8ec5aefcc
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/image.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/large-raster-root.yaml b/gfx/wr/wrench/reftests/transforms/large-raster-root.yaml
new file mode 100644
index 0000000000..0c5176d06d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/large-raster-root.yaml
@@ -0,0 +1,14 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 600, 600]
+ perspective: 20
+ items:
+ - type: stacking-context
+ transform: rotate-z(-45) rotate-x(-45)
+ filters: drop-shadow([0, 0], 10000, blue)
+ items:
+ - type: rect
+ bounds: [0, 0, 20000, 100]
+ color: green
diff --git a/gfx/wr/wrench/reftests/transforms/local-clip.png b/gfx/wr/wrench/reftests/transforms/local-clip.png
new file mode 100644
index 0000000000..1f5004902c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/local-clip.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/local-clip.yaml b/gfx/wr/wrench/reftests/transforms/local-clip.yaml
new file mode 100644
index 0000000000..035a97cc17
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/local-clip.yaml
@@ -0,0 +1,26 @@
+---
+root:
+ items:
+ -
+ bounds: [0, 0, 1000, 1000]
+ "clip-rect": [0, 0, 1000, 1000]
+ type: clip
+ id: 3
+ "content-size": [1000, 1000]
+ -
+ bounds: [0, 0, 0, 0]
+ clip-chain: [3]
+ type: "stacking-context"
+ transform: rotate(-45) translate(200, 200)
+ items:
+ -
+ bounds: [0, 0, 100, 100]
+ "clip-rect": [0, 0, 100, 100]
+ type: clip
+ id: 4
+ "content-size": [100, 100]
+ -
+ bounds: [2, -182, 152, 216]
+ "clip-rect": [2, 0, 152, 34]
+ clip-chain: [3, 4]
+ image: solid-color(255, 0, 0, 255, 100, 100)
diff --git a/gfx/wr/wrench/reftests/transforms/near-plane-clip.png b/gfx/wr/wrench/reftests/transforms/near-plane-clip.png
new file mode 100644
index 0000000000..05dabb1fe2
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/near-plane-clip.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/near-plane-clip.yaml b/gfx/wr/wrench/reftests/transforms/near-plane-clip.yaml
new file mode 100644
index 0000000000..3bbafca974
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/near-plane-clip.yaml
@@ -0,0 +1,18 @@
+# Test the calculation for screen bounds for transformed primitives that cross the near plane.
+---
+root:
+ items:
+ -
+ bounds: [0, 0, 1000, 1000]
+ type: "stacking-context"
+ perspective: 200
+ items:
+ -
+ bounds: [0, 0, 1000, 1000]
+ type: "stacking-context"
+ transform: rotate-x(30)
+ items:
+ -
+ bounds: [350, 200, 260, 300]
+ image: checkerboard(2, 16, 16)
+ stretch-size: 260 260
diff --git a/gfx/wr/wrench/reftests/transforms/nested-local-scale-ref.yaml b/gfx/wr/wrench/reftests/transforms/nested-local-scale-ref.yaml
new file mode 100644
index 0000000000..79007ad26a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/nested-local-scale-ref.yaml
@@ -0,0 +1,23 @@
+---
+root:
+ items:
+ - type: "stacking-context"
+ transform: rotate(45)
+ transform-origin: 450 150
+ raster-space: local(5.0)
+ items:
+ - type: "stacking-context"
+ transform: scale(5)
+ transform-origin: 50 50
+ filters: [identity]
+ raster-space: local(5.0)
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [0, 0, 100, 100]
+ radius: 10
+ - type: rect
+ clip-chain: [2]
+ bounds: 0 0 100 100
+ color: red
diff --git a/gfx/wr/wrench/reftests/transforms/nested-local-scale.yaml b/gfx/wr/wrench/reftests/transforms/nested-local-scale.yaml
new file mode 100644
index 0000000000..6733cdb416
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/nested-local-scale.yaml
@@ -0,0 +1,24 @@
+# Ensure that local raster scale from a parent is propagated to child surfaces
+---
+root:
+ items:
+ - type: "stacking-context"
+ transform: rotate(45)
+ transform-origin: 450 150
+ raster-space: local(5.0)
+ items:
+ - type: "stacking-context"
+ transform: scale(5)
+ transform-origin: 50 50
+ filters: [identity]
+ raster-space: local(1.0)
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [0, 0, 100, 100]
+ radius: 10
+ - type: rect
+ clip-chain: [2]
+ bounds: 0 0 100 100
+ color: red
diff --git a/gfx/wr/wrench/reftests/transforms/nested-preserve-3d.png b/gfx/wr/wrench/reftests/transforms/nested-preserve-3d.png
new file mode 100644
index 0000000000..686a22cf0a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/nested-preserve-3d.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/nested-preserve-3d.yaml b/gfx/wr/wrench/reftests/transforms/nested-preserve-3d.yaml
new file mode 100644
index 0000000000..e7f0f05407
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/nested-preserve-3d.yaml
@@ -0,0 +1,28 @@
+---
+root:
+ items:
+ -
+ bounds: [260, 260, 231, 231]
+ "clip-rect": [260, 260, 231, 231]
+ type: border
+ width: 3
+ "border-type": normal
+ color: 0 0 255 1.0000
+ style: dashed
+ -
+ bounds: [300, 300, 0, 0]
+ "clip-rect": [300, 300, 0, 0]
+ type: "stacking-context"
+ transform: [1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 75, -75, 1]
+ transform-style: preserve-3d
+ items:
+ -
+ type: "stacking-context"
+ transform: [1, 0, 0, 0, 0, 0.8660254, -0.5, 0, 0, 0.5, 0.8660254, 0, 0, 10.048096, 37.5, 1]
+ transform-style: preserve-3d
+ items:
+ -
+ bounds: [0, 0, 150, 150]
+ "clip-rect": [0, 0, 150, 150]
+ type: rect
+ color: 255 255 0 0.4000
diff --git a/gfx/wr/wrench/reftests/transforms/nested-rotate-x-flat.png b/gfx/wr/wrench/reftests/transforms/nested-rotate-x-flat.png
new file mode 100644
index 0000000000..ca33ca1336
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/nested-rotate-x-flat.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/nested-rotate-x-flat.yaml b/gfx/wr/wrench/reftests/transforms/nested-rotate-x-flat.yaml
new file mode 100644
index 0000000000..ed25c8f09d
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/nested-rotate-x-flat.yaml
@@ -0,0 +1,27 @@
+# This is the same as nested-rotate-x.yaml but without the preserve-3d.
+---
+root:
+ items:
+ -
+ bounds: [260, 260, 231, 231]
+ "clip-rect": [260, 260, 231, 231]
+ type: border
+ width: 3
+ "border-type": normal
+ color: 0 0 255 1.0000
+ style: dashed
+ -
+ bounds: [300, 300, 0, 0]
+ "clip-rect": [300, 300, 0, 0]
+ type: "stacking-context"
+ transform: [1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 75, -75, 1]
+ items:
+ -
+ type: "stacking-context"
+ transform: [1, 0, 0, 0, 0, 0.8660254, -0.5, 0, 0, 0.5, 0.8660254, 0, 0, 10.048096, 37.5, 1]
+ items:
+ -
+ bounds: [0, 0, 150, 150]
+ "clip-rect": [0, 0, 150, 150]
+ type: rect
+ color: 255 255 0 0.4000
diff --git a/gfx/wr/wrench/reftests/transforms/nested-rotate-x.png b/gfx/wr/wrench/reftests/transforms/nested-rotate-x.png
new file mode 100644
index 0000000000..73ad1cec41
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/nested-rotate-x.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/nested-rotate-x.yaml b/gfx/wr/wrench/reftests/transforms/nested-rotate-x.yaml
new file mode 100644
index 0000000000..f1f8bd36c9
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/nested-rotate-x.yaml
@@ -0,0 +1,27 @@
+---
+root:
+ items:
+ -
+ bounds: [260, 260, 231, 231]
+ "clip-rect": [260, 260, 231, 231]
+ type: border
+ width: 3
+ "border-type": normal
+ color: 0 0 255 1.0000
+ style: dashed
+ -
+ bounds: [300, 300, 0, 0]
+ "clip-rect": [300, 300, 0, 0]
+ type: "stacking-context"
+ transform: [1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 75, -75, 1]
+ transform-style: preserve-3d
+ items:
+ -
+ type: "stacking-context"
+ transform: [1, 0, 0, 0, 0, 0.8660254, -0.5, 0, 0, 0.5, 0.8660254, 0, 0, 10.048096, 37.5, 1]
+ items:
+ -
+ bounds: [0, 0, 150, 150]
+ "clip-rect": [0, 0, 150, 150]
+ type: rect
+ color: 255 255 0 0.4000
diff --git a/gfx/wr/wrench/reftests/transforms/non-inversible-world-rect.yaml b/gfx/wr/wrench/reftests/transforms/non-inversible-world-rect.yaml
new file mode 100644
index 0000000000..f94404f58e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/non-inversible-world-rect.yaml
@@ -0,0 +1,23 @@
+# Tests that `get_raster_rects` raster -> world transform is inversible in general,
+# but one of the vertices of the world rectangles can't map back to the raster.
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: 0 0 400 400
+ perspective: 800
+ perspective-origin: 50% 200
+ items:
+ - type: stacking-context
+ bounds: 0 0 400 400
+ transform-style: preserve-3d
+ transform: rotate-z(40) translate(400, 200, 0)
+ margin: 100
+ items:
+ - type: stacking-context
+ bounds: 0 0 1000 1000
+ transform: rotate-y(-75) translate(0, 0, -500)
+ items:
+ - type: rect
+ bounds: [0, 0, 200, 200]
+ color: red
diff --git a/gfx/wr/wrench/reftests/transforms/perspective-border-radius.png b/gfx/wr/wrench/reftests/transforms/perspective-border-radius.png
new file mode 100644
index 0000000000..bf74b0a502
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/perspective-border-radius.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/perspective-border-radius.yaml b/gfx/wr/wrench/reftests/transforms/perspective-border-radius.yaml
new file mode 100644
index 0000000000..5eb1f4230c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/perspective-border-radius.yaml
@@ -0,0 +1,26 @@
+---
+root:
+ items:
+ -
+ bounds: [0, 0, 1000, 1000]
+ type: "stacking-context"
+ perspective: 256
+ items:
+ -
+ bounds: [128, 128, 256, 256]
+ type: "stacking-context"
+ transform: rotate-x(-60) rotate-y(-120)
+ items:
+ - type: clip
+ id: 101
+ complex:
+ - rect: [128, 128, 256, 256]
+ radius: [64, 32]
+ - type: clip-chain
+ id: 201
+ clips: [101]
+ -
+ bounds: [128, 128, 256, 256]
+ type: rect
+ color: blue
+ clip-chain: 201
diff --git a/gfx/wr/wrench/reftests/transforms/perspective-box-shadow-ref.yaml b/gfx/wr/wrench/reftests/transforms/perspective-box-shadow-ref.yaml
new file mode 100644
index 0000000000..36df831e19
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/perspective-box-shadow-ref.yaml
@@ -0,0 +1,24 @@
+---
+root:
+ items:
+ -
+ type: "stacking-context"
+ transform-style: "preserve-3d"
+ items:
+ -
+ type: "reference-frame"
+ transform: [1, 0, 0, 0, -0.20399817824363708, 0.05275486409664154, 0.9659258127212524, -0.0008049382013268769, -0.05466114357113838, -1.0211405754089355, 0.258819043636322, -0.00021568253578152508, 52.22355651855469, 242.49476623535156, -247.27700805664063, 1.206064224243164]
+ items:
+ -
+ type: "stacking-context"
+ items:
+ -
+ type: "box-shadow"
+ bounds: [30, 0, 507, 512]
+ box-bounds: [30, 0, 507, 512]
+ offset: [-14, 4]
+ blur-radius: 8
+ spread-radius: -4
+ border-radius: 256
+ clip-mode: "inset"
+ color: red
diff --git a/gfx/wr/wrench/reftests/transforms/perspective-box-shadow.yaml b/gfx/wr/wrench/reftests/transforms/perspective-box-shadow.yaml
new file mode 100644
index 0000000000..99e247bf49
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/perspective-box-shadow.yaml
@@ -0,0 +1,23 @@
+---
+root:
+ items:
+ -
+ type: "stacking-context"
+ items:
+ -
+ type: "reference-frame"
+ transform: [1, 0, 0, 0, -0.20399817824363708, 0.05275486409664154, 0.9659258127212524, -0.0008049382013268769, -0.05466114357113838, -1.0211405754089355, 0.258819043636322, -0.00021568253578152508, 52.22355651855469, 242.49476623535156, -247.27700805664063, 1.206064224243164]
+ items:
+ -
+ type: "stacking-context"
+ items:
+ -
+ type: "box-shadow"
+ bounds: [30, 0, 507, 512]
+ box-bounds: [30, 0, 507, 512]
+ offset: [-14, 4]
+ blur-radius: 8
+ spread-radius: -4
+ border-radius: 256
+ clip-mode: "inset"
+ color: red
diff --git a/gfx/wr/wrench/reftests/transforms/perspective-clip-1.png b/gfx/wr/wrench/reftests/transforms/perspective-clip-1.png
new file mode 100644
index 0000000000..2cc992c168
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/perspective-clip-1.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/perspective-clip-1.yaml b/gfx/wr/wrench/reftests/transforms/perspective-clip-1.yaml
new file mode 100644
index 0000000000..82668e28cc
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/perspective-clip-1.yaml
@@ -0,0 +1,30 @@
+---
+root:
+ items:
+ -
+ type: stacking-context
+ bounds: [0, 0, 0, 0]
+ perspective: [1, 0, 0, 0, 0, 1, 0, 0, -300, -250, 1, -0.5, 0, 0, 0, 1]
+ items:
+ -
+ bounds: [0, 0, 0, 0]
+ type: stacking-context
+ transform: [10, 0, 0, 0, 0, 10, 0, 0, 0, 0, 1, 0, -5382, -3222, -18, 1]
+ items:
+ -
+ bounds: [0, 0, 100, 100]
+ type: rect
+ color: red
+ - type: clip
+ id: 101
+ complex:
+ - rect: [0, 0, 100, 100]
+ radius: 16
+ - type: clip-chain
+ id: 201
+ clips: [101]
+ -
+ bounds: [0, 0, 100, 100]
+ type: rect
+ color: green
+ clip-chain: 201
diff --git a/gfx/wr/wrench/reftests/transforms/perspective-clip.png b/gfx/wr/wrench/reftests/transforms/perspective-clip.png
new file mode 100644
index 0000000000..ef97fb311e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/perspective-clip.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/perspective-clip.yaml b/gfx/wr/wrench/reftests/transforms/perspective-clip.yaml
new file mode 100644
index 0000000000..bbb9c35efb
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/perspective-clip.yaml
@@ -0,0 +1,27 @@
+# Test that a local space clip is correctly applied to
+# a primitive with a perspective transform.
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [225, 200, 150, 200]
+ radius: 32
+ -
+ type: "stacking-context"
+ bounds: 0 0 1000 1000
+ perspective: 100
+ perspective-origin: 300 300
+ items:
+ -
+ type: "stacking-context"
+ transform: rotate-x(-10)
+ transform-origin: 300 300
+ filters: identity
+ items:
+ -
+ bounds: [200, 200, 200, 200]
+ image: checkerboard(2, 16, 12)
+ stretch-size: 200 200
+ clip-chain: [2]
diff --git a/gfx/wr/wrench/reftests/transforms/perspective-mask.png b/gfx/wr/wrench/reftests/transforms/perspective-mask.png
new file mode 100644
index 0000000000..fdcb7230e3
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/perspective-mask.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/perspective-mask.yaml b/gfx/wr/wrench/reftests/transforms/perspective-mask.yaml
new file mode 100644
index 0000000000..9f95549554
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/perspective-mask.yaml
@@ -0,0 +1,24 @@
+---
+root:
+ items:
+ -
+ type: "scroll-frame"
+ id: 2
+ "content-size": [500, 100]
+ bounds: [0, 0, 500, 100]
+ clip-to-frame: true
+ items:
+ -
+ type: "stacking-context"
+ perspective: 125
+ items:
+ -
+ type: "stacking-context"
+ bounds: [0, 0, 250, 100]
+ transform: rotate-y(54)
+ items:
+ -
+ bounds: [0, 0, 128, 128]
+ type: rect
+ color: red
+ clip-chain: [2]
diff --git a/gfx/wr/wrench/reftests/transforms/perspective-origin.png b/gfx/wr/wrench/reftests/transforms/perspective-origin.png
new file mode 100644
index 0000000000..581d524767
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/perspective-origin.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/perspective-origin.yaml b/gfx/wr/wrench/reftests/transforms/perspective-origin.yaml
new file mode 100644
index 0000000000..ca5eb5f95c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/perspective-origin.yaml
@@ -0,0 +1,16 @@
+---
+root:
+ items:
+ -
+ bounds: [0, 0, 1000, 1000]
+ type: "stacking-context"
+ perspective: 200
+ items:
+ -
+ bounds: [0, 0, 1000, 1000]
+ type: "stacking-context"
+ transform: rotate-x(-45)
+ items:
+ -
+ bounds: [350, 400, 260, 260]
+ image: checkerboard(2, 16, 16)
diff --git a/gfx/wr/wrench/reftests/transforms/perspective-shadow.png b/gfx/wr/wrench/reftests/transforms/perspective-shadow.png
new file mode 100644
index 0000000000..07befbd35b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/perspective-shadow.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/perspective-shadow.yaml b/gfx/wr/wrench/reftests/transforms/perspective-shadow.yaml
new file mode 100644
index 0000000000..8b8e7c87d2
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/perspective-shadow.yaml
@@ -0,0 +1,27 @@
+---
+root:
+ items:
+ -
+ type: stacking-context
+ bounds: [0, 0, 0, 0]
+ transform: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 139, 0, 1]
+ items:
+ -
+ type: stacking-context
+ bounds: [0, 0, 0, 0]
+ perspective: [1, 0, 0, 0, 0, 1, 0, 0, -299, -250, 1, -0.5, 0, 0, 0, 1]
+ items:
+ -
+ bounds: [0, 0, 0, 0]
+ type: stacking-context
+ transform: [10, 0, 0, 0, 0, 10, 0, 0, 0, 0, 1, 0, -5382, -3222, -18, 1]
+ items:
+ -
+ type: box-shadow
+ bounds: [548, 200, 100, 100]
+ color: 0 0 0 0.7020
+ blur-radius: 20
+ -
+ bounds: [548, 200, 100, 100]
+ type: rect
+ color: 0 255 0 1.0000
diff --git a/gfx/wr/wrench/reftests/transforms/perspective-surface-scale.png b/gfx/wr/wrench/reftests/transforms/perspective-surface-scale.png
new file mode 100644
index 0000000000..f80f5938d5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/perspective-surface-scale.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/perspective-surface-scale.yaml b/gfx/wr/wrench/reftests/transforms/perspective-surface-scale.yaml
new file mode 100644
index 0000000000..6888f91c7e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/perspective-surface-scale.yaml
@@ -0,0 +1,14 @@
+# Verify that we select a reasonable scale factor for perspective surfaces that don't
+# have a requested scale factor supplied by the caller (based on projected screen rect)
+root:
+ items:
+ - type: "stacking-context"
+ perspective: 1000
+ transform-style: preserve-3d
+ items:
+ - type: "stacking-context"
+ transform-origin: 175 175
+ transform: rotate-x(-20) rotate-y(20)
+ items:
+ - image: checkerboard(2, 22, 16)
+ bounds: [0, 0, 350, 350]
diff --git a/gfx/wr/wrench/reftests/transforms/perspective.png b/gfx/wr/wrench/reftests/transforms/perspective.png
new file mode 100644
index 0000000000..05000384c2
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/perspective.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/perspective.yaml b/gfx/wr/wrench/reftests/transforms/perspective.yaml
new file mode 100644
index 0000000000..2ebd0899c6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/perspective.yaml
@@ -0,0 +1,54 @@
+---
+root:
+ items:
+ -
+ type: "stacking-context"
+ items:
+ -
+ type: "stacking-context"
+ transform: [1, 0, 0, 0, -1.0606601, -0.35355338, 0.70710677, -0.0035355338, -1.0606601, -1.767767, 0.70710677, -0.0035355338, 306.06604, 335.35535, -70.71068, 1.3535534]
+ items:
+ -
+ bounds: [0, 0, 200, 200]
+ image: checkerboard(2, 16, 12)
+ stretch-size: 200 200
+ -
+ type: "stacking-context"
+ transform: [3.1819804, 1.0606601, -0.70710677, 0.0035355338, 0, 1, 0, 0, -1.7677668, -1.0606601, 0.70710677, -0.0035355338, 381.80194, 93.93398, 70.71068, 0.6464466]
+ items:
+ -
+ bounds: [0, 0, 200, 200]
+ image: checkerboard(2, 16, 12)
+ stretch-size: 200 200
+ -
+ type: "stacking-context"
+ transform: [3.182372, 1.088346, -0.4353384, 0.002176692, -1.0183089, 0.7350135, 0.10599462, -0.00052997307, -4.481691, -1.2350134, 0.89400536, -0.0044700266, 883.5937, 117.66405, 32.93438, 0.8353281]
+ items:
+ -
+ bounds: [0, 0, 200, 200]
+ image: checkerboard(2, 16, 12)
+ stretch-size: 200 200
+ -
+ type: "stacking-context"
+ transform: [0.29134378, -0.9896399, 0.6324555, -0.0015811388, 0.3543281, 1.4948199, -0.31622776, 0.0007905694, -1.1627856, -0.92120904, 0.70710677, -0.0017677669, 235.43283, 649.48206, -31.622776, 1.079057]
+ items:
+ -
+ bounds: [0, 0, 200, 200]
+ image: checkerboard(2, 16, 12)
+ stretch-size: 200 200
+ -
+ type: "stacking-context"
+ transform: [1, 0, 0, 0, 1, 1, 0, 0, -3.5, -3.5, 1, -0.005, 500, 600, 0, 1]
+ items:
+ -
+ bounds: [0, 0, 200, 200]
+ image: checkerboard(2, 16, 12)
+ stretch-size: 200 200
+ -
+ type: "stacking-context"
+ transform: [0.82903755, 0.78133285, 0, 0, -0.5591929, 0.67920226, 0, 0, -5.5, -3.5, 1, -0.005, 1073.0155, 553.9465, 0, 1]
+ items:
+ -
+ bounds: [0, 0, 200, 200]
+ image: checkerboard(2, 16, 12)
+ stretch-size: 200 200
diff --git a/gfx/wr/wrench/reftests/transforms/prim-suite.png b/gfx/wr/wrench/reftests/transforms/prim-suite.png
new file mode 100644
index 0000000000..0ae3ebe2ed
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/prim-suite.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/prim-suite.yaml b/gfx/wr/wrench/reftests/transforms/prim-suite.yaml
new file mode 100644
index 0000000000..241a2e1c0a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/prim-suite.yaml
@@ -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]
diff --git a/gfx/wr/wrench/reftests/transforms/raster-root-huge-scale.yaml b/gfx/wr/wrench/reftests/transforms/raster-root-huge-scale.yaml
new file mode 100644
index 0000000000..acdcc61d56
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/raster-root-huge-scale.yaml
@@ -0,0 +1,35 @@
+# Tests that raster roots with very large scaling factors do not cause device rects to overflow.
+# See bug 1642440
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: 0 0 0 0
+ transform: scale(99999999)
+ transform-style: preserve-3d
+ filters: invert(1)
+ items:
+ - type: stacking-context
+ bounds: 0 0 0 0
+ transform: scale(0.0000001)
+ transform-style: preserve-3d
+ items:
+ - type: clip
+ clip-rect: [0, 0, 60000, 60000]
+ id: 2
+ complex:
+ - rect: [0, 0, 100, 100]
+ radius: [20, 20]
+ "clip-mode": clip
+ - type: stacking-context
+ bounds: 0 0 0 0
+ items:
+ - rect: 0 0 100 100
+ color: red
+ clip-chain: [2]
+ - type: box-shadow
+ bounds: 0 0 100 100
+ color: blue
+ offset: 10 20
+ blur-radius: 10
+ border-radius: [ 20, 20, 20, 20 ]
diff --git a/gfx/wr/wrench/reftests/transforms/raster-root-large-mask.yaml b/gfx/wr/wrench/reftests/transforms/raster-root-large-mask.yaml
new file mode 100644
index 0000000000..15cea6a697
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/raster-root-large-mask.yaml
@@ -0,0 +1,28 @@
+# Make sure we don't panic by trying to create an excessively large mask render task
+# See bug 1637796
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: 0 0 0 0
+ transform: perspective(300)
+ items:
+ - type: stacking-context
+ bounds: 0 0 0 0
+ transform-style: preserve-3d
+ transform: ["scale(0.001667, 0.001667)"]
+ items:
+ - type: clip
+ clip-rect: [10, 10, 60000, 60000]
+ id: 2
+ complex:
+ - rect: [10, 10, 60000, 60000]
+ radius: [10000, 10000]
+ "clip-mode": clip
+ - type: stacking-context
+ bounds: 0 0 0 0
+ items:
+ - type: rect
+ bounds: 0 0 60000 60000
+ color: blue
+ clip-chain: [2]
diff --git a/gfx/wr/wrench/reftests/transforms/raster-root-scaling-2-ref.yaml b/gfx/wr/wrench/reftests/transforms/raster-root-scaling-2-ref.yaml
new file mode 100644
index 0000000000..925d81951e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/raster-root-scaling-2-ref.yaml
@@ -0,0 +1,11 @@
+# Tests that the correct device pixel scale is propagated to child surfaces.
+# See bug 1639729
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 0, 0]
+ filters: invert(1)
+ items:
+ - image: checkerboard(2, 16, 16)
+ bounds: [0, 0, 260, 260]
diff --git a/gfx/wr/wrench/reftests/transforms/raster-root-scaling-2.yaml b/gfx/wr/wrench/reftests/transforms/raster-root-scaling-2.yaml
new file mode 100644
index 0000000000..4455b286e5
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/raster-root-scaling-2.yaml
@@ -0,0 +1,20 @@
+# Tests that the correct device pixel scale is propagated to child surfaces.
+# See bug 1639729
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 0, 0]
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 0, 0]
+ transform-style: preserve-3d
+ transform: scale(10,10)
+ items:
+ - type: stacking-context
+ bounds: 0 0 0 0
+ # Create a non-raster root surface
+ filters: invert(1)
+ items:
+ - image: checkerboard(2, 16, 16)
+ bounds: [0, 0, 26, 26]
diff --git a/gfx/wr/wrench/reftests/transforms/raster-root-scaling-ref.yaml b/gfx/wr/wrench/reftests/transforms/raster-root-scaling-ref.yaml
new file mode 100644
index 0000000000..5088e01756
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/raster-root-scaling-ref.yaml
@@ -0,0 +1,10 @@
+
+# Tests that surfaces created by raster roots are scaled based on the surface to parent transform.
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 0, 0]
+ items:
+ - image: checkerboard(2, 16, 16)
+ bounds: [0, 0, 260, 260]
diff --git a/gfx/wr/wrench/reftests/transforms/raster-root-scaling.yaml b/gfx/wr/wrench/reftests/transforms/raster-root-scaling.yaml
new file mode 100644
index 0000000000..e7d873888e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/raster-root-scaling.yaml
@@ -0,0 +1,14 @@
+# Tests that surfaces created by raster roots are scaled based on the surface to parent transform.
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 0, 0]
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 0, 0]
+ transform-style: preserve-3d
+ transform: scale(10,10)
+ items:
+ - image: checkerboard(2, 16, 16)
+ bounds: [0, 0, 26, 26]
diff --git a/gfx/wr/wrench/reftests/transforms/raster_root_A_8192.yaml b/gfx/wr/wrench/reftests/transforms/raster_root_A_8192.yaml
new file mode 100644
index 0000000000..99813bacb8
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/raster_root_A_8192.yaml
@@ -0,0 +1,26 @@
+root:
+ items:
+ - type: "stacking-context"
+ transform: scale(0.125)
+ items:
+ - type: "stacking-context"
+ perspective: 100
+ perspective-origin: 100 50
+ items:
+ - image: checkerboard(0, 512, 16);
+ bounds: [1600, 1600, 8192, 8192]
+ - type: clip
+ id: 101
+ complex:
+ - rect: [2048, 2048, 4096, 4096]
+ radius: [1024, 1024]
+ - type: clip-chain
+ id: 201
+ clips: [101]
+ - type: "stacking-context"
+ bounds: [0, 0, 8192, 8192]
+ mix-blend-mode: difference
+ clip-chain: 201
+ items:
+ - image: checkerboard(0, 4096, 2);
+ bounds: [0, 0, 8192, 8192]
diff --git a/gfx/wr/wrench/reftests/transforms/raster_root_A_ref.yaml b/gfx/wr/wrench/reftests/transforms/raster_root_A_ref.yaml
new file mode 100644
index 0000000000..82073907b6
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/raster_root_A_ref.yaml
@@ -0,0 +1,26 @@
+root:
+ items:
+ - type: "stacking-context"
+ transform: scale(0.5)
+ items:
+ - type: "stacking-context"
+ perspective: 100
+ perspective-origin: 100 50
+ items:
+ - image: checkerboard(0, 128, 16);
+ bounds: 400 400 2048 2048
+ - type: clip
+ id: 101
+ complex:
+ - rect: [512, 512, 1024, 1024]
+ radius: [256, 256]
+ - type: clip-chain
+ id: 201
+ clips: [101]
+ - type: "stacking-context"
+ bounds: [0, 0, 2048, 2048]
+ mix-blend-mode: difference
+ clip-chain: 201
+ items:
+ - image: checkerboard(0, 1024, 2);
+ bounds: [0, 0, 2048, 2048]
diff --git a/gfx/wr/wrench/reftests/transforms/raster_root_B_8192.yaml b/gfx/wr/wrench/reftests/transforms/raster_root_B_8192.yaml
new file mode 100644
index 0000000000..9f8a58f5cc
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/raster_root_B_8192.yaml
@@ -0,0 +1,14 @@
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 600, 600]
+ perspective: 100
+ items:
+ - type: stacking-context
+ transform: rotate-z(20) rotate-x(60)
+ filters: [invert(1)]
+ mix-blend-mode: difference
+ items:
+ - type: rect
+ bounds: [0, 0, 20000, 100]
+ color: [20, 120, 18, 1.0]
diff --git a/gfx/wr/wrench/reftests/transforms/raster_root_B_ref.yaml b/gfx/wr/wrench/reftests/transforms/raster_root_B_ref.yaml
new file mode 100644
index 0000000000..3fea3a19db
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/raster_root_B_ref.yaml
@@ -0,0 +1,14 @@
+root:
+ items:
+ - type: stacking-context
+ bounds: [0, 0, 600, 600]
+ perspective: 100
+ items:
+ - type: stacking-context
+ transform: rotate-z(20) rotate-x(60)
+ filters: [invert(1)]
+ mix-blend-mode: difference
+ items:
+ - type: rect
+ bounds: [0, 0, 4000, 100]
+ color: [20, 120, 18, 1.0]
diff --git a/gfx/wr/wrench/reftests/transforms/reftest.list b/gfx/wr/wrench/reftests/transforms/reftest.list
new file mode 100644
index 0000000000..04dd52479e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/reftest.list
@@ -0,0 +1,59 @@
+platform(linux,mac) == local-clip.yaml local-clip.png
+platform(linux,mac) == rotated-clip.yaml rotated-clip.png
+platform(linux,mac) == rotated-clip-large.yaml rotated-clip-large.png
+platform(linux,mac) == image-rotated-clip.yaml image-rotated-clip.png
+# Something leaks the state: the test passes if only run `reftest reftests/transform`
+# but fails when all the tests are run
+platform(linux,mac) fuzzy(1,6) == rotated-image.yaml rotated-image.png
+== singular.yaml singular-ref.yaml
+platform(linux) fuzzy(1,630) == perspective.yaml perspective.png
+platform(linux,mac) fuzzy(3,8100) == prim-suite.yaml prim-suite.png
+fuzzy(2,26) == segments-bug.yaml segments-bug-ref.yaml
+platform(linux,mac) == content-offset.yaml content-offset.png
+platform(linux,mac) == coord-system.yaml coord-system.png
+platform(linux,mac) fuzzy(1,15) == border-zoom.yaml border-zoom.png
+platform(linux) fuzzy(1,520) == perspective-origin.yaml perspective-origin.png
+platform(linux,mac) color_targets(3) alpha_targets(0) fuzzy(1,180) == screen-space-blit.yaml screen-space-blit.png
+platform(linux,mac) fuzzy(1,346) color_targets(2) alpha_targets(0) == screen-space-blit-trivial.yaml screen-space-blit-trivial.png
+platform(linux) fuzzy(11,4592) == screen-space-blur.yaml screen-space-blur.png
+platform(linux,mac) fuzzy(1,25) == nested-rotate-x.yaml nested-rotate-x.png
+platform(linux,mac) != nested-rotate-x.yaml nested-rotate-x-flat.yaml
+platform(linux,mac) fuzzy(1,25) == nested-rotate-x-flat.yaml nested-rotate-x-flat.png
+platform(linux,mac) fuzzy(1,25) == nested-preserve-3d.yaml nested-preserve-3d.png
+platform(linux,mac) fuzzy(1,283) == near-plane-clip.yaml near-plane-clip.png
+platform(linux,mac) == perspective-mask.yaml perspective-mask.png
+== rotate-clip.yaml rotate-clip-ref.yaml
+== clip-translate.yaml clip-translate-ref.yaml
+platform(linux,mac) fuzzy(1,1) == perspective-clip.yaml perspective-clip.png
+platform(linux,mac) fuzzy(1,2) == perspective-clip-1.yaml perspective-clip-1.png
+platform(linux,mac) fuzzy(1,2) == perspective-shadow.yaml perspective-shadow.png
+# The ref YAML here produces significantly worse quality
+fuzzy(200,4200) == perspective-box-shadow.yaml perspective-box-shadow-ref.yaml
+== complex-preserve-3d.yaml blank.yaml
+platform(linux,mac) fuzzy(38,348) == perspective-border-radius.yaml perspective-border-radius.png
+fuzzy(1,38) == snapped-preserve-3d.yaml snapped-preserve-3d-ref.yaml
+platform(linux,mac) fuzzy(1,122) == border-scale.yaml border-scale.png
+platform(linux,mac) fuzzy(1,16) == border-scale-2.yaml border-scale-2.png
+platform(linux,mac) fuzzy(1,69) == border-scale-3.yaml border-scale-3.png
+platform(linux,mac) fuzzy(1,74) == border-scale-4.yaml border-scale-4.png
+# Just make sure we aren't crashing here
+!= large-raster-root.yaml blank.yaml
+== flatten-preserve-3d-root.yaml flatten-preserve-3d-root-ref.yaml
+== flatten-twice.yaml flatten-twice-ref.yaml
+fuzzy(1,10) == flatten-all-flat.yaml flatten-all-flat-ref.yaml
+== strange-w.yaml strange-w-ref.yaml
+== big-axis-aligned-scale.yaml big-axis-aligned-scale-ref.yaml
+# Compare ~8K raster root (>MAX_SURFACE_SIZE) with ~2K raster root. fuzzy due to lerping on edges.
+skip_on(android) fuzzy-range(<=3,*3077,<=10,*133,<=93,*490) == raster_root_A_8192.yaml raster_root_A_ref.yaml
+# Same as large-raster-root.yaml but resulting in a 10302×100 raster root (= >4096) vs 4000x100 in ref:
+skip_on(android) fuzzy(60,917) == raster_root_B_8192.yaml raster_root_B_ref.yaml
+# Make sure we don't panic
+!= raster-root-large-mask.yaml blank.yaml
+skip_on(android) == raster-root-scaling.yaml raster-root-scaling-ref.yaml
+skip_on(android) == raster-root-scaling-2.yaml raster-root-scaling-2-ref.yaml
+# Make sure we don't panic
+!= raster-root-huge-scale.yaml blank.yaml
+!= non-inversible-world-rect.yaml blank.yaml
+== nested-local-scale.yaml nested-local-scale-ref.yaml
+platform(linux,mac) == perspective-surface-scale.yaml perspective-surface-scale.png
+fuzzy(1,8762) == computed-rotation-yflip.yaml computed-rotation-yflip.png
diff --git a/gfx/wr/wrench/reftests/transforms/rotate-clip-ref.yaml b/gfx/wr/wrench/reftests/transforms/rotate-clip-ref.yaml
new file mode 100644
index 0000000000..133279fca8
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/rotate-clip-ref.yaml
@@ -0,0 +1,7 @@
+---
+root:
+ items:
+ -
+ bounds: [100, 146, 150, 107]
+ type: rect
+ color: 0 128 0 1.0000
diff --git a/gfx/wr/wrench/reftests/transforms/rotate-clip.yaml b/gfx/wr/wrench/reftests/transforms/rotate-clip.yaml
new file mode 100644
index 0000000000..c1c01d0dc4
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/rotate-clip.yaml
@@ -0,0 +1,19 @@
+---
+root:
+ items:
+ -
+ type: clip
+ id: 2
+ bounds: [0, 0, 2000, 2000]
+ clip-rect: [0, 0, 2000, 2000]
+ -
+ bounds: [100, 0, 150, 150]
+ type: "stacking-context"
+ transform: rotate-x(45)
+ transform-origin: [0, 500]
+ items:
+ -
+ bounds: [0, 0, 150, 150]
+ type: rect
+ color: 0 128 0 1.0000
+ clip-chain: [2]
diff --git a/gfx/wr/wrench/reftests/transforms/rotated-clip-large.png b/gfx/wr/wrench/reftests/transforms/rotated-clip-large.png
new file mode 100644
index 0000000000..e3c8ce7ab4
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/rotated-clip-large.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/rotated-clip-large.yaml b/gfx/wr/wrench/reftests/transforms/rotated-clip-large.yaml
new file mode 100644
index 0000000000..cfea755a1e
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/rotated-clip-large.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ -
+ bounds: 100 100 300 300
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [20, 20, 200, 200]
+ radius: 32
+ - type: rect
+ bounds: 20 20 200 200
+ color: blue
+ clip-chain: [2]
+ type: stacking-context
+ transform: rotate(-33)
diff --git a/gfx/wr/wrench/reftests/transforms/rotated-clip.png b/gfx/wr/wrench/reftests/transforms/rotated-clip.png
new file mode 100644
index 0000000000..fe6e2aedb0
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/rotated-clip.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/rotated-clip.yaml b/gfx/wr/wrench/reftests/transforms/rotated-clip.yaml
new file mode 100644
index 0000000000..534685541b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/rotated-clip.yaml
@@ -0,0 +1,17 @@
+---
+root:
+ items:
+ -
+ bounds: 100 100 100 100
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [20, 20, 100, 100]
+ radius: 32
+ - type: rect
+ bounds: 20 20 100 100
+ color: blue
+ clip-chain: [2]
+ type: stacking-context
+ transform: rotate(-30)
diff --git a/gfx/wr/wrench/reftests/transforms/rotated-image.png b/gfx/wr/wrench/reftests/transforms/rotated-image.png
new file mode 100644
index 0000000000..012ffe6045
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/rotated-image.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/rotated-image.yaml b/gfx/wr/wrench/reftests/transforms/rotated-image.yaml
new file mode 100644
index 0000000000..382aac71e7
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/rotated-image.yaml
@@ -0,0 +1,72 @@
+---
+root:
+ items:
+ -
+ bounds: [0, 0, 2880, 1482]
+ "clip-rect": [0, 0, 2880, 1482]
+ "backface-visible": true
+ type: rect
+ color: white
+ -
+ bounds: [0, 0, 2880, 1482]
+ "clip-rect": [0, 0, 2880, 1482]
+ "backface-visible": true
+ type: clip
+ id: 10
+ "content-size": [2880, 1482]
+ -
+ "clip-rect": [0, 0, 2880, 1482]
+ "backface-visible": true
+ type: "scroll-frame"
+ id: 2
+ "content-size": [2880, 1482]
+ bounds: [0, 0, 2880, 1482]
+ -
+ bounds: [0, 0, 2880, 1482]
+ "clip-rect": [0, 0, 2880, 1482]
+ "backface-visible": true
+ type: clip
+ id: 3
+ "content-size": [2880, 1482]
+ -
+ bounds: [0, 0, 2880, 1482]
+ "clip-rect": [0, 0, 2880, 1482]
+ "backface-visible": true
+ type: rect
+ color: white
+ -
+ bounds: [0, 0, 0, 0]
+ "clip-rect": [0, 0, 0, 0]
+ clip-chain: [10, 3]
+ "backface-visible": true
+ type: "stacking-context"
+ "scroll-policy": scrollable
+ transform: rotate-z(-1)
+ "transform-style": flat
+ items:
+ -
+ bounds: [2, 2, 200, 200]
+ "clip-rect": [2, 2, 200, 200]
+ "backface-visible": true
+ type: clip
+ id: 4
+ "content-size": [200, 200]
+ -
+ bounds: [2, -182, 152, 216]
+ "clip-rect": [2, 0, 152, 34]
+ clip-chain: [10, 3, 4]
+ "backface-visible": true
+ image: "image.png"
+ "stretch-size": [152, 216]
+ "tile-spacing": [0, 0]
+ -
+ bounds: [0, 0, 204, 204]
+ "clip-rect": [0, 0, 204, 204]
+ "backface-visible": true
+ type: border
+ width: 2
+ "border-type": normal
+ color: 0 0 255 1.0000
+ style: solid
+ id: [1, 1]
+pipelines: []
diff --git a/gfx/wr/wrench/reftests/transforms/screen-space-blit-trivial.png b/gfx/wr/wrench/reftests/transforms/screen-space-blit-trivial.png
new file mode 100644
index 0000000000..a3cd78ba6b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/screen-space-blit-trivial.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/screen-space-blit-trivial.yaml b/gfx/wr/wrench/reftests/transforms/screen-space-blit-trivial.yaml
new file mode 100644
index 0000000000..76b2578faa
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/screen-space-blit-trivial.yaml
@@ -0,0 +1,22 @@
+# This test is similar to "screen-space-blit" but without filters,
+# so the implementation doesn't attempt to bake the contents
+# into a separate render target.
+---
+root:
+ items:
+ - type: "stacking-context"
+ perspective: 100
+ perspective-origin: 100 100
+ items:
+ - type: "stacking-context"
+ transform-origin: 235 235
+ transform: rotate-x(-15)
+ items:
+ - image: checkerboard(2, 16, 16)
+ bounds: [100, 100, 260, 260]
+ - type: "stacking-context"
+ transform-origin: 635 235
+ transform: rotate-z(-45)
+ items:
+ - image: checkerboard(2, 16, 16)
+ bounds: [500, 100, 260, 260]
diff --git a/gfx/wr/wrench/reftests/transforms/screen-space-blit.png b/gfx/wr/wrench/reftests/transforms/screen-space-blit.png
new file mode 100644
index 0000000000..39b7afb8e8
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/screen-space-blit.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/screen-space-blit.yaml b/gfx/wr/wrench/reftests/transforms/screen-space-blit.yaml
new file mode 100644
index 0000000000..0bae3a3736
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/screen-space-blit.yaml
@@ -0,0 +1,22 @@
+# This test uses `identity` filter on a preserve3D context to test how
+# filters mix up with the SC's baking for preserve3d.
+---
+root:
+ items:
+ - type: "stacking-context"
+ perspective: 100
+ perspective-origin: 100 100
+ items:
+ - type: "stacking-context"
+ transform-origin: 235 235
+ transform: rotate-x(-15)
+ filters: identity
+ items:
+ - image: checkerboard(2, 16, 16)
+ bounds: [100, 100, 260, 260]
+ - type: "stacking-context"
+ transform-origin: 635 235
+ transform: rotate-z(-45)
+ items:
+ - image: checkerboard(2, 16, 16)
+ bounds: [500, 100, 260, 260]
diff --git a/gfx/wr/wrench/reftests/transforms/screen-space-blur.png b/gfx/wr/wrench/reftests/transforms/screen-space-blur.png
new file mode 100644
index 0000000000..86ce88f896
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/screen-space-blur.png
Binary files differ
diff --git a/gfx/wr/wrench/reftests/transforms/screen-space-blur.yaml b/gfx/wr/wrench/reftests/transforms/screen-space-blur.yaml
new file mode 100644
index 0000000000..6d93260c83
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/screen-space-blur.yaml
@@ -0,0 +1,20 @@
+root:
+ items:
+ - type: "stacking-context"
+ perspective: 100
+ perspective-origin: 100 100
+ items:
+ - type: "stacking-context"
+ transform-origin: 235 235
+ transform: rotate-x(-15)
+ filters: blur(3, 3)
+ items:
+ - image: checkerboard(2, 16, 16)
+ bounds: [100, 100, 260, 260]
+ - type: "stacking-context"
+ transform-origin: 635 235
+ transform: rotate-z(-45)
+ filters: blur(3, 3)
+ items:
+ - image: checkerboard(2, 16, 16)
+ bounds: [500, 100, 260, 260]
diff --git a/gfx/wr/wrench/reftests/transforms/segments-bug-ref.yaml b/gfx/wr/wrench/reftests/transforms/segments-bug-ref.yaml
new file mode 100644
index 0000000000..528a440e5b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/segments-bug-ref.yaml
@@ -0,0 +1,22 @@
+---
+root:
+ items:
+ -
+ type: "stacking-context"
+ items:
+ -
+ type: clip
+ id: 4
+ complex:
+ -
+ rect: [12, 12, 130, 130]
+ radius: 20
+ "clip-mode": clip
+ -
+ type: "stacking-context"
+ items:
+ -
+ bounds: [12, 12, 130, 130]
+ clip-chain: [4]
+ type: rect
+ color: 0 128 0 1.0000
diff --git a/gfx/wr/wrench/reftests/transforms/segments-bug.yaml b/gfx/wr/wrench/reftests/transforms/segments-bug.yaml
new file mode 100644
index 0000000000..6ac89e094b
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/segments-bug.yaml
@@ -0,0 +1,26 @@
+# Test that opaque/alpha segments are correctly calculated
+# when the clip is in a different (but compatible) coordinate
+# space from the primitive.
+---
+root:
+ items:
+ -
+ type: "stacking-context"
+ items:
+ -
+ type: clip
+ id: 4
+ complex:
+ -
+ rect: [12, 12, 130, 130]
+ radius: 20
+ "clip-mode": clip
+ -
+ type: "stacking-context"
+ transform: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 12, 12, 0, 1]
+ items:
+ -
+ bounds: [0, 0, 130, 130]
+ type: rect
+ color: 0 128 0 1.0000
+ clip-chain: [4]
diff --git a/gfx/wr/wrench/reftests/transforms/singular-ref.yaml b/gfx/wr/wrench/reftests/transforms/singular-ref.yaml
new file mode 100644
index 0000000000..4d31e4f89c
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/singular-ref.yaml
@@ -0,0 +1,28 @@
+---
+root:
+ items:
+ -
+ bounds: [0, 0, 200, 200]
+ "clip-rect": [0, 0, 200, 200]
+ type: clip
+ id: 2
+ "content-size": [200, 200]
+ -
+ bounds: [0, 100, 100, 100]
+ "clip-rect": [0, 100, 100, 100]
+ clip-chain: [2]
+ type: rect
+ color: blue
+ -
+ bounds: [100, 0, 100, 100]
+ "clip-rect": [100, 0, 100, 100]
+ clip-chain: [2]
+ type: rect
+ color: green
+ -
+ bounds: [100, 100, 100, 100]
+ "clip-rect": [100, 100, 100, 100]
+ clip-chain: [2]
+ type: rect
+ color: red
+
diff --git a/gfx/wr/wrench/reftests/transforms/singular.yaml b/gfx/wr/wrench/reftests/transforms/singular.yaml
new file mode 100644
index 0000000000..9778518695
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/singular.yaml
@@ -0,0 +1,39 @@
+---
+root:
+ items:
+ -
+ bounds: [0, 0, 200, 200]
+ "clip-rect": [0, 0, 200, 200]
+ type: clip
+ id: 2
+ "content-size": [200, 200]
+ -
+ bounds: [10, 10, 80, 80]
+ clip-chain: [2]
+ type: "stacking-context"
+ transform: [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
+ items:
+ -
+ bounds: [0, 0, 80, 80]
+ "clip-rect": [0, 0, 80, 80]
+ type: rect
+ color: black
+ -
+ bounds: [0, 100, 100, 100]
+ "clip-rect": [0, 100, 100, 100]
+ clip-chain: [2]
+ type: rect
+ color: blue
+ -
+ bounds: [100, 0, 100, 100]
+ "clip-rect": [100, 0, 100, 100]
+ clip-chain: [2]
+ type: rect
+ color: green
+ -
+ bounds: [100, 100, 100, 100]
+ "clip-rect": [100, 100, 100, 100]
+ clip-chain: [2]
+ type: rect
+ color: red
+
diff --git a/gfx/wr/wrench/reftests/transforms/snapped-preserve-3d-ref.yaml b/gfx/wr/wrench/reftests/transforms/snapped-preserve-3d-ref.yaml
new file mode 100644
index 0000000000..a2e8972981
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/snapped-preserve-3d-ref.yaml
@@ -0,0 +1,19 @@
+---
+root:
+ items:
+ -
+ type: "stacking-context"
+ transform: translate(10, 10.5, 0)
+ items:
+ -
+ type: "stacking-context"
+ transform: translate(10, 10, 0)
+ items:
+ -
+ bounds: [10, 10.5, 200, 1]
+ type: rect
+ color: red
+ -
+ bounds: [10, 0, 200, 1]
+ type: rect
+ color: red
diff --git a/gfx/wr/wrench/reftests/transforms/snapped-preserve-3d.yaml b/gfx/wr/wrench/reftests/transforms/snapped-preserve-3d.yaml
new file mode 100644
index 0000000000..cd2cd8b1a7
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/snapped-preserve-3d.yaml
@@ -0,0 +1,21 @@
+---
+root:
+ items:
+ -
+ type: "stacking-context"
+ transform: translate(10, 10.5, 0)
+ "transform-style": "preserve-3d"
+ items:
+ -
+ type: "stacking-context"
+ transform: translate(10, 10, 0)
+ "transform-style": "preserve-3d"
+ items:
+ -
+ bounds: [10, 10.5, 200, 1]
+ type: rect
+ color: red
+ -
+ bounds: [10, 0, 200, 1]
+ type: rect
+ color: red
diff --git a/gfx/wr/wrench/reftests/transforms/strange-w-ref.yaml b/gfx/wr/wrench/reftests/transforms/strange-w-ref.yaml
new file mode 100644
index 0000000000..be50589d2a
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/strange-w-ref.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [50, 50, 600, 600]
+ perspective: 20
+ items:
+ - type: stacking-context
+ items:
+ - type: rect
+ bounds: [25, 25, 50, 50]
+ color: red
diff --git a/gfx/wr/wrench/reftests/transforms/strange-w.yaml b/gfx/wr/wrench/reftests/transforms/strange-w.yaml
new file mode 100644
index 0000000000..8e2a71b197
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/strange-w.yaml
@@ -0,0 +1,15 @@
+# don't assume w=1 when checking if the transform is just a translation
+
+---
+root:
+ items:
+ - type: stacking-context
+ bounds: [50, 50, 600, 600]
+ perspective: 20
+ items:
+ - type: stacking-context
+ transform: [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,2]
+ items:
+ - type: rect
+ bounds: [50, 50, 100, 100]
+ color: red
diff --git a/gfx/wr/wrench/res/wrench.exe.manifest b/gfx/wr/wrench/res/wrench.exe.manifest
new file mode 100644
index 0000000000..223d04d93f
--- /dev/null
+++ b/gfx/wr/wrench/res/wrench.exe.manifest
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1"
+ manifestVersion="1.0"
+ xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
+ <assemblyIdentity type="win32"
+ name="webrender.Wrench"
+ version="0.1.0.0"/>
+
+ <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+ <application>
+ <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> <!-- Windows 7 -->
+ <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> <!-- Windows 8 -->
+ <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> <!-- Windows 8.1 -->
+ <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> <!-- Windows 10 -->
+ </application>
+ </compatibility>
+
+ <asmv3:application>
+ <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
+ <dpiAware>true</dpiAware>
+ </asmv3:windowsSettings>
+ </asmv3:application>
+</assembly>
+
diff --git a/gfx/wr/wrench/script/benchmark_server.py b/gfx/wr/wrench/script/benchmark_server.py
new file mode 100644
index 0000000000..cb40388130
--- /dev/null
+++ b/gfx/wr/wrench/script/benchmark_server.py
@@ -0,0 +1,59 @@
+# 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/.
+
+from __future__ import print_function
+import json
+import os
+import subprocess
+import time
+import urllib2
+
+FILE = 'perf.json'
+URL = 'https://wrperf.org/submit'
+
+while True:
+ try:
+ # Remove any previous results
+ try:
+ os.remove(FILE)
+ except Exception:
+ pass
+
+ # Pull latest code
+ subprocess.call(["git", "pull"])
+
+ # Get the git revision of this build
+ revision = subprocess.check_output(["git", "rev-parse", "HEAD"]).strip()
+
+ # Build
+ subprocess.call(["cargo", "build", "--release"])
+
+ # Run benchmarks
+ env = os.environ.copy()
+ # Ensure that vsync is disabled, to get meaningful 'composite' times.
+ env['vblank_mode'] = '0'
+ subprocess.call(["cargo", "run", "--release", "--", "perf", FILE], env=env)
+
+ # Read the results
+ with open(FILE) as file:
+ results = json.load(file)
+
+ # Post the results to server
+ payload = {
+ 'key': env['WEBRENDER_PERF_KEY'],
+ 'revision': revision,
+ 'timestamp': str(time.time()),
+ 'tests': results['tests'],
+ }
+
+ req = urllib2.Request(URL,
+ headers={"Content-Type": "application/json"},
+ data=json.dumps(payload))
+
+ f = urllib2.urlopen(req)
+ except Exception as e:
+ print(e)
+
+ # Delay a bit until next benchmark
+ time.sleep(60 * 60)
diff --git a/gfx/wr/wrench/script/gen-many-images.py b/gfx/wr/wrench/script/gen-many-images.py
new file mode 100644
index 0000000000..bd97ad05c2
--- /dev/null
+++ b/gfx/wr/wrench/script/gen-many-images.py
@@ -0,0 +1,15 @@
+# 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/.
+
+SIZE = 8
+
+with open("../benchmarks/many-images.yaml", "w") as text_file:
+ text_file.write("root:\n")
+ text_file.write(" items:\n")
+ for y in range(0, 64):
+ yb = SIZE * y
+ for x in range(0, 128):
+ xb = SIZE * x
+ text_file.write(" - image: solid-color({0}, {1}, 0, 255, {2}, {2})\n".format(x, y, SIZE))
+ text_file.write(" bounds: {0} {1} {2} {2}\n".format(xb, yb, SIZE))
diff --git a/gfx/wr/wrench/script/headless.py b/gfx/wr/wrench/script/headless.py
new file mode 100755
index 0000000000..109fca1cb2
--- /dev/null
+++ b/gfx/wr/wrench/script/headless.py
@@ -0,0 +1,152 @@
+#!/usr/bin/python
+
+# 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/.
+
+# Build and run wrench with off-screen software rendering (OSMesa/LLVMpipe)
+# for platform-independent results. This is good for running reference tests.
+#
+# Usage: headless.py ARGS
+#
+# Pass ARGS through to wrench, after '--headless' and '--no-scissor'.
+#
+# Environment variables:
+#
+# WRENCH_HEADLESS_TARGET: If set, don't rebuild wrench. Instead, the value should
+# be the path to an already-built cargo 'target' directory. This is useful
+# for running a cross-compiled wrench.
+#
+# CARGOFLAGS: Extra flags to be passed to 'cargo build'. Split on whitespace.
+#
+# OPTIMIZED: This script uses the release build by default, but if this variable
+# is set to '0' or 'false', the script uses the debug build.
+#
+# DEBUGGER: If set, run wrench under a debugger. Permitted values are 'rr' (run
+# under 'rr record'), or 'gdb', 'rust-gdb', or 'cgdb' (run under the given
+# debugger, and arrange to supply ARGS to the wrench debuggee, not the
+# debugger)
+
+from __future__ import print_function
+import contextlib
+import os
+import subprocess
+import sys
+from os import path
+from glob import glob
+
+
+@contextlib.contextmanager
+def cd(new_path):
+ """Context manager for changing the current working directory"""
+ previous_path = os.getcwd()
+ try:
+ os.chdir(new_path)
+ yield
+ finally:
+ os.chdir(previous_path)
+
+
+def find_dep_path_newest(package, bin_path):
+ deps_path = path.join(path.split(bin_path)[0], "build")
+ with cd(deps_path):
+ candidates = glob(package + '-*')
+ candidates = (path.join(deps_path, c) for c in candidates)
+ """ For some reason cargo can create an extra osmesa-src without libs """
+ candidates = (c for c in candidates if path.exists(path.join(c, 'out')))
+ candidate_times = sorted(((path.getmtime(c), c) for c in candidates), reverse=True)
+ if len(candidate_times) > 0:
+ return candidate_times[0][1]
+ return None
+
+
+def is_windows():
+ """ Detect windows, mingw, cygwin """
+ return sys.platform == 'win32' or sys.platform == 'msys' or sys.platform == 'cygwin'
+
+
+def is_macos():
+ return sys.platform == 'darwin'
+
+
+def is_linux():
+ return sys.platform.startswith('linux')
+
+
+def debugger():
+ if "DEBUGGER" in os.environ:
+ return os.environ["DEBUGGER"]
+ return None
+
+
+def use_gdb():
+ return debugger() in ['gdb', 'cgdb', 'rust-gdb']
+
+
+def use_rr():
+ return debugger() == 'rr'
+
+
+def optimized_build():
+ if "OPTIMIZED" in os.environ:
+ opt = os.environ["OPTIMIZED"]
+ return opt not in ["0", "false"]
+ return True
+
+
+def set_osmesa_env(bin_path):
+ """Set proper LD_LIBRARY_PATH and DRIVE for software rendering on Linux and OSX"""
+ base = find_dep_path_newest('osmesa-src', bin_path)
+ osmesa_path = path.join(base, "out", "mesa", "src", "gallium", "targets", "osmesa")
+ os.environ["GALLIUM_DRIVER"] = "llvmpipe"
+ if is_linux():
+ print(osmesa_path)
+ os.environ["LD_LIBRARY_PATH"] = osmesa_path
+ elif is_macos():
+ osmesa_path = path.join(base, "out", "mesa", "src", "gallium", "targets", "osmesa")
+ glapi_path = path.join(base, "out", "mesa", "src", "mapi", "shared-glapi")
+ os.environ["DYLD_LIBRARY_PATH"] = osmesa_path + ":" + glapi_path
+
+
+extra_flags = os.getenv('CARGOFLAGS', None)
+extra_flags = extra_flags.split(' ') if extra_flags else []
+
+wrench_headless_target = os.getenv('WRENCH_HEADLESS_TARGET', None)
+
+if wrench_headless_target:
+ target_folder = wrench_headless_target
+else:
+ target_folder = '../target/'
+
+if optimized_build():
+ target_folder += 'release/'
+else:
+ target_folder += 'debug/'
+
+# For CI purposes, don't build if WRENCH_HEADLESS_TARGET is set.
+# This environment variable is used to point to the location of a cross-compiled
+# wrench for the CI on some platforms.
+if not wrench_headless_target:
+ build_cmd = ['cargo', 'build'] + extra_flags + ['--verbose', '--features', 'headless']
+ if optimized_build():
+ build_cmd += ['--release']
+ subprocess.check_call(build_cmd)
+
+dbg_cmd = []
+if use_rr():
+ dbg_cmd = ['rr', 'record']
+elif use_gdb():
+ dbg_cmd = [debugger(), '--args']
+elif debugger():
+ print("Unknown debugger: " + debugger())
+ sys.exit(1)
+
+set_osmesa_env(target_folder)
+# TODO(gw): We have an occasional accuracy issue or bug (could be WR or OSMesa)
+# where the output of a previous test that uses intermediate targets can
+# cause 1.0 / 255.0 pixel differences in a subsequent test. For now, we
+# run tests with no-scissor mode, which ensures a complete target clear
+# between test runs. But we should investigate this further...
+cmd = dbg_cmd + [target_folder + 'wrench', '--no-scissor', '--headless'] + sys.argv[1:]
+print('Running: `' + ' '.join(cmd) + '`')
+subprocess.check_call(cmd, stderr=subprocess.STDOUT)
diff --git a/gfx/wr/wrench/script/reftest-analyzer.xhtml b/gfx/wr/wrench/script/reftest-analyzer.xhtml
new file mode 100644
index 0000000000..9fad3544b3
--- /dev/null
+++ b/gfx/wr/wrench/script/reftest-analyzer.xhtml
@@ -0,0 +1,857 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- -*- Mode: HTML; tab-width: 2; indent-tabs-mode: nil; -*- -->
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent expandtab: -->
+<!-- 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/. -->
+<!--
+
+Features to add:
+* make the left and right parts of the viewer independently scrollable
+* make the test list filterable
+** default to only showing unexpecteds
+* add other ways to highlight differences other than circling?
+* add zoom/pan to images
+* Add ability to load log via XMLHttpRequest (also triggered via URL param)
+* color the test list based on pass/fail and expected/unexpected/random/skip
+* ability to load multiple logs ?
+** rename them by clicking on the name and editing
+** turn the test list into a collapsing tree view
+** move log loading into popup from viewer UI
+
+-->
+<!DOCTYPE html>
+<html lang="en-US" xml:lang="en-US" xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Reftest analyzer</title>
+ <style type="text/css"><![CDATA[
+
+ html, body { margin: 0; }
+ html { padding: 0; }
+ body { padding: 4px; }
+
+ #pixelarea, #itemlist, #images { position: absolute; }
+ #itemlist, #images { overflow: auto; }
+ #pixelarea { top: 0; left: 0; width: 320px; height: 84px; overflow: visible }
+ #itemlist { top: 84px; left: 0; width: 320px; bottom: 0; }
+ #images { top: 0; bottom: 0; left: 320px; right: 0; }
+
+ #leftpane { width: 320px; }
+ #images { position: fixed; top: 10px; left: 340px; }
+
+ form#imgcontrols { margin: 0; display: block; }
+
+ #itemlist > table { border-collapse: collapse; }
+ #itemlist > table > tbody > tr > td { border: 1px solid; padding: 1px; }
+ #itemlist td.activeitem { background-color: yellow; }
+
+ /*
+ #itemlist > table > tbody > tr.pass > td.url { background: lime; }
+ #itemlist > table > tbody > tr.fail > td.url { background: red; }
+ */
+
+ #magnification > svg { display: block; width: 84px; height: 84px; }
+
+ #pixelinfo { font: small sans-serif; position: absolute; width: 200px; left: 84px; }
+ #pixelinfo table { border-collapse: collapse; }
+ #pixelinfo table th { white-space: nowrap; text-align: left; padding: 0; }
+ #pixelinfo table td { font-family: monospace; padding: 0 0 0 0.25em; }
+
+ #pixelhint { display: inline; color: #88f; cursor: help; }
+ #pixelhint > * { display: none; position: absolute; margin: 8px 0 0 8px; padding: 4px; width: 400px; background: #ffa; color: black; box-shadow: 3px 3px 2px #888; z-index: 1; }
+ #pixelhint:hover { color: #000; }
+ #pixelhint:hover > * { display: block; }
+ #pixelhint p { margin: 0; }
+ #pixelhint p + p { margin-top: 1em; }
+
+ ]]></style>
+ <script type="text/javascript"><![CDATA[
+
+let heatmapCanvas = null;
+let heatmapUMouse;
+let gl = null;
+
+function heatmap_render_setup(canvas) {
+ gl = canvas.getContext('webgl', {antialias: false, depth: false, preserveDrawingBuffer:false});
+
+ const vertices = [
+ 0, 0,
+ 1, 0,
+ 0, 1,
+ 1, 1,
+ ];
+
+ const vertexBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
+
+ const vsCode =
+ `
+ attribute vec2 a_vertCoord;
+ varying vec2 v_texCoord;
+ void main(void) {
+ gl_Position = vec4(2.0 * a_vertCoord - 1.0, 0.0, 1.0);
+ v_texCoord = a_vertCoord;
+ }`;
+
+ const VS = gl.createShader(gl.VERTEX_SHADER);
+ gl.shaderSource(VS, vsCode);
+ gl.compileShader(VS);
+
+ const psCode =
+ `
+ precision mediump float;
+ uniform vec2 heatmapUMouse;
+ varying vec2 v_texCoord;
+ uniform sampler2D u_image1, u_image2;
+ void main(void) {
+ vec2 dxy = abs(heatmapUMouse - gl_FragCoord.xy);
+ if(dxy.x < 1.0 || dxy.y < 1.0) { // crosshair
+ gl_FragColor = vec4( 1.0, 1.0, 0.5, 1.0 );
+ return;
+ }
+
+ vec3 img1 = texture2D(u_image1, v_texCoord).rgb;
+ vec3 img2 = texture2D(u_image2, v_texCoord).rgb;
+
+ bool is_top = gl_FragCoord.y > float(heatmapUMouse.y);
+ bool is_left = gl_FragCoord.x < float(heatmapUMouse.x);
+
+ vec3 rgb;
+ if(is_top) {
+ if(is_left) {
+ rgb = img1;
+ } else {
+ rgb = img2;
+ }
+ } else {
+ vec3 diff = abs(img1 - img2);
+ if(is_left) {
+ rgb = diff;
+ } else {
+ float max_diff = max(diff.r, max(diff.g, diff.b));
+ if(max_diff == 0.0) {
+ rgb = vec3(0.0, 0.0, 0.2);
+ } else {
+ // some arbitrary colorization -- transition from green to red
+ // with some contrast tweaks to make red stand out a bit more
+ // at about 0.5'ish
+ rgb = vec3( pow(max_diff, 0.5), pow(1.0 - max_diff, 3.0), 0.0 );
+ }
+ }
+ }
+
+ gl_FragColor = vec4( rgb, 1.0 );
+ }`;
+
+ const FS = gl.createShader(gl.FRAGMENT_SHADER);
+ gl.shaderSource(FS, psCode);
+ gl.compileShader(FS);
+
+ const program = gl.createProgram();
+ gl.attachShader(program, VS);
+ gl.attachShader(program, FS);
+ gl.linkProgram(program);
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
+ console.error('Link failed: ' + gl.getProgramInfoLog(program));
+ console.error('vs info-log: ' + gl.getShaderInfoLog(VS));
+ console.error('fs info-log: ' + gl.getShaderInfoLog(FS));
+ return; // don't assign heatmapCanvas
+ }
+ gl.useProgram(program);
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
+
+ const coord = gl.getAttribLocation(program, "a_vertCoord");
+ gl.vertexAttribPointer(coord, 2, gl.FLOAT, false, 0, 0);
+ gl.enableVertexAttribArray(coord);
+
+ heatmapUMouse = gl.getUniformLocation(program, "heatmapUMouse");
+
+ gl.uniform1i(gl.getUniformLocation(program, 'u_image1'), 0);
+ gl.uniform1i(gl.getUniformLocation(program, 'u_image2'), 1);
+
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
+ heatmapCanvas = canvas;
+}
+
+function heatmap_change_image(index, image) {
+ if (heatmapCanvas === null) {
+ return;
+ }
+ const texture = gl.createTexture();
+ gl.activeTexture(gl.TEXTURE0 + index);
+ gl.bindTexture (gl.TEXTURE_2D, texture);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
+ gl.texImage2D (gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
+}
+
+function heatmap_render(mouse_x, mouse_y) {
+ if (heatmapCanvas === null) {
+ return;
+ }
+
+ gl.uniform2f(heatmapUMouse, mouse_x, mouse_y);
+
+ // the canvas resizes as user selects different reftests
+ gl.viewport(0, 0, heatmapCanvas.width, heatmapCanvas.height);
+
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+}
+
+function heatmap_on_mousemove(mousemove_event) {
+ if (heatmapCanvas === null) {
+ return;
+ }
+ const rect = heatmapCanvas.getBoundingClientRect();
+ let x = mousemove_event.clientX - rect.left;
+ let y = mousemove_event.clientY - rect.top;
+ x = x * heatmapCanvas.width / heatmapCanvas.clientWidth;
+ y = y * heatmapCanvas.height / heatmapCanvas.clientHeight;
+
+ // mouse has Y == 0 at the top, GL has it at the bottom:
+ const flip_y = heatmapCanvas.height-1 - y;
+ heatmap_render(x, flip_y);
+
+ return { x:x, y:y };
+}
+
+ ]]></script>
+
+ <script type="text/javascript"><![CDATA[
+
+var XLINK_NS = "http://www.w3.org/1999/xlink";
+var SVG_NS = "http://www.w3.org/2000/svg";
+var IMAGE_NOT_AVAILABLE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKAAAAASCAYAAADczdVTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAHy0lEQVRoge2aX2hb5xnGf2dYabROgqQkpMuKnWUJLmxHMFaa/SscteQiF5EvUgqLctEVrDJKK1+MolzkQr4IctgW+SLIheJc1BpFpswJw92FbaZsTCGTL0465AtntUekJdJ8lByVHbnnwLsLKbKdSJbiZBVjeuAYn+/P+z3fc97vfd9zbEVEhB566BK+1m0CPfx/o+eAPXQVbR3QqVapOl8FlR46h0O1Wu02iacCZfsasMKEz8vbx1JYE6fY/dXx6mEbFObPcvDVDBlznpc9G+2r8xNcvLqK2w39r4UI+fs7tFjmytgFFu718865EIebPGincI3zFz7Bcrtx97/GL0P+p+IPbSOgRwXtW3vpewqL/a/g5rgf39hit2m0hGUAHOHrrq3trmef4/lDB7Ay57n01zuPZXPX7jUunv+Yf9ktR7D/0CHca7/n3KXPsHbAuynkCWCZptgiImKLaVqP9NuW1bT9ceybpr3j+WJbYrVa3rbEatGZi2uixvWdrysilmWKae2M+5PqlktoosayLfubcrN10dAk24aynUsIxMVsadwUs+EX7dEyAlaXLqMoCj6fj5HkUqO9MD+Govjx+xXcXi+uoRAhvwuv182Z8Ws4AJUlxoZ8uNxuvF43ii/EtdXNNUuV68lR/IqC4gsxPj7KkE/BF5qmClRXrzFSt+/1ulDOjLNU6eQ4OcyPDqH4hhg5O4LicuN2K4xcvk6jjHUKJM8O1fvcKMoZkouFOq1VPp1OcuXGAvrvfsv0lWmSySTzN0sdH+jyYhK/ouB2e/G6XfjPJikBVG8SUhT8fl99nwVGfQp+vx+f4iO5VO1AtwJjfgXF58M/kqSVJP9ef0xuAI6NlwWmL41xxqeg+PyMXr72yBqW3cI4JaZHh1DcXrxeLy5liORiB7q1PiZFyeV0mQqz9TRZeUmFVUGLSjqdkgCIFp2RTCosEJOiiIihSyKWkDl9WYrFnCQCCNF0w0QmHhBQJTEzJ+nZSQmAoEYks2KIGBkJgASiM5I3LbGMnCSCCEQl38GJMvMZiag1e+nlFcmmIgKaZEwREaPGhWGZ1VfEMFZkNj4sgCSyhoihSzwSlqCGoAUlEo1IJByW+Oxyh+dZJJ+eklhiRnIrRcnrM6KCxLOmiNiipyICSGR2pTY2O1m7T2XEsNrrJmJLfjkn6amwoMbFaMEhG28eAVtzExErW3sOBCWVzkpmNiEqCOEZ2RyLTT3eJAKaMhVEUMOSXjHEtg3JTIUFkNTK9rGwbQrWm2xGb6QoWxIqEtdtEWO28aDtoi6JSFCAjUtL1AUzJA4SSW/IZ2VjjU0V0zEBJBiJSzwWk1g8IZEAAmrdidrBkoSKxB4IW08tGVNEzIxoIJM5a8v4SQ1RY5lGSy6x8xScz6QkHFBre1Zre49nH+y1KDEQLV7TcyU1LBCtHVppp9smxk2dYAMtHXA7blZWNJDZ4sZ4MxPbdHjrbc3WNuvOq4YlkYhLLBaXeKx2sLcrBUS2ScFtUbUBh3WgajvgOYgGuKjw4Rsqb1uvkssbWLbJXFQFqL/I9IEKa2WzYcqy16E2BNteB1R+cuwoRwcHGRx4nlfenWMuPclRDx3goSraqd+7Gj/Y5d76SrXLu3VKLYW1rMZbo/QpB4+9zt6fT1I0Law/LRMBaLzC7ePNuSgL7/2GpcotLr7+AZG5t9gH0Fa3zuFq1tiWG4DKs5tebV1NDDW1XYd26iWO9A8wODjAUfUN5ubm+Ch4ZFuuLRzQoVwqUCqXyN9fg3tFSuUShVIZhyr5O2vo94o42DwD/PP23fq8Bf5urLO+BoHBwxzc20c++wcmz+lAkWLFATwcf3+YDwIDhMYmuDw+wt5j5+C5ZwDYP/gSoLP6xX5+fOIkJ47/lIP8g49/Nc3tDj59OZUiRR3uFYsAVO/eZoE1yvkyeA6gAaff+zU3SxUcp8LilQucnoFTP3hhix19/garlQqFW9eZOBti9Mqt9mubXwBw+NALeDC4cfVDzgP3i3keUN/nf4uo+hEver/DRaK84/9mY/72uoFTKVMolVn5/HPgPvlSmVKhRL2bSrlEqVyidH8N/d7t2u/lakfcKneLgM4rvxhncbXA6tI8kTffB+0NjnrAqZYplcrk83ceXdtzgB+psHD7S/pfPs7JkydQB1x8dnWS2SVje9GaxkVLl+DmNNC4NJn/S6JxH5nJyNRwrW7Qi7oMgxBMyd9molvmRKO1cExgshG6l9NTEhkOynAkLlOJoKBuhPV8ZlK0h9aNTqVbv3ltEK/VIiAQEN0yZVLbuM+aImLoEgts3VdsJrfFil1M1/ZSv9RAROaWO8n/hkyF1Q3bgeFGygvPrDRG5Wcf1IJbq9rlNrrNbra96aqlUVMSWrNnNiw5uw23T/4o4Xq7FtA29h2My3K9WtETgRZr13UxdIk+pGswkpCcsX0N2OZD9BOgWqFsgWePp20KWb0ywkDgEIa8y55Gq0O5XKHP7cGz++l/haxWylgOuD17aG7eoVpxwL27RX8b27jZ42n1qdahXKrg2bfnUW0eQ7edoD232l+/LPp2pHvNfh8eT2f8/3sO2AZLyRAvns6gqToLOgxP6Uz87HvdoNJDF9E1B6ysLrLw5yW+3PUNvv3dH/L9wX3doNFDl9E1B+yhB+j9O1YPXcZ/AAl9BWJNvZE7AAAAAElFTkSuQmCC";
+
+var gPhases = null;
+
+var gIDCache = {};
+
+var gMagPixPaths = []; // 2D array of array-of-two <path> objects used in the pixel magnifier
+var gMagWidth = 5; // number of zoomed in pixels to show horizontally
+var gMagHeight = 5; // number of zoomed in pixels to show vertically
+var gMagZoom = 16; // size of the zoomed in pixels
+var gImage1Data; // ImageData object for the reference image
+var gImage2Data; // ImageData object for the test output image
+var gFlashingPixels = []; // array of <path> objects that should be flashed due to pixel color mismatch
+var gParams;
+
+function ID(id) {
+ if (!(id in gIDCache))
+ gIDCache[id] = document.getElementById(id);
+ return gIDCache[id];
+}
+
+function hash_parameters() {
+ var result = { };
+ var params = window.location.hash.substr(1).split(/[&;]/);
+ for (var i = 0; i < params.length; i++) {
+ var parts = params[i].split("=");
+ result[parts[0]] = unescape(unescape(parts[1]));
+ }
+ return result;
+}
+
+function load() {
+ gPhases = [ ID("entry"), ID("loading"), ID("viewer") ];
+ build_mag();
+ gParams = hash_parameters();
+ if (gParams.log) {
+ show_phase("loading");
+ process_log(gParams.log);
+ } else if (gParams.logurl) {
+ show_phase("loading");
+ var req = new XMLHttpRequest();
+ req.onreadystatechange = function() {
+ if (req.readyState === 4) {
+ process_log(req.responseText);
+ }
+ };
+ req.open('GET', gParams.logurl, true);
+ req.send();
+ }
+ window.addEventListener('keypress', handle_keyboard_shortcut);
+ ID("image1").addEventListener('error', image_load_error);
+ ID("image2").addEventListener('error', image_load_error);
+}
+
+function image_load_error(e) {
+ e.target.setAttributeNS(XLINK_NS, "xlink:href", IMAGE_NOT_AVAILABLE);
+}
+
+function build_mag() {
+ var mag = ID("mag");
+
+ var r = document.createElementNS(SVG_NS, "rect");
+ r.setAttribute("x", gMagZoom * -gMagWidth / 2);
+ r.setAttribute("y", gMagZoom * -gMagHeight / 2);
+ r.setAttribute("width", gMagZoom * gMagWidth);
+ r.setAttribute("height", gMagZoom * gMagHeight);
+ mag.appendChild(r);
+
+ mag.setAttribute("transform", "translate(" + (gMagZoom * (gMagWidth / 2) + 1) + "," + (gMagZoom * (gMagHeight / 2) + 1) + ")");
+
+ for (var x = 0; x < gMagWidth; x++) {
+ gMagPixPaths[x] = [];
+ for (var y = 0; y < gMagHeight; y++) {
+ var p1 = document.createElementNS(SVG_NS, "path");
+ p1.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "h" + -gMagZoom + "v" + gMagZoom);
+ p1.setAttribute("stroke", "black");
+ p1.setAttribute("stroke-width", "1px");
+ p1.setAttribute("fill", "#aaa");
+
+ var p2 = document.createElementNS(SVG_NS, "path");
+ p2.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "v" + gMagZoom + "h" + -gMagZoom);
+ p2.setAttribute("stroke", "black");
+ p2.setAttribute("stroke-width", "1px");
+ p2.setAttribute("fill", "#888");
+
+ mag.appendChild(p1);
+ mag.appendChild(p2);
+ gMagPixPaths[x][y] = [p1, p2];
+ }
+ }
+
+ var flashedOn = false;
+ setInterval(function() {
+ flashedOn = !flashedOn;
+ flash_pixels(flashedOn);
+ }, 500);
+}
+
+function show_phase(phaseid) {
+ for (var i in gPhases) {
+ var phase = gPhases[i];
+ phase.style.display = (phase.id == phaseid) ? "" : "none";
+ }
+
+ if (phase == "viewer")
+ ID("images").style.display = "none";
+}
+
+function fileentry_changed() {
+ show_phase("loading");
+ var input = ID("fileentry");
+ var files = input.files;
+ if (files.length > 0) {
+ // Only handle the first file; don't handle multiple selection.
+ // The parts of the log we care about are ASCII-only. Since we
+ // can ignore lines we don't care about, best to read in as
+ // iso-8859-1, which guarantees we don't get decoding errors.
+ var fileReader = new FileReader();
+ fileReader.onload = function(e) {
+ var log = null;
+
+ log = e.target.result;
+
+ if (log)
+ process_log(log);
+ else
+ show_phase("entry");
+ }
+ fileReader.readAsText(files[0], "iso-8859-1");
+ }
+ // So the user can process the same filename again (after
+ // overwriting the log), clear the value on the form input so we
+ // will always get an onchange event.
+ input.value = "";
+}
+
+function log_pasted() {
+ show_phase("loading");
+ var entry = ID("logentry");
+ var log = entry.value;
+ entry.value = "";
+ process_log(log);
+}
+
+var gTestItems;
+
+function process_log(contents) {
+ var lines = contents.split(/[\r\n]+/);
+ gTestItems = [];
+ for (var j in lines) {
+ var line = lines[j];
+ // Ignore duplicated output in logcat.
+ if (line.match(/I\/Gecko.*?REFTEST/))
+ continue;
+ var match = line.match(/^(?:.*? (?:INFO|ERROR) -\s+)?(?:REFTEST\s+)?(.*)$/);
+ if (!match)
+ continue;
+ line = match[1];
+ match = line.match(/^(TEST-PASS|TEST-UNEXPECTED-PASS|TEST-KNOWN-FAIL|TEST-UNEXPECTED-FAIL|TEST-DEBUG-INFO)(\(EXPECTED RANDOM\)|) \| ([^\|]+) \|(.*)/);
+ if (match) {
+ var state = match[1];
+ var random = match[2];
+ var url = match[3];
+ var extra = match[4];
+ gTestItems.push(
+ {
+ pass: !state.match(/DEBUG-INFO$|FAIL$/),
+ // only one of the following three should ever be true
+ unexpected: !!state.match(/^TEST-UNEXPECTED/),
+ random: (random == "(EXPECTED RANDOM)"),
+ skip: (extra == " (SKIP)"),
+ url: url,
+ images: [],
+ imageLabels: []
+ });
+ continue;
+ }
+ match = line.match(/IMAGE([^:]*): (data:.*)$/);
+ if (match) {
+ var item = gTestItems[gTestItems.length - 1];
+ item.images.push(match[2]);
+ item.imageLabels.push(match[1]);
+ }
+ }
+
+ build_viewer();
+}
+
+function build_viewer() {
+ if (gTestItems.length == 0) {
+ show_phase("entry");
+ return;
+ }
+
+ var cell = ID("itemlist");
+ while (cell.childNodes.length > 0)
+ cell.removeChild(cell.childNodes[cell.childNodes.length - 1]);
+
+ var table = document.createElement("table");
+ var tbody = document.createElement("tbody");
+ table.appendChild(tbody);
+
+ for (var i in gTestItems) {
+ var item = gTestItems[i];
+
+ // optional url filter for only showing unexpected results
+ if (parseInt(gParams.only_show_unexpected) && !item.unexpected)
+ continue;
+
+ // XXX regardless skip expected pass items until we have filtering UI
+ if (item.pass && !item.unexpected)
+ continue;
+
+ var tr = document.createElement("tr");
+ var rowclass = item.pass ? "pass" : "fail";
+ var td;
+ var text;
+
+ td = document.createElement("td");
+ text = "";
+ if (item.unexpected) { text += "!"; rowclass += " unexpected"; }
+ if (item.random) { text += "R"; rowclass += " random"; }
+ if (item.skip) { text += "S"; rowclass += " skip"; }
+ td.appendChild(document.createTextNode(text));
+ tr.appendChild(td);
+
+ td = document.createElement("td");
+ td.id = "item" + i;
+ td.className = "url";
+ // Only display part of URL after "/mozilla/".
+ var match = item.url.match(/\/mozilla\/(.*)/);
+ text = document.createTextNode(match ? match[1] : item.url);
+ if (item.images.length > 0) {
+ var a = document.createElement("a");
+ a.href = "javascript:show_images(" + i + ")";
+ a.appendChild(text);
+ td.appendChild(a);
+ } else {
+ td.appendChild(text);
+ }
+ tr.appendChild(td);
+
+ tbody.appendChild(tr);
+ }
+
+ cell.appendChild(table);
+
+ show_phase("viewer");
+}
+
+function get_image_data(src, whenReady) {
+ var img = new Image();
+ img.onload = function() {
+ var canvas = document.createElement("canvas");
+ canvas.width = img.naturalWidth;
+ canvas.height = img.naturalHeight;
+
+ var ctx = canvas.getContext("2d");
+ ctx.drawImage(img, 0, 0);
+
+ whenReady(ctx.getImageData(0, 0, img.naturalWidth, img.naturalHeight));
+ };
+ img.src = src;
+}
+
+function sync_svg_size(imageData) {
+ // We need the size of the 'svg' and its 'image' elements to match the size
+ // of the ImageData objects that we're going to read pixels from or else our
+ // magnify() function will be very broken.
+ ID("svg").setAttribute("width", imageData.width);
+ ID("svg").setAttribute("height", imageData.height);
+}
+
+function sync_heatmap_size(imageData) {
+ ID("heat_canvas").setAttribute("width" , imageData.width);
+ ID("heat_canvas").setAttribute("height", imageData.height);
+}
+
+function show_images(i) {
+ var item = gTestItems[i];
+ var cell = ID("images");
+
+ // Remove activeitem class from any existing elements
+ var activeItems = document.querySelectorAll(".activeitem");
+ for (var activeItemIdx = activeItems.length; activeItemIdx-- != 0;) {
+ activeItems[activeItemIdx].classList.remove("activeitem");
+ }
+
+ ID("item" + i).classList.add("activeitem");
+ ID("image1").style.display = "";
+ ID("image2").style.display = "none";
+ show_diff_none();
+ ID("imgcontrols").reset();
+ ID("diffcontrols").reset();
+
+ ID("image1").setAttributeNS(XLINK_NS, "xlink:href", item.images[0]);
+ // Making the href be #image1 doesn't seem to work
+ ID("feimage1").setAttributeNS(XLINK_NS, "xlink:href", item.images[0]);
+ if (item.images.length == 1) {
+ ID("imgcontrols").style.display = "none";
+ } else {
+ ID("imgcontrols").style.display = "";
+
+ ID("image2").setAttributeNS(XLINK_NS, "xlink:href", item.images[1]);
+ // Making the href be #image2 doesn't seem to work
+ ID("feimage2").setAttributeNS(XLINK_NS, "xlink:href", item.images[1]);
+
+ ID("label1").textContent = 'Image ' + item.imageLabels[0];
+ ID("label2").textContent = 'Image ' + item.imageLabels[1];
+ }
+
+ cell.style.display = "";
+
+ get_image_data(item.images[0], function(data) { gImage1Data = data; sync_svg_size(gImage1Data); sync_heatmap_size(gImage1Data); heatmap_change_image(0, gImage1Data); });
+ get_image_data(item.images[1], function(data) { gImage2Data = data; heatmap_change_image(1, gImage2Data); });
+}
+
+function show_image(i) {
+ if (i == 1) {
+ ID("image1").style.display = "";
+ ID("image2").style.display = "none";
+ } else {
+ ID("image1").style.display = "none";
+ ID("image2").style.display = "";
+ }
+}
+
+function handle_keyboard_shortcut(event) {
+ switch (event.charCode) {
+ case 49: // "1" key
+ document.getElementById("radio1").checked = true;
+ show_image(1);
+ break;
+ case 50: // "2" key
+ document.getElementById("radio2").checked = true;
+ show_image(2);
+ break;
+ case 100: // "d" key
+ document.getElementById("radio_diff_circle").click();
+ break;
+ case 104: // "h" key
+ document.getElementById("radio_diff_heatmap").click();
+ break;
+ case 112: // "p" key
+ shift_images(-1);
+ break;
+ case 110: // "n" key
+ shift_images(1);
+ break;
+ }
+}
+
+function shift_images(dir) {
+ var activeItem = document.querySelector(".activeitem");
+ if (!activeItem) {
+ return;
+ }
+ for (var elm = activeItem; elm; elm = elm.parentElement) {
+ if (elm.tagName != "tr") {
+ continue;
+ }
+ elm = dir > 0 ? elm.nextElementSibling : elm.previousElementSibling;
+ if (elm) {
+ elm.getElementsByTagName("a")[0].click();
+ }
+ return;
+ }
+}
+
+function show_diff_none() {
+ ID("svg") .style.display = "";
+ ID("diffrect") .style.display = "none";
+ ID("heat_canvas").style.display = "none";
+}
+
+function show_diff_circle() {
+ ID("svg") .style.display = "";
+ ID("diffrect") .style.display = "";
+ ID("heat_canvas").style.display = "none";
+}
+
+function show_diff_heatmap() {
+ ID("svg") .style.display = "none";
+ ID("diffrect") .style.display = "none";
+ ID("heat_canvas").style.display = "";
+
+ if (heatmapCanvas === null) {
+ canvas = document.getElementById('heat_canvas');
+ heatmap_render_setup(canvas);
+ heatmap_change_image(0, gImage1Data);
+ heatmap_change_image(1, gImage2Data);
+ heatmap_render(0, 0);
+
+ window.addEventListener('mousemove', e => {
+ var { x: x, y: y } = heatmap_on_mousemove(e);
+ magnify_around(Math.floor(x), Math.floor(y));
+ });
+ }
+}
+
+function flash_pixels(on) {
+ var stroke = on ? "red" : "black";
+ var strokeWidth = on ? "2px" : "1px";
+ for (var i = 0; i < gFlashingPixels.length; i++) {
+ gFlashingPixels[i].setAttribute("stroke", stroke);
+ gFlashingPixels[i].setAttribute("stroke-width", strokeWidth);
+ }
+}
+
+function cursor_point(evt) {
+ var m = evt.target.getScreenCTM().inverse();
+ var p = ID("svg").createSVGPoint();
+ p.x = evt.clientX;
+ p.y = evt.clientY;
+ p = p.matrixTransform(m);
+ return { x: Math.floor(p.x), y: Math.floor(p.y) };
+}
+
+function hex2(i) {
+ return (i < 16 ? "0" : "") + i.toString(16);
+}
+
+function canvas_pixel_as_hex(data, x, y) {
+ var offset = (y * data.width + x) * 4;
+ var r = data.data[offset];
+ var g = data.data[offset + 1];
+ var b = data.data[offset + 2];
+ return "#" + hex2(r) + hex2(g) + hex2(b);
+}
+
+function hex_as_rgb(hex) {
+ return "rgb(" + [parseInt(hex.substring(1, 3), 16), parseInt(hex.substring(3, 5), 16), parseInt(hex.substring(5, 7), 16)] + ")";
+}
+
+function magnify(evt) {
+ var { x: x, y: y } = cursor_point(evt);
+ magnify_around(x, y);
+}
+
+function magnify_around(x, y) {
+ if (x < 0 || y < 0 || x >= gImage1Data.width || y >= gImage1Data.height) {
+ return;
+ }
+ var centerPixelColor1, centerPixelColor2;
+
+ var dx_lo = -Math.floor(gMagWidth / 2);
+ var dx_hi = Math.floor(gMagWidth / 2);
+ var dy_lo = -Math.floor(gMagHeight / 2);
+ var dy_hi = Math.floor(gMagHeight / 2);
+
+ flash_pixels(false);
+ gFlashingPixels = [];
+ for (var j = dy_lo; j <= dy_hi; j++) {
+ for (var i = dx_lo; i <= dx_hi; i++) {
+ var px = x + i;
+ var py = y + j;
+ var p1 = gMagPixPaths[i + dx_hi][j + dy_hi][0];
+ var p2 = gMagPixPaths[i + dx_hi][j + dy_hi][1];
+ // Here we just use the dimensions of gImage1Data since we expect test
+ // and reference to have the same dimensions.
+ if (px < 0 || py < 0 || px >= gImage1Data.width || py >= gImage1Data.height) {
+ p1.setAttribute("fill", "#aaa");
+ p2.setAttribute("fill", "#888");
+ } else {
+ var color1 = canvas_pixel_as_hex(gImage1Data, x + i, y + j);
+ var color2 = canvas_pixel_as_hex(gImage2Data, x + i, y + j);
+ p1.setAttribute("fill", color1);
+ p2.setAttribute("fill", color2);
+ if (color1 != color2) {
+ gFlashingPixels.push(p1, p2);
+ p1.parentNode.appendChild(p1);
+ p2.parentNode.appendChild(p2);
+ }
+ if (i == 0 && j == 0) {
+ centerPixelColor1 = color1;
+ centerPixelColor2 = color2;
+ }
+ }
+ }
+ }
+ flash_pixels(true);
+ show_pixelinfo(x, y, centerPixelColor1, hex_as_rgb(centerPixelColor1), centerPixelColor2, hex_as_rgb(centerPixelColor2));
+}
+
+function show_pixelinfo(x, y, pix1rgb, pix1hex, pix2rgb, pix2hex) {
+ var pixelinfo = ID("pixelinfo");
+ ID("coords").textContent = [x, y];
+ ID("pix1hex").textContent = pix1hex;
+ ID("pix1rgb").textContent = pix1rgb;
+ ID("pix2hex").textContent = pix2hex;
+ ID("pix2rgb").textContent = pix2rgb;
+}
+
+ ]]></script>
+
+</head>
+<body onload="load()">
+
+<div id="entry">
+
+<h1>Reftest analyzer: load reftest log</h1>
+
+<p>Either paste your log into this textarea:<br />
+<textarea cols="80" rows="10" id="logentry"/><br/>
+<input type="button" value="Process pasted log" onclick="log_pasted()" /></p>
+
+<p>... or load it from a file:<br/>
+<input type="file" id="fileentry" onchange="fileentry_changed()" />
+</p>
+</div>
+
+<div id="loading" style="display:none">Loading log...</div>
+
+<div id="viewer" style="display:none">
+ <div id="pixelarea">
+ <div id="pixelinfo">
+ <table>
+ <tbody>
+ <tr><th>Pixel at:</th><td colspan="2" id="coords"/></tr>
+ <tr><th>Image 1:</th><td id="pix1rgb"></td><td id="pix1hex"></td></tr>
+ <tr><th>Image 2:</th><td id="pix2rgb"></td><td id="pix2hex"></td></tr>
+ </tbody>
+ </table>
+ <div>
+ <div id="pixelhint">★
+ <div>
+ <p>Move the mouse over the reftest image on the right to show
+ magnified pixels on the left. The color information above is for
+ the pixel centered in the magnified view.</p>
+ <p>Image 1 is shown in the upper triangle of each pixel and Image 2
+ is shown in the lower triangle.</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="magnification">
+ <svg xmlns="http://www.w3.org/2000/svg" width="84" height="84" shape-rendering="optimizeSpeed">
+ <g id="mag"/>
+ </svg>
+ </div>
+ </div>
+ <div id="itemlist"></div>
+ <div id="images" style="display:none">
+ <form id="imgcontrols">
+ <input id="radio1" type="radio" name="which" value="0" onchange="show_image(1)" checked="checked" /><label id="label1" title="1" for="radio1">Image 1</label>
+ <input id="radio2" type="radio" name="which" value="1" onchange="show_image(2)" /><label id="label2" title="2" for="radio2">Image 2</label>
+ </form>
+
+ <form id="diffcontrols">
+ Differences:
+ <input id="radio_diff_none" name="diff" value="0" type="radio" onchange="show_diff_none()" checked="checked"/>
+ <label for="radio_diff_none">None</label>
+ <input id="radio_diff_circle" name="diff" value="1" type="radio" onchange="show_diff_circle()" />
+ <label for="radio_diff_circle">Circle</label>
+ <input id="radio_diff_heatmap" name="diff" value="2" type="radio" onchange="show_diff_heatmap()" />
+ <label for="radio_diff_heatmap">Heatmap</label>
+ </form>
+
+ <canvas width="800" height="1000" id="heat_canvas" style="display:none;"></canvas>
+
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="800" height="1000" id="svg">
+ <defs>
+ <!-- use sRGB to avoid loss of data -->
+ <filter id="showDifferences" x="0%" y="0%" width="100%" height="100%"
+ style="color-interpolation-filters: sRGB">
+ <feImage id="feimage1" result="img1" xlink:href="#image1" />
+ <feImage id="feimage2" result="img2" xlink:href="#image2" />
+ <!-- inv1 and inv2 are the images with RGB inverted -->
+ <feComponentTransfer result="inv1" in="img1">
+ <feFuncR type="linear" slope="-1" intercept="1" />
+ <feFuncG type="linear" slope="-1" intercept="1" />
+ <feFuncB type="linear" slope="-1" intercept="1" />
+ </feComponentTransfer>
+ <feComponentTransfer result="inv2" in="img2">
+ <feFuncR type="linear" slope="-1" intercept="1" />
+ <feFuncG type="linear" slope="-1" intercept="1" />
+ <feFuncB type="linear" slope="-1" intercept="1" />
+ </feComponentTransfer>
+ <!-- w1 will have non-white pixels anywhere that img2
+ is brighter than img1, and w2 for the reverse.
+ It would be nice not to have to go through these
+ intermediate states, but feComposite
+ type="arithmetic" can't transform the RGB channels
+ and leave the alpha channel untouched. -->
+ <feComposite result="w1" in="img1" in2="inv2" operator="arithmetic" k2="1" k3="1" />
+ <feComposite result="w2" in="img2" in2="inv1" operator="arithmetic" k2="1" k3="1" />
+ <!-- c1 will have non-black pixels anywhere that img2
+ is brighter than img1, and c2 for the reverse -->
+ <feComponentTransfer result="c1" in="w1">
+ <feFuncR type="linear" slope="-1" intercept="1" />
+ <feFuncG type="linear" slope="-1" intercept="1" />
+ <feFuncB type="linear" slope="-1" intercept="1" />
+ </feComponentTransfer>
+ <feComponentTransfer result="c2" in="w2">
+ <feFuncR type="linear" slope="-1" intercept="1" />
+ <feFuncG type="linear" slope="-1" intercept="1" />
+ <feFuncB type="linear" slope="-1" intercept="1" />
+ </feComponentTransfer>
+ <!-- c will be nonblack (and fully on) for every pixel+component where there are differences -->
+ <feComposite result="c" in="c1" in2="c2" operator="arithmetic" k2="255" k3="255" />
+ <!-- a will be opaque for every pixel with differences and transparent for all others -->
+ <feColorMatrix result="a" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0" />
+
+ <!-- a, dilated by 1 pixel -->
+ <feMorphology result="dila1" in="a" operator="dilate" radius="1" />
+ <!-- a, dilated by 2 pixels -->
+ <feMorphology result="dila2" in="dila1" operator="dilate" radius="1" />
+
+ <!-- all the pixels in the 2-pixel dilation of a but not in the 1-pixel dilation, to highlight the diffs -->
+ <feComposite result="highlight" in="dila2" in2="dila1" operator="out" />
+
+ <feFlood result="red" flood-color="red" />
+ <feComposite result="redhighlight" in="red" in2="highlight" operator="in" />
+ <feFlood result="black" flood-color="black" flood-opacity="0.5" />
+ <feMerge>
+ <feMergeNode in="black" />
+ <feMergeNode in="redhighlight" />
+ </feMerge>
+ </filter>
+ </defs>
+ <g onmousemove="magnify(evt)">
+ <image x="0" y="0" width="100%" height="100%" id="image1" />
+ <image x="0" y="0" width="100%" height="100%" id="image2" />
+ </g>
+ <rect id="diffrect" filter="url(#showDifferences)" pointer-events="none" x="0" y="0" width="100%" height="100%" />
+ </svg>
+ </div>
+</div>
+
+</body>
+</html>
diff --git a/gfx/wr/wrench/script/reftest-debugger.py b/gfx/wr/wrench/script/reftest-debugger.py
new file mode 100755
index 0000000000..e62dd704c8
--- /dev/null
+++ b/gfx/wr/wrench/script/reftest-debugger.py
@@ -0,0 +1,15 @@
+#!/usr/bin/python
+
+# 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/.
+
+from __future__ import print_function
+import subprocess
+
+with open('reftest.log', "w") as out:
+ try:
+ subprocess.check_call(['script/headless.py', 'reftest'], stdout=out)
+ print("All tests passed.")
+ except subprocess.CalledProcessError:
+ subprocess.check_call(['firefox', 'reftest-analyzer.xhtml#logurl=reftest.log'])
diff --git a/gfx/wr/wrench/script/wrench_with_renderer.py b/gfx/wr/wrench/script/wrench_with_renderer.py
new file mode 100755
index 0000000000..e2685e8476
--- /dev/null
+++ b/gfx/wr/wrench/script/wrench_with_renderer.py
@@ -0,0 +1,52 @@
+#!/usr/bin/python
+
+# 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/.
+
+import os
+import subprocess
+import sys
+
+
+def is_linux():
+ return sys.platform.startswith('linux')
+
+
+if is_linux():
+ requested_renderer = sys.argv[1]
+ renderer = "default"
+
+ if requested_renderer == 'hardware':
+ pass
+ elif requested_renderer == 'llvmpipe':
+ os.environ["LIBGL_ALWAYS_SOFTWARE"] = "1"
+ os.environ["GALLIUM_DRIVER"] = "llvmpipe"
+ elif requested_renderer == 'softpipe':
+ os.environ["LIBGL_ALWAYS_SOFTWARE"] = "1"
+ os.environ["GALLIUM_DRIVER"] = "softpipe"
+ elif requested_renderer == 'swiftshader':
+ # TODO: Set up LD_LIBRARY_PATH to SwiftShader libraries automatically.
+ renderer = 'es3'
+ else:
+ print('Unknown renderer ' + requested_renderer)
+ sys.exit(1)
+
+ cmd = [
+ 'cargo',
+ 'run',
+ '--release',
+ '--',
+ '--no-block', # Run as fast as possible, for benchmarking
+ '--no-picture-caching', # Disable picture caching, to test rasterization performance
+ '--no-subpixel-aa', # SwiftShader doesn't support dual source blending
+ '--renderer', # Select GL3/ES3 renderer API
+ renderer,
+ 'load'
+ ]
+
+ cmd += sys.argv[2:]
+ print('Running: ' + ' '.join(cmd))
+ subprocess.check_call(cmd)
+else:
+ print('This script is only supported on Linux')
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
+ }
+ }
+ }
+}