#include #include #include #include #include #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 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 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 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(0x1); ASSERT_TRUE(platform::ConvertSdlEvent(non_owning_window, source, &event)); EXPECT_EQ(event.timestamp_ns, 123U); const auto* focus = std::get_if(&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(&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(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(&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(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(&event.payload); if (key != nullptr && key->pressed && key->scancode == static_cast(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 android_extensions = { "VK_KHR_surface", graphics::kAndroidSurfaceExtension, "VK_EXT_debug_utils", "VK_KHR_surface"}; const std::vector host_wsi_extensions = { "VK_KHR_surface", "VK_KHR_xcb_surface"}; std::vector translated; Status status = graphics::TranslateAndroidVulkanInstanceExtensions( android_extensions, host_wsi_extensions, &translated); ASSERT_TRUE(status.ok()) << status.message(); EXPECT_EQ(translated, (std::vector{"VK_KHR_surface", "VK_EXT_debug_utils", "VK_KHR_xcb_surface"})); } TEST(AndroidVulkanWsiAdapterTest, RejectsSurfaceWithoutHostWsiExtensions) { std::vector translated; Status status = graphics::TranslateAndroidVulkanInstanceExtensions( {graphics::kAndroidSurfaceExtension}, {}, &translated); EXPECT_FALSE(status.ok()); EXPECT_EQ(status.code(), StatusCode::kFailedPrecondition); } TEST(AndroidVulkanWsiAdapterTest, LeavesNonSurfaceExtensionsUnchanged) { std::vector 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{"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(-1000001004); EXPECT_EQ(graphics::NormalizeAndroidSwapchainResult(kOutOfDate), kOutOfDate); } } // namespace } // namespace mocktail