From 5e1caabde6d9b7a86ea120f869e77294c76067b4 Mon Sep 17 00:00:00 2001 From: Caleb Burke Date: Sun, 20 Sep 2026 16:21:22 -0700 Subject: [PATCH 01/26] Add offscreen render targets and rename AssetRegistry to ResourceRegistry Add a RenderTarget resource so the scene can render offscreen instead of straight to the swapchain. The editor uses it to show the scene inside the Viewport panel. - Rename AssetRegistry to ResourceRegistry (AssetTraits -> ResourceTraits, assets() -> resources()) and move the pools from engine/src/assets to engine/src/resources. It now covers non-asset GPU resources as well. - Replace loadMesh() with create(...), and add create(), requestResize(), getExtent() and isAlive(). - Add RenderTarget and RenderTargetPool with RenderTargetHandle. Resizes are deferred and applied at the start of the next frame, when the GPU is idle. - Add Application::setSceneTarget(). With no target set, the scene still renders to the swapchain. With a target set, the scene renders offscreen and the swapchain receives only the UI. Frame extent and camera projection follow the target's size. - Add sophia::UI (Application::ui()) with image() and getTextureId() to draw a render target in an ImGui window, sizing the target to the available space. ImGuiRenderer now registers the target textures for this. - ViewportPanel takes the UI and a target, and draws the scene image in place of the flat background fill. - Update the testbed, the boids example, and the glossary for the rename. --- _Design/glossary.md | 16 +-- editor/main.cpp | 5 +- editor/panels/viewport_panel.cpp | 3 +- editor/panels/viewport_panel.hpp | 12 +- engine/include/sophia/application.hpp | 18 ++- engine/include/sophia/asset_registry.hpp | 64 --------- engine/include/sophia/components.hpp | 2 +- engine/include/sophia/handles.hpp | 2 + engine/include/sophia/resource_registry.hpp | 103 ++++++++++++++ engine/include/sophia/ui/ui.hpp | 40 ++++++ engine/src/application.cpp | 128 ++++++++++++------ engine/src/assets/asset_registry.cpp | 60 -------- engine/src/image.cpp | 12 ++ engine/src/imgui_renderer.cpp | 51 ++++++- engine/src/imgui_renderer.hpp | 21 ++- engine/src/mesh.hpp | 2 +- engine/src/render_target.cpp | 52 +++++++ engine/src/render_target.hpp | 51 +++++++ .../{assets => resources}/pools/mesh_pool.cpp | 2 +- .../{assets => resources}/pools/mesh_pool.hpp | 0 .../resources/pools/render_target_pool.cpp | 81 +++++++++++ .../resources/pools/render_target_pool.hpp | 44 ++++++ engine/src/resources/resource_registry.cpp | 104 ++++++++++++++ engine/src/ui/ui.cpp | 27 ++++ examples/boids_2d/main.cpp | 2 +- testbed/main.cpp | 30 +++- 26 files changed, 743 insertions(+), 189 deletions(-) delete mode 100644 engine/include/sophia/asset_registry.hpp create mode 100644 engine/include/sophia/resource_registry.hpp create mode 100644 engine/include/sophia/ui/ui.hpp delete mode 100644 engine/src/assets/asset_registry.cpp create mode 100644 engine/src/render_target.cpp create mode 100644 engine/src/render_target.hpp rename engine/src/{assets => resources}/pools/mesh_pool.cpp (97%) rename engine/src/{assets => resources}/pools/mesh_pool.hpp (100%) create mode 100644 engine/src/resources/pools/render_target_pool.cpp create mode 100644 engine/src/resources/pools/render_target_pool.hpp create mode 100644 engine/src/resources/resource_registry.cpp create mode 100644 engine/src/ui/ui.cpp diff --git a/_Design/glossary.md b/_Design/glossary.md index f69d60c..d06b4fd 100644 --- a/_Design/glossary.md +++ b/_Design/glossary.md @@ -1,15 +1,15 @@ # Glossary -## AssetRegistry +## ResourceRegistry -An AssetRegistry owns GPU-backed assets on the caller's behalf and hands out +A ResourceRegistry owns GPU-backed resources 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: +resource type: ``` -AssetRegistry +ResourceRegistry ├── MeshPool → MeshHandle ├── TexturePool → TextureHandle (planned) └── MaterialPool → MaterialHandle (planned) @@ -18,19 +18,19 @@ AssetRegistry 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). +someone else's resource (or freed memory). Typical lifecycle: ``` -load(path) / loadMesh(...) → MeshHandle +load(path) / create(...) → MeshHandle │ get(handle) → const Mesh& │ release(handle) ``` -Think: "Where do I get or release a GPU asset by handle, without owning its +Think: "Where do I get or release a GPU resource by handle, without owning its lifetime directly?" ## Buffer @@ -256,7 +256,7 @@ This is primarily a UI concept, rather than a GPU concept. 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 +Internally it's a generation-checked slot pool, the same shape ResourceRegistry's per-type pools use: ``` diff --git a/editor/main.cpp b/editor/main.cpp index bb66b1f..b673768 100644 --- a/editor/main.cpp +++ b/editor/main.cpp @@ -18,7 +18,10 @@ class Editor : public sophia::Application { panels().emplace(); panels().emplace(); panels().emplace(); - panels().emplace(); + sophia::RenderTargetHandle sceneTarget = + resources().create(); + setSceneTarget(sceneTarget); + panels().emplace(ui(), sceneTarget); panels().setDefaultLayout( {.left = {"Scene Hierarchy"}, diff --git a/editor/panels/viewport_panel.cpp b/editor/panels/viewport_panel.cpp index ef14bed..74ec7b8 100644 --- a/editor/panels/viewport_panel.cpp +++ b/editor/panels/viewport_panel.cpp @@ -24,8 +24,9 @@ void ViewportPanel::onUpdate(const sophia::FrameInfo&) { const ImVec2 size = ImGui::GetContentRegionAvail(); const ImVec2 end(origin.x + size.x, origin.y + size.y); + ui.image(target); + ImDrawList* drawList = ImGui::GetWindowDrawList(); - drawList->AddRectFilled(origin, end, IM_COL32(24, 26, 32, 255)); if (showGrid) { constexpr float GRID_STEP = 40.0f; diff --git a/editor/panels/viewport_panel.hpp b/editor/panels/viewport_panel.hpp index b68205b..d470529 100644 --- a/editor/panels/viewport_panel.hpp +++ b/editor/panels/viewport_panel.hpp @@ -1,15 +1,25 @@ #pragma once #include +#include #include +#include class ViewportPanel : public sophia::Panel { public: - ViewportPanel() : Panel{{.name = "Viewport"}} {} + ViewportPanel(sophia::UI& ui, sophia::RenderTargetHandle target) + : Panel{{.name = "Viewport", + .windowFlags = ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse}}, + ui{ui}, + target{target} {} void onUpdate(const sophia::FrameInfo& frameInfo) override; private: + sophia::UI& ui; + sophia::RenderTargetHandle target; + enum class GizmoMode { Move, Rotate, Scale }; GizmoMode gizmoMode = GizmoMode::Move; diff --git a/engine/include/sophia/application.hpp b/engine/include/sophia/application.hpp index 4870fcb..67360f3 100644 --- a/engine/include/sophia/application.hpp +++ b/engine/include/sophia/application.hpp @@ -3,14 +3,15 @@ #include #include #include -#include #include #include #include #include +#include #include #include #include +#include #include #include @@ -44,8 +45,9 @@ class Application { virtual void onRender() {} virtual void onDestroy() {} - AssetRegistry& assets() { return this->assetRegistry; } + ResourceRegistry& resources() { return this->resourceRegistry; } PanelRegistry& panels() { return this->panelRegistry; } + UI& ui() { return this->_ui; } DebugUI& debugUI() { return this->_debugUI; } @@ -57,6 +59,15 @@ class Application { void drawMesh(MeshHandle handle, const glm::mat4& transform); + /** + * Chooses where the scene is rendered. An invalid handle (the default) means + * the swapchain; a target created with + * ResourceRegistry::create() redirects it offscreen, and the + * swapchain then only receives the UI. FrameInfo::currentFramebufferExtent + * and the camera projection follow the target's size. + */ + void setSceneTarget(RenderTargetHandle handle); + private: friend class Canvas; @@ -66,8 +77,9 @@ class Application { class Impl; std::unique_ptr impl; - AssetRegistry assetRegistry; + ResourceRegistry resourceRegistry; PanelRegistry panelRegistry; + UI _ui; DebugUI _debugUI; }; diff --git a/engine/include/sophia/asset_registry.hpp b/engine/include/sophia/asset_registry.hpp deleted file mode 100644 index 4ccf294..0000000 --- a/engine/include/sophia/asset_registry.hpp +++ /dev/null @@ -1,64 +0,0 @@ -#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/components.hpp b/engine/include/sophia/components.hpp index bcebe53..76c783b 100644 --- a/engine/include/sophia/components.hpp +++ b/engine/include/sophia/components.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #define GLM_FORCE_RADIANS diff --git a/engine/include/sophia/handles.hpp b/engine/include/sophia/handles.hpp index a668790..cd27a6a 100644 --- a/engine/include/sophia/handles.hpp +++ b/engine/include/sophia/handles.hpp @@ -26,8 +26,10 @@ struct Handle { struct MeshTag {}; struct PanelTag {}; +struct RenderTargetTag {}; using MeshHandle = Handle; using PanelHandle = Handle; +using RenderTargetHandle = Handle; } // namespace sophia diff --git a/engine/include/sophia/resource_registry.hpp b/engine/include/sophia/resource_registry.hpp new file mode 100644 index 0000000..c22657c --- /dev/null +++ b/engine/include/sophia/resource_registry.hpp @@ -0,0 +1,103 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "sophia/canvas.hpp" +#include "sophia/types.hpp" + +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEPTH_ZERO_TO_ONE +#include + +namespace sophia { + +class Device; +class Mesh; +class RenderTarget; +class ImGuiRenderer; + +template +struct ResourceTraits; + +template <> +struct ResourceTraits { + using Handle = MeshHandle; +}; + +template <> +struct ResourceTraits { + using Handle = RenderTargetHandle; +}; + +class ResourceRegistry { + public: + ~ResourceRegistry(); + + ResourceRegistry(const ResourceRegistry&) = delete; + ResourceRegistry& operator=(const ResourceRegistry&) = delete; + + template + typename ResourceTraits::Handle load(const std::string& path); + + template + typename ResourceTraits::Handle create(Args&&... args) { + return createResource(std::type_identity{}, std::forward(args)...); + } + + void requestResize(RenderTargetHandle handle, u32 width, u32 height); + + glm::vec2 getExtent(RenderTargetHandle handle) const; + + bool isAlive(RenderTargetHandle handle) const; + + /** + * Waits for the GPU to go idle, so don't call this every frame. Don't release + * the target the scene is currently rendering into. + */ + template + void release(typename ResourceTraits::Handle handle); + + private: + friend class Application; + friend class ImGuiRenderer; + + // `renderTargetFormat` is a VkFormat value; it must match the color format + // the scene pipelines are built against. + ResourceRegistry(Device& device, u32 renderTargetFormat); + + MeshHandle createResource(std::type_identity, + std::vector vertices, + std::vector indices = {}); + + RenderTargetHandle createResource(std::type_identity, + u32 width = 1, u32 height = 1); + + RenderTarget& getRenderTarget(RenderTargetHandle handle); + const RenderTarget& getRenderTarget(RenderTargetHandle handle) const; + void applyPendingResizes(); + + template + const T& get(typename ResourceTraits::Handle handle) const; + + class Impl; + std::unique_ptr impl; +}; + +template <> +MeshHandle ResourceRegistry::load(const std::string& path); + +template <> +void ResourceRegistry::release(MeshHandle handle); + +template <> +void ResourceRegistry::release(RenderTargetHandle handle); + +template <> +const Mesh& ResourceRegistry::get(MeshHandle handle) const; + +} // namespace sophia diff --git a/engine/include/sophia/ui/ui.hpp b/engine/include/sophia/ui/ui.hpp new file mode 100644 index 0000000..17d419a --- /dev/null +++ b/engine/include/sophia/ui/ui.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include + +#include + +namespace sophia { + +class ImGuiRenderer; +class ResourceRegistry; + +class UI { + public: + UI(const UI&) = delete; + UI& operator=(const UI&) = delete; + + /** + * Draws the target filling the space left in the current window, and keeps + * the target sized to match. The target catches up on the next frame, so the + * image may be a frame behind while the window is being resized. + */ + void image(RenderTargetHandle handle); + + /** + * Texture to hand to ImGui::Image() that shows the target's contents. + * Changes when the target is resized, so fetch it each frame rather than + * caching it. + */ + ImTextureID getTextureId(RenderTargetHandle handle); + + private: + friend class Application; + + explicit UI(ResourceRegistry& resources) : resources{resources} {} + + ResourceRegistry& resources; + ImGuiRenderer* renderer = nullptr; +}; + +} // namespace sophia diff --git a/engine/src/application.cpp b/engine/src/application.cpp index b838e7b..ed843e7 100644 --- a/engine/src/application.cpp +++ b/engine/src/application.cpp @@ -20,6 +20,7 @@ #include "mesh.hpp" #include "pipelines/graphics_pipeline.hpp" #include "render_context.hpp" +#include "render_target.hpp" #include "shader.hpp" #include "systems/model_render_system.hpp" #include "window.hpp" @@ -41,6 +42,38 @@ struct PushConstants { std::vector commandLineArgs; +void beginColorPass(vk::CommandBuffer commandBuffer, vk::ImageView view, + vk::Extent2D extent) { + vk::ClearValue clearValue{}; + clearValue.color = + vk::ClearColorValue{std::array{0.01f, 0.01f, 0.02f, 1.0f}}; + + vk::RenderingAttachmentInfo colorAttachment{}; + colorAttachment.imageView = view; + colorAttachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; + colorAttachment.loadOp = vk::AttachmentLoadOp::eClear; + colorAttachment.storeOp = vk::AttachmentStoreOp::eStore; + colorAttachment.clearValue = clearValue; + + vk::RenderingInfo renderingInfo{}; + renderingInfo.renderArea = vk::Rect2D{{0, 0}, extent}; + renderingInfo.layerCount = 1; + renderingInfo.colorAttachmentCount = 1; + renderingInfo.pColorAttachments = &colorAttachment; + + commandBuffer.beginRendering(renderingInfo); + + vk::Viewport viewport{0.0f, + 0.0f, + static_cast(extent.width), + static_cast(extent.height), + 0.0f, + 1.0f}; + vk::Rect2D scissor{{0, 0}, extent}; + commandBuffer.setViewport(0, viewport); + commandBuffer.setScissor(0, scissor); +} + } // namespace class Application::Impl { @@ -55,7 +88,9 @@ class Application::Impl { void run() { ModelRenderSystem modelRenderSystem{device, renderContext}; ImGuiRenderer imguiRenderer{window, device, renderContext.getImageFormat(), - renderContext.getImageCount()}; + renderContext.getImageCount(), + this->application.resourceRegistry}; + this->application._ui.renderer = &imguiRenderer; this->application.consoleCommandRegistry.registerCommand( "reset_layout", "Restore the default panel layout", @@ -83,10 +118,22 @@ class Application::Impl { double elapsedTime = std::chrono::duration(currentTime - startTime).count(); + // Resizing waits for the GPU, so it has to happen before any of this + // frame's work (or ImGui's references to the targets) is recorded. + this->application.resourceRegistry.applyPendingResizes(); + + RenderTarget* sceneTarget = + this->sceneTargetHandle.isValid() + ? &this->application.resourceRegistry.getRenderTarget( + this->sceneTargetHandle) + : nullptr; + vk::Extent2D extent = renderContext.getExtent(); + vk::Extent2D sceneExtent = + sceneTarget ? sceneTarget->getExtent() : extent; FrameInfo frameInfo{frameIndex, elapsedTime, deltaTime, - glm::vec2(static_cast(extent.width), - static_cast(extent.height)), + glm::vec2(static_cast(sceneExtent.width), + static_cast(sceneExtent.height)), this->application.camera}; this->application.onUpdate(frameInfo); this->application.camera.setProjection( @@ -105,38 +152,6 @@ class Application::Impl { this->application.panelRegistry.update(frameInfo); this->application.onRender(); - renderContext.transitionCurrentImage( - commandBuffer, vk::ImageLayout::eColorAttachmentOptimal); - - vk::ClearValue clearValue{}; - clearValue.color = - vk::ClearColorValue{std::array{0.01f, 0.01f, 0.02f, 1.0f}}; - - vk::RenderingAttachmentInfo colorAttachment{}; - colorAttachment.imageView = renderContext.getCurrentImageView(); - colorAttachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; - colorAttachment.loadOp = vk::AttachmentLoadOp::eClear; - colorAttachment.storeOp = vk::AttachmentStoreOp::eStore; - colorAttachment.clearValue = clearValue; - - vk::RenderingInfo renderingInfo{}; - renderingInfo.renderArea = vk::Rect2D{{0, 0}, extent}; - renderingInfo.layerCount = 1; - renderingInfo.colorAttachmentCount = 1; - renderingInfo.pColorAttachments = &colorAttachment; - - commandBuffer.beginRendering(renderingInfo); - - vk::Viewport viewport{0.0f, - 0.0f, - static_cast(extent.width), - static_cast(extent.height), - 0.0f, - 1.0f}; - vk::Rect2D scissor{{0, 0}, extent}; - commandBuffer.setViewport(0, viewport); - commandBuffer.setScissor(0, scissor); - std::vector instances; instances.reserve(this->meshes.size() + this->drawCalls.size()); for (const std::unique_ptr& mesh : this->meshes) { @@ -144,10 +159,28 @@ class Application::Impl { } instances.insert(instances.end(), this->drawCalls.begin(), this->drawCalls.end()); - - modelRenderSystem.render(commandBuffer, instances, frameInfo); this->drawCalls.clear(); + if (sceneTarget) { + Image& targetImage = sceneTarget->getImage(); + targetImage.transitionLayout(commandBuffer, + vk::ImageLayout::eColorAttachmentOptimal); + beginColorPass(commandBuffer, targetImage.getView(), sceneExtent); + modelRenderSystem.render(commandBuffer, instances, frameInfo); + commandBuffer.endRendering(); + targetImage.transitionLayout(commandBuffer, + vk::ImageLayout::eShaderReadOnlyOptimal); + } + + renderContext.transitionCurrentImage( + commandBuffer, vk::ImageLayout::eColorAttachmentOptimal); + beginColorPass(commandBuffer, renderContext.getCurrentImageView(), + extent); + + if (!sceneTarget) { + modelRenderSystem.render(commandBuffer, instances, frameInfo); + } + imguiRenderer.endFrame(commandBuffer); commandBuffer.endRendering(); @@ -164,6 +197,8 @@ class Application::Impl { device.waitIdle(); + this->application._ui.renderer = nullptr; + auto endTime = std::chrono::high_resolution_clock::now(); double totalRuntime = std::chrono::duration(endTime - startTime).count(); @@ -193,6 +228,14 @@ class Application::Impl { this->drawCalls.push_back({&mesh, transform}); } + void setSceneTarget(RenderTargetHandle handle) { + this->sceneTargetHandle = handle; + } + + u32 getColorFormat() const { + return static_cast(this->renderContext.getImageFormat()); + } + Device& getDevice() { return device; } private: @@ -204,6 +247,8 @@ class Application::Impl { std::vector> meshes; std::vector drawCalls; + + RenderTargetHandle sceneTargetHandle = RenderTargetHandle::invalid(); }; void Application::setCommandLineArgs(int argc, const char* const* argv) { @@ -214,7 +259,8 @@ Application::Application(const Config& config) : impl{std::make_unique(*this, config)}, canvas{this}, args{std::move(commandLineArgs)}, - assetRegistry{impl->getDevice()}, + resourceRegistry{impl->getDevice(), impl->getColorFormat()}, + _ui{resourceRegistry}, _debugUI{this->panelRegistry, this->consoleCommandRegistry} {} Application::~Application() = default; @@ -226,10 +272,14 @@ void Application::pushMesh(std::vector vertices, impl->pushMesh(std::move(vertices), std::move(indices)); } +void Application::setSceneTarget(RenderTargetHandle handle) { + impl->setSceneTarget(handle); +} + void Application::clearMeshes() { impl->clearMeshes(); } void Application::drawMesh(MeshHandle handle, const glm::mat4& transform) { - impl->drawMesh(assetRegistry.get(handle), transform); + impl->drawMesh(resourceRegistry.get(handle), transform); } } // namespace sophia diff --git a/engine/src/assets/asset_registry.cpp b/engine/src/assets/asset_registry.cpp deleted file mode 100644 index 27fcdd9..0000000 --- a/engine/src/assets/asset_registry.cpp +++ /dev/null @@ -1,60 +0,0 @@ -#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/image.cpp b/engine/src/image.cpp index 00ff258..e2b8a70 100644 --- a/engine/src/image.cpp +++ b/engine/src/image.cpp @@ -63,6 +63,18 @@ void Image::transitionLayout(vk::CommandBuffer commandBuffer, barrier.dstAccessMask = {}; srcStage = vk::PipelineStageFlagBits::eColorAttachmentOutput; dstStage = vk::PipelineStageFlagBits::eBottomOfPipe; + } else if (this->layout == vk::ImageLayout::eColorAttachmentOptimal && + newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) { + barrier.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; + barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead; + srcStage = vk::PipelineStageFlagBits::eColorAttachmentOutput; + dstStage = vk::PipelineStageFlagBits::eFragmentShader; + } else if (this->layout == vk::ImageLayout::eShaderReadOnlyOptimal && + newLayout == vk::ImageLayout::eColorAttachmentOptimal) { + barrier.srcAccessMask = vk::AccessFlagBits::eShaderRead; + barrier.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; + srcStage = vk::PipelineStageFlagBits::eFragmentShader; + dstStage = vk::PipelineStageFlagBits::eColorAttachmentOutput; } else if (this->layout == vk::ImageLayout::ePresentSrcKHR && newLayout == vk::ImageLayout::eColorAttachmentOptimal) { barrier.srcAccessMask = {}; diff --git a/engine/src/imgui_renderer.cpp b/engine/src/imgui_renderer.cpp index 538c29c..c892d64 100644 --- a/engine/src/imgui_renderer.cpp +++ b/engine/src/imgui_renderer.cpp @@ -5,6 +5,10 @@ #include #include +#include + +#include "render_target.hpp" + namespace sophia { namespace { @@ -20,7 +24,8 @@ void dockAll(const std::vector& windows, ImGuiID nodeId) { ImGuiRenderer::ImGuiRenderer(Window& window, Device& device, vk::Format colorAttachmentFormat, - u32 minImageCount) { + u32 minImageCount, ResourceRegistry& resources) + : resources{resources} { // 1. Construct ImGui context IMGUI_CHECKVERSION(); ImGui::CreateContext(); @@ -67,12 +72,17 @@ ImGuiRenderer::ImGuiRenderer(Window& window, Device& device, } ImGuiRenderer::~ImGuiRenderer() { + for (const auto& [index, texture] : textures) { + ImGui_ImplVulkan_RemoveTexture(texture.descriptorSet); + } ImGui_ImplVulkan_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); } void ImGuiRenderer::beginFrame() { + releaseStaleTextures(); + ImGui_ImplVulkan_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); @@ -82,6 +92,45 @@ void ImGuiRenderer::beginFrame() { applyLayoutIfNeeded(dockspaceId, DOCKSPACE_FLAGS); } +ImTextureID ImGuiRenderer::getTextureId(RenderTargetHandle handle) { + vk::ImageView view = + std::as_const(resources).getRenderTarget(handle).getImage().getView(); + + auto it = textures.find(handle.index); + if (it != textures.end()) { + if (it->second.generation == handle.generation && it->second.view == view) { + return (ImTextureID)it->second.descriptorSet; + } + ImGui_ImplVulkan_RemoveTexture(it->second.descriptorSet); + textures.erase(it); + } + + // ImGui owns the sampler; it only needs the view and the layout the image is + // left in once the scene pass has finished. + VkDescriptorSet descriptorSet = ImGui_ImplVulkan_AddTexture( + static_cast(view), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + textures[handle.index] = {handle.generation, view, descriptorSet}; + return (ImTextureID)descriptorSet; +} + +// Resizing and releasing a target both wait for the GPU before destroying its +// image, so nothing in flight can still be using a stale texture. +void ImGuiRenderer::releaseStaleTextures() { + for (auto it = textures.begin(); it != textures.end();) { + RenderTargetHandle handle{it->first, it->second.generation}; + bool stale = + !resources.isAlive(handle) || + std::as_const(resources).getRenderTarget(handle).getImage().getView() != + it->second.view; + if (stale) { + ImGui_ImplVulkan_RemoveTexture(it->second.descriptorSet); + it = textures.erase(it); + } else { + ++it; + } + } +} + void ImGuiRenderer::setDefaultLayout(DockLayout layout) { defaultLayout = std::move(layout); seedIfUnsaved = true; diff --git a/engine/src/imgui_renderer.hpp b/engine/src/imgui_renderer.hpp index 34765cb..d224b39 100644 --- a/engine/src/imgui_renderer.hpp +++ b/engine/src/imgui_renderer.hpp @@ -2,31 +2,50 @@ #include +#include #include #include +#include #include "device.hpp" #include "window.hpp" namespace sophia { +class ResourceRegistry; + class ImGuiRenderer { public: ImGuiRenderer(const ImGuiRenderer&) = delete; ImGuiRenderer& operator=(const ImGuiRenderer&) = delete; ImGuiRenderer(Window& window, Device& device, - vk::Format colorAttachmentFormat, u32 minImageCount); + vk::Format colorAttachmentFormat, u32 minImageCount, + ResourceRegistry& resources); ~ImGuiRenderer(); void beginFrame(); void endFrame(vk::CommandBuffer commandBuffer); + ImTextureID getTextureId(RenderTargetHandle handle); + void setDefaultLayout(DockLayout layout); void resetLayout(); private: void applyLayoutIfNeeded(ImGuiID dockspaceId, ImGuiDockNodeFlags flags); + void releaseStaleTextures(); + + struct Texture { + u32 generation; + vk::ImageView view; + VkDescriptorSet descriptorSet; + }; + + ResourceRegistry& resources; + + // Keyed by RenderTargetHandle::index. + std::unordered_map textures; DockLayout defaultLayout; bool seedIfUnsaved = false; diff --git a/engine/src/mesh.hpp b/engine/src/mesh.hpp index 7ee69a8..d800dd7 100644 --- a/engine/src/mesh.hpp +++ b/engine/src/mesh.hpp @@ -1,7 +1,7 @@ #pragma once #include -#include +#include #include #include diff --git a/engine/src/render_target.cpp b/engine/src/render_target.cpp new file mode 100644 index 0000000..68dfc0b --- /dev/null +++ b/engine/src/render_target.cpp @@ -0,0 +1,52 @@ +#include "render_target.hpp" + +#include + +namespace sophia { + +RenderTarget::RenderTarget(Device& device, vk::Format format, u32 width, + u32 height) + : device{device}, format{format} { + create(std::max(width, 1u), std::max(height, 1u)); +} + +RenderTarget::~RenderTarget() { destroy(); } + +vk::Extent2D RenderTarget::getExtent() const { + vk::Extent3D extent = this->image->getExtent(); + return {extent.width, extent.height}; +} + +void RenderTarget::requestResize(u32 width, u32 height) { + this->pendingExtent = vk::Extent2D{std::max(width, 1u), std::max(height, 1u)}; +} + +void RenderTarget::applyPendingResize() { + if (!this->pendingExtent) { + return; + } + + vk::Extent2D extent = *this->pendingExtent; + this->pendingExtent.reset(); + if (extent == getExtent()) { + return; + } + + // Earlier frames may still be sampling the old image (e.g. through the UI). + this->device.waitIdle(); + destroy(); + create(extent.width, extent.height); +} + +void RenderTarget::create(u32 width, u32 height) { + this->image = Image::Builder(this->device) + .setExtent(width, height) + .setFormat(this->format) + .asColorAttachment() + .withView() + .build(); +} + +void RenderTarget::destroy() { this->image.reset(); } + +} // namespace sophia diff --git a/engine/src/render_target.hpp b/engine/src/render_target.hpp new file mode 100644 index 0000000..c73d4a1 --- /dev/null +++ b/engine/src/render_target.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include +#include + +#include "device.hpp" +#include "image.hpp" + +namespace sophia { + +/** + * A place rendering can go. Currently always backed by an offscreen color + * image. + * + * Owned by the ResourceRegistry and addressed through a RenderTargetHandle. + */ +class RenderTarget { + public: + RenderTarget(Device& device, vk::Format format, u32 width, u32 height); + ~RenderTarget(); + + RenderTarget(const RenderTarget&) = delete; + RenderTarget& operator=(const RenderTarget&) = delete; + + Image& getImage() const { return *this->image; } + + vk::Extent2D getExtent() const; + + void requestResize(u32 width, u32 height); + + /** + * Applies a pending resize. Waits for the GPU, so it must run before any of + * the frame's work (or any UI reference to the image) is recorded. + */ + void applyPendingResize(); + + private: + void create(u32 width, u32 height); + void destroy(); + + Device& device; + vk::Format format; + + std::unique_ptr image; + + std::optional pendingExtent; +}; + +} // namespace sophia diff --git a/engine/src/assets/pools/mesh_pool.cpp b/engine/src/resources/pools/mesh_pool.cpp similarity index 97% rename from engine/src/assets/pools/mesh_pool.cpp rename to engine/src/resources/pools/mesh_pool.cpp index 2183ebe..36ce528 100644 --- a/engine/src/assets/pools/mesh_pool.cpp +++ b/engine/src/resources/pools/mesh_pool.cpp @@ -1,4 +1,4 @@ -#include "assets/pools/mesh_pool.hpp" +#include "resources/pools/mesh_pool.hpp" #include diff --git a/engine/src/assets/pools/mesh_pool.hpp b/engine/src/resources/pools/mesh_pool.hpp similarity index 100% rename from engine/src/assets/pools/mesh_pool.hpp rename to engine/src/resources/pools/mesh_pool.hpp diff --git a/engine/src/resources/pools/render_target_pool.cpp b/engine/src/resources/pools/render_target_pool.cpp new file mode 100644 index 0000000..f0c0d41 --- /dev/null +++ b/engine/src/resources/pools/render_target_pool.cpp @@ -0,0 +1,81 @@ +#include "resources/pools/render_target_pool.hpp" + +#include +#include + +namespace sophia { + +namespace { +constexpr u32 GENERATION_MASK = (1u << 12) - 1; +} // namespace + +RenderTargetPool::RenderTargetPool(Device& device, vk::Format format) + : device{device}, format{format} {} + +RenderTargetPool::~RenderTargetPool() = default; + +RenderTargetHandle RenderTargetPool::create(u32 width, u32 height) { + auto target = std::make_unique(device, format, width, height); + + if (!freeList.empty()) { + u32 index = freeList.back(); + freeList.pop_back(); + slots[index].target = std::move(target); + return RenderTargetHandle{index, slots[index].generation}; + } + + slots.push_back(Slot{std::move(target), 0}); + return RenderTargetHandle{static_cast(slots.size() - 1), 0}; +} + +const RenderTargetPool::Slot& RenderTargetPool::validate( + RenderTargetHandle handle) const { + SOPHIA_ASSERT(handle.isValid(), "invalid RenderTargetHandle"); + SOPHIA_ASSERT(handle.index < slots.size(), + "RenderTargetHandle index out of range:", handle.index); + + const Slot& slot = slots[handle.index]; + SOPHIA_ASSERT( + slot.generation == handle.generation, + "stale RenderTargetHandle (generation mismatch):", handle.index); + SOPHIA_ASSERT( + slot.target != nullptr, + "RenderTargetHandle refers to a released target:", handle.index); + return slot; +} + +RenderTarget& RenderTargetPool::get(RenderTargetHandle handle) { + return const_cast(std::as_const(*this).get(handle)); +} + +const RenderTarget& RenderTargetPool::get(RenderTargetHandle handle) const { + return *validate(handle).target; +} + +void RenderTargetPool::release(RenderTargetHandle handle) { + validate(handle); + + // Earlier frames may still be rendering into or sampling the target. + device.waitIdle(); + + Slot& slot = slots[handle.index]; + slot.target.reset(); + slot.generation = (slot.generation + 1) & GENERATION_MASK; + freeList.push_back(handle.index); +} + +bool RenderTargetPool::isAlive(RenderTargetHandle handle) const { + return handle.isValid() && handle.index < slots.size() && + slots[handle.index].generation == handle.generation && + slots[handle.index].target != nullptr; +} + +void RenderTargetPool::applyPendingResizes() { + for (Slot& slot : slots) { + if (slot.target) { + slot.target->applyPendingResize(); + } + } +} + +} // namespace sophia diff --git a/engine/src/resources/pools/render_target_pool.hpp b/engine/src/resources/pools/render_target_pool.hpp new file mode 100644 index 0000000..5e8055c --- /dev/null +++ b/engine/src/resources/pools/render_target_pool.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include + +#include "device.hpp" +#include "render_target.hpp" + +namespace sophia { + +class RenderTargetPool { + public: + RenderTargetPool(Device& device, vk::Format format); + ~RenderTargetPool(); + + RenderTargetPool(const RenderTargetPool&) = delete; + RenderTargetPool& operator=(const RenderTargetPool&) = delete; + + RenderTargetHandle create(u32 width, u32 height); + RenderTarget& get(RenderTargetHandle handle); + const RenderTarget& get(RenderTargetHandle handle) const; + void release(RenderTargetHandle handle); + + bool isAlive(RenderTargetHandle handle) const; + + void applyPendingResizes(); + + private: + struct Slot { + std::unique_ptr target; + u32 generation = 0; + }; + + const Slot& validate(RenderTargetHandle handle) const; + + Device& device; + vk::Format format; + + std::vector slots; + std::vector freeList; +}; + +} // namespace sophia diff --git a/engine/src/resources/resource_registry.cpp b/engine/src/resources/resource_registry.cpp new file mode 100644 index 0000000..1f72aed --- /dev/null +++ b/engine/src/resources/resource_registry.cpp @@ -0,0 +1,104 @@ +#include +#include + +#include "mesh.hpp" +#include "pools/mesh_pool.hpp" +#include "pools/render_target_pool.hpp" +#include "render_target.hpp" + +namespace sophia { + +class ResourceRegistry::Impl { + public: + Impl(Device& device, vk::Format renderTargetFormat) + : meshPool{device}, renderTargetPool{device, renderTargetFormat} {} + + MeshPool meshPool; + RenderTargetPool renderTargetPool; + // TexturePool texturePool; + // MaterialPool materialPool; +}; + +ResourceRegistry::ResourceRegistry(Device& device, u32 renderTargetFormat) + : impl{std::make_unique( + device, static_cast(renderTargetFormat))} {} + +ResourceRegistry::~ResourceRegistry() = default; + +template <> +MeshHandle ResourceRegistry::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 ResourceTraits::Handle load(const std::string&) { + static_assert(sizeof(T) == 0, + "load is not implemented for this resource type"); +} + +template <> +void ResourceRegistry::release(MeshHandle handle) { + impl->meshPool.release(handle); +} + +template <> +void ResourceRegistry::release(RenderTargetHandle handle) { + impl->renderTargetPool.release(handle); +} + +template <> +const Mesh& ResourceRegistry::get(MeshHandle handle) const { + return impl->meshPool.get(handle); +} + +MeshHandle ResourceRegistry::createResource(std::type_identity, + 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); +} + +RenderTargetHandle ResourceRegistry::createResource( + std::type_identity, u32 width, u32 height) { + return impl->renderTargetPool.create(width, height); +} + +void ResourceRegistry::requestResize(RenderTargetHandle handle, u32 width, + u32 height) { + impl->renderTargetPool.get(handle).requestResize(width, height); +} + +glm::vec2 ResourceRegistry::getExtent(RenderTargetHandle handle) const { + vk::Extent2D extent = + std::as_const(impl->renderTargetPool).get(handle).getExtent(); + return {static_cast(extent.width), static_cast(extent.height)}; +} + +bool ResourceRegistry::isAlive(RenderTargetHandle handle) const { + return impl->renderTargetPool.isAlive(handle); +} + +RenderTarget& ResourceRegistry::getRenderTarget(RenderTargetHandle handle) { + return impl->renderTargetPool.get(handle); +} + +const RenderTarget& ResourceRegistry::getRenderTarget( + RenderTargetHandle handle) const { + return std::as_const(impl->renderTargetPool).get(handle); +} + +void ResourceRegistry::applyPendingResizes() { + impl->renderTargetPool.applyPendingResizes(); +} + +} // namespace sophia diff --git a/engine/src/ui/ui.cpp b/engine/src/ui/ui.cpp new file mode 100644 index 0000000..5d18f70 --- /dev/null +++ b/engine/src/ui/ui.cpp @@ -0,0 +1,27 @@ +#include +#include +#include + +#include "imgui_renderer.hpp" + +namespace sophia { + +void UI::image(RenderTargetHandle handle) { + const ImVec2 size = ImGui::GetContentRegionAvail(); + if (size.x < 1.0f || size.y < 1.0f) { + return; + } + + resources.requestResize(handle, static_cast(size.x), + static_cast(size.y)); + ImTextureID textureId = getTextureId(handle); + ImGui::Image(textureId, size); +} + +ImTextureID UI::getTextureId(RenderTargetHandle handle) { + SOPHIA_ASSERT(renderer != nullptr, + "UI::getTextureId() called before the UI was set up"); + return renderer->getTextureId(handle); +} + +} // namespace sophia diff --git a/examples/boids_2d/main.cpp b/examples/boids_2d/main.cpp index 1eaba8b..3d723fd 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 = assets().loadMesh(std::move(vertices), {}); + boidMesh = resources().create(std::move(vertices)); } void onUpdate(const sophia::FrameInfo& frameInfo) override { diff --git a/testbed/main.cpp b/testbed/main.cpp index 05515d6..dd39c5a 100644 --- a/testbed/main.cpp +++ b/testbed/main.cpp @@ -1,17 +1,35 @@ -#include +#include + #include +#include +#include + +class Viewport : public sophia::Panel { + public: + Viewport(sophia::UI& ui, sophia::RenderTargetHandle target) + : Panel{{.name = "Viewport"}}, ui{ui}, target{target} {} + + void onUpdate(const sophia::FrameInfo&) override { + ImGui::Text("Test ViewPort"); + + ui.image(target); + } + + private: + sophia::UI& ui; + sophia::RenderTargetHandle target; +}; class Testbed : public sophia::Application { public: Testbed() : Application{{.width = 750, .height = 1000, .name = "Testbed"}} {} void onCreate() override { - std::cout << "Testbed created" << std::endl; - - debugUI().show(); + sophia::RenderTargetHandle target = + resources().create(); + setSceneTarget(target); + panels().emplace(ui(), target); } - - void onUpdate(const sophia::FrameInfo& frameInfo) override {} }; SOPHIA_MAIN(Testbed) From fe40d4a6296ac1f802d34dac2d50202533026751 Mon Sep 17 00:00:00 2001 From: Caleb Burke Date: Sun, 20 Sep 2026 18:58:51 -0700 Subject: [PATCH 02/26] Simply --- engine/src/imgui_renderer.cpp | 2 -- engine/src/imgui_renderer.hpp | 3 +-- engine/src/shader.cpp | 5 ++--- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/engine/src/imgui_renderer.cpp b/engine/src/imgui_renderer.cpp index c892d64..89b169f 100644 --- a/engine/src/imgui_renderer.cpp +++ b/engine/src/imgui_renderer.cpp @@ -5,8 +5,6 @@ #include #include -#include - #include "render_target.hpp" namespace sophia { diff --git a/engine/src/imgui_renderer.hpp b/engine/src/imgui_renderer.hpp index d224b39..3fe3273 100644 --- a/engine/src/imgui_renderer.hpp +++ b/engine/src/imgui_renderer.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -12,8 +13,6 @@ namespace sophia { -class ResourceRegistry; - class ImGuiRenderer { public: ImGuiRenderer(const ImGuiRenderer&) = delete; diff --git a/engine/src/shader.cpp b/engine/src/shader.cpp index 1093a52..a75b6bc 100644 --- a/engine/src/shader.cpp +++ b/engine/src/shader.cpp @@ -74,9 +74,8 @@ void Shader::load(const std::string& path, ShaderStage shaderStage) { throw std::runtime_error("invalid SPIR-V binary: " + path); } - this->spirv.assign( - reinterpret_cast(bytes.data()), - reinterpret_cast(bytes.data() + bytes.size())); + this->spirv.assign(reinterpret_cast(bytes.data()), + reinterpret_cast(bytes.data() + bytes.size())); log::info("Loaded precompiled shader: " + path + " (" + std::to_string(bytes.size()) + " bytes)"); From 16f7bb7da3167ab5f9cc00fafb6683b96164d67b Mon Sep 17 00:00:00 2001 From: Caleb Burke Date: Sun, 20 Sep 2026 20:52:09 -0700 Subject: [PATCH 03/26] Upgrading pool pattern --- _Design/glossary.md | 41 +++++-- engine/include/sophia/application.hpp | 7 +- engine/include/sophia/components.hpp | 2 +- engine/include/sophia/handles.hpp | 2 + ...registry.hpp => resource_registry_ref.hpp} | 51 ++++----- engine/include/sophia/ui/ui.hpp | 6 +- engine/src/imgui_renderer.cpp | 9 +- engine/src/imgui_renderer.hpp | 2 +- engine/src/mesh.hpp | 2 +- engine/src/resources/pools/mesh_pool.cpp | 58 ---------- engine/src/resources/pools/mesh_pool.hpp | 35 ------ engine/src/resources/pools/pool.hpp | 90 +++++++++++++++ .../resources/pools/render_target_pool.cpp | 67 +---------- .../resources/pools/render_target_pool.hpp | 22 ++-- engine/src/resources/resource_registry.cpp | 104 ------------------ engine/src/resources/resource_registry.hpp | 29 +++++ .../src/resources/resource_registry_ref.cpp | 72 ++++++++++++ engine/src/ui/ui.cpp | 1 - 18 files changed, 268 insertions(+), 332 deletions(-) rename engine/include/sophia/{resource_registry.hpp => resource_registry_ref.hpp} (55%) delete mode 100644 engine/src/resources/pools/mesh_pool.cpp delete mode 100644 engine/src/resources/pools/mesh_pool.hpp create mode 100644 engine/src/resources/pools/pool.hpp delete mode 100644 engine/src/resources/resource_registry.cpp create mode 100644 engine/src/resources/resource_registry.hpp create mode 100644 engine/src/resources/resource_registry_ref.cpp diff --git a/_Design/glossary.md b/_Design/glossary.md index d06b4fd..01efd25 100644 --- a/_Design/glossary.md +++ b/_Design/glossary.md @@ -3,34 +3,55 @@ ## ResourceRegistry A ResourceRegistry owns GPU-backed resources on the caller's behalf and hands out -lightweight Handles instead of pointers or ownership. +lightweight Handles instead of pointers or ownership. It is engine-internal: +frontend code never sees it, and reaches it through a ResourceRegistryRef. -Internally it's a collection of per-type pools, each responsible for one -resource type: +Internally it's a collection of per-type pools, each a `Pool` +(a generation-checked slot pool). A type that needs extra behaviour wraps its +pool in a thin class instead: ``` ResourceRegistry - ├── MeshPool → MeshHandle - ├── TexturePool → TextureHandle (planned) - └── MaterialPool → MaterialHandle (planned) + ├── meshes Pool + ├── renderTargets RenderTargetPool → RenderTargetHandle + │ (wraps Pool; waits for the GPU on release, applies resizes) + ├── scenes Pool + ├── textures (planned) + └── materials (planned) ``` +Engine code (`Application`, `ImGuiRenderer`, `SceneRenderer`) uses the pools +directly, e.g. `resources.renderTargets.get(handle)`. + 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 resource (or freed memory). +Think: "Where does the engine keep every GPU resource, and how does it look one +up by handle?" + +## ResourceRegistryRef + +A ResourceRegistryRef is the frontend's non-owning, cheap-to-copy view of the +ResourceRegistry, returned by `Application::resources()`. It exposes only what +frontend code needs and hands out no pools, so no Vulkan-facing types leak +through the public API: + +``` +create(...) load(path) release(handle) +requestResize(target, w, h) getExtent(target) isAlive(handle) +``` + Typical lifecycle: ``` load(path) / create(...) → MeshHandle │ - get(handle) → const Mesh& - │ release(handle) ``` -Think: "Where do I get or release a GPU resource by handle, without owning its +Think: "How do I create or release a GPU resource by handle, without owning its lifetime directly?" ## Buffer @@ -257,7 +278,7 @@ 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 ResourceRegistry's -per-type pools use: +`Pool` uses: ``` PanelRegistry diff --git a/engine/include/sophia/application.hpp b/engine/include/sophia/application.hpp index 67360f3..88b333e 100644 --- a/engine/include/sophia/application.hpp +++ b/engine/include/sophia/application.hpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include #include #include #include @@ -45,7 +45,7 @@ class Application { virtual void onRender() {} virtual void onDestroy() {} - ResourceRegistry& resources() { return this->resourceRegistry; } + ResourceRegistryRef resources(); PanelRegistry& panels() { return this->panelRegistry; } UI& ui() { return this->_ui; } @@ -62,7 +62,7 @@ class Application { /** * Chooses where the scene is rendered. An invalid handle (the default) means * the swapchain; a target created with - * ResourceRegistry::create() redirects it offscreen, and the + * ResourceRegistryRef::create() redirects it offscreen, and the * swapchain then only receives the UI. FrameInfo::currentFramebufferExtent * and the camera projection follow the target's size. */ @@ -77,7 +77,6 @@ class Application { class Impl; std::unique_ptr impl; - ResourceRegistry resourceRegistry; PanelRegistry panelRegistry; UI _ui; diff --git a/engine/include/sophia/components.hpp b/engine/include/sophia/components.hpp index 76c783b..ac37353 100644 --- a/engine/include/sophia/components.hpp +++ b/engine/include/sophia/components.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #define GLM_FORCE_RADIANS diff --git a/engine/include/sophia/handles.hpp b/engine/include/sophia/handles.hpp index cd27a6a..acee081 100644 --- a/engine/include/sophia/handles.hpp +++ b/engine/include/sophia/handles.hpp @@ -27,9 +27,11 @@ struct Handle { struct MeshTag {}; struct PanelTag {}; struct RenderTargetTag {}; +struct SceneTag {}; using MeshHandle = Handle; using PanelHandle = Handle; using RenderTargetHandle = Handle; +using SceneHandle = Handle; } // namespace sophia diff --git a/engine/include/sophia/resource_registry.hpp b/engine/include/sophia/resource_registry_ref.hpp similarity index 55% rename from engine/include/sophia/resource_registry.hpp rename to engine/include/sophia/resource_registry_ref.hpp index c22657c..5823b63 100644 --- a/engine/include/sophia/resource_registry.hpp +++ b/engine/include/sophia/resource_registry_ref.hpp @@ -1,25 +1,23 @@ #pragma once -#include +#include #include +#include #include #include #include #include -#include "sophia/canvas.hpp" -#include "sophia/types.hpp" - #define GLM_FORCE_RADIANS #define GLM_FORCE_DEPTH_ZERO_TO_ONE #include namespace sophia { -class Device; class Mesh; class RenderTarget; -class ImGuiRenderer; +class Scene; +class ResourceRegistry; template struct ResourceTraits; @@ -34,13 +32,13 @@ struct ResourceTraits { using Handle = RenderTargetHandle; }; -class ResourceRegistry { +template <> +struct ResourceTraits { + using Handle = SceneHandle; +}; + +class ResourceRegistryRef { public: - ~ResourceRegistry(); - - ResourceRegistry(const ResourceRegistry&) = delete; - ResourceRegistry& operator=(const ResourceRegistry&) = delete; - template typename ResourceTraits::Handle load(const std::string& path); @@ -54,21 +52,16 @@ class ResourceRegistry { glm::vec2 getExtent(RenderTargetHandle handle) const; bool isAlive(RenderTargetHandle handle) const; + bool isAlive(SceneHandle handle) const; - /** - * Waits for the GPU to go idle, so don't call this every frame. Don't release - * the target the scene is currently rendering into. - */ template void release(typename ResourceTraits::Handle handle); private: friend class Application; - friend class ImGuiRenderer; - // `renderTargetFormat` is a VkFormat value; it must match the color format - // the scene pipelines are built against. - ResourceRegistry(Device& device, u32 renderTargetFormat); + explicit ResourceRegistryRef(ResourceRegistry& registry) + : registry{®istry} {} MeshHandle createResource(std::type_identity, std::vector vertices, @@ -77,27 +70,21 @@ class ResourceRegistry { RenderTargetHandle createResource(std::type_identity, u32 width = 1, u32 height = 1); - RenderTarget& getRenderTarget(RenderTargetHandle handle); - const RenderTarget& getRenderTarget(RenderTargetHandle handle) const; - void applyPendingResizes(); + SceneHandle createResource(std::type_identity); - template - const T& get(typename ResourceTraits::Handle handle) const; - - class Impl; - std::unique_ptr impl; + ResourceRegistry* registry; }; template <> -MeshHandle ResourceRegistry::load(const std::string& path); +MeshHandle ResourceRegistryRef::load(const std::string& path); template <> -void ResourceRegistry::release(MeshHandle handle); +void ResourceRegistryRef::release(MeshHandle handle); template <> -void ResourceRegistry::release(RenderTargetHandle handle); +void ResourceRegistryRef::release(RenderTargetHandle handle); template <> -const Mesh& ResourceRegistry::get(MeshHandle handle) const; +void ResourceRegistryRef::release(SceneHandle handle); } // namespace sophia diff --git a/engine/include/sophia/ui/ui.hpp b/engine/include/sophia/ui/ui.hpp index 17d419a..9db7074 100644 --- a/engine/include/sophia/ui/ui.hpp +++ b/engine/include/sophia/ui/ui.hpp @@ -3,11 +3,11 @@ #include #include +#include namespace sophia { class ImGuiRenderer; -class ResourceRegistry; class UI { public: @@ -31,9 +31,9 @@ class UI { private: friend class Application; - explicit UI(ResourceRegistry& resources) : resources{resources} {} + explicit UI(ResourceRegistryRef resources) : resources{resources} {} - ResourceRegistry& resources; + ResourceRegistryRef resources; ImGuiRenderer* renderer = nullptr; }; diff --git a/engine/src/imgui_renderer.cpp b/engine/src/imgui_renderer.cpp index 89b169f..de739d9 100644 --- a/engine/src/imgui_renderer.cpp +++ b/engine/src/imgui_renderer.cpp @@ -92,7 +92,7 @@ void ImGuiRenderer::beginFrame() { ImTextureID ImGuiRenderer::getTextureId(RenderTargetHandle handle) { vk::ImageView view = - std::as_const(resources).getRenderTarget(handle).getImage().getView(); + resources.renderTargetPool.get(handle).getImage().getView(); auto it = textures.find(handle.index); if (it != textures.end()) { @@ -116,10 +116,11 @@ ImTextureID ImGuiRenderer::getTextureId(RenderTargetHandle handle) { void ImGuiRenderer::releaseStaleTextures() { for (auto it = textures.begin(); it != textures.end();) { RenderTargetHandle handle{it->first, it->second.generation}; + RenderTargetPool& renderTargetPool = resources.renderTargetPool; + bool stale = - !resources.isAlive(handle) || - std::as_const(resources).getRenderTarget(handle).getImage().getView() != - it->second.view; + !renderTargetPool.isAlive(handle) || + renderTargetPool.get(handle).getImage().getView() != it->second.view; if (stale) { ImGui_ImplVulkan_RemoveTexture(it->second.descriptorSet); it = textures.erase(it); diff --git a/engine/src/imgui_renderer.hpp b/engine/src/imgui_renderer.hpp index 3fe3273..cfc79a1 100644 --- a/engine/src/imgui_renderer.hpp +++ b/engine/src/imgui_renderer.hpp @@ -3,12 +3,12 @@ #include #include -#include #include #include #include #include "device.hpp" +#include "resources/resource_registry.hpp" #include "window.hpp" namespace sophia { diff --git a/engine/src/mesh.hpp b/engine/src/mesh.hpp index d800dd7..f25f7db 100644 --- a/engine/src/mesh.hpp +++ b/engine/src/mesh.hpp @@ -1,7 +1,7 @@ #pragma once #include -#include +#include #include #include diff --git a/engine/src/resources/pools/mesh_pool.cpp b/engine/src/resources/pools/mesh_pool.cpp deleted file mode 100644 index 36ce528..0000000 --- a/engine/src/resources/pools/mesh_pool.cpp +++ /dev/null @@ -1,58 +0,0 @@ -#include "resources/pools/mesh_pool.hpp" - -#include - -namespace sophia { - -namespace { -constexpr u32 GENERATION_MASK = (1u << 12) - 1; -} // namespace - -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()) { - u32 index = freeList.back(); - freeList.pop_back(); - slots[index].mesh = std::move(mesh); - return MeshHandle{index, slots[index].generation}; - } - - 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.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.isValid(), "invalid MeshHandle"); - SOPHIA_ASSERT(handle.index < slots.size(), - "MeshHandle index out of range:", handle.index); - - 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/resources/pools/mesh_pool.hpp b/engine/src/resources/pools/mesh_pool.hpp deleted file mode 100644 index 218f0bb..0000000 --- a/engine/src/resources/pools/mesh_pool.hpp +++ /dev/null @@ -1,35 +0,0 @@ -#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: - struct Slot { - std::unique_ptr mesh; - u32 generation = 0; - }; - - Device& device; - - std::vector slots; - std::vector freeList; -}; - -} // namespace sophia diff --git a/engine/src/resources/pools/pool.hpp b/engine/src/resources/pools/pool.hpp new file mode 100644 index 0000000..99669bd --- /dev/null +++ b/engine/src/resources/pools/pool.hpp @@ -0,0 +1,90 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace sophia { + +template +class Pool { + public: + explicit Pool(const char* name) : name{name} {} + + Pool(const Pool&) = delete; + Pool& operator=(const Pool&) = delete; + + template + HandleT create(Args&&... args) { + auto item = std::make_unique(std::forward(args)...); + + if (!freeList.empty()) { + u32 index = freeList.back(); + freeList.pop_back(); + slots[index].item = std::move(item); + return HandleT{index, slots[index].generation}; + } + + slots.push_back(Slot{std::move(item), 0}); + return HandleT{static_cast(slots.size() - 1), 0}; + } + + T& get(HandleT handle) { + return const_cast(std::as_const(*this).get(handle)); + } + + const T& get(HandleT handle) const { return *validate(handle).item; } + + void release(HandleT handle) { + validate(handle); + + Slot& slot = slots[handle.index]; + slot.item.reset(); + slot.generation = (slot.generation + 1) & GENERATION_MASK; + freeList.push_back(handle.index); + } + + bool isAlive(HandleT handle) const { + return handle.isValid() && handle.index < slots.size() && + slots[handle.index].generation == handle.generation && + slots[handle.index].item != nullptr; + } + + template + void forEach(Fn&& fn) { + for (Slot& slot : slots) { + if (slot.item) { + fn(*slot.item); + } + } + } + + private: + static constexpr u32 GENERATION_MASK = (1u << 12) - 1; + + struct Slot { + std::unique_ptr item; + u32 generation = 0; + }; + + const Slot& validate(HandleT handle) const { + SOPHIA_ASSERT(handle.isValid(), "invalid handle:", name); + SOPHIA_ASSERT(handle.index < slots.size(), name, + "handle index out of range:", handle.index); + + const Slot& slot = slots[handle.index]; + SOPHIA_ASSERT(slot.generation == handle.generation, "stale", name, + "handle (generation mismatch):", handle.index); + SOPHIA_ASSERT(slot.item != nullptr, name, + "handle refers to a released item:", handle.index); + return slot; + } + + const char* name; + std::vector slots; + std::vector freeList; +}; + +} // namespace sophia diff --git a/engine/src/resources/pools/render_target_pool.cpp b/engine/src/resources/pools/render_target_pool.cpp index f0c0d41..fb3ee70 100644 --- a/engine/src/resources/pools/render_target_pool.cpp +++ b/engine/src/resources/pools/render_target_pool.cpp @@ -1,81 +1,22 @@ #include "resources/pools/render_target_pool.hpp" -#include -#include - namespace sophia { -namespace { -constexpr u32 GENERATION_MASK = (1u << 12) - 1; -} // namespace - RenderTargetPool::RenderTargetPool(Device& device, vk::Format format) : device{device}, format{format} {} -RenderTargetPool::~RenderTargetPool() = default; - RenderTargetHandle RenderTargetPool::create(u32 width, u32 height) { - auto target = std::make_unique(device, format, width, height); - - if (!freeList.empty()) { - u32 index = freeList.back(); - freeList.pop_back(); - slots[index].target = std::move(target); - return RenderTargetHandle{index, slots[index].generation}; - } - - slots.push_back(Slot{std::move(target), 0}); - return RenderTargetHandle{static_cast(slots.size() - 1), 0}; -} - -const RenderTargetPool::Slot& RenderTargetPool::validate( - RenderTargetHandle handle) const { - SOPHIA_ASSERT(handle.isValid(), "invalid RenderTargetHandle"); - SOPHIA_ASSERT(handle.index < slots.size(), - "RenderTargetHandle index out of range:", handle.index); - - const Slot& slot = slots[handle.index]; - SOPHIA_ASSERT( - slot.generation == handle.generation, - "stale RenderTargetHandle (generation mismatch):", handle.index); - SOPHIA_ASSERT( - slot.target != nullptr, - "RenderTargetHandle refers to a released target:", handle.index); - return slot; -} - -RenderTarget& RenderTargetPool::get(RenderTargetHandle handle) { - return const_cast(std::as_const(*this).get(handle)); -} - -const RenderTarget& RenderTargetPool::get(RenderTargetHandle handle) const { - return *validate(handle).target; + return pool.create(device, format, width, height); } void RenderTargetPool::release(RenderTargetHandle handle) { - validate(handle); - - // Earlier frames may still be rendering into or sampling the target. + SOPHIA_ASSERT(pool.isAlive(handle), "invalid RenderTargetHandle"); device.waitIdle(); - - Slot& slot = slots[handle.index]; - slot.target.reset(); - slot.generation = (slot.generation + 1) & GENERATION_MASK; - freeList.push_back(handle.index); -} - -bool RenderTargetPool::isAlive(RenderTargetHandle handle) const { - return handle.isValid() && handle.index < slots.size() && - slots[handle.index].generation == handle.generation && - slots[handle.index].target != nullptr; + pool.release(handle); } void RenderTargetPool::applyPendingResizes() { - for (Slot& slot : slots) { - if (slot.target) { - slot.target->applyPendingResize(); - } - } + pool.forEach([](RenderTarget& target) { target.applyPendingResize(); }); } } // namespace sophia diff --git a/engine/src/resources/pools/render_target_pool.hpp b/engine/src/resources/pools/render_target_pool.hpp index 5e8055c..ff7ca0f 100644 --- a/engine/src/resources/pools/render_target_pool.hpp +++ b/engine/src/resources/pools/render_target_pool.hpp @@ -1,44 +1,36 @@ #pragma once -#include #include -#include #include "device.hpp" #include "render_target.hpp" +#include "resources/pools/pool.hpp" namespace sophia { class RenderTargetPool { public: RenderTargetPool(Device& device, vk::Format format); - ~RenderTargetPool(); RenderTargetPool(const RenderTargetPool&) = delete; RenderTargetPool& operator=(const RenderTargetPool&) = delete; RenderTargetHandle create(u32 width, u32 height); - RenderTarget& get(RenderTargetHandle handle); - const RenderTarget& get(RenderTargetHandle handle) const; + RenderTarget& get(RenderTargetHandle handle) { return pool.get(handle); } + const RenderTarget& get(RenderTargetHandle handle) const { + return pool.get(handle); + } void release(RenderTargetHandle handle); - bool isAlive(RenderTargetHandle handle) const; + bool isAlive(RenderTargetHandle handle) const { return pool.isAlive(handle); } void applyPendingResizes(); private: - struct Slot { - std::unique_ptr target; - u32 generation = 0; - }; - - const Slot& validate(RenderTargetHandle handle) const; - Device& device; vk::Format format; - std::vector slots; - std::vector freeList; + Pool pool{"RenderTarget"}; }; } // namespace sophia diff --git a/engine/src/resources/resource_registry.cpp b/engine/src/resources/resource_registry.cpp deleted file mode 100644 index 1f72aed..0000000 --- a/engine/src/resources/resource_registry.cpp +++ /dev/null @@ -1,104 +0,0 @@ -#include -#include - -#include "mesh.hpp" -#include "pools/mesh_pool.hpp" -#include "pools/render_target_pool.hpp" -#include "render_target.hpp" - -namespace sophia { - -class ResourceRegistry::Impl { - public: - Impl(Device& device, vk::Format renderTargetFormat) - : meshPool{device}, renderTargetPool{device, renderTargetFormat} {} - - MeshPool meshPool; - RenderTargetPool renderTargetPool; - // TexturePool texturePool; - // MaterialPool materialPool; -}; - -ResourceRegistry::ResourceRegistry(Device& device, u32 renderTargetFormat) - : impl{std::make_unique( - device, static_cast(renderTargetFormat))} {} - -ResourceRegistry::~ResourceRegistry() = default; - -template <> -MeshHandle ResourceRegistry::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 ResourceTraits::Handle load(const std::string&) { - static_assert(sizeof(T) == 0, - "load is not implemented for this resource type"); -} - -template <> -void ResourceRegistry::release(MeshHandle handle) { - impl->meshPool.release(handle); -} - -template <> -void ResourceRegistry::release(RenderTargetHandle handle) { - impl->renderTargetPool.release(handle); -} - -template <> -const Mesh& ResourceRegistry::get(MeshHandle handle) const { - return impl->meshPool.get(handle); -} - -MeshHandle ResourceRegistry::createResource(std::type_identity, - 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); -} - -RenderTargetHandle ResourceRegistry::createResource( - std::type_identity, u32 width, u32 height) { - return impl->renderTargetPool.create(width, height); -} - -void ResourceRegistry::requestResize(RenderTargetHandle handle, u32 width, - u32 height) { - impl->renderTargetPool.get(handle).requestResize(width, height); -} - -glm::vec2 ResourceRegistry::getExtent(RenderTargetHandle handle) const { - vk::Extent2D extent = - std::as_const(impl->renderTargetPool).get(handle).getExtent(); - return {static_cast(extent.width), static_cast(extent.height)}; -} - -bool ResourceRegistry::isAlive(RenderTargetHandle handle) const { - return impl->renderTargetPool.isAlive(handle); -} - -RenderTarget& ResourceRegistry::getRenderTarget(RenderTargetHandle handle) { - return impl->renderTargetPool.get(handle); -} - -const RenderTarget& ResourceRegistry::getRenderTarget( - RenderTargetHandle handle) const { - return std::as_const(impl->renderTargetPool).get(handle); -} - -void ResourceRegistry::applyPendingResizes() { - impl->renderTargetPool.applyPendingResizes(); -} - -} // namespace sophia diff --git a/engine/src/resources/resource_registry.hpp b/engine/src/resources/resource_registry.hpp new file mode 100644 index 0000000..6dc5629 --- /dev/null +++ b/engine/src/resources/resource_registry.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include "device.hpp" +#include "mesh.hpp" +#include "resources/pools/pool.hpp" +#include "resources/pools/render_target_pool.hpp" +#include "scene.hpp" + +namespace sophia { + +class ResourceRegistry { + public: + ResourceRegistry(const ResourceRegistry&) = delete; + ResourceRegistry& operator=(const ResourceRegistry&) = delete; + + ResourceRegistry(Device& device, vk::Format renderTargetFormat) + : device{device}, renderTargetPool{device, renderTargetFormat} {} + + Device& device; + Pool meshPool{"Mesh"}; + RenderTargetPool renderTargetPool; + Pool scenePool{"Scene"}; + // TexturePool textures; + // MaterialPool materials; +}; + +} // namespace sophia diff --git a/engine/src/resources/resource_registry_ref.cpp b/engine/src/resources/resource_registry_ref.cpp new file mode 100644 index 0000000..5e24d7b --- /dev/null +++ b/engine/src/resources/resource_registry_ref.cpp @@ -0,0 +1,72 @@ +#include +#include + +#include "resources/resource_registry.hpp" + +namespace sophia { + +template <> +MeshHandle ResourceRegistryRef::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 <> +void ResourceRegistryRef::release(MeshHandle handle) { + registry->meshPool.release(handle); +} + +template <> +void ResourceRegistryRef::release(RenderTargetHandle handle) { + registry->renderTargetPool.release(handle); +} + +template <> +void ResourceRegistryRef::release(SceneHandle handle) { + registry->scenePool.release(handle); +} + +MeshHandle ResourceRegistryRef::createResource( + std::type_identity, 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 registry->meshPool.create(registry->device, builder); +} + +RenderTargetHandle ResourceRegistryRef::createResource( + std::type_identity, u32 width, u32 height) { + return registry->renderTargetPool.create(width, height); +} + +SceneHandle ResourceRegistryRef::createResource(std::type_identity) { + return registry->scenePool.create(); +} + +void ResourceRegistryRef::requestResize(RenderTargetHandle handle, u32 width, + u32 height) { + registry->renderTargetPool.get(handle).requestResize(width, height); +} + +glm::vec2 ResourceRegistryRef::getExtent(RenderTargetHandle handle) const { + vk::Extent2D extent = registry->renderTargetPool.get(handle).getExtent(); + return {static_cast(extent.width), static_cast(extent.height)}; +} + +bool ResourceRegistryRef::isAlive(RenderTargetHandle handle) const { + return registry->renderTargetPool.isAlive(handle); +} + +bool ResourceRegistryRef::isAlive(SceneHandle handle) const { + return registry->scenePool.isAlive(handle); +} + +} // namespace sophia diff --git a/engine/src/ui/ui.cpp b/engine/src/ui/ui.cpp index 5d18f70..8a08da7 100644 --- a/engine/src/ui/ui.cpp +++ b/engine/src/ui/ui.cpp @@ -1,4 +1,3 @@ -#include #include #include From f3c0f61eb1c87823db0c09c47bf08b17f3dbf68d Mon Sep 17 00:00:00 2001 From: Caleb Burke Date: Mon, 21 Sep 2026 00:26:50 -0700 Subject: [PATCH 04/26] Big changes --- _Design/glossary.md | 49 +++++++++--- engine/include/sophia/application.hpp | 4 + .../include/sophia/resource_registry_ref.hpp | 12 --- engine/include/sophia/scene.hpp | 21 +++++ engine/include/sophia/scene_registry.hpp | 31 ++++++++ engine/include/sophia/ui/panel_registry.hpp | 56 +++++-------- .../pools => include/sophia/util}/pool.hpp | 49 +++++++++--- engine/src/render_context.cpp | 5 +- engine/src/render_context.hpp | 10 +-- .../resources/pools/render_target_pool.hpp | 2 +- engine/src/resources/resource_registry.hpp | 4 +- .../src/resources/resource_registry_ref.cpp | 13 --- engine/src/scene.cpp | 3 +- engine/src/scene.hpp | 16 ---- engine/src/swapchain.hpp | 3 - engine/src/ui/panel_registry.cpp | 79 ++----------------- 16 files changed, 170 insertions(+), 187 deletions(-) create mode 100644 engine/include/sophia/scene.hpp create mode 100644 engine/include/sophia/scene_registry.hpp rename engine/{src/resources/pools => include/sophia/util}/pool.hpp (60%) delete mode 100644 engine/src/scene.hpp diff --git a/_Design/glossary.md b/_Design/glossary.md index 01efd25..370cea5 100644 --- a/_Design/glossary.md +++ b/_Design/glossary.md @@ -12,16 +12,15 @@ pool in a thin class instead: ``` ResourceRegistry - ├── meshes Pool - ├── renderTargets RenderTargetPool → RenderTargetHandle - │ (wraps Pool; waits for the GPU on release, applies resizes) - ├── scenes Pool - ├── textures (planned) - └── materials (planned) + ├── meshPool Pool + ├── renderTargetPool RenderTargetPool → RenderTargetHandle + │ (wraps Pool; waits for the GPU on release, applies resizes) + ├── texturePool (planned) + └── materialPool (planned) ``` Engine code (`Application`, `ImGuiRenderer`, `SceneRenderer`) uses the pools -directly, e.g. `resources.renderTargets.get(handle)`. +directly, e.g. `resources.renderTargetPool.get(handle)`. 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 @@ -277,12 +276,14 @@ This is primarily a UI concept, rather than a GPU concept. 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 ResourceRegistry's -`Pool` uses: +Internally it wraps a `Pool` (the same generation-checked +slot pool ResourceRegistry uses, in `sophia/util/pool.hpp`) and adds the +Panel-specific parts: lookup by type or name, and the default dock layout: ``` PanelRegistry - └── Slot { Panel, generation } → PanelHandle + └── Pool + └── Slot { Panel, generation } → PanelHandle ``` Typical lifecycle: @@ -300,6 +301,34 @@ 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?" +## SceneRegistry + +A SceneRegistry owns Scenes on the caller's behalf and hands out lightweight +SceneHandles instead of pointers or ownership. Like PanelRegistry, it is owned +by Application and reached from frontend code directly (`scenes()`), rather than +through ResourceRegistryRef: scenes are CPU-side entity data, not GPU resources. + +Internally it wraps a `Pool`: + +``` +SceneRegistry + └── Pool + └── Slot { Scene, generation } → SceneHandle +``` + +Typical lifecycle: + +``` +create() → SceneHandle + │ + get(handle) → Scene& + │ + release(handle) +``` + +Think: "Where do I create or look up a Scene 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. diff --git a/engine/include/sophia/application.hpp b/engine/include/sophia/application.hpp index 88b333e..f7675b0 100644 --- a/engine/include/sophia/application.hpp +++ b/engine/include/sophia/application.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -47,6 +48,7 @@ class Application { ResourceRegistryRef resources(); PanelRegistry& panels() { return this->panelRegistry; } + SceneRegistry& scenes() { return this->sceneRegistry; } UI& ui() { return this->_ui; } DebugUI& debugUI() { return this->_debugUI; } @@ -60,6 +62,7 @@ class Application { void drawMesh(MeshHandle handle, const glm::mat4& transform); /** + * !TEMP !TODO remove this func eventually * Chooses where the scene is rendered. An invalid handle (the default) means * the swapchain; a target created with * ResourceRegistryRef::create() redirects it offscreen, and the @@ -78,6 +81,7 @@ class Application { std::unique_ptr impl; PanelRegistry panelRegistry; + SceneRegistry sceneRegistry; UI _ui; DebugUI _debugUI; diff --git a/engine/include/sophia/resource_registry_ref.hpp b/engine/include/sophia/resource_registry_ref.hpp index 5823b63..34d4785 100644 --- a/engine/include/sophia/resource_registry_ref.hpp +++ b/engine/include/sophia/resource_registry_ref.hpp @@ -16,7 +16,6 @@ namespace sophia { class Mesh; class RenderTarget; -class Scene; class ResourceRegistry; template @@ -32,11 +31,6 @@ struct ResourceTraits { using Handle = RenderTargetHandle; }; -template <> -struct ResourceTraits { - using Handle = SceneHandle; -}; - class ResourceRegistryRef { public: template @@ -52,7 +46,6 @@ class ResourceRegistryRef { glm::vec2 getExtent(RenderTargetHandle handle) const; bool isAlive(RenderTargetHandle handle) const; - bool isAlive(SceneHandle handle) const; template void release(typename ResourceTraits::Handle handle); @@ -70,8 +63,6 @@ class ResourceRegistryRef { RenderTargetHandle createResource(std::type_identity, u32 width = 1, u32 height = 1); - SceneHandle createResource(std::type_identity); - ResourceRegistry* registry; }; @@ -84,7 +75,4 @@ void ResourceRegistryRef::release(MeshHandle handle); template <> void ResourceRegistryRef::release(RenderTargetHandle handle); -template <> -void ResourceRegistryRef::release(SceneHandle handle); - } // namespace sophia diff --git a/engine/include/sophia/scene.hpp b/engine/include/sophia/scene.hpp new file mode 100644 index 0000000..bbc1c70 --- /dev/null +++ b/engine/include/sophia/scene.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +namespace sophia { + +class Scene { + public: + Scene(); + ~Scene(); + + // The camera a render request uses when it doesn't name one. Null until set. + entt::entity getActiveCamera() const { return activeCamera; } + void setActiveCamera(entt::entity camera) { activeCamera = camera; } + + private: + entt::registry registry; + entt::entity activeCamera = entt::null; +}; + +} // namespace sophia diff --git a/engine/include/sophia/scene_registry.hpp b/engine/include/sophia/scene_registry.hpp new file mode 100644 index 0000000..70e2739 --- /dev/null +++ b/engine/include/sophia/scene_registry.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include + +namespace sophia { + +class SceneRegistry { + public: + SceneRegistry(const SceneRegistry&) = delete; + SceneRegistry& operator=(const SceneRegistry&) = delete; + + SceneRegistry() = default; + ~SceneRegistry() = default; + + SceneHandle create() { return pool.create(); } + + const Scene& get(SceneHandle handle) const { return pool.get(handle); } + Scene& get(SceneHandle handle) { return pool.get(handle); } + + void release(SceneHandle handle) { pool.release(handle); } + bool isAlive(SceneHandle handle) const { return pool.isAlive(handle); } + + u32 size() const { return pool.size(); } + + private: + Pool pool{"Scene"}; +}; + +} // namespace sophia diff --git a/engine/include/sophia/ui/panel_registry.hpp b/engine/include/sophia/ui/panel_registry.hpp index 09b3b87..b53b11c 100644 --- a/engine/include/sophia/ui/panel_registry.hpp +++ b/engine/include/sophia/ui/panel_registry.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -21,17 +22,17 @@ class PanelRegistry { PanelRegistry() = default; ~PanelRegistry() = default; - PanelHandle add(std::unique_ptr panel); + PanelHandle add(std::unique_ptr panel) { + return pool.add(std::move(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)); - } + const Panel& get(PanelHandle handle) const { return pool.get(handle); } + Panel& get(PanelHandle handle) { return pool.get(handle); } template const T& get(PanelHandle handle) const { @@ -46,19 +47,17 @@ class PanelRegistry { return const_cast(std::as_const(*this).template get(handle)); } - void remove(PanelHandle handle); - bool isValid(PanelHandle handle) const; + void remove(PanelHandle handle) { pool.release(handle); } + bool isValid(PanelHandle handle) const { return pool.isAlive(handle); } 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; + const T* result = nullptr; + pool.findIf([&result](const Panel& panel) { + result = dynamic_cast(&panel); + return result != nullptr; + }); + return result; } template @@ -67,29 +66,22 @@ class PanelRegistry { } 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; + return pool.findIf( + [name](const Panel& panel) { return panel.getName() == name; }); } Panel* find(std::string_view name) { return const_cast(std::as_const(*this).find(name)); } - u32 size() const; + u32 size() const { return pool.size(); } 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()); - } - } + result.reserve(pool.size()); + pool.forEach( + [&result](const Panel& panel) { result.push_back(panel.getName()); }); return result; } @@ -101,13 +93,7 @@ class PanelRegistry { const DockLayout& getDefaultLayout() const { return defaultLayout; } private: - struct Slot { - std::unique_ptr panel; - u32 generation = 0; - }; - - std::vector slots; - std::vector freeList; + Pool pool{"Panel"}; DockLayout defaultLayout; }; diff --git a/engine/src/resources/pools/pool.hpp b/engine/include/sophia/util/pool.hpp similarity index 60% rename from engine/src/resources/pools/pool.hpp rename to engine/include/sophia/util/pool.hpp index 99669bd..f5b8a53 100644 --- a/engine/src/resources/pools/pool.hpp +++ b/engine/include/sophia/util/pool.hpp @@ -8,7 +8,7 @@ namespace sophia { -template +template class Pool { public: explicit Pool(const char* name) : name{name} {} @@ -17,27 +17,29 @@ class Pool { Pool& operator=(const Pool&) = delete; template - HandleT create(Args&&... args) { - auto item = std::make_unique(std::forward(args)...); + HandleType create(Args&&... args) { + return add(std::make_unique(std::forward(args)...)); + } + HandleType add(std::unique_ptr item) { if (!freeList.empty()) { u32 index = freeList.back(); freeList.pop_back(); slots[index].item = std::move(item); - return HandleT{index, slots[index].generation}; + return HandleType{index, slots[index].generation}; } slots.push_back(Slot{std::move(item), 0}); - return HandleT{static_cast(slots.size() - 1), 0}; + return HandleType{static_cast(slots.size() - 1), 0}; } - T& get(HandleT handle) { + T& get(HandleType handle) { return const_cast(std::as_const(*this).get(handle)); } - const T& get(HandleT handle) const { return *validate(handle).item; } + const T& get(HandleType handle) const { return *validate(handle).item; } - void release(HandleT handle) { + void release(HandleType handle) { validate(handle); Slot& slot = slots[handle.index]; @@ -46,12 +48,14 @@ class Pool { freeList.push_back(handle.index); } - bool isAlive(HandleT handle) const { + bool isAlive(HandleType handle) const { return handle.isValid() && handle.index < slots.size() && slots[handle.index].generation == handle.generation && slots[handle.index].item != nullptr; } + u32 size() const { return static_cast(slots.size() - freeList.size()); } + template void forEach(Fn&& fn) { for (Slot& slot : slots) { @@ -61,6 +65,31 @@ class Pool { } } + template + void forEach(Fn&& fn) const { + for (const Slot& slot : slots) { + if (slot.item) { + fn(*slot.item); + } + } + } + + template + T* findIf(Predicate&& predicate) { + return const_cast( + std::as_const(*this).findIf(std::forward(predicate))); + } + + template + const T* findIf(Predicate&& predicate) const { + for (const Slot& slot : slots) { + if (slot.item && predicate(*slot.item)) { + return slot.item.get(); + } + } + return nullptr; + } + private: static constexpr u32 GENERATION_MASK = (1u << 12) - 1; @@ -69,7 +98,7 @@ class Pool { u32 generation = 0; }; - const Slot& validate(HandleT handle) const { + const Slot& validate(HandleType handle) const { SOPHIA_ASSERT(handle.isValid(), "invalid handle:", name); SOPHIA_ASSERT(handle.index < slots.size(), name, "handle index out of range:", handle.index); diff --git a/engine/src/render_context.cpp b/engine/src/render_context.cpp index 76296c6..c07dd0e 100644 --- a/engine/src/render_context.cpp +++ b/engine/src/render_context.cpp @@ -5,6 +5,8 @@ #include #include +#include "rendering/access.hpp" + namespace sophia { RenderContext::RenderContext(Window& window, Device& device) @@ -80,8 +82,7 @@ void RenderContext::endFrame() { vk::Semaphore waitSemaphore = frame.imageAvailable.get(); vk::Semaphore signalSemaphore = this->renderFinishedSemaphores.at(this->currentImageIndex).get(); - vk::PipelineStageFlags waitStage = - vk::PipelineStageFlagBits::eColorAttachmentOutput; + vk::PipelineStageFlags waitStage = SWAPCHAIN_ACQUIRE_STAGE; vk::CommandBuffer commandBuffer = frame.commandBuffer; vk::SubmitInfo submitInfo{}; diff --git a/engine/src/render_context.hpp b/engine/src/render_context.hpp index 34e7ac1..afe4728 100644 --- a/engine/src/render_context.hpp +++ b/engine/src/render_context.hpp @@ -31,14 +31,8 @@ class RenderContext { vk::CommandBuffer beginFrame(); void endFrame(); - void transitionCurrentImage(vk::CommandBuffer commandBuffer, - vk::ImageLayout newLayout) const { - this->swapchain->getImage(static_cast(this->currentImageIndex)) - .transitionLayout(commandBuffer, newLayout); - } - vk::ImageView getCurrentImageView() const { - return this->swapchain->getImageView( - static_cast(this->currentImageIndex)); + Image& getCurrentImage() const { + return this->swapchain->getImage(static_cast(this->currentImageIndex)); } vk::Extent2D getExtent() const { return this->swapchain->getExtent(); } diff --git a/engine/src/resources/pools/render_target_pool.hpp b/engine/src/resources/pools/render_target_pool.hpp index ff7ca0f..63a6afb 100644 --- a/engine/src/resources/pools/render_target_pool.hpp +++ b/engine/src/resources/pools/render_target_pool.hpp @@ -1,10 +1,10 @@ #pragma once #include +#include #include "device.hpp" #include "render_target.hpp" -#include "resources/pools/pool.hpp" namespace sophia { diff --git a/engine/src/resources/resource_registry.hpp b/engine/src/resources/resource_registry.hpp index 6dc5629..1acb28d 100644 --- a/engine/src/resources/resource_registry.hpp +++ b/engine/src/resources/resource_registry.hpp @@ -1,12 +1,11 @@ #pragma once #include +#include #include "device.hpp" #include "mesh.hpp" -#include "resources/pools/pool.hpp" #include "resources/pools/render_target_pool.hpp" -#include "scene.hpp" namespace sophia { @@ -21,7 +20,6 @@ class ResourceRegistry { Device& device; Pool meshPool{"Mesh"}; RenderTargetPool renderTargetPool; - Pool scenePool{"Scene"}; // TexturePool textures; // MaterialPool materials; }; diff --git a/engine/src/resources/resource_registry_ref.cpp b/engine/src/resources/resource_registry_ref.cpp index 5e24d7b..6d963a8 100644 --- a/engine/src/resources/resource_registry_ref.cpp +++ b/engine/src/resources/resource_registry_ref.cpp @@ -24,11 +24,6 @@ void ResourceRegistryRef::release(RenderTargetHandle handle) { registry->renderTargetPool.release(handle); } -template <> -void ResourceRegistryRef::release(SceneHandle handle) { - registry->scenePool.release(handle); -} - MeshHandle ResourceRegistryRef::createResource( std::type_identity, std::vector vertices, std::vector indices) { @@ -47,10 +42,6 @@ RenderTargetHandle ResourceRegistryRef::createResource( return registry->renderTargetPool.create(width, height); } -SceneHandle ResourceRegistryRef::createResource(std::type_identity) { - return registry->scenePool.create(); -} - void ResourceRegistryRef::requestResize(RenderTargetHandle handle, u32 width, u32 height) { registry->renderTargetPool.get(handle).requestResize(width, height); @@ -65,8 +56,4 @@ bool ResourceRegistryRef::isAlive(RenderTargetHandle handle) const { return registry->renderTargetPool.isAlive(handle); } -bool ResourceRegistryRef::isAlive(SceneHandle handle) const { - return registry->scenePool.isAlive(handle); -} - } // namespace sophia diff --git a/engine/src/scene.cpp b/engine/src/scene.cpp index 865868f..375424b 100644 --- a/engine/src/scene.cpp +++ b/engine/src/scene.cpp @@ -1,6 +1,5 @@ -#include "scene.hpp" - #include +#include #include namespace sophia { diff --git a/engine/src/scene.hpp b/engine/src/scene.hpp deleted file mode 100644 index ed40054..0000000 --- a/engine/src/scene.hpp +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include - -namespace sophia { - -class Scene { - public: - Scene(); - ~Scene(); - - private: - entt::registry registry; -}; - -} // namespace sophia diff --git a/engine/src/swapchain.hpp b/engine/src/swapchain.hpp index 8168700..0422aec 100644 --- a/engine/src/swapchain.hpp +++ b/engine/src/swapchain.hpp @@ -23,9 +23,6 @@ class Swapchain { Image& getImage(int index) const { return *this->images.at(index); } Image& getDepthImage(int index) const { return *this->depthImages.at(index); } - vk::ImageView getImageView(int index) const { - return this->images.at(index)->getView(); - } vk::ImageView getDepthImageView(int index) const { return this->depthImages.at(index)->getView(); } diff --git a/engine/src/ui/panel_registry.cpp b/engine/src/ui/panel_registry.cpp index 5b34ea9..f372ffd 100644 --- a/engine/src/ui/panel_registry.cpp +++ b/engine/src/ui/panel_registry.cpp @@ -1,88 +1,23 @@ #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()) { + pool.forEach([&count](const Panel& panel) { + if (panel.isVisible()) { count++; } - } + }); return count; } void PanelRegistry::update(const FrameInfo& frameInfo) { - for (auto& slot : slots) { - if (slot.panel && slot.panel->isVisible()) { - slot.panel->draw(frameInfo); + pool.forEach([&frameInfo](Panel& panel) { + if (panel.isVisible()) { + panel.draw(frameInfo); } - } + }); } } // namespace sophia From 2569eff1305b3208dff8a73a06a13903faf4e76a Mon Sep 17 00:00:00 2001 From: Caleb Burke Date: Mon, 21 Sep 2026 00:30:11 -0700 Subject: [PATCH 05/26] changes --- engine/src/image.cpp | 101 ++++++++----------------------- engine/src/image.hpp | 16 +++++ engine/src/rendering/access.cpp | 104 ++++++++++++++++++++++++++++++++ engine/src/rendering/access.hpp | 63 +++++++++++++++++++ 4 files changed, 208 insertions(+), 76 deletions(-) create mode 100644 engine/src/rendering/access.cpp create mode 100644 engine/src/rendering/access.hpp diff --git a/engine/src/image.cpp b/engine/src/image.cpp index e2b8a70..a54f08b 100644 --- a/engine/src/image.cpp +++ b/engine/src/image.cpp @@ -8,86 +8,35 @@ namespace sophia { -void Image::transitionLayout(vk::CommandBuffer commandBuffer, - vk::ImageLayout newLayout) { - vk::ImageMemoryBarrier barrier{}; - barrier.oldLayout = this->layout; - barrier.newLayout = newLayout; - barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.image = this->get(); - barrier.subresourceRange.aspectMask = this->aspectMask; - barrier.subresourceRange.baseMipLevel = 0; - barrier.subresourceRange.levelCount = this->mipLevels; - barrier.subresourceRange.baseArrayLayer = 0; - barrier.subresourceRange.layerCount = this->arrayLayers; +vk::ImageSubresourceRange Image::getSubresourceRange() const { + return vk::ImageSubresourceRange{this->aspectMask, 0, this->mipLevels, 0, + this->arrayLayers}; +} - vk::PipelineStageFlags srcStage; - vk::PipelineStageFlags dstStage; +AccessState Image::getAccessState() const { + AccessState state = getStateForLayout(this->layout); - if (this->layout == vk::ImageLayout::eUndefined && - newLayout == vk::ImageLayout::eTransferDstOptimal) { - barrier.srcAccessMask = {}; - barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite; - srcStage = vk::PipelineStageFlagBits::eTopOfPipe; - dstStage = vk::PipelineStageFlagBits::eTransfer; - } else if (this->layout == vk::ImageLayout::eTransferDstOptimal && - newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) { - barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; - barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead; - srcStage = vk::PipelineStageFlagBits::eTransfer; - dstStage = vk::PipelineStageFlagBits::eFragmentShader; - } else if (this->layout == vk::ImageLayout::eUndefined && - newLayout == vk::ImageLayout::eDepthStencilAttachmentOptimal) { - barrier.srcAccessMask = {}; - barrier.dstAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentRead | - vk::AccessFlagBits::eDepthStencilAttachmentWrite; - srcStage = vk::PipelineStageFlagBits::eTopOfPipe; - dstStage = vk::PipelineStageFlagBits::eEarlyFragmentTests; - } else if (this->layout == vk::ImageLayout::eUndefined && - newLayout == vk::ImageLayout::eColorAttachmentOptimal) { - barrier.srcAccessMask = {}; - barrier.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; - srcStage = vk::PipelineStageFlagBits::eTopOfPipe; - dstStage = vk::PipelineStageFlagBits::eColorAttachmentOutput; - } else if (this->layout == vk::ImageLayout::eUndefined && - newLayout == vk::ImageLayout::eGeneral) { - barrier.srcAccessMask = {}; - barrier.dstAccessMask = - vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite; - srcStage = vk::PipelineStageFlagBits::eTopOfPipe; - dstStage = vk::PipelineStageFlagBits::eComputeShader; - } else if (this->layout == vk::ImageLayout::eColorAttachmentOptimal && - newLayout == vk::ImageLayout::ePresentSrcKHR) { - barrier.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; - barrier.dstAccessMask = {}; - srcStage = vk::PipelineStageFlagBits::eColorAttachmentOutput; - dstStage = vk::PipelineStageFlagBits::eBottomOfPipe; - } else if (this->layout == vk::ImageLayout::eColorAttachmentOptimal && - newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) { - barrier.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; - barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead; - srcStage = vk::PipelineStageFlagBits::eColorAttachmentOutput; - dstStage = vk::PipelineStageFlagBits::eFragmentShader; - } else if (this->layout == vk::ImageLayout::eShaderReadOnlyOptimal && - newLayout == vk::ImageLayout::eColorAttachmentOptimal) { - barrier.srcAccessMask = vk::AccessFlagBits::eShaderRead; - barrier.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; - srcStage = vk::PipelineStageFlagBits::eFragmentShader; - dstStage = vk::PipelineStageFlagBits::eColorAttachmentOutput; - } else if (this->layout == vk::ImageLayout::ePresentSrcKHR && - newLayout == vk::ImageLayout::eColorAttachmentOptimal) { - barrier.srcAccessMask = {}; - barrier.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; - srcStage = vk::PipelineStageFlagBits::eTopOfPipe; - dstStage = vk::PipelineStageFlagBits::eColorAttachmentOutput; - } else { - log::fatal("Unsupported image layout transition."); - throw std::runtime_error("Unsupported image layout transition."); + // A wrapped (swapchain) image is handed to us by the acquire semaphore, so + // whatever we do next has to be ordered after that wait. + if (this->isExternal() && (this->layout == vk::ImageLayout::eUndefined || + this->layout == vk::ImageLayout::ePresentSrcKHR)) { + state.stage = SWAPCHAIN_ACQUIRE_STAGE; } - commandBuffer.pipelineBarrier(srcStage, dstStage, {}, nullptr, nullptr, - barrier); + return state; +} + +void Image::transitionLayout(vk::CommandBuffer commandBuffer, + vk::ImageLayout newLayout) { + AccessState from = this->getAccessState(); + AccessState to = getStateForLayout(newLayout); + + if (needsBarrier(from, to)) { + vk::ImageMemoryBarrier barrier = + makeImageBarrier(this->get(), this->getSubresourceRange(), from, to); + commandBuffer.pipelineBarrier(from.stage, to.stage, {}, nullptr, nullptr, + barrier); + } this->layout = newLayout; } diff --git a/engine/src/image.hpp b/engine/src/image.hpp index b93095f..af62af7 100644 --- a/engine/src/image.hpp +++ b/engine/src/image.hpp @@ -4,6 +4,8 @@ #include #include +#include "rendering/access.hpp" + namespace sophia { class Device; @@ -28,6 +30,20 @@ class Image { vk::Format getFormat() const { return this->format; } vk::Extent3D getExtent() const { return this->extent; } vk::ImageLayout getLayout() const { return this->layout; } + vk::ImageSubresourceRange getSubresourceRange() const; + + /** + * The layout this image was last left in, plus the stages and accesses that + * a barrier out of it has to wait on. + */ + AccessState getAccessState() const; + + /** + * Updates the tracked layout without recording a barrier. For code that + * does its own transitions (the RenderGraph) and needs to hand the final + * layout back. + */ + void recordLayout(vk::ImageLayout newLayout) { this->layout = newLayout; } void transitionLayout(vk::CommandBuffer commandBuffer, vk::ImageLayout newLayout); diff --git a/engine/src/rendering/access.cpp b/engine/src/rendering/access.cpp new file mode 100644 index 0000000..4dd417c --- /dev/null +++ b/engine/src/rendering/access.cpp @@ -0,0 +1,104 @@ +#include "rendering/access.hpp" + +#include +#include +#include + +namespace sophia { + +namespace { + +constexpr std::array ALL_ACCESSES = { + Access::ColorWrite, Access::DepthWrite, Access::SampledRead, + Access::StorageReadWrite, Access::TransferSrc, Access::TransferDst, + Access::Present, +}; + +constexpr vk::AccessFlags WRITE_ACCESSES = + vk::AccessFlagBits::eColorAttachmentWrite | + vk::AccessFlagBits::eDepthStencilAttachmentWrite | + vk::AccessFlagBits::eShaderWrite | vk::AccessFlagBits::eTransferWrite; + +} // namespace + +AccessState getAccessState(Access access) { + switch (access) { + case Access::ColorWrite: + return {vk::ImageLayout::eColorAttachmentOptimal, + vk::PipelineStageFlagBits::eColorAttachmentOutput, + vk::AccessFlagBits::eColorAttachmentRead | + vk::AccessFlagBits::eColorAttachmentWrite}; + case Access::DepthWrite: + return {vk::ImageLayout::eDepthStencilAttachmentOptimal, + vk::PipelineStageFlagBits::eEarlyFragmentTests | + vk::PipelineStageFlagBits::eLateFragmentTests, + vk::AccessFlagBits::eDepthStencilAttachmentRead | + vk::AccessFlagBits::eDepthStencilAttachmentWrite}; + case Access::SampledRead: + return {vk::ImageLayout::eShaderReadOnlyOptimal, + vk::PipelineStageFlagBits::eFragmentShader, + vk::AccessFlagBits::eShaderRead}; + case Access::StorageReadWrite: + return { + vk::ImageLayout::eGeneral, vk::PipelineStageFlagBits::eComputeShader, + vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite}; + case Access::TransferSrc: + return {vk::ImageLayout::eTransferSrcOptimal, + vk::PipelineStageFlagBits::eTransfer, + vk::AccessFlagBits::eTransferRead}; + case Access::TransferDst: + return {vk::ImageLayout::eTransferDstOptimal, + vk::PipelineStageFlagBits::eTransfer, + vk::AccessFlagBits::eTransferWrite}; + case Access::Present: + return {vk::ImageLayout::ePresentSrcKHR, + vk::PipelineStageFlagBits::eBottomOfPipe, vk::AccessFlags{}}; + } + + log::fatal("Unknown Access value."); + throw std::runtime_error("Unknown Access value."); +} + +AccessState getStateForLayout(vk::ImageLayout layout) { + if (layout == vk::ImageLayout::eUndefined) { + return AccessState{}; + } + + for (Access access : ALL_ACCESSES) { + AccessState state = getAccessState(access); + if (state.layout == layout) { + return state; + } + } + + log::fatal("Unsupported image layout:", vk::to_string(layout)); + throw std::runtime_error("Unsupported image layout."); +} + +bool writesImage(const AccessState& state) { + return static_cast(state.access & WRITE_ACCESSES); +} + +bool needsBarrier(const AccessState& from, const AccessState& to) { + return from.layout != to.layout || writesImage(from) || writesImage(to); +} + +vk::ImageMemoryBarrier makeImageBarrier(vk::Image image, + const vk::ImageSubresourceRange& range, + const AccessState& from, + const AccessState& to) { + vk::ImageMemoryBarrier barrier{}; + barrier.oldLayout = from.layout; + barrier.newLayout = to.layout; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = image; + barrier.subresourceRange = range; + // Only writes need to be made available; reads just need the execution + // dependency the stage masks already give. + barrier.srcAccessMask = from.access & WRITE_ACCESSES; + barrier.dstAccessMask = to.access; + return barrier; +} + +} // namespace sophia diff --git a/engine/src/rendering/access.hpp b/engine/src/rendering/access.hpp new file mode 100644 index 0000000..67c35f4 --- /dev/null +++ b/engine/src/rendering/access.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include + +namespace sophia { + +/** + * How an image is used at a point in a frame. Each value implies an image + * layout plus the pipeline stages and memory accesses that go with it, so a + * barrier between any two of them can be derived instead of hand-written. + */ +enum class Access { + ColorWrite, + DepthWrite, + SampledRead, + StorageReadWrite, + TransferSrc, + TransferDst, + Present, +}; + +/** + * The layout, stages and accesses an image is in after some use. Barriers go + * from one of these to another. + */ +struct AccessState { + vk::ImageLayout layout = vk::ImageLayout::eUndefined; + vk::PipelineStageFlags stage = vk::PipelineStageFlagBits::eTopOfPipe; + vk::AccessFlags access; +}; + +/** + * The stage RenderContext waits on for the swapchain image-available + * semaphore. A barrier that takes a swapchain image out of its + * just-acquired state has to use this as its source stage, otherwise the + * layout transition isn't ordered after the acquire. + */ +inline constexpr vk::PipelineStageFlags SWAPCHAIN_ACQUIRE_STAGE = + vk::PipelineStageFlagBits::eColorAttachmentOutput; + +AccessState getAccessState(Access access); + +/** + * The state of an image sitting in `layout` with no outstanding work. + * eUndefined maps to "nothing to wait on"; layouts with no matching Access + * throw. + */ +AccessState getStateForLayout(vk::ImageLayout layout); + +bool writesImage(const AccessState& state); + +/** + * Whether going from `from` to `to` needs a barrier: a layout change, or any + * hazard involving a write. Read-after-read in the same layout doesn't. + */ +bool needsBarrier(const AccessState& from, const AccessState& to); + +vk::ImageMemoryBarrier makeImageBarrier(vk::Image image, + const vk::ImageSubresourceRange& range, + const AccessState& from, + const AccessState& to); + +} // namespace sophia From 8fbabda095e6f693ee8e7d338f7cc5eeb30ca683 Mon Sep 17 00:00:00 2001 From: Caleb Burke Date: Mon, 21 Sep 2026 01:03:16 -0700 Subject: [PATCH 06/26] Render Graph concept started! --- engine/include/sophia/components.hpp | 32 ++++++ engine/src/application.cpp | 72 +++++++----- engine/src/rendering/render_graph.cpp | 159 ++++++++++++++++++++++++++ engine/src/rendering/render_graph.hpp | 147 ++++++++++++++++++++++++ 4 files changed, 380 insertions(+), 30 deletions(-) create mode 100644 engine/src/rendering/render_graph.cpp create mode 100644 engine/src/rendering/render_graph.hpp diff --git a/engine/include/sophia/components.hpp b/engine/include/sophia/components.hpp index ac37353..979c929 100644 --- a/engine/include/sophia/components.hpp +++ b/engine/include/sophia/components.hpp @@ -34,4 +34,36 @@ struct MeshComponent { MeshHandle handle; }; +struct CameraComponent { + struct Orthographic { + float height = 10.f; + float nearPlane = -1.f; + float farPlane = 1.f; + }; + + struct Perspective { + float fov = glm::radians(60.f); + float nearPlane = 0.1f; + float farPlane = 100.f; + }; + + enum class Projection { Orthographic, Perspective }; + + Projection projection = Projection::Orthographic; + Orthographic ortho; + Perspective perspective; + + glm::mat4 getProjection(float aspect) const { + if (projection == Projection::Perspective) { + return glm::perspective(perspective.fov, aspect, perspective.nearPlane, + perspective.farPlane); + } + + float halfHeight = ortho.height / 2.f; + float halfWidth = halfHeight * aspect; + return glm::ortho(-halfWidth, halfWidth, -halfHeight, halfHeight, + ortho.nearPlane, ortho.farPlane); + } +}; + } // namespace sophia diff --git a/engine/src/application.cpp b/engine/src/application.cpp index ed843e7..19b58fb 100644 --- a/engine/src/application.cpp +++ b/engine/src/application.cpp @@ -21,6 +21,8 @@ #include "pipelines/graphics_pipeline.hpp" #include "render_context.hpp" #include "render_target.hpp" +#include "rendering/render_graph.hpp" +#include "resources/resource_registry.hpp" #include "shader.hpp" #include "systems/model_render_system.hpp" #include "window.hpp" @@ -42,11 +44,12 @@ struct PushConstants { std::vector commandLineArgs; +constexpr ClearColor CLEAR_COLOR = {0.01f, 0.01f, 0.02f, 1.0f}; + void beginColorPass(vk::CommandBuffer commandBuffer, vk::ImageView view, vk::Extent2D extent) { vk::ClearValue clearValue{}; - clearValue.color = - vk::ClearColorValue{std::array{0.01f, 0.01f, 0.02f, 1.0f}}; + clearValue.color = vk::ClearColorValue{CLEAR_COLOR}; vk::RenderingAttachmentInfo colorAttachment{}; colorAttachment.imageView = view; @@ -83,13 +86,14 @@ class Application::Impl { config{config}, window{config.width, config.height, config.name}, device{&window}, - renderContext{window, device} {} + renderContext{window, device}, + resourceRegistry{device, renderContext.getImageFormat()} {} void run() { ModelRenderSystem modelRenderSystem{device, renderContext}; ImGuiRenderer imguiRenderer{window, device, renderContext.getImageFormat(), renderContext.getImageCount(), - this->application.resourceRegistry}; + this->resourceRegistry}; this->application._ui.renderer = &imguiRenderer; this->application.consoleCommandRegistry.registerCommand( @@ -100,8 +104,13 @@ class Application::Impl { }); this->application.onCreate(); - imguiRenderer.setDefaultLayout( - this->application.panelRegistry.getDefaultLayout()); + + auto& meshPool = this->resourceRegistry.meshPool; + auto& renderTargetPool = this->resourceRegistry.renderTargetPool; + + PanelRegistry& panelRegistry = this->application.panelRegistry; + + imguiRenderer.setDefaultLayout(panelRegistry.getDefaultLayout()); auto startTime = std::chrono::high_resolution_clock::now(); auto currentTime = startTime; @@ -120,12 +129,11 @@ class Application::Impl { // Resizing waits for the GPU, so it has to happen before any of this // frame's work (or ImGui's references to the targets) is recorded. - this->application.resourceRegistry.applyPendingResizes(); + renderTargetPool.applyPendingResizes(); RenderTarget* sceneTarget = this->sceneTargetHandle.isValid() - ? &this->application.resourceRegistry.getRenderTarget( - this->sceneTargetHandle) + ? &renderTargetPool.get(this->sceneTargetHandle) : nullptr; vk::Extent2D extent = renderContext.getExtent(); @@ -149,7 +157,7 @@ class Application::Impl { imguiRenderer.beginFrame(); - this->application.panelRegistry.update(frameInfo); + panelRegistry.update(frameInfo); this->application.onRender(); std::vector instances; @@ -172,21 +180,25 @@ class Application::Impl { vk::ImageLayout::eShaderReadOnlyOptimal); } - renderContext.transitionCurrentImage( - commandBuffer, vk::ImageLayout::eColorAttachmentOptimal); - beginColorPass(commandBuffer, renderContext.getCurrentImageView(), - extent); + RenderGraph graph; + ImageRef backbuffer = + graph.importImage(renderContext.getCurrentImage(), Access::Present); - if (!sceneTarget) { - modelRenderSystem.render(commandBuffer, instances, frameInfo); - } + graph.addPass( + "main", + [&](RenderGraph::PassBuilder& pass) { + pass.writeColor(backbuffer, vk::AttachmentLoadOp::eClear, + CLEAR_COLOR); + }, + [&](vk::CommandBuffer commandBuffer) { + if (!sceneTarget) { + modelRenderSystem.render(commandBuffer, instances, frameInfo); + } - imguiRenderer.endFrame(commandBuffer); + imguiRenderer.endFrame(commandBuffer); + }); - commandBuffer.endRendering(); - - renderContext.transitionCurrentImage(commandBuffer, - vk::ImageLayout::ePresentSrcKHR); + graph.execute(commandBuffer); renderContext.endFrame(); @@ -232,11 +244,7 @@ class Application::Impl { this->sceneTargetHandle = handle; } - u32 getColorFormat() const { - return static_cast(this->renderContext.getImageFormat()); - } - - Device& getDevice() { return device; } + ResourceRegistry& getResourceRegistry() { return resourceRegistry; } private: Application& application; @@ -244,6 +252,7 @@ class Application::Impl { Window window; Device device; RenderContext renderContext; + ResourceRegistry resourceRegistry; std::vector> meshes; std::vector drawCalls; @@ -259,14 +268,17 @@ Application::Application(const Config& config) : impl{std::make_unique(*this, config)}, canvas{this}, args{std::move(commandLineArgs)}, - resourceRegistry{impl->getDevice(), impl->getColorFormat()}, - _ui{resourceRegistry}, + _ui{ResourceRegistryRef{impl->getResourceRegistry()}}, _debugUI{this->panelRegistry, this->consoleCommandRegistry} {} Application::~Application() = default; void Application::run() { impl->run(); } +ResourceRegistryRef Application::resources() { + return ResourceRegistryRef{impl->getResourceRegistry()}; +} + void Application::pushMesh(std::vector vertices, std::vector indices) { impl->pushMesh(std::move(vertices), std::move(indices)); @@ -279,7 +291,7 @@ void Application::setSceneTarget(RenderTargetHandle handle) { void Application::clearMeshes() { impl->clearMeshes(); } void Application::drawMesh(MeshHandle handle, const glm::mat4& transform) { - impl->drawMesh(resourceRegistry.get(handle), transform); + impl->drawMesh(impl->getResourceRegistry().meshPool.get(handle), transform); } } // namespace sophia diff --git a/engine/src/rendering/render_graph.cpp b/engine/src/rendering/render_graph.cpp new file mode 100644 index 0000000..b9a1e60 --- /dev/null +++ b/engine/src/rendering/render_graph.cpp @@ -0,0 +1,159 @@ +#include "rendering/render_graph.hpp" + +#include +#include + +namespace sophia { + +ImageRef RenderGraph::importImage(Image& image, Access finalAccess) { + ImageRef ref{static_cast(this->images.size())}; + this->images.push_back({&image, finalAccess, image.getAccessState()}); + return ref; +} + +void RenderGraph::addPass(std::string name, const SetupFn& setup, + ExecuteFn execute) { + Pass& pass = this->passes.emplace_back(); + pass.name = std::move(name); + pass.execute = std::move(execute); + + PassBuilder builder{*this, pass}; + setup(builder); +} + +void RenderGraph::execute(vk::CommandBuffer commandBuffer) { + for (Pass& pass : this->passes) { + this->recordPass(commandBuffer, pass); + } + + PendingBarriers pending; + for (u32 i = 0; i < this->images.size(); i++) { + this->transitionTo(ImageRef{i}, getAccessState(this->images[i].finalAccess), + pending); + } + this->flush(commandBuffer, pending); + + for (ImportedImage& imported : this->images) { + imported.image->recordLayout(imported.state.layout); + } + + this->passes.clear(); +} + +void RenderGraph::transitionTo(ImageRef ref, const AccessState& next, + PendingBarriers& pending) { + SOPHIA_ASSERT(ref.isValid() && ref.index < this->images.size(), + "ImageRef doesn't belong to this graph"); + + ImportedImage& imported = this->images[ref.index]; + + if (needsBarrier(imported.state, next)) { + pending.barriers.push_back(makeImageBarrier( + imported.image->get(), imported.image->getSubresourceRange(), + imported.state, next)); + pending.srcStages |= imported.state.stage; + pending.dstStages |= next.stage; + imported.state = next; + } else { + // Same-layout read after read. Nothing to record, but a later write has + // to wait on this reader too. + imported.state.stage |= next.stage; + imported.state.access |= next.access; + } +} + +void RenderGraph::flush(vk::CommandBuffer commandBuffer, + PendingBarriers& pending) { + if (pending.barriers.empty()) { + return; + } + + commandBuffer.pipelineBarrier(pending.srcStages, pending.dstStages, {}, + nullptr, nullptr, pending.barriers); + pending = PendingBarriers{}; +} + +void RenderGraph::recordPass(vk::CommandBuffer commandBuffer, Pass& pass) { + PendingBarriers pending; + for (const ImageUse& use : pass.uses) { + this->transitionTo(use.image, getAccessState(use.access), pending); + } + this->flush(commandBuffer, pending); + + if (pass.colorAttachments.empty()) { + pass.execute(commandBuffer); + return; + } + + std::vector attachments; + attachments.reserve(pass.colorAttachments.size()); + + vk::Extent3D imageExtent = + this->images[pass.colorAttachments.front().image.index] + .image->getExtent(); + vk::Extent2D extent{imageExtent.width, imageExtent.height}; + + for (const ColorAttachment& color : pass.colorAttachments) { + const Image& image = *this->images[color.image.index].image; + SOPHIA_ASSERT(image.getExtent().width == extent.width && + image.getExtent().height == extent.height, + "Pass", pass.name, + "has color attachments of different sizes"); + + vk::RenderingAttachmentInfo attachment{}; + attachment.imageView = image.getView(); + attachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; + attachment.loadOp = color.loadOp; + attachment.storeOp = vk::AttachmentStoreOp::eStore; + attachment.clearValue.color = vk::ClearColorValue{color.clearColor}; + attachments.push_back(attachment); + } + + vk::RenderingInfo renderingInfo{}; + renderingInfo.renderArea = vk::Rect2D{{0, 0}, extent}; + renderingInfo.layerCount = 1; + renderingInfo.colorAttachmentCount = static_cast(attachments.size()); + renderingInfo.pColorAttachments = attachments.data(); + + commandBuffer.beginRendering(renderingInfo); + + vk::Viewport viewport{0.0f, + 0.0f, + static_cast(extent.width), + static_cast(extent.height), + 0.0f, + 1.0f}; + vk::Rect2D scissor{{0, 0}, extent}; + commandBuffer.setViewport(0, viewport); + commandBuffer.setScissor(0, scissor); + + pass.execute(commandBuffer); + + commandBuffer.endRendering(); +} + +RenderGraph::PassBuilder& RenderGraph::PassBuilder::writeColor( + ImageRef image, vk::AttachmentLoadOp loadOp, ClearColor clearColor) { + this->use(image, Access::ColorWrite); + this->pass.colorAttachments.push_back({image, loadOp, clearColor}); + return *this; +} + +RenderGraph::PassBuilder& RenderGraph::PassBuilder::sample(ImageRef image) { + return this->use(image, Access::SampledRead); +} + +RenderGraph::PassBuilder& RenderGraph::PassBuilder::use(ImageRef image, + Access access) { + SOPHIA_ASSERT(image.isValid() && image.index < this->graph.images.size(), + "ImageRef doesn't belong to this graph"); + for (const ImageUse& existing : this->pass.uses) { + SOPHIA_ASSERT(existing.image.index != image.index, "Pass", this->pass.name, + "uses the same image twice"); + } + + this->pass.uses.push_back({image, access}); + return *this; +} + +} // namespace sophia diff --git a/engine/src/rendering/render_graph.hpp b/engine/src/rendering/render_graph.hpp new file mode 100644 index 0000000..17ce0fb --- /dev/null +++ b/engine/src/rendering/render_graph.hpp @@ -0,0 +1,147 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "image.hpp" +#include "rendering/access.hpp" + +namespace sophia { + +/** + * Names an image within one RenderGraph. Only valid for the graph that + * returned it. + */ +struct ImageRef { + u32 index = INVALID_INDEX; + + static constexpr u32 INVALID_INDEX = 0xFFFFFFFF; + + bool isValid() const { return this->index != INVALID_INDEX; } +}; + +using ClearColor = std::array; + +/** + * A frame's passes and the images they touch, recorded into a single command + * buffer. Passes run in the order they were added; the graph's job is to + * work out the barriers and layout transitions between them from what each + * pass says it does to each image, and to begin/end rendering for passes that + * write color attachments. + * + * Built fresh every frame: + * + * RenderGraph graph; + * ImageRef backbuffer = graph.importImage(swapchainImage, Access::Present); + * graph.addPass( + * "main", [&](PassBuilder& pass) { pass.writeColor(backbuffer); }, + * [&](vk::CommandBuffer cb) { draw(cb); }); + * graph.execute(commandBuffer); + * + * Imported images start in whatever layout Image reports and are left in + * their final access when execute() returns, with the Image's tracked layout + * updated to match. + */ +class RenderGraph { + public: + class PassBuilder; + + using SetupFn = std::function; + using ExecuteFn = std::function; + + RenderGraph() = default; + + RenderGraph(const RenderGraph&) = delete; + RenderGraph& operator=(const RenderGraph&) = delete; + + /** + * Brings an image the graph doesn't own into it. It must outlive + * execute(). After the last pass it is transitioned to `finalAccess`. + */ + ImageRef importImage(Image& image, Access finalAccess); + + /** + * Adds a pass. `setup` runs immediately and declares what the pass does to + * images; `execute` runs later, from execute(), with the barriers already + * recorded and (if the pass writes color) rendering begun. + */ + void addPass(std::string name, const SetupFn& setup, ExecuteFn execute); + + /** + * Records every pass, and the barriers between them, into `commandBuffer`. + */ + void execute(vk::CommandBuffer commandBuffer); + + private: + struct ImageUse { + ImageRef image; + Access access; + }; + + struct ColorAttachment { + ImageRef image; + vk::AttachmentLoadOp loadOp; + ClearColor clearColor; + }; + + struct Pass { + std::string name; + std::vector uses; + std::vector colorAttachments; + ExecuteFn execute; + }; + + struct ImportedImage { + Image* image; + Access finalAccess; + // What the image was last left in, as of the barriers recorded so far. + AccessState state; + }; + + struct PendingBarriers { + std::vector barriers; + vk::PipelineStageFlags srcStages; + vk::PipelineStageFlags dstStages; + }; + + void transitionTo(ImageRef ref, const AccessState& next, + PendingBarriers& pending); + void flush(vk::CommandBuffer commandBuffer, PendingBarriers& pending); + void recordPass(vk::CommandBuffer commandBuffer, Pass& pass); + + std::vector images; + std::vector passes; +}; + +class RenderGraph::PassBuilder { + public: + /** + * Renders into `image` as a color attachment. Passes with one or more of + * these get rendering begun (and viewport/scissor set to the attachment's + * extent) before their execute callback runs. + */ + PassBuilder& writeColor( + ImageRef image, + vk::AttachmentLoadOp loadOp = vk::AttachmentLoadOp::eClear, + ClearColor clearColor = {0.f, 0.f, 0.f, 1.f}); + + /** Samples `image` in a shader. */ + PassBuilder& sample(ImageRef image); + + /** Any other use, e.g. storage or transfer access. */ + PassBuilder& use(ImageRef image, Access access); + + private: + friend class RenderGraph; + + PassBuilder(RenderGraph& graph, Pass& pass) : graph{graph}, pass{pass} {} + + RenderGraph& graph; + Pass& pass; +}; + +} // namespace sophia From 650b4d813ff43d9dde3ca9bb423717c1bda6b77a Mon Sep 17 00:00:00 2001 From: Caleb Burke Date: Mon, 21 Sep 2026 01:10:13 -0700 Subject: [PATCH 07/26] moving files to better dirs --- engine/src/application.cpp | 10 +++++----- engine/src/{ => rendering}/imgui_renderer.cpp | 4 ++-- engine/src/{ => rendering}/imgui_renderer.hpp | 0 .../src/{systems => rendering}/model_render_system.cpp | 2 +- .../src/{systems => rendering}/model_render_system.hpp | 4 ++-- engine/src/{ => rendering}/render_context.cpp | 2 +- engine/src/{ => rendering}/render_context.hpp | 0 engine/src/{ => resources}/mesh.cpp | 2 +- engine/src/{ => resources}/mesh.hpp | 0 engine/src/resources/pools/render_target_pool.hpp | 2 +- engine/src/{ => resources}/render_target.cpp | 2 +- engine/src/{ => resources}/render_target.hpp | 0 engine/src/resources/resource_registry.hpp | 2 +- engine/src/ui/ui.cpp | 2 +- 14 files changed, 16 insertions(+), 16 deletions(-) rename engine/src/{ => rendering}/imgui_renderer.cpp (98%) rename engine/src/{ => rendering}/imgui_renderer.hpp (100%) rename engine/src/{systems => rendering}/model_render_system.cpp (98%) rename engine/src/{systems => rendering}/model_render_system.hpp (93%) rename engine/src/{ => rendering}/render_context.cpp (99%) rename engine/src/{ => rendering}/render_context.hpp (100%) rename engine/src/{ => resources}/mesh.cpp (99%) rename engine/src/{ => resources}/mesh.hpp (100%) rename engine/src/{ => resources}/render_target.cpp (97%) rename engine/src/{ => resources}/render_target.hpp (100%) diff --git a/engine/src/application.cpp b/engine/src/application.cpp index 19b58fb..405c4d4 100644 --- a/engine/src/application.cpp +++ b/engine/src/application.cpp @@ -16,15 +16,15 @@ #include "buffer.hpp" #include "device.hpp" -#include "imgui_renderer.hpp" -#include "mesh.hpp" #include "pipelines/graphics_pipeline.hpp" -#include "render_context.hpp" -#include "render_target.hpp" +#include "rendering/imgui_renderer.hpp" +#include "rendering/model_render_system.hpp" +#include "rendering/render_context.hpp" #include "rendering/render_graph.hpp" +#include "resources/mesh.hpp" +#include "resources/render_target.hpp" #include "resources/resource_registry.hpp" #include "shader.hpp" -#include "systems/model_render_system.hpp" #include "window.hpp" namespace sophia { diff --git a/engine/src/imgui_renderer.cpp b/engine/src/rendering/imgui_renderer.cpp similarity index 98% rename from engine/src/imgui_renderer.cpp rename to engine/src/rendering/imgui_renderer.cpp index de739d9..d458938 100644 --- a/engine/src/imgui_renderer.cpp +++ b/engine/src/rendering/imgui_renderer.cpp @@ -1,11 +1,11 @@ -#include "imgui_renderer.hpp" +#include "rendering/imgui_renderer.hpp" #include #include #include #include -#include "render_target.hpp" +#include "resources/render_target.hpp" namespace sophia { diff --git a/engine/src/imgui_renderer.hpp b/engine/src/rendering/imgui_renderer.hpp similarity index 100% rename from engine/src/imgui_renderer.hpp rename to engine/src/rendering/imgui_renderer.hpp diff --git a/engine/src/systems/model_render_system.cpp b/engine/src/rendering/model_render_system.cpp similarity index 98% rename from engine/src/systems/model_render_system.cpp rename to engine/src/rendering/model_render_system.cpp index 699602c..a0a796f 100644 --- a/engine/src/systems/model_render_system.cpp +++ b/engine/src/rendering/model_render_system.cpp @@ -1,4 +1,4 @@ -#include "model_render_system.hpp" +#include "rendering/model_render_system.hpp" #include diff --git a/engine/src/systems/model_render_system.hpp b/engine/src/rendering/model_render_system.hpp similarity index 93% rename from engine/src/systems/model_render_system.hpp rename to engine/src/rendering/model_render_system.hpp index 93d70b8..03b1b6d 100644 --- a/engine/src/systems/model_render_system.hpp +++ b/engine/src/rendering/model_render_system.hpp @@ -5,9 +5,9 @@ #include #include "device.hpp" -#include "mesh.hpp" #include "pipelines/graphics_pipeline.hpp" -#include "render_context.hpp" +#include "rendering/render_context.hpp" +#include "resources/mesh.hpp" namespace sophia { diff --git a/engine/src/render_context.cpp b/engine/src/rendering/render_context.cpp similarity index 99% rename from engine/src/render_context.cpp rename to engine/src/rendering/render_context.cpp index c07dd0e..891f7f0 100644 --- a/engine/src/render_context.cpp +++ b/engine/src/rendering/render_context.cpp @@ -1,4 +1,4 @@ -#include "render_context.hpp" +#include "rendering/render_context.hpp" #include #include diff --git a/engine/src/render_context.hpp b/engine/src/rendering/render_context.hpp similarity index 100% rename from engine/src/render_context.hpp rename to engine/src/rendering/render_context.hpp diff --git a/engine/src/mesh.cpp b/engine/src/resources/mesh.cpp similarity index 99% rename from engine/src/mesh.cpp rename to engine/src/resources/mesh.cpp index b865bad..eba9e28 100644 --- a/engine/src/mesh.cpp +++ b/engine/src/resources/mesh.cpp @@ -1,4 +1,4 @@ -#include "mesh.hpp" +#include "resources/mesh.hpp" #include #include diff --git a/engine/src/mesh.hpp b/engine/src/resources/mesh.hpp similarity index 100% rename from engine/src/mesh.hpp rename to engine/src/resources/mesh.hpp diff --git a/engine/src/resources/pools/render_target_pool.hpp b/engine/src/resources/pools/render_target_pool.hpp index 63a6afb..d86d62a 100644 --- a/engine/src/resources/pools/render_target_pool.hpp +++ b/engine/src/resources/pools/render_target_pool.hpp @@ -4,7 +4,7 @@ #include #include "device.hpp" -#include "render_target.hpp" +#include "resources/render_target.hpp" namespace sophia { diff --git a/engine/src/render_target.cpp b/engine/src/resources/render_target.cpp similarity index 97% rename from engine/src/render_target.cpp rename to engine/src/resources/render_target.cpp index 68dfc0b..0005943 100644 --- a/engine/src/render_target.cpp +++ b/engine/src/resources/render_target.cpp @@ -1,4 +1,4 @@ -#include "render_target.hpp" +#include "resources/render_target.hpp" #include diff --git a/engine/src/render_target.hpp b/engine/src/resources/render_target.hpp similarity index 100% rename from engine/src/render_target.hpp rename to engine/src/resources/render_target.hpp diff --git a/engine/src/resources/resource_registry.hpp b/engine/src/resources/resource_registry.hpp index 1acb28d..8e94a63 100644 --- a/engine/src/resources/resource_registry.hpp +++ b/engine/src/resources/resource_registry.hpp @@ -4,7 +4,7 @@ #include #include "device.hpp" -#include "mesh.hpp" +#include "resources/mesh.hpp" #include "resources/pools/render_target_pool.hpp" namespace sophia { diff --git a/engine/src/ui/ui.cpp b/engine/src/ui/ui.cpp index 8a08da7..164722a 100644 --- a/engine/src/ui/ui.cpp +++ b/engine/src/ui/ui.cpp @@ -1,7 +1,7 @@ #include #include -#include "imgui_renderer.hpp" +#include "rendering/imgui_renderer.hpp" namespace sophia { From 4d037e1250b5dbc5bca523778ab108a50b7ca124 Mon Sep 17 00:00:00 2001 From: Caleb Burke Date: Mon, 21 Sep 2026 01:15:58 -0700 Subject: [PATCH 08/26] moving stuff around --- engine/src/application.cpp | 8 ++++---- engine/src/rendering/imgui_renderer.hpp | 2 +- engine/src/rendering/model_render_system.hpp | 4 ++-- engine/src/rendering/render_context.cpp | 2 +- engine/src/rendering/render_context.hpp | 4 ++-- engine/src/rendering/render_graph.hpp | 4 ++-- engine/src/resources/mesh.hpp | 4 ++-- engine/src/resources/pools/render_target_pool.hpp | 2 +- engine/src/resources/render_target.hpp | 4 ++-- engine/src/resources/resource_registry.hpp | 2 +- engine/src/{rendering => vulkan}/access.cpp | 2 +- engine/src/{rendering => vulkan}/access.hpp | 0 engine/src/{ => vulkan}/buffer.cpp | 2 +- engine/src/{ => vulkan}/buffer.hpp | 2 +- engine/src/{ => vulkan}/descriptors/descriptor_pool.cpp | 0 engine/src/{ => vulkan}/descriptors/descriptor_pool.hpp | 2 +- .../{ => vulkan}/descriptors/descriptor_set_layout.cpp | 0 .../{ => vulkan}/descriptors/descriptor_set_layout.hpp | 2 +- engine/src/{ => vulkan}/descriptors/descriptor_writer.cpp | 0 engine/src/{ => vulkan}/descriptors/descriptor_writer.hpp | 2 +- engine/src/{ => vulkan}/device.cpp | 2 +- engine/src/{ => vulkan}/device.hpp | 0 engine/src/{ => vulkan}/image.cpp | 4 ++-- engine/src/{ => vulkan}/image.hpp | 2 +- engine/src/{ => vulkan}/pipelines/compute_pipeline.cpp | 0 engine/src/{ => vulkan}/pipelines/compute_pipeline.hpp | 4 ++-- engine/src/{ => vulkan}/pipelines/graphics_pipeline.cpp | 0 engine/src/{ => vulkan}/pipelines/graphics_pipeline.hpp | 4 ++-- engine/src/{ => vulkan}/pipelines/pipeline.cpp | 0 engine/src/{ => vulkan}/pipelines/pipeline.hpp | 2 +- engine/src/{ => vulkan}/shader.cpp | 2 +- engine/src/{ => vulkan}/shader.hpp | 0 engine/src/{ => vulkan}/swapchain.cpp | 2 +- engine/src/{ => vulkan}/swapchain.hpp | 4 ++-- 34 files changed, 37 insertions(+), 37 deletions(-) rename engine/src/{rendering => vulkan}/access.cpp (99%) rename engine/src/{rendering => vulkan}/access.hpp (100%) rename engine/src/{ => vulkan}/buffer.cpp (99%) rename engine/src/{ => vulkan}/buffer.hpp (98%) rename engine/src/{ => vulkan}/descriptors/descriptor_pool.cpp (100%) rename engine/src/{ => vulkan}/descriptors/descriptor_pool.hpp (97%) rename engine/src/{ => vulkan}/descriptors/descriptor_set_layout.cpp (100%) rename engine/src/{ => vulkan}/descriptors/descriptor_set_layout.hpp (97%) rename engine/src/{ => vulkan}/descriptors/descriptor_writer.cpp (100%) rename engine/src/{ => vulkan}/descriptors/descriptor_writer.hpp (95%) rename engine/src/{ => vulkan}/device.cpp (99%) rename engine/src/{ => vulkan}/device.hpp (100%) rename engine/src/{ => vulkan}/image.cpp (99%) rename engine/src/{ => vulkan}/image.hpp (99%) rename engine/src/{ => vulkan}/pipelines/compute_pipeline.cpp (100%) rename engine/src/{ => vulkan}/pipelines/compute_pipeline.hpp (92%) rename engine/src/{ => vulkan}/pipelines/graphics_pipeline.cpp (100%) rename engine/src/{ => vulkan}/pipelines/graphics_pipeline.hpp (98%) rename engine/src/{ => vulkan}/pipelines/pipeline.cpp (100%) rename engine/src/{ => vulkan}/pipelines/pipeline.hpp (95%) rename engine/src/{ => vulkan}/shader.cpp (99%) rename engine/src/{ => vulkan}/shader.hpp (100%) rename engine/src/{ => vulkan}/swapchain.cpp (99%) rename engine/src/{ => vulkan}/swapchain.hpp (97%) diff --git a/engine/src/application.cpp b/engine/src/application.cpp index 405c4d4..1fd3776 100644 --- a/engine/src/application.cpp +++ b/engine/src/application.cpp @@ -14,9 +14,6 @@ #include -#include "buffer.hpp" -#include "device.hpp" -#include "pipelines/graphics_pipeline.hpp" #include "rendering/imgui_renderer.hpp" #include "rendering/model_render_system.hpp" #include "rendering/render_context.hpp" @@ -24,7 +21,10 @@ #include "resources/mesh.hpp" #include "resources/render_target.hpp" #include "resources/resource_registry.hpp" -#include "shader.hpp" +#include "vulkan/buffer.hpp" +#include "vulkan/device.hpp" +#include "vulkan/pipelines/graphics_pipeline.hpp" +#include "vulkan/shader.hpp" #include "window.hpp" namespace sophia { diff --git a/engine/src/rendering/imgui_renderer.hpp b/engine/src/rendering/imgui_renderer.hpp index cfc79a1..5ee558a 100644 --- a/engine/src/rendering/imgui_renderer.hpp +++ b/engine/src/rendering/imgui_renderer.hpp @@ -7,8 +7,8 @@ #include #include -#include "device.hpp" #include "resources/resource_registry.hpp" +#include "vulkan/device.hpp" #include "window.hpp" namespace sophia { diff --git a/engine/src/rendering/model_render_system.hpp b/engine/src/rendering/model_render_system.hpp index 03b1b6d..b3d7470 100644 --- a/engine/src/rendering/model_render_system.hpp +++ b/engine/src/rendering/model_render_system.hpp @@ -4,10 +4,10 @@ #include #include -#include "device.hpp" -#include "pipelines/graphics_pipeline.hpp" #include "rendering/render_context.hpp" #include "resources/mesh.hpp" +#include "vulkan/device.hpp" +#include "vulkan/pipelines/graphics_pipeline.hpp" namespace sophia { diff --git a/engine/src/rendering/render_context.cpp b/engine/src/rendering/render_context.cpp index 891f7f0..e57ace9 100644 --- a/engine/src/rendering/render_context.cpp +++ b/engine/src/rendering/render_context.cpp @@ -5,7 +5,7 @@ #include #include -#include "rendering/access.hpp" +#include "vulkan/access.hpp" namespace sophia { diff --git a/engine/src/rendering/render_context.hpp b/engine/src/rendering/render_context.hpp index afe4728..25001e1 100644 --- a/engine/src/rendering/render_context.hpp +++ b/engine/src/rendering/render_context.hpp @@ -6,8 +6,8 @@ #include #include -#include "device.hpp" -#include "swapchain.hpp" +#include "vulkan/device.hpp" +#include "vulkan/swapchain.hpp" #include "window.hpp" namespace sophia { diff --git a/engine/src/rendering/render_graph.hpp b/engine/src/rendering/render_graph.hpp index 17ce0fb..d7374e3 100644 --- a/engine/src/rendering/render_graph.hpp +++ b/engine/src/rendering/render_graph.hpp @@ -7,8 +7,8 @@ #include #include -#include "image.hpp" -#include "rendering/access.hpp" +#include "vulkan/access.hpp" +#include "vulkan/image.hpp" namespace sophia { diff --git a/engine/src/resources/mesh.hpp b/engine/src/resources/mesh.hpp index f25f7db..3c64513 100644 --- a/engine/src/resources/mesh.hpp +++ b/engine/src/resources/mesh.hpp @@ -9,8 +9,8 @@ #define GLM_FORCE_DEPTH_ZERO_TO_ONE #include -#include "buffer.hpp" -#include "device.hpp" +#include "vulkan/buffer.hpp" +#include "vulkan/device.hpp" namespace sophia { diff --git a/engine/src/resources/pools/render_target_pool.hpp b/engine/src/resources/pools/render_target_pool.hpp index d86d62a..7a8b870 100644 --- a/engine/src/resources/pools/render_target_pool.hpp +++ b/engine/src/resources/pools/render_target_pool.hpp @@ -3,8 +3,8 @@ #include #include -#include "device.hpp" #include "resources/render_target.hpp" +#include "vulkan/device.hpp" namespace sophia { diff --git a/engine/src/resources/render_target.hpp b/engine/src/resources/render_target.hpp index c73d4a1..fd4f4f8 100644 --- a/engine/src/resources/render_target.hpp +++ b/engine/src/resources/render_target.hpp @@ -5,8 +5,8 @@ #include #include -#include "device.hpp" -#include "image.hpp" +#include "vulkan/device.hpp" +#include "vulkan/image.hpp" namespace sophia { diff --git a/engine/src/resources/resource_registry.hpp b/engine/src/resources/resource_registry.hpp index 8e94a63..24882ef 100644 --- a/engine/src/resources/resource_registry.hpp +++ b/engine/src/resources/resource_registry.hpp @@ -3,9 +3,9 @@ #include #include -#include "device.hpp" #include "resources/mesh.hpp" #include "resources/pools/render_target_pool.hpp" +#include "vulkan/device.hpp" namespace sophia { diff --git a/engine/src/rendering/access.cpp b/engine/src/vulkan/access.cpp similarity index 99% rename from engine/src/rendering/access.cpp rename to engine/src/vulkan/access.cpp index 4dd417c..640e369 100644 --- a/engine/src/rendering/access.cpp +++ b/engine/src/vulkan/access.cpp @@ -1,4 +1,4 @@ -#include "rendering/access.hpp" +#include "vulkan/access.hpp" #include #include diff --git a/engine/src/rendering/access.hpp b/engine/src/vulkan/access.hpp similarity index 100% rename from engine/src/rendering/access.hpp rename to engine/src/vulkan/access.hpp diff --git a/engine/src/buffer.cpp b/engine/src/vulkan/buffer.cpp similarity index 99% rename from engine/src/buffer.cpp rename to engine/src/vulkan/buffer.cpp index 341c436..88428af 100644 --- a/engine/src/buffer.cpp +++ b/engine/src/vulkan/buffer.cpp @@ -2,7 +2,7 @@ * Based on * https://github.com/blurrypiano/littleVulkanEngine/blob/main/src/lve_buffer.cpp */ -#include "buffer.hpp" +#include "vulkan/buffer.hpp" #include diff --git a/engine/src/buffer.hpp b/engine/src/vulkan/buffer.hpp similarity index 98% rename from engine/src/buffer.hpp rename to engine/src/vulkan/buffer.hpp index 7c313fa..2320ab4 100644 --- a/engine/src/buffer.hpp +++ b/engine/src/vulkan/buffer.hpp @@ -4,7 +4,7 @@ */ #pragma once -#include "device.hpp" +#include "vulkan/device.hpp" namespace sophia { diff --git a/engine/src/descriptors/descriptor_pool.cpp b/engine/src/vulkan/descriptors/descriptor_pool.cpp similarity index 100% rename from engine/src/descriptors/descriptor_pool.cpp rename to engine/src/vulkan/descriptors/descriptor_pool.cpp diff --git a/engine/src/descriptors/descriptor_pool.hpp b/engine/src/vulkan/descriptors/descriptor_pool.hpp similarity index 97% rename from engine/src/descriptors/descriptor_pool.hpp rename to engine/src/vulkan/descriptors/descriptor_pool.hpp index 78ee0c7..dcab1d2 100644 --- a/engine/src/descriptors/descriptor_pool.hpp +++ b/engine/src/vulkan/descriptors/descriptor_pool.hpp @@ -3,7 +3,7 @@ #include #include -#include "device.hpp" +#include "vulkan/device.hpp" namespace sophia { diff --git a/engine/src/descriptors/descriptor_set_layout.cpp b/engine/src/vulkan/descriptors/descriptor_set_layout.cpp similarity index 100% rename from engine/src/descriptors/descriptor_set_layout.cpp rename to engine/src/vulkan/descriptors/descriptor_set_layout.cpp diff --git a/engine/src/descriptors/descriptor_set_layout.hpp b/engine/src/vulkan/descriptors/descriptor_set_layout.hpp similarity index 97% rename from engine/src/descriptors/descriptor_set_layout.hpp rename to engine/src/vulkan/descriptors/descriptor_set_layout.hpp index ff75b55..b56d0da 100644 --- a/engine/src/descriptors/descriptor_set_layout.hpp +++ b/engine/src/vulkan/descriptors/descriptor_set_layout.hpp @@ -4,7 +4,7 @@ #include #include -#include "device.hpp" +#include "vulkan/device.hpp" namespace sophia { diff --git a/engine/src/descriptors/descriptor_writer.cpp b/engine/src/vulkan/descriptors/descriptor_writer.cpp similarity index 100% rename from engine/src/descriptors/descriptor_writer.cpp rename to engine/src/vulkan/descriptors/descriptor_writer.cpp diff --git a/engine/src/descriptors/descriptor_writer.hpp b/engine/src/vulkan/descriptors/descriptor_writer.hpp similarity index 95% rename from engine/src/descriptors/descriptor_writer.hpp rename to engine/src/vulkan/descriptors/descriptor_writer.hpp index 95effbb..47e53c8 100644 --- a/engine/src/descriptors/descriptor_writer.hpp +++ b/engine/src/vulkan/descriptors/descriptor_writer.hpp @@ -4,7 +4,7 @@ #include "descriptor_pool.hpp" #include "descriptor_set_layout.hpp" -#include "device.hpp" +#include "vulkan/device.hpp" namespace sophia { diff --git a/engine/src/device.cpp b/engine/src/vulkan/device.cpp similarity index 99% rename from engine/src/device.cpp rename to engine/src/vulkan/device.cpp index e909b26..742f15b 100644 --- a/engine/src/device.cpp +++ b/engine/src/vulkan/device.cpp @@ -1,4 +1,4 @@ -#include "device.hpp" +#include "vulkan/device.hpp" #include diff --git a/engine/src/device.hpp b/engine/src/vulkan/device.hpp similarity index 100% rename from engine/src/device.hpp rename to engine/src/vulkan/device.hpp diff --git a/engine/src/image.cpp b/engine/src/vulkan/image.cpp similarity index 99% rename from engine/src/image.cpp rename to engine/src/vulkan/image.cpp index a54f08b..5ea66c1 100644 --- a/engine/src/image.cpp +++ b/engine/src/vulkan/image.cpp @@ -1,10 +1,10 @@ -#include "image.hpp" +#include "vulkan/image.hpp" #include #include #include -#include "device.hpp" +#include "vulkan/device.hpp" namespace sophia { diff --git a/engine/src/image.hpp b/engine/src/vulkan/image.hpp similarity index 99% rename from engine/src/image.hpp rename to engine/src/vulkan/image.hpp index af62af7..5f8647f 100644 --- a/engine/src/image.hpp +++ b/engine/src/vulkan/image.hpp @@ -4,7 +4,7 @@ #include #include -#include "rendering/access.hpp" +#include "vulkan/access.hpp" namespace sophia { diff --git a/engine/src/pipelines/compute_pipeline.cpp b/engine/src/vulkan/pipelines/compute_pipeline.cpp similarity index 100% rename from engine/src/pipelines/compute_pipeline.cpp rename to engine/src/vulkan/pipelines/compute_pipeline.cpp diff --git a/engine/src/pipelines/compute_pipeline.hpp b/engine/src/vulkan/pipelines/compute_pipeline.hpp similarity index 92% rename from engine/src/pipelines/compute_pipeline.hpp rename to engine/src/vulkan/pipelines/compute_pipeline.hpp index 74e5e90..e8f7f43 100644 --- a/engine/src/pipelines/compute_pipeline.hpp +++ b/engine/src/vulkan/pipelines/compute_pipeline.hpp @@ -3,9 +3,9 @@ #include #include -#include "device.hpp" #include "pipeline.hpp" -#include "shader.hpp" +#include "vulkan/device.hpp" +#include "vulkan/shader.hpp" namespace sophia { diff --git a/engine/src/pipelines/graphics_pipeline.cpp b/engine/src/vulkan/pipelines/graphics_pipeline.cpp similarity index 100% rename from engine/src/pipelines/graphics_pipeline.cpp rename to engine/src/vulkan/pipelines/graphics_pipeline.cpp diff --git a/engine/src/pipelines/graphics_pipeline.hpp b/engine/src/vulkan/pipelines/graphics_pipeline.hpp similarity index 98% rename from engine/src/pipelines/graphics_pipeline.hpp rename to engine/src/vulkan/pipelines/graphics_pipeline.hpp index 600253c..2a77943 100644 --- a/engine/src/pipelines/graphics_pipeline.hpp +++ b/engine/src/vulkan/pipelines/graphics_pipeline.hpp @@ -6,9 +6,9 @@ #include #include -#include "device.hpp" #include "pipeline.hpp" -#include "shader.hpp" +#include "vulkan/device.hpp" +#include "vulkan/shader.hpp" namespace sophia { diff --git a/engine/src/pipelines/pipeline.cpp b/engine/src/vulkan/pipelines/pipeline.cpp similarity index 100% rename from engine/src/pipelines/pipeline.cpp rename to engine/src/vulkan/pipelines/pipeline.cpp diff --git a/engine/src/pipelines/pipeline.hpp b/engine/src/vulkan/pipelines/pipeline.hpp similarity index 95% rename from engine/src/pipelines/pipeline.hpp rename to engine/src/vulkan/pipelines/pipeline.hpp index 92b44fb..28f0b0d 100644 --- a/engine/src/pipelines/pipeline.hpp +++ b/engine/src/vulkan/pipelines/pipeline.hpp @@ -4,7 +4,7 @@ #include #include -#include "device.hpp" +#include "vulkan/device.hpp" namespace sophia { diff --git a/engine/src/shader.cpp b/engine/src/vulkan/shader.cpp similarity index 99% rename from engine/src/shader.cpp rename to engine/src/vulkan/shader.cpp index a75b6bc..e0de91b 100644 --- a/engine/src/shader.cpp +++ b/engine/src/vulkan/shader.cpp @@ -1,4 +1,4 @@ -#include "shader.hpp" +#include "vulkan/shader.hpp" #include #include diff --git a/engine/src/shader.hpp b/engine/src/vulkan/shader.hpp similarity index 100% rename from engine/src/shader.hpp rename to engine/src/vulkan/shader.hpp diff --git a/engine/src/swapchain.cpp b/engine/src/vulkan/swapchain.cpp similarity index 99% rename from engine/src/swapchain.cpp rename to engine/src/vulkan/swapchain.cpp index ed07c2e..67744d9 100644 --- a/engine/src/swapchain.cpp +++ b/engine/src/vulkan/swapchain.cpp @@ -1,4 +1,4 @@ -#include "swapchain.hpp" +#include "vulkan/swapchain.hpp" #include #include diff --git a/engine/src/swapchain.hpp b/engine/src/vulkan/swapchain.hpp similarity index 97% rename from engine/src/swapchain.hpp rename to engine/src/vulkan/swapchain.hpp index 0422aec..1a8b558 100644 --- a/engine/src/swapchain.hpp +++ b/engine/src/vulkan/swapchain.hpp @@ -5,8 +5,8 @@ #include #include -#include "device.hpp" -#include "image.hpp" +#include "vulkan/device.hpp" +#include "vulkan/image.hpp" namespace sophia { From 72e4ae79e2afa93db09eb74463fc9e940d128db1 Mon Sep 17 00:00:00 2001 From: Caleb Burke Date: Mon, 21 Sep 2026 01:30:21 -0700 Subject: [PATCH 09/26] gitea issue helper --- CLAUDE.md | 17 ++++++++++ bin/dev | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..86b81fd --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,17 @@ +# Sophia + +## Dev helper (`bin/dev`) + +`bin/dev` (on PATH via direnv as `dev`) wraps common project tasks. Run `dev bash-completions` to list commands. + +### Creating issues + +Use `dev issue` to create Gitea issues instead of calling the API or `tea` directly: + +``` +dev issue "" [-b "<body>"] [-l <label>]... [-a <user>]... [--open] +``` + +- Labels are repo label names (`bug`, `enhancement`, `question`, ...). +- The Gitea API cannot add an issue to a project. After creating an issue, tell the user its URL so they can add it to the "sophia" project (issue sidebar > Projects), or pass `--open`. +- Only create issues when the user asks; issues are visible on the remote. diff --git a/bin/dev b/bin/dev index b1dd9b3..dfb676f 100755 --- a/bin/dev +++ b/bin/dev @@ -19,6 +19,7 @@ WAIT_FOR_PROCESS = "wait_for_process" class DevHelper: SHADER_EXTENSIONS = (".vert", ".frag", ".comp", ".geom", ".tesc", ".tese") + PROJECT_NAME = "sophia" def __init__(self): self.last_exit_status = 0 @@ -61,6 +62,40 @@ class DevHelper: flag = "" if exists else "-b " self._exec_in_root(f"git checkout {flag}{shlex.quote(branch_name)}", replace=True) + ## + # Creates an issue on this project's Gitea repo. + # dev issue <title> [-b|--body <text>] [-l|--label <name>]... [-a|--assignee <user>]... [-o|--open] + # The body is read from stdin if --body is not given and stdin is piped. + # Gitea's API cannot add issues to a project (only the web UI can), so the + # issue URL is printed and --open opens it in the browser to do so. + def issue(self, *args, **kwargs): + title, body, labels, assignees, open_in_browser = self._parse_issue_args(args) + + repo = self._origin_repo() + if repo is None: + raise RuntimeError("could not determine the Gitea repo from the origin remote") + + payload = {"title": title, "body": body} + if labels: + label_ids = self._fetch_label_ids(repo) + unknown = [label for label in labels if label not in label_ids] + if unknown: + raise ValueError(f"Unknown label(s): {', '.join(unknown)} (available: {', '.join(sorted(label_ids))})") + payload["labels"] = [label_ids[label] for label in labels] + if assignees: + payload["assignees"] = assignees + + issue = self._tea_api(f"repos/{repo}/issues", payload) + print(f"Created issue #{issue['number']}: {issue['title']}") + print(issue["html_url"]) + print(f"Add it to the project from the issue sidebar: Projects > {self.PROJECT_NAME}") + + if open_in_browser: + opener = shutil.which("xdg-open") or shutil.which("open") + if opener is None: + raise RuntimeError("xdg-open not found on PATH") + subprocess.Popen([opener, issue["html_url"]], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + def renderdoc(self, *args, **kwargs): qrenderdoc = self._find_qrenderdoc() self._exec_in_root(qrenderdoc, replace=True) @@ -126,6 +161,63 @@ class DevHelper: raise RuntimeError(f"failed to fetch issue title from {endpoint}") return json.loads(result.stdout)["title"] + def _parse_issue_args(self, args): + title = None + body = None + labels = [] + assignees = [] + open_in_browser = False + + args = list(args) + while args: + arg = args.pop(0) + if arg in ("-b", "--body", "-l", "--label", "-a", "--assignee"): + if not args: + raise ValueError(f"{arg} requires a value") + value = args.pop(0) + if arg in ("-b", "--body"): + body = value + elif arg in ("-l", "--label"): + labels.append(value) + else: + assignees.append(value) + elif arg in ("-o", "--open"): + open_in_browser = True + elif arg.startswith("-"): + raise ValueError(f"Unknown option: {arg}") + elif title is None: + title = arg + else: + title = f"{title} {arg}" + + if not title: + raise ValueError("Must provide an issue title. Usage: dev issue <title> [-b <body>] [-l <label>]... [-a <user>]... [-o]") + if body is None: + body = "" if sys.stdin.isatty() else sys.stdin.read() + return title, body, labels, assignees, open_in_browser + + def _tea_api(self, endpoint, payload=None): + if shutil.which("tea") is None: + raise RuntimeError("tea not found on PATH (install gitea tea and run `tea login add`)") + + command = ["tea", "api", endpoint] + if payload is not None: + command += ["-d", "@-"] + print(f"Running: {' '.join(command)}") + result = subprocess.run( + command, + input=json.dumps(payload) if payload is not None else None, + stdout=subprocess.PIPE, + text=True, + ) + if result.returncode != 0: + raise RuntimeError(f"tea api request failed: {endpoint}") + return json.loads(result.stdout) + + def _fetch_label_ids(self, repo): + labels = self._tea_api(f"repos/{repo}/labels?limit=100") + return {label["name"]: label["id"] for label in labels} + def _format_branch_name(self, issue_repo, issue_number, issue_title): formatted_title = re.sub(r"-+", "-", re.sub(r"[^a-z0-9\-]", "", re.sub(r"\s+", "-", issue_title.lower().strip()))) if issue_repo == self._origin_repo(): From ce54f2ffcc4b4927ce354370eecc30add3120b21 Mon Sep 17 00:00:00 2001 From: Caleb Burke <calebburke91@gmail.com> Date: Mon, 21 Sep 2026 01:33:39 -0700 Subject: [PATCH 10/26] PM stuff --- .gitea/ISSUE_TEMPLATE/bug_report.md | 31 ++++++++++++++++++++++++ .gitea/ISSUE_TEMPLATE/feature_request.md | 23 ++++++++++++++++++ .gitea/pull_request_template.md | 18 ++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 .gitea/ISSUE_TEMPLATE/bug_report.md create mode 100644 .gitea/ISSUE_TEMPLATE/feature_request.md create mode 100644 .gitea/pull_request_template.md diff --git a/.gitea/ISSUE_TEMPLATE/bug_report.md b/.gitea/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..b483bd5 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "" +labels: "" +assignees: "" +--- + +**Desktop (please complete the following information):** + +- OS: [e.g. iOS] +- Browser [e.g. chrome, safari] +- Version [e.g. 22] + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +1. Something +2. ... + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.gitea/ISSUE_TEMPLATE/feature_request.md b/.gitea/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..328180c --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "" +labels: "" +assignees: "" +--- + +Relates to: + +- TODO + +## Context + +TODO + +## Implementation + +TODO + +## Additional resources + +TODO diff --git a/.gitea/pull_request_template.md b/.gitea/pull_request_template.md new file mode 100644 index 0000000..900c085 --- /dev/null +++ b/.gitea/pull_request_template.md @@ -0,0 +1,18 @@ +resolves TODO + +## Context + +TODO + +## Implementation + +TODO + +## Screenshots + +TODO + +## Testing Instructions + +1. Build engine via `bin/dev build && cd build/ && make -j4` +2. Run testbed via `./testbed` From 39253771f16073da1f0fad0367d8641b9c7f94f8 Mon Sep 17 00:00:00 2001 From: Caleb Burke <calebburke91@gmail.com> Date: Mon, 21 Sep 2026 11:34:29 -0700 Subject: [PATCH 11/26] starting scene renderer stuff --- engine/src/rendering/scene_renderer.cpp | 12 ++++++++++++ engine/src/rendering/scene_renderer.hpp | 25 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 engine/src/rendering/scene_renderer.cpp create mode 100644 engine/src/rendering/scene_renderer.hpp diff --git a/engine/src/rendering/scene_renderer.cpp b/engine/src/rendering/scene_renderer.cpp new file mode 100644 index 0000000..4abc935 --- /dev/null +++ b/engine/src/rendering/scene_renderer.cpp @@ -0,0 +1,12 @@ +#include "rendering/scene_renderer.hpp" + +namespace sophia { + +SceneRenderer::SceneRenderer(ResourceRegistry& resources) + : resources{resources} {} + +SceneRenderer::~SceneRenderer() {} + +void SceneRenderer::render(const Scene& scene, RenderTargetHandle target) {} + +} // namespace sophia diff --git a/engine/src/rendering/scene_renderer.hpp b/engine/src/rendering/scene_renderer.hpp new file mode 100644 index 0000000..39e4537 --- /dev/null +++ b/engine/src/rendering/scene_renderer.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include <sophia/handles.hpp> +#include <sophia/scene.hpp> + +#include "resources/resource_registry.hpp" + +namespace sophia { + +class SceneRenderer { + public: + SceneRenderer(const SceneRenderer&) = delete; + SceneRenderer& operator=(const SceneRenderer&) = delete; + + SceneRenderer(ResourceRegistry& resources); + ~SceneRenderer(); + + // add camera as a arg (entity) + void render(const Scene& scene, RenderTargetHandle target); + + private: + ResourceRegistry& resources; +}; + +} // namespace sophia From a98eac3dc713be938de40b77ab90bb1ae4398751 Mon Sep 17 00:00:00 2001 From: Caleb Burke <calebburke91@gmail.com> Date: Wed, 23 Sep 2026 04:20:43 +0000 Subject: [PATCH 12/26] laptop changes --- engine/include/sophia/scene.hpp | 6 +++ engine/include/sophia/ui/debug_ui.hpp | 6 +-- engine/src/entity.cpp | 7 +++ engine/src/entity.hpp | 66 +++++++++++++++++++++++++ engine/src/rendering/scene_renderer.cpp | 51 ++++++++++++++++++- engine/src/rendering/scene_renderer.hpp | 7 ++- engine/src/ui/debug_ui.cpp | 1 + 7 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 engine/src/entity.cpp create mode 100644 engine/src/entity.hpp diff --git a/engine/include/sophia/scene.hpp b/engine/include/sophia/scene.hpp index bbc1c70..d23850e 100644 --- a/engine/include/sophia/scene.hpp +++ b/engine/include/sophia/scene.hpp @@ -4,6 +4,9 @@ namespace sophia { +class Entity; +class SceneRenderer; // !TEMP remove this + class Scene { public: Scene(); @@ -14,6 +17,9 @@ class Scene { void setActiveCamera(entt::entity camera) { activeCamera = camera; } private: + friend class Entity; + friend class SceneRenderer; // !TEMP remove this + entt::registry registry; entt::entity activeCamera = entt::null; }; diff --git a/engine/include/sophia/ui/debug_ui.hpp b/engine/include/sophia/ui/debug_ui.hpp index 0037196..22d27a9 100644 --- a/engine/include/sophia/ui/debug_ui.hpp +++ b/engine/include/sophia/ui/debug_ui.hpp @@ -17,9 +17,9 @@ class DebugUI { void show(); void hide(); - void showPanelRegistry(bool show = true); - void showFrameStats(bool show = true); - void showConsole(bool show = true); + void showPanelRegistry(bool show = false); + void showFrameStats(bool show = false); + void showConsole(bool show = false); private: PanelRegistry& registry; diff --git a/engine/src/entity.cpp b/engine/src/entity.cpp new file mode 100644 index 0000000..e891f90 --- /dev/null +++ b/engine/src/entity.cpp @@ -0,0 +1,7 @@ +#include "entity.hpp" + +namespace sophia { + +Entity::Entity(entt::entity handle, Scene* scene) : handle(handle), scene(scene) {} + +} // namespace sophia diff --git a/engine/src/entity.hpp b/engine/src/entity.hpp new file mode 100644 index 0000000..67a4a7a --- /dev/null +++ b/engine/src/entity.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include <sophia/scene.hpp> +#include <entt/entt.hpp> +#include <sophia/util/assert.hpp> +#include <sophia/types.hpp> + +namespace sophia { + +class Entity { +public: + Entity(const Entity&) = default; + Entity& operator=(const Entity&) = default; + + Entity() = default; + Entity(entt::entity handle, Scene* scene); + + template<typename T, typename... Args> + T& addComponent(Args&&... args) { + SOPHIA_ASSERT(!hasComponent<T>(), "Entity already has component!"); + T& component = scene->registry.emplace<T>(handle, std::forward<Args>(args)...); + // scene->OnComponentAdded<T>(*this, component); + return component; + } + + template<typename T> + T& getComponent() + { + SOPHIA_ASSERT(hasComponent<T>(), "Entity does not have component!"); + return scene->registry.get<T>(handle); + } + + template<typename T> + bool hasComponent() + { + return scene->registry.all_of<T>(handle); + } + + template<typename T> + void removeComponent() + { + SOPHIA_ASSERT(hasComponent<T>(), "Entity does not have component!"); + scene->registry.remove<T>(handle); + } + + operator bool() const { return handle != entt::null; } + operator entt::entity() const { return handle; } + operator u32() const { return (u32)handle; } + + // UUID GetUUID() { return GetComponent<IDComponent>().ID; } + // const std::string& GetName() { return GetComponent<TagComponent>().Tag; } + + bool operator==(const Entity& other) const { + return handle == other.handle && scene == other.scene; + } + + bool operator!=(const Entity& other) const { + return !(*this == other); + } + +private: + entt::entity handle { entt::null}; + Scene* scene = nullptr; +}; + +} // namespace sophia diff --git a/engine/src/rendering/scene_renderer.cpp b/engine/src/rendering/scene_renderer.cpp index 4abc935..3262802 100644 --- a/engine/src/rendering/scene_renderer.cpp +++ b/engine/src/rendering/scene_renderer.cpp @@ -1,12 +1,61 @@ #include "rendering/scene_renderer.hpp" +#include <sophia/util/assert.hpp> +#include <sophia/components.hpp> + namespace sophia { +namespace { + struct PushConstantData { + glm::mat4 transform{1.f}; + glm::vec4 color{1.f}; + }; +} + SceneRenderer::SceneRenderer(ResourceRegistry& resources) : resources{resources} {} SceneRenderer::~SceneRenderer() {} -void SceneRenderer::render(const Scene& scene, RenderTargetHandle target) {} +void SceneRenderer::render(const Scene& scene, Entity camera, RenderTargetHandle target) { + SOPHIA_ASSERT(camera.hasComponent<CameraComponent>(), "camera does not have camera component!"); + + auto& R = scene.registry; + + // Get all mesh + // For each mesh, get all entities(ES) that have a transform and that mesh + // For e in ES: add e's transform component to a list + + // We now have + /* + M = mesh + T = transform + + Work Batch + |- M0 + | |- T0 + | \- T2 + | + |- M1 + | |- T1 + | \- T5 + | + \- M2 + |- T3 + \- T4 + + */ + + /* + for batch in Work Batch: + batch.mesh.bind() + + for transform in batch.transforms: + commandBuffer.pushConstants(transform) + batch.mesh.draw(commandBuffer) + + NOTE: Use instanced rendering? + */ +} } // namespace sophia diff --git a/engine/src/rendering/scene_renderer.hpp b/engine/src/rendering/scene_renderer.hpp index 39e4537..1ef0bce 100644 --- a/engine/src/rendering/scene_renderer.hpp +++ b/engine/src/rendering/scene_renderer.hpp @@ -3,10 +3,14 @@ #include <sophia/handles.hpp> #include <sophia/scene.hpp> +#include "entity.hpp" #include "resources/resource_registry.hpp" namespace sophia { + /** + * !TEMP to be decoupled once scene batching and rendering impl + */ class SceneRenderer { public: SceneRenderer(const SceneRenderer&) = delete; @@ -15,8 +19,7 @@ class SceneRenderer { SceneRenderer(ResourceRegistry& resources); ~SceneRenderer(); - // add camera as a arg (entity) - void render(const Scene& scene, RenderTargetHandle target); + void render(const Scene& scene, Entity camera, RenderTargetHandle target); private: ResourceRegistry& resources; diff --git a/engine/src/ui/debug_ui.cpp b/engine/src/ui/debug_ui.cpp index f52cf17..ffd5d17 100644 --- a/engine/src/ui/debug_ui.cpp +++ b/engine/src/ui/debug_ui.cpp @@ -12,6 +12,7 @@ DebugUI::DebugUI(PanelRegistry& registry, ConsoleCommandRegistry& commands) this->panelRegistryHandle = registry.emplace<PanelRegistryPanel>(registry); this->frameStatsHandle = registry.emplace<FrameStatsPanel>(); this->consoleHandle = registry.emplace<ConsolePanel>(commands); + hide(); // #endif // !NDEBUG } From 6a50cdb4e8badc784fde7b84320d7306fbb9ec84 Mon Sep 17 00:00:00 2001 From: Caleb Burke <calebburke91@gmail.com> Date: Wed, 23 Sep 2026 00:42:47 -0700 Subject: [PATCH 13/26] Formatting --- engine/src/entity.cpp | 5 +- engine/src/entity.hpp | 38 ++++++------- engine/src/rendering/scene_renderer.cpp | 76 +++++++++++++------------ engine/src/rendering/scene_renderer.hpp | 6 +- 4 files changed, 62 insertions(+), 63 deletions(-) diff --git a/engine/src/entity.cpp b/engine/src/entity.cpp index e891f90..eed9281 100644 --- a/engine/src/entity.cpp +++ b/engine/src/entity.cpp @@ -2,6 +2,7 @@ namespace sophia { -Entity::Entity(entt::entity handle, Scene* scene) : handle(handle), scene(scene) {} +Entity::Entity(entt::entity handle, Scene* scene) + : handle(handle), scene(scene) {} -} // namespace sophia +} // namespace sophia diff --git a/engine/src/entity.hpp b/engine/src/entity.hpp index 67a4a7a..0c3950d 100644 --- a/engine/src/entity.hpp +++ b/engine/src/entity.hpp @@ -1,44 +1,42 @@ #pragma once -#include <sophia/scene.hpp> #include <entt/entt.hpp> -#include <sophia/util/assert.hpp> +#include <sophia/scene.hpp> #include <sophia/types.hpp> +#include <sophia/util/assert.hpp> namespace sophia { class Entity { -public: + public: Entity(const Entity&) = default; Entity& operator=(const Entity&) = default; Entity() = default; - Entity(entt::entity handle, Scene* scene); + Entity(entt::entity handle, Scene* scene); - template<typename T, typename... Args> + template <typename T, typename... Args> T& addComponent(Args&&... args) { SOPHIA_ASSERT(!hasComponent<T>(), "Entity already has component!"); - T& component = scene->registry.emplace<T>(handle, std::forward<Args>(args)...); + T& component = + scene->registry.emplace<T>(handle, std::forward<Args>(args)...); // scene->OnComponentAdded<T>(*this, component); return component; } - template<typename T> - T& getComponent() - { + template <typename T> + T& getComponent() { SOPHIA_ASSERT(hasComponent<T>(), "Entity does not have component!"); return scene->registry.get<T>(handle); } - template<typename T> - bool hasComponent() - { + template <typename T> + bool hasComponent() { return scene->registry.all_of<T>(handle); } - template<typename T> - void removeComponent() - { + template <typename T> + void removeComponent() { SOPHIA_ASSERT(hasComponent<T>(), "Entity does not have component!"); scene->registry.remove<T>(handle); } @@ -54,13 +52,11 @@ public: return handle == other.handle && scene == other.scene; } - bool operator!=(const Entity& other) const { - return !(*this == other); - } + bool operator!=(const Entity& other) const { return !(*this == other); } -private: - entt::entity handle { entt::null}; + private: + entt::entity handle{entt::null}; Scene* scene = nullptr; }; -} // namespace sophia +} // namespace sophia diff --git a/engine/src/rendering/scene_renderer.cpp b/engine/src/rendering/scene_renderer.cpp index 3262802..3b32ab3 100644 --- a/engine/src/rendering/scene_renderer.cpp +++ b/engine/src/rendering/scene_renderer.cpp @@ -1,61 +1,63 @@ #include "rendering/scene_renderer.hpp" -#include <sophia/util/assert.hpp> #include <sophia/components.hpp> +#include <sophia/util/assert.hpp> namespace sophia { namespace { - struct PushConstantData { - glm::mat4 transform{1.f}; - glm::vec4 color{1.f}; - }; -} +struct PushConstantData { + glm::mat4 transform{1.f}; + glm::vec4 color{1.f}; +}; +} // namespace SceneRenderer::SceneRenderer(ResourceRegistry& resources) : resources{resources} {} SceneRenderer::~SceneRenderer() {} -void SceneRenderer::render(const Scene& scene, Entity camera, RenderTargetHandle target) { - SOPHIA_ASSERT(camera.hasComponent<CameraComponent>(), "camera does not have camera component!"); +void SceneRenderer::render(const Scene& scene, Entity camera, + RenderTargetHandle target) { + SOPHIA_ASSERT(camera.hasComponent<CameraComponent>(), + "camera does not have camera component!"); - auto& R = scene.registry; + auto& registry = scene.registry; - // Get all mesh - // For each mesh, get all entities(ES) that have a transform and that mesh - // For e in ES: add e's transform component to a list + // Get all mesh + // For each mesh, get all entities(ES) that have a transform and that mesh + // For e in ES: add e's transform component to a list - // We now have - /* - M = mesh - T = transform + // We now have + /* + M = mesh + T = transform - Work Batch - |- M0 - | |- T0 - | \- T2 - | - |- M1 - | |- T1 - | \- T5 - | - \- M2 - |- T3 - \- T4 + Work Batch + |- M0 + | |- T0 + | \- T2 + | + |- M1 + | |- T1 + | \- T5 + | + \- M2 + |- T3 + \- T4 - */ + */ - /* - for batch in Work Batch: - batch.mesh.bind() + /* + for batch in Work Batch: + batch.mesh.bind() - for transform in batch.transforms: - commandBuffer.pushConstants(transform) - batch.mesh.draw(commandBuffer) + for transform in batch.transforms: + commandBuffer.pushConstants(transform) + batch.mesh.draw(commandBuffer) - NOTE: Use instanced rendering? - */ + NOTE: Use instanced rendering? + */ } } // namespace sophia diff --git a/engine/src/rendering/scene_renderer.hpp b/engine/src/rendering/scene_renderer.hpp index 1ef0bce..0eaf29c 100644 --- a/engine/src/rendering/scene_renderer.hpp +++ b/engine/src/rendering/scene_renderer.hpp @@ -8,9 +8,9 @@ namespace sophia { - /** - * !TEMP to be decoupled once scene batching and rendering impl - */ +/** + * !TEMP to be decoupled once scene batching and rendering impl + */ class SceneRenderer { public: SceneRenderer(const SceneRenderer&) = delete; From 97267f1a266835888657764ba866a7d61ca19f0d Mon Sep 17 00:00:00 2001 From: Caleb Burke <calebburke91@gmail.com> Date: Thu, 24 Sep 2026 05:31:38 +0000 Subject: [PATCH 14/26] stuff --- engine/include/sophia/components.hpp | 16 ++++++++++++++++ engine/include/sophia/scene.hpp | 9 +++++++-- engine/src/rendering/scene_renderer.cpp | 7 +++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/engine/include/sophia/components.hpp b/engine/include/sophia/components.hpp index 979c929..a67b198 100644 --- a/engine/include/sophia/components.hpp +++ b/engine/include/sophia/components.hpp @@ -1,5 +1,7 @@ #pragma once +#include <iostream> +#include <ostream> #include <sophia/handles.hpp> #include <string> @@ -28,6 +30,20 @@ struct TransformComponent { glm::mat3 normalMatrix() const { return glm::mat3{glm::transpose(glm::inverse(mat4()))}; } + + friend std::ostream& operator<<(std::ostream& os, + const TransformComponent& transform) { + os << "TransformComponent {\n" + << " Position: (" << transform.position.x << ", " + << transform.position.y << ", " << transform.position.z << ")\n" + << " Rotation (w, x, y, z): (" << transform.rotation.w << ", " + << transform.rotation.x << ", " << transform.rotation.y << ", " + << transform.rotation.z << ")\n" + << " Scale: (" << transform.scale.x << ", " << transform.scale.y << ", " + << transform.scale.z << ")\n" + << "}"; + return os; + } }; struct MeshComponent { diff --git a/engine/include/sophia/scene.hpp b/engine/include/sophia/scene.hpp index d23850e..687b50a 100644 --- a/engine/include/sophia/scene.hpp +++ b/engine/include/sophia/scene.hpp @@ -1,6 +1,7 @@ #pragma once #include <entt/entt.hpp> +#include <string> namespace sophia { @@ -12,13 +13,17 @@ class Scene { Scene(); ~Scene(); - // The camera a render request uses when it doesn't name one. Null until set. + void createEntity(const std::string& name = {}); + void destroyEntity(Entity entity); + + // The camera a render request uses when it doesn't name one. Null until + // set. entt::entity getActiveCamera() const { return activeCamera; } void setActiveCamera(entt::entity camera) { activeCamera = camera; } private: friend class Entity; - friend class SceneRenderer; // !TEMP remove this + friend class SceneRenderer; // !TEMP remove this entt::registry registry; entt::entity activeCamera = entt::null; diff --git a/engine/src/rendering/scene_renderer.cpp b/engine/src/rendering/scene_renderer.cpp index 3b32ab3..dea107d 100644 --- a/engine/src/rendering/scene_renderer.cpp +++ b/engine/src/rendering/scene_renderer.cpp @@ -24,6 +24,13 @@ void SceneRenderer::render(const Scene& scene, Entity camera, auto& registry = scene.registry; + auto view = registry.view<MeshComponent, TransformComponent>(); + + for (auto entity : view) { + auto& transform = view.get<TransformComponent>(entity); + log::info(transform); + } + // Get all mesh // For each mesh, get all entities(ES) that have a transform and that mesh // For e in ES: add e's transform component to a list From ae8db07d6f3af066932129bee60349119fa99ff1 Mon Sep 17 00:00:00 2001 From: Caleb Burke <calebburke91@gmail.com> Date: Wed, 23 Sep 2026 23:24:23 -0700 Subject: [PATCH 15/26] Simplified handles --- engine/include/sophia/handles.hpp | 18 ++++++++-------- .../include/sophia/resource_registry_ref.hpp | 21 +++---------------- engine/include/sophia/scene_registry.hpp | 2 +- engine/include/sophia/ui/panel_registry.hpp | 2 +- engine/include/sophia/util/pool.hpp | 21 ++++++++++--------- engine/src/resources/mesh.hpp | 2 -- .../resources/pools/render_target_pool.hpp | 2 +- engine/src/resources/resource_registry.hpp | 2 +- 8 files changed, 27 insertions(+), 43 deletions(-) diff --git a/engine/include/sophia/handles.hpp b/engine/include/sophia/handles.hpp index acee081..3a97886 100644 --- a/engine/include/sophia/handles.hpp +++ b/engine/include/sophia/handles.hpp @@ -6,7 +6,7 @@ namespace sophia { -template <typename Tag> +template <typename T> struct Handle { static constexpr u32 INVALID_INDEX = (1u << 20) - 1; @@ -24,14 +24,14 @@ struct Handle { [[nodiscard]] auto operator<=>(const Handle&) const = default; }; -struct MeshTag {}; -struct PanelTag {}; -struct RenderTargetTag {}; -struct SceneTag {}; +class Mesh; +class Panel; +class RenderTarget; +class Scene; -using MeshHandle = Handle<MeshTag>; -using PanelHandle = Handle<PanelTag>; -using RenderTargetHandle = Handle<RenderTargetTag>; -using SceneHandle = Handle<SceneTag>; +using MeshHandle = Handle<Mesh>; +using PanelHandle = Handle<Panel>; +using RenderTargetHandle = Handle<RenderTarget>; +using SceneHandle = Handle<Scene>; } // namespace sophia diff --git a/engine/include/sophia/resource_registry_ref.hpp b/engine/include/sophia/resource_registry_ref.hpp index 34d4785..f63bf09 100644 --- a/engine/include/sophia/resource_registry_ref.hpp +++ b/engine/include/sophia/resource_registry_ref.hpp @@ -14,30 +14,15 @@ namespace sophia { -class Mesh; -class RenderTarget; class ResourceRegistry; -template <typename T> -struct ResourceTraits; - -template <> -struct ResourceTraits<Mesh> { - using Handle = MeshHandle; -}; - -template <> -struct ResourceTraits<RenderTarget> { - using Handle = RenderTargetHandle; -}; - class ResourceRegistryRef { public: template <typename T> - typename ResourceTraits<T>::Handle load(const std::string& path); + Handle<T> load(const std::string& path); template <typename T, typename... Args> - typename ResourceTraits<T>::Handle create(Args&&... args) { + Handle<T> create(Args&&... args) { return createResource(std::type_identity<T>{}, std::forward<Args>(args)...); } @@ -48,7 +33,7 @@ class ResourceRegistryRef { bool isAlive(RenderTargetHandle handle) const; template <typename T> - void release(typename ResourceTraits<T>::Handle handle); + void release(Handle<T> handle); private: friend class Application; diff --git a/engine/include/sophia/scene_registry.hpp b/engine/include/sophia/scene_registry.hpp index 70e2739..2403b79 100644 --- a/engine/include/sophia/scene_registry.hpp +++ b/engine/include/sophia/scene_registry.hpp @@ -25,7 +25,7 @@ class SceneRegistry { u32 size() const { return pool.size(); } private: - Pool<Scene, SceneHandle> pool{"Scene"}; + Pool<Scene> pool{"Scene"}; }; } // namespace sophia diff --git a/engine/include/sophia/ui/panel_registry.hpp b/engine/include/sophia/ui/panel_registry.hpp index b53b11c..5836fee 100644 --- a/engine/include/sophia/ui/panel_registry.hpp +++ b/engine/include/sophia/ui/panel_registry.hpp @@ -93,7 +93,7 @@ class PanelRegistry { const DockLayout& getDefaultLayout() const { return defaultLayout; } private: - Pool<Panel, PanelHandle> pool{"Panel"}; + Pool<Panel> pool{"Panel"}; DockLayout defaultLayout; }; diff --git a/engine/include/sophia/util/pool.hpp b/engine/include/sophia/util/pool.hpp index f5b8a53..083aa95 100644 --- a/engine/include/sophia/util/pool.hpp +++ b/engine/include/sophia/util/pool.hpp @@ -1,6 +1,7 @@ #pragma once #include <memory> +#include <sophia/handles.hpp> #include <sophia/types.hpp> #include <sophia/util/assert.hpp> #include <utility> @@ -8,7 +9,7 @@ namespace sophia { -template <typename T, typename HandleType> +template <typename T> class Pool { public: explicit Pool(const char* name) : name{name} {} @@ -17,29 +18,29 @@ class Pool { Pool& operator=(const Pool&) = delete; template <typename... Args> - HandleType create(Args&&... args) { + Handle<T> create(Args&&... args) { return add(std::make_unique<T>(std::forward<Args>(args)...)); } - HandleType add(std::unique_ptr<T> item) { + Handle<T> add(std::unique_ptr<T> item) { if (!freeList.empty()) { u32 index = freeList.back(); freeList.pop_back(); slots[index].item = std::move(item); - return HandleType{index, slots[index].generation}; + return Handle<T>{index, slots[index].generation}; } slots.push_back(Slot{std::move(item), 0}); - return HandleType{static_cast<u32>(slots.size() - 1), 0}; + return Handle<T>{static_cast<u32>(slots.size() - 1), 0}; } - T& get(HandleType handle) { + T& get(Handle<T> handle) { return const_cast<T&>(std::as_const(*this).get(handle)); } - const T& get(HandleType handle) const { return *validate(handle).item; } + const T& get(Handle<T> handle) const { return *validate(handle).item; } - void release(HandleType handle) { + void release(Handle<T> handle) { validate(handle); Slot& slot = slots[handle.index]; @@ -48,7 +49,7 @@ class Pool { freeList.push_back(handle.index); } - bool isAlive(HandleType handle) const { + bool isAlive(Handle<T> handle) const { return handle.isValid() && handle.index < slots.size() && slots[handle.index].generation == handle.generation && slots[handle.index].item != nullptr; @@ -98,7 +99,7 @@ class Pool { u32 generation = 0; }; - const Slot& validate(HandleType handle) const { + const Slot& validate(Handle<T> handle) const { SOPHIA_ASSERT(handle.isValid(), "invalid handle:", name); SOPHIA_ASSERT(handle.index < slots.size(), name, "handle index out of range:", handle.index); diff --git a/engine/src/resources/mesh.hpp b/engine/src/resources/mesh.hpp index 3c64513..54b8306 100644 --- a/engine/src/resources/mesh.hpp +++ b/engine/src/resources/mesh.hpp @@ -16,8 +16,6 @@ namespace sophia { class Mesh { public: - using HandleType = MeshHandle; - struct Vertex { glm::vec2 position{}; glm::vec3 color{}; diff --git a/engine/src/resources/pools/render_target_pool.hpp b/engine/src/resources/pools/render_target_pool.hpp index 7a8b870..5f4b525 100644 --- a/engine/src/resources/pools/render_target_pool.hpp +++ b/engine/src/resources/pools/render_target_pool.hpp @@ -30,7 +30,7 @@ class RenderTargetPool { Device& device; vk::Format format; - Pool<RenderTarget, RenderTargetHandle> pool{"RenderTarget"}; + Pool<RenderTarget> pool{"RenderTarget"}; }; } // namespace sophia diff --git a/engine/src/resources/resource_registry.hpp b/engine/src/resources/resource_registry.hpp index 24882ef..3912cf7 100644 --- a/engine/src/resources/resource_registry.hpp +++ b/engine/src/resources/resource_registry.hpp @@ -18,7 +18,7 @@ class ResourceRegistry { : device{device}, renderTargetPool{device, renderTargetFormat} {} Device& device; - Pool<Mesh, MeshHandle> meshPool{"Mesh"}; + Pool<Mesh> meshPool{"Mesh"}; RenderTargetPool renderTargetPool; // TexturePool textures; // MaterialPool materials; From b5d28ce3ebc57f844e0af14a9ff142fd3db23711 Mon Sep 17 00:00:00 2001 From: Caleb Burke <calebburke91@gmail.com> Date: Thu, 24 Sep 2026 23:00:02 -0700 Subject: [PATCH 16/26] math.hpp added --- engine/CMakeLists.txt | 5 +++++ engine/include/sophia/application.hpp | 5 +---- engine/include/sophia/camera_2d.hpp | 4 +--- engine/include/sophia/canvas.hpp | 5 +---- engine/include/sophia/components.hpp | 9 +++------ engine/include/sophia/frame_info.hpp | 5 +---- engine/include/sophia/math.hpp | 17 +++++++++++++++++ engine/include/sophia/resource_registry_ref.hpp | 5 +---- engine/src/application.cpp | 9 +++------ engine/src/resources/mesh.hpp | 5 +---- 10 files changed, 34 insertions(+), 35 deletions(-) create mode 100644 engine/include/sophia/math.hpp diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 41f862c..abb899f 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -17,6 +17,11 @@ target_include_directories(${ENGINE_NAME} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" ) +target_compile_definitions(${ENGINE_NAME} PUBLIC + GLM_FORCE_RADIANS + GLM_FORCE_DEPTH_ZERO_TO_ONE +) + target_link_libraries(${ENGINE_NAME} PUBLIC vulkan shaderc diff --git a/engine/include/sophia/application.hpp b/engine/include/sophia/application.hpp index f7675b0..f2a2542 100644 --- a/engine/include/sophia/application.hpp +++ b/engine/include/sophia/application.hpp @@ -7,6 +7,7 @@ #include <sophia/canvas.hpp> #include <sophia/console/console_command_registry.hpp> #include <sophia/frame_info.hpp> +#include <sophia/math.hpp> #include <sophia/resource_registry_ref.hpp> #include <sophia/scene_registry.hpp> #include <sophia/types.hpp> @@ -16,10 +17,6 @@ #include <string> #include <vector> -#define GLM_FORCE_RADIANS -#define GLM_FORCE_DEPTH_ZERO_TO_ONE -#include <glm/glm.hpp> - namespace sophia { class Application { diff --git a/engine/include/sophia/camera_2d.hpp b/engine/include/sophia/camera_2d.hpp index fed29ed..88a87f0 100644 --- a/engine/include/sophia/camera_2d.hpp +++ b/engine/include/sophia/camera_2d.hpp @@ -1,9 +1,7 @@ #pragma once -#define GLM_FORCE_RADIANS -#define GLM_FORCE_DEPTH_ZERO_TO_ONE -#include <glm/glm.hpp> #include <glm/gtc/matrix_transform.hpp> +#include <sophia/math.hpp> namespace sophia { diff --git a/engine/include/sophia/canvas.hpp b/engine/include/sophia/canvas.hpp index 54b6d84..f11b811 100644 --- a/engine/include/sophia/canvas.hpp +++ b/engine/include/sophia/canvas.hpp @@ -2,12 +2,9 @@ #include <memory> #include <sophia/frame_info.hpp> +#include <sophia/math.hpp> #include <vector> -#define GLM_FORCE_RADIANS -#define GLM_FORCE_DEPTH_ZERO_TO_ONE -#include <glm/glm.hpp> - namespace sophia { class Application; diff --git a/engine/include/sophia/components.hpp b/engine/include/sophia/components.hpp index a67b198..9b39b76 100644 --- a/engine/include/sophia/components.hpp +++ b/engine/include/sophia/components.hpp @@ -1,20 +1,17 @@ #pragma once +#include <glm/gtc/matrix_transform.hpp> #include <iostream> #include <ostream> #include <sophia/handles.hpp> +#include <sophia/math.hpp> #include <string> -#define GLM_FORCE_RADIANS -#define GLM_FORCE_DEPTH_ZERO_TO_ONE -#include <glm/glm.hpp> -#include <glm/gtc/matrix_transform.hpp> -#include <glm/gtc/quaternion.hpp> - namespace sophia { struct TagComponent { std::string tag; // entt::hashed_string ? + uint32_t mask = 0; }; struct TransformComponent { diff --git a/engine/include/sophia/frame_info.hpp b/engine/include/sophia/frame_info.hpp index b542555..78f92bb 100644 --- a/engine/include/sophia/frame_info.hpp +++ b/engine/include/sophia/frame_info.hpp @@ -1,12 +1,9 @@ #pragma once #include <sophia/camera_2d.hpp> +#include <sophia/math.hpp> #include <sophia/types.hpp> -#define GLM_FORCE_RADIANS -#define GLM_FORCE_DEPTH_ZERO_TO_ONE -#include <glm/glm.hpp> - namespace sophia { struct FrameInfo { diff --git a/engine/include/sophia/math.hpp b/engine/include/sophia/math.hpp new file mode 100644 index 0000000..1ecfa71 --- /dev/null +++ b/engine/include/sophia/math.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include <glm/glm.hpp> +#include <glm/gtc/quaternion.hpp> + +namespace sophia { + +using vec2 = glm::vec2; +using vec3 = glm::vec3; +using vec4 = glm::vec4; +using ivec2 = glm::ivec2; +using uvec2 = glm::uvec2; +using mat3 = glm::mat3; +using mat4 = glm::mat4; +using quat = glm::quat; + +} // namespace sophia diff --git a/engine/include/sophia/resource_registry_ref.hpp b/engine/include/sophia/resource_registry_ref.hpp index f63bf09..9ea4200 100644 --- a/engine/include/sophia/resource_registry_ref.hpp +++ b/engine/include/sophia/resource_registry_ref.hpp @@ -2,16 +2,13 @@ #include <sophia/canvas.hpp> #include <sophia/handles.hpp> +#include <sophia/math.hpp> #include <sophia/types.hpp> #include <string> #include <type_traits> #include <utility> #include <vector> -#define GLM_FORCE_RADIANS -#define GLM_FORCE_DEPTH_ZERO_TO_ONE -#include <glm/glm.hpp> - namespace sophia { class ResourceRegistry; diff --git a/engine/src/application.cpp b/engine/src/application.cpp index 1fd3776..cf7f12f 100644 --- a/engine/src/application.cpp +++ b/engine/src/application.cpp @@ -1,6 +1,9 @@ +#include <imgui.h> + #include <array> #include <chrono> #include <cstddef> +#include <glm/glm.hpp> #include <sophia/application.hpp> #include <sophia/util/logger.hpp> #include <string> @@ -8,12 +11,6 @@ #include <vector> #include <vulkan/vulkan.hpp> -#define GLM_FORCE_RADIANS -#define GLM_FORCE_DEPTH_ZERO_TO_ONE -#include <imgui.h> - -#include <glm/glm.hpp> - #include "rendering/imgui_renderer.hpp" #include "rendering/model_render_system.hpp" #include "rendering/render_context.hpp" diff --git a/engine/src/resources/mesh.hpp b/engine/src/resources/mesh.hpp index 54b8306..86bd302 100644 --- a/engine/src/resources/mesh.hpp +++ b/engine/src/resources/mesh.hpp @@ -2,13 +2,10 @@ #include <memory> #include <sophia/handles.hpp> +#include <sophia/math.hpp> #include <sophia/types.hpp> #include <vector> -#define GLM_FORCE_RADIANS -#define GLM_FORCE_DEPTH_ZERO_TO_ONE -#include <glm/glm.hpp> - #include "vulkan/buffer.hpp" #include "vulkan/device.hpp" From 75fc14f4ac534c6e4456936960cac9e31ba657b5 Mon Sep 17 00:00:00 2001 From: Caleb Burke <calebburke91@gmail.com> Date: Thu, 24 Sep 2026 23:00:45 -0700 Subject: [PATCH 17/26] handy invalid handle macro --- engine/include/sophia/handles.hpp | 9 +++++++++ examples/boids_2d/main.cpp | 9 +++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/engine/include/sophia/handles.hpp b/engine/include/sophia/handles.hpp index 3a97886..242535f 100644 --- a/engine/include/sophia/handles.hpp +++ b/engine/include/sophia/handles.hpp @@ -24,6 +24,15 @@ struct Handle { [[nodiscard]] auto operator<=>(const Handle&) const = default; }; +struct InvalidHandle { + template <typename T> + [[nodiscard]] constexpr operator Handle<T>() const { + return Handle<T>::invalid(); + } +}; + +inline constexpr InvalidHandle INVALID_HANDLE{}; + class Mesh; class Panel; class RenderTarget; diff --git a/examples/boids_2d/main.cpp b/examples/boids_2d/main.cpp index 3d723fd..8dcaa57 100644 --- a/examples/boids_2d/main.cpp +++ b/examples/boids_2d/main.cpp @@ -1,6 +1,9 @@ #include <algorithm> #include <cmath> #include <cstdint> +#include <glm/glm.hpp> +#include <glm/gtc/constants.hpp> +#include <glm/gtc/matrix_transform.hpp> #include <iostream> #include <random> #include <sophia/application.hpp> @@ -10,12 +13,6 @@ #include <string> #include <vector> -#define GLM_FORCE_RADIANS -#define GLM_FORCE_DEPTH_ZERO_TO_ONE -#include <glm/glm.hpp> -#include <glm/gtc/constants.hpp> -#include <glm/gtc/matrix_transform.hpp> - struct Boid { glm::vec2 position; glm::vec2 velocity; From ebc05435358032966d065c8138762962166c1cb1 Mon Sep 17 00:00:00 2001 From: Caleb Burke <calebburke91@gmail.com> Date: Fri, 25 Sep 2026 19:04:01 -0700 Subject: [PATCH 18/26] better dev command --- CLAUDE.md | 6 +++--- bin/dev | 11 ++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 86b81fd..a021cc1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,14 +2,14 @@ ## Dev helper (`bin/dev`) -`bin/dev` (on PATH via direnv as `dev`) wraps common project tasks. Run `dev bash-completions` to list commands. +`bin/dev` wraps common project tasks. Run `bin/dev bash-completions` to list commands. ### Creating issues -Use `dev issue` to create Gitea issues instead of calling the API or `tea` directly: +Use `bin/dev issue` to create Gitea issues instead of calling the API or `tea` directly: ``` -dev issue "<title>" [-b "<body>"] [-l <label>]... [-a <user>]... [--open] +bin/dev issue "<title>" [-b "<body>"] [-l <label>]... [-a <user>]... [--open] ``` - Labels are repo label names (`bug`, `enhancement`, `question`, ...). diff --git a/bin/dev b/bin/dev index dfb676f..3519cf2 100755 --- a/bin/dev +++ b/bin/dev @@ -27,6 +27,9 @@ class DevHelper: # Core logic def call(self, *args, **kwargs): if not args: + print("Usage: dev <command> [args...]\n\nCommands:") + for command in self._commands(): + print(f" {command}") return command, rest = args[0], args[1:] @@ -136,14 +139,16 @@ class DevHelper: raise NotImplementedError(f"Not implemented for platform {sys.platform}") def bash_completions(self, *args, **kwargs): - completions = sorted( + print(" ".join(self._commands())) + + # Private helpers + def _commands(self): + return sorted( METHOD_TO_COMMAND.get(name, name) for name in vars(DevHelper) if not name.startswith("_") and name != "call" and callable(getattr(DevHelper, name)) ) - print(" ".join(completions)) - # Private helpers def _parse_issue_url(self, url): match = re.match(r"^(https?://[^/]+)/([^/]+)/([^/]+)/issues/(\d+)", url) if match is None: From 15932879e814f5294158a36b02959b16ca0255ef Mon Sep 17 00:00:00 2001 From: Caleb Burke <calebburke91@gmail.com> Date: Fri, 25 Sep 2026 19:21:41 -0700 Subject: [PATCH 19/26] CLAUDE updates --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index a021cc1..cc57b33 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ Use `bin/dev issue` to create Gitea issues instead of calling the API or `tea` directly: -``` +```sh bin/dev issue "<title>" [-b "<body>"] [-l <label>]... [-a <user>]... [--open] ``` From 86b36f27c638df53b23f78d1417721add60c4aae Mon Sep 17 00:00:00 2001 From: Caleb Burke <calebburke91@gmail.com> Date: Fri, 25 Sep 2026 19:32:33 -0700 Subject: [PATCH 20/26] moving dir name --- {_Design => docs}/glossary.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {_Design => docs}/glossary.md (100%) diff --git a/_Design/glossary.md b/docs/glossary.md similarity index 100% rename from _Design/glossary.md rename to docs/glossary.md From 1ccb591959be945409c6497074a1a5e2e5d099e7 Mon Sep 17 00:00:00 2001 From: Caleb Burke <calebburke91@gmail.com> Date: Fri, 25 Sep 2026 19:58:11 -0700 Subject: [PATCH 21/26] build system update --- .vscode/settings.json | 4 +-- CMakeLists.txt | 4 +-- CMakePresets.json | 66 ++++++++++++++++++++++++++++++++++++++++ README.md | 12 +++++--- bin/dev | 71 +++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 146 insertions(+), 11 deletions(-) create mode 100644 CMakePresets.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 9fc89bf..43db259 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -41,8 +41,8 @@ "LD_LIBRARY_PATH": "/home/caleb/.local/share/vulkan-sdk/1.4.357.1/x86_64/lib/VulkanLoader/lib:/home/caleb/.local/share/vulkan-sdk/1.4.357.1/x86_64/lib", "VK_ADD_LAYER_PATH": "/home/caleb/.local/share/vulkan-sdk/1.4.357.1/x86_64/share/vulkan/explicit_layer.d" }, - "cmake.buildDirectory": "${workspaceFolder}/build", - "cmake.generator": "Ninja", + "cmake.useCMakePresets": "always", + "cmake.copyCompileCommands": "${workspaceFolder}/build/compile_commands.json", "cmake.configureOnOpen": true, "files.associations": { ".clang-format": "yaml", diff --git a/CMakeLists.txt b/CMakeLists.txt index 56431db..89400a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.22.2) +cmake_minimum_required(VERSION 3.25.2) set(NAME sophia) @@ -15,8 +15,6 @@ endif() if(CMAKE_BUILD_TYPE STREQUAL "Debug") message(STATUS "Debug build: enabling debug logging") -else() - add_compile_definitions(NDEBUG) endif() set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..c5e528c --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,66 @@ +{ + "version": 6, + "configurePresets": [ + { + "name": "base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "debug", + "displayName": "Debug", + "inherits": "base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "release", + "displayName": "Release (with debug info)", + "inherits": "base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo" + } + }, + { + "name": "asan", + "displayName": "Debug + AddressSanitizer/UBSan", + "inherits": "debug", + "cacheVariables": { + "CMAKE_CXX_FLAGS": "-fsanitize=address,undefined -fno-omit-frame-pointer", + "CMAKE_C_FLAGS": "-fsanitize=address,undefined -fno-omit-frame-pointer" + } + }, + { + "name": "tsan", + "displayName": "Debug + ThreadSanitizer", + "inherits": "debug", + "cacheVariables": { + "CMAKE_CXX_FLAGS": "-fsanitize=thread", + "CMAKE_C_FLAGS": "-fsanitize=thread" + } + } + ], + "buildPresets": [ + { + "name": "debug", + "configurePreset": "debug" + }, + { + "name": "release", + "configurePreset": "release" + }, + { + "name": "asan", + "configurePreset": "asan" + }, + { + "name": "tsan", + "configurePreset": "tsan" + } + ] +} diff --git a/README.md b/README.md index 2637f3c..6e268ef 100644 --- a/README.md +++ b/README.md @@ -8,17 +8,21 @@ COMING SOON ### Dependencies -- [cmake](https://cmake.org/) +- [cmake](https://cmake.org/) 3.25+ +- [Ninja](https://ninja-build.org/) - [Vulkan SDK](https://vulkan.lunarg.com/) - With validation layers ### Build instructions +Builds use the presets in `CMakePresets.json` (`debug`, `release`), each in its own `build/<preset>` directory: + ```sh -cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -cd build -make -j +cmake --preset debug +cmake --build --preset debug ``` +Or with the dev helper: `dev build [preset] [--fresh]`. + ## Third Party libraries used - [GLFW](https://github.com/glfw/glfw) - Window and input handling diff --git a/bin/dev b/bin/dev index 3519cf2..f9425eb 100755 --- a/bin/dev +++ b/bin/dev @@ -20,6 +20,7 @@ WAIT_FOR_PROCESS = "wait_for_process" class DevHelper: SHADER_EXTENSIONS = (".vert", ".frag", ".comp", ".geom", ".tesc", ".tese") PROJECT_NAME = "sophia" + DEFAULT_PRESET = "debug" def __init__(self): self.last_exit_status = 0 @@ -39,9 +40,29 @@ class DevHelper: return method(*rest, **kwargs) # Primary command wrappers + + ## + # Configures and builds a CMake preset (see CMakePresets.json). + # dev build [preset] [--fresh] + # The preset defaults to debug and builds into build/<preset>. --fresh + # discards the preset's CMake cache before configuring. def build(self, *args, **kwargs): - command = f"rm -rf build && cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug" - self._exec_in_root(command, replace=True) + preset, fresh = self._parse_preset_args(args, "build") + self._build_preset(preset, fresh) + + ## + # Builds the debug preset (incrementally), then runs the testbed from + # build/debug. Any arguments are passed through to the testbed. + # dev testbed [args...] + def testbed(self, *args, **kwargs): + preset = self.DEFAULT_PRESET + self._build_preset(preset, fresh=False) + + preset_dir = os.path.join(self._project_root(), "build", preset) + binary = os.path.join(preset_dir, "bin", "testbed") + print(f"Running: {shlex.join([binary, *args])}") + os.chdir(preset_dir) + os.execv(binary, [binary, *args]) ## # Checks out a branch named after a Gitea issue URL. @@ -149,6 +170,52 @@ class DevHelper: if not name.startswith("_") and name != "call" and callable(getattr(DevHelper, name)) ) + def _parse_preset_args(self, args, command): + preset = None + fresh = False + for arg in args: + if arg == "--fresh": + fresh = True + elif arg.startswith("-"): + raise ValueError(f"Unknown option: {arg}") + elif preset is None: + preset = arg + else: + raise ValueError(f"Usage: dev {command} [preset] [--fresh]") + + preset = preset or self.DEFAULT_PRESET + presets = self._configure_presets() + if preset not in presets: + raise ValueError(f"Unknown preset: {preset} (available: {', '.join(presets)})") + return preset, fresh + + def _configure_presets(self): + with open(os.path.join(self._project_root(), "CMakePresets.json")) as f: + presets = json.load(f)["configurePresets"] + return [preset["name"] for preset in presets if not preset.get("hidden")] + + def _build_preset(self, preset, fresh): + configure = ["cmake", "--preset", preset] + if fresh: + configure.append("--fresh") + self._run_in_root(configure) + self._copy_compile_commands(preset) + self._run_in_root(["cmake", "--build", "--preset", preset]) + + # Editor tooling reads build/compile_commands.json, so point it at the + # most recently configured preset. + def _copy_compile_commands(self, preset): + build_dir = os.path.join(self._project_root(), "build") + source = os.path.join(build_dir, preset, "compile_commands.json") + if os.path.isfile(source): + shutil.copyfile(source, os.path.join(build_dir, "compile_commands.json")) + + def _run_in_root(self, command): + print(f"Running: {shlex.join(command)}") + result = subprocess.run(command, cwd=self._project_root()) + if result.returncode != 0: + sys.exit(result.returncode) + def _parse_issue_url(self, url): match = re.match(r"^(https?://[^/]+)/([^/]+)/([^/]+)/issues/(\d+)", url) if match is None: From 09706507cfc615a47dbcdd89e7efcffdce733b43 Mon Sep 17 00:00:00 2001 From: Caleb Burke <calebburke91@gmail.com> Date: Fri, 25 Sep 2026 20:09:51 -0700 Subject: [PATCH 22/26] adding compiled shaders to project --- .gitignore | 1 - engine/src/shaders/fullscreen.vert.spv.h | 100 ++++++++++++++++++ .../shaders/model_render_system.frag.spv.h | 35 ++++++ .../shaders/model_render_system.vert.spv.h | 69 ++++++++++++ 4 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 engine/src/shaders/fullscreen.vert.spv.h create mode 100644 engine/src/shaders/model_render_system.frag.spv.h create mode 100644 engine/src/shaders/model_render_system.vert.spv.h diff --git a/.gitignore b/.gitignore index aa505e8..02d09c9 100644 --- a/.gitignore +++ b/.gitignore @@ -24,7 +24,6 @@ CMakeUserPresets.json *.bin *.out *.spv -*.spv.h # Runtime data pids diff --git a/engine/src/shaders/fullscreen.vert.spv.h b/engine/src/shaders/fullscreen.vert.spv.h new file mode 100644 index 0000000..08b2353 --- /dev/null +++ b/engine/src/shaders/fullscreen.vert.spv.h @@ -0,0 +1,100 @@ +#pragma once + +#include <array> +#include <cstdint> + +namespace sophia::shaders { + +inline constexpr std::array<uint32_t, 355> fullscreen_vert_spv = { + 0x07230203, 0x00010000, 0x000d000b, 0x00000032, + 0x00000000, 0x00020011, 0x00000001, 0x0006000b, + 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, + 0x00000000, 0x0003000e, 0x00000000, 0x00000001, + 0x0008000f, 0x00000000, 0x00000004, 0x6e69616d, + 0x00000000, 0x00000018, 0x00000022, 0x0000002c, + 0x00030003, 0x00000002, 0x000001c2, 0x000a0004, + 0x475f4c47, 0x4c474f4f, 0x70635f45, 0x74735f70, + 0x5f656c79, 0x656e696c, 0x7269645f, 0x69746365, + 0x00006576, 0x00080004, 0x475f4c47, 0x4c474f4f, + 0x6e695f45, 0x64756c63, 0x69645f65, 0x74636572, + 0x00657669, 0x00040005, 0x00000004, 0x6e69616d, + 0x00000000, 0x00050005, 0x0000000c, 0x69736f70, + 0x6e6f6974, 0x00000073, 0x00030005, 0x00000015, + 0x00736f70, 0x00060005, 0x00000018, 0x565f6c67, + 0x65747265, 0x646e4978, 0x00007865, 0x00060005, + 0x00000020, 0x505f6c67, 0x65567265, 0x78657472, + 0x00000000, 0x00060006, 0x00000020, 0x00000000, + 0x505f6c67, 0x7469736f, 0x006e6f69, 0x00070006, + 0x00000020, 0x00000001, 0x505f6c67, 0x746e696f, + 0x657a6953, 0x00000000, 0x00070006, 0x00000020, + 0x00000002, 0x435f6c67, 0x4470696c, 0x61747369, + 0x0065636e, 0x00070006, 0x00000020, 0x00000003, + 0x435f6c67, 0x446c6c75, 0x61747369, 0x0065636e, + 0x00030005, 0x00000022, 0x00000000, 0x00030005, + 0x0000002c, 0x00007675, 0x00040047, 0x00000018, + 0x0000000b, 0x0000002a, 0x00030047, 0x00000020, + 0x00000002, 0x00050048, 0x00000020, 0x00000000, + 0x0000000b, 0x00000000, 0x00050048, 0x00000020, + 0x00000001, 0x0000000b, 0x00000001, 0x00050048, + 0x00000020, 0x00000002, 0x0000000b, 0x00000003, + 0x00050048, 0x00000020, 0x00000003, 0x0000000b, + 0x00000004, 0x00040047, 0x0000002c, 0x0000001e, + 0x00000000, 0x00020013, 0x00000002, 0x00030021, + 0x00000003, 0x00000002, 0x00030016, 0x00000006, + 0x00000020, 0x00040017, 0x00000007, 0x00000006, + 0x00000002, 0x00040015, 0x00000008, 0x00000020, + 0x00000000, 0x0004002b, 0x00000008, 0x00000009, + 0x00000006, 0x0004001c, 0x0000000a, 0x00000007, + 0x00000009, 0x00040020, 0x0000000b, 0x00000006, + 0x0000000a, 0x0004003b, 0x0000000b, 0x0000000c, + 0x00000006, 0x0004002b, 0x00000006, 0x0000000d, + 0xbf800000, 0x0005002c, 0x00000007, 0x0000000e, + 0x0000000d, 0x0000000d, 0x0004002b, 0x00000006, + 0x0000000f, 0x3f800000, 0x0005002c, 0x00000007, + 0x00000010, 0x0000000f, 0x0000000d, 0x0005002c, + 0x00000007, 0x00000011, 0x0000000d, 0x0000000f, + 0x0005002c, 0x00000007, 0x00000012, 0x0000000f, + 0x0000000f, 0x0009002c, 0x0000000a, 0x00000013, + 0x0000000e, 0x00000010, 0x00000011, 0x00000011, + 0x00000010, 0x00000012, 0x00040020, 0x00000014, + 0x00000007, 0x00000007, 0x00040015, 0x00000016, + 0x00000020, 0x00000001, 0x00040020, 0x00000017, + 0x00000001, 0x00000016, 0x0004003b, 0x00000017, + 0x00000018, 0x00000001, 0x00040020, 0x0000001a, + 0x00000006, 0x00000007, 0x00040017, 0x0000001d, + 0x00000006, 0x00000004, 0x0004002b, 0x00000008, + 0x0000001e, 0x00000001, 0x0004001c, 0x0000001f, + 0x00000006, 0x0000001e, 0x0006001e, 0x00000020, + 0x0000001d, 0x00000006, 0x0000001f, 0x0000001f, + 0x00040020, 0x00000021, 0x00000003, 0x00000020, + 0x0004003b, 0x00000021, 0x00000022, 0x00000003, + 0x0004002b, 0x00000016, 0x00000023, 0x00000000, + 0x0004002b, 0x00000006, 0x00000025, 0x00000000, + 0x00040020, 0x00000029, 0x00000003, 0x0000001d, + 0x00040020, 0x0000002b, 0x00000003, 0x00000007, + 0x0004003b, 0x0000002b, 0x0000002c, 0x00000003, + 0x0004002b, 0x00000006, 0x0000002e, 0x3f000000, + 0x00050036, 0x00000002, 0x00000004, 0x00000000, + 0x00000003, 0x000200f8, 0x00000005, 0x0004003b, + 0x00000014, 0x00000015, 0x00000007, 0x0003003e, + 0x0000000c, 0x00000013, 0x0004003d, 0x00000016, + 0x00000019, 0x00000018, 0x00050041, 0x0000001a, + 0x0000001b, 0x0000000c, 0x00000019, 0x0004003d, + 0x00000007, 0x0000001c, 0x0000001b, 0x0003003e, + 0x00000015, 0x0000001c, 0x0004003d, 0x00000007, + 0x00000024, 0x00000015, 0x00050051, 0x00000006, + 0x00000026, 0x00000024, 0x00000000, 0x00050051, + 0x00000006, 0x00000027, 0x00000024, 0x00000001, + 0x00070050, 0x0000001d, 0x00000028, 0x00000026, + 0x00000027, 0x00000025, 0x0000000f, 0x00050041, + 0x00000029, 0x0000002a, 0x00000022, 0x00000023, + 0x0003003e, 0x0000002a, 0x00000028, 0x0004003d, + 0x00000007, 0x0000002d, 0x00000015, 0x0005008e, + 0x00000007, 0x0000002f, 0x0000002d, 0x0000002e, + 0x00050050, 0x00000007, 0x00000030, 0x0000002e, + 0x0000002e, 0x00050081, 0x00000007, 0x00000031, + 0x0000002f, 0x00000030, 0x0003003e, 0x0000002c, + 0x00000031, 0x000100fd, 0x00010038, +}; + +} // namespace sophia::shaders diff --git a/engine/src/shaders/model_render_system.frag.spv.h b/engine/src/shaders/model_render_system.frag.spv.h new file mode 100644 index 0000000..e5f1c2f --- /dev/null +++ b/engine/src/shaders/model_render_system.frag.spv.h @@ -0,0 +1,35 @@ +#pragma once + +#include <array> +#include <cstdint> + +namespace sophia::shaders { + +inline constexpr std::array<uint32_t, 143> model_render_system_frag_spv = { + 0x07230203, 0x00010000, 0x000d000b, 0x00000013, 0x00000000, 0x00020011, + 0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, + 0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0007000f, 0x00000004, + 0x00000004, 0x6e69616d, 0x00000000, 0x00000009, 0x0000000c, 0x00030010, + 0x00000004, 0x00000007, 0x00030003, 0x00000002, 0x000001c2, 0x000a0004, + 0x475f4c47, 0x4c474f4f, 0x70635f45, 0x74735f70, 0x5f656c79, 0x656e696c, + 0x7269645f, 0x69746365, 0x00006576, 0x00080004, 0x475f4c47, 0x4c474f4f, + 0x6e695f45, 0x64756c63, 0x69645f65, 0x74636572, 0x00657669, 0x00040005, + 0x00000004, 0x6e69616d, 0x00000000, 0x00050005, 0x00000009, 0x4374756f, + 0x726f6c6f, 0x00000000, 0x00050005, 0x0000000c, 0x67617266, 0x6f6c6f43, + 0x00000072, 0x00040047, 0x00000009, 0x0000001e, 0x00000000, 0x00040047, + 0x0000000c, 0x0000001e, 0x00000000, 0x00020013, 0x00000002, 0x00030021, + 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, + 0x00000007, 0x00000006, 0x00000004, 0x00040020, 0x00000008, 0x00000003, + 0x00000007, 0x0004003b, 0x00000008, 0x00000009, 0x00000003, 0x00040017, + 0x0000000a, 0x00000006, 0x00000003, 0x00040020, 0x0000000b, 0x00000001, + 0x0000000a, 0x0004003b, 0x0000000b, 0x0000000c, 0x00000001, 0x0004002b, + 0x00000006, 0x0000000e, 0x3f800000, 0x00050036, 0x00000002, 0x00000004, + 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x0004003d, 0x0000000a, + 0x0000000d, 0x0000000c, 0x00050051, 0x00000006, 0x0000000f, 0x0000000d, + 0x00000000, 0x00050051, 0x00000006, 0x00000010, 0x0000000d, 0x00000001, + 0x00050051, 0x00000006, 0x00000011, 0x0000000d, 0x00000002, 0x00070050, + 0x00000007, 0x00000012, 0x0000000f, 0x00000010, 0x00000011, 0x0000000e, + 0x0003003e, 0x00000009, 0x00000012, 0x000100fd, 0x00010038, +}; + +} // namespace sophia::shaders diff --git a/engine/src/shaders/model_render_system.vert.spv.h b/engine/src/shaders/model_render_system.vert.spv.h new file mode 100644 index 0000000..0ad2b04 --- /dev/null +++ b/engine/src/shaders/model_render_system.vert.spv.h @@ -0,0 +1,69 @@ +#pragma once + +#include <array> +#include <cstdint> + +namespace sophia::shaders { + +inline constexpr std::array<uint32_t, 345> model_render_system_vert_spv = { + 0x07230203, 0x00010000, 0x000d000b, 0x00000029, 0x00000000, 0x00020011, + 0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, + 0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0009000f, 0x00000000, + 0x00000004, 0x6e69616d, 0x00000000, 0x0000000d, 0x00000019, 0x00000025, + 0x00000027, 0x00030003, 0x00000002, 0x000001c2, 0x000a0004, 0x475f4c47, + 0x4c474f4f, 0x70635f45, 0x74735f70, 0x5f656c79, 0x656e696c, 0x7269645f, + 0x69746365, 0x00006576, 0x00080004, 0x475f4c47, 0x4c474f4f, 0x6e695f45, + 0x64756c63, 0x69645f65, 0x74636572, 0x00657669, 0x00040005, 0x00000004, + 0x6e69616d, 0x00000000, 0x00060005, 0x0000000b, 0x505f6c67, 0x65567265, + 0x78657472, 0x00000000, 0x00060006, 0x0000000b, 0x00000000, 0x505f6c67, + 0x7469736f, 0x006e6f69, 0x00070006, 0x0000000b, 0x00000001, 0x505f6c67, + 0x746e696f, 0x657a6953, 0x00000000, 0x00070006, 0x0000000b, 0x00000002, + 0x435f6c67, 0x4470696c, 0x61747369, 0x0065636e, 0x00070006, 0x0000000b, + 0x00000003, 0x435f6c67, 0x446c6c75, 0x61747369, 0x0065636e, 0x00030005, + 0x0000000d, 0x00000000, 0x00040005, 0x00000011, 0x68737550, 0x00000000, + 0x00060006, 0x00000011, 0x00000000, 0x6e617274, 0x726f6673, 0x0000006d, + 0x00050006, 0x00000011, 0x00000001, 0x6f6c6f63, 0x00000072, 0x00040005, + 0x00000013, 0x68737570, 0x00000000, 0x00050005, 0x00000019, 0x69736f70, + 0x6e6f6974, 0x00000000, 0x00050005, 0x00000025, 0x67617266, 0x6f6c6f43, + 0x00000072, 0x00040005, 0x00000027, 0x6f6c6f63, 0x00000072, 0x00030047, + 0x0000000b, 0x00000002, 0x00050048, 0x0000000b, 0x00000000, 0x0000000b, + 0x00000000, 0x00050048, 0x0000000b, 0x00000001, 0x0000000b, 0x00000001, + 0x00050048, 0x0000000b, 0x00000002, 0x0000000b, 0x00000003, 0x00050048, + 0x0000000b, 0x00000003, 0x0000000b, 0x00000004, 0x00030047, 0x00000011, + 0x00000002, 0x00040048, 0x00000011, 0x00000000, 0x00000005, 0x00050048, + 0x00000011, 0x00000000, 0x00000007, 0x00000010, 0x00050048, 0x00000011, + 0x00000000, 0x00000023, 0x00000000, 0x00050048, 0x00000011, 0x00000001, + 0x00000023, 0x00000040, 0x00040047, 0x00000019, 0x0000001e, 0x00000000, + 0x00040047, 0x00000025, 0x0000001e, 0x00000000, 0x00040047, 0x00000027, + 0x0000001e, 0x00000001, 0x00020013, 0x00000002, 0x00030021, 0x00000003, + 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, + 0x00000006, 0x00000004, 0x00040015, 0x00000008, 0x00000020, 0x00000000, + 0x0004002b, 0x00000008, 0x00000009, 0x00000001, 0x0004001c, 0x0000000a, + 0x00000006, 0x00000009, 0x0006001e, 0x0000000b, 0x00000007, 0x00000006, + 0x0000000a, 0x0000000a, 0x00040020, 0x0000000c, 0x00000003, 0x0000000b, + 0x0004003b, 0x0000000c, 0x0000000d, 0x00000003, 0x00040015, 0x0000000e, + 0x00000020, 0x00000001, 0x0004002b, 0x0000000e, 0x0000000f, 0x00000000, + 0x00040018, 0x00000010, 0x00000007, 0x00000004, 0x0004001e, 0x00000011, + 0x00000010, 0x00000007, 0x00040020, 0x00000012, 0x00000009, 0x00000011, + 0x0004003b, 0x00000012, 0x00000013, 0x00000009, 0x00040020, 0x00000014, + 0x00000009, 0x00000010, 0x00040017, 0x00000017, 0x00000006, 0x00000002, + 0x00040020, 0x00000018, 0x00000001, 0x00000017, 0x0004003b, 0x00000018, + 0x00000019, 0x00000001, 0x0004002b, 0x00000006, 0x0000001b, 0x00000000, + 0x0004002b, 0x00000006, 0x0000001c, 0x3f800000, 0x00040020, 0x00000021, + 0x00000003, 0x00000007, 0x00040017, 0x00000023, 0x00000006, 0x00000003, + 0x00040020, 0x00000024, 0x00000003, 0x00000023, 0x0004003b, 0x00000024, + 0x00000025, 0x00000003, 0x00040020, 0x00000026, 0x00000001, 0x00000023, + 0x0004003b, 0x00000026, 0x00000027, 0x00000001, 0x00050036, 0x00000002, + 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x00050041, + 0x00000014, 0x00000015, 0x00000013, 0x0000000f, 0x0004003d, 0x00000010, + 0x00000016, 0x00000015, 0x0004003d, 0x00000017, 0x0000001a, 0x00000019, + 0x00050051, 0x00000006, 0x0000001d, 0x0000001a, 0x00000000, 0x00050051, + 0x00000006, 0x0000001e, 0x0000001a, 0x00000001, 0x00070050, 0x00000007, + 0x0000001f, 0x0000001d, 0x0000001e, 0x0000001b, 0x0000001c, 0x00050091, + 0x00000007, 0x00000020, 0x00000016, 0x0000001f, 0x00050041, 0x00000021, + 0x00000022, 0x0000000d, 0x0000000f, 0x0003003e, 0x00000022, 0x00000020, + 0x0004003d, 0x00000023, 0x00000028, 0x00000027, 0x0003003e, 0x00000025, + 0x00000028, 0x000100fd, 0x00010038, +}; + +} // namespace sophia::shaders From 4ebae57cf8b273dce27e916a030a20f7b009bbd2 Mon Sep 17 00:00:00 2001 From: Caleb Burke <calebburke91@gmail.com> Date: Fri, 25 Sep 2026 20:21:25 -0700 Subject: [PATCH 23/26] Moving stuff around --- engine/{src => include/sophia}/entity.hpp | 0 engine/src/{ => scene}/entity.cpp | 2 +- engine/src/{ => scene}/scene.cpp | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename engine/{src => include/sophia}/entity.hpp (100%) rename engine/src/{ => scene}/entity.cpp (82%) rename engine/src/{ => scene}/scene.cpp (100%) diff --git a/engine/src/entity.hpp b/engine/include/sophia/entity.hpp similarity index 100% rename from engine/src/entity.hpp rename to engine/include/sophia/entity.hpp diff --git a/engine/src/entity.cpp b/engine/src/scene/entity.cpp similarity index 82% rename from engine/src/entity.cpp rename to engine/src/scene/entity.cpp index eed9281..553a759 100644 --- a/engine/src/entity.cpp +++ b/engine/src/scene/entity.cpp @@ -1,4 +1,4 @@ -#include "entity.hpp" +#include <sophia/entity.hpp> namespace sophia { diff --git a/engine/src/scene.cpp b/engine/src/scene/scene.cpp similarity index 100% rename from engine/src/scene.cpp rename to engine/src/scene/scene.cpp From 0d145f615d404abbfde19fde54af608e57a1def0 Mon Sep 17 00:00:00 2001 From: Caleb Burke <calebburke91@gmail.com> Date: Fri, 25 Sep 2026 20:22:29 -0700 Subject: [PATCH 24/26] changes --- engine/include/sophia/util/pool.hpp | 11 ++++++++ engine/src/rendering/render_graph.cpp | 11 +++++++- engine/src/rendering/render_graph.hpp | 4 ++- engine/src/rendering/scene_renderer.cpp | 12 ++++++--- engine/src/rendering/scene_renderer.hpp | 11 ++++---- engine/src/resources/render_target.cpp | 2 +- engine/src/vulkan/debug_label.cpp | 34 +++++++++++++++++++++++++ engine/src/vulkan/debug_label.hpp | 13 ++++++++++ engine/src/vulkan/device.cpp | 4 +++ testbed/main.cpp | 14 +++++++--- 10 files changed, 100 insertions(+), 16 deletions(-) create mode 100644 engine/src/vulkan/debug_label.cpp create mode 100644 engine/src/vulkan/debug_label.hpp diff --git a/engine/include/sophia/util/pool.hpp b/engine/include/sophia/util/pool.hpp index 083aa95..6ab2416 100644 --- a/engine/include/sophia/util/pool.hpp +++ b/engine/include/sophia/util/pool.hpp @@ -75,6 +75,17 @@ class Pool { } } + /** Calls `fn(handle, item)` for every live item. */ + template <typename Fn> + void forEachWithHandle(Fn&& fn) const { + for (u32 index = 0; index < slots.size(); index++) { + const Slot& slot = slots[index]; + if (slot.item) { + fn(Handle<T>{index, slot.generation}, *slot.item); + } + } + } + template <typename Predicate> T* findIf(Predicate&& predicate) { return const_cast<T*>( diff --git a/engine/src/rendering/render_graph.cpp b/engine/src/rendering/render_graph.cpp index b9a1e60..b15d6b6 100644 --- a/engine/src/rendering/render_graph.cpp +++ b/engine/src/rendering/render_graph.cpp @@ -3,6 +3,8 @@ #include <sophia/util/assert.hpp> #include <utility> +#include "vulkan/debug_label.hpp" + namespace sophia { ImageRef RenderGraph::importImage(Image& image, Access finalAccess) { @@ -74,6 +76,8 @@ void RenderGraph::flush(vk::CommandBuffer commandBuffer, } void RenderGraph::recordPass(vk::CommandBuffer commandBuffer, Pass& pass) { + beginDebugLabel(commandBuffer, pass.name.c_str()); + PendingBarriers pending; for (const ImageUse& use : pass.uses) { this->transitionTo(use.image, getAccessState(use.access), pending); @@ -82,9 +86,14 @@ void RenderGraph::recordPass(vk::CommandBuffer commandBuffer, Pass& pass) { if (pass.colorAttachments.empty()) { pass.execute(commandBuffer); - return; + } else { + this->recordRendering(commandBuffer, pass); } + endDebugLabel(commandBuffer); +} + +void RenderGraph::recordRendering(vk::CommandBuffer commandBuffer, Pass& pass) { std::vector<vk::RenderingAttachmentInfo> attachments; attachments.reserve(pass.colorAttachments.size()); diff --git a/engine/src/rendering/render_graph.hpp b/engine/src/rendering/render_graph.hpp index d7374e3..ff2cd32 100644 --- a/engine/src/rendering/render_graph.hpp +++ b/engine/src/rendering/render_graph.hpp @@ -67,7 +67,8 @@ class RenderGraph { /** * Adds a pass. `setup` runs immediately and declares what the pass does to * images; `execute` runs later, from execute(), with the barriers already - * recorded and (if the pass writes color) rendering begun. + * recorded and (if the pass writes color) rendering begun. `name` labels + * the pass's commands in debugging tools such as RenderDoc. */ void addPass(std::string name, const SetupFn& setup, ExecuteFn execute); @@ -112,6 +113,7 @@ class RenderGraph { PendingBarriers& pending); void flush(vk::CommandBuffer commandBuffer, PendingBarriers& pending); void recordPass(vk::CommandBuffer commandBuffer, Pass& pass); + void recordRendering(vk::CommandBuffer commandBuffer, Pass& pass); std::vector<ImportedImage> images; std::vector<Pass> passes; diff --git a/engine/src/rendering/scene_renderer.cpp b/engine/src/rendering/scene_renderer.cpp index dea107d..fc1e8be 100644 --- a/engine/src/rendering/scene_renderer.cpp +++ b/engine/src/rendering/scene_renderer.cpp @@ -12,13 +12,17 @@ struct PushConstantData { }; } // namespace -SceneRenderer::SceneRenderer(ResourceRegistry& resources) - : resources{resources} {} +SceneRenderer::SceneRenderer(const SceneRegistry& sceneRegistry, + ResourceRegistry& resources) + : sceneRegistry{sceneRegistry}, resources{resources} {} SceneRenderer::~SceneRenderer() {} -void SceneRenderer::render(const Scene& scene, Entity camera, - RenderTargetHandle target) { +void SceneRenderer::render(SceneView sceneView) { + auto& handle = sceneView.scene; + auto& scene = this->sceneRegistry.get(handle); + + auto& camera = sceneView.camera; SOPHIA_ASSERT(camera.hasComponent<CameraComponent>(), "camera does not have camera component!"); diff --git a/engine/src/rendering/scene_renderer.hpp b/engine/src/rendering/scene_renderer.hpp index 0eaf29c..abaf69b 100644 --- a/engine/src/rendering/scene_renderer.hpp +++ b/engine/src/rendering/scene_renderer.hpp @@ -1,9 +1,8 @@ #pragma once -#include <sophia/handles.hpp> -#include <sophia/scene.hpp> +#include <sophia/scene_registry.hpp> +#include <sophia/scene_view.hpp> -#include "entity.hpp" #include "resources/resource_registry.hpp" namespace sophia { @@ -16,12 +15,14 @@ class SceneRenderer { SceneRenderer(const SceneRenderer&) = delete; SceneRenderer& operator=(const SceneRenderer&) = delete; - SceneRenderer(ResourceRegistry& resources); + SceneRenderer(const SceneRegistry& sceneRegistry, + ResourceRegistry& resources); ~SceneRenderer(); - void render(const Scene& scene, Entity camera, RenderTargetHandle target); + void render(SceneView sceneView); private: + const SceneRegistry& sceneRegistry; ResourceRegistry& resources; }; diff --git a/engine/src/resources/render_target.cpp b/engine/src/resources/render_target.cpp index 0005943..b4e81e1 100644 --- a/engine/src/resources/render_target.cpp +++ b/engine/src/resources/render_target.cpp @@ -32,8 +32,8 @@ void RenderTarget::applyPendingResize() { return; } - // Earlier frames may still be sampling the old image (e.g. through the UI). this->device.waitIdle(); + destroy(); create(extent.width, extent.height); } diff --git a/engine/src/vulkan/debug_label.cpp b/engine/src/vulkan/debug_label.cpp new file mode 100644 index 0000000..3d44e45 --- /dev/null +++ b/engine/src/vulkan/debug_label.cpp @@ -0,0 +1,34 @@ +#include "vulkan/debug_label.hpp" + +namespace sophia { + +static PFN_vkCmdBeginDebugUtilsLabelEXT cmdBeginDebugUtilsLabel = nullptr; +static PFN_vkCmdEndDebugUtilsLabelEXT cmdEndDebugUtilsLabel = nullptr; + +void loadDebugLabelFunctions(vk::Instance instance) { + cmdBeginDebugUtilsLabel = reinterpret_cast<PFN_vkCmdBeginDebugUtilsLabelEXT>( + vkGetInstanceProcAddr(instance, "vkCmdBeginDebugUtilsLabelEXT")); + cmdEndDebugUtilsLabel = reinterpret_cast<PFN_vkCmdEndDebugUtilsLabelEXT>( + vkGetInstanceProcAddr(instance, "vkCmdEndDebugUtilsLabelEXT")); +} + +void beginDebugLabel(vk::CommandBuffer commandBuffer, const char* name) { + if (!cmdBeginDebugUtilsLabel) { + return; + } + + VkDebugUtilsLabelEXT label{}; + label.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT; + label.pLabelName = name; + cmdBeginDebugUtilsLabel(commandBuffer, &label); +} + +void endDebugLabel(vk::CommandBuffer commandBuffer) { + if (!cmdEndDebugUtilsLabel) { + return; + } + + cmdEndDebugUtilsLabel(commandBuffer); +} + +} // namespace sophia diff --git a/engine/src/vulkan/debug_label.hpp b/engine/src/vulkan/debug_label.hpp new file mode 100644 index 0000000..95401c4 --- /dev/null +++ b/engine/src/vulkan/debug_label.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include <vulkan/vulkan.hpp> + +namespace sophia { + +void loadDebugLabelFunctions(vk::Instance instance); + +void beginDebugLabel(vk::CommandBuffer commandBuffer, const char* name); + +void endDebugLabel(vk::CommandBuffer commandBuffer); + +} // namespace sophia diff --git a/engine/src/vulkan/device.cpp b/engine/src/vulkan/device.cpp index 742f15b..2e8fc9c 100644 --- a/engine/src/vulkan/device.cpp +++ b/engine/src/vulkan/device.cpp @@ -5,6 +5,8 @@ #include <set> #include <sophia/util/logger.hpp> +#include "vulkan/debug_label.hpp" + namespace sophia { static VKAPI_ATTR vk::Bool32 VKAPI_CALL @@ -288,6 +290,8 @@ void Device::setupDebugMessenger() { nullptr, &this->debugMessenger) != VK_SUCCESS) { throw std::runtime_error("failed to set up debug callback!"); } + + loadDebugLabelFunctions(*instance); } bool Device::checkValidationLayerSupport() { diff --git a/testbed/main.cpp b/testbed/main.cpp index dd39c5a..453c229 100644 --- a/testbed/main.cpp +++ b/testbed/main.cpp @@ -1,7 +1,9 @@ #include <imgui.h> #include <sophia/application.hpp> +#include <sophia/entity.hpp> #include <sophia/frame_info.hpp> +#include <sophia/scene_view.hpp> #include <sophia/ui/panel.hpp> class Viewport : public sophia::Panel { @@ -25,11 +27,15 @@ class Testbed : public sophia::Application { Testbed() : Application{{.width = 750, .height = 1000, .name = "Testbed"}} {} void onCreate() override { - sophia::RenderTargetHandle target = - resources().create<sophia::RenderTarget>(); - setSceneTarget(target); - panels().emplace<Viewport>(ui(), target); + view = views().create({.scene = scenes().create(), + .target = resources().create<sophia::RenderTarget>(), + .clearColor = {0.1f, 0.1f, 0.15f, 1.f}}); + + panels().emplace<Viewport>(ui(), views().get(view).target); } + + private: + sophia::SceneViewHandle view; }; SOPHIA_MAIN(Testbed) From 341249bb96c26b263a21f0bcbfadddba8845f750 Mon Sep 17 00:00:00 2001 From: Caleb Burke <calebburke91@gmail.com> Date: Fri, 25 Sep 2026 20:23:12 -0700 Subject: [PATCH 25/26] scene_view concept --- editor/main.cpp | 2 +- engine/include/sophia/application.hpp | 13 +-- engine/include/sophia/handles.hpp | 2 + engine/include/sophia/scene_view.hpp | 34 ++++++ engine/include/sophia/scene_view_registry.hpp | 86 ++++++++++++++ engine/src/application.cpp | 109 ++++++++---------- 6 files changed, 173 insertions(+), 73 deletions(-) create mode 100644 engine/include/sophia/scene_view.hpp create mode 100644 engine/include/sophia/scene_view_registry.hpp diff --git a/editor/main.cpp b/editor/main.cpp index b673768..7e0eb49 100644 --- a/editor/main.cpp +++ b/editor/main.cpp @@ -20,7 +20,7 @@ class Editor : public sophia::Application { panels().emplace<AssetBrowserPanel>(); sophia::RenderTargetHandle sceneTarget = resources().create<sophia::RenderTarget>(); - setSceneTarget(sceneTarget); + views().create({.scene = scenes().create(), .target = sceneTarget}); panels().emplace<ViewportPanel>(ui(), sceneTarget); panels().setDefaultLayout( diff --git a/engine/include/sophia/application.hpp b/engine/include/sophia/application.hpp index f2a2542..a1e5c6c 100644 --- a/engine/include/sophia/application.hpp +++ b/engine/include/sophia/application.hpp @@ -10,6 +10,7 @@ #include <sophia/math.hpp> #include <sophia/resource_registry_ref.hpp> #include <sophia/scene_registry.hpp> +#include <sophia/scene_view_registry.hpp> #include <sophia/types.hpp> #include <sophia/ui/debug_ui.hpp> #include <sophia/ui/panel_registry.hpp> @@ -46,6 +47,7 @@ class Application { ResourceRegistryRef resources(); PanelRegistry& panels() { return this->panelRegistry; } SceneRegistry& scenes() { return this->sceneRegistry; } + SceneViewRegistry& views() { return this->sceneViewRegistry; } UI& ui() { return this->_ui; } DebugUI& debugUI() { return this->_debugUI; } @@ -58,16 +60,6 @@ class Application { void drawMesh(MeshHandle handle, const glm::mat4& transform); - /** - * !TEMP !TODO remove this func eventually - * Chooses where the scene is rendered. An invalid handle (the default) means - * the swapchain; a target created with - * ResourceRegistryRef::create<RenderTarget>() redirects it offscreen, and the - * swapchain then only receives the UI. FrameInfo::currentFramebufferExtent - * and the camera projection follow the target's size. - */ - void setSceneTarget(RenderTargetHandle handle); - private: friend class Canvas; @@ -79,6 +71,7 @@ class Application { PanelRegistry panelRegistry; SceneRegistry sceneRegistry; + SceneViewRegistry sceneViewRegistry; UI _ui; DebugUI _debugUI; diff --git a/engine/include/sophia/handles.hpp b/engine/include/sophia/handles.hpp index 242535f..298b51b 100644 --- a/engine/include/sophia/handles.hpp +++ b/engine/include/sophia/handles.hpp @@ -37,10 +37,12 @@ class Mesh; class Panel; class RenderTarget; class Scene; +struct SceneView; using MeshHandle = Handle<Mesh>; using PanelHandle = Handle<Panel>; using RenderTargetHandle = Handle<RenderTarget>; using SceneHandle = Handle<Scene>; +using SceneViewHandle = Handle<SceneView>; } // namespace sophia diff --git a/engine/include/sophia/scene_view.hpp b/engine/include/sophia/scene_view.hpp new file mode 100644 index 0000000..7d83da9 --- /dev/null +++ b/engine/include/sophia/scene_view.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include <sophia/entity.hpp> +#include <sophia/handles.hpp> +#include <sophia/math.hpp> +#include <sophia/scene.hpp> + +namespace sophia { + +enum class ClearFlags : u8 { + None = 0, + Color = 1 << 0, + Depth = 1 << 1, + Stencil = 1 << 2, + All = Color | Depth | Stencil +}; + +struct SceneView { + SceneHandle scene = INVALID_HANDLE; + RenderTargetHandle target = INVALID_HANDLE; + Entity camera; + + vec4 viewport{0.0f, 0.0f, 1.0f, 1.0f}; + + vec4 clearColor{0.f, 0.f, 0.f, 1.f}; + ClearFlags clearFlags = ClearFlags::All; + + u32 priority = 0; // Lower priority renders first + u32 layerMask = 0xFFFFFFFF; + + bool enabled = true; // Disabled views are kept but not rendered +}; + +} // namespace sophia diff --git a/engine/include/sophia/scene_view_registry.hpp b/engine/include/sophia/scene_view_registry.hpp new file mode 100644 index 0000000..fc2ef54 --- /dev/null +++ b/engine/include/sophia/scene_view_registry.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include <algorithm> +#include <sophia/handles.hpp> +#include <sophia/scene_view.hpp> +#include <sophia/util/pool.hpp> +#include <utility> +#include <vector> + +namespace sophia { + +/** + * Every live SceneView. The application renders all enabled views each frame, + * in ascending priority order, before the UI. + */ +class SceneViewRegistry { + public: + SceneViewRegistry(const SceneViewRegistry&) = delete; + SceneViewRegistry& operator=(const SceneViewRegistry&) = delete; + + SceneViewRegistry() = default; + ~SceneViewRegistry() = default; + + SceneViewHandle create(const SceneView& view = {}) { + isRenderQueueDirty = true; + return pool.create(view); + } + + const SceneView& get(SceneViewHandle handle) const { + return pool.get(handle); + } + + SceneView& get(SceneViewHandle handle) { + isRenderQueueDirty = true; + return pool.get(handle); + } + + void release(SceneViewHandle handle) { + isRenderQueueDirty = true; + pool.release(handle); + } + + bool isAlive(SceneViewHandle handle) const { return pool.isAlive(handle); } + + u32 size() const { return pool.size(); } + + template <typename Fn> + void forEach(Fn&& fn) { + isRenderQueueDirty = true; + pool.forEach(std::forward<Fn>(fn)); + } + + template <typename Fn> + void forEach(Fn&& fn) const { + pool.forEach(std::forward<Fn>(fn)); + } + + const std::vector<SceneViewHandle>& GetRenderQueue() const { + if (isRenderQueueDirty) { + rebuildRenderQueue(); + } + return renderQueue; + } + + private: + void rebuildRenderQueue() const { + renderQueue.clear(); + pool.forEachWithHandle([&](SceneViewHandle handle, const SceneView& view) { + if (view.enabled) { + renderQueue.push_back(handle); + } + }); + + std::ranges::stable_sort(renderQueue, {}, [this](SceneViewHandle handle) { + return pool.get(handle).priority; + }); + isRenderQueueDirty = false; + } + + Pool<SceneView> pool{"SceneView"}; + + mutable std::vector<SceneViewHandle> renderQueue; + mutable bool isRenderQueueDirty = true; +}; + +} // namespace sophia diff --git a/engine/src/application.cpp b/engine/src/application.cpp index cf7f12f..8891820 100644 --- a/engine/src/application.cpp +++ b/engine/src/application.cpp @@ -1,9 +1,11 @@ #include <imgui.h> +#include <algorithm> #include <array> #include <chrono> #include <cstddef> #include <glm/glm.hpp> +#include <map> #include <sophia/application.hpp> #include <sophia/util/logger.hpp> #include <string> @@ -15,6 +17,7 @@ #include "rendering/model_render_system.hpp" #include "rendering/render_context.hpp" #include "rendering/render_graph.hpp" +#include "rendering/scene_renderer.hpp" #include "resources/mesh.hpp" #include "resources/render_target.hpp" #include "resources/resource_registry.hpp" @@ -43,35 +46,8 @@ std::vector<std::string> commandLineArgs; constexpr ClearColor CLEAR_COLOR = {0.01f, 0.01f, 0.02f, 1.0f}; -void beginColorPass(vk::CommandBuffer commandBuffer, vk::ImageView view, - vk::Extent2D extent) { - vk::ClearValue clearValue{}; - clearValue.color = vk::ClearColorValue{CLEAR_COLOR}; - - vk::RenderingAttachmentInfo colorAttachment{}; - colorAttachment.imageView = view; - colorAttachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; - colorAttachment.loadOp = vk::AttachmentLoadOp::eClear; - colorAttachment.storeOp = vk::AttachmentStoreOp::eStore; - colorAttachment.clearValue = clearValue; - - vk::RenderingInfo renderingInfo{}; - renderingInfo.renderArea = vk::Rect2D{{0, 0}, extent}; - renderingInfo.layerCount = 1; - renderingInfo.colorAttachmentCount = 1; - renderingInfo.pColorAttachments = &colorAttachment; - - commandBuffer.beginRendering(renderingInfo); - - vk::Viewport viewport{0.0f, - 0.0f, - static_cast<float>(extent.width), - static_cast<float>(extent.height), - 0.0f, - 1.0f}; - vk::Rect2D scissor{{0, 0}, extent}; - commandBuffer.setViewport(0, viewport); - commandBuffer.setScissor(0, scissor); +bool hasFlag(ClearFlags flags, ClearFlags flag) { + return (static_cast<u8>(flags) & static_cast<u8>(flag)) != 0; } } // namespace @@ -88,6 +64,8 @@ class Application::Impl { void run() { ModelRenderSystem modelRenderSystem{device, renderContext}; + SceneRenderer sceneRenderer{this->application.sceneRegistry, + this->resourceRegistry}; ImGuiRenderer imguiRenderer{window, device, renderContext.getImageFormat(), renderContext.getImageCount(), this->resourceRegistry}; @@ -106,6 +84,7 @@ class Application::Impl { auto& renderTargetPool = this->resourceRegistry.renderTargetPool; PanelRegistry& panelRegistry = this->application.panelRegistry; + const SceneViewRegistry& sceneViews = this->application.sceneViewRegistry; imguiRenderer.setDefaultLayout(panelRegistry.getDefaultLayout()); @@ -128,17 +107,10 @@ class Application::Impl { // frame's work (or ImGui's references to the targets) is recorded. renderTargetPool.applyPendingResizes(); - RenderTarget* sceneTarget = - this->sceneTargetHandle.isValid() - ? &renderTargetPool.get(this->sceneTargetHandle) - : nullptr; - vk::Extent2D extent = renderContext.getExtent(); - vk::Extent2D sceneExtent = - sceneTarget ? sceneTarget->getExtent() : extent; FrameInfo frameInfo{frameIndex, elapsedTime, deltaTime, - glm::vec2(static_cast<float>(sceneExtent.width), - static_cast<float>(sceneExtent.height)), + glm::vec2(static_cast<float>(extent.width), + static_cast<float>(extent.height)), this->application.camera}; this->application.onUpdate(frameInfo); this->application.camera.setProjection( @@ -166,31 +138,54 @@ class Application::Impl { this->drawCalls.end()); this->drawCalls.clear(); - if (sceneTarget) { - Image& targetImage = sceneTarget->getImage(); - targetImage.transitionLayout(commandBuffer, - vk::ImageLayout::eColorAttachmentOptimal); - beginColorPass(commandBuffer, targetImage.getView(), sceneExtent); - modelRenderSystem.render(commandBuffer, instances, frameInfo); - commandBuffer.endRendering(); - targetImage.transitionLayout(commandBuffer, - vk::ImageLayout::eShaderReadOnlyOptimal); - } - RenderGraph graph; ImageRef backbuffer = graph.importImage(renderContext.getCurrentImage(), Access::Present); + std::map<RenderTargetHandle, ImageRef> viewTargets; + for (SceneViewHandle handle : sceneViews.GetRenderQueue()) { + const SceneView& view = sceneViews.get(handle); + if (!this->application.sceneRegistry.isAlive(view.scene) || + !renderTargetPool.isAlive(view.target)) { + continue; + } + + auto [it, inserted] = viewTargets.try_emplace(view.target); + if (inserted) { + it->second = + graph.importImage(renderTargetPool.get(view.target).getImage(), + Access::SampledRead); + } + + const vec4& color = view.clearColor; + graph.addPass( + "scene view", + [&](RenderGraph::PassBuilder& pass) { + pass.writeColor(it->second, + hasFlag(view.clearFlags, ClearFlags::Color) + ? vk::AttachmentLoadOp::eClear + : vk::AttachmentLoadOp::eLoad, + {color.r, color.g, color.b, color.a}); + }, + [&sceneRenderer, &sceneViews, handle](vk::CommandBuffer) { + const SceneView& view = sceneViews.get(handle); + if (view.camera) { + sceneRenderer.render(view); + } + }); + } + graph.addPass( "main", [&](RenderGraph::PassBuilder& pass) { pass.writeColor(backbuffer, vk::AttachmentLoadOp::eClear, CLEAR_COLOR); + for (const auto& [handle, target] : viewTargets) { + pass.sample(target); + } }, [&](vk::CommandBuffer commandBuffer) { - if (!sceneTarget) { - modelRenderSystem.render(commandBuffer, instances, frameInfo); - } + modelRenderSystem.render(commandBuffer, instances, frameInfo); imguiRenderer.endFrame(commandBuffer); }); @@ -237,10 +232,6 @@ class Application::Impl { this->drawCalls.push_back({&mesh, transform}); } - void setSceneTarget(RenderTargetHandle handle) { - this->sceneTargetHandle = handle; - } - ResourceRegistry& getResourceRegistry() { return resourceRegistry; } private: @@ -253,8 +244,6 @@ class Application::Impl { std::vector<std::unique_ptr<Mesh>> meshes; std::vector<ModelRenderSystem::MeshInstance> drawCalls; - - RenderTargetHandle sceneTargetHandle = RenderTargetHandle::invalid(); }; void Application::setCommandLineArgs(int argc, const char* const* argv) { @@ -281,10 +270,6 @@ void Application::pushMesh(std::vector<CanvasVertex> vertices, impl->pushMesh(std::move(vertices), std::move(indices)); } -void Application::setSceneTarget(RenderTargetHandle handle) { - impl->setSceneTarget(handle); -} - void Application::clearMeshes() { impl->clearMeshes(); } void Application::drawMesh(MeshHandle handle, const glm::mat4& transform) { From 6e12d8a710c204458157cbd890194cef534fc63b Mon Sep 17 00:00:00 2001 From: Caleb Burke <calebburke91@gmail.com> Date: Fri, 25 Sep 2026 20:39:04 -0700 Subject: [PATCH 26/26] changes --- engine/src/application.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/engine/src/application.cpp b/engine/src/application.cpp index 8891820..84ae53e 100644 --- a/engine/src/application.cpp +++ b/engine/src/application.cpp @@ -84,7 +84,10 @@ class Application::Impl { auto& renderTargetPool = this->resourceRegistry.renderTargetPool; PanelRegistry& panelRegistry = this->application.panelRegistry; - const SceneViewRegistry& sceneViews = this->application.sceneViewRegistry; + SceneRegistry& sceneRegistry = this->application.sceneRegistry; + SceneViewRegistry& sceneViewRegistry = this->application.sceneViewRegistry; + const SceneViewRegistry& sceneViewRegistryConst = + this->application.sceneViewRegistry; imguiRenderer.setDefaultLayout(panelRegistry.getDefaultLayout()); @@ -143,9 +146,9 @@ class Application::Impl { graph.importImage(renderContext.getCurrentImage(), Access::Present); std::map<RenderTargetHandle, ImageRef> viewTargets; - for (SceneViewHandle handle : sceneViews.GetRenderQueue()) { - const SceneView& view = sceneViews.get(handle); - if (!this->application.sceneRegistry.isAlive(view.scene) || + for (SceneViewHandle handle : sceneViewRegistryConst.GetRenderQueue()) { + const SceneView& view = sceneViewRegistryConst.get(handle); + if (!sceneRegistry.isAlive(view.scene) || !renderTargetPool.isAlive(view.target)) { continue; } @@ -167,8 +170,8 @@ class Application::Impl { : vk::AttachmentLoadOp::eLoad, {color.r, color.g, color.b, color.a}); }, - [&sceneRenderer, &sceneViews, handle](vk::CommandBuffer) { - const SceneView& view = sceneViews.get(handle); + [&sceneRenderer, &sceneViewRegistry, handle](vk::CommandBuffer) { + const SceneView& view = sceneViewRegistry.get(handle); if (view.camera) { sceneRenderer.render(view); }