From 4839ede0ed6edf3d0f94af83bc79e59b20340aef Mon Sep 17 00:00:00 2001 From: foxnne Date: Fri, 9 Oct 2026 11:50:27 -0500 Subject: [PATCH] test: a fizzy suite for claiming a transparent window with the GPU API The first of the suites for fizzy's patches, on SDL's own test harness. testautomation_fizzy_gpu.c checks "GPU: let each driver decide whether it can claim a transparent window": for the Metal and Vulkan drivers it creates an SDL_WINDOW_TRANSPARENT window, checks the flag stuck, claims it with SDL_ClaimWindowForGPUDevice(), clears it to a premultiplied 50% red and presents it. An opaque claim with each driver is the control. A test whose driver is unavailable is skipped, unless the runner's --require-gpu names it. testfizzy.c runs the fizzy suites the way testautomation runs SDL's (--filter, --seed, --list). It is named outside testautomation*.c so the CMake build's glob does not take a second main(); the suite itself is compiled into testautomation there, unreferenced (checked with zig cc -Wall -Wextra -Wmissing-prototypes -Wmissing-variable-declarations, not with a CMake build). fizzyedit/sdl_zig's `zig build test-fizzy` builds SDL_test and these. On release-3.4.16 the transparent Metal claim fails ("The GPU API doesn't support transparent windows"); on fizzy-3.4 all of it passes. Part of fizzyedit/fizzy#280. Co-Authored-By: Claude Opus 5.5 --- test/testautomation_fizzy.h | 23 ++++ test/testautomation_fizzy_gpu.c | 195 ++++++++++++++++++++++++++++++++ test/testfizzy.c | 127 +++++++++++++++++++++ 3 files changed, 345 insertions(+) create mode 100644 test/testautomation_fizzy.h create mode 100644 test/testautomation_fizzy_gpu.c create mode 100644 test/testfizzy.c diff --git a/test/testautomation_fizzy.h b/test/testautomation_fizzy.h new file mode 100644 index 0000000000000..bd8bb86fe0e0a --- /dev/null +++ b/test/testautomation_fizzy.h @@ -0,0 +1,23 @@ +/** + * Test suites for fizzy's patches to SDL. + * + * Each suite checks one patch on the fizzy-3.4 branch of fizzyedit/SDL and is + * expected to fail on the SDL release the branch is based on. testfizzy.c runs + * them. + */ + +#ifndef testautomation_fizzy_h_ +#define testautomation_fizzy_h_ + +#include + +/* Test collections */ +extern SDLTest_TestSuiteReference fizzyGPUTestSuite; + +/* Comma-separated GPU drivers ("metal,vulkan") a test must find: when one of + * them is missing the test fails instead of being skipped. Set by testfizzy's + * --require-gpu option, for CI runners that are known to have the driver. + */ +extern const char *fizzyGPURequiredDrivers; + +#endif /* testautomation_fizzy_h_ */ diff --git a/test/testautomation_fizzy_gpu.c b/test/testautomation_fizzy_gpu.c new file mode 100644 index 0000000000000..4b44bd63c61eb --- /dev/null +++ b/test/testautomation_fizzy_gpu.c @@ -0,0 +1,195 @@ +/** + * GPU test suite for fizzy's patches + * + * "GPU: let each driver decide whether it can claim a transparent window": + * SDL_ClaimWindowForGPUDevice() used to refuse every SDL_WINDOW_TRANSPARENT + * window, though the Metal and Vulkan drivers composite the swapchain's alpha + * for one. These tests claim a transparent window with each driver, clear it to + * a premultiplied 50% red and present it. The opaque claims are the control: a + * driver that cannot claim an opaque window says nothing about the patch. + */ +#include +#include +#include "testautomation_fizzy.h" + +const char *fizzyGPURequiredDrivers = NULL; + +/* Private helpers */ + +static bool GPUDriverIsRequired(const char *driver) +{ + const char *list = fizzyGPURequiredDrivers; + const size_t length = SDL_strlen(driver); + + while (list && *list) { + const char *end = SDL_strchr(list, ','); + const size_t item_length = end ? (size_t)(end - list) : SDL_strlen(list); + + if (item_length == length && SDL_strncasecmp(list, driver, length) == 0) { + return true; + } + list = end ? end + 1 : NULL; + } + return false; +} + +/** + * Creates a window with the given flags, claims it for a GPU device made with + * the given driver, then clears and presents one frame. + * + * Skips when the driver is unavailable, unless it is required. + */ +static int gpu_claimWindowWithDriver(const char *driver, SDL_WindowFlags flags) +{ + const SDL_GPUShaderFormat formats = SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_DXIL | SDL_GPU_SHADERFORMAT_MSL | SDL_GPU_SHADERFORMAT_METALLIB; + SDL_GPUDevice *device; + SDL_Window *window; + SDL_GPUCommandBuffer *cmdbuf; + SDL_GPUTexture *swapchain = NULL; + Uint32 w = 0, h = 0; + bool result; + + device = SDL_CreateGPUDevice(formats, false, driver); + SDLTest_AssertPass("Call to SDL_CreateGPUDevice(..., \"%s\")", driver); + if (!device) { + if (GPUDriverIsRequired(driver)) { + SDLTest_AssertCheck(false, "Validate that the required GPU driver '%s' is available, got error: %s", driver, SDL_GetError()); + return TEST_COMPLETED; + } + SDLTest_Log("GPU driver '%s' is not available (%s), skipping test", driver, SDL_GetError()); + return TEST_SKIPPED; + } + SDLTest_AssertCheck(SDL_strcmp(SDL_GetGPUDeviceDriver(device), driver) == 0, + "Validate the device's driver, expected: %s, got: %s", driver, SDL_GetGPUDeviceDriver(device)); + + window = SDL_CreateWindow("testautomation_fizzy_gpu", 320, 240, flags); + SDLTest_AssertPass("Call to SDL_CreateWindow('Title',320,240,%" SDL_PRIu64 ")", flags); + SDLTest_AssertCheck(window != NULL, "Validate that returned window is not NULL, got error: %s", window ? "none" : SDL_GetError()); + if (!window) { + SDL_DestroyGPUDevice(device); + return TEST_COMPLETED; + } + + /* If the video driver quietly dropped the flag, the claim tests nothing */ + if (flags & SDL_WINDOW_TRANSPARENT) { + SDLTest_AssertCheck((SDL_GetWindowFlags(window) & SDL_WINDOW_TRANSPARENT) != 0, "Validate that the window is transparent"); + } + + result = SDL_ClaimWindowForGPUDevice(device, window); + SDLTest_AssertPass("Call to SDL_ClaimWindowForGPUDevice()"); + SDLTest_AssertCheck(result, "Validate that the window was claimed, got error: %s", result ? "none" : SDL_GetError()); + + if (result) { + cmdbuf = SDL_AcquireGPUCommandBuffer(device); + SDLTest_AssertPass("Call to SDL_AcquireGPUCommandBuffer()"); + SDLTest_AssertCheck(cmdbuf != NULL, "Validate that returned command buffer is not NULL, got error: %s", cmdbuf ? "none" : SDL_GetError()); + if (cmdbuf) { + result = SDL_WaitAndAcquireGPUSwapchainTexture(cmdbuf, window, &swapchain, &w, &h); + SDLTest_AssertPass("Call to SDL_WaitAndAcquireGPUSwapchainTexture()"); + SDLTest_AssertCheck(result, "Validate that a swapchain texture was acquired, got error: %s", result ? "none" : SDL_GetError()); + + if (swapchain) { + SDL_GPUColorTargetInfo target; + SDL_GPURenderPass *pass; + + SDLTest_Log("Swapchain texture is %" SDL_PRIu32 "x%" SDL_PRIu32, w, h); + SDL_zero(target); + target.texture = swapchain; + target.clear_color.r = 0.5f; + target.clear_color.a = 0.5f; + target.load_op = SDL_GPU_LOADOP_CLEAR; + target.store_op = SDL_GPU_STOREOP_STORE; + pass = SDL_BeginGPURenderPass(cmdbuf, &target, 1, NULL); + SDLTest_AssertCheck(pass != NULL, "Validate that returned render pass is not NULL"); + if (pass) { + SDL_EndGPURenderPass(pass); + } + } else { + /* Allowed, for example while the window is occluded */ + SDLTest_Log("No swapchain texture this frame"); + } + + result = SDL_SubmitGPUCommandBuffer(cmdbuf); + SDLTest_AssertPass("Call to SDL_SubmitGPUCommandBuffer()"); + SDLTest_AssertCheck(result, "Validate that the command buffer was submitted, got error: %s", result ? "none" : SDL_GetError()); + } + + SDL_WaitForGPUIdle(device); + SDL_ReleaseWindowFromGPUDevice(device, window); + SDLTest_AssertPass("Call to SDL_ReleaseWindowFromGPUDevice()"); + } + + SDL_DestroyWindow(window); + SDL_DestroyGPUDevice(device); + return TEST_COMPLETED; +} + +/* Test case functions */ + +/** + * Claims an opaque window with the Metal driver (the control). + */ +static int SDLCALL gpu_claimOpaqueWindowMetal(void *arg) +{ + return gpu_claimWindowWithDriver("metal", 0); +} + +/** + * Claims a transparent window with the Metal driver. + */ +static int SDLCALL gpu_claimTransparentWindowMetal(void *arg) +{ + return gpu_claimWindowWithDriver("metal", SDL_WINDOW_TRANSPARENT); +} + +/** + * Claims an opaque window with the Vulkan driver (the control). + */ +static int SDLCALL gpu_claimOpaqueWindowVulkan(void *arg) +{ + return gpu_claimWindowWithDriver("vulkan", 0); +} + +/** + * Claims a transparent window with the Vulkan driver. + */ +static int SDLCALL gpu_claimTransparentWindowVulkan(void *arg) +{ + return gpu_claimWindowWithDriver("vulkan", SDL_WINDOW_TRANSPARENT); +} + +/* ================= Test References ================== */ + +/* GPU test cases */ +static const SDLTest_TestCaseReference gpuTestClaimOpaqueWindowMetal = { + gpu_claimOpaqueWindowMetal, "gpu_claimOpaqueWindowMetal", "Claims an opaque window with the Metal driver and presents a frame", TEST_ENABLED +}; + +static const SDLTest_TestCaseReference gpuTestClaimTransparentWindowMetal = { + gpu_claimTransparentWindowMetal, "gpu_claimTransparentWindowMetal", "Claims a transparent window with the Metal driver and presents a frame", TEST_ENABLED +}; + +static const SDLTest_TestCaseReference gpuTestClaimOpaqueWindowVulkan = { + gpu_claimOpaqueWindowVulkan, "gpu_claimOpaqueWindowVulkan", "Claims an opaque window with the Vulkan driver and presents a frame", TEST_ENABLED +}; + +static const SDLTest_TestCaseReference gpuTestClaimTransparentWindowVulkan = { + gpu_claimTransparentWindowVulkan, "gpu_claimTransparentWindowVulkan", "Claims a transparent window with the Vulkan driver and presents a frame", TEST_ENABLED +}; + +/* Sequence of GPU test cases */ +static const SDLTest_TestCaseReference *gpuTests[] = { + &gpuTestClaimOpaqueWindowMetal, + &gpuTestClaimTransparentWindowMetal, + &gpuTestClaimOpaqueWindowVulkan, + &gpuTestClaimTransparentWindowVulkan, + NULL +}; + +/* GPU test suite (global) */ +SDLTest_TestSuiteReference fizzyGPUTestSuite = { + "FizzyGPU", + NULL, + gpuTests, + NULL +}; diff --git a/test/testfizzy.c b/test/testfizzy.c new file mode 100644 index 0000000000000..42eb9bf3d93bd --- /dev/null +++ b/test/testfizzy.c @@ -0,0 +1,127 @@ +/* + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely. +*/ + +/* Runs the suites for fizzy's patches (testautomation_fizzy*.c), the way + * testautomation runs SDL's own: --filter, --seed and --list work the same. + */ + +#include + +#include +#include +#include + +#include "testautomation_fizzy.h" + +static SDLTest_CommonState *state; +static SDLTest_TestSuiteRunner *runner; + +/* All test suites */ +static SDLTest_TestSuiteReference *testSuites[] = { + &fizzyGPUTestSuite, + NULL +}; + +/* Call this instead of exit(), so we can clean up SDL: atexit() is evil. */ +static void +quit(int rc) +{ + SDLTest_DestroyTestSuiteRunner(runner); + SDLTest_CommonQuit(state); + /* Let 'main()' return normally */ + if (rc != 0) { + exit(rc); + } +} + +int main(int argc, char *argv[]) +{ + int result; + int i, done; + SDL_Event event; + int list = 0; + + /* Initialize test framework */ + state = SDLTest_CommonCreateState(argv, SDL_INIT_VIDEO); + if (!state) { + return 1; + } + + /* The suites create the windows they need */ + state->num_windows = 0; + + runner = SDLTest_CreateTestSuiteRunner(state, testSuites); + + /* Parse commandline */ + for (i = 1; i < argc;) { + int consumed; + + consumed = SDLTest_CommonArg(state, i); + if (consumed == 0) { + consumed = -1; + + if (SDL_strcasecmp(argv[i], "--list") == 0) { + consumed = 1; + list = 1; + } else if (SDL_strcasecmp(argv[i], "--require-gpu") == 0 && argv[i + 1]) { + consumed = 2; + fizzyGPURequiredDrivers = argv[i + 1]; + } + } + if (consumed < 0) { + static const char *options[] = { + "[--list]", + "[--require-gpu driver[,driver]]", + NULL }; + SDLTest_CommonLogUsage(state, argv[0], options); + quit(1); + } + + i += consumed; + } + + /* List all suites. */ + if (list) { + int suiteCounter; + for (suiteCounter = 0; testSuites[suiteCounter]; ++suiteCounter) { + int testCounter; + SDLTest_TestSuiteReference *testSuite = testSuites[suiteCounter]; + SDL_Log("Test suite: %s", testSuite->name); + for (testCounter = 0; testSuite->testCases[testCounter]; ++testCounter) { + const SDLTest_TestCaseReference *testCase = testSuite->testCases[testCounter]; + SDL_Log(" test: %s%s", testCase->name, testCase->enabled ? "" : " (disabled)"); + } + } + return 0; + } + + /* Initialize common state */ + if (!SDLTest_CommonInit(state)) { + quit(2); + } + + /* Call Harness */ + result = SDLTest_ExecuteTestSuiteRunner(runner); + + /* Empty event queue */ + done = 0; + for (i = 0; i < 100; i++) { + while (SDL_PollEvent(&event)) { + SDLTest_CommonEvent(state, &event, &done); + } + SDL_Delay(10); + } + + /* Shutdown everything */ + quit(0); + return result; +}