mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
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:
@@ -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`:
|
||||
|
||||
|
||||
@@ -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
32
runtime/vk_fg_reset.h
Normal 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
46
tests/fg_reset_check.c
Normal 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
40
tests/fg_reset_check.py
Normal 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())
|
||||
Reference in New Issue
Block a user