diff --git a/CMakeLists.txt b/CMakeLists.txt index c74dc440..8e3979e9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -299,6 +299,15 @@ set_target_properties(Engine PROPERTIES ) target_compile_features(Engine PUBLIC cxx_std_23) +# --- Release link-time optimization --- +include(CheckIPOSupported) +check_ipo_supported(RESULT ENGINE_IPO_SUPPORTED LANGUAGES CXX) +if(ENGINE_IPO_SUPPORTED) + set_property(TARGET Engine PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELEASE ON) +endif() +target_compile_options(Engine PRIVATE + $<$,$>:-fno-math-errno>) + # --- Generated version header --- set(ENGINE_GENERATED_INCLUDE_DIR ${CMAKE_CURRENT_BINARY_DIR}/generated/include) configure_file( diff --git a/assets/shaders/ssao.frag b/assets/shaders/ssao.frag index b3e8d035..4cb478ce 100644 --- a/assets/shaders/ssao.frag +++ b/assets/shaders/ssao.frag @@ -43,6 +43,12 @@ vec3 ReconstructWorldPos(vec2 uv, float depth) { return world.xyz / world.w; } +// View-space Z for a depth value, solved from the projection's z and w rows. +float ViewZFromDepth(float depth) { + return (Projection[3][2] - depth * Projection[3][3]) / + (depth * Projection[2][3] - Projection[2][2]); +} + void main() { // Skip sky pixels — sky should read 1.0 (no occlusion). float depth = texture(gDepth, fragUV).r; @@ -92,24 +98,22 @@ void main() { if (sampleUV.x < 0.0 || sampleUV.x > 1.0 || sampleUV.y < 0.0 || sampleUV.y > 1.0) continue; - // Reconstruct the world-space position at this screen location and - // transform into view space for depth comparison. + // View-space depth at this screen location for the comparison. float sampleDepth = texture(gDepth, sampleUV).r; if (sampleDepth >= 0.9999) continue; // sky — no occluder - vec3 sampleWorld = ReconstructWorldPos(sampleUV, sampleDepth); - vec3 sampleViewPos = (View * vec4(sampleWorld, 1.0)).xyz; + float sampleViewZ = ViewZFromDepth(sampleDepth); // Range check — sample contributes less when the depth difference // between the frag and the occluder is much larger than the radius. // (avoids haloing around silhouettes) float rangeCheck = smoothstep(0.0, 1.0, - Radius / max(abs(viewPos.z - sampleViewPos.z), 0.0001)); + Radius / max(abs(viewPos.z - sampleViewZ), 0.0001)); // In view space the camera looks down -Z — "closer to the camera" // means a LESS negative Z (i.e. larger value). The occluder blocks - // the sample when sampleViewPos.z >= samplePos.z + bias. - occlusion += (sampleViewPos.z >= samplePos.z + Bias ? 1.0 : 0.0) * rangeCheck; + // the sample when sampleViewZ >= samplePos.z + bias. + occlusion += (sampleViewZ >= samplePos.z + Bias ? 1.0 : 0.0) * rangeCheck; } float ao = 1.0 - (occlusion / float(kSize)) * Intensity; diff --git a/assets/shaders/ssao.frag.spv b/assets/shaders/ssao.frag.spv index b9ed2607..90a81ddf 100644 Binary files a/assets/shaders/ssao.frag.spv and b/assets/shaders/ssao.frag.spv differ diff --git a/assets/shaders/ssao_gl.frag b/assets/shaders/ssao_gl.frag index 23b20225..d4e298e3 100644 --- a/assets/shaders/ssao_gl.frag +++ b/assets/shaders/ssao_gl.frag @@ -52,6 +52,14 @@ vec3 ReconstructWorldPos(vec2 uv, float depth) { return world.xyz / world.w; } +// View-space z from depth; equals (View * ReconstructWorldPos(uv, depth)).z +// because the projection's z and w rows depend on view z alone. +float LinearizeViewZ(float depth) { + float ndcZ = depth * 2.0 - 1.0; + return (Projection[3][2] - Projection[3][3] * ndcZ) + / (Projection[2][3] * ndcZ - Projection[2][2]); +} + void main() { if (ssaoEnabled == 0u) { outColor = 1.0; @@ -76,19 +84,18 @@ void main() { vec3 bitangent = cross(worldN, tangent); mat3 TBN = mat3(tangent, bitangent, worldN); - // Reference view-space Z for the current fragment. - // View * worldPos here only needs to be consistent with itself — the - // comparison is self-referential, so even if the view matrix is - // column- vs row-major mis-interpreted, both sides of the compare are - // transformed the same way and the result stays meaningful. - float fragViewZ = (View * vec4(worldPos, 1.0)).z; + // Reference view-space Z for the current fragment; View's z row maps a + // world offset to its view-space z delta. + float fragViewZ = LinearizeViewZ(depth); + vec3 viewZAxis = vec3(View[0][2], View[1][2], View[2][2]); mat4 VP = Projection * View; float occlusion = 0.0; for (uint i = 0u; i < ssaoKernelSize; ++i) { - vec3 sampleWorld = worldPos + (TBN * ssaoKernel[i].xyz) * ssaoRadius; + vec3 sampleOffset = (TBN * ssaoKernel[i].xyz) * ssaoRadius; + vec3 sampleWorld = worldPos + sampleOffset; vec4 clipSample = VP * vec4(sampleWorld, 1.0); if (clipSample.w <= 0.0) continue; @@ -98,9 +105,8 @@ void main() { float sampleDepth = texture(depthTexture, uv).r; if (sampleDepth >= 1.0) continue; // sky — no occluder - vec3 actualWorld = ReconstructWorldPos(uv, sampleDepth); - float actualViewZ = (View * vec4(actualWorld, 1.0)).z; - float sampleViewZ = (View * vec4(sampleWorld, 1.0)).z; + float actualViewZ = LinearizeViewZ(sampleDepth); + float sampleViewZ = fragViewZ + dot(viewZAxis, sampleOffset); float rangeCheck = smoothstep(0.0, 1.0, ssaoRadius / max(abs(fragViewZ - actualViewZ), 1e-4)); diff --git a/include/private/Graphics/Common/RenderCommandQueue.hpp b/include/private/Graphics/Common/RenderCommandQueue.hpp index 0ce63ed9..848c5a6a 100644 --- a/include/private/Graphics/Common/RenderCommandQueue.hpp +++ b/include/private/Graphics/Common/RenderCommandQueue.hpp @@ -2,9 +2,14 @@ #define _RENDERCOMMANDQUEUE_H #include -#include "RenderCommands.hpp" -#include #include +#include +#include +#include +#include +#include + +#include "RenderCommands.hpp" namespace Sleak { namespace RenderEngine { @@ -61,7 +66,7 @@ namespace Sleak { void ExecuteShadowPass(RenderContext* context); - bool HasCachedShadowDraws() const { return cachedShadowDraws.GetSize() > 0; } + bool HasCachedShadowDraws() const { return !cachedShadowDraws.empty(); } /// Drops queued commands for the current frame, keeping the cached shadow draw list. void Clear(); @@ -82,18 +87,73 @@ namespace Sleak { /// A draw command paired with the transform/bone buffers it needs to replay in the shadow pass. struct ShadowDrawEntry { - RefPtr command; - RefPtr transformBuffer; // last-bound slot-0 buffer - RefPtr boneBuffer; // slot-3 bone UBO for skinned draws + RenderCommandBase* command = nullptr; + const RefPtr* transformBuffer = + nullptr; // last-bound slot-0 buffer + const RefPtr* boneBuffer = + nullptr; // slot-3 bone UBO for skinned draws }; private: - static constexpr int RETIRE_FRAMES = 3; - static RenderCommandQueue* Instance; - Queue> commands; - List cachedShadowDraws; - List m_retiredShadowDraws[RETIRE_FRAMES]; - int m_retireIndex = 0; + /// Bump allocator owning one frame's commands until the slot is + /// recycled. + class CommandArena { + public: + CommandArena() = default; + CommandArena(const CommandArena&) = delete; + CommandArena& operator=(const CommandArena&) = delete; + ~CommandArena() { Reset(); } + + /// Constructs a command in arena memory and appends it to the + /// frame's list. + template + T* Create(Args&&... args) { + void* memory = Allocate(sizeof(T), alignof(T)); + T* command = new (memory) T(std::forward(args)...); + m_commands.push_back(command); + return command; + } + + /// Destroys every command and rewinds the blocks for reuse. + void Reset(); + + const std::vector& Commands() const { + return m_commands; + } + + private: + struct Block { + std::unique_ptr data; + size_t size = 0; + }; + + /// Returns aligned storage from the current block, adding blocks + /// as needed. + void* Allocate(size_t size, size_t alignment); + + static constexpr size_t BLOCK_SIZE = 64 * 1024; + std::vector m_blocks; + size_t m_blockIndex = 0; + size_t m_offset = 0; + std::vector m_commands; + }; + + /// Moves submission to the next arena, destroying the commands it last + /// held. + void AdvanceArena(); + + CommandArena& CurrentArena() { return m_arenas[m_currentArena]; } + + // A frame's commands stay alive while its shadow replay and GPU frames + // in flight may use them + static constexpr int RETIRE_FRAMES = 3; + static constexpr int ARENA_COUNT = RETIRE_FRAMES + 2; + static RenderCommandQueue* Instance; + CommandArena m_arenas[ARENA_COUNT]; + int m_currentArena = 0; + std::vector cachedShadowDraws; + std::vector m_pendingState; + std::vector m_forwardCmds; }; } } diff --git a/include/private/Graphics/Common/RenderCommands.hpp b/include/private/Graphics/Common/RenderCommands.hpp index 5e4539ef..abe33a9b 100644 --- a/include/private/Graphics/Common/RenderCommands.hpp +++ b/include/private/Graphics/Common/RenderCommands.hpp @@ -148,7 +148,9 @@ namespace Sleak { RENDER_COMMAND(BindConstantBuffer) - RefPtr GetBuffer() const { return constantBuffer; } + const RefPtr& GetBuffer() const { + return constantBuffer; + } int GetSlot() const { return slot; } private: diff --git a/include/private/Graphics/DirectX11/DirectX11Renderer.hpp b/include/private/Graphics/DirectX11/DirectX11Renderer.hpp index be8d4612..5f3ee786 100644 --- a/include/private/Graphics/DirectX11/DirectX11Renderer.hpp +++ b/include/private/Graphics/DirectX11/DirectX11Renderer.hpp @@ -5,17 +5,22 @@ #ifndef _DIRECTX11RENDERER_H #define _DIRECTX11RENDERER_H -#include "Graphics/Common/RenderContext.hpp" -#include "Graphics/Common/Renderer.hpp" -#include "Graphics/Common/ResourceManager.hpp" -#include +#include #include -#include #include -#include -#include +#include + #include +#include +#include #include +#include +#include +#include + +#include "Graphics/Common/RenderContext.hpp" +#include "Graphics/Common/Renderer.hpp" +#include "Graphics/Common/ResourceManager.hpp" namespace Sleak { namespace RenderEngine { @@ -112,6 +117,13 @@ class ENGINE_API DirectX11Renderer : public Renderer, public RenderContext { bool CreateRenderTargetView(); /// Creates and binds the rasterizer state matching current fill/cull settings. bool SetRasterState(); + /// Binds the depth state matching the tracked depth write and compare. + void ApplyDepthState(); + /// Returns the cached rasterizer state for desc, creating it on a miss. + ID3D11RasterizerState* GetRasterState(const D3D11_RASTERIZER_DESC& desc); + /// Returns the cached depth-stencil state for desc, creating it on a miss. + ID3D11DepthStencilState* GetDepthState( + const D3D11_DEPTH_STENCIL_DESC& desc); /// Creates the alpha-blend state used for transparent draws. bool CreateBlendState(); @@ -141,7 +153,7 @@ class ENGINE_API DirectX11Renderer : public Renderer, public RenderContext { ID3D11RenderTargetView* renderTargetView; // Render target view ID3D11InputLayout* layout; - ID3D11DepthStencilState* depthStencilState; + ID3D11DepthStencilState* depthStencilState = nullptr; ID3D11DepthStencilView* depthStencilView; ID3D11Texture2D* depthStencilBuffer; @@ -151,8 +163,8 @@ class ENGINE_API DirectX11Renderer : public Renderer, public RenderContext { ID3D11RenderTargetView* msaaRenderTargetView = nullptr; ID3D11Texture2D* msaaDepthStencilBuffer = nullptr; ID3D11DepthStencilView* msaaDepthStencilView = nullptr; - - ID3D11Query* query; + + ID3D11Query* query = nullptr; D3D11_PRIMITIVE_TOPOLOGY topology; D3D11_FILL_MODE fillMode; @@ -166,6 +178,14 @@ class ENGINE_API DirectX11Renderer : public Renderer, public RenderContext { bool m_depthWriteEnabled = true; D3D11_COMPARISON_FUNC m_depthFunc = D3D11_COMPARISON_LESS; + // Immutable state objects keyed by description, owned until Cleanup + std::vector>> + m_rasterStates; + std::vector>> + m_depthStates; + ImGuiContext* ImCon; // Shadow mapping @@ -186,6 +206,9 @@ class ENGINE_API DirectX11Renderer : public Renderer, public RenderContext { float m_pendingLightVP[16] = {}; bool m_hasPendingLightVP = false; ID3D11Buffer* m_shadowTransformCB = nullptr; // Dedicated CB for shadow pass transforms + // World last uploaded to m_shadowTransformCB, valid until lightVP changes + float m_shadowWorldCache[16] = {}; + bool m_shadowCBCacheValid = false; bool m_inShadowPass = false; void SetLightVP(const float* mat) override; /// Allocates the shadow-pass depth texture, DSV/SRV, and comparison sampler. diff --git a/include/private/Graphics/DirectX12/DirectX12Renderer.hpp b/include/private/Graphics/DirectX12/DirectX12Renderer.hpp index 9adc6df4..c998a8e3 100644 --- a/include/private/Graphics/DirectX12/DirectX12Renderer.hpp +++ b/include/private/Graphics/DirectX12/DirectX12Renderer.hpp @@ -267,6 +267,9 @@ class ENGINE_API DirectX12Renderer : public Renderer, public RenderContext { float m_lightVP[16] = {}; float m_pendingLightVP[16] = {}; bool m_hasPendingLightVP = false; + // Last shadow transform uploaded this pass and the World it was built from + float m_shadowWorldCache[16] = {}; + D3D12_GPU_VIRTUAL_ADDRESS m_shadowCBAddress = 0; bool m_inShadowPass = false; // Depth-only PSO for shadow pass (no PS, no RTV, CULL_NONE) ID3D12PipelineState* m_shadowPassPSO = nullptr; diff --git a/include/private/Graphics/OpenGL/OpenGLRenderer.hpp b/include/private/Graphics/OpenGL/OpenGLRenderer.hpp index 4f926baf..f1f0f1e0 100644 --- a/include/private/Graphics/OpenGL/OpenGLRenderer.hpp +++ b/include/private/Graphics/OpenGL/OpenGLRenderer.hpp @@ -1,18 +1,23 @@ #ifndef _OPENGLRENDERER_H #define _OPENGLRENDERER_H -#include -#include "../Common/Renderer.hpp" -#include "../Common/RenderContext.hpp" -#include "../../Core/Window.hpp" #include +#include #include -#include #include -#include + +#include +#include +#include + +#include "../../Core/Window.hpp" +#include "../Common/RenderContext.hpp" +#include "../Common/Renderer.hpp" namespace Sleak { +struct VertexLayoutDesc; + namespace RenderEngine { /// OpenGL 3.3+ backend: deferred GBuffer pipeline, SSAO, shadow mapping, and baked IBL. @@ -104,6 +109,17 @@ class ENGINE_API OpenGLRenderer : public Renderer, public RenderContext { GLuint m_VAO = 0; bool m_debugLineMode = false; + // Vertex layout currently applied to m_VAO + static constexpr uint32_t NO_CONFIGURED_LAYOUT = UINT32_MAX; + uint32_t m_configuredLayout = NO_CONFIGURED_LAYOUT; + const VertexLayoutDesc* m_configuredLayoutDesc = nullptr; + uint32_t m_enabledAttribMask = 0; + + // Last viewport set outside the shadow pass (x, y, width, height) + std::array m_viewport = {}; + /// Sets the GL viewport and records it so passes can restore it later. + void ApplyViewport(GLint x, GLint y, GLsizei width, GLsizei height); + // MSAA FBO resources GLuint m_msaaFBO = 0; GLuint m_msaaColorRBO = 0; @@ -115,8 +131,11 @@ class ENGINE_API OpenGLRenderer : public Renderer, public RenderContext { virtual void ConfigureRenderMode() override; virtual void ConfigureRenderFace() override; - /// Configures the shared VAO's vertex attribute layout for the standard mesh format. + /// Creates the shared mesh VAO and resets its cached attribute layout. void SetupVertexLayout(); + /// Applies a vertex layout's attribute formats to m_VAO unless it is + /// already the active one; a null desc selects the default Vertex. + void ConfigureVertexLayout(uint32_t layout, const VertexLayoutDesc* desc); // Shadow mapping GLuint m_shadowFBO = 0; @@ -132,6 +151,10 @@ class ENGINE_API OpenGLRenderer : public Renderer, public RenderContext { bool m_shadowMapRendered = false; bool m_inShadowPass = false; GLuint m_shadowTransformUBO = 0; + // Last shadow transform uploaded: [LightVP*World][World], keyed on World + float m_shadowTransformCache[32] = {}; + float m_shadowWorldCache[16] = {}; + bool m_shadowTransformCacheValid = false; void SetLightVP(const float* mat) override; /// Uploads light and shadow data, sampling with the matrix the shadow /// map was rendered with. diff --git a/include/private/Graphics/Vulkan/VulkanRenderer.hpp b/include/private/Graphics/Vulkan/VulkanRenderer.hpp index 95b56fae..2af3b0b7 100644 --- a/include/private/Graphics/Vulkan/VulkanRenderer.hpp +++ b/include/private/Graphics/Vulkan/VulkanRenderer.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include "../Common/RenderContext.hpp" @@ -735,6 +736,17 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { m_pbrMaterialChunks; VkDeviceSize m_pbrMaterialUBOStride = 0; uint32_t m_pbrMaterialSlot[MAX_FRAMES_IN_FLIGHT] = {}; + static constexpr uint32_t PBR_TEXTURE_COUNT = 6; + /// A material's written set contents, reused when it is bound again this + /// frame. + struct PBRMaterialBinding { + PBRMaterialParams params{}; + std::array images{}; + VkDescriptorSet set = VK_NULL_HANDLE; + }; + std::array, + MAX_FRAMES_IN_FLIGHT> + m_pbrMaterialBindings; bool m_pbrMaterialResourcesCreated = false; VkPipelineLayout m_gbufferGeomLayout = VK_NULL_HANDLE; diff --git a/include/public/Core/Application.hpp b/include/public/Core/Application.hpp index b847a51f..61a3dd4f 100644 --- a/include/public/Core/Application.hpp +++ b/include/public/Core/Application.hpp @@ -221,6 +221,7 @@ class ENGINE_API Application { Benchmark* m_benchmark = nullptr; GraphicsConfig m_graphicsConfig; float DeltaTime; + float m_cpuFrameMs = 0.0f; Timer FrameTimer; diff --git a/include/public/Core/GameObject.hpp b/include/public/Core/GameObject.hpp index bc39fbf3..636b3258 100644 --- a/include/public/Core/GameObject.hpp +++ b/include/public/Core/GameObject.hpp @@ -12,6 +12,9 @@ namespace Sleak { namespace Math {class Vector3D;}; class SceneBase; + class TransformComponent; + class MaterialComponent; + class MeshComponent; /// Scene entity holding components and an optional parent/child transform /// hierarchy. Scene owns the instance; destroy via Scene::DestroyObject(). /// @@ -80,6 +83,7 @@ namespace Sleak { RefPtr newComponent = RefPtr(new T(this, std::forward(args)...)); if (bIsInitialized) InitializeComponent(newComponent.get()); Components.add(std::move(newComponent)); + RefreshComponentCache(); } /// Destroys and detaches the first component of type T, if present. @@ -91,6 +95,7 @@ namespace Sleak { if (dynamic_cast(Components[i].get()) != nullptr) { Components[i]->OnDestroy(); Components.erase(i); + RefreshComponentCache(); break; } } @@ -102,15 +107,23 @@ namespace Sleak { static_assert(std::is_base_of_v, "T must derive from Component!"); - for (size_t i = 0; i < Components.GetSize(); ++i) { - Component* rawPtr = Components[i].get(); - if (!rawPtr) continue; + if constexpr (std::is_same_v) { + return m_cachedTransform; + } else if constexpr (std::is_same_v) { + return m_cachedMaterial; + } else if constexpr (std::is_same_v) { + return m_cachedMesh; + } else { + for (size_t i = 0; i < Components.GetSize(); ++i) { + Component* rawPtr = Components[i].get(); + if (!rawPtr) continue; + + T* component = dynamic_cast(rawPtr); + if (component) return component; + } - T* component = dynamic_cast(rawPtr); - if (component) return component; + return nullptr; } - - return nullptr; } /// True if a component of type T is attached. @@ -171,12 +184,19 @@ namespace Sleak { List> Components; + // First matches in Components, kept in sync by RefreshComponentCache + TransformComponent* m_cachedTransform = nullptr; + MaterialComponent* m_cachedMaterial = nullptr; + MeshComponent* m_cachedMesh = nullptr; + // Hierarchy GameObject* m_parent; List m_children; /// Calls OnDestroy() on and drops every attached component. void DestroyComponents(); + /// Re-resolves the cached transform, material and mesh pointers. + void RefreshComponentCache(); /// Initializes a component once, then enables it if active. void InitializeComponent(Component* component); diff --git a/include/public/ECS/Components/MaterialComponent.hpp b/include/public/ECS/Components/MaterialComponent.hpp index 73106988..b8288e0e 100644 --- a/include/public/ECS/Components/MaterialComponent.hpp +++ b/include/public/ECS/Components/MaterialComponent.hpp @@ -32,7 +32,7 @@ namespace Sleak { /// Initializes the owned material's GPU resources. bool Initialize() override; - /// Submits the material to the render command queue for this frame. + /// Submits the material bind unless a sibling mesh does it. void Update(float DeltaTime) override; void OnEnable() override; void OnDisable() override; @@ -54,8 +54,13 @@ namespace Sleak { void SetRoughness(float roughness); private: - RefPtr m_material; - bool m_enabled = true; + friend class MeshComponent; + + /// Queues the material bind while enabled and a material is set. + void SubmitRenderState(); + + RefPtr m_material; + bool m_enabled = true; }; } diff --git a/include/public/ECS/Components/MeshComponent.hpp b/include/public/ECS/Components/MeshComponent.hpp index 1da11242..4d2dfa3d 100644 --- a/include/public/ECS/Components/MeshComponent.hpp +++ b/include/public/ECS/Components/MeshComponent.hpp @@ -32,7 +32,8 @@ namespace Sleak { /// Validates that vertex and index buffers were created successfully. virtual bool Initialize() override; - /// Culls against cull bounds if set, then submits an indexed draw call. + /// Culls, then submits sibling transform and material state and the + /// draw. virtual void Update(float deltaTime) override; void SetVertexBuffer(RefPtr& buffer); diff --git a/include/public/ECS/Components/TransformComponent.hpp b/include/public/ECS/Components/TransformComponent.hpp index e4939684..a07b1f9f 100644 --- a/include/public/ECS/Components/TransformComponent.hpp +++ b/include/public/ECS/Components/TransformComponent.hpp @@ -35,10 +35,10 @@ namespace Sleak { /// Allocates the transform's GPU constant buffer. bool Initialize() override; - /// Refreshes and rebinds the transform constant buffer for this frame. + /// Submits this frame's constant buffer unless a sibling mesh does it. void Update(float DeltaTime) override; - // Transformation matrix calculation + /// World matrix (S*R*T), recomputed only after a transform change. Matrix4 GetTransformMatrix(); // Transformations @@ -91,20 +91,31 @@ namespace Sleak { }; protected: - Vector3D position = Vector3D(0.0f, 0.0f, 0.0f); - Quaternion rotation = Quaternion(); - Vector3D scale = Vector3D(1.0f, 1.0f, 1.0f); - TransformMatrix* Transform = nullptr; - - private: - RefPtr ConstantBuffer; - - /// Rebuilds the transform matrix and pushes it to the GPU constant buffer. - void UpdateConstantBuffer(); - /// Recomputes the cached Translation/Rotation/Scaling matrices from position/rotation/scale. - void UpdateTransform(); - /// Combines the transform with the main camera's view and projection matrices. - Math::Matrix4 GetMVP(); + /// Flags the cached world matrix stale; call after writing fields + /// directly. + void MarkWorldDirty() { m_worldDirty = true; } + + Vector3D position = Vector3D(0.0f, 0.0f, 0.0f); + Quaternion rotation = Quaternion(); + Vector3D scale = Vector3D(1.0f, 1.0f, 1.0f); + + private: + friend class MeshComponent; + + RefPtr ConstantBuffer; + Math::Matrix4 m_world = Math::Matrix4::Identity(); + bool m_worldDirty = true; + + /// Queues the constant buffer upload followed by its slot 0 bind. + void SubmitRenderState(); + /// Pushes the world matrix and main camera matrices to the GPU constant + /// buffer. + void UpdateConstantBuffer(); + /// Returns the cached world matrix, rebuilding it first if dirty. + const Math::Matrix4& WorldMatrix(); + /// Combines the transform with the main camera's view and projection + /// matrices. + Math::Matrix4 GetMVP(); }; } // namespace Sleak diff --git a/include/public/Math/Matrix.hpp b/include/public/Math/Matrix.hpp index bdcaf509..f792b138 100644 --- a/include/public/Math/Matrix.hpp +++ b/include/public/Math/Matrix.hpp @@ -2,14 +2,15 @@ #define _MATRIX_H_ #include -#include "Math.hpp" +#include #include #include #include #include -#include "Vector.hpp" +#include "Math.hpp" #include "Quaternion.hpp" +#include "Vector.hpp" namespace Sleak { namespace Math { @@ -57,18 +58,15 @@ class Matrix { } } - // Bounds-checked element access + /// Element access; indices are bounds-checked by assert in debug builds. T& operator()(size_t row, size_t col) { - if (row >= Rows || col >= Cols) { - throw std::out_of_range("Matrix indices out of range"); - } + assert(row < Rows && col < Cols && "Matrix indices out of range"); return data[row][col]; } + /// Read-only element access; bounds-checked by assert in debug builds. const T& operator()(size_t row, size_t col) const { - if (row >= Rows || col >= Cols) { - throw std::out_of_range("Matrix indices out of range"); - } + assert(row < Rows && col < Cols && "Matrix indices out of range"); return data[row][col]; } @@ -94,14 +92,12 @@ class Matrix { return result; } - // Optimized matrix multiplication with early exit for zero-like cases + /// Row-by-column matrix product. template Matrix operator*( const Matrix& other) const { Matrix result; - // Quick zero check - bool isZero = true; for (size_t i = 0; i < Rows; ++i) { for (size_t j = 0; j < OtherCols; ++j) { T sum = static_cast(0); @@ -109,11 +105,6 @@ class Matrix { sum += data[i][k] * other(k, j); } result(i, j) = sum; - - // Track if result is non-zero - if (std::abs(sum) > std::numeric_limits::epsilon()) { - isZero = false; - } } } diff --git a/include/public/Physics/PhysicsWorld.hpp b/include/public/Physics/PhysicsWorld.hpp index 4a1e3d3d..99e0cd07 100644 --- a/include/public/Physics/PhysicsWorld.hpp +++ b/include/public/Physics/PhysicsWorld.hpp @@ -108,11 +108,12 @@ namespace Sleak { float maxDist, uint32_t layerMask = 0xFFFFFFFF) const; private: - void UpdateBroadphase(); - void FindPairsAndResolve(); + /// Refits every proxy, enlarging dynamic ones along their velocity. + void UpdateBroadphase(float dt); + void FindPairsAndResolve(); - DynamicAABBTree m_tree; - std::vector m_colliders; + DynamicAABBTree m_tree; + std::vector m_colliders; }; } // namespace Physics diff --git a/include/public/Utility/Container/HashTable.hpp b/include/public/Utility/Container/HashTable.hpp index 9f54953d..13fa482f 100644 --- a/include/public/Utility/Container/HashTable.hpp +++ b/include/public/Utility/Container/HashTable.hpp @@ -1,19 +1,22 @@ #ifndef _HASH_TABLE_H_ #define _HASH_TABLE_H_ -#include // For uint64_t -#include // For std::move (can be removed if unwanted) +#include +#include +#include +#include namespace Sleak { - /** +/** * @class HashTable - * @brief Implements a hash table (or map) for efficient key-value storage and retrieval. + * @brief Implements a hash table (or map) for efficient key-value storage and + * retrieval. * - * The HashTable class provides fast access to values based on their associated keys. - * It is suitable for scenarios where you need to quickly look up data using unique - * identifiers. Use this class when you need to store and retrieve data based on keys, - * such as symbol tables, dictionaries, or caching systems. + * The HashTable class provides fast access to values based on their associated + * keys. It is suitable for scenarios where you need to quickly look up data + * using unique identifiers. Use this class when you need to store and retrieve + * data based on keys, such as symbol tables, dictionaries, or caching systems. * * Example Use Cases: * - Implementing a symbol table in a compiler. @@ -23,162 +26,189 @@ namespace Sleak * * Implementation Details: * - Uses a hash function to map keys to indices in an internal array. - * - Handles collisions using chaining or open addressing (specify which one you used). + * - Handles collisions with linear-probing open addressing and tombstones. * - Provides methods for inserting, deleting, and retrieving key-value pairs. * @ingroup utility */ - template - class HashTable - { - private: - /// One slot: key/value plus occupied and tombstone-deleted flags. - struct Entry - { - Key key; - Value value; - bool occupied; - bool deleted; // To handle tombstones in deletion - - Entry() : occupied(false), deleted(false) {} - }; - - Entry* table; - size_t capacity; - size_t size; - float loadFactor; - - static constexpr size_t DEFAULT_CAPACITY = 16; - static constexpr float DEFAULT_LOAD_FACTOR = 0.7f; - - /// Maps a key to a starting slot index via linear probing. - size_t hash(const Key& key) const - { - return reinterpret_cast(&key) % capacity; - } +template +class HashTable { + private: + /// One slot: key/value plus occupied and tombstone-deleted flags. + struct Entry { + Key key; + Value value; + bool occupied; + bool deleted; // To handle tombstones in deletion + + Entry() : occupied(false), deleted(false) {} + }; - /// Doubles capacity and rehashes every live entry into the new table. - void resize() - { - size_t newCapacity = capacity * 2; - Entry* newTable = new Entry[newCapacity]; - - for (size_t i = 0; i < capacity; ++i) - { - if (table[i].occupied && !table[i].deleted) - { - size_t index = hash(table[i].key) % newCapacity; - while (newTable[index].occupied) - { - index = (index + 1) % newCapacity; - } - newTable[index] = std::move(table[i]); + Entry* table; + size_t capacity; + size_t size; + size_t tombstones; + float loadFactor; + + static constexpr size_t DEFAULT_CAPACITY = 16; + static constexpr float DEFAULT_LOAD_FACTOR = 0.7f; + static constexpr float MAX_LOAD_FACTOR = 1.0f; + static constexpr size_t MIN_CAPACITY = 1; + static constexpr size_t GROWTH_FACTOR = 2; + + /// Home slot of key in a table with tableCapacity slots. + static size_t slotFor(const Key& key, size_t tableCapacity) { + return std::hash{}(key) % tableCapacity; + } + + /// Home slot of key in the current table. + size_t hash(const Key& key) const { return slotFor(key, capacity); } + + /// Slot holding a live entry for key, or capacity if there is none. + size_t findSlot(const Key& key) const { + size_t index = hash(key); + for (size_t probes = 0; probes < capacity && table[index].occupied; + ++probes) { + if (!table[index].deleted && table[index].key == key) { + return index; + } + index = (index + 1) % capacity; + } + return capacity; + } + + /// Moves live entries into a newCapacity table, dropping tombstones. + void rehash(size_t newCapacity) { + Entry* newTable = new Entry[newCapacity]; + + for (size_t i = 0; i < capacity; ++i) { + if (table[i].occupied && !table[i].deleted) { + size_t index = slotFor(table[i].key, newCapacity); + while (newTable[index].occupied) { + index = (index + 1) % newCapacity; } + newTable[index] = std::move(table[i]); } - - delete[] table; - table = newTable; - capacity = newCapacity; } - public: - HashTable(size_t initCapacity = DEFAULT_CAPACITY, float loadFactor = DEFAULT_LOAD_FACTOR) - : capacity(initCapacity), size(0), loadFactor(loadFactor) - { - table = new Entry[capacity]; + delete[] table; + table = newTable; + capacity = newCapacity; + tombstones = 0; + } + + /// Grows if live entries fill the table, else purges tombstones only. + void resize() { + const bool mostlyLive = size * GROWTH_FACTOR >= capacity * loadFactor; + rehash(mostlyLive ? capacity * GROWTH_FACTOR : capacity); + } + + public: + HashTable(size_t initCapacity = DEFAULT_CAPACITY, + float loadFactor = DEFAULT_LOAD_FACTOR) + : capacity(initCapacity > MIN_CAPACITY ? initCapacity : MIN_CAPACITY), + size(0), + tombstones(0), + loadFactor(loadFactor > 0.0f && loadFactor <= MAX_LOAD_FACTOR + ? loadFactor + : DEFAULT_LOAD_FACTOR) { + table = new Entry[capacity]; + } + + /// Deep-copies other's slots, including its capacity and load factor. + HashTable(const HashTable& other) + : table(new Entry[other.capacity]), + capacity(other.capacity), + size(other.size), + tombstones(other.tombstones), + loadFactor(other.loadFactor) { + for (size_t i = 0; i < capacity; ++i) { + table[i] = other.table[i]; } - - ~HashTable() - { - delete[] table; + } + + /// Replaces this table with a deep copy of other. + HashTable& operator=(const HashTable& other) { + if (this != &other) { + HashTable copy(other); + std::swap(table, copy.table); + std::swap(capacity, copy.capacity); + std::swap(size, copy.size); + std::swap(tombstones, copy.tombstones); + std::swap(loadFactor, copy.loadFactor); } - - /// Inserts or updates the value for key, resizing first if over the load factor. - void insert(const Key& key, const Value& value) - { - if (size >= capacity * loadFactor) - { - resize(); - } - - size_t index = hash(key) % capacity; - while (table[index].occupied && !table[index].deleted && table[index].key != key) - { - index = (index + 1) % capacity; - } - - if (!table[index].occupied || table[index].deleted) - { - table[index].key = key; - table[index].value = value; - table[index].occupied = true; - table[index].deleted = false; - ++size; - } - else - { - table[index].value = value; - } + return *this; + } + + ~HashTable() { delete[] table; } + + /// Inserts or updates the value for key, resizing first if over the load + /// factor. + void insert(const Key& key, const Value& value) { + const size_t existing = findSlot(key); + if (existing != capacity) { + table[existing].value = value; + return; } - /// Tombstones the entry for key; returns false if it wasn't present. - bool remove(const Key& key) - { - size_t index = hash(key) % capacity; - while (table[index].occupied) - { - if (!table[index].deleted && table[index].key == key) - { - table[index].deleted = true; - --size; - return true; - } - index = (index + 1) % capacity; - } - return false; + if (size + tombstones >= capacity * loadFactor) { + resize(); } - /// Looks up key and writes its value into outValue; returns false if not found. - bool get(const Key& key, Value& outValue) const - { - size_t index = hash(key) % capacity; - while (table[index].occupied) - { - if (!table[index].deleted && table[index].key == key) - { - outValue = table[index].value; - return true; - } - index = (index + 1) % capacity; - } - return false; + size_t index = hash(key); + while (table[index].occupied && !table[index].deleted) { + index = (index + 1) % capacity; } - /// True if key currently has a live entry. - bool contains(const Key& key) const - { - size_t index = hash(key) % capacity; - while (table[index].occupied) - { - if (!table[index].deleted && table[index].key == key) - { - return true; - } - index = (index + 1) % capacity; - } + if (table[index].deleted) { + --tombstones; + } + table[index].key = key; + table[index].value = value; + table[index].occupied = true; + table[index].deleted = false; + ++size; + } + + /// Tombstones the entry for key; returns false if it wasn't present. + bool remove(const Key& key) { + const size_t index = findSlot(key); + if (index == capacity) { return false; } - - /// Drops every entry and reallocates the table at its current capacity. - void clear() - { - delete[] table; - table = new Entry[capacity]; - size = 0; + table[index].key = Key(); + table[index].value = Value(); + table[index].deleted = true; + --size; + ++tombstones; + return true; + } + + /// Looks up key and writes its value into outValue; returns false if not + /// found. + bool get(const Key& key, Value& outValue) const { + const size_t index = findSlot(key); + if (index == capacity) { + return false; } - - size_t getSize() const { return size; } - size_t getCapacity() const { return capacity; } - }; + outValue = table[index].value; + return true; + } + + /// True if key currently has a live entry. + bool contains(const Key& key) const { return findSlot(key) != capacity; } + + /// Drops every entry and reallocates the table at its current capacity. + void clear() { + Entry* emptyTable = new Entry[capacity]; + delete[] table; + table = emptyTable; + size = 0; + tombstones = 0; + } + + size_t getSize() const { return size; } + size_t getCapacity() const { return capacity; } +}; } #endif // _HASH_TABLE_H_ diff --git a/include/public/Utility/Container/List.hpp b/include/public/Utility/Container/List.hpp index 49c48cb6..7a48b6ab 100644 --- a/include/public/Utility/Container/List.hpp +++ b/include/public/Utility/Container/List.hpp @@ -4,6 +4,7 @@ #include #include // For std::swap #include +#include namespace Sleak { /** @@ -23,45 +24,20 @@ class List { size_t size = 0; size_t capacity = 0; - /// Recursive quicksort driving sort(). - void quickSort(int low, int high, - std::function compare) { - if (low < high) { - int pivotIndex = partition(low, high, compare); - quickSort(low, pivotIndex - 1, compare); - quickSort(pivotIndex + 1, high, compare); - } - } - - /// Lomuto partition step for quickSort; returns the pivot's final index. - int partition(int low, int high, - std::function compare) { - T pivot = data[high]; - int i = low - 1; - - for (int j = low; j < high; j++) { - if (compare(data[j], pivot)) { - i++; - std::swap(data[i], data[j]); - } - } - - std::swap(data[i + 1], data[high]); - return i + 1; - } - public: List() noexcept = default; /// Adopts an existing buffer directly; List takes ownership. List(T* Data, size_t Size) : data(Data), size(Size), capacity(Size) {} - // Copy constructor - List(const List& other) : size(other.size), capacity(other.capacity) { - data = new T[capacity]; - for (size_t i = 0; i < size; ++i) { - data[i] = other.data[i]; - } + /// Copies other's elements into an exact-size buffer; empty allocates none. + List(const List& other) { + if (other.size == 0) return; + std::unique_ptr copy(new T[other.size]); + std::copy(other.data, other.data + other.size, copy.get()); + data = copy.release(); + size = other.size; + capacity = other.size; } // Move constructor @@ -72,16 +48,11 @@ class List { other.capacity = 0; } - // Copy assignment + /// Replaces contents with a copy of other; strong exception guarantee. List& operator=(const List& other) { if (this != &other) { - delete[] data; - size = other.size; - capacity = other.capacity; - data = new T[capacity]; - for (size_t i = 0; i < size; ++i) { - data[i] = other.data[i]; - } + List copy(other); + swap(copy); } return *this; } @@ -225,8 +196,10 @@ class List { } // Operations - void sort(std::function compare) { - quickSort(0, size - 1, compare); + /// Sorts in place; compare must be a strict weak ordering. + template + void sort(Compare compare) { + std::sort(data, data + size, compare); } void reverse() { diff --git a/include/public/Utility/Container/Queue.hpp b/include/public/Utility/Container/Queue.hpp index f0847813..a4b6fad6 100644 --- a/include/public/Utility/Container/Queue.hpp +++ b/include/public/Utility/Container/Queue.hpp @@ -1,98 +1,146 @@ #ifndef _QUEUE_H_ #define _QUEUE_H_ -#include "List.hpp" +#include #include +#include + +#include "List.hpp" namespace Sleak { - - /** - * @class Queue - * @brief Implements a FIFO (First-In, First-Out) queue data structure. - * - * The Queue class maintains a collection of elements in the order they were added. It - * is suitable for scenarios where you need to process elements in a first-come, - * first-served manner. Use this class when you need to implement task queues, - * message queues, or breadth-first search algorithms. - * - * Example Use Cases: - * - Implementing a task queue for background processing. - * - Managing message queues in a network communication system. - * - Performing breadth-first search in graph algorithms. - * - Simulating real-world queues, such as waiting lines. - * - * Implementation Details: - * - Uses an underlying List to store the queue elements. - * - Provides methods for enqueueing (adding) and dequeueing (removing) elements. - * - Maintains the FIFO order of elements. - * @ingroup utility - */ - template - class Queue { - public: - Queue() = default; - - /// Adds value to the back of the queue. - void push(const T& value) { - data.add(value); - } - - /// Removes and returns the front element; throws if empty. - T pop() { - if (isEmpty()) { - throw Sleak::EmptyContainerException(); - } - - T front = std::move(data[0]); - - data.erase(0); - - return front; - } - - /// Front element without removing it; throws if empty. - T& front() { - if (isEmpty()) { - throw Sleak::EmptyContainerException(); - } - return data[0]; - } - - const T& front() const { - if (isEmpty()) { - throw Sleak::EmptyContainerException(); - } - return data[0]; - } - - /// Reverses the queue's element order in place. - const void reverse() { - data.reverse(); - } - - bool isEmpty() const { - return data.GetSize() == 0; - } - - size_t size() const { return data.GetSize(); } - - void clear() { - data.clear(); - } - - /// Copies out the underlying List, front-to-back. - List GetData() { return data; } - - // Iterators - T* begin() { return data.begin(); } - const T* begin() const { return data.begin(); } - T* end() { return data.end(); } - const T* end() const { return data.end(); } - - private: - List data; - }; + +/** + * @class Queue + * @brief Implements a FIFO (First-In, First-Out) queue data structure. + * + * The Queue class maintains a collection of elements in the order they were + * added. It is suitable for scenarios where you need to process elements in a + * first-come, first-served manner. Use this class when you need to implement + * task queues, message queues, or breadth-first search algorithms. + * + * Example Use Cases: + * - Implementing a task queue for background processing. + * - Managing message queues in a network communication system. + * - Performing breadth-first search in graph algorithms. + * - Simulating real-world queues, such as waiting lines. + * + * Implementation Details: + * - Uses an underlying List plus a head index; pop is amortized O(1). + * - Provides methods for enqueueing (adding) and dequeueing (removing) + * elements. + * - Maintains the FIFO order of elements. + * @ingroup utility + */ +template +class Queue { + public: + Queue() = default; + Queue(const Queue&) = default; + Queue& operator=(const Queue&) = default; + + /// Takes other's elements, leaving it empty. + Queue(Queue&& other) noexcept + : data(std::move(other.data)), head(std::exchange(other.head, 0)) {} + + /// Replaces this queue with other's elements, leaving other empty. + Queue& operator=(Queue&& other) noexcept { + if (this != &other) { + data = std::move(other.data); + head = std::exchange(other.head, 0); + } + return *this; + } + + /// Adds value to the back of the queue. + void push(const T& value) { data.add(value); } + + /// Moves value onto the back of the queue. + void push(T&& value) { data.add(std::move(value)); } + + /// Removes and returns the front element; throws if empty. + T pop() { + if (isEmpty()) { + throw Sleak::EmptyContainerException(); + } + + T* slots = data.GetData(); + T front = std::move(slots[head]); + slots[head] = T(); + ++head; + compactIfSparse(); + + return front; + } + + /// Front element without removing it; throws if empty. + T& front() { + if (isEmpty()) { + throw Sleak::EmptyContainerException(); + } + return data.GetData()[head]; + } + + const T& front() const { + if (isEmpty()) { + throw Sleak::EmptyContainerException(); + } + return data.GetData()[head]; + } + + /// Reverses the queue's element order in place. + const void reverse() { std::reverse(begin(), end()); } + + bool isEmpty() const { return size() == 0; } + + size_t size() const { return data.GetSize() - head; } + + void clear() { + data.clear(); + head = 0; + } + + /// Copies out the queued elements, front-to-back. + List GetData() { + List out; + out.resize(size()); + for (const T& value : *this) { + out.add(value); + } + return out; + } + + // Iterators + T* begin() { return data.begin() + head; } + const T* begin() const { return data.begin() + head; } + T* end() { return data.end(); } + const T* end() const { return data.end(); } + + private: + /// Slides live elements to the front once popped slots are the majority. + void compactIfSparse() { + const size_t live = size(); + if (head < live) { + return; + } + if (live == 0) { + clear(); + return; + } + + T* slots = data.GetData(); + for (size_t i = 0; i < live; ++i) { + slots[i] = std::move(slots[head + i]); + } + while (data.GetSize() > live) { + data.erase(data.GetSize() - 1); + } + head = 0; + } + + List data; + size_t head = 0; +}; } #endif // _QUEUE_H_ \ No newline at end of file diff --git a/src/Animation/AnimatorComponent.cpp b/src/Animation/AnimatorComponent.cpp index 919f9ed4..374f8add 100644 --- a/src/Animation/AnimatorComponent.cpp +++ b/src/Animation/AnimatorComponent.cpp @@ -1,10 +1,11 @@ +#include +#include #include #include -#include -#include #include +#include #include -#include +#include #include namespace Sleak { @@ -223,11 +224,13 @@ void AnimatorComponent::ProcessNodeHierarchy(int nodeIndex, /// Finds the keyframe pair straddling time and the lerp factor between them. template static std::pair FindKeyframe(const std::vector>& keys, float time) { - int idx = 0; - for (int i = 0; i < static_cast(keys.size()) - 1; ++i) { - if (time < keys[i + 1].time) { idx = i; break; } - idx = i; - } + if (keys.size() < 2) return {0, 0.0f}; + + // First key in [1, size - 1) later than time; the last key otherwise + auto upper = std::upper_bound( + keys.begin() + 1, keys.end() - 1, time, + [](float t, const Keyframe& key) { return t < key.time; }); + int idx = static_cast(upper - keys.begin()) - 1; int next = idx + 1; if (next >= static_cast(keys.size())) return {idx, 0.0f}; diff --git a/src/Assets/ModelLoader.cpp b/src/Assets/ModelLoader.cpp index 5cc3dc0c..ea014cdc 100644 --- a/src/Assets/ModelLoader.cpp +++ b/src/Assets/ModelLoader.cpp @@ -45,7 +45,6 @@ GameObject* ModelLoader::Load(const std::string& filePath, const ModelLoadOptions& options) { Assimp::Importer importer; - // First pass: read without PreTransformVertices to check for animations unsigned int baseFlags = aiProcess_Triangulate | aiProcess_GenSmoothNormals | aiProcess_CalcTangentSpace @@ -55,7 +54,9 @@ GameObject* ModelLoader::Load(const std::string& filePath, if (options.flipUVs) baseFlags |= aiProcess_FlipUVs; - const aiScene* scene = importer.ReadFile(filePath, baseFlags); + // Parse once without post-processing to learn whether it is animated + constexpr unsigned int rawImportFlags = 0; + const aiScene* scene = importer.ReadFile(filePath, rawImportFlags); if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) { SLEAK_ERROR("Assimp: Failed to load model '{}': {}", filePath, @@ -65,6 +66,18 @@ GameObject* ModelLoader::Load(const std::string& filePath, bool hasAnimations = scene->HasAnimations(); + // Static models also get PreTransformVertices, in the same pipeline order + unsigned int postFlags = baseFlags; + if (!hasAnimations) postFlags |= aiProcess_PreTransformVertices; + scene = importer.ApplyPostProcessing(postFlags); + + if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || + !scene->mRootNode) { + SLEAK_ERROR("Assimp: Failed to post-process model '{}': {}", filePath, + importer.GetErrorString()); + return nullptr; + } + SLEAK_INFO("Model loaded: {} ({} meshes, {} materials, {} textures, {} animations)", filePath, scene->mNumMeshes, scene->mNumMaterials, scene->mNumTextures, scene->mNumAnimations); @@ -75,18 +88,6 @@ GameObject* ModelLoader::Load(const std::string& filePath, MaterialCache materialCache; if (!hasAnimations) { - // Static model: reimport with PreTransformVertices for optimization - importer.FreeScene(); - unsigned int staticFlags = baseFlags | aiProcess_PreTransformVertices; - scene = importer.ReadFile(filePath, staticFlags); - - if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) { - SLEAK_ERROR("Assimp: Failed to reimport model '{}': {}", filePath, - importer.GetErrorString()); - delete root; - return nullptr; - } - ProcessNode(scene->mRootNode, scene, root, directory, options, textureCache, materialCache); } else { diff --git a/src/Core/Application.cpp b/src/Core/Application.cpp index c7e7dc73..00fa941c 100644 --- a/src/Core/Application.cpp +++ b/src/Core/Application.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -145,6 +146,8 @@ namespace Sleak { m_benchmark = new Benchmark(); m_benchmark->Initialize(renderer); renderer->RegisterBenchmarkMetrics(*m_benchmark); + m_benchmark->RegisterMetric("CPU_frame_ms", + [this]() { return m_cpuFrameMs; }); { auto& cfg = m_DebugOverlay->GetConfig(); cfg.ShowCameraPanel = false; @@ -252,6 +255,7 @@ namespace Sleak { Debug::ShaderHotReload::Poll(); renderer->BeginRender(); + const auto cpuFrameStart = std::chrono::steady_clock::now(); // Update active scene if present if (Game && Game->GetActiveScene()) { @@ -285,6 +289,10 @@ namespace Sleak { if (queue && context) queue->ExecuteCommands(context); + m_cpuFrameMs = + std::chrono::duration( + std::chrono::steady_clock::now() - cpuFrameStart) + .count(); renderer->EndRender(); if (captureArmed && !renderer->IsFrameCapturePending()) diff --git a/src/Culling/CullingSystem.cpp b/src/Culling/CullingSystem.cpp index dd5cb2e1..fec8f9a7 100644 --- a/src/Culling/CullingSystem.cpp +++ b/src/Culling/CullingSystem.cpp @@ -1,15 +1,23 @@ #include - #include #include #include +#include #include +#if defined(__SSE2__) || defined(_M_X64) +#define SLEAK_CULL_SSE2 1 +#include +#else +#define SLEAK_CULL_SSE2 0 +#endif + namespace Sleak { namespace { constexpr float kWEps = 1e-4f; constexpr float kZBias = 1e-4f; +constexpr int kLanes = 4; /// Clip-space vertex (row-vector convention: clip = point * VP). struct ClipVert { @@ -102,6 +110,143 @@ int ClipNear(const ClipVert* in, int n, ClipVert* out) { return m; } +/// Clamped pixel bounds, edge values at the first pixel center and their +/// per-pixel (A) and per-row (B) steps for one screen triangle. +struct TriSetup { + int minX, maxX, minY, maxY; + float w0Row, w1Row, w2Row; + float A0, A1, A2; + float B0, B1, B2; + float z0, z1, z2; + float invArea; + bool positive; +}; + +#if SLEAK_CULL_SSE2 +/// Per-lane inside test for one pixel column; kPositive picks the winding. +template +inline __m128 EdgeMask(__m128 w0, __m128 w1, __m128 w2) { + const __m128 zero = _mm_setzero_ps(); + if constexpr (kPositive) { + return _mm_and_ps( + _mm_and_ps(_mm_cmpge_ps(w0, zero), _mm_cmpge_ps(w1, zero)), + _mm_cmpge_ps(w2, zero)); + } else { + return _mm_and_ps( + _mm_and_ps(_mm_cmple_ps(w0, zero), _mm_cmple_ps(w1, zero)), + _mm_cmple_ps(w2, zero)); + } +} + +/// Min-merges the first n lanes of v into dst with the scalar compare. +inline void MinStorePartial(float* dst, __m128 v, int n) { + alignas(16) float tmp[kLanes]; + _mm_store_ps(tmp, v); + for (int i = 0; i < n; ++i) + if (tmp[i] < dst[i]) dst[i] = tmp[i]; +} + +/// SSE2 fill of 4-row strips; lanes are rows so each lane repeats the scalar +/// x-stepping exactly, and 4x4 tiles are transposed for row-major stores. +template +void RasterTriSse2(const TriSetup& t, float* depth, int W) { + const __m128 a0 = _mm_set1_ps(t.A0); + const __m128 a1 = _mm_set1_ps(t.A1); + const __m128 a2 = _mm_set1_ps(t.A2); + const __m128 zv0 = _mm_set1_ps(t.z0); + const __m128 zv1 = _mm_set1_ps(t.z1); + const __m128 zv2 = _mm_set1_ps(t.z2); + const __m128 inv = _mm_set1_ps(t.invArea); + const __m128 zero = _mm_setzero_ps(); + const __m128 one = _mm_set1_ps(1.0f); + const __m128 untouched = + _mm_set1_ps(std::numeric_limits::infinity()); + + float w0Row = t.w0Row, w1Row = t.w1Row, w2Row = t.w2Row; + for (int y = t.minY; y <= t.maxY; y += kLanes) { + alignas(16) float r0[kLanes], r1[kLanes], r2[kLanes]; + for (int i = 0; i < kLanes; ++i) { + r0[i] = w0Row; + r1[i] = w1Row; + r2[i] = w2Row; + w0Row += t.B0; + w1Row += t.B1; + w2Row += t.B2; + } + const int rows = std::min(kLanes, t.maxY - y + 1); + __m128 e0 = _mm_load_ps(r0); + __m128 e1 = _mm_load_ps(r1); + __m128 e2 = _mm_load_ps(r2); + float* rowBase = depth + y * W; + + for (int x = t.minX; x <= t.maxX; x += kLanes) { + __m128 s0 = e0, s1 = e1, s2 = e2; + __m128 m[kLanes]; + __m128 any = zero; + for (int k = 0; k < kLanes; ++k) { + m[k] = EdgeMask(e0, e1, e2); + any = _mm_or_ps(any, m[k]); + e0 = _mm_add_ps(e0, a0); + e1 = _mm_add_ps(e1, a1); + e2 = _mm_add_ps(e2, a2); + } + if (_mm_movemask_ps(any) == 0) continue; + + __m128 zc[kLanes]; + for (int k = 0; k < kLanes; ++k) { + __m128 z = _mm_add_ps( + _mm_add_ps(_mm_mul_ps(s0, zv0), _mm_mul_ps(s1, zv1)), + _mm_mul_ps(s2, zv2)); + z = _mm_min_ps(one, _mm_mul_ps(z, inv)); + zc[k] = _mm_max_ps(_mm_andnot_ps(m[k], untouched), z); + s0 = _mm_add_ps(s0, a0); + s1 = _mm_add_ps(s1, a1); + s2 = _mm_add_ps(s2, a2); + } + _MM_TRANSPOSE4_PS(zc[0], zc[1], zc[2], zc[3]); + + float* p = rowBase + x; + const int cols = std::min(kLanes, t.maxX - x + 1); + if (cols == kLanes) { + for (int r = 0; r < rows; ++r) { + float* q = p + r * W; + _mm_storeu_ps(q, _mm_min_ps(zc[r], _mm_loadu_ps(q))); + } + } else { + for (int r = 0; r < rows; ++r) + MinStorePartial(p + r * W, zc[r], cols); + } + } + } +} +#else +/// Scalar fill of the triangle's bounding box with min-depth writes. +void RasterTriScalar(const TriSetup& t, float* depth, int W) { + float w0Row = t.w0Row, w1Row = t.w1Row, w2Row = t.w2Row; + for (int y = t.minY; y <= t.maxY; ++y) { + float w0 = w0Row, w1 = w1Row, w2 = w2Row; + int row = y * W; + for (int x = t.minX; x <= t.maxX; ++x) { + bool inside = t.positive ? (w0 >= 0.0f && w1 >= 0.0f && w2 >= 0.0f) + : (w0 <= 0.0f && w1 <= 0.0f && w2 <= 0.0f); + if (inside) { + float z = (w0 * t.z0 + w1 * t.z1 + w2 * t.z2) * t.invArea; + if (z < 0.0f) z = 0.0f; + if (z > 1.0f) z = 1.0f; + int idx = row + x; + if (z < depth[idx]) depth[idx] = z; + } + w0 += t.A0; + w1 += t.A1; + w2 += t.A2; + } + w0Row += t.B0; + w1Row += t.B1; + w2Row += t.B2; + } +} +#endif + /// Edge-function rasterizer with incremental row stepping, min-depth write. void RasterScreenTri(float x0, float y0, float z0, float x1, float y1, float z1, float x2, float y2, float z2) { @@ -130,35 +275,34 @@ void RasterScreenTri(float x0, float y0, float z0, float x1, float y1, float px0 = minX + 0.5f; float py0 = minY + 0.5f; - float w0Row = (x2 - x1) * (py0 - y1) - (y2 - y1) * (px0 - x1); - float w1Row = (x0 - x2) * (py0 - y2) - (y0 - y2) * (px0 - x2); - float w2Row = (x1 - x0) * (py0 - y0) - (y1 - y0) * (px0 - x0); - - float A0 = y1 - y2, A1 = y2 - y0, A2 = y0 - y1; - float B0 = x2 - x1, B1 = x0 - x2, B2 = x1 - x0; - bool positive = area > 0.0f; - - for (int y = minY; y <= maxY; ++y) { - float w0 = w0Row, w1 = w1Row, w2 = w2Row; - int row = y * W; - for (int x = minX; x <= maxX; ++x) { - bool inside = positive ? (w0 >= 0.0f && w1 >= 0.0f && w2 >= 0.0f) - : (w0 <= 0.0f && w1 <= 0.0f && w2 <= 0.0f); - if (inside) { - float z = (w0 * z0 + w1 * z1 + w2 * z2) * invArea; - if (z < 0.0f) z = 0.0f; - if (z > 1.0f) z = 1.0f; - int idx = row + x; - if (z < s.depth[idx]) s.depth[idx] = z; - } - w0 += A0; - w1 += A1; - w2 += A2; - } - w0Row += B0; - w1Row += B1; - w2Row += B2; - } + TriSetup t; + t.minX = minX; + t.maxX = maxX; + t.minY = minY; + t.maxY = maxY; + t.w0Row = (x2 - x1) * (py0 - y1) - (y2 - y1) * (px0 - x1); + t.w1Row = (x0 - x2) * (py0 - y2) - (y0 - y2) * (px0 - x2); + t.w2Row = (x1 - x0) * (py0 - y0) - (y1 - y0) * (px0 - x0); + t.A0 = y1 - y2; + t.A1 = y2 - y0; + t.A2 = y0 - y1; + t.B0 = x2 - x1; + t.B1 = x0 - x2; + t.B2 = x1 - x0; + t.z0 = z0; + t.z1 = z1; + t.z2 = z2; + t.invArea = invArea; + t.positive = area > 0.0f; + +#if SLEAK_CULL_SSE2 + if (t.positive) + RasterTriSse2(t, s.depth.data(), W); + else + RasterTriSse2(t, s.depth.data(), W); +#else + RasterTriScalar(t, s.depth.data(), W); +#endif } /// Near-clip a clip-space triangle, fan-triangulate, rasterize. @@ -212,6 +356,32 @@ void RasterBox(const Math::AABB& box, const Math::Matrix4& vp, if (cam.GetZ() > mxz) quad(c001, c101, c111, c011); } +/// True when any depth in the inclusive pixel rect is >= thresh. +bool AnyDepthAtOrBehind(const float* depth, int W, int minX, int maxX, int minY, + int maxY, float thresh) { +#if SLEAK_CULL_SSE2 + if (maxX - minX + 1 >= kLanes) { + const __m128 t = _mm_set1_ps(thresh); + const int lastX = maxX - kLanes + 1; + for (int y = minY; y <= maxY; ++y) { + const float* row = depth + y * W; + __m128 hit = _mm_cmpge_ps(_mm_loadu_ps(row + lastX), t); + for (int x = minX; x < lastX; x += kLanes) + hit = _mm_or_ps(hit, _mm_cmpge_ps(_mm_loadu_ps(row + x), t)); + if (_mm_movemask_ps(hit) != 0) return true; + } + return false; + } +#endif + for (int y = minY; y <= maxY; ++y) { + const float* row = depth + y * W; + for (int x = minX; x <= maxX; ++x) { + if (row[x] >= thresh) return true; + } + } + return false; +} + /// Conservative depth test: true when the box could be in front of the /// rasterized occluders anywhere in its projected rect. bool DepthRectVisible(const Math::AABB& box) { @@ -244,14 +414,8 @@ bool DepthRectVisible(const Math::AABB& box) { if (maxY > H - 1) maxY = H - 1; if (minX > maxX || minY > maxY) return true; - float thresh = boxMinZ - kZBias; - for (int y = minY; y <= maxY; ++y) { - int row = y * W; - for (int x = minX; x <= maxX; ++x) { - if (s.depth[row + x] >= thresh) return true; - } - } - return false; + return AnyDepthAtOrBehind(s.depth.data(), W, minX, maxX, minY, maxY, + boxMinZ - kZBias); } /// True when the box projects entirely in front but spans < 2x2 px. diff --git a/src/Graphics/Common/RenderCommandQueue.cpp b/src/Graphics/Common/RenderCommandQueue.cpp index b5946090..2696d507 100644 --- a/src/Graphics/Common/RenderCommandQueue.cpp +++ b/src/Graphics/Common/RenderCommandQueue.cpp @@ -1,10 +1,13 @@ #include "../../include/private/Graphics/Common/RenderCommandQueue.hpp" + +#include +#include +#include +#include + #include "../../include/private/Graphics/Common/BufferBase.hpp" -#include "../../include/private/Graphics/Common/RenderContext.hpp" #include "../../include/private/Graphics/Common/RenderCommands.hpp" -#include -#include -#include +#include "../../include/private/Graphics/Common/RenderContext.hpp" namespace Sleak { namespace RenderEngine { @@ -15,8 +18,9 @@ namespace Sleak { List> constantBuffers, uint32_t vertexCount, uint32_t startVertexLocation) { - auto command = RefPtr(new DrawCommand(vertexBuffer,constantBuffers,vertexCount, startVertexLocation)); - commands.push(std::move(command)); + CurrentArena().Create( + std::move(vertexBuffer), std::move(constantBuffers), + vertexCount, startVertexLocation); } void RenderCommandQueue::SubmitDrawIndexed( @@ -24,43 +28,37 @@ namespace Sleak { List> constantBuffers, uint32_t indexCount, uint32_t startIndexLocation, int32_t baseVertexLocation, bool castsShadow) { - auto command = RefPtr(new DrawIndexedCommand( - vertexBuffer, indexBuffer, constantBuffers, indexCount, - startIndexLocation, baseVertexLocation)); + auto* command = CurrentArena().Create( + std::move(vertexBuffer), std::move(indexBuffer), + std::move(constantBuffers), indexCount, startIndexLocation, + baseVertexLocation); command->SetCastsShadow(castsShadow); - commands.push(std::move(command)); } void RenderCommandQueue::SubmitBindConstantBuffer(RefPtr buffer, uint8_t slot) { - auto command = RefPtr(new BindConstantBufferCommand(buffer, slot)); - commands.push(std::move(command)); + CurrentArena().Create(std::move(buffer), + slot); } void RenderCommandQueue::SubmitUpdateConstantBuffer(RefPtr buffer, void* Data, uint16_t Size) { - auto command = RefPtr(new UpdateConstantBufferCommand(buffer, Data, Size)); - commands.push(std::move(command)); + CurrentArena().Create( + std::move(buffer), Data, Size); } void RenderCommandQueue::SubmitBindMaterial(::Sleak::Material* material) { - auto command = RefPtr(new BindMaterialCommand(material)); - commands.push(std::move(command)); + CurrentArena().Create(material); } void RenderCommandQueue::SubmitSetRenderMode(RenderMode mode) { - auto command = RefPtr(new SetRenderModeCommand(mode)); - commands.push(command); + CurrentArena().Create(mode); } void RenderCommandQueue::SubmitSetRenderFace(RenderFace face) { - auto command = RefPtr(new SetRenderFaceCommand(face)); - commands.push(command); + CurrentArena().Create(face); } void RenderCommandQueue::SubmitCustomCommand(CustomCommand::ExecuteFunction function) { - auto command = - RefPtr(new CustomCommand(function)); - commands.push(std::move(command)); - + CurrentArena().Create(std::move(function)); } void RenderCommandQueue::ExecuteCommands(RenderContext* context) { @@ -68,27 +66,29 @@ namespace Sleak { SortCommands(); OptimizeBatching(); + const auto& commands = CurrentArena().Commands(); + // Cache draw commands for shadow pass replay next frame, // pairing each with the last-bound slot-0 (transform) buffer. - m_retiredShadowDraws[m_retireIndex].clear(); - m_retiredShadowDraws[m_retireIndex] = std::move(cachedShadowDraws); - m_retireIndex = (m_retireIndex + 1) % RETIRE_FRAMES; cachedShadowDraws.clear(); { - RefPtr lastSlot0Buffer; - RefPtr lastSlot3Buffer; - for (auto& cmd : commands) { + const RefPtr* lastSlot0Buffer = nullptr; + const RefPtr* lastSlot3Buffer = nullptr; + for (RenderCommandBase* cmd : commands) { auto type = cmd->GetType(); if (type == CommandType::BindConstantBuffer) { - auto* bindCmd = static_cast(cmd.get()); + auto* bindCmd = + static_cast(cmd); if (bindCmd->GetSlot() == 0) { - lastSlot0Buffer = bindCmd->GetBuffer(); + lastSlot0Buffer = &bindCmd->GetBuffer(); } else if (bindCmd->GetSlot() == 3) { - lastSlot3Buffer = bindCmd->GetBuffer(); + lastSlot3Buffer = &bindCmd->GetBuffer(); } } - if ((type == CommandType::Draw || type == CommandType::DrawIndexed) - && lastSlot0Buffer && cmd->CastsShadow()) { + if ((type == CommandType::Draw || + type == CommandType::DrawIndexed) && + lastSlot0Buffer && *lastSlot0Buffer && + cmd->CastsShadow()) { ShadowDrawEntry entry; entry.command = cmd; entry.transformBuffer = lastSlot0Buffer; @@ -96,7 +96,7 @@ namespace Sleak { // casters don't accidentally inherit a stale bone UBO. if (cmd->IsSkinned()) entry.boneBuffer = lastSlot3Buffer; - cachedShadowDraws.add(entry); + cachedShadowDraws.push_back(entry); } } } @@ -112,8 +112,10 @@ namespace Sleak { if (context->IsDeferredEnabled()) { // Per-group accumulator: state commands preceding the current draw. - List> pendingState; - List> forwardCmds; + auto& pendingState = m_pendingState; + auto& forwardCmds = m_forwardCmds; + pendingState.clear(); + forwardCmds.clear(); ::Sleak::Material* lastMaterial = nullptr; bool lastMaterialForward = false; @@ -125,31 +127,33 @@ namespace Sleak { || t == CommandType::SetFace; }; - for (auto& cmd : commands) { + for (RenderCommandBase* cmd : commands) { auto type = cmd->GetType(); if (type == CommandType::BindMaterial) { - auto* bmc = static_cast(cmd.get()); + auto* bmc = static_cast(cmd); lastMaterial = bmc->GetMaterial(); lastMaterialForward = lastMaterial && lastMaterial->IsForwardRendered(); // Always add material bind to pending state - pendingState.add(cmd); + pendingState.push_back(cmd); continue; } if (type == CommandType::CustomCommand) { // Skybox, debug lines — always rendered after the lighting pass. // Flush any pending state to forward group first so state is correct. - for (auto& s : pendingState) forwardCmds.add(s); + forwardCmds.insert(forwardCmds.end(), + pendingState.begin(), + pendingState.end()); pendingState.clear(); - forwardCmds.add(cmd); + forwardCmds.push_back(cmd); lastMaterial = nullptr; lastMaterialForward = false; continue; } if (isStateCmd(type)) { - pendingState.add(cmd); + pendingState.push_back(cmd); continue; } @@ -160,8 +164,10 @@ namespace Sleak { const bool isSkinned = cmd->IsSkinned(); if (lastMaterialForward || isSkinned) { // Transparent/forward/skinned draw — defer to forward pass - for (auto& s : pendingState) forwardCmds.add(s); - forwardCmds.add(cmd); + forwardCmds.insert(forwardCmds.end(), + pendingState.begin(), + pendingState.end()); + forwardCmds.push_back(cmd); } else { // Opaque draw — execute now in geometry pass for (auto& s : pendingState) s->Execute(context); @@ -179,32 +185,30 @@ namespace Sleak { context->ExecuteDeferredLightingPass(); context->BeginForwardTransparentPass(); - for (auto& cmd : forwardCmds) - cmd->Execute(context); - context->EndForwardTransparentPass(); + for (RenderCommandBase* cmd : forwardCmds) + cmd->Execute(context); + context->EndForwardTransparentPass(); - commands.clear(); - return; + AdvanceArena(); + return; } // ---- Forward rendering path (unchanged behavior) ---- - for (auto& cmd : commands) - cmd->Execute(context); - commands.clear(); + for (RenderCommandBase* cmd : commands) cmd->Execute(context); + AdvanceArena(); } void RenderCommandQueue::ExecuteShadowPass(RenderContext* context) { - for (size_t i = 0; i < cachedShadowDraws.GetSize(); ++i) { - auto& entry = cachedShadowDraws[i]; + for (const ShadowDrawEntry& entry : cachedShadowDraws) { // Bind the transform buffer (slot 0) before drawing — shadow mode // in VulkanRenderer::BindConstantBuffer computes LightVP * World - if (entry.transformBuffer) { - context->BindConstantBuffer(entry.transformBuffer, 0); + if (entry.transformBuffer && *entry.transformBuffer) { + context->BindConstantBuffer(*entry.transformBuffer, 0); } // Skinned casters need their bone matrices bound so the shadow // depth vert can transform verts into the posed skeleton space. - if (entry.boneBuffer) { - context->BindBoneBuffer(entry.boneBuffer); + if (entry.boneBuffer && *entry.boneBuffer) { + context->BindBoneBuffer(*entry.boneBuffer); } entry.command->ExecuteShadow(context); } @@ -223,18 +227,50 @@ namespace Sleak { // TODO: Implement this method } - void RenderCommandQueue::Clear() { - while (!commands.isEmpty()) - commands.pop(); - } + void RenderCommandQueue::Clear() { CurrentArena().Reset(); } void RenderCommandQueue::ClearAll() { - while (!commands.isEmpty()) - commands.pop(); cachedShadowDraws.clear(); - for (int i = 0; i < RETIRE_FRAMES; ++i) - m_retiredShadowDraws[i].clear(); - m_retireIndex = 0; + m_pendingState.clear(); + m_forwardCmds.clear(); + for (CommandArena& arena : m_arenas) arena.Reset(); + m_currentArena = 0; + } + + void RenderCommandQueue::AdvanceArena() { + m_pendingState.clear(); + m_forwardCmds.clear(); + m_currentArena = (m_currentArena + 1) % ARENA_COUNT; + CurrentArena().Reset(); + } + + void* RenderCommandQueue::CommandArena::Allocate(size_t size, + size_t alignment) { + for (;;) { + if (m_blockIndex < m_blocks.size()) { + Block& block = m_blocks[m_blockIndex]; + const size_t start = + (m_offset + alignment - 1) & ~(alignment - 1); + if (start + size <= block.size) { + m_offset = start + size; + return block.data.get() + start; + } + ++m_blockIndex; + m_offset = 0; + continue; + } + const size_t blockSize = std::max(BLOCK_SIZE, size + alignment); + m_blocks.push_back( + {std::make_unique(blockSize), blockSize}); + } + } + + void RenderCommandQueue::CommandArena::Reset() { + for (RenderCommandBase* command : m_commands) + command->~RenderCommandBase(); + m_commands.clear(); + m_blockIndex = 0; + m_offset = 0; } void RenderCommandQueue::Shutdown() { diff --git a/src/Graphics/Common/RenderCommands.cpp b/src/Graphics/Common/RenderCommands.cpp index 5e97575b..e2fd49e1 100644 --- a/src/Graphics/Common/RenderCommands.cpp +++ b/src/Graphics/Common/RenderCommands.cpp @@ -13,8 +13,8 @@ namespace RenderEngine { DrawCommand::DrawCommand(RefPtr buffer, List> constantBuffers, uint32_t vertexCount, uint32_t vertexLocation) - : m_vertexBuffer(buffer), - m_constantBuffers(constantBuffers), + : m_vertexBuffer(std::move(buffer)), + m_constantBuffers(std::move(constantBuffers)), m_vertexCount(vertexCount), m_startVertexLocation(vertexLocation) {} @@ -51,9 +51,9 @@ DrawIndexedCommand::DrawIndexedCommand(RefPtr vertexBuffer, uint32_t indexCount, uint32_t startIndexLocation, int32_t baseVertexLocation) - : m_vertexBuffer(vertexBuffer), - m_indexBuffer(indexBuffer), - m_constantBuffers(constantBuffers), + : m_vertexBuffer(std::move(vertexBuffer)), + m_indexBuffer(std::move(indexBuffer)), + m_constantBuffers(std::move(constantBuffers)), m_indexCount(indexCount), m_startIndexLocation(startIndexLocation), m_baseVertexLocation(baseVertexLocation) {} @@ -110,11 +110,9 @@ bool DrawIndexedCommand::IsSkinned() const { } /// Copies the payload into inline storage, falling back to a heap allocation past INLINE_CAPACITY. -UpdateConstantBufferCommand::UpdateConstantBufferCommand(RefPtr buffer, - void* Data, - uint16_t Size) : - constantBuffer(buffer), - Size(Size) { +UpdateConstantBufferCommand::UpdateConstantBufferCommand( + RefPtr buffer, void* Data, uint16_t Size) + : constantBuffer(std::move(buffer)), Size(Size) { if (Size <= INLINE_CAPACITY) { // Fast path: no heap allocation for small buffers memcpy(m_inlineData, Data, Size); diff --git a/src/Graphics/DirectX11/DirectX11Renderer.cpp b/src/Graphics/DirectX11/DirectX11Renderer.cpp index 3d96421b..06a1cd13 100644 --- a/src/Graphics/DirectX11/DirectX11Renderer.cpp +++ b/src/Graphics/DirectX11/DirectX11Renderer.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include "Graphics/Common/RenderCommandQueue.hpp" @@ -33,6 +34,29 @@ namespace Sleak { constexpr const char* TONEMAP_STEM = "tonemap"; #endif + // Transform CB layout: [WVP][World]; shadow CB: [LightVP*World][World] + constexpr int MATRIX_DIM = 4; + constexpr int MATRIX_FLOATS = MATRIX_DIM * MATRIX_DIM; + constexpr int TRANSFORM_CB_FLOATS = 2 * MATRIX_FLOATS; + constexpr UINT TRANSFORM_CB_BYTES = TRANSFORM_CB_FLOATS * sizeof(float); + constexpr size_t TRANSFORM_WORLD_OFFSET = MATRIX_FLOATS * sizeof(float); + + /// Writes World * LightVP (row-major) followed by World into out. + void BuildShadowTransform(const float* world, const float* lightVP, + float* out) { + for (int r = 0; r < MATRIX_DIM; ++r) { + for (int c = 0; c < MATRIX_DIM; ++c) { + float sum = 0.0f; + for (int k = 0; k < MATRIX_DIM; ++k) { + sum += + world[r * MATRIX_DIM + k] * lightVP[k * MATRIX_DIM + c]; + } + out[r * MATRIX_DIM + c] = sum; + } + } + std::memcpy(out + MATRIX_FLOATS, world, MATRIX_FLOATS * sizeof(float)); + } + /// Logs a failed D3DCompileFromFile call with the compiler output. void LogCompileError(const char* path, const char* entry, HRESULT hr, ID3DBlob* errors) { @@ -45,6 +69,56 @@ namespace Sleak { } } + /// Compares two rasterizer descriptions field by field. + bool SameDesc(const D3D11_RASTERIZER_DESC& a, + const D3D11_RASTERIZER_DESC& b) { + return a.FillMode == b.FillMode && a.CullMode == b.CullMode && + a.FrontCounterClockwise == b.FrontCounterClockwise && + a.DepthBias == b.DepthBias && + a.DepthBiasClamp == b.DepthBiasClamp && + a.SlopeScaledDepthBias == b.SlopeScaledDepthBias && + a.DepthClipEnable == b.DepthClipEnable && + a.ScissorEnable == b.ScissorEnable && + a.MultisampleEnable == b.MultisampleEnable && + a.AntialiasedLineEnable == b.AntialiasedLineEnable; + } + + /// Compares two stencil face descriptions field by field. + bool SameDesc(const D3D11_DEPTH_STENCILOP_DESC& a, + const D3D11_DEPTH_STENCILOP_DESC& b) { + return a.StencilFailOp == b.StencilFailOp && + a.StencilDepthFailOp == b.StencilDepthFailOp && + a.StencilPassOp == b.StencilPassOp && + a.StencilFunc == b.StencilFunc; + } + + /// Compares two depth-stencil descriptions field by field. + bool SameDesc(const D3D11_DEPTH_STENCIL_DESC& a, + const D3D11_DEPTH_STENCIL_DESC& b) { + return a.DepthEnable == b.DepthEnable && + a.DepthWriteMask == b.DepthWriteMask && + a.DepthFunc == b.DepthFunc && + a.StencilEnable == b.StencilEnable && + a.StencilReadMask == b.StencilReadMask && + a.StencilWriteMask == b.StencilWriteMask && + SameDesc(a.FrontFace, b.FrontFace) && + SameDesc(a.BackFace, b.BackFace); + } + + /// Finds the state cached for desc, or creates and caches it on a miss. + template + State* FindOrCreateState( + std::vector>>& cache, + const Desc& desc, Create create) { + for (const auto& [key, state] : cache) { + if (SameDesc(key, desc)) return state.Get(); + } + Microsoft::WRL::ComPtr state; + if (FAILED(create(&desc, state.GetAddressOf()))) return nullptr; + cache.emplace_back(desc, state); + return state.Get(); + } + } // namespace /// Registers this backend's buffer/shader/texture factories with ResourceManager. @@ -179,9 +253,9 @@ bool DirectX11Renderer::Initialize() { 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); + if (bEnabledPerformanceCounter && query) { + while (deviceContext->GetData(query, nullptr, 0, 0) == S_FALSE) + std::this_thread::yield(); deviceContext->Begin(query); } @@ -196,8 +270,10 @@ void DirectX11Renderer::BeginRender() { ApplyMSAAChange(); // Commit staged lightVP before shadow pass so shadow + main agree. - if (m_hasPendingLightVP) { + if (m_hasPendingLightVP && + std::memcmp(m_lightVP, m_pendingLightVP, sizeof(m_lightVP)) != 0) { std::memcpy(m_lightVP, m_pendingLightVP, sizeof(m_lightVP)); + m_shadowCBCacheValid = false; } // Create tonemap resources on demand @@ -312,6 +388,9 @@ void DirectX11Renderer::Cleanup() { blendState = nullptr; } + m_rasterStates.clear(); + m_depthStates.clear(); + if(query) { query->Release(); @@ -395,12 +474,15 @@ void DirectX11Renderer::SetDepthStencilState(bool enableDepth, depthStencilDesc.BackFace.StencilPassOp = D3D11_STENCIL_OP_KEEP; depthStencilDesc.BackFace.StencilFunc = D3D11_COMPARISON_ALWAYS; - HRESULT hr = device->CreateDepthStencilState(&depthStencilDesc, &depthStencilState); + ID3D11DepthStencilState* state = nullptr; + HRESULT hr = device->CreateDepthStencilState(&depthStencilDesc, &state); if (FAILED(hr)) { SLEAK_ERROR("Failed to create depth stencil state!"); return; } + if (depthStencilState) depthStencilState->Release(); + depthStencilState = state; deviceContext->OMSetDepthStencilState(depthStencilState, 1); } @@ -535,95 +617,75 @@ void DirectX11Renderer::ClearDepthStencil(bool clearDepth, bool clearStencil, // Buffer binding void DirectX11Renderer::BindVertexBuffer(RefPtr buffer, uint32_t slot) { - if (!buffer) - return; + auto* dx11Buf = static_cast(buffer.get()); + if (!dx11Buf) return; + ID3D11Buffer* d3d11Buffer = dx11Buf->GetD3DBuffer(); UINT stride = sizeof(Sleak::Vertex); UINT offset = 0; - - try - { - auto d3d11Buffer = dynamic_cast - (buffer.get()) - ->GetD3DBuffer(); - - deviceContext->IASetVertexBuffers(slot, 1, &d3d11Buffer, &stride, &offset); - - } - catch (std::exception& e) - { - SLEAK_ERROR("Failed to cast Vertex Buffer! {}", e.what()); - } + deviceContext->IASetVertexBuffers(slot, 1, &d3d11Buffer, &stride, &offset); } void DirectX11Renderer::BindIndexBuffer(RefPtr buffer, uint32_t slot) { - if (!buffer) return; - - UINT offset = 0; - - try { - auto d3d11Buffer = - dynamic_cast(buffer.get())->GetD3DBuffer(); + auto* dx11Buf = static_cast(buffer.get()); + if (!dx11Buf) return; - deviceContext->IASetIndexBuffer(d3d11Buffer, DXGI_FORMAT_R32_UINT, 0); - - } catch (std::exception& e) { - SLEAK_ERROR("Failed to cast Index Buffer! {}", e.what()); - } + deviceContext->IASetIndexBuffer(dx11Buf->GetD3DBuffer(), + DXGI_FORMAT_R32_UINT, 0); } void DirectX11Renderer::BindConstantBuffer(RefPtr buffer, uint32_t slot) { - try { - auto* dx11Buf = dynamic_cast(buffer.get()); - if (!dx11Buf) return; - - // Shadow pass: use dedicated shadow CB with LightVP*World for slot 0 - if (m_inShadowPass && slot == 0 && dx11Buf->GetCPUShadowCopySize() >= 128) { - const float* srcWorld = reinterpret_cast( - static_cast(dx11Buf->GetCPUShadowCopy()) + 64); - - float shadowPC[32]; - for (int r = 0; r < 4; ++r) { - for (int c = 0; c < 4; ++c) { - float sum = 0.0f; - for (int k = 0; k < 4; ++k) { - sum += srcWorld[r * 4 + k] * m_lightVP[k * 4 + c]; - } - shadowPC[r * 4 + c] = sum; - } - } - std::memcpy(&shadowPC[16], srcWorld, sizeof(float) * 16); - - // Create dedicated shadow transform CB on first use - if (!m_shadowTransformCB) { - D3D11_BUFFER_DESC desc{}; - desc.ByteWidth = 128; - desc.Usage = D3D11_USAGE_DYNAMIC; - desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; - desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; - device->CreateBuffer(&desc, nullptr, &m_shadowTransformCB); - } + auto* dx11Buf = static_cast(buffer.get()); + if (!dx11Buf) return; + + // Shadow pass: use dedicated shadow CB with LightVP*World for slot 0 + if (m_inShadowPass && slot == 0 && + dx11Buf->GetCPUShadowCopySize() >= TRANSFORM_CB_BYTES) { + const float* srcWorld = reinterpret_cast( + static_cast(dx11Buf->GetCPUShadowCopy()) + + TRANSFORM_WORLD_OFFSET); + + // Create dedicated shadow transform CB on first use + if (!m_shadowTransformCB) { + D3D11_BUFFER_DESC desc{}; + desc.ByteWidth = TRANSFORM_CB_BYTES; + desc.Usage = D3D11_USAGE_DYNAMIC; + desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + if (FAILED( + device->CreateBuffer(&desc, nullptr, &m_shadowTransformCB))) + return; + m_shadowCBCacheValid = false; + } + + // The CB keeps its contents, so only a new World needs an upload + if (!m_shadowCBCacheValid || + std::memcmp(srcWorld, m_shadowWorldCache, + sizeof(m_shadowWorldCache)) != 0) { + float shadowPC[TRANSFORM_CB_FLOATS]; + BuildShadowTransform(srcWorld, m_lightVP, shadowPC); - // Update and bind the shadow CB instead of the original D3D11_MAPPED_SUBRESOURCE mapped{}; - if (SUCCEEDED(deviceContext->Map(m_shadowTransformCB, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped))) { + if (SUCCEEDED(deviceContext->Map(m_shadowTransformCB, 0, + D3D11_MAP_WRITE_DISCARD, 0, + &mapped))) { std::memcpy(mapped.pData, shadowPC, sizeof(shadowPC)); deviceContext->Unmap(m_shadowTransformCB, 0); + std::memcpy(m_shadowWorldCache, srcWorld, + sizeof(m_shadowWorldCache)); + m_shadowCBCacheValid = true; } - deviceContext->VSSetConstantBuffers(slot, 1, &m_shadowTransformCB); - deviceContext->PSSetConstantBuffers(slot, 1, &m_shadowTransformCB); - return; } - - auto d3d11Buffer = dx11Buf->GetD3DBuffer(); - deviceContext->VSSetConstantBuffers(slot, 1, &d3d11Buffer); - deviceContext->PSSetConstantBuffers(slot, 1, &d3d11Buffer); - - } catch (std::exception& e) { - SLEAK_ERROR("Failed to cast Constant Buffer! {}", e.what()); + deviceContext->VSSetConstantBuffers(slot, 1, &m_shadowTransformCB); + deviceContext->PSSetConstantBuffers(slot, 1, &m_shadowTransformCB); + return; } + + auto d3d11Buffer = dx11Buf->GetD3DBuffer(); + deviceContext->VSSetConstantBuffers(slot, 1, &d3d11Buffer); + deviceContext->PSSetConstantBuffers(slot, 1, &d3d11Buffer); } void DirectX11Renderer::ConfigureRenderMode() { @@ -662,7 +724,6 @@ void DirectX11Renderer::ConfigureRenderFace() { } bool DirectX11Renderer::SetRasterState() { - ID3D11RasterizerState* rasterState; D3D11_RASTERIZER_DESC RasterDesc = {}; RasterDesc.AntialiasedLineEnable = true; RasterDesc.MultisampleEnable = true; @@ -675,16 +736,31 @@ bool DirectX11Renderer::SetRasterState() { RasterDesc.FillMode = fillMode; RasterDesc.FrontCounterClockwise = false; - if (FAILED(device->CreateRasterizerState(&RasterDesc, &rasterState))) { - return false; - } else { - deviceContext->RSSetState(rasterState); - rasterState->Release(); - } + ID3D11RasterizerState* rasterState = GetRasterState(RasterDesc); + if (!rasterState) return false; + deviceContext->RSSetState(rasterState); return true; } +ID3D11RasterizerState* DirectX11Renderer::GetRasterState( + const D3D11_RASTERIZER_DESC& desc) { + return FindOrCreateState( + m_rasterStates, desc, + [this](const D3D11_RASTERIZER_DESC* d, ID3D11RasterizerState** out) { + return device->CreateRasterizerState(d, out); + }); +} + +ID3D11DepthStencilState* DirectX11Renderer::GetDepthState( + const D3D11_DEPTH_STENCIL_DESC& desc) { + return FindOrCreateState(m_depthStates, desc, + [this](const D3D11_DEPTH_STENCIL_DESC* d, + ID3D11DepthStencilState** out) { + return device->CreateDepthStencilState(d, out); + }); +} + void DirectX11Renderer::SetRenderMode(RenderMode mode) { this->Mode = mode; ConfigureRenderMode(); @@ -778,19 +854,19 @@ Texture* DirectX11Renderer::CreateCubemapTextureFromPanorama( void DirectX11Renderer::SetDepthWrite(bool enabled) { m_depthWriteEnabled = enabled; + ApplyDepthState(); +} +void DirectX11Renderer::ApplyDepthState() { D3D11_DEPTH_STENCIL_DESC desc = {}; desc.DepthEnable = TRUE; - desc.DepthWriteMask = enabled ? D3D11_DEPTH_WRITE_MASK_ALL - : D3D11_DEPTH_WRITE_MASK_ZERO; + desc.DepthWriteMask = m_depthWriteEnabled ? D3D11_DEPTH_WRITE_MASK_ALL + : D3D11_DEPTH_WRITE_MASK_ZERO; desc.DepthFunc = m_depthFunc; desc.StencilEnable = FALSE; - ID3D11DepthStencilState* state = nullptr; - if (SUCCEEDED(device->CreateDepthStencilState(&desc, &state))) { + if (ID3D11DepthStencilState* state = GetDepthState(desc)) deviceContext->OMSetDepthStencilState(state, 1); - state->Release(); - } } void DirectX11Renderer::SetDepthCompare(DepthCompare compare) { @@ -805,18 +881,7 @@ void DirectX11Renderer::SetDepthCompare(DepthCompare compare) { case DepthCompare::Never: m_depthFunc = D3D11_COMPARISON_NEVER; break; } - D3D11_DEPTH_STENCIL_DESC desc = {}; - desc.DepthEnable = TRUE; - desc.DepthWriteMask = m_depthWriteEnabled ? D3D11_DEPTH_WRITE_MASK_ALL - : D3D11_DEPTH_WRITE_MASK_ZERO; - desc.DepthFunc = m_depthFunc; - desc.StencilEnable = FALSE; - - ID3D11DepthStencilState* state = nullptr; - if (SUCCEEDED(device->CreateDepthStencilState(&desc, &state))) { - deviceContext->OMSetDepthStencilState(state, 1); - state->Release(); - } + ApplyDepthState(); } void DirectX11Renderer::SetCullEnabled(bool enabled) { @@ -1158,6 +1223,7 @@ void DirectX11Renderer::CleanupShadowResources() { if (m_shadowCB) { m_shadowCB->Release(); m_shadowCB = nullptr; } m_shadowCBCreated = false; if (m_shadowTransformCB) { m_shadowTransformCB->Release(); m_shadowTransformCB = nullptr; } + m_shadowCBCacheValid = false; if (m_shadowRasterState) { m_shadowRasterState->Release(); m_shadowRasterState = nullptr; } if (m_shadowSampler) { m_shadowSampler->Release(); m_shadowSampler = nullptr; } if (m_shadowSRV) { m_shadowSRV->Release(); m_shadowSRV = nullptr; } diff --git a/src/Graphics/DirectX12/DirectX12Renderer.cpp b/src/Graphics/DirectX12/DirectX12Renderer.cpp index 7c18ed92..b5655bba 100644 --- a/src/Graphics/DirectX12/DirectX12Renderer.cpp +++ b/src/Graphics/DirectX12/DirectX12Renderer.cpp @@ -70,6 +70,28 @@ constexpr const char* SKYBOX_STEM = "skybox"; constexpr const char* DEBUG_LINE_STEM = "debug_line"; #endif +// Transform CB layout: [WVP][World]; shadow CB: [LightVP*World][World] +constexpr int MATRIX_DIM = 4; +constexpr int MATRIX_FLOATS = MATRIX_DIM * MATRIX_DIM; +constexpr int TRANSFORM_CB_FLOATS = 2 * MATRIX_FLOATS; +constexpr size_t TRANSFORM_CB_BYTES = TRANSFORM_CB_FLOATS * sizeof(float); +constexpr size_t TRANSFORM_WORLD_OFFSET = MATRIX_FLOATS * sizeof(float); + +/// Writes World * LightVP (row-major) followed by World into out. +void BuildShadowTransform(const float* world, const float* lightVP, + float* out) { + for (int r = 0; r < MATRIX_DIM; ++r) { + for (int c = 0; c < MATRIX_DIM; ++c) { + float sum = 0.0f; + for (int k = 0; k < MATRIX_DIM; ++k) { + sum += world[r * MATRIX_DIM + k] * lightVP[k * MATRIX_DIM + c]; + } + out[r * MATRIX_DIM + c] = sum; + } + } + memcpy(out + MATRIX_FLOATS, world, MATRIX_FLOATS * sizeof(float)); +} + /// Compiles one fixed-pass stage, logging the compiler output on failure. Microsoft::WRL::ComPtr CompileStage(const char* path, const char* entry, @@ -1215,26 +1237,23 @@ void DirectX12Renderer::BindConstantBuffer(RefPtr buffer, if (!dx12Buf) return; // Shadow pass: use a dedicated shadow CB with LightVP*World for slot 0 - if (m_inShadowPass && slot == 0 && dx12Buf->GetCPUShadowCopySize() >= 128) { + if (m_inShadowPass && slot == 0 && + dx12Buf->GetCPUShadowCopySize() >= TRANSFORM_CB_BYTES) { const float* srcWorld = reinterpret_cast( - static_cast(dx12Buf->GetCPUShadowCopy()) + 64); - - float shadowPC[32]; - for (int r = 0; r < 4; ++r) { - for (int c = 0; c < 4; ++c) { - float sum = 0.0f; - for (int k = 0; k < 4; ++k) { - sum += srcWorld[r * 4 + k] * m_lightVP[k * 4 + c]; - } - shadowPC[r * 4 + c] = sum; - } - } - memcpy(&shadowPC[16], srcWorld, sizeof(float) * 16); + static_cast(dx12Buf->GetCPUShadowCopy()) + + TRANSFORM_WORLD_OFFSET); - DirectX12UploadRing::Allocation alloc; - if (!m_uploadRing.Allocate(sizeof(shadowPC), alloc)) return; - memcpy(alloc.cpu, shadowPC, sizeof(shadowPC)); - commandList->SetGraphicsRootConstantBufferView(slot, alloc.gpu); + // Casters sharing a World reuse this pass's ring copy + if (!m_shadowCBAddress || memcmp(srcWorld, m_shadowWorldCache, + sizeof(m_shadowWorldCache)) != 0) { + DirectX12UploadRing::Allocation alloc; + if (!m_uploadRing.Allocate(TRANSFORM_CB_BYTES, alloc)) return; + BuildShadowTransform(srcWorld, m_lightVP, + static_cast(alloc.cpu)); + memcpy(m_shadowWorldCache, srcWorld, sizeof(m_shadowWorldCache)); + m_shadowCBAddress = alloc.gpu; + } + commandList->SetGraphicsRootConstantBufferView(slot, m_shadowCBAddress); return; } @@ -1526,6 +1545,9 @@ void DirectX12Renderer::RenderShadowPass() { } BindLightConstants(); + // Ring memory is per frame and lightVP may have changed since last pass + m_shadowCBAddress = 0; + // Execute shadow draw commands m_inShadowPass = true; if (queue) { diff --git a/src/Graphics/OpenGL/OpenGLRenderer.cpp b/src/Graphics/OpenGL/OpenGLRenderer.cpp index 225ca347..ac4cb249 100644 --- a/src/Graphics/OpenGL/OpenGLRenderer.cpp +++ b/src/Graphics/OpenGL/OpenGLRenderer.cpp @@ -28,6 +28,42 @@ namespace Sleak { namespace RenderEngine { +namespace { + +constexpr GLuint VERTEX_BUFFER_BINDING = 0; +constexpr uint32_t DEFAULT_VERTEX_LAYOUT = 0; +constexpr uint32_t ATTRIB_MASK_BITS = 32; + +// Default Vertex attribute locations +constexpr GLuint ATTRIB_POSITION = 0; +constexpr GLuint ATTRIB_NORMAL = 1; +constexpr GLuint ATTRIB_TANGENT = 2; +constexpr GLuint ATTRIB_COLOR = 3; +constexpr GLuint ATTRIB_UV = 4; +constexpr GLuint ATTRIB_BONE_IDS = 5; +constexpr GLuint ATTRIB_BONE_WEIGHTS = 6; + +// Transform CB layout read by the shadow pass: [WVP][World] +constexpr int MAT4_DIM = 4; +constexpr size_t MAT4_FLOATS = MAT4_DIM * MAT4_DIM; +constexpr size_t MAT4_BYTES = MAT4_FLOATS * sizeof(float); +constexpr size_t TRANSFORM_CB_BYTES = 2 * MAT4_BYTES; + +/// Sets one attribute's format on vao and routes it to the shared binding. +void SetAttribFormat(GLuint vao, GLuint location, GLint components, GLenum type, + bool integer, size_t offset) { + const GLuint relativeOffset = static_cast(offset); + if (integer) + glVertexArrayAttribIFormat(vao, location, components, type, + relativeOffset); + else + glVertexArrayAttribFormat(vao, location, components, type, GL_FALSE, + relativeOffset); + glVertexArrayAttribBinding(vao, location, VERTEX_BUFFER_BINDING); +} + +} // namespace + /// Registers this backend's buffer/shader/texture factories with ResourceManager. OpenGLRenderer::OpenGLRenderer(Window* window) : m_Window(window) { @@ -81,7 +117,7 @@ bool OpenGLRenderer::Initialize() { int width, height; SDL_GetWindowSize(m_Window->GetSDLWindow(), &width, &height); - glViewport(0, 0, width, height); + ApplyViewport(0, 0, width, height); glEnable(GL_DEPTH_TEST); glEnable(GL_BLEND); @@ -117,7 +153,83 @@ bool OpenGLRenderer::Initialize() { } void OpenGLRenderer::SetupVertexLayout() { - glGenVertexArrays(1, &m_VAO); + glCreateVertexArrays(1, &m_VAO); + m_configuredLayout = NO_CONFIGURED_LAYOUT; + m_configuredLayoutDesc = nullptr; + m_enabledAttribMask = 0; +} + +void OpenGLRenderer::ConfigureVertexLayout(uint32_t layout, + const VertexLayoutDesc* desc) { + if (layout == m_configuredLayout) return; + m_configuredLayout = layout; + m_configuredLayoutDesc = desc; + + uint32_t enabledMask = 0; + if (desc) { + for (const auto& attr : desc->attributes) { + switch (attr.format) { + case VertexAttribFormat::Float1: + SetAttribFormat(m_VAO, attr.location, 1, GL_FLOAT, false, + attr.offset); + break; + case VertexAttribFormat::Float2: + SetAttribFormat(m_VAO, attr.location, 2, GL_FLOAT, false, + attr.offset); + break; + case VertexAttribFormat::Float3: + SetAttribFormat(m_VAO, attr.location, 3, GL_FLOAT, false, + attr.offset); + break; + case VertexAttribFormat::Float4: + SetAttribFormat(m_VAO, attr.location, 4, GL_FLOAT, false, + attr.offset); + break; + case VertexAttribFormat::UInt1: + SetAttribFormat(m_VAO, attr.location, 1, GL_UNSIGNED_INT, + true, attr.offset); + break; + case VertexAttribFormat::Int1: + SetAttribFormat(m_VAO, attr.location, 1, GL_INT, true, + attr.offset); + break; + } + if (attr.location < ATTRIB_MASK_BITS) + enabledMask |= (1u << attr.location); + else + glEnableVertexArrayAttrib(m_VAO, attr.location); + } + } else { + // Standard 96-byte Vertex layout: 7 attributes + SetAttribFormat(m_VAO, ATTRIB_POSITION, 3, GL_FLOAT, false, + offsetof(Vertex, px)); + SetAttribFormat(m_VAO, ATTRIB_NORMAL, 3, GL_FLOAT, false, + offsetof(Vertex, nx)); + SetAttribFormat(m_VAO, ATTRIB_TANGENT, 4, GL_FLOAT, false, + offsetof(Vertex, tx)); + SetAttribFormat(m_VAO, ATTRIB_COLOR, 4, GL_FLOAT, false, + offsetof(Vertex, r)); + SetAttribFormat(m_VAO, ATTRIB_UV, 2, GL_FLOAT, false, + offsetof(Vertex, u)); + SetAttribFormat(m_VAO, ATTRIB_BONE_IDS, 4, GL_INT, true, + offsetof(Vertex, boneIDs)); + SetAttribFormat(m_VAO, ATTRIB_BONE_WEIGHTS, 4, GL_FLOAT, false, + offsetof(Vertex, boneWeights)); + for (GLuint loc = ATTRIB_POSITION; loc <= ATTRIB_BONE_WEIGHTS; ++loc) + enabledMask |= (1u << loc); + } + + // Toggle only the slots whose enable state differs from the last layout + const uint32_t changed = enabledMask ^ m_enabledAttribMask; + for (uint32_t loc = 0; loc < ATTRIB_MASK_BITS; ++loc) { + const uint32_t bit = 1u << loc; + if (!(changed & bit)) continue; + if (enabledMask & bit) + glEnableVertexArrayAttrib(m_VAO, loc); + else + glDisableVertexArrayAttrib(m_VAO, loc); + } + m_enabledAttribMask = enabledMask; } void OpenGLRenderer::BeginRender() { @@ -252,7 +364,8 @@ bool OpenGLRenderer::ReloadShaders(const std::vector& stems) { #endif void OpenGLRenderer::Resize(uint32_t width, uint32_t height) { - glViewport(0, 0, width, height); + ApplyViewport(0, 0, static_cast(width), + static_cast(height)); // Recreate MSAA FBO at new size if (m_msaaFBO != 0) { CleanupMSAAFramebuffer(); @@ -336,12 +449,18 @@ void OpenGLRenderer::SetRenderMode(RenderMode mode) { void OpenGLRenderer::SetViewport(float x, float y, float width, float height, float minDepth, float maxDepth) { - glViewport(static_cast(x), static_cast(y), - static_cast(width), - static_cast(height)); + ApplyViewport(static_cast(x), static_cast(y), + static_cast(width), static_cast(height)); glDepthRangef(minDepth, maxDepth); } +void OpenGLRenderer::ApplyViewport(GLint x, GLint y, GLsizei width, + GLsizei height) { + glViewport(x, y, width, height); + // GL rejects negative sizes and keeps the previous viewport + if (width >= 0 && height >= 0) m_viewport = {x, y, width, height}; +} + void OpenGLRenderer::ClearRenderTarget(float r, float g, float b, float a) { glClearColor(r, g, b, a); @@ -398,89 +517,21 @@ void OpenGLRenderer::BindVertexBuffer(RefPtr buffer, auto* glBuf = dynamic_cast(buffer.get()); if (!glBuf) return; - glBindBuffer(GL_ARRAY_BUFFER, glBuf->GetGLBuffer()); - - // A registered layout drives the attribute pointers; 0 means default Vertex - const uint32_t customFormat = buffer->GetVertexFormat(); - const VertexLayoutDesc* customDesc = - customFormat != 0 ? VertexFormatRegistry::Get(customFormat) : nullptr; + // A registered layout drives the attribute formats; 0 means default Vertex + const uint32_t format = buffer->GetVertexFormat(); + const VertexLayoutDesc* desc = m_configuredLayoutDesc; + if (format != m_configuredLayout) { + desc = format != 0 ? VertexFormatRegistry::Get(format) : nullptr; + ConfigureVertexLayout(desc ? format : DEFAULT_VERTEX_LAYOUT, desc); + } - if (customDesc && customDesc->transparentReadsScene && - m_inForwardTransparentPass) + if (desc && desc->transparentReadsScene && m_inForwardTransparentPass) PrepareSceneRead(); - if (customDesc) { - // One attribute pointer per registry entry, tracking which slots stay live - const GLsizei stride = static_cast(customDesc->stride); - uint32_t enabledMask = 0; - for (const auto& attr : customDesc->attributes) { - const void* off = reinterpret_cast( - static_cast(attr.offset)); - glEnableVertexAttribArray(attr.location); - switch (attr.format) { - case VertexAttribFormat::Float1: - glVertexAttribPointer(attr.location, 1, GL_FLOAT, GL_FALSE, stride, off); - break; - case VertexAttribFormat::Float2: - glVertexAttribPointer(attr.location, 2, GL_FLOAT, GL_FALSE, stride, off); - break; - case VertexAttribFormat::Float3: - glVertexAttribPointer(attr.location, 3, GL_FLOAT, GL_FALSE, stride, off); - break; - case VertexAttribFormat::Float4: - glVertexAttribPointer(attr.location, 4, GL_FLOAT, GL_FALSE, stride, off); - break; - case VertexAttribFormat::UInt1: - glVertexAttribIPointer(attr.location, 1, GL_UNSIGNED_INT, stride, off); - break; - case VertexAttribFormat::Int1: - glVertexAttribIPointer(attr.location, 1, GL_INT, stride, off); - break; - } - if (attr.location < 32) enabledMask |= (1u << attr.location); - } - // Disable attribute slots left enabled by a previous bind - constexpr uint32_t TRACKED_ATTRIB_SLOTS = 8; - for (uint32_t loc = 0; loc < TRACKED_ATTRIB_SLOTS; ++loc) { - if (!(enabledMask & (1u << loc))) glDisableVertexAttribArray(loc); - } - } else { - // Standard 96-byte Vertex layout: 7 attributes - // Position: 3 floats at offset 0 - glEnableVertexAttribArray(0); - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), - (void*)offsetof(Vertex, px)); - - // Normal: 3 floats at offset 12 - glEnableVertexAttribArray(1); - glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), - (void*)offsetof(Vertex, nx)); - - // Tangent: 4 floats at offset 24 - glEnableVertexAttribArray(2); - glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), - (void*)offsetof(Vertex, tx)); - - // Color: 4 floats at offset 40 - glEnableVertexAttribArray(3); - glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), - (void*)offsetof(Vertex, r)); - - // UV: 2 floats at offset 56 - glEnableVertexAttribArray(4); - glVertexAttribPointer(4, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), - (void*)offsetof(Vertex, u)); - - // BoneIDs: 4 ints (must use IPointer for integer attributes) - glEnableVertexAttribArray(5); - glVertexAttribIPointer(5, 4, GL_INT, sizeof(Vertex), - (void*)offsetof(Vertex, boneIDs)); - - // BoneWeights: 4 floats - glEnableVertexAttribArray(6); - glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), - (void*)offsetof(Vertex, boneWeights)); - } + const GLsizei stride = desc ? static_cast(desc->stride) + : static_cast(sizeof(Vertex)); + glVertexArrayVertexBuffer(m_VAO, VERTEX_BUFFER_BINDING, + glBuf->GetGLBuffer(), 0, stride); } void OpenGLRenderer::BindIndexBuffer(RefPtr buffer, @@ -497,33 +548,42 @@ void OpenGLRenderer::BindConstantBuffer(RefPtr buffer, if (!glBuf) return; // Shadow pass: use dedicated shadow CB with LightVP*World for slot 0 - if (m_inShadowPass && slot == 0 && glBuf->GetCPUShadowCopySize() >= 128) { + if (m_inShadowPass && slot == 0 && + glBuf->GetCPUShadowCopySize() >= TRANSFORM_CB_BYTES) { const float* srcWorld = reinterpret_cast( - static_cast(glBuf->GetCPUShadowCopy()) + 64); - - float shadowPC[32]; - for (int r = 0; r < 4; ++r) { - for (int c = 0; c < 4; ++c) { - float sum = 0.0f; - for (int k = 0; k < 4; ++k) { - sum += srcWorld[r * 4 + k] * m_lightVP[k * 4 + c]; - } - shadowPC[r * 4 + c] = sum; - } - } - memcpy(&shadowPC[16], srcWorld, sizeof(float) * 16); + static_cast(glBuf->GetCPUShadowCopy()) + MAT4_BYTES); // Create dedicated shadow transform UBO on first use if (m_shadowTransformUBO == 0) { - glGenBuffers(1, &m_shadowTransformUBO); - glBindBuffer(GL_UNIFORM_BUFFER, m_shadowTransformUBO); - glBufferData(GL_UNIFORM_BUFFER, 128, nullptr, GL_DYNAMIC_DRAW); - glBindBuffer(GL_UNIFORM_BUFFER, 0); + glCreateBuffers(1, &m_shadowTransformUBO); + glNamedBufferData(m_shadowTransformUBO, TRANSFORM_CB_BYTES, nullptr, + GL_DYNAMIC_DRAW); + m_shadowTransformCacheValid = false; + } + + // LightVP is fixed for the pass, so only a new World needs an upload + static_assert(sizeof(m_shadowTransformCache) == TRANSFORM_CB_BYTES); + static_assert(sizeof(m_shadowWorldCache) == MAT4_BYTES); + if (!m_shadowTransformCacheValid || + memcmp(srcWorld, m_shadowWorldCache, MAT4_BYTES) != 0) { + for (int r = 0; r < MAT4_DIM; ++r) { + for (int c = 0; c < MAT4_DIM; ++c) { + float sum = 0.0f; + for (int k = 0; k < MAT4_DIM; ++k) { + sum += srcWorld[r * MAT4_DIM + k] * + m_lightVP[k * MAT4_DIM + c]; + } + m_shadowTransformCache[r * MAT4_DIM + c] = sum; + } + } + memcpy(&m_shadowTransformCache[MAT4_FLOATS], srcWorld, MAT4_BYTES); + memcpy(m_shadowWorldCache, srcWorld, MAT4_BYTES); + m_shadowTransformCacheValid = true; + glNamedBufferSubData(m_shadowTransformUBO, 0, + sizeof(m_shadowTransformCache), + m_shadowTransformCache); } - glBindBuffer(GL_UNIFORM_BUFFER, m_shadowTransformUBO); - glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(shadowPC), shadowPC); - glBindBuffer(GL_UNIFORM_BUFFER, 0); glBindBufferBase(GL_UNIFORM_BUFFER, slot, m_shadowTransformUBO); return; } @@ -795,9 +855,7 @@ void OpenGLRenderer::RenderShadowPass() { if (!CreateShadowMapResources()) return; } - // Save current viewport - GLint savedViewport[4]; - glGetIntegerv(GL_VIEWPORT, savedViewport); + const std::array savedViewport = m_viewport; // Bind shadow FBO glBindFramebuffer(GL_FRAMEBUFFER, m_shadowFBO); @@ -819,6 +877,7 @@ void OpenGLRenderer::RenderShadowPass() { // Execute shadow draw commands m_inShadowPass = true; + m_shadowTransformCacheValid = false; if (queue) { queue->ExecuteShadowPass(this); } @@ -837,7 +896,8 @@ void OpenGLRenderer::RenderShadowPass() { else glBindFramebuffer(GL_FRAMEBUFFER, 0); - glViewport(savedViewport[0], savedViewport[1], savedViewport[2], savedViewport[3]); + ApplyViewport(savedViewport[0], savedViewport[1], savedViewport[2], + savedViewport[3]); // Bind shadow map texture at unit 3 glActiveTexture(GL_TEXTURE3); @@ -1403,7 +1463,7 @@ void OpenGLRenderer::ExecuteSSAOPass() { // ---- Pass 1: SSAO ---- glBindFramebuffer(GL_FRAMEBUFFER, m_ssaoFBO); - glViewport(0, 0, m_gbufferWidth, m_gbufferHeight); + ApplyViewport(0, 0, m_gbufferWidth, m_gbufferHeight); glDisable(GL_DEPTH_TEST); glDepthMask(GL_FALSE); glDisable(GL_BLEND); diff --git a/src/Graphics/Vulkan/VulkanDescriptors.cpp b/src/Graphics/Vulkan/VulkanDescriptors.cpp index 67199c0d..e8060e45 100644 --- a/src/Graphics/Vulkan/VulkanDescriptors.cpp +++ b/src/Graphics/Vulkan/VulkanDescriptors.cpp @@ -1,10 +1,11 @@ -#include "../../include/private/Graphics/Vulkan/VulkanRenderer.hpp" -#include "../../include/private/Graphics/Vulkan/VulkanTexture.hpp" - #include +#include #include #include #include + +#include "../../include/private/Graphics/Vulkan/VulkanRenderer.hpp" +#include "../../include/private/Graphics/Vulkan/VulkanTexture.hpp" #include "Core/Logger.hpp" namespace Sleak { @@ -660,17 +661,6 @@ void VulkanRenderer::BindPBRMaterial(Sleak::Material* material) { vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, m_gbufferPipeline); m_activeCustomFormat = 0; - // Claim this material's own ring slot (own set + own UBO region) so the - // set is never rewritten while already bound by a prior draw this frame. - const uint32_t slot = m_pbrMaterialSlot[currentFrame]; - auto& chunks = m_pbrMaterialChunks[currentFrame]; - const uint32_t chunkIdx = slot / PBR_SETS_PER_FRAME; - if (chunkIdx >= chunks.size() && !AddPBRMaterialChunk(currentFrame)) return; - PBRMaterialChunk& chunk = chunks[chunkIdx]; - const uint32_t local = slot % PBR_SETS_PER_FRAME; - const VkDescriptorSet set = chunk.sets[local]; - const VkDeviceSize uboOffset = local * m_pbrMaterialUBOStride; - // Build PBRMaterialParams from Material properties PBRMaterialParams params{}; auto dc = material->GetDiffuseColor(); @@ -699,10 +689,6 @@ void VulkanRenderer::BindPBRMaterial(Sleak::Material* material) { params.hasAOMap = material->HasAOTexture() ? 1u : 0u; params.hasEmissiveMap = material->HasEmissiveTexture() ? 1u : 0u; - if (chunk.mapped) - memcpy(static_cast(chunk.mapped) + uboOffset, ¶ms, - sizeof(params)); - // Resolve textures — fall back to the default white 1×1 texture if absent auto resolveView = [&](Texture* tex) -> VkImageView { auto* vt = tex ? static_cast(tex) : nullptr; @@ -715,29 +701,64 @@ void VulkanRenderer::BindPBRMaterial(Sleak::Material* material) { return vt ? vt->GetSampler() : VK_NULL_HANDLE; }; - Texture* textures[6] = { + Texture* textures[PBR_TEXTURE_COUNT] = { material->GetDiffuseTexture(), - material->HasNormalTexture() ? material->GetNormalTexture() : nullptr, - material->HasMetallicTexture() ? material->GetMetallicTexture() : nullptr, - material->HasRoughnessTexture() ? material->GetRoughnessTexture() : nullptr, - material->HasAOTexture() ? material->GetAOTexture() : nullptr, - material->HasEmissiveTexture() ? material->GetEmissiveTexture() : nullptr, + material->HasNormalTexture() ? material->GetNormalTexture() : nullptr, + material->HasMetallicTexture() ? material->GetMetallicTexture() + : nullptr, + material->HasRoughnessTexture() ? material->GetRoughnessTexture() + : nullptr, + material->HasAOTexture() ? material->GetAOTexture() : nullptr, + material->HasEmissiveTexture() ? material->GetEmissiveTexture() + : nullptr, }; - std::array imageInfos{}; - for (uint32_t i = 0; i < 6; ++i) { + std::array imageInfos{}; + for (uint32_t i = 0; i < PBR_TEXTURE_COUNT; ++i) { imageInfos[i].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; imageInfos[i].imageView = resolveView(textures[i]); imageInfos[i].sampler = resolveSampler(textures[i]); } + // Same material with unchanged contents this frame: rebind its set + auto& bindings = m_pbrMaterialBindings[currentFrame]; + auto cached = bindings.find(material); + if (cached != bindings.end() && + memcmp(&cached->second.params, ¶ms, sizeof(params)) == 0 && + std::equal( + imageInfos.begin(), imageInfos.end(), cached->second.images.begin(), + [](const VkDescriptorImageInfo& a, const VkDescriptorImageInfo& b) { + return a.sampler == b.sampler && a.imageView == b.imageView && + a.imageLayout == b.imageLayout; + })) { + vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, + m_gbufferGeomLayout, 0, 1, &cached->second.set, + 0, nullptr); + return; + } + + // Claim this material's own ring slot (own set + own UBO region) so the + // set is never rewritten while already bound by a prior draw this frame. + const uint32_t slot = m_pbrMaterialSlot[currentFrame]; + auto& chunks = m_pbrMaterialChunks[currentFrame]; + const uint32_t chunkIdx = slot / PBR_SETS_PER_FRAME; + if (chunkIdx >= chunks.size() && !AddPBRMaterialChunk(currentFrame)) return; + PBRMaterialChunk& chunk = chunks[chunkIdx]; + const uint32_t local = slot % PBR_SETS_PER_FRAME; + const VkDescriptorSet set = chunk.sets[local]; + const VkDeviceSize uboOffset = local * m_pbrMaterialUBOStride; + + if (chunk.mapped) + memcpy(static_cast(chunk.mapped) + uboOffset, ¶ms, + sizeof(params)); + VkDescriptorBufferInfo bufInfo{}; bufInfo.buffer = chunk.ubo; bufInfo.offset = uboOffset; bufInfo.range = sizeof(PBRMaterialParams); - std::array writes{}; - for (uint32_t i = 0; i < 6; ++i) { + std::array writes{}; + for (uint32_t i = 0; i < PBR_TEXTURE_COUNT; ++i) { writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[i].dstSet = set; writes[i].dstBinding = i; @@ -746,19 +767,23 @@ void VulkanRenderer::BindPBRMaterial(Sleak::Material* material) { writes[i].descriptorCount = 1; writes[i].pImageInfo = &imageInfos[i]; } - writes[6].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - writes[6].dstSet = set; - writes[6].dstBinding = 6; - writes[6].dstArrayElement = 0; - writes[6].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; - writes[6].descriptorCount = 1; - writes[6].pBufferInfo = &bufInfo; + writes[PBR_TEXTURE_COUNT].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[PBR_TEXTURE_COUNT].dstSet = set; + writes[PBR_TEXTURE_COUNT].dstBinding = PBR_TEXTURE_COUNT; + writes[PBR_TEXTURE_COUNT].dstArrayElement = 0; + writes[PBR_TEXTURE_COUNT].descriptorType = + VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + writes[PBR_TEXTURE_COUNT].descriptorCount = 1; + writes[PBR_TEXTURE_COUNT].pBufferInfo = &bufInfo; - vkUpdateDescriptorSets(device, 7, writes.data(), 0, nullptr); + vkUpdateDescriptorSets(device, static_cast(writes.size()), + writes.data(), 0, nullptr); vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, m_gbufferGeomLayout, 0, 1, &set, 0, nullptr); + bindings[material] = {params, imageInfos, set}; + // Advance the ring for the next material this frame. m_pbrMaterialSlot[currentFrame] = slot + 1; } diff --git a/src/Graphics/Vulkan/VulkanRenderer.cpp b/src/Graphics/Vulkan/VulkanRenderer.cpp index 177a1db2..9a3562f3 100644 --- a/src/Graphics/Vulkan/VulkanRenderer.cpp +++ b/src/Graphics/Vulkan/VulkanRenderer.cpp @@ -291,6 +291,7 @@ void VulkanRenderer::BeginRender() { bFrameStarted = true; m_pbrMaterialSlot[currentFrame] = 0; // reset PBR material ring for this frame + m_pbrMaterialBindings[currentFrame].clear(); m_boneSlot[currentFrame] = 1; // slot 0 is the identity palette BeginGpuFrame(); diff --git a/src/Physics/CollisionDetection.cpp b/src/Physics/CollisionDetection.cpp index 2f2e5124..398a8e86 100644 --- a/src/Physics/CollisionDetection.cpp +++ b/src/Physics/CollisionDetection.cpp @@ -300,15 +300,48 @@ static BoundingCapsule TransformCapsule(const BoundingCapsule& c, const Vector3D return BoundingCapsule(c.center * scale + pos, c.radius * maxScale, c.halfHeight * axisScale, c.axis); } -static TriangleMesh TransformMesh(const TriangleMesh& m, const Vector3D& pos, const Vector3D& scale) { - TriangleMesh result; - result.indices = m.indices; - result.vertices.resize(m.vertices.size()); - for (size_t i = 0; i < m.vertices.size(); ++i) { - result.vertices[i] = m.vertices[i] * scale + pos; +/// Relative padding that keeps the mesh bounds early-out exact under rounding. +static constexpr float MESH_BOUNDS_SLACK = 1e-4f; + +/// Largest absolute component of v. +static float MaxAbsComponent(const Vector3D& v) { + return std::max( + {std::abs(v.GetX()), std::abs(v.GetY()), std::abs(v.GetZ())}); +} + +/// False only when the sphere cannot touch anything inside bounds. +static bool SphereMayTouchBounds(const BoundingSphere& s, const AABB& bounds) { + float magnitude = 1.0f + std::abs(s.radius) + MaxAbsComponent(s.center) + + MaxAbsComponent(bounds.min) + MaxAbsComponent(bounds.max); + float reach = s.radius + MESH_BOUNDS_SLACK * magnitude; + Vector3D diff = s.center - ClosestPointOnAABB(bounds, s.center); + return diff.Dot(diff) <= reach * reach; +} + +/// Sphere vs a mesh placed at pos with per-axis scale, without copying it. +static CollisionManifold TestSphereVsPlacedMesh(const BoundingSphere& sphere, + const TriangleMesh& mesh, + const Vector3D& pos, + const Vector3D& scale) { + CollisionManifold deepest; + AABB worldBounds = TransformAABB(mesh.bounds, pos, scale); + if (!SphereMayTouchBounds(sphere, worldBounds)) return deepest; + + const std::vector& verts = mesh.vertices; + const std::vector& idx = mesh.indices; + for (size_t i = 0; i + 2 < idx.size(); i += 3) { + Vector3D v0 = verts[idx[i]] * scale + pos; + Vector3D v1 = verts[idx[i + 1]] * scale + pos; + Vector3D v2 = verts[idx[i + 2]] * scale + pos; + + CollisionManifold m = TestSphereVsTriangle(sphere, v0, v1, v2); + if (m.hasCollision && + m.contact.penetration > deepest.contact.penetration) { + deepest = m; + } } - result.bounds = TransformAABB(m.bounds, pos, scale); - return result; + + return deepest; } CollisionManifold TestCollision(const ColliderShape& shapeA, const Vector3D& posA, const Vector3D& scaleA, @@ -351,18 +384,24 @@ CollisionManifold TestCollision(const ColliderShape& shapeA, const Vector3D& pos return TestCapsuleVsCapsule(TransformCapsule(a, posA, scaleA), TransformCapsule(b, posB, scaleB)); } else if constexpr (std::is_same_v && std::is_same_v) { - return TestSphereVsMesh(TransformSphere(a, posA, scaleA), TransformMesh(b, posB, scaleB)); + return TestSphereVsPlacedMesh(TransformSphere(a, posA, scaleA), b, + posB, scaleB); } else if constexpr (std::is_same_v && std::is_same_v) { - auto m = TestSphereVsMesh(TransformSphere(b, posB, scaleB), TransformMesh(a, posA, scaleA)); + auto m = TestSphereVsPlacedMesh(TransformSphere(b, posB, scaleB), a, + posA, scaleA); m.contact.normal = m.contact.normal * -1.0f; return m; } else if constexpr (std::is_same_v && std::is_same_v) { - return TestAABBvsMesh(TransformAABB(a, posA, scaleA), TransformMesh(b, posB, scaleB)); + return TestSphereVsPlacedMesh( + BoundingSphere::FromAABB(TransformAABB(a, posA, scaleA)), b, + posB, scaleB); } else if constexpr (std::is_same_v && std::is_same_v) { - auto m = TestAABBvsMesh(TransformAABB(b, posB, scaleB), TransformMesh(a, posA, scaleA)); + auto m = TestSphereVsPlacedMesh( + BoundingSphere::FromAABB(TransformAABB(b, posB, scaleB)), a, + posA, scaleA); m.contact.normal = m.contact.normal * -1.0f; return m; } diff --git a/src/Physics/PhysicsWorld.cpp b/src/Physics/PhysicsWorld.cpp index d4d6783e..0877e06c 100644 --- a/src/Physics/PhysicsWorld.cpp +++ b/src/Physics/PhysicsWorld.cpp @@ -1,15 +1,55 @@ -#include -#include -#include -#include #include -#include +#include #include +#include +#include +#include +#include #include +#include namespace Sleak { namespace Physics { +namespace { + +/// Rigidbody that collision response can push, or nullptr if none. +RigidbodyComponent* GetMovableBody(ColliderComponent* collider) { + GameObject* owner = collider->GetOwner(); + if (!owner) return nullptr; + auto* rb = owner->GetComponent(); + if (!rb || rb->GetBodyType() == BodyType::Static) return nullptr; + return rb; +} + +/// World position and scale the narrowphase uses for a collider. +void GetColliderPose(ColliderComponent* collider, Vector3D& pos, + Vector3D& scale) { + GameObject* owner = collider->GetOwner(); + if (auto* transform = owner->GetComponent()) { + pos = transform->GetWorldPosition() + collider->GetOffset(); + scale = transform->GetWorldScale(); + } else if (auto* cam = dynamic_cast(owner)) { + pos = cam->GetPosition() + collider->GetOffset(); + } +} + +/// Distance a dynamic body travels next step, zero for anything else. +Vector3D PredictDisplacement(ColliderComponent* collider, float dt) { + Vector3D none(0, 0, 0); + RigidbodyComponent* rb = GetMovableBody(collider); + if (!rb || rb->GetBodyType() != BodyType::Dynamic) return none; + + Vector3D d = rb->GetVelocity() * dt; + if (!std::isfinite(d.GetX()) || !std::isfinite(d.GetY()) || + !std::isfinite(d.GetZ())) { + return none; + } + return d; +} + +} // namespace + void PhysicsWorld::RegisterCollider(ColliderComponent* collider) { if (!collider) return; @@ -90,17 +130,17 @@ void PhysicsWorld::Step(float dt) { } } - UpdateBroadphase(); + UpdateBroadphase(dt); FindPairsAndResolve(); } -void PhysicsWorld::UpdateBroadphase() { +void PhysicsWorld::UpdateBroadphase(float dt) { for (auto* collider : m_colliders) { int proxyId = collider->GetProxyId(); if (proxyId < 0) continue; AABB newAABB = collider->GetWorldAABB(); - m_tree.MoveProxy(proxyId, newAABB, Vector3D(0, 0, 0)); + m_tree.MoveProxy(proxyId, newAABB, PredictDisplacement(collider, dt)); } } @@ -109,63 +149,58 @@ void PhysicsWorld::FindPairsAndResolve() { ColliderComponent* colliderA = m_colliders[i]; if (colliderA->GetProxyId() < 0) continue; + // Passive colliders never drive queries; their pairs have no response + RigidbodyComponent* rbA = GetMovableBody(colliderA); + if (!rbA) continue; + AABB worldA = colliderA->GetWorldAABB(); m_tree.Query(worldA, [&](int proxyId) -> bool { - auto* colliderB = static_cast(m_tree.GetUserData(proxyId)); + auto* colliderB = + static_cast(m_tree.GetUserData(proxyId)); if (colliderB == colliderA) return true; - // Skip duplicate pairs: only process when A < B (pointer order) - if (colliderA > colliderB) return true; + // Movable pairs are visited from both sides: keep pointer order + RigidbodyComponent* rbB = GetMovableBody(colliderB); + if (rbB && colliderA > colliderB) return true; // Layer filtering - if ((colliderA->GetLayer() & colliderB->GetMask()) == 0) return true; - if ((colliderB->GetLayer() & colliderA->GetMask()) == 0) return true; - - // Get transform data - auto* ownerA = colliderA->GetOwner(); - auto* ownerB = colliderB->GetOwner(); - if (!ownerA || !ownerB) return true; - - Vector3D posA, posB; - Vector3D scaleA(1, 1, 1), scaleB(1, 1, 1); - - auto* transformA = ownerA->GetComponent(); - if (transformA) { - posA = transformA->GetWorldPosition() + colliderA->GetOffset(); - scaleA = transformA->GetWorldScale(); - } else if (auto* camA = dynamic_cast(ownerA)) { - posA = camA->GetPosition() + colliderA->GetOffset(); - } + if ((colliderA->GetLayer() & colliderB->GetMask()) == 0) + return true; + if ((colliderB->GetLayer() & colliderA->GetMask()) == 0) + return true; - auto* transformB = ownerB->GetComponent(); - if (transformB) { - posB = transformB->GetWorldPosition() + colliderB->GetOffset(); - scaleB = transformB->GetWorldScale(); - } else if (auto* camB = dynamic_cast(ownerB)) { - posB = camB->GetPosition() + colliderB->GetOffset(); - } + // Skip if both are triggers + if (colliderA->IsTrigger() && colliderB->IsTrigger()) return true; - CollisionManifold manifold = TestCollision( - colliderA->GetShape(), posA, scaleA, - colliderB->GetShape(), posB, scaleB); + if (!colliderA->GetOwner() || !colliderB->GetOwner()) return true; - if (!manifold.hasCollision) return true; + // Narrowphase argument order stays pointer-ordered + const bool aFirst = colliderA < colliderB; + ColliderComponent* first = aFirst ? colliderA : colliderB; + ColliderComponent* second = aFirst ? colliderB : colliderA; + RigidbodyComponent* rbFirst = aFirst ? rbA : rbB; + RigidbodyComponent* rbSecond = aFirst ? rbB : rbA; - // Skip if both are triggers - if (colliderA->IsTrigger() && colliderB->IsTrigger()) return true; + Vector3D posFirst, posSecond; + Vector3D scaleFirst(1, 1, 1), scaleSecond(1, 1, 1); + GetColliderPose(first, posFirst, scaleFirst); + GetColliderPose(second, posSecond, scaleSecond); - // Resolve: push rigidbodies apart - auto* rbA = ownerA->GetComponent(); - auto* rbB = ownerB->GetComponent(); + CollisionManifold manifold = + TestCollision(first->GetShape(), posFirst, scaleFirst, + second->GetShape(), posSecond, scaleSecond); + + if (!manifold.hasCollision) return true; - if (rbA && rbA->GetBodyType() != BodyType::Static) { - rbA->ResolveCollision(manifold.contact.normal * -1.0f, - manifold.contact.penetration); + // Resolve: push rigidbodies apart + if (rbFirst) { + rbFirst->ResolveCollision(manifold.contact.normal * -1.0f, + manifold.contact.penetration); } - if (rbB && rbB->GetBodyType() != BodyType::Static) { - rbB->ResolveCollision(manifold.contact.normal, - manifold.contact.penetration); + if (rbSecond) { + rbSecond->ResolveCollision(manifold.contact.normal, + manifold.contact.penetration); } return true; diff --git a/src/Scene/GameObject.cpp b/src/Scene/GameObject.cpp index 227b4822..2c382f1e 100644 --- a/src/Scene/GameObject.cpp +++ b/src/Scene/GameObject.cpp @@ -152,6 +152,26 @@ namespace Sleak { Components[i]->OnDestroy(); } Components.clear(); + RefreshComponentCache(); + } + + void GameObject::RefreshComponentCache() { + m_cachedTransform = nullptr; + m_cachedMaterial = nullptr; + m_cachedMesh = nullptr; + + for (size_t i = 0; i < Components.GetSize(); ++i) { + Component* component = Components[i].get(); + if (!component) continue; + + if (!m_cachedTransform) + m_cachedTransform = + dynamic_cast(component); + if (!m_cachedMaterial) + m_cachedMaterial = dynamic_cast(component); + if (!m_cachedMesh) + m_cachedMesh = dynamic_cast(component); + } } /// Fallback material for the primitive factories below, using the default shader. @@ -167,9 +187,9 @@ namespace Sleak { MeshData meshData = GetPlaneMesh(width, height); object->AddComponent(meshData, Physics::ColliderType::AABB); object->AddComponent(BodyType::Static); + object->AddComponent(position); object->AddComponent(CreateDefaultMaterial()); object->AddComponent(std::move(meshData)); - object->AddComponent(position); object->Initialize(); return object; @@ -223,9 +243,9 @@ namespace Sleak { MeshData meshData = GetCylinderMesh(segments, height, radius); object->AddComponent(meshData, Physics::ColliderType::AABB); object->AddComponent(BodyType::Static); + object->AddComponent(position); object->AddComponent(CreateDefaultMaterial()); object->AddComponent(std::move(meshData)); - object->AddComponent(position); object->Initialize(); return object; diff --git a/src/Scene/MaterialComponent.cpp b/src/Scene/MaterialComponent.cpp index 037ca3a8..7c2cd6c0 100644 --- a/src/Scene/MaterialComponent.cpp +++ b/src/Scene/MaterialComponent.cpp @@ -1,4 +1,6 @@ +#include #include +#include #include namespace Sleak { @@ -41,6 +43,14 @@ namespace Sleak { } void MaterialComponent::Update(float DeltaTime) { + MeshComponent* mesh = + owner ? owner->GetComponent() : nullptr; + if (mesh && mesh->IsInitialized()) return; + + SubmitRenderState(); + } + + void MaterialComponent::SubmitRenderState() { if (!m_enabled || !m_material) return; diff --git a/src/Scene/MeshComponent.cpp b/src/Scene/MeshComponent.cpp index 186e2d37..9cca42b3 100644 --- a/src/Scene/MeshComponent.cpp +++ b/src/Scene/MeshComponent.cpp @@ -1,27 +1,41 @@ -#include "../../include/private/Graphics/Common/ResourceManager.hpp" -#include "../../include/private/Graphics/Common/BufferBase.hpp" -#include "../../include/private/Graphics/Common/ConstantBuffer.hpp" -#include "../../include/private/Graphics/Common/RenderCommandQueue.hpp" -#include -#include #include #include +#include +#include +#include #include +#include + +#include "../../include/private/Graphics/Common/BufferBase.hpp" +#include "../../include/private/Graphics/Common/ConstantBuffer.hpp" +#include "../../include/private/Graphics/Common/RenderCommandQueue.hpp" +#include "../../include/private/Graphics/Common/ResourceManager.hpp" namespace Sleak { namespace { - // World point (row-vector convention: world = local * M) as a - // degenerate AABB for accumulation. - Math::AABB TransformedPoint(const Math::Vector3D& p, - const Math::Matrix4& m) { - float x = p.GetX(), y = p.GetY(), z = p.GetZ(); - Math::Vector3D w( - x * m(0, 0) + y * m(1, 0) + z * m(2, 0) + m(3, 0), - x * m(0, 1) + y * m(1, 1) + z * m(2, 1) + m(3, 1), - x * m(0, 2) + y * m(1, 2) + z * m(2, 2) + m(3, 2)); - return Math::AABB(w, w); - } +/// World AABB of a local box under m (world = local * m), via Arvo's method. +Math::AABB TransformBounds(const Math::AABB& box, const Math::Matrix4& m) { + const Math::Vector3D c = box.Center(); + const Math::Vector3D e = box.Extents(); + const float cx = c.GetX(), cy = c.GetY(), cz = c.GetZ(); + const float ex = std::fabs(e.GetX()); + const float ey = std::fabs(e.GetY()); + const float ez = std::fabs(e.GetZ()); + + const Math::Vector3D center( + cx * m(0, 0) + cy * m(1, 0) + cz * m(2, 0) + m(3, 0), + cx * m(0, 1) + cy * m(1, 1) + cz * m(2, 1) + m(3, 1), + cx * m(0, 2) + cy * m(1, 2) + cz * m(2, 2) + m(3, 2)); + const Math::Vector3D extents( + ex * std::fabs(m(0, 0)) + ey * std::fabs(m(1, 0)) + + ez * std::fabs(m(2, 0)), + ex * std::fabs(m(0, 1)) + ey * std::fabs(m(1, 1)) + + ez * std::fabs(m(2, 1)), + ex * std::fabs(m(0, 2)) + ey * std::fabs(m(1, 2)) + + ez * std::fabs(m(2, 2))); + return Math::AABB(center - extents, center + extents); +} } // namespace MeshComponent::MeshComponent(GameObject* object, MeshData data) : Component(object) { @@ -55,19 +69,17 @@ MeshComponent::MeshComponent(GameObject* object, MeshData data) : Component(obje if (!bIsInitialized) return; + TransformComponent* tc = + owner ? owner->GetComponent() : nullptr; + if (m_hasLocalBounds || m_hasCullBounds) { Math::AABB worldBounds = m_cullBounds; bool cullReady = m_hasCullBounds; - if (m_hasLocalBounds && owner) { - if (auto* tc = owner->GetComponent()) { - Math::Matrix4 m = tc->GetTransformMatrix(); - Math::AABB acc = TransformedPoint(m_localBounds.Corner(0), m); - for (int i = 1; i < 8; ++i) - acc.Merge(TransformedPoint(m_localBounds.Corner(i), m)); - worldBounds = acc; - cullReady = true; - } + if (m_hasLocalBounds && tc) { + worldBounds = + TransformBounds(m_localBounds, tc->GetTransformMatrix()); + cullReady = true; } if (cullReady && @@ -75,6 +87,11 @@ MeshComponent::MeshComponent(GameObject* object, MeshData data) : Component(obje return; } + if (tc) tc->SubmitRenderState(); + if (MaterialComponent* mc = + owner ? owner->GetComponent() : nullptr) + mc->SubmitRenderState(); + RenderEngine::RenderCommandQueue::GetInstance()->SubmitDrawIndexed(VertexBuffer,IndexBuffer,ConstantBuffers,IndexCount); } diff --git a/src/Scene/TransformComponent.cpp b/src/Scene/TransformComponent.cpp index a0be4276..ab866ddf 100644 --- a/src/Scene/TransformComponent.cpp +++ b/src/Scene/TransformComponent.cpp @@ -26,18 +26,12 @@ namespace Sleak { scale(scale) {} - TransformComponent::~TransformComponent() { - delete Transform; - Transform = nullptr; - } + TransformComponent::~TransformComponent() = default; bool TransformComponent::Initialize() { - if (!Transform) Transform = new TransformMatrix(); - UpdateTransform(); if (ConstantBuffer.IsValid()) return true; - auto world = GetTransformMatrix(); - RenderEngine::TransformBuffer tb(world, + RenderEngine::TransformBuffer tb(WorldMatrix(), Camera::GetMainViewMatrix(), Camera::GetMainProjectionMatrix()); ConstantBuffer = RefPtr( @@ -49,21 +43,34 @@ namespace Sleak { } void TransformComponent::Update(float DeltaTime) { + MeshComponent* mesh = + owner ? owner->GetComponent() : nullptr; + if (mesh && mesh->IsInitialized()) return; + + SubmitRenderState(); + } + + void TransformComponent::SubmitRenderState() { UpdateConstantBuffer(); RenderEngine::RenderCommandQueue::GetInstance()->SubmitBindConstantBuffer(ConstantBuffer, 0); } - // Transformation matrix calculation - Matrix4 TransformComponent::GetTransformMatrix() { - return Transform->CalculateTransformMatrix(); + Matrix4 TransformComponent::GetTransformMatrix() { return WorldMatrix(); } + + const Matrix4& TransformComponent::WorldMatrix() { + if (m_worldDirty) { + m_world = (Matrix4::Scale(scale) * Matrix4::Rotate(rotation)) * + Matrix4::Translate(position); + m_worldDirty = false; + } + return m_world; } // Transformations void TransformComponent::Translate(const Vector3D& translation) { position += translation; - Transform->Translation = Matrix4::Translate(position); - UpdateTransform(); + m_worldDirty = true; } @@ -73,7 +80,7 @@ namespace Sleak { void TransformComponent::Rotate(const Quaternion& rotation) { this->rotation *= rotation; - UpdateTransform(); + m_worldDirty = true; } void TransformComponent::Scale(float amount) { @@ -82,20 +89,23 @@ namespace Sleak { void TransformComponent::Scale(const Vector3D& scale) { this->scale = scale * this->scale; - UpdateTransform(); + m_worldDirty = true; } // Setters void TransformComponent::SetPosition(const Vector3D& position) { this->position = position; + m_worldDirty = true; } void TransformComponent::SetRotation(const Quaternion& rotation) { this->rotation = rotation; + m_worldDirty = true; } void TransformComponent::SetScale(const Vector3D& scale) { this->scale = scale; + m_worldDirty = true; } // Direction vectors @@ -124,6 +134,7 @@ namespace Sleak { Vector3D direction = (target - position).Normalize(); Vector3D Up = VECTOR_Up; rotation = Quaternion::LookRotation(direction, Up); + m_worldDirty = true; } // Getters @@ -139,25 +150,13 @@ namespace Sleak { return scale; } - void TransformComponent::UpdateTransform() { - Transform->Translation = Matrix4::Translate(position); - Transform->Rotation = Matrix4::Rotate(rotation); - Transform->Scaling = Matrix4::Scale(scale); - - Transform->CalculateTransformMatrix(); - } - - void TransformComponent::UpdateConstantBuffer() { if (!ConstantBuffer) { SLEAK_ERROR("Constant buffer is not found to update for object {}", owner->GetName()); return; } - UpdateTransform(); - - auto world = GetTransformMatrix(); - RenderEngine::TransformBuffer tb(world, + RenderEngine::TransformBuffer tb(WorldMatrix(), Camera::GetMainViewMatrix(), Camera::GetMainProjectionMatrix()); @@ -166,8 +165,7 @@ namespace Sleak { Math::Matrix4 TransformComponent::GetMVP() { - auto mat = GetTransformMatrix() * - Camera::GetMainViewMatrix() * + auto mat = WorldMatrix() * Camera::GetMainViewMatrix() * Camera::GetMainProjectionMatrix(); return mat;