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:
+89
-39
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <sophia/asset_registry.hpp>
|
||||
#include <sophia/resource_registry.hpp>
|
||||
#include <sophia/types.hpp>
|
||||
#include <vector>
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user