跳到主要内容

渲染循环

每一帧都会经历三个阶段:

  1. 推进。 sm->advanceAndApply(dt) 更新状态机、动画、布局和数据绑定。
  2. 记录。 RiveRenderer 将绘制命令记录到当前活动的 RenderContext 中。
  3. 提交。 renderContext->flush(...) 构建 GPU 工作,并让后端提交它。
// 1. Advance.
sm->advanceAndApply(dt);

// 2. Record draws.
RenderContext::FrameDescriptor frame{};
frame.renderTargetWidth = w;
frame.renderTargetHeight = h;
frame.clearColor = 0xff202020;
renderContext->beginFrame(frame);

RiveRenderer renderer(renderContext.get());
renderer.save();
renderer.align(Fit::contain, Alignment::center,
AABB(0, 0, w, h), artboard->bounds());
sm->draw(&renderer);
renderer.restore();

// 3. Submit.
RenderContext::FlushResources flush{};
flush.renderTarget = renderTarget.get();
renderContext->flush(flush);

FrameDescriptor

配置即将开始的帧。每次 beginFrame 时这些值都会重置。

字段默认值作用
renderTargetWidth / renderTargetHeight0必须与你的渲染目标匹配。
loadActionclearclear / preserveRenderTarget / dontCare
clearColor0ARGB;仅在 loadAction == clear 时使用。
msaaSampleCount0非零值会强制启用 MSAA 模式。
disableRasterOrderingfalse即使支持光栅排序,也强制使用原子模式。
ditherModeinterleavedGradientNoise设置为 none 可禁用。
virtualTileWidth / virtualTileHeight0仅 Vulkan:用于抢占的帧分块。

FlushResources

携带后端提交已记录工作所需的一切内容。

字段用途
renderTarget你为本帧的后台缓冲区绑定的 RenderTarget*
externalCommandBuffer后端命令缓冲区 — VkCommandBuffer(Vulkan)、id<MTLCommandBuffer>(Metal)、WGPUCommandEncoder(WebGPU)。在 D3D11 / GL 上未使用。
currentFrameNumber用于资源生命周期跟踪的单调递增帧 ID。
safeFrameNumberGPU 工作已完成的最新帧(即 fence 已发出信号)。在此帧或更早帧最后使用的资源可以被回收。

在 D3D12、Vulkan 和 Metal 上,你必须每帧根据 fence 值更新 currentFrameNumbersafeFrameNumber。否则渲染器无法安全地回收暂存缓冲区,你可能会看到飞行中的数据被覆盖,或者内存无界增长。

固定时间步累加器

状态机和动画在固定 dt 下是确定性的。用累加器包裹真实经过时间的 delta,使播放在不同帧率下都可复现:

constexpr float kFixedSimDt = 1.f / 120.f;
constexpr float kMaxFrameDt = 0.25f; // cap after stalls

void tick(float realDt) {
if (realDt > kMaxFrameDt) realDt = kMaxFrameDt;
accumulator += realDt;
while (accumulator >= kFixedSimDt) {
sm->advanceAndApply(kFixedSimDt);
accumulator -= kFixedSimDt;
}
drawFrame();
}

上限(kMaxFrameDt)可以防止应用在调试器中暂停或被操作系统放到后台之后出现追赶风暴。

调整尺寸处理

调整尺寸时需要完成三件事:

  1. 调整你的交换链 / 帧缓冲区大小。
  2. 重新创建 RenderTarget
  3. 要么将新尺寸传给 artboard(用于 Fit::layout),要么调用 resetSize(),然后运行 advanceAndApply(0),让布局在下一次绘制前完成求解。
void onResize(uint32_t w, uint32_t h) {
swapChain->ResizeBuffers(0, w, h, DXGI_FORMAT_UNKNOWN, 0);

auto* impl = renderContext->static_impl_cast<RenderContextD3DImpl>();
renderTarget = impl->makeRenderTarget(w, h);

if (fit == Fit::layout) {
artboard->width(static_cast<float>(w));
artboard->height(static_cast<float>(h));
} else {
artboard->resetSize();
}
sm->advanceAndApply(0.f);
}

将 Artboard 对齐到视口

使用 computeAlignment(或 Renderer::align)将 artboard 空间映射到视口。保存这个矩阵 —— 你需要它的逆矩阵来把指针坐标转换回 artboard 空间。

#include "rive/renderer.hpp"

Mat2D align = computeAlignment(
Fit::contain, Alignment::center,
AABB(0, 0, w, h),
artboard->bounds());

renderer.save();
renderer.transform(align);
sm->draw(&renderer);
renderer.restore();

// Later, on input:
Vec2D pt = align.invertOrIdentity() * Vec2D{(float)mx, (float)my};
sm->pointerMove(pt);

何时跳过一帧

如果状态机还有更多工作,advanceAndApply 返回 true;如果一切都已稳定,则返回 false。对于已经循环到静止状态的动画,你可以跳过 advance 步骤和 draw 调用,在用户输入之间将 CPU/GPU 使用率降到零。

指针事件和外部 view-model 写入可以重新唤醒状态机 —— 每次发生后至少重新绘制一次。

销毁

sm.reset();
artboard.reset();
file = nullptr;
renderTarget = nullptr;
renderContext.reset(); // last

始终在 RenderContext 和底层 GPU 设备之前销毁 Rive 对象。Rive 对象持有引用计数的 GPU 资源;如果先销毁设备,它们会留下悬空句柄,并在释放时崩溃。