资源管理模块的实践开发日志
一、从规划到代码
上篇我把资源管理模块的设计理顺了。这周框架和模块都定下来了,技术底座是 Vulkan + Proxy DLL + procaddr 拦截。我负责的 Resource & State Manager,需要跟踪的对象变成了 VkDevice、VkQueue、VkSwapchainKHR、VkImage,以及 present 时那个关键的 imageIndex。
资源管理不是“把所有对象都管起来”,而是从注入点反推需要什么。present 前我们要做的事情很明确——根据 swapchain + imageIndex 找到目标图,有能提交命令的 queue 和 command buffer,有串联同步用的 semaphore。这三个需求定下来,模块的边界就清楚了。
二、搭骨架:单例 + 状态机
单例结构还是 Meyer's Singleton。之前第三篇里画过类图,这里不再重复。状态机五个状态(IDLE → WAITING_RESOURCES → READY → PROCESSING → FRAME_END)也保持不变。
不过这周开始写实际的 Vulkan 对象追踪,类里面多了些实在的东西:
class ResourceStateManager {
public:
static ResourceStateManager& Get() {
static ResourceStateManager instance;
return instance;
}
// swapchain 注册与查询
void RegisterSwapchain(VkSwapchainKHR swapchain,
const std::vector<VkImage>& images,
VkFormat format, VkExtent2D extent,
VkImageUsageFlags usage);
VkImage GetPresentImage(VkSwapchainKHR swapchain, uint32_t imageIndex);
// queue 注册
void RegisterQueue(VkQueue queue, uint32_t familyIndex, VkDevice device);
QueueData* GetQueueData(VkQueue queue);
// device ↔ physical device 映射
void MapPhysicalDevice(VkDevice device, VkPhysicalDevice physical);
VkPhysicalDevice GetPhysicalDevice(VkDevice device);
bool CanUseTransfer(VkSwapchainKHR swapchain);
void OnFrameEnd();
private:
std::mutex mtx;
FrameState state = FrameState::IDLE;
struct SwapchainData {
std::vector<VkImage> images;
VkFormat format;
VkExtent2D extent;
VkImageUsageFlags usage;
// 阶段 2 的中间图,后面会写到
VkImage downscaleImage = VK_NULL_HANDLE;
};
struct QueueData {
uint32_t familyIndex;
VkDevice device;
VkCommandPool commandPool;
VkCommandBuffer commandBuffer;
VkSemaphore doneSemaphore;
};
std::unordered_map<VkSwapchainKHR, SwapchainData> swapchains;
std::unordered_map<VkQueue, QueueData> queues;
std::unordered_map<VkDevice, VkPhysicalDevice> deviceToPhysical;
};
三、到底要追踪哪些对象——从注入点反推
写完骨架之后停了一下,跟框架同学对了对“最小跟踪”的边界。文档上写得很直白:不是所有 Vulkan 对象都归 Manager 管,只跟踪注入所必需的。从 `vkQueuePresentKHR` 这个注入点反推,需要的东西列出来就是这样:
Swapchain 维度。 present 时拿到的是 swapchain + imageIndex,不存下 swapchain 对应的 image 数组,根本不知道要往哪张图上写。所以 `vkCreateSwapchainKHR` 和 `vkGetSwapchainImagesKHR` 两个 hook 都要拦,前者记 format/extent/usage,后者把 image 数组存下来。
Queue 维度。 注入意味着在 present 前额外提交一次 `vkQueueSubmit`。要有 queue 可提交、有 command pool 可分配 command buffer、有 semaphore 去串同步链。这里面藏着一个容易漏的点:创建 command pool 必须填 `queueFamilyIndex`,而 `queueFamilyIndex` 只在 `vkGetDeviceQueue` 的参数里出现。如果没 hook `vkGetDeviceQueue`,这个值就丢了,command pool 就建不起来。
我当时就是在没拦 `vkGetDeviceQueue` 的情况下先写了 command pool 创建,结果 `familyIndex` 永远是 0,换了一台机器直接炸。加上 hook 之后才正常。
在 `vkGetDeviceQueue` 里把 familyIndex 绑定到 queue 上:
void ResourceStateManager::RegisterQueue(VkQueue queue, uint32_t familyIndex, VkDevice device) {
std::lock_guard<std::mutex> lock(mtx);
QueueData data = {};
data.familyIndex = familyIndex;
data.device = device;
queues[queue] = data;
}
Device 维度。 主要是函数指针缓存。Vulkan 的函数指针不是全局唯一的,不同 layer、不同 extension 组合下 `vkGetDeviceProcAddr` 返回的可能不一样。如果每帧都做字符串查找去拿 `vkCmdBlitImage`,开销不小。缓存一次,后续直接用。
还有就是 physical device 的映射——创建中间纹理(阶段 2 的 downscale image)需要分配设备内存,`vkAllocateMemory` 需要选 `memoryTypeIndex`,而候选集合要从 `VkPhysicalDeviceMemoryProperties` 拿。所以在 `vkCreateDevice` 成功时记下 physical device:
if (r == VK_SUCCESS && device && *device) {
deviceToPhysical[*device] = physicalDevice;
}
这堆东西列下来,回头再看“最小跟踪”四个字,才算真明白什么意思——不是做减法做到简陋,是先把注入要用的路径画出来,路径上缺什么就补什么,路径外的一概不管。
四、锁的粒度问题
单例加锁一开始我是进任何函数都 `scoped_lock`,图省事。按照模块文档的建议,“在锁内只做查表与复制句柄,录制放到锁外”,我对 present 路径的加锁方式做了调整。
这个改动是因为 present hook 跑在每帧的高频路径上。大锁里如果包含 command buffer 录制甚至 `vkQueueSubmit`,GPU 那边的时间是不确定的。万一应用在另一个线程里做 swapchain 重建,就会一直等在锁外面,画面直接卡死。
改法是把查询和录制拆开:
SwapchainSnapshot snap{};
{
std::scoped_lock lock(mtx);
auto& sc = swapchains[swapchain];
snap.image = sc.images[imageIndex];
snap.extent = sc.extent;
snap.format = sc.format;
snap.usage = sc.usage;
}
// 锁外:barrier / blit / submit
InjectionLayer::RecordAndSubmit(cmdBuf, snap);
锁里只做查表 + 复制必要句柄,几十个 CPU 周期的事。录制 command buffer、提交、等 fence 全部放锁外。改了之后卡顿感明显没了,但得额外确保 `SwapchainSnapshot` 里复制的 VkImage 句柄在录制期间不会被销毁——这个由 swapchain 销毁 hook 里清空 images 数组之前等所有提交结束来保证。
五、帧边界清理的坑
进度非常快就遇到了第一个 bug。
我在 `vkCreateSwapchainKHR` 的 hook 里调用 `RegisterSwapchain`,把返回的 images 存下来。然后在 `vkQueuePresentKHR` 的 hook 里用 imageIndex 去查。跑了几次 Vulkan 官方示例,偶尔崩在 image 访问上,debug 打了半天日志才发现问题。
有些 Vulkan 应用在窗口 resize 时会重建 swapchain——先 `vkDestroySwapchainKHR` 把旧的销毁,再创建一个新的。但我的 Manager 里没有处理 swapchain 销毁的通知。应用自己已经把那组 VkImage 回收了,我的 map 里还存着旧指针。等下下次 present 时(如果 resize 过程中正好有残留调用),拿到的 imageIndex 可能命中旧的 vector,里面的 VkImage 已经失效了。
修法是在 `vkDestroySwapchainKHR` 的 hook 里加清理:
void ResourceStateManager::UnregisterSwapchain(VkSwapchainKHR swapchain) {
std::lock_guard<std::mutex> lock(mtx);
auto it = swapchains.find(swapchain);
if (it != swapchains.end()) {
Log("Swapchain %p destroyed, %zu images unreferenced",
swapchain, it->second.images.size());
swapchains.erase(it);
}
}
不是什么复杂的修法,但就是这类"应用自己做了清理、我没跟上"的同步问题,最容易出奇怪的偶发崩溃。`vkDestroySwapchainKHR` 里主动把他相关的 image views、framebuffers、render pass、downscale image 全释放掉。这一步不做,窗口一 resize 就崩。
六、为什么不做完整 layout tracking
写到资源生命周期这块,我确实冒出过一个念头:要不要把 image layout 的状态机也完整追踪起来?
反正 Vulkan 的 barrier 本来就要源布局和目标布局,如果我能知道当前 image 到底在什么 layout 下,就可以只转必要的那一步,而不是每次都“转过去再转回来”。
但实际翻了翻 Vulkan spec,很快就放弃了。同个 image 可能在多个 queue、多个 command buffer、多个 render pass 里被反复转 layout,甚至 external memory 和平台扩展都会影响 layout 状态机。要在 MVP 阶段做到“完整追踪”,代码量不会比整个注入层少。
而且项目文档里一句话说得很对:半吊子 tracking 比强制转换更危险。 layout 误判会导致 barrier 写错,错误极难复现,在 RenderDoc 里看也可能只是一帧闪烁,根本排查不到。
所以现在用的是“强制转换”策略:在 present 前先 barrier 转到 `TRANSFER_DST`(或 `COLOR_ATTACHMENT`),注入完成后 barrier 转回 `PRESENT_SRC_KHR`。确实有点笨,但行为是确定的,validation layer 能验,出问题也容易定位。后面如果上 compute/FSR2,再考虑要不要扩 layout 追踪的范围。
七、当前状态
Resource & State Manager 模块在 Vulkan 路径下已经稳定跑了几个官方示例:
| 做了的事 | 状态 |
|---|---|
| 单例 + 状态机 + 锁粒度优化 | 跑通 |
| Swapchain / Image 注册与销毁清理 | 稳定 |
| PhysicalDevice 映射 | 已加 |
| QueueFamilyIndex 追踪 | 已补,command pool 创建正常 |
| Transfer usage 检测与回退 | 几个官方示例测过 |
| 锁粒度优化(查表复制、录制放锁外) | 完成 |
| 中间纹理(downscale image) | 设计已明确,代码跟进中 |
| Layout 强制转换 | 已加,验证层通过 |
| 多 swapchain 同时 present | 没碰 |
八、体会
这周写下来,最大的感受是 Vulkan 的显式控制让中间件好写也难写。好写在状态都是看得见的——barrier、layout、usage 全在明面上;难写在必须自己处理同步,漏一个 semaphore 或者 barrier 就崩。
九、接下来
downscale image 的创建和 resize 重建,这周把代码补上
继续拿更多 Vulkan 示例跑,尤其是有复杂渲染管线的
开始看 compute shader 做缩放的基础设施——descriptor set 管理、pipeline 缓存这些
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)