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<T>(...), and add create<RenderTarget>(),
  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.
This commit is contained in:
2026-09-20 16:21:22 -07:00
parent 1485ce01f3
commit 5e1caabde6
26 changed files with 743 additions and 189 deletions
+15 -3
View File
@@ -3,14 +3,15 @@
#include <cstdlib>
#include <iostream>
#include <memory>
#include <sophia/asset_registry.hpp>
#include <sophia/camera_2d.hpp>
#include <sophia/canvas.hpp>
#include <sophia/console/console_command_registry.hpp>
#include <sophia/frame_info.hpp>
#include <sophia/resource_registry.hpp>
#include <sophia/types.hpp>
#include <sophia/ui/debug_ui.hpp>
#include <sophia/ui/panel_registry.hpp>
#include <sophia/ui/ui.hpp>
#include <string>
#include <vector>
@@ -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<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;
@@ -66,8 +77,9 @@ class Application {
class Impl;
std::unique_ptr<Impl> impl;
AssetRegistry assetRegistry;
ResourceRegistry resourceRegistry;
PanelRegistry panelRegistry;
UI _ui;
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
#include <sophia/asset_registry.hpp>
#include <sophia/resource_registry.hpp>
#include <string>
#define GLM_FORCE_RADIANS
+2
View File
@@ -26,8 +26,10 @@ struct Handle {
struct MeshTag {};
struct PanelTag {};
struct RenderTargetTag {};
using MeshHandle = Handle<MeshTag>;
using PanelHandle = Handle<PanelTag>;
using RenderTargetHandle = Handle<RenderTargetTag>;
} // 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 "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<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
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<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 sceneExtent =
sceneTarget ? sceneTarget->getExtent() : extent;
FrameInfo frameInfo{frameIndex, elapsedTime, deltaTime,
glm::vec2(static_cast<float>(extent.width),
static_cast<float>(extent.height)),
glm::vec2(static_cast<float>(sceneExtent.width),
static_cast<float>(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<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;
instances.reserve(this->meshes.size() + this->drawCalls.size());
for (const std::unique_ptr<Mesh>& 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<double>(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<u32>(this->renderContext.getImageFormat());
}
Device& getDevice() { return device; }
private:
@@ -204,6 +247,8 @@ 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) {
@@ -214,7 +259,8 @@ Application::Application(const Config& config)
: impl{std::make_unique<Impl>(*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<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) {
impl->drawMesh(assetRegistry.get<Mesh>(handle), transform);
impl->drawMesh(resourceRegistry.get<Mesh>(handle), transform);
}
} // 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 = {};
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 = {};
+50 -1
View File
@@ -5,6 +5,10 @@
#include <imgui_impl_vulkan.h>
#include <imgui_internal.h>
#include <sophia/resource_registry.hpp>
#include "render_target.hpp"
namespace sophia {
namespace {
@@ -20,7 +24,8 @@ void dockAll(const std::vector<std::string>& 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<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) {
defaultLayout = std::move(layout);
seedIfUnsaved = true;
+20 -1
View File
@@ -2,31 +2,50 @@
#include <imgui.h>
#include <sophia/handles.hpp>
#include <sophia/types.hpp>
#include <sophia/ui/dock_layout.hpp>
#include <unordered_map>
#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<u32, Texture> textures;
DockLayout defaultLayout;
bool seedIfUnsaved = false;
+1 -1
View File
@@ -1,7 +1,7 @@
#pragma once
#include <memory>
#include <sophia/asset_registry.hpp>
#include <sophia/resource_registry.hpp>
#include <sophia/types.hpp>
#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>
@@ -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