Add offscreen render targets and rename AssetRegistry to ResourceRegistry #8

Merged
burkkyy merged 26 commits from dev into main 2026-09-25 20:39:26 -07:00
26 changed files with 743 additions and 189 deletions
Showing only changes of commit 5e1caabde6 - Show all commits
+8 -8
View File
@@ -1,15 +1,15 @@
# Glossary # 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. lightweight Handles instead of pointers or ownership.
Internally it's a collection of per-type pools, each responsible for one Internally it's a collection of per-type pools, each responsible for one
asset type: resource type:
``` ```
AssetRegistry ResourceRegistry
├── MeshPool → MeshHandle ├── MeshPool → MeshHandle
├── TexturePool → TextureHandle (planned) ├── TexturePool → TextureHandle (planned)
└── MaterialPool → MaterialHandle (planned) └── MaterialPool → MaterialHandle (planned)
@@ -18,19 +18,19 @@ AssetRegistry
A Handle is a small (index, generation) pair. The index locates the slot in 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 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 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: Typical lifecycle:
``` ```
load<Mesh>(path) / loadMesh(...) → MeshHandle load<Mesh>(path) / create<Mesh>(...) → MeshHandle
│ │
get<Mesh>(handle) → const Mesh& get<Mesh>(handle) → const Mesh&
│ │
release<Mesh>(handle) release<Mesh>(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?" lifetime directly?"
## Buffer ## 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 A PanelRegistry owns Panels on the caller's behalf and hands out lightweight
PanelHandles instead of pointers or ownership. 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: per-type pools use:
``` ```
+4 -1
View File
@@ -18,7 +18,10 @@ class Editor : public sophia::Application {
panels().emplace<SceneHierarchyPanel>(); panels().emplace<SceneHierarchyPanel>();
panels().emplace<InspectorPanel>(); panels().emplace<InspectorPanel>();
panels().emplace<AssetBrowserPanel>(); panels().emplace<AssetBrowserPanel>();
panels().emplace<ViewportPanel>(); sophia::RenderTargetHandle sceneTarget =
resources().create<sophia::RenderTarget>();
setSceneTarget(sceneTarget);
panels().emplace<ViewportPanel>(ui(), sceneTarget);
panels().setDefaultLayout( panels().setDefaultLayout(
{.left = {"Scene Hierarchy"}, {.left = {"Scene Hierarchy"},
+2 -1
View File
@@ -24,8 +24,9 @@ void ViewportPanel::onUpdate(const sophia::FrameInfo&) {
const ImVec2 size = ImGui::GetContentRegionAvail(); const ImVec2 size = ImGui::GetContentRegionAvail();
const ImVec2 end(origin.x + size.x, origin.y + size.y); const ImVec2 end(origin.x + size.x, origin.y + size.y);
ui.image(target);
ImDrawList* drawList = ImGui::GetWindowDrawList(); ImDrawList* drawList = ImGui::GetWindowDrawList();
drawList->AddRectFilled(origin, end, IM_COL32(24, 26, 32, 255));
if (showGrid) { if (showGrid) {
constexpr float GRID_STEP = 40.0f; constexpr float GRID_STEP = 40.0f;
+11 -1
View File
@@ -1,15 +1,25 @@
#pragma once #pragma once
#include <sophia/frame_info.hpp> #include <sophia/frame_info.hpp>
#include <sophia/handles.hpp>
#include <sophia/ui/panel.hpp> #include <sophia/ui/panel.hpp>
#include <sophia/ui/ui.hpp>
class ViewportPanel : public sophia::Panel { class ViewportPanel : public sophia::Panel {
public: 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; void onUpdate(const sophia::FrameInfo& frameInfo) override;
private: private:
sophia::UI& ui;
sophia::RenderTargetHandle target;
enum class GizmoMode { Move, Rotate, Scale }; enum class GizmoMode { Move, Rotate, Scale };
GizmoMode gizmoMode = GizmoMode::Move; GizmoMode gizmoMode = GizmoMode::Move;
+15 -3
View File
@@ -3,14 +3,15 @@
#include <cstdlib> #include <cstdlib>
#include <iostream> #include <iostream>
#include <memory> #include <memory>
#include <sophia/asset_registry.hpp>
#include <sophia/camera_2d.hpp> #include <sophia/camera_2d.hpp>
#include <sophia/canvas.hpp> #include <sophia/canvas.hpp>
#include <sophia/console/console_command_registry.hpp> #include <sophia/console/console_command_registry.hpp>
#include <sophia/frame_info.hpp> #include <sophia/frame_info.hpp>
#include <sophia/resource_registry.hpp>
#include <sophia/types.hpp> #include <sophia/types.hpp>
#include <sophia/ui/debug_ui.hpp> #include <sophia/ui/debug_ui.hpp>
#include <sophia/ui/panel_registry.hpp> #include <sophia/ui/panel_registry.hpp>
#include <sophia/ui/ui.hpp>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -44,8 +45,9 @@ class Application {
virtual void onRender() {} virtual void onRender() {}
virtual void onDestroy() {} virtual void onDestroy() {}
AssetRegistry& assets() { return this->assetRegistry; } ResourceRegistry& resources() { return this->resourceRegistry; }
PanelRegistry& panels() { return this->panelRegistry; } PanelRegistry& panels() { return this->panelRegistry; }
UI& ui() { return this->_ui; }
DebugUI& debugUI() { return this->_debugUI; } DebugUI& debugUI() { return this->_debugUI; }
@@ -57,6 +59,15 @@ class Application {
void drawMesh(MeshHandle handle, const glm::mat4& transform); 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<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: private:
friend class Canvas; friend class Canvas;
@@ -66,8 +77,9 @@ class Application {
class Impl; class Impl;
std::unique_ptr<Impl> impl; std::unique_ptr<Impl> impl;
AssetRegistry assetRegistry; ResourceRegistry resourceRegistry;
PanelRegistry panelRegistry; PanelRegistry panelRegistry;
UI _ui;
DebugUI _debugUI; DebugUI _debugUI;
}; };
-64
View File
@@ -1,64 +0,0 @@
#pragma once
#include <memory>
#include <sophia/handles.hpp>
#include <string>
#include <vector>
#include "sophia/canvas.hpp"
#include "sophia/types.hpp"
namespace sophia {
class Device;
class Mesh;
template <typename T>
struct AssetTraits;
template <>
struct AssetTraits<Mesh> {
using Handle = MeshHandle;
};
class AssetRegistry {
public:
~AssetRegistry();
AssetRegistry(const AssetRegistry&) = delete;
AssetRegistry& operator=(const AssetRegistry&) = delete;
template <typename T>
typename AssetTraits<T>::Handle load(const std::string& path);
/**
* TEMPORARY: Remove once load<Mesh> can read real model files from disk.
*/
MeshHandle loadMesh(std::vector<CanvasVertex> vertices,
std::vector<u32> indices);
template <typename T>
void release(typename AssetTraits<T>::Handle handle);
private:
friend class Application;
explicit AssetRegistry(Device& device);
template <typename T>
const T& get(typename AssetTraits<T>::Handle handle) const;
class Impl;
std::unique_ptr<Impl> impl;
};
template <>
MeshHandle AssetRegistry::load<Mesh>(const std::string& path);
template <>
void AssetRegistry::release<Mesh>(MeshHandle handle);
template <>
const Mesh& AssetRegistry::get<Mesh>(MeshHandle handle) const;
} // namespace sophia
+1 -1
View File
@@ -1,6 +1,6 @@
#pragma once #pragma once
#include <sophia/asset_registry.hpp> #include <sophia/resource_registry.hpp>
#include <string> #include <string>
#define GLM_FORCE_RADIANS #define GLM_FORCE_RADIANS
+2
View File
@@ -26,8 +26,10 @@ struct Handle {
struct MeshTag {}; struct MeshTag {};
struct PanelTag {}; struct PanelTag {};
struct RenderTargetTag {};
using MeshHandle = Handle<MeshTag>; using MeshHandle = Handle<MeshTag>;
using PanelHandle = Handle<PanelTag>; using PanelHandle = Handle<PanelTag>;
using RenderTargetHandle = Handle<RenderTargetTag>;
} // namespace sophia } // namespace sophia
+103
View File
@@ -0,0 +1,103 @@
#pragma once
#include <memory>
#include <sophia/handles.hpp>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#include "sophia/canvas.hpp"
#include "sophia/types.hpp"
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
namespace sophia {
class Device;
class Mesh;
class RenderTarget;
class ImGuiRenderer;
template <typename T>
struct ResourceTraits;
template <>
struct ResourceTraits<Mesh> {
using Handle = MeshHandle;
};
template <>
struct ResourceTraits<RenderTarget> {
using Handle = RenderTargetHandle;
};
class ResourceRegistry {
public:
~ResourceRegistry();
ResourceRegistry(const ResourceRegistry&) = delete;
ResourceRegistry& operator=(const ResourceRegistry&) = delete;
template <typename T>
typename ResourceTraits<T>::Handle load(const std::string& path);
template <typename T, typename... Args>
typename ResourceTraits<T>::Handle create(Args&&... args) {
return createResource(std::type_identity<T>{}, std::forward<Args>(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 <typename T>
void release(typename ResourceTraits<T>::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<Mesh>,
std::vector<CanvasVertex> vertices,
std::vector<u32> indices = {});
RenderTargetHandle createResource(std::type_identity<RenderTarget>,
u32 width = 1, u32 height = 1);
RenderTarget& getRenderTarget(RenderTargetHandle handle);
const RenderTarget& getRenderTarget(RenderTargetHandle handle) const;
void applyPendingResizes();
template <typename T>
const T& get(typename ResourceTraits<T>::Handle handle) const;
class Impl;
std::unique_ptr<Impl> impl;
};
template <>
MeshHandle ResourceRegistry::load<Mesh>(const std::string& path);
template <>
void ResourceRegistry::release<Mesh>(MeshHandle handle);
template <>
void ResourceRegistry::release<RenderTarget>(RenderTargetHandle handle);
template <>
const Mesh& ResourceRegistry::get<Mesh>(MeshHandle handle) const;
} // namespace sophia
+40
View File
@@ -0,0 +1,40 @@
#pragma once
#include <imgui.h>
#include <sophia/handles.hpp>
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
+89 -39
View File
@@ -20,6 +20,7 @@
#include "mesh.hpp" #include "mesh.hpp"
#include "pipelines/graphics_pipeline.hpp" #include "pipelines/graphics_pipeline.hpp"
#include "render_context.hpp" #include "render_context.hpp"
#include "render_target.hpp"
#include "shader.hpp" #include "shader.hpp"
#include "systems/model_render_system.hpp" #include "systems/model_render_system.hpp"
#include "window.hpp" #include "window.hpp"
@@ -41,6 +42,38 @@ struct PushConstants {
std::vector<std::string> commandLineArgs; std::vector<std::string> commandLineArgs;
void beginColorPass(vk::CommandBuffer commandBuffer, vk::ImageView view,
vk::Extent2D extent) {
vk::ClearValue clearValue{};
clearValue.color =
vk::ClearColorValue{std::array<float, 4>{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<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);
}
} // namespace } // namespace
class Application::Impl { class Application::Impl {
@@ -55,7 +88,9 @@ class Application::Impl {
void run() { void run() {
ModelRenderSystem modelRenderSystem{device, renderContext}; ModelRenderSystem modelRenderSystem{device, renderContext};
ImGuiRenderer imguiRenderer{window, device, renderContext.getImageFormat(), ImGuiRenderer imguiRenderer{window, device, renderContext.getImageFormat(),
renderContext.getImageCount()}; renderContext.getImageCount(),
this->application.resourceRegistry};
this->application._ui.renderer = &imguiRenderer;
this->application.consoleCommandRegistry.registerCommand( this->application.consoleCommandRegistry.registerCommand(
"reset_layout", "Restore the default panel layout", "reset_layout", "Restore the default panel layout",
@@ -83,10 +118,22 @@ class Application::Impl {
double elapsedTime = double elapsedTime =
std::chrono::duration<double>(currentTime - startTime).count(); std::chrono::duration<double>(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 extent = renderContext.getExtent();
vk::Extent2D sceneExtent =
sceneTarget ? sceneTarget->getExtent() : extent;
FrameInfo frameInfo{frameIndex, elapsedTime, deltaTime, FrameInfo frameInfo{frameIndex, elapsedTime, deltaTime,
glm::vec2(static_cast<float>(extent.width), glm::vec2(static_cast<float>(sceneExtent.width),
static_cast<float>(extent.height)), static_cast<float>(sceneExtent.height)),
this->application.camera}; this->application.camera};
this->application.onUpdate(frameInfo); this->application.onUpdate(frameInfo);
this->application.camera.setProjection( this->application.camera.setProjection(
@@ -105,38 +152,6 @@ class Application::Impl {
this->application.panelRegistry.update(frameInfo); this->application.panelRegistry.update(frameInfo);
this->application.onRender(); this->application.onRender();
renderContext.transitionCurrentImage(
commandBuffer, vk::ImageLayout::eColorAttachmentOptimal);
vk::ClearValue clearValue{};
clearValue.color =
vk::ClearColorValue{std::array<float, 4>{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<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);
std::vector<ModelRenderSystem::MeshInstance> instances; std::vector<ModelRenderSystem::MeshInstance> instances;
instances.reserve(this->meshes.size() + this->drawCalls.size()); instances.reserve(this->meshes.size() + this->drawCalls.size());
for (const std::unique_ptr<Mesh>& mesh : this->meshes) { for (const std::unique_ptr<Mesh>& mesh : this->meshes) {
@@ -144,10 +159,28 @@ class Application::Impl {
} }
instances.insert(instances.end(), this->drawCalls.begin(), instances.insert(instances.end(), this->drawCalls.begin(),
this->drawCalls.end()); this->drawCalls.end());
modelRenderSystem.render(commandBuffer, instances, frameInfo);
this->drawCalls.clear(); 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); imguiRenderer.endFrame(commandBuffer);
commandBuffer.endRendering(); commandBuffer.endRendering();
@@ -164,6 +197,8 @@ class Application::Impl {
device.waitIdle(); device.waitIdle();
this->application._ui.renderer = nullptr;
auto endTime = std::chrono::high_resolution_clock::now(); auto endTime = std::chrono::high_resolution_clock::now();
double totalRuntime = double totalRuntime =
std::chrono::duration<double>(endTime - startTime).count(); std::chrono::duration<double>(endTime - startTime).count();
@@ -193,6 +228,14 @@ class Application::Impl {
this->drawCalls.push_back({&mesh, transform}); this->drawCalls.push_back({&mesh, transform});
} }
void setSceneTarget(RenderTargetHandle handle) {
this->sceneTargetHandle = handle;
}
u32 getColorFormat() const {
return static_cast<u32>(this->renderContext.getImageFormat());
}
Device& getDevice() { return device; } Device& getDevice() { return device; }
private: private:
@@ -204,6 +247,8 @@ class Application::Impl {
std::vector<std::unique_ptr<Mesh>> meshes; std::vector<std::unique_ptr<Mesh>> meshes;
std::vector<ModelRenderSystem::MeshInstance> drawCalls; std::vector<ModelRenderSystem::MeshInstance> drawCalls;
RenderTargetHandle sceneTargetHandle = RenderTargetHandle::invalid();
}; };
void Application::setCommandLineArgs(int argc, const char* const* argv) { void Application::setCommandLineArgs(int argc, const char* const* argv) {
@@ -214,7 +259,8 @@ Application::Application(const Config& config)
: impl{std::make_unique<Impl>(*this, config)}, : impl{std::make_unique<Impl>(*this, config)},
canvas{this}, canvas{this},
args{std::move(commandLineArgs)}, args{std::move(commandLineArgs)},
assetRegistry{impl->getDevice()}, resourceRegistry{impl->getDevice(), impl->getColorFormat()},
_ui{resourceRegistry},
_debugUI{this->panelRegistry, this->consoleCommandRegistry} {} _debugUI{this->panelRegistry, this->consoleCommandRegistry} {}
Application::~Application() = default; Application::~Application() = default;
@@ -226,10 +272,14 @@ void Application::pushMesh(std::vector<CanvasVertex> vertices,
impl->pushMesh(std::move(vertices), std::move(indices)); impl->pushMesh(std::move(vertices), std::move(indices));
} }
void Application::setSceneTarget(RenderTargetHandle handle) {
impl->setSceneTarget(handle);
}
void Application::clearMeshes() { impl->clearMeshes(); } void Application::clearMeshes() { impl->clearMeshes(); }
void Application::drawMesh(MeshHandle handle, const glm::mat4& transform) { void Application::drawMesh(MeshHandle handle, const glm::mat4& transform) {
impl->drawMesh(assetRegistry.get<Mesh>(handle), transform); impl->drawMesh(resourceRegistry.get<Mesh>(handle), transform);
} }
} // namespace sophia } // namespace sophia
-60
View File
@@ -1,60 +0,0 @@
#include <sophia/asset_registry.hpp>
#include <sophia/util/assert.hpp>
#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<Impl>(device)} {}
AssetRegistry::~AssetRegistry() = default;
template <>
MeshHandle AssetRegistry::load<Mesh>(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 T>
typename AssetTraits<T>::Handle load(const std::string&) {
static_assert(sizeof(T) == 0,
"load<T> is not implemented for this asset type");
}
template <>
void AssetRegistry::release<Mesh>(MeshHandle handle) {
impl->meshPool.release(handle);
}
template <>
const Mesh& AssetRegistry::get<Mesh>(MeshHandle handle) const {
return impl->meshPool.get(handle);
}
MeshHandle AssetRegistry::loadMesh(std::vector<CanvasVertex> vertices,
std::vector<u32> 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
+12
View File
@@ -63,6 +63,18 @@ void Image::transitionLayout(vk::CommandBuffer commandBuffer,
barrier.dstAccessMask = {}; barrier.dstAccessMask = {};
srcStage = vk::PipelineStageFlagBits::eColorAttachmentOutput; srcStage = vk::PipelineStageFlagBits::eColorAttachmentOutput;
dstStage = vk::PipelineStageFlagBits::eBottomOfPipe; 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 && } else if (this->layout == vk::ImageLayout::ePresentSrcKHR &&
newLayout == vk::ImageLayout::eColorAttachmentOptimal) { newLayout == vk::ImageLayout::eColorAttachmentOptimal) {
barrier.srcAccessMask = {}; barrier.srcAccessMask = {};
+50 -1
View File
@@ -5,6 +5,10 @@
#include <imgui_impl_vulkan.h> #include <imgui_impl_vulkan.h>
#include <imgui_internal.h> #include <imgui_internal.h>
#include <sophia/resource_registry.hpp>
#include "render_target.hpp"
namespace sophia { namespace sophia {
namespace { namespace {
@@ -20,7 +24,8 @@ void dockAll(const std::vector<std::string>& windows, ImGuiID nodeId) {
ImGuiRenderer::ImGuiRenderer(Window& window, Device& device, ImGuiRenderer::ImGuiRenderer(Window& window, Device& device,
vk::Format colorAttachmentFormat, vk::Format colorAttachmentFormat,
u32 minImageCount) { u32 minImageCount, ResourceRegistry& resources)
: resources{resources} {
// 1. Construct ImGui context // 1. Construct ImGui context
IMGUI_CHECKVERSION(); IMGUI_CHECKVERSION();
ImGui::CreateContext(); ImGui::CreateContext();
@@ -67,12 +72,17 @@ ImGuiRenderer::ImGuiRenderer(Window& window, Device& device,
} }
ImGuiRenderer::~ImGuiRenderer() { ImGuiRenderer::~ImGuiRenderer() {
for (const auto& [index, texture] : textures) {
ImGui_ImplVulkan_RemoveTexture(texture.descriptorSet);
}
ImGui_ImplVulkan_Shutdown(); ImGui_ImplVulkan_Shutdown();
ImGui_ImplGlfw_Shutdown(); ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext(); ImGui::DestroyContext();
} }
void ImGuiRenderer::beginFrame() { void ImGuiRenderer::beginFrame() {
releaseStaleTextures();
ImGui_ImplVulkan_NewFrame(); ImGui_ImplVulkan_NewFrame();
ImGui_ImplGlfw_NewFrame(); ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame(); ImGui::NewFrame();
@@ -82,6 +92,45 @@ void ImGuiRenderer::beginFrame() {
applyLayoutIfNeeded(dockspaceId, DOCKSPACE_FLAGS); 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<VkImageView>(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) { void ImGuiRenderer::setDefaultLayout(DockLayout layout) {
defaultLayout = std::move(layout); defaultLayout = std::move(layout);
seedIfUnsaved = true; seedIfUnsaved = true;
+20 -1
View File
@@ -2,31 +2,50 @@
#include <imgui.h> #include <imgui.h>
#include <sophia/handles.hpp>
#include <sophia/types.hpp> #include <sophia/types.hpp>
#include <sophia/ui/dock_layout.hpp> #include <sophia/ui/dock_layout.hpp>
#include <unordered_map>
#include "device.hpp" #include "device.hpp"
#include "window.hpp" #include "window.hpp"
namespace sophia { namespace sophia {
class ResourceRegistry;
class ImGuiRenderer { class ImGuiRenderer {
public: public:
ImGuiRenderer(const ImGuiRenderer&) = delete; ImGuiRenderer(const ImGuiRenderer&) = delete;
ImGuiRenderer& operator=(const ImGuiRenderer&) = delete; ImGuiRenderer& operator=(const ImGuiRenderer&) = delete;
ImGuiRenderer(Window& window, Device& device, ImGuiRenderer(Window& window, Device& device,
vk::Format colorAttachmentFormat, u32 minImageCount); vk::Format colorAttachmentFormat, u32 minImageCount,
ResourceRegistry& resources);
~ImGuiRenderer(); ~ImGuiRenderer();
void beginFrame(); void beginFrame();
void endFrame(vk::CommandBuffer commandBuffer); void endFrame(vk::CommandBuffer commandBuffer);
ImTextureID getTextureId(RenderTargetHandle handle);
void setDefaultLayout(DockLayout layout); void setDefaultLayout(DockLayout layout);
void resetLayout(); void resetLayout();
private: private:
void applyLayoutIfNeeded(ImGuiID dockspaceId, ImGuiDockNodeFlags flags); 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<u32, Texture> textures;
DockLayout defaultLayout; DockLayout defaultLayout;
bool seedIfUnsaved = false; bool seedIfUnsaved = false;
+1 -1
View File
@@ -1,7 +1,7 @@
#pragma once #pragma once
#include <memory> #include <memory>
#include <sophia/asset_registry.hpp> #include <sophia/resource_registry.hpp>
#include <sophia/types.hpp> #include <sophia/types.hpp>
#include <vector> #include <vector>
+52
View File
@@ -0,0 +1,52 @@
#include "render_target.hpp"
#include <algorithm>
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
+51
View File
@@ -0,0 +1,51 @@
#pragma once
#include <memory>
#include <optional>
#include <sophia/types.hpp>
#include <vulkan/vulkan.hpp>
#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> image;
std::optional<vk::Extent2D> pendingExtent;
};
} // namespace sophia
@@ -1,4 +1,4 @@
#include "assets/pools/mesh_pool.hpp" #include "resources/pools/mesh_pool.hpp"
#include <sophia/util/assert.hpp> #include <sophia/util/assert.hpp>
@@ -0,0 +1,81 @@
#include "resources/pools/render_target_pool.hpp"
#include <sophia/util/assert.hpp>
#include <utility>
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<RenderTarget>(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<u32>(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<RenderTarget&>(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
@@ -0,0 +1,44 @@
#pragma once
#include <memory>
#include <sophia/handles.hpp>
#include <vector>
#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<RenderTarget> target;
u32 generation = 0;
};
const Slot& validate(RenderTargetHandle handle) const;
Device& device;
vk::Format format;
std::vector<Slot> slots;
std::vector<u32> freeList;
};
} // namespace sophia
+104
View File
@@ -0,0 +1,104 @@
#include <sophia/resource_registry.hpp>
#include <sophia/util/assert.hpp>
#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<Impl>(
device, static_cast<vk::Format>(renderTargetFormat))} {}
ResourceRegistry::~ResourceRegistry() = default;
template <>
MeshHandle ResourceRegistry::load<Mesh>(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 T>
typename ResourceTraits<T>::Handle load(const std::string&) {
static_assert(sizeof(T) == 0,
"load<T> is not implemented for this resource type");
}
template <>
void ResourceRegistry::release<Mesh>(MeshHandle handle) {
impl->meshPool.release(handle);
}
template <>
void ResourceRegistry::release<RenderTarget>(RenderTargetHandle handle) {
impl->renderTargetPool.release(handle);
}
template <>
const Mesh& ResourceRegistry::get<Mesh>(MeshHandle handle) const {
return impl->meshPool.get(handle);
}
MeshHandle ResourceRegistry::createResource(std::type_identity<Mesh>,
std::vector<CanvasVertex> vertices,
std::vector<u32> 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<RenderTarget>, 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<float>(extent.width), static_cast<float>(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
+27
View File
@@ -0,0 +1,27 @@
#include <sophia/resource_registry.hpp>
#include <sophia/ui/ui.hpp>
#include <sophia/util/assert.hpp>
#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<u32>(size.x),
static_cast<u32>(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
+1 -1
View File
@@ -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}},
{{-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<sophia::Mesh>(std::move(vertices));
} }
void onUpdate(const sophia::FrameInfo& frameInfo) override { void onUpdate(const sophia::FrameInfo& frameInfo) override {
+24 -6
View File
@@ -1,17 +1,35 @@
#include <iostream> #include <imgui.h>
#include <sophia/application.hpp> #include <sophia/application.hpp>
#include <sophia/frame_info.hpp>
#include <sophia/ui/panel.hpp>
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 { class Testbed : public sophia::Application {
public: public:
Testbed() : Application{{.width = 750, .height = 1000, .name = "Testbed"}} {} Testbed() : Application{{.width = 750, .height = 1000, .name = "Testbed"}} {}
void onCreate() override { void onCreate() override {
std::cout << "Testbed created" << std::endl; sophia::RenderTargetHandle target =
resources().create<sophia::RenderTarget>();
debugUI().show(); setSceneTarget(target);
panels().emplace<Viewport>(ui(), target);
} }
void onUpdate(const sophia::FrameInfo& frameInfo) override {}
}; };
SOPHIA_MAIN(Testbed) SOPHIA_MAIN(Testbed)