From 25016d3e08011ec1e7cd6eef94a14bff6a1eebbc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 25 Oct 2026 12:18:31 +0000 Subject: [PATCH 1/4] Load Vulkan at runtime through DynamicLibrary. Graphics no longer links the Vulkan loader. DynamicLibrary is the only LoadLibrary/dlopen caller. VulkanLoader fills the VK_NO_PROTOTYPES function table, and a missing runtime exits with code 2. Co-authored-by: Ben Hoffman --- .github/workflows/build.yml | 38 ++- FlingEngine/Core/CMakeLists.txt | 3 + FlingEngine/Core/inc/DynamicLibrary.h | 70 +++++ FlingEngine/Core/src/DynamicLibrary.cpp | 217 +++++++++++++++ FlingEngine/Graphics/CMakeLists.txt | 16 +- FlingEngine/Graphics/src/DesktopWindow.cpp | 2 + FlingEngine/Graphics/src/DesktopWindow.h | 3 + FlingEngine/Graphics/src/FlingVulkan.h | 36 ++- FlingEngine/Graphics/src/Instance.cpp | 3 + FlingEngine/Graphics/src/LogicalDevice.cpp | 3 + FlingEngine/Graphics/src/VulkanApp.cpp | 37 +++ FlingEngine/Graphics/src/VulkanFunctions.inl | 167 +++++++++++ FlingEngine/Graphics/src/VulkanLoader.cpp | 159 +++++++++++ FlingEngine/Graphics/src/VulkanLoader.h | 37 +++ FlingTests/CMakeLists.txt | 7 + FlingTests/Core/DynamicLibraryTests.cpp | 105 +++++++ FlingTests/VulkanLoad/CheckVulkanCalls.cmake | 72 +++++ .../VulkanLoad/ExpectNoVulkanImport.cmake | 112 ++++++++ .../VulkanLoad/RunWithoutVulkanLoader.sh | 50 ++++ FlingTests/VulkanLoad/pe_imports.py | 126 +++++++++ Plans/vulkan-loading.md | 260 +++++++++--------- Sandbox/CMakeLists.txt | 9 + Skills/architecture.md | 14 +- Skills/testing.md | 6 + 24 files changed, 1398 insertions(+), 154 deletions(-) create mode 100644 FlingEngine/Core/inc/DynamicLibrary.h create mode 100644 FlingEngine/Core/src/DynamicLibrary.cpp create mode 100644 FlingEngine/Graphics/src/VulkanFunctions.inl create mode 100644 FlingEngine/Graphics/src/VulkanLoader.cpp create mode 100644 FlingEngine/Graphics/src/VulkanLoader.h create mode 100644 FlingTests/Core/DynamicLibraryTests.cpp create mode 100644 FlingTests/VulkanLoad/CheckVulkanCalls.cmake create mode 100644 FlingTests/VulkanLoad/ExpectNoVulkanImport.cmake create mode 100755 FlingTests/VulkanLoad/RunWithoutVulkanLoader.sh create mode 100644 FlingTests/VulkanLoad/pe_imports.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 335efde9..56635d73 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -64,7 +64,7 @@ jobs: run: | sudo apt-get update -qq sudo apt-get install -y --no-install-recommends \ - ninja-build pkg-config \ + ninja-build pkg-config binutils \ xorg-dev libx11-dev libwayland-dev libxkbcommon-dev libglu1-mesa-dev \ libvulkan-dev @@ -208,6 +208,42 @@ jobs: if: runner.os == 'Windows' run: ctest --test-dir build -C Debug --output-on-failure --no-tests=error --timeout 300 + # Build with the Vulkan SDK, then run Sandbox where libvulkan.so.1 cannot be + # loaded. The windowed run must exit 2 and log the missing-runtime line. + # --null-render must still exit 0, because it does not open the loader. + # The mount namespace hides the host copies. It does not uninstall packages + # and it does not change the other jobs. + no-vulkan-runtime: + name: Linux no Vulkan runtime + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v5 + with: + submodules: recursive + + - name: Install Linux dependencies + run: | + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends \ + ninja-build pkg-config \ + xorg-dev libx11-dev libwayland-dev libxkbcommon-dev libglu1-mesa-dev \ + libvulkan-dev + + - name: Build and install Catch2 + run: | + cmake -DFLING_INIT_CONFIGS=Release \ + -DFLING_INIT_SKIP_SUBMODULES=ON -DFLING_INIT_SKIP_CONFIGURE=ON \ + -P scripts/init.cmake + + - name: Configure + run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + + - name: Build Sandbox + run: cmake --build build --target Sandbox --parallel + + - name: Run with the loader hidden + run: bash FlingTests/VulkanLoad/RunWithoutVulkanLoader.sh build/Sandbox/bin/Sandbox + # Doc-comment conventions for first-party sources (see Skills/coding-style.md / issue #166). # clang-format cannot enforce comment *content*; this script does. comment-style: diff --git a/FlingEngine/Core/CMakeLists.txt b/FlingEngine/Core/CMakeLists.txt index 9c308249..799a11c6 100644 --- a/FlingEngine/Core/CMakeLists.txt +++ b/FlingEngine/Core/CMakeLists.txt @@ -11,6 +11,9 @@ fling_add_module(Core FlingExt::cereal ) +# DynamicLibrary is the only dlopen caller. Older glibc keeps it in libdl. +target_link_libraries(Core PRIVATE ${CMAKE_DL_LIBS}) + # Feature flags are read from Core headers (FlingTypes.h) and from every module # that links Core, so they live here and propagate to everything. target_compile_definitions(Core PUBLIC diff --git a/FlingEngine/Core/inc/DynamicLibrary.h b/FlingEngine/Core/inc/DynamicLibrary.h new file mode 100644 index 00000000..33fc9b1b --- /dev/null +++ b/FlingEngine/Core/inc/DynamicLibrary.h @@ -0,0 +1,70 @@ +#pragma once + +#include "CoreAPI.h" + +#include +#include +#include + +namespace Fling { + /** + * Load and unload one shared library. + * + * This is the only Fling type that calls LoadLibrary or dlopen. + * Move a DynamicLibrary. Do not copy it. + */ + class CORE_API DynamicLibrary { + public: + DynamicLibrary() = default; + + /** Unload the library when this object is still open. */ + ~DynamicLibrary(); + + DynamicLibrary(const DynamicLibrary&) = delete; + DynamicLibrary& operator=(const DynamicLibrary&) = delete; + + DynamicLibrary(DynamicLibrary&& t_Other) noexcept; + DynamicLibrary& operator=(DynamicLibrary&& t_Other) noexcept; + + /** + * Open the shared library t_Name. + * + * An empty object means that the load failed. When t_Error is not + * null, write the platform message there on failure. Clear t_Error + * on success. + */ + static DynamicLibrary Open(std::string_view t_Name, std::string* t_Error = nullptr); + + /** + * Open the first library in t_Names that loads. + * + * When every name fails and t_Error is not null, write the last + * platform message. + */ + static DynamicLibrary OpenFirst(std::span t_Names, std::string* t_Error = nullptr); + + /** True when a library is open. */ + explicit operator bool() const; + + /** + * Return the address of t_Name. + * Return null when the library is closed or the symbol is missing. + */ + void* Symbol(std::string_view t_Name) const; + + /** + * Return the address of t_Name as T_Fn. + * Return null when the library is closed or the symbol is missing. + */ + template + T_Fn SymbolAs(std::string_view t_Name) const { + return reinterpret_cast(Symbol(t_Name)); + } + + /** Close the library. A second call does nothing. */ + void Close(); + + private: + void* m_Handle = nullptr; + }; +} // namespace Fling diff --git a/FlingEngine/Core/src/DynamicLibrary.cpp b/FlingEngine/Core/src/DynamicLibrary.cpp new file mode 100644 index 00000000..84a97a15 --- /dev/null +++ b/FlingEngine/Core/src/DynamicLibrary.cpp @@ -0,0 +1,217 @@ +#include "DynamicLibrary.h" + +#include "Platform.h" + +#include +#include +#include +#include + +#if FLING_WINDOWS + +namespace { + std::wstring Utf8ToWide(std::string_view t_Text) { + if (t_Text.empty() || t_Text.size() > static_cast(std::numeric_limits::max())) { + return {}; + } + + const int Size = static_cast(t_Text.size()); + const int Needed = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, t_Text.data(), Size, nullptr, 0); + if (Needed <= 0) { + return {}; + } + + std::wstring Wide(static_cast(Needed), L'\0'); + const int Written = + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, t_Text.data(), Size, Wide.data(), Needed); + if (Written <= 0) { + return {}; + } + return Wide; + } + + std::string WideToUtf8(std::wstring_view t_Text) { + if (t_Text.empty() || t_Text.size() > static_cast(std::numeric_limits::max())) { + return {}; + } + + const int Size = static_cast(t_Text.size()); + const int Needed = WideCharToMultiByte(CP_UTF8, 0, t_Text.data(), Size, nullptr, 0, nullptr, nullptr); + if (Needed <= 0) { + return {}; + } + + std::string Text(static_cast(Needed), '\0'); + WideCharToMultiByte(CP_UTF8, 0, t_Text.data(), Size, Text.data(), Needed, nullptr, nullptr); + return Text; + } + + std::string WindowsErrorMessage(DWORD t_Code) { + wchar_t* Buffer = nullptr; + const DWORD Flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS; + const DWORD Length = FormatMessageW(Flags, + nullptr, + t_Code, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + reinterpret_cast(&Buffer), + 0, + nullptr); + + std::string Message; + if (Length > 0 && Buffer != nullptr) { + Message = WideToUtf8(std::wstring_view(Buffer, Length)); + while (!Message.empty() && (Message.back() == '\n' || Message.back() == '\r' || Message.back() == ' ')) { + Message.pop_back(); + } + } + if (Buffer != nullptr) { + LocalFree(Buffer); + } + if (Message.empty()) { + Message = "LoadLibraryW failed (code " + std::to_string(t_Code) + ")"; + } + return Message; + } +} // namespace + +#elif FLING_LINUX + +#include + +namespace { + std::string LinuxErrorMessage() { + const char* Error = dlerror(); + if (Error == nullptr || Error[0] == '\0') { + return "dlopen failed"; + } + return Error; + } +} // namespace + +#else + +#error DynamicLibrary supports Windows and Linux only + +#endif + +namespace { + void WriteError(std::string* t_Error, std::string t_Message) { + if (t_Error != nullptr) { + *t_Error = std::move(t_Message); + } + } + + void ClearError(std::string* t_Error) { + if (t_Error != nullptr) { + t_Error->clear(); + } + } +} // namespace + +namespace Fling { + DynamicLibrary::~DynamicLibrary() { + Close(); + } + + DynamicLibrary::DynamicLibrary(DynamicLibrary&& t_Other) noexcept : m_Handle(t_Other.m_Handle) { + t_Other.m_Handle = nullptr; + } + + DynamicLibrary& DynamicLibrary::operator=(DynamicLibrary&& t_Other) noexcept { + if (this != &t_Other) { + Close(); + m_Handle = t_Other.m_Handle; + t_Other.m_Handle = nullptr; + } + return *this; + } + + DynamicLibrary DynamicLibrary::Open(std::string_view t_Name, std::string* t_Error) { + if (t_Name.empty()) { + WriteError(t_Error, "The shared library name is empty"); + return {}; + } + +#if FLING_WINDOWS + const std::wstring WideName = Utf8ToWide(t_Name); + if (WideName.empty()) { + WriteError(t_Error, "The shared library name is not valid UTF-8"); + return {}; + } + + HMODULE Handle = LoadLibraryW(WideName.c_str()); + if (Handle == nullptr) { + WriteError(t_Error, WindowsErrorMessage(GetLastError())); + return {}; + } + + DynamicLibrary Library; + Library.m_Handle = static_cast(Handle); + ClearError(t_Error); + return Library; + +#elif FLING_LINUX + const std::string Name(t_Name); + dlerror(); + void* Handle = dlopen(Name.c_str(), RTLD_NOW | RTLD_LOCAL); + if (Handle == nullptr) { + WriteError(t_Error, LinuxErrorMessage()); + return {}; + } + + DynamicLibrary Library; + Library.m_Handle = Handle; + ClearError(t_Error); + return Library; +#endif + } + + DynamicLibrary DynamicLibrary::OpenFirst(std::span t_Names, std::string* t_Error) { + for (const std::string_view Name : t_Names) { + DynamicLibrary Library = Open(Name, t_Error); + if (Library) { + return Library; + } + } + + if (t_Error != nullptr && t_Error->empty()) { + *t_Error = "No shared library name was given"; + } + return {}; + } + + DynamicLibrary::operator bool() const { + return m_Handle != nullptr; + } + + void* DynamicLibrary::Symbol(std::string_view t_Name) const { + if (m_Handle == nullptr || t_Name.empty()) { + return nullptr; + } + + const std::string Name(t_Name); + +#if FLING_WINDOWS + const FARPROC Proc = GetProcAddress(static_cast(m_Handle), Name.c_str()); + void* Address = nullptr; + static_assert(sizeof(Address) == sizeof(Proc), "FARPROC must be the same size as void*"); + std::memcpy(&Address, &Proc, sizeof(Address)); + return Address; +#elif FLING_LINUX + return dlsym(m_Handle, Name.c_str()); +#endif + } + + void DynamicLibrary::Close() { + if (m_Handle == nullptr) { + return; + } + +#if FLING_WINDOWS + FreeLibrary(static_cast(m_Handle)); +#elif FLING_LINUX + dlclose(m_Handle); +#endif + m_Handle = nullptr; + } +} // namespace Fling diff --git a/FlingEngine/Graphics/CMakeLists.txt b/FlingEngine/Graphics/CMakeLists.txt index 4db39bfb..822f996d 100644 --- a/FlingEngine/Graphics/CMakeLists.txt +++ b/FlingEngine/Graphics/CMakeLists.txt @@ -13,7 +13,7 @@ fling_add_module(Graphics # Public headers do not include Vulkan, GLFW, SPIRV-Cross, or stb. # tinyobjloader and ImGui stay private as well. PRIVATE_LIBS - Vulkan::Vulkan + Vulkan::Headers glfw spirv-cross-core spirv-cross-cpp @@ -26,12 +26,8 @@ if(WITH_IMGUI_FLAG) target_link_libraries(Graphics PRIVATE ImGui) endif() -# Load vulkan-1.dll on the first Vulkan call, not when the process starts. A -# --null-render run makes no Vulkan call, so it can start on a machine that has -# no Vulkan runtime (CI runners, servers). MinGW has no /DELAYLOAD. This is a -# short-term fix: Plans/vulkan-loading.md replaces it on every platform. -if(MSVC) - # LNK4199: a target that links Graphics but makes no Vulkan call (ApplicationTests) - target_link_options(Graphics INTERFACE /DELAYLOAD:vulkan-1.dll /IGNORE:4199) - target_link_libraries(Graphics INTERFACE delayimp) -endif() +# Keep prototypes off even when a file includes vulkan.h before FlingVulkan.h. +# The loader is opened at runtime. Do not link Vulkan::Vulkan. +target_compile_definitions(Graphics PRIVATE VK_NO_PROTOTYPES) + +set_source_files_properties(src/VulkanFunctions.inl PROPERTIES HEADER_FILE_ONLY TRUE) diff --git a/FlingEngine/Graphics/src/DesktopWindow.cpp b/FlingEngine/Graphics/src/DesktopWindow.cpp index 2153e996..487d6348 100644 --- a/FlingEngine/Graphics/src/DesktopWindow.cpp +++ b/FlingEngine/Graphics/src/DesktopWindow.cpp @@ -19,6 +19,8 @@ namespace Fling // Initialize GLFW if we have to if (!bGlfwInitalized) { + // Give GLFW the loader that Fling opened. Call this before glfwInit. + glfwInitVulkanLoader(vkGetInstanceProcAddr); glfwInit(); glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); bGlfwInitalized = true; diff --git a/FlingEngine/Graphics/src/DesktopWindow.h b/FlingEngine/Graphics/src/DesktopWindow.h index 6d717396..9b98980c 100644 --- a/FlingEngine/Graphics/src/DesktopWindow.h +++ b/FlingEngine/Graphics/src/DesktopWindow.h @@ -2,6 +2,9 @@ #include "FlingWindow.h" +// FlingVulkan.h sets VK_NO_PROTOTYPES before glfw3.h includes vulkan.h. +#include "FlingVulkan.h" + #ifndef GLFW_INCLUDE_VULKAN # define GLFW_INCLUDE_VULKAN #endif diff --git a/FlingEngine/Graphics/src/FlingVulkan.h b/FlingEngine/Graphics/src/FlingVulkan.h index 9e3b99fb..f4ec2279 100644 --- a/FlingEngine/Graphics/src/FlingVulkan.h +++ b/FlingEngine/Graphics/src/FlingVulkan.h @@ -1,12 +1,17 @@ #pragma once +// The loader is opened at runtime. The SDK header must not declare the functions. +#ifndef VK_NO_PROTOTYPES +#define VK_NO_PROTOTYPES +#endif + #ifdef NOMINMAX #undef NOMINMAX #include -#else +#else #define NOMINMAX @@ -16,10 +21,26 @@ #endif // NOMINMAX +// The names match the C API, so call sites stay vkCreateBuffer(...). +extern "C" { +#define FLING_VK_GLOBAL(name) extern PFN_##name name; +#define FLING_VK_GLOBAL_OPTIONAL(name) extern PFN_##name name; +#define FLING_VK_INSTANCE(name) extern PFN_##name name; +#define FLING_VK_INSTANCE_OPTIONAL(name) extern PFN_##name name; +#define FLING_VK_DEVICE(name) extern PFN_##name name; +#define FLING_VK_DEVICE_OPTIONAL(name) extern PFN_##name name; +#include "VulkanFunctions.inl" +#undef FLING_VK_GLOBAL +#undef FLING_VK_GLOBAL_OPTIONAL +#undef FLING_VK_INSTANCE +#undef FLING_VK_INSTANCE_OPTIONAL +#undef FLING_VK_DEVICE +#undef FLING_VK_DEVICE_OPTIONAL +} + // Some Vulkan constant definitions // Grabbed these from Granite: https://github.com/Themaister/Granite/blob/master/vulkan/shader.cpp -namespace Fling -{ +namespace Fling { static const unsigned VULKAN_NUM_DESCRIPTOR_SETS = 4; static const unsigned VULKAN_NUM_BINDINGS = 16; static const unsigned VULKAN_NUM_ATTACHMENTS = 8; @@ -29,9 +50,8 @@ namespace Fling static const unsigned VULKAN_MAX_UBO_SIZE = 16 * 1024; static const unsigned VULKAN_NUM_SPEC_CONSTANTS = 8; - namespace VkConfig - { - static const int MAX_FRAMES_IN_FLIGHT = 2; - } + namespace VkConfig { + static const int MAX_FRAMES_IN_FLIGHT = 2; + } -} // namespace Fling \ No newline at end of file +} // namespace Fling diff --git a/FlingEngine/Graphics/src/Instance.cpp b/FlingEngine/Graphics/src/Instance.cpp index 4470bf43..e5df77d5 100644 --- a/FlingEngine/Graphics/src/Instance.cpp +++ b/FlingEngine/Graphics/src/Instance.cpp @@ -1,4 +1,5 @@ #include "Instance.h" +#include "VulkanLoader.h" #include "FlingConfig.h" #include #include "Logger.h" @@ -65,6 +66,8 @@ namespace Fling F_LOG_FATAL( "Failed to create the Vulkan instance!" ); } + VulkanLoader::LoadInstance(m_Instance); + // Find out what extensions are available uint32_t extensionCount = 0; vkEnumerateInstanceExtensionProperties( nullptr, &extensionCount, nullptr ); diff --git a/FlingEngine/Graphics/src/LogicalDevice.cpp b/FlingEngine/Graphics/src/LogicalDevice.cpp index 632be172..92540277 100644 --- a/FlingEngine/Graphics/src/LogicalDevice.cpp +++ b/FlingEngine/Graphics/src/LogicalDevice.cpp @@ -1,4 +1,5 @@ #include "LogicalDevice.h" +#include "VulkanLoader.h" #include "Instance.h" #include "PhyscialDevice.h" #include "Logger.h" @@ -127,6 +128,8 @@ namespace Fling F_LOG_FATAL( "Failed to create logical Device!" ); } + VulkanLoader::LoadDevice(m_Device); + vkGetDeviceQueue(m_Device, m_GraphicsFamily, 0, &m_GraphicsQueue); vkGetDeviceQueue(m_Device, m_PresentFamily, 0, &m_PresentQueue); } diff --git a/FlingEngine/Graphics/src/VulkanApp.cpp b/FlingEngine/Graphics/src/VulkanApp.cpp index b1f57079..26d92206 100644 --- a/FlingEngine/Graphics/src/VulkanApp.cpp +++ b/FlingEngine/Graphics/src/VulkanApp.cpp @@ -21,9 +21,36 @@ #include "Version.h" #include "BuildConfig.h" #include "GitVersion.h" +#include "VulkanLoader.h" + +#include +#include namespace { + [[noreturn]] void StopForMissingVulkan(const std::string& t_Detail) + { + const char* Message = "Vulkan runtime not found: install a GPU driver with Vulkan support"; + if (const auto Console = Fling::Logger::GetCurrentConsole()) + { + if (!t_Detail.empty()) + { + Console->error("Vulkan loader: {}", t_Detail); + } + Console->error(Message); + Console->flush(); + } + if (const auto File = Fling::Logger::GetCurrentLogFile()) + { + if (!t_Detail.empty()) + { + File->error("Vulkan loader: {}", t_Detail); + } + File->error(Message); + } + throw std::runtime_error(Message); + } + VkExtent2D ChooseSwapExtent(Fling::FlingWindow* t_Window, Fling::PhysicalDevice* t_PhysicalDevice, VkSurfaceKHR t_Surface) { @@ -80,6 +107,13 @@ namespace Fling void VulkanApp::Init(PipelineFlags t_Conf, entt::registry& t_Reg, std::shared_ptr t_DebugUI) { + // Open the loader before Prepare. Prepare calls glfwInit. + std::string LoaderError; + if (!VulkanLoader::LoadLoader(&LoaderError)) + { + StopForMissingVulkan(LoaderError); + } + Singleton::Init(); Prepare(); @@ -711,6 +745,9 @@ namespace Fling delete m_CurrentWindow; m_CurrentWindow = nullptr; + // vkDestroyInstance already ran in the Instance destructor. + VulkanLoader::UnloadLoader(); + F_LOG_TRACE("Vulkan App Shutdown!"); } } // namespace Fling \ No newline at end of file diff --git a/FlingEngine/Graphics/src/VulkanFunctions.inl b/FlingEngine/Graphics/src/VulkanFunctions.inl new file mode 100644 index 00000000..a00ee373 --- /dev/null +++ b/FlingEngine/Graphics/src/VulkanFunctions.inl @@ -0,0 +1,167 @@ +// Vulkan commands that first-party code calls. +// +// Include this file once for each task. Define the FLING_VK_* macros you need. +// A macro that you do not define is empty. This file has no include guard. +// +// FLING_VK_GLOBAL Loader commands. Load them with vkGetInstanceProcAddr(nullptr, ...). +// FLING_VK_GLOBAL_OPTIONAL Same level. A null pointer is not an error. +// FLING_VK_INSTANCE Instance commands, including VkPhysicalDevice commands. +// Load them with vkGetInstanceProcAddr(instance, ...). +// FLING_VK_INSTANCE_OPTIONAL Extension commands. A null pointer is not an error. +// FLING_VK_DEVICE Commands whose first parameter is VkDevice, VkQueue, or VkCommandBuffer. +// Load them with vkGetDeviceProcAddr(device, ...). +// FLING_VK_DEVICE_OPTIONAL Same level. A null pointer is not an error. +// +// vkGetInstanceProcAddr comes from DynamicLibrary::Symbol. Do not replace that +// pointer with the result of calling vkGetInstanceProcAddr on itself. +// vkGetDeviceProcAddr takes VkDevice, but load it with vkGetInstanceProcAddr. +// It is how device commands are loaded. There is one LogicalDevice, so one table. + +#ifndef FLING_VK_GLOBAL +#define FLING_VK_GLOBAL(name) +#define FLING_VK_CLEAR_GLOBAL +#endif +#ifndef FLING_VK_GLOBAL_OPTIONAL +#define FLING_VK_GLOBAL_OPTIONAL(name) +#define FLING_VK_CLEAR_GLOBAL_OPTIONAL +#endif +#ifndef FLING_VK_INSTANCE +#define FLING_VK_INSTANCE(name) +#define FLING_VK_CLEAR_INSTANCE +#endif +#ifndef FLING_VK_INSTANCE_OPTIONAL +#define FLING_VK_INSTANCE_OPTIONAL(name) +#define FLING_VK_CLEAR_INSTANCE_OPTIONAL +#endif +#ifndef FLING_VK_DEVICE +#define FLING_VK_DEVICE(name) +#define FLING_VK_CLEAR_DEVICE +#endif +#ifndef FLING_VK_DEVICE_OPTIONAL +#define FLING_VK_DEVICE_OPTIONAL(name) +#define FLING_VK_CLEAR_DEVICE_OPTIONAL +#endif + +FLING_VK_GLOBAL(vkCreateInstance) +FLING_VK_GLOBAL(vkEnumerateInstanceExtensionProperties) +FLING_VK_GLOBAL(vkEnumerateInstanceLayerProperties) +FLING_VK_GLOBAL(vkGetInstanceProcAddr) + +FLING_VK_GLOBAL_OPTIONAL(vkEnumerateInstanceVersion) + +FLING_VK_INSTANCE(vkCreateDevice) +FLING_VK_INSTANCE(vkDestroyInstance) +FLING_VK_INSTANCE(vkDestroySurfaceKHR) +FLING_VK_INSTANCE(vkEnumerateDeviceExtensionProperties) +FLING_VK_INSTANCE(vkEnumeratePhysicalDevices) +FLING_VK_INSTANCE(vkGetDeviceProcAddr) +FLING_VK_INSTANCE(vkGetPhysicalDeviceFeatures) +FLING_VK_INSTANCE(vkGetPhysicalDeviceFormatProperties) +FLING_VK_INSTANCE(vkGetPhysicalDeviceMemoryProperties) +FLING_VK_INSTANCE(vkGetPhysicalDeviceProperties) +FLING_VK_INSTANCE(vkGetPhysicalDeviceQueueFamilyProperties) +FLING_VK_INSTANCE(vkGetPhysicalDeviceSurfaceCapabilitiesKHR) +FLING_VK_INSTANCE(vkGetPhysicalDeviceSurfaceFormatsKHR) +FLING_VK_INSTANCE(vkGetPhysicalDeviceSurfacePresentModesKHR) +FLING_VK_INSTANCE(vkGetPhysicalDeviceSurfaceSupportKHR) + +FLING_VK_INSTANCE_OPTIONAL(vkCreateDebugUtilsMessengerEXT) +FLING_VK_INSTANCE_OPTIONAL(vkDestroyDebugUtilsMessengerEXT) + +FLING_VK_DEVICE(vkAcquireNextImageKHR) +FLING_VK_DEVICE(vkAllocateCommandBuffers) +FLING_VK_DEVICE(vkAllocateDescriptorSets) +FLING_VK_DEVICE(vkAllocateMemory) +FLING_VK_DEVICE(vkBeginCommandBuffer) +FLING_VK_DEVICE(vkBindBufferMemory) +FLING_VK_DEVICE(vkBindImageMemory) +FLING_VK_DEVICE(vkCmdBeginRenderPass) +FLING_VK_DEVICE(vkCmdBindDescriptorSets) +FLING_VK_DEVICE(vkCmdBindIndexBuffer) +FLING_VK_DEVICE(vkCmdBindPipeline) +FLING_VK_DEVICE(vkCmdBindVertexBuffers) +FLING_VK_DEVICE(vkCmdBlitImage) +FLING_VK_DEVICE(vkCmdCopyBuffer) +FLING_VK_DEVICE(vkCmdCopyBufferToImage) +FLING_VK_DEVICE(vkCmdDrawIndexed) +FLING_VK_DEVICE(vkCmdEndRenderPass) +FLING_VK_DEVICE(vkCmdNextSubpass) +FLING_VK_DEVICE(vkCmdPipelineBarrier) +FLING_VK_DEVICE(vkCmdPushConstants) +FLING_VK_DEVICE(vkCmdSetScissor) +FLING_VK_DEVICE(vkCmdSetViewport) +FLING_VK_DEVICE(vkCreateBuffer) +FLING_VK_DEVICE(vkCreateCommandPool) +FLING_VK_DEVICE(vkCreateDescriptorPool) +FLING_VK_DEVICE(vkCreateDescriptorSetLayout) +FLING_VK_DEVICE(vkCreateFence) +FLING_VK_DEVICE(vkCreateFramebuffer) +FLING_VK_DEVICE(vkCreateGraphicsPipelines) +FLING_VK_DEVICE(vkCreateImage) +FLING_VK_DEVICE(vkCreateImageView) +FLING_VK_DEVICE(vkCreatePipelineCache) +FLING_VK_DEVICE(vkCreatePipelineLayout) +FLING_VK_DEVICE(vkCreateRenderPass) +FLING_VK_DEVICE(vkCreateSampler) +FLING_VK_DEVICE(vkCreateSemaphore) +FLING_VK_DEVICE(vkCreateShaderModule) +FLING_VK_DEVICE(vkCreateSwapchainKHR) +FLING_VK_DEVICE(vkDestroyBuffer) +FLING_VK_DEVICE(vkDestroyCommandPool) +FLING_VK_DEVICE(vkDestroyDescriptorPool) +FLING_VK_DEVICE(vkDestroyDescriptorSetLayout) +FLING_VK_DEVICE(vkDestroyDevice) +FLING_VK_DEVICE(vkDestroyFence) +FLING_VK_DEVICE(vkDestroyFramebuffer) +FLING_VK_DEVICE(vkDestroyImage) +FLING_VK_DEVICE(vkDestroyImageView) +FLING_VK_DEVICE(vkDestroyPipeline) +FLING_VK_DEVICE(vkDestroyPipelineCache) +FLING_VK_DEVICE(vkDestroyPipelineLayout) +FLING_VK_DEVICE(vkDestroyRenderPass) +FLING_VK_DEVICE(vkDestroySampler) +FLING_VK_DEVICE(vkDestroySemaphore) +FLING_VK_DEVICE(vkDestroyShaderModule) +FLING_VK_DEVICE(vkDestroySwapchainKHR) +FLING_VK_DEVICE(vkDeviceWaitIdle) +FLING_VK_DEVICE(vkEndCommandBuffer) +FLING_VK_DEVICE(vkFlushMappedMemoryRanges) +FLING_VK_DEVICE(vkFreeCommandBuffers) +FLING_VK_DEVICE(vkFreeMemory) +FLING_VK_DEVICE(vkGetBufferMemoryRequirements) +FLING_VK_DEVICE(vkGetDeviceQueue) +FLING_VK_DEVICE(vkGetImageMemoryRequirements) +FLING_VK_DEVICE(vkGetSwapchainImagesKHR) +FLING_VK_DEVICE(vkMapMemory) +FLING_VK_DEVICE(vkQueuePresentKHR) +FLING_VK_DEVICE(vkQueueSubmit) +FLING_VK_DEVICE(vkQueueWaitIdle) +FLING_VK_DEVICE(vkResetFences) +FLING_VK_DEVICE(vkUnmapMemory) +FLING_VK_DEVICE(vkUpdateDescriptorSets) +FLING_VK_DEVICE(vkWaitForFences) + +#ifdef FLING_VK_CLEAR_GLOBAL +#undef FLING_VK_GLOBAL +#undef FLING_VK_CLEAR_GLOBAL +#endif +#ifdef FLING_VK_CLEAR_GLOBAL_OPTIONAL +#undef FLING_VK_GLOBAL_OPTIONAL +#undef FLING_VK_CLEAR_GLOBAL_OPTIONAL +#endif +#ifdef FLING_VK_CLEAR_INSTANCE +#undef FLING_VK_INSTANCE +#undef FLING_VK_CLEAR_INSTANCE +#endif +#ifdef FLING_VK_CLEAR_INSTANCE_OPTIONAL +#undef FLING_VK_INSTANCE_OPTIONAL +#undef FLING_VK_CLEAR_INSTANCE_OPTIONAL +#endif +#ifdef FLING_VK_CLEAR_DEVICE +#undef FLING_VK_DEVICE +#undef FLING_VK_CLEAR_DEVICE +#endif +#ifdef FLING_VK_CLEAR_DEVICE_OPTIONAL +#undef FLING_VK_DEVICE_OPTIONAL +#undef FLING_VK_CLEAR_DEVICE_OPTIONAL +#endif diff --git a/FlingEngine/Graphics/src/VulkanLoader.cpp b/FlingEngine/Graphics/src/VulkanLoader.cpp new file mode 100644 index 00000000..b083dabe --- /dev/null +++ b/FlingEngine/Graphics/src/VulkanLoader.cpp @@ -0,0 +1,159 @@ +#include "VulkanLoader.h" + +#include "DynamicLibrary.h" +#include "Logger.h" +#include "Platform.h" + +#include +#include +#include + +extern "C" { +#define FLING_VK_GLOBAL(name) PFN_##name name = nullptr; +#define FLING_VK_GLOBAL_OPTIONAL(name) PFN_##name name = nullptr; +#define FLING_VK_INSTANCE(name) PFN_##name name = nullptr; +#define FLING_VK_INSTANCE_OPTIONAL(name) PFN_##name name = nullptr; +#define FLING_VK_DEVICE(name) PFN_##name name = nullptr; +#define FLING_VK_DEVICE_OPTIONAL(name) PFN_##name name = nullptr; +#include "VulkanFunctions.inl" +#undef FLING_VK_GLOBAL +#undef FLING_VK_GLOBAL_OPTIONAL +#undef FLING_VK_INSTANCE +#undef FLING_VK_INSTANCE_OPTIONAL +#undef FLING_VK_DEVICE +#undef FLING_VK_DEVICE_OPTIONAL +} + +namespace { + Fling::DynamicLibrary g_Loader; + + [[noreturn]] void FatalVulkan(const std::string& t_Message) { + if (const auto Console = Fling::Logger::GetCurrentConsole()) { + Console->error(t_Message); + Console->flush(); + } + if (const auto File = Fling::Logger::GetCurrentLogFile()) { + File->error(t_Message); + } + throw std::runtime_error(t_Message); + } + + template + void LoadGlobal(const char* t_Name, T_Fn& t_Out) { + // Vulkan 1.0 and 1.1 do not return this pointer from itself. + // DynamicLibrary::Symbol already stored the export. + if (std::string_view(t_Name) == "vkGetInstanceProcAddr") { + return; + } + t_Out = reinterpret_cast(vkGetInstanceProcAddr(nullptr, t_Name)); + } + + template + void LoadInstanceProc(VkInstance t_Instance, const char* t_Name, T_Fn& t_Out) { + t_Out = reinterpret_cast(vkGetInstanceProcAddr(t_Instance, t_Name)); + } + + template + void LoadDeviceProc(VkDevice t_Device, const char* t_Name, T_Fn& t_Out) { + t_Out = reinterpret_cast(vkGetDeviceProcAddr(t_Device, t_Name)); + } + + template + void RequireProc(const char* t_Name, const T_Fn& t_Fn) { + if (t_Fn == nullptr) { + FatalVulkan(std::string("Required Vulkan function is null: ") + t_Name); + } + } + + template + void ClearProc(T_Fn& t_Fn) { + t_Fn = nullptr; + } +} // namespace + +namespace Fling { + bool VulkanLoader::LoadLoader(std::string* t_Error) { +#if FLING_WINDOWS + DynamicLibrary Library = DynamicLibrary::Open("vulkan-1.dll", t_Error); +#elif FLING_LINUX + const std::string_view Names[] = {"libvulkan.so.1", "libvulkan.so"}; + DynamicLibrary Library = DynamicLibrary::OpenFirst(Names, t_Error); +#else +#error VulkanLoader supports Windows and Linux only +#endif + if (!Library) { + return false; + } + + const PFN_vkGetInstanceProcAddr GetInstanceProcAddr = + Library.SymbolAs("vkGetInstanceProcAddr"); + if (GetInstanceProcAddr == nullptr) { + if (t_Error != nullptr) { + *t_Error = "vkGetInstanceProcAddr is missing from the Vulkan loader"; + } + Library.Close(); + return false; + } + + g_Loader = std::move(Library); + vkGetInstanceProcAddr = GetInstanceProcAddr; + +#define FLING_VK_GLOBAL(name) LoadGlobal(#name, name); +#define FLING_VK_GLOBAL_OPTIONAL(name) LoadGlobal(#name, name); +#include "VulkanFunctions.inl" +#undef FLING_VK_GLOBAL +#undef FLING_VK_GLOBAL_OPTIONAL + +#define FLING_VK_GLOBAL(name) RequireProc(#name, name); +#include "VulkanFunctions.inl" +#undef FLING_VK_GLOBAL + + return true; + } + + void VulkanLoader::LoadInstance(VkInstance t_Instance) { +#define FLING_VK_INSTANCE(name) LoadInstanceProc(t_Instance, #name, name); +#define FLING_VK_INSTANCE_OPTIONAL(name) LoadInstanceProc(t_Instance, #name, name); +#include "VulkanFunctions.inl" +#undef FLING_VK_INSTANCE +#undef FLING_VK_INSTANCE_OPTIONAL + +#define FLING_VK_INSTANCE(name) RequireProc(#name, name); +#include "VulkanFunctions.inl" +#undef FLING_VK_INSTANCE + } + + void VulkanLoader::LoadDevice(VkDevice t_Device) { + if (vkGetDeviceProcAddr == nullptr) { + FatalVulkan("Required Vulkan function is null: vkGetDeviceProcAddr"); + } + +#define FLING_VK_DEVICE(name) LoadDeviceProc(t_Device, #name, name); +#define FLING_VK_DEVICE_OPTIONAL(name) LoadDeviceProc(t_Device, #name, name); +#include "VulkanFunctions.inl" +#undef FLING_VK_DEVICE +#undef FLING_VK_DEVICE_OPTIONAL + +#define FLING_VK_DEVICE(name) RequireProc(#name, name); +#include "VulkanFunctions.inl" +#undef FLING_VK_DEVICE + } + + void VulkanLoader::UnloadLoader() { +#define FLING_VK_GLOBAL(name) ClearProc(name); +#define FLING_VK_GLOBAL_OPTIONAL(name) ClearProc(name); +#define FLING_VK_INSTANCE(name) ClearProc(name); +#define FLING_VK_INSTANCE_OPTIONAL(name) ClearProc(name); +#define FLING_VK_DEVICE(name) ClearProc(name); +#define FLING_VK_DEVICE_OPTIONAL(name) ClearProc(name); +#include "VulkanFunctions.inl" +#undef FLING_VK_GLOBAL +#undef FLING_VK_GLOBAL_OPTIONAL +#undef FLING_VK_INSTANCE +#undef FLING_VK_INSTANCE_OPTIONAL +#undef FLING_VK_DEVICE +#undef FLING_VK_DEVICE_OPTIONAL + + g_Loader.Close(); + } +} // namespace Fling diff --git a/FlingEngine/Graphics/src/VulkanLoader.h b/FlingEngine/Graphics/src/VulkanLoader.h new file mode 100644 index 00000000..29e9cd54 --- /dev/null +++ b/FlingEngine/Graphics/src/VulkanLoader.h @@ -0,0 +1,37 @@ +#pragma once + +#include "FlingVulkan.h" + +#include + +namespace Fling { + /** + * Load the Vulkan loader library and the Vulkan function pointers. + * + * Graphics calls these methods. It does not call LoadLibrary, dlopen, + * GetProcAddress, or dlsym. + */ + class VulkanLoader { + public: + /** + * Open the Vulkan loader and load the global commands. + * + * Return false when the loader library is missing. Return false when + * vkGetInstanceProcAddr is missing. A null required command stops the + * process. + */ + static bool LoadLoader(std::string* t_Error = nullptr); + + /** Load the instance commands. A null required command stops the process. */ + static void LoadInstance(VkInstance t_Instance); + + /** Load the device commands. A null required command stops the process. */ + static void LoadDevice(VkDevice t_Device); + + /** + * Set every Vulkan function pointer to null. Then close the loader. + * Call this after vkDestroyInstance. + */ + static void UnloadLoader(); + }; +} // namespace Fling diff --git a/FlingTests/CMakeLists.txt b/FlingTests/CMakeLists.txt index f0d8d083..3d6c76f0 100644 --- a/FlingTests/CMakeLists.txt +++ b/FlingTests/CMakeLists.txt @@ -102,6 +102,13 @@ if( WITH_EDITOR_FLAG ) fling_add_isolation_test( EditorHeaders Fling::Editor "BaseEditor.h" VISIBLE ) endif() +# Every first-party vk* call must be named in VulkanFunctions.inl. +add_test(NAME "Vulkan calls are listed" + COMMAND ${CMAKE_COMMAND} + -DSOURCE_ROOT=${FLING_ROOT_DIR} + -P "${CMAKE_CURRENT_SOURCE_DIR}/VulkanLoad/CheckVulkanCalls.cmake" +) + # Unattended crash tests. CrashProbe crashes on purpose after # CrashHandler::InstallUnattended. Each test passes only if the process exits # with code 2 and prints a stack trace. A modal dialog makes the test time out. diff --git a/FlingTests/Core/DynamicLibraryTests.cpp b/FlingTests/Core/DynamicLibraryTests.cpp new file mode 100644 index 00000000..9ea7c122 --- /dev/null +++ b/FlingTests/Core/DynamicLibraryTests.cpp @@ -0,0 +1,105 @@ +#include + +#include "DynamicLibrary.h" +#include "Platform.h" + +#include +#include +#include +#include + +namespace { +#if FLING_LINUX + constexpr const char* LibraryName = "libc.so.6"; + constexpr const char* SymbolName = "strlen"; +#elif FLING_WINDOWS + constexpr const char* LibraryName = "kernel32.dll"; + constexpr const char* SymbolName = "GetCurrentProcessId"; +#else +#error DynamicLibrary tests support Windows and Linux only +#endif + + void CallLoadedSymbol(const Fling::DynamicLibrary& t_Library) { + REQUIRE(static_cast(t_Library)); + REQUIRE(t_Library.Symbol(SymbolName) != nullptr); + +#if FLING_LINUX + auto* Fn = t_Library.SymbolAs(SymbolName); + REQUIRE(Fn != nullptr); + REQUIRE(Fn("abcd") == 4); +#elif FLING_WINDOWS + auto* Fn = t_Library.SymbolAs(SymbolName); + REQUIRE(Fn != nullptr); + REQUIRE(Fn() != 0); +#endif + } +} // namespace + +TEST_CASE("DynamicLibrary", "[core][DynamicLibrary]") { + SECTION("Open and call a symbol") { + std::string Error = "stale"; + Fling::DynamicLibrary Library = Fling::DynamicLibrary::Open(LibraryName, &Error); + REQUIRE(Error.empty()); + CallLoadedSymbol(Library); + REQUIRE(Library.Symbol("fling_no_such_symbol_42") == nullptr); + REQUIRE(static_cast(Library)); + } + + SECTION("A missing library returns an empty object and an error") { + std::string Error; + Fling::DynamicLibrary Library = Fling::DynamicLibrary::Open("fling_no_such_library_42", &Error); + REQUIRE_FALSE(static_cast(Library)); + REQUIRE_FALSE(Error.empty()); + REQUIRE(Library.Symbol(SymbolName) == nullptr); + + Fling::DynamicLibrary NoError = Fling::DynamicLibrary::Open("fling_no_such_library_42", nullptr); + REQUIRE_FALSE(static_cast(NoError)); + } + + SECTION("OpenFirst skips a bad name") { + const std::string_view Names[] = {"fling_no_such_library_42", LibraryName}; + std::string Error = "stale"; + Fling::DynamicLibrary Library = Fling::DynamicLibrary::OpenFirst(Names, &Error); + REQUIRE(Error.empty()); + CallLoadedSymbol(Library); + } + + SECTION("Move leaves the source empty") { + Fling::DynamicLibrary Source = Fling::DynamicLibrary::Open(LibraryName); + REQUIRE(static_cast(Source)); + + Fling::DynamicLibrary Dest = std::move(Source); + REQUIRE(static_cast(Dest)); + REQUIRE_FALSE(static_cast(Source)); + REQUIRE(Source.Symbol(SymbolName) == nullptr); + CallLoadedSymbol(Dest); + + Fling::DynamicLibrary Second = Fling::DynamicLibrary::Open(LibraryName); + Second = std::move(Dest); + REQUIRE(static_cast(Second)); + REQUIRE_FALSE(static_cast(Dest)); + REQUIRE(Dest.Symbol(SymbolName) == nullptr); + } + + SECTION("Close then Symbol returns null") { + Fling::DynamicLibrary Library = Fling::DynamicLibrary::Open(LibraryName); + CallLoadedSymbol(Library); + Library.Close(); + REQUIRE_FALSE(static_cast(Library)); + REQUIRE(Library.Symbol(SymbolName) == nullptr); + Library.Close(); + REQUIRE(Library.Symbol(SymbolName) == nullptr); + } + + SECTION("Destructor unloads a moved library") { + Fling::DynamicLibrary Outer = Fling::DynamicLibrary::Open(LibraryName); + REQUIRE(static_cast(Outer)); + { + Fling::DynamicLibrary Inner = std::move(Outer); + REQUIRE_FALSE(static_cast(Outer)); + CallLoadedSymbol(Inner); + } + REQUIRE_FALSE(static_cast(Outer)); + REQUIRE(Outer.Symbol(SymbolName) == nullptr); + } +} diff --git a/FlingTests/VulkanLoad/CheckVulkanCalls.cmake b/FlingTests/VulkanLoad/CheckVulkanCalls.cmake new file mode 100644 index 00000000..2c5d3dc9 --- /dev/null +++ b/FlingTests/VulkanLoad/CheckVulkanCalls.cmake @@ -0,0 +1,72 @@ +# Fail when a first-party vk[A-Z] call is missing from VulkanFunctions.inl. +# +# cmake -DSOURCE_ROOT= -P CheckVulkanCalls.cmake + +cmake_minimum_required(VERSION 3.21) + +if(NOT DEFINED SOURCE_ROOT) + message(FATAL_ERROR "Set -DSOURCE_ROOT= to the repository root") +endif() + +set(_inl "${SOURCE_ROOT}/FlingEngine/Graphics/src/VulkanFunctions.inl") +if(NOT EXISTS "${_inl}") + message(FATAL_ERROR "Vulkan function list not found: ${_inl}") +endif() + +file(READ "${_inl}" _inl_text) +string(REGEX MATCHALL "\\(vk[A-Za-z0-9]+\\)" _listed_raw "${_inl_text}") +set(_listed "") +foreach(_raw IN LISTS _listed_raw) + string(REGEX REPLACE "[()]" "" _name "${_raw}") + list(APPEND _listed "${_name}") +endforeach() +list(REMOVE_DUPLICATES _listed) + +function(fling_strip_inactive _input _output) + set(_text "${_input}") + # Block comments, then line comments. A vk* name inside a string is not a call: + # the pattern below requires the '(' of a call. + string(REGEX REPLACE "/\\*([^*]|\\*+[^*/])*\\*+/" "" _text "${_text}") + string(REGEX REPLACE "//[^\n]*" "" _text "${_text}") + set(${_output} "${_text}" PARENT_SCOPE) +endfunction() + +set(_calls "") +foreach(_root IN ITEMS FlingEngine Sandbox FlingTests) + file(GLOB_RECURSE _files + "${SOURCE_ROOT}/${_root}/*.cpp" + "${SOURCE_ROOT}/${_root}/*.cc" + "${SOURCE_ROOT}/${_root}/*.h" + "${SOURCE_ROOT}/${_root}/*.hpp" + "${SOURCE_ROOT}/${_root}/*.inl" + ) + foreach(_file IN LISTS _files) + file(READ "${_file}" _text) + fling_strip_inactive("${_text}" _text) + string(REGEX MATCHALL "(^|[^A-Za-z0-9_])vk[A-Z][A-Za-z0-9]*[ \t\r\n]*\\(" _matches "${_text}") + foreach(_match IN LISTS _matches) + string(REGEX MATCH "vk[A-Z][A-Za-z0-9]*" _name "${_match}") + if(_name) + list(APPEND _calls "${_name}") + endif() + endforeach() + endforeach() +endforeach() +list(REMOVE_DUPLICATES _calls) +list(SORT _calls) + +set(_missing "") +foreach(_call IN LISTS _calls) + if(NOT _call IN_LIST _listed) + list(APPEND _missing "${_call}") + endif() +endforeach() + +if(_missing) + list(JOIN _missing "\n " _missing_text) + message(FATAL_ERROR "Vulkan calls missing from VulkanFunctions.inl:\n ${_missing_text}") +endif() + +list(LENGTH _calls _call_count) +list(LENGTH _listed _listed_count) +message(STATUS "Vulkan function list covers ${_call_count} first-party calls (${_listed_count} names listed)") diff --git a/FlingTests/VulkanLoad/ExpectNoVulkanImport.cmake b/FlingTests/VulkanLoad/ExpectNoVulkanImport.cmake new file mode 100644 index 00000000..20edae04 --- /dev/null +++ b/FlingTests/VulkanLoad/ExpectNoVulkanImport.cmake @@ -0,0 +1,112 @@ +# Fail when the Sandbox binary imports the Vulkan loader. +# +# cmake -DBINARY= -P ExpectNoVulkanImport.cmake +# +# Linux reads DT_NEEDED with readelf. Windows reads the PE import table with +# dumpbin /IMPORTS, objdump -p, or pe_imports.py. + +cmake_minimum_required(VERSION 3.21) + +if(NOT DEFINED BINARY) + message(FATAL_ERROR "Set -DBINARY= to the Sandbox executable") +endif() +if(NOT EXISTS "${BINARY}") + message(FATAL_ERROR "Sandbox binary not found: ${BINARY}") +endif() + +function(fling_reject_vulkan_imports _text) + string(TOLOWER "${_text}" _lower) + if(_lower MATCHES "libvulkan" OR _lower MATCHES "vulkan-1\\.dll") + message(FATAL_ERROR "Sandbox imports the Vulkan loader:\n${_text}") + endif() +endfunction() + +if(CMAKE_HOST_WIN32 OR BINARY MATCHES "\\.[Ee][Xx][Ee]$") + set(_import_text "") + set(_found FALSE) + + find_program(OBJDUMP_EXE NAMES objdump llvm-objdump) + if(OBJDUMP_EXE) + execute_process( + COMMAND "${OBJDUMP_EXE}" -p "${BINARY}" + RESULT_VARIABLE _rc + OUTPUT_VARIABLE _out + ERROR_VARIABLE _err + ) + if(_rc EQUAL 0) + string(REGEX MATCHALL "[^\n]*DLL Name:[^\n]*" _lines "${_out}") + string(JOIN "\n" _import_text ${_lines}) + set(_found TRUE) + endif() + endif() + + if(NOT _found) + find_program(DUMPBIN_EXE NAMES dumpbin) + if(NOT DUMPBIN_EXE) + file(GLOB _dumpbins + "C:/Program Files/Microsoft Visual Studio/2022/*/VC/Tools/MSVC/*/bin/Hostx64/x64/dumpbin.exe" + "C:/Program Files (x86)/Microsoft Visual Studio/2022/*/VC/Tools/MSVC/*/bin/Hostx64/x64/dumpbin.exe" + ) + if(_dumpbins) + list(GET _dumpbins 0 DUMPBIN_EXE) + endif() + endif() + if(DUMPBIN_EXE) + execute_process( + COMMAND "${DUMPBIN_EXE}" /IMPORTS "${BINARY}" + RESULT_VARIABLE _rc + OUTPUT_VARIABLE _out + ERROR_VARIABLE _err + ) + if(NOT _rc EQUAL 0) + message(FATAL_ERROR "dumpbin /IMPORTS failed (${_rc}): ${_err}") + endif() + set(_import_text "${_out}") + set(_found TRUE) + endif() + endif() + + if(NOT _found) + find_program(PYTHON_EXE NAMES python3 python) + if(NOT PYTHON_EXE) + message(FATAL_ERROR "Need objdump, dumpbin, or python to read the PE import table") + endif() + execute_process( + COMMAND "${PYTHON_EXE}" "${CMAKE_CURRENT_LIST_DIR}/pe_imports.py" "${BINARY}" + RESULT_VARIABLE _rc + OUTPUT_VARIABLE _out + ERROR_VARIABLE _err + ) + if(NOT _rc EQUAL 0) + message(FATAL_ERROR "pe_imports.py failed (${_rc}): ${_err}") + endif() + set(_import_text "${_out}") + endif() + + if(_import_text STREQUAL "") + message(FATAL_ERROR "The PE import reader returned no DLL names") + endif() + fling_reject_vulkan_imports("${_import_text}") + message(STATUS "Sandbox PE imports do not include vulkan-1.dll") +else() + find_program(READELF_EXE NAMES readelf llvm-readelf) + if(NOT READELF_EXE) + message(FATAL_ERROR "readelf is required to check that Sandbox does not import libvulkan") + endif() + execute_process( + COMMAND "${READELF_EXE}" -d "${BINARY}" + RESULT_VARIABLE _rc + OUTPUT_VARIABLE _out + ERROR_VARIABLE _err + ) + if(NOT _rc EQUAL 0) + message(FATAL_ERROR "readelf -d failed (${_rc}): ${_err}") + endif() + string(REGEX MATCHALL "[^\n]*\\(NEEDED\\)[^\n]*" _needed "${_out}") + if(NOT _needed) + message(FATAL_ERROR "readelf reported no DT_NEEDED entries for ${BINARY}") + endif() + string(JOIN "\n" _needed_text ${_needed}) + fling_reject_vulkan_imports("${_needed_text}") + message(STATUS "Sandbox DT_NEEDED does not include libvulkan") +endif() diff --git a/FlingTests/VulkanLoad/RunWithoutVulkanLoader.sh b/FlingTests/VulkanLoad/RunWithoutVulkanLoader.sh new file mode 100755 index 00000000..42c2db20 --- /dev/null +++ b/FlingTests/VulkanLoad/RunWithoutVulkanLoader.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Run one Sandbox twice with the Vulkan loader not loadable. +# The windowed run must exit 2 and log the missing-runtime line. +# The --null-render run must exit 0. It does not open the loader. +# +# Usage: RunWithoutVulkanLoader.sh + +set -euo pipefail + +if [[ $# -ne 1 ]]; then + echo "usage: $0 " >&2 + exit 2 +fi + +binary=$(realpath "$1") +root=$(cd "$(dirname "$0")/../.." && pwd) + +if [[ "$(id -u)" -ne 0 ]]; then + exec sudo -E bash "$0" "$binary" +fi + +exec unshare --mount bash -s -- "$binary" "$root" << 'EOF' +set -euo pipefail +binary=$1 +root=$2 +cd "$root" + +mapfile -t paths < <(ldconfig -p | awk '/libvulkan\.so/{print $NF}' | sort -u) +for path in "${paths[@]}"; do + if [[ -e "$path" || -L "$path" ]]; then + mount --bind /dev/null "$path" + fi +done + +set +e +output=$("$binary" --unattended --frames 1 --seed 1 2>&1) +code=$? +set -e +printf '%s\n' "$output" +if [[ "$code" -ne 2 ]]; then + echo "expected exit code 2 when the Vulkan loader is hidden, got ${code}" >&2 + exit 1 +fi +if ! grep -q "Vulkan runtime not found: install a GPU driver with Vulkan support" <<< "$output"; then + echo "missing Vulkan runtime log line" >&2 + exit 1 +fi + +"$binary" --null-render --unattended --frames 120 --seed 1 +EOF diff --git a/FlingTests/VulkanLoad/pe_imports.py b/FlingTests/VulkanLoad/pe_imports.py new file mode 100644 index 00000000..299c88a8 --- /dev/null +++ b/FlingTests/VulkanLoad/pe_imports.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +"""Print the import and delay-import DLL names of a PE binary, one per line.""" + +from __future__ import annotations + +import struct +import sys +from pathlib import Path + + +def _u16(data: bytes, offset: int) -> int: + return struct.unpack_from(" int: + return struct.unpack_from(" list[tuple[int, int, int]]: + sections: list[tuple[int, int, int]] = [] + for index in range(count): + base = offset + index * 40 + virtual_address = _u32(data, base + 12) + raw_size = _u32(data, base + 16) + raw_pointer = _u32(data, base + 20) + sections.append((virtual_address, raw_pointer, raw_size)) + return sections + + +def _rva_to_offset(rva: int, sections: list[tuple[int, int, int]]) -> int | None: + for virtual_address, raw_pointer, raw_size in sections: + if raw_size and virtual_address <= rva < virtual_address + raw_size: + return raw_pointer + (rva - virtual_address) + return None + + +def _cstring(data: bytes, offset: int) -> str: + end = data.find(b"\x00", offset) + if end < 0: + raise ValueError("DLL name is not terminated") + return data[offset:end].decode("ascii", "replace") + + +def import_names(path: Path) -> list[str]: + data = path.read_bytes() + if data[:2] != b"MZ": + raise ValueError("not a PE file") + pe = _u32(data, 0x3C) + if data[pe : pe + 4] != b"PE\x00\x00": + raise ValueError("bad PE signature") + + coff = pe + 4 + section_count = _u16(data, coff + 2) + optional_size = _u16(data, coff + 16) + optional = coff + 20 + magic = _u16(data, optional) + if magic == 0x20B: + directory_count = _u32(data, optional + 108) + directory = optional + 112 + elif magic == 0x10B: + directory_count = _u32(data, optional + 92) + directory = optional + 96 + else: + raise ValueError(f"unknown PE magic {magic:#x}") + + sections = _sections(data, optional + optional_size, section_count) + names: list[str] = [] + + def directory_entry(index: int) -> tuple[int, int]: + if index >= directory_count: + return 0, 0 + base = directory + index * 8 + return _u32(data, base), _u32(data, base + 4) + + import_rva, import_size = directory_entry(1) + if import_rva and import_size: + offset = _rva_to_offset(import_rva, sections) + if offset is None: + raise ValueError("import directory RVA is outside the file") + while True: + original, _time, _forwarder, name_rva, first_thunk = struct.unpack_from(" int: + if len(sys.argv) != 2: + print("usage: pe_imports.py ", file=sys.stderr) + return 2 + try: + for name in import_names(Path(sys.argv[1])): + print(name) + except (OSError, ValueError, struct.error) as error: + print(f"pe_imports: {error}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Plans/vulkan-loading.md b/Plans/vulkan-loading.md index 35f5ad1a..841734b1 100644 --- a/Plans/vulkan-loading.md +++ b/Plans/vulkan-loading.md @@ -4,8 +4,10 @@ Design for how the Graphics module loads Vulkan at runtime, with only the official Vulkan SDK (Khronos headers and loader) and no third-party Vulkan helper libraries. -This is a plan, not an established standard. When a phase is done, move the -rules that it establishes into [`Skills/`](../Skills/). +L1 and L2 are implemented. This file stays a plan until the rules move fully +into [`Skills/`](../Skills/). The rules that already hold are also noted in +[`Skills/architecture.md`](../Skills/architecture.md) and +[`Skills/testing.md`](../Skills/testing.md). Related plans: [`improve-ai-iteration.md`](improve-ai-iteration.md) (phase A, `--null-render`) and [`project-model.md`](project-model.md) (P5, the @@ -13,182 +15,178 @@ Related plans: [`improve-ai-iteration.md`](improve-ai-iteration.md) (phase A, ## Problem -- Graphics links `Vulkan::Vulkan`, the import library of the Vulkan loader. - The operating system then loads the loader (`vulkan-1.dll`, - `libvulkan.so.1`) when the process starts, before `main()`. A process cannot - start on a machine that has no Vulkan runtime, even with `--null-render`. -- The short-term fix (PR #219) delay-loads `vulkan-1.dll` with MSVC - `/DELAYLOAD` (`FlingEngine/Graphics/CMakeLists.txt`). It works only with - MSVC: - - Linux still loads `libvulkan.so.1` at process start. A headless run on a - server image with no `libvulkan1` package does not start. - - MinGW has no `/DELAYLOAD`, so it still loads `vulkan-1.dll` at process start. -- With delay loading, a missing runtime in a windowed run is an exception on - the first Vulkan call (a crash), not an error that the engine can report. -- Every Vulkan call goes through the loader's dispatch code. Device functions - that come from `vkGetDeviceProcAddr` skip that step. +- Graphics used to link `Vulkan::Vulkan`, the import library of the Vulkan + loader. The operating system then loaded the loader (`vulkan-1.dll`, + `libvulkan.so.1`) when the process started, before `main()`. A process could + not start on a machine that has no Vulkan runtime, even with `--null-render`. +- The short-term fix (PR #219) delay-loaded `vulkan-1.dll` with MSVC + `/DELAYLOAD`. It worked only with MSVC. Linux still loaded `libvulkan.so.1` + at process start. MinGW has no `/DELAYLOAD`. +- With delay loading, a missing runtime in a windowed run was an exception on + the first Vulkan call, not an error that the engine can report. ## Goals 1. A `--null-render` run starts with no Vulkan runtime, on every platform and every compiler. -2. A windowed run with no Vulkan runtime, or with no Vulkan driver, logs a - clear error and exits with `ExitCode::Crash` (2). There is no exception and +2. A windowed run with no Vulkan runtime logs a clear error and exits with + `ExitCode::Crash` (2). There is no exception from the operating system and no "DLL not found" dialog. 3. Only the official Vulkan SDK: the Khronos headers and the Khronos loader. No volk and no other third-party Vulkan helper library. 4. Call sites in Graphics do not change. `vkCreateBuffer(...)` stays `vkCreateBuffer(...)`. -5. Building needs only the Vulkan headers, not the import library. +5. Building needs the Vulkan headers. The linked binary must not import + `libvulkan` or `vulkan-1.dll`. ## Non-goals - More than one Vulkan device at a time. The engine has one `LogicalDevice`. - A per-device function table can come later if that changes. -- Vulkan-Hpp (`vulkan.hpp`). It is part of the SDK and has its own dynamic - dispatcher, but Graphics uses the C API. Moving to Vulkan-Hpp is a much - larger change than this plan. + One global function table is correct. A per-device table can come later if + that changes. +- Vulkan-Hpp (`vulkan.hpp`). Graphics uses the C API. - A Graphics module that is not linked. That is the `Server` target (P5 in `project-model.md`). +- A player message box. A log line and exit code 2 are enough. +- Generating `VulkanFunctions.inl` from `vk.xml`. The hand-kept list is enough. ## Design ### 1. No prototypes `FlingVulkan.h` is the only first-party file that includes ``. -It defines `VK_NO_PROTOTYPES` before the include. The SDK header then declares -the `PFN_vk...` function-pointer types, but no functions. Nothing links against -the loader, so the import library is no longer needed. +It defines `VK_NO_PROTOTYPES` before the include. Graphics also compiles with +`VK_NO_PROTOTYPES`, so a direct include cannot turn the prototypes back on. +The SDK header then declares the `PFN_vk...` types and no functions. -Every first-party file must get Vulkan through `FlingVulkan.h`. -`DesktopWindow.h` defines `GLFW_INCLUDE_VULKAN`, and `glfw3.h` then includes -`` itself. It must include `FlingVulkan.h` first, so -`VK_NO_PROTOTYPES` is set (or stop using `GLFW_INCLUDE_VULKAN`). A build check: -compile one file that calls a Vulkan function without the table; it must fail. +`DesktopWindow.h` includes `FlingVulkan.h` before it defines +`GLFW_INCLUDE_VULKAN`. `glfw3.h` then includes `` with the +prototypes already off. ### 2. One list of functions -A hand-written X-macro list, `Graphics/src/VulkanFunctions.inl`, names every -Vulkan function that Graphics calls (about 90 today). It has three sections, -one for each level at which Vulkan gives the pointer: +`FlingEngine/Graphics/src/VulkanFunctions.inl` is the hand-kept X-macro list of +every Vulkan command first-party code calls. The same list declares the +`extern "C"` pointers in `FlingVulkan.h`, defines them in `VulkanLoader.cpp`, +and loads them. A command added to the list is added to all three. + +Groups: + +- `FLING_VK_GLOBAL` — loader commands, from `vkGetInstanceProcAddr(nullptr, ...)`. +- `FLING_VK_GLOBAL_OPTIONAL` — same level. A null pointer is not an error. + `vkEnumerateInstanceVersion` is in this group. Older loaders may not export it. +- `FLING_VK_INSTANCE` — instance commands, including commands whose first + parameter is `VkPhysicalDevice`. Load them with `vkGetInstanceProcAddr(instance, ...)`. +- `FLING_VK_INSTANCE_OPTIONAL` — extension commands that may be absent. + `vkCreateDebugUtilsMessengerEXT` and `vkDestroyDebugUtilsMessengerEXT` are here. + A null pointer is not an error. +- `FLING_VK_DEVICE` — commands whose first parameter is `VkDevice`, `VkQueue`, + or `VkCommandBuffer`. Load them with `vkGetDeviceProcAddr`. +- `FLING_VK_DEVICE_OPTIONAL` — same level. A null pointer is not an error. + The group is empty today. + +`vkGetDeviceProcAddr` takes `VkDevice`, but it is loaded with +`vkGetInstanceProcAddr`. It is how the device commands are loaded. Physical-device +commands stay on the instance list. + +Each pointer is a global with the C API name: ```cpp -// Loader level: from vkGetInstanceProcAddr(nullptr, ...) -FLING_VK_GLOBAL(vkCreateInstance) -FLING_VK_GLOBAL(vkEnumerateInstanceExtensionProperties) -FLING_VK_GLOBAL(vkEnumerateInstanceLayerProperties) - -// Instance level: from vkGetInstanceProcAddr(instance, ...) -FLING_VK_INSTANCE(vkDestroyInstance) -FLING_VK_INSTANCE(vkEnumeratePhysicalDevices) -FLING_VK_INSTANCE(vkGetPhysicalDeviceSurfaceCapabilitiesKHR) -FLING_VK_INSTANCE(vkCreateDevice) -// ... - -// Device level: from vkGetDeviceProcAddr(device, ...) -FLING_VK_DEVICE(vkCreateBuffer) -FLING_VK_DEVICE(vkCmdDraw) -FLING_VK_DEVICE(vkQueuePresentKHR) -// ... +extern "C" PFN_vkCreateBuffer vkCreateBuffer; ``` -- The same list declares the pointers (in `FlingVulkan.h`), defines them, and - loads them (in `VulkanLoader.cpp`). A function that is added to one place is - added to all three. -- Each pointer is a global with the same name as the Vulkan function: - `extern PFN_vkCreateBuffer vkCreateBuffer;`. `VK_NO_PROTOTYPES` removes the - real declarations, so these names are free, and call sites do not change. - This is the same approach that the Khronos samples use when they load - Vulkan dynamically. -- The SDK's `vk.xml` registry says at which level each function is (a - `VkDevice`, `VkQueue`, or `VkCommandBuffer` first parameter means device - level). The list is short enough to keep by hand. A generator from `vk.xml` - is an option later, if the list grows a lot. -- A function from an extension (`vkCreateSwapchainKHR`, debug utils) is - loaded only after its extension is enabled. A null pointer for an optional - extension is not an error. For a required extension it is. +`VK_NO_PROTOTYPES` removes the real declarations, so these names are free and +call sites do not change. + +A null required pointer logs `Required Vulkan function is null: ` and +stops the process with `ExitCode::Crash`. A null optional pointer is not an +error. + +`FlingTests/VulkanLoad/CheckVulkanCalls.cmake` fails when a first-party +`vk[A-Z]` call is missing from the list. CTest runs it as +`Vulkan calls are listed`. Do not generate the list from `vk.xml` unless the +hand-kept list becomes a problem. ### 3. Loader -`VulkanLoader` (Graphics, private) has three steps. `VulkanApp` calls them in -order: +`Fling::DynamicLibrary` (Core) is the only code that calls `LoadLibrary` or +`dlopen`. Graphics does not call `LoadLibrary`, `dlopen`, `GetProcAddress`, or +`dlsym`. + +`DynamicLibrary` is move-only. `Open` loads one file name. `OpenFirst` tries +each name and returns the first library that opens. `Symbol` returns an +address, or null when the library is closed or the symbol is missing. `Close` +unloads the library and is safe to call twice. On Windows it uses `LoadLibraryW` +(UTF-8 name to UTF-16), `GetProcAddress`, and `FreeLibrary`. On Linux it uses +`dlopen(name, RTLD_NOW | RTLD_LOCAL)`, `dlsym`, and `dlclose`. There is no flag +parameter. macOS is not a target. + +`VulkanLoader` (Graphics, private) uses that type: | Step | Called from | Does | | --- | --- | --- | -| `LoadLoader()` | Start of `VulkanApp::Init` | `LoadLibraryW(L"vulkan-1.dll")` on Windows, `dlopen("libvulkan.so.1", RTLD_NOW \| RTLD_LOCAL)` on Linux. Gets `vkGetInstanceProcAddr`, then the loader-level pointers. Returns false if the library or the symbol is missing | -| `LoadInstance(VkInstance)` | After `vkCreateInstance` (`Instance.cpp`) | Instance-level pointers | -| `LoadDevice(VkDevice)` | After `vkCreateDevice` (`LogicalDevice.cpp`) | Device-level pointers, from `vkGetDeviceProcAddr` | - -- `UnloadLoader()` runs at the end of `VulkanApp::Shutdown`, after - `vkDestroyInstance`. It sets every pointer back to null. -- If `LoadLoader()` fails, `VulkanApp::Init` logs - "Vulkan runtime not found: install a GPU driver with Vulkan support" and the - engine exits with `ExitCode::Crash`. If a required pointer is null after - `LoadInstance` or `LoadDevice`, it logs the function name. -- Under `--null-render`, `VulkanApp::Init` is not called, so the loader is - never loaded. -- macOS (MoltenVK) is not a target today (`Platform.h` stops the build). If it - becomes one, `LoadLoader()` also tries `libvulkan.1.dylib`. +| `LoadLoader()` | Start of `VulkanApp::Init`, before GLFW | Linux: `OpenFirst` of `libvulkan.so.1`, then `libvulkan.so`. Windows: `Open("vulkan-1.dll")`. `SymbolAs` reads `vkGetInstanceProcAddr` from the library, then the global commands are loaded. Returns false when the library or that symbol is missing | +| `LoadInstance(VkInstance)` | After `vkCreateInstance` (`Instance.cpp`) | Instance commands, from `vkGetInstanceProcAddr` | +| `LoadDevice(VkDevice)` | After `vkCreateDevice` (`LogicalDevice.cpp`) | Device commands, from `vkGetDeviceProcAddr` | +| `UnloadLoader()` | End of `VulkanApp::Shutdown`, after `vkDestroyInstance` | Sets every Vulkan function pointer to null, then `Close()` | + +`vkGetInstanceProcAddr` is taken from the library with `Symbol`. It is not +replaced by calling `vkGetInstanceProcAddr` on itself. Vulkan 1.0 and 1.1 +loaders do not return that pointer. + +When `LoadLoader()` fails, `VulkanApp::Init` logs +"Vulkan runtime not found: install a GPU driver with Vulkan support" and +throws. `main` catches that and returns `ExitCode::Crash` (2). There is no +message box. Under `--null-render`, `VulkanApp::Init` is not called, so +`DynamicLibrary::Open` is not called. ### 4. GLFW -GLFW loads the Vulkan loader itself for `glfwCreateWindowSurface` and -`glfwGetRequiredInstanceExtensions`. GLFW 3.4 and newer (the vendored version -is 3.5) has `glfwInitVulkanLoader(PFN_vkGetInstanceProcAddr)`. Call it with our -`vkGetInstanceProcAddr` before `glfwInit()`, so that GLFW uses the same loader -and does not open a second copy. +GLFW 3.4 and newer can take our loader. `DesktopWindow` calls +`glfwInitVulkanLoader(vkGetInstanceProcAddr)` and then `glfwInit()`. GLFW uses +that pointer and does not open a second copy. `LoadLoader()` runs first, so the +pointer is set. ### 5. CMake -- Graphics links `Vulkan::Headers` (from CMake's `FindVulkan`) instead of - `Vulkan::Vulkan`. -- Remove the `/DELAYLOAD:vulkan-1.dll` and `delayimp` lines from - `FlingEngine/Graphics/CMakeLists.txt`. -- Linux: link `${CMAKE_DL_LIBS}` for `dlopen` (glibc 2.34 and newer has it in - libc, but older systems need `libdl`). -- `find_package(Vulkan REQUIRED)` stays. The SDK is still how a developer gets - the headers, and the validation layers for debug runs. +- Graphics links `Vulkan::Headers`, not `Vulkan::Vulkan`. +- `/DELAYLOAD:vulkan-1.dll` and `delayimp` are gone. +- `find_package(Vulkan REQUIRED)` stays. Configure time still needs the SDK + library so CMake can find the headers. The linked Sandbox binary must not + import `libvulkan` or `vulkan-1.dll`. +- Core links `${CMAKE_DL_LIBS}` privately. `dlopen` is in libc on glibc 2.34 + and newer. Older glibc keeps it in `libdl`. Graphics does not call `dlopen`. ## Tests -- **No Vulkan import.** A CTest that fails if the Sandbox binary imports the - Vulkan loader: `readelf -d` (no `libvulkan` in `NEEDED`) on Linux, and the - PE import table on Windows (`dumpbin /imports` or `objdump -p`). Run it with - `cmake -P`, like `FlingTests/CrashProbe/ExpectCrash.cmake`. -- **Headless with no runtime.** One Linux CI job runs the headless CTest tests - in a container without the `libvulkan1` package (build in a separate step - that has it). -- **Missing runtime in a windowed run.** In the same container with no - `libvulkan1`, a windowed Sandbox run must exit with code 2 and log +- **No Vulkan import.** `Sandbox: no Vulkan import` runs + `FlingTests/VulkanLoad/ExpectNoVulkanImport.cmake`. Linux: `readelf -d` has + no `libvulkan` in `NEEDED`. Windows: the PE import table (and the delay-import + table) has no `vulkan-1.dll`, read with `objdump -p`, `dumpbin /IMPORTS`, or + `pe_imports.py`. +- **Call list.** `Vulkan calls are listed` runs `CheckVulkanCalls.cmake`. +- **Headless.** `Sandbox --null-render --unattended --frames 120 --seed 1` + exits 0. It does not open the loader. +- **Missing runtime.** The `Linux no Vulkan runtime` CI job builds with the SDK + installed, then runs Sandbox in a mount namespace where `libvulkan.so.1` and + `libvulkan.so` are not loadable. The windowed run must exit 2 and log "Vulkan runtime not found". `LoadLoader()` runs before GLFW opens a window, - so the test does not need a display. -- The existing headless and windowed runs must give the same results as - before. + so the test does not need a display. The same namespace runs the headless + command and expects exit 0. The other CI jobs are unchanged. ## Phases -Each phase must leave the engine, Sandbox, and FlingTests building and passing -on every CI platform. - -| Phase | Work | Depends on | +| Phase | Status | Work | | --- | --- | --- | -| **L1** | `VK_NO_PROTOTYPES` in `FlingVulkan.h`, `VulkanFunctions.inl`, `VulkanLoader`, calls from `VulkanApp`, `Instance`, and `LogicalDevice`. Link `Vulkan::Headers`. Remove `/DELAYLOAD`. "No Vulkan import" test | None | -| **L2** | `glfwInitVulkanLoader`. Clear error and `ExitCode::Crash` when the runtime is missing. Linux CI job with no `libvulkan1` | L1 | -| **Later** | Generate `VulkanFunctions.inl` from `vk.xml`, if the hand-kept list becomes a problem. A per-device table, if the engine uses more than one device | L1 | - -When L1 is done, update the Graphics rules in -[`Skills/testing.md`](../Skills/testing.md#headless-runs) and -[`Skills/architecture.md`](../Skills/architecture.md): include Vulkan only -through `FlingVulkan.h`, and add each new Vulkan function to -`VulkanFunctions.inl`. - -## Open questions - -- Is the hand-kept list enough, or do we generate it from `vk.xml` from the - start? About 90 functions today suggests that by hand is fine. -- Do device-level functions get loaded with `vkGetDeviceProcAddr` (faster - calls, one device only), or all with `vkGetInstanceProcAddr` (slower calls, - but works with any device)? The plan picks `vkGetDeviceProcAddr`, because - there is one device. -- Does the "runtime not found" case need a message box for players (not - `--unattended`), or is a log line and exit code enough? +| **L1** | Done | `VK_NO_PROTOTYPES`, `VulkanFunctions.inl`, `VulkanLoader`, `DynamicLibrary`. Link `Vulkan::Headers`. Remove `/DELAYLOAD`. No-import test and call-list test | +| **L2** | Done | `glfwInitVulkanLoader`. Log line and `ExitCode::Crash` when the runtime is missing. Linux CI job with the loader not loadable | +| **Later** | Not started | Generate `VulkanFunctions.inl` from `vk.xml` if the hand-kept list becomes a problem. A per-device table, if the engine uses more than one device | + +## Decisions + +- The hand-kept list is enough. A CTest fails when a first-party `vk[A-Z]` call + is missing from it. Do not generate from `vk.xml` now. +- Device commands load with `vkGetDeviceProcAddr`. There is one `LogicalDevice`, + so one table is correct. Physical-device commands stay on the instance list. +- A missing runtime logs one line and exits with code 2. There is no player + message box. diff --git a/Sandbox/CMakeLists.txt b/Sandbox/CMakeLists.txt index 46aa16d5..ad0cb39e 100644 --- a/Sandbox/CMakeLists.txt +++ b/Sandbox/CMakeLists.txt @@ -52,10 +52,19 @@ if( FLING_BUILD_TESTS ) PASS_REGULAR_EXPRESSION "Fling exit: code=3 ticks=0" ) + # The linked binary must not import libvulkan / vulkan-1.dll. The loader is + # opened at runtime by Fling::DynamicLibrary. + add_test(NAME "Sandbox: no Vulkan import" + COMMAND ${CMAKE_COMMAND} + -DBINARY=$ + -P "${FLING_ROOT_DIR}/FlingTests/VulkanLoad/ExpectNoVulkanImport.cmake" + ) + set_tests_properties( "Sandbox: headless run" "Sandbox: headless json logs" "Sandbox: bad launch argument" + "Sandbox: no Vulkan import" PROPERTIES TIMEOUT 120 ) endif() diff --git a/Skills/architecture.md b/Skills/architecture.md index e7b169e9..c97972f8 100644 --- a/Skills/architecture.md +++ b/Skills/architecture.md @@ -11,9 +11,11 @@ touching CMake targets or cross-folder includes. The phases of the split are in - `FlingEngine/Core` — the `Core` module (`Fling::Core`, built with `fling_add_module()`): platform macros, logging, math, memory, timing, - `FlingPaths`, the platform-independent `Input` class, and launch support - (`LaunchOptions`, `ExitCode`, `CrashHandler`). Core includes and - links no other engine module, and its own include path enforces that. + `FlingPaths`, the platform-independent `Input` class, launch support + (`LaunchOptions`, `ExitCode`, `CrashHandler`), and `DynamicLibrary`. + `DynamicLibrary` is the only code that calls `LoadLibrary` or `dlopen`. + Core includes and links no other engine module, and it does not depend on + Vulkan. Its include path enforces that. `WITH_IMGUI`, `WITH_EDITOR`, `WITH_CRASH_HANDLER`, and `FLING_SHIPPING` are PUBLIC definitions on Core, so every target sees them. - `FlingEngine/Resources` — the `Resources` module (depends on Core): config @@ -31,7 +33,11 @@ touching CMake targets or cross-folder includes. The phases of the split are in Engine): the Vulkan renderer, window, GPU resources, render components (`MeshRenderer`, lights), and the GLFW input backends (`LinuxInput`, `WindowsInput`, keycodes). Developer UI plugs in through `IDebugUI`, so - Graphics never includes Editor headers. + Graphics never includes Editor headers. Include Vulkan only through + `FlingVulkan.h` (`VK_NO_PROTOTYPES`). Add every new `vk*` call to + `VulkanFunctions.inl`. `VulkanLoader` loads those pointers with + `DynamicLibrary`. Graphics links `Vulkan::Headers` and does not link the + Vulkan loader. - `FlingEngine/Application` — the `Application` module (depends on Engine and Graphics): the top-level startup layer. `Engine.h`/`Engine.cpp` (`Engine::Run`, the main loop) and `FlingEngine.h`. It boots a game client, diff --git a/Skills/testing.md b/Skills/testing.md index 0783fbd4..9d01489e 100644 --- a/Skills/testing.md +++ b/Skills/testing.md @@ -121,6 +121,12 @@ the game on machines that have no display and no Vulkan device: A flag takes its value as `--frames 120` or `--frames=120`. The flags are parsed by `LaunchOptions::Parse` (`FlingEngine/Core/inc/LaunchOptions.h`). +`--null-render` does not call `VulkanApp::Init`, so it does not open the Vulkan +loader. The Sandbox binary does not import `libvulkan` or `vulkan-1.dll`. +CTest checks that (`Sandbox: no Vulkan import`) and checks that every +first-party `vk[A-Z]` call is named in `VulkanFunctions.inl` +(`Vulkan calls are listed`). + Exit codes (`Fling::ExitCode` in `ExitCode.h`): `0` success, `1` scenario or assertion failure, `2` crash or fatal error, `3` bad arguments or bad content. At exit the engine prints one summary line to stdout, for example From e857d41898d37bafd08cba4e952223b7a948c1a0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 25 Oct 2026 12:40:57 +0000 Subject: [PATCH 2/4] Reject unsigned Windows DLLs in DynamicLibrary. WinVerifyTrust runs before LoadLibraryExW. A bare name is resolved in System32, then the executable directory. The current directory and PATH are not searched. Co-authored-by: Ben Hoffman --- FlingEngine/Core/CMakeLists.txt | 5 + FlingEngine/Core/inc/DynamicLibrary.h | 13 +- FlingEngine/Core/src/DynamicLibrary.cpp | 199 +++++++++++++++++++++++- FlingTests/Core/DynamicLibraryTests.cpp | 160 +++++++++++++++++++ Plans/vulkan-loading.md | 41 ++++- Skills/architecture.md | 4 +- 6 files changed, 409 insertions(+), 13 deletions(-) diff --git a/FlingEngine/Core/CMakeLists.txt b/FlingEngine/Core/CMakeLists.txt index 799a11c6..bcf5ca6d 100644 --- a/FlingEngine/Core/CMakeLists.txt +++ b/FlingEngine/Core/CMakeLists.txt @@ -14,6 +14,11 @@ fling_add_module(Core # DynamicLibrary is the only dlopen caller. Older glibc keeps it in libdl. target_link_libraries(Core PRIVATE ${CMAKE_DL_LIBS}) +# DynamicLibrary calls WinVerifyTrust before LoadLibraryExW. +if(WIN32) + target_link_libraries(Core PRIVATE wintrust) +endif() + # Feature flags are read from Core headers (FlingTypes.h) and from every module # that links Core, so they live here and propagate to everything. target_compile_definitions(Core PUBLIC diff --git a/FlingEngine/Core/inc/DynamicLibrary.h b/FlingEngine/Core/inc/DynamicLibrary.h index 33fc9b1b..e36cb507 100644 --- a/FlingEngine/Core/inc/DynamicLibrary.h +++ b/FlingEngine/Core/inc/DynamicLibrary.h @@ -10,8 +10,16 @@ namespace Fling { /** * Load and unload one shared library. * - * This is the only Fling type that calls LoadLibrary or dlopen. + * This is the only Fling type that calls LoadLibraryEx or dlopen. * Move a DynamicLibrary. Do not copy it. + * + * On Windows, check the Authenticode signature before the load. + * A valid signature is embedded or catalog. + * Resolve a bare name in System32, then in the executable directory. + * Windows does not search the current directory or PATH. + * An explicit path contains a slash, a backslash, or a drive prefix. + * Verify and load that path only. + * Linux calls dlopen. Linux does not check a signature. */ class CORE_API DynamicLibrary { public: @@ -31,7 +39,8 @@ namespace Fling { * * An empty object means that the load failed. When t_Error is not * null, write the platform message there on failure. Clear t_Error - * on success. + * on success. On Windows, a missing file sets + * "The shared library was not found". */ static DynamicLibrary Open(std::string_view t_Name, std::string* t_Error = nullptr); diff --git a/FlingEngine/Core/src/DynamicLibrary.cpp b/FlingEngine/Core/src/DynamicLibrary.cpp index 84a97a15..ab332a25 100644 --- a/FlingEngine/Core/src/DynamicLibrary.cpp +++ b/FlingEngine/Core/src/DynamicLibrary.cpp @@ -9,6 +9,10 @@ #if FLING_WINDOWS +#include +#include +#include + namespace { std::wstring Utf8ToWide(std::string_view t_Text) { if (t_Text.empty() || t_Text.size() > static_cast(std::numeric_limits::max())) { @@ -68,10 +72,146 @@ namespace { LocalFree(Buffer); } if (Message.empty()) { - Message = "LoadLibraryW failed (code " + std::to_string(t_Code) + ")"; + Message = "LoadLibraryExW failed (code " + std::to_string(t_Code) + ")"; } return Message; } + + bool IsExplicitPath(std::wstring_view t_Name) { + if (t_Name.find(L'\\') != std::wstring_view::npos || t_Name.find(L'/') != std::wstring_view::npos) { + return true; + } + if (t_Name.size() >= 2 && t_Name[1] == L':') { + const wchar_t Drive = t_Name[0]; + if ((Drive >= L'A' && Drive <= L'Z') || (Drive >= L'a' && Drive <= L'z')) { + return true; + } + } + return false; + } + + std::wstring FullPath(const std::wstring& t_Path) { + const DWORD Needed = GetFullPathNameW(t_Path.c_str(), 0, nullptr, nullptr); + if (Needed == 0) { + return {}; + } + + std::wstring Path(static_cast(Needed), L'\0'); + const DWORD Written = GetFullPathNameW(t_Path.c_str(), Needed, Path.data(), nullptr); + if (Written == 0 || Written >= Needed) { + return {}; + } + Path.resize(Written); + return Path; + } + + std::wstring SystemDirectory() { + const UINT Needed = GetSystemDirectoryW(nullptr, 0); + if (Needed == 0) { + return {}; + } + + std::wstring Path(static_cast(Needed), L'\0'); + const UINT Written = GetSystemDirectoryW(Path.data(), Needed); + if (Written == 0 || Written >= Needed) { + return {}; + } + Path.resize(Written); + return Path; + } + + std::wstring ExecutableDirectory() { + std::wstring Path(MAX_PATH, L'\0'); + DWORD Written = GetModuleFileNameW(nullptr, Path.data(), static_cast(Path.size())); + while (Written == Path.size()) { + Path.resize(Path.size() * 2); + Written = GetModuleFileNameW(nullptr, Path.data(), static_cast(Path.size())); + } + if (Written == 0) { + return {}; + } + + Path.resize(Written); + const size_t Slash = Path.find_last_of(L"\\/"); + if (Slash == std::wstring::npos) { + return {}; + } + return Path.substr(0, Slash); + } + + std::wstring JoinPath(std::wstring_view t_Directory, std::wstring_view t_Name) { + std::wstring Path; + Path.reserve(t_Directory.size() + 1 + t_Name.size()); + Path.assign(t_Directory.begin(), t_Directory.end()); + if (!Path.empty() && Path.back() != L'\\' && Path.back() != L'/') { + Path.push_back(L'\\'); + } + Path.append(t_Name.begin(), t_Name.end()); + return Path; + } + + enum class Candidate { Missing, Found, Failed }; + + Candidate ProbeCandidate(const std::wstring& t_Path) { + const DWORD Attributes = GetFileAttributesW(t_Path.c_str()); + if (Attributes != INVALID_FILE_ATTRIBUTES) { + return Candidate::Found; + } + + const DWORD Code = GetLastError(); + if (Code == ERROR_FILE_NOT_FOUND || Code == ERROR_PATH_NOT_FOUND) { + return Candidate::Missing; + } + return Candidate::Failed; + } + + enum class Signature { Valid, NotSigned, NotTrusted }; + + struct SignatureResult { + Signature Status = Signature::NotTrusted; + LONG Code = 0; + }; + + SignatureResult VerifyAuthenticode(const std::wstring& t_Path) { + WINTRUST_FILE_INFO FileInfo{}; + FileInfo.cbStruct = sizeof(FileInfo); + FileInfo.pcwszFilePath = t_Path.c_str(); + + GUID Action = WINTRUST_ACTION_GENERIC_VERIFY_V2; + + WINTRUST_DATA Trust{}; + Trust.cbStruct = sizeof(Trust); + Trust.dwUIChoice = WTD_UI_NONE; + Trust.fdwRevocationChecks = WTD_REVOKE_NONE; + Trust.dwUnionChoice = WTD_CHOICE_FILE; + Trust.dwStateAction = WTD_STATEACTION_VERIFY; + Trust.dwProvFlags = WTD_SAFER_FLAG | WTD_CACHE_ONLY_URL_RETRIEVAL; + Trust.pFile = &FileInfo; + + const LONG Status = WinVerifyTrust(nullptr, &Action, &Trust); + + Trust.dwStateAction = WTD_STATEACTION_CLOSE; + WinVerifyTrust(nullptr, &Action, &Trust); + + SignatureResult Result; + Result.Code = Status; + if (Status == ERROR_SUCCESS) { + Result.Status = Signature::Valid; + } else if (Status == static_cast(TRUST_E_NOSIGNATURE) || + Status == static_cast(TRUST_E_SUBJECT_FORM_UNKNOWN) || + Status == static_cast(TRUST_E_PROVIDER_UNKNOWN)) { + Result.Status = Signature::NotSigned; + } else { + Result.Status = Signature::NotTrusted; + } + return Result; + } + + std::string UntrustedMessage(LONG t_Status) { + char Hex[16] = {}; + std::snprintf(Hex, sizeof(Hex), "%08lX", static_cast(t_Status)); + return std::string("The shared library signature is not trusted (0x") + Hex + ")"; + } } // namespace #elif FLING_LINUX @@ -139,7 +279,62 @@ namespace Fling { return {}; } - HMODULE Handle = LoadLibraryW(WideName.c_str()); + std::wstring Path; + if (IsExplicitPath(WideName)) { + Path = FullPath(WideName); + if (Path.empty()) { + WriteError(t_Error, WindowsErrorMessage(GetLastError())); + return {}; + } + const Candidate Lookup = ProbeCandidate(Path); + if (Lookup == Candidate::Failed) { + WriteError(t_Error, WindowsErrorMessage(GetLastError())); + return {}; + } + if (Lookup != Candidate::Found) { + WriteError(t_Error, "The shared library was not found"); + return {}; + } + } else { + const std::wstring SystemDir = SystemDirectory(); + const std::wstring SystemCandidate = SystemDir.empty() ? std::wstring{} : JoinPath(SystemDir, WideName); + const Candidate SystemLookup = + SystemCandidate.empty() ? Candidate::Missing : ProbeCandidate(SystemCandidate); + if (SystemLookup == Candidate::Failed) { + WriteError(t_Error, WindowsErrorMessage(GetLastError())); + return {}; + } + if (SystemLookup == Candidate::Found) { + Path = SystemCandidate; + } else { + const std::wstring ExeDir = ExecutableDirectory(); + const std::wstring ExeCandidate = ExeDir.empty() ? std::wstring{} : JoinPath(ExeDir, WideName); + const Candidate ExeLookup = ExeCandidate.empty() ? Candidate::Missing : ProbeCandidate(ExeCandidate); + if (ExeLookup == Candidate::Failed) { + WriteError(t_Error, WindowsErrorMessage(GetLastError())); + return {}; + } + if (ExeLookup != Candidate::Found) { + WriteError(t_Error, "The shared library was not found"); + return {}; + } + Path = ExeCandidate; + } + } + + const SignatureResult Checked = VerifyAuthenticode(Path); + if (Checked.Status == Signature::NotSigned) { + WriteError(t_Error, "The shared library is not signed"); + return {}; + } + if (Checked.Status != Signature::Valid) { + WriteError(t_Error, UntrustedMessage(Checked.Code)); + return {}; + } + + // LoadLibraryExW does not open a dialog when the DLL is missing. + HMODULE Handle = + LoadLibraryExW(Path.c_str(), nullptr, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32); if (Handle == nullptr) { WriteError(t_Error, WindowsErrorMessage(GetLastError())); return {}; diff --git a/FlingTests/Core/DynamicLibraryTests.cpp b/FlingTests/Core/DynamicLibraryTests.cpp index 9ea7c122..4e1e9699 100644 --- a/FlingTests/Core/DynamicLibraryTests.cpp +++ b/FlingTests/Core/DynamicLibraryTests.cpp @@ -4,10 +4,15 @@ #include "Platform.h" #include +#include #include #include #include +#if FLING_LINUX +#include +#endif + namespace { #if FLING_LINUX constexpr const char* LibraryName = "libc.so.6"; @@ -33,6 +38,104 @@ namespace { REQUIRE(Fn() != 0); #endif } + +#if FLING_WINDOWS + std::string WideToUtf8(std::wstring_view t_Text) { + if (t_Text.empty()) { + return {}; + } + const int Size = static_cast(t_Text.size()); + const int Needed = WideCharToMultiByte(CP_UTF8, 0, t_Text.data(), Size, nullptr, 0, nullptr, nullptr); + REQUIRE(Needed > 0); + std::string Text(static_cast(Needed), '\0'); + WideCharToMultiByte(CP_UTF8, 0, t_Text.data(), Size, Text.data(), Needed, nullptr, nullptr); + return Text; + } + + std::wstring TempDirectory() { + wchar_t Buffer[MAX_PATH] = {}; + const DWORD Length = GetTempPathW(MAX_PATH, Buffer); + REQUIRE(Length > 0); + REQUIRE(Length < MAX_PATH); + return std::wstring(Buffer, Length); + } + + void WriteTempBytes(const std::wstring& t_Path) { + const HANDLE File = + CreateFileW(t_Path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + REQUIRE(File != INVALID_HANDLE_VALUE); + const char Bytes[] = {'M', 'Z', 0, 0}; + DWORD Written = 0; + const BOOL Ok = WriteFile(File, Bytes, sizeof(Bytes), &Written, nullptr); + CloseHandle(File); + REQUIRE(Ok); + REQUIRE(Written == sizeof(Bytes)); + } + + /** Delete one file when this guard leaves scope. */ + class DeleteFileGuard { + public: + explicit DeleteFileGuard(std::wstring t_Path) : m_Path(std::move(t_Path)) {} + + ~DeleteFileGuard() { DeleteFileW(m_Path.c_str()); } + + DeleteFileGuard(const DeleteFileGuard&) = delete; + DeleteFileGuard& operator=(const DeleteFileGuard&) = delete; + + private: + std::wstring m_Path; + }; + + /** Delete one empty directory when this guard leaves scope. */ + class DeleteDirectoryGuard { + public: + explicit DeleteDirectoryGuard(std::wstring t_Path) : m_Path(std::move(t_Path)) {} + + ~DeleteDirectoryGuard() { RemoveDirectoryW(m_Path.c_str()); } + + DeleteDirectoryGuard(const DeleteDirectoryGuard&) = delete; + DeleteDirectoryGuard& operator=(const DeleteDirectoryGuard&) = delete; + + private: + std::wstring m_Path; + }; + + /** + * Restore the process current directory when this guard leaves scope. + * A failed REQUIRE still runs the destructor. + */ + class CurrentDirectoryGuard { + public: + CurrentDirectoryGuard() { + const DWORD Needed = GetCurrentDirectoryW(0, nullptr); + if (Needed == 0) { + return; + } + m_Previous.resize(Needed); + const DWORD Written = GetCurrentDirectoryW(Needed, m_Previous.data()); + if (Written == 0 || Written >= Needed) { + m_Previous.clear(); + return; + } + m_Previous.resize(Written); + } + + ~CurrentDirectoryGuard() { + if (!m_Previous.empty()) { + SetCurrentDirectoryW(m_Previous.c_str()); + } + } + + CurrentDirectoryGuard(const CurrentDirectoryGuard&) = delete; + CurrentDirectoryGuard& operator=(const CurrentDirectoryGuard&) = delete; + + /** True when the previous directory was saved. */ + bool Captured() const { return !m_Previous.empty(); } + + private: + std::wstring m_Previous; + }; +#endif } // namespace TEST_CASE("DynamicLibrary", "[core][DynamicLibrary]") { @@ -102,4 +205,61 @@ TEST_CASE("DynamicLibrary", "[core][DynamicLibrary]") { REQUIRE_FALSE(static_cast(Outer)); REQUIRE(Outer.Symbol(SymbolName) == nullptr); } + +#if FLING_WINDOWS + SECTION("An unsigned absolute path is rejected") { + const std::wstring Path = + TempDirectory() + L"fling_unsigned_" + std::to_wstring(GetCurrentProcessId()) + L".dll"; + const DeleteFileGuard RemoveFile(Path); + WriteTempBytes(Path); + + std::string Error; + Fling::DynamicLibrary Library = Fling::DynamicLibrary::Open(WideToUtf8(Path), &Error); + REQUIRE_FALSE(static_cast(Library)); + REQUIRE(Error.find("not signed") != std::string::npos); + } + + SECTION("A bare name does not search the current directory") { + const DWORD ProcessId = GetCurrentProcessId(); + const std::wstring Dir = TempDirectory() + L"fling_dynlib_cwd_" + std::to_wstring(ProcessId); + const BOOL Created = CreateDirectoryW(Dir.c_str(), nullptr); + REQUIRE((Created != FALSE || GetLastError() == ERROR_ALREADY_EXISTS)); + const DeleteDirectoryGuard RemoveDir(Dir); + + const std::wstring Bare = L"fling_planted_" + std::to_wstring(ProcessId) + L".dll"; + const DeleteFileGuard RemoveFile(Dir + L"\\" + Bare); + WriteTempBytes(Dir + L"\\" + Bare); + + const CurrentDirectoryGuard Cwd; + REQUIRE(Cwd.Captured()); + REQUIRE(SetCurrentDirectoryW(Dir.c_str())); + + std::string Error; + Fling::DynamicLibrary Library = Fling::DynamicLibrary::Open(WideToUtf8(Bare), &Error); + REQUIRE_FALSE(static_cast(Library)); + REQUIRE(Error.find("not signed") == std::string::npos); + } +#endif + +#if FLING_LINUX + SECTION("An absolute path to a text file fails") { + const std::string Path = "/tmp/fling_dynlib_" + std::to_string(static_cast(::getpid())) + ".txt"; + struct RemoveFile { + std::string Path; + ~RemoveFile() { std::remove(Path.c_str()); } + } const Guard{Path}; + + FILE* File = std::fopen(Path.c_str(), "wb"); + REQUIRE(File != nullptr); + const char Bytes[] = "not a shared library\n"; + const std::size_t Written = std::fwrite(Bytes, 1, sizeof(Bytes) - 1, File); + std::fclose(File); + REQUIRE(Written == sizeof(Bytes) - 1); + + std::string Error; + Fling::DynamicLibrary Library = Fling::DynamicLibrary::Open(Path, &Error); + REQUIRE_FALSE(static_cast(Library)); + REQUIRE_FALSE(Error.empty()); + } +#endif } diff --git a/Plans/vulkan-loading.md b/Plans/vulkan-loading.md index 841734b1..a2097d6c 100644 --- a/Plans/vulkan-loading.md +++ b/Plans/vulkan-loading.md @@ -109,17 +109,41 @@ hand-kept list becomes a problem. ### 3. Loader -`Fling::DynamicLibrary` (Core) is the only code that calls `LoadLibrary` or -`dlopen`. Graphics does not call `LoadLibrary`, `dlopen`, `GetProcAddress`, or -`dlsym`. +`Fling::DynamicLibrary` (Core) is the only code that calls `LoadLibraryEx` or +`dlopen`. Graphics does not call `LoadLibrary`, `LoadLibraryEx`, `dlopen`, +`GetProcAddress`, or `dlsym`. `DynamicLibrary` is move-only. `Open` loads one file name. `OpenFirst` tries each name and returns the first library that opens. `Symbol` returns an address, or null when the library is closed or the symbol is missing. `Close` -unloads the library and is safe to call twice. On Windows it uses `LoadLibraryW` -(UTF-8 name to UTF-16), `GetProcAddress`, and `FreeLibrary`. On Linux it uses -`dlopen(name, RTLD_NOW | RTLD_LOCAL)`, `dlsym`, and `dlclose`. There is no flag -parameter. macOS is not a target. +unloads the library and is safe to call twice. + +On Windows, `Open` calls `WinVerifyTrust` with +`WINTRUST_ACTION_GENERIC_VERIFY_V2` before it maps the file. The check uses +`WTD_UI_NONE`, `WTD_REVOKE_NONE`, and +`WTD_SAFER_FLAG | WTD_CACHE_ONLY_URL_RETRIEVAL`. Startup shows no UI and does +not use the network. `ERROR_SUCCESS` means the signature is valid. That covers +an embedded signature and a catalog signature, so `kernel32.dll` still loads. +`TRUST_E_NOSIGNATURE`, `TRUST_E_SUBJECT_FORM_UNKNOWN`, and +`TRUST_E_PROVIDER_UNKNOWN` report "The shared library is not signed". Any other +failure reports "The shared library signature is not trusted". + +A bare Windows name is resolved in System32 (`GetSystemDirectoryW`), then in +the directory of the executable (`GetModuleFileNameW`). The current directory +and `PATH` are not searched. When a candidate file exists and the signature +check fails, `Open` returns that failure and does not try the next directory. +A name that contains a slash, a backslash, or a drive prefix is an explicit +path. `Open` resolves it with `GetFullPathNameW`, verifies that file, and does +not search. The load uses `LoadLibraryExW` on the absolute path that passed +the check, with +`LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32`. A missing +file reports "The shared library was not found". The name is UTF-8 and the +Win32 calls take UTF-16. A missing DLL does not open a dialog. `Symbol` uses +`GetProcAddress`. `Close` uses `FreeLibrary`. + +On Linux, `Open` uses `dlopen(name, RTLD_NOW | RTLD_LOCAL)`. Linux does not +check a signature. `Symbol` uses `dlsym` and `Close` uses `dlclose`. There is +no flag parameter. macOS is not a target. `VulkanLoader` (Graphics, private) uses that type: @@ -155,7 +179,8 @@ pointer is set. library so CMake can find the headers. The linked Sandbox binary must not import `libvulkan` or `vulkan-1.dll`. - Core links `${CMAKE_DL_LIBS}` privately. `dlopen` is in libc on glibc 2.34 - and newer. Older glibc keeps it in `libdl`. Graphics does not call `dlopen`. + and newer. Older glibc keeps it in `libdl`. On Windows, Core links + `wintrust` for `WinVerifyTrust`. Graphics does not call `dlopen`. ## Tests diff --git a/Skills/architecture.md b/Skills/architecture.md index c97972f8..25c55665 100644 --- a/Skills/architecture.md +++ b/Skills/architecture.md @@ -13,7 +13,9 @@ touching CMake targets or cross-folder includes. The phases of the split are in `fling_add_module()`): platform macros, logging, math, memory, timing, `FlingPaths`, the platform-independent `Input` class, launch support (`LaunchOptions`, `ExitCode`, `CrashHandler`), and `DynamicLibrary`. - `DynamicLibrary` is the only code that calls `LoadLibrary` or `dlopen`. + `DynamicLibrary` is the only code that calls `LoadLibraryEx` or `dlopen`. + On Windows it checks the Authenticode signature before the load and resolves + a bare name in System32, then the executable directory. Core includes and links no other engine module, and it does not depend on Vulkan. Its include path enforces that. `WITH_IMGUI`, `WITH_EDITOR`, `WITH_CRASH_HANDLER`, and `FLING_SHIPPING` are PUBLIC definitions on From 87b873f181436c1015fd0bccbe51157197e93a3c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 25 Oct 2026 12:48:55 +0000 Subject: [PATCH 3/4] Put opening braces on the next line. The project brace style is Allman. clang-format left `{` on the same line because .clang-format did not set BreakBeforeBraces. Co-authored-by: Ben Hoffman --- .clang-format | 3 + FlingEngine/Core/inc/DynamicLibrary.h | 9 +- FlingEngine/Core/src/DynamicLibrary.cpp | 253 ++++++++++----- FlingEngine/Graphics/src/DesktopWindow.cpp | 314 ++++++++++--------- FlingEngine/Graphics/src/DesktopWindow.h | 81 +++-- FlingEngine/Graphics/src/FlingVulkan.h | 9 +- FlingEngine/Graphics/src/Instance.cpp | 346 +++++++++++---------- FlingEngine/Graphics/src/LogicalDevice.cpp | 164 +++++----- FlingEngine/Graphics/src/VulkanLoader.cpp | 63 ++-- FlingEngine/Graphics/src/VulkanLoader.h | 6 +- FlingTests/Core/DynamicLibraryTests.cpp | 75 +++-- 11 files changed, 743 insertions(+), 580 deletions(-) diff --git a/.clang-format b/.clang-format index 1f2859f6..4ffb40ad 100644 --- a/.clang-format +++ b/.clang-format @@ -21,6 +21,9 @@ NamespaceIndentation: All IndentCaseLabels: false IndentPPDirectives: None +# Braces. The opening brace goes on the next line. +BreakBeforeBraces: Allman + # Pointers / references PointerAlignment: Left ReferenceAlignment: Left diff --git a/FlingEngine/Core/inc/DynamicLibrary.h b/FlingEngine/Core/inc/DynamicLibrary.h index e36cb507..d37f33cb 100644 --- a/FlingEngine/Core/inc/DynamicLibrary.h +++ b/FlingEngine/Core/inc/DynamicLibrary.h @@ -6,7 +6,8 @@ #include #include -namespace Fling { +namespace Fling +{ /** * Load and unload one shared library. * @@ -21,7 +22,8 @@ namespace Fling { * Verify and load that path only. * Linux calls dlopen. Linux does not check a signature. */ - class CORE_API DynamicLibrary { + class CORE_API DynamicLibrary + { public: DynamicLibrary() = default; @@ -66,7 +68,8 @@ namespace Fling { * Return null when the library is closed or the symbol is missing. */ template - T_Fn SymbolAs(std::string_view t_Name) const { + T_Fn SymbolAs(std::string_view t_Name) const + { return reinterpret_cast(Symbol(t_Name)); } diff --git a/FlingEngine/Core/src/DynamicLibrary.cpp b/FlingEngine/Core/src/DynamicLibrary.cpp index ab332a25..fc2c29c1 100644 --- a/FlingEngine/Core/src/DynamicLibrary.cpp +++ b/FlingEngine/Core/src/DynamicLibrary.cpp @@ -13,35 +13,43 @@ #include #include -namespace { - std::wstring Utf8ToWide(std::string_view t_Text) { - if (t_Text.empty() || t_Text.size() > static_cast(std::numeric_limits::max())) { +namespace +{ + std::wstring Utf8ToWide(std::string_view t_Text) + { + if (t_Text.empty() || t_Text.size() > static_cast(std::numeric_limits::max())) + { return {}; } const int Size = static_cast(t_Text.size()); const int Needed = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, t_Text.data(), Size, nullptr, 0); - if (Needed <= 0) { + if (Needed <= 0) + { return {}; } std::wstring Wide(static_cast(Needed), L'\0'); const int Written = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, t_Text.data(), Size, Wide.data(), Needed); - if (Written <= 0) { + if (Written <= 0) + { return {}; } return Wide; } - std::string WideToUtf8(std::wstring_view t_Text) { - if (t_Text.empty() || t_Text.size() > static_cast(std::numeric_limits::max())) { + std::string WideToUtf8(std::wstring_view t_Text) + { + if (t_Text.empty() || t_Text.size() > static_cast(std::numeric_limits::max())) + { return {}; } const int Size = static_cast(t_Text.size()); const int Needed = WideCharToMultiByte(CP_UTF8, 0, t_Text.data(), Size, nullptr, 0, nullptr, nullptr); - if (Needed <= 0) { + if (Needed <= 0) + { return {}; } @@ -50,7 +58,8 @@ namespace { return Text; } - std::string WindowsErrorMessage(DWORD t_Code) { + std::string WindowsErrorMessage(DWORD t_Code) + { wchar_t* Buffer = nullptr; const DWORD Flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS; const DWORD Length = FormatMessageW(Flags, @@ -62,117 +71,152 @@ namespace { nullptr); std::string Message; - if (Length > 0 && Buffer != nullptr) { + if (Length > 0 && Buffer != nullptr) + { Message = WideToUtf8(std::wstring_view(Buffer, Length)); - while (!Message.empty() && (Message.back() == '\n' || Message.back() == '\r' || Message.back() == ' ')) { + while (!Message.empty() && (Message.back() == '\n' || Message.back() == '\r' || Message.back() == ' ')) + { Message.pop_back(); } } - if (Buffer != nullptr) { + if (Buffer != nullptr) + { LocalFree(Buffer); } - if (Message.empty()) { + if (Message.empty()) + { Message = "LoadLibraryExW failed (code " + std::to_string(t_Code) + ")"; } return Message; } - bool IsExplicitPath(std::wstring_view t_Name) { - if (t_Name.find(L'\\') != std::wstring_view::npos || t_Name.find(L'/') != std::wstring_view::npos) { + bool IsExplicitPath(std::wstring_view t_Name) + { + if (t_Name.find(L'\\') != std::wstring_view::npos || t_Name.find(L'/') != std::wstring_view::npos) + { return true; } - if (t_Name.size() >= 2 && t_Name[1] == L':') { + if (t_Name.size() >= 2 && t_Name[1] == L':') + { const wchar_t Drive = t_Name[0]; - if ((Drive >= L'A' && Drive <= L'Z') || (Drive >= L'a' && Drive <= L'z')) { + if ((Drive >= L'A' && Drive <= L'Z') || (Drive >= L'a' && Drive <= L'z')) + { return true; } } return false; } - std::wstring FullPath(const std::wstring& t_Path) { + std::wstring FullPath(const std::wstring& t_Path) + { const DWORD Needed = GetFullPathNameW(t_Path.c_str(), 0, nullptr, nullptr); - if (Needed == 0) { + if (Needed == 0) + { return {}; } std::wstring Path(static_cast(Needed), L'\0'); const DWORD Written = GetFullPathNameW(t_Path.c_str(), Needed, Path.data(), nullptr); - if (Written == 0 || Written >= Needed) { + if (Written == 0 || Written >= Needed) + { return {}; } Path.resize(Written); return Path; } - std::wstring SystemDirectory() { + std::wstring SystemDirectory() + { const UINT Needed = GetSystemDirectoryW(nullptr, 0); - if (Needed == 0) { + if (Needed == 0) + { return {}; } std::wstring Path(static_cast(Needed), L'\0'); const UINT Written = GetSystemDirectoryW(Path.data(), Needed); - if (Written == 0 || Written >= Needed) { + if (Written == 0 || Written >= Needed) + { return {}; } Path.resize(Written); return Path; } - std::wstring ExecutableDirectory() { + std::wstring ExecutableDirectory() + { std::wstring Path(MAX_PATH, L'\0'); DWORD Written = GetModuleFileNameW(nullptr, Path.data(), static_cast(Path.size())); - while (Written == Path.size()) { + while (Written == Path.size()) + { Path.resize(Path.size() * 2); Written = GetModuleFileNameW(nullptr, Path.data(), static_cast(Path.size())); } - if (Written == 0) { + if (Written == 0) + { return {}; } Path.resize(Written); const size_t Slash = Path.find_last_of(L"\\/"); - if (Slash == std::wstring::npos) { + if (Slash == std::wstring::npos) + { return {}; } return Path.substr(0, Slash); } - std::wstring JoinPath(std::wstring_view t_Directory, std::wstring_view t_Name) { + std::wstring JoinPath(std::wstring_view t_Directory, std::wstring_view t_Name) + { std::wstring Path; Path.reserve(t_Directory.size() + 1 + t_Name.size()); Path.assign(t_Directory.begin(), t_Directory.end()); - if (!Path.empty() && Path.back() != L'\\' && Path.back() != L'/') { + if (!Path.empty() && Path.back() != L'\\' && Path.back() != L'/') + { Path.push_back(L'\\'); } Path.append(t_Name.begin(), t_Name.end()); return Path; } - enum class Candidate { Missing, Found, Failed }; + enum class Candidate + { + Missing, + Found, + Failed + }; - Candidate ProbeCandidate(const std::wstring& t_Path) { + Candidate ProbeCandidate(const std::wstring& t_Path) + { const DWORD Attributes = GetFileAttributesW(t_Path.c_str()); - if (Attributes != INVALID_FILE_ATTRIBUTES) { + if (Attributes != INVALID_FILE_ATTRIBUTES) + { return Candidate::Found; } const DWORD Code = GetLastError(); - if (Code == ERROR_FILE_NOT_FOUND || Code == ERROR_PATH_NOT_FOUND) { + if (Code == ERROR_FILE_NOT_FOUND || Code == ERROR_PATH_NOT_FOUND) + { return Candidate::Missing; } return Candidate::Failed; } - enum class Signature { Valid, NotSigned, NotTrusted }; + enum class Signature + { + Valid, + NotSigned, + NotTrusted + }; - struct SignatureResult { + struct SignatureResult + { Signature Status = Signature::NotTrusted; LONG Code = 0; }; - SignatureResult VerifyAuthenticode(const std::wstring& t_Path) { + SignatureResult VerifyAuthenticode(const std::wstring& t_Path) + { WINTRUST_FILE_INFO FileInfo{}; FileInfo.cbStruct = sizeof(FileInfo); FileInfo.pcwszFilePath = t_Path.c_str(); @@ -195,19 +239,25 @@ namespace { SignatureResult Result; Result.Code = Status; - if (Status == ERROR_SUCCESS) { + if (Status == ERROR_SUCCESS) + { Result.Status = Signature::Valid; - } else if (Status == static_cast(TRUST_E_NOSIGNATURE) || - Status == static_cast(TRUST_E_SUBJECT_FORM_UNKNOWN) || - Status == static_cast(TRUST_E_PROVIDER_UNKNOWN)) { + } + else if (Status == static_cast(TRUST_E_NOSIGNATURE) || + Status == static_cast(TRUST_E_SUBJECT_FORM_UNKNOWN) || + Status == static_cast(TRUST_E_PROVIDER_UNKNOWN)) + { Result.Status = Signature::NotSigned; - } else { + } + else + { Result.Status = Signature::NotTrusted; } return Result; } - std::string UntrustedMessage(LONG t_Status) { + std::string UntrustedMessage(LONG t_Status) + { char Hex[16] = {}; std::snprintf(Hex, sizeof(Hex), "%08lX", static_cast(t_Status)); return std::string("The shared library signature is not trusted (0x") + Hex + ")"; @@ -218,10 +268,13 @@ namespace { #include -namespace { - std::string LinuxErrorMessage() { +namespace +{ + std::string LinuxErrorMessage() + { const char* Error = dlerror(); - if (Error == nullptr || Error[0] == '\0') { + if (Error == nullptr || Error[0] == '\0') + { return "dlopen failed"; } return Error; @@ -234,31 +287,41 @@ namespace { #endif -namespace { - void WriteError(std::string* t_Error, std::string t_Message) { - if (t_Error != nullptr) { +namespace +{ + void WriteError(std::string* t_Error, std::string t_Message) + { + if (t_Error != nullptr) + { *t_Error = std::move(t_Message); } } - void ClearError(std::string* t_Error) { - if (t_Error != nullptr) { + void ClearError(std::string* t_Error) + { + if (t_Error != nullptr) + { t_Error->clear(); } } } // namespace -namespace Fling { - DynamicLibrary::~DynamicLibrary() { +namespace Fling +{ + DynamicLibrary::~DynamicLibrary() + { Close(); } - DynamicLibrary::DynamicLibrary(DynamicLibrary&& t_Other) noexcept : m_Handle(t_Other.m_Handle) { + DynamicLibrary::DynamicLibrary(DynamicLibrary&& t_Other) noexcept : m_Handle(t_Other.m_Handle) + { t_Other.m_Handle = nullptr; } - DynamicLibrary& DynamicLibrary::operator=(DynamicLibrary&& t_Other) noexcept { - if (this != &t_Other) { + DynamicLibrary& DynamicLibrary::operator=(DynamicLibrary&& t_Other) noexcept + { + if (this != &t_Other) + { Close(); m_Handle = t_Other.m_Handle; t_Other.m_Handle = nullptr; @@ -266,55 +329,70 @@ namespace Fling { return *this; } - DynamicLibrary DynamicLibrary::Open(std::string_view t_Name, std::string* t_Error) { - if (t_Name.empty()) { + DynamicLibrary DynamicLibrary::Open(std::string_view t_Name, std::string* t_Error) + { + if (t_Name.empty()) + { WriteError(t_Error, "The shared library name is empty"); return {}; } #if FLING_WINDOWS const std::wstring WideName = Utf8ToWide(t_Name); - if (WideName.empty()) { + if (WideName.empty()) + { WriteError(t_Error, "The shared library name is not valid UTF-8"); return {}; } std::wstring Path; - if (IsExplicitPath(WideName)) { + if (IsExplicitPath(WideName)) + { Path = FullPath(WideName); - if (Path.empty()) { + if (Path.empty()) + { WriteError(t_Error, WindowsErrorMessage(GetLastError())); return {}; } const Candidate Lookup = ProbeCandidate(Path); - if (Lookup == Candidate::Failed) { + if (Lookup == Candidate::Failed) + { WriteError(t_Error, WindowsErrorMessage(GetLastError())); return {}; } - if (Lookup != Candidate::Found) { + if (Lookup != Candidate::Found) + { WriteError(t_Error, "The shared library was not found"); return {}; } - } else { + } + else + { const std::wstring SystemDir = SystemDirectory(); const std::wstring SystemCandidate = SystemDir.empty() ? std::wstring{} : JoinPath(SystemDir, WideName); const Candidate SystemLookup = SystemCandidate.empty() ? Candidate::Missing : ProbeCandidate(SystemCandidate); - if (SystemLookup == Candidate::Failed) { + if (SystemLookup == Candidate::Failed) + { WriteError(t_Error, WindowsErrorMessage(GetLastError())); return {}; } - if (SystemLookup == Candidate::Found) { + if (SystemLookup == Candidate::Found) + { Path = SystemCandidate; - } else { + } + else + { const std::wstring ExeDir = ExecutableDirectory(); const std::wstring ExeCandidate = ExeDir.empty() ? std::wstring{} : JoinPath(ExeDir, WideName); const Candidate ExeLookup = ExeCandidate.empty() ? Candidate::Missing : ProbeCandidate(ExeCandidate); - if (ExeLookup == Candidate::Failed) { + if (ExeLookup == Candidate::Failed) + { WriteError(t_Error, WindowsErrorMessage(GetLastError())); return {}; } - if (ExeLookup != Candidate::Found) { + if (ExeLookup != Candidate::Found) + { WriteError(t_Error, "The shared library was not found"); return {}; } @@ -323,11 +401,13 @@ namespace Fling { } const SignatureResult Checked = VerifyAuthenticode(Path); - if (Checked.Status == Signature::NotSigned) { + if (Checked.Status == Signature::NotSigned) + { WriteError(t_Error, "The shared library is not signed"); return {}; } - if (Checked.Status != Signature::Valid) { + if (Checked.Status != Signature::Valid) + { WriteError(t_Error, UntrustedMessage(Checked.Code)); return {}; } @@ -335,7 +415,8 @@ namespace Fling { // LoadLibraryExW does not open a dialog when the DLL is missing. HMODULE Handle = LoadLibraryExW(Path.c_str(), nullptr, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32); - if (Handle == nullptr) { + if (Handle == nullptr) + { WriteError(t_Error, WindowsErrorMessage(GetLastError())); return {}; } @@ -349,7 +430,8 @@ namespace Fling { const std::string Name(t_Name); dlerror(); void* Handle = dlopen(Name.c_str(), RTLD_NOW | RTLD_LOCAL); - if (Handle == nullptr) { + if (Handle == nullptr) + { WriteError(t_Error, LinuxErrorMessage()); return {}; } @@ -361,26 +443,33 @@ namespace Fling { #endif } - DynamicLibrary DynamicLibrary::OpenFirst(std::span t_Names, std::string* t_Error) { - for (const std::string_view Name : t_Names) { + DynamicLibrary DynamicLibrary::OpenFirst(std::span t_Names, std::string* t_Error) + { + for (const std::string_view Name : t_Names) + { DynamicLibrary Library = Open(Name, t_Error); - if (Library) { + if (Library) + { return Library; } } - if (t_Error != nullptr && t_Error->empty()) { + if (t_Error != nullptr && t_Error->empty()) + { *t_Error = "No shared library name was given"; } return {}; } - DynamicLibrary::operator bool() const { + DynamicLibrary::operator bool() const + { return m_Handle != nullptr; } - void* DynamicLibrary::Symbol(std::string_view t_Name) const { - if (m_Handle == nullptr || t_Name.empty()) { + void* DynamicLibrary::Symbol(std::string_view t_Name) const + { + if (m_Handle == nullptr || t_Name.empty()) + { return nullptr; } @@ -397,8 +486,10 @@ namespace Fling { #endif } - void DynamicLibrary::Close() { - if (m_Handle == nullptr) { + void DynamicLibrary::Close() + { + if (m_Handle == nullptr) + { return; } diff --git a/FlingEngine/Graphics/src/DesktopWindow.cpp b/FlingEngine/Graphics/src/DesktopWindow.cpp index 487d6348..1e01a944 100644 --- a/FlingEngine/Graphics/src/DesktopWindow.cpp +++ b/FlingEngine/Graphics/src/DesktopWindow.cpp @@ -7,171 +7,175 @@ namespace Fling { - static bool bGlfwInitalized = false; - - FlingWindow* FlingWindow::Create(const WindowProps& t_Props) - { - return new DesktopWindow(t_Props); - } - - DesktopWindow::DesktopWindow(const WindowProps& t_Props) - { - // Initialize GLFW if we have to - if (!bGlfwInitalized) - { - // Give GLFW the loader that Fling opened. Call this before glfwInit. - glfwInitVulkanLoader(vkGetInstanceProcAddr); - glfwInit(); - glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); - bGlfwInitalized = true; - } - - m_Window = glfwCreateWindow(t_Props.m_Width, t_Props.m_Height, t_Props.m_Title.c_str(), nullptr, nullptr); - - glfwSetFramebufferSizeCallback(m_Window, &DesktopWindow::FrameBufferResizeCallback); - } - - void DesktopWindow::FrameBufferResizeCallback(GLFWwindow* t_Window, int t_Width, int t_Height) - { - VulkanApp::Get().OnWindowResized(t_Width, t_Height); - } - - void DesktopWindow::CreateSurface(void* t_GraphicsInstance, void* t_SurfData) - { - if (glfwCreateWindowSurface((VkInstance)t_GraphicsInstance, m_Window, nullptr, (VkSurfaceKHR*)t_SurfData) != VK_SUCCESS) - { - F_LOG_FATAL("Failed to created the window surface!"); - } - } - - void DesktopWindow::WaitForNewWindowSize() - { - // If the window is minimized then wait for it to come to the foreground before displaying it again - // #TODO: Handle minimizing windows asynchronously or in a way that the renderer doesn't block - int width = 0, height = 0; - while (width == 0 || height == 0) - { - glfwGetFramebufferSize(m_Window, &width, &height); - glfwWaitEvents(); - } - } - - DesktopWindow::~DesktopWindow() - { - glfwDestroyWindow(m_Window); - glfwTerminate(); - } - - void DesktopWindow::Update() - { - glfwPollEvents(); - } - - int DesktopWindow::ShouldClose() - { - return glfwWindowShouldClose(m_Window); - } - - bool DesktopWindow::IsMinimized() const - { - return false; - } - - uint32 DesktopWindow::GetWidth() const - { - int width, height; - glfwGetFramebufferSize(m_Window, &width, &height); - - return width; - } - - uint32 DesktopWindow::GetHeight() const - { - int width, height; - glfwGetFramebufferSize(m_Window, &width, &height); - - return height; - } - - float DesktopWindow::GetAspectRatio() const - { - int width, height; - glfwGetFramebufferSize(m_Window, &width, &height); - - return static_cast(width) / static_cast(height); - } - - void DesktopWindow::SetMouseVisible(bool t_IsVisible) - { - m_IsMouseVisible = t_IsVisible; - if (t_IsVisible) - { - glfwSetInputMode(m_Window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); - } - else - { - glfwSetInputMode(m_Window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); - } - } - - bool DesktopWindow::GetMouseVisible() - { - return m_IsMouseVisible; - } - - void DesktopWindow::SetWindowIcon(Guid t_ID) - { - // Load an image - std::shared_ptr Icon = Texture::Create(t_ID); - assert(Icon); - - // Set the Pixel data for this image - GLFWimage GLFW_Image; - GLFW_Image.height = Icon->GetHeight(); - GLFW_Image.width = Icon->GetWidth(); - GLFW_Image.pixels = Icon->GetPixelData(); - - // Set it via GLFW - glfwSetWindowIcon(m_Window, 1, &GLFW_Image); - } - - void DesktopWindow::SetWindowMode(WindowMode t_WindowMode) - { - //early return if nothing would change + static bool bGlfwInitalized = false; + + FlingWindow* FlingWindow::Create(const WindowProps& t_Props) + { + return new DesktopWindow(t_Props); + } + + DesktopWindow::DesktopWindow(const WindowProps& t_Props) + { + // Initialize GLFW if we have to + if (!bGlfwInitalized) + { + // Give GLFW the loader that Fling opened. Call this before glfwInit. + glfwInitVulkanLoader(vkGetInstanceProcAddr); + glfwInit(); + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + bGlfwInitalized = true; + } + + m_Window = glfwCreateWindow(t_Props.m_Width, t_Props.m_Height, t_Props.m_Title.c_str(), nullptr, nullptr); + + glfwSetFramebufferSizeCallback(m_Window, &DesktopWindow::FrameBufferResizeCallback); + } + + void DesktopWindow::FrameBufferResizeCallback(GLFWwindow* t_Window, int t_Width, int t_Height) + { + VulkanApp::Get().OnWindowResized(t_Width, t_Height); + } + + void DesktopWindow::CreateSurface(void* t_GraphicsInstance, void* t_SurfData) + { + if (glfwCreateWindowSurface((VkInstance)t_GraphicsInstance, m_Window, nullptr, (VkSurfaceKHR*)t_SurfData) != + VK_SUCCESS) + { + F_LOG_FATAL("Failed to created the window surface!"); + } + } + + void DesktopWindow::WaitForNewWindowSize() + { + // If the window is minimized then wait for it to come to the foreground before displaying it again + // #TODO: Handle minimizing windows asynchronously or in a way that the renderer doesn't block + int width = 0, height = 0; + while (width == 0 || height == 0) + { + glfwGetFramebufferSize(m_Window, &width, &height); + glfwWaitEvents(); + } + } + + DesktopWindow::~DesktopWindow() + { + glfwDestroyWindow(m_Window); + glfwTerminate(); + } + + void DesktopWindow::Update() + { + glfwPollEvents(); + } + + int DesktopWindow::ShouldClose() + { + return glfwWindowShouldClose(m_Window); + } + + bool DesktopWindow::IsMinimized() const + { + return false; + } + + uint32 DesktopWindow::GetWidth() const + { + int width, height; + glfwGetFramebufferSize(m_Window, &width, &height); + + return width; + } + + uint32 DesktopWindow::GetHeight() const + { + int width, height; + glfwGetFramebufferSize(m_Window, &width, &height); + + return height; + } + + float DesktopWindow::GetAspectRatio() const + { + int width, height; + glfwGetFramebufferSize(m_Window, &width, &height); + + return static_cast(width) / static_cast(height); + } + + void DesktopWindow::SetMouseVisible(bool t_IsVisible) + { + m_IsMouseVisible = t_IsVisible; + if (t_IsVisible) + { + glfwSetInputMode(m_Window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + } + else + { + glfwSetInputMode(m_Window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); + } + } + + bool DesktopWindow::GetMouseVisible() + { + return m_IsMouseVisible; + } + + void DesktopWindow::SetWindowIcon(Guid t_ID) + { + // Load an image + std::shared_ptr Icon = Texture::Create(t_ID); + assert(Icon); + + // Set the Pixel data for this image + GLFWimage GLFW_Image; + GLFW_Image.height = Icon->GetHeight(); + GLFW_Image.width = Icon->GetWidth(); + GLFW_Image.pixels = Icon->GetPixelData(); + + // Set it via GLFW + glfwSetWindowIcon(m_Window, 1, &GLFW_Image); + } + + void DesktopWindow::SetWindowMode(WindowMode t_WindowMode) + { + // early return if nothing would change if (t_WindowMode == m_WindowMode) { return; } - const int windowedWidth = FlingConfig::GetInt("Windowed", "WindowWidth") ? FlingConfig::GetInt("Windowed", "WindowWidth") : FLING_DEFAULT_WINDOW_WIDTH; - const int windowedHeight = FlingConfig::GetInt("Windowed", "WindowHeight") ? FlingConfig::GetInt("Windowed", "WindowHeight") : FLING_DEFAULT_WINDOW_HEIGHT; + const int windowedWidth = FlingConfig::GetInt("Windowed", "WindowWidth") + ? FlingConfig::GetInt("Windowed", "WindowWidth") + : FLING_DEFAULT_WINDOW_WIDTH; + const int windowedHeight = FlingConfig::GetInt("Windowed", "WindowHeight") + ? FlingConfig::GetInt("Windowed", "WindowHeight") + : FLING_DEFAULT_WINDOW_HEIGHT; - const GLFWvidmode* mode = glfwGetVideoMode(glfwGetPrimaryMonitor()); + const GLFWvidmode* mode = glfwGetVideoMode(glfwGetPrimaryMonitor()); - switch (t_WindowMode) - { - case WindowMode::Fullscreen: - glfwSetWindowAttrib(m_Window, GLFW_DECORATED, GLFW_TRUE); - glfwSetWindowMonitor(m_Window, glfwGetPrimaryMonitor(), 0, 0, mode->width, mode->height, 0); - break; - case WindowMode::Windowed: - glfwSetWindowAttrib(m_Window, GLFW_DECORATED, GLFW_TRUE); - glfwSetWindowMonitor(m_Window, nullptr, 100, 100, windowedWidth, windowedHeight, 0); + switch (t_WindowMode) + { + case WindowMode::Fullscreen: + glfwSetWindowAttrib(m_Window, GLFW_DECORATED, GLFW_TRUE); + glfwSetWindowMonitor(m_Window, glfwGetPrimaryMonitor(), 0, 0, mode->width, mode->height, 0); + break; + case WindowMode::Windowed: + glfwSetWindowAttrib(m_Window, GLFW_DECORATED, GLFW_TRUE); + glfwSetWindowMonitor(m_Window, nullptr, 100, 100, windowedWidth, windowedHeight, 0); break; case WindowMode::BorderlessWindowed: glfwSetWindowAttrib(m_Window, GLFW_DECORATED, GLFW_FALSE); glfwSetWindowMonitor(m_Window, nullptr, 100, 100, windowedWidth, windowedHeight, 0); - break; - default: - break; - } - m_WindowMode = t_WindowMode; - - } + break; + default: + break; + } + m_WindowMode = t_WindowMode; + } - WindowMode DesktopWindow::GetWindowMode() - { - return m_WindowMode; - } + WindowMode DesktopWindow::GetWindowMode() + { + return m_WindowMode; + } -} // namespace Fling \ No newline at end of file +} // namespace Fling \ No newline at end of file diff --git a/FlingEngine/Graphics/src/DesktopWindow.h b/FlingEngine/Graphics/src/DesktopWindow.h index 9b98980c..ee1a33e6 100644 --- a/FlingEngine/Graphics/src/DesktopWindow.h +++ b/FlingEngine/Graphics/src/DesktopWindow.h @@ -6,64 +6,63 @@ #include "FlingVulkan.h" #ifndef GLFW_INCLUDE_VULKAN -# define GLFW_INCLUDE_VULKAN +#define GLFW_INCLUDE_VULKAN #endif #include namespace Fling { - /** - * Base class that represents a window to the Fling Engine using GLFW - */ - class DesktopWindow : public FlingWindow - { - public: + /** + * Base class that represents a window to the Fling Engine using GLFW + */ + class DesktopWindow : public FlingWindow + { + public: + DesktopWindow(const WindowProps& t_Props); + virtual ~DesktopWindow(); - DesktopWindow(const WindowProps& t_Props); - virtual ~DesktopWindow(); + virtual void CreateSurface(void* t_GraphicsInstance, void* t_SurfData) override; - virtual void CreateSurface(void* t_GraphicsInstance, void* t_SurfData) override; + /** Tick the window (poll input from GLFW) */ + virtual void Update() override; - /** Tick the window (poll input from GLFW) */ - virtual void Update() override; + /** BLocks until we get a new window size from GLFW */ + virtual void WaitForNewWindowSize() override; - /** BLocks until we get a new window size from GLFW */ - virtual void WaitForNewWindowSize() override; + /** + * Check if this window should close + * @return 0 if the window should not close, non-zero if it should. + */ + virtual int ShouldClose() override; - /** - * Check if this window should close - * @return 0 if the window should not close, non-zero if it should. - */ - virtual int ShouldClose() override; + /** Is this window currently minimized? */ + virtual bool IsMinimized() const override final; - /** Is this window currently minimized? */ - virtual bool IsMinimized() const override final; + virtual uint32 GetWidth() const override; - virtual uint32 GetWidth() const override; + virtual uint32 GetHeight() const override; - virtual uint32 GetHeight() const override; + virtual float GetAspectRatio() const override; - virtual float GetAspectRatio() const override; + virtual void SetMouseVisible(bool t_IsVisible) override; - virtual void SetMouseVisible(bool t_IsVisible) override; + virtual bool GetMouseVisible() override; - virtual bool GetMouseVisible() override; + /** + * Set this window's icon. + * @param t_ID the GUID of the window icon + */ + void SetWindowIcon(Guid t_ID) override; - /** - * Set this window's icon. - * @param t_ID the GUID of the window icon - */ - void SetWindowIcon(Guid t_ID) override; + /** get the current GLFW window */ + GLFWwindow* GetGlfwWindow() const { return m_Window; } - /** get the current GLFW window */ - GLFWwindow* GetGlfwWindow() const { return m_Window; } + virtual void SetWindowMode(WindowMode t_WindowMode) override; + virtual WindowMode GetWindowMode() override; - virtual void SetWindowMode(WindowMode t_WindowMode) override; - virtual WindowMode GetWindowMode() override; - private: + private: + GLFWwindow* m_Window = nullptr; - GLFWwindow* m_Window = nullptr; - - static void FrameBufferResizeCallback(GLFWwindow* t_Window, int t_Width, int t_Height); - }; -} // namespace Fling \ No newline at end of file + static void FrameBufferResizeCallback(GLFWwindow* t_Window, int t_Width, int t_Height); + }; +} // namespace Fling \ No newline at end of file diff --git a/FlingEngine/Graphics/src/FlingVulkan.h b/FlingEngine/Graphics/src/FlingVulkan.h index f4ec2279..562ec40f 100644 --- a/FlingEngine/Graphics/src/FlingVulkan.h +++ b/FlingEngine/Graphics/src/FlingVulkan.h @@ -22,7 +22,8 @@ #endif // NOMINMAX // The names match the C API, so call sites stay vkCreateBuffer(...). -extern "C" { +extern "C" +{ #define FLING_VK_GLOBAL(name) extern PFN_##name name; #define FLING_VK_GLOBAL_OPTIONAL(name) extern PFN_##name name; #define FLING_VK_INSTANCE(name) extern PFN_##name name; @@ -40,7 +41,8 @@ extern "C" { // Some Vulkan constant definitions // Grabbed these from Granite: https://github.com/Themaister/Granite/blob/master/vulkan/shader.cpp -namespace Fling { +namespace Fling +{ static const unsigned VULKAN_NUM_DESCRIPTOR_SETS = 4; static const unsigned VULKAN_NUM_BINDINGS = 16; static const unsigned VULKAN_NUM_ATTACHMENTS = 8; @@ -50,7 +52,8 @@ namespace Fling { static const unsigned VULKAN_MAX_UBO_SIZE = 16 * 1024; static const unsigned VULKAN_NUM_SPEC_CONSTANTS = 8; - namespace VkConfig { + namespace VkConfig + { static const int MAX_FRAMES_IN_FLIGHT = 2; } diff --git a/FlingEngine/Graphics/src/Instance.cpp b/FlingEngine/Graphics/src/Instance.cpp index e5df77d5..410d1bde 100644 --- a/FlingEngine/Graphics/src/Instance.cpp +++ b/FlingEngine/Graphics/src/Instance.cpp @@ -10,7 +10,7 @@ namespace Fling Instance::Instance() { m_EnableValidationLayers = FlingConfig::GetBool("Vulkan", "EnableValidationLayers", false); - F_LOG_TRACE("[Renderer] m_EnableValidationLayers is {}", (m_EnableValidationLayers ? "TRUE" : "FALSE")); + F_LOG_TRACE("[Renderer] m_EnableValidationLayers is {}", (m_EnableValidationLayers ? "TRUE" : "FALSE")); CreateInstance(); @@ -21,186 +21,200 @@ namespace Fling void Instance::CreateInstance() { - if( m_EnableValidationLayers && !CheckValidationLayerSupport() ) - { - F_LOG_WARN( "Validation layers are requested, but not available!" ); - } - - // Basic app data that we can modify - VkApplicationInfo appInfo = {}; - // Most structs in Vulkan require you to specify the sType and - // the pNext values - appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; - appInfo.pApplicationName = "Engine Bay 0.1"; - appInfo.applicationVersion = VK_MAKE_VERSION(Version::EngineVersion.Major, Version::EngineVersion.Minor, Version::EngineVersion.Patch); - appInfo.pEngineName = "Fling Engine"; - appInfo.engineVersion = VK_MAKE_VERSION(Version::EngineVersion.Major, Version::EngineVersion.Minor, Version::EngineVersion.Patch); - appInfo.apiVersion = VK_API_VERSION_1_0; - - // Instance creation info, similar to how DX11 worked - VkInstanceCreateInfo createInfo = {}; - createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; - createInfo.pApplicationInfo = &appInfo; - - // Because we are using GLFW as an extension interface for window creation - // Vulkan needs to know how many extensions are available - std::vector extensions = GetRequiredExtensions(); - createInfo.enabledExtensionCount = static_cast( extensions.size() ); - createInfo.ppEnabledExtensionNames = extensions.data(); - - // Determine global validation layer count - if( m_EnableValidationLayers ) - { - createInfo.enabledLayerCount = static_cast( m_ValidationLayers.size() ); - createInfo.ppEnabledLayerNames = m_ValidationLayers.data(); - } - else - { - createInfo.enabledLayerCount = 0; - createInfo.pNext = nullptr; - } - - // Create the vulkan instance! Throw exception if it fails - if( vkCreateInstance( &createInfo, nullptr, &m_Instance ) != VK_SUCCESS ) - { - F_LOG_FATAL( "Failed to create the Vulkan instance!" ); - } - - VulkanLoader::LoadInstance(m_Instance); - - // Find out what extensions are available - uint32_t extensionCount = 0; - vkEnumerateInstanceExtensionProperties( nullptr, &extensionCount, nullptr ); - std::vector vk_extensions( extensionCount ); - vkEnumerateInstanceExtensionProperties( nullptr, &extensionCount, vk_extensions.data() ); - - F_LOG_TRACE( "{} Extensions are supported!", extensionCount ); - for( const auto& extension : vk_extensions ) - { - F_LOG_TRACE( "\t{}", extension.extensionName ); - } + if (m_EnableValidationLayers && !CheckValidationLayerSupport()) + { + F_LOG_WARN("Validation layers are requested, but not available!"); + } + + // Basic app data that we can modify + VkApplicationInfo appInfo = {}; + // Most structs in Vulkan require you to specify the sType and + // the pNext values + appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; + appInfo.pApplicationName = "Engine Bay 0.1"; + appInfo.applicationVersion = + VK_MAKE_VERSION(Version::EngineVersion.Major, Version::EngineVersion.Minor, Version::EngineVersion.Patch); + appInfo.pEngineName = "Fling Engine"; + appInfo.engineVersion = + VK_MAKE_VERSION(Version::EngineVersion.Major, Version::EngineVersion.Minor, Version::EngineVersion.Patch); + appInfo.apiVersion = VK_API_VERSION_1_0; + + // Instance creation info, similar to how DX11 worked + VkInstanceCreateInfo createInfo = {}; + createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + createInfo.pApplicationInfo = &appInfo; + + // Because we are using GLFW as an extension interface for window creation + // Vulkan needs to know how many extensions are available + std::vector extensions = GetRequiredExtensions(); + createInfo.enabledExtensionCount = static_cast(extensions.size()); + createInfo.ppEnabledExtensionNames = extensions.data(); + + // Determine global validation layer count + if (m_EnableValidationLayers) + { + createInfo.enabledLayerCount = static_cast(m_ValidationLayers.size()); + createInfo.ppEnabledLayerNames = m_ValidationLayers.data(); + } + else + { + createInfo.enabledLayerCount = 0; + createInfo.pNext = nullptr; + } + + // Create the vulkan instance! Throw exception if it fails + if (vkCreateInstance(&createInfo, nullptr, &m_Instance) != VK_SUCCESS) + { + F_LOG_FATAL("Failed to create the Vulkan instance!"); + } + + VulkanLoader::LoadInstance(m_Instance); + + // Find out what extensions are available + uint32_t extensionCount = 0; + vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr); + std::vector vk_extensions(extensionCount); + vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, vk_extensions.data()); + + F_LOG_TRACE("{} Extensions are supported!", extensionCount); + for (const auto& extension : vk_extensions) + { + F_LOG_TRACE("\t{}", extension.extensionName); + } } bool Instance::CheckValidationLayerSupport() - { - // Get the list of available validation layers - // Validation layers are ways for us to choose what types of debugging - // features we want from the Vulkan SDK - uint32 layerCount = 0; - vkEnumerateInstanceLayerProperties( &layerCount, nullptr ); - - std::vector availableLayers( layerCount ); - vkEnumerateInstanceLayerProperties( &layerCount, availableLayers.data() ); - - for( const char* layerName : m_ValidationLayers ) - { - bool layerFound = false; - for( const VkLayerProperties& layerProperties : availableLayers ) - { - if( strcmp( layerName, layerProperties.layerName ) == 0 ) - { - layerFound = true; - break; - } - } - if( !layerFound ) - { - return false; - } - } - - return true; - } + { + // Get the list of available validation layers + // Validation layers are ways for us to choose what types of debugging + // features we want from the Vulkan SDK + uint32 layerCount = 0; + vkEnumerateInstanceLayerProperties(&layerCount, nullptr); + + std::vector availableLayers(layerCount); + vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data()); + + for (const char* layerName : m_ValidationLayers) + { + bool layerFound = false; + for (const VkLayerProperties& layerProperties : availableLayers) + { + if (strcmp(layerName, layerProperties.layerName) == 0) + { + layerFound = true; + break; + } + } + if (!layerFound) + { + return false; + } + } + + return true; + } std::vector Instance::GetRequiredExtensions() - { - uint32 glfwExtensionCount = 0; - const char** glfwExtensions; - glfwExtensions = glfwGetRequiredInstanceExtensions( &glfwExtensionCount ); + { + uint32 glfwExtensionCount = 0; + const char** glfwExtensions; + glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); - std::vector extensions( glfwExtensions, glfwExtensions + glfwExtensionCount ); + std::vector extensions(glfwExtensions, glfwExtensions + glfwExtensionCount); - if( m_EnableValidationLayers ) - { + if (m_EnableValidationLayers) + { #if FLING_DEBUG - extensions.push_back( VK_EXT_DEBUG_UTILS_EXTENSION_NAME ); + extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); #endif - } - - return extensions; - } + } + return extensions; + } #if FLING_DEBUG void Instance::SetupDebugMessages() { - if( !m_EnableValidationLayers ) { return; } - - F_LOG_TRACE("Setup VK Debug messages!"); - - VkDebugUtilsMessengerCreateInfoEXT createInfo = {}; - createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; - createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; - createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; - createInfo.pfnUserCallback = DebugCallback; - createInfo.pUserData = nullptr; // Optional - - if( CreateDebugUtilsMessengerEXT( m_Instance, &createInfo, nullptr, &m_DebugMessenger ) != VK_SUCCESS ) - { - F_LOG_FATAL( "Failed to set up debug messenger!" ); - } + if (!m_EnableValidationLayers) + { + return; + } + + F_LOG_TRACE("Setup VK Debug messages!"); + + VkDebugUtilsMessengerCreateInfoEXT createInfo = {}; + createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; + createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | + VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | + VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; + createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | + VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | + VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; + createInfo.pfnUserCallback = DebugCallback; + createInfo.pUserData = nullptr; // Optional + + if (CreateDebugUtilsMessengerEXT(m_Instance, &createInfo, nullptr, &m_DebugMessenger) != VK_SUCCESS) + { + F_LOG_FATAL("Failed to set up debug messenger!"); + } + } + + VKAPI_ATTR VkBool32 VKAPI_CALL Instance::DebugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT t_messageSeverity, + VkDebugUtilsMessageTypeFlagsEXT t_messageType, + const VkDebugUtilsMessengerCallbackDataEXT* t_CallbackData, + void* t_UserData) + { + switch (t_messageSeverity) + { + case VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT: + F_LOG_WARN("Validation layer: {}", t_CallbackData->pMessage); + break; + case VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT: + case VK_DEBUG_UTILS_MESSAGE_SEVERITY_FLAG_BITS_MAX_ENUM_EXT: + F_LOG_ERROR("Validation layer error: {}", t_CallbackData->pMessage); + FLING_BREAK(); + break; + case VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT: + case VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT: + default: + F_LOG_TRACE("Validation layer: {}", t_CallbackData->pMessage); + break; + } + + return VK_FALSE; + } + + VkResult Instance::CreateDebugUtilsMessengerEXT(VkInstance instance, + const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo, + const VkAllocationCallbacks* pAllocator, + VkDebugUtilsMessengerEXT* pDebugMessenger) + { + PFN_vkCreateDebugUtilsMessengerEXT func = + (PFN_vkCreateDebugUtilsMessengerEXT)vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT"); + if (func != nullptr) + { + return func(instance, pCreateInfo, pAllocator, pDebugMessenger); + } + else + { + return VK_ERROR_EXTENSION_NOT_PRESENT; + } + } + + void Instance::DestroyDebugUtilsMessengerEXT(VkInstance instance, + VkDebugUtilsMessengerEXT debugMessenger, + const VkAllocationCallbacks* pAllocator) + { + PFN_vkDestroyDebugUtilsMessengerEXT func = + (PFN_vkDestroyDebugUtilsMessengerEXT)vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT"); + if (func != nullptr) + { + func(instance, m_DebugMessenger, pAllocator); + } } - VKAPI_ATTR VkBool32 VKAPI_CALL Instance::DebugCallback( - VkDebugUtilsMessageSeverityFlagBitsEXT t_messageSeverity, - VkDebugUtilsMessageTypeFlagsEXT t_messageType, - const VkDebugUtilsMessengerCallbackDataEXT* t_CallbackData, - void* t_UserData ) - { - switch( t_messageSeverity ) - { - case VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT: - F_LOG_WARN( "Validation layer: {}", t_CallbackData->pMessage ); - break; - case VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT: - case VK_DEBUG_UTILS_MESSAGE_SEVERITY_FLAG_BITS_MAX_ENUM_EXT: - F_LOG_ERROR( "Validation layer error: {}", t_CallbackData->pMessage ); - FLING_BREAK(); - break; - case VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT: - case VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT: - default: - F_LOG_TRACE( "Validation layer: {}", t_CallbackData->pMessage ); - break; - } - - return VK_FALSE; - } - - VkResult Instance::CreateDebugUtilsMessengerEXT( VkInstance instance, const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugUtilsMessengerEXT* pDebugMessenger ) - { - PFN_vkCreateDebugUtilsMessengerEXT func = (PFN_vkCreateDebugUtilsMessengerEXT)vkGetInstanceProcAddr( instance, "vkCreateDebugUtilsMessengerEXT" ); - if( func != nullptr ) - { - return func( instance, pCreateInfo, pAllocator, pDebugMessenger ); - } - else - { - return VK_ERROR_EXTENSION_NOT_PRESENT; - } - } - - void Instance::DestroyDebugUtilsMessengerEXT( VkInstance instance, VkDebugUtilsMessengerEXT debugMessenger, const VkAllocationCallbacks* pAllocator ) - { - PFN_vkDestroyDebugUtilsMessengerEXT func = (PFN_vkDestroyDebugUtilsMessengerEXT)vkGetInstanceProcAddr( instance, "vkDestroyDebugUtilsMessengerEXT" ); - if( func != nullptr ) - { - func(instance, m_DebugMessenger, pAllocator); - } - } - -#endif // FLING_DEBUG +#endif // FLING_DEBUG Instance::~Instance() { @@ -212,6 +226,6 @@ namespace Fling #endif } - vkDestroyInstance( m_Instance, nullptr ); + vkDestroyInstance(m_Instance, nullptr); } -} // namespace Fling \ No newline at end of file +} // namespace Fling \ No newline at end of file diff --git a/FlingEngine/Graphics/src/LogicalDevice.cpp b/FlingEngine/Graphics/src/LogicalDevice.cpp index 92540277..126f0e8a 100644 --- a/FlingEngine/Graphics/src/LogicalDevice.cpp +++ b/FlingEngine/Graphics/src/LogicalDevice.cpp @@ -9,82 +9,81 @@ namespace Fling { LogicalDevice::LogicalDevice(Instance* t_Instance, PhysicalDevice* t_PhysDevice, const VkSurfaceKHR t_Surface) - : m_Instance(t_Instance) - , m_PhysicalDevice(t_PhysDevice) - , m_Surface(t_Surface) + : m_Instance(t_Instance), m_PhysicalDevice(t_PhysDevice), m_Surface(t_Surface) { - CreateQueueIndecies(); + CreateQueueIndecies(); CreateDevice(); } - void LogicalDevice::CreateQueueIndecies() - { - uint32 QueueFamilyCount = 0; - VkPhysicalDevice PhysDevice = m_PhysicalDevice->GetVkPhysicalDevice(); - vkGetPhysicalDeviceQueueFamilyProperties(PhysDevice, &QueueFamilyCount, nullptr); - std::vector QueueFamilies(QueueFamilyCount); - vkGetPhysicalDeviceQueueFamilyProperties(PhysDevice, &QueueFamilyCount, QueueFamilies.data()); - - std::optional graphicsFamily; - std::optional presentFamily; - std::optional computeFamily; - std::optional transferFamily; - - for (uint32_t i = 0; i < QueueFamilyCount; ++i) - { - // Check for graphics support. - if (QueueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) - { - graphicsFamily = i; - m_GraphicsFamily = i; - m_SupportedQueues |= VK_QUEUE_GRAPHICS_BIT; - } - - // Check for presentation support. - VkBool32 presentSupport; - vkGetPhysicalDeviceSurfaceSupportKHR(m_PhysicalDevice->GetVkPhysicalDevice(), i, m_Surface, &presentSupport); - - if (QueueFamilies[i].queueCount > 0 && presentSupport) - { - presentFamily = i; - m_PresentFamily = i; - } - - // Check for compute support. - if (QueueFamilies[i].queueFlags & VK_QUEUE_COMPUTE_BIT) - { - computeFamily = i; - m_ComputeFamily = i; - m_SupportedQueues |= VK_QUEUE_COMPUTE_BIT; - } - - // Check for transfer support. - if (QueueFamilies[i].queueFlags & VK_QUEUE_TRANSFER_BIT) - { - transferFamily = i; - m_TransferFamily = i; - m_SupportedQueues |= VK_QUEUE_TRANSFER_BIT; - } - - if (graphicsFamily && presentFamily && computeFamily && transferFamily) - { - break; - } - } - - if (!graphicsFamily) - { - F_LOG_FATAL("Failed to find queue family supporting VK_QUEUE_GRAPHICS_BIT"); - } - } - - void LogicalDevice::CreateDevice() + void LogicalDevice::CreateQueueIndecies() { - std::set UniqueQueueFamilies = { m_GraphicsFamily, m_PresentFamily }; + uint32 QueueFamilyCount = 0; + VkPhysicalDevice PhysDevice = m_PhysicalDevice->GetVkPhysicalDevice(); + vkGetPhysicalDeviceQueueFamilyProperties(PhysDevice, &QueueFamilyCount, nullptr); + std::vector QueueFamilies(QueueFamilyCount); + vkGetPhysicalDeviceQueueFamilyProperties(PhysDevice, &QueueFamilyCount, QueueFamilies.data()); + + std::optional graphicsFamily; + std::optional presentFamily; + std::optional computeFamily; + std::optional transferFamily; + + for (uint32_t i = 0; i < QueueFamilyCount; ++i) + { + // Check for graphics support. + if (QueueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) + { + graphicsFamily = i; + m_GraphicsFamily = i; + m_SupportedQueues |= VK_QUEUE_GRAPHICS_BIT; + } + + // Check for presentation support. + VkBool32 presentSupport; + vkGetPhysicalDeviceSurfaceSupportKHR( + m_PhysicalDevice->GetVkPhysicalDevice(), i, m_Surface, &presentSupport); + + if (QueueFamilies[i].queueCount > 0 && presentSupport) + { + presentFamily = i; + m_PresentFamily = i; + } + + // Check for compute support. + if (QueueFamilies[i].queueFlags & VK_QUEUE_COMPUTE_BIT) + { + computeFamily = i; + m_ComputeFamily = i; + m_SupportedQueues |= VK_QUEUE_COMPUTE_BIT; + } + + // Check for transfer support. + if (QueueFamilies[i].queueFlags & VK_QUEUE_TRANSFER_BIT) + { + transferFamily = i; + m_TransferFamily = i; + m_SupportedQueues |= VK_QUEUE_TRANSFER_BIT; + } + + if (graphicsFamily && presentFamily && computeFamily && transferFamily) + { + break; + } + } - // Generate the CreatinInfo for each queue family - std::vector QueueCreateInfos; + if (!graphicsFamily) + { + F_LOG_FATAL("Failed to find queue family supporting VK_QUEUE_GRAPHICS_BIT"); + } + } + + void LogicalDevice::CreateDevice() + { + std::set UniqueQueueFamilies = {m_GraphicsFamily, m_PresentFamily}; + + // Generate the CreatinInfo for each queue family + std::vector QueueCreateInfos; float Priority = 1.0f; for (const uint32 fam : UniqueQueueFamilies) @@ -98,11 +97,10 @@ namespace Fling } VkPhysicalDeviceFeatures DevicesFeatures = {}; - DevicesFeatures.samplerAnisotropy = VK_TRUE; - DevicesFeatures.sampleRateShading = VK_TRUE; - + DevicesFeatures.samplerAnisotropy = VK_TRUE; + DevicesFeatures.sampleRateShading = VK_TRUE; - // Device creation + // Device creation VkDeviceCreateInfo CreateInfo = {}; CreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; CreateInfo.queueCreateInfoCount = static_cast(QueueCreateInfos.size()); @@ -113,19 +111,19 @@ namespace Fling CreateInfo.enabledExtensionCount = static_cast(m_Instance->GetEnabledExtensions().size()); CreateInfo.ppEnabledExtensionNames = m_Instance->GetEnabledExtensions().data(); - if( m_Instance->IsValidationEnabled() ) + if (m_Instance->IsValidationEnabled()) { CreateInfo.enabledLayerCount = m_Instance->EnabledValidationLayerCount(); CreateInfo.ppEnabledLayerNames = m_Instance->GetEnabledValidationLayers().data(); } - else + else { CreateInfo.enabledLayerCount = 0; } - if(vkCreateDevice(m_PhysicalDevice->GetVkPhysicalDevice(), &CreateInfo, nullptr, &m_Device) != VK_SUCCESS) + if (vkCreateDevice(m_PhysicalDevice->GetVkPhysicalDevice(), &CreateInfo, nullptr, &m_Device) != VK_SUCCESS) { - F_LOG_FATAL( "Failed to create logical Device!" ); + F_LOG_FATAL("Failed to create logical Device!"); } VulkanLoader::LoadDevice(m_Device); @@ -134,15 +132,15 @@ namespace Fling vkGetDeviceQueue(m_Device, m_PresentFamily, 0, &m_PresentQueue); } - void LogicalDevice::WaitForIdle() - { - vkDeviceWaitIdle(m_Device); - } + void LogicalDevice::WaitForIdle() + { + vkDeviceWaitIdle(m_Device); + } LogicalDevice::~LogicalDevice() { - WaitForIdle(); + WaitForIdle(); vkDestroyDevice(m_Device, nullptr); } -} // namespace Fling \ No newline at end of file +} // namespace Fling \ No newline at end of file diff --git a/FlingEngine/Graphics/src/VulkanLoader.cpp b/FlingEngine/Graphics/src/VulkanLoader.cpp index b083dabe..0fe34b03 100644 --- a/FlingEngine/Graphics/src/VulkanLoader.cpp +++ b/FlingEngine/Graphics/src/VulkanLoader.cpp @@ -8,7 +8,8 @@ #include #include -extern "C" { +extern "C" +{ #define FLING_VK_GLOBAL(name) PFN_##name name = nullptr; #define FLING_VK_GLOBAL_OPTIONAL(name) PFN_##name name = nullptr; #define FLING_VK_INSTANCE(name) PFN_##name name = nullptr; @@ -24,55 +25,68 @@ extern "C" { #undef FLING_VK_DEVICE_OPTIONAL } -namespace { +namespace +{ Fling::DynamicLibrary g_Loader; - [[noreturn]] void FatalVulkan(const std::string& t_Message) { - if (const auto Console = Fling::Logger::GetCurrentConsole()) { + [[noreturn]] void FatalVulkan(const std::string& t_Message) + { + if (const auto Console = Fling::Logger::GetCurrentConsole()) + { Console->error(t_Message); Console->flush(); } - if (const auto File = Fling::Logger::GetCurrentLogFile()) { + if (const auto File = Fling::Logger::GetCurrentLogFile()) + { File->error(t_Message); } throw std::runtime_error(t_Message); } template - void LoadGlobal(const char* t_Name, T_Fn& t_Out) { + void LoadGlobal(const char* t_Name, T_Fn& t_Out) + { // Vulkan 1.0 and 1.1 do not return this pointer from itself. // DynamicLibrary::Symbol already stored the export. - if (std::string_view(t_Name) == "vkGetInstanceProcAddr") { + if (std::string_view(t_Name) == "vkGetInstanceProcAddr") + { return; } t_Out = reinterpret_cast(vkGetInstanceProcAddr(nullptr, t_Name)); } template - void LoadInstanceProc(VkInstance t_Instance, const char* t_Name, T_Fn& t_Out) { + void LoadInstanceProc(VkInstance t_Instance, const char* t_Name, T_Fn& t_Out) + { t_Out = reinterpret_cast(vkGetInstanceProcAddr(t_Instance, t_Name)); } template - void LoadDeviceProc(VkDevice t_Device, const char* t_Name, T_Fn& t_Out) { + void LoadDeviceProc(VkDevice t_Device, const char* t_Name, T_Fn& t_Out) + { t_Out = reinterpret_cast(vkGetDeviceProcAddr(t_Device, t_Name)); } template - void RequireProc(const char* t_Name, const T_Fn& t_Fn) { - if (t_Fn == nullptr) { + void RequireProc(const char* t_Name, const T_Fn& t_Fn) + { + if (t_Fn == nullptr) + { FatalVulkan(std::string("Required Vulkan function is null: ") + t_Name); } } template - void ClearProc(T_Fn& t_Fn) { + void ClearProc(T_Fn& t_Fn) + { t_Fn = nullptr; } } // namespace -namespace Fling { - bool VulkanLoader::LoadLoader(std::string* t_Error) { +namespace Fling +{ + bool VulkanLoader::LoadLoader(std::string* t_Error) + { #if FLING_WINDOWS DynamicLibrary Library = DynamicLibrary::Open("vulkan-1.dll", t_Error); #elif FLING_LINUX @@ -81,14 +95,17 @@ namespace Fling { #else #error VulkanLoader supports Windows and Linux only #endif - if (!Library) { + if (!Library) + { return false; } const PFN_vkGetInstanceProcAddr GetInstanceProcAddr = Library.SymbolAs("vkGetInstanceProcAddr"); - if (GetInstanceProcAddr == nullptr) { - if (t_Error != nullptr) { + if (GetInstanceProcAddr == nullptr) + { + if (t_Error != nullptr) + { *t_Error = "vkGetInstanceProcAddr is missing from the Vulkan loader"; } Library.Close(); @@ -111,7 +128,8 @@ namespace Fling { return true; } - void VulkanLoader::LoadInstance(VkInstance t_Instance) { + void VulkanLoader::LoadInstance(VkInstance t_Instance) + { #define FLING_VK_INSTANCE(name) LoadInstanceProc(t_Instance, #name, name); #define FLING_VK_INSTANCE_OPTIONAL(name) LoadInstanceProc(t_Instance, #name, name); #include "VulkanFunctions.inl" @@ -123,8 +141,10 @@ namespace Fling { #undef FLING_VK_INSTANCE } - void VulkanLoader::LoadDevice(VkDevice t_Device) { - if (vkGetDeviceProcAddr == nullptr) { + void VulkanLoader::LoadDevice(VkDevice t_Device) + { + if (vkGetDeviceProcAddr == nullptr) + { FatalVulkan("Required Vulkan function is null: vkGetDeviceProcAddr"); } @@ -139,7 +159,8 @@ namespace Fling { #undef FLING_VK_DEVICE } - void VulkanLoader::UnloadLoader() { + void VulkanLoader::UnloadLoader() + { #define FLING_VK_GLOBAL(name) ClearProc(name); #define FLING_VK_GLOBAL_OPTIONAL(name) ClearProc(name); #define FLING_VK_INSTANCE(name) ClearProc(name); diff --git a/FlingEngine/Graphics/src/VulkanLoader.h b/FlingEngine/Graphics/src/VulkanLoader.h index 29e9cd54..822e0f58 100644 --- a/FlingEngine/Graphics/src/VulkanLoader.h +++ b/FlingEngine/Graphics/src/VulkanLoader.h @@ -4,14 +4,16 @@ #include -namespace Fling { +namespace Fling +{ /** * Load the Vulkan loader library and the Vulkan function pointers. * * Graphics calls these methods. It does not call LoadLibrary, dlopen, * GetProcAddress, or dlsym. */ - class VulkanLoader { + class VulkanLoader + { public: /** * Open the Vulkan loader and load the global commands. diff --git a/FlingTests/Core/DynamicLibraryTests.cpp b/FlingTests/Core/DynamicLibraryTests.cpp index 4e1e9699..184fe81d 100644 --- a/FlingTests/Core/DynamicLibraryTests.cpp +++ b/FlingTests/Core/DynamicLibraryTests.cpp @@ -13,7 +13,8 @@ #include #endif -namespace { +namespace +{ #if FLING_LINUX constexpr const char* LibraryName = "libc.so.6"; constexpr const char* SymbolName = "strlen"; @@ -24,7 +25,8 @@ namespace { #error DynamicLibrary tests support Windows and Linux only #endif - void CallLoadedSymbol(const Fling::DynamicLibrary& t_Library) { + void CallLoadedSymbol(const Fling::DynamicLibrary& t_Library) + { REQUIRE(static_cast(t_Library)); REQUIRE(t_Library.Symbol(SymbolName) != nullptr); @@ -40,8 +42,10 @@ namespace { } #if FLING_WINDOWS - std::string WideToUtf8(std::wstring_view t_Text) { - if (t_Text.empty()) { + std::string WideToUtf8(std::wstring_view t_Text) + { + if (t_Text.empty()) + { return {}; } const int Size = static_cast(t_Text.size()); @@ -52,7 +56,8 @@ namespace { return Text; } - std::wstring TempDirectory() { + std::wstring TempDirectory() + { wchar_t Buffer[MAX_PATH] = {}; const DWORD Length = GetTempPathW(MAX_PATH, Buffer); REQUIRE(Length > 0); @@ -60,7 +65,8 @@ namespace { return std::wstring(Buffer, Length); } - void WriteTempBytes(const std::wstring& t_Path) { + void WriteTempBytes(const std::wstring& t_Path) + { const HANDLE File = CreateFileW(t_Path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); REQUIRE(File != INVALID_HANDLE_VALUE); @@ -73,7 +79,8 @@ namespace { } /** Delete one file when this guard leaves scope. */ - class DeleteFileGuard { + class DeleteFileGuard + { public: explicit DeleteFileGuard(std::wstring t_Path) : m_Path(std::move(t_Path)) {} @@ -87,7 +94,8 @@ namespace { }; /** Delete one empty directory when this guard leaves scope. */ - class DeleteDirectoryGuard { + class DeleteDirectoryGuard + { public: explicit DeleteDirectoryGuard(std::wstring t_Path) : m_Path(std::move(t_Path)) {} @@ -104,24 +112,30 @@ namespace { * Restore the process current directory when this guard leaves scope. * A failed REQUIRE still runs the destructor. */ - class CurrentDirectoryGuard { + class CurrentDirectoryGuard + { public: - CurrentDirectoryGuard() { + CurrentDirectoryGuard() + { const DWORD Needed = GetCurrentDirectoryW(0, nullptr); - if (Needed == 0) { + if (Needed == 0) + { return; } m_Previous.resize(Needed); const DWORD Written = GetCurrentDirectoryW(Needed, m_Previous.data()); - if (Written == 0 || Written >= Needed) { + if (Written == 0 || Written >= Needed) + { m_Previous.clear(); return; } m_Previous.resize(Written); } - ~CurrentDirectoryGuard() { - if (!m_Previous.empty()) { + ~CurrentDirectoryGuard() + { + if (!m_Previous.empty()) + { SetCurrentDirectoryW(m_Previous.c_str()); } } @@ -138,8 +152,10 @@ namespace { #endif } // namespace -TEST_CASE("DynamicLibrary", "[core][DynamicLibrary]") { - SECTION("Open and call a symbol") { +TEST_CASE("DynamicLibrary", "[core][DynamicLibrary]") +{ + SECTION("Open and call a symbol") + { std::string Error = "stale"; Fling::DynamicLibrary Library = Fling::DynamicLibrary::Open(LibraryName, &Error); REQUIRE(Error.empty()); @@ -148,7 +164,8 @@ TEST_CASE("DynamicLibrary", "[core][DynamicLibrary]") { REQUIRE(static_cast(Library)); } - SECTION("A missing library returns an empty object and an error") { + SECTION("A missing library returns an empty object and an error") + { std::string Error; Fling::DynamicLibrary Library = Fling::DynamicLibrary::Open("fling_no_such_library_42", &Error); REQUIRE_FALSE(static_cast(Library)); @@ -159,7 +176,8 @@ TEST_CASE("DynamicLibrary", "[core][DynamicLibrary]") { REQUIRE_FALSE(static_cast(NoError)); } - SECTION("OpenFirst skips a bad name") { + SECTION("OpenFirst skips a bad name") + { const std::string_view Names[] = {"fling_no_such_library_42", LibraryName}; std::string Error = "stale"; Fling::DynamicLibrary Library = Fling::DynamicLibrary::OpenFirst(Names, &Error); @@ -167,7 +185,8 @@ TEST_CASE("DynamicLibrary", "[core][DynamicLibrary]") { CallLoadedSymbol(Library); } - SECTION("Move leaves the source empty") { + SECTION("Move leaves the source empty") + { Fling::DynamicLibrary Source = Fling::DynamicLibrary::Open(LibraryName); REQUIRE(static_cast(Source)); @@ -184,7 +203,8 @@ TEST_CASE("DynamicLibrary", "[core][DynamicLibrary]") { REQUIRE(Dest.Symbol(SymbolName) == nullptr); } - SECTION("Close then Symbol returns null") { + SECTION("Close then Symbol returns null") + { Fling::DynamicLibrary Library = Fling::DynamicLibrary::Open(LibraryName); CallLoadedSymbol(Library); Library.Close(); @@ -194,7 +214,8 @@ TEST_CASE("DynamicLibrary", "[core][DynamicLibrary]") { REQUIRE(Library.Symbol(SymbolName) == nullptr); } - SECTION("Destructor unloads a moved library") { + SECTION("Destructor unloads a moved library") + { Fling::DynamicLibrary Outer = Fling::DynamicLibrary::Open(LibraryName); REQUIRE(static_cast(Outer)); { @@ -207,7 +228,8 @@ TEST_CASE("DynamicLibrary", "[core][DynamicLibrary]") { } #if FLING_WINDOWS - SECTION("An unsigned absolute path is rejected") { + SECTION("An unsigned absolute path is rejected") + { const std::wstring Path = TempDirectory() + L"fling_unsigned_" + std::to_wstring(GetCurrentProcessId()) + L".dll"; const DeleteFileGuard RemoveFile(Path); @@ -219,7 +241,8 @@ TEST_CASE("DynamicLibrary", "[core][DynamicLibrary]") { REQUIRE(Error.find("not signed") != std::string::npos); } - SECTION("A bare name does not search the current directory") { + SECTION("A bare name does not search the current directory") + { const DWORD ProcessId = GetCurrentProcessId(); const std::wstring Dir = TempDirectory() + L"fling_dynlib_cwd_" + std::to_wstring(ProcessId); const BOOL Created = CreateDirectoryW(Dir.c_str(), nullptr); @@ -242,9 +265,11 @@ TEST_CASE("DynamicLibrary", "[core][DynamicLibrary]") { #endif #if FLING_LINUX - SECTION("An absolute path to a text file fails") { + SECTION("An absolute path to a text file fails") + { const std::string Path = "/tmp/fling_dynlib_" + std::to_string(static_cast(::getpid())) + ".txt"; - struct RemoveFile { + struct RemoveFile + { std::string Path; ~RemoveFile() { std::remove(Path.c_str()); } } const Guard{Path}; From 674f0205d14a3d9627afc26b8452dc2c1142e8e7 Mon Sep 17 00:00:00 2001 From: Ben Hoffman Date: Sun, 25 Oct 2026 09:47:39 -0400 Subject: [PATCH 4/4] Draw the skybox in a deferred subpass from a folder cube map. Cubemap is now a resource loaded from one folder with posx..negz faces, in place of the old ten-argument class that was not used. SkyboxSubpass draws a full screen triangle before the deferred composite, and the composite discards pixels that have no geometry. The [Graphics] Skybox config value selects the folder. Fixes #162 Co-Authored-By: Claude Opus 5.5 --- Assets/Shaders/Deferred/deferred.frag | 11 +- Assets/Shaders/Deferred/deferred_frag.spv | Bin 16592 -> 16608 bytes Assets/Shaders/skybox/skybox.frag | 18 +- Assets/Shaders/skybox/skybox.frag.spv | Bin 640 -> 1452 bytes Assets/Shaders/skybox/skybox.vert | 25 +- Assets/Shaders/skybox/skybox.vert.spv | Bin 1676 -> 1012 bytes Config/EngineConf.ini | 5 + FlingEngine/Application/src/Engine.cpp | 2 +- FlingEngine/Graphics/inc/VulkanApp.h | 1 + FlingEngine/Graphics/src/Cubemap.cpp | 667 ++++++------------ FlingEngine/Graphics/src/Cubemap.h | 168 ++--- FlingEngine/Graphics/src/RenderPipeline.cpp | 3 +- FlingEngine/Graphics/src/SkyboxSubpass.cpp | 135 ++++ FlingEngine/Graphics/src/SkyboxSubpass.h | 67 ++ .../Graphics/src/UniformBufferObject.h | 6 - FlingEngine/Graphics/src/VulkanApp.cpp | 30 +- FlingEngine/Graphics/src/VulkanFunctions.inl | 1 + THIRD_PARTY_NOTICES.md | 10 + 18 files changed, 528 insertions(+), 621 deletions(-) create mode 100644 FlingEngine/Graphics/src/SkyboxSubpass.cpp create mode 100644 FlingEngine/Graphics/src/SkyboxSubpass.h diff --git a/Assets/Shaders/Deferred/deferred.frag b/Assets/Shaders/Deferred/deferred.frag index aceb3ffa..8501131f 100644 --- a/Assets/Shaders/Deferred/deferred.frag +++ b/Assets/Shaders/Deferred/deferred.frag @@ -40,7 +40,16 @@ layout (binding = 7) uniform UBO void main() { // Get G-Buffer values - vec3 fragPos = texture(samplerposition, inUV).rgb; + vec4 position = texture(samplerposition, inUV); + + // The offscreen pass writes alpha 1 for geometry and clears to 0. Keep what is + // already in the frame (the skybox, if there is one) where there is no geometry. + if (position.a == 0.0) + { + discard; + } + + vec3 fragPos = position.rgb; vec3 normal = normalize(texture(samplerNormal, inUV).rgb); vec4 albedo = texture(samplerAlbedo, inUV); float roughness = texture(samplerRoughness, inUV).x; diff --git a/Assets/Shaders/Deferred/deferred_frag.spv b/Assets/Shaders/Deferred/deferred_frag.spv index 8f481a9413dd6bf66395d8abcbdc88e9006fc039..f854f40451a3caeebbe7eaa1ac8c5026f1482a30 100644 GIT binary patch delta 5919 zcmZ9Pd304*6~@m?UIsES_@FO92&AE6Y{9lzC8e+u(i9LZ36;_m+xmc)5S!#d28Taf z7naMlbS8v*SjVQu#$c_ebwVqp1FhfhzO%#4yDNvgzi$s` zpL6!z=kfTy#KgWtb#YNcnR5wO>B?R2f$Fs-_avO_I@CMME9PXQz zZhSy&rgwXGXD-ivP1r|o@wB{4+(Y5WwA9jXp4V>94R`PA&khdSd?%#q%MKAf%5x-4 zRHWDc0FT|-9j&?E+(4u$aX%Bcv8Q|2kS9xAvHOL%9~vr*JiHy+NaOee;}#>)8%#wX+0EUmfW{-G%5c$le7Ux5V?&gYTe{R#2CtXccT zdQS;&S+gnfJQW_QOfQAepXQfX{@TRSOg6!Vgs~2$C+LiQ?ADDY)2G3g=I$ z*v*l=(MPS7yt8L;D4#*ny>5zM(!H?&zo`JP09VD*yehn`df}|*0<+ezqk2Iq={l$I zrS9f%Pj$MnOKfg<=o;1h4qt6Qr?dx$421$z z$2blGyW2HLo$sPvGEohmUrN$-9qX8Rswi=1*pzAr?vnAH8jIxkalLuqR(ecT@uY=6 zC7JzUpRvDN7&Sch;CnnbNBE%RMbh(J?D1#Cv547<@`Z#8CMP;91&>WkS;jGC$zy>Y zF3?kkrYFfC3RgVQu<8<}%uYNi3}++HZ*%VFBCnN!4OaHJIGk8PosNqto+r#8xFzwV z_;TS|`B2r^!PUuXEWCSmIv~RDHTHTd>XsUZ!;1REhCu}MWbg;Q*=O`@33$SD=udhX zzZ&5`>rHkPL!EM`fW!rp3esla{1+(+juFAZRE-_%vtDL7*uUU06M*Ywh^@tI^q7UQ zHAkdt7ngLcc>=?&B9<>--eL!GgD{*80%wJ(4kjWoa94P4&XVAMG5k#6RM4>NT>Wv( z)o&PkE`7r(;fD(Fi5Mmxm$=!3xxhURqLSeDHiOkN;MOsW1Gi2bpDDm+W0-`_72sE6 zn0VY%`FOgtAh@V(0&0|t$}oB^E5l5{g=M%_ml@`T} z$3(!jX<*BCQ19Y{$%(%%g1%`Je?xM7;s=GXfhPbT2%IJUEy?s*^FJ;*?*DCJY~VYz zzwGxTiNHA&JOt${dql&6en;|%#~ku^HR3_PCyWie1^vEcc#C;LG8{2EkH;jFe6)L2 zB7P(Q`B->zZaVm}BwX74=KVx6oQ*1fj6W5pKetTdQ3*uBmSxZk&?D0bxF(S40S{}u zpyTK6Pf4JIT3Wb=K71IbTN>YkP2?;1>SoyId77>?@w4-Ir|I-B}W&yhUI(L(h>8MDYPD{qnPWL~OaiYHMQ~s|oJ|u|`7(0@s{{JWWkk>t@ zH`sK~OTO3Z&gk6)L16B!7d5!s2#fvdLlm*f6t_ww2 z`Y2h3IX4KSe^3c>_brx8KzOdc*Xqr~o}s%)1CBjIwn>*r&QDtE_-*%kjS1QclcB_L zOcPw}mDUa}5r*U8KxrXMh56lKEd-36`7D!+4?AN%ZxF`EDiFLZ&_C8fE|rXv{vs!S z4O}LGleG{qcII=rWPJF6$wc@p7siK$aU5XmSRhAug=A|xo%~vYFy)&iTi@ZA6PUnM zS(D_#wsWO8c&o0JlHsnTMKtnT3x?=a!YawucKF!^j&$Lk`txn4MQpt7tPzH5iP}!H zBpA#1YRT4iS|x+6?X*c=sd?EBR?3RViMjciM=X3S)-{sjZD*}89C6snsO>=Vx+TNer@Bir9FK85o+F-d@AO=eTS7Kv(L}e3BmfQ=kS(k4l6>p2bBmvz*Jq#q zM-;4CYWs1(0&f!sZ%6l0$sY!5;73o}FBw028f&234oHT#ulr+?;SPico6|L+BqG7S M;&l3Yuh8E7AI8JaBme*a literal 16592 zcma)?33#1VoyNa5$xSJxrIe-6QW{!lDYVe0rG!#Qx)-|9g#wO*+vKJVB=?q^n*xeb zL=>F?5dnv)SV6^gfVzN)QgNF>5ODzoov8|lD99pED$BgT@18d~`DC7Fz9;87@B99j zv!DCjBsGI4)F;WHq&^vvoIf~8pOML6DM_+-l1uBw%hoKL)>CPpHgnc-I1Eo}Qa^k~ zD6&>Hr90nQ#Bj3=1?AdTe&}jIs76_n|9Sm>I(rK53ym?n%cb6pn~H@VO^xhz$r#nEN}a{Zvd)d0Dv4IAs!2}fm%2)2)j5qE zKh|kgsYg7Qa@JvKu~I1ZbXG1-{i?C7>gs%Pqo!5ah9~1yw^ZcUs}-88zW26OruH{F zbxX(0v~*0L+A)24I*p2}O~xs%tJ*E`y5unN3rpp$b~>S_PCILo>cxdpcR@4KwnmFz zy0Ejjv2Rvbl5}R6iwW}eyi64K+8iP5IXY5!;2aH0*sRMt+sdVmd|RP1Z}oyj+5BpP zmgGk}Q&hX1qlLSIevImBn{~-l;eqYcCDWv7Ao;FneU6pZqc!(h)M-U&XMVn`t+z|_ z**}l}znS$WRhL7r)1EWmjmq0U-?hn1@nWgmo!0=E|5@S#=f5^NURqneJ6#ccW{YRX zb%L~k`(mJ zuZrP_msk($*?n7=j7pYNebfA_=B@Tzy2)sdR+sw!m`3%qHfdI_cph8C<9VDXo}I_} z(&BksAfBDah0?O~xJcUD&EsP9f11bIG)fd`8vx261u{w+)=?xn#R=d!bnBURzpoL2tfHe)8Rh zu6s^Z8grNGe5I1&`-RU>_1ffN)wFv?%4?Ecs;g(YCV5i0+V9%rDPcY$x>b|^=?s5n z0Ix}&75CVl8(^zVp3m?X2k@HYWpU=8c3v4^t4(%i_?`@ZJ;UG1@IiwHj-w{S>oR0dEWRP%l`pmx+Bx@b_Z;c*INMX+Kh93^xZg$bxZfr5cz;}w;XN7No8cE` z_{9TwF1bYf?fShi1RY`K}Fx_7ZWqO#HCXTip#|w~k+U}2GAJrz0$v^!Jk|Xo`2k~|DR;Dpv zp3UVFZ^`M0Be~Eqb(h);U0XT}7YZ{zblUdklQr7inD6e+r+(G=TH(S)+Pb~zRi3u} zgyMTQl+u1ZDIA}(zh(Ff8U9j+@6PZ&8UA{Pzcql@BsmSFy3cBoeTA2)zs%P`!u_8& zwaEnWxSz*n`1A~K%<$P6-jv~UGW_HWUzy=+2k=~SmUue%^Gjvka5M~!BbR<(D)m+t z=}Kt}ukZRSs!i{e!*SAM-XN^$N%_qJyl?Yh=RS%3;SI8>+0@QG65HX}*%QQbcf@u$ z_PSu_zKHE`?ChT<^746~AkJe=Zs~hG-;1o#C;Y~%(DpColoRH?9UsmuPn^qV8_l_V zzMadlH6eDQYVtpt$?r2A=SQPiG>^$=7|m^>IsY}ni^c3mTb$!~p5P6NXM9U!_gQ9M z>92X*7i;x>?N;NIDqRo9!HH{DiM9T?{<{Fbt&v~f7$S{v<9D43qq{W7Z$zXMH!5&) z5x2i;uw$^(-XTF}jN|0N_hU5c)xOmxW2Mmt>`!4f;-fO5o_uMV-xev|{P^3Kbsn zl_}eLiuwAKZ*Mj~M||6j2cK+yZfZ~Z{5Oa{_`m~mZEsKcvOfO~@u832a+2+v#JAme zW1syd@$2t>q|x?U#UCHBwD|z__jcj&DuYz8!|w>3x!57hd^1P3-y_Vva=r(J_4JMB zp(<~&eK^UJt*M>-YsA^J&bL0bC&|HD-%kiLhd~mmKIUW8@+U>`qko?Y2DCYW^Ulb_ zzT7TMKH9_oGvb)h@rU0WG{)inDZVQ zbKXPYbG5$QNuO5XK7wyoeTxeB&3f$u_RMF6@j+jy9YO5pgwZKHR7$JQCH<~Q1iSNp zL74l>`M)R}=l_y8KIqQ>Wnpv*4;A@`>2%QkS0tbjUCMS<#Pbc+2yn4HMRJFYVH#1SJd`TRX?P{gZ*d0PEFsqdmmOIM~mi@ zJNdUNkEyIzvCr>Wa{JG>Q;XkR6e~k(@{J5W= zR>8&^Fvoum8hxj4+6Ds7zpK-ntbnm5C#nC7{ zR6ge~iC`y|ef63!`<;Gp=fZ9KhcJ5%+aA?$xA}(fS5@5Ro5E-m9xAu_x(Ie+89RoZ z;^ciRI5z2)z&1#>C90V_=7sUoXZl7T$VH5E4VFK9`7+H9@ipRT``*_8RK(PFHCM~&dpw`hpO-pyLYftyX|4Z#7;?Oxa@F6O+NBdyMjufuuuTS^Re&XckJsX?bVGoTG$L{sq zUl@(TqcCN!=4PG`5ZNa2@-XiQiW7s*-0*peMqltBEsVxz0sHP?VRFt3He&cpMkfZJ zLxj;ho-x865AR)vs(3uu7?)$m1^Zp=3x%8jaNZq4#95oaN%m3 zxn!d75h`vI8-Bd&9T{xCM<$7*@vy$!J(I?w(^ri1FP6$ImfSg^A%^l6HuhCQb}}z1NNxMx*dBejxdH&%#by?1>YEeP8j;1-I=);k!dWc>jW9 zV}G9{9N$-S#nC7{ahtrWVfXk>5#AZ{^4Xh1aM!cAYS>=zhmKLzv&OxLg_9 zSuY(MF`kDrg~^Z2ZKJvEvxVdFzC)bx#&_1gNJFO`#{W)X+D3Pu&Jjj;;Zu`!XqBdP z=^D}&em?K#3S;N7mJb>+);0tUSGU(DLOlN18QWQ^(LJ^fVKnF2D2&EqZBx+V_Bw-x z>ne?c{db-S@%UqB{IppR#`cCrd4dYxff@HcDm?t&akl*Ut~E5m{6CQOVZxlB;i~yw z%)Q6o>B-}9j$_$->bOae?2hJZYi9u&ypq;7WeSw|d5&ArxB}`s?E(`N{wlICf z_NL|v9?v&tglB|`=XGCU=5;^So>%_gfq6COe;#6<&+xVkFJ$<}z}XAr=69Q5@}3(o zIX47MzV?90)e$gxHmP|2`5P7e_ne+5On%$X4|dO4mpB@Jj_DSrZS$fq{o-z54N5Bb zpnIJ+2OS&5^M8RbZD9A^QWmD4*xW{sFk>{Y2s2K%*(;0>y4&0$jPBw#`THYnU?(5z zbdgFtFZ|zGJTLDGcF)VZ#nJF{pDqz*9^C$Wgwb5w{(GenkKJRwRG2A4eY+xuMlP~u({0-2s5wd9~7od-_chJ zsTQB%4~x?VcFq>uXN!F@T7_qS6`!pGB79)Lqf{TH!kIf*wa?g9!u1iqHp6eo@NF5s zJ#fZKo~u=8-~1!O^uc%GHNyCy`!4*b@E{fLLhLJ4R;qAT=^u8_<;R54%&!aF>v_F6 zKIp_V9yI%ZT$p(<|M$Qhf1@})=)^OAH2Z%-7|r~Xfjjodai*ZgK-e0*2mB8mNus<7Adw1#ZF{}$6vk)13bEK( zFZzPbbMh@=H1oRy_Z)p&93OQ0J4*F;ROs(LDvtYZ(Cu^Y+xYC1hVIIKF%BV_`ET{*xk1u3Zt3-C~)`V$Kv>)v*z%4%_oWT9IoQ~ zX+nf2M)-(;nWrOF*bAH)?}ht?SugX419#tkB2Jvo!2`l*jNfbfQ(?x=KEeK=is$cV z!q^{DVZCV&jr_Fxa~1k-{tIFH@4M}n!uX(ju74%$xyByPHT}o#{y!p&X8ve~|5}_r zd2N3q%-GPq9*+qVhs}8&7bed0@>^m2&^`9w3DZ6{))DS?WbDVO@EoP$b(|7m_LB8u z0%jejMtEA}bF47yJ6$#F$@$-<60h%*5y$>ihCdx~+IlA9_&=NB&t>@Y8UA9#$@g-^ z$@fZz@6PZ&5hwojh!g)-h7Z!l@OZJ;M4b4#z`fsoFO9i1|AR2|&bpH4k1F_}`+M~h z!b+3={RBJvdY5W+=lPQ`Yl)3HhTHaM;aw`&o>mRVmQ(!~71kFUa}RG)Pq6(}WwZ+U zh{KK_Yl_YJ|0c|su(3XH+g=dfrGkyMg4_0zFnb6a>jtOodewhd*++$Z#9_ye{eaCm zUKRGZSzEYmuL-+ftTWuUe+chVAt!4N$HutdQ1P6w{_v*oo$pQINh-8OPGa$KUKYyz zVXwe#8!XIP($Vn_dzDtG((`Wo>m-B$; z+_~BZ(CxFIa2zvA zocVIh{^Izc`?KHxVg00^G33O@V?R)MxC%agM?Xjy-G#feUh$))F%In9o$&bXoFgBe z87hA7X$+WiKQqF!B7A(n+y%2Ed_v^Y6yXzvxl>P4?K^dh`a&Pg505x`Cq;a$_~eMw zw_B&&cq`44;+Zvm;KvrVO7Gaq`d2@aDjISK1-_Sn)>Hci^itc!=slrJ?)% zZ=5h1g@?=Nd0g47%^p24VfA_b7JG19OY+JQIXjGxLe!ao!`u@j-Xq zBZc*o=EY84)(D+EV`Mu@m@#5wec*26XkpsG##+IfbQ5tG9V0wmH8I%mVZG5EKTVkZ zXMU_WGo zf3t9ld}*7tg4vFnz_w8p5%0hi(yOzhPrN;kI2Q%sgUaZQ;)OE@Ac{ zHr5&LJM`Vc+@bW5zR)JQ$$>91>^=P4)_a5*uk*iG7$4txmkRs-q_)0QdVCl94!ume ze$sg*7xDNy?tQ{>oY($x>DY*O-`+3GJopa1LY!E1-!~r+CKj8u4+^6*Hpa~UNAno3 z6h<@ukT7T5cg=^z@jWikxI1h`VB8(VL-YKOhu+S>Z65o4i{ z1sXFRM#3?)*l#5r9n;`Cm>^D`3pf?18p2PKim-$6MVRCGKDN5eH0ks>!W6hagtt$( z>7tyS<&(~rcS-wevzvZyf9NHt7aDsC-QP*G+TNdZUOlU4zG7HJ?D<1QBG}|N+Ri!gAGeIHG|02Z zYG(bjmj-P6wpYb?cQ|O zC-SrY#e8NtV@J`}bk+yC?L2bR*kkAl7y>J~J$Q2}y7@g8S)4&UG+Fy6PZhJ9WF@=YvjT+TUp@ts`xE1&}OkLIygIeW)* zpx++doxJsZk6XaE(6-JhFy?(e<898y$j3SEaF!QJoFne`E}St+)Qx9s zY$~hDigfTApjBF3%|3M~BSoj{-96oVksr9w#l9E4xuG9=^ZvU(h@<}+ZH;FmZ{lX7 zg&X-^>%1Ab!G~ql>F?aE?#t}?B7O+ns6C&|LqX|swxb%kX)y7__M-1Ux&S_t+;cp8 zdF^SxlibYgm_rIM=S~vziyOjtXCI$yeN7JW*ht)3vfkG^C$6qMke;^ybMLzT1yx=X z{(6P+0Cy-iTdL@nFRsNZm`@yOr>KgKe%p`=IQoI{Krp|oy6%U*B1|uM^k6)}UFwDK q!(9pP@FgazdLxz@B_;jA^w?E$z9$u)YDzTlx9Wg5xNl9kQvLv9HZ>Ih diff --git a/Assets/Shaders/skybox/skybox.vert b/Assets/Shaders/skybox/skybox.vert index 32b1ee1b..38068bcf 100644 --- a/Assets/Shaders/skybox/skybox.vert +++ b/Assets/Shaders/skybox/skybox.vert @@ -1,25 +1,18 @@ #version 450 -layout (location = 0) in vec3 inPos; -layout(location = 1) in vec3 inColor; -layout(location = 2) in vec2 inTexCoord; +// Normalized device coordinates of this pixel, for the fragment shader to make a view ray +layout (location = 0) out vec2 outNDC; -layout (binding = 0) uniform UBO -{ - mat4 projection; - mat4 modelView; -} ubo; - -layout (location = 0) out vec3 outUVW; - -out gl_PerVertex +out gl_PerVertex { vec4 gl_Position; }; -void main() +void main() { - vec3 position = mat3(ubo.modelView) * inPos.xyz; - gl_Position = (ubo.projection * vec4(position, 0.0)).xyzz; - outUVW = inPos.xyz; + // One triangle that covers the full screen, made from the vertex index with no vertex buffer + // @see https://www.saschawillems.de/blog/2016/08/13/vulkan-tutorial-on-rendering-a-fullscreen-quad-without-buffers/ + vec2 uv = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); + outNDC = uv * 2.0 - 1.0; + gl_Position = vec4(outNDC, 1.0, 1.0); } diff --git a/Assets/Shaders/skybox/skybox.vert.spv b/Assets/Shaders/skybox/skybox.vert.spv index f63c6481e24c7abd891d9624605009100957721d..819c367cf0368128d6f912e8f39c69716ee93dd6 100644 GIT binary patch literal 1012 zcmZ9KSxZ|{6orpT6UQ3osSeknbpr80MGz+{q=HDK(AQEhlt4{Dqxe|x-5(_d|B^34 zzwh3AKs;e__FjAKb@w?Z#lm7|2pyp}bcMB$t-(-$32^0fubzEB+qh|8ZftFDs^|~J zRH&vxpX&p-`qgL}_kjtp0E+wy>`#bGsNU*I)VitN-dQWBeZT+w{87Jd*Y8i8m-YLs zN3Ygx`~2i@Usdo)iu{Mv+8!#RtDzhhG96v5I&8;Qm-j19YCGSajZ#=8(Z7jb>-P{@F)^mOT zyL^B=`i~Pki~jbTdnP&ioox!Rj$%HYrhgBecaB(%?=nkQ!Y=3R^X&R0KyUK}5WSsw z5xCT^uv@c#hx>CzeO>Y|FeP@qycgfzx%bKS`UpJxKUC(Gob&w8IX9T|-a(5#XCDIc z(Px-gpV%`(Ecdo&lw7`suQQARedG_A*4gCrv_1*s9&`Ptobzs@#|*J|D;{#4|1GuF z^o{?w=(_|jcY)rsdi{*T@qU+qT5Iu}$9VV#tH5{iZTzOW`d1uw!GEUz BH4y*+ literal 1676 zcmZ9L+inv<42CC%O$((hrS!llXgi~9P5D2JHdJ!o>&~O<+vOS#%o=G8JUvMtx7TmIX>e995@?xA@awRXDt@o|QG#NDZ_8;Q0 z;tJjopH+9(>qUJ}K1CypYeG$UBs>uggo1u~$zKX8DyG@$HQViG>us+;9`~lnk73vw zMx(Gdh$dk_iN1ySEzbExqyBIYo70EBdUMHM%!$}gJ%JjM5 zS?pOU2w4pzU}RdFnA?!4@+G+!mtmoiB{ILk-> z=1)BRV^ae*Zw4Fxb$yXl<>2L^Z%QjkR)j72Q#UsI71@iDaDaVYDf_&aZGEW=4!f)` zvxEc0FH3R`{PWCa25=j)S0yV#PT0+C=8aAKnk2sD>Ewa8m~H9sfc1aOC2_=%$6_u^hX*W+>1Zn4>lJ}o$c-;OP#^D? zJN>C1u&)dF!oSpHm;;>6;f8ePP#0_tH>JZ{&Rf!nqet%KwgAU6?v8Xg>NM_d#?e;M1& diff --git a/Config/EngineConf.ini b/Config/EngineConf.ini index efa62ee1..4951a132 100644 --- a/Config/EngineConf.ini +++ b/Config/EngineConf.ini @@ -21,6 +21,11 @@ WindowIconImage=FlingEngineLogo.png EnableValidationLayers=false #EnableValidationLayers=true +[Graphics] +; Cube map folder for the skybox, relative to the Assets directory. Leave empty for no skybox. +; @see Cubemap.h for the face file names +Skybox=Textures/Skybox/Garden + [Camera] MoveSpeed=10 RotationSpeed=700 diff --git a/FlingEngine/Application/src/Engine.cpp b/FlingEngine/Application/src/Engine.cpp index fa74db06..de0c8df3 100644 --- a/FlingEngine/Application/src/Engine.cpp +++ b/FlingEngine/Application/src/Engine.cpp @@ -128,7 +128,7 @@ namespace Fling else { VulkanApp::Get().Init( - static_cast(PipelineFlags::DEFERRED | PipelineFlags::IMGUI), + static_cast(PipelineFlags::DEFERRED | PipelineFlags::CUBEMAP | PipelineFlags::IMGUI), g_Registry, m_DebugUI ); diff --git a/FlingEngine/Graphics/inc/VulkanApp.h b/FlingEngine/Graphics/inc/VulkanApp.h index 1c880b61..d3eb6cc9 100644 --- a/FlingEngine/Graphics/inc/VulkanApp.h +++ b/FlingEngine/Graphics/inc/VulkanApp.h @@ -16,6 +16,7 @@ namespace Fling DEFERRED = (1u << 0), REFLECTIONS = (1u << 1), IMGUI = (1u << 2), + /** Draw a skybox behind the DEFERRED pipeline. The cube map folder is the [Graphics] Skybox config value. */ CUBEMAP = (1u << 3), DEBUG = (1u << 4), ALL = 0xff diff --git a/FlingEngine/Graphics/src/Cubemap.cpp b/FlingEngine/Graphics/src/Cubemap.cpp index 0ed6fcc7..1adc871e 100644 --- a/FlingEngine/Graphics/src/Cubemap.cpp +++ b/FlingEngine/Graphics/src/Cubemap.cpp @@ -2,523 +2,264 @@ #include "ResourceManager.h" #include "GraphicsHelpers.h" #include "LogicalDevice.h" -#include "PhyscialDevice.h" -#include "HDRImage.h" #include "VulkanApp.h" +#include "Buffer.h" #include "Logger.h" +#include "stb_image.h" + +#include +#include +#include +#include namespace Fling { - Cubemap::Cubemap( - Guid t_PosX_ID, - Guid t_NegX_ID, - Guid t_PosY_ID, - Guid t_NegY_ID, - Guid t_PosZ_ID, - Guid t_NegZ_ID, - Guid t_VertexShader, - Guid t_FragShader, - VkRenderPass t_RenderPass, - LogicalDevice* t_LogicalDevice) : - m_VertexShader(t_VertexShader), - m_FragShader(t_FragShader), - m_Device(t_LogicalDevice), - m_RenderPass(t_RenderPass) + namespace { - m_Cube = Model::Create("Models/cube.obj"_hs); - - LoadCubemapImages( - t_PosX_ID, - t_NegX_ID, - t_PosY_ID, - t_NegY_ID, - t_PosZ_ID, - t_NegZ_ID); - } + constexpr VkFormat CubemapFormat = VK_FORMAT_R8G8B8A8_UNORM; + constexpr uint32 CubemapFaceCount = 6; - Cubemap::Cubemap(Guid t_CubeMap_ID, VkRenderPass t_Renderpass, VkDevice t_LogicalDevice) - { - m_Cube = Model::Create("Models/cube.obj"_hs); + /** Face file names in Vulkan cube layer order: +X, -X, +Y, -Y, +Z, -Z */ + constexpr std::array FaceNames = { + "posx", "negx", "posy", "negy", "posz", "negz"}; - LoadCubeMapImage(t_CubeMap_ID); - } + constexpr std::array FaceExtensions = {".png", ".jpg", ".jpeg", ".tga", ".bmp"}; - Cubemap::~Cubemap() - { - if (m_ImageMemory) + /** Find the file for one face in the folder. Returns an empty string if there is no file. */ + std::string FindFaceFile(const std::string& t_Folder, const char* t_FaceName) { - vkFreeMemory(m_Device->GetVkDevice(), m_ImageMemory, nullptr); - m_ImageMemory = nullptr; + for (const char* Ext : FaceExtensions) + { + std::string Path = t_Folder + "/" + t_FaceName + Ext; + if (std::filesystem::exists(Path)) + { + return Path; + } + } + return {}; } + } // namespace - delete m_GraphicsPipeline; - m_GraphicsPipeline = nullptr; - - vkDestroyDescriptorPool(m_Device->GetVkDevice(), m_DescriptorPool, nullptr); - vkDestroyDescriptorSetLayout(m_Device->GetVkDevice(), m_DescriptorSetLayout, nullptr); - vkDestroyImage(m_Device->GetVkDevice(), m_Image, nullptr); - vkDestroyImageView(m_Device->GetVkDevice(), m_Imageview, nullptr); - vkDestroySampler(m_Device->GetVkDevice(), m_Sampler, nullptr); - } - - void Cubemap::Init(Camera* t_Camera, uint32 t_CurrentImage, size_t t_NumFramesInFlight, Multisampler* t_Sampler) + std::shared_ptr Cubemap::Create(Guid t_ID) { - // Initialize uniform buffers - m_numsFrameInFlight = t_NumFramesInFlight; - m_UniformBuffers.resize(m_numsFrameInFlight); - for (size_t i = 0; i < m_numsFrameInFlight; ++i) - { - m_UniformBuffers[i] = std::make_unique( - sizeof(UboSkyboxVS), - VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, - VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); - - m_UniformBuffers[i]->MapMemory(); - } - - UpdateUniformBuffer(t_CurrentImage, t_Camera->GetProjectionMatrix(), t_Camera->GetViewMatrix()); - SetupDescriptors(); - PreparePipeline(t_Sampler); + return ResourceManager::LoadResource(t_ID); } - void Cubemap::PreparePipeline(Multisampler* t_Sampler) + Cubemap::Cubemap(Guid t_ID) : Resource(t_ID) { - std::vector shaders = + // Under --null-render there is no GPU device, so there is no image to make + if (!VulkanApp::Get().HasDevice()) { - Shader::Create(m_VertexShader, m_Device).get(), - Shader::Create(m_FragShader, m_Device).get(), - }; - - m_GraphicsPipeline = new GraphicsPipeline( - shaders, - m_Device->GetVkDevice(), - VK_POLYGON_MODE_FILL, - GraphicsPipeline::Depth::ReadWrite, - VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, - VK_CULL_MODE_FRONT_BIT, - VK_FRONT_FACE_COUNTER_CLOCKWISE); - - m_GraphicsPipeline->CreateGraphicsPipeline(m_RenderPass, t_Sampler); - } - - void Cubemap::LoadCubeMapImage(Guid t_CubeMap_ID) - { - std::shared_ptr image = ResourceManager::LoadResource(t_CubeMap_ID, m_Device); - - m_ImageSize = image->GetImageSize(); - m_NumChannels = image->GetChannels(); - m_LayerSize = m_ImageSize / 6; - m_MipLevels = image->GetMipLevels(); - //m_MipLevels = 1.0f; - m_Format = image->GetVkImageFormat(); - - std::unique_ptr stagingBuffer = std::make_unique(); - stagingBuffer->CreateBuffer(m_ImageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, true); - stagingBuffer->MapMemory(); - - float* pixelDst = (float*)(stagingBuffer->m_MappedMem); - const float* pixels = image->GetPixelData(); - memcpy(pixelDst, pixels, m_ImageSize); - - stagingBuffer->UnmapMemory(); - - - GraphicsHelpers::CreateVkImage( - m_Device->GetVkDevice(), - image->GetWidth(), - image->GetHeight(), - m_MipLevels, // MipLevels - 1, // Depth - 6, // Array layers - m_Format, - VK_IMAGE_TILING_OPTIMAL, - VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, - VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, - VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT, - m_Image, - m_ImageMemory); - - VkCommandBuffer copyCmd = GraphicsHelpers::BeginSingleTimeCommands(); - - std::vector< VkBufferImageCopy> bufferCopyRegions; - VkDeviceSize offset = 0; - - for (size_t face = 0; face < 6; face++) - { - VkBufferImageCopy bufferCopyRegion = {}; - bufferCopyRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - bufferCopyRegion.imageSubresource.mipLevel = 0; - bufferCopyRegion.imageSubresource.baseArrayLayer = face; - bufferCopyRegion.imageSubresource.layerCount = 1; - bufferCopyRegion.imageExtent.width = image->GetWidth(); - bufferCopyRegion.imageExtent.height = image->GetHeight(); - bufferCopyRegion.imageExtent.depth = 1; - bufferCopyRegion.bufferOffset = offset; - - bufferCopyRegions.push_back(bufferCopyRegion); - - // Increase offset into staging buffer for next level / face - offset += image->GetImageSize(); + return; } - // Image barrier for optimal image (target) - // Set initial layout for all array layers (faces) of the optimal (target) tiled texture - VkImageSubresourceRange subresourceRange = {}; - subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - subresourceRange.baseMipLevel = 0; - subresourceRange.levelCount = m_MipLevels; - subresourceRange.layerCount = 6; - - GraphicsHelpers::SetImageLayout( - copyCmd, - m_Image, - VK_IMAGE_LAYOUT_UNDEFINED, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - subresourceRange, - VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, - VK_PIPELINE_STAGE_ALL_COMMANDS_BIT); - - // Copy the cube map faces from the staging buffer to the optimal tiled image - vkCmdCopyBufferToImage( - copyCmd, - stagingBuffer->GetVkBuffer(), - m_Image, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - static_cast(bufferCopyRegions.size()), - bufferCopyRegions.data() - ); - - // Change texture image layout to shader read after all faces have been copied - m_ImageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - GraphicsHelpers::SetImageLayout( - copyCmd, - m_Image, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - m_ImageLayout, - subresourceRange, - VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, - VK_PIPELINE_STAGE_ALL_COMMANDS_BIT); - - GraphicsHelpers::EndSingleTimeCommands(copyCmd); - - // Create sampler - VkSamplerCreateInfo sampler = Initializers::SamplerCreateInfo(); - sampler.magFilter = VK_FILTER_LINEAR; - sampler.minFilter = VK_FILTER_LINEAR; - sampler.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; - sampler.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - sampler.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - sampler.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - sampler.mipLodBias = 0.0f; - sampler.compareOp = VK_COMPARE_OP_NEVER; - sampler.minLod = 0.0f; - sampler.maxLod = static_cast(m_MipLevels); - sampler.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE; - sampler.maxAnisotropy = 1.0f; - - // Handle anisotropy - const PhysicalDevice* Device = VulkanApp::Get().GetPhysicalDevice(); - if (Device->GetDeivceFeatures().samplerAnisotropy) + LoadVulkanImage(); + if (m_Image == VK_NULL_HANDLE) { - sampler.maxAnisotropy = Device->GetDeviceProps().limits.maxSamplerAnisotropy; - sampler.anisotropyEnable = VK_TRUE; + return; } - if (vkCreateSampler(m_Device->GetVkDevice(), &sampler, nullptr, &m_Sampler) != VK_SUCCESS) - { - F_LOG_ERROR("Cube failed to create sampler"); - } + CreateImageView(); + CreateSampler(); - VkImageViewCreateInfo view = Initializers::ImageViewCreateInfo(); - view.viewType = VK_IMAGE_VIEW_TYPE_CUBE; - view.format = m_Format; - view.components = { VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A }; - view.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - view.subresourceRange.layerCount = 6; - view.subresourceRange.levelCount = m_MipLevels; - view.image = m_Image; - if (vkCreateImageView(m_Device->GetVkDevice(), &view, nullptr, &m_Imageview) != VK_SUCCESS) - { - F_LOG_ERROR("Cube failed to create image view"); - } + m_ImageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + m_ImageInfo.imageView = m_ImageView; + m_ImageInfo.sampler = m_Sampler; } - void Cubemap::LoadCubemapImages( - Guid t_PosX_ID, - Guid t_NegX_ID, - Guid t_PosY_ID, - Guid t_NegY_ID, - Guid t_PosZ_ID, - Guid t_NegZ_ID) + void Cubemap::LoadVulkanImage() { - std::array, 6> images = + const std::string Folder = GetFilepathReleativeToAssets(); + + // Load every face first, so that a missing or bad face makes no GPU objects + std::array Pixels = {}; + auto FreePixels = [&Pixels]() { - ResourceManager::LoadResource(t_PosX_ID), - ResourceManager::LoadResource(t_NegX_ID), - ResourceManager::LoadResource(t_PosY_ID), - ResourceManager::LoadResource(t_NegY_ID), - ResourceManager::LoadResource(t_PosZ_ID), - ResourceManager::LoadResource(t_NegZ_ID), + for (stbi_uc*& Face : Pixels) + { + stbi_image_free(Face); + Face = nullptr; + } }; - m_ImageSize = images[0]->GetImageSize() * 6; - m_NumChannels = images[0]->GetChannels(); - m_LayerSize = m_ImageSize / 6; - //TODO: add mip levels to image - m_MipLevels = images[0]->GetMipLevels(); - m_Format = images[0]->GetVkImageFormat(); - - - std::unique_ptr stagingBuffer = std::make_unique(); - stagingBuffer->CreateBuffer(m_ImageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, true); - stagingBuffer->MapMemory(); - - stbi_uc* pixelDst = (stbi_uc*)(stagingBuffer->m_MappedMem); - for (size_t i = 0; i < 6; i++) + for (uint32 Face = 0; Face < CubemapFaceCount; ++Face) { - const stbi_uc* pixels = images[i]->GetPixelData(); - memcpy(pixelDst, pixels, m_LayerSize); - pixelDst += m_LayerSize; - } + const std::string Path = FindFaceFile(Folder, FaceNames[Face]); + if (Path.empty()) + { + F_LOG_ERROR("Cubemap {}: cannot find face '{}'", Folder, FaceNames[Face]); + FreePixels(); + return; + } + + int Width = 0; + int Height = 0; + int Channels = 0; + Pixels[Face] = stbi_load(Path.c_str(), &Width, &Height, &Channels, STBI_rgb_alpha); + if (!Pixels[Face]) + { + F_LOG_ERROR("Cubemap {}: failed to load face {}", Folder, Path); + FreePixels(); + return; + } - stagingBuffer->UnmapMemory(); - - GraphicsHelpers::CreateVkImage( - m_Device->GetVkDevice(), - images[0]->GetWidth(), - images[0]->GetHeight(), - m_MipLevels, // MipLevels - 1, // Depth - 6, // Array layers - m_Format, - VK_IMAGE_TILING_OPTIMAL, - VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, - VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, - VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT, - m_Image, - m_ImageMemory); - - VkCommandBuffer copyCmd = GraphicsHelpers::BeginSingleTimeCommands(); - - std::vector< VkBufferImageCopy> bufferCopyRegions; - VkDeviceSize offset = 0; - - for (size_t face = 0; face < 6; face++) - { - VkBufferImageCopy bufferCopyRegion = {}; - bufferCopyRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - bufferCopyRegion.imageSubresource.mipLevel = 0; - bufferCopyRegion.imageSubresource.baseArrayLayer = face; - bufferCopyRegion.imageSubresource.layerCount = 1; - bufferCopyRegion.imageExtent.width = images[face]->GetWidth(); - bufferCopyRegion.imageExtent.height = images[face]->GetHeight(); - bufferCopyRegion.imageExtent.depth = 1; - bufferCopyRegion.bufferOffset = offset; - - bufferCopyRegions.push_back(bufferCopyRegion); - - // Increase offset into staging buffer for next level / face - offset += images[face]->GetImageSize(); + if (Width != Height || (Face > 0 && static_cast(Width) != m_FaceSize)) + { + F_LOG_ERROR("Cubemap {}: face {} is {}x{}. All faces must be square and the same size.", + Folder, + Path, + Width, + Height); + FreePixels(); + return; + } + m_FaceSize = static_cast(Width); } - // Image barrier for optimal image (target) - // Set initial layout for all array layers (faces) of the optimal (target) tiled texture - VkImageSubresourceRange subresourceRange = {}; - subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - subresourceRange.baseMipLevel = 0; - subresourceRange.levelCount = m_MipLevels; - subresourceRange.layerCount = 6; - - GraphicsHelpers::SetImageLayout( - copyCmd, - m_Image, - VK_IMAGE_LAYOUT_UNDEFINED, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - subresourceRange, - VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, - VK_PIPELINE_STAGE_ALL_COMMANDS_BIT); - - // Copy the cube map faces from the staging buffer to the optimal tiled image - vkCmdCopyBufferToImage( - copyCmd, - stagingBuffer->GetVkBuffer(), - m_Image, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - static_cast(bufferCopyRegions.size()), - bufferCopyRegions.data() - ); - - // Change texture image layout to shader read after all faces have been copied - m_ImageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - GraphicsHelpers::SetImageLayout( - copyCmd, - m_Image, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - m_ImageLayout, - subresourceRange, - VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, - VK_PIPELINE_STAGE_ALL_COMMANDS_BIT); - - GraphicsHelpers::EndSingleTimeCommands(copyCmd); - - // Create sampler - VkSamplerCreateInfo sampler = Initializers::SamplerCreateInfo(); - sampler.magFilter = VK_FILTER_LINEAR; - sampler.minFilter = VK_FILTER_LINEAR; - sampler.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; - sampler.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - sampler.addressModeV = sampler.addressModeU; - sampler.addressModeW = sampler.addressModeU; - sampler.mipLodBias = 0.0f; - sampler.compareOp = VK_COMPARE_OP_NEVER; - sampler.minLod = 0.0f; - sampler.maxLod = static_cast(m_MipLevels); - sampler.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE; - - // Handle anisotropy - const PhysicalDevice* Device = VulkanApp::Get().GetPhysicalDevice(); - if (Device->GetDeivceFeatures().samplerAnisotropy) + // Put all six faces one after the other in a staging buffer + const VkDeviceSize FaceBytes = static_cast(m_FaceSize) * m_FaceSize * 4; + Buffer StagingBuffer(FaceBytes * CubemapFaceCount, + VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + StagingBuffer.MapMemory(); + for (uint32 Face = 0; Face < CubemapFaceCount; ++Face) { - sampler.maxAnisotropy = Device->GetDeviceProps().limits.maxSamplerAnisotropy; - sampler.anisotropyEnable = VK_TRUE; + std::memcpy(static_cast(StagingBuffer.m_MappedMem) + FaceBytes * Face, Pixels[Face], FaceBytes); } - - if (vkCreateSampler(m_Device->GetVkDevice(), &sampler, nullptr, &m_Sampler) != VK_SUCCESS) + StagingBuffer.UnmapMemory(); + FreePixels(); + + VkDevice Device = VulkanApp::Get().GetLogicalDevice()->GetVkDevice(); + GraphicsHelpers::CreateVkImage(Device, + m_FaceSize, + m_FaceSize, + /* Mip levels */ 1, + /* Depth */ 1, + /* Array layers */ CubemapFaceCount, + CubemapFormat, + VK_IMAGE_TILING_OPTIMAL, + VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, + VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT, + m_Image, + m_ImageMemory); + + std::array Regions = {}; + for (uint32 Face = 0; Face < CubemapFaceCount; ++Face) { - F_LOG_ERROR("Cube failed to create sampler"); + VkBufferImageCopy& Region = Regions[Face]; + Region.bufferOffset = FaceBytes * Face; + Region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + Region.imageSubresource.mipLevel = 0; + Region.imageSubresource.baseArrayLayer = Face; + Region.imageSubresource.layerCount = 1; + Region.imageExtent = {m_FaceSize, m_FaceSize, 1}; } - VkImageViewCreateInfo view = Initializers::ImageViewCreateInfo(); - view.viewType = VK_IMAGE_VIEW_TYPE_CUBE; - view.format = m_Format; - view.components = { VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A }; - view.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - view.subresourceRange.layerCount = 6; - view.subresourceRange.levelCount = m_MipLevels; - view.image = m_Image; - if (vkCreateImageView(m_Device->GetVkDevice(), &view, nullptr, &m_Imageview) != VK_SUCCESS) - { - F_LOG_ERROR("Cube failed to create image view"); - } + VkImageSubresourceRange Range = {}; + Range.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + Range.baseMipLevel = 0; + Range.levelCount = 1; + Range.baseArrayLayer = 0; + Range.layerCount = CubemapFaceCount; + + VkCommandBuffer CmdBuf = GraphicsHelpers::BeginSingleTimeCommands(); + + GraphicsHelpers::SetImageLayout(CmdBuf, + m_Image, + VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + Range, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT); + + vkCmdCopyBufferToImage(CmdBuf, + StagingBuffer.GetVkBuffer(), + m_Image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + static_cast(Regions.size()), + Regions.data()); + + GraphicsHelpers::SetImageLayout(CmdBuf, + m_Image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + Range, + VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + + GraphicsHelpers::EndSingleTimeCommands(CmdBuf); } - void Cubemap::SetupDescriptors() + void Cubemap::CreateImageView() { - //Descriptor pools - std::vector poolSizes = - { - Initializers::DescriptorPoolSize(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 2), - Initializers::DescriptorPoolSize(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 2) - }; + VkImageViewCreateInfo ViewInfo = Initializers::ImageViewCreateInfo(); + ViewInfo.image = m_Image; + ViewInfo.viewType = VK_IMAGE_VIEW_TYPE_CUBE; + ViewInfo.format = CubemapFormat; + ViewInfo.components = { + VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A}; + ViewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, CubemapFaceCount}; + + VK_CHECK_RESULT( + vkCreateImageView(VulkanApp::Get().GetLogicalDevice()->GetVkDevice(), &ViewInfo, nullptr, &m_ImageView)); + } - VkDescriptorPoolCreateInfo descriptorPoolInfo = - Initializers::DescriptorPoolCreateInfo(poolSizes, 2); + void Cubemap::CreateSampler() + { + VkSamplerCreateInfo SamplerInfo = Initializers::SamplerCreateInfo(); + SamplerInfo.magFilter = VK_FILTER_LINEAR; + SamplerInfo.minFilter = VK_FILTER_LINEAR; + SamplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; + // Clamp so that the edges between faces do not show seams + SamplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + SamplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + SamplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + SamplerInfo.compareOp = VK_COMPARE_OP_NEVER; + SamplerInfo.minLod = 0.0f; + SamplerInfo.maxLod = 0.0f; + SamplerInfo.maxAnisotropy = 1.0f; + SamplerInfo.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE; + + VK_CHECK_RESULT( + vkCreateSampler(VulkanApp::Get().GetLogicalDevice()->GetVkDevice(), &SamplerInfo, nullptr, &m_Sampler)); + } - if (vkCreateDescriptorPool(m_Device->GetVkDevice(), &descriptorPoolInfo, nullptr, &m_DescriptorPool) != VK_SUCCESS) + void Cubemap::Release() + { + LogicalDevice* LogDevice = VulkanApp::Get().GetLogicalDevice(); + if (LogDevice == nullptr) { - F_LOG_ERROR("Cube map failed to create descriptor pool"); + // --null-render: the constructor made no GPU objects + return; } - //Descriptor SetLayout - std::vector setLayoutBinding = - { - //Binding 0 : vertex shader uniform buffer - Initializers::DescriptorSetLayoutBinding( - VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, - VK_SHADER_STAGE_VERTEX_BIT, - 0), - - Initializers::DescriptorSetLayoutBinding( - VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, - VK_SHADER_STAGE_FRAGMENT_BIT, - 1), - }; - - VkDescriptorSetLayoutCreateInfo descriptorLayout = - Initializers::DescriptorSetLayoutCreateInfo( - setLayoutBinding); + VkDevice Device = LogDevice->GetVkDevice(); - if (vkCreateDescriptorSetLayout(m_Device->GetVkDevice(), &descriptorLayout, nullptr, &m_DescriptorSetLayout) != VK_SUCCESS) + if (m_Sampler != VK_NULL_HANDLE) { - F_LOG_ERROR("Cube map failed to create descriptor set layout"); + vkDestroySampler(Device, m_Sampler, nullptr); + m_Sampler = VK_NULL_HANDLE; } - - //Descriptor Sets - m_DescriptorImageInfo = - Initializers::DescriptorImageInfo( - m_Sampler, - m_Imageview, - m_ImageLayout); - - VkDescriptorSetAllocateInfo allocInfo = - Initializers::DescriptorSetAllocateInfo( - m_DescriptorPool, - &m_DescriptorSetLayout, - 1); - - if (vkAllocateDescriptorSets(m_Device->GetVkDevice(), &allocInfo, &m_DescriptorSet) != VK_SUCCESS) + if (m_ImageView != VK_NULL_HANDLE) { - F_LOG_ERROR("Cube map failed to allocate descriptor sets") + vkDestroyImageView(Device, m_ImageView, nullptr); + m_ImageView = VK_NULL_HANDLE; } - - for (size_t i = 0; i < m_numsFrameInFlight; ++i) + if (m_Image != VK_NULL_HANDLE) { - m_UniformBufferDescriptor = {}; - m_UniformBufferDescriptor.buffer = m_UniformBuffers[i]->GetVkBuffer(); - m_UniformBufferDescriptor.offset = 0; - m_UniformBufferDescriptor.range = VK_WHOLE_SIZE; - - std::vector writeDescriptorSets = - { - // Binding 0 : Vertex shader uniform buffer - Initializers::WriteDescriptorSet( - m_DescriptorSet, - VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, - 0, - &m_UniformBufferDescriptor), - - // Binding 1 : Fragment shader cubemap sampler - Initializers::WriteDescriptorSet( - m_DescriptorSet, - VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, - 1, - &m_DescriptorImageInfo), - }; - - vkUpdateDescriptorSets(m_Device->GetVkDevice(), writeDescriptorSets.size(), writeDescriptorSets.data(), 0, NULL); + vkDestroyImage(Device, m_Image, nullptr); + m_Image = VK_NULL_HANDLE; + } + if (m_ImageMemory != VK_NULL_HANDLE) + { + vkFreeMemory(Device, m_ImageMemory, nullptr); + m_ImageMemory = VK_NULL_HANDLE; } } - void Cubemap::UpdateUniformBuffer(uint32 t_CurrentImage, const glm::mat4& t_ProjectionMatrix, const glm::mat4& t_ViewMatrix) - { - m_UboVS.Projection = t_ProjectionMatrix; - m_UboVS.Projection[1][1] *= -1.0f; - - m_UboVS.ModelView = t_ViewMatrix; - m_UboVS.ModelView[3][0] = 0.0f; - m_UboVS.ModelView[3][1] = 0.0f; - m_UboVS.ModelView[3][2] = 0.0f; - - memcpy(m_UniformBuffers[t_CurrentImage]->m_MappedMem, &m_UboVS, sizeof(m_UboVS)); - } - - void Cubemap::BindCmdBuffer(VkCommandBuffer& t_CommandBuffer) + Cubemap::~Cubemap() { - vkCmdBindDescriptorSets( - t_CommandBuffer, - VK_PIPELINE_BIND_POINT_GRAPHICS, - m_GraphicsPipeline->GetPipelineLayout(), - 0, - 1, - &m_DescriptorSet, - 0, - NULL); - - VkDeviceSize offsets[1] = { 0 }; - - vkCmdBindVertexBuffers(t_CommandBuffer, 0, 1, &GetVertexBuffer()->GetVkBuffer(), offsets); - vkCmdBindIndexBuffer(t_CommandBuffer, GetIndexBuffer()->GetVkBuffer(), 0, GetIndexType()); - vkCmdBindPipeline(t_CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, m_GraphicsPipeline->GetPipeline()); - vkCmdDrawIndexed(t_CommandBuffer, GetIndexCount(), 1, 0, 0, 0); + Release(); } -} \ No newline at end of file +} // namespace Fling diff --git a/FlingEngine/Graphics/src/Cubemap.h b/FlingEngine/Graphics/src/Cubemap.h index 46304447..a5f36e86 100644 --- a/FlingEngine/Graphics/src/Cubemap.h +++ b/FlingEngine/Graphics/src/Cubemap.h @@ -1,132 +1,54 @@ #pragma once -#include "Model.h" -#include "Texture.h" +#include "Resource.h" #include "FlingVulkan.h" -#include "Buffer.h" -#include "FlingTypes.h" -#include "FlingMath.h" -#include "File.h" -#include "LogicalDevice.h" -#include "UniformBufferObject.h" -#include "Camera.h" -#include "GraphicsPipeline.h" -#include "MultiSampler.h" #include -#include namespace Fling { - class LogicalDevice; - - class Cubemap + /** + * A cube map image made from six face images in one folder. The Guid is the folder path + * relative to the assets directory, for example "Textures/Skybox/Garden". The folder must + * have the files posx, negx, posy, negy, posz and negz, each with an image extension + * (.png, .jpg, .jpeg, .tga or .bmp). All faces must be square and the same size. + */ + class Cubemap : public Resource { - public: - Cubemap( - Guid t_PosX_ID, - Guid t_NegX_ID, - Guid t_PosY_ID, - Guid t_NegY_ID, - Guid t_PosZ_ID, - Guid t_NegZ_ID, - Guid t_VertexShader, - Guid t_FragShader, - VkRenderPass t_RenderPass, - LogicalDevice* t_LogicalDevice); - - Cubemap( - Guid t_CubeMap_ID, - VkRenderPass t_Renderpass, - VkDevice t_LogicalDevice); - - ~Cubemap(); - - void Init(Camera* t_Camera, uint32 t_CurrentImage, size_t t_NumeFramesInFlight, Multisampler* t_Sampler); - - void UpdateUniformBuffer(uint32 t_CurrentImage, const glm::mat4& t_ProjectionMatrix, const glm::mat4& t_ViewMatrix); - - void BindCmdBuffer(VkCommandBuffer& t_CommandBuffer); - - std::unique_ptr GetGraphicsPipeline() const { return std::unique_ptr(m_GraphicsPipeline); } - - /** - * Get the Descriptor Sets object - * - * @return const VkDescriptorSet> - */ - VkDescriptorSet& GetDescriptorSet() { return m_DescriptorSet; } - - /** - * Get the Vertex Buffer object - * - * @return const Buffer* - */ - Buffer* GetVertexBuffer() const { return m_Cube->GetVertexBuffer(); } - - /** - * Get the Index Buffer object - * - * @return const Buffer* - */ - Buffer* GetIndexBuffer() const { return m_Cube->GetIndexBuffer(); } - - /** - * Get the Index Count object - */ - uint32 GetIndexCount() const { return m_Cube->GetIndexCount(); } - - VkIndexType GetIndexType() const { return m_Cube->GetIndexType(); } - - VkImage GetImage() const { return m_Image; } - VkDeviceMemory GetImageMemory() const{ return m_ImageMemory; } - VkDescriptorImageInfo& GetImageInfo() { return m_DescriptorImageInfo; } - - private: - - void PreparePipeline(Multisampler* t_Sampler); - - /** Loads cubemaps with file format .hdr for ibl **/ - void LoadCubeMapImage(Guid t_CubeMap_ID); - - void LoadCubemapImages( - Guid t_PosX_ID, - Guid t_NegX_ID, - Guid t_PosY_ID, - Guid t_NegY_ID, - Guid t_PosZ_ID, - Guid t_NegZ_ID); - - void SetupDescriptors(); - - std::vector m_cubeMapImages; - std::vector> m_UniformBuffers; - - VkImage m_Image; - VkImageView m_Imageview; - VkImageLayout m_ImageLayout; - VkDeviceMemory m_ImageMemory; - VkSampler m_Sampler; - - VkDescriptorSetLayout m_DescriptorSetLayout; - VkDescriptorImageInfo m_DescriptorImageInfo; - VkDescriptorSet m_DescriptorSet; - VkDescriptorPool m_DescriptorPool; - VkRenderPass m_RenderPass; - LogicalDevice* m_Device; - VkDescriptorBufferInfo m_UniformBufferDescriptor; - - VkDeviceSize m_ImageSize; - VkDeviceSize m_LayerSize; - VkFormat m_Format; - uint32 m_NumChannels; - uint32 m_MipLevels; - size_t m_numsFrameInFlight; - - UboSkyboxVS m_UboVS; - std::shared_ptr m_Cube; - GraphicsPipeline* m_GraphicsPipeline; - - Guid m_FragShader; - Guid m_VertexShader; + public: + static std::shared_ptr Create(Guid t_ID); + + explicit Cubemap(Guid t_ID); + virtual ~Cubemap(); + + FORCEINLINE uint32 GetFaceSize() const { return m_FaceSize; } + + /** True when the faces loaded and the GPU image exists */ + FORCEINLINE bool IsValid() const { return m_ImageView != VK_NULL_HANDLE; } + + FORCEINLINE const VkDescriptorImageInfo* GetDescriptorInfo() const { return &m_ImageInfo; } + + /** Release the Vulkan resources of this cube map */ + void Release(); + + private: + /** Load the six faces and upload them to one cube compatible image with six layers */ + void LoadVulkanImage(); + + void CreateImageView(); + + void CreateSampler(); + + /** Width and height of each face, in pixels */ + uint32 m_FaceSize = 0; + + VkImage m_Image = VK_NULL_HANDLE; + + VkDeviceMemory m_ImageMemory = VK_NULL_HANDLE; + + VkImageView m_ImageView = VK_NULL_HANDLE; + + VkSampler m_Sampler = VK_NULL_HANDLE; + + VkDescriptorImageInfo m_ImageInfo{}; }; -} \ No newline at end of file +} // namespace Fling diff --git a/FlingEngine/Graphics/src/RenderPipeline.cpp b/FlingEngine/Graphics/src/RenderPipeline.cpp index 746f0a0c..23c18da1 100644 --- a/FlingEngine/Graphics/src/RenderPipeline.cpp +++ b/FlingEngine/Graphics/src/RenderPipeline.cpp @@ -106,7 +106,8 @@ namespace Fling poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; poolInfo.poolSizeCount = static_cast(poolSizes.size()); poolInfo.pPoolSizes = poolSizes.data(); - poolInfo.maxSets = SwapImageCount; + // Each subpass may allocate one set for each swap chain image + poolInfo.maxSets = SwapImageCount * static_cast(m_Subpasses.size()); VK_CHECK_RESULT(vkCreateDescriptorPool(m_Device->GetVkDevice(), &poolInfo, nullptr, &m_DescriptorPool)); diff --git a/FlingEngine/Graphics/src/SkyboxSubpass.cpp b/FlingEngine/Graphics/src/SkyboxSubpass.cpp new file mode 100644 index 00000000..1ac9bdbf --- /dev/null +++ b/FlingEngine/Graphics/src/SkyboxSubpass.cpp @@ -0,0 +1,135 @@ +#include "SkyboxSubpass.h" +#include "Buffer.h" +#include "CommandBuffer.h" +#include "Cubemap.h" +#include "FirstPersonCamera.h" +#include "GraphicsHelpers.h" +#include "GraphicsPipeline.h" +#include "LogicalDevice.h" +#include "SwapChain.h" +#include "VulkanApp.h" +#include "VulkanAppState.h" + +namespace Fling +{ + SkyboxSubpass::SkyboxSubpass(const LogicalDevice* t_Dev, + const Swapchain* t_Swap, + VkRenderPass t_GlobalRenderPass, + FirstPersonCamera* t_Cam, + std::shared_ptr t_Cubemap, + std::shared_ptr t_Vert, + std::shared_ptr t_Frag) + : Subpass(t_Dev, t_Swap, t_Vert, t_Frag), + m_GlobalRenderPass(t_GlobalRenderPass), + m_Camera(t_Cam), + m_Cubemap(std::move(t_Cubemap)) + { + assert(m_GlobalRenderPass != VK_NULL_HANDLE && m_Camera && m_Cubemap && m_Cubemap->IsValid()); + + const VkDeviceSize BufferSize = sizeof(SkyboxUbo); + m_UboBuffers.resize(m_SwapChain->GetImageCount()); + for (std::unique_ptr& Buf : m_UboBuffers) + { + Buf = std::make_unique(BufferSize, + VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + + Buf->MapMemory(BufferSize); + } + } + + SkyboxSubpass::~SkyboxSubpass() = default; + + void + SkyboxSubpass::Draw(CommandBuffer& t_CmdBuf, uint32 t_ActiveFrameInFlight, entt::registry& t_reg, float DeltaTime) + { + // Invert the projection Y to match the Vulkan clip space, like the other passes + glm::mat4 Projection = m_Camera->GetProjectionMatrix(); + Projection[1][1] *= -1.0f; + + // Only the camera rotation moves the sky + const glm::mat4 ViewRotation = glm::mat4(glm::mat3(m_Camera->GetViewMatrix())); + + SkyboxUbo Ubo; + Ubo.InvViewProjection = glm::inverse(Projection * ViewRotation); + memcpy(m_UboBuffers[t_ActiveFrameInFlight]->m_MappedMem, &Ubo, sizeof(Ubo)); + + vkCmdBindDescriptorSets(t_CmdBuf.GetHandle(), + VK_PIPELINE_BIND_POINT_GRAPHICS, + m_GraphicsPipeline->GetPipelineLayout(), + 0, + 1, + &m_DescriptorSets[t_ActiveFrameInFlight], + 0, + nullptr); + + vkCmdBindPipeline(t_CmdBuf.GetHandle(), VK_PIPELINE_BIND_POINT_GRAPHICS, m_GraphicsPipeline->GetPipeline()); + + // One full screen triangle. The vertex shader makes the positions from gl_VertexIndex. + vkCmdDraw(t_CmdBuf.GetHandle(), 3, 1, 0, 0); + } + + void SkyboxSubpass::CreateDescriptorSets(VkDescriptorPool t_Pool, entt::registry& t_reg) + { + // We only need to do the actual allocation of sets ONCE + if (m_DescPool == VK_NULL_HANDLE) + { + m_DescPool = t_Pool; + + const size_t ImageCount = m_SwapChain->GetImageCount(); + m_DescriptorSets.resize(ImageCount); + + std::vector Layouts(ImageCount, m_GraphicsPipeline->GetDescriptorSetLayout()); + VkDescriptorSetAllocateInfo AllocInfo = {}; + AllocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + AllocInfo.descriptorPool = t_Pool; + AllocInfo.descriptorSetCount = static_cast(ImageCount); + AllocInfo.pSetLayouts = Layouts.data(); + + VK_CHECK_RESULT(vkAllocateDescriptorSets(m_Device->GetVkDevice(), &AllocInfo, m_DescriptorSets.data())); + } + + for (size_t i = 0; i < m_DescriptorSets.size(); ++i) + { + // The cube map info is const, but the Vulkan write struct wants a mutable pointer + VkDescriptorImageInfo CubemapInfo = *m_Cubemap->GetDescriptorInfo(); + + std::vector WriteDescriptorSets = { + // 0 : Camera UBO to the fragment shader + Initializers::WriteDescriptorSetUniform(m_UboBuffers[i].get(), m_DescriptorSets[i], 0), + // 1 : Cube map sampler + Initializers::WriteDescriptorSet( + m_DescriptorSets[i], VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, &CubemapInfo), + }; + + vkUpdateDescriptorSets(m_Device->GetVkDevice(), + static_cast(WriteDescriptorSets.size()), + WriteDescriptorSets.data(), + 0, + nullptr); + } + } + + void SkyboxSubpass::CreateGraphicsPipeline() + { + // The full screen triangle covers every pixel, so cull nothing and ignore depth + m_GraphicsPipeline->m_RasterizationState = Initializers::PipelineRasterizationStateCreateInfo( + VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE); + + m_GraphicsPipeline->m_DepthStencilState.depthTestEnable = VK_FALSE; + m_GraphicsPipeline->m_DepthStencilState.depthWriteEnable = VK_FALSE; + + m_GraphicsPipeline->CreateGraphicsPipeline(m_GlobalRenderPass, nullptr); + } + + void SkyboxSubpass::OnSwapchainResized(entt::registry& t_reg) + { + m_GlobalRenderPass = VulkanApp::Get().GetState().m_RenderPass; + CreateGraphicsPipeline(); + + if (m_DescPool != VK_NULL_HANDLE) + { + CreateDescriptorSets(m_DescPool, t_reg); + } + } +} // namespace Fling diff --git a/FlingEngine/Graphics/src/SkyboxSubpass.h b/FlingEngine/Graphics/src/SkyboxSubpass.h new file mode 100644 index 00000000..6620ec64 --- /dev/null +++ b/FlingEngine/Graphics/src/SkyboxSubpass.h @@ -0,0 +1,67 @@ +#pragma once + +#include "Subpass.h" +#include "FlingMath.h" +#include +#include + +namespace Fling +{ + class Buffer; + class CommandBuffer; + class Cubemap; + class FirstPersonCamera; + class LogicalDevice; + class Swapchain; + + /** + * Draws a cube map behind the scene. It draws one full screen triangle with no depth test + * in the global render pass, before the deferred composite. The composite discards pixels + * that have no geometry, so the sky shows through there. + * + * @see GeometrySubpass + */ + class SkyboxSubpass : public Subpass + { + public: + SkyboxSubpass(const LogicalDevice* t_Dev, + const Swapchain* t_Swap, + VkRenderPass t_GlobalRenderPass, + FirstPersonCamera* t_Cam, + std::shared_ptr t_Cubemap, + std::shared_ptr t_Vert, + std::shared_ptr t_Frag); + + virtual ~SkyboxSubpass(); + + void + Draw(CommandBuffer& t_CmdBuf, uint32 t_ActiveFrameInFlight, entt::registry& t_reg, float DeltaTime) override; + + void CreateDescriptorSets(VkDescriptorPool t_Pool, entt::registry& t_reg) override; + + void CreateGraphicsPipeline() override; + + void OnSwapchainResized(entt::registry& t_reg) override; + + private: + /** Matches the UBO at binding 0 of skybox.frag */ + struct SkyboxUbo + { + /** Inverse of projection * view, with the camera translation removed */ + glm::mat4 InvViewProjection; + }; + + VkRenderPass m_GlobalRenderPass = VK_NULL_HANDLE; + + const FirstPersonCamera* m_Camera = nullptr; + + std::shared_ptr m_Cubemap; + + /** One UBO for each swap chain image */ + std::vector> m_UboBuffers; + + std::vector m_DescriptorSets; + + VkDescriptorPool m_DescPool = VK_NULL_HANDLE; + }; +} // namespace Fling diff --git a/FlingEngine/Graphics/src/UniformBufferObject.h b/FlingEngine/Graphics/src/UniformBufferObject.h index 60b6a0f6..982c9f83 100644 --- a/FlingEngine/Graphics/src/UniformBufferObject.h +++ b/FlingEngine/Graphics/src/UniformBufferObject.h @@ -16,10 +16,4 @@ namespace Fling // The world position of this object glm::vec3 ObjPos; }; - - struct UboSkyboxVS - { - glm::mat4 Projection; - glm::mat4 ModelView; - }; } // namespace Fling \ No newline at end of file diff --git a/FlingEngine/Graphics/src/VulkanApp.cpp b/FlingEngine/Graphics/src/VulkanApp.cpp index 2634d1a5..1e829b1c 100644 --- a/FlingEngine/Graphics/src/VulkanApp.cpp +++ b/FlingEngine/Graphics/src/VulkanApp.cpp @@ -3,6 +3,7 @@ #include "RenderPipeline.h" #include "GeometrySubpass.h" +#include "SkyboxSubpass.h" #include "OffscreenSubpass.h" #include "ImGuiSubpass.h" #include "DebugSubpass.h" @@ -17,6 +18,7 @@ #include "FirstPersonCamera.h" #include "GraphicsHelpers.h" #include "DepthBuffer.h" +#include "Cubemap.h" #include "Logger.h" #include "Version.h" #include "BuildConfig.h" @@ -424,6 +426,28 @@ namespace Fling assert(Offscreen); FrameBuffer* OffscreenBuf = Offscreen->GetOffscreenFrameBuffer(); assert(OffscreenBuf); + + // Skybox pass ------ + // Draws before the geometry pass, which discards the pixels that have no geometry + if (t_Conf & PipelineFlags::CUBEMAP) + { + const std::string SkyboxPath = FlingConfig::GetString("Graphics", "Skybox", "Textures/Skybox/Garden"); + std::shared_ptr Sky = SkyboxPath.empty() ? nullptr : Cubemap::Create(HS(SkyboxPath.c_str())); + if (Sky && Sky->IsValid()) + { + F_LOG_TRACE("Add skybox {} to the DEFERRED render pipeline", SkyboxPath); + std::shared_ptr SkyboxVert = + Shader::Create(HS("Shaders/skybox/skybox.vert.spv"), m_LogicalDevice); + std::shared_ptr SkyboxFrag = + Shader::Create(HS("Shaders/skybox/skybox.frag.spv"), m_LogicalDevice); + Subpasses.emplace_back(std::make_unique( + m_LogicalDevice, m_SwapChain, m_State->m_RenderPass, m_Camera, Sky, SkyboxVert, SkyboxFrag)); + } + else if (!SkyboxPath.empty()) + { + F_LOG_ERROR("Skybox {} did not load, so the DEFERRED render pipeline has no skybox", SkyboxPath); + } + } std::shared_ptr GeomVert = Shader::Create(HS("Shaders/Deferred/deferred_vert.spv"), m_LogicalDevice); std::shared_ptr GeomFrag = @@ -445,12 +469,6 @@ namespace Fling F_LOG_WARN("Bulid REFLECTIONS render pipeline! (NOT YET IMPL)"); } - if (t_Conf & PipelineFlags::CUBEMAP) - { - F_LOG_WARN("Build CUBEMAP render pipeline!"); - // Add a cubemap - } - /*if (t_Conf & PipelineFlags::DEBUG) { F_LOG_WARN("Build DEBUG render pipeline!"); diff --git a/FlingEngine/Graphics/src/VulkanFunctions.inl b/FlingEngine/Graphics/src/VulkanFunctions.inl index a00ee373..fedc9158 100644 --- a/FlingEngine/Graphics/src/VulkanFunctions.inl +++ b/FlingEngine/Graphics/src/VulkanFunctions.inl @@ -83,6 +83,7 @@ FLING_VK_DEVICE(vkCmdBindVertexBuffers) FLING_VK_DEVICE(vkCmdBlitImage) FLING_VK_DEVICE(vkCmdCopyBuffer) FLING_VK_DEVICE(vkCmdCopyBufferToImage) +FLING_VK_DEVICE(vkCmdDraw) FLING_VK_DEVICE(vkCmdDrawIndexed) FLING_VK_DEVICE(vkCmdEndRenderPass) FLING_VK_DEVICE(vkCmdNextSubpass) diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index dc07a869..8c6954d9 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -40,6 +40,16 @@ freely, subject to the following restrictions: 3. This notice may not be removed or altered from any source distribution. ``` +## Garden skybox by Humus + +The six face images in +[`Assets/Textures/Skybox/Garden/`](Assets/Textures/Skybox/Garden/) are the work +of Emil Persson, aka [Humus](http://www.humus.name). The default skybox uses them +(see the `[Graphics] Skybox` value in `Config/EngineConf.ini`). They are +licensed under the +[Creative Commons Attribution 3.0 Unported License](http://creativecommons.org/licenses/by/3.0/). +The author's `readme.txt` is in the same folder. + ## Libraries in `external/` These are git submodules. Each one keeps its own license file, listed here.