mirror of
https://github.com/BillyOutlast/bloodborne_pc.git
synced 2026-10-12 02:25:51 +00:00
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:
co-authored by
Claude Opus 5.5
parent
ac9fe90ae5
commit
37c8eacb8b
@@ -3,3 +3,7 @@ __pycache__/
|
||||
user/
|
||||
*.mp4
|
||||
bbport.ini
|
||||
fsr4_shaders/
|
||||
pgo/
|
||||
*.png
|
||||
last_run.log
|
||||
|
||||
+147
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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 шлейфов у него меньше.
|
||||
@@ -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`
|
||||
|
||||
Тестирование:
|
||||
- Сборка: ✅ успешно
|
||||
- Требуется проверка в игре
|
||||
@@ -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. Генерация кадров
|
||||
|
||||
|
||||
@@ -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 в игровых сценах. Визуально исчезновение шлейфа
|
||||
на пиле ещё нужно подтвердить.
|
||||
@@ -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 получает этот вектор, в остальных местах — вектор камеры.
|
||||
- Позиции сохраняются только для движущихся отрисовок: тех, у которых изменились константы вершинного шейдера (матрица объекта, кости). Статичная геометрия памяти не тратит.
|
||||
- Это учитывает любую анимацию, которую считает вершинный шейдер: скиннинг, оружие, ткань, врагов.
|
||||
|
||||
Загрузись, пожалуйста, в игру и напиши «готов». Я проверю лог: сколько отрисовок движется и у скольких нашлись позиции прошлого кадра. Потом побегай с пилой на спине. Для сравнения я смогу выключать векторы объектов на ходу, остаются только векторы камеры.
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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{};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(®s.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(®s.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(®s.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(®s.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:
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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)];
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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, ¶m) != 0) {
|
||||
setpriority(PRIO_PROCESS, static_cast<id_t>(gettid()), 19);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace BbThreads
|
||||
@@ -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},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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, ®ion);
|
||||
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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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). */
|
||||
|
||||
@@ -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(); }
|
||||
}
|
||||
|
||||
@@ -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)");
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)");
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user