[全球首发] 完全破解亚楠显存管理机制,底层彻底重构,完美解决爆显存带来的一系列问题
SossViewer
编辑于 2026年10月12日 20:14

发这个文章是因为太兴奋太有成就感了,纯记录开发过程和技术分享,还带一点炫耀。攻破了一个顽固问题。

需求版本的同学还得再等一等,还有点优化要做,有bug要修,还请耐心等待。

以下内容均为本人在AI的辅助下,进行大量轮次的实验验证和内存堆栈分析(ai神力,放在以前这根本不是人力短时间搞得定的)得出的原创结论,最终我设计了一条完美的显存回收世界线。

为什么 PS4 版《BB》在 PC 上越玩越吃显存?

—— 彻底重构显存管理模块,从「撞墙才救火」改成「提前持续释放」

有人 20 分钟从 2.7G 一路涨到 7.5G,最后不是崩溃就是卡成幻灯片。这不是 bug,这是模拟器的架构决定的。今天我来把它拆了,给大家深入讲解这个久攻不下的问题原理。

一、先说清楚 PS4 版是怎么用显存的

PS4 有 8GB 显存,但游戏里那块 8G 不是给你一个人用的——系统菜单、别的游戏、录制都在里面。真正留给游戏的,大概 6G 出头。

更关键的一点:PS4 的游戏内存是「虚拟」的。游戏以为自己有 8G,其实是操作系统给它开的一个地址空间。真正的数据在硬盘上(PS4 本来就是机械硬盘),显存里只放最近正在用的那一部分。

所以血源的原始设计是这样的:

这套设计在真机上跑得很好,因为真机的显存就那么大,引擎从小就被逼着学会「取舍」。它甚至有一套很聪明的复用判定机制——我利用AI逆向出来后发现,血源内部确实存在「这个资源已经没人用了」的权威判定,只是不给外部调用。这个发现是后面一切的基础。

二、问题出在模拟器身上

shadPS4 是把 PS4 的图形栈翻译成 Vulkan 跑的。它的显存管理思路,是照搬 PC 游戏的老套路:

建个缓存池,装满了就按「最近没人用」扔掉一批。

在 PC 上这招很常见,因为 PC 显存大(16~24G)、游戏也没那么较劲。问题在于三个致命的不对称:

第一,它只会加,不会减。

模拟器的显存驻留表是「只增不减」的——一块内存被占用了,就永远不还回去。对比一下:PS4 上引擎会主动归还;模拟器这边连一条释放路径都没有。

第二,它等的是「内存告急」,不是「该清了」。

现在的回收逻辑挂在 GC 触发条件上(占用超过 60% 才动)。翻译成人话就是:显存越宽裕,越不清理。这跟我的目标——「提前持续释放」——正好是反的。

第三,也是最要命的:它根本不知道什么叫「死亡」。

它只有两个判断依据:这个块多久没被碰(LRU)、显存还剩多少(阈值)。而游戏自己心里清楚的事情——「这个资源我已经不要了」——它一无所知。下面这一节就是我如何利用AI把这件事挖出来的。

三、我是怎么找到「真实的死亡标记」的

  • 第一步,先推翻自己的结论。我最初判断「血源内部不存在这套信息」,理由是它的 SDK 释放层什么回调都不给。但后来转念一想,如果真的不回收,那PS4实机是怎么不爆显存的?反思之后,我怀疑我把「SDK 释放层」和「资源复用判定层」混成了一层。纠正后的事实是:血源内部确实有一套判定,只是它是内部私有的,不对外暴露,不实时释放。

  • 第二步,找到销毁分发器。既然游戏自己知道资源死了,那它内部必然有一个「资源销毁」的统一入口。我通过AI逆向分析本体的核心管理器对象,找到它的虚表第 0x70 槽——那正是资源销毁事件的分发器,地址 0x1FBEBB0。

  • 第三步,确认它真的会被调用。用断点在这个函数上蹲守,第一次成功抓到了一起真实的死亡事件,参数是一个 guest 内存地址。这验证了「它是必经之路」,不是死代码。

  • 第四步,读出信号内容。函数的第一个参数就是「刚刚作废的 guest 资源地址」,我在程序侧挂了一个轻量记录:记录这个地址,写进一个 6.5 万条的无锁环形队列,然后尾调用原函数继续执行。开销是每条事件几条CPU指令,可以忽略。

  • 第五步,验证信号质量。这一步最关键,因为它决定整个方案成不成立:

一次场景加载(传送)产生 6184 次死亡事件——量足够大,足以驱动回收

地址全部落在 guest 的直接内存区,不是野指针

地址分布与「驻留变动最活跃的区域」高度重合——也就是说,它报出来的正是我们的游戏运行时显存里那些块

结论:死亡信号是真的,而且量足够。这就是最终能能实现「精确回收」的全部依据。

四、为什么 shad 不知道怎么释放「真实的」GPU cache

知道了信号从哪来,反过来就更容易理解 shad 为什么做不到。不是它偷懒,是它缺三样东西:

  • 第一,没有入口。死亡判定是游戏内部私有的,不走任何对外接口,模拟器在 guest 外面根本看不到它。shad 只能看到「这块内存在显存里」,看不到「游戏已经不要它了」。

  • 第二,地址不是句柄。就算程序把地址硬塞给它,它也反查不出对应的是哪个纹理、哪个几何缓冲区——因为虚拟地址和模拟器的内部对象之间没有映射表。这个映射表得由我们自己去建。

  • 第三,不知道「多少算死」。同一块显存可能同时被几个资源共享,地址落在它的区间上,不代表整块都没人要了。要判断「是不是真的全死了」,需要知道这个资源和谁在共享——这个信息只有游戏自己知道。

还有一个更根本的模型差异:shad 的驻留表只增不减,它的显存管理模型里压根没有「回收」这个动作。它只有「缓存满了要腾地方」这一种被动策略。这套策略在 PC 上够用,因为 PC 显存大;对着血源这种「虚拟内存 4.7G、真实需求远小于上限」的游戏,就永远等不到「满了」那一刻。

一句话:shad 不是不想释放,是它缺一个「权威失效信号」的入口,而这个入口只存在于游戏内部。

还有一种可能,shadPS4的作者是知道这个机制的,但我想他的初衷是开发一款适用于ps4所有游戏的通用性ps4模拟器,所以只能忠实模拟ps4的内存分配机理,没办法底层只针对血源一款游戏做优化。

而bbport这个项目则能让我和其他开发者彻底打破这层桎梏,让一切代码只为一个目标运行:原生运行血源。

五、怎么用这个机制做完美管理

整套方案分成三段:信号采集 → 失效映射 → 无损回收。

第一段:采集——把信号接进来

guest 内部是单线程的游戏逻辑,模拟器的 GPU 线程是另一条线程。我让 detour 只做两件事:把地址写进环形队列、立刻返回原函数。队列满了就覆盖最老的(丢掉最旧的一批死亡事件只会让回收晚一点,不会误回收)。整个采集路径无锁、不阻塞。

第二段:映射——从地址找到显存块

这一步是技术上最难的一环。两个消费者,两条完全不同的路:

·       几何/顶点侧:我利用ai实现了一张常驻区间表(guest 地址区间 → 显存分配)。死亡地址进来后,在区间表里反查落在哪些已驻留区间内,一条死亡事件平均覆盖 15%~27% 的区间,单轮能覆盖 300~800 MiB 的已驻留显存——信号量足够驱动回收。

·       纹理侧:用纹理页表反查死亡地址落在哪些纹理上,作为「死亡候选」,再逐个过安全窗判断能不能真删。

第三段:回收——无损地把显存还回去

核心思想:数据本来就在 RAM 里(游戏本来就有一份完整的数据),显存里那份只是副本。所以还回去不丢任何东西,游戏下次要用,RAM 会自动补上来。这是整个方案「无损」的根据,也是它比传统 GC 驱逐强的地方——传统驱逐是真把数据扔了,游戏得重新读盘,所以会出现「资源不足就闪烁」。

具体的回收策略分两侧:

  • 几何/顶点侧走「arena 降级」:把已经 15 秒没被碰、且游戏没写过的那块显存解绑,归还给驱动。归还前会先排进一个延迟队列,等 64 帧(确保 GPU 不可能再寻址到它)才真正释放。

  • 纹理侧走「候选驱逐」:过了安全窗、且判定为「干净」(显存里的数据和 RAM 一致)的候选,直接释放。

回收的强度由一条连续函数决定,不是固定阈值:水位越高、每轮回收越多、越不挑冷热;水位宽裕时只收真正冷掉的。目标水位取「显存预算的一半」和「绝对上限 2 GiB」里的小者——后者保证大显存卡上不会因为比例算出一个高得没用的线。

回收的时机也很讲究。我原本设计了三种时机(进标题菜单、场景切换、定时兜底),实测下来发现:只要定时兜底已经能把占用压平,事件触发就是多余的。所以暂时只保留定时这一种,把复杂度压到最低。

 

六、预期效果

七、实测数据,半步圆满

数字先说话(RX 6750XT / 12GB,40 分钟、数十次传送):

八、还没解决的:真正的大头在别人手里

最后把显存分项做成明细后,发现了一件不太舒服的事:

总占用 4.3 GB

├─ 纹理         1.0 GB   ← 程序能管

├─ 几何/顶点     0.9 GB   ← 程序能管

├─ 固定开销     0.26 GB  ← 能管(128 MB 的地址翻译表等)

└─ 残差         2.5 GB   ← 管线缓存、描述符池、交换链、驱动内部,组成暂时不清楚

超过一半在项目自己的代码管不到的地方。这带来两个结论,对不同容量的显卡意义完全不同:

  • 一、加显存对这个问题几乎没用。残差 2.5G 在 4GB 卡和 24GB 卡上占比都是 52~55%——显存做大,只是把用不上的地方变大。

  • 二、小显存卡必须能调低显存占用上限的目标水位。4GB 卡按现在的公式,目标水位是 1.75G,加上 2.5G 残差和 0.26G 固定开销,必然不够用——而现在这个数字是编译时写死的,玩家没有任何自救手段。

所以下一步很明确:给目标水位加一个玩家可调的滑块,并测清楚那 2.5G 残差到底跟分辨率、画质是什么关系。如果它跟着分辨率走,那小显存用户的正解就不是「我们少用点」,而是「少渲染点」(这一步也可以通过实时纹理缩放实现,我尝试过,但是还没找到合理的技术路线来实现)。

九、下一步我要做什么

  既然我已经知道了,显存占用构造里的大头部分还是不明晰的,不可控的。那下一步我的工作自然就是找到他,干掉他,争取将显存占用率优化到极致。

  除此之外的,修好了显存占用率的问题,还有无数个问题在后面排着,gpu侧的巨大差异还带来了许多难以定位难以解决的bug,闪退,崩溃,卡死,兼容性问题,都需要进一步的逐个定位,任重道远。但我也会在完成我心中完美的移植之前,持续不断的学习和优化下去。

  bbport和shadps4都是非常优秀且具有开创性的项目,居功至伟。作为一个血源爱好者,也作为一个资深码农,怎么会不兴奋不想钻探呢。

  总之,总有一天血源pc版会从社区长出来的,感谢所有为血源社区的进步孜孜不倦的各位开发者和持续支持社区进步的可爱的玩家们。