diff --git a/engine/include/sophia/components.hpp b/engine/include/sophia/components.hpp index ac37353..979c929 100644 --- a/engine/include/sophia/components.hpp +++ b/engine/include/sophia/components.hpp @@ -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 diff --git a/engine/src/application.cpp b/engine/src/application.cpp index ed843e7..19b58fb 100644 --- a/engine/src/application.cpp +++ b/engine/src/application.cpp @@ -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 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{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 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); - if (!sceneTarget) { - modelRenderSystem.render(commandBuffer, instances, frameInfo); - } + 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); + 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(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> meshes; std::vector drawCalls; @@ -259,14 +268,17 @@ Application::Application(const Config& config) : impl{std::make_unique(*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 vertices, std::vector 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(handle), transform); + impl->drawMesh(impl->getResourceRegistry().meshPool.get(handle), transform); } } // namespace sophia diff --git a/engine/src/rendering/render_graph.cpp b/engine/src/rendering/render_graph.cpp new file mode 100644 index 0000000..b9a1e60 --- /dev/null +++ b/engine/src/rendering/render_graph.cpp @@ -0,0 +1,159 @@ +#include "rendering/render_graph.hpp" + +#include +#include + +namespace sophia { + +ImageRef RenderGraph::importImage(Image& image, Access finalAccess) { + ImageRef ref{static_cast(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 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(attachments.size()); + renderingInfo.pColorAttachments = attachments.data(); + + commandBuffer.beginRendering(renderingInfo); + + vk::Viewport viewport{0.0f, + 0.0f, + static_cast(extent.width), + static_cast(extent.height), + 0.0f, + 1.0f}; + vk::Rect2D scissor{{0, 0}, extent}; + commandBuffer.setViewport(0, viewport); + commandBuffer.setScissor(0, scissor); + + 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 diff --git a/engine/src/rendering/render_graph.hpp b/engine/src/rendering/render_graph.hpp new file mode 100644 index 0000000..17ce0fb --- /dev/null +++ b/engine/src/rendering/render_graph.hpp @@ -0,0 +1,147 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#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; + +/** + * 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; + using ExecuteFn = std::function; + + 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 uses; + std::vector 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 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 images; + std::vector 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