Files
InstanceBrick--nightcap/tests/platform_graphics_foundation_test.cc
T
vii 79d0cdfb40 chore(merge): merge upstream komaruworld/mocktail main
Keep our captured-pointer handling in place of upstream's relative_mode
version. Keep upstream's motion coalescing guard on our relative flag.
2026-10-04 20:14:13 -05:00

333 lines
12 KiB
C++

#include <SDL3/SDL.h>
#include <gtest/gtest.h>
#include <cstdlib>
#include <string>
#include <vector>
#include "mocktail/graphics/android_vulkan_wsi_adapter.h"
#include "mocktail/graphics/angle_probe.h"
#include "mocktail/graphics/graphics_backend.h"
#include "mocktail/graphics/sdl_vulkan_wsi.h"
#include "mocktail/platform/sdl_application_metadata.h"
#include "mocktail/platform/sdl_event_converter.h"
#include "mocktail/platform/sdl_platform_runtime.h"
#include "mocktail/platform/sdl_window_icon.h"
namespace mocktail {
namespace {
using graphics::BackendCapability;
using graphics::BackendSelectionPolicy;
using graphics::CapabilityState;
using graphics::GraphicsBackendKind;
using graphics::HardwareAcceleration;
TEST(SdlApplicationMetadataTest, MatchesInstalledDesktopIdentity) {
Status status = platform::ConfigureSdlApplicationMetadata();
ASSERT_TRUE(status.ok()) << status.message();
EXPECT_STREQ(SDL_GetAppMetadataProperty(SDL_PROP_APP_METADATA_NAME_STRING),
platform::kMocktailApplicationName);
EXPECT_STREQ(
SDL_GetAppMetadataProperty(SDL_PROP_APP_METADATA_IDENTIFIER_STRING),
platform::kMocktailApplicationIdentifier);
EXPECT_STREQ(SDL_GetAppMetadataProperty(SDL_PROP_APP_METADATA_TYPE_STRING),
"game");
}
TEST(GraphicsBackendSelectionTest, FailsClosedWithoutRequestedBackend) {
BackendSelectionPolicy policy;
policy.requested = GraphicsBackendKind::kAngleVulkan;
const std::vector<BackendCapability> capabilities = {
{GraphicsBackendKind::kDirectVulkan, CapabilityState::kReady,
HardwareAcceleration::kHardware, "direct Vulkan ready"}};
auto result = graphics::SelectGraphicsBackend(policy, capabilities);
EXPECT_FALSE(result.status.ok());
EXPECT_EQ(result.backend, GraphicsBackendKind::kAngleVulkan);
}
TEST(GraphicsBackendSelectionTest, UsesFallbackOnlyWhenExplicitlyAllowed) {
BackendSelectionPolicy policy;
policy.requested = GraphicsBackendKind::kAngleVulkan;
policy.allow_fallback = true;
const std::vector<BackendCapability> capabilities = {
{GraphicsBackendKind::kDirectVulkan, CapabilityState::kReady,
HardwareAcceleration::kHardware, "direct Vulkan ready"}};
auto result = graphics::SelectGraphicsBackend(policy, capabilities);
ASSERT_TRUE(result.status.ok()) << result.status.message();
EXPECT_EQ(result.backend, GraphicsBackendKind::kDirectVulkan);
}
TEST(GraphicsBackendSelectionTest, RejectsUnvalidatedLoaderByDefault) {
const std::vector<BackendCapability> capabilities = {
{GraphicsBackendKind::kAngleVulkan, CapabilityState::kLoadable,
HardwareAcceleration::kHardware, "symbols exist"}};
auto result = graphics::SelectGraphicsBackend({}, capabilities);
EXPECT_FALSE(result.status.ok());
}
TEST(AngleProbeTest, RequiresExplicitPinnedLibraryPaths) {
auto capability = graphics::ProbeAngleVulkan({});
EXPECT_EQ(capability.state, CapabilityState::kUnavailable);
EXPECT_NE(capability.detail.find("required"), std::string::npos);
EXPECT_EQ(graphics::InspectAngleLibraries({}).state,
CapabilityState::kUnavailable);
}
TEST(AngleProbeTest, ValidatesConfiguredAngleVulkanDistribution) {
const char* egl = std::getenv("MOCKTAIL_TEST_ANGLE_EGL_LIBRARY");
const char* gles = std::getenv("MOCKTAIL_TEST_ANGLE_GLES_LIBRARY");
if (egl == nullptr || gles == nullptr) {
GTEST_SKIP() << "pinned ANGLE test paths were not provided";
}
graphics::AngleProbeOptions options;
options.egl_library_path = egl;
options.gles_library_path = gles;
const auto inspected = graphics::InspectAngleLibraries(options);
ASSERT_EQ(inspected.state, CapabilityState::kLoadable) << inspected.detail;
auto capability = graphics::ProbeAngleVulkan(options);
EXPECT_EQ(capability.state, CapabilityState::kReady) << capability.detail;
EXPECT_EQ(capability.acceleration, HardwareAcceleration::kHardware);
}
TEST(SdlPlatformRuntimeTest, RejectsInvalidWindowDimensionsBeforeSdlInit) {
auto runtime = platform::CreateSdlPlatformRuntime();
platform::WindowOptions options;
options.width = 0;
Status status = runtime->Initialize(options);
EXPECT_FALSE(status.ok());
EXPECT_EQ(status.code(), StatusCode::kInvalidArgument);
EXPECT_FALSE(runtime->IsInitialized());
}
TEST(SdlWindowIconTest, RejectsNullWindow) {
EXPECT_EQ(platform::ApplySdlWindowIcon(nullptr).code(),
StatusCode::kInvalidArgument);
}
TEST(SdlWindowIconTest, AppliesEmbeddedPngToRealWindow) {
const char* enabled = std::getenv("MOCKTAIL_TEST_SDL_WINDOW");
if (enabled == nullptr || std::string(enabled) != "1") {
GTEST_SKIP() << "real SDL window test was not explicitly enabled";
}
ASSERT_TRUE(SDL_Init(SDL_INIT_VIDEO)) << SDL_GetError();
SDL_Window* window =
SDL_CreateWindow("Mocktail icon test", 64, 64, SDL_WINDOW_HIDDEN);
ASSERT_NE(window, nullptr) << SDL_GetError();
// Setting a window icon needs compositor support: on Wayland that is the
// xdg_toplevel_icon_v1 protocol, which many compositors do not implement.
// Probe with a throwaway surface so this covers the embedded PNG wherever
// icons work, instead of failing on a platform limitation the callers of
// ApplySdlWindowIcon already treat as non-fatal.
SDL_Surface* probe = SDL_CreateSurface(1, 1, SDL_PIXELFORMAT_RGBA32);
ASSERT_NE(probe, nullptr) << SDL_GetError();
SDL_ClearError();
const bool icons_supported = SDL_SetWindowIcon(window, probe);
const std::string probe_error = icons_supported ? std::string() : SDL_GetError();
SDL_DestroySurface(probe);
if (!icons_supported) {
SDL_DestroyWindow(window);
SDL_Quit();
GTEST_SKIP() << "this video driver cannot set a window icon: "
<< probe_error;
}
const Status status = platform::ApplySdlWindowIcon(window);
EXPECT_TRUE(status.ok()) << status.message();
SDL_DestroyWindow(window);
SDL_Quit();
}
TEST(SdlEventConverterTest, ConvertsFocusWithoutOwningTheSdlQueue) {
SDL_Event source{};
source.type = SDL_EVENT_WINDOW_FOCUS_LOST;
source.window.timestamp = 123;
platform::PlatformEvent event;
auto* non_owning_window = reinterpret_cast<SDL_Window*>(0x1);
ASSERT_TRUE(platform::ConvertSdlEvent(non_owning_window, source, &event));
EXPECT_EQ(event.timestamp_ns, 123U);
const auto* focus = std::get_if<platform::WindowFocusEvent>(&event.payload);
ASSERT_NE(focus, nullptr);
EXPECT_FALSE(focus->focused);
source.type = SDL_EVENT_WINDOW_FOCUS_GAINED;
ASSERT_TRUE(platform::ConvertSdlEvent(non_owning_window, source, &event));
focus = std::get_if<platform::WindowFocusEvent>(&event.payload);
ASSERT_NE(focus, nullptr);
EXPECT_TRUE(focus->focused);
}
TEST(SdlEventConverterTest, PreservesHostWheelDirectionAndPrecision) {
SDL_Event source{};
source.type = SDL_EVENT_MOUSE_WHEEL;
source.wheel.mouse_x = 120.0f;
source.wheel.mouse_y = 240.0f;
platform::PlatformEvent event;
auto* non_owning_window = reinterpret_cast<SDL_Window*>(0x1);
for (const auto direction : {SDL_MOUSEWHEEL_NORMAL, SDL_MOUSEWHEEL_FLIPPED}) {
source.wheel.direction = direction;
source.wheel.x = 0.25f;
source.wheel.y = -1.5f;
ASSERT_TRUE(platform::ConvertSdlEvent(non_owning_window, source, &event));
const auto* wheel = std::get_if<platform::MouseWheelEvent>(&event.payload);
ASSERT_NE(wheel, nullptr);
EXPECT_FLOAT_EQ(wheel->delta_x, 0.25f);
EXPECT_FLOAT_EQ(wheel->delta_y, -1.5f);
EXPECT_FLOAT_EQ(wheel->mouse_x, 120.0f);
EXPECT_FLOAT_EQ(wheel->mouse_y, 240.0f);
}
}
TEST(SdlEventConverterTest, RejectsInvalidArgumentsAndUnsupportedEvents) {
SDL_Event source{};
source.type = SDL_EVENT_USER;
platform::PlatformEvent event;
auto* non_owning_window = reinterpret_cast<SDL_Window*>(0x1);
EXPECT_FALSE(platform::ConvertSdlEvent(nullptr, source, &event));
EXPECT_FALSE(platform::ConvertSdlEvent(non_owning_window, source, nullptr));
EXPECT_FALSE(platform::ConvertSdlEvent(non_owning_window, source, &event));
}
TEST(SdlPlatformRuntimeTest, CreatesConfiguredRealNativeWindow) {
const char* enabled = std::getenv("MOCKTAIL_TEST_SDL_WINDOW");
if (enabled == nullptr || std::string(enabled) != "1") {
GTEST_SKIP() << "real SDL window test was not explicitly enabled";
}
auto runtime = platform::CreateSdlPlatformRuntime();
platform::WindowOptions options;
options.title = "Mocktail foundation test";
options.width = 320;
options.height = 180;
options.initially_hidden = true;
options.require_native_window = true;
Status status = runtime->Initialize(options);
ASSERT_TRUE(status.ok()) << status.message();
EXPECT_TRUE(runtime->IsInitialized());
EXPECT_NE(runtime->GetNativeWindow().backend_window, nullptr);
EXPECT_TRUE(runtime->GetNativeWindow().HasPlatformHandle());
status = runtime->ShowWindow();
ASSERT_TRUE(status.ok()) << status.message();
if (std::getenv("MOCKTAIL_TEST_SDL_WINDOW_HOLD") != nullptr) {
SDL_Delay(3000);
}
SDL_Event source{};
source.type = SDL_EVENT_KEY_DOWN;
source.key.timestamp = 42;
source.key.down = true;
source.key.scancode = SDL_SCANCODE_A;
source.key.key = SDLK_A;
ASSERT_TRUE(SDL_PushEvent(&source));
bool received_key = false;
for (int i = 0; i < 32 && !received_key; ++i) {
platform::PlatformEvent event;
bool has_event = false;
status = runtime->PollEvent(&event, &has_event);
ASSERT_TRUE(status.ok()) << status.message();
if (!has_event) {
break;
}
const auto* key = std::get_if<platform::KeyEvent>(&event.payload);
if (key != nullptr && key->pressed &&
key->scancode == static_cast<std::uint32_t>(SDL_SCANCODE_A)) {
received_key = true;
EXPECT_EQ(event.timestamp_ns, 42u);
}
}
EXPECT_TRUE(received_key);
runtime->Shutdown();
EXPECT_FALSE(runtime->IsInitialized());
}
TEST(SdlVulkanWsiTest, DiscoversHostWsiWithoutCreatingRenderer) {
const char* enabled = std::getenv("MOCKTAIL_TEST_SDL_WINDOW");
if (enabled == nullptr || std::string(enabled) != "1") {
GTEST_SKIP() << "real SDL window test was not explicitly enabled";
}
auto runtime = platform::CreateSdlPlatformRuntime();
platform::WindowOptions options;
options.title = "Mocktail Vulkan WSI test";
options.width = 320;
options.height = 180;
options.surface_api = platform::WindowSurfaceApi::kDirectVulkan;
Status status = runtime->Initialize(options);
ASSERT_TRUE(status.ok()) << status.message();
graphics::SdlVulkanWsi wsi;
status = wsi.Initialize(runtime->GetNativeWindow());
ASSERT_TRUE(status.ok()) << status.message();
EXPECT_FALSE(wsi.required_instance_extensions().empty());
EXPECT_NE(wsi.GetInstanceProcAddress(), nullptr);
EXPECT_EQ(wsi.DescribeCapability().state, CapabilityState::kLoadable);
wsi.Shutdown();
runtime->Shutdown();
}
TEST(AndroidVulkanWsiAdapterTest, RewritesAndroidSurfaceToHostWsiExtensions) {
const std::vector<std::string> android_extensions = {
"VK_KHR_surface", graphics::kAndroidSurfaceExtension,
"VK_EXT_debug_utils",
"VK_KHR_surface"};
const std::vector<std::string> host_wsi_extensions = {
"VK_KHR_surface", "VK_KHR_xcb_surface"};
std::vector<std::string> translated;
Status status = graphics::TranslateAndroidVulkanInstanceExtensions(
android_extensions, host_wsi_extensions, &translated);
ASSERT_TRUE(status.ok()) << status.message();
EXPECT_EQ(translated,
(std::vector<std::string>{"VK_KHR_surface",
"VK_EXT_debug_utils",
"VK_KHR_xcb_surface"}));
}
TEST(AndroidVulkanWsiAdapterTest, RejectsSurfaceWithoutHostWsiExtensions) {
std::vector<std::string> translated;
Status status = graphics::TranslateAndroidVulkanInstanceExtensions(
{graphics::kAndroidSurfaceExtension}, {}, &translated);
EXPECT_FALSE(status.ok());
EXPECT_EQ(status.code(), StatusCode::kFailedPrecondition);
}
TEST(AndroidVulkanWsiAdapterTest, LeavesNonSurfaceExtensionsUnchanged) {
std::vector<std::string> translated;
Status status = graphics::TranslateAndroidVulkanInstanceExtensions(
{"VK_EXT_debug_utils"}, {"VK_KHR_surface", "VK_KHR_wayland_surface"},
&translated);
ASSERT_TRUE(status.ok()) << status.message();
EXPECT_EQ(translated,
(std::vector<std::string>{"VK_EXT_debug_utils"}));
}
TEST(AndroidVulkanWsiAdapterTest, AcceptsSuboptimalSwapchainImages) {
EXPECT_EQ(graphics::NormalizeAndroidSwapchainResult(VK_SUBOPTIMAL_KHR),
VK_SUCCESS);
EXPECT_EQ(graphics::NormalizeAndroidSwapchainResult(VK_SUCCESS),
VK_SUCCESS);
constexpr VkResult kOutOfDate = static_cast<VkResult>(-1000001004);
EXPECT_EQ(graphics::NormalizeAndroidSwapchainResult(kOutOfDate), kOutOfDate);
}
} // namespace
} // namespace mocktail