跳到主要内容

Command Queue

CommandQueue 是直接 File / Artboard / StateMachineInstance API(在其他文档中介绍)的异步、线程安全替代方案。你的应用线程会将命令入队(加载文件、推进状态机、转发指针事件),然后由 CommandServer 工作线程在渲染线程上取出并执行这些命令。

适用场景:

  • 你的应用线程和渲染线程是分离的,并且你不想跨线程调用直接 API。
  • 你想要一个与 GPU 线程解耦的 Rive 内容线程。
  • 你需要为 File / Artboard / StateMachineInstance 对象提供跨线程边界的、稳定的基于句柄的标识。

如果你的渲染循环和应用逻辑已经在同一个线程上,请优先使用直接 API —— CommandQueue 会引入你并不需要的间接层和监听器机制。

架构

CommandQueue 架构:应用线程上的 CommandQueue 将命令发送到渲染线程上的 CommandServer;CommandServer 拥有真实的 File、ArtboardInstance 和 StateMachineInstance 对象;服务器再通过应用线程上的监听器返回结果。

  • CommandQueue引用计数的(rcp<CommandQueue>),并且是线程安全的。应用线程持有一个引用;服务器持有另一端。
  • CommandServer 拥有真实的 FileArtboardInstanceStateMachineInstance 对象。它们永远不会离开渲染线程。
  • 所有跨线程标识符都是带类型的句柄FileHandleArtboardHandleStateMachineHandleViewModelInstanceHandleRenderImageHandleFontHandleAudioSourceHandle。应用线程持有句柄;服务器将它们解析为真实对象。
  • 异步结果(文件已加载、状态机已稳定、图像已解码)会通过针对句柄注册的监听器回调返回。

设置 Queue 和 Server

#include "rive/command_queue.hpp"
#include "rive/command_server.hpp"

// Shared between threads.
rcp<CommandQueue> queue = make_rcp<CommandQueue>();

在渲染线程上创建 CommandServer 并驱动它:

// `factory` is your Factory* — usually your RenderContext.
CommandServer server(queue, factory);

// Drain pending commands once per frame...
server.processCommands();

// ...or block on a dedicated thread until disconnect.
// server.serveUntilDisconnect();

processCommands() 是非阻塞的 —— 在每一帧之前调用它。serveUntilDisconnect() 是用于工作线程的运行循环变体:它会阻塞等待命令,并在应用线程调用 queue->disconnect() 后返回。

加载文件并创建状态机

以下所有调用都发生在应用线程上。它们会将命令入队并立即返回句柄 —— 实际工作会在服务器线程上执行。

std::vector<uint8_t> rivBytes = readFile("hero.riv");

FileHandle file = queue->loadFile(std::move(rivBytes));
ArtboardHandle artboard = queue->instantiateDefaultArtboard(file);
StateMachineHandle sm = queue->instantiateDefaultStateMachine(artboard);

这些句柄可以立即使用,即使加载尚未实际发生 —— 后续针对这些句柄入队的命令会按顺序在服务器上执行。

推进和绘制

// App thread:
queue->advanceStateMachine(sm, /* dt = */ 1.f / 60.f);

绘制的处理方式不同 —— 你需要注册一个绘制回调,该回调会在队列要求服务器执行时运行在服务器线程上。模式是:创建一个绘制键,附加一个回调,然后告诉服务器运行该键。

关于完整的绘制回调 API,请参阅 CommandQueue::drawCallbackrunDraw 方法(位于 command_queue.hpp);规范示例是 rive monorepo 中的 command-queue D3D11 示例(packages/sample_win32_d3d11_cq/)。

异步结果:监听器

由于命令是异步运行的,结果会通过监听器回调返回。每种句柄类型都有一个对应的监听器,其中包含 on… 方法:

class MyFileListener : public CommandQueue::FileListener
{
public:
void onFileLoaded(const FileHandle, uint64_t requestId) override
{
// Safe to instantiate artboards now, etc.
}
};

MyFileListener listener;
listener.attach(queue, file); // register against this handle

可用的监听器包括:

  • FileListeneronFileLoadedonFileDeletedonArtboardsListedonViewModelsListed,等等。
  • ArtboardListeneronArtboardInstancedonArtboardErroronArtboardDeleted
  • StateMachineListeneronStateMachineInstancedonStateMachineSettled(会在导致状态机稳定的请求上调用),等等。
  • ViewModelInstanceListenerRenderImageListenerFontListenerAudioSourceListener — 用于其他资源类型。

requestId 可让你将回调与特定命令关联起来。向接受 requestId 的队列方法传入非零值(例如 deleteFile(handle, requestId)),监听器就会收到相同的 ID。

指针事件

指针事件也通过队列传递。CommandQueue::PointerEvent 携带屏幕空间位置;服务器会使用状态机最近一次的变换将其转换为 artboard 空间:

CommandQueue::PointerEvent ev;
ev.position = { mouseX, mouseY };
ev.kind = CommandQueue::PointerEvent::Kind::move;
queue->queuePointerEvent(sm, ev);

清理

应用线程通知服务器停止:

queue->disconnect();

如果服务器正在运行 serveUntilDisconnect(),当 disconnect 命令到达时该调用会返回。之后,你需要自行负责 join 服务器线程。

资源句柄可以通过显式入队删除命令来清理(或让服务器析构函数回收):

queue->deleteStateMachine(sm);
queue->deleteArtboard(artboard);
queue->deleteFile(file);

何时改用直接 API

当满足以下情况时,其他页面中介绍的直接 File / Artboard / StateMachineInstance API 更简单:

  • 你的应用已经从渲染线程驱动 Rive。
  • 你不需要跨线程的基于句柄的标识。
  • 你想要同步返回值,而不是监听器回调。

CommandQueue 是为多线程场景而存在的 —— 对于引擎、渲染服务器以及拥有专用 GPU 线程的应用来说,它是合适的工具。