Tell FSR 3.1 frame generation of camera cuts, so a generated frame is never blended from two unrelated pictures.

vk_dlaa_fg_inputs hands out DLSS's new history with the frame's depth and motion, and FSR 4's and NVIDIA's frame
generation pass it on, but vk_fg_frame dropped it: FSR 3.1 takes a cut only in the generation callback, which its
swapchain calls later on its own thread. vk_fg_frame now notes each frame id's cut in a small ring
(runtime/vk_fg_reset.h), and generate sets the callback's reset from it. A frame after one that was not prepared (the
menu open, no inputs) counts as a cut too, and so does an id the ring no longer holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
FIocker
2026-11-12 17:17:31 +01:00
committed by chrg6
parent 5fee8d7ab2
commit f0d0fae13b
5 changed files with 137 additions and 2 deletions

View File

@@ -473,6 +473,11 @@ builds the header alone and replays an area change after a loading screen from a
half-resolution passes are per-frame again at once, a lookup table, a volume's slices and an icon capture still wait,
and an image taken over by another texture starts without the history of the one before. No GPU, no game files.
A change to which frames FSR 3.1 frame generation is told are camera cuts (`runtime/vk_fg_reset.h`, `runtime/vk_fg.cpp`
`generate` and `vk_fg_frame`) passes `python tests\fg_reset_check.py`, which builds the header alone and checks that each
frame id gets its own frame's cut, that an id never noted or no longer held counts as a cut, and that a slot rewritten
on another thread never answers an old id with a newer frame's value. No GPU, no game files.
The lifter (`recomp/`) and the CPU headers (`runtime/cpu.h`, `runtime/simd.h`) are checked against Unicorn. Every lifter
change passes these, all run with `.venv`:

View File

@@ -28,6 +28,7 @@ extern "C" {
#include "vk_dlssg.h"
#include "vk_ffx.h"
#include "vk_fg.h"
#include "vk_fg_reset.h"
#include "ui/menu.h"
}
@@ -45,6 +46,9 @@ struct {
int failed = 0;
uint64_t generated = 0, paused = 0, no_inputs = 0, hudless = 0, no_hudless = 0;
int on = 0; // the last configured frame had generation on (vk_fg_generating)
FgrRing cuts; // each frame id's camera cut, for the generation callback (vk_fg_reset.h)
uint64_t cut_frames = 0; // frames handed to the callback as cuts
int prepared = 0; // the last frame was prepared (generation on with its inputs)
} F;
VkResult submit_game(uint32_t n, const VkSubmitInfo *si, VkFence fence) {
@@ -56,6 +60,8 @@ VkResult submit_game(uint32_t n, const VkSubmitInfo *si, VkFence fence) {
ffxReturnCode_t generate(ffxDispatchDescFrameGeneration *d, void *user) {
F.generated++;
// the swapchain's own reset (a frame id that did not follow the last) stays; a cut vk_fg_frame noted is added
if (fgr_cut(&F.cuts, d->frameID)) d->reset = true;
return vk_ffx_dispatch((ffxContext *)user, &d->header);
}
@@ -267,6 +273,11 @@ extern "C" void vk_fg_frame(VkCommandBuffer cb, VkSwapchainKHR sc, VkFormat fmt,
else if (!hudless) F.no_hudless++;
else if (!on) F.paused++;
F.frame++;
// A cut: DLSS started a new history (vk_dlaa_fg_inputs), or the frame before was not prepared (generation off for
// the menu, no inputs), so the frame between the two would be made from a stale depth and motion
fgr_note(&F.cuts, F.frame, reset || !F.prepared);
if ((reset || !F.prepared) && on) F.cut_frames++;
F.prepared = on;
ffxConfigureDescFrameGeneration cfg = {};
cfg.header.type = FFX_API_CONFIGURE_DESC_TYPE_FRAMEGENERATION;
cfg.swapChain = (void *)sc;
@@ -312,9 +323,9 @@ extern "C" void vk_fg_frame(VkCommandBuffer cb, VkSwapchainKHR sc, VkFormat fmt,
if (r != FFX_API_RETURN_OK) return fail("frame generation preparation", r);
if (F.frame == 2 || F.frame % 3600 == 0)
rt_log("fg: %llu frames presented, %llu generated (%llu with the HUD kept out); paused %llu (menu), %llu without depth "
"and motion, %llu without the HUD-less picture", (unsigned long long)F.frame, (unsigned long long)F.generated,
"and motion, %llu without the HUD-less picture; %llu after a camera cut", (unsigned long long)F.frame, (unsigned long long)F.generated,
(unsigned long long)F.hudless, (unsigned long long)F.paused, (unsigned long long)F.no_inputs,
(unsigned long long)F.no_hudless);
(unsigned long long)F.no_hudless, (unsigned long long)F.cut_frames);
}
extern "C" void vk_fg_release(void) {
@@ -322,4 +333,5 @@ extern "C" void vk_fg_release(void) {
if (F.fg) vk_ffx_destroy(&F.fg), F.fg = nullptr;
if (F.sc) vk_ffx_destroy(&F.sc), F.sc = nullptr;
__atomic_store_n(&F.on, 0, __ATOMIC_RELAXED);
F.prepared = 0;
}

32
runtime/vk_fg_reset.h Normal file
View File

@@ -0,0 +1,32 @@
// Camera cuts for FSR 3.1 frame generation (vk_fg.cpp). DLSS starts a new history on a cut, another scene size or
// another target, and vk_dlaa.c vk_dlaa_fg_inputs hands that out with the frame's depth and motion vectors. FSR 3.1
// takes it only in the generation callback (ffxDispatchDescFrameGeneration.reset), which its swapchain calls later,
// on its own present thread, for the frame id the configuration gave. So vk_fg_frame notes each frame's cut here
// under its id, and the callback looks it up. Without it every generated frame of a cut was blended from two unrelated
// pictures: an area load or a cutscene's cut flashed a frame of both.
//
// A ring of FGR_SLOTS frames; each slot holds the frame id and its cut as one atomic word, so a slot being rewritten
// never pairs an old id with a new frame's answer. An id the ring no longer holds (the callback fell that far behind,
// or the slot was never written) counts as a cut: a lost history costs one plain frame, a wrong one blends two.
// Own header so tests/fg_reset_check.c checks it without a GPU.
#pragma once
#include <stdint.h>
#define FGR_SLOTS 32
typedef struct {
uint64_t slot[FGR_SLOTS]; // (frame id << 1) | cut, written by vk_fg_frame; 0 never matches (ids start at 1)
} FgrRing;
// Frame ids above this do not fit beside the cut bit; they always count as a cut.
#define FGR_MAX_ID (UINT64_MAX >> 1)
static inline void fgr_note(FgrRing *r, uint64_t id, int cut) {
if (id && id <= FGR_MAX_ID) __atomic_store_n(&r->slot[id % FGR_SLOTS], id << 1 | (cut != 0), __ATOMIC_RELEASE);
}
static inline int fgr_cut(const FgrRing *r, uint64_t id) {
if (!id || id > FGR_MAX_ID) return 1;
uint64_t v = __atomic_load_n(&r->slot[id % FGR_SLOTS], __ATOMIC_ACQUIRE);
return v >> 1 != id || (v & 1);
}

46
tests/fg_reset_check.c Normal file
View File

@@ -0,0 +1,46 @@
// runtime/vk_fg_reset.h: the camera cut FSR 3.1 frame generation's callback is told of for a frame id. Built and run
// by tests/fg_reset_check.py; prints each failed check and exits 1 on any.
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include "vk_fg_reset.h"
static int fails;
#define CHECK(c, ...) do { if (!(c)) { fails++; printf("FAIL line %d: ", __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0)
static FgrRing ring;
static volatile int stop;
// Rewrites one slot over and over: frame 3, a cut, and frame 3 + FGR_SLOTS, none.
static void *writer(void *arg) {
(void)arg;
while (!stop) fgr_note(&ring, 3, 1), fgr_note(&ring, 3 + FGR_SLOTS, 0);
return NULL;
}
int main(void) {
// nothing noted: every id is a cut, id 0 and ids past the range too
CHECK(fgr_cut(&ring, 0) && fgr_cut(&ring, 1) && fgr_cut(&ring, 77), "an id never noted is a cut");
CHECK(fgr_cut(&ring, FGR_MAX_ID + 1) && fgr_cut(&ring, UINT64_MAX), "an id past the range is a cut");
// a run of frames, a cut at 40: the callback for each id gets that frame's answer
for (uint64_t f = 1; f <= 60; f++) fgr_note(&ring, f, f == 40);
int wrong = 0;
for (uint64_t f = 60 - FGR_SLOTS + 1; f <= 60; f++) wrong += fgr_cut(&ring, f) != (f == 40);
CHECK(!wrong, "%d of the last %d frames answered wrong", wrong, FGR_SLOTS);
// the callback fell behind by more than the ring: its frame's slot holds a later one, so it is a cut
CHECK(fgr_cut(&ring, 60 - FGR_SLOTS), "a frame the ring no longer holds is a cut");
CHECK(fgr_cut(&ring, 61), "a frame not noted yet is a cut");
// the id and its cut are one word: a slot rewritten on another thread never answers frame 3 with the next one's
// "no cut"
memset(&ring, 0, sizeof ring);
pthread_t t;
pthread_create(&t, NULL, writer, NULL);
int paired = 0;
for (int i = 0; i < 2000000; i++) paired += !fgr_cut(&ring, 3);
stop = 1;
pthread_join(t, NULL);
CHECK(!paired, "frame 3 was answered with the other frame's value %d times", paired);
printf(fails ? "fg_reset_check: %d failed\n" : "fg_reset_check: ok\n", fails);
return fails != 0;
}

40
tests/fg_reset_check.py Normal file
View File

@@ -0,0 +1,40 @@
"""Build and run tests/fg_reset_check.c: runtime/vk_fg_reset.h, which frames FSR 3.1 frame generation's callback is
told are camera cuts.
No GPU, no game files, any Python.
Usage: fg_reset_check.py
"""
import os
import shutil
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MSYS_BIN = Path(r'C:\msys64\ucrt64\bin')
def compiler():
"""$CC, else MSYS2 UCRT64's clang, else clang on PATH (as tests/diff/common.py picks it, without its Unicorn)."""
if os.environ.get('CC'):
return os.environ['CC']
if os.name == 'nt' and (MSYS_BIN / 'clang.exe').exists():
return str(MSYS_BIN / 'clang.exe')
return shutil.which('clang') or 'clang'
def main():
work = ROOT / 'build' / 'test' / 'fg_reset'
work.mkdir(parents=True, exist_ok=True)
exe = work / ('fg_reset_check.exe' if os.name == 'nt' else 'fg_reset_check')
env = dict(os.environ)
if os.name == 'nt' and MSYS_BIN.exists():
env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
subprocess.check_call([compiler(), '-std=gnu11', '-O1', '-Wall', '-Werror', '-pthread', '-I', str(ROOT / 'runtime'),
str(ROOT / 'tests' / 'fg_reset_check.c'), '-o', str(exe)], env=env)
return subprocess.run([str(exe)], env=env).returncode
if __name__ == '__main__':
sys.exit(main())