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
4 changed files with 380 additions and 30 deletions
Showing only changes of commit 8fbabda095 - Show all commits
+32
View File
@@ -34,4 +34,36 @@ struct MeshComponent {
MeshHandle handle;
};
struct CameraComponent {
struct Orthographic {
float height = 10.f;
float nearPlane = -1.f;
float farPlane = 1.f;
};
struct Perspective {
float fov = glm::radians(60.f);
float nearPlane = 0.1f;
float farPlane = 100.f;
};
enum class Projection { Orthographic, Perspective };
Projection projection = Projection::Orthographic;
Orthographic ortho;
Perspective perspective;
glm::mat4 getProjection(float aspect) const {
if (projection == Projection::Perspective) {
return glm::perspective(perspective.fov, aspect, perspective.nearPlane,
perspective.farPlane);
}
float halfHeight = ortho.height / 2.f;
float halfWidth = halfHeight * aspect;
return glm::ortho(-halfWidth, halfWidth, -halfHeight, halfHeight,
ortho.nearPlane, ortho.farPlane);
}
};
} // namespace sophia
+38 -26
View File
@@ -21,6 +21,8 @@
#include "pipelines/graphics_pipeline.hpp"
#include "render_context.hpp"
#include "render_target.hpp"
#include "rendering/render_graph.hpp"
#include "resources/resource_registry.hpp"
#include "shader.hpp"
#include "systems/model_render_system.hpp"
#include "window.hpp"
@@ -42,11 +44,12 @@ struct PushConstants {
std::vector<std::string> commandLineArgs;
constexpr ClearColor CLEAR_COLOR = {0.01f, 0.01f, 0.02f, 1.0f};
void beginColorPass(vk::CommandBuffer commandBuffer, vk::ImageView view,
vk::Extent2D extent) {
vk::ClearValue clearValue{};
clearValue.color =
vk::ClearColorValue{std::array<float, 4>{0.01f, 0.01f, 0.02f, 1.0f}};
clearValue.color = vk::ClearColorValue{CLEAR_COLOR};
vk::RenderingAttachmentInfo colorAttachment{};
colorAttachment.imageView = view;
@@ -83,13 +86,14 @@ class Application::Impl {
config{config},
window{config.width, config.height, config.name},
device{&window},
renderContext{window, device} {}
renderContext{window, device},
resourceRegistry{device, renderContext.getImageFormat()} {}
void run() {
ModelRenderSystem modelRenderSystem{device, renderContext};
ImGuiRenderer imguiRenderer{window, device, renderContext.getImageFormat(),
renderContext.getImageCount(),
this->application.resourceRegistry};
this->resourceRegistry};
this->application._ui.renderer = &imguiRenderer;
this->application.consoleCommandRegistry.registerCommand(
@@ -100,8 +104,13 @@ class Application::Impl {
});
this->application.onCreate();
imguiRenderer.setDefaultLayout(
this->application.panelRegistry.getDefaultLayout());
auto& meshPool = this->resourceRegistry.meshPool;
auto& renderTargetPool = this->resourceRegistry.renderTargetPool;
PanelRegistry& panelRegistry = this->application.panelRegistry;
imguiRenderer.setDefaultLayout(panelRegistry.getDefaultLayout());
auto startTime = std::chrono::high_resolution_clock::now();
auto currentTime = startTime;
@@ -120,12 +129,11 @@ class Application::Impl {
// Resizing waits for the GPU, so it has to happen before any of this
// frame's work (or ImGui's references to the targets) is recorded.
this->application.resourceRegistry.applyPendingResizes();
renderTargetPool.applyPendingResizes();
RenderTarget* sceneTarget =
this->sceneTargetHandle.isValid()
? &this->application.resourceRegistry.getRenderTarget(
this->sceneTargetHandle)
? &renderTargetPool.get(this->sceneTargetHandle)
: nullptr;
vk::Extent2D extent = renderContext.getExtent();
@@ -149,7 +157,7 @@ class Application::Impl {
imguiRenderer.beginFrame();
this->application.panelRegistry.update(frameInfo);
panelRegistry.update(frameInfo);
this->application.onRender();
std::vector<ModelRenderSystem::MeshInstance> instances;
@@ -172,21 +180,25 @@ class Application::Impl {
vk::ImageLayout::eShaderReadOnlyOptimal);
}
renderContext.transitionCurrentImage(
commandBuffer, vk::ImageLayout::eColorAttachmentOptimal);
beginColorPass(commandBuffer, renderContext.getCurrentImageView(),
extent);
RenderGraph graph;
ImageRef backbuffer =
graph.importImage(renderContext.getCurrentImage(), Access::Present);
graph.addPass(
"main",
[&](RenderGraph::PassBuilder& pass) {
pass.writeColor(backbuffer, vk::AttachmentLoadOp::eClear,
CLEAR_COLOR);
},
[&](vk::CommandBuffer commandBuffer) {
if (!sceneTarget) {
modelRenderSystem.render(commandBuffer, instances, frameInfo);
}
imguiRenderer.endFrame(commandBuffer);
});
commandBuffer.endRendering();
renderContext.transitionCurrentImage(commandBuffer,
vk::ImageLayout::ePresentSrcKHR);
graph.execute(commandBuffer);
renderContext.endFrame();
@@ -232,11 +244,7 @@ class Application::Impl {
this->sceneTargetHandle = handle;
}
u32 getColorFormat() const {
return static_cast<u32>(this->renderContext.getImageFormat());
}
Device& getDevice() { return device; }
ResourceRegistry& getResourceRegistry() { return resourceRegistry; }
private:
Application& application;
@@ -244,6 +252,7 @@ class Application::Impl {
Window window;
Device device;
RenderContext renderContext;
ResourceRegistry resourceRegistry;
std::vector<std::unique_ptr<Mesh>> meshes;
std::vector<ModelRenderSystem::MeshInstance> drawCalls;
@@ -259,14 +268,17 @@ Application::Application(const Config& config)
: impl{std::make_unique<Impl>(*this, config)},
canvas{this},
args{std::move(commandLineArgs)},
resourceRegistry{impl->getDevice(), impl->getColorFormat()},
_ui{resourceRegistry},
_ui{ResourceRegistryRef{impl->getResourceRegistry()}},
_debugUI{this->panelRegistry, this->consoleCommandRegistry} {}
Application::~Application() = default;
void Application::run() { impl->run(); }
ResourceRegistryRef Application::resources() {
return ResourceRegistryRef{impl->getResourceRegistry()};
}
void Application::pushMesh(std::vector<CanvasVertex> vertices,
std::vector<u32> indices) {
impl->pushMesh(std::move(vertices), std::move(indices));
@@ -279,7 +291,7 @@ void Application::setSceneTarget(RenderTargetHandle handle) {
void Application::clearMeshes() { impl->clearMeshes(); }
void Application::drawMesh(MeshHandle handle, const glm::mat4& transform) {
impl->drawMesh(resourceRegistry.get<Mesh>(handle), transform);
impl->drawMesh(impl->getResourceRegistry().meshPool.get(handle), transform);
}
} // namespace sophia
+159
View File
@@ -0,0 +1,159 @@
#include "rendering/render_graph.hpp"
#include <sophia/util/assert.hpp>
#include <utility>
namespace sophia {
ImageRef RenderGraph::importImage(Image& image, Access finalAccess) {
ImageRef ref{static_cast<u32>(this->images.size())};
this->images.push_back({&image, finalAccess, image.getAccessState()});
return ref;
}
void RenderGraph::addPass(std::string name, const SetupFn& setup,
ExecuteFn execute) {
Pass& pass = this->passes.emplace_back();
pass.name = std::move(name);
pass.execute = std::move(execute);
PassBuilder builder{*this, pass};
setup(builder);
}
void RenderGraph::execute(vk::CommandBuffer commandBuffer) {
for (Pass& pass : this->passes) {
this->recordPass(commandBuffer, pass);
}
PendingBarriers pending;
for (u32 i = 0; i < this->images.size(); i++) {
this->transitionTo(ImageRef{i}, getAccessState(this->images[i].finalAccess),
pending);
}
this->flush(commandBuffer, pending);
for (ImportedImage& imported : this->images) {
imported.image->recordLayout(imported.state.layout);
}
this->passes.clear();
}
void RenderGraph::transitionTo(ImageRef ref, const AccessState& next,
PendingBarriers& pending) {
SOPHIA_ASSERT(ref.isValid() && ref.index < this->images.size(),
"ImageRef doesn't belong to this graph");
ImportedImage& imported = this->images[ref.index];
if (needsBarrier(imported.state, next)) {
pending.barriers.push_back(makeImageBarrier(
imported.image->get(), imported.image->getSubresourceRange(),
imported.state, next));
pending.srcStages |= imported.state.stage;
pending.dstStages |= next.stage;
imported.state = next;
} else {
// Same-layout read after read. Nothing to record, but a later write has
// to wait on this reader too.
imported.state.stage |= next.stage;
imported.state.access |= next.access;
}
}
void RenderGraph::flush(vk::CommandBuffer commandBuffer,
PendingBarriers& pending) {
if (pending.barriers.empty()) {
return;
}
commandBuffer.pipelineBarrier(pending.srcStages, pending.dstStages, {},
nullptr, nullptr, pending.barriers);
pending = PendingBarriers{};
}
void RenderGraph::recordPass(vk::CommandBuffer commandBuffer, Pass& pass) {
PendingBarriers pending;
for (const ImageUse& use : pass.uses) {
this->transitionTo(use.image, getAccessState(use.access), pending);
}
this->flush(commandBuffer, pending);
if (pass.colorAttachments.empty()) {
pass.execute(commandBuffer);
return;
}
std::vector<vk::RenderingAttachmentInfo> attachments;
attachments.reserve(pass.colorAttachments.size());
vk::Extent3D imageExtent =
this->images[pass.colorAttachments.front().image.index]
.image->getExtent();
vk::Extent2D extent{imageExtent.width, imageExtent.height};
for (const ColorAttachment& color : pass.colorAttachments) {
const Image& image = *this->images[color.image.index].image;
SOPHIA_ASSERT(image.getExtent().width == extent.width &&
image.getExtent().height == extent.height,
"Pass", pass.name,
"has color attachments of different sizes");
vk::RenderingAttachmentInfo attachment{};
attachment.imageView = image.getView();
attachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal;
attachment.loadOp = color.loadOp;
attachment.storeOp = vk::AttachmentStoreOp::eStore;
attachment.clearValue.color = vk::ClearColorValue{color.clearColor};
attachments.push_back(attachment);
}
vk::RenderingInfo renderingInfo{};
renderingInfo.renderArea = vk::Rect2D{{0, 0}, extent};
renderingInfo.layerCount = 1;
renderingInfo.colorAttachmentCount = static_cast<u32>(attachments.size());
renderingInfo.pColorAttachments = attachments.data();
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);
pass.execute(commandBuffer);
commandBuffer.endRendering();
}
RenderGraph::PassBuilder& RenderGraph::PassBuilder::writeColor(
ImageRef image, vk::AttachmentLoadOp loadOp, ClearColor clearColor) {
this->use(image, Access::ColorWrite);
this->pass.colorAttachments.push_back({image, loadOp, clearColor});
return *this;
}
RenderGraph::PassBuilder& RenderGraph::PassBuilder::sample(ImageRef image) {
return this->use(image, Access::SampledRead);
}
RenderGraph::PassBuilder& RenderGraph::PassBuilder::use(ImageRef image,
Access access) {
SOPHIA_ASSERT(image.isValid() && image.index < this->graph.images.size(),
"ImageRef doesn't belong to this graph");
for (const ImageUse& existing : this->pass.uses) {
SOPHIA_ASSERT(existing.image.index != image.index, "Pass", this->pass.name,
"uses the same image twice");
}
this->pass.uses.push_back({image, access});
return *this;
}
} // namespace sophia
+147
View File
@@ -0,0 +1,147 @@
#pragma once
#include <array>
#include <functional>
#include <sophia/types.hpp>
#include <string>
#include <vector>
#include <vulkan/vulkan.hpp>
#include "image.hpp"
#include "rendering/access.hpp"
namespace sophia {
/**
* Names an image within one RenderGraph. Only valid for the graph that
* returned it.
*/
struct ImageRef {
u32 index = INVALID_INDEX;
static constexpr u32 INVALID_INDEX = 0xFFFFFFFF;
bool isValid() const { return this->index != INVALID_INDEX; }
};
using ClearColor = std::array<float, 4>;
/**
* A frame's passes and the images they touch, recorded into a single command
* buffer. Passes run in the order they were added; the graph's job is to
* work out the barriers and layout transitions between them from what each
* pass says it does to each image, and to begin/end rendering for passes that
* write color attachments.
*
* Built fresh every frame:
*
* RenderGraph graph;
* ImageRef backbuffer = graph.importImage(swapchainImage, Access::Present);
* graph.addPass(
* "main", [&](PassBuilder& pass) { pass.writeColor(backbuffer); },
* [&](vk::CommandBuffer cb) { draw(cb); });
* graph.execute(commandBuffer);
*
* Imported images start in whatever layout Image reports and are left in
* their final access when execute() returns, with the Image's tracked layout
* updated to match.
*/
class RenderGraph {
public:
class PassBuilder;
using SetupFn = std::function<void(PassBuilder&)>;
using ExecuteFn = std::function<void(vk::CommandBuffer)>;
RenderGraph() = default;
RenderGraph(const RenderGraph&) = delete;
RenderGraph& operator=(const RenderGraph&) = delete;
/**
* Brings an image the graph doesn't own into it. It must outlive
* execute(). After the last pass it is transitioned to `finalAccess`.
*/
ImageRef importImage(Image& image, Access finalAccess);
/**
* Adds a pass. `setup` runs immediately and declares what the pass does to
* images; `execute` runs later, from execute(), with the barriers already
* recorded and (if the pass writes color) rendering begun.
*/
void addPass(std::string name, const SetupFn& setup, ExecuteFn execute);
/**
* Records every pass, and the barriers between them, into `commandBuffer`.
*/
void execute(vk::CommandBuffer commandBuffer);
private:
struct ImageUse {
ImageRef image;
Access access;
};
struct ColorAttachment {
ImageRef image;
vk::AttachmentLoadOp loadOp;
ClearColor clearColor;
};
struct Pass {
std::string name;
std::vector<ImageUse> uses;
std::vector<ColorAttachment> colorAttachments;
ExecuteFn execute;
};
struct ImportedImage {
Image* image;
Access finalAccess;
// What the image was last left in, as of the barriers recorded so far.
AccessState state;
};
struct PendingBarriers {
std::vector<vk::ImageMemoryBarrier> barriers;
vk::PipelineStageFlags srcStages;
vk::PipelineStageFlags dstStages;
};
void transitionTo(ImageRef ref, const AccessState& next,
PendingBarriers& pending);
void flush(vk::CommandBuffer commandBuffer, PendingBarriers& pending);
void recordPass(vk::CommandBuffer commandBuffer, Pass& pass);
std::vector<ImportedImage> images;
std::vector<Pass> passes;
};
class RenderGraph::PassBuilder {
public:
/**
* Renders into `image` as a color attachment. Passes with one or more of
* these get rendering begun (and viewport/scissor set to the attachment's
* extent) before their execute callback runs.
*/
PassBuilder& writeColor(
ImageRef image,
vk::AttachmentLoadOp loadOp = vk::AttachmentLoadOp::eClear,
ClearColor clearColor = {0.f, 0.f, 0.f, 1.f});
/** Samples `image` in a shader. */
PassBuilder& sample(ImageRef image);
/** Any other use, e.g. storage or transfer access. */
PassBuilder& use(ImageRef image, Access access);
private:
friend class RenderGraph;
PassBuilder(RenderGraph& graph, Pass& pass) : graph{graph}, pass{pass} {}
RenderGraph& graph;
Pass& pass;
};
} // namespace sophia