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:
deadinsideandClaude Opus 5.5 committed 2026-10-01 05:31:22 +03:00
1 parent 10f3a4a027
commit 86dea1a25a
14 files changed
+930 -4

No files matched your search

+34
View File
@@ -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 её не принимает).
+4
View File
@@ -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
+4
View File
@@ -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)
@@ -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
+8
View File
@@ -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) {
+524
View File
@@ -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, &region);
region.bufferOffset = color_bytes;
vkCmdCopyBufferToImage(cmd, staging.buffer, motion.image, VK_IMAGE_LAYOUT_GENERAL, 1, &region);
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, &region);
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,
&region);
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;
}
+4
View File
@@ -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
+1 -1
View File
@@ -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
];
}
+26
View File
@@ -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"
+61
View File
@@ -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;
+42
View File
@@ -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