打开 Godot 4 项目的动态分辨率或拖拽窗口大小,画面闪一下然后继续跑——那"闪一下"的底下是一整套 GPU 资源的销毁重建。管这件事的是 servers/rendering/renderer_rd/storage_rd/render_scene_buffers_rd.h 的 RenderSceneBuffersRD:每个 3D 视口(含 XR 的每个眼睛)一份实例,持有本视口所有中间渲染目标。这篇拆它管理的资源清单、重建触发链,以及为什么这块代码刻意不做增量更新。
管什么:一张 RID 清单
RenderSceneBuffersRD 的成员就是一张"本视口渲染用到的全部中间纹理"清单(字段名以 4.x 为准,各小版本有增删):
// servers/rendering/renderer_rd/storage_rd/render_scene_buffers_rd.h(4.x,节选)
class RenderSceneBuffersRD : public RenderSceneBuffers {
// 场景 3D 缓冲(内部渲染分辨率,非窗口分辨率)
RID rb_color; // 半浮点 HDR color
RID rb_depth; // depth(或 MSAA 版 + resolve 对)
RID rb_velocity; // 运动向量,TAA/运动模糊用
// 各效果模块的专属缓冲(归属各 effect 但生命周期挂在这里)
RID sdfgi 系列、ssao 系列、bloom 链...
// 配置
RenderSceneBuffersConfigurationRD config;
};
注意三层分辨率概念被刻意分开:窗口分辨率(swapchain,OS 管)、内部渲染分辨率(rb_*,可动态缩放,Viewport 的 scaling 系)、效果内部分辨率(SSAO 半分辨率、bloom mip 链)。RenderSceneBuffers 只管中间两层,swapchain 不归它——这是职责切分的关键,窗口事件和渲染资源解耦。
配置结构 RenderSceneBuffersConfigurationRD(4.2 引入)把影响资源形态的参数收拢:内部尺寸、MSAA 档、view 数量(XR stereo 是 2)、是否开 TAA/SSAO/SDFGI 等。它是纯值类型,每次 configure 全量传入。
重建触发链:为什么是全量而非增量
触发入口在 RenderSceneBuffersRD::configure(),调用链:
窗口 resize / 动态分辨率调档 / 效果开关变化
→ Viewport 标记渲染缓冲失效
→ RendererSceneRenderRD::render_buffers_configure()
→ RenderSceneBuffersRD::configure(config)
├─ config == 当前配置?直接返回(早退)
└─ 不同 → free 全部 rb_* RID → 按新 config 重建
早退逻辑很直接:配置没变就什么都不做(每帧都会走到这里,这是热路径,所以比较必须 O(1)——配置结构支持相等比较)。变了就全量销毁重建,没有"只重建 depth"这种增量路径。为什么?
两个原因。一是MSAA 的连坐效应:MSAA 档变化时 color/depth 都要换采样数,velocity、所有效果的输入引用全部失效——真正独立的资源很少,增量逻辑写出来是 90% 的重建 + 10% 的例外分支,不如全量。二是正确性便宜:RID 销毁后旧 framebuffer/pipeline 引用会在使用时被 RD 账簿拦住(见 RID 那篇的 generation 机制),全量重建不会出现"新旧资源混用"的中间态。用一次性重建的卡顿换永不出现撕裂帧,对 resize 这种低频事件是对的取舍。
重建的成本与可见症状
全量重建单次成本:释放 N 张纹理(RD 销毁是即时的,GPU 侧由帧栅栏兜底复用),再创建 N 张 + 重新 hash 查 uniform set 缓存(贴图 RID 变了,UniformSet 缓存键全换)+ 部分效果模块重建内部 RT。实测桌面 GPU 上一次完整重建约 1~3ms 的 CPU + 后续几帧的 GPU 空闲,用户看到的就是 resize 时那一下卡顿。
两个实战推论:
- 动态分辨率要限速。每帧变档等于每帧重建,API 虽然支持,实际项目必须做迟滞(只在帧时间持续超预算时降档,稳定后才升回)。Godot 自带的 dynamic resolution 模式内部有档位和冷却,自己实现要抄这个思想。
- resize 事件风暴要做合并。窗口拖拽连续触发 configure,每次全量重建,拖拽期间帧率崩一半很正常。4.x 的做法是视口层面合帧(每帧最多应用一次尺寸变更),自己写 window 逻辑时不要绕开它直接调 viewport 尺寸。
XR:多视角布局
config 里的 view 数量把这套机制推向最复杂的形态:XR stereo 时 view_count=2,rb_color 这类纹理按 layered texture(数组纹理)创建,一次 draw 用 multiview 扩展渲染两只眼,而不是两个 RenderSceneBuffers 实例。这个设计的收益在 cluster、SSAO 等效果模块——它们只看一份配置、跑一份 dispatch,天然支持 stereo;代价是纹理创建参数带 layer 维度,重建链和单眼完全一致,只是每张纹理更大。OpenXR 插件换分辨率(重投影模式切换)触发的同样是这条 configure 链。
一图流
排查线索
渲染相关的随机花屏如果集中在"窗口操作之后",先怀疑这条链:确认 configure 被触发的频率(profiler 里找 configure 采样点,频繁出现=有代码每帧改配置),再查自定义效果模块有没有缓存了旧 RID——模块在 configure 后不重置内部 RT 引用是最常见的新增 bug 源,引擎自带模块都处理了 RB_INTERNAL_TEXTURE_... 通知,自己写 RenderSceneBuffersExtension 时不要漏。
本文基于 Godot 4.2~4.4 源码:servers/rendering/renderer_rd/storage_rd/render_scene_buffers_rd.h/.cpp(资源清单与 configure 链)、servers/rendering/renderer_rd/render_scene_buffers_configuration_rd.h(配置结构)、servers/rendering/renderer_rd/renderer_scene_render_rd.cpp(触发点)。