From 10b00b05252592bf3b30ce0fd780af0e656a0044 Mon Sep 17 00:00:00 2001 From: Bram Date: Thu, 8 Jan 2026 02:43:34 +0100 Subject: [PATCH] WE GOT TEXTURES BABY --- destrum/CMakeLists.txt | 2 + destrum/assets_src/shaders/mesh.frag | 7 +- destrum/assets_src/textures/kobe.png | Bin 0 -> 44413 bytes destrum/include/destrum/App.h | 4 +- .../destrum/Graphics/BindlessSetManager.h | 33 ++ destrum/include/destrum/Graphics/GfxDevice.h | 7 + destrum/include/destrum/Graphics/ImageCache.h | 3 +- destrum/include/destrum/Graphics/Material.h | 2 +- .../include/destrum/Graphics/MaterialCache.h | 10 +- .../destrum/Graphics/MeshDrawCommand.h | 2 +- destrum/include/destrum/Graphics/Renderer.h | 8 +- .../include/destrum/Graphics/Resources/Mesh.h | 12 +- destrum/include/destrum/Graphics/ids.h | 4 +- destrum/include/destrum/Input/InputManager.h | 98 ++++-- destrum/src/App.cpp | 50 +-- destrum/src/Graphics/BindlessSetManager.cpp | 184 +++++++++++ destrum/src/Graphics/Camera.cpp | 239 ++++++-------- destrum/src/Graphics/GfxDevice.cpp | 41 +++ destrum/src/Graphics/ImageCache.cpp | 2 +- destrum/src/Graphics/MaterialCache.cpp | 77 +++++ .../src/Graphics/Pipelines/MeshPipeline.cpp | 9 +- destrum/src/Graphics/Renderer.cpp | 4 +- destrum/src/Graphics/Util.cpp | 10 + destrum/src/Input/InputManager.cpp | 292 +++++++++++++++--- 24 files changed, 828 insertions(+), 272 deletions(-) create mode 100644 destrum/assets_src/textures/kobe.png create mode 100644 destrum/include/destrum/Graphics/BindlessSetManager.h create mode 100644 destrum/src/Graphics/BindlessSetManager.cpp create mode 100644 destrum/src/Graphics/MaterialCache.cpp diff --git a/destrum/CMakeLists.txt b/destrum/CMakeLists.txt index 735a860..a95b7cb 100644 --- a/destrum/CMakeLists.txt +++ b/destrum/CMakeLists.txt @@ -3,12 +3,14 @@ add_subdirectory(third_party) set(SRC_FILES "src/App.cpp" + "src/Graphics/BindlessSetManager.cpp" "src/Graphics/Camera.cpp" "src/Graphics/GfxDevice.cpp" "src/Graphics/ImageCache.cpp" "src/Graphics/ImageLoader.cpp" "src/Graphics/ImmediateExecuter.cpp" "src/Graphics/Init.cpp" + "src/Graphics/MaterialCache.cpp" "src/Graphics/MeshCache.cpp" "src/Graphics/Pipeline.cpp" "src/Graphics/Renderer.cpp" diff --git a/destrum/assets_src/shaders/mesh.frag b/destrum/assets_src/shaders/mesh.frag index ef4b52c..83e219e 100644 --- a/destrum/assets_src/shaders/mesh.frag +++ b/destrum/assets_src/shaders/mesh.frag @@ -18,7 +18,10 @@ void main() { MaterialData material = pcs.sceneData.materials.data[pcs.materialID]; -// vec4 diffuse = sampleTexture2DLinear(material.diffuseTex, inUV); + vec4 diffuse = sampleTexture2DLinear(material.diffuseTex, inUV); + outFragColor = diffuse; + + // if (diffuse.a < 0.1) { // discard; // } @@ -121,5 +124,5 @@ void main() // // fragColor = normal; // #endif - outFragColor = vec4(1.0f, 0.0f, 0.0f, 1.0f); +// outFragColor = vec4(1.0f, 0.0f, 0.0f, 1.0f); } diff --git a/destrum/assets_src/textures/kobe.png b/destrum/assets_src/textures/kobe.png new file mode 100644 index 0000000000000000000000000000000000000000..24a718cd1b87b3f7723784157ab0e35869d5121c GIT binary patch literal 44413 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 + +#include + +struct GPUImage; + +class BindlessSetManager { +public: + void init(VkDevice device, float maxAnisotropy); + void cleanup(VkDevice device); + + VkDescriptorSetLayout getDescSetLayout() const { return descSetLayout; } + const VkDescriptorSet& getDescSet() const { return descSet; } + + void addImage(VkDevice device, std::uint32_t id, const VkImageView imageView); + void addSampler(VkDevice device, std::uint32_t id, VkSampler sampler); + +private: + void initDefaultSamplers(VkDevice device, float maxAnisotropy); + + VkDescriptorPool descPool; + VkDescriptorSetLayout descSetLayout; + VkDescriptorSet descSet; + + VkSampler nearestSampler; + VkSampler linearSampler; + VkSampler shadowMapSampler; +}; + +#endif //BINDLESSSETMANAGER_H diff --git a/destrum/include/destrum/Graphics/GfxDevice.h b/destrum/include/destrum/Graphics/GfxDevice.h index 4f2ae03..538ac9c 100644 --- a/destrum/include/destrum/Graphics/GfxDevice.h +++ b/destrum/include/destrum/Graphics/GfxDevice.h @@ -57,6 +57,11 @@ public: void waitIdle(); + BindlessSetManager& getBindlessSetManager(); + VkDescriptorSetLayout getBindlessDescSetLayout() const; + const VkDescriptorSet& getBindlessDescSet() const; + void bindBindlessDescSet(VkCommandBuffer cmd, VkPipelineLayout layout) const; + void immediateSubmit(ImmediateExecuteFunction&& f) const; vkb::Device getDevice() const { return device; } @@ -96,6 +101,8 @@ public: GPUImage loadImageFromFileRaw(const std::filesystem::path& path, VkFormat format, VkImageUsageFlags usage, bool mipMap) const; void destroyImage(const GPUImage& image) const; + ImageID getWhiteTextureID() { return whiteImageId; } + private: vkb::Instance instance; vkb::PhysicalDevice physicalDevice; diff --git a/destrum/include/destrum/Graphics/ImageCache.h b/destrum/include/destrum/Graphics/ImageCache.h index 69c16c8..3157a88 100644 --- a/destrum/include/destrum/Graphics/ImageCache.h +++ b/destrum/include/destrum/Graphics/ImageCache.h @@ -9,6 +9,7 @@ #include #include +#include // #include @@ -34,7 +35,7 @@ public: void destroyImages(); - // BindlessSetManager bindlessSetManager; + BindlessSetManager bindlessSetManager; void setErrorImageId(ImageID id) { errorImageId = id; } diff --git a/destrum/include/destrum/Graphics/Material.h b/destrum/include/destrum/Graphics/Material.h index f6ca1b6..a6de891 100644 --- a/destrum/include/destrum/Graphics/Material.h +++ b/destrum/include/destrum/Graphics/Material.h @@ -5,7 +5,7 @@ #include struct MaterialData { - glm::vec3 baseColor; + glm::vec4 baseColor; glm::vec4 metalRoughnessEmissive; std::uint32_t diffuseTex; std::uint32_t normalTex; diff --git a/destrum/include/destrum/Graphics/MaterialCache.h b/destrum/include/destrum/Graphics/MaterialCache.h index 35f13d3..464c2e8 100644 --- a/destrum/include/destrum/Graphics/MaterialCache.h +++ b/destrum/include/destrum/Graphics/MaterialCache.h @@ -17,11 +17,11 @@ public: void init(GfxDevice& gfxDevice); void cleanup(GfxDevice& gfxDevice); - MaterialId addMaterial(GfxDevice& gfxDevice, Material material); - const Material& getMaterial(MaterialId id) const; + MaterialID addMaterial(GfxDevice& gfxDevice, Material material); + const Material& getMaterial(MaterialID id) const; - MaterialId getFreeMaterialId() const; - MaterialId getPlaceholderMaterialId() const; + MaterialID getFreeMaterialId() const; + MaterialID getPlaceholderMaterialId() const; const GPUBuffer& getMaterialDataBuffer() const { return materialDataBuffer; } VkDeviceAddress getMaterialDataBufferAddress() const { return materialDataBuffer.address; } @@ -33,7 +33,7 @@ private: GPUBuffer materialDataBuffer; // material which is used for meshes without materials - MaterialId placeholderMaterialId{NULL_MATERIAL_ID}; + MaterialID placeholderMaterialId{NULL_MATERIAL_ID}; ImageID defaultNormalMapTextureID{NULL_IMAGE_ID}; }; diff --git a/destrum/include/destrum/Graphics/MeshDrawCommand.h b/destrum/include/destrum/Graphics/MeshDrawCommand.h index f3aff65..28bafe0 100644 --- a/destrum/include/destrum/Graphics/MeshDrawCommand.h +++ b/destrum/include/destrum/Graphics/MeshDrawCommand.h @@ -15,7 +15,7 @@ struct MeshDrawCommand { // If set - mesh will be drawn with overrideMaterialId // instead of whatever material the mesh has - MaterialId materialId{NULL_MATERIAL_ID}; + MaterialID materialId{NULL_MATERIAL_ID}; bool castShadow{true}; diff --git a/destrum/include/destrum/Graphics/Renderer.h b/destrum/include/destrum/Graphics/Renderer.h index 06682b2..ed681c4 100644 --- a/destrum/include/destrum/Graphics/Renderer.h +++ b/destrum/include/destrum/Graphics/Renderer.h @@ -23,7 +23,7 @@ public: float fogDensity; }; - explicit GameRenderer(MeshCache& meshCache); + explicit GameRenderer(MeshCache& meshCache, MaterialCache& matCache); void init(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize); void beginDrawing(GfxDevice& gfxDevice); @@ -32,7 +32,7 @@ public: void draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& camera, const SceneData& sceneData); void cleanup(); - void drawMesh(MeshID id, const glm::mat4& transform, MaterialId materialId); + void drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId); const GPUImage& getDrawImage(const GfxDevice& gfx_device) const; void resize(GfxDevice& gfxDevice, const glm::ivec2& newSize) { @@ -43,7 +43,7 @@ private: void createDrawImage(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize, bool firstCreate); MeshCache& meshCache; - // MaterialCache& materialCache; + MaterialCache& materialCache; std::vector meshDrawCommands; std::vector sortedMeshDrawCommands; @@ -77,8 +77,6 @@ private: NBuffer sceneDataBuffer; - MaterialCache materialCache; - std::unique_ptr meshPipeline; }; diff --git a/destrum/include/destrum/Graphics/Resources/Mesh.h b/destrum/include/destrum/Graphics/Resources/Mesh.h index f0f0bd6..9d847a2 100644 --- a/destrum/include/destrum/Graphics/Resources/Mesh.h +++ b/destrum/include/destrum/Graphics/Resources/Mesh.h @@ -94,12 +94,12 @@ static std::vector vertices = { }; static std::vector indices = { - 0, 1, 2, 2, 3, 0, // Front - 4, 5, 6, 6, 7, 4, // Back - 8, 9,10, 10,11, 8, // Right - 12,13,14, 14,15,12, // Left - 16,17,18, 18,19,16, // Top - 20,21,22, 22,23,20 // Bottom + 0, 1, 2, 2, 3, 0, // Front (+Z) + 4, 7, 6, 6, 5, 4, // Back (-Z) + 8, 9,10, 10,11, 8, // Right (+X) + 12,13,14, 14,15,12, // Left (-X) + 16,17,18, 18,19,16, // Top (+Y) + 20,21,22, 22,23,20 // Bottom(-Y) }; diff --git a/destrum/include/destrum/Graphics/ids.h b/destrum/include/destrum/Graphics/ids.h index 6501758..0a26d11 100644 --- a/destrum/include/destrum/Graphics/ids.h +++ b/destrum/include/destrum/Graphics/ids.h @@ -10,8 +10,8 @@ constexpr MeshID NULL_MESH_ID = std::numeric_limits::max(); using ImageID = std::uint16_t; constexpr ImageID NULL_IMAGE_ID = std::numeric_limits::max(); -using MaterialId = std::uint32_t; -constexpr MaterialId NULL_MATERIAL_ID = std::numeric_limits::max(); +using MaterialID = std::uint32_t; +constexpr MaterialID NULL_MATERIAL_ID = std::numeric_limits::max(); using BindlessID = std::uint32_t; constexpr BindlessID NULL_BINDLESS_ID = std::numeric_limits::max(); diff --git a/destrum/include/destrum/Input/InputManager.h b/destrum/include/destrum/Input/InputManager.h index 016d6b5..4195146 100644 --- a/destrum/include/destrum/Input/InputManager.h +++ b/destrum/include/destrum/Input/InputManager.h @@ -6,59 +6,81 @@ #include #include #include +#include +#include -class InputManager { +#include + +class InputManager: public Singleton { public: - // Call once at startup (optional, but convenient) + friend class Singleton; + void Init(); - - // Call at the start of every frame void BeginFrame(); - - // Feed SDL events into this (call for each SDL_PollEvent) void ProcessEvent(const SDL_Event& e); - - // Call at end of frame if you want (not required) void EndFrame() {} - // ---- Queries ---- - bool IsKeyDown(SDL_Scancode sc) const; // held - bool WasKeyPressed(SDL_Scancode sc) const; // pressed this frame - bool WasKeyReleased(SDL_Scancode sc) const; // released this frame + bool IsKeyDown(SDL_Scancode sc) const; + bool WasKeyPressed(SDL_Scancode sc) const; + bool WasKeyReleased(SDL_Scancode sc) const; - bool IsMouseDown(Uint8 button) const; // held (SDL_BUTTON_LEFT etc.) - bool WasMousePressed(Uint8 button) const; // pressed this frame - bool WasMouseReleased(Uint8 button) const; // released this frame + bool IsMouseDown(Uint8 button) const; + bool WasMousePressed(Uint8 button) const; + bool WasMouseReleased(Uint8 button) const; - // Mouse position (window space) int MouseX() const { return m_mouseX; } int MouseY() const { return m_mouseY; } int MouseDeltaX() const { return m_mouseDX; } int MouseDeltaY() const { return m_mouseDY; } - // Mouse wheel (accumulated per frame) int WheelX() const { return m_wheelX; } int WheelY() const { return m_wheelY; } - // ---- Text input ---- void StartTextInput(); void StopTextInput(); bool IsTextInputActive() const { return m_textInputActive; } - const std::string& GetTextInput() const { return m_textInput; } // captured this frame + const std::string& GetTextInput() const { return m_textInput; } - // ---- Action mapping ---- - enum class Device { Keyboard, MouseButton, MouseWheel }; + bool IsPadButtonDown(SDL_JoystickID id, SDL_GameControllerButton btn) const; + bool WasPadButtonPressed(SDL_JoystickID id, SDL_GameControllerButton btn) const; + bool WasPadButtonReleased(SDL_JoystickID id, SDL_GameControllerButton btn) const; + + // axis in [-1..1] + float GetPadAxis(SDL_JoystickID id, SDL_GameControllerAxis axis) const; + + enum class Device { Keyboard, MouseButton, MouseWheel, GamepadButton, GamepadAxis }; enum class ButtonState { Down, Pressed, Released }; struct Binding { Device device; - int code; // SDL_Scancode for keyboard, Uint8 for mouse button, wheel axis sign encoding - // For MouseWheel: code = +1/-1 for Y, +2/-2 for X (simple encoding) + + // Keyboard: code = SDL_Scancode + // MouseButton: code = Uint8 + // MouseWheel: code = +1/-1 => Y, +2/-2 => X + // + // GamepadButton: code = (padIndex<<8) | button (padIndex: 0..255) + // GamepadAxis: code = (padIndex<<8) | axis (axis: SDL_GameControllerAxis) + int code = 0; + + // For GamepadAxis "button-like" checks: + // sign: -1 means negative direction, +1 positive direction + // threshold: absolute value in [0..1] that must be exceeded + int sign = 0; // only used for GamepadAxis + float threshold = 0.5f; // only used for GamepadAxis }; void BindAction(const std::string& action, const Binding& binding); bool GetAction(const std::string& action, ButtonState state) const; + // Convenience helpers to build controller bindings + static Binding PadButton(uint8_t padIndex, SDL_GameControllerButton btn); + static Binding PadAxis(uint8_t padIndex, SDL_GameControllerAxis axis, int sign, float threshold = 0.5f); + + // Which controller is "padIndex 0/1/2..."? (based on connection order) + SDL_JoystickID GetPadInstanceId(uint8_t padIndex) const; + + void SetAxisDeadzone(int deadzone) { m_axisDeadzone = deadzone; } // 0..32767 + private: // Key states std::unordered_set m_keysDown; @@ -75,18 +97,42 @@ private: int m_prevMouseX = 0, m_prevMouseY = 0; int m_mouseDX = 0, m_mouseDY = 0; - // Wheel (per-frame) int m_wheelX = 0, m_wheelY = 0; - // Text input (per-frame) bool m_textInputActive = false; std::string m_textInput; - // Action bindings std::unordered_map> m_bindings; + struct PadState { + SDL_GameController* controller = nullptr; + SDL_JoystickID instanceId = -1; + + std::unordered_set buttonsDown; + std::unordered_set buttonsPressed; + std::unordered_set buttonsReleased; + + // raw s16 axes (SDL gives Sint16) + std::array axes{}; + std::array prevAxes{}; + }; + + // Map instanceId -> PadState + std::unordered_map m_pads; + + // padIndex -> instanceId (connection order list) + std::vector m_padOrder; + + int m_axisDeadzone = 8000; // typical deadzone + private: bool QueryBinding(const Binding& b, ButtonState state) const; + + void AddController(int deviceIndex); + void RemoveController(SDL_JoystickID instanceId); + + uint8_t GetPadIndexFromCode(int code) const { return static_cast((code >> 8) & 0xFF); } + uint8_t GetLow8FromCode(int code) const { return static_cast(code & 0xFF); } }; #endif //INPUTMANAGER_H diff --git a/destrum/src/App.cpp b/destrum/src/App.cpp index c5956c7..a68e458 100644 --- a/destrum/src/App.cpp +++ b/destrum/src/App.cpp @@ -7,7 +7,7 @@ #include "glm/gtx/transform.hpp" #include "spdlog/spdlog.h" -App::App(): renderer{meshCache} { +App::App(): renderer{meshCache, materialCache} { } void App::init(const AppParams& params) { @@ -34,6 +34,7 @@ void App::init(const AppParams& params) { } gfxDevice.init(window, params.appName, false); + materialCache.init(gfxDevice); renderer.init(gfxDevice, params.renderSize); //Read whole file @@ -47,14 +48,22 @@ void App::init(const AppParams& params) { testMeshID = meshCache.addMesh(gfxDevice, testMesh); spdlog::info("TestMesh uploaded with id: {}", testMeshID); + const auto testimgpath = AssetFS::GetInstance().GetFullPath("engine://textures/kobe.png"); + auto testimgID = gfxDevice.loadImageFromFile(testimgpath); + spdlog::info("Test image loaded with id: {}", testimgID); + testMaterialID = materialCache.addMaterial(gfxDevice, { + .baseColor = glm::vec3(1.f), + .diffuseTexture = testimgID, + }); + spdlog::info("Test material created with id: {}", testMaterialID); + float aspectRatio = static_cast(params.renderSize.x) / static_cast(params.renderSize.y); camera.setAspectRatio(aspectRatio); //Look 90 deg to the right camera.SetRotation(glm::radians(glm::vec2(90.f, 0.f))); - inputManager.Init(); - + InputManager::GetInstance().Init(); } void App::run() { @@ -90,7 +99,7 @@ void App::run() { } while (accumulator >= dt) { - inputManager.BeginFrame(); + InputManager::GetInstance().BeginFrame(); SDL_Event event; while (SDL_PollEvent(&event)) { if (event.type == SDL_QUIT) { @@ -106,39 +115,12 @@ void App::run() { break; } } - inputManager.ProcessEvent(event); + InputManager::GetInstance().ProcessEvent(event); - if (inputManager.IsKeyDown(SDL_SCANCODE_W)) { - camera.m_position += camera.GetForward() * dt * 5.f; - } - if (inputManager.IsKeyDown(SDL_SCANCODE_S)) { - camera.m_position -= camera.GetForward() * dt * 5.f; - } - if (inputManager.IsKeyDown(SDL_SCANCODE_A)) { - camera.m_position -= camera.GetRight() * dt * 5.f; - } - if (inputManager.IsKeyDown(SDL_SCANCODE_D)) { - camera.m_position += camera.GetRight() * dt * 5.f; - } - - // rotation - if (inputManager.IsKeyDown(SDL_SCANCODE_LEFT)) { - camera.SetRotation(camera.GetYaw() - glm::radians(90.f) * dt, camera.GetPitch()); - } - if (inputManager.IsKeyDown(SDL_SCANCODE_RIGHT)) { - camera.SetRotation(camera.GetYaw() + glm::radians(90.f) * dt, camera.GetPitch()); - } - if (inputManager.IsKeyDown(SDL_SCANCODE_UP)) { - camera.SetRotation(camera.GetYaw(), camera.GetPitch() + glm::radians(90.f) * dt); - } - if (inputManager.IsKeyDown(SDL_SCANCODE_DOWN)) { - camera.SetRotation(camera.GetYaw(), camera.GetPitch() - glm::radians(90.f) * dt); - } - - camera.Update(dt); } + camera.Update(dt); if (gfxDevice.needsSwapchainRecreate()) { spdlog::info("Recreating swapchain to size: {}x{}", m_params.windowSize.x, m_params.windowSize.y); gfxDevice.recreateSwapchain(m_params.windowSize.x, m_params.windowSize.y); @@ -154,7 +136,7 @@ void App::run() { glm::mat4 objMatrix = glm::mat4(1.f); objMatrix = glm::translate(objMatrix, glm::vec3(0.f, -3.0f, 0.f)); renderer.beginDrawing(gfxDevice); - renderer.drawMesh(testMeshID, glm::mat4(1.f), 0); + renderer.drawMesh(testMeshID, glm::mat4(1.f), testMaterialID); renderer.drawMesh(testMeshID, objMatrix, 0); renderer.endDrawing(); diff --git a/destrum/src/Graphics/BindlessSetManager.cpp b/destrum/src/Graphics/BindlessSetManager.cpp new file mode 100644 index 0000000..3d3f78c --- /dev/null +++ b/destrum/src/Graphics/BindlessSetManager.cpp @@ -0,0 +1,184 @@ +#include + +#include +#include + +#include + +namespace +{ +static const std::uint32_t maxBindlessResources = 16536; +static const std::uint32_t maxSamplers = 32; + +static const std::uint32_t texturesBinding = 0; +static const std::uint32_t samplersBinding = 1; +} + +void BindlessSetManager::init(VkDevice device, float maxAnisotropy) +{ + { // create pool + const auto poolSizesBindless = std::array{{ + {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, maxBindlessResources}, + {VK_DESCRIPTOR_TYPE_SAMPLER, maxSamplers}, + }}; + + const auto poolInfo = VkDescriptorPoolCreateInfo{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, + .flags = VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT_EXT, + .maxSets = 10, + .poolSizeCount = (std::uint32_t)poolSizesBindless.size(), + .pPoolSizes = poolSizesBindless.data(), + }; + + VK_CHECK(vkCreateDescriptorPool(device, &poolInfo, nullptr, &descPool)); + } + + { // build desc set layout + const auto bindings = std::array{{ + { + .binding = texturesBinding, + .descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, + .descriptorCount = maxBindlessResources, + .stageFlags = VK_SHADER_STAGE_ALL, + }, + { + .binding = samplersBinding, + .descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER, + .descriptorCount = maxSamplers, + .stageFlags = VK_SHADER_STAGE_ALL, + }, + }}; + + const VkDescriptorBindingFlags bindlessFlags = + VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT; + const auto bindingFlags = std::array{bindlessFlags, bindlessFlags}; + + const auto flagInfo = VkDescriptorSetLayoutBindingFlagsCreateInfo{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO, + .bindingCount = (std::uint32_t)bindingFlags.size(), + .pBindingFlags = bindingFlags.data(), + }; + + const auto info = VkDescriptorSetLayoutCreateInfo{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, + .pNext = &flagInfo, + .flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT_EXT, + .bindingCount = (std::uint32_t)bindings.size(), + .pBindings = bindings.data(), + }; + + VK_CHECK(vkCreateDescriptorSetLayout(device, &info, nullptr, &descSetLayout)); + } + + { // alloc desc set + const auto allocInfo = VkDescriptorSetAllocateInfo{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, + .descriptorPool = descPool, + .descriptorSetCount = 1, + .pSetLayouts = &descSetLayout, + }; + + std::uint32_t maxBinding = maxBindlessResources - 1; + const auto countInfo = VkDescriptorSetVariableDescriptorCountAllocateInfo{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_VARIABLE_DESCRIPTOR_COUNT_ALLOCATE_INFO, + .descriptorSetCount = 1, + .pDescriptorCounts = &maxBinding, + }; + + VK_CHECK(vkAllocateDescriptorSets(device, &allocInfo, &descSet)); + } + + initDefaultSamplers(device, maxAnisotropy); +} + +void BindlessSetManager::initDefaultSamplers(VkDevice device, float maxAnisotropy) +{ + // Keep in sync with bindless.glsl + static const std::uint32_t nearestSamplerId = 0; + static const std::uint32_t linearSamplerId = 1; + static const std::uint32_t shadowSamplerId = 2; + + { // init nearest sampler + const auto samplerCreateInfo = VkSamplerCreateInfo{ + .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, + .magFilter = VK_FILTER_NEAREST, + .minFilter = VK_FILTER_NEAREST, + }; + VK_CHECK(vkCreateSampler(device, &samplerCreateInfo, nullptr, &nearestSampler)); + vkutil::addDebugLabel(device, nearestSampler, "nearest"); + addSampler(device, nearestSamplerId, nearestSampler); + } + + { // init linear sampler + const auto samplerCreateInfo = VkSamplerCreateInfo{ + .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, + .magFilter = VK_FILTER_LINEAR, + .minFilter = VK_FILTER_LINEAR, + .mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR, + // TODO: make possible to disable anisotropy or set other values? + .anisotropyEnable = VK_TRUE, + .maxAnisotropy = maxAnisotropy, + }; + VK_CHECK(vkCreateSampler(device, &samplerCreateInfo, nullptr, &linearSampler)); + vkutil::addDebugLabel(device, linearSampler, "linear"); + addSampler(device, linearSamplerId, linearSampler); + } + + { // init shadow map sampler + const auto samplerCreateInfo = VkSamplerCreateInfo{ + .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, + .magFilter = VK_FILTER_LINEAR, + .minFilter = VK_FILTER_LINEAR, + .compareEnable = VK_TRUE, + .compareOp = VK_COMPARE_OP_GREATER_OR_EQUAL, + }; + VK_CHECK(vkCreateSampler(device, &samplerCreateInfo, nullptr, &shadowMapSampler)); + vkutil::addDebugLabel(device, shadowMapSampler, "shadow"); + addSampler(device, shadowSamplerId, shadowMapSampler); + } +} + +void BindlessSetManager::cleanup(VkDevice device) +{ + vkDestroySampler(device, nearestSampler, nullptr); + vkDestroySampler(device, linearSampler, nullptr); + vkDestroySampler(device, shadowMapSampler, nullptr); + + vkDestroyDescriptorSetLayout(device, descSetLayout, nullptr); + vkDestroyDescriptorPool(device, descPool, nullptr); +} + +void BindlessSetManager::addImage( + const VkDevice device, + std::uint32_t id, + const VkImageView imageView) +{ + const auto imageInfo = VkDescriptorImageInfo{ + .imageView = imageView, .imageLayout = VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL}; + const auto writeSet = VkWriteDescriptorSet{ + .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .dstSet = descSet, + .dstBinding = texturesBinding, + .dstArrayElement = id, + .descriptorCount = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, + .pImageInfo = &imageInfo, + }; + vkUpdateDescriptorSets(device, 1, &writeSet, 0, nullptr); +} + +void BindlessSetManager::addSampler(const VkDevice device, std::uint32_t id, VkSampler sampler) +{ + const auto imageInfo = + VkDescriptorImageInfo{.sampler = sampler, .imageLayout = VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL}; + const auto writeSet = VkWriteDescriptorSet{ + .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .dstSet = descSet, + .dstBinding = samplersBinding, + .dstArrayElement = id, + .descriptorCount = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER, + .pImageInfo = &imageInfo, + }; + vkUpdateDescriptorSets(device, 1, &writeSet, 0, nullptr); +} diff --git a/destrum/src/Graphics/Camera.cpp b/destrum/src/Graphics/Camera.cpp index 0774947..33f31c0 100644 --- a/destrum/src/Graphics/Camera.cpp +++ b/destrum/src/Graphics/Camera.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -6,146 +7,112 @@ #include #include +#include "glm/gtx/norm.hpp" + + Camera::Camera(const glm::vec3& position, const glm::vec3& up): m_position{position}, m_up{up} { } void Camera::Update(float deltaTime) { -// const auto MyWindow = static_cast(glfwGetWindowUserPointer(Window::gWindow)); -// -// #pragma region Keyboard Movement -// totalPitch = glm::clamp(totalPitch, -glm::half_pi() + 0.01f, glm::half_pi() - 0.01f); -// -// float MovementSpeed = m_movementSpeed; -// if (glfwGetKey(Window::gWindow, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS) { -// MovementSpeed *= 2.0f; -// } -// -// if (glfwGetKey(Window::gWindow, GLFW_KEY_W) == GLFW_PRESS) { -// m_position += m_forward * deltaTime * MovementSpeed; -// } -// if (glfwGetKey(Window::gWindow, GLFW_KEY_A) == GLFW_PRESS) { -// m_position -= m_right * deltaTime * MovementSpeed; -// } -// if (glfwGetKey(Window::gWindow, GLFW_KEY_S) == GLFW_PRESS) { -// m_position -= m_forward * deltaTime * MovementSpeed; -// } -// if (glfwGetKey(Window::gWindow, GLFW_KEY_D) == GLFW_PRESS) { -// m_position += m_right * deltaTime * MovementSpeed; -// } -// if (glfwGetKey(Window::gWindow, GLFW_KEY_E) == GLFW_PRESS) { -// m_position += m_up * deltaTime * MovementSpeed; -// } -// if (glfwGetKey(Window::gWindow, GLFW_KEY_Q) == GLFW_PRESS) { -// m_position -= m_up * deltaTime * MovementSpeed; -// } -// -// // Looking around with arrow keys -// constexpr float lookSpeed = 1.0f * glm::radians(1.f); -// if (glfwGetKey(Window::gWindow, GLFW_KEY_UP) == GLFW_PRESS) { -// totalPitch += lookSpeed; -// } -// if (glfwGetKey(Window::gWindow, GLFW_KEY_DOWN) == GLFW_PRESS) { -// totalPitch -= lookSpeed; -// } -// if (glfwGetKey(Window::gWindow, GLFW_KEY_LEFT) == GLFW_PRESS) { -// totalYaw -= lookSpeed; -// } -// if (glfwGetKey(Window::gWindow, GLFW_KEY_RIGHT) == GLFW_PRESS) { -// totalYaw += lookSpeed; -// } -// -// totalPitch = glm::clamp(totalPitch, -glm::half_pi(), glm::half_pi()); -// -// const glm::mat4 yawMatrix = glm::rotate(glm::mat4(1.0f), -totalYaw, glm::vec3(0, 1, 0)); -// const glm::mat4 pitchMatrix = glm::rotate(glm::mat4(1.0f), totalPitch, glm::vec3(0, 0, 1)); -// -// const glm::mat4 rotationMatrix = yawMatrix * pitchMatrix; -// -// m_forward = glm::vec3(rotationMatrix * glm::vec4(1, 0, 0, 0)); -// m_right = glm::normalize(glm::cross(m_forward, glm::vec3(0, 1, 0))); -// m_up = glm::normalize(glm::cross(m_right, m_forward)); -// #pragma endregion -// -// #pragma region Mouse Looking -// static bool wasCursorLockedLastFrame = false; -// static double lastMouseX = 0.0; -// static double lastMouseY = 0.0; -// static bool firstMouse = true; -// -// bool isLocked = MyWindow->isCursorLocked(); -// if (isLocked) { -// double mouseX, mouseY; -// glfwGetCursorPos(Window::gWindow, &mouseX, &mouseY); -// -// // Reset mouse reference when locking the cursor (prevents jump) -// if (!wasCursorLockedLastFrame) { -// lastMouseX = mouseX; -// lastMouseY = mouseY; -// firstMouse = false; -// } -// -// float xOffset = static_cast(mouseX - lastMouseX); -// float yOffset = static_cast(lastMouseY - mouseY); // Reversed: y goes up -// -// lastMouseX = mouseX; -// lastMouseY = mouseY; -// -// constexpr float sensitivity = 0.002f; -// xOffset *= sensitivity; -// yOffset *= sensitivity; -// -// totalYaw += xOffset; -// totalPitch += yOffset; -// -// totalPitch = glm::clamp(totalPitch, -glm::half_pi() + 0.01f, glm::half_pi() - 0.01f); -// -// m_frustum.update(m_projectionMatrix * m_viewMatrix); -// } -// -// wasCursorLockedLastFrame = isLocked; -// #pragma endregion -// -// #pragma region Controller Movement -// -// if (glfwJoystickIsGamepad(GLFW_JOYSTICK_1)) { -// GLFWgamepadstate state; -// if (glfwJoystickIsGamepad(GLFW_JOYSTICK_1) && glfwGetGamepadState(GLFW_JOYSTICK_1, &state)) { -// float moveSpeed = MovementSpeed * deltaTime; -// const float rotSpeed = 1.5f * deltaTime; -// -// -// //TODO: god knows what this is -// auto deadzone = [] (float value, float threshold = 0.1f) { -// if (fabs(value) < threshold) -// return 0.0f; -// const float sign = (value > 0) ? 1.0f : -1.0f; -// return sign * (fabs(value) - threshold) / (1.0f - threshold); -// }; -// -// //LT is x2 speed -// const bool isLTPressed = state.buttons[GLFW_GAMEPAD_BUTTON_LEFT_BUMPER] == GLFW_PRESS; -// if (isLTPressed) { -// moveSpeed *= 3.0f; -// } -// -// const float lx = deadzone(state.axes[GLFW_GAMEPAD_AXIS_LEFT_X]); -// const float ly = deadzone(state.axes[GLFW_GAMEPAD_AXIS_LEFT_Y]); -// const float rx = deadzone(state.axes[GLFW_GAMEPAD_AXIS_RIGHT_X]); -// const float ry = deadzone(state.axes[GLFW_GAMEPAD_AXIS_RIGHT_Y]); -// -// m_position += m_forward * (-ly * moveSpeed); -// m_position += m_right * (lx * moveSpeed); -// -// const float lTrigger = (state.axes[GLFW_GAMEPAD_AXIS_LEFT_TRIGGER] + 1.0f) / 2.0f; -// const float rTrigger = (state.axes[GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER] + 1.0f) / 2.0f; -// const float vertical = (rTrigger - lTrigger); -// m_position += m_up * vertical * moveSpeed; -// -// totalYaw += rx * rotSpeed; -// totalPitch -= ry * rotSpeed; -// } -// } -// #pragma endregion + auto& input = InputManager::GetInstance(); + + // --- tuning --- + float moveSpeed = m_movementSpeed; + if (input.IsKeyDown(SDL_SCANCODE_LSHIFT) || input.IsKeyDown(SDL_SCANCODE_RSHIFT)) { + moveSpeed *= 2.0f; + } + + // Controller speed boost (pad0 LB) + const SDL_JoystickID pad0 = input.GetPadInstanceId(0); + if (pad0 >= 0 && input.IsPadButtonDown(pad0, SDL_CONTROLLER_BUTTON_LEFTSHOULDER)) { + moveSpeed *= 3.0f; + } + + // Clamp pitch like your old code + m_pitch = glm::clamp(m_pitch, -glm::half_pi() + 0.01f, glm::half_pi() - 0.01f); + + // ========================= + // Movement (Keyboard) + // ========================= + glm::vec3 move(0.0f); + + if (input.IsKeyDown(SDL_SCANCODE_W)) move += m_forward; + if (input.IsKeyDown(SDL_SCANCODE_S)) move -= m_forward; + if (input.IsKeyDown(SDL_SCANCODE_D)) move += m_right; + if (input.IsKeyDown(SDL_SCANCODE_A)) move -= m_right; + + if (input.IsKeyDown(SDL_SCANCODE_Q)) move += m_up; + if (input.IsKeyDown(SDL_SCANCODE_E)) move -= m_up; + + if (glm::length2(move) > 0.0f) { + move = glm::normalize(move); + m_position += move * (moveSpeed * deltaTime); + } + + // ========================= + // Movement (Controller) + // ========================= + if (pad0 >= 0) { + const float lx = input.GetPadAxis(pad0, SDL_CONTROLLER_AXIS_LEFTX); // [-1..1] + const float ly = input.GetPadAxis(pad0, SDL_CONTROLLER_AXIS_LEFTY); // [-1..1] + + // SDL Y is typically +down, so invert for "forward" + glm::vec3 padMove(0.0f); + padMove += m_forward * (-ly); + padMove += m_right * ( lx); + + // Triggers for vertical movement (optional) + // SDL controller triggers are axes too: 0..1-ish after normalization in our helper, but signless. + // With our NormalizeAxis, triggers will sit near 0 until pressed (depending on mapping). + // If your NormalizeAxis maps triggers weirdly, swap to raw event value approach. + const float lt = input.GetPadAxis(pad0, SDL_CONTROLLER_AXIS_TRIGGERLEFT); + const float rt = input.GetPadAxis(pad0, SDL_CONTROLLER_AXIS_TRIGGERRIGHT); + const float vertical = (rt - lt); + padMove += m_up * vertical; + + if (glm::length2(padMove) > 0.0001f) { + // do NOT normalize: preserve analog magnitude for smooth movement + m_position += padMove * (moveSpeed * deltaTime); + } + } + + // ========================= + // Look (Keyboard arrows only) + // ========================= + // Use radians/sec so framerate-independent + const float keyLookSpeed = glm::radians(120.0f); // degrees per second + + if (input.IsKeyDown(SDL_SCANCODE_UP)) m_pitch += keyLookSpeed * deltaTime; + if (input.IsKeyDown(SDL_SCANCODE_DOWN)) m_pitch -= keyLookSpeed * deltaTime; + if (input.IsKeyDown(SDL_SCANCODE_LEFT)) m_yaw -= keyLookSpeed * deltaTime; + if (input.IsKeyDown(SDL_SCANCODE_RIGHT)) m_yaw += keyLookSpeed * deltaTime; + + // ========================= + // Look (Controller right stick) + // ========================= + if (pad0 >= 0) { + const float rx = input.GetPadAxis(pad0, SDL_CONTROLLER_AXIS_RIGHTX); + const float ry = input.GetPadAxis(pad0, SDL_CONTROLLER_AXIS_RIGHTY); + + const float padLookSpeed = 2.2f; // radians/sec at full deflection + m_yaw += rx * padLookSpeed * deltaTime; + m_pitch -= ry * padLookSpeed * deltaTime; + } + + // Clamp pitch again after modifications + m_pitch = glm::clamp(m_pitch, -glm::half_pi() + 0.01f, glm::half_pi() - 0.01f); + + // Recompute basis from yaw/pitch (same convention you used) + const glm::mat4 yawMatrix = glm::rotate(glm::mat4(1.0f), -m_yaw, glm::vec3(0, 1, 0)); + const glm::mat4 pitchMatrix = glm::rotate(glm::mat4(1.0f), m_pitch, glm::vec3(0, 0, 1)); + const glm::mat4 rotation = yawMatrix * pitchMatrix; + + m_forward = glm::normalize(glm::vec3(rotation * glm::vec4(1, 0, 0, 0))); // +X forward + m_right = glm::normalize(glm::cross(m_forward, glm::vec3(0, 1, 0))); + m_up = glm::normalize(glm::cross(m_right, m_forward)); + + // keep target mode off when manually controlled + m_useTarget = false; CalculateProjectionMatrix(); CalculateViewMatrix(); diff --git a/destrum/src/Graphics/GfxDevice.cpp b/destrum/src/Graphics/GfxDevice.cpp index 8872d31..40eaf10 100644 --- a/destrum/src/Graphics/GfxDevice.cpp +++ b/destrum/src/Graphics/GfxDevice.cpp @@ -99,6 +99,23 @@ void GfxDevice::init(SDL_Window* window, const std::string& appName, bool vSync) swapchainFormat = VK_FORMAT_B8G8R8A8_SRGB; swapchain.createSwapchain(this, swapchainFormat, w, h, vSync); + VkPhysicalDeviceProperties props{}; + vkGetPhysicalDeviceProperties(physicalDevice, &props); + + imageCache.bindlessSetManager.init(device, props.limits.maxSamplerAnisotropy); + + { // create white texture + std::uint32_t pixel = 0xFFFFFFFF; + whiteImageId = createImage( + { + .format = VK_FORMAT_R8G8B8A8_UNORM, + .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, + .extent = VkExtent3D{1, 1, 1}, + }, + "white texture", + &pixel); + } + swapchain.initSync(device); const auto poolCreateInfo = vkinit::commandPoolCreateInfo(VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, graphicsQueueFamily); @@ -223,6 +240,30 @@ void GfxDevice::waitIdle() { VK_CHECK(vkDeviceWaitIdle(device)); } +BindlessSetManager& GfxDevice::getBindlessSetManager() { + return imageCache.bindlessSetManager; +} + +VkDescriptorSetLayout GfxDevice::getBindlessDescSetLayout() const { + return imageCache.bindlessSetManager.getDescSetLayout(); +} + +const VkDescriptorSet& GfxDevice::getBindlessDescSet() const { + return imageCache.bindlessSetManager.getDescSet(); +} + +void GfxDevice::bindBindlessDescSet(VkCommandBuffer cmd, VkPipelineLayout layout) const { + vkCmdBindDescriptorSets( + cmd, + VK_PIPELINE_BIND_POINT_GRAPHICS, + layout, + 0, + 1, + &imageCache.bindlessSetManager.getDescSet(), + 0, + nullptr); +} + void GfxDevice::immediateSubmit(ImmediateExecuteFunction&& f) const { executor.immediateSubmit(std::move(f)); } diff --git a/destrum/src/Graphics/ImageCache.cpp b/destrum/src/Graphics/ImageCache.cpp index 726e9db..5f68381 100644 --- a/destrum/src/Graphics/ImageCache.cpp +++ b/destrum/src/Graphics/ImageCache.cpp @@ -52,7 +52,7 @@ ImageID ImageCache::addImage(ImageID id, GPUImage image) } else { images.push_back(std::move(image)); } - // bindlessSetManager.addImage(gfxDevice.getDevice(), id, image.imageView); + bindlessSetManager.addImage(gfxDevice.getDevice(), id, image.imageView); return id; } diff --git a/destrum/src/Graphics/MaterialCache.cpp b/destrum/src/Graphics/MaterialCache.cpp new file mode 100644 index 0000000..7684107 --- /dev/null +++ b/destrum/src/Graphics/MaterialCache.cpp @@ -0,0 +1,77 @@ +#include + +#include +#include + +#include "spdlog/spdlog.h" + +void MaterialCache::init(GfxDevice& gfxDevice) +{ + materialDataBuffer = gfxDevice.createBuffer( + MAX_MATERIALS * sizeof(MaterialData), + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); + vkutil::addDebugLabel(gfxDevice.getDevice(), materialDataBuffer.buffer, "material data"); + + { // create default normal map texture + std::uint32_t normal = 0xFFFF8080; // (0.5, 0.5, 1.0, 1.0) + defaultNormalMapTextureID = gfxDevice.createImage( + { + .format = VK_FORMAT_R8G8B8A8_UNORM, + .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, + .extent = VkExtent3D{1, 1, 1}, + }, + "normal map placeholder texture", + &normal); + } + + Material placeholderMaterial{.name = "PLACEHOLDER_MATERIAL"}; + placeholderMaterialId = addMaterial(gfxDevice, placeholderMaterial); +} + +void MaterialCache::cleanup(GfxDevice& gfxDevice) +{ + gfxDevice.destroyBuffer(materialDataBuffer); +} + +MaterialID MaterialCache::addMaterial(GfxDevice& gfxDevice, Material material) +{ + const auto getTextureOrElse = [](ImageID imageId, ImageID placeholder) { + spdlog::warn("Using placeholder texture for material given texture ID: {}", imageId); + return imageId != NULL_IMAGE_ID ? imageId : placeholder; + }; + + // store on GPU + MaterialData* data = (MaterialData*)materialDataBuffer.info.pMappedData; + auto whiteTextureID = gfxDevice.getWhiteTextureID(); + auto id = getFreeMaterialId(); + assert(id < MAX_MATERIALS); + data[id] = MaterialData{ + .baseColor = glm::vec4(material.baseColor, 1.0f), + .metalRoughnessEmissive = glm::vec4{material.metallicFactor, material.roughnessFactor, material.emissiveFactor, 0.f}, + .diffuseTex = getTextureOrElse(material.diffuseTexture, whiteTextureID), + .normalTex = whiteTextureID, + .metallicRoughnessTex = whiteTextureID, + .emissiveTex = whiteTextureID, + }; + + // store on CPU + materials.push_back(std::move(material)); + + return id; +} + +const Material& MaterialCache::getMaterial(MaterialID id) const +{ + return materials.at(id); +} + +MaterialID MaterialCache::getFreeMaterialId() const +{ + return materials.size(); +} + +MaterialID MaterialCache::getPlaceholderMaterialId() const +{ + assert(placeholderMaterialId != NULL_MATERIAL_ID && "MaterialCache::init not called"); + return placeholderMaterialId; +} diff --git a/destrum/src/Graphics/Pipelines/MeshPipeline.cpp b/destrum/src/Graphics/Pipelines/MeshPipeline.cpp index 83aeaf9..b166322 100644 --- a/destrum/src/Graphics/Pipelines/MeshPipeline.cpp +++ b/destrum/src/Graphics/Pipelines/MeshPipeline.cpp @@ -28,14 +28,14 @@ void MeshPipeline::init(GfxDevice& gfxDevice, VkFormat drawImageFormat, VkFormat }; const auto pushConstantRanges = std::array{bufferRange}; - // const auto layouts = std::array{gfxDevice.getBindlessDescSetLayout()}; + const auto layouts = std::array{gfxDevice.getBindlessDescSetLayout()}; // m_pipelineLayout = vkutil::createPipelineLayout(device, layouts, pushConstantRanges); // vkutil::addDebugLabel(device, pipelineLayout, "mesh pipeline layout"); VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; - // pipelineLayoutInfo.setLayoutCount = static_cast(descriptorSetLayouts.size()); - // pipelineLayoutInfo.pSetLayouts = descriptorSetLayouts.data(); + pipelineLayoutInfo.setLayoutCount = static_cast(layouts.size()); + pipelineLayoutInfo.pSetLayouts = layouts.data(); pipelineLayoutInfo.pushConstantRangeCount = 1; pipelineLayoutInfo.pPushConstantRanges = pushConstantRanges.data(); @@ -72,9 +72,8 @@ void MeshPipeline::draw(VkCommandBuffer cmd, const GPUBuffer& sceneDataBuffer, const std::vector& drawCommands, const std::vector& sortedDrawCommands) { - // vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); m_pipeline->bind(cmd); - // gfxDevice.bindBindlessDescSet(cmd, pipelineLayout); + gfxDevice.bindBindlessDescSet(cmd, m_pipelineLayout); const auto viewport = VkViewport{ .x = 0, diff --git a/destrum/src/Graphics/Renderer.cpp b/destrum/src/Graphics/Renderer.cpp index 45808b9..09f9bcb 100644 --- a/destrum/src/Graphics/Renderer.cpp +++ b/destrum/src/Graphics/Renderer.cpp @@ -4,7 +4,7 @@ #include "spdlog/spdlog.h" -GameRenderer::GameRenderer(MeshCache& meshCache): meshCache{meshCache} { +GameRenderer::GameRenderer(MeshCache& meshCache, MaterialCache& matCache): meshCache{meshCache}, materialCache{matCache} { } void GameRenderer::init(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize) { @@ -89,7 +89,7 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& void GameRenderer::cleanup() { } -void GameRenderer::drawMesh(MeshID id, const glm::mat4& transform, MaterialId materialId) { +void GameRenderer::drawMesh(MeshID id, const glm::mat4& transform, MaterialID materialId) { const auto& mesh = meshCache.getMesh(id); // const auto worldBoundingSphere = edge::calculateBoundingSphereWorld(transform, mesh.boundingSphere, false); assert(materialId != NULL_MATERIAL_ID); diff --git a/destrum/src/Graphics/Util.cpp b/destrum/src/Graphics/Util.cpp index d1d97ac..e50f128 100644 --- a/destrum/src/Graphics/Util.cpp +++ b/destrum/src/Graphics/Util.cpp @@ -101,6 +101,16 @@ void vkutil::addDebugLabel(VkDevice device, VkBuffer buffer, const char* label) vkSetDebugUtilsObjectNameEXT(device, &nameInfo); } +void vkutil::addDebugLabel(VkDevice device, VkSampler sampler, const char* label) { + const auto nameInfo = VkDebugUtilsObjectNameInfoEXT{ + .sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT, + .objectType = VK_OBJECT_TYPE_SAMPLER, + .objectHandle = (std::uint64_t)sampler, + .pObjectName = label, + }; + vkSetDebugUtilsObjectNameEXT(device, &nameInfo); +} + vkutil::RenderInfo vkutil::createRenderingInfo(const RenderingInfoParams& params) { assert( (params.colorImageView || params.depthImageView != nullptr) && diff --git a/destrum/src/Input/InputManager.cpp b/destrum/src/Input/InputManager.cpp index b071f9b..50632d0 100644 --- a/destrum/src/Input/InputManager.cpp +++ b/destrum/src/Input/InputManager.cpp @@ -1,8 +1,38 @@ #include +#include +#include + +static float NormalizeAxis(Sint16 v, int deadzone) { + // Deadzone in raw units. Return [-1..1] + const int iv = (int)v; + + if (std::abs(iv) <= deadzone) return 0.0f; + + // Normalize to [-1..1] while preserving sign. + // SDL axis range is roughly [-32768..32767] + const float denom = (iv < 0) ? 32768.0f : 32767.0f; + float f = (float)iv / denom; + + // Optional: re-scale so that value starts at 0 outside deadzone + // (nice for sticks; comment out if you prefer raw normalize) + const float dz = (float)deadzone / 32767.0f; + const float sign = (f < 0.0f) ? -1.0f : 1.0f; + const float af = std::abs(f); + const float scaled = (af - dz) / (1.0f - dz); + return sign * std::clamp(scaled, 0.0f, 1.0f); +} void InputManager::Init() { - // Nothing mandatory here. You can also enable relative mouse mode, etc. - // SDL_SetRelativeMouseMode(SDL_TRUE); + // Make sure gamecontroller subsystem is active (safe even if already) + SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER); + + // Open any controllers already connected + const int num = SDL_NumJoysticks(); + for (int i = 0; i < num; ++i) { + if (SDL_IsGameController(i)) { + AddController(i); + } + } } void InputManager::BeginFrame() { @@ -21,74 +51,160 @@ void InputManager::BeginFrame() { m_mouseDY = m_mouseY - m_prevMouseY; m_prevMouseX = m_mouseX; m_prevMouseY = m_mouseY; + + // Clear controller "edge" sets + snapshot axes + for (auto& [id, pad]: m_pads) { + pad.buttonsPressed.clear(); + pad.buttonsReleased.clear(); + pad.prevAxes = pad.axes; + } +} + +void InputManager::AddController(int deviceIndex) { + if (!SDL_IsGameController(deviceIndex)) return; + + SDL_GameController* gc = SDL_GameControllerOpen(deviceIndex); + if (!gc) return; + + SDL_Joystick* joy = SDL_GameControllerGetJoystick(gc); + SDL_JoystickID instanceId = SDL_JoystickInstanceID(joy); + + PadState ps; + ps.controller = gc; + ps.instanceId = instanceId; + ps.axes.fill(0); + ps.prevAxes.fill(0); + + m_pads[instanceId] = ps; + + // Track order (padIndex mapping) + m_padOrder.push_back(instanceId); +} + +void InputManager::RemoveController(SDL_JoystickID instanceId) { + auto it = m_pads.find(instanceId); + if (it != m_pads.end()) { + if (it->second.controller) { + SDL_GameControllerClose(it->second.controller); + } + m_pads.erase(it); + } + + // Remove from order list + m_padOrder.erase( + std::remove(m_padOrder.begin(), m_padOrder.end(), instanceId), + m_padOrder.end() + ); } void InputManager::ProcessEvent(const SDL_Event& e) { switch (e.type) { case SDL_KEYDOWN: { - if (e.key.repeat) break; // avoid repeat spam for "Pressed" + if (e.key.repeat) break; SDL_Scancode sc = e.key.keysym.scancode; m_keysDown.insert(sc); m_keysPressed.insert(sc); - } break; + } + break; case SDL_KEYUP: { SDL_Scancode sc = e.key.keysym.scancode; m_keysDown.erase(sc); m_keysReleased.insert(sc); - } break; + } + break; case SDL_MOUSEBUTTONDOWN: { Uint8 b = e.button.button; m_mouseDown.insert(b); m_mousePressed.insert(b); - } break; + } + break; case SDL_MOUSEBUTTONUP: { Uint8 b = e.button.button; m_mouseDown.erase(b); m_mouseReleased.insert(b); - } break; + } + break; case SDL_MOUSEMOTION: { m_mouseX = e.motion.x; m_mouseY = e.motion.y; - } break; + } + break; case SDL_MOUSEWHEEL: { - // Accumulate wheel per frame m_wheelX += e.wheel.x; m_wheelY += e.wheel.y; - } break; + } + break; case SDL_TEXTINPUT: { - // text typed this frame m_textInput += e.text.text; - } break; + } + break; + + // ---- Controller hotplug ---- + case SDL_CONTROLLERDEVICEADDED: { + // e.cdevice.which is the device index here + AddController(e.cdevice.which); + } + break; + + case SDL_CONTROLLERDEVICEREMOVED: { + // e.cdevice.which is instanceId here + RemoveController((SDL_JoystickID)e.cdevice.which); + } + break; + + // ---- Controller buttons ---- + case SDL_CONTROLLERBUTTONDOWN: { + SDL_JoystickID id = (SDL_JoystickID)e.cbutton.which; + auto it = m_pads.find(id); + if (it == m_pads.end()) break; + + uint8_t btn = (uint8_t)e.cbutton.button; + it->second.buttonsDown.insert(btn); + it->second.buttonsPressed.insert(btn); + } + break; + + case SDL_CONTROLLERBUTTONUP: { + SDL_JoystickID id = (SDL_JoystickID)e.cbutton.which; + auto it = m_pads.find(id); + if (it == m_pads.end()) break; + + uint8_t btn = (uint8_t)e.cbutton.button; + it->second.buttonsDown.erase(btn); + it->second.buttonsReleased.insert(btn); + } + break; + + // ---- Controller axes ---- + case SDL_CONTROLLERAXISMOTION: { + SDL_JoystickID id = (SDL_JoystickID)e.caxis.which; + auto it = m_pads.find(id); + if (it == m_pads.end()) break; + + const int axis = (int)e.caxis.axis; + if (axis >= 0 && axis < SDL_CONTROLLER_AXIS_MAX) { + it->second.axes[(size_t)axis] = e.caxis.value; + } + } + break; default: break; } } -bool InputManager::IsKeyDown(SDL_Scancode sc) const { - return m_keysDown.find(sc) != m_keysDown.end(); -} -bool InputManager::WasKeyPressed(SDL_Scancode sc) const { - return m_keysPressed.find(sc) != m_keysPressed.end(); -} -bool InputManager::WasKeyReleased(SDL_Scancode sc) const { - return m_keysReleased.find(sc) != m_keysReleased.end(); -} +bool InputManager::IsKeyDown(SDL_Scancode sc) const { return m_keysDown.count(sc) != 0; } +bool InputManager::WasKeyPressed(SDL_Scancode sc) const { return m_keysPressed.count(sc) != 0; } +bool InputManager::WasKeyReleased(SDL_Scancode sc) const { return m_keysReleased.count(sc) != 0; } -bool InputManager::IsMouseDown(Uint8 button) const { - return m_mouseDown.find(button) != m_mouseDown.end(); -} -bool InputManager::WasMousePressed(Uint8 button) const { - return m_mousePressed.find(button) != m_mousePressed.end(); -} -bool InputManager::WasMouseReleased(Uint8 button) const { - return m_mouseReleased.find(button) != m_mouseReleased.end(); -} +bool InputManager::IsMouseDown(Uint8 button) const { return m_mouseDown.count(button) != 0; } +bool InputManager::WasMousePressed(Uint8 button) const { return m_mousePressed.count(button) != 0; } +bool InputManager::WasMouseReleased(Uint8 button) const { return m_mouseReleased.count(button) != 0; } void InputManager::StartTextInput() { if (!m_textInputActive) { @@ -96,6 +212,7 @@ void InputManager::StartTextInput() { m_textInputActive = true; } } + void InputManager::StopTextInput() { if (m_textInputActive) { SDL_StopTextInput(); @@ -103,6 +220,56 @@ void InputManager::StopTextInput() { } } +// ---- Controller direct queries ---- +SDL_JoystickID InputManager::GetPadInstanceId(uint8_t padIndex) const { + if (padIndex >= m_padOrder.size()) return -1; + return m_padOrder[padIndex]; +} + +bool InputManager::IsPadButtonDown(SDL_JoystickID id, SDL_GameControllerButton btn) const { + auto it = m_pads.find(id); + if (it == m_pads.end()) return false; + return it->second.buttonsDown.count((uint8_t)btn) != 0; +} + +bool InputManager::WasPadButtonPressed(SDL_JoystickID id, SDL_GameControllerButton btn) const { + auto it = m_pads.find(id); + if (it == m_pads.end()) return false; + return it->second.buttonsPressed.count((uint8_t)btn) != 0; +} + +bool InputManager::WasPadButtonReleased(SDL_JoystickID id, SDL_GameControllerButton btn) const { + auto it = m_pads.find(id); + if (it == m_pads.end()) return false; + return it->second.buttonsReleased.count((uint8_t)btn) != 0; +} + +float InputManager::GetPadAxis(SDL_JoystickID id, SDL_GameControllerAxis axis) const { + auto it = m_pads.find(id); + if (it == m_pads.end()) return 0.0f; + const int a = (int)axis; + if (a < 0 || a >= SDL_CONTROLLER_AXIS_MAX) return 0.0f; + return NormalizeAxis(it->second.axes[(size_t)a], m_axisDeadzone); +} + +// ---- Binding helpers ---- +InputManager::Binding InputManager::PadButton(uint8_t padIndex, SDL_GameControllerButton btn) { + Binding b; + b.device = Device::GamepadButton; + b.code = ((int)padIndex << 8) | ((int)btn & 0xFF); + return b; +} + +InputManager::Binding InputManager::PadAxis(uint8_t padIndex, SDL_GameControllerAxis axis, int sign, float threshold) { + Binding b; + b.device = Device::GamepadAxis; + b.code = ((int)padIndex << 8) | ((int)axis & 0xFF); + b.sign = (sign < 0) ? -1 : +1; + b.threshold = threshold; + return b; +} + +// ---- Action mapping ---- void InputManager::BindAction(const std::string& action, const Binding& binding) { m_bindings[action].push_back(binding); } @@ -111,28 +278,67 @@ bool InputManager::QueryBinding(const Binding& b, ButtonState state) const { switch (b.device) { case Device::Keyboard: { auto sc = static_cast(b.code); - if (state == ButtonState::Down) return IsKeyDown(sc); - if (state == ButtonState::Pressed) return WasKeyPressed(sc); + if (state == ButtonState::Down) return IsKeyDown(sc); + if (state == ButtonState::Pressed) return WasKeyPressed(sc); if (state == ButtonState::Released) return WasKeyReleased(sc); - } break; + } + break; case Device::MouseButton: { auto mb = static_cast(b.code); - if (state == ButtonState::Down) return IsMouseDown(mb); - if (state == ButtonState::Pressed) return WasMousePressed(mb); + if (state == ButtonState::Down) return IsMouseDown(mb); + if (state == ButtonState::Pressed) return WasMousePressed(mb); if (state == ButtonState::Released) return WasMouseReleased(mb); - } break; + } + break; case Device::MouseWheel: { - // Encoding: - // +1/-1 => wheel Y (up/down), +2/-2 => wheel X (right/left) const int c = b.code; - if (state != ButtonState::Pressed) return false; // wheel is "impulse" - if (c == 1) return m_wheelY > 0; + if (state != ButtonState::Pressed) return false; + if (c == 1) return m_wheelY > 0; if (c == -1) return m_wheelY < 0; - if (c == 2) return m_wheelX > 0; + if (c == 2) return m_wheelX > 0; if (c == -2) return m_wheelX < 0; - } break; + } + break; + + case Device::GamepadButton: { + const uint8_t padIndex = GetPadIndexFromCode(b.code); + const uint8_t btn = GetLow8FromCode(b.code); + + const SDL_JoystickID id = GetPadInstanceId(padIndex); + if (id < 0) return false; + + if (state == ButtonState::Down) return IsPadButtonDown(id, (SDL_GameControllerButton)btn); + if (state == ButtonState::Pressed) return WasPadButtonPressed(id, (SDL_GameControllerButton)btn); + if (state == ButtonState::Released) return WasPadButtonReleased(id, (SDL_GameControllerButton)btn); + } + break; + + case Device::GamepadAxis: { + // Axis bindings are treated as "digital" by thresholding + const uint8_t padIndex = GetPadIndexFromCode(b.code); + const uint8_t axis = GetLow8FromCode(b.code); + + const SDL_JoystickID id = GetPadInstanceId(padIndex); + if (id < 0) return false; + + auto it = m_pads.find(id); + if (it == m_pads.end()) return false; + + if (axis >= SDL_CONTROLLER_AXIS_MAX) return false; + + const float cur = NormalizeAxis(it->second.axes[axis], m_axisDeadzone) * (float)b.sign; + const float prev = NormalizeAxis(it->second.prevAxes[axis], m_axisDeadzone) * (float)b.sign; + + const bool curOn = cur >= b.threshold; + const bool prevOn = prev >= b.threshold; + + if (state == ButtonState::Down) return curOn; + if (state == ButtonState::Pressed) return (curOn && !prevOn); + if (state == ButtonState::Released) return (!curOn && prevOn); + } + break; } return false; } @@ -141,7 +347,7 @@ bool InputManager::GetAction(const std::string& action, ButtonState state) const auto it = m_bindings.find(action); if (it == m_bindings.end()) return false; - for (const auto& b : it->second) { + for (const auto& b: it->second) { if (QueryBinding(b, state)) return true; } return false;