渲染流程
本文说明当前 main.cpp、app/dsl_app.h 和 core::dsl::Runtime 的真实行为:主动帧率节流、按需渲染、脏区缓存、blur 的特殊处理。
文件职责
main.cpp:窗口创建、GLFW/GLAD 初始化、DPI 和鼠标比例计算、主循环、主动帧率节流。app/app.h:应用生命周期接口。app/dsl_app.h:把 DSL compose 接到 Runtime,管理是否需要重新 compose。core/dsl_runtime.h:布局后的状态同步、交互、动画推进、dirty rect、framebuffer cache、实际渲染。core/primitive.h/core/text.cpp:底层 OpenGL 图元绘制。
主循环
当前主循环不是无脑满帧刷新。
静止时:
glfwWaitEvents()
有动画时:
按窗口所在显示器 refreshRate 计算下一帧时间
等待到下一帧时间点
update
如果有 dirty/full redraw:
render
glfwSwapBuffers
glfwPollEvents
当前使用主动节流:
glfwSwapInterval(0),不再依赖SwapBuffers的 vsync 阻塞。- 通过窗口和显示器重叠面积判断窗口当前主要在哪个显示器上。
- 使用该显示器
GLFWvidmode::refreshRate作为默认动画帧率。 - 如果
DslAppConfig::fps为正数,则使用min(refreshRate, fps)。 - Windows 下使用
timeBeginPeriod(1)提高等待精度,避免 240Hz 被普通计时器拖到约 60/64 FPS。
gallery 和 calculator 当前默认 fps = 90.0。gallery 的 Settings 中打开 Unlock 90 FPS limit 后会把 fps 同步为 0.0,动画节流回到屏幕刷新率。
App 生命周期
底层入口在 app/app.h:
namespace app {
const char* windowTitle();
bool showFrameCountInTitle();
double frameRateLimit();
int initialWindowWidth();
int initialWindowHeight();
bool initialize(GLFWwindow* window);
bool update(GLFWwindow* window,
float deltaSeconds,
int windowWidth,
int windowHeight,
float dpiScale,
float pointerScale);
bool isAnimating();
void render(int windowWidth, int windowHeight, float dpiScale);
void shutdown();
}
DSL app 一般不直接实现这些函数,而是包含 app/dsl_app.h,然后实现:
const DslAppConfig& dslAppConfig();
void compose(core::dsl::Ui& ui, const core::dsl::Screen& screen);
DSL App 更新流程
app/dsl_app.h 当前不会每轮循环都重新 compose。
它只在这些情况 compose:
- 首次运行。
- 逻辑窗口尺寸变化。
- Runtime 检测到点击回调导致业务状态变化,并设置
needsCompose()。
流程大致是:
如果首次或逻辑尺寸变化:
composeFrame()
changed = runtime.update(...)
如果 runtime.needsCompose():
composeFrame()
runtime.update(..., deltaSeconds = 0)
runtime.markFullRedraw()
changed = true
return changed
点击后多做一次 deltaSeconds = 0 的 Runtime update,是为了让新 DSL 树和 Runtime 缓存实例马上同步。否则可能出现“新声明 + 旧实例坐标”混在同一帧里,导致文本或控件短暂错位。
脏区渲染
当前 Runtime 使用一个离屏 framebuffer cache 做保守脏区渲染。
核心策略:
1. Runtime 按 id 缓存每个 Rect / Text 的动画值和 primitive。 2. 元素视觉值变化时,计算变化前后的 visual rect。 3. 对旧 rect 和新 rect 取 union,加入 dirty rect 列表。 4. render 时先渲染到离屏 cache。 5. full redraw 时重画整棵 UI。 6. 局部变化时:
- 绑定 cache framebuffer。
- 对 dirty rect 开
glScissor。 - 清理 dirty rect 内背景。
- 只重画与 dirty rect 相交的元素。
- 把 cache blit 到默认 framebuffer。
当前 dirty rect 已考虑:
- frame 变化。
- color / opacity / radius / border / shadow / blur 变化。
- transform 的 scale / rotate / translate 外接矩形。
- Text 的 frame / color / opacity 变化。
- visualStateFrom 带来的按下缩放。
Blur 与脏区
blur 是特殊图元,不等同于普通半透明 rect。
当前 blur 的实现方式是 backdrop blur:
1. 从当前 framebuffer/cache 中取 blur rect 附近的 backdrop。 2. 用 framebuffer blit 复制到 1/2 尺寸的 backdrop texture。 3. fragment shader 多次采样这块降采样 texture。 4. 把采样结果和自身颜色混合。
这意味着 blur 依赖“它背后的已绘制内容”。如果只做局部 dirty patch,某些情况下 cache 里可能是半旧半新的:
- dirty rect 被 scissor 清掉了一部分。
- 周围卡片 hover/press/rotate 后只重画了局部。
- blur 自己或附近元素上一帧的内容还残留在 cache 的其他区域。
这时 blur 会把“半旧半新的 cache”采进去,就可能出现竖条、污染、错位的模糊背景。
为保证正确性,Runtime 当前做了保守处理:
- 如果 dirty rect 触碰到任意 blur 元素的保护范围;
- 本帧会自动升级为 full redraw;
- 让 blur 采样完整、按顺序重画后的 cache。
保护范围目前按 blur rect 中心放大 1.2 倍计算。它不是完整 blur 半径的精确范围,而是性能和正确性之间的折中:减少 blur 周围普通交互触发 full redraw 的次数,同时避免最明显的半旧 cache 采样污染。
代价是:blur 附近交互会比普通 rect 更贵。gallery 默认关闭 Glass/blur,也是为了默认功耗更稳。
Full Redraw 触发
这些情况会触发或升级为 full redraw:
- 首帧。
- framebuffer 尺寸变化。
- Runtime cache 创建或重建。
- 点击回调导致业务状态变化并重新 compose。
- dirty rect 触碰到 blur 元素 1.2 倍保护范围。
- 调用
Runtime::markFullRedraw()。
当前限制
- 脏区是保守矩形,不是像素级 diff。
- 所有内容共用一个 framebuffer cache,还没有独立 layer tree。
- blur 依赖 framebuffer 内容,正确性优先时会扩大到 full redraw。
- 最终仍会把 cache blit 到默认 framebuffer。
- 已有基础矩形 clip 和 z-index;clip 通过 scissor 合成,z-index 影响同级绘制和 topmost hit-test。
- 当前不是 QML 那种 retained scene graph + batch renderer,复杂图元在高刷新率下成本会更明显。