From 1f0c09c33ef182cdee2a55dc94b6b9eb90e3cd1f Mon Sep 17 00:00:00 2001 From: stm <14291421+stephanmeesters@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:15:14 +0100 Subject: [PATCH 1/3] ci(test): Add rendering unit tests Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 22 ++ .../workflows/reusable-build-toolchain.yml | 13 + .../workflows/reusable-check-rendering.yml | 235 ++++++++++++++++++ Tests/Google/Core/CMakeLists.txt | 12 + .../Rendering/EXAMPLE_TEMPORARY_ClearTest.cpp | 29 +++ .../Rendering/EXAMPLE_TEMPORARY_LaserTest.cpp | 58 +++++ .../EXAMPLE_TEMPORARY_ParticlesTest.cpp | 76 ++++++ ...EXAMPLE_TEMPORARY_Render2DBlendingTest.cpp | 56 +++++ .../EXAMPLE_TEMPORARY_Render2DShapesTest.cpp | 43 ++++ .../EXAMPLE_TEMPORARY_Render2DTextureTest.cpp | 60 +++++ .../Rendering/EXAMPLE_TEMPORARY_TextTest.cpp | 54 ++++ .../EXAMPLE_TEMPORARY_TracerTest.cpp | 36 +++ .../EXAMPLE_TEMPORARY_TriangleTest.cpp | 45 ++++ .../Core/Rendering/RenderingTestFixture.cpp | 159 ++++++++++++ .../Core/Rendering/RenderingTestFixture.h | 56 +++++ 15 files changed, 954 insertions(+) create mode 100644 .github/workflows/reusable-check-rendering.yml create mode 100644 Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_ClearTest.cpp create mode 100644 Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_LaserTest.cpp create mode 100644 Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_ParticlesTest.cpp create mode 100644 Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_Render2DBlendingTest.cpp create mode 100644 Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_Render2DShapesTest.cpp create mode 100644 Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_Render2DTextureTest.cpp create mode 100644 Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TextTest.cpp create mode 100644 Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TracerTest.cpp create mode 100644 Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TriangleTest.cpp create mode 100644 Tests/Google/Core/Rendering/RenderingTestFixture.cpp create mode 100644 Tests/Google/Core/Rendering/RenderingTestFixture.h diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3fd5b0b952d..6aaae5184fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -237,3 +237,25 @@ jobs: userdata: "GeneralsReplays/GeneralsZH/1.04" preset: ${{ matrix.preset }} secrets: inherit + + renderingcheck-generals: + name: Rendering Check Generals + needs: [detect-changes, build-generals-win32] + if: ${{ github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.generals == 'true' || needs.detect-changes.outputs.shared == 'true' }} + permissions: + contents: read + actions: read + uses: ./.github/workflows/reusable-check-rendering.yml + with: + game: "Generals" + + renderingcheck-generalsmd: + name: Rendering Check GeneralsMD + needs: [detect-changes, build-generalsmd-win32] + if: ${{ github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.generalsmd == 'true' || needs.detect-changes.outputs.shared == 'true' }} + permissions: + contents: read + actions: read + uses: ./.github/workflows/reusable-check-rendering.yml + with: + game: "GeneralsMD" diff --git a/.github/workflows/reusable-build-toolchain.yml b/.github/workflows/reusable-build-toolchain.yml index 40edcc2d01d..3997a68eb81 100644 --- a/.github/workflows/reusable-build-toolchain.yml +++ b/.github/workflows/reusable-build-toolchain.yml @@ -178,9 +178,22 @@ jobs: - name: Test ${{ inputs.game }} with CTest Using ${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }} Preset if: ${{ inputs.tests }} shell: pwsh + env: + GTEST_FILTER: -RenderingTest.* run: | ctest --preset ${{ inputs.preset }} --verbose + # For the rendering check in ci.yml. + - name: Upload Rendering Test Executable + if: ${{ inputs.tests && inputs.preset == 'win32' }} + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: ${{ inputs.game }}-win32-rendering-tests + path: build/${{ inputs.preset }}/Tests/Google/Release/${{ inputs.game == 'Generals' && 'g' || 'z' }}_googletest.exe + # Pull requests compare their rendering with main. + retention-days: ${{ github.ref == 'refs/heads/main' && 90 || 7 }} + if-no-files-found: error + - name: Collect ${{ inputs.game }} ${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }} Artifact shell: pwsh run: | diff --git a/.github/workflows/reusable-check-rendering.yml b/.github/workflows/reusable-check-rendering.yml new file mode 100644 index 00000000000..e92427516c1 --- /dev/null +++ b/.github/workflows/reusable-check-rendering.yml @@ -0,0 +1,235 @@ +name: Check Rendering + +# WARNING: Do not declare permissions in this reusable workflow. +# Declare the required permissions in each calling job: +# contents: read - required for rendering checks. +# actions: read - required to download the rendering tests of the base commit. + +on: + workflow_call: + inputs: + game: + required: true + type: string + description: "Game to check (Generals, GeneralsMD)" + +jobs: + rendering: + name: win32 + runs-on: windows-2022 + timeout-minutes: 15 + env: + GAME: ${{ inputs.game }} + TEST_NAME: ${{ inputs.game == 'Generals' && 'g_googletest' || 'z_googletest' }} + GTEST_FILTER: RenderingTest.* + GTEST_FAIL_IF_NO_TEST_SELECTED: 1 + GTEST_OUTPUT: xml:results.xml + DXVK_VERSION: 2.6.2 + DXVK_SHA256: 17761876556afd55736cb895d184f5a1c55d43350f1b1e3b129f8d28706d7992 + MESA_VERSION: 26.2.4 + MESA_SHA256: 351fc8c8b695878ffb3eaa044b3ead08672a48b1a045e3c3e3975811df0f6695 + VULKAN_RUNTIME_VERSION: 1.4.363.0 + VULKAN_RUNTIME_SHA256: a25a927aa8b9f0371048f1861cf88ac3b9bc9b1fb332c42d897c8ab32695769a + steps: + - name: Checkout Code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Download MSVC Rendering Tests + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + with: + name: ${{ inputs.game }}-win32-rendering-tests + path: build/rendering-tests + + - name: Install DXVK and Lavapipe + run: | + $PSNativeCommandUseErrorActionPreference = $true + function Get-Checked($url, $out, $sha) { + curl.exe --fail --location --retry 3 --output $out $url + if ((Get-FileHash $out SHA256).Hash -ne $sha) { throw "SHA256 mismatch for $url" } + } + $dxvk = "dxvk-$env:DXVK_VERSION" + Get-Checked "https://github.com/doitsujin/dxvk/releases/download/v$env:DXVK_VERSION/$dxvk.tar.gz" build/dxvk.tar.gz $env:DXVK_SHA256 + Get-Checked "https://github.com/pal1000/mesa-dist-win/releases/download/$env:MESA_VERSION/mesa3d-$env:MESA_VERSION-release-msvc.7z" build/mesa.7z $env:MESA_SHA256 + Get-Checked "https://sdk.lunarg.com/sdk/download/$env:VULKAN_RUNTIME_VERSION/windows/vulkan-runtime-components.zip" build/vulkan.zip $env:VULKAN_RUNTIME_SHA256 + New-Item -ItemType Directory build/runtime | Out-Null + tar -C build/runtime -xf build/dxvk.tar.gz --strip-components=2 "$dxvk/x32/d3d8.dll" "$dxvk/x32/d3d9.dll" + 7z e build/vulkan.zip -obuild/runtime "VulkanRT-X64-$env:VULKAN_RUNTIME_VERSION-Components/x86/vulkan-1.dll" + 7z e build/mesa.7z -obuild/lavapipe x86/vulkan_lvp.dll x86/lvp_icd.x86.json + # The elevated runner ignores VK_DRIVER_FILES. + reg add HKLM\SOFTWARE\WOW6432Node\Khronos\Vulkan\Drivers /v "$PWD\build\lavapipe\lvp_icd.x86.json" /t REG_DWORD /d 0 /f + + - name: Run Rendering Google Tests + working-directory: build/rendering-tests + timeout-minutes: 5 + run: | + Copy-Item ../runtime/* . + & "./$env:TEST_NAME.exe" + + - name: Download Base Rendering Tests + id: base + if: always() + env: + GH_TOKEN: ${{ github.token }} + PUSH_BEFORE: ${{ github.event.before }} + PR_BASE: ${{ github.event.pull_request.base.sha }} + run: | + # The previous main commit for a push, the main commit that a pull request merges into, or else the merge base. + $repo = "repos/$env:GITHUB_REPOSITORY" + $base = switch ($env:GITHUB_EVENT_NAME) { + 'push' { $env:PUSH_BEFORE } + 'pull_request' { $env:PR_BASE } + default { gh api "$repo/compare/main...$env:GITHUB_SHA" --jq .merge_base_commit.sha } + } + if ($LASTEXITCODE) { $base = $null } + # Without a package, the nearest ancestor that has one renders the same. + $commits = @() + if ($base) { + $commits = gh api "$repo/commits?sha=$base&per_page=20" --jq '.[].sha' + if ($LASTEXITCODE) { $commits = @() } + } + foreach ($commit in $commits) { + $runs = gh api "$repo/actions/runs?head_sha=$commit" --jq ".workflow_runs[] | select(.id != $env:GITHUB_RUN_ID) | .id" + if ($LASTEXITCODE) { continue } + foreach ($run in $runs) { + gh run download $run --repo $env:GITHUB_REPOSITORY --name "${env:GAME}-win32-rendering-tests" --dir build/rendering-tests-base + if ($LASTEXITCODE -eq 0) { + "Using the rendering tests of run $run for commit $commit (base commit $base)" + "sha=$commit" >> $env:GITHUB_OUTPUT + exit 0 + } + Remove-Item -Recurse -Force -ErrorAction Ignore build/rendering-tests-base + } + } + "::notice title=Rendering::No rendering tests found for base commit $($base ?? 'unknown'), so the images are not compared" + exit 0 + + - name: Run Base Rendering Google Tests + if: ${{ always() && steps.base.outputs.sha }} + working-directory: build/rendering-tests-base + timeout-minutes: 5 + run: | + Copy-Item ../runtime/* . + & "./$env:TEST_NAME.exe" + if ($LASTEXITCODE) { "::warning title=Rendering::The base rendering tests failed, so only the images they wrote are compared" } + exit 0 + + - name: Compare Rendering with Base + id: compare + if: always() + env: + BASE_SHA: ${{ steps.base.outputs.sha }} + run: | + $since = if ($env:BASE_SHA) { "since base $($env:BASE_SHA.Substring(0, 10))" } else { '(no base found)' } + function Get-Image($path) { + if (Test-Path $path) { "" } + } + # A pixel changes when a color channel differs by more than 16 levels, as in the ignoreLess mode of Resemble.js. + 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++ } + } + # Rounded up, so that a single pixel shows. + [Math]::Ceiling(10000 * $count / ($old.Length / 4)) / 100 + } + $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 + $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 + $counts = ($images | Group-Object State | ForEach-Object { "$($_.Count) $($_.Name)" }) -join ', ' + "### $env:GAME Rendering`n`n$counts $since.`n" >> $env:GITHUB_STEP_SUMMARY + # One self-contained page with a card per test, changes 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

+

$counts $since

+
Base
This commit
Difference
+ $($cards -join "`n") + + "@ | Set-Content "build/$env:GAME-rendering-report.html" + + - name: Upload Rendering Report + id: report + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + path: build/${{ inputs.game }}-rendering-report.html + archive: false + retention-days: 7 + if-no-files-found: warn + + # Changes fail pull requests and are warnings on main. + - name: Check Rendering Changes + if: always() + env: + CHANGES: ${{ steps.compare.outputs.changes }} + MESSAGE: ${{ steps.compare.outputs.message }} + REPORT_URL: ${{ steps.report.outputs.artifact-url }} + run: | + "[Open the $env:GAME rendering report]($env:REPORT_URL)" >> $env:GITHUB_STEP_SUMMARY + $level = if (!$env:CHANGES) { 'notice' } elseif ($env:GITHUB_EVENT_NAME -eq 'pull_request') { 'error' } else { 'warning' } + "::$level title=$env:GAME rendering::$env:MESSAGE. Report: $env:REPORT_URL" + if ($level -eq 'error') { exit 1 } diff --git a/Tests/Google/Core/CMakeLists.txt b/Tests/Google/Core/CMakeLists.txt index 68c37d4b96d..c4be45b6eda 100644 --- a/Tests/Google/Core/CMakeLists.txt +++ b/Tests/Google/Core/CMakeLists.txt @@ -15,6 +15,17 @@ set(COREI_GOOGLETEST_SRC Libraries/WWVegas/WWMath/WWMathKeys.h Libraries/WWVegas/WWMath/WWMathTest.cpp Libraries/WWVegas/WWMath/WWMathTestCase.h + Rendering/EXAMPLE_TEMPORARY_ClearTest.cpp + Rendering/EXAMPLE_TEMPORARY_LaserTest.cpp + Rendering/EXAMPLE_TEMPORARY_ParticlesTest.cpp + Rendering/EXAMPLE_TEMPORARY_Render2DBlendingTest.cpp + Rendering/EXAMPLE_TEMPORARY_Render2DShapesTest.cpp + Rendering/EXAMPLE_TEMPORARY_Render2DTextureTest.cpp + Rendering/EXAMPLE_TEMPORARY_TextTest.cpp + Rendering/EXAMPLE_TEMPORARY_TracerTest.cpp + Rendering/EXAMPLE_TEMPORARY_TriangleTest.cpp + Rendering/RenderingTestFixture.cpp + Rendering/RenderingTestFixture.h ) add_library(corei_googletest INTERFACE) @@ -26,6 +37,7 @@ target_link_libraries(corei_googletest INTERFACE depsi_googletest GTest::gtest imm32 + stb vfw32 winmm ) diff --git a/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_ClearTest.cpp b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_ClearTest.cpp new file mode 100644 index 00000000000..b68150a68e2 --- /dev/null +++ b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_ClearTest.cpp @@ -0,0 +1,29 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "RenderingTestFixture.h" + +// Clearing the render target fills it with the clear color from corner to corner. +TEST_F(RenderingTest, Clear) +{ + const unsigned int color = 0x102030; + clear(color); + capture(); + EXPECT_EQ(pixel(0, 0), color); + EXPECT_EQ(pixel(Width - 1, Height - 1), color); +} diff --git a/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_LaserTest.cpp b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_LaserTest.cpp new file mode 100644 index 00000000000..6992cc60739 --- /dev/null +++ b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_LaserTest.cpp @@ -0,0 +1,58 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "RenderingTestFixture.h" +#include "WW3D2/segline.h" +#include "WW3D2/texture.h" + +// Lasers are segmented lines with a glowing texture across their width, which W3DLaserDraw adds to the scene with +// additive blending, so crossing beams add up. +TEST_F(RenderingTest, Laser) +{ + // Brightest along the middle of the beam, dark at its edges. + TextureClass *glow = createTexture(32, 32, [](int x, int) -> unsigned int + { + const unsigned int brightness = 255 - abs(2 * x - 31) * 255 / 31; + return 0xFF000000 | brightness << 16 | brightness << 8 | brightness; + }); + + // 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)}; + SegmentedLineClass *red = NEW_REF(SegmentedLineClass, ()); + red->Set_Points(2, eastWest); + red->Set_Color(Vector3(1.0f, 0.2f, 0.1f)); + SegmentedLineClass *blue = NEW_REF(SegmentedLineClass, ()); + blue->Set_Points(2, diagonal); + blue->Set_Color(Vector3(0.2f, 0.4f, 1.0f)); + for (SegmentedLineClass *laser : {red, blue}) + { + laser->Set_Texture(glow); + laser->Set_Shader(ShaderClass::_PresetAdditiveShader); + laser->Set_Width(0.8f); + } + + render({red, blue}); + REF_PTR_RELEASE(red); + REF_PTR_RELEASE(blue); + REF_PTR_RELEASE(glow); + + capture(); + EXPECT_EQ(pixel(8, 8), 0x000000); + EXPECT_NE(pixel(Width / 2, Height / 2), 0x000000); +} diff --git a/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_ParticlesTest.cpp b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_ParticlesTest.cpp new file mode 100644 index 00000000000..82884624766 --- /dev/null +++ b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_ParticlesTest.cpp @@ -0,0 +1,76 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "RenderingTestFixture.h" +#include "WW3D2/camera.h" +#include "WW3D2/pointgr.h" +#include "WW3D2/rinfo.h" +#include "WW3D2/texture.h" +#include "WW3D2/ww3d.h" +#include "WWLib/sharebuf.h" + +// Particles are camera facing quads with a soft texture, which W3DParticleSystemManager draws in batches with +// PointGroupClass and additive blending. +TEST_F(RenderingTest, Particles) +{ + // A round puff that fades out towards its edge. + TextureClass *puff = createTexture(32, 32, [](int x, int y) -> unsigned int + { + const float dx = (x - 15.5f) / 16.0f; + const float dy = (y - 15.5f) / 16.0f; + const unsigned int brightness = static_cast(255.0f * max(0.0f, 1.0f - sqrtf(dx * dx + dy * dy))); + 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. + constexpr int Count = 13; + ShareBufferClass *locations = NEW_REF(ShareBufferClass, (Count, "ParticlesTest")); + ShareBufferClass *colors = NEW_REF(ShareBufferClass, (Count, "ParticlesTest")); + ShareBufferClass *sizes = NEW_REF(ShareBufferClass, (Count, "ParticlesTest")); + for (int i = 0; i < Count; ++i) + { + const float angle = i * WWMATH_PI * 2.0f / (Count - 1); + 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; + } + + // Set up as W3DParticleSystemManager sets up its batches. + PointGroupClass particles; + particles.Set_Texture(puff); + particles.Set_Shader(ShaderClass::_PresetAdditiveSpriteShader); + particles.Set_Flag(PointGroupClass::TRANSFORM, true); + particles.Set_Point_Mode(PointGroupClass::QUADS); + particles.Set_Billboard(true); + particles.Set_Arrays(locations, colors, nullptr, sizes, nullptr, nullptr, Count); + + // The game draws particles from its scene, so this renders them as WW3D::Render renders a scene. + RenderInfoClass rinfo(*s_camera); + s_camera->Apply(); + particles.Render(rinfo); + WW3D::Flush(rinfo); + REF_PTR_RELEASE(locations); + REF_PTR_RELEASE(colors); + REF_PTR_RELEASE(sizes); + REF_PTR_RELEASE(puff); + + capture(); + EXPECT_EQ(pixel(8, 8), 0x000000); + EXPECT_NE(pixel(Width / 2, Height / 2), 0x000000); +} diff --git a/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_Render2DBlendingTest.cpp b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_Render2DBlendingTest.cpp new file mode 100644 index 00000000000..0dbd8af4ee4 --- /dev/null +++ b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_Render2DBlendingTest.cpp @@ -0,0 +1,56 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "RenderingTestFixture.h" +#include "WW3D2/render2d.h" + +// The UI and the effects blend over what is below them: alpha fades windows in, additive blending makes glows and +// multiplicative blending darkens for shadows. +TEST_F(RenderingTest, Render2DBlending) +{ + const unsigned int background = 0x808080; + clear(background); + Render2DClass renderer; + renderer.Set_Coordinate_Range(RectClass(0.0f, 0.0f, Width, Height)); + renderer.Enable_Texturing(false); + + // Half transparent red. + renderer.Enable_Alpha(true); + renderer.Add_Quad(RectClass(16.0f, 16.0f, 112.0f, 240.0f), 0x80FF0000); + renderer.Render(); + + // Green added on top, partly over the red. + renderer.Reset(); + renderer.Enable_Additive(true); + 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. + 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.Render(); + + capture(); + // The exact blended colors depend on how the GPU rounds, so they are left to the image comparison of CI. + EXPECT_EQ(pixel(8, 8), background); + EXPECT_NE(pixel(48, 128), background); + EXPECT_NE(pixel(128, 128), background); + EXPECT_NE(pixel(208, 128), background); +} diff --git a/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_Render2DShapesTest.cpp b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_Render2DShapesTest.cpp new file mode 100644 index 00000000000..7d2163d1eb3 --- /dev/null +++ b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_Render2DShapesTest.cpp @@ -0,0 +1,43 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "RenderingTestFixture.h" +#include "WW3D2/render2d.h" + +// The UI draws filled and outlined rectangles, lines and gradients with Render2DClass, as W3DDisplay::drawFillRect +// and its siblings do. +TEST_F(RenderingTest, Render2DShapes) +{ + Render2DClass renderer; + 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. + 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_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(); + + capture(); + // The fill and the border of the rectangle, and the inside and the edge of the outline. + EXPECT_EQ(pixel(64, 64), 0x2040C0); + EXPECT_EQ(pixel(17, 64), 0xFFFFFF); + EXPECT_EQ(pixel(192, 64), 0x000000); + EXPECT_EQ(pixel(145, 64), 0xFFC000); +} diff --git a/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_Render2DTextureTest.cpp b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_Render2DTextureTest.cpp new file mode 100644 index 00000000000..f1899f6fb9b --- /dev/null +++ b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_Render2DTextureTest.cpp @@ -0,0 +1,60 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "RenderingTestFixture.h" +#include "WW3D2/render2d.h" +#include "WW3D2/texture.h" + +// The UI draws its images as textured quads with Render2DClass. Scaled up, point filtering keeps the texels sharp while +// linear filtering blends them, and transparent texels let the background show through. +TEST_F(RenderingTest, Render2DTexture) +{ + const unsigned int background = 0x204060; + clear(background); + + // A red and white checkerboard with a transparent top left quarter. + const auto checkerboard = [](int x, int y) -> unsigned int + { + const unsigned int alpha = (x < 4 && y < 4) ? 0x00000000 : 0xFF000000; + return alpha | (((x ^ y) & 1) ? 0xFFFFFF : 0xC02020); + }; + TextureClass *sharp = createTexture(8, 8, checkerboard); + sharp->Get_Filter().Set_Min_Filter(TextureFilterClass::FILTER_TYPE_NONE); + sharp->Get_Filter().Set_Mag_Filter(TextureFilterClass::FILTER_TYPE_NONE); + TextureClass *smooth = createTexture(8, 8, checkerboard); + smooth->Get_Filter().Set_Min_Filter(TextureFilterClass::FILTER_TYPE_FAST); + smooth->Get_Filter().Set_Mag_Filter(TextureFilterClass::FILTER_TYPE_FAST); + + // The sharp image on the left and the smooth one on the right, both scaled from 8 to 96 pixels. + Render2DClass renderer(sharp); + renderer.Set_Coordinate_Range(RectClass(0.0f, 0.0f, Width, Height)); + renderer.Add_Quad(RectClass(16.0f, 64.0f, 112.0f, 160.0f)); + renderer.Render(); + renderer.Reset(); + renderer.Set_Texture(smooth); + renderer.Add_Quad(RectClass(144.0f, 64.0f, 240.0f, 160.0f)); + renderer.Render(); + REF_PTR_RELEASE(sharp); + REF_PTR_RELEASE(smooth); + + capture(); + // Each texel covers 12 by 12 pixels. These are the centers of texel (5, 5), which is red, and texel (1, 1), + // which is transparent. + EXPECT_EQ(pixel(16 + 5 * 12 + 6, 64 + 5 * 12 + 6), 0xC02020); + EXPECT_EQ(pixel(16 + 1 * 12 + 6, 64 + 1 * 12 + 6), background); +} diff --git a/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TextTest.cpp b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TextTest.cpp new file mode 100644 index 00000000000..565f42fd2a1 --- /dev/null +++ b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TextTest.cpp @@ -0,0 +1,54 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "RenderingTestFixture.h" +#include "WW3D2/render2dsentence.h" + +// The UI draws its text with Render2DSentenceClass from glyphs that GDI renders, as W3DDisplayString does, +// including the drop shadow below the text. +TEST_F(RenderingTest, Text) +{ + const unsigned int background = 0x204060; + clear(background); + + FontCharsClass *font = NEW_REF(FontCharsClass, ()); + ASSERT_TRUE(font->Initialize_GDI_Font("Arial", 16, true)); + Render2DSentenceClass sentence; + sentence.Set_Font(font); + REF_PTR_RELEASE(font); + + // The black shadow first, then the text over it. + const WCHAR *text = L"Command & Conquer"; + sentence.Build_Sentence(text, nullptr, nullptr); + sentence.Set_Location(Vector2(18.0f, 18.0f)); + sentence.Draw_Sentence(0xFF000000); + sentence.Set_Location(Vector2(16.0f, 16.0f)); + sentence.Draw_Sentence(0xFFFFC000); + sentence.Render(); + const Vector2 extents = sentence.Get_Text_Extents(text); + + capture(); + EXPECT_GT(extents.X, 100.0f); + // GDI and its fonts differ between systems, so only some text color is expected in the middle row of the text. + bool found = false; + for (int x = 16; x < 16 + extents.X; ++x) + { + found |= pixel(x, 16 + static_cast(extents.Y) / 2) == 0xFFC000; + } + EXPECT_TRUE(found); +} diff --git a/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TracerTest.cpp b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TracerTest.cpp new file mode 100644 index 00000000000..5373116f34f --- /dev/null +++ b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TracerTest.cpp @@ -0,0 +1,36 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "RenderingTestFixture.h" +#include "WW3D2/line3d.h" + +// 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. + 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)); + + render({solid, faded}); + REF_PTR_RELEASE(solid); + REF_PTR_RELEASE(faded); + + capture(); + EXPECT_EQ(pixel(8, 8), 0x000000); + EXPECT_NE(pixel(Width / 2, Height / 2), 0x000000); +} diff --git a/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TriangleTest.cpp b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TriangleTest.cpp new file mode 100644 index 00000000000..3ca84150baa --- /dev/null +++ b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TriangleTest.cpp @@ -0,0 +1,45 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#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. +TEST_F(RenderingTest, Triangle) +{ + const unsigned int background = 0x102030; + clear(background); + Render2DClass renderer; + renderer.Set_Coordinate_Range(RectClass(0.0f, 0.0f, Width, Height)); + renderer.Enable_Texturing(false); + // Draws the triangle whichever way round its corners go. + renderer.Get_Shader()->Set_Cull_Mode(ShaderClass::CULL_MODE_DISABLE); + const Vector2 uv(0.0f, 0.0f); + renderer.Add_Tri(Vector2(128.0f, 32.0f), Vector2(224.0f, 224.0f), Vector2(32.0f, 224.0f), uv, uv, uv); + // Add_Tri gives all corners one color. The color array is a DynamicVectorClass in Zero Hour and a + // SimpleDynVecClass in Generals. + auto &colors = renderer.Get_Color_Array(); + colors[0] = 0xFFFF0000; + colors[1] = 0xFF00FF00; + colors[2] = 0xFF0000FF; + 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); +} diff --git a/Tests/Google/Core/Rendering/RenderingTestFixture.cpp b/Tests/Google/Core/Rendering/RenderingTestFixture.cpp new file mode 100644 index 00000000000..559c1391ac5 --- /dev/null +++ b/Tests/Google/Core/Rendering/RenderingTestFixture.cpp @@ -0,0 +1,159 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "RenderingTestFixture.h" +#include +#include +#include + +#include "WW3D2/Renderer.h" +#include "WW3D2/camera.h" +#include "WW3D2/dx8wrapper.h" +#include "WW3D2/scene.h" +#include "WW3D2/surfaceclass.h" +#include "WW3D2/texture.h" +#include "WW3D2/ww3d.h" +#include "WWMath/wwmath.h" + +#ifdef RTS_ZEROHOUR +static const char ImageFolder[] = "z_RenderTest"; +#else +static const char ImageFolder[] = "g_RenderTest"; +#endif + +CameraClass *RenderingTest::s_camera = nullptr; +HWND RenderingTest::s_window = nullptr; + +void RenderingTest::SetUpTestSuite() +{ + // Borderless, so that the window is as large as the back buffer. + s_window = CreateWindowA("STATIC", "RenderingTest", WS_POPUP, 0, 0, Width, Height, nullptr, nullptr, GetModuleHandle(nullptr), nullptr); + ASSERT_NE(s_window, nullptr); + WWMath::Init(); + ASSERT_EQ(WW3D::Init(s_window), WW3D_ERROR_OK); + WW3D::Set_Screen_UV_Bias(TRUE); + // Thumbnails need an asset manager. + WW3D::Set_Thumbnail_Enabled(false); + ASSERT_EQ(WW3D::Set_Render_Device(0, Width, Height, 32, TRUE), WW3D_ERROR_OK); + + s_camera = NEW_REF(CameraClass, ()); + Matrix3D transform; + transform.Look_At(Vector3(0.0f, -12.0f, 6.0f), Vector3(0.0f, 0.0f, 0.0f), 0.0f); + s_camera->Set_Transform(transform); + s_camera->Set_View_Plane(DEG_TO_RADF(50.0f), DEG_TO_RADF(50.0f)); + s_camera->Set_Clip_Planes(1.0f, 100.0f); + + std::filesystem::create_directory(ImageFolder); +} + +void RenderingTest::TearDownTestSuite() +{ + REF_PTR_RELEASE(s_camera); + WW3D::Shutdown(); + WWMath::Shutdown(); + if (s_window != nullptr) + { + DestroyWindow(s_window); + s_window = nullptr; + } +} + +void RenderingTest::SetUp() +{ + ASSERT_EQ(WW3D::Begin_Render(true, true, Vector3(0.0f, 0.0f, 0.0f)), WW3D_ERROR_OK); + m_rendering = true; +} + +void RenderingTest::TearDown() +{ + if (m_rendering) + { + WW3D::End_Render(false); + } +} + +void RenderingTest::clear(unsigned int color) +{ + const Vector3 rgb((color >> 16 & 0xFF) / 255.0f, (color >> 8 & 0xFF) / 255.0f, (color & 0xFF) / 255.0f); + Renderer::Clear(true, false, rgb); +} + +void RenderingTest::render(std::initializer_list objects) +{ + SimpleSceneClass *scene = NEW_REF(SimpleSceneClass, ()); + scene->Set_Ambient_Light(Vector3(1.0f, 1.0f, 1.0f)); + for (RenderObjClass *object : objects) + { + scene->Add_Render_Object(object); + } + EXPECT_EQ(WW3D::Render(scene, s_camera), WW3D_ERROR_OK); + REF_PTR_RELEASE(scene); +} + +TextureClass *RenderingTest::createTexture(int width, int height, const std::function &color) +{ + SurfaceClass *surface = NEW_REF(SurfaceClass, (width, height, WW3D_FORMAT_A8R8G8B8)); + int pitch = 0; + unsigned char *bits = static_cast(surface->Lock(&pitch)); + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + reinterpret_cast(bits + y * pitch)[x] = color(x, y); + } + } + surface->Unlock(); + TextureClass *texture = NEW_REF(TextureClass, (surface, MIP_LEVELS_1)); + REF_PTR_RELEASE(surface); + return texture; +} + +// DX8 specific until Renderer can read the back buffer. +void RenderingTest::capture() +{ + ASSERT_TRUE(m_rendering); + const std::string imageName = (std::filesystem::path(ImageFolder) / ::testing::UnitTest::GetInstance()->current_test_info()->name()).string() + "_" + std::to_string(m_captures++) + ".png"; + WW3D::End_Render(false); + m_rendering = false; + SurfaceClass *backBuffer = DX8Wrapper::_Get_DX8_Back_Buffer(); + ASSERT_NE(backBuffer, nullptr); + SurfaceClass *copy = NEW_REF(SurfaceClass, (Width, Height, Renderer::Get_Back_Buffer_Format())); + DX8Wrapper::_Copy_DX8_Rects(backBuffer->Peek_D3D_Surface(), nullptr, 0, copy->Peek_D3D_Surface(), nullptr); + REF_PTR_RELEASE(backBuffer); + int pitch = 0; + const unsigned char *bits = static_cast(copy->Lock(&pitch)); + std::vector rgb; + rgb.reserve(Width * Height * 3); + for (int y = 0; y < Height; ++y) + { + const unsigned int *row = reinterpret_cast(bits + y * pitch); + for (int x = 0; x < Width; ++x) + { + const unsigned int color = row[x] & 0x00ffffff; + m_pixels[y * Width + x] = color; + rgb.push_back(static_cast(color >> 16)); + rgb.push_back(static_cast(color >> 8)); + rgb.push_back(static_cast(color)); + } + } + copy->Unlock(); + REF_PTR_RELEASE(copy); + EXPECT_NE(stbi_write_png(imageName.c_str(), Width, Height, 3, rgb.data(), Width * 3), 0) << imageName; + ASSERT_EQ(WW3D::Begin_Render(true, true, Vector3(0.0f, 0.0f, 0.0f)), WW3D_ERROR_OK); + m_rendering = true; +} diff --git a/Tests/Google/Core/Rendering/RenderingTestFixture.h b/Tests/Google/Core/Rendering/RenderingTestFixture.h new file mode 100644 index 00000000000..0a53dc352f5 --- /dev/null +++ b/Tests/Google/Core/Rendering/RenderingTestFixture.h @@ -0,0 +1,56 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#pragma once + +#include +#include +#include +#include +#include + +class CameraClass; +class RenderObjClass; +class TextureClass; + +// Each test renders with WW3D into a black frame and captures it for the rendering check of CI. +class RenderingTest : public ::testing::Test +{ +protected: + static constexpr int Width = 256; + static constexpr int Height = 256; + + static void SetUpTestSuite(); + static void TearDownTestSuite(); + virtual void SetUp() override; + virtual void TearDown() override; + + static void clear(unsigned int color); + static void render(std::initializer_list objects); + static TextureClass *createTexture(int width, int height, const std::function &color); // color(x, y) returns 0xAARRGGBB. + void capture(); // Saves the frame as _.png and starts a new one. + unsigned int pixel(int x, int y) const { return m_pixels[y * Width + x]; } // 0xRRGGBB of the last capture. + + static CameraClass *s_camera; // Looks at the origin from the south, with z up. + +private: + std::vector m_pixels = std::vector(Width * Height); + int m_captures = 0; + bool m_rendering = false; + static HWND s_window; +}; From 178d336455ef6edc0670b77fcbd5c8b565045bcf Mon Sep 17 00:00:00 2001 From: stm <14291421+stephanmeesters@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:43:09 +0100 Subject: [PATCH 2/3] ci: Remove the unused checkout from the rendering check Co-Authored-By: Claude Opus 5.5 --- .github/workflows/reusable-check-rendering.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/reusable-check-rendering.yml b/.github/workflows/reusable-check-rendering.yml index e92427516c1..3b95e6e2564 100644 --- a/.github/workflows/reusable-check-rendering.yml +++ b/.github/workflows/reusable-check-rendering.yml @@ -31,9 +31,6 @@ jobs: VULKAN_RUNTIME_VERSION: 1.4.363.0 VULKAN_RUNTIME_SHA256: a25a927aa8b9f0371048f1861cf88ac3b9bc9b1fb332c42d897c8ab32695769a steps: - - name: Checkout Code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Download MSVC Rendering Tests uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: From 60330f9d15d3f86e4b0004cc3e5fb488859b20d2 Mon Sep 17 00:00:00 2001 From: stm <14291421+stephanmeesters@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:43:16 +0100 Subject: [PATCH 3/3] ci: Build only the Zero Hour win32 preset on this fork Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 175 ++------------------------------------- 1 file changed, 7 insertions(+), 168 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6aaae5184fa..5e5dde73010 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,106 +59,8 @@ jobs: echo "- Shared: ${{ steps.filter.outputs.shared == 'true' && '✅' || '❌' }}" >> $GITHUB_STEP_SUMMARY # Note the build jobs are split into two jobs for vc6 and win32 because the replay checks - # only require the vc6 build and compiling vc6 is much faster than win32 - build-generals-vc6: - name: Build Generals${{ matrix.preset && '' }} - needs: detect-changes - if: ${{ github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.generals == 'true' || needs.detect-changes.outputs.shared == 'true' }} - permissions: - contents: read - strategy: - matrix: - include: - - preset: "vc6" - tools: true - extras: true - - preset: "vc6-profile" - tools: true - extras: true - - preset: "vc6-debug" - tools: true - extras: true - - preset: "vc6-releaselog" - tools: true - extras: true - fail-fast: false - uses: ./.github/workflows/reusable-build-toolchain.yml - with: - game: "Generals" - preset: ${{ matrix.preset }} - tools: ${{ matrix.tools }} - extras: ${{ matrix.extras }} - secrets: inherit - - build-generals-win32: - name: Build Generals${{ matrix.preset && '' }} - needs: detect-changes - if: ${{ github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.generals == 'true' || needs.detect-changes.outputs.shared == 'true' }} - permissions: - contents: read - packages: write - strategy: - matrix: - include: - - preset: "win32" - tools: true - extras: true - - preset: "win32-profile" - tools: true - extras: true - - preset: "win32-debug" - tools: true - extras: true - - preset: "win32-vcpkg" - tools: true - extras: true - - preset: "win32-vcpkg-profile" - tools: true - extras: true - - preset: "win32-vcpkg-debug" - tools: true - extras: true - fail-fast: false - uses: ./.github/workflows/reusable-build-toolchain.yml - with: - game: "Generals" - preset: ${{ matrix.preset }} - tools: ${{ matrix.tools }} - extras: ${{ matrix.extras }} - tests: true - benchmarks: ${{ matrix.preset == 'win32' }} - secrets: inherit - - build-generalsmd-vc6: - name: Build GeneralsMD${{ matrix.preset && '' }} - needs: detect-changes - if: ${{ github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.generalsmd == 'true' || needs.detect-changes.outputs.shared == 'true' }} - permissions: - contents: read - strategy: - matrix: - include: - - preset: "vc6" - tools: true - extras: true - - preset: "vc6-profile" - tools: true - extras: true - - preset: "vc6-debug" - tools: true - extras: true - - preset: "vc6-releaselog" - tools: true - extras: true - fail-fast: false - uses: ./.github/workflows/reusable-build-toolchain.yml - with: - game: "GeneralsMD" - preset: ${{ matrix.preset }} - tools: ${{ matrix.tools }} - extras: ${{ matrix.extras }} - secrets: inherit - + # only require the vc6 build and compiling vc6 is much faster than win32, + # and the rendering checks only require the win32 build. build-generalsmd-win32: name: Build GeneralsMD${{ matrix.preset && '' }} needs: detect-changes @@ -169,25 +71,11 @@ jobs: strategy: matrix: include: + # Only the build that the rendering check needs, which keeps the runs of this fork short. - preset: "win32" + # The artifact step collects the core tools, so they are built as well. tools: true - extras: true - - preset: "win32-profile" - tools: true - extras: true - - preset: "win32-debug" - tools: true - extras: true - - preset: "win32-vcpkg" - tools: true - extras: true - ffmpeg: true - - preset: "win32-vcpkg-profile" - tools: true - extras: true - - preset: "win32-vcpkg-debug" - tools: true - extras: true + extras: false fail-fast: false uses: ./.github/workflows/reusable-build-toolchain.yml with: @@ -195,60 +83,11 @@ jobs: preset: ${{ matrix.preset }} tools: ${{ matrix.tools }} extras: ${{ matrix.extras }} - ffmpeg: ${{ matrix.ffmpeg || false}} + ffmpeg: false tests: true - benchmarks: ${{ matrix.preset == 'win32' }} + benchmarks: false secrets: inherit - replaycheck-generals: - name: Replay Check Generals${{ matrix.preset && '' }} - needs: [detect-changes, build-generals-vc6] - if: ${{ github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.generals == 'true' || needs.detect-changes.outputs.shared == 'true' }} - permissions: - contents: read - strategy: - matrix: - include: - - preset: "vc6+t+e" - - preset: "vc6-releaselog+t+e" # optimized build with logging and crashing enabled should be compatible, so we test that here. - fail-fast: false - uses: ./.github/workflows/reusable-check-replays.yml - with: - game: "Generals" - userdata: "GeneralsReplays/Generals/1.08" - preset: ${{ matrix.preset }} - secrets: inherit - - replaycheck-generalsmd: - name: Replay Check GeneralsMD${{ matrix.preset && '' }} - needs: [detect-changes, build-generalsmd-vc6] - if: ${{ github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.generalsmd == 'true' || needs.detect-changes.outputs.shared == 'true' }} - permissions: - contents: read - strategy: - matrix: - include: - - preset: "vc6+t+e" - - preset: "vc6-releaselog+t+e" # optimized build with logging and crashing enabled should be compatible, so we test that here. - fail-fast: false - uses: ./.github/workflows/reusable-check-replays.yml - with: - game: "GeneralsMD" - userdata: "GeneralsReplays/GeneralsZH/1.04" - preset: ${{ matrix.preset }} - secrets: inherit - - renderingcheck-generals: - name: Rendering Check Generals - needs: [detect-changes, build-generals-win32] - if: ${{ github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.generals == 'true' || needs.detect-changes.outputs.shared == 'true' }} - permissions: - contents: read - actions: read - uses: ./.github/workflows/reusable-check-rendering.yml - with: - game: "Generals" - renderingcheck-generalsmd: name: Rendering Check GeneralsMD needs: [detect-changes, build-generalsmd-win32]