当前位置: 首页 > news >正文

资源管理模块的实践开发日志

一、从图到代码

上篇我把资源管理模块的设计思路理了一遍:全局单例、五个状态的帧状态机、用哈希做纹理弱引用。那会儿觉得自己想得挺明白的,真坐到电脑前开始写第一行 `std::mutex` 的时候才知道,想明白和写出来之间隔了起码十个坑。

这篇记录的,就是我把那些图变成代码的过程,以及中间碰到的各种意料之外的问题。

二、搭骨架:单例和状态机

先从最简单的开始。全局单例没什么好说的,Meyer's Singleton 几行写完:


class ResourceStateManager {
public:
static ResourceStateManager& Get() {
static ResourceStateManager instance;
return instance;
}

// 禁止拷贝
ResourceStateManager(const ResourceStateManager&) = delete;
ResourceStateManager& operator=(const ResourceStateManager&) = delete;

private:
ResourceStateManager() = default;

std::mutex mtx;
FrameState state = FrameState::IDLE;
std::unordered_map<TextureSlot, TrackedTexture> textures;
};

`Get()` 就是全局入口,任何一个钩子函数里都能直接 `ResourceStateManager::Get()` 拿到实例。这步很顺利。

状态机的五个状态我用了一个简单的枚举:


enum class FrameState {
IDLE,
WAITING_RESOURCES,
READY,
PROCESSING,
FRAME_END
};

状态流转的逻辑我一开始写得很粗暴——在每个资源注册函数里硬改状态。比如收到深度缓冲的时候,如果当前是 `IDLE`,就切到 `WAITING_RESOURCES`;所有必须纹理到齐了,就切到 `READY`。当时觉得没啥问题,测了两款游戏也确实没崩。

但第三款游戏就翻了。

测的是《地狱之刃:塞娜的献祭》,一款虚幻引擎的游戏。它的渲染管线里深度缓冲会创建两次——一次在主场景,一次在UI层。我的代码在收到第二份深度缓冲时,状态已经是 `WAITING_RESOURCES` 了,然后纹理映射表里 `TextureSlot::DEPTH` 被更新成了第二份深度缓冲。可这第二份是UI层的,分辨率比场景层低。等运动矢量到齐、状态切到 `READY`、上采样真正执行的时候,深度缓冲是UI层的分辨率,FSR 后端直接抛了异常。

这个问题让我意识到,纹理追踪不能只看“有没有”,还得看“对不对”。如果同一个槽位被多次注册,到底该保留第一份还是第二份,不能一概而论。

后来我在纹理注册函数里加了一层校验,拿到新纹理的时候和已有的比较一下分辨率,取大的那个,并且打一条日志。这也不是什么优雅方案,但目前够用:


void ResourceStateManager::RegisterTexture(TextureSlot slot,
ID3D12Resource* resource,
const D3D12_RESOURCE_DESC& desc) {
std::lock_guard<std::mutex> lock(mtx);

if (state == FrameState::IDLE) {
state = FrameState::WAITING_RESOURCES;
}

auto it = textures.find(slot);
if (it != textures.end()) {
// 同一帧里这个槽位已经有纹理了,取分辨率大的
const auto& oldDesc = it->second.desc;
if (desc.Width <= oldDesc.Width && desc.Height <= oldDesc.Height) {
// 新的没大的大,忽略
return;
}
Log("Slot %d overwritten: %dx%d -> %dx%d",
(int)slot, oldDesc.Width, oldDesc.Height, desc.Width, desc.Height);
}

TrackedTexture tracked;
tracked.resource = resource;
tracked.desc = desc;
tracked.frameNumber = currentFrame;
textures[slot] = tracked;

CheckReady();
}

`CheckReady()` 会遍历必须纹理是否到齐,到了就把状态拉成 `READY`。这个小函数后面还会出问题,先留着。

三、帧边界清理的坑

上篇里我提到,参考项目的帧结束清理依赖手动调 `Reset()`,在某些场景下会被跳过。我在自己代码里试着把这个清理动作绑到 Present 钩子上,思路是每次交换链前面都把上一帧的东西清干净。


void ResourceStateManager::OnPresent() {
std::lock_guard<std::mutex> lock(mtx);

if (state == FrameState::READY ||
state == FrameState::PROCESSING ||
state == FrameState::FRAME_END) {
// 清理上一帧的残留
textures.clear();
state = FrameState::IDLE;
}
}

写完之后用《巫师3》测,正常跑没问题。但一进游戏内菜单再退出来,挂了。原因是《巫师3》的菜单界面进了一个新的渲染路径,这期间没有正常的 Present 调用,但我的 Manager 里还挂着上一帧的纹理引用。等菜单退出、场景恢复渲染时,游戏引擎重新创建了一轮纹理,可我的哈希表里还残留着旧的记录,于是 `CheckReady()` 早早判断满足条件——拿着已经失效的旧纹理指针去提交上采样,崩得渣都不剩。

修法也简单,不只依赖 Present 清理,还要加一个场景切换时的检查:如果连续几帧没收新纹理,就主动清空。这个我还没写得很完善,目前暂时用了一个简单的帧间隔计时器来兜底。后面得专门抽时间搞健壮。

四、反应掩膜缺失——弹性适配的第一次实战

DX12 路径下用 FSR 3.1 后端,需要反应掩膜(Reactive Mask)。这东西在正常游戏视角下基本都有,但有些游戏的过场动画、地图界面就不生成。

我最初的 `CheckReady()` 是这样的:


void ResourceStateManager::CheckReady() {
if (textures.count(TextureSlot::COLOR) == 0) return;
if (textures.count(TextureSlot::DEPTH) == 0) return;
if (textures.count(TextureSlot::MOTION_VECTORS) == 0) return;
if (textures.count(TextureSlot::REACTIVE_MASK) == 0) return; // 这里卡死了

state = FrameState::READY;
}

《巫师3》自由探索时一切正常,但一打开地图界面,`REACTIVE_MASK` 永远不会来,状态永远卡在 `WAITING_RESOURCES`,上采样被无限期跳过。画面倒是没崩,就是画质突然掉回原生低分辨率,观感瞬间变差。

我把反应掩膜标记为“可选资源”,逻辑改成:如果到上采样提交时还没收到,就生成一张 1×1 的默认纹理顶上。


ID3D12Resource* ResourceStateManager::GetOrCreatePlaceholder(TextureSlot slot) {
if (textures.count(slot)) {
return textures[slot].resource;
}

// 只有可选资源才允许用占位纹理
if (slot == TextureSlot::REACTIVE_MASK || slot == TextureSlot::COMPOSITION_MASK) {
if (!placeholderMask) {
Create1x1Texture(DXGI_FORMAT_R8_UNORM, &placeholderMask);
}
return placeholderMask;
}

return nullptr; // 必须资源没到,调用者自己处理
}

`Create1x1Texture` 第一次被需要的时候建一张 1×1 的 R8 纹理,后面一直复用。这玩意儿对画质肯定有影响——反应掩膜全零意味着 FSR 对画面所有区域一视同仁,UI 边缘可能会有轻微晃动——但比直接退回到原生低分辨率强太多了。

这个改动做完,地图界面打开再关闭的过渡顺滑了很多。

五、当前进展和真实感受

到这个周末为止,Resource & State Manager 模块的 DX12 路径已经跑通了几个基础场景。FSR 3.1 和 XeSS 后端都能正常拿到资源,我也在《巫师3》和《地狱之刃》里反复进菜单、切场景去折腾它。目前的状态:

做了的事状态
单例结构 + 锁跑通了,性能还没压测
帧状态机基本流转大部分情况正常,场景切换的清理还要加强
纹理哈希追踪 + 重复注册处理暂时能用了,但逻辑写死取大分辨率有点粗暴
必须 / 可选资源拆分反应掩膜和合成掩膜已改为可选,用了占位纹理
Present 钩子自动清理写好了,但需要配合场景切换兜底
DLSS 输入适配刚开始看接口,还没动手
Vulkan 路径没碰

最大的感受:中间件级别的代码,容错设计比功能实现要花更多时间。写功能的时候思路是“拿到 A 就做 B”,很顺畅;写容错的时候思路是“拿不到 A 怎么办、拿到两个 A 怎么办、拿到 A 但 A 是上一帧的怎么办”——脑子要转好几个弯。

另外,DX12 的资源命名和调试工具帮了大忙。PIX 里可以直接看纹理的生命周期和引用关系,没它的话上面那个深度缓冲被覆盖的 bug 我可能得摸好几天。

六、接下来

这周把骨架搭起来了,也踩了几个不致命但能学东西的坑。下面几周打算:

把场景切换的清理策略写完,别再让残留纹理害我崩程序
补上 DLSS 作为输出时的资源映射,把“后端无关输入包”这个抽象层再推进一步
拿更多不同类型游戏测试,尤其是那些渲染管线比较特殊的(比如用延迟渲染做 UI 的)
如果有余力,开始看 Vulkan 的资源追踪跟 DX12 差在哪

写代码的时候一旦发现自己在凭想象做假设,十有八九后面会出问题。还是那句话,跑起来再说话。

http://www.jsqmd.com/news/767197/

相关文章:

  • 从命令行工具到API服务:构建安全高效的智能体能力网关
  • UE4SS完整指南:5步掌握虚幻引擎游戏修改与脚本开发
  • TMS320DM642到DM648/DM6437 DSP软件迁移指南
  • LocalAI:开源本地大模型推理服务器,兼容OpenAI API的私有化部署方案
  • Godot引擎与Rust结合:gdext项目实战指南
  • “RAMageddon“席卷全球:廉价手机与笔记本电脑的时代已走到尽头?
  • AI多智能体协作开发:构建自动化软件团队的架构与实践
  • 【Docker 27跨架构构建终极指南】:27个生产级镜像构建案例,覆盖ARM64/AMD64/PPC64LE全场景,错过再等一年!
  • BilibiliDown:三分钟掌握B站视频下载的终极指南
  • 前端工程化:CI/CD最佳实践
  • Arm Cortex-R82 PMU架构与性能监控实战指南
  • BLDC电机无传感器控制技术与反电动势信号处理
  • 85.YOLOv8完整可运行代码,从数据准备到结果可视化,一步到位
  • Python资源管理库resourcelib:基于上下文管理器的声明式依赖注入实践
  • Vision Transformer非平滑组件原理与优化实践
  • 番茄小说下载器:5个步骤打造你的个人数字图书馆 [特殊字符]
  • Java 云原生开发中的服务发现:实现微服务架构的关键
  • 2026年哪款充电宝性价比高?充电宝性价比最高的十大品牌推荐!
  • 从订阅者到消费者:移动通信网络的架构演进
  • OpenClaw智能体集群会话清理工具swarm-janitor设计与实践
  • 5个步骤掌握TranslucentTB:Windows任务栏透明化的终极解决方案
  • 从账单明细看Taotoken按Token计费模式的实际开销
  • 高效解决Linux Wi-Fi 6连接问题:Realtek 8852AE驱动完整部署实战指南
  • AI面试必杀技:3分钟搞懂RAG/Agentic Search/Deep Research如何分层,面试官抢着要!
  • PotPlayer字幕翻译插件终极指南:免费实现外语视频实时翻译
  • IDEA 删除一行快捷键
  • Cursor编辑器MCP插件一键安装工具:cursor-mcp-installer使用指南
  • Rust实现Bard API客户端:类型安全与异步编程实践
  • 为自动化脚本Agent配置Taotoken作为统一模型供应商的实践
  • 终极指南:如何用Reloaded-II轻松管理游戏模组,告别复杂安装流程