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)
" + } + "
$($rows -join '')
" } @" $env:GAME rendering $since - -

$env:GAME rendering $since

- - - $($rows -join "`n") -
BaseThis commitDifference
+

$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); }