From 1d9279a94fc995174a1854fd936197755289aa00 Mon Sep 17 00:00:00 2001 From: jamylak Date: Sat, 4 May 2024 11:11:04 +1000 Subject: [PATCH] Initial commit --- .clang-format | 4 + .github/workflows/test.yml | 41 + .gitignore | 5 + CMakeLists.txt | 42 + Makefile | 4 + README.md | 69 ++ flake.lock | 61 ++ flake.nix | 68 ++ include/fileutils.h | 32 + include/filewatcher/filewatcher.h | 34 + include/filewatcher/filewatcher_factory.h | 23 + include/filewatcher/inotify_utils.h | 40 + include/filewatcher/linux_filewatcher.h | 23 + include/filewatcher/mac_filewatcher.h | 37 + include/glfwutils.h | 53 ++ include/sdf_renderer.h | 80 ++ include/shader_utils.h | 13 + include/vkutils.h | 994 ++++++++++++++++++++++ linux-tests.Dockerfile | 29 + scripts/build.sh | 2 + scripts/build_quiet.sh | 2 + scripts/test.sh | 4 + shaders/fullscreenquad.vert | 14 + shaders/toytemplate.frag | 31 + shaders/vulktemplate.frag | 21 + src/filewatcher/linux_filewatcher.cpp | 104 +++ src/filewatcher/mac_filewatcher.cpp | 95 +++ src/main.cpp | 40 + src/sdf_renderer.cpp | 236 +++++ src/shader_utils.cpp | 154 ++++ tests/CMakeLists.txt | 14 + tests/filewatcher/CMakeLists.txt | 29 + tests/filewatcher/test_filewatcher.cpp | 115 +++ tests/test_shader_comp.cpp | 70 ++ tests/test_utils.h | 22 + 35 files changed, 2605 insertions(+) create mode 100644 .clang-format create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 Makefile create mode 100644 README.md create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 include/fileutils.h create mode 100644 include/filewatcher/filewatcher.h create mode 100644 include/filewatcher/filewatcher_factory.h create mode 100644 include/filewatcher/inotify_utils.h create mode 100644 include/filewatcher/linux_filewatcher.h create mode 100644 include/filewatcher/mac_filewatcher.h create mode 100644 include/glfwutils.h create mode 100644 include/sdf_renderer.h create mode 100644 include/shader_utils.h create mode 100644 include/vkutils.h create mode 100644 linux-tests.Dockerfile create mode 100755 scripts/build.sh create mode 100755 scripts/build_quiet.sh create mode 100755 scripts/test.sh create mode 100644 shaders/fullscreenquad.vert create mode 100644 shaders/toytemplate.frag create mode 100644 shaders/vulktemplate.frag create mode 100644 src/filewatcher/linux_filewatcher.cpp create mode 100644 src/filewatcher/mac_filewatcher.cpp create mode 100644 src/main.cpp create mode 100644 src/sdf_renderer.cpp create mode 100644 src/shader_utils.cpp create mode 100644 tests/CMakeLists.txt create mode 100644 tests/filewatcher/CMakeLists.txt create mode 100644 tests/filewatcher/test_filewatcher.cpp create mode 100644 tests/test_shader_comp.cpp create mode 100644 tests/test_utils.h diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..960bc8c --- /dev/null +++ b/.clang-format @@ -0,0 +1,4 @@ +BasedOnStyle: LLVM +IndentWidth: 4 +TabWidth: 4 +UseTab: Never diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..ee084f6 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,41 @@ +name: Continuous Integration + +on: + pull_request: + branches: + - main + +jobs: + build-and-test-linux: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Build Docker image + run: docker build -t linux-tests -f linux-tests.Dockerfile . + + - name: Test with Docker + run: docker run linux-tests + + build-and-test-macos: + name: Build and Test on macOS + runs-on: macos-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Install dependencies + run: | + brew install glfw glslang spdlog glm vulkan-tools googletest + + - name: Configure + run: cmake -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTS=ON -B build + + - name: Build + run: cmake --build build + + - name: Run tests + run: build/tests/vsdf_tests && build/tests/filewatcher/filewatcher_tests diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0c3ee31 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +a.out +.cache/clangd/index +build +testshaders +shaders/fullscreenquad.spv diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..53d6ea1 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,42 @@ +cmake_minimum_required(VERSION 3.6) + +set(CMAKE_CXX_STANDARD 20) +project(vsdf) + +find_package(glfw3 REQUIRED) +find_package(Vulkan REQUIRED) +find_package(glslang REQUIRED) +find_package(spdlog REQUIRED) +find_package(glm REQUIRED) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +if (CMAKE_BUILD_TYPE STREQUAL "Debug") + add_compile_options(-fsanitize=address) + add_link_options(-fsanitize=address) +endif() + +add_executable(${PROJECT_NAME} src/main.cpp src/shader_utils.cpp src/sdf_renderer.cpp) + +if(APPLE) + message("Building filewatcher for macOS") + target_sources(${PROJECT_NAME} PRIVATE src/filewatcher/mac_filewatcher.cpp) + find_library(CORE_SERVICES_LIBRARY CoreServices) + target_link_libraries(${PROJECT_NAME} PRIVATE ${CORE_SERVICES_LIBRARY}) +elseif(UNIX AND NOT APPLE) + # Print building linux + message("Building filewatcher for Linux") + find_package(Threads REQUIRED) + target_sources(${PROJECT_NAME} PRIVATE src/filewatcher/linux_filewatcher.cpp) + target_link_libraries(${PROJECT_NAME} PRIVATE Threads::Threads) +endif() + +target_link_libraries(${PROJECT_NAME} PRIVATE glfw Vulkan::Vulkan spdlog::spdlog glslang::glslang glslang::glslang-default-resource-limits glslang::SPIRV) +include_directories(${PROJECT_NAME} PRIVATE include ${GLM_INCLUDE_DIRS}) + +# Option to enable or disable the building of tests +option(BUILD_TESTS "Build the tests" OFF) + +if(BUILD_TESTS) + enable_testing() + add_subdirectory(tests) +endif() diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6eee4bd --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +# I mainly just use this to do quick :make +# See README.md for real build instructions +all: + @./scripts/build_quiet.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..ddb6c4e --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# VSDF +Vulkan SDF Renderer + Hot Reloader + +![Preview](https://i.imgur.com/88KG4NL.gif) + +Render an SDF like ShaderToy using Vulkan and hot reload based on frag shader changes. +That way you can use your favourite editor / LSP and also utilise git. + +## Platforms +Supports Mac & Linux currently because it contains filewatcher implementations for those platforms so far but could add Windows and then the rest of the code should work on there... + +## Build +```sh +cmake -B build . +cmake --build build +./build/vsdf {filepath}.frag +``` + +## Usage +### With shader toy shaders (most seem to work) +```sh +./vsdf --toy path/to/shader.frag +``` + +Note: That if you use `--toy` we will prepend a template in +`shaders/toytemplate.frag` which sets up the push constants +like `iTime` and we will also follow this logic. + +```cpp +if (useToyTemplate) { + Client = glslang::EShClientOpenGL; + ClientVersion = glslang::EShTargetOpenGL_450; +} else { + Client = glslang::EShClientVulkan; + ClientVersion = glslang::EShTargetVulkan_1_0; +} +``` + +### Manually create your own vulkan compatible shader +If you don't want any template prepended or you have issues +loading that way, I recommend copying `shaders/vulktemplate.frag` +and adjusting it to your liking + +- See `shaders/vulktemplate.frag` to see how push constants + are passed in +```sh +./vsdf path/to/shader.frag +``` + +## Test Build +```sh +cmake -B build -DBUILD_TESTS=ON -DDEBUG=ON +./build/tests/vsdf_tests +./build/tests/filewatcher/filewatcher_tests +``` + +## Nix Develop Shell +```sh +nix develop +``` + +### Credits: +- https://shadertoy.com +- https://iquilezles.org/ +- https://www.youtube.com/playlist?list=PL0JVLUVCkk-l7CWCn3-cdftR0oajugYvd (zeux) + +### CPP Guidelines +(I should try follow this but haven't gotten through it all yet) +https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..f0ad25e --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1701282334, + "narHash": "sha256-MxCVrXY6v4QmfTwIysjjaX0XUhqBbxTWWB4HXtDYsdk=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "057f9aecfb71c4437d2b27d3323df7f93c010b7e", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "23.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "utils": "utils" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..cd5ebb8 --- /dev/null +++ b/flake.nix @@ -0,0 +1,68 @@ +# Based on +# https://github.com/nixvital/nix-based-cpp-starterkit/tree/main +{ + description = "vsdf"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/23.11"; + utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, ... }@inputs: inputs.utils.lib.eachSystem [ + "x86_64-linux" + "i686-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ] + (system: + let + pkgs = import nixpkgs { + inherit system; + + # Add overlays here if you need to override the nixpkgs + # official packages. + overlays = [ ]; + + # Uncomment this if you need unfree software (e.g. cuda) for + # your project. + # + # config.allowUnfree = true; + }; + in + { + devShells.default = pkgs.mkShell rec { + name = "vsdf"; + + packages = with pkgs; [ + # Development Tools + llvmPackages_14.clang + cmake + cmakeCurses + + # Development time dependencies + gtest + + # Dependencies + glfw + glm + spdlog + boost + vulkan-loader + vulkan-headers + ]; + + # Setting up the environment variables you need during + # development. + shellHook = + let + icon = "f121"; + in + '' + export PS1="$(echo -e '\u${icon}') {\[$(tput sgr0)\]\[\033[38;5;228m\]\w\[$(tput sgr0)\]\[\033[38;5;15m\]} (${name}) \\$ \[$(tput sgr0)\]" + ''; + }; + + packages.default = pkgs.callPackage ./default.nix { }; + }); +} diff --git a/include/fileutils.h b/include/fileutils.h new file mode 100644 index 0000000..87c251d --- /dev/null +++ b/include/fileutils.h @@ -0,0 +1,32 @@ +#ifndef FILEUTILS_H +#define FILEUTILS_H + +#include +#include +#include +#include +#include + +[[nodiscard]] static std::vector +loadBinaryFile(const std::string &filename) { + std::ifstream file(filename, std::ios::ate | std::ios::binary); + + if (!file.is_open()) { + throw std::runtime_error("Failed to open file: " + filename); + } + + size_t fileSize = static_cast(file.tellg()); + std::vector buffer(fileSize); + + file.seekg(0); + file.read(buffer.data(), fileSize); + + if (file.gcount() != fileSize) { + throw std::runtime_error("Failed to read the complete file: " + + filename); + } + + return buffer; +}; + +#endif // FILEUTILS_H diff --git a/include/filewatcher/filewatcher.h b/include/filewatcher/filewatcher.h new file mode 100644 index 0000000..5346b83 --- /dev/null +++ b/include/filewatcher/filewatcher.h @@ -0,0 +1,34 @@ +#ifndef FILE_WATCHER_H +#define FILE_WATCHER_H + +#include +#include + +// Abstract class for file watching +class FileWatcher { + public: + FileWatcher() = default; + using FileChangeCallback = std::function; + + virtual ~FileWatcher() = default; + + // Delete copy constructor and copy assignment operator + FileWatcher(const FileWatcher &) = delete; + FileWatcher &operator=(const FileWatcher &) = delete; + + // Delete move constructor and move assignment operator + FileWatcher(FileWatcher &&) = delete; + FileWatcher &operator=(FileWatcher &&) = delete; + + // Starts a thread to watch the file + virtual void startWatching(const std::string &filepath, + FileChangeCallback callback) = 0; + + // Allows the thread watching the file to instantly stop + virtual void stopWatching() = 0; + + protected: + FileChangeCallback callback; +}; + +#endif // FILE_WATCHER_H diff --git a/include/filewatcher/filewatcher_factory.h b/include/filewatcher/filewatcher_factory.h new file mode 100644 index 0000000..a7f33af --- /dev/null +++ b/include/filewatcher/filewatcher_factory.h @@ -0,0 +1,23 @@ +#ifndef FILEWATCHERFACTORY_H +#define FILEWATCHERFACTORY_H + +#ifdef __APPLE__ +#include "mac_filewatcher.h" +#elif __linux__ +#include "linux_filewatcher.h" +#else +#error "Unsupported platform." +#endif + +namespace filewatcher_factory { +static std::unique_ptr createFileWatcher() { +#ifdef __APPLE__ + return std::make_unique(); +#elif __linux__ + return std::make_unique(); +#else + return nullptr; // Handle unsupported platforms gracefully +#endif +} +} // namespace filewatcher_factory +#endif // FILEWATCHERFACTORY_H diff --git a/include/filewatcher/inotify_utils.h b/include/filewatcher/inotify_utils.h new file mode 100644 index 0000000..34b6942 --- /dev/null +++ b/include/filewatcher/inotify_utils.h @@ -0,0 +1,40 @@ +#ifndef INOTIFY_UTILS_H +#define INOTIFY_UTILS_H +#include +#include + +namespace inotify_utils { +static void logInotifyEvent(inotify_event *event) noexcept { + spdlog::debug("Event name {}", event->name); + spdlog::debug("Event mask {}", event->mask); + spdlog::debug("Event cookie {}", event->cookie); + spdlog::debug("Event len {}", event->len); + + // Print mask information + spdlog::debug("Modify: {} ", (event->mask & IN_MODIFY) ? "yes" : "no"); + spdlog::debug("Access: {} ", (event->mask & IN_ACCESS) ? "yes" : "no"); + spdlog::debug("Attrib: {} ", (event->mask & IN_ATTRIB) ? "yes" : "no"); + spdlog::debug("Close write: {} ", + (event->mask & IN_CLOSE_WRITE) ? "yes" : "no"); + spdlog::debug("Close nowrite: {} ", + (event->mask & IN_CLOSE_NOWRITE) ? "yes" : "no"); + spdlog::debug("Open: {} ", (event->mask & IN_OPEN) ? "yes" : "no"); + spdlog::debug("Moved from: {} ", + (event->mask & IN_MOVED_FROM) ? "yes" : "no"); + spdlog::debug("Moved to: {} ", (event->mask & IN_MOVED_TO) ? "yes" : "no"); + spdlog::debug("Create: {} ", (event->mask & IN_CREATE) ? "yes" : "no"); + spdlog::debug("Delete: {} ", (event->mask & IN_DELETE) ? "yes" : "no"); + spdlog::debug("Delete self: {} ", + (event->mask & IN_DELETE_SELF) ? "yes" : "no"); + spdlog::debug("Move self: {} ", + (event->mask & IN_MOVE_SELF) ? "yes" : "no"); + spdlog::debug("Is dir: {} ", (event->mask & IN_ISDIR) ? "yes" : "no"); + spdlog::debug("Unmount: {} ", (event->mask & IN_UNMOUNT) ? "yes" : "no"); + spdlog::debug("Q overflow: {} ", + (event->mask & IN_Q_OVERFLOW) ? "yes" : "no"); + spdlog::debug("Ignored: {} ", (event->mask & IN_IGNORED) ? "yes" : "no"); + spdlog::debug("In close write: {} ", + (event->mask & IN_CLOSE_WRITE) ? "yes" : "no"); +} +} // namespace inotify_utils +#endif diff --git a/include/filewatcher/linux_filewatcher.h b/include/filewatcher/linux_filewatcher.h new file mode 100644 index 0000000..8c24e50 --- /dev/null +++ b/include/filewatcher/linux_filewatcher.h @@ -0,0 +1,23 @@ +#ifndef LINUX_FILEWATCHER_H +#define LINUX_FILEWATCHER_H +#include "filewatcher.h" +#include +#include + +class LinuxFileWatcher : public FileWatcher { + public: + ~LinuxFileWatcher() { stopWatching(); } + + void startWatching(const std::string &filepath, + FileChangeCallback callback) override; + void stopWatching() override; + + private: + void watchFile(); + std::thread watcherThread; + std::string filename; // Relative filname we watch + int fd = -1; // File descriptor + int wd = -1; // Watch descriptor + bool running = true; +}; +#endif diff --git a/include/filewatcher/mac_filewatcher.h b/include/filewatcher/mac_filewatcher.h new file mode 100644 index 0000000..6d2def1 --- /dev/null +++ b/include/filewatcher/mac_filewatcher.h @@ -0,0 +1,37 @@ +#ifndef MAC_FILE_WATCHER_H +#define MAC_FILE_WATCHER_H +#include "filewatcher/filewatcher.h" +#include +#include +#include +#include +#include +#include + +class MacFileWatcher : public FileWatcher { + public: + // Using the callback type from the base class + using FileWatcher::FileChangeCallback; + + ~MacFileWatcher() { stopWatching(); } + void startWatching(const std::string &filepath, + FileChangeCallback callback) override; + + void stopWatching() override; + + private: + // Callback function for FSEvent + static void fsEventsCallback(ConstFSEventStreamRef streamRef, + void *clientCallBackInfo, size_t numEvents, + void *eventPaths, + const FSEventStreamEventFlags eventFlags[], + const FSEventStreamEventId eventIds[]); + + std::string filename; // absolute filename to watch + std::thread watchThread; + std::atomic running; + std::mutex mtx; + std::condition_variable cv; +}; + +#endif // MAC_FILE_WATCHER_H diff --git a/include/glfwutils.h b/include/glfwutils.h new file mode 100644 index 0000000..da51232 --- /dev/null +++ b/include/glfwutils.h @@ -0,0 +1,53 @@ +#ifndef GLFWUTILS_H +#define GLFWUTILS_H + +#define GLFW_INCLUDE_VULKAN +#include +#include +#include + +/** + * Utility namespace for common GLFW operations, particularly focused + * on Vulkan applications. Includes initialization and window creation + * functionalities. + */ +namespace glfwutils { + +/** + * Initializes GLFW library. Throws runtime_error if initialization fails. + * Ensures GLFW is only initialized once. + */ +static void initGLFW() { + static bool isInitialized = false; + if (!isInitialized) { + if (!glfwInit()) { + throw std::runtime_error("Failed to initialize GLFW"); + } + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); // No OpenGL context + isInitialized = true; + } +} + +/** + * Creates a GLFW window for Vulkan rendering. + * + * @param width The width of the window. + * @param height The height of the window. + * @param title The title of the window. + * @return A pointer to the created GLFWwindow. + * @throws std::runtime_error if window creation fails. + */ +static GLFWwindow *createGLFWwindow(int width = 800, int height = 600, + const std::string &title = "Vulkan") { + GLFWwindow *window = + glfwCreateWindow(width, height, title.c_str(), nullptr, nullptr); + if (!window) { + glfwTerminate(); + throw std::runtime_error("Failed to create GLFW window"); + } + return window; +} + +} // namespace glfwutils + +#endif // GLFWUTILS_H diff --git a/include/sdf_renderer.h b/include/sdf_renderer.h new file mode 100644 index 0000000..6574e9f --- /dev/null +++ b/include/sdf_renderer.h @@ -0,0 +1,80 @@ +#ifndef SDF_RENDERER_H +#define SDF_RENDERER_H +#include "vkutils.h" +#include + +inline constexpr uint WINDOW_WIDTH = 800; +inline constexpr uint WINDOW_HEIGHT = 600; +inline constexpr char WINDOW_TITLE[] = "Vulkan"; +inline constexpr char FULL_SCREEN_QUAD_VERT_SHADER_PATH[] = + "shaders/fullscreenquad.vert"; + +struct GLFWApplication { + bool framebufferResized = false; +}; + +class SDFRenderer { + private: + // GLFW Setup + GLFWApplication app; + GLFWwindow *window; + + // Vulkan Setup + VkInstance instance; + VkPhysicalDevice physicalDevice; + VkPhysicalDeviceProperties deviceProperties; + VkSurfaceKHR surface; + uint32_t graphicsQueueIndex; + VkDevice logicalDevice; + VkQueue queue; + VkQueryPool queryPool = VK_NULL_HANDLE; + VkSurfaceFormatKHR swapchainFormat; + VkCommandPool commandPool; + VkPushConstantRange pushConstantRange; + vkutils::Semaphores imageAvailableSemaphores; + vkutils::Semaphores renderFinishedSemaphores; + vkutils::Fences fences; + + // Shader Modules. + // Full screen quad vert shader + frag shader + VkShaderModule vertShaderModule; + VkShaderModule fragShaderModule; + std::string fragShaderPath; + bool useToyTemplate; + + // Render Context + VkSwapchainKHR swapchain = VK_NULL_HANDLE; + VkSurfaceCapabilitiesKHR surfaceCapabilities; + VkExtent2D swapchainSize; + vkutils::SwapchainImages swapchainImages; + vkutils::SwapchainImageViews swapchainImageViews; + VkRenderPass renderPass; + vkutils::FrameBuffers frameBuffers; + VkPipelineLayout pipelineLayout; + VkPipeline pipeline; + vkutils::CommandBuffers commandBuffers; + + // Timing + std::chrono::time_point cpuStartFrame, + cpuEndFrame; + + void glfwSetup(); + void vulkanSetup(); + void setupRenderContext(); + void createCommandBuffers(); + void createPipeline(); + void calcTimestamps(uint32_t imageIndex); + void destroyRenderContext(); + void destroyPipeline(); + void destroy(); + + [[nodiscard]] vkutils::PushConstants + getPushConstants(uint32_t currentFrame) noexcept; + + public: + SDFRenderer(const std::string &fragShaderPath, bool useToyTemplate = false); + void setup(); + void gameLoop(); +}; + +#endif // SDF_RENDERER_H diff --git a/include/shader_utils.h b/include/shader_utils.h new file mode 100644 index 0000000..8013efe --- /dev/null +++ b/include/shader_utils.h @@ -0,0 +1,13 @@ +#ifndef SHADER_UTILS_H +#define SHADER_UTILS_H +#include +#include + +namespace shader_utils { +// Take a shader file eg. planet.frag +// and produce planet.spv +std::filesystem::path compile(const std::string &shaderFilename, + bool useToyTemplate = false); +} // namespace shader_utils + +#endif // SHADER_UTILS_H diff --git a/include/vkutils.h b/include/vkutils.h new file mode 100644 index 0000000..026abb4 --- /dev/null +++ b/include/vkutils.h @@ -0,0 +1,994 @@ +#ifndef VKUTILS_H +#define VKUTILS_H +#include +#include +#include +#include +#include +#include +#define GLFW_INCLUDE_VULKAN +#include "fileutils.h" +#include +#include +#include +#include + +#define VK_CHECK(x) \ + do { \ + VkResult err = x; \ + if (err) \ + throw std::logic_error("Got a runtime_error"); \ + } while (0); +#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0])) + +inline constexpr size_t MAX_SWAPCHAIN_IMAGES = 10; + +namespace vkutils { +struct PushConstants { + float iTime; + uint iFrame; + glm::vec2 iResolution; + glm::vec2 iMouse; +}; + +/* Helper structs so that we can pass around swapchain images + * or image views on the stack without having to go to the heap + * unnesicarily. I want to sometimes avoid vector + */ +struct SwapchainImages { + VkImage images[MAX_SWAPCHAIN_IMAGES]; + uint32_t count; +}; + +struct SwapchainImageViews { + VkImageView imageViews[MAX_SWAPCHAIN_IMAGES]; + uint32_t count; +}; + +struct CommandBuffers { + VkCommandBuffer commandBuffers[MAX_SWAPCHAIN_IMAGES]; + uint32_t count; +}; + +struct Fences { + VkFence fences[MAX_SWAPCHAIN_IMAGES]; + uint32_t count; +}; + +struct Semaphores { + VkSemaphore semaphores[MAX_SWAPCHAIN_IMAGES]; + uint32_t count; +}; + +struct FrameBuffers { + VkFramebuffer framebuffers[MAX_SWAPCHAIN_IMAGES]; + uint32_t count; +}; + +[[nodiscard]] static VkInstance setupVulkanInstance() { + const VkApplicationInfo appInfo = { + .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, + .pNext = nullptr, + .pApplicationName = "Emerald", + .applicationVersion = VK_MAKE_VERSION(1, 0, 0), + .pEngineName = "Emerald Engine", + .engineVersion = VK_MAKE_VERSION(1, 0, 0), + .apiVersion = VK_API_VERSION_1_2, + }; + spdlog::info("Size of push constants {}", sizeof(PushConstants)); + + uint32_t extensionCount = 0; + const char **glfwExtensions = + glfwGetRequiredInstanceExtensions(&extensionCount); + + // Use vector for convenience here, this is only run once at startup + std::vector extensions(glfwExtensions, + glfwExtensions + extensionCount); + +#ifdef __APPLE__ + extensions.push_back("VK_KHR_portability_enumeration"); +#endif + + spdlog::debug("Using the following extensions: "); + for (const auto &extension : extensions) { + spdlog::debug("- {}", extension); + } + + spdlog::debug("Creating vk instance..."); + + static const char *validationLayers[] = { + "VK_LAYER_KHRONOS_validation", + // "VK_LAYER_LUNARG_api_dump", + // "VK_LAYER_LUNARG_parameter_validation", + // "VK_LAYER_LUNARG_screenshot", + // "VK_LAYER_LUNARG_core_validation", + // "VK_LAYER_LUNARG_device_limits", + // "VK_LAYER_LUNARG_object_tracker", + }; + + static const VkInstanceCreateInfo createInfo = { + .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, + .pNext = nullptr, +#ifdef __APPLE__ + .flags = VkInstanceCreateFlagBits:: + VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR, +#else + .flags = 0, +#endif + .pApplicationInfo = &appInfo, + .enabledLayerCount = ARRAY_SIZE(validationLayers), + .ppEnabledLayerNames = validationLayers, + .enabledExtensionCount = static_cast(extensions.size()), + .ppEnabledExtensionNames = extensions.data(), + }; + + VkInstance instance; + VK_CHECK(vkCreateInstance(&createInfo, nullptr, &instance)); + return instance; +} + +[[nodiscard]] static VkPhysicalDeviceProperties +getDeviceProperties(VkPhysicalDevice physicalDevice) { + VkPhysicalDeviceProperties deviceProperties; + vkGetPhysicalDeviceProperties(physicalDevice, &deviceProperties); + return deviceProperties; +} + +[[nodiscard]] static VkPhysicalDevice findGPU(VkInstance instance) { + uint32_t deviceCount = 0; + spdlog::debug("Enumerating devices..."); + VK_CHECK(vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr)); + + if (deviceCount == 0) { + spdlog::error("No devices found!"); + throw std::runtime_error("No devices found!"); + } + + spdlog::info("Found {} devices", deviceCount); + std::vector devices(deviceCount); + VK_CHECK( + vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data())); + + for (uint32_t i = 0; i < deviceCount; i++) { + VkPhysicalDeviceProperties deviceProperties; + vkGetPhysicalDeviceProperties(devices[i], &deviceProperties); + spdlog::debug("Device {} has Vulkan version {}", i, + deviceProperties.apiVersion); + spdlog::debug("Device {} has driver version {}", i, + deviceProperties.driverVersion); + spdlog::debug("Device {} has vendor ID {}", i, + deviceProperties.vendorID); + spdlog::debug("Device {} has device ID {}", i, + deviceProperties.deviceID); + spdlog::debug("Device {} has device type {}", i, + static_cast(deviceProperties.deviceType)); + spdlog::debug("Device {} has device name {}", i, + deviceProperties.deviceName); + + // Example of selecting a discrete GPU + if (deviceProperties.deviceType == + VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { + spdlog::info("Selecting discrete GPU: {}", + deviceProperties.deviceName); + return devices[i]; + } + } + // If no discrete GPU is found, fallback to the first device + spdlog::debug("No discrete GPU found. Fallback to the first device."); + return devices[0]; +} + +[[nodiscard]] static VkSurfaceKHR createVulkanSurface(VkInstance instance, + GLFWwindow *window) { + spdlog::debug("Creating Vulkan surface..."); + VkSurfaceKHR surface; + VkResult result = + glfwCreateWindowSurface(instance, window, nullptr, &surface); + if (result != VK_SUCCESS) { + spdlog::error("Failed to create Vulkan surface"); + spdlog::error("Result: 0x{:x}", static_cast(result)); + vkDestroyInstance(instance, nullptr); + glfwTerminate(); + throw std::runtime_error("Failed to create Vulkan surface"); + } + spdlog::debug("Created vulkan surface"); + return surface; +} + +[[nodiscard]] static uint32_t +getVulkanGraphicsQueueIndex(VkPhysicalDevice physicalDevice, + VkSurfaceKHR surface) { + uint32_t queueFamilyCount; + vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, + nullptr); + + if (queueFamilyCount == 0) + throw std::runtime_error("No queue families found"); + + spdlog::debug("Found {} queue families", queueFamilyCount); + + std::vector queueFamilies(queueFamilyCount); + vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, + queueFamilies.data()); + + // Print debug info for all queue families + for (uint32_t i = 0; i < queueFamilyCount; i++) { + spdlog::debug("Queue family {} has {} queues", i, + queueFamilies[i].queueCount); + spdlog::debug("Queue family {} supports graphics: {} ", i, + queueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT); + spdlog::debug("Queue family {} supports compute: {} ", i, + queueFamilies[i].queueFlags & VK_QUEUE_COMPUTE_BIT); + spdlog::debug("Queue family {} supports transfer: {} ", i, + queueFamilies[i].queueFlags & VK_QUEUE_TRANSFER_BIT); + spdlog::debug("Queue family {} supports sparse binding: {} ", i, + queueFamilies[i].queueFlags & + VK_QUEUE_SPARSE_BINDING_BIT); + spdlog::debug("Queue family {} supports protected: {} ", i, + queueFamilies[i].queueFlags & VK_QUEUE_PROTECTED_BIT); + + VkBool32 supportsPresent; + vkGetPhysicalDeviceSurfaceSupportKHR(physicalDevice, i, surface, + &supportsPresent); + spdlog::debug("Queue family {} supports present: {} ", i, + supportsPresent); + + if (queueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT && + supportsPresent) + return i; + } + + throw std::runtime_error("Failed to find graphics queue"); +} + +[[nodiscard]] static VkDevice +createVulkanLogicalDevice(VkPhysicalDevice physicalDevice, + uint32_t graphicsQueueIndex) { + float queuePriority = 1.0f; + + spdlog::debug("Create a queue..."); + VkDeviceQueueCreateInfo queueInfo = { + .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, + .queueFamilyIndex = graphicsQueueIndex, + .queueCount = 1, + .pQueuePriorities = &queuePriority, + }; + + static const char *requiredExtensions[] = { + "VK_KHR_swapchain", +#ifdef __APPLE__ + "VK_KHR_portability_subset", +#endif + }; + + spdlog::debug("Create a logical device..."); + VkDevice device; + + VkPhysicalDeviceDynamicRenderingFeaturesKHR dynamicRenderingFeatures{ + .sType = + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES_KHR, + .dynamicRendering = VK_TRUE, + }; + + VkDeviceCreateInfo deviceCreateInfo = { + .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, + .pNext = &dynamicRenderingFeatures, + .queueCreateInfoCount = 1, + .pQueueCreateInfos = &queueInfo, + .enabledExtensionCount = ARRAY_SIZE(requiredExtensions), + .ppEnabledExtensionNames = requiredExtensions, + }; + + VK_CHECK( + vkCreateDevice(physicalDevice, &deviceCreateInfo, nullptr, &device)); + spdlog::debug("Created logical device"); + + return device; +} + +[[nodiscard]] static VkSurfaceCapabilitiesKHR +getSurfaceCapabilities(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface) { + // Surface Capabilities + spdlog::debug("Get surface capabilities"); + VkSurfaceCapabilitiesKHR surfaceCapabilities; + VK_CHECK(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface, + &surfaceCapabilities)); + return surfaceCapabilities; +} + +[[nodiscard]] consteval auto getPreferredFormats() { + return std::to_array({ + VK_FORMAT_R8G8B8_SRGB, + VK_FORMAT_R8G8B8_UNORM, + }); +} + +[[nodiscard]] static VkSurfaceFormatKHR +selectSwapchainFormat(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface) { + // Get surface formats + uint32_t surfaceFormatCount; + VK_CHECK(vkGetPhysicalDeviceSurfaceFormatsKHR( + physicalDevice, surface, &surfaceFormatCount, nullptr)); + spdlog::debug("Surface format count: {}", surfaceFormatCount); + + if (surfaceFormatCount == 0) { + throw std::runtime_error("Failed to find any surface formats."); + } + + // NOTE: Would like to move to array but not sure of a good max + std::vector surfaceFormats(surfaceFormatCount); + VK_CHECK(vkGetPhysicalDeviceSurfaceFormatsKHR( + physicalDevice, surface, &surfaceFormatCount, surfaceFormats.data())); + + // Handle the special case where the surface format is undefined. + if (surfaceFormatCount == 1 && + surfaceFormats[0].format == VK_FORMAT_UNDEFINED) { + spdlog::info("Surface format is undefined, selecting " + "VK_FORMAT_R8G8B8A8_SRGB as default."); + return {VK_FORMAT_R8G8B8A8_SRGB, surfaceFormats[0].colorSpace}; + } + + static constexpr auto preferredFormatArray = getPreferredFormats(); + for (const auto &candidate : surfaceFormats) { + if (std::find(preferredFormatArray.begin(), preferredFormatArray.end(), + candidate.format) != preferredFormatArray.end()) { + return candidate; + } + } + + // Fallback: if no preferred formats are found, use first available format. + spdlog::debug( + "No preferred format found, using the first available format."); + return surfaceFormats[0]; +} + +[[nodiscard]] static VkExtent2D +getSwapchainSize(GLFWwindow *window, + const VkSurfaceCapabilitiesKHR &surfaceCapabilities) { + VkExtent2D swapchainSize; + if (surfaceCapabilities.currentExtent.width == 0xFFFFFFFF) { + int width, height; + glfwGetFramebufferSize(window, &width, &height); + swapchainSize = {static_cast(width), + static_cast(height)}; + } else { + swapchainSize = surfaceCapabilities.currentExtent; + } + + spdlog::debug("Swapchain size: {}x{}", swapchainSize.width, + swapchainSize.height); + return swapchainSize; +} + +[[nodiscard]] static VkSwapchainKHR +createSwapchain(VkPhysicalDevice physicalDevice, VkDevice device, + VkSurfaceKHR surface, + const VkSurfaceCapabilitiesKHR &surfaceCapabilities, + VkExtent2D swapchainSize, VkSurfaceFormatKHR surfaceFormat, + GLFWwindow *window, VkSwapchainKHR oldSwapchain) { + // Determine the number of VkImage's to use in the swapchain. + // Ideally, we desire to own 1 image at a time, the rest of the images can + // either be rendered to and/or being queued up for display. + uint32_t desiredSwapchainImages = surfaceCapabilities.minImageCount + 1; + if ((surfaceCapabilities.maxImageCount > 0) && + (desiredSwapchainImages > surfaceCapabilities.maxImageCount)) { + // Application must settle for fewer images than desired. + desiredSwapchainImages = surfaceCapabilities.maxImageCount; + } + spdlog::debug("Desired swapchain images: {}", desiredSwapchainImages); + + // Just set identity bit transform + VkSurfaceTransformFlagBitsKHR preTransform = + VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; + + // Query the list of supported present modes + uint32_t presentModeCount; + vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface, + &presentModeCount, nullptr); + + static constexpr uint32_t presentModeCountMax = 30; // more than enough + std::array presentModes; + vkGetPhysicalDeviceSurfacePresentModesKHR( + physicalDevice, surface, &presentModeCount, presentModes.data()); + + VkPresentModeKHR swapchainPresentMode = + VK_PRESENT_MODE_FIFO_KHR; // Default mode + for (const auto &mode : presentModes) { + if (mode == VK_PRESENT_MODE_MAILBOX_KHR) { + swapchainPresentMode = VK_PRESENT_MODE_MAILBOX_KHR; + break; // Highest priority + } else if (mode == VK_PRESENT_MODE_IMMEDIATE_KHR) { + swapchainPresentMode = VK_PRESENT_MODE_IMMEDIATE_KHR; + // Don't break to keep checking for MAILBOX + } + } + + VkCompositeAlphaFlagBitsKHR composite = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + if (surfaceCapabilities.supportedCompositeAlpha & + VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR) { + composite = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + } else if (surfaceCapabilities.supportedCompositeAlpha & + VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR) { + composite = VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR; + } else if (surfaceCapabilities.supportedCompositeAlpha & + VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR) { + composite = VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR; + } else if (surfaceCapabilities.supportedCompositeAlpha & + VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR) { + composite = VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR; + } + + spdlog::debug("Composite alpha: {}", static_cast(composite)); + + spdlog::debug("Selected surface format"); + spdlog::info("Surface format: {}", static_cast(surfaceFormat.format)); + spdlog::info("Color space: {}", static_cast(surfaceFormat.colorSpace)); + + // Create a swapchain + spdlog::debug("Create a swapchain"); + VkSwapchainCreateInfoKHR swapchainCreateInfo{ + .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, + .pNext = nullptr, + .surface = surface, + .minImageCount = desiredSwapchainImages, + .imageFormat = surfaceFormat.format, + .imageColorSpace = surfaceFormat.colorSpace, + .imageExtent = + { + .width = swapchainSize.width, + .height = swapchainSize.height, + }, + .imageArrayLayers = 1, + .imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, + .imageSharingMode = VK_SHARING_MODE_EXCLUSIVE, + .preTransform = preTransform, + .compositeAlpha = composite, + .presentMode = swapchainPresentMode, + .clipped = VK_TRUE, + .oldSwapchain = oldSwapchain, + }; + + VkSwapchainKHR swapchain; + VK_CHECK(vkCreateSwapchainKHR(device, &swapchainCreateInfo, nullptr, + &swapchain)); + + return swapchain; +} + +[[nodiscard]] static SwapchainImages +getSwapchainImages(VkDevice device, VkSwapchainKHR swapchain) { + SwapchainImages swapchainImages; + uint32_t swapchainImageCount; + VK_CHECK(vkGetSwapchainImagesKHR(device, swapchain, &swapchainImageCount, + nullptr)); + spdlog::debug("Swapchain image count: {}", swapchainImageCount); + swapchainImages.count = swapchainImageCount; + + if (swapchainImageCount > MAX_SWAPCHAIN_IMAGES) { + throw std::runtime_error(fmt::format("Swapchain image count {} exceeds " + "maximum images {}", + swapchainImageCount, + MAX_SWAPCHAIN_IMAGES)); + } + + VK_CHECK(vkGetSwapchainImagesKHR(device, swapchain, &swapchainImageCount, + swapchainImages.images)); + for (uint32_t i = 0; i < swapchainImageCount; i++) { + spdlog::debug("Swapchain image {}", i); + } + return swapchainImages; +} + +[[nodiscard]] static SwapchainImageViews +createSwapchainImageViews(VkDevice device, VkSurfaceFormatKHR surfaceFormat, + const SwapchainImages &swapchainImages) { + // Create image views + SwapchainImageViews swapchainImageViews; + swapchainImageViews.count = swapchainImages.count; + for (uint32_t i = 0; i < swapchainImages.count; i++) { + VkImageViewCreateInfo imageViewCreateInfo{ + .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + .flags = 0, + .image = swapchainImages.images[i], + .viewType = VK_IMAGE_VIEW_TYPE_2D, + .format = surfaceFormat.format, + .components = + { + .r = VK_COMPONENT_SWIZZLE_IDENTITY, + .g = VK_COMPONENT_SWIZZLE_IDENTITY, + .b = VK_COMPONENT_SWIZZLE_IDENTITY, + .a = VK_COMPONENT_SWIZZLE_IDENTITY, + }, + .subresourceRange = + { + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }; + imageViewCreateInfo.pNext = nullptr; + + VK_CHECK(vkCreateImageView(device, &imageViewCreateInfo, nullptr, + &swapchainImageViews.imageViews[i])); + } + return swapchainImageViews; +} + +[[nodiscard]] static VkCommandPool +createCommandPool(VkDevice device, uint32_t graphicsQueueIndex) { + spdlog::debug("Create command pool"); + VkCommandPoolCreateInfo commandPoolCreateInfo{ + .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, + .flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, + .queueFamilyIndex = graphicsQueueIndex, + }; + VkCommandPool commandPool = VK_NULL_HANDLE; + VK_CHECK(vkCreateCommandPool(device, &commandPoolCreateInfo, nullptr, + &commandPool)); + return commandPool; +} + +[[nodiscard]] static VkDescriptorSetLayout +createDescriptorSetLayout(VkDevice device) { + VkDescriptorSetLayoutBinding layoutBinding{ + .binding = 0, + .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + .descriptorCount = 1, + .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT, + }; + + VkDescriptorSetLayoutCreateInfo layoutInfo{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, + .bindingCount = 1, + .pBindings = &layoutBinding, + }; + + VkDescriptorSetLayout descriptorSetLayout; + VK_CHECK(vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, + &descriptorSetLayout)); + return descriptorSetLayout; +} + +[[nodiscard]] static VkDescriptorPool createDescriptorPool(VkDevice &device) { + VkDescriptorPoolSize poolSize{}; + poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + poolSize.descriptorCount = 1; + + VkDescriptorPoolCreateInfo poolInfo{}; + poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + poolInfo.poolSizeCount = 1; + poolInfo.pPoolSizes = &poolSize; + poolInfo.maxSets = 1; + + VkDescriptorPool descriptorPool; + VK_CHECK( + vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool)); + return descriptorPool; +} + +[[nodiscard]] static VkDescriptorSet +allocateDescriptorSet(VkDescriptorPool &descriptorPool, VkDevice &device, + VkDescriptorSetLayout &descriptorSetLayout) { + VkDescriptorSetAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocInfo.descriptorPool = descriptorPool; + allocInfo.descriptorSetCount = 1; + allocInfo.pSetLayouts = &descriptorSetLayout; + + VkDescriptorSet descriptorSet; + VK_CHECK(vkAllocateDescriptorSets(device, &allocInfo, &descriptorSet)); + return descriptorSet; +} + +[[nodiscard]] static CommandBuffers +createCommandBuffers(VkDevice device, VkCommandPool commandPool, + uint32_t commandBufferCount) { + CommandBuffers commandBuffers; + commandBuffers.count = commandBufferCount; + spdlog::info("Create command buffers"); + VkCommandBufferAllocateInfo commandBufferAllocateInfo{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, + .pNext = nullptr, + .commandPool = commandPool, + .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, + .commandBufferCount = commandBufferCount, + }; + VK_CHECK(vkAllocateCommandBuffers(device, &commandBufferAllocateInfo, + commandBuffers.commandBuffers)); + return commandBuffers; +} + +[[nodiscard]] static Fences createFences(VkDevice device, uint32_t count) { + spdlog::info("Create fences"); + Fences fences; + fences.count = count; + for (uint32_t i = 0; i < count; i++) { + VkFenceCreateInfo fenceCreateInfo{ + .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, + .flags = VK_FENCE_CREATE_SIGNALED_BIT, + }; + VK_CHECK(vkCreateFence(device, &fenceCreateInfo, nullptr, + &fences.fences[i])); + } + return fences; +} + +[[nodiscard]] static VkSemaphore createSemaphore(VkDevice device) { + VkSemaphoreCreateInfo semaphoreCreateInfo{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, + }; + VkSemaphore semaphore; + VK_CHECK( + vkCreateSemaphore(device, &semaphoreCreateInfo, nullptr, &semaphore)); + return semaphore; +} + +[[nodiscard]] static Semaphores createSemaphores(VkDevice device, + uint32_t count) { + Semaphores semaphores; + semaphores.count = count; + for (uint32_t i = 0; i < count; i++) { + semaphores.semaphores[i] = createSemaphore(device); + } + return semaphores; +} + +[[nodiscard]] static VkRenderPass createRenderPass(VkDevice device, + VkFormat format) { + spdlog::debug("Create render pass"); + VkAttachmentDescription colorAttachment{ + .format = format, + .samples = VK_SAMPLE_COUNT_1_BIT, + .loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE, + .storeOp = VK_ATTACHMENT_STORE_OP_STORE, + .stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE, + .stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE, + .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + }; + + VkAttachmentReference colorAttachmentRef{ + .attachment = 0, + .layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + }; + + VkSubpassDescription subpass{ + .pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS, + .colorAttachmentCount = 1, + .pColorAttachments = &colorAttachmentRef, + }; + + VkRenderPassCreateInfo renderPassInfo{ + .sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO, + .attachmentCount = 1, + .pAttachments = &colorAttachment, + .subpassCount = 1, + .pSubpasses = &subpass, + }; + + VkRenderPass renderPass; + VK_CHECK(vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass)); + + return renderPass; +} + +[[nodiscard]] static FrameBuffers +createFrameBuffers(VkDevice device, VkRenderPass renderPass, VkExtent2D extent, + const SwapchainImageViews &swapchainImageViews) { + spdlog::info("Create framebuffers"); + FrameBuffers frameBuffers; + frameBuffers.count = swapchainImageViews.count; + for (uint32_t i = 0; i < swapchainImageViews.count; i++) { + VkFramebufferCreateInfo framebufferInfo{ + .sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO, + .renderPass = renderPass, + .attachmentCount = 1, + .pAttachments = &swapchainImageViews.imageViews[i], + .width = extent.width, + .height = extent.height, + .layers = 1, + }; + + VK_CHECK(vkCreateFramebuffer(device, &framebufferInfo, nullptr, + &frameBuffers.framebuffers[i])); + } + return frameBuffers; +} + +[[nodiscard]] static VkPipelineLayout createPipelineLayout(VkDevice device) { + spdlog::info("Create pipeline layout"); + VkPushConstantRange pushConstantRange{ + .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT, + .offset = 0, + .size = sizeof(vkutils::PushConstants), + }; + VkPipelineLayoutCreateInfo pipelineLayoutInfo{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, + .setLayoutCount = 0, + .pushConstantRangeCount = 1, + .pPushConstantRanges = &pushConstantRange, + }; + + VkPipelineLayout pipelineLayout; + VK_CHECK(vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, + &pipelineLayout)); + return pipelineLayout; +} + +[[nodiscard]] static VkShaderModule +createShaderModule(VkDevice device, const std::string &filename) { + spdlog::info("Create shader module"); + VkShaderModule shaderModule; + auto code = loadBinaryFile(filename); + VkShaderModuleCreateInfo createinfo{ + .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, + .codeSize = code.size(), + .pCode = reinterpret_cast(code.data()), + }; + + VK_CHECK(vkCreateShaderModule(device, &createinfo, nullptr, &shaderModule)); + return shaderModule; +} + +[[nodiscard]] static VkPipeline +createGraphicsPipeline(VkDevice device, VkRenderPass renderPass, + VkPipelineLayout pipelineLayout, VkExtent2D extent, + VkShaderModule vertShaderModule, + VkShaderModule fragShaderModule) { + spdlog::info("Create graphics pipeline"); + VkPipeline pipeline; + + VkPipelineShaderStageCreateInfo shaderStages[2] = { + // Note: This one would never change + { + .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .stage = VK_SHADER_STAGE_VERTEX_BIT, + .module = vertShaderModule, + .pName = "main", + }, + { + .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .stage = VK_SHADER_STAGE_FRAGMENT_BIT, + .module = fragShaderModule, + .pName = "main", + }, + }; + + VkPipelineVertexInputStateCreateInfo vertexInputInfo{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, + .vertexBindingDescriptionCount = 0, + .vertexAttributeDescriptionCount = 0, + }; + + VkPipelineInputAssemblyStateCreateInfo inputAssembly{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, + .topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, + .primitiveRestartEnable = VK_FALSE, + }; + + VkViewport viewport{ + .x = 0.0f, + .y = 0.0f, + .width = static_cast(extent.width), + .height = static_cast(extent.height), + .minDepth = 0.0f, + .maxDepth = 1.0f, + }; + + VkRect2D scissor{ + .offset = {0, 0}, + .extent = extent, + }; + + VkDynamicState dynamicStates[] = {VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR}; + VkPipelineDynamicStateCreateInfo dynamicStateInfo{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO, + .dynamicStateCount = 2, + .pDynamicStates = dynamicStates, + }; + + VkPipelineViewportStateCreateInfo viewportState{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO, + .viewportCount = 1, + .pViewports = &viewport, + .scissorCount = 1, + .pScissors = &scissor, + }; + + VkPipelineRasterizationStateCreateInfo rasterizer{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO, + .depthClampEnable = VK_FALSE, + .rasterizerDiscardEnable = VK_FALSE, + .polygonMode = VK_POLYGON_MODE_FILL, + .cullMode = VK_CULL_MODE_NONE, + .frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE, + .depthBiasEnable = VK_FALSE, + .lineWidth = 1.0f, // It doesn't matter really we are doing SDF + }; + + VkPipelineMultisampleStateCreateInfo multisampling{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, + .rasterizationSamples = VK_SAMPLE_COUNT_1_BIT, + .sampleShadingEnable = VK_FALSE, + }; + + VkPipelineColorBlendAttachmentState colorBlendAttachment{ + .blendEnable = VK_FALSE, + .colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | + VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT, + }; + + VkPipelineColorBlendStateCreateInfo colorBlending{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, + .logicOpEnable = VK_FALSE, + .logicOp = VK_LOGIC_OP_COPY, + .attachmentCount = 1, + .pAttachments = &colorBlendAttachment, + }; + + VkGraphicsPipelineCreateInfo pipelineInfo = { + .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizer, + .pMultisampleState = &multisampling, + .pColorBlendState = &colorBlending, + .pDynamicState = &dynamicStateInfo, + .layout = pipelineLayout, + .renderPass = renderPass, + .subpass = 0, + }; + VK_CHECK(vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, + nullptr, &pipeline)); + spdlog::info("Created graphics pipeline"); + return pipeline; +} + +[[nodiscard]] static VkQueryPool createQueryPool(VkDevice device, + uint32_t numSwapchainImages) { + // Query pool used for calculating frame processing duration + VkQueryPoolCreateInfo queryPooolCreateInfo{ + .sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO, + .queryType = VK_QUERY_TYPE_TIMESTAMP, + .queryCount = 2 * numSwapchainImages, // 2 per frame, start and end + }; + + VkQueryPool queryPool; + VK_CHECK( + vkCreateQueryPool(device, &queryPooolCreateInfo, nullptr, &queryPool)); + return queryPool; +} + +static void recordCommandBuffer(VkDevice device, VkCommandPool commandPool, + VkQueryPool queryPool, VkRenderPass renderPass, + VkExtent2D extent, VkPipeline pipeline, + VkPipelineLayout pipelineLayout, + VkCommandBuffer commandBuffer, + VkFramebuffer framebuffer, + const PushConstants &pushConstants, + uint32_t imageIndex) { + vkResetCommandBuffer(commandBuffer, 0); + VkCommandBufferBeginInfo beginInfo{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + .flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT, + }; + + VkRenderPassBeginInfo renderPassBeginInfo{ + .sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, + .renderPass = renderPass, + .framebuffer = framebuffer, + .renderArea = {{0, 0}, extent}, + .clearValueCount = 0, + }; + spdlog::debug("Record command buffer"); + VK_CHECK(vkBeginCommandBuffer(commandBuffer, &beginInfo)); + vkCmdResetQueryPool(commandBuffer, queryPool, imageIndex * 2, 2); + vkCmdWriteTimestamp(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + queryPool, imageIndex * 2); + vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, + VK_SUBPASS_CONTENTS_INLINE); + vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + + vkCmdPushConstants(commandBuffer, pipelineLayout, + VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(PushConstants), + &pushConstants); + VkRect2D scissor{ + .offset = {0, 0}, + .extent = {extent.width, extent.height}, + }; + + VkViewport viewport{ + .x = 0.0f, + .y = 0.0f, + .width = static_cast(extent.width), + .height = static_cast(extent.height), + .minDepth = 0.0f, + .maxDepth = 1.0f, + }; + + vkCmdSetViewport(commandBuffer, 0, 1, &viewport); + vkCmdSetScissor(commandBuffer, 0, 1, &scissor); + + vkCmdDraw(commandBuffer, 6, 1, 0, 0); + vkCmdWriteTimestamp(commandBuffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, + queryPool, imageIndex * 2 + 1); + vkCmdEndRenderPass(commandBuffer); + spdlog::debug("End command buffer"); + VK_CHECK(vkEndCommandBuffer(commandBuffer)); + spdlog::debug("Ended command buffer"); +} + +static void submitCommandBuffer(VkQueue queue, VkCommandBuffer commandBuffer, + VkSemaphore imageAvailableSemaphore, + VkSemaphore renderFinishedSemaphore, + VkFence fence) { + VkPipelineStageFlags waitStages[] = { + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT}; + VkSubmitInfo submitInfo{ + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, + .waitSemaphoreCount = 1, + .pWaitSemaphores = &imageAvailableSemaphore, + .pWaitDstStageMask = waitStages, + .commandBufferCount = 1, + .pCommandBuffers = &commandBuffer, + .signalSemaphoreCount = 1, + .pSignalSemaphores = &renderFinishedSemaphore, + }; + VK_CHECK(vkQueueSubmit(queue, 1, &submitInfo, fence)); +} // namespace vkutils + +static void presentImage(VkQueue queue, VkSwapchainKHR swapchain, + VkSemaphore renderFinishedSemaphore, + uint32_t imageIndex) { + VkPresentInfoKHR presentInfo{ + .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, + .waitSemaphoreCount = 1, + .pWaitSemaphores = &renderFinishedSemaphore, + .swapchainCount = 1, + .pSwapchains = &swapchain, + .pImageIndices = &imageIndex, + }; + VK_CHECK(vkQueuePresentKHR(queue, &presentInfo)); +} + +static void +destroySwapchainImageViews(VkDevice device, + SwapchainImageViews &swapchainImageViews) noexcept { + for (uint32_t i = 0; i < swapchainImageViews.count; ++i) { + vkDestroyImageView(device, swapchainImageViews.imageViews[i], nullptr); + swapchainImageViews.imageViews[i] = VK_NULL_HANDLE; + } +} + +static void destroyFrameBuffers(VkDevice device, + FrameBuffers &frameBuffers) noexcept { + for (uint32_t i = 0; i < frameBuffers.count; ++i) { + vkDestroyFramebuffer(device, frameBuffers.framebuffers[i], nullptr); + frameBuffers.framebuffers[i] = VK_NULL_HANDLE; + } +} + +static void destroyFences(VkDevice device, Fences &fences) noexcept { + for (uint32_t i = 0; i < fences.count; ++i) { + vkDestroyFence(device, fences.fences[i], nullptr); + fences.fences[i] = VK_NULL_HANDLE; + } +} + +static void destroySemaphores(VkDevice device, + Semaphores &semaphores) noexcept { + for (uint32_t i = 0; i < semaphores.count; ++i) { + vkDestroySemaphore(device, semaphores.semaphores[i], nullptr); + semaphores.semaphores[i] = VK_NULL_HANDLE; + } +} + +} // namespace vkutils + +#endif diff --git a/linux-tests.Dockerfile b/linux-tests.Dockerfile new file mode 100644 index 0000000..865d27e --- /dev/null +++ b/linux-tests.Dockerfile @@ -0,0 +1,29 @@ +# Test build and run tests on ubuntu +FROM ubuntu:latest + +WORKDIR /app + +# Install necessary packages +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + g++ \ + cmake \ + ninja-build \ + make \ + libgtest-dev \ + libspdlog-dev \ + libglfw3 libglfw3-dev \ + libvulkan-dev \ + glslang-tools \ + glslang-dev \ + libglm-dev + # && rm -rf /var/lib/apt/lists/* + +COPY . /app + +RUN mkdir -p build && \ + cmake -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTS=ON -B build -G Ninja . + +RUN cmake --build build + +CMD build/tests/vsdf_tests && build/tests/filewatcher/filewatcher_tests diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..bfd52f4 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,2 @@ +cmake -B build -G Ninja . +cmake --build build diff --git a/scripts/build_quiet.sh b/scripts/build_quiet.sh new file mode 100755 index 0000000..f484aae --- /dev/null +++ b/scripts/build_quiet.sh @@ -0,0 +1,2 @@ +cmake -B build -G Ninja . >/dev/null +cmake --build build -- --quiet diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..986dbf8 --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,4 @@ +# This doesn't seem to work +# ctest --test-dir build --rerun-failed --output-on-failure +./build/tests/vsdf_tests +./build/tests/filewatcher/filewatcher_tests diff --git a/shaders/fullscreenquad.vert b/shaders/fullscreenquad.vert new file mode 100644 index 0000000..631cdf1 --- /dev/null +++ b/shaders/fullscreenquad.vert @@ -0,0 +1,14 @@ +#version 450 + +layout(location = 0) out vec2 texCoord; + +const vec2 vertices[6] = vec2[]( + vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(1.0, 1.0), + vec2(-1.0, -1.0), vec2(1.0, 1.0), vec2(-1.0, 1.0) +); + +void main() { + uint index = gl_VertexIndex % 6; // Ensure the index wraps around if needed + gl_Position = vec4(vertices[index], 0.0, 1.0); + texCoord = vertices[index] * 0.5 + 0.5; +} diff --git a/shaders/toytemplate.frag b/shaders/toytemplate.frag new file mode 100644 index 0000000..151f632 --- /dev/null +++ b/shaders/toytemplate.frag @@ -0,0 +1,31 @@ +#version 450 + +// ALl setup needed to make most things work +// eg. for a shader toy shader. +// Not everything yet... + +layout (push_constant) uniform PushConstants { + float iTime; + int iFrame; + vec2 iResolution; + vec2 iMouse; +} pc; + +layout (location = 0) in vec2 TexCoord; +layout (location = 0) out vec4 color; + +#define iTime pc.iTime +#define iResolution pc.iResolution +#define iFrame pc.iFrame +#define iMouse pc.iMouse + +void mainImage(out vec4 fragColor, in vec2 fragCoord); +void main() { + // Call your existing mainImage function + vec4 fragColor; + // Convert from vulkan to glsl + mainImage(fragColor, vec2(gl_FragCoord.x, iResolution.y - gl_FragCoord.y)); + // Output color + color = fragColor; +} + diff --git a/shaders/vulktemplate.frag b/shaders/vulktemplate.frag new file mode 100644 index 0000000..2673723 --- /dev/null +++ b/shaders/vulktemplate.frag @@ -0,0 +1,21 @@ +#version 450 + +layout (push_constant) uniform PushConstants { + float iTime; + int iFrame; + vec2 iResolution; + vec2 iMouse; +} pc; + +layout (location = 0) in vec2 TexCoord; +layout (location = 0) out vec4 color; + +#define iTime pc.iTime +#define iResolution pc.iResolution +#define iFrame pc.iFrame +#define iMouse pc.iMouse + +void main() { + color = vec4(1.0, 0.0, 0.0, 1.0);; +} + diff --git a/src/filewatcher/linux_filewatcher.cpp b/src/filewatcher/linux_filewatcher.cpp new file mode 100644 index 0000000..61f630c --- /dev/null +++ b/src/filewatcher/linux_filewatcher.cpp @@ -0,0 +1,104 @@ +#include "filewatcher/linux_filewatcher.h" +#include "filewatcher/inotify_utils.h" +#include +#include +#include +#include +#include +#include +#include +#include // for close() + +// https://man7.org/linux/man-pages/man7/inotify.7.html +// inotify event has flexible array member: name +// so assume 16 byte name +#define EVENT_SIZE (sizeof(struct inotify_event)) +#define BUF_LEN (1024 * 4 * (EVENT_SIZE + 16)) + +using namespace std::chrono; + +void LinuxFileWatcher::watchFile() { + spdlog::info("I'm a thread 2"); + // Do this instead of getting min time because then we + // get a huge nevative difference result value + auto lastEventTime = + std::chrono::steady_clock::now() - std::chrono::seconds(100); + + while (running) { + char buffer[BUF_LEN]; + int length = read(fd, buffer, BUF_LEN); + if (length < 0) + throw std::runtime_error("Failed to read filebuffer " + + std::string(strerror(errno))); + + int i = 0; + + while (i < length) { + // Remember this will be events for whole dir + spdlog::debug("Read {} bytes from inotify", length); + inotify_event *event = (struct inotify_event *)buffer; + inotify_utils::logInotifyEvent(event); + if (filename == event->name) { + auto currentTime = steady_clock::now(); + auto elapsedTime = currentTime - lastEventTime; + lastEventTime = currentTime; + spdlog::debug("Elapsed time: {}", elapsedTime.count()); + if (elapsedTime < 50ms) + spdlog::debug("Skipping event as it may be double write"); + else { + spdlog::info("Tracked file change: {}", filename); + callback(); + } + } + i += EVENT_SIZE + event->len; + } + } + spdlog::info("I finished"); +} + +void LinuxFileWatcher::startWatching(const std::string &filepath, + FileChangeCallback callback) { + spdlog::info("Start watching"); + this->callback = callback; + + fd = inotify_init(); + if (fd < 0) { + throw std::runtime_error("Failed to initialize inotify " + + std::string(strerror(errno))); + } + // We watch the dirpath and then filter + // by filename under that in case file is + // created and recreated, or else we'd lose + // track of the inode + std::filesystem::path path(std::filesystem::absolute(filepath)); + std::string dirPath = path.parent_path(); + filename = path.filename(); + spdlog::info("Watching dirPath: {} for file path {}", dirPath, + path.string()); + + wd = inotify_add_watch(fd, dirPath.c_str(), IN_MODIFY); + if (wd == -1) { + close(fd); + throw std::runtime_error("Failed to initialize watch " + + std::string(strerror(errno))); + } + + watcherThread = std::thread{&LinuxFileWatcher::watchFile, this}; +} + +void LinuxFileWatcher::stopWatching() { + spdlog ::debug("Stop watching"); + running = false; + if (wd != -1) + inotify_rm_watch(fd, wd); + // Now it should receive 1 final event which is the + // inotify watch IN_IGNORED and then end because + // running is false + if (watcherThread.joinable()) { + watcherThread.join(); + spdlog::info("Watcher thread succesfully joined"); + } + if (fd != -1) + close(fd); + spdlog::debug("Finished: Stop watching"); +} diff --git a/src/filewatcher/mac_filewatcher.cpp b/src/filewatcher/mac_filewatcher.cpp new file mode 100644 index 0000000..5040f4f --- /dev/null +++ b/src/filewatcher/mac_filewatcher.cpp @@ -0,0 +1,95 @@ +#include "filewatcher/mac_filewatcher.h" +#include +#include +#include +#include + +// LATENCY +// ### function `FSEventStreamCreate` +// The number of seconds the service should wait after hearing about an event +// from the kernel before passing it along to the client via its callback. +// Specifying a larger value may result in more effective temporal coalescing, +// resulting in fewer callbacks and greater overall efficiency. +static constexpr float LATENCY = 0.0; + +void MacFileWatcher::fsEventsCallback( + ConstFSEventStreamRef streamRef, void *clientCallBackInfo, size_t numEvents, + void *eventPaths, const FSEventStreamEventFlags eventFlags[], + const FSEventStreamEventId eventIds[]) { + char **paths = static_cast(eventPaths); + MacFileWatcher *watcher = static_cast(clientCallBackInfo); + + // Loop through each event + for (size_t i = 0; i < numEvents; ++i) { + std::string filePath(paths[i]); + spdlog::debug("Checking change: {}", filePath); + spdlog::debug("Against target: {}", watcher->filename); + + // Check if the event is related to a file + if (kFSEventStreamEventFlagItemIsFile && + ((eventFlags[i] & kFSEventStreamEventFlagItemCreated || + eventFlags[i] & kFSEventStreamEventFlagItemModified) && + !(eventFlags[i] & kFSEventStreamEventFlagItemRemoved))) { + // Check if filename matches the target + if (filePath == watcher->filename) { + spdlog::info("File changed: {}", filePath); + watcher->callback(); + } + } + } + + spdlog::debug("File in watched dir changed"); +} +void MacFileWatcher::startWatching(const std::string &path, + FileChangeCallback callback) { + this->callback = callback; + running = true; + std::filesystem::path abspath(std::filesystem::absolute(path)); + // So /tmp -> /private/tmp and matchs with the fseventstream paths + std::filesystem::path canonicalPath = std::filesystem::canonical(abspath); + std::string dirPath = canonicalPath.parent_path(); + filename = canonicalPath.string(); + watchThread = std::thread([this, dirPath]() { + spdlog::info("Watching file: {}", dirPath); + CFStringRef pathToWatch = CFStringCreateWithCString( + NULL, dirPath.c_str(), kCFStringEncodingUTF8); + spdlog::info("Path to watch: {}", dirPath.c_str()); + CFArrayRef pathsToWatch = + CFArrayCreate(NULL, (const void **)&pathToWatch, 1, NULL); + spdlog::info("Setup FSEvent Stream"); + + FSEventStreamContext context = {0, NULL, NULL, NULL, NULL}; + context.info = this; + FSEventStreamRef stream = + FSEventStreamCreate(NULL, fsEventsCallback, &context, pathsToWatch, + kFSEventStreamEventIdSinceNow, LATENCY, + kFSEventStreamCreateFlagFileEvents); + spdlog::info("Create dispatch queue"); + + dispatch_queue_t queue = + dispatch_queue_create("com.example.filewatcherqueue", NULL); + FSEventStreamSetDispatchQueue(stream, queue); + FSEventStreamStart(stream); + + std::unique_lock lock(mtx); + cv.wait(lock, [this] { return !running; }); + + FSEventStreamStop(stream); + FSEventStreamInvalidate(stream); + FSEventStreamRelease(stream); + CFRelease(pathsToWatch); + CFRelease(pathToWatch); + dispatch_release(queue); + spdlog::info("Watcher thread finished"); + }); +} + +void MacFileWatcher::stopWatching() { + running = false; + cv.notify_one(); // Signal the condition variable to unblock the thread + + if (watchThread.joinable()) { + watchThread.join(); + spdlog::info("Watcher thread succesfully joined"); + } +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..d59564b --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,40 @@ +#include "sdf_renderer.h" +#include +#include + +int main(int argc, char **argv) { + bool useToyTemplate = false; + std::filesystem::path shaderFile; + + if (argc < 2) + throw std::runtime_error("No shader file provided."); + + std::string arg; + for (int i = 1; i < argc; ++i) { + arg = argv[i]; + if (arg == "--toy") { + useToyTemplate = true; + continue; + } else if (arg.substr(0, 2) != + "--") { // Assuming shader file is not preceded by "--" + shaderFile = arg; + break; + } + } + + if (!std::filesystem::exists(shaderFile)) + throw std::runtime_error("Shader file does not exist: " + + shaderFile.string()); + if (shaderFile.extension() != ".frag") + throw std::runtime_error("Shader file is not a .frag file: " + + shaderFile.string()); + + spdlog::set_level(spdlog::level::info); + spdlog::info("Setting things up..."); + spdlog::default_logger()->set_pattern("[%H:%M:%S] [%l] %v"); + + SDFRenderer renderer{shaderFile, useToyTemplate}; + renderer.setup(); + renderer.gameLoop(); + return 0; +} diff --git a/src/sdf_renderer.cpp b/src/sdf_renderer.cpp new file mode 100644 index 0000000..8e53245 --- /dev/null +++ b/src/sdf_renderer.cpp @@ -0,0 +1,236 @@ +#include "sdf_renderer.h" +#include "filewatcher/filewatcher_factory.h" +#include "glfwutils.h" +#include "shader_utils.h" +#include "vkutils.h" +#include +#include + +void framebufferResizeCallback(GLFWwindow *window, int width, + int height) noexcept { + auto app = + reinterpret_cast(glfwGetWindowUserPointer(window)); + app->framebufferResized = true; + spdlog::info("Framebuffer resized to {}x{}", width, height); +}; + +SDFRenderer::SDFRenderer(const std::string &fragShaderPath, bool useToyTemplate) + : fragShaderPath(fragShaderPath), useToyTemplate(useToyTemplate) {} + +void SDFRenderer::setup() { + glfwSetup(); + vulkanSetup(); + setupRenderContext(); + createPipeline(); + createCommandBuffers(); +} + +void SDFRenderer::glfwSetup() { + // GLFW Setup + glfwutils::initGLFW(); + window = + glfwutils::createGLFWwindow(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE); + glfwSetWindowUserPointer(window, &app); + glfwSetFramebufferSizeCallback(window, framebufferResizeCallback); +} + +void SDFRenderer::vulkanSetup() { + instance = vkutils::setupVulkanInstance(); + physicalDevice = vkutils::findGPU(instance); + deviceProperties = vkutils::getDeviceProperties(physicalDevice); + spdlog::info("Device limits {:.3f}", + deviceProperties.limits.timestampPeriod); + surface = vkutils::createVulkanSurface(instance, window); + graphicsQueueIndex = + vkutils::getVulkanGraphicsQueueIndex(physicalDevice, surface); + logicalDevice = + vkutils::createVulkanLogicalDevice(physicalDevice, graphicsQueueIndex); + queue = VK_NULL_HANDLE; + vkGetDeviceQueue(logicalDevice, graphicsQueueIndex, 0, &queue); + swapchainFormat = vkutils::selectSwapchainFormat(physicalDevice, surface); + renderPass = + vkutils::createRenderPass(logicalDevice, swapchainFormat.format); + commandPool = vkutils::createCommandPool(logicalDevice, graphicsQueueIndex); + // Since it's SDF, only need to set up full screen quad vert shader once + std::filesystem::path vertSpirvPath{ + shader_utils::compile(FULL_SCREEN_QUAD_VERT_SHADER_PATH)}; + vertShaderModule = + vkutils::createShaderModule(logicalDevice, vertSpirvPath); +} + +void SDFRenderer::setupRenderContext() { + spdlog::info("Setting up render context"); + surfaceCapabilities = + vkutils::getSurfaceCapabilities(physicalDevice, surface); + swapchainSize = vkutils::getSwapchainSize(window, surfaceCapabilities); + auto oldSwapchain = swapchain; + swapchain = vkutils::createSwapchain(physicalDevice, logicalDevice, surface, + surfaceCapabilities, swapchainSize, + swapchainFormat, window, oldSwapchain); + if (oldSwapchain != VK_NULL_HANDLE) + vkDestroySwapchainKHR(logicalDevice, oldSwapchain, nullptr); + swapchainImages = vkutils::getSwapchainImages(logicalDevice, swapchain); + if (queryPool == VK_NULL_HANDLE) + queryPool = + vkutils::createQueryPool(logicalDevice, swapchainImages.count); + if (imageAvailableSemaphores.count == 0) { + imageAvailableSemaphores = + vkutils::createSemaphores(logicalDevice, swapchainImages.count); + renderFinishedSemaphores = + vkutils::createSemaphores(logicalDevice, swapchainImages.count); + fences = vkutils::createFences(logicalDevice, swapchainImages.count); + } + swapchainImageViews = vkutils::createSwapchainImageViews( + logicalDevice, swapchainFormat, swapchainImages); + frameBuffers = vkutils::createFrameBuffers( + logicalDevice, renderPass, swapchainSize, swapchainImageViews); +} + +void SDFRenderer::createPipeline() { + pipelineLayout = vkutils::createPipelineLayout(logicalDevice); + auto fragSpirvPath = shader_utils::compile(fragShaderPath, useToyTemplate); + fragShaderModule = + vkutils::createShaderModule(logicalDevice, fragSpirvPath); + pipeline = vkutils::createGraphicsPipeline( + logicalDevice, renderPass, pipelineLayout, swapchainSize, + vertShaderModule, fragShaderModule); +} + +void SDFRenderer::createCommandBuffers() { + commandBuffers = vkutils::createCommandBuffers(logicalDevice, commandPool, + swapchainImages.count); +} + +void SDFRenderer::destroyPipeline() { + vkDestroyPipeline(logicalDevice, pipeline, nullptr); + vkDestroyPipelineLayout(logicalDevice, pipelineLayout, nullptr); + vkDestroyShaderModule(logicalDevice, fragShaderModule, nullptr); +} + +void SDFRenderer::destroyRenderContext() { + VK_CHECK(vkDeviceWaitIdle(logicalDevice)); + VK_CHECK(vkResetCommandPool(logicalDevice, commandPool, 0)); + vkutils::destroyFrameBuffers(logicalDevice, frameBuffers); + vkutils::destroySwapchainImageViews(logicalDevice, swapchainImageViews); + // Swapchain gets destroyed after passing oldSwapchain to createSwapchain +} + +[[nodiscard]] vkutils::PushConstants +SDFRenderer::getPushConstants(uint32_t currentFrame) noexcept { + vkutils::PushConstants pushConstants = { + .iTime = static_cast(glfwGetTime()), + .iFrame = currentFrame, + .iResolution = glm::vec2(swapchainSize.width, swapchainSize.height), + }; + double xpos, ypos; + glfwGetCursorPos(window, &xpos, &ypos); + if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS) { + pushConstants.iMouse = glm::vec2{xpos, ypos}; + } else { + pushConstants.iMouse = glm::vec2{-1000, -1000}; + } + return pushConstants; +} + +void SDFRenderer::calcTimestamps(uint32_t imageIndex) { + // Get GPU Time + uint64_t timestamps[2]; + vkGetQueryPoolResults(logicalDevice, queryPool, imageIndex * 2, 2, + sizeof(timestamps), ×tamps, sizeof(uint64_t), + VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT); + // deviceProperties.limits.timestampPeriod is + // the number of nanoseconds required for a timestamp query + // to be incremented by 1 + // https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkPhysicalDeviceLimits.html + double totalGpuTime = (timestamps[1] - timestamps[0]) * + deviceProperties.limits.timestampPeriod * 1e-6; + + auto cpuDuration = + std::chrono::duration(cpuEndFrame - cpuStartFrame) + .count(); + + std::string title = fmt::format("VSDF - CPU: {:.3f}ms GPU: {:.3f}ms", + cpuDuration, totalGpuTime); + glfwSetWindowTitle(window, title.c_str()); +} + +void SDFRenderer::gameLoop() { + uint32_t currentFrame = 0; + uint32_t semaphoreIndex = 0; + bool pipelineUpdated = false; + auto filewatcher = filewatcher_factory::createFileWatcher(); + filewatcher->startWatching(fragShaderPath, + [&]() { pipelineUpdated = true; }); + while (!glfwWindowShouldClose(window)) { + cpuStartFrame = std::chrono::high_resolution_clock::now(); + glfwPollEvents(); + uint32_t imageIndex; + + if (app.framebufferResized) { + destroyRenderContext(); + setupRenderContext(); + app.framebufferResized = false; + semaphoreIndex = 0; + spdlog::info("Framebuffer resized!"); + } + if (pipelineUpdated) { + spdlog::info("Recreating pipeline"); + VK_CHECK(vkDeviceWaitIdle(logicalDevice)); + destroyPipeline(); + createPipeline(); + pipelineUpdated = false; + } + + VK_CHECK(vkAcquireNextImageKHR( + logicalDevice, swapchain, UINT64_MAX, + imageAvailableSemaphores.semaphores[semaphoreIndex], VK_NULL_HANDLE, + &imageIndex)); + + VK_CHECK(vkWaitForFences(logicalDevice, 1, &fences.fences[imageIndex], + VK_TRUE, UINT64_MAX)); + VK_CHECK(vkResetFences(logicalDevice, 1, &fences.fences[imageIndex])); + vkutils::recordCommandBuffer( + logicalDevice, commandPool, queryPool, renderPass, swapchainSize, + pipeline, pipelineLayout, commandBuffers.commandBuffers[imageIndex], + frameBuffers.framebuffers[imageIndex], + getPushConstants(currentFrame), imageIndex); + vkutils::submitCommandBuffer( + queue, commandBuffers.commandBuffers[imageIndex], + imageAvailableSemaphores.semaphores[imageIndex], + renderFinishedSemaphores.semaphores[imageIndex], + fences.fences[imageIndex]); + vkutils::presentImage(queue, swapchain, + renderFinishedSemaphores.semaphores[imageIndex], + imageIndex); + semaphoreIndex = (semaphoreIndex + 1) % swapchainImages.count; + currentFrame++; + cpuEndFrame = std::chrono::high_resolution_clock::now(); + calcTimestamps(imageIndex); + } + + filewatcher->stopWatching(); + spdlog::info("Done!"); + destroy(); +} + +void SDFRenderer::destroy() { + VK_CHECK(vkDeviceWaitIdle(logicalDevice)); + vkutils::destroySemaphores(logicalDevice, imageAvailableSemaphores); + vkutils::destroySemaphores(logicalDevice, renderFinishedSemaphores); + vkutils::destroyFences(logicalDevice, fences); + vkDestroyPipeline(logicalDevice, pipeline, nullptr); + vkDestroyPipelineLayout(logicalDevice, pipelineLayout, nullptr); + vkDestroyShaderModule(logicalDevice, vertShaderModule, nullptr); + vkDestroyShaderModule(logicalDevice, fragShaderModule, nullptr); + vkutils::destroyFrameBuffers(logicalDevice, frameBuffers); + vkDestroyRenderPass(logicalDevice, renderPass, nullptr); + vkutils::destroySwapchainImageViews(logicalDevice, swapchainImageViews); + vkDestroySwapchainKHR(logicalDevice, swapchain, nullptr); + vkDestroyQueryPool(logicalDevice, queryPool, nullptr); + vkDestroyCommandPool(logicalDevice, commandPool, nullptr); + vkDestroyDevice(logicalDevice, nullptr); + vkDestroySurfaceKHR(instance, surface, nullptr); + vkDestroyInstance(instance, nullptr); + glfwDestroyWindow(window); + glfwTerminate(); +} diff --git a/src/shader_utils.cpp b/src/shader_utils.cpp new file mode 100644 index 0000000..e769575 --- /dev/null +++ b/src/shader_utils.cpp @@ -0,0 +1,154 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +static constexpr char FRAG_SHADER_TEMPLATE[] = "shaders/toytemplate.frag"; + +EShLanguage getShaderLang(const std::string &extension) { + if (extension.length() < 5) + throw std::runtime_error("Invalid shader extension: " + extension); + + switch (extension[1]) { + case 'v': // .vert + return EShLangVertex; + case 't': + switch (extension[3]) { + case 'c': + return EShLangTessControl; // .tesc + case 'e': + return EShLangTessEvaluation; // .tese + } + case 'g': + return EShLangGeometry; // .geom + case 'f': + [[likely]] return EShLangFragment; // .frag + case 'c': + return EShLangCompute; // .comp + default: + break; + } + + throw std::runtime_error("Unsupported shader extension: " + extension); +} + +namespace shader_utils { +// Read shader source code from file +std::string readShaderSource(const std::string &filename) { + std::ifstream file(filename); + if (!file.is_open()) { + spdlog::error("Failed to open shader source file: {}", filename); + return ""; + } + return std::string((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); +} + +// Apply our template to the shader which includes things +// like iTime, iMouse and redefined main to allow +// running shadertoy shaders +std::string readShaderSourceWithTemplate(const std::string &templateFilename, + const std::string &userFilename) { + std::ifstream templateFile(templateFilename); + std::ifstream userFile(userFilename); + + if (!templateFile.is_open()) { + spdlog::error("Failed to open template shader source file: {}", + templateFilename); + return ""; + } + + if (!userFile.is_open()) { + spdlog::error("Failed to open user shader source file: {}", + userFilename); + return ""; + } + + std::stringstream shaderSourceStream; + shaderSourceStream << templateFile.rdbuf() << '\n' << userFile.rdbuf(); + + return shaderSourceStream.str(); +} + +std::filesystem::path compile(const std::string &shaderFilename, + bool useToyTemplate = false) { + spdlog::info("Compiling shader: {}", shaderFilename); + std::string shaderString; + const char *shaderSource; + std::string shaderExtension = + std::filesystem::path(shaderFilename).extension().string(); + EShLanguage lang = getShaderLang(shaderExtension); + if (useToyTemplate) { + spdlog::debug("Using template for fragment shader {}", shaderFilename); + shaderString = + readShaderSourceWithTemplate(FRAG_SHADER_TEMPLATE, shaderFilename); + } else { + shaderString = readShaderSource(shaderFilename); + } + shaderSource = shaderString.data(); + + glslang::InitializeProcess(); + glslang::TShader shader(lang); + + // https://github.com/KhronosGroup/glslang/blob/main/StandAlone/StandAlone.cpp#L588 + glslang::EShClient Client; + glslang::EshTargetClientVersion ClientVersion; + if (useToyTemplate) { + spdlog::debug("FOR OPENGL"); + Client = glslang::EShClientOpenGL; + ClientVersion = glslang::EShTargetOpenGL_450; + } else { + Client = glslang::EShClientVulkan; + ClientVersion = glslang::EShTargetVulkan_1_0; + } + + // https://github.com/KhronosGroup/glslang/blob/main/StandAlone/StandAlone.cpp#L1097 + glslang::EShTargetLanguage TargetLanguage = glslang::EShTargetSpv; + glslang::EShTargetLanguageVersion TargetVersion = glslang::EShTargetSpv_1_0; + shader.setEnvClient(Client, ClientVersion); + shader.setEnvTarget(TargetLanguage, TargetVersion); + + shader.setStrings(&shaderSource, 1); + + bool result = shader.parse(GetDefaultResources(), 100, ENoProfile, false, + false, EShMsgVulkanRules); + spdlog::info("Shader parsed: {}", result); + spdlog::info("Shader info log: {}", shader.getInfoLog()); + spdlog::debug("Shader source: {}", shaderSource); + + glslang::TProgram program; + program.addShader(&shader); + bool linked = program.link(EShMsgDefault); + spdlog::info("Shader linked: {}", linked); + spdlog::info("Program info log: {}", program.getInfoLog()); + + // Now save SPIR-V binary to a file + std::vector spirv; + spv::SpvBuildLogger logger{}; + glslang::GlslangToSpv(*program.getIntermediate(lang), spirv, &logger, + nullptr); + + spdlog::info("Logger messages: {}", logger.getAllMessages()); + + // Save to file + std::filesystem::path outputPath = shaderFilename; + outputPath.replace_extension(".spv"); + glslang::OutputSpvBin(spirv, outputPath.c_str()); + + // Print some spirv as a test + for (int i = 0; i < 3; i++) { + spdlog::debug("spirv[{}]: {}", i, spirv[i]); + } + + glslang::FinalizeProcess(); + return outputPath; +} +} // namespace shader_utils diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..e65af01 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,14 @@ +project(${PROJECT_NAME}_tests) +find_package(GTest REQUIRED) + +# Include the Google Test library +include_directories(${GTEST_INCLUDE_DIRS}) + +# Add test executable +add_executable(${PROJECT_NAME} ../src/shader_utils.cpp test_shader_comp.cpp) +target_link_libraries(${PROJECT_NAME} ${GTEST_BOTH_LIBRARIES} pthread spdlog::spdlog glslang::glslang glslang::glslang-default-resource-limits glslang::SPIRV) + +# Discover and run tests +include(GoogleTest) +add_subdirectory(filewatcher) +gtest_discover_tests(${PROJECT_NAME}) diff --git a/tests/filewatcher/CMakeLists.txt b/tests/filewatcher/CMakeLists.txt new file mode 100644 index 0000000..3d81194 --- /dev/null +++ b/tests/filewatcher/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.10) + +project(filewatcher_tests) + +find_package(GTest REQUIRED) +find_package(spdlog REQUIRED) + +include_directories(${GTEST_INCLUDE_DIRS} ${PROJECT_SOURCE_DIR}/include) + +set(SOURCE_FILES test_filewatcher.cpp) +if(APPLE) + list(APPEND SOURCE_FILES ../../src/filewatcher/mac_filewatcher.cpp) + find_library(CORE_SERVICES_LIBRARY CoreServices) + set(EXTRA_LIBS ${CORE_SERVICES_LIBRARY}) +elseif(UNIX AND NOT APPLE) + list(APPEND SOURCE_FILES ../../src/filewatcher/linux_filewatcher.cpp) + set(EXTRA_LIBS pthread) # Typically needed for Linux builds +endif() + +add_executable(${PROJECT_NAME} ${SOURCE_FILES}) +target_link_libraries(${PROJECT_NAME} ${GTEST_BOTH_LIBRARIES} ${EXTRA_LIBS} spdlog::spdlog) + +if (CMAKE_BUILD_TYPE STREQUAL "Debug") + target_compile_options(${PROJECT_NAME} PRIVATE -fsanitize=address) + target_link_options(${PROJECT_NAME} PRIVATE -fsanitize=address) +endif() + +include(GoogleTest) +gtest_discover_tests(${PROJECT_NAME}) diff --git a/tests/filewatcher/test_filewatcher.cpp b/tests/filewatcher/test_filewatcher.cpp new file mode 100644 index 0000000..13130a4 --- /dev/null +++ b/tests/filewatcher/test_filewatcher.cpp @@ -0,0 +1,115 @@ +// #include "file_watcher.h" +#include "filewatcher/filewatcher_factory.h" +#include +#include +#include +#include +#include + +// How long to wait for the callback to be called +#define THREAD_WAIT_TIME_MS 50 + +// Helper function to simulate file modification + +void createFile(const std::string &path, const std::string &content) { + std::ofstream file(path); + if (file.is_open()) { + file << content; + file.close(); + } +} + +void appendToFile(const std::string &path, const std::string &content) { + std::ofstream file(path, std::ios_base::app); + if (file.is_open()) { + file << content; + file.close(); + } +} + +// Helper function to simulate file deletion and creation +void replaceFile(const std::string &path, const std::string &content) { + std::remove(path.c_str()); // Delete the file + createFile(path, content); // Create a new file with content +} + +class FileWatcherTest : public ::testing::Test { + protected: + // Our test file + std::string testFilePath = "testfile.txt"; + // A different file just to make sure we only care about test file + std::string differentFilePath = "differenttestfile.txt"; + + virtual void SetUp() { + std::remove(testFilePath.c_str()); + std::remove(differentFilePath.c_str()); + } + + virtual void TearDown() { + std::remove(testFilePath.c_str()); + std::remove(differentFilePath.c_str()); + } +}; + +TEST_F(FileWatcherTest, NoChangeCallbackNotCalled) { + bool callbackCalled = false; + auto callback = [&callbackCalled]() { callbackCalled = true; }; + createFile(testFilePath, "New content"); + createFile(differentFilePath, "Different content"); + + auto watcher = filewatcher_factory::createFileWatcher(); + watcher->startWatching(testFilePath, callback); + appendToFile(differentFilePath, "New content"); + std::this_thread::sleep_for(std::chrono::milliseconds(THREAD_WAIT_TIME_MS)); + watcher->stopWatching(); + + EXPECT_FALSE(callbackCalled); +} + +TEST_F(FileWatcherTest, FileModifiedCallbackCalled) { + bool callbackCalled = false; + auto callback = [&callbackCalled]() { callbackCalled = true; }; + createFile(testFilePath, "New content"); + + auto watcher = filewatcher_factory::createFileWatcher(); + watcher->startWatching(testFilePath, callback); + std::this_thread::sleep_for(std::chrono::milliseconds(THREAD_WAIT_TIME_MS)); + appendToFile(testFilePath, "New content"); + std::this_thread::sleep_for(std::chrono::milliseconds(THREAD_WAIT_TIME_MS)); + watcher->stopWatching(); + + EXPECT_TRUE(callbackCalled); +} + +TEST_F(FileWatcherTest, FileDeletedAndReplacedCallbackCalled) { + bool callbackCalled = false; + auto callback = [&callbackCalled]() { callbackCalled = true; }; + createFile(testFilePath, "New content"); + + auto watcher = filewatcher_factory::createFileWatcher(); + watcher->startWatching(testFilePath, callback); + std::this_thread::sleep_for(std::chrono::milliseconds(THREAD_WAIT_TIME_MS)); + replaceFile(testFilePath, "Replacement content"); + std::this_thread::sleep_for(std::chrono::milliseconds(THREAD_WAIT_TIME_MS)); + watcher->stopWatching(); + + EXPECT_TRUE(callbackCalled); +} + +TEST_F(FileWatcherTest, FileReplacedMultipleTimesCallbackCalled) { + int callbackCount = 0; + auto callback = [&callbackCount]() { callbackCount++; }; + + createFile(testFilePath, "New content"); + auto watcher = filewatcher_factory::createFileWatcher(); + watcher->startWatching(testFilePath, callback); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + for (int i = 0; i < 10; ++i) { + replaceFile(testFilePath, "Content " + std::to_string(i)); + std::this_thread::sleep_for( + std::chrono::milliseconds(50)); // Wait a bit between replacements + } + watcher->stopWatching(); + + EXPECT_GE(callbackCount, 10); // Ensure callback was called at least once +} diff --git a/tests/test_shader_comp.cpp b/tests/test_shader_comp.cpp new file mode 100644 index 0000000..76b6232 --- /dev/null +++ b/tests/test_shader_comp.cpp @@ -0,0 +1,70 @@ +#include "shader_utils.h" +#include "test_utils.h" +#include +#include +#include + +TEST(ShaderUtilsTest, CompileTest) { + TempShaderFile tempShader("temp_shader.frag", + "#version 450\nvoid main() {}"); + shader_utils::compile(tempShader.filename()); + + std::string expectedSpvFilename = "temp_shader.spv"; + std::ifstream spvFile(expectedSpvFilename); + bool fileExists = spvFile.good(); + spvFile.close(); + std::remove(expectedSpvFilename.c_str()); + + ASSERT_TRUE(fileExists); +} + +TEST(ShaderUtilsTest2, CompileTestBadVersion) { + TempShaderFile tempShader( + "temp_shader.frag", "// GLSL Fragment shader example\nvoid main() {}"); + ASSERT_TRUE(1); // Replace with actual error-checking logic + + // Simulate the compilation process and validate the error handling +} + +TEST(ShaderUtilsTest, CompileVertexShader) { + TempShaderFile tempShader( + "temp_vertex.vert", + "#version 450\nvoid main() { gl_Position = vec4(0.0); }"); + shader_utils::compile(tempShader.filename()); + + std::string expectedSpvFilename = "temp_vertex.spv"; + std::ifstream spvFile(expectedSpvFilename); + bool fileExists = spvFile.good(); + spvFile.close(); + std::remove(expectedSpvFilename.c_str()); + + ASSERT_TRUE(fileExists); +} + +TEST(ShaderUtilsTest, CompileGLSLESTest) { + // Example of a simple GLSL ES shader like those used in Shadertoy + // By passing true param here it should prepend the template + // that will make this compatible with our push constants + // so we do not need to specify version + TempShaderFile tempShader( + "temp_glsl_es.frag", + "precision highp float;\n" + "void mainImage( out vec4 fragColor, in vec2 fragCoord ){\n" + " fragColor = vec4(1.0, 0.0, 0.0, 1.0); // Red\n" + "}"); + + shader_utils::compile(tempShader.filename(), true); + + std::string expectedSpvFilename = "temp_glsl_es.spv"; + std::ifstream spvFile(expectedSpvFilename); + bool fileExists = spvFile.good(); + spvFile.close(); + std::remove(expectedSpvFilename.c_str()); + + ASSERT_TRUE(fileExists); +} + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/test_utils.h b/tests/test_utils.h new file mode 100644 index 0000000..10c8775 --- /dev/null +++ b/tests/test_utils.h @@ -0,0 +1,22 @@ +#ifndef TEST_UTILS_H +#define TEST_UTILS_H +#include +#include + +class TempShaderFile { + public: + TempShaderFile(const std::string &filename, const std::string &content) + : filename_(filename) { + std::ofstream shaderFile(filename_); + shaderFile << content; + shaderFile.close(); + } + + ~TempShaderFile() { std::remove(filename_.c_str()); } + + const std::string &filename() const { return filename_; } + + private: + std::string filename_; +}; +#endif // TEST_UTILS_H