feat: 引入 RenderGraph、Application 框架、ECS 与资产句柄系统

架构分层(自上而下):
  App(SandboxApp) → Application(生命周期+事件) → Scene(ECS) / AssetManager(句柄) → Renderer → RenderGraph → Vulkan 模块

- RenderGraph:声明式帧图。虚拟资源+生成号句柄、死 pass 剔除、
  拓扑排序、资源生命周期、barrier 编译期预算(Synchronization2)、
  物理镜像池按帧槽位隔离(帧间竞争从根上消除)
- 帧同步修复:per-frame-slot 命令缓冲与 UBO ring(原先单份存在
  数据竞争);统一 kMaxFramesInFlight / FrameContext
- Application:生命周期钩子(OnInit/OnUpdate/OnRender/OnShutdown)
  + 事件系统(GLFW 回调 → POD 事件 → 类型化钩子);main 只 new 派生类
- ECS:自研稀疏集 Registry(Entity=(index,generation) 防悬空,
  O(1) 增删查,Each 取最小池遍历);Scene 延迟销毁队列;
  IdComponent{UUID} 稳定外部身份
- Assets:AssetManager + AssetHandle。组件存句柄不存裸指针/Ref
  (单一所有者 + 多使用者),Renderer 删除 LoadModel 改为纯消费
- 修复退出析构:成员析构前 GPU waitIdle(资产层先于 Renderer
  析构导致 VUID-00922 资源销毁时仍在使用)
- 引入 entt 子模块备用

validation 全程零错误零警告(RTX 5060 实测)
This commit is contained in:
2026-09-22 13:46:05 +08:00
parent 7d628f8d8a
commit 8afdd9b245
33 changed files with 3013 additions and 561 deletions
@@ -0,0 +1,37 @@
//
// Created by atdunbg on 2026/9/22.
//
// AssetHandle:资产句柄 = (池内索引, 生成号),与 Entity/RG 句柄同一哲学。
//
// 组件、场景数据只存句柄;所有权归 AssetManager。
// 悬空句柄(generation 不匹配)会被 Get 显式拒绝,杜绝 use-after-free。
//
#ifndef HELLOVULKAN_ASSETHANDLE_H
#define HELLOVULKAN_ASSETHANDLE_H
#include <Engine/Core/Types.h>
// Model 前向声明(句柄不依赖完整类型)
namespace Engine { class Model; }
namespace Engine::Assets {
inline constexpr u32 kInvalidAssetIndex = 0xFFFFFFFF;
template<typename T>
struct AssetHandle {
u32 index = kInvalidAssetIndex;
u32 generation = 0;
bool Valid() const { return index != kInvalidAssetIndex; }
explicit operator bool() const { return Valid(); }
bool operator==(const AssetHandle& o) const = default;
};
// 常用别名
using ModelHandle = AssetHandle<Engine::Model>;
} // namespace Engine::Assets
#endif //HELLOVULKAN_ASSETHANDLE_H
+173
View File
@@ -0,0 +1,173 @@
//
// Created by atdunbg on 2026/9/22.
//
// AssetManager:引擎资产层。
//
// 职责划分(对照 Renderer):
// - 资产层:路径 → 解析/加载 → 缓存去重 → 句柄分发 + 生命周期管理
// - 渲染层:拿已解析资产创建/绑定 GPU 资源
//
// Model 的构造仍需要 GPU 上下文(buffer/image upload),这是现阶段的
// 折中:资产层"使用"GPU 层的接口,但 Renderer 不再知道磁盘和缓存。
// 后续演进:把 Model 拆成 CPU 解析数据 + GPU 上传两段,实现真正的
// 异步加载(IO 线程解析 → 主线程/上传队列提交),本文件结构已为此留好位置。
//
// 组件存句柄不存指针的理由:
// shared_ptr 会引入共享所有权 → 原子引用计数(swap-remove 拷贝时)
// + 不可预测的销毁时机(GPU 可能还在用)+ 热重载被引用计数卡死。
// 句柄 + 单一所有者(本类)是引擎级标准形态。
//
#ifndef HELLOVULKAN_ASSETMANAGER_H
#define HELLOVULKAN_ASSETMANAGER_H
#include <Engine/Core/Types.h>
#include <Engine/Core/Log.h>
#include <Engine/Assets/AssetHandle.h>
#include <Engine/Renderer/Model.h>
#include <functional>
#include <string>
#include <unordered_map>
#include <vector>
namespace Engine {
class Device;
class MemoryAllocator;
class DescriptorManager;
namespace Assets {
// ---------- 通用资产缓存(不感知 Vulkan / 磁盘细节由 loader 决定) ----------
template<typename T>
class AssetCache {
public:
// loader:path → 资产实例(失败返回 nullptr)
using LoaderFn = std::function<Scope<T>(const std::string&)>;
// 加载(带路径去重)。失败返回无效句柄。
AssetHandle<T> Load(const std::string& path, const LoaderFn& loader)
{
if (auto it = m_ByPath.find(path); it != m_ByPath.end()) {
return it->second;
}
Scope<T> asset = loader(path);
if (!asset) {
ENGINE_ERROR("AssetCache: failed to load '{}'", path);
return {};
}
AssetHandle<T> h;
if (!m_Free.empty()) {
h.index = m_Free.back();
m_Free.pop_back();
h.generation = m_Entries[h.index].generation;
m_Entries[h.index] = Entry{path, h.generation, std::move(asset), true};
} else {
h.index = static_cast<u32>(m_Entries.size());
h.generation = 1;
m_Entries.push_back(Entry{path, h.generation, std::move(asset), true});
}
m_ByPath.emplace(path, h);
return h;
}
// 句柄 → 资产指针。悬空句柄 FATAL(bug 信号);已卸载返回 nullptr。
T* Get(AssetHandle<T> h) const
{
if (!h.Valid() || h.index >= m_Entries.size()) {
ENGINE_FATAL("AssetCache::Get: invalid handle (index={}, gen={})",
h.index, h.generation);
}
const Entry& e = m_Entries[h.index];
if (e.generation != h.generation) {
ENGINE_FATAL("AssetCache::Get: stale handle for '{}' (gen {} != {})",
e.path, h.generation, e.generation);
}
return e.alive ? e.asset.get() : nullptr;
}
// 卸载:句柄即刻失效(generation 递增),路径缓存清除。
// 注意:GPU 侧何时安全释放由上层(Renderer fence)保证——
// 本类只管所有权,不管 GPU 生命周期。
void Unload(AssetHandle<T> h)
{
if (!h.Valid() || h.index >= m_Entries.size()) return;
Entry& e = m_Entries[h.index];
if (e.generation != h.generation || !e.alive) return;
m_ByPath.erase(e.path);
e.path.clear();
e.asset.reset();
e.alive = false;
++e.generation;
m_Free.push_back(h.index);
}
const std::string& Path(AssetHandle<T> h) const
{
static const std::string kEmpty;
if (!h.Valid() || h.index >= m_Entries.size()) return kEmpty;
const Entry& e = m_Entries[h.index];
return (e.generation == h.generation && e.alive) ? e.path : kEmpty;
}
u32 Count() const
{
return static_cast<u32>(m_Entries.size()) - static_cast<u32>(m_Free.size());
}
private:
struct Entry {
std::string path;
u32 generation = 1;
Scope<T> asset;
bool alive = false;
};
std::vector<Entry> m_Entries;
std::vector<u32> m_Free;
std::unordered_map<std::string, AssetHandle<T>> m_ByPath;
};
// ---------- AssetManager:全引擎资产入口 ----------
// (ModelHandle 别名来自 AssetHandle.h)
class AssetManager {
public:
// GPU 上下文由组合根(Application)在 Renderer 装配完成后注入
AssetManager(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptors,
vk::DescriptorSetLayout textureLayout);
~AssetManager() = default;
AssetManager(const AssetManager&) = delete;
AssetManager& operator=(const AssetManager&) = delete;
ModelHandle LoadModel(const std::string& path);
Model* GetModel(ModelHandle h) const { return m_Models.Get(h); }
void UnloadModel(ModelHandle h) { m_Models.Unload(h); }
AssetCache<Model>& Models() { return m_Models; }
// 后续扩展点:TextureHandle / MaterialHandle / ShaderHandle
// 各自一个 AssetCache<T> + 便捷方法,模式与 Model 完全一致。
private:
Device& m_Device;
MemoryAllocator& m_Allocator;
DescriptorManager& m_Descriptors;
vk::DescriptorSetLayout m_TextureLayout;
AssetCache<Model> m_Models;
};
} // namespace Assets
} // namespace Engine
#endif //HELLOVULKAN_ASSETMANAGER_H
+101
View File
@@ -0,0 +1,101 @@
//
// Created by atdunbg on 2026/9/22.
//
// Application:程序生命周期基类。
//
// 分层目标:main 不再知道 Window/Renderer 的存在——
// main 只 new 一个派生类并 Run();
// 派生类通过生命周期钩子(OnInit/OnUpdate/OnRender/OnShutdown)
// 和事件钩子(OnEvent + 类型化事件)定义程序行为;
// runtime、editor 等不同程序形态只是不同的派生类。
//
// 主循环固定为:PollEvents → OnUpdate(dt) → OnRender → 判退出。
// dt 由框架统一计时(含首帧与卡顿钳制),派生层无需自己管理。
//
#ifndef HELLOVULKAN_APPLICATION_H
#define HELLOVULKAN_APPLICATION_H
#include <Engine/Core/Types.h>
#include <Engine/Core/Event.h>
#include <chrono>
#include <string>
namespace Engine {
class Window;
class Renderer;
namespace ECS { class Scene; }
namespace Assets { class AssetManager; }
class Application {
public:
Application(std::string title, u32 width, u32 height);
virtual ~Application();
Application(const Application&) = delete;
Application& operator=(const Application&) = delete;
// 进入主循环(阻塞直到 Close() 或窗口关闭)
void Run();
// 请求退出(当帧结束后生效)
void Close() { m_Running = false; }
// ---- 供派生类访问的模块 ----
Window& GetWindow() const { return *m_Window; }
Renderer& GetRenderer() const { return *m_Renderer; }
ECS::Scene& GetScene() const { return *m_Scene; }
Assets::AssetManager& GetAssets() const { return *m_Assets; }
f32 GetDeltaTime() const { return m_DeltaTime; }
u64 GetFrameCount() const { return m_FrameCount; }
protected:
// ---- 生命周期钩子 ----
// 框架装配完成(Window/Renderer 可用),派生类做初始化(建场景等)
virtual void OnInit() {}
// 每帧逻辑更新(输入、动画、物理……)
virtual void OnUpdate(f32 deltaTime) {}
// 每帧渲染;默认实现驱动 Renderer 主循环,派生类可覆写
virtual void OnRender();
// 主循环结束后、析构前(保存、清理场景等)
virtual void OnShutdown() {}
// ---- 事件钩子 ----
// OnEvent 收到所有事件(原始入口,适合做全局日志/拦截);
// 类型化钩子按事件类型分别回调,派生类按需覆写。
virtual void OnEvent(const Event& event) {}
virtual void OnWindowClose(const Event& event);
virtual void OnWindowResize(const Event& event) {}
virtual void OnWindowFocus(const Event& event) {}
virtual void OnWindowLostFocus(const Event& event) {}
virtual void OnKeyPress(const Event& event) {}
virtual void OnKeyRelease(const Event& event) {}
virtual void OnMouseButtonPress(const Event& event) {}
virtual void OnMouseButtonRelease(const Event& event) {}
virtual void OnMouseMove(const Event& event) {}
virtual void OnMouseScroll(const Event& event) {}
private:
void DispatchEvent(const Event& event);
Scope<Window> m_Window;
Scope<ECS::Scene> m_Scene;
Scope<Renderer> m_Renderer;
Scope<Assets::AssetManager> m_Assets;
bool m_Running = false;
f32 m_DeltaTime = 0.0f;
u64 m_FrameCount = 0;
std::chrono::steady_clock::time_point m_LastFrameTime;
};
} // namespace Engine
#endif //HELLOVULKAN_APPLICATION_H
+76
View File
@@ -0,0 +1,76 @@
//
// Created by atdunbg on 2026/9/22.
//
// Event:轻量级事件。
//
// 设计取舍:不用继承体系(每事件一个类 + dynamic_cast 分发),
// 用单个 POD 结构 + 类型枚举。事件每帧可能产生成百上千条
// (MouseMove 尤其频繁),POD 按值传递/排队最便宜;
// 类型安全由字段约定保证,规模上来后再演进为 variant/队列池。
//
#ifndef HELLOVULKAN_EVENT_H
#define HELLOVULKAN_EVENT_H
#include <Engine/Core/Types.h>
namespace Engine {
enum class EventType : u8 {
None = 0,
// 窗口
WindowClose,
WindowResize,
WindowFocus,
WindowLostFocus,
// 键盘
KeyPress,
KeyRelease,
// 鼠标
MouseButtonPress,
MouseButtonRelease,
MouseMove,
MouseScroll,
};
struct Event {
EventType type = EventType::None;
// 键盘(GLFW key code)/ 鼠标按钮
i32 key = 0;
i32 button = 0;
// 鼠标位置 / 滚轮
f32 x = 0.0f;
f32 y = 0.0f;
f32 deltaX = 0.0f;
f32 deltaY = 0.0f;
// 窗口尺寸
i32 width = 0;
i32 height = 0;
};
inline const char* ToString(EventType type)
{
switch (type) {
case EventType::WindowClose: return "WindowClose";
case EventType::WindowResize: return "WindowResize";
case EventType::WindowFocus: return "WindowFocus";
case EventType::WindowLostFocus: return "WindowLostFocus";
case EventType::KeyPress: return "KeyPress";
case EventType::KeyRelease: return "KeyRelease";
case EventType::MouseButtonPress: return "MouseButtonPress";
case EventType::MouseButtonRelease: return "MouseButtonRelease";
case EventType::MouseMove: return "MouseMove";
case EventType::MouseScroll: return "MouseScroll";
default: return "None";
}
}
} // namespace Engine
#endif //HELLOVULKAN_EVENT_H
+29
View File
@@ -0,0 +1,29 @@
//
// Created by atdunbg on 2026/9/22.
//
// FrameContext:一帧在 GPU 侧的全部上下文。
// 引擎中所有 per-frame 资源(命令缓冲、UBO ring、描述符池等)
// 都以 frameSlot 为索引,保证 kMaxFramesInFlight 帧并发时互不踩踏。
//
#ifndef HELLOVULKAN_FRAMECONTEXT_H
#define HELLOVULKAN_FRAMECONTEXT_H
#include <Engine/Core/Types.h>
#include <vulkan/vulkan.hpp>
namespace Engine {
// 引擎统一的最大并发帧数。
// 所有 per-frame 资源必须以此为 ring 长度,不允许再出现"单独一个"的帧资源。
inline constexpr u32 kMaxFramesInFlight = 2;
struct FrameContext {
u32 frameSlot = 0; // 帧-in-flight 槽位 [0, kMaxFramesInFlight)
u32 imageIndex = 0; // 本次 acquire 到的 swapchain image 索引
vk::CommandBuffer cmd;
};
} // namespace Engine
#endif //HELLOVULKAN_FRAMECONTEXT_H
+52
View File
@@ -0,0 +1,52 @@
//
// Created by atdunbg on 2026/9/22.
//
// UUID:64 位随机唯一标识,实体的稳定外部身份。
//
// 用途:序列化、跨场景引用、编辑器对象选择——这些场景需要
// "实体销毁重建后 ID 不变"或"独立于槽位分配"的稳定标识。
// ECS 内部遍历仍用 (index, generation) 句柄(性能),UUID 只做外部身份。
//
#ifndef HELLOVULKAN_UUID_H
#define HELLOVULKAN_UUID_H
#include <Engine/Core/Types.h>
#include <functional>
#include <random>
namespace Engine {
struct UUID {
UUID() = default;
explicit UUID(u64 value) : m_Value(value) {}
// 生成全局唯一 ID(非确定性种子,跨进程不重复)
static UUID New()
{
static std::mt19937_64 rng{std::random_device{}()};
return UUID{rng()};
}
u64 Value() const { return m_Value; }
bool Valid() const { return m_Value != 0; }
bool operator==(const UUID& o) const = default;
bool operator!=(const UUID& o) const = default;
private:
u64 m_Value = 0; // 0 保留为"无效"
};
} // namespace Engine
namespace std {
template<> struct hash<Engine::UUID> {
size_t operator()(const Engine::UUID& id) const noexcept {
return std::hash<std::uint64_t>{}(id.Value());
}
};
}
#endif //HELLOVULKAN_UUID_H
+14
View File
@@ -11,6 +11,7 @@
#include <functional>
#include "Types.h"
#include "Event.h"
struct GLFWwindow;
@@ -27,6 +28,11 @@ namespace Engine {
bool ShouldClose() const;
void PollEvents();
// 事件出口:GLFW 回调 → Event → 上层回调(由 Application 注册)。
// 未注册时事件直接丢弃。
using EventCallbackFn = std::function<void(const Event&)>;
void SetEventCallback(EventCallbackFn callback) { m_EventCallback = std::move(callback); }
vk::SurfaceKHR CreateSurface(vk::Instance instance);
void DestroySurface(vk::Instance instance, vk::SurfaceKHR surface);
@@ -57,7 +63,15 @@ namespace Engine {
std::string m_Title;
bool m_FramebufferResized = false;
EventCallbackFn m_EventCallback;
// GLFW 静态回调(通过 window user pointer 找回 this)
static void FramebufferResizeCallback(GLFWwindow* window, int width, int height);
static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods);
static void MouseButtonCallback(GLFWwindow* window, int button, int action, int mods);
static void CursorPosCallback(GLFWwindow* window, double xpos, double ypos);
static void ScrollCallback(GLFWwindow* window, double xoffset, double yoffset);
static void WindowFocusCallback(GLFWwindow* window, int focused);
};
} // namespace Engine
+62
View File
@@ -0,0 +1,62 @@
//
// Created by atdunbg on 2026/9/22.
//
// Components:内置组件集合。
// 组件是纯数据(POD 倾向),逻辑在系统/应用层。
//
#ifndef HELLOVULKAN_COMPONENTS_H
#define HELLOVULKAN_COMPONENTS_H
#include <Engine/Core/Types.h>
#include <Engine/Core/UUID.h>
#include <Engine/Renderer/Camera.h>
#include <Engine/Assets/AssetHandle.h>
#include <glm/glm.hpp>
#include <string>
namespace Engine {
class Model;
}
namespace Engine::ECS {
// 实体稳定身份:CreateEntity 时自动分配,用于序列化/跨引用。
// ECS 内部遍历不用它(用 index+generation 句柄,性能)。
struct IdComponent {
UUID uuid;
};
// 实体名(调试/查询用;大规模场景建议演进为哈希 id)
struct NameComponent {
std::string name;
};
// 世界变换。当前直接存世界矩阵(渲染热路径友好);
// 需要 SRT 编辑时再加 LocalTransform 组件 + 层级同步系统。
struct TransformComponent {
glm::mat4 world{1.0f};
};
// 网格渲染体:引用共享的 Model 资产。
// 存句柄不存指针/Ref:所有权归 AssetManager,悬空由句柄 generation 拦截。
struct MeshRendererComponent {
Assets::ModelHandle model;
};
// 相机。一个场景中 primary=true 的实体是当前渲染视角。
struct CameraComponent {
Camera camera;
bool primary = false;
};
// 方向光
struct LightComponent {
glm::vec3 direction{0.0f, -1.0f, 0.0f};
glm::vec4 color{1.0f, 1.0f, 1.0f, 1.0f};
};
} // namespace Engine::ECS
#endif //HELLOVULKAN_COMPONENTS_H
+32
View File
@@ -0,0 +1,32 @@
//
// Created by atdunbg on 2026/9/22.
//
// Entity:实体句柄 = (池内索引, 生成号)。
//
// 生成号用于甄别悬空引用:实体销毁后索引会回收复用,
// 旧的 Entity 句柄因 generation 不匹配会被 Registry 拒绝,
// 从机制上杜绝"引用到复用后的错误实体"这类隐式 bug。
//
#ifndef HELLOVULKAN_ENTITY_H
#define HELLOVULKAN_ENTITY_H
#include <Engine/Core/Types.h>
namespace Engine {
struct Entity {
static constexpr u32 kInvalid = 0xFFFFFFFF;
u32 index = kInvalid; // 实体槽位索引
u32 generation = 0; // 槽位复用计数
bool Valid() const { return index != kInvalid; }
explicit operator bool() const { return Valid(); }
bool operator==(const Entity& o) const = default;
bool operator!=(const Entity& o) const = default;
};
} // namespace Engine
#endif //HELLOVULKAN_ENTITY_H
+312
View File
@@ -0,0 +1,312 @@
//
// Created by atdunbg on 2026/9/22.
//
// Registry:稀疏集 ECS 核心。
//
// 数据结构(与 EnTT 同族,自研实现):
// - 每个组件类型一个 ComponentPool<T>:sparse 索引表 + dense 实体/组件数组
// - Add/Remove/Get 全部 O(1);Remove 用 swap-remove(O(1),代价是
// dense 顺序会变——渲染遍历只要求"每帧确定",不要求跨帧稳定)
// - Each<A,B,...> 以最小的池为主表遍历,其余池做 O(1) Contains 探测,
// 复杂度 O(min(|A|,|B|,...))——大场景下的关键保证
//
// 约束(引擎级正确性):
// - 悬空句柄:generation 不匹配直接 FATAL,不静默
// - 遍历中禁止结构变更(Add/Remove 组件、Create/Destroy 实体),
// 会使池失效——销毁走延迟队列(Scene::DestroyEntity → FlushDestroyQueue)
// - 全程无虚调用开销在热路径上(Get/Add 直接走模板池)
//
#ifndef HELLOVULKAN_REGISTRY_H
#define HELLOVULKAN_REGISTRY_H
#include <Engine/Core/Types.h>
#include <Engine/Core/Log.h>
#include <Engine/ECS/Entity.h>
#include <array>
#include <tuple>
#include <vector>
#include <memory>
namespace Engine::ECS {
// ---------- 组件类型 ID(编译期静态分配) ----------
using ComponentTypeId = u32;
namespace detail {
inline ComponentTypeId NextComponentTypeId()
{
static ComponentTypeId counter = 0;
return counter++;
}
}
template<typename T>
ComponentTypeId ComponentTypeIdOf()
{
static const ComponentTypeId id = detail::NextComponentTypeId();
return id;
}
// ---------- 类型擦除池接口(Registry 管理生命周期 + 视图主表遍历用) ----------
class IPool {
public:
virtual ~IPool() = default;
virtual u32 Size() const = 0;
virtual Entity EntityAt(u32 denseIndex) const = 0;
virtual bool Remove(Entity e) = 0; // 不存在返回 false
};
// ---------- 稀疏集组件池 ----------
template<typename T>
class ComponentPool final : public IPool {
public:
static constexpr u32 kInvalid = 0xFFFFFFFF;
T& Add(Entity e, T component)
{
if (Contains(e)) {
ENGINE_FATAL("ComponentPool: entity ({},{}) already has this component",
e.index, e.generation);
}
if (e.index >= m_Sparse.size()) {
m_Sparse.resize(e.index + 1, kInvalid);
}
m_Sparse[e.index] = static_cast<u32>(m_Components.size());
m_Entities.push_back(e);
m_Components.push_back(std::move(component));
return m_Components.back();
}
bool Contains(Entity e) const
{
return e.index < m_Sparse.size() &&
m_Sparse[e.index] != kInvalid &&
m_Entities[m_Sparse[e.index]] == e;
}
T& Get(Entity e)
{
if (!Contains(e)) {
ENGINE_FATAL("ComponentPool: entity ({},{}) does not have this component",
e.index, e.generation);
}
return m_Components[m_Sparse[e.index]];
}
const T& Get(Entity e) const
{
return const_cast<ComponentPool*>(this)->Get(e);
}
bool Remove(Entity e) override
{
if (!Contains(e)) return false;
const u32 denseIdx = m_Sparse[e.index];
const u32 lastIdx = static_cast<u32>(m_Components.size()) - 1;
if (denseIdx != lastIdx) {
// swap-remove:尾元素补位
m_Entities[denseIdx] = m_Entities[lastIdx];
m_Components[denseIdx] = std::move(m_Components[lastIdx]);
m_Sparse[m_Entities[denseIdx].index] = denseIdx;
}
m_Sparse[e.index] = kInvalid;
m_Entities.pop_back();
m_Components.pop_back();
return true;
}
u32 Size() const override { return static_cast<u32>(m_Components.size()); }
Entity EntityAt(u32 denseIndex) const override { return m_Entities[denseIndex]; }
private:
std::vector<u32> m_Sparse; // 实体索引 -> dense 下标(kInvalid = 无)
std::vector<Entity> m_Entities; // dense:与 m_Components 平行
std::vector<T> m_Components; // dense:组件数据(连续内存,缓存友好)
};
// ---------- Registry ----------
class Registry {
public:
Registry() = default;
~Registry() = default;
Registry(const Registry&) = delete;
Registry& operator=(const Registry&) = delete;
// ---- 实体生命周期 ----
Entity Create()
{
if (!m_FreeIndices.empty()) {
const u32 index = m_FreeIndices.back();
m_FreeIndices.pop_back();
m_Records[index].alive = true; // generation 已在 Destroy 时递增
return Entity{index, m_Records[index].generation};
}
m_Records.push_back({0, true});
return Entity{static_cast<u32>(m_Records.size() - 1), 0};
}
void Destroy(Entity e)
{
if (!IsAlive(e)) {
ENGINE_FATAL("Registry::Destroy: stale entity ({},{})",
e.index, e.generation);
}
for (auto& pool : m_Pools) {
if (pool) pool->Remove(e); // 不在池中就跳过
}
auto& record = m_Records[e.index];
record.alive = false;
++record.generation;
m_FreeIndices.push_back(e.index);
}
bool IsAlive(Entity e) const
{
return e.Valid() && e.index < m_Records.size() &&
m_Records[e.index].alive &&
m_Records[e.index].generation == e.generation;
}
// ---- 组件操作 ----
template<typename T, typename... Args>
T& Add(Entity e, Args&&... args)
{
if (!IsAlive(e)) {
ENGINE_FATAL("Registry::Add: stale entity ({},{})", e.index, e.generation);
}
return AssurePool<T>().Add(e, T{std::forward<Args>(args)...});
}
template<typename T>
T& Get(Entity e)
{
if (!IsAlive(e)) {
ENGINE_FATAL("Registry::Get: stale entity ({},{})", e.index, e.generation);
}
auto* pool = GetPool<T>();
if (!pool) {
ENGINE_FATAL("Registry::Get: no component pool of this type exists");
}
return pool->Get(e);
}
template<typename T>
T* TryGet(Entity e)
{
if (!IsAlive(e)) return nullptr;
auto* pool = GetPool<T>();
return (pool && pool->Contains(e)) ? &pool->Get(e) : nullptr;
}
template<typename T>
bool Has(Entity e)
{
if (!IsAlive(e)) return false;
auto* pool = GetPool<T>();
return pool && pool->Contains(e);
}
template<typename T>
bool Remove(Entity e)
{
if (!IsAlive(e)) return false;
auto* pool = GetPool<T>();
return pool && pool->Remove(e);
}
// ---- 遍历(视图)----
// fn(Entity, Ts&...);以最小的池为主表,复杂度 O(min pool size)。
// 遍历中禁止结构变更(见文件头约束)。
template<typename... Ts, typename Fn>
void Each(Fn&& fn)
{
static_assert(sizeof...(Ts) > 0, "Each requires at least one component type");
std::tuple<ComponentPool<Ts>*...> pools{GetPool<Ts>()...};
// 任一类型池不存在 → 空集
bool allExist = true;
std::apply([&allExist](auto*... p) {
((p || (allExist = false)) && ...);
}, pools);
if (!allExist) return;
// 主池:取最小者
const std::array<IPool*, sizeof...(Ts)> basePools{GetPool<Ts>()...};
IPool* primary = nullptr;
u32 minSize = 0xFFFFFFFF;
for (auto* p : basePools) {
if (p && p->Size() < minSize) {
minSize = p->Size();
primary = p;
}
}
if (!primary) return;
for (u32 i = 0; i < primary->Size(); ++i) {
const Entity e = primary->EntityAt(i);
if (!IsAlive(e)) continue;
bool all = true;
std::apply([&](auto*... p) {
all = (p->Contains(e) && ...); // 短路折叠
}, pools);
if (!all) continue;
std::apply([&](auto*... p) {
fn(e, p->Get(e)...);
}, pools);
}
}
private:
template<typename T>
ComponentPool<T>* GetPool()
{
const ComponentTypeId id = ComponentTypeIdOf<T>();
if (id >= m_Pools.size() || !m_Pools[id]) return nullptr;
return static_cast<ComponentPool<T>*>(m_Pools[id].get());
}
template<typename T>
ComponentPool<T>& AssurePool()
{
const ComponentTypeId id = ComponentTypeIdOf<T>();
if (id >= m_Pools.size()) {
m_Pools.resize(id + 1);
}
if (!m_Pools[id]) {
m_Pools[id] = std::make_unique<ComponentPool<T>>();
}
return static_cast<ComponentPool<T>&>(*m_Pools[id]);
}
struct EntityRecord {
u32 generation = 0;
bool alive = false;
};
std::vector<EntityRecord> m_Records;
std::vector<u32> m_FreeIndices; // 可复用槽位
std::vector<std::unique_ptr<IPool>> m_Pools; // 按 ComponentTypeId 索引
};
} // namespace Engine::ECS
#endif //HELLOVULKAN_REGISTRY_H
+84
View File
@@ -0,0 +1,84 @@
//
// Created by atdunbg on 2026/9/22.
//
// Scene:世界容器 = Registry + 场景级设置 + 安全的生命周期封装。
//
// 销毁策略:DestroyEntity 只入队,FlushDestroyQueue 在帧尾统一执行,
// 保证遍历(Each)过程中组件池结构不变——这是 ECS 正确性的关键约束。
//
#ifndef HELLOVULKAN_SCENE_H
#define HELLOVULKAN_SCENE_H
#include <Engine/Core/Types.h>
#include <Engine/ECS/Registry.h>
#include <Engine/ECS/Components.h>
#include <string>
#include <vector>
namespace Engine::ECS {
class Scene {
public:
Scene() = default;
~Scene() = default;
Scene(const Scene&) = delete;
Scene& operator=(const Scene&) = delete;
// ---- 场景级设置 ----
struct Settings {
glm::vec4 ambientColor{0.15f, 0.15f, 0.2f, 0.0f};
};
Settings& GetSettings() { return m_Settings; }
// ---- 实体 ----
Entity CreateEntity(std::string name = {});
void DestroyEntity(Entity e); // 延迟:帧尾统一销毁
void FlushDestroyQueue(); // 每帧末尾由 Application 调用
bool IsAlive(Entity e) const { return m_Registry.IsAlive(e); }
// 按稳定 UUID 查实体(序列化/跨引用场景);找不到返回无效句柄
Entity FindEntityByUUID(UUID uuid);
Registry& GetRegistry() { return m_Registry; }
// ---- 组件便捷接口(转发 Registry)----
template<typename T, typename... Args>
T& AddComponent(Entity e, Args&&... args)
{
return m_Registry.Add<T>(e, std::forward<Args>(args)...);
}
template<typename T>
T& GetComponent(Entity e) { return m_Registry.Get<T>(e); }
template<typename T>
T* TryGetComponent(Entity e) { return m_Registry.TryGet<T>(e); }
template<typename T>
bool HasComponent(Entity e) { return m_Registry.Has<T>(e); }
template<typename T>
void RemoveComponent(Entity e) { m_Registry.Remove<T>(e); }
template<typename... Ts, typename Fn>
void Each(Fn&& fn) { m_Registry.Each<Ts...>(std::forward<Fn>(fn)); }
// ---- 常用查询 ----
// primary 相机(渲染视角来源);无则返回 nullptr
CameraComponent* GetPrimaryCamera();
private:
Registry m_Registry;
std::vector<Entity> m_PendingDestroy;
Settings m_Settings;
};
} // namespace Engine::ECS
#endif //HELLOVULKAN_SCENE_H
@@ -0,0 +1,299 @@
//
// Created by atdunbg on 2026/9/22.
//
// RenderGraph:声明式帧图内核。
//
// 设计原则(面向引擎,非 demo):
// 1. 声明与执行分离:图结构(pass + 虚拟资源)只声明一次并 Compile,
// 每帧 Execute 零图结构开销、零堆分配(barrier 列表全部编译期预算)。
// 2. 虚拟资源 + 生成号句柄:上层永远不持有裸 vulkan 句柄,
// 生命周期、layout、物理内存全部由图管理。
// 3. 物理资源池:真实 GpuImage 按 (format, extent, usage) 池化复用,
// 每个虚拟资源按帧槽位拥有 kMaxFramesInFlight 个物理实例,
// 从根上避免帧间读写竞争,同时杜绝每帧重建 GPU 资源。
// 4. 自动同步:layout/访问状态链在 Compile 期解算,产出批量
// vk::ImageMemoryBarrier2(Synchronization2),上层不再手写 barrier。
//
// 已知边界(后续演进点):
// - 暂不支持异步 compute queue / 跨队列信号量
// - 暂不支持内存别名(需 VMA)与帧历史资源(TAA 等)
// - 同一 pass 内对同一资源"读+写"(input attachment / RWM)未建模
//
#ifndef HELLOVULKAN_RENDERGRAPH_H
#define HELLOVULKAN_RENDERGRAPH_H
#include <Engine/Core/Types.h>
#include <Engine/Core/FrameContext.h>
#include <Engine/Vulkan/MemoryAllocator.h>
#include <vulkan/vulkan.hpp>
#include <functional>
#include <string>
#include <unordered_map>
#include <vector>
namespace Engine::RG {
// ---------- 句柄 ----------
// 带生成号的索引句柄:图永远不会删除条目(只追加),
// 生成号用于上层误持过期句柄时的校验,防止 use-after-free 式错误。
inline constexpr u32 kInvalidHandle = 0xFFFFFFFF;
template<typename Tag>
struct Handle {
u32 index = kInvalidHandle;
u32 generation = 0;
bool Valid() const { return index != kInvalidHandle; }
explicit operator bool() const { return Valid(); }
};
struct TextureTag;
using TextureHandle = Handle<TextureTag>;
// ---------- 资源使用方式 ----------
// 声明 pass 怎么用资源;layout/access/stage 映射和 image usage
// 都由这里推导,上层不需要(也不允许)手写。
enum class ResourceUse : u8 {
ColorAttachment,
DepthAttachment,
ShaderRead,
TransferSrc,
TransferDst,
Present, // 仅用于 swapchain 导入资源:transition 到 ePresentSrcKHR
};
// ---------- 虚拟纹理描述 ----------
struct TextureDesc {
vk::Format format = vk::Format::eUndefined;
u32 width = 0; // 0 = 跟随目标(swapchain)尺寸
u32 height = 0;
};
// ---------- 物理镜像 ----------
struct PhysicalImage {
vk::Image image{};
vk::ImageView view{};
vk::Extent2D extent{};
vk::Format format{};
vk::ImageUsageFlags usage{};
MemoryAllocator::Allocation allocation{};
bool valid = false;
};
// ---------- Pass 上下文(Execute 回调收到的) ----------
struct RenderGraphContext {
vk::CommandBuffer cmd;
FrameContext frame;
vk::Extent2D extent{};
// 按句柄取当前帧槽位的物理资源(image/view)
vk::ImageView View(TextureHandle h) const;
vk::Image Image(TextureHandle h) const;
class RenderGraph* graph = nullptr;
};
using ExecuteFn = std::function<void(RenderGraphContext&)>;
// ---------- 图内部结构 ----------
struct TextureEntry {
std::string name;
u32 index = 0; // 自身在 m_Textures 中的下标
TextureDesc desc;
bool imported = false; // swapchain 导入资源无物理实例
u32 generation = 1;
// Compile 期推导
vk::ImageUsageFlags usage{};
vk::ImageAspectFlagBits aspect = vk::ImageAspectFlagBits::eColor;
i32 firstPass = -1;
i32 lastPass = -1;
// 物理实例:每个帧槽位一个,created 资源才拥有
std::vector<PhysicalImage*> physical;
// 导入资源每帧绑定
vk::Image importedImage{};
vk::ImageView importedView{};
// 帧槽位状态:物理镜像是否已被使用过(决定 entry barrier 变体)
struct SlotState {
bool used = false;
};
std::vector<SlotState> slots;
// Compile 期状态链解算用(执行序上的"当前"状态)
vk::ImageLayout chainLayout = vk::ImageLayout::eUndefined;
vk::PipelineStageFlags2 chainStage{};
vk::AccessFlags2 chainAccess{};
bool chainWasWrite = false;
// 稳态:上一轮执行结束时(lastPass 之后)的状态。
// 同一帧槽位再次进入图时,entry barrier 从这里出发。
vk::ImageLayout restingLayout = vk::ImageLayout::eUndefined;
vk::PipelineStageFlags2 restingStage{};
vk::AccessFlags2 restingAccess{};
};
struct PassNode {
std::string name;
struct Use {
u32 resIndex;
ResourceUse use;
vk::ShaderStageFlags readStage; // ShaderRead 时的消费 stage
};
std::vector<Use> uses;
ExecuteFn exec;
bool sideEffect = false; // 无读写但必须执行(compute 写 buffer 等)
bool culled = false;
// Compile 期预算好的 barrier:每个资源两个变体
// steady = 该槽位已被使用过(从 resting 状态出发)
// first = 该槽位首次使用(从 Undefined 出发)
// imported 资源恒用 first(acquire 后 layout 语义上为 Undefined)
struct BarrierPair {
u32 resIndex;
vk::ImageMemoryBarrier2 steady;
vk::ImageMemoryBarrier2 first;
};
std::vector<BarrierPair> barriers;
};
// ---------- RenderGraph ----------
class RenderGraph {
public:
RenderGraph(Device& device, MemoryAllocator& allocator, u32 framesInFlight);
~RenderGraph();
RenderGraph(const RenderGraph&) = delete;
RenderGraph& operator=(const RenderGraph&) = delete;
// ---- 声明阶段(AddPass 之后不得再声明资源;全部声明完成后 Compile)----
// 声明一张图拥有的瞬时纹理(此时不分配任何 GPU 内存)
TextureHandle CreateTexture(std::string name, const TextureDesc& desc);
// 导入 swapchain 颜色附件(每帧通过 SetImportedImage 绑定物理 image)
TextureHandle ImportSwapchain(std::string name, vk::Format format);
// 每帧 acquire 后调用,把当帧 swapchain image 绑到导入资源
void SetImportedImage(TextureHandle h, vk::Image image, vk::ImageView view);
// ---- Pass 声明 ----
class PassBuilder {
public:
PassBuilder& Write(TextureHandle h, ResourceUse use);
PassBuilder& Read(TextureHandle h, ResourceUse use,
vk::ShaderStageFlags stage = vk::ShaderStageFlagBits::eFragment);
PassBuilder& SideEffect();
private:
friend class RenderGraph;
PassBuilder(RenderGraph& graph, u32 passIndex)
: m_Graph(graph), m_PassIndex(passIndex) {}
RenderGraph& m_Graph;
u32 m_PassIndex;
};
[[nodiscard]] PassBuilder AddPass(std::string name, ExecuteFn exec);
// ---- 编译与执行 ----
// 编译:死 pass 剔除 → 拓扑排序 → 生命周期 → barrier 预算 → 物理分配。
// 目标尺寸变化时调用 SetTargetExtent 会自动重编译并轮换物理资源。
void Compile();
void SetTargetExtent(vk::Extent2D extent);
vk::Extent2D GetTargetExtent() const { return m_TargetExtent; }
// 每帧执行:按编译好的顺序录命令。要求 cmd 处于 recording 状态。
void Execute(const FrameContext& frame);
// ---- 统计 ----
u32 GetPassCount() const;
u32 GetActivePassCount() const;
u32 GetTextureCount() const;
u32 GetBarrierCount() const;
u32 GetPooledImageCount() const;
private:
friend struct RenderGraphContext;
// 编译子步骤
void ResolveUsage(TextureEntry& entry);
void CullPasses();
void Toposort();
void ComputeLifetimes();
void PrecomputeBarriers();
void AllocatePhysical();
void ReleasePhysical();
void PurgePool(vk::Extent2D target);
PhysicalImage* AcquirePhysical(vk::Format format, vk::Extent2D extent,
vk::ImageUsageFlags usage,
vk::ImageAspectFlagBits aspect);
TextureEntry& GetEntry(TextureHandle h);
const TextureEntry& GetEntry(TextureHandle h) const;
void CheckHandle(TextureHandle h) const;
void CheckMutable() const; // 声明期校验:Compile 后禁止再改图
static vk::ImageLayout UseLayout(ResourceUse use);
static vk::PipelineStageFlags2 UseDstStage(const PassNode::Use& use);
static vk::PipelineStageFlags2 UseSrcStage(ResourceUse use);
static vk::AccessFlags2 UseDstAccess(const PassNode::Use& use);
static vk::AccessFlags2 UseSrcAccess(ResourceUse use);
Device& m_Device;
MemoryAllocator& m_Allocator;
u32 m_FramesInFlight;
bool m_Compiled = false;
std::vector<TextureEntry> m_Textures;
std::vector<PassNode> m_Passes;
u32 m_ActivePassCount = 0;
vk::Extent2D m_TargetExtent{};
// 拓扑排序后的执行序(存 pass 下标)
std::vector<u32> m_ExecutionOrder;
// 物理资源池:key = (format, extent, usage),value = 空闲镜像列表
struct PoolKey {
u32 format;
u32 width;
u32 height;
u32 usage;
bool operator==(const PoolKey& o) const {
return format == o.format && width == o.width &&
height == o.height && usage == o.usage;
}
};
struct PoolKeyHash {
size_t operator()(const PoolKey& k) const {
size_t h = std::hash<u64>{}(
(static_cast<u64>(k.format) << 32) ^ k.usage);
h ^= std::hash<u64>{}((static_cast<u64>(k.width) << 32) ^ k.height) << 1;
return h;
}
};
std::unordered_map<PoolKey, std::vector<Scope<PhysicalImage>>, PoolKeyHash> m_Pool;
// Execute 用的 barrier 暂存(成员变量,避免每帧堆分配)
std::vector<vk::ImageMemoryBarrier2> m_ScratchBarriers;
};
} // namespace Engine::RG
#endif //HELLOVULKAN_RENDERGRAPH_H
+66
View File
@@ -0,0 +1,66 @@
//
// Created by atdunbg on 2026/9/22.
//
// Camera:独立的相机模块。
// 职责:输入驱动的运动更新 + 视图/投影矩阵计算。
// 不再依赖具体渲染器——Renderer/RenderGraph 只消费它的矩阵输出。
//
#ifndef HELLOVULKAN_CAMERA_H
#define HELLOVULKAN_CAMERA_H
#include <Engine/Core/Types.h>
#include <glm/glm.hpp>
namespace Engine {
class Window;
class Camera {
public:
// 每帧调用:读取窗口输入,更新位置与朝向
void Update(Window& window, f32 deltaTime);
glm::mat4 GetView() const;
// Vulkan 投影:内部完成 Y 翻转(clip space Y 向下)和 [0,1] 深度
glm::mat4 GetProjection(f32 aspect, f32 nearZ, f32 farZ) const;
glm::vec3 GetPosition() const { return m_Position; }
void SetPosition(const glm::vec3& pos) { m_Position = pos; }
void SetYaw(f32 yaw) { m_Yaw = yaw; }
void SetPitch(f32 pitch) { m_Pitch = pitch; }
f32 GetYaw() const { return m_Yaw; }
f32 GetPitch() const { return m_Pitch; }
void SetMoveSpeed(f32 speed) { m_MoveSpeed = speed; }
void SetMouseSensitivity(f32 sens) { m_MouseSensitivity = sens; }
// 鼠标视角默认关闭(需要先做 cursor capture 再打开)
void SetMouseLookEnabled(bool enabled) { m_MouseLookEnabled = enabled; }
bool IsMouseLookEnabled() const { return m_MouseLookEnabled; }
glm::vec3 GetForward() const { return m_Forward; }
glm::vec3 GetRight() const { return m_Right; }
glm::vec3 GetUp() const { return m_Up; }
private:
void UpdateBasis();
glm::vec3 m_Position{0.0f, 0.0f, 7.0f};
f32 m_Yaw = -90.0f; // 朝向 -Z
f32 m_Pitch = 0.0f;
f32 m_MoveSpeed = 2.5f;
f32 m_MouseSensitivity = 0.1f;
bool m_MouseLookEnabled = false;
// 缓存的方向基向量,Update 时刷新,避免每帧三角函数重复计算
glm::vec3 m_Forward{0.0f, 0.0f, -1.0f};
glm::vec3 m_Right{1.0f, 0.0f, 0.0f};
glm::vec3 m_Up{0.0f, 1.0f, 0.0f};
};
} // namespace Engine
#endif //HELLOVULKAN_CAMERA_H
+35 -37
View File
@@ -8,14 +8,13 @@
#pragma once
#include <Engine/Core/Types.h>
#include <Engine/Core/FrameContext.h>
#include <Engine/RenderGraph/RenderGraph.h>
#include <vulkan/vulkan.hpp>
#include <chrono>
#include <vector>
#include "Engine/Vulkan/Texture.h"
#include <glm/glm.hpp>
#include "Model.h"
namespace Engine {
class Window;
@@ -24,16 +23,20 @@ namespace Engine {
class DescriptorManager;
class ShaderManager;
class Swapchain;
class CommandManager;
class SyncManager;
class Buffer;
class CommandManager;
class UniformBuffer;
class Model;
struct Object {
glm::mat4 transform = glm::mat4(1.0f);
Model* model = nullptr;
};
namespace ECS { class Scene; }
namespace Assets { class AssetManager; }
// Renderer 职责(收敛后):
// 1. 装配底层模块(Device/Swapchain/Sync/Command/Descriptor/Shader)
// 2. 声明并驱动 RenderGraph(pass 结构在这里组装)
// 3. Render(Scene&, AssetManager&):从 ECS 场景读取相机/灯光/渲染体,
// 经资产层解析句柄后绘制
// 不拥有场景,不加载资产(资产归 AssetManager,磁盘 IO 与这里无关)。
class Renderer {
public:
Renderer(Window& window);
@@ -42,15 +45,21 @@ namespace Engine {
Renderer(const Renderer&) = delete;
Renderer& operator=(const Renderer&) = delete;
void Render();
void Render(ECS::Scene& scene, Assets::AssetManager& assets);
// ---- 组合根(Application)装配其他模块时需要的访问器 ----
Device& GetDevice() const { return *m_Device; }
MemoryAllocator& GetAllocator() const { return *m_Allocator; }
DescriptorManager& GetDescriptorManager() const { return *m_DescriptorManager; }
vk::DescriptorSetLayout GetTextureSetLayout() const { return m_TextureSetLayout; }
private:
void RecordCommands(vk::CommandBuffer cmd, u32 imageIndex);
void TransitionImageLayout(vk::CommandBuffer cmd, vk::Image image,
vk::ImageLayout oldLayout, vk::ImageLayout newLayout);
void BuildGraph(); // 声明 pass 结构 + 首次 Compile
void HandleResize();
Window& m_Window;
// ---- 底层模块(析构序:后声明者先析构,RG 在 Device/Allocator 之前)----
Scope<Device> m_Device;
Scope<MemoryAllocator> m_Allocator;
Scope<DescriptorManager> m_DescriptorManager;
@@ -58,38 +67,27 @@ namespace Engine {
Scope<Swapchain> m_Swapchain;
Scope<SyncManager> m_SyncManager;
Scope<CommandManager> m_CommandManager;
Scope<Model> m_Model;
std::vector<Object> m_Objects;
Scope<UniformBuffer> m_UniformBuffer;
Scope<RG::RenderGraph> m_RenderGraph;
// ---- 管线装配(shader object 方案,无 vk::Pipeline)----
vk::PipelineLayout m_PipelineLayout;
vk::DescriptorSetLayout m_UBOSetLayout;
vk::DescriptorSet m_UBODescriptorSet;
vk::DescriptorSetLayout m_TextureSetLayout;
std::vector<vk::DescriptorSet> m_UBODescriptorSets; // 每帧槽位一个
vk::ShaderEXT m_VertShader{};
vk::ShaderEXT m_FragShader{};
// ---- RenderGraph 资源句柄 ----
RG::TextureHandle m_Backbuffer{};
RG::TextureHandle m_Depth{};
vk::ShaderEXT m_VertShader;
vk::ShaderEXT m_FragShader;
// ---- 当帧绑定(Render() 期间有效,供 pass 回调读取)----
ECS::Scene* m_BoundScene = nullptr;
Assets::AssetManager* m_BoundAssets = nullptr;
vk::Extent2D m_Extent;
vk::Format m_Format;
static constexpr u32 kMaxFramesInFlight = 2;
vk::Extent2D m_Extent{};
u32 m_CurrentFrame = 0;
// 相机
glm::vec3 m_CameraPos = glm::vec3(0.0f, 0.0f, 7.0f);
f32 m_CameraYaw = -90.0f; // 朝向 -Z
f32 m_CameraPitch = 0.0f;
f32 m_CameraSpeed = 2.5f;
f32 m_MouseSensitivity = 0.1f;
f32 m_DeltaTime = 0.016f;
f32 m_LastFrameTime = 0.0f;
std::chrono::steady_clock::time_point m_StartTime;
};
} // namespace Engine
+14 -6
View File
@@ -5,32 +5,40 @@
#ifndef HELLOVULKAN_COMMANDMANAGER_H
#define HELLOVULKAN_COMMANDMANAGER_H
#include <Engine/Core/Types.h>
#include <Engine/Core/FrameContext.h>
#include <vulkan/vulkan.hpp>
#include <vector>
namespace Engine {
class Device;
// 每个帧槽位独立命令缓冲。
//
// 关键正确性约束:kMaxFramesInFlight 帧并发时,第 N 帧录制时第 N-1 帧的
// 命令缓冲可能仍在 GPU 上执行,因此绝不能共享/复用同一个 command buffer。
// 每个 frameSlot 一个 buffer,reset 只发生在"该槽位 fence 已确认完成"之后。
class CommandManager {
public:
CommandManager(Device& device);
CommandManager(Device& device, u32 frameCount);
~CommandManager();
CommandManager(const CommandManager&) = delete;
CommandManager& operator=(const CommandManager&) = delete;
// 开始录制
vk::CommandBuffer BeginFrame();
// 开始录制指定帧槽位的命令缓冲
vk::CommandBuffer BeginFrame(u32 frameSlot);
// 结束录制
void EndFrame();
void EndFrame(u32 frameSlot);
vk::CommandBuffer GetBuffer() const { return m_Buffer; }
vk::CommandBuffer GetBuffer(u32 frameSlot) const { return m_Buffers[frameSlot]; }
private:
Device& m_Device;
vk::CommandPool m_Pool;
vk::CommandBuffer m_Buffer;
std::vector<vk::CommandBuffer> m_Buffers;
};
} // namespace Engine
+5
View File
@@ -34,6 +34,11 @@ public:
uint32_t FindMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags props) const;
// 在候选列表中找到第一个满足 tiling+features 的格式,找不到则 FATAL
vk::Format FindSupportedFormat(const std::vector<vk::Format>& candidates,
vk::ImageTiling tiling,
vk::FormatFeatureFlags features) const;
// 1.4 特性查询
bool IsDynamicRenderingSupported() const { return m_DynamicRenderingSupported; }
bool IsShaderObjectSupported() const { return m_ShaderObjectSupported; }
+2 -13
View File
@@ -33,10 +33,8 @@ namespace Engine
const std::vector<vk::Image>& GetImages() const { return m_Images; }
const std::vector<vk::ImageView>& GetImageViews() const { return m_ImageViews; }
vk::ImageView GetDepthImageView() const { return m_DepthImageView; }
vk::Format GetDepthFormat() const { return m_DepthFormat; }
vk::Image GetDepthImage() const { return m_DepthImage; }
// 深度图不在 Swapchain 管理——渲染目标(含深度)统一由 RenderGraph 创建。
// Swapchain 只负责"呈现"这一件事。
void Recreate(Window& window);
private:
@@ -47,10 +45,6 @@ namespace Engine
vk::PresentModeKHR ChoosePresentMode(const std::vector<vk::PresentModeKHR>& modes);
vk::Extent2D ChooseExtent(const vk::SurfaceCapabilitiesKHR& caps, Window& window);
void CreateDepthResources();
void DestroyDepthResources();
vk::Format FindDepthFormat();
Device& m_Device;
@@ -58,11 +52,6 @@ namespace Engine
std::vector<vk::Image> m_Images;
std::vector<vk::ImageView> m_ImageViews;
vk::Image m_DepthImage;
vk::DeviceMemory m_DepthMemory;
vk::ImageView m_DepthImageView;
vk::Format m_DepthFormat;
MemoryAllocator& m_Allocator;
vk::Format m_Format;
+17 -8
View File
@@ -5,33 +5,42 @@
#ifndef HELLOVULKAN_UNIFORMBUFFER_H
#define HELLOVULKAN_UNIFORMBUFFER_H
#include <Engine/Core/Types.h>
#include <Engine/Core/FrameContext.h>
#include <Engine/Vulkan/Buffer.h>
#include <vulkan/vulkan.hpp>
#include <vector>
namespace Engine {
class Device;
class MemoryAllocator;
// Per-frame UBO ring:每个帧槽位一个独立 buffer。
//
// 正确性:CPU 在第 N 帧写 UBO 时,GPU 可能仍在读取第 N-1 帧的同槽位数据。
// 共享单个 buffer 会产生数据竞争;ring 化后每槽位的写入严格发生在
// 该槽位 fence 确认完成之后。
// 后续演进方向:单个大 ring buffer + 动态 offset(省描述符、省内存),
// 接口已按"写入哪个槽位"设计,届时只是内部实现替换。
class UniformBuffer {
public:
UniformBuffer(Device& device, MemoryAllocator& allocator, u64 size);
~UniformBuffer();
UniformBuffer(Device& device, MemoryAllocator& allocator,
u64 size, u32 frameCount);
~UniformBuffer() = default;
UniformBuffer(const UniformBuffer&) = delete;
UniformBuffer& operator=(const UniformBuffer&) = delete;
vk::Buffer Get() const { return m_Buffer->Get(); }
vk::Buffer Get(u32 frameSlot) const { return m_Buffers[frameSlot]->Get(); }
u64 GetSize() const { return m_Size; }
// 每帧更新数据
void Update(const void* data, u64 size);
// 写入指定帧槽位的 UBO(仅在该槽位 fence 确认完成后调用)
void Update(u32 frameSlot, const void* data, u64 size);
private:
Scope<Buffer> m_Buffer;
u64 m_Size;
std::vector<Scope<Buffer>> m_Buffers;
u64 m_Size = 0;
};
} // namespace Engine