mirror of
https://github.com/FelipePrado0/SpartanPort.git
synced 2026-11-07 02:36:06 -10:00
FSR 4: post pass through shared memory (2.9 -> 0.8 ms at 4K), benchmark, pass profile
The v07 post pass stores each 2x2 block into three images one pixel at a time; collecting the workgroup's 16x16 block in shared memory and storing contiguous rows makes it 3.5x faster on RDNA3, bit-exact with the original (tools/fsr4_verify.sh). tools/fsr4_optimize.sh builds the variants into fsr4_shaders/opt (spirv-cross -> fsr4_post_lds.pl -> glslang), run.sh and fetch_fsr4_assets.sh call it, vk_fsr4.cpp prefers them (BB_FSR4_OPT=0 off). out/gpu/fsr4-bench runs FSR 4 outside the game; the provider (submodule, local branch, patch in gpu/patches/fsr-vulkan) gains BB_FSR4_PROFILE and BB_FSR4_STATS. No reactive mask work when FSR 4 is used. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
10f3a4a027
commit
86dea1a25a
14 files changed
+930
-4
No files matched your search
@@ -83,3 +83,37 @@ The previous frame's matrices are not there; the port keeps them itself.
|
||||
|
||||
Осталось: спрайты (по 4 индекса) в цвет сцены со сценической глубиной — свечения, огоньки —
|
||||
по-прежнему не сдвигаются jitter (правило «≤ 6 индексов = полноэкранный проход»).
|
||||
|
||||
## 2026-10-01: FSR 4 быстрее — проход post (2.9 → 0.8 мс в 4K)
|
||||
|
||||
Замеры: `BB_FSR4_PROFILE=1` (время каждого прохода FSR 4, патч провайдера в субмодуле,
|
||||
см. `gpu/patches/fsr-vulkan`), бенчмарк вне игры `out/gpu/fsr4-bench` (`ninja -C out/gpu
|
||||
fsr4-bench`; `--stats` — регистры и инструкции от RADV). 4K Balanced (2260x1272 → 3840x2160),
|
||||
RX 7800 XT: весь FSR 4 — 5.8 мс, из них **post 2.5–2.9 мс** (не нейросеть: последние слои,
|
||||
pixel shuffle 2x2 и смешивание с историей), 12 проходов модели — 2.4 мс, pre 0.55.
|
||||
|
||||
Причина: каждый поток считает блок 2x2 выходных пикселей и пишет их по одному в три образа
|
||||
(рекуррентное состояние, история, выход) — каждая инструкция записи волны пишет пиксели через
|
||||
один. Убрав любую из трёх записей, проход ускорялся в 1.3–4 раза при том же коде.
|
||||
|
||||
Решение: `tools/fsr4_optimize.sh` декомпилирует post (spirv-cross), `tools/fsr4_post_lds.pl`
|
||||
собирает блок 16x16 рабочей группы в shared memory и пишет сплошными строками, glslang
|
||||
компилирует обратно в `fsr4_shaders/opt/`; `vk_fsr4.cpp` берёт его оттуда (`BB_FSR4_OPT=0` —
|
||||
оригинал). Две тонкости, найденные сравнением выходов:
|
||||
- spirv-cross переводит знаковую распаковку int8 (`OpBitcast` в `i8vec4`) как `unpack8(uint)`
|
||||
(беззнаковую) — без исправления результат совсем другой (PSNR 17 дБ);
|
||||
- значения в shared memory должны оставаться float: half из shared memory компилятор
|
||||
превращает в 16-битную запись, а она иначе округляет в unorm8.
|
||||
|
||||
`tools/fsr4_verify.sh` сравнивает оригинал и оптимизированный post для всех пресетов при выводе
|
||||
1080p/1440p/4K на псевдослучайных входах: 1440p и 4K — побитово одинаково; post 4K 2.1–3.5 →
|
||||
0.81–0.87 мс, 1440p 1.2–1.5 → 0.36–0.38 мс, 1080p 0.37–0.69 → 0.20–0.22 мс. В игре (4K
|
||||
Balanced): FSR 4 5.8 → 4.0 мс, кадр GPU 13.1 → ~11.6 мс; дальше FPS упирается в CPU (поток
|
||||
GPU-команд ждёт копии гостевой памяти, «host copies» ~20%).
|
||||
|
||||
Найдено попутно: **оригинальный FSR 4 на уровне 1080 (вывод 1920x1080) недетерминирован** — от
|
||||
запуска к запуску на одинаковых входах меняется полоса у левого края (столбцы 0–132), то есть
|
||||
где-то гонка или чтение неинициализированной памяти в модели/провайдере. При выводе 1080p это
|
||||
может давать мерцание у левого края кадра. Не исследовано.
|
||||
|
||||
Маска реактивности при FSR 4 больше не считается (FSR 4 её не принимает).
|
||||
@@ -31,3 +31,7 @@ for file in "${files[@]}"; do
|
||||
fetched=$((fetched + 1))
|
||||
done
|
||||
echo "FSR 4 assets: ${#files[@]} files in $PWD/$dest ($fetched downloaded)"
|
||||
# Faster, bit-exact post passes (fsr4_shaders/opt), when spirv-cross and glslang are available.
|
||||
if command -v spirv-cross >/dev/null && command -v glslangValidator >/dev/null; then
|
||||
bash tools/fsr4_optimize.sh
|
||||
fi
|
||||
@@ -154,3 +154,7 @@ target_compile_options(scene-resolution-test PRIVATE -UNDEBUG -fPIC)
|
||||
set_source_files_properties(../test_scene_resolution.cpp PROPERTIES
|
||||
COMPILE_OPTIONS "-fvisibility=hidden;-fvisibility-inlines-hidden")
|
||||
target_link_libraries(scene-resolution-test PRIVATE bbgpu)
|
||||
|
||||
# bbport: FSR 4 benchmark outside the game (tools/fsr4_bench.cpp): ninja -C out/gpu fsr4-bench.
|
||||
add_executable(fsr4-bench EXCLUDE_FROM_ALL tools/fsr4_bench.cpp)
|
||||
target_link_libraries(fsr4-bench PRIVATE ffx-vulkan::fsr4-v07-vulkan ffx-vulkan::fsr4-v07-assets Vulkan::Vulkan)
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
From 707fe318eec1f9912b0e1fa700655a2e1679cd79 Mon Sep 17 00:00:00 2001
|
||||
From: deadinside <[email protected]>
|
||||
Date: Thu, 1 Oct 2026 05:31:00 +0300
|
||||
Subject: [PATCH] FSR 4 v07 provider: BB_FSR4_PROFILE pass timings,
|
||||
BB_FSR4_STATS driver statistics
|
||||
|
||||
bbport: GPU timestamps around each dispatch, read when the frame retires and
|
||||
printed every 300 frames; pipelines created with CAPTURE_STATISTICS print
|
||||
the driver's register/instruction statistics (the device must enable
|
||||
pipelineExecutableInfo).
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
||||
---
|
||||
src/ffx_vk_fsr4_v07.c | 109 ++++++++++++++++++++++++++++++++++++++++++
|
||||
1 file changed, 109 insertions(+)
|
||||
|
||||
diff --git a/src/ffx_vk_fsr4_v07.c b/src/ffx_vk_fsr4_v07.c
|
||||
index c30f7cf..eb42c16 100644
|
||||
--- a/src/ffx_vk_fsr4_v07.c
|
||||
+++ b/src/ffx_vk_fsr4_v07.c
|
||||
@@ -35,6 +35,7 @@
|
||||
#define NUM_POOL_FRAMES 3 /* maximum unretired host dispatches */
|
||||
#define MAX_STAGING 32
|
||||
#define MAX_EXTERNAL_IMAGES 8 /* color, depth, motion, output + host slack */
|
||||
+#define PROF_MAX_JOBS 32 /* timestamped dispatches per frame */
|
||||
#define CBUF_FRAME_BYTES (512 * 1024)
|
||||
#define CBUF_RING_BYTES (CBUF_FRAME_BYTES * NUM_POOL_FRAMES)
|
||||
|
||||
@@ -182,6 +183,16 @@ struct FfxFsr4VkContext {
|
||||
VkBool32 int8Supported;
|
||||
VkBool32 dotProductSupported;
|
||||
VkBool32 computeDerivativesSupported;
|
||||
+
|
||||
+ /* bbport: BB_FSR4_PROFILE=1 — GPU time per pass (timestamps around each
|
||||
+ * dispatch, read back when the frame retires, printed every 300 frames). */
|
||||
+ VkQueryPool profPool;
|
||||
+ float profPeriodNs;
|
||||
+ uint32_t profCount[NUM_POOL_FRAMES];
|
||||
+ uint32_t profPass[NUM_POOL_FRAMES][PROF_MAX_JOBS];
|
||||
+ double profMs[MAX_PIPELINES];
|
||||
+ uint32_t profCalls[MAX_PIPELINES];
|
||||
+ uint64_t profFrames;
|
||||
};
|
||||
|
||||
static FfxErrorCode restoreExternalImages(FfxFsr4VkContext* c,
|
||||
@@ -625,6 +636,36 @@ static VkFormat toVkFmt(FfxSurfaceFormat f)
|
||||
|
||||
// ── pipeline creation ─────────────────────────────────────────────────────────
|
||||
|
||||
+/* bbport: BB_FSR4_STATS=1 (the device must enable pipelineExecutableInfo) prints the
|
||||
+ * driver's statistics of each pass: registers, spills, instruction counts. */
|
||||
+static void printPipelineStats(FfxFsr4VkContext* c, uint32_t passIdx, VkPipeline pipeline)
|
||||
+{
|
||||
+ PFN_vkGetPipelineExecutableStatisticsKHR getStats =
|
||||
+ (PFN_vkGetPipelineExecutableStatisticsKHR)vkGetDeviceProcAddr(
|
||||
+ c->dev, "vkGetPipelineExecutableStatisticsKHR");
|
||||
+ if (!getStats) return;
|
||||
+ VkPipelineExecutableInfoKHR info;
|
||||
+ memset(&info, 0, sizeof(info));
|
||||
+ info.sType = VK_STRUCTURE_TYPE_PIPELINE_EXECUTABLE_INFO_KHR;
|
||||
+ info.pipeline = pipeline;
|
||||
+ uint32_t count = 0;
|
||||
+ if (getStats(c->dev, &info, &count, NULL) != VK_SUCCESS || count == 0) return;
|
||||
+ VkPipelineExecutableStatisticKHR* s = calloc(count, sizeof(*s));
|
||||
+ if (!s) return;
|
||||
+ for (uint32_t i = 0; i < count; ++i) s[i].sType = VK_STRUCTURE_TYPE_PIPELINE_EXECUTABLE_STATISTIC_KHR;
|
||||
+ if (getStats(c->dev, &info, &count, s) == VK_SUCCESS) {
|
||||
+ printf("FSR 4 pass %u:", passIdx);
|
||||
+ for (uint32_t i = 0; i < count; ++i) {
|
||||
+ if (s[i].format == VK_PIPELINE_EXECUTABLE_STATISTIC_FORMAT_UINT64_KHR)
|
||||
+ printf(" %s=%llu", s[i].name, (unsigned long long)s[i].value.u64);
|
||||
+ else if (s[i].format == VK_PIPELINE_EXECUTABLE_STATISTIC_FORMAT_BOOL32_KHR)
|
||||
+ printf(" %s=%u", s[i].name, s[i].value.b32);
|
||||
+ }
|
||||
+ printf("\n");
|
||||
+ }
|
||||
+ free(s);
|
||||
+}
|
||||
+
|
||||
static VkResult mkPipeline(FfxFsr4VkContext* c, uint32_t passIdx, VkPipe* out)
|
||||
{
|
||||
if (!c || !out || passIdx >= FFX_FSR4_VK_PASS_COUNT)
|
||||
@@ -709,7 +750,11 @@ static VkResult mkPipeline(FfxFsr4VkContext* c, uint32_t passIdx, VkPipe* out)
|
||||
cpci.stage.pName=blob->entryPoint ? blob->entryPoint : "main";
|
||||
cpci.layout=out->layout;
|
||||
|
||||
+ const char* statsEnv = getenv("BB_FSR4_STATS");
|
||||
+ const int stats = statsEnv && statsEnv[0] == '1';
|
||||
+ if (stats) cpci.flags |= VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR;
|
||||
r = vkCreateComputePipelines(c->dev, VK_NULL_HANDLE, 1, &cpci, c->alloc, &out->pipeline);
|
||||
+ if (stats && r == VK_SUCCESS) printPipelineStats(c, passIdx, out->pipeline);
|
||||
vkDestroyShaderModule(c->dev, sm, c->alloc);
|
||||
if (r) {
|
||||
vkDestroyPipelineLayout(c->dev, out->layout, c->alloc);
|
||||
@@ -1433,6 +1478,38 @@ static FfxErrorCode writeFullDs(FfxFsr4VkContext* c, const VkPipe* pipe, VkDescr
|
||||
|
||||
// ── execute all queued jobs ───────────────────────────────────────────────────
|
||||
|
||||
+static void profCollect(FfxFsr4VkContext* c, uint32_t slot)
|
||||
+{
|
||||
+ const uint32_t count = c->profCount[slot];
|
||||
+ if (!c->profPool || count == 0) return;
|
||||
+ c->profCount[slot] = 0;
|
||||
+ uint64_t ts[PROF_MAX_JOBS + 1];
|
||||
+ if (vkGetQueryPoolResults(c->dev, c->profPool, slot * (PROF_MAX_JOBS + 1), count + 1,
|
||||
+ sizeof(ts), ts, sizeof(uint64_t), VK_QUERY_RESULT_64_BIT) != VK_SUCCESS)
|
||||
+ return;
|
||||
+ for (uint32_t j = 0; j < count; ++j) {
|
||||
+ const uint32_t pass = c->profPass[slot][j];
|
||||
+ c->profMs[pass] += (double)(ts[j + 1] - ts[j]) * c->profPeriodNs * 1e-6;
|
||||
+ c->profCalls[pass]++;
|
||||
+ }
|
||||
+ if (++c->profFrames % 300 != 0) return;
|
||||
+ static const char* names[] = {"pre", "model 1", "model 2", "model 3", "model 4",
|
||||
+ "model 5", "model 6", "model 7", "model 8", "model 9", "model 10", "model 11",
|
||||
+ "model 12", "post", "rcas", "spd auto exposure"};
|
||||
+ double total = 0.0;
|
||||
+ for (uint32_t p = 0; p < MAX_PIPELINES; ++p) total += c->profMs[p];
|
||||
+ printf("FSR 4 profile: %.3f ms/frame over %llu frames\n", total / 300.0,
|
||||
+ (unsigned long long)300);
|
||||
+ for (uint32_t p = 0; p < MAX_PIPELINES; ++p) {
|
||||
+ if (!c->profCalls[p]) continue;
|
||||
+ printf(" %6.3f ms/frame %s\n", c->profMs[p] / 300.0,
|
||||
+ p < sizeof(names) / sizeof(names[0]) ? names[p] : "?");
|
||||
+ c->profMs[p] = 0.0;
|
||||
+ c->profCalls[p] = 0;
|
||||
+ }
|
||||
+ fflush(stdout);
|
||||
+}
|
||||
+
|
||||
static FfxErrorCode cbExecuteJobs(FfxInterface* I, FfxCommandList cl, FfxUInt32 id) {
|
||||
(void)id;
|
||||
if (!I || !I->device) return FFX_ERROR_INVALID_POINTER;
|
||||
@@ -1503,6 +1580,12 @@ static FfxErrorCode cbExecuteJobs(FfxInterface* I, FfxCommandList cl, FfxUInt32
|
||||
* Move them to the provider's GENERAL compute ABI only after all resource
|
||||
* registration has succeeded, then restore them during unregister. */
|
||||
transitionExternalImagesToCompute(c, cmd);
|
||||
+ const uint32_t profBase = cur * (PROF_MAX_JOBS + 1);
|
||||
+ uint32_t profJobs = 0;
|
||||
+ if (c->profPool) {
|
||||
+ vkCmdResetQueryPool(cmd, c->profPool, profBase, PROF_MAX_JOBS + 1);
|
||||
+ vkCmdWriteTimestamp(cmd, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, c->profPool, profBase);
|
||||
+ }
|
||||
|
||||
for (uint32_t ji=0; ji<c->jobCount; ji++) {
|
||||
FfxGpuJobDescription* job=&c->jobs[ji].d;
|
||||
@@ -1582,6 +1665,11 @@ static FfxErrorCode cbExecuteJobs(FfxInterface* I, FfxCommandList cl, FfxUInt32
|
||||
bp->layout,0,1,&ds,0,NULL);
|
||||
|
||||
vkCmdDispatch(cmd,cj->dimensions[0],cj->dimensions[1],cj->dimensions[2]);
|
||||
+ if (c->profPool && profJobs < PROF_MAX_JOBS) {
|
||||
+ vkCmdWriteTimestamp(cmd, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, c->profPool,
|
||||
+ profBase + 1 + profJobs);
|
||||
+ c->profPass[cur][profJobs++] = passIdx;
|
||||
+ }
|
||||
|
||||
// Memory barrier between compute passes — ensures writes from this
|
||||
// dispatch are visible to the next dispatch (pre→model→post chain).
|
||||
@@ -1610,6 +1698,7 @@ static FfxErrorCode cbExecuteJobs(FfxInterface* I, FfxCommandList cl, FfxUInt32
|
||||
} else EXECUTE_FAIL(FFX_ERROR_INVALID_ENUM);
|
||||
}
|
||||
|
||||
+ c->profCount[cur] = profJobs;
|
||||
c->jobCount=0;
|
||||
c->recordingFrame = UINT32_MAX;
|
||||
#undef EXECUTE_FAIL
|
||||
@@ -1694,6 +1783,21 @@ VkResult ffxFsr4VkCreateContext(const FfxFsr4VkCreateInfo* ci, FfxInterface* out
|
||||
sci.addressModeU=sci.addressModeV=sci.addressModeW=VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||
sci.maxLod=VK_LOD_CLAMP_NONE;
|
||||
r=vkCreateSampler(c->dev,&sci,c->alloc,&c->linearSampler);
|
||||
+ {
|
||||
+ const char* prof = getenv("BB_FSR4_PROFILE");
|
||||
+ if (r == VK_SUCCESS && prof && prof[0] == '1') {
|
||||
+ VkPhysicalDeviceProperties props;
|
||||
+ vkGetPhysicalDeviceProperties(c->phys, &props);
|
||||
+ c->profPeriodNs = props.limits.timestampPeriod;
|
||||
+ VkQueryPoolCreateInfo qci;
|
||||
+ memset(&qci, 0, sizeof(qci));
|
||||
+ qci.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
|
||||
+ qci.queryType = VK_QUERY_TYPE_TIMESTAMP;
|
||||
+ qci.queryCount = NUM_POOL_FRAMES * (PROF_MAX_JOBS + 1);
|
||||
+ if (vkCreateQueryPool(c->dev, &qci, c->alloc, &c->profPool) != VK_SUCCESS)
|
||||
+ c->profPool = VK_NULL_HANDLE;
|
||||
+ }
|
||||
+ }
|
||||
if (r) goto fail;
|
||||
|
||||
// cbuffer ring buffer (host-visible, persistently mapped)
|
||||
@@ -1809,6 +1913,10 @@ void ffxFsr4VkDestroyContext(FfxFsr4VkContext* c)
|
||||
}
|
||||
memset(c->frame, 0, sizeof(c->frame));
|
||||
c->recordingFrame = UINT32_MAX;
|
||||
+ if (c->profPool) {
|
||||
+ vkDestroyQueryPool(c->dev, c->profPool, c->alloc);
|
||||
+ c->profPool = VK_NULL_HANDLE;
|
||||
+ }
|
||||
if (c->linearSampler) {
|
||||
vkDestroySampler(c->dev,c->linearSampler,c->alloc);
|
||||
c->linearSampler = VK_NULL_HANDLE;
|
||||
@@ -1853,6 +1961,7 @@ VkResult ffxFsr4VkRetireFrame(FfxInterface* iface, uint64_t completedFrameId)
|
||||
for (uint32_t i = 0; i < NUM_POOL_FRAMES; ++i) {
|
||||
FrameLifetime* frame = &c->frame[i];
|
||||
if (!frame->inUse || frame->frameId > completedFrameId) continue;
|
||||
+ profCollect(c, i);
|
||||
if (vkResetDescriptorPool(c->dev, c->pool[i], 0) != VK_SUCCESS)
|
||||
return VK_ERROR_DEVICE_LOST;
|
||||
memset(frame, 0, sizeof(*frame));
|
||||
--
|
||||
2.55.0
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Local changes to gpu/third_party/fsr-vulkan
|
||||
|
||||
The submodule points at commits on a local `bbport` branch that are not upstream
|
||||
(FireBurn/FSR-Vulkan). After a fresh clone, apply them on top of the upstream commit
|
||||
(`c64f093`):
|
||||
|
||||
git -C gpu/third_party/fsr-vulkan checkout -b bbport c64f093
|
||||
git -C gpu/third_party/fsr-vulkan am ../../patches/fsr-vulkan/*.patch
|
||||
@@ -119,7 +119,15 @@ struct Fsr4Upscaler::Impl {
|
||||
for (u32 pass = 0; pass < FFX_FSR4_MODEL_PASS_COUNT; ++pass) {
|
||||
if (!load(assets.model[pass], code[1 + pass])) return false;
|
||||
}
|
||||
if (!load(assets.post, code[13]) || !load(assets.rcas, code[14]) ||
|
||||
// bbport: tools/fsr4_optimize.sh puts a faster, bit-exact post pass into opt/ (stores
|
||||
// through shared memory); BB_FSR4_OPT=0 keeps the original.
|
||||
const char* opt_env = std::getenv("BB_FSR4_OPT");
|
||||
const bool opt = !(opt_env && opt_env[0] == '0') &&
|
||||
ReadFile(dir + "opt/" + assets.post, code[13]);
|
||||
if (opt) {
|
||||
std::printf("Upscaler: FSR 4 optimized post pass (%s)\n", assets.post);
|
||||
}
|
||||
if (!(opt || load(assets.post, code[13])) || !load(assets.rcas, code[14]) ||
|
||||
!load(assets.spdAutoExposure, code[15]) || !load(assets.initializer, initializer) ||
|
||||
!load(assets.prePassWeights, weights)) {
|
||||
return false;
|
||||
|
||||
@@ -220,7 +220,8 @@ bool TemporalUpscaler::Active() const {
|
||||
}
|
||||
|
||||
bool TemporalUpscaler::ReactiveOn() const {
|
||||
return BbSettings::Get().reactive && !BbToggle::Disabled(1u << 27);
|
||||
// FSR 4 takes no reactive mask: the opaque snapshot and the mask pass would be wasted.
|
||||
return BbSettings::Get().reactive && !BbToggle::Disabled(1u << 27) && !UseFsr4();
|
||||
}
|
||||
|
||||
void TemporalUpscaler::OnSceneColor(VideoCore::ImageId color) {
|
||||
|
||||
Vendored
+1
-1
Submodule gpu/third_party/fsr-vulkan updated: c64f093404...707fe318ee.
@@ -0,0 +1,524 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// bbport: FSR 4 benchmark outside the game. Runs the v07 INT8 provider (the same assets and
|
||||
// provider as vk_fsr4.cpp) on synthetic inputs and prints GPU time per pass (BB_FSR4_PROFILE)
|
||||
// and, with --stats, the driver's statistics of every pass (BB_FSR4_STATS).
|
||||
//
|
||||
// fsr4-bench [render WxH] [output WxH] [preset 0-4] [frames] [--stats]
|
||||
// defaults: 2260x1272 3840x2160 2 (balanced) 900
|
||||
// BENCH_NOISE=1: pseudo-random inputs (a fixed seed); BENCH_DUMP=<file>: the output after the
|
||||
// last frame, raw RGBA16F, for comparing shader variants.
|
||||
// Assets: BB_FSR4_DIR or fsr4_shaders in the working directory.
|
||||
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
#include "ffx_vk_fsr4_v07.h"
|
||||
#include "ffx_vk_fsr4_v07_assets.h"
|
||||
|
||||
namespace {
|
||||
|
||||
#define CHECK(call) \
|
||||
do { \
|
||||
const VkResult r_ = (call); \
|
||||
if (r_ != VK_SUCCESS) { \
|
||||
std::fprintf(stderr, "%s failed: %d\n", #call, int(r_)); \
|
||||
std::exit(1); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
bool ReadFile(const std::string& path, std::vector<unsigned char>& data) {
|
||||
std::ifstream file(path, std::ios::binary | std::ios::ate);
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
data.resize(size_t(file.tellg()));
|
||||
file.seekg(0);
|
||||
return bool(file.read(reinterpret_cast<char*>(data.data()), std::streamsize(data.size())));
|
||||
}
|
||||
|
||||
struct Gpu {
|
||||
VkInstance instance{};
|
||||
VkPhysicalDevice physical{};
|
||||
VkDevice device{};
|
||||
VkQueue queue{};
|
||||
uint32_t family = 0;
|
||||
VkPhysicalDeviceMemoryProperties memory{};
|
||||
};
|
||||
|
||||
uint32_t MemoryType(const Gpu& gpu, uint32_t bits, VkMemoryPropertyFlags flags) {
|
||||
for (uint32_t i = 0; i < gpu.memory.memoryTypeCount; ++i) {
|
||||
if ((bits & (1u << i)) && (gpu.memory.memoryTypes[i].propertyFlags & flags) == flags) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
std::fprintf(stderr, "no memory type\n");
|
||||
std::exit(1);
|
||||
}
|
||||
|
||||
Gpu CreateGpu(bool stats) {
|
||||
Gpu gpu;
|
||||
VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO};
|
||||
app.pApplicationName = "fsr4-bench";
|
||||
app.apiVersion = VK_API_VERSION_1_3;
|
||||
VkInstanceCreateInfo ici{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
|
||||
ici.pApplicationInfo = &app;
|
||||
CHECK(vkCreateInstance(&ici, nullptr, &gpu.instance));
|
||||
uint32_t count = 0;
|
||||
CHECK(vkEnumeratePhysicalDevices(gpu.instance, &count, nullptr));
|
||||
std::vector<VkPhysicalDevice> devices(count);
|
||||
CHECK(vkEnumeratePhysicalDevices(gpu.instance, &count, devices.data()));
|
||||
gpu.physical = devices.at(0);
|
||||
for (const auto device : devices) {
|
||||
VkPhysicalDeviceProperties props;
|
||||
vkGetPhysicalDeviceProperties(device, &props);
|
||||
if (props.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) {
|
||||
gpu.physical = device;
|
||||
break;
|
||||
}
|
||||
}
|
||||
VkPhysicalDeviceProperties props;
|
||||
vkGetPhysicalDeviceProperties(gpu.physical, &props);
|
||||
std::printf("GPU: %s\n", props.deviceName);
|
||||
vkGetPhysicalDeviceMemoryProperties(gpu.physical, &gpu.memory);
|
||||
|
||||
uint32_t families = 0;
|
||||
vkGetPhysicalDeviceQueueFamilyProperties(gpu.physical, &families, nullptr);
|
||||
std::vector<VkQueueFamilyProperties> family_props(families);
|
||||
vkGetPhysicalDeviceQueueFamilyProperties(gpu.physical, &families, family_props.data());
|
||||
while (gpu.family < families && !(family_props[gpu.family].queueFlags & VK_QUEUE_COMPUTE_BIT)) {
|
||||
++gpu.family;
|
||||
}
|
||||
|
||||
// Everything the device supports, as the game's device enables what FSR 4 needs.
|
||||
VkPhysicalDevicePipelineExecutablePropertiesFeaturesKHR executable{
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_EXECUTABLE_PROPERTIES_FEATURES_KHR};
|
||||
VkPhysicalDeviceComputeShaderDerivativesFeaturesKHR derivatives{
|
||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_COMPUTE_SHADER_DERIVATIVES_FEATURES_KHR};
|
||||
VkPhysicalDeviceVulkan13Features f13{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES};
|
||||
VkPhysicalDeviceVulkan12Features f12{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES};
|
||||
VkPhysicalDeviceVulkan11Features f11{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES};
|
||||
VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2};
|
||||
features.pNext = &f11;
|
||||
f11.pNext = &f12;
|
||||
f12.pNext = &f13;
|
||||
f13.pNext = &derivatives;
|
||||
if (stats) {
|
||||
derivatives.pNext = &executable;
|
||||
}
|
||||
vkGetPhysicalDeviceFeatures2(gpu.physical, &features);
|
||||
features.features.robustBufferAccess = VK_FALSE; // as the game: no robustness cost
|
||||
f13.robustImageAccess = VK_FALSE;
|
||||
std::vector<const char*> extensions{VK_KHR_COMPUTE_SHADER_DERIVATIVES_EXTENSION_NAME};
|
||||
if (stats) {
|
||||
extensions.push_back(VK_KHR_PIPELINE_EXECUTABLE_PROPERTIES_EXTENSION_NAME);
|
||||
}
|
||||
const float priority = 1.0f;
|
||||
VkDeviceQueueCreateInfo qci{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
|
||||
qci.queueFamilyIndex = gpu.family;
|
||||
qci.queueCount = 1;
|
||||
qci.pQueuePriorities = &priority;
|
||||
VkDeviceCreateInfo dci{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
|
||||
dci.pNext = &features;
|
||||
dci.queueCreateInfoCount = 1;
|
||||
dci.pQueueCreateInfos = &qci;
|
||||
dci.enabledExtensionCount = uint32_t(extensions.size());
|
||||
dci.ppEnabledExtensionNames = extensions.data();
|
||||
CHECK(vkCreateDevice(gpu.physical, &dci, nullptr, &gpu.device));
|
||||
vkGetDeviceQueue(gpu.device, gpu.family, 0, &gpu.queue);
|
||||
return gpu;
|
||||
}
|
||||
|
||||
struct Buffer {
|
||||
VkBuffer buffer{};
|
||||
VkDeviceMemory memory{};
|
||||
void* data = nullptr;
|
||||
};
|
||||
|
||||
Buffer CreateHostBuffer(const Gpu& gpu, VkDeviceSize size, VkBufferUsageFlags usage) {
|
||||
Buffer b;
|
||||
VkBufferCreateInfo ci{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
|
||||
ci.size = size;
|
||||
ci.usage = usage;
|
||||
CHECK(vkCreateBuffer(gpu.device, &ci, nullptr, &b.buffer));
|
||||
VkMemoryRequirements req;
|
||||
vkGetBufferMemoryRequirements(gpu.device, b.buffer, &req);
|
||||
VkMemoryAllocateInfo ai{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
ai.allocationSize = req.size;
|
||||
ai.memoryTypeIndex = MemoryType(gpu, req.memoryTypeBits,
|
||||
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
|
||||
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
|
||||
CHECK(vkAllocateMemory(gpu.device, &ai, nullptr, &b.memory));
|
||||
CHECK(vkBindBufferMemory(gpu.device, b.buffer, b.memory, 0));
|
||||
CHECK(vkMapMemory(gpu.device, b.memory, 0, VK_WHOLE_SIZE, 0, &b.data));
|
||||
return b;
|
||||
}
|
||||
|
||||
uint16_t Half(float f) {
|
||||
// Inputs in [0, 2): exact enough for test data (truncating conversion).
|
||||
uint32_t x;
|
||||
std::memcpy(&x, &f, 4);
|
||||
const uint32_t sign = (x >> 16) & 0x8000u;
|
||||
const int exp = int((x >> 23) & 0xffu) - 127 + 15;
|
||||
if (exp <= 0) {
|
||||
return uint16_t(sign);
|
||||
}
|
||||
return uint16_t(sign | (uint32_t(exp) << 10) | ((x >> 13) & 0x3ffu));
|
||||
}
|
||||
|
||||
struct Image {
|
||||
VkImage image{};
|
||||
VkImageView view{};
|
||||
VkDeviceMemory memory{};
|
||||
uint32_t width = 0, height = 0;
|
||||
};
|
||||
|
||||
Image CreateImage(const Gpu& gpu, VkFormat format, uint32_t w, uint32_t h, VkImageUsageFlags usage,
|
||||
VkImageAspectFlags aspect) {
|
||||
Image image;
|
||||
image.width = w;
|
||||
image.height = h;
|
||||
VkImageCreateInfo ci{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
||||
ci.imageType = VK_IMAGE_TYPE_2D;
|
||||
ci.format = format;
|
||||
ci.extent = {w, h, 1};
|
||||
ci.mipLevels = 1;
|
||||
ci.arrayLayers = 1;
|
||||
ci.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
ci.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
ci.usage = usage;
|
||||
CHECK(vkCreateImage(gpu.device, &ci, nullptr, &image.image));
|
||||
VkMemoryRequirements req;
|
||||
vkGetImageMemoryRequirements(gpu.device, image.image, &req);
|
||||
VkMemoryAllocateInfo ai{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
ai.allocationSize = req.size;
|
||||
ai.memoryTypeIndex = MemoryType(gpu, req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||
CHECK(vkAllocateMemory(gpu.device, &ai, nullptr, &image.memory));
|
||||
CHECK(vkBindImageMemory(gpu.device, image.image, image.memory, 0));
|
||||
VkImageViewCreateInfo vi{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
||||
vi.image = image.image;
|
||||
vi.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
vi.format = format;
|
||||
vi.subresourceRange = {aspect, 0, 1, 0, 1};
|
||||
CHECK(vkCreateImageView(gpu.device, &vi, nullptr, &image.view));
|
||||
return image;
|
||||
}
|
||||
|
||||
FfxFsr4ModelPreset ModelPreset(int preset) {
|
||||
switch (preset) {
|
||||
case 0:
|
||||
return FFX_FSR4_MODEL_PRESET_NATIVE_AA;
|
||||
case 1:
|
||||
return FFX_FSR4_MODEL_PRESET_QUALITY;
|
||||
case 2:
|
||||
return FFX_FSR4_MODEL_PRESET_BALANCED;
|
||||
case 3:
|
||||
return FFX_FSR4_MODEL_PRESET_PERFORMANCE;
|
||||
default:
|
||||
return FFX_FSR4_MODEL_PRESET_ULTRA_PERFORMANCE;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
uint32_t rw = 2260, rh = 1272, ow = 3840, oh = 2160;
|
||||
int preset = 2, frames = 900;
|
||||
bool stats = false;
|
||||
int positional = 0;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
if (std::strcmp(argv[i], "--stats") == 0) {
|
||||
stats = true;
|
||||
continue;
|
||||
}
|
||||
switch (positional++) {
|
||||
case 0:
|
||||
std::sscanf(argv[i], "%ux%u", &rw, &rh);
|
||||
break;
|
||||
case 1:
|
||||
std::sscanf(argv[i], "%ux%u", &ow, &oh);
|
||||
break;
|
||||
case 2:
|
||||
preset = std::atoi(argv[i]);
|
||||
break;
|
||||
case 3:
|
||||
frames = std::atoi(argv[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
setenv("BB_FSR4_PROFILE", "1", 0);
|
||||
if (stats) {
|
||||
setenv("BB_FSR4_STATS", "1", 1);
|
||||
}
|
||||
const Gpu gpu = CreateGpu(stats);
|
||||
|
||||
// Assets, as vk_fsr4.cpp loads them.
|
||||
FfxFsr4V07AssetSet assets{};
|
||||
if (!ffxFsr4V07BuildAssetSet(ModelPreset(preset), ow, oh, &assets)) {
|
||||
std::fprintf(stderr, "unsupported output size\n");
|
||||
return 1;
|
||||
}
|
||||
const char* dir_env = std::getenv("BB_FSR4_DIR");
|
||||
const std::string dir = std::string{dir_env && dir_env[0] ? dir_env : "fsr4_shaders"} + "/";
|
||||
std::array<std::vector<unsigned char>, FFX_FSR4_VK_PASS_COUNT> code;
|
||||
std::vector<unsigned char> initializer, weights;
|
||||
const auto load = [&](const char* name, std::vector<unsigned char>& data) {
|
||||
if (!ReadFile(dir + name, data)) {
|
||||
std::fprintf(stderr, "missing %s%s\n", dir.c_str(), name);
|
||||
std::exit(1);
|
||||
}
|
||||
};
|
||||
load(assets.pre, code[0]);
|
||||
for (uint32_t pass = 0; pass < FFX_FSR4_MODEL_PASS_COUNT; ++pass) {
|
||||
load(assets.model[pass], code[1 + pass]);
|
||||
}
|
||||
// As vk_fsr4.cpp: the optimized post pass from opt/ unless BB_FSR4_OPT=0.
|
||||
const char* opt_env = std::getenv("BB_FSR4_OPT");
|
||||
if (!(opt_env && opt_env[0] == '0') && ReadFile(dir + "opt/" + assets.post, code[13])) {
|
||||
std::printf("optimized post pass\n");
|
||||
} else {
|
||||
load(assets.post, code[13]);
|
||||
}
|
||||
load(assets.rcas, code[14]);
|
||||
load(assets.spdAutoExposure, code[15]);
|
||||
load(assets.initializer, initializer);
|
||||
load(assets.prePassWeights, weights);
|
||||
std::array<std::string, FFX_FSR4_VK_PASS_COUNT> entries;
|
||||
entries.fill("main");
|
||||
for (uint32_t pass = 1; pass <= FFX_FSR4_MODEL_PASS_COUNT; ++pass) {
|
||||
entries[pass] = "fsr4_model_v07_i8_pass" + std::to_string(pass);
|
||||
}
|
||||
FfxFsr4VkCreateInfo ci{};
|
||||
ci.device = gpu.device;
|
||||
ci.physicalDevice = gpu.physical;
|
||||
for (uint32_t i = 0; i < FFX_FSR4_VK_PASS_COUNT; ++i) {
|
||||
ci.shaders[i] = {reinterpret_cast<const uint32_t*>(code[i].data()), code[i].size(),
|
||||
entries[i].c_str()};
|
||||
}
|
||||
ci.modelInitializer = initializer.data();
|
||||
ci.modelInitializerSize = initializer.size();
|
||||
ci.prePassWeights = weights.data();
|
||||
ci.prePassWeightsSize = weights.size();
|
||||
std::vector<unsigned char> scratch(ffxFsr4VkGetScratchMemorySize());
|
||||
ci.scratchBuffer = scratch.data();
|
||||
ci.scratchBufferSize = scratch.size();
|
||||
FfxInterface backend{};
|
||||
CHECK(ffxFsr4VkCreateContext(&ci, &backend));
|
||||
|
||||
ffxCreateContextDescUpscale desc{};
|
||||
desc.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_UPSCALE;
|
||||
desc.flags = FFX_UPSCALE_ENABLE_HIGH_DYNAMIC_RANGE | FFX_UPSCALE_ENABLE_AUTO_EXPOSURE;
|
||||
desc.maxRenderSize = {(ow + 7) & ~7u, (oh + 7) & ~7u};
|
||||
desc.maxUpscaleSize = {(ow + 7) & ~7u, (oh + 7) & ~7u};
|
||||
ffxFsr4V07SetBackendInterface(&backend);
|
||||
ffxContext context{};
|
||||
if (ffxFsr4V07CreateContext(&context, &desc.header, nullptr) != FFX_API_RETURN_OK) {
|
||||
std::fprintf(stderr, "provider context creation failed\n");
|
||||
return 1;
|
||||
}
|
||||
ffxFsr4V07SetBackendInterface(nullptr);
|
||||
std::printf("FSR 4 %s %ux%u -> %ux%u, %d frames\n",
|
||||
ffxFsr4ModelPresetName(ModelPreset(preset)), rw, rh, ow, oh, frames);
|
||||
|
||||
const VkImageUsageFlags rw_usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT |
|
||||
VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
||||
Image color = CreateImage(gpu, VK_FORMAT_R16G16B16A16_SFLOAT, rw, rh, rw_usage,
|
||||
VK_IMAGE_ASPECT_COLOR_BIT);
|
||||
Image depth = CreateImage(gpu, VK_FORMAT_D32_SFLOAT, rw, rh,
|
||||
VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
|
||||
VK_IMAGE_ASPECT_DEPTH_BIT);
|
||||
Image motion = CreateImage(gpu, VK_FORMAT_R16G16_SFLOAT, rw, rh, rw_usage,
|
||||
VK_IMAGE_ASPECT_COLOR_BIT);
|
||||
Image output = CreateImage(gpu, VK_FORMAT_R16G16B16A16_SFLOAT, ow, oh, rw_usage,
|
||||
VK_IMAGE_ASPECT_COLOR_BIT);
|
||||
|
||||
VkCommandPoolCreateInfo pci{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
||||
pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
||||
pci.queueFamilyIndex = gpu.family;
|
||||
VkCommandPool pool;
|
||||
CHECK(vkCreateCommandPool(gpu.device, &pci, nullptr, &pool));
|
||||
VkCommandBufferAllocateInfo cai{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
|
||||
cai.commandPool = pool;
|
||||
cai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
cai.commandBufferCount = 1;
|
||||
VkCommandBuffer cmd;
|
||||
CHECK(vkAllocateCommandBuffers(gpu.device, &cai, &cmd));
|
||||
VkFenceCreateInfo fci{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
|
||||
VkFence fence;
|
||||
CHECK(vkCreateFence(gpu.device, &fci, nullptr, &fence));
|
||||
|
||||
const auto submit = [&] {
|
||||
CHECK(vkEndCommandBuffer(cmd));
|
||||
VkSubmitInfo si{VK_STRUCTURE_TYPE_SUBMIT_INFO};
|
||||
si.commandBufferCount = 1;
|
||||
si.pCommandBuffers = &cmd;
|
||||
CHECK(vkQueueSubmit(gpu.queue, 1, &si, fence));
|
||||
CHECK(vkWaitForFences(gpu.device, 1, &fence, VK_TRUE, UINT64_MAX));
|
||||
CHECK(vkResetFences(gpu.device, 1, &fence));
|
||||
};
|
||||
const VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
|
||||
|
||||
// Inputs: a mid-grey frame, far depth, small motion; all in General.
|
||||
CHECK(vkBeginCommandBuffer(cmd, &begin));
|
||||
const auto to_general = [&](const Image& image, VkImageAspectFlags aspect) {
|
||||
VkImageMemoryBarrier b{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
||||
b.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
b.newLayout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
b.srcQueueFamilyIndex = b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
b.image = image.image;
|
||||
b.subresourceRange = {aspect, 0, 1, 0, 1};
|
||||
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &b);
|
||||
};
|
||||
to_general(color, VK_IMAGE_ASPECT_COLOR_BIT);
|
||||
to_general(depth, VK_IMAGE_ASPECT_DEPTH_BIT);
|
||||
to_general(motion, VK_IMAGE_ASPECT_COLOR_BIT);
|
||||
to_general(output, VK_IMAGE_ASPECT_COLOR_BIT);
|
||||
const VkImageSubresourceRange color_range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
const VkClearColorValue grey{{0.4f, 0.35f, 0.3f, 1.0f}}, mv{{0.25f, -0.5f, 0.0f, 0.0f}};
|
||||
vkCmdClearColorImage(cmd, color.image, VK_IMAGE_LAYOUT_GENERAL, &grey, 1, &color_range);
|
||||
vkCmdClearColorImage(cmd, motion.image, VK_IMAGE_LAYOUT_GENERAL, &mv, 1, &color_range);
|
||||
const VkClearDepthStencilValue far{0.5f, 0};
|
||||
const VkImageSubresourceRange depth_range{VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1};
|
||||
vkCmdClearDepthStencilImage(cmd, depth.image, VK_IMAGE_LAYOUT_GENERAL, &far, 1, &depth_range);
|
||||
VkMemoryBarrier mb{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
mb.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
mb.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
|
||||
0, 1, &mb, 0, nullptr, 0, nullptr);
|
||||
submit();
|
||||
|
||||
const char* noise_env = std::getenv("BENCH_NOISE");
|
||||
if (noise_env && noise_env[0] == '1') {
|
||||
// Pseudo-random color, motion within +-2 pixels, depth; one upload.
|
||||
const VkDeviceSize color_bytes = VkDeviceSize(rw) * rh * 8, motion_bytes = VkDeviceSize(rw) * rh * 4,
|
||||
depth_bytes = VkDeviceSize(rw) * rh * 4;
|
||||
Buffer staging = CreateHostBuffer(gpu, color_bytes + motion_bytes + depth_bytes,
|
||||
VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
|
||||
uint32_t seed = 12345u;
|
||||
const auto next = [&] {
|
||||
seed = seed * 1664525u + 1013904223u;
|
||||
return float(seed >> 8) / float(1u << 24);
|
||||
};
|
||||
auto* bytes = static_cast<unsigned char*>(staging.data);
|
||||
auto* c16 = reinterpret_cast<uint16_t*>(bytes);
|
||||
for (VkDeviceSize i = 0; i < VkDeviceSize(rw) * rh; ++i) {
|
||||
c16[i * 4 + 0] = Half(next() * 1.5f);
|
||||
c16[i * 4 + 1] = Half(next() * 1.5f);
|
||||
c16[i * 4 + 2] = Half(next() * 1.5f);
|
||||
c16[i * 4 + 3] = Half(1.0f);
|
||||
}
|
||||
auto* m16 = reinterpret_cast<uint16_t*>(bytes + color_bytes);
|
||||
for (VkDeviceSize i = 0; i < VkDeviceSize(rw) * rh * 2; ++i) {
|
||||
const float v = next() * 4.0f - 2.0f;
|
||||
m16[i] = Half(v);
|
||||
}
|
||||
auto* d32 = reinterpret_cast<float*>(bytes + color_bytes + motion_bytes);
|
||||
for (VkDeviceSize i = 0; i < VkDeviceSize(rw) * rh; ++i) {
|
||||
d32[i] = 0.9f + next() * 0.1f;
|
||||
}
|
||||
CHECK(vkBeginCommandBuffer(cmd, &begin));
|
||||
VkBufferImageCopy region{};
|
||||
region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
region.imageExtent = {rw, rh, 1};
|
||||
vkCmdCopyBufferToImage(cmd, staging.buffer, color.image, VK_IMAGE_LAYOUT_GENERAL, 1, ®ion);
|
||||
region.bufferOffset = color_bytes;
|
||||
vkCmdCopyBufferToImage(cmd, staging.buffer, motion.image, VK_IMAGE_LAYOUT_GENERAL, 1, ®ion);
|
||||
region.bufferOffset = color_bytes + motion_bytes;
|
||||
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||
vkCmdCopyBufferToImage(cmd, staging.buffer, depth.image, VK_IMAGE_LAYOUT_GENERAL, 1, ®ion);
|
||||
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &mb, 0, nullptr, 0,
|
||||
nullptr);
|
||||
submit();
|
||||
}
|
||||
|
||||
const auto resource = [](const Image& image, uint32_t format, uint32_t state) {
|
||||
FfxApiResource r{};
|
||||
r.resource = reinterpret_cast<void*>(image.view);
|
||||
r.description.type = FFX_RESOURCE_TYPE_TEXTURE2D;
|
||||
r.description.format = format;
|
||||
r.description.width = image.width;
|
||||
r.description.height = image.height;
|
||||
r.description.depth = 1;
|
||||
r.description.mipCount = 1;
|
||||
r.state = state;
|
||||
return r;
|
||||
};
|
||||
const auto register_image = [&](const Image& image, VkAccessFlags access) {
|
||||
const FfxFsr4VkExternalImageState state{
|
||||
sizeof(FfxFsr4VkExternalImageState), image.image, image.view,
|
||||
VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, access,
|
||||
VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, access};
|
||||
CHECK(ffxFsr4VkSetExternalImageState(&backend, &state));
|
||||
};
|
||||
for (int frame = 1; frame <= frames; ++frame) {
|
||||
CHECK(ffxFsr4VkBeginFrame(&backend, uint64_t(frame)));
|
||||
CHECK(vkBeginCommandBuffer(cmd, &begin));
|
||||
register_image(color, VK_ACCESS_SHADER_READ_BIT);
|
||||
register_image(depth, VK_ACCESS_SHADER_READ_BIT);
|
||||
register_image(motion, VK_ACCESS_SHADER_READ_BIT);
|
||||
register_image(output, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT);
|
||||
ffxDispatchDescUpscale d{};
|
||||
d.header.type = FFX_API_DISPATCH_DESC_TYPE_UPSCALE;
|
||||
d.commandList = cmd;
|
||||
d.color = resource(color, FFX_SURFACE_FORMAT_R16G16B16A16_FLOAT,
|
||||
FFX_API_RESOURCE_STATE_COMPUTE_READ);
|
||||
d.depth = resource(depth, FFX_SURFACE_FORMAT_R32_FLOAT, FFX_API_RESOURCE_STATE_COMPUTE_READ);
|
||||
d.motionVectors = resource(motion, FFX_SURFACE_FORMAT_R16G16_FLOAT,
|
||||
FFX_API_RESOURCE_STATE_COMPUTE_READ);
|
||||
d.output = resource(output, FFX_SURFACE_FORMAT_R16G16B16A16_FLOAT,
|
||||
FFX_API_RESOURCE_STATE_UNORDERED_ACCESS);
|
||||
d.jitterOffset = {0.25f * float(frame % 4) - 0.375f, 0.125f};
|
||||
d.motionVectorScale = {1.0f, 1.0f};
|
||||
d.renderSize = {rw, rh};
|
||||
d.upscaleSize = {ow, oh};
|
||||
d.enableSharpening = true;
|
||||
d.sharpness = 0.5f;
|
||||
d.enableAutoExposure = true;
|
||||
d.frameTimeDelta = 10.0f;
|
||||
d.preExposure = 1.0f;
|
||||
d.reset = frame == 1;
|
||||
d.cameraNear = 0.05f;
|
||||
d.cameraFar = 3000.0f;
|
||||
d.cameraFovAngleVertical = 0.75f;
|
||||
d.viewSpaceToMetersFactor = 1.0f;
|
||||
if (ffxFsr4V07Dispatch(&context, &d.header) != FFX_API_RETURN_OK) {
|
||||
std::fprintf(stderr, "dispatch failed\n");
|
||||
return 1;
|
||||
}
|
||||
submit();
|
||||
CHECK(ffxFsr4VkRetireFrame(&backend, uint64_t(frame)));
|
||||
}
|
||||
CHECK(vkDeviceWaitIdle(gpu.device));
|
||||
if (const char* dump = std::getenv("BENCH_DUMP")) {
|
||||
const VkDeviceSize bytes = VkDeviceSize(ow) * oh * 8;
|
||||
Buffer readback = CreateHostBuffer(gpu, bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT);
|
||||
CHECK(vkBeginCommandBuffer(cmd, &begin));
|
||||
VkMemoryBarrier rb{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
rb.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
|
||||
rb.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
||||
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &rb, 0, nullptr, 0, nullptr);
|
||||
VkBufferImageCopy region{};
|
||||
region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
region.imageExtent = {ow, oh, 1};
|
||||
vkCmdCopyImageToBuffer(cmd, output.image, VK_IMAGE_LAYOUT_GENERAL, readback.buffer, 1,
|
||||
®ion);
|
||||
submit();
|
||||
if (FILE* f = std::fopen(dump, "wb")) {
|
||||
std::fwrite(readback.data, 1, size_t(bytes), f);
|
||||
std::fclose(f);
|
||||
}
|
||||
}
|
||||
ffxFsr4V07DestroyContext(&context, nullptr);
|
||||
ffxFsr4VkDestroyContext(reinterpret_cast<FfxFsr4VkContext*>(scratch.data()));
|
||||
return 0;
|
||||
}
|
||||
@@ -66,6 +66,10 @@ fi
|
||||
if [[ -z ${BB_VBLANK_HZ:-} ]]; then
|
||||
case $fps in uncap) export BB_VBLANK_HZ=0 ;; 90) export BB_VBLANK_HZ=90 ;; *) export BB_VBLANK_HZ=60 ;; esac
|
||||
fi
|
||||
# FSR 4: faster post passes next to the downloaded ones (incremental; tools/fsr4_optimize.sh).
|
||||
if [[ -z ${BB_PREBUILT:-} && -d fsr4_shaders ]] && command -v spirv-cross >/dev/null; then
|
||||
bash tools/fsr4_optimize.sh || echo 'FSR 4: optimized post passes not built' >&2
|
||||
fi
|
||||
if [[ -n ${BB_PREBUILT:-} ]]; then
|
||||
probe=${BB_PROBE:-bin/bb-probe}
|
||||
else
|
||||
|
||||
@@ -5,6 +5,6 @@ pkgs.mkShell {
|
||||
gcc gnumake cmake ninja pkg-config python3 binutils
|
||||
vulkan-headers vulkan-loader sdl3
|
||||
# GPU library (gpu/): shadPS4 video core dependencies
|
||||
ffmpeg-headless boost fmt magic-enum robin-map xxhash vulkan-memory-allocator glslang xbyak zydis spirv-headers miniz libx11 libxcb xorgproto wayland
|
||||
ffmpeg-headless boost fmt magic-enum robin-map xxhash vulkan-memory-allocator glslang spirv-cross xbyak zydis spirv-headers miniz libx11 libxcb xorgproto wayland
|
||||
];
|
||||
}
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
# tools/fsr4_optimize.sh: builds RDNA3-friendly variants of the FSR 4 v07 post passes into
|
||||
# fsr4_shaders/opt/ (vk_fsr4.cpp prefers them; BB_FSR4_OPT=0 uses the originals). Each post pass
|
||||
# is decompiled (spirv-cross), rewritten by fsr4_post_lds.pl (stores through shared memory,
|
||||
# bit-exact) and compiled again (glslang). Needs spirv-cross and glslangValidator (shell.nix).
|
||||
# tools/fsr4_verify.sh checks every variant against the original with the benchmark.
|
||||
set -euo pipefail
|
||||
cd -- "$(dirname -- "$0")/.."
|
||||
src=${BB_FSR4_DIR:-fsr4_shaders}
|
||||
dest=$src/opt
|
||||
mkdir -p "$dest"
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
count=0
|
||||
for spv in "$src"/fsr4_model_v07_i8_*_post.spv; do
|
||||
name=$(basename "$spv")
|
||||
out=$dest/$name
|
||||
# Rebuilt when the original or the rewrite changed.
|
||||
if [[ -s $out && $out -nt $spv && $out -nt tools/fsr4_post_lds.pl ]]; then continue; fi
|
||||
spirv-cross "$spv" --vulkan-semantics --output "$tmp/post.comp"
|
||||
perl tools/fsr4_post_lds.pl < "$tmp/post.comp" > "$tmp/post_lds.comp"
|
||||
glslangValidator -V --target-env vulkan1.3 -S comp "$tmp/post_lds.comp" -o "$tmp/out.spv" >/dev/null
|
||||
mv "$tmp/out.spv" "$out"
|
||||
count=$((count + 1))
|
||||
done
|
||||
echo "FSR 4 optimized post passes: $count built in $dest"
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env perl
|
||||
# fsr4_post_lds.pl < post.comp > post_lds.comp
|
||||
# bbport: rewrites a decompiled FSR 4 v07 post pass (spirv-cross GLSL) for RDNA3.
|
||||
#
|
||||
# Each invocation computes a 2x2 block of output pixels and stores them into three images
|
||||
# (recurrent state, history, output). Stores of every other pixel from three images cost ~4x
|
||||
# the arithmetic of the pass (2.9 of 6.1 ms at 2260x1272 -> 3840x2160 on an RX 7800 XT). Here
|
||||
# the 8x8 workgroup collects its 16x16 block in shared memory and stores it in contiguous rows.
|
||||
# Values stay float in shared memory and are converted by the store as before (a half value
|
||||
# from shared memory would become a 16-bit store, which rounds differently), so the result is
|
||||
# bit-exact with the original pass.
|
||||
#
|
||||
# spirv-cross also translates OpBitcast uint -> i8vec4 + OpSConvert as unpack8(uint), which is
|
||||
# unsigned; the residual input of the pass is signed. unpack8(int(x)) restores that.
|
||||
use strict;
|
||||
use warnings;
|
||||
local $/;
|
||||
my $src = <STDIN>;
|
||||
die "unexpected local size\n"
|
||||
unless $src =~ /layout\(local_size_x = 8, local_size_y = 8, local_size_z = 1\) in;/;
|
||||
|
||||
my $signed = ($src =~ s/ivec4\(unpack8\((_\d+)\)\)/ivec4(unpack8(int($1)))/g);
|
||||
die "expected the signed residual unpacks\n" unless $signed;
|
||||
|
||||
my %slot = (rw_recurrent_0 => ['bb_rec', 'vec4'], rw_history_color => ['bb_hist', 'vec4'],
|
||||
rw_mlsr_output_color => ['bb_out', 'vec4']);
|
||||
for my $image (sort keys %slot) {
|
||||
my ($array, $type) = @{$slot{$image}};
|
||||
my $n = ($src =~ s/imageStore\($image, ivec2\((_\d+)\), /$array\[bbLocal($1)\] = $type(/g);
|
||||
die "expected one store to $image, found $n\n" unless $n == 1;
|
||||
}
|
||||
my $decl = <<'GLSL';
|
||||
// bbport: the workgroup's 16x16 output block, stored in contiguous rows after the loop.
|
||||
shared vec4 bb_rec[256];
|
||||
shared vec4 bb_hist[256];
|
||||
shared vec4 bb_out[256];
|
||||
uint bbLocal(uvec2 p)
|
||||
{
|
||||
uvec2 l = p - gl_WorkGroupID.xy * 16u;
|
||||
return l.y * 16u + l.x;
|
||||
}
|
||||
|
||||
void main()
|
||||
GLSL
|
||||
$src =~ s/^void main\(\)\n/$decl/m or die "no main\n";
|
||||
my $flush = <<'GLSL';
|
||||
barrier();
|
||||
uvec2 bbBase = gl_WorkGroupID.xy * 16u;
|
||||
for (uint bbK = 0u; bbK < 4u; bbK++)
|
||||
{
|
||||
uint bbI = gl_LocalInvocationIndex + 64u * bbK;
|
||||
ivec2 bbP = ivec2(bbBase + uvec2(bbI % 16u, bbI / 16u));
|
||||
imageStore(rw_recurrent_0, bbP, bb_rec[bbI]);
|
||||
imageStore(rw_history_color, bbP, bb_hist[bbI]);
|
||||
imageStore(rw_mlsr_output_color, bbP, bb_out[bbI]);
|
||||
}
|
||||
}
|
||||
GLSL
|
||||
# main is the last function: its closing brace ends the file.
|
||||
$src =~ s/\n}\s*\z/\n$flush/ or die "no end of main\n";
|
||||
print $src;
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# tools/fsr4_verify.sh [frames]: runs the FSR 4 benchmark (out/gpu/fsr4-bench, pseudo-random
|
||||
# inputs) for every preset and output size with the original and the optimized post pass
|
||||
# (tools/fsr4_optimize.sh), checks that the outputs are bit-exact and prints both times.
|
||||
set -euo pipefail
|
||||
cd -- "$(dirname -- "$0")/.."
|
||||
frames=${1:-12}
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
fail=0
|
||||
time_of() { grep -E '^ +[0-9.]+ ms/frame post$' "$1" | tail -1 | awk '{print $1}'; }
|
||||
for out in 1920x1080 2560x1440 3840x2160; do
|
||||
ow=${out%x*}; oh=${out#*x}
|
||||
preset=0
|
||||
for ratio in 1.0 1.5 1.7 2.0 3.0; do
|
||||
render=$(awk -v w="$ow" -v h="$oh" -v r="$ratio" 'BEGIN { printf "%dx%d", int(w / r + 0.5), int(h / r + 0.5) }')
|
||||
for run in 0 0b 1; do
|
||||
BB_FSR4_OPT=${run%b} BENCH_NOISE=1 BENCH_DUMP=$tmp/$run.raw \
|
||||
out/gpu/fsr4-bench "$render" "$out" $preset "$frames" > "$tmp/$run.log" 2>&1
|
||||
done
|
||||
for opt in 0 1; do
|
||||
BB_FSR4_OPT=$opt out/gpu/fsr4-bench "$render" "$out" $preset 300 > "$tmp/t$opt.log" 2>&1
|
||||
done
|
||||
# The original is not deterministic at some sizes (1080 tier: a strip at the left edge);
|
||||
# then the optimized pass may differ from it by no more than it differs from itself.
|
||||
if cmp -s "$tmp/0.raw" "$tmp/1.raw"; then
|
||||
result=bit-exact
|
||||
else
|
||||
self=$(cmp -l "$tmp/0.raw" "$tmp/0b.raw" | wc -l || true)
|
||||
other=$(cmp -l "$tmp/0.raw" "$tmp/1.raw" | wc -l || true)
|
||||
if (( self > 0 && other <= self * 3 / 2 )); then
|
||||
result="original nondeterministic ($self bytes differ between its runs, $other vs optimized)"
|
||||
else
|
||||
result="DIFFERS ($other bytes; original vs itself $self)"; fail=1
|
||||
fi
|
||||
fi
|
||||
printf '%-9s preset %d %-9s post %s -> %s ms %s\n' "$out" $preset "$render" \
|
||||
"$(time_of "$tmp/t0.log")" "$(time_of "$tmp/t1.log")" "$result"
|
||||
preset=$((preset + 1))
|
||||
done
|
||||
done
|
||||
exit $fail
|
||||
Reference in new issue
Block a user