Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 76 additions & 17 deletions .github/workflows/reusable-check-rendering.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,36 +131,95 @@ jobs:
function Get-Image($path) {
if (Test-Path $path) { "<img src=`"data:image/png;base64,$([Convert]::ToBase64String([IO.File]::ReadAllBytes((Resolve-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') { "<div class=diff>$($image.Old)$($image.New)</div>" }
"<tr><th>$($image.Name)<br>$($image.State)</th><td>$($image.Old)</td><td>$($image.New)</td><td>$diff</td></tr>"
$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 <test>_<n>.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) { "<h2>$($test.Name)</h2>" }
$number = if ($test.Images.Count -gt 1) { "<span class=number>$($image.Name -replace '^.*_(\d+)\.png$', '#$1')</span>" }
$diff, $percent = if ($image.State -eq 'changed') { ' class=diff', "<span class=percent>$($image.Mismatch)% of the pixels differ</span>" }
"<div class=`"image $($image.State)`"><div>$title$number<span class=`"state $($image.State)`">$($image.State)</span>$percent</div><div>$($image.Old)</div><div>$($image.New)</div><div$diff></div></div>"
}
"<section class=$($test.State)>$($rows -join '')</section>"
}
@"
<!doctype html>
<meta charset="utf-8">
<title>$env:GAME rendering $since</title>
<style>
body { font-family: sans-serif } th, td { padding: 8px; text-align: left; vertical-align: top }
.diff { position: relative; filter: brightness(1000) } .diff img + img { position: absolute; top: 0; left: 0; mix-blend-mode: difference }
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3/dist/index.global.js" integrity="sha384-2ql948lIdLcGEE0/qxNiudyTjgauA3RDJERu5xW75kFCvSl5a9odyQYCb6tEjnmB" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/resemblejs@5.0.0/resemble.js" integrity="sha384-DzcP9yqL1XF598WKUVQbQhW9mpjmq4x8k9J9T/JTfVJffHmQWHLdFXDfQPjdRQd6" crossorigin="anonymous"></script>
<style type="text/tailwindcss">
body { @apply bg-slate-100 p-8 font-sans text-slate-800; }
h1 { @apply text-2xl font-semibold; }
h1 + p { @apply mt-1 text-slate-500; }
.columns, .image { @apply grid gap-8; grid-template-columns: 12rem repeat(3, 256px); }
.columns { @apply mt-6 px-4 text-sm font-medium text-slate-500; }
section { @apply mt-3 w-fit rounded-xl bg-white p-4 shadow-sm; }
section.changed { @apply bg-rose-50 shadow-rose-200; }
.image + .image { @apply mt-4; }
.image.changed > :nth-child(3) img { @apply ring-2 ring-rose-400; }
h2 { @apply mb-2 font-mono text-sm font-medium; }
img { @apply rounded-md; }
.number { @apply mr-2 text-xs text-slate-500; }
.state { @apply inline-block rounded-full px-2 py-0.5 text-xs; }
.state.changed { @apply bg-rose-200 text-rose-900; }
.state.new { @apply bg-sky-100 text-sky-800; }
.state.removed { @apply bg-slate-200 text-slate-700; }
.state.unchanged { @apply bg-emerald-100 text-emerald-800; }
.percent { @apply mt-1 block text-xs text-slate-500; }
</style>
<h1>$env:GAME rendering $since</h1>
<table>
<tr><th></th><th>Base</th><th>This commit</th><th>Difference</th></tr>
$($rows -join "`n")
</table>
<h1>$env:GAME rendering</h1>
<p>$counts $since</p>
<div class=columns><div></div><div>Base</div><div>This commit</div><div>Difference</div></div>
$($cards -join "`n")
<script>
resemble.outputSettings({ transparency: 0.3 });
for (const cell of document.querySelectorAll('.diff')) {
const row = cell.parentElement;
const [base, current] = row.querySelectorAll('img');
resemble(base.src).compareTo(current.src).ignoreLess().onComplete(data => {
cell.innerHTML = '<img src="' + data.getImageDataUrl() + '">';
});
}
</script>
"@ | Set-Content "build/$env:GAME-rendering-report.html"

# A single file is uploaded unzipped, so its link opens it in the browser.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vector3> *locations = NEW_REF(ShareBufferClass<Vector3>, (Count, "ParticlesTest"));
ShareBufferClass<Vector4> *colors = NEW_REF(ShareBufferClass<Vector4>, (Count, "ParticlesTest"));
Expand All @@ -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<float>(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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TextTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TracerTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
16 changes: 14 additions & 2 deletions Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TriangleTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
Loading