mirror of
https://github.com/Liondarkz/issn_pc.git
synced 2026-11-07 04:36:01 -10:00
Compare commits
144
Commits
windows-v0.3.2
...
issn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44e475870d | ||
|
|
fd5437b3b2 | ||
|
|
665ec7e084 | ||
|
|
492930ef7a | ||
|
|
a4d7593d11 | ||
|
|
3b62a1dff6 | ||
|
|
e23073cecc | ||
|
|
369cdc646c | ||
|
|
6b38995761 | ||
|
|
43bf6f88bb | ||
|
|
47492dded2 | ||
|
|
e1be566815 | ||
|
|
9f962de970 | ||
|
|
a990c28d2e | ||
|
|
85ffb45a50 | ||
|
|
aa103d9b76 | ||
|
|
9ee3ed9ab7 | ||
|
|
0a1b680385 | ||
|
|
0e92f69731 | ||
|
|
b9b3859b64 | ||
|
|
c0ed9951fe | ||
|
|
afe0ec2587 | ||
|
|
fd7d3cca10 | ||
|
|
c9cb7070eb | ||
|
|
f4612c5290 | ||
|
|
82e8e9a0ff | ||
|
|
4156e98c52 | ||
|
|
87a4aaaa55 | ||
|
|
f5d6c438c6 | ||
|
|
200a34f1e7 | ||
|
|
05b649633d | ||
|
|
17c093c829 | ||
|
|
1c7fe5348d | ||
|
|
05f4926d0b | ||
|
|
48a3179cae | ||
|
|
86af332e2e | ||
|
|
4700403c69 | ||
|
|
30782e12f6 | ||
|
|
f0bacead70 | ||
|
|
4ee1f0ef8b | ||
|
|
986f8e9b71 | ||
|
|
590417d81e | ||
|
|
02a538ea3d | ||
|
|
a53de5f8ac | ||
|
|
67836e533a | ||
|
|
ecfd5be52e | ||
|
|
d22c4ec6ce | ||
|
|
ccd8d6b802 | ||
|
|
487d87a2f4 | ||
|
|
32f2b7fc17 | ||
|
|
dca8aba1e8 | ||
|
|
2b7d836732 | ||
|
|
af5169d02f | ||
|
|
3737f2f4b3 | ||
|
|
d2568a8e20 | ||
|
|
b2e5a8245c | ||
|
|
e2b59a2119 | ||
|
|
ce1d7a488a | ||
|
|
8f0dd133f7 | ||
|
|
7b60ff3600 | ||
|
|
a773bf9e8b | ||
|
|
53f6813f05 | ||
|
|
530db36de4 | ||
|
|
4057c119d2 | ||
|
|
6bfbfa001d | ||
|
|
c96347f766 | ||
|
|
890f9a15aa | ||
|
|
b5450660a1 | ||
|
|
48e9d0e8b1 | ||
|
|
16fb9ec80b | ||
|
|
afb6bd09cb | ||
|
|
73610ee7c1 | ||
|
|
0ed6f29d63 | ||
|
|
70657ac9d7 | ||
|
|
b1e890cc3a | ||
|
|
1636b479df | ||
|
|
4744f90334 | ||
|
|
0b2352a1eb | ||
|
|
17c849193c | ||
|
|
9fd663d533 | ||
|
|
4b1782382b | ||
|
|
b18a0b8440 | ||
|
|
a62fbc2107 | ||
|
|
f7aa5e4038 | ||
|
|
bb15a12f62 | ||
|
|
a918386487 | ||
|
|
6b4e87af11 | ||
|
|
70a697afbe | ||
|
|
ec196970ae | ||
|
|
d2134843b7 | ||
|
|
1ff2d72fe3 | ||
|
|
83821a8b6d | ||
|
|
ac3d0f1f55 | ||
|
|
592733a257 | ||
|
|
f452fed351 | ||
|
|
0df09103d6 | ||
|
|
23ea744497 | ||
|
|
318e35057a | ||
|
|
3d3087d26f | ||
|
|
cd2533e799 | ||
|
|
c946b25394 | ||
|
|
a88a1dd060 | ||
|
|
ee9f2dc7b2 | ||
|
|
2fda4b3b77 | ||
|
|
8a166c4820 | ||
|
|
bfe4bebae4 | ||
|
|
ef29f91517 | ||
|
|
75b7c09809 | ||
|
|
9c1f621909 | ||
|
|
29692214f1 | ||
|
|
91f677775d | ||
|
|
24246500d0 | ||
|
|
a8f48461c4 | ||
|
|
740a34f4d8 | ||
|
|
b6a53ff4c5 | ||
|
|
c21fa9eb58 | ||
|
|
e9ad222302 | ||
|
|
ba8de44ad6 | ||
|
|
dc49c92f22 | ||
|
|
7903b08dbf | ||
|
|
6ba97e1ed4 | ||
|
|
ef7ee87844 | ||
|
|
cfb2bf0255 | ||
|
|
d71b3906c3 | ||
|
|
a2b14ea510 | ||
|
|
b9e71a4531 | ||
|
|
eb3563b088 | ||
|
|
a17c45bd7c | ||
|
|
18abe1c866 | ||
|
|
54bf76369b | ||
|
|
de0e30ead0 | ||
|
|
dc3dc52c6e | ||
|
|
c2a6853e31 | ||
|
|
b0682899b8 | ||
|
|
b562e53e07 | ||
|
|
e7a3ca8f18 | ||
|
|
475ca8f771 | ||
|
|
38f9a9e8ac | ||
|
|
753cdd9b82 | ||
|
|
a7a72f436e | ||
|
|
21b3cf28ef | ||
|
|
7e3814ff30 | ||
|
|
b5255093ae | ||
|
|
8271ee2ecc |
No files matched your search
@@ -0,0 +1,3 @@
|
||||
# The "Sponsor" button on the repository page (GitHub reads it from the default branch, issn).
|
||||
github: Liondarkz
|
||||
ko_fi: liondarkz
|
||||
@@ -9,6 +9,8 @@ vez).
|
||||
> Projeto sem relação com a Sony Interactive Entertainment ou a Sucker Punch Productions.
|
||||
> inFAMOUS é marca da Sony Interactive Entertainment.
|
||||
|
||||
💬 **Discord:** https://discord.gg/y82HQ5uTQg (novidades, ajuda e builds de teste / news, help and test builds)
|
||||
|
||||
[English below](#english)
|
||||
|
||||
## Download
|
||||
@@ -20,8 +22,21 @@ assim mesmo (o programa não é assinado).
|
||||
|
||||
Para atualizar, extraia o zip novo por cima da pasta antiga: saves e configurações ficam.
|
||||
|
||||
**Requisitos:** Windows 10 (1903+) ou 11 de 64 bits, processador com AVX, placa de vídeo com
|
||||
Vulkan 1.3 e driver atualizado. Versões do jogo: CUSA00223 (EUA) e CUSA00004 (Europa).
|
||||
**Requisitos:** Windows 10 (1903+) ou 11 de 64 bits, placa de vídeo com Vulkan 1.3 e driver
|
||||
atualizado. Versões do jogo: CUSA00223 (EUA) e CUSA00004 (Europa), com a atualização 01.07.
|
||||
|
||||
| | Mínimo (1080p, ~30-45 FPS) | Recomendado (1080p, 60 FPS) | Testado (1080p, ~100 FPS) |
|
||||
|---|---|---|---|
|
||||
| Processador | Intel Core 4ª geração (i5-4xxx) / AMD Ryzen 1000 / FX-8300 | Intel Core i5 8ª geração / Ryzen 5 1600 | Ryzen 7 3700X |
|
||||
| Placa de vídeo | GTX 1060 6 GB / RX 580 8 GB | GTX 1070 / RTX 2060 / RX 6600 | RX 9060 XT 16 GB |
|
||||
| Memória (RAM) | 12 GB | 16 GB | 32 GB |
|
||||
| Memória de vídeo | 6 GB | 8 GB | 16 GB |
|
||||
|
||||
O processador precisa de AVX (Intel 2ª geração / AMD FX ou mais novo); instruções que faltam em
|
||||
alguns modelos (MOVBE, F16C, BMI1, LZCNT, SSE4A) o port traduz sozinho. Em 1440p o jogo reserva
|
||||
mais memória: 16 GB de RAM; em 4K, 32 GB. FSR 4 precisa de Radeon RX 7000 / 9000; XeSS roda em
|
||||
Arc, Radeon RX 5000+ e GeForce GTX 10+; DLSS precisa de GeForce RTX. Os números do mínimo e do
|
||||
recomendado são estimativas do próprio launcher (Gráficos > Seu PC); só o PC testado foi medido.
|
||||
|
||||
## O que tem
|
||||
|
||||
@@ -67,7 +82,12 @@ playable, but expect bugs. The first start prepares the shaders once (a few minu
|
||||
|
||||
Download `issn-windows.zip` from [Releases](../../releases), extract it anywhere and open
|
||||
`inFAMOUS Second Son.exe`, choose the game folder (with `eboot.bin`) and click PLAY. Windows 10
|
||||
(1903+) / 11 64-bit, an AVX CPU and a Vulkan 1.3 GPU are needed. Features: axylpup's graphics core,
|
||||
(1903+) / 11 64-bit and a Vulkan 1.3 GPU are needed. Minimum (1080p, ~30-45 FPS): Intel Core 4th
|
||||
gen / Ryzen 1000 / FX-8300, GTX 1060 6 GB / RX 580 8 GB, 12 GB RAM. Recommended (1080p, 60 FPS):
|
||||
Core i5 8th gen / Ryzen 5 1600, GTX 1070 / RTX 2060 / RX 6600, 16 GB RAM (32 GB for 4K). The CPU
|
||||
needs AVX; the port translates the missing MOVBE / F16C / BMI1 / LZCNT / SSE4A. Minimum and
|
||||
recommended are the launcher's estimates (Graphics > Your PC); measured: Ryzen 7 3700X + RX 9060 XT,
|
||||
about 100 FPS in the city. Features: axylpup's graphics core,
|
||||
the game's AA, FSR 4 (included) or DLSS (bring NVIDIA's Streamline 2.14.16 files into `bin\dlss`),
|
||||
shaders prepared on the first start from a shipped seed (shader list and settings, no game
|
||||
code; new ones compile in the background), internal resolution with FSR 4 upscaling, performance
|
||||
@@ -79,9 +99,16 @@ Add to Steam. Building: [packaging/windows/README.md](packaging/windows/README.m
|
||||
|
||||
- Gráficos / graphics: o build do shadPS4 do **axylpup** para o Second Son
|
||||
([axylpup/shadPS4](https://github.com/axylpup/shadPS4)), sobre o
|
||||
[shadPS4](https://github.com/shadps4-emu/shadPS4); trabalhos anteriores no inFAMOUS de
|
||||
**Ayoub** (shadPS4-AYOUB1080p) e **HayatoG** (iss_pc). Mouse 1:1, layout de teclado e
|
||||
sensor de movimento sintético: design do axylpup.
|
||||
[shadPS4](https://github.com/shadps4-emu/shadPS4); trabalho anterior no inFAMOUS de
|
||||
**Ayoub** (shadPS4-AYOUB1080p). Mouse 1:1 e layout de teclado: design do axylpup. Sensor de
|
||||
movimento sintético: o modelo atual é do axylpup; o modo sensor para controles sem giroscópio
|
||||
(L3+R3, inclinar com o analógico, sacudir com um botão) veio antes no iss_pc do HayatoG (13/10).
|
||||
- **HayatoG** ([iss_pc](https://github.com/HayatoG/iss_pc)), o outro port do inFAMOUS para PC
|
||||
(mesma base gow3_pc): core de vídeo/GPU (merge three-way: readbacks precisos, compute
|
||||
pipelines fora da thread da GPU, correção do índice do shader cache, permutações de shader,
|
||||
write merge, atalho de região com "algum bit marcado"), distância de desenho (view hooks e os
|
||||
valores 0.6 / 0.8), respostas de PS4 base offline, correções de travamento no Windows (read
|
||||
fault especulativo, pool do guest, mutex do Wwise) e nomes de NIDs.
|
||||
- Base do port / port base: [gow3_pc](https://github.com/FelipePrado0/gow3_pc) (Felipe Prado),
|
||||
que vem do [bloodborne_pc](https://github.com/Supermedo/bloodborne_pc) (deadinside28,
|
||||
Supermedo), com cuesta4 e a equipe do shadPS4. Documentação herdada: [docs/base](docs/base/README.md).
|
||||
|
||||
@@ -3,7 +3,9 @@ set -euo pipefail
|
||||
cd -- "$(dirname -- "$0")"
|
||||
mkdir -p out
|
||||
# Windows: MSYS2 CLANG64 (packages in packaging/windows/README.md). The GPU library is static
|
||||
# there and CMake links issn-probe.exe as well; no PGO (GCC-only flags), LTO off by default.
|
||||
# there and CMake links issn-probe.exe as well. LTO on by default (ISSN_LTO=OFF to build without);
|
||||
# PGO: ISSN_PGO=generate builds the instrumented executable for a training run, and the default
|
||||
# uses pgo-windows/issn-windows.profdata when it is there (ISSN_PGO=off to build without).
|
||||
case $(uname -s) in
|
||||
MINGW*|MSYS*)
|
||||
# shellcheck source=msys2-env.sh
|
||||
@@ -29,9 +31,14 @@ MINGW*|MSYS*)
|
||||
git -C gpu/third_party/fsr-vulkan apply "$PWD/$patch"
|
||||
fi
|
||||
done
|
||||
cmake -S gpu -B out/gpu -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DISSN_PGO=off \
|
||||
-DISSN_LTO="${ISSN_LTO:-OFF}" -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ >/dev/null
|
||||
echo "GPU library and loader: LTO ${ISSN_LTO:-OFF}"
|
||||
pgo=${ISSN_PGO:-}
|
||||
if [[ -z $pgo ]]; then
|
||||
if [[ -f pgo-windows/issn-windows.profdata ]]; then pgo=use; else pgo=off; fi
|
||||
fi
|
||||
cmake -S gpu -B out/gpu -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DISSN_PGO="$pgo" \
|
||||
-DISSN_PGO_FILE="$PWD/pgo-windows/issn-windows.profdata" \
|
||||
-DISSN_LTO="${ISSN_LTO:-ON}" -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ >/dev/null
|
||||
echo "GPU library and loader: LTO ${ISSN_LTO:-ON}, PGO $pgo"
|
||||
if ! ninja -C out/gpu issn-probe issn-gpu-capabilities issn-play > out/gpu-build.log 2>&1; then
|
||||
grep -v '^\[' out/gpu-build.log | tail -40 >&2
|
||||
echo 'Build failed (full log: out/gpu-build.log)' >&2; exit 1
|
||||
@@ -41,6 +48,31 @@ MINGW*|MSYS*)
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
# Linux defaults; macOS replaces them below.
|
||||
no_pie=(-no-pie) export_dynamic=-rdynamic origin='$ORIGIN' heap=() cmake_platform=()
|
||||
# macOS: an x86_64 build that Rosetta 2 runs, Vulkan through MoltenVK. Dependencies come from
|
||||
# packaging/macos/build-deps-x86_64.sh (ISSN_MAC_DEPS), never from the arm64 Homebrew.
|
||||
if [[ $(uname -s) == Darwin ]]; then
|
||||
deps=${ISSN_MAC_DEPS:-$PWD/out/macos-deps}
|
||||
[[ -f $deps/lib/libMoltenVK.dylib ]] || { echo "macOS dependencies missing: run packaging/macos/build-deps-x86_64.sh" >&2; exit 1; }
|
||||
export MACOSX_DEPLOYMENT_TARGET=13.0 PATH="$deps/bin:$PATH"
|
||||
export PKG_CONFIG_LIBDIR="$deps/lib/pkgconfig:$deps/share/pkgconfig"
|
||||
printf '#!/bin/sh\nexec clang -arch x86_64 -mmacosx-version-min=13.0 "$@"\n' > out/cc-x86_64
|
||||
chmod +x out/cc-x86_64
|
||||
CC=$PWD/out/cc-x86_64
|
||||
# The guest packs pointers into 40 bits: mimalloc, patched to map in 32-64 GiB, replaces
|
||||
# the system malloc (near 128 TiB). It must come first on the link line.
|
||||
no_pie=() export_dynamic=-Wl,-export_dynamic,-rpath,"$deps/lib" origin=@loader_path
|
||||
heap=("$deps/lib/mimalloc-3.5/mimalloc.o" src/compat_mac.c)
|
||||
# shadPS4 points the Vulkan loader at the executable's directory (VK_DRIVER_FILES).
|
||||
printf '{"file_format_version":"1.0.0","ICD":{"library_path":"%s","api_version":"1.4.0","is_portability_driver":true}}\n' \
|
||||
"$deps/lib/libMoltenVK.dylib" > out/MoltenVK_icd.json
|
||||
boost=$(ls -d /opt/homebrew/lib/cmake/Boost-* 2>/dev/null | tail -1)
|
||||
cmake_platform=(-DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=13.0
|
||||
-DCMAKE_PREFIX_PATH="$deps" -DCMAKE_IGNORE_PREFIX_PATH=/opt/homebrew
|
||||
-DBoost_DIR="$boost" -DCMAKE_POLICY_VERSION_MINIMUM=3.5)
|
||||
ISSN_PGO=${ISSN_PGO:-off} ISSN_LTO=${ISSN_LTO:-OFF}
|
||||
fi
|
||||
if [[ -z ${CC:-} ]]; then
|
||||
CC=$(command -v cc || command -v gcc || true)
|
||||
if [[ -z $CC ]]; then
|
||||
@@ -86,7 +118,7 @@ for patch in gpu/patches/fsr-vulkan/*.patch; do
|
||||
fi
|
||||
done
|
||||
cmake -S gpu -B out/gpu -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DISSN_PGO="$pgo" \
|
||||
-DISSN_LTO="${ISSN_LTO:-ON}" -DISSN_PGO_DIR="$PWD/pgo" >/dev/null
|
||||
-DISSN_LTO="${ISSN_LTO:-ON}" -DISSN_PGO_DIR="$PWD/pgo" ${cmake_platform[@]+"${cmake_platform[@]}"} >/dev/null
|
||||
echo "GPU library: PGO $pgo, LTO ${ISSN_LTO:-ON}"
|
||||
# A failed GPU build must stop here: an older libissngpu.so would otherwise be used silently.
|
||||
if ! ninja -C out/gpu issngpu > out/gpu-build.log 2>&1; then
|
||||
@@ -94,7 +126,7 @@ if ! ninja -C out/gpu issngpu > out/gpu-build.log 2>&1; then
|
||||
echo 'GPU library build failed (full log: out/gpu-build.log)' >&2; exit 1
|
||||
fi
|
||||
# $ORIGIN/gpu: packaged copies keep the library next to the binary without patching it.
|
||||
gpu=(-Lout/gpu -lissngpu -Wl,-rpath,'$ORIGIN/gpu' -Wl,-rpath,"$PWD/out/gpu" -rdynamic)
|
||||
gpu=(-Lout/gpu -lissngpu -Wl,-rpath,"$origin/gpu" -Wl,-rpath,"$PWD/out/gpu" "$export_dynamic")
|
||||
runtime=(src/runtime*.c)
|
||||
# Third-party decoders: compiled once, without this project's -Werror policy.
|
||||
atrac9=(third_party/LibAtrac9/C/src/*.c)
|
||||
@@ -105,10 +137,10 @@ if [[ ! -f out/libatrac9.a || -n $(find third_party/LibAtrac9/C/src -newer out/l
|
||||
fi
|
||||
# isa_emulate runs inside guest code: no XMM/YMM.
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -mgeneral-regs-only -c src/isa_emulate.c -o out/isa_emulate.o
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -no-pie "${includes[@]}" -I. -Isrc src/probe.c "${runtime[@]}" src/cpu_check.c src/vulkan_smoke.c out/isa_emulate.o src/isa_rewrite.c out/libatrac9.a -lm "${gpu[@]}" "${libraries[@]}" -o out/issn-probe
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread ${no_pie[@]+"${no_pie[@]}"} "${includes[@]}" -I. -Isrc ${heap[@]+"${heap[@]}"} src/probe.c "${runtime[@]}" src/cpu_check.c src/vulkan_smoke.c out/isa_emulate.o src/isa_rewrite.c out/libatrac9.a -lm "${gpu[@]}" "${libraries[@]}" -o out/issn-probe
|
||||
echo "Built $PWD/out/issn-probe"
|
||||
# GPU check for run.sh (live_resolution=auto): links only the Vulkan loader.
|
||||
"$CC" -std=c11 -O2 -Wall -Wextra -Werror -Igpu/shadps4 tools/gpu_capabilities.c "${libraries[@]}" -o out/issn-gpu-capabilities
|
||||
"$CC" -std=c11 -O2 -Wall -Wextra -Werror "${includes[@]}" -Igpu/shadps4 tools/gpu_capabilities.c "${libraries[@]}" -o out/issn-gpu-capabilities
|
||||
if [[ ${1:-} == --test ]]; then
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread "${includes[@]}" -I. -Isrc tests/test_pad.c "${libraries[@]}" -lm -o out/pad-test
|
||||
out/pad-test
|
||||
@@ -126,11 +158,23 @@ if [[ ${1:-} == --test ]]; then
|
||||
out/mp3-test tests/data/sine.mp3
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread "${includes[@]}" -I. -Isrc tests/test_videodec.c "${libraries[@]}" -o out/videodec-test
|
||||
out/videodec-test tests/data/tiny.h264
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -I. -Isrc tests/test_services.c -o out/services-test
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -I. -Isrc tests/test_services.c src/runtime_trophy.c -o out/services-test
|
||||
out/services-test
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -I. -Isrc tests/test_trophy.c src/runtime_trophy.c -o out/trophy-test
|
||||
out/trophy-test
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -mbmi -mlzcnt -mmovbe -I. -Isrc tests/test_isa_emulate.c out/isa_emulate.o -o out/isa-emulate-test
|
||||
out/isa-emulate-test
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -c src/isa_rewrite.c -I. -Isrc -o out/isa_rewrite.o
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -mbmi -mlzcnt -I. -Isrc tests/test_isa_stub.c out/isa_rewrite.o out/isa_emulate.o -o out/isa-stub-test
|
||||
out/isa-stub-test tests/data/isa_fixture.bin
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -I. -Isrc tests/test_isa_extrq.c out/isa_rewrite.o out/isa_emulate.o -o out/isa-extrq-test
|
||||
out/isa-extrq-test
|
||||
# The run helper calls through inline asm: no red zone in the test itself.
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -mno-red-zone -I. -Isrc tests/test_isa_f16c.c out/isa_rewrite.o out/isa_emulate.o -o out/isa-f16c-test
|
||||
out/isa-f16c-test
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -I. -Isrc tests/test_redzone_guard.c -o out/redzone-guard-test
|
||||
out/redzone-guard-test
|
||||
# Calls the stubs through inline asm too: no red zone.
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -mno-red-zone -I. -Isrc tests/test_hero_stubs.c -o out/hero-stubs-test
|
||||
out/hero-stubs-test
|
||||
fi
|
||||
+33
-5
@@ -27,7 +27,26 @@ if (ISSN_LTO)
|
||||
message(WARNING "LTO unavailable: ${issn_ipo_error}")
|
||||
endif()
|
||||
endif()
|
||||
if (ISSN_PGO STREQUAL "generate")
|
||||
# issn: Clang (the Windows build, MSYS2 CLANG64) uses LLVM's instrumentation: generate writes
|
||||
# <ISSN_GPU_USER_DIR>/pgo/issn-%p.profraw (merged by tools/pgo_merge.sh into
|
||||
# pgo-windows/issn-windows.profdata), use reads ISSN_PGO_FILE. axylpup's own .profdata cannot be
|
||||
# used: his clang-cl build has MSVC symbol names, this one Itanium names.
|
||||
set(ISSN_PGO_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../pgo-windows/issn-windows.profdata" CACHE FILEPATH
|
||||
"Clang profile for ISSN_PGO=use")
|
||||
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND ISSN_PGO STREQUAL "generate")
|
||||
add_compile_options(-fprofile-instr-generate -fprofile-update=atomic)
|
||||
add_link_options(-fprofile-instr-generate)
|
||||
add_compile_definitions(ISSN_PGO_GENERATE ISSN_PGO_LLVM)
|
||||
elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND ISSN_PGO STREQUAL "use")
|
||||
if (EXISTS "${ISSN_PGO_FILE}")
|
||||
add_compile_options(-fprofile-instr-use=${ISSN_PGO_FILE} -Wno-profile-instr-unprofiled
|
||||
-Wno-profile-instr-out-of-date -Wno-profile-instr-missing
|
||||
-Wno-backend-plugin)
|
||||
message(STATUS "PGO: ${ISSN_PGO_FILE}")
|
||||
else()
|
||||
message(WARNING "PGO: ${ISSN_PGO_FILE} not found, built without a profile")
|
||||
endif()
|
||||
elseif (ISSN_PGO STREQUAL "generate")
|
||||
# Atomic counters: the GPU, recording, preparation and copy threads run the same code.
|
||||
add_compile_options(-fprofile-generate=${ISSN_PGO_DIR} -fprofile-update=atomic)
|
||||
add_link_options(-fprofile-generate=${ISSN_PGO_DIR})
|
||||
@@ -63,7 +82,8 @@ endif()
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(XXHASH REQUIRED IMPORTED_TARGET libxxhash)
|
||||
pkg_check_modules(SDL3 REQUIRED IMPORTED_TARGET sdl3)
|
||||
if (NOT WIN32)
|
||||
# macOS presents through MoltenVK's Metal surface (SDL), with no X11.
|
||||
if (NOT WIN32 AND NOT APPLE)
|
||||
pkg_check_modules(X11 REQUIRED IMPORTED_TARGET x11)
|
||||
endif()
|
||||
find_package(miniz QUIET)
|
||||
@@ -150,16 +170,19 @@ target_compile_definitions(issngpu PRIVATE IMGUI_IMPL_VULKAN_NO_PROTOTYPES
|
||||
set_source_files_properties(shim/issn_overlay.cpp PROPERTIES
|
||||
OBJECT_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/third_party/fonts/DejaVuSans.ttf)
|
||||
target_include_directories(issngpu PRIVATE shim shadps4 ${SHADER_INCLUDE} third_party/gcn/include third_party/half/include
|
||||
third_party/streamline-sdk/include third_party/stb)
|
||||
third_party/streamline-sdk/include third_party/xess-sdk/include third_party/stb)
|
||||
target_compile_definitions(issngpu PRIVATE BOOST_ASIO_STANDALONE NDEBUG)
|
||||
target_compile_options(issngpu PRIVATE -Wno-deprecated-declarations -Wno-missing-field-initializers)
|
||||
# Runtime symbols (runtime_memory_*, runtime_process_*) resolve against issn-probe.
|
||||
if (NOT WIN32)
|
||||
if (APPLE)
|
||||
target_link_options(issngpu PRIVATE -Wl,-undefined,dynamic_lookup)
|
||||
elseif (NOT WIN32)
|
||||
target_link_options(issngpu PRIVATE -Wl,--allow-shlib-undefined)
|
||||
endif()
|
||||
# glslang's own CMake package: SPIRV and the default resource limits are separate targets in
|
||||
# older releases only.
|
||||
target_link_libraries(issngpu PRIVATE glslang::glslang ffx-vulkan::fsr4-v07-vulkan)
|
||||
target_link_libraries(issngpu PRIVATE glslang::glslang ffx-vulkan::fsr4-v07-vulkan
|
||||
ffx-vulkan::fsr3-vk-framegeneration-3.1.6) # issn-axy: frame generation (issn_framegen.cpp)
|
||||
foreach(glslang_part glslang::SPIRV glslang::glslang-default-resource-limits)
|
||||
if (TARGET ${glslang_part})
|
||||
target_link_libraries(issngpu PRIVATE ${glslang_part})
|
||||
@@ -174,6 +197,8 @@ if (WIN32)
|
||||
target_compile_definitions(issngpu PUBLIC NOMINMAX WIN32_LEAN_AND_MEAN)
|
||||
target_compile_options(issngpu PRIVATE -include ${CMAKE_CURRENT_SOURCE_DIR}/shim/issn_win_compat.h)
|
||||
target_link_libraries(issngpu PUBLIC Vulkan::Vulkan ws2_32 bcrypt ntdll synchronization winmm pdh)
|
||||
elseif (APPLE)
|
||||
target_link_libraries(issngpu PUBLIC Vulkan::Vulkan)
|
||||
else()
|
||||
target_link_libraries(issngpu PUBLIC PkgConfig::X11)
|
||||
endif()
|
||||
@@ -279,6 +304,9 @@ add_executable(graphics-settings-test EXCLUDE_FROM_ALL ../tests/test_graphics_se
|
||||
shim/issn_settings.cpp)
|
||||
target_include_directories(graphics-settings-test PRIVATE shim)
|
||||
target_compile_options(graphics-settings-test PRIVATE -UNDEBUG)
|
||||
add_executable(postfx-test EXCLUDE_FROM_ALL ../tests/test_postfx.cpp)
|
||||
target_include_directories(postfx-test PRIVATE shadps4)
|
||||
target_compile_options(postfx-test PRIVATE -UNDEBUG -Wall -Wextra -Werror)
|
||||
add_executable(actor-hook-test EXCLUDE_FROM_ALL ../tests/test_actor_hook.cpp)
|
||||
target_include_directories(actor-hook-test PRIVATE shim)
|
||||
target_compile_options(actor-hook-test PRIVATE -UNDEBUG -Wall -Wextra -Werror)
|
||||
|
||||
@@ -38,6 +38,8 @@ void issngpu_dump_guest_writes(void *ucontext);
|
||||
void issngpu_dump_gpu_state(void);
|
||||
/* issn: a guest memcpy/memset into GPU-modified memory written without a fault or GPU wait
|
||||
* (ISSN_WRITE_MERGE=1 with precise readbacks, from HayatoG's iss_pc); 0: write normally. */
|
||||
/* issn 0.3.6: fill -2 = a file read (src/runtime_file.c): chunks of 64 KB or more are marked
|
||||
* CPU-written in one go and copied without a fault per page. */
|
||||
int issngpu_cpu_write(void *dst, const void *src, int fill, uint64_t size);
|
||||
/* issn: frames the guest has finished (sceGnmSubmitDone calls), for the hang watchdog. */
|
||||
uint64_t issngpu_frame_count(void);
|
||||
@@ -50,8 +52,31 @@ int issngpu_text_input_poll(char *out_utf8, uint64_t size);
|
||||
* poll: -1 while open, -2 cancelled, else the chosen index. */
|
||||
int issngpu_choice_begin(const char *title, const char *const *items, const char *const *images, int count, int focus);
|
||||
int issngpu_choice_poll(void);
|
||||
/* issn-axy: the game finished writing a save (`what`: its subtitle, e.g. the mission): a note
|
||||
* in the corner of the screen for a few seconds. */
|
||||
void issngpu_note_game_saved(const char *what);
|
||||
/* issn 11-04: a trophy was unlocked (src/runtime_trophy.c): a popup with its icon (PNG bytes from
|
||||
* the game's trophy file, NULL: none) and name ("": "Trophy N"); grade "P", "G", "S", "B" or "".
|
||||
* The data is copied before the call returns. */
|
||||
void issngpu_note_trophy(int id, const char *name, const char *grade, const void *png, uint64_t size);
|
||||
/* 1 while the in-game settings menu is open: the game's pad input is held neutral. */
|
||||
int issngpu_overlay_captures_input(void);
|
||||
/* issn 11-04: the menu's sensitivities, read live by runtime_pad.c. mouse: a factor on the mouse
|
||||
* camera's degrees per pixel (1 = default); pad: the right stick (1 = the game's own feel).
|
||||
* set_mouse: - / = in play change it (saved to issn.ini). */
|
||||
float issngpu_mouse_sensitivity(void);
|
||||
void issngpu_set_mouse_sensitivity(float value);
|
||||
float issngpu_pad_sensitivity(void);
|
||||
/* issn 11-04: the spray can pose (pad tilted) for pads without motion sensors: the menu's
|
||||
* switch (Insert > Controls) and H in play. Not saved. */
|
||||
int issngpu_spray_pose(void);
|
||||
/* issn 11-04: the exposure meter's mailbox (src/exposure_hook.h, IssnExposureMailbox) and the
|
||||
* probe's note that its cave is in. */
|
||||
void *issngpu_exposure_mailbox(void);
|
||||
void issngpu_exposure_attached(void);
|
||||
void issngpu_set_spray_pose(int on);
|
||||
/* issn 0.3.6: issn.ini key_map (keys changed in the game's menu), "" when none. */
|
||||
const char *issngpu_saved_key_map(void);
|
||||
/* Number of symbols registered by the vendored libraries (diagnostics). */
|
||||
unsigned issngpu_symbol_count(void);
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
diff --git a/include/ffx_vk_fsr3_3_1_5_bridge.h b/include/ffx_vk_fsr3_3_1_5_bridge.h
|
||||
index 863476c..69e59d7 100644
|
||||
--- a/include/ffx_vk_fsr3_3_1_5_bridge.h
|
||||
+++ b/include/ffx_vk_fsr3_3_1_5_bridge.h
|
||||
@@ -252,6 +252,10 @@ typedef struct FfxVkFsr3_3_1_6FrameGenerationCreateInfo {
|
||||
uint32_t displayWidth;
|
||||
uint32_t displayHeight;
|
||||
VkFormat colorFormat;
|
||||
+ /* issn: FfxFrameInterpolationInitializationFlagBits for the interpolation context
|
||||
+ * (FFX_FRAMEINTERPOLATION_ENABLE_DEPTH_INVERTED = 1, ..._DEPTH_INFINITE = 2); zero keeps
|
||||
+ * the SDK's defaults (depth 0..1, near to far). */
|
||||
+ uint32_t interpolationFlags;
|
||||
} FfxVkFsr3_3_1_6FrameGenerationCreateInfo;
|
||||
|
||||
typedef struct FfxVkFsr3_3_1_6FrameGenerationPrepareInfo {
|
||||
diff --git a/src/ffx_vk_fsr3_3_1_6_framegen.cpp b/src/ffx_vk_fsr3_3_1_6_framegen.cpp
|
||||
index 9636cec..d389103 100644
|
||||
--- a/src/ffx_vk_fsr3_3_1_6_framegen.cpp
|
||||
+++ b/src/ffx_vk_fsr3_3_1_6_framegen.cpp
|
||||
@@ -360,6 +360,7 @@ ffxVkFsr3_3_1_6FrameGenerationContextCreate(
|
||||
context->opticalFlowCreated = true;
|
||||
FfxFrameInterpolationContextDescription interpolationDescription{};
|
||||
interpolationDescription.backendInterface = context->backend;
|
||||
+ interpolationDescription.flags = createInfo->interpolationFlags;
|
||||
interpolationDescription.maxRenderSize = {createInfo->maxRenderWidth, createInfo->maxRenderHeight};
|
||||
interpolationDescription.displaySize = {createInfo->displayWidth, createInfo->displayHeight};
|
||||
interpolationDescription.backBufferFormat = to_ffx_format(context->colorFormat);
|
||||
@@ -10,3 +10,5 @@ patches here to its working tree when they are not applied yet:
|
||||
- `0003-...`: the rest of the standard includes the FFX 1.1.4 SDK relies on being pulled in
|
||||
transitively by libstdc++ but not by libc++ / MinGW: `<cstdlib>` for `_countof`, `<wchar.h>`
|
||||
for `swprintf_s`, `<cmath>` for `log2` and `<new>` for placement new.
|
||||
- `0004-...`: `interpolationFlags` in the FSR 3.1.6 frame generation create info, passed to the
|
||||
interpolation context (the game's depth is reversed: `FFX_FRAMEINTERPOLATION_ENABLE_DEPTH_INVERTED`).
|
||||
@@ -652,6 +652,19 @@ std::ofstream OpenCsv() {
|
||||
|
||||
} // namespace (ReportLoop helpers; gmc census below is public)
|
||||
|
||||
// issn 0.3.6: the counters the frame-stats hitch lines compare across one slow frame
|
||||
// (core/libraries/videoout/driver.cpp). Counts are always on; times only with
|
||||
// SHADPS4_FAULT_TIMERS builds (0 otherwise).
|
||||
void HitchCounters(u64 out[HitchCounterCount]) {
|
||||
const Sample s = Read();
|
||||
const u64 values[HitchCounterCount] = {s.read, s.write, s.waits, s.wait_ns,
|
||||
s.wrm_count, s.vo_count, s.dl_calls, s.dl_bytes,
|
||||
s.draws, s.submits, s.disp_count};
|
||||
for (u32 i = 0; i < HitchCounterCount; ++i) {
|
||||
out[i] = values[i];
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
struct GmCensus {
|
||||
std::array<std::atomic<u64>, 16> wait_ticks{}, wait_n{};
|
||||
|
||||
@@ -115,6 +115,10 @@ void MarkGameMainThread();
|
||||
bool JobCensus();
|
||||
/// Called at guest thread start with its name; registers JobWorker* threads for the job census.
|
||||
void NoteGuestThread(const char* name);
|
||||
/// issn 0.3.6: read faults, write faults, GPU waits, wait ns, wait-reg-mem, VO label waits,
|
||||
/// downloads, download bytes, draws, submits, dispatches (for the hitch lines).
|
||||
inline constexpr u32 HitchCounterCount = 11;
|
||||
void HitchCounters(u64 out[HitchCounterCount]);
|
||||
/// Cycles spent in one NativeRecDraw / NativeRecShader call (job census only).
|
||||
void NoteNativeRecCycles(u64 cycles);
|
||||
u64 CensusTicks();
|
||||
|
||||
@@ -1117,8 +1117,11 @@ void JitterFrame() {
|
||||
const double ratio = now.width > 0 ? std::max(1.0, double(ref.width) / now.width) : 1.0;
|
||||
const u32 phases = std::clamp(u32(std::lround(8.0 * ratio * ratio)), 8u, 64u);
|
||||
g_jitter_k = (g_jitter_k + 1) % phases;
|
||||
const float jx = Halton(g_jitter_k + 1, 2) - 0.5f; // render px, (-0.5, 0.5)
|
||||
const float jy = Halton(g_jitter_k + 1, 3) - 0.5f;
|
||||
// Anti-aliasing "Off" (vk_dlss.h AaOff): the patch holds the camera at the pixel centre - the
|
||||
// game's own 2 phases would make the un-anti-aliased picture shake by half a pixel.
|
||||
const bool still = Vulkan::Dlss::AaOff();
|
||||
const float jx = still ? 0.0f : Halton(g_jitter_k + 1, 2) - 0.5f; // render px, (-0.5, 0.5)
|
||||
const float jy = still ? 0.0f : Halton(g_jitter_k + 1, 3) - 0.5f;
|
||||
// The image moves by (-X/2, +Y/2) px for numerators (X, Y) (study sec. 2).
|
||||
struct Slot {
|
||||
float x, y;
|
||||
|
||||
@@ -90,6 +90,9 @@ void RequeueLivePurge();
|
||||
bool InLiveResizeWindow();
|
||||
/// A size of the render shape (+-3%): the game's screen-sized targets and their half / quarter.
|
||||
bool IsRenderShaped(u32 width, u32 height);
|
||||
/// axylpup 1.1.1: the fitted movie quad's corner (x, y) when movies are fitted to another screen
|
||||
/// shape (issn_pc: issn_game_patches.cpp), else false.
|
||||
bool MovieFitRect(float& x, float& y);
|
||||
/// Milliseconds since the last change (INT64_MAX: none) - DLSS's grace window.
|
||||
s64 MsSinceLiveResize();
|
||||
|
||||
|
||||
@@ -34,4 +34,9 @@ void HideOwnConsole();
|
||||
/// The game's eboot.bin: saved choice, else a folder picker (saved on success). nullopt = cancelled.
|
||||
std::optional<std::filesystem::path> GamePath();
|
||||
|
||||
/// 1.1.1 (GitHub #5): a player-facing box before an out-of-memory assert closes the game - what did
|
||||
/// not fit and what usually helps. Once per process; blocks the calling thread until closed.
|
||||
/// issn_pc: defined in gpu/shim/issngpu.cpp (SDL message box; release_mode.cpp is not built).
|
||||
void ShowOutOfMemoryNotice(unsigned long long bytes);
|
||||
|
||||
} // namespace Common::ReleaseMode
|
||||
@@ -508,6 +508,21 @@ u64 PhysicalRamBytes() {
|
||||
return GlobalMemoryStatusEx(&ms) ? ms.ullTotalPhys : 0;
|
||||
}
|
||||
|
||||
std::string DescribeSystemMemory() {
|
||||
MEMORYSTATUSEX ms{};
|
||||
ms.dwLength = sizeof(ms);
|
||||
if (!GlobalMemoryStatusEx(&ms)) {
|
||||
return {};
|
||||
}
|
||||
u64 private_bytes = 0, working_set = 0;
|
||||
ProcessMemory(private_bytes, working_set);
|
||||
// ullTotalPageFile is the commit limit (RAM + page files), ullAvailPageFile what is left of it.
|
||||
return fmt::format("Windows: RAM free {} of {} MB, commit free {} of limit {} MB, this process "
|
||||
"{} MB private",
|
||||
ms.ullAvailPhys >> 20, ms.ullTotalPhys >> 20, ms.ullAvailPageFile >> 20,
|
||||
ms.ullTotalPageFile >> 20, private_bytes >> 20);
|
||||
}
|
||||
|
||||
CommitCensus WalkCommitted() {
|
||||
CommitCensus c{};
|
||||
u64 cur_base = 0, cur_bytes = 0;
|
||||
@@ -838,6 +853,9 @@ u64 PhysicalRamBytes() {
|
||||
const char* sim = std::getenv("SHADPS4_SIM_RAM_MB");
|
||||
return sim ? std::strtoull(sim, nullptr, 10) << 20 : 0;
|
||||
}
|
||||
std::string DescribeSystemMemory() {
|
||||
return {};
|
||||
}
|
||||
void ApplyHotThreadPlacement(HotThread) {}
|
||||
void ApplyCriticalThreadPriority(HotThread) {}
|
||||
void SampleCommandProcessorCore() {}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include "common/types.h"
|
||||
|
||||
namespace Common {
|
||||
@@ -67,6 +68,10 @@ void ProcessMemory(u64& private_bytes, u64& working_set);
|
||||
/// Installed physical memory in bytes (0 where unknown). SHADPS4_SIM_RAM_MB stands in for it.
|
||||
u64 PhysicalRamBytes();
|
||||
|
||||
/// 1.1.1 (GitHub #5): Windows' memory state for an out-of-memory line - free RAM, free commit (RAM +
|
||||
/// page file) of the commit limit, this process's private bytes. Empty where unknown.
|
||||
std::string DescribeSystemMemory();
|
||||
|
||||
/// SHADPS4_MEM_CENSUS: committed bytes by kind and place (VirtualQuery walk): private / mapped,
|
||||
/// inside the guest range 0x200000000 - 0x1000000000 or outside it (emulator, driver, heaps).
|
||||
struct CommitCensus {
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
#include <cstdlib>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include "common/assert.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/fault_monitor.h"
|
||||
@@ -15,12 +19,17 @@
|
||||
#include "core/libraries/videoout/driver.h"
|
||||
#include "core/libraries/videoout/videoout_error.h"
|
||||
#include "imgui/renderer/imgui_core.h"
|
||||
#include "issn_overlay.h"
|
||||
#include "video_core/amdgpu/liverpool.h"
|
||||
#include "video_core/renderer_vulkan/vk_presenter.h"
|
||||
|
||||
extern std::unique_ptr<Vulkan::Presenter> presenter;
|
||||
extern std::unique_ptr<AmdGpu::Liverpool> liverpool;
|
||||
|
||||
// issn 0.3.6: the runtime's disk read totals (src/runtime_file.c).
|
||||
extern "C" void runtime_file_io_stats(std::uint64_t* bytes, std::uint64_t* read_ns,
|
||||
std::uint64_t* copy_ns, std::uint64_t* opened);
|
||||
|
||||
namespace Libraries::VideoOut {
|
||||
|
||||
constexpr static bool Is32BppPixelFormat(PixelFormat format) {
|
||||
@@ -247,6 +256,23 @@ static bool VoEarlyRelease() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
// issn-axy: frame generation without frame pacing: the game's frame goes to the screen half of
|
||||
// the measured interval between game frames after the generated one (at most 25 ms).
|
||||
static void PaceGeneratedFrame(std::chrono::steady_clock::time_point flip_start) {
|
||||
static std::chrono::steady_clock::time_point last_flip{};
|
||||
static double interval_ms = 16.6;
|
||||
const double since = std::chrono::duration<double, std::milli>(flip_start - last_flip).count();
|
||||
last_flip = flip_start;
|
||||
if (since > 2.0 && since < 100.0) {
|
||||
interval_ms = interval_ms * 0.9 + since * 0.1;
|
||||
}
|
||||
const auto target = flip_start + std::chrono::microseconds(
|
||||
static_cast<s64>(std::min(interval_ms * 0.5, 25.0) * 1000));
|
||||
if (std::chrono::steady_clock::now() < target) {
|
||||
std::this_thread::sleep_until(target);
|
||||
}
|
||||
}
|
||||
|
||||
void VideoOutDriver::Flip(const Request& req) {
|
||||
// Update HDR status before presenting.
|
||||
presenter->SetHDR(req.port->is_hdr);
|
||||
@@ -258,9 +284,24 @@ void VideoOutDriver::Flip(const Request& req) {
|
||||
// reuse until the repeats are submitted: the game, running ahead in this mode, waits for a
|
||||
// free frame and would otherwise draw into it while it is still being shown (review 10-21).
|
||||
const u32 divider = Vulkan::VsyncDivider();
|
||||
IssnOverlay::NoteGameFrame(); // the FPS readout counts game frames, not the repeats
|
||||
// issn-axy: frame generation (issn_framegen.h): the frame made between the previous one and
|
||||
// this is shown first. With frame pacing it takes one of the frame's screen refreshes (72 FPS
|
||||
// on 144 Hz become 144); without, this frame follows half a frame interval later.
|
||||
const auto flip_clock = std::chrono::steady_clock::now();
|
||||
Vulkan::Frame* const generated = std::exchange(req.frame->generated, nullptr);
|
||||
if (generated) {
|
||||
presenter->Present(generated);
|
||||
// With V-Sync each present already waits for a refresh, so the game's frame follows the
|
||||
// generated one by one refresh. Sleeping here on top held the flip back and the game
|
||||
// dropped from ~70 to 48 FPS (test 10-30); pacing by time only without V-Sync.
|
||||
if (divider < 2 && !Vulkan::PresentWaitsForVsync()) {
|
||||
PaceGeneratedFrame(flip_clock);
|
||||
}
|
||||
}
|
||||
presenter->Present(req.frame, false, divider > 1);
|
||||
if (divider > 1) {
|
||||
for (u32 i = 1; i < divider; ++i) {
|
||||
for (u32 i = generated ? 2 : 1; i < divider; ++i) {
|
||||
DrawLastFrame();
|
||||
}
|
||||
presenter->ReleaseFrame(req.frame);
|
||||
@@ -320,6 +361,33 @@ void VideoOutDriver::Flip(const Request& req) {
|
||||
const auto now = Clock::now();
|
||||
const double frame_ms = std::chrono::duration<double, std::milli>(now - last).count();
|
||||
last = now;
|
||||
// issn 0.3.6: one line per slow frame with what happened during it, to tell streaming
|
||||
// (disk reads and copies into guest memory), guest faults, GPU waits and readbacks
|
||||
// apart (João's log 10-29: the spikes came with the game opening its b*.xpps blocks).
|
||||
static u64 prev_counters[Common::FaultMonitor::HitchCounterCount]{};
|
||||
static u64 prev_io[4]{};
|
||||
static u32 hitch_lines = 0;
|
||||
u64 counters[Common::FaultMonitor::HitchCounterCount];
|
||||
Common::FaultMonitor::HitchCounters(counters);
|
||||
u64 io[4];
|
||||
runtime_file_io_stats(&io[0], &io[1], &io[2], &io[3]);
|
||||
if (frame_ms > 45.0 && frame_ms < 5000.0 && hitch_lines < 400 && prev_io[0] != 0) {
|
||||
++hitch_lines;
|
||||
const auto d = [&](u32 i) { return counters[i] - prev_counters[i]; };
|
||||
std::printf("Hitch: %.0f ms | disk %.1f MB, read %.1f ms, copy into game memory %.1f ms, "
|
||||
"%llu files opened | faults read %llu write %llu | GPU waits %llu "
|
||||
"(%.1f ms) wait-reg-mem %llu flip-label %llu | readbacks %llu (%.0f kB) "
|
||||
"| draws %llu dispatches %llu submits %llu\n",
|
||||
frame_ms, double(io[0] - prev_io[0]) / 1048576.0,
|
||||
double(io[1] - prev_io[1]) / 1e6, double(io[2] - prev_io[2]) / 1e6,
|
||||
(unsigned long long)(io[3] - prev_io[3]), (unsigned long long)d(0),
|
||||
(unsigned long long)d(1), (unsigned long long)d(2), double(d(3)) / 1e6,
|
||||
(unsigned long long)d(4), (unsigned long long)d(5),
|
||||
(unsigned long long)d(6), double(d(7)) / 1024.0, (unsigned long long)d(8),
|
||||
(unsigned long long)d(10), (unsigned long long)d(9));
|
||||
}
|
||||
std::memcpy(prev_counters, counters, sizeof(counters));
|
||||
std::memcpy(prev_io, io, sizeof(io));
|
||||
worst_ms = std::max(worst_ms, frame_ms);
|
||||
over_33 += frame_ms > 33.4 ? 1 : 0;
|
||||
++frames;
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <type_traits>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -780,7 +783,11 @@ struct PM4CmdWriteData {
|
||||
|
||||
template <typename T>
|
||||
T Address() const {
|
||||
return reinterpret_cast<T>(addr64);
|
||||
if constexpr (std::is_integral_v<T>) {
|
||||
return static_cast<T>(addr64);
|
||||
} else {
|
||||
return reinterpret_cast<T>(static_cast<uintptr_t>(addr64));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -811,7 +818,12 @@ struct PM4CmdEventWriteEos {
|
||||
|
||||
template <typename T = u32*>
|
||||
T Address() const {
|
||||
return reinterpret_cast<T>(address_lo | u64(address_hi) << 32);
|
||||
const u64 address = address_lo | u64(address_hi) << 32;
|
||||
if constexpr (std::is_integral_v<T>) {
|
||||
return static_cast<T>(address);
|
||||
} else {
|
||||
return reinterpret_cast<T>(static_cast<uintptr_t>(address));
|
||||
}
|
||||
}
|
||||
|
||||
u32 DataDWord() const {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "common/alignment.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/release_mode.h"
|
||||
#include <immintrin.h>
|
||||
#include "video_core/buffer_cache/buffer.h"
|
||||
#include "video_core/renderer_vulkan/liverpool_to_vk.h"
|
||||
@@ -61,6 +62,22 @@ std::string_view BufferTypeName(MemoryUsage type) {
|
||||
return VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
|
||||
}
|
||||
|
||||
static thread_local bool t_require_video = false;
|
||||
|
||||
RequireVideoMemory::RequireVideoMemory() {
|
||||
t_require_video = true;
|
||||
}
|
||||
|
||||
RequireVideoMemory::~RequireVideoMemory() {
|
||||
t_require_video = false;
|
||||
}
|
||||
|
||||
/// RequireVideoMemory: device-local memory types only, VMA's budget estimate skipped.
|
||||
static void RequireVideo(VmaAllocationCreateInfo& ci) {
|
||||
ci.flags &= ~VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT;
|
||||
ci.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
|
||||
}
|
||||
|
||||
UniqueBuffer::UniqueBuffer(vk::Device device_, VmaAllocator allocator_)
|
||||
: device{device_}, allocator{allocator_} {}
|
||||
|
||||
@@ -100,14 +117,33 @@ void UniqueBuffer::Create(const vk::BufferCreateInfo& buffer_ci, MemoryUsage usa
|
||||
|
||||
const VkBufferCreateInfo buffer_ci_unsafe = static_cast<VkBufferCreateInfo>(buffer_ci);
|
||||
VkBuffer unsafe_buffer{};
|
||||
// v1.0.2: over VMA's budget, retry without it (vk_memory_budget.h) instead of asserting.
|
||||
VkResult result = Vulkan::AllocateWithBudgetRetry(
|
||||
allocator, alloc_ci, "buffer", buffer_ci.size,
|
||||
[&] { return std::string{BufferTypeName(usage)}; },
|
||||
[&](const VmaAllocationCreateInfo& ci) {
|
||||
return vmaCreateBuffer(allocator, &buffer_ci_unsafe, &ci, &unsafe_buffer, &allocation,
|
||||
out_alloc_info);
|
||||
});
|
||||
VkResult result = VK_ERROR_OUT_OF_DEVICE_MEMORY;
|
||||
if (t_require_video && usage == MemoryUsage::DeviceLocal) {
|
||||
VmaAllocationCreateInfo video_ci = alloc_ci;
|
||||
RequireVideo(video_ci);
|
||||
result = vmaCreateBuffer(allocator, &buffer_ci_unsafe, &video_ci, &unsafe_buffer,
|
||||
&allocation, out_alloc_info);
|
||||
if (result != VK_SUCCESS) {
|
||||
LOG_WARNING(Render_Vulkan,
|
||||
"A {} MB buffer did not fit in video memory ({}); the normal allocation "
|
||||
"decides; {}",
|
||||
buffer_ci.size >> 20, vk::to_string(vk::Result{result}),
|
||||
Vulkan::DescribeDeviceLocalBudgets(allocator));
|
||||
}
|
||||
}
|
||||
if (result != VK_SUCCESS) {
|
||||
// v1.0.2: over VMA's budget, retry without it (vk_memory_budget.h) instead of asserting.
|
||||
result = Vulkan::AllocateWithBudgetRetry(
|
||||
allocator, alloc_ci, "buffer", buffer_ci.size,
|
||||
[&] { return std::string{BufferTypeName(usage)}; },
|
||||
[&](const VmaAllocationCreateInfo& ci) {
|
||||
return vmaCreateBuffer(allocator, &buffer_ci_unsafe, &ci, &unsafe_buffer,
|
||||
&allocation, out_alloc_info);
|
||||
});
|
||||
}
|
||||
if (result != VK_SUCCESS && buffer_ci.size >= (256ull << 20)) {
|
||||
Common::ReleaseMode::ShowOutOfMemoryNotice(buffer_ci.size); // 1.1.1 (GitHub #5)
|
||||
}
|
||||
ASSERT_MSG(result == VK_SUCCESS, "Failed allocating buffer of {} MB ({}) with error {}",
|
||||
buffer_ci.size >> 20, BufferTypeName(usage), vk::to_string(vk::Result{result}));
|
||||
buffer = vk::Buffer{unsafe_buffer};
|
||||
@@ -122,6 +158,56 @@ void UniqueBuffer::Create(const vk::BufferCreateInfo& buffer_ci, MemoryUsage usa
|
||||
}
|
||||
}
|
||||
|
||||
bool ProbeBufferAllocation(const Vulkan::Instance& instance, MemoryUsage usage,
|
||||
vk::BufferUsageFlags flags, u64 size, u32& heap, bool require_video) {
|
||||
// The same create info and memory request as Buffer::Buffer + UniqueBuffer::Create.
|
||||
if (instance.IsNativeBindingsActive() &&
|
||||
(flags & (vk::BufferUsageFlagBits::eUniformBuffer |
|
||||
vk::BufferUsageFlagBits::eStorageBuffer))) {
|
||||
flags |= vk::BufferUsageFlagBits::eShaderDeviceAddress;
|
||||
}
|
||||
const std::array<u32, 2> families = {instance.GetGraphicsQueueFamilyIndex(),
|
||||
instance.GetReadbackQueueFamilyIndex()};
|
||||
const bool concurrent = instance.BuffersShareReadbackFamily();
|
||||
const VkBufferCreateInfo buffer_ci = static_cast<VkBufferCreateInfo>(vk::BufferCreateInfo{
|
||||
.size = size,
|
||||
.usage = flags,
|
||||
.sharingMode = concurrent ? vk::SharingMode::eConcurrent : vk::SharingMode::eExclusive,
|
||||
.queueFamilyIndexCount = concurrent ? 2u : 0u,
|
||||
.pQueueFamilyIndices = concurrent ? families.data() : nullptr,
|
||||
});
|
||||
const bool with_bda = bool(flags & vk::BufferUsageFlagBits::eShaderDeviceAddress);
|
||||
VmaAllocationCreateInfo alloc_ci = {
|
||||
.flags = VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT |
|
||||
(with_bda ? VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT : 0) |
|
||||
MemoryUsageVmaFlags(usage),
|
||||
.usage = MemoryUsageVma(usage),
|
||||
.requiredFlags = 0,
|
||||
.preferredFlags = MemoryUsagePreferredVmaFlags(usage),
|
||||
};
|
||||
if (require_video && usage == MemoryUsage::DeviceLocal) {
|
||||
RequireVideo(alloc_ci);
|
||||
}
|
||||
VmaAllocator allocator = instance.GetAllocator();
|
||||
VkBuffer probe{};
|
||||
VmaAllocation allocation{};
|
||||
VmaAllocationInfo info{};
|
||||
VkResult result = vmaCreateBuffer(allocator, &buffer_ci, &alloc_ci, &probe, &allocation, &info);
|
||||
if (result == VK_ERROR_OUT_OF_DEVICE_MEMORY && Vulkan::AllowOverBudget() &&
|
||||
(alloc_ci.flags & VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT) != 0) {
|
||||
alloc_ci.flags &= ~VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT;
|
||||
result = vmaCreateBuffer(allocator, &buffer_ci, &alloc_ci, &probe, &allocation, &info);
|
||||
}
|
||||
if (result != VK_SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
const VkPhysicalDeviceMemoryProperties* props{};
|
||||
vmaGetMemoryProperties(allocator, &props);
|
||||
heap = props->memoryTypes[info.memoryType].heapIndex;
|
||||
vmaDestroyBuffer(allocator, probe, allocation);
|
||||
return true;
|
||||
}
|
||||
|
||||
Buffer::Buffer(const Vulkan::Instance& instance_, Vulkan::Scheduler& scheduler_,
|
||||
vk::Buffer imported, VAddr cpu_addr_, u64 size_bytes_)
|
||||
: cpu_addr{cpu_addr_}, size_bytes{size_bytes_}, instance{&instance_}, scheduler{&scheduler_},
|
||||
|
||||
@@ -104,6 +104,26 @@ struct UniqueBuffer {
|
||||
vk::DeviceAddress bda_addr = 0;
|
||||
};
|
||||
|
||||
/// 1.1.1 (GitHub #5): allocates what a Buffer of `size` bytes with `flags` would (same create info
|
||||
/// and memory request, the over-budget retry included) and frees it again at once. true = it fit;
|
||||
/// `heap` = the memory heap it landed in. A refusal logs nothing: the caller steps down.
|
||||
/// `require_video`: as a buffer made under RequireVideoMemory would be.
|
||||
bool ProbeBufferAllocation(const Vulkan::Instance& instance, MemoryUsage usage,
|
||||
vk::BufferUsageFlags flags, u64 size, u32& heap,
|
||||
bool require_video = false);
|
||||
|
||||
/// 1.1.1 (GitHub #30: a 6 GB card's 2816 MB mesh buffer landed in system RAM): while one is alive
|
||||
/// on this thread, a DeviceLocal buffer must be in video memory and skips VMA's 80%-of-heap
|
||||
/// estimate (the caller checked the driver's own budget). If the driver refuses, the normal
|
||||
/// allocation runs.
|
||||
class RequireVideoMemory {
|
||||
public:
|
||||
RequireVideoMemory();
|
||||
~RequireVideoMemory();
|
||||
RequireVideoMemory(const RequireVideoMemory&) = delete;
|
||||
RequireVideoMemory& operator=(const RequireVideoMemory&) = delete;
|
||||
};
|
||||
|
||||
class Buffer {
|
||||
public:
|
||||
explicit Buffer(const Vulkan::Instance& instance, Vulkan::Scheduler& scheduler,
|
||||
|
||||
@@ -470,6 +470,8 @@ void StartTickPoller(Vulkan::MasterSemaphore* semaphore) {
|
||||
|
||||
static constexpr size_t DataShareBufferSize = 64_KB;
|
||||
static constexpr size_t StagingBufferSize = 512_MB;
|
||||
// 1.1.1 (#36): an upload whose one host buffer is refused goes through a buffer of this size.
|
||||
static constexpr size_t UploadPieceSize = 256_MB;
|
||||
static constexpr size_t DownloadBufferSize = 32_MB;
|
||||
static constexpr size_t UboStreamBufferSize = 64_MB;
|
||||
static constexpr size_t DeviceBufferSize = 128_MB;
|
||||
@@ -632,7 +634,7 @@ static std::pair<VAddr, u64> WidenReadFault(const Buffer& buffer, VAddr device_a
|
||||
return {device_addr, size};
|
||||
}
|
||||
const VAddr begin = std::max(Common::AlignDown(device_addr, window), buffer.CpuAddr());
|
||||
const VAddr end = std::min(Common::AlignUp(device_addr + size, window),
|
||||
const VAddr end = std::min<VAddr>(Common::AlignUp(device_addr + size, window),
|
||||
buffer.CpuAddr() + buffer.SizeBytes());
|
||||
return {begin, end - begin};
|
||||
}
|
||||
@@ -1697,7 +1699,7 @@ bool BufferCache::DownloadWindowMultiBuffer(VAddr window_addr, u64 window_size)
|
||||
}
|
||||
seen.push_back(id);
|
||||
const VAddr lo = std::max(window_addr, buffer.CpuAddr());
|
||||
const VAddr hi = std::min(window_addr + window_size, buffer.CpuAddr() + buffer.SizeBytes());
|
||||
const VAddr hi = std::min<VAddr>(window_addr + window_size, buffer.CpuAddr() + buffer.SizeBytes());
|
||||
if (hi <= lo) {
|
||||
return;
|
||||
}
|
||||
@@ -2146,7 +2148,7 @@ BufferCache::FaultTicket BufferCache::PrepareFaultDownload(VAddr device_addr, u6
|
||||
seen.push_back(id);
|
||||
const VAddr lo = std::max(addr, window_buffer.CpuAddr());
|
||||
const VAddr hi =
|
||||
std::min(addr + bytes, window_buffer.CpuAddr() + window_buffer.SizeBytes());
|
||||
std::min<VAddr>(addr + bytes, window_buffer.CpuAddr() + window_buffer.SizeBytes());
|
||||
if (hi <= lo) {
|
||||
return;
|
||||
}
|
||||
@@ -3256,7 +3258,7 @@ u64 BufferCache::NewestAscWriter(std::span<const FaultCopy> copies, u64 done_bel
|
||||
VAddr clo = ~VAddr{0}, chi = 0; // q11e: the copies' bounding box
|
||||
for (const FaultCopy& copy : copies) {
|
||||
clo = std::min(clo, copy.addr);
|
||||
chi = std::max(chi, copy.addr + copy.size);
|
||||
chi = std::max<VAddr>(chi, copy.addr + copy.size);
|
||||
}
|
||||
for (u64 v = top; v != 0 && v > done_below && top - v < asc_write_hist.size(); --v) {
|
||||
const size_t i = v % asc_write_hist.size();
|
||||
@@ -7196,6 +7198,20 @@ void BufferCache::LogLeanStats() {
|
||||
s[2], s[3], s[4], s[5]);
|
||||
}
|
||||
|
||||
// 1.1.1 (GitHub #30): whether `size` more bytes fit the driver's device-local budget with a 256 MB
|
||||
// margin (BIG_GROW's mesh buffer); nullopt when the driver gives no budget (VK_EXT_memory_budget).
|
||||
// `freed`: the joined buffers' bytes, released right after the copy - credited up to a quarter of
|
||||
// the new size (i30: 524 MB of pieces at the first join; a whole-buffer regrow gets no free pass,
|
||||
// its old copy would stay over budget for the whole copy).
|
||||
static std::optional<bool> MeshFitsVideo(const Vulkan::Instance& instance, u64 size, u64 freed) {
|
||||
const auto free = Vulkan::DriverFreeDeviceLocal(instance.GetAllocator());
|
||||
if (!free) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const u64 credit = std::min(freed, size / 4);
|
||||
return size + (256ull << 20) <= *free + credit;
|
||||
}
|
||||
|
||||
BufferCache::OverlapResult BufferCache::ResolveOverlaps(VAddr device_addr, u32 wanted_size) {
|
||||
static constexpr int STREAM_LEAP_THRESHOLD = 16;
|
||||
boost::container::small_vector<BufferId, 16> overlap_ids;
|
||||
@@ -7203,6 +7219,7 @@ BufferCache::OverlapResult BufferCache::ResolveOverlaps(VAddr device_addr, u32 w
|
||||
VAddr end = device_addr + wanted_size;
|
||||
int stream_score = 0;
|
||||
bool has_stream_leap = false;
|
||||
bool mesh = false; // a BIG_GROW mesh-sized join (>= 1 GB)
|
||||
const auto expand_begin = [&](VAddr add_value) {
|
||||
static constexpr VAddr min_page = CACHING_PAGESIZE + DEVICE_PAGESIZE;
|
||||
if (add_value > begin - min_page) {
|
||||
@@ -7358,9 +7375,81 @@ BufferCache::OverlapResult BufferCache::ResolveOverlaps(VAddr device_addr, u32 w
|
||||
// rooftop at the last one - the play hitches). Buffers inside the extension are joined
|
||||
// like any overlap, so no page keeps two buffers.
|
||||
const u64 cur = end - begin;
|
||||
// 1.1.1 (GitHub #30): every mesh-sized join prefers video memory (CreateBuffer checks the
|
||||
// driver's budget for its real size) - not only the extended one: the city save's first
|
||||
// join was already 2766 MB, above a 2560 MB step (s1584), and later regrows are joins too.
|
||||
mesh = true;
|
||||
// r15d: straight to the cap (3.75 GB) at the first big join - normally the load - so the
|
||||
// region never grows again in the scene (r15b: an in-scene growth's coherent upload stalled ~1 s).
|
||||
const u64 want = big_grow_bytes;
|
||||
// 1.1.1 (GitHub #5: an RTX 5050 8 GB on 16 GB of RAM - the 3 GB buffer fitted neither in
|
||||
// video nor in system memory at the first big join, on 1.0.2 and 1.1, and the assert closed
|
||||
// the game): the first big join tries the tier first (allocated and freed again), then the
|
||||
// smaller tiers down to 2.75 GB, then no extension (the old growth path). The pick holds
|
||||
// for the session. SHADPS4_BIG_GROW_PROBE_FAIL_ABOVE_MB (test): a try above it asks for
|
||||
// 64 GB instead, which the driver really refuses.
|
||||
u64 pieces = 0; // the joined buffers' bytes (MeshFitsVideo's credit)
|
||||
for (const BufferId id : overlap_ids) {
|
||||
pieces += slot_buffers[id].SizeBytes();
|
||||
}
|
||||
static const u64 want = [&]() -> u64 {
|
||||
u64 fail_above = ~0ull;
|
||||
if (const char* f = std::getenv("SHADPS4_BIG_GROW_PROBE_FAIL_ABOVE_MB")) {
|
||||
fail_above = std::strtoull(f, nullptr, 10) << 20;
|
||||
}
|
||||
const VmaAllocator allocator = instance.GetAllocator();
|
||||
const vk::BufferUsageFlags flags = AllFlags | vk::BufferUsageFlagBits::eShaderDeviceAddress;
|
||||
// The tier, then the smaller tiers (3 GB, 2.75 GB) as steps down; below 7 GiB of video
|
||||
// memory (the 2.75 GB tier) also 2.5 GB: the city's mesh memory reached 2790 MB (s1558),
|
||||
// so that one regrows once in the scene, still better than system RAM (GitHub #30).
|
||||
boost::container::small_vector<u64, 4> sizes{big_grow_bytes};
|
||||
for (const u64 step : {u64{0xC0000000ull}, u64{0xB0000000ull}}) {
|
||||
if (step < big_grow_bytes) {
|
||||
sizes.push_back(step);
|
||||
}
|
||||
}
|
||||
if (big_grow_bytes <= 0xB0000000ull) {
|
||||
sizes.push_back(0xA0000000ull);
|
||||
}
|
||||
const auto& mp = instance.GetMemoryProperties();
|
||||
for (const u64 size : sizes) {
|
||||
const u64 ask = size > fail_above ? (64ull << 30) : size;
|
||||
// 1.1.1 (GitHub #30, RX 5600 XT 6 GB): the probe used to count any heap as a fit -
|
||||
// VMA's budget is its own 80%-of-heap estimate (4697 of 5872 MiB), so the 2816 MB
|
||||
// buffer went to system RAM (heap 1) although the card had room (2370 MB in use).
|
||||
// Now: video memory only, sized against the driver's budget when it gives one.
|
||||
// Without a driver budget: the old probe, but only a device-local heap counts.
|
||||
const std::optional<bool> in_budget = MeshFitsVideo(instance, ask, pieces);
|
||||
u32 heap = 0;
|
||||
bool fits = false;
|
||||
bool system_heap = false;
|
||||
if (in_budget.value_or(true)) {
|
||||
fits = ProbeBufferAllocation(instance, MemoryUsage::DeviceLocal, flags, ask, heap,
|
||||
/*require_video=*/in_budget.has_value());
|
||||
system_heap =
|
||||
fits && !(mp.memoryHeaps[heap].flags & vk::MemoryHeapFlagBits::eDeviceLocal);
|
||||
fits = fits && !system_heap;
|
||||
}
|
||||
if (fits) {
|
||||
LOG_INFO(Render_Vulkan, "BIG_GROW: the {} MB mesh buffer fits (heap {}){}; {}",
|
||||
size >> 20, heap, size < big_grow_bytes ? " - stepped down" : "",
|
||||
Vulkan::DescribeDriverBudgets(allocator));
|
||||
return size;
|
||||
}
|
||||
LOG_WARNING(Render_Vulkan,
|
||||
"BIG_GROW: a {} MB mesh buffer does not fit in video memory{}{}; {}; {}; {}",
|
||||
ask >> 20,
|
||||
in_budget == false ? " (over the driver's budget with a 256 MB margin)"
|
||||
: system_heap ? " (it would land in system memory)"
|
||||
: "",
|
||||
ask != size ? " (test: SHADPS4_BIG_GROW_PROBE_FAIL_ABOVE_MB)" : "",
|
||||
Vulkan::DescribeDeviceLocalBudgets(allocator),
|
||||
Vulkan::DescribeDriverBudgets(allocator),
|
||||
Common::DescribeSystemMemory());
|
||||
}
|
||||
LOG_WARNING(Render_Vulkan, "BIG_GROW: no mesh buffer size fits - the old growth path "
|
||||
"(the mesh memory regrows as blocks stream in)");
|
||||
return 0;
|
||||
}();
|
||||
const u64 mapped = memory->TryClampRangeSize(begin, want);
|
||||
VAddr grow_end = Common::AlignDown(begin + mapped, CACHING_PAGESIZE);
|
||||
if (mapped > cur && grow_end > end) {
|
||||
@@ -7402,6 +7491,7 @@ BufferCache::OverlapResult BufferCache::ResolveOverlaps(VAddr device_addr, u32 w
|
||||
.begin = begin,
|
||||
.end = end,
|
||||
.has_stream_leap = has_stream_leap,
|
||||
.mesh = mesh,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7491,9 +7581,16 @@ BufferId BufferCache::CreateBuffer(VAddr device_addr, u32 wanted_size) {
|
||||
big_old = std::max(big_old, s);
|
||||
}
|
||||
}
|
||||
// 1.1.1 (GitHub #30): BIG_GROW's mesh buffer goes to video memory whenever the driver's budget
|
||||
// has room for it now (the joined buffers still alive), not where VMA's estimate sends it.
|
||||
std::optional<RequireVideoMemory> video;
|
||||
if (overlap.mesh && MeshFitsVideo(instance, size, big_copy).value_or(false)) {
|
||||
video.emplace();
|
||||
}
|
||||
const BufferId new_buffer_id =
|
||||
slot_buffers.insert(instance, scheduler, MemoryUsage::DeviceLocal, overlap.begin,
|
||||
AllFlags | vk::BufferUsageFlagBits::eShaderDeviceAddress, size);
|
||||
video.reset();
|
||||
auto& new_buffer = slot_buffers[new_buffer_id];
|
||||
new_buffer.memo_serial = ++buffer_memo_serial;
|
||||
for (const BufferId overlap_id : overlap.ids) {
|
||||
@@ -8229,6 +8326,23 @@ vk::Buffer BufferCache::UploadCopies(Buffer& buffer, std::span<vk::BufferCopy> c
|
||||
staging_buffer.Commit();
|
||||
return staging_buffer.Handle();
|
||||
} else {
|
||||
// 1.1.1 (GitHub #36, RTX 5070 12 GB, 32 GB RAM, driver 621.10): the 3.75 GB mesh buffer's
|
||||
// first upload asked for one 3822 MB host buffer and the driver refused it (error -2, with
|
||||
// 12.7 GB of the system heap's budget free) - the game closed at the loading screen. Probed
|
||||
// first; refused -> UploadCopiesInPieces (256 MB at a time). PCs where it fits are unchanged.
|
||||
// SHADPS4_UPLOAD_PIECES_TEST=1 (test): every upload above 256 MB takes the pieces path.
|
||||
static const bool pieces_test = [] {
|
||||
const char* v = std::getenv("SHADPS4_UPLOAD_PIECES_TEST");
|
||||
return v && v[0] == '1';
|
||||
}();
|
||||
u32 heap = 0;
|
||||
if (total_size_bytes > UploadPieceSize &&
|
||||
(pieces_test ||
|
||||
!ProbeBufferAllocation(instance, MemoryUsage::Upload,
|
||||
vk::BufferUsageFlagBits::eTransferSrc, total_size_bytes, heap))) {
|
||||
UploadCopiesInPieces(buffer, copies, total_size_bytes, pieces_test);
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
// For large one time transfers use a temporary host buffer.
|
||||
auto temp_buffer =
|
||||
std::make_unique<Buffer>(instance, scheduler, MemoryUsage::Upload, 0,
|
||||
@@ -8245,6 +8359,83 @@ vk::Buffer BufferCache::UploadCopies(Buffer& buffer, std::span<vk::BufferCopy> c
|
||||
}
|
||||
}
|
||||
|
||||
void BufferCache::UploadCopiesInPieces(Buffer& buffer, std::span<const vk::BufferCopy> copies,
|
||||
size_t total_size_bytes, bool test) {
|
||||
// One host buffer of UploadPieceSize, refilled per piece; the GPU finishes each piece's copies
|
||||
// before the next fill (once per mesh buffer at a loading screen, so the waits do not matter).
|
||||
auto piece = std::make_unique<Buffer>(instance, scheduler, MemoryUsage::Upload, 0,
|
||||
vk::BufferUsageFlagBits::eTransferSrc, UploadPieceSize);
|
||||
u8* const staging = piece->mapped_data.data();
|
||||
const vk::BufferMemoryBarrier2 pre_barrier = {
|
||||
.srcStageMask = vk::PipelineStageFlagBits2::eAllCommands,
|
||||
.srcAccessMask = vk::AccessFlagBits2::eMemoryRead | vk::AccessFlagBits2::eMemoryWrite,
|
||||
.dstStageMask = vk::PipelineStageFlagBits2::eTransfer,
|
||||
.dstAccessMask = vk::AccessFlagBits2::eTransferWrite,
|
||||
.buffer = buffer.Handle(),
|
||||
.offset = 0,
|
||||
.size = buffer.SizeBytes(),
|
||||
};
|
||||
const vk::BufferMemoryBarrier2 post_barrier = {
|
||||
.srcStageMask = vk::PipelineStageFlagBits2::eTransfer,
|
||||
.srcAccessMask = vk::AccessFlagBits2::eTransferWrite,
|
||||
.dstStageMask = vk::PipelineStageFlagBits2::eAllCommands,
|
||||
.dstAccessMask = vk::AccessFlagBits2::eMemoryRead | vk::AccessFlagBits2::eMemoryWrite,
|
||||
.buffer = buffer.Handle(),
|
||||
.offset = 0,
|
||||
.size = buffer.SizeBytes(),
|
||||
};
|
||||
boost::container::small_vector<vk::BufferCopy, 16> batch;
|
||||
u64 used = 0;
|
||||
u32 pieces = 0;
|
||||
const auto submit = [&] {
|
||||
if (batch.empty()) {
|
||||
return;
|
||||
}
|
||||
scheduler.EndRendering();
|
||||
{
|
||||
auto access = scheduler.AcquireCommandBuffer();
|
||||
const auto cmdbuf = access.Handle();
|
||||
cmdbuf.pipelineBarrier2(vk::DependencyInfo{
|
||||
.dependencyFlags = vk::DependencyFlagBits::eByRegion,
|
||||
.bufferMemoryBarrierCount = 1,
|
||||
.pBufferMemoryBarriers = &pre_barrier,
|
||||
});
|
||||
cmdbuf.copyBuffer(piece->Handle(), buffer.Handle(), batch);
|
||||
cmdbuf.pipelineBarrier2(vk::DependencyInfo{
|
||||
.dependencyFlags = vk::DependencyFlagBits::eByRegion,
|
||||
.bufferMemoryBarrierCount = 1,
|
||||
.pBufferMemoryBarriers = &post_barrier,
|
||||
});
|
||||
}
|
||||
scheduler.Finish(); // the piece buffer is refilled next
|
||||
batch.clear();
|
||||
used = 0;
|
||||
++pieces;
|
||||
};
|
||||
for (const vk::BufferCopy& copy : copies) {
|
||||
u64 done = 0;
|
||||
while (done < copy.size) {
|
||||
if (used == UploadPieceSize) {
|
||||
submit();
|
||||
}
|
||||
const u64 n = std::min<u64>(copy.size - done, UploadPieceSize - used);
|
||||
const VAddr device_addr = buffer.CpuAddr() + copy.dstOffset + done;
|
||||
memory->CopySparseMemory(device_addr, staging + used, n);
|
||||
batch.push_back({used, copy.dstOffset + done, n});
|
||||
used += n;
|
||||
done += n;
|
||||
}
|
||||
}
|
||||
submit();
|
||||
LOG_WARNING(Render_Vulkan,
|
||||
"Large upload of {} MB in {} pieces of up to {} MB ({}); {}", total_size_bytes >> 20,
|
||||
pieces, UploadPieceSize >> 20,
|
||||
test ? "SHADPS4_UPLOAD_PIECES_TEST" : "one host buffer of that size was refused",
|
||||
Vulkan::DescribeDeviceLocalBudgets(instance.GetAllocator()));
|
||||
scheduler.DeferOperation([piece = std::move(piece)]() mutable { piece.reset(); });
|
||||
TouchBuffer(buffer);
|
||||
}
|
||||
|
||||
bool BufferCache::SynchronizeBufferFromImage(Buffer& buffer, VAddr device_addr, u32 size) {
|
||||
WriterTagScope census_tag{*this, 7};
|
||||
buffer.NoteMainAccess(scheduler.CurrentTick(), true);
|
||||
|
||||
@@ -186,6 +186,7 @@ public:
|
||||
VAddr begin;
|
||||
VAddr end;
|
||||
bool has_stream_leap = false;
|
||||
bool mesh = false; ///< a BIG_GROW mesh-sized join: video memory if it fits (GitHub #30)
|
||||
};
|
||||
|
||||
public:
|
||||
@@ -880,6 +881,9 @@ private:
|
||||
vk::Buffer UploadCopies(Buffer& buffer, std::span<vk::BufferCopy> copies,
|
||||
size_t total_size_bytes);
|
||||
|
||||
void UploadCopiesInPieces(Buffer& buffer, std::span<const vk::BufferCopy> copies,
|
||||
size_t total_size_bytes, bool test);
|
||||
|
||||
bool SynchronizeBufferFromImage(Buffer& buffer, VAddr device_addr, u32 size);
|
||||
|
||||
void WriteDataBuffer(Buffer& buffer, VAddr address, const void* value, u32 num_bytes);
|
||||
|
||||
@@ -13,6 +13,7 @@ set(SHADER_FILES
|
||||
detilers/micro_8bpp.comp
|
||||
color_to_ms_depth.frag
|
||||
dlss_composite.comp
|
||||
framegen_depth.comp
|
||||
dlss5_compose.comp
|
||||
dlss5_compose.frag
|
||||
ms_image_blit.frag
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#version 450
|
||||
|
||||
// issn-axy: frame generation (issn_framegen.cpp). The game's depth is reversed (near = 1, far
|
||||
// = 0); the FSR 3.1 frame interpolation shaders the bridge embeds read near = 0, far = 1. Turned
|
||||
// round in place (1 - d, the standard depth of an infinite far plane), so the interpolation
|
||||
// keeps the character in front of what moves behind him.
|
||||
|
||||
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
|
||||
|
||||
layout(set = 0, binding = 0, r32f) uniform image2D depth;
|
||||
|
||||
void main() {
|
||||
const ivec2 pos = ivec2(gl_GlobalInvocationID.xy);
|
||||
const ivec2 size = imageSize(depth);
|
||||
if (pos.x >= size.x || pos.y >= size.y) {
|
||||
return;
|
||||
}
|
||||
imageStore(depth, pos, vec4(1.0 - imageLoad(depth, pos).r));
|
||||
}
|
||||
@@ -21,7 +21,11 @@
|
||||
|
||||
#ifndef _WIN64
|
||||
#include <sys/mman.h>
|
||||
#ifdef __APPLE__
|
||||
#include <sys/ucontext.h>
|
||||
#else
|
||||
#include <ucontext.h>
|
||||
#endif
|
||||
#include "common/adaptive_mutex.h"
|
||||
#ifdef ENABLE_USERFAULTFD
|
||||
#include <thread>
|
||||
@@ -212,7 +216,11 @@ static bool ReadFaultRipEnabled() {
|
||||
static void SnapshotReadFaultPage(VAddr) {}
|
||||
static void LogReadFaultRip(void*, VAddr) {}
|
||||
static u64 FaultRip(void* context) {
|
||||
#ifdef __APPLE__
|
||||
return static_cast<u64>(static_cast<ucontext_t*>(context)->uc_mcontext->__ss.__rip);
|
||||
#else
|
||||
return static_cast<u64>(static_cast<ucontext_t*>(context)->uc_mcontext.gregs[REG_RIP]);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -0,0 +1,689 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// issn-axy: FSR 3.1 frame generation (issn_framegen.h).
|
||||
#include "video_core/renderer_vulkan/issn_framegen.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
#include "common/logging/log.h"
|
||||
#include "ffx_vk_fsr3_3_1_5_bridge.h"
|
||||
#include "video_core/buffer_cache/buffer.h"
|
||||
#include "video_core/host_shaders/framegen_depth_comp.h"
|
||||
#include "video_core/renderer_vulkan/vk_dlss.h"
|
||||
#include "video_core/renderer_vulkan/vk_instance.h"
|
||||
#include "video_core/renderer_vulkan/vk_platform.h"
|
||||
#include "video_core/renderer_vulkan/vk_scheduler.h"
|
||||
#include "video_core/renderer_vulkan/vk_shader_util.h"
|
||||
#include "video_core/texture_cache/image.h"
|
||||
|
||||
namespace Vulkan::FrameGen {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr vk::Format ColorFormat = vk::Format::eR8G8B8A8Unorm; // FSR 3.1's SDR colour
|
||||
// FFX_FRAMEINTERPOLATION_ENABLE_DEPTH_INVERTED would match the game's reversed depth, but the
|
||||
// bridge embeds only the shader variants of the default (10-30: creating the context with it
|
||||
// failed, FFX_ERROR_INVALID_ARGUMENT at the pipeline bindings). The Bloodborne port passes its
|
||||
// reversed depth the same way; the interpolation then judges near and far the other way round
|
||||
// at object edges only.
|
||||
constexpr u32 InterpolationFlags = 0u;
|
||||
|
||||
constexpr vk::ImageSubresourceRange ColorRange{
|
||||
.aspectMask = vk::ImageAspectFlagBits::eColor,
|
||||
.levelCount = 1,
|
||||
.layerCount = 1,
|
||||
};
|
||||
constexpr vk::ImageSubresourceLayers ColorLayers{
|
||||
.aspectMask = vk::ImageAspectFlagBits::eColor,
|
||||
.mipLevel = 0,
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = 1,
|
||||
};
|
||||
constexpr vk::ImageUsageFlags Usage = vk::ImageUsageFlagBits::eSampled |
|
||||
vk::ImageUsageFlagBits::eStorage |
|
||||
vk::ImageUsageFlagBits::eTransferSrc |
|
||||
vk::ImageUsageFlagBits::eTransferDst;
|
||||
|
||||
std::atomic<bool> g_failed{false};
|
||||
std::atomic<u64> g_generated{0}; ///< GeneratedCount
|
||||
std::atomic<int> g_enabled{-1}; ///< the menu's switch; -1 = not read from ISSN_FRAME_GEN yet
|
||||
std::atomic<bool> g_reset_request{false}; ///< switched on again: start a new history
|
||||
|
||||
struct Owned {
|
||||
VideoCore::UniqueImage image;
|
||||
vk::UniqueImageView view; ///< destroyed before the image (declared after it)
|
||||
vk::ImageLayout layout = vk::ImageLayout::eUndefined;
|
||||
vk::Format format = vk::Format::eUndefined;
|
||||
u32 width = 0, height = 0;
|
||||
};
|
||||
|
||||
void To(vk::CommandBuffer cmdbuf, Owned& owned, vk::ImageLayout layout) {
|
||||
cmdbuf.pipelineBarrier(
|
||||
vk::PipelineStageFlagBits::eAllCommands, vk::PipelineStageFlagBits::eAllCommands, {}, {},
|
||||
{},
|
||||
vk::ImageMemoryBarrier{
|
||||
.srcAccessMask = vk::AccessFlagBits::eMemoryWrite | vk::AccessFlagBits::eMemoryRead,
|
||||
.dstAccessMask = vk::AccessFlagBits::eMemoryWrite | vk::AccessFlagBits::eMemoryRead,
|
||||
.oldLayout = owned.layout,
|
||||
.newLayout = layout,
|
||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.image = owned.image,
|
||||
.subresourceRange = ColorRange,
|
||||
});
|
||||
owned.layout = layout;
|
||||
}
|
||||
|
||||
void Barrier(vk::CommandBuffer cmdbuf, vk::Image image, vk::ImageLayout from, vk::ImageLayout to) {
|
||||
cmdbuf.pipelineBarrier(
|
||||
vk::PipelineStageFlagBits::eAllCommands, vk::PipelineStageFlagBits::eAllCommands, {}, {},
|
||||
{},
|
||||
vk::ImageMemoryBarrier{
|
||||
.srcAccessMask = vk::AccessFlagBits::eMemoryWrite | vk::AccessFlagBits::eMemoryRead,
|
||||
.dstAccessMask = vk::AccessFlagBits::eMemoryWrite | vk::AccessFlagBits::eMemoryRead,
|
||||
.oldLayout = from,
|
||||
.newLayout = to,
|
||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.image = image,
|
||||
.subresourceRange = ColorRange,
|
||||
});
|
||||
}
|
||||
|
||||
void Blit(vk::CommandBuffer cmdbuf, vk::Image source, vk::ImageLayout source_layout, u32 sw,
|
||||
u32 sh, vk::Image dest, u32 dw, u32 dh) {
|
||||
const vk::ImageBlit blit{
|
||||
.srcSubresource = ColorLayers,
|
||||
.srcOffsets = std::array{vk::Offset3D{0, 0, 0}, vk::Offset3D{s32(sw), s32(sh), 1}},
|
||||
.dstSubresource = ColorLayers,
|
||||
.dstOffsets = std::array{vk::Offset3D{0, 0, 0}, vk::Offset3D{s32(dw), s32(dh), 1}},
|
||||
};
|
||||
cmdbuf.blitImage(source, source_layout, dest, vk::ImageLayout::eTransferDstOptimal, blit,
|
||||
vk::Filter::eNearest);
|
||||
}
|
||||
|
||||
struct State {
|
||||
const Instance& instance;
|
||||
Scheduler& scheduler;
|
||||
std::mutex mutex;
|
||||
Owned depth, motion; ///< this frame's capture (render size)
|
||||
Owned color, output; ///< the finished frame and the generated one (display size)
|
||||
Owned composite; ///< the generated scene with the HUD over it (HUD-free path)
|
||||
std::unique_ptr<VideoCore::UniqueBuffer> depth_buffer;
|
||||
u64 depth_buffer_size = 0;
|
||||
std::deque<std::pair<u64, Owned>> retired_images;
|
||||
std::deque<std::pair<u64, std::unique_ptr<VideoCore::UniqueBuffer>>> retired_buffers;
|
||||
bool captured = false; ///< a capture since the last TakeCapture
|
||||
float jitter_x = 0.0f, jitter_y = 0.0f; ///< its jitter
|
||||
Ticket ticket; ///< the flip being generated (Interpolate)
|
||||
vk::UniqueDescriptorSetLayout depth_set_layout;
|
||||
vk::UniquePipelineLayout depth_layout;
|
||||
vk::UniquePipeline depth_pipeline;
|
||||
bool depth_pipeline_failed = false;
|
||||
FfxVkFsr3_3_1_6FrameGenerationContext* context = nullptr;
|
||||
u32 ctx_width = 0, ctx_height = 0, ctx_render_w = 0, ctx_render_h = 0;
|
||||
u64 frame_id = 0;
|
||||
std::deque<std::pair<u64, u64>> pending; ///< frame id, scheduler tick
|
||||
bool reset = true;
|
||||
u32 misses = 0; ///< flips in a row without a capture
|
||||
std::chrono::steady_clock::time_point last_generate{};
|
||||
u64 generated = 0, copied = 0;
|
||||
std::chrono::steady_clock::time_point last_summary{};
|
||||
u64 summary_generated = 0, summary_copied = 0;
|
||||
std::string problem;
|
||||
|
||||
State(const Instance& instance_, Scheduler& scheduler_)
|
||||
: instance{instance_}, scheduler{scheduler_} {}
|
||||
|
||||
void Ensure(Owned& owned, vk::Format format, u32 width, u32 height) {
|
||||
if (owned.image && owned.format == format && owned.width == width &&
|
||||
owned.height == height) {
|
||||
return;
|
||||
}
|
||||
if (owned.image) {
|
||||
retired_images.emplace_back(scheduler.CurrentTick(), std::move(owned));
|
||||
}
|
||||
owned.image = VideoCore::UniqueImage(instance.GetDevice(), instance.GetAllocator());
|
||||
owned.image.Create(vk::ImageCreateInfo{
|
||||
.imageType = vk::ImageType::e2D,
|
||||
.format = format,
|
||||
.extent{width, height, 1},
|
||||
.mipLevels = 1,
|
||||
.arrayLayers = 1,
|
||||
.samples = vk::SampleCountFlagBits::e1,
|
||||
.usage = Usage,
|
||||
.initialLayout = vk::ImageLayout::eUndefined,
|
||||
});
|
||||
owned.view = Check<"create frame generation view">(
|
||||
instance.GetDevice().createImageViewUnique(vk::ImageViewCreateInfo{
|
||||
.image = owned.image,
|
||||
.viewType = vk::ImageViewType::e2D,
|
||||
.format = format,
|
||||
.subresourceRange = ColorRange,
|
||||
}));
|
||||
owned.layout = vk::ImageLayout::eUndefined;
|
||||
owned.format = format;
|
||||
owned.width = width;
|
||||
owned.height = height;
|
||||
}
|
||||
|
||||
void Retire() {
|
||||
while (context && !pending.empty() && scheduler.IsFree(pending.front().second)) {
|
||||
ffxVkFsr3_3_1_6FrameGenerationContextRetireFrame(context, pending.front().first);
|
||||
pending.pop_front();
|
||||
}
|
||||
while (!retired_images.empty() && scheduler.IsFree(retired_images.front().first)) {
|
||||
retired_images.pop_front();
|
||||
}
|
||||
while (!retired_buffers.empty() && scheduler.IsFree(retired_buffers.front().first)) {
|
||||
retired_buffers.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
void DestroyContext() {
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
// A size change (rare): the old context's submitted frames must be done. Only the
|
||||
// device is waited for: this runs while a command buffer is being recorded.
|
||||
(void)instance.GetDevice().waitIdle();
|
||||
ffxVkFsr3_3_1_6FrameGenerationContextRetireFrame(context, frame_id);
|
||||
ffxVkFsr3_3_1_6FrameGenerationContextDestroy(context);
|
||||
context = nullptr;
|
||||
pending.clear();
|
||||
}
|
||||
|
||||
void Fail(std::string reason) {
|
||||
if (problem != reason) {
|
||||
LOG_WARNING(Render_Vulkan, "Frame generation off: {}", reason);
|
||||
std::printf("Frame generation: off (%s)\n", reason.c_str());
|
||||
std::fflush(stdout);
|
||||
}
|
||||
problem = std::move(reason);
|
||||
g_failed.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void Capture(vk::CommandBuffer cmdbuf, const Fsr4Aa::Input& d, const Fsr4Aa::Input& m, float jx,
|
||||
float jy) {
|
||||
Retire();
|
||||
Ensure(depth, vk::Format::eR32Sfloat, d.width, d.height);
|
||||
Ensure(motion, vk::Format::eR16G16Sfloat, m.width, m.height);
|
||||
const u64 depth_bytes = u64(d.width) * d.height * 4;
|
||||
if (!depth_buffer || depth_buffer_size < depth_bytes) {
|
||||
if (depth_buffer) {
|
||||
retired_buffers.emplace_back(scheduler.CurrentTick(), std::move(depth_buffer));
|
||||
}
|
||||
depth_buffer = std::make_unique<VideoCore::UniqueBuffer>(instance.GetDevice(),
|
||||
instance.GetAllocator());
|
||||
depth_buffer->Create(vk::BufferCreateInfo{
|
||||
.size = depth_bytes,
|
||||
.usage = vk::BufferUsageFlagBits::eTransferSrc |
|
||||
vk::BufferUsageFlagBits::eTransferDst,
|
||||
},
|
||||
VideoCore::MemoryUsage::DeviceLocal, nullptr);
|
||||
depth_buffer_size = depth_bytes;
|
||||
}
|
||||
To(cmdbuf, depth, vk::ImageLayout::eTransferDstOptimal);
|
||||
To(cmdbuf, motion, vk::ImageLayout::eTransferDstOptimal);
|
||||
Blit(cmdbuf, m.image, vk::ImageLayout::eTransferSrcOptimal, m.width, m.height,
|
||||
motion.image, m.width, m.height);
|
||||
// The depth aspect of D32S8 goes through a buffer into R32F (as FSR 4's CopyInputs).
|
||||
const vk::BufferImageCopy depth_copy{
|
||||
.bufferOffset = 0,
|
||||
.bufferRowLength = 0,
|
||||
.bufferImageHeight = 0,
|
||||
.imageSubresource{
|
||||
.aspectMask = vk::ImageAspectFlagBits::eDepth,
|
||||
.mipLevel = 0,
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = 1,
|
||||
},
|
||||
.imageOffset{0, 0, 0},
|
||||
.imageExtent{d.width, d.height, 1},
|
||||
};
|
||||
cmdbuf.copyImageToBuffer(d.image, vk::ImageLayout::eTransferSrcOptimal, *depth_buffer,
|
||||
depth_copy);
|
||||
cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
|
||||
vk::PipelineStageFlagBits::eTransfer, {},
|
||||
vk::MemoryBarrier{
|
||||
.srcAccessMask = vk::AccessFlagBits::eTransferWrite,
|
||||
.dstAccessMask = vk::AccessFlagBits::eTransferRead,
|
||||
},
|
||||
{}, {});
|
||||
vk::BufferImageCopy colour_copy = depth_copy;
|
||||
colour_copy.imageSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
|
||||
cmdbuf.copyBufferToImage(*depth_buffer, depth.image, vk::ImageLayout::eTransferDstOptimal,
|
||||
colour_copy);
|
||||
To(cmdbuf, depth, vk::ImageLayout::eGeneral);
|
||||
To(cmdbuf, motion, vk::ImageLayout::eGeneral);
|
||||
jitter_x = jx;
|
||||
jitter_y = jy;
|
||||
captured = true;
|
||||
}
|
||||
|
||||
/// The captured depth turned round in place (framegen_depth.comp). ISSN_FRAME_GEN_DEPTH=raw
|
||||
/// leaves it as the game wrote it (to compare).
|
||||
void InvertDepth(vk::CommandBuffer cmdbuf) {
|
||||
static const bool raw = [] {
|
||||
const char* v = std::getenv("ISSN_FRAME_GEN_DEPTH");
|
||||
return v && std::string_view{v} == "raw";
|
||||
}();
|
||||
if (raw || depth_pipeline_failed) {
|
||||
return;
|
||||
}
|
||||
const vk::Device device = instance.GetDevice();
|
||||
if (!depth_pipeline) {
|
||||
const vk::DescriptorSetLayoutBinding binding{
|
||||
.binding = 0,
|
||||
.descriptorType = vk::DescriptorType::eStorageImage,
|
||||
.descriptorCount = 1,
|
||||
.stageFlags = vk::ShaderStageFlagBits::eCompute,
|
||||
};
|
||||
depth_set_layout = Check<"create frame generation depth set layout">(
|
||||
device.createDescriptorSetLayoutUnique({
|
||||
.flags = vk::DescriptorSetLayoutCreateFlagBits::ePushDescriptor,
|
||||
.bindingCount = 1,
|
||||
.pBindings = &binding,
|
||||
}));
|
||||
depth_layout = Check<"create frame generation depth layout">(
|
||||
device.createPipelineLayoutUnique({
|
||||
.setLayoutCount = 1,
|
||||
.pSetLayouts = &depth_set_layout.get(),
|
||||
}));
|
||||
const auto module =
|
||||
Compile(HostShaders::FRAMEGEN_DEPTH_COMP, vk::ShaderStageFlagBits::eCompute, device);
|
||||
if (!module) {
|
||||
depth_pipeline_failed = true;
|
||||
std::printf("Frame generation: the depth pass did not compile; depth goes as the "
|
||||
"game wrote it\n");
|
||||
std::fflush(stdout);
|
||||
return;
|
||||
}
|
||||
depth_pipeline = Check<"create frame generation depth pipeline">(
|
||||
device.createComputePipelineUnique(
|
||||
{}, vk::ComputePipelineCreateInfo{
|
||||
.stage{
|
||||
.stage = vk::ShaderStageFlagBits::eCompute,
|
||||
.module = module,
|
||||
.pName = "main",
|
||||
},
|
||||
.layout = depth_layout.get(),
|
||||
}));
|
||||
device.destroyShaderModule(module);
|
||||
}
|
||||
To(cmdbuf, depth, vk::ImageLayout::eGeneral);
|
||||
const vk::DescriptorImageInfo info{.imageView = *depth.view,
|
||||
.imageLayout = vk::ImageLayout::eGeneral};
|
||||
const vk::WriteDescriptorSet write{
|
||||
.dstBinding = 0,
|
||||
.descriptorCount = 1,
|
||||
.descriptorType = vk::DescriptorType::eStorageImage,
|
||||
.pImageInfo = &info,
|
||||
};
|
||||
cmdbuf.bindPipeline(vk::PipelineBindPoint::eCompute, *depth_pipeline);
|
||||
cmdbuf.pushDescriptorSetKHR(vk::PipelineBindPoint::eCompute, *depth_layout, 0, write);
|
||||
cmdbuf.dispatch((depth.width + 7) / 8, (depth.height + 7) / 8, 1);
|
||||
To(cmdbuf, depth, vk::ImageLayout::eGeneral); // write -> FSR's reads
|
||||
}
|
||||
|
||||
bool Interpolate(vk::CommandBuffer cmdbuf, u32 width, u32 height, float frame_ms) {
|
||||
if (g_reset_request.exchange(false, std::memory_order_relaxed)) {
|
||||
reset = true; // switched on again from the menu: no history across the gap
|
||||
}
|
||||
if (context && (ctx_width != width || ctx_height != height ||
|
||||
ctx_render_w != depth.width || ctx_render_h != depth.height)) {
|
||||
DestroyContext();
|
||||
}
|
||||
if (!context) {
|
||||
const FfxVkFsr3_3_1_6FrameGenerationCreateInfo create{
|
||||
.physicalDevice = instance.GetPhysicalDevice(),
|
||||
.device = instance.GetDevice(),
|
||||
.maxRenderWidth = depth.width,
|
||||
.maxRenderHeight = depth.height,
|
||||
.displayWidth = width,
|
||||
.displayHeight = height,
|
||||
.colorFormat = static_cast<VkFormat>(ColorFormat),
|
||||
.interpolationFlags = InterpolationFlags,
|
||||
};
|
||||
if (ffxVkFsr3_3_1_6FrameGenerationContextCreate(&create, &context) !=
|
||||
FFX_VK_FSR3_3_1_6_FRAMEGEN_OK) {
|
||||
context = nullptr;
|
||||
Fail("FSR 3.1 context creation failed");
|
||||
return false;
|
||||
}
|
||||
ctx_width = width;
|
||||
ctx_height = height;
|
||||
ctx_render_w = depth.width;
|
||||
ctx_render_h = depth.height;
|
||||
reset = true;
|
||||
FfxVkFsr3_3_1_6FrameGenerationMemoryUsage usage{};
|
||||
ffxVkFsr3_3_1_6FrameGenerationContextGetMemoryUsage(context, &usage);
|
||||
LOG_WARNING(Render_Vulkan,
|
||||
"Frame generation: FSR 3.1 at {}x{} (depth and motion {}x{}), {:.1f} MB",
|
||||
width, height, depth.width, depth.height, usage.totalUsageInBytes / 1e6);
|
||||
std::printf("Frame generation: FSR 3.1, %ux%u\n", width, height);
|
||||
std::fflush(stdout);
|
||||
}
|
||||
const auto image = [](const Owned& owned) {
|
||||
return FfxVkFsr3_3_1_6FrameGenerationImage{
|
||||
.image = static_cast<VkImage>(static_cast<vk::Image>(owned.image)),
|
||||
.format = static_cast<VkFormat>(owned.format),
|
||||
.width = owned.width,
|
||||
.height = owned.height,
|
||||
.layout = VK_IMAGE_LAYOUT_GENERAL,
|
||||
.usage = static_cast<VkImageUsageFlags>(Usage),
|
||||
};
|
||||
};
|
||||
// The game's vectors are screen UV, current minus previous; FSR wants render pixels
|
||||
// towards the previous position (as FSR 4). ISSN_FRAME_GEN_MV=+ flips the sign.
|
||||
static const float mv_sign = [] {
|
||||
const char* v = std::getenv("ISSN_FRAME_GEN_MV");
|
||||
return v && v[0] == '+' ? 1.0f : -1.0f;
|
||||
}();
|
||||
InvertDepth(cmdbuf);
|
||||
const u64 id = ++frame_id;
|
||||
const FfxVkFsr3_3_1_6FrameGenerationPrepareInfo prepare{
|
||||
.commandBuffer = cmdbuf,
|
||||
.color = image(color),
|
||||
.depth = image(depth),
|
||||
.motionVectors = image(motion),
|
||||
.renderWidth = depth.width,
|
||||
.renderHeight = depth.height,
|
||||
.jitterOffsetX = ticket.jitter_x,
|
||||
.jitterOffsetY = ticket.jitter_y,
|
||||
.motionVectorScaleX = mv_sign * float(motion.width),
|
||||
.motionVectorScaleY = mv_sign * float(motion.height),
|
||||
.frameTimeMilliseconds = frame_ms,
|
||||
.minLuminance = 0.0f,
|
||||
.maxLuminance = 1.0f,
|
||||
.transferFunction = FFX_VK_FSR3_3_1_6_FRAMEGEN_TRANSFER_SRGB,
|
||||
.cameraNear = 0.1f,
|
||||
.cameraFar = 10000.0f,
|
||||
.viewSpaceToMeters = 1.0f,
|
||||
.cameraVerticalFovRadians = 1.0f,
|
||||
.cameraPosition = {0.0f, 0.0f, 0.0f},
|
||||
.cameraUp = {0.0f, 1.0f, 0.0f},
|
||||
.cameraRight = {1.0f, 0.0f, 0.0f},
|
||||
.cameraForward = {0.0f, 0.0f, 1.0f},
|
||||
.frameId = id,
|
||||
.reset = reset ? VK_TRUE : VK_FALSE,
|
||||
};
|
||||
if (ffxVkFsr3_3_1_6FrameGenerationContextRecordPrepare(context, &prepare) !=
|
||||
FFX_VK_FSR3_3_1_6_FRAMEGEN_OK) {
|
||||
pending.emplace_back(id, scheduler.CurrentTick());
|
||||
Fail("FSR 3.1 prepare failed");
|
||||
return false;
|
||||
}
|
||||
const FfxVkFsr3_3_1_6FrameGenerationDispatchInfo dispatch{
|
||||
.commandBuffer = cmdbuf,
|
||||
.color = prepare.color,
|
||||
.output = image(output),
|
||||
.distortionField = {},
|
||||
.displayWidth = width,
|
||||
.displayHeight = height,
|
||||
.frameTimeMilliseconds = frame_ms,
|
||||
.cameraNear = prepare.cameraNear,
|
||||
.cameraFar = prepare.cameraFar,
|
||||
.viewSpaceToMeters = prepare.viewSpaceToMeters,
|
||||
.cameraVerticalFovRadians = prepare.cameraVerticalFovRadians,
|
||||
.minLuminance = 0.0f,
|
||||
.maxLuminance = 1.0f,
|
||||
.transferFunction = FFX_VK_FSR3_3_1_6_FRAMEGEN_TRANSFER_SRGB,
|
||||
.frameId = id,
|
||||
.reset = prepare.reset,
|
||||
};
|
||||
const auto result = ffxVkFsr3_3_1_6FrameGenerationContextRecordDispatch(context, &dispatch);
|
||||
pending.emplace_back(id, scheduler.CurrentTick());
|
||||
if (result != FFX_VK_FSR3_3_1_6_FRAMEGEN_OK) {
|
||||
Fail("FSR 3.1 dispatch failed");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
float FrameMs() {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
float frame_ms = std::chrono::duration<float, std::milli>(now - last_generate).count();
|
||||
last_generate = now;
|
||||
return frame_ms > 1.0f && frame_ms < 200.0f ? frame_ms : 16.6f;
|
||||
}
|
||||
|
||||
void Tell(bool have_capture, bool hudless) {
|
||||
// The log says once whether the inputs arrive (10-30: a test showed no generated frames
|
||||
// and the log said nothing about frame generation) and which path the frames take.
|
||||
// Each message once (10-30: flips with and without a capture alternate while playing,
|
||||
// and the change was printed every time); Count sums them up once a minute.
|
||||
static bool told[3] = {};
|
||||
const int now = have_capture ? (hudless ? 2 : 1) : 0;
|
||||
if (!told[now]) {
|
||||
told[now] = true;
|
||||
std::printf("Frame generation: %s\n",
|
||||
now == 2 ? "depth and motion vectors arrive, interpolating the scene "
|
||||
"(the HUD is laid over it sharp)"
|
||||
: now == 1 ? "depth and motion vectors arrive, interpolating the whole "
|
||||
"frame (no HUD layer: the HUD can break up in motion)"
|
||||
: "no depth / motion capture for this frame (menu, movie, or "
|
||||
"the anti-aliasing is not FSR 4 / XeSS / DLSS): shown as is");
|
||||
std::fflush(stdout);
|
||||
}
|
||||
}
|
||||
|
||||
void Count(bool generated) {
|
||||
++(generated ? this->generated : copied);
|
||||
if (generated) {
|
||||
g_generated.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now - last_summary >= std::chrono::seconds(60)) {
|
||||
if (last_summary != std::chrono::steady_clock::time_point{}) {
|
||||
std::printf("Frame generation: last minute %llu frames generated, %llu flips "
|
||||
"without a new frame to interpolate\n",
|
||||
static_cast<unsigned long long>(this->generated - summary_generated),
|
||||
static_cast<unsigned long long>(copied - summary_copied));
|
||||
std::fflush(stdout);
|
||||
}
|
||||
last_summary = now;
|
||||
summary_generated = this->generated;
|
||||
summary_copied = copied;
|
||||
}
|
||||
}
|
||||
|
||||
Ticket TakeCapture() {
|
||||
Ticket taken{captured, jitter_x, jitter_y};
|
||||
captured = false;
|
||||
return taken;
|
||||
}
|
||||
|
||||
std::optional<SceneFrame> GenerateScene(vk::CommandBuffer cmdbuf, const Ticket& flip,
|
||||
vk::Image scene, u32 width, u32 height) {
|
||||
if (!flip.captured || g_failed.load(std::memory_order_relaxed)) {
|
||||
return std::nullopt; // Generate copies the finished frame (and says why)
|
||||
}
|
||||
Retire();
|
||||
const float frame_ms = FrameMs();
|
||||
ticket = flip;
|
||||
Tell(true, true);
|
||||
Ensure(color, ColorFormat, width, height);
|
||||
Ensure(output, ColorFormat, width, height);
|
||||
Ensure(composite, vk::Format::eR16G16B16A16Sfloat, width, height);
|
||||
// The scene (the upscaler's output, this frame's HUD not in it) into FSR's colour image;
|
||||
// the scene goes back to the layout the upscaler keeps track of.
|
||||
Barrier(cmdbuf, scene, vk::ImageLayout::eShaderReadOnlyOptimal,
|
||||
vk::ImageLayout::eTransferSrcOptimal);
|
||||
To(cmdbuf, color, vk::ImageLayout::eTransferDstOptimal);
|
||||
Blit(cmdbuf, scene, vk::ImageLayout::eTransferSrcOptimal, width, height, color.image, width,
|
||||
height);
|
||||
Barrier(cmdbuf, scene, vk::ImageLayout::eTransferSrcOptimal,
|
||||
vk::ImageLayout::eShaderReadOnlyOptimal);
|
||||
To(cmdbuf, color, vk::ImageLayout::eGeneral);
|
||||
To(cmdbuf, output, vk::ImageLayout::eGeneral);
|
||||
if (!Interpolate(cmdbuf, width, height, frame_ms)) {
|
||||
reset = true;
|
||||
return std::nullopt;
|
||||
}
|
||||
reset = false;
|
||||
misses = 0;
|
||||
To(cmdbuf, output, vk::ImageLayout::eShaderReadOnlyOptimal);
|
||||
Count(true);
|
||||
return SceneFrame{
|
||||
.scene = *output.view,
|
||||
.composite_image = composite.image,
|
||||
.composite_view = *composite.view,
|
||||
};
|
||||
}
|
||||
|
||||
bool Generate(vk::CommandBuffer cmdbuf, const Ticket& flip, vk::Image frame, vk::Image out,
|
||||
u32 width, u32 height) {
|
||||
Retire();
|
||||
const bool have_capture = flip.captured;
|
||||
const float frame_ms = FrameMs();
|
||||
ticket = flip;
|
||||
Tell(have_capture, false);
|
||||
// The composed frame (the post pass wrote it) into FSR's colour image.
|
||||
Barrier(cmdbuf, frame, vk::ImageLayout::eGeneral, vk::ImageLayout::eGeneral);
|
||||
Barrier(cmdbuf, out, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal);
|
||||
bool generated = false;
|
||||
if (have_capture && !g_failed.load(std::memory_order_relaxed)) {
|
||||
Ensure(color, ColorFormat, width, height);
|
||||
Ensure(output, ColorFormat, width, height);
|
||||
To(cmdbuf, color, vk::ImageLayout::eTransferDstOptimal);
|
||||
Blit(cmdbuf, frame, vk::ImageLayout::eGeneral, width, height, color.image, width,
|
||||
height);
|
||||
To(cmdbuf, color, vk::ImageLayout::eGeneral);
|
||||
To(cmdbuf, output, vk::ImageLayout::eGeneral);
|
||||
generated = Interpolate(cmdbuf, width, height, frame_ms);
|
||||
if (generated) {
|
||||
reset = false;
|
||||
misses = 0;
|
||||
To(cmdbuf, output, vk::ImageLayout::eTransferSrcOptimal);
|
||||
Blit(cmdbuf, output.image, vk::ImageLayout::eTransferSrcOptimal, width, height,
|
||||
out, width, height);
|
||||
}
|
||||
}
|
||||
if (!generated) {
|
||||
// No inputs this frame or a failure: the frame itself. The next generation starts a
|
||||
// new history only after a few such flips (a menu, a movie): the game also flips a
|
||||
// frame now and then without rendering a new one (10-30 log: captures and flips
|
||||
// without one alternating while playing), and a reset there made the next
|
||||
// interpolated frame start from nothing.
|
||||
if (++misses >= 4 || !have_capture && g_failed.load(std::memory_order_relaxed)) {
|
||||
reset = true;
|
||||
}
|
||||
Blit(cmdbuf, frame, vk::ImageLayout::eGeneral, width, height, out, width, height);
|
||||
}
|
||||
Barrier(cmdbuf, out, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eGeneral);
|
||||
Count(generated);
|
||||
return generated;
|
||||
}
|
||||
};
|
||||
|
||||
std::mutex g_state_mutex;
|
||||
std::unique_ptr<State> g_state;
|
||||
|
||||
State& Get(const Instance& instance, Scheduler& scheduler) {
|
||||
std::scoped_lock lock{g_state_mutex};
|
||||
if (!g_state) {
|
||||
g_state = std::make_unique<State>(instance, scheduler);
|
||||
}
|
||||
return *g_state;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Wanted() {
|
||||
static const bool capable = [] {
|
||||
const int mode = Dlss::Mode();
|
||||
const char* off = std::getenv("ISSN_FRAME_GEN");
|
||||
// ISSN_FRAME_GEN=off: not even available from the menu (fewer presentation frames).
|
||||
return (mode == 1 || mode == 3 || mode == 4) && !(off && std::string_view{off} == "off");
|
||||
}();
|
||||
return capable;
|
||||
}
|
||||
|
||||
bool Enabled() {
|
||||
int on = g_enabled.load(std::memory_order_relaxed);
|
||||
if (on < 0) {
|
||||
const char* v = std::getenv("ISSN_FRAME_GEN");
|
||||
on = v && v[0] == '1' ? 1 : 0;
|
||||
int expected = -1;
|
||||
g_enabled.compare_exchange_strong(expected, on);
|
||||
on = g_enabled.load(std::memory_order_relaxed);
|
||||
}
|
||||
return on == 1;
|
||||
}
|
||||
|
||||
void SetEnabled(bool on) {
|
||||
if (Enabled() == on) {
|
||||
return;
|
||||
}
|
||||
g_enabled.store(on ? 1 : 0, std::memory_order_relaxed);
|
||||
if (on) {
|
||||
g_reset_request.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
std::printf("Frame generation: %s from the menu\n", on ? "on" : "off");
|
||||
std::fflush(stdout);
|
||||
}
|
||||
|
||||
bool Active() {
|
||||
return Wanted() && Enabled() && !g_failed.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void Capture(const Instance& instance, Scheduler& scheduler, vk::CommandBuffer cmdbuf,
|
||||
const Fsr4Aa::Input& depth, const Fsr4Aa::Input& motion, float jitter_x,
|
||||
float jitter_y) {
|
||||
if (!Active()) {
|
||||
return;
|
||||
}
|
||||
State& state = Get(instance, scheduler);
|
||||
std::scoped_lock lock{state.mutex};
|
||||
state.Capture(cmdbuf, depth, motion, jitter_x, jitter_y);
|
||||
}
|
||||
|
||||
Ticket TakeCapture() {
|
||||
std::scoped_lock lock{g_state_mutex};
|
||||
if (!g_state) {
|
||||
return {};
|
||||
}
|
||||
std::scoped_lock state_lock{g_state->mutex};
|
||||
return g_state->TakeCapture();
|
||||
}
|
||||
|
||||
bool Generate(const Instance& instance, Scheduler& scheduler, vk::CommandBuffer cmdbuf,
|
||||
const Ticket& ticket, vk::Image frame, vk::Image out, u32 width, u32 height) {
|
||||
State& state = Get(instance, scheduler);
|
||||
std::scoped_lock lock{state.mutex};
|
||||
return state.Generate(cmdbuf, ticket, frame, out, width, height);
|
||||
}
|
||||
|
||||
std::optional<SceneFrame> GenerateScene(const Instance& instance, Scheduler& scheduler,
|
||||
vk::CommandBuffer cmdbuf, const Ticket& ticket,
|
||||
vk::Image scene, u32 width, u32 height) {
|
||||
State& state = Get(instance, scheduler);
|
||||
std::scoped_lock lock{state.mutex};
|
||||
return state.GenerateScene(cmdbuf, ticket, scene, width, height);
|
||||
}
|
||||
|
||||
u64 GeneratedCount() {
|
||||
return g_generated.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void Shutdown() {
|
||||
std::scoped_lock lock{g_state_mutex};
|
||||
if (g_state) {
|
||||
std::scoped_lock state_lock{g_state->mutex};
|
||||
g_state->DestroyContext();
|
||||
g_state.reset();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Vulkan::FrameGen
|
||||
@@ -0,0 +1,89 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// issn-axy: FSR 3.1 frame generation (ISSN_FRAME_GEN=1). From two finished frames of the game it
|
||||
// makes the one in between, and the presenter shows it before the game's frame: 60 FPS on the
|
||||
// screen become 120. AMD's FSR 3.1.6 frame interpolation through FireBurn/FSR-Vulkan (MIT, native
|
||||
// Vulkan, any GPU). After the Bloodborne port's FrameGenerator (Supermedo's bloodborne_pc,
|
||||
// vk_frame_generation.cpp, GPL-2.0-or-later); the inputs here are Second Son's own.
|
||||
//
|
||||
// Inputs: the depth and motion vectors are copied where the game's T2x resolve is replaced
|
||||
// (Capture, from the DLSS / FSR 4 / XeSS slot, so frame generation needs one of those as
|
||||
// anti-aliasing); the colour is the presenter's finished frame (Generate). Frames without a
|
||||
// capture (menus, movies) are shown as they are.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include "common/types.h"
|
||||
#include "video_core/renderer_vulkan/issn_fsr4.h"
|
||||
#include "video_core/renderer_vulkan/vk_common.h"
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
class Instance;
|
||||
class Scheduler;
|
||||
|
||||
namespace FrameGen {
|
||||
|
||||
/// Frame generation can run this session: the anti-aliasing is DLSS, FSR 4 or XeSS (the depth and
|
||||
/// motion capture needs that slot). The presenter keeps twice the frames then.
|
||||
bool Wanted();
|
||||
|
||||
/// On (ISSN_FRAME_GEN=1 at start, the in-game menu's switch live), Wanted, and not switched off
|
||||
/// by a failure (the game's frames are then shown alone).
|
||||
bool Active();
|
||||
|
||||
/// The in-game menu's switch (live); turning it on starts a new history.
|
||||
void SetEnabled(bool on);
|
||||
bool Enabled();
|
||||
|
||||
/// The rasterizer, where the resolve is replaced: this frame's depth (reversed Z, the depth
|
||||
/// aspect is read) and motion vectors (screen UV, current minus previous), both in
|
||||
/// TransferSrcOptimal layout, at the render size, and the frame's jitter in render pixels.
|
||||
void Capture(const Instance& instance, Scheduler& scheduler, vk::CommandBuffer cmdbuf,
|
||||
const Fsr4Aa::Input& depth, const Fsr4Aa::Input& motion, float jitter_x,
|
||||
float jitter_y);
|
||||
|
||||
/// This flip's share of the captures (the presenter, when it prepares the flip, on the GPU
|
||||
/// thread in stream order): whether a capture arrived since the last flip, and its jitter. Taken
|
||||
/// there, not where the passes are recorded: with SHADPS4_PRESENT_ASYNC the passes are recorded
|
||||
/// later, after the next frame's capture may already have run, and frames went without
|
||||
/// generation while others took the next frame's jitter (10-30 test: "game 48 + FG 38").
|
||||
struct Ticket {
|
||||
bool captured = false;
|
||||
float jitter_x = 0.0f, jitter_y = 0.0f;
|
||||
};
|
||||
Ticket TakeCapture();
|
||||
|
||||
/// The presenter, after a frame was composed into `frame` (General layout, `width` x `height`,
|
||||
/// the swapchain's SDR format): writes the frame between the previous one and this into `out`
|
||||
/// (same size and format; left in General layout). Without a capture for this frame, or on a
|
||||
/// failure, `out` gets a copy of `frame` (shown twice, as without generation). True when a frame
|
||||
/// was generated.
|
||||
bool Generate(const Instance& instance, Scheduler& scheduler, vk::CommandBuffer cmdbuf,
|
||||
const Ticket& ticket, vk::Image frame, vk::Image out, u32 width, u32 height);
|
||||
|
||||
/// The HUD-free path (the HUD layer of vk_dlss_upscale.h is on while frame generation is): the
|
||||
/// upscaler's scene image (`scene`, RGBA8 UNORM, `width` x `height`, ShaderReadOnlyOptimal and
|
||||
/// left so) is interpolated alone. On success the presenter composites this frame's HUD over
|
||||
/// `scene` into `composite` (an image of frame generation's own, RGBA16F) and runs the post pass
|
||||
/// from it into the generated frame: the HUD stays sharp instead of being interpolated. Nothing
|
||||
/// when no frame was generated (no capture, a failure): then Generate copies the finished frame.
|
||||
struct SceneFrame {
|
||||
vk::ImageView scene; ///< the generated scene, ShaderReadOnlyOptimal
|
||||
vk::Image composite_image; ///< for Dlss::CompositeJob::out_image
|
||||
vk::ImageView composite_view;
|
||||
};
|
||||
std::optional<SceneFrame> GenerateScene(const Instance& instance, Scheduler& scheduler,
|
||||
vk::CommandBuffer cmdbuf, const Ticket& ticket,
|
||||
vk::Image scene, u32 width, u32 height);
|
||||
|
||||
/// Frames really generated so far (not the copies of frames without a capture): the overlay
|
||||
/// shows their rate next to the game's ("100 FPS (game 50 + FG 50)").
|
||||
u64 GeneratedCount();
|
||||
|
||||
/// Frees the context and images (the device is idle).
|
||||
void Shutdown();
|
||||
|
||||
} // namespace FrameGen
|
||||
} // namespace Vulkan
|
||||
@@ -519,8 +519,14 @@ struct Upscaler::Impl {
|
||||
color.height != render_height) {
|
||||
if (context_ok || backend_ok) {
|
||||
// A size change (rare: the game renders 1920x1080) - the old context's frames
|
||||
// must be done first.
|
||||
// must be done first. issn: the whole device, not only this scheduler (the
|
||||
// presenter's queue may still read FSR's images; AMD driver fault, 10-27).
|
||||
scheduler.Finish();
|
||||
(void)instance.GetDevice().waitIdle();
|
||||
while (!in_flight.empty()) {
|
||||
ffxFsr4VkRetireFrame(&backend, in_flight.front().first);
|
||||
in_flight.pop_front();
|
||||
}
|
||||
if (context_ok) {
|
||||
ffxFsr4V07DestroyContext(&context, nullptr);
|
||||
context_ok = false;
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include "common/types.h"
|
||||
|
||||
// issn 11-04 (Discord poll, 13 votes): switches for the game's motion blur and chromatic aberration.
|
||||
// Each effect is one or more of the game's full-screen post passes, known by the pixel (or compute)
|
||||
// shader's program hash, as fs_a6d23116 is the T2x resolve (vk_dlss.h). With a switch off, the
|
||||
// pass is neutralised on the back (Rasterizer::PostFxDraw / PostFxDispatch): its target gets a
|
||||
// copy of its input of the same size, or, with no such input, the draw is dropped.
|
||||
//
|
||||
// Finding the hashes: Insert > Image > "Find post effects (test)" lists the full-screen passes of
|
||||
// the last frame up to the T2x resolve (after the scene, before the HUD) with their hash, target
|
||||
// and inputs, and turns any of them off live; ISSN_POSTFX_LOG=1 writes the same list to the log
|
||||
// once a second. The found hashes go in MotionBlurHashes / ChromaticHashes below, or, without a
|
||||
// rebuild, in ISSN_POSTFX_MB / ISSN_POSTFX_CA (comma-separated hex).
|
||||
namespace Vulkan::PostFx {
|
||||
|
||||
enum Effect : u32 { MotionBlur = 0, Chromatic = 1, EffectCount = 2 };
|
||||
|
||||
/// Built-in hashes per effect (CUSA00223 01.07). Empty until the in-game test names them.
|
||||
inline constexpr u64 MotionBlurHashes[] = {0};
|
||||
inline constexpr u64 ChromaticHashes[] = {0};
|
||||
|
||||
constexpr u32 MaxHashes = 8;
|
||||
struct EffectPrograms {
|
||||
std::array<u64, MaxHashes> hashes{};
|
||||
u32 count = 0;
|
||||
};
|
||||
|
||||
inline void ParseHashList(const char* text, EffectPrograms& out) {
|
||||
while (text && *text && out.count < MaxHashes) {
|
||||
char* end = nullptr;
|
||||
const u64 h = std::strtoull(text, &end, 16);
|
||||
if (end == text) {
|
||||
++text;
|
||||
continue;
|
||||
}
|
||||
if (h != 0) {
|
||||
out.hashes[out.count++] = h;
|
||||
}
|
||||
text = end;
|
||||
}
|
||||
}
|
||||
|
||||
/// The hashes of each effect: the built-in ones, then the environment's.
|
||||
inline const std::array<EffectPrograms, EffectCount>& Programs() {
|
||||
static const std::array<EffectPrograms, EffectCount> programs = [] {
|
||||
std::array<EffectPrograms, EffectCount> p{};
|
||||
const auto add = [](EffectPrograms& e, const u64* list, size_t n, const char* env) {
|
||||
for (size_t i = 0; i < n && e.count < MaxHashes; ++i) {
|
||||
if (list[i] != 0) {
|
||||
e.hashes[e.count++] = list[i];
|
||||
}
|
||||
}
|
||||
ParseHashList(std::getenv(env), e);
|
||||
};
|
||||
add(p[MotionBlur], MotionBlurHashes, std::size(MotionBlurHashes), "ISSN_POSTFX_MB");
|
||||
add(p[Chromatic], ChromaticHashes, std::size(ChromaticHashes), "ISSN_POSTFX_CA");
|
||||
return p;
|
||||
}();
|
||||
return programs;
|
||||
}
|
||||
|
||||
/// Is the effect's pass known for this game build (the menu shows the switch only then)?
|
||||
inline bool Known(Effect e) {
|
||||
return Programs()[e].count != 0;
|
||||
}
|
||||
|
||||
/// The player's switches (Insert > Image, issn.ini motion_blur / chromatic_aberration): true =
|
||||
/// the effect is off. Live.
|
||||
inline std::array<std::atomic<bool>, EffectCount> g_off{};
|
||||
|
||||
/// The test list's live switches: up to 16 program hashes turned off by hand.
|
||||
inline std::array<std::atomic<u64>, 16> g_test_off{};
|
||||
/// Some hash in g_test_off (kept by SetTestOff, so a draw reads one flag, not 16).
|
||||
inline std::atomic<bool> g_any_test{false};
|
||||
/// Test: 0 = copy the input (as the effect switches do), 1 = drop the draw.
|
||||
inline std::atomic<int> g_test_mode{0};
|
||||
|
||||
/// The menu's test section is open (the back collects the list only while it is, or with the log).
|
||||
inline std::atomic<bool> g_collect{false};
|
||||
inline bool LogEnabled() {
|
||||
static const bool on = [] {
|
||||
const char* v = std::getenv("ISSN_POSTFX_LOG");
|
||||
return v && v[0] == '1';
|
||||
}();
|
||||
return on;
|
||||
}
|
||||
|
||||
/// Anything to do on the back: one relaxed load per switch, so the draw path stays as it was
|
||||
/// while everything is on and the test is closed.
|
||||
inline bool Active() {
|
||||
if (g_collect.load(std::memory_order_relaxed) || LogEnabled()) {
|
||||
return true;
|
||||
}
|
||||
for (const auto& off : g_off) {
|
||||
if (off.load(std::memory_order_relaxed)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return g_any_test.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
enum class Action : u8 { Draw, Copy, Drop };
|
||||
|
||||
/// What to do with a pass of this program (back thread).
|
||||
inline Action Decide(u64 hash) {
|
||||
if (hash == 0) {
|
||||
return Action::Draw;
|
||||
}
|
||||
for (u32 e = 0; e < EffectCount; ++e) {
|
||||
if (!g_off[e].load(std::memory_order_relaxed)) {
|
||||
continue;
|
||||
}
|
||||
const auto& p = Programs()[e];
|
||||
for (u32 i = 0; i < p.count; ++i) {
|
||||
if (p.hashes[i] == hash) {
|
||||
return Action::Copy;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto& h : g_test_off) {
|
||||
if (h.load(std::memory_order_relaxed) == hash) {
|
||||
return g_test_mode.load(std::memory_order_relaxed) == 1 ? Action::Drop : Action::Copy;
|
||||
}
|
||||
}
|
||||
return Action::Draw;
|
||||
}
|
||||
|
||||
/// Turns one program off or on in the test list (menu).
|
||||
inline void SetTestOff(u64 hash, bool off) {
|
||||
for (auto& h : g_test_off) {
|
||||
if (h.load(std::memory_order_relaxed) == hash) {
|
||||
if (!off) {
|
||||
h.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
off = false; // already listed
|
||||
}
|
||||
}
|
||||
if (off) {
|
||||
for (auto& h : g_test_off) {
|
||||
u64 expected = 0;
|
||||
if (h.compare_exchange_strong(expected, hash, std::memory_order_relaxed)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
bool any = false;
|
||||
for (const auto& h : g_test_off) {
|
||||
any |= h.load(std::memory_order_relaxed) != 0;
|
||||
}
|
||||
g_any_test.store(any, std::memory_order_relaxed);
|
||||
}
|
||||
inline bool IsTestOff(u64 hash) {
|
||||
for (const auto& h : g_test_off) {
|
||||
if (h.load(std::memory_order_relaxed) == hash) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- The list of full-screen passes ----
|
||||
|
||||
struct Input {
|
||||
VAddr addr{};
|
||||
u16 width{}, height{};
|
||||
u16 fmt{}; ///< data format | number format << 8
|
||||
};
|
||||
struct Pass {
|
||||
u64 hash{}; ///< pixel shader (or compute shader) program hash
|
||||
u64 vs_hash{};
|
||||
VAddr target{}; ///< colour target 0 (compute: 0)
|
||||
u16 width{}, height{};
|
||||
u16 fmt{};
|
||||
u8 compute{};
|
||||
u8 action{}; ///< Action taken this frame
|
||||
u8 inputs{};
|
||||
std::array<Input, 4> in{};
|
||||
};
|
||||
|
||||
/// The frame's passes up to the T2x resolve; the back fills `building`, the resolve publishes it.
|
||||
struct List {
|
||||
std::mutex mutex;
|
||||
std::vector<Pass> last; ///< the last published frame
|
||||
u64 frame = 0; ///< published frames
|
||||
};
|
||||
inline List g_list;
|
||||
constexpr size_t MaxPasses = 96;
|
||||
|
||||
} // namespace Vulkan::PostFx
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,79 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// issn-axy: Intel XeSS Super Resolution in the slot DLSS and FSR 4 use: the game's SMAA T2x
|
||||
// temporal resolve is replaced by XeSS from the same inputs (tonemap output, motion vectors,
|
||||
// reversed-Z depth, the game's +-0.25 px jitter). ISSN_XESS=1 turns it on (vk_dlss.h Mode() == 4).
|
||||
// XeSS-SR runs on Vulkan on any vendor's GPU that has shaderStorageImageWriteWithoutFormat and
|
||||
// VK_EXT_mutable_descriptor_type (Radeon RX 5000 and later, GeForce GTX 10 and later, Arc).
|
||||
//
|
||||
// Only Intel's MIT headers are compiled in (third_party/xess-sdk). The runtime, libxess.dll, is
|
||||
// loaded at run time from the xess folder next to the EXE (ISSN_XESS_DIR overrides): the player
|
||||
// copies it there from Intel's SDK, as with DLSS. Missing DLL, a GPU without the features or any
|
||||
// failure: the game's own anti-aliasing stays.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "common/types.h"
|
||||
#include "video_core/renderer_vulkan/issn_fsr4.h"
|
||||
#include "video_core/renderer_vulkan/vk_common.h"
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
class Instance;
|
||||
class Scheduler;
|
||||
|
||||
namespace Xess {
|
||||
|
||||
/// ISSN_XESS=1.
|
||||
bool Wanted();
|
||||
|
||||
/// Before vkCreateInstance: loads libxess.dll and adds the instance extensions XeSS needs.
|
||||
void AddInstanceExtensions(std::vector<const char*>& extensions);
|
||||
|
||||
/// Before vkCreateDevice: the device extensions XeSS needs on this GPU into `out`. False when it
|
||||
/// cannot run here (no DLL, or XeSS refused the device).
|
||||
bool DeviceExtensions(vk::Instance instance, vk::PhysicalDevice physical_device,
|
||||
std::vector<const char*>& out);
|
||||
|
||||
/// The feature structures of the device being created, which XeSS's requirements are merged into:
|
||||
/// promoted features go into the Vulkan 1.1 / 1.2 / 1.3 structures, the rest (the mutable
|
||||
/// descriptor type feature) is spliced into the create info's pNext chain.
|
||||
struct FeatureTargets {
|
||||
vk::DeviceCreateInfo* create_info;
|
||||
vk::PhysicalDeviceFeatures* core;
|
||||
vk::PhysicalDeviceVulkan11Features* v11;
|
||||
vk::PhysicalDeviceVulkan12Features* v12;
|
||||
vk::PhysicalDeviceVulkan13Features* v13;
|
||||
};
|
||||
/// False when XeSS's feature query failed (the device then lacks what it needs).
|
||||
bool AddDeviceFeatures(vk::Instance instance, vk::PhysicalDevice physical_device,
|
||||
const FeatureTargets& targets);
|
||||
|
||||
/// The device was created with XeSS's extensions and features (set by vk_instance).
|
||||
bool DeviceSupported();
|
||||
void SetDeviceSupported(bool supported);
|
||||
|
||||
class Upscaler {
|
||||
public:
|
||||
Upscaler(const Instance& instance, Scheduler& scheduler);
|
||||
~Upscaler();
|
||||
|
||||
/// Records XeSS into `cmdbuf`, as Fsr4Aa::Upscaler::Record: color / motion / depth (layout
|
||||
/// TransferSrc) at the render size, `output` at the same size (XeSS's native AA) or larger
|
||||
/// (upscaling; the quality mode follows the ratio). False = not run; the caller falls back.
|
||||
bool Record(vk::CommandBuffer cmdbuf, const Fsr4Aa::Input& color, const Fsr4Aa::Input& motion,
|
||||
const Fsr4Aa::Input& depth, const Fsr4Aa::Output& output, float jitter_x,
|
||||
float jitter_y, bool reset);
|
||||
|
||||
/// The last failure, or empty; permanent ones stop further attempts.
|
||||
[[nodiscard]] const std::string& Problem() const noexcept;
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl;
|
||||
};
|
||||
|
||||
} // namespace Xess
|
||||
} // namespace Vulkan
|
||||
@@ -40,15 +40,48 @@ inline int Mode() {
|
||||
if (v && (v[0] == '1' || v[0] == '2')) {
|
||||
return v[0] - '0';
|
||||
}
|
||||
// issn 11-04 (Discord poll): ISSN_AA_OFF=1 - anti-aliasing "Off", the player's name for
|
||||
// mode 2: the T2x resolve becomes a plain copy of the tonemap output and the camera
|
||||
// jitter is held at zero (memory_patcher.cpp JitterFrame), so the picture stays still.
|
||||
const char* off = std::getenv("ISSN_AA_OFF");
|
||||
if (off && off[0] == '1') {
|
||||
return 2;
|
||||
}
|
||||
// issn-axy: ISSN_FSR4=1 - FSR 4 Native AA in the same slot (issn_fsr4.h).
|
||||
const char* fsr4 = std::getenv("ISSN_FSR4");
|
||||
return fsr4 && fsr4[0] == '1' ? 3 : 0;
|
||||
if (fsr4 && fsr4[0] == '1') {
|
||||
return 3;
|
||||
}
|
||||
// issn-axy: ISSN_XESS=1 - Intel XeSS in the same slot (issn_xess.h).
|
||||
const char* xess = std::getenv("ISSN_XESS");
|
||||
return xess && xess[0] == '1' ? 4 : 0;
|
||||
}();
|
||||
return mode;
|
||||
}
|
||||
inline bool Enabled() {
|
||||
return Mode() != 0;
|
||||
}
|
||||
/// FSR 4 or XeSS: the port's own upscalers, which share one path in the rasterizer.
|
||||
inline bool OwnUpscaler() {
|
||||
return Mode() == 3 || Mode() == 4;
|
||||
}
|
||||
/// Anti-aliasing "Off" (ISSN_AA_OFF=1 or SHADPS4_DLSS=2): no T2x, no upscaler, no jitter.
|
||||
inline bool AaOff() {
|
||||
return Mode() == 2;
|
||||
}
|
||||
/// The name players see for the running mode.
|
||||
inline const char* ModeName() {
|
||||
switch (Mode()) {
|
||||
case 3:
|
||||
return "FSR 4";
|
||||
case 4:
|
||||
return "XeSS";
|
||||
case 2:
|
||||
return "Off";
|
||||
default:
|
||||
return "DLSS";
|
||||
}
|
||||
}
|
||||
|
||||
/// Streamline is up and the feature exists on this device (=1), or the diagnostic copy (=2).
|
||||
inline std::atomic<bool> g_ready{false};
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include "common/assert.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "video_core/host_shaders/dlss_composite_comp.h"
|
||||
#include "video_core/renderer_vulkan/vk_dlss_upscale.h"
|
||||
#include "video_core/renderer_vulkan/issn_framegen.h"
|
||||
#include "video_core/renderer_vulkan/vk_instance.h"
|
||||
#include "video_core/renderer_vulkan/vk_platform.h"
|
||||
#include "video_core/renderer_vulkan/vk_shader_util.h"
|
||||
@@ -30,9 +32,12 @@ bool UpscaleWanted() {
|
||||
const char* v = std::getenv("SHADPS4_DLSS_UPSCALE");
|
||||
// issn-axy: FSR 4 upscales the same way (ISSN_FSR4_UPSCALE=0 keeps it at the game's size;
|
||||
// it only engages while the window is larger than the render size, SetOutputSize).
|
||||
// XeSS as FSR 4 (ISSN_XESS_UPSCALE=0 keeps it at the game's size).
|
||||
const char* fsr4 = std::getenv("ISSN_FSR4_UPSCALE");
|
||||
const char* xess = std::getenv("ISSN_XESS_UPSCALE");
|
||||
return (Mode() == 1 && v != nullptr && v[0] == '1') ||
|
||||
(Mode() == 3 && !(fsr4 != nullptr && fsr4[0] == '0'));
|
||||
(Mode() == 3 && !(fsr4 != nullptr && fsr4[0] == '0')) ||
|
||||
(Mode() == 4 && !(xess != nullptr && xess[0] == '0'));
|
||||
}();
|
||||
return wanted;
|
||||
}
|
||||
@@ -112,7 +117,7 @@ Upscaler::Upscaler(const Instance& instance_) : instance{instance_}, device{inst
|
||||
|
||||
Upscaler::~Upscaler() = default;
|
||||
|
||||
void Upscaler::SetOutputSize(u32 width, u32 height) {
|
||||
void Upscaler::SetOutputSize(u32 width, u32 height, bool hdr) {
|
||||
++flips;
|
||||
while (!retired.empty() && retired.front().first + 16 <= flips) {
|
||||
retired.pop_front();
|
||||
@@ -142,12 +147,33 @@ void Upscaler::SetOutputSize(u32 width, u32 height) {
|
||||
const u32 fit_h = std::max<u32>(game_h, static_cast<u32>(out_h * fit));
|
||||
output_size = force ? vk::Extent2D{game_w, game_h} : vk::Extent2D{fit_w, fit_h};
|
||||
// DLSS needs an output larger than its input; smaller windows keep DLAA in place.
|
||||
const bool on = (force || (width > game_w && height > game_h)) && g_ready.load() &&
|
||||
// issn-axy: frame generation keeps the HUD layer at any size: it interpolates the scene
|
||||
// alone and lays the newest HUD over the frame it makes (the HUD broke up in fast motion
|
||||
// when the whole frame was interpolated, 10-30).
|
||||
// issn-axy: never while the game outputs HDR. The composite replaces the game's last pass,
|
||||
// which is the one that encodes HDR (BT.2020 PQ); the composite writes SDR (sRGB decoded to
|
||||
// linear), and the post pass hands HDR frames to the display as they are, so the picture came
|
||||
// out far too dark with HDR on and the upscaler at a render size below the window (Reddit
|
||||
// reports, 10-30). Off, the upscaler works in place at the render size and the game's own
|
||||
// HDR pass runs; the post pass scales the frame to the window.
|
||||
const bool wanted_size = force || FrameGen::Active() || (width > game_w && height > game_h);
|
||||
const bool on = !hdr && wanted_size && g_ready.load() &&
|
||||
instance.IsDynamicColorWriteMaskSupported();
|
||||
static bool told_hdr = false;
|
||||
if (hdr && wanted_size && !told_hdr) {
|
||||
told_hdr = true;
|
||||
LOG_WARNING(Render_Vulkan, "{} upscale: off while the game outputs HDR (its own HDR pass "
|
||||
"runs; the upscaler works at the render size)",
|
||||
ModeName());
|
||||
std::printf("%s upscale: off while the game outputs HDR (the upscaler works at the render "
|
||||
"size; the frame is scaled to the window)\n",
|
||||
ModeName());
|
||||
std::fflush(stdout);
|
||||
}
|
||||
if (on != g_hud_layer.load(std::memory_order_relaxed)) {
|
||||
// issn-axy: a warning (the port's log level), and FSR 4 named when it is the upscaler.
|
||||
LOG_WARNING(Render_Vulkan, "{} upscale: {} (game {}x{}, output {}x{}{})",
|
||||
Mode() == 3 ? "FSR 4" : "DLSS", on ? "on" : "off", game_w, game_h,
|
||||
ModeName(), on ? "on" : "off", game_w, game_h,
|
||||
output_size.width, output_size.height, force ? ", forced" : "");
|
||||
g_hud_layer.store(on, std::memory_order_relaxed);
|
||||
}
|
||||
@@ -157,7 +183,7 @@ void Upscaler::SetOutputSize(u32 width, u32 height) {
|
||||
told_no_mask = true;
|
||||
LOG_WARNING(Render_Vulkan, "{} upscale: off - the driver has no dynamic colour write mask "
|
||||
"(the HUD layer needs it); the window gets the game's size scaled",
|
||||
Mode() == 3 ? "FSR 4" : "DLSS");
|
||||
ModeName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,6 +283,7 @@ CompositeJob Upscaler::TakeJob(vk::ImageView hud_view) {
|
||||
.layout = pipeline_layout.get(),
|
||||
.hud_view = hud_view,
|
||||
.scene_view = scene->view.get(),
|
||||
.scene_image = scene->image,
|
||||
.out_image = out->image,
|
||||
.out_view = out->view.get(),
|
||||
.size = image_size,
|
||||
|
||||
@@ -63,6 +63,7 @@ struct CompositeJob {
|
||||
vk::PipelineLayout layout;
|
||||
vk::ImageView hud_view; ///< the HUD layer (game resolution), shader-read layout
|
||||
vk::ImageView scene_view; ///< DLSS output, shader-read layout
|
||||
vk::Image scene_image; ///< its image (frame generation interpolates the HUD-free scene)
|
||||
vk::Image out_image;
|
||||
vk::ImageView out_view;
|
||||
vk::Extent2D size;
|
||||
@@ -76,7 +77,8 @@ public:
|
||||
~Upscaler();
|
||||
|
||||
/// Presenter, each flip (GPU thread): the output size; turns the HUD-layer mode on / off.
|
||||
void SetOutputSize(u32 width, u32 height);
|
||||
/// `hdr`: the game outputs HDR (BT.2020 PQ): the HUD-layer mode stays off.
|
||||
void SetOutputSize(u32 width, u32 height, bool hdr = false);
|
||||
vk::Extent2D OutputSize() const {
|
||||
return output_size;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <utility>
|
||||
#include <boost/container/small_vector.hpp>
|
||||
|
||||
@@ -39,7 +41,8 @@ GraphicsPipeline::GraphicsPipeline(
|
||||
std::shared_ptr<const Shader::Gcn::FetchShaderData> fetch_shader_,
|
||||
std::span<const vk::ShaderModule> modules, SerializationSupport& sdata, bool preloading,
|
||||
const DescSetBindings* derived /*= nullptr*/,
|
||||
u32 native_records_binding_ /*= NoNativeRecords*/, bool native_bindless_ /*= false*/)
|
||||
u32 native_records_binding_ /*= NoNativeRecords*/, bool native_bindless_ /*= false*/,
|
||||
bool refusable /*= false*/)
|
||||
: Pipeline{instance, scheduler, desc_heap, profile, pipeline_cache}, key{key_},
|
||||
fetch_shader{std::move(fetch_shader_)} {
|
||||
native_records_binding = native_records_binding_;
|
||||
@@ -472,6 +475,28 @@ GraphicsPipeline::GraphicsPipeline(
|
||||
debug_str, vk::to_string(pipeline_result));
|
||||
return;
|
||||
}
|
||||
if (pipeline_result == vk::Result::eSuccess && refusable) {
|
||||
// SHADPS4_REFUSE_VARIANT_TEST=N (test, 2026-10-22): every Nth refusable variant is treated
|
||||
// as refused by the driver (the created pipeline is dropped), to run the path below on a
|
||||
// driver that never refuses.
|
||||
static const u32 every = [] {
|
||||
const char* v = std::getenv("SHADPS4_REFUSE_VARIANT_TEST");
|
||||
return v ? static_cast<u32>(std::strtoul(v, nullptr, 10)) : 0u;
|
||||
}();
|
||||
static std::atomic<u32> made{0};
|
||||
if (every != 0 && made.fetch_add(1, std::memory_order_relaxed) % every == every - 1) {
|
||||
pipeline_result = vk::Result::eErrorUnknown;
|
||||
pipe.reset();
|
||||
}
|
||||
}
|
||||
if (pipeline_result != vk::Result::eSuccess && refusable) {
|
||||
// 1.1.1 (GitHub #25 RX 6800 XT, #31 RX 570): AMD drivers refused some NativeColour
|
||||
// variants with ErrorUnknown and this assert ended the game. A variant is optional - the
|
||||
// caller keeps the normal pipeline for that draw.
|
||||
LOG_WARNING(Render_Vulkan, "Graphics pipeline variant {} refused by the driver ({})",
|
||||
debug_str, vk::to_string(pipeline_result));
|
||||
return;
|
||||
}
|
||||
ASSERT_MSG(pipeline_result == vk::Result::eSuccess, "Failed to create graphics pipeline: {}",
|
||||
vk::to_string(pipeline_result));
|
||||
pipeline = std::move(pipe);
|
||||
|
||||
@@ -109,7 +109,8 @@ public:
|
||||
std::shared_ptr<const Shader::Gcn::FetchShaderData> fetch_shader,
|
||||
std::span<const vk::ShaderModule> modules, SerializationSupport& sdata,
|
||||
bool preloading, const DescSetBindings* derived = nullptr,
|
||||
u32 native_records_binding = NoNativeRecords, bool native_bindless = false);
|
||||
u32 native_records_binding = NoNativeRecords, bool native_bindless = false,
|
||||
bool refusable = false);
|
||||
~GraphicsPipeline();
|
||||
|
||||
static constexpr u32 NoNativeRecords = ~0u;
|
||||
@@ -122,7 +123,8 @@ public:
|
||||
std::span<const Shader::RuntimeInfo, MaxShaderStages> runtime_infos,
|
||||
const Shader::Gcn::FetchShaderData* fetch_shader, SerializationSupport& sdata);
|
||||
|
||||
/// False only for a preloaded pipeline the driver refused (the constructor logged it).
|
||||
/// False only for a preloaded or `refusable` pipeline the driver refused (the constructor
|
||||
/// logged it).
|
||||
bool IsCreated() const noexcept {
|
||||
return static_cast<bool>(pipeline);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string_view>
|
||||
#include <boost/container/static_vector.hpp>
|
||||
@@ -20,7 +21,9 @@
|
||||
#include "video_core/renderer_vulkan/vk_dlss.h"
|
||||
#include "video_core/renderer_vulkan/vk_dlss5.h"
|
||||
#include "video_core/renderer_vulkan/vk_instance.h"
|
||||
#include "video_core/renderer_vulkan/vk_memory_budget.h"
|
||||
#include "video_core/renderer_vulkan/issn_fsr4.h"
|
||||
#include "video_core/renderer_vulkan/issn_xess.h"
|
||||
#include "video_core/renderer_vulkan/vk_platform.h"
|
||||
|
||||
#include <vk_mem_alloc.h>
|
||||
@@ -282,6 +285,11 @@ bool Instance::CreateDevice() {
|
||||
return false;
|
||||
};
|
||||
|
||||
#ifdef __APPLE__
|
||||
// port: MoltenVK is a portability implementation, which must have this enabled when it
|
||||
// offers it (the name is a beta extension's, so spelled out).
|
||||
add_extension("VK_KHR_portability_subset");
|
||||
#endif
|
||||
// Required
|
||||
ASSERT_MSG(add_extension(VK_KHR_SWAPCHAIN_EXTENSION_NAME),
|
||||
"Required Vulkan extension unavailable: {}", VK_KHR_SWAPCHAIN_EXTENSION_NAME);
|
||||
@@ -402,6 +410,7 @@ bool Instance::CreateDevice() {
|
||||
add_extension(VK_KHR_COMPUTE_SHADER_DERIVATIVES_EXTENSION_NAME);
|
||||
}
|
||||
supports_memory_budget = add_extension(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME);
|
||||
g_driver_budget.store(supports_memory_budget, std::memory_order_relaxed); // vk_memory_budget.h
|
||||
if (const char* fault = std::getenv("SHADPS4_DEVICE_FAULT"); fault && fault[0] == '1') {
|
||||
device_fault = add_extension(VK_EXT_DEVICE_FAULT_EXTENSION_NAME);
|
||||
LOG_INFO(Render_Vulkan, "DeviceFault: VK_EXT_device_fault {}",
|
||||
@@ -454,6 +463,23 @@ bool Instance::CreateDevice() {
|
||||
}
|
||||
}
|
||||
|
||||
// issn-axy: ISSN_XESS=1 (issn_xess.h) - the device extensions XeSS asked for on this GPU.
|
||||
bool xess_device = false;
|
||||
if (Dlss::Mode() == 4) {
|
||||
std::vector<const char*> xess_exts;
|
||||
xess_device = Xess::DeviceExtensions(*instance, physical_device, xess_exts);
|
||||
for (const char* ext : xess_exts) {
|
||||
if (std::ranges::any_of(enabled_extensions,
|
||||
[ext](const char* e) { return std::strcmp(e, ext) == 0; })) {
|
||||
continue;
|
||||
}
|
||||
if (enabled_extensions.size() == enabled_extensions.capacity() || !add_extension(ext)) {
|
||||
xess_device = false;
|
||||
}
|
||||
}
|
||||
LOG_INFO(Render_Vulkan, "XeSS: device extensions {}", xess_device ? "enabled" : "missing");
|
||||
}
|
||||
|
||||
// Opt-in descriptor heap experiment. Nothing below is touched unless
|
||||
// SHADPS4_NATIVE_BINDINGS=1 and the device passes every capability check.
|
||||
bool native_bindings = QueryNativeBindings();
|
||||
@@ -641,7 +667,8 @@ bool Instance::CreateDevice() {
|
||||
.subgroupSizeControl = vk13_features.subgroupSizeControl,
|
||||
.synchronization2 = vk13_features.synchronization2,
|
||||
.dynamicRendering = vk13_features.dynamicRendering,
|
||||
.shaderIntegerDotProduct = Fsr4Aa::Wanted() && vk13_features.shaderIntegerDotProduct,
|
||||
.shaderIntegerDotProduct =
|
||||
(Fsr4Aa::Wanted() || xess_device) && vk13_features.shaderIntegerDotProduct,
|
||||
.maintenance4 = vk13_features.maintenance4,
|
||||
},
|
||||
// Extensions
|
||||
@@ -798,10 +825,36 @@ bool Instance::CreateDevice() {
|
||||
derivative_features.pNext = const_cast<void*>(device_ci.pNext);
|
||||
device_ci.pNext = &derivative_features;
|
||||
}
|
||||
const bool fsr4_ok = fsr4_derivatives && vk12_features.shaderFloat16 &&
|
||||
// issn 11-04: FSR 4 (its INT8 model) on AMD cards only. An RTX 5080 has every feature below,
|
||||
// chose FSR 4 in the in-game menu and lost the device a few seconds into play (GitHub issue
|
||||
// 28); AMD made the model for Radeons. ISSN_FSR4_ANY_GPU=1: any card with the features.
|
||||
static const bool fsr4_any_gpu = [] {
|
||||
const char* v = std::getenv("ISSN_FSR4_ANY_GPU");
|
||||
return v && v[0] == '1';
|
||||
}();
|
||||
const bool fsr4_vendor = fsr4_any_gpu || physical_device.getProperties().vendorID == 0x1002;
|
||||
if (Dlss::Mode() == 3 && !fsr4_vendor) {
|
||||
std::printf("FSR 4: only on AMD Radeon cards: the game's own anti-aliasing stays (choose "
|
||||
"DLSS or XeSS)\n");
|
||||
std::fflush(stdout);
|
||||
}
|
||||
const bool fsr4_ok = fsr4_vendor && fsr4_derivatives && vk12_features.shaderFloat16 &&
|
||||
vk12_features.shaderInt8 && features.shaderInt16 &&
|
||||
vk13_features.shaderIntegerDotProduct &&
|
||||
features.shaderStorageImageExtendedFormats;
|
||||
// issn-axy: ISSN_XESS - XeSS's features merged into the chain (its mutable descriptor type
|
||||
// structure spliced like the ones above).
|
||||
if (xess_device) {
|
||||
xess_device = Xess::AddDeviceFeatures(
|
||||
*instance, physical_device,
|
||||
Xess::FeatureTargets{
|
||||
.create_info = &device_chain.get<vk::DeviceCreateInfo>(),
|
||||
.core = &device_chain.get<vk::PhysicalDeviceFeatures2>().features,
|
||||
.v11 = &device_chain.get<vk::PhysicalDeviceVulkan11Features>(),
|
||||
.v12 = &device_chain.get<vk::PhysicalDeviceVulkan12Features>(),
|
||||
.v13 = &device_chain.get<vk::PhysicalDeviceVulkan13Features>(),
|
||||
});
|
||||
}
|
||||
// SHADPS4_ADDR_BIND_REPORT: the same splice.
|
||||
vk::PhysicalDeviceAddressBindingReportFeaturesEXT bind_report_features = {
|
||||
.reportAddressBinding = true,
|
||||
@@ -914,6 +967,10 @@ bool Instance::CreateDevice() {
|
||||
if (dlss_device || Dlss::Mode() == 2) {
|
||||
Dlss::DeviceCreated(*instance, physical_device, *device, queue_family_index, 0);
|
||||
}
|
||||
if (Dlss::AaOff()) {
|
||||
// Anti-aliasing "Off": nothing to load (DeviceCreated's stub off Windows sets nothing).
|
||||
Dlss::g_ready.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
if (Dlss::Mode() == 3) {
|
||||
// ISSN_FSR4: the T2x resolve is replaced once the device can run the model.
|
||||
Fsr4Aa::SetDeviceSupported(fsr4_ok);
|
||||
@@ -923,6 +980,15 @@ bool Instance::CreateDevice() {
|
||||
: "the GPU lacks INT8 dot products / compute "
|
||||
"derivatives - the game's AA stays");
|
||||
}
|
||||
if (Dlss::Mode() == 4) {
|
||||
// ISSN_XESS: the T2x resolve is replaced once the device has what XeSS needs.
|
||||
Xess::SetDeviceSupported(xess_device);
|
||||
Dlss::g_ready.store(xess_device, std::memory_order_relaxed);
|
||||
LOG_INFO(Render_Vulkan, "XeSS: {}", xess_device ? "device ready (in place of the game's T2x)"
|
||||
: "not available - the game's AA stays");
|
||||
std::printf("XeSS: %s\n", xess_device ? "device ready" : "not available - the game's AA stays");
|
||||
std::fflush(stdout);
|
||||
}
|
||||
if (dlss_device && Dlss::g_ready.load(std::memory_order_relaxed)) {
|
||||
Dlss5::DeviceCreated(*instance, physical_device, *device);
|
||||
} else if (Dlss::Mode() == 1) {
|
||||
@@ -1222,6 +1288,11 @@ void Instance::CollectDeviceParameters() {
|
||||
LOG_INFO(Render_Vulkan, "GPU_Vulkan_Driver: {}", driver_name);
|
||||
LOG_INFO(Render_Vulkan, "GPU_Vulkan_Version: {}", api_version);
|
||||
LOG_INFO(Render_Vulkan, "GPU_Vulkan_Extensions: {}", extensions);
|
||||
// issn-axy: the card and driver in every log (the log level hides the lines above; a tester's
|
||||
// device-lost log, 10-30, did not say which GPU it came from).
|
||||
std::printf("GPU: %s (%s, Vulkan %s%s)\n", model_name.c_str(), driver_name.c_str(),
|
||||
api_version.c_str(), IsIntegrated() ? ", integrated" : "");
|
||||
std::fflush(stdout);
|
||||
}
|
||||
|
||||
void Instance::CollectPhysicalMemoryInfo() {
|
||||
@@ -1394,6 +1465,23 @@ u64 Instance::GetDeviceMemoryUsage() const {
|
||||
return total_usage;
|
||||
}
|
||||
|
||||
std::pair<u64, u64> Instance::GetDeviceMemoryUsageAndBudget() const {
|
||||
if (!CanReportMemoryUsage()) {
|
||||
return {0, 0};
|
||||
}
|
||||
vk::PhysicalDeviceMemoryBudgetPropertiesEXT memory_budget_props{};
|
||||
vk::PhysicalDeviceMemoryProperties2 props = {
|
||||
.pNext = &memory_budget_props,
|
||||
};
|
||||
physical_device.getMemoryProperties2(&props);
|
||||
u64 usage = 0, budget = 0;
|
||||
for (const size_t heap : valid_heaps) {
|
||||
usage += memory_budget_props.heapUsage[heap];
|
||||
budget += memory_budget_props.heapBudget[heap];
|
||||
}
|
||||
return {usage, budget};
|
||||
}
|
||||
|
||||
vk::FormatFeatureFlags2 Instance::GetFormatFeatureFlags(vk::Format format) const {
|
||||
const auto it = format_properties.find(format);
|
||||
if (it == format_properties.end()) {
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <span>
|
||||
#include <unordered_map>
|
||||
|
||||
@@ -532,6 +534,9 @@ public:
|
||||
|
||||
/// Returns the amount of memory used.
|
||||
[[nodiscard]] u64 GetDeviceMemoryUsage() const;
|
||||
/// issn-axy: this process's device-local usage and the driver's budget for it now (the budget
|
||||
/// shrinks while other programs hold video memory); {0, 0} without VK_EXT_memory_budget.
|
||||
[[nodiscard]] std::pair<u64, u64> GetDeviceMemoryUsageAndBudget() const;
|
||||
|
||||
/// Returns the total memory budget available to the device.
|
||||
[[nodiscard]] u64 GetTotalMemoryBudget() const {
|
||||
|
||||
@@ -6,10 +6,13 @@
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <fmt/format.h>
|
||||
#include "common/logging/log.h"
|
||||
#include "common/thread.h"
|
||||
#include "common/types.h"
|
||||
#include "video_core/renderer_vulkan/vk_common.h"
|
||||
|
||||
#include <vk_mem_alloc.h>
|
||||
|
||||
@@ -49,6 +52,64 @@ inline std::string DescribeDeviceLocalBudgets(VmaAllocator allocator) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/// 1.1.1 (GitHub #5): VK_EXT_memory_budget is on (Instance sets it), so the driver's own numbers
|
||||
/// can be read - VMA's above are its 80%-of-heap estimate against its own blocks only.
|
||||
inline std::atomic<bool> g_driver_budget{false};
|
||||
|
||||
/// The driver's view of every heap: "driver: heap i used / budget MB". Empty without the extension.
|
||||
inline std::string DescribeDriverBudgets(VmaAllocator allocator) {
|
||||
if (!g_driver_budget.load(std::memory_order_relaxed)) {
|
||||
return {};
|
||||
}
|
||||
VmaAllocatorInfo info{};
|
||||
vmaGetAllocatorInfo(allocator, &info);
|
||||
vk::PhysicalDeviceMemoryBudgetPropertiesEXT budget{};
|
||||
vk::PhysicalDeviceMemoryProperties2 props = {
|
||||
.pNext = &budget,
|
||||
};
|
||||
vk::PhysicalDevice{info.physicalDevice}.getMemoryProperties2(&props);
|
||||
std::string out = "driver:";
|
||||
for (u32 i = 0; i < props.memoryProperties.memoryHeapCount; ++i) {
|
||||
out += fmt::format("{} heap {} {} / {} MB", i == 0 ? "" : ",", i,
|
||||
budget.heapUsage[i] >> 20, budget.heapBudget[i] >> 20);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/// 1.1.1 (GitHub #30): the driver's free bytes (budget - usage) on its largest device-local heap,
|
||||
/// or nullopt without VK_EXT_memory_budget. SHADPS4_SIM_DRIVER_FREE_MB (test) caps the answer.
|
||||
inline std::optional<u64> DriverFreeDeviceLocal(VmaAllocator allocator) {
|
||||
if (!g_driver_budget.load(std::memory_order_relaxed)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
VmaAllocatorInfo info{};
|
||||
vmaGetAllocatorInfo(allocator, &info);
|
||||
vk::PhysicalDeviceMemoryBudgetPropertiesEXT budget{};
|
||||
vk::PhysicalDeviceMemoryProperties2 props = {
|
||||
.pNext = &budget,
|
||||
};
|
||||
vk::PhysicalDevice{info.physicalDevice}.getMemoryProperties2(&props);
|
||||
u64 largest = 0;
|
||||
std::optional<u64> free;
|
||||
for (u32 i = 0; i < props.memoryProperties.memoryHeapCount; ++i) {
|
||||
const auto& heap = props.memoryProperties.memoryHeaps[i];
|
||||
if ((heap.flags & vk::MemoryHeapFlagBits::eDeviceLocal) && heap.size > largest) {
|
||||
largest = heap.size;
|
||||
free = budget.heapBudget[i] > budget.heapUsage[i]
|
||||
? budget.heapBudget[i] - budget.heapUsage[i]
|
||||
: 0;
|
||||
}
|
||||
}
|
||||
static const u64 sim = [] {
|
||||
const char* e = std::getenv("SHADPS4_SIM_DRIVER_FREE_MB");
|
||||
return e ? std::strtoull(e, nullptr, 10) << 20 : ~0ull;
|
||||
}();
|
||||
if (free && *free > sim) {
|
||||
free = sim;
|
||||
}
|
||||
return free;
|
||||
}
|
||||
|
||||
/// Runs `alloc(create_info)`; on VK_ERROR_OUT_OF_DEVICE_MEMORY with the budget bit set, retries
|
||||
/// once without it. Logs the first retries of each kind and every final failure (with the size,
|
||||
/// `detail()` and VMA's budgets, so a player's log says what did not fit).
|
||||
@@ -72,9 +133,10 @@ VkResult AllocateWithBudgetRetry(VmaAllocator allocator, VmaAllocationCreateInfo
|
||||
}
|
||||
}
|
||||
if (result != VK_SUCCESS) {
|
||||
LOG_CRITICAL(Render_Vulkan, "Allocation failed: {} of {} bytes ({} MB) {}: error {}; {}",
|
||||
LOG_CRITICAL(Render_Vulkan, "Allocation failed: {} of {} bytes ({} MB) {}: error {}; {}; {}; {}",
|
||||
what, bytes, bytes >> 20, detail(), static_cast<int>(result),
|
||||
DescribeDeviceLocalBudgets(allocator));
|
||||
DescribeDeviceLocalBudgets(allocator), DescribeDriverBudgets(allocator),
|
||||
Common::DescribeSystemMemory());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
#include "video_core/renderer_vulkan/vk_pipeline_compile_pool.h"
|
||||
#include "video_core/renderer_vulkan/vk_pipeline_serialization.h"
|
||||
#include "video_core/renderer_vulkan/vk_shader_util.h"
|
||||
#include <immintrin.h> // _mm_pause
|
||||
#include "video_core/renderer_vulkan/vk_scheduler.h"
|
||||
#include "video_core/renderer_vulkan/vk_shader_util.h"
|
||||
|
||||
@@ -540,16 +541,35 @@ private:
|
||||
queue.pop_front();
|
||||
busy = true;
|
||||
}
|
||||
// 1.1.1 (GitHub #25, #31): named before the driver sees it, so a driver crash or
|
||||
// refusal in a player's log says which variant (vs / ps program hashes).
|
||||
const u64 key_hash = std::hash<GraphicsPipelineKey>{}(job->key);
|
||||
const auto pgm = [&](size_t i) { return job->infos[i] ? job->infos[i]->pgm_hash : 0; };
|
||||
LOG_INFO(Render_Vulkan,
|
||||
"NativeColour: compiling variant of {:#x} slot {} (vs {:#x} ps {:#x}, "
|
||||
"bindless {}, {} bindings)",
|
||||
key_hash, job->slot, pgm(size_t(Shader::LogicalStage::Vertex)),
|
||||
pgm(size_t(Shader::LogicalStage::Fragment)), job->bindless,
|
||||
job->bindings.size());
|
||||
const auto t0 = CompileClock::now();
|
||||
auto variant = std::make_unique<GraphicsPipeline>(
|
||||
instance, scheduler, desc_heap, profile, job->key, pipeline_cache, job->infos,
|
||||
job->runtime_infos, job->fetch_shader, job->modules, job->sdata, false,
|
||||
&job->bindings, job->records_binding, job->bindless);
|
||||
&job->bindings, job->records_binding, job->bindless, /*refusable=*/true);
|
||||
const double ms = MsSince(t0);
|
||||
variant->sparse_dead_mask = job->dead_mask; // q3 (before the slot publishes it)
|
||||
variant->native_bindless_vs = job->bindless_vs;
|
||||
auto& slot = job->base->native_colour[job->slot];
|
||||
if (!variant->UsesPushDescriptors()) {
|
||||
if (!variant->IsCreated()) {
|
||||
// The driver refused it (logged by the constructor): not eligible, the normal
|
||||
// pipeline draws this description - as for no push descriptors below.
|
||||
LOG_WARNING(Render_Vulkan,
|
||||
"NativeColour: variant of {:#x} refused by the driver - not eligible, "
|
||||
"the normal path draws it",
|
||||
key_hash);
|
||||
job->base->native_colour_why.store("driver refused", std::memory_order_relaxed);
|
||||
slot.state.store(2, std::memory_order_release);
|
||||
} else if (!variant->UsesPushDescriptors()) {
|
||||
// Too many descriptors for push descriptors (NVIDIA: 32): the slot stays claimed
|
||||
// with no variant, so the back keeps the generic path for this description.
|
||||
LOG_INFO(Render_Vulkan,
|
||||
@@ -1379,9 +1399,20 @@ void PipelineCache::PrepareNativeVariantLocked(const GraphicsPipeline* base,
|
||||
params.hash);
|
||||
}
|
||||
GraphicsPipeline::SerializationSupport sdata{};
|
||||
LOG_INFO(Render_Vulkan, "NativePass: compiling variant of {:#x} (vs {:#x})",
|
||||
std::hash<GraphicsPipelineKey>{}(graphics_key),
|
||||
infos[size_t(Shader::LogicalStage::Vertex)] ? infos[size_t(Shader::LogicalStage::Vertex)]->pgm_hash : 0);
|
||||
auto variant = std::make_unique<GraphicsPipeline>(
|
||||
instance, scheduler, desc_heap, profile, graphics_key, *pipeline_cache, infos,
|
||||
runtime_infos, fetch_shader, native_modules, sdata, false, nullptr, records_binding);
|
||||
runtime_infos, fetch_shader, native_modules, sdata, false, nullptr, records_binding,
|
||||
false, /*refusable=*/true);
|
||||
if (!variant->IsCreated()) {
|
||||
// 1.1.1 (GitHub #25, #31): a variant the driver refuses is left out; the base draws.
|
||||
LOG_WARNING(Render_Vulkan, "NativePass: variant of {:#x} refused by the driver - not used",
|
||||
std::hash<GraphicsPipelineKey>{}(graphics_key));
|
||||
native_variants.push_back(std::move(variant));
|
||||
return;
|
||||
}
|
||||
if (!variant->UsesPushDescriptors()) {
|
||||
LOG_WARNING(Render_Vulkan, "NativePass: variant of {:#x} has no push descriptors",
|
||||
std::hash<GraphicsPipelineKey>{}(graphics_key));
|
||||
@@ -2569,18 +2600,22 @@ PipelineCache::Result PipelineCache::GetProgram(const AmdGpu::Regs& regs, Stage
|
||||
it_pgm.value() = std::make_unique<Program>(stage, l_stage, params);
|
||||
auto& program = it_pgm.value();
|
||||
auto start = binding;
|
||||
const auto module = CompileModule(program->info, runtime_info, params.code, 0, binding);
|
||||
// A program none of whose cached metas loaded: index 0 may still name one on disk.
|
||||
const size_t first_idx = FreePermIndex(params.hash, 0);
|
||||
const auto module =
|
||||
CompileModule(program->info, runtime_info, params.code, first_idx, binding);
|
||||
auto spec = Shader::StageSpecialization(program->info, runtime_info, profile, start);
|
||||
const auto perm_hash = HashCombine(params.hash, 0);
|
||||
const auto perm_hash = HashCombine(params.hash, first_idx);
|
||||
|
||||
RegisterShaderMeta(program->info, spec.fetch_shader_data, spec, perm_hash, 0);
|
||||
program->AddPermut(module, std::move(spec));
|
||||
NoteStageSelection(l_stage, program.get(), 0, true);
|
||||
RegisterShaderMeta(program->info, spec.fetch_shader_data, spec, perm_hash, first_idx);
|
||||
disk_metas.insert(perm_hash);
|
||||
program->InsertPermut(module, std::move(spec), first_idx);
|
||||
NoteStageSelection(l_stage, program.get(), first_idx, true);
|
||||
Dlss::NoteWeightsInfo(program->info, params.Base());
|
||||
Dlss::NoteShaderInfo(program->info, params.Base());
|
||||
lookup_translate_ms += MsSince(t_translate);
|
||||
return std::make_tuple(&program->info, module, SharedFetchMetadata(program->modules[0]),
|
||||
perm_hash);
|
||||
return std::make_tuple(&program->info, module,
|
||||
SharedFetchMetadata(program->modules[first_idx]), perm_hash);
|
||||
}
|
||||
|
||||
auto& program = it_pgm.value();
|
||||
@@ -2666,6 +2701,8 @@ PipelineCache::Result PipelineCache::GetProgram(const AmdGpu::Regs& regs, Stage
|
||||
}
|
||||
}
|
||||
if (compiled) {
|
||||
perm_idx = FreePermIndex(params.hash, perm_idx);
|
||||
perm_hash = HashCombine(params.hash, perm_idx);
|
||||
const auto t_translate = CompileClock::now();
|
||||
auto new_info = Shader::Info(stage, l_stage, params);
|
||||
// The pipeline layout and descriptor binding of every permutation come from the
|
||||
@@ -2697,7 +2734,8 @@ PipelineCache::Result PipelineCache::GetProgram(const AmdGpu::Regs& regs, Stage
|
||||
// the program from it as a live one nor learns from the guessed Info.
|
||||
RegisterShaderMeta(info, spec.fetch_shader_data, spec, perm_hash, perm_idx,
|
||||
program->precompiled);
|
||||
program->AddPermut(module, std::move(spec));
|
||||
disk_metas.insert(perm_hash);
|
||||
program->InsertPermut(module, std::move(spec), perm_idx);
|
||||
lookup_translate_ms += MsSince(t_translate);
|
||||
} else {
|
||||
info.AddBindings(binding);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <unordered_set>
|
||||
#include <variant>
|
||||
#include <tsl/robin_map.h>
|
||||
#include "common/hash.h"
|
||||
#include "shader_recompiler/prepared_specialization.h"
|
||||
#include "shader_recompiler/profile.h"
|
||||
#include "shader_recompiler/recompiler.h"
|
||||
@@ -515,6 +516,19 @@ private:
|
||||
// their marked metas are not loaded. All other marked compute metas load after the unmarked
|
||||
// ones (deferred), so a live meta, never a guessed one, creates a program's shared Info.
|
||||
std::unordered_set<u64> precompile_retired;
|
||||
// issn_pc (2026-10-24): the shader metas in the cache folder at boot (file name = permutation
|
||||
// hash) plus those saved since. A new permutation never takes an index whose meta file exists:
|
||||
// a seed leaves metas the boot did not load (not stamped, differing, a key that failed at
|
||||
// another stage), and modules.size() reused their indices, so the new meta replaced one that
|
||||
// other pipeline keys still name - on the next boot those keys loaded the wrong shader (the
|
||||
// driver refused pipelines, then the game closed with "stack smashing detected").
|
||||
std::unordered_set<u64> disk_metas;
|
||||
size_t FreePermIndex(u64 pgm_hash, size_t from) const {
|
||||
while (disk_metas.contains(HashCombine(pgm_hash, from))) {
|
||||
++from;
|
||||
}
|
||||
return from;
|
||||
}
|
||||
struct DeferredComputeLoad {
|
||||
ComputePipelineKey key{};
|
||||
ComputePipeline::SerializationSupport sdata{};
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <deque>
|
||||
#include <filesystem>
|
||||
@@ -45,6 +46,9 @@ static constexpr u32 PrecompiledMetaBit = 0x80000000u;
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
// issn: permutations skipped at the preload because their index changed (one line per warm-up).
|
||||
static std::atomic<u64> g_permutation_conflicts{0};
|
||||
|
||||
namespace {
|
||||
// SHADPS4_CACHE_REBUILD WarmUp time split (warmup_times).
|
||||
using WarmClock = std::chrono::steady_clock;
|
||||
@@ -232,6 +236,7 @@ bool PipelineCache::LoadComputePipeline(Serialization::Archive& ar) {
|
||||
modules.fill(nullptr);
|
||||
fetch_shader.reset();
|
||||
compute_key.Deserialize(ar);
|
||||
disk_metas.insert(compute_key.value); // issn_pc: its permutation index stays taken
|
||||
|
||||
ComputePipeline::SerializationSupport sdata{};
|
||||
sdata.Deserialize(ar);
|
||||
@@ -450,8 +455,19 @@ bool PipelineCache::LoadGraphicsPipeline(Serialization::Archive& ar) {
|
||||
modules.fill(nullptr);
|
||||
fetch_shader.reset();
|
||||
graphics_key.Deserialize(ar);
|
||||
// A seed key preloads only where the stamp allows graphics (seed_graphics_keys).
|
||||
if (!seed_stamp.graphics &&
|
||||
// issn_pc: the permutations this key names stay taken even when their meta is missing (a seed
|
||||
// drops differing metas but keeps the keys): a new permutation there would make this key load
|
||||
// the wrong shader on a later boot.
|
||||
for (const auto stage_hash : graphics_key.stage_hashes) {
|
||||
if (stage_hash) {
|
||||
disk_metas.insert(stage_hash);
|
||||
}
|
||||
}
|
||||
// A seed key preloads only where the stamp allows graphics (seed_graphics_keys). issn_pc: not
|
||||
// in a VERIFY run, which reads no stamp (that needs bit 1) and checks the folder's own SPIR-V:
|
||||
// the 0.3.2 release verify skipped every graphics key of a folder grown from a seed, so its
|
||||
// new stamp held only compute and the folder's few own graphics stages.
|
||||
if (!seed_stamp.graphics && (CacheRebuildMode() & 2) == 0 &&
|
||||
seed_graphics_keys.contains(std::hash<GraphicsPipelineKey>{}(graphics_key))) {
|
||||
++seed_graphics_keys_skipped;
|
||||
return false;
|
||||
@@ -777,10 +793,13 @@ bool PipelineCache::LoadPipelineStage(Serialization::Archive& ar, size_t stage)
|
||||
// left to compile at runtime.
|
||||
const auto idx = std::distance(it_pgm.value()->modules.begin(), it);
|
||||
if (perm_idx != idx) {
|
||||
LOG_WARNING(Render_Vulkan,
|
||||
"Cached permutation {} of {}_{:x} conflicts with index {}, skipping "
|
||||
"preload",
|
||||
perm_idx, program->info.stage, program->info.pgm_hash, idx);
|
||||
// issn: harmless and frequent (~440 lines in every tester log, burying what
|
||||
// matters): counted, one line at the end of the warm-up.
|
||||
g_permutation_conflicts.fetch_add(1, std::memory_order_relaxed);
|
||||
LOG_DEBUG(Render_Vulkan,
|
||||
"Cached permutation {} of {}_{:x} conflicts with index {}, skipping "
|
||||
"preload",
|
||||
perm_idx, program->info.stage, program->info.pgm_hash, idx);
|
||||
return false;
|
||||
}
|
||||
module = it->module;
|
||||
@@ -922,7 +941,19 @@ void PipelineCache::ReadSeedStamp() {
|
||||
const bool same_vertex_input =
|
||||
(dynamic_vertex_input != 0) == instance.IsVertexInputDynamicState();
|
||||
seed_stamp.compute = true;
|
||||
seed_stamp.graphics = same_profile && same_vertex_input;
|
||||
// issn 0.3.6 (test, ISSN_SEED_ANY_PROFILE=1): graphics stages on another GPU's profile too
|
||||
// (NVIDIA, older AMD), as compute already is: each rebuilt stage still has to match the
|
||||
// seed's resource layout (layout_mismatch refuses it) and the game file's code. Off by
|
||||
// default until an NVIDIA run shows the counts and a clean picture.
|
||||
const bool any_profile = [] {
|
||||
const char* v = std::getenv("ISSN_SEED_ANY_PROFILE");
|
||||
return v && v[0] == '1';
|
||||
}();
|
||||
seed_stamp.graphics = same_vertex_input && (same_profile || any_profile);
|
||||
if (!same_profile && any_profile && same_vertex_input) {
|
||||
LOG_WARNING(Render, "CacheRebuild: ISSN_SEED_ANY_PROFILE=1: graphics stages are rebuilt on "
|
||||
"this GPU's profile too (each checked against the seed's layout)");
|
||||
}
|
||||
seed_stamp.shaders = std::move(shaders);
|
||||
seed_stamp.perms = std::move(perms);
|
||||
if (seed_stamp.graphics) {
|
||||
@@ -1202,6 +1233,18 @@ void PipelineCache::WarmUp() {
|
||||
}
|
||||
});
|
||||
|
||||
// issn_pc: every shader meta on disk (PipelineCache::disk_metas), so a new permutation never
|
||||
// takes the index of one this boot does not load.
|
||||
disk_metas.clear();
|
||||
if (const auto folder = Storage::DataBase::Instance().SidecarPath(""); !folder.empty()) {
|
||||
std::error_code ec;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(folder, ec)) {
|
||||
const auto name = entry.path().filename().string();
|
||||
if (name.size() == 23 && name.starts_with("0x") && name.ends_with(".meta")) {
|
||||
disk_metas.insert(std::strtoull(name.c_str() + 2, nullptr, 16));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (profile_data.empty()) {
|
||||
std::vector<u8> fresh(sizeof(profile));
|
||||
std::memcpy(fresh.data(), &profile, sizeof(profile));
|
||||
@@ -1510,6 +1553,11 @@ void PipelineCache::WarmUp() {
|
||||
LOG_INFO(Render, "CacheRebuild: vs-formats.bin written, {} entries", count);
|
||||
}
|
||||
}
|
||||
if (const u64 conflicts = g_permutation_conflicts.exchange(0); conflicts) {
|
||||
LOG_INFO(Render, "{} cached permutations from another cache generation are compiled when "
|
||||
"first used (not preloaded)",
|
||||
conflicts);
|
||||
}
|
||||
if (num_total_pipelines > num_pipelines) {
|
||||
LOG_WARNING(Render, "{} stale pipelines were found. Consider re-generating the cache",
|
||||
num_total_pipelines - num_pipelines);
|
||||
@@ -1518,6 +1566,18 @@ void PipelineCache::WarmUp() {
|
||||
PrecompileFromExtract();
|
||||
|
||||
Storage::DataBase::Instance().FinishPreload();
|
||||
|
||||
// issn_pc: SHADPS4_EXIT_AFTER_WARMUP=1 (packaging/windows/make_seed.sh): the process ends here,
|
||||
// once the boot preload (and a VERIFY run's stamp) is done, before the game starts.
|
||||
if (const char* exit_env = std::getenv("SHADPS4_EXIT_AFTER_WARMUP");
|
||||
exit_env && exit_env[0] == '1') {
|
||||
Storage::DataBase::Instance().Close();
|
||||
std::printf("Shader cache: boot preload done (%zu shaders checked); exiting as "
|
||||
"SHADPS4_EXIT_AFTER_WARMUP asks\n",
|
||||
stamp_perms.size());
|
||||
std::fflush(nullptr);
|
||||
std::_Exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
void PipelineCache::Sync() {
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
#undef Bool
|
||||
#endif
|
||||
#include "video_core/renderer_vulkan/vk_dlss.h"
|
||||
#include "video_core/renderer_vulkan/issn_xess.h"
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <mach-o/dyld.h>
|
||||
@@ -221,8 +222,15 @@ std::vector<const char*> GetInstanceExtensions(Frontend::WindowSystemType window
|
||||
extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
|
||||
}
|
||||
|
||||
#ifdef __APPLE__
|
||||
// port: the loader only lists MoltenVK (a portability driver) when the instance asks for it.
|
||||
extensions.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME);
|
||||
#endif
|
||||
|
||||
// SHADPS4_DLSS=1: Streamline starts here, before vkCreateInstance, and adds what it needs.
|
||||
Dlss::AddInstanceExtensions(extensions);
|
||||
// issn-axy: ISSN_XESS=1 - libxess loads here and adds what it needs (issn_xess.h).
|
||||
Xess::AddInstanceExtensions(extensions);
|
||||
|
||||
// Sanitize extension list
|
||||
std::erase_if(extensions, [&](const char* extension) -> bool {
|
||||
@@ -421,6 +429,9 @@ vk::UniqueInstance CreateInstance(Frontend::WindowSystemType window_type, bool e
|
||||
|
||||
vk::StructureChain<vk::InstanceCreateInfo, vk::LayerSettingsCreateInfoEXT> instance_ci_chain = {
|
||||
vk::InstanceCreateInfo{
|
||||
#ifdef __APPLE__
|
||||
.flags = vk::InstanceCreateFlagBits::eEnumeratePortabilityKHR,
|
||||
#endif
|
||||
.pApplicationInfo = &application_info,
|
||||
.enabledLayerCount = static_cast<u32>(layers.size()),
|
||||
.ppEnabledLayerNames = layers.data(),
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
#include "video_core/renderer_vulkan/vk_memory_budget.h"
|
||||
#include "video_core/renderer_vulkan/vk_platform.h"
|
||||
#include "video_core/renderer_vulkan/vk_presenter.h"
|
||||
#include "video_core/renderer_vulkan/issn_framegen.h"
|
||||
#include <cstdio>
|
||||
#include "video_core/renderer_vulkan/vk_rasterizer.h"
|
||||
#include "video_core/texture_cache/image.h"
|
||||
|
||||
@@ -633,8 +635,16 @@ Presenter::Presenter(Frontend::WindowSDL& window_, AmdGpu::Liverpool* liverpool_
|
||||
const u32 num_images = [&] {
|
||||
const char* e = std::getenv("SHADPS4_PRESENT_FRAMES");
|
||||
const u32 want = e ? static_cast<u32>(std::atoi(e)) : 0u;
|
||||
return std::clamp(want, swapchain.GetImageCount(), 8u);
|
||||
// issn-axy: frame generation takes a second frame per flip for the generated one.
|
||||
const u32 least = FrameGen::Wanted() ? std::min(2 * swapchain.GetImageCount(), 8u)
|
||||
: swapchain.GetImageCount();
|
||||
return std::clamp(want, least, 8u);
|
||||
}();
|
||||
if (FrameGen::Wanted()) {
|
||||
std::printf("Frame generation: available (FSR 3.1, %s now), %u presentation frames\n",
|
||||
FrameGen::Enabled() ? "on" : "off", num_images);
|
||||
std::fflush(stdout);
|
||||
}
|
||||
LOG_INFO(Render_Vulkan, "Presenter: {} presentation frames (swapchain images {})", num_images,
|
||||
swapchain.GetImageCount());
|
||||
const vk::Device device = instance.GetDevice();
|
||||
@@ -667,6 +677,7 @@ Presenter::~Presenter() {
|
||||
draw_scheduler.Finish();
|
||||
present_scheduler.Finish();
|
||||
flip_scheduler.Finish();
|
||||
FrameGen::Shutdown();
|
||||
Check(draw_scheduler.AcquireCommandBuffer().Handle().reset());
|
||||
Check(present_scheduler.AcquireCommandBuffer().Handle().reset());
|
||||
Check(flip_scheduler.AcquireCommandBuffer().Handle().reset());
|
||||
@@ -907,7 +918,7 @@ Frame* Presenter::PrepareFrame(const Libraries::VideoOut::BufferAttributeGroup&
|
||||
dlss_job = upscaler->TakeJob(*hud.FindView(hud_view_info).image_view);
|
||||
}
|
||||
}
|
||||
upscaler->SetOutputSize(frame->width, frame->height);
|
||||
upscaler->SetOutputSize(frame->width, frame->height, frame->is_hdr);
|
||||
}
|
||||
|
||||
// DLSS 5 on video cutscenes (vk_dlss5.h): a frame the game did not render through the T2x
|
||||
@@ -928,6 +939,82 @@ Frame* Presenter::PrepareFrame(const Libraries::VideoOut::BufferAttributeGroup&
|
||||
const char* e = std::getenv("SHADPS4_PRESENT_ASYNC");
|
||||
return e && e[0] == '1';
|
||||
}();
|
||||
// issn-axy: frame generation (issn_framegen.h): a second frame for the one made between the
|
||||
// previous frame and this; HDR frames and DLSS 5 video frames are shown alone.
|
||||
Frame* generated = nullptr;
|
||||
if (FrameGen::Active() && !frame->is_hdr && !dlss5_video) {
|
||||
generated = GetRenderFrame();
|
||||
if (generated->width != frame->width || generated->height != frame->height ||
|
||||
generated->is_hdr) {
|
||||
ReleaseFrame(generated);
|
||||
generated = nullptr;
|
||||
}
|
||||
}
|
||||
if (FrameGen::Wanted()) {
|
||||
static int told = -1; // why a frame goes without generation, once per reason
|
||||
const int why = generated ? 0 : frame->is_hdr ? 1 : dlss5_video ? 2 : !FrameGen::Active() ? 3 : 4;
|
||||
if (why != told) {
|
||||
told = why;
|
||||
static constexpr const char* reasons[] = {
|
||||
"frames are generated", "off while the output is HDR", "off on DLSS 5 video frames",
|
||||
"off (the menu's switch, or a failure above)", "no second frame of the same size"};
|
||||
std::printf("Frame generation: %s\n", reasons[why]);
|
||||
std::fflush(stdout);
|
||||
}
|
||||
}
|
||||
frame->generated = generated;
|
||||
// With the upscaler's HUD layer (dlss_job; vk_dlss_upscale.cpp keeps it on while frame
|
||||
// generation is) the scene is interpolated alone and this frame's HUD laid over the result,
|
||||
// through the same composite and post pass as the frame itself: the HUD stays sharp. Without
|
||||
// it the whole finished frame is interpolated (or copied, when nothing was captured).
|
||||
// Whether this frame's depth and motion were captured, taken now (stream order; the passes
|
||||
// below may be recorded later, after the next frame's capture).
|
||||
const FrameGen::Ticket fg_ticket = FrameGen::Wanted() ? FrameGen::TakeCapture() : FrameGen::Ticket{};
|
||||
const auto record_generated = [this, generated, fg_ticket, fc = *frame](
|
||||
vk::CommandBuffer cb,
|
||||
const std::optional<Dlss::CompositeJob>& job,
|
||||
const HostPasses::PostProcessingPass::Settings& pp) {
|
||||
if (!generated) {
|
||||
return;
|
||||
}
|
||||
FrameGen::Ticket ticket = fg_ticket;
|
||||
if (job) {
|
||||
if (const auto scene = FrameGen::GenerateScene(instance, draw_scheduler, cb, ticket,
|
||||
job->scene_image, job->size.width,
|
||||
job->size.height)) {
|
||||
Dlss::CompositeJob composite = *job;
|
||||
composite.scene_view = scene->scene;
|
||||
composite.out_image = scene->composite_image;
|
||||
composite.out_view = scene->composite_view;
|
||||
Dlss::RecordComposite(cb, composite);
|
||||
const vk::ImageMemoryBarrier2 to_draw{
|
||||
.srcStageMask = vk::PipelineStageFlagBits2::eAllCommands,
|
||||
.srcAccessMask = vk::AccessFlagBits2::eMemoryRead,
|
||||
.dstStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput,
|
||||
.dstAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite,
|
||||
.oldLayout = vk::ImageLayout::eUndefined,
|
||||
.newLayout = vk::ImageLayout::eColorAttachmentOptimal,
|
||||
.image = generated->image,
|
||||
.subresourceRange{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1},
|
||||
};
|
||||
cb.pipelineBarrier2(vk::DependencyInfo{
|
||||
.imageMemoryBarrierCount = 1,
|
||||
.pImageMemoryBarriers = &to_draw,
|
||||
});
|
||||
pp_pass.Render(cb, composite.out_view, composite.size, *generated, pp);
|
||||
return;
|
||||
}
|
||||
ticket.captured = false; // spent (or none): the finished frame is copied
|
||||
}
|
||||
FrameGen::Generate(instance, draw_scheduler, cb, ticket, fc.image, generated->image,
|
||||
fc.width, fc.height);
|
||||
};
|
||||
const auto finish_generated = [&] {
|
||||
if (generated) {
|
||||
generated->ready_semaphore = frame->ready_semaphore;
|
||||
generated->ready_tick = frame->ready_tick;
|
||||
}
|
||||
};
|
||||
if (present_async && capture_game_only_count == 0 &&
|
||||
GuestLayout(vk::ImageLayout::eShaderReadOnlyOptimal) ==
|
||||
vk::ImageLayout::eShaderReadOnlyOptimal) { // q6b (3)
|
||||
@@ -937,7 +1024,7 @@ Frame* Presenter::PrepareFrame(const Libraries::VideoOut::BufferAttributeGroup&
|
||||
// q6b (2): ordered (FsrPass keeps state across flips); the Frame by value.
|
||||
draw_scheduler.RecordOrdered([this, pre_barrier, image_view, image_size, fc = *frame,
|
||||
fsr = fsr_settings, pp = pp_settings, dlss_job, dlss5_video,
|
||||
frame_linear](vk::CommandBuffer cb) mutable {
|
||||
frame_linear, record_generated](vk::CommandBuffer cb) mutable {
|
||||
cb.pipelineBarrier2(vk::DependencyInfo{
|
||||
.imageMemoryBarrierCount = 1,
|
||||
.pImageMemoryBarriers = &pre_barrier,
|
||||
@@ -945,6 +1032,7 @@ Frame* Presenter::PrepareFrame(const Libraries::VideoOut::BufferAttributeGroup&
|
||||
if (dlss_job) {
|
||||
Dlss::RecordComposite(cb, *dlss_job);
|
||||
pp_pass.Render(cb, dlss_job->out_view, dlss_job->size, fc, pp);
|
||||
record_generated(cb, dlss_job, pp);
|
||||
return;
|
||||
}
|
||||
vk::ImageView source = image_view;
|
||||
@@ -957,11 +1045,13 @@ Frame* Presenter::PrepareFrame(const Libraries::VideoOut::BufferAttributeGroup&
|
||||
const auto view =
|
||||
fsr_pass.Render(cb, source, image_size, {fc.width, fc.height}, fsr, fc.is_hdr);
|
||||
pp_pass.Render(cb, view, image_size, fc, pp);
|
||||
record_generated(cb, std::nullopt, pp);
|
||||
});
|
||||
DebugState.game_resolution = {image_size.width, image_size.height};
|
||||
DebugState.output_resolution = {frame->width, frame->height};
|
||||
frame->ready_semaphore = draw_scheduler.GetMasterSemaphore()->Handle();
|
||||
frame->ready_tick = draw_scheduler.CurrentTick();
|
||||
finish_generated();
|
||||
SubmitInfo info{};
|
||||
draw_scheduler.Flush(info);
|
||||
return frame;
|
||||
@@ -973,7 +1063,11 @@ Frame* Presenter::PrepareFrame(const Libraries::VideoOut::BufferAttributeGroup&
|
||||
.pImageMemoryBarriers = &pre_barrier,
|
||||
});
|
||||
std::vector<ScreenshotReadback> pending_screenshots;
|
||||
if (capture_game_only_count > 0) {
|
||||
// issn: with the DLSS / FSR 4 / XeSS upscaler the display buffer holds only the HUD layer (the
|
||||
// scene is in the upscaler's image), so F12 saved a black picture with the HUD (tester 10-30,
|
||||
// 1080p and 1440p with DLSS). Then the finished frame is captured after the passes instead.
|
||||
const bool capture_composed = capture_game_only_count > 0 && dlss_job.has_value();
|
||||
if (capture_game_only_count > 0 && !capture_composed) {
|
||||
pending_screenshots.reserve(1);
|
||||
const bool hdr_encoded =
|
||||
attribute.attrib.pixel_format == Libraries::VideoOut::PixelFormat::A2R10G10B10Bt2020Pq;
|
||||
@@ -997,6 +1091,29 @@ Frame* Presenter::PrepareFrame(const Libraries::VideoOut::BufferAttributeGroup&
|
||||
if (dlss_job) {
|
||||
Dlss::RecordComposite(cmdbuf, *dlss_job);
|
||||
pp_pass.Render(cmdbuf, dlss_job->out_view, dlss_job->size, *frame, pp_settings);
|
||||
if (capture_composed) {
|
||||
// The frame image (the post pass's target, General layout) at the window size.
|
||||
pending_screenshots.emplace_back(
|
||||
instance, draw_scheduler, ScreenshotKind::GameOnly,
|
||||
BuildScreenshotPaths(ScreenshotKind::GameOnly, capture_game_only_count),
|
||||
frame->width, frame->height, swapchain.GetSurfaceFormat().format, frame->is_hdr);
|
||||
const vk::ImageMemoryBarrier2 to_copy{
|
||||
.srcStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput,
|
||||
.srcAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite,
|
||||
.dstStageMask = vk::PipelineStageFlagBits2::eTransfer,
|
||||
.dstAccessMask = vk::AccessFlagBits2::eTransferRead,
|
||||
.oldLayout = vk::ImageLayout::eGeneral,
|
||||
.newLayout = vk::ImageLayout::eGeneral,
|
||||
.image = frame->image,
|
||||
.subresourceRange{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1},
|
||||
};
|
||||
cmdbuf.pipelineBarrier2(vk::DependencyInfo{
|
||||
.imageMemoryBarrierCount = 1,
|
||||
.pImageMemoryBarriers = &to_copy,
|
||||
});
|
||||
CopyImageToReadback(cmdbuf, frame->image, vk::ImageLayout::eGeneral,
|
||||
pending_screenshots.back());
|
||||
}
|
||||
} else {
|
||||
if (dlss5_video) {
|
||||
if (const auto v = Dlss5::ApplyVideo(instance, cmdbuf, image_view, image_size.width,
|
||||
@@ -1008,6 +1125,7 @@ Frame* Presenter::PrepareFrame(const Libraries::VideoOut::BufferAttributeGroup&
|
||||
{frame->width, frame->height}, fsr_settings, frame->is_hdr);
|
||||
pp_pass.Render(cmdbuf, image_view, image_size, *frame, pp_settings);
|
||||
}
|
||||
record_generated(cmdbuf, dlss_job, pp_settings);
|
||||
|
||||
DebugState.game_resolution = {image_size.width, image_size.height};
|
||||
DebugState.output_resolution = {frame->width, frame->height};
|
||||
@@ -1023,6 +1141,7 @@ Frame* Presenter::PrepareFrame(const Libraries::VideoOut::BufferAttributeGroup&
|
||||
// Flush frame creation commands.
|
||||
frame->ready_semaphore = draw_scheduler.GetMasterSemaphore()->Handle();
|
||||
frame->ready_tick = draw_scheduler.CurrentTick();
|
||||
finish_generated();
|
||||
SubmitInfo info{};
|
||||
draw_scheduler.Flush(info);
|
||||
return frame;
|
||||
@@ -1119,22 +1238,40 @@ void Presenter::Present(Frame* frame, bool is_reusing_frame, bool hold_frame) {
|
||||
}
|
||||
};
|
||||
|
||||
// Recreate the swapchain if the window was resized or F3's Present Mode changed (live).
|
||||
// Recreate the swapchain if the window was resized or F3's Present Mode changed (live), or
|
||||
// the driver called the last image suboptimal (issn).
|
||||
if (window.GetWidth() != swapchain.GetWidth() || window.GetHeight() != swapchain.GetHeight() ||
|
||||
swapchain.PresentModeChanged()) {
|
||||
swapchain.PresentModeChanged() || swapchain.RecreatePending()) {
|
||||
swapchain.Recreate(window.GetWidth(), window.GetHeight());
|
||||
}
|
||||
|
||||
const auto acquire_start = Common::FaultMonitor::Now();
|
||||
static u32 skipped = 0;
|
||||
if (!swapchain.AcquireNextImage()) {
|
||||
swapchain.Recreate(window.GetWidth(), window.GetHeight());
|
||||
if (!swapchain.AcquireNextImage()) {
|
||||
// User resizes the window too fast and GPU can't keep up. Skip this frame.
|
||||
LOG_WARNING(Render_Vulkan, "Skipping frame!");
|
||||
// issn: once, then every 600 frames with why (a tester's log was 300 lines of it); an
|
||||
// HDR swapchain the display keeps refusing falls back to SDR after 2 s of them.
|
||||
if (skipped++ % 600 == 0) {
|
||||
LOG_WARNING(Render_Vulkan, "Skipping frame! ({} in a row; window {}x{}, swapchain "
|
||||
"{}, last acquire {}, HDR {})", skipped, window.GetWidth(),
|
||||
window.GetHeight(), swapchain.HasSwapchain() ? "ready" : "none",
|
||||
vk::to_string(swapchain.LastAcquire()), swapchain.GetHDR());
|
||||
}
|
||||
if (skipped == 120 && swapchain.GetHDR()) {
|
||||
LOG_WARNING(Render_Vulkan, "HDR output keeps failing: back to SDR");
|
||||
hdr_refused = true;
|
||||
SetHDR(false);
|
||||
}
|
||||
free_frame();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (skipped) {
|
||||
LOG_INFO(Render_Vulkan, "Frames shown again after {} skipped", skipped);
|
||||
skipped = 0;
|
||||
}
|
||||
|
||||
const u64 acquire_ns = Common::FaultMonitor::SinceNs(acquire_start);
|
||||
|
||||
|
||||
@@ -35,6 +35,9 @@ struct Frame {
|
||||
u64 ready_tick;
|
||||
bool is_hdr{false};
|
||||
u8 id{};
|
||||
/// issn-axy: frame generation (issn_framegen.h) - the frame made between the previous one
|
||||
/// and this, shown first (driver Flip); null without generation.
|
||||
Frame* generated{};
|
||||
|
||||
};
|
||||
|
||||
@@ -63,6 +66,7 @@ public:
|
||||
return window;
|
||||
}
|
||||
|
||||
bool hdr_refused = false; // issn: the HDR swapchain never showed a frame: SDR from then on
|
||||
Rasterizer& GetRasterizer() const {
|
||||
return *rasterizer.get();
|
||||
}
|
||||
@@ -72,7 +76,7 @@ public:
|
||||
}
|
||||
|
||||
void SetHDR(bool enable) {
|
||||
if (!IsHDRSupported()) {
|
||||
if (!IsHDRSupported() || (enable && hdr_refused)) { // issn: see Present
|
||||
return;
|
||||
}
|
||||
swapchain.SetHDR(enable);
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include <set>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
@@ -41,6 +43,8 @@
|
||||
#include "video_core/amdgpu/mtb_census.h"
|
||||
#include "video_core/renderer_vulkan/vk_rasterizer.h"
|
||||
#include "video_core/renderer_vulkan/issn_fsr4.h"
|
||||
#include "video_core/renderer_vulkan/issn_xess.h"
|
||||
#include "video_core/renderer_vulkan/issn_framegen.h"
|
||||
#include "video_core/renderer_vulkan/vk_dlss.h"
|
||||
#include "video_core/renderer_vulkan/vk_dlss_upscale.h"
|
||||
#include "video_core/renderer_vulkan/vk_dlss5.h"
|
||||
@@ -1660,6 +1664,9 @@ void Rasterizer::DlssResolve() {
|
||||
// a plain copy of the tonemap output (the game without its temporal AA for that frame).
|
||||
++dlss_calls;
|
||||
++dlss_phases[dlss_phase_in & 3];
|
||||
if (PostFx::Active()) [[unlikely]] {
|
||||
PostFxPublish(); // the end of the scene's post chain (issn_postfx.h)
|
||||
}
|
||||
{
|
||||
// issn-axy: more than 100 ms since the last resolve (Home switched it off, a menu, a
|
||||
// load): FSR's history is another picture, so it starts over.
|
||||
@@ -1687,7 +1694,7 @@ void Rasterizer::DlssResolve() {
|
||||
address, out.info.size.width, out.info.size.height);
|
||||
return id ? &texture_cache.GetImage(id) : nullptr;
|
||||
};
|
||||
const bool fsr4_mode = Dlss::Mode() == 3;
|
||||
const bool fsr4_mode = Dlss::OwnUpscaler(); // FSR 4 or XeSS (one path)
|
||||
// issn-axy: axylpup's 1.1 learns the inputs from this frame's SMAA draws (above). Where
|
||||
// they hold no colour image of the target's format, the offset search of 0.3.0: the colour
|
||||
// and the motion vectors move together between game builds (CUSA00223 1.07: +0x2c0000), and
|
||||
@@ -1745,16 +1752,17 @@ void Rasterizer::DlssResolve() {
|
||||
const bool steady = fsr4_fail_streak == 120;
|
||||
if (fsr4_misses <= 4 || steady || (fsr4_misses & 1023) == 0) {
|
||||
LOG_WARNING(Render_Vulkan,
|
||||
"FSR 4: not run ({}; {} of {} resolves) - target {:#x} {} {}x{}; at "
|
||||
"{}: not run ({}; {} of {} resolves) - target {:#x} {} {}x{}; at "
|
||||
"{:#x}: {}; motion {:#x}: {}; depth {:#x}: {}",
|
||||
why, fsr4_misses, dlss_calls, regs.color_buffers[0].Address(),
|
||||
Dlss::ModeName(), why, fsr4_misses, dlss_calls, regs.color_buffers[0].Address(),
|
||||
vk::to_string(out.info.pixel_format), out.info.size.width,
|
||||
out.info.size.height, colour_addr,
|
||||
texture_cache.DescribeImagesAt(colour_addr), motion_at,
|
||||
texture_cache.DescribeImagesAt(motion_at), depth_addr,
|
||||
texture_cache.DescribeImagesAt(depth_addr));
|
||||
if (fsr4_misses <= 2 || steady) {
|
||||
LOG_WARNING(Render_Vulkan, "FSR 4: recent {}x{} images: {}", out.info.size.width,
|
||||
LOG_WARNING(Render_Vulkan, "{}: recent {}x{} images: {}", Dlss::ModeName(),
|
||||
out.info.size.width,
|
||||
out.info.size.height,
|
||||
texture_cache.DescribeRecentImages(out.info.size.width,
|
||||
out.info.size.height, 24));
|
||||
@@ -1764,13 +1772,21 @@ void Rasterizer::DlssResolve() {
|
||||
constexpr u32 FSR4_GIVE_UP = 300;
|
||||
if (fsr4_fail_streak == FSR4_GIVE_UP) {
|
||||
Dlss::g_ready.store(false, std::memory_order_relaxed);
|
||||
LOG_WARNING(Render_Vulkan, "FSR 4: off ({}) - the game's own anti-aliasing is back",
|
||||
why);
|
||||
std::printf("FSR 4: off (%.*s) - the game's own anti-aliasing is back\n",
|
||||
int(why.size()), why.data());
|
||||
LOG_WARNING(Render_Vulkan, "{}: off ({}) - the game's own anti-aliasing is back",
|
||||
Dlss::ModeName(), why);
|
||||
std::printf("%s: off (%.*s) - the game's own anti-aliasing is back\n",
|
||||
Dlss::ModeName(), int(why.size()), why.data());
|
||||
std::fflush(stdout);
|
||||
}
|
||||
};
|
||||
// issn 10-28 (Reddit: DLSS "ready" but never running with a quality preset): at a render size
|
||||
// other than 1080p the inputs are found by the offset search above, which runs every 30th
|
||||
// resolve; DLSS stepped aside at the first miss, before it could. Now, as FSR 4, it waits
|
||||
// ~5 s of misses in a row first.
|
||||
static u32 dlss_size_misses = 0;
|
||||
if (in) {
|
||||
dlss_size_misses = 0;
|
||||
}
|
||||
if (!in) [[unlikely]] {
|
||||
if (fsr4_mode) {
|
||||
fsr4_missed("no colour input");
|
||||
@@ -1781,6 +1797,14 @@ void Rasterizer::DlssResolve() {
|
||||
// session: g_ready off stops the classification (the game's own T2x resolve runs from the
|
||||
// next frame on, on every path), the upscaler and DLSS 5.
|
||||
if (out.info.size.width != 1920 || out.info.size.height != 1080) {
|
||||
if (++dlss_size_misses == 1) {
|
||||
LOG_WARNING(Render_Vulkan, "DLSS: no colour input yet at {:#x} for the game's {}x{} "
|
||||
"frame; searching", colour_addr, out.info.size.width,
|
||||
out.info.size.height);
|
||||
}
|
||||
if (dlss_size_misses < 300) {
|
||||
return;
|
||||
}
|
||||
if (Dlss::g_ready.exchange(false, std::memory_order_relaxed)) {
|
||||
LOG_WARNING(Render_Vulkan,
|
||||
"DLSS steps aside: no colour input at {:#x} for the game's {}x{} "
|
||||
@@ -1804,14 +1828,44 @@ void Rasterizer::DlssResolve() {
|
||||
regs.color_buffers[0].Address());
|
||||
}
|
||||
scheduler.EndRendering();
|
||||
// issn-axy: frame generation (issn_framegen.h) keeps a copy of this frame's depth and motion
|
||||
// vectors for the presenter; the branches below set the layouts they need again.
|
||||
if (FrameGen::Active()) {
|
||||
VideoCore::Image* fg_motion = find(motion_at);
|
||||
VideoCore::Image* fg_depth = find(depth_addr);
|
||||
if (fg_motion && fg_depth && (fg_depth->aspect_mask & vk::ImageAspectFlagBits::eDepth)) {
|
||||
for (VideoCore::Image* image : {fg_motion, fg_depth}) {
|
||||
image->Transit(vk::ImageLayout::eTransferSrcOptimal,
|
||||
vk::AccessFlagBits2::eTransferRead, {});
|
||||
}
|
||||
float jx = 0.0f, jy = 0.0f;
|
||||
if (dlss_phase_in == 1 || dlss_phase_in == 2) {
|
||||
const float j = dlss_phase_in == 1 ? 0.25f : -0.25f;
|
||||
const u32 combo = Dlss::g_jitter_combo.load(std::memory_order_relaxed);
|
||||
jx = (combo & 1) ? -j : j;
|
||||
jy = (combo & 2) ? -j : j;
|
||||
} else if (dlss_phase_in == 4) {
|
||||
jx = dlss_jitter_in[0];
|
||||
jy = dlss_jitter_in[1];
|
||||
}
|
||||
const auto fg_input = [](VideoCore::Image& image) {
|
||||
return Fsr4Aa::Input{image.GetImage(), vk::ImageView{}, image.info.size.width,
|
||||
image.info.size.height, image.info.pixel_format};
|
||||
};
|
||||
FrameGen::Capture(instance, scheduler, scheduler.AcquireCommandBuffer().Handle(),
|
||||
fg_input(*fg_depth), fg_input(*fg_motion), jx, jy);
|
||||
}
|
||||
}
|
||||
bool evaluated = false;
|
||||
bool hud_layer = false;
|
||||
if (Dlss::Mode() == 3) {
|
||||
// issn-axy: ISSN_FSR4 - FSR 4 Native AA from the same inputs (issn_fsr4.h).
|
||||
if (Dlss::OwnUpscaler()) {
|
||||
// issn-axy: ISSN_FSR4 - FSR 4 Native AA from the same inputs (issn_fsr4.h); ISSN_XESS -
|
||||
// Intel XeSS the same way (issn_xess.h).
|
||||
const bool use_xess = Dlss::Mode() == 4;
|
||||
VideoCore::Image* motion = find(motion_at);
|
||||
VideoCore::Image* depth = find(depth_addr);
|
||||
if (motion && depth) [[likely]] {
|
||||
// FSR 4 copies them into images of its own (issn_fsr4.cpp CopyInputs).
|
||||
// FSR 4 / XeSS copy them into images of their own (CopyInputs).
|
||||
for (VideoCore::Image* image : {in, motion, depth}) {
|
||||
image->Transit(vk::ImageLayout::eTransferSrcOptimal, vk::AccessFlagBits2::eTransferRead,
|
||||
{});
|
||||
@@ -1831,14 +1885,35 @@ void Rasterizer::DlssResolve() {
|
||||
return Fsr4Aa::Input{image.GetImage(), *view.image_view, image.info.size.width,
|
||||
image.info.size.height, image.info.pixel_format};
|
||||
};
|
||||
// issn 11-04: phase 4 is the emulator's Halton offset (memory_patcher.cpp JitterFrame),
|
||||
// which also runs with FSR 4 / XeSS (Dlss::Enabled covers them). This branch read only
|
||||
// the game's 2 phases, so they got jitter 0 while the camera moved up to +-0.5 px each
|
||||
// frame: a soft picture. Now as DLSS: the exact offset, a new history on a switch.
|
||||
float jx = 0.0f, jy = 0.0f;
|
||||
if (dlss_phase_in == 1 || dlss_phase_in == 2) {
|
||||
const bool explicit_jitter = dlss_phase_in == 4;
|
||||
dlss_reset_pending |= explicit_jitter != dlss_last_explicit;
|
||||
dlss_last_explicit = explicit_jitter;
|
||||
if (explicit_jitter) {
|
||||
jx = dlss_jitter_in[0];
|
||||
jy = dlss_jitter_in[1];
|
||||
} else if (dlss_phase_in == 1 || dlss_phase_in == 2) {
|
||||
const float j = dlss_phase_in == 1 ? 0.25f : -0.25f;
|
||||
const u32 combo = Dlss::g_jitter_combo.load(std::memory_order_relaxed);
|
||||
jx = (combo & 1) ? -j : j;
|
||||
jy = (combo & 2) ? -j : j;
|
||||
}
|
||||
if (!fsr4) {
|
||||
// ISSN_JITTER_LOG=1 (test): the first 8 frames' jitter as the upscaler gets it.
|
||||
static const bool jitter_log = [] {
|
||||
const char* v = std::getenv("ISSN_JITTER_LOG");
|
||||
return v && v[0] == '1';
|
||||
}();
|
||||
if (jitter_log && dlss_evaluated < 8) {
|
||||
LOG_WARNING(Render_Vulkan, "{}: jitter phase {} -> ({:+.3f}, {:+.3f}) render px",
|
||||
Dlss::ModeName(), dlss_phase_in, jx, jy);
|
||||
}
|
||||
if (use_xess && !xess) {
|
||||
xess = std::make_unique<Xess::Upscaler>(instance, scheduler);
|
||||
} else if (!use_xess && !fsr4) {
|
||||
fsr4 = std::make_unique<Fsr4Aa::Upscaler>(instance, scheduler);
|
||||
}
|
||||
const auto cmdbuf = scheduler.AcquireCommandBuffer().Handle();
|
||||
@@ -1854,8 +1929,10 @@ void Rasterizer::DlssResolve() {
|
||||
const Dlss::EvalImage scene = upscaler->SceneTarget(cmdbuf);
|
||||
target = {scene.image, scene.format, scene.layout, scene.width, scene.height};
|
||||
}
|
||||
evaluated = fsr4->Record(cmdbuf, input(*in), input(*motion), input(*depth), target,
|
||||
jx, jy, dlss_reset_pending);
|
||||
evaluated = use_xess ? xess->Record(cmdbuf, input(*in), input(*motion), input(*depth),
|
||||
target, jx, jy, dlss_reset_pending)
|
||||
: fsr4->Record(cmdbuf, input(*in), input(*motion), input(*depth),
|
||||
target, jx, jy, dlss_reset_pending);
|
||||
dlss_reset_pending = false;
|
||||
if (upscaler && evaluated) {
|
||||
upscaler->SceneWritten(cmdbuf);
|
||||
@@ -1864,11 +1941,12 @@ void Rasterizer::DlssResolve() {
|
||||
scheduler.GetDynamicState().Invalidate();
|
||||
GraphicsBindTracker::Invalidate();
|
||||
scheduler.InvalidateNativeHeaps();
|
||||
if (!evaluated && !Fsr4Aa::DeviceSupported()) {
|
||||
const std::string& problem = use_xess ? xess->Problem() : fsr4->Problem();
|
||||
if (!evaluated &&
|
||||
!(use_xess ? Xess::DeviceSupported() : Fsr4Aa::DeviceSupported())) {
|
||||
Dlss::g_ready.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
if (!evaluated && !fsr4->Problem().empty() &&
|
||||
fsr4->Problem().starts_with("missing")) {
|
||||
if (!evaluated && !problem.empty() && problem.starts_with("missing")) {
|
||||
// Assets missing: from the next frame on the game's own T2x runs again.
|
||||
Dlss::g_ready.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
@@ -1876,15 +1954,18 @@ void Rasterizer::DlssResolve() {
|
||||
if (evaluated) {
|
||||
fsr4_fail_streak = 0;
|
||||
if (dlss_evaluated == 0) {
|
||||
LOG_WARNING(Render_Vulkan, "FSR 4: first frame resolved (colour {} {}x{} into {})",
|
||||
vk::to_string(in->info.pixel_format), in->info.size.width,
|
||||
LOG_WARNING(Render_Vulkan, "{}: first frame resolved (colour {} {}x{} into {})",
|
||||
Dlss::ModeName(), vk::to_string(in->info.pixel_format), in->info.size.width,
|
||||
in->info.size.height, vk::to_string(out.info.pixel_format));
|
||||
}
|
||||
} else {
|
||||
fsr4_missed(!motion ? "no motion vectors"
|
||||
: !depth ? "no depth"
|
||||
: (fsr4 && !fsr4->Problem().empty() ? fsr4->Problem().c_str()
|
||||
: "FSR did not record"));
|
||||
: (use_xess && xess && !xess->Problem().empty()
|
||||
? xess->Problem().c_str()
|
||||
: !use_xess && fsr4 && !fsr4->Problem().empty()
|
||||
? fsr4->Problem().c_str()
|
||||
: "the upscaler did not record"));
|
||||
}
|
||||
} else if (Dlss::Mode() == 1) {
|
||||
VideoCore::Image* motion = find(motion_at);
|
||||
@@ -2025,8 +2106,8 @@ void Rasterizer::DlssResolve() {
|
||||
out.flags |= VideoCore::ImageFlagBits::GpuModified;
|
||||
out.flags &= ~VideoCore::ImageFlagBits::Dirty;
|
||||
if (fsr4_mode && (dlss_calls & 4095) == 0) {
|
||||
LOG_WARNING(Render_Vulkan, "FSR 4: {} of {} resolves done by FSR, {} not run",
|
||||
dlss_evaluated, dlss_calls, fsr4_misses);
|
||||
LOG_WARNING(Render_Vulkan, "{}: {} of {} resolves done by it, {} not run",
|
||||
Dlss::ModeName(), dlss_evaluated, dlss_calls, fsr4_misses);
|
||||
}
|
||||
if ((dlss_calls & 1023) == 1) {
|
||||
LOG_INFO(Render_Vulkan,
|
||||
@@ -2038,6 +2119,239 @@ void Rasterizer::DlssResolve() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- issn 11-04: motion blur / chromatic aberration switches and the pass list (issn_postfx.h) --
|
||||
|
||||
namespace {
|
||||
u16 PostFxFormat(AmdGpu::DataFormat data, AmdGpu::NumberFormat number) {
|
||||
return static_cast<u16>(static_cast<u32>(data) | (static_cast<u32>(number) << 8));
|
||||
}
|
||||
|
||||
/// A shader's textures as the pass list shows them (at most 4).
|
||||
template <typename Bound>
|
||||
u8 PostFxInputs(const Bound& bound, std::array<PostFx::Input, 4>& in) {
|
||||
u8 n = 0;
|
||||
if (!bound.info) {
|
||||
return 0;
|
||||
}
|
||||
for (const auto& desc : bound.info->images) {
|
||||
if (n == in.size()) {
|
||||
break;
|
||||
}
|
||||
if (desc.is_written) {
|
||||
continue;
|
||||
}
|
||||
const AmdGpu::Image image = desc.GetSharp(bound);
|
||||
if (!image.Valid() || image.Address() == 0) {
|
||||
continue;
|
||||
}
|
||||
in[n++] = PostFx::Input{static_cast<VAddr>(image.Address()),
|
||||
static_cast<u16>(image.width + 1), static_cast<u16>(image.height + 1),
|
||||
PostFxFormat(image.GetDataFmt(), image.GetNumberFmt())};
|
||||
}
|
||||
return n;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool Rasterizer::PostFxDraw(const GraphicsPipeline* pipeline) {
|
||||
u64 ps = 0, vs = 0;
|
||||
for (const auto* stage : pipeline->GetStages()) {
|
||||
if (!stage) {
|
||||
continue;
|
||||
}
|
||||
if (stage->l_stage == Shader::LogicalStage::Fragment) {
|
||||
ps = stage->pgm_hash;
|
||||
} else if (stage->l_stage == Shader::LogicalStage::Vertex) {
|
||||
vs = stage->pgm_hash;
|
||||
}
|
||||
}
|
||||
if (ps == Dlss::ResolvePsHash) {
|
||||
PostFxPublish(); // the game's own T2x resolve: the end of the scene's post chain
|
||||
return false;
|
||||
}
|
||||
const auto& regs = liverpool->BackRegs();
|
||||
const auto& cb = regs.color_buffers[0];
|
||||
const VAddr target = cb.Address();
|
||||
const auto hint = liverpool->BackCbExtent(0);
|
||||
const u32 width = hint.width ? hint.width : cb.Pitch();
|
||||
const u32 height = hint.height ? hint.height : cb.Height();
|
||||
// A full-screen pass: one triangle or quad into a colour target at least a quarter of the
|
||||
// game's width (blur chains run at 1/2 and 1/4 size).
|
||||
const bool full_screen = target != 0 && regs.num_indices <= 6 &&
|
||||
regs.num_instances.NumInstances() <= 1 &&
|
||||
width * 4 >= Dlss::g_game_width.load(std::memory_order_relaxed);
|
||||
const PostFx::Action action = full_screen ? PostFx::Decide(ps) : PostFx::Action::Draw;
|
||||
const auto& fs = GetBoundStage(Shader::LogicalStage::Fragment);
|
||||
PostFx::Pass pass{};
|
||||
if (full_screen && (PostFx::g_collect.load(std::memory_order_relaxed) || PostFx::LogEnabled())) {
|
||||
pass.hash = ps;
|
||||
pass.vs_hash = vs;
|
||||
pass.target = target;
|
||||
pass.width = static_cast<u16>(width);
|
||||
pass.height = static_cast<u16>(height);
|
||||
pass.fmt = PostFxFormat(cb.GetDataFmt(), cb.GetNumberFmt());
|
||||
pass.action = static_cast<u8>(action);
|
||||
pass.inputs = PostFxInputs(fs, pass.in);
|
||||
if (postfx_frame.size() < PostFx::MaxPasses) {
|
||||
postfx_frame.push_back(pass);
|
||||
}
|
||||
}
|
||||
if (action == PostFx::Action::Draw) {
|
||||
return false;
|
||||
}
|
||||
if (action == PostFx::Action::Copy) {
|
||||
// The pass's own output replaced by its input of the target's size: the picture as it was
|
||||
// before the effect. Without such an input the draw is dropped (the target keeps what it
|
||||
// had), so the test can still show what the pass does.
|
||||
if (pass.hash == 0) {
|
||||
pass.inputs = PostFxInputs(fs, pass.in);
|
||||
}
|
||||
VideoCore::TextureCache::ImageDesc out_desc{cb, hint};
|
||||
auto& out = texture_cache.GetImage(texture_cache.FindImage(out_desc, true));
|
||||
VideoCore::Image* in = nullptr;
|
||||
for (u8 i = 0; i < pass.inputs && !in; ++i) {
|
||||
if (pass.in[i].addr == target) {
|
||||
continue;
|
||||
}
|
||||
if (const VideoCore::ImageId id = texture_cache.FindImageAtSize(
|
||||
pass.in[i].addr, out.info.size.width, out.info.size.height, out.info.pixel_format)) {
|
||||
in = &texture_cache.GetImage(id);
|
||||
}
|
||||
}
|
||||
for (u8 i = 0; i < pass.inputs && !in; ++i) {
|
||||
if (pass.in[i].addr == target) {
|
||||
continue;
|
||||
}
|
||||
if (const VideoCore::ImageId id = texture_cache.FindImageAtSize(
|
||||
pass.in[i].addr, out.info.size.width, out.info.size.height)) {
|
||||
VideoCore::Image& image = texture_cache.GetImage(id);
|
||||
if (!(image.aspect_mask & vk::ImageAspectFlagBits::eDepth)) {
|
||||
in = ℑ
|
||||
}
|
||||
}
|
||||
}
|
||||
if (in) {
|
||||
if (native_batch.open) [[unlikely]] {
|
||||
NativeClose(12);
|
||||
}
|
||||
rs_memo.valid = false;
|
||||
scheduler.EndRendering();
|
||||
if (in->info.pixel_format == out.info.pixel_format) {
|
||||
out.CopyImage(*in);
|
||||
} else {
|
||||
in->Transit(vk::ImageLayout::eTransferSrcOptimal, vk::AccessFlagBits2::eTransferRead, {});
|
||||
out.Transit(vk::ImageLayout::eTransferDstOptimal, vk::AccessFlagBits2::eTransferWrite, {});
|
||||
const vk::ImageSubresourceLayers layers{.aspectMask = vk::ImageAspectFlagBits::eColor,
|
||||
.mipLevel = 0,
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = 1};
|
||||
const vk::ImageBlit blit{
|
||||
.srcSubresource = layers,
|
||||
.srcOffsets = std::array{vk::Offset3D{0, 0, 0},
|
||||
vk::Offset3D{s32(in->info.size.width),
|
||||
s32(in->info.size.height), 1}},
|
||||
.dstSubresource = layers,
|
||||
.dstOffsets = std::array{vk::Offset3D{0, 0, 0},
|
||||
vk::Offset3D{s32(out.info.size.width),
|
||||
s32(out.info.size.height), 1}},
|
||||
};
|
||||
scheduler.AcquireCommandBuffer().Handle().blitImage(
|
||||
in->GetImage(), vk::ImageLayout::eTransferSrcOptimal, out.GetImage(),
|
||||
vk::ImageLayout::eTransferDstOptimal, blit, vk::Filter::eNearest);
|
||||
}
|
||||
out.flags |= VideoCore::ImageFlagBits::GpuModified;
|
||||
out.flags &= ~VideoCore::ImageFlagBits::Dirty;
|
||||
}
|
||||
}
|
||||
ResetBindings();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Rasterizer::PostFxDispatch(const ComputePipeline* pipeline) {
|
||||
u64 cs = 0;
|
||||
for (const auto* stage : pipeline->GetStages()) {
|
||||
if (stage) {
|
||||
cs = stage->pgm_hash;
|
||||
}
|
||||
}
|
||||
const auto& bound = GetBoundStage(Shader::LogicalStage::Compute);
|
||||
// A full-screen compute pass writes an image at least a quarter of the game's width.
|
||||
VAddr target = 0;
|
||||
u32 width = 0, height = 0;
|
||||
u16 fmt = 0;
|
||||
if (bound.info) {
|
||||
for (const auto& desc : bound.info->images) {
|
||||
if (!desc.is_written) {
|
||||
continue;
|
||||
}
|
||||
const AmdGpu::Image image = desc.GetSharp(bound);
|
||||
if (image.Valid() && image.Address() != 0) {
|
||||
target = image.Address();
|
||||
width = u32(image.width) + 1;
|
||||
height = u32(image.height) + 1;
|
||||
fmt = PostFxFormat(image.GetDataFmt(), image.GetNumberFmt());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const bool full_screen =
|
||||
target != 0 && width * 4 >= Dlss::g_game_width.load(std::memory_order_relaxed);
|
||||
const PostFx::Action action = full_screen ? PostFx::Decide(cs) : PostFx::Action::Draw;
|
||||
if (full_screen && (PostFx::g_collect.load(std::memory_order_relaxed) || PostFx::LogEnabled()) &&
|
||||
postfx_frame.size() < PostFx::MaxPasses) {
|
||||
PostFx::Pass pass{};
|
||||
pass.hash = cs;
|
||||
pass.target = target;
|
||||
pass.width = static_cast<u16>(width);
|
||||
pass.height = static_cast<u16>(height);
|
||||
pass.fmt = fmt;
|
||||
pass.compute = 1;
|
||||
// Compute passes are only dropped: they write in place, so there is nothing to copy.
|
||||
pass.action = static_cast<u8>(action == PostFx::Action::Draw ? action : PostFx::Action::Drop);
|
||||
pass.inputs = PostFxInputs(bound, pass.in);
|
||||
postfx_frame.push_back(pass);
|
||||
}
|
||||
return action != PostFx::Action::Draw;
|
||||
}
|
||||
|
||||
void Rasterizer::PostFxPublish() {
|
||||
if (!PostFx::g_collect.load(std::memory_order_relaxed) && !PostFx::LogEnabled()) {
|
||||
postfx_frame.clear();
|
||||
return;
|
||||
}
|
||||
{
|
||||
std::scoped_lock lock{PostFx::g_list.mutex};
|
||||
PostFx::g_list.last.swap(postfx_frame);
|
||||
++PostFx::g_list.frame;
|
||||
}
|
||||
postfx_frame.clear();
|
||||
if (!PostFx::LogEnabled()) {
|
||||
return;
|
||||
}
|
||||
const u64 now_ns = u64(std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count());
|
||||
if (postfx_log_ns != 0 && now_ns - postfx_log_ns < 1'000'000'000) {
|
||||
return;
|
||||
}
|
||||
postfx_log_ns = now_ns;
|
||||
std::scoped_lock lock{PostFx::g_list.mutex};
|
||||
const auto& list = PostFx::g_list.last;
|
||||
LOG_INFO(Render_Vulkan, "PostFx: {} full-screen passes before the T2x resolve (frame {})",
|
||||
list.size(), PostFx::g_list.frame);
|
||||
static constexpr const char* actions[] = {"", " [copied]", " [dropped]"};
|
||||
for (size_t i = 0; i < list.size(); ++i) {
|
||||
const auto& p = list[i];
|
||||
std::string inputs;
|
||||
for (u8 k = 0; k < p.inputs; ++k) {
|
||||
inputs += fmt::format(" {:#x} {}x{} f{:#x}", p.in[k].addr, p.in[k].width, p.in[k].height,
|
||||
p.in[k].fmt);
|
||||
}
|
||||
LOG_INFO(Render_Vulkan, "PostFx {:2}: {}_{:08x} (vs_{:08x}) -> {:#x} {}x{} f{:#x}; in:{}{}", i,
|
||||
p.compute ? "cs" : "fs", p.hash, p.vs_hash, p.target, p.width, p.height, p.fmt,
|
||||
inputs.empty() ? " none" : inputs, actions[std::min<u8>(p.action, 2)]);
|
||||
}
|
||||
}
|
||||
|
||||
bool Rasterizer::FilterDraw() {
|
||||
// The classifier reads the front's registers (it runs where the pipeline lookup runs); the
|
||||
// actions read the back's through BackRegs().
|
||||
@@ -2426,6 +2740,11 @@ void Rasterizer::Draw(bool is_indexed, u32 index_offset) {
|
||||
return;
|
||||
}
|
||||
BindStageData(pipeline);
|
||||
if (PostFx::Active()) [[unlikely]] {
|
||||
if (PostFxDraw(pipeline)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
DrawTail(pipeline, is_indexed, index_offset, sample, t_entry, t_pipeline, pipeline_ns);
|
||||
}
|
||||
|
||||
@@ -2623,6 +2942,12 @@ void Rasterizer::ExecuteDraw(const SplitDraw& draw,
|
||||
return false;
|
||||
}
|
||||
bound_stages = stages;
|
||||
// issn 11-04: motion blur / chromatic aberration off, the pass list (issn_postfx.h).
|
||||
if (PostFx::Active() && !draw.args.is_indirect) [[unlikely]] {
|
||||
if (PostFxDraw(draw.pipeline)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// SHADPS4_DRAW_REPEAT_STATS=1: census of draws whose binding inputs repeat the previous
|
||||
// draw's (scripts/patch/draw-repeat-census-2026-10-02.py).
|
||||
static const bool repeat_stats = [] {
|
||||
@@ -9055,6 +9380,12 @@ void Rasterizer::DispatchDirectTail(const ComputePipeline* pipeline) {
|
||||
if (ExecuteShaderHLE(cs, liverpool->BackRegs(), cs_program, *this)) {
|
||||
return;
|
||||
}
|
||||
if (PostFx::Active()) [[unlikely]] {
|
||||
if (PostFxDispatch(pipeline)) {
|
||||
ResetBindings();
|
||||
return;
|
||||
}
|
||||
}
|
||||
const bool pb_census = VideoCore::g_pb_census != 0;
|
||||
if (pb_census) [[unlikely]] {
|
||||
PbCensusDispatch(pipeline, 0);
|
||||
@@ -13806,6 +14137,34 @@ RenderState Rasterizer::BeginRendering(const GraphicsPipeline* pipeline) {
|
||||
for (u32 cb = state.num_color_attachments; cb < state.color_attachments.size(); ++cb) {
|
||||
state.color_attachments[cb] = {};
|
||||
}
|
||||
// axylpup 1.1.1 (user 10-22): black bars beside a fitted movie (issn_game_patches.cpp
|
||||
// ApplyMovieFit). On the PS4 the movie quad covered its target whole, so beside the smaller quad
|
||||
// the target kept the last game frame. The full-screen movie draw (vs b94242a8 + the NV12 ps
|
||||
// b03b3012) whose first vertex is the fitted corner clears its target to black first; menu
|
||||
// videos use the same shaders at other positions and are left alone. A cleared state never
|
||||
// comes from the render-state memo (RenderStateMemoStore refuses it).
|
||||
if (float fx, fy; pipeline && state.num_color_attachments >= 1 &&
|
||||
state.color_attachments[0].image_view && MemoryPatcher::MovieFitRect(fx, fy))
|
||||
[[unlikely]] {
|
||||
const auto& stages = pipeline->GetStages();
|
||||
const auto* vs = stages[u32(Shader::LogicalStage::Vertex)];
|
||||
const auto* fs = stages[u32(Shader::LogicalStage::Fragment)];
|
||||
if (vs && fs && vs->pgm_hash == 0xb94242a8 && fs->pgm_hash == 0xb03b3012 &&
|
||||
!vs->buffers.empty()) {
|
||||
const AmdGpu::Buffer quad =
|
||||
vs->buffers[0].GetSharp(GetBoundStage(Shader::LogicalStage::Vertex));
|
||||
std::array<float, 16> pos{}; // 4 x vec4: vertex 0 = (right, top) ... 3 = (left, bottom)
|
||||
if (quad.GetSize() >= sizeof(pos) &&
|
||||
memory->TryReadBacking(quad.base_address, reinterpret_cast<u8*>(pos.data()),
|
||||
sizeof(pos)) &&
|
||||
std::abs(pos[0] - fx) < 1e-4f && std::abs(pos[1] - fy) < 1e-4f &&
|
||||
std::abs(pos[12] + fx) < 1e-4f && std::abs(pos[13] + fy) < 1e-4f) {
|
||||
auto& attachment = state.color_attachments[0];
|
||||
attachment.is_clear = true;
|
||||
attachment.clear_value = {0u, 0u, 0u, std::bit_cast<u32>(1.0f)};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (auto image_id = db_desc.first; image_id) {
|
||||
auto& desc = db_desc.second;
|
||||
@@ -13910,6 +14269,34 @@ RenderState Rasterizer::BeginRendering(const GraphicsPipeline* pipeline) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// issn 11-05 (issues #28, #32): on NVIDIA the GPU is lost right after the first pass with no
|
||||
// attachment at all (no colour, no depth) in the opening village, whatever the anti-aliasing.
|
||||
// ISSN_EMPTY_PASS=skip (test): such passes rasterize nothing (scissors 0, as the water blur
|
||||
// skip above), counted in the log.
|
||||
{
|
||||
static const bool skip_empty = [] {
|
||||
const char* e = std::getenv("ISSN_EMPTY_PASS");
|
||||
const bool on = e && std::strcmp(e, "skip") == 0;
|
||||
if (on) {
|
||||
LOG_WARNING(Render_Vulkan, "ISSN_EMPTY_PASS=skip: passes with no attachment draw nothing (test)");
|
||||
}
|
||||
return on;
|
||||
}();
|
||||
if (skip_empty && !state.depth_stencil_attachment.image_view) [[unlikely]] {
|
||||
bool any_color = false;
|
||||
for (u32 cb = 0; cb < state.num_color_attachments; ++cb) {
|
||||
any_color = any_color || state.color_attachments[cb].image_view;
|
||||
}
|
||||
if (!any_color) {
|
||||
state.skip = 1;
|
||||
static std::atomic<u64> skipped{0};
|
||||
const u64 n = skipped.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
if (n == 1 || n == 100 || n == 10000) {
|
||||
LOG_WARNING(Render_Vulkan, "ISSN_EMPTY_PASS=skip: {} passes skipped", n);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// The draw's dynamic state is committed before Scheduler::BeginRendering runs: the pass's
|
||||
// scale must be in place now (review 2026-10-14 BUG 1; a no-op while the flag is off).
|
||||
scheduler.GetDynamicState().SetRenderScale(state.scale, state.skip != 0);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "video_core/page_manager.h"
|
||||
#include "video_core/renderer_vulkan/vk_bindless.h"
|
||||
#include "video_core/renderer_vulkan/vk_native_bindings.h"
|
||||
#include "video_core/renderer_vulkan/issn_postfx.h"
|
||||
#include "video_core/renderer_vulkan/vk_occlusion.h"
|
||||
#include "shader_recompiler/native_emit.h"
|
||||
#include "video_core/renderer_vulkan/vk_pipeline_cache.h"
|
||||
@@ -38,6 +39,9 @@ namespace Vulkan {
|
||||
namespace Fsr4Aa {
|
||||
class Upscaler;
|
||||
}
|
||||
namespace Xess {
|
||||
class Upscaler;
|
||||
}
|
||||
|
||||
class Scheduler;
|
||||
class RenderState;
|
||||
@@ -329,6 +333,15 @@ public:
|
||||
void DeriveNeoIdBuffer(VideoCore::Image& id_image);
|
||||
/// SHADPS4_DLSS: the back-side action that replaces the T2x resolve draw.
|
||||
void DlssResolve();
|
||||
/// issn 11-04 (issn_postfx.h): the switches for motion blur and chromatic aberration and the
|
||||
/// list of full-screen passes. Back thread, with the draw's stages bound; true = the draw is
|
||||
/// done here (copied or dropped).
|
||||
bool PostFxDraw(const GraphicsPipeline* pipeline);
|
||||
bool PostFxDispatch(const ComputePipeline* pipeline);
|
||||
/// The T2x resolve (the game's own or replaced): the frame's list is published.
|
||||
void PostFxPublish();
|
||||
std::vector<PostFx::Pass> postfx_frame;
|
||||
u64 postfx_log_ns{};
|
||||
u64 dlss_calls{};
|
||||
u64 dlss_missing{};
|
||||
u8 dlss_phase_in{}; ///< the phase of the action being executed
|
||||
@@ -348,6 +361,7 @@ public:
|
||||
u64 dlss_delta_searches{};
|
||||
u64 dlss_last_resolve_ns{}; ///< a gap (Home off, menus, loads) restarts FSR's history ///< the colour input differs in format from the target (blit, no copy)
|
||||
std::unique_ptr<Fsr4Aa::Upscaler> fsr4; ///< ISSN_FSR4 (issn_fsr4.h), made at the first resolve
|
||||
std::unique_ptr<Xess::Upscaler> xess; ///< ISSN_XESS (issn_xess.h), made at the first resolve
|
||||
struct NeoIdDerive;
|
||||
std::shared_ptr<NeoIdDerive> neo_id_derive;
|
||||
VAddr neo_depth_addr{};
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
@@ -95,10 +98,26 @@ void Swapchain::Create(u32 width_, u32 height_) {
|
||||
width = width_;
|
||||
height = height_;
|
||||
needs_recreation = false;
|
||||
recreate_pending = false;
|
||||
|
||||
Destroy();
|
||||
|
||||
SetSurfaceProperties();
|
||||
// issn: a minimized fullscreen window (Alt+Tab) has a 0x0 surface and a lost surface cannot be
|
||||
// queried: no swapchain until the window is back; acquire and present skip frames meanwhile.
|
||||
// Both asserted before and closed the game on Alt+Tab in fullscreen (tester 10-27).
|
||||
if (!SetSurfaceProperties() || extent.width == 0 || extent.height == 0) {
|
||||
static u32 empty_logged;
|
||||
if (extent.width == 0 || extent.height == 0) {
|
||||
if (empty_logged++ % 600 == 0) {
|
||||
LOG_WARNING(Render_Vulkan, "Swapchain: the window's surface is {}x{} (minimized or "
|
||||
"off screen): frames are skipped until it is back", extent.width,
|
||||
extent.height);
|
||||
}
|
||||
}
|
||||
needs_recreation = true;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(16)); // no busy loop while away
|
||||
return;
|
||||
}
|
||||
|
||||
const std::array queue_family_indices = {
|
||||
instance.GetGraphicsQueueFamilyIndex(),
|
||||
@@ -130,8 +149,12 @@ void Swapchain::Create(u32 width_, u32 height_) {
|
||||
};
|
||||
|
||||
auto [swapchain_result, chain] = instance.GetDevice().createSwapchainKHR(swapchain_info);
|
||||
ASSERT_MSG(swapchain_result == vk::Result::eSuccess, "Failed to create swapchain: {}",
|
||||
vk::to_string(swapchain_result));
|
||||
if (swapchain_result != vk::Result::eSuccess) {
|
||||
LOG_WARNING(Render_Vulkan, "Swapchain not created ({}): frames are skipped until it is",
|
||||
vk::to_string(swapchain_result));
|
||||
needs_recreation = true;
|
||||
return;
|
||||
}
|
||||
swapchain = chain;
|
||||
|
||||
SetupImages();
|
||||
@@ -156,23 +179,48 @@ void Swapchain::SetHDR(bool hdr) {
|
||||
}
|
||||
|
||||
needs_hdr = hdr;
|
||||
// issn-axy: the log says when the game switches HDR (Reddit reports of a dark picture with
|
||||
// HDR on, 10-30: the log did not say whether HDR was in use at all).
|
||||
std::printf("HDR output: %s\n", hdr ? "on (the game outputs BT.2020 PQ)" : "off (SDR)");
|
||||
std::fflush(stdout);
|
||||
Recreate(width, height);
|
||||
ImGui::Core::OnSurfaceFormatChange(needs_hdr ? SURFACE_FORMAT_HDR.format
|
||||
: surface_format.format);
|
||||
}
|
||||
|
||||
// VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT (Windows only in the headers).
|
||||
constexpr auto FullscreenExclusiveLost = static_cast<vk::Result>(-1000255000);
|
||||
|
||||
bool Swapchain::AcquireNextImage() {
|
||||
if (!swapchain) {
|
||||
needs_recreation = true;
|
||||
return false;
|
||||
}
|
||||
vk::Device device = instance.GetDevice();
|
||||
vk::Result result =
|
||||
device.acquireNextImageKHR(swapchain, std::numeric_limits<u64>::max(),
|
||||
image_acquired[frame_index], VK_NULL_HANDLE, &image_index);
|
||||
|
||||
last_acquire = result;
|
||||
switch (result) {
|
||||
case vk::Result::eSuccess:
|
||||
suboptimal_streak = 0;
|
||||
break;
|
||||
case vk::Result::eSuboptimalKHR:
|
||||
// issn: the image is ours and can be shown. Rebuild the swapchain before the next frame
|
||||
// a few times; a driver that stays suboptimal is presented to as it is.
|
||||
if (++suboptimal_streak <= 3) {
|
||||
recreate_pending = true;
|
||||
} else if (suboptimal_streak == 4) {
|
||||
LOG_WARNING(Render_Vulkan, "Swapchain: the driver keeps reporting it suboptimal; "
|
||||
"presenting anyway");
|
||||
}
|
||||
break;
|
||||
case vk::Result::eErrorSurfaceLostKHR:
|
||||
case vk::Result::eErrorOutOfDateKHR:
|
||||
case FullscreenExclusiveLost:
|
||||
case vk::Result::eTimeout:
|
||||
case vk::Result::eNotReady:
|
||||
case vk::Result::eErrorUnknown:
|
||||
needs_recreation = true;
|
||||
break;
|
||||
@@ -187,6 +235,10 @@ bool Swapchain::AcquireNextImage() {
|
||||
}
|
||||
|
||||
bool Swapchain::Present() {
|
||||
if (!swapchain) {
|
||||
needs_recreation = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
const vk::PresentInfoKHR present_info = {
|
||||
.waitSemaphoreCount = 1,
|
||||
@@ -206,8 +258,13 @@ bool Swapchain::Present() {
|
||||
} else {
|
||||
result = instance.GetPresentQueue().presentKHR(present_info);
|
||||
}
|
||||
if (result == vk::Result::eErrorOutOfDateKHR || result == vk::Result::eSuboptimalKHR) {
|
||||
needs_recreation = true;
|
||||
if (result == vk::Result::eSuboptimalKHR) {
|
||||
// issn: shown; the acquire side decides whether to rebuild (see AcquireNextImage)
|
||||
} else if (result == vk::Result::eErrorOutOfDateKHR ||
|
||||
result == vk::Result::eErrorSurfaceLostKHR ||
|
||||
result == FullscreenExclusiveLost ||
|
||||
result == vk::Result::eErrorUnknown) {
|
||||
needs_recreation = true; // issn: Alt+Tab in fullscreen; the window comes back
|
||||
} else {
|
||||
ASSERT_MSG(result == vk::Result::eSuccess, "Swapchain presentation failed: {}",
|
||||
vk::to_string(result));
|
||||
@@ -232,6 +289,15 @@ void Swapchain::FindPresentFormat() {
|
||||
return format == SURFACE_FORMAT_HDR;
|
||||
}) != formats.end();
|
||||
// Also make sure that user allowed us to use HDR
|
||||
static bool told_hdr = false;
|
||||
if (!told_hdr && EmulatorSettings.IsHdrAllowed()) {
|
||||
told_hdr = true;
|
||||
std::printf("HDR: %s\n", supports_hdr
|
||||
? "allowed, and the display offers HDR10 (the game decides)"
|
||||
: "allowed in the launcher, but the display offers no HDR10 "
|
||||
"format (is HDR on in Windows?): SDR");
|
||||
std::fflush(stdout);
|
||||
}
|
||||
supports_hdr &= EmulatorSettings.IsHdrAllowed();
|
||||
|
||||
// If there is a single undefined surface format, the device doesn't care, so we'll just use
|
||||
@@ -258,11 +324,16 @@ void Swapchain::FindPresentFormat() {
|
||||
}
|
||||
|
||||
static std::atomic<u32> g_vsync_divider{1};
|
||||
static std::atomic<bool> g_present_fifo{true}; ///< PresentWaitsForVsync
|
||||
|
||||
u32 VsyncDivider() {
|
||||
return g_vsync_divider.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool PresentWaitsForVsync() {
|
||||
return g_present_fifo.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool Swapchain::PresentModeChanged() {
|
||||
if (++present_mode_checks % 30 != 0 ||
|
||||
EmulatorSettings.GetPresentMode() == present_mode_setting) {
|
||||
@@ -292,6 +363,7 @@ void Swapchain::FindPresentMode() {
|
||||
LOG_ERROR(Render, "Failed to query available present modes, falling back to Fifo as "
|
||||
"guaranteed supported option.");
|
||||
present_mode = vk::PresentModeKHR::eFifo;
|
||||
g_present_fifo.store(true, std::memory_order_relaxed);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -318,14 +390,20 @@ void Swapchain::FindPresentMode() {
|
||||
vk::to_string(present_mode), vk::to_string(fallback));
|
||||
present_mode = fallback;
|
||||
}
|
||||
g_present_fifo.store(present_mode == vk::PresentModeKHR::eFifo ||
|
||||
present_mode == vk::PresentModeKHR::eFifoRelaxed,
|
||||
std::memory_order_relaxed);
|
||||
LOG_INFO(Render_Vulkan, "Present mode: {}", vk::to_string(present_mode));
|
||||
}
|
||||
|
||||
void Swapchain::SetSurfaceProperties() {
|
||||
bool Swapchain::SetSurfaceProperties() {
|
||||
const auto [capabilities_result, capabilities] =
|
||||
instance.GetPhysicalDevice().getSurfaceCapabilitiesKHR(surface);
|
||||
ASSERT_MSG(capabilities_result == vk::Result::eSuccess,
|
||||
"Failed to query surface capabilities: {}", vk::to_string(capabilities_result));
|
||||
if (capabilities_result != vk::Result::eSuccess) {
|
||||
LOG_WARNING(Render_Vulkan, "Surface capabilities unavailable ({}): the window is gone for now",
|
||||
vk::to_string(capabilities_result));
|
||||
return false;
|
||||
}
|
||||
|
||||
extent = capabilities.currentExtent;
|
||||
if (capabilities.currentExtent.width == std::numeric_limits<u32>::max()) {
|
||||
@@ -359,14 +437,21 @@ void Swapchain::SetSurfaceProperties() {
|
||||
if (!(capabilities.supportedCompositeAlpha & vk::CompositeAlphaFlagBitsKHR::eOpaque)) {
|
||||
composite_alpha = vk::CompositeAlphaFlagBitsKHR::eInherit;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Swapchain::Destroy() {
|
||||
vk::Device device = instance.GetDevice();
|
||||
const auto wait_result = WaitForSwapchainIdle(instance);
|
||||
if (wait_result != vk::Result::eSuccess) {
|
||||
LOG_WARNING(Render_Vulkan, "Failed to wait for device to become idle: {}",
|
||||
vk::to_string(wait_result));
|
||||
// issn 11-05: one line per recreation filled a log with 13,800 of them; the first 3, then
|
||||
// every 1000th.
|
||||
static std::atomic<u64> failed{0};
|
||||
const u64 n = failed.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
if (n <= 3 || n % 1000 == 0) {
|
||||
LOG_WARNING(Render_Vulkan, "Failed to wait for device to become idle: {} ({} times)",
|
||||
vk::to_string(wait_result), n);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& image_view : images_view) {
|
||||
@@ -376,6 +461,7 @@ void Swapchain::Destroy() {
|
||||
|
||||
if (swapchain) {
|
||||
device.destroySwapchainKHR(swapchain);
|
||||
swapchain = nullptr;
|
||||
}
|
||||
|
||||
for (const auto& sem : image_acquired) {
|
||||
|
||||
@@ -27,6 +27,9 @@ class Scheduler;
|
||||
/// "Fifo (half refresh)" = 2, or SHADPS4_VSYNC_DIVIDER=2..4; both force FIFO (V-Sync). Set when
|
||||
/// the swapchain picks its present mode (PERF-PLAN 15).
|
||||
u32 VsyncDivider();
|
||||
/// issn-axy: the swapchain presents with FIFO (V-Sync): each present waits for a screen refresh.
|
||||
/// Frame generation then needs no pacing of its own (driver.cpp Flip).
|
||||
bool PresentWaitsForVsync();
|
||||
|
||||
class Swapchain {
|
||||
public:
|
||||
@@ -106,6 +109,15 @@ public:
|
||||
/// F3's Present Mode changed since the swapchain chose its mode (checked every 30th call):
|
||||
/// picks the new mode; the caller recreates the swapchain (live, no restart).
|
||||
bool PresentModeChanged();
|
||||
bool RecreatePending() const {
|
||||
return recreate_pending;
|
||||
}
|
||||
vk::Result LastAcquire() const {
|
||||
return last_acquire;
|
||||
}
|
||||
bool HasSwapchain() const {
|
||||
return static_cast<bool>(swapchain);
|
||||
}
|
||||
|
||||
private:
|
||||
/// Selects the best available swapchain image format
|
||||
@@ -115,7 +127,8 @@ private:
|
||||
void FindPresentMode();
|
||||
|
||||
/// Sets the surface properties according to device capabilities
|
||||
void SetSurfaceProperties();
|
||||
/// false when the surface cannot be queried (lost while the window is away).
|
||||
bool SetSurfaceProperties();
|
||||
|
||||
/// Destroys current swapchain resources
|
||||
void Destroy();
|
||||
@@ -149,6 +162,12 @@ private:
|
||||
u32 image_index = 0;
|
||||
u32 frame_index = 0;
|
||||
bool needs_recreation = true;
|
||||
// issn: a driver that keeps answering "suboptimal" (tester 10-28, black screen for good: every
|
||||
// acquire was dropped and the swapchain rebuilt) gets its images presented anyway; the
|
||||
// swapchain is rebuilt for it a few times only.
|
||||
bool recreate_pending = false;
|
||||
u32 suboptimal_streak = 0;
|
||||
vk::Result last_acquire = vk::Result::eSuccess;
|
||||
bool needs_hdr = false; // The game requested HDR swapchain
|
||||
bool supports_hdr = false; // SC supports HDR output
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <atomic>
|
||||
#include "video_core/renderer_vulkan/vk_gpu_batch_timer.h"
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
@@ -191,9 +192,14 @@ Image::Image(const Vulkan::Instance& instance_, Vulkan::Scheduler& scheduler_,
|
||||
const auto image_format_properties =
|
||||
instance->GetPhysicalDevice().getImageFormatProperties2(format_info);
|
||||
if (image_format_properties.result == vk::Result::eErrorFormatNotSupported) {
|
||||
LOG_ERROR(Render_Vulkan, "image format {} type {} is not supported (flags {}, usage {})",
|
||||
vk::to_string(supported_format), vk::to_string(format_info.type),
|
||||
vk::to_string(format_info.flags), vk::to_string(format_info.usage));
|
||||
// issn: the same few formats on every texture (~700 lines a session): first 8 only.
|
||||
static std::atomic<u32> logged{0};
|
||||
if (logged.fetch_add(1, std::memory_order_relaxed) < 8) {
|
||||
LOG_ERROR(Render_Vulkan,
|
||||
"image format {} type {} is not supported (flags {}, usage {}) (logged 8 times)",
|
||||
vk::to_string(supported_format), vk::to_string(format_info.type),
|
||||
vk::to_string(format_info.flags), vk::to_string(format_info.usage));
|
||||
}
|
||||
}
|
||||
supported_samples = image_format_properties.result == vk::Result::eSuccess
|
||||
? image_format_properties.value.imageFormatProperties.sampleCounts
|
||||
@@ -705,9 +711,14 @@ static std::pair<u32, u32> SanitizeCopyLayers(const ImageInfo& src_info, const I
|
||||
// If the image type is equal, layer count must match. Take the minimum of both.
|
||||
if (vk_src_type == vk_dst_type) {
|
||||
if (src_layers != dst_layers) {
|
||||
LOG_WARNING(Render_Vulkan,
|
||||
"Coercing copy source layers {} and destination layers {} to minimum.",
|
||||
src_layers, dst_layers);
|
||||
// issn: hundreds per minute in tester logs; the first few are enough.
|
||||
static std::atomic<u32> logged{0};
|
||||
if (logged.fetch_add(1, std::memory_order_relaxed) < 3) {
|
||||
LOG_WARNING(Render_Vulkan,
|
||||
"Coercing copy source layers {} and destination layers {} to minimum "
|
||||
"(logged 3 times)",
|
||||
src_layers, dst_layers);
|
||||
}
|
||||
src_layers = dst_layers = std::min(src_layers, dst_layers);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <algorithm>
|
||||
#include "video_core/renderer_vulkan/liverpool_to_vk.h"
|
||||
@@ -9,15 +10,50 @@
|
||||
|
||||
namespace VideoCore {
|
||||
|
||||
namespace {
|
||||
std::atomic<float> g_forced_aniso{-1.0f}; ///< -1 = not read from ISSN_ANISO yet
|
||||
}
|
||||
|
||||
float ForcedAniso() {
|
||||
float level = g_forced_aniso.load(std::memory_order_relaxed);
|
||||
if (level < 0.0f) {
|
||||
const char* v = std::getenv("ISSN_ANISO");
|
||||
level = v ? std::max(static_cast<float>(std::atof(v)), 0.0f) : 0.0f;
|
||||
float expected = -1.0f;
|
||||
g_forced_aniso.compare_exchange_strong(expected, level);
|
||||
level = g_forced_aniso.load(std::memory_order_relaxed);
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
void SetForcedAniso(float level) {
|
||||
g_forced_aniso.store(std::max(level, 0.0f), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
Sampler::Sampler(const Vulkan::Instance& instance, const AmdGpu::Sampler& sampler,
|
||||
const AmdGpu::BorderColorBuffer border_color_base) {
|
||||
using namespace Vulkan;
|
||||
// issn: ISSN_ANISO=4/8/16 (launcher: Graphics > Texture filtering) raises the anisotropy of the
|
||||
// game's filtered, mipmapped colour samplers to at least that (sharper ground and walls at an
|
||||
// angle, like a PC game's setting); point, unmipmapped and depth-compare samplers stay as the
|
||||
// game set them. Unset or 1: the game's own filtering.
|
||||
const float forced_aniso = ForcedAniso();
|
||||
const bool force_aniso = forced_aniso > 1.0f &&
|
||||
sampler.xy_min_filter != AmdGpu::Filter::Point &&
|
||||
sampler.xy_min_filter != AmdGpu::Filter::AnisoPoint &&
|
||||
sampler.mip_filter != AmdGpu::MipFilter::None &&
|
||||
sampler.depth_compare_func == AmdGpu::DepthCompare::Never &&
|
||||
// an aniso sampler cut to 1x (BindTextures: a texture without mips)
|
||||
!(AmdGpu::IsAnisoFilter(sampler.xy_min_filter) &&
|
||||
sampler.max_aniso.Value() == AmdGpu::AnisoRatio::One);
|
||||
const bool anisotropy_enable = instance.IsAnisotropicFilteringSupported() &&
|
||||
(AmdGpu::IsAnisoFilter(sampler.xy_mag_filter) ||
|
||||
AmdGpu::IsAnisoFilter(sampler.xy_min_filter));
|
||||
AmdGpu::IsAnisoFilter(sampler.xy_min_filter) || force_aniso);
|
||||
const float max_anisotropy =
|
||||
anisotropy_enable ? std::clamp(sampler.MaxAniso(), 1.0f, instance.MaxSamplerAnisotropy())
|
||||
: 1.0f;
|
||||
anisotropy_enable
|
||||
? std::clamp(std::max(sampler.MaxAniso(), force_aniso ? forced_aniso : 1.0f), 1.0f,
|
||||
instance.MaxSamplerAnisotropy())
|
||||
: 1.0f;
|
||||
auto border_color = LiverpoolToVK::BorderColor(sampler.border_color_type);
|
||||
if (border_color == vk::BorderColor::eFloatCustomEXT &&
|
||||
!instance.IsCustomBorderColorSupported()) {
|
||||
|
||||
@@ -14,6 +14,12 @@ class Instance;
|
||||
|
||||
namespace VideoCore {
|
||||
|
||||
/// issn-axy: the anisotropy forced on the game's filtered colour samplers (ISSN_ANISO at start,
|
||||
/// the in-game menu's Texture filtering live; 0 = the game's own). The texture cache keys its
|
||||
/// samplers by it, so a change makes new samplers on the next draws.
|
||||
float ForcedAniso();
|
||||
void SetForcedAniso(float level);
|
||||
|
||||
class Sampler {
|
||||
public:
|
||||
explicit Sampler(const Vulkan::Instance& instance, const AmdGpu::Sampler& sampler,
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <chrono>
|
||||
#include <unordered_map>
|
||||
#include <boost/container/small_vector.hpp>
|
||||
#include <cstdlib>
|
||||
@@ -898,12 +901,47 @@ std::tuple<ImageId, int, int> TextureCache::ResolveOverlap(const ImageInfo& imag
|
||||
// Left overlap, the image from cache is a possible subresource of the image requested
|
||||
if (auto mip = cache_image.info.MipOf(image_info); mip >= 0) {
|
||||
if (auto slice = cache_image.info.SliceOf(image_info, mip); slice >= 0) {
|
||||
// issn 11-04 (dark night): what the game rendered into a slice of a layered image
|
||||
// (shadow maps of the city's lights, probes) before the whole image was asked for.
|
||||
// SHADPS4_SLICE_LOG=1: the first 32 of these merges in the log.
|
||||
static const bool slice_log = [] {
|
||||
const char* v = std::getenv("SHADPS4_SLICE_LOG");
|
||||
return v && v[0] == '1';
|
||||
}();
|
||||
if (slice_log) {
|
||||
static std::atomic<u32> logged{0};
|
||||
if (logged.fetch_add(1, std::memory_order_relaxed) < 32) {
|
||||
LOG_WARNING(Render_Vulkan,
|
||||
"SliceMerge: {} {}x{} {} at {:#x} (target {}) -> slice {} mip {} of "
|
||||
"{}x{} {} layers {} at {:#x}{}",
|
||||
vk::to_string(cache_image.info.pixel_format),
|
||||
cache_image.info.size.width, cache_image.info.size.height,
|
||||
+cache_image.info.props.is_depth, cache_image.info.guest_address,
|
||||
+cache_image.binding.is_target, slice, mip, image_info.size.width,
|
||||
image_info.size.height, vk::to_string(image_info.pixel_format),
|
||||
image_info.resources.layers, image_info.guest_address,
|
||||
merged_image_id ? "" : " (no merged image yet: contents lost)");
|
||||
}
|
||||
}
|
||||
// SHADPS4_SLICE_RT_COPY=1 (test, off by default until a run shows it): a slice
|
||||
// bound as a render target also hands its contents to the larger image before it
|
||||
// is freed (only same-kind images: color into color, depth into depth).
|
||||
static const bool copy_targets = [] {
|
||||
const char* v = std::getenv("SHADPS4_SLICE_RT_COPY");
|
||||
return v && v[0] == '1';
|
||||
}();
|
||||
|
||||
// We have a larger image created and a separate one, representing a subres of it
|
||||
// bound as render target. In this case we need to rebind render target.
|
||||
if (cache_image.binding.is_target) {
|
||||
cache_image.binding.needs_rebind = 1u;
|
||||
if (merged_image_id) {
|
||||
GetImage(merged_image_id).binding.is_target = 1u;
|
||||
auto& merged_image = GetImage(merged_image_id);
|
||||
merged_image.binding.is_target = 1u;
|
||||
if (copy_targets &&
|
||||
merged_image.info.props.is_depth == cache_image.info.props.is_depth) {
|
||||
merged_image.CopyMip(cache_image, mip, slice);
|
||||
}
|
||||
}
|
||||
|
||||
FreeImage(cache_image_id);
|
||||
@@ -1795,12 +1833,25 @@ void TextureCache::RefreshImage(Image& image) {
|
||||
g_refresh_lock_cyc = 0;
|
||||
}
|
||||
|
||||
// issn-axy: samplers keyed by the guest's descriptor and the forced anisotropy of the moment (the
|
||||
// menu's Texture filtering, sampler.h): a change makes new ones; the old ones age out (GC).
|
||||
static u64 SamplerKey(const AmdGpu::Sampler& sampler, float aniso) {
|
||||
return XXH3_64bits(&sampler, sizeof(sampler)) ^
|
||||
(static_cast<u64>(aniso * 16.0f) * 0x9E3779B97F4A7C15ull);
|
||||
}
|
||||
|
||||
vk::Sampler TextureCache::GetSampler(const AmdGpu::Sampler& sampler,
|
||||
AmdGpu::BorderColorBuffer border_color_base,
|
||||
u32* native_index) {
|
||||
const u64 hash = XXH3_64bits(&sampler, sizeof(sampler));
|
||||
const float aniso = VideoCore::ForcedAniso();
|
||||
const u64 hash = SamplerKey(sampler, aniso);
|
||||
|
||||
std::scoped_lock lock{samplers_mutex};
|
||||
if (aniso != samplers_aniso) {
|
||||
// Draws that kept a sampler (bind memos) look again: the generation tells them.
|
||||
samplers_aniso = aniso;
|
||||
sampler_generation.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
const auto [it, new_sampler] = samplers.try_emplace(hash, instance, sampler, border_color_base);
|
||||
if (new_sampler) {
|
||||
samplers.at(hash).lru_id = sampler_lru_cache.Insert(hash, gc_tick);
|
||||
@@ -1856,7 +1907,7 @@ void TextureCache::TouchImageAccess(Image& image) {
|
||||
}
|
||||
|
||||
size_t TextureCache::SamplerLruId(const AmdGpu::Sampler& sampler) {
|
||||
const u64 hash = XXH3_64bits(&sampler, sizeof(sampler));
|
||||
const u64 hash = SamplerKey(sampler, VideoCore::ForcedAniso());
|
||||
std::scoped_lock lock{samplers_mutex};
|
||||
const auto it = samplers.find(hash);
|
||||
return it != samplers.end() ? it->second.lru_id : ~size_t{0};
|
||||
@@ -2109,15 +2160,72 @@ void TextureCache::UntrackImageTail(ImageId image_id) {
|
||||
}
|
||||
|
||||
void TextureCache::GarbageCollectImages() {
|
||||
// issn-axy (10-30, an 8 GB NVIDIA card in the neon district: usage climbed past the budget,
|
||||
// DLSS refused to run - Streamline eWarnOutOfVRAM - then the device was lost): the limits
|
||||
// set at start-up ignore that the driver's budget shrinks while other programs hold video
|
||||
// memory, and textures were only freed after 16-160 frames unused, 10-40 per pass. Now the
|
||||
// limits follow the budget of the moment, and close to it ("emergency") textures unused for
|
||||
// half a second go, up to 200 per frame.
|
||||
u64 budget_now = 0;
|
||||
if (instance.CanReportMemoryUsage()) {
|
||||
total_used_memory = instance.GetDeviceMemoryUsage();
|
||||
const auto [usage, budget] = instance.GetDeviceMemoryUsageAndBudget();
|
||||
total_used_memory = usage;
|
||||
budget_now = budget;
|
||||
// issn 11-04 (GitHub issue 32, RTX 3060 Laptop 6 GB: usage 5395 MB over a 5091 MB budget,
|
||||
// DLSS refused, device lost 30 s in): ISSN_VRAM_LIMIT_MB (launcher: Video memory limit)
|
||||
// caps the budget the clean-up aims for, below what the driver allows.
|
||||
static const u64 user_limit = [] {
|
||||
const char* v = std::getenv("ISSN_VRAM_LIMIT_MB");
|
||||
const long mb = v ? std::atol(v) : 0;
|
||||
if (mb >= 2048) {
|
||||
std::printf("Video memory: the clean-up keeps the game under %ld MB (launcher)\n", mb);
|
||||
std::fflush(stdout);
|
||||
return u64(mb) << 20;
|
||||
}
|
||||
return u64(0);
|
||||
}();
|
||||
if (user_limit && (budget_now == 0 || user_limit < budget_now)) {
|
||||
budget_now = user_limit;
|
||||
}
|
||||
// A line a minute in the log: how close the game runs to the budget.
|
||||
static auto last_report = std::chrono::steady_clock::time_point{};
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now - last_report >= std::chrono::seconds(60)) {
|
||||
last_report = now;
|
||||
std::printf("Video memory: %llu of %llu MB the driver allows now\n",
|
||||
static_cast<unsigned long long>(usage >> 20),
|
||||
static_cast<unsigned long long>(budget >> 20));
|
||||
std::fflush(stdout);
|
||||
}
|
||||
}
|
||||
if (total_used_memory < trigger_gc_memory) {
|
||||
u64 pressure_limit = pressure_gc_memory;
|
||||
u64 critical_limit = critical_gc_memory;
|
||||
bool emergency = false;
|
||||
if (budget_now > 2_GB) {
|
||||
pressure_limit = std::min<u64>(pressure_limit, budget_now - 1_GB);
|
||||
critical_limit = std::min<u64>(critical_limit, budget_now - 512_MB);
|
||||
// Cards of 8 GB or less fill up in one loading burst (issue 32): a wider margin there.
|
||||
const u64 margin = budget_now <= 8_GB ? 768_MB : 384_MB;
|
||||
emergency = total_used_memory + margin >= budget_now;
|
||||
}
|
||||
if (emergency) {
|
||||
static auto last_told = std::chrono::steady_clock::time_point{};
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now - last_told >= std::chrono::seconds(30)) {
|
||||
last_told = now;
|
||||
std::printf("Video memory nearly full (%llu of %llu MB the driver allows now): "
|
||||
"freeing textures not used for half a second\n",
|
||||
static_cast<unsigned long long>(total_used_memory >> 20),
|
||||
static_cast<unsigned long long>(budget_now >> 20));
|
||||
std::fflush(stdout);
|
||||
}
|
||||
}
|
||||
if (total_used_memory < trigger_gc_memory && !emergency) {
|
||||
return;
|
||||
}
|
||||
// The walk tests every image's own use tick (review 10-20: it runs every frame where the
|
||||
// trigger is 0 - cards of <= 8 GB or no memory report): every 4th tick unless critical.
|
||||
if ((gc_tick & 3) != 0 && total_used_memory < critical_gc_memory) {
|
||||
if ((gc_tick & 3) != 0 && total_used_memory < critical_limit) {
|
||||
return;
|
||||
}
|
||||
CacheLock lock{*this};
|
||||
@@ -2127,11 +2235,11 @@ void TextureCache::GarbageCollectImages() {
|
||||
size_t num_deletions = 0;
|
||||
|
||||
const auto configure = [&](bool allow_aggressive) {
|
||||
pressured = total_used_memory >= pressure_gc_memory;
|
||||
aggresive = allow_aggressive && total_used_memory >= critical_gc_memory;
|
||||
ticks_to_destroy = aggresive ? 160 : pressured ? 80 : 16;
|
||||
pressured = emergency || total_used_memory >= pressure_limit;
|
||||
aggresive = allow_aggressive && total_used_memory >= critical_limit;
|
||||
ticks_to_destroy = emergency ? 30 : aggresive ? 160 : pressured ? 80 : 16;
|
||||
ticks_to_destroy = std::min(ticks_to_destroy, gc_tick);
|
||||
num_deletions = aggresive ? 40 : pressured ? 20 : 10;
|
||||
num_deletions = emergency ? 200 : aggresive ? 40 : pressured ? 20 : 10;
|
||||
};
|
||||
const auto clean_up = [&](ImageId image_id) {
|
||||
if (num_deletions == 0) {
|
||||
@@ -2152,13 +2260,16 @@ void TextureCache::GarbageCollectImages() {
|
||||
DownloadImageMemory(image_id);
|
||||
}
|
||||
FreeImage(image_id);
|
||||
if (total_used_memory < critical_gc_memory) {
|
||||
if (emergency) {
|
||||
return false; // the usage figure is the driver's: read again next frame
|
||||
}
|
||||
if (total_used_memory < critical_limit) {
|
||||
if (aggresive) {
|
||||
num_deletions >>= 2;
|
||||
aggresive = false;
|
||||
return false;
|
||||
}
|
||||
if (pressured && total_used_memory < pressure_gc_memory) {
|
||||
if (pressured && total_used_memory < pressure_limit) {
|
||||
num_deletions >>= 1;
|
||||
pressured = false;
|
||||
}
|
||||
@@ -2177,7 +2288,7 @@ void TextureCache::GarbageCollectImages() {
|
||||
configure(false);
|
||||
lru_cache.ForEachItem(old_enough);
|
||||
|
||||
if (total_used_memory >= critical_gc_memory) {
|
||||
if (!emergency && total_used_memory >= critical_limit) {
|
||||
// If we are still over the critical limit, run an aggressive GC
|
||||
configure(true);
|
||||
lru_cache.ForEachItem(old_enough);
|
||||
|
||||
@@ -683,6 +683,7 @@ private:
|
||||
// SHADPS4_BIND_MEMO_STATS: see StructGeneration and SamplerGeneration.
|
||||
std::atomic<u64> struct_generation{0};
|
||||
std::atomic<u64> sampler_generation{0};
|
||||
float samplers_aniso = -1.0f; ///< the forced anisotropy the samplers were made with (GetSampler)
|
||||
struct MetaDataInfo {
|
||||
enum class Type {
|
||||
CMask,
|
||||
|
||||
@@ -26,7 +26,8 @@ public:
|
||||
s32 GetGpuId() { static const auto value = s32(Number("ISSN_GPU_ID", -1)); return value; }
|
||||
u32 GetInternalScreenWidth() { static const auto value = u32(Number("ISSN_INTERNAL_WIDTH", 1920)); return value; }
|
||||
u32 GetInternalScreenHeight() { static const auto value = u32(Number("ISSN_INTERNAL_HEIGHT", 1080)); return value; }
|
||||
// issn-axy: ISSN_PRESENT_MODE wins; else the in-game menu's VSync (issn.ini, read at start):
|
||||
// issn-axy: ISSN_PRESENT_MODE wins; else the in-game menu's VSync (read live: the swapchain
|
||||
// checks it every 30 presents and is rebuilt when it changes):
|
||||
// on = Fifo (the display's rate), off = Immediate (Mailbox is missing on AMD's Windows driver,
|
||||
// and the core falls back from it to Fifo).
|
||||
std::string GetPresentMode() {
|
||||
@@ -40,14 +41,12 @@ public:
|
||||
// ISSN_VBLANK_HZ (> 0) wins; else the menu's frame rate limit (issn.ini fps_limit: 30, 60, 120,
|
||||
// 240; 0 = unlimited, 240 Hz as in axylpup's build). Second Son steps its simulation by the
|
||||
// frame time, so it keeps its speed above 60 (tester: 100 FPS in the city).
|
||||
// Read live: the vblank thread follows it (driver.cpp), so the menu's limit applies at once.
|
||||
u32 GetVblankFrequency() {
|
||||
static const u32 value = [] {
|
||||
const long hz = Number("ISSN_VBLANK_HZ", 0);
|
||||
if (hz > 0) return u32(std::max<long>(hz, 30));
|
||||
const int limit = IssnFpsLimit();
|
||||
return limit > 0 ? u32(std::max(limit, 30)) : 240u;
|
||||
}();
|
||||
return value;
|
||||
static const long forced = Number("ISSN_VBLANK_HZ", 0);
|
||||
if (forced > 0) return u32(std::max<long>(forced, 30));
|
||||
const int limit = IssnFpsLimit();
|
||||
return limit > 0 ? u32(std::max(limit, 30)) : 240u;
|
||||
}
|
||||
bool IsCopyGpuBuffers() { static const auto value = Flag("ISSN_COPY_GPU_BUFFERS", false); return value; }
|
||||
bool IsDirectMemoryAccessEnabled() { static const auto value = Flag("ISSN_DIRECT_MEMORY_ACCESS", false); return value; }
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// issn 11-04: the exposure meter's mailbox (src/exposure_hook.h). The game's thread writes it
|
||||
// through the cave; the menu reads it and can hold the value (dark-night diagnosis).
|
||||
#include "issn_exposure.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include "../../src/exposure_hook.h"
|
||||
#include "common/memory_patcher.h"
|
||||
|
||||
namespace {
|
||||
alignas(64) IssnExposureMailbox g_box{};
|
||||
std::atomic<bool> g_attached{false};
|
||||
|
||||
float Load(const float& f) {
|
||||
float v;
|
||||
std::memcpy(&v, const_cast<const float*>(&f), sizeof(v));
|
||||
return v;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
extern "C" void* issngpu_exposure_mailbox(void) {
|
||||
return &g_box;
|
||||
}
|
||||
|
||||
extern "C" void issngpu_exposure_attached(void) {
|
||||
g_attached = true;
|
||||
}
|
||||
|
||||
namespace IssnExposure {
|
||||
|
||||
Reading Read() {
|
||||
Reading r;
|
||||
r.attached = g_attached.load();
|
||||
const volatile IssnExposureMailbox& box = g_box;
|
||||
r.value = Load(const_cast<const IssnExposureMailbox&>(box).value);
|
||||
r.sum = Load(const_cast<const IssnExposureMailbox&>(box).sum);
|
||||
r.rows = box.rows;
|
||||
r.calls = box.calls;
|
||||
r.held = box.held != 0;
|
||||
return r;
|
||||
}
|
||||
|
||||
void Hold(bool on) {
|
||||
volatile IssnExposureMailbox& box = g_box;
|
||||
if (!on) {
|
||||
box.held = 0;
|
||||
std::printf("Exposure meter: following the scene again\n");
|
||||
} else {
|
||||
float v = Read().value;
|
||||
uint32_t bits;
|
||||
std::memcpy(&bits, &v, 4);
|
||||
box.held = bits ? bits : 1u;
|
||||
std::printf("Exposure meter: held at %g\n", v);
|
||||
}
|
||||
std::fflush(stdout);
|
||||
}
|
||||
|
||||
namespace {
|
||||
constexpr uintptr_t TodState = 0x7f8e800; // eboot .bss: current, fallback, wanted id
|
||||
std::atomic<uint64_t> g_last_good{0};
|
||||
|
||||
uint64_t Read64(uintptr_t address) {
|
||||
uint64_t v;
|
||||
std::memcpy(&v, reinterpret_cast<const void*>(address), 8);
|
||||
return v;
|
||||
}
|
||||
// A guest heap pointer: inside the 64-bit guest space the port maps (not null, aligned).
|
||||
bool Plausible(uint64_t p) {
|
||||
return p >= 0x10000 && p < (1ull << 47) && (p & 7) == 0;
|
||||
}
|
||||
bool PlausibleText(uint64_t p) {
|
||||
return p >= 0x10000 && p < (1ull << 47);
|
||||
}
|
||||
// A guest C string (printable ASCII only, cut at size-1).
|
||||
void CopyText(uint64_t p, char* out, size_t size) {
|
||||
out[0] = 0;
|
||||
if (!PlausibleText(p)) {
|
||||
return;
|
||||
}
|
||||
const char* s = reinterpret_cast<const char*>(p);
|
||||
size_t i = 0;
|
||||
for (; i + 1 < size && s[i]; ++i) {
|
||||
const unsigned char c = static_cast<unsigned char>(s[i]);
|
||||
out[i] = c >= 0x20 && c < 0x7f ? static_cast<char>(c) : '?';
|
||||
}
|
||||
out[i] = 0;
|
||||
}
|
||||
uint32_t Read32(uintptr_t address) {
|
||||
uint32_t v;
|
||||
std::memcpy(&v, reinterpret_cast<const void*>(address), 4);
|
||||
return v;
|
||||
}
|
||||
// The manager's table: (id, name, node) of each entry, up to max.
|
||||
struct TodNode {
|
||||
uint64_t id;
|
||||
uint64_t obj;
|
||||
};
|
||||
int WalkTable(uintptr_t state, TodNode* out, int max) {
|
||||
const uint32_t count = Read32(state + 0x30);
|
||||
const uint64_t buckets = Read64(state + 0x38);
|
||||
if (count == 0 || count > 65536 || !Plausible(buckets)) {
|
||||
return 0;
|
||||
}
|
||||
int n = 0;
|
||||
for (uint32_t b = 0; b < count && n < max; ++b) {
|
||||
uint64_t node = Read64(buckets + 8ull * b);
|
||||
for (int guard = 0; Plausible(node) && guard < 64 && n < max; ++guard) {
|
||||
out[n++] = {Read64(node + 8), Read64(node + 0x10)};
|
||||
node = Read64(node + 0x18);
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
void NameOf(uint64_t obj, char* out, size_t size) {
|
||||
out[0] = 0;
|
||||
if (!Plausible(obj)) {
|
||||
return;
|
||||
}
|
||||
const uint64_t data = Read64(obj + 0x28);
|
||||
if (Plausible(data)) {
|
||||
CopyText(Read64(data + 0x58), out, size);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TimeOfDay ReadTimeOfDay() {
|
||||
TimeOfDay t;
|
||||
const uintptr_t base = MemoryPatcher::g_eboot_address;
|
||||
if (base == 0) {
|
||||
return t;
|
||||
}
|
||||
t.known = true;
|
||||
const uintptr_t state = base + TodState;
|
||||
const uint64_t current = Read64(state), fallback = Read64(state + 8);
|
||||
t.wanted = Read64(state + 0x10);
|
||||
t.current = ~0ull;
|
||||
if (Plausible(current)) {
|
||||
const uint64_t data = Read64(current + 0x28);
|
||||
if (Plausible(data)) {
|
||||
t.current = Read64(data + 0x18);
|
||||
t.lighting = Read64(data + 0x68) != 0;
|
||||
}
|
||||
t.fallback = current == fallback;
|
||||
}
|
||||
if (t.lighting && !t.fallback && t.current != ~0ull) {
|
||||
g_last_good = t.current;
|
||||
}
|
||||
t.last_good = g_last_good.load();
|
||||
return t;
|
||||
}
|
||||
|
||||
bool ForceLastGoodTimeOfDay() {
|
||||
const TimeOfDay t = ReadTimeOfDay();
|
||||
if (!t.known || t.last_good == 0) {
|
||||
return false;
|
||||
}
|
||||
const uint64_t id = t.last_good;
|
||||
std::memcpy(reinterpret_cast<void*>(MemoryPatcher::g_eboot_address + TodState + 0x10), &id, 8);
|
||||
std::printf("Time of day: asked the game for %#llx (test)\n", static_cast<unsigned long long>(id));
|
||||
std::fflush(stdout);
|
||||
return true;
|
||||
}
|
||||
|
||||
TodDetail ReadTodDetail() {
|
||||
TodDetail d;
|
||||
const uintptr_t base = MemoryPatcher::g_eboot_address;
|
||||
if (base == 0) {
|
||||
return d;
|
||||
}
|
||||
d.known = true;
|
||||
const uintptr_t state = base + TodState;
|
||||
d.wanted = Read64(state + 0x10);
|
||||
TodNode nodes[64];
|
||||
const int n = WalkTable(state, nodes, 64);
|
||||
d.entries = static_cast<uint32_t>(n);
|
||||
size_t used = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
char name[40];
|
||||
NameOf(nodes[i].obj, name, sizeof(name));
|
||||
if (nodes[i].id == d.wanted) {
|
||||
d.in_table = true;
|
||||
std::snprintf(d.name, sizeof(d.name), "%s", name);
|
||||
}
|
||||
if (used + 1 < sizeof(d.list)) {
|
||||
const int w = std::snprintf(d.list + used, sizeof(d.list) - used, "%s%s", i ? ", " : "",
|
||||
name[0] ? name : "?");
|
||||
used += w > 0 ? static_cast<size_t>(w) : 0;
|
||||
}
|
||||
}
|
||||
if (!d.in_table || !d.name[0]) {
|
||||
return d;
|
||||
}
|
||||
// The section name and its hash (eboot 0x5ed7c0: h = h * 101 + tolower(c); "Nil" = none).
|
||||
char world[48];
|
||||
CopyText(Read64(base + 0x7f111e0 + 0x1c0a0) + 2, world, sizeof(world));
|
||||
std::snprintf(d.section, sizeof(d.section), "%s_x_%s", world, d.name);
|
||||
uint64_t h = 0;
|
||||
for (const char* c = d.section; *c; ++c) {
|
||||
const unsigned char u = static_cast<unsigned char>(*c);
|
||||
h = h * 101 + static_cast<uint64_t>(u >= 'A' && u <= 'Z' ? u + 32 : u);
|
||||
}
|
||||
// The world's section table (eboot 0x3c4e40): 997 buckets at +0x198, node {next, id, +0x10 index}.
|
||||
const uintptr_t table = base + 0x7d37610;
|
||||
const uint32_t key = static_cast<uint32_t>(h ^ (h >> 32)) % 997u;
|
||||
uint64_t node = Read64(table + 0x198 + 8ull * key);
|
||||
for (int guard = 0; Plausible(node) && guard < 256; ++guard) {
|
||||
if (Read64(node + 8) == h) {
|
||||
break;
|
||||
}
|
||||
node = Read64(node);
|
||||
}
|
||||
if (!Plausible(node) || Read64(node + 8) != h) {
|
||||
return d; // no such section: the game does not wait for one
|
||||
}
|
||||
int16_t raw;
|
||||
std::memcpy(&raw, reinterpret_cast<const void*>(node + 0x10), 2);
|
||||
d.section_index = raw >> 8;
|
||||
// eboot 0x3c4db0: ready unless the section object says it is still on its way.
|
||||
const uint32_t count = Read32(table + 0xd0);
|
||||
const uint64_t array = Read64(table + 0xc8);
|
||||
if (static_cast<uint32_t>(d.section_index) >= count || !Plausible(array)) {
|
||||
d.section_state = 1;
|
||||
return d;
|
||||
}
|
||||
const uint64_t sec = Read64(array + 8ull * static_cast<uint32_t>(d.section_index));
|
||||
if (!Plausible(sec)) {
|
||||
d.section_state = 3;
|
||||
return d;
|
||||
}
|
||||
std::memcpy(d.flags, reinterpret_cast<const void*>(sec + 0x44), 4);
|
||||
bool ready;
|
||||
if (d.flags[3]) {
|
||||
ready = true;
|
||||
} else if (d.flags[2]) {
|
||||
ready = false;
|
||||
} else if (Read64(table + 0xf8) == 0) {
|
||||
ready = true;
|
||||
} else {
|
||||
ready = d.flags[1] && d.flags[0];
|
||||
}
|
||||
d.section_state = ready ? 1 : 2;
|
||||
return d;
|
||||
}
|
||||
|
||||
bool ForceNextTimeOfDay() {
|
||||
const uintptr_t base = MemoryPatcher::g_eboot_address;
|
||||
if (base == 0) {
|
||||
return false;
|
||||
}
|
||||
const uintptr_t state = base + TodState;
|
||||
TodNode nodes[64];
|
||||
const int n = WalkTable(state, nodes, 64);
|
||||
if (n == 0) {
|
||||
return false;
|
||||
}
|
||||
const uint64_t wanted = Read64(state + 0x10);
|
||||
int next = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (nodes[i].id == wanted) {
|
||||
next = (i + 1) % n;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const uint64_t id = nodes[next].id;
|
||||
char name[40];
|
||||
NameOf(nodes[next].obj, name, sizeof(name));
|
||||
std::memcpy(reinterpret_cast<void*>(state + 0x10), &id, 8);
|
||||
std::printf("Time of day: asked the game for %#llx '%s' (test, %d of %d)\n",
|
||||
static_cast<unsigned long long>(id), name, next + 1, n);
|
||||
std::fflush(stdout);
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// issn 11-05 (3): the black-city fix. Tested by João on Lvcn's save (24%, "Drogas no Lixo"): the
|
||||
// game wants time_of_day_dismal, whose world section (world_seaport_x_time_of_day_dismal) never
|
||||
// finishes streaming in (flags 3 1 1 0: on its way, for ever), so the manager waits for ever and
|
||||
// no time of day is in use: no sky, no ambient light, black city. Asking for early_morning lit it
|
||||
// at once. Fix: when the wanted one has waited for its section with nothing in use for 30 s, ask
|
||||
// for another (and the next one when that one also hangs 15 s). ISSN_TOD_RESCUE=0 turns it off.
|
||||
constexpr const char* kRescueOrder[] = {
|
||||
"time_of_day_blustery", "time_of_day_early_morning", "time_of_day_hazy_sunset",
|
||||
"time_of_day_sunset_candy", "time_of_day_hdr_night",
|
||||
};
|
||||
struct RescueState {
|
||||
uint64_t stuck_id = 0;
|
||||
std::chrono::steady_clock::time_point since{};
|
||||
uint64_t asked[8] = {};
|
||||
int asked_count = 0;
|
||||
bool last_was_ours = false;
|
||||
char status[160] = "nothing to do";
|
||||
};
|
||||
RescueState g_rescue;
|
||||
std::atomic<int> g_rescues{0};
|
||||
|
||||
bool RescueEnabled() {
|
||||
static const bool on = [] {
|
||||
const char* e = std::getenv("ISSN_TOD_RESCUE");
|
||||
return !(e && e[0] == '0');
|
||||
}();
|
||||
return on;
|
||||
}
|
||||
|
||||
void RescueTick() {
|
||||
if (!RescueEnabled() || MemoryPatcher::g_eboot_address == 0) {
|
||||
return;
|
||||
}
|
||||
using Clock = std::chrono::steady_clock;
|
||||
static auto next = Clock::now();
|
||||
const auto now = Clock::now();
|
||||
if (now < next) {
|
||||
return;
|
||||
}
|
||||
next = now + std::chrono::seconds(1);
|
||||
auto& r = g_rescue;
|
||||
const TimeOfDay t = ReadTimeOfDay();
|
||||
const TodDetail d = ReadTodDetail();
|
||||
const bool stuck = d.in_table && d.section_state == 2 && t.current == ~0ull;
|
||||
if (!stuck) {
|
||||
if (r.stuck_id != 0 && t.lighting) {
|
||||
std::snprintf(r.status, sizeof(r.status), "%s", g_rescues.load() ? "fixed (see the log)" : "nothing to do");
|
||||
}
|
||||
r.stuck_id = 0;
|
||||
return;
|
||||
}
|
||||
if (d.wanted != r.stuck_id) {
|
||||
r.last_was_ours = false;
|
||||
for (int i = 0; i < r.asked_count; ++i) {
|
||||
r.last_was_ours = r.last_was_ours || r.asked[i] == d.wanted;
|
||||
}
|
||||
r.stuck_id = d.wanted;
|
||||
r.since = now;
|
||||
std::printf("Black-city fix: '%s' waits for its section '%s' and no time of day is in use\n",
|
||||
d.name, d.section);
|
||||
std::fflush(stdout);
|
||||
}
|
||||
const auto waited = std::chrono::duration_cast<std::chrono::seconds>(now - r.since).count();
|
||||
const long long limit = r.last_was_ours ? 15 : 30;
|
||||
if (waited < limit) {
|
||||
std::snprintf(r.status, sizeof(r.status), "'%s' stuck for %llds (acts at %llds)", d.name,
|
||||
static_cast<long long>(waited), limit);
|
||||
return;
|
||||
}
|
||||
// The first one of the list in the game's table, not tried yet this session.
|
||||
const uintptr_t state = MemoryPatcher::g_eboot_address + TodState;
|
||||
TodNode nodes[64];
|
||||
const int n = WalkTable(state, nodes, 64);
|
||||
for (const char* want : kRescueOrder) {
|
||||
for (int i = 0; i < n; ++i) {
|
||||
char name[40];
|
||||
NameOf(nodes[i].obj, name, sizeof(name));
|
||||
if (std::strcmp(name, want) != 0 || nodes[i].id == d.wanted) {
|
||||
continue;
|
||||
}
|
||||
bool tried = false;
|
||||
for (int k = 0; k < r.asked_count; ++k) {
|
||||
tried = tried || r.asked[k] == nodes[i].id;
|
||||
}
|
||||
if (tried) {
|
||||
continue;
|
||||
}
|
||||
const uint64_t id = nodes[i].id;
|
||||
std::memcpy(reinterpret_cast<void*>(state + 0x10), &id, 8);
|
||||
if (r.asked_count < 8) {
|
||||
r.asked[r.asked_count++] = id;
|
||||
}
|
||||
g_rescues.fetch_add(1);
|
||||
std::printf("Black-city fix: '%s' hung %llds waiting for its section; asked the game for '%s'\n",
|
||||
d.name, static_cast<long long>(waited), name);
|
||||
std::fflush(stdout);
|
||||
std::snprintf(r.status, sizeof(r.status), "'%s' hung: asked for '%s'", d.name, name);
|
||||
r.stuck_id = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
std::snprintf(r.status, sizeof(r.status), "'%s' stuck, no other time of day left to try", d.name);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
const char* RescueStatus() {
|
||||
return RescueEnabled() ? g_rescue.status : "off (ISSN_TOD_RESCUE=0)";
|
||||
}
|
||||
|
||||
void Tick() {
|
||||
RescueTick();
|
||||
if (!g_attached.load(std::memory_order_relaxed)) {
|
||||
return;
|
||||
}
|
||||
using Clock = std::chrono::steady_clock;
|
||||
static const auto start = Clock::now();
|
||||
static auto next = start + std::chrono::seconds(5);
|
||||
const auto now = Clock::now();
|
||||
if (now < next) {
|
||||
return;
|
||||
}
|
||||
const bool early = now - start < std::chrono::minutes(2);
|
||||
next = now + std::chrono::seconds(early ? 5 : 30);
|
||||
const Reading r = Read();
|
||||
const TimeOfDay t = ReadTimeOfDay();
|
||||
std::printf("Exposure meter: value %g, tile sum %g, %u tile rows, %u frames%s; time of day wanted "
|
||||
"%#llx, in use %#llx%s, lighting %s\n",
|
||||
r.value, r.sum, r.rows, r.calls, r.held ? " (held)" : "",
|
||||
static_cast<unsigned long long>(t.wanted), static_cast<unsigned long long>(t.current),
|
||||
t.fallback ? " (the game's fallback)" : "", t.lighting ? "loaded" : "NONE");
|
||||
// The reason, when it changes (and the table once).
|
||||
const TodDetail d = ReadTodDetail();
|
||||
static uint64_t last_wanted = 1;
|
||||
static int last_state = -1;
|
||||
static bool listed = false;
|
||||
if (d.known && (d.wanted != last_wanted || d.section_state != last_state || !listed)) {
|
||||
last_wanted = d.wanted;
|
||||
last_state = d.section_state;
|
||||
static const char* const states[] = {"no section to wait for", "section ready",
|
||||
"WAITING for the section (not streamed in)",
|
||||
"section object missing"};
|
||||
std::printf("Time of day detail: wanted %#llx %s '%s'; section '%s' index %d: %s (flags %u %u %u %u)\n",
|
||||
static_cast<unsigned long long>(d.wanted),
|
||||
d.in_table ? "is in the table as" : "is NOT in the table", d.name, d.section,
|
||||
d.section_index, states[d.section_state & 3], d.flags[0], d.flags[1], d.flags[2],
|
||||
d.flags[3]);
|
||||
if (!listed && d.entries) {
|
||||
std::printf("Time of day detail: the table has %u: %s\n", d.entries, d.list);
|
||||
listed = true;
|
||||
}
|
||||
}
|
||||
std::fflush(stdout);
|
||||
}
|
||||
|
||||
} // namespace IssnExposure
|
||||
@@ -0,0 +1,60 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// issn 11-04: the exposure meter's diagnostic hook (src/exposure_hook.h), for the menu.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace IssnExposure {
|
||||
|
||||
struct Reading {
|
||||
bool attached = false; ///< the probe put the cave in
|
||||
float value = 0.0f; ///< the metered value the game stored last
|
||||
float sum = 0.0f; ///< the raw sum of the tiles
|
||||
uint32_t rows = 0; ///< tile rows (render height / 72)
|
||||
uint32_t calls = 0; ///< frames metered so far
|
||||
bool held = false; ///< the menu holds the value
|
||||
};
|
||||
|
||||
Reading Read();
|
||||
/// On: the game keeps the value metered now; off: it meters again.
|
||||
void Hold(bool on);
|
||||
/// Called each presented frame: the black-city fix (once a second), and a log line now and then
|
||||
/// (every 5 s for 2 minutes, then every 30 s).
|
||||
void Tick();
|
||||
|
||||
/// issn 11-05: the game's time-of-day (atmosphere) state, eboot .bss 0x7f8e800 (manager at
|
||||
/// 0x59cf80): the dark saves never load a time of day's lighting files (sh-, skydome, imposter-).
|
||||
struct TimeOfDay {
|
||||
bool known = false; ///< the eboot address is set
|
||||
uint64_t wanted = 0; ///< the id the game asks for (+0x10; ~0 = none)
|
||||
uint64_t current = 0; ///< the id of the one in use (~0 = none)
|
||||
bool fallback = false; ///< in use is the game's "ensured time of day" (no files of its own)
|
||||
bool lighting = false; ///< the one in use has lighting data (what loads sh-/imposter-)
|
||||
uint64_t last_good = 0; ///< the last id seen with lighting data this session (0 = none)
|
||||
};
|
||||
TimeOfDay ReadTimeOfDay();
|
||||
/// Test: ask the game for the last good time of day (false when there is none).
|
||||
bool ForceLastGoodTimeOfDay();
|
||||
|
||||
/// issn 11-05 (2): why the wanted one is not used. The manager looks the wanted id up in its table
|
||||
/// (+0x30 count, +0x38 buckets); found, it waits until the world section "<world>_x_<name>" is
|
||||
/// streamed in (eboot 0x3c4e40/0x3c4db0 on the table at .bss 0x7d37610); not found, it falls back.
|
||||
struct TodDetail {
|
||||
bool known = false;
|
||||
uint64_t wanted = 0;
|
||||
uint32_t entries = 0; ///< times of day in the manager's table
|
||||
bool in_table = false; ///< the wanted id is one of them
|
||||
char name[48] = {}; ///< its name (atmosphere)
|
||||
char section[112] = {}; ///< the world section the game waits for
|
||||
int section_index = -1; ///< -1: no such section (the game does not wait)
|
||||
int section_state = 0; ///< 0 no section, 1 ready, 2 waiting (not streamed in), 3 no object
|
||||
uint8_t flags[4] = {}; ///< the section's bytes +0x44..+0x47
|
||||
char list[400] = {}; ///< "id name, ..." of the table (for the log)
|
||||
};
|
||||
TodDetail ReadTodDetail();
|
||||
/// Test: ask the game for the next time of day in its table (cycles; false when the table is empty).
|
||||
bool ForceNextTimeOfDay();
|
||||
/// The black-city fix (in Tick, on by default; ISSN_TOD_RESCUE=0 off): what it is doing now.
|
||||
const char* RescueStatus();
|
||||
|
||||
} // namespace IssnExposure
|
||||
+192
-16
@@ -19,10 +19,17 @@
|
||||
#include "common/logging/log.h"
|
||||
#include "common/memory_patcher.h"
|
||||
#include "common/types.h"
|
||||
#include "video_core/renderer_vulkan/vk_dlss.h"
|
||||
|
||||
namespace VideoCore {
|
||||
void PS4_SYSV_ABI ParticleArenaDiscardHook(); // buffer_cache.cpp
|
||||
}
|
||||
// issn_pc: the screen shape (issngpu.cpp: window size; MemoryPatcher::IsRenderShaped / MovieFitRect).
|
||||
namespace IssnShape {
|
||||
bool ScreenPixels(int& width, int& height);
|
||||
extern double g_aspect;
|
||||
extern float g_movie_x, g_movie_y;
|
||||
} // namespace IssnShape
|
||||
|
||||
namespace {
|
||||
using MemoryPatcher::g_eboot_address;
|
||||
@@ -255,7 +262,11 @@ static void ApplyLightListCapacity(u32 width, u32 height) {
|
||||
std::memcpy(&mov_imm, at(0x16be1f), 4);
|
||||
if (std::memcmp(at(0x16be04), imul_head, 3) != 0 || std::memcmp(at(0x16be1d), mov_head, 2) != 0 ||
|
||||
imul_imm != Original || mov_imm != Original) {
|
||||
std::printf("Render size: light list not enlarged (the code differs in this eboot.bin)\n");
|
||||
if (imul_imm == mov_imm && imul_imm >= entries) {
|
||||
std::printf("Render size: light list already %u entries (patch)\n", imul_imm);
|
||||
} else {
|
||||
std::printf("Render size: light list not enlarged (the code differs in this eboot.bin)\n");
|
||||
}
|
||||
return;
|
||||
}
|
||||
std::memcpy(at(0x16be07), &entries, 4);
|
||||
@@ -263,30 +274,171 @@ static void ApplyLightListCapacity(u32 width, u32 height) {
|
||||
std::printf("Render size: light list %u entries for %ux%u\n", entries, width, height);
|
||||
}
|
||||
|
||||
static void ApplyRenderSize() {
|
||||
u32 height = 0;
|
||||
if (const char* v = std::getenv("ISSN_RENDER_HEIGHT"); v && std::atoi(v) > 0) {
|
||||
height = static_cast<u32>(std::atoi(v));
|
||||
} else if (const char* p = std::getenv("ISSN_RENDER_SCALE"); p && std::atof(p) > 0.0) {
|
||||
height = static_cast<u32>(std::lround(1080.0 * std::atof(p) / 100.0));
|
||||
}
|
||||
if (height == 0) {
|
||||
// issn 11-04: the exposure meter (CPU sum at eboot 0x171610) reads one float per 128x72-pixel tile
|
||||
// from buffers made for 625 tiles (2500 bytes, eboot 0x171310 and 0x172320): enough up to 1800p.
|
||||
// At 4K (900 tiles) it summed bytes past them, and the picture went dark and back (tester logs,
|
||||
// GitHub issue 15). The patch "Bigger light list" enlarges them too; without it, this does.
|
||||
static void ApplyExposureCapacity(u32 width, u32 height) {
|
||||
const u64 tiles = u64((width + 127) / 128) * ((height + 71) / 72);
|
||||
if (tiles <= 625) {
|
||||
return;
|
||||
}
|
||||
height = std::clamp<u32>(Round8(height), 360, 2160);
|
||||
const u32 width = Round8(height * 16.0 / 9.0);
|
||||
static constexpr u64 sizes[4] = {0x17132b, 0x171375, 0x172341, 0x172388}; // mov esi, 0x9c4
|
||||
static constexpr u64 counts[4] = {0x17134f, 0x171399, 0x172364, 0x1723ab}; // mov ecx, 0x271
|
||||
const auto at = [](u64 offset) { return reinterpret_cast<u8*>(g_eboot_address + offset); };
|
||||
bool original = true, patched = true;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
u32 size = 0, count = 0;
|
||||
std::memcpy(&size, at(sizes[i]), 4);
|
||||
std::memcpy(&count, at(counts[i]), 4);
|
||||
original = original && at(sizes[i])[-1] == 0xbe && at(counts[i])[-1] == 0xb9 && size == 0x9c4 &&
|
||||
count == 0x271;
|
||||
patched = patched && size >= tiles * 4 && count >= tiles;
|
||||
}
|
||||
if (patched) {
|
||||
std::printf("Exposure meter: room for %llu tiles (patch)\n", static_cast<unsigned long long>(tiles));
|
||||
return;
|
||||
}
|
||||
if (!original) {
|
||||
std::printf("Exposure meter: not enlarged (the code differs in this eboot.bin): the picture may go dark\n");
|
||||
return;
|
||||
}
|
||||
const u32 count = 2048, size = count * 4;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
std::memcpy(at(sizes[i]), &size, 4);
|
||||
std::memcpy(at(counts[i]), &count, 4);
|
||||
}
|
||||
std::printf("Exposure meter: %u tiles for %ux%u\n", count, width, height);
|
||||
}
|
||||
|
||||
/// The render size at 16:9-basis height h0 in the shape `aspect` (axylpup's SizeAt): wide shapes
|
||||
/// keep the height (up to 32:9), narrow ones keep the 16:9 width (down to 1:1).
|
||||
static void SizeAt(u32 h0, double aspect, u32& width, u32& height) {
|
||||
if (aspect < 16.0 / 9.0 - 1e-6) {
|
||||
width = Round8(h0 * 16.0 / 9.0);
|
||||
height = std::min(width, Round8(width / aspect));
|
||||
} else {
|
||||
width = std::min(Round8(h0 * 32.0 / 9.0), Round8(h0 * aspect));
|
||||
height = h0;
|
||||
}
|
||||
}
|
||||
|
||||
// Movies fit inside the screen (axylpup 1.1.1): the full-screen movie draw FUN_8004575b0 samples
|
||||
// the movie by f = (ccc4 * 16) / (ccc0 * 9) on a quad at (-1, -1, 1, 1); on another shape f != 1
|
||||
// cut the movie (21:9: 12% lost above and below). 16 / 9 become the screen space's size (f = 1)
|
||||
// and the quad takes the movie's shape inside the screen (bars, never stretched); the rasterizer
|
||||
// clears the target first (MemoryPatcher::MovieFitRect).
|
||||
static void ApplyMovieFit(u32 sw, u32 sh) {
|
||||
const double f = double(sh) * 16.0 / (double(sw) * 9.0);
|
||||
if (std::abs(f - 1.0) < 0.01) {
|
||||
return;
|
||||
}
|
||||
static constexpr float k_orig[2] = {16.0f, 9.0f};
|
||||
static constexpr float rect_orig[4] = {-1.0f, -1.0f, 1.0f, 1.0f};
|
||||
auto* k = reinterpret_cast<float*>(g_eboot_address + 0xaadbe0);
|
||||
auto* rect = reinterpret_cast<float*>(g_eboot_address + 0xaadd60);
|
||||
if (std::memcmp(k, k_orig, sizeof(k_orig)) != 0 || std::memcmp(rect, rect_orig, sizeof(rect_orig)) != 0) {
|
||||
std::printf("Screen shape: movies keep the 16:9 draw (constants differ in this eboot)\n");
|
||||
return;
|
||||
}
|
||||
const float x = f < 1.0 ? float(f) : 1.0f;
|
||||
const float y = f < 1.0 ? 1.0f : float(1.0 / f);
|
||||
const float k_new[2] = {float(sw), float(sh)};
|
||||
const float rect_new[4] = {-x, -y, x, y};
|
||||
std::memcpy(k, k_new, sizeof(k_new));
|
||||
std::memcpy(rect, rect_new, sizeof(rect_new));
|
||||
IssnShape::g_movie_x = x;
|
||||
IssnShape::g_movie_y = y;
|
||||
}
|
||||
|
||||
// Render size and screen shape (axylpup 1.1 render height + SHADPS4_ASPECT, src/common/
|
||||
// memory_patcher.cpp ApplyAspectRatio), decided at boot:
|
||||
// - ISSN_RENDER_HEIGHT / ISSN_RENDER_SCALE (launcher: Internal resolution): the lines the game
|
||||
// renders (360 - 4320, 1080 = the game's own; above 2160 experimental).
|
||||
// - ISSN_ASPECT (launcher: Screen shape): auto (default) = the game window's shape (fullscreen:
|
||||
// the monitor's), off = 16:9, or W:H (21:9, 32:9, 16:10, 4:3). Wider screens see more at the
|
||||
// sides (the vertical view stays), narrower ones keep the 16:9 width and see more above and
|
||||
// below (the projection's 2.0 becomes 2 / k); the HUD anchors to the new edges in a 2D screen
|
||||
// space kept at the 1080-line scale; movies keep their shape with bars.
|
||||
// The renderer's size setup FUN_8001eff00 (render + output size) and the mode toggle FUN_8001f0be0
|
||||
// load 1920 / 1080 immediates; the 2D screen space (HUD, camera aspect) is 0x1f0403 / 0x1f03fb.
|
||||
static void ApplyRenderSize() {
|
||||
constexpr double base = 16.0 / 9.0;
|
||||
u32 h0 = 1080;
|
||||
if (const char* v = std::getenv("ISSN_RENDER_HEIGHT"); v && std::atoi(v) > 0) {
|
||||
h0 = static_cast<u32>(std::atoi(v));
|
||||
} else if (const char* p = std::getenv("ISSN_RENDER_SCALE"); p && std::atof(p) > 0.0) {
|
||||
h0 = static_cast<u32>(std::lround(1080.0 * std::atof(p) / 100.0));
|
||||
}
|
||||
const char* aspect_env = std::getenv("ISSN_ASPECT");
|
||||
const std::string mode = aspect_env && *aspect_env ? aspect_env : "auto";
|
||||
double aspect = base;
|
||||
int screen_w = 0, screen_h = 0;
|
||||
const bool have_screen = IssnShape::ScreenPixels(screen_w, screen_h) && screen_h > 0 && screen_w > 0;
|
||||
if (const auto colon = mode.find(':'); colon != std::string::npos) {
|
||||
const double w = std::atof(mode.substr(0, colon).c_str());
|
||||
const double h = std::atof(mode.substr(colon + 1).c_str());
|
||||
aspect = h > 0.0 && w > 0.0 ? w / h : base;
|
||||
} else if (mode == "auto" && have_screen) {
|
||||
aspect = double(screen_w) / double(screen_h);
|
||||
}
|
||||
// ISSN_UPSCALE_FACTOR (launcher: FSR 4 / DLSS quality): the window's 16:9-basis height divided
|
||||
// by it (Quality 1.5 ... Ultra Performance 3); the upscaler rebuilds the window size.
|
||||
double factor = 0.0;
|
||||
if (const char* f = std::getenv("ISSN_UPSCALE_FACTOR"); f && std::atof(f) >= 1.0) {
|
||||
factor = std::atof(f);
|
||||
const double basis = !have_screen ? 1080.0
|
||||
: double(screen_w) / double(screen_h) >= base ? double(screen_h)
|
||||
: double(screen_w) / base;
|
||||
h0 = static_cast<u32>(std::lround(basis / factor));
|
||||
}
|
||||
h0 = std::clamp<u32>(Round8(h0), 360, 4320); // issn 10-28: 5K and 8K (testers' requests)
|
||||
// 3%: a "1280x720" window reports a client area like 1279x724 (axylpup's measurement).
|
||||
if (std::abs(aspect - base) < 0.03 * base) {
|
||||
aspect = base;
|
||||
}
|
||||
aspect = std::clamp(aspect, 1.0, 32.0 / 9.0);
|
||||
u32 width, height;
|
||||
SizeAt(h0, aspect, width, height);
|
||||
// At most 8K's pixels in any shape. axylpup capped this at 4K's (larger lost the device) before
|
||||
// the light list grew with the size (ApplyLightListCapacity, below); 5K / 8K are the
|
||||
// launcher's "experimental" sizes.
|
||||
while (u64(width) * height > 7680ull * 4320ull && h0 > 360) {
|
||||
h0 -= 8;
|
||||
SizeAt(h0, aspect, width, height);
|
||||
}
|
||||
if (width == 1920 && height == 1080) {
|
||||
return;
|
||||
}
|
||||
const bool narrow = aspect < base - 1e-6;
|
||||
// Narrow shapes: the projection builder 0x800581c90 computes P00 / P11 from one load of 2.0
|
||||
// (0x581d10); it is pointed at 2 / k in the 4 padding bytes before the function (0x581c84).
|
||||
static constexpr u8 two_load[8] = {0xc5, 0xfa, 0x10, 0x1d, 0x98, 0x27, 0x56, 0x00};
|
||||
static constexpr u8 pad[4] = {0x90, 0x90, 0x90, 0x90};
|
||||
if (narrow && (std::memcmp(reinterpret_cast<const void*>(g_eboot_address + 0x581d10), two_load, 8) != 0 ||
|
||||
std::memcmp(reinterpret_cast<const void*>(g_eboot_address + 0x581c84), pad, 4) != 0)) {
|
||||
std::printf("Screen shape: %.3f refused (the projection code differs in this eboot); 16:9\n", aspect);
|
||||
aspect = base;
|
||||
SizeAt(h0, aspect, width, height);
|
||||
if (width == 1920 && height == 1080) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const bool shaped = aspect != base;
|
||||
// The HUD space stays at the 1080-line scale (axylpup s1440: at 1280x720 some HUD parts sized
|
||||
// in 1080p pixels broke); 16:9 keeps 1920x1080.
|
||||
const u32 sw = u32(std::lround(width * 1080.0 / h0)), sh = u32(std::lround(height * 1080.0 / h0));
|
||||
struct Imm {
|
||||
u64 offset;
|
||||
u32 original;
|
||||
bool width;
|
||||
bool screen_space;
|
||||
};
|
||||
static constexpr Imm imms[] = {
|
||||
{0x1eff4a, 0x780, true}, {0x1eff51, 0x438, false}, // render size
|
||||
{0x1eff57, 0x780, true}, {0x1eff5e, 0x438, false}, // output size
|
||||
{0x1f0d3e, 0x780, true}, {0x1f0d4d, 0x438, false}, // mode toggle
|
||||
{0x1eff4a, 0x780, true, false}, {0x1eff51, 0x438, false, false}, // render size
|
||||
{0x1eff57, 0x780, true, false}, {0x1eff5e, 0x438, false, false}, // output size
|
||||
{0x1f0403, 0x780, true, true}, {0x1f03fb, 0x438, false, true}, // 2D screen space
|
||||
{0x1f0d3e, 0x780, true, false}, {0x1f0d4d, 0x438, false, false}, // mode toggle
|
||||
};
|
||||
for (const auto& imm : imms) {
|
||||
u32 live = 0;
|
||||
@@ -298,11 +450,35 @@ static void ApplyRenderSize() {
|
||||
}
|
||||
}
|
||||
for (const auto& imm : imms) {
|
||||
const u32 value = imm.width ? width : height;
|
||||
const u32 value = imm.screen_space ? (imm.width ? sw : sh) : (imm.width ? width : height);
|
||||
std::memcpy(reinterpret_cast<void*>(g_eboot_address + imm.offset), &value, 4);
|
||||
}
|
||||
ApplyLightListCapacity(width, height);
|
||||
std::printf("Render size: %ux%u (%.0f%% of 1080 lines)\n", width, height, height * 100.0 / 1080.0);
|
||||
ApplyExposureCapacity(width, height);
|
||||
if (narrow) {
|
||||
const float two_over_k = float(2.0 / (base / aspect));
|
||||
std::memcpy(reinterpret_cast<void*>(g_eboot_address + 0x581c84), &two_over_k, 4);
|
||||
const s32 disp = s32(0x581c84 - (0x581d10 + 8)); // rip-relative from the next instruction
|
||||
std::memcpy(reinterpret_cast<void*>(g_eboot_address + 0x581d10 + 4), &disp, 4);
|
||||
}
|
||||
IssnShape::g_aspect = aspect;
|
||||
// issn: the upscaler learns the game's size from its first resolve; told now, FSR 4 / DLSS
|
||||
// start at the right size and are not created twice. Made again after the first frame,
|
||||
// FSR 4 crashed inside the AMD driver (Lucas 10-27: 67% in a 1920x1078 window).
|
||||
Vulkan::Dlss::g_game_width.store(width, std::memory_order_relaxed);
|
||||
Vulkan::Dlss::g_game_height.store(height, std::memory_order_relaxed);
|
||||
if (shaped) {
|
||||
ApplyMovieFit(sw, sh);
|
||||
}
|
||||
if (factor > 0.0) {
|
||||
std::printf("Render size: the window %dx%d / %.1f (FSR 4 / DLSS quality)\n", screen_w, screen_h, factor);
|
||||
}
|
||||
std::printf("Render size: %ux%u (%.0f%% of 1080 lines)%s\n", width, height, h0 * 100.0 / 1080.0,
|
||||
shaped ? fmt::format(", screen shape {:.3f} ({}; window {}x{}), HUD space {}x{}{}", aspect,
|
||||
mode, screen_w, screen_h, sw, sh,
|
||||
narrow ? ", 16:9 width kept, more picture above and below" : "")
|
||||
.c_str()
|
||||
: "");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// issn-axy: Delsin's cheats from the in-game menu (src/hero_hook.h): infinite health (with the
|
||||
// probe's SetHitPoints stub), infinite powers and the Karma Bomb meter always full. The probe
|
||||
// verifies the 01.07 eboot, installs the stub and calls issn_hero_attach; a thread then follows
|
||||
// the menu's switches and refills the meters of the hero the game currently has.
|
||||
// 11-05: infinite health also stops the hits themselves (no gray screen), infinite missiles, and
|
||||
// the Karma Bomb is the karmic streak (the old switch filled the shield).
|
||||
// 11-04 (src/dash_hook.h): the neon run and the video flight while the dash button is held (two
|
||||
// probe stubs read the flags below), and a long smoke dash (the hero's MAX_SMOKE_DASH_RATIO).
|
||||
#include "issn_settings.h"
|
||||
|
||||
#include "../../src/hero_hook.h"
|
||||
#include "../../src/dash_hook.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <thread>
|
||||
|
||||
namespace {
|
||||
|
||||
unsigned char* image = nullptr;
|
||||
// Read by the SetHitPoints stub: 1 while infinite health is on.
|
||||
volatile unsigned char health_flag = 0;
|
||||
// Read by the dash stubs (src/dash_hook.h): 1 while the switch is on.
|
||||
volatile unsigned char neon_flag = 0, tv_flag = 0;
|
||||
|
||||
// The long smoke dash: the game's ratio (kept while the switch is on) times SmokeFactor. The
|
||||
// value is the game's again when the switch goes off, unless the game wrote another meanwhile.
|
||||
constexpr float SmokeFactor = 3.0f;
|
||||
unsigned char* smoke_hero = nullptr;
|
||||
float smoke_game = 1.0f, smoke_written = 0.0f;
|
||||
|
||||
// A pointer the game keeps, read as it is now (null when there is no hero: menus, loading).
|
||||
unsigned char* Pointer(const unsigned char* at) {
|
||||
std::uintptr_t value;
|
||||
std::memcpy(&value, at, sizeof(value));
|
||||
// Guest objects live in the guest's own address space (above 4 GB, below 2^47).
|
||||
return value >= (1ull << 32) && value < (1ull << 47) ? reinterpret_cast<unsigned char*>(value) : nullptr;
|
||||
}
|
||||
|
||||
float Get(const unsigned char* object, std::uint32_t offset) {
|
||||
float v;
|
||||
std::memcpy(&v, object + offset, 4);
|
||||
return v;
|
||||
}
|
||||
|
||||
void Fill(unsigned char* object, std::uint32_t current, std::uint32_t maximum) {
|
||||
const float top = Get(object, maximum);
|
||||
if (top > 0.0f && top < 1.0e7f && Get(object, current) < top) {
|
||||
std::memcpy(object + current, &top, 4);
|
||||
}
|
||||
}
|
||||
|
||||
void SmokeDash(unsigned char* hero, bool on) {
|
||||
if (!on || hero != smoke_hero) {
|
||||
// Off, or another hero object (a new one after loading): give the old one its value back.
|
||||
if (smoke_hero && hero == smoke_hero && Get(hero, ISSN_DASH_SMOKE_RATIO) == smoke_written) {
|
||||
std::memcpy(hero + ISSN_DASH_SMOKE_RATIO, &smoke_game, 4);
|
||||
}
|
||||
smoke_hero = nullptr;
|
||||
if (!on || !hero) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!hero) {
|
||||
return; // no hero yet (boot, loading): with the switch on and nothing remembered, the
|
||||
// check above let a null hero through (crash at start, 11-04)
|
||||
}
|
||||
const float now = Get(hero, ISSN_DASH_SMOKE_RATIO);
|
||||
if (!(now > 0.0f && now < 100.0f)) {
|
||||
return; // not a ratio: leave it alone
|
||||
}
|
||||
if (hero != smoke_hero || now != smoke_written) {
|
||||
smoke_hero = hero; // first tick on, or the game set a new ratio (an upgrade)
|
||||
smoke_game = now;
|
||||
smoke_written = now * SmokeFactor;
|
||||
std::memcpy(hero + ISSN_DASH_SMOKE_RATIO, &smoke_written, 4);
|
||||
}
|
||||
}
|
||||
|
||||
std::int32_t GetInt(const unsigned char* object, std::uint32_t offset) {
|
||||
std::int32_t v;
|
||||
std::memcpy(&v, object + offset, 4);
|
||||
return v;
|
||||
}
|
||||
|
||||
void SetInt(unsigned char* object, std::uint32_t offset, std::int32_t v) {
|
||||
std::memcpy(object + offset, &v, 4);
|
||||
}
|
||||
|
||||
// The missiles' ammo (src/hero_hook.h): full, with the change counted as the game's own setter
|
||||
// (0x1e35a0) counts it, so the HUD and the save see it.
|
||||
void FillMissiles(unsigned char* hero) {
|
||||
const std::int32_t top = GetInt(hero, ISSN_HERO_HEAVY_MAX);
|
||||
unsigned char* player = image + ISSN_HERO_PLAYER;
|
||||
if (top <= 0 || top > 1000 || GetInt(player, ISSN_HERO_HEAVY) >= top) {
|
||||
return;
|
||||
}
|
||||
SetInt(player, ISSN_HERO_HEAVY, top);
|
||||
if (player[0x20ac9]) {
|
||||
SetInt(player, 0x20ad0, GetInt(player, 0x20ad0) + 1);
|
||||
if (GetInt(player, 0x20ad4) == std::int32_t(0xffffc7c0)) {
|
||||
SetInt(player, 0x20ad4, GetInt(image + ISSN_HERO_CLOCK, 0x24));
|
||||
}
|
||||
SetInt(player, 0x20acc, GetInt(player, 0x20acc) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// The Karma Bomb (src/hero_hook.h): the karmic streak at what this side needs, ready with no time
|
||||
// limit. Using the bomb clears it; the next tick (50 ms) fills it again.
|
||||
void FillKarma(unsigned char* hero) {
|
||||
const std::int32_t side = GetInt(image + ISSN_HERO_PLAYER, ISSN_HERO_PLAYER_SIDE);
|
||||
if (side < 0 || side > 1) {
|
||||
return;
|
||||
}
|
||||
unsigned char* streak = hero + ISSN_HERO_STREAK;
|
||||
const std::int32_t need =
|
||||
GetInt(streak, ISSN_HERO_STREAK_REQUIRED + std::uint32_t(side) * ISSN_HERO_STREAK_SIDE_STRIDE);
|
||||
if (need <= 0 || need > 1000) {
|
||||
return; // no Karma Bomb yet (early story) or not a count
|
||||
}
|
||||
if (GetInt(streak, ISSN_HERO_STREAK_COUNT) < need) {
|
||||
SetInt(streak, ISSN_HERO_STREAK_COUNT, need);
|
||||
}
|
||||
if (GetInt(streak, ISSN_HERO_STREAK_READY_UNTIL) != 0x7fffffff) {
|
||||
SetInt(streak, ISSN_HERO_STREAK_READY_UNTIL, 0x7fffffff);
|
||||
}
|
||||
}
|
||||
|
||||
void Tick() {
|
||||
const auto& s = IssnSettings::Get();
|
||||
health_flag = s.hero_health.load() ? 1 : 0;
|
||||
neon_flag = s.hero_neon_run.load() ? 1 : 0;
|
||||
tv_flag = s.hero_video_flight.load() ? 1 : 0;
|
||||
const bool powers = s.hero_powers.load();
|
||||
const bool karma = s.hero_karma.load() && s.hero_karma_supported.load();
|
||||
const bool missiles = s.hero_missiles.load() && s.hero_missiles_supported.load();
|
||||
const bool smoke = s.hero_smoke_dash.load();
|
||||
if (!health_flag && !powers && !karma && !missiles && !smoke && !smoke_hero) {
|
||||
return;
|
||||
}
|
||||
unsigned char* hero = Pointer(image + ISSN_HERO_GLOBAL);
|
||||
SmokeDash(hero, smoke);
|
||||
if (!hero) {
|
||||
return;
|
||||
}
|
||||
if (health_flag) {
|
||||
// Hit points lost before the switch went on come back (the stub keeps them from then).
|
||||
if (unsigned char* character = Pointer(hero + ISSN_HERO_CHARACTER)) {
|
||||
Fill(character, ISSN_HERO_HP, ISSN_HERO_HP_MAX);
|
||||
}
|
||||
}
|
||||
if (powers) {
|
||||
Fill(hero, ISSN_HERO_MAGIC, ISSN_HERO_MAGIC_MAX);
|
||||
}
|
||||
if (missiles) {
|
||||
FillMissiles(hero);
|
||||
}
|
||||
if (karma) {
|
||||
FillKarma(hero);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" const volatile unsigned char* issn_hero_health_flag(void) {
|
||||
return &health_flag;
|
||||
}
|
||||
|
||||
extern "C" const volatile unsigned char* issn_hero_neon_flag(void) {
|
||||
return &neon_flag;
|
||||
}
|
||||
|
||||
extern "C" const volatile unsigned char* issn_hero_tv_flag(void) {
|
||||
return &tv_flag;
|
||||
}
|
||||
|
||||
extern "C" void issn_hero_dash_supported(int neon, int tv) {
|
||||
IssnSettings::Get().hero_neon_supported = neon != 0;
|
||||
IssnSettings::Get().hero_video_supported = tv != 0;
|
||||
}
|
||||
|
||||
extern "C" void issn_hero_extra_supported(int missiles, int karma, int no_hit) {
|
||||
IssnSettings::Get().hero_missiles_supported = missiles != 0;
|
||||
IssnSettings::Get().hero_karma_supported = karma != 0;
|
||||
IssnSettings::Get().hero_no_hit_supported = no_hit != 0;
|
||||
}
|
||||
|
||||
extern "C" void issn_hero_attach(unsigned char* guest_image) {
|
||||
image = guest_image;
|
||||
IssnSettings::Get().hero_supported = true;
|
||||
std::printf("Cheats: health, powers and Karma Bomb ready (Insert / R3+L2 > Game)\n");
|
||||
std::thread([] {
|
||||
for (;;) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
Tick();
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// issn-axy: the in-game menu's live picture choices handed to the renderer (issn_settings.h
|
||||
// ApplyLiveChoices). Apart from issn_settings.cpp, whose tests build without the video core.
|
||||
#include "issn_settings.h"
|
||||
#include "video_core/renderer_vulkan/issn_framegen.h"
|
||||
#include "video_core/renderer_vulkan/issn_postfx.h"
|
||||
#include "video_core/texture_cache/sampler.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace IssnSettings {
|
||||
|
||||
void ApplyLiveChoices() {
|
||||
auto& v = Get();
|
||||
static constexpr float levels[] = {0.0f, 4.0f, 8.0f, 16.0f};
|
||||
VideoCore::SetForcedAniso(levels[std::clamp(v.menu_aniso.load(), 0, 3)]);
|
||||
if (Vulkan::FrameGen::Wanted()) {
|
||||
Vulkan::FrameGen::SetEnabled(v.menu_frame_gen.load() > 0);
|
||||
}
|
||||
// issn 11-04: the game's motion blur and chromatic aberration (issn_postfx.h).
|
||||
Vulkan::PostFx::g_off[Vulkan::PostFx::MotionBlur].store(!v.motion_blur.load(), std::memory_order_relaxed);
|
||||
Vulkan::PostFx::g_off[Vulkan::PostFx::Chromatic].store(!v.chromatic_aberration.load(),
|
||||
std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
} // namespace IssnSettings
|
||||
+946
-60
File diff suppressed because it is too large.
Load diff
@@ -39,6 +39,19 @@ bool Visible();
|
||||
/// Present thread: draws into `view` (layout ColorAttachmentOptimal).
|
||||
void Render(vk::CommandBuffer cmdbuf, vk::ImageView view, vk::Extent2D extent);
|
||||
|
||||
/// The driver's Flip: a new game frame goes to the screen. The FPS readout counts these, not the
|
||||
/// presents: with frame pacing (SHADPS4_VSYNC_DIVIDER) each frame is presented 2-4 times, and
|
||||
/// counting presents showed the screen's refresh rate (144 on a 144 Hz screen at 72 FPS).
|
||||
void NoteGameFrame();
|
||||
|
||||
/// The game finished writing a save: "Game saved" and `what` (the save's subtitle) in a corner
|
||||
/// for a few seconds (settings: save_note).
|
||||
void NoteGameSaved(const std::string& what);
|
||||
|
||||
/// issn 11-04: a trophy was unlocked: its icon and name in a corner for a few seconds, one after
|
||||
/// another when several come together (settings: trophy_note). `name` empty: "Trophy N".
|
||||
void NoteTrophy(int id, const std::string& name, const std::string& grade, IssnPng::Image icon);
|
||||
|
||||
/// The menu is open or a text entry is shown: the game's input is held neutral.
|
||||
bool CapturesInput();
|
||||
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
#include <vector>
|
||||
#include "video_core/renderer_vulkan/vk_instance.h"
|
||||
|
||||
#ifndef _WIN32
|
||||
#ifdef __APPLE__
|
||||
#include <mach/mach.h>
|
||||
#include <sys/resource.h>
|
||||
#include <sys/sysctl.h>
|
||||
#elif !defined(_WIN32)
|
||||
#include <cstring>
|
||||
#include <dlfcn.h>
|
||||
#include <sys/sysinfo.h>
|
||||
@@ -123,6 +127,52 @@ void Run(const Vulkan::Instance* instance) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
}
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
// port: macOS measurements. CPU from getrusage, RAM from the Mach task and hw.memsize, VRAM from
|
||||
// VK_EXT_memory_budget (MoltenVK); GPU load stays "n/a".
|
||||
uint64_t ProcessMicros() { // user + system time of the whole process
|
||||
rusage r{};
|
||||
if (getrusage(RUSAGE_SELF, &r) != 0) return 0;
|
||||
return uint64_t(r.ru_utime.tv_sec + r.ru_stime.tv_sec) * 1000000u +
|
||||
uint64_t(r.ru_utime.tv_usec + r.ru_stime.tv_usec);
|
||||
}
|
||||
|
||||
void Run(const Vulkan::Instance* instance) {
|
||||
const unsigned cores = std::max(1u, std::thread::hardware_concurrency());
|
||||
constexpr double GiB = 1024.0 * 1024.0 * 1024.0;
|
||||
uint64_t ram_total = 0;
|
||||
size_t length = sizeof(ram_total);
|
||||
if (sysctlbyname("hw.memsize", &ram_total, &length, nullptr, 0) != 0) ram_total = 0;
|
||||
uint64_t last_process = 0;
|
||||
auto last_wall = std::chrono::steady_clock::now();
|
||||
while (true) {
|
||||
Sample s;
|
||||
s.gpu_name = std::string(instance->GetModelName());
|
||||
const uint64_t process = ProcessMicros();
|
||||
const auto wall = std::chrono::steady_clock::now();
|
||||
if (last_process) {
|
||||
const uint64_t wall_us = uint64_t(std::chrono::duration<double, std::micro>(wall - last_wall).count());
|
||||
s.cpu_percent = CpuPercent(process - last_process, wall_us, cores);
|
||||
}
|
||||
last_process = process;
|
||||
last_wall = wall;
|
||||
mach_task_basic_info_data_t task{};
|
||||
mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT;
|
||||
if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, reinterpret_cast<task_info_t>(&task), &count) == KERN_SUCCESS)
|
||||
s.ram_used_gb = double(task.resident_size) / GiB;
|
||||
if (ram_total) s.ram_total_gb = double(ram_total) / GiB;
|
||||
if (instance->CanReportMemoryUsage()) {
|
||||
s.vram_used_gb = double(instance->GetDeviceMemoryUsage()) / GiB;
|
||||
s.vram_total_gb = double(instance->GetTotalMemoryBudget()) / GiB;
|
||||
}
|
||||
s.gpu_percent = -1.0f;
|
||||
{
|
||||
std::scoped_lock lock{mutex};
|
||||
latest = std::move(s);
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
}
|
||||
}
|
||||
#else
|
||||
// port: Linux measurements. CPU and RAM from /proc, VRAM from VK_EXT_memory_budget, GPU load from
|
||||
// NVML (NVIDIA, loaded at run time) or the amdgpu sysfs counter; anything else stays "n/a".
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
@@ -31,7 +32,12 @@ enum Item : unsigned {
|
||||
inline constexpr unsigned DefaultItems = Fps | FrameTime | Gpu | Vram;
|
||||
|
||||
struct Sample {
|
||||
float fps = 0.0f, frame_ms = 0.0f;
|
||||
/// fps / frame_ms: frames on the screen (what MSI Afterburner and the like count, frame
|
||||
/// pacing repeats and generated frames included); game_fps: frames the game made, shown
|
||||
/// next to it when the two differ (0 = not known).
|
||||
float fps = 0.0f, frame_ms = 0.0f, game_fps = 0.0f;
|
||||
/// Frames made by frame generation per second (0 = none).
|
||||
float generated_fps = 0.0f;
|
||||
std::string upscaler;
|
||||
std::string gpu_name;
|
||||
float gpu_percent = -1.0f, cpu_percent = -1.0f;
|
||||
@@ -66,7 +72,14 @@ inline std::string Format(const Sample& s, unsigned mask, bool row) {
|
||||
}
|
||||
};
|
||||
if (mask & Fps) {
|
||||
std::snprintf(part, sizeof(part), "%.0f FPS", s.fps);
|
||||
if (s.generated_fps >= 1.0f && s.game_fps > 0.0f) {
|
||||
std::snprintf(part, sizeof(part), "%.0f FPS (game %.0f + FG %.0f)", s.fps, s.game_fps,
|
||||
s.generated_fps);
|
||||
} else if (s.game_fps > 0.0f && std::abs(s.game_fps - s.fps) > 0.05f * s.fps) {
|
||||
std::snprintf(part, sizeof(part), "%.0f FPS (game %.0f)", s.fps, s.game_fps);
|
||||
} else {
|
||||
std::snprintf(part, sizeof(part), "%.0f FPS", s.fps);
|
||||
}
|
||||
add();
|
||||
}
|
||||
if (mask & FrameTime) {
|
||||
|
||||
+110
-5
@@ -6,6 +6,7 @@
|
||||
#include "issn_view.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -25,6 +26,21 @@ float Clamp(float v, float lo, float hi) {
|
||||
return std::clamp(v, lo, hi);
|
||||
}
|
||||
|
||||
template <size_t N>
|
||||
int IndexOf(const char* const (&keys)[N], const std::string& value) {
|
||||
for (size_t k = 0; k < N; ++k) {
|
||||
if (value == keys[k]) {
|
||||
return int(k);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* Env(const char* name) {
|
||||
const char* v = std::getenv(name);
|
||||
return v ? v : "";
|
||||
}
|
||||
|
||||
void Set(Values& v, const std::string& key, const std::string& value) {
|
||||
const float f = float(std::atof(value.c_str()));
|
||||
const int i = std::atoi(value.c_str());
|
||||
@@ -54,7 +70,7 @@ void Set(Values& v, const std::string& key, const std::string& value) {
|
||||
return;
|
||||
}
|
||||
if (key == "menu_tab") {
|
||||
v.menu_tab = i >= 0 && i <= 3 ? i : 0;
|
||||
v.menu_tab = i >= 0 && i <= 4 ? i : 0;
|
||||
return;
|
||||
}
|
||||
const char* cheat_keys[] = {"cheat_health", "cheat_magic", "cheat_item", "cheat_rage", "cheat_orbs",
|
||||
@@ -121,14 +137,24 @@ void Set(Values& v, const std::string& key, const std::string& value) {
|
||||
v.reactive_max = Clamp(f, 0.0f, 1.0f);
|
||||
} else if (key == "debug_view") {
|
||||
v.debug_view = std::clamp(i, 0, DebugViewCount - 1);
|
||||
} else if (key == "motion_blur") {
|
||||
v.motion_blur = i != 0;
|
||||
} else if (key == "chromatic_aberration") {
|
||||
v.chromatic_aberration = i != 0;
|
||||
} else if (key == "show_fps") {
|
||||
v.show_fps = i != 0;
|
||||
} else if (key == "save_note") {
|
||||
v.save_note = i != 0;
|
||||
} else if (key == "trophy_note") {
|
||||
v.trophy_note = i != 0;
|
||||
} else if (key == "mouse_enabled") {
|
||||
v.mouse_enabled = i != 0;
|
||||
} else if (key == "mouse_invert_y") {
|
||||
v.mouse_invert_y = i != 0;
|
||||
} else if (key == "mouse_sensitivity") {
|
||||
v.mouse_sensitivity = Clamp(value.empty() ? 1.0f : f, 0.05f, 10.0f);
|
||||
} else if (key == "pad_sensitivity") {
|
||||
v.pad_sensitivity = Clamp(value.empty() ? 1.0f : f, 0.25f, 3.0f);
|
||||
} else if (key == "mouse_smoothing") {
|
||||
v.mouse_smoothing = Clamp(f, 0.0f, 0.95f);
|
||||
} else if (key == "mouse_accel") {
|
||||
@@ -193,6 +219,32 @@ void Set(Values& v, const std::string& key, const std::string& value) {
|
||||
v.far_clip = IssnView::ClampFarClip(f);
|
||||
} else if (key == "mip_lod_bias") {
|
||||
v.mip_lod_bias = IssnView::ClampMipLodBias(f);
|
||||
} else if (key == "hero_health") {
|
||||
v.hero_health = i != 0;
|
||||
} else if (key == "hero_powers") {
|
||||
v.hero_powers = i != 0;
|
||||
} else if (key == "hero_karma") {
|
||||
v.hero_karma = i != 0;
|
||||
} else if (key == "hero_neon_run") {
|
||||
v.hero_neon_run = i != 0;
|
||||
} else if (key == "hero_video_flight") {
|
||||
v.hero_video_flight = i != 0;
|
||||
} else if (key == "hero_smoke_dash") {
|
||||
v.hero_smoke_dash = i != 0;
|
||||
} else if (key == "hero_missiles") {
|
||||
v.hero_missiles = i != 0;
|
||||
} else if (key == "fov") {
|
||||
v.fov = IssnView::ClampFov(value.empty() ? 1.0f : f);
|
||||
} else if (key == "menu_anti_aliasing") {
|
||||
v.menu_anti_aliasing = IndexOf(AntiAliasingKeys, value);
|
||||
} else if (key == "menu_upscale_quality") {
|
||||
v.menu_upscale_quality = IndexOf(UpscaleQualityKeys, value);
|
||||
} else if (key == "menu_aniso") {
|
||||
v.menu_aniso = IndexOf(AnisoKeys, value);
|
||||
} else if (key == "menu_draw_distance") {
|
||||
v.menu_draw_distance = IndexOf(DrawDistanceKeys, value);
|
||||
} else if (key == "menu_frame_gen") {
|
||||
v.menu_frame_gen = value.empty() ? -1 : i != 0;
|
||||
} else if (key == "fsr4_auto_exposure") {
|
||||
v.fsr4_auto_exposure = i != 0;
|
||||
} else if (key == "fsr4_invert_jitter") {
|
||||
@@ -247,13 +299,13 @@ void Load() {
|
||||
{"ISSN_STALE_READBACK", "stale_readback"}, {"ISSN_FSR1", "fsr1"},
|
||||
{"ISSN_RCAS", "rcas"}, {"ISSN_FRAMES_AHEAD", "frames_queued"},
|
||||
{"ISSN_SHOW_FPS", "show_fps"}, {"ISSN_SENSOR_AIM", "sensor_aim"},
|
||||
{"ISSN_MOUSE", "mouse_enabled"}, {"ISSN_MOUSE_SENS", "mouse_sensitivity"},
|
||||
{"ISSN_MOUSE", "mouse_enabled"}, {"ISSN_MOUSE_SENS", "mouse_sensitivity"}, {"ISSN_PAD_SENS", "pad_sensitivity"},
|
||||
{"ISSN_MOUSE_INVERT_Y", "mouse_invert_y"}, {"ISSN_MOUSE_SMOOTHING", "mouse_smoothing"},
|
||||
{"ISSN_MOUSE_ACCEL", "mouse_accel"}, {"ISSN_MOUSE_DEADZONE", "mouse_deadzone"},
|
||||
{"ISSN_LOD_SCALE", "lod_scale"}, {"ISSN_LOD_BANDS", "lod_bands"},
|
||||
{"ISSN_REGION_LOD", "region_lod"}, {"ISSN_REGION_FADE", "region_fade"},
|
||||
{"ISSN_NEAR_CLIP", "near_clip"}, {"ISSN_FAR_CLIP", "far_clip"},
|
||||
{"ISSN_MIP_LOD_BIAS", "mip_lod_bias"},
|
||||
{"ISSN_MIP_LOD_BIAS", "mip_lod_bias"}, {"ISSN_FOV", "fov"},
|
||||
};
|
||||
for (const auto& [env, key] : env_keys) {
|
||||
if (const char* value = std::getenv(env); value && value[0]) {
|
||||
@@ -272,6 +324,37 @@ void Load() {
|
||||
: v.engine_fps.load();
|
||||
v.startup_deferred_readback = v.deferred_readback;
|
||||
v.startup_stale_readback = v.stale_readback;
|
||||
ReadLaunchChoices();
|
||||
}
|
||||
|
||||
void ReadLaunchChoices() {
|
||||
auto& v = Get();
|
||||
// The launcher passes its choices as these variables (launcher/issn_launcher_win.py
|
||||
// game_environment); the menu starts from them where issn.ini has no menu_* key yet.
|
||||
v.startup_anti_aliasing = Env("ISSN_AA_OFF")[0] == '1' ? 4
|
||||
: Env("SHADPS4_DLSS")[0] == '1' ? 3
|
||||
: Env("ISSN_XESS")[0] == '1' ? 2
|
||||
: Env("ISSN_FSR4")[0] == '1' ? 1
|
||||
: 0;
|
||||
const int quality = IndexOf(UpscaleQualityKeys, Env("ISSN_UPSCALE_FACTOR"));
|
||||
v.startup_upscale_quality = quality < 0 ? 0 : quality;
|
||||
const int aniso = IndexOf(AnisoKeys, Env("ISSN_ANISO"));
|
||||
v.startup_aniso = aniso < 0 ? 0 : aniso;
|
||||
const float lod = IssnView::ClampLodScale(float(std::atof(Env("ISSN_LOD_SCALE"))));
|
||||
int distance = 1;
|
||||
for (int d = 0; d < 4; ++d) {
|
||||
if (std::abs(DrawDistanceScales[d] - lod) < 0.01f) distance = d;
|
||||
}
|
||||
v.startup_draw_distance = distance;
|
||||
v.startup_frame_gen = Env("ISSN_FRAME_GEN")[0] == '1';
|
||||
if (v.menu_anti_aliasing < 0) v.menu_anti_aliasing = v.startup_anti_aliasing;
|
||||
if (v.menu_upscale_quality < 0) v.menu_upscale_quality = v.startup_upscale_quality;
|
||||
if (v.menu_aniso < 0) v.menu_aniso = v.startup_aniso;
|
||||
if (v.menu_draw_distance < 0) v.menu_draw_distance = v.startup_draw_distance;
|
||||
if (v.menu_frame_gen < 0) v.menu_frame_gen = v.startup_frame_gen;
|
||||
// The live draw distance follows the launcher's choice this launch (ISSN_LOD_SCALE wins over
|
||||
// issn.ini); the menu's choice is the one shown and saved.
|
||||
v.menu_draw_distance = v.startup_draw_distance;
|
||||
}
|
||||
|
||||
bool GraphicsNeedRestart() {
|
||||
@@ -387,6 +470,27 @@ void Save() {
|
||||
v.lod_scale.load(), v.region_lod.load(), v.region_fade.load(), v.near_clip.load(),
|
||||
v.far_clip.load(), v.mip_lod_bias.load(), v.lod_bands[0].load(), v.lod_bands[1].load(),
|
||||
v.lod_bands[2].load(), v.lod_bands[3].load(), v.lod_bands[4].load(), v.lod_bands[5].load());
|
||||
std::fprintf(file, "fov=%.3f\nsave_note=%d\n", v.fov.load(), int(v.save_note.load()));
|
||||
std::fprintf(file, "motion_blur=%d\nchromatic_aberration=%d\n", int(v.motion_blur.load()),
|
||||
int(v.chromatic_aberration.load()));
|
||||
std::fprintf(file, "trophy_note=%d\n", int(v.trophy_note.load()));
|
||||
std::fprintf(file, "hero_health=%d\nhero_powers=%d\nhero_karma=%d\n", int(v.hero_health.load()),
|
||||
int(v.hero_powers.load()), int(v.hero_karma.load()));
|
||||
std::fprintf(file, "hero_neon_run=%d\nhero_video_flight=%d\nhero_smoke_dash=%d\n",
|
||||
int(v.hero_neon_run.load()), int(v.hero_video_flight.load()),
|
||||
int(v.hero_smoke_dash.load()));
|
||||
std::fprintf(file, "hero_missiles=%d\n", int(v.hero_missiles.load()));
|
||||
// For the launcher (next start): its own setting names and values.
|
||||
const auto pick = [](const auto& keys, int index) {
|
||||
const int n = int(sizeof(keys) / sizeof(keys[0]));
|
||||
return keys[std::clamp(index, 0, n - 1)];
|
||||
};
|
||||
std::fprintf(file,
|
||||
"menu_anti_aliasing=%s\nmenu_upscale_quality=%s\nmenu_aniso=%s\n"
|
||||
"menu_draw_distance=%s\nmenu_frame_gen=%d\n",
|
||||
pick(AntiAliasingKeys, v.menu_anti_aliasing), pick(UpscaleQualityKeys, v.menu_upscale_quality),
|
||||
pick(AnisoKeys, v.menu_aniso), pick(DrawDistanceKeys, v.menu_draw_distance),
|
||||
v.menu_frame_gen.load() > 0 ? 1 : 0);
|
||||
// Fixes: applied on the next start by the launcher (ISSN_BPnn / ISSN_<NAME>); only the ones
|
||||
// the player or the launcher chose are written.
|
||||
for (int n = 0; n < kBackportPrefs; ++n) {
|
||||
@@ -397,9 +501,10 @@ void Save() {
|
||||
}
|
||||
std::fprintf(file,
|
||||
"mouse_enabled=%d\nmouse_sensitivity=%.2f\nmouse_invert_y=%d\nmouse_smoothing=%.2f\n"
|
||||
"mouse_accel=%.2f\nmouse_deadzone=%.2f\nkey_map=%s\n",
|
||||
"mouse_accel=%.2f\nmouse_deadzone=%.2f\npad_sensitivity=%.2f\nkey_map=%s\n",
|
||||
int(v.mouse_enabled.load()), v.mouse_sensitivity.load(), int(v.mouse_invert_y.load()),
|
||||
v.mouse_smoothing.load(), v.mouse_accel.load(), v.mouse_deadzone.load(), v.key_map.c_str());
|
||||
v.mouse_smoothing.load(), v.mouse_accel.load(), v.mouse_deadzone.load(),
|
||||
v.pad_sensitivity.load(), v.key_map.c_str());
|
||||
// Read by run.sh at start.
|
||||
std::fprintf(file, "live_resolution=%s\n", v.live_resolution < 0 ? "auto"
|
||||
: v.live_resolution ? "1" : "0");
|
||||
|
||||
@@ -49,6 +49,10 @@ struct Values {
|
||||
std::atomic<float> reactive_max{0.9f};
|
||||
std::atomic<int> debug_view{DebugNone};
|
||||
std::atomic<bool> show_fps{false};
|
||||
/// "Game saved" note in a corner when the game writes a save (issn_overlay.cpp).
|
||||
std::atomic<bool> save_note{true};
|
||||
/// issn 11-04: the trophy popup (icon and name) when the game unlocks a trophy.
|
||||
std::atomic<bool> trophy_note{true};
|
||||
/// Sensor mode's aiming speed (the graffiti cursor), percent of the default: 25..200.
|
||||
std::atomic<int> sensor_aim{100};
|
||||
/// The graffiti guide was shown once; the menu's Controls tab opens it again.
|
||||
@@ -138,11 +142,40 @@ struct Values {
|
||||
std::atomic<bool> view_supported{false};
|
||||
/// Extra texture LOD bias of the game's draws (vk_rasterizer.cpp), any game.
|
||||
std::atomic<float> mip_lod_bias{0.0f};
|
||||
/// Delsin's cheats (src/hero_hook.h, issn_hero.cpp), live; hero_supported once hooked.
|
||||
std::atomic<bool> hero_health{false}, hero_powers{false}, hero_karma{false};
|
||||
std::atomic<bool> hero_supported{false};
|
||||
/// issn-axy 11-04 (src/dash_hook.h): neon run and video flight while the dash button is held,
|
||||
/// a long smoke dash; *_supported once the probe's stubs are in.
|
||||
std::atomic<bool> hero_neon_run{false}, hero_video_flight{false}, hero_smoke_dash{false};
|
||||
std::atomic<bool> hero_neon_supported{false}, hero_video_supported{false};
|
||||
/// 11-05: infinite missiles (src/hero_hook.h); which of the probe's checks passed.
|
||||
std::atomic<bool> hero_missiles{false};
|
||||
std::atomic<bool> hero_missiles_supported{false}, hero_karma_supported{false},
|
||||
hero_no_hit_supported{false};
|
||||
/// issn 11-04 (video_core issn_postfx.h): the game's motion blur and chromatic aberration,
|
||||
/// live from the menu and the launcher (issn.ini motion_blur / chromatic_aberration).
|
||||
std::atomic<bool> motion_blur{true}, chromatic_aberration{true};
|
||||
/// Field of view factor (src/fov_hook.h, issn_view.cpp), live; fov_supported once hooked.
|
||||
std::atomic<float> fov{1.0f};
|
||||
std::atomic<bool> fov_supported{false};
|
||||
|
||||
// Launcher settings the menu changes for the next start (issn-axy): kept in issn.ini as
|
||||
// menu_* keys, which the launcher reads back into its own settings (and writes when they
|
||||
// change there). Index into the menu's choices; -1 = not known (the menu shows this launch's).
|
||||
std::atomic<int> menu_anti_aliasing{-1}, menu_upscale_quality{-1}, menu_aniso{-1},
|
||||
menu_draw_distance{-1}, menu_frame_gen{-1};
|
||||
int startup_anti_aliasing = 0, startup_upscale_quality = 0, startup_aniso = 0,
|
||||
startup_draw_distance = 1, startup_frame_gen = 0;
|
||||
|
||||
// Mouse and keyboard (src/runtime_pad.c through issngpu_input_settings, src/mouse_stick.h):
|
||||
// the mouse turns the camera (right stick). Live from the menu.
|
||||
std::atomic<bool> mouse_enabled{true}, mouse_invert_y{false};
|
||||
std::atomic<float> mouse_sensitivity{1.0f}, mouse_smoothing{0.5f}, mouse_accel{0.0f}, mouse_deadzone{0.1f};
|
||||
/// issn 11-04: right stick sensitivity (runtime_pad.c), live; 1 = the game's own feel.
|
||||
std::atomic<float> pad_sensitivity{1.0f};
|
||||
/// issn 11-04: the spray can pose for pads without motion sensors (runtime_pad.c), not saved.
|
||||
std::atomic<bool> spray_pose{false};
|
||||
/// Keyboard and mouse button remap (ISSN_KEY_MAP syntax); written by Load only, read once.
|
||||
std::string key_map;
|
||||
};
|
||||
@@ -164,6 +197,17 @@ bool GraphicsNeedRestart();
|
||||
/// Writes the file (menu changes).
|
||||
void Save();
|
||||
|
||||
/// The launcher's choices the menu offers (menu_* keys; the values are the launcher's own).
|
||||
inline constexpr const char* AntiAliasingKeys[] = {"game", "fsr4", "xess", "dlss", "off"};
|
||||
inline constexpr const char* UpscaleQualityKeys[] = {"", "1.5", "1.7", "2", "3"};
|
||||
inline constexpr const char* AnisoKeys[] = {"", "4", "8", "16"};
|
||||
inline constexpr const char* DrawDistanceKeys[] = {"0.6", "", "1.5", "2.0"};
|
||||
inline constexpr float DrawDistanceScales[] = {0.6f, 1.0f, 1.5f, 2.0f};
|
||||
/// What this launch runs with (environment), as menu indexes; filled by Load.
|
||||
void ReadLaunchChoices();
|
||||
/// The menu's live choices to the renderer: texture filtering, frame generation on / off.
|
||||
void ApplyLiveChoices();
|
||||
|
||||
/// Render resolution divisor of a preset (1.0 native, 1.5 quality, ...).
|
||||
float PresetScale(int preset);
|
||||
const char* PresetName(int preset);
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
#ifdef _WIN32
|
||||
#include <bit>
|
||||
#include <windows.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include <pthread.h>
|
||||
#include <sys/qos.h>
|
||||
#else
|
||||
#include <sched.h>
|
||||
#include <sys/resource.h>
|
||||
@@ -31,6 +34,17 @@ inline unsigned Available() {
|
||||
inline void MakeBackground() {
|
||||
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_IDLE);
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
/// Hardware threads available to the process (macOS has no affinity masks).
|
||||
inline unsigned Available() {
|
||||
return std::max(1u, std::thread::hardware_concurrency());
|
||||
}
|
||||
|
||||
/// The calling thread runs at background QoS: on Apple silicon, the efficiency cores, so
|
||||
/// it never takes time from the game's threads.
|
||||
inline void MakeBackground() {
|
||||
pthread_set_qos_class_self_np(QOS_CLASS_BACKGROUND, 0);
|
||||
}
|
||||
#else
|
||||
|
||||
/// Hardware threads available to the process.
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "issn_view.h"
|
||||
|
||||
#include "../../src/view_hook.h"
|
||||
#include "../../src/fov_hook.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
@@ -115,6 +116,74 @@ void Apply() {
|
||||
|
||||
} // namespace
|
||||
|
||||
// Field of view (src/fov_hook.h): the cave the camera's four sites and the angle readers read, in
|
||||
// a read-write page next to the image: 0.5 x the menu's factor, the factor, and the camera SetFov
|
||||
// last saw. A thread of its own (the draw distance one exists only when that hook is in place).
|
||||
namespace {
|
||||
IssnFovCave* fov_cave = nullptr;
|
||||
|
||||
// The camera works its tan out only when its angle (+0x590) differs from the target (+0x5d4):
|
||||
// a nudge to the angle makes its next update do it again with the new factor.
|
||||
float ReadFloat(const unsigned char* at) {
|
||||
float v;
|
||||
std::memcpy(&v, at, 4);
|
||||
return v;
|
||||
}
|
||||
|
||||
// A slot holds a camera when its state agrees with itself: angle and target in range, an aspect,
|
||||
// and a tan its update made from the angle with one of the menu's factors (0.9x to 1.4x; a camera
|
||||
// that missed earlier nudges still has an older one).
|
||||
bool LooksLikeCamera(const unsigned char* camera) {
|
||||
const float angle = ReadFloat(camera + 0x590), target = ReadFloat(camera + 0x5d4);
|
||||
const float aspect = ReadFloat(camera + 0x59c), tan_half = ReadFloat(camera + 0x5a4);
|
||||
if (!(angle > 0.05f && angle < 3.5f && target > 0.05f && target < 3.5f && aspect > 0.3f && aspect < 5.0f)) {
|
||||
return false;
|
||||
}
|
||||
return tan_half > std::tan(0.5f * 0.89f * angle) - 1e-3f && tan_half < std::tan(0.5f * 1.41f * angle) + 1e-3f;
|
||||
}
|
||||
|
||||
void NudgeCameras(float factor) {
|
||||
int nudged = 0, seen = 0;
|
||||
for (const std::uint64_t at : fov_cave->camera) {
|
||||
if (at < (1ull << 32) || at >= (1ull << 47)) {
|
||||
continue;
|
||||
}
|
||||
++seen;
|
||||
auto* camera = reinterpret_cast<unsigned char*>(at);
|
||||
if (!LooksLikeCamera(camera)) {
|
||||
continue;
|
||||
}
|
||||
const float nudged_angle = ReadFloat(camera + 0x5d4) * 1.001f;
|
||||
std::memcpy(camera + 0x590, &nudged_angle, 4);
|
||||
++nudged;
|
||||
}
|
||||
std::printf("Field of view: x%.2f, %d of %d cameras seen asked to work their view out again\n", factor,
|
||||
nudged, seen);
|
||||
}
|
||||
|
||||
void ApplyFov() {
|
||||
const float factor = IssnView::ClampFov(IssnSettings::Get().fov);
|
||||
if (fov_cave->factor != factor) {
|
||||
fov_cave->factor = factor;
|
||||
fov_cave->half_factor = 0.5f * factor;
|
||||
NudgeCameras(factor);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
extern "C" void issn_fov_attach(IssnFovCave* cave) {
|
||||
fov_cave = cave;
|
||||
ApplyFov(); // before the game's first frame
|
||||
IssnSettings::Get().fov_supported = true;
|
||||
std::printf("Field of view: menu control ready (x%.2f)\n", IssnSettings::Get().fov.load());
|
||||
std::thread([] {
|
||||
for (;;) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
ApplyFov();
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
|
||||
extern "C" uintptr_t issn_view_region_params(void) {
|
||||
return reinterpret_cast<uintptr_t>(region_params);
|
||||
}
|
||||
|
||||
@@ -22,4 +22,6 @@ inline float ClampFarClip(float v) { return Clamp(v, 1000.0f, 1.0e7f, 1.0e7f); }
|
||||
inline float ClampBand(float v) { return Clamp(v, 1.0f, 1.0e6f, 400.0f); }
|
||||
/// Extra texture LOD bias of the game's draws: negative is sharper, positive is blurrier.
|
||||
inline float ClampMipLodBias(float v) { return Clamp(v, -3.0f, 3.0f, 0.0f); }
|
||||
/// Field of view factor (src/fov_hook.h): 1 the game's angle.
|
||||
inline float ClampFov(float v) { return Clamp(v, 0.75f, 1.6f, 1.0f); }
|
||||
} // namespace IssnView
|
||||
@@ -9,6 +9,10 @@
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#undef near
|
||||
#elif defined(__APPLE__)
|
||||
#include <pthread.h>
|
||||
#include <sys/ucontext.h>
|
||||
#include <unistd.h>
|
||||
#else
|
||||
#include <ucontext.h>
|
||||
#include <unistd.h>
|
||||
@@ -61,7 +65,15 @@ void Note(std::uint64_t address, const void* data, std::uint64_t size, Source so
|
||||
}
|
||||
|
||||
void Record(std::uint64_t address, const void* data, std::uint64_t size, Source source) {
|
||||
#ifdef __APPLE__
|
||||
static thread_local const std::uint32_t tid = [] {
|
||||
std::uint64_t id = 0;
|
||||
pthread_threadid_np(nullptr, &id);
|
||||
return static_cast<std::uint32_t>(id);
|
||||
}();
|
||||
#else
|
||||
static thread_local const std::uint32_t tid = static_cast<std::uint32_t>(gettid());
|
||||
#endif
|
||||
Entry e{address, size, 0, __rdtsc(), source, tid};
|
||||
std::memcpy(&e.first, data, size < 8 ? size : 8);
|
||||
Push(ring, head, e);
|
||||
@@ -93,6 +105,9 @@ extern "C" void issngpu_dump_guest_writes(void* ucontext) {
|
||||
// The loader passes EXCEPTION_POINTERS (see signal_context.cpp).
|
||||
const CONTEXT* c = static_cast<const EXCEPTION_POINTERS*>(ucontext)->ContextRecord;
|
||||
const std::uint64_t regs[] = {c->Rax, c->Rbx, c->Rcx, c->Rdx, c->Rsi, c->Rdi, c->R14, c->R15};
|
||||
#elif defined(__APPLE__)
|
||||
const auto& s = static_cast<const ucontext_t*>(ucontext)->uc_mcontext->__ss;
|
||||
const std::uint64_t regs[] = {s.__rax, s.__rbx, s.__rcx, s.__rdx, s.__rsi, s.__rdi, s.__r14, s.__r15};
|
||||
#else
|
||||
const auto* uc = static_cast<const ucontext_t*>(ucontext);
|
||||
const auto* g = uc->uc_mcontext.gregs;
|
||||
|
||||
+141
-6
@@ -30,6 +30,7 @@
|
||||
#include <SDL3/SDL.h>
|
||||
#include "../issngpu.h"
|
||||
#include "common/elf_info.h"
|
||||
#include "common/release_mode.h"
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
@@ -65,6 +66,27 @@ void runtime_report(void);
|
||||
|
||||
Frontend::WindowSDL* g_window = nullptr;
|
||||
|
||||
// issn_pc: the screen shape (issn_game_patches.cpp ApplyRenderSize).
|
||||
namespace IssnShape {
|
||||
double g_aspect = 16.0 / 9.0;
|
||||
float g_movie_x = 0.0f, g_movie_y = 0.0f;
|
||||
/// The game window's size in pixels; a fullscreen window: its display's desktop mode.
|
||||
bool ScreenPixels(int& width, int& height) {
|
||||
SDL_Window* window = g_window ? g_window->GetSDLWindow() : nullptr;
|
||||
if (!window) {
|
||||
return false;
|
||||
}
|
||||
if (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) {
|
||||
if (const SDL_DisplayMode* mode = SDL_GetDesktopDisplayMode(SDL_GetDisplayForWindow(window))) {
|
||||
width = mode->w;
|
||||
height = mode->h;
|
||||
return width > 0 && height > 0;
|
||||
}
|
||||
}
|
||||
return SDL_GetWindowSizeInPixels(window, &width, &height) && width > 0 && height > 0;
|
||||
}
|
||||
} // namespace IssnShape
|
||||
|
||||
// issn-axy: what axylpup's tree takes from its loader and patcher.
|
||||
namespace MemoryPatcher {
|
||||
uintptr_t g_eboot_address = 0;
|
||||
@@ -84,8 +106,14 @@ bool InLiveResizeWindow() {
|
||||
return false;
|
||||
}
|
||||
bool IsRenderShaped(u32 width, u32 height) {
|
||||
// The game's 16:9 render shape (+-3%), as his check with the default aspect.
|
||||
return height != 0 && std::abs(double(width) / height - 16.0 / 9.0) < 0.03 * (16.0 / 9.0);
|
||||
// The game's render shape (+-3%): 16:9, or the screen shape issn_game_patches.cpp set.
|
||||
const double aspect = IssnShape::g_aspect;
|
||||
return height != 0 && std::abs(double(width) / height - aspect) < 0.03 * aspect;
|
||||
}
|
||||
bool MovieFitRect(float& x, float& y) {
|
||||
x = IssnShape::g_movie_x;
|
||||
y = IssnShape::g_movie_y;
|
||||
return x > 0.0f;
|
||||
}
|
||||
s64 MsSinceLiveResize() {
|
||||
return INT64_MAX;
|
||||
@@ -127,6 +155,28 @@ void SymbolsResolver::AddSymbol(const char* nid, const char* library, const char
|
||||
}
|
||||
} // namespace Core::Loader
|
||||
|
||||
// axylpup 1.1.1 (GitHub #5): the box shown before an out-of-memory assert closes the game.
|
||||
namespace Common::ReleaseMode {
|
||||
void ShowOutOfMemoryNotice(unsigned long long bytes) {
|
||||
static std::atomic<bool> shown{false};
|
||||
if (shown.exchange(true)) {
|
||||
return;
|
||||
}
|
||||
const std::string text = fmt::format(
|
||||
"Windows could not give the game {} MB of memory for the graphics card, so the game has "
|
||||
"to close.\n\nWhat usually helps:\n"
|
||||
"- close other programs (browser, Discord, overlays)\n"
|
||||
"- lower the internal resolution or turn Performance mode on (launcher: Performance)\n"
|
||||
"- turn the page file on: Advanced system settings > Performance Settings > Advanced > "
|
||||
"Virtual memory > \"Automatically manage paging file size\"\n"
|
||||
"- keep at least 20 GB free on C:\n\n"
|
||||
"user\\last_run.log has the details - please attach it to a bug report.",
|
||||
bytes >> 20);
|
||||
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "inFAMOUS Second Son: out of memory",
|
||||
text.c_str(), nullptr);
|
||||
}
|
||||
} // namespace Common::ReleaseMode
|
||||
|
||||
// ElfInfo's fields are private to Core::Emulator; the port fills them here.
|
||||
namespace Core {
|
||||
class Emulator {
|
||||
@@ -419,7 +469,32 @@ u32 IssnDisplayRefreshHz() {
|
||||
return hz;
|
||||
}
|
||||
|
||||
#ifdef ISSN_PGO_GENERATE
|
||||
#if defined(ISSN_PGO_GENERATE) && defined(ISSN_PGO_LLVM)
|
||||
extern "C" int __llvm_profile_write_file(void);
|
||||
extern "C" void __llvm_profile_set_filename(const char*);
|
||||
// Instrumented Clang build: the counters are cumulative, so each write replaces the file with
|
||||
// the totals so far (the game often ends through _exit, which skips the write at exit). One file
|
||||
// per process: <user>/pgo/issn-<pid>.profraw.
|
||||
static void StartProfileWriter() {
|
||||
static std::string path = [] {
|
||||
const char* user = std::getenv("ISSN_GPU_USER_DIR");
|
||||
const std::filesystem::path dir = std::filesystem::path(user && *user ? user : "user") / "pgo";
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(dir, ec);
|
||||
return (dir / "issn-%p.profraw").string();
|
||||
}();
|
||||
__llvm_profile_set_filename(path.c_str());
|
||||
std::printf("PGO: instrumented build, profile in %s every 30 s\n", path.c_str());
|
||||
std::thread([] {
|
||||
Common::SetCurrentThreadName("port:pgo");
|
||||
for (;;) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(30));
|
||||
__llvm_profile_write_file();
|
||||
std::printf("PGO: profile written\n");
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
#elif defined(ISSN_PGO_GENERATE)
|
||||
extern "C" void __gcov_dump(void);
|
||||
extern "C" void __gcov_reset(void);
|
||||
// Instrumented build (ISSN_PGO=generate): the game often ends through _exit (watchdog, guest
|
||||
@@ -596,6 +671,46 @@ ScreenshotRequests ConsumeScreenshotRequests() {
|
||||
}
|
||||
} // namespace VideoCore
|
||||
|
||||
extern "C" const char* issngpu_saved_key_map(void) {
|
||||
static std::string copy;
|
||||
copy = IssnSettings::Get().key_map;
|
||||
return copy.c_str();
|
||||
}
|
||||
|
||||
extern "C" float issngpu_mouse_sensitivity(void) {
|
||||
return IssnSettings::Get().mouse_sensitivity.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
extern "C" void issngpu_set_mouse_sensitivity(float value) {
|
||||
IssnSettings::Get().mouse_sensitivity = std::clamp(value, 0.05f, 10.0f);
|
||||
IssnSettings::Save();
|
||||
}
|
||||
|
||||
extern "C" int issngpu_spray_pose(void) {
|
||||
return IssnSettings::Get().spray_pose.load(std::memory_order_relaxed) ? 1 : 0;
|
||||
}
|
||||
|
||||
extern "C" void issngpu_set_spray_pose(int on) {
|
||||
IssnSettings::Get().spray_pose = on != 0;
|
||||
}
|
||||
|
||||
extern "C" float issngpu_pad_sensitivity(void) {
|
||||
return IssnSettings::Get().pad_sensitivity.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
extern "C" void issngpu_note_game_saved(const char* what) {
|
||||
IssnOverlay::NoteGameSaved(what ? what : "");
|
||||
}
|
||||
|
||||
extern "C" void issngpu_note_trophy(int id, const char* name, const char* grade, const void* png, uint64_t size) {
|
||||
// The icon is decoded here, on the game's thread; the overlay makes it a texture.
|
||||
IssnPng::Image icon;
|
||||
if (png && size && !IssnPng::DecodeRgba(static_cast<const unsigned char*>(png), size_t(size), icon)) {
|
||||
LOG_WARNING(Lib_NpTrophy, "trophy {}: icon not decoded", id);
|
||||
}
|
||||
IssnOverlay::NoteTrophy(id, name ? name : "", grade ? grade : "", std::move(icon));
|
||||
}
|
||||
|
||||
extern "C" int issngpu_overlay_captures_input(void) {
|
||||
return IssnOverlay::CapturesInput() ? 1 : 0;
|
||||
}
|
||||
@@ -637,7 +752,27 @@ extern "C" int issngpu_text_input_poll(char* out, uint64_t size) {
|
||||
return state;
|
||||
}
|
||||
|
||||
// issn: ISSN_WRITE_MERGE was the old core's; the new one has no merge path (writes fault normally).
|
||||
extern "C" int issngpu_cpu_write(void*, const void*, int, uint64_t) {
|
||||
return 0;
|
||||
// issn 0.3.6: a file read copied into GPU-tracked memory (src/runtime_file.c bounce_read). The
|
||||
// game streams its city blocks this way, 10-30 MB at a time; page by page that was one write
|
||||
// fault per 4 KB page (João's hitch lines 10-29: 4,000-7,600 faults and 50-170 ms in the frames
|
||||
// that read a block). The whole chunk is now marked CPU-written first, exactly as its faults
|
||||
// would have done (GuestWriteFault: GPU-written pages are read back, the rest unprotected), then
|
||||
// copied without faulting. Small reads keep the fault path. ISSN_READ_INVALIDATE=0: off.
|
||||
// Only file reads (fill -2, src/runtime_file.c): the guest memset / memcpy (src/runtime.c) call
|
||||
// this too, memset with no source (the first try copied from it: crash at start, 10-29).
|
||||
extern "C" int issngpu_cpu_write(void* dst, const void* src, int fill, uint64_t size) {
|
||||
static const bool enabled = [] {
|
||||
const char* v = std::getenv("ISSN_READ_INVALIDATE");
|
||||
return !(v && v[0] == '0');
|
||||
}();
|
||||
if (!enabled || fill != -2 || !src || size < (64u << 10)) {
|
||||
return 0;
|
||||
}
|
||||
auto* rasterizer = Core::Memory::Instance()->GetRasterizer();
|
||||
if (!rasterizer) {
|
||||
return 0;
|
||||
}
|
||||
rasterizer->InvalidateMemory(reinterpret_cast<VAddr>(dst), size, VideoCore::InvalidateOrigin::GuestWriteFault);
|
||||
std::memcpy(dst, src, size);
|
||||
return 1;
|
||||
}
|
||||
@@ -5,6 +5,9 @@
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <SDL3/SDL.h>
|
||||
#ifdef __APPLE__
|
||||
#include <SDL3/SDL_metal.h>
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
@@ -116,6 +119,12 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, const char* title) : width{width_}
|
||||
window_info.type = WindowSystemType::Windows;
|
||||
window_info.render_surface = SDL_GetPointerProperty(wp, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr);
|
||||
} else
|
||||
#elif defined(__APPLE__)
|
||||
// port: MoltenVK draws into the CAMetalLayer of an SDL Metal view.
|
||||
if (driver && !std::strcmp(driver, "cocoa")) {
|
||||
window_info.type = WindowSystemType::Metal;
|
||||
window_info.render_surface = SDL_Metal_GetLayer(SDL_Metal_CreateView(window));
|
||||
} else
|
||||
#endif
|
||||
if (driver && !std::strcmp(driver, "x11")) {
|
||||
window_info.type = WindowSystemType::X11;
|
||||
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
# XeSS SDK headers
|
||||
|
||||
`include/xess/xess.h` and `include/xess/xess_vk.h` from Intel's XeSS SDK
|
||||
(https://github.com/intel/xess), MIT-licensed (see the SPDX line in each file).
|
||||
|
||||
Only the headers are here. The runtime, `libxess.dll`, is Intel's binary under the Intel Simplified
|
||||
Software License; the port loads it at run time from the `xess` folder next to `issn-probe.exe`
|
||||
(ISSN_XESS=1, `gpu/shadps4/video_core/renderer_vulkan/issn_xess.cpp`).
|
||||
+426
@@ -0,0 +1,426 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2026 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef XESS_H
|
||||
#define XESS_H
|
||||
|
||||
#ifdef XESS_SHARED_LIB
|
||||
#ifdef _WIN32
|
||||
#ifdef XESS_EXPORT_API
|
||||
#define XESS_API __declspec(dllexport)
|
||||
#else
|
||||
#define XESS_API __declspec(dllimport)
|
||||
#endif
|
||||
#else
|
||||
#define XESS_API __attribute__((visibility("default")))
|
||||
#endif
|
||||
#else
|
||||
#define XESS_API
|
||||
#endif
|
||||
|
||||
#if !defined _MSC_VER || (_MSC_VER >= 1929)
|
||||
#define XESS_PRAGMA(x) _Pragma(#x)
|
||||
#else
|
||||
#define XESS_PRAGMA(x) __pragma(x)
|
||||
#endif
|
||||
#define XESS_PACK_B_X(x) XESS_PRAGMA(pack(push, x))
|
||||
#define XESS_PACK_E() XESS_PRAGMA(pack(pop))
|
||||
#define XESS_PACK_B() XESS_PACK_B_X(8)
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct _xess_context_handle_t* xess_context_handle_t;
|
||||
|
||||
XESS_PACK_B()
|
||||
/**
|
||||
* @brief XeSS version.
|
||||
*
|
||||
* XeSS uses major.minor.patch version format and Numeric 90+ scheme for development stage builds.
|
||||
*/
|
||||
typedef struct _xess_version_t
|
||||
{
|
||||
/** A major version increment indicates a new API and potentially a
|
||||
* break in functionality. */
|
||||
uint16_t major;
|
||||
/** A minor version increment indicates incremental changes such as
|
||||
* optional inputs or flags. This does not break existing functionality. */
|
||||
uint16_t minor;
|
||||
/** A patch version increment may include performance or quality tweaks or fixes for known issues.
|
||||
* There's no change in the interfaces.
|
||||
* Versions beyond 90 used for development builds to change the interface for the next release.
|
||||
*/
|
||||
uint16_t patch;
|
||||
/** Reserved for future use. */
|
||||
uint16_t reserved;
|
||||
} xess_version_t;
|
||||
XESS_PACK_E()
|
||||
|
||||
XESS_PACK_B()
|
||||
/**
|
||||
* @brief 2D variable.
|
||||
*/
|
||||
typedef struct _xess_2d_t
|
||||
{
|
||||
uint32_t x;
|
||||
uint32_t y;
|
||||
} xess_2d_t;
|
||||
XESS_PACK_E()
|
||||
|
||||
/**
|
||||
* @brief 2D coordinates.
|
||||
*/
|
||||
typedef xess_2d_t xess_coord_t;
|
||||
|
||||
/**
|
||||
* @brief XeSS quality settings.
|
||||
*/
|
||||
typedef enum _xess_quality_settings_t
|
||||
{
|
||||
XESS_QUALITY_SETTING_ULTRA_PERFORMANCE = 100,
|
||||
XESS_QUALITY_SETTING_PERFORMANCE = 101,
|
||||
XESS_QUALITY_SETTING_BALANCED = 102,
|
||||
XESS_QUALITY_SETTING_QUALITY = 103,
|
||||
XESS_QUALITY_SETTING_ULTRA_QUALITY = 104,
|
||||
XESS_QUALITY_SETTING_ULTRA_QUALITY_PLUS = 105,
|
||||
XESS_QUALITY_SETTING_AA = 106,
|
||||
} xess_quality_settings_t;
|
||||
|
||||
/**
|
||||
* @brief XeSS initialization flags.
|
||||
*/
|
||||
typedef enum _xess_init_flags_t
|
||||
{
|
||||
XESS_INIT_FLAG_NONE = 0,
|
||||
/** Use motion vectors at target resolution. */
|
||||
XESS_INIT_FLAG_HIGH_RES_MV = 1 << 0,
|
||||
/** Use inverted (increased precision) depth encoding */
|
||||
XESS_INIT_FLAG_INVERTED_DEPTH = 1 << 1,
|
||||
/** Use exposure texture to scale input color. */
|
||||
XESS_INIT_FLAG_EXPOSURE_SCALE_TEXTURE = 1 << 2,
|
||||
/** Use responsive pixel mask texture. */
|
||||
XESS_INIT_FLAG_RESPONSIVE_PIXEL_MASK = 1 << 3,
|
||||
/** Use velocity in NDC */
|
||||
XESS_INIT_FLAG_USE_NDC_VELOCITY = 1 << 4,
|
||||
/** Use external descriptor heap */
|
||||
XESS_INIT_FLAG_EXTERNAL_DESCRIPTOR_HEAP = 1 << 5,
|
||||
/** Disable tonemapping for input and output */
|
||||
XESS_INIT_FLAG_LDR_INPUT_COLOR = 1 << 6,
|
||||
/** Remove jitter from input velocity*/
|
||||
XESS_INIT_FLAG_JITTERED_MV = 1 << 7,
|
||||
/** Enable automatic exposure calculation. */
|
||||
XESS_INIT_FLAG_ENABLE_AUTOEXPOSURE = 1 << 8
|
||||
} xess_init_flags_t;
|
||||
|
||||
XESS_PACK_B()
|
||||
/**
|
||||
* @brief Properties for internal XeSS resources.
|
||||
*/
|
||||
typedef struct _xess_properties_t
|
||||
{
|
||||
/** Required amount of descriptors for XeSS */
|
||||
uint32_t requiredDescriptorCount;
|
||||
/** The heap size required by XeSS for temporary buffer storage. */
|
||||
uint64_t tempBufferHeapSize;
|
||||
/** The heap size required by XeSS for temporary texture storage. */
|
||||
uint64_t tempTextureHeapSize;
|
||||
} xess_properties_t;
|
||||
XESS_PACK_E()
|
||||
|
||||
/**
|
||||
* @brief XeSS return codes.
|
||||
*/
|
||||
typedef enum _xess_result_t
|
||||
{
|
||||
/** Warning. Folder to store dump data doesn't exist. Write operation skipped.*/
|
||||
XESS_RESULT_WARNING_NONEXISTING_FOLDER = 1,
|
||||
/** An old or outdated driver. */
|
||||
XESS_RESULT_WARNING_OLD_DRIVER = 2,
|
||||
/** XeSS operation was successful. */
|
||||
XESS_RESULT_SUCCESS = 0,
|
||||
/** XeSS not supported on the GPU. An SM 6.4 capable GPU is required. */
|
||||
XESS_RESULT_ERROR_UNSUPPORTED_DEVICE = -1,
|
||||
/** An unsupported driver. */
|
||||
XESS_RESULT_ERROR_UNSUPPORTED_DRIVER = -2,
|
||||
/** Execute called without initialization. */
|
||||
XESS_RESULT_ERROR_UNINITIALIZED = -3,
|
||||
/** Invalid argument such as descriptor handles. */
|
||||
XESS_RESULT_ERROR_INVALID_ARGUMENT = -4,
|
||||
/** Not enough available GPU memory. */
|
||||
XESS_RESULT_ERROR_DEVICE_OUT_OF_MEMORY = -5,
|
||||
/** Device function failed, such as resource or descriptor creation. */
|
||||
XESS_RESULT_ERROR_DEVICE = -6,
|
||||
/** The function is not implemented */
|
||||
XESS_RESULT_ERROR_NOT_IMPLEMENTED = -7,
|
||||
/** Invalid context. */
|
||||
XESS_RESULT_ERROR_INVALID_CONTEXT = -8,
|
||||
/** Operation not finished yet. */
|
||||
XESS_RESULT_ERROR_OPERATION_IN_PROGRESS = -9,
|
||||
/** Operation not supported in current configuration. */
|
||||
XESS_RESULT_ERROR_UNSUPPORTED = -10,
|
||||
/** The library cannot be loaded. */
|
||||
XESS_RESULT_ERROR_CANT_LOAD_LIBRARY = -11,
|
||||
/** Call to function done in invalid order. */
|
||||
XESS_RESULT_ERROR_WRONG_CALL_ORDER = -12,
|
||||
|
||||
/** Unknown internal failure */
|
||||
XESS_RESULT_ERROR_UNKNOWN = -1000,
|
||||
} xess_result_t;
|
||||
|
||||
/**
|
||||
* @brief XeSS logging level
|
||||
*/
|
||||
typedef enum _xess_logging_level_t
|
||||
{
|
||||
XESS_LOGGING_LEVEL_DEBUG = 0,
|
||||
XESS_LOGGING_LEVEL_INFO = 1,
|
||||
XESS_LOGGING_LEVEL_WARNING = 2,
|
||||
XESS_LOGGING_LEVEL_ERROR = 3
|
||||
} xess_logging_level_t;
|
||||
|
||||
/**
|
||||
* A logging callback provided by the application. This callback can be called from other threads.
|
||||
* Message pointer is only valid inside function and may be invalid right after return call.
|
||||
* Message is a null-terminated utf-8 string
|
||||
*/
|
||||
typedef void (*xess_app_log_callback_t)(const char *message, xess_logging_level_t loggingLevel);
|
||||
|
||||
#ifndef XESS_TYPES_ONLY
|
||||
|
||||
/** @addtogroup xess XeSS API exports
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Gets the XeSS version. This is baked into the XeSS SDK release.
|
||||
* @param[out] pVersion Returned XeSS version.
|
||||
* @return XeSS return status code.
|
||||
*/
|
||||
XESS_API xess_result_t xessGetVersion(xess_version_t* pVersion);
|
||||
|
||||
/**
|
||||
* @brief Gets the version of the loaded Intel XeFX library. When running on Intel platforms
|
||||
* this function will return the version of the loaded Intel XeFX library, for other
|
||||
* platforms 0.0.0 will be returned.
|
||||
* @param hContext The XeSS context handle.
|
||||
* @param[out] pVersion Returned Intel XeFX library version.
|
||||
* @return XeSS return status code.
|
||||
*/
|
||||
XESS_API xess_result_t xessGetIntelXeFXVersion(xess_context_handle_t hContext,
|
||||
xess_version_t* pVersion);
|
||||
|
||||
/**
|
||||
* @brief Gets XeSS internal resources properties.
|
||||
* @param hContext The XeSS context handle.
|
||||
* @param pOutputResolution Output resolution to calculate properties for.
|
||||
* @param[out] pBindingProperties Returned properties.
|
||||
* @return XeSS return status code.
|
||||
*/
|
||||
XESS_API xess_result_t xessGetProperties(xess_context_handle_t hContext,
|
||||
const xess_2d_t* pOutputResolution, xess_properties_t* pBindingProperties);
|
||||
|
||||
/**
|
||||
* @brief Get the input resolution for a specified output resolution for a given quality setting.
|
||||
* XeSS expects all the input buffers except motion vectors to be in the returned resolution.
|
||||
* Motion vectors can be either in output resolution (XESS_INIT_FLAG_HIGH_RES_MV) or
|
||||
* returned resolution (default).
|
||||
*
|
||||
* @param hContext The XeSS context handle.
|
||||
* @param pOutputResolution Output resolution to calculate input resolution for.
|
||||
* @param qualitySettings Desired quality setting.
|
||||
* @param[out] pInputResolution Required input resolution.
|
||||
* @return XeSS return status code.
|
||||
*/
|
||||
XESS_API xess_result_t xessGetInputResolution(xess_context_handle_t hContext,
|
||||
const xess_2d_t* pOutputResolution, xess_quality_settings_t qualitySettings,
|
||||
xess_2d_t* pInputResolution);
|
||||
|
||||
/**
|
||||
* @brief Get the optimal input resolution and possible range for a specified output resolution for a given quality setting.
|
||||
* XeSS expects all the input buffers except motion vectors to be in the returned resolution range
|
||||
* and all input buffers to be in the same resolution.
|
||||
* Motion vectors can be either in output resolution (XESS_INIT_FLAG_HIGH_RES_MV) or
|
||||
* in the same resolution as other input buffers (by default).
|
||||
*
|
||||
* @note Aspect ratio of the input resolution must be the same as for the output resolution.
|
||||
*
|
||||
* @param hContext The XeSS context handle.
|
||||
* @param pOutputResolution Output resolution to calculate input resolution for.
|
||||
* @param qualitySettings Desired quality setting.
|
||||
* @param[out] pInputResolutionOptimal Optimal input resolution.
|
||||
* @param[out] pInputResolutionMin Required minimal input resolution.
|
||||
* @param[out] pInputResolutionMax Required maximal input resolution.
|
||||
* @return XeSS return status code.
|
||||
*/
|
||||
XESS_API xess_result_t xessGetOptimalInputResolution(xess_context_handle_t hContext,
|
||||
const xess_2d_t* pOutputResolution, xess_quality_settings_t qualitySettings,
|
||||
xess_2d_t* pInputResolutionOptimal, xess_2d_t* pInputResolutionMin, xess_2d_t* pInputResolutionMax);
|
||||
|
||||
/**
|
||||
* @brief Gets jitter scale value.
|
||||
* @param hContext The XeSS context handle.
|
||||
* @param[out] pX Jitter scale pointer for the X axis.
|
||||
* @param[out] pY Jitter scale pointer for the Y axis.
|
||||
* @return XeSS return status code.
|
||||
*/
|
||||
XESS_API xess_result_t xessGetJitterScale(xess_context_handle_t hContext, float* pX, float* pY);
|
||||
|
||||
/**
|
||||
* @brief Gets velocity scale value.
|
||||
* @param hContext The XeSS context handle.
|
||||
* @param[out] pX Velocity scale pointer for the X axis.
|
||||
* @param[out] pY Velocity scale pointer for the Y axis.
|
||||
* @return XeSS return status code.
|
||||
*/
|
||||
XESS_API xess_result_t xessGetVelocityScale(xess_context_handle_t hContext, float* pX, float* pY);
|
||||
|
||||
/**
|
||||
* @brief Destroys the XeSS context.
|
||||
* The user must ensure that any pending command lists are completed before destroying the context.
|
||||
* @param hContext: The XeSS context handle.
|
||||
* @return XeSS return status code.
|
||||
*/
|
||||
XESS_API xess_result_t xessDestroyContext(xess_context_handle_t hContext);
|
||||
|
||||
/**
|
||||
* @brief Sets jitter scale value
|
||||
*
|
||||
* @param hContext The XeSS context handle.
|
||||
* @param x scale for the X axis
|
||||
* @param y scale for the Y axis
|
||||
* @return XeSS return status code.
|
||||
*/
|
||||
XESS_API xess_result_t xessSetJitterScale(xess_context_handle_t hContext, float x, float y);
|
||||
|
||||
/**
|
||||
* @brief Sets velocity scale value
|
||||
*
|
||||
* @param hContext The XeSS context handle.
|
||||
* @param x scale for the X axis
|
||||
* @param y scale for the Y axis
|
||||
* @return XeSS return status code.
|
||||
*/
|
||||
XESS_API xess_result_t xessSetVelocityScale(xess_context_handle_t hContext, float x, float y);
|
||||
|
||||
/**
|
||||
* @brief Sets exposure scale value
|
||||
*
|
||||
* This value will be applied on top of any passed exposure value or automatically calculated exposure.
|
||||
*
|
||||
* @param hContext The XeSS context handle.
|
||||
* @param scale scale value.
|
||||
* @return XeSS return status code.
|
||||
*/
|
||||
XESS_API xess_result_t xessSetExposureMultiplier(xess_context_handle_t hContext, float scale);
|
||||
|
||||
/**
|
||||
* @brief Gets exposure scale value
|
||||
*
|
||||
* @param hContext The XeSS context handle.
|
||||
* @param[out] pScale Exposure scale pointer.
|
||||
* @return XeSS return status code.
|
||||
*/
|
||||
XESS_API xess_result_t xessGetExposureMultiplier(xess_context_handle_t hContext, float *pScale);
|
||||
|
||||
/**
|
||||
* @brief Sets maximum value for responsive mask
|
||||
*
|
||||
* This value used to clip responsive mask values. Final responsive mask value calculated as
|
||||
* clip(responsive_mask, 0.0, max_value). Value must be within range [0.0; 1.0]
|
||||
*
|
||||
* @param hContext The XeSS context handle.
|
||||
* @param value maximum clip value.
|
||||
* @return XeSS return status code.
|
||||
*/
|
||||
XESS_API xess_result_t xessSetMaxResponsiveMaskValue(xess_context_handle_t hContext, float value);
|
||||
|
||||
/**
|
||||
* @brief Gets maximum value for responsive mask
|
||||
*
|
||||
* @param hContext The XeSS context handle.
|
||||
* @param[out] pValue maximum clip value pointer.
|
||||
* @return XeSS return status code.
|
||||
*/
|
||||
XESS_API xess_result_t xessGetMaxResponsiveMaskValue(xess_context_handle_t hContext, float *pValue);
|
||||
|
||||
/**
|
||||
* @brief Sets logging callback
|
||||
*
|
||||
* @param hContext The XeSS context handle.
|
||||
* @param loggingLevel Minimum logging level for logging callback.
|
||||
* @param loggingCallback Logging callback
|
||||
* @return XeSS return status code.
|
||||
*/
|
||||
XESS_API xess_result_t xessSetLoggingCallback(xess_context_handle_t hContext,
|
||||
xess_logging_level_t loggingLevel, xess_app_log_callback_t loggingCallback);
|
||||
|
||||
/**
|
||||
* @brief Indicates if the installed driver supports best XeSS experience.
|
||||
*
|
||||
* @param hContext The XeSS context handle.
|
||||
* @return xessIsOptimalDriver returns XESS_RESULT_SUCCESS, or XESS_RESULT_WARNING_OLD_DRIVER
|
||||
* if installed driver may result in degraded performance or visual quality.
|
||||
* xessD3D12CreateContext(..) will return XESS_RESULT_ERROR_UNSUPPORTED_DRIVER if driver does
|
||||
* not support XeSS at all.
|
||||
*/
|
||||
XESS_API xess_result_t xessIsOptimalDriver(xess_context_handle_t hContext);
|
||||
|
||||
/**
|
||||
* @brief Forces usage of legacy (pre 1.3.0) scale factors
|
||||
*
|
||||
* Following scale factors will be applied:
|
||||
* @li XESS_QUALITY_SETTING_ULTRA_PERFORMANCE: 3.0
|
||||
* @li XESS_QUALITY_SETTING_PERFORMANCE: 2.0
|
||||
* @li XESS_QUALITY_SETTING_BALANCED: 1.7
|
||||
* @li XESS_QUALITY_SETTING_QUALITY: 1.5
|
||||
* @li XESS_QUALITY_SETTING_ULTRA_QUALITY: 1.3
|
||||
* @li XESS_QUALITY_SETTING_AA: 1.0
|
||||
* In order to apply new scale factors application should call xessGetOptimalInputResolution and
|
||||
* initialization function (xess*Init)
|
||||
*
|
||||
* @param hContext The XeSS context handle.
|
||||
* @param force if set to true legacy scale factors will be forced, if set to false - scale factors
|
||||
* will be selected by XeSS
|
||||
*
|
||||
* @return XeSS return status code.
|
||||
*/
|
||||
XESS_API xess_result_t xessForceLegacyScaleFactors(xess_context_handle_t hContext, bool force);
|
||||
|
||||
/**
|
||||
* @brief Returns current state of pipeline build
|
||||
* This function can only be called after xess*BuildPipelines and
|
||||
* before corresponding xess*Init.
|
||||
* This call returns XESS_RESULT_SUCCESS if pipelines already built, and
|
||||
* XESS_RESULT_ERROR_OPERATION_IN_PROGRESS if pipeline build is in progress.
|
||||
* If function called before @ref xess*BuildPipelines or after @ref xess*Init -
|
||||
* XESS_RESULT_ERROR_WRONG_CALL_ORDER will be returned.
|
||||
*
|
||||
* @param hContext The XeSS context handle.
|
||||
* @return XESS_RESULT_SUCCESS if pipelines already built.
|
||||
* XESS_RESULT_ERROR_OPERATION_IN_PROGRESS if pipeline build is in progress.
|
||||
* XESS_RESULT_ERROR_WRONG_CALL_ORDER if the function is called out of order.
|
||||
*/
|
||||
XESS_API xess_result_t xessGetPipelineBuildStatus(xess_context_handle_t hContext);
|
||||
|
||||
/** @}*/
|
||||
|
||||
#endif
|
||||
|
||||
// Enum size checks. All enums must be 4 bytes
|
||||
typedef char sizecheck__LINE__[ (sizeof(xess_quality_settings_t) == 4) ? 1 : -1];
|
||||
typedef char sizecheck__LINE__[ (sizeof(xess_init_flags_t) == 4) ? 1 : -1];
|
||||
typedef char sizecheck__LINE__[ (sizeof(xess_result_t) == 4) ? 1 : -1];
|
||||
typedef char sizecheck__LINE__[ (sizeof(xess_logging_level_t) == 4) ? 1 : -1];
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2026 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef XESS_VK_H
|
||||
#define XESS_VK_H
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
#include "xess.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
XESS_PACK_B()
|
||||
|
||||
typedef struct _xess_vk_image_view_info
|
||||
{
|
||||
VkImageView imageView;
|
||||
VkImage image;
|
||||
VkImageSubresourceRange subresourceRange;
|
||||
VkFormat format;
|
||||
unsigned int width;
|
||||
unsigned int height;
|
||||
} xess_vk_image_view_info;
|
||||
|
||||
XESS_PACK_E()
|
||||
|
||||
XESS_PACK_B()
|
||||
/**
|
||||
* @brief Execution parameters for XeSS Vulkan.
|
||||
*/
|
||||
typedef struct _xess_vk_execute_params_t
|
||||
{
|
||||
/** Input color texture. Must be in VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL state.*/
|
||||
xess_vk_image_view_info colorTexture;
|
||||
/** Input motion vector texture. Must be in VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL state.*/
|
||||
xess_vk_image_view_info velocityTexture;
|
||||
/** Optional depth texture. Required if XESS_INIT_FLAG_HIGH_RES_MV has not been specified.
|
||||
* Must be in VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL state.*/
|
||||
xess_vk_image_view_info depthTexture;
|
||||
/** Optional 1x1 exposure scale texture. Required if XESS_INIT_FLAG_EXPOSURE_TEXTURE has been
|
||||
* specified. Must be in VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL state */
|
||||
xess_vk_image_view_info exposureScaleTexture;
|
||||
/** Optional responsive pixel mask texture. Required if XESS_INIT_FLAG_RESPONSIVE_PIXEL_MASK
|
||||
* has been specified. Must be in VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL state */
|
||||
xess_vk_image_view_info responsivePixelMaskTexture;
|
||||
/** Output texture in target resolution. Must be in VK_IMAGE_LAYOUT_GENERAL state.*/
|
||||
xess_vk_image_view_info outputTexture;
|
||||
|
||||
/** Jitter X coordinate in the range [-0.5, 0.5]. */
|
||||
float jitterOffsetX;
|
||||
/** Jitter Y coordinate in the range [-0.5, 0.5]. */
|
||||
float jitterOffsetY;
|
||||
/** Optional input color scaling. Default is 1. */
|
||||
float exposureScale;
|
||||
/** Resets the history accumulation in this frame. */
|
||||
uint32_t resetHistory;
|
||||
/** Input color width. */
|
||||
uint32_t inputWidth;
|
||||
/** Input color height. */
|
||||
uint32_t inputHeight;
|
||||
/** Base coordinate for the input color in the texture. Default is (0,0). */
|
||||
xess_coord_t inputColorBase;
|
||||
/** Base coordinate for the input motion vector in the texture. Default is (0,0). */
|
||||
xess_coord_t inputMotionVectorBase;
|
||||
/** Base coordinate for the input depth in the texture. Default is (0,0). */
|
||||
xess_coord_t inputDepthBase;
|
||||
/** Base coordinate for the input responsive pixel mask in the texture. Default is (0,0). */
|
||||
xess_coord_t inputResponsiveMaskBase;
|
||||
/** Reserved parameter. */
|
||||
xess_coord_t reserved0;
|
||||
/** Base coordinate for the output color. Default is (0,0). */
|
||||
xess_coord_t outputColorBase;
|
||||
} xess_vk_execute_params_t;
|
||||
XESS_PACK_E()
|
||||
|
||||
XESS_PACK_B()
|
||||
/**
|
||||
* @brief Initialization parameters for XeSS VK.
|
||||
*/
|
||||
typedef struct _xess_vk_init_params_t
|
||||
{
|
||||
/** Output width and height. */
|
||||
xess_2d_t outputResolution;
|
||||
/** Quality setting */
|
||||
xess_quality_settings_t qualitySetting;
|
||||
/** Initialization flags. */
|
||||
uint32_t initFlags;
|
||||
/** Specifies the node mask for internally created resources on
|
||||
* multi-adapter systems. */
|
||||
uint32_t creationNodeMask;
|
||||
/** Specifies the node visibility mask for internally created resources
|
||||
* on multi-adapter systems. */
|
||||
uint32_t visibleNodeMask;
|
||||
/** Optional externally allocated buffer memory for XeSS. If VK_NULL_HANDLE the
|
||||
* memory is allocated internally. If provided, the memory must be allocated from
|
||||
* memory type that supports allocating buffers. The memory type should be DEVICE_LOCAL.
|
||||
* This memory is not accessed by the CPU. */
|
||||
VkDeviceMemory tempBufferHeap;
|
||||
/** Offset in the externally allocated memory for temporary buffer storage. */
|
||||
uint64_t bufferHeapOffset;
|
||||
/** Optional externally allocated texture memory for XeSS. If VK_NULL_HANDLE the
|
||||
* memory is allocated internally. If provided, the memory must be allocated from
|
||||
* memory type that supports allocating textures. The memory type should be DEVICE_LOCAL.
|
||||
* This memory is not accessed by the CPU. */
|
||||
VkDeviceMemory tempTextureHeap;
|
||||
/** Offset in the externally allocated memory for temporary texture storage. */
|
||||
uint64_t textureHeapOffset;
|
||||
/** Optional pipeline cache. If not VK_NULL_HANDLE will be used for pipeline creation. */
|
||||
VkPipelineCache pipelineCache;
|
||||
} xess_vk_init_params_t;
|
||||
XESS_PACK_E()
|
||||
|
||||
/** @addtogroup xess-vulkan XeSS VK API exports
|
||||
* @{
|
||||
*/
|
||||
/**
|
||||
* @brief Get required extensions for Vulkan instance that would run XeSS.
|
||||
* This function must be called to get instance extensions need by XeSS. These
|
||||
* extensions must be enabled in subsequent vkCreateInstance call, that would create a VkInstance
|
||||
* object to be passed to @ref xessVKCreateContext
|
||||
* @param[out] instanceExtensionsCount returns a count of instance extensions to be enabled in Vulkan instance
|
||||
* @param[out] instanceExtensions returns a pointer to an array of \p instanceExtensionsCount required extension names.
|
||||
* The memory used by the array is owned by the XeSS library and should not be freed by the application.
|
||||
* @param[out] minVkApiVersion The Vulkan API version that XeSS would use. When calling vkCreateInstance, the application
|
||||
* should set VkApplicationInfo.apiVersion to value greater or equal to \p minVkApiVersion
|
||||
* @return XeSS return status code.
|
||||
*/
|
||||
XESS_API xess_result_t xessVKGetRequiredInstanceExtensions(uint32_t* instanceExtensionsCount, const char* const** instanceExtensions, uint32_t* minVkApiVersion);
|
||||
|
||||
|
||||
/**
|
||||
* @brief Get required extensions for Vulkan device that would run XeSS.
|
||||
* This function must be called to get device extensions need by XeSS. These
|
||||
* extensions must be enabled in subsequent vkCreateDevice call, that would create a VkDevice
|
||||
* object to be passed to @ref xessVKCreateContext
|
||||
* @param instance A VkInstance object created by the user
|
||||
* @param physicalDevice A VkPhysicalDevice selected by the user from instance
|
||||
* @param[out] deviceExtensionsCount returns a count of device extensions to be enabled in Vulkan device
|
||||
* @param[out] deviceExtensions returns a pointer to an array of \p deviceExtensionsCount required extension names
|
||||
* The memory used by the array is owned by the XeSS library and should not be freed by the application.
|
||||
* @return XeSS return status code.
|
||||
*/
|
||||
XESS_API xess_result_t xessVKGetRequiredDeviceExtensions(VkInstance instance, VkPhysicalDevice physicalDevice,
|
||||
uint32_t* deviceExtensionsCount, const char* const** deviceExtensions);
|
||||
|
||||
|
||||
/**
|
||||
* @brief Get required features for Vulkan device that would run XeSS.
|
||||
* This function must be called to get device features need by XeSS. These
|
||||
* features must be enabled in subsequent vkCreateDevice call, that would create a VkDevice
|
||||
* object to be passed to @ref xessVKCreateContext.
|
||||
* @param instance A VkInstance object created by the user
|
||||
* @param physicalDevice A VkPhysicalDevice selected by the user from instance
|
||||
* @param[out] features a pointer to writable chain of feature structures, that this function would patch
|
||||
* with required features, by filling required fields and attaching new structures to the chain if needed.
|
||||
* The returned pointer should be passed to vkCreateDevice as pNext chain of VkDeviceCreateInfo structure.
|
||||
* If null is passed, than the function constructs a new structure chain that should be merged
|
||||
* into the chain that application would use with VkDeviceCreateInfo, with application responsibility to
|
||||
* avoid any duplicates with its own structures.
|
||||
*
|
||||
* It is an error to chain VkDeviceCreateInfo structure with not null pEnabledFeatures field, as this field is const
|
||||
* and cannot be patched by this function. VkPhysicalDeviceFeatures2 structure should be used instead.
|
||||
*
|
||||
* The memory used by the structures added by this funtion to the chain is owned by the XeSS library and
|
||||
* should not be freed by the application.
|
||||
* @return XeSS return status code.
|
||||
*/
|
||||
XESS_API xess_result_t xessVKGetRequiredDeviceFeatures(VkInstance instance, VkPhysicalDevice physicalDevice, void** features);
|
||||
|
||||
|
||||
/**
|
||||
* @brief Create an XeSS VK context.
|
||||
* @param instance A VkInstance object created by the user
|
||||
* @param physicalDevice A VkPhysicalDevice selected by the user from instance
|
||||
* @param device A VK device created by the user from physicalDevice
|
||||
* @param[out] phContext Returned xess context handle.
|
||||
* @return XeSS return status code.
|
||||
*/
|
||||
XESS_API xess_result_t xessVKCreateContext(VkInstance instance, VkPhysicalDevice physicalDevice, VkDevice device,
|
||||
xess_context_handle_t* phContext);
|
||||
|
||||
/**
|
||||
* @brief Initiates pipeline build process
|
||||
* This function can only be called between @ref xessVKCreateContext and
|
||||
* @ref xessVKInit
|
||||
* This call initiates build of Vulkan pipelines and kernel compilation
|
||||
* This call can be blocking (if @p blocking set to true) or non-blocking.
|
||||
* In case of non-blocking call library will wait for pipeline build on call to
|
||||
* @ref xessVKInit
|
||||
* If @p pipelineCache passed to this call - same pipeline library must be passed
|
||||
* to @ref xessVKInit
|
||||
*
|
||||
* @param hContext The XeSS context handle.
|
||||
* @param pipelineCache Optional pointer to pipeline library for pipeline caching.
|
||||
* @param blocking Wait for kernel compilation and pipeline creation to finish or not
|
||||
* @param initFlags Initialization flags. *Must* be identical to flags passed to @ref xessVKInit
|
||||
*/
|
||||
XESS_API xess_result_t xessVKBuildPipelines(xess_context_handle_t hContext,
|
||||
VkPipelineCache pipelineCache, bool blocking, uint32_t initFlags);
|
||||
|
||||
/**
|
||||
* @brief Initialize XeSS VK.
|
||||
* This is a blocking call that initializes XeSS and triggers internal
|
||||
* resources allocation and JIT for the XeSS kernels. The user must ensure that
|
||||
* any pending command lists are completed before re-initialization. When
|
||||
* During initialization, XeSS can create staging buffers and copy queues to
|
||||
* upload internal data. These will be destroyed at the end of initialization.
|
||||
*
|
||||
* @note XeSS supports devices starting from VK_RESOURCE_HEAP_TIER_1, which means
|
||||
* that buffers and textures can not live in the same resource heap.
|
||||
*
|
||||
* @param hContext: The XeSS context handle.
|
||||
* @param pInitParams: Initialization parameters.
|
||||
* @return XeSS return status code.
|
||||
*/
|
||||
XESS_API xess_result_t xessVKInit(
|
||||
xess_context_handle_t hContext, const xess_vk_init_params_t* pInitParams);
|
||||
|
||||
/**
|
||||
* @brief Get XeSS VK initialization parameters.
|
||||
*
|
||||
* @note This function will return @ref XESS_RESULT_ERROR_UNINITIALIZED if @ref xessVKInit has not been called.
|
||||
*
|
||||
* @param hContext: The XeSS context handle.
|
||||
* @param[out] pInitParams: Returned initialization parameters.
|
||||
* @return XeSS return status code.
|
||||
*/
|
||||
XESS_API xess_result_t xessVKGetInitParams(
|
||||
xess_context_handle_t hContext, xess_vk_init_params_t* pInitParams);
|
||||
|
||||
/**
|
||||
* @brief Record XeSS upscaling commands into the command list.
|
||||
* @param hContext: The XeSS context handle.
|
||||
* @param commandBuffer: The command buffer for XeSS commands.
|
||||
* @param pExecParams: Execution parameters.
|
||||
* @return XeSS return status code.
|
||||
*/
|
||||
|
||||
XESS_API xess_result_t xessVKExecute(xess_context_handle_t hContext,
|
||||
VkCommandBuffer commandBuffer, const xess_vk_execute_params_t* pExecParams);
|
||||
/** @}*/
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
#endif // XESS_VK_H
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,297 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
"""What this PC can do with inFAMOUS Second Son, and the settings for the player's goal.
|
||||
|
||||
The idea and the score scale come from the Bloodborne port's hardware check (Supermedo's
|
||||
bloodborne_pc, launcher/bbport_hardware.py, GPL-2.0-or-later: a score per graphics card and per
|
||||
processor generation); the numbers here are tuned for this port. Second Son leans on the
|
||||
processor in the open city: the reference PC (Ryzen 7 3700X + RX 9060 XT) runs it at about 95 FPS
|
||||
there at 1080p, a Ryzen 5 1600 at about 60. So the processor sets the frame rate, the graphics
|
||||
card the resolution it holds.
|
||||
|
||||
No Tk here (tests/test_launcher_hardware.py); the launcher shows the result and applies it.
|
||||
"""
|
||||
import ctypes
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
# Graphics cards: (regex on the Vulkan name, score, video memory in GB). The score is roughly
|
||||
# relative raster speed (RX 9060 XT 16 GB = 160). The first match wins, so longer names first.
|
||||
GPUS = [
|
||||
(r'rtx 5090', 430, 32), (r'rtx 5080', 300, 16), (r'rtx 5070 ti', 270, 16), (r'rtx 5070', 215, 12),
|
||||
(r'rtx 5060 ti', 162, 16), (r'rtx 5060', 140, 8), (r'rtx 5050', 112, 8),
|
||||
(r'rtx 4090', 360, 24), (r'rtx 4080', 280, 16), (r'rtx 4070 ti', 230, 12), (r'rtx 4070 super', 205, 12),
|
||||
(r'rtx 4070', 175, 12), (r'rtx 4060 ti', 141, 8), (r'rtx 4060', 115, 8), (r'rtx 4050', 92, 6),
|
||||
(r'rtx 3090', 205, 24), (r'rtx 3080 ti', 192, 12), (r'rtx 3080', 178, 10), (r'rtx 3070 ti', 148, 8),
|
||||
(r'rtx 3070', 140, 8), (r'rtx 3060 ti', 125, 8), (r'rtx 3060', 98, 12), (r'rtx 3050', 70, 8),
|
||||
(r'rtx 2080 ti', 155, 11), (r'rtx 2080', 130, 8), (r'rtx 2070', 110, 8), (r'rtx 2060 super', 100, 8),
|
||||
(r'rtx 2060', 88, 6), (r'gtx 1660', 72, 6), (r'gtx 1650', 52, 4), (r'gtx 1080 ti', 118, 11),
|
||||
(r'gtx 1080', 92, 8), (r'gtx 1070', 80, 8), (r'gtx 1060', 58, 6), (r'gtx 1050', 34, 4),
|
||||
(r'gtx 9[78]0', 56, 4),
|
||||
(r'rx 9070 xt', 275, 16), (r'rx 9070', 245, 16), (r'rx 9060 xt', 160, 16), (r'rx 9060', 140, 8),
|
||||
(r'rx 7900 xtx', 290, 24), (r'rx 7900', 250, 20), (r'rx 7800 xt', 205, 16), (r'rx 7700', 170, 12),
|
||||
(r'rx 7600', 122, 8), (r'rx 69[05]0 xt', 220, 16), (r'rx 6800 xt', 200, 16), (r'rx 6800', 175, 16),
|
||||
(r'rx 67[05]0', 145, 12), (r'rx 66[05]0 xt', 118, 8), (r'rx 6600', 100, 8), (r'rx 65[05]0', 48, 4),
|
||||
(r'rx 64[05]0', 40, 4), (r'rx 5700', 108, 8), (r'rx 5600', 88, 6), (r'rx 5500', 62, 4),
|
||||
(r'vega', 90, 8), (r'rx 5[789]0', 66, 8), (r'rx 4[78]0', 60, 8),
|
||||
(r'arc b580', 130, 12), (r'arc b570', 115, 10), (r'arc a770', 120, 16), (r'arc a750', 108, 8),
|
||||
(r'arc a580', 95, 8), (r'arc a380', 45, 6),
|
||||
(r'radeon 8[0-9]0m', 60, 4), (r'radeon 7[0-9]0m', 45, 4), (r'radeon(\(tm\))? graphics', 25, 2),
|
||||
(r'intel.*(iris|uhd|arc graphics)', 25, 2),
|
||||
]
|
||||
|
||||
# Processors: score of the desktop chip (Ryzen 7 3700X = 130). Intel Core per generation,
|
||||
# Ryzen per series, each with its i3/i5/i7/i9 or Ryzen 3/5/7/9 tier.
|
||||
INTEL_CORE = {14: (142, 192, 222, 235), 13: (138, 188, 215, 230), 12: (132, 168, 195, 205),
|
||||
11: (105, 135, 145, 148), 10: (100, 120, 132, 140), 9: (95, 112, 122, 130),
|
||||
8: (92, 108, 118, 118), 7: (78, 92, 105, 120), 6: (72, 88, 98, 98), 5: (62, 78, 88, 88),
|
||||
4: (62, 78, 88, 88)}
|
||||
RYZEN = {9000: (200, 215, 235, 242), 8000: (140, 172, 185, 185), 7000: (150, 200, 222, 228),
|
||||
6000: (130, 140, 155, 160), 5000: (118, 160, 180, 188), 4000: (85, 105, 115, 118),
|
||||
3000: (100, 120, 130, 138), 2000: (72, 92, 102, 102), 1000: (68, 85, 95, 95)}
|
||||
CORE_ULTRA = {'2': (188, 215, 228), '1': (125, 140, 148)} # Core Ultra 5 / 7 / 9 by series
|
||||
|
||||
# City frame rate per point of score (calibrated on the tester's 3700X: ~95 FPS) and the
|
||||
# graphics card's at 1080p (RX 9060 XT: ~120 FPS when the processor allows it).
|
||||
CPU_FPS_PER_POINT = 0.73
|
||||
GPU_FPS_PER_POINT = 0.75
|
||||
LAPTOP_SHARE = 0.75 # laptop chips hold lower clocks under load
|
||||
|
||||
|
||||
@dataclass
|
||||
class Pc:
|
||||
gpu_name: str = ''
|
||||
gpu_score: int = 0
|
||||
vram_gb: int = 0
|
||||
integrated: bool = False
|
||||
cpu_name: str = ''
|
||||
cpu_score: int = 0
|
||||
ram_gb: int = 0
|
||||
notes: list = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def vendor(self):
|
||||
name = self.gpu_name.lower()
|
||||
return 'nvidia' if 'nvidia' in name or 'geforce' in name else 'amd' if re.search(r'amd|radeon', name) \
|
||||
else 'intel' if 'intel' in name else ''
|
||||
|
||||
@property
|
||||
def fsr4(self):
|
||||
"""FSR 4 runs on RX 7000 and RX 9000 cards."""
|
||||
return bool(re.search(r'rx [79]\d{3}', self.gpu_name.lower()))
|
||||
|
||||
@property
|
||||
def dlss(self):
|
||||
return bool(re.search(r'rtx', self.gpu_name.lower()))
|
||||
|
||||
|
||||
def gpu_entry(name):
|
||||
"""(score, video memory GB) for a Vulkan device name, or (0, 0) when unknown."""
|
||||
text = ' '.join(re.sub(r'\((r|tm)\)', ' ', name.lower()).split())
|
||||
for pattern, score, vram in GPUS:
|
||||
if re.search(pattern, text):
|
||||
if re.search(r'laptop|mobile|max-q', text):
|
||||
score = round(score * LAPTOP_SHARE)
|
||||
return score, vram
|
||||
return 0, 0
|
||||
|
||||
|
||||
def cpu_score(name):
|
||||
"""The processor's score from its Windows name, or 0 when unknown."""
|
||||
n = ' '.join(re.sub(r'\((R|TM|tm|r)\)', ' ', name).split())
|
||||
laptop = False
|
||||
score = 0
|
||||
m = re.search(r'\bCore\s+Ultra\s+([579])\s+(\d)\d\d', n)
|
||||
if m:
|
||||
score = CORE_ULTRA.get(m.group(2), CORE_ULTRA['2'])['579'.index(m.group(1))]
|
||||
elif (m := re.search(r'\bi([3579])-(\d{4,5})([A-Z]*)', n)):
|
||||
tier, digits = '3579'.index(m.group(1)), m.group(2)
|
||||
gen = int(digits[:2]) if len(digits) == 5 else int(digits[0])
|
||||
scores = INTEL_CORE.get(min(gen, 14))
|
||||
score = scores[tier] if scores else 0
|
||||
laptop = bool(re.match(r'(H(?!X)|U|Y|P|M|G\d)', m.group(3)))
|
||||
elif (m := re.search(r'\bRyzen\s+([3579])\s+(?:PRO\s+)?(\d)\d{3}([A-Z]*)', n)):
|
||||
scores = RYZEN.get(int(m.group(2)) * 1000)
|
||||
score = scores['3579'.index(m.group(1))] if scores else 0
|
||||
laptop = bool(re.match(r'(H(?!X)|U|HS)', m.group(3)))
|
||||
elif re.search(r'\bRyzen\s+AI\b', n):
|
||||
score = 185
|
||||
return round(score * LAPTOP_SHARE) if laptop and score else score
|
||||
|
||||
|
||||
# ---- detection on this PC -----------------------------------------------------------------------
|
||||
def processor_name():
|
||||
if sys.platform == 'win32':
|
||||
try:
|
||||
import winreg
|
||||
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'HARDWARE\DESCRIPTION\System\CentralProcessor\0') as key:
|
||||
return ' '.join(str(winreg.QueryValueEx(key, 'ProcessorNameString')[0]).split())
|
||||
except OSError:
|
||||
return ''
|
||||
try:
|
||||
with open('/proc/cpuinfo') as f:
|
||||
for line in f:
|
||||
if line.startswith('model name'):
|
||||
return ' '.join(line.split(':', 1)[1].split())
|
||||
except OSError:
|
||||
pass
|
||||
return ''
|
||||
|
||||
|
||||
def memory_gb():
|
||||
"""Installed memory in GB."""
|
||||
if sys.platform == 'win32':
|
||||
try:
|
||||
kb = ctypes.c_ulonglong()
|
||||
if ctypes.windll.kernel32.GetPhysicallyInstalledSystemMemory(ctypes.byref(kb)):
|
||||
return round(kb.value / 1024 ** 2)
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
return 0
|
||||
try:
|
||||
with open('/proc/meminfo') as f:
|
||||
for line in f:
|
||||
if line.startswith('MemTotal:'):
|
||||
return round(int(line.split()[1]) / 1024 ** 2)
|
||||
except OSError:
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
def detect(gpus):
|
||||
"""A Pc for this machine; `gpus` is the launcher's parse_gpu_list rows (index, name, type, ...),
|
||||
the game's card first when the tool marks it."""
|
||||
pc = Pc()
|
||||
rows = sorted(gpus, key=lambda row: (not (len(row) > 4 and row[4] == '1'), 'discrete' not in row[2]))
|
||||
if rows:
|
||||
pc.gpu_name = rows[0][1]
|
||||
pc.integrated = 'discrete' not in rows[0][2]
|
||||
pc.gpu_score, pc.vram_gb = gpu_entry(pc.gpu_name)
|
||||
pc.cpu_name = processor_name()
|
||||
pc.cpu_score = cpu_score(pc.cpu_name)
|
||||
pc.ram_gb = memory_gb()
|
||||
return pc
|
||||
|
||||
|
||||
# ---- what to expect, and the settings for a goal ------------------------------------------------
|
||||
RESOLUTION_PIXELS = {'720': 1280 * 720, '900': 1600 * 900, '': 1920 * 1080, '1440': 2560 * 1440,
|
||||
'1800': 3200 * 1800, '2160': 3840 * 2160}
|
||||
|
||||
|
||||
def city_fps(pc):
|
||||
"""The frame rate the processor allows in the open city (None: unknown processor)."""
|
||||
return round(pc.cpu_score * CPU_FPS_PER_POINT) if pc.cpu_score else None
|
||||
|
||||
|
||||
def gpu_fps(pc, render_height=''):
|
||||
"""The frame rate the graphics card holds at a render size (None: unknown card). Frame time
|
||||
grows a little slower than the pixel count (part of it does not depend on the size)."""
|
||||
if not pc.gpu_score:
|
||||
return None
|
||||
ratio = RESOLUTION_PIXELS.get(render_height, RESOLUTION_PIXELS['']) / RESOLUTION_PIXELS['']
|
||||
return round(pc.gpu_score * GPU_FPS_PER_POINT / (0.25 + 0.75 * ratio))
|
||||
|
||||
|
||||
def expected_fps(pc, render_height=''):
|
||||
values = [v for v in (city_fps(pc), gpu_fps(pc, render_height)) if v]
|
||||
return min(values) if values else None
|
||||
|
||||
|
||||
def limit(pc):
|
||||
"""'cpu' or 'gpu': what sets the frame rate at 1080p, or '' when unknown."""
|
||||
c, g = city_fps(pc), gpu_fps(pc)
|
||||
if not c or not g:
|
||||
return ''
|
||||
return 'cpu' if c <= g else 'gpu'
|
||||
|
||||
|
||||
TIERS = ('unknown', 'below', 'minimum', 'good', 'great')
|
||||
|
||||
|
||||
def tier(pc):
|
||||
fps = expected_fps(pc)
|
||||
if fps is None:
|
||||
return 'unknown'
|
||||
if fps < 30 or (pc.ram_gb and pc.ram_gb < 12) or pc.integrated:
|
||||
return 'below'
|
||||
if fps < 55:
|
||||
return 'minimum'
|
||||
if fps < 90:
|
||||
return 'good'
|
||||
return 'great'
|
||||
|
||||
|
||||
GOALS = ('smooth', 'balanced', 'quiet')
|
||||
|
||||
|
||||
def paced_rate(refresh_hz, cpu_fps):
|
||||
"""(divider, frame rate) for the even frame pacing on a fast screen: half the refresh rate, or
|
||||
a third when the processor cannot hold half in the city; (1, 0) on a 60-75 Hz screen."""
|
||||
if not refresh_hz or refresh_hz < 100:
|
||||
return 1, 0
|
||||
for divider in (2, 3, 4):
|
||||
rate = refresh_hz / divider
|
||||
if rate <= max(cpu_fps, 40) or divider == 4:
|
||||
return divider, round(rate)
|
||||
return 1, 0
|
||||
|
||||
|
||||
def settings_for(pc, goal, screen_height=1080, refresh_hz=60):
|
||||
"""The launcher settings (app keys and issn.ini keys) for the goal on this PC, and the reasons,
|
||||
as (settings, reasons). recommended: what plays best on most PCs (the testers' pick, 10-30):
|
||||
the game's Balanced performance mode, original detail, shaders in the background and, on a
|
||||
screen of 100 Hz or more, every frame shown for the same time (half the refresh rate);
|
||||
smooth: the highest frame rate up to 120; balanced: 60 FPS with the sharpest picture the card
|
||||
holds; quiet: 60 FPS with less work for the card."""
|
||||
app, ini, reasons = {}, {}, []
|
||||
cpu = city_fps(pc) or 60
|
||||
if goal == 'recommended':
|
||||
divider, rate = paced_rate(refresh_hz, cpu)
|
||||
chosen, reasons = settings_for(pc, 'balanced', screen_height)
|
||||
app, ini = chosen['app'], chosen['ini']
|
||||
if app['perf_mode'] == '':
|
||||
app['perf_mode'] = 'low'
|
||||
app['draw_distance'] = ''
|
||||
app['shader_compile'] = ''
|
||||
ini['vsync'] = '1'
|
||||
if divider > 1:
|
||||
# The pacing holds the rate; the limit stays out of its way.
|
||||
app['frame_pacing'] = str(divider)
|
||||
ini['fps_limit'] = '0'
|
||||
reasons.append(('paced', (refresh_hz, rate)))
|
||||
else:
|
||||
app['frame_pacing'] = ''
|
||||
ini['fps_limit'] = '60'
|
||||
return {'app': app, 'ini': ini}, reasons
|
||||
heights = [h for h in ('720', '900', '', '1440', '1800', '2160')
|
||||
if (int(h) if h else 1080) <= max(screen_height, 1080)]
|
||||
target = 120 if goal == 'smooth' else 60
|
||||
if goal == 'smooth' and cpu < 100:
|
||||
target = 60 if cpu < 75 else 90
|
||||
reasons.append(('cpu', target))
|
||||
# The sharpest render size the card holds at the target (with a margin).
|
||||
margin = 1.15 if goal == 'quiet' else 1.0
|
||||
pick = '720'
|
||||
for h in heights:
|
||||
g = gpu_fps(pc, h)
|
||||
if g is None or g >= target * margin:
|
||||
pick = h
|
||||
if goal == 'quiet' and pick not in ('720', '900', ''):
|
||||
pick = ''
|
||||
app['render_height'] = pick
|
||||
# Anti-aliasing: FSR 4 on the cards that run it, else the game's own.
|
||||
app['anti_aliasing'] = 'fsr4' if pc.fsr4 else 'game'
|
||||
app['upscale_quality'] = ''
|
||||
# The game's lower settings when the processor cannot hold the target in the city.
|
||||
app['perf_mode'] = '' if cpu >= target * 1.05 else 'low' if cpu >= target * 0.8 else 'office'
|
||||
if app['perf_mode']:
|
||||
reasons.append(('perf', app['perf_mode']))
|
||||
app['aniso'] = '16' if (gpu_fps(pc, pick) or 0) >= target * 1.3 else '8'
|
||||
ini['fps_limit'] = str(target if target in (60, 120) else 120)
|
||||
ini['vsync'] = '1'
|
||||
app['frame_pacing'] = ''
|
||||
if pc.ram_gb and pc.ram_gb < 16:
|
||||
reasons.append(('ram', pc.ram_gb))
|
||||
if pc.vram_gb and pc.vram_gb < 6:
|
||||
reasons.append(('vram', pc.vram_gb))
|
||||
return {'app': app, 'ini': ini}, reasons
|
||||
@@ -0,0 +1,168 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
"""Help for the problems players run into: the launcher checks the installation and reads the
|
||||
last game log for the messages it knows, and the Help page shows each problem with what it means
|
||||
and the steps that fix it. After the Bloodborne port's Help page (Supermedo's bloodborne_pc,
|
||||
launcher/bbport_help.py, GPL-2.0-or-later); the problems and the log messages are this port's.
|
||||
|
||||
No Tk here (tests/test_launcher_help.py). Texts are English; the launcher translates them.
|
||||
"""
|
||||
import hashlib
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
# The 01.07 eboot.bin (CUSA00223 and CUSA00004 share it) as the update unpacks it.
|
||||
EBOOT_107_SHA256 = '2d1ca79630d7bbe6fa29575f59aa7996d43071d74ac8e4f7d40137967d9039ab'
|
||||
|
||||
# key: (title, what it means, [steps]).
|
||||
TOPICS = {
|
||||
'no_game': (
|
||||
'No game folder yet',
|
||||
'The launcher needs the folder of your own dump of the game: the one with eboot.bin, sce_module and '
|
||||
'sce_sys.',
|
||||
['Game > Game folder: choose the folder that has eboot.bin in it.',
|
||||
'If the 01.07 update is in a folder of its own, choose it in Game > Update folder.']),
|
||||
'game_incomplete': (
|
||||
'The game folder is incomplete',
|
||||
'eboot.bin is there but sce_sys\\param.sfo or the art folder is missing: the dump was not copied whole, '
|
||||
'or this is the update alone.',
|
||||
['Copy the whole dump again (eboot.bin, sce_module, sce_sys and art).',
|
||||
'With the update in a folder of its own: the base game in Game folder, the update in Update folder.']),
|
||||
'version': (
|
||||
'The game is not on the 01.07 update',
|
||||
'The port is made for the 01.07 update (US CUSA00223 or EU CUSA00004). Another version stops at the start.',
|
||||
['Apply the 01.07 update to your dump (the eboot.bin of 01.07 replaces the old one).',
|
||||
'Or choose the folder of the update in Game > Update folder.']),
|
||||
'cpu': (
|
||||
'This processor lacks instructions the game needs',
|
||||
'The game\'s code uses instruction sets this processor does not have, and the port could not translate '
|
||||
'them all.',
|
||||
['Send your last_run.log on the Discord: the line starting with "STOP" names what is missing.']),
|
||||
'crash': (
|
||||
'The game closed with an error',
|
||||
'The log ends with a fault. Some are known and fixed in newer versions; others need the log to be found.',
|
||||
['Update the port (What\'s new) if a newer version is out.',
|
||||
'Save a report (below) and send it on the Discord with what you were doing when it closed.']),
|
||||
'freeze': (
|
||||
'The game stopped responding',
|
||||
'No new frame for a long time: the game waited for something that never came.',
|
||||
['Update the port if a newer version is out.',
|
||||
'Save a report and send it on the Discord, with where in the game it froze.']),
|
||||
'device_lost': (
|
||||
'The graphics driver stopped the game',
|
||||
'The graphics card reported "device lost": the driver reset itself, often from running out of video '
|
||||
'memory or an unstable overclock.',
|
||||
['Lower Graphics > Resolution, or use FSR 4 / DLSS quality.',
|
||||
'Update the graphics driver; remove a GPU overclock or undervolt for a test.']),
|
||||
'memory': (
|
||||
'Not enough memory',
|
||||
'The game reserves several GB at its start, more at high resolutions.',
|
||||
['Close other programs (browsers, recorders) before playing.',
|
||||
'Let Windows manage the page file (System > Advanced > Performance > Virtual memory: automatic).',
|
||||
'Lower Graphics > Resolution: 1440p and above need more memory.']),
|
||||
'no_seed': (
|
||||
'The shaders were not prepared before playing',
|
||||
'This start had no shader seed, so every new place and effect compiled its shaders while you played '
|
||||
'(stutter in the first hours).',
|
||||
['Install the release zip again over the folder (the seed is in user\\cache), or update from the '
|
||||
'launcher.',
|
||||
'Let the first start finish its shader preparation (a few minutes, with a progress screen).']),
|
||||
'skipping_frames': (
|
||||
'The picture was not shown for a while',
|
||||
'The window was minimized, covered or lost by the graphics driver, so frames were skipped. If the screen '
|
||||
'stayed black while the game was in front, this is the cause.',
|
||||
['Turn Graphics > Allow HDR output off.',
|
||||
'Play in Borderless instead of Fullscreen (Graphics > Display mode).',
|
||||
'Update the graphics driver.']),
|
||||
'dlss_files': (
|
||||
'DLSS could not start',
|
||||
'DLSS needs NVIDIA\'s Streamline files of the exact version the port uses; the launcher downloads them.',
|
||||
['Graphics > Download DLSS now (the launcher fetches Streamline 2.14.16 from NVIDIA\'s GitHub).',
|
||||
'A newer Streamline crashed the game for a tester: the launcher uses 2.14.16.']),
|
||||
'xess_files': (
|
||||
'XeSS could not start',
|
||||
'XeSS needs Intel\'s libxess.dll, which the launcher downloads, and a graphics card with the Vulkan '
|
||||
'features it uses.',
|
||||
['Graphics > Download XeSS now (the launcher fetches it from Intel\'s GitHub).',
|
||||
'Update the graphics driver. If XeSS still does not start, pick FSR 4 or the game\'s own.']),
|
||||
'location': (
|
||||
'The port is in a folder that causes problems',
|
||||
'Inside a zip or temporary folder, Program Files or a synced folder (OneDrive) the port cannot write '
|
||||
'its files reliably.',
|
||||
['Extract the zip to a normal folder, for example C:\\Games\\inFAMOUS-PC, and start it from there.']),
|
||||
}
|
||||
|
||||
# Log lines (regular expressions) and the topic each means. The first match per topic counts.
|
||||
LOG_HINTS = [
|
||||
(r'version \S+: this port needs the game\'s 01\.07 update', 'version'),
|
||||
(r'^STOP: this CPU lacks', 'cpu'),
|
||||
(r'^STOP: no new frame for|^STOP: watchdog timeout', 'freeze'),
|
||||
(r'^Fault: |^Guest fault \(signal|^Host fault \(signal|^STOP: abort\(\)|^STOP: std::terminate', 'crash'),
|
||||
(r'Device lost|ErrorDeviceLost', 'device_lost'),
|
||||
(r'out of memory|error 1450|ERROR_NO_SYSTEM_RESOURCES|ErrorOutOfDeviceMemory', 'memory'),
|
||||
(r'CacheRebuild: no valid seed-stamp\.bin', 'no_seed'),
|
||||
(r'Skipping frame! \((?:[6-9]\d{2}|\d{4,}) in a row', 'skipping_frames'),
|
||||
(r'sl\.interposer.*(not found|missing)|Streamline.*(failed to initialize|not supported)', 'dlss_files'),
|
||||
(r'XeSS: .*libxess\.(dll|so) not found|XeSS: not available|XeSS unavailable: ', 'xess_files'),
|
||||
]
|
||||
TOPIC_ORDER = ['location', 'no_game', 'game_incomplete', 'version', 'cpu', 'memory', 'crash', 'freeze',
|
||||
'device_lost', 'skipping_frames', 'no_seed', 'dlss_files', 'xess_files']
|
||||
|
||||
|
||||
def hints_in_log(text):
|
||||
"""The topics the log's lines point to, in TOPIC_ORDER."""
|
||||
found = set()
|
||||
for line in (text or '').splitlines():
|
||||
for pattern, topic in LOG_HINTS:
|
||||
if topic not in found and re.search(pattern, line):
|
||||
found.add(topic)
|
||||
# Skipped frames while minimized are normal: only when the game also ended badly or the log
|
||||
# holds no "minimized" note.
|
||||
if 'skipping_frames' in found and re.search(r'surface is 0x0 \(minimized', text or ''):
|
||||
found.discard('skipping_frames')
|
||||
return [t for t in TOPIC_ORDER if t in found]
|
||||
|
||||
|
||||
def crash_line(text):
|
||||
"""The log's first fault or STOP line (shown with the crash topic), or ''."""
|
||||
for line in (text or '').splitlines():
|
||||
if re.match(r'(Fault: |Guest fault|Host fault|STOP: )', line):
|
||||
return line.strip()
|
||||
return ''
|
||||
|
||||
|
||||
def game_problems(game_dir, update_dir=''):
|
||||
"""Topics for the game folder: none chosen, incomplete, not 01.07."""
|
||||
if not game_dir or not (Path(game_dir) / 'eboot.bin').is_file():
|
||||
return ['no_game']
|
||||
game = Path(game_dir)
|
||||
problems = []
|
||||
if not (game / 'sce_sys' / 'param.sfo').is_file() or not (game / 'art').is_dir():
|
||||
problems.append('game_incomplete')
|
||||
eboot = Path(update_dir) / 'eboot.bin' if update_dir and (Path(update_dir) / 'eboot.bin').is_file() \
|
||||
else game / 'eboot.bin'
|
||||
try:
|
||||
digest = hashlib.sha256(eboot.read_bytes()).hexdigest()
|
||||
except OSError:
|
||||
digest = ''
|
||||
if digest and digest != EBOOT_107_SHA256:
|
||||
problems.append('version')
|
||||
return problems
|
||||
|
||||
|
||||
ARCHIVE_FOLDER = re.compile(r'[\\/](?:rar\$ex[^\\/]*|7zo[^\\/]*|temp\d*_[^\\/]*\.zip)(?:[\\/]|$)', re.I)
|
||||
|
||||
|
||||
def location_problem(port_dir):
|
||||
"""True when the port runs from a temporary, protected or synced folder."""
|
||||
text = str(port_dir)
|
||||
return bool(ARCHIVE_FOLDER.search(text) or re.search(r'[\\/]Program Files( \(x86\))?[\\/]', text, re.I)
|
||||
or re.search(r'[\\/]OneDrive[^\\/]*[\\/]', text, re.I)
|
||||
or re.search(r'[\\/]AppData[\\/]Local[\\/]Temp[\\/]', text, re.I))
|
||||
|
||||
|
||||
def check(port_dir, game_dir, update_dir, log_text):
|
||||
"""Every topic for this installation and its last log, in TOPIC_ORDER."""
|
||||
found = set(game_problems(game_dir, update_dir)) | set(hints_in_log(log_text))
|
||||
if location_problem(port_dir):
|
||||
found.add('location')
|
||||
return [t for t in TOPIC_ORDER if t in found]
|
||||
+472
-8
@@ -759,6 +759,33 @@ EXTRA = {
|
||||
'Very long (least pop-in, slower)': 'Muito longa (quase sem pop-in, mais lento)',
|
||||
"Replaces the game's own temporal anti-aliasing. FSR 4 (included) needs a Radeon RX 7000 or 9000; DLSS needs an NVIDIA RTX card and NVIDIA's files (below). Without what they need the game keeps its own.": 'Substitui o anti-aliasing temporal do próprio jogo. O FSR 4 (já incluído) precisa de uma Radeon RX 7000 ou 9000; o DLSS precisa de uma placa NVIDIA RTX e dos arquivos da NVIDIA (abaixo). Sem o que eles precisam, o jogo usa o dele.',
|
||||
'Open the DLSS folder': 'Abrir a pasta do DLSS',
|
||||
"Replaces the game's own temporal anti-aliasing. FSR 4 (included) needs a Radeon RX 7000 or 9000; XeSS runs on most recent cards (Arc, Radeon RX 5000+, GeForce GTX 10+) with Intel's file; DLSS needs an NVIDIA RTX card and NVIDIA's files (below). Without what they need the game keeps its own.": 'Substitui o anti-aliasing temporal do próprio jogo. O FSR 4 (já incluído) precisa de uma Radeon RX 7000 ou 9000; o XeSS roda na maioria das placas recentes (Arc, Radeon RX 5000+, GeForce GTX 10+) com o arquivo da Intel; o DLSS precisa de uma placa NVIDIA RTX e dos arquivos da NVIDIA (abaixo). Sem o que eles precisam, o jogo usa o dele.',
|
||||
'XeSS (Intel; also Radeon and GeForce)': 'XeSS (Intel; também Radeon e GeForce)',
|
||||
'Open the XeSS folder': 'Abrir a pasta do XeSS',
|
||||
'Download {} now': 'Baixar {} agora',
|
||||
'Open the {} folder': 'Abrir a pasta do {}',
|
||||
'{} files: ready': 'Arquivos do {}: prontos',
|
||||
'{} files: not downloaded yet': 'Arquivos do {}: ainda não baixados',
|
||||
'Downloading {} files… {}%': 'Baixando os arquivos do {}… {}%',
|
||||
'Downloading {} files…': 'Baixando os arquivos do {}…',
|
||||
'{} download failed: {}': 'Falha ao baixar o {}: {}',
|
||||
"XeSS and DLSS use Intel's and NVIDIA's own files, which the port may not include: the launcher downloads them from Intel's and NVIDIA's GitHub releases (XeSS SDK 3.0.2, about 60 MB; Streamline 2.14.16, about 35 MB) when you pick one, and checks each file.": 'O XeSS e o DLSS usam arquivos da própria Intel e da NVIDIA, que o port não pode incluir: o launcher baixa direto dos lançamentos deles no GitHub (XeSS SDK 3.0.2, uns 60 MB; Streamline 2.14.16, uns 35 MB) quando você escolhe um, e confere cada arquivo.',
|
||||
'Recommended (best on most PCs)': 'Recomendado (o melhor na maioria dos PCs)',
|
||||
'Frame generation (FSR 3.1, test)': 'Geração de quadros (FSR 3.1, teste)',
|
||||
'Makes a frame between every two of the game: 60 FPS on the screen become 120. Needs FSR 4, XeSS or DLSS as anti-aliasing (Graphics). Adds a little input delay; best with a base of 60 FPS or more, and with Frame pacing at half on fast screens (72 becomes 144 on 144 Hz).': 'Cria um quadro entre cada dois do jogo: 60 FPS na tela viram 120. Precisa de FSR 4, XeSS ou DLSS no anti-aliasing (Gráficos). Aumenta um pouco o atraso dos comandos; fica melhor com 60 FPS ou mais de base e, em telas rápidas, com o Ritmo dos quadros na metade (72 vira 144 em 144 Hz).',
|
||||
'Frame pacing: {}': 'Ritmo dos quadros: {}',
|
||||
'Shader compilation: in the background': 'Compilação de shaders: em segundo plano',
|
||||
'a {} Hz screen shows {} FPS evenly': 'uma tela de {} Hz mostra {} FPS por igual',
|
||||
"DLSS needs NVIDIA's Streamline files of the exact version the port uses; the launcher downloads them.": 'O DLSS precisa dos arquivos do Streamline da NVIDIA na versão exata que o port usa; o launcher baixa eles.',
|
||||
"Graphics > Download DLSS now (the launcher fetches Streamline 2.14.16 from NVIDIA's GitHub).": 'Gráficos > Baixar DLSS agora (o launcher baixa o Streamline 2.14.16 do GitHub da NVIDIA).',
|
||||
'A newer Streamline crashed the game for a tester: the launcher uses 2.14.16.': 'Um Streamline mais novo fez o jogo fechar para um testador: o launcher usa o 2.14.16.',
|
||||
"XeSS needs Intel's libxess.dll, which the launcher downloads, and a graphics card with the Vulkan features it uses.": 'O XeSS precisa do libxess.dll da Intel, que o launcher baixa, e de uma placa de vídeo com os recursos de Vulkan que ele usa.',
|
||||
"Graphics > Download XeSS now (the launcher fetches it from Intel's GitHub).": 'Gráficos > Baixar XeSS agora (o launcher baixa do GitHub da Intel).',
|
||||
'Download page (XeSS SDK)': 'Página de download (XeSS SDK)',
|
||||
'XeSS file: ready': 'Arquivo do XeSS: pronto',
|
||||
'XeSS file missing: libxess.dll': 'Falta o arquivo do XeSS: libxess.dll',
|
||||
'XeSS: from the XeSS SDK zip, folder bin, copy libxess.dll into the XeSS folder.': 'XeSS: do zip do XeSS SDK, pasta bin, copie o libxess.dll para a pasta do XeSS.',
|
||||
'With FSR 4, XeSS or DLSS (Anti-aliasing below): the game draws smaller than the window and the upscaler rebuilds a sharp picture at the window size. Quality looks closest to native, Ultra Performance is the fastest. When not Off it decides the size instead of Resolution.': 'Com FSR 4, XeSS ou DLSS (Anti-aliasing abaixo): o jogo desenha menor que a janela e o upscaler reconstrói uma imagem nítida no tamanho da janela. Qualidade é a mais próxima do nativo, Ultra Desempenho é a mais rápida. Quando não está Desligado, ela decide o tamanho no lugar da Resolução.',
|
||||
'Download page (Streamline 2.14.16)': 'Página de download (Streamline 2.14.16)',
|
||||
'DLSS files: ready': 'Arquivos do DLSS: prontos',
|
||||
'DLSS files missing: {}': 'Faltam arquivos do DLSS: {}',
|
||||
@@ -778,29 +805,56 @@ EXTRA = {
|
||||
'Screenshots: F12 (saved in the user/screenshots folder).': 'Prints: F12 (salvos na pasta user/screenshots).',
|
||||
'The game writes its log to {}': 'O jogo grava o log em {}',
|
||||
'Internal resolution': 'Resolução interna',
|
||||
'1064 × 600 (like 800 × 600, the fastest)': '1064 × 600 (como 800 × 600, o mais rápido)',
|
||||
'1280 × 720 (720p)': '1280 × 720 (720p)',
|
||||
'1368 × 768': '1368 × 768',
|
||||
'1600 × 900': '1600 × 900',
|
||||
"1920 × 1080 (1080p, the game's own, default)": '1920 × 1080 (1080p, a do próprio jogo, padrão)',
|
||||
'2560 × 1440 (2K)': '2560 × 1440 (2K)',
|
||||
'3200 × 1800': '3200 × 1800',
|
||||
'3840 × 2160 (4K)': '3840 × 2160 (4K)',
|
||||
'FSR 4 / DLSS quality': 'Qualidade do FSR 4 / DLSS',
|
||||
'Off: the game draws at the Resolution above': 'Desligado: o jogo desenha na Resolução acima',
|
||||
'Quality (window ÷ 1.5)': 'Qualidade (janela ÷ 1,5)',
|
||||
'Balanced (window ÷ 1.7)': 'Equilibrado (janela ÷ 1,7)',
|
||||
'Performance (window ÷ 2)': 'Desempenho (janela ÷ 2)',
|
||||
'Ultra Performance (window ÷ 3)': 'Ultra Desempenho (janela ÷ 3)',
|
||||
'The size the game draws at. Lower is faster; higher is sharper and heavier (2K and 4K need more memory, given by itself). The picture is then fitted to the window, and the HUD keeps its layout. On wider screens the width grows with the shape.': 'O tamanho em que o jogo desenha. Menor é mais rápido; maior é mais nítido e mais pesado (2K e 4K precisam de mais memória, que o port dá sozinho). Depois a imagem é ajustada à janela, e o HUD mantém o layout. Em telas mais largas a largura cresce com o formato.',
|
||||
'With FSR 4 or DLSS (Anti-aliasing below): the game draws smaller than the window and the upscaler rebuilds a sharp picture at the window size. Quality looks closest to native, Ultra Performance is the fastest. When not Off it decides the size instead of Resolution.': 'Com FSR 4 ou DLSS (Anti-aliasing abaixo): o jogo desenha menor que a janela e o upscaler reconstrói uma imagem nítida no tamanho da janela. Qualidade é a mais próxima do nativo, Ultra Desempenho é a mais rápida. Quando não está Desligado, ela decide o tamanho no lugar da Resolução.',
|
||||
'FSR 4 (AMD Radeon RX 7000 / 9000)': 'FSR 4 (AMD Radeon RX 7000 / 9000)',
|
||||
"100%: 1920x1080, the game's own (default)": '100%: 1920x1080, a do próprio jogo (padrão)',
|
||||
'83%: 1592x896': '83%: 1592x896',
|
||||
'67%: 1280x720 (FSR 4 Quality)': '67%: 1280x720 (FSR 4 Qualidade)',
|
||||
'58%: 1112x624 (FSR 4 Balanced)': '58%: 1112x624 (FSR 4 Equilibrado)',
|
||||
'50%: 968x544 (FSR 4 Performance)': '50%: 968x544 (FSR 4 Desempenho)',
|
||||
'The size the game draws at. Below 100% is faster; with FSR 4 (Graphics > Anti-aliasing) the picture is rebuilt at the window size, otherwise it is only scaled up and looks soft. The HUD keeps its layout.': 'O tamanho em que o jogo desenha. Abaixo de 100% fica mais rápido; com o FSR 4 (Gráficos > Anti-aliasing) a imagem é reconstruída no tamanho da janela, sem ele ela só é esticada e fica borrada. O HUD mantém o layout.',
|
||||
'The size the game draws at. Below 100% is faster; with FSR 4 (Graphics > Anti-aliasing) the picture is rebuilt at the window size, otherwise it is only scaled up and looks soft. 1440p, 1800p and 4K draw natively at that size (sharper, heavier). The HUD keeps its layout.': 'O tamanho em que o jogo desenha. Abaixo de 100% fica mais rápido; com o FSR 4 (Gráficos > Anti-aliasing) a imagem é reconstruída no tamanho da janela, sem ele ela só é esticada e fica borrada. 1440p, 1800p e 4K desenham nativamente nesse tamanho (mais nítido, mais pesado). O HUD mantém o layout.',
|
||||
'1440p: 2560x1440 (native 2K)': '1440p: 2560x1440 (2K nativo)',
|
||||
'1800p: 3200x1800': '1800p: 3200x1800',
|
||||
'2160p: 3840x2160 (native 4K)': '2160p: 3840x2160 (4K nativo)',
|
||||
'Off: full quality (default)': 'Desligado: qualidade máxima (padrão)',
|
||||
'Balanced: about 13% more FPS in the city': 'Equilibrado: cerca de 13% mais FPS na cidade',
|
||||
'Maximum: about 19% more FPS, fewer pedestrians': 'Máximo: cerca de 19% mais FPS, menos pedestres',
|
||||
"The game's own lower settings, from axylpup's 1.1: shorter detail distance, one shadow level less, lighter particles and fewer high-resolution textures nearby. Figures are from his PC in the heaviest city view.": 'Os ajustes mais leves do próprio jogo, da versão 1.1 do axylpup: distância de detalhe menor, um nível de sombra a menos, partículas mais leves e menos texturas em alta resolução por perto. Os números são do PC dele, na vista mais pesada da cidade.',
|
||||
'For Xbox, 8BitDo and other pads without a touchpad or motion sensors. Touchpad click: Select/Back (with L1: the left side). Swipe: hold Select/Back and push the right stick. Two fingers: hold Select/Back + RB. Spray can: L3 + R3 together tilts the pad, LB rattles it, the right stick aims. Automatic turns it off on a DualShock 4 or DualSense.': 'Para controles Xbox, 8BitDo e outros sem touchpad ou sensor de movimento. Clique no touchpad: Select/Back (com L1: o lado esquerdo). Deslizar: segure Select/Back e empurre o analógico direito. Dois dedos: segure Select/Back + RB. Lata de spray: L3 + R3 juntos inclinam o controle, LB chacoalha, o analógico direito mira. No Automático ele desliga num DualShock 4 ou DualSense.',
|
||||
"Keyboard and mouse: WASD moves, IJKL turns the camera. Click in the game window and the mouse aims (1:1, as in axylpup's build); Y or F7 lets the mouse go and takes it back, - / = change its speed. Left click R2 (shoot), right click L2 (aim). Spray can: hold Z to tilt the pad, tap Z or hold X to rattle it, move the mouse to aim. R / G: left / right touchpad (G + IJKL: swipe). Arrows: d-pad. F12: screenshot. Insert: the port's menu.": 'Teclado e mouse: WASD anda, IJKL gira a câmera. Clique na janela do jogo e o mouse mira (1:1, como no build do axylpup); Y ou F7 solta o mouse e pega de volta, - / = mudam a velocidade. Clique esquerdo R2 (atirar), direito L2 (mirar). Lata de spray: segure Z para inclinar o controle, toque Z ou segure X para chacoalhar, mova o mouse para mirar. R / G: lado esquerdo / direito do touchpad (G + IJKL: deslizar). Setas: direcional. F12: print. Insert: menu do port.',
|
||||
'For Xbox, 8BitDo and other pads without a touchpad or motion sensors. Touchpad click: Select/Back (with L1: the left side). Swipe: hold Select/Back and push the right stick. Two fingers: hold Select/Back + RB. Spray can: Insert > Controls > Spray can tilts the pad, LB rattles it, the right stick aims. Automatic turns it off on a DualShock 4 or DualSense.': 'Para controles Xbox, 8BitDo e outros sem touchpad ou sensor de movimento. Clique no touchpad: Select/Back (com L1: o lado esquerdo). Deslizar: segure Select/Back e empurre o analógico direito. Dois dedos: segure Select/Back + RB. Lata de spray: Insert > Controls > Spray can inclina o controle, LB chacoalha, o analógico direito mira. No Automático ele desliga num DualShock 4 ou DualSense.',
|
||||
"Keyboard and mouse: WASD moves, IJKL turns the camera. Click in the game window and the mouse aims (1:1, as in axylpup's build); Y or F7 lets the mouse go and takes it back, - / = change its speed. Left click R2 (shoot), right click L2 (aim). Spray can: hold Z to tilt the pad, tap Z or hold X to rattle it, move the mouse to aim. R: the touchpad (held, both thumbs: the drains, the car on Reggie); R + W/A/S/D: a swipe (doors, wreckage, the Mobile Command Center core: hold R + W and click to hit it); G: its left side (photo mode). Arrows: d-pad. F12: screenshot. Insert: the port's menu.": 'Teclado e mouse: WASD anda, IJKL gira a câmera. Clique na janela do jogo e o mouse mira (1:1, como no build do axylpup); Y ou F7 solta o mouse e pega de volta, - / = mudam a velocidade. Clique esquerdo R2 (atirar), direito L2 (mirar). Lata de spray: segure Z para inclinar o controle, toque Z ou segure X para chacoalhar, mova o mouse para mirar. R: o touchpad (segurado, os dois polegares: os drenos, o carro em cima do Reggie); R + W/A/S/D: deslizar (portas, destroços, o núcleo do Centro de Comando Móvel: segure R + W e clique para bater); G: o lado esquerdo (modo foto). Setas: direcional. F12: print. Insert: menu do port.',
|
||||
'Steam is open. Close Steam, add the game and open Steam again?': 'O Steam está aberto. Fechar o Steam, adicionar o jogo e abrir o Steam de novo?',
|
||||
'Steam did not close. Close it (Steam > Exit) and click again.': 'O Steam não fechou. Feche-o (Steam > Sair) e clique de novo.',
|
||||
'Effects': 'Efeitos',
|
||||
'Shader compilation': 'Compilação de shaders',
|
||||
'Screen shape': 'Formato da tela',
|
||||
'Automatic: the shape of the window / monitor (default)': 'Automático: o formato da janela / monitor (padrão)',
|
||||
"16:9, the game's own": '16:9, o do próprio jogo',
|
||||
'21:9 ultrawide': '21:9 ultrawide',
|
||||
'32:9 super ultrawide': '32:9 super ultrawide',
|
||||
'16:10': '16:10',
|
||||
'Ultrawide and other screens: the game fills the shape and sees more at the sides (or above and below on 16:10); the HUD moves to the edges and movies keep their shape with bars. Automatic follows the window, so play in fullscreen on an ultrawide monitor.': 'Ultrawide e outras telas: o jogo preenche o formato e mostra mais nas laterais (ou em cima e embaixo no 16:10); o HUD vai para as bordas e os vídeos mantêm o formato com faixas pretas. O Automático segue a janela, então jogue em tela cheia num monitor ultrawide.',
|
||||
'In the background: no stutter, new things can appear a moment later (default)': 'Em segundo plano: sem travadas, coisas novas podem aparecer um instante depois (padrão)',
|
||||
'In the frame: the original, the game stutters while new shaders compile': 'No quadro: o original, o jogo trava enquanto shaders novos compilam',
|
||||
'The first time a place or effect appears its shaders are compiled. In the background the game keeps running and what they draw shows up a moment later; in the frame the game waits for them (a stutter). Either way they are kept in the cache for the next time.': 'Na primeira vez que um lugar ou efeito aparece, os shaders dele são compilados. Em segundo plano o jogo continua rodando e o que eles desenham aparece um instante depois; no quadro o jogo espera por eles (uma travada). Nos dois casos eles ficam no cache para a próxima vez.',
|
||||
'Full effects (default)': 'Completos (padrão)',
|
||||
'Light: faster, some effects missing': 'Leves: mais rápido, alguns efeitos somem',
|
||||
'Off: fastest, most effects missing': 'Desligados: o mais rápido, a maioria dos efeitos some',
|
||||
'Smoke, sparks, neon and bullets are computed on the graphics card and read back by the game. Full keeps all of them; Light and Off are faster but lose effects.': 'Fumaça, faíscas, neon e tiros são calculados na placa de vídeo e lidos de volta pelo jogo. Completos mantém todos; Leves e Desligados são mais rápidos, mas perdem efeitos.',
|
||||
'Smoke, sparks, neon, bullets and the light metering of the picture are computed on the graphics card and read back by the game. Full keeps all of them; Light and Off are faster but lose effects and can leave the picture too dark at night.': 'Fumaça, faíscas, neon, tiros e a medição de luz da imagem são calculados na placa de vídeo e lidos de volta pelo jogo. Completos mantém todos; Leves e Desligados são mais rápidos, mas perdem efeitos e podem deixar a imagem escura demais à noite.',
|
||||
'Effects work goes ahead of drawing (faster)': 'Efeitos passam na frente do desenho (mais rápido)',
|
||||
'The effects the game waits for are handled before the rest of the frame. About 29 to 39 FPS in combat on an RX 9060 XT. Switch it off if the game freezes.': 'Os efeitos que o jogo espera são atendidos antes do resto do quadro. Cerca de 29 para 39 FPS em combate numa RX 9060 XT. Desligue se o jogo congelar.',
|
||||
'Draw distance': 'Distância de detalhe',
|
||||
@@ -808,7 +862,170 @@ EXTRA = {
|
||||
'Automatic (on for pads without a touchpad)': 'Automático (liga em controles sem touchpad)',
|
||||
'On': 'Ligado',
|
||||
'Off': 'Desligado',
|
||||
'For Xbox, 8BitDo and other pads without a touchpad or motion sensors. Touchpad click: Select/Back (with L1: the left side). Swipe: hold Select/Back and push the right stick. Two fingers: hold Select/Back + RB. Spray can: L3 + R3 together, LB shakes it, the right stick aims. Automatic turns it off on a DualShock 4 or DualSense.': 'Para controles Xbox, 8BitDo e outros sem touchpad ou sensor de movimento. Clique no touchpad: Select/Back (com L1: o lado esquerdo). Deslizar: segure Select/Back e empurre o analógico direito. Dois dedos: segure Select/Back + RB. Lata de spray: L3 + R3 juntos, LB sacode, o analógico direito mira. No Automático ele desliga num DualShock 4 ou DualSense.',
|
||||
'For Xbox, 8BitDo and other pads without a touchpad or motion sensors. Touchpad click: Select/Back (with L1: the left side). Swipe: hold Select/Back and push the right stick. Two fingers: hold Select/Back + RB. Spray can: Insert > Controls > Spray can tilts the pad, LB rattles it, the right stick aims. Automatic turns it off on a DualShock 4 or DualSense.': 'Para controles Xbox, 8BitDo e outros sem touchpad ou sensor de movimento. Clique no touchpad: Select/Back (com L1: o lado esquerdo). Deslizar: segure Select/Back e empurre o analógico direito. Dois dedos: segure Select/Back + RB. Lata de spray: Insert > Controls > Spray can inclina o controle, LB sacode, o analógico direito mira. No Automático ele desliga num DualShock 4 ou DualSense.',
|
||||
'Spray can stick speed': 'Velocidade do analógico na lata',
|
||||
'Stick dead zone': 'Zona morta dos analógicos',
|
||||
'Skip the intro logos': 'Pular as logos da abertura',
|
||||
'Video memory limit': 'Limite de memória de vídeo',
|
||||
'Automatic (what the driver allows)': 'Automático (o que o driver permite)',
|
||||
'3.5 GB (4 GB cards)': '3,5 GB (placas de 4 GB)',
|
||||
'5 GB (6 GB cards)': '5 GB (placas de 6 GB)',
|
||||
'6.5 GB (8 GB cards)': '6,5 GB (placas de 8 GB)',
|
||||
'9.5 GB (10-12 GB cards)': '9,5 GB (placas de 10 a 12 GB)',
|
||||
'How much video memory the game may fill before it frees textures it is not using. Lower it if the game closes with "Device lost" or the log says the video memory is nearly full, mainly on 4-8 GB cards and laptops.': 'Quanta memória de vídeo o jogo pode ocupar antes de liberar texturas que não está usando. Diminua se o jogo fechar com "Device lost" ou se o log disser que a memória de vídeo está quase cheia, principalmente em placas de 4 a 8 GB e notebooks.',
|
||||
'Controller sensitivity': 'Sensibilidade do controle',
|
||||
'Mouse sensitivity': 'Sensibilidade do mouse',
|
||||
'The right stick (camera). Below 1: slower. Above 1: small stick moves turn the camera more. Also in the game: Insert > Controls.': 'O analógico direito (câmera). Abaixo de 1: mais lenta. Acima de 1: movimentos pequenos do analógico giram mais a câmera. Também no jogo: Insert > Controls.',
|
||||
'How far the camera turns per mouse movement. Also in the game: Insert > Controls, or - / =.': 'Quanto a câmera gira por movimento do mouse. Também no jogo: Insert > Controls, ou - / =.',
|
||||
'Back up saves before a start': 'Fazer backup dos saves antes de jogar',
|
||||
'A copy of your saves before each start when they changed (kept in user\\savedata_backups). Off: no new copies; the old ones stay and can still be restored.': 'Uma cópia dos seus saves antes de cada início, quando eles mudaram (fica em user\\savedata_backups). Desligado: nenhuma cópia nova; as antigas ficam e ainda podem ser restauradas.',
|
||||
'Frame pacing': 'Ritmo dos quadros',
|
||||
"What's new": 'Novidades',
|
||||
'Help': 'Ajuda',
|
||||
'The game ended with an error: Help shows what the log says.': 'O jogo fechou com um erro: a Ajuda mostra o que o log diz.',
|
||||
'Checks your installation and the last game log, and shows how to fix what it finds.': 'Confere a sua instalação e o último log do jogo, e mostra como resolver o que encontrar.',
|
||||
'Check again': 'Verificar de novo',
|
||||
'Open the log': 'Abrir o log',
|
||||
'Ask on the Discord': 'Perguntar no Discord',
|
||||
'No problem found in the installation or the last game log.': 'Nenhum problema encontrado na instalação nem no último log do jogo.',
|
||||
'Something still wrong? Save a bug report and send it on the Discord.': 'Ainda tem algo errado? Salve um relatório de bug e mande no Discord.',
|
||||
'No game folder yet': 'Nenhuma pasta do jogo ainda',
|
||||
'The launcher needs the folder of your own dump of the game: the one with eboot.bin, sce_module and sce_sys.': 'O launcher precisa da pasta do dump do seu próprio jogo: a que tem eboot.bin, sce_module e sce_sys.',
|
||||
'Game > Game folder: choose the folder that has eboot.bin in it.': 'Jogo > Pasta do jogo: escolha a pasta que tem o eboot.bin dentro.',
|
||||
'If the 01.07 update is in a folder of its own, choose it in Game > Update folder.': 'Se a atualização 01.07 estiver numa pasta separada, escolha ela em Jogo > Pasta da atualização.',
|
||||
'The game folder is incomplete': 'A pasta do jogo está incompleta',
|
||||
'eboot.bin is there but sce_sys\\param.sfo or the art folder is missing: the dump was not copied whole, or this is the update alone.': 'O eboot.bin está lá, mas falta o sce_sys\\param.sfo ou a pasta art: o dump não foi copiado inteiro, ou esta é só a atualização.',
|
||||
'Copy the whole dump again (eboot.bin, sce_module, sce_sys and art).': 'Copie o dump inteiro de novo (eboot.bin, sce_module, sce_sys e art).',
|
||||
'With the update in a folder of its own: the base game in Game folder, the update in Update folder.': 'Com a atualização numa pasta separada: o jogo base em Pasta do jogo, a atualização em Pasta da atualização.',
|
||||
'The game is not on the 01.07 update': 'O jogo não está na atualização 01.07',
|
||||
'The port is made for the 01.07 update (US CUSA00223 or EU CUSA00004). Another version stops at the start.': 'O port é feito para a atualização 01.07 (EUA CUSA00223 ou Europa CUSA00004). Outra versão para logo no início.',
|
||||
'Apply the 01.07 update to your dump (the eboot.bin of 01.07 replaces the old one).': 'Aplique a atualização 01.07 no seu dump (o eboot.bin da 01.07 substitui o antigo).',
|
||||
'Or choose the folder of the update in Game > Update folder.': 'Ou escolha a pasta da atualização em Jogo > Pasta da atualização.',
|
||||
'This processor lacks instructions the game needs': 'Este processador não tem instruções que o jogo precisa',
|
||||
"The game's code uses instruction sets this processor does not have, and the port could not translate them all.": 'O código do jogo usa conjuntos de instruções que este processador não tem, e o port não conseguiu traduzir todos.',
|
||||
'Send your last_run.log on the Discord: the line starting with "STOP" names what is missing.': 'Mande o seu last_run.log no Discord: a linha que começa com "STOP" diz o que falta.',
|
||||
'The game closed with an error': 'O jogo fechou com um erro',
|
||||
'The log ends with a fault. Some are known and fixed in newer versions; others need the log to be found.': 'O log termina com uma falha. Algumas já são conhecidas e corrigidas em versões novas; outras precisam do log para serem encontradas.',
|
||||
"Update the port (What's new) if a newer version is out.": 'Atualize o port (Novidades) se houver uma versão nova.',
|
||||
'Save a report (below) and send it on the Discord with what you were doing when it closed.': 'Salve um relatório e mande no Discord, contando o que você estava fazendo quando fechou.',
|
||||
'The game stopped responding': 'O jogo parou de responder',
|
||||
'No new frame for a long time: the game waited for something that never came.': 'Nenhum quadro novo por muito tempo: o jogo ficou esperando algo que nunca chegou.',
|
||||
'Update the port if a newer version is out.': 'Atualize o port se houver uma versão nova.',
|
||||
'Save a report and send it on the Discord, with where in the game it froze.': 'Salve um relatório e mande no Discord, dizendo em que parte do jogo travou.',
|
||||
'The graphics driver stopped the game': 'O driver de vídeo parou o jogo',
|
||||
'The graphics card reported "device lost": the driver reset itself, often from running out of video memory or an unstable overclock.': 'A placa de vídeo informou "device lost": o driver se reiniciou, muitas vezes por falta de memória de vídeo ou overclock instável.',
|
||||
'Lower Graphics > Resolution, or use FSR 4 / DLSS quality.': 'Diminua Gráficos > Resolução, ou use a qualidade do FSR 4 / DLSS.',
|
||||
'Update the graphics driver; remove a GPU overclock or undervolt for a test.': 'Atualize o driver de vídeo; tire overclock ou undervolt da placa para testar.',
|
||||
'Not enough memory': 'Memória insuficiente',
|
||||
'The game reserves several GB at its start, more at high resolutions.': 'O jogo reserva vários GB ao abrir, mais ainda em resoluções altas.',
|
||||
'Close other programs (browsers, recorders) before playing.': 'Feche outros programas (navegadores, gravadores) antes de jogar.',
|
||||
'Let Windows manage the page file (System > Advanced > Performance > Virtual memory: automatic).': 'Deixe o Windows gerenciar o arquivo de paginação (Sistema > Avançado > Desempenho > Memória virtual: automático).',
|
||||
'Lower Graphics > Resolution: 1440p and above need more memory.': 'Diminua Gráficos > Resolução: 1440p ou mais precisa de mais memória.',
|
||||
'The shaders were not prepared before playing': 'Os shaders não foram preparados antes de jogar',
|
||||
'This start had no shader seed, so every new place and effect compiled its shaders while you played (stutter in the first hours).': 'Esta abertura não tinha a semente de shaders, então cada lugar e efeito novo compilou os shaders enquanto você jogava (engasgos nas primeiras horas).',
|
||||
'Install the release zip again over the folder (the seed is in user\\cache), or update from the launcher.': 'Extraia o zip do release de novo por cima da pasta (a semente fica em user\\cache), ou atualize pelo launcher.',
|
||||
'Let the first start finish its shader preparation (a few minutes, with a progress screen).': 'Deixe a primeira abertura terminar de preparar os shaders (alguns minutos, com tela de progresso).',
|
||||
'The picture was not shown for a while': 'A imagem ficou um tempo sem aparecer',
|
||||
'The window was minimized, covered or lost by the graphics driver, so frames were skipped. If the screen stayed black while the game was in front, this is the cause.': 'A janela foi minimizada, coberta ou perdida pelo driver de vídeo, e os quadros foram pulados. Se a tela ficou preta com o jogo na frente, a causa é esta.',
|
||||
'Turn Graphics > Allow HDR output off.': 'Desligue Gráficos > Permitir saída HDR.',
|
||||
'Play in Borderless instead of Fullscreen (Graphics > Display mode).': 'Jogue em Sem bordas em vez de Tela cheia (Gráficos > Modo de exibição).',
|
||||
'Update the graphics driver.': 'Atualize o driver de vídeo.',
|
||||
'DLSS could not start': 'O DLSS não conseguiu iniciar',
|
||||
"DLSS needs NVIDIA's Streamline files (not included) of the exact version the port uses.": 'O DLSS precisa dos arquivos Streamline da NVIDIA (não vêm junto) na versão exata que o port usa.',
|
||||
'XeSS could not start': 'O XeSS não conseguiu iniciar',
|
||||
"XeSS needs Intel's libxess.dll (not included), and a graphics card with the Vulkan features it uses.": 'O XeSS precisa do libxess.dll da Intel (não incluído) e de uma placa de vídeo com os recursos de Vulkan que ele usa.',
|
||||
'Graphics > Open the XeSS folder, and copy libxess.dll from the XeSS SDK zip (folder bin).': 'Gráficos > Abrir a pasta do XeSS, e copie o libxess.dll do zip do XeSS SDK (pasta bin).',
|
||||
"Update the graphics driver. If XeSS still does not start, pick FSR 4 or the game's own.": 'Atualize o driver de vídeo. Se o XeSS ainda não iniciar, escolha o FSR 4 ou o do próprio jogo.',
|
||||
'Graphics > Open the DLSS folder, and copy the five files from Streamline SDK 2.14.16 (bin\\x64).': 'Gráficos > Abrir a pasta do DLSS, e copie os cinco arquivos do Streamline SDK 2.14.16 (bin\\x64).',
|
||||
'A newer Streamline crashed the game for a tester: use 2.14.16.': 'Um Streamline mais novo fechou o jogo para um tester: use a 2.14.16.',
|
||||
'The port is in a folder that causes problems': 'O port está numa pasta que causa problemas',
|
||||
'Inside a zip or temporary folder, Program Files or a synced folder (OneDrive) the port cannot write its files reliably.': 'Dentro de um zip ou pasta temporária, em Arquivos de Programas ou numa pasta sincronizada (OneDrive), o port não consegue gravar os arquivos direito.',
|
||||
'Extract the zip to a normal folder, for example C:\\Games\\inFAMOUS-PC, and start it from there.': 'Extraia o zip numa pasta normal, por exemplo C:\\Jogos\\inFAMOUS-PC, e abra por lá.',
|
||||
'Your PC': 'Seu PC',
|
||||
'Looking at this PC…': 'Analisando este PC…',
|
||||
'Set up the graphics for this PC:': 'Configurar os gráficos para este PC:',
|
||||
'Smooth (up to 120 FPS)': 'Suave (até 120 FPS)',
|
||||
'Balanced (60 FPS, sharpest)': 'Equilibrado (60 FPS, mais nítido)',
|
||||
'Quiet (60 FPS, lighter on the card)': 'Silencioso (60 FPS, mais leve para a placa)',
|
||||
'not recognised': 'não reconhecido',
|
||||
'Graphics card: {}': 'Placa de vídeo: {}',
|
||||
'Processor: {}': 'Processador: {}',
|
||||
'Memory: {} GB': 'Memória: {} GB',
|
||||
'Memory: {}': 'Memória: {}',
|
||||
'Runs it very well': 'Roda muito bem',
|
||||
'Runs it well': 'Roda bem',
|
||||
'Runs it, at lower settings': 'Roda, com configurações mais baixas',
|
||||
'Below what the game needs': 'Abaixo do que o jogo precisa',
|
||||
'Not in the list yet': 'Ainda não está na lista',
|
||||
'about {} FPS in the open city at 1080p': 'cerca de {} FPS na cidade aberta em 1080p',
|
||||
'the processor sets the limit': 'o processador é o limite',
|
||||
'the graphics card sets the limit': 'a placa de vídeo é o limite',
|
||||
'This is an integrated graphics chip: a graphics card is needed for this game.': 'Este é um vídeo integrado: este jogo precisa de uma placa de vídeo.',
|
||||
'16 GB of memory or more is recommended.': '16 GB de memória ou mais é o recomendado.',
|
||||
'Resolution: {}': 'Resolução: {}',
|
||||
'Frame rate limit: {}': 'Limite de FPS: {}',
|
||||
'Anti-aliasing: {}': 'Anti-aliasing: {}',
|
||||
"the game's own": 'o do jogo',
|
||||
'Performance mode: {}': 'Modo de desempenho: {}',
|
||||
'off': 'desligado',
|
||||
'Balanced': 'Equilibrado',
|
||||
'Maximum': 'Máximo',
|
||||
'Texture filtering: {}x': 'Filtragem de texturas: {}x',
|
||||
'the processor holds about {} FPS in the city': 'o processador aguenta cerca de {} FPS na cidade',
|
||||
'{} GB of memory': '{} GB de memória',
|
||||
'{} GB of video memory': '{} GB de memória de vídeo',
|
||||
'Set: {}.': 'Ajustado: {}.',
|
||||
'Because: {}.': 'Motivo: {}.',
|
||||
'Press Save to keep it; every setting below can still be changed.': 'Clique em Salvar para manter; dá para mudar qualquer opção abaixo.',
|
||||
'The latest versions of the port and what changed in each.': 'As últimas versões do port e o que mudou em cada uma.',
|
||||
'Open this page to load the notes.': 'Abra esta página para carregar as notas.',
|
||||
'Loading the release notes from GitHub…': 'Carregando as notas das versões do GitHub…',
|
||||
'Could not load the release notes: {}': 'Não foi possível carregar as notas das versões: {}',
|
||||
'No releases yet.': 'Ainda não há versões.',
|
||||
'New': 'Nova',
|
||||
'Installed': 'Instalada',
|
||||
'Open on GitHub': 'Abrir no GitHub',
|
||||
'Prepare the drawing shaders on any graphics card (test)': 'Preparar os shaders de desenho em qualquer placa (teste)',
|
||||
'NVIDIA and older AMD cards: the first start also prepares the shaders that otherwise compile while playing. A test: send the log of the first start and say if anything looks wrong.': 'Placas NVIDIA e AMD mais antigas: a primeira abertura também prepara os shaders que senão compilam durante o jogo. É um teste: mande o log da primeira abertura e diga se algo aparecer errado.',
|
||||
'Normal: a new frame at every refresh it is ready (default)': 'Normal: um quadro novo a cada atualização em que ele fica pronto (padrão)',
|
||||
'Half the refresh rate, even (72 FPS on 144 Hz, 120 on 240 Hz)': 'Metade da taxa da tela, constante (72 FPS em 144 Hz, 120 em 240 Hz)',
|
||||
'A third of the refresh rate, even (48 FPS on 144 Hz, 80 on 240 Hz)': 'Um terço da taxa da tela, constante (48 FPS em 144 Hz, 80 em 240 Hz)',
|
||||
'For screens without G-Sync / FreeSync: when the game runs between half and the full refresh rate, some frames stay on screen longer than others and motion judders even at a high FPS. Half or a third shows every frame for the same time (V-Sync on). Pick the one your PC holds in the heaviest places.': 'Para telas sem G-Sync / FreeSync: quando o jogo roda entre a metade e a taxa cheia da tela, alguns quadros ficam mais tempo na tela que outros e o movimento engasga mesmo com FPS alto. Metade ou um terço mostra todo quadro pelo mesmo tempo (V-Sync ligado). Escolha a que o seu PC aguenta nos lugares mais pesados.',
|
||||
'The game opens straight on its title screen, without the PlayStation and Sucker Punch logos.': 'O jogo abre direto na tela de título, sem as logos da PlayStation e da Sucker Punch.',
|
||||
'Small: 5% (default)': 'Pequena: 5% (padrão)',
|
||||
'Medium: 10%': 'Média: 10%',
|
||||
'Large: 15%': 'Grande: 15%',
|
||||
'Very large: 20% (worn sticks)': 'Muito grande: 20% (analógicos gastos)',
|
||||
'How far a stick must move before the game sees it. Raise it if Delsin walks or the camera turns on its own while you do not touch the stick (drift).': 'Quanto o analógico precisa se mover para o jogo perceber. Aumente se o Delsin anda ou a câmera gira sozinha sem você tocar no analógico (drift).',
|
||||
'Button icons': 'Ícones dos botões',
|
||||
'Made by {}': 'Feito por {}',
|
||||
'About': 'Sobre',
|
||||
'Support the project': 'Apoie o projeto',
|
||||
"The port is free and always will be. If it gave you some good moments and you'd like to help keep it and the next projects going, or just say thanks, any amount is welcome and means a lot. Thank you! ♥": 'O port é e sempre será gratuito. Se ele te rendeu bons momentos e você quiser ajudar a manter este e os próximos projetos de pé, ou só agradecer, qualquer valor é bem-vindo e faz muita diferença. Obrigado! ♥',
|
||||
"Donations only support the work: they don't unlock anything, and the port never includes game files.": 'As doações são só um apoio ao trabalho: não liberam nada extra, e o port nunca inclui arquivos do jogo.',
|
||||
'Copied ✓': 'Copiado ✓',
|
||||
'Texture filtering': 'Filtragem de texturas',
|
||||
'5120 × 2880 (5K, experimental: 16 GB of RAM or more)': '5120 × 2880 (5K, experimental: 16 GB de RAM ou mais)',
|
||||
'7680 × 4320 (8K, experimental: 32 GB of RAM, 16 GB of VRAM)': '7680 × 4320 (8K, experimental: 32 GB de RAM, 16 GB de VRAM)',
|
||||
"The game's own (default)": 'A do próprio jogo (padrão)',
|
||||
'16x (sharpest)': '16x (mais nítida)',
|
||||
'Anisotropic filtering: ground, roads and walls seen at an angle stay sharp in the distance. Costs a little FPS on weak graphics cards; it does not change stutter.': 'Filtragem anisotrópica: chão, ruas e paredes vistos de lado ficam nítidos ao longe. Custa um pouco de FPS em placas fracas; não muda as travadas.',
|
||||
'Pix (Brazil)': 'Pix (Brasil)',
|
||||
'Random key': 'Chave aleatória',
|
||||
'Copy key': 'Copiar chave',
|
||||
'Copy Pix code': 'Copiar Pix copia e cola',
|
||||
'Any country': 'Qualquer país',
|
||||
'Card or PayPal, in any currency, no account needed.': 'Cartão ou PayPal, em qualquer moeda, sem precisar de conta.',
|
||||
'Thanks to {}.': 'Agradecimentos: {}.',
|
||||
"PlayStation, the game's own (default)": 'PlayStation, os do próprio jogo (padrão)',
|
||||
'Automatic: keyboard and mouse, or the connected controller': 'Automático: teclado e mouse, ou o controle conectado',
|
||||
'Keyboard and mouse': 'Teclado e mouse',
|
||||
'The buttons the game shows in its hints and menus. Keyboard and mouse shows the keys (they follow your key changes above) and the mouse buttons; Automatic picks the keyboard when no controller is connected at start, Xbox letters for Xbox and other pads.': 'Os botões que o jogo mostra nas dicas e nos menus. Teclado e mouse mostra as teclas (seguindo as suas trocas de tecla acima) e os botões do mouse; o Automático escolhe o teclado quando nenhum controle está conectado ao abrir, e as letras do Xbox para controles Xbox e outros.',
|
||||
'Normal (default)': 'Normal (padrão)',
|
||||
'Slow': 'Lenta',
|
||||
'Fast': 'Rápida',
|
||||
'Very fast (0.3.4 and earlier)': 'Muito rápida (0.3.4 e anteriores)',
|
||||
'How fast the right stick turns the spray can while the pad is tilted (Insert > Controls > Spray can). A light push moves it a little, a full push at this speed. In the game, - / = change it too.': 'Quão rápido o analógico direito vira a lata de spray com o controle inclinado (L3 + R3). Um toque leve move pouco; empurrado até o fim, nesta velocidade. No jogo, - / = também mudam.',
|
||||
'Automatic (launcher language)': 'Automático (idioma do launcher)',
|
||||
'Portuguese (Brazil)': 'Português (Brasil)',
|
||||
'Portuguese (Portugal)': 'Português (Portugal)',
|
||||
@@ -921,7 +1138,7 @@ EXTRA = {
|
||||
'RUNNING': 'EXECUTANDO',
|
||||
'{} · version {}': '{} · versão {}',
|
||||
'Choose your game folder to play.': 'Escolha a pasta do jogo para jogar.',
|
||||
'Made before every start, newest first; the last {} are kept. Restoring backs up the current saves first.': 'Feito antes de cada início, do mais novo ao mais antigo; os últimos {} são mantidos. Restaurar faz backup dos saves atuais antes.',
|
||||
'Made before every start when the saves changed, newest first. Kept: the last {}, one a day for {} days and the ones made before a restore or an import. Restoring backs up the current saves first.': 'Feito antes de cada início quando os saves mudaram, do mais novo ao mais antigo. Ficam guardados: os últimos {}, um por dia durante {} dias e os feitos antes de restaurar ou importar. Restaurar faz backup dos saves atuais antes.',
|
||||
'shadPS4/GoldHEN XML patch files for this game version. Empty: {}': 'Arquivos de patch XML do shadPS4/GoldHEN para esta versão do jogo. Vazio: {}',
|
||||
'{} · version {} · {}. Resolution and frame rate patches follow Performance.': '{} · versão {} · {}. Os patches de resolução e de taxa de quadros seguem o Desempenho.',
|
||||
'Automatic lets SDL choose. Pick X11 if the window misbehaves on Wayland (it runs through XWayland).': 'O modo automático deixa o SDL escolher. Escolha X11 se a janela se comportar mal no Wayland (roda pelo XWayland).',
|
||||
@@ -930,6 +1147,19 @@ EXTRA = {
|
||||
'Restore': 'Restaurar',
|
||||
'Replace the current saves with the backup {}? The current saves are backed up first.': 'Substituir os saves atuais pelo backup {}? Os saves atuais passam por backup antes.',
|
||||
'Saves restored from {}.': 'Saves restaurados de {}.',
|
||||
'Import saves': 'Importar saves',
|
||||
'Import from another port…': 'Importar de outro port…',
|
||||
"Brings your progress from another PC port of the game (HayatoG's iss_pc, or an older folder of this one): pick its folder. The current saves are backed up first.":
|
||||
'Traz seu progresso de outro port do jogo para PC (o iss_pc do HayatoG, ou uma pasta antiga deste): escolha a pasta dele. Os saves atuais passam por backup antes.',
|
||||
'Close the game before importing saves.': 'Feche o jogo antes de importar saves.',
|
||||
'Saves of another port were found in {}. Import them?': 'Saves de outro port foram encontrados em {}. Importar?',
|
||||
"Choose the other port's folder (or its user / savedata folder)": 'Escolha a pasta do outro port (ou a pasta user / savedata dele)',
|
||||
"No inFAMOUS Second Son saves in {}. Choose the other port's folder, or its user or savedata folder.":
|
||||
'Nenhum save de inFAMOUS Second Son em {}. Escolha a pasta do outro port, ou a pasta user ou savedata dele.',
|
||||
'Replace the current saves with the ones in {}? The current saves are backed up first.':
|
||||
'Substituir os saves atuais pelos de {}? Os saves atuais passam por backup antes.',
|
||||
'Could not import: {}': 'Não foi possível importar: {}',
|
||||
'{} saves imported from {}.': '{} saves importados de {}.',
|
||||
'Reset to defaults': 'Restaurar padrões',
|
||||
'⚠ {} file(s) replaced by more than one mod (the last one wins):': '⚠ {} arquivo(s) substituído(s) por mais de um mod (vale o último):',
|
||||
' … and {} more': ' … e mais {}',
|
||||
@@ -987,6 +1217,81 @@ EXTRA = {
|
||||
'Original (default)': 'Original (padrão)',
|
||||
'Match my controller': 'Igual ao meu controle',
|
||||
"Replaces the PlayStation button icons inside the game with the ones of your controller. Works on a copy of two game files in the port's data folder (about 70 MB); your game folder is never changed. Applied when the game starts.": 'Troca os ícones de botões PlayStation dentro do jogo pelos do seu controle. Funciona com uma cópia de dois arquivos do jogo na pasta de dados do port (cerca de 70 MB); a pasta do jogo nunca é alterada. Aplicado quando o jogo inicia.',
|
||||
# 0.3.7: OptiScaler (optional), installed by the launcher.
|
||||
'OptiScaler (optional)':
|
||||
'OptiScaler (opcional)',
|
||||
"A separate program by the OptiScaler team that takes the port's XeSS input and turns it into another upscaler. Not part of the port: the launcher downloads it from the project's GitHub when you ask and can take it out again.":
|
||||
'Um programa à parte, da equipe do OptiScaler, que pega a entrada do XeSS do port e transforma em outro upscaler. Não faz parte do port: o launcher baixa do GitHub do projeto quando você pede e pode tirar de novo.',
|
||||
'OptiScaler output':
|
||||
'Saída do OptiScaler',
|
||||
'What OptiScaler makes of the XeSS input. FSR 2.2 is the one tested with the port; FSR 4 goes through DirectX 12 and needs a Radeon RX 7000 or 9000. Written to OptiScaler.ini at once; it applies when the game starts.':
|
||||
'O que o OptiScaler faz com a entrada do XeSS. O FSR 2.2 é o testado com o port; o FSR 4 passa pelo DirectX 12 e precisa de uma Radeon RX 7000 ou 9000. Gravado no OptiScaler.ini na hora; vale quando o jogo abrir.',
|
||||
'FSR 4 (Radeon RX 7000 / 9000, RDNA 3/4)':
|
||||
'FSR 4 (Radeon RX 7000 / 9000, RDNA 3/4)',
|
||||
'FSR 2.2 (tested with the port)':
|
||||
'FSR 2.2 (testado com o port)',
|
||||
'Download and install':
|
||||
'Baixar e instalar',
|
||||
'Update / install again':
|
||||
'Atualizar / instalar de novo',
|
||||
'Remove':
|
||||
'Remover',
|
||||
'How to use':
|
||||
'Como usar',
|
||||
'Not installed.':
|
||||
'Não instalado.',
|
||||
'bin\\winmm.dll is another program, not OptiScaler: the launcher leaves it alone.':
|
||||
'bin\\winmm.dll é outro programa, não o OptiScaler: o launcher não mexe nele.',
|
||||
'Installed: OptiScaler {} · output {} · its menu in the game: {}.':
|
||||
'Instalado: OptiScaler {} · saída {} · menu dele no jogo: {}.',
|
||||
"Unpacked by hand: Remove takes its files out; Download and install replaces it with the launcher's, set for the port.":
|
||||
'Instalado à mão: Remover tira os arquivos dele; Baixar e instalar troca pelo do launcher, já configurado para o port.',
|
||||
'{} is also a key of the port: install it again from here to move its menu to F8.':
|
||||
'{} também é uma tecla do port: instale de novo por aqui para o menu dele ir para o F8.',
|
||||
"⚠ The port's anti-aliasing is not XeSS, so OptiScaler gets nothing: choose XeSS in Anti-aliasing above.":
|
||||
'⚠ O anti-aliasing do port não está em XeSS, então o OptiScaler não recebe nada: escolha XeSS em Anti-aliasing acima.',
|
||||
'Close the game first: it is using the OptiScaler files.':
|
||||
'Feche o jogo primeiro: ele está usando os arquivos do OptiScaler.',
|
||||
'OptiScaler.ini could not be changed: {}':
|
||||
'Não deu para mudar o OptiScaler.ini: {}',
|
||||
'OptiScaler output: {} (the next time the game starts).':
|
||||
'Saída do OptiScaler: {} (na próxima vez que o jogo abrir).',
|
||||
"OptiScaler is a separate program by the OptiScaler team (GPL-3.0), not part of the port.\n\nThe launcher downloads its newest stable release from github.com/optiscaler/OptiScaler (about 55 MB), checks it and puts it in {}, set for the port: its menu on F8, the output you chose. The port's anti-aliasing is set to XeSS, which is what OptiScaler reads.\n\nDownload and install it now?":
|
||||
'O OptiScaler é um programa à parte, da equipe do OptiScaler (GPL-3.0), e não faz parte do port.\n\nO launcher baixa a versão estável mais nova de github.com/optiscaler/OptiScaler (uns 55 MB), confere e coloca em {}, já configurado para o port: menu dele no F8 e a saída que você escolheu. O anti-aliasing do port passa para XeSS, que é o que o OptiScaler lê.\n\nBaixar e instalar agora?',
|
||||
'Downloading OptiScaler {}… {{}}%':
|
||||
'Baixando o OptiScaler {}… {{}}%',
|
||||
'Installing OptiScaler… {}%':
|
||||
'Instalando o OptiScaler… {}%',
|
||||
'OptiScaler was not installed: {}':
|
||||
'O OptiScaler não foi instalado: {}',
|
||||
"OptiScaler {} is installed. The port's anti-aliasing is now XeSS.\n\nIn the game, F8 opens OptiScaler's menu (Insert stays the port's). Remove takes it out again.":
|
||||
'O OptiScaler {} está instalado. O anti-aliasing do port agora é XeSS.\n\nNo jogo, o F8 abre o menu do OptiScaler (o Insert continua sendo do port). Remover tira ele de novo.',
|
||||
'Looking for the newest OptiScaler…':
|
||||
'Procurando o OptiScaler mais novo…',
|
||||
"Remove OptiScaler? Its files leave the bin folder and the port runs as before. The port's anti-aliasing stays XeSS; change it above if you want.":
|
||||
'Remover o OptiScaler? Os arquivos dele saem da pasta bin e o port fica como antes. O anti-aliasing do port continua em XeSS; mude acima se quiser.',
|
||||
'OptiScaler was not removed completely: {}':
|
||||
'O OptiScaler não foi removido por completo: {}',
|
||||
'OptiScaler removed ({} files).':
|
||||
'OptiScaler removido ({} arquivos).',
|
||||
'OptiScaler {} installed.':
|
||||
'OptiScaler {} instalado.',
|
||||
"OptiScaler is installed, but the port's anti-aliasing is not XeSS, so OptiScaler gets nothing and the game runs without it.\n\nSwitch the anti-aliasing to XeSS now?":
|
||||
'O OptiScaler está instalado, mas o anti-aliasing do port não está em XeSS, então o OptiScaler não recebe nada e o jogo roda sem ele.\n\nMudar o anti-aliasing para XeSS agora?',
|
||||
"OptiScaler (optional)\n\nWhat it is: a separate program (OptiScaler team, GPL-3.0) that takes the picture, depth and motion the port gives XeSS and runs another upscaler on them: FSR 4, FSR 3.1, XeSS, DLSS or FSR 2.2.\n\n1. Download and install. The port's anti-aliasing must be XeSS (the launcher sets it): with anything else OptiScaler gets nothing.\n2. Choose the output here (it applies when the game starts). The size the game draws at is still the port's 'FSR 4 / DLSS quality'.\n3. In the game, F8 opens OptiScaler's menu (output, sharpness, debug views). Insert is still the port's menu and Home its anti-aliasing switch. Page Up, Page Down and End are OptiScaler's (its FPS counter and frame generation).\n4. To go back: Remove. Every file it added leaves the bin folder.\n\nThird-party: problems with OptiScaler itself go to its project, not to the port.\nProject: github.com/optiscaler/OptiScaler · License: GPL-3.0\nOpen the project page (source code and license)?":
|
||||
"OptiScaler (opcional)\n\nO que é: um programa à parte (equipe do OptiScaler, GPL-3.0) que pega a imagem, a profundidade e o movimento que o port entrega ao XeSS e roda outro upscaler em cima: FSR 4, FSR 3.1, XeSS, DLSS ou FSR 2.2.\n\n1. Baixar e instalar. O anti-aliasing do port precisa estar em XeSS (o launcher já coloca): com qualquer outro, o OptiScaler não recebe nada.\n2. Escolha a saída aqui (vale quando o jogo abrir). O tamanho em que o jogo desenha continua sendo o 'Qualidade do FSR 4 / DLSS' do port.\n3. No jogo, o F8 abre o menu do OptiScaler (saída, nitidez, visões de debug). O Insert continua sendo o menu do port e o Home a troca de anti-aliasing. Page Up, Page Down e End são do OptiScaler (contador de FPS e geração de quadros dele).\n4. Para voltar: Remover. Todo arquivo que ele colocou sai da pasta bin.\n\nDe terceiros: problemas do próprio OptiScaler vão para o projeto dele, não para o port.\nProjeto: github.com/optiscaler/OptiScaler · Licença: GPL-3.0\nAbrir a página do projeto (código e licença)?",
|
||||
'Off (no anti-aliasing)':
|
||||
'Desligado (sem anti-aliasing)',
|
||||
'Off: no anti-aliasing at all, the sharpest picture with jagged edges (no smearing in motion).':
|
||||
'Desligado: nenhum anti-aliasing, a imagem mais nítida com bordas serrilhadas (sem borrão em movimento).',
|
||||
'Motion blur':
|
||||
'Desfoque de movimento',
|
||||
'Chromatic aberration':
|
||||
'Aberração cromática',
|
||||
'The blur when the camera or Delsin moves fast. Off gives a sharp picture in motion; it also changes at once in the game (Insert > Image).':
|
||||
'O borrão quando a câmera ou o Delsin se movem rápido. Desligado deixa a imagem nítida em movimento; também muda na hora no jogo (Insert > Image).',
|
||||
'The red and blue fringes toward the edges of the screen. Turning it off helps players who feel sick with it; it also changes at once in the game (Insert > Image).':
|
||||
'As franjas vermelhas e azuis perto das bordas da tela. Desligar ajuda quem passa mal com elas; também muda na hora no jogo (Insert > Image).',
|
||||
},
|
||||
'es': {
|
||||
'Game multisampling': 'Multisampling del juego',
|
||||
@@ -1106,8 +1411,8 @@ EXTRA = {
|
||||
'RUNNING': 'EN EJECUCIÓN',
|
||||
'{} · version {}': '{} · versión {}',
|
||||
'Choose your game folder to play.': 'Elige la carpeta del juego para jugar.',
|
||||
'Made before every start, newest first; the last {} are kept. Restoring backs up the current saves first.':
|
||||
'Se hace antes de cada inicio, de la más reciente a la más antigua; se conservan las últimas {}. Antes de restaurar se hace una copia de las partidas actuales.',
|
||||
'Made before every start when the saves changed, newest first. Kept: the last {}, one a day for {} days and the ones made before a restore or an import. Restoring backs up the current saves first.':
|
||||
'Se hace antes de cada inicio si las partidas cambiaron, de la más reciente a la más antigua. Se conservan: las últimas {}, una por día durante {} días y las hechas antes de restaurar o importar. Antes de restaurar se hace una copia de las partidas actuales.',
|
||||
'shadPS4/GoldHEN XML patch files for this game version. Empty: {}':
|
||||
'Archivos de parche XML de shadPS4/GoldHEN para esta versión del juego. Vacío: {}',
|
||||
'{} · version {} · {}. Resolution and frame rate patches follow Performance.':
|
||||
@@ -1121,6 +1426,19 @@ EXTRA = {
|
||||
'Replace the current saves with the backup {}? The current saves are backed up first.':
|
||||
'¿Reemplazar las partidas actuales por la copia {}? Antes se hace una copia de las partidas actuales.',
|
||||
'Saves restored from {}.': 'Partidas restauradas desde {}.',
|
||||
'Import saves': 'Importar partidas',
|
||||
'Import from another port…': 'Importar de otro port…',
|
||||
"Brings your progress from another PC port of the game (HayatoG's iss_pc, or an older folder of this one): pick its folder. The current saves are backed up first.":
|
||||
'Trae tu progreso de otro port del juego para PC (el iss_pc de HayatoG, o una carpeta antigua de este): elige su carpeta. Antes se hace una copia de las partidas actuales.',
|
||||
'Close the game before importing saves.': 'Cierra el juego antes de importar partidas.',
|
||||
'Saves of another port were found in {}. Import them?': 'Se encontraron partidas de otro port en {}. ¿Importarlas?',
|
||||
"Choose the other port's folder (or its user / savedata folder)": 'Elige la carpeta del otro port (o su carpeta user / savedata)',
|
||||
"No inFAMOUS Second Son saves in {}. Choose the other port's folder, or its user or savedata folder.":
|
||||
'No hay partidas de inFAMOUS Second Son en {}. Elige la carpeta del otro port, o su carpeta user o savedata.',
|
||||
'Replace the current saves with the ones in {}? The current saves are backed up first.':
|
||||
'¿Reemplazar las partidas actuales por las de {}? Antes se hace una copia de las actuales.',
|
||||
'Could not import: {}': 'No se pudo importar: {}',
|
||||
'{} saves imported from {}.': '{} partidas importadas desde {}.',
|
||||
'Reset to defaults': 'Restablecer valores predeterminados',
|
||||
'⚠ {} file(s) replaced by more than one mod (the last one wins):':
|
||||
'⚠ {} archivo(s) reemplazado(s) por más de un mod (gana el último):',
|
||||
@@ -1192,9 +1510,155 @@ EXTRA = {
|
||||
'Match my controller': 'Igual que mi mando',
|
||||
"Replaces the PlayStation button icons inside the game with the ones of your controller. Works on a copy of two game files in the port's data folder (about 70 MB); your game folder is never changed. Applied when the game starts.":
|
||||
'Sustituye los iconos de botones de PlayStation dentro del juego por los de tu mando. Trabaja sobre una copia de dos archivos del juego en la carpeta de datos del port (unos 70 MB); tu carpeta del juego nunca se modifica. Se aplica al iniciar el juego.',
|
||||
# 0.3.7: OptiScaler (optional), installed by the launcher.
|
||||
'OptiScaler (optional)':
|
||||
'OptiScaler (opcional)',
|
||||
"A separate program by the OptiScaler team that takes the port's XeSS input and turns it into another upscaler. Not part of the port: the launcher downloads it from the project's GitHub when you ask and can take it out again.":
|
||||
'Un programa aparte, del equipo de OptiScaler, que toma la entrada de XeSS del port y la convierte en otro escalador. No forma parte del port: el lanzador lo descarga del GitHub del proyecto cuando lo pides y puede quitarlo de nuevo.',
|
||||
'OptiScaler output':
|
||||
'Salida de OptiScaler',
|
||||
'What OptiScaler makes of the XeSS input. FSR 2.2 is the one tested with the port; FSR 4 goes through DirectX 12 and needs a Radeon RX 7000 or 9000. Written to OptiScaler.ini at once; it applies when the game starts.':
|
||||
'Lo que OptiScaler hace con la entrada de XeSS. FSR 2.2 es el probado con el port; FSR 4 pasa por DirectX 12 y necesita una Radeon RX 7000 o 9000. Se escribe en OptiScaler.ini al momento; vale al iniciar el juego.',
|
||||
'FSR 4 (Radeon RX 7000 / 9000, RDNA 3/4)':
|
||||
'FSR 4 (Radeon RX 7000 / 9000, RDNA 3/4)',
|
||||
'FSR 2.2 (tested with the port)':
|
||||
'FSR 2.2 (probado con el port)',
|
||||
'Download and install':
|
||||
'Descargar e instalar',
|
||||
'Update / install again':
|
||||
'Actualizar / instalar de nuevo',
|
||||
'Remove':
|
||||
'Quitar',
|
||||
'How to use':
|
||||
'Cómo usarlo',
|
||||
'Not installed.':
|
||||
'No instalado.',
|
||||
'bin\\winmm.dll is another program, not OptiScaler: the launcher leaves it alone.':
|
||||
'bin\\winmm.dll es otro programa, no OptiScaler: el lanzador no lo toca.',
|
||||
'Installed: OptiScaler {} · output {} · its menu in the game: {}.':
|
||||
'Instalado: OptiScaler {} · salida {} · su menú en el juego: {}.',
|
||||
"Unpacked by hand: Remove takes its files out; Download and install replaces it with the launcher's, set for the port.":
|
||||
'Instalado a mano: Quitar saca sus archivos; Descargar e instalar lo cambia por el del lanzador, configurado para el port.',
|
||||
'{} is also a key of the port: install it again from here to move its menu to F8.':
|
||||
'{} también es una tecla del port: instálalo de nuevo desde aquí para pasar su menú a F8.',
|
||||
"⚠ The port's anti-aliasing is not XeSS, so OptiScaler gets nothing: choose XeSS in Anti-aliasing above.":
|
||||
'⚠ El antialiasing del port no está en XeSS, así que OptiScaler no recibe nada: elige XeSS en Anti-aliasing, arriba.',
|
||||
'Close the game first: it is using the OptiScaler files.':
|
||||
'Cierra el juego primero: está usando los archivos de OptiScaler.',
|
||||
'OptiScaler.ini could not be changed: {}':
|
||||
'No se pudo cambiar OptiScaler.ini: {}',
|
||||
'OptiScaler output: {} (the next time the game starts).':
|
||||
'Salida de OptiScaler: {} (la próxima vez que se inicie el juego).',
|
||||
"OptiScaler is a separate program by the OptiScaler team (GPL-3.0), not part of the port.\n\nThe launcher downloads its newest stable release from github.com/optiscaler/OptiScaler (about 55 MB), checks it and puts it in {}, set for the port: its menu on F8, the output you chose. The port's anti-aliasing is set to XeSS, which is what OptiScaler reads.\n\nDownload and install it now?":
|
||||
'OptiScaler es un programa aparte, del equipo de OptiScaler (GPL-3.0), y no forma parte del port.\n\nEl lanzador descarga su versión estable más nueva de github.com/optiscaler/OptiScaler (unos 55 MB), la comprueba y la pone en {}, configurada para el port: su menú en F8 y la salida que elegiste. El antialiasing del port pasa a XeSS, que es lo que lee OptiScaler.\n\n¿Descargarlo e instalarlo ahora?',
|
||||
'Downloading OptiScaler {}… {{}}%':
|
||||
'Descargando OptiScaler {}… {{}}%',
|
||||
'Installing OptiScaler… {}%':
|
||||
'Instalando OptiScaler… {}%',
|
||||
'OptiScaler was not installed: {}':
|
||||
'OptiScaler no se instaló: {}',
|
||||
"OptiScaler {} is installed. The port's anti-aliasing is now XeSS.\n\nIn the game, F8 opens OptiScaler's menu (Insert stays the port's). Remove takes it out again.":
|
||||
'OptiScaler {} está instalado. El antialiasing del port ahora es XeSS.\n\nEn el juego, F8 abre el menú de OptiScaler (Insert sigue siendo del port). Quitar lo saca de nuevo.',
|
||||
'Looking for the newest OptiScaler…':
|
||||
'Buscando el OptiScaler más nuevo…',
|
||||
"Remove OptiScaler? Its files leave the bin folder and the port runs as before. The port's anti-aliasing stays XeSS; change it above if you want.":
|
||||
'¿Quitar OptiScaler? Sus archivos salen de la carpeta bin y el port queda como antes. El antialiasing del port sigue en XeSS; cámbialo arriba si quieres.',
|
||||
'OptiScaler was not removed completely: {}':
|
||||
'OptiScaler no se quitó del todo: {}',
|
||||
'OptiScaler removed ({} files).':
|
||||
'OptiScaler quitado ({} archivos).',
|
||||
'OptiScaler {} installed.':
|
||||
'OptiScaler {} instalado.',
|
||||
"OptiScaler is installed, but the port's anti-aliasing is not XeSS, so OptiScaler gets nothing and the game runs without it.\n\nSwitch the anti-aliasing to XeSS now?":
|
||||
'OptiScaler está instalado, pero el antialiasing del port no está en XeSS, así que OptiScaler no recibe nada y el juego va sin él.\n\n¿Cambiar el antialiasing a XeSS ahora?',
|
||||
"OptiScaler (optional)\n\nWhat it is: a separate program (OptiScaler team, GPL-3.0) that takes the picture, depth and motion the port gives XeSS and runs another upscaler on them: FSR 4, FSR 3.1, XeSS, DLSS or FSR 2.2.\n\n1. Download and install. The port's anti-aliasing must be XeSS (the launcher sets it): with anything else OptiScaler gets nothing.\n2. Choose the output here (it applies when the game starts). The size the game draws at is still the port's 'FSR 4 / DLSS quality'.\n3. In the game, F8 opens OptiScaler's menu (output, sharpness, debug views). Insert is still the port's menu and Home its anti-aliasing switch. Page Up, Page Down and End are OptiScaler's (its FPS counter and frame generation).\n4. To go back: Remove. Every file it added leaves the bin folder.\n\nThird-party: problems with OptiScaler itself go to its project, not to the port.\nProject: github.com/optiscaler/OptiScaler · License: GPL-3.0\nOpen the project page (source code and license)?":
|
||||
"OptiScaler (opcional)\n\nQué es: un programa aparte (equipo de OptiScaler, GPL-3.0) que toma la imagen, la profundidad y el movimiento que el port da a XeSS y ejecuta otro escalador sobre ellos: FSR 4, FSR 3.1, XeSS, DLSS o FSR 2.2.\n\n1. Descargar e instalar. El antialiasing del port debe estar en XeSS (el lanzador lo pone): con cualquier otro, OptiScaler no recibe nada.\n2. Elige la salida aquí (vale al iniciar el juego). El tamaño al que dibuja el juego sigue siendo 'FSR 4 / DLSS quality' del port.\n3. En el juego, F8 abre el menú de OptiScaler (salida, nitidez, vistas de depuración). Insert sigue siendo el menú del port e Inicio su cambio de antialiasing. Re Pág, Av Pág y Fin son de OptiScaler (su contador de FPS y su generación de fotogramas).\n4. Para volver: Quitar. Todo archivo que añadió sale de la carpeta bin.\n\nDe terceros: los problemas del propio OptiScaler van a su proyecto, no al port.\nProyecto: github.com/optiscaler/OptiScaler · Licencia: GPL-3.0\n¿Abrir la página del proyecto (código y licencia)?",
|
||||
'Off (no anti-aliasing)':
|
||||
'Desactivado (sin antialiasing)',
|
||||
'Off: no anti-aliasing at all, the sharpest picture with jagged edges (no smearing in motion).':
|
||||
'Desactivado: ningún antialiasing, la imagen más nítida con bordes dentados (sin borrón en movimiento).',
|
||||
'Effects':
|
||||
'Efectos',
|
||||
'Motion blur':
|
||||
'Desenfoque de movimiento',
|
||||
'Chromatic aberration':
|
||||
'Aberración cromática',
|
||||
'The blur when the camera or Delsin moves fast. Off gives a sharp picture in motion; it also changes at once in the game (Insert > Image).':
|
||||
'El desenfoque cuando la cámara o Delsin se mueven rápido. Desactivado da una imagen nítida en movimiento; también cambia al momento en el juego (Insert > Image).',
|
||||
'The red and blue fringes toward the edges of the screen. Turning it off helps players who feel sick with it; it also changes at once in the game (Insert > Image).':
|
||||
'Los bordes rojos y azules cerca de los lados de la pantalla. Desactivarla ayuda a quien se marea con ella; también cambia al momento en el juego (Insert > Image).',
|
||||
},
|
||||
}
|
||||
|
||||
# issn 11-04: the Achievements page (trophies). Kept apart from EXTRA so its additions do not
|
||||
# collide with other pages' texts.
|
||||
TROPHY_TEXTS = {
|
||||
'pt': {
|
||||
'Achievements': 'Conquistas',
|
||||
"The game's trophies: the ones unlocked in this port are in color, with the date.":
|
||||
'Os troféus do jogo: os liberados neste port ficam coloridos, com a data.',
|
||||
'In the game': 'No jogo',
|
||||
'Trophy popup': 'Aviso de troféu',
|
||||
"The trophy's icon and name in a top corner for a few seconds when the game unlocks one. Also in Insert > Overlay.":
|
||||
'O ícone e o nome do troféu num canto de cima por alguns segundos quando o jogo libera um. Também em Insert > Overlay.',
|
||||
'Trophy names (optional)': 'Nomes dos troféus (opcional)',
|
||||
"The names and descriptions in the game's trophy file are encrypted. Without a key the port uses its own list (English, Portuguese and Spanish, written from public trophy lists). If you have your own PS4 trophy key (the same one shadPS4 asks for), paste it here to see the game's own texts: it stays only on this PC, in user\\trophy-key.txt, and the port never ships or sends it. A name shows once its trophy is unlocked.":
|
||||
'Os nomes e as descrições no arquivo de troféus do jogo são criptografados. Sem uma chave, o port usa a lista própria dele (inglês, português e espanhol, escrita a partir de listas públicas de troféus). Se você tem a sua própria chave de troféus do PS4 (a mesma que o shadPS4 pede), cole aqui para ver os textos do próprio jogo: ela fica só neste PC, em user\\trophy-key.txt, e o port nunca a distribui nem envia. O nome aparece quando o troféu é liberado.',
|
||||
'Save key': 'Salvar chave',
|
||||
'Remove key': 'Remover chave',
|
||||
'Trophy key': 'Chave de troféus',
|
||||
'No key: names are not shown.': 'Sem chave: os nomes não aparecem.',
|
||||
"Key saved on this PC: the names are the game's own.": 'Chave salva neste PC: os nomes são os do próprio jogo.',
|
||||
"No key: the names come from the port's own list.": 'Sem chave: os nomes vêm da lista própria do port.',
|
||||
'The key is saved but the names could not be read: {}': 'A chave está salva, mas os nomes não puderam ser lidos: {}',
|
||||
'No trophy file in the game folder (sce_sys\\trophy\\trophy00.trp, or Sc0\\trophy beside it). Choose the game folder on the Game page.':
|
||||
'Nenhum arquivo de troféus na pasta do jogo (sce_sys\\trophy\\trophy00.trp, ou Sc0\\trophy ao lado). Escolha a pasta do jogo na página Jogo.',
|
||||
'{} of {} unlocked ({}%)': '{} de {} liberados ({}%)',
|
||||
'Platinum! Every trophy unlocked': 'Platinado com sucesso!',
|
||||
'Platinum': 'Platina', 'Gold': 'Ouro', 'Silver': 'Prata', 'Bronze': 'Bronze',
|
||||
'Trophy {}': 'Troféu {}',
|
||||
'Unlocked {}': 'Liberado em {}',
|
||||
'Locked': 'Bloqueado',
|
||||
'A trophy key is 32 hexadecimal digits (0-9, A-F).': 'A chave de troféus tem 32 dígitos hexadecimais (0-9, A-F).',
|
||||
'This key does not open the trophy names: {}': 'Esta chave não abre os nomes dos troféus: {}',
|
||||
'Could not save the key: {}': 'Não foi possível salvar a chave: {}',
|
||||
'Could not remove the key: {}': 'Não foi possível remover a chave: {}',
|
||||
},
|
||||
'es': {
|
||||
'Achievements': 'Logros',
|
||||
"The game's trophies: the ones unlocked in this port are in color, with the date.":
|
||||
'Los trofeos del juego: los desbloqueados en este port aparecen en color, con la fecha.',
|
||||
'In the game': 'En el juego',
|
||||
'Trophy popup': 'Aviso de trofeo',
|
||||
"The trophy's icon and name in a top corner for a few seconds when the game unlocks one. Also in Insert > Overlay.":
|
||||
'El icono y el nombre del trofeo en una esquina superior durante unos segundos cuando el juego desbloquea uno. También en Insert > Overlay.',
|
||||
'Trophy names (optional)': 'Nombres de los trofeos (opcional)',
|
||||
"The names and descriptions in the game's trophy file are encrypted. Without a key the port uses its own list (English, Portuguese and Spanish, written from public trophy lists). If you have your own PS4 trophy key (the same one shadPS4 asks for), paste it here to see the game's own texts: it stays only on this PC, in user\\trophy-key.txt, and the port never ships or sends it. A name shows once its trophy is unlocked.":
|
||||
'Los nombres y las descripciones del archivo de trofeos del juego están cifrados. Sin una clave, el port usa su propia lista (inglés, portugués y español, escrita a partir de listas públicas de trofeos). Si tienes tu propia clave de trofeos de PS4 (la misma que pide shadPS4), pégala aquí para ver los textos del propio juego: se queda solo en este PC, en user\\trophy-key.txt, y el port nunca la distribuye ni la envía. El nombre aparece cuando se desbloquea el trofeo.',
|
||||
'Save key': 'Guardar clave',
|
||||
'Remove key': 'Quitar clave',
|
||||
'Trophy key': 'Clave de trofeos',
|
||||
'No key: names are not shown.': 'Sin clave: los nombres no se muestran.',
|
||||
"Key saved on this PC: the names are the game's own.": 'Clave guardada en este PC: los nombres son los del propio juego.',
|
||||
"No key: the names come from the port's own list.": 'Sin clave: los nombres vienen de la lista propia del port.',
|
||||
'The key is saved but the names could not be read: {}': 'La clave está guardada, pero no se pudieron leer los nombres: {}',
|
||||
'No trophy file in the game folder (sce_sys\\trophy\\trophy00.trp, or Sc0\\trophy beside it). Choose the game folder on the Game page.':
|
||||
'No hay archivo de trofeos en la carpeta del juego (sce_sys\\trophy\\trophy00.trp, o Sc0\\trophy al lado). Elige la carpeta del juego en la página Juego.',
|
||||
'{} of {} unlocked ({}%)': '{} de {} desbloqueados ({}%)',
|
||||
'Platinum! Every trophy unlocked': '¡Platino! Todos los trofeos',
|
||||
'Platinum': 'Platino', 'Gold': 'Oro', 'Silver': 'Plata', 'Bronze': 'Bronce',
|
||||
'Trophy {}': 'Trofeo {}',
|
||||
'Unlocked {}': 'Desbloqueado el {}',
|
||||
'Locked': 'Bloqueado',
|
||||
'A trophy key is 32 hexadecimal digits (0-9, A-F).': 'Una clave de trofeos tiene 32 dígitos hexadecimales (0-9, A-F).',
|
||||
'This key does not open the trophy names: {}': 'Esta clave no abre los nombres de los trofeos: {}',
|
||||
'Could not save the key: {}': 'No se pudo guardar la clave: {}',
|
||||
'Could not remove the key: {}': 'No se pudo quitar la clave: {}',
|
||||
},
|
||||
}
|
||||
for _language, _texts in TROPHY_TEXTS.items():
|
||||
EXTRA.setdefault(_language, {}).update(_texts)
|
||||
|
||||
_tables = {}
|
||||
|
||||
|
||||
|
||||
+1550
-146
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,202 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
"""The launcher's widgets drawn by Pillow: rounded buttons and fields, switches, a chevron on the
|
||||
drop-downs, a slim scroll bar and the PLAY button, instead of ttk clam's square boxes.
|
||||
|
||||
The technique is the Bloodborne port's launcher theme (Supermedo's bloodborne_pc,
|
||||
launcher/bbport_theme.py, GPL-2.0-or-later): each image is drawn at four times its size and
|
||||
scaled down so its corners stay smooth at any display scale, then stretched as a nine-slice ttk
|
||||
element. ttk fills an image's transparent pixels with nothing, so each image is drawn over the
|
||||
colour of the surface it sits on, and every style used on a card has a Card. twin.
|
||||
|
||||
The colours are the launcher's own (Seattle at night: wet concrete, Delsin's smoke red, neon).
|
||||
"""
|
||||
from PIL import Image, ImageDraw, ImageTk
|
||||
|
||||
SS = 4 # supersampling of the drawn images
|
||||
|
||||
|
||||
def rgba(color, alpha=255):
|
||||
color = color.lstrip('#')
|
||||
return tuple(int(color[i:i + 2], 16) for i in (0, 2, 4)) + (alpha,)
|
||||
|
||||
|
||||
def mix(a, b, f):
|
||||
a, b = rgba(a), rgba(b)
|
||||
return '#' + ''.join(f'{round(x + (y - x) * f):02x}' for x, y in zip(a[:3], b[:3]))
|
||||
|
||||
|
||||
def rounded(size, radius, fill=None, outline=None, width=1.0):
|
||||
"""A rounded rectangle of `size` on a transparent image, drawn supersampled."""
|
||||
w, h = size
|
||||
image = Image.new('RGBA', (w * SS, h * SS), (0, 0, 0, 0))
|
||||
ImageDraw.Draw(image).rounded_rectangle(
|
||||
(0, 0, w * SS - 1, h * SS - 1), radius * SS, fill=fill and rgba(fill), outline=outline and rgba(outline),
|
||||
width=max(1, round(width * SS)) if outline else 0)
|
||||
return image.resize(size, Image.LANCZOS)
|
||||
|
||||
|
||||
def solid(image, bg):
|
||||
"""`image` over the colour `bg`, without transparency."""
|
||||
base = Image.new('RGBA', image.size, rgba(bg))
|
||||
base.alpha_composite(image.convert('RGBA'))
|
||||
return base.convert('RGB')
|
||||
|
||||
|
||||
def stretch(image, size, border):
|
||||
"""A nine-slice image (corners of `border` px) enlarged to `size`: the corners stay, the edges
|
||||
and the middle are repeated (they are flat). ttk would tile a small middle; a big one is drawn
|
||||
once."""
|
||||
w, h = image.size
|
||||
W, H = max(size[0], w), max(size[1], h)
|
||||
bl, bt, br, bb = (border,) * 4 if isinstance(border, int) else border
|
||||
out = Image.new(image.mode, (W, H))
|
||||
xs = [(0, bl, 0, bl), (bl, w - br, bl, W - br), (w - br, w, W - br, W)]
|
||||
ys = [(0, bt, 0, bt), (bt, h - bb, bt, H - bb), (h - bb, h, H - bb, H)]
|
||||
for sx0, sx1, dx0, dx1 in xs:
|
||||
for sy0, sy1, dy0, dy1 in ys:
|
||||
if sx1 > sx0 and sy1 > sy0 and dx1 > dx0 and dy1 > dy0:
|
||||
out.paste(image.crop((sx0, sy0, sx1, sy1)).resize((dx1 - dx0, dy1 - dy0), Image.NEAREST), (dx0, dy0))
|
||||
return out
|
||||
|
||||
|
||||
def vertical_gradient(size, top, bottom):
|
||||
w, h = size
|
||||
strip = Image.new('RGBA', (1, h))
|
||||
for y in range(h):
|
||||
strip.putpixel((0, y), rgba(mix(top, bottom, y / max(1, h - 1))))
|
||||
return strip.resize(size)
|
||||
|
||||
|
||||
class Theme:
|
||||
"""Draws the images (kept alive here: Tk keeps no reference) and fills a ttk.Style."""
|
||||
|
||||
def __init__(self, root, dpi, colors):
|
||||
self.root, self.dpi, self.c = root, dpi, colors
|
||||
self.keep = []
|
||||
|
||||
def px(self, value):
|
||||
return max(1, round(value * self.dpi))
|
||||
|
||||
def photo(self, image):
|
||||
photo = ImageTk.PhotoImage(image, master=self.root)
|
||||
self.keep.append(photo)
|
||||
return photo
|
||||
|
||||
def box(self, w, h, r, fill, outline, bg, big, border, width=1.0):
|
||||
image = solid(rounded((self.px(w), self.px(h)), self.px(r), fill, outline, self.dpi * width), bg)
|
||||
return self.photo(stretch(image, (self.px(big[0]), self.px(big[1])), self.px(border)))
|
||||
|
||||
def switch(self, on, track, knob, bg, gap=10):
|
||||
w, h, d = self.px(34), self.px(18), self.px(12)
|
||||
size = (w + self.px(gap), h)
|
||||
image = Image.new('RGBA', (size[0] * SS, size[1] * SS), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(image)
|
||||
draw.rounded_rectangle((0, 0, w * SS - 1, h * SS - 1), h * SS // 2, fill=rgba(track))
|
||||
x = w - (h - d) // 2 - d if on else (h - d) // 2
|
||||
draw.ellipse((x * SS, (h - d) / 2 * SS, (x + d) * SS, (h + d) / 2 * SS), fill=rgba(knob))
|
||||
return self.photo(solid(image.resize(size, Image.LANCZOS), bg))
|
||||
|
||||
def chevron(self, color, bg, width=26, height=14):
|
||||
size = (self.px(width), self.px(height))
|
||||
image = Image.new('RGBA', (size[0] * SS, size[1] * SS), (0, 0, 0, 0))
|
||||
cx, cy, s = size[0] / 2 - self.px(3), size[1] / 2, self.px(3.4)
|
||||
pts = [(cx - s, cy - s / 2), (cx, cy + s / 2), (cx + s, cy - s / 2)]
|
||||
ImageDraw.Draw(image).line([(x * SS, y * SS) for x, y in pts], fill=rgba(color),
|
||||
width=round(1.6 * self.dpi * SS), joint='curve')
|
||||
return self.photo(solid(image.resize(size, Image.LANCZOS), bg))
|
||||
|
||||
def play(self, top, bottom, line, bg):
|
||||
px = self.px
|
||||
w, h, r = px(160), px(54), px(10)
|
||||
fill = vertical_gradient((w * SS, h * SS), top, bottom)
|
||||
mask = rounded((w, h), r, '#ffffff').getchannel('A').resize((w * SS, h * SS))
|
||||
image = Image.new('RGBA', (w * SS, h * SS), (0, 0, 0, 0))
|
||||
image.paste(fill, (0, 0), mask)
|
||||
image = image.resize((w, h), Image.LANCZOS)
|
||||
image.alpha_composite(rounded((w, h), r, None, line, self.dpi))
|
||||
return self.photo(stretch(solid(image, bg), (px(520), h), (px(14), 0, px(14), 0)))
|
||||
|
||||
def style(self, s, surfaces, fonts):
|
||||
"""Fills the ttk.Style `s`. surfaces: {style prefix: colour under the widgets} ('' is the
|
||||
page). fonts: (body family, display font tuple for PLAY)."""
|
||||
c, px = self.c, self.px
|
||||
body, play_font = fonts
|
||||
s.theme_use('clam')
|
||||
for name in ('.', 'TButton', 'TCheckbutton', 'TRadiobutton', 'TCombobox', 'TEntry', 'TSpinbox',
|
||||
'TScrollbar', 'TLabel', 'TFrame'):
|
||||
s.map(name, background=[]) # clam lightens hovered and disabled widgets; ours keep their surface
|
||||
s.configure('ComboboxPopdownFrame', background=c['field'], bordercolor=c['border_strong'], relief='solid',
|
||||
borderwidth=1)
|
||||
label = lambda element: [(element, {'sticky': 'nswe', 'children': [
|
||||
('Button.padding', {'sticky': 'nswe', 'children': [('Button.label', {'sticky': 'nswe'})]})]})]
|
||||
for prefix, bg in surfaces.items():
|
||||
tag = 'Issn' + prefix.rstrip('.')
|
||||
button = lambda fill, line, bg=bg: self.box(40, 34, 8, fill, line, bg, (420, 96), 10)
|
||||
s.element_create(f'{tag}.Button.border', 'image', button(c['card_hover'], c['border_strong']),
|
||||
('disabled', button(c['field'], c['border'])),
|
||||
('pressed', button(c['field'], c['accent'])),
|
||||
('active', button(c['hover'], c['accent'])),
|
||||
border=px(10), padding=px(2), width=px(40), height=px(34), sticky='nsew')
|
||||
s.element_create(f'{tag}.Accent.border', 'image', button(c['accent'], c['accent_hi']),
|
||||
('disabled', button('#2a1416', '#3a2024')),
|
||||
('pressed', button(c['accent_dark'], c['accent_hi'])),
|
||||
('active', button(c['accent_hi'], '#ff7383')),
|
||||
border=px(10), padding=px(2), width=px(40), height=px(34), sticky='nsew')
|
||||
s.layout(prefix + 'TButton', label(f'{tag}.Button.border'))
|
||||
s.layout(prefix + 'Accent.TButton', label(f'{tag}.Accent.border'))
|
||||
field = lambda fill, line, bg=bg: self.box(40, 34, 7, fill, line, bg, (640, 64), 9)
|
||||
s.element_create(f'{tag}.field', 'image', field(c['field'], c['border_strong']),
|
||||
('disabled', field(bg, c['border'])), ('focus', field(c['field'], c['neon'])),
|
||||
('hover', field(c['field'], c['border_hover'])),
|
||||
border=px(9), padding=(px(3), px(2)), width=px(40), height=px(34), sticky='nsew')
|
||||
s.layout(prefix + 'TEntry', [(f'{tag}.field', {'sticky': 'nswe', 'children': [
|
||||
('Entry.padding', {'sticky': 'nswe', 'children': [('Entry.textarea', {'sticky': 'nswe'})]})]})])
|
||||
s.layout(prefix + 'TCombobox', [(f'{tag}.field', {'sticky': 'nswe', 'children': [
|
||||
('Issn.Combobox.downarrow', {'side': 'right', 'sticky': ''}),
|
||||
('Combobox.padding', {'sticky': 'nswe', 'children': [('Combobox.textarea', {'sticky': 'nswe'})]})]})])
|
||||
s.element_create(f'{tag}.switch', 'image', self.switch(False, '#2a2d38', '#8c90a0', bg),
|
||||
('disabled', 'selected', self.switch(True, '#3a1518', '#7d7470', bg)),
|
||||
('disabled', self.switch(False, '#1b1c22', '#45474f', bg)),
|
||||
('active', 'selected', self.switch(True, c['accent_hi'], '#ffffff', bg)),
|
||||
('selected', self.switch(True, c['accent'], '#ffffff', bg)),
|
||||
('active', self.switch(False, '#343846', '#b0b4c2', bg)), sticky='')
|
||||
s.layout(prefix + 'TCheckbutton', [('Checkbutton.padding', {'sticky': 'nswe', 'children': [
|
||||
(f'{tag}.switch', {'side': 'left', 'sticky': ''}),
|
||||
('Checkbutton.label', {'side': 'left', 'sticky': 'nswe'})]})])
|
||||
s.configure(prefix + 'TButton', background=bg, foreground=c['text'], padding=(px(14), px(6)),
|
||||
anchor='center', font=(body, 10))
|
||||
s.map(prefix + 'TButton', foreground=[('disabled', c['muted']), ('active', '#ffffff')])
|
||||
s.configure(prefix + 'Accent.TButton', background=bg, foreground='#ffffff', padding=(px(14), px(6)),
|
||||
anchor='center', font=(body, 10, 'bold'))
|
||||
s.configure(prefix + 'TCheckbutton', background=bg, foreground=c['text'], padding=(0, px(4)))
|
||||
s.map(prefix + 'TCheckbutton', background=[('active', bg)],
|
||||
foreground=[('disabled', c['muted']), ('active', '#ffffff')])
|
||||
for name in ('TEntry', 'TCombobox'):
|
||||
s.configure(prefix + name, background=bg, foreground=c['text'], padding=(px(9), px(5), px(4), px(5)),
|
||||
insertcolor=c['text'], selectbackground=c['accent'], selectforeground='#ffffff')
|
||||
s.map(prefix + name, foreground=[('disabled', c['muted'])], fieldbackground=[('readonly', c['field'])],
|
||||
selectbackground=[('readonly', c['field'])], selectforeground=[('readonly', c['text'])])
|
||||
s.element_create('Issn.Combobox.downarrow', 'image', self.chevron(c['neon'], c['field']),
|
||||
('disabled', self.chevron(c['muted'], c['panel'])), sticky='')
|
||||
# PLAY on the bottom bar.
|
||||
bar = surfaces.get('Bar.', c['bg'])
|
||||
s.element_create('Issn.Play.border', 'image', self.play('#ec3247', '#b8172a', '#ff6b7c', bar),
|
||||
('disabled', self.play('#262a35', '#1d2029', '#323645', bar)),
|
||||
('pressed', self.play('#b8172a', '#9a1222', '#ff6b7c', bar)),
|
||||
('active', self.play('#ff4a5e', '#cf1d31', '#ffffff', bar)),
|
||||
border=px(14), padding=px(2), width=px(160), height=px(54), sticky='nsew')
|
||||
s.layout('Play.TButton', label('Issn.Play.border'))
|
||||
s.configure('Play.TButton', background=bar, foreground='#ffffff', font=play_font, padding=(px(44), px(10)))
|
||||
s.map('Play.TButton', foreground=[('disabled', '#6d7080')])
|
||||
# A slim scroll bar.
|
||||
thumb = lambda fill: self.photo(stretch(solid(rounded((px(8), px(28)), px(3), fill), c['panel']),
|
||||
(px(8), px(1600)), (0, px(5), 0, px(5))))
|
||||
s.element_create('Issn.Vertical.Scrollbar.trough', 'image',
|
||||
self.photo(Image.new('RGB', (px(8), px(2400)), rgba(c['panel'])[:3])),
|
||||
width=px(8), height=px(8), sticky='ns')
|
||||
s.element_create('Issn.Vertical.Scrollbar.thumb', 'image', thumb(c['border_strong']),
|
||||
('pressed', thumb(c['accent'])), ('active', thumb(c['border_hover'])),
|
||||
border=(0, px(5)), width=px(8), height=px(28), sticky='nsew')
|
||||
s.layout('Vertical.TScrollbar', [('Issn.Vertical.Scrollbar.trough', {'sticky': 'ns', 'children': [
|
||||
('Issn.Vertical.Scrollbar.thumb', {'expand': '1', 'sticky': 'nswe'})]})])
|
||||
s.configure('Vertical.TScrollbar', background=c['panel'])
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
@@ -30,17 +30,19 @@ Graphics
|
||||
- Anti-aliasing (launcher: Graphics > Anti-aliasing): the game's own or FSR 4 (included; Radeon
|
||||
RX 7000 / 9000 on RADV). DLSS is Windows only (NVIDIA's Streamline). In the game, Home switches
|
||||
FSR 4 and the game's own anti-aliasing live.
|
||||
- Internal resolution and Performance mode (launcher: Performance): as on Windows.
|
||||
- Internal resolution and Performance mode (launcher: Performance), Screen shape (Graphics: ultrawide
|
||||
and other shapes; play fullscreen): as on Windows.
|
||||
- F12 saves a screenshot (user/screenshots), Insert opens the port's menu.
|
||||
|
||||
Controls
|
||||
- DualShock 4 and DualSense: touchpad, motion sensors and the light bar work as on the PS4.
|
||||
- Pads without a touchpad or motion sensors (Xbox, 8BitDo, ...): Select/Back is the touchpad
|
||||
click (with L1: the left side; hold it and push the right stick to swipe), L3 + R3 tilts the pad
|
||||
for the spray can, L1 rattles it, the right stick aims.
|
||||
for the spray can, L1 rattles it, the right stick aims (launcher: Spray can stick speed).
|
||||
- Keyboard and mouse: WASD moves, IJKL turns the camera; click in the game window and the mouse
|
||||
aims 1:1 (Y or F7 lets it go); left click shoots (R2), right click aims (L2); Space / Left Shift
|
||||
/ C / F are Cross / Circle / Square / Triangle; Z held tilts the spray can, X rattles it.
|
||||
/ C / F are Cross / Circle / Square / Triangle; Z held tilts the spray can, X rattles it, the
|
||||
mouse aims it (- / = change its speed).
|
||||
The launcher's Game > Controls page changes the buttons.
|
||||
|
||||
Data
|
||||
@@ -54,7 +56,7 @@ Reporting problems
|
||||
each thread was doing.
|
||||
|
||||
Credits: axylpup's shadPS4 build for inFAMOUS Second Son (graphics), earlier inFAMOUS work by
|
||||
Ayoub and HayatoG, the God of War III Remastered port (gow3_pc, Felipe Prado) and bloodborne_pc
|
||||
Ayoub and HayatoG (iss_pc, github.com/HayatoG/iss_pc: video core, draw distance, sensor mode), the God of War III Remastered port (gow3_pc, Felipe Prado) and bloodborne_pc
|
||||
(deadinside28, Supermedo) it is built on, the shadPS4 project. FSR 4: AMD FidelityFX (MIT).
|
||||
Launcher title face: Bebas Neue (SIL Open Font License). Source: github.com/Liondarkz/issn_pc
|
||||
(GPL-2.0-or-later).
|
||||
|
||||
@@ -26,6 +26,7 @@ install -m644 launcher/issn_font.ttf launcher/OFL-BebasNeue.txt "$share/launcher
|
||||
icon=launcher/issn.png
|
||||
[[ -f packaging/linux/issn.png ]] && icon=packaging/linux/issn.png
|
||||
install -m644 "$icon" "$share/launcher/issn.png"
|
||||
install -m644 launcher/pix_qr.png "$share/launcher/pix_qr.png"
|
||||
|
||||
# The launcher also runs run.py (--run) and the preparation scripts (--script) itself, as on
|
||||
# Windows: the standard modules those scripts import come along as hidden imports.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
issn on macOS (Apple silicon) - TEST BUILD
|
||||
==========================================
|
||||
|
||||
This is an early test build. The port compiles and its checks pass on an M4
|
||||
(Vulkan through MoltenVK: command submission + readback PASS; CPU: complete
|
||||
but SSE4A, which the port emulates as on Intel). It has NOT yet been run with
|
||||
the game: every report helps.
|
||||
|
||||
What you need
|
||||
- A Mac with Apple silicon, macOS 13 or newer, Rosetta 2
|
||||
(softwareupdate --install-rosetta).
|
||||
- Python 3 (python3; macOS offers to install it on first use).
|
||||
- Your own dump of the game, prepared as described in the main README.
|
||||
This package contains no game files.
|
||||
|
||||
Running
|
||||
1. Unzip, open Terminal in the folder, and run: bash run.sh
|
||||
(the same arguments and environment variables as on Linux).
|
||||
2. macOS may refuse the unsigned binaries the first time: run
|
||||
xattr -dr com.apple.quarantine . in the folder, then try again.
|
||||
|
||||
How it works on a Mac
|
||||
- The build is x86_64 and runs under Rosetta 2: the game's own x86-64 code
|
||||
runs as on a PC. issn-probe restarts itself with ROSETTA_ADVERTISE_AVX=1,
|
||||
which Rosetta needs before it reports AVX/AVX2/F16C/BMI in CPUID.
|
||||
- Graphics: Vulkan through MoltenVK (bin/libMoltenVK.dylib) onto Metal.
|
||||
- Memory: the guest packs pointers into 40 bits, so the whole process heap
|
||||
must stay below 1 TiB. A patched mimalloc (packaging/macos) keeps it in
|
||||
32-64 GiB instead of macOS's usual ~128 TiB.
|
||||
|
||||
Reporting
|
||||
Please send: the Mac model, the macOS version, what happened, and the whole
|
||||
terminal output (and out/*.log if there are any).
|
||||
|
||||
Building it yourself: see packaging/macos/README.md.
|
||||
@@ -0,0 +1,35 @@
|
||||
# issn on macOS
|
||||
|
||||
The port builds on macOS as an **x86_64** program that Rosetta 2 runs on Apple silicon, with
|
||||
Vulkan through MoltenVK. It is not a GPTK/Wine build: the same sources as on Linux, with a
|
||||
`__APPLE__` branch where the systems differ. Status: builds, `issn-probe --vulkan-only` and
|
||||
`--cpu-features` pass on an M4; not yet run with the game.
|
||||
|
||||
## Building
|
||||
|
||||
The repository path must not contain spaces (FFmpeg's configure does not accept them).
|
||||
|
||||
```sh
|
||||
brew install cmake ninja pkg-config boost # host tools; Boost is header-only here
|
||||
bash packaging/macos/build-deps-x86_64.sh # x86_64 dependencies into out/macos-deps (once)
|
||||
bash build.sh # out/issn-probe, out/gpu/libissngpu.dylib
|
||||
bash packaging/macos/package.sh # optional: dist/issn-macos-x86_64.zip for testers
|
||||
```
|
||||
|
||||
`ISSN_MAC_DEPS` points both scripts at another dependency prefix. The arm64 Homebrew is never
|
||||
linked: only Boost's headers are taken from it.
|
||||
|
||||
## What differs from Linux
|
||||
|
||||
| Linux | macOS |
|
||||
|---|---|
|
||||
| guest TLS: `arch_prctl(ARCH_SET_GS)` | a pthread key, read as `gs:[8*key]`; the `fs:` sites are rewritten as on Windows (`patch_tcb_loads`) |
|
||||
| host heap below 1 TiB: non-PIE brk heap, `mallopt` | mimalloc 3.5.3 patched to map only in 32-64 GiB (`mimalloc-os-window.patch`), linked first |
|
||||
| `ucontext` `gregs` | `uc_mcontext->__ss` |
|
||||
| `process_vm_readv`, `/proc/self/task` | `mach_vm_read_overwrite`, `task_threads` |
|
||||
| `memfd_create` | `shm_open` + immediate unlink |
|
||||
| timed mutex/rwlock waits, `CLOCK_MONOTONIC` waits | try + short sleep (`compat_mac.c`); `CLOCK_REALTIME` (as on Windows) |
|
||||
| `SCHED_IDLE` helpers | `QOS_CLASS_BACKGROUND` |
|
||||
| X11/Wayland surface | SDL Metal view, `VK_EXT_metal_surface` |
|
||||
| — | `VK_KHR_portability_enumeration` + `VK_KHR_portability_subset` (MoltenVK) |
|
||||
| — | Rosetta reports AVX in CPUID only with `ROSETTA_ADVERTISE_AVX=1`: `issn-probe` restarts itself with it |
|
||||
Executable
+124
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env bash
|
||||
# Dependências x86_64 (Intel, rodam no M4 via Rosetta 2) para o port issn_pc no macOS.
|
||||
# Instala tudo em out/macos-deps (ou ISSN_MAC_DEPS) sem tocar no Homebrew ARM (/opt/homebrew).
|
||||
# Idempotente: cada etapa concluída deixa um carimbo em $P/.stamps e é pulada na próxima vez.
|
||||
# O caminho do repositório não pode ter espaços (o configure do FFmpeg não aceita).
|
||||
set -euo pipefail
|
||||
|
||||
D="$(cd -- "$(dirname -- "$0")" && pwd)" # packaging/macos (o patch do mimalloc)
|
||||
ROOT="$(cd -- "$D/../.." && pwd)"
|
||||
P="${ISSN_MAC_DEPS:-$ROOT/out/macos-deps}" # prefixo de instalação (x86_64)
|
||||
S="$ROOT/out/macos-deps-src" # códigos-fonte
|
||||
mkdir -p "$P/.stamps" "$S"
|
||||
|
||||
export MACOSX_DEPLOYMENT_TARGET=13.0
|
||||
ARCH="-arch x86_64 -mmacosx-version-min=$MACOSX_DEPLOYMENT_TARGET"
|
||||
JOBS=$(sysctl -n hw.ncpu)
|
||||
|
||||
CMAKE_COMMON=(-G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||
-DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=$MACOSX_DEPLOYMENT_TARGET
|
||||
-DCMAKE_INSTALL_PREFIX="$P" -DCMAKE_PREFIX_PATH="$P"
|
||||
-DCMAKE_IGNORE_PREFIX_PATH=/opt/homebrew # nunca pegar biblioteca ARM do Homebrew
|
||||
-DCMAKE_POLICY_VERSION_MINIMUM=3.5)
|
||||
|
||||
step() { echo; echo "=================== $1 ($(date +%H:%M:%S))"; }
|
||||
done_() { touch "$P/.stamps/$1"; echo "OK: $1"; }
|
||||
have() { [[ -f "$P/.stamps/$1" ]]; }
|
||||
fetch() { # fetch <dir> <url> <tag> [--recursive]
|
||||
[[ -d "$S/$1/.git" ]] || git clone --depth 1 --branch "$3" ${4:-} "$2" "$S/$1" -q
|
||||
}
|
||||
cm() { # cm <name> <srcdir> [cmake args...]
|
||||
local name=$1 src=$2; shift 2
|
||||
cmake -S "$src" -B "$S/build-$name" "${CMAKE_COMMON[@]}" "$@" > "$S/build-$name.cmake.log"
|
||||
cmake --build "$S/build-$name" -j "$JOBS" > "$S/build-$name.build.log"
|
||||
cmake --install "$S/build-$name" > "$S/build-$name.install.log"
|
||||
}
|
||||
|
||||
if ! have vulkan-headers; then step "Vulkan-Headers"
|
||||
fetch Vulkan-Headers https://github.com/KhronosGroup/Vulkan-Headers.git vulkan-sdk-1.4.363.0
|
||||
cm vulkan-headers "$S/Vulkan-Headers"; done_ vulkan-headers; fi
|
||||
|
||||
if ! have vulkan-loader; then step "Vulkan-Loader"
|
||||
fetch Vulkan-Loader https://github.com/KhronosGroup/Vulkan-Loader.git vulkan-sdk-1.4.363.0
|
||||
cm vulkan-loader "$S/Vulkan-Loader" -DVULKAN_HEADERS_INSTALL_DIR="$P" -DBUILD_TESTS=OFF
|
||||
done_ vulkan-loader; fi
|
||||
|
||||
if ! have moltenvk; then step "MoltenVK v1.4.2 (binário universal oficial da Khronos)"
|
||||
mkdir -p "$S/moltenvk" && cd "$S/moltenvk"
|
||||
[[ -f MoltenVK-macos.tar ]] || curl -sSL -o MoltenVK-macos.tar \
|
||||
https://github.com/KhronosGroup/MoltenVK/releases/download/v1.4.2/MoltenVK-macos.tar
|
||||
rm -rf x && mkdir x && tar -xf MoltenVK-macos.tar -C x
|
||||
dylib=$(find x -name libMoltenVK.dylib -path '*macOS*' | head -1)
|
||||
[[ -n $dylib ]] || dylib=$(find x -name libMoltenVK.dylib | head -1)
|
||||
lipo -info "$dylib"
|
||||
mkdir -p "$P/lib" "$P/share/vulkan/icd.d"
|
||||
cp "$dylib" "$P/lib/libMoltenVK.dylib"
|
||||
printf '{\n "file_format_version": "1.0.0",\n "ICD": {\n "library_path": "../../../lib/libMoltenVK.dylib",\n "api_version": "1.4.0",\n "is_portability_driver": true\n }\n}\n' \
|
||||
> "$P/share/vulkan/icd.d/MoltenVK_icd.json"
|
||||
cd "$D"; done_ moltenvk; fi
|
||||
|
||||
if ! have vma; then step "VulkanMemoryAllocator (só cabeçalho)"
|
||||
fetch VMA https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git v3.5.0
|
||||
cm vma "$S/VMA"; done_ vma; fi
|
||||
|
||||
if ! have robin-map; then step "tsl-robin-map (só cabeçalho)"
|
||||
fetch robin-map https://github.com/Tessil/robin-map.git v1.4.1
|
||||
cm robin-map "$S/robin-map"; done_ robin-map; fi
|
||||
|
||||
if ! have fmt; then step "fmt 12.2.0"
|
||||
fetch fmt https://github.com/fmtlib/fmt.git 12.2.0
|
||||
cm fmt "$S/fmt" -DFMT_TEST=OFF -DFMT_DOC=OFF; done_ fmt; fi
|
||||
|
||||
if ! have xxhash; then step "xxHash 0.8.4"
|
||||
fetch xxHash https://github.com/Cyan4973/xxHash.git v0.8.4
|
||||
make -C "$S/xxHash" -j"$JOBS" CC="clang $ARCH" PREFIX="$P" lib > "$S/build-xxhash.log"
|
||||
make -C "$S/xxHash" CC="clang $ARCH" PREFIX="$P" install >> "$S/build-xxhash.log"
|
||||
done_ xxhash; fi
|
||||
|
||||
if ! have zydis; then step "Zydis 4.1.1"
|
||||
fetch zydis https://github.com/zyantific/zydis.git v4.1.1 --recursive
|
||||
cm zydis "$S/zydis" -DZYDIS_BUILD_TOOLS=OFF -DZYDIS_BUILD_EXAMPLES=OFF -DZYDIS_BUILD_SHARED_LIB=OFF
|
||||
done_ zydis; fi
|
||||
|
||||
if ! have zycore; then step "Zycore (a dependência do Zydis, instalada à parte para o zydis-config)"
|
||||
cm zycore "$S/zydis/dependencies/zycore" -DZYCORE_BUILD_SHARED_LIB=OFF
|
||||
done_ zycore; fi
|
||||
|
||||
if ! have mimalloc; then step "mimalloc 3.5.3 com janela de endereços abaixo de 1 TiB"
|
||||
# O guest do PS4 guarda ponteiros em 40 bits; o malloc do macOS fica perto de 128 TiB.
|
||||
# O patch (mimalloc-os-window.patch) põe toda memória do processo em 32–64 GiB.
|
||||
fetch mimalloc https://github.com/microsoft/mimalloc.git v3.5.3
|
||||
git -C "$S/mimalloc" apply --reverse --check "$D/mimalloc-os-window.patch" 2>/dev/null ||
|
||||
git -C "$S/mimalloc" apply "$D/mimalloc-os-window.patch"
|
||||
cm mimalloc "$S/mimalloc" -DMI_OVERRIDE=ON -DMI_OSX_ZONE=ON -DMI_OSX_INTERPOSE=ON \
|
||||
-DMI_BUILD_SHARED=OFF -DMI_BUILD_STATIC=ON -DMI_BUILD_OBJECT=ON -DMI_BUILD_TESTS=OFF \
|
||||
"-DCMAKE_C_FLAGS=-DMI_OS_WINDOW_START=0x800000000ULL -DMI_OS_WINDOW_END=0x1000000000ULL"
|
||||
done_ mimalloc; fi
|
||||
|
||||
if ! have sdl3; then step "SDL 3.4.18"
|
||||
fetch SDL https://github.com/libsdl-org/SDL.git release-3.4.18
|
||||
cm sdl3 "$S/SDL" -DSDL_SHARED=ON -DSDL_STATIC=OFF -DSDL_TESTS=OFF -DSDL_EXAMPLES=OFF
|
||||
done_ sdl3; fi
|
||||
|
||||
if ! have glslang; then step "glslang 16.6.0"
|
||||
fetch glslang https://github.com/KhronosGroup/glslang.git 16.6.0
|
||||
cm glslang "$S/glslang" -DENABLE_OPT=OFF -DGLSLANG_TESTS=OFF -DBUILD_SHARED_LIBS=OFF \
|
||||
-DENABLE_GLSLANG_BINARIES=ON -DGLSLANG_ENABLE_INSTALL=ON
|
||||
done_ glslang; fi
|
||||
|
||||
if ! have ffmpeg; then step "FFmpeg 9.0.2 (só as bibliotecas que o port usa)"
|
||||
fetch FFmpeg https://github.com/FFmpeg/FFmpeg.git n9.0.2
|
||||
cd "$S/FFmpeg"
|
||||
./configure --prefix="$P" --arch=x86_64 --target-os=darwin --enable-cross-compile \
|
||||
--cc="clang $ARCH" --cxx="clang++ $ARCH" --nm=nm --ar=ar --ranlib=ranlib \
|
||||
--enable-shared --disable-static --disable-programs --disable-doc \
|
||||
--disable-autodetect --disable-network --disable-avdevice --disable-avfilter \
|
||||
> "$S/build-ffmpeg.configure.log"
|
||||
make -j"$JOBS" > "$S/build-ffmpeg.build.log" 2>&1
|
||||
make install > "$S/build-ffmpeg.install.log"
|
||||
cd "$D"; done_ ffmpeg; fi
|
||||
|
||||
step "Verificação"
|
||||
for f in "$P"/lib/*.dylib; do [[ -L $f ]] || printf '%-34s %s\n' "$(basename "$f")" "$(lipo -archs "$f")"; done
|
||||
echo "pkg-config:"; PKG_CONFIG_LIBDIR="$P/lib/pkgconfig:$P/share/pkgconfig" pkg-config --modversion sdl3 libxxhash libavcodec vulkan 2>&1
|
||||
echo; echo "TODAS AS DEPENDÊNCIAS PRONTAS ($(date +%H:%M:%S))"
|
||||
@@ -0,0 +1,38 @@
|
||||
diff --git a/src/prim/unix/prim.c b/src/prim/unix/prim.c
|
||||
index 25c2ade..d5fa76c 100644
|
||||
--- a/src/prim/unix/prim.c
|
||||
+++ b/src/prim/unix/prim.c
|
||||
@@ -305,7 +305,33 @@ static int unix_madvise(void* addr, size_t size, int advice) {
|
||||
#endif
|
||||
}
|
||||
|
||||
+#if defined(MI_OS_WINDOW_START) && defined(MI_OS_WINDOW_END)
|
||||
+// Optional address window: every non-fixed OS allocation is placed in
|
||||
+// [MI_OS_WINDOW_START, MI_OS_WINDOW_END). issn_pc builds mimalloc with a window below 1 TiB on
|
||||
+// macOS, where the system heap lives near 128 TiB: heap pointers then stay small, like a Linux
|
||||
+// brk heap (its PS4 guest code packs pointers into 40 bits).
|
||||
+static uintptr_t mi_os_window_next = MI_OS_WINDOW_START;
|
||||
+static void* unix_mmap_window(size_t size, int protect_flags, int flags, int fd) {
|
||||
+ const uintptr_t align = (uintptr_t)64 << 20; // 64 MiB covers the arena and page alignments
|
||||
+ for (;;) {
|
||||
+ uintptr_t at = __atomic_load_n(&mi_os_window_next, __ATOMIC_RELAXED);
|
||||
+ const uintptr_t start = (at + align - 1) & ~(align - 1);
|
||||
+ if (start + size > (uintptr_t)MI_OS_WINDOW_END || start + size < start) { errno = ENOMEM; return MAP_FAILED; }
|
||||
+ if (!__atomic_compare_exchange_n(&mi_os_window_next, &at, start + size, true, __ATOMIC_ACQ_REL, __ATOMIC_RELAXED)) continue;
|
||||
+ void* p = mmap((void*)start, size, protect_flags, flags, fd, 0);
|
||||
+ if (p == (void*)start) return p;
|
||||
+ if (p != MAP_FAILED) munmap(p, size); // that range is taken: the kernel placed it elsewhere
|
||||
+ }
|
||||
+}
|
||||
+#endif
|
||||
+
|
||||
static void* unix_mmap_prim(void* addr, size_t size, int protect_flags, int flags, int fd) {
|
||||
+ #if defined(MI_OS_WINDOW_START) && defined(MI_OS_WINDOW_END)
|
||||
+ if ((flags & MAP_FIXED) == 0 &&
|
||||
+ ((uintptr_t)addr < (uintptr_t)MI_OS_WINDOW_START || (uintptr_t)addr >= (uintptr_t)MI_OS_WINDOW_END)) {
|
||||
+ return unix_mmap_window(size, protect_flags, flags, fd);
|
||||
+ }
|
||||
+ #endif
|
||||
void* p = mmap(addr, size, protect_flags, flags, fd, 0 /* offset */);
|
||||
#if defined(__linux__) && defined(PR_SET_VMA)
|
||||
if (p!=MAP_FAILED && p!=NULL) {
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds dist/issn-macos-x86_64.zip: the port's current macOS build (run build.sh first), its
|
||||
# scripts and every non-system library it loads, with relative install names, so a tester
|
||||
# needs neither Homebrew nor the dependency build. Unzip, then: bash run.sh (ISSN_PREBUILT=1).
|
||||
set -euo pipefail
|
||||
cd -- "$(dirname -- "$0")/../.."
|
||||
deps=${ISSN_MAC_DEPS:-$PWD/out/macos-deps}
|
||||
[[ -f out/issn-probe && -f out/gpu/libissngpu.dylib ]] || { echo 'Build first: bash build.sh' >&2; exit 1; }
|
||||
name=issn-macos-x86_64
|
||||
stage=dist/$name
|
||||
rm -rf "$stage" "dist/$name.zip" && mkdir -p "$stage/bin/gpu"
|
||||
cp out/issn-probe out/issn-gpu-capabilities "$stage/bin/"
|
||||
cp out/gpu/libissngpu.dylib "$stage/bin/gpu/"
|
||||
cp "$deps/lib/libMoltenVK.dylib" "$stage/bin/"
|
||||
# shadPS4 points the Vulkan loader at the executable's directory; the path is the file's own.
|
||||
printf '{"file_format_version":"1.0.0","ICD":{"library_path":"./libMoltenVK.dylib","api_version":"1.4.0","is_portability_driver":true}}\n' \
|
||||
> "$stage/bin/MoltenVK_icd.json"
|
||||
|
||||
# The libraries of $deps a Mach-O file loads, by install name (absolute or @rpath/).
|
||||
deps_of() {
|
||||
otool -L "$1" | tail -n +2 | awk '{print $1}' | while read -r dep; do
|
||||
case $dep in
|
||||
"$deps"/lib/*|@rpath/*) [[ $dep == */libissngpu.dylib ]] || echo "$dep" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
# Copies them into bin/ until nothing new appears (FFmpeg's libraries load each other).
|
||||
queue=("$stage/bin/issn-probe" "$stage/bin/issn-gpu-capabilities" "$stage/bin/gpu/libissngpu.dylib")
|
||||
while ((${#queue[@]})); do
|
||||
file=${queue[0]} queue=("${queue[@]:1}")
|
||||
for dep in $(deps_of "$file"); do
|
||||
base=$(basename "$dep")
|
||||
if [[ ! -f $stage/bin/$base ]]; then
|
||||
cp -L "$deps/lib/$base" "$stage/bin/$base"
|
||||
chmod u+w "$stage/bin/$base"
|
||||
install_name_tool -id "@rpath/$base" "$stage/bin/$base" 2>/dev/null
|
||||
queue+=("$stage/bin/$base")
|
||||
fi
|
||||
install_name_tool -change "$dep" "@rpath/$base" "$file" 2>/dev/null
|
||||
done
|
||||
done
|
||||
# Search paths: bin/ for everything, never the build machine's directories.
|
||||
for file in "$stage"/bin/issn-probe "$stage"/bin/issn-gpu-capabilities "$stage"/bin/gpu/libissngpu.dylib "$stage"/bin/*.dylib; do
|
||||
for path in $(otool -l "$file" | awk '/LC_RPATH/{getline; getline; print $2}'); do
|
||||
[[ $path == @* ]] || install_name_tool -delete_rpath "$path" "$file" 2>/dev/null
|
||||
done
|
||||
done
|
||||
install_name_tool -add_rpath @loader_path "$stage/bin/issn-probe" 2>/dev/null || true
|
||||
install_name_tool -add_rpath @loader_path "$stage/bin/issn-gpu-capabilities" 2>/dev/null || true
|
||||
install_name_tool -add_rpath @loader_path/.. "$stage/bin/gpu/libissngpu.dylib" 2>/dev/null || true
|
||||
install_name_tool -add_rpath @loader_path "$stage/bin/gpu/libissngpu.dylib" 2>/dev/null || true
|
||||
# install_name_tool invalidates signatures; Apple silicon (Rosetta included) needs one: ad hoc.
|
||||
for file in "$stage"/bin/issn-probe "$stage"/bin/issn-gpu-capabilities "$stage"/bin/gpu/*.dylib "$stage"/bin/*.dylib; do
|
||||
codesign --force --sign - "$file" 2>/dev/null
|
||||
done
|
||||
|
||||
cp -R run.sh run.py scripts launcher patches LICENSE "$stage/"
|
||||
cp packaging/macos/README-macOS.txt "$stage/"
|
||||
# run.sh on a packaged build: nothing is built (ISSN_PREBUILT).
|
||||
sed -i '' 's/^data=\${ISSN_DATA_DIR:-.}$/data=${ISSN_DATA_DIR:-.}; export ISSN_PREBUILT=${ISSN_PREBUILT:-1}/' "$stage/run.sh"
|
||||
(cd dist && ditto -c -k --keepParent "$name" "$name.zip")
|
||||
ls -lh "dist/$name.zip"
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"version": "0.3.6.1",
|
||||
"en": {
|
||||
"sub": "PC PORT / UPDATE 0.3.6.1",
|
||||
"hero": "STUTTER FIX",
|
||||
"hero_sub": "After updating to 0.3.6 from the launcher, the game could start without its shader seed and compile every shader while playing. 0.3.6.1 puts the seed in place.",
|
||||
"hero_pts": [
|
||||
"Update from the launcher: it fixes itself",
|
||||
"New test: FSR 3.1 frame generation"
|
||||
],
|
||||
"shot": "PERFORMANCE · FRAME GENERATION",
|
||||
"new": "WHAT'S NEW",
|
||||
"cards": [
|
||||
[
|
||||
"red",
|
||||
"Shader seed after updating",
|
||||
"No more empty cache and heavy stutter."
|
||||
],
|
||||
[
|
||||
"neon",
|
||||
"Frame generation (test)",
|
||||
"FSR 3.1: a frame between every two of the game."
|
||||
],
|
||||
[
|
||||
"pink",
|
||||
"FPS counter like MSI",
|
||||
"Frames on screen, with the game's own beside them."
|
||||
],
|
||||
[
|
||||
"green",
|
||||
"Clearer logs",
|
||||
"The log names the shader cache folder in use."
|
||||
],
|
||||
[
|
||||
"gold",
|
||||
"Mac version started",
|
||||
"Community work by Sanvtto, experimental, not out yet."
|
||||
],
|
||||
[
|
||||
"neon",
|
||||
"Launcher tweaks",
|
||||
"Recommended preset on its own row."
|
||||
]
|
||||
],
|
||||
"also": "ALSO",
|
||||
"also_pts": [
|
||||
"Frame generation needs FSR 4, XeSS or DLSS",
|
||||
"Best with frame pacing at half"
|
||||
],
|
||||
"next": "COMING NEXT",
|
||||
"next_tag": "planned for the next updates",
|
||||
"next_pts": [
|
||||
"Frame generation with a sharp HUD",
|
||||
"DLSS Frame Generation (NVIDIA RTX)",
|
||||
"Fewer hitches in combat",
|
||||
"Launcher: guided first start and controller test"
|
||||
],
|
||||
"disc": "JOIN THE DISCORD",
|
||||
"disc_sub": "News, help and test builds"
|
||||
},
|
||||
"pt": {
|
||||
"sub": "PORT PARA PC / ATUALIZAÇÃO 0.3.6.1",
|
||||
"hero": "FIM DAS ENGASGADAS",
|
||||
"hero_sub": "Depois de atualizar para a 0.3.6 pelo launcher, o jogo podia abrir sem a semente de shaders e compilar tudo enquanto você jogava. A 0.3.6.1 coloca a semente no lugar.",
|
||||
"hero_pts": [
|
||||
"Atualize pelo launcher: corrige sozinho",
|
||||
"Novo teste: geração de quadros FSR 3.1"
|
||||
],
|
||||
"shot": "DESEMPENHO · GERAÇÃO DE QUADROS",
|
||||
"new": "NOVIDADES",
|
||||
"cards": [
|
||||
[
|
||||
"red",
|
||||
"Semente de shaders ao atualizar",
|
||||
"Sem cache vazio e sem engasgos pesados."
|
||||
],
|
||||
[
|
||||
"neon",
|
||||
"Geração de quadros (teste)",
|
||||
"FSR 3.1: um quadro entre cada dois do jogo."
|
||||
],
|
||||
[
|
||||
"pink",
|
||||
"Contador de FPS igual ao MSI",
|
||||
"Quadros na tela, com os do jogo ao lado."
|
||||
],
|
||||
[
|
||||
"green",
|
||||
"Logs mais claros",
|
||||
"O log mostra a pasta de cache em uso."
|
||||
],
|
||||
[
|
||||
"gold",
|
||||
"Versão para Mac começou",
|
||||
"Trabalho do Sanvtto, experimental, ainda sem lançamento."
|
||||
],
|
||||
[
|
||||
"neon",
|
||||
"Ajustes no launcher",
|
||||
"Preset Recomendado numa linha própria."
|
||||
]
|
||||
],
|
||||
"also": "E MAIS",
|
||||
"also_pts": [
|
||||
"Geração de quadros precisa de FSR 4, XeSS ou DLSS",
|
||||
"Melhor com o ritmo na metade"
|
||||
],
|
||||
"next": "PRÓXIMA ATUALIZAÇÃO",
|
||||
"next_tag": "planejado para as próximas versões",
|
||||
"next_pts": [
|
||||
"Geração de quadros com HUD nítido",
|
||||
"DLSS Frame Generation (NVIDIA RTX)",
|
||||
"Menos engasgos em combate",
|
||||
"Launcher: primeira abertura guiada e teste do controle"
|
||||
],
|
||||
"disc": "ENTRE NO DISCORD",
|
||||
"disc_sub": "Novidades, ajuda e builds de teste"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"version": "0.3.6.2",
|
||||
"en": {
|
||||
"sub": "PC PORT / UPDATE 0.3.6.2",
|
||||
"hero": "BRING YOUR SAVES",
|
||||
"hero_sub": "Coming from HayatoG's iss_pc? Both ports keep saves the same way, so the launcher now imports them in one click, with no conversion.",
|
||||
"hero_pts": [
|
||||
"Game > Saves > Import from another port",
|
||||
"Your current saves are backed up first"
|
||||
],
|
||||
"shot": "GAME · SAVES",
|
||||
"new": "WHAT'S NEW",
|
||||
"cards": [
|
||||
[
|
||||
"red",
|
||||
"Import saves",
|
||||
"From HayatoG's iss_pc or an old folder of this port."
|
||||
],
|
||||
[
|
||||
"green",
|
||||
"Backup before importing",
|
||||
"Your current saves go to Save backups first."
|
||||
],
|
||||
[
|
||||
"neon",
|
||||
"Finds the other port",
|
||||
"Reads its launcher's data folder by itself."
|
||||
],
|
||||
[
|
||||
"pink",
|
||||
"Smoother frame generation",
|
||||
"No extra wait with V-Sync: the game keeps its FPS."
|
||||
],
|
||||
[
|
||||
"gold",
|
||||
"System requirements",
|
||||
"Minimum and recommended PCs in the README."
|
||||
],
|
||||
[
|
||||
"neon",
|
||||
"Controller drift logs",
|
||||
"Stick values in the log to track down drift."
|
||||
]
|
||||
],
|
||||
"also": "ALSO",
|
||||
"also_pts": [
|
||||
"Same layout: savedata\\1\\CUSA00223",
|
||||
"Pick any level: port, user or savedata folder"
|
||||
],
|
||||
"next": "COMING NEXT",
|
||||
"next_tag": "planned for the next updates",
|
||||
"next_pts": [
|
||||
"Frame generation with a sharp HUD",
|
||||
"Option to turn off motion blur",
|
||||
"Support for CPUs without F16C",
|
||||
"Multi-frame generation"
|
||||
],
|
||||
"disc": "JOIN THE DISCORD",
|
||||
"disc_sub": "News, help and test builds"
|
||||
},
|
||||
"pt": {
|
||||
"sub": "PORT PARA PC / ATUALIZAÇÃO 0.3.6.2",
|
||||
"hero": "TRAGA SEUS SAVES",
|
||||
"hero_sub": "Vindo do iss_pc do HayatoG? Os dois ports guardam os saves do mesmo jeito, então o launcher agora importa com um clique, sem conversão.",
|
||||
"hero_pts": [
|
||||
"Jogo > Saves > Importar de outro port",
|
||||
"Os saves atuais passam por backup antes"
|
||||
],
|
||||
"shot": "JOGO · SAVES",
|
||||
"new": "NOVIDADES",
|
||||
"cards": [
|
||||
[
|
||||
"red",
|
||||
"Importar saves",
|
||||
"Do iss_pc do HayatoG ou de uma pasta antiga deste port."
|
||||
],
|
||||
[
|
||||
"green",
|
||||
"Backup antes de importar",
|
||||
"Os saves atuais vão para os Backups de saves."
|
||||
],
|
||||
[
|
||||
"neon",
|
||||
"Acha o outro port sozinho",
|
||||
"Lê a pasta de dados do launcher dele."
|
||||
],
|
||||
[
|
||||
"pink",
|
||||
"Geração de quadros mais leve",
|
||||
"Sem espera extra com V-Sync: o jogo mantém o FPS."
|
||||
],
|
||||
[
|
||||
"gold",
|
||||
"Requisitos do sistema",
|
||||
"PC mínimo e recomendado no README."
|
||||
],
|
||||
[
|
||||
"neon",
|
||||
"Logs do drift do controle",
|
||||
"Valores dos analógicos no log para achar o drift."
|
||||
]
|
||||
],
|
||||
"also": "E MAIS",
|
||||
"also_pts": [
|
||||
"Mesmo formato: savedata\\1\\CUSA00223",
|
||||
"Serve a pasta do port, user ou savedata"
|
||||
],
|
||||
"next": "PRÓXIMA ATUALIZAÇÃO",
|
||||
"next_tag": "planejado para as próximas versões",
|
||||
"next_pts": [
|
||||
"Geração de quadros com HUD nítido",
|
||||
"Opção para desligar o motion blur",
|
||||
"Suporte a processadores sem F16C",
|
||||
"Geração de vários quadros (multi frame gen)"
|
||||
],
|
||||
"disc": "ENTRE NO DISCORD",
|
||||
"disc_sub": "Novidades, ajuda e builds de teste"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"version": "0.3.6.3",
|
||||
"en": {
|
||||
"sub": "PC PORT / UPDATE 0.3.6.3",
|
||||
"hero": "TUNE IT IN-GAME",
|
||||
"hero_sub": "Press Insert (or R3 + L2) while playing: draw distance, texture filtering and frame generation change on the spot, no restart.",
|
||||
"hero_pts": [
|
||||
"Image tab: draw distance, filtering, frame gen",
|
||||
"Game tab: God Mode and other cheats for Delsin"
|
||||
],
|
||||
"shot": "IN-GAME MENU",
|
||||
"new": "WHAT'S NEW",
|
||||
"cards": [
|
||||
[
|
||||
"red",
|
||||
"Cheats for Delsin",
|
||||
"God Mode, infinite health and powers, Karma Bomb."
|
||||
],
|
||||
[
|
||||
"pink",
|
||||
"Sharp HUD with frame gen",
|
||||
"The HUD is laid over the generated frame."
|
||||
],
|
||||
[
|
||||
"neon",
|
||||
"No more Delsin smear",
|
||||
"Frame gen keeps him whole in motion."
|
||||
],
|
||||
[
|
||||
"green",
|
||||
"\"Game saved\" note",
|
||||
"Shows on screen each time the game saves."
|
||||
],
|
||||
[
|
||||
"gold",
|
||||
"Older CPUs run it",
|
||||
"CPUs without F16C (Intel 2nd gen) now start."
|
||||
],
|
||||
[
|
||||
"neon",
|
||||
"Less VRAM trouble",
|
||||
"Textures follow the card's real memory budget."
|
||||
]
|
||||
],
|
||||
"also": "ALSO",
|
||||
"also_pts": [
|
||||
"Safer save backups: one a day for 14 days",
|
||||
"Saves from any region import; HDR picture fixed"
|
||||
],
|
||||
"next": "COMING NEXT",
|
||||
"next_tag": "planned for the next updates",
|
||||
"next_pts": [
|
||||
"Fix for the dark nights in some areas",
|
||||
"Turn off motion blur and chromatic aberration",
|
||||
"Save now button (Ctrl + Shift + S)",
|
||||
"Field of view back, with the aim fixed"
|
||||
],
|
||||
"disc": "JOIN THE DISCORD",
|
||||
"disc_sub": "News, help and test builds"
|
||||
},
|
||||
"pt": {
|
||||
"sub": "PORT PARA PC / ATUALIZAÇÃO 0.3.6.3",
|
||||
"hero": "AJUSTE DENTRO DO JOGO",
|
||||
"hero_sub": "Aperte Insert (ou R3 + L2) jogando: distância de visão, filtro de textura e geração de quadros mudam na hora, sem reiniciar.",
|
||||
"hero_pts": [
|
||||
"Aba Image: distância de visão, filtro, frame gen",
|
||||
"Aba Game: God Mode e outros cheats para o Delsin"
|
||||
],
|
||||
"shot": "MENU NO JOGO",
|
||||
"new": "NOVIDADES",
|
||||
"cards": [
|
||||
[
|
||||
"red",
|
||||
"Cheats para o Delsin",
|
||||
"God Mode, vida e poderes infinitos, Karma Bomb."
|
||||
],
|
||||
[
|
||||
"pink",
|
||||
"HUD nítido com frame gen",
|
||||
"O HUD vai por cima do quadro gerado."
|
||||
],
|
||||
[
|
||||
"neon",
|
||||
"Delsin sem borrão",
|
||||
"A geração de quadros mantém ele inteiro."
|
||||
],
|
||||
[
|
||||
"green",
|
||||
"Aviso de \"Jogo salvo\"",
|
||||
"Aparece na tela sempre que o jogo salva."
|
||||
],
|
||||
[
|
||||
"gold",
|
||||
"Roda em CPUs antigas",
|
||||
"CPUs sem F16C (Intel 2ª geração) agora abrem."
|
||||
],
|
||||
[
|
||||
"neon",
|
||||
"Menos problema de VRAM",
|
||||
"Texturas seguem a memória real da placa."
|
||||
]
|
||||
],
|
||||
"also": "E MAIS",
|
||||
"also_pts": [
|
||||
"Backups de save mais seguros: um por dia por 14 dias",
|
||||
"Saves de qualquer região importam; HDR corrigido"
|
||||
],
|
||||
"next": "PRÓXIMA ATUALIZAÇÃO",
|
||||
"next_tag": "planejado para as próximas versões",
|
||||
"next_pts": [
|
||||
"Correção das noites escuras em algumas áreas",
|
||||
"Desligar motion blur e aberração cromática",
|
||||
"Botão Salvar agora (Ctrl + Shift + S)",
|
||||
"Campo de visão de volta, com a mira certa"
|
||||
],
|
||||
"disc": "ENTRE NO DISCORD",
|
||||
"disc_sub": "Novidades, ajuda e builds de teste"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"version": "0.3.6",
|
||||
"en": {
|
||||
"sub": "PC PORT / UPDATE 0.3.6",
|
||||
"hero": "XeSS IS HERE",
|
||||
"hero_sub": "Intel XeSS anti-aliasing and upscaling. Runs on Intel Arc, Radeon RX 5000+ and GeForce GTX 10+: cards without FSR 4 or DLSS get it too.",
|
||||
"hero_pts": [
|
||||
"XeSS and DLSS files download by themselves",
|
||||
"About 1 ms per frame on an RX 9060 XT"
|
||||
],
|
||||
"shot": "NEW LAUNCHER · YOUR PC",
|
||||
"new": "WHAT'S NEW",
|
||||
"cards": [
|
||||
[
|
||||
"red",
|
||||
"Recommended preset",
|
||||
"One click: the best settings for most PCs."
|
||||
],
|
||||
[
|
||||
"neon",
|
||||
"Faster Windows build",
|
||||
"About 100 FPS in the city on a Ryzen 7 3700X."
|
||||
],
|
||||
[
|
||||
"pink",
|
||||
"Smooth frame pacing",
|
||||
"Even frames at half the refresh rate, no G-Sync needed."
|
||||
],
|
||||
[
|
||||
"green",
|
||||
"Less streaming stutter",
|
||||
"File reads no longer fault page by page."
|
||||
],
|
||||
[
|
||||
"gold",
|
||||
"Change keys in game",
|
||||
"Menu > Controls: press the new key, done."
|
||||
],
|
||||
[
|
||||
"neon",
|
||||
"New launcher",
|
||||
"Side bar, What's new page, Help that reads your log."
|
||||
]
|
||||
],
|
||||
"also": "ALSO",
|
||||
"also_pts": [
|
||||
"Esc opens Options",
|
||||
"R + mouse = touchpad finger",
|
||||
"Skip the intro logos",
|
||||
"Real FPS counter with frame pacing"
|
||||
],
|
||||
"next": "COMING NEXT",
|
||||
"next_tag": "planned for the next updates",
|
||||
"next_pts": [
|
||||
"FSR 3.1 Frame Generation (all GPUs)",
|
||||
"DLSS Frame Generation (NVIDIA RTX)",
|
||||
"Fewer hitches in combat",
|
||||
"Launcher: guided first start and controller test"
|
||||
],
|
||||
"disc": "JOIN THE DISCORD",
|
||||
"disc_sub": "News, help and test builds"
|
||||
},
|
||||
"pt": {
|
||||
"sub": "PORT PARA PC / ATUALIZAÇÃO 0.3.6",
|
||||
"hero": "CHEGOU O XeSS",
|
||||
"hero_sub": "Anti-aliasing e upscaling XeSS da Intel. Roda em Intel Arc, Radeon RX 5000+ e GeForce GTX 10+: quem não tem FSR 4 nem DLSS também ganha.",
|
||||
"hero_pts": [
|
||||
"Arquivos do XeSS e do DLSS baixam sozinhos",
|
||||
"Cerca de 1 ms por quadro numa RX 9060 XT"
|
||||
],
|
||||
"shot": "LAUNCHER NOVO · SEU PC",
|
||||
"new": "NOVIDADES",
|
||||
"cards": [
|
||||
[
|
||||
"red",
|
||||
"Preset Recomendado",
|
||||
"Um clique: o melhor ajuste na maioria dos PCs."
|
||||
],
|
||||
[
|
||||
"neon",
|
||||
"Build do Windows mais rápida",
|
||||
"Cerca de 100 FPS na cidade num Ryzen 7 3700X."
|
||||
],
|
||||
[
|
||||
"pink",
|
||||
"Ritmo dos quadros liso",
|
||||
"Quadros por igual na metade da taxa da tela, sem G-Sync."
|
||||
],
|
||||
[
|
||||
"green",
|
||||
"Menos engasgos ao carregar",
|
||||
"A leitura de arquivos não falha mais página por página."
|
||||
],
|
||||
[
|
||||
"gold",
|
||||
"Trocar teclas no jogo",
|
||||
"Menu > Controles: aperte a tecla nova e pronto."
|
||||
],
|
||||
[
|
||||
"neon",
|
||||
"Launcher novo",
|
||||
"Barra lateral, página de Novidades, Ajuda que lê o seu log."
|
||||
]
|
||||
],
|
||||
"also": "E MAIS",
|
||||
"also_pts": [
|
||||
"Esc abre o Options",
|
||||
"R + mouse = dedo no touchpad",
|
||||
"Pular os logos de abertura",
|
||||
"Contador de FPS real com o ritmo"
|
||||
],
|
||||
"next": "PRÓXIMA ATUALIZAÇÃO",
|
||||
"next_tag": "planejado para as próximas versões",
|
||||
"next_pts": [
|
||||
"FSR 3.1 Frame Generation (todas as placas)",
|
||||
"DLSS Frame Generation (NVIDIA RTX)",
|
||||
"Menos engasgos em combate",
|
||||
"Launcher: primeira abertura guiada e teste do controle"
|
||||
],
|
||||
"disc": "ENTRE NO DISCORD",
|
||||
"disc_sub": "Novidades, ajuda e builds de teste"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"version": "0.3.7",
|
||||
"en": {
|
||||
"sub": "PC PORT / UPDATE 0.3.7",
|
||||
"hero": "THE CITY LIGHTS UP",
|
||||
"hero_sub": "Saves stuck in a pitch-black Seattle now light up by themselves: the port spots the time of day that never loads and swaps in a similar one.",
|
||||
"hero_pts": [
|
||||
"Black city fix: on by itself after 30 seconds",
|
||||
"4K: no more dark picture that comes and goes"
|
||||
],
|
||||
"shot": "BLACK CITY FIXED",
|
||||
"new": "WHAT'S NEW",
|
||||
"cards": [
|
||||
[
|
||||
"gold",
|
||||
"Achievements",
|
||||
"Trophy popups with icons, a page in the launcher."
|
||||
],
|
||||
[
|
||||
"neon",
|
||||
"New cheats",
|
||||
"No damage, infinite missiles, Karma Bomb ready."
|
||||
],
|
||||
[
|
||||
"pink",
|
||||
"Field of view",
|
||||
"Wider view in the menu, the aim follows it."
|
||||
],
|
||||
[
|
||||
"green",
|
||||
"New loading screen",
|
||||
"The shader screen got art, a bar and tips."
|
||||
],
|
||||
[
|
||||
"red",
|
||||
"OptiScaler built in",
|
||||
"Install, remove and set it in the launcher."
|
||||
],
|
||||
[
|
||||
"neon",
|
||||
"Anti-aliasing Off",
|
||||
"No T2x blur and a still camera."
|
||||
]
|
||||
],
|
||||
"also": "ALSO",
|
||||
"also_pts": [
|
||||
"Dash cheats: endless neon run, video flight, long smoke",
|
||||
"Mouse and pad sensitivity; F11 hides the overlay"
|
||||
],
|
||||
"next": "COMING NEXT",
|
||||
"next_tag": "planned for the next updates",
|
||||
"next_pts": [
|
||||
"Fix for the NVIDIA crash in the opening village",
|
||||
"Turn off motion blur and chromatic aberration",
|
||||
"Sharper FSR 4, DLSS and XeSS",
|
||||
"Save now button (Ctrl + Shift + S)"
|
||||
],
|
||||
"disc": "JOIN THE DISCORD",
|
||||
"disc_sub": "News, help and test builds"
|
||||
},
|
||||
"pt": {
|
||||
"sub": "PORT PARA PC / ATUALIZAÇÃO 0.3.7",
|
||||
"hero": "A CIDADE ACENDEU",
|
||||
"hero_sub": "Saves presos numa Seattle toda preta agora acendem sozinhos: o port percebe o horário que nunca carrega e troca por um parecido.",
|
||||
"hero_pts": [
|
||||
"Correção da cidade preta: age sozinha em 30 s",
|
||||
"4K: acabou a tela que escurece e volta"
|
||||
],
|
||||
"shot": "CIDADE PRETA CORRIGIDA",
|
||||
"new": "NOVIDADES",
|
||||
"cards": [
|
||||
[
|
||||
"gold",
|
||||
"Conquistas",
|
||||
"Troféus com ícone na tela e página no launcher."
|
||||
],
|
||||
[
|
||||
"neon",
|
||||
"Cheats novos",
|
||||
"Sem dano, mísseis infinitos, Karma Bomb pronta."
|
||||
],
|
||||
[
|
||||
"pink",
|
||||
"Campo de visão",
|
||||
"Visão mais aberta no menu, a mira acompanha."
|
||||
],
|
||||
[
|
||||
"green",
|
||||
"Tela de carregamento",
|
||||
"A tela dos shaders ganhou arte, barra e dicas."
|
||||
],
|
||||
[
|
||||
"red",
|
||||
"OptiScaler no launcher",
|
||||
"Instale, remova e configure pelo launcher."
|
||||
],
|
||||
[
|
||||
"neon",
|
||||
"Anti-aliasing Off",
|
||||
"Sem o borrão do T2x e câmera parada."
|
||||
]
|
||||
],
|
||||
"also": "E MAIS",
|
||||
"also_pts": [
|
||||
"Cheats de dash: neon sem fim, voo do vídeo, fumaça longa",
|
||||
"Sensibilidade do mouse e controle; F11 esconde o overlay"
|
||||
],
|
||||
"next": "PRÓXIMA ATUALIZAÇÃO",
|
||||
"next_tag": "planejado para as próximas versões",
|
||||
"next_pts": [
|
||||
"Correção do crash da NVIDIA na vila do começo",
|
||||
"Desligar motion blur e aberração cromática",
|
||||
"FSR 4, DLSS e XeSS mais nítidos",
|
||||
"Botão Salvar agora (Ctrl + Shift + S)"
|
||||
],
|
||||
"disc": "ENTRE NO DISCORD",
|
||||
"disc_sub": "Novidades, ajuda e builds de teste"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
inFAMOUS Second Son for PC (issn_pc) - TEST BUILD 0.3.2
|
||||
inFAMOUS Second Son for PC (issn_pc) - TEST BUILD 0.3.7
|
||||
======================================================
|
||||
|
||||
A test build: the game plays from the start, at about 90-105 FPS standing in the open city on a
|
||||
@@ -6,13 +6,207 @@ Ryzen 7 3700X + RX 9060 XT. The first start prepares the shaders once (a few min
|
||||
progress screen); new ones that still appear while playing compile in the background. Please
|
||||
report how far you get and what goes wrong (see Reporting problems).
|
||||
|
||||
DLSS (optional, NVIDIA RTX)
|
||||
NVIDIA's files are not included (licence). Download Streamline SDK 2.14.16 from
|
||||
https://github.com/NVIDIA-RTX/Streamline/releases (exactly that version) and copy these five files
|
||||
from its bin\x64 folder (not development) into bin\dlss next to issn-probe.exe (launcher:
|
||||
Graphics > Open the DLSS folder): sl.interposer.dll, sl.common.dll, sl.dlss.dll, sl.pcl.dll,
|
||||
nvngx_dlss.dll. Then choose DLSS in Graphics > Anti-aliasing. Untested by us (no NVIDIA card):
|
||||
please report how it goes. Without the files the game keeps its own anti-aliasing.
|
||||
Requirements (Windows 10 1903+ / 11 64-bit, a Vulkan 1.3 graphics driver)
|
||||
- Minimum (1080p, about 30-45 FPS): Intel Core 4th gen (i5-4xxx) / AMD Ryzen 1000 / FX-8300,
|
||||
GeForce GTX 1060 6 GB / Radeon RX 580 8 GB, 12 GB RAM.
|
||||
- Recommended (1080p, 60 FPS): Intel Core i5 8th gen / Ryzen 5 1600, GTX 1070 / RTX 2060 /
|
||||
RX 6600, 16 GB RAM, 8 GB video memory.
|
||||
- 1440p: 16 GB RAM; 4K: 32 GB RAM (the game reserves more memory at higher resolutions).
|
||||
- The CPU needs AVX (Intel 2nd gen / AMD FX or newer); the port translates the MOVBE, F16C,
|
||||
BMI1, LZCNT and SSE4A instructions some CPUs lack.
|
||||
Minimum and recommended are the launcher's estimates (Graphics > Your PC); measured: Ryzen 7 3700X
|
||||
+ RX 9060 XT, about 100 FPS in the open city.
|
||||
|
||||
Community: Discord https://discord.gg/y82HQ5uTQg (news, help, test builds).
|
||||
|
||||
XeSS and DLSS (optional)
|
||||
Intel's and NVIDIA's files are not included. When you choose XeSS or DLSS in Graphics >
|
||||
Anti-aliasing, the launcher downloads them from the vendors' own GitHub releases (XeSS SDK 3.0.2,
|
||||
about 60 MB; Streamline SDK 2.14.16, about 35 MB), checks each file and puts them in bin\xess and
|
||||
bin\dlss next to issn-probe.exe. Graphics > Download XeSS now / Download DLSS now does the same.
|
||||
XeSS runs on Intel Arc, Radeon RX 5000 and later and GeForce GTX 10 and later; DLSS needs an
|
||||
NVIDIA RTX card. XeSS needs the Visual C++ 2015-2022 runtime (vc_redist.x64). Without the files
|
||||
the game keeps its own anti-aliasing.
|
||||
|
||||
OptiScaler (optional, third-party)
|
||||
OptiScaler (https://github.com/optiscaler/OptiScaler, GPL-3.0) is a separate program by its own
|
||||
team, not part of the port and never in this zip. It takes the picture, depth and motion the port
|
||||
gives XeSS and runs another upscaler on them: FSR 4 (Radeon RX 7000 / 9000, through DirectX 12),
|
||||
FSR 3.1, XeSS, DLSS (NVIDIA RTX) or FSR 2.2 (the one tested with the port).
|
||||
- Install: Graphics > OptiScaler (optional) > Download and install. The launcher downloads the
|
||||
newest stable release from the project's GitHub (about 55 MB), checks it and puts it in bin\:
|
||||
OptiScaler.dll as winmm.dll, its DLLs beside it, OptiScaler.ini set for the port.
|
||||
- The port's anti-aliasing must be XeSS (Graphics > Anti-aliasing; the install sets it): with
|
||||
anything else OptiScaler gets nothing. The size the game draws at is still the port's
|
||||
FSR 4 / DLSS quality.
|
||||
- Output: Graphics > OptiScaler output, written to bin\OptiScaler.ini (VulkanUpscaler); it applies
|
||||
when the game starts.
|
||||
- In the game, F8 opens OptiScaler's menu. Insert stays the port's menu and Home its anti-aliasing
|
||||
switch (a copy unpacked by hand uses Insert: install it from the launcher to move it to F8).
|
||||
Page Up, Page Down and End are OptiScaler's own keys (its FPS counter and frame generation).
|
||||
- Remove: Graphics > OptiScaler (optional) > Remove takes out every file it put (listed in
|
||||
bin\optiscaler.json) and the port runs as before. Close the game first.
|
||||
- last_run.log starts with a line when OptiScaler is installed. Problems with OptiScaler itself
|
||||
go to its project.
|
||||
|
||||
What's new in 0.3.7
|
||||
- Black city fix: in some saves the game asks for a time of day (the grey "dismal" sky) whose part
|
||||
of the map never finishes loading, so no sky or ambient light is in use and the city stays
|
||||
black. After 30 seconds the port asks the game for a similar one (rainy "blustery") and the city
|
||||
lights up by itself. Insert > Image > Exposure shows what it did (ISSN_TOD_RESCUE=0 turns it off).
|
||||
- 4K: the game's exposure meter gets room for 4K, so the picture no longer goes dark and back.
|
||||
- Achievements: the game's trophies pop up with their icon, are kept on this PC
|
||||
(user\trophies.txt) and the launcher has an Achievements page with the percentage.
|
||||
- Field of view in the menu, with the aim following it (on by default).
|
||||
- The port's own shader loading screen (art, progress bar, tips).
|
||||
- Cheats (Game tab): infinite health now also stops the hits themselves (no gray screen), a new
|
||||
"Infinite missiles" switch (God Mode turns it on too), the Karma Bomb stays ready; endless neon
|
||||
run and video flight while the dash button is held, long smoke dash.
|
||||
- Anti-aliasing "Off" (launcher and menu): no T2x and a still camera.
|
||||
- OptiScaler (optional): install, remove and choose its output from the launcher (see above).
|
||||
- In-game menu: VSync and the frame rate limit change at once; Apply and restart starts the
|
||||
anti-aliasing, quality and frame generation chosen there; a note says when the launcher's frame
|
||||
pacing caps the game.
|
||||
- Controls: mouse and controller sensitivity (menu and launcher); F11 hides the performance
|
||||
overlay; L3 + R3 always reach the game, the spray can pose of pads without a motion sensor is a
|
||||
menu switch (and the H key).
|
||||
- Video memory limit in the launcher (Performance), for cards of 6 GB or less.
|
||||
- FSR 4 only turns on with AMD Radeon cards.
|
||||
- Save backup: a switch in the launcher, and none when the savedata folder is far bigger than the
|
||||
game's saves.
|
||||
- A crash right after the start with a game folder whose all_shaders.xpps does not match the
|
||||
01.07 eboot.bin now says so in the log.
|
||||
|
||||
What's new in 0.3.6.3
|
||||
- In-game menu (Insert, or R3 + L2 on a controller), Image tab: draw distance, texture filtering
|
||||
and frame generation change at once; anti-aliasing and upscaler quality are chosen there for the
|
||||
next start (the launcher picks them up).
|
||||
- In-game menu, Game tab: cheats for Delsin - God Mode, infinite health, infinite powers and the Karma
|
||||
Bomb always ready.
|
||||
- Frame generation (test): the generated frame is made from the scene alone and this frame's HUD
|
||||
is laid over it, so the HUD stays sharp; Delsin no longer smears or vanishes in motion.
|
||||
- A "Game saved" note at the top of the screen when the game saves (Overlay tab: on/off).
|
||||
- Import saves: saves of another region (CUSA00004 and others) go into this game's own folder.
|
||||
- Save backups: besides the last 10, one a day for 14 days and the copies made before an import
|
||||
or a restore are kept; a start with unchanged saves makes no new backup.
|
||||
- CPUs without F16C (Intel 2nd gen, Sandy Bridge): its 13 half-float conversions are translated
|
||||
instead of stopping the game.
|
||||
- Video memory: textures are cleaned up by the card's live memory budget (fewer missing textures
|
||||
on 8 GB cards); the log shows the usage once a minute.
|
||||
- HDR: the upscaler's HUD layer stays off while the game outputs HDR (the picture was too dark).
|
||||
|
||||
What's new in 0.3.6.2
|
||||
- Game > Saves > Import from another port: brings your saves from HayatoG's iss_pc (or an older
|
||||
folder of this port). Both ports keep saves the same way (savedata\1\CUSA00223), so they are
|
||||
copied as they are. The other launcher's data folder is found by itself; otherwise pick its
|
||||
folder (the port's folder, its user or savedata folder, or CUSA00223). The current saves are
|
||||
backed up first and show up in Save backups.
|
||||
- Frame generation: no extra wait between the two frames when V-Sync already spaces them, so the
|
||||
game's own frame rate no longer drops with it on (100 Hz screen: about 100 FPS, game 50 + FG 50).
|
||||
- The log has the raw controller stick values for the first two minutes (to find stick drift).
|
||||
|
||||
What's new in 0.3.6.1
|
||||
- Fixed: after updating to 0.3.6 from the launcher, the game could start without its shader
|
||||
seed (only a user\cache\issn-seed-0.3.6.part folder was left) and compiled every shader while
|
||||
playing: heavy stutter. The seed is now placed even when Windows holds the new files for a
|
||||
moment; updating to 0.3.6.1 brings it.
|
||||
- The log names the shader cache folder in use and why ("Shader cache: ...").
|
||||
- The FPS counter shows the frames on the screen, as MSI Afterburner and similar tools count them,
|
||||
with the game's own frame rate next to it when they differ (frame pacing, frame generation):
|
||||
"100 FPS (game 50)"; with frame generation working, "100 FPS (game 50 + FG 50)".
|
||||
- Performance > Frame generation (FSR 3.1, test): a frame between every two of the game; needs
|
||||
FSR 4, XeSS or DLSS as anti-aliasing. A first test version: the HUD can blur in motion.
|
||||
|
||||
What's new in 0.3.6
|
||||
- XeSS (Intel) as anti-aliasing / upscaler: Graphics > Anti-aliasing > XeSS. It runs on Intel Arc,
|
||||
Radeon RX 5000 and later and GeForce GTX 10 and later, so cards without FSR 4 or DLSS get a
|
||||
modern anti-aliasing too (about 1 ms per frame on a Radeon RX 9060 XT).
|
||||
- XeSS and DLSS files download by themselves: pick one and the launcher fetches Intel's and
|
||||
NVIDIA's files from their GitHub releases and checks them. No more copying DLLs by hand.
|
||||
- Graphics > Your PC: the launcher finds your graphics card, processor and memory, says what to
|
||||
expect in the city and sets the graphics for a goal. Recommended is the testers' pick for most
|
||||
PCs: Balanced performance mode, original detail, shaders in the background and, on a 100 Hz or
|
||||
faster screen, even frame pacing at half the refresh rate.
|
||||
- Performance > Frame pacing: half or a third of the screen's refresh rate with every frame shown
|
||||
for the same time, so motion is smooth without G-Sync / FreeSync. The FPS counter now shows the
|
||||
game's real frame rate in this mode (it showed the screen's refresh rate).
|
||||
- Faster Windows build (link-time and profile-guided optimisation from a training run): about
|
||||
100 FPS in the city on a Ryzen 7 3700X in our tests.
|
||||
- Fewer hitches while the city streams in: file reads into memory the graphics card uses no
|
||||
longer fault page by page.
|
||||
- In-game menu (Insert / R3+L2) > Controls: change any key by pressing the new one, live.
|
||||
- Keyboard: Options (Start) is Escape for everyone; R held with the mouse moved is a finger on the
|
||||
touchpad (hand scanner, engine pull) - G is still the touchpad's left side.
|
||||
- Game > Skip the intro logos.
|
||||
- Launcher: a new look (side bar with the pages, rounded buttons and switches), a What's new page
|
||||
with the release notes, a Help page that checks the installation and the last game log and
|
||||
says how to fix what it finds, Resolution on the Home page, and the Discord link. Updating from
|
||||
the launcher keeps the shader seed of the new release.
|
||||
|
||||
What's new in 0.3.5.1
|
||||
- Fixed: the picture staying dark at night (only lights and glow visible) on some PCs. The
|
||||
launcher's Graphics > Effects at Light or Off kept the game from reading its light metering back
|
||||
from the graphics card; that choice is gone and a saved Light / Off now plays as Full.
|
||||
- Fixed: on some NVIDIA PCs with DLSS the port's code fixes did not apply (the 0x1cfaaf crash
|
||||
guard, draw distance, 1:1 mouse aim); the log now shows them all as ready.
|
||||
- The shader seed is back (0.3.5 shipped without one, so every shader compiled while playing),
|
||||
and it now also reaches a user folder in Documents. On NVIDIA and older AMD cards the effect
|
||||
shaders come ready; the rest still compile in the background while playing.
|
||||
- Game > Controls > Stick dead zone (5% to 20%): raise it if Delsin walks or the camera turns on
|
||||
its own (a worn stick drifting).
|
||||
- Black screen that never recovered on some PCs (the window reported as "suboptimal"): those
|
||||
frames are now shown; with HDR on and no frame shown for two seconds, HDR is turned off.
|
||||
- Graphics > Texture filtering: anisotropic filtering 4x / 8x / 16x over the game's own.
|
||||
- Resolution: 5K and 8K (experimental). DLSS waits for its inputs at render sizes other than
|
||||
1080p instead of stepping aside at once (untested: no NVIDIA card here).
|
||||
|
||||
What's new in 0.3.5
|
||||
- Native resolutions (launcher: Graphics > Resolution, from 800x600-class 1064x600 up to 4K): the
|
||||
game draws at the chosen size itself, with the memory it needs above 1080p (no more black screen
|
||||
at 1440p / 4K). FSR 4 / DLSS quality is now its own setting next to it.
|
||||
- Button icons for the keyboard and mouse or an Xbox controller (launcher: Game > Button
|
||||
icons; idea of u/humonculus87 on Reddit): the game's hints show the keys and mouse buttons (they
|
||||
follow your key changes) or the Xbox letters, and so do the prompts in the world: subdue and
|
||||
execute (F / C, or Y / X in the karma colours), the d-pad of missions and karma choices (the
|
||||
arrow keys, the lit one in its colour), the spray can's painted R2 (the left mouse button, or
|
||||
RT). The first start with them makes copies of four game packages (~470 MB) in the port's out
|
||||
folder; the game files are not changed.
|
||||
- Keyboard: R is the touchpad (it "recharges" the powers), and holding it is both thumbs (the
|
||||
drains, the car on Reggie); R with W/A/S/D (or an arrow) is a swipe (doors, wreckage, the
|
||||
Mobile Command Center's core: hold R + W and click to hit it); G is now the touchpad's left side
|
||||
alone (photo mode). The touchpad prompts in the world show the R key (the View button with an
|
||||
Xbox pad), and the game's touchpad hints name the keys in English and Portuguese.
|
||||
- Fixed: the game closing after the intro on some Intel + NVIDIA PCs (crash at 0x1cfaaf /
|
||||
0x1cf8f9); freezes when absorbing a power and on rooftops (1.07); Alt+Tab in fullscreen closing
|
||||
the game; FSR 4 Quality crashing at start; fixes from HayatoG's iss_pc (freezes, a memory error
|
||||
on start, error 1450).
|
||||
- A game version other than 01.07 now stops at start with what to do, instead of a crash later
|
||||
(a 01.07 eboot with an older param.sfo is accepted).
|
||||
- Spray can on the right stick: calmer (30 degrees a second at full push, gentle near the
|
||||
centre); Game > Spray can stick speed.
|
||||
- Launcher: "Made by Liondarkz" and Advanced > About with the credits; Advanced > Support the
|
||||
project (Pix, Ko-fi) for whoever wants to help: the port stays free.
|
||||
|
||||
What's new in 0.3.4
|
||||
- Intel processors: the game no longer closes right after the Sucker Punch logo. It uses AMD's
|
||||
SSE4A instruction EXTRQ (96 places), which no Intel CPU has; the port now replaces it with an
|
||||
equivalent on Intel (Windows and Linux).
|
||||
- Ultrawide and other screen shapes (launcher: Graphics > Screen shape, from axylpup's 1.1): the
|
||||
game fills 21:9, 32:9, 16:10 ... and sees more at the sides (or above and below on 16:10); the
|
||||
HUD moves to the edges and movies keep their shape with black bars. Automatic follows the
|
||||
window: play in fullscreen on an ultrawide monitor.
|
||||
- Spray can with the mouse: about 9x calmer (2.5 degrees per 100 px, as axylpup's build); - / =
|
||||
change its speed while the pad is tilted (ISSN_SPRAY_MOUSE sets it).
|
||||
|
||||
What's new in 0.3.3
|
||||
- Fixed: after playing a while with 0.3.2 the game could stop opening (the shader cache paired
|
||||
shaders wrongly: a new shader took the slot of one the seed had left out). New shaders now
|
||||
never reuse a slot the cache still names, and the cache starts in a new folder
|
||||
(user\cache\issn-seed-0.3.3); user\cache\issn-seed-0.3.2 can be deleted.
|
||||
- axylpup's 1.1.1 fixes in the graphics core: AMD cards whose driver refused a fast-path shader
|
||||
variant (RX 6800 XT, RX 570) no longer close; 6-12 GB cards keep the big mesh buffer in video
|
||||
memory and try smaller sizes before running out; large uploads go through in pieces when the
|
||||
driver refuses one big buffer; out of memory shows a message instead of closing silently.
|
||||
|
||||
What's new in 0.3.2
|
||||
- Shaders prepared on the first start: the package carries a shader seed (the list of the game's
|
||||
@@ -30,7 +224,7 @@ What's new in 0.3.2
|
||||
scaled up. Helps when the graphics card is the limit, not the processor.
|
||||
- Every 10 s while new shaders appear the log says how many and how long anything waited.
|
||||
- The game writes user\last_run.log itself also with "Close the launcher on play".
|
||||
- The shader cache moves to a new folder (user\cache\issn-seed-0.3.2); user\cache\CUSA00223 or
|
||||
- The shader cache moves to a new folder (user\cache\issn-seed-<version>); user\cache\CUSA00223 or
|
||||
CUSA00004 from older versions can be deleted.
|
||||
|
||||
What's new in 0.3.1
|
||||
@@ -69,7 +263,7 @@ a folder that contains eboot.bin, sce_module and sce_sys. How to dump it: docs/i
|
||||
Built on the God of War III Remastered port (gow3_pc, by Felipe Prado), which comes from
|
||||
bloodborne_pc (deadinside28, Supermedo). Graphics: axylpup's shadPS4 build for inFAMOUS Second Son
|
||||
(github.com/axylpup/shadPS4), from the shadPS4 video core, with earlier work by Ayoub
|
||||
(shadPS4-AYOUB1080p) and HayatoG (iss_pc). FSR 4: AMD FidelityFX (MIT). Launcher title face: Bebas
|
||||
(shadPS4-AYOUB1080p) and HayatoG (iss_pc, github.com/HayatoG/iss_pc: video core, draw distance, sensor mode). FSR 4: AMD FidelityFX (MIT). Launcher title face: Bebas
|
||||
Neue (SIL Open Font License).
|
||||
|
||||
Requirements
|
||||
@@ -88,8 +282,8 @@ Controller
|
||||
light bar work as on the PS4.
|
||||
- Pads without a touchpad or motion sensors (Xbox, 8BitDo, ...), on by itself for them (launcher:
|
||||
Game > Controls > Controller without touchpad):
|
||||
- Spray can: L3 + R3 together tilts the pad like the spray can; L1 rattles it, the right
|
||||
stick aims. L3 + R3 again levels it.
|
||||
- Spray can: Insert > Controls > "Spray can: tilt the pad" tilts it like the spray can; L1 rattles it, the right
|
||||
stick aims (launcher: Spray can stick speed; - / = in the game). Turn the switch off to level it (H on the keyboard does both).
|
||||
- Touchpad swipe (lift, push): hold Select/Back and push the right stick that way.
|
||||
- Two fingers on the touchpad (lifting the tree): hold Select/Back + RB (R1).
|
||||
- Touchpad click: Select/Back.
|
||||
@@ -97,10 +291,13 @@ Controller
|
||||
- WASD moves, IJKL turns the camera. Click in the game window and the mouse aims 1:1 (axylpup's
|
||||
camera hook); Y or F7 lets the mouse go and takes it back; - / = change the mouse speed.
|
||||
- Space Cross, Left Shift Circle, C Square, F Triangle (numpad 2/6/4/8 too), Q L1, E R1,
|
||||
U L3, O R3, Enter Options, arrows d-pad.
|
||||
U L3, O R3, Esc Options, arrows d-pad.
|
||||
- Left click R2 (shoot), right click L2 (aim) while the mouse aims; 1 / 3 also work.
|
||||
- R / G: left / right side of the touchpad click (G + IJKL: swipe).
|
||||
- Spray can: hold Z to tilt the pad, tap Z or hold X to rattle it, move the mouse to aim.
|
||||
- R: the touchpad, held with both thumbs (drains, the car on Reggie); G: its left side alone
|
||||
(photo mode). R + W/A/S/D (or an arrow): a swipe that way (doors, wreckage, the Mobile Command
|
||||
Center core); keep the key down to hold the finger there and click to hit.
|
||||
- Spray can: hold Z to tilt the pad, tap Z or hold X to rattle it, move the mouse to aim
|
||||
(- / = change its speed while the pad is tilted).
|
||||
- F12 screenshot, Insert the port's menu.
|
||||
- Started from Steam: the port ignores Steam Input's device filter so the controller is seen
|
||||
(ISSN_KEEP_STEAM_PAD_FILTER=1 keeps it). If a pad still is not found, the log lists what SDL
|
||||
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# A release package with a shader seed, in one command (MSYS2 CLANG64, after `bash build.sh`):
|
||||
# bash packaging/windows/make_seed.sh [cache folder]
|
||||
# 1. packages dist/issn-windows (package.sh) with the executable just built;
|
||||
# 2. runs a VERIFY boot of that executable on the cache folder (default: the one the dist copy
|
||||
# plays with, else user/cache/CUSA00223): every cached stage is made again from the game's own
|
||||
# files and compared word for word, the stamp written, and the game closes by itself before it
|
||||
# starts (SHADPS4_EXIT_AFTER_WARMUP); the launcher's saved game folder is used (--play);
|
||||
# 3. packages again with the seed (tools/make_shader_seed.py: keys and metas, no shader code).
|
||||
# The cache folder must sit in dist/issn-windows/user/cache: package.sh checks that the verify ran
|
||||
# with the packaged issn-probe.exe. Nothing is built here, so the executable cannot change between
|
||||
# the verify and the package.
|
||||
set -euo pipefail
|
||||
cd -- "$(dirname -- "$0")/../.."
|
||||
cache_root=dist/issn-windows/user/cache
|
||||
if [[ $# -ge 1 ]]; then
|
||||
source=${1%/}
|
||||
elif [[ -f dist/issn-windows/shader-seed.txt ]] &&
|
||||
[[ -d $cache_root/$(tr -d '\r\n' < dist/issn-windows/shader-seed.txt) ]]; then
|
||||
source=$cache_root/$(tr -d '\r\n' < dist/issn-windows/shader-seed.txt)
|
||||
else
|
||||
source=$cache_root/CUSA00223
|
||||
fi
|
||||
[[ -d $source ]] || { echo "No cache folder $source" >&2; exit 1; }
|
||||
[[ $(cd -- "$(dirname -- "$source")" && pwd) == $(cd -- "$cache_root" && pwd) ]] ||
|
||||
{ echo "The cache folder must be in $cache_root" >&2; exit 1; }
|
||||
name=$(basename -- "$source")
|
||||
echo "Shader seed from $source"
|
||||
|
||||
bash packaging/windows/package.sh
|
||||
|
||||
rm -f -- "$source/seed-stamp.bin"
|
||||
echo 'Verifying the cache with the packaged executable (the game window opens and closes by itself)...'
|
||||
SHADPS4_CACHE_NAME=$name SHADPS4_CACHE_REBUILD=10 SHADPS4_EXIT_AFTER_WARMUP=1 \
|
||||
"dist/issn-windows/inFAMOUS Second Son.exe" --play | grep -E 'CacheRebuild: stage|boot preload done|STOP' || true
|
||||
[[ -f $source/seed-stamp.bin ]] || {
|
||||
echo "The verify wrote no seed-stamp.bin (see dist/issn-windows/user/last_run.log)" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
ISSN_SHADER_SEED_FROM=$source bash packaging/windows/package.sh
|
||||
echo 'Done: dist/issn-windows.zip carries the shader seed (the "Shader seed:" line above has the count).'
|
||||
@@ -27,8 +27,8 @@ fi
|
||||
out/pyenv/Scripts/python.exe -m pip install -q --disable-pip-version-check pyinstaller pillow iced-x86==1.21.0
|
||||
# The scripts run inside inFAMOUS Second Son.exe (--script): the standard modules they import come along.
|
||||
hidden=()
|
||||
for module in argparse base64 collections hashlib json re shutil struct tempfile xml.etree.ElementTree \
|
||||
urllib.request ctypes.wintypes iced_x86; do
|
||||
for module in argparse base64 collections hashlib json mmap re shutil struct tempfile xml.etree.ElementTree \
|
||||
urllib.request ctypes.wintypes iced_x86 PIL.Image PIL.ImageDraw PIL.ImageFilter PIL.ImageFont; do
|
||||
hidden+=(--hidden-import "$module")
|
||||
done
|
||||
out/pyenv/Scripts/python.exe -m PyInstaller --noconfirm --clean --log-level WARN --windowed --copy-metadata pillow \
|
||||
@@ -43,7 +43,7 @@ rm -rf -- out/stage
|
||||
mkdir -p "$dest/bin" "$dest/launcher"
|
||||
cp -r "out/pyi-dist/inFAMOUS Second Son/." "$dest/"
|
||||
llvm-strip -o "$dest/Play inFAMOUS Second Son.exe" out/issn-play.exe
|
||||
cp launcher/issn.ico launcher/issn.png "$dest/launcher/"
|
||||
cp launcher/issn.ico launcher/issn.png launcher/pix_qr.png "$dest/launcher/"
|
||||
# issn: the title face ships only when there is one (launcher/issn_font.ttf; none yet).
|
||||
[[ -f launcher/issn_font.ttf ]] && cp launcher/issn_font.ttf "$dest/launcher/"
|
||||
# The executables without debug information (out/ keeps the symbols for crash reports).
|
||||
@@ -113,6 +113,14 @@ else
|
||||
mkdir -p "$play"
|
||||
find "$play" -mindepth 1 -maxdepth 1 ! -name user ! -name mods ! -name issn.ini \
|
||||
! -name mods.json ! -name patches.json -exec rm -rf {} +
|
||||
cp -r "$dest/." "$play/"
|
||||
# Everything but user/: the shader seed goes in only where that cache folder does not exist
|
||||
# yet. Copied over a folder that was played with, the seed's metas would replace the folder's
|
||||
# own under the same names while its SPIR-V stays: mismatched shaders (2026-10-24, a seed made
|
||||
# from such a folder crashed the game).
|
||||
find "$dest" -mindepth 1 -maxdepth 1 ! -name user -exec cp -r {} "$play/" \;
|
||||
if [[ -n ${seed_name:-} && ! -e $play/user/cache/$seed_name ]]; then
|
||||
mkdir -p "$play/user/cache"
|
||||
cp -r "$dest/user/cache/$seed_name" "$play/user/cache/"
|
||||
fi
|
||||
fi
|
||||
du -sh "$dest" dist/issn-windows.zip
|
||||
@@ -29,10 +29,21 @@
|
||||
<Line Type="bytes" Address="0x0072fa15" Value="E9A65C99006690" OriginalHash="3ee41d36233286e4"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
<Metadata Title="inFAMOUS Second Son" Name="Bigger light list (needed for native 4K)" Note="The game sizes its light lists for 25 600 tiles of 16 pixels (enough up to 1800p); 4K needs 32 400. FR : liste des lumieres agrandie, necessaire pour la 4K native." Author="AYOUB1080p" PatchVer="1.0" AppVer="01.00" AppElf="eboot.bin" isEnabled="true">
|
||||
<Metadata Title="inFAMOUS Second Son" Name="Bigger light list (needed for native 4K)" Note="The game sizes its light lists for 25 600 tiles of 16 pixels (enough up to 1800p); 4K needs 32 400. Also the exposure meter (625 tiles of 128x72 px; 4K needs 900, else the picture goes dark). FR : liste des lumieres agrandie, necessaire pour la 4K native." Author="AYOUB1080p" PatchVer="1.0" AppVer="01.00" AppElf="eboot.bin" isEnabled="true">
|
||||
<PatchList>
|
||||
<Line Type="bytes" Address="0x0056be07" Value="00004800" OriginalHash="4cf5e27f9da5a7b7"/>
|
||||
<Line Type="bytes" Address="0x0056be1f" Value="00004800" OriginalHash="4cf5e27f9da5a7b7"/>
|
||||
<!-- issn 11-04: the exposure meter's tile buffers (four, 2500 bytes / 625 tiles of 128x72 px:
|
||||
enough up to 1800p; 4K needs 900, so the camera's exposure read garbage past them and
|
||||
the picture went dark and back). 8192 bytes / 2048 tiles each. -->
|
||||
<Line Type="bytes" Address="0x0057132b" Value="00200000" OriginalHash="05de299f0f9c851e"/>
|
||||
<Line Type="bytes" Address="0x00571375" Value="00200000" OriginalHash="05de299f0f9c851e"/>
|
||||
<Line Type="bytes" Address="0x00572341" Value="00200000" OriginalHash="05de299f0f9c851e"/>
|
||||
<Line Type="bytes" Address="0x00572388" Value="00200000" OriginalHash="05de299f0f9c851e"/>
|
||||
<Line Type="bytes" Address="0x0057134f" Value="00080000" OriginalHash="bd7e02077b7bafea"/>
|
||||
<Line Type="bytes" Address="0x00571399" Value="00080000" OriginalHash="bd7e02077b7bafea"/>
|
||||
<Line Type="bytes" Address="0x00572364" Value="00080000" OriginalHash="bd7e02077b7bafea"/>
|
||||
<Line Type="bytes" Address="0x005723ab" Value="00080000" OriginalHash="bd7e02077b7bafea"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
<Metadata Title="inFAMOUS Second Son" Name="Streaming: closest city blocks first" Note="Lower priority penalties of the world block streamer (3000/2000/2000 -> 1000/700/700): the blocks nearest to the player are loaded and kept in high detail sooner. FR : streaming, les blocs de la ville les plus proches passent en priorite." Author="AYOUB1080p" PatchVer="1.0" AppVer="01.00" AppElf="eboot.bin" isEnabled="false">
|
||||
|
||||
Binary file not shown.
@@ -8,6 +8,7 @@ The in-game "Apply and restart" runs this script again through ISSN_RESTART_COMM
|
||||
`--after PID` waits for the previous game process to end first (its GPU device and memory).
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import shutil
|
||||
@@ -32,7 +33,33 @@ def shader_seed_name(port, user_dir):
|
||||
return None
|
||||
if not name or not all(c.isalnum() or c in '-_.' for c in name) or name.startswith('.'):
|
||||
return None
|
||||
return name if (Path(user_dir) / 'cache' / name).is_dir() else None
|
||||
target = Path(user_dir) / 'cache' / name
|
||||
shipped = Path(port) / 'user' / 'cache' / name
|
||||
# A user folder elsewhere (Documents\inFAMOUS-PC, the launcher's choice) gets a copy of the
|
||||
# seed the package ships: it is a fresh folder named after the release, never merged into a
|
||||
# cache that was played with (package.sh). Without it those players compiled every shader
|
||||
# while playing (tester logs, 10-29: "no valid seed-stamp.bin in the cache folder").
|
||||
if not target.is_dir() and (shipped / 'seed-stamp.bin').is_file():
|
||||
part = target.with_name(name + '.part')
|
||||
try:
|
||||
shutil.rmtree(part, ignore_errors=True)
|
||||
shutil.copytree(shipped, part, dirs_exist_ok=True)
|
||||
# 10-30: the rename can fail right after the copy (an antivirus holds the new files):
|
||||
# tried again for a while, then the seed is copied under its own name.
|
||||
for attempt in range(20):
|
||||
try:
|
||||
part.rename(target)
|
||||
break
|
||||
except OSError:
|
||||
if attempt == 19:
|
||||
shutil.copytree(shipped, target, dirs_exist_ok=True)
|
||||
shutil.rmtree(part, ignore_errors=True)
|
||||
else:
|
||||
time.sleep(0.5)
|
||||
print(f'Shader seed {name} copied to {target.parent}', flush=True)
|
||||
except OSError as error:
|
||||
print(f'Shader seed: could not copy it to {target.parent} ({error})', flush=True)
|
||||
return name if target.is_dir() else None
|
||||
|
||||
def fail(message):
|
||||
print(message, file=sys.stderr)
|
||||
@@ -75,10 +102,11 @@ def parse_cpu_features(output):
|
||||
|
||||
|
||||
def isa_rewrite_wanted(missing, env):
|
||||
"""ISSN_ISA_REWRITE=1 forces the ISA rewrite table, 0 disables it; otherwise the CPU decides."""
|
||||
"""ISSN_ISA_REWRITE=1 (or sse4a: the EXTRQ sites only) forces the ISA rewrite table, 0 disables
|
||||
it; otherwise the CPU decides."""
|
||||
policy = env.get('ISSN_ISA_REWRITE')
|
||||
if policy in ('0', '1'):
|
||||
return policy == '1'
|
||||
if policy in ('0', '1', 'sse4a'):
|
||||
return policy != '0'
|
||||
return bool(missing)
|
||||
|
||||
|
||||
@@ -131,6 +159,10 @@ def glyph_mounts(env, probe, game, out):
|
||||
auto, xbox or nintendo nothing is asked, drawn or mounted, and old copies are removed. The
|
||||
game folder is never written."""
|
||||
choice = env.get('ISSN_GLYPH_STYLE', 'off')
|
||||
import game_profiles
|
||||
import patches as game_patches
|
||||
if game_patches.game_title_id(game) in game_profiles.INFAMOUS_SECOND_SON:
|
||||
return infamous_glyph_mounts(env, probe, game, out, choice)
|
||||
if choice not in ('auto', 'xbox', 'nintendo'):
|
||||
shutil.rmtree(Path(out) / 'glyphs', ignore_errors=True)
|
||||
return [], None
|
||||
@@ -154,6 +186,45 @@ def glyph_mounts(env, probe, game, out):
|
||||
return [part for guest, path in mounts.items() for part in ('--mount', f'{guest}={path}')], style
|
||||
|
||||
|
||||
def infamous_glyph_mounts(env, probe, game, out, choice):
|
||||
"""inFAMOUS Second Son's button icons (scripts/infamous_glyphs.py): keyboard and mouse keys or
|
||||
Xbox letters drawn into copies of the language packages' icon atlas. auto: the connected pad's
|
||||
style (a PlayStation pad keeps the game's own), the keyboard without one. The tester idea of
|
||||
u/humonculus87 (Reddit)."""
|
||||
import infamous_glyphs
|
||||
style = choice if choice in ('keyboard', 'xbox') else None
|
||||
name = ''
|
||||
if choice == 'auto':
|
||||
try:
|
||||
import glyphs
|
||||
pad = subprocess.run([str(probe), '--gamepad'], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||
text=True, timeout=15, creationflags=no_console()).stdout
|
||||
found = glyphs.parse_gamepad(pad)
|
||||
except Exception: # noqa: BLE001 (icons only)
|
||||
found = None
|
||||
style = 'keyboard' if not found else None if found[0] == 'playstation' else 'xbox'
|
||||
name = found[2] if found else ''
|
||||
if not style:
|
||||
infamous_glyphs.build(game, out, 'off')
|
||||
if choice == 'auto':
|
||||
print(f'Button icons: the game\'s own (PlayStation controller{f" {name}" if name else ""})', flush=True)
|
||||
return [], None
|
||||
try:
|
||||
mounts = infamous_glyphs.build(game, out, style, env.get('ISSN_KEY_MAP', ''))
|
||||
except Exception as error: # noqa: BLE001 (icons only: whatever fails, the game still starts)
|
||||
print(f'Button icons: {error}; the game keeps its own', flush=True)
|
||||
return [], None
|
||||
if not mounts:
|
||||
print('Button icons: no language package with the icon font in this game folder', flush=True)
|
||||
return [], None
|
||||
label = {'keyboard': 'keyboard and mouse', 'xbox': 'Xbox'}[style]
|
||||
languages = sum(1 for guest in mounts if '/lang_' in guest)
|
||||
prompts = ', in-world prompts' if len(mounts) > languages else ''
|
||||
print(f'Button icons: {label}{f" ({name})" if name else ""}, {languages} language packages{prompts} redrawn '
|
||||
f'in {Path(out) / "glyphs"}; game files untouched', flush=True)
|
||||
return [part for guest, path in mounts.items() for part in ('--mount', f'{guest}={path}')], style
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
if len(args) >= 2 and args[0] == '--after':
|
||||
@@ -186,6 +257,16 @@ def main():
|
||||
if key not in env:
|
||||
env[key] = value
|
||||
derived.append(key)
|
||||
# issn-axy: a restart from the in-game menu starts its anti-aliasing / quality / frame gen.
|
||||
ini = graphics_options.read_ini(env['ISSN_CONFIG'])
|
||||
if ini.get('restart_unconfirmed') == '1' and not reverted:
|
||||
menu_env, menu_off = graphics_options.menu_environment(ini, WINDOWS)
|
||||
for key in menu_off:
|
||||
env.pop(key, None)
|
||||
env.update(menu_env)
|
||||
if menu_env or menu_off:
|
||||
print('Graphics: the in-game menu\'s anti-aliasing for this restart: '
|
||||
+ (', '.join(f'{k}={v}' for k, v in menu_env.items()) or "the game's own"), flush=True)
|
||||
if not env.get('ISSN_FSR411_DIR') and not (PORT / 'fsr4_411').is_dir() and (data / 'fsr4_411').is_dir():
|
||||
env['ISSN_FSR411_DIR'] = str(data / 'fsr4_411')
|
||||
|
||||
@@ -219,6 +300,28 @@ def main():
|
||||
game = update_view
|
||||
print(f'Game and update combined from {env["ISSN_PATCH_DIR"]}', flush=True)
|
||||
original_game = game.resolve()
|
||||
# issn 11-05 (#33): the shader package the game reads first; its size and the folder it really
|
||||
# comes from (the update or the base game) tell a mismatched dump at a glance.
|
||||
shaders = game / 'art' / 'cache' / 'all_shaders.xpps'
|
||||
try:
|
||||
print(f'Game data: all_shaders.xpps {shaders.stat().st_size:,} bytes from {shaders.resolve().parent.parent.parent}',
|
||||
flush=True)
|
||||
except OSError as error:
|
||||
print(f'Game data: art/cache/all_shaders.xpps missing or unreadable ({error})', flush=True)
|
||||
# issn_pc: the port is made for the 01.07 update (code patches, mouse hook, seed). A base 01.00
|
||||
# dump ends in an unsupported system call after the intro (sceKernelLoadStartModule, tester log
|
||||
# 10-24) - say so here instead. ISSN_ALLOW_OLD_VERSION=1 tries anyway.
|
||||
import game_profiles
|
||||
import patches as game_patches
|
||||
# Decided by the eboot.bin: some dumps carry the 01.07 eboot.bin under the base game's
|
||||
# param.sfo (APP_VER 01.00; tester log 10-25 played that way).
|
||||
title, version = game_patches.game_title_id(game), game_patches.game_app_version(game)
|
||||
if (title in game_profiles.INFAMOUS_SECOND_SON and version and version != '01.07'
|
||||
and env.get('ISSN_ALLOW_OLD_VERSION') != '1'
|
||||
and game_patches.eboot_sha256(game) not in game_profiles.INFAMOUS_107_EBOOTS):
|
||||
fail(f'{title} version {version}: this port needs the game\'s 01.07 update. In the launcher, '
|
||||
'set "Update folder" to the 01.07 update (the folder with its eboot.bin and sce_sys), or '
|
||||
'lay the update over the game folder, then press PLAY again.')
|
||||
game = Path(run_script('mods.py', game, '--out', out,
|
||||
'--mods-dir', env.get('ISSN_MODS_DIR', data / 'mods'),
|
||||
'--config', env.get('ISSN_MODS_CONFIG', data / 'mods.json'),
|
||||
@@ -266,6 +369,12 @@ def main():
|
||||
env.setdefault('ISSN_WINDOW_ICON', str(out / 'window_icon.bmp'))
|
||||
except (ImportError, OSError):
|
||||
pass
|
||||
# issn-axy: the shader loading screen shows the dump's own background art (read from the
|
||||
# player's game folder at run time, never shipped with the port).
|
||||
for art in (game / 'sce_sys' / 'pic1.png', original_game / 'sce_sys' / 'pic1.png'):
|
||||
if art.is_file():
|
||||
env.setdefault('ISSN_LOADING_ART', str(art))
|
||||
break
|
||||
# The temporal upscalers and motion vectors are not calibrated for this game yet: they stay
|
||||
# off unless the launcher's experimental switch is on (then issn.ini decides).
|
||||
if env.get('ISSN_EXPERIMENTAL_UPSCALER') != '1':
|
||||
@@ -277,6 +386,13 @@ def main():
|
||||
if profile[3] == 'God of War III':
|
||||
names = graphics_options.patch_names(names, graphics)
|
||||
names = graphics_options.restrict_to_available(names, patches.available_patches(profile[1], profile[2]))
|
||||
# issn: Internal resolution (the shim's render size) replaces the old fixed-size
|
||||
# resolution patches; both would rewrite the same immediates.
|
||||
if profile[3] == 'inFAMOUS Second Son' and (env.get('ISSN_RENDER_HEIGHT') or env.get('ISSN_RENDER_SCALE')):
|
||||
dropped = [n for n in names if n.startswith('Native ') and '(render and output)' in n]
|
||||
if dropped:
|
||||
print(f'Patches: {", ".join(dropped)} left out: Internal resolution sets the size', flush=True)
|
||||
names = [n for n in names if n not in dropped]
|
||||
env['ISSN_PATCHES'] = ';'.join(names)
|
||||
env['ISSN_PATCHES_ONLY'] = '1'
|
||||
(out / 'patch_status.json').unlink(missing_ok=True)
|
||||
@@ -289,6 +405,8 @@ def main():
|
||||
applied = (patch_status(out) or {}).get('applied', [])
|
||||
engine_fps = graphics_options.applied_engine_fps(applied)
|
||||
dmem, vblank = patch_requirements(profile[1], applied, profile[2])
|
||||
if profile[3] == 'inFAMOUS Second Son':
|
||||
dmem = max(dmem or 0, infamous_dmem_mb(env, applied)) or None
|
||||
for key, value in (('ISSN_DMEM_MB', dmem), ('ISSN_VBLANK_HZ', vblank)):
|
||||
if value and key not in env:
|
||||
env[key] = str(value)
|
||||
@@ -341,9 +459,29 @@ def main():
|
||||
# issn_pc: the shader seed of this package (packaging/windows/package.sh) is the cache folder.
|
||||
if (seed_name := shader_seed_name(PORT, user_dir)):
|
||||
env.setdefault('SHADPS4_CACHE_NAME', seed_name)
|
||||
# The cache folder in the log (10-30: a start on an empty CUSA00223 folder compiled every
|
||||
# shader while playing, and nothing in the log said why).
|
||||
try:
|
||||
listed = (PORT / 'shader-seed.txt').read_text().strip()
|
||||
except OSError:
|
||||
listed = ''
|
||||
cache_name = env.get('SHADPS4_CACHE_NAME', 'CUSA00223')
|
||||
others = sorted(p.name for p in (user_dir / 'cache').glob('*') if p.is_dir() and p.name != cache_name)
|
||||
print(f'Shader cache: {cache_name}' + ('' if seed_name else
|
||||
f" (shader-seed.txt {'names ' + listed + ', which is missing' if listed else 'is missing'})")
|
||||
+ (f"; other cache folders: {', '.join(others)}" if others else ''), flush=True)
|
||||
# issn 11-04: the game's trophy file (icons for the popup, read from the player's dump) and,
|
||||
# with the player's own trophy key, the names (scripts/trophies.py).
|
||||
import trophies
|
||||
try:
|
||||
print(trophies.prepare(env, user_dir), flush=True)
|
||||
except OSError as error:
|
||||
print(f'Trophies: {error}', flush=True)
|
||||
from save_backup import backup
|
||||
try:
|
||||
if made := backup(user_dir):
|
||||
if env.get('ISSN_SAVE_BACKUP') == '0':
|
||||
print('Save backup: off (launcher)', flush=True)
|
||||
elif made := backup(user_dir):
|
||||
print(f'Saves backed up to {made}', flush=True)
|
||||
except OSError as error:
|
||||
print(f'Save backup failed, starting anyway: {error}', flush=True)
|
||||
@@ -374,6 +512,40 @@ def main():
|
||||
game_update.remove_view(update_view)
|
||||
|
||||
|
||||
def infamous_render_lines(env, applied):
|
||||
"""The lines inFAMOUS Second Son renders: Internal resolution (ISSN_RENDER_HEIGHT /
|
||||
ISSN_RENDER_SCALE, the shim's render size) or an old "Native WxH" patch, else 1080."""
|
||||
try:
|
||||
if env.get('ISSN_UPSCALE_FACTOR') and float(env['ISSN_UPSCALE_FACTOR']) >= 1:
|
||||
# the window decides (unknown here): a 4K screen, the most it can be
|
||||
return max(360, min(2160, round(2160 / float(env['ISSN_UPSCALE_FACTOR']))))
|
||||
if env.get('ISSN_RENDER_HEIGHT'):
|
||||
return max(360, min(4320, int(env['ISSN_RENDER_HEIGHT'])))
|
||||
if env.get('ISSN_RENDER_SCALE'):
|
||||
return max(360, min(2160, round(1080 * float(env['ISSN_RENDER_SCALE']) / 100)))
|
||||
except ValueError:
|
||||
pass
|
||||
for name in applied:
|
||||
if m := re.match(r'Native (\d+)x(\d+) \(render and output\)', name):
|
||||
return int(m.group(2))
|
||||
return 1080
|
||||
|
||||
|
||||
def infamous_dmem_mb(env, applied):
|
||||
"""Direct memory for inFAMOUS Second Son above 1080 lines (0 at or below). The game gives its
|
||||
GPU heap all the direct memory left at boot (3.3 GiB of the retail 5056 MiB); its render
|
||||
targets at 4K need about four times their 1080p room, and with the retail pool the picture
|
||||
stayed black after the opening poster (RX 7900 XT tester, native 4K patch, 10-24). 4 GiB more
|
||||
at 4K (as the shadPS4 notes ask for God of War III's), in proportion to the pixels below."""
|
||||
lines = infamous_render_lines(env, applied)
|
||||
if lines <= 1080:
|
||||
return 0
|
||||
extra = 4096 * ((lines / 1080) ** 2 - 1) / 3
|
||||
# 16 GiB at most: the probe's pool limit (more is ignored there) and below the guest image,
|
||||
# which starts 24 GiB above the pool (5K needs about 13.3 GiB; 8K is held at 16).
|
||||
return min(16384, 5056 + int(-(-extra // 256) * 256))
|
||||
|
||||
|
||||
# Windows exit codes of a process that ended without its own report.
|
||||
EXIT_CODES = {
|
||||
0xC0000005: 'access violation outside the loader\'s handler',
|
||||
|
||||
@@ -10,6 +10,9 @@ from pathlib import Path
|
||||
# (AYOUB1080p, github.com/weshayoub92/shadPS4-perf): compute workgroups wider than a host subgroup
|
||||
# need barriers where the game relies on GCN waves of 64 running in lockstep.
|
||||
INFAMOUS_SECOND_SON = ('CUSA00004', 'CUSA00223')
|
||||
# issn: sha256 of the 01.07 eboot.bin (the same for CUSA00004 and CUSA00223), whatever APP_VER
|
||||
# the folder's param.sfo says.
|
||||
INFAMOUS_107_EBOOTS = frozenset({'2d1ca79630d7bbe6fa29575f59aa7996d43071d74ac8e4f7d40137967d9039ab'})
|
||||
|
||||
# issn-axy: the GPU core of axylpup's inFAMOUS Second Son build (github.com/axylpup/shadPS4,
|
||||
# second-son 2212a345 = release v1.0.2), with the switches of its release (shadps4-flags.txt).
|
||||
|
||||
@@ -129,3 +129,36 @@ def environment(options):
|
||||
return {'ISSN_DEFERRED_READBACK': options['deferred_readback'],
|
||||
'ISSN_STALE_READBACK': options['stale_readback'],
|
||||
'ISSN_ASYNC_SHADERS': options['async_shaders']}
|
||||
|
||||
|
||||
# issn-axy (tester 10-31): "Apply and restart" in the menu also starts the anti-aliasing, the
|
||||
# upscaler quality and frame generation chosen there (menu_* keys, the launcher's values). The
|
||||
# launcher's own start keeps its environment; a menu restart (restart_unconfirmed=1) takes these.
|
||||
AA_SWITCHES = ('SHADPS4_DLSS', 'SHADPS4_DLSS_UPSCALE', 'ISSN_FSR4', 'ISSN_XESS', 'ISSN_AA_OFF')
|
||||
UPSCALER_SWITCHES = AA_SWITCHES[:4] # frame generation needs one of these
|
||||
UPSCALE_FACTORS = ('1.5', '1.7', '2', '3')
|
||||
|
||||
|
||||
def menu_environment(ini, windows):
|
||||
"""(set, remove) for the environment of a restart from the in-game menu, as the launcher's
|
||||
game_environment would make them from the same choices; ({}, ()) when the ini has none."""
|
||||
if 'menu_anti_aliasing' not in ini:
|
||||
return {}, ()
|
||||
aa = ini.get('menu_anti_aliasing', 'game')
|
||||
env = {}
|
||||
if aa == 'off':
|
||||
env['ISSN_AA_OFF'] = '1'
|
||||
elif aa == 'fsr4':
|
||||
env['ISSN_FSR4'] = '1'
|
||||
elif aa == 'xess' and windows:
|
||||
env['ISSN_XESS'] = '1'
|
||||
elif aa == 'dlss' and windows:
|
||||
env['SHADPS4_DLSS'] = '1'
|
||||
env['SHADPS4_DLSS_UPSCALE'] = '1'
|
||||
if ini.get('menu_frame_gen') == '1' and any(k in env for k in UPSCALER_SWITCHES):
|
||||
env['ISSN_FRAME_GEN'] = '1'
|
||||
if ini.get('menu_upscale_quality') in UPSCALE_FACTORS:
|
||||
env['ISSN_UPSCALE_FACTOR'] = ini['menu_upscale_quality']
|
||||
remove = tuple(k for k in (*AA_SWITCHES, 'ISSN_UPSCALE_FACTOR', 'ISSN_FRAME_GEN') if k not in env)
|
||||
return env, remove
|
||||
|
||||
Loaded 100 of 172 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user