相机切换之后,TAA 历史必须从空状态重新建立
玩家视角切到击杀回放的第一帧,场景边缘却短暂残留旧镜头里的墙体轮廓。把混合权重调低只能减轻拖影,不能解释为什么一张已经离开当前视锥的历史纹理仍有资格参与采样。真正需要回答的是:镜头改变之后,谁有权宣布上一帧历史失效,又怎样阻止旧帧稍后完成时把它重新装回去?

历史纹理还隐含了一个投影域
TAA 会把当前颜色与上一帧历史重投影后混合。历史有效不仅要求“纹理存在”,还要求它与当前相机、渲染尺寸和连续帧域相符。相机切换改变了观察变换,尺寸变化改变了像素网格;任一条件变化,旧颜色都不再是当前采样点的合法前序状态。
探针把这组条件收进 TaaHistoryOwner。它是唯一可以推进代次并替换活动历史的对象。渲染任务拿到的 HistoryTicket 是一次不可变快照;提交时必须重新验证,而不是相信任务启动时的判断。
readonly record struct HistoryTicket(
int Generation, int Frame, string Camera, int Width, int Height);
readonly record struct HistoryFrame(
int Generation, int Frame, string Camera, int Width, int Height);
sealed class TaaHistoryOwner
{
private int generation;
private string camera;
private int width;
private int height;
private HistoryFrame? active;
public TaaHistoryOwner(int generation, string camera, int width, int height)
{
this.generation = generation;
this.camera = camera;
this.width = width;
this.height = height;
}
public HistoryTicket BeginFrame(int frame) =>
new(generation, frame, camera, width, height);
public bool CanSample(HistoryTicket ticket) =>
active is { } history &&
ticket.Generation == generation &&
history.Generation == generation &&
history.Camera == camera &&
history.Width == width && history.Height == height;
public string Commit(HistoryTicket ticket)
{
if (ticket.Generation != generation) return "StaleGeneration";
if (ticket.Camera != camera) return "WrongCamera";
if (ticket.Width != width || ticket.Height != height) return "WrongExtent";
active = new(generation, ticket.Frame, camera, width, height);
return "Committed";
}
public void CameraCut(string nextCamera)
{
generation++;
camera = nextCamera;
active = null;
}
public void Resize(int nextWidth, int nextHeight)
{
generation++;
width = nextWidth;
height = nextHeight;
active = null;
}
}
代码路径是 $PROBE_ROOT/Program.cs,入口为 BeginFrame,随后依次经过 CanSample 与 Commit。这里没有让纹理池拥有有效性判断:池只负责存储,无法知道相机是否切换,也无法决定某个异步完成是否仍属于当前帧域。
清空历史还不够,旧完成也必须失去提交权
固定样本从 g7/Gameplay/1920x1080 开始。frame 420 没有历史,提交后 frame 421 可以采样。随后切换到 KillCam,相机所有者把代次推进到 g8 并清空历史;frame 422 因而只使用当前颜色建立新基线。
危险发生在 frame 421 的工作稍后才结束时。如果系统只在切镜时把纹理引用设为空,这个完成回调仍可能再次写入旧结果。代次检查把提交资格绑定到请求产生时的世界:g7 票据面对 g8 所有者只能得到 StaleGeneration,活动状态保持为空。

分辨率从 1920x1080 改为 1280x720 时使用同一规则,代次推进到 g9。这里不尝试缩放并继承旧历史,因为探针验证的是安全的生命周期基线;若产品决定保留历史,必须额外定义重采样滤波、运动向量尺度和有效区域,不能把尺寸变化悄悄隐藏在纹理池内部。
固定输出证明的是提交合同
当天在 $WORK_DIR 执行:
$ dotnet build $PROBE_ROOT/TaaHistoryLab.csproj -c Release --no-restore
Build succeeded.
0 Warning(s)
0 Error(s)
exit code: 0
$ dotnet run --project $PROBE_ROOT/TaaHistoryLab.csproj -c Release --no-build
RESULT passed=8 failed=0 skipped=0 elapsed_ms=18
FIXED stable=g7/Gameplay/frame421 sample=true
FIXED cut=g8/KillCam/frame422 sample=false late=StaleGeneration commit=Committed
FIXED resized=g=9 camera=KillCam extent=1280x720 history=none late=StaleGeneration
exit code: 0
八项检查覆盖首帧无历史、稳定帧采样、切镜失效、迟到拒绝、新基线提交、尺寸失效和旧尺寸拒绝。18 毫秒是探针内部计时,只证明这些状态断言在本次运行中完成,不代表 GPU 成本或 TAA 性能。
代次应该位于历史所有者,而不是散落在 Pass 中
把 if (cameraCut) 分散到每个渲染 Pass,会让失效规则依赖调用顺序:某个 Pass 清空颜色历史,另一个 Pass 仍可能读取旧深度或速度历史。更稳定的边界是让一个历史所有者管理同一投影域下的颜色、深度、速度和曝光资源,并在相机、尺寸或渲染配置破坏兼容性时统一推进代次。
当前探针没有处理缓慢相机运动、遮挡揭露、运动向量精度或历史钳制;这些属于同一代次内的样本质量问题。它解决的是更靠前的资格问题:相机切换后旧历史不参与首帧混合,旧任务即使完成也不能恢复已经失效的状态。只要渲染管线允许异步生产历史,这个提交检查就不能由一次清空操作替代。