[forma] Add end to end rendering test.
Change-Id: I15fbb049701c7829de24f0af6a3cda84d418a169
Reviewed-on: https://fuchsia-review.googlesource.com/c/forma/+/689485
Reviewed-by: DragoČ™ Tiselice <dtiselice@google.com>
diff --git a/Cargo.toml b/Cargo.toml
index c34c978..6c5fa9f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,6 +6,7 @@
default-members = [
"benches",
"demo",
+ "e2e_tests",
"forma",
"gpu/conveyor-sort",
"gpu/painter",
@@ -16,6 +17,7 @@
members = [
"benches",
"demo",
+ "e2e_tests",
"forma",
"gpu/conveyor-sort",
"gpu/painter",
diff --git a/e2e_tests/Cargo.toml b/e2e_tests/Cargo.toml
new file mode 100644
index 0000000..a4adc3d
--- /dev/null
+++ b/e2e_tests/Cargo.toml
@@ -0,0 +1,19 @@
+# Copyright 2022 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+[package]
+edition = "2021"
+name = "e2e_tests"
+version = "0.1.0"
+
+[dependencies]
+forma = { path = "../forma", features = ["gpu"] }
+image = "0.23"
+wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "c226a10" }
+pollster = "0.2"
+once_cell = "1.12.0"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0.81"
+anyhow = "1.0.57"
+base64 = "0.13.0"
diff --git a/e2e_tests/expected/tests::blend_modes::Color::cpu.png b/e2e_tests/expected/tests::blend_modes::Color::cpu.png
new file mode 100644
index 0000000..ed1bbd3
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::Color::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::Color::gpu.png b/e2e_tests/expected/tests::blend_modes::Color::gpu.png
new file mode 100644
index 0000000..187ff4d
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::Color::gpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::ColorBurn::cpu.png b/e2e_tests/expected/tests::blend_modes::ColorBurn::cpu.png
new file mode 100644
index 0000000..f2aeffe
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::ColorBurn::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::ColorBurn::gpu.png b/e2e_tests/expected/tests::blend_modes::ColorBurn::gpu.png
new file mode 100644
index 0000000..3391de2
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::ColorBurn::gpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::ColorDodge::cpu.png b/e2e_tests/expected/tests::blend_modes::ColorDodge::cpu.png
new file mode 100644
index 0000000..ae63635
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::ColorDodge::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::ColorDodge::gpu.png b/e2e_tests/expected/tests::blend_modes::ColorDodge::gpu.png
new file mode 100644
index 0000000..ef5e30d
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::ColorDodge::gpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::Darken::cpu.png b/e2e_tests/expected/tests::blend_modes::Darken::cpu.png
new file mode 100644
index 0000000..c16f632
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::Darken::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::Darken::gpu.png b/e2e_tests/expected/tests::blend_modes::Darken::gpu.png
new file mode 100644
index 0000000..a89226e
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::Darken::gpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::Difference::cpu.png b/e2e_tests/expected/tests::blend_modes::Difference::cpu.png
new file mode 100644
index 0000000..ebc7c0f
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::Difference::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::Difference::gpu.png b/e2e_tests/expected/tests::blend_modes::Difference::gpu.png
new file mode 100644
index 0000000..babbbad
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::Difference::gpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::Exclusion::cpu.png b/e2e_tests/expected/tests::blend_modes::Exclusion::cpu.png
new file mode 100644
index 0000000..4400255
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::Exclusion::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::Exclusion::gpu.png b/e2e_tests/expected/tests::blend_modes::Exclusion::gpu.png
new file mode 100644
index 0000000..9096409
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::Exclusion::gpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::HardLight::cpu.png b/e2e_tests/expected/tests::blend_modes::HardLight::cpu.png
new file mode 100644
index 0000000..cdc7610
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::HardLight::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::HardLight::gpu.png b/e2e_tests/expected/tests::blend_modes::HardLight::gpu.png
new file mode 100644
index 0000000..f05aa56
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::HardLight::gpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::Hue::cpu.png b/e2e_tests/expected/tests::blend_modes::Hue::cpu.png
new file mode 100644
index 0000000..4189755
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::Hue::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::Hue::gpu.png b/e2e_tests/expected/tests::blend_modes::Hue::gpu.png
new file mode 100644
index 0000000..ac4c98a
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::Hue::gpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::Lighten::cpu.png b/e2e_tests/expected/tests::blend_modes::Lighten::cpu.png
new file mode 100644
index 0000000..abdced2
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::Lighten::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::Lighten::gpu.png b/e2e_tests/expected/tests::blend_modes::Lighten::gpu.png
new file mode 100644
index 0000000..1336159
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::Lighten::gpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::Luminosity::cpu.png b/e2e_tests/expected/tests::blend_modes::Luminosity::cpu.png
new file mode 100644
index 0000000..7cde9ef
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::Luminosity::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::Luminosity::gpu.png b/e2e_tests/expected/tests::blend_modes::Luminosity::gpu.png
new file mode 100644
index 0000000..ad72dca
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::Luminosity::gpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::Multiply::cpu.png b/e2e_tests/expected/tests::blend_modes::Multiply::cpu.png
new file mode 100644
index 0000000..35805f7
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::Multiply::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::Multiply::gpu.png b/e2e_tests/expected/tests::blend_modes::Multiply::gpu.png
new file mode 100644
index 0000000..b4469eb
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::Multiply::gpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::Over::cpu.png b/e2e_tests/expected/tests::blend_modes::Over::cpu.png
new file mode 100644
index 0000000..5bdd859
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::Over::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::Over::gpu.png b/e2e_tests/expected/tests::blend_modes::Over::gpu.png
new file mode 100644
index 0000000..2358f16
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::Over::gpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::Overlay::cpu.png b/e2e_tests/expected/tests::blend_modes::Overlay::cpu.png
new file mode 100644
index 0000000..4bdcd08
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::Overlay::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::Overlay::gpu.png b/e2e_tests/expected/tests::blend_modes::Overlay::gpu.png
new file mode 100644
index 0000000..a464a1a
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::Overlay::gpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::Saturation::cpu.png b/e2e_tests/expected/tests::blend_modes::Saturation::cpu.png
new file mode 100644
index 0000000..f956664
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::Saturation::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::Saturation::gpu.png b/e2e_tests/expected/tests::blend_modes::Saturation::gpu.png
new file mode 100644
index 0000000..08d4cda
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::Saturation::gpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::Screen::cpu.png b/e2e_tests/expected/tests::blend_modes::Screen::cpu.png
new file mode 100644
index 0000000..ca8f0fb
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::Screen::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::Screen::gpu.png b/e2e_tests/expected/tests::blend_modes::Screen::gpu.png
new file mode 100644
index 0000000..6b569c8
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::Screen::gpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::SoftLight::cpu.png b/e2e_tests/expected/tests::blend_modes::SoftLight::cpu.png
new file mode 100644
index 0000000..089dc5e
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::SoftLight::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::blend_modes::SoftLight::gpu.png b/e2e_tests/expected/tests::blend_modes::SoftLight::gpu.png
new file mode 100644
index 0000000..f4fccaf
--- /dev/null
+++ b/e2e_tests/expected/tests::blend_modes::SoftLight::gpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::clipping::cpu.png b/e2e_tests/expected/tests::clipping::cpu.png
new file mode 100644
index 0000000..76b0d55
--- /dev/null
+++ b/e2e_tests/expected/tests::clipping::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::clipping::gpu.png b/e2e_tests/expected/tests::clipping::gpu.png
new file mode 100644
index 0000000..efa4ae7
--- /dev/null
+++ b/e2e_tests/expected/tests::clipping::gpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::fill_rules::EvenOdd::cpu.png b/e2e_tests/expected/tests::fill_rules::EvenOdd::cpu.png
new file mode 100644
index 0000000..cda929a
--- /dev/null
+++ b/e2e_tests/expected/tests::fill_rules::EvenOdd::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::fill_rules::NonZero::cpu.png b/e2e_tests/expected/tests::fill_rules::NonZero::cpu.png
new file mode 100644
index 0000000..9d69fff
--- /dev/null
+++ b/e2e_tests/expected/tests::fill_rules::NonZero::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::linear_gradient::cpu.png b/e2e_tests/expected/tests::linear_gradient::cpu.png
new file mode 100644
index 0000000..0769a5d
--- /dev/null
+++ b/e2e_tests/expected/tests::linear_gradient::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::linear_gradient::gpu.png b/e2e_tests/expected/tests::linear_gradient::gpu.png
new file mode 100644
index 0000000..93e6d0f
--- /dev/null
+++ b/e2e_tests/expected/tests::linear_gradient::gpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::pixel::cpu.png b/e2e_tests/expected/tests::pixel::cpu.png
new file mode 100644
index 0000000..74da306
--- /dev/null
+++ b/e2e_tests/expected/tests::pixel::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::radial_gradient::cpu.png b/e2e_tests/expected/tests::radial_gradient::cpu.png
new file mode 100644
index 0000000..82a96e0
--- /dev/null
+++ b/e2e_tests/expected/tests::radial_gradient::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::radial_gradient::gpu.png b/e2e_tests/expected/tests::radial_gradient::gpu.png
new file mode 100644
index 0000000..0f31068
--- /dev/null
+++ b/e2e_tests/expected/tests::radial_gradient::gpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::solid_color::blue::cpu.png b/e2e_tests/expected/tests::solid_color::blue::cpu.png
new file mode 100644
index 0000000..7a43adf
--- /dev/null
+++ b/e2e_tests/expected/tests::solid_color::blue::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::solid_color::dark_blue::cpu.png b/e2e_tests/expected/tests::solid_color::dark_blue::cpu.png
new file mode 100644
index 0000000..b1427e3
--- /dev/null
+++ b/e2e_tests/expected/tests::solid_color::dark_blue::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::solid_color::dark_green::cpu.png b/e2e_tests/expected/tests::solid_color::dark_green::cpu.png
new file mode 100644
index 0000000..32643f1
--- /dev/null
+++ b/e2e_tests/expected/tests::solid_color::dark_green::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::solid_color::dark_red::cpu.png b/e2e_tests/expected/tests::solid_color::dark_red::cpu.png
new file mode 100644
index 0000000..b6f7a9d
--- /dev/null
+++ b/e2e_tests/expected/tests::solid_color::dark_red::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::solid_color::green::cpu.png b/e2e_tests/expected/tests::solid_color::green::cpu.png
new file mode 100644
index 0000000..f944ebb
--- /dev/null
+++ b/e2e_tests/expected/tests::solid_color::green::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::solid_color::red::cpu.png b/e2e_tests/expected/tests::solid_color::red::cpu.png
new file mode 100644
index 0000000..55335a0
--- /dev/null
+++ b/e2e_tests/expected/tests::solid_color::red::cpu.png
Binary files differ
diff --git a/e2e_tests/expected/tests::solid_color::transparent_black::cpu.png b/e2e_tests/expected/tests::solid_color::transparent_black::cpu.png
new file mode 100644
index 0000000..246d030
--- /dev/null
+++ b/e2e_tests/expected/tests::solid_color::transparent_black::cpu.png
Binary files differ
diff --git a/e2e_tests/tests/report.html b/e2e_tests/tests/report.html
new file mode 100644
index 0000000..dd27df9
--- /dev/null
+++ b/e2e_tests/tests/report.html
@@ -0,0 +1,295 @@
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <title>Forma test report</title>
+ <style>
+ body {
+ font-family: "Segoe UI", Arial, sans-serif;
+ }
+
+ .overview {
+ padding: 0 1rem;
+ margin-bottom: 1rem;
+ }
+
+ .testcase {
+ padding: 0 1rem;
+ margin-bottom: 1rem;
+ border: 1px solid transparent;
+ border-radius: 0.25rem;
+ }
+
+ .status_KO {
+ color: #721c24;
+ background-color: #f8d7da;
+ border-color: #f5c6cb;
+ }
+
+ .status_OK {
+ color: #155724;
+ background-color: #d4edda;
+ border-color: #c3e6cb;
+ }
+
+ h3 {
+ margin: 10px 0;
+ }
+
+ pre {
+ margin-top: 0;
+ }
+
+ .note {
+ height: 2em;
+ }
+
+ .superpose {
+ position: relative;
+ width: 256px;
+ height: 256px;
+ border: 2px solid #000;
+ margin: 4px;
+ background-color: #ffffff;
+ }
+
+ .cpu_gpu_pair {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ }
+
+ .diff {
+ display: flex;
+ flex-direction: row
+ }
+
+ .viewer {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ image-rendering: pixelated;
+ }
+
+ .hide_overlay .overlay_canvas {
+ visibility: hidden;
+ }
+
+ .hide_background .background_canvas {
+ visibility: hidden;
+ }
+
+ .superpose canvas {
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+
+ img {
+ display: none;
+ }
+
+ kbd {
+ padding: 0.2rem 0.4rem;
+ font-size: 87.5%;
+ color: #fff;
+ background-color: #212529;
+ border-radius: 0.2rem;
+ }
+
+ .shortcuts {
+ margin: 1em;
+ text-align: right;
+ }
+
+ .shortcuts div {
+ display: inline-block;
+ }
+
+ .overview {
+ clear: both;
+ }
+ </style>
+
+</head>
+
+<body>
+ <div>
+ <h1 style="float:left">Forma test report</h1>
+ <div class="shortcuts">
+ <div><kbd>b</kbd> toggle background draw checker |</div>
+ <div><kbd>d</kbd> toggle diff overlay.</div>
+ </div>
+ </div>
+ <div class="overview">
+ <div class="template"><a class="status_{status}" href="#{name}">{name}</a></div>
+ </div>
+ <div id="root" class="hide_overlay">
+ <div class="template">
+ <div class="testcase status_{status}" id="{name}">
+ <h3>{name}</h3>
+ <pre>{message}</pre>
+ <div class="cpu_gpu_pair">
+ <div class="diff">
+ <div class="viewer cpu_actual">
+ CPU Actual
+ <div class="superpose">
+ <canvas width="256" height="256" class="background_canvas"></canvas>
+ <canvas width="256" height="256" class="image_canvas"></canvas>
+ <canvas width="256" height="256" class="overlay_canvas"></canvas>
+ </div>
+ <img src="{cpu_actual}"/>
+ <pre class="note"></pre>
+ </div>
+ <div class="viewer" class="cpu_expected">
+ CPU Expected
+ <div class="superpose">
+ <canvas width="256" height="256" class="background_canvas"></canvas>
+ <canvas width="256" height="256" class="image_canvas"></canvas>
+ <canvas width="256" height="256" class="overlay_canvas"></canvas>
+ </div>
+ <img src="{cpu_expected}"/>
+ <pre class="note"></pre>
+ </div>
+ </div>
+ <div class="diff">
+ <div class="viewer" class="gpu_actual">
+ GPU Actual
+ <div class="superpose">
+ <canvas width="256" height="256" class="background_canvas"></canvas>
+ <canvas width="256" height="256" class="image_canvas"></canvas>
+ <canvas width="256" height="256" class="overlay_canvas"></canvas>
+ </div>
+ <img src="{gpu_actual}"/>
+ <pre class="note"></pre>
+ </div>
+ <div class="viewer" class="gpu_expected">
+ GPU Expected
+ <div class="superpose">
+ <canvas width="256" height="256" class="background_canvas"></canvas>
+ <canvas width="256" height="256" class="image_canvas"></canvas>
+ <canvas width="256" height="256" class="overlay_canvas"></canvas>
+ </div>
+ <img src="{gpu_expected}"/>
+ <pre class="note"></pre>
+ </div>
+ </div>
+
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <script>
+ // Scale factor expressed as a bit shift.
+ const shift = 2;
+
+ let entries = [/* generated */];
+
+ // Get issues first and the sort by name.
+ entries.sort((a, b) => a.name.localeCompare(b.name));
+ entries.sort((a, b) => a.status.localeCompare(b.status));
+
+ // Instanciate templates.
+ for (const template of Array.from(document.getElementsByClassName('template'))) {
+ const parentNode = template.parentNode;
+ parentNode.removeChild(template);
+ for (const entry of entries) {
+ const elem = document.createElement("div");
+ var html = template.innerHTML
+ for (const property in entry) html = html.replaceAll(`{${property}}`, entry[property]);
+ elem.innerHTML = html;
+ parentNode.appendChild(elem);
+ }
+ console.log(template);
+ }
+
+ // Show color picker on hover.
+ document.addEventListener('mousemove', e => {
+ const get_row = canvas => canvas.closest(".testcase");
+ const get_viewer = canvas => canvas.closest(".viewer");
+ if (e.path[0].tagName !== "CANVAS") return;
+ const row = get_row(e.path[0]);
+ for (const canvas of Array.from(document.getElementsByClassName("image_canvas"))) {
+ const note = get_viewer(canvas).getElementsByClassName("note")[0];
+ if (get_row(canvas).isSameNode(row)) {
+ try {
+ const data = canvas.getContext("2d").getImageData(e.offsetX, e.offsetY, 1, 1).data;
+ const rgba = v => data[v].toString().padStart(3, " ")
+ note.innerHTML = `(x: ${e.offsetX >> shift}, y: ${e.offsetY >> shift})\nr: ${rgba(0)}, g: ${rgba(1)}, b: ${rgba(2)}, a: ${rgba(3)}`;
+ } catch (error) {
+ console.warn(error);
+ }
+ } else {
+ note.innerHTML = "";
+ }
+ }
+
+ });
+
+ // Toggle debug view on key press.
+ document.addEventListener('keypress', e => {
+ console.log(e);
+ let elem = document.getElementById("root");
+ if (e.key == "b") elem.classList.toggle("hide_background");
+ if (e.key == "d") elem.classList.toggle("hide_overlay");
+ });
+
+ function drawChecker(canvas) {
+ const rows = 8, cols = 8;
+ const w = canvas.clientWidth, h = canvas.clientHeight;
+ const ctx = canvas.getContext("2d");
+ ctx.clearRect(0, 0, w, h);
+ ctx.fillStyle = "LightGray";
+ for (let r = 0; r < rows; r++) {
+ for (let c = r % 2; c < cols; c += 2) {
+ ctx.beginPath();
+ ctx.rect(w * r / rows, h * c / cols, w / rows, h / cols);
+ ctx.fill();
+ }
+ }
+ }
+
+ // Load the impage into canvas for inspection, and compute diffs.
+ window.onload = function () {
+ for (const elem of Array.from(document.getElementsByClassName("viewer"))) {
+ const img = elem.getElementsByTagName("img")[0];
+ if (img.getAttribute("src") === "null") continue;
+ const context = elem.getElementsByClassName("image_canvas")[0].getContext("2d");
+ context.webkitImageSmoothingEnabled = false;
+ context.mozImageSmoothingEnabled = false;
+ context.msImageSmoothingEnabled = false;
+ context.imageSmoothingEnabled = false;
+ context.drawImage(img, 0, 0, 256, 256);
+ }
+
+ for (const canvas of Array.from(document.getElementsByClassName("background_canvas"))) {
+ drawChecker(canvas);
+ }
+
+ for (const diff of Array.from(document.getElementsByClassName("diff"))) {
+ const canvases = diff.getElementsByClassName("image_canvas");
+ const w = canvases[0].clientWidth, h = canvases[0].clientHeight;
+ let id0 = canvases[0].getContext("2d").getImageData(0, 0, w, h);
+ let id1 = canvases[1].getContext("2d").getImageData(0, 0, w, h);
+ let d0 = id0.data, d1 = id1.data;
+ let colorize = (sum) => {
+ if (sum > 8) return [255, 60, 0, 128];
+ else if (sum > 4) return [255, 140, 0, 128];
+ return [0, 0, 0, 0];
+ }
+ let delta = (i) => Math.abs(d0[i] - d1[i]);
+ for (let i = w * h * 4 - 4; i >= 0; i -= 4) {
+ const sum = delta(i) + delta(i + 1) + delta(i + 2) + delta(i + 3);
+ [d0[i], d0[i + 1], d0[i + 2], d0[i + 3]] = colorize(sum);
+ }
+ for (const canvas of Array.from(diff.getElementsByClassName("overlay_canvas"))) {
+ canvas.getContext("2d").putImageData(id0, 0, 0);
+ }
+ }
+ };
+
+ </script>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/e2e_tests/tests/test_env.rs b/e2e_tests/tests/test_env.rs
new file mode 100644
index 0000000..d67754e
--- /dev/null
+++ b/e2e_tests/tests/test_env.rs
@@ -0,0 +1,373 @@
+/// End to end rendering test for forma GPU and CPU.
+/// A report is written to ${TARGET}/tmp/tests/report.html
+use std::{
+ cell::RefCell,
+ env,
+ fmt::Debug,
+ fs,
+ fs::{create_dir_all, remove_dir_all},
+ num::NonZeroU32,
+ path,
+ path::PathBuf,
+ sync::Mutex,
+};
+
+use anyhow::{anyhow, Context};
+use forma::{
+ buffer::{layout::LinearLayout, BufferBuilder},
+ Color, Composition, CpuRenderer, GpuRenderer, RGBA,
+};
+use image::RgbaImage;
+use once_cell::sync::OnceCell;
+use serde::Serialize;
+
+pub const WIDTH: f32 = 64.0;
+pub const HEIGHT: f32 = 64.0;
+pub const PADDING: f32 = 8.0;
+
+fn cpu_render(composition: &mut Composition, width: usize, height: usize) -> RgbaImage {
+ let mut data = vec![0; width * 4 * height];
+ let mut layout = LinearLayout::new(width, width * 4, height);
+ let mut buffer = BufferBuilder::new(&mut data, &mut layout).build();
+ let mut renderer = CpuRenderer::new();
+ renderer.render(composition, &mut buffer, RGBA, Color { r: 1.0, g: 1.0, b: 1.0, a: 0.0 }, None);
+ let image = RgbaImage::from_raw(width as u32, height as u32, data).unwrap();
+ image
+}
+
+fn gpu_render(composition: &mut Composition, width: usize, height: usize) -> RgbaImage {
+ let instance = wgpu::Instance::new(wgpu::Backends::PRIMARY);
+ let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
+ power_preference: wgpu::PowerPreference::HighPerformance,
+ ..Default::default()
+ }))
+ .expect("failed to find an appropriate adapter");
+
+ let (device, queue) = pollster::block_on(adapter.request_device(
+ &wgpu::DeviceDescriptor {
+ label: None,
+ features: Default::default(),
+ limits: wgpu::Limits {
+ max_texture_dimension_2d: 4096,
+ max_storage_buffer_binding_size: 1 << 30,
+ ..wgpu::Limits::downlevel_defaults()
+ },
+ },
+ None,
+ ))
+ .expect("failed to get device");
+
+ let texture_desc = wgpu::TextureDescriptor {
+ size: wgpu::Extent3d {
+ width: width as u32,
+ height: width as u32,
+ depth_or_array_layers: 1,
+ },
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format: wgpu::TextureFormat::Rgba8UnormSrgb,
+ usage: wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::RENDER_ATTACHMENT,
+ label: None,
+ };
+ let texture = device.create_texture(&texture_desc);
+
+ let mut renderer = GpuRenderer::new(&device, texture_desc.format, false);
+ renderer.render_to_texture(
+ composition,
+ &device,
+ &queue,
+ &texture,
+ width as u32,
+ height as u32,
+ Color { r: 1.0, g: 1.0, b: 1.0, a: 0.0 },
+ );
+
+ let output_buffer_size = (4 * width * height) as wgpu::BufferAddress;
+ let output_buffer_desc = wgpu::BufferDescriptor {
+ size: output_buffer_size,
+ usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
+ label: None,
+ mapped_at_creation: false,
+ };
+ let output_buffer = device.create_buffer(&output_buffer_desc);
+
+ let mut encoder =
+ device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
+ encoder.copy_texture_to_buffer(
+ wgpu::ImageCopyTexture {
+ aspect: wgpu::TextureAspect::All,
+ texture: &texture,
+ mip_level: 0,
+ origin: wgpu::Origin3d::ZERO,
+ },
+ wgpu::ImageCopyBuffer {
+ buffer: &output_buffer,
+ layout: wgpu::ImageDataLayout {
+ offset: 0,
+ bytes_per_row: NonZeroU32::new(4 * width as u32),
+ rows_per_image: NonZeroU32::new(width as u32),
+ },
+ },
+ texture_desc.size,
+ );
+ queue.submit(Some(encoder.finish()));
+ // We need to scope the mapping variables so that we can
+ // unmap the buffer
+ let image = {
+ let buffer_slice = output_buffer.slice(..);
+ // NOTE: We have to create the mapping THEN device.poll() before await
+ // the future. Otherwise the application will freeze.
+ let mapping = buffer_slice.map_async(wgpu::MapMode::Read);
+ device.poll(wgpu::Maintain::Wait);
+ pollster::block_on(mapping).unwrap();
+
+ let data = buffer_slice.get_mapped_range();
+ RgbaImage::from_raw(width as u32, height as u32, data.to_vec()).unwrap()
+ };
+ output_buffer.unmap();
+ image
+}
+
+fn compare_images(expected: &RgbaImage, actual: &RgbaImage, tolerance: u8) -> anyhow::Result<()> {
+ let offenders = expected
+ .pixels()
+ .zip(actual.pixels())
+ .filter(|(e, a)| e.0.iter().zip(a.0.iter()).any(|(e, a)| e.abs_diff(*a) > tolerance))
+ .count();
+ if offenders > 0 {
+ Err(anyhow!(
+ "{} pixels differs have a channel that differ by more than {} units",
+ offenders,
+ tolerance
+ ))
+ } else {
+ Ok(())
+ }
+}
+
+#[derive(Debug)]
+enum RendererType {
+ CPU,
+ GPU,
+}
+
+#[derive(Serialize)]
+struct TestReportEntry {
+ // Unique name for this test.
+ name: String,
+ // Actual and expected strings contains the image encoded as
+ // valid `src` attribute for an HTML `img` element.
+ cpu_actual: String,
+ // `None` when no reference image file exist.
+ // CPU reference is required for test success.
+ // It is typically missing when a new test is created.
+ cpu_expected: Option<String>,
+ gpu_actual: String,
+ // `None` when no reference exist.
+ gpu_expected: Option<String>,
+ // "OK" or "KO".
+ status: String,
+ // Test status message. Empty for successful tests.
+ message: String,
+}
+
+// Report collecting all information about all tests that is used for
+// HTML report generation.
+struct TestReport {
+ output_dir: PathBuf,
+ entries: Mutex<Vec<TestReportEntry>>,
+}
+
+// Object implementing soft failure so that we can collect
+pub struct TestEnv {
+ test_name: String,
+ failures: RefCell<Vec<String>>,
+}
+
+impl TestEnv {
+ pub fn new(test_name: &str) -> TestEnv {
+ TestEnv { test_name: test_name.to_string(), failures: RefCell::new(vec![]) }
+ }
+ pub fn test_render<F>(self, compose: F)
+ where
+ F: Fn(&mut Composition) -> (),
+ {
+ if let Err(err) = test_render(compose, &self.test_name) {
+ self.failures.borrow_mut().push(format!("{:?}", err));
+ }
+ }
+
+ pub fn test_render_param<F, T: Debug>(&self, compose: F, t: T)
+ where
+ F: Fn(&mut Composition) -> (),
+ {
+ let t = format!("{:?}", t).replace("\"", "");
+ if let Err(err) = test_render(compose, &format!("{}::{}", self.test_name, t)) {
+ self.failures.borrow_mut().push(format!("{:?}", err));
+ }
+ }
+}
+
+// Implements soft failure so that all test can run.
+impl Drop for TestEnv {
+ fn drop(&mut self) {
+ if self.failures.borrow().is_empty() {
+ return;
+ }
+
+ println!("Test {}:", self.test_name);
+ self.failures
+ .borrow()
+ .iter()
+ .enumerate()
+ .for_each(|(i, e)| println!(" - Error [{}]: {}", i, e));
+ println!("Report generated at target/tmp/tests/report.html");
+ panic!();
+ }
+}
+
+fn test_render<F>(compose: F, test_name: &str) -> anyhow::Result<()>
+where
+ F: Fn(&mut Composition) -> (),
+{
+ let cpu_actual = {
+ let mut composition = Composition::new();
+ compose(&mut composition);
+ cpu_render(&mut composition, WIDTH as usize, HEIGHT as usize)
+ };
+
+ let gpu_actual = {
+ let mut composition = Composition::new();
+ compose(&mut composition);
+ gpu_render(&mut composition, WIDTH as usize, HEIGHT as usize)
+ };
+
+ let result = (|| {
+ let tolerance = 8;
+ let expected_cpu = expected_image(test_name, RendererType::CPU)
+ .context("CPU reference image is missing.")?;
+ compare_images(&expected_cpu, &cpu_actual, tolerance)
+ .context("CPU result differs from the reference image.")?;
+ let expected_gpu = expected_image(test_name, RendererType::GPU).unwrap_or(expected_cpu);
+ compare_images(&expected_gpu, &gpu_actual, tolerance)
+ .context("GPU result differs from the reference image.")
+ })();
+ add_result_to_report(test_name, &cpu_actual, &gpu_actual, &result);
+ result
+}
+
+/// Path to the reference image to which the rendering is compared to.
+fn expected_image_path(test_name: &str, renderer: RendererType) -> PathBuf {
+ let renderer = format!("{:?}", renderer).to_lowercase();
+ env::current_dir().unwrap().join("expected").join(format!("{}::{}.png", test_name, renderer))
+}
+
+/// Returns the reference image buffer if the such image exist.
+fn expected_image(test_name: &str, renderer: RendererType) -> anyhow::Result<RgbaImage> {
+ let path = expected_image_path(test_name, renderer);
+ if !path.exists() {
+ return Err(anyhow!("Unable to open file {:?}", path));
+ }
+ // Panic if the file exists but is not valid.
+ Ok(image::io::Reader::open(path)
+ .context("Unable to open file")?
+ .decode()
+ .context("Unable to open file")?
+ .into_rgba8())
+}
+
+fn add_result_to_report(
+ test_name: &str,
+ cpu_actual: &RgbaImage,
+ gpu_actual: &RgbaImage,
+ status: &anyhow::Result<()>,
+) {
+ static REPORT: OnceCell<Mutex<TestReport>> = OnceCell::new();
+ let lock = REPORT
+ .get_or_init(|| {
+ let output_dir =
+ path::Path::new(env!("CARGO_TARGET_TMPDIR")).join(env!("CARGO_CRATE_NAME"));
+ let _ = remove_dir_all(&output_dir);
+ create_dir_all(&output_dir).unwrap();
+ Mutex::new(TestReport::new(output_dir))
+ })
+ .lock();
+ let report = lock.unwrap();
+
+ let cpu_expected = expected_image_path(test_name, RendererType::CPU);
+ let gpu_expected = expected_image_path(test_name, RendererType::GPU);
+ report.add_result(
+ test_name,
+ cpu_expected.exists().then(|| cpu_expected),
+ cpu_actual,
+ gpu_expected.exists().then(|| gpu_expected),
+ gpu_actual,
+ status,
+ );
+}
+
+impl TestReport {
+ fn new(output_dir: path::PathBuf) -> TestReport {
+ TestReport { output_dir: output_dir, entries: Mutex::new(vec![]) }
+ }
+
+ fn add_result(
+ &self,
+ test_name: &str,
+ cpu_expected: Option<PathBuf>,
+ cpu_actual: &RgbaImage,
+ gpu_expected: Option<PathBuf>,
+ gpu_actual: &RgbaImage,
+ status: &anyhow::Result<()>,
+ ) {
+ let path =
+ |dir, suffix| self.output_dir.join(dir).join(format!("{}::{}.png", test_name, suffix));
+ let to_base64_img_src = |path: &path::PathBuf| {
+ format!("data:image/png;base64, {}", base64::encode(fs::read(path).unwrap()))
+ };
+ let save_actual = |image: &RgbaImage, suffix| {
+ let path = path("actual", suffix);
+ fs::create_dir_all(&path.parent().unwrap()).unwrap();
+ image.save(&path).unwrap();
+ to_base64_img_src(&path)
+ };
+ let entry = TestReportEntry {
+ name: test_name.to_string(),
+ cpu_actual: save_actual(cpu_actual, "cpu"),
+ cpu_expected: cpu_expected.as_ref().map(to_base64_img_src),
+ gpu_actual: save_actual(gpu_actual, "gpu"),
+ gpu_expected: gpu_expected.or(cpu_expected).as_ref().map(to_base64_img_src),
+ status: match status {
+ Ok(_) => "OK".to_owned(),
+ Err(_) => "KO".to_owned(),
+ },
+ message: match status {
+ Ok(_) => "".to_owned(),
+ Err(e) => format!("{:?}", e),
+ },
+ };
+ let mut entries = self.entries.lock().unwrap();
+ entries.push(entry);
+
+ // Update the report every time.
+ // The is no hook in the test framework to generate this report at the end.
+ let report = include_str!("report.html").replace(
+ "[/* generated */]",
+ &serde_json::to_string_pretty(entries.as_slice()).unwrap(),
+ );
+ fs::write(&self.output_dir.join("report.html"), report).unwrap();
+ }
+}
+
+macro_rules! test_env {
+ () => {{
+ fn f() {}
+ fn type_name_of<T>(_: T) -> &'static str {
+ std::any::type_name::<T>()
+ }
+
+ TestEnv::new(type_name_of(f).strip_prefix("tests::").unwrap().strip_suffix("::f").unwrap())
+ }};
+}
+pub(crate) use test_env;
diff --git a/e2e_tests/tests/tests.rs b/e2e_tests/tests/tests.rs
new file mode 100644
index 0000000..69da6c9
--- /dev/null
+++ b/e2e_tests/tests/tests.rs
@@ -0,0 +1,330 @@
+mod test_env;
+
+#[cfg(test)]
+mod tests {
+ use super::test_env::*;
+ use forma::{
+ BlendMode, Color, Fill, FillRule, Func, Gradient, GradientBuilder, Order, Path,
+ PathBuilder, Point, Props, Style,
+ };
+
+ fn triangle() -> Path {
+ PathBuilder::new()
+ .move_to(Point { x: PADDING, y: PADDING })
+ .line_to(Point { x: WIDTH - PADDING, y: PADDING })
+ .line_to(Point { x: WIDTH - PADDING, y: HEIGHT - PADDING })
+ .build()
+ }
+
+ fn square() -> Path {
+ PathBuilder::new()
+ .move_to(Point { x: PADDING, y: PADDING })
+ .line_to(Point { x: WIDTH - PADDING, y: PADDING })
+ .line_to(Point { x: WIDTH - PADDING, y: HEIGHT - PADDING })
+ .line_to(Point { x: PADDING, y: HEIGHT - PADDING })
+ .build()
+ }
+
+ fn inner_square() -> Path {
+ PathBuilder::new()
+ .move_to(Point { x: PADDING * 2.0, y: PADDING * 2.0 })
+ .line_to(Point { x: WIDTH - PADDING * 2.0, y: PADDING * 2.0 })
+ .line_to(Point { x: WIDTH - PADDING * 2.0, y: HEIGHT - PADDING * 2.0 })
+ .line_to(Point { x: PADDING * 2.0, y: HEIGHT - PADDING * 2.0 })
+ .build()
+ }
+
+ fn circle() -> Path {
+ let weight = 2.0f32.sqrt() / 2.0;
+ let x = WIDTH * 0.5;
+ let y = HEIGHT * 0.5;
+ let radius = WIDTH * 0.5 - PADDING;
+ PathBuilder::new()
+ .move_to(Point::new(x + radius, y))
+ .rat_quad_to(Point::new(x + radius, y - radius), Point::new(x, y - radius), weight)
+ .rat_quad_to(Point::new(x - radius, y - radius), Point::new(x - radius, y), weight)
+ .rat_quad_to(Point::new(x - radius, y + radius), Point::new(x, y + radius), weight)
+ .rat_quad_to(Point::new(x + radius, y + radius), Point::new(x + radius, y), weight)
+ .build()
+ }
+
+ fn rainbow_colors(gradient_builder: &mut GradientBuilder) {
+ gradient_builder
+ .color(Color { r: 1.00, g: 0.00, b: 0.00, a: 1.0 })
+ .color(Color { r: 1.00, g: 0.32, b: 0.00, a: 1.0 })
+ .color(Color { r: 0.63, g: 0.73, b: 0.02, a: 1.0 })
+ .color(Color { r: 0.08, g: 0.72, b: 0.07, a: 1.0 })
+ .color(Color { r: 0.05, g: 0.70, b: 0.69, a: 1.0 })
+ .color(Color { r: 0.03, g: 0.58, b: 0.76, a: 1.0 })
+ .color(Color { r: 0.01, g: 0.21, b: 0.85, a: 1.0 })
+ .color(Color { r: 0.11, g: 0.01, b: 0.89, a: 1.0 })
+ .color(Color { r: 0.49, g: 0.00, b: 0.94, a: 1.0 })
+ .color(Color { r: 0.96, g: 0.00, b: 0.69, a: 1.0 })
+ .color(Color { r: 1.00, g: 0.00, b: 0.00, a: 1.0 });
+ }
+
+ fn vertical_rainbow() -> Gradient {
+ let mut gradient_builder = GradientBuilder::new(
+ Point { x: PADDING, y: 0.0 },
+ Point { x: WIDTH - PADDING, y: 0.0 },
+ );
+ rainbow_colors(&mut gradient_builder);
+ gradient_builder.build().unwrap()
+ }
+
+ fn horizontal_rainbow() -> Gradient {
+ let mut gradient_builder = GradientBuilder::new(
+ Point { x: 0.0, y: PADDING },
+ Point { x: 0.0, y: WIDTH - PADDING },
+ );
+ rainbow_colors(&mut gradient_builder);
+ gradient_builder.build().unwrap()
+ }
+
+ fn solid_color_props(color: Color) -> Props {
+ Props {
+ func: Func::Draw(Style { fill: Fill::Solid(color), ..Default::default() }),
+ ..Default::default()
+ }
+ }
+
+ #[test]
+ fn linear_gradient() {
+ let test_env = test_env!();
+ test_env.test_render(|composition| {
+ let mut gradient_builder = GradientBuilder::new(
+ Point { x: PADDING, y: 0.0 },
+ Point { x: WIDTH - PADDING, y: 0.0 },
+ );
+ gradient_builder
+ .color(Color { r: 0.0, g: 0.0, b: 1.0, a: 1.0 })
+ .color(Color { r: 1.0, g: 1.0, b: 1.0, a: 1.0 })
+ .color(Color { r: 1.0, g: 0.0, b: 0.0, a: 1.0 });
+ let props = Props {
+ func: Func::Draw(Style {
+ fill: Fill::Gradient(gradient_builder.build().unwrap()),
+ ..Default::default()
+ }),
+ ..Default::default()
+ };
+ composition
+ .get_mut_or_insert_default(Order::new(1).unwrap())
+ .insert(&triangle())
+ .set_props(props);
+ });
+ }
+
+ #[test]
+ fn radial_gradient() {
+ let test_env = test_env!();
+ test_env.test_render(|composition| {
+ let mut gradient_builder = GradientBuilder::new(
+ Point { x: WIDTH * 0.5, y: HEIGHT * 0.5 },
+ Point { x: WIDTH - PADDING * 2.0, y: HEIGHT * 0.5 },
+ );
+ gradient_builder
+ .r#type(forma::GradientType::Radial)
+ .color(Color { r: 0.0, g: 0.0, b: 1.0, a: 1.0 })
+ .color(Color { r: 1.0, g: 1.0, b: 1.0, a: 1.0 })
+ .color(Color { r: 1.0, g: 0.0, b: 0.0, a: 1.0 });
+ let props = Props {
+ func: Func::Draw(Style {
+ fill: Fill::Gradient(gradient_builder.build().unwrap()),
+ ..Default::default()
+ }),
+ ..Default::default()
+ };
+ composition
+ .get_mut_or_insert_default(Order::new(1).unwrap())
+ .insert(&circle())
+ .set_props(props);
+ });
+ }
+
+ #[test]
+ fn solid_color() {
+ let test_env = test_env!();
+ let colors = vec![
+ (Color { r: 0.0, g: 0.0, b: 1.0, a: 1.0 }, "blue"),
+ (Color { r: 0.0, g: 0.0, b: 0.5, a: 1.0 }, "dark_blue"),
+ (Color { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, "red"),
+ (Color { r: 0.5, g: 0.0, b: 0.0, a: 1.0 }, "dark_red"),
+ (Color { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, "green"),
+ (Color { r: 0.0, g: 0.5, b: 0.0, a: 1.0 }, "dark_green"),
+ (Color { r: 0.0, g: 0.0, b: 0.0, a: 0.5 }, "transparent_black"),
+ ];
+ for (color, name) in colors {
+ test_env.test_render_param(
+ |composition| {
+ composition
+ .get_mut_or_insert_default(Order::new(1).unwrap())
+ .insert(&square())
+ .set_props(solid_color_props(color));
+ },
+ name,
+ );
+ }
+ }
+
+ #[test]
+ fn pixel() {
+ // This test is useful when the reasterizer is brocken as it emmits 2 pixel segments.
+ test_env!().test_render(|composition| {
+ composition
+ .get_mut_or_insert_default(Order::new(1).unwrap())
+ .insert(
+ &PathBuilder::new()
+ .move_to(Point { x: PADDING, y: PADDING })
+ .line_to(Point { x: PADDING + 1.0, y: PADDING })
+ .line_to(Point { x: PADDING + 1.0, y: PADDING + 1.0 })
+ .line_to(Point { x: PADDING, y: PADDING + 1.0 })
+ .build(),
+ )
+ .set_props(solid_color_props(Color { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }));
+ });
+ }
+
+ #[test]
+ fn blend_modes() {
+ let test_env = test_env!();
+ let blend_modes = [
+ BlendMode::Over,
+ BlendMode::Multiply,
+ BlendMode::Screen,
+ BlendMode::Overlay,
+ BlendMode::Darken,
+ BlendMode::Lighten,
+ BlendMode::ColorDodge,
+ BlendMode::ColorBurn,
+ BlendMode::HardLight,
+ BlendMode::SoftLight,
+ BlendMode::Difference,
+ BlendMode::Exclusion,
+ BlendMode::Hue,
+ BlendMode::Saturation,
+ BlendMode::Color,
+ BlendMode::Luminosity,
+ ];
+ for blend_mode in blend_modes {
+ test_env.test_render_param(
+ |composition| {
+ composition
+ .get_mut_or_insert_default(Order::new(0).unwrap())
+ .insert(&square())
+ .set_props(Props {
+ func: Func::Draw(Style {
+ fill: Fill::Gradient(horizontal_rainbow()),
+ ..Default::default()
+ }),
+ ..Default::default()
+ });
+
+ composition
+ .get_mut_or_insert_default(Order::new(1).unwrap())
+ .insert(&triangle())
+ .set_props(Props {
+ func: Func::Draw(Style {
+ fill: Fill::Gradient(vertical_rainbow()),
+ blend_mode,
+ ..Default::default()
+ }),
+ ..Default::default()
+ });
+ },
+ blend_mode,
+ );
+ }
+ }
+
+ #[test]
+ fn fill_rules() {
+ let test_env = test_env!();
+ let fill_rules = [FillRule::EvenOdd, FillRule::NonZero];
+ for fill_rule in fill_rules {
+ test_env.test_render_param(
+ |composition| {
+ let path = PathBuilder::new()
+ .move_to(Point { x: PADDING, y: PADDING })
+ .line_to(Point { x: WIDTH / 2.0 + PADDING, y: HEIGHT / 2.0 + PADDING })
+ .line_to(Point { x: WIDTH / 2.0 - PADDING, y: HEIGHT / 2.0 + PADDING })
+ .line_to(Point { x: WIDTH - PADDING, y: PADDING })
+ .line_to(Point { x: WIDTH - PADDING, y: HEIGHT - PADDING })
+ .line_to(Point { x: PADDING, y: HEIGHT - PADDING })
+ .build();
+ composition
+ .get_mut_or_insert_default(Order::new(0).unwrap())
+ .insert(&path)
+ .set_props(Props {
+ fill_rule,
+ func: Func::Draw(Style {
+ fill: Fill::Solid(Color { r: 0.0, g: 0.0, b: 0.0, a: 0.8 }),
+ ..Default::default()
+ }),
+ });
+ },
+ fill_rule,
+ );
+ }
+ }
+
+ #[test]
+ fn clipping() {
+ let test_env = test_env!();
+ test_env.test_render(|composition| {
+ // First layer is not clipped.
+ composition
+ .get_mut_or_insert_default(Order::new(0).unwrap())
+ .insert(&square())
+ .set_props(solid_color_props(Color { r: 0.0, g: 0.0, b: 0.0, a: 0.2 }));
+
+ // Triangular clip shape applies to the next 3 layers ids.
+ let props = Props { func: Func::Clip(3), ..Default::default() };
+ composition
+ .get_mut_or_insert_default(Order::new(1).unwrap())
+ .insert(&triangle())
+ .set_props(props);
+
+ // The blue square is clipped.
+ composition
+ .get_mut_or_insert_default(Order::new(2).unwrap())
+ .insert(&square())
+ .set_props(Props {
+ func: Func::Draw(Style {
+ fill: Fill::Solid(Color { r: 0.5, g: 0.5, b: 1.0, a: 0.6 }),
+ is_clipped: true,
+ ..Default::default()
+ }),
+ ..Default::default()
+ });
+
+ // Order No. 3 is intentionnaly left empty to test the clip implementation.
+
+ // The pink circle is clipped.
+ composition
+ .get_mut_or_insert_default(Order::new(4).unwrap())
+ .insert(&circle())
+ .set_props(Props {
+ func: Func::Draw(Style {
+ fill: Fill::Solid(Color { r: 1.0, g: 0.5, b: 0.5, a: 0.6 }),
+ is_clipped: true,
+ ..Default::default()
+ }),
+ ..Default::default()
+ });
+
+ // This is not drawn given that `is_clipped: true` and no clipping
+ // is active at order 5.
+ composition
+ .get_mut_or_insert_default(Order::new(5).unwrap())
+ .insert(&inner_square())
+ .set_props(Props {
+ func: Func::Draw(Style {
+ fill: Fill::Solid(Color { r: 0.5, g: 0.5, b: 1.0, a: 0.6 }),
+ is_clipped: true,
+ ..Default::default()
+ }),
+ ..Default::default()
+ });
+ });
+ }
+}
diff --git a/gpu/renderer/src/lib.rs b/gpu/renderer/src/lib.rs
index eab569b..d3be4e6 100644
--- a/gpu/renderer/src/lib.rs
+++ b/gpu/renderer/src/lib.rs
@@ -108,7 +108,7 @@
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
- surface: &wgpu::Surface,
+ texture: &wgpu::Texture,
width: u32,
height: u32,
segments: &[u64],
@@ -116,8 +116,7 @@
styles: &[u32],
background_color: Color,
) -> Result<Option<Timings>, Error> {
- let frame = surface.get_current_texture()?;
- let view = frame.texture.create_view(&wgpu::TextureViewDescriptor::default());
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let timestamp_context = self.has_timestamp_query.then(|| {
let timestamp = device.create_query_set(&wgpu::QuerySetDescriptor {
@@ -176,8 +175,16 @@
let len_in_blocks =
u32::try_from(div_round_up(old_len, BLOCK_SIZE.block_len as usize)).unwrap();
- segments
- .resize((len_in_blocks * BLOCK_SIZE.block_len) as usize, bytemuck::cast(u64::MAX));
+ // Adds padding to the data to please the sort. `BLOCK_SIZE.block_len` should
+ // be enough, but twice this amount is required for an unknown reason.
+ // TODO remove this when the sort is replaced by a better version.
+ segments.resize(
+ std::cmp::max(
+ (len_in_blocks * BLOCK_SIZE.block_len) as usize,
+ (BLOCK_SIZE.block_len * 2) as usize,
+ ),
+ bytemuck::cast(u64::MAX),
+ );
let slice_size = segments.len() * std::mem::size_of::<u64>();
let size = slice_size as wgpu::BufferAddress;
@@ -288,7 +295,6 @@
}
queue.submit(Some(encoder.finish()));
- frame.present();
let timings = timestamp_context.as_ref().map(|(_, timestamp_period, data_buffer)| {
use bytemuck::{Pod, Zeroable};