From 4190ff847bdbe7a2bbf56163400afddd0079f771 Mon Sep 17 00:00:00 2001 From: Caleb Burke Date: Wed, 9 Sep 2026 11:36:07 -0700 Subject: [PATCH 1/9] asset_manager concept --- engine/include/sophia/application.hpp | 11 + engine/include/sophia/components.hpp | 35 +++ engine/include/sophia/types.hpp | 2 + engine/src/application.cpp | 41 +++- engine/src/assets/asset_manager.cpp | 24 ++ engine/src/assets/asset_manager.hpp | 41 ++++ engine/src/assets/pools/mesh_pool.cpp | 41 ++++ engine/src/assets/pools/mesh_pool.hpp | 30 +++ engine/src/scene.cpp | 17 ++ engine/src/scene.hpp | 16 ++ engine/src/systems/model_render_system.cpp | 16 +- engine/src/systems/model_render_system.hpp | 12 +- examples/boids_2d/CMakeLists.txt | 3 + examples/boids_2d/main.cpp | 248 +++++++++++++++++++++ 14 files changed, 525 insertions(+), 12 deletions(-) create mode 100644 engine/include/sophia/components.hpp create mode 100644 engine/src/assets/asset_manager.cpp create mode 100644 engine/src/assets/asset_manager.hpp create mode 100644 engine/src/assets/pools/mesh_pool.cpp create mode 100644 engine/src/assets/pools/mesh_pool.hpp create mode 100644 engine/src/scene.cpp create mode 100644 engine/src/scene.hpp create mode 100644 examples/boids_2d/CMakeLists.txt create mode 100644 examples/boids_2d/main.cpp diff --git a/engine/include/sophia/application.hpp b/engine/include/sophia/application.hpp index 89d7e29..31a89a6 100644 --- a/engine/include/sophia/application.hpp +++ b/engine/include/sophia/application.hpp @@ -10,6 +10,10 @@ #include #include +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEPTH_ZERO_TO_ONE +#include + namespace sophia { class Application { @@ -40,6 +44,13 @@ class Application { Camera2D camera; std::vector args; + // Creates a mesh that persists across frames, for callers that want to + // draw the same geometry many times with different transforms (e.g. + // instancing) instead of resubmitting vertices every frame via Canvas. + MeshHandle createMesh(std::vector vertices, + std::vector indices); + void drawMesh(MeshHandle handle, const glm::mat4& transform); + private: friend class Canvas; diff --git a/engine/include/sophia/components.hpp b/engine/include/sophia/components.hpp new file mode 100644 index 0000000..61b9ac3 --- /dev/null +++ b/engine/include/sophia/components.hpp @@ -0,0 +1,35 @@ +#pragma once + +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEPTH_ZERO_TO_ONE +#include +#include +#include +#include + +namespace sophia { + +struct TagComponent { + std::string tag; // entt::hashed_string ? +}; + +struct TransformComponent { + glm::vec3 position{0.f, 0.f, 0.f}; + glm::quat rotation{1.f, 0.f, 0.f, 0.f}; + glm::vec3 scale{1.f, 1.f, 1.f}; + + glm::mat4 mat4() const { + return glm::translate(glm::mat4{1.f}, position) * glm::mat4_cast(rotation) * + glm::scale(glm::mat4{1.f}, scale); + } + + glm::mat3 normalMatrix() const { + return glm::mat3{glm::transpose(glm::inverse(mat4()))}; + } +}; + +struct MeshComponent { + MeshHandle handle; +}; + +} // namespace sophia diff --git a/engine/include/sophia/types.hpp b/engine/include/sophia/types.hpp index 83fc851..f121532 100644 --- a/engine/include/sophia/types.hpp +++ b/engine/include/sophia/types.hpp @@ -17,4 +17,6 @@ using s64 = int64_t; using f32 = float; using f64 = double; +using MeshHandle = u32; + } // namespace sophia diff --git a/engine/src/application.cpp b/engine/src/application.cpp index 85275c7..3fee77b 100644 --- a/engine/src/application.cpp +++ b/engine/src/application.cpp @@ -183,7 +183,16 @@ class Application::Impl { commandBuffer.setViewport(0, viewport); commandBuffer.setScissor(0, scissor); - modelRenderSystem.render(commandBuffer, this->meshes, frameInfo); + std::vector instances; + instances.reserve(this->meshes.size() + this->drawCalls.size()); + for (const std::unique_ptr& mesh : this->meshes) { + instances.push_back({mesh.get(), glm::mat4{1.f}}); + } + instances.insert(instances.end(), this->drawCalls.begin(), + this->drawCalls.end()); + + modelRenderSystem.render(commandBuffer, instances, frameInfo); + this->drawCalls.clear(); commandBuffer.endRendering(); @@ -224,6 +233,25 @@ class Application::Impl { this->meshes.clear(); } + MeshHandle createMesh(std::vector vertices, + std::vector indices) { + Mesh::Builder builder{}; + builder.vertices.reserve(vertices.size()); + for (const CanvasVertex& vertex : vertices) { + builder.vertices.push_back({vertex.position, vertex.color}); + } + builder.indices = std::move(indices); + + this->persistentMeshes.push_back( + std::make_unique(this->device, builder)); + return static_cast(this->persistentMeshes.size() - 1); + } + + void drawMesh(MeshHandle handle, const glm::mat4& transform) { + this->drawCalls.push_back( + {this->persistentMeshes[handle].get(), transform}); + } + private: Application& application; const Application::Config config; @@ -232,6 +260,8 @@ class Application::Impl { RenderContext renderContext; std::vector> meshes; + std::vector> persistentMeshes; + std::vector drawCalls; }; void Application::setCommandLineArgs(int argc, const char* const* argv) { @@ -254,4 +284,13 @@ void Application::pushMesh(std::vector vertices, void Application::clearMeshes() { impl->clearMeshes(); } +MeshHandle Application::createMesh(std::vector vertices, + std::vector indices) { + return impl->createMesh(std::move(vertices), std::move(indices)); +} + +void Application::drawMesh(MeshHandle handle, const glm::mat4& transform) { + impl->drawMesh(handle, transform); +} + } // namespace sophia diff --git a/engine/src/assets/asset_manager.cpp b/engine/src/assets/asset_manager.cpp new file mode 100644 index 0000000..7ec4d74 --- /dev/null +++ b/engine/src/assets/asset_manager.cpp @@ -0,0 +1,24 @@ +#include "assets/asset_manager.hpp" + +#include "mesh.hpp" + +namespace sophia { + +AssetManager::AssetManager(Device& device) : device{device}, meshPool{device} {} + +template <> +MeshHandle AssetManager::create(const Mesh::Builder& builder) { + return meshPool.create(builder); +} + +template <> +const Mesh& AssetManager::get(MeshHandle handle) const { + return meshPool.get(handle); +} + +template <> +void AssetManager::release(MeshHandle handle) { + meshPool.release(handle); +} + +} // namespace sophia diff --git a/engine/src/assets/asset_manager.hpp b/engine/src/assets/asset_manager.hpp new file mode 100644 index 0000000..91abc30 --- /dev/null +++ b/engine/src/assets/asset_manager.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include "device.hpp" +#include "pools/mesh_pool.hpp" + +namespace sophia { + +class AssetManager { + public: + explicit AssetManager(Device& device); + ~AssetManager() = default; + + AssetManager(const AssetManager&) = delete; + AssetManager& operator=(const AssetManager&) = delete; + + template + using HandleFor = typename T::HandleType; + + template + HandleFor load(const std::string& path); + + template + HandleFor create(const typename T::Builder& builder); + + template + const T& get(HandleFor handle) const; + + template + void release(HandleFor handle); + + private: + Device& device; + + MeshPool meshPool; + // TexturePool texturePool; + // MaterialPool materialPool; +}; + +} // namespace sophia diff --git a/engine/src/assets/pools/mesh_pool.cpp b/engine/src/assets/pools/mesh_pool.cpp new file mode 100644 index 0000000..c7c4010 --- /dev/null +++ b/engine/src/assets/pools/mesh_pool.cpp @@ -0,0 +1,41 @@ +#include "assets/pools/mesh_pool.hpp" + +#include + +namespace sophia { + +MeshPool::MeshPool(Device& device) : device{device} {} + +MeshPool::~MeshPool() = default; + +MeshHandle MeshPool::create(const Mesh::Builder& builder) { + auto mesh = std::make_unique(device, builder); + + if (!freeList.empty()) { + MeshHandle handle = freeList.back(); + freeList.pop_back(); + meshes[handle] = std::move(mesh); + return handle; + } + + meshes.push_back(std::move(mesh)); + return static_cast(meshes.size() - 1); +} + +const Mesh& MeshPool::get(MeshHandle handle) const { + SOPHIA_ASSERT(handle < meshes.size(), "invalid MeshHandle:", handle); + SOPHIA_ASSERT(meshes[handle] != nullptr, + "MeshHandle refers to a released mesh:", handle); + return *meshes[handle]; +} + +void MeshPool::release(MeshHandle handle) { + SOPHIA_ASSERT(handle < meshes.size(), "invalid MeshHandle:", handle); + SOPHIA_ASSERT(meshes[handle] != nullptr, + "MeshHandle already released:", handle); + + meshes[handle].reset(); + freeList.push_back(handle); +} + +} // namespace sophia diff --git a/engine/src/assets/pools/mesh_pool.hpp b/engine/src/assets/pools/mesh_pool.hpp new file mode 100644 index 0000000..09845e5 --- /dev/null +++ b/engine/src/assets/pools/mesh_pool.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include "device.hpp" +#include "mesh.hpp" + +namespace sophia { + +class MeshPool { + public: + explicit MeshPool(Device& device); + ~MeshPool(); + + MeshPool(const MeshPool&) = delete; + MeshPool& operator=(const MeshPool&) = delete; + + MeshHandle create(const Mesh::Builder& builder); + const Mesh& get(MeshHandle handle) const; + void release(MeshHandle handle); + + private: + Device& device; + + std::vector> meshes; + std::vector freeList; +}; + +} // namespace sophia diff --git a/engine/src/scene.cpp b/engine/src/scene.cpp new file mode 100644 index 0000000..865868f --- /dev/null +++ b/engine/src/scene.cpp @@ -0,0 +1,17 @@ +#include "scene.hpp" + +#include +#include + +namespace sophia { + +Scene::Scene() { + entt::entity entity = this->registry.create(); + + auto& a = this->registry.emplace(entity); + auto& b = this->registry.get(entity); +} + +Scene::~Scene() { log::trace("destroying sophia::Scene"); } + +} // namespace sophia diff --git a/engine/src/scene.hpp b/engine/src/scene.hpp new file mode 100644 index 0000000..ed40054 --- /dev/null +++ b/engine/src/scene.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace sophia { + +class Scene { + public: + Scene(); + ~Scene(); + + private: + entt::registry registry; +}; + +} // namespace sophia diff --git a/engine/src/systems/model_render_system.cpp b/engine/src/systems/model_render_system.cpp index b6b7e8a..699602c 100644 --- a/engine/src/systems/model_render_system.cpp +++ b/engine/src/systems/model_render_system.cpp @@ -1,6 +1,6 @@ #include "model_render_system.hpp" -#include +#include #include "shaders/model_render_system.frag.spv.h" #include "shaders/model_render_system.vert.spv.h" @@ -12,8 +12,8 @@ ModelRenderSystem::ModelRenderSystem(Device& device, : device{device}, renderContext{renderContext} { createPipelineLayout(); - assert(this->pipelineLayout.get() != nullptr && - "Cannot create pipeline before pipeline layout"); + SOPHIA_ASSERT(this->pipelineLayout.get() != nullptr, + "Cannot create pipeline before pipeline layout"); auto vertexShader = Shader::Builder() .fromSPIRV(shaders::model_render_system_vert_spv) @@ -42,23 +42,23 @@ ModelRenderSystem::~ModelRenderSystem() { } void ModelRenderSystem::render(vk::CommandBuffer commandBuffer, - std::span> meshes, + std::span instances, const FrameInfo& frameInfo) { glm::mat4 viewProjection = frameInfo.camera.getViewProjection(); this->graphicsPipeline->bind(commandBuffer); - for (const std::unique_ptr& mesh : meshes) { + for (const MeshInstance& instance : instances) { PushConstantData push{}; - push.modelMatrix = viewProjection; + push.transform = viewProjection * instance.transform; commandBuffer.pushConstants( this->pipelineLayout.get(), vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, 0, sizeof(PushConstantData), &push); - mesh->bind(commandBuffer); - mesh->draw(commandBuffer); + instance.mesh->bind(commandBuffer); + instance.mesh->draw(commandBuffer); } } diff --git a/engine/src/systems/model_render_system.hpp b/engine/src/systems/model_render_system.hpp index 4d0e4be..93d70b8 100644 --- a/engine/src/systems/model_render_system.hpp +++ b/engine/src/systems/model_render_system.hpp @@ -13,9 +13,15 @@ namespace sophia { class ModelRenderSystem { public: + // Layout must match the shader's `Push` block in model_render_system.vert. struct PushConstantData { - glm::mat4 modelMatrix{1.f}; - glm::mat4 normalMatrix{1.f}; + glm::mat4 transform{1.f}; + glm::vec4 color{1.f}; + }; + + struct MeshInstance { + const Mesh* mesh; + glm::mat4 transform{1.f}; }; ModelRenderSystem(const ModelRenderSystem&) = delete; @@ -25,7 +31,7 @@ class ModelRenderSystem { ~ModelRenderSystem(); void render(vk::CommandBuffer commandBuffer, - std::span> meshes, + std::span instances, const FrameInfo& frameInfo); private: diff --git a/examples/boids_2d/CMakeLists.txt b/examples/boids_2d/CMakeLists.txt new file mode 100644 index 0000000..dce21a0 --- /dev/null +++ b/examples/boids_2d/CMakeLists.txt @@ -0,0 +1,3 @@ +add_executable(boids_2d main.cpp) + +target_link_libraries(boids_2d PRIVATE ${PROJECT_NAME}) diff --git a/examples/boids_2d/main.cpp b/examples/boids_2d/main.cpp new file mode 100644 index 0000000..3d89b12 --- /dev/null +++ b/examples/boids_2d/main.cpp @@ -0,0 +1,248 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEPTH_ZERO_TO_ONE +#include +#include +#include + +struct Boid { + glm::vec2 position; + glm::vec2 velocity; +}; + +constexpr uint32_t DEFAULT_NUMBER_OF_BOIDS = 25; + +class Boids2D : public sophia::Application { + public: + static constexpr float SCROLL_SPEED = 200.0f; + + // World is a fixed box independent of the camera; boids wrap at its edges. + static constexpr float WORLD_HALF_WIDTH = 1000.0f; + static constexpr float WORLD_HALF_HEIGHT = 1000.0f; + + static constexpr float MAX_SPEED = 120.0f; + static constexpr float MAX_FORCE = 200.0f; + static constexpr float PERCEPTION_RADIUS = 150.0f; + static constexpr float SEPARATION_RADIUS = 40.0f; + static constexpr float ALIGNMENT_WEIGHT = 1.0f; + static constexpr float COHESION_WEIGHT = 1.0f; + static constexpr float SEPARATION_WEIGHT = 1.5f; + + static constexpr float BOID_LENGTH = 20.0f; + static constexpr float BOID_WIDTH = 12.0f; + + // Neighbor lookups use a uniform grid instead of an O(n^2) scan. Cell size + // equals PERCEPTION_RADIUS so that a boid's neighbors can only ever fall in + // its own cell or the 8 cells surrounding it. + static constexpr float CELL_SIZE = PERCEPTION_RADIUS; + static constexpr int GRID_WIDTH = + static_cast(2.0f * WORLD_HALF_WIDTH / CELL_SIZE) + 1; + static constexpr int GRID_HEIGHT = + static_cast(2.0f * WORLD_HALF_HEIGHT / CELL_SIZE) + 1; + + Boids2D() : Application{{.width = 750, .height = 1000, .name = "Boids2D"}} { + uint32_t numberOfBoids = DEFAULT_NUMBER_OF_BOIDS; + if (args.size() > 1) { + numberOfBoids = static_cast(std::stoul(args[1])); + } + boids.resize(numberOfBoids); + + sophia::EventSystem::get().addListener( + [this](sophia::KeyPressedEvent& e) { + setScrollKeyHeld(e.getKeyCode(), true); + }); + sophia::EventSystem::get().addListener( + [this](sophia::KeyReleasedEvent& e) { + setScrollKeyHeld(e.getKeyCode(), false); + }); + } + + void onCreate() override { + std::cout << "Boids2D created" << std::endl; + + std::mt19937 rng{std::random_device{}()}; + std::uniform_real_distribution posX{-WORLD_HALF_WIDTH, + WORLD_HALF_WIDTH}; + std::uniform_real_distribution posY{-WORLD_HALF_HEIGHT, + WORLD_HALF_HEIGHT}; + std::uniform_real_distribution angle{0.0f, glm::two_pi()}; + + for (Boid& boid : boids) { + boid.position = {posX(rng), posY(rng)}; + float theta = angle(rng); + boid.velocity = glm::vec2{std::cos(theta), std::sin(theta)} * MAX_SPEED; + } + + // Boid shape in local space, nose pointing along +x; each boid is drawn + // as an instance of this one mesh with a per-boid transform. + std::vector vertices{ + {{BOID_LENGTH * 0.5f, 0.0f}, {1.0f, 1.0f, 1.0f}}, + {{-BOID_LENGTH * 0.5f, BOID_WIDTH * 0.5f}, {1.0f, 1.0f, 1.0f}}, + {{-BOID_LENGTH * 0.5f, -BOID_WIDTH * 0.5f}, {1.0f, 1.0f, 1.0f}}, + }; + boidMesh = createMesh(std::move(vertices), {}); + } + + void onUpdate(const sophia::FrameInfo& frameInfo) override { + float scrollDistance = + SCROLL_SPEED * static_cast(frameInfo.deltaTime); + if (scrollLeft) camera.position.x -= scrollDistance; + if (scrollRight) camera.position.x += scrollDistance; + if (scrollUp) camera.position.y -= scrollDistance; + if (scrollDown) camera.position.y += scrollDistance; + + updateBoids(static_cast(frameInfo.deltaTime)); + + for (const Boid& boid : boids) { + drawBoid(boid); + } + } + + private: + // Maps a world position to its grid cell, clamped to the grid bounds. + glm::ivec2 cellOf(glm::vec2 position) const { + int cx = static_cast((position.x + WORLD_HALF_WIDTH) / CELL_SIZE); + int cy = static_cast((position.y + WORLD_HALF_HEIGHT) / CELL_SIZE); + return {std::clamp(cx, 0, GRID_WIDTH - 1), + std::clamp(cy, 0, GRID_HEIGHT - 1)}; + } + + void rebuildGrid() { + for (std::vector& cell : grid) cell.clear(); + + for (uint32_t i = 0; i < boids.size(); ++i) { + glm::ivec2 cell = cellOf(boids[i].position); + grid[cell.y * GRID_WIDTH + cell.x].push_back(i); + } + } + + void updateBoids(float deltaTime) { + rebuildGrid(); + + std::vector accelerations(boids.size()); + + for (size_t i = 0; i < boids.size(); ++i) { + glm::vec2 separation{0.0f}; + glm::vec2 averageVelocity{0.0f}; + glm::vec2 averagePosition{0.0f}; + int neighborCount = 0; + + glm::ivec2 cell = cellOf(boids[i].position); + + for (int cy = std::max(cell.y - 1, 0); + cy <= std::min(cell.y + 1, GRID_HEIGHT - 1); ++cy) { + for (int cx = std::max(cell.x - 1, 0); + cx <= std::min(cell.x + 1, GRID_WIDTH - 1); ++cx) { + for (uint32_t j : grid[cy * GRID_WIDTH + cx]) { + if (j == i) continue; + + glm::vec2 offset = boids[i].position - boids[j].position; + float dist = glm::length(offset); + if (dist >= PERCEPTION_RADIUS) continue; + + averageVelocity += boids[j].velocity; + averagePosition += boids[j].position; + ++neighborCount; + + if (dist > 0.0f && dist < SEPARATION_RADIUS) { + separation += offset / (dist * dist); + } + } + } + } + + glm::vec2 acceleration = + steerTowards(boids[i], separation) * SEPARATION_WEIGHT; + + if (neighborCount > 0) { + averageVelocity /= static_cast(neighborCount); + acceleration += + steerTowards(boids[i], averageVelocity) * ALIGNMENT_WEIGHT; + + averagePosition /= static_cast(neighborCount); + glm::vec2 towardsGroup = averagePosition - boids[i].position; + acceleration += steerTowards(boids[i], towardsGroup) * COHESION_WEIGHT; + } + + accelerations[i] = acceleration; + } + + for (size_t i = 0; i < boids.size(); ++i) { + Boid& boid = boids[i]; + boid.velocity += accelerations[i] * deltaTime; + + float speed = glm::length(boid.velocity); + if (speed > MAX_SPEED) { + boid.velocity = boid.velocity / speed * MAX_SPEED; + } + + boid.position += boid.velocity * deltaTime; + wrapPosition(boid.position); + } + } + + // Steers boid towards desiredDirection at up to MAX_SPEED, clamped to + // MAX_FORCE. + glm::vec2 steerTowards(const Boid& boid, glm::vec2 desiredDirection) { + float directionLength = glm::length(desiredDirection); + if (directionLength < 1e-5f) return glm::vec2{0.0f}; + + glm::vec2 desired = desiredDirection / directionLength * MAX_SPEED; + glm::vec2 steer = desired - boid.velocity; + + float steerLength = glm::length(steer); + if (steerLength > MAX_FORCE) { + steer = steer / steerLength * MAX_FORCE; + } + return steer; + } + + void wrapPosition(glm::vec2& position) { + if (position.x > WORLD_HALF_WIDTH) position.x = -WORLD_HALF_WIDTH; + if (position.x < -WORLD_HALF_WIDTH) position.x = WORLD_HALF_WIDTH; + if (position.y > WORLD_HALF_HEIGHT) position.y = -WORLD_HALF_HEIGHT; + if (position.y < -WORLD_HALF_HEIGHT) position.y = WORLD_HALF_HEIGHT; + } + + void drawBoid(const Boid& boid) { + glm::vec2 direction = boid.velocity; + float speed = glm::length(direction); + direction = speed > 1e-5f ? direction / speed : glm::vec2{0.0f, -1.0f}; + float angle = std::atan2(direction.y, direction.x); + + glm::mat4 transform = + glm::translate(glm::mat4{1.f}, glm::vec3(boid.position, 0.0f)) * + glm::rotate(glm::mat4{1.f}, angle, glm::vec3(0.0f, 0.0f, 1.0f)); + + drawMesh(boidMesh, transform); + } + + void setScrollKeyHeld(sophia::KeyCode keycode, bool held) { + if (keycode == sophia::Key::Left) scrollLeft = held; + if (keycode == sophia::Key::Right) scrollRight = held; + if (keycode == sophia::Key::Up) scrollUp = held; + if (keycode == sophia::Key::Down) scrollDown = held; + } + + std::vector boids; + std::vector> grid{ + static_cast(GRID_WIDTH * GRID_HEIGHT)}; + sophia::MeshHandle boidMesh{}; + bool scrollLeft = false; + bool scrollRight = false; + bool scrollUp = false; + bool scrollDown = false; +}; + +SOPHIA_MAIN(Boids2D) From 6e2c6c0fc5d76df5e3ff1abc4de20cdd790497ca Mon Sep 17 00:00:00 2001 From: Caleb Burke Date: Sun, 13 Sep 2026 00:52:08 -0700 Subject: [PATCH 2/9] 2d boids example done --- examples/boids_2d/main.cpp | 2 +- testbed/main.cpp | 193 +------------------------------------ 2 files changed, 4 insertions(+), 191 deletions(-) diff --git a/examples/boids_2d/main.cpp b/examples/boids_2d/main.cpp index 3d89b12..1eaba8b 100644 --- a/examples/boids_2d/main.cpp +++ b/examples/boids_2d/main.cpp @@ -91,7 +91,7 @@ class Boids2D : public sophia::Application { {{-BOID_LENGTH * 0.5f, BOID_WIDTH * 0.5f}, {1.0f, 1.0f, 1.0f}}, {{-BOID_LENGTH * 0.5f, -BOID_WIDTH * 0.5f}, {1.0f, 1.0f, 1.0f}}, }; - boidMesh = createMesh(std::move(vertices), {}); + boidMesh = assets().loadMesh(std::move(vertices), {}); } void onUpdate(const sophia::FrameInfo& frameInfo) override { diff --git a/testbed/main.cpp b/testbed/main.cpp index 2f73775..c6047ea 100644 --- a/testbed/main.cpp +++ b/testbed/main.cpp @@ -1,200 +1,13 @@ -#include #include -#include #include -#include -#include -#include -#include -#include - -#define GLM_FORCE_RADIANS -#define GLM_FORCE_DEPTH_ZERO_TO_ONE -#include -#include - -struct Boid { - glm::vec2 position; - glm::vec2 velocity; -}; - -constexpr uint32_t DEFAULT_NUMBER_OF_BOIDS = 25; class Testbed : public sophia::Application { public: - static constexpr float SCROLL_SPEED = 200.0f; + Testbed() : Application{{.width = 750, .height = 1000, .name = "Testbed"}} {} - // World is a fixed box independent of the camera; boids wrap at its edges. - static constexpr float WORLD_HALF_WIDTH = 375.0f; - static constexpr float WORLD_HALF_HEIGHT = 500.0f; + void onCreate() override { std::cout << "Testbed created" << std::endl; } - static constexpr float MAX_SPEED = 120.0f; - static constexpr float MAX_FORCE = 200.0f; - static constexpr float PERCEPTION_RADIUS = 150.0f; - static constexpr float SEPARATION_RADIUS = 40.0f; - static constexpr float ALIGNMENT_WEIGHT = 1.0f; - static constexpr float COHESION_WEIGHT = 1.0f; - static constexpr float SEPARATION_WEIGHT = 1.5f; - - Testbed() : Application{{.width = 750, .height = 1000, .name = "Testbed"}} { - uint32_t numberOfBoids = DEFAULT_NUMBER_OF_BOIDS; - if (args.size() > 1) { - numberOfBoids = static_cast(std::stoul(args[1])); - } - boids.resize(numberOfBoids); - - sophia::EventSystem::get().addListener( - [this](sophia::KeyPressedEvent& e) { - setScrollKeyHeld(e.getKeyCode(), true); - }); - sophia::EventSystem::get().addListener( - [this](sophia::KeyReleasedEvent& e) { - setScrollKeyHeld(e.getKeyCode(), false); - }); - } - - void onCreate() override { - std::cout << "Testbed created" << std::endl; - - std::mt19937 rng{std::random_device{}()}; - std::uniform_real_distribution posX{-WORLD_HALF_WIDTH, - WORLD_HALF_WIDTH}; - std::uniform_real_distribution posY{-WORLD_HALF_HEIGHT, - WORLD_HALF_HEIGHT}; - std::uniform_real_distribution angle{0.0f, glm::two_pi()}; - - for (Boid& boid : boids) { - boid.position = {posX(rng), posY(rng)}; - float theta = angle(rng); - boid.velocity = glm::vec2{std::cos(theta), std::sin(theta)} * MAX_SPEED; - } - } - - void onUpdate(const sophia::FrameInfo& frameInfo) override { - float scrollDistance = - SCROLL_SPEED * static_cast(frameInfo.deltaTime); - if (scrollLeft) camera.position.x -= scrollDistance; - if (scrollRight) camera.position.x += scrollDistance; - if (scrollUp) camera.position.y -= scrollDistance; - if (scrollDown) camera.position.y += scrollDistance; - - updateBoids(static_cast(frameInfo.deltaTime)); - - canvas.clear(); - for (const Boid& boid : boids) { - drawBoid(boid); - } - } - - private: - void updateBoids(float deltaTime) { - std::vector accelerations(boids.size()); - - for (size_t i = 0; i < boids.size(); ++i) { - glm::vec2 separation{0.0f}; - glm::vec2 averageVelocity{0.0f}; - glm::vec2 averagePosition{0.0f}; - int neighborCount = 0; - - for (size_t j = 0; j < boids.size(); ++j) { - if (i == j) continue; - - glm::vec2 offset = boids[i].position - boids[j].position; - float dist = glm::length(offset); - if (dist >= PERCEPTION_RADIUS) continue; - - averageVelocity += boids[j].velocity; - averagePosition += boids[j].position; - ++neighborCount; - - if (dist > 0.0f && dist < SEPARATION_RADIUS) { - separation += offset / (dist * dist); - } - } - - glm::vec2 acceleration = - steerTowards(boids[i], separation) * SEPARATION_WEIGHT; - - if (neighborCount > 0) { - averageVelocity /= static_cast(neighborCount); - acceleration += - steerTowards(boids[i], averageVelocity) * ALIGNMENT_WEIGHT; - - averagePosition /= static_cast(neighborCount); - glm::vec2 towardsGroup = averagePosition - boids[i].position; - acceleration += steerTowards(boids[i], towardsGroup) * COHESION_WEIGHT; - } - - accelerations[i] = acceleration; - } - - for (size_t i = 0; i < boids.size(); ++i) { - Boid& boid = boids[i]; - boid.velocity += accelerations[i] * deltaTime; - - float speed = glm::length(boid.velocity); - if (speed > MAX_SPEED) { - boid.velocity = boid.velocity / speed * MAX_SPEED; - } - - boid.position += boid.velocity * deltaTime; - wrapPosition(boid.position); - } - } - - // Steers boid towards desiredDirection at up to MAX_SPEED, clamped to - // MAX_FORCE. - glm::vec2 steerTowards(const Boid& boid, glm::vec2 desiredDirection) { - float directionLength = glm::length(desiredDirection); - if (directionLength < 1e-5f) return glm::vec2{0.0f}; - - glm::vec2 desired = desiredDirection / directionLength * MAX_SPEED; - glm::vec2 steer = desired - boid.velocity; - - float steerLength = glm::length(steer); - if (steerLength > MAX_FORCE) { - steer = steer / steerLength * MAX_FORCE; - } - return steer; - } - - void wrapPosition(glm::vec2& position) { - if (position.x > WORLD_HALF_WIDTH) position.x = -WORLD_HALF_WIDTH; - if (position.x < -WORLD_HALF_WIDTH) position.x = WORLD_HALF_WIDTH; - if (position.y > WORLD_HALF_HEIGHT) position.y = -WORLD_HALF_HEIGHT; - if (position.y < -WORLD_HALF_HEIGHT) position.y = WORLD_HALF_HEIGHT; - } - - void drawBoid(const Boid& boid) { - constexpr float LENGTH = 20.0f; - constexpr float WIDTH = 12.0f; - - glm::vec2 direction = boid.velocity; - float speed = glm::length(direction); - direction = speed > 1e-5f ? direction / speed : glm::vec2{0.0f, -1.0f}; - glm::vec2 perpendicular{-direction.y, direction.x}; - - glm::vec2 nose = boid.position + direction * (LENGTH * 0.5f); - glm::vec2 tailLeft = boid.position - direction * (LENGTH * 0.5f) + - perpendicular * (WIDTH * 0.5f); - glm::vec2 tailRight = boid.position - direction * (LENGTH * 0.5f) - - perpendicular * (WIDTH * 0.5f); - - canvas.triangle(nose, tailLeft, tailRight, {1.0f, 1.0f, 1.0f}); - } - - void setScrollKeyHeld(sophia::KeyCode keycode, bool held) { - if (keycode == sophia::Key::Left) scrollLeft = held; - if (keycode == sophia::Key::Right) scrollRight = held; - if (keycode == sophia::Key::Up) scrollUp = held; - if (keycode == sophia::Key::Down) scrollDown = held; - } - - std::vector boids; - bool scrollLeft = false; - bool scrollRight = false; - bool scrollUp = false; - bool scrollDown = false; + void onUpdate(const sophia::FrameInfo& frameInfo) override {} }; SOPHIA_MAIN(Testbed) From 12a1e6eca9842ff118d93b03a1d8bf10a4f02ce0 Mon Sep 17 00:00:00 2001 From: Caleb Burke Date: Tue, 15 Sep 2026 02:20:38 -0700 Subject: [PATCH 3/9] AssetRegistry and small changes --- engine/include/sophia/asset_registry.hpp | 64 ++++++++++++++++++++++++ engine/include/sophia/canvas.hpp | 2 +- engine/include/sophia/components.hpp | 4 +- engine/include/sophia/handles.hpp | 33 ++++++++++++ engine/include/sophia/types.hpp | 2 - engine/src/assets/asset_manager.cpp | 24 --------- engine/src/assets/asset_manager.hpp | 41 --------------- engine/src/assets/asset_registry.cpp | 60 ++++++++++++++++++++++ engine/src/mesh.cpp | 2 +- engine/src/mesh.hpp | 1 + engine/src/render_context.cpp | 2 - engine/src/swapchain.cpp | 2 +- 12 files changed, 164 insertions(+), 73 deletions(-) create mode 100644 engine/include/sophia/asset_registry.hpp create mode 100644 engine/include/sophia/handles.hpp delete mode 100644 engine/src/assets/asset_manager.cpp delete mode 100644 engine/src/assets/asset_manager.hpp create mode 100644 engine/src/assets/asset_registry.cpp diff --git a/engine/include/sophia/asset_registry.hpp b/engine/include/sophia/asset_registry.hpp new file mode 100644 index 0000000..4ccf294 --- /dev/null +++ b/engine/include/sophia/asset_registry.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include +#include + +#include "sophia/canvas.hpp" +#include "sophia/types.hpp" + +namespace sophia { + +class Device; +class Mesh; + +template +struct AssetTraits; + +template <> +struct AssetTraits { + using Handle = MeshHandle; +}; + +class AssetRegistry { + public: + ~AssetRegistry(); + + AssetRegistry(const AssetRegistry&) = delete; + AssetRegistry& operator=(const AssetRegistry&) = delete; + + template + typename AssetTraits::Handle load(const std::string& path); + + /** + * TEMPORARY: Remove once load can read real model files from disk. + */ + MeshHandle loadMesh(std::vector vertices, + std::vector indices); + + template + void release(typename AssetTraits::Handle handle); + + private: + friend class Application; + + explicit AssetRegistry(Device& device); + + template + const T& get(typename AssetTraits::Handle handle) const; + + class Impl; + std::unique_ptr impl; +}; + +template <> +MeshHandle AssetRegistry::load(const std::string& path); + +template <> +void AssetRegistry::release(MeshHandle handle); + +template <> +const Mesh& AssetRegistry::get(MeshHandle handle) const; + +} // namespace sophia diff --git a/engine/include/sophia/canvas.hpp b/engine/include/sophia/canvas.hpp index 47d884e..54b6d84 100644 --- a/engine/include/sophia/canvas.hpp +++ b/engine/include/sophia/canvas.hpp @@ -33,7 +33,7 @@ class Canvas { private: friend class Application; - Canvas(Application* application); + explicit Canvas(Application* application); class Impl; std::unique_ptr impl; diff --git a/engine/include/sophia/components.hpp b/engine/include/sophia/components.hpp index 61b9ac3..bcebe53 100644 --- a/engine/include/sophia/components.hpp +++ b/engine/include/sophia/components.hpp @@ -1,11 +1,13 @@ #pragma once +#include +#include + #define GLM_FORCE_RADIANS #define GLM_FORCE_DEPTH_ZERO_TO_ONE #include #include #include -#include namespace sophia { diff --git a/engine/include/sophia/handles.hpp b/engine/include/sophia/handles.hpp new file mode 100644 index 0000000..a668790 --- /dev/null +++ b/engine/include/sophia/handles.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include "sophia/types.hpp" + +namespace sophia { + +template +struct Handle { + static constexpr u32 INVALID_INDEX = (1u << 20) - 1; + + u32 index : 20 = INVALID_INDEX; + u32 generation : 12 = 1; + + [[nodiscard]] static constexpr Handle invalid() { + return Handle{INVALID_INDEX, 0}; + } + + [[nodiscard]] constexpr bool isValid() const { + return index != INVALID_INDEX; + } + + [[nodiscard]] auto operator<=>(const Handle&) const = default; +}; + +struct MeshTag {}; +struct PanelTag {}; + +using MeshHandle = Handle; +using PanelHandle = Handle; + +} // namespace sophia diff --git a/engine/include/sophia/types.hpp b/engine/include/sophia/types.hpp index f121532..83fc851 100644 --- a/engine/include/sophia/types.hpp +++ b/engine/include/sophia/types.hpp @@ -17,6 +17,4 @@ using s64 = int64_t; using f32 = float; using f64 = double; -using MeshHandle = u32; - } // namespace sophia diff --git a/engine/src/assets/asset_manager.cpp b/engine/src/assets/asset_manager.cpp deleted file mode 100644 index 7ec4d74..0000000 --- a/engine/src/assets/asset_manager.cpp +++ /dev/null @@ -1,24 +0,0 @@ -#include "assets/asset_manager.hpp" - -#include "mesh.hpp" - -namespace sophia { - -AssetManager::AssetManager(Device& device) : device{device}, meshPool{device} {} - -template <> -MeshHandle AssetManager::create(const Mesh::Builder& builder) { - return meshPool.create(builder); -} - -template <> -const Mesh& AssetManager::get(MeshHandle handle) const { - return meshPool.get(handle); -} - -template <> -void AssetManager::release(MeshHandle handle) { - meshPool.release(handle); -} - -} // namespace sophia diff --git a/engine/src/assets/asset_manager.hpp b/engine/src/assets/asset_manager.hpp deleted file mode 100644 index 91abc30..0000000 --- a/engine/src/assets/asset_manager.hpp +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -#include - -#include "device.hpp" -#include "pools/mesh_pool.hpp" - -namespace sophia { - -class AssetManager { - public: - explicit AssetManager(Device& device); - ~AssetManager() = default; - - AssetManager(const AssetManager&) = delete; - AssetManager& operator=(const AssetManager&) = delete; - - template - using HandleFor = typename T::HandleType; - - template - HandleFor load(const std::string& path); - - template - HandleFor create(const typename T::Builder& builder); - - template - const T& get(HandleFor handle) const; - - template - void release(HandleFor handle); - - private: - Device& device; - - MeshPool meshPool; - // TexturePool texturePool; - // MaterialPool materialPool; -}; - -} // namespace sophia diff --git a/engine/src/assets/asset_registry.cpp b/engine/src/assets/asset_registry.cpp new file mode 100644 index 0000000..27fcdd9 --- /dev/null +++ b/engine/src/assets/asset_registry.cpp @@ -0,0 +1,60 @@ +#include +#include + +#include "mesh.hpp" +#include "pools/mesh_pool.hpp" + +namespace sophia { + +class AssetRegistry::Impl { + public: + explicit Impl(Device& device) : meshPool{device} {} + + MeshPool meshPool; + // TexturePool texturePool; + // MaterialPool materialPool; +}; + +AssetRegistry::AssetRegistry(Device& device) + : impl{std::make_unique(device)} {} + +AssetRegistry::~AssetRegistry() = default; + +template <> +MeshHandle AssetRegistry::load(const std::string& path) { + (void)path; + SOPHIA_ASSERT(false, + "Mesh::load(path) is not implemented yet - no model file " + "format is supported; build one via Mesh::Builder for now"); + return MeshHandle{}; +} + +template +typename AssetTraits::Handle load(const std::string&) { + static_assert(sizeof(T) == 0, + "load is not implemented for this asset type"); +} + +template <> +void AssetRegistry::release(MeshHandle handle) { + impl->meshPool.release(handle); +} + +template <> +const Mesh& AssetRegistry::get(MeshHandle handle) const { + return impl->meshPool.get(handle); +} + +MeshHandle AssetRegistry::loadMesh(std::vector vertices, + std::vector indices) { + Mesh::Builder builder{}; + builder.vertices.reserve(vertices.size()); + for (const CanvasVertex& vertex : vertices) { + builder.vertices.push_back({vertex.position, vertex.color}); + } + builder.indices = std::move(indices); + + return impl->meshPool.create(builder); +} + +} // namespace sophia diff --git a/engine/src/mesh.cpp b/engine/src/mesh.cpp index 533fefd..b865bad 100644 --- a/engine/src/mesh.cpp +++ b/engine/src/mesh.cpp @@ -36,7 +36,7 @@ Mesh::Mesh(Device& device, const Builder& builder) : device{device} { createIndexBuffers(builder.indices); } -Mesh::~Mesh() {} +Mesh::~Mesh() = default; void Mesh::bind(vk::CommandBuffer commandBuffer) const { vk::Buffer buffers[] = {this->vertexBuffer->getBuffer()}; diff --git a/engine/src/mesh.hpp b/engine/src/mesh.hpp index 880fb09..7ee69a8 100644 --- a/engine/src/mesh.hpp +++ b/engine/src/mesh.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include diff --git a/engine/src/render_context.cpp b/engine/src/render_context.cpp index 9cfe4fd..76296c6 100644 --- a/engine/src/render_context.cpp +++ b/engine/src/render_context.cpp @@ -142,8 +142,6 @@ void RenderContext::recreateSwapchain() { // Old fences referred to images owned by the now-replaced swapchain; // there's nothing valid left to wait on. this->imagesInFlight.assign(this->swapchain->imageCount(), vk::Fence{}); - - log::trace("recreated swapchain"); } void RenderContext::createFrames() { diff --git a/engine/src/swapchain.cpp b/engine/src/swapchain.cpp index a7f49ee..ed07c2e 100644 --- a/engine/src/swapchain.cpp +++ b/engine/src/swapchain.cpp @@ -186,7 +186,7 @@ vk::PresentModeKHR Swapchain::choosePresentMode( bestMode = availablePresentMode; } } - log::warning("Present mode MAILBOX not available."); + log::trace("Present mode MAILBOX not available."); if (bestMode == vk::PresentModeKHR::eImmediate) { log::info("Selected present mode: IMMEDIATE (no vsync)"); From 05fe3c2ee8d9a5cfb44c77debe97e4c266811465 Mon Sep 17 00:00:00 2001 From: Caleb Burke Date: Tue, 15 Sep 2026 02:23:22 -0700 Subject: [PATCH 4/9] changes for ImGui --- engine/include/sophia/application.hpp | 20 +++- engine/include/sophia/ui/debug_ui.hpp | 29 +++++ engine/include/sophia/ui/panel.hpp | 35 ++++++ engine/include/sophia/ui/panel_registry.hpp | 107 ++++++++++++++++++ engine/src/imgui_renderer.cpp | 85 ++++++++++++++ engine/src/imgui_renderer.hpp | 23 ++++ engine/src/render_context.hpp | 3 + engine/src/ui/debug_ui.cpp | 37 ++++++ engine/src/ui/panel_registry.cpp | 88 ++++++++++++++ engine/src/ui/panels/ecs_inspector_panel.cpp | 57 ++++++++++ engine/src/ui/panels/ecs_inspector_panel.hpp | 23 ++++ engine/src/ui/panels/frame_stats_panel.hpp | 44 +++++++ engine/src/ui/panels/panel_registry_panel.cpp | 28 +++++ engine/src/ui/panels/panel_registry_panel.hpp | 20 ++++ 14 files changed, 593 insertions(+), 6 deletions(-) create mode 100644 engine/include/sophia/ui/debug_ui.hpp create mode 100644 engine/include/sophia/ui/panel.hpp create mode 100644 engine/include/sophia/ui/panel_registry.hpp create mode 100644 engine/src/imgui_renderer.cpp create mode 100644 engine/src/imgui_renderer.hpp create mode 100644 engine/src/ui/debug_ui.cpp create mode 100644 engine/src/ui/panel_registry.cpp create mode 100644 engine/src/ui/panels/ecs_inspector_panel.cpp create mode 100644 engine/src/ui/panels/ecs_inspector_panel.hpp create mode 100644 engine/src/ui/panels/frame_stats_panel.hpp create mode 100644 engine/src/ui/panels/panel_registry_panel.cpp create mode 100644 engine/src/ui/panels/panel_registry_panel.hpp diff --git a/engine/include/sophia/application.hpp b/engine/include/sophia/application.hpp index 31a89a6..afdba72 100644 --- a/engine/include/sophia/application.hpp +++ b/engine/include/sophia/application.hpp @@ -3,10 +3,13 @@ #include #include #include +#include #include #include #include #include +#include +#include #include #include @@ -27,7 +30,7 @@ class Application { Application(const Application&) = delete; Application& operator=(const Application&) = delete; - Application(const Config& config); + explicit Application(const Config& config); virtual ~Application(); void run(); @@ -40,15 +43,15 @@ class Application { virtual void onRender() {} virtual void onDestroy() {} + AssetRegistry& assets() { return this->assetRegistry; } + PanelRegistry& panels() { return this->panelRegistry; } + + DebugUI& debugUI() { return this->_debugUI; } + Canvas canvas; Camera2D camera; std::vector args; - // Creates a mesh that persists across frames, for callers that want to - // draw the same geometry many times with different transforms (e.g. - // instancing) instead of resubmitting vertices every frame via Canvas. - MeshHandle createMesh(std::vector vertices, - std::vector indices); void drawMesh(MeshHandle handle, const glm::mat4& transform); private: @@ -59,6 +62,11 @@ class Application { class Impl; std::unique_ptr impl; + + AssetRegistry assetRegistry; + PanelRegistry panelRegistry; + + DebugUI _debugUI; }; } // namespace sophia diff --git a/engine/include/sophia/ui/debug_ui.hpp b/engine/include/sophia/ui/debug_ui.hpp new file mode 100644 index 0000000..45e78fd --- /dev/null +++ b/engine/include/sophia/ui/debug_ui.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +namespace sophia { + +class DebugUI { + public: + DebugUI(const DebugUI&) = delete; + DebugUI& operator=(const DebugUI&) = delete; + + explicit DebugUI(PanelRegistry& registry); + ~DebugUI() = default; + + void show(); + void hide(); + + void showPanelRegistry(bool show = true); + void showFrameStats(bool show = true); + + private: + PanelRegistry& registry; + + PanelHandle panelRegistryHandle; + PanelHandle frameStatsHandle; +}; + +} // namespace sophia diff --git a/engine/include/sophia/ui/panel.hpp b/engine/include/sophia/ui/panel.hpp new file mode 100644 index 0000000..ef81a54 --- /dev/null +++ b/engine/include/sophia/ui/panel.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +namespace sophia { + +class Panel { + public: + Panel(const Panel&) = delete; + Panel& operator=(const Panel&) = delete; + + Panel(Panel&&) noexcept = default; + Panel& operator=(Panel&&) noexcept = default; + + explicit Panel(std::string name = {}) : name{std::move(name)} {} + virtual ~Panel() = default; + + virtual void onUpdate(const FrameInfo& frameInfo) = 0; + + const std::string& getName() const { return name; } + + bool isVisible() const { return this->visible; } + void setVisible(bool visible) { this->visible = visible; } + void toggleVisible() { this->visible = !this->visible; } + + protected: + bool visible = true; + + private: + std::string name; +}; + +} // namespace sophia diff --git a/engine/include/sophia/ui/panel_registry.hpp b/engine/include/sophia/ui/panel_registry.hpp new file mode 100644 index 0000000..9c425ed --- /dev/null +++ b/engine/include/sophia/ui/panel_registry.hpp @@ -0,0 +1,107 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sophia { + +class PanelRegistry { + public: + PanelRegistry(const PanelRegistry&) = delete; + PanelRegistry& operator=(const PanelRegistry&) = delete; + + PanelRegistry() = default; + ~PanelRegistry() = default; + + PanelHandle add(std::unique_ptr panel); + + template + PanelHandle emplace(Args&&... args) { + return add(std::make_unique(std::forward(args)...)); + } + + const Panel& get(PanelHandle handle) const; + Panel& get(PanelHandle handle) { + return const_cast(std::as_const(*this).get(handle)); + } + + template + const T& get(PanelHandle handle) const { + const Panel& panel = get(handle); + SOPHIA_ASSERT(dynamic_cast(&panel) != nullptr, + "PanelHandle does not refer to a", typeid(T).name()); + return static_cast(panel); + } + + template + T& get(PanelHandle handle) { + return const_cast(std::as_const(*this).template get(handle)); + } + + void remove(PanelHandle handle); + bool isValid(PanelHandle handle) const; + + template + const T* find() const { + for (const Slot& slot : slots) { + if (slot.panel) { + if (const T* panel = dynamic_cast(slot.panel.get())) { + return panel; + } + } + } + return nullptr; + } + + template + T* find() { + return const_cast(std::as_const(*this).template find()); + } + + const Panel* find(std::string_view name) const { + for (const Slot& slot : slots) { + if (slot.panel && slot.panel->getName() == name) { + return slot.panel.get(); + } + } + return nullptr; + } + + Panel* find(std::string_view name) { + return const_cast(std::as_const(*this).find(name)); + } + + u32 size() const; + u32 visibleCount() const; + + std::vector names() const { + std::vector result; + result.reserve(slots.size()); + for (const Slot& slot : slots) { + if (slot.panel) { + result.push_back(slot.panel->getName()); + } + } + return result; + } + + void update(const FrameInfo& frameInfo); + + private: + struct Slot { + std::unique_ptr panel; + u32 generation = 0; + }; + + std::vector slots; + std::vector freeList; +}; + +} // namespace sophia diff --git a/engine/src/imgui_renderer.cpp b/engine/src/imgui_renderer.cpp new file mode 100644 index 0000000..f0e2f04 --- /dev/null +++ b/engine/src/imgui_renderer.cpp @@ -0,0 +1,85 @@ +#include "imgui_renderer.hpp" + +#include +#include +#include + +namespace sophia { + +ImGuiRenderer::ImGuiRenderer(Window& window, Device& device, + vk::Format colorAttachmentFormat, + u32 minImageCount) { + // 1. Construct ImGui context + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + + ImGui_ImplGlfw_InitForVulkan(window.getGLFWwindow(), true); + + ImGui_ImplVulkan_InitInfo initInfo = {}; + device.populateImGuiInitInfo(initInfo); + initInfo.DescriptorPoolSize = + IMGUI_IMPL_VULKAN_MINIMUM_SAMPLED_IMAGE_POOL_SIZE + 5; + initInfo.MinImageCount = minImageCount; + initInfo.ImageCount = minImageCount; + + VkFormat rawColorAttachmentFormat = + static_cast(colorAttachmentFormat); + initInfo.UseDynamicRendering = true; + initInfo.PipelineInfoMain.PipelineRenderingCreateInfo.sType = + VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR; + initInfo.PipelineInfoMain.PipelineRenderingCreateInfo.colorAttachmentCount = + 1; + initInfo.PipelineInfoMain.PipelineRenderingCreateInfo + .pColorAttachmentFormats = &rawColorAttachmentFormat; + + ImGui_ImplVulkan_Init(&initInfo); + + // 2. Enable dark theme + ImGui::StyleColorsDark(); + + // 3. Enable docking / keyboard + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + + /** + * When viewports are enabled we tweak WindowRounding/WindowBg so + * platform windows can look identical to regular ones. + */ + ImGuiStyle& style = ImGui::GetStyle(); + if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { + style.WindowRounding = 0.0f; + style.Colors[ImGuiCol_WindowBg].w = 1.0f; + } +} + +ImGuiRenderer::~ImGuiRenderer() { + ImGui_ImplVulkan_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); +} + +void ImGuiRenderer::beginFrame() { + ImGui_ImplVulkan_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + + ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport(), + ImGuiDockNodeFlags_PassthruCentralNode); +} + +void ImGuiRenderer::endFrame(vk::CommandBuffer commandBuffer) { + ImGui::Render(); + ImDrawData* mainDrawData = ImGui::GetDrawData(); + + ImGuiIO& io = ImGui::GetIO(); + if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { + ImGui::UpdatePlatformWindows(); + ImGui::RenderPlatformWindowsDefault(); + } + + ImGui_ImplVulkan_RenderDrawData(mainDrawData, commandBuffer); +} + +} // namespace sophia diff --git a/engine/src/imgui_renderer.hpp b/engine/src/imgui_renderer.hpp new file mode 100644 index 0000000..0016730 --- /dev/null +++ b/engine/src/imgui_renderer.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include "device.hpp" +#include "window.hpp" + +namespace sophia { + +class ImGuiRenderer { + public: + ImGuiRenderer(const ImGuiRenderer&) = delete; + ImGuiRenderer& operator=(const ImGuiRenderer&) = delete; + + ImGuiRenderer(Window& window, Device& device, + vk::Format colorAttachmentFormat, u32 minImageCount); + ~ImGuiRenderer(); + + void beginFrame(); + void endFrame(vk::CommandBuffer commandBuffer); +}; + +} // namespace sophia diff --git a/engine/src/render_context.hpp b/engine/src/render_context.hpp index a7be9cb..34e7ac1 100644 --- a/engine/src/render_context.hpp +++ b/engine/src/render_context.hpp @@ -45,6 +45,9 @@ class RenderContext { vk::Format getImageFormat() const { return this->swapchain->getImageFormat(); } + u32 getImageCount() const { + return static_cast(this->swapchain->imageCount()); + } private: void recreateSwapchain(); diff --git a/engine/src/ui/debug_ui.cpp b/engine/src/ui/debug_ui.cpp new file mode 100644 index 0000000..2803ea4 --- /dev/null +++ b/engine/src/ui/debug_ui.cpp @@ -0,0 +1,37 @@ +#include + +#include "panels/frame_stats_panel.hpp" +#include "panels/panel_registry_panel.hpp" + +namespace sophia { + +DebugUI::DebugUI(PanelRegistry& registry) : registry{registry} { + // #ifndef NDEBUG + this->panelRegistryHandle = registry.emplace(registry); + this->frameStatsHandle = registry.emplace(); + // #endif // !NDEBUG +} + +void DebugUI::show() { + showPanelRegistry(true); + showFrameStats(true); +} + +void DebugUI::hide() { + showPanelRegistry(false); + showFrameStats(false); +} + +void DebugUI::showPanelRegistry(bool show) { + if (registry.isValid(panelRegistryHandle)) { + registry.get(panelRegistryHandle).setVisible(show); + } +} + +void DebugUI::showFrameStats(bool show) { + if (registry.isValid(frameStatsHandle)) { + registry.get(frameStatsHandle).setVisible(show); + } +} + +} // namespace sophia diff --git a/engine/src/ui/panel_registry.cpp b/engine/src/ui/panel_registry.cpp new file mode 100644 index 0000000..92b58d6 --- /dev/null +++ b/engine/src/ui/panel_registry.cpp @@ -0,0 +1,88 @@ +#include +#include + +namespace sophia { + +namespace { +constexpr u32 GENERATION_MASK = (1u << 12) - 1; +} // namespace + +PanelHandle PanelRegistry::add(std::unique_ptr panel) { + if (!freeList.empty()) { + u32 index = freeList.back(); + freeList.pop_back(); + slots[index].panel = std::move(panel); + return PanelHandle{index, slots[index].generation}; + } + + slots.push_back(Slot{std::move(panel), 0}); + return PanelHandle{static_cast(slots.size() - 1), 0}; +} + +const Panel& PanelRegistry::get(PanelHandle handle) const { + SOPHIA_ASSERT(handle.isValid(), "invalid PanelHandle"); + SOPHIA_ASSERT(handle.index < slots.size(), + "PanelHandle index out of range:", handle.index); + + const Slot& slot = slots[handle.index]; + SOPHIA_ASSERT(slot.generation == handle.generation, + "stale PanelHandle (generation mismatch):", handle.index); + SOPHIA_ASSERT(slot.panel != nullptr, + "PanelHandle refers to a removed panel:", handle.index); + return *slot.panel; +} + +void PanelRegistry::remove(PanelHandle handle) { + SOPHIA_ASSERT(handle.isValid(), "invalid PanelHandle"); + SOPHIA_ASSERT(handle.index < slots.size(), + "PanelHandle index out of range:", handle.index); + + Slot& slot = slots[handle.index]; + SOPHIA_ASSERT(slot.generation == handle.generation, + "stale PanelHandle (generation mismatch):", handle.index); + SOPHIA_ASSERT(slot.panel != nullptr, + "PanelHandle already removed:", handle.index); + + slot.panel.reset(); + slot.generation = (slot.generation + 1) & GENERATION_MASK; + freeList.push_back(handle.index); +} + +bool PanelRegistry::isValid(PanelHandle handle) const { + if (!handle.isValid() || handle.index >= slots.size()) { + return false; + } + + const Slot& slot = slots[handle.index]; + return slot.generation == handle.generation && slot.panel != nullptr; +} + +u32 PanelRegistry::size() const { + u32 count = 0; + for (const Slot& slot : slots) { + if (slot.panel) { + count++; + } + } + return count; +} + +u32 PanelRegistry::visibleCount() const { + u32 count = 0; + for (const Slot& slot : slots) { + if (slot.panel && slot.panel->isVisible()) { + count++; + } + } + return count; +} + +void PanelRegistry::update(const FrameInfo& frameInfo) { + for (auto& slot : slots) { + if (slot.panel && slot.panel->isVisible()) { + slot.panel->onUpdate(frameInfo); + } + } +} + +} // namespace sophia diff --git a/engine/src/ui/panels/ecs_inspector_panel.cpp b/engine/src/ui/panels/ecs_inspector_panel.cpp new file mode 100644 index 0000000..49e504e --- /dev/null +++ b/engine/src/ui/panels/ecs_inspector_panel.cpp @@ -0,0 +1,57 @@ +#include "ecs_inspector_panel.hpp" + +#include + +#include + +namespace sophia { + +void EcsInspectorPanel::onUpdate(const FrameInfo&) { + ImGui::Begin("ECS Inspector"); + + entityScratch.clear(); + for (entt::entity entity : registry.storage()) { + entityScratch.push_back(entity); + } + + ImGui::BeginChild("EntityList", ImVec2(0, 200), true); + ImGuiListClipper clipper; + clipper.Begin(static_cast(entityScratch.size())); + while (clipper.Step()) { + for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) { + entt::entity entity = entityScratch[static_cast(i)]; + const TagComponent* tag = registry.try_get(entity); + std::string label = tag ? tag->tag : "Entity"; + + ImGui::PushID(static_cast(entt::to_integral(entity))); + if (ImGui::Selectable(label.c_str(), selected == entity)) { + selected = entity; + } + ImGui::PopID(); + } + } + ImGui::EndChild(); + + ImGui::Separator(); + + if (registry.valid(selected)) { + if (auto* transform = registry.try_get(selected)) { + ImGui::DragFloat3("Position", &transform->position.x, 0.1f); + ImGui::DragFloat3("Scale", &transform->scale.x, 0.1f); + ImGui::Text("Rotation: %.2f, %.2f, %.2f, %.2f", transform->rotation.w, + transform->rotation.x, transform->rotation.y, + transform->rotation.z); + } + + if (auto* mesh = registry.try_get(selected)) { + ImGui::Text("Mesh: index=%u generation=%u", mesh->handle.index, + mesh->handle.generation); + } + } else { + ImGui::TextDisabled("No entity selected"); + } + + ImGui::End(); +} + +} // namespace sophia diff --git a/engine/src/ui/panels/ecs_inspector_panel.hpp b/engine/src/ui/panels/ecs_inspector_panel.hpp new file mode 100644 index 0000000..1666ff5 --- /dev/null +++ b/engine/src/ui/panels/ecs_inspector_panel.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include +#include + +namespace sophia { + +class EcsInspectorPanel : public Panel { + public: + explicit EcsInspectorPanel(entt::registry& registry) + : Panel{"ECS Inspector"}, registry{registry} {} + + void onUpdate(const FrameInfo& frameInfo) override; + + private: + entt::registry& registry; + entt::entity selected = entt::null; + std::vector entityScratch; +}; + +} // namespace sophia diff --git a/engine/src/ui/panels/frame_stats_panel.hpp b/engine/src/ui/panels/frame_stats_panel.hpp new file mode 100644 index 0000000..0d7917f --- /dev/null +++ b/engine/src/ui/panels/frame_stats_panel.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include + +#include +#include + +namespace sophia { + +class FrameStatsPanel : public Panel { + public: + FrameStatsPanel() : Panel{"Frame Stats"} {} + + void onUpdate(const FrameInfo& frameInfo) override { + frameCount++; + accumulatedTime += frameInfo.deltaTime; + + if (frameInfo.elapsedTime >= nextRefreshTime) { + displayedFps = static_cast(frameCount) / accumulatedTime; + displayedFrameTimeMs = + (accumulatedTime / static_cast(frameCount)) * 1000.0; + + frameCount = 0; + accumulatedTime = 0.0; + nextRefreshTime = frameInfo.elapsedTime + REFRESH_INTERVAL; + } + + ImGui::Begin("Frame Stats"); + ImGui::Text("FPS: %.1f", displayedFps); + ImGui::Text("Frame time: %.3f ms", displayedFrameTimeMs); + ImGui::End(); + } + + private: + static constexpr double REFRESH_INTERVAL = 1.0; + + double accumulatedTime = 0.0; + u32 frameCount = 0; + double nextRefreshTime = 0.0; + double displayedFps = 0.0; + double displayedFrameTimeMs = 0.0; +}; + +} // namespace sophia diff --git a/engine/src/ui/panels/panel_registry_panel.cpp b/engine/src/ui/panels/panel_registry_panel.cpp new file mode 100644 index 0000000..c481b5a --- /dev/null +++ b/engine/src/ui/panels/panel_registry_panel.cpp @@ -0,0 +1,28 @@ +#include "panel_registry_panel.hpp" + +#include + +namespace sophia { + +void PanelRegistryPanel::onUpdate(const FrameInfo&) { + ImGui::Begin("PanelRegistry"); + ImGui::Text("%u panels (%u visible)", registry.size(), + registry.visibleCount()); + ImGui::Separator(); + + for (std::string_view name : registry.names()) { + Panel* panel = registry.find(name); + if (!panel || panel == this) { + continue; + } + + bool visible = panel->isVisible(); + if (ImGui::Checkbox(name.data(), &visible)) { + panel->setVisible(visible); + } + } + + ImGui::End(); +} + +} // namespace sophia diff --git a/engine/src/ui/panels/panel_registry_panel.hpp b/engine/src/ui/panels/panel_registry_panel.hpp new file mode 100644 index 0000000..29e16e2 --- /dev/null +++ b/engine/src/ui/panels/panel_registry_panel.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + +namespace sophia { + +class PanelRegistryPanel : public Panel { + public: + explicit PanelRegistryPanel(PanelRegistry& registry) + : Panel{"PanelRegistry"}, registry{registry} {} + + void onUpdate(const FrameInfo& frameInfo) override; + + private: + PanelRegistry& registry; +}; + +} // namespace sophia From c97db37b31d881c14e17318a1d650f6e1377d7d2 Mon Sep 17 00:00:00 2001 From: Caleb Burke Date: Tue, 15 Sep 2026 02:23:41 -0700 Subject: [PATCH 5/9] mesh pool --- engine/src/assets/pools/mesh_pool.cpp | 45 ++++++++++++++++++--------- engine/src/assets/pools/mesh_pool.hpp | 9 ++++-- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/engine/src/assets/pools/mesh_pool.cpp b/engine/src/assets/pools/mesh_pool.cpp index c7c4010..2183ebe 100644 --- a/engine/src/assets/pools/mesh_pool.cpp +++ b/engine/src/assets/pools/mesh_pool.cpp @@ -4,6 +4,10 @@ namespace sophia { +namespace { +constexpr u32 GENERATION_MASK = (1u << 12) - 1; +} // namespace + MeshPool::MeshPool(Device& device) : device{device} {} MeshPool::~MeshPool() = default; @@ -12,30 +16,43 @@ MeshHandle MeshPool::create(const Mesh::Builder& builder) { auto mesh = std::make_unique(device, builder); if (!freeList.empty()) { - MeshHandle handle = freeList.back(); + u32 index = freeList.back(); freeList.pop_back(); - meshes[handle] = std::move(mesh); - return handle; + slots[index].mesh = std::move(mesh); + return MeshHandle{index, slots[index].generation}; } - meshes.push_back(std::move(mesh)); - return static_cast(meshes.size() - 1); + slots.push_back(Slot{std::move(mesh), 0}); + return MeshHandle{static_cast(slots.size() - 1), 0}; } const Mesh& MeshPool::get(MeshHandle handle) const { - SOPHIA_ASSERT(handle < meshes.size(), "invalid MeshHandle:", handle); - SOPHIA_ASSERT(meshes[handle] != nullptr, - "MeshHandle refers to a released mesh:", handle); - return *meshes[handle]; + SOPHIA_ASSERT(handle.isValid(), "invalid MeshHandle"); + SOPHIA_ASSERT(handle.index < slots.size(), + "MeshHandle index out of range:", handle.index); + + const Slot& slot = slots[handle.index]; + SOPHIA_ASSERT(slot.generation == handle.generation, + "stale MeshHandle (generation mismatch):", handle.index); + SOPHIA_ASSERT(slot.mesh != nullptr, + "MeshHandle refers to a released mesh:", handle.index); + return *slot.mesh; } void MeshPool::release(MeshHandle handle) { - SOPHIA_ASSERT(handle < meshes.size(), "invalid MeshHandle:", handle); - SOPHIA_ASSERT(meshes[handle] != nullptr, - "MeshHandle already released:", handle); + SOPHIA_ASSERT(handle.isValid(), "invalid MeshHandle"); + SOPHIA_ASSERT(handle.index < slots.size(), + "MeshHandle index out of range:", handle.index); - meshes[handle].reset(); - freeList.push_back(handle); + Slot& slot = slots[handle.index]; + SOPHIA_ASSERT(slot.generation == handle.generation, + "stale MeshHandle (generation mismatch):", handle.index); + SOPHIA_ASSERT(slot.mesh != nullptr, + "MeshHandle already released:", handle.index); + + slot.mesh.reset(); + slot.generation = (slot.generation + 1) & GENERATION_MASK; + freeList.push_back(handle.index); } } // namespace sophia diff --git a/engine/src/assets/pools/mesh_pool.hpp b/engine/src/assets/pools/mesh_pool.hpp index 09845e5..218f0bb 100644 --- a/engine/src/assets/pools/mesh_pool.hpp +++ b/engine/src/assets/pools/mesh_pool.hpp @@ -21,10 +21,15 @@ class MeshPool { void release(MeshHandle handle); private: + struct Slot { + std::unique_ptr mesh; + u32 generation = 0; + }; + Device& device; - std::vector> meshes; - std::vector freeList; + std::vector slots; + std::vector freeList; }; } // namespace sophia From 34faf8b61e99d7b0ffebb0331dd6a78f86215ca4 Mon Sep 17 00:00:00 2001 From: Caleb Burke Date: Tue, 15 Sep 2026 02:24:17 -0700 Subject: [PATCH 6/9] glossary --- _Design/glossary.md | 329 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 329 insertions(+) create mode 100644 _Design/glossary.md diff --git a/_Design/glossary.md b/_Design/glossary.md new file mode 100644 index 0000000..f69d60c --- /dev/null +++ b/_Design/glossary.md @@ -0,0 +1,329 @@ +# Glossary + +## AssetRegistry + +An AssetRegistry owns GPU-backed assets on the caller's behalf and hands out +lightweight Handles instead of pointers or ownership. + +Internally it's a collection of per-type pools, each responsible for one +asset type: + +``` +AssetRegistry + ├── MeshPool → MeshHandle + ├── TexturePool → TextureHandle (planned) + └── MaterialPool → MaterialHandle (planned) +``` + +A Handle is a small (index, generation) pair. The index locates the slot in +the pool; the generation is bumped when that slot is released, so a Handle +kept around after release() is detected as stale instead of resolving to +someone else's asset (or freed memory). + +Typical lifecycle: + +``` +load(path) / loadMesh(...) → MeshHandle + │ + get(handle) → const Mesh& + │ + release(handle) +``` + +Think: "Where do I get or release a GPU asset by handle, without owning its +lifetime directly?" + +## Buffer + +A Buffer is a linear region of GPU memory used to store arbitrary data. + +Unlike an Image, which is designed for multidimensional image data, a buffer is essentially: + +``` +[ byte byte byte byte byte byte ... ] +``` + +Buffers commonly contain: + +Vertex data +Index data +Uniform data +Storage data +Indirect draw commands + +For example: + +``` +Vertex Buffer +┌─────────────────────────────────┐ +│ Vertex │ Vertex │ Vertex │ ... │ +└─────────────────────────────────┘ +``` + +In Vulkan, VkBuffer / vk::Buffer is a fundamental resource. + +Think: "A linear chunk of GPU-accessible data." + +## Camera + +A Camera defines how the scene is viewed and projected onto the screen. + +It generally contains: + +Position +Orientation +Projection parameters +View matrix +Projection matrix + +For example: + +``` +Camera + ├── Position + ├── Rotation + ├── Projection + │ ├── Perspective + │ └── Orthographic + ├── View Matrix + └── Projection Matrix +``` + +Think: "From where and how am I looking at the scene?" + +## Canvas + +A Canvas is a coordinate/rendering space onto which things are drawn. + +For UI, for example: + +``` +Canvas +┌──────────────────────────────┐ +│ │ +│ ┌──────────────┐ │ +│ │ Panel │ │ +│ └──────────────┘ │ +│ │ +│ Button │ +│ │ +└──────────────────────────────┘ +``` + +A canvas establishes things such as: + +Dimensions +Coordinate system +Origin +Scaling +Potentially clipping + +In a UI system, a Canvas is essentially: + +The space in which UI elements are laid out and rendered. + +## Frame + +Frame is context-dependent, but in a renderer it usually means one iteration of rendering the scene to produce one displayed image. + +## Layer + +A Layer is a logical ordering/grouping of content. + +For example: + +Layer 0: Background +Layer 1: World +Layer 2: Characters +Layer 3: Effects +Layer 4: UI + +Higher layers can be rendered over lower layers. + +In a 2D engine, layers might determine draw order: + +``` +Background + ↓ +Tiles + ↓ +Player + ↓ +Particles + ↓ +UI +``` + +However, Layer is very engine-specific. In a 3D renderer it could instead represent a rendering category, visibility mask, or render-pass grouping. + +## Material + +A Material describes how a surface should be rendered. + +It usually combines: + +Shader/pipeline configuration +Textures +Colors +Numerical parameters + +For example: + +``` +Material + ├── Shader + ├── Albedo Texture + ├── Normal Texture + ├── Roughness + ├── Metallic + └── Base Color +``` + +Then: + +``` +Mesh + Material + Transform + ↓ + Renderer + ↓ + GPU +``` + +A useful conceptual distinction is: + +Mesh = what the object is shaped like. +Material = how that shape should look. + +## Mesh + +A Mesh is the geometric data used to describe the shape of an object. + +Typically contains: + +Vertex positions +Normals +Texture coordinates (UVs) +Tangents +Indices + +For example, a cube mesh contains the vertices and triangles necessary to describe a cube. + +``` +Mesh + ├── Vertices + ├── Indices + └── Vertex attributes + ├── Position + ├── Normal + ├── UV + └── Tangent +``` + +Think: "What shape is this object?" + +## Panel + +A Panel is a rectangular UI region/container. + +For example: + +``` +┌──────────────────────────────┐ +│ Toolbar │ +├───────────┬──────────────────┤ +│ │ │ +│ Scene │ Viewport │ +│ │ │ +├───────────┼──────────────────┤ +│ Inspector │ Console │ +└───────────┴──────────────────┘ +``` + +Each of those regions could be a panel. + +A panel generally has: + +Position +Size +Child elements +UI contents +Potentially its own rendering/context + +This is primarily a UI concept, rather than a GPU concept. + +## PanelRegistry + +A PanelRegistry owns Panels on the caller's behalf and hands out lightweight +PanelHandles instead of pointers or ownership. + +Internally it's a generation-checked slot pool, the same shape AssetRegistry's +per-type pools use: + +``` +PanelRegistry + └── Slot { Panel, generation } → PanelHandle +``` + +Typical lifecycle: + +``` +add(panel) / emplace(...) → PanelHandle + │ + get(handle) / get(handle) → Panel& / T& + │ + remove(handle) +``` + +Each frame, update(frameInfo) calls onUpdate on every visible Panel it holds. + +Think: "Where do I register or look up a UI panel by handle, without owning +its lifetime directly?" + +## Sprite + +A Sprite is a 2D renderable representation of an image, usually a rectangular region of a texture. + +For example: + +``` +Texture Atlas +┌─────────────────────┐ +│ Player │ Enemy │ +│ │ │ +├────────┼────────────┤ +│ Tree │ Rock │ +└─────────────────────┘ +``` + +A sprite might reference only the Player region of that texture. + +A useful engine abstraction might be: + +```cpp +Sprite { + Texture* texture; + Rect uv; +} +``` + +Think: "Which 2D picture/region am I drawing?" + +## Texture + +A Texture is image data intended to be used by a shader. + +It can represent: + +Color/albedo +Normal maps +Roughness +Metallic values +Masks +Height maps +etc. + +A texture doesn't necessarily have to be displayed directly. + +Texture → Shader → Material/Object + +Think: "What image/data does the shader sample?" From 1229f5c0172115b9a4c6e70a08e9736ea4f32cd5 Mon Sep 17 00:00:00 2001 From: Caleb Burke Date: Tue, 15 Sep 2026 02:24:44 -0700 Subject: [PATCH 7/9] test bed testing debugUI --- testbed/main.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/testbed/main.cpp b/testbed/main.cpp index c6047ea..05515d6 100644 --- a/testbed/main.cpp +++ b/testbed/main.cpp @@ -5,7 +5,11 @@ class Testbed : public sophia::Application { public: Testbed() : Application{{.width = 750, .height = 1000, .name = "Testbed"}} {} - void onCreate() override { std::cout << "Testbed created" << std::endl; } + void onCreate() override { + std::cout << "Testbed created" << std::endl; + + debugUI().show(); + } void onUpdate(const sophia::FrameInfo& frameInfo) override {} }; From b16507c61db0510b1edef92711fff50aa0aa8f3f Mon Sep 17 00:00:00 2001 From: Caleb Burke Date: Tue, 15 Sep 2026 02:28:28 -0700 Subject: [PATCH 8/9] application updated --- engine/src/application.cpp | 108 +++++++------------------------------ 1 file changed, 19 insertions(+), 89 deletions(-) diff --git a/engine/src/application.cpp b/engine/src/application.cpp index 3fee77b..f3c7852 100644 --- a/engine/src/application.cpp +++ b/engine/src/application.cpp @@ -10,10 +10,13 @@ #define GLM_FORCE_RADIANS #define GLM_FORCE_DEPTH_ZERO_TO_ONE +#include + #include #include "buffer.hpp" #include "device.hpp" +#include "imgui_renderer.hpp" #include "mesh.hpp" #include "pipelines/graphics_pipeline.hpp" #include "render_context.hpp" @@ -49,72 +52,10 @@ class Application::Impl { device{&window}, renderContext{window, device} {} - /*void run(const std::string& vertShaderPath, - const std::string& fragShaderPath) { - log::info("Running Render Triangle Test..."); - - RenderContext renderContext{window, device}; - - auto vertexShader = Shader::Builder() - .fromGLSL(vertShaderPath) - .setStage(ShaderStage::VERTEX) - .build(); - auto fragmentShader = Shader::Builder() - .fromGLSL(fragShaderPath) - .setStage(ShaderStage::FRAGMENT) - .build(); - - vk::PushConstantRange pushConstantRange{}; - pushConstantRange.stageFlags = - vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment; - pushConstantRange.offset = 0; - pushConstantRange.size = sizeof(PushConstants); - - vk::PipelineLayoutCreateInfo pipelineLayoutInfo{}; - pipelineLayoutInfo.pushConstantRangeCount = 1; - pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange; - - vk::UniquePipelineLayout pipelineLayout = - device.get()->createPipelineLayoutUnique(pipelineLayoutInfo); - - std::vector bindingDescriptions{ - {0, sizeof(Vertex), vk::VertexInputRate::eVertex}}; - std::vector attributeDescriptions{ - {0, 0, vk::Format::eR32G32Sfloat, offsetof(Vertex, position)}, - {1, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, color)}}; - - std::unique_ptr graphicsPipeline = - GraphicsPipeline::Builder(device) - .setVertexShader(*vertexShader) - .setFragmentShader(*fragmentShader) - .setPipelineLayout(pipelineLayout.get()) - .setColorAttachmentFormats({renderContext.getImageFormat()}) - .setVertexInput(bindingDescriptions, attributeDescriptions) - .build(); - - // Oversized triangle covering the full screen so the fragment shader runs - // for every pixel. - std::vector vertices{ - {{-1.0f, -1.0f}, {1.0f, 0.0f, 0.0f}}, - {{3.0f, -1.0f}, {0.0f, 1.0f, 0.0f}}, - {{-1.0f, 3.0f}, {0.0f, 0.0f, 1.0f}}, - }; - - Buffer vertexBuffer{device, sizeof(Vertex), - static_cast(vertices.size()), - vk::BufferUsageFlagBits::eVertexBuffer, - vk::MemoryPropertyFlagBits::eHostVisible | - vk::MemoryPropertyFlagBits::eHostCoherent}; - vertexBuffer.map(); - vertexBuffer.writeToBuffer(vertices.data(), - sizeof(Vertex) * vertices.size()); - vertexBuffer.unmap(); - - - }*/ - void run() { ModelRenderSystem modelRenderSystem{device, renderContext}; + ImGuiRenderer imguiRenderer{window, device, renderContext.getImageFormat(), + renderContext.getImageCount()}; this->application.onCreate(); @@ -139,7 +80,6 @@ class Application::Impl { static_cast(extent.height)), this->application.camera}; this->application.onUpdate(frameInfo); - this->application.onRender(); this->application.camera.setProjection( -frameInfo.currentFramebufferExtent.x / 2.f, frameInfo.currentFramebufferExtent.x / 2.f, @@ -151,6 +91,11 @@ class Application::Impl { continue; } + imguiRenderer.beginFrame(); + + this->application.panelRegistry.update(frameInfo); + this->application.onRender(); + renderContext.transitionCurrentImage( commandBuffer, vk::ImageLayout::eColorAttachmentOptimal); @@ -194,6 +139,8 @@ class Application::Impl { modelRenderSystem.render(commandBuffer, instances, frameInfo); this->drawCalls.clear(); + imguiRenderer.endFrame(commandBuffer); + commandBuffer.endRendering(); renderContext.transitionCurrentImage(commandBuffer, @@ -233,24 +180,11 @@ class Application::Impl { this->meshes.clear(); } - MeshHandle createMesh(std::vector vertices, - std::vector indices) { - Mesh::Builder builder{}; - builder.vertices.reserve(vertices.size()); - for (const CanvasVertex& vertex : vertices) { - builder.vertices.push_back({vertex.position, vertex.color}); - } - builder.indices = std::move(indices); - - this->persistentMeshes.push_back( - std::make_unique(this->device, builder)); - return static_cast(this->persistentMeshes.size() - 1); + void drawMesh(const Mesh& mesh, const glm::mat4& transform) { + this->drawCalls.push_back({&mesh, transform}); } - void drawMesh(MeshHandle handle, const glm::mat4& transform) { - this->drawCalls.push_back( - {this->persistentMeshes[handle].get(), transform}); - } + Device& getDevice() { return device; } private: Application& application; @@ -260,7 +194,6 @@ class Application::Impl { RenderContext renderContext; std::vector> meshes; - std::vector> persistentMeshes; std::vector drawCalls; }; @@ -271,7 +204,9 @@ void Application::setCommandLineArgs(int argc, const char* const* argv) { Application::Application(const Config& config) : impl{std::make_unique(*this, config)}, canvas{this}, - args{std::move(commandLineArgs)} {} + args{std::move(commandLineArgs)}, + assetRegistry{impl->getDevice()}, + _debugUI{this->panelRegistry} {} Application::~Application() = default; @@ -284,13 +219,8 @@ void Application::pushMesh(std::vector vertices, void Application::clearMeshes() { impl->clearMeshes(); } -MeshHandle Application::createMesh(std::vector vertices, - std::vector indices) { - return impl->createMesh(std::move(vertices), std::move(indices)); -} - void Application::drawMesh(MeshHandle handle, const glm::mat4& transform) { - impl->drawMesh(handle, transform); + impl->drawMesh(assetRegistry.get(handle), transform); } } // namespace sophia From a6b98e0613304c7802d9e0a155f5fd0c178babb1 Mon Sep 17 00:00:00 2001 From: Caleb Burke Date: Tue, 15 Sep 2026 02:36:23 -0700 Subject: [PATCH 9/9] Added console concept --- engine/include/sophia/ui/debug_ui.hpp | 2 + engine/src/ui/debug_ui.cpp | 10 ++ engine/src/ui/panels/console_panel.cpp | 136 +++++++++++++++++++++++++ engine/src/ui/panels/console_panel.hpp | 50 +++++++++ 4 files changed, 198 insertions(+) create mode 100644 engine/src/ui/panels/console_panel.cpp create mode 100644 engine/src/ui/panels/console_panel.hpp diff --git a/engine/include/sophia/ui/debug_ui.hpp b/engine/include/sophia/ui/debug_ui.hpp index 45e78fd..09b4c17 100644 --- a/engine/include/sophia/ui/debug_ui.hpp +++ b/engine/include/sophia/ui/debug_ui.hpp @@ -18,12 +18,14 @@ class DebugUI { void showPanelRegistry(bool show = true); void showFrameStats(bool show = true); + void showConsole(bool show = true); private: PanelRegistry& registry; PanelHandle panelRegistryHandle; PanelHandle frameStatsHandle; + PanelHandle consoleHandle; }; } // namespace sophia diff --git a/engine/src/ui/debug_ui.cpp b/engine/src/ui/debug_ui.cpp index 2803ea4..fe3a19a 100644 --- a/engine/src/ui/debug_ui.cpp +++ b/engine/src/ui/debug_ui.cpp @@ -1,5 +1,6 @@ #include +#include "panels/console_panel.hpp" #include "panels/frame_stats_panel.hpp" #include "panels/panel_registry_panel.hpp" @@ -9,17 +10,20 @@ DebugUI::DebugUI(PanelRegistry& registry) : registry{registry} { // #ifndef NDEBUG this->panelRegistryHandle = registry.emplace(registry); this->frameStatsHandle = registry.emplace(); + this->consoleHandle = registry.emplace(); // #endif // !NDEBUG } void DebugUI::show() { showPanelRegistry(true); showFrameStats(true); + showConsole(true); } void DebugUI::hide() { showPanelRegistry(false); showFrameStats(false); + showConsole(false); } void DebugUI::showPanelRegistry(bool show) { @@ -34,4 +38,10 @@ void DebugUI::showFrameStats(bool show) { } } +void DebugUI::showConsole(bool show) { + if (registry.isValid(consoleHandle)) { + registry.get(consoleHandle).setVisible(show); + } +} + } // namespace sophia diff --git a/engine/src/ui/panels/console_panel.cpp b/engine/src/ui/panels/console_panel.cpp new file mode 100644 index 0000000..b9962dc --- /dev/null +++ b/engine/src/ui/panels/console_panel.cpp @@ -0,0 +1,136 @@ +#include "console_panel.hpp" + +#include + +namespace sophia { + +namespace { +constexpr ImVec4 COLOR_DEFAULT = ImVec4(0.85f, 0.85f, 0.85f, 1.0f); +constexpr ImVec4 COLOR_COMMAND = ImVec4(1.0f, 0.78f, 0.37f, 1.0f); +constexpr ImVec4 COLOR_ERROR = ImVec4(1.0f, 0.4f, 0.4f, 1.0f); +constexpr ImVec4 COLOR_INFO = ImVec4(0.4f, 0.7f, 1.0f, 1.0f); +} // namespace + +void ConsolePanel::registerDefaultCommands() { + log("Console ready. Type 'help' for a list of commands."); +} + +void ConsolePanel::log(const std::string& message) { + entries.push_back(Entry{message, COLOR_DEFAULT}); + scrollToBottom = true; +} + +void ConsolePanel::clear() { entries.clear(); } + +void ConsolePanel::execute(const std::string& command) { + entries.push_back(Entry{"> " + command, COLOR_COMMAND}); + scrollToBottom = true; + + historyPos = -1; + history.erase(std::remove(history.begin(), history.end(), command), + history.end()); + history.push_back(command); + + if (command == "help") { + entries.push_back( + Entry{"Available commands: help, clear, history", COLOR_INFO}); + } else if (command == "clear") { + clear(); + } else if (command == "history") { + for (const std::string& past : history) { + entries.push_back(Entry{past, COLOR_DEFAULT}); + } + } else if (commandCallback) { + commandCallback(*this, command); + } else { + entries.push_back(Entry{"Unknown command: '" + command + "'", COLOR_ERROR}); + } +} + +int ConsolePanel::textEditCallback(ImGuiInputTextCallbackData* data) { + if (data->EventFlag != ImGuiInputTextFlags_CallbackHistory) { + return 0; + } + + const int previousPos = historyPos; + if (data->EventKey == ImGuiKey_UpArrow) { + if (historyPos == -1) { + historyPos = static_cast(history.size()) - 1; + } else if (historyPos > 0) { + historyPos--; + } + } else if (data->EventKey == ImGuiKey_DownArrow) { + if (historyPos != -1 && ++historyPos >= static_cast(history.size())) { + historyPos = -1; + } + } + + if (previousPos != historyPos) { + const std::string& historyStr = + historyPos >= 0 ? history[static_cast(historyPos)] : ""; + data->DeleteChars(0, data->BufTextLen); + data->InsertChars(0, historyStr.c_str()); + } + + return 0; +} + +void ConsolePanel::onUpdate(const FrameInfo&) { + ImGui::Begin("Console"); + + if (ImGui::SmallButton("Clear")) { + clear(); + } + ImGui::SameLine(); + ImGui::Checkbox("Auto-scroll", &autoScroll); + + ImGui::Separator(); + + const float footerHeight = + ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing(); + ImGui::BeginChild("ScrollRegion", ImVec2(0, -footerHeight), false, + ImGuiWindowFlags_HorizontalScrollbar); + + for (const Entry& entry : entries) { + ImGui::PushStyleColor(ImGuiCol_Text, entry.color); + ImGui::TextUnformatted(entry.text.c_str()); + ImGui::PopStyleColor(); + } + + if (scrollToBottom || + (autoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY())) { + ImGui::SetScrollHereY(1.0f); + } + scrollToBottom = false; + + ImGui::EndChild(); + ImGui::Separator(); + + ImGuiInputTextFlags inputFlags = ImGuiInputTextFlags_EnterReturnsTrue | + ImGuiInputTextFlags_CallbackHistory; + + bool reclaimFocus = false; + if (ImGui::InputText( + "Input", inputBuffer, sizeof(inputBuffer), inputFlags, + [](ImGuiInputTextCallbackData* data) -> int { + auto* console = static_cast(data->UserData); + return console->textEditCallback(data); + }, + this)) { + std::string command{inputBuffer}; + if (!command.empty()) { + execute(command); + } + inputBuffer[0] = '\0'; + reclaimFocus = true; + } + + ImGui::SetItemDefaultFocus(); + if (reclaimFocus) { + ImGui::SetKeyboardFocusHere(-1); + } + + ImGui::End(); +} + +} // namespace sophia diff --git a/engine/src/ui/panels/console_panel.hpp b/engine/src/ui/panels/console_panel.hpp new file mode 100644 index 0000000..0afb993 --- /dev/null +++ b/engine/src/ui/panels/console_panel.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace sophia { + +class ConsolePanel : public Panel { + public: + using CommandCallback = + std::function; + + ConsolePanel() : Panel{"Console"} { registerDefaultCommands(); } + + void onUpdate(const FrameInfo& frameInfo) override; + + void log(const std::string& message); + void clear(); + + void setCommandCallback(CommandCallback callback) { + this->commandCallback = std::move(callback); + } + + private: + struct Entry { + std::string text; + ImVec4 color; + }; + + void registerDefaultCommands(); + void execute(const std::string& command); + int textEditCallback(struct ImGuiInputTextCallbackData* data); + + std::vector entries; + std::vector history; + int historyPos = -1; + + char inputBuffer[256] = {}; + bool autoScroll = true; + bool scrollToBottom = false; + + CommandCallback commandCallback; +}; + +} // namespace sophia