diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3fd5b0b952d..8fb05c29c1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +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 + # only require the vc6 build and compiling vc6 is much faster than win32, + # and the rendering checks only require the win32 build. build-generals-vc6: name: Build Generals${{ matrix.preset && '' }} needs: detect-changes @@ -237,3 +238,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..bedf78d8955 100644 --- a/.github/workflows/reusable-build-toolchain.yml +++ b/.github/workflows/reusable-build-toolchain.yml @@ -175,12 +175,27 @@ jobs: # The benchmarks are only built, because running them takes too long here. # Verbose prints the Google Test output, which lists the test cases of each executable. + # The rendering tests run in the rendering check of ci.yml instead. - 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 + # Keep this package separate from the game release artifacts. Only the plain + # win32 Release build is needed for the rendering check in ci.yml, whose runner has the CRT. + - 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 + # Kept longer on main, where pull requests download it to compare their rendering with. + 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..9320e2f4ee6 --- /dev/null +++ b/.github/workflows/reusable-check-rendering.yml @@ -0,0 +1,191 @@ +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: DXVK and lavapipe + runs-on: windows-2022 + timeout-minutes: 15 + env: + GAME: ${{ inputs.game }} + TEST_NAME: ${{ inputs.game == 'Generals' && 'g_googletest' || 'z_googletest' }} + # Google Test reads its flags from these as well. + GTEST_FILTER: RenderingTest.* + GTEST_FAIL_IF_NO_TEST_SELECTED: 1 + GTEST_OUTPUT: xml:results.xml + # The pinned Mesa version decides the rendering, not the runner image. + 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 + # The 32-bit DLLs that go beside each test exe. + 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 runner is elevated, so the Vulkan loader ignores VK_DRIVER_FILES. Register 32-bit lavapipe instead. + 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 }} + PR_HEAD: ${{ github.event.pull_request.head.sha }} + PR_HEAD_OWNER: ${{ github.event.pull_request.head.repo.owner.login }} + run: | + # Compare with the commit this change is based on: the previous main commit for a push, + # otherwise the merge base with main. Its GenCI run already built the rendering tests. + # A failed gh api call prints the error to stdout, so each call checks its exit code. + $repo = "repos/$env:GITHUB_REPOSITORY" + $base = switch ($env:GITHUB_EVENT_NAME) { + 'push' { $env:PUSH_BEFORE } + 'pull_request' { gh api "$repo/compare/${env:PR_BASE}...${env:PR_HEAD_OWNER}:${env:PR_HEAD}" --jq .merge_base_commit.sha } + default { gh api "$repo/compare/main...$env:GITHUB_SHA" --jq .merge_base_commit.sha } + } + if ($LASTEXITCODE) { $base = $null } + # A commit has no package when it was pushed together with later commits, or when its run + # skipped this game's build because nothing it builds from changed. The nearest ancestor + # with a package then 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) { "" } + } + # The same PNG encoder wrote both images, so equal pixels give equal files. + $images = foreach ($name in (Get-ChildItem build/rendering-tests*/*.png).Name | Sort-Object -Unique) { + $old = Get-Image build/rendering-tests-base/$name + $new = Get-Image build/rendering-tests/$name + $state = if (!$old) { 'new' } elseif (!$new) { 'removed' } elseif ($old -eq $new) { 'unchanged' } else { 'changed' } + [pscustomobject]@{ Name = $name; State = $state; Old = $old; New = $new } + } + $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" + } + @" + + + $env:GAME rendering $since + +

$env:GAME rendering $since

+ + + $($rows -join "`n") +
BaseThis commitDifference
+ "@ | Set-Content "build/$env:GAME-rendering-report.html" + + # A single file is uploaded unzipped, so its link opens it in the browser. + - 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 + + # Comes after the upload, so the message that a failed check opens with links to the report. + # Changes fail pull requests, so reviewers check in the report whether they are intended. + # On main they are warnings, because main becomes the base of later changes. + - 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..775d16d4126 100644 --- a/Tests/Google/Core/CMakeLists.txt +++ b/Tests/Google/Core/CMakeLists.txt @@ -15,6 +15,10 @@ 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_TriangleTest.cpp + Rendering/RenderingTestFixture.cpp + Rendering/RenderingTestFixture.h ) add_library(corei_googletest INTERFACE) @@ -26,6 +30,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..1f67e37de62 --- /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 DWORD color = 0x102030; + clear(color); + capture("clear.png"); + EXPECT_EQ(pixel(0, 0), color); + EXPECT_EQ(pixel(Width - 1, Height - 1), color); +} 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..c00478777a8 --- /dev/null +++ b/Tests/Google/Core/Rendering/EXAMPLE_TEMPORARY_TriangleTest.cpp @@ -0,0 +1,35 @@ +/* +** 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" + +// A triangle with a red, a green and a blue corner blends the corner colors across its area. +TEST_F(RenderingTest, Triangle) +{ + const DWORD background = 0x102030; + clear(background); + drawTriangles({ + {128.0f, 80.0f, 0xff0000}, + {192.0f, 200.0f, 0x00ff00}, + {64.0f, 200.0f, 0x0000ff}, + }); + capture("triangle.png"); + // 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..a18dcd89b6a --- /dev/null +++ b/Tests/Google/Core/Rendering/RenderingTestFixture.cpp @@ -0,0 +1,136 @@ +/* +** 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 + +IDirect3DDevice8 *RenderingTest::s_device = nullptr; +HWND RenderingTest::s_window = nullptr; +IDirect3D8 *RenderingTest::s_d3d = nullptr; +IDirect3DSurface8 *RenderingTest::s_target = nullptr; +IDirect3DSurface8 *RenderingTest::s_readback = nullptr; +DWORD RenderingTest::s_defaultState = 0; + +namespace +{ +template +void release(T *&object) +{ + if (object != nullptr) + { + object->Release(); + object = nullptr; + } +} +} // namespace + +// Runs once before the first test of the suite. When it fails, Google Test skips the tests of the suite. +void RenderingTest::SetUpTestSuite() +{ + s_window = CreateWindowA("STATIC", "RenderingTest", WS_OVERLAPPEDWINDOW, 0, 0, Width, Height, nullptr, nullptr, GetModuleHandle(nullptr), nullptr); + ASSERT_NE(s_window, nullptr); + s_d3d = Direct3DCreate8(D3D_SDK_VERSION); + ASSERT_NE(s_d3d, nullptr); + + D3DPRESENT_PARAMETERS params = {}; + params.BackBufferWidth = Width; + params.BackBufferHeight = Height; + params.BackBufferFormat = D3DFMT_X8R8G8B8; + params.BackBufferCount = 1; + params.SwapEffect = D3DSWAPEFFECT_DISCARD; + params.hDeviceWindow = s_window; + params.Windowed = TRUE; + ASSERT_HRESULT_SUCCEEDED(s_d3d->CreateDevice(0, D3DDEVTYPE_HAL, s_window, D3DCREATE_SOFTWARE_VERTEXPROCESSING, ¶ms, &s_device)); + ASSERT_HRESULT_SUCCEEDED(s_device->CreateRenderTarget(Width, Height, D3DFMT_X8R8G8B8, D3DMULTISAMPLE_NONE, FALSE, &s_target)); + ASSERT_HRESULT_SUCCEEDED(s_device->SetRenderTarget(s_target, nullptr)); + ASSERT_HRESULT_SUCCEEDED(s_device->CreateImageSurface(Width, Height, D3DFMT_X8R8G8B8, &s_readback)); + // No test has run yet, so this captures the default state of the device. + ASSERT_HRESULT_SUCCEEDED(s_device->CreateStateBlock(D3DSBT_ALL, &s_defaultState)); +} + +void RenderingTest::TearDownTestSuite() +{ + if (s_defaultState != 0) + { + s_device->DeleteStateBlock(s_defaultState); + s_defaultState = 0; + } + release(s_readback); + release(s_target); + release(s_device); + release(s_d3d); + if (s_window != nullptr) + { + DestroyWindow(s_window); + s_window = nullptr; + } +} + +// Undoes what the previous test changed. The state comes first, because it sets the viewport that Clear fills. +void RenderingTest::SetUp() +{ + ASSERT_HRESULT_SUCCEEDED(s_device->SetRenderTarget(s_target, nullptr)); + ASSERT_HRESULT_SUCCEEDED(s_device->ApplyStateBlock(s_defaultState)); + clear(0x000000); +} + +void RenderingTest::clear(DWORD color) +{ + EXPECT_HRESULT_SUCCEEDED(s_device->Clear(0, nullptr, D3DCLEAR_TARGET, color, 1.0f, 0)); +} + +void RenderingTest::drawTriangles(std::initializer_list vertices) +{ + struct TransformedVertex + { + float x, y, z, rhw; + DWORD color; + }; + std::vector transformed; + for (const Vertex &vertex : vertices) + { + transformed.push_back({vertex.x, vertex.y, 0.0f, 1.0f, vertex.color}); + } + ASSERT_HRESULT_SUCCEEDED(s_device->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE)); + ASSERT_HRESULT_SUCCEEDED(s_device->BeginScene()); + EXPECT_HRESULT_SUCCEEDED(s_device->DrawPrimitiveUP(D3DPT_TRIANGLELIST, transformed.size() / 3, transformed.data(), sizeof(TransformedVertex))); + ASSERT_HRESULT_SUCCEEDED(s_device->EndScene()); +} + +void RenderingTest::capture(const char *imageName) +{ + ASSERT_HRESULT_SUCCEEDED(s_device->CopyRects(s_target, nullptr, 0, s_readback, nullptr)); + D3DLOCKED_RECT locked = {}; + ASSERT_HRESULT_SUCCEEDED(s_readback->LockRect(&locked, nullptr, D3DLOCK_READONLY)); + std::vector rgb; + rgb.reserve(Width * Height * 3); + for (UINT y = 0; y < Height; ++y) + { + const DWORD *row = reinterpret_cast(static_cast(locked.pBits) + y * locked.Pitch); + for (UINT x = 0; x < Width; ++x) + { + const DWORD 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)); + } + } + ASSERT_HRESULT_SUCCEEDED(s_readback->UnlockRect()); + EXPECT_NE(stbi_write_png(imageName, Width, Height, 3, rgb.data(), Width * 3), 0) << imageName; +} diff --git a/Tests/Google/Core/Rendering/RenderingTestFixture.h b/Tests/Google/Core/Rendering/RenderingTestFixture.h new file mode 100644 index 00000000000..9e97802b0f2 --- /dev/null +++ b/Tests/Google/Core/Rendering/RenderingTestFixture.h @@ -0,0 +1,65 @@ +/* +** 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 + +// Each test renders with a real D3D8 device into an offscreen render target. The CTest step of CI skips these tests; +// its rendering check runs them on Windows under DXVK and Mesa lavapipe and compares the saved images with the base commit. +// Write each test as TEST_F(RenderingTest, Name) in a file of its own. The tests share one device, which is created +// once for the suite, and each test starts with the default device state and a black render target. +class RenderingTest : public ::testing::Test +{ +protected: + static constexpr UINT Width = 256; + static constexpr UINT Height = 256; + + // A position on the render target in pixels, and its color as 0xRRGGBB. + struct Vertex + { + float x, y; + DWORD color; + }; + + static void SetUpTestSuite(); + static void TearDownTestSuite(); + virtual void SetUp() override; + + static void clear(DWORD color); + // Draws a triangle list and blends the vertex colors across each triangle. + static void drawTriangles(std::initializer_list vertices); + // Saves the render target as a PNG for the CI report and keeps its pixels for pixel(). + void capture(const char *imageName); + // The color of a captured pixel as 0xRRGGBB. + DWORD pixel(UINT x, UINT y) const { return m_pixels[y * Width + x]; } + + static IDirect3DDevice8 *s_device; + +private: + std::vector m_pixels = std::vector(Width * Height); + static HWND s_window; + static IDirect3D8 *s_d3d; + static IDirect3DSurface8 *s_target; + static IDirect3DSurface8 *s_readback; + static DWORD s_defaultState; +};