diff --git a/Assets/Shaders/glsl/skinning.vs b/Assets/Shaders/glsl/skinning.vs index 0e5a8a64..abec4202 100644 --- a/Assets/Shaders/glsl/skinning.vs +++ b/Assets/Shaders/glsl/skinning.vs @@ -46,10 +46,12 @@ void main() { } mat4 finalBonesMatrix = bonesTransformMatrices[bone_ids[i]]; - + totalPosition += finalBonesMatrix * vec4(vertex, 1.0f) * bone_weights[i]; - - mat3 normalMatrix = mat3(transpose(inverse(finalBonesMatrix))); + + // Skeletal bones are rigid (rotation + translation), so the 3x3 already is the + // correct normal transform -- no need for a per-bone, per-vertex inverse-transpose. + mat3 normalMatrix = mat3(finalBonesMatrix); totalNormal += normalMatrix * normal * bone_weights[i]; totalTangent += normalMatrix * tangent * bone_weights[i]; totalBitangent += normalMatrix * bitangent * bone_weights[i]; @@ -65,6 +67,7 @@ void main() { gl_Position = uProjection * uView * model * totalPosition; - fview = (inverse(uView) * vec4(0, 0, 0, 1)).xyz; + // Camera world position comes from the UBO instead of a per-vertex inverse(uView). + fview = uCameraPos.xyz; ftex_coords = tex_coords; } \ No newline at end of file diff --git a/Assets/Shaders/glsl/vert_uniforms.glsl b/Assets/Shaders/glsl/vert_uniforms.glsl index 2a813dc5..acd225cb 100644 --- a/Assets/Shaders/glsl/vert_uniforms.glsl +++ b/Assets/Shaders/glsl/vert_uniforms.glsl @@ -1,4 +1,5 @@ layout(std140, binding = 0) uniform SceneData { mat4 uProjection; mat4 uView; + vec4 uCameraPos; // world-space camera position (xyz) }; \ No newline at end of file diff --git a/ICE/Assets/include/AssetBank.h b/ICE/Assets/include/AssetBank.h index e6ff08a4..e4ac9e29 100644 --- a/ICE/Assets/include/AssetBank.h +++ b/ICE/Assets/include/AssetBank.h @@ -159,12 +159,10 @@ class AssetBank { } AssetPath getName(AssetUID uid) { - for (const auto& [path, id] : nameMapping) { - if (id == uid) { - return path; - } - } - return AssetPath(""); + // O(1): the entry already stores its path. This used to linear-scan nameMapping, + // which made project saves (getName per asset) O(n^2). + auto it = resources.find(uid); + return it == resources.end() ? AssetPath("") : it->second.path; } AssetUID getUID(const AssetPath& name) { diff --git a/ICE/Assets/include/AssetPath.h b/ICE/Assets/include/AssetPath.h index db9c15d7..a0cc9831 100644 --- a/ICE/Assets/include/AssetPath.h +++ b/ICE/Assets/include/AssetPath.h @@ -15,9 +15,9 @@ namespace ICE { class AssetPath { public: - AssetPath(const AssetPath& cpy); + AssetPath(const AssetPath& cpy) = default; // copy members directly (no re-parse) AssetPath(std::string path); - std::string toString() const; + const std::string& toString() const; std::string prefix() const; template static AssetPath WithTypePrefix(std::string path) { @@ -26,7 +26,8 @@ class AssetPath { return AssetPath(typenames.at(typeid(T)) + ASSET_PATH_SEPARATOR + path); } - bool operator==(AssetPath other) const { return (other.toString() == this->toString()); } + // Compare the pre-computed canonical string (no allocation / re-parse per comparison). + bool operator==(const AssetPath& other) const { return m_string == other.m_string; } std::vector getPath() const; @@ -37,6 +38,7 @@ class AssetPath { private: std::vector path; std::string name; + std::string m_string; // canonical "prefix/name", computed once in the constructor static std::unordered_map typenames; }; } // namespace ICE diff --git a/ICE/Assets/include/Material.h b/ICE/Assets/include/Material.h index ea491459..59b96e6b 100644 --- a/ICE/Assets/include/Material.h +++ b/ICE/Assets/include/Material.h @@ -39,7 +39,7 @@ class Material : public Asset { void renameUniform(const std::string& previous_name, const std::string& new_name); void removeUniform(const std::string& name); - std::unordered_map getAllUniforms() const; + const std::unordered_map& getAllUniforms() const; AssetUID getShader() const; void setShader(AssetUID shader_id); bool isTransparent() const; diff --git a/ICE/Assets/src/AssetPath.cpp b/ICE/Assets/src/AssetPath.cpp index 67b5dc6b..db14bcd8 100644 --- a/ICE/Assets/src/AssetPath.cpp +++ b/ICE/Assets/src/AssetPath.cpp @@ -27,10 +27,11 @@ AssetPath::AssetPath(std::string path) { } } name = path.substr(last, path.length() - last); + m_string = prefix() + name; // canonical form, computed once } -std::string AssetPath::toString() const { - return (prefix() + name); +const std::string& AssetPath::toString() const { + return m_string; } std::vector AssetPath::getPath() const { @@ -43,9 +44,7 @@ std::string AssetPath::getName() const { void AssetPath::setName(const std::string &name) { AssetPath::name = name; -} - -AssetPath::AssetPath(const AssetPath &cpy) : AssetPath(cpy.toString()) { + m_string = prefix() + name; // keep the canonical string in sync } std::string AssetPath::prefix() const { diff --git a/ICE/Assets/src/Material.cpp b/ICE/Assets/src/Material.cpp index 2ceb8559..530cb1be 100644 --- a/ICE/Assets/src/Material.cpp +++ b/ICE/Assets/src/Material.cpp @@ -35,7 +35,7 @@ void Material::removeUniform(const std::string& name) { } } -std::unordered_map Material::getAllUniforms() const { +const std::unordered_map& Material::getAllUniforms() const { return m_uniforms; } diff --git a/ICE/Core/src/ICEEngine.cpp b/ICE/Core/src/ICEEngine.cpp index fad6e831..8564286d 100644 --- a/ICE/Core/src/ICEEngine.cpp +++ b/ICE/Core/src/ICEEngine.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,10 @@ void ICEEngine::initialize(const std::shared_ptr &graphics_fact } void ICEEngine::step() { + // Snapshot the previous frame's profiler samples and start a new frame. + Profiler::get().beginFrame(); + ICE_PROFILE_SCOPE("Engine::step"); + // Delta time in seconds as a double: the old integer-millisecond cast truncated to 0 // above ~1000 fps (freezing animation) and lost ~13% at 144 Hz. auto now = std::chrono::steady_clock::now(); @@ -46,7 +51,19 @@ void ICEEngine::step() { } auto render_system = project->getCurrentScene()->getRegistry()->getSystem(); render_system->setTarget(m_target_fb); - project->getCurrentScene()->getRegistry()->updateSystems(m_delta_time); + { + ICE_PROFILE_SCOPE("updateSystems"); + project->getCurrentScene()->getRegistry()->updateSystems(m_delta_time); + } + + // Periodically surface the previous frame's timing breakdown (every ~5s at 60fps). + static int s_profile_log_counter = 0; + if (++s_profile_log_counter >= 300) { + s_profile_log_counter = 0; + for (const auto& [name, ms] : Profiler::get().lastFrame()) { + Logger::Log(Logger::DEBUG, "Profiler", "%s: %.3f ms", name.c_str(), ms); + } + } } void ICEEngine::setupScene(const std::shared_ptr &camera_) { diff --git a/ICE/Graphics/include/ForwardRenderer.h b/ICE/Graphics/include/ForwardRenderer.h index 8fb84081..23cbe2e5 100644 --- a/ICE/Graphics/include/ForwardRenderer.h +++ b/ICE/Graphics/include/ForwardRenderer.h @@ -29,7 +29,7 @@ class ForwardRenderer : public Renderer { ForwardRenderer(const std::shared_ptr &api, const std::shared_ptr &factory); void submitSkybox(const Skybox &e) override; - void submitDrawable(const Drawable &e) override; + void submitDrawable(Drawable e) override; void submitLight(const Light &e) override; void prepareFrame(Camera &camera) override; diff --git a/ICE/Graphics/include/GeometryPass.h b/ICE/Graphics/include/GeometryPass.h index 21fd7149..460a6476 100644 --- a/ICE/Graphics/include/GeometryPass.h +++ b/ICE/Graphics/include/GeometryPass.h @@ -2,6 +2,9 @@ #include +#include +#include + #include "Framebuffer.h" #include "GraphicsFactory.h" #include "RenderCommand.h" @@ -23,6 +26,9 @@ class GeometryPass : public RenderPass { // Reused every draw for per-instance data instead of allocating a fresh GL buffer // per command per frame. std::shared_ptr m_instance_buffer; + // Reused scratch buffer for packing a skinned mesh's bone palette into a contiguous, + // id-indexed array for a single glUniformMatrix4fv upload. + std::vector m_bone_palette; std::vector* m_render_queue; }; } // namespace ICE \ No newline at end of file diff --git a/ICE/Graphics/include/GraphicsAPI.h b/ICE/Graphics/include/GraphicsAPI.h index c76866c3..e0fddaf1 100644 --- a/ICE/Graphics/include/GraphicsAPI.h +++ b/ICE/Graphics/include/GraphicsAPI.h @@ -32,6 +32,13 @@ class RendererAPI { virtual void setDepthTest(bool enable) const = 0; virtual void setDepthMask(bool enable) const = 0; virtual void setDepthFunc(DepthFunc func) const = 0; + virtual void setBlend(bool enable) const = 0; + + // GPU timing via double-buffered GL_TIME_ELAPSED queries. begin/end bracket the GPU work; + // endGPUTimer returns the elapsed GPU time in milliseconds of a *previous* frame (one + // frame of latency) so reading the result never stalls the pipeline. + virtual void beginGPUTimer() const = 0; + virtual double endGPUTimer() const = 0; virtual void setBackfaceCulling(bool enable) const = 0; virtual void checkAndLogErrors() const = 0; diff --git a/ICE/Graphics/include/RenderCommand.h b/ICE/Graphics/include/RenderCommand.h index 39d3b7a1..f9617e20 100644 --- a/ICE/Graphics/include/RenderCommand.h +++ b/ICE/Graphics/include/RenderCommand.h @@ -38,6 +38,9 @@ struct RenderCommand { bool depthTest : 1 = true; bool depthWrite : 1 = true; bool is_instanced : 1 = false; + // Alpha blending: only transparent materials need it; opaque draws leave it off so they + // don't lose early-Z/HSR to a blend that does nothing. + bool blend : 1 = false; DepthFunc depth_func = DepthFunc::Less; diff --git a/ICE/Graphics/include/Renderer.h b/ICE/Graphics/include/Renderer.h index f7bf282f..721e0b6a 100644 --- a/ICE/Graphics/include/Renderer.h +++ b/ICE/Graphics/include/Renderer.h @@ -41,6 +41,7 @@ struct alignas(16) SceneLightsUBO { struct alignas(16) CameraUBO { Eigen::Matrix4f projection; Eigen::Matrix4f view; + Eigen::Vector4f cameraPos; // world-space camera position (xyz); matches std140 SceneData }; struct Skybox { @@ -70,7 +71,9 @@ class Renderer { public: virtual ~Renderer() = default; virtual void submitSkybox(const Skybox& e) = 0; - virtual void submitDrawable(const Drawable& e) = 0; + // By value so the caller's temporary (with its texture/bone maps) can be moved into the + // renderer's queue instead of copied. + virtual void submitDrawable(Drawable e) = 0; virtual void submitLight(const Light& e) = 0; virtual void prepareFrame(Camera& camera) = 0; virtual std::shared_ptr render() = 0; diff --git a/ICE/Graphics/include/ShaderProgram.h b/ICE/Graphics/include/ShaderProgram.h index 57f0747d..88e16b73 100644 --- a/ICE/Graphics/include/ShaderProgram.h +++ b/ICE/Graphics/include/ShaderProgram.h @@ -14,10 +14,12 @@ class ShaderProgram { virtual void loadInts(const std::string &name, int *array, uint32_t size) = 0; virtual void loadFloat(const std::string &name, float v) = 0; - virtual void loadFloat2(const std::string &name, Eigen::Vector2f vec) = 0; - virtual void loadFloat3(const std::string &name, Eigen::Vector3f vec) = 0; - virtual void loadFloat4(const std::string &name, Eigen::Vector4f vec) = 0; + virtual void loadFloat2(const std::string &name, const Eigen::Vector2f &vec) = 0; + virtual void loadFloat3(const std::string &name, const Eigen::Vector3f &vec) = 0; + virtual void loadFloat4(const std::string &name, const Eigen::Vector4f &vec) = 0; - virtual void loadMat4(const std::string &name, Eigen::Matrix4f mat) = 0; + virtual void loadMat4(const std::string &name, const Eigen::Matrix4f &mat) = 0; + // Upload a contiguous array of matrices in one call (e.g. a bone palette). + virtual void loadMat4v(const std::string &name, const Eigen::Matrix4f *data, uint32_t count) = 0; }; } // namespace ICE \ No newline at end of file diff --git a/ICE/Graphics/src/ForwardRenderer.cpp b/ICE/Graphics/src/ForwardRenderer.cpp index 8031e933..b1d359c3 100644 --- a/ICE/Graphics/src/ForwardRenderer.cpp +++ b/ICE/Graphics/src/ForwardRenderer.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -29,8 +30,8 @@ ForwardRenderer::ForwardRenderer(const std::shared_ptr& api, const void ForwardRenderer::submitSkybox(const Skybox& e) { m_skybox.emplace(e); } -void ForwardRenderer::submitDrawable(const Drawable& e) { - m_drawables.push_back(e); +void ForwardRenderer::submitDrawable(Drawable e) { + m_drawables.push_back(std::move(e)); } void ForwardRenderer::submitLight(const Light& e) { m_lights.push_back(e); @@ -39,8 +40,10 @@ void ForwardRenderer::submitLight(const Light& e) { void ForwardRenderer::prepareFrame(Camera& camera) { auto view_mat = camera.lookThrough(); auto proj_mat = camera.getProjection(); + auto cam_pos = camera.getPosition(); - CameraUBO camera_ubo_data{.projection = proj_mat, .view = view_mat}; + CameraUBO camera_ubo_data{ + .projection = proj_mat, .view = view_mat, .cameraPos = Eigen::Vector4f(cam_pos.x(), cam_pos.y(), cam_pos.z(), 1.0f)}; m_camera_ubo->putData(&camera_ubo_data, sizeof(CameraUBO)); SceneLightsUBO light_ubo_data; @@ -110,6 +113,7 @@ void ForwardRenderer::prepareFrame(Camera& camera) { cmd.depthTest = true; cmd.faceCulling = true; cmd.is_instanced = false; + cmd.blend = cmd.material->isTransparent(); cmd.computeSortKey(cmd.material->isTransparent(), dist); m_render_commands.push_back(cmd); } else { @@ -137,6 +141,7 @@ void ForwardRenderer::prepareFrame(Camera& camera) { cmd.is_instanced = true; cmd.instance_count = batch.size(); cmd.instance_data = &instance_data_vec; // Link to stored data + cmd.blend = cmd.material->isTransparent(); cmd.computeSortKey(cmd.material->isTransparent(), dist); m_render_commands.push_back(cmd); } @@ -155,6 +160,7 @@ void ForwardRenderer::prepareFrame(Camera& camera) { cmd.faceCulling = true; cmd.bones = &drawable->bone_matrices; cmd.is_instanced = false; + cmd.blend = cmd.material->isTransparent(); cmd.computeSortKey(cmd.material->isTransparent(), dist); m_render_commands.push_back(cmd); } @@ -165,9 +171,10 @@ void ForwardRenderer::prepareFrame(Camera& camera) { } std::shared_ptr ForwardRenderer::render() { + m_api->beginGPUTimer(); m_geometry_pass.execute(); - auto result = m_geometry_pass.getResult(); - return result; + Profiler::get().addSample("GPU::geometry", m_api->endGPUTimer()); + return m_geometry_pass.getResult(); } void ForwardRenderer::endFrame() { diff --git a/ICE/Graphics/src/GeometryPass.cpp b/ICE/Graphics/src/GeometryPass.cpp index a7785a2a..f3bf2d51 100644 --- a/ICE/Graphics/src/GeometryPass.cpp +++ b/ICE/Graphics/src/GeometryPass.cpp @@ -1,5 +1,7 @@ #include "GeometryPass.h" +#include + #include "InstanceData.h" namespace ICE { @@ -18,27 +20,59 @@ void GeometryPass::execute() { Material* current_material = nullptr; GPUMesh* current_mesh = nullptr; + // Cache render state within the pass so identical consecutive commands (e.g. a run of + // opaque draws) don't re-issue the same GL state calls. Forced on the first command. + bool state_init = false; + bool cur_cull = false, cur_depth_test = false, cur_depth_write = false, cur_blend = false; + DepthFunc cur_depth_func = DepthFunc::Less; + for (const auto& command : *m_render_queue) { auto& shader = command.shader; auto& material = command.material; auto& mesh = command.mesh; - m_api->setBackfaceCulling(command.faceCulling); - m_api->setDepthTest(command.depthTest); - m_api->setDepthMask(command.depthWrite); - m_api->setDepthFunc(command.depth_func); + if (!state_init || cur_cull != command.faceCulling) { + m_api->setBackfaceCulling(command.faceCulling); + cur_cull = command.faceCulling; + } + if (!state_init || cur_depth_test != command.depthTest) { + m_api->setDepthTest(command.depthTest); + cur_depth_test = command.depthTest; + } + if (!state_init || cur_depth_write != command.depthWrite) { + m_api->setDepthMask(command.depthWrite); + cur_depth_write = command.depthWrite; + } + if (!state_init || cur_depth_func != command.depth_func) { + m_api->setDepthFunc(command.depth_func); + cur_depth_func = command.depth_func; + } + if (!state_init || cur_blend != command.blend) { + m_api->setBlend(command.blend); + cur_blend = command.blend; + } + state_init = true; if (shader != current_shader) { shader->bind(); current_shader = shader; } - // Handle bone matrices (non-instanced only) + // Handle bone matrices (non-instanced only). Pack into a contiguous, id-indexed + // buffer (reused across draws) and upload the whole palette in one glUniformMatrix4fv + // call instead of one string-built uniform lookup + upload per bone. if (!command.is_instanced && command.bones && !command.bones->empty()) { - // TODO: Use UBO instead of individual uploads for better performance + int max_id = 0; for (const auto& [id, matrix] : *command.bones) { - current_shader->loadMat4("bonesTransformMatrices[" + std::to_string(id) + "]", matrix); + max_id = std::max(max_id, id); } + m_bone_palette.assign(static_cast(max_id) + 1, Eigen::Matrix4f::Identity()); + for (const auto& [id, matrix] : *command.bones) { + if (id >= 0) { + m_bone_palette[id] = matrix; + } + } + current_shader->loadMat4v("bonesTransformMatrices", m_bone_palette.data(), static_cast(m_bone_palette.size())); } if (material != current_material) { @@ -100,6 +134,10 @@ void GeometryPass::execute() { va->pushVertexBuffer(m_instance_buffer, 7, 16, 1); m_api->renderVertexArrayInstanced(va, command.instance_count); } + + // Restore the globally-on blend state expected by other passes (final blit, editor + // picking), since opaque commands turned it off. + m_api->setBlend(true); } std::shared_ptr GeometryPass::getResult() const { diff --git a/ICE/GraphicsAPI/OpenGL/include/OpenGLRendererAPI.h b/ICE/GraphicsAPI/OpenGL/include/OpenGLRendererAPI.h index 24600532..58d54941 100644 --- a/ICE/GraphicsAPI/OpenGL/include/OpenGLRendererAPI.h +++ b/ICE/GraphicsAPI/OpenGL/include/OpenGLRendererAPI.h @@ -35,9 +35,23 @@ namespace ICE { void setDepthFunc(DepthFunc func) const override; + void setBlend(bool enable) const override; + + void beginGPUTimer() const override; + double endGPUTimer() const override; + void setBackfaceCulling(bool enable) const override; void checkAndLogErrors() const override; + + private: + // Double-buffered GL_TIME_ELAPSED query state (GPU state, hence mutable behind the + // const API). + mutable GLuint m_gpu_query[2] = {0, 0}; + mutable bool m_gpu_query_used[2] = {false, false}; + mutable int m_gpu_query_idx = 0; + mutable bool m_gpu_query_init = false; + mutable double m_last_gpu_ms = 0.0; }; } diff --git a/ICE/GraphicsAPI/OpenGL/include/OpenGLShader.h b/ICE/GraphicsAPI/OpenGL/include/OpenGLShader.h index b2e6ab9a..a2a7fe5b 100644 --- a/ICE/GraphicsAPI/OpenGL/include/OpenGLShader.h +++ b/ICE/GraphicsAPI/OpenGL/include/OpenGLShader.h @@ -33,13 +33,15 @@ class OpenGLShader : public ShaderProgram { void loadFloat(const std::string &name, float v) override; - void loadFloat2(const std::string &name, Eigen::Vector2f vec) override; + void loadFloat2(const std::string &name, const Eigen::Vector2f &vec) override; - void loadFloat3(const std::string &name, Eigen::Vector3f vec) override; + void loadFloat3(const std::string &name, const Eigen::Vector3f &vec) override; - void loadFloat4(const std::string &name, Eigen::Vector4f vec) override; + void loadFloat4(const std::string &name, const Eigen::Vector4f &vec) override; - void loadMat4(const std::string &name, Eigen::Matrix4f mat) override; + void loadMat4(const std::string &name, const Eigen::Matrix4f &mat) override; + + void loadMat4v(const std::string &name, const Eigen::Matrix4f *data, uint32_t count) override; private: GLint getLocation(const std::string &name); diff --git a/ICE/GraphicsAPI/OpenGL/src/OpenGLRendererAPI.cpp b/ICE/GraphicsAPI/OpenGL/src/OpenGLRendererAPI.cpp index e26a0c54..0f95184b 100644 --- a/ICE/GraphicsAPI/OpenGL/src/OpenGLRendererAPI.cpp +++ b/ICE/GraphicsAPI/OpenGL/src/OpenGLRendererAPI.cpp @@ -62,6 +62,38 @@ void OpenGLRendererAPI::setDepthFunc(DepthFunc func) const { glDepthFunc(func == DepthFunc::LEqual ? GL_LEQUAL : GL_LESS); } +void OpenGLRendererAPI::setBlend(bool enable) const { + if (enable) { + glEnable(GL_BLEND); + } else { + glDisable(GL_BLEND); + } +} + +void OpenGLRendererAPI::beginGPUTimer() const { + if (!m_gpu_query_init) { + glGenQueries(2, m_gpu_query); + m_gpu_query_init = true; + } + glBeginQuery(GL_TIME_ELAPSED, m_gpu_query[m_gpu_query_idx]); +} + +double OpenGLRendererAPI::endGPUTimer() const { + glEndQuery(GL_TIME_ELAPSED); + m_gpu_query_used[m_gpu_query_idx] = true; + + // Read the other buffer's result (last frame's query): one frame old, so it's ready and + // reading it doesn't stall. + const int other = 1 - m_gpu_query_idx; + if (m_gpu_query_used[other]) { + GLuint64 elapsed_ns = 0; + glGetQueryObjectui64v(m_gpu_query[other], GL_QUERY_RESULT, &elapsed_ns); + m_last_gpu_ms = static_cast(elapsed_ns) / 1.0e6; + } + m_gpu_query_idx = other; + return m_last_gpu_ms; +} + void OpenGLRendererAPI::setBackfaceCulling(bool enable) const { if (enable) { glEnable(GL_CULL_FACE); diff --git a/ICE/GraphicsAPI/OpenGL/src/OpenGLShader.cpp b/ICE/GraphicsAPI/OpenGL/src/OpenGLShader.cpp index d911d80b..2b6e4ed4 100644 --- a/ICE/GraphicsAPI/OpenGL/src/OpenGLShader.cpp +++ b/ICE/GraphicsAPI/OpenGL/src/OpenGLShader.cpp @@ -67,28 +67,37 @@ void OpenGLShader::loadFloat(const std::string &name, float v) { glUniform1f(getLocation(name), v); } -void OpenGLShader::loadFloat2(const std::string &name, Eigen::Vector2f vec) { +void OpenGLShader::loadFloat2(const std::string &name, const Eigen::Vector2f &vec) { glUniform2f(getLocation(name), vec.x(), vec.y()); } -void OpenGLShader::loadFloat3(const std::string &name, Eigen::Vector3f vec) { +void OpenGLShader::loadFloat3(const std::string &name, const Eigen::Vector3f &vec) { glUniform3f(getLocation(name), vec.x(), vec.y(), vec.z()); } -void OpenGLShader::loadFloat4(const std::string &name, Eigen::Vector4f vec) { +void OpenGLShader::loadFloat4(const std::string &name, const Eigen::Vector4f &vec) { glUniform4f(getLocation(name), vec.x(), vec.y(), vec.z(), vec.w()); } -void OpenGLShader::loadMat4(const std::string &name, Eigen::Matrix4f mat) { +void OpenGLShader::loadMat4(const std::string &name, const Eigen::Matrix4f &mat) { glUniformMatrix4fv(getLocation(name), 1, GL_FALSE, mat.data()); } +void OpenGLShader::loadMat4v(const std::string &name, const Eigen::Matrix4f *data, uint32_t count) { + // std::vector / a Matrix4f array is contiguous, column-major -- exactly what + // glUniformMatrix4fv expects for a mat4[] uniform, so one call uploads the whole array. + glUniformMatrix4fv(getLocation(name), count, GL_FALSE, data->data()); +} + GLint OpenGLShader::getLocation(const std::string &name) { - if (!m_locations.contains(name)) { - GLint location = glGetUniformLocation(m_programID, name.c_str()); - m_locations[name] = static_cast(location); + // Single hash lookup on the hot path (was contains + operator[] insert + operator[]). + auto it = m_locations.find(name); + if (it != m_locations.end()) { + return static_cast(it->second); } - return m_locations[name]; + GLint location = glGetUniformLocation(m_programID, name.c_str()); + m_locations.emplace(name, static_cast(location)); + return location; } bool compileShader(GLenum type, const std::string &source, GLint *shader) { diff --git a/ICE/Math/src/AABB.cpp b/ICE/Math/src/AABB.cpp index 81040a04..3237c343 100644 --- a/ICE/Math/src/AABB.cpp +++ b/ICE/Math/src/AABB.cpp @@ -6,7 +6,12 @@ namespace ICE { -AABB::AABB(const Eigen::Vector3f &min, const Eigen::Vector3f &max) : AABB(std::vector{min, max}) { +AABB::AABB(const Eigen::Vector3f &a, const Eigen::Vector3f &b) { + // Set corners directly (no heap-allocated std::vector). cwiseMin/Max keeps min <= max + // even when callers pass swapped corners (e.g. scaledBy with a negative scale). + min = a.cwiseMin(b); + max = a.cwiseMax(b); + precomputeCenterAndExtent(); } AABB::AABB(const std::vector &points) { if (points.empty()) { diff --git a/ICE/Multithreading/include/ThreadSafeQueue.h b/ICE/Multithreading/include/ThreadSafeQueue.h index 71e7b15f..4b2108d6 100644 --- a/ICE/Multithreading/include/ThreadSafeQueue.h +++ b/ICE/Multithreading/include/ThreadSafeQueue.h @@ -20,7 +20,7 @@ class ThreadSafeQueue { // Push an item into the queue void push(T value) { std::lock_guard lock(m_mutex); - m_queue.push(value); + m_queue.push(std::move(value)); m_cond_var.notify_one(); } @@ -43,7 +43,7 @@ class ThreadSafeQueue { m_cond_var.wait(lock, [this] { return !m_queue.empty() || m_stop; }); if (m_stop && m_queue.empty()) throw std::runtime_error("Queue stopped"); - T value = m_queue.front(); + T value = std::move(m_queue.front()); m_queue.pop(); return value; } diff --git a/ICE/System/include/SceneGraphSystem.h b/ICE/System/include/SceneGraphSystem.h index 161c6464..d28e3f1b 100644 --- a/ICE/System/include/SceneGraphSystem.h +++ b/ICE/System/include/SceneGraphSystem.h @@ -16,6 +16,10 @@ class SceneGraphSystem : public System { int updateOrder() const override { return SceneGraphSystemOrder; } + // Recursive transform propagation over raw node pointers (no per-frame std::function + // allocation, no shared_ptr refcount churn per node). + void updateNode(SceneGraph::SceneNode *node, const Eigen::Matrix4f &parentMatrix, bool parent_changed); + std::vector getSignatures(const ComponentManager &comp_manager) const override { Signature signature0; signature0.set(comp_manager.getComponentType()); diff --git a/ICE/System/include/System.h b/ICE/System/include/System.h index efbe1dc1..17568e4b 100644 --- a/ICE/System/include/System.h +++ b/ICE/System/include/System.h @@ -60,7 +60,13 @@ class SystemManager { void entitySignatureChanged(Entity entity, Signature entitySignature, const ComponentManager& comp_manager) { // Notify each system that an entity's signature changed for (auto const& system : orderedSystems) { - auto const& systemSignature = system->getSignatures(comp_manager); + // A system's signatures are fixed once component types are registered, so cache + // them instead of rebuilding the vector on every component add/remove. + auto sig_it = m_signatureCache.find(system.get()); + if (sig_it == m_signatureCache.end()) { + sig_it = m_signatureCache.emplace(system.get(), system->getSignatures(comp_manager)).first; + } + const auto& systemSignature = sig_it->second; // Entity signature matches system signature - insert into set. onEntityAdded is // fired on every matching signature change (not just the first insert): a system @@ -114,5 +120,8 @@ class SystemManager { std::unordered_map> systems; // Same systems, kept sorted by updateOrder() for deterministic iteration. std::vector> orderedSystems; + // Cached signatures per system (stable after component registration); avoids rebuilding + // the vector on every entitySignatureChanged call. + std::unordered_map> m_signatureCache; }; } // namespace ICE diff --git a/ICE/System/src/RenderSystem.cpp b/ICE/System/src/RenderSystem.cpp index 48563613..86af6fc8 100644 --- a/ICE/System/src/RenderSystem.cpp +++ b/ICE/System/src/RenderSystem.cpp @@ -50,7 +50,9 @@ void RenderSystem::update(double delta) { auto model_mat = tc->getWorldMatrix(); - if (!m_culling_cache.contains(e) || m_culling_cache[e].lastTransformVersion != tc->getVersion() || m_culling_cache[e].lastMesh != rc->mesh) { + // Single hash lookup instead of contains + several operator[] per entity per frame. + auto cache_it = m_culling_cache.find(e); + if (cache_it == m_culling_cache.end() || cache_it->second.lastTransformVersion != tc->getVersion() || cache_it->second.lastMesh != rc->mesh) { auto local_aabb = m_gpu_bank->getMeshAABB(rc->mesh); Eigen::Vector3f localCenter = local_aabb.getCenter(); Eigen::Vector3f localExtents = local_aabb.getExtent(); @@ -63,15 +65,20 @@ void RenderSystem::update(double delta) { Eigen::Matrix3f absR = R.cwiseAbs(); Eigen::Vector3f worldExtents = absR * localExtents; - m_culling_cache[e] = CullingData{ + CullingData data{ .lastTransformVersion = tc->getVersion(), .lastMesh = rc->mesh, .worldCenter = worldCenter, .worldExtents = worldExtents, }; + if (cache_it == m_culling_cache.end()) { + cache_it = m_culling_cache.emplace(e, data).first; + } else { + cache_it->second = data; + } } - if (!isAABBInFrustum(frustum, m_culling_cache[e].worldCenter, m_culling_cache[e].worldExtents)) + if (!isAABBInFrustum(frustum, cache_it->second.worldCenter, cache_it->second.worldExtents)) continue; auto mesh = m_gpu_bank->getMesh(rc->mesh); diff --git a/ICE/System/src/SceneGraphSystem.cpp b/ICE/System/src/SceneGraphSystem.cpp index 6b9d45e7..3d82a3e4 100644 --- a/ICE/System/src/SceneGraphSystem.cpp +++ b/ICE/System/src/SceneGraphSystem.cpp @@ -12,29 +12,29 @@ void SceneGraphSystem::onEntityRemoved(Entity e) { m_transformVersions.erase(e); } void SceneGraphSystem::update(double delta) { - auto root = m_scene->getGraph()->getRoot(); - std::function &, const Eigen::Matrix4f &, bool)> updateNode; - updateNode = [this, &updateNode](const std::shared_ptr &node, const Eigen::Matrix4f &parentMatrix, bool parent_changed) { - Eigen::Matrix4f newParentMatrix = parentMatrix; - if (node->entity != 0 && m_scene->getRegistry()->entityHasComponent(node->entity)) { - auto tc = m_scene->getRegistry()->getComponent(node->entity); - - if (parent_changed) { - tc->updateParentMatrix(parentMatrix); - } + updateNode(m_scene->getGraph()->getRoot().get(), Eigen::Matrix4f::Identity(), false); +} - if (!m_transformVersions.contains(node->entity) || m_transformVersions[node->entity] != tc->getVersion()) { - parent_changed = true; - m_transformVersions[node->entity] = tc->getVersion(); - } - newParentMatrix = tc->getWorldMatrix(); +void SceneGraphSystem::updateNode(SceneGraph::SceneNode *node, const Eigen::Matrix4f &parentMatrix, bool parent_changed) { + auto *registry = m_scene->getRegistry().get(); + Eigen::Matrix4f newParentMatrix = parentMatrix; + if (node->entity != NULL_ENTITY && registry->entityHasComponent(node->entity)) { + auto tc = registry->getComponent(node->entity); + if (parent_changed) { + tc->updateParentMatrix(parentMatrix); } - for (const auto &child : node->children) { - updateNode(child, newParentMatrix, parent_changed); + + auto it = m_transformVersions.find(node->entity); + if (it == m_transformVersions.end() || it->second != tc->getVersion()) { + parent_changed = true; + m_transformVersions[node->entity] = tc->getVersion(); } - }; - updateNode(root, Eigen::Matrix4f::Identity(), false); + newParentMatrix = tc->getWorldMatrix(); + } + for (const auto &child : node->children) { + updateNode(child.get(), newParentMatrix, parent_changed); + } } } // namespace ICE \ No newline at end of file diff --git a/ICE/Util/include/Profiler.h b/ICE/Util/include/Profiler.h new file mode 100644 index 00000000..72b60256 --- /dev/null +++ b/ICE/Util/include/Profiler.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include + +namespace ICE { + +// Minimal per-frame CPU profiler. Scoped timers accumulate wall-clock time per named +// section; beginFrame() snapshots the accumulated results and starts a fresh frame, so +// lastFrame() always exposes a complete, stable set of timings (in milliseconds) for a UI +// or logging to read. Single-threaded (the engine's main loop); not synchronized. +class Profiler { + public: + static Profiler& get() { + static Profiler instance; + return instance; + } + + void beginFrame() { + m_last = std::move(m_current); + m_current.clear(); + } + + void addSample(const std::string& name, double ms) { m_current[name] += ms; } + + const std::unordered_map& lastFrame() const { return m_last; } + + private: + std::unordered_map m_current; + std::unordered_map m_last; +}; + +// RAII: records the elapsed time of its scope into the Profiler under `name`. +class ScopedCPUTimer { + public: + explicit ScopedCPUTimer(std::string name) : m_name(std::move(name)), m_start(std::chrono::steady_clock::now()) {} + ~ScopedCPUTimer() { + const double ms = std::chrono::duration(std::chrono::steady_clock::now() - m_start).count(); + Profiler::get().addSample(m_name, ms); + } + + ScopedCPUTimer(const ScopedCPUTimer&) = delete; + ScopedCPUTimer& operator=(const ScopedCPUTimer&) = delete; + + private: + std::string m_name; + std::chrono::steady_clock::time_point m_start; +}; + +#define ICE_PROFILE_CONCAT_(a, b) a##b +#define ICE_PROFILE_CONCAT(a, b) ICE_PROFILE_CONCAT_(a, b) +// Times the enclosing scope under `name` (a string literal or std::string). +#define ICE_PROFILE_SCOPE(name) ICE::ScopedCPUTimer ICE_PROFILE_CONCAT(ice_scoped_timer_, __LINE__)(name) + +} // namespace ICE