diff --git a/CMakeLists.txt b/CMakeLists.txt index cb569903..c74dc440 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -113,6 +113,7 @@ set(CORE_SOURCES src/Core/Application.cpp src/Core/CommandLine.cpp src/Core/File.cpp + src/Core/FileWatcher.cpp src/Core/Logger.cpp src/Core/ScopedTimer.cpp src/Core/Serializable.cpp @@ -128,6 +129,7 @@ set(DEBUG_SOURCES src/Debug/Benchmark.cpp src/Debug/DebugLineRenderer.cpp src/Debug/DebugOverlay.cpp + src/Debug/ShaderHotReload.cpp src/Debug/SystemMetrics.cpp ) @@ -373,6 +375,7 @@ endforeach() if(GLSLC) set(ENGINE_SPIRV_DIR "${CMAKE_CURRENT_BINARY_DIR}/shaders") file(MAKE_DIRECTORY "${ENGINE_SPIRV_DIR}") + target_compile_definitions(Engine PRIVATE SLEAK_GLSLC_PATH="${GLSLC}") foreach(_src ${ENGINE_VULKAN_SHADERS}) get_filename_component(_name "${_src}" NAME) set(_spv "${ENGINE_SPIRV_DIR}/${_name}.spv") @@ -429,6 +432,10 @@ add_dependencies(Engine CompileShaders) set_property(GLOBAL PROPERTY ENGINE_SPIRV_FILES ${ENGINE_SPIRV_FILES}) set_property(GLOBAL PROPERTY ENGINE_COMMITTED_SPIRV ${ENGINE_COMMITTED_SPIRV}) +target_compile_definitions(Engine + PUBLIC $<$:SLEAK_HOT_RELOAD> + PRIVATE SLEAK_ENGINE_SHADER_DIR="${CMAKE_CURRENT_SOURCE_DIR}/assets/shaders") + function(_engine_collect_targets dir out) get_property(_targets DIRECTORY "${dir}" PROPERTY BUILDSYSTEM_TARGETS) get_property(_subdirs DIRECTORY "${dir}" PROPERTY SUBDIRECTORIES) diff --git a/include/private/Graphics/Common/Renderer.hpp b/include/private/Graphics/Common/Renderer.hpp index 13203f02..e6c936c7 100644 --- a/include/private/Graphics/Common/Renderer.hpp +++ b/include/private/Graphics/Common/Renderer.hpp @@ -1,9 +1,11 @@ #pragma once - -#include "RenderContext.hpp" #include #include +#include +#include + +#include "RenderContext.hpp" namespace Sleak { @@ -49,6 +51,12 @@ class ENGINE_API Renderer { virtual void Cleanup() = 0; /// Blocks until the GPU has finished all outstanding work. virtual void WaitIdle() {} + /// Rebuilds what uses the given shader stems from the files on disk. + /// Returns false when the backend has no hot reload support. + virtual bool ReloadShaders(const std::vector& stems) { + (void)stems; + return false; + } /// Flushes any buffered upload/staging transfers still in flight. virtual void FlushPendingTransfers() {} diff --git a/include/private/Graphics/DirectX11/DirectX11Renderer.hpp b/include/private/Graphics/DirectX11/DirectX11Renderer.hpp index 22767ab6..be8d4612 100644 --- a/include/private/Graphics/DirectX11/DirectX11Renderer.hpp +++ b/include/private/Graphics/DirectX11/DirectX11Renderer.hpp @@ -30,6 +30,10 @@ class ENGINE_API DirectX11Renderer : public Renderer, public RenderContext { void BeginRender() override; void EndRender() override; void Cleanup() override; +#ifdef SLEAK_HOT_RELOAD + /// Recompiles every live shader and fixed pass built from one of the stems. + bool ReloadShaders(const std::vector& stems) override; +#endif /// Allocates the depth/stencil texture and view sized to the swapchain. bool CreateDepthStencilBuffer(uint32_t width, uint32_t height); @@ -200,6 +204,12 @@ class ENGINE_API DirectX11Renderer : public Renderer, public RenderContext { bool m_tonemapResourcesCreated = false; /// Allocates the HDR offscreen target and tonemap shaders/sampler. bool CreateTonemapResources(); + /// Compiles tonemap.hlsl, writing the outputs only on success. + bool CompileTonemapShaders(ID3D11VertexShader** vs, ID3D11PixelShader** ps); +#ifdef SLEAK_HOT_RELOAD + /// Swaps in recompiled tonemap shaders, keeping the old ones on failure. + void ReloadTonemapShaders(); +#endif void CleanupTonemapResources(); /// Runs the ACES tonemap full-screen pass from the HDR target to the back buffer. void ExecuteTonemapPass(); diff --git a/include/private/Graphics/DirectX11/DirectX11Shader.hpp b/include/private/Graphics/DirectX11/DirectX11Shader.hpp index 493ccb01..26c5842c 100644 --- a/include/private/Graphics/DirectX11/DirectX11Shader.hpp +++ b/include/private/Graphics/DirectX11/DirectX11Shader.hpp @@ -9,6 +9,9 @@ #include #include #include +#ifdef SLEAK_HOT_RELOAD +#include +#endif #include "../Common/Shader.hpp" @@ -20,6 +23,8 @@ class DirectX11Shader : public Shader { public: DirectX11Shader(ID3D11Device* device); ~DirectX11Shader() override; + DirectX11Shader(const DirectX11Shader&) = delete; + DirectX11Shader& operator=(const DirectX11Shader&) = delete; /// Compiles the combined-path convention (vs_main/ps_main entry points) from a single HLSL file. bool compile(const std::string& shaderPath) override; @@ -33,7 +38,41 @@ class DirectX11Shader : public Shader { ID3D11InputLayout* createInputLayout(); ID3D11InputLayout* getInputLayout() const { return Layout.Get(); } +#ifdef SLEAK_HOT_RELOAD + /// Rebuilds stages and input layout, keeping the old ones on failure. + bool Reload(); + /// Shares the stages and layout of a shader built from the same files. + void AdoptStages(const DirectX11Shader& source); + /// Returns true when both shaders compile the same files and entry points. + bool HasSameSources(const DirectX11Shader& other) const { + return m_sources == other.m_sources; + } + /// Returns the stem of the vertex source, e.g. skybox for skybox.hlsl. + const std::string& GetStem() const { return m_vertStem; } + /// Returns true when the vertex or pixel source has the given stem. + bool UsesStem(const std::string& stem) const { + return stem == m_vertStem || stem == m_fragStem; + } + /// Returns every live DirectX11Shader instance. + static const std::unordered_set& GetLiveShaders(); +#endif + private: + /// Files and entry points the two stages compile from. + struct StageSources { + std::string vertPath; + std::string vertEntry; + std::string fragPath; + std::string fragEntry; + + bool operator==(const StageSources&) const = default; + }; + + /// Compiles both stages into new objects and swaps them in on success. + bool BuildStages(StageSources sources); + /// Creates an input layout matching the inputs of the vertex bytecode. + bool CreateLayoutFor(ID3DBlob* vertexBlob, + Microsoft::WRL::ComPtr& layout); /// Compiles one HLSL entry point/profile via D3DCompile into a shader object and bytecode blob. template bool compileShader(const std::string& filePath, @@ -54,6 +93,12 @@ class DirectX11Shader : public Shader { m_vertexShaderBlob; // For input layout creation Microsoft::WRL::ComPtr m_pixelShaderBlob; +#ifdef SLEAK_HOT_RELOAD + StageSources m_sources; + std::string m_vertStem; + std::string m_fragStem; +#endif + D3D11_INPUT_ELEMENT_DESC DefaultLayout[5] = { // Semantic Index Format Slot Offset Class Step Rate {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0}, diff --git a/include/private/Graphics/DirectX12/DirectX12Renderer.hpp b/include/private/Graphics/DirectX12/DirectX12Renderer.hpp index fdbb5a95..9adc6df4 100644 --- a/include/private/Graphics/DirectX12/DirectX12Renderer.hpp +++ b/include/private/Graphics/DirectX12/DirectX12Renderer.hpp @@ -40,6 +40,10 @@ class ENGINE_API DirectX12Renderer : public Renderer, public RenderContext { void EndRender() override; void Cleanup() override; void WaitIdle() override { WaitForGPU(); } +#ifdef SLEAK_HOT_RELOAD + /// Rebuilds the PSOs of live shaders and fixed passes using the stems. + bool ReloadShaders(const std::vector& stems) override; +#endif virtual void Resize(uint32_t width, uint32_t height) override; @@ -136,6 +140,23 @@ class ENGINE_API DirectX12Renderer : public Renderer, public RenderContext { /// Returns the cached PSO for the key, building it on a miss. ID3D12PipelineState* GetPipeline(const PipelineKey& key, ID3DBlob* vs, ID3DBlob* ps); + /// Returns the default-state PSO for a vertex/pixel shader pair. + ID3D12PipelineState* GetShaderPipeline(ID3DBlob* vs, ID3DBlob* ps); +#ifdef SLEAK_HOT_RELOAD + /// Recompiles live shaders using the stems, collecting their old PSOs. + void ReloadLiveShaders(const std::vector& stems, + std::vector& replaced); + /// Rebuilds a fixed pass PSO, keeping the old one on failure. + void ReloadPassPipeline(ID3D12PipelineState*& pso, bool& failed, + bool (DirectX12Renderer::*create)(), + const char* stem, + std::vector& replaced); + /// Recompiles the shadow vertex shader and rebuilds the shadow PSO. + void ReloadShadowPassPSO(std::vector& replaced); + /// Drops cached PSOs that were replaced and that nothing binds anymore. + void PruneReplacedPipelines( + const std::vector& replaced); +#endif /// Reapplies the last shader PSO after a pass override ends. void RestoreShaderPipeline(); /// Copies the light/fog constants into the ring and binds them at b2. diff --git a/include/private/Graphics/DirectX12/DirectX12Shader.hpp b/include/private/Graphics/DirectX12/DirectX12Shader.hpp index b0dad66b..aab32feb 100644 --- a/include/private/Graphics/DirectX12/DirectX12Shader.hpp +++ b/include/private/Graphics/DirectX12/DirectX12Shader.hpp @@ -8,6 +8,9 @@ #include #include #include +#ifdef SLEAK_HOT_RELOAD +#include +#endif #include "../Common/Shader.hpp" @@ -20,6 +23,8 @@ class DirectX12Shader : public Shader { public: DirectX12Shader(ID3D12Device* device); ~DirectX12Shader() override; + DirectX12Shader(const DirectX12Shader&) = delete; + DirectX12Shader& operator=(const DirectX12Shader&) = delete; /// Compiles the combined-path convention (vs_main/ps_main entry points) from a single HLSL file. bool compile(const std::string& shaderPath) override; @@ -39,12 +44,50 @@ class DirectX12Shader : public Shader { /// Routes bind() through the renderer so pass-specific PSOs are respected. void SetRenderer(DirectX12Renderer* renderer); +#ifdef SLEAK_HOT_RELOAD + /// Recompiles both stages from their files without touching the live ones. + bool RecompileStages(Microsoft::WRL::ComPtr& vs, + Microsoft::WRL::ComPtr& ps) const; + /// Swaps in new bytecode together with the PSO built from it. + void ReplaceStages(Microsoft::WRL::ComPtr vs, + Microsoft::WRL::ComPtr ps, + ID3D12PipelineState* pso); + /// Returns true when both shaders compile the same files and entry points. + bool HasSameSources(const DirectX12Shader& other) const { + return m_sources == other.m_sources; + } + /// Returns the stem of the vertex source, e.g. skybox for skybox_dx12.hlsl. + const std::string& GetStem() const { return m_vertStem; } + /// Returns true when the vertex or pixel source has the given stem. + bool UsesStem(const std::string& stem) const { + return stem == m_vertStem || stem == m_fragStem; + } + /// Returns every live DirectX12Shader instance. + static const std::unordered_set& GetLiveShaders(); +#endif + private: + /// Files and entry points the two stages compile from. + struct StageSources { + std::string vertPath; + std::string vertEntry; + std::string fragPath; + std::string fragEntry; + + bool operator==(const StageSources&) const = default; + }; + + /// Compiles both stages and swaps them in only when both succeed. + bool BuildStages(StageSources sources); + /// Compiles both stages of the given sources into new blobs. + bool CompileSources(const StageSources& sources, + Microsoft::WRL::ComPtr& vs, + Microsoft::WRL::ComPtr& ps) const; /// Compiles one HLSL entry point/profile via D3DCompileFromFile into bytecode. bool compileShader(const std::string& filePath, - const std::string& entryPoint, - const std::string& profile, - Microsoft::WRL::ComPtr& blob); + const std::string& entryPoint, + const std::string& profile, + Microsoft::WRL::ComPtr& blob) const; Microsoft::WRL::ComPtr m_device; @@ -53,6 +96,11 @@ class DirectX12Shader : public Shader { Microsoft::WRL::ComPtr m_pipelineState; ID3D12GraphicsCommandList* m_commandList = nullptr; DirectX12Renderer* m_renderer = nullptr; +#ifdef SLEAK_HOT_RELOAD + StageSources m_sources; + std::string m_vertStem; + std::string m_fragStem; +#endif }; } // namespace RenderEngine diff --git a/include/private/Graphics/OpenGL/OpenGLRenderer.hpp b/include/private/Graphics/OpenGL/OpenGLRenderer.hpp index 566a1156..4f926baf 100644 --- a/include/private/Graphics/OpenGL/OpenGLRenderer.hpp +++ b/include/private/Graphics/OpenGL/OpenGLRenderer.hpp @@ -25,6 +25,10 @@ class ENGINE_API OpenGLRenderer : public Renderer, public RenderContext { void BeginRender() override; void EndRender() override; void Cleanup() override; +#ifdef SLEAK_HOT_RELOAD + /// Relinks every live shader built from one of the given stems. + bool ReloadShaders(const std::vector& stems) override; +#endif virtual uint32_t GetFeatureCaps() const override { return CapDeferred | CapSSAO | CapIBL | CapShadows; diff --git a/include/private/Graphics/OpenGL/OpenGLShader.hpp b/include/private/Graphics/OpenGL/OpenGLShader.hpp index 37537319..da7bdf86 100644 --- a/include/private/Graphics/OpenGL/OpenGLShader.hpp +++ b/include/private/Graphics/OpenGL/OpenGLShader.hpp @@ -1,9 +1,15 @@ #ifndef OPENGLSHADER_HPP_ #define OPENGLSHADER_HPP_ -#include "../Common/Shader.hpp" #include +#include + +#include "../Common/Shader.hpp" +#ifdef SLEAK_HOT_RELOAD +#include +#endif + namespace Sleak { namespace RenderEngine { @@ -12,6 +18,8 @@ class ENGINE_API OpenGLShader : public Shader { public: OpenGLShader(); ~OpenGLShader() override; + OpenGLShader(const OpenGLShader&) = delete; + OpenGLShader& operator=(const OpenGLShader&) = delete; /// Splits a combined shader file into vertex/fragment stages and links them. bool compile(const std::string& shaderPath) override; @@ -21,13 +29,33 @@ class ENGINE_API OpenGLShader : public Shader { GLuint GetProgram() const { return m_program; } -private: + /// Rebuilds the program from its files, keeping the old one on failure. + bool Reload(); + /// Returns the stem of the vertex source, e.g. gbuffer for gbuffer_gl.vert. + const std::string& GetStem() const { return m_vertStem; } + /// Returns true when the vertex or fragment source has the given stem. + bool UsesStem(const std::string& stem) const { + return stem == m_vertStem || stem == m_fragStem; + } + +#ifdef SLEAK_HOT_RELOAD + /// Returns every live OpenGLShader instance. + static const std::unordered_set& GetLiveShaders(); +#endif + + private: /// Compiles a single shader stage from source text. bool CompileShaderSource(const std::string& source, GLenum type, - GLuint& shader); + const std::string& path, GLuint& shader); + /// Links a new program from the stored paths and swaps it in on success. + bool BuildProgram(); std::string ReadFile(const std::string& path); GLuint m_program = 0; + std::string m_vertPath; + std::string m_fragPath; + std::string m_vertStem; + std::string m_fragStem; }; } // namespace RenderEngine diff --git a/include/private/Graphics/Vulkan/VulkanRenderer.hpp b/include/private/Graphics/Vulkan/VulkanRenderer.hpp index cde5d94c..95b56fae 100644 --- a/include/private/Graphics/Vulkan/VulkanRenderer.hpp +++ b/include/private/Graphics/Vulkan/VulkanRenderer.hpp @@ -228,6 +228,12 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { /// Recreates the extent-dependent shadow map objects at the queued resolution. void ApplyShadowResolutionChange() override; +#ifdef SLEAK_HOT_RELOAD + /// Rebuilds every pipeline from the SPIR-V on disk; a pipeline that fails + /// to rebuild keeps its previous version. + bool ReloadShaders(const std::vector& stems) override; +#endif + // Measurement /// Adds per-pass GPU millisecond columns when --gpuprofile is on. void RegisterBenchmarkMetrics(Benchmark& bench) override; @@ -543,6 +549,25 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { /// Destroys every cached custom-format pipeline (all variants, all formats). void DestroyCustomFormatPipelines(); +#ifdef SLEAK_HOT_RELOAD + // Shader hot reload + /// The pipelines, layouts, and shaders one create function writes, swapped + /// as a unit when shaders reload. + struct PipelineGroup { + const char* name; + std::vector pipelines; + std::vector layouts; + std::vector shaders; + bool (VulkanRenderer::*create)(); + }; + /// Rebuilds a live group through its create function, keeping the old + /// objects when the rebuild fails. + void ReloadPipelineGroup(const PipelineGroup& group); + /// Rebuilds every cached custom-format variant, keeping the old pipeline of + /// a variant that fails. + void ReloadCustomFormatPipelines(); +#endif + // Bone palette ring (set 1, binding 0, dynamic offset per bind) struct BoneUBOChunk { VkDescriptorPool pool = VK_NULL_HANDLE; @@ -875,6 +900,8 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { /// Creates the SSR image, render pass, framebuffer, descriptors, UBOs, and pipeline. bool CreateSSRResources(); + /// Compiles the SSR shaders and creates the SSR pipeline. + bool CreateSSRPipeline(); /// Destroys the SSR pipeline, framebuffer, descriptors, image, and sampler. void CleanupSSRResources(); /// Fills the SSR UBO with the cached camera matrices, camera position, and ray march parameters. @@ -928,6 +955,8 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { /// Creates the ping-pong TAA history images, render pass, framebuffers, descriptors, and pipeline. bool CreateTAAResources(); + /// Compiles the TAA shaders and creates the TAA resolve pipeline. + bool CreateTAAPipeline(); /// Destroys the TAA pipeline, framebuffers, descriptors, images, and sampler. void CleanupTAAResources(); /// Computes the inverse current view-projection and reprojection blend factor into the TAA UBO. diff --git a/include/public/Core/FileWatcher.hpp b/include/public/Core/FileWatcher.hpp new file mode 100644 index 00000000..27ba0e13 --- /dev/null +++ b/include/public/Core/FileWatcher.hpp @@ -0,0 +1,34 @@ +#ifndef _FILE_WATCHER_HPP_ +#define _FILE_WATCHER_HPP_ + +#include +#include +#include +#include + +namespace Sleak { + +/// Reports files saved inside watched directories. Not recursive. +/// Generic: shader, library and asset hot reload all build on it. +/// @ingroup core +class ENGINE_API FileWatcher { + public: + FileWatcher(); + ~FileWatcher(); + FileWatcher(const FileWatcher&) = delete; + FileWatcher& operator=(const FileWatcher&) = delete; + + /// Starts watching a directory. Returns false if it can't be watched. + bool AddDirectory(const std::string& dir); + + /// Appends full paths of files saved since the last call. Never blocks. + void Poll(std::vector& outChanged); + + private: + struct Impl; + std::unique_ptr m_impl; +}; + +} // namespace Sleak + +#endif diff --git a/include/public/Debug/ShaderHotReload.hpp b/include/public/Debug/ShaderHotReload.hpp new file mode 100644 index 00000000..d1fbfd92 --- /dev/null +++ b/include/public/Debug/ShaderHotReload.hpp @@ -0,0 +1,25 @@ +#ifndef _SHADER_HOT_RELOAD_HPP_ +#define _SHADER_HOT_RELOAD_HPP_ + +#include +#include + +namespace Sleak { +namespace Debug { + +/// Recompiles and reloads shaders when their source files are saved. +/// Debug builds only; every call is a no-op in Release. +/// @ingroup debug +class ENGINE_API ShaderHotReload { + public: + /// Watches a shader source folder. Later folders override earlier ones. + static void AddSourceDirectory(const std::string& dir); + + /// Rebuilds changed shaders. Called once per frame by Application. + static void Poll(); +}; + +} // namespace Debug +} // namespace Sleak + +#endif diff --git a/src/Core/Application.cpp b/src/Core/Application.cpp index 86e21a6d..c7e7dc73 100644 --- a/src/Core/Application.cpp +++ b/src/Core/Application.cpp @@ -1,38 +1,39 @@ #include "../../include/public/Core/Application.hpp" -#include "../../include/public/Core/CommandLine.hpp" -#include "../../include/private/Graphics/Common/RendererFactory.hpp" -#include "../../include/private/Graphics/Common/RenderCommandQueue.hpp" -#include -#include -#include -#include -#include -#include -#include -#include "Graphics/Vulkan/VulkanRenderer.hpp" -#include "Core/Logger.hpp" -#include #include -#include #include - #include -#include -#include -#include #include -#include -#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include + +#include "../../include/private/Graphics/Common/RenderCommandQueue.hpp" +#include "../../include/private/Graphics/Common/RendererFactory.hpp" +#include "../../include/public/Core/CommandLine.hpp" +#include "Core/Logger.hpp" +#include "ECS/Components/FirstPersonController.hpp" +#include "ECS/Components/FreeLookCameraController.hpp" #include "ECS/Components/MeshComponent.hpp" #include "ECS/Components/TransformComponent.hpp" -#include "ECS/Components/FreeLookCameraController.hpp" -#include "ECS/Components/FirstPersonController.hpp" -#include -#include -#include +#include "Graphics/Vulkan/VulkanRenderer.hpp" #define DEFAULT_CAPTURE_FRAME 600 @@ -177,6 +178,8 @@ namespace Sleak { uint64_t frameIndex = 0; bool captureArmed = false; + Debug::ShaderHotReload::AddSourceDirectory(SLEAK_ENGINE_SHADER_DIR); + // Initialize and begin the game (and scene) if (Game) { if (!Game->Initialize()) { @@ -246,6 +249,8 @@ namespace Sleak { } } + Debug::ShaderHotReload::Poll(); + renderer->BeginRender(); // Update active scene if present diff --git a/src/Core/FileWatcher.cpp b/src/Core/FileWatcher.cpp new file mode 100644 index 00000000..c0177ee2 --- /dev/null +++ b/src/Core/FileWatcher.cpp @@ -0,0 +1,188 @@ +#include +#include +#include + +#if defined(PLATFORM_LINUX) +#include +#include + +#include +#include +#include +#elif defined(PLATFORM_WIN) +#define WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif + +namespace Sleak { + +namespace { +constexpr size_t EVENT_BUFFER_SIZE = 4096; +} + +#if defined(PLATFORM_LINUX) + +struct FileWatcher::Impl { + int fd = -1; + std::unordered_map dirs; +}; + +FileWatcher::FileWatcher() : m_impl(std::make_unique()) { + m_impl->fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + if (m_impl->fd < 0) + SLEAK_ERROR("FileWatcher: inotify_init1 failed: {}", + std::strerror(errno)); +} + +FileWatcher::~FileWatcher() { + if (m_impl->fd >= 0) close(m_impl->fd); +} + +bool FileWatcher::AddDirectory(const std::string& dir) { + if (m_impl->fd < 0) return false; + const int wd = inotify_add_watch(m_impl->fd, dir.c_str(), + IN_CLOSE_WRITE | IN_MOVED_TO); + if (wd < 0) { + SLEAK_ERROR("FileWatcher: can't watch {}: {}", dir, + std::strerror(errno)); + return false; + } + m_impl->dirs[wd] = dir; + return true; +} + +void FileWatcher::Poll(std::vector& outChanged) { + if (m_impl->fd < 0) return; + alignas(inotify_event) char buf[EVENT_BUFFER_SIZE]; + for (;;) { + const ssize_t len = read(m_impl->fd, buf, sizeof(buf)); + if (len <= 0) break; + for (char* p = buf; p < buf + len;) { + const auto* ev = reinterpret_cast(p); + p += sizeof(inotify_event) + ev->len; + if (ev->len == 0) continue; + const auto dir = m_impl->dirs.find(ev->wd); + if (dir == m_impl->dirs.end()) continue; + outChanged.push_back(dir->second + "/" + ev->name); + } + } +} + +#elif defined(PLATFORM_WIN) + +namespace { +constexpr DWORD WATCH_FILTER = + FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_FILE_NAME; + +struct WatchedDirectory { + std::string path; + HANDLE handle = INVALID_HANDLE_VALUE; + OVERLAPPED overlapped = {}; + alignas(DWORD) char buffer[EVENT_BUFFER_SIZE]; +}; + +/// Queues the next asynchronous change read for a directory. +bool Arm(WatchedDirectory& dir) { + ResetEvent(dir.overlapped.hEvent); + return ReadDirectoryChangesW(dir.handle, dir.buffer, sizeof(dir.buffer), + FALSE, WATCH_FILTER, nullptr, &dir.overlapped, + nullptr) != 0; +} + +/// Converts a counted UTF-16 file name to UTF-8. +std::string ToUtf8(const WCHAR* name, int length) { + const int size = WideCharToMultiByte(CP_UTF8, 0, name, length, nullptr, 0, + nullptr, nullptr); + std::string out(size, '\0'); + WideCharToMultiByte(CP_UTF8, 0, name, length, out.data(), size, nullptr, + nullptr); + return out; +} +} // namespace + +struct FileWatcher::Impl { + std::vector > dirs; +}; + +FileWatcher::FileWatcher() : m_impl(std::make_unique()) {} + +FileWatcher::~FileWatcher() { + for (auto& dir : m_impl->dirs) { + DWORD bytes = 0; + CancelIoEx(dir->handle, &dir->overlapped); + GetOverlappedResult(dir->handle, &dir->overlapped, &bytes, TRUE); + CloseHandle(dir->overlapped.hEvent); + CloseHandle(dir->handle); + } +} + +bool FileWatcher::AddDirectory(const std::string& dir) { + auto watched = std::make_unique(); + watched->path = dir; + watched->handle = + CreateFileA(dir.c_str(), FILE_LIST_DIRECTORY, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, nullptr); + if (watched->handle == INVALID_HANDLE_VALUE) { + SLEAK_ERROR("FileWatcher: can't open {} (error {})", dir, + GetLastError()); + return false; + } + watched->overlapped.hEvent = CreateEventA(nullptr, TRUE, FALSE, nullptr); + if (!watched->overlapped.hEvent || !Arm(*watched)) { + SLEAK_ERROR("FileWatcher: can't watch {} (error {})", dir, + GetLastError()); + if (watched->overlapped.hEvent) CloseHandle(watched->overlapped.hEvent); + CloseHandle(watched->handle); + return false; + } + m_impl->dirs.push_back(std::move(watched)); + return true; +} + +void FileWatcher::Poll(std::vector& outChanged) { + for (auto& dir : m_impl->dirs) { + DWORD bytes = 0; + if (!GetOverlappedResult(dir->handle, &dir->overlapped, &bytes, FALSE)) + continue; + for (DWORD offset = 0; bytes > 0;) { + const auto* info = reinterpret_cast( + dir->buffer + offset); + if (info->Action != FILE_ACTION_REMOVED && + info->Action != FILE_ACTION_RENAMED_OLD_NAME) { + const int length = + static_cast(info->FileNameLength / sizeof(WCHAR)); + outChanged.push_back(dir->path + "/" + + ToUtf8(info->FileName, length)); + } + if (info->NextEntryOffset == 0) break; + offset += info->NextEntryOffset; + } + Arm(*dir); + } +} + +#else + +struct FileWatcher::Impl {}; + +FileWatcher::FileWatcher() : m_impl(std::make_unique()) {} + +FileWatcher::~FileWatcher() = default; + +bool FileWatcher::AddDirectory(const std::string& dir) { + SLEAK_WARN("FileWatcher: not supported on this platform ({})", dir); + return false; +} + +void FileWatcher::Poll(std::vector& outChanged) { + (void)outChanged; +} + +#endif + +} // namespace Sleak diff --git a/src/Debug/ShaderHotReload.cpp b/src/Debug/ShaderHotReload.cpp new file mode 100644 index 00000000..cee15336 --- /dev/null +++ b/src/Debug/ShaderHotReload.cpp @@ -0,0 +1,298 @@ +#include + +#ifdef SLEAK_HOT_RELOAD +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef PLATFORM_WIN +#include +#else +#include +#endif +#endif + +namespace Sleak { +namespace Debug { + +#ifdef SLEAK_HOT_RELOAD +namespace { + +namespace fs = std::filesystem; + +constexpr float DEBOUNCE_SECONDS = 0.15f; +constexpr float MS_PER_SECOND = 1000.0f; +constexpr size_t PROCESS_LINE_SIZE = 512; +constexpr const char* RUNTIME_SHADER_DIR = "assets/shaders"; +constexpr const char* SPIRV_EXTENSION = ".spv"; +constexpr const char* TEMP_EXTENSION = ".tmp"; +constexpr std::string_view GL_SUFFIX = "_gl"; +constexpr std::string_view DX12_SUFFIX = "_dx12"; + +struct State { + FileWatcher watcher; + std::vector directories; + std::set pending; + Timer quietTimer; +}; + +/// Lazily created hot reload state shared by all calls. +State& GetState() { + static State state; + return state; +} + +/// Per-process temp name next to a target, so two running games never clash. +fs::path TempPathFor(const fs::path& target) { +#ifdef PLATFORM_WIN + const int pid = _getpid(); +#else + const int pid = static_cast(getpid()); +#endif + fs::path tmp = target; + tmp += "." + std::to_string(pid) + TEMP_EXTENSION; + return tmp; +} + +/// Atomically replaces a target with a finished temp file. +bool ReplaceWith(const fs::path& tmp, const fs::path& target) { + std::error_code ec; + fs::rename(tmp, target, ec); + if (!ec) return true; + SLEAK_ERROR("Hot reload: can't replace {}: {}", target.filename().string(), + ec.message()); + fs::remove(tmp, ec); + return false; +} + +/// True for shader sources the watcher reacts to. +bool IsShaderSource(const fs::path& file) { + const fs::path ext = file.extension(); + return ext == ".vert" || ext == ".frag" || ext == ".hlsl"; +} + +/// True for GLSL that glslc compiles to SPIR-V for Vulkan. +bool IsVulkanSource(const fs::path& file) { + const fs::path ext = file.extension(); + if (ext != ".vert" && ext != ".frag") return false; + return !file.stem().string().ends_with(GL_SUFFIX); +} + +/// Stem shared by all backend variants, e.g. lighting_pass_gl.frag. +std::string StemOf(const fs::path& file) { + std::string name = file.filename().string(); + name = name.substr(0, name.find('.')); + for (std::string_view suffix : {GL_SUFFIX, DX12_SUFFIX}) { + if (name.ends_with(suffix)) { + name.resize(name.size() - suffix.size()); + break; + } + } + return name; +} + +/// Backend that loads this source file, picked from its suffix. +RenderEngine::RendererType BackendOf(const fs::path& file) { + using RenderEngine::RendererType; + const std::string stem = file.stem().string(); + if (file.extension() == ".hlsl") + return stem.ends_with(DX12_SUFFIX) ? RendererType::DirectX12 + : RendererType::DirectX11; + return stem.ends_with(GL_SUFFIX) ? RendererType::OpenGL + : RendererType::Vulkan; +} + +/// File name pattern a backend loads for a stem. +std::string VariantFor(const std::string& stem, + RenderEngine::RendererType type) { + using RenderEngine::RendererType; + switch (type) { + case RendererType::Vulkan: + return stem + ".vert / " + stem + ".frag"; + case RendererType::OpenGL: + return stem + "_gl.vert / " + stem + "_gl.frag"; + case RendererType::DirectX11: + return stem + ".hlsl"; + case RendererType::DirectX12: + return stem + "_dx12.hlsl"; + } + return stem; +} + +/// True when a later source folder has a file with the same name. +bool IsOverridden(const fs::path& file, const State& state) { + const auto dir = std::find(state.directories.begin(), + state.directories.end(), file.parent_path()); + if (dir == state.directories.end()) return false; + for (auto later = dir + 1; later != state.directories.end(); ++later) { + std::error_code ec; + if (fs::exists(*later / file.filename(), ec)) return true; + } + return false; +} + +/// Compiles GLSL to SPIR-V next to the source. Keeps the old .spv on error. +bool CompileSpirv(const fs::path& src, const fs::path& spv) { +#ifndef SLEAK_GLSLC_PATH + (void)spv; + SLEAK_WARN("Hot reload: glslc was not found at configure time, {} skipped", + src.filename().string()); + return false; +#else + const fs::path tmp = TempPathFor(spv); + std::string cmd = "\"" SLEAK_GLSLC_PATH "\" \"" + src.string() + + "\" -o \"" + tmp.string() + "\" 2>&1"; +#ifdef PLATFORM_WIN + cmd = "\"" + cmd + "\""; + FILE* pipe = _popen(cmd.c_str(), "r"); +#else + FILE* pipe = popen(cmd.c_str(), "r"); +#endif + if (!pipe) { + SLEAK_ERROR("Hot reload: can't run glslc for {}", + src.filename().string()); + return false; + } + std::string output; + char line[PROCESS_LINE_SIZE]; + while (std::fgets(line, sizeof(line), pipe)) output += line; +#ifdef PLATFORM_WIN + const int status = _pclose(pipe); +#else + const int status = pclose(pipe); +#endif + if (status != 0) { + std::error_code ec; + fs::remove(tmp, ec); + SLEAK_ERROR( + "Hot reload: {} failed to compile, keeping the old " + "shader\n{}", + src.filename().string(), output); + return false; + } + return ReplaceWith(tmp, spv); +#endif +} + +/// Copies a file into the runtime shader folder the backends load from. +bool CopyToRuntime(const fs::path& file) { + const fs::path target = fs::path(RUNTIME_SHADER_DIR) / file.filename(); + const fs::path tmp = TempPathFor(target); + std::error_code ec; + fs::copy_file(file, tmp, fs::copy_options::overwrite_existing, ec); + if (ec) { + SLEAK_ERROR("Hot reload: can't copy {}: {}", file.filename().string(), + ec.message()); + return false; + } + return ReplaceWith(tmp, target); +} + +/// Compiles and stages one saved file. Returns its stem, or empty to skip. +std::string ProcessFile(const fs::path& file, const State& state) { + std::error_code ec; + if (!fs::exists(file, ec)) return {}; + if (IsOverridden(file, state)) { + SLEAK_WARN( + "Hot reload: {} is overridden by a later shader folder, " + "edit that copy instead", + file.filename().string()); + return {}; + } + if (IsVulkanSource(file)) { + fs::path spv = file; + spv += SPIRV_EXTENSION; + if (!CompileSpirv(file, spv) || !CopyToRuntime(spv)) return {}; + } + if (!CopyToRuntime(file)) return {}; + return StemOf(file); +} + +} // namespace +#endif + +void ShaderHotReload::AddSourceDirectory(const std::string& dir) { +#ifdef SLEAK_HOT_RELOAD + std::error_code ec; + const fs::path path = fs::canonical(dir, ec); + if (ec) { + SLEAK_WARN("Hot reload: shader folder {} not found", dir); + return; + } + State& state = GetState(); + if (std::find(state.directories.begin(), state.directories.end(), path) != + state.directories.end()) + return; + if (!state.watcher.AddDirectory(path.string())) return; + state.directories.push_back(path); + SLEAK_INFO("Hot reload: watching {}", path.string()); +#else + (void)dir; +#endif +} + +void ShaderHotReload::Poll() { +#ifdef SLEAK_HOT_RELOAD + State& state = GetState(); + if (state.directories.empty()) return; + + std::vector changed; + state.watcher.Poll(changed); + for (const std::string& path : changed) { + const fs::path file(path); + if (!IsShaderSource(file)) continue; + state.pending.insert(file); + state.quietTimer.Reset(); + } + if (state.pending.empty() || state.quietTimer.Elapsed() < DEBOUNCE_SECONDS) + return; + + Application* app = Application::GetInstance(); + RenderEngine::Renderer* renderer = app ? app->GetRenderer() : nullptr; + if (!renderer) return; + const RenderEngine::RendererType active = renderer->GetType(); + + Timer reloadTimer; + std::vector stems; + for (const fs::path& file : state.pending) { + std::string stem = ProcessFile(file, state); + if (stem.empty()) continue; + if (BackendOf(file) != active) { + SLEAK_WARN( + "Hot reload: {} is not used by the {} renderer, edit {} " + "instead", + file.filename().string(), app->GetRendererTypeStr(), + VariantFor(stem, active)); + continue; + } + if (std::find(stems.begin(), stems.end(), stem) == stems.end()) + stems.push_back(std::move(stem)); + } + state.pending.clear(); + if (stems.empty()) return; + + app->WaitGPUIdle(); + if (!renderer->ReloadShaders(stems)) { + SLEAK_WARN("Hot reload: the {} backend can't reload shaders yet", + app->GetRendererTypeStr()); + return; + } + + std::string names; + for (const std::string& stem : stems) + names += (names.empty() ? "" : ", ") + stem; + SLEAK_INFO("Hot reload: reloaded {} in {:.0f} ms", names, + reloadTimer.Elapsed() * MS_PER_SECOND); +#endif +} + +} // namespace Debug +} // namespace Sleak diff --git a/src/Graphics/DirectX11/DirectX11Renderer.cpp b/src/Graphics/DirectX11/DirectX11Renderer.cpp index 727b28a1..e9b1a4bc 100644 --- a/src/Graphics/DirectX11/DirectX11Renderer.cpp +++ b/src/Graphics/DirectX11/DirectX11Renderer.cpp @@ -1,25 +1,52 @@ #include "../../include/private/Graphics/DirectX11/DirectX11Renderer.hpp" #ifdef PLATFORM_WIN -#include #include +#include #include +#include +#include +#include +#include + +#include +#include #include #include -#include "Graphics/DirectX11/DirectX11Texture.hpp" -#include "Graphics/DirectX11/DirectX11CubemapTexture.hpp" #include -#include -#include -#include -#include -#include +#include #include +#include +#include + #include "Graphics/Common/RenderCommandQueue.hpp" +#include "Graphics/DirectX11/DirectX11CubemapTexture.hpp" +#include "Graphics/DirectX11/DirectX11Texture.hpp" namespace Sleak { namespace RenderEngine { - + + namespace { + + constexpr const char* TONEMAP_SHADER_PATH = "assets/shaders/tonemap.hlsl"; +#ifdef SLEAK_HOT_RELOAD + constexpr const char* TONEMAP_STEM = "tonemap"; +#endif + + /// Logs a failed D3DCompileFromFile call with the compiler output. + void LogCompileError(const char* path, const char* entry, HRESULT hr, + ID3DBlob* errors) { + if (errors) { + SLEAK_ERROR("{} ({}) compile error: {}", path, entry, + static_cast(errors->GetBufferPointer())); + } else { + SLEAK_ERROR("{} ({}) compile error: 0x{:08X}", path, entry, + static_cast(hr)); + } + } + + } // namespace + /// Registers this backend's buffer/shader/texture factories with ResourceManager. DirectX11Renderer::DirectX11Renderer(Window* window) : window(window), @@ -68,9 +95,9 @@ bool DirectX11Renderer::Initialize() { swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; UINT createDeviceFlags = 0; - #ifdef _DEBUG - createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG; - #endif +#ifdef _DEBUG + createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG; +#endif D3D_FEATURE_LEVEL featureLevels[] = { D3D_FEATURE_LEVEL_11_0 }; HRESULT hr = D3D11CreateDeviceAndSwapChain( @@ -103,43 +130,41 @@ bool DirectX11Renderer::Initialize() { SLEAK_INFO("DirectX 11 has successfully created!"); - #ifdef _DEBUG - ID3D11InfoQueue* infoQueue; - if (SUCCEEDED(device->QueryInterface(__uuidof(ID3D11InfoQueue), (void**)&infoQueue))) { - // Only break on corruption (critical), not on errors — breaking on - // errors without a debugger attached silently drops draw calls. - infoQueue->SetBreakOnSeverity(D3D11_MESSAGE_SEVERITY_CORRUPTION, true); +#ifdef _DEBUG + ID3D11InfoQueue* infoQueue; + if (SUCCEEDED(device->QueryInterface(__uuidof(ID3D11InfoQueue), + (void**)&infoQueue))) { + // Only break on corruption (critical), not on errors — breaking on + // errors without a debugger attached silently drops draw calls. + infoQueue->SetBreakOnSeverity(D3D11_MESSAGE_SEVERITY_CORRUPTION, true); - D3D11_MESSAGE_SEVERITY severities[] = { - D3D11_MESSAGE_SEVERITY_INFO - }; + D3D11_MESSAGE_SEVERITY severities[] = {D3D11_MESSAGE_SEVERITY_INFO}; - D3D11_INFO_QUEUE_FILTER filter = {}; - filter.DenyList.NumSeverities = _countof(severities); - filter.DenyList.pSeverityList = severities; + D3D11_INFO_QUEUE_FILTER filter = {}; + filter.DenyList.NumSeverities = _countof(severities); + filter.DenyList.pSeverityList = severities; - infoQueue->AddStorageFilterEntries(&filter); - infoQueue->Release(); - } - #endif + infoQueue->AddStorageFilterEntries(&filter); + infoQueue->Release(); + } +#endif - SetPerformanceCounter(true); - SetRenderMode(RenderMode::Fill); - SetRenderFace(RenderFace::Front); + SetPerformanceCounter(true); + SetRenderMode(RenderMode::Fill); + SetRenderFace(RenderFace::Front); - ConfigureRenderMode(); + ConfigureRenderMode(); - SetDepthStencilState(true, true); + SetDepthStencilState(true, true); - msaaSampleCount = 1; + msaaSampleCount = 1; + + D3D11_QUERY_DESC queryDesc; + queryDesc.Query = D3D11_QUERY::D3D11_QUERY_PIPELINE_STATISTICS; + queryDesc.MiscFlags = 0; + if (FAILED(device->CreateQuery(&queryDesc, &query))) + SLEAK_WARN("Could not create pipeline statics object!"); - D3D11_QUERY_DESC queryDesc; - queryDesc.Query = D3D11_QUERY::D3D11_QUERY_PIPELINE_STATISTICS; - queryDesc.MiscFlags = 0; - if(FAILED(device->CreateQuery(&queryDesc,&query))) - SLEAK_WARN("Could not create pipeline statics object!"); - - if(bEnabledPerformanceCounter){ if(query) while (deviceContext->GetData(query, nullptr, 0, 0) == S_FALSE); @@ -286,16 +311,16 @@ void DirectX11Renderer::Cleanup() { bImInitialized = false; } - #ifdef _DEBUG - if(device) { - ID3D11Debug* debugDevice; - device->QueryInterface(__uuidof(ID3D11Debug), (void**)&debugDevice); - if (debugDevice) { - debugDevice->ReportLiveDeviceObjects(D3D11_RLDO_DETAIL); - debugDevice->Release(); - } +#ifdef _DEBUG + if (device) { + ID3D11Debug* debugDevice; + device->QueryInterface(__uuidof(ID3D11Debug), (void**)&debugDevice); + if (debugDevice) { + debugDevice->ReportLiveDeviceObjects(D3D11_RLDO_DETAIL); + debugDevice->Release(); } - #endif + } +#endif if (device) { device->Release(); @@ -853,12 +878,75 @@ Shader* DirectX11Renderer::CreateShader(const std::string& shaderSource) { if (!bIsLayoutCreated && il) { layout = il; bIsLayoutCreated = true; + } else if (il) { + il->Release(); } return static_cast(shader); } + delete shader; return nullptr; } +#ifdef SLEAK_HOT_RELOAD +bool DirectX11Renderer::ReloadShaders(const std::vector& stems) { + const auto usesAnyStem = [&stems](const DirectX11Shader* shader) { + return std::ranges::any_of(stems, [shader](const std::string& stem) { + return shader->UsesStem(stem); + }); + }; + + // Shaders built from the same files compile once and share the result + std::vector rebuilt; + std::vector failed; + for (DirectX11Shader* shader : DirectX11Shader::GetLiveShaders()) { + if (!usesAnyStem(shader)) continue; + const auto sameSources = [shader](const DirectX11Shader* other) { + return shader->HasSameSources(*other); + }; + if (std::ranges::any_of(failed, sameSources)) continue; + + ID3D11InputLayout* oldLayout = shader->getInputLayout(); + const auto twin = std::ranges::find_if(rebuilt, sameSources); + if (twin != rebuilt.end()) { + shader->AdoptStages(**twin); + } else if (shader->Reload()) { + rebuilt.push_back(shader); + SLEAK_INFO("Reloaded DirectX 11 shader '{}'", shader->GetStem()); + } else { + failed.push_back(shader); + SLEAK_WARN("Shader '{}' failed to reload, keeping the old one", + shader->GetStem()); + continue; + } + + // The renderer holds its own reference to the first shader's layout + if (bIsLayoutCreated && oldLayout && oldLayout == layout) { + ID3D11InputLayout* fresh = shader->getInputLayout(); + if (fresh) fresh->AddRef(); + layout->Release(); + layout = fresh; + } + } + + if (std::find(stems.begin(), stems.end(), TONEMAP_STEM) != stems.end()) + ReloadTonemapShaders(); + + for (const std::string& stem : stems) { + const bool used = + stem == TONEMAP_STEM || + std::ranges::any_of(DirectX11Shader::GetLiveShaders(), + [&stem](const DirectX11Shader* shader) { + return shader->UsesStem(stem); + }); + if (!used) + SLEAK_WARN( + "Hot reload: no loaded DirectX 11 shader is built from '{}'", + stem); + } + return true; +} +#endif + bool DirectX11Renderer::CreateBlendState() { //(+) : Binary Operator // Color = SourceP (X) SourceFactor (+) DestP (X) DestFactor @@ -1308,52 +1396,72 @@ bool DirectX11Renderer::CreateTonemapResources() { hr = device->CreateBuffer(&cbDesc, nullptr, &m_postProcessCB); if (FAILED(hr)) return false; - // Compile tonemap shaders - ID3DBlob* vsBlob = nullptr; - ID3DBlob* psBlob = nullptr; - ID3DBlob* errorBlob = nullptr; + if (!CompileTonemapShaders(&m_tonemapVS, &m_tonemapPS)) return false; - // Build path to shader (relative to working directory = bin/) - std::string narrowPath = "assets/shaders/tonemap.hlsl"; - std::wstring shaderPath(narrowPath.begin(), narrowPath.end()); + m_tonemapResourcesCreated = true; + SLEAK_INFO("Tonemapping post-process resources created successfully"); + return true; +} - hr = D3DCompileFromFile(shaderPath.c_str(), nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, +bool DirectX11Renderer::CompileTonemapShaders(ID3D11VertexShader** vs, + ID3D11PixelShader** ps) { + // Path is relative to the working directory (bin/) + const std::string narrowPath = TONEMAP_SHADER_PATH; + const std::wstring shaderPath(narrowPath.begin(), narrowPath.end()); + + Microsoft::WRL::ComPtr vsBlob; + Microsoft::WRL::ComPtr psBlob; + Microsoft::WRL::ComPtr errorBlob; + HRESULT hr = D3DCompileFromFile( + shaderPath.c_str(), nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, "VS_Main", "vs_5_0", D3DCOMPILE_DEBUG, 0, &vsBlob, &errorBlob); if (FAILED(hr)) { - if (errorBlob) { - SLEAK_ERROR("Tonemap VS compile error: {}", - (const char*)errorBlob->GetBufferPointer()); - errorBlob->Release(); - } + LogCompileError(TONEMAP_SHADER_PATH, "VS_Main", hr, errorBlob.Get()); return false; } - - hr = D3DCompileFromFile(shaderPath.c_str(), nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, - "PS_Main", "ps_5_0", D3DCOMPILE_DEBUG, 0, &psBlob, &errorBlob); + hr = D3DCompileFromFile(shaderPath.c_str(), nullptr, + D3D_COMPILE_STANDARD_FILE_INCLUDE, "PS_Main", + "ps_5_0", D3DCOMPILE_DEBUG, 0, &psBlob, &errorBlob); if (FAILED(hr)) { - if (errorBlob) { - SLEAK_ERROR("Tonemap PS compile error: {}", - (const char*)errorBlob->GetBufferPointer()); - errorBlob->Release(); - } - vsBlob->Release(); + LogCompileError(TONEMAP_SHADER_PATH, "PS_Main", hr, errorBlob.Get()); return false; } - hr = device->CreateVertexShader(vsBlob->GetBufferPointer(), - vsBlob->GetBufferSize(), nullptr, &m_tonemapVS); - vsBlob->Release(); - if (FAILED(hr)) { psBlob->Release(); return false; } + Microsoft::WRL::ComPtr vertexShader; + Microsoft::WRL::ComPtr pixelShader; + if (FAILED(device->CreateVertexShader(vsBlob->GetBufferPointer(), + vsBlob->GetBufferSize(), nullptr, + &vertexShader)) || + FAILED(device->CreatePixelShader(psBlob->GetBufferPointer(), + psBlob->GetBufferSize(), nullptr, + &pixelShader))) { + SLEAK_ERROR("Failed to create tonemap shader objects"); + return false; + } + *vs = vertexShader.Detach(); + *ps = pixelShader.Detach(); + return true; +} - hr = device->CreatePixelShader(psBlob->GetBufferPointer(), - psBlob->GetBufferSize(), nullptr, &m_tonemapPS); - psBlob->Release(); - if (FAILED(hr)) return false; +#ifdef SLEAK_HOT_RELOAD +void DirectX11Renderer::ReloadTonemapShaders() { + // Not built yet, the first tonemap pass compiles the new file + if (!m_tonemapResourcesCreated) return; - m_tonemapResourcesCreated = true; - SLEAK_INFO("Tonemapping post-process resources created successfully"); - return true; + ID3D11VertexShader* vs = nullptr; + ID3D11PixelShader* ps = nullptr; + if (!CompileTonemapShaders(&vs, &ps)) { + SLEAK_WARN("Shader '{}' failed to reload, keeping the old one", + TONEMAP_STEM); + return; + } + if (m_tonemapVS) m_tonemapVS->Release(); + if (m_tonemapPS) m_tonemapPS->Release(); + m_tonemapVS = vs; + m_tonemapPS = ps; + SLEAK_INFO("Reloaded DirectX 11 shader '{}'", TONEMAP_STEM); } +#endif void DirectX11Renderer::CleanupTonemapResources() { if (m_tonemapVS) { m_tonemapVS->Release(); m_tonemapVS = nullptr; } diff --git a/src/Graphics/DirectX11/DirectX11Shader.cpp b/src/Graphics/DirectX11/DirectX11Shader.cpp index 2df09543..19a72123 100644 --- a/src/Graphics/DirectX11/DirectX11Shader.cpp +++ b/src/Graphics/DirectX11/DirectX11Shader.cpp @@ -8,53 +8,111 @@ #include #include #include +#include +#ifdef SLEAK_HOT_RELOAD +#include +#endif namespace Sleak { namespace RenderEngine { +namespace { + +constexpr const char* VERTEX_ENTRY = "VS_Main"; +constexpr const char* PIXEL_ENTRY = "PS_Main"; +constexpr const char* SPLIT_FILE_ENTRY = "Main"; +constexpr const char* VERTEX_PROFILE = "vs_5_0"; +constexpr const char* PIXEL_PROFILE = "ps_5_0"; + +#ifdef SLEAK_HOT_RELOAD +/// Returns the file name up to its first '.', e.g. skybox for skybox.hlsl. +std::string StemFromPath(const std::string& path) { + const std::string name = std::filesystem::path(path).filename().string(); + return name.substr(0, name.find('.')); +} + +/// Returns the live shader registry, leaked to survive static teardown. +std::unordered_set& LiveShaders() { + static auto* shaders = new std::unordered_set(); + return *shaders; +} +#endif + +} // namespace + DirectX11Shader::DirectX11Shader(ID3D11Device* device) : m_device(device) { assert(m_device != nullptr); // Ensure the device is valid m_device->GetImmediateContext(m_deviceContext.GetAddressOf()); +#ifdef SLEAK_HOT_RELOAD + LiveShaders().insert(this); +#endif } DirectX11Shader::~DirectX11Shader() { - // Resources are automatically released by ComPtr +#ifdef SLEAK_HOT_RELOAD + LiveShaders().erase(this); +#endif } -bool DirectX11Shader::compile(const std::string& shaderPath) { - // Compile vertex shader - if (!compileShader(shaderPath, "VS_Main", "vs_5_0", m_vertexShader, - m_vertexShaderBlob)) { - SLEAK_ERROR("Failed to compile vertex shader!"); - return false; - } +#ifdef SLEAK_HOT_RELOAD +const std::unordered_set& DirectX11Shader::GetLiveShaders() { + return LiveShaders(); +} - // Compile pixel shader - if (!compileShader(shaderPath, "PS_Main", "ps_5_0", m_pixelShader, - m_pixelShaderBlob)) { - SLEAK_ERROR("Failed to compile pixel shader!"); - return false; - } +bool DirectX11Shader::Reload() { + if (m_sources.vertPath.empty() || m_sources.fragPath.empty()) return false; + return BuildStages(m_sources); +} - return true; +void DirectX11Shader::AdoptStages(const DirectX11Shader& source) { + m_vertexShader = source.m_vertexShader; + m_pixelShader = source.m_pixelShader; + m_vertexShaderBlob = source.m_vertexShaderBlob; + m_pixelShaderBlob = source.m_pixelShaderBlob; + Layout = source.Layout; +} +#endif + +bool DirectX11Shader::compile(const std::string& shaderPath) { + return BuildStages({shaderPath, VERTEX_ENTRY, shaderPath, PIXEL_ENTRY}); } bool DirectX11Shader::compile(const std::string& vertPath, const std::string& fragPath) { - // Compile vertex shader - if (!compileShader(vertPath, "Main", "vs_5_0", m_vertexShader, - m_vertexShaderBlob)) { - SLEAK_ERROR("Failed to compile vertex shader!"); + return BuildStages( + {vertPath, SPLIT_FILE_ENTRY, fragPath, SPLIT_FILE_ENTRY}); +} + +bool DirectX11Shader::BuildStages(StageSources sources) { + Microsoft::WRL::ComPtr vertexShader; + Microsoft::WRL::ComPtr pixelShader; + Microsoft::WRL::ComPtr vertexBlob; + Microsoft::WRL::ComPtr pixelBlob; + if (!compileShader(sources.vertPath, sources.vertEntry, VERTEX_PROFILE, + vertexShader, vertexBlob)) { + SLEAK_ERROR("Failed to compile vertex shader {}", sources.vertPath); return false; } - - // Compile pixel shader - if (!compileShader(fragPath, "Main", "ps_5_0", m_pixelShader, - m_pixelShaderBlob)) { - SLEAK_ERROR("Failed to compile pixel shader!"); + if (!compileShader(sources.fragPath, sources.fragEntry, PIXEL_PROFILE, + pixelShader, pixelBlob)) { + SLEAK_ERROR("Failed to compile pixel shader {}", sources.fragPath); return false; } + // A rebuild needs a layout validated against the new vertex inputs + Microsoft::WRL::ComPtr layout; + if (Layout && !CreateLayoutFor(vertexBlob.Get(), layout)) return false; + + m_vertexShader = std::move(vertexShader); + m_pixelShader = std::move(pixelShader); + m_vertexShaderBlob = std::move(vertexBlob); + m_pixelShaderBlob = std::move(pixelBlob); + if (layout) Layout = std::move(layout); +#ifdef SLEAK_HOT_RELOAD + m_vertStem = StemFromPath(sources.vertPath); + m_fragStem = StemFromPath(sources.fragPath); + m_sources = std::move(sources); +#endif return true; } @@ -74,13 +132,25 @@ ID3DBlob* DirectX11Shader::getVertexShaderBlob() const { } ID3D11InputLayout* DirectX11Shader::createInputLayout() { + Microsoft::WRL::ComPtr layout; + if (!m_vertexShaderBlob || + !CreateLayoutFor(m_vertexShaderBlob.Get(), layout)) + return nullptr; + + // Store per-shader so bind() can set it, and keep a ref for the caller + Layout = layout; + return layout.Detach(); +} + +bool DirectX11Shader::CreateLayoutFor( + ID3DBlob* vertexBlob, Microsoft::WRL::ComPtr& layout) { // Check via reflection if the shader uses BLENDINDICES (skinned shader) bool isSkinned = false; Microsoft::WRL::ComPtr reflector; - HRESULT hr = D3DReflect(m_vertexShaderBlob->GetBufferPointer(), - m_vertexShaderBlob->GetBufferSize(), - IID_ID3D11ShaderReflection, - reinterpret_cast(reflector.GetAddressOf())); + HRESULT hr = + D3DReflect(vertexBlob->GetBufferPointer(), vertexBlob->GetBufferSize(), + IID_ID3D11ShaderReflection, + reinterpret_cast(reflector.GetAddressOf())); if (SUCCEEDED(hr)) { D3D11_SHADER_DESC shaderDesc; reflector->GetDesc(&shaderDesc); @@ -97,22 +167,16 @@ ID3D11InputLayout* DirectX11Shader::createInputLayout() { D3D11_INPUT_ELEMENT_DESC* layoutDesc = isSkinned ? SkinnedLayout : DefaultLayout; UINT layoutCount = isSkinned ? ARRAYSIZE(SkinnedLayout) : ARRAYSIZE(DefaultLayout); - ID3D11InputLayout* inputLayout = nullptr; hr = m_device->CreateInputLayout( - layoutDesc, layoutCount, - m_vertexShaderBlob->GetBufferPointer(), - m_vertexShaderBlob->GetBufferSize(), &inputLayout); + layoutDesc, layoutCount, vertexBlob->GetBufferPointer(), + vertexBlob->GetBufferSize(), layout.ReleaseAndGetAddressOf()); if (FAILED(hr)) { - SLEAK_ERROR("Failed to create input layout: 0x{:08X}", hr); - return nullptr; + SLEAK_ERROR("Failed to create input layout: 0x{:08X}", + static_cast(hr)); + return false; } - - // Store per-shader so bind() can set it - Layout.Attach(inputLayout); - inputLayout->AddRef(); // Keep a ref for the caller too - - return inputLayout; + return true; } /// Compiles a shader stage from an HLSL file and creates the matching D3D11 shader object. @@ -137,8 +201,11 @@ bool DirectX11Shader::compileShader(const std::string& filePath, if (FAILED(hr)) { if (errorBlob) { - SLEAK_ERROR("Shader compilation error: {}", - (char*)errorBlob->GetBufferPointer()); + SLEAK_ERROR("Shader compilation error in {} ({}): {}", filePath, + entryPoint, (char*)errorBlob->GetBufferPointer()); + } else { + SLEAK_ERROR("Shader compilation error in {} ({}): 0x{:08X}", + filePath, entryPoint, static_cast(hr)); } return false; } @@ -154,6 +221,10 @@ bool DirectX11Shader::compileShader(const std::string& filePath, reinterpret_cast(shader.GetAddressOf())); } + if (FAILED(hr)) { + SLEAK_ERROR("Failed to create shader object for {} ({}): 0x{:08X}", + filePath, entryPoint, static_cast(hr)); + } return SUCCEEDED(hr); } diff --git a/src/Graphics/DirectX12/DirectX12Renderer.cpp b/src/Graphics/DirectX12/DirectX12Renderer.cpp index eb900ec1..7c18ed92 100644 --- a/src/Graphics/DirectX12/DirectX12Renderer.cpp +++ b/src/Graphics/DirectX12/DirectX12Renderer.cpp @@ -10,12 +10,15 @@ #include #include #include +#include #include #include #include #include #include #include +#include +#include #include "Graphics/Common/RenderCommandQueue.hpp" @@ -52,21 +55,48 @@ uint64_t HashBytecode(ID3DBlob* blob) { return hash; } -Microsoft::WRL::ComPtr CompileStage(const wchar_t* path, +constexpr const char* VERTEX_ENTRY = "VS_Main"; +constexpr const char* PIXEL_ENTRY = "PS_Main"; +constexpr const char* VERTEX_PROFILE = "vs_5_0"; +constexpr const char* PIXEL_PROFILE = "ps_5_0"; +constexpr const char* SHADOW_SHADER_PATH = + "assets/shaders/default_shader_dx12.hlsl"; +constexpr const char* SKYBOX_SHADER_PATH = "assets/shaders/skybox_dx12.hlsl"; +constexpr const char* DEBUG_LINE_SHADER_PATH = + "assets/shaders/debug_line_dx12.hlsl"; +#ifdef SLEAK_HOT_RELOAD +constexpr const char* SHADOW_SHADER_STEM = "default_shader"; +constexpr const char* SKYBOX_STEM = "skybox"; +constexpr const char* DEBUG_LINE_STEM = "debug_line"; +#endif + +/// Compiles one fixed-pass stage, logging the compiler output on failure. +Microsoft::WRL::ComPtr CompileStage(const char* path, const char* entry, const char* profile) { + const std::wstring widePath(path, path + std::strlen(path)); Microsoft::WRL::ComPtr blob, errorBlob; - HRESULT hr = D3DCompileFromFile(path, nullptr, nullptr, entry, profile, 0, - 0, &blob, &errorBlob); + HRESULT hr = D3DCompileFromFile(widePath.c_str(), nullptr, nullptr, entry, + profile, 0, 0, &blob, &errorBlob); if (FAILED(hr)) { if (errorBlob) { - SLEAK_ERROR("{} ({}) compile error: {}", entry, profile, + SLEAK_ERROR("{} {} ({}) compile error: {}", path, entry, profile, (char*)errorBlob->GetBufferPointer()); + } else { + SLEAK_ERROR("{} {} ({}) compile error: 0x{:08X}", path, entry, + profile, static_cast(hr)); } return nullptr; } return blob; } + +#ifdef SLEAK_HOT_RELOAD +/// True when the reload batch contains the stem. +bool HasStem(const std::vector& stems, const char* stem) { + return std::find(stems.begin(), stems.end(), stem) != stems.end(); +} +#endif } // namespace /// Registers this backend's buffer/shader/texture factories with ResourceManager. @@ -572,13 +602,172 @@ ID3D12PipelineState* DirectX12Renderer::GetPipeline(const PipelineKey& key, return pso.Get(); } +ID3D12PipelineState* DirectX12Renderer::GetShaderPipeline(ID3DBlob* vs, + ID3DBlob* ps) { + PipelineKey key; + key.vsHash = HashBytecode(vs); + key.psHash = HashBytecode(ps); + return GetPipeline(key, vs, ps); +} + +#ifdef SLEAK_HOT_RELOAD +bool DirectX12Renderer::ReloadShaders(const std::vector& stems) { + // Old PSOs are released below, so no frame may still use them + WaitForGPU(); + + std::vector replaced; + ReloadLiveShaders(stems, replaced); + if (HasStem(stems, SHADOW_SHADER_STEM)) ReloadShadowPassPSO(replaced); + if (HasStem(stems, SKYBOX_STEM)) + ReloadPassPipeline(m_skyboxPipelineState, m_skyboxPipelineFailed, + &DirectX12Renderer::CreateSkyboxPipelineState, + SKYBOX_STEM, replaced); + if (HasStem(stems, DEBUG_LINE_STEM)) + ReloadPassPipeline(m_debugLinePipelineState, m_debugLinePipelineFailed, + &DirectX12Renderer::CreateDebugLinePipelineState, + DEBUG_LINE_STEM, replaced); + PruneReplacedPipelines(replaced); + m_shaderPipeline = nullptr; + m_passPipeline = nullptr; + + for (const std::string& stem : stems) { + const bool used = + stem == SHADOW_SHADER_STEM || stem == SKYBOX_STEM || + stem == DEBUG_LINE_STEM || + std::ranges::any_of(DirectX12Shader::GetLiveShaders(), + [&stem](const DirectX12Shader* shader) { + return shader->UsesStem(stem); + }); + if (!used) + SLEAK_WARN( + "Hot reload: no loaded DirectX 12 shader is built from '{}'", + stem); + } + return true; +} + +void DirectX12Renderer::ReloadLiveShaders( + const std::vector& stems, + std::vector& replaced) { + const auto usesAnyStem = [&stems](const DirectX12Shader* shader) { + return std::ranges::any_of(stems, [shader](const std::string& stem) { + return shader->UsesStem(stem); + }); + }; + + // Shaders built from the same files compile once and share the result + std::vector rebuilt; + std::vector failed; + for (DirectX12Shader* shader : DirectX12Shader::GetLiveShaders()) { + if (!usesAnyStem(shader)) continue; + const auto sameSources = [shader](const DirectX12Shader* other) { + return shader->HasSameSources(*other); + }; + if (std::ranges::any_of(failed, sameSources)) continue; + + Microsoft::WRL::ComPtr vs; + Microsoft::WRL::ComPtr ps; + ID3D12PipelineState* pso = nullptr; + const auto twin = std::ranges::find_if(rebuilt, sameSources); + if (twin != rebuilt.end()) { + vs = (*twin)->getVertexShaderBlob(); + ps = (*twin)->getPixelShaderBlob(); + pso = (*twin)->GetPipelineState(); + } else if (shader->RecompileStages(vs, ps)) { + pso = GetShaderPipeline(vs.Get(), ps.Get()); + } + if (!pso) { + failed.push_back(shader); + SLEAK_WARN("Shader '{}' failed to reload, keeping the old pipeline", + shader->GetStem()); + continue; + } + if (twin == rebuilt.end()) { + rebuilt.push_back(shader); + SLEAK_INFO("Reloaded DirectX 12 shader '{}'", shader->GetStem()); + } + + // The first shader also provides the default PSO and shadow fallback + if (m_cachedVSBlob && + m_cachedVSBlob.Get() == shader->getVertexShaderBlob()) { + m_cachedVSBlob = vs; + m_defaultPipelineState = pso; + } + if (ID3D12PipelineState* old = shader->GetPipelineState()) + replaced.push_back(old); + shader->ReplaceStages(std::move(vs), std::move(ps), pso); + } +} + +void DirectX12Renderer::ReloadPassPipeline( + ID3D12PipelineState*& pso, bool& failed, + bool (DirectX12Renderer::*create)(), const char* stem, + std::vector& replaced) { + ID3D12PipelineState* old = std::exchange(pso, nullptr); + failed = false; + // Not built yet, the first pass compiles the new file + if (!old) return; + + if ((this->*create)()) { + replaced.push_back(old); + SLEAK_INFO("Reloaded DirectX 12 pipeline '{}'", stem); + return; + } + pso = old; + failed = false; + SLEAK_WARN("Pipeline '{}' failed to reload, keeping the old one", stem); +} + +void DirectX12Renderer::ReloadShadowPassPSO( + std::vector& replaced) { + // Not built yet, the first shadow pass compiles the new file + if (!m_shadowVSCompiled) return; + + Microsoft::WRL::ComPtr vs = + CompileStage(SHADOW_SHADER_PATH, VERTEX_ENTRY, VERTEX_PROFILE); + if (!vs) { + SLEAK_WARN("Shadow pass shader failed to reload, keeping the old one"); + return; + } + ID3D12PipelineState* oldPSO = std::exchange(m_shadowPassPSO, nullptr); + Microsoft::WRL::ComPtr oldVS = std::exchange(m_shadowVSBlob, vs); + if (CreateShadowPassPSO()) { + if (oldPSO) replaced.push_back(oldPSO); + SLEAK_INFO("Reloaded DirectX 12 pipeline 'shadow pass'"); + return; + } + m_shadowPassPSO = oldPSO; + m_shadowVSBlob = std::move(oldVS); + SLEAK_WARN("Shadow pass PSO failed to rebuild, keeping the old one"); +} + +void DirectX12Renderer::PruneReplacedPipelines( + const std::vector& replaced) { + const auto inUse = [this](ID3D12PipelineState* pso) { + if (pso == m_defaultPipelineState || pso == m_shadowPassPSO || + pso == m_skyboxPipelineState || pso == m_debugLinePipelineState) + return true; + return std::ranges::any_of(DirectX12Shader::GetLiveShaders(), + [pso](const DirectX12Shader* shader) { + return shader->GetPipelineState() == pso; + }); + }; + std::erase_if(m_pipelineCache, [&](const auto& entry) { + ID3D12PipelineState* pso = entry.second.Get(); + return std::find(replaced.begin(), replaced.end(), pso) != + replaced.end() && + !inUse(pso); + }); +} +#endif + bool DirectX12Renderer::CreateShadowPassPSO() { if (m_shadowPassPSO) return true; if (!m_shadowVSCompiled) { m_shadowVSCompiled = true; - m_shadowVSBlob = CompileStage( - L"assets/shaders/default_shader_dx12.hlsl", "VS_Main", "vs_5_0"); + m_shadowVSBlob = + CompileStage(SHADOW_SHADER_PATH, VERTEX_ENTRY, VERTEX_PROFILE); } ID3DBlob* vs = m_shadowVSBlob ? m_shadowVSBlob.Get() : m_cachedVSBlob.Get(); if (!vs) { @@ -1095,10 +1284,7 @@ Shader* DirectX12Renderer::CreateShader(const std::string& shaderSource) { ID3DBlob* vs = shader->getVertexShaderBlob(); ID3DBlob* ps = shader->getPixelShaderBlob(); - PipelineKey key; - key.vsHash = HashBytecode(vs); - key.psHash = HashBytecode(ps); - if (ID3D12PipelineState* pso = GetPipeline(key, vs, ps)) { + if (ID3D12PipelineState* pso = GetShaderPipeline(vs, ps)) { shader->SetPipelineState( Microsoft::WRL::ComPtr(pso)); if (!m_defaultPipelineState) { @@ -1358,10 +1544,8 @@ bool DirectX12Renderer::CreateSkyboxPipelineState() { if (m_skyboxPipelineFailed) return false; m_skyboxPipelineFailed = true; - auto vs = - CompileStage(L"assets/shaders/skybox_dx12.hlsl", "VS_Main", "vs_5_0"); - auto ps = - CompileStage(L"assets/shaders/skybox_dx12.hlsl", "PS_Main", "ps_5_0"); + auto vs = CompileStage(SKYBOX_SHADER_PATH, VERTEX_ENTRY, VERTEX_PROFILE); + auto ps = CompileStage(SKYBOX_SHADER_PATH, PIXEL_ENTRY, PIXEL_PROFILE); if (!vs || !ps) return false; // No culling, depth write off with LEQUAL, and depth clip off so the @@ -1414,10 +1598,9 @@ bool DirectX12Renderer::CreateDebugLinePipelineState() { if (m_debugLinePipelineFailed) return false; m_debugLinePipelineFailed = true; - auto vs = CompileStage(L"assets/shaders/debug_line_dx12.hlsl", "VS_Main", - "vs_5_0"); - auto ps = CompileStage(L"assets/shaders/debug_line_dx12.hlsl", "PS_Main", - "ps_5_0"); + auto vs = + CompileStage(DEBUG_LINE_SHADER_PATH, VERTEX_ENTRY, VERTEX_PROFILE); + auto ps = CompileStage(DEBUG_LINE_SHADER_PATH, PIXEL_ENTRY, PIXEL_PROFILE); if (!vs || !ps) return false; PipelineKey key; diff --git a/src/Graphics/DirectX12/DirectX12Shader.cpp b/src/Graphics/DirectX12/DirectX12Shader.cpp index 05c4a32a..8d00e6b6 100644 --- a/src/Graphics/DirectX12/DirectX12Shader.cpp +++ b/src/Graphics/DirectX12/DirectX12Shader.cpp @@ -5,17 +5,80 @@ #include #include +#include +#ifdef SLEAK_HOT_RELOAD +#include +#include +#endif #include "../../include/private/Graphics/DirectX12/DirectX12Renderer.hpp" namespace Sleak { namespace RenderEngine { +namespace { + +constexpr const char* VERTEX_ENTRY = "VS_Main"; +constexpr const char* PIXEL_ENTRY = "PS_Main"; +constexpr const char* SPLIT_FILE_ENTRY = "Main"; +constexpr const char* VERTEX_PROFILE = "vs_5_0"; +constexpr const char* PIXEL_PROFILE = "ps_5_0"; + +#ifdef SLEAK_HOT_RELOAD +constexpr std::string_view DX12_STEM_SUFFIX = "_dx12"; + +/// Returns the file name up to its first '.', minus a trailing _dx12. +std::string StemFromPath(const std::string& path) { + std::string stem = std::filesystem::path(path).filename().string(); + stem = stem.substr(0, stem.find('.')); + if (stem.ends_with(DX12_STEM_SUFFIX)) { + stem.resize(stem.size() - DX12_STEM_SUFFIX.size()); + } + return stem; +} + +/// Returns the live shader registry, leaked to survive static teardown. +std::unordered_set& LiveShaders() { + static auto* shaders = new std::unordered_set(); + return *shaders; +} +#endif + +} // namespace + DirectX12Shader::DirectX12Shader(ID3D12Device* device) : m_device(device) { assert(m_device != nullptr); +#ifdef SLEAK_HOT_RELOAD + LiveShaders().insert(this); +#endif +} + +DirectX12Shader::~DirectX12Shader() { +#ifdef SLEAK_HOT_RELOAD + LiveShaders().erase(this); +#endif } -DirectX12Shader::~DirectX12Shader() = default; +#ifdef SLEAK_HOT_RELOAD +const std::unordered_set& DirectX12Shader::GetLiveShaders() { + return LiveShaders(); +} + +bool DirectX12Shader::RecompileStages( + Microsoft::WRL::ComPtr& vs, + Microsoft::WRL::ComPtr& ps) const { + if (m_sources.vertPath.empty() || m_sources.fragPath.empty()) return false; + return CompileSources(m_sources, vs, ps); +} + +void DirectX12Shader::ReplaceStages(Microsoft::WRL::ComPtr vs, + Microsoft::WRL::ComPtr ps, + ID3D12PipelineState* pso) { + m_vertexShaderBlob = std::move(vs); + m_pixelShaderBlob = std::move(ps); + m_pipelineState = pso; +} +#endif bool DirectX12Shader::compile(const std::string& shaderPath) { // Strip .hlsl extension if present, then use _dx12.hlsl @@ -26,33 +89,43 @@ bool DirectX12Shader::compile(const std::string& shaderPath) { } dx12Path += "_dx12.hlsl"; - // Compile shaders - if (!compileShader(dx12Path, "VS_Main", "vs_5_0", m_vertexShaderBlob)) { - SLEAK_ERROR("Failed to compile vertex shader!"); - return false; - } + return BuildStages({dx12Path, VERTEX_ENTRY, dx12Path, PIXEL_ENTRY}); +} - if (!compileShader(dx12Path, "PS_Main", "ps_5_0", m_pixelShaderBlob)) { - SLEAK_ERROR("Failed to compile pixel shader!"); - return false; - } +bool DirectX12Shader::compile(const std::string& vertPath, + const std::string& fragPath) { + return BuildStages( + {vertPath, SPLIT_FILE_ENTRY, fragPath, SPLIT_FILE_ENTRY}); +} +bool DirectX12Shader::BuildStages(StageSources sources) { + Microsoft::WRL::ComPtr vs; + Microsoft::WRL::ComPtr ps; + if (!CompileSources(sources, vs, ps)) return false; + + m_vertexShaderBlob = std::move(vs); + m_pixelShaderBlob = std::move(ps); +#ifdef SLEAK_HOT_RELOAD + m_vertStem = StemFromPath(sources.vertPath); + m_fragStem = StemFromPath(sources.fragPath); + m_sources = std::move(sources); +#endif return true; } -bool DirectX12Shader::compile(const std::string& vertPath, - const std::string& fragPath) { - // Compile shaders - if (!compileShader(vertPath, "Main", "vs_5_0", m_vertexShaderBlob)) { - SLEAK_ERROR("Failed to compile vertex shader!"); +bool DirectX12Shader::CompileSources( + const StageSources& sources, Microsoft::WRL::ComPtr& vs, + Microsoft::WRL::ComPtr& ps) const { + if (!compileShader(sources.vertPath, sources.vertEntry, VERTEX_PROFILE, + vs)) { + SLEAK_ERROR("Failed to compile vertex shader {}", sources.vertPath); return false; } - - if (!compileShader(fragPath, "Main", "ps_5_0", m_pixelShaderBlob)) { - SLEAK_ERROR("Failed to compile pixel shader!"); + if (!compileShader(sources.fragPath, sources.fragEntry, PIXEL_PROFILE, + ps)) { + SLEAK_ERROR("Failed to compile pixel shader {}", sources.fragPath); return false; } - return true; } @@ -89,10 +162,9 @@ void DirectX12Shader::SetRenderer(DirectX12Renderer* renderer) { m_renderer = renderer; } -bool DirectX12Shader::compileShader(const std::string& filePath, - const std::string& entryPoint, - const std::string& profile, - Microsoft::WRL::ComPtr& blob) { +bool DirectX12Shader::compileShader( + const std::string& filePath, const std::string& entryPoint, + const std::string& profile, Microsoft::WRL::ComPtr& blob) const { Microsoft::WRL::ComPtr errorBlob; HRESULT hr = D3DCompileFromFile( std::wstring(filePath.begin(), filePath.end()).c_str(), @@ -107,7 +179,11 @@ bool DirectX12Shader::compileShader(const std::string& filePath, if (FAILED(hr)) { if (errorBlob) { - SLEAK_ERROR("Shader error: {}", (char*)errorBlob->GetBufferPointer()); + SLEAK_ERROR("Shader error in {} ({}): {}", filePath, entryPoint, + (char*)errorBlob->GetBufferPointer()); + } else { + SLEAK_ERROR("Shader error in {} ({}): 0x{:08X}", filePath, + entryPoint, static_cast(hr)); } return false; } diff --git a/src/Graphics/OpenGL/OpenGLRenderer.cpp b/src/Graphics/OpenGL/OpenGLRenderer.cpp index 48c316d3..225ca347 100644 --- a/src/Graphics/OpenGL/OpenGLRenderer.cpp +++ b/src/Graphics/OpenGL/OpenGLRenderer.cpp @@ -1,25 +1,29 @@ #include "../../include/private/Graphics/OpenGL/OpenGLRenderer.hpp" -#include "../../include/private/Graphics/OpenGL/OpenGLBuffer.hpp" -#include "../../include/private/Graphics/OpenGL/OpenGLShader.hpp" -#include "../../include/private/Graphics/OpenGL/OpenGLTexture.hpp" -#include "../../include/private/Graphics/OpenGL/OpenGLCubemapTexture.hpp" -#include "../../include/private/Graphics/OpenGL/OpenGLIBL.hpp" -#include -#include -#include "Graphics/Common/ResourceManager.hpp" -#include "Graphics/Common/ConstantBuffer.hpp" -#include "Graphics/Common/RenderCommandQueue.hpp" -#include "Graphics/Common/SSAOKernel.hpp" + +#include + #include #include +#include #include -#include #include -#include -#include -#include +#include +#include +#include +#include #include #include +#include + +#include "../../include/private/Graphics/OpenGL/OpenGLBuffer.hpp" +#include "../../include/private/Graphics/OpenGL/OpenGLCubemapTexture.hpp" +#include "../../include/private/Graphics/OpenGL/OpenGLIBL.hpp" +#include "../../include/private/Graphics/OpenGL/OpenGLShader.hpp" +#include "../../include/private/Graphics/OpenGL/OpenGLTexture.hpp" +#include "Graphics/Common/ConstantBuffer.hpp" +#include "Graphics/Common/RenderCommandQueue.hpp" +#include "Graphics/Common/ResourceManager.hpp" +#include "Graphics/Common/SSAOKernel.hpp" namespace Sleak { namespace RenderEngine { @@ -218,6 +222,35 @@ void OpenGLRenderer::Cleanup() { } } +#ifdef SLEAK_HOT_RELOAD +bool OpenGLRenderer::ReloadShaders(const std::vector& stems) { + for (OpenGLShader* shader : OpenGLShader::GetLiveShaders()) { + const bool changed = + std::ranges::any_of(stems, [shader](const std::string& stem) { + return shader->UsesStem(stem); + }); + if (!changed) continue; + if (shader->Reload()) { + SLEAK_INFO("Reloaded OpenGL shader '{}'", shader->GetStem()); + } else { + SLEAK_WARN("Shader '{}' failed to reload, keeping the old program", + shader->GetStem()); + } + } + for (const std::string& stem : stems) { + const bool used = + std::ranges::any_of(OpenGLShader::GetLiveShaders(), + [&stem](const OpenGLShader* shader) { + return shader->UsesStem(stem); + }); + if (!used) + SLEAK_WARN("Hot reload: no loaded OpenGL shader is built from '{}'", + stem); + } + return true; +} +#endif + void OpenGLRenderer::Resize(uint32_t width, uint32_t height) { glViewport(0, 0, width, height); // Recreate MSAA FBO at new size diff --git a/src/Graphics/OpenGL/OpenGLShader.cpp b/src/Graphics/OpenGL/OpenGLShader.cpp index 65984ac4..0d0c55d6 100644 --- a/src/Graphics/OpenGL/OpenGLShader.cpp +++ b/src/Graphics/OpenGL/OpenGLShader.cpp @@ -1,20 +1,76 @@ #include "../../include/private/Graphics/OpenGL/OpenGLShader.hpp" + #include +#include #include #include +#include namespace Sleak { namespace RenderEngine { -OpenGLShader::OpenGLShader() {} +namespace { + +constexpr std::string_view GL_STEM_SUFFIX = "_gl"; + +/// Returns the file name up to its first '.', minus a trailing _gl. +std::string StemFromPath(const std::string& path) { + std::string stem = std::filesystem::path(path).filename().string(); + stem = stem.substr(0, stem.find('.')); + if (stem.ends_with(GL_STEM_SUFFIX)) { + stem.resize(stem.size() - GL_STEM_SUFFIX.size()); + } + return stem; +} + +/// Returns the info log of a shader or program object. +std::string ReadInfoLog(GLuint object, PFNGLGETSHADERIVPROC getParam, + PFNGLGETSHADERINFOLOGPROC getLog) { + GLint length = 0; + getParam(object, GL_INFO_LOG_LENGTH, &length); + if (length <= 0) return {}; + std::string log(static_cast(length), '\0'); + GLsizei written = 0; + getLog(object, length, &written, log.data()); + log.resize(static_cast(written)); + while (!log.empty() && (log.back() == '\n' || log.back() == '\r')) { + log.pop_back(); + } + return log; +} + +#ifdef SLEAK_HOT_RELOAD +/// Returns the live shader registry, leaked to survive static teardown. +std::unordered_set& LiveShaders() { + static auto* shaders = new std::unordered_set(); + return *shaders; +} +#endif + +} // namespace + +OpenGLShader::OpenGLShader() { +#ifdef SLEAK_HOT_RELOAD + LiveShaders().insert(this); +#endif +} OpenGLShader::~OpenGLShader() { +#ifdef SLEAK_HOT_RELOAD + LiveShaders().erase(this); +#endif if (m_program != 0) { glDeleteProgram(m_program); m_program = 0; } } +#ifdef SLEAK_HOT_RELOAD +const std::unordered_set& OpenGLShader::GetLiveShaders() { + return LiveShaders(); +} +#endif + bool OpenGLShader::compile(const std::string& shaderPath) { // Strip .hlsl extension if present, then use _gl.vert/_gl.frag std::string basePath = shaderPath; @@ -27,46 +83,61 @@ bool OpenGLShader::compile(const std::string& shaderPath) { bool OpenGLShader::compile(const std::string& vertPath, const std::string& fragPath) { - std::string vertSource = ReadFile(vertPath); - std::string fragSource = ReadFile(fragPath); + m_vertPath = vertPath; + m_fragPath = fragPath; + m_vertStem = StemFromPath(vertPath); + m_fragStem = StemFromPath(fragPath); + return BuildProgram(); +} - if (vertSource.empty() || fragSource.empty()) { - SLEAK_ERROR("Failed to read shader files: {} and {}", vertPath, - fragPath); - return false; - } +bool OpenGLShader::Reload() { + if (m_vertPath.empty() || m_fragPath.empty()) return false; + return BuildProgram(); +} - GLuint vertShader = 0, fragShader = 0; +bool OpenGLShader::BuildProgram() { + const std::string vertSource = ReadFile(m_vertPath); + const std::string fragSource = ReadFile(m_fragPath); - if (!CompileShaderSource(vertSource, GL_VERTEX_SHADER, vertShader)) { + if (vertSource.empty() || fragSource.empty()) { + SLEAK_ERROR("Failed to read shader files: {} and {}", m_vertPath, + m_fragPath); return false; } - if (!CompileShaderSource(fragSource, GL_FRAGMENT_SHADER, fragShader)) { - glDeleteShader(vertShader); + + GLuint vertShader = 0; + GLuint fragShader = 0; + if (!CompileShaderSource(vertSource, GL_VERTEX_SHADER, m_vertPath, + vertShader)) { return false; } - - m_program = glCreateProgram(); - glAttachShader(m_program, vertShader); - glAttachShader(m_program, fragShader); - glLinkProgram(m_program); - - GLint success = 0; - glGetProgramiv(m_program, GL_LINK_STATUS, &success); - if (!success) { - char infoLog[512]; - glGetProgramInfoLog(m_program, 512, nullptr, infoLog); - SLEAK_ERROR("Shader program linking failed: {}", infoLog); - glDeleteProgram(m_program); - m_program = 0; + if (!CompileShaderSource(fragSource, GL_FRAGMENT_SHADER, m_fragPath, + fragShader)) { glDeleteShader(vertShader); - glDeleteShader(fragShader); return false; } + GLuint program = glCreateProgram(); + glAttachShader(program, vertShader); + glAttachShader(program, fragShader); + glLinkProgram(program); + glDetachShader(program, vertShader); + glDetachShader(program, fragShader); glDeleteShader(vertShader); glDeleteShader(fragShader); + GLint linked = GL_FALSE; + glGetProgramiv(program, GL_LINK_STATUS, &linked); + if (linked != GL_TRUE) { + SLEAK_ERROR("Shader program linking failed ({} + {}): {}", m_vertPath, + m_fragPath, + ReadInfoLog(program, glGetProgramiv, glGetProgramInfoLog)); + glDeleteProgram(program); + return false; + } + + if (m_program != 0) glDeleteProgram(m_program); + m_program = program; return true; } @@ -76,19 +147,19 @@ void OpenGLShader::bind() { } } -bool OpenGLShader::CompileShaderSource(const std::string& source, - GLenum type, GLuint& shader) { +bool OpenGLShader::CompileShaderSource(const std::string& source, GLenum type, + const std::string& path, + GLuint& shader) { shader = glCreateShader(type); const char* src = source.c_str(); glShaderSource(shader, 1, &src, nullptr); glCompileShader(shader); - GLint success = 0; + GLint success = GL_FALSE; glGetShaderiv(shader, GL_COMPILE_STATUS, &success); - if (!success) { - char infoLog[512]; - glGetShaderInfoLog(shader, 512, nullptr, infoLog); - SLEAK_ERROR("Shader compilation failed: {}", infoLog); + if (success != GL_TRUE) { + SLEAK_ERROR("Shader compilation failed ({}): {}", path, + ReadInfoLog(shader, glGetShaderiv, glGetShaderInfoLog)); glDeleteShader(shader); shader = 0; return false; diff --git a/src/Graphics/Vulkan/VulkanPipelines.cpp b/src/Graphics/Vulkan/VulkanPipelines.cpp index 8d5d7685..59f64313 100644 --- a/src/Graphics/Vulkan/VulkanPipelines.cpp +++ b/src/Graphics/Vulkan/VulkanPipelines.cpp @@ -1446,5 +1446,147 @@ void VulkanRenderer::EndSkinnedPass() { vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); } +#ifdef SLEAK_HOT_RELOAD +/// Rebuilds every pipeline from the SPIR-V on disk; this version ignores the +/// stems and rebuilds them all. +bool VulkanRenderer::ReloadShaders(const std::vector& stems) { + (void)stems; + vkDeviceWaitIdle(device); + + const PipelineGroup groups[] = { + {"default_shader", + {&pipeline}, + {}, + {&simpleShader}, + &VulkanRenderer::CreateGraphicsPipeline}, + {"skybox", + {&skyboxPipeline}, + {}, + {&skyboxShader}, + &VulkanRenderer::CreateSkyboxPipeline}, + {"debug_line", + {&debugLinePipeline}, + {}, + {&debugLineShader}, + &VulkanRenderer::CreateDebugLinePipeline}, + {"skinned_shader", + {&skinnedPipeline}, + {}, + {&skinnedShader}, + &VulkanRenderer::CreateSkinnedPipeline}, + {"shadow_depth", + {&m_shadowPipeline}, + {}, + {&m_shadowShader}, + &VulkanRenderer::CreateShadowPipeline}, + {"gbuffer", + {&m_gbufferPipeline}, + {}, + {&m_gbufferShader}, + &VulkanRenderer::CreateGBufferPipeline}, + {"skinned gbuffer", + {&m_skinnedGbufferPipeline}, + {}, + {}, + &VulkanRenderer::CreateSkinnedGbufferPipeline}, + {"lighting_pass", + {&m_lightingPipeline}, + {&m_lightingPipelineLayout}, + {&m_lightingShader}, + &VulkanRenderer::CreateLightingPipeline}, + {"ssao", + {&m_ssaoPipeline, &m_ssaoBlurPipeline}, + {&m_ssaoPipelineLayout, &m_ssaoBlurPipelineLayout}, + {&m_ssaoShader, &m_ssaoBlurShader}, + &VulkanRenderer::CreateSSAOPipelines}, + {"ssr", + {&m_ssrPipeline}, + {}, + {&m_ssrShader}, + &VulkanRenderer::CreateSSRPipeline}, + {"taa", + {&m_taaPipeline}, + {}, + {&m_taaShader}, + &VulkanRenderer::CreateTAAPipeline}, + {"bloom", + {&m_bloomThresholdPipeline, &m_bloomDownsamplePipeline, + &m_bloomUpsamplePipeline, &m_bloomCompositePipeline}, + {&m_bloomFilterPipelineLayout, &m_bloomCompositePipelineLayout}, + {&m_bloomThresholdShader, &m_bloomDownsampleShader, + &m_bloomUpsampleShader, &m_bloomCompositeShader}, + &VulkanRenderer::CreateBloomPipelines}, + }; + for (const PipelineGroup& group : groups) ReloadPipelineGroup(group); + ReloadCustomFormatPipelines(); + return true; +} + +/// Rebuilds a live group through its create function, keeping the old +/// objects when the rebuild fails. +void VulkanRenderer::ReloadPipelineGroup(const PipelineGroup& group) { + // Pass is off or still builds lazily from disk + if (*group.pipelines.front() == VK_NULL_HANDLE) return; + + // Detach the live objects so the create function builds new ones + std::vector pipelines; + std::vector layouts; + std::vector shaders; + for (VkPipeline* pipe : group.pipelines) + pipelines.push_back(std::exchange(*pipe, VK_NULL_HANDLE)); + for (VkPipelineLayout* layout : group.layouts) + layouts.push_back(std::exchange(*layout, VK_NULL_HANDLE)); + for (VulkanShader** shader : group.shaders) + shaders.push_back(std::exchange(*shader, nullptr)); + + bool built = (this->*group.create)(); + for (VkPipeline* pipe : group.pipelines) + built = built && *pipe != VK_NULL_HANDLE; + if (!built) + SLEAK_ERROR("Hot reload: {} rebuild failed, keeping the previous one", + group.name); + + // Destroy the losing generation + for (size_t i = 0; i < pipelines.size(); ++i) { + if (!built) std::swap(*group.pipelines[i], pipelines[i]); + if (pipelines[i]) vkDestroyPipeline(device, pipelines[i], nullptr); + } + for (size_t i = 0; i < layouts.size(); ++i) { + if (!built) std::swap(*group.layouts[i], layouts[i]); + if (layouts[i]) vkDestroyPipelineLayout(device, layouts[i], nullptr); + } + for (size_t i = 0; i < shaders.size(); ++i) { + if (!built) std::swap(*group.shaders[i], shaders[i]); + delete shaders[i]; + } +} + +/// Rebuilds every cached custom-format variant, keeping the old pipeline of +/// a variant that fails. +void VulkanRenderer::ReloadCustomFormatPipelines() { + for (size_t i = 0; i < m_customFormatPipelines.size(); ++i) { + const auto format = static_cast(i + 1); + CustomFormatPipelines& pipes = m_customFormatPipelines[i]; + // Variants that failed before retry on next use + pipes.failed.fill(false); + const VertexLayoutDesc* desc = VertexFormatRegistry::Get(format); + if (!desc) continue; + for (size_t slot = 0; slot < CUSTOM_PASS_COUNT; ++slot) { + VkPipeline& live = pipes.pipelines[slot]; + if (live == VK_NULL_HANDLE) continue; + const VkPipeline rebuilt = BuildCustomFormatPipeline( + format, *desc, static_cast(slot)); + if (rebuilt == VK_NULL_HANDLE) { + SLEAK_ERROR( + "Hot reload: vertex format {} keeps its previous pipeline", + format); + continue; + } + vkDestroyPipeline(device, live, nullptr); + live = rebuilt; + } + } +} +#endif } } diff --git a/src/Graphics/Vulkan/VulkanSSR.cpp b/src/Graphics/Vulkan/VulkanSSR.cpp index 6728e67d..b55e45a7 100644 --- a/src/Graphics/Vulkan/VulkanSSR.cpp +++ b/src/Graphics/Vulkan/VulkanSSR.cpp @@ -280,6 +280,16 @@ bool VulkanRenderer::CreateSSRResources() { } // ---- 8. Pipeline ---- + if (!CreateSSRPipeline()) return false; + + m_ssrResourcesCreated = true; + SLEAK_INFO("SSR resources created ({}x{})", scExtent.width, + scExtent.height); + return true; +} + +/// Compiles the SSR shaders and creates the SSR pipeline. +bool VulkanRenderer::CreateSSRPipeline() { { m_ssrShader = new VulkanShader(device); if (!m_ssrShader->compile("assets/shaders/ssr.vert.spv", @@ -356,9 +366,6 @@ bool VulkanRenderer::CreateSSRResources() { return false; } } - - m_ssrResourcesCreated = true; - SLEAK_INFO("SSR resources created ({}x{})", scExtent.width, scExtent.height); return true; } diff --git a/src/Graphics/Vulkan/VulkanTAA.cpp b/src/Graphics/Vulkan/VulkanTAA.cpp index 9cdc7877..ea402d92 100644 --- a/src/Graphics/Vulkan/VulkanTAA.cpp +++ b/src/Graphics/Vulkan/VulkanTAA.cpp @@ -288,6 +288,16 @@ bool VulkanRenderer::CreateTAAResources() { } // ---- 10. Pipeline ---- + if (!CreateTAAPipeline()) return false; + + m_taaResourcesCreated = true; + SLEAK_INFO("TAA resources created ({}x{})", scExtent.width, + scExtent.height); + return true; +} + +/// Compiles the TAA shaders and creates the TAA resolve pipeline. +bool VulkanRenderer::CreateTAAPipeline() { { m_taaShader = new VulkanShader(device); if (!m_taaShader->compile("assets/shaders/taa.vert.spv", @@ -358,9 +368,6 @@ bool VulkanRenderer::CreateTAAResources() { return false; } } - - m_taaResourcesCreated = true; - SLEAK_INFO("TAA resources created ({}x{})", scExtent.width, scExtent.height); return true; }