diff --git a/.github/workflows/reusable-check-rendering.yml b/.github/workflows/reusable-check-rendering.yml
index 0bfcaa45074..02010659b13 100644
--- a/.github/workflows/reusable-check-rendering.yml
+++ b/.github/workflows/reusable-check-rendering.yml
@@ -131,36 +131,95 @@ jobs:
function Get-Image($path) {
if (Test-Path $path) { "
" }
}
- # The same PNG encoder wrote both images, so equal pixels give equal files.
+ # GPUs round colors differently, so a pixel only counts as changed when a color channel differs by more than
+ # 16 levels. That is the tolerance of the ignoreLess mode of Resemble.js, which marks the same pixels in the report.
+ Add-Type -AssemblyName System.Drawing
+ function Get-Mismatch($oldPath, $newPath) {
+ $old, $new = foreach ($path in $oldPath, $newPath) {
+ $bitmap = [Drawing.Bitmap]::new((Resolve-Path $path).Path)
+ $data = $bitmap.LockBits([Drawing.Rectangle]::new(0, 0, $bitmap.Width, $bitmap.Height), 'ReadOnly', 'Format32bppArgb')
+ $bytes = [byte[]]::new($data.Stride * $data.Height)
+ [Runtime.InteropServices.Marshal]::Copy($data.Scan0, $bytes, 0, $bytes.Length)
+ $bitmap.Dispose()
+ , $bytes
+ }
+ if ($old.Length -ne $new.Length) { return 100 }
+ $count = 0
+ for ($i = 0; $i -lt $old.Length; $i += 4) {
+ if ([Math]::Abs($old[$i] - $new[$i]) -gt 16 -or [Math]::Abs($old[$i + 1] - $new[$i + 1]) -gt 16 -or [Math]::Abs($old[$i + 2] - $new[$i + 2]) -gt 16) { $count++ }
+ }
+ # In percent, rounded up so that a single pixel shows.
+ [Math]::Ceiling(10000 * $count / ($old.Length / 4)) / 100
+ }
+ # The same PNG encoder wrote both images, so only differing files need comparing pixel by pixel.
$images = foreach ($name in (Get-ChildItem build/rendering-tests*/*_RenderTest/*.png).Name | Sort-Object -Unique) {
$old = Get-Image build/rendering-tests-base/*_RenderTest/$name
$new = Get-Image build/rendering-tests/*_RenderTest/$name
- $state = if (!$old) { 'new' } elseif (!$new) { 'removed' } elseif ($old -eq $new) { 'unchanged' } else { 'changed' }
- [pscustomobject]@{ Name = $name; State = $state; Old = $old; New = $new }
+ $mismatch = if ($old -and $new -and $old -ne $new) { Get-Mismatch build/rendering-tests-base/*_RenderTest/$name build/rendering-tests/*_RenderTest/$name } else { 0 }
+ $state = if (!$old) { 'new' } elseif (!$new) { 'removed' } elseif ($mismatch -eq 0) { 'unchanged' } else { 'changed' }
+ [pscustomobject]@{ Name = $name; State = $state; Old = $old; New = $new; Mismatch = $mismatch }
}
$changes = ($images | Where-Object State -in 'changed', 'removed' | ForEach-Object { "$($_.Name) $($_.State)" }) -join ', '
"changes=$changes" >> $env:GITHUB_OUTPUT
"message=$(if ($changes) { $changes } else { 'No changes' }) $since" >> $env:GITHUB_OUTPUT
- "### $env:GAME Rendering`n`n$(($images | Group-Object State | ForEach-Object { "$($_.Count) $($_.Name)" }) -join ', ') $since.`n" >> $env:GITHUB_STEP_SUMMARY
- # One page with the images inline, so the browser opens the artifact without unzipping. The difference
- # blends the new image over the base and brightens the result, so only changed pixels show.
- $rows = foreach ($image in $images | Sort-Object State, Name) {
- $diff = if ($image.State -eq 'changed') { "
$($image.Old)$($image.New)
" }
- "$($image.Name) $($image.State) | $($image.Old) | $($image.New) | $diff |
"
+ $counts = ($images | Group-Object State | ForEach-Object { "$($_.Count) $($_.Name)" }) -join ', '
+ "### $env:GAME Rendering`n`n$counts $since.`n" >> $env:GITHUB_STEP_SUMMARY
+ # One page with the images inline, so the browser opens the artifact without unzipping. Each test gets a card
+ # with its images, which capture() names _.png. Changes come first.
+ $order = 'changed', 'removed', 'new', 'unchanged'
+ $tests = $images | Group-Object { $_.Name -replace '_\d+\.png$' } | ForEach-Object {
+ [pscustomobject]@{ Name = $_.Name; Images = $_.Group; State = $_.Group.State | Sort-Object { $order.IndexOf($_) } | Select-Object -First 1 }
+ }
+ $cards = foreach ($test in $tests | Sort-Object { $order.IndexOf($_.State) }, Name) {
+ $index = 0
+ $rows = foreach ($image in $test.Images | Sort-Object { ($_.Name -replace '^.*_(\d+)\.png$', '$1') -as [int] }) {
+ $title = if ($index++ -eq 0) { "$($test.Name)
" }
+ $number = if ($test.Images.Count -gt 1) { "$($image.Name -replace '^.*_(\d+)\.png$', '#$1')" }
+ $diff, $percent = if ($image.State -eq 'changed') { ' class=diff', "$($image.Mismatch)% of the pixels differ" }
+ "$title$number$($image.State)$percent
$($image.Old)
$($image.New)
"
+ }
+ ""
}
@"
$env:GAME rendering $since
-
- $env:GAME rendering $since
-
- | Base | This commit | Difference |
- $($rows -join "`n")
-
+ $env:GAME rendering
+ $counts $since
+ Base
This commit
Difference
+ $($cards -join "`n")
+
"@ | Set-Content "build/$env:GAME-rendering-report.html"
# A single file is uploaded unzipped, so its link opens it in the browser.
diff --git a/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_LaserTest.cpp b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_LaserTest.cpp
index 6992cc60739..8ddd131e37b 100644
--- a/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_LaserTest.cpp
+++ b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_LaserTest.cpp
@@ -33,7 +33,7 @@ TEST_F(RenderingTest, Laser)
// A red beam from west to east and a blue one that crosses it on the way up.
Vector3 eastWest[] = {Vector3(-6.0f, 0.0f, 0.0f), Vector3(6.0f, 0.0f, 0.0f)};
- Vector3 diagonal[] = {Vector3(-5.0f, -3.0f, 0.0f), Vector3(5.0f, 3.0f, 2.0f)};
+ Vector3 diagonal[] = {Vector3(-5.0f, -3.0f, 0.0f), Vector3(5.0f, 3.0f, 4.0f)};
SegmentedLineClass *red = NEW_REF(SegmentedLineClass, ());
red->Set_Points(2, eastWest);
red->Set_Color(Vector3(1.0f, 0.2f, 0.1f));
diff --git a/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_ParticlesTest.cpp b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_ParticlesTest.cpp
index 82884624766..f3deb752ba7 100644
--- a/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_ParticlesTest.cpp
+++ b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_ParticlesTest.cpp
@@ -37,7 +37,7 @@ TEST_F(RenderingTest, Particles)
return 0xFF000000 | brightness << 16 | brightness << 8 | brightness;
});
- // A ring of particles on the ground that turn from red to yellow and grow, around one in the middle.
+ // A ring of particles on the ground that turn from red to yellow and grow, around a larger one in the middle.
constexpr int Count = 13;
ShareBufferClass *locations = NEW_REF(ShareBufferClass, (Count, "ParticlesTest"));
ShareBufferClass *colors = NEW_REF(ShareBufferClass, (Count, "ParticlesTest"));
@@ -48,7 +48,7 @@ TEST_F(RenderingTest, Particles)
const float radius = i == 0 ? 0.0f : 4.0f;
locations->Get_Array()[i] = Vector3(radius * cosf(angle), radius * sinf(angle), 0.0f);
colors->Get_Array()[i] = Vector4(1.0f, static_cast(i) / Count, 0.1f, 1.0f);
- sizes->Get_Array()[i] = 1.0f + i * 0.1f;
+ sizes->Get_Array()[i] = i == 0 ? 2.0f : 1.0f + i * 0.1f;
}
// Set up as W3DParticleSystemManager sets up its batches.
diff --git a/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_Render2DBlendingTest.cpp b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_Render2DBlendingTest.cpp
index 0dbd8af4ee4..e9b0dbae6f9 100644
--- a/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_Render2DBlendingTest.cpp
+++ b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_Render2DBlendingTest.cpp
@@ -40,11 +40,11 @@ TEST_F(RenderingTest, Render2DBlending)
renderer.Add_Quad(RectClass(80.0f, 16.0f, 176.0f, 240.0f), 0xFF006000);
renderer.Render();
- // Blue multiplied with what is below, partly over the green.
+ // Orange multiplied with what is below, partly over the green.
renderer.Reset();
*renderer.Get_Shader() = ShaderClass::_PresetMultiplicative2DShader;
renderer.Enable_Texturing(false);
- renderer.Add_Quad(RectClass(144.0f, 16.0f, 240.0f, 240.0f), 0xFF4080FF);
+ renderer.Add_Quad(RectClass(144.0f, 16.0f, 240.0f, 240.0f), 0xFFFF8040);
renderer.Render();
capture();
diff --git a/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_Render2DShapesTest.cpp b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_Render2DShapesTest.cpp
index 7d2163d1eb3..21704399861 100644
--- a/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_Render2DShapesTest.cpp
+++ b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_Render2DShapesTest.cpp
@@ -27,9 +27,9 @@ TEST_F(RenderingTest, Render2DShapes)
renderer.Set_Coordinate_Range(RectClass(0.0f, 0.0f, Width, Height));
renderer.Enable_Texturing(false);
- // A blue rectangle with a white border, an orange outline, a green line and a red to blue gradient.
+ // A blue rectangle with a white border, a cyan outline, a green line and a red to blue gradient.
renderer.Add_Rect(RectClass(16.0f, 16.0f, 112.0f, 112.0f), 4.0f, 0xFFFFFFFF, 0xFF2040C0);
- renderer.Add_Outline(RectClass(144.0f, 16.0f, 240.0f, 112.0f), 2.0f, 0xFFFFC000);
+ renderer.Add_Outline(RectClass(144.0f, 16.0f, 240.0f, 112.0f), 2.0f, 0xFF00C0FF);
renderer.Add_Line(Vector2(16.0f, 136.0f), Vector2(240.0f, 184.0f), 3.0f, 0xFF40FF40);
renderer.Add_Quad_HGradient(RectClass(16.0f, 208.0f, 240.0f, 240.0f), 0xFFFF0000, 0xFF0000FF);
renderer.Render();
@@ -39,5 +39,5 @@ TEST_F(RenderingTest, Render2DShapes)
EXPECT_EQ(pixel(64, 64), 0x2040C0);
EXPECT_EQ(pixel(17, 64), 0xFFFFFF);
EXPECT_EQ(pixel(192, 64), 0x000000);
- EXPECT_EQ(pixel(145, 64), 0xFFC000);
+ EXPECT_EQ(pixel(145, 64), 0x00C0FF);
}
diff --git a/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_Render2DTextureTest.cpp b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_Render2DTextureTest.cpp
index f1899f6fb9b..a3fd6cb0c6d 100644
--- a/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_Render2DTextureTest.cpp
+++ b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_Render2DTextureTest.cpp
@@ -27,10 +27,10 @@ TEST_F(RenderingTest, Render2DTexture)
const unsigned int background = 0x204060;
clear(background);
- // A red and white checkerboard with a transparent top left quarter.
+ // A red and white checkerboard with a transparent top left corner.
const auto checkerboard = [](int x, int y) -> unsigned int
{
- const unsigned int alpha = (x < 4 && y < 4) ? 0x00000000 : 0xFF000000;
+ const unsigned int alpha = (x < 2 && y < 2) ? 0x00000000 : 0xFF000000;
return alpha | (((x ^ y) & 1) ? 0xFFFFFF : 0xC02020);
};
TextureClass *sharp = createTexture(8, 8, checkerboard);
diff --git a/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TextTest.cpp b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TextTest.cpp
index 565f42fd2a1..154fc889333 100644
--- a/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TextTest.cpp
+++ b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TextTest.cpp
@@ -33,7 +33,7 @@ TEST_F(RenderingTest, Text)
REF_PTR_RELEASE(font);
// The black shadow first, then the text over it.
- const WCHAR *text = L"Command & Conquer";
+ const WCHAR *text = L"Command + Conquer";
sentence.Build_Sentence(text, nullptr, nullptr);
sentence.Set_Location(Vector2(18.0f, 18.0f));
sentence.Draw_Sentence(0xFF000000);
diff --git a/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TracerTest.cpp b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TracerTest.cpp
index 5373116f34f..f3e552f1932 100644
--- a/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TracerTest.cpp
+++ b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TracerTest.cpp
@@ -22,9 +22,9 @@
// Tracers are 3D lines, which W3DTracerDraw fades out by lowering their opacity.
TEST_F(RenderingTest, Tracer)
{
- // A solid yellow tracer, crossed by a red one at half opacity.
+ // A solid yellow tracer, crossed by a blue one at half opacity.
Line3DClass *solid = NEW_REF(Line3DClass, (Vector3(-6.0f, -2.0f, 0.0f), Vector3(6.0f, 2.0f, 0.0f), 0.2f, 1.0f, 0.9f, 0.2f));
- Line3DClass *faded = NEW_REF(Line3DClass, (Vector3(-6.0f, 2.0f, 0.0f), Vector3(6.0f, -2.0f, 0.0f), 0.2f, 1.0f, 0.3f, 0.2f, 0.5f));
+ Line3DClass *faded = NEW_REF(Line3DClass, (Vector3(-6.0f, 2.0f, 0.0f), Vector3(6.0f, -2.0f, 0.0f), 0.2f, 0.2f, 0.4f, 1.0f, 0.5f));
render({solid, faded});
REF_PTR_RELEASE(solid);
diff --git a/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TriangleTest.cpp b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TriangleTest.cpp
index 3ca84150baa..48cc5878c40 100644
--- a/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TriangleTest.cpp
+++ b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TriangleTest.cpp
@@ -19,7 +19,7 @@
#include "RenderingTestFixture.h"
#include "WW3D2/render2d.h"
-// A triangle with a red, a green and a blue corner blends the corner colors across its area.
+// A triangle with a red, a green and a white corner blends the corner colors across its area.
TEST_F(RenderingTest, Triangle)
{
const unsigned int background = 0x102030;
@@ -36,10 +36,22 @@ TEST_F(RenderingTest, Triangle)
auto &colors = renderer.Get_Color_Array();
colors[0] = 0xFFFF0000;
colors[1] = 0xFF00FF00;
- colors[2] = 0xFF0000FF;
+ colors[2] = 0xFFFFFFFF;
renderer.Render();
capture();
// GPUs round the blended colors differently, so the exact colors are left to the image comparison of CI.
EXPECT_EQ(pixel(8, 8), background);
EXPECT_NE(pixel(128, 160), background);
+
+ // A smaller triangle in a second image, which shows a test that captures more than one image.
+ clear(background);
+ renderer.Reset();
+ renderer.Add_Tri(Vector2(128.0f, 96.0f), Vector2(176.0f, 192.0f), Vector2(80.0f, 192.0f), uv, uv, uv);
+ colors[0] = 0xFFFF0000;
+ colors[1] = 0xFF00FF00;
+ colors[2] = 0xFFFFFFFF;
+ renderer.Render();
+ capture();
+ EXPECT_EQ(pixel(8, 8), background);
+ EXPECT_NE(pixel(128, 160), background);
}