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