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)