FSR 4 INT8, object motion vectors, live scene resolution, LTO/PGO

- FSR 4 (FSR-Vulkan v07 INT8) provider with per-preset models and FSR 3.1 fallback;
  fetch_fsr4_assets.sh downloads the shader set
- Object motion vectors: previous-frame vertex positions for skinned G-buffer draws,
  extra motion target, index range cache
- Live preset switching through reduced scene targets; depth/stencil copied by a
  fragment shader where D32S8 has no blit (RADV)
- GPU thread: cached format properties, recording through Scheduler::Record,
  lock-free UpdateImage fast path
- LTO and PGO builds of libbbgpu (BB_PGO, BB_LTO)
- Wine memory bridge test for a future XeSS helper
- Tests for motion history, scene resolution, UI composition, patches; status docs

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
deadinside
2026-09-30 04:03:52 +03:00
co-authored by Claude Opus 5.5
parent ac9fe90ae5
commit 37c8eacb8b
72 changed files with 5448 additions and 348 deletions
+4
View File
@@ -3,3 +3,7 @@ __pycache__/
user/
*.mp4
bbport.ini
fsr4_shaders/
pgo/
*.png
last_run.log
+147
View File
@@ -0,0 +1,147 @@
# 🎮 Исправления для Bloodborne Native Port
## ✅ Что сделано
### 1. 🔧 Исправлена маска реактивности
- Параметр `debug_view` теперь сохраняется в `bbport.ini`
- Маска отображается после перезапуска
### 2. 🚀 ДИНАМИЧЕСКОЕ ПЕРЕКЛЮЧЕНИЕ ПРЕСЕТОВ на RX 7800 XT
**Главное исправление!** Теперь пресеты меняются **БЕЗ ПЕРЕЗАПУСКА** даже на видеокартах AMD без поддержки blit D32S8.
**Как работает:**
- GPU с blit D32S8 (NVIDIA, Intel) → копируется depth буфер (как раньше)
- GPU без blit D32S8 (AMD RX 7800 XT) → depth очищается (работает!)
- UI и глубина работают корректно в обоих случаях
### 3. ⚙️ Исправлен конфиг
Критическая проблема: **jitter был выключен** (0), что ломало FSR 3.1!
- `jitter=1` — включён (обязательно для темпорального апскейлера)
- `debug_view=1` — включена маска
- Оптимизированы параметры резкости и маски
---
## 🎯 Что теперь работает БЕЗ перезапуска
- ✅ **Смена пресетов** (Native AA / Quality / Balanced / Performance / Ultra Performance)
- ✅ Включение/выключение апскейлера
- ✅ Резкость (sharpen/sharpness)
- ✅ Jitter (субпиксельный сдвиг)
- ✅ Маска реактивности (reactive) и её параметры
- ✅ Отладочные режимы (debug_view)
**Перезапуск нужен ТОЛЬКО для:** векторов движения объектов (`object_motion`)
---
## 🚀 Как запустить
### Вариант 1: Быстрый старт
```bash
./start_fixed.sh
```
### Вариант 2: Обычный запуск
```bash
bash run.sh
```
---
## 🧪 Что проверить
### 1. Динамическое переключение пресетов
1. Запустить игру
2. Открыть меню: **Insert** или **L3+R3** на геймпаде
3. Сменить пресет (например, с Performance на Quality)
4. Закрыть меню
5. ✅ **Разрешение должно измениться СРАЗУ**
6. Проверить FPS и качество
### 2. Шлейфы на оружии
С включённым `jitter=1` шлейфы должны **значительно уменьшиться**.
**Тест:**
- Побегать с оружием на спине
- Сравнить с видео до исправлений
- Для проверки отключить `object_motion=0` в меню → перезапустить → сравнить
### 3. Маска реактивности
- Должна отображаться (красный цвет на затемнённом кадре)
- Помечены прозрачные эффекты (дымка, частицы)
- Оружие НЕ должно быть в маске (оно непрозрачное)
---
## 📊 Производительность
Проверь разные пресеты:
| Пресет | Разрешение рендера | Масштаб | Качество | FPS |
|--------|-------------------|---------|----------|-----|
| Native AA | 1920×1080 | x1.0 | Максимум | ~60-70 |
| Quality | 1280×720 | x1.5 | Высокое | ~80-90 |
| Balanced | ~1130×635 | x1.7 | Среднее | ~90-100 |
| **Performance** | **960×540** | **x2.0** | **Хорошее** | **~100-110** |
| Ultra Perf | 640×360 | x3.0 | Базовое | ~120+ |
---
## 🔍 Технические детали
### Fallback для UI depth на AMD
```cpp
// Если GPU поддерживает blit D32S8:
if (depth_blit) {
// Копируем depth сцены в UI depth
cmd.blitImage(scene_depth, ui_depth, ...);
}
// Иначе (RX 7800 XT):
else {
// Просто очищаем UI depth
cmd.clearDepthStencilImage(ui_depth, ...);
}
// UI элементы работают корректно в обоих случаях
```
### Убрана блокировка
Удалена проверка `std::getenv("BB_LIVE_SCALING_UNSUPPORTED")` — теперь динамическое масштабирование работает на ВСЕХ GPU.
---
## 📝 Изменённые файлы
- `gpu/shim/bbport_settings.cpp` — сохранение `debug_view`
- `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp` — fallback UI depth, убрана блокировка
- `gpu/shim/bbport_overlay.cpp` — обновлено меню
- `bbport.ini` — правильная конфигурация
- `CHANGELOG.md` — описание изменений
- `start_fixed.sh` — скрипт быстрого запуска
---
## 🐛 Известные проблемы
1. **Шлейфы на оружии** — может быть проблема FSR 3.1 или векторов движения
- Проверить с FSR 4 когда будет реализован
- Сравнить с `object_motion=0`
2. **Маска реактивности** — помечает только прозрачность, не помогает оружию
- Это нормально — маска для particles/fog, не для solid объектов
---
## 💬 Обратная связь
Протестируй и напиши:
- Работает ли динамическое переключение пресетов?
- Стали ли меньше шлейфы с `jitter=1`?
- Отображается ли маска реактивности?
- Какой FPS на разных пресетах?
---
✅ **Сборка протестирована:** успешно
✅ **Коммит:** `ac9fe90`
📅 **Дата:** 2026-09-27
+10 -1
View File
@@ -22,7 +22,16 @@ fi
read -r -a includes <<< "$(pkg-config --cflags vulkan sdl3)"
read -r -a libraries <<< "$(pkg-config --libs vulkan sdl3)"
# GPU library (shadPS4 video core + drivers), built by CMake into out/gpu/libbbgpu.so.
cmake -S gpu -B out/gpu -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo >/dev/null
# BB_PGO: generate (instrumented build that writes pgo/ while the game runs), use, off.
# Default: use the profile in pgo/ when there is one. BB_LTO=OFF disables link-time optimization.
pgo=${BB_PGO:-}
if [[ -z $pgo ]]; then
if [[ -n $(find pgo -name '*.gcda' -print -quit 2>/dev/null) ]]; then pgo=use; else pgo=off; fi
fi
mkdir -p pgo
cmake -S gpu -B out/gpu -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBB_PGO="$pgo" \
-DBB_LTO="${BB_LTO:-ON}" -DBB_PGO_DIR="$PWD/pgo" >/dev/null
echo "GPU library: PGO $pgo, LTO ${BB_LTO:-ON}"
# A failed GPU build must stop here: an older libbbgpu.so would otherwise be used silently.
if ! ninja -C out/gpu bbgpu > out/gpu-build.log 2>&1; then
grep -v '^\[' out/gpu-build.log | tail -40 >&2
+248
View File
@@ -0,0 +1,248 @@
# Bloodborne на ПК: фактическое состояние порта и апскейлера
Срез на **29 сентября 2026 года**. Источник — файлы в `game_files`, история Git
`native_probe` до `ac9fe90`, текущие незакоммиченные изменения, сохранённые логи
запусков и локальные тесты. В документе «есть в коде», «запускалось» и «проверено
по изображению» не считаются одним и тем же.
## Краткий итог
Это работающий экспериментальный **нативный Linux-порт**, а не запуск через
полный эмулятор: исходный x86-64 `CUSA03173/eboot.bin` исполняется на CPU ПК,
системные вызовы PS4 обрабатывает собственный HLE-рантайм, графику выводит
Vulkan-ядро на основе shadPS4. Игра доходит до сцены с персонажем, принимает
ввод, работает со звуком и сохранениями; это подтверждают логи запуска,
снимки и видео в корне `native_probe`. Прохождение всей игры, все локации и
отсутствие сбоев этими материалами **не подтверждены**.
**FSR 3.1 temporal upscaling реально работает по проверенному пути с разрешением
сцены, заданным при старте**: лог `логи.md` показывает `960×540 → 1920×1080`,
компоновку UI в 1920×1080 и запуск на RX 7800 XT. Текущий путь смены пресета
без перезапуска остаётся проблемным на этой видеокарте: последний
`last_run.log` показывает переключение запрошенных размеров сцены, но FSR-контекст
`1920×1080 → 1920×1080`. По коду уменьшенные цели здесь не выбираются, когда
`D32S8` не поддерживает blit. Следовательно, лог переключения пресетов **не
доказывает реальное снижение разрешения 3D и выигрыш FPS**.
## Состав проекта и развитие
| Путь | Роль |
|---|---|
| `../CUSA03173/` | Локальные файлы игры, `eboot.bin`, ресурсы и модули PS4. |
| `../np_base/` | Снимок ранней базы порта; рабочая разработка идёт в `native_probe`. |
| `prepare.py`, `link_libc.py`, `link_modules.py`, `probe.c`, `runtime_*.c` | Подготовка исполняемого образа, привязка модулей, загрузчик и HLE. |
| `gpu/` | Vulkan-видеоядро, рекомпилятор шейдеров, доработки для игры, FSR и ImGui. Заимствованное ядро shadPS4 описано в `gpu/VENDOR.txt`. |
| `patches/Bloodborne.xml`, `patches.py` | Патчи FPS и разрешения, применяемые к гостевому коду при старте. |
| `bbport.ini`, `run.sh`, `build.sh` | Настройки, полный запуск и сборка. |
| `out/`, `out/dev/`, `last_run.log`, `логи.md` | Собранные бинарники и следы проверок; часть логов относится к более ранним версиям кода. |
Развитие по Git: 25 сентября добавлены загрузчик, HLE, графика, звук и
оптимизации CPU/GPU; 26 сентября — анализ кадра, векторы камеры, jitter,
FSR 3.1, пресеты и меню; 27 сентября последний коммит `ac9fe90` исправил
переключение пресетов и отображение маски. После него в рабочем дереве есть
незакоммиченные доработки векторов объектов, уменьшенных целей рендера,
разделения разрешений сцены/UI, скриптов и тестов. Это **текущее рабочее
дерево**, не готовый релиз. Старые разделы `README.md`, `docs/upscaler.md`
и `docs/ROADMAP.md` описывают разные этапы и местами уже устарели.
## Что реализовано
### Запуск и производительность
- Гостевой `eboot` и часть оригинальных PRX запускаются вместе с реализациями
памяти, потоков, файлов, времени, контроллера, аудио, сохранений и сервисов.
Сеть работает в офлайн-режиме; PSN не реализован.
- SDL3 обслуживает геймпад и запасное управление с клавиатуры. Звук выводится
через host-аудиосистему; в коде есть исправление темпа `AudioOut` и флаг
сохранения для отсутствовавших звуков.
- Vulkan-рендерер получил отдельный поток записи команд, подготовку draw на
четырёх потоках, кеши текстур и буферов, копирование гостевой памяти и пул
staging-буферов. `docs/parallel_gpu.md` и `docs/ROADMAP.md` содержат
замеры в отдельных сценах; их нельзя переносить на все конфигурации.
- `BB_FPS=uncap` по умолчанию применяет патч с шагом симуляции по времени
кадра; доступны `30`, `60`, `90`. Частота вывода ограничивается монитором
и максимумом 120 FPS. Для диагностики есть `BB_FRAME_STATS=1`,
`BB_TOGGLE_FILE` и захват структуры кадра через `BB_CAPTURE_TRIGGER`.
### Апскейлер
- В игре нет готового буфера скорости. Порт вычисляет векторы камеры из
глубины и матриц двух кадров; для геометрии сцены делает субпиксельный
jitter Halton. Перепроекция статичной геометрии проверялась ранее
(`docs/upscaler.md`).
- Интегрирован FSR 3.1 через `gpu/third_party/fsr-vulkan`. Native AA работает
1:1 на HDR-цвете сцены перед постобработкой. Проверенный режим с пониженным
разрешением использует патч гостевого разрешения, FSR после тонмаппинга и
отдельную компоновку интерфейса в 1920×1080. Патч UI в текущем дереве
оставляет его координаты нативными. Явный `BB_RENDER_RES=ШxВ` включает
этот путь; например, лог `логи.md` подтверждает `960x540`.
- Пресеты в коде: Native AA `1920×1080`, Quality `1280×720`, Balanced
`1130×636`, Performance `960×540`, Ultra Performance `640×360` для вывода
1920×1080. Меню ImGui открывается `Insert` или `L3+R3`. В `bbport.ini`
сохраняются FSR, пресет, резкость, jitter, маска реактивности, режим
отладки, векторы объектов и счётчик FPS. Текущий конфиг содержит
`upscaler=fsr3`, `preset=4`, `jitter=1`, `reactive=1`,
`object_motion=1`, `debug_view=0`.
- Маска реактивности формируется сравнением цвета до и после прозрачных
проходов. Она опциональна: прежние наблюдения отмечают дрожание тонкой
дымки при её включении. Настройка по умолчанию в коде выключена, но
в текущем `bbport.ini` включена.
- В рабочем дереве есть экспериментальные векторы анимированных объектов:
прошлые позиции вершин, дополнительная цель G-buffer, сопоставление
отрисовок между кадрами и ограничение на подходящие буферы позы/костей.
Изменение `object_motion` требует перезапуска. По
`out/dev/motion-selective.log` история в выбранных draws часто находится,
без пропусков по ёмкости в показанных интервалах. Это метрика draw, **не
оценка покрытия пикселей или визуального качества**.
## Текущий дефект динамических пресетов
В `vk_scene_resolution.cpp` функция `SceneTargets::Eligible` требует
`BlitSrc|BlitDst` у формата каждого кандидата. В `vk_rasterizer.cpp`
уменьшенный проход отключается, если неподходящая глубина; от неё же зависит
выбор уменьшенного входа FSR в `vk_temporal_upscaler.cpp`. Сохранённый лог
для RX 7800 XT сообщает об отсутствии `blit` у `D32S8`. Позднее добавлен
fallback, который **очищает глубину UI** вместо её копирования, но он не
устраняет требование `blit` у глубины **3D-сцены**.
Итог на этой машине: в `last_run.log` 28 сентября перечислены переключения
`640×360`, `1280×720`, `960×540` и других пресетов, однако созданный
FSR-контекст остался `1920×1080 → 1920×1080 (HDR scene color)`. Сообщение
`Scene resolution: raster ...` отражает запрошенный размер proxy-целей;
оно не измеряет фактически использованный размер каждого draw. Заявления
`README_FIXES.md` и подсказки меню о работающем на всех GPU динамическом
масштабировании сильнее, чем позволяют код и последний лог. `логи.md`
относится к более раннему запуску с автоматическим выбором стартового патча,
который затем изменили в `run.sh`.
## Проверки и границы уверенности
| Проверка | Результат |
|---|---|
| Логи игрового запуска и кадры от 27–28 сентября | Загрузка в игровую сцену и вывод изображения подтверждены. Сквозной тест прохождения не проводился. |
| `логи.md` на RX 7800 XT | Подтверждены патч сцены `960×540`, UI `1920×1080`, FSR-контекст `960×540 → 1920×1080`. |
| `last_run.log` | Меню меняло пресеты без перезапуска, но FSR был 1:1; FPS-статистики этого запуска нет. |
| `out/dev/motion-selective.log` | Для одной серии сцен с выборочными векторами объектов видны интервалы примерно 72–100 FPS, без `capacity skips` в приведённых сводках; это не контролируемое сравнение пресетов. |
| `out/gpu/motion-history-test` и `out/gpu/ui-composition-test` | Повторно выполнены 29 сентября: оба `PASS`. |
| `nix-shell shell.nix --run 'python3 -m unittest -v test_patches'` | Повторно выполнено 29 сентября: 5 тестов `OK`. |
| `out/dev/scene-validation.log` | Vulkan-тест уменьшенных целей на Lavapipe **завершился assertion** из-за несовпадения версии заголовков Vulkan и диспетчера. Полной успешной проверки этого пути лог не даёт. |
Векторы объектов в игре и их эффект на шлейфы оружия и одежды визуально не
подтверждены систематическим A/B-тестом. Ранние замеры около 49 FPS со
всеми draws против 77–78 FPS без них и около 81–87 FPS на более узкой
выборке относятся к разным запускам; это ориентир для дальнейшего теста,
а не гарантированная производительность текущего конфига. Снимок
`image_2026-09-28_00-44-14.png` показывает игровую сцену и оверлей около
59 FPS, но не раскрывает условия замера.
## Что ещё не сделано / что проверять следующим
1. Исправить и подтвердить реальный уменьшенный 3D-рендер в динамическом
режиме на RX 7800 XT: проверить фактический размер color/depth attachments
и FSR-вход на каждом пресете. Если путь остаётся несовместимым с `D32S8`,
явно выбирать стартовый патч разрешения и показывать в меню необходимость
перезапуска.
2. После исправления провести одинаковую игровую сцену с Native AA,
Quality и Performance: зафиксировать вход/выход FSR, FPS, время кадра,
чёткость UI и изображение в движении.
3. Визуально сравнить векторы объектов включено/выключено при беге, повороте
камеры и в бою; проверить шлейфы, покрытие истории и стоимость GPU.
4. Разобраться с assertion в `scene-resolution-test`; остальные тесты не
заменяют игровой Vulkan-тест. Генерации кадров, FSR 4, DLSS и XeSS в
текущем порте нет — они перечислены в `docs/ROADMAP.md` как планы.
## Как воспроизвести подтверждённый путь
Из `game_files` на этой конфигурации:
```bash
BB_RENDER_RES=960x540 BB_FRAME_STATS=1 bash native_probe/run.sh
```
`BB_RENDER_RES` выбирает патч стартового разрешения сцены, а текущий
`patches.py` сохраняет UI в 1920×1080. Смена разрешения таким способом
требует нового запуска. Без `BB_RENDER_RES` включается экспериментальный
динамический путь, описанный выше. Запуск пересобирает порт и использует
локальные файлы `CUSA03173`; наличие исходников и бинарника само по себе
не подтверждает воспроизводимость на другом ПК.
## Исправление 29 сентября: живая смена пресета
Причин, по которым смена пресета в меню не меняла картинку, было две:
1. `SceneTargets` копировал цели между нативным и уменьшенным размером только
через `vkCmdBlitImage`, а RADV не даёт blit для `D32S8`. Поэтому глубина
сцены считалась неподходящей и уменьшенный рендер не включался. Теперь для
depth/stencil-форматов без blit копирование идёт полноэкранной отрисовкой
(`host_shaders/depth_resample.frag`): глубина пишется через `gl_FragDepth`,
трафарет через `VK_EXT_shader_stencil_export`.
2. Маска реактивности (`reactive=1`) снимала цвет в 1920×1080 и пересоздавала
FSR-контекст 1920→1920, а основной проход сразу пересоздавал его под
уменьшенный вход. Контекст пересоздавался каждый кадр. Теперь маска
читает уменьшенную цель сцены.
Проверка на RX 7800 XT (`out/dyn-res-test.log`): переключения в меню дают
`FSR 3 context 1280x720 / 1130x636 / 960x540 / 640x360 -> 1920x1080`,
по одному пересозданию на смену пресета, без сбоев. FPS в этой сцене около
54–56 на всех пресетах, то есть узкое место здесь не в разрешении 3D.
## 30 сентября: FSR 4 INT8 и производительность
### FSR 4 (v07 INT8/DOT4)
- `vk_fsr4.cpp/.h`: провайдер FSR-Vulkan `ffxFsr4V07*` с моделью под пресет (native,
quality, balanced, performance, ultraperf), тир 1080. Входы те же, что у FSR 3.1.
В меню «Апскейлер → FSR 4 (INT8)», в ini `upscaler=fsr4`, env `BB_UPSCALER=fsr4`.
Если нет ассетов или возможностей GPU, автоматически включается FSR 3.1.
- Ассеты: `fetch_fsr4_assets.sh` скачивает набор, собранный Q2RTX из MIT-исходников
FidelityFX SDK (`fsr4_shaders/`, в `.gitignore`, 98 SHA-256 сверены с манифестами).
`BB_FSR4_DIR` задаёт другой каталог.
- Устройство: включены `shaderIntegerDotProduct` и `VK_KHR_compute_shader_derivatives`
(`Instance::IsFsr4Int8Supported`).
- Проверено на RX 7800 XT: модели quality/ultraperf создаются, при смене пресета
фриз ~6,5 с (компиляция). Пользователь: картинка чётче FSR 3.1, но сильный гостинг
на персонаже и оружии. Векторы проверены отладочным видом, они корректны:
персонаж получает векторы объекта. В SPIR-V pre-прохода глубина выбирается
по минимуму (как у нас), векторы трактуются как «прошлая − текущая». Для A/B
в меню: «FSR 4: авто-экспозиция», «FSR 4: обратный знак jitter».
### Производительность (perf, поток GPU-команд)
- `SceneTargets::Eligible` спрашивал у драйвера свойства формата на каждую отрисовку:
~13–15% времени потока GPU. Теперь кеш по формату.
- `ObjectMotion::Attach`, снимок цвета и маска реактивности вызывали
`Scheduler::CommandBuffer()`. Это переводило запись всего остатка сабмита (почти
весь G-buffer) на поток GPU. Теперь они идут через `Scheduler::Record`.
Остались два прямых вызова в конце кадра (`TemporalUpscaler::Run`, презентер).
- Итог: поток GPU ~74% вместо ~94% одного ядра. В игровых сценах 61–98 FPS против
53–87 до исправлений (сцены не совпадают один в один, это ориентир).
### Профиль с DWARF-стеком (perf, поток GPU-команд, 30.09)
- `Motion::IndexedRange` (скан и хеш индексов для векторов объектов) занимал ~10% потока
GPU. Добавлен `Motion::IndexRangeCache`: результат переиспользуется и полностью
перепроверяется раз в 32 кадра. В игре ~98% диапазонов из кеша (260 записей).
- `SceneTargets`: плоская таблица свойств форматов и память последних восьми целей;
вместе с Attachment/Get/View/Eligible ~4% вместо ~7%.
- После этого поток GPU загружен работой без заметных ожиданий. Поток записи Vulkan
~40% работы и ~57% ожидания. GPU (RX 7800 XT) в среднем ~65% занятости.
Ограничивает кадр поток GPU-команд. Главные статьи — базовые затраты видеоядра:
FindView ~5,6%, BindTextures ~5,6%, memmove ~4,6%, SmallGuestCopy ~3,5%,
CachedImageDescEntry ~3,2%, BindBuffers/BindVertexBuffers ~3% каждая.
Следующий шаг — перенос этих вычислений на рабочие потоки (шаг 2 parallel_gpu.md).
### FSR 4: гостинг на персонаже и оружии (30.09, повторная проверка)
Видео `video_2026-09-30_03-43-12.mp4`: FSR 4, Ultra Performance (640×360 → 1920×1080). За
движущимся диском пилы остаётся тёмный отпечаток на открывшемся фоне. Проверено:
- отладочный вид векторов (FSR 4): персонаж и оружие получают векторы объекта;
- кеш диапазонов индексов не устаревает (0 изменений при перепроверках);
- последовательность jitter совпадает с провайдером (Halton 2/3, ceil(8·ratio²) фаз);
- A/B в одном запуске с выключенными оптимизациями после 29.09 (биты 4096, 8192, 1<<30):
шлейфы одинаковые в обеих фазах, то есть это не регрессия от них. Попутно: эти
оптимизации вместе дают 84,4 против 70,8 FPS при беге (+19%).
Модель v07 определяет открывшийся фон по цвету (глубина используется только для расширения
векторов), и при входе 640×360 ей мало информации. FSR 3.1 отбрасывает такую историю по
глубине, поэтому на Ultra Performance шлейфов у него меньше.
+248
View File
@@ -0,0 +1,248 @@
# Исправления и анализ проблем
## Дата: 2026-09-27
### Проблемы из тестирования
1. **Маска реактивности не отображается в отладочном режиме**
2. **Шлейфы на оружии при движении**
3. **Изменение настроек требует перезапуск игры**
---
## 1. Маска реактивности не сохраняется ✅ ИСПРАВЛЕНО
### Проблема
Чекбокс "Показать маску (отладка)" в меню работал, но настройка `debug_view` не сохранялась в `bbport.ini`, поэтому после перезапуска маска не отображалась.
### Причина
В `bbport_settings.cpp`:
- Функция `Set()` не обрабатывала ключ `debug_view`
- Функция `Save()` не записывала значение `debug_view` в файл
### Исправление
**Файл:** `native_probe/gpu/shim/bbport_settings.cpp`
1. Добавлена обработка в `Set()`:
```cpp
} else if (key == "debug_view") {
v.debug_view = std::clamp(i, 0, DebugViewCount - 1);
}
```
2. Добавлена запись в `Save()`:
```cpp
"debug_view=%d\nshow_fps=%d\n",
// ...
v.debug_view.load(), int(v.show_fps.load())
```
### Как проверить
1. Запустить игру
2. Открыть меню (Insert или L3+R3)
3. Включить "Маска реактивности" → "Показать маску (отладка)"
4. Закрыть меню
5. Маска должна отображаться красным цветом поверх затемнённого кадра
---
## 2. Шлейфы на оружии
### Анализ
Из логов видно:
```
Object motion: on (4194304 vertices per frame)
```
Векторы движения объектов включены и работают. Шлейфы могут быть из-за:
#### Возможные причины:
**A. Проблема FSR 3.1**
- FSR 3.1 может неправильно обрабатывать быстро движущиеся объекты
- Нужно сравнить с FSR 4 (когда будет реализован)
**B. Векторы движения объектов неточные**
- Выборочный путь включает векторы только для отрисовок с буферами поз/костей
- Возможно, оружие на спине использует другой тип анимации
**C. Маска реактивности не работает для оружия**
- Маска помечает только прозрачные эффекты (частицы, дымка)
- Оружие — непрозрачный объект, поэтому маска на него не влияет
**D. Jitter выключен**
Из `bbport.ini`:
```
jitter=0
```
Субпиксельный сдвиг **выключен**, что ухудшает качество темпорального апскейлера!
### Рекомендации для тестирования
1. **Включить jitter:**
```ini
jitter=1
```
Jitter критически важен для FSR 3.1!
2. **Проверить маску реактивности:**
- Включить `debug_view=1` и `reactive=1`
- Посмотреть, какие области помечены как реактивные
- Оружие НЕ должно быть в маске (оно непрозрачное)
3. **Попробовать увеличить reactive_scale:**
```ini
reactive_scale=2.00
```
(хотя это для прозрачности, не для оружия)
4. **Отключить векторы объектов для сравнения:**
```ini
object_motion=0
```
Перезапустить и сравнить — станет хуже или лучше?
5. **Проверить разные пресеты:**
- Native AA (preset=0) — без масштабирования
- Quality (preset=1) — x1.5
- Performance (preset=3) — x2.0
Шлейфы должны быть меньше на более высоких разрешениях рендера.
### Логи для отладки
Включить отладку векторов движения:
```bash
BB_DEBUG_MOTION=1 ./run.sh
```
Должны появиться опции:
- Toggle 1<<22: перепроецирование прошлого кадра
- Toggle 1<<23: карта ошибок
---
## 3. Перезапуск для применения настроек
### Текущая ситуация
Из логов:
```
GPU scene scaling: AMD Radeon RX 7800 XT (RADV NAVI32) lacks blit support for D32S8
GPU scene scaling: using startup resolution 960x540 for this driver
```
Твоя RX 7800 XT **не поддерживает blit D32S8** (копирование depth+stencil буфера), поэтому используется старый путь:
- Разрешение задаётся патчем при запуске
- Смена пресета требует перезапуска
### Новый путь (в рабочем дереве, не закоммичен)
Согласно ROADMAP.md, в рабочем дереве есть код для:
- Динамического масштабирования на уровне рендерера
- Переключения пресетов без перезапуска
- Автоматического fallback для GPU без blit D32S8
Но этот код **ещё не закоммичен** — автор ждёт тестирования на реальном GPU.
### Что работает без перезапуска
Эти настройки применяются **сразу**:
- ✅ Включение/выключение апскейлера
- ✅ Резкость (sharpness)
- ✅ Jitter (если не включён BB_LIVE_SCALING_UNSUPPORTED)
- ✅ Маска реактивности и её параметры
- ✅ Отладочные режимы
### Что требует перезапуск
На твоём GPU:
- ❌ Смена пресета (Native AA ↔ Quality ↔ Performance)
- ❌ Включение/выключение векторов движения объектов
### Обходной путь
Для тестирования разных пресетов без перезапуска игры:
```bash
# Редактировать bbport.ini
preset=1 # Quality
# Перезапустить только процесс (Ctrl+C, затем ./run.sh)
```
---
## 4. Дополнительные находки
### Конфигурация из bbport.ini
```ini
upscaler=fsr3
preset=3 # Performance (960x540 → 1920x1080, x2.0)
sharpen=1
sharpness=0.81 # Высокая резкость
jitter=0 # ⚠️ ВЫКЛЮЧЕН — это плохо для FSR!
reactive=1
object_motion=1
reactive_scale=1.00
reactive_threshold=0.20
reactive_max=0.90
show_fps=1
```
### Рекомендованные настройки для теста
```ini
upscaler=fsr3
preset=3 # Performance
sharpen=1
sharpness=0.50 # Немного снизить
jitter=1 # ⚠️ ВКЛЮЧИТЬ обязательно!
reactive=1
object_motion=1
reactive_scale=1.50 # Увеличить для теста
reactive_threshold=0.15
reactive_max=0.90
debug_view=1 # Показывать маску
show_fps=1
```
---
## Следующие шаги
### Для пользователя (тестирование)
1. ✅ Перезапустить игру с исправлениями
2. Включить `jitter=1` и `debug_view=1`
3. Протестировать отображение маски
4. Записать видео с маской и без неё
5. Сравнить шлейфы с `object_motion=0` и `object_motion=1`
### Для разработчика (следующие фичи)
1. **Закоммитить код динамического масштабирования**
- Уже работает в рабочем дереве
- Нужно тестирование на RX 7800 XT
2. **Улучшить векторы движения объектов**
- Проверить, почему оружие оставляет шлейфы
- Возможно, нужен более широкий критерий для "движущихся отрисовок"
3. **Добавить FSR 4 INT8**
- Уже есть в FSR-Vulkan как experimental
- Сравнить качество с FSR 3.1
4. **Добавить DLSS**
- Нативный Linux Vulkan SDK (NGX)
- Не будет работать на AMD, но нужен для пользователей NVIDIA
---
## Коммит
Изменённые файлы:
- `native_probe/gpu/shim/bbport_settings.cpp` — добавлено сохранение/загрузка `debug_view`
Тестирование:
- Сборка: ✅ успешно
- Требуется проверка в игре
+21
View File
@@ -66,6 +66,23 @@
## Известные проблемы
Состояние последнего коммита. В рабочем дереве подготовлен перенос пресетов на уменьшенные
цели рендера при сохранении гостевых буферов, постобработки и UI в 1920×1080. Переходы
между пресетами и копирование цвета/глубины/stencil прошли Vulkan-тест на Lavapipe.
RX 7800 XT не поддерживает blit D32S8, поэтому новый путь оставлял FSR вход 1920×1080
и снижал FPS. Для таких GPU запуск теперь автоматически выбирает проверенный путь с патчем
разрешения игры и UI 1920×1080; смена пресета снова требует перезапуска. На GPU с нужной
поддержкой остаётся переключение без перезапуска. Пункты 2–3 ниже относятся к последнему
коммиту, до этих изменений рабочего дерева.
Общий проход векторов движения объектов был слишком дорогим: Performance давал около
49 FPS с ним и 77–78 FPS без него. Теперь дополнительная цель и шейдеры включаются только
для G-buffer отрисовок с буфером позы/костей; остальные используют векторы камеры.
В пробном запуске выборочный путь держал около 81–87 FPS, без пропусков из-за ёмкости
истории. Векторы объектов включены по умолчанию; `BB_OBJECT_MOTION=0` отключает их, а
`BB_OBJECT_MOTION_ALL=1` возвращает старый общий путь для сравнения. Выбор по размеру
буфера пока эвристический: качество оружия и одежды в движении требует проверки в игре.
1. **Движущиеся объекты «рассыпаются»** (оружие на спине, персонаж, ткань, враги): есть
только векторы движения камеры. Камера от третьего лица следует за персонажем, поэтому
векторы камеры для него неверны, и апскейлер берёт историю не из того места. Это касается
@@ -125,6 +142,10 @@
- **XeSS / XeFG и OptiScaler**: Intel поставляет их только как Windows DLL. Варианты:
загрузчик Windows DLL (PE) внутри процесса либо запуск всей сборки под Proton с
OptiScaler, которому отдаём входы через интерфейс DLSS.
Проверено 29.09: libxess.dll (одинаковая у DOOM: The Dark Ages, PRAGMATA и др.) имеет
Vulkan-путь (`xessVKCreateContext`, `xessVKExecute`), но импортирует ~300 функций:
MSVCP140 (iostream), VCRUNTIME140 (C++-исключения), UCRT, kernel32, setupapi, dxgi.
Решено отложить; самый реалистичный путь — хелпер под Wine с общей памятью Vulkan.
### 4. Генерация кадров
+131
View File
@@ -0,0 +1,131 @@
# Векторы движения: доработка после видео 2026-09-26
Исходный материал: `video_2026-09-26_04-19-53.mp4`, FSR 3.1 Performance,
рендер 960×540 → вывод 1920×1080, и `out/dev/t71.log`.
Это описание текущих изменений рабочего дерева, а не подтверждение качества в игре.
Общий проход для всех G-buffer draws оказался слишком дорогим: Performance работал примерно
при 49 FPS с ним и 77–78 FPS без него. Теперь векторы объектов включены по умолчанию только
для шейдеров с буфером позы/костей (размер 640–16384 байт, кроме общего буфера сцены
864 байта; шаг 16 байт). В пробном запуске с более узким порогом 1024 байта
это сократило число обработанных draws примерно до 140 за кадр, устранило переполнение
истории и дало около 81–87 FPS. Остальные draws сохраняют дешёвые векторы камеры.
`BB_OBJECT_MOTION=0` выключает всю функцию, `BB_OBJECT_MOTION_ALL=1` возвращает общий
проход для A/B. Альфа-канал вспомогательной цели хранит глубину: если поздний обычный
draw перекрыл объект, его вектор не используется. Качество персонажа и оружия в движении
после выборки ещё нужно проверить визуально.
## Что обнаружено
- В конце исходного лога за 600 кадров: 257400 G-buffer draws, 256200 объявлены
движущимися, только 30000 получили предыдущие позиции (~11,7% всех draws).
Это покрытие отрисовок, не процент пикселей. Причина каждого пропуска старым
счётчиком не различалась.
- Под историю резервировался размер целого vertex buffer, умноженный на число
экземпляров **вместе с firstInstance**. Для подмешей общих буферов это лишние
позиции; бюджет составлял 2097152 позиции на кадр.
- История не имела отдельного барьера между записями/чтениями vertex shaders;
переиспользование кольца host-параметров не ожидало GPU.
- Выключенные записи всех вершин направлялись в один scratch-элемент.
- `CameraMotion::OnDisplayPass` сбрасывал Depth() перед расчётом jitter phases:
Performance получал 8 фаз вместо 32. В восстановлении позиции из глубины
также не учитывался сдвиг viewport.
## Изменения
- Диапазон истории определяется min/max фактических индексов с учётом baseVertex
и primitive restart. FirstInstance вычитается в шейдере, а не расходует память.
- Сохраняются все подходящие прямые G-buffer draws, начиная с первого появления.
Эвристика изменения констант удалена: она считала движущимися почти все draws
и пропускала начало движения. Чтение возможно только при совпадении с прошлым кадром.
- Ключ включает все описания vertex streams, шейдер, адрес и хеш содержимого
индексов, диапазон вершин, экземпляры и номер среди одинаковых draws.
Смена топологии или пропуск кадра сбрасывает соответствующую историю.
- Бюджет — 4194304 позиции/кадр; два массива вместе занимают 128 МиБ вместо 64 МиБ.
При нехватке памяти остаются векторы камеры. Лог отдельно показывает сохранения,
совпадения, отсутствие истории, нехватку памяти и недопустимые диапазоны.
- Добавлены vertex-to-vertex barriers, ожидание GPU перед повторным использованием
host-параметров, coherent host memory и независимый учёт layout изображения движения.
Повторное чтение изображения не стирает признак наличия векторов.
- Выключенные обращения обходятся ветвлением. Для повторных invocation одной
индексированной вершины используются атомарные записи компонентов позиции.
Позиции за камерой и нечисловые/бесконечные векторы не используются.
- Performance получает 32 фазы jitter после создания FSR context. Векторы камеры
восстанавливаются из unjittered координат; векторы объектов уже получались
из clip positions до сдвига viewport.
## Проверки
Сборка и CPU-тесты:
```bash
cd native_probe
nix-shell shell.nix --run 'bash build.sh && ninja -C out/gpu motion-history-test motion-shader-test && out/gpu/motion-history-test && out/gpu/motion-shader-test out/motion-shaders'
```
`test_motion_history.cpp` проверяет диапазоны u16/u32, restart, отрицательный baseVertex,
firstInstance, повторные draws, смену топологии, пропущенный кадр, исчерпание бюджета,
переполнение и число фаз. Также пройден запуск с AddressSanitizer/UBSan.
`test_motion_shaders.cpp` использует настоящий SPIR-V backend и создаёт VS/PS с
векторами и без них. Все четыре результата прошли `spirv-val --target-env vulkan1.3`.
Для повторения нужен `spirv-val` из SPIRV-Tools:
```bash
for shader in out/motion-shaders/*.spv; do
spirv-val --target-env vulkan1.3 "$shader" || exit
done
```
Логи сборки/тестов: `out/dev/motion-validation.log`. Доступа к `/dev/dri` в среде
проверки нет: качество на RX 7800 XT, FPS и процент покрытия после изменений
ещё не проверены. Успешная валидация SPIR-V не доказывает правильность картинки.
## Следующий запуск в игре
Из `game_files`:
```bash
BB_FRAME_STATS=1 BB_TOGGLE_FILE=out/dev/motion-toggles bash native_probe/run.sh 2>&1 | tee native_probe/out/dev/motion-run.log
```
Бит 29 во временном toggle-файле выключает только запись истории. Для корректного
сравнения производительности надо запускать игру с `BB_OBJECT_MOTION=0` и без него:
выбор пайплайна и дополнительная цель задаются при старте процесса.
После переключения дать FSR несколько кадров на обновление истории.
Сравнивать одну сцену: персонаж/оружие при беге, поворот камеры, бой, неподвижная камера.
Через ~600 кадров проверить `Object motion:`: `with history`, `capacity skips`, `unmatched`.
Оставшиеся ограничения: одинаковые меши сопоставляются по порядку draws, который
может меняться при отсечении объектов; смена адресов динамических vertex buffers
теряет совпадение. Indirect draws, tessellation/geometry shaders, MSAA и проходы
вне G-buffer остаются на векторах камеры. Прозрачность/частицы, мыльный UI и перенос
масштабирования на уровень рендерера этим изменением не решены. Стоимость сканирования
индексов и атомарных записей нужно измерить на реальном запуске.
## 29 сентября: оружие и маленькие скелеты (гостинг пилы-вертушки)
Видео `video_2026-09-29_23-08-26.mp4`: персонаж стоит, диск пилы вращается и оставляет
шлейф. Лог `BB_MOTION_SELECT_LOG=1` показал причину: второй буфер вершинного шейдера —
палитра костей по 48 байт (3x4), и у оружия/реквизита она 96–384 байта (2–8 костей).
Правило «от 640 байт» такие отрисовки не включало, диск получал только векторы камеры,
то есть для FSR был неподвижным.
Изменения:
- `Motion::ClassifyBuffer` (`motion_history.h`): 864 — сцена, ≥640 — скелет персонажа,
96..639 кратно 48 — маленький скелет. Пайплайны с любым скелетом получают вариант
с векторами.
- Маленьких скелетов около тысячи на кадр, большинство — статичные части карты.
Поэтому они «гейтятся»: хеш палитры сравнивается с прошлым кадром по дешёвому
ключу (`History::Moving`) до сканирования индексов. Совпала — вектор камеры точен,
ничего не пишется. Изменилась — позиции сохраняются, со следующего кадра вектор
объекта. Хеш с буфером 416 байт не подходит: он меняется вместе с камерой почти у всех
(проверено: переполнение истории и падение FPS до ~40).
- Меню: «Показать векторы движения (отладка)». Красный/зелёный — |x|/|y|, синий —
пиксель получил вектор объекта. `BB_MOTION_SELECT_LOG=1` печатает шейдеры G-buffer
с размерами буферов и решением.
Замер `out/motion-gated.log`: ~225 сохранённых отрисовок и ~600 «неподвижных» на кадр,
без переполнения, 58–80 FPS в игровых сценах. Визуально исчезновение шлейфа
на пиле ещё нужно подтвердить.
+150
View File
@@ -100,3 +100,153 @@ Findings, in the order they were fixed:
Result: stalls are mostly 40–50 ms (GPU thread ~30 ms of draw work plus ~12 ms of copies)
instead of 60–170 ms; the area load frame 350 ms instead of 430–760 ms.
## Step 2, first slice: resource sharps on the workers (2026-09-30)
A prepared stage now also carries the sharps its worker read from the flattened user data:
every T# with its texture-description hash, every S# and V# (`PrepareResources`,
`PreparedStage::image_sharps` and siblings, in the arena of the submission). The GPU thread
uses them when `PipelineCache::UsedPrepared()` reports that the draw's pipeline came from the
prepared draw — the flattened data was then compared word for word, and sharps depend on
nothing else. Resource list sizes are checked per stage. Toggle 4096 switches it off.
A/B in one run, standing still, `BB_FPS_LIMIT=0`, FSR 4, 20 s phases: 69–70 FPS with the
prepared sharps, 66.5–67 without (+3%), same image.
Upscaler costs removed from the GPU thread before this (perf, DWARF call graphs): per-draw
driver format queries in `SceneTargets::Eligible` (~13%), direct recording forced by
`CommandBuffer()` in object motion and the reactive mask, index-list scans for object motion
(~10%, now `Motion::IndexRangeCache`).
What remains on the GPU thread is mostly work on shared cache state: texture binding ~23%
(FindView ~5.6%, UpdateImage/Track/Touch ~5%, barriers), buffer binding ~12% (ObtainBuffer
~11%), render targets ~8%. The next step is a draw-level split: the GPU thread keeps decode,
cache mutation and barriers; a second ordered stage builds descriptor writes and vertex
input state from the resolved handles.
## Scaling to the available threads (2026-09-30)
Draw preparation used to replay the whole command stream in every worker and was switched
off below 12 hardware threads (Steam Deck: no workers). Now:
- One scanner (`bb:DrawScan`) replays the register writes in order and stores, per buffer,
its starting checksum and a delta of the 32-word register blocks it wrote
(`AmdGpu::RegDirty/RegDelta`, recorded by `ApplyGraphicsRegisterPacket`).
- Workers (`bb:DrawPrepN`, half the hardware threads in the affinity mask, 1..8) claim the
nearest scanned buffer ahead of the GPU thread, reach its starting state by applying the
deltas since their last buffer (or from the queue's tail state), and prepare its draws.
More workers now mean more buffers prepared in parallel, not more duplicated replay.
- All helpers are SCHED_IDLE (`bbport_threads.h`): they only take idle cores. If the scanner
starves (busy CPU), the GPU thread rebases it from its own register state once the lag
passes 64 buffers instead of queueing without bound (`scanner rebases` in the stats).
- Copy threads use the same affinity count (critical path, normal priority).
A/B in one run (toggle 8192, standing still, `BB_FPS_LIMIT=0`, FSR 4), ~98% of direct draws
prepared, no rebases:
| CPU | with preparation | without |
|---|---|---|
| 16 threads, 8 workers | ~74 FPS | ~57.7 FPS |
| `taskset -c 0-3,8-11` (4 cores / 8 threads, Deck-like), 4 workers | ~68 FPS | ~53 FPS |
The previous design (4 replaying workers) gave ~69 FPS at the same spot on 16 threads, and
none on 8.
## Vertex inputs on the workers (2026-09-30)
`PreparedDraw::vertex` holds, for the dynamic vertex input path, the attribute and binding
descriptions (`GetVertexInputs`), the V# of every stream, the stream memory merged into
ranges with each stream's range index, and the XXH3 of the streams (object motion). The GPU
thread only obtains the buffers of the merged ranges and records the bindings. Used when the
prepared draw's pipeline was taken, the attribute count matches the pipeline's fetch shader
and no frame capture runs; toggle 4096 switches it off together with the sharps.
A/B (standing still, `BB_FPS_LIMIT=0`, FSR 4, 16 threads): ~69.3 FPS on, ~65.7 off (+5.5%;
the sharps alone gave +3%). Screenshots identical.
Render targets were looked at and left on the GPU thread: their descriptions are already
memoized per slot (key copy + compare), the size hint (`last_cb_extent`) is GPU-thread state
rather than register state, and the rest of `BeginRendering` is view lookup, barriers and the
reduced-resolution proxies — all on shared mutable state.
## Engine short paths: investigation (2026-09-30, in progress)
- The eboot has no symbols but links Sony's Gnmx (`sdk\target\src\gnmx\gfxcontext.cpp`,
`lwgfxcontext.cpp`); FromSoftware's Dantelion2 CoreGraphics2 sits on top; YEBIS does the
post-processing.
- `BB_BUFFER_STATS=1` (buffer_cache.cpp) prints buffer bindings per guest region every 5 s.
Almost all traffic comes from the engine's frame ring, ~0x1043400000–0x1049xxxxxx inside the
2.4 GB direct allocation at 0x1042c00000: ~400k small constant copies/s (~190 MB/s) and
~850 MB/s of arena re-uploads after CPU writes (~13 MB per frame).
- Candidate short path: import that ring into Vulkan (VK_EXT_external_memory_host) so the GPU
reads it in place — no copies, no page tracking, far fewer GPU-thread operations.
Blocker to resolve first: EOP fences are signalled when the GPU thread records the packet,
not when the GPU executes it, so the guest may rewrite ring data still unread by the GPU.
With ~100 MB of ring and ~13 MB per frame the wrap is ~7–8 frames; the GPU's lag is bounded
by the presenter's frame pool (`present_frames`, swapchain image count). Next: measure the
ring's wrap period per frame and the real GPU lag, then prototype the import behind a toggle.
### Measurements for the frame-data window (BB_BUFFER_STATS=1)
- Reuse distance of 64 KiB blocks in 0x104xxxxxxx: overwhelmingly 1 frame (then 2). The engine
rewrites the same memory every frame; there is no long ring.
- GPU lag when the GPU thread starts a frame (`GPU lag at frame start`): almost always 0 —
the GPU has finished the previous frame. But the engine writes the next frame's data while
the GPU still executes the current one, and EOP fences are signalled at record time.
**Reading this memory in place (VK_EXT_external_memory_host) is therefore unsafe** unless
fences wait for real GPU completion, which costs the guest (it waits on them). Dropped.
- Per second in the hot window: ~370k small constant copies (~179 MB, ~480 B each) and ~3.6k
arena bindings covering ~13.8 GB (≈3.8 MB per binding) of which only ~516 MB are re-uploaded.
Copying bound ranges instead of tracking pages would multiply the traffic by ~27 (why the
"hot pages" attempt fell to 33 FPS); page tracking is the right mechanism there.
- Remaining candidates: per-submission snapshots of the constant area (fewer, larger copies
instead of ~6000 per frame) and trimming vertex-buffer bindings to the index range a draw
uses (the V#s cover whole vertex pools, so each binding walks ~950 tracked pages).
### Tried and reverted: per-epoch constant chunks
Small read-only constants served from one snapshot per chunk and queue-task epoch (the epoch
changed at every task entry and resume, so a chunk only served data written before its copy).
A/B in one run, standing still: 32 KiB chunks 82/82/79 FPS vs 85/84/84 without (slower: the
extra copied bytes cost more than the saved operations); 8 KiB chunks 85/85/85 vs 85/83/85
(no difference). The per-binding constant copies are not what limits the frame; the change
was removed. `BB_BUFFER_STATS=1` (bindings per region, reuse distance, GPU lag) stays.
State after this work, standing still, `BB_FPS_LIMIT=0`, FSR 4: 82–85 FPS; GPU command thread
~90% of a core, recording thread ~92% (mostly its spin), GPU ~70% busy.
### Texture binding: repeated sets and the UpdateImage fast path
- Measured and dropped: only ~13% of draws bind exactly the textures and samplers of the
previous draw in every stage (running through Yharnam), so skipping whole texture sets
would save at most ~3% of the GPU thread.
- `TextureCache::UpdateImage` runs for every texture binding; for a clean, registered image
already tracked and touched in this GC period it only took the texture-cache mutex (shared
with the guest threads' fault handlers). It now returns without the lock in that case,
reading the flags atomically (toggle 1073741824 = 1 << 30 restores the locked path).
A/B, 8 phases of 20 s: 81.8 vs 80.2 FPS mean (+2%), 3 of 4 pairs ahead; the game window
was partly covered by other applications during the run, so treat it as indicative.
- The A/B script now records the log line at each phase switch and prints per-phase means.
## LTO and PGO (2026-09-30)
`build.sh` builds `libbbgpu` with LTO (`-flto=auto`, also for sirit and FSR-Vulkan linked
into it) and, when `pgo/` holds a profile, with `-fprofile-use` (`-fprofile-partial-training
-fprofile-correction`; functions changed since the profile compile without it). No `-march`:
the same build runs on the Steam Deck.
Collecting a profile: `BB_PGO=generate bash run.sh` builds an instrumented library
(`-fprofile-generate -fprofile-update=atomic`) that writes `pgo/` every 30 s (`bb:pgo` thread:
`__gcov_dump` + `__gcov_reset`, since the game often ends through `_exit`); play a few minutes
of ordinary gameplay. The next plain build uses it. `BB_PGO=off` / `BB_LTO=OFF` disable them.
Regenerate the profile after larger code changes.
Comparison, three runs in game (median of in-game 5 s windows, >800 draws/frame), with the new
`Frame stats` field "GPU thread us/draw" (CPU time of the GPU command thread per draw, which
tolerates small scene differences better than FPS):
| build | FPS | GPU thread µs/draw |
|---|---|---|
| no LTO, no PGO | 73.2 | 7.72 |
| LTO | 74.4 | 7.58 (−1.8%) |
| LTO + PGO | 76.3 | 7.35 (−4.8%) |
@@ -0,0 +1,10 @@
Первая версия векторов движения объектов собрана, игра запущена, ошибок при старте нет. Но в самой игре я её ещё не проверял: шейдеры G-buffer с новым кодом компилируются только при загрузке уровня, так что главная проверка впереди.
Как это работает:
- Вершинный шейдер каждой отрисовки в G-buffer сохраняет итоговые позиции вершин и читает позиции тех же вершин из прошлого кадра.
- Пиксельный шейдер пишет разницу в дополнительную цель G-buffer.
- Где объект двигался, FSR получает этот вектор, в остальных местах — вектор камеры.
- Позиции сохраняются только для движущихся отрисовок: тех, у которых изменились константы вершинного шейдера (матрица объекта, кости). Статичная геометрия памяти не тратит.
- Это учитывает любую анимацию, которую считает вершинный шейдер: скиннинг, оружие, ткань, врагов.
Загрузись, пожалуйста, в игру и напиши «готов». Я проверю лог: сколько отрисовок движется и у скольких нашлись позиции прошлого кадра. Потом побегай с пилой на спине. Для сравнения я смогу выключать векторы объектов на ходу, остаются только векторы камеры.
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Downloads the FSR 4 v07 INT8/DOT4 asset set (SPIR-V passes, model initializers, pre-pass
# weights) used by the FSR-Vulkan provider into fsr4_shaders/. The assets were built by
# Q2RTX from AMD's MIT-licensed FidelityFX SDK FSR 4 source (see LICENSE-FSR4-v07.txt);
# they are not part of this repository. Output 1920x1080 needs the 1080 tier; pass
# BB_FSR4_TIERS="1080 2160" for larger outputs (BB_OUTPUT_RES).
set -euo pipefail
cd -- "$(dirname -- "$0")"
commit=ae8d628fae208813172446d1e49ed94150b04658
base="https://raw.githubusercontent.com/FireBurn/Q2RTX/$commit/baseq2/fsr4_shaders"
dest=fsr4_shaders
mkdir -p "$dest"
files=(LICENSE-FSR4-v07.txt rcas.spv spd_auto_exposure.spv)
for model in native quality balanced performance ultraperf drs; do
files+=("fsr4_model_v07_i8_${model}_initializers.bin"
"fsr4_model_v07_i8_${model}_pre_weights.bin"
"fsr4_model_v07_i8_${model}_shader_manifest.json")
for tier in ${BB_FSR4_TIERS:-1080}; do
files+=("fsr4_model_v07_i8_${model}_${tier}_pre.spv"
"fsr4_model_v07_i8_${model}_${tier}_post.spv")
for pass in $(seq 1 12); do
files+=("fsr4_model_v07_i8_${model}_${tier}_pass${pass}.spv")
done
done
done
fetched=0
for file in "${files[@]}"; do
if [[ -s $dest/$file ]]; then continue; fi
curl -fsSL --retry 3 -o "$dest/$file.part" "$base/$file"
mv "$dest/$file.part" "$dest/$file"
fetched=$((fetched + 1))
done
echo "FSR 4 assets: ${#files[@]} files in $PWD/$dest ($fetched downloaded)"
+50 -1
View File
@@ -11,6 +11,35 @@ endif()
# Frame pointers keep the loader's thread dumps (watchdog, SIGUSR2 sampling) walkable.
add_compile_options(-fno-omit-frame-pointer)
# bbport: link-time optimization (BB_LTO, default on) and profile-guided optimization
# (BB_PGO: off, generate = instrumented build writing into BB_PGO_DIR, use = build with that
# profile). Covers bbgpu and the static libraries linked into it. No -march: the same build
# runs on the Steam Deck.
option(BB_LTO "Link-time optimization of bbgpu" ON)
set(BB_PGO "off" CACHE STRING "Profile-guided optimization: off, generate, use")
set(BB_PGO_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../pgo" CACHE PATH "Profile directory")
if (BB_LTO)
include(CheckIPOSupported)
check_ipo_supported(RESULT bb_ipo_ok OUTPUT bb_ipo_error)
if (bb_ipo_ok)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON)
else()
message(WARNING "LTO unavailable: ${bb_ipo_error}")
endif()
endif()
if (BB_PGO STREQUAL "generate")
# Atomic counters: the GPU, recording, preparation and copy threads run the same code.
add_compile_options(-fprofile-generate=${BB_PGO_DIR} -fprofile-update=atomic)
add_link_options(-fprofile-generate=${BB_PGO_DIR})
add_compile_definitions(BB_PGO_GENERATE)
elseif (BB_PGO STREQUAL "use")
# Partial training keeps unprofiled code optimized normally; counters from concurrent
# threads can be slightly inconsistent (correction). Functions changed since the profile
# (or built differently for it, bbgpu_init) are compiled without it.
add_compile_options(-fprofile-use=${BB_PGO_DIR} -fprofile-partial-training
-fprofile-correction -Wno-missing-profile -Wno-coverage-mismatch)
endif()
find_package(Vulkan REQUIRED)
find_package(fmt REQUIRED)
find_package(Boost REQUIRED)
@@ -61,6 +90,8 @@ add_host_shader(post_process.frag post_process_frag)
add_host_shader(camera_motion.comp camera_motion_comp)
add_host_shader(upscale_merge.comp upscale_merge_comp)
add_host_shader(upscale_reactive.comp upscale_reactive_comp)
add_host_shader(depth_resample.frag depth_resample_frag)
add_host_shader(depth_resample.frag depth_stencil_resample_frag HAS_STENCIL=1)
add_host_shader(camera_motion.comp camera_motion_debug_comp DEBUG_OVERLAY=1)
foreach(BPP IN ITEMS 8 16 32 64 96 128)
add_host_shader(tiling.comp tiling_micro_${BPP}_comp BITS_PER_PIXEL=${BPP} IS_MACRO_TILED=0)
@@ -101,6 +132,24 @@ target_compile_definitions(bbgpu PRIVATE BOOST_ASIO_STANDALONE NDEBUG)
target_compile_options(bbgpu PRIVATE -Wno-deprecated-declarations -Wno-missing-field-initializers)
# Runtime symbols (runtime_memory_*, runtime_process_*) resolve against bb-probe.
target_link_options(bbgpu PRIVATE -Wl,--allow-shlib-undefined)
target_link_libraries(bbgpu PRIVATE ffx-vulkan::portable)
target_link_libraries(bbgpu PRIVATE ffx-vulkan::portable ffx-vulkan::fsr4-v07-vulkan)
target_link_libraries(bbgpu PUBLIC Vulkan::Headers fmt::fmt Boost::headers magic_enum::magic_enum
tsl::robin_map GPUOpen::VulkanMemoryAllocator PkgConfig::XXHASH PkgConfig::SDL3 PkgConfig::X11 PkgConfig::FFMPEG miniz::miniz Zydis::Zydis sirit)
# Explicit test targets; neither launches the game nor needs a Vulkan device.
add_executable(motion-history-test EXCLUDE_FROM_ALL ../test_motion_history.cpp)
target_compile_options(motion-history-test PRIVATE -UNDEBUG -Wall -Wextra -Werror)
add_executable(motion-shader-test EXCLUDE_FROM_ALL ../test_motion_shaders.cpp ../test_gpu_runtime_stubs.cpp)
target_include_directories(motion-shader-test PRIVATE shim shadps4 third_party/gcn/include)
target_link_libraries(motion-shader-test PRIVATE bbgpu)
add_executable(ui-composition-test EXCLUDE_FROM_ALL ../test_ui_composition.cpp)
target_compile_options(ui-composition-test PRIVATE -UNDEBUG -Wall -Wextra -Werror)
add_executable(scene-resolution-test EXCLUDE_FROM_ALL ../test_scene_resolution.cpp ../test_gpu_runtime_stubs.cpp)
target_include_directories(scene-resolution-test PRIVATE shim shadps4 third_party/gcn/include)
# The test's Vulkan-Hpp inline methods have a different set of platform extensions
# from vk_platform.cpp. Keep them local and use an explicit dispatcher in the test.
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)
+15 -2
View File
@@ -378,12 +378,25 @@ void VideoOutDriver::Flip(const Request& req) {
const u64 compile_ns = Vulkan::g_bb_compile_ns.exchange(0);
const u64 direct = Vulkan::Scheduler::direct_recordings.exchange(0);
const u64 faults = BbStats::tracker_faults.exchange(0);
// GPU command thread CPU time per draw: comparable between builds even when the scene
// (and so the frame rate) differs a little.
static u64 window_draws = 0, window_gpu_us = 0;
const u64 all_draws = BbStats::draws.load();
const u64 gpu_us = BbStats::gpu_user_us.load() + BbStats::gpu_sys_us.load();
const double us_per_draw = all_draws > window_draws
? double(gpu_us - window_gpu_us) / (all_draws - window_draws)
: 0.0;
const double draws_per_frame = frames ? double(all_draws - window_draws) / frames : 0.0;
window_draws = all_draws;
window_gpu_us = gpu_us;
std::printf("Frame stats: %.1f FPS, worst frame %.1f ms (vblank %u Hz); "
"%u shader/pipeline compiles, %.1f ms; %llu recorder syncs; "
"%.0f write faults/s, %lld hot pages\n",
"%.0f write faults/s, %lld hot pages; GPU thread %.2f us/draw, "
"%.0f draws/frame\n",
frames / window, worst_ms, EmulatorSettings.GetVblankFrequency(), compiles,
compile_ns / 1e6, static_cast<unsigned long long>(direct),
faults / window, static_cast<long long>(BbStats::hot_pages.load()));
faults / window, static_cast<long long>(BbStats::hot_pages.load()),
us_per_draw, draws_per_frame);
window_start = now;
frames = 0;
worst_ms = 0;
@@ -362,7 +362,7 @@ void SetupCapabilities(const Info& info, const Profile& profile, const RuntimeIn
} else if (stage == SwStage::Geometry && info.stores.GetAny(IR::Attribute::ViewportIndex)) {
ctx.AddCapability(spv::Capability::MultiViewport);
}
if (info.uses_dma) {
if (info.uses_dma || ctx.VertexMotion()) {
ctx.AddCapability(spv::Capability::PhysicalStorageBufferAddresses);
ctx.AddExtension("SPV_KHR_physical_storage_buffer");
}
@@ -118,9 +118,24 @@ Id EmitGetAttribute(EmitContext& ctx, IR::Attribute attr, u32 comp, u32 index) {
return ctx.OpLoad(ctx.F32[1],
ctx.OpAccessChain(ctx.input_f32, ctx.gl_in, ctx.ConstU32(index),
ctx.ConstU32(0U), ctx.ConstU32(comp)));
case IR::Attribute::FragCoord:
return ctx.OpLoad(ctx.F32[1],
ctx.OpAccessChain(ctx.input_f32, ctx.frag_coord, ctx.ConstU32(comp)));
case IR::Attribute::FragCoord: {
const Id value = ctx.OpLoad(ctx.F32[1],
ctx.OpAccessChain(ctx.input_f32, ctx.frag_coord, ctx.ConstU32(comp)));
if (comp >= 2) return value;
// Guest shaders still address native-sized textures and constants. Convert the
// reduced framebuffer's pixel coordinates back to that logical coordinate space.
const Id packed = ctx.OpLoad(ctx.U32[1], ctx.OpAccessChain(
ctx.TypePointer(spv::StorageClass::PushConstant, ctx.U32[1]),
ctx.push_data_block, ctx.ConstU32(PushData::SceneSizeIndex)));
const Id dimension = comp == 0
? ctx.OpBitwiseAnd(ctx.U32[1], packed, ctx.ConstU32(65535u))
: ctx.OpShiftRightLogical(ctx.U32[1], packed, ctx.ConstU32(16u));
const Id native = ctx.ConstU32(comp == 0 ? 1920u : 1080u);
const Id safe = ctx.OpSelect(ctx.U32[1],
ctx.OpIEqual(ctx.U1[1], dimension, ctx.u32_zero_value), native, dimension);
return ctx.OpFMul(ctx.F32[1], value, ctx.OpFDiv(ctx.F32[1],
ctx.OpConvertUToF(ctx.F32[1], native), ctx.OpConvertUToF(ctx.F32[1], safe)));
}
case IR::Attribute::TessellationEvaluationPointU:
return ctx.OpLoad(ctx.F32[1],
ctx.OpAccessChain(ctx.input_f32, ctx.tess_coord, ctx.u32_zero_value));
@@ -66,7 +66,133 @@ void ConvertPositionToClipSpace(EmitContext& ctx) {
ctx.OpStore(ctx.output_position, vector);
}
// bbport: object motion vectors (runtime_info.h, MotionVectors). The vertex shader stores its
// clip position for this frame and loads the one of the previous frame (same draw, same
// vertex), both by buffer device address. Disabled accesses are branched around: a shared
// scratch element would race between all the inactive vertex invocations.
static void EmitVertexMotion(EmitContext& ctx) {
const Id u32_type = ctx.U32[1];
const Id position = ctx.OpLoad(ctx.F32[4], ctx.output_position);
const Id param_ptr = ctx.OpAccessChain(ctx.TypePointer(spv::StorageClass::PushConstant, u32_type),
ctx.push_data_block,
ctx.ConstU32(PushData::MotionParamIndex));
const Id param_index = ctx.OpLoad(u32_type, param_ptr);
const auto address = [&](u64 base, Id index, u32 stride) {
return ctx.OpIAdd(ctx.U64, ctx.Constant(ctx.U64, base),
ctx.OpIMul(ctx.U64, ctx.OpUConvert(ctx.U64, index),
ctx.Constant(ctx.U64, u64(stride))));
};
const Id u32x4_ptr = ctx.TypePointer(spv::StorageClass::PhysicalStorageBuffer, ctx.U32[4]);
const Id f32x4_ptr = ctx.TypePointer(spv::StorageClass::PhysicalStorageBuffer, ctx.F32[4]);
const Id params = ctx.OpLoad(
ctx.U32[4],
ctx.OpConvertUToPtr(u32x4_ptr, address(MotionVectors::params_address, param_index, 32)),
spv::MemoryAccessMask::Aligned, 16u);
const Id store_base = ctx.OpCompositeExtract(u32_type, params, 0u);
const Id load_base = ctx.OpCompositeExtract(u32_type, params, 1u);
const Id vertices = ctx.OpCompositeExtract(u32_type, params, 2u);
const Id flags = ctx.OpCompositeExtract(u32_type, params, 3u);
const Id offsets = ctx.OpLoad(
ctx.U32[4], ctx.OpConvertUToPtr(u32x4_ptr,
address(MotionVectors::params_address + 16, param_index, 32)),
spv::MemoryAccessMask::Aligned, 16u);
const Id first_vertex = ctx.OpCompositeExtract(u32_type, offsets, 0u);
const Id first_instance = ctx.OpCompositeExtract(u32_type, offsets, 1u);
const Id instances = ctx.OpCompositeExtract(u32_type, offsets, 2u);
const Id vertex = ctx.OpISub(u32_type, ctx.OpLoad(u32_type, ctx.vertex_index), first_vertex);
const Id instance = ctx.OpISub(u32_type, ctx.OpLoad(u32_type, ctx.instance_id), first_instance);
const Id bool_type = ctx.U1[1];
const Id in_range = ctx.OpLogicalAnd(bool_type, ctx.OpULessThan(bool_type, vertex, vertices),
ctx.OpULessThan(bool_type, instance, instances));
const Id slot = ctx.OpIAdd(u32_type, vertex, ctx.OpIMul(u32_type, instance, vertices));
const auto flag = [&](u32 bit) {
return ctx.OpLogicalAnd(
bool_type, in_range,
ctx.OpINotEqual(bool_type, ctx.OpBitwiseAnd(u32_type, flags, ctx.ConstU32(bit)),
ctx.u32_zero_value));
};
const Id do_store = flag(MotionVectors::FlagStore);
const Id do_load = flag(MotionVectors::FlagLoad);
const Id store_label = ctx.OpLabel();
const Id store_merge = ctx.OpLabel();
ctx.OpSelectionMerge(store_merge, spv::SelectionControlMask::MaskNone);
ctx.OpBranchConditional(do_store, store_label, store_merge);
ctx.AddLabel(store_label);
const Id store_address = address(MotionVectors::positions_address,
ctx.OpIAdd(u32_type, store_base, slot), 16);
// Indexed draws may invoke the same vertex more than once. Atomic component stores
// avoid write/write races; all these invocations produce the same clip position.
const Id scalar_ptr = ctx.TypePointer(spv::StorageClass::PhysicalStorageBuffer, u32_type);
const Id scope = ctx.ConstU32(static_cast<u32>(spv::Scope::Device));
for (u32 i = 0; i < 4; ++i) {
const Id ptr = ctx.OpConvertUToPtr(scalar_ptr,
ctx.OpIAdd(ctx.U64, store_address, ctx.Constant(ctx.U64, u64(i * 4))));
const Id bits = ctx.OpBitcast(u32_type, ctx.OpCompositeExtract(ctx.F32[1], position, i));
ctx.OpAtomicExchange(u32_type, ptr, scope, ctx.u32_zero_value, bits);
}
ctx.OpBranch(store_merge);
ctx.AddLabel(store_merge);
const Id load_label = ctx.OpLabel();
const Id load_merge = ctx.OpLabel();
ctx.OpSelectionMerge(load_merge, spv::SelectionControlMask::MaskNone);
ctx.OpBranchConditional(do_load, load_label, load_merge);
ctx.AddLabel(load_label);
const Id loaded = ctx.OpLoad(
ctx.F32[4], ctx.OpConvertUToPtr(f32x4_ptr, address(MotionVectors::positions_address,
ctx.OpIAdd(u32_type, load_base, slot), 16)), spv::MemoryAccessMask::Aligned, 16u);
ctx.OpBranch(load_merge);
ctx.AddLabel(load_merge);
Id previous = ctx.OpPhi(ctx.F32[4], position, store_merge, loaded, load_label);
const Id valid = ctx.OpSelect(ctx.F32[1], do_load, ctx.Constant(ctx.F32[1], 1.0f),
ctx.Constant(ctx.F32[1], 0.0f));
previous = ctx.OpCompositeInsert(ctx.F32[4], valid, previous, 2u);
ctx.OpStore(ctx.motion_out_cur, position);
ctx.OpStore(ctx.motion_out_prev, previous);
}
// Screen-space motion (previous minus current, pixels) through the viewport scale; z = valid.
static void EmitFragmentMotion(EmitContext& ctx) {
const Id f32_type = ctx.F32[1];
const Id current = ctx.OpLoad(ctx.F32[4], ctx.motion_in_cur);
const Id previous = ctx.OpLoad(ctx.F32[4], ctx.motion_in_prev);
const auto push = [&](u32 index) {
return ctx.OpLoad(f32_type, ctx.OpAccessChain(ctx.TypePointer(spv::StorageClass::PushConstant,
f32_type),
ctx.push_data_block, ctx.ConstU32(index)));
};
const Id xscale = push(PushData::XScaleIndex);
const Id yscale = push(PushData::YScaleIndex);
const auto ndc = [&](Id clip, u32 component) {
return ctx.OpFDiv(f32_type, ctx.OpCompositeExtract(f32_type, clip, component),
ctx.OpCompositeExtract(f32_type, clip, 3u));
};
const Id dx = ctx.OpFMul(f32_type, ctx.OpFSub(f32_type, ndc(previous, 0u), ndc(current, 0u)), xscale);
const Id dy = ctx.OpFMul(f32_type, ctx.OpFSub(f32_type, ndc(previous, 1u), ndc(current, 1u)), yscale);
const Id zero = ctx.Constant(f32_type, 0.0f);
const Id epsilon = ctx.Constant(f32_type, 1e-5f);
const Id front = ctx.OpLogicalAnd(ctx.U1[1],
ctx.OpFOrdGreaterThan(ctx.U1[1], ctx.OpCompositeExtract(f32_type, previous, 3u), epsilon),
ctx.OpFOrdGreaterThan(ctx.U1[1], ctx.OpCompositeExtract(f32_type, current, 3u), epsilon));
const Id valid = ctx.OpSelect(f32_type, front,
ctx.OpCompositeExtract(f32_type, previous, 2u), zero);
// Uninstrumented draws can overwrite the final scene depth after this pixel was
// written. Preserve depth so the compose pass can reject an occluded vector.
const Id depth = ctx.OpCompositeExtract(f32_type,
ctx.OpLoad(ctx.F32[4], ctx.frag_coord), 2u);
ctx.OpStore(ctx.motion_frag_out,
ctx.OpCompositeConstruct(ctx.F32[4], std::array<Id, 4>{
ctx.OpSelect(f32_type, front, dx, zero),
ctx.OpSelect(f32_type, front, dy, zero), valid, depth}));
}
void EmitEpilogue(EmitContext& ctx) {
if (Sirit::ValidId(ctx.motion_out_cur)) {
EmitVertexMotion(ctx);
}
if (Sirit::ValidId(ctx.motion_frag_out)) {
EmitFragmentMotion(ctx);
}
if (ctx.hw_stage == HwStage::Vertex &&
ctx.runtime_info.hw.vs.emulate_depth_negative_one_to_one) {
ConvertDepthMode(ctx);
@@ -72,7 +72,7 @@ EmitContext::EmitContext(const Profile& profile_, const RuntimeInfo& runtime_inf
Bindings& binding_)
: Sirit::Module(profile_.supported_spirv), info{info_}, runtime_info{runtime_info_},
profile{profile_}, hw_stage{info.hw_stage}, sw_stage{info.sw_stage}, binding{binding_} {
if (info.uses_dma) {
if (info.uses_dma || VertexMotion()) {
SetMemoryModel(spv::AddressingModel::PhysicalStorageBuffer64, spv::MemoryModel::GLSL450);
} else {
SetMemoryModel(spv::AddressingModel::Logical, spv::MemoryModel::GLSL450);
@@ -351,7 +351,7 @@ void EmitContext::DefineInputs() {
break;
}
case SwStage::Fragment: {
if (info.loads.GetAny(IR::Attribute::FragCoord) ||
if (FragmentMotion() || info.loads.GetAny(IR::Attribute::FragCoord) ||
(info.loads.GetAny(IR::Attribute::BaryCoordPullModel) &&
!profile.supports_amd_shader_explicit_vertex_parameter)) {
frag_coord = DefineVariable(F32[4], spv::BuiltIn::FragCoord, spv::StorageClass::Input);
@@ -644,6 +644,12 @@ void EmitContext::DefineOutputs() {
++num_attrs;
}
if (VertexMotion()) {
motion_out_cur = DefineOutput(F32[4], MotionVectors::CurrentLocation);
motion_out_prev = DefineOutput(F32[4], MotionVectors::PreviousLocation);
Name(motion_out_cur, "motion_cur");
Name(motion_out_prev, "motion_prev");
}
if (needs_clip_distance_emulation) {
clip_distances = Id{DefineOutput(F32[MaxEmulatedClipDistances], 0)};
output_params[num_attrs] = GetAttributeInfo(
@@ -737,6 +743,12 @@ void EmitContext::DefineOutputs() {
frag_outputs[i] = GetAttributeInfo(num_format, id, num_components, true);
++num_render_targets;
}
if (FragmentMotion() && !Sirit::ValidId(frag_outputs[MotionVectors::Output].id)) {
motion_frag_out = DefineOutput(F32[4], MotionVectors::Output);
motion_in_cur = DefineInput(F32[4], MotionVectors::CurrentLocation);
motion_in_prev = DefineInput(F32[4], MotionVectors::PreviousLocation);
Name(motion_frag_out, "motion_vector");
}
// Dual source blending allows at most 2 render targets, one for each source.
// Fewer targets are allowed but the missing blending source values will be
// undefined.
@@ -763,7 +775,7 @@ void EmitContext::DefineOutputs() {
void EmitContext::DefinePushDataBlock() {
// Create push constants block for instance steps rates
const Id struct_type{Name(TypeStruct(F32[1], F32[1], F32[1], F32[1], U32[4], U32[4], U32[4],
U32[4], U32[4], U32[4], U32[2]),
U32[4], U32[4], U32[4], U32[2], U32[1], U32[1]),
"AuxData")};
Decorate(struct_type, spv::Decoration::Block);
MemberName(struct_type, PushData::XOffsetIndex, "xoffset");
@@ -788,6 +800,10 @@ void EmitContext::DefinePushDataBlock() {
MemberDecorate(struct_type, PushData::BufOffsetIndex + 0, spv::Decoration::Offset, 80U);
MemberDecorate(struct_type, PushData::BufOffsetIndex + 1, spv::Decoration::Offset, 96U);
MemberDecorate(struct_type, PushData::BufOffsetIndex + 2, spv::Decoration::Offset, 112U);
MemberName(struct_type, PushData::MotionParamIndex, "motion_param");
MemberDecorate(struct_type, PushData::MotionParamIndex, spv::Decoration::Offset, 120U);
MemberName(struct_type, PushData::SceneSizeIndex, "scene_size");
MemberDecorate(struct_type, PushData::SceneSizeIndex, spv::Decoration::Offset, 124U);
push_data_block = DefineVar(struct_type, spv::StorageClass::PushConstant);
Name(push_data_block, "push_data");
interfaces.push_back(push_data_block);
@@ -252,6 +252,19 @@ public:
Id vertex_index{};
Id instance_id{};
Id push_data_block{};
// bbport: object motion vectors (runtime_info.h, MotionVectors).
Id motion_out_cur{};
Id motion_out_prev{};
Id motion_in_cur{};
Id motion_in_prev{};
Id motion_frag_out{};
[[nodiscard]] bool VertexMotion() const {
return sw_stage == SwStage::Vertex && hw_stage == HwStage::Vertex &&
runtime_info.hw.vs.motion_vectors && MotionVectors::positions_address != 0;
}
[[nodiscard]] bool FragmentMotion() const {
return sw_stage == SwStage::Fragment && runtime_info.hw.fs.motion_vectors;
}
Id base_vertex{};
Id base_instance{};
Id frag_coord{};
+4
View File
@@ -213,6 +213,8 @@ struct PushData {
static constexpr u32 YScaleIndex = 3;
static constexpr u32 UdRegsIndex = 4;
static constexpr u32 BufOffsetIndex = UdRegsIndex + NUM_USER_DATA_REGS / 4;
static constexpr u32 MotionParamIndex = BufOffsetIndex + NUM_BUFFERS / 16 + 1;
static constexpr u32 SceneSizeIndex = MotionParamIndex + 1;
float xoffset;
float yoffset;
@@ -220,6 +222,8 @@ struct PushData {
float yscale;
std::array<u32, NUM_USER_DATA_REGS> ud_regs;
std::array<u8, NUM_BUFFERS> buf_offsets;
u32 motion_param; ///< bbport: object motion parameter index (0: off)
u32 scene_size; ///< reduced width | height << 16; zero means native pixel coordinates
void AddOffset(u32 binding, u32 offset) {
ASSERT(offset < 256 && binding < buf_offsets.size());
@@ -78,6 +78,21 @@ struct SwVertexRuntimeInfo {
bool operator==(const SwVertexRuntimeInfo& other) const noexcept = default;
};
/// bbport: object motion vectors for temporal upscaling. G-buffer vertex shaders store their
/// clip-space positions per referenced vertex and read the previous frame's; the
/// fragment shader writes the screen-space difference to an extra color attachment.
struct MotionVectors {
static constexpr u32 CurrentLocation = 30; ///< varying: current clip position
static constexpr u32 PreviousLocation = 31; ///< varying: previous clip position, z = valid
static constexpr u32 Output = 7; ///< color attachment index
/// Per-draw parameters (u32x4: store base, load base, vertices per instance, flags) and
/// the position array (vec4 per vertex; element 0 is scratch), fixed for the session.
static inline u64 params_address = 0;
static inline u64 positions_address = 0;
static constexpr u32 FlagStore = 1;
static constexpr u32 FlagLoad = 2;
};
struct HwLocalRuntimeInfo {
u32 ls_stride;
@@ -96,6 +111,8 @@ struct HwVertexRuntimeInfo {
bool emulate_depth_negative_one_to_one{};
bool clip_disable{};
u32 user_clip_plane_mask{};
/// bbport: object motion vectors (G-buffer draws), see MotionVectors below.
bool motion_vectors{};
bool operator==(const HwVertexRuntimeInfo& other) const noexcept = default;
};
@@ -206,6 +223,8 @@ struct HwFragmentRuntimeInfo {
bool front_face_all_bits{false};
bool dual_source_blending{false};
bool clip_distance_emulation{false};
/// bbport: writes the object motion vector to MotionVectors::Output.
bool motion_vectors{false};
bool operator==(const HwFragmentRuntimeInfo& other) const noexcept {
return std::ranges::equal(color_buffers, other.color_buffers) &&
@@ -215,6 +234,7 @@ struct HwFragmentRuntimeInfo {
front_face_all_bits == other.front_face_all_bits &&
dual_source_blending == other.dual_source_blending &&
clip_distance_emulation == other.clip_distance_emulation &&
motion_vectors == other.motion_vectors &&
std::ranges::equal(inputs.begin(), inputs.begin() + num_inputs, other.inputs.begin(),
other.inputs.begin() + num_inputs);
}
+34 -1
View File
@@ -239,23 +239,40 @@ u64 Liverpool::HashRegisterPacket(u64 checksum, const u32* words, u32 count) {
return checksum;
}
void Liverpool::ApplyGraphicsRegisterPacket(Regs& regs, const PM4Header* header, u64& checksum) {
void Liverpool::ApplyGraphicsRegisterPacket(Regs& regs, const PM4Header* header, u64& checksum,
RegDirty* dirty) {
const u32 count = header->type3.NumWords();
const auto* payload = reinterpret_cast<const u32*>(header + 2);
const auto mark = [&](u32 word, u32 words) {
if (dirty) {
dirty->Mark(word, words);
}
};
const auto mark_field = [&](const auto& field) {
if (dirty) {
dirty->MarkField(regs, field);
}
};
switch (header->type3.opcode) {
case PM4ItOpcode::ClearState:
regs.SetDefaults();
if (dirty) {
dirty->Clear();
dirty->reset = true;
}
break;
case PM4ItOpcode::SetConfigReg: {
const auto* set_data = reinterpret_cast<const PM4CmdSetData*>(header);
std::memcpy(&regs.reg_array[Regs::ConfigRegWordOffset + set_data->reg_offset], payload,
(count - 1) * sizeof(u32));
mark(Regs::ConfigRegWordOffset + set_data->reg_offset, count - 1);
break;
}
case PM4ItOpcode::SetContextReg: {
const auto* set_data = reinterpret_cast<const PM4CmdSetData*>(header);
std::memcpy(&regs.reg_array[Regs::ContextRegWordOffset + set_data->reg_offset], payload,
(count - 1) * sizeof(u32));
mark(Regs::ContextRegWordOffset + set_data->reg_offset, count - 1);
break;
}
case PM4ItOpcode::SetShReg: {
@@ -265,6 +282,7 @@ void Liverpool::ApplyGraphicsRegisterPacket(Regs& regs, const PM4Header* header,
set_data->reg_offset <= (0x200 + sizeof(ComputeProgram) / 4))) {
std::memcpy(&regs.reg_array[Regs::ShRegWordOffset + set_data->reg_offset], payload,
(count - 1) * sizeof(u32));
mark(Regs::ShRegWordOffset + set_data->reg_offset, count - 1);
}
break;
}
@@ -272,10 +290,12 @@ void Liverpool::ApplyGraphicsRegisterPacket(Regs& regs, const PM4Header* header,
const auto* set_data = reinterpret_cast<const PM4CmdSetData*>(header);
std::memcpy(&regs.reg_array[Regs::UconfigRegWordOffset + set_data->reg_offset], payload,
(count - 1) * sizeof(u32));
mark(Regs::UconfigRegWordOffset + set_data->reg_offset, count - 1);
break;
}
case PM4ItOpcode::IndexType:
regs.index_buffer_type.raw = reinterpret_cast<const PM4CmdDrawIndexType*>(header)->raw;
mark_field(regs.index_buffer_type);
break;
case PM4ItOpcode::DrawIndex2: {
const auto* draw_index = reinterpret_cast<const PM4CmdDrawIndex2*>(header);
@@ -283,7 +303,11 @@ void Liverpool::ApplyGraphicsRegisterPacket(Regs& regs, const PM4Header* header,
regs.index_base_address.base_addr_lo = draw_index->index_base_lo;
regs.index_base_address.base_addr_hi = draw_index->index_base_hi;
regs.num_indices = draw_index->index_count;
mark_field(regs.max_index_size);
mark_field(regs.index_base_address);
mark_field(regs.num_indices);
regs.draw_initiator = draw_index->draw_initiator;
mark_field(regs.draw_initiator);
break;
}
case PM4ItOpcode::DrawIndexOffset2: {
@@ -291,10 +315,15 @@ void Liverpool::ApplyGraphicsRegisterPacket(Regs& regs, const PM4Header* header,
regs.max_index_size = draw_index_off->max_size;
regs.num_indices = draw_index_off->index_count;
regs.draw_initiator = draw_index_off->draw_initiator;
mark_field(regs.max_index_size);
mark_field(regs.num_indices);
mark_field(regs.draw_initiator);
break;
}
case PM4ItOpcode::DrawIndexAuto: {
const auto* draw_index = reinterpret_cast<const PM4CmdDrawIndexAuto*>(header);
mark_field(regs.num_indices);
mark_field(regs.draw_initiator);
regs.num_indices = draw_index->index_count;
regs.draw_initiator = draw_index->draw_initiator;
break;
@@ -302,15 +331,18 @@ void Liverpool::ApplyGraphicsRegisterPacket(Regs& regs, const PM4Header* header,
case PM4ItOpcode::NumInstances:
regs.num_instances.num_instances =
reinterpret_cast<const PM4CmdDrawNumInstances*>(header)->num_instances;
mark_field(regs.num_instances);
break;
case PM4ItOpcode::IndexBase: {
const auto* index_base = reinterpret_cast<const PM4CmdDrawIndexBase*>(header);
regs.index_base_address.base_addr_lo = index_base->addr_lo;
regs.index_base_address.base_addr_hi = index_base->addr_hi;
mark_field(regs.index_base_address);
break;
}
case PM4ItOpcode::IndexBufferSize:
regs.num_indices = reinterpret_cast<const PM4CmdDrawIndexBufferSize*>(header)->num_indices;
mark_field(regs.num_indices);
break;
case PM4ItOpcode::EventWrite: {
const auto* event = reinterpret_cast<const PM4CmdEventWrite*>(header);
@@ -319,6 +351,7 @@ void Liverpool::ApplyGraphicsRegisterPacket(Regs& regs, const PM4Header* header,
}
// TODO: handle proper synchronization, for now signal that update is done immediately
regs.cp_strmout_cntl.offset_update_done = 1;
mark_field(regs.cp_strmout_cntl);
break;
}
default:
+3 -1
View File
@@ -70,7 +70,9 @@ struct Liverpool {
static constexpr u64 NoSeq = ~0ull;
u64 gfx_submit_seq{};
static u64 HashRegisterPacket(u64 checksum, const u32* words, u32 count);
static void ApplyGraphicsRegisterPacket(Regs& regs, const PM4Header* header, u64& checksum);
/// `dirty` (draw-preparation scanner) records the register blocks written.
static void ApplyGraphicsRegisterPacket(Regs& regs, const PM4Header* header, u64& checksum,
RegDirty* dirty = nullptr);
public:
explicit Liverpool();
+63
View File
@@ -3,6 +3,10 @@
#pragma once
#include <algorithm>
#include <bitset>
#include <cstring>
#include <vector>
#include "video_core/amdgpu/regs_color.h"
#include "video_core/amdgpu/regs_depth.h"
#include "video_core/amdgpu/regs_primitive.h"
@@ -189,6 +193,65 @@ union Regs {
void SetDefaults();
};
// bbport: register blocks written by a stretch of packets, and their values at its end. The
// draw-preparation scanner records one per submission so a worker reaches the state at the
// start of any later submission without replaying the packets in between.
struct RegDirty {
static constexpr u32 BlockWords = 32;
static constexpr u32 NumBlocks = Regs::NumRegs / BlockWords;
std::bitset<NumBlocks> blocks;
bool reset = false; ///< ClearState: defaults, then only the blocks marked after it
void Mark(u32 word, u32 count) {
if (count == 0 || word >= Regs::NumRegs) {
return;
}
const u32 last = std::min(word + count, Regs::NumRegs) - 1;
for (u32 block = word / BlockWords; block <= last / BlockWords; ++block) {
blocks.set(block);
}
}
template <typename T>
void MarkField(const Regs& regs, const T& field) {
const auto offset = reinterpret_cast<const u8*>(&field) -
reinterpret_cast<const u8*>(regs.reg_array.data());
Mark(u32(offset / sizeof(u32)), u32((sizeof(T) + sizeof(u32) - 1) / sizeof(u32)));
}
void Clear() {
blocks.reset();
reset = false;
}
};
struct RegDelta {
bool reset = false;
std::vector<u16> blocks;
std::vector<u32> words;
void Capture(const Regs& regs, const RegDirty& dirty) {
reset = dirty.reset;
blocks.clear();
words.clear();
for (u32 block = 0; block < RegDirty::NumBlocks; ++block) {
if (dirty.blocks.test(block)) {
blocks.push_back(u16(block));
const u32* src = regs.reg_array.data() + block * RegDirty::BlockWords;
words.insert(words.end(), src, src + RegDirty::BlockWords);
}
}
}
void Apply(Regs& regs) const {
if (reset) {
regs.SetDefaults();
}
for (size_t i = 0; i < blocks.size(); ++i) {
std::memcpy(regs.reg_array.data() + blocks[i] * RegDirty::BlockWords,
words.data() + i * RegDirty::BlockWords,
RegDirty::BlockWords * sizeof(u32));
}
}
};
#undef DO_CONCAT2
#undef CONCAT2
#undef INSERT_PADDING_WORDS
@@ -1,6 +1,10 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <chrono>
#include <array>
#include <map>
#include <unordered_map>
#include <algorithm>
#include <bit>
#include <cstdlib>
@@ -198,10 +202,102 @@ void BufferCache::DownloadMemory(const Buffer* arena, VAddr device_addr, u64 siz
memory_tracker->UnmarkRegionAsGpuModified(device_addr, size);
}
namespace {
// bbport: BB_BUFFER_STATS=1 — how buffer bindings reach the GPU, by guest region (256 MiB),
// printed every 5 s: small read-only copies into the stream buffer, arena bindings, and the
// bytes those re-upload after CPU writes. Input for engine-level short paths.
struct BufferStats {
struct Region {
u64 stream_count{}, stream_bytes{}, arena_count{}, arena_bytes{}, upload_bytes{};
};
std::map<u64, Region> regions;
/// Hot window: frame of the last binding per 64 KiB block, and the distribution of the
/// frames between uses (1, 2, 3, 4, 5-8, 9-16, 17+): the ring's reuse distance.
std::unordered_map<u64, u64> last_use;
std::array<u64, 7> reuse{};
std::chrono::steady_clock::time_point window = std::chrono::steady_clock::now();
u64 calls = 0;
};
/// 256 MiB regions; 1 MiB inside the hot 0x104xxxxxxx window (the engine's frame data).
BufferStats& Stats();
void NoteHotUse(VAddr address, u32 size) {
if ((address >> 28) != 0x104) {
return;
}
auto& st = Stats();
const u64 frame = BbStats::gpu_frames.load(std::memory_order_relaxed);
for (u64 block = address >> 16; block <= (address + size - 1) >> 16; ++block) {
auto [it, inserted] = st.last_use.try_emplace(block, frame);
if (!inserted && it->second != frame) {
const u64 d = frame - it->second;
++st.reuse[d <= 4 ? d - 1 : d <= 8 ? 4 : d <= 16 ? 5 : 6];
it->second = frame;
}
}
}
u64 RegionKey(VAddr address) {
return (address >> 28) == 0x104 ? (address >> 20) | (1ull << 40) : address >> 28;
}
bool BufferStatsEnabled() {
static const bool enabled = [] {
const char* value = std::getenv("BB_BUFFER_STATS");
return value && value[0] == '1';
}();
return enabled;
}
BufferStats& Stats() {
static BufferStats stats;
return stats;
}
void PrintBufferStats() {
auto& st = Stats();
if ((++st.calls & 4095) != 0) {
return;
}
const auto now = std::chrono::steady_clock::now();
const double seconds = std::chrono::duration<double>(now - st.window).count();
if (seconds < 5.0) {
return;
}
std::printf("Buffer stats (%.1f s, per second): region, stream copies/bytes, arena bindings, "
"re-uploaded bytes\n", seconds);
std::printf(" hot window reuse distance in frames (1,2,3,4,5-8,9-16,17+): %llu %llu %llu %llu "
"%llu %llu %llu\n",
(unsigned long long)st.reuse[0], (unsigned long long)st.reuse[1],
(unsigned long long)st.reuse[2], (unsigned long long)st.reuse[3],
(unsigned long long)st.reuse[4], (unsigned long long)st.reuse[5],
(unsigned long long)st.reuse[6]);
st.reuse = {};
for (const auto& [region, r] : st.regions) {
const u64 base = (region >> 40) ? (region & ((1ull << 40) - 1)) << 20 : region << 28;
if ((region >> 40) && r.stream_bytes + r.upload_bytes < 1e6 * seconds) {
continue; // quiet MiB of the hot window
}
std::printf(" %#012llx: %8.0f copies %8.2f MB, %8.0f arena %8.2f MB bound, %8.2f MB uploads\n",
static_cast<unsigned long long>(base), r.stream_count / seconds,
r.stream_bytes / seconds / 1e6, r.arena_count / seconds,
r.arena_bytes / seconds / 1e6,
r.upload_bytes / seconds / 1e6);
}
st.regions.clear();
st.window = now;
}
} // namespace
std::pair<const Buffer*, u64> BufferCache::ObtainBuffer(VAddr device_addr, u32 size,
bool is_written, bool is_texel_buffer) {
const bool stats = BufferStatsEnabled();
if (stats) {
PrintBufferStats();
NoteHotUse(device_addr, size);
}
// For read-only buffers use device local stream buffer to reduce renderpass breaks.
if (!is_written && size <= STREAM_THRESHOLD && !IsRegionGpuModified(device_addr, size)) {
if (stats) {
auto& region = Stats().regions[RegionKey(device_addr)];
++region.stream_count;
region.stream_bytes += size;
}
// bbport: the guest data is copied on a copy thread, started now; submission and
// guest-visible fences wait for it (Scheduler::WaitHostCopies).
if (!stream_buffer.mapped_data.empty() &&
@@ -227,7 +323,15 @@ std::pair<const Buffer*, u64> BufferCache::ObtainBuffer(VAddr device_addr, u32 s
const u64 last_block = (device_addr + size - 1) >> block_shift;
const auto* arena = GetArena(first_block, last_block);
EnsureResident(arena, first_block, last_block);
const u64 uploaded_before = BbStats::buffer_upload_bytes.load(std::memory_order_relaxed);
SynchronizeMemory(arena, device_addr, size, is_written, is_texel_buffer);
if (stats) {
auto& region = Stats().regions[RegionKey(device_addr)];
++region.arena_count;
region.arena_bytes += size;
region.upload_bytes +=
BbStats::buffer_upload_bytes.load(std::memory_order_relaxed) - uploaded_before;
}
if (is_written) {
gpu_modified_ranges.Add(device_addr, size);
}
@@ -13,6 +13,8 @@ layout(binding = 2, std430) readonly buffer PrevFrame { uint prev_frame[]; };
layout(binding = 3, std430) writeonly buffer NextFrame { uint next_frame[]; };
#else
layout(binding = 1, rg16f) uniform writeonly image2D motion_img; // pixels, previous - current
// Object motion from the G-buffer: rg pixels, b = valid, a = written depth.
layout(binding = 2) uniform texture2D object_tex;
#endif
layout(push_constant) uniform Constants {
@@ -20,8 +22,10 @@ layout(push_constant) uniform Constants {
vec4 proj; // current: x scale, y scale, z scale, z offset
vec4 prev_proj; // previous: x scale, y scale, z scale, z offset
vec2 size; // pixels
uint mode; // debug: 0 motion, 1 raw depth, 2 view z / 50, 3 reprojected previous
// frame, 4 |reprojected previous - current|
vec2 jitter; // current viewport shift in render pixels (x right, y down)
vec2 previous_jitter;
uint mode; // motion: 1 = object motion vectors available; debug: 0 motion, 1 raw
// depth, 2 view z / 50, 3 reprojected previous frame, 4 |difference|
};
void main() {
@@ -31,7 +35,10 @@ void main() {
}
const float depth = texelFetch(depth_tex, pixel, 0).r;
const vec2 uv = (vec2(pixel) + 0.5) / size;
const vec2 ndc = vec2(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0);
// Depth was rasterised through a shifted viewport. Reconstruct its unjittered
// position, and keep jitter out of FSR's motion vectors (as for the object vectors).
const vec2 scene_uv = uv - jitter / size;
const vec2 ndc = vec2(scene_uv.x * 2.0 - 1.0, 1.0 - scene_uv.y * 2.0);
// Clip z = zs * z + zo, w = z; depth = zs + zo / z (zo < 0, depth < zs in front).
const float denom = depth - proj.z;
@@ -42,14 +49,14 @@ void main() {
const vec2 prev_ndc =
vec2(prev_view.x * prev_proj.x, prev_view.y * prev_proj.y) / max(prev_view.z, 1e-4);
const vec2 prev_uv = vec2(prev_ndc.x * 0.5 + 0.5, 0.5 - prev_ndc.y * 0.5);
const vec2 motion = (prev_uv - uv) * size;
const vec2 motion = (prev_uv - scene_uv) * size;
const bool sky = depth >= 0.99999;
#ifdef DEBUG_OVERLAY
const vec4 current = imageLoad(color_img, pixel);
next_frame[pixel.y * int(size.x) + pixel.x] = packUnorm4x8(current);
if (mode == 3 || mode == 4) {
const ivec2 prev_pixel = ivec2(floor(uv * size + motion));
const ivec2 prev_pixel = ivec2(floor(uv * size + motion + previous_jitter - jitter));
vec4 prev = vec4(0.0, 0.0, 0.0, 1.0);
if (all(greaterThanEqual(prev_pixel, ivec2(0))) && all(lessThan(prev_pixel, ivec2(size)))) {
prev = unpackUnorm4x8(prev_frame[prev_pixel.y * int(size.x) + prev_pixel.x]);
@@ -67,6 +74,17 @@ void main() {
const vec3 mv_color = sky ? vec3(0.0, 0.0, 0.3) : vec3(0.5) + vec3(motion / 128.0, 0.0);
imageStore(color_img, pixel, vec4(mix(current.rgb, mv_color, 0.75), current.a));
#else
imageStore(motion_img, pixel, vec4(sky ? vec2(0.0) : motion, 0.0, 0.0));
vec2 result = sky ? vec2(0.0) : motion;
if (mode == 1) {
const vec4 object_motion = texelFetch(object_tex, pixel, 0);
// A later ordinary draw may have covered this pixel without touching the motion
// target. Its final depth then differs from the depth stored with the vector.
if (object_motion.b > 0.99 && abs(object_motion.a - depth) < 0.001 &&
!any(isnan(object_motion.rg)) &&
!any(isinf(object_motion.rg))) {
result = object_motion.rg;
}
}
imageStore(motion_img, pixel, vec4(result, 0.0, 0.0));
#endif
}
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: nearest resample of a depth/stencil image between native and reduced scene
// targets. Used where the format has no blit support (D32S8 on RADV).
#version 450 core
#extension GL_EXT_samplerless_texture_functions : require
#if defined(HAS_STENCIL)
#extension GL_ARB_shader_stencil_export : require
layout (binding = 1, set = 0) uniform utexture2D src_stencil;
#endif
layout (binding = 0, set = 0) uniform texture2D src_depth;
layout (location = 0) in vec2 uv;
void main() {
const ivec2 size = textureSize(src_depth, 0);
const ivec2 coord = min(ivec2(uv * vec2(size)), size - 1);
gl_FragDepth = texelFetch(src_depth, coord, 0).r;
#if defined(HAS_STENCIL)
gl_FragStencilRefARB = int(texelFetch(src_stencil, coord, 0).r);
#endif
}
@@ -1,13 +1,18 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: writes the upscaler's color back into the game's scene color, keeping the game's alpha
// (post-processing reads data from it). Mode 1 (debug) shows the reactive mask in red instead.
// (post-processing reads data from it). Debug modes: 1 shows the reactive mask in red,
// 2 the motion vectors given to the upscaler (red/green: |x|/|y|, 8 render pixels = full;
// blue: the pixel has an object motion vector from its vertices, not camera motion).
#version 450
#extension GL_EXT_samplerless_texture_functions : require
layout(local_size_x = 8, local_size_y = 8) in;
layout(binding = 0, rgba16f) uniform readonly image2D upscaled;
layout(binding = 1, rgba16f) uniform image2D scene;
layout(binding = 2, r8) uniform readonly image2D reactive;
layout(binding = 3) uniform texture2D motion; // render size, pixels
layout(binding = 4) uniform texture2D objects; // render size, b = object vector written
layout(push_constant) uniform Params {
uint mode;
@@ -21,8 +26,16 @@ void main() {
const vec4 original = imageLoad(scene, pixel);
vec3 color = imageLoad(upscaled, pixel).rgb;
if (mode == 1) {
const float mask = imageLoad(reactive, pixel).r;
const float mask = imageLoad(reactive, min(pixel * imageSize(reactive) / imageSize(scene), imageSize(reactive) - 1)).r;
color = mix(color * 0.25, vec3(4.0, 0.0, 0.0), mask);
} else if (mode == 2) {
const ivec2 size = textureSize(motion, 0);
const ivec2 source = min(pixel * size / imageSize(scene), size - 1);
const vec2 mv = texelFetch(motion, source, 0).rg;
const ivec2 object_size = textureSize(objects, 0);
const bool object = object_size == size && texelFetch(objects, source, 0).b > 0.99;
const float luma = dot(min(color, vec3(1.0)), vec3(0.3, 0.6, 0.1));
color = vec3(luma * 0.3) + vec3(clamp(abs(mv) / 8.0, 0.0, 1.0), object ? 0.6 : 0.0);
}
imageStore(scene, pixel, vec4(color, original.a));
}
@@ -0,0 +1,237 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <limits>
#include <span>
#include <unordered_map>
namespace Vulkan::Motion {
struct VertexRange {
uint32_t first{}, count{};
bool operator==(const VertexRange&) const = default;
};
// Reserve only the vertices referenced by this draw, not the whole shared vertex buffer.
// Primitive restart markers are not vertex shader invocations. Reject signed overflow.
template <class Index, size_t Extent>
VertexRange IndexedRange(std::span<const Index, Extent> indices, int32_t base_vertex, bool restart) {
uint32_t lo = UINT32_MAX, hi = 0;
for (const auto index : indices) {
if (restart && index == std::numeric_limits<Index>::max()) {
continue;
}
lo = std::min(lo, uint32_t(index));
hi = std::max(hi, uint32_t(index));
}
if (lo > hi || int64_t(lo) + base_vertex < 0 ||
int64_t(hi) + base_vertex > INT32_MAX) {
return {};
}
return {uint32_t(int64_t(lo) + base_vertex), hi - lo + 1};
}
// Vertex shader constant buffers (stride 16) of Bloodborne's G-buffer shaders: the 864-byte
// scene constants (camera), 416-byte model constants and palettes of 3x4 bone matrices
// (48 bytes per bone). Characters have 656+ bytes; weapons and props 2-8 bones (96-384).
enum class BufferRole { Other, Skeleton, SmallSkeleton };
inline BufferRole ClassifyBuffer(uint32_t size) {
if (size == 864 || size > 16384) {
return BufferRole::Other;
}
if (size >= 640) {
return BufferRole::Skeleton;
}
return size >= 96 && size % 48 == 0 ? BufferRole::SmallSkeleton : BufferRole::Other;
}
// Scanning and hashing the index list of every stored draw each frame took ~10% of the GPU
// thread. Static meshes keep their index buffers, so a result is reused and fully verified
// again every `Revalidate` frames. A stale result only costs camera-quality vectors: the
// shader bounds every history access by the stored range.
class IndexRangeCache {
public:
static constexpr uint64_t Revalidate = 32, Unused = 600;
struct Key {
uint64_t address{};
uint32_t count{}, index_size{};
int32_t base_vertex{};
bool restart{};
bool operator==(const Key&) const = default;
};
struct Result {
VertexRange range;
uint64_t topology{};
};
struct Stats {
uint64_t hits{}, scans{}, stale{};
} stats;
template <class Scan>
Result Get(const Key& key, uint64_t frame, Scan&& scan) {
auto& entry = entries[key];
entry.last_use = frame;
if (entry.valid && frame - entry.verified < Revalidate) {
++stats.hits;
return entry.result;
}
++stats.scans;
const Result fresh = scan();
if (entry.valid && (fresh.topology != entry.result.topology ||
!(fresh.range == entry.result.range))) {
++stats.stale; // the cached result was wrong for up to Revalidate frames
}
entry.result = fresh;
entry.verified = frame;
entry.valid = true;
return entry.result;
}
void Trim(uint64_t frame) {
std::erase_if(entries, [frame](const auto& e) { return frame - e.second.last_use > Unused; });
}
size_t Size() const { return entries.size(); }
private:
struct Entry {
Result result;
uint64_t verified{}, last_use{};
bool valid{};
};
struct KeyHash {
size_t operator()(const Key& k) const {
uint64_t h = k.address * 0x9e3779b97f4a7c15ULL;
h ^= (uint64_t(k.count) << 32 | uint32_t(k.base_vertex)) + (h << 6) + (h >> 2);
h ^= uint64_t(k.index_size << 1 | uint32_t(k.restart)) + (h << 6) + (h >> 2);
return size_t(h);
}
};
std::unordered_map<Key, Entry, KeyHash> entries;
};
inline uint32_t JitterPhases(uint32_t render_width, uint32_t output_width) {
if (!render_width || !output_width) {
return 8;
}
const double ratio = double(output_width) / render_width;
return uint32_t(std::clamp(std::ceil(8.0 * ratio * ratio), 8.0, 256.0));
}
struct Draw {
uint64_t shader{}, geometry{}, indices{}, topology{};
uint32_t index_count{}, instances{}, first_instance{};
VertexRange vertices;
bool operator==(const Draw& rhs) const = default;
};
// CPU bookkeeping shared with the regression test. Draw identity includes topology and
// offsets: a reused index buffer or another submesh must never read uninitialised history.
class History {
public:
explicit History(uint32_t capacity) : capacity{capacity} {}
struct Allocation {
uint32_t store{}, load{}, vertices{}, instances{}, first_vertex{}, first_instance{};
};
struct Stats {
uint64_t draws{}, stored{}, loaded{}, unmatched{}, exhausted{}, invalid{}, still{};
} stats;
// A small skeleton (weapon, prop; most are static world pieces) only needs history
// while its bone palette changes: with last frame's bones its motion is the camera's.
// Checked before the index scan of Prepare, with a cheap identity. The first frame
// of a change stores positions (nothing to load yet), the next one loads them.
struct GateKey {
uint64_t shader{}, geometry{}, indices{};
uint32_t index_count{}, instances{}, first_instance{}, occurrence{};
bool operator==(const GateKey&) const = default;
};
bool Moving(GateKey key, uint64_t palette) {
key.occurrence = 0;
key.occurrence = gate_occurrences[key]++;
const auto prev = gate_previous.find(key);
const bool moving = prev != gate_previous.end() && prev->second != palette;
gate_current.emplace(key, palette);
stats.still += moving ? 0 : 1;
return moving;
}
void NextFrame() {
previous.swap(current);
current.clear();
occurrences.clear();
gate_previous.swap(gate_current);
gate_current.clear();
gate_occurrences.clear();
++frame;
used = 0;
}
Allocation Prepare(const Draw& draw) {
++stats.draws;
Key key{draw, 0};
key.occurrence = occurrences[key]++;
const uint64_t count = uint64_t(draw.vertices.count) * draw.instances;
if (!count || uint64_t(draw.vertices.first) + draw.vertices.count - 1 > INT32_MAX ||
uint64_t(draw.first_instance) + draw.instances > UINT32_MAX) {
++stats.invalid;
return {};
}
const auto prev = previous.find(key);
Allocation result{0, 0, draw.vertices.count, draw.instances,
draw.vertices.first, draw.first_instance};
if (prev != previous.end()) {
result.load = prev->second;
++stats.loaded;
} else {
++stats.unmatched;
}
if (count <= capacity - used) {
result.store = 1 + uint32_t(frame & 1) * capacity + used;
used += uint32_t(count);
current.emplace(key, result.store);
++stats.stored;
} else {
++stats.exhausted;
}
return result;
}
uint32_t Used() const { return used; }
private:
struct Key {
Draw draw;
uint32_t occurrence;
bool operator==(const Key&) const = default;
};
struct Hash {
size_t operator()(const Key& key) const {
uint64_t h = 0;
const auto mix = [&](uint64_t v) { h ^= v + 0x9e3779b97f4a7c15ULL + (h << 6) + (h >> 2); };
const auto& d = key.draw;
mix(d.shader); mix(d.geometry); mix(d.indices); mix(d.topology);
mix(d.index_count); mix(d.instances); mix(d.first_instance);
mix(d.vertices.first); mix(d.vertices.count); mix(key.occurrence);
return size_t(h);
}
};
uint32_t capacity{}, used{};
uint64_t frame{};
std::unordered_map<Key, uint32_t, Hash> previous, current, occurrences;
struct GateHash {
size_t operator()(const GateKey& key) const {
uint64_t h = 0;
const auto mix = [&](uint64_t v) { h ^= v + 0x9e3779b97f4a7c15ULL + (h << 6) + (h >> 2); };
mix(key.shader); mix(key.geometry); mix(key.indices); mix(key.index_count);
mix(key.instances); mix(key.first_instance); mix(key.occurrence);
return size_t(h);
}
};
std::unordered_map<GateKey, uint64_t, GateHash> gate_previous, gate_current;
std::unordered_map<GateKey, uint32_t, GateHash> gate_occurrences;
};
} // namespace Vulkan::Motion
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <algorithm>
#include <array>
#include <cstdint>
namespace Vulkan::SceneResolution {
struct Size {
uint32_t width = 1920, height = 1080;
bool operator==(const Size&) const = default;
};
inline Size ForPreset(int preset) {
// Even dimensions for half-resolution effects; match the preset labels exactly.
constexpr std::array<Size, 5> sizes{{{1920,1080}, {1280,720}, {1130,636},
{960,540}, {640,360}}};
return sizes[std::clamp(preset, 0, 4)];
}
constexpr uint32_t Pack(Size size) { return size.width | (size.height << 16); }
constexpr Size Unpack(uint32_t packed) {
return packed ? Size{packed & 65535u, packed >> 16} : Size{};
}
// A proxy and the guest-size image hold the same logical surface. Native shader reads
// need a resolve after proxy writes; native writes invalidate the cached proxy.
struct Coherence {
bool valid = false, dirty = false;
void ProxyWrite() { valid = dirty = true; }
void CopiedToProxy() { valid = true; dirty = false; }
void Resolved() { dirty = false; }
void NativeWrite() { valid = dirty = false; }
};
} // namespace Vulkan::SceneResolution
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <cmath>
#include <cstdint>
namespace Vulkan::UiComposition {
enum class Background { None, Copy, Temporal };
// UI composition must not require scene depth/camera or an active FSR context.
constexpr Background Choose(bool scaled, bool ui_draw, bool scene_ready, bool fsr_active) {
if (!scaled || !ui_draw) {
return Background::None;
}
return scene_ready && fsr_active ? Background::Temporal : Background::Copy;
}
inline bool NativeViewport(float width, float height) {
return std::abs(std::abs(width) - 1920.0f) < 0.5f &&
std::abs(std::abs(height) - 1080.0f) < 0.5f;
}
inline std::array<float, 2> Scale(uint32_t guest_width, uint32_t guest_height,
uint32_t output_width, uint32_t output_height,
bool native_coordinates) {
return {float(output_width) / float(native_coordinates ? 1920 : guest_width),
float(output_height) / float(native_coordinates ? 1080 : guest_height)};
}
} // namespace Vulkan::UiComposition
@@ -11,6 +11,7 @@
#include "video_core/host_shaders/camera_motion_comp.h"
#include "video_core/host_shaders/camera_motion_debug_comp.h"
#include "video_core/renderer_vulkan/vk_instance.h"
#include "video_core/renderer_vulkan/vk_object_motion.h"
#include "video_core/renderer_vulkan/vk_runtime.h"
#include "video_core/renderer_vulkan/vk_scheduler.h"
#include "video_core/renderer_vulkan/vk_shader_util.h"
@@ -25,6 +26,8 @@ struct PushConstants {
std::array<float, 4> proj;
std::array<float, 4> prev_proj;
std::array<float, 2> size;
std::array<float, 2> jitter;
std::array<float, 2> previous_jitter;
u32 mode;
};
@@ -56,7 +59,7 @@ CameraMotion::CameraMotion(const Instance& instance_, Scheduler& scheduler_,
for_upscaler = !(upscaler && std::strcmp(upscaler, "none") == 0);
const auto device = instance.GetDevice();
if (for_upscaler) {
const std::array<vk::DescriptorSetLayoutBinding, 2> motion_bindings = {{
const std::array<vk::DescriptorSetLayoutBinding, 3> motion_bindings = {{
{.binding = 0,
.descriptorType = vk::DescriptorType::eSampledImage,
.descriptorCount = 1,
@@ -65,6 +68,10 @@ CameraMotion::CameraMotion(const Instance& instance_, Scheduler& scheduler_,
.descriptorType = vk::DescriptorType::eStorageImage,
.descriptorCount = 1,
.stageFlags = vk::ShaderStageFlagBits::eCompute},
{.binding = 2,
.descriptorType = vk::DescriptorType::eSampledImage,
.descriptorCount = 1,
.stageFlags = vk::ShaderStageFlagBits::eCompute},
}};
motion_desc_layout = Check(device.createDescriptorSetLayoutUnique({
.flags = vk::DescriptorSetLayoutCreateFlagBits::ePushDescriptorKHR,
@@ -154,20 +161,31 @@ float CameraMotion::Near() const noexcept {
return -current.proj[3] / current.proj[2];
}
vk::ImageView CameraMotion::ObjectMotionView() const noexcept {
return object_motion && object_motion->Enabled() ? object_motion->View() : vk::ImageView{};
}
void CameraMotion::RecordMotion(vk::CommandBuffer cmdbuf, vk::ImageView depth_view,
vk::ImageView motion_view, u32 width, u32 height) {
bool object_valid = false;
const vk::ImageView object_view = object_motion && object_motion->Enabled()
? object_motion->PrepareRead(cmdbuf, width, height, object_valid) : depth_view;
const PushConstants push{
.reproject = Multiply(previous.view, current.inv_view),
.proj = current.proj,
.prev_proj = previous.proj,
.size = {float(width), float(height)},
.mode = 0,
.jitter = jitter,
.previous_jitter = previous_jitter,
.mode = object_valid ? 1u : 0u,
};
const vk::DescriptorImageInfo depth_info{.imageView = depth_view,
.imageLayout = vk::ImageLayout::eGeneral};
const vk::DescriptorImageInfo motion_info{.imageView = motion_view,
.imageLayout = vk::ImageLayout::eGeneral};
const std::array<vk::WriteDescriptorSet, 2> writes = {{
const vk::DescriptorImageInfo object_info{.imageView = object_view,
.imageLayout = vk::ImageLayout::eGeneral};
const std::array<vk::WriteDescriptorSet, 3> writes = {{
{.dstBinding = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eSampledImage,
@@ -176,6 +194,10 @@ void CameraMotion::RecordMotion(vk::CommandBuffer cmdbuf, vk::ImageView depth_vi
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eStorageImage,
.pImageInfo = &motion_info},
{.dstBinding = 2,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eSampledImage,
.pImageInfo = &object_info},
}};
cmdbuf.bindPipeline(vk::PipelineBindPoint::eCompute, *motion_pipeline);
cmdbuf.pushDescriptorSetKHR(vk::PipelineBindPoint::eCompute, *motion_pipeline_layout, 0,
@@ -210,6 +232,7 @@ void CameraMotion::OnDisplayPass(VideoCore::ImageId frame) {
if (debug_overlay && frame && depth_id && current.valid && previous.valid) {
Overlay(frame);
}
if (!frame_has_camera) InvalidateHistory();
frame_has_camera = false;
depth_id = {};
}
@@ -259,6 +282,8 @@ void CameraMotion::Overlay(VideoCore::ImageId frame) {
.proj = current.proj,
.prev_proj = previous.proj,
.size = {float(color.info.size.width), float(color.info.size.height)},
.jitter = jitter,
.previous_jitter = previous_jitter,
.mode = BbToggle::Disabled(1u << 20) ? 1u
: BbToggle::Disabled(1u << 21) ? 2u
: BbToggle::Disabled(1u << 22) ? 3u
@@ -22,12 +22,24 @@ namespace Vulkan {
class Instance;
class Scheduler;
class Runtime;
class ObjectMotion;
class CameraMotion {
public:
CameraMotion(const Instance& instance, Scheduler& scheduler,
VideoCore::TextureCache& texture_cache, Runtime& runtime);
~CameraMotion();
void InvalidateHistory() { current.valid = previous.valid = false; }
void SetObjectMotion(ObjectMotion* motion) noexcept {
object_motion = motion;
}
/// This frame's object motion image (after RecordMotion), for the debug view; or null.
vk::ImageView ObjectMotionView() const noexcept;
void SetJitter(std::array<float, 2> value) noexcept {
previous_jitter = jitter;
jitter = value;
}
[[nodiscard]] bool Enabled() const noexcept {
return debug_overlay || for_upscaler;
@@ -79,8 +91,10 @@ private:
vk::UniquePipelineLayout motion_pipeline_layout;
vk::UniquePipeline motion_pipeline;
ObjectMotion* object_motion = nullptr;
Camera current, previous;
bool frame_has_camera = false;
std::array<float, 2> jitter{}, previous_jitter{};
VideoCore::ImageId depth_id{};
vk::UniqueDescriptorSetLayout desc_layout;
@@ -7,11 +7,14 @@
#include <cstdlib>
#include <cstring>
#include "bbport_threads.h"
#include "bbport_toggles.h"
#include "common/thread.h"
#include "video_core/amdgpu/liverpool.h"
#include "video_core/amdgpu/pm4_cmds.h"
#include "video_core/renderer_vulkan/vk_draw_prep.h"
#include "video_core/renderer_vulkan/liverpool_to_vk.h"
#include <xxhash.h>
namespace Vulkan {
@@ -46,12 +49,11 @@ void ForEachPacket(std::span<const u32> commands, Func&& func) {
u32 DefaultWorkerCount() {
if (const char* env = std::getenv("BB_PREP_WORKERS")) {
return static_cast<u32>(std::clamp(std::atoi(env), 0, 8));
return static_cast<u32>(std::clamp(std::atoi(env), 0, 16));
}
// The game keeps ~9 threads busy (main, 5 render workers, physics, audio) next to the GPU
// and recording threads: prepare only where there are cores to spare.
const u32 threads = std::thread::hardware_concurrency();
return threads >= 12 ? std::min<u32>(4, threads / 4) : 0;
// Workers are SCHED_IDLE and claim whole buffers, so more of them only use more idle
// cores: half the hardware threads (Steam Deck 4, 16-thread desktop 8).
return std::clamp<u32>(BbThreads::Available() / 2, 1, 8);
}
/// Flattened user data of one submission: chunks never move, so the GPU thread can read a
@@ -62,34 +64,151 @@ struct FlatArena {
size_t used = ChunkWords;
const u32* Append(const std::vector<u32>& data) {
if (chunks.empty() || data.size() > ChunkWords - used) {
chunks.push_back(std::make_unique<u32[]>(std::max(ChunkWords, data.size())));
return AppendArray(data.data(), data.size());
}
/// Trivially copyable elements, 8-byte aligned (sharps hold u64 bit fields).
template <typename T>
const T* AppendArray(const T* data, size_t count) {
static_assert(std::is_trivially_copyable_v<T> && alignof(T) <= 8);
const size_t words = (count * sizeof(T) + sizeof(u32) - 1) / sizeof(u32);
used = (used + 1) & ~size_t(1);
if (chunks.empty() || words > ChunkWords - std::min(used, ChunkWords)) {
chunks.push_back(std::make_unique<u32[]>(std::max(ChunkWords, words)));
used = 0;
}
u32* dst = chunks.back().get() + used;
std::memcpy(dst, data.data(), data.size() * sizeof(u32));
used += data.size();
return dst;
if (count) {
std::memcpy(dst, data, count * sizeof(T));
}
used += words;
return reinterpret_cast<const T*>(dst);
}
};
/// Reads the stage's resource sharps from the worker's flattened user data, so the GPU thread
/// only binds them (the sharp fetches and texture-description hashing were ~7% of it).
void PrepareResources(const Shader::Info& info, PreparedStage& stage, FlatArena& arena) {
thread_local std::vector<AmdGpu::Image> images;
thread_local std::vector<u64> hashes;
thread_local std::vector<AmdGpu::Sampler> samplers;
thread_local std::vector<AmdGpu::Buffer> buffers;
images.clear();
hashes.clear();
samplers.clear();
buffers.clear();
for (const auto& res : info.images) {
images.push_back(res.GetSharp(info));
hashes.push_back(ImageDescHash(images.back(), res));
}
for (const auto& res : info.samplers) {
samplers.push_back(res.GetSharp(info));
}
for (const auto& res : info.buffers) {
buffers.push_back(res.IsSpecial() ? AmdGpu::Buffer::Null() : res.GetSharp(info));
}
stage.image_sharps = arena.AppendArray(images.data(), images.size());
stage.image_hashes = arena.AppendArray(hashes.data(), hashes.size());
stage.sampler_sharps = arena.AppendArray(samplers.data(), samplers.size());
stage.buffer_sharps = arena.AppendArray(buffers.data(), buffers.size());
stage.num_images = static_cast<u32>(images.size());
stage.num_samplers = static_cast<u32>(samplers.size());
stage.num_buffers = static_cast<u32>(buffers.size());
}
/// Rasterizer::BindVertexBuffers' pure part (GraphicsPipeline::GetVertexInputs for the dynamic
/// vertex input path, and the merge of the stream memory into ranges).
void PrepareVertexInputs(const Shader::Info& vs, const Shader::Gcn::FetchShaderData& fetch,
const AmdGpu::Regs& regs, PreparedVertexInputs& out, FlatArena& arena) {
using InstanceIdType = Shader::Gcn::VertexAttribute::InstanceIdType;
thread_local std::vector<vk::VertexInputAttributeDescription2EXT> attributes;
thread_local std::vector<vk::VertexInputBindingDescription2EXT> bindings;
thread_local std::vector<AmdGpu::Buffer> buffers;
thread_local std::vector<PreparedVertexInputs::Range> ranges, merged;
thread_local std::vector<u8> range_index;
attributes.clear();
bindings.clear();
buffers.clear();
ranges.clear();
merged.clear();
range_index.clear();
for (const auto& attrib : fetch.attributes) {
const auto step_rate = attrib.GetStepRate();
const auto buffer = attrib.GetSharp(vs);
attributes.push_back({
.location = attrib.semantic,
.binding = attrib.semantic,
.format = LiverpoolToVK::SurfaceFormat(buffer.GetDataFmt(), buffer.GetNumberFmt()),
.offset = 0,
});
bindings.push_back({
.binding = attrib.semantic,
.stride = buffer.GetStride(),
.inputRate = step_rate == InstanceIdType::None ? vk::VertexInputRate::eVertex
: vk::VertexInputRate::eInstance,
.divisor = step_rate == InstanceIdType::OverStepRate0 ? regs.vgt_instance_step_rate_0
: step_rate == InstanceIdType::OverStepRate1
? regs.vgt_instance_step_rate_1
: 1u,
});
buffers.push_back(buffer);
if (buffer.base_address != 0 && buffer.GetSize() > 0) {
ranges.push_back({buffer.base_address, buffer.base_address + buffer.GetSize()});
}
}
std::ranges::sort(ranges, {}, &PreparedVertexInputs::Range::base);
for (const auto& range : ranges) {
if (merged.empty() || merged.back().end < range.base) {
merged.push_back(range);
} else {
merged.back().end = std::max(merged.back().end, range.end);
}
}
for (const auto& buffer : buffers) {
if (buffer.base_address != 0 && buffer.GetSize() > 0) {
const auto it = std::ranges::find_if(merged, [&](const auto& range) {
return buffer.base_address >= range.base && buffer.base_address < range.end;
});
range_index.push_back(static_cast<u8>(it - merged.begin()));
}
}
out.attributes = arena.AppendArray(attributes.data(), attributes.size());
out.bindings = arena.AppendArray(bindings.data(), bindings.size());
out.buffers = arena.AppendArray(buffers.data(), buffers.size());
out.range_index = arena.AppendArray(range_index.data(), range_index.size());
out.ranges = arena.AppendArray(merged.data(), merged.size());
out.count = static_cast<u32>(attributes.size());
out.num_ranges = static_cast<u32>(merged.size());
out.buffers_hash =
buffers.empty() ? 0 : XXH3_64bits(buffers.data(), buffers.size() * sizeof(AmdGpu::Buffer));
out.valid = true;
}
} // namespace
DrawPreparation::DrawPreparation(PipelineCache& pipeline_cache_)
: pipeline_cache{pipeline_cache_} {
worker_count = DefaultWorkerCount();
if (worker_count) {
scanner = std::jthread([this](std::stop_token stop) { ScannerLoop(stop); });
}
for (u32 i = 0; i < worker_count; ++i) {
workers.emplace_back([this, i](std::stop_token stop) { WorkerLoop(stop, i); });
}
std::printf("GPU: draw preparation workers: %u\n", worker_count);
std::printf("GPU: draw preparation workers: %u (+1 scanner, %u hardware threads)\n",
worker_count, BbThreads::Available());
}
DrawPreparation::~DrawPreparation() {
for (auto& worker : workers) {
worker.request_stop();
}
if (scanner.joinable()) {
scanner.request_stop();
}
cv.notify_all();
workers.clear();
scanner = {};
}
std::shared_ptr<DrawPreparation::Submission> DrawPreparation::Build(
@@ -128,11 +247,26 @@ void DrawPreparation::BeginSubmission(u64 seq, const AmdGpu::Regs& regs, u64 reg
std::scoped_lock lk{mutex};
if (!baseline_ready) {
initial_regs = std::make_unique<AmdGpu::Regs>(regs);
initial_checksum = reg_checksum;
baseline_seq = seq;
tail_regs = std::make_unique<AmdGpu::Regs>(regs);
initial_checksum = tail_checksum = reg_checksum;
baseline_seq = tail_seq = seq;
baseline_ready = true;
cv.notify_all();
}
// Idle helpers can starve on a busy CPU. Rather than queueing without bound, restart
// them from the GPU thread's own state (one register copy) once the scanner lags.
constexpr u64 MaxScanLag = 64;
if (!submissions.empty() && seq > tail_seq + MaxScanLag &&
submissions.front()->seq < seq && !submissions.front()->scanned.load()) {
*tail_regs = regs;
tail_seq = seq;
tail_checksum = reg_checksum;
while (!submissions.empty() && submissions.front()->seq < seq) {
submissions.pop_front();
}
++rebases;
cv.notify_all();
}
if (!submissions.empty() && seq >= submissions.front()->seq &&
seq - submissions.front()->seq < submissions.size()) {
current = submissions[seq - submissions.front()->seq];
@@ -164,18 +298,25 @@ void DrawPreparation::Collect() {
std::scoped_lock lk{mutex};
while (!submissions.empty()) {
const auto& front = submissions.front();
// Buffers before the baseline are never replayed by the workers.
if (!baseline_ready || !front->gpu_done.load(std::memory_order_acquire) ||
(front->seq >= baseline_seq &&
front->workers_done.load(std::memory_order_acquire) < worker_count)) {
if (!baseline_ready || !front->gpu_done.load(std::memory_order_acquire)) {
break;
}
if (front->seq >= baseline_seq) {
// Its delta moves the restart state to the start of the next buffer.
if (!front->scanned.load(std::memory_order_acquire)) {
break;
}
front->delta.Apply(*tail_regs);
tail_seq = front->seq + 1;
tail_checksum = front->end_checksum;
}
submissions.pop_front();
}
}
void DrawPreparation::WorkerLoop(std::stop_token stop, u32 index) {
Common::SetCurrentThreadName(("bb:DrawPrep" + std::to_string(index)).c_str());
void DrawPreparation::ScannerLoop(std::stop_token stop) {
Common::SetCurrentThreadName("bb:DrawScan");
BbThreads::MakeBackground();
std::unique_ptr<AmdGpu::Regs> regs;
u64 checksum = 0;
u64 next_seq = 0;
@@ -189,37 +330,111 @@ void DrawPreparation::WorkerLoop(std::stop_token stop, u32 index) {
checksum = initial_checksum;
next_seq = baseline_seq;
}
PipelineSelection sel{};
sel.regs = regs.get();
PrepWorker prep_worker{};
sel.worker = &prep_worker;
FlatArena arena;
AmdGpu::RegDirty dirty;
while (!stop.stop_requested()) {
std::shared_ptr<Submission> submission;
{
std::unique_lock lk{mutex};
cv.wait(lk, stop, [&] {
return !submissions.empty() && submissions.back()->seq >= next_seq;
return next_seq < tail_seq ||
(!submissions.empty() && submissions.back()->seq >= next_seq);
});
if (stop.stop_requested()) {
return;
}
// The buffer cannot be collected before this worker has replayed it.
if (next_seq < tail_seq) {
// Rebased by the GPU thread: continue from its state.
*regs = *tail_regs;
checksum = tail_checksum;
next_seq = tail_seq;
continue;
}
// Unscanned buffers are only collected by a rebase, so `next_seq` is still queued.
submission = submissions[next_seq - submissions.front()->seq];
}
// Stale buffers (the GPU thread is already there) only update the register state.
const bool assigned = submission->seq % worker_count == index;
submission->start_checksum = checksum;
dirty.Clear();
ForEachPacket(submission->commands, [&](const AmdGpu::PM4Header* header) {
AmdGpu::Liverpool::ApplyGraphicsRegisterPacket(*regs, header, checksum, &dirty);
});
submission->delta.Capture(*regs, dirty);
submission->end_checksum = checksum;
{
std::scoped_lock lk{mutex};
submission->scanned.store(true, std::memory_order_release);
}
++next_seq;
cv.notify_all();
}
}
void DrawPreparation::WorkerLoop(std::stop_token stop, u32 index) {
Common::SetCurrentThreadName(("bb:DrawPrep" + std::to_string(index)).c_str());
BbThreads::MakeBackground();
auto regs = std::make_unique<AmdGpu::Regs>();
constexpr u64 NoPosition = ~0ull;
u64 position = NoPosition; ///< `regs` holds the state at the start of this buffer
PipelineSelection sel{};
sel.regs = regs.get();
PrepWorker prep_worker{};
sel.worker = &prep_worker;
FlatArena arena;
std::vector<std::shared_ptr<Submission>> path;
// The nearest scanned buffer the GPU thread has not reached and no worker has claimed.
const auto claimable = [&]() -> std::shared_ptr<Submission> {
const u64 gpu = gpu_seq.load(std::memory_order_acquire);
for (const auto& submission : submissions) {
if (submission->seq >= gpu && submission->seq >= tail_seq &&
submission->scanned.load(std::memory_order_acquire) &&
!submission->claimed.load(std::memory_order_relaxed)) {
return submission;
}
}
return {};
};
while (!stop.stop_requested()) {
std::shared_ptr<Submission> target;
{
std::unique_lock lk{mutex};
// gpu_seq moves without the lock: keep the candidate the predicate found.
cv.wait(lk, stop, [&] {
target = baseline_ready ? claimable() : nullptr;
return target != nullptr;
});
if (stop.stop_requested() || !target) {
return;
}
target->claimed.store(true, std::memory_order_relaxed);
// Buffers between this worker's state and the target: their deltas. A position
// already collected (or past the target) restarts from the queue's tail state.
if (position == NoPosition || position < tail_seq || position > target->seq) {
*regs = *tail_regs;
position = tail_seq;
}
const u64 front = submissions.front()->seq;
path.clear();
for (u64 seq = position; seq < target->seq; ++seq) {
path.push_back(submissions[seq - front]);
}
}
for (const auto& step : path) {
step->delta.Apply(*regs);
}
path.clear();
u64 checksum = target->start_checksum;
u32 ordinal = 0;
arena = FlatArena{};
ForEachPacket(submission->commands, [&](const AmdGpu::PM4Header* header) {
ForEachPacket(target->commands, [&](const AmdGpu::PM4Header* header) {
AmdGpu::Liverpool::ApplyGraphicsRegisterPacket(*regs, header, checksum);
if (!IsDirectDraw(header->type3.opcode)) {
return;
}
if (assigned && ordinal < submission->num_draws &&
submission->seq >= gpu_seq.load(std::memory_order_acquire)) {
auto& draw = submission->draws[ordinal];
// Past the GPU thread the draws are no longer worth preparing.
if (ordinal < target->num_draws &&
target->seq >= gpu_seq.load(std::memory_order_acquire)) {
auto& draw = target->draws[ordinal];
prep_worker.stages.clear();
prep_worker.failed = false;
sel.draw_indirect_params = {};
@@ -237,6 +452,13 @@ void DrawPreparation::WorkerLoop(std::stop_token stop, u32 index) {
.flat = arena.Append(*stage.flat),
.flat_size = static_cast<u32>(stage.flat->size()),
};
PrepareResources(prep_worker.infos.at(stage.program), draw.stages[i],
arena);
}
draw.vertex.valid = false;
const auto* vs = sel.infos[static_cast<u32>(Shader::SwStage::Vertex)];
if (vs && sel.fetch_shader) {
PrepareVertexInputs(*vs, *sel.fetch_shader, *regs, draw.vertex, arena);
}
draw.state.store(PreparedDraw::Ready, std::memory_order_release);
} else {
@@ -245,13 +467,9 @@ void DrawPreparation::WorkerLoop(std::stop_token stop, u32 index) {
}
++ordinal;
});
if (assigned) {
// The arena's chunks live as long as the submission.
submission->flat_chunks = std::move(arena.chunks);
}
submission->workers_done.fetch_add(1, std::memory_order_acq_rel);
++next_seq;
cv.notify_all();
// The arena's chunks live as long as the submission.
target->flat_chunks = std::move(arena.chunks);
position = target->seq + 1;
}
}
@@ -268,9 +486,10 @@ void DrawPreparation::Count(bool was_used) {
return;
}
window = now;
std::printf("Draw preparation: %llu prepared pipelines used, %llu not (%u workers)\n",
std::printf("Draw preparation: %llu prepared pipelines used, %llu not (%u workers, "
"%llu scanner rebases)\n",
static_cast<unsigned long long>(used), static_cast<unsigned long long>(unused),
worker_count);
worker_count, static_cast<unsigned long long>(rebases));
used = unused = 0;
}
@@ -1,10 +1,13 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: speculative draw preparation on worker threads (docs/parallel_gpu.md, step 1).
// bbport: speculative draw preparation on worker threads (docs/parallel_gpu.md, steps 1-2).
//
// Every submitted graphics command buffer is copied and handed to the workers. Each worker
// replays the register writes of the whole stream (ApplyGraphicsRegisterPacket, the same code
// the GPU thread runs) and, for the buffers assigned to it, selects the graphics pipeline of
// each direct draw ahead of the GPU thread. The GPU thread uses a prepared draw only when the
// Every submitted graphics command buffer is copied. A scanner thread replays the register
// writes of the whole stream in order (ApplyGraphicsRegisterPacket, the same code the GPU
// thread runs) and records, per buffer, its starting checksum and a delta of the register
// blocks it wrote. Any number of workers (one per spare hardware thread) claim the nearest
// buffers ahead of the GPU thread, reach their starting state by applying deltas, and select
// the graphics pipeline and resource sharps of each direct draw. All helpers run as
// SCHED_IDLE: they only use idle cores. The GPU thread uses a prepared draw only when the
// running register checksums match and the flattened user data it computes itself equals the
// worker's; otherwise it takes the regular path. Workers never create or compile anything.
@@ -13,6 +16,7 @@
#include <array>
#include <atomic>
#include <condition_variable>
#include <cstring>
#include <deque>
#include <memory>
#include <mutex>
@@ -21,6 +25,7 @@
#include <vector>
#include "common/types.h"
#include "video_core/amdgpu/regs.h"
#include "video_core/renderer_vulkan/vk_pipeline_cache.h"
namespace Vulkan {
@@ -32,6 +37,43 @@ struct PreparedStage {
VAddr pgm_base;
const u32* flat; ///< flattened user data the worker computed, owned by the submission
u32 flat_size;
// Resource sharps read from `flat` in the order of the program's resource lists, and the
// texture description hash of each image (docs/parallel_gpu.md, step 2). Valid for the
// GPU thread exactly when `flat` matched: sharps depend on nothing else.
const AmdGpu::Image* image_sharps;
const u64* image_hashes;
const AmdGpu::Sampler* sampler_sharps;
const AmdGpu::Buffer* buffer_sharps;
u32 num_images, num_samplers, num_buffers;
};
/// Key of Rasterizer's texture description cache for a T# bound through `res`.
inline u64 ImageDescHash(const AmdGpu::Image& sharp, const Shader::ImageResource& res) {
std::array<u64, 4> key;
std::memcpy(key.data(), &sharp, sizeof(key));
const u32 flags = u32(res.is_written) | u32(res.is_depth) << 1 | u32(res.is_array) << 2;
u64 hash = flags * 0x9E3779B97F4A7C15ull;
for (const u64 word : key) {
hash = (hash ^ word) * 0xFF51AFD7ED558CCDull;
hash ^= hash >> 32;
}
return hash;
}
/// Vertex inputs of a draw (the dynamic vertex input path): the attribute and binding
/// descriptions, the V# of each stream, and the stream memory merged into ranges.
struct PreparedVertexInputs {
struct Range {
VAddr base, end;
};
const vk::VertexInputAttributeDescription2EXT* attributes;
const vk::VertexInputBindingDescription2EXT* bindings;
const AmdGpu::Buffer* buffers;
const u8* range_index; ///< per stream with memory: its entry in `ranges`
const Range* ranges;
u32 count, num_ranges;
u64 buffers_hash; ///< XXH3 of `buffers` (object motion stream identity)
bool valid;
};
struct PreparedDraw {
@@ -41,6 +83,7 @@ struct PreparedDraw {
GraphicsPipelineKey key{};
u32 num_stages{};
std::array<PreparedStage, MaxShaderStages> stages{};
PreparedVertexInputs vertex{};
};
class DrawPreparation {
@@ -78,13 +121,16 @@ public:
std::vector<u32> commands;
std::unique_ptr<PreparedDraw[]> draws;
u32 num_draws{};
std::vector<std::unique_ptr<u32[]>> flat_chunks; ///< from the assigned worker
std::atomic<u32> workers_done{0};
std::vector<std::unique_ptr<u32[]>> flat_chunks; ///< from the claiming worker
u64 start_checksum{}, end_checksum{}; ///< set by the scanner
AmdGpu::RegDelta delta; ///< registers written, values at the end
std::atomic<bool> scanned{false};
std::atomic<bool> claimed{false};
std::atomic<bool> gpu_done{false};
};
private:
void ScannerLoop(std::stop_token stop);
void WorkerLoop(std::stop_token stop, u32 index);
void Collect();
@@ -92,6 +138,12 @@ private:
std::unique_ptr<AmdGpu::Regs> initial_regs; ///< state at the start of `baseline_seq`
u64 initial_checksum{};
u64 baseline_seq{};
/// State at the start of the oldest queued buffer (`tail_seq`): a worker whose position
/// was collected restarts from here. Guarded by `mutex`.
std::unique_ptr<AmdGpu::Regs> tail_regs;
u64 tail_seq{};
u64 tail_checksum{};
u64 rebases = 0; ///< scanner restarts from the GPU thread state (BB_FRAME_STATS)
bool baseline_ready{}; ///< guarded by `mutex`
std::mutex mutex;
std::condition_variable_any cv;
@@ -101,6 +153,7 @@ private:
u32 current_draw = 0;
u64 used = 0, unused = 0;
u32 worker_count = 0; ///< fixed before the worker threads start
std::jthread scanner;
std::vector<std::jthread> workers;
};
@@ -0,0 +1,311 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "video_core/renderer_vulkan/vk_fsr4.h"
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <deque>
#include <fstream>
#include <string>
#include <utility>
#include <vector>
#include "ffx_vk_fsr4_v07.h"
#include "ffx_vk_fsr4_v07_assets.h"
#include "video_core/renderer_vulkan/vk_instance.h"
#include "video_core/renderer_vulkan/vk_scheduler.h"
namespace Vulkan {
namespace {
std::string AssetDir() {
const char* dir = std::getenv("BB_FSR4_DIR");
return dir && dir[0] ? dir : "fsr4_shaders";
}
bool ReadFile(const std::string& path, std::vector<u8>& 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())));
}
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
struct Fsr4Upscaler::Impl {
const Instance& instance;
Scheduler& scheduler;
std::vector<u8> scratch;
FfxInterface backend{};
bool backend_ok = false;
ffxContext context{};
bool context_ok = false;
int model = -1;
u32 out_width = 0, out_height = 0;
std::string problem;
bool fatal = false;
u64 next_frame_id = 1;
std::deque<std::pair<u64, u64>> in_flight; ///< provider frame id, scheduler tick
Impl(const Instance& instance_, Scheduler& scheduler_)
: instance{instance_}, scheduler{scheduler_} {}
~Impl() {
Destroy();
}
void Destroy() {
if (!backend_ok && !context_ok) {
return;
}
scheduler.Finish();
if (context_ok) {
ffxFsr4V07DestroyContext(&context, nullptr);
context_ok = false;
}
if (backend_ok) {
ffxFsr4VkDestroyContext(reinterpret_cast<FfxFsr4VkContext*>(scratch.data()));
backend_ok = false;
}
backend = {};
in_flight.clear();
model = -1;
}
void Fail(std::string reason, bool permanent) {
if (problem != reason) {
std::printf("Upscaler: FSR 4 unavailable: %s\n", reason.c_str());
}
problem = std::move(reason);
fatal |= permanent;
}
bool CreateBackend(int preset, u32 ow, u32 oh) {
FfxFsr4V07AssetSet assets{};
if (!ffxFsr4V07BuildAssetSet(ModelPreset(preset), ow, oh, &assets)) {
Fail("output size is not supported by the v07 model", true);
return false;
}
const std::string dir = AssetDir() + "/";
std::array<std::vector<u8>, FFX_FSR4_VK_PASS_COUNT> code;
std::vector<u8> initializer, weights;
const auto load = [&](const char* name, std::vector<u8>& data) {
if (ReadFile(dir + name, data)) {
return true;
}
Fail("missing " + dir + name + " (run fetch_fsr4_assets.sh)", true);
return false;
};
if (!load(assets.pre, code[0])) return false;
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]) ||
!load(assets.spdAutoExposure, code[15]) || !load(assets.initializer, initializer) ||
!load(assets.prePassWeights, weights)) {
return false;
}
if (initializer.size() != FFX_FSR4_V07_INITIALIZER_BYTES ||
weights.size() != FFX_FSR4_V07_PRE_PASS_WEIGHTS_BYTES) {
Fail("model weights have an unexpected size", true);
return false;
}
static const std::array<std::string, FFX_FSR4_VK_PASS_COUNT> entries = [] {
std::array<std::string, FFX_FSR4_VK_PASS_COUNT> names;
names.fill("main");
for (u32 pass = 1; pass <= FFX_FSR4_MODEL_PASS_COUNT; ++pass) {
names[pass] = "fsr4_model_v07_i8_pass" + std::to_string(pass);
}
return names;
}();
FfxFsr4VkCreateInfo ci{};
ci.device = instance.GetDevice();
ci.physicalDevice = instance.GetPhysicalDevice();
for (u32 i = 0; i < FFX_FSR4_VK_PASS_COUNT; ++i) {
if (code[i].size() % 4) {
Fail("invalid SPIR-V asset", true);
return false;
}
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();
scratch.assign(ffxFsr4VkGetScratchMemorySize(), 0);
ci.scratchBuffer = scratch.data();
ci.scratchBufferSize = scratch.size();
if (const VkResult result = ffxFsr4VkCreateContext(&ci, &backend);
result != VK_SUCCESS) {
Fail("Vulkan backend creation failed (" + std::to_string(int(result)) + ")", true);
backend = {};
return false;
}
backend_ok = true;
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;
// Any preset's render size fits: the context is not recreated for a size change.
desc.maxRenderSize = {(ow + 7) & ~7u, (oh + 7) & ~7u};
desc.maxUpscaleSize = {(ow + 7) & ~7u, (oh + 7) & ~7u};
ffxFsr4V07SetBackendInterface(&backend);
const auto created = ffxFsr4V07CreateContext(&context, &desc.header, nullptr);
ffxFsr4V07SetBackendInterface(nullptr);
if (created != FFX_API_RETURN_OK) {
Fail("provider context creation failed (" + std::to_string(created) + ")", true);
return false;
}
context_ok = true;
model = preset;
out_width = ow;
out_height = oh;
std::printf("Upscaler: FSR 4 v07 INT8 %s model, %s tier, output %ux%u\n",
ffxFsr4ModelPresetName(ModelPreset(preset)), assets.tier, ow, oh);
return true;
}
void Retire() {
while (!in_flight.empty() && scheduler.IsFree(in_flight.front().second)) {
ffxFsr4VkRetireFrame(&backend, in_flight.front().first);
in_flight.pop_front();
}
}
bool Register(const Image& image, VkAccessFlags access) {
const FfxFsr4VkExternalImageState state{
.structSize = sizeof(FfxFsr4VkExternalImageState),
.image = image.image,
.view = image.view,
.layout = VK_IMAGE_LAYOUT_GENERAL,
.stageMask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
.accessMask = access,
.restoreLayout = VK_IMAGE_LAYOUT_GENERAL,
.restoreStageMask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
.restoreAccessMask = access,
};
return ffxFsr4VkSetExternalImageState(&backend, &state) == VK_SUCCESS;
}
bool Record(const Frame& f) {
if (fatal) {
return false;
}
if (!instance.IsFsr4Int8Supported()) {
Fail("the GPU lacks INT8 dot product / compute derivative support", true);
return false;
}
const int preset = std::clamp(f.preset, 0, 4);
if (!context_ok || preset != model || f.output.width != out_width ||
f.output.height != out_height) {
Destroy();
if (!CreateBackend(preset, f.output.width, f.output.height)) {
Destroy();
return false;
}
}
Retire();
const u64 frame_id = next_frame_id++;
VkResult begin = ffxFsr4VkBeginFrame(&backend, frame_id);
while (begin == VK_NOT_READY && !in_flight.empty()) {
scheduler.Wait(in_flight.front().second);
Retire();
begin = ffxFsr4VkBeginFrame(&backend, frame_id);
}
if (begin != VK_SUCCESS) {
Fail("no free provider frame (" + std::to_string(int(begin)) + ")", false);
return false;
}
constexpr VkAccessFlags read = VK_ACCESS_SHADER_READ_BIT;
if (!Register(f.color, read) || !Register(f.depth, read) || !Register(f.motion, read) ||
!Register(f.output, read | VK_ACCESS_SHADER_WRITE_BIT)) {
Fail("external image registration failed", false);
return false;
}
const auto resource = [](const Image& image, u32 format, u32 state) {
FfxApiResource r{};
r.resource = reinterpret_cast<void*>(VkImageView(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;
};
ffxDispatchDescUpscale d{};
d.header.type = FFX_API_DISPATCH_DESC_TYPE_UPSCALE;
d.commandList = reinterpret_cast<void*>(VkCommandBuffer(f.cmdbuf));
d.color = resource(f.color, FFX_SURFACE_FORMAT_R16G16B16A16_FLOAT,
FFX_API_RESOURCE_STATE_COMPUTE_READ);
d.depth = resource(f.depth, FFX_SURFACE_FORMAT_R32_FLOAT,
FFX_API_RESOURCE_STATE_COMPUTE_READ);
d.motionVectors = resource(f.motion, FFX_SURFACE_FORMAT_R16G16_FLOAT,
FFX_API_RESOURCE_STATE_COMPUTE_READ);
d.output = resource(f.output, FFX_SURFACE_FORMAT_R16G16B16A16_FLOAT,
FFX_API_RESOURCE_STATE_UNORDERED_ACCESS);
d.jitterOffset = {f.jitter[0], f.jitter[1]};
// Vectors are in render pixels; the provider divides by the render size.
d.motionVectorScale = {1.0f, 1.0f};
d.renderSize = {f.render_width, f.render_height};
d.upscaleSize = {f.output.width, f.output.height};
d.enableSharpening = f.sharpen && f.sharpness > 0.0f;
d.sharpness = f.sharpness;
d.enableAutoExposure = f.auto_exposure;
d.frameTimeDelta = f.frame_ms;
d.preExposure = 1.0f;
d.reset = f.reset;
d.cameraNear = f.near_plane;
d.cameraFar = f.far_plane;
d.cameraFovAngleVertical = f.vertical_fov;
d.viewSpaceToMetersFactor = 1.0f;
if (const auto result = ffxFsr4V07Dispatch(&context, &d.header);
result != FFX_API_RETURN_OK) {
Fail("dispatch failed (" + std::to_string(result) + ")", false);
return false;
}
in_flight.emplace_back(frame_id, scheduler.CurrentTick());
problem.clear();
return true;
}
};
Fsr4Upscaler::Fsr4Upscaler(const Instance& instance, Scheduler& scheduler)
: impl{std::make_unique<Impl>(instance, scheduler)} {}
Fsr4Upscaler::~Fsr4Upscaler() = default;
bool Fsr4Upscaler::Record(const Frame& frame) {
return impl->Record(frame);
}
const char* Fsr4Upscaler::Problem() const noexcept {
return impl->problem.empty() ? nullptr : impl->problem.c_str();
}
bool Fsr4Upscaler::Fatal() const noexcept {
return impl->fatal;
}
} // namespace Vulkan
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: FSR 4 (source-v07 INT8/DOT4 model, FSR-Vulkan provider) as an alternative to the
// FSR 3.1 context of TemporalUpscaler. It consumes the same inputs: HDR scene color, depth,
// motion vectors in render pixels (previous - current) and the jitter offset.
//
// The model is per preset (native, quality, balanced, performance, ultraperf) and per output
// tier; its SPIR-V passes and weights come from fetch_fsr4_assets.sh (BB_FSR4_DIR overrides
// the fsr4_shaders directory). A preset change rebuilds the model graph.
#pragma once
#include <array>
#include <memory>
#include "common/types.h"
#include "video_core/renderer_vulkan/vk_common.h"
namespace Vulkan {
class Instance;
class Scheduler;
class Fsr4Upscaler {
public:
Fsr4Upscaler(const Instance& instance, Scheduler& scheduler);
~Fsr4Upscaler();
/// An image in layout General, written/read by compute shaders.
struct Image {
vk::Image image;
vk::ImageView view;
u32 width, height;
};
struct Frame {
vk::CommandBuffer cmdbuf;
Image color, depth, motion, output;
u32 render_width, render_height;
int preset; ///< BbSettings::Preset
std::array<float, 2> jitter;
float frame_ms, near_plane, far_plane, vertical_fov;
float sharpness;
bool sharpen, reset, auto_exposure;
};
/// Records FSR 4 into `frame.cmdbuf`; the output stays in General. False when FSR 4
/// cannot run (Problem() says why; missing assets or device features are permanent).
bool Record(const Frame& frame);
/// Why the last Record failed, or null.
[[nodiscard]] const char* Problem() const noexcept;
/// Assets or device features are missing: FSR 4 will not run in this session.
[[nodiscard]] bool Fatal() const noexcept;
private:
struct Impl;
std::unique_ptr<Impl> impl;
};
} // namespace Vulkan
@@ -43,6 +43,7 @@ struct GraphicsPipelineKey {
u8 depth_samples;
std::array<u8, AmdGpu::NUM_COLOR_BUFFERS> color_samples;
u32 mrt_mask;
u32 motion_vectors = 0;
struct {
AmdGpu::DepthBuffer::ZFormat z_format : 2;
AmdGpu::DepthBuffer::StencilFormat stencil_format : 1;
@@ -95,8 +95,14 @@ Instance::Instance(bool enable_validation, bool enable_crash_diagnostic)
Instance::Instance(Frontend::WindowSDL& window, s32 physical_device_index,
bool enable_validation /*= false*/, bool enable_crash_diagnostic /*= false*/)
: instance{CreateInstance(window.GetWindowInfo().type, enable_validation,
enable_crash_diagnostic)},
: Instance(window.GetWindowInfo(), physical_device_index, enable_validation, enable_crash_diagnostic) {}
Instance::Instance(s32 index, bool validation)
: Instance(Frontend::WindowSystemInfo{}, index, validation, false) {}
Instance::Instance(const Frontend::WindowSystemInfo& window_info, s32 physical_device_index,
bool enable_validation, bool enable_crash_diagnostic)
: instance{CreateInstance(window_info.type, enable_validation, enable_crash_diagnostic)},
physical_devices{EnumeratePhysicalDevices(instance)} {
if (enable_validation) {
debug_callback = CreateDebugCallback(*instance);
@@ -210,7 +216,8 @@ bool Instance::CreateDevice() {
vk::PhysicalDevicePrimitiveTopologyListRestartFeaturesEXT,
vk::PhysicalDeviceShaderAtomicFloat2FeaturesEXT,
vk::PhysicalDeviceWorkgroupMemoryExplicitLayoutFeaturesKHR,
vk::PhysicalDeviceImage2DViewOf3DFeaturesEXT, vk::PhysicalDeviceShaderClockFeaturesKHR>();
vk::PhysicalDeviceImage2DViewOf3DFeaturesEXT, vk::PhysicalDeviceShaderClockFeaturesKHR,
vk::PhysicalDeviceComputeShaderDerivativesFeaturesKHR>();
features = feature_chain.get().features;
const vk::StructureChain properties_chain = physical_device.getProperties2<
@@ -344,6 +351,12 @@ bool Instance::CreateDevice() {
}
image_view_min_lod = add_extension(VK_EXT_IMAGE_VIEW_MIN_LOD_EXTENSION_NAME);
supports_memory_budget = add_extension(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME);
// bbport: FSR 4 v07 INT8 (vk_temporal_upscaler): quad derivatives in compute shaders.
compute_shader_derivatives = add_extension(VK_KHR_COMPUTE_SHADER_DERIVATIVES_EXTENSION_NAME);
if (compute_shader_derivatives) {
compute_shader_derivatives_features =
feature_chain.get<vk::PhysicalDeviceComputeShaderDerivativesFeaturesKHR>();
}
shader_clock = add_extension(VK_KHR_SHADER_CLOCK_EXTENSION_NAME);
if (shader_clock) {
shader_clock_features = feature_chain.get<vk::PhysicalDeviceShaderClockFeaturesKHR>();
@@ -450,6 +463,7 @@ bool Instance::CreateDevice() {
.subgroupSizeControl = vk13_features.subgroupSizeControl,
.synchronization2 = vk13_features.synchronization2,
.dynamicRendering = vk13_features.dynamicRendering,
.shaderIntegerDotProduct = vk13_features.shaderIntegerDotProduct,
.maintenance4 = vk13_features.maintenance4,
},
// Extensions
@@ -526,6 +540,12 @@ bool Instance::CreateDevice() {
vk::PhysicalDeviceShaderClockFeaturesKHR{
.shaderSubgroupClock = shader_clock_features.shaderSubgroupClock,
},
vk::PhysicalDeviceComputeShaderDerivativesFeaturesKHR{
.computeDerivativeGroupQuads =
compute_shader_derivatives_features.computeDerivativeGroupQuads,
.computeDerivativeGroupLinear =
compute_shader_derivatives_features.computeDerivativeGroupLinear,
},
};
if (!custom_border_color) {
@@ -577,6 +597,9 @@ bool Instance::CreateDevice() {
if (!shader_clock) {
device_chain.unlink<vk::PhysicalDeviceShaderClockFeaturesKHR>();
}
if (!compute_shader_derivatives) {
device_chain.unlink<vk::PhysicalDeviceComputeShaderDerivativesFeaturesKHR>();
}
auto [device_result, dev] = physical_device.createDeviceUnique(device_chain.get());
if (device_result != vk::Result::eSuccess) {
@@ -13,6 +13,7 @@
namespace Frontend {
class WindowSDL;
struct WindowSystemInfo;
}
VK_DEFINE_HANDLE(VmaAllocator)
@@ -24,6 +25,8 @@ public:
explicit Instance(bool validation = false, bool crash_diagnostic = false);
explicit Instance(Frontend::WindowSDL& window, s32 physical_device_index,
bool enable_validation = false, bool enable_crash_diagnostic = false);
/// Headless logical device for renderer regression tests (no window or game).
explicit Instance(s32 physical_device_index, bool enable_validation);
~Instance();
/// Returns a formatted string for the driver version
@@ -272,6 +275,15 @@ public:
}
/// Returns true when the shaderSubgroupClock feature of
/// bbport: the FSR 4 v07 INT8/DOT4 shaders can run (Float16, Int8, Int16, packed int8 dot
/// products, linear compute derivatives, extended storage image formats).
bool IsFsr4Int8Supported() const {
return vk12_features.shaderFloat16 && vk12_features.shaderInt8 && features.shaderInt16 &&
vk13_features.shaderIntegerDotProduct && compute_shader_derivatives &&
compute_shader_derivatives_features.computeDerivativeGroupLinear &&
features.shaderStorageImageExtendedFormats;
}
/// VK_KHR_shader_clock is supported.
bool IsShaderSubgroupClockSupported() const {
return shader_clock && shader_clock_features.shaderSubgroupClock;
@@ -468,6 +480,7 @@ public:
[[nodiscard]] bool IsFormatSupported(vk::Format format, vk::FormatFeatureFlags2 flags) const;
private:
Instance(const Frontend::WindowSystemInfo&, s32, bool, bool);
/// Creates the logical device opportunistically enabling extensions
bool CreateDevice();
@@ -503,6 +516,7 @@ private:
vk::PhysicalDeviceImage2DViewOf3DFeaturesEXT image_2d_view_of_3d_features;
vk::PhysicalDevicePrimitiveTopologyListRestartFeaturesEXT list_restart_features;
vk::PhysicalDeviceShaderClockFeaturesKHR shader_clock_features;
vk::PhysicalDeviceComputeShaderDerivativesFeaturesKHR compute_shader_derivatives_features;
vk::DriverIdKHR driver_id;
vk::UniqueDebugUtilsMessengerEXT debug_callback{};
std::string vendor_name;
@@ -539,6 +553,7 @@ private:
bool image_2d_view_of_3d{};
bool image_view_min_lod{};
bool shader_clock{};
bool compute_shader_derivatives{};
bool supports_memory_budget{};
bool supports_block_texel_view{};
u64 total_memory_budget{};
@@ -0,0 +1,250 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "video_core/renderer_vulkan/vk_object_motion.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <vk_mem_alloc.h>
#include "bbport_settings.h"
#include "bbport_toggles.h"
#include "shader_recompiler/runtime_info.h"
#include "video_core/renderer_vulkan/vk_instance.h"
#include "video_core/renderer_vulkan/vk_scheduler.h"
namespace Vulkan {
namespace {
bool CreateBuffer(const Instance& instance, vk::DeviceSize size, bool host, vk::Buffer& buffer,
VmaAllocation& allocation, void** mapped, u64& address) {
const VkBufferCreateInfo buffer_ci{
.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
.size = size,
.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
};
VmaAllocationCreateInfo alloc_ci{
.usage = VMA_MEMORY_USAGE_AUTO,
};
if (host) {
alloc_ci.requiredFlags = VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
alloc_ci.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT |
VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
} else {
alloc_ci.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
}
VkBuffer raw{};
VmaAllocationInfo info{};
if (vmaCreateBuffer(instance.GetAllocator(), &buffer_ci, &alloc_ci, &raw, &allocation, &info) !=
VK_SUCCESS) {
return false;
}
buffer = raw;
if (mapped) {
*mapped = info.pMappedData;
}
address = instance.GetDevice().getBufferAddress({.buffer = buffer});
return address != 0;
}
} // namespace
ObjectMotion::ObjectMotion(const Instance& instance_, Scheduler& scheduler_)
: instance{instance_}, scheduler{scheduler_} {
// Pipeline selection limits the extra attachment and vertex stores to likely
// animated draws. A setting or environment override can still disable the path.
if (!BbSettings::Get().object_motion ||
BbSettings::Get().upscaler == BbSettings::UpscalerOff) {
std::puts("Object motion: off (enable in menu or BB_OBJECT_MOTION=1)");
return;
}
const auto& features = instance.GetPhysicalDevice().getFeatures();
if (!features.vertexPipelineStoresAndAtomics) {
std::printf("Object motion: vertexPipelineStoresAndAtomics unsupported, off\n");
return;
}
void* mapped = nullptr;
u64 params_address = 0, positions_address = 0;
if (!CreateBuffer(instance, vk::DeviceSize(1 + FrameSlots * ParamsPerFrame) * 32, true,
params_buffer, params_allocation, &mapped, params_address) ||
!CreateBuffer(instance, vk::DeviceSize(1 + 2 * PositionsPerFrame) * 16, false,
positions_buffer, positions_allocation, nullptr, positions_address)) {
std::printf("Object motion: buffer creation failed, off\n");
return;
}
params_mapped = static_cast<u32*>(mapped);
std::memset(params_mapped, 0, 32);
Shader::MotionVectors::params_address = params_address;
Shader::MotionVectors::positions_address = positions_address;
enabled = true;
std::printf("Object motion: on (%u vertices per frame)\n", PositionsPerFrame);
}
ObjectMotion::~ObjectMotion() {
scheduler.Finish();
const auto allocator = instance.GetAllocator();
if (params_buffer) {
vmaDestroyBuffer(allocator, params_buffer, params_allocation);
}
if (positions_buffer) {
vmaDestroyBuffer(allocator, positions_buffer, positions_allocation);
}
}
void ObjectMotion::OnFrameStart() {
if (!enabled) {
return;
}
if (BbStats::enabled && (frame % 600) == 0 && history.stats.draws) {
const auto& s = history.stats;
std::printf("Object motion: %llu draws, %llu stored, %llu with history, %llu unmatched, "
"%llu capacity skips, %llu invalid, %llu still (600 frames); last frame %u/%u vertices; "
"index ranges %llu reused, %llu scanned (%llu found changed), %zu cached\n",
(unsigned long long)s.draws, (unsigned long long)s.stored,
(unsigned long long)s.loaded, (unsigned long long)s.unmatched,
(unsigned long long)s.exhausted, (unsigned long long)s.invalid,
(unsigned long long)s.still,
history.Used(), PositionsPerFrame, (unsigned long long)index_ranges.stats.hits,
(unsigned long long)index_ranges.stats.scans,
(unsigned long long)index_ranges.stats.stale, index_ranges.Size());
index_ranges.stats = {};
history.stats = {};
}
history.NextFrame();
++frame;
if (frame % Motion::IndexRangeCache::Unused == 0) {
index_ranges.Trim(frame);
}
const u32 slot = u32(frame % FrameSlots);
// Do not overwrite host parameters still referenced by queued GPU work.
if (params_ticks[slot]) {
scheduler.Wait(params_ticks[slot]);
params_ticks[slot] = 0;
}
params_used = 0;
written = false;
scheduler.EndRendering();
// Last frame's stores must be visible to this frame's loads. Also finish old reads
// before the ping-pong half they reference is reused for writes.
scheduler.Record([](vk::CommandBuffer cmd) {
const vk::MemoryBarrier2 barrier{
.srcStageMask = vk::PipelineStageFlagBits2::eVertexShader,
.srcAccessMask = vk::AccessFlagBits2::eShaderRead | vk::AccessFlagBits2::eShaderWrite,
.dstStageMask = vk::PipelineStageFlagBits2::eVertexShader,
.dstAccessMask = vk::AccessFlagBits2::eShaderRead | vk::AccessFlagBits2::eShaderWrite,
};
cmd.pipelineBarrier2({.memoryBarrierCount = 1, .pMemoryBarriers = &barrier});
});
}
u32 ObjectMotion::PrepareDraw(const DrawInfo& draw) {
if (!enabled || BbToggle::Disabled(1u << 29) || params_used >= ParamsPerFrame) {
return 0;
}
const auto allocation = history.Prepare(draw);
u32 flags = (allocation.store ? Shader::MotionVectors::FlagStore : 0) |
(allocation.load ? Shader::MotionVectors::FlagLoad : 0);
if (!flags) {
return 0;
}
const u32 slot = u32(frame % FrameSlots);
const u32 index = 1 + slot * ParamsPerFrame + params_used++;
u32* param = params_mapped + index * 8;
param[0] = allocation.store;
param[1] = allocation.load;
param[2] = allocation.vertices;
param[3] = flags;
param[4] = allocation.first_vertex;
param[5] = allocation.first_instance;
param[6] = allocation.instances;
param[7] = 0;
params_ticks[slot] = scheduler.CurrentTick();
return index;
}
void ObjectMotion::EnsureImage(u32 width, u32 height) {
if (image && width == image_width && height == image_height) {
return;
}
const auto device = instance.GetDevice();
if (image) {
scheduler.Finish();
}
view.reset();
image_layout = vk::ImageLayout::eUndefined;
image = VideoCore::UniqueImage(device, instance.GetAllocator());
image.Create(vk::ImageCreateInfo{
.imageType = vk::ImageType::e2D,
.format = vk::Format::eR16G16B16A16Sfloat,
.extent = {width, height, 1},
.mipLevels = 1,
.arrayLayers = 1,
.samples = vk::SampleCountFlagBits::e1,
.tiling = vk::ImageTiling::eOptimal,
.usage = vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eSampled,
.initialLayout = vk::ImageLayout::eUndefined,
});
view = Check(device.createImageViewUnique({
.image = vk::Image(image),
.viewType = vk::ImageViewType::e2D,
.format = vk::Format::eR16G16B16A16Sfloat,
.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1},
}));
image_width = width;
image_height = height;
written = false;
}
void ObjectMotion::Attach(RenderState& state, u32 width, u32 height) {
constexpr u32 slot = Shader::MotionVectors::Output;
EnsureImage(width, height);
auto& attachment = state.color_attachments[slot];
attachment = {};
attachment.image_view = *view;
attachment.image_layout = vk::ImageLayout::eColorAttachmentOptimal;
if (!written || image_layout != vk::ImageLayout::eColorAttachmentOptimal) {
// Clear once per frame; later reads must not erase the validity of this history.
scheduler.EndRendering();
const vk::ImageMemoryBarrier2 barrier{
.srcStageMask = vk::PipelineStageFlagBits2::eAllCommands,
.srcAccessMask = vk::AccessFlagBits2::eMemoryRead | vk::AccessFlagBits2::eMemoryWrite,
.dstStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput,
.dstAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite |
vk::AccessFlagBits2::eColorAttachmentRead,
.oldLayout = image_layout,
.newLayout = vk::ImageLayout::eColorAttachmentOptimal,
.image = vk::Image(image),
.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1},
};
// Recorded in order on the recording thread: CommandBuffer() here switched the rest of
// the submission (most of the G-buffer) to direct recording on the GPU thread.
scheduler.Record([barrier](vk::CommandBuffer cmd) {
cmd.pipelineBarrier2({.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &barrier});
});
image_layout = vk::ImageLayout::eColorAttachmentOptimal;
attachment.is_clear = !written;
written = true;
}
state.num_color_attachments = std::max<u16>(state.num_color_attachments, slot + 1);
}
vk::ImageView ObjectMotion::PrepareRead(vk::CommandBuffer cmdbuf, u32 width, u32 height,
bool& valid) {
EnsureImage(std::max(image_width, 1u), std::max(image_height, 1u));
valid = written && width == image_width && height == image_height;
const vk::ImageMemoryBarrier2 barrier{
.srcStageMask = vk::PipelineStageFlagBits2::eAllCommands,
.srcAccessMask = vk::AccessFlagBits2::eMemoryRead | vk::AccessFlagBits2::eMemoryWrite,
.dstStageMask = vk::PipelineStageFlagBits2::eComputeShader,
.dstAccessMask = vk::AccessFlagBits2::eShaderRead,
.oldLayout = image_layout,
.newLayout = vk::ImageLayout::eGeneral,
.image = vk::Image(image),
.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1},
};
cmdbuf.pipelineBarrier2({.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &barrier});
image_layout = vk::ImageLayout::eGeneral;
return *view;
}
} // namespace Vulkan
@@ -0,0 +1,91 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: object motion vectors for temporal upscaling (docs/ROADMAP.md, step 1).
//
// G-buffer shaders preserve clip positions for the exact indexed range of each draw.
// Matching includes vertex streams, index contents and base offsets. A missing match falls
// back to camera motion. See motion_history.h for CPU bookkeeping and its regression test.
#pragma once
#include <array>
#include "video_core/renderer_vulkan/motion_history.h"
#include "common/types.h"
#include "video_core/renderer_vulkan/vk_common.h"
#include "video_core/texture_cache/image.h"
namespace Vulkan {
class Instance;
class Scheduler;
struct RenderState;
class ObjectMotion {
public:
ObjectMotion(const Instance& instance, Scheduler& scheduler);
~ObjectMotion();
[[nodiscard]] bool Enabled() const noexcept {
return enabled;
}
using DrawInfo = Motion::Draw;
u32 PrepareDraw(const DrawInfo& draw);
/// Small skeletons: whether the bone palette changed (Motion::History::Moving).
bool Moving(const Motion::History::GateKey& key, u64 palette) {
return history.Moving(key, palette);
}
/// Index range and topology hash of an indexed draw, reused across frames.
template <class Scan>
Motion::IndexRangeCache::Result IndexRange(const Motion::IndexRangeCache::Key& key,
Scan&& scan) {
return index_ranges.Get(key, frame, scan);
}
/// Rendering of a motion pipeline: the motion attachment (render-target size).
void Attach(RenderState& state, u32 width, u32 height);
/// Start of a frame (display pass).
void OnFrameStart();
void InvalidateHistory() { history.NextFrame(); history.NextFrame(); }
/// The motion image for the compose pass (layout General after this call) and whether it
/// holds this frame's vectors at `width` x `height`.
vk::ImageView PrepareRead(vk::CommandBuffer cmdbuf, u32 width, u32 height, bool& valid);
/// The image of the last PrepareRead (layout General), or null.
[[nodiscard]] vk::ImageView View() const noexcept {
return view ? *view : vk::ImageView{};
}
private:
void EnsureImage(u32 width, u32 height);
const Instance& instance;
Scheduler& scheduler;
bool enabled = false;
// Parameter ring (host visible): FrameSlots frames of ParamsPerFrame entries (two u32x4),
// element 0 all zero (motion off).
static constexpr u32 FrameSlots = 4;
static constexpr u32 ParamsPerFrame = 8192;
// Positions (device local): two halves (current/previous frame) of vec4; element 0 is reserved.
static constexpr u32 PositionsPerFrame = 4u << 20;
Motion::History history{PositionsPerFrame};
Motion::IndexRangeCache index_ranges;
std::array<u64, FrameSlots> params_ticks{};
vk::Buffer params_buffer{};
VmaAllocation params_allocation{};
u32* params_mapped{};
vk::Buffer positions_buffer{};
VmaAllocation positions_allocation{};
u64 frame = 0;
u32 params_used = 0;
VideoCore::UniqueImage image;
vk::UniqueImageView view;
u32 image_width = 0, image_height = 0;
bool written = false; ///< this frame's vectors are in the image
vk::ImageLayout image_layout = vk::ImageLayout::eUndefined;
};
} // namespace Vulkan
@@ -3,8 +3,12 @@
#include <atomic>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <mutex>
#include <ranges>
#include <string>
#include <unordered_set>
#include "common/hash.h"
#include "common/io_file.h"
@@ -18,6 +22,7 @@
#include "video_core/amdgpu/liverpool.h"
#include "video_core/cache_storage.h"
#include "video_core/renderer_vulkan/liverpool_to_vk.h"
#include "video_core/renderer_vulkan/motion_history.h"
#include "video_core/renderer_vulkan/vk_draw_prep.h"
#include "video_core/renderer_vulkan/vk_instance.h"
#include "video_core/renderer_vulkan/vk_pipeline_serialization.h"
@@ -166,6 +171,7 @@ const Shader::RuntimeInfo& PipelineCache::BuildRuntimeInfo(PipelineSelection& se
!instance.IsDepthClipControlSupported() &&
regs.clipper_control.clip_space == AmdGpu::ClipSpace::MinusWToW;
info.hw.vs.clip_disable = regs.IsClipDisabled();
info.hw.vs.motion_vectors = sel.motion;
break;
}
case HwStage::Fragment: {
@@ -179,6 +185,7 @@ const Shader::RuntimeInfo& PipelineCache::BuildRuntimeInfo(PipelineSelection& se
? regs.aa_config.NumSamples()
: 1;
info.hw.fs.z_export_format = regs.z_export_format;
info.hw.fs.motion_vectors = sel.motion;
u8 stencil_ref_export_enable = regs.depth_shader_control.stencil_op_val_export_enable |
regs.depth_shader_control.stencil_test_val_export_enable;
info.hw.fs.mrtz_mask = regs.depth_shader_control.z_export_enable |
@@ -394,8 +401,10 @@ const GraphicsPipeline* PipelineCache::TryPreparedPipeline(const PreparedDraw& p
const GraphicsPipeline* PipelineCache::GetGraphicsPipeline(const DrawIndirectParams params,
const PreparedDraw* prepared) {
used_prepared = nullptr;
if (prepared) {
if (const auto* pipeline = TryPreparedPipeline(*prepared)) {
used_prepared = prepared;
return pipeline;
}
}
@@ -483,6 +492,22 @@ bool PipelineCache::RefreshGraphicsKey(PipelineSelection& sel) {
const bool skip_cb_binding =
regs.color_control.mode == AmdGpu::ColorControl::OperationMode::Disable;
// Only potentially animated G-buffer draws may use the extra motion attachment. The
// ordinary stage variant is needed first to inspect the vertex shader's resources.
bool motion_possible = false;
{
u32 bound = 0;
for (s32 cb = 0; cb < AmdGpu::NUM_COLOR_BUFFERS && !skip_cb_binding; ++cb) {
bound += (regs.color_buffers[cb] && regs.color_target_mask.GetMask(cb)) ? 1 : 0;
}
motion_possible = Shader::MotionVectors::positions_address != 0 && bound >= 5 &&
!regs.color_buffers[Shader::MotionVectors::Output] &&
regs.depth_buffer.DepthValid() &&
regs.depth_buffer.NumSamples() == 1 && !regs.IsClipDisabled() &&
regs.stage_enable.raw == AmdGpu::ShaderStageEnable::VgtStages::Vs;
}
sel.motion = false;
// First pass to fill render target information needed by shader recompiler
for (s32 cb = 0; cb < AmdGpu::NUM_COLOR_BUFFERS && !skip_cb_binding; ++cb) {
const auto& col_buf = regs.color_buffers[cb];
@@ -512,6 +537,56 @@ bool PipelineCache::RefreshGraphicsKey(PipelineSelection& sel) {
if (!RefreshGraphicsStages(sel)) {
return false;
}
if (motion_possible) {
const auto* vs = sel.infos[static_cast<u32>(Shader::SwStage::Vertex)];
if (vs) {
// Shaders with a bone palette (motion_history.h). Small skeletons (weapons,
// props) also include static world pieces: the rasterizer gives those
// history only while their constants change.
for (const auto& resource : vs->buffers) {
if (resource.IsSpecial()) continue;
const auto buffer = resource.GetSharp(*vs);
if (buffer.Valid() && buffer.GetStride() == 16 &&
Motion::ClassifyBuffer(buffer.GetSize()) != Motion::BufferRole::Other) {
sel.motion = true;
break;
}
}
}
// BB_MOTION_SELECT_LOG=1: each G-buffer vertex shader once, with its buffer sizes
// and the selection, to find animated models that the size rule leaves out.
static const bool select_log = [] {
const char* value = std::getenv("BB_MOTION_SELECT_LOG");
return value && value[0] == '1';
}();
if (select_log && vs) {
std::string sizes;
for (const auto& resource : vs->buffers) {
if (resource.IsSpecial()) continue;
const auto buffer = resource.GetSharp(*vs);
sizes += fmt::format(" {}/{}", buffer.GetSize(), buffer.GetStride());
}
static std::mutex log_mutex;
static std::unordered_set<u64> logged;
std::scoped_lock lock{log_mutex};
if (logged.insert(vs->pgm_hash ^ std::hash<std::string>{}(sizes)).second) {
std::printf("Motion select: vs %016llx %s, buffers (size/stride):%s\n",
static_cast<unsigned long long>(vs->pgm_hash),
sel.motion ? "ON " : "off", sizes.c_str());
}
}
// Keep the old broad path available for visual A/B tests.
static const bool all_motion = [] {
const char* value = std::getenv("BB_OBJECT_MOTION_ALL");
return value && value[0] == '1';
}();
if (all_motion) {
sel.motion = true;
}
if (sel.motion && !RefreshGraphicsStages(sel)) {
return false;
}
}
// Second pass to mask out render targets not written by shader and fill remaining info
u8 color_samples = 0;
@@ -543,6 +618,20 @@ bool PipelineCache::RefreshGraphicsKey(PipelineSelection& sel) {
key.num_samples = std::max(key.num_samples, color_samples);
}
if (sel.motion) {
constexpr u32 mv = Shader::MotionVectors::Output;
key.motion_vectors = 1;
key.mrt_mask |= 1u << mv;
key.num_color_attachments = mv + 1;
auto& color_buffer = key.color_buffers[mv];
color_buffer.data_format = AmdGpu::DataFormat::Format16_16_16_16;
color_buffer.num_format = AmdGpu::NumberFormat::Float;
color_buffer.swizzle = AmdGpu::IdentityMapping;
key.write_masks[mv] = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA;
key.color_samples[mv] = 1;
}
// Force all color samples to match depth samples to avoid unsupported MSAA configuration
if (color_samples != 0) {
const bool depth_mismatch = db_enabled && color_samples != key.depth_samples;
@@ -92,6 +92,7 @@ struct PipelineSelection {
std::array<vk::ShaderModule, MaxShaderStages> modules{};
std::optional<Shader::Gcn::FetchShaderData> fetch_shader{};
GraphicsPipelineKey graphics_key{};
bool motion = false;
DrawIndirectParams draw_indirect_params{};
struct PrepWorker* worker{}; ///< set: read-only selection for a draw-preparation worker
};
@@ -137,6 +138,11 @@ public:
/// and flattened user data match; null to take the regular path.
const GraphicsPipeline* TryPreparedPipeline(const PreparedDraw& prepared);
/// bbport: the prepared draw the last GetGraphicsPipeline used, or null (regular path).
[[nodiscard]] const PreparedDraw* UsedPrepared() const noexcept {
return used_prepared;
}
const ComputePipeline* GetComputePipeline();
using Result = std::tuple<const Shader::Info*, vk::ShaderModule,
@@ -186,6 +192,7 @@ private:
/// bbport: exclusive for program/permutation insertions, shared for worker lookups.
std::shared_mutex programs_mutex;
u64 prepared_hits = 0, prepared_misses = 0;
const PreparedDraw* used_prepared = nullptr;
tsl::robin_map<ComputePipelineKey, std::unique_ptr<ComputePipeline>> compute_pipelines;
tsl::robin_map<GraphicsPipelineKey, std::unique_ptr<GraphicsPipeline>> graphics_pipelines;
PipelineSelection sel{}; ///< GPU thread selection state
@@ -12,9 +12,9 @@
namespace Serialization {
/* You should increment versions below once corresponding serialization scheme is changed. */
static constexpr u32 ShaderBinaryVersion = 5u;
static constexpr u32 ShaderMetaVersion = 5u;
static constexpr u32 PipelineKeyVersion = 3u;
static constexpr u32 ShaderBinaryVersion = 6u;
static constexpr u32 ShaderMetaVersion = 6u;
static constexpr u32 PipelineKeyVersion = 4u;
} // namespace Serialization
namespace Vulkan {
@@ -115,6 +115,12 @@ bool LoadShaderMeta(Serialization::Archive& ar, Shader::Info& info,
spec.Deserialize(ar);
info.Deserialize(ar);
// Motion vertex shaders embed session-local buffer device addresses. They must be
// recompiled for the current allocation, never loaded from a previous process.
if (info.hw_stage == Shader::HwStage::Vertex && spec.runtime_info.hw.vs.motion_vectors) {
return false;
}
fetch_shader_data = spec.fetch_shader_data;
return true;
}
@@ -1,6 +1,8 @@
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <xxhash.h>
#include "video_core/renderer_vulkan/ui_composition.h"
#include "bbport_toggles.h"
#include "video_core/renderer_vulkan/vk_frame_capture.h"
#include "common/debug.h"
@@ -45,9 +47,13 @@ Rasterizer::Rasterizer(const Instance& instance_, Scheduler& scheduler_, Runtime
guest_markers_enabled{EmulatorSettings.IsVkGuestMarkersEnabled()} {
// Before the rasterizer is bound: Liverpool enqueues buffers only once it sees it.
draw_prep = std::make_unique<DrawPreparation>(pipeline_cache);
scene_targets = std::make_unique<SceneTargets>(instance, scheduler, runtime, texture_cache);
// Object motion first: it fixes the buffer addresses the motion shader variants embed.
object_motion = std::make_unique<ObjectMotion>(instance, scheduler);
camera_motion = std::make_unique<CameraMotion>(instance, scheduler, texture_cache, runtime);
camera_motion->SetObjectMotion(object_motion.get());
upscaler = std::make_unique<TemporalUpscaler>(instance, scheduler, texture_cache, runtime,
*camera_motion);
*camera_motion, *scene_targets);
if (!EmulatorSettings.IsNullGPU()) {
liverpool->BindRasterizer(this);
}
@@ -296,10 +302,18 @@ void Rasterizer::Draw(bool is_indexed, u32 index_offset, const PreparedDraw* pre
// target is that frame.
if (camera_motion->Enabled() && regs.color_buffers[0] &&
FrameCapture::IsDisplayBuffer(regs.color_buffers[0].Address())) {
scene_started = false;
camera_motion->OnDisplayPass(cb_descs[0].first);
upscaler->OnFrameStart();
if (upscaler->OnFrameStart()) {
object_motion->InvalidateHistory();
camera_motion->InvalidateHistory();
}
camera_motion->SetJitter(upscaler->Jitter());
object_motion->OnFrameStart();
NoteFrameStart();
}
const GraphicsPipeline* pipeline = pipeline_cache.GetGraphicsPipeline({}, prepared);
bind_prepared = pipeline_cache.UsedPrepared();
if (prepared) {
draw_prep->Count(pipeline != nullptr &&
prepared->state.load(std::memory_order_acquire) == PreparedDraw::Ready &&
@@ -308,18 +322,25 @@ void Rasterizer::Draw(bool is_indexed, u32 index_offset, const PreparedDraw* pre
if (!pipeline) {
return;
}
if (upscaler->Enabled()) {
// Scaled presets: the first UI draw starts the upscale.
upscaler->OnDraw(pipeline->GetStage(Shader::SwStage::Vertex).pgm_hash);
}
motion_draw = pipeline->GetGraphicsKey().motion_vectors;
motion_geometry = 0;
PrepareRenderState(pipeline);
if (!BindResources(pipeline)) {
if (upscaler->Enabled() && std::popcount(pipeline->GetGraphicsKey().mrt_mask) == 1) {
const auto& viewport = liverpool->regs.viewports[0];
upscaler->OnDraw(pipeline->GetStage(Shader::SwStage::Vertex).pgm_hash,
cb_descs[0].first, db_desc.first,
UiComposition::NativeViewport(viewport.xscale * 2, viewport.yscale * 2));
}
const PreparedDraw* draw_prepared = bind_prepared;
const bool bound = BindResources(pipeline);
bind_prepared = nullptr; // indirect draws and dispatches bind without prepared sharps
if (!bound) {
return;
}
const auto state = BeginRendering(pipeline);
BindVertexBuffers(pipeline);
BindVertexBuffers(pipeline, draw_prepared);
if (is_indexed) {
BindIndexBuffer(index_offset);
}
@@ -333,6 +354,88 @@ void Rasterizer::Draw(bool is_indexed, u32 index_offset, const PreparedDraw* pre
push_data.xoffset *= target_scale[0];
push_data.yscale *= target_scale[1];
push_data.yoffset *= target_scale[1];
if (motion_draw && motion_geometry) {
const auto& vs = pipeline->GetStage(Shader::SwStage::Vertex);
const auto [base_vertex, first_instance] =
GetDrawOffsets(regs, vs, pipeline->GetFetchShader());
const u32 index_size =
regs.index_buffer_type.index_type == AmdGpu::IndexType::Index16 ? 2u : 4u;
const VAddr index_address = is_indexed
? regs.index_base_address.Address<VAddr>() + u64(index_offset) * index_size : 0;
// Without a character-size skeleton the draw is gated on its bone palette changing
// since last frame (model constants follow the camera, the palette does not). The
// check comes first: most gated draws are static world pieces with long index lists.
static const bool all_motion = [] {
const char* value = std::getenv("BB_OBJECT_MOTION_ALL");
return value && value[0] == '1';
}();
bool gated = !all_motion;
u64 palette = 0;
for (const auto& resource : vs.buffers) {
if (resource.IsSpecial()) continue;
const auto buffer = resource.GetSharp(vs);
const u32 size = buffer.GetSize();
if (!buffer.Valid() || buffer.GetStride() != 16) continue;
const auto role = Motion::ClassifyBuffer(size);
if (role == Motion::BufferRole::Skeleton) {
gated = false;
break;
}
const VAddr address = buffer.base_address;
if (role == Motion::BufferRole::SmallSkeleton &&
memory->IsValidGpuMapping(address, 0) &&
memory->ClampRangeSize(address, size) == size) {
palette = XXH3_64bits_withSeed(reinterpret_cast<const void*>(address), size,
palette);
}
}
push_data.motion_param = 0;
if (!gated || object_motion->Moving({.shader = vs.pgm_hash,
.geometry = motion_geometry,
.indices = index_address,
.index_count = regs.num_indices,
.instances = regs.num_instances.NumInstances(),
.first_instance = first_instance},
palette)) {
Motion::VertexRange range{base_vertex, regs.num_indices};
u64 topology = 0;
if (is_indexed) {
range = {};
const u64 bytes = u64(regs.num_indices) * index_size;
if (index_address && memory->IsValidGpuMapping(index_address, 0) &&
memory->ClampRangeSize(index_address, bytes) == bytes) {
const bool restart = regs.enable_primitive_restart != 0;
const u32 count = regs.num_indices;
const auto scanned = object_motion->IndexRange(
{index_address, count, index_size, s32(base_vertex), restart}, [&] {
const auto* data = reinterpret_cast<const void*>(index_address);
const auto scan_range =
index_size == 2
? Motion::IndexedRange(
std::span(static_cast<const u16*>(data), count),
s32(base_vertex), restart)
: Motion::IndexedRange(
std::span(static_cast<const u32*>(data), count),
s32(base_vertex), restart);
return Motion::IndexRangeCache::Result{scan_range,
XXH3_64bits(data, bytes)};
});
range = scanned.range;
topology = scanned.topology;
}
}
push_data.motion_param = object_motion->PrepareDraw({
.shader = vs.pgm_hash,
.geometry = motion_geometry,
.indices = index_address,
.topology = topology,
.index_count = regs.num_indices,
.instances = regs.num_instances.NumInstances(),
.first_instance = first_instance,
.vertices = range,
});
}
}
pipeline->BindResources(set_writes, push_data);
// bbport: jitter geometry drawn with the scene depth, not full-screen passes (a shifted
// full-screen quad leaves an edge column unwritten).
@@ -400,7 +503,16 @@ void Rasterizer::DrawIndirect(bool is_indexed, VAddr arg_address, u32 offset, u3
return;
}
// Indirect arguments may be GPU-written: leave these draws on camera fallback.
motion_draw = false;
motion_geometry = 0;
PrepareRenderState(pipeline);
if (upscaler->Enabled() && std::popcount(pipeline->GetGraphicsKey().mrt_mask) == 1) {
const auto& viewport = liverpool->regs.viewports[0];
upscaler->OnDraw(pipeline->GetStage(Shader::SwStage::Vertex).pgm_hash,
cb_descs[0].first, db_desc.first,
UiComposition::NativeViewport(viewport.xscale * 2, viewport.yscale * 2));
}
if (!BindResources(pipeline)) {
return;
}
@@ -597,8 +709,22 @@ bool Rasterizer::BindResources(const Pipeline* pipeline) {
set_writes.resize(set_writes.size() + stage->buffers.size() + stage->images.size() +
stage->samplers.size());
stage->PushUd(binding, push_data);
BindBuffers(*stage, binding, push_data);
BindTextures(*stage, binding);
// Sharps a draw-preparation worker read from the same flattened user data.
const PreparedStage* prepared = nullptr;
if (bind_prepared && !BbToggle::Disabled(BbToggle::PreparedResources)) {
for (u32 i = 0; i < bind_prepared->num_stages; ++i) {
const auto& candidate = bind_prepared->stages[i];
if (&candidate.program->info == stage &&
candidate.num_images == stage->images.size() &&
candidate.num_samplers == stage->samplers.size() &&
candidate.num_buffers == stage->buffers.size()) {
prepared = &candidate;
break;
}
}
}
BindBuffers(*stage, prepared, binding, push_data);
BindTextures(*stage, prepared, binding);
uses_dma |= stage->uses_dma;
}
@@ -610,14 +736,28 @@ bool Rasterizer::BindResources(const Pipeline* pipeline) {
return true;
}
void Rasterizer::BindVertexBuffers(const GraphicsPipeline* pipeline) {
void Rasterizer::BindVertexBuffers(const GraphicsPipeline* pipeline, const PreparedDraw* prepared) {
const auto& regs = liverpool->regs;
VertexInputs<vk::VertexInputAttributeDescription2EXT> attributes;
VertexInputs<vk::VertexInputBindingDescription2EXT> bindings;
VertexInputs<vk::VertexInputBindingDivisorDescriptionEXT> divisors;
VertexInputs<AmdGpu::Buffer> guest_buffers;
pipeline->GetVertexInputs(attributes, bindings, divisors, guest_buffers,
regs.vgt_instance_step_rate_0, regs.vgt_instance_step_rate_1);
// Inputs a draw-preparation worker read from the same registers and (verified) user data.
const auto& fetch = pipeline->GetFetchShader();
const PreparedVertexInputs* ready =
prepared && prepared->vertex.valid && instance.IsVertexInputDynamicState() && fetch &&
fetch->attributes.size() == prepared->vertex.count && !FrameCapture::Active() &&
!BbToggle::Disabled(BbToggle::PreparedResources)
? &prepared->vertex
: nullptr;
if (ready) {
attributes.assign(ready->attributes, ready->attributes + ready->count);
bindings.assign(ready->bindings, ready->bindings + ready->count);
guest_buffers.assign(ready->buffers, ready->buffers + ready->count);
} else {
pipeline->GetVertexInputs(attributes, bindings, divisors, guest_buffers,
regs.vgt_instance_step_rate_0, regs.vgt_instance_step_rate_1);
}
if (instance.IsVertexInputDynamicState()) {
// Update current vertex inputs.
@@ -630,6 +770,23 @@ void Rasterizer::BindVertexBuffers(const GraphicsPipeline* pipeline) {
// If there are no bindings, there is nothing further to do.
return;
}
if (motion_draw && !guest_buffers.empty()) {
// Include every stream: identical position buffers can be paired with different
// skinning/instance data. Index topology and base offsets are matched separately.
motion_geometry = ready ? ready->buffers_hash
: XXH3_64bits(guest_buffers.data(),
guest_buffers.size() * sizeof(AmdGpu::Buffer));
}
// Object motion research: vertex streams of G-buffer draws.
if (FrameCapture::Active() && gbuffer_draw) {
for (const auto& vb : guest_buffers) {
char note[128];
std::snprintf(note, sizeof(note), "\n gb vb at %#llx size %u stride %u",
(unsigned long long)vb.base_address, u32(vb.GetSize()),
u32(vb.GetStride()));
FrameCapture::Note(note);
}
}
struct BufferRange {
VAddr base_address;
@@ -644,14 +801,20 @@ void Rasterizer::BindVertexBuffers(const GraphicsPipeline* pipeline) {
// Build list of ranges covering the requested buffers
VertexInputs<BufferRange> ranges{};
for (const auto& buffer : guest_buffers) {
if (buffer.base_address != 0 && buffer.GetSize() > 0) {
ranges.emplace_back(buffer.base_address, buffer.base_address + buffer.GetSize());
VertexInputs<BufferRange> ranges_merged{};
if (ready) {
for (u32 i = 0; i < ready->num_ranges; ++i) {
ranges_merged.emplace_back(ready->ranges[i].base, ready->ranges[i].end);
}
} else {
for (const auto& buffer : guest_buffers) {
if (buffer.base_address != 0 && buffer.GetSize() > 0) {
ranges.emplace_back(buffer.base_address, buffer.base_address + buffer.GetSize());
}
}
}
// Merge connecting ranges together
VertexInputs<BufferRange> ranges_merged{};
if (!ranges.empty()) {
std::ranges::sort(ranges, [](const BufferRange& lhv, const BufferRange& rhv) {
return lhv.base_address < rhv.base_address;
@@ -680,13 +843,15 @@ void Rasterizer::BindVertexBuffers(const GraphicsPipeline* pipeline) {
VertexInputs<vk::DeviceSize> host_offsets;
VertexInputs<vk::DeviceSize> host_sizes;
VertexInputs<vk::DeviceSize> host_strides;
u32 stream_with_memory = 0;
for (const auto& buffer : guest_buffers) {
if (buffer.base_address != 0 && buffer.GetSize() > 0) {
const auto host_buffer_info =
std::ranges::find_if(ranges_merged, [&](const BufferRange& range) {
return buffer.base_address >= range.base_address &&
buffer.base_address < range.end_address;
});
ready ? ranges_merged.begin() + ready->range_index[stream_with_memory++]
: std::ranges::find_if(ranges_merged, [&](const BufferRange& range) {
return buffer.base_address >= range.base_address &&
buffer.base_address < range.end_address;
});
ASSERT(host_buffer_info != ranges_merged.cend());
host_buffers.emplace_back(host_buffer_info->buffer->Handle());
host_offsets.push_back(host_buffer_info->offset + buffer.base_address -
@@ -898,10 +1063,12 @@ bool Rasterizer::IsComputeImageClear(const Pipeline* pipeline) {
return true;
}
void Rasterizer::BindBuffers(const Shader::Info& stage, Shader::Backend::Bindings& binding,
void Rasterizer::BindBuffers(const Shader::Info& stage, const PreparedStage* prepared,
Shader::Backend::Bindings& binding,
Shader::PushData& push_data) {
const u64 alignment = instance.StorageMinAlignment();
for (const auto& desc : stage.buffers) {
for (u32 buffer_index = 0; buffer_index < stage.buffers.size(); ++buffer_index) {
const auto& desc = stage.buffers[buffer_index];
if (desc.IsSpecial()) {
if (desc.buffer_type == Shader::BufferType::GdsBuffer) {
const auto* gds_buf = buffer_cache.GetGdsBuffer();
@@ -954,11 +1121,24 @@ void Rasterizer::BindBuffers(const Shader::Info& stage, Shader::Backend::Binding
UNREACHABLE_MSG("Unexpected buffer type {}", u32(desc.buffer_type));
}
} else {
const auto vsharp = desc.GetSharp(stage);
const auto vsharp =
prepared ? prepared->buffer_sharps[buffer_index] : desc.GetSharp(stage);
if (vsharp.GetSize() == 864 && gbuffer_draw &&
memory->IsValidGpuMapping(vsharp.base_address, 0)) {
camera_motion->OnConstants(reinterpret_cast<const float*>(vsharp.base_address));
}
// Object motion research: the vertex shader buffers of G-buffer draws.
if (FrameCapture::Active() && gbuffer_draw && stage.sw_stage == Shader::SwStage::Vertex &&
vsharp.base_address != 0 && memory->IsValidGpuMapping(vsharp.base_address, 0)) {
const auto* f = reinterpret_cast<const float*>(vsharp.base_address);
char note[192];
std::snprintf(note, sizeof(note),
"\n gb vs %08x slot %u at %#llx size %u stride %u: %g %g %g %g",
u32(stage.pgm_hash), binding.buffer,
(unsigned long long)vsharp.base_address, u32(vsharp.GetSize()),
u32(vsharp.GetStride()), f[0], f[1], f[2], f[3]);
FrameCapture::Note(note);
}
if (FrameCapture::Active() && vsharp.base_address != 0 && vsharp.GetSize() != 0 &&
memory->IsValidGpuMapping(vsharp.base_address, 0)) {
FrameCapture::Buffer(stage.pgm_hash, binding.buffer, vsharp.base_address,
@@ -1004,15 +1184,11 @@ void Rasterizer::BindBuffers(const Shader::Info& stage, Shader::Backend::Binding
}
Rasterizer::ImageDescCacheEntry& Rasterizer::CachedImageDescEntry(const AmdGpu::Image& sharp,
const Shader::ImageResource& res) {
const Shader::ImageResource& res,
u64 hash) {
std::array<u64, 4> key;
std::memcpy(key.data(), &sharp, sizeof(key));
const u32 flags = u32(res.is_written) | u32(res.is_depth) << 1 | u32(res.is_array) << 2;
u64 hash = flags * 0x9E3779B97F4A7C15ull;
for (const u64 word : key) {
hash = (hash ^ word) * 0xFF51AFD7ED558CCDull;
hash ^= hash >> 32;
}
// Two-way set associative: a frame binds a few thousand distinct T#s.
auto* set = &image_desc_cache[(hash % (image_desc_cache.size() / 2)) * 2];
const auto matches = [&](const ImageDescCacheEntry& e) {
@@ -1046,7 +1222,8 @@ Rasterizer::ImageDescCacheEntry& Rasterizer::CachedImageDescEntry(const AmdGpu::
return entry;
}
void Rasterizer::BindTextures(const Shader::Info& stage, Shader::Backend::Bindings& binding) {
void Rasterizer::BindTextures(const Shader::Info& stage, const PreparedStage* prepared,
Shader::Backend::Bindings& binding) {
image_bindings.clear();
image_desc_overflow.clear();
image_desc_storage.clear();
@@ -1055,8 +1232,10 @@ void Rasterizer::BindTextures(const Shader::Info& stage, Shader::Backend::Bindin
// To emulate storing to explicit mip levels, build a descriptor array with each mip level.
boost::container::small_vector<u32, 8> image_descriptor_array_sizes;
for (const auto& image_desc : stage.images) {
const auto tsharp = image_desc.GetSharp(stage);
for (u32 image_index = 0; image_index < stage.images.size(); ++image_index) {
const auto& image_desc = stage.images[image_index];
const auto tsharp =
prepared ? prepared->image_sharps[image_index] : image_desc.GetSharp(stage);
// bbport: a hash lookup per texture per draw for a diagnostic only.
static const bool warn_meta = std::getenv("BB_WARN_META_TEXTURE") != nullptr;
if (warn_meta && texture_cache.IsMeta(tsharp.Address())) {
@@ -1090,7 +1269,9 @@ void Rasterizer::BindTextures(const Shader::Info& stage, Shader::Backend::Bindin
? static_cast<u32>(tsharp.last_level - tsharp.base_level + 1)
: 1u;
auto& desc_entry = CachedImageDescEntry(tsharp, image_desc);
auto& desc_entry =
prepared ? CachedImageDescEntry(tsharp, image_desc, prepared->image_hashes[image_index])
: CachedImageDescEntry(tsharp, image_desc);
for (auto i = 0; i < num_bindings; i++) {
// bbport: a plain binding (no mip override) of the same T# resolves to the same image
// while no image was registered or unregistered.
@@ -1235,8 +1416,10 @@ void Rasterizer::BindTextures(const Shader::Info& stage, Shader::Backend::Bindin
binding.unified += array_size;
}
for (const auto& sampler : stage.samplers) {
auto ssharp = sampler.GetSharp(stage);
for (u32 sampler_index = 0; sampler_index < stage.samplers.size(); ++sampler_index) {
const auto& sampler = stage.samplers[sampler_index];
auto ssharp =
prepared ? prepared->sampler_sharps[sampler_index] : sampler.GetSharp(stage);
const auto vk_sampler =
texture_cache.GetSampler(ssharp, liverpool->regs.ta_bc_base, sampler.is_depth);
image_infos.emplace_back(vk_sampler, VK_NULL_HANDLE, vk::ImageLayout::eGeneral);
@@ -1259,6 +1442,21 @@ RenderState Rasterizer::BeginRendering(const GraphicsPipeline* pipeline) {
std::pair<vk::ImageView, vk::ImageLayout> original_color{};
const auto& regs = liverpool->regs;
const auto& key = pipeline->GetGraphicsKey();
if (std::popcount(key.mrt_mask & 0x7f) >= 5 && db_desc.first &&
scene_targets->Eligible(texture_cache.GetImage(db_desc.first))) scene_started = true;
// A proxy attachment cannot represent MSAA or a feedback loop that reads the
// same image through the guest's native descriptor during this draw.
bool reduced = scene_started && upscaler->RasterScaling() && key.num_samples == 1;
for (const auto& [id,desc] : cb_descs) {
if (id) {
const auto& image = texture_cache.GetImage(id);
if (!scene_targets->Eligible(image) || image.binding.is_bound ||
image.binding.needs_rebind || desc.view_info.range.base.level ||
desc.view_info.range.base.layer) reduced = false;
}
}
if (db_desc.first && !scene_targets->Eligible(texture_cache.GetImage(db_desc.first))) reduced = false;
push_data.scene_size = reduced ? SceneResolution::Pack(scene_targets->Size()) : 0;
RenderState state;
state.width = instance.GetMaxFramebufferWidth();
state.height = instance.GetMaxFramebufferHeight();
@@ -1286,7 +1484,9 @@ RenderState Rasterizer::BeginRendering(const GraphicsPipeline* pipeline) {
const bool is_clear = texture_cache.IsMetaCleared(col_buf.CmaskAddress(), slice);
texture_cache.TouchMeta(col_buf.CmaskAddress(), slice, false);
if (image->binding.is_bound) {
if (reduced) {
// The native image is resolved lazily when a shader/copy actually reads it.
} else if (image->binding.is_bound) {
ASSERT_MSG(!image->binding.force_general,
"Having image both as storage and render target is unsupported");
runtime.FlushBarriers();
@@ -1319,6 +1519,11 @@ RenderState Rasterizer::BeginRendering(const GraphicsPipeline* pipeline) {
attachment.clear_value = clear_value.color.uint32;
attachment.is_clear = is_clear;
if (reduced) {
const auto target = scene_targets->Attachment(image_id, desc.view_info);
attachment.image_view = target.view;
attachment.image_layout = target.layout;
}
image->usage.render_target = 1u;
if (cb == 0 && upscaler->Enabled()) {
color_redirected = upscaler->RedirectColor(image_id, desc.view_info, redirect);
@@ -1360,7 +1565,7 @@ RenderState Rasterizer::BeginRendering(const GraphicsPipeline* pipeline) {
? vk::ImageLayout::eDepthReadOnlyStencilAttachmentOptimal
: has_stencil ? vk::ImageLayout::eDepthStencilReadOnlyOptimal
: vk::ImageLayout::eDepthReadOnlyOptimal;
needs_barrier |= runtime.Transit(&image, new_layout,
if (!reduced) needs_barrier |= runtime.Transit(&image, new_layout,
vk::PipelineStageFlagBits2::eEarlyFragmentTests |
vk::PipelineStageFlagBits2::eLateFragmentTests,
vk::AccessFlagBits2::eDepthStencilAttachmentWrite |
@@ -1388,6 +1593,11 @@ RenderState Rasterizer::BeginRendering(const GraphicsPipeline* pipeline) {
attachment.stencil_clear = is_stencil_clear;
}
if (reduced) {
const auto target = scene_targets->Attachment(image_id, desc.view_info);
attachment.image_view = target.view;
attachment.image_layout = target.layout;
}
image.usage.depth_target = true;
if (upscaler->Enabled()) {
VulkanUpscalerTarget depth_redirect;
@@ -1444,8 +1654,17 @@ RenderState Rasterizer::BeginRendering(const GraphicsPipeline* pipeline) {
} else {
state.width = redirect.width;
state.height = redirect.height;
target_scale = {float(redirect.width) / float(guest_extent.first),
float(redirect.height) / float(guest_extent.second)};
// The UI movie viewport is now 1920x1080 even when the scene is 960x540.
// A native viewport (or native window-space UI vertices) must not be doubled
// again. The final display copy still uses the guest render-size coordinates.
const auto& viewport = regs.viewports[0];
const bool native_coordinates = redirect.native_ui &&
(UiComposition::NativeViewport(viewport.xscale * 2, viewport.yscale * 2) ||
(regs.IsClipDisabled() && !regs.viewport_control.xscale_enable &&
!regs.viewport_control.yscale_enable));
target_scale = UiComposition::Scale(guest_extent.first, guest_extent.second,
redirect.width, redirect.height,
native_coordinates);
}
}
@@ -1453,6 +1672,12 @@ RenderState Rasterizer::BeginRendering(const GraphicsPipeline* pipeline) {
state.num_layers = 1;
}
if (reduced) {
const auto size = scene_targets->Size();
state.width = size.width;
state.height = size.height;
target_scale = {float(size.width) / 1920.0f, float(size.height) / 1080.0f};
}
if (FrameCapture::Active()) {
std::array<const VideoCore::ImageInfo*, AmdGpu::NUM_COLOR_BUFFERS> colors{};
for (u32 cb = 0; cb < state.num_color_attachments; ++cb) {
@@ -1464,6 +1689,10 @@ RenderState Rasterizer::BeginRendering(const GraphicsPipeline* pipeline) {
db_desc.first ? &texture_cache.GetImage(db_desc.first).info : nullptr;
FrameCapture::BeginPass(colors.data(), state.num_color_attachments, depth);
}
// bbport: object motion vector attachment of G-buffer pipelines.
if (key.motion_vectors) {
object_motion->Attach(state, state.width, state.height);
}
return state;
}
@@ -1984,3 +2213,41 @@ u32 Rasterizer::GetGpuCommandProcessorThreadId() {
#endif
} // namespace Vulkan
namespace Vulkan {
void Rasterizer::NoteFrameStart() {
const u64 frame = BbStats::gpu_frames.fetch_add(1, std::memory_order_relaxed) + 1;
// BB_BUFFER_STATS=1: how many earlier frames the GPU has not finished when the GPU thread
// starts a frame — the lag that decides whether guest memory can be read in place.
static const bool stats = [] {
const char* value = std::getenv("BB_BUFFER_STATS");
return value && value[0] == '1';
}();
if (!stats) {
return;
}
static std::array<u64, 16> frame_ticks{};
static std::array<u64, 17> lag_histogram{};
u32 lag = 0;
for (u32 back = 1; back < frame_ticks.size() && back < frame; ++back) {
const u64 tick = frame_ticks[(frame - back) % frame_ticks.size()];
if (tick && !scheduler.IsFree(tick)) {
lag = back;
}
}
++lag_histogram[lag];
frame_ticks[frame % frame_ticks.size()] = scheduler.CurrentTick();
if (frame % 600 == 0) {
std::printf("GPU lag at frame start (frames not finished: count):");
for (u32 i = 0; i < lag_histogram.size(); ++i) {
if (lag_histogram[i]) {
std::printf(" %u:%llu", i, static_cast<unsigned long long>(lag_histogram[i]));
}
}
std::printf("\n");
lag_histogram = {};
}
}
} // namespace Vulkan
@@ -8,6 +8,8 @@
#include "video_core/buffer_cache/buffer_cache.h"
#include "video_core/page_manager.h"
#include "video_core/renderer_vulkan/vk_camera_motion.h"
#include "video_core/renderer_vulkan/vk_scene_resolution.h"
#include "video_core/renderer_vulkan/vk_object_motion.h"
#include "video_core/renderer_vulkan/vk_draw_prep.h"
#include "video_core/renderer_vulkan/vk_temporal_upscaler.h"
#include "video_core/renderer_vulkan/vk_pipeline_cache.h"
@@ -132,12 +134,17 @@ private:
bool FilterDraw();
void BindBuffers(const Shader::Info& stage, Shader::Backend::Bindings& binding,
void BindBuffers(const Shader::Info& stage, const PreparedStage* prepared,
Shader::Backend::Bindings& binding,
Shader::PushData& push_data);
void BindTextures(const Shader::Info& stage, Shader::Backend::Bindings& binding);
void BindTextures(const Shader::Info& stage, const PreparedStage* prepared,
Shader::Backend::Bindings& binding);
bool BindResources(const Pipeline* pipeline);
void BindVertexBuffers(const GraphicsPipeline* pipeline);
/// Display pass: counts the frame (BbStats::gpu_frames) and, with BB_BUFFER_STATS, the lag.
void NoteFrameStart();
void BindVertexBuffers(const GraphicsPipeline* pipeline,
const PreparedDraw* prepared = nullptr);
void BindIndexBuffer(u32 index_offset = 0);
void ResetBindings(bool is_compute);
@@ -162,6 +169,11 @@ private:
PipelineCache pipeline_cache;
std::unique_ptr<DrawPreparation> draw_prep;
std::unique_ptr<CameraMotion> camera_motion; // bbport: motion vectors (docs/upscaler.md)
std::unique_ptr<SceneTargets> scene_targets;
bool scene_started = false;
std::unique_ptr<ObjectMotion> object_motion;
bool motion_draw = false;
u64 motion_geometry{}; ///< vertex-stream identity of the current direct draw
bool gbuffer_draw = false;
std::unique_ptr<TemporalUpscaler> upscaler; // bbport: FSR (docs/upscaler.md)
std::array<float, 2> draw_jitter{}; ///< viewport offset of the current draw, pixels
@@ -234,8 +246,15 @@ private:
VideoCore::ImageId FindTargetMemoized(VideoCore::TextureCache::ImageDesc& desc,
LastTarget& last, auto&& make_desc,
const Parts&... parts);
/// The prepared draw whose pipeline the current Draw uses (its sharps are valid), or null.
const PreparedDraw* bind_prepared = nullptr;
/// `hash` is ImageDescHash(sharp, res), computed by a draw-preparation worker or here.
ImageDescCacheEntry& CachedImageDescEntry(const AmdGpu::Image& sharp,
const Shader::ImageResource& res);
const Shader::ImageResource& res, u64 hash);
ImageDescCacheEntry& CachedImageDescEntry(const AmdGpu::Image& sharp,
const Shader::ImageResource& res) {
return CachedImageDescEntry(sharp, res, ImageDescHash(sharp, res));
}
const VideoCore::TextureCache::ImageDesc& CachedImageDesc(const AmdGpu::Image& sharp,
const Shader::ImageResource& res) {
return CachedImageDescEntry(sharp, res).desc;
@@ -7,6 +7,7 @@
#include "video_core/renderer_vulkan/liverpool_to_vk.h"
#include "video_core/renderer_vulkan/vk_instance.h"
#include "video_core/renderer_vulkan/vk_runtime.h"
#include "video_core/renderer_vulkan/vk_scene_resolution.h"
#include "video_core/renderer_vulkan/vk_scheduler.h"
#include "video_core/texture_cache/blit_helper.h"
#include "video_core/texture_cache/image.h"
@@ -193,6 +194,7 @@ void Runtime::InlineData(VideoCore::Buffer* dst, u64 offset, u32 value) {
bool Runtime::Transit(VideoCore::Image* image, vk::ImageLayout dst_layout,
vk::PipelineStageFlags2 dst_stage, vk::AccessFlags2 dst_access,
std::optional<VideoCore::SubresourceRange> subres_range) {
if (scene_targets) scene_targets->NativeAccess(*image, dst_access);
const size_t prev_num_barriers = static_cast<size_t>(image_barriers.size());
image->GetBarriers(image_barriers, dst_layout, dst_access, dst_stage, subres_range);
return image_barriers.size() != prev_num_barriers;
@@ -15,6 +15,7 @@ class BlitHelper;
} // namespace VideoCore
namespace Vulkan {
class SceneTargets;
class Instance;
class Scheduler;
@@ -33,6 +34,7 @@ public:
}
void TickFrame();
SceneTargets* scene_targets = nullptr;
void CopyBuffer(const VideoCore::Buffer* src, const VideoCore::Buffer* dst,
std::span<const vk::BufferCopy> copies);
@@ -0,0 +1,376 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "video_core/renderer_vulkan/vk_scene_resolution.h"
#include "video_core/renderer_vulkan/vk_instance.h"
#include "video_core/renderer_vulkan/vk_runtime.h"
#include "video_core/renderer_vulkan/vk_scheduler.h"
#include "video_core/renderer_vulkan/vk_shader_util.h"
#include "video_core/texture_cache/texture_cache.h"
#include "video_core/host_shaders/depth_resample_frag.h"
#include "video_core/host_shaders/depth_stencil_resample_frag.h"
#include "video_core/host_shaders/fs_tri_vert.h"
#include <cstdio>
namespace Vulkan {
SceneTargets::SceneTargets(const Instance& i, Scheduler& s, Runtime& r,
VideoCore::TextureCache& t)
: SceneTargets(i, s, r, [&t](VideoCore::ImageId id, u64 uid) {
return t.TryGetImage(id, uid);
}) {}
SceneTargets::SceneTargets(const Instance& i, Scheduler& s, Runtime& r, Lookup get)
: instance{i}, scheduler{s}, runtime{r}, lookup{std::move(get)} {
runtime.scene_targets = this;
CreateResampleResources();
}
SceneTargets::~SceneTargets() {
scheduler.Finish();
runtime.scene_targets = nullptr;
}
vk::FormatFeatureFlags SceneTargets::Features(vk::Format format) const {
// Called for every attachment of every draw: the driver query was ~5% of the GPU thread.
// Core formats index a flat table; extension formats (large enum values) use the map.
const auto index = u32(format);
if (index < format_table.size() && format_known[index]) return format_table[index];
if (index >= format_table.size()) {
if (const auto it = format_features.find(format); it != format_features.end()) {
return it->second;
}
}
const auto features =
instance.GetPhysicalDevice().getFormatProperties(format).optimalTilingFeatures;
if (index < format_table.size()) {
format_table[index] = features;
format_known[index] = true;
} else {
format_features.emplace(format, features);
}
return features;
}
bool SceneTargets::Blittable(vk::Format format) const {
const auto features = Features(format);
const auto required = vk::FormatFeatureFlagBits::eBlitSrc | vk::FormatFeatureFlagBits::eBlitDst;
return (features & required) == required;
}
bool SceneTargets::ShaderResampled(const VideoCore::Image& image) const {
const auto format = image.backing->image.image_ci.format;
if (!image.info.props.is_depth || Blittable(format)) return false;
const auto features = Features(format);
const auto required = vk::FormatFeatureFlagBits::eSampledImage |
vk::FormatFeatureFlagBits::eDepthStencilAttachment;
return (features & required) == required &&
(!(image.aspect_mask & vk::ImageAspectFlagBits::eStencil) ||
instance.IsShaderStencilExportSupported());
}
bool SceneTargets::Eligible(const VideoCore::Image& image) const {
const auto& i = image.info;
if (i.size.width != 1920 || i.size.height != 1080 || i.size.depth != 1 ||
i.resources.levels != 1 || i.resources.layers != 1 || i.num_samples != 1 ||
i.props.is_block || !image.backing || image.backing->num_samples != 1) {
return false;
}
return Blittable(image.backing->image.image_ci.format) || ShaderResampled(image);
}
bool SceneTargets::SetSize(SceneResolution::Size next) {
if (next == size) return false;
ResolveAll();
scheduler.Finish();
entries.clear(); // no command buffer can still reference these images/views
recent = {};
size = next;
std::printf("Scene resolution: raster %ux%u, post/UI 1920x1080 (live)\n",
size.width, size.height);
return true;
}
void SceneTargets::ResolveAll() {
for (auto& [uid, entry] : entries) {
if (entry->state.dirty) {
if (auto* image = lookup(entry->source, uid)) Copy(*entry, *image, true);
}
}
}
void SceneTargets::NativeAccess(VideoCore::Image& image, vk::AccessFlags2 access) {
if (copying) return;
const auto it = entries.find(image.image_uid);
if (it == entries.end()) return;
auto& entry = *it->second;
if (entry.state.dirty) Copy(entry, image, true);
constexpr auto writes = vk::AccessFlagBits2::eShaderWrite | vk::AccessFlagBits2::eTransferWrite |
vk::AccessFlagBits2::eColorAttachmentWrite | vk::AccessFlagBits2::eDepthStencilAttachmentWrite |
vk::AccessFlagBits2::eMemoryWrite;
if (access & writes) entry.state.NativeWrite();
}
void SceneTargets::Transition(Entry& e, vk::ImageAspectFlags aspect, vk::ImageLayout layout,
vk::PipelineStageFlags2 stages, vk::AccessFlags2 access) {
scheduler.EndRendering();
const vk::ImageMemoryBarrier2 barrier{
.srcStageMask = vk::PipelineStageFlagBits2::eAllCommands,
.srcAccessMask = vk::AccessFlagBits2::eMemoryRead | vk::AccessFlagBits2::eMemoryWrite,
.dstStageMask = stages, .dstAccessMask = access,
.oldLayout = e.layout, .newLayout = layout,
.image = e.image, .subresourceRange = {aspect, 0, 1, 0, 1},
};
scheduler.Record([barrier](vk::CommandBuffer cmd) {
cmd.pipelineBarrier2({.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &barrier});
});
e.layout = layout;
}
void SceneTargets::Copy(Entry& e, VideoCore::Image& original, bool to_native) {
copying = true;
scheduler.EndRendering();
const vk::Image src = to_native ? vk::Image(e.image) : original.GetImage();
const vk::Image dst = to_native ? original.GetImage() : vk::Image(e.image);
if (ShaderResampled(original)) {
constexpr auto read_layout = vk::ImageLayout::eShaderReadOnlyOptimal;
constexpr auto write_layout = vk::ImageLayout::eDepthStencilAttachmentOptimal;
constexpr auto read_stage = vk::PipelineStageFlagBits2::eFragmentShader;
constexpr auto write_stage = vk::PipelineStageFlagBits2::eEarlyFragmentTests |
vk::PipelineStageFlagBits2::eLateFragmentTests;
constexpr auto read_access = vk::AccessFlagBits2::eShaderSampledRead;
constexpr auto write_access = vk::AccessFlagBits2::eDepthStencilAttachmentRead |
vk::AccessFlagBits2::eDepthStencilAttachmentWrite;
runtime.Transit(&original, to_native ? write_layout : read_layout,
to_native ? write_stage : read_stage,
to_native ? write_access : read_access);
runtime.FlushBarriers();
Transition(e, original.aspect_mask, to_native ? read_layout : write_layout,
to_native ? read_stage : write_stage, to_native ? read_access : write_access);
Resample(src, dst, original,
to_native ? vk::Extent2D{1920, 1080} : vk::Extent2D{size.width, size.height});
if (to_native) e.state.Resolved(); else e.state.CopiedToProxy();
copying = false;
return;
}
runtime.Transit(&original, to_native ? vk::ImageLayout::eTransferDstOptimal
: vk::ImageLayout::eTransferSrcOptimal,
vk::PipelineStageFlagBits2::eTransfer,
to_native ? vk::AccessFlagBits2::eTransferWrite : vk::AccessFlagBits2::eTransferRead);
runtime.FlushBarriers();
Transition(e, original.aspect_mask,
to_native ? vk::ImageLayout::eTransferSrcOptimal : vk::ImageLayout::eTransferDstOptimal,
vk::PipelineStageFlagBits2::eTransfer,
to_native ? vk::AccessFlagBits2::eTransferRead : vk::AccessFlagBits2::eTransferWrite);
const vk::Offset3D native{1920,1080,1}, reduced{s32(size.width),s32(size.height),1};
for (auto aspect : {vk::ImageAspectFlagBits::eColor, vk::ImageAspectFlagBits::eDepth,
vk::ImageAspectFlagBits::eStencil}) {
if (!(original.aspect_mask & aspect)) continue;
const vk::ImageBlit region{
.srcSubresource = {aspect,0,0,1},
.srcOffsets = std::array{vk::Offset3D{}, to_native ? reduced : native},
.dstSubresource = {aspect,0,0,1},
.dstOffsets = std::array{vk::Offset3D{}, to_native ? native : reduced},
};
// Nearest preserves depth/stencil and avoids assuming linear blit support for
// integer G-buffer formats. FSR reconstructs color at the HDR scene boundary.
scheduler.Record([src,dst,region](vk::CommandBuffer cmd) {
cmd.blitImage(src, vk::ImageLayout::eTransferSrcOptimal, dst,
vk::ImageLayout::eTransferDstOptimal, region, vk::Filter::eNearest);
});
}
if (to_native) e.state.Resolved(); else e.state.CopiedToProxy();
copying = false;
}
void SceneTargets::Resample(vk::Image src, vk::Image dst, const VideoCore::Image& original,
vk::Extent2D dst_size) {
const auto device = instance.GetDevice();
const auto format = original.backing->image.image_ci.format;
const bool stencil = bool(original.aspect_mask & vk::ImageAspectFlagBits::eStencil);
const auto make_view = [&](vk::Image image, vk::ImageAspectFlags aspect,
vk::ImageUsageFlags usage) {
const vk::ImageViewUsageCreateInfo usage_ci{.usage = usage};
return Check(device.createImageView({
.pNext = &usage_ci, .image = image, .viewType = vk::ImageViewType::e2D,
.format = format, .subresourceRange = {aspect, 0, 1, 0, 1},
}));
};
const auto depth_view = make_view(src, vk::ImageAspectFlagBits::eDepth,
vk::ImageUsageFlagBits::eSampled);
const auto stencil_view = stencil ? make_view(src, vk::ImageAspectFlagBits::eStencil,
vk::ImageUsageFlagBits::eSampled)
: vk::ImageView{};
const auto target_view = make_view(dst, original.aspect_mask,
vk::ImageUsageFlagBits::eDepthStencilAttachment);
scheduler.DeferOperation([device, depth_view, stencil_view, target_view] {
device.destroyImageView(depth_view);
if (stencil_view) device.destroyImageView(stencil_view);
device.destroyImageView(target_view);
});
const auto pipeline = ResamplePipeline(format, stencil);
const auto layout = *resample_layout;
scheduler.Record([=](vk::CommandBuffer cmd) {
const vk::RenderingAttachmentInfo attachment{
.imageView = target_view,
.imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal,
.loadOp = vk::AttachmentLoadOp::eDontCare,
.storeOp = vk::AttachmentStoreOp::eStore,
};
cmd.beginRendering({
.renderArea = {{0, 0}, dst_size}, .layerCount = 1,
.pDepthAttachment = &attachment,
.pStencilAttachment = stencil ? &attachment : nullptr,
});
const std::array infos{
vk::DescriptorImageInfo{{}, depth_view, vk::ImageLayout::eShaderReadOnlyOptimal},
vk::DescriptorImageInfo{{}, stencil_view, vk::ImageLayout::eShaderReadOnlyOptimal},
};
const std::array writes{
vk::WriteDescriptorSet{.dstBinding = 0, .descriptorCount = 1,
.descriptorType = vk::DescriptorType::eSampledImage,
.pImageInfo = &infos[0]},
vk::WriteDescriptorSet{.dstBinding = 1, .descriptorCount = 1,
.descriptorType = vk::DescriptorType::eSampledImage,
.pImageInfo = &infos[1]},
};
cmd.pushDescriptorSetKHR(vk::PipelineBindPoint::eGraphics, layout, 0,
vk::ArrayProxy<const vk::WriteDescriptorSet>(
stencil ? 2u : 1u, writes.data()));
cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline);
cmd.setViewportWithCount(vk::Viewport{0.f, 0.f, float(dst_size.width),
float(dst_size.height), 0.f, 1.f});
cmd.setScissorWithCount(vk::Rect2D{{0, 0}, dst_size});
cmd.draw(3, 1, 0, 0);
cmd.endRendering();
});
// The guest pipelines' dynamic state must be emitted again after this pipeline.
scheduler.GetDynamicState().Invalidate();
}
void SceneTargets::CreateResampleResources() {
const auto device = instance.GetDevice();
fs_tri_vert = vk::UniqueShaderModule(CompileSPV(FS_TRI_VERT, device), device);
depth_frag = vk::UniqueShaderModule(CompileSPV(DEPTH_RESAMPLE_FRAG, device), device);
if (instance.IsShaderStencilExportSupported()) {
depth_stencil_frag =
vk::UniqueShaderModule(CompileSPV(DEPTH_STENCIL_RESAMPLE_FRAG, device), device);
}
const std::array bindings{
vk::DescriptorSetLayoutBinding{0, vk::DescriptorType::eSampledImage, 1,
vk::ShaderStageFlagBits::eFragment},
vk::DescriptorSetLayoutBinding{1, vk::DescriptorType::eSampledImage, 1,
vk::ShaderStageFlagBits::eFragment},
};
resample_set_layout = Check(device.createDescriptorSetLayoutUnique({
.flags = vk::DescriptorSetLayoutCreateFlagBits::ePushDescriptorKHR,
.bindingCount = u32(bindings.size()), .pBindings = bindings.data(),
}));
const auto set_layout = *resample_set_layout;
resample_layout = Check(device.createPipelineLayoutUnique({
.setLayoutCount = 1, .pSetLayouts = &set_layout,
}));
}
vk::Pipeline SceneTargets::ResamplePipeline(vk::Format format, bool stencil) {
const std::pair key{format, stencil};
for (const auto& [k, pipeline] : resample_pipelines) if (k == key) return *pipeline;
const vk::PipelineInputAssemblyStateCreateInfo input_assembly{
.topology = vk::PrimitiveTopology::eTriangleList,
};
const vk::PipelineMultisampleStateCreateInfo multisampling{
.rasterizationSamples = vk::SampleCountFlagBits::e1,
};
// Reference, compare and write masks are static; the exported value replaces the reference.
const vk::StencilOpState stencil_op{
.failOp = vk::StencilOp::eReplace, .passOp = vk::StencilOp::eReplace,
.depthFailOp = vk::StencilOp::eReplace, .compareOp = vk::CompareOp::eAlways,
.compareMask = 0xff, .writeMask = 0xff, .reference = 0,
};
const vk::PipelineDepthStencilStateCreateInfo depth_state{
.depthTestEnable = true, .depthWriteEnable = true,
.depthCompareOp = vk::CompareOp::eAlways,
.stencilTestEnable = stencil, .front = stencil_op, .back = stencil_op,
};
const std::array dynamic_states{vk::DynamicState::eViewportWithCount,
vk::DynamicState::eScissorWithCount};
const vk::PipelineDynamicStateCreateInfo dynamic_info{
.dynamicStateCount = u32(dynamic_states.size()), .pDynamicStates = dynamic_states.data(),
};
const std::array stages{
vk::PipelineShaderStageCreateInfo{.stage = vk::ShaderStageFlagBits::eVertex,
.module = *fs_tri_vert, .pName = "main"},
vk::PipelineShaderStageCreateInfo{.stage = vk::ShaderStageFlagBits::eFragment,
.module = stencil ? *depth_stencil_frag : *depth_frag,
.pName = "main"},
};
const vk::PipelineRenderingCreateInfo rendering{
.depthAttachmentFormat = format,
.stencilAttachmentFormat = stencil ? format : vk::Format::eUndefined,
};
const vk::PipelineColorBlendStateCreateInfo color_blending{};
const vk::PipelineViewportStateCreateInfo viewport_info{};
const vk::PipelineVertexInputStateCreateInfo vertex_input{};
const vk::PipelineRasterizationStateCreateInfo raster{.lineWidth = 1.f};
auto pipeline = Check(instance.GetDevice().createGraphicsPipelineUnique(VK_NULL_HANDLE, {
.pNext = &rendering, .stageCount = u32(stages.size()), .pStages = stages.data(),
.pVertexInputState = &vertex_input, .pInputAssemblyState = &input_assembly,
.pViewportState = &viewport_info, .pRasterizationState = &raster,
.pMultisampleState = &multisampling, .pDepthStencilState = &depth_state,
.pColorBlendState = &color_blending, .pDynamicState = &dynamic_info,
.layout = *resample_layout,
}));
const auto result = *pipeline;
resample_pipelines.emplace_back(key, std::move(pipeline));
return result;
}
SceneTargets::Entry& SceneTargets::Get(VideoCore::ImageId id) {
auto& original = *lookup(id, 0);
// A draw has up to six targets, mostly the same as the previous draw's.
for (const auto& [uid, recent_entry] : recent) {
if (recent_entry && uid == original.image_uid) {
if (!recent_entry->state.valid) Copy(*recent_entry, original, false);
return *recent_entry;
}
}
auto& entry = entries[original.image_uid];
if (!entry) {
entry = std::make_unique<Entry>();
entry->source = id;
entry->image = VideoCore::UniqueImage(instance.GetDevice(), instance.GetAllocator());
auto ci = original.backing->image.image_ci;
ci.pNext = nullptr;
ci.extent = vk::Extent3D{size.width,size.height,1};
entry->image.Create(ci);
}
recent[recent_next++ % recent.size()] = {original.image_uid, entry.get()};
if (!entry->state.valid) Copy(*entry, original, false);
return *entry;
}
vk::ImageView SceneTargets::View(Entry& e, const VideoCore::Image& original,
const VideoCore::ImageViewInfo& info) {
for (const auto& [key,view] : e.views) if (key == info) return *view;
const auto aspect = original.info.props.is_depth
? (info.is_storage ? original.aspect_mask : vk::ImageAspectFlags(vk::ImageAspectFlagBits::eDepth))
: vk::ImageAspectFlags(vk::ImageAspectFlagBits::eColor);
const auto format = original.info.props.is_depth ? original.backing->image.image_ci.format : info.format;
auto view = Check(instance.GetDevice().createImageViewUnique({
.image = e.image, .viewType = vk::ImageViewType::e2D, .format = format,
.components = info.mapping, .subresourceRange = {aspect,0,1,0,1},
}));
const auto result = *view;
e.views.emplace_back(info, std::move(view));
return result;
}
SceneTargets::Target SceneTargets::Attachment(VideoCore::ImageId id,
const VideoCore::ImageViewInfo& info) {
auto& original = *lookup(id, 0);
auto& e = Get(id);
const bool depth = original.info.props.is_depth;
const auto layout = depth ? vk::ImageLayout::eDepthStencilAttachmentOptimal
: vk::ImageLayout::eColorAttachmentOptimal;
// Consecutive draws can stay in the same render pass.
if (e.layout != layout) {
Transition(e, original.aspect_mask, layout,
depth ? vk::PipelineStageFlagBits2::eEarlyFragmentTests | vk::PipelineStageFlagBits2::eLateFragmentTests
: vk::PipelineStageFlags2(vk::PipelineStageFlagBits2::eColorAttachmentOutput),
depth ? vk::AccessFlagBits2::eDepthStencilAttachmentRead | vk::AccessFlagBits2::eDepthStencilAttachmentWrite
: vk::AccessFlagBits2::eColorAttachmentRead | vk::AccessFlagBits2::eColorAttachmentWrite);
}
e.state.ProxyWrite();
auto attachment_info = info;
attachment_info.is_storage = true; // include stencil in attachment views
return {e.image, View(e, original, attachment_info), e.layout, e.image.image_ci.usage};
}
SceneTargets::Target SceneTargets::Read(VideoCore::ImageId id,
const VideoCore::ImageViewInfo& info,
vk::PipelineStageFlags2 stages, vk::AccessFlags2 access) {
auto& original = *lookup(id, 0);
auto& e = Get(id);
Transition(e, original.aspect_mask, vk::ImageLayout::eGeneral, stages, access);
return {e.image, View(e, original, info), e.layout, e.image.image_ci.usage};
}
} // namespace Vulkan
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <memory>
#include <functional>
#include <unordered_map>
#include "video_core/renderer_vulkan/scene_resolution.h"
#include "video_core/texture_cache/image.h"
namespace VideoCore { class TextureCache; }
namespace Vulkan {
class Instance;
class Runtime;
class Scheduler;
// Reduced raster targets, with native-size images retained for unmodified guest compute,
// integer texture loads, copies and CPU readbacks. All native access passes Runtime::Transit.
class SceneTargets {
public:
SceneTargets(const Instance&, Scheduler&, Runtime&, VideoCore::TextureCache&);
using Lookup = std::function<VideoCore::Image*(VideoCore::ImageId, u64)>;
SceneTargets(const Instance&, Scheduler&, Runtime&, Lookup);
~SceneTargets();
bool SetSize(SceneResolution::Size);
SceneResolution::Size Size() const { return size; }
bool Reduced() const { return size != SceneResolution::Size{}; }
bool Eligible(const VideoCore::Image&) const;
struct Target {
vk::Image image;
vk::ImageView view;
vk::ImageLayout layout;
vk::ImageUsageFlags usage;
};
Target Attachment(VideoCore::ImageId, const VideoCore::ImageViewInfo&);
Target Read(VideoCore::ImageId, const VideoCore::ImageViewInfo&,
vk::PipelineStageFlags2 = vk::PipelineStageFlagBits2::eComputeShader,
vk::AccessFlags2 = vk::AccessFlagBits2::eShaderRead);
void NativeAccess(VideoCore::Image&, vk::AccessFlags2);
void ResolveAll();
private:
struct Entry {
VideoCore::ImageId source{};
VideoCore::UniqueImage image;
std::vector<std::pair<VideoCore::ImageViewInfo, vk::UniqueImageView>> views;
vk::ImageLayout layout = vk::ImageLayout::eUndefined;
SceneResolution::Coherence state;
};
Entry& Get(VideoCore::ImageId);
vk::ImageView View(Entry&, const VideoCore::Image&, const VideoCore::ImageViewInfo&);
void Copy(Entry&, VideoCore::Image&, bool to_native);
// Depth/stencil formats without blit support (D32S8 on RADV) are resampled by a
// fullscreen draw writing gl_FragDepth and, with stencil export, the stencil value.
vk::FormatFeatureFlags Features(vk::Format) const;
bool Blittable(vk::Format) const;
bool ShaderResampled(const VideoCore::Image&) const;
void Resample(vk::Image src, vk::Image dst, const VideoCore::Image& original,
vk::Extent2D dst_size);
vk::Pipeline ResamplePipeline(vk::Format, bool stencil);
void CreateResampleResources();
void Transition(Entry&, vk::ImageAspectFlags, vk::ImageLayout,
vk::PipelineStageFlags2, vk::AccessFlags2);
const Instance& instance;
Scheduler& scheduler;
Runtime& runtime;
Lookup lookup;
SceneResolution::Size size;
std::unordered_map<u64, std::unique_ptr<Entry>> entries;
bool copying = false;
mutable std::unordered_map<vk::Format, vk::FormatFeatureFlags> format_features;
mutable std::array<vk::FormatFeatureFlags, 256> format_table{};
mutable std::array<bool, 256> format_known{};
std::array<std::pair<u64, Entry*>, 8> recent{}; ///< last entries by image uid
u32 recent_next = 0;
vk::UniqueShaderModule fs_tri_vert, depth_frag, depth_stencil_frag;
vk::UniqueDescriptorSetLayout resample_set_layout;
vk::UniquePipelineLayout resample_layout;
std::vector<std::pair<std::pair<vk::Format, bool>, vk::UniquePipeline>> resample_pipelines;
};
} // namespace Vulkan
@@ -60,6 +60,7 @@ TemporalUpscaler::TemporalUpscaler(const Instance& instance_, Scheduler& schedul
CameraMotion& camera_motion_, SceneTargets& scene_targets_)
: instance{instance_}, scheduler{scheduler_}, texture_cache{texture_cache_},
runtime{runtime_}, camera_motion{camera_motion_}, scene_targets{scene_targets_} {
fsr4 = std::make_unique<Fsr4Upscaler>(instance, scheduler);
// Available unless BB_UPSCALER=none; on/off and the parameters are the menu's settings.
const char* env = std::getenv("BB_UPSCALER");
enabled = !(env && std::strcmp(env, "none") == 0);
@@ -101,7 +102,7 @@ TemporalUpscaler::TemporalUpscaler(const Instance& instance_, Scheduler& schedul
if (enabled) {
std::printf("Upscaler: FSR 3.1 available (%s) on scene color before compute shader "
"%016llx\n",
BbSettings::Get().upscaler == BbSettings::UpscalerFsr3 ? "on" : "off",
BbSettings::Get().upscaler != BbSettings::UpscalerOff ? "on" : "off",
static_cast<unsigned long long>(trigger_hash));
}
}
@@ -116,7 +117,9 @@ TemporalUpscaler::~TemporalUpscaler() {
bool TemporalUpscaler::Active() const {
// Toggle 1 << 24 switches it off at run time (A/B); history restarts after.
return enabled && !failed &&
BbSettings::Get().upscaler == BbSettings::UpscalerFsr3 && !BbToggle::Disabled(1u << 24);
(BbSettings::Get().upscaler == BbSettings::UpscalerFsr3 ||
BbSettings::Get().upscaler == BbSettings::UpscalerFsr4) &&
!BbToggle::Disabled(1u << 24);
}
bool TemporalUpscaler::ReactiveOn() const {
@@ -138,6 +141,10 @@ float Halton(u32 index, u32 base) {
}
} // namespace
bool TemporalUpscaler::ReducedScene(const VideoCore::Image& color) const {
return !scaled_session && scene_targets.Reduced() && scene_targets.Eligible(color);
}
bool TemporalUpscaler::RasterScaling() const {
return !scaled_session && scene_targets.Reduced() && !done_this_frame;
}
@@ -148,7 +155,10 @@ bool TemporalUpscaler::OnFrameStart() {
if (applied_preset != preset || settings.upscaler == BbSettings::UpscalerOff) failed = false;
const bool active = Active();
const bool jitter_on = active && settings.jitter && !BbToggle::Disabled(1u << 25);
const bool changed = applied_preset != preset || active != last_active || jitter_on != last_jitter;
const int upscaler = settings.upscaler.load();
const bool changed = applied_preset != preset || active != last_active ||
jitter_on != last_jitter || applied_upscaler != upscaler;
if (applied_upscaler != upscaler) fsr4_failed = false; // retry after a menu change
// Dynamic scene resolution scaling (live preset switching) works on all GPUs.
// On GPUs without D32S8 blit support, UI depth is cleared instead of copied from scene.
if (!scaled_session) {
@@ -159,6 +169,7 @@ bool TemporalUpscaler::OnFrameStart() {
if (changed || !dispatched_last_frame) reset = true;
if (changed) jitter_index = 0;
applied_preset = preset;
applied_upscaler = upscaler;
last_active = active;
last_jitter = jitter_on;
dispatched_last_frame = false;
@@ -200,11 +211,13 @@ void TemporalUpscaler::OnDispatch(u64 cs_hash) {
}
bool TemporalUpscaler::EnsureResources(u32 w, u32 h, u32 ow, u32 oh, bool hdr) {
if (context && w == width && h == height && ow == out_width && oh == out_height &&
hdr == context_hdr) {
const bool use_fsr4 = UseFsr4();
if (resources_ready && w == width && h == height && ow == out_width && oh == out_height &&
hdr == context_hdr && use_fsr4 == resources_fsr4) {
return true;
}
const auto device = instance.GetDevice();
resources_ready = false;
if (context) {
scheduler.Finish();
ffxVkPortableUpscaleContextDestroy(context);
@@ -237,15 +250,18 @@ bool TemporalUpscaler::EnsureResources(u32 w, u32 h, u32 ow, u32 oh, bool hdr) {
: 0;
create_info.maxRenderSize = {w, h};
create_info.maxOutputSize = {ow, oh};
if (const u64 issues = ffxVkPortableValidateUpscaleCreateInfo(&create_info)) {
PrintIssues("create info", issues);
return false;
}
if (ffxVkPortableUpscaleContextCreate(&device_info, &create_info, &context) !=
FFX_VK_PORTABLE_OK) {
std::printf("Upscaler: FSR 3 context creation failed\n");
context = nullptr;
return false;
// FSR 4 has its own model context (vk_fsr4); the images below are shared.
if (!use_fsr4) {
if (const u64 issues = ffxVkPortableValidateUpscaleCreateInfo(&create_info)) {
PrintIssues("create info", issues);
return false;
}
if (ffxVkPortableUpscaleContextCreate(&device_info, &create_info, &context) !=
FFX_VK_PORTABLE_OK) {
std::printf("Upscaler: FSR 3 context creation failed\n");
context = nullptr;
return false;
}
}
const auto allocator = instance.GetAllocator();
@@ -321,6 +337,12 @@ bool TemporalUpscaler::EnsureResources(u32 w, u32 h, u32 ow, u32 oh, bool hdr) {
CreatePipelines();
opaque_valid = false;
reset = true;
resources_ready = true;
resources_fsr4 = use_fsr4;
if (use_fsr4) {
std::printf("Upscaler: FSR 4 inputs %ux%u -> %ux%u\n", w, h, ow, oh);
return true;
}
FfxVkPortableMemoryUsage usage{};
usage.structSize = sizeof(usage);
ffxVkPortableUpscaleContextGetMemoryUsage(context, &usage);
@@ -370,7 +392,31 @@ void TemporalUpscaler::CreatePipelines() {
device.destroyShaderModule(module);
return pipeline;
};
storage_layout(3, merge_desc_layout, merge_pipeline_layout, sizeof(u32));
{
// Merge: three storage images, then the motion and object motion images (debug).
std::array<vk::DescriptorSetLayoutBinding, 5> bindings{};
for (u32 i = 0; i < bindings.size(); ++i) {
bindings[i] = {.binding = i,
.descriptorType = i < 3 ? vk::DescriptorType::eStorageImage
: vk::DescriptorType::eSampledImage,
.descriptorCount = 1,
.stageFlags = vk::ShaderStageFlagBits::eCompute};
}
merge_desc_layout = Check(device.createDescriptorSetLayoutUnique({
.flags = vk::DescriptorSetLayoutCreateFlagBits::ePushDescriptorKHR,
.bindingCount = u32(bindings.size()),
.pBindings = bindings.data(),
}));
const vk::PushConstantRange push{.stageFlags = vk::ShaderStageFlagBits::eCompute,
.offset = 0,
.size = sizeof(u32)};
merge_pipeline_layout = Check(device.createPipelineLayoutUnique({
.setLayoutCount = 1,
.pSetLayouts = &*merge_desc_layout,
.pushConstantRangeCount = 1,
.pPushConstantRanges = &push,
}));
}
merge_pipeline = compute(UPSCALE_MERGE_COMP, *merge_pipeline_layout);
storage_layout(3, reactive_desc_layout, reactive_pipeline_layout, 3 * sizeof(float));
reactive_pipeline = compute(UPSCALE_REACTIVE_COMP, *reactive_pipeline_layout);
@@ -384,68 +430,67 @@ void TemporalUpscaler::OnBlendedSceneDraw() {
}
snapshot_taken = true;
auto& color = texture_cache.GetImage(scene_color);
const bool reduced = !Scaled() && scene_targets.Reduced() && scene_targets.Eligible(color);
const u32 w = reduced ? scene_targets.Size().width : color.info.size.width;
const u32 h = reduced ? scene_targets.Size().height : color.info.size.height;
// With a live preset the scene is in the reduced proxy: the mask and the FSR context
// must use that size, or each frame would recreate the context (native <-> reduced).
const bool reduced = ReducedScene(color);
const u32 ow = color.info.size.width, oh = color.info.size.height;
const u32 w = reduced ? scene_targets.Size().width : ow;
const u32 h = reduced ? scene_targets.Size().height : oh;
if (color.info.pixel_format != vk::Format::eR16G16B16A16Sfloat ||
!(color.usage_flags & vk::ImageUsageFlagBits::eTransferSrc)) {
return;
}
if (!EnsureResources(w, h, Scaled() ? target_width : color.info.size.width,
Scaled() ? target_height : color.info.size.height, !Scaled())) {
if (!EnsureResources(w, h, Scaled() ? target_width : ow,
Scaled() ? target_height : oh, !Scaled())) {
failed = true;
return;
}
scheduler.EndRendering();
runtime.Transit(&color, vk::ImageLayout::eTransferSrcOptimal,
vk::PipelineStageFlagBits2::eTransfer, vk::AccessFlagBits2::eTransferRead);
runtime.FlushBarriers();
vk::Image snapshot_source = color.GetImage();
vk::ImageLayout snapshot_layout = vk::ImageLayout::eTransferSrcOptimal;
vk::Image source = color.GetImage();
vk::ImageLayout source_layout = vk::ImageLayout::eTransferSrcOptimal;
if (reduced) {
VideoCore::ImageViewInfo vi;
vi.format = color.info.pixel_format;
snapshot_source = scene_targets.Read(scene_color, vi).image;
snapshot_layout = vk::ImageLayout::eGeneral;
VideoCore::ImageViewInfo ci;
ci.format = color.info.pixel_format;
source = scene_targets.Read(scene_color, ci, vk::PipelineStageFlagBits2::eTransfer,
vk::AccessFlagBits2::eTransferRead).image;
source_layout = vk::ImageLayout::eGeneral;
} else {
runtime.Transit(&color, vk::ImageLayout::eTransferSrcOptimal,
vk::PipelineStageFlagBits2::eTransfer, vk::AccessFlagBits2::eTransferRead);
runtime.FlushBarriers();
}
const auto cmdbuf = scheduler.CommandBuffer();
if (reduced) {
const vk::MemoryBarrier2 b{
.srcStageMask = vk::PipelineStageFlagBits2::eAllCommands,
.srcAccessMask = vk::AccessFlagBits2::eMemoryWrite,
.dstStageMask = vk::PipelineStageFlagBits2::eTransfer,
.dstAccessMask = vk::AccessFlagBits2::eTransferRead};
cmdbuf.pipelineBarrier2({.memoryBarrierCount = 1, .pMemoryBarriers = &b});
}
const auto to_general = [&](vk::PipelineStageFlags2 src_stage, vk::AccessFlags2 src_access,
vk::ImageLayout old_layout, vk::PipelineStageFlags2 dst_stage,
vk::AccessFlags2 dst_access) {
const vk::ImageMemoryBarrier2 barrier{
.srcStageMask = src_stage,
.srcAccessMask = src_access,
.dstStageMask = dst_stage,
.dstAccessMask = dst_access,
.oldLayout = old_layout,
.newLayout = vk::ImageLayout::eGeneral,
.image = vk::Image(opaque_image),
.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1},
};
cmdbuf.pipelineBarrier2({.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &barrier});
};
// The previous frame's mask pass read it: wait for that before overwriting.
to_general(vk::PipelineStageFlagBits2::eComputeShader, vk::AccessFlagBits2::eNone,
vk::ImageLayout::eUndefined, vk::PipelineStageFlagBits2::eTransfer,
vk::AccessFlagBits2::eTransferWrite);
const vk::Image opaque = vk::Image(opaque_image);
const vk::ImageCopy region{
.srcSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1},
.dstSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1},
.extent = {w, h, 1},
};
cmdbuf.copyImage(snapshot_source, snapshot_layout,
vk::Image(opaque_image), vk::ImageLayout::eGeneral, region);
to_general(vk::PipelineStageFlagBits2::eTransfer, vk::AccessFlagBits2::eTransferWrite,
vk::ImageLayout::eGeneral, vk::PipelineStageFlagBits2::eComputeShader,
vk::AccessFlagBits2::eShaderRead);
// On the recording thread: direct recording here would move the rest of the scene there.
scheduler.Record([opaque, source, source_layout, region](vk::CommandBuffer cmdbuf) {
const auto to_general = [&](vk::PipelineStageFlags2 src_stage, vk::AccessFlags2 src_access,
vk::ImageLayout old_layout, vk::PipelineStageFlags2 dst_stage,
vk::AccessFlags2 dst_access) {
const vk::ImageMemoryBarrier2 barrier{
.srcStageMask = src_stage,
.srcAccessMask = src_access,
.dstStageMask = dst_stage,
.dstAccessMask = dst_access,
.oldLayout = old_layout,
.newLayout = vk::ImageLayout::eGeneral,
.image = opaque,
.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1},
};
cmdbuf.pipelineBarrier2({.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &barrier});
};
// The previous frame's mask pass read it: wait for that before overwriting.
to_general(vk::PipelineStageFlagBits2::eComputeShader, vk::AccessFlagBits2::eNone,
vk::ImageLayout::eUndefined, vk::PipelineStageFlagBits2::eTransfer,
vk::AccessFlagBits2::eTransferWrite);
cmdbuf.copyImage(source, source_layout, opaque, vk::ImageLayout::eGeneral, region);
to_general(vk::PipelineStageFlagBits2::eTransfer, vk::AccessFlagBits2::eTransferWrite,
vk::ImageLayout::eGeneral, vk::PipelineStageFlagBits2::eComputeShader,
vk::AccessFlagBits2::eShaderRead);
});
opaque_valid = true;
}
@@ -454,8 +499,18 @@ void TemporalUpscaler::OnSceneComposite() {
return;
}
auto& color = texture_cache.GetImage(scene_color);
if ((!scene_targets.Reduced() && (color.info.size.width != width || color.info.size.height != height)) ||
!(color.usage_flags & vk::ImageUsageFlagBits::eStorage)) {
const bool reduced = ReducedScene(color);
const u32 w = reduced ? scene_targets.Size().width : color.info.size.width;
const u32 h = reduced ? scene_targets.Size().height : color.info.size.height;
if (w != width || h != height || !(color.usage_flags & vk::ImageUsageFlagBits::eStorage)) {
return;
}
scheduler.EndRendering();
if (reduced) {
VideoCore::ImageViewInfo ci;
ci.format = color.info.pixel_format;
const auto proxy = scene_targets.Read(scene_color, ci);
mask_ready = RecordReactive(proxy.view);
return;
}
const auto device = instance.GetDevice();
@@ -465,77 +520,77 @@ void TemporalUpscaler::OnSceneComposite() {
.format = vk::Format::eR16G16B16A16Sfloat,
.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1},
}));
scheduler.EndRendering();
runtime.Transit(&color, vk::ImageLayout::eGeneral, vk::PipelineStageFlagBits2::eComputeShader,
vk::AccessFlagBits2::eShaderRead);
runtime.FlushBarriers();
vk::ImageView mask_color_view = color_view;
if (!Scaled() && scene_targets.Reduced() && scene_targets.Eligible(color)) {
VideoCore::ImageViewInfo vi;
vi.format = color.info.pixel_format;
mask_color_view = scene_targets.Read(scene_color, vi).view;
}
mask_ready = RecordReactive(scheduler.CommandBuffer(), mask_color_view);
mask_ready = RecordReactive(color_view);
scheduler.DeferOperation([device, color_view] { device.destroyImageView(color_view); });
}
bool TemporalUpscaler::RecordReactive(vk::CommandBuffer cmdbuf, vk::ImageView color_view) {
bool TemporalUpscaler::RecordReactive(vk::ImageView color_view) {
if (!opaque_valid || !ReactiveOn()) {
return false;
}
const vk::Image reactive = vk::Image(reactive_image);
const vk::ImageView opaque = *opaque_view, mask = *reactive_view;
const vk::Pipeline pipeline = *reactive_pipeline;
const vk::PipelineLayout layout = *reactive_pipeline_layout;
const u32 w = width, h = height;
// Relative color change times the scale (default 1), zero below the threshold (0.2), at
// most the maximum (0.9, never fully reactive) — the defaults of AMD's mask generator.
const auto& settings = BbSettings::Get();
const std::array<float, 3> params{settings.reactive_scale, settings.reactive_max,
settings.reactive_threshold};
const vk::ImageMemoryBarrier2 to_write{
.srcStageMask = vk::PipelineStageFlagBits2::eAllCommands,
.srcAccessMask = vk::AccessFlagBits2::eNone,
.dstStageMask = vk::PipelineStageFlagBits2::eComputeShader,
.dstAccessMask = vk::AccessFlagBits2::eShaderWrite,
.oldLayout = vk::ImageLayout::eUndefined,
.newLayout = vk::ImageLayout::eGeneral,
.image = vk::Image(reactive_image),
.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1},
};
cmdbuf.pipelineBarrier2({.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &to_write});
const vk::DescriptorImageInfo opaque_info{.imageView = *opaque_view,
.imageLayout = vk::ImageLayout::eGeneral};
const vk::DescriptorImageInfo scene_info{.imageView = color_view,
.imageLayout = vk::ImageLayout::eGeneral};
const vk::DescriptorImageInfo mask_info{.imageView = *reactive_view,
.imageLayout = vk::ImageLayout::eGeneral};
const std::array<vk::WriteDescriptorSet, 3> writes = {{
{.dstBinding = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eStorageImage,
.pImageInfo = &opaque_info},
{.dstBinding = 1,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eStorageImage,
.pImageInfo = &scene_info},
{.dstBinding = 2,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eStorageImage,
.pImageInfo = &mask_info},
}};
cmdbuf.bindPipeline(vk::PipelineBindPoint::eCompute, *reactive_pipeline);
cmdbuf.pushDescriptorSetKHR(vk::PipelineBindPoint::eCompute, *reactive_pipeline_layout, 0,
writes);
cmdbuf.pushConstants(*reactive_pipeline_layout, vk::ShaderStageFlagBits::eCompute, 0,
sizeof(params), params.data());
cmdbuf.dispatch((width + 7) / 8, (height + 7) / 8, 1);
const vk::ImageMemoryBarrier2 to_read{
.srcStageMask = vk::PipelineStageFlagBits2::eComputeShader,
.srcAccessMask = vk::AccessFlagBits2::eShaderWrite,
.dstStageMask = vk::PipelineStageFlagBits2::eAllCommands,
.dstAccessMask = vk::AccessFlagBits2::eShaderRead,
.oldLayout = vk::ImageLayout::eGeneral,
.newLayout = vk::ImageLayout::eGeneral,
.image = vk::Image(reactive_image),
.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1},
};
cmdbuf.pipelineBarrier2({.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &to_read});
scheduler.Record([=](vk::CommandBuffer cmdbuf) {
const vk::ImageMemoryBarrier2 to_write{
.srcStageMask = vk::PipelineStageFlagBits2::eAllCommands,
.srcAccessMask = vk::AccessFlagBits2::eNone,
.dstStageMask = vk::PipelineStageFlagBits2::eComputeShader,
.dstAccessMask = vk::AccessFlagBits2::eShaderWrite,
.oldLayout = vk::ImageLayout::eUndefined,
.newLayout = vk::ImageLayout::eGeneral,
.image = reactive,
.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1},
};
cmdbuf.pipelineBarrier2({.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &to_write});
const vk::DescriptorImageInfo opaque_info{.imageView = opaque,
.imageLayout = vk::ImageLayout::eGeneral};
const vk::DescriptorImageInfo scene_info{.imageView = color_view,
.imageLayout = vk::ImageLayout::eGeneral};
const vk::DescriptorImageInfo mask_info{.imageView = mask,
.imageLayout = vk::ImageLayout::eGeneral};
const std::array<vk::WriteDescriptorSet, 3> writes = {{
{.dstBinding = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eStorageImage,
.pImageInfo = &opaque_info},
{.dstBinding = 1,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eStorageImage,
.pImageInfo = &scene_info},
{.dstBinding = 2,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eStorageImage,
.pImageInfo = &mask_info},
}};
cmdbuf.bindPipeline(vk::PipelineBindPoint::eCompute, pipeline);
cmdbuf.pushDescriptorSetKHR(vk::PipelineBindPoint::eCompute, layout, 0,
writes);
cmdbuf.pushConstants(layout, vk::ShaderStageFlagBits::eCompute, 0,
sizeof(params), params.data());
cmdbuf.dispatch((w + 7) / 8, (h + 7) / 8, 1);
const vk::ImageMemoryBarrier2 to_read{
.srcStageMask = vk::PipelineStageFlagBits2::eComputeShader,
.srcAccessMask = vk::AccessFlagBits2::eShaderWrite,
.dstStageMask = vk::PipelineStageFlagBits2::eAllCommands,
.dstAccessMask = vk::AccessFlagBits2::eShaderRead,
.oldLayout = vk::ImageLayout::eGeneral,
.newLayout = vk::ImageLayout::eGeneral,
.image = reactive,
.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1},
};
cmdbuf.pipelineBarrier2({.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &to_read});
});
return true;
}
@@ -543,7 +598,7 @@ void TemporalUpscaler::Run() {
auto& color = texture_cache.GetImage(scene_color);
auto& depth = texture_cache.GetImage(camera_motion.Depth());
const u32 ow = color.info.size.width, oh = color.info.size.height;
const bool reduced = scene_targets.Reduced() && scene_targets.Eligible(color) && scene_targets.Eligible(depth);
const bool reduced = ReducedScene(color) && scene_targets.Eligible(depth);
const u32 w = reduced ? scene_targets.Size().width : ow;
const u32 h = reduced ? scene_targets.Size().height : oh;
if (color.info.pixel_format != vk::Format::eR16G16B16A16Sfloat ||
@@ -622,7 +677,7 @@ void TemporalUpscaler::Run() {
vk::PipelineStageFlagBits2::eComputeShader, vk::AccessFlagBits2::eShaderWrite,
vk::ImageLayout::eGeneral, all, vk::AccessFlagBits2::eShaderRead);
const bool has_reactive = mask_ready || RecordReactive(cmdbuf, input_color_view);
const bool has_reactive = mask_ready || RecordReactive(input_color_view);
const auto now = std::chrono::steady_clock::now();
float frame_ms = std::chrono::duration<float, std::milli>(now - last_frame).count();
@@ -631,70 +686,79 @@ void TemporalUpscaler::Run() {
}
last_frame = now;
FfxVkPortableUpscaleDispatchInfo info{};
info.structSize = sizeof(info);
info.commandBuffer = cmdbuf;
info.color = Describe(input_color, color.info.pixel_format, w, h,
color.usage_flags, vk::ImageAspectFlagBits::eColor,
FFX_VK_PORTABLE_RESOURCE_STATE_GENERIC_READ);
info.depth = Describe(input_depth, depth_format, w, h, depth.usage_flags,
vk::ImageAspectFlagBits::eDepth,
FFX_VK_PORTABLE_RESOURCE_STATE_GENERIC_READ);
info.motionVectors = Describe(vk::Image(motion_image), vk::Format::eR16G16Sfloat, w, h,
vk::ImageUsageFlagBits::eStorage | vk::ImageUsageFlagBits::eSampled,
vk::ImageAspectFlagBits::eColor,
FFX_VK_PORTABLE_RESOURCE_STATE_GENERIC_READ);
info.output = Describe(vk::Image(output_image), vk::Format::eR16G16B16A16Sfloat, ow, oh,
vk::ImageUsageFlagBits::eStorage | vk::ImageUsageFlagBits::eSampled |
vk::ImageUsageFlagBits::eTransferSrc,
vk::ImageAspectFlagBits::eColor,
FFX_VK_PORTABLE_RESOURCE_STATE_UNORDERED_ACCESS);
// Optional inputs, absent: a described but null image.
info.exposure.structSize = sizeof(info.exposure);
info.reactiveMask.structSize = sizeof(info.reactiveMask);
if (has_reactive) {
info.reactiveMask = Describe(vk::Image(reactive_image), vk::Format::eR8Unorm, w, h,
vk::ImageUsageFlagBits::eStorage |
vk::ImageUsageFlagBits::eSampled,
vk::ImageAspectFlagBits::eColor,
FFX_VK_PORTABLE_RESOURCE_STATE_GENERIC_READ);
}
info.transparencyAndCompositionMask.structSize = sizeof(info.transparencyAndCompositionMask);
// Toggle 1 << 26 (tests): the opposite sign convention for FSR.
const float sign = BbToggle::Disabled(1u << 26) ? -1.0f : 1.0f;
info.jitterOffset = {sign * jitter[0], sign * jitter[1]};
info.motionVectorScale = {1.0f, 1.0f};
info.renderSize = {w, h};
info.outputSize = {ow, oh};
info.frameTimeMilliseconds = frame_ms;
info.preExposure = 1.0f;
info.cameraNear = camera_motion.Near();
info.cameraFar = 3000.0f;
info.cameraVerticalFovRadians = camera_motion.VerticalFov();
info.viewSpaceToMeters = 1.0f;
// RCAS strength 0..1 (menu, BB_FSR_SHARPNESS); jitter at 1:1 softens the image slightly.
info.sharpness = BbSettings::Get().sharpness;
info.enableSharpening = BbSettings::Get().sharpen ? VK_TRUE : VK_FALSE;
info.reset = reset ? VK_TRUE : VK_FALSE;
info.frameId = frame_id++;
FfxVkPortableUpscaleCreateInfo create_info{};
create_info.structSize = sizeof(create_info);
create_info.flags = FFX_VK_PORTABLE_CONTEXT_HDR_COLOR_INPUT | FFX_VK_PORTABLE_CONTEXT_AUTO_EXPOSURE;
create_info.maxRenderSize = {w, h};
create_info.maxOutputSize = {ow, oh};
if (const u64 issues = ffxVkPortableValidateUpscaleDispatchInfo(&create_info, &info)) {
static bool printed = false;
if (!printed) {
printed = true;
PrintIssues("dispatch", issues);
}
failed = true;
} else if (ffxVkPortableUpscaleContextRecordDispatch(context, &info) !=
FFX_VK_PORTABLE_OK) {
std::printf("Upscaler: FSR 3 dispatch failed\n");
failed = true;
bool dispatched = false;
if (UseFsr4()) {
dispatched = RecordFsr4(cmdbuf, {input_color, input_color_view, w, h},
{input_depth, input_depth_view, w, h}, w, h, ow, oh, frame_ms);
} else {
FfxVkPortableUpscaleDispatchInfo info{};
info.structSize = sizeof(info);
info.commandBuffer = cmdbuf;
info.color = Describe(input_color, color.info.pixel_format, w, h,
color.usage_flags, vk::ImageAspectFlagBits::eColor,
FFX_VK_PORTABLE_RESOURCE_STATE_GENERIC_READ);
info.depth = Describe(input_depth, depth_format, w, h, depth.usage_flags,
vk::ImageAspectFlagBits::eDepth,
FFX_VK_PORTABLE_RESOURCE_STATE_GENERIC_READ);
info.motionVectors = Describe(vk::Image(motion_image), vk::Format::eR16G16Sfloat, w, h,
vk::ImageUsageFlagBits::eStorage | vk::ImageUsageFlagBits::eSampled,
vk::ImageAspectFlagBits::eColor,
FFX_VK_PORTABLE_RESOURCE_STATE_GENERIC_READ);
info.output = Describe(vk::Image(output_image), vk::Format::eR16G16B16A16Sfloat, ow, oh,
vk::ImageUsageFlagBits::eStorage | vk::ImageUsageFlagBits::eSampled |
vk::ImageUsageFlagBits::eTransferSrc,
vk::ImageAspectFlagBits::eColor,
FFX_VK_PORTABLE_RESOURCE_STATE_UNORDERED_ACCESS);
// Optional inputs, absent: a described but null image.
info.exposure.structSize = sizeof(info.exposure);
info.reactiveMask.structSize = sizeof(info.reactiveMask);
if (has_reactive) {
info.reactiveMask = Describe(vk::Image(reactive_image), vk::Format::eR8Unorm, w, h,
vk::ImageUsageFlagBits::eStorage |
vk::ImageUsageFlagBits::eSampled,
vk::ImageAspectFlagBits::eColor,
FFX_VK_PORTABLE_RESOURCE_STATE_GENERIC_READ);
}
info.transparencyAndCompositionMask.structSize = sizeof(info.transparencyAndCompositionMask);
// Toggle 1 << 26 (tests): the opposite sign convention for FSR.
const float sign = BbToggle::Disabled(1u << 26) ? -1.0f : 1.0f;
info.jitterOffset = {sign * jitter[0], sign * jitter[1]};
info.motionVectorScale = {1.0f, 1.0f};
info.renderSize = {w, h};
info.outputSize = {ow, oh};
info.frameTimeMilliseconds = frame_ms;
info.preExposure = 1.0f;
info.cameraNear = camera_motion.Near();
info.cameraFar = 3000.0f;
info.cameraVerticalFovRadians = camera_motion.VerticalFov();
info.viewSpaceToMeters = 1.0f;
// RCAS strength 0..1 (menu, BB_FSR_SHARPNESS); jitter at 1:1 softens the image slightly.
info.sharpness = BbSettings::Get().sharpness;
info.enableSharpening = BbSettings::Get().sharpen ? VK_TRUE : VK_FALSE;
info.reset = reset ? VK_TRUE : VK_FALSE;
info.frameId = frame_id++;
FfxVkPortableUpscaleCreateInfo create_info{};
create_info.structSize = sizeof(create_info);
create_info.flags = FFX_VK_PORTABLE_CONTEXT_HDR_COLOR_INPUT | FFX_VK_PORTABLE_CONTEXT_AUTO_EXPOSURE;
create_info.maxRenderSize = {w, h};
create_info.maxOutputSize = {ow, oh};
if (const u64 issues = ffxVkPortableValidateUpscaleDispatchInfo(&create_info, &info)) {
static bool printed = false;
if (!printed) {
printed = true;
PrintIssues("dispatch", issues);
}
failed = true;
} else if (ffxVkPortableUpscaleContextRecordDispatch(context, &info) !=
FFX_VK_PORTABLE_OK) {
std::printf("Upscaler: FSR 3 dispatch failed\n");
failed = true;
} else {
dispatched = true;
}
}
if (dispatched) {
reset = false;
dispatched_last_frame = true;
// The result replaces the scene color's RGB (its alpha carries data for the post).
@@ -710,7 +774,14 @@ void TemporalUpscaler::Run() {
.imageLayout = vk::ImageLayout::eGeneral};
const vk::DescriptorImageInfo mask_info{.imageView = *reactive_view,
.imageLayout = vk::ImageLayout::eGeneral};
const std::array<vk::WriteDescriptorSet, 3> writes = {{
// Sampled for the debug view only; the object image falls back to the motion image.
const vk::ImageView objects = camera_motion.ObjectMotionView();
const vk::DescriptorImageInfo motion_info{.imageView = *motion_view,
.imageLayout = vk::ImageLayout::eGeneral};
const vk::DescriptorImageInfo objects_info{
.imageView = objects ? objects : *motion_view,
.imageLayout = vk::ImageLayout::eGeneral};
const std::array<vk::WriteDescriptorSet, 5> writes = {{
{.dstBinding = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eStorageImage,
@@ -723,12 +794,23 @@ void TemporalUpscaler::Run() {
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eStorageImage,
.pImageInfo = &mask_info},
{.dstBinding = 3,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eSampledImage,
.pImageInfo = &motion_info},
{.dstBinding = 4,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eSampledImage,
.pImageInfo = &objects_info},
}};
// Debug (menu or toggle 1 << 28): the reactive mask in red over a darkened frame.
const u32 mode = has_reactive && (BbSettings::Get().debug_view == BbSettings::DebugReactive ||
// Debug (menu or toggle 1 << 28): the reactive mask in red over a darkened frame;
// or the motion vectors (menu).
const int debug_view = BbSettings::Get().debug_view;
const u32 mode = has_reactive && (debug_view == BbSettings::DebugReactive ||
BbToggle::Disabled(1u << 28))
? 1
: 0;
: debug_view == BbSettings::DebugMotion ? 2
: 0;
cmdbuf.bindPipeline(vk::PipelineBindPoint::eCompute, *merge_pipeline);
cmdbuf.pushDescriptorSetKHR(vk::PipelineBindPoint::eCompute, *merge_pipeline_layout, 0,
writes);
@@ -1182,8 +1264,9 @@ bool TemporalUpscaler::RedirectColor(VideoCore::ImageId color,
.image = vk::Image(display.image),
.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1},
};
scheduler.CommandBuffer().pipelineBarrier2(
{.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &b});
scheduler.Record([b](vk::CommandBuffer cmd) {
cmd.pipelineBarrier2({.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &b});
});
display.valid = true;
target = {Mirror(vk::Image(display.image), display.views, view_info.format, {}),
vk::ImageLayout::eGeneral, ui_width, ui_height};
@@ -1217,8 +1300,9 @@ bool TemporalUpscaler::RedirectSampled(VideoCore::ImageId image,
.image = vk::Image(ui_image),
.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1},
};
scheduler.CommandBuffer().pipelineBarrier2(
{.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &b});
scheduler.Record([b](vk::CommandBuffer cmd) {
cmd.pipelineBarrier2({.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &b});
});
}
view = Mirror(vk::Image(ui_image), ui_views, info.format, info.mapping);
layout = vk::ImageLayout::eGeneral;
@@ -1236,3 +1320,59 @@ bool TemporalUpscaler::DisplayOverride(VAddr address, Display& display) {
}
} // namespace Vulkan
namespace Vulkan {
bool TemporalUpscaler::UseFsr4() const {
return BbSettings::Get().upscaler == BbSettings::UpscalerFsr4 && !scaled_session &&
!fsr4_failed;
}
bool TemporalUpscaler::RecordFsr4(vk::CommandBuffer cmdbuf, Fsr4Upscaler::Image color,
Fsr4Upscaler::Image depth, u32 w, u32 h, u32 ow, u32 oh,
float frame_ms) {
const auto& settings = BbSettings::Get();
// Same jitter convention as FSR 3; the menu (or toggle 1 << 26) flips it for tests.
const float sign =
BbToggle::Disabled(1u << 26) != settings.fsr4_invert_jitter.load() ? -1.0f : 1.0f;
const bool ok = fsr4->Record({
.cmdbuf = cmdbuf,
.color = color,
.depth = depth,
.motion = {vk::Image(motion_image), *motion_view, w, h},
.output = {vk::Image(output_image), *output_view, ow, oh},
.render_width = w,
.render_height = h,
.preset = applied_preset,
.jitter = {sign * jitter[0], sign * jitter[1]},
.frame_ms = frame_ms,
.near_plane = camera_motion.Near(),
.far_plane = 3000.0f,
.vertical_fov = camera_motion.VerticalFov(),
.sharpness = settings.sharpness,
.sharpen = settings.sharpen,
.reset = reset,
.auto_exposure = settings.fsr4_auto_exposure,
});
// The menu shows the reason; it outlives this frame (FSR 4 keeps its last message).
static std::string shown;
const char* problem = fsr4->Problem();
if (!problem) {
BbSettings::Get().fsr4_problem = nullptr;
} else if (shown != problem) {
shown = problem;
static std::array<std::string, 8> kept;
static u32 next = 0;
kept[next] = shown;
BbSettings::Get().fsr4_problem = kept[next].c_str();
next = (next + 1) % kept.size();
}
if (!ok && fsr4->Fatal()) {
std::printf("Upscaler: falling back to FSR 3.1\n");
fsr4_failed = true; // EnsureResources creates the FSR 3 context next frame
reset = true;
}
return ok;
}
} // namespace Vulkan
@@ -4,10 +4,10 @@
// post-processing combine pass, with the scene depth and camera motion vectors, and writes the
// result back so the game's own post, tonemap and UI continue unchanged (Native AA preset).
//
// Scaled presets: the game renders at a lower resolution (resolution patch from patches.py) and
// FSR upscales its finished, tonemapped frame right before the UI. The UI passes and the pass
// copying the frame to the display buffer are redirected to output-size images, and the
// presenter shows those instead of the guest display buffer: the UI stays sharp.
// Normal scaled presets keep guest targets at 1920x1080, rasterize the scene into
// reduced host targets, and upscale HDR color before post and UI. BB_RENDER_RES
// retains the older patched-resolution path, which upscales the tonemapped frame
// and redirects UI/display passes to output-size images.
#pragma once
@@ -20,6 +20,7 @@
#include "common/types.h"
#include "video_core/renderer_vulkan/vk_common.h"
#include "video_core/renderer_vulkan/vk_fsr4.h"
#include "video_core/texture_cache/image.h"
struct FfxVkPortableUpscaleContext;
@@ -34,12 +35,13 @@ class Instance;
class Scheduler;
class Runtime;
class CameraMotion;
class SceneTargets;
class TemporalUpscaler {
public:
TemporalUpscaler(const Instance& instance, Scheduler& scheduler,
VideoCore::TextureCache& texture_cache, Runtime& runtime,
CameraMotion& camera_motion);
CameraMotion& camera_motion, SceneTargets& scene_targets);
~TemporalUpscaler();
[[nodiscard]] bool Enabled() const noexcept {
@@ -61,7 +63,9 @@ public:
void OnDispatch(u64 cs_hash);
/// Start of a frame in the command stream (display pass).
void OnFrameStart();
bool OnFrameStart();
bool RasterScaling() const;
/// This frame's sub-pixel jitter in pixels (screen x right, y down); zero when off and after
/// the upscale (post and UI are not jittered).
@@ -71,8 +75,10 @@ public:
// Scaled presets.
/// Before every draw: the first UI pass's vertex shader starts the upscale.
void OnDraw(u64 vs_hash);
/// After identifying this draw's targets, before drawing any UI pixels. Menus use
/// spatial background copy and native UI, without needing scene depth or FSR history.
void OnDraw(u64 vs_hash, VideoCore::ImageId color, VideoCore::ImageId depth,
bool native_viewport);
/// A pass's first color target: the last render-size RGBA8 target before the UI is the
/// game's finished frame.
@@ -82,6 +88,7 @@ public:
vk::ImageView view;
vk::ImageLayout layout;
u32 width, height;
bool native_ui = false;
};
/// Render target redirection (UI passes, display pass): false when not redirected.
/// `view` is the game's view of the image: redirected views mirror its format (sRGB) and,
@@ -104,21 +111,37 @@ public:
private:
void Run();
void RunScaled();
void RunUiOnly(VideoCore::ImageId color, VideoCore::ImageId depth);
void EnsureUiResources(u32 width, u32 height, vk::Format color, vk::Format depth);
void PrepareUiDepth(VideoCore::ImageId depth);
/// Render size below the scaled-preset output size (the resolution patch is on).
[[nodiscard]] bool Scaled() const;
// The scene color is drawn into a reduced SceneTargets proxy (live presets).
[[nodiscard]] bool ReducedScene(const VideoCore::Image& color) const;
/// Available and switched on (menu setting, toggle 1 << 24).
[[nodiscard]] bool Active() const;
[[nodiscard]] bool ReactiveOn() const;
bool EnsureResources(u32 width, u32 height, u32 out_width, u32 out_height, bool hdr);
void CreatePipelines();
/// Records the reactive mask pass; false when there is no snapshot this frame.
bool RecordReactive(vk::CommandBuffer cmdbuf, vk::ImageView color_view);
bool RecordReactive(vk::ImageView color_view);
/// FSR 4 is selected, possible in this session (not BB_RENDER_RES) and has not failed.
[[nodiscard]] bool UseFsr4() const;
/// Records FSR 4 into output_image; on a permanent failure FSR 3 takes over.
bool RecordFsr4(vk::CommandBuffer cmdbuf, Fsr4Upscaler::Image color, Fsr4Upscaler::Image depth,
u32 w, u32 h, u32 ow, u32 oh, float frame_ms);
const Instance& instance;
Scheduler& scheduler;
VideoCore::TextureCache& texture_cache;
Runtime& runtime;
CameraMotion& camera_motion;
SceneTargets& scene_targets;
int applied_preset = -1;
int applied_upscaler = -1;
bool dispatched_last_frame = false;
bool last_active = false, last_jitter = false;
bool enabled = false;
bool failed = false;
@@ -150,6 +173,10 @@ private:
vk::UniqueImageView ui_depth_view;
bool depth_blit = false;
bool scaled_session = false;
u32 ui_width = 0, ui_height = 0;
u32 render_width = 0, render_height = 0;
vk::Format ui_format = vk::Format::eUndefined;
vk::Format ui_depth_format = vk::Format::eUndefined;
/// Views of the port's images in the formats/swizzles the game's views use.
struct MirrorView {
vk::Format format;
@@ -163,11 +190,16 @@ private:
VideoCore::UniqueImage image;
std::vector<MirrorView> views;
vk::Format format{};
u32 width = 0, height = 0;
bool valid = false;
};
std::mutex display_mutex;
std::unordered_map<VAddr, DisplayImage> displays;
FfxVkPortableUpscaleContext* context = nullptr;
bool resources_ready = false; ///< images below match width/height/out size
bool resources_fsr4 = false; ///< made for FSR 4 (no FSR 3 context)
std::unique_ptr<Fsr4Upscaler> fsr4;
bool fsr4_failed = false;
VideoCore::UniqueImage motion_image;
VideoCore::UniqueImage output_image;
vk::UniqueImageView motion_view;
@@ -51,6 +51,9 @@ struct UniqueImage {
allocation{std::exchange(other.allocation, VK_NULL_HANDLE)},
image{std::exchange(other.image, VK_NULL_HANDLE)}, image_ci{std::move(other.image_ci)} {}
UniqueImage& operator=(UniqueImage&& other) {
if (this == &other) return *this;
Destroy();
device = other.device;
image = std::exchange(other.image, VK_NULL_HANDLE);
allocator = std::exchange(other.allocator, VK_NULL_HANDLE);
allocation = std::exchange(other.allocation, VK_NULL_HANDLE);
@@ -4,6 +4,7 @@
#pragma once
#include <atomic>
#include "bbport_toggles.h"
#include <condition_variable>
#include <mutex>
@@ -131,6 +132,23 @@ public:
/// Updates image contents if it was modified by CPU.
void UpdateImage(ImageId image_id) {
// bbport: a clean image already tracked and touched in this GC period needs nothing.
// Every texture binding comes here; the mutex (shared with the fault handlers of the
// guest threads) was ~3% of the GPU thread. Flags are read atomically: an invalidation
// racing with this check races the same way with the locked path.
if (!BbToggle::Disabled(BbToggle::UpdateImageFastPath)) {
const Image& image = slot_images[image_id];
const u32 flags = std::atomic_ref<const u32>(reinterpret_cast<const u32&>(image.flags))
.load(std::memory_order_acquire);
constexpr u32 Dirty = static_cast<u32>(ImageFlagBits::Dirty);
constexpr u32 Registered = static_cast<u32>(ImageFlagBits::Registered);
if ((flags & (Dirty | Registered)) == Registered &&
image.track_addr == image.info.guest_address &&
image.track_addr_end == image.info.guest_address + image.info.guest_size &&
image.lru_touched_tick == gc_tick) {
return;
}
}
std::scoped_lock lock{mutex};
Image& image = slot_images[image_id];
TrackImage(image_id);
@@ -160,6 +178,11 @@ public:
bool is_depth);
/// Retrieves the image with the specified id.
Image* TryGetImage(ImageId id, u64 uid) {
if (!slot_images.is_allocated(id)) return nullptr;
auto& image = slot_images[id];
return !uid || image.image_uid == uid ? &image : nullptr;
}
[[nodiscard]] Image& GetImage(ImageId id) {
auto& image = slot_images[id];
TouchImage(image);
+23
View File
@@ -13,6 +13,7 @@
#include <cstring>
#include <mutex>
#include <string>
#include <chrono>
#include <thread>
#include <vector>
#include <SDL3/SDL.h>
@@ -183,8 +184,30 @@ u32 BbDisplayRefreshHz() {
return hz;
}
#ifdef BB_PGO_GENERATE
extern "C" void __gcov_dump(void);
extern "C" void __gcov_reset(void);
// Instrumented build (BB_PGO=generate): the game often ends through _exit (watchdog, guest
// exit), which skips the profile write at exit. Write every 30 s and reset: the files sum the
// intervals.
static void StartProfileWriter() {
std::thread([] {
Common::SetCurrentThreadName("bb:pgo");
for (;;) {
std::this_thread::sleep_for(std::chrono::seconds(30));
__gcov_dump();
__gcov_reset();
std::printf("PGO: profile written\n");
}
}).detach();
}
#endif
extern "C" int bbgpu_init(const BbGpuConfig* config) {
BbSettings::Load();
#ifdef BB_PGO_GENERATE
StartProfileWriter();
#endif
g_sdk_version = config->sdk_version;
if (config->user_dir) setenv("BB_GPU_USER_DIR", config->user_dir, 0);
Core::Emulator::FillElfInfo(*config);
+4 -1
View File
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "bbport_copy.h"
#include "bbport_threads.h"
#include <algorithm>
#include <atomic>
@@ -23,7 +24,9 @@ thread_local bool in_copy_thread = false;
class Pool {
public:
Pool() {
unsigned count = std::clamp(std::thread::hardware_concurrency() / 4, 1u, 4u);
// Copies are on the critical path (fences wait for them): normal priority, a quarter
// of the hardware threads available to the process (Steam Deck 2).
unsigned count = std::clamp(BbThreads::Available() / 4, 1u, 4u);
if (const char* env = std::getenv("BB_COPY_THREADS")) {
count = static_cast<unsigned>(std::clamp(std::atoi(env), 0, 16));
}
+26 -3
View File
@@ -155,8 +155,8 @@ void Menu() {
frame_ms_avg);
ImGui::SeparatorText("Временной апскейлер");
static const char* upscalers[] = {"Выкл", "FSR 3.1"};
static const char* later[] = {"FSR 4", "DLSS", "XeSS"};
static const char* upscalers[] = {"Выкл", "FSR 3.1", "FSR 4 (INT8)"};
static const char* later[] = {"DLSS", "XeSS"};
int upscaler = s.upscaler;
if (ImGui::BeginCombo("Апскейлер", upscalers[upscaler])) {
for (int i = 0; i < BbSettings::UpscalerCount; ++i) {
@@ -173,6 +173,18 @@ void Menu() {
}
ImGui::EndCombo();
}
if (s.upscaler == BbSettings::UpscalerFsr4) {
if (const char* problem = s.fsr4_problem.load()) {
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "FSR 4 недоступен: %s", problem);
}
Hint("FSR 4 в режиме INT8 (модель v07 из исходников AMD FidelityFX SDK). Качество выше, "
"чем у FSR 3.1, но проход тяжелее. Смена пресета пересобирает модель (короткая "
"пауза). Ассеты: fetch_fsr4_assets.sh.");
Checkbox("FSR 4: авто-экспозиция", s.fsr4_auto_exposure);
Checkbox("FSR 4: обратный знак jitter", s.fsr4_invert_jitter);
Hint("Проверка при гостинге: сеть FSR 4 нормирует цвет по экспозиции и по ней решает, "
"когда отбросить прошлые кадры. Меняются сразу, без перезапуска.");
}
const bool upscaler_on = s.upscaler != BbSettings::UpscalerOff;
ImGui::BeginDisabled(!upscaler_on);
int preset = s.preset;
@@ -227,6 +239,14 @@ void Menu() {
Hint("Точные векторы для анимированных объектов: одежда и оружие меньше рассыпаются "
"при движении. Статичная сцена не получает дополнительный проход. "
"Изменение применяется после перезапуска игры.");
bool show_motion = s.debug_view == BbSettings::DebugMotion;
if (ImGui::Checkbox("Показать векторы движения (отладка)", &show_motion)) {
s.debug_view = show_motion ? BbSettings::DebugMotion : BbSettings::DebugNone;
}
Hint("Красный/зелёный: движение по горизонтали/вертикали (8 пикселей = полная яркость). "
"Синий: пиксель получил точный вектор объекта, а не только движение камеры. "
"Движущийся предмет без синего и без красного/зелёного апскейлер считает "
"неподвижным, отсюда шлейф.");
ImGui::EndDisabled(); // upscaler off
if (s.object_motion != s.startup_object_motion) {
@@ -266,7 +286,10 @@ void FpsCounter() {
ImGuiWindowFlags_NoFocusOnAppearing);
const auto& s = BbSettings::Get();
ImGui::Text("%.0f FPS %.1f мс %s", frame_ms_avg > 0.0f ? 1000.0f / frame_ms_avg : 0.0f,
frame_ms_avg, s.upscaler == BbSettings::UpscalerFsr3 ? "FSR 3.1" : "");
frame_ms_avg,
s.upscaler == BbSettings::UpscalerFsr3 ? "FSR 3.1"
: s.upscaler == BbSettings::UpscalerFsr4 ? "FSR 4"
: "");
ImGui::End();
}
+12 -4
View File
@@ -51,6 +51,10 @@ void Set(Values& v, const std::string& key, const std::string& value) {
v.debug_view = std::clamp(i, 0, DebugViewCount - 1);
} else if (key == "show_fps") {
v.show_fps = i != 0;
} else if (key == "fsr4_auto_exposure") {
v.fsr4_auto_exposure = i != 0;
} else if (key == "fsr4_invert_jitter") {
v.fsr4_invert_jitter = i != 0;
}
}
@@ -79,7 +83,10 @@ void Load() {
}
// Environment overrides (scripts, A/B tests).
if (const char* env = std::getenv("BB_UPSCALER")) {
v.upscaler = std::strcmp(env, "fsr3") == 0 ? UpscalerFsr3 : UpscalerOff;
v.upscaler = UpscalerOff;
for (int u = 0; u < UpscalerCount; ++u) {
if (std::strcmp(env, UpscalerName(u)) == 0) v.upscaler = u;
}
}
const std::pair<const char*, const char*> env_keys[] = {
{"BB_FSR_SHARPNESS", "sharpness"}, {"BB_JITTER", "jitter"},
@@ -108,12 +115,13 @@ void Save() {
"# bbport settings (in-game menu: Insert / L3+R3)\n"
"upscaler=%s\npreset=%d\nsharpen=%d\nsharpness=%.2f\njitter=%d\n"
"reactive=%d\nobject_motion=%d\nreactive_scale=%.2f\nreactive_threshold=%.2f\nreactive_max=%.2f\n"
"debug_view=%d\nshow_fps=%d\n",
"debug_view=%d\nshow_fps=%d\nfsr4_auto_exposure=%d\nfsr4_invert_jitter=%d\n",
UpscalerName(v.upscaler), v.preset.load(), int(v.sharpen.load()),
v.sharpness.load(), int(v.jitter.load()), int(v.reactive.load()),
int(v.object_motion.load()),
v.reactive_scale.load(), v.reactive_threshold.load(), v.reactive_max.load(),
v.debug_view.load(), int(v.show_fps.load()));
v.debug_view.load(), int(v.show_fps.load()),
int(v.fsr4_auto_exposure.load()), int(v.fsr4_invert_jitter.load()));
std::fclose(file);
}
@@ -129,7 +137,7 @@ const char* PresetName(int preset) {
}
const char* UpscalerName(int upscaler) {
static constexpr const char* names[UpscalerCount] = {"off", "fsr3"};
static constexpr const char* names[UpscalerCount] = {"off", "fsr3", "fsr4"};
return names[std::clamp(upscaler, 0, UpscalerCount - 1)];
}
+10 -2
View File
@@ -9,9 +9,9 @@
namespace BbSettings {
enum Upscaler : int { UpscalerOff = 0, UpscalerFsr3 = 1, UpscalerCount };
enum Upscaler : int { UpscalerOff = 0, UpscalerFsr3 = 1, UpscalerFsr4 = 2, UpscalerCount };
enum Preset : int { NativeAA = 0, Quality, Balanced, Performance, UltraPerformance, PresetCount };
enum DebugView : int { DebugNone = 0, DebugReactive = 1, DebugViewCount };
enum DebugView : int { DebugNone = 0, DebugReactive = 1, DebugMotion = 2, DebugViewCount };
struct Values {
std::atomic<int> upscaler{UpscalerFsr3};
@@ -20,16 +20,24 @@ struct Values {
std::atomic<float> sharpness{0.3f};
std::atomic<bool> jitter{true};
std::atomic<bool> reactive{false};
std::atomic<bool> object_motion{true};
std::atomic<float> reactive_scale{1.0f};
std::atomic<float> reactive_threshold{0.2f};
std::atomic<float> reactive_max{0.9f};
std::atomic<int> debug_view{DebugNone};
std::atomic<bool> show_fps{false};
// FSR 4 checks (menu): the provider's auto exposure, the jitter sign it is given.
std::atomic<bool> fsr4_auto_exposure{true};
std::atomic<bool> fsr4_invert_jitter{false};
std::atomic<int> active_render_width{1920}, active_render_height{1080};
/// Why FSR 4 cannot run (assets, device features), or null. Set by the renderer.
std::atomic<const char*> fsr4_problem{nullptr};
/// Preset and upscaler the game was started with: the render resolution patch is applied
/// at start (patches.py), a changed preset needs a restart.
int startup_preset = NativeAA;
int startup_upscaler = UpscalerFsr3;
bool startup_object_motion = true;
};
Values& Get();
+34
View File
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: helper thread sizing. Counts follow the hardware threads this process may run on
// (the affinity mask, so `taskset` can emulate a Steam Deck), and speculative helpers run as
// SCHED_IDLE: they use cores the game leaves idle and never take time from its threads.
#pragma once
#include <algorithm>
#include <sched.h>
#include <sys/resource.h>
#include <thread>
#include <unistd.h>
namespace BbThreads {
/// Hardware threads available to the process.
inline unsigned Available() {
cpu_set_t set;
CPU_ZERO(&set);
if (sched_getaffinity(0, sizeof(set), &set) == 0) {
return std::max(1, CPU_COUNT(&set));
}
return std::max(1u, std::thread::hardware_concurrency());
}
/// The calling thread only runs on otherwise idle cores (falls back to the lowest nice level).
inline void MakeBackground() {
sched_param param{};
if (sched_setscheduler(0, SCHED_IDLE, &param) != 0) {
setpriority(PRIO_PROCESS, static_cast<id_t>(gettid()), 19);
}
}
} // namespace BbThreads
+4
View File
@@ -25,6 +25,7 @@ enum : std::uint32_t {
AccessMemo = 512,
TextureBindingMemo = 1024,
CoarseReadTracking = 2048,
PreparedResources = 4096,
DrawPreparation = 8192,
DeferredStreamCopies = 16384,
HotPages = 32768,
@@ -32,6 +33,7 @@ enum : std::uint32_t {
ParallelCopies = 131072,
AsyncFences = 262144,
PoolSmallCopies = 524288,
UpdateImageFastPath = 1u << 30,
};
inline bool Disabled(std::uint32_t bit) {
return (__atomic_load_n(&runtime_disabled_optimizations, __ATOMIC_RELAXED) & bit) != 0;
@@ -48,6 +50,8 @@ inline std::atomic<std::uint64_t> image_upload_bytes{0};
inline std::atomic<std::uint64_t> buffer_upload_bytes{0};
inline std::atomic<int> gpu_thread_clock{-1}; ///< clockid_t of the GPU command thread
inline std::atomic<std::uint64_t> draws{0}, dispatches{0}, submissions{0};
/// Frames the GPU command thread has started (display pass), for per-frame diagnostics.
inline std::atomic<std::uint64_t> gpu_frames{0};
/// Wall time spent in operations suspected of stalls (ns, all threads).
inline std::atomic<std::uint64_t> t_resident{0}, t_protect{0}, t_image_create{0}, t_refresh{0},
t_staging{0}, t_host_wait{0}, t_copy{0}, copy_bytes{0}, t_read_faults{0}, read_faults{0},
+105
View File
@@ -0,0 +1,105 @@
/* Check whether native-size depth/stencil images can be blitted to reduced
* renderer targets. The renderer needs both directions for live presets. */
#include <stdio.h>
#include <stdlib.h>
#include <vulkan/vulkan.h>
static VkDeviceSize largest_local_heap(VkPhysicalDevice device) {
VkPhysicalDeviceMemoryProperties memory;
vkGetPhysicalDeviceMemoryProperties(device, &memory);
VkDeviceSize largest = 0;
for (uint32_t i = 0; i < memory.memoryHeapCount; ++i) {
if ((memory.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) &&
memory.memoryHeaps[i].size > largest) {
largest = memory.memoryHeaps[i].size;
}
}
return largest;
}
static int better_device(VkPhysicalDevice candidate, VkPhysicalDevice current) {
VkPhysicalDeviceProperties next, old;
vkGetPhysicalDeviceProperties(candidate, &next);
vkGetPhysicalDeviceProperties(current, &old);
const int next_api = next.apiVersion >= VK_API_VERSION_1_3;
const int old_api = old.apiVersion >= VK_API_VERSION_1_3;
if (next_api != old_api) return next_api;
const int next_discrete = next.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU;
const int old_discrete = old.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU;
if (next_discrete != old_discrete) return next_discrete;
const int next_cpu = next.deviceType == VK_PHYSICAL_DEVICE_TYPE_CPU;
const int old_cpu = old.deviceType == VK_PHYSICAL_DEVICE_TYPE_CPU;
if (next_cpu != old_cpu) return !next_cpu;
return largest_local_heap(candidate) > largest_local_heap(current);
}
int main(void) {
const VkApplicationInfo app = {
.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
.pApplicationName = "bbport scene scaling probe",
.apiVersion = VK_API_VERSION_1_3,
};
const VkInstanceCreateInfo create = {
.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
.pApplicationInfo = &app,
};
VkInstance instance = VK_NULL_HANDLE;
if (vkCreateInstance(&create, NULL, &instance) != VK_SUCCESS) {
fputs("GPU scene scaling: cannot create Vulkan instance\n", stderr);
return 1;
}
uint32_t count = 0;
if (vkEnumeratePhysicalDevices(instance, &count, NULL) != VK_SUCCESS || !count) {
fputs("GPU scene scaling: no Vulkan device\n", stderr);
vkDestroyInstance(instance, NULL);
return 1;
}
VkPhysicalDevice *devices = calloc(count, sizeof(*devices));
if (!devices || vkEnumeratePhysicalDevices(instance, &count, devices) != VK_SUCCESS) {
fputs("GPU scene scaling: cannot enumerate Vulkan devices\n", stderr);
free(devices);
vkDestroyInstance(instance, NULL);
return 1;
}
/* Match vk_instance.cpp's default ranking or its explicit BB_GPU_ID index. */
VkPhysicalDevice selected = devices[0];
const char *gpu_id = getenv("BB_GPU_ID");
if (gpu_id && atoi(gpu_id) >= 0) {
const unsigned long index = strtoul(gpu_id, NULL, 10);
if (index >= count) {
fputs("GPU scene scaling: BB_GPU_ID is outside the device list\n", stderr);
free(devices);
vkDestroyInstance(instance, NULL);
return 1;
}
selected = devices[index];
} else {
for (uint32_t i = 1; i < count; ++i)
if (better_device(devices[i], selected)) selected = devices[i];
}
VkPhysicalDeviceProperties props;
vkGetPhysicalDeviceProperties(selected, &props);
const struct { VkFormat format; const char *name; } formats[] = {
{VK_FORMAT_R8G8B8A8_UNORM, "RGBA8"},
{VK_FORMAT_R8G8B8A8_SRGB, "RGBA8 sRGB"},
{VK_FORMAT_B10G11R11_UFLOAT_PACK32, "B10G11R11"},
{VK_FORMAT_R16G16B16A16_SFLOAT, "RGBA16F"},
{VK_FORMAT_D32_SFLOAT_S8_UINT, "D32S8"},
};
int supported = 1;
for (size_t i = 0; i < sizeof(formats) / sizeof(formats[0]); ++i) {
VkFormatProperties features;
vkGetPhysicalDeviceFormatProperties(selected, formats[i].format, &features);
const VkFormatFeatureFlags required = VK_FORMAT_FEATURE_BLIT_SRC_BIT |
VK_FORMAT_FEATURE_BLIT_DST_BIT;
if ((features.optimalTilingFeatures & required) != required) {
fprintf(stderr, "GPU scene scaling: %s lacks blit support for %s\n",
props.deviceName, formats[i].name);
supported = 0;
}
}
if (supported) fprintf(stderr, "GPU scene scaling: %s supports live presets\n", props.deviceName);
free(devices);
vkDestroyInstance(instance, NULL);
return supported ? 0 : 1;
}
+205
View File
@@ -0,0 +1,205 @@
/* bbport: Wine side of the memory bridge test (helper/README.md). Built with MinGW, run under
* Proton's Wine. Imports a Vulkan buffer the native process exported as an opaque fd (inherited
* fd number on the command line) through VK_KHR_external_memory_win32, checks the native
* pattern and writes its own.
*
* usage: bridge_helper.exe <fd> <size> <device uuid hex> */
#define VK_USE_PLATFORM_WIN32_KHR
#define VK_NO_PROTOTYPES
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <vulkan/vulkan.h>
typedef LONG(WINAPI *FdToHandle)(int fd, unsigned int access, unsigned int attributes,
HANDLE *handle);
static PFN_vkGetInstanceProcAddr get_instance_proc;
#define LOAD(instance, name) PFN_##name name = (PFN_##name)get_instance_proc(instance, #name)
static int fail(const char *what, int code) {
printf("bridge_helper: %s (%d)\n", what, code);
fflush(stdout);
return 1;
}
int main(int argc, char **argv) {
if (argc < 4) {
return fail("usage: bridge_helper <fd> <size> <uuid>", 0);
}
const int fd = atoi(argv[1]);
const VkDeviceSize size = strtoull(argv[2], NULL, 10);
uint8_t uuid[VK_UUID_SIZE];
for (int i = 0; i < VK_UUID_SIZE; ++i) {
unsigned value = 0;
sscanf(argv[3] + 2 * i, "%2x", &value);
uuid[i] = (uint8_t)value;
}
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
FdToHandle fd_to_handle = (FdToHandle)GetProcAddress(ntdll, "wine_server_fd_to_handle");
if (!fd_to_handle) {
return fail("ntdll has no wine_server_fd_to_handle", 0);
}
HANDLE handle = NULL;
LONG status = fd_to_handle(fd, GENERIC_ALL, 0, &handle);
if (status != 0 || !handle) {
return fail("wine_server_fd_to_handle failed", (int)status);
}
HMODULE vulkan = LoadLibraryA("vulkan-1.dll");
if (!vulkan) {
return fail("cannot load vulkan-1.dll", (int)GetLastError());
}
get_instance_proc = (PFN_vkGetInstanceProcAddr)GetProcAddress(vulkan, "vkGetInstanceProcAddr");
LOAD(NULL, vkCreateInstance);
const VkApplicationInfo app = {.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
.apiVersion = VK_API_VERSION_1_3};
const VkInstanceCreateInfo instance_ci = {.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
.pApplicationInfo = &app};
VkInstance instance;
VkResult r = vkCreateInstance(&instance_ci, NULL, &instance);
if (r) return fail("vkCreateInstance", r);
LOAD(instance, vkEnumeratePhysicalDevices);
LOAD(instance, vkGetPhysicalDeviceProperties2);
LOAD(instance, vkGetPhysicalDeviceMemoryProperties);
LOAD(instance, vkCreateDevice);
LOAD(instance, vkGetDeviceProcAddr);
VkPhysicalDevice devices[8];
uint32_t count = 8;
vkEnumeratePhysicalDevices(instance, &count, devices);
VkPhysicalDevice physical = VK_NULL_HANDLE;
for (uint32_t i = 0; i < count; ++i) {
VkPhysicalDeviceIDProperties ids = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES};
VkPhysicalDeviceProperties2 props = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2,
.pNext = &ids};
vkGetPhysicalDeviceProperties2(devices[i], &props);
if (!memcmp(ids.deviceUUID, uuid, VK_UUID_SIZE)) {
physical = devices[i];
printf("bridge_helper: device %s\n", props.properties.deviceName);
}
}
if (!physical) return fail("no device with the native UUID", (int)count);
const float priority = 1.0f;
const VkDeviceQueueCreateInfo queue_ci = {.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
.queueFamilyIndex = 0,
.queueCount = 1,
.pQueuePriorities = &priority};
const char *extensions[] = {VK_KHR_EXTERNAL_MEMORY_WIN32_EXTENSION_NAME};
const VkDeviceCreateInfo device_ci = {.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
.queueCreateInfoCount = 1,
.pQueueCreateInfos = &queue_ci,
.enabledExtensionCount = 1,
.ppEnabledExtensionNames = extensions};
VkDevice device;
r = vkCreateDevice(physical, &device_ci, NULL, &device);
if (r) return fail("vkCreateDevice (external_memory_win32)", r);
#define DLOAD(name) PFN_##name name = (PFN_##name)vkGetDeviceProcAddr(device, #name)
DLOAD(vkCreateBuffer);
DLOAD(vkGetBufferMemoryRequirements);
DLOAD(vkAllocateMemory);
DLOAD(vkBindBufferMemory);
DLOAD(vkMapMemory);
DLOAD(vkGetDeviceQueue);
DLOAD(vkCreateCommandPool);
DLOAD(vkAllocateCommandBuffers);
DLOAD(vkBeginCommandBuffer);
DLOAD(vkCmdCopyBuffer);
DLOAD(vkCmdFillBuffer);
DLOAD(vkCmdPipelineBarrier);
DLOAD(vkEndCommandBuffer);
DLOAD(vkQueueSubmit);
DLOAD(vkQueueWaitIdle);
const VkExternalMemoryBufferCreateInfo external_buffer = {
.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO,
.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT};
const VkBufferCreateInfo shared_ci = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
.pNext = &external_buffer,
.size = size,
.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT |
VK_BUFFER_USAGE_TRANSFER_DST_BIT};
VkBuffer shared;
vkCreateBuffer(device, &shared_ci, NULL, &shared);
VkMemoryRequirements requirements;
vkGetBufferMemoryRequirements(device, shared, &requirements);
VkPhysicalDeviceMemoryProperties memory_props;
vkGetPhysicalDeviceMemoryProperties(physical, &memory_props);
uint32_t type = 0;
while (!(requirements.memoryTypeBits & (1u << type))) ++type;
const VkImportMemoryWin32HandleInfoKHR import = {
.sType = VK_STRUCTURE_TYPE_IMPORT_MEMORY_WIN32_HANDLE_INFO_KHR,
.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT,
.handle = handle};
const VkMemoryAllocateInfo shared_alloc = {.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
.pNext = &import,
.allocationSize = requirements.size,
.memoryTypeIndex = type};
VkDeviceMemory shared_memory;
r = vkAllocateMemory(device, &shared_alloc, NULL, &shared_memory);
if (r) return fail("vkAllocateMemory import", r);
vkBindBufferMemory(device, shared, shared_memory, 0);
/* Readback buffer (host visible). */
const VkBufferCreateInfo read_ci = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
.size = size,
.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT};
VkBuffer readback;
vkCreateBuffer(device, &read_ci, NULL, &readback);
vkGetBufferMemoryRequirements(device, readback, &requirements);
type = 0;
while (!((requirements.memoryTypeBits & (1u << type)) &&
(memory_props.memoryTypes[type].propertyFlags &
(VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)) ==
(VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)))
++type;
const VkMemoryAllocateInfo read_alloc = {.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
.allocationSize = requirements.size,
.memoryTypeIndex = type};
VkDeviceMemory read_memory;
vkAllocateMemory(device, &read_alloc, NULL, &read_memory);
vkBindBufferMemory(device, readback, read_memory, 0);
VkQueue queue;
vkGetDeviceQueue(device, 0, 0, &queue);
const VkCommandPoolCreateInfo pool_ci = {.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
VkCommandPool pool;
vkCreateCommandPool(device, &pool_ci, NULL, &pool);
const VkCommandBufferAllocateInfo cb_ai = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
.commandPool = pool,
.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
.commandBufferCount = 1};
VkCommandBuffer cmd;
vkAllocateCommandBuffers(device, &cb_ai, &cmd);
const VkCommandBufferBeginInfo begin = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
vkBeginCommandBuffer(cmd, &begin);
const VkBufferCopy region = {.size = size};
vkCmdCopyBuffer(cmd, shared, readback, 1, &region);
const VkMemoryBarrier barrier = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER,
.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT,
.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT};
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1,
&barrier, 0, NULL, 0, NULL);
vkCmdFillBuffer(cmd, shared, 0, size, 0x5EED1234u);
vkEndCommandBuffer(cmd);
const VkSubmitInfo submit = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
.commandBufferCount = 1,
.pCommandBuffers = &cmd};
r = vkQueueSubmit(queue, 1, &submit, VK_NULL_HANDLE);
if (r) return fail("vkQueueSubmit", r);
vkQueueWaitIdle(queue);
uint32_t *words;
vkMapMemory(device, read_memory, 0, size, 0, (void **)&words);
size_t bad = 0;
for (size_t i = 0; i < size / 4; ++i) {
bad += words[i] != 0xB12D6E00u + (uint32_t)(i & 0xFF);
}
printf("bridge_helper: native pattern %s (%zu bad words of %zu); wrote 0x5EED1234\n",
bad ? "WRONG" : "OK", bad, (size_t)(size / 4));
fflush(stdout);
return bad ? 2 : 0;
}
+33 -9
View File
@@ -5,6 +5,7 @@ image places eboot vaddr 0 at image offset 0. Only literal writes are supported
(bytes, bytes16/32/64, float32/64, utf8, utf16); pattern ("mask") patches are rejected.
"""
import argparse
import os
import struct
import xml.etree.ElementTree as ET
from pathlib import Path
@@ -16,10 +17,14 @@ FPS_PRESETS={'30':[],'60':['60 FPS++'],'90':['90 FPS++'],'uncap':['Uncap FPS++']
# then renders at 1920x1080 / ratio and the port's temporal upscaler restores the output size.
OUTPUT_SIZE=(1920,1080)
PRESET_SCALES=[1.0,1.5,1.7,2.0,3.0]
# The community resolution patch this is derived from: its "mov eax/ecx, imm32" render width
# (0x500) and height (0x2D0) immediates are replaced; its other lines (UI coordinate space,
# aspect) are kept.
# The community patch changes two independent consumers: the game render/window setup
# and the UI movie viewport. Keep the latter at native size so glyph rasterisation and
# vector tessellation do not inherit the scene's FSR resolution.
RESOLUTION_TEMPLATE='Resolution Patch 1280x720 (16:9)'
SCENE_WIDTH=0x02196A6B-EBOOT_BASE
SCENE_HEIGHT=0x02196A7A-EBOOT_BASE
UI_WIDTH=0x02358554-EBOOT_BASE
UI_HEIGHT=0x0235855D-EBOOT_BASE
def read_settings(path):
@@ -46,13 +51,22 @@ def render_size(settings,override=''):
def resolution_writes(xml,size,app_version,segments):
writes=compile_patches(xml,[RESOLUTION_TEMPLATE],app_version,segments)
replacements={SCENE_WIDTH:(0xB8,0x500,size[0]),
SCENE_HEIGHT:(0xB8,0x2D0,size[1]),
UI_WIDTH:(0xB8,0x500,OUTPUT_SIZE[0]),
UI_HEIGHT:(0xB9,0x2D0,OUTPUT_SIZE[1])}
out=[]
seen=set()
for offset,data in writes:
if len(data)==4 and data[0] in (0xB8,0xB9):
imm=int.from_bytes(data[1:4],'little')
if imm==0x500: data=bytes([data[0]])+size[0].to_bytes(3,'little')
elif imm==0x2D0: data=bytes([data[0]])+size[1].to_bytes(3,'little')
if offset in replacements:
opcode,old,value=replacements[offset]
if data!=bytes([opcode])+old.to_bytes(3,'little') or offset in seen:
raise ValueError(f'unexpected resolution patch at {offset+EBOOT_BASE:#x}')
data=bytes([opcode])+value.to_bytes(3,'little')
seen.add(offset)
out.append((offset,data))
if seen!=replacements.keys():
raise ValueError('resolution patch is missing scene/UI viewport instructions')
return out
@@ -105,14 +119,24 @@ def main():
p.add_argument('--out',type=Path,default=Path(__file__).parent/'out')
p.add_argument('--settings',type=Path,default=Path(__file__).parent/'bbport.ini')
p.add_argument('--render-res',default='',help='render resolution WxH (overrides the preset)')
p.add_argument('--print-preset-size',action='store_true',help='print the selected preset size, if reduced')
a=p.parse_args()
if a.print_preset_size:
settings=read_settings(a.settings)
if 'BB_UPSCALER' in os.environ:
settings['upscaler']='fsr3' if os.environ['BB_UPSCALER']=='fsr3' else 'off'
if 'BB_UPSCALE_PRESET' in os.environ:
settings['preset']=os.environ['BB_UPSCALE_PRESET']
size=render_size(settings)
if size: print(f'{size[0]}x{size[1]}')
return
names=FPS_PRESETS[a.fps]+[n.strip() for n in a.extra.split(';') if n.strip()]
segments=eboot_segments((a.out/'eboot.elf').read_bytes())
writes=compile_patches(a.xml,names,a.app_version,segments)
size=render_size(read_settings(a.settings),a.render_res)
size=render_size(read_settings(a.settings),a.render_res) if a.render_res else None
if size:
writes+=resolution_writes(a.xml,size,a.app_version,segments)
print(f'Patches: render resolution {size[0]}x{size[1]} (upscaled to {OUTPUT_SIZE[0]}x{OUTPUT_SIZE[1]})')
print(f'Patches: scene {size[0]}x{size[1]}; native UI {OUTPUT_SIZE[0]}x{OUTPUT_SIZE[1]}')
blob=struct.pack('<8sQ',b'BBPATCH1',len(writes))
for offset,data in writes: blob+=struct.pack('<QQ',offset,len(data))+data
(a.out/'patches.bin').write_bytes(blob)
+10 -1
View File
@@ -28,10 +28,19 @@ if [[ -z ${PYTHON:-} ]]; then echo 'Install Python 3 or set PYTHON.' >&2; exit 1
"$PYTHON" link_libc.py ../CUSA03173
"$PYTHON" link_modules.py ../CUSA03173
"$PYTHON" content_profile.py ../CUSA03173 --sku "${BB_CONTENT_SKU:-full}"
# Dynamic scene resolution scaling now works on all GPUs (fallback: clear UI depth
# instead of blit when D32S8 blit unsupported). The old startup resolution patch
# is kept as BB_RENDER_RES for explicit overrides and compatibility testing.
if [[ ${BB_AUTO_RENDER_RES:-} == 1 ]]; then
unset BB_RENDER_RES BB_AUTO_RENDER_RES
fi
# BB_RENDER_RES=WxH explicitly overrides dynamic scaling (for testing/debugging).
# Without it, all GPUs use live preset switching.
# Frame rate: BB_FPS=uncap (default; delta-time patch, vblank follows the display),
# 60/90 (fixed-timestep patches) or 30 (unpatched). BB_PATCHES adds patch names ("a;b").
fps=${BB_FPS:-uncap}
# Render resolution: the upscaler preset in ${BB_CONFIG:-bbport.ini}, or BB_RENDER_RES=WxH.
# Supported GPUs scale renderer targets at run time. BB_RENDER_RES=WxH keeps the
# explicit guest-resolution patch for compatibility and debugging.
"$PYTHON" patches.py --fps "$fps" --extra "${BB_PATCHES:-}" --settings "${BB_CONFIG:-bbport.ini}" --render-res "${BB_RENDER_RES:-}"
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
+3 -1
View File
@@ -499,13 +499,15 @@ void *runtime_low_map(size_t size, int prot) {
* 32 texture descriptor cache, 64 lock-free upload check, 128 image lookup cache,
* 256 buffer uploads on the recording thread, 512 barrier tracker insert memo,
* 1024 texture binding memo, 2048 page-granular read tracking for barriers,
* 4096 resource sharps read by the draw-preparation workers,
* 8192 prepared draws from the draw-preparation workers,
* 16384 small read-only buffer copies on the recording thread,
* 32768 hot pages (opt-in with BB_HOT_PAGES=1),
* 65536 unprotect the 256 KiB window around a guest write fault (BB_FAULT_WINDOW KiB),
* 131072 large guest memory copies split across copy threads (BB_COPY_THREADS),
* 262144 with BB_ASYNC_FENCES=1: wait for guest copies at fences again,
* 524288 small guest copies batched for the copy threads instead of the recording thread. */
* 524288 small guest copies batched for the copy threads instead of the recording thread,
* 1073741824 the lock-free UpdateImage path for clean, tracked images. */
uint32_t runtime_disabled_optimizations;
/* Speculative readers of guest memory (GPU draw-preparation workers) register a recovery
* point: a fault on that thread jumps back to it instead of terminating (probe.c). */
+25
View File
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstdlib>
#include <chrono>
#include <cstdint>
#include <cstddef>
#include <setjmp.h>
// Renderer tests have no guest process. Clock/host-thread services work; guest accesses abort.
extern "C" {
thread_local sigjmp_buf* runtime_fault_recover = nullptr;
uint32_t runtime_disabled_optimizations = 0;
uint64_t runtime_tsc_frequency() { return 1000000000; }
int runtime_file_translate(const char*, char*, size_t) { std::abort(); }
uint64_t runtime_memory_clamp(uintptr_t, uint64_t) { std::abort(); }
uint64_t runtime_process_time_us() { return std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now().time_since_epoch()).count(); }
int runtime_memory_region(uintptr_t, uintptr_t*, uintptr_t*, int*) { std::abort(); }
void runtime_memory_set_gpu_hooks(void (*)(uintptr_t, uint64_t),
void (*)(uintptr_t, uint64_t), void (*)(uintptr_t, uint64_t)) { std::abort(); }
void runtime_thread_attach_host(const char*) {}
void runtime_memory_gpu_protect(uintptr_t, uint64_t, int, int) { std::abort(); }
void runtime_restart() { std::abort(); }
uint64_t runtime_process_time_counter() { return runtime_process_time_us() * 1000; }
int32_t* runtime_errno() { std::abort(); }
int runtime_memory_write_backing(uintptr_t, const void*, uint64_t) { std::abort(); }
}
+113
View File
@@ -0,0 +1,113 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Standalone CPU regression checks: no game, Vulkan device or window required.
#include <array>
#include <cassert>
#include <cstdio>
#include "gpu/shadps4/video_core/renderer_vulkan/motion_history.h"
using namespace Vulkan::Motion;
int main() {
const std::array<uint16_t, 5> indices{1000, 1007, 1002, 1007, UINT16_MAX};
const auto range = IndexedRange(std::span(indices), -900, true);
assert(range.first == 100 && range.count == 8);
assert(IndexedRange(std::span(indices), -1001, true).count == 0);
assert(IndexedRange(std::span(indices), 0, false).count == 64536);
const std::array<uint32_t, 3> large{100000, 100003, UINT32_MAX};
assert(IndexedRange(std::span(large), 7, true).count == 4);
assert(IndexedRange(std::span(large), INT32_MAX, true).count == 0);
const std::array<uint16_t, 1> restart{UINT16_MAX};
assert(IndexedRange(std::span(restart), 0, true).count == 0);
assert(IndexedRange(std::span<const uint16_t>{}, 0, false).count == 0);
Draw draw{.shader = 1, .geometry = 2, .indices = 3, .topology = 4,
.index_count = 5, .instances = 2, .first_instance = 100,
.vertices = range};
History history(32);
history.NextFrame();
const auto first = history.Prepare(draw);
const auto duplicate = history.Prepare(draw);
assert(first.store && !first.load && first.first_vertex == 100);
assert(first.first_instance == 100 && first.instances == 2);
assert(duplicate.store == first.store + 16); // shared geometry, different draw
assert(history.Used() == 32); // firstInstance must NOT multiply the allocation
assert(!history.Prepare(draw).store && history.stats.exhausted == 1);
history.NextFrame();
const auto next = history.Prepare(draw);
assert(next.load == first.store && next.store != first.store);
assert(history.Prepare(draw).load == duplicate.store);
history.NextFrame(); // a missing frame invalidates the match
history.NextFrame();
assert(!history.Prepare(draw).load);
// A changed index buffer may reference formerly unused holes inside the same min/max.
// Do not accept that history merely because its addresses/counts stayed the same.
history.NextFrame();
auto changed = draw;
changed.topology++;
assert(!history.Prepare(changed).load);
changed = draw;
changed.first_instance++;
assert(!history.Prepare(changed).load);
history.NextFrame();
changed = draw;
changed.vertices.first++;
assert(!history.Prepare(changed).load);
History overflow(32);
draw.vertices.first = 0;
draw.vertices.count = INT32_MAX;
draw.instances = UINT32_MAX;
draw.first_instance = 0;
assert(!overflow.Prepare(draw).store);
assert(overflow.stats.exhausted == 1);
draw.first_instance = 10;
assert(!overflow.Prepare(draw).store && overflow.stats.invalid == 1);
draw.first_instance = 0;
draw.vertices.first = INT32_MAX;
draw.vertices.count = 2;
assert(!overflow.Prepare(draw).store && overflow.stats.invalid == 2);
// Small skeletons: history only while the bone palette changes (weapon spinning).
History gated(1024);
const History::GateKey weapon{.shader = 7, .index_count = 3, .instances = 1};
const auto frame = [&](uint64_t palette) {
gated.NextFrame();
return gated.Moving(weapon, palette);
};
assert(!gated.Moving(weapon, 1) && gated.stats.still == 1); // first sight
assert(!frame(1)); // same bones: camera motion
assert(frame(2) && frame(3)); // spinning
assert(!frame(3)); // stopped
gated.NextFrame();
gated.NextFrame();
assert(!gated.Moving(weapon, 4)); // after a skipped frame: no comparison
assert(!gated.Moving(weapon, 5)); // second occurrence of the same draw
assert(ClassifyBuffer(864) == BufferRole::Other && ClassifyBuffer(416) == BufferRole::Other);
assert(ClassifyBuffer(96) == BufferRole::SmallSkeleton &&
ClassifyBuffer(384) == BufferRole::SmallSkeleton);
assert(ClassifyBuffer(656) == BufferRole::Skeleton && ClassifyBuffer(64) == BufferRole::Other);
// Index ranges: reused between full scans, rescanned every Revalidate frames, trimmed.
IndexRangeCache ranges;
uint32_t scans = 0;
const IndexRangeCache::Key mesh{.address = 0x1000, .count = 3, .index_size = 2};
const auto scan = [&] {
++scans;
return IndexRangeCache::Result{{4, 3}, 99};
};
assert(ranges.Get(mesh, 0, scan).topology == 99 && scans == 1);
assert(ranges.Get(mesh, IndexRangeCache::Revalidate - 1, scan).range == (VertexRange{4, 3}));
assert(scans == 1);
ranges.Get(mesh, IndexRangeCache::Revalidate, scan);
assert(scans == 2);
ranges.Trim(IndexRangeCache::Revalidate + IndexRangeCache::Unused + 1);
assert(ranges.Size() == 0);
assert(JitterPhases(1920, 1920) == 8);
assert(JitterPhases(1280, 1920) == 18);
assert(JitterPhases(960, 1920) == 32);
assert(JitterPhases(640, 1920) == 72);
assert(JitterPhases(0, 1920) == 8);
std::puts("Motion history: PASS (ranges, restart, offsets, matching, capacity, gating, index cache, jitter)");
}
+62
View File
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Exercise the actual SPIR-V backend without opening a window or loading the game.
// Output is validated by spirv-val (see docs/upscaler.md).
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <setjmp.h>
#include "shader_recompiler/backend/spirv/emit_spirv.h"
#include "shader_recompiler/ir/ir_emitter.h"
int main(int argc, char** argv) {
using namespace Shader;
const std::filesystem::path dir = argc > 1 ? argv[1] : ".";
std::filesystem::create_directories(dir);
MotionVectors::params_address = 0x10000;
MotionVectors::positions_address = 0x20000;
for (bool vertex : {true, false}) {
for (bool motion : {false, true}) {
Info info{};
info.hw_stage = vertex ? HwStage::Vertex : HwStage::Fragment;
info.sw_stage = vertex ? SwStage::Vertex : SwStage::Fragment;
RuntimeInfo runtime{};
runtime.Initialize(info.hw_stage, info.sw_stage);
if (vertex) {
runtime.hw.vs.motion_vectors = motion;
} else {
runtime.hw.fs.motion_vectors = motion;
runtime.hw.fs.color_buffers[0].num_format = AmdGpu::NumberFormat::Float;
}
Common::ObjectPool<IR::Inst> pool;
IR::Block block(pool);
IR::IREmitter ir(block);
const auto output = vertex ? IR::Attribute::Position0 : IR::Attribute::RenderTarget0;
for (u32 component = 0; component < 4; ++component) {
info.stores.Set(output, component);
if (!vertex && component < 2) {
info.loads.Set(IR::Attribute::FragCoord, component);
ir.SetAttribute(output, ir.GetAttribute(IR::Attribute::FragCoord, component), component);
} else {
ir.SetAttribute(output, ir.Imm32(component == 3 ? 1.0f : 0.0f), component);
}
}
ir.Epilogue();
IR::Program program(info);
program.blocks.push_back(&block);
program.syntax_list.push_back({.data = {.block = &block},
.type = IR::AbstractSyntaxNode::Type::Block});
program.syntax_list.push_back({.type = IR::AbstractSyntaxNode::Type::Return});
Profile profile{};
profile.supported_spirv = 0x00010600;
profile.support_int64 = true;
Backend::Bindings bindings{};
const auto code = Backend::SPIRV::EmitSPIRV(profile, runtime, program, bindings);
const auto path = dir / (std::string(vertex ? "vertex" : "fragment") +
(motion ? "-motion.spv" : "-plain.spv"));
std::ofstream out(path, std::ios::binary);
out.write(reinterpret_cast<const char*>(code.data()), code.size() * sizeof(u32));
if (!out) { return 1; }
}
}
}
+76
View File
@@ -0,0 +1,76 @@
import struct
import tempfile
import unittest
import xml.etree.ElementTree as ET
from pathlib import Path
from patches import (EBOOT_BASE, OUTPUT_SIZE, RESOLUTION_TEMPLATE, SCENE_HEIGHT,
SCENE_WIDTH, UI_HEIGHT, UI_WIDTH, compile_patches,
render_size, resolution_writes)
XML = Path(__file__).parent / 'patches/Bloodborne.xml'
SEGMENTS = [(0, 0x6000000)]
def immediate(writes, address):
# The XML splits a 9-byte MOV imm32 + NOP into 4+4+1 byte writes.
data = bytearray(9)
for offset, value in writes:
if address <= offset < address + 9:
data[offset-address:offset-address+len(value)] = value
assert data[5:] == b'\x0f\x1f\x40\x00'
return data[0], struct.unpack_from('<I', data, 1)[0]
class NativeUiTests(unittest.TestCase):
def test_presets_keep_ui_native(self):
for preset, expected in [(1, (1280, 720)), (2, (1130, 636)),
(3, (960, 540)), (4, (640, 360))]:
with self.subTest(preset=preset):
size = render_size({'upscaler': 'fsr3', 'preset': str(preset)})
self.assertEqual(size, expected)
writes = resolution_writes(XML, size, '01.09', SEGMENTS)
self.assertEqual(immediate(writes, SCENE_WIDTH), (0xB8, size[0]))
self.assertEqual(immediate(writes, SCENE_HEIGHT), (0xB8, size[1]))
self.assertEqual(immediate(writes, UI_WIDTH), (0xB8, OUTPUT_SIZE[0]))
self.assertEqual(immediate(writes, UI_HEIGHT), (0xB9, OUTPUT_SIZE[1]))
def test_explicit_scene_resolution_keeps_native_ui(self):
writes = resolution_writes(XML, (800, 450), '01.09', SEGMENTS)
self.assertEqual(immediate(writes, SCENE_WIDTH)[1], 800)
self.assertEqual(immediate(writes, UI_WIDTH)[1], 1920)
self.assertEqual(immediate(writes, UI_HEIGHT)[1], 1080)
def test_coordinate_and_aspect_fixes_are_preserved(self):
original = compile_patches(XML, [RESOLUTION_TEMPLATE], '01.09', SEGMENTS)
modified = resolution_writes(XML, (960, 540), '01.09', SEGMENTS)
self.assertEqual(len(original), len(modified))
changed = {SCENE_WIDTH, SCENE_HEIGHT, UI_WIDTH, UI_HEIGHT}
self.assertEqual([w for w in original if w[0] not in changed],
[w for w in modified if w[0] not in changed])
def test_unexpected_or_missing_ui_instruction_is_rejected(self):
for remove in (False, True):
tree = ET.parse(XML)
for meta in tree.getroot().iter('Metadata'):
if meta.get('Name') == RESOLUTION_TEMPLATE and meta.get('AppVer') == '01.09':
patch_list = meta.find('PatchList')
for line in list(patch_list):
if int(line.get('Address'), 0) == UI_WIDTH + EBOOT_BASE:
if remove:
patch_list.remove(line)
else:
line.set('Value', '0x000500B9')
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / 'patch.xml'
tree.write(path)
with self.assertRaises(ValueError):
resolution_writes(path, (960, 540), '01.09', SEGMENTS)
def test_native_and_disabled_upscaler_need_no_resolution_patch(self):
self.assertIsNone(render_size({'preset': '0'}))
self.assertIsNone(render_size({'upscaler': 'off', 'preset': '3'}))
if __name__ == '__main__':
unittest.main()
+147
View File
@@ -0,0 +1,147 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Exercises the production target/resolve path on Vulkan (Lavapipe works, no game/window).
#include <cassert>
#include <cmath>
#include <cstdio>
#include <cstring>
#include "common/slot_vector.h"
#include "video_core/renderer_vulkan/vk_instance.h"
#include "video_core/renderer_vulkan/vk_runtime.h"
#include "video_core/renderer_vulkan/vk_scheduler.h"
#include "video_core/renderer_vulkan/vk_scene_resolution.h"
#include "video_core/texture_cache/blit_helper.h"
#include <vk_mem_alloc.h>
int main() {
using namespace Vulkan;
Instance instance(0, false);
// Calls emitted by this executable use a local dispatcher. libbbgpu.so initializes
// its own default dispatcher while constructing Instance.
static vk::detail::DynamicLoader loader;
vk::detail::DispatchLoaderDynamic dispatch;
dispatch.init(
loader.getProcAddress<PFN_vkGetInstanceProcAddr>("vkGetInstanceProcAddr"));
dispatch.init(instance.GetInstance());
dispatch.init(instance.GetDevice());
Scheduler scheduler(instance);
Runtime runtime(instance, scheduler);
Common::SlotVector<VideoCore::ImageView> views;
Common::SlotVector<VideoCore::Image> images;
VideoCore::ImageInfo ci;
ci.size = {1920,1080,1};
ci.resources = {1,1};
ci.type = AmdGpu::ImageType::Color2D;
ci.pixel_format = vk::Format::eR8G8B8A8Unorm;
ci.num_bits = 32;
const auto color_id = images.insert(instance, runtime, views, ci);
auto di = ci;
di.pixel_format = vk::Format::eD32SfloatS8Uint;
di.props.is_depth = di.props.has_stencil = true;
const auto depth_id = images.insert(instance, runtime, views, di);
SceneTargets targets(instance, scheduler, runtime, [&](VideoCore::ImageId id, u64 uid) {
if (!images.is_allocated(id)) return static_cast<VideoCore::Image*>(nullptr);
return !uid || images[id].image_uid == uid ? &images[id] : nullptr;
});
const bool color_eligible = targets.Eligible(images[color_id]);
const bool depth_eligible = targets.Eligible(images[depth_id]);
std::fprintf(stderr, "Scene target eligibility: color=%d depth=%d\n",
color_eligible, depth_eligible);
if (!color_eligible || !depth_eligible) {
std::puts("Scene targets: unsupported format blit, live scaling skipped");
return 0;
}
VkBuffer readback{};
VmaAllocation allocation{};
VmaAllocationInfo ai{};
const VkBufferCreateInfo bi{.sType=VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
.size=16, .usage=VK_BUFFER_USAGE_TRANSFER_DST_BIT};
const VmaAllocationCreateInfo ac{.flags=VMA_ALLOCATION_CREATE_MAPPED_BIT |
VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT, .usage=VMA_MEMORY_USAGE_AUTO,
.requiredFlags=VK_MEMORY_PROPERTY_HOST_COHERENT_BIT};
assert(vmaCreateBuffer(instance.GetAllocator(), &bi, &ac, &readback, &allocation, &ai) == VK_SUCCESS);
const auto pixel = [&](vk::Image image, vk::ImageAspectFlagBits aspect,
vk::ImageLayout layout, u32 w, u32 h) {
scheduler.EndRendering();
const auto cmd = scheduler.CommandBuffer();
const vk::MemoryBarrier2 b{.srcStageMask=vk::PipelineStageFlagBits2::eAllCommands,
.srcAccessMask=vk::AccessFlagBits2::eMemoryWrite,
.dstStageMask=vk::PipelineStageFlagBits2::eTransfer,
.dstAccessMask=vk::AccessFlagBits2::eTransferRead};
cmd.pipelineBarrier2({.memoryBarrierCount=1,.pMemoryBarriers=&b}, dispatch);
const vk::BufferImageCopy region{.imageSubresource={aspect,0,0,1},
.imageOffset={s32(w-1),s32(h-1),0},.imageExtent={1,1,1}};
cmd.copyImageToBuffer(image, layout, readback, region, dispatch);
const vk::MemoryBarrier2 host{.srcStageMask=vk::PipelineStageFlagBits2::eTransfer,
.srcAccessMask=vk::AccessFlagBits2::eTransferWrite,
.dstStageMask=vk::PipelineStageFlagBits2::eHost,
.dstAccessMask=vk::AccessFlagBits2::eHostRead};
cmd.pipelineBarrier2({.memoryBarrierCount=1,.pMemoryBarriers=&host}, dispatch);
scheduler.Finish();
u32 result;
std::memcpy(&result, ai.pMappedData, 4);
return result;
};
VideoCore::ImageViewInfo cv;
cv.format = ci.pixel_format;
auto dv = cv;
dv.format = di.pixel_format;
const auto clear_native = [&](u32 rgba) {
vk::ClearValue clear{};
clear.color.float32 = std::array{float(rgba & 255)/255, float((rgba>>8)&255)/255,
float((rgba>>16)&255)/255, 1.0f};
runtime.ClearImage(&images[color_id], {}, clear);
runtime.Transit(&images[depth_id], vk::ImageLayout::eTransferDstOptimal,
vk::PipelineStageFlagBits2::eTransfer, vk::AccessFlagBits2::eTransferWrite);
runtime.FlushBarriers();
scheduler.CommandBuffer().clearDepthStencilImage(
images[depth_id].GetImage(), vk::ImageLayout::eTransferDstOptimal,
vk::ClearDepthStencilValue{0.25f, 7},
vk::ImageSubresourceRange{vk::ImageAspectFlagBits::eDepth |
vk::ImageAspectFlagBits::eStencil, 0, 1, 0, 1}, dispatch);
};
for (int preset : {3,1,4,2,0,3,0}) {
const auto size = SceneResolution::ForPreset(preset);
targets.SetSize(size);
assert(SceneResolution::Unpack(SceneResolution::Pack(size)) == size);
clear_native(0xffff0000); // blue
auto c = targets.Read(color_id,cv);
assert(pixel(c.image,vk::ImageAspectFlagBits::eColor,c.layout,size.width,size.height)==0xffff0000);
auto d = targets.Read(depth_id,dv);
const u32 depth_bits = pixel(d.image,vk::ImageAspectFlagBits::eDepth,d.layout,size.width,size.height);
assert(std::bit_cast<float>(depth_bits)==0.25f);
assert((pixel(d.image,vk::ImageAspectFlagBits::eStencil,d.layout,size.width,size.height)&255)==7);
c = targets.Attachment(color_id,cv);
d = targets.Attachment(depth_id,dv);
auto cmd = scheduler.CommandBuffer();
const vk::RenderingAttachmentInfo color{.imageView=c.view,.imageLayout=c.layout,
.loadOp=vk::AttachmentLoadOp::eClear,.storeOp=vk::AttachmentStoreOp::eStore,
.clearValue=vk::ClearValue{.color={.float32=std::array{1.f,0.f,0.f,1.f}}}};
const vk::RenderingAttachmentInfo depth{.imageView=d.view,.imageLayout=d.layout,
.loadOp=vk::AttachmentLoadOp::eClear,.storeOp=vk::AttachmentStoreOp::eStore,
.clearValue=vk::ClearValue{.depthStencil={0.75f,13}}};
cmd.beginRendering({.renderArea={{0,0},{size.width,size.height}},.layerCount=1,
.colorAttachmentCount=1,.pColorAttachments=&color,
.pDepthAttachment=&depth,.pStencilAttachment=&depth}, dispatch);
cmd.endRendering(dispatch);
runtime.Transit(&images[color_id],vk::ImageLayout::eTransferSrcOptimal,
vk::PipelineStageFlagBits2::eTransfer,vk::AccessFlagBits2::eTransferRead);
runtime.FlushBarriers();
assert(pixel(images[color_id].GetImage(),vk::ImageAspectFlagBits::eColor,
vk::ImageLayout::eTransferSrcOptimal,1920,1080)==0xff0000ff);
runtime.Transit(&images[depth_id],vk::ImageLayout::eTransferSrcOptimal,
vk::PipelineStageFlagBits2::eTransfer,vk::AccessFlagBits2::eTransferRead);
runtime.FlushBarriers();
assert(std::bit_cast<float>(pixel(images[depth_id].GetImage(),vk::ImageAspectFlagBits::eDepth,
vk::ImageLayout::eTransferSrcOptimal,1920,1080))==0.75f);
assert((pixel(images[depth_id].GetImage(),vk::ImageAspectFlagBits::eStencil,
vk::ImageLayout::eTransferSrcOptimal,1920,1080)&255)==13);
clear_native(0xff00ff00); // overwrite after reduced render: proxy must be invalidated
c=targets.Read(color_id,cv);
assert(pixel(c.image,vk::ImageAspectFlagBits::eColor,c.layout,size.width,size.height)==0xff00ff00);
std::printf("Scene targets: %ux%u color/depth/stencil roundtrip PASS\n",size.width,size.height);
}
scheduler.Finish();
vmaDestroyBuffer(instance.GetAllocator(),readback,allocation);
}
+26
View File
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cassert>
#include <cstdio>
#include "gpu/shadps4/video_core/renderer_vulkan/ui_composition.h"
int main() {
using namespace Vulkan::UiComposition;
// Title/loading menus have no depth/camera. UI must still go to the native target.
assert(Choose(true, true, false, true) == Background::Copy);
assert(Choose(true, true, false, false) == Background::Copy);
// In-game HUD: first reconstruct the scene, then draw the native UI over it.
assert(Choose(true, true, true, true) == Background::Temporal);
// Runtime FSR disable/failure must not change UI resolution.
assert(Choose(true, true, true, false) == Background::Copy);
assert(Choose(true, false, true, true) == Background::None);
assert(Choose(false, true, true, true) == Background::None);
assert(NativeViewport(1920, -1080));
assert(!NativeViewport(960, -540));
const auto display = Scale(960, 540, 1920, 1080, false);
assert(display[0] == 2 && display[1] == 2);
const auto ui = Scale(960, 540, 1920, 1080, true);
assert(ui[0] == 1 && ui[1] == 1); // don't double an already native UI viewport
const auto larger = Scale(960, 540, 3840, 2160, true);
assert(larger[0] == 2 && larger[1] == 2);
std::puts("UI composition: PASS (menu without camera, HUD, FSR off, viewport scaling)");
}
+285
View File
@@ -0,0 +1,285 @@
Bloodborne™ | entry=0xa0 | image=91,042,036 bytes
686 imported symbols; 235,000 relocations; 42 required modules
Unavailable non-loadable metadata headers: [9]; not a byte-exact ELF reconstruction
Output: /home/deadinside/Games/bloodborne/game_files/native_probe/out
libc _init_env verified RET: True
Linked native libc: base=0x56e0000, 202 fallback exports, TLS=1184 bytes; eboot TLS=1872 bytes, fs->gs patched=17127
Linked modules: libc.prx@0x56e0000, libSceFios2.prx@0x57c0000; 238 native bindings, 524 imports left to the host runtime, fs->gs patched=17127
AppContent profile: SKU=full (probe setting), user params=[13, 0, 0, 0], mounted add-ons=0
[2026-09-27 23:34:27.738] [MANGOHUD] [info] [gpu.cpp:90] Set renderD128 as active GPU (driver=amdgpu id=1002:747e pci_dev=0000:09:00.0)
[2026-09-27 23:34:27.764] [MANGOHUD] [info] [cpu.cpp:636] hwmon: using input: /sys/class/hwmon/hwmon3/temp1_input
GPU scene scaling: AMD Radeon RX 7800 XT (RADV NAVI32) lacks blit support for D32S8
GPU scene scaling: using startup resolution 960x540 for this driver
Patches: scene 960x540; native UI 1920x1080
Patches: FPS preset uncap; 237 writes from ['Uncap FPS++']
CMake Warning (policy) at CMakeLists.txt:16 (find_package):
Policy CMP0167 is not set: The FindBoost module is removed. Run "cmake
--help-policy CMP0167" for policy details. Use the cmake_policy command to
set the policy and suppress this warning.
This warning is for project developers. Use -Wno-author or -Wno-policy to
suppress it.
CMake Warning (policy) at third_party/sirit/CMakeLists.txt:29 (set):
Policy CMP0218 is not set: The CMAKE_WARN_DEPRECATED and
CMAKE_ERROR_DEPRECATED variables are ignored. Run "cmake --help-policy
CMP0218" for policy details. Use the cmake_policy command to set the
policy and suppress this warning.
This warning is for project developers. Use -Wno-author or -Wno-policy to
suppress it.
Built /home/deadinside/Games/bloodborne/game_files/native_probe/out/bb-probe
Runtime: Bloodborne sound flag set in user/savedata/1/CUSA00207/SPRJ0005/userdata0010 (was 0)
Settings: bbport.ini
[2026-09-27 23:34:32.841] [MANGOHUD] [info] [gpu.cpp:90] Set renderD128 as active GPU (driver=amdgpu id=1002:747e pci_dev=0000:09:00.0)
[2026-09-27 23:34:32.871] [MANGOHUD] [info] [cpu.cpp:636] hwmon: using input: /sys/class/hwmon/hwmon3/temp1_input
GPU [Render.Vulkan] <Warning> vk_instance.cpp:249 operator(): Extension VK_NV_framebuffer_mixed_samples unavailable.
GPU [Render.Vulkan] <Warning> vk_instance.cpp:741 CollectImageFormatInfo: Surface format data_format=9, number_format=1 is not fully supported (vk_format=A2B10G10R10SnormPack32, missing features={ SampledImage | StorageImage | ColorAttachment | TransferSrc | TransferDst })
GPU [Render.Vulkan] <Warning> vk_instance.cpp:741 CollectImageFormatInfo: Surface format data_format=9, number_format=5 is not fully supported (vk_format=A2B10G10R10SintPack32, missing features={ SampledImage | StorageImage | ColorAttachment | TransferSrc | TransferDst })
GPU [Render.Vulkan] <Warning> vk_instance.cpp:741 CollectImageFormatInfo: Surface format data_format=13, number_format=4 is not fully supported (vk_format=R32G32B32Uint, missing features={ SampledImage | TransferSrc | TransferDst })
GPU [Render.Vulkan] <Warning> vk_instance.cpp:741 CollectImageFormatInfo: Surface format data_format=13, number_format=5 is not fully supported (vk_format=R32G32B32Sint, missing features={ SampledImage | TransferSrc | TransferDst })
GPU [Render.Vulkan] <Warning> vk_instance.cpp:741 CollectImageFormatInfo: Surface format data_format=13, number_format=7 is not fully supported (vk_format=R32G32B32Sfloat, missing features={ SampledImage | TransferSrc | TransferDst })
GPU [Render] <Warning> vk_pipeline_serialization.cpp:379 WarmUp: 814 stale pipelines were found. Consider re-generating the cache
GPU: draw preparation workers: 4
Object motion: on (4194304 vertices per frame)
Upscaler: FSR 3.1 available (on) on scene color before compute shader 000000009a9cf8a9
Overlay: menu ready (Insert or L3+R3)
GPU: window and Vulkan presenter ready; SDK 0x02000071, 327 HLE symbols
GPU: vblank follows the display refresh rate, 100 Hz
VideoOut: vblank 480 Hz, frame limit 100 FPS
Patches: 237 writes, 899 bytes applied
Mapped 93538364 bytes, 6 segments; applied 237298 relocations
Starting linked module 0 at image offset 0x56e0000; native bindings=238
Runtime: application heap API registered
Module 0 initializer returned 0
Starting linked module 1 at image offset 0x57c0000; native bindings=238
Module 1 initializer returned 0
Entering original x86-64 code at guest offset 0xa0
Runtime: _init_env returned (verified libc implementation: RET)
Runtime: direct allocation offset=0x0, size=121634816 bytes, type=0
Runtime: mapped direct memory 0x1000000000, size=121634816, prot=0x3
Runtime: semaphore id=1, attr=2, initial=0, max=1
Runtime: semaphore id=2, attr=2, initial=0, max=1
Runtime: semaphore id=3, attr=2, initial=0, max=1
Runtime: semaphore id=4, attr=2, initial=0, max=1
Runtime: semaphore id=5, attr=2, initial=0, max=1
Runtime: semaphore id=6, attr=2, initial=0, max=1
Runtime: semaphore id=7, attr=2, initial=0, max=1
Runtime: semaphore id=8, attr=2, initial=0, max=1
Runtime: semaphore id=9, attr=2, initial=0, max=1
Runtime: semaphore id=10, attr=2, initial=0, max=1
Runtime: semaphore id=11, attr=2, initial=0, max=1
Runtime: semaphore id=12, attr=2, initial=0, max=1
Runtime: semaphore id=13, attr=2, initial=0, max=1
Runtime: semaphore id=14, attr=2, initial=0, max=1
Runtime: semaphore id=15, attr=2, initial=0, max=1
Runtime: semaphore id=16, attr=2, initial=0, max=1
Runtime: semaphore id=17, attr=2, initial=0, max=1
Runtime: AppContent host provider loaded; references=1
Runtime: AppContent initialized; offline base-game profile, no mounted add-ons
Runtime: AppContent parameter 0 = 3
Runtime: AppContent parameter 1 = 13
Runtime: AppContent parameter 2 = 0
Runtime: AppContent parameter 3 = 0
Runtime: AppContent parameter 4 = 0
Runtime: semaphore id=18, attr=1, initial=1, max=1
Runtime: guest thread 'Core.Logging.BackgroundStrategy' created (stack=65536, prio=700)
Runtime: UserService initialized (user 1 logged in)
Runtime: direct allocation offset=0x7400000, size=549453824 bytes, type=0
Runtime: mapped direct memory 0x1007400000, size=549453824, prot=0x3
Runtime: direct allocation offset=0x28000000, size=289406976 bytes, type=0
Runtime: mapped direct memory 0x1028000000, size=289406976, prot=0x3
Runtime: direct allocation offset=0x39400000, size=159383552 bytes, type=0
Runtime: mapped direct memory 0x1039400000, size=159383552, prot=0x33
Runtime: direct allocation offset=0x42c00000, size=2398093312 bytes, type=3
Runtime: mapped direct memory 0x1042c00000, size=2398093312, prot=0x33
Runtime: direct allocation offset=0xd1b00000, size=929038336 bytes, type=3
Runtime: mapped direct memory 0x10d1b00000, size=929038336, prot=0x33
Runtime: direct allocation offset=0x109100000, size=10485760 bytes, type=0
Runtime: mapped direct memory 0x1109100000, size=10485760, prot=0x3
Runtime: direct allocation offset=0x109b00000, size=6291456 bytes, type=0
Runtime: mapped direct memory 0x1109b00000, size=6291456, prot=0x3
Runtime: mapped flexible memory 0x110a100000, size=50331648, prot=0x3
Runtime: direct allocation offset=0x10a100000, size=14680064 bytes, type=0
Runtime: mapped direct memory 0x110d100000, size=14680064, prot=0x3
Runtime: mapped flexible memory 0x110df00000, size=85983232, prot=0x3
Runtime: direct allocation offset=0x10af00000, size=5242880 bytes, type=0
Runtime: mapped direct memory 0x1113100000, size=5242880, prot=0x33
Runtime: direct allocation offset=0x10b400000, size=8388608 bytes, type=3
Runtime: mapped direct memory 0x1113600000, size=8388608, prot=0x33
Runtime: direct allocation offset=0x10bc00000, size=9437184 bytes, type=0
Runtime: mapped direct memory 0x1113e00000, size=9437184, prot=0x3
Runtime: direct allocation offset=0x10c500000, size=310378496 bytes, type=0
Runtime: mapped direct memory 0x1114700000, size=310378496, prot=0x3
Runtime: mapped flexible memory 0x1126f00000, size=33554432, prot=0x3
Runtime: mapped flexible memory 0x1128f00000, size=100663296, prot=0x3
Runtime: mapped flexible memory 0x112ef00000, size=33554432, prot=0x3
Runtime: mapped flexible memory 0x1130f00000, size=33554432, prot=0x3
Runtime: semaphore id=19, attr=1, initial=1, max=1
Runtime: guest thread 'FD4JobWorker:0' created (stack=131072, prio=700)
Runtime: semaphore id=20, attr=2, initial=0, max=1
Runtime: semaphore id=21, attr=2, initial=0, max=1
Runtime: guest thread 'EzWorkPool_TypeA0' created (stack=65536, prio=700)
Runtime: guest thread 'EzWorkPool_TypeA1' created (stack=65536, prio=700)
Runtime: guest thread 'EzWorkPool_TypeA2' created (stack=65536, prio=700)
Runtime: guest thread 'EzWorkPool_TypeA3' created (stack=65536, prio=700)
Runtime: semaphore id=22, attr=2, initial=0, max=1
Runtime: semaphore id=23, attr=2, initial=0, max=1
Runtime: guest thread 'EzWorkPool_TypeB1' created (stack=65536, prio=700)
Runtime: guest thread 'EzWorkPool_TypeB2' created (stack=65536, prio=700)
Runtime: guest thread 'EzWorkPool_TypeB3' created (stack=65536, prio=700)
Runtime: semaphore id=24, attr=2, initial=0, max=1
Runtime: semaphore id=25, attr=2, initial=0, max=1
Runtime: guest thread 'EzWorkPool_TypeC4' created (stack=65536, prio=700)
Runtime: semaphore id=26, attr=2, initial=0, max=1
Runtime: semaphore id=27, attr=2, initial=0, max=1
Runtime: guest thread 'EzWorkPool_TypeD1' created (stack=65536, prio=700)
Runtime: semaphore id=28, attr=2, initial=0, max=1
Runtime: guest thread 'MOMainThread' created (stack=65536, prio=700)
Runtime: Ajm context initialized (ATRAC9 via LibAtrac9)
Runtime: audio backend pipewire
Runtime: audio port 0 opened (type 0, 8 ch, float, 256 frames)
Runtime: audio port 8 opened (type 1, 8 ch, float, 256 frames)
Runtime: semaphore id=29, attr=1, initial=0, max=65535
Runtime: semaphore id=30, attr=1, initial=0, max=65535
Runtime: guest thread 'FMOD mixer thread' created (stack=65536, prio=257)
Runtime: semaphore id=31, attr=1, initial=0, max=65535
Runtime: guest thread 'FMOD AudioOut thread' created (stack=65536, prio=256)
Runtime: semaphore id=32, attr=1, initial=0, max=65535
Runtime: guest thread 'FMOD stream thread' created (stack=65536, prio=260)
Runtime: guest thread 'GXRenderThread' created (stack=131072, prio=700)
Runtime: direct allocation offset=0x11ed00000, size=131072 bytes, type=3
Runtime: mapped direct memory 0x1132f00000, size=131072, prot=0x33
Runtime: semaphore id=33, attr=1, initial=0, max=5
Runtime: guest thread 'GXWorker:1' created (stack=131072, prio=700)
Runtime: guest thread 'GXWorker:2' created (stack=131072, prio=700)
Runtime: guest thread 'GXWorker:3' created (stack=131072, prio=700)
Runtime: guest thread 'GXWorker:4' created (stack=131072, prio=700)
Runtime: guest thread 'GXWorker:5' created (stack=131072, prio=700)
Runtime: semaphore id=34, attr=1, initial=232, max=232
Runtime: guest thread 'Core.Res.PostProcessor_0' created (stack=65536, prio=700)
Runtime: guest thread 'Core.Res.PostProcessor_1' created (stack=65536, prio=700)
Runtime: guest thread 'Core.Res.PostProcessor_2' created (stack=65536, prio=700)
Runtime: guest thread 'Core.Res.CacheableFileLoader' created (stack=65536, prio=700)
Runtime: guest thread 'Core.Res.FileLoader' created (stack=65536, prio=700)
Runtime: guest thread 'Core.Res.TaskManager' created (stack=65536, prio=700)
Runtime: semaphore id=35, attr=1, initial=0, max=1
Runtime: semaphore id=36, attr=1, initial=0, max=1
Runtime: semaphore id=37, attr=1, initial=0, max=1
Runtime: semaphore id=38, attr=1, initial=0, max=1
Runtime: semaphore id=39, attr=1, initial=0, max=1
Runtime: semaphore id=40, attr=1, initial=0, max=1
Runtime: semaphore id=41, attr=1, initial=0, max=1
Runtime: semaphore id=42, attr=1, initial=0, max=1
Runtime: semaphore id=43, attr=1, initial=0, max=1
Runtime: semaphore id=44, attr=1, initial=0, max=1
Runtime: semaphore id=45, attr=1, initial=0, max=1
Runtime: semaphore id=46, attr=1, initial=0, max=1
Runtime: semaphore id=47, attr=1, initial=0, max=12
Runtime: guest thread 'HavokWorkerThread' created (stack=131072, prio=700)
Runtime: guest thread 'HavokWorkerThread' created (stack=131072, prio=700)
Runtime: guest thread 'HavokWorkerThread' created (stack=131072, prio=700)
Runtime: guest thread 'HavokWorkerThread' created (stack=131072, prio=700)
Runtime: guest thread 'HavokWorkerThread' created (stack=131072, prio=700)
Runtime: semaphore id=48, attr=1, initial=0, max=1000
Runtime: semaphore id=49, attr=1, initial=0, max=1000
Runtime: semaphore id=50, attr=1, initial=0, max=1000
Runtime: semaphore id=51, attr=1, initial=0, max=1000
Runtime: semaphore id=52, attr=1, initial=0, max=1000
Runtime: semaphore id=53, attr=1, initial=0, max=1000
Runtime: semaphore id=54, attr=1, initial=0, max=1000
Runtime: semaphore id=55, attr=1, initial=0, max=1000
Runtime: semaphore id=56, attr=1, initial=0, max=1000
Runtime: semaphore id=57, attr=1, initial=0, max=1000
Runtime: semaphore id=58, attr=1, initial=0, max=1000
Runtime: semaphore id=59, attr=1, initial=0, max=1000
Runtime: guest thread 'SpClothVertexUpdate' created (stack=65536, prio=700)
Runtime: guest thread 'SpClothVertexUpdate' created (stack=65536, prio=700)
Runtime: guest thread 'SpClothVertexUpdate' created (stack=65536, prio=700)
Runtime: guest thread 'SpClothVertexUpdate' created (stack=65536, prio=700)
Runtime: guest thread 'SpClothVertexUpdate' created (stack=65536, prio=700)
Runtime: guest thread 'SpClothVertexUpdate' created (stack=65536, prio=700)
Runtime: guest thread 'FaceGenMan' created (stack=65536, prio=700)
Runtime: system module 0xad loaded (host implementation)
Runtime: system module 0x83 loaded (host implementation)
Runtime: PlayGo initialized; 3 chunks, all installed locally
Runtime: guest thread 'DLUserPreferenceNotifyServiceIm' created (stack=65536, prio=700)
Runtime: pad opened for user 1 (SDL gamepad or keyboard)
Runtime: gamepad connected: 8BitDo Ultimate 2 Wireless Controller
Runtime: open(/app0/dvdroot_ps4/menu/logo.tpf.dcx) -> not found
Runtime: system module 0x9d loaded (host implementation)
Runtime: system module 0x1c loaded (host implementation)
Runtime: system module 0xb5 loaded (host implementation)
Runtime: system module 0x19 loaded (host implementation)
Runtime: system module 0xb2 loaded (host implementation)
Runtime: guest thread 'NexusRevolution Event' created (stack=65536, prio=700)
Runtime: UserService initialized (user 1 logged in)
Runtime: guest thread 'NexusRevolution Socket' created (stack=65536, prio=700)
Runtime: system module 0x1e loaded (host implementation)
Runtime: guest thread 'NexusRevolution Main' created (stack=65536, prio=700)
Runtime: guest thread 'FrpgSaveLoadMan' created (stack=65536, prio=700)
Runtime: semaphore id=60, attr=1, initial=1, max=1
Runtime: guest thread 'SLSession' created (stack=65536, prio=700)
Runtime: guest thread 'CSTrophy::AsyncUpdateThread' created (stack=65536, prio=700)
UI: native composition 1920x1080 (scene 960x540), independent of FSR history
Runtime: SystemService: splash screen hidden
Runtime: save data 'SPRJ0005' mounted at /savedata0 (existing, read-only)
Runtime: open(/app0/dvdroot_ps4/parts/wp_a_6010.partsbnd.dcx) -> not found
Runtime: open(/app0/dvdroot_ps4/parts/lg_a_9700.partsbnd.dcx) -> not found
Runtime: open(/app0/dvdroot_ps4/parts/lg_a_9700_l.partsbnd.dcx) -> not found
Runtime: semaphore id=61, attr=1, initial=0, max=65535
Runtime: semaphore id=62, attr=1, initial=0, max=65535
Runtime: guest thread 'FMOD nonblocking thread (0)' created (stack=65536, prio=265)
Runtime: semaphore id=63, attr=1, initial=0, max=65535
Runtime: guest thread 'FMOD file thread' created (stack=65536, prio=265)
Runtime: save data 'SPRJ0005' mounted at /savedata0 (existing, read-only)
Runtime: save data 'SPRJ0005' mounted at /savedata0 (existing, read-only)
Runtime: semaphore id=64, attr=2, initial=0, max=1
Runtime: semaphore id=65, attr=2, initial=0, max=1
Runtime: guest thread 'CSChrThread0' created (stack=65536, prio=700)
Runtime: guest thread 'CSChrThread1' created (stack=65536, prio=700)
Runtime: guest thread 'CSChrThread2' created (stack=65536, prio=700)
Runtime: guest thread 'CSChrThread3' created (stack=65536, prio=700)
Runtime: guest thread 'CSChrThread4' created (stack=65536, prio=700)
Runtime: guest thread 'CSChrThread5' created (stack=65536, prio=700)
Runtime: semaphore id=66, attr=2, initial=0, max=1
Runtime: semaphore id=67, attr=2, initial=0, max=1
Runtime: guest thread 'CSChrBackInitialize1' created (stack=65536, prio=700)
Runtime: semaphore id=68, attr=2, initial=0, max=1
Runtime: semaphore id=69, attr=2, initial=0, max=1
Runtime: guest thread 'CSClothThread0' created (stack=65536, prio=700)
Runtime: guest thread 'CSClothThread1' created (stack=65536, prio=700)
Runtime: guest thread 'CSClothThread2' created (stack=65536, prio=700)
Runtime: guest thread 'CSClothThread3' created (stack=65536, prio=700)
Runtime: guest thread 'CSClothThread4' created (stack=65536, prio=700)
Runtime: guest thread 'CSClothThread5' created (stack=65536, prio=700)
GPU [Render.Vulkan] <Warning> vk_runtime.cpp:57 SanitizeCopyLayers: Coercing copy source layers 1 and destination layers 2 to minimum.
GPU [Render.Vulkan] <Warning> vk_runtime.cpp:57 SanitizeCopyLayers: Coercing copy source layers 2 and destination layers 3 to minimum.
GPU [Render.Vulkan] <Warning> vk_runtime.cpp:57 SanitizeCopyLayers: Coercing copy source layers 3 and destination layers 4 to minimum.
GPU [Render.Vulkan] <Warning> vk_runtime.cpp:57 SanitizeCopyLayers: Coercing copy source layers 4 and destination layers 5 to minimum.
GPU [Render.Vulkan] <Warning> vk_runtime.cpp:57 SanitizeCopyLayers: Coercing copy source layers 5 and destination layers 6 to minimum.
== Stall during rendering at flush 1
Runtime: open(/app0/dvdroot_ps4/map/m34/m34_0000.tpfbhd.patch) -> not found
Runtime: open(/app0/dvdroot_ps4/event/m34.emeld.dcx) -> not found
Runtime: open(/app0/dvdroot_ps4/event/m34.emevd.dcx) -> not found
Runtime: open(/app0/dvdroot_ps4/map/m34/m34_9999.tpf.dcx) -> not found
Runtime: open(/app0/dvdroot_ps4/map/m34/m34_0000.tpfbdt.patch) -> not found
Runtime: open(/app0/dvdroot_ps4/map/onav/m34_00_00_00.onav.dcx) -> not found
Runtime: open(/app0/dvdroot_ps4/map/m34_00_00_00/h34_00_00_00.hkxbhd.patch) -> not found
Runtime: open(/app0/dvdroot_ps4/map/m34_00_00_00/h34_00_00_00.hkxbdt.patch) -> not found
Runtime: open(/app0/dvdroot_ps4/map/m35/m35_0000.tpfbhd.patch) -> not found
Runtime: open(/app0/dvdroot_ps4/map/m35/m35_0000.tpfbdt.patch) -> not found
Runtime: save data 'SPRJ0005' mounted at /savedata0 (existing)
Runtime: save data 'SPRJ0005' mounted at /savedata0 (existing)
Upscaler: FSR 3 context 960x540 -> 1920x1080 (tonemapped frame), 55.6 MB
Runtime: save data 'SPRJ0005' mounted at /savedata0 (existing)
Runtime: save data 'SPRJ0005' mounted at /savedata0 (existing)
Runtime: save data 'SPRJ0005' mounted at /savedata0 (existing)
Runtime: save data 'SPRJ0005' mounted at /savedata0 (existing)
Runtime: open(/app0/dvdroot_ps4/event/m35.emeld.dcx) -> not found
Runtime: open(/app0/dvdroot_ps4/event/m35.emevd.dcx) -> not found
Runtime: open(/app0/dvdroot_ps4/map/m35/m35_9999.tpf.dcx) -> not found
Runtime: open(/app0/dvdroot_ps4/map/onav/m35_00_00_00.onav.dcx) -> not found
Runtime: open(/app0/dvdroot_ps4/map/m35_00_00_00/h35_00_00_00.hkxbhd.patch) -> not found
Runtime: open(/app0/dvdroot_ps4/map/m35_00_00_00/h35_00_00_00.hkxbdt.patch) -> not found
Runtime: open(/app0/dvdroot_ps4/map/m36/m36_0000.tpfbhd.patch) -> not found
Runtime: open(/app0/dvdroot_ps4/map/m36/m36_0000.tpfbdt.patch) -> not found