音频限流:准入与记账必须是同一次事务
自动关联目录:音频限流:准入与记账必须是同一次事务
核心判断:数量裁剪不是“播放前看一次 Count,播放后再加一”。检查和占位之间只要存在启动窗口,多个请求就能同时越过上限;若开始与结束又使用不同规则,计数会进一步漂移。
一、具体业务:不同声音通道按距离和数量裁剪
音频裁剪把音乐、音效、语音等类型分开,并允许场景交互物使用独立通道和独立距离/数量配置。播放前根据第三人称、角色类别、相机距离、循环属性和当前数量给出明确裁剪原因;播放成功后以 PlayingId 记账,结束回调再扣减。
分类通道、原因枚举和 PlayingId 生命周期都值得保留。它们比在业务调用点散落 if (distance) 更易调参、诊断,也为精确释放提供了身份基础。问题出在这三步没有共享一次预算所有权。
二、实际错账链路
数量准入使用 Count > Max。当当前数量恰好等于上限时,请求仍被允许启动。开始回调中的 Tracker 却用 Count >= Max 拒绝加一,因此该 Voice 已经播放但没有进入计数。
调用方随后无条件保存 PlayingId -> Channel。Voice 结束时,结束回调看到这条映射,就会对 Tracker 扣一;也就是说,一个从未加过的 Voice 可以减掉另一个 Voice 占用的名额。计数虽然被钳到零,却已经低于真实活动数,后续请求会继续被放行。
还有两处同类失配:独立场景通道初始化时复制了普通通道上限,而准入读取独立上限;运行时配置可以修改当前上限,但 Tracker 保存的是初始化时副本。这样“谁允许播放”和“谁拒绝记账”甚至不使用同一份 Policy。
三、目标方案:Reserve -> Start -> Commit/Release
struct FAudioBudgetLease {
uint64 LeaseId;
FAudioChannel Channel;
uint32 PolicyRevision;
ELeaseState State; // Reserved / Active / Released
};
auto lease = Budget.TryReserve(channel); // 原子检查并占位
if (!lease)
return Culled(CountBudget);
PlayingId id = Middleware.Post(event, emitter);
if (!id.IsValid()) {
Budget.Release(lease);
return FailedToStart;
}
Budget.Commit(lease, id); // PlayingId 与唯一 Lease 绑定
void OnEnded(PlayingId id) {
Budget.ReleaseByPlayingId(id); // 未知、重复结束均为幂等 no-op
}TryReserve 必须在调用中间件之前完成,使用 Used = Reserved + Active 判断 Used >= Capacity。否则 Post 到开始回调之间的多个请求仍可一起通过。若启动回调才产生 PlayingId,预留先由 RequestId 持有,再原子 Remap 到 PlayingId。
预算器只读取一份当前 Policy。容量降低时要明确策略:通常不强停已有关键声音,而是暂停新准入,直到 Used <= Capacity;需要抢占时则按重要度选择受害者,并先释放其 Lease 再 Stop。Policy Revision 进入 Lease 和诊断,避免旧配置回调污染新通道。
四、与 Voice 虚拟化的边界
音频预算:静音 Voice 仍然不是零成本讨论 Logical/Virtual/Physical Voice 和排序策略;本文只解决“一个物理槽位是否被准确占有”。即使未来支持虚拟化,Physical Voice 的晋升也必须先拿 Lease,降级或结束只释放自己的 Lease。预算策略可以变化,账本不变量不能变化。
五、测试与指标
| 场景 | 预期 |
|---|---|
| Capacity = 0 | 所有普通请求在 Post 前被拒绝 |
| Capacity = 1,同帧发起两个请求 | 只有一个得到 Reservation |
| Post 失败或超时 | Reservation 回收,不产生 PlayingId 映射 |
| End 重复、未知 ID | 不改变 Used |
| 普通/独立通道上限不同 | 各自使用同一 Policy 完成准入与记账 |
| 运行时降低容量 | 不再超发;保留或淘汰遵循显式策略 |
诊断同时输出 Capacity、Reserved、Active、LeaseId、PlayingId、PolicyRevision、ReleaseReason,并始终满足 Used == 有效 Lease 数。计数器无法自证时,限流参数调得再精细也只是建立在错误样本上。