Compare commits
93 Commits
add-self-u
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
9e41d8320a | |
|
|
e998f21e71 | |
|
|
3197ef093c | |
|
|
fa0e81bfba | |
|
|
01ce973882 | |
|
|
f57d2c0965 | |
|
|
735a229236 | |
|
|
9ade7e3582 | |
|
|
259a40abfc | |
|
|
f1577a5a94 | |
|
|
d08a6cef64 | |
|
|
907c65f54c | |
|
|
fe6499b86e | |
|
|
6309ac838d | |
|
|
19799b3079 | |
|
|
4e16234d32 | |
|
|
3c9da9c3f4 | |
|
|
ff6dffe298 | |
|
|
5f6dc0a8d0 | |
|
|
34893dd8c8 | |
|
|
50595d1c13 | |
|
|
717c54e085 | |
|
|
31b7be9ff2 | |
|
|
40692dfb64 | |
|
|
9a3aca963a | |
|
|
1fa79d5cac | |
|
|
c9ec82e32e | |
|
|
2f88a0a3b2 | |
|
|
7e884f55e0 | |
|
|
d70cbd4184 | |
|
|
9bcf80bf67 | |
|
|
ed1195d1e7 | |
|
|
25e1cc7ec9 | |
|
|
a287a54f26 | |
|
|
56738aa4e1 | |
|
|
c11254a996 | |
|
|
aec8c88755 | |
|
|
68d7bc94e2 | |
|
|
8ee31abd9d | |
|
|
9a7c54da58 | |
|
|
5dc6914688 | |
|
|
a7a734b5d3 | |
|
|
e2c8b61de6 | |
|
|
145578ac03 | |
|
|
f01a3750d4 | |
|
|
0e9c77b0a2 | |
|
|
d692d8ee08 | |
|
|
460a196fca | |
|
|
652154aead | |
|
|
49ebbfd627 | |
|
|
7688093ca3 | |
|
|
df08b83287 | |
|
|
fa68e104ea | |
|
|
fd66f580b4 | |
|
|
8f7d745383 | |
|
|
f4692a450e | |
|
|
df5b9b92be | |
|
|
5b0ef07742 | |
|
|
5ce9a52936 | |
|
|
dad900c61d | |
|
|
5b730d4df0 | |
|
|
e699a35f20 | |
|
|
41beda4367 | |
|
|
048fe9f413 | |
|
|
e51de54ee4 | |
|
|
984cfcebd1 | |
|
|
df0412afb5 | |
|
|
f28c0fccb2 | |
|
|
4c87dbebb2 | |
|
|
16a7fe689a | |
|
|
6f0cddd440 | |
|
|
4084a008ea | |
|
|
c0ebf22476 | |
|
|
cb375ec35a | |
|
|
6f3bebcb50 | |
|
|
1029511bbc | |
|
|
65144ce4ac | |
|
|
f848d2761b | |
|
|
730e1a3cf3 | |
|
|
d4bb7fd786 | |
|
|
fbea51c684 | |
|
|
a6f4e8e7f4 | |
|
|
448a4c8a6a | |
|
|
ece4e28740 | |
|
|
58ba1e4cb4 | |
|
|
cf77fb4353 | |
|
|
7e4e142aae | |
|
|
b7b335ac63 | |
|
|
9a5dd9faff | |
|
|
947627d10d | |
|
|
1cfb9522a3 | |
|
|
f63d3befdc | |
|
|
307fdcf7ad |
|
|
@ -257,6 +257,7 @@ jobs:
|
|||
"$install_dir/libMoltenVK.dylib" \
|
||||
"$app_dir/Contents/MacOS/KytyPS5" \
|
||||
"$app_dir/Contents/MacOS/kyty_emulator" \
|
||||
"$app_dir/Contents/MacOS/kyty_updater" \
|
||||
"$app_dir/Contents/Frameworks/libMoltenVK.dylib"; do
|
||||
file "$binary"
|
||||
lipo "$binary" -verify_arch x86_64
|
||||
|
|
@ -266,6 +267,7 @@ jobs:
|
|||
"$install_dir/libMoltenVK.dylib" \
|
||||
"$app_dir/Contents/MacOS/KytyPS5" \
|
||||
"$app_dir/Contents/MacOS/kyty_emulator" \
|
||||
"$app_dir/Contents/MacOS/kyty_updater" \
|
||||
"$app_dir/Contents/Frameworks/libMoltenVK.dylib"; do
|
||||
codesign --verify --strict "$binary"
|
||||
done
|
||||
|
|
@ -407,6 +409,7 @@ jobs:
|
|||
run: |
|
||||
file _Build/linux/install/launcher
|
||||
file _Build/linux/install/kyty_emulator
|
||||
test -x _Build/linux/install/kyty_updater
|
||||
file _Build/linux/install/kyty_emulator | grep -q "ELF 64-bit LSB .*x86-64"
|
||||
ldd _Build/linux/install/kyty_emulator > /dev/null
|
||||
readelf -d _Build/linux/install/launcher | grep -q 'RPATH.*\$ORIGIN/lib'
|
||||
|
|
@ -495,7 +498,8 @@ jobs:
|
|||
test -d "$linux_dir"
|
||||
chmod a+x \
|
||||
"$linux_dir/launcher" \
|
||||
"$linux_dir/kyty_emulator"
|
||||
"$linux_dir/kyty_emulator" \
|
||||
"$linux_dir/kyty_updater"
|
||||
tar -C "$linux_dir" -czf \
|
||||
"$GITHUB_WORKSPACE/$RELEASE_NAME-Linux-x86_64.tar.gz" .
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,11 @@ if (NOT (WIN32 OR LINUX OR APPLE))
|
|||
message(FATAL_ERROR "only Windows, Linux, and macOS builds are supported")
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
# Prevent Windows headers from defining min/max macros in all targets.
|
||||
add_compile_definitions(NOMINMAX)
|
||||
endif()
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
|
@ -298,28 +303,27 @@ list(APPEND kyty_emulator_src "${fault_buffer_shader_header}")
|
|||
list(APPEND kyty_emulator_src ${gpu_tiler_shader_headers})
|
||||
|
||||
set(gpu_blit_generated_dir "${PROJECT_BINARY_DIR}/gpu_blit_shaders")
|
||||
set(gpu_blit_shader_sources
|
||||
"${gpu_tiler_shader_dir}/gpu_blit_fs_triangle.vert"
|
||||
"${gpu_tiler_shader_dir}/gpu_blit_color_to_ms_depth.frag"
|
||||
"${gpu_tiler_shader_dir}/gpu_video_out_overlay.frag"
|
||||
)
|
||||
foreach(shader_source IN LISTS gpu_blit_shader_sources)
|
||||
get_filename_component(shader_name "${shader_source}" NAME_WE)
|
||||
function(add_gpu_blit_shader shader_name shader_file)
|
||||
set(shader_source "${gpu_tiler_shader_dir}/${shader_file}")
|
||||
set(shader_spv "${gpu_blit_generated_dir}/${shader_name}.spv")
|
||||
set(shader_header "${gpu_blit_generated_dir}/${shader_name}_spv.h")
|
||||
string(TOUPPER "${shader_name}_SPV" shader_symbol)
|
||||
add_custom_command(
|
||||
OUTPUT "${shader_header}"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${gpu_blit_generated_dir}"
|
||||
COMMAND "${KYTY_GLSLANG_VALIDATOR}" -V --target-env vulkan1.3 -Os
|
||||
COMMAND "${KYTY_GLSLANG_VALIDATOR}" -V --target-env vulkan1.3 -Os ${ARGN}
|
||||
"-I${gpu_tiler_shader_dir}" -o "${shader_spv}" "${shader_source}"
|
||||
COMMAND ${CMAKE_COMMAND} -DINPUT=${shader_spv} -DOUTPUT=${shader_header}
|
||||
-DSYMBOL=${shader_symbol} -P "${KYTY_SOURCE_DIR}/embed_spirv.cmake"
|
||||
DEPENDS "${shader_source}"
|
||||
VERBATIM
|
||||
)
|
||||
list(APPEND gpu_blit_shader_headers "${shader_header}")
|
||||
endforeach()
|
||||
set(gpu_blit_shader_headers ${gpu_blit_shader_headers} "${shader_header}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
add_gpu_blit_shader(gpu_blit_fs_triangle gpu_blit_fs_triangle.vert)
|
||||
add_gpu_blit_shader(gpu_blit_color_to_ms_depth gpu_blit_color_to_ms_depth.frag)
|
||||
add_gpu_blit_shader(gpu_video_out_overlay gpu_video_out_overlay.frag)
|
||||
add_gpu_blit_shader(gpu_imgui_hdr gpu_video_out_overlay.frag -DIMGUI_HDR=1)
|
||||
list(APPEND kyty_emulator_src ${gpu_blit_shader_headers})
|
||||
|
||||
list(APPEND kyty_emulator_src
|
||||
|
|
@ -399,7 +403,7 @@ add_executable(scalar_provenance_tests EXCLUDE_FROM_ALL
|
|||
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp"
|
||||
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/SrtWalker.cpp"
|
||||
)
|
||||
target_link_libraries(scalar_provenance_tests fmt::fmt)
|
||||
target_link_libraries(scalar_provenance_tests fmt::fmt xbyak::xbyak)
|
||||
target_include_directories(scalar_provenance_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(page_manager_tests EXCLUDE_FROM_ALL
|
||||
|
|
@ -428,6 +432,14 @@ add_executable(avplayer_file_tests EXCLUDE_FROM_ALL
|
|||
target_link_libraries(avplayer_file_tests common FFmpeg::ffmpeg ZArchive::zarchive)
|
||||
target_include_directories(avplayer_file_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(vdecsw_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/VdecswTests.cpp"
|
||||
"${KYTY_SOURCE_DIR}/libs/videoDec2Decoder.cpp"
|
||||
"${KYTY_SOURCE_DIR}/loader/timer.cpp"
|
||||
)
|
||||
target_link_libraries(vdecsw_tests common FFmpeg::ffmpeg)
|
||||
target_include_directories(vdecsw_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(bit_array_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/BitArrayTests.cpp"
|
||||
)
|
||||
|
|
@ -467,7 +479,7 @@ add_executable(resource_materialization_tests EXCLUDE_FROM_ALL
|
|||
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp"
|
||||
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/SrtWalker.cpp"
|
||||
)
|
||||
target_link_libraries(resource_materialization_tests fmt::fmt)
|
||||
target_link_libraries(resource_materialization_tests fmt::fmt xbyak::xbyak)
|
||||
target_include_directories(resource_materialization_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(resource_tracking_tests EXCLUDE_FROM_ALL
|
||||
|
|
@ -480,7 +492,7 @@ add_executable(resource_tracking_tests EXCLUDE_FROM_ALL
|
|||
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp"
|
||||
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/ShaderInfoCollection.cpp"
|
||||
)
|
||||
target_link_libraries(resource_tracking_tests fmt::fmt)
|
||||
target_link_libraries(resource_tracking_tests fmt::fmt xbyak::xbyak)
|
||||
target_include_directories(resource_tracking_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(audio_out2_port_tests EXCLUDE_FROM_ALL
|
||||
|
|
@ -618,6 +630,7 @@ if(BUILD_TESTING)
|
|||
add_test(NAME archive_file COMMAND $<TARGET_FILE:archive_file_tests>)
|
||||
add_test(NAME trophy_system COMMAND $<TARGET_FILE:trophy_system_tests>)
|
||||
add_test(NAME avplayer_file COMMAND $<TARGET_FILE:avplayer_file_tests>)
|
||||
add_test(NAME vdecsw COMMAND $<TARGET_FILE:vdecsw_tests>)
|
||||
add_test(NAME bit_array COMMAND $<TARGET_FILE:bit_array_tests>)
|
||||
add_test(NAME lru_cache COMMAND $<TARGET_FILE:lru_cache_tests>)
|
||||
add_test(NAME shader_vertex_metadata COMMAND $<TARGET_FILE:shader_vertex_metadata_tests>)
|
||||
|
|
@ -694,6 +707,7 @@ if(BUILD_TESTING)
|
|||
archive_file_tests
|
||||
trophy_system_tests
|
||||
avplayer_file_tests
|
||||
vdecsw_tests
|
||||
bit_array_tests
|
||||
lru_cache_tests
|
||||
shader_vertex_metadata_tests
|
||||
|
|
|
|||
91
README.md
91
README.md
|
|
@ -7,12 +7,12 @@
|
|||
[](#current-status)
|
||||
[](LICENSE)
|
||||
|
||||
**[Weekly updates](https://github.com/KytyPS5/KytyPS5/discussions/862)** — game progress, recent fixes and ongoing development.
|
||||
**[Weekly updates](https://github.com/KytyPS5/KytyPS5/discussions/862)** — game progress, recent fixes, and ongoing development.
|
||||
|
||||
**[Development on Discord](https://discord.gg/UNrkMqGaBg)** — KytyPS5 development.
|
||||
|
||||
KytyPS5 is a free and open-source PlayStation 5 emulator written in C++ for Windows and Linux,
|
||||
with experimental macOS support. It is based on a heavily modified version of
|
||||
KytyPS5 is a free and open-source PlayStation 5 translation layer written in C++ for Windows
|
||||
and Linux, with experimental macOS support. It is based on a heavily modified version of
|
||||
[Kyty](https://github.com/InoriRus/Kyty). The project is in active development, and behavior
|
||||
can change significantly between builds.
|
||||
|
||||
|
|
@ -23,13 +23,13 @@ can change significantly between builds.
|
|||
|
||||
## Current Status
|
||||
|
||||
KytyPS5 can boot 2D games and a selection of 3D games, including titles built with Unreal Engine
|
||||
KytyPS5 can boot 2D and 3D games, including titles built with Unreal Engine
|
||||
4/5, Unity, and custom engines. External low-level emulation modules are neither required nor
|
||||
planned.
|
||||
|
||||
Development is currently focused on expanding game compatibility and improving boot reliability.
|
||||
|
||||
Windows and Linux are the primary platforms and receive the most testing.
|
||||
Windows and Linux are the primary target platforms and receive the most testing.
|
||||
|
||||
macOS support is experimental. The emulator is built for x86-64 and runs on Apple Silicon under
|
||||
Rosetta 2, with Vulkan provided by MoltenVK. A small number of titles have been verified in-game
|
||||
|
|
@ -85,14 +85,20 @@ or graphical glitches, so please include the version you tested when reporting a
|
|||
Testing games and submitting detailed bug reports are useful ways to contribute. Search existing
|
||||
issues first, then use the **Game Emulation Status Report** template and attach the complete log file.
|
||||
|
||||
Code contributions should be focused, build successfully on the platforms they touch, and include
|
||||
relevant tests where practical. Windows is the primary target, so a change that alters shared code
|
||||
should not regress it; changes confined to a platform's own code paths only need to build there. Because KytyPS5 is still evolving quickly, consider opening an issue before
|
||||
starting a large change.
|
||||
**Pull requests for general fixes, architectural changes, or refactoring that do not address a concrete bug or launch problem in a specific game will be automatically closed.**
|
||||
|
||||
**Large, unfocused commits or pull requests that bundle unrelated changes without a clear purpose or game-related benefit will also be automatically closed.**
|
||||
|
||||
Keep each PR focused. Name the affected game, describe the exact problem, and provide evidence showing what the change fixes.
|
||||
|
||||
Windows and Linux are the primary target platforms, so changes to shared code should not
|
||||
introduce regressions on either platform. Changes confined to a platform's own code paths
|
||||
only need to build on that platform. Because KytyPS5 is still evolving quickly, consider
|
||||
opening an issue before starting a large change.
|
||||
|
||||
### Formatting
|
||||
|
||||
Set up the clang-format hook after cloning:
|
||||
Set up the clang-format hook after cloning.
|
||||
|
||||
Install `pre-commit` using the method appropriate for your platform:
|
||||
|
||||
|
|
@ -171,9 +177,9 @@ The finished application and its runtime dependencies will be placed in
|
|||
|
||||
### Building on Linux
|
||||
|
||||
Install the toolchain and the libraries the bundled SDL3 needs. Without the audio, Wayland and
|
||||
udev development packages SDL3 quietly configures itself without those backends, and the resulting
|
||||
build has no working sound and no gamepad hotplug:
|
||||
Install the toolchain and the libraries the bundled SDL3 needs. Without the audio, Wayland, and
|
||||
udev development packages, SDL3 quietly configures itself without those backends, and the resulting
|
||||
build has no working audio or gamepad hotplug support:
|
||||
|
||||
```bash
|
||||
sudo apt-get install --no-install-recommends \
|
||||
|
|
@ -183,8 +189,8 @@ sudo apt-get install --no-install-recommends \
|
|||
libasound2-dev libpulse-dev libudev-dev libdbus-1-dev libwayland-dev wayland-protocols
|
||||
```
|
||||
|
||||
Qt 6 (Concurrent, Network, Widgets) is required for the launcher — either the distribution packages
|
||||
(`qt6-base-dev`) or an official Qt installation.
|
||||
Qt 6 (Concurrent, Network, and Widgets) is required for the launcher. Use your distribution's
|
||||
packages (`qt6-base-dev`) or an official Qt installation.
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
|
|
@ -198,7 +204,7 @@ cmake --install _Build/linux --prefix _Build/linux/install
|
|||
```
|
||||
|
||||
The install step copies the Qt libraries and plugins next to the binaries, so
|
||||
`_Build/linux/install` runs without a matching system Qt. FFmpeg is linked statically
|
||||
`_Build/linux/install` runs without a matching system Qt installation. FFmpeg is linked statically
|
||||
from the pinned [KytyPS5 FFmpeg core](https://github.com/KytyPS5/ext-ffmpeg-core)
|
||||
release, including VP9 and WebM support. System FFmpeg packages are not required.
|
||||
|
||||
|
|
@ -242,6 +248,43 @@ sources, so it needs network access; a fully sandboxed `nix build` would require
|
|||
inputs. A Vulkan 1.3 driver must be available at runtime (on NixOS,
|
||||
`hardware.graphics.enable = true`).
|
||||
|
||||
### Building on Fedora and Fedora Atomic (Bazzite, Bluefin, Silverblue)
|
||||
|
||||
On regular Fedora, install dependencies on the host. On Fedora Atomic variants such as Bazzite,
|
||||
Bluefin and Silverblue, use [Distrobox](https://distrobox.it/) and run the install and build steps
|
||||
inside the container. Keep the checkout in your home directory so it is shared with the container.
|
||||
Add `--nvidia` to the create command for hosts using the proprietary NVIDIA driver:
|
||||
|
||||
```bash
|
||||
distrobox create --name kyty-build --image registry.fedoraproject.org/fedora:latest
|
||||
distrobox enter kyty-build
|
||||
```
|
||||
|
||||
Install the toolchain, Qt 6 and SDL3 dependencies:
|
||||
|
||||
```bash
|
||||
sudo dnf install -y \
|
||||
clang lld ninja-build cmake git glslang python3 pkgconf-pkg-config \
|
||||
mesa-libGL-devel libX11-devel libXcursor-devel libXext-devel libXfixes-devel \
|
||||
libXi-devel libXrandr-devel libXScrnSaver-devel libXtst-devel libxkbcommon-devel \
|
||||
alsa-lib-devel pulseaudio-libs-devel systemd-devel dbus-devel \
|
||||
wayland-devel wayland-protocols-devel qt6-qtbase-devel
|
||||
```
|
||||
|
||||
From the repository root, follow the launcher configure, build and install steps in
|
||||
[Building on Linux](#building-on-linux), omitting `-DCMAKE_PREFIX_PATH="$Qt6_DIR"`; CMake finds
|
||||
Fedora's packaged Qt 6 automatically.
|
||||
|
||||
Run `./_Build/linux/install/launcher` on regular Fedora or inside the container. To launch a
|
||||
container build from the host, leave the container with `exit`, then run from the repository root:
|
||||
|
||||
```bash
|
||||
distrobox enter kyty-build -- ./_Build/linux/install/launcher
|
||||
```
|
||||
|
||||
Qt libraries and plugins are bundled. You can also launch directly on a Fedora Atomic host
|
||||
if its other system libraries are compatible with the container build.
|
||||
|
||||
### Building on macOS
|
||||
|
||||
macOS builds target x86-64 and run under Rosetta 2 on Apple Silicon, so the PS5's x86-64 game
|
||||
|
|
@ -253,7 +296,7 @@ Requirements:
|
|||
- An Apple Silicon Mac with Rosetta 2 installed (`softwareupdate --install-rosetta`)
|
||||
- Xcode (or the Command Line Tools)
|
||||
- Homebrew packages: `brew install cmake ninja glslang python`
|
||||
- Qt 6 (Concurrent, Network, Widgets) with x86-64 support. The official Qt installation is
|
||||
- Qt 6 (Concurrent, Network, and Widgets) with x86-64 support. The official Qt installation is
|
||||
universal and works; Homebrew's Qt is arm64-only and will not link
|
||||
|
||||
```bash
|
||||
|
|
@ -269,14 +312,14 @@ cmake --install _Build/macos --prefix _Build/macos/install
|
|||
```
|
||||
|
||||
The build re-signs `kyty_emulator` with the JIT entitlements it needs to execute translated
|
||||
guest code; no manual signing step is required. When the launcher is built, the install
|
||||
guest code; no manual signing step is required. When the launcher is built, the installation
|
||||
also produces `_Build/macos/install/KytyPS5.app` — double-click to launch the GUI.
|
||||
A flat `kyty_emulator` is kept for CLI usage.
|
||||
|
||||
Vulkan comes from MoltenVK. Download `MoltenVK-macos.tar` from the
|
||||
[MoltenVK releases](https://github.com/KhronosGroup/MoltenVK/releases), then copy
|
||||
`MoltenVK/dynamic/dylib/macOS/libMoltenVK.dylib` next to the flat `kyty_emulator`
|
||||
(and, for the bundle, into `KytyPS5.app/Contents/Frameworks/`) and ad-hoc sign it:
|
||||
(and, for the bundle, into `KytyPS5.app/Contents/Frameworks/`) and sign it with an ad hoc signature:
|
||||
|
||||
```bash
|
||||
codesign --force --sign - _Build/macos/install/libMoltenVK.dylib
|
||||
|
|
@ -338,6 +381,14 @@ folders recursively for game directories containing `eboot.bin` and ZArchive (`.
|
|||
whose archive root contains `eboot.bin`. Select a detected game and run it from the game list.
|
||||
ZArchive dumps are mounted read-only and streamed directly; they do not need to be extracted first.
|
||||
|
||||
Official release builds include a built-in updater. Leave **Check for updates on startup** enabled,
|
||||
or click **Check for updates**, then choose **Install Update**. The launcher downloads the package
|
||||
for your platform from GitHub, checks its SHA-256 checksum, and restarts after installation. Close
|
||||
any running games first. Settings, saves, patches, and other files outside the release package are
|
||||
preserved; replacement failures restore the previous files. The installation folder must be writable.
|
||||
If a package cannot be installed automatically, the updater offers the GitHub release page.
|
||||
Source builds and builds from forks do not replace themselves with official releases.
|
||||
|
||||
The emulator can also be started directly with a legally obtained game directory, ELF file, or
|
||||
ZArchive dump:
|
||||
|
||||
|
|
@ -393,5 +444,5 @@ licenses included with those components.
|
|||
- [InoriRus/Kyty](https://github.com/InoriRus/Kyty) — KytyPS5 is based on a heavily modified version
|
||||
of the original Kyty project.
|
||||
- [shadps4-emu/shadPS4](https://github.com/shadps4-emu/shadPS4) — reference for understanding PS4
|
||||
memory behavior, GPU resource aliasing and cache coherency,
|
||||
memory behavior, GPU resource aliasing, cache coherency,
|
||||
and the AVPlayer implementation.
|
||||
|
|
|
|||
|
|
@ -72,6 +72,10 @@ bool FullscreenEnabled() {
|
|||
return g_config->fullscreen_enabled;
|
||||
}
|
||||
|
||||
bool HdrEnabled() {
|
||||
return g_config->hdr_enabled;
|
||||
}
|
||||
|
||||
bool HideCursorEnabled() {
|
||||
return g_config->hide_cursor_enabled;
|
||||
}
|
||||
|
|
@ -96,6 +100,10 @@ bool VulkanValidationEnabled() {
|
|||
return g_config->vulkan_validation_enabled;
|
||||
}
|
||||
|
||||
bool IsNetworkConnected() {
|
||||
return g_config->network_connected;
|
||||
}
|
||||
|
||||
bool ShaderValidationEnabled() {
|
||||
return g_config->shader_validation_enabled;
|
||||
}
|
||||
|
|
@ -182,4 +190,8 @@ const Keymap& GetKeymap() {
|
|||
return g_config->keymap;
|
||||
}
|
||||
|
||||
const std::vector<std::string>& GetGuestArguments() {
|
||||
return g_config->guest_arguments;
|
||||
}
|
||||
|
||||
} // namespace Config
|
||||
|
|
|
|||
|
|
@ -53,9 +53,12 @@ struct ConfigOptions {
|
|||
PresentMode present_mode = PresentMode::Mailbox;
|
||||
int32_t gpu_index = -1;
|
||||
bool fullscreen_enabled = false;
|
||||
bool hdr_enabled = false;
|
||||
bool hide_cursor_enabled = false;
|
||||
bool splash_enabled = true;
|
||||
bool vr_enabled = false;
|
||||
bool amd_cpu_enabled = false;
|
||||
bool network_connected = false;
|
||||
uint32_t vblank_frequency = 60;
|
||||
uint32_t console_language = DEFAULT_CONSOLE_LANGUAGE;
|
||||
bool vulkan_validation_enabled = false;
|
||||
|
|
@ -82,6 +85,8 @@ struct ConfigOptions {
|
|||
bool red_zone_protection_enabled = false;
|
||||
#endif
|
||||
Keymap keymap;
|
||||
// Ordinary boots need a non-null argv[0]; --guest-exec supplies the caller's arguments.
|
||||
std::vector<std::string> guest_arguments {""};
|
||||
};
|
||||
|
||||
void Load(const ConfigOptions& cfg);
|
||||
|
|
@ -97,9 +102,11 @@ uint32_t GetControllerVibrationIntensity();
|
|||
PresentMode GetPresentMode();
|
||||
int32_t GetGpuIndex();
|
||||
bool FullscreenEnabled();
|
||||
bool HdrEnabled();
|
||||
bool HideCursorEnabled();
|
||||
bool VrEnabled();
|
||||
bool AmdCpuEnabled();
|
||||
bool IsNetworkConnected();
|
||||
uint32_t GetVblankFrequency();
|
||||
uint32_t GetConsoleLanguage();
|
||||
bool VulkanValidationEnabled();
|
||||
|
|
@ -135,6 +142,7 @@ bool RedZoneProtectionEnabled();
|
|||
#endif
|
||||
|
||||
const Keymap& GetKeymap();
|
||||
const std::vector<std::string>& GetGuestArguments();
|
||||
|
||||
} // namespace Config
|
||||
|
||||
|
|
|
|||
|
|
@ -8,10 +8,6 @@
|
|||
#include <condition_variable> // IWYU pragma: keep
|
||||
#include <mutex>
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS && KYTY_COMPILER == KYTY_COMPILER_CLANG
|
||||
#define KYTY_WIN_CS
|
||||
#endif
|
||||
|
||||
// macOS has no clock_nanosleep.
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS && !defined(__APPLE__)
|
||||
#define KYTY_POSIX_HIGH_RES_SLEEP
|
||||
|
|
@ -22,7 +18,7 @@
|
|||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#ifdef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
#include <windows.h> // IWYU pragma: keep
|
||||
// IWYU pragma: no_include <winbase.h>
|
||||
constexpr DWORD KYTY_CS_SPIN_COUNT = 4000;
|
||||
|
|
@ -133,7 +129,7 @@ namespace Common {
|
|||
using thread_id_t = std::thread::id;
|
||||
|
||||
struct MutexPrivate {
|
||||
#ifdef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
MutexPrivate() { InitializeCriticalSectionAndSpinCount(&m_cs, KYTY_CS_SPIN_COUNT); }
|
||||
~MutexPrivate() { DeleteCriticalSection(&m_cs); }
|
||||
KYTY_CLASS_NO_COPY(MutexPrivate);
|
||||
|
|
@ -144,7 +140,7 @@ struct MutexPrivate {
|
|||
};
|
||||
|
||||
struct CondVarPrivate {
|
||||
#ifdef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
CondVarPrivate() {
|
||||
static auto func = ResolveInitializeConditionVariable();
|
||||
EXIT_NOT_IMPLEMENTED(func == nullptr);
|
||||
|
|
@ -216,7 +212,7 @@ void Thread::Detach() {
|
|||
}
|
||||
|
||||
void Thread::SleepMicro(uint32_t micros) {
|
||||
#ifdef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
SleepHighResolution100ns(static_cast<uint64_t>(micros) * 10);
|
||||
#elif defined(KYTY_POSIX_HIGH_RES_SLEEP)
|
||||
SleepHighResolutionNanos(static_cast<uint64_t>(micros) * 1000);
|
||||
|
|
@ -226,7 +222,7 @@ void Thread::SleepMicro(uint32_t micros) {
|
|||
}
|
||||
|
||||
void Thread::SleepNano(uint64_t nanos) {
|
||||
#ifdef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
SleepHighResolution100ns((nanos + 99) / 100);
|
||||
#elif defined(KYTY_POSIX_HIGH_RES_SLEEP)
|
||||
SleepHighResolutionNanos(nanos);
|
||||
|
|
@ -262,7 +258,7 @@ Mutex::~Mutex() {
|
|||
}
|
||||
|
||||
void Mutex::Lock() {
|
||||
#ifdef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
EnterCriticalSection(&m_mutex->m_cs);
|
||||
#else
|
||||
m_mutex->m_mutex.lock();
|
||||
|
|
@ -270,7 +266,7 @@ void Mutex::Lock() {
|
|||
}
|
||||
|
||||
void Mutex::Unlock() {
|
||||
#ifdef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
LeaveCriticalSection(&m_mutex->m_cs);
|
||||
#else
|
||||
m_mutex->m_mutex.unlock();
|
||||
|
|
@ -278,7 +274,7 @@ void Mutex::Unlock() {
|
|||
}
|
||||
|
||||
bool Mutex::TryLock() {
|
||||
#ifdef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
return (TryEnterCriticalSection(&m_mutex->m_cs) != 0);
|
||||
#else
|
||||
return m_mutex->m_mutex.try_lock();
|
||||
|
|
@ -292,7 +288,7 @@ CondVar::~CondVar() {
|
|||
}
|
||||
|
||||
void CondVar::Wait(Mutex* mutex) {
|
||||
#ifndef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
|
||||
std::unique_lock<std::recursive_mutex> cpp_lock(mutex->m_mutex->m_mutex, std::adopt_lock_t());
|
||||
#endif
|
||||
auto poll_callback = [&] {
|
||||
|
|
@ -300,7 +296,7 @@ void CondVar::Wait(Mutex* mutex) {
|
|||
if (callback == nullptr) {
|
||||
return;
|
||||
}
|
||||
#if defined(KYTY_WIN_CS)
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
LeaveCriticalSection(&mutex->m_mutex->m_cs);
|
||||
callback();
|
||||
EnterCriticalSection(&mutex->m_mutex->m_cs);
|
||||
|
|
@ -310,7 +306,7 @@ void CondVar::Wait(Mutex* mutex) {
|
|||
cpp_lock.lock();
|
||||
#endif
|
||||
};
|
||||
#ifdef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
static auto func = ResolveSleepConditionVariableCS();
|
||||
EXIT_NOT_IMPLEMENTED(func == nullptr);
|
||||
if (g_cond_wait_poll_callback == nullptr) {
|
||||
|
|
@ -340,10 +336,10 @@ void CondVar::SetWaitPollCallback(wait_poll_func_t callback) {
|
|||
|
||||
bool CondVar::WaitFor(Mutex* mutex, uint32_t micros) {
|
||||
bool ok = false;
|
||||
#ifndef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
|
||||
std::unique_lock<std::recursive_mutex> cpp_lock(mutex->m_mutex->m_mutex, std::adopt_lock_t());
|
||||
#endif
|
||||
#ifdef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
static auto func = ResolveSleepConditionVariableCS();
|
||||
EXIT_NOT_IMPLEMENTED(func == nullptr);
|
||||
ok = !(func(&m_cond_var->m_cv, &mutex->m_mutex->m_cs, (micros < 1000 ? 1 : micros / 1000)) ==
|
||||
|
|
@ -358,7 +354,7 @@ bool CondVar::WaitFor(Mutex* mutex, uint32_t micros) {
|
|||
}
|
||||
|
||||
void CondVar::Signal() {
|
||||
#ifdef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
static auto func = ResolveWakeConditionVariable();
|
||||
EXIT_NOT_IMPLEMENTED(func == nullptr);
|
||||
func(&m_cond_var->m_cv);
|
||||
|
|
@ -368,7 +364,7 @@ void CondVar::Signal() {
|
|||
}
|
||||
|
||||
void CondVar::SignalAll() {
|
||||
#ifdef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
static auto func = ResolveWakeAllConditionVariable();
|
||||
EXIT_NOT_IMPLEMENTED(func == nullptr);
|
||||
func(&m_cond_var->m_cv);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include "common/subsystems.h"
|
||||
#include "common/systemInfo.h"
|
||||
#include "common/threads.h"
|
||||
#include "graphics/presentation/systemOverlay.h"
|
||||
#include "graphics/presentation/window.h"
|
||||
#include "kernel/fileSystem.h"
|
||||
#include "kernel/memory.h"
|
||||
|
|
@ -138,6 +139,9 @@ static void Init(const Config::ConfigOptions& cfg, const std::filesystem::path&
|
|||
Libs::LibKernel::Memory::SetFlexibleMemorySize(flexible_memory_size);
|
||||
}
|
||||
}
|
||||
if (cfg.splash_enabled) {
|
||||
Libs::Graphics::InitializeSplashScreen(param_json.parent_path());
|
||||
}
|
||||
|
||||
// Initialization order is explicit; destruction is automatic and reversed.
|
||||
subsystems.Initialize<Loader::Timer::Lifecycle>();
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ struct RunOptions {
|
|||
};
|
||||
|
||||
void Run(const RunOptions& options);
|
||||
int LoadExec(const char* path, const char* const argv[]);
|
||||
|
||||
} // namespace Emulator
|
||||
|
||||
|
|
|
|||
|
|
@ -1934,13 +1934,7 @@ KYTY_CP_OP_PARSER(CpOpIndirectBuffer) {
|
|||
|
||||
auto* indirect_buffer =
|
||||
reinterpret_cast<const uint32_t*>(buffer[0] | (static_cast<uint64_t>(buffer[1]) << 32u));
|
||||
uint32_t indirect_num_dw = control & 0xfffffu;
|
||||
static std::atomic<uint32_t> indirect_log_count {0};
|
||||
if (indirect_log_count.fetch_add(1) < 128) {
|
||||
LOGF("\t indirect buffer: addr=0x%016" PRIx64 ", num_dw=%" PRIu32 ", control=0x%08" PRIx32
|
||||
"\n",
|
||||
reinterpret_cast<uint64_t>(indirect_buffer), indirect_num_dw, control);
|
||||
}
|
||||
const uint32_t indirect_num_dw = control & 0xfffffu;
|
||||
|
||||
if (indirect_num_dw == 0) {
|
||||
return 3;
|
||||
|
|
@ -2148,27 +2142,23 @@ KYTY_CP_OP_PARSER(CpOpIndirectUcRegs) {
|
|||
KYTY_CP_OP_PARSER(CpOpMarker) {
|
||||
KYTY_PROFILER_FUNCTION();
|
||||
|
||||
// EXIT_NOT_IMPLEMENTED(cmd_id != 0xC0001000);
|
||||
|
||||
uint32_t id = buffer[0] & 0xfffu;
|
||||
uint32_t len_dw = ((cmd_id >> 16u) & 0x3fffu);
|
||||
|
||||
switch (id) {
|
||||
case 0x0: break;
|
||||
case 0x4: cp.SetUserDataMarker(HW::UserSgprType::Vsharp); break;
|
||||
case 0xd: cp.SetUserDataMarker(HW::UserSgprType::Region); break;
|
||||
case 0x777: {
|
||||
switch (buffer[0]) {
|
||||
case 0x68750004: cp.SetUserDataMarker(HW::UserSgprType::Vsharp); break;
|
||||
case 0x6875000d: cp.SetUserDataMarker(HW::UserSgprType::Region); break;
|
||||
case 0x68750777: {
|
||||
cp.Flip();
|
||||
break;
|
||||
}
|
||||
case 0x778: {
|
||||
case 0x68750778: {
|
||||
auto* addr =
|
||||
reinterpret_cast<void*>(buffer[1] | (static_cast<uint64_t>(buffer[2]) << 32u));
|
||||
uint32_t value = buffer[3];
|
||||
cp.Flip(addr, value);
|
||||
break;
|
||||
}
|
||||
case 0x781: {
|
||||
case 0x68750781: {
|
||||
auto* addr =
|
||||
reinterpret_cast<void*>(buffer[1] | (static_cast<uint64_t>(buffer[2]) << 32u));
|
||||
uint32_t value = buffer[3];
|
||||
|
|
@ -2177,7 +2167,8 @@ KYTY_CP_OP_PARSER(CpOpMarker) {
|
|||
cp.FlipWithInterrupt(eop_event_type, cache_action, addr, value);
|
||||
break;
|
||||
}
|
||||
default: EXIT("unknown marker at %05" PRIx32 ": 0x%" PRIx32 "\n", num_dw - dw, id);
|
||||
// NOP payloads are caller-owned data; only complete known marker tags execute.
|
||||
default: break;
|
||||
}
|
||||
|
||||
return len_dw + 1;
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ namespace {
|
|||
|
||||
struct FormatInfo {
|
||||
BufferFormat format;
|
||||
uint32_t bytes_per_element;
|
||||
uint32_t block_compressed_bytes_per_block;
|
||||
uint32_t render_target_bytes_per_element;
|
||||
uint8_t bytes_per_element;
|
||||
uint8_t block_compressed_bytes_per_block;
|
||||
uint8_t render_target_bytes_per_element;
|
||||
bool sampled_texture;
|
||||
bool uint_texture;
|
||||
bool sint_texture = false;
|
||||
|
|
@ -21,7 +21,7 @@ struct FormatInfo {
|
|||
constexpr FormatInfo kFormatInfo[] = {
|
||||
{BufferFormat::k8UNorm, 1, 0, 1, true, false},
|
||||
{BufferFormat::k8UScaled, 1, 0, 0, true, false},
|
||||
{BufferFormat::k8SNorm, 1, 0, 1, false, false},
|
||||
{BufferFormat::k8SNorm, 1, 0, 1, true, false},
|
||||
{BufferFormat::k8UInt, 1, 0, 1, true, true},
|
||||
{BufferFormat::k16UNorm, 2, 0, 2, true, false},
|
||||
{BufferFormat::k16SNorm, 2, 0, 2, true, false},
|
||||
|
|
@ -41,6 +41,7 @@ constexpr FormatInfo kFormatInfo[] = {
|
|||
{BufferFormat::k16_16UInt, 4, 0, 4, true, true},
|
||||
{BufferFormat::k16_16SInt, 4, 0, 4, true, false, true},
|
||||
{BufferFormat::k16_16Float, 4, 0, 4, true, false},
|
||||
{BufferFormat::k11_11_10UNorm, 4, 0, 0, true, false},
|
||||
{BufferFormat::k11_11_10UInt, 4, 0, 4, true, true},
|
||||
{BufferFormat::k11_11_10Float, 4, 0, 4, true, false},
|
||||
{BufferFormat::k10_11_11Float, 4, 0, 4, true, false},
|
||||
|
|
@ -237,6 +238,7 @@ BufferFormat RemapTextureFormat(BufferFormat format) {
|
|||
switch (format) {
|
||||
case BufferFormat::k8UScaled: return BufferFormat::k8UNorm;
|
||||
case BufferFormat::k8_8UScaled: return BufferFormat::k8_8UNorm;
|
||||
case BufferFormat::k11_11_10UNorm:
|
||||
case BufferFormat::k11_11_10UInt: return BufferFormat::k32UInt;
|
||||
default: return format;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -576,12 +576,7 @@ bool GuestGpu::Process(Submission& submission) {
|
|||
}
|
||||
}
|
||||
if (progressed) {
|
||||
if (complete) {
|
||||
m_renderer.RunGarbageCollector();
|
||||
}
|
||||
cp.BufferFlush();
|
||||
} else if (complete) {
|
||||
m_renderer.RunGarbageCollector();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -602,20 +597,16 @@ bool GuestGpu::Process(Submission& submission) {
|
|||
complete = cp.Process(submission.command_execution, submission.commands) ==
|
||||
Pm4ProcessResult::Complete;
|
||||
if (submission.command_execution.MadeProgress()) {
|
||||
if (complete) {
|
||||
m_renderer.RunGarbageCollector();
|
||||
}
|
||||
cp.BufferFlush();
|
||||
} else if (complete) {
|
||||
m_renderer.RunGarbageCollector();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SubmissionType::FlipPreparation:
|
||||
m_renderer.RunGarbageCollector();
|
||||
cp.PrepareCpuFlip(submission.flip_request_id);
|
||||
break;
|
||||
case SubmissionType::SuspendPoint:
|
||||
// Age caches at the frame boundary, not once per command list.
|
||||
m_renderer.RunGarbageCollector();
|
||||
cp.EmitGlobalBarrier();
|
||||
m_renderer.GetCommandScheduler().DeferPriorityOperation(
|
||||
[ready = m_suspend_point_ready] { ready->release(); });
|
||||
|
|
@ -1072,8 +1063,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
|||
case 0x03: with_interrupt = false; break;
|
||||
case 0x01:
|
||||
if (!IsAsyncComputeQueue()) {
|
||||
Sync::TriggerEopEventAtEndOfPipe(command, m_interrupt_event_id,
|
||||
interrupt_context_id);
|
||||
TriggerEopEventAtEndOfPipe(interrupt_context_id);
|
||||
return;
|
||||
}
|
||||
with_interrupt = true;
|
||||
|
|
@ -1082,24 +1072,15 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
|||
default: EXIT("unknown interrupt_selector\n");
|
||||
}
|
||||
|
||||
auto write32 = [&](bool with_writeback) {
|
||||
auto write32 = [&] {
|
||||
auto* dst = static_cast<uint32_t*>(dst_gpu_addr);
|
||||
auto data = static_cast<uint32_t>(value);
|
||||
EXIT_IF(dst == nullptr);
|
||||
(void)command.Handle();
|
||||
std::memcpy(dst, &data, sizeof(data));
|
||||
|
||||
if (with_interrupt) {
|
||||
if (with_writeback) {
|
||||
Sync::WriteAtEndOfPipeWithInterruptWriteBack32(m_submit_id, command, dst,
|
||||
data, m_interrupt_event_id,
|
||||
interrupt_context_id);
|
||||
} else {
|
||||
Sync::WriteAtEndOfPipeWithInterrupt32(m_submit_id, command, dst, data,
|
||||
m_interrupt_event_id, interrupt_context_id);
|
||||
}
|
||||
} else if (with_writeback) {
|
||||
Sync::WriteAtEndOfPipeWithWriteBack32(m_submit_id, command, dst, data);
|
||||
} else {
|
||||
Sync::WriteAtEndOfPipe32(m_submit_id, command, dst, data);
|
||||
TriggerEopEventAtEndOfPipe(interrupt_context_id);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1111,15 +1092,13 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
|||
SynchronizeGpu();
|
||||
Sync::ReadGds(*m_renderer.GetBufferCache().GetGdsBuffer(), dst, value & 0xffffu,
|
||||
value >> 16u);
|
||||
Sync::WriteAtEndOfPipeGds32(m_submit_id, command, dst, value & 0xffffu,
|
||||
value >> 16u);
|
||||
if (with_interrupt) {
|
||||
m_renderer.TriggerInterrupt(m_interrupt_event_id, interrupt_context_id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if (eop_event_type == 0x04 && cache_action == 0x00 && event_index == 0x05) {
|
||||
write32(false);
|
||||
write32();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
|
@ -1128,8 +1107,8 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
|||
if constexpr (sizeof(T) == sizeof(uint32_t)) {
|
||||
if (event_write_source == 0x02 && eop_event_type == 0x2f && event_index == 0x06) {
|
||||
switch (cache_action) {
|
||||
case 0x00: write32(false); return;
|
||||
case 0x38: write32(true); return;
|
||||
case 0x00:
|
||||
case 0x38: write32(); return;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
|
@ -1137,25 +1116,14 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
|||
if (event_write_source == 0x04) {
|
||||
value = Sync::ReadReferenceClock();
|
||||
}
|
||||
auto write64 = [&](bool with_writeback) {
|
||||
auto write64 = [&] {
|
||||
auto* dst = static_cast<uint64_t*>(dst_gpu_addr);
|
||||
EXIT_IF(dst == nullptr);
|
||||
(void)command.Handle();
|
||||
std::memcpy(dst, &value, sizeof(value));
|
||||
|
||||
if (with_interrupt) {
|
||||
if (with_writeback) {
|
||||
Sync::WriteAtEndOfPipeWithInterruptWriteBack64(
|
||||
m_submit_id, command, dst, value, m_interrupt_event_id,
|
||||
interrupt_context_id);
|
||||
} else {
|
||||
Sync::WriteAtEndOfPipeWithInterrupt64(m_submit_id, command, dst,
|
||||
value, m_interrupt_event_id,
|
||||
interrupt_context_id);
|
||||
}
|
||||
} else if (with_writeback) {
|
||||
Sync::WriteAtEndOfPipeWithWriteBack64(m_submit_id, command, dst,
|
||||
value);
|
||||
} else {
|
||||
Sync::WriteAtEndOfPipe64(m_submit_id, command, dst, value);
|
||||
TriggerEopEventAtEndOfPipe(interrupt_context_id);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1164,7 +1132,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
|||
switch (eop_event_type) {
|
||||
case 0x04:
|
||||
if (event_index == 0x05) {
|
||||
write64(false);
|
||||
write64();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
|
@ -1172,7 +1140,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
|||
case 0x28:
|
||||
case 0x2f:
|
||||
if (event_index == 0x00) {
|
||||
write64(false);
|
||||
write64();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
|
@ -1180,7 +1148,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
|||
case 0x2d:
|
||||
case 0x30:
|
||||
if (event_index == 0x00 && !with_interrupt) {
|
||||
write64(false);
|
||||
write64();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
|
@ -1195,20 +1163,20 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
|||
if (((eop_event_type == 0x04 || eop_event_type == 0x28) &&
|
||||
event_index == 0x05) ||
|
||||
(event_index == 0x00)) {
|
||||
write64(true);
|
||||
write64();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 0x2b:
|
||||
case 0x2d:
|
||||
if (event_index == 0x00 && !with_interrupt) {
|
||||
write64(true);
|
||||
write64();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 0x2f:
|
||||
if (event_index == 0x06 && !with_interrupt) {
|
||||
write64(true);
|
||||
write64();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
|
@ -1217,7 +1185,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
|||
break;
|
||||
case 0x3b:
|
||||
if (eop_event_type == 0x04 && event_index == 0x05 && with_interrupt) {
|
||||
write64(true);
|
||||
write64();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
|
@ -1270,7 +1238,11 @@ void CommandProcessor::EmitGlobalBarrier() {
|
|||
}
|
||||
|
||||
void CommandProcessor::TriggerEopEventAtEndOfPipe(uint32_t interrupt_context_id) {
|
||||
Sync::TriggerEopEventAtEndOfPipe(CurrentBuffer(), m_interrupt_event_id, interrupt_context_id);
|
||||
(void)CurrentBuffer().Handle();
|
||||
GetScheduler().DeferPriorityOperation(
|
||||
[&renderer = m_renderer, event_id = m_interrupt_event_id, interrupt_context_id] {
|
||||
renderer.TriggerInterrupt(event_id, interrupt_context_id);
|
||||
});
|
||||
}
|
||||
|
||||
void CommandProcessor::TriggerEvent(uint32_t event_type, uint32_t event_index,
|
||||
|
|
@ -1360,8 +1332,9 @@ void CommandProcessor::Flip() {
|
|||
auto& command = CurrentBuffer();
|
||||
auto request = Sync::PrepareVideoOutFlip(command, m_flip.handle, m_flip.index, m_flip.flip_mode,
|
||||
m_flip.flip_arg);
|
||||
Sync::WriteAtEndOfPipeOnlyFlip(m_submit_id, command, m_flip.handle, m_flip.index,
|
||||
m_flip.flip_mode, m_flip.flip_arg, request);
|
||||
(void)command.Handle();
|
||||
GetScheduler().DeferPriorityOperation(
|
||||
[&renderer = m_renderer, request] { renderer.GetVideoOut().CompleteFlip(request); });
|
||||
GetScheduler().Flush();
|
||||
}
|
||||
|
||||
|
|
@ -1375,12 +1348,13 @@ void CommandProcessor::Flip(void* dst_gpu_addr, uint32_t value) {
|
|||
reinterpret_cast<uint64_t>(dst_gpu_addr), value);
|
||||
}
|
||||
|
||||
EXIT_IF(dst_gpu_addr == nullptr);
|
||||
std::memcpy(dst_gpu_addr, &value, sizeof(value));
|
||||
auto request = Sync::PrepareVideoOutFlip(command, m_flip.handle, m_flip.index, m_flip.flip_mode,
|
||||
m_flip.flip_arg);
|
||||
Sync::WriteAtEndOfPipeWithFlip32(m_submit_id, command, static_cast<uint32_t*>(dst_gpu_addr),
|
||||
value, m_flip.handle, m_flip.index, m_flip.flip_mode,
|
||||
m_flip.flip_arg, request);
|
||||
(void)command.Handle();
|
||||
GetScheduler().DeferPriorityOperation(
|
||||
[&renderer = m_renderer, request] { renderer.GetVideoOut().CompleteFlip(request); });
|
||||
GetScheduler().Flush();
|
||||
}
|
||||
|
||||
|
|
@ -1400,12 +1374,16 @@ void CommandProcessor::FlipWithInterrupt(uint32_t eop_event_type, uint32_t cache
|
|||
if (eop_event_type != 0x00000004 || cache_action != 0x00000038) {
|
||||
EXIT("unknown event type\n");
|
||||
}
|
||||
EXIT_IF(dst_gpu_addr == nullptr);
|
||||
std::memcpy(dst_gpu_addr, &value, sizeof(value));
|
||||
auto request = Sync::PrepareVideoOutFlip(command, m_flip.handle, m_flip.index, m_flip.flip_mode,
|
||||
m_flip.flip_arg);
|
||||
Sync::WriteAtEndOfPipeWithInterruptWriteBackFlip32(
|
||||
m_submit_id, command, static_cast<uint32_t*>(dst_gpu_addr), value, m_flip.handle,
|
||||
m_flip.index, m_flip.flip_mode, m_flip.flip_arg, request, m_interrupt_event_id);
|
||||
(void)command.Handle();
|
||||
GetScheduler().DeferPriorityOperation(
|
||||
[&renderer = m_renderer, event_id = m_interrupt_event_id, request] {
|
||||
renderer.GetVideoOut().CompleteFlip(request);
|
||||
renderer.TriggerInterrupt(event_id, 0);
|
||||
});
|
||||
GetScheduler().Flush();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -299,8 +299,8 @@ bool TileGetTextureBlockLayout(Prospero::BufferFormat format, Prospero::TileMode
|
|||
if (!TileGetTextureElementLayout(format, element)) {
|
||||
return false;
|
||||
}
|
||||
if ((family == TileBlockFamily::Depth64KB || family == TileBlockFamily::RenderTarget64KB) &&
|
||||
Prospero::RenderTargetBytesPerElement(format) != element.bytes) {
|
||||
if (family == TileBlockFamily::Depth64KB &&
|
||||
(element.texel_width != 1 || element.texel_height != 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ struct GraphicContext {
|
|||
// bool fp64_denorm_preserve = false; // Temporarily disabled.
|
||||
bool attachment_feedback_loop_enabled = false;
|
||||
bool provoking_vertex_last_enabled = false;
|
||||
bool maintenance8_enabled = false;
|
||||
bool supports_block_texel_view = false;
|
||||
bool mesh_shader_enabled = false;
|
||||
vk::PhysicalDeviceMeshShaderPropertiesEXT mesh_shader_properties = {};
|
||||
|
|
|
|||
|
|
@ -27,6 +27,26 @@ namespace {
|
|||
constexpr uint64_t MiB = 1024 * 1024;
|
||||
constexpr uint64_t GdsBufferSize = 64 * 1024;
|
||||
|
||||
std::pair<Buffer*, uint64_t> UploadMemory(GraphicContext& graphics, CommandScheduler& scheduler,
|
||||
StreamBuffer& staging, uint64_t size, uint64_t alignment,
|
||||
vk::BufferUsageFlags usage, const auto& write) {
|
||||
const auto [mapped, offset] = staging.Map(size, alignment);
|
||||
if (mapped != nullptr) {
|
||||
write(mapped, offset);
|
||||
staging.Commit();
|
||||
return {&staging, offset};
|
||||
}
|
||||
|
||||
// Large transfers use the same one-time host allocation as ordinary buffer uploads.
|
||||
auto temporary = std::make_unique<Buffer>(graphics, scheduler, MemoryUsage::Upload, 0,
|
||||
usage, size);
|
||||
write(temporary->Mapped().data(), 0);
|
||||
temporary->Flush(0, size);
|
||||
auto* buffer = temporary.get();
|
||||
scheduler.DeferOperation([owner = std::move(temporary)]() mutable { owner.reset(); });
|
||||
return {buffer, 0};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void BufferCache::WriteDataBuffer(Buffer& buffer, uint64_t address, const void* source,
|
||||
|
|
@ -447,28 +467,17 @@ vk::Buffer BufferCache::UploadCopies(Buffer& buffer, std::span<vk::BufferCopy> c
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
auto [mapped, base_offset] = m_staging_buffer.Map(total_size, 4);
|
||||
if (mapped != nullptr) {
|
||||
const auto source = UploadMemory(m_graphics, m_scheduler, m_staging_buffer, total_size, 4,
|
||||
vk::BufferUsageFlagBits::eTransferSrc, [&](uint8_t* mapped, uint64_t base_offset) {
|
||||
for (auto& copy: copies) {
|
||||
const auto address = buffer.CpuAddress() + copy.dstOffset;
|
||||
std::memcpy(mapped + copy.srcOffset, reinterpret_cast<const void*>(address), copy.size);
|
||||
if (!LibKernel::Memory::CopySparseMemory(address, mapped + copy.srcOffset, copy.size)) {
|
||||
EXIT("BufferCache: failed to read sparse guest buffer memory\n");
|
||||
}
|
||||
copy.srcOffset += base_offset;
|
||||
}
|
||||
m_staging_buffer.Commit();
|
||||
return m_staging_buffer.Handle();
|
||||
}
|
||||
|
||||
auto temporary = std::make_unique<Buffer>(m_graphics, m_scheduler, MemoryUsage::Upload, 0,
|
||||
vk::BufferUsageFlagBits::eTransferSrc, total_size);
|
||||
for (const auto& copy: copies) {
|
||||
const auto address = buffer.CpuAddress() + copy.dstOffset;
|
||||
std::memcpy(temporary->Mapped().data() + copy.srcOffset,
|
||||
reinterpret_cast<const void*>(address), copy.size);
|
||||
}
|
||||
temporary->Flush(0, total_size);
|
||||
const auto handle = temporary->Handle();
|
||||
m_scheduler.DeferOperation([owner = std::move(temporary)]() mutable { owner.reset(); });
|
||||
return handle;
|
||||
});
|
||||
return source.first->Handle();
|
||||
}
|
||||
|
||||
std::pair<Buffer*, uint64_t> BufferCache::ObtainBuffer(uint64_t vaddr, uint64_t size,
|
||||
|
|
@ -486,7 +495,9 @@ std::pair<Buffer*, uint64_t> BufferCache::ObtainBuffer(uint64_t vaddr, uint64_t
|
|||
m_graphics.physical_device_properties.limits.minUniformBufferOffsetAlignment, 1);
|
||||
auto [mapped, offset] = m_stream_buffer.Map(size, alignment, false);
|
||||
if (mapped != nullptr) {
|
||||
std::memcpy(mapped, reinterpret_cast<const void*>(vaddr), size);
|
||||
if (!LibKernel::Memory::CopySparseMemory(vaddr, mapped, size)) {
|
||||
EXIT("BufferCache: failed to read sparse guest buffer memory\n");
|
||||
}
|
||||
m_stream_buffer.Commit();
|
||||
return {&m_stream_buffer, offset};
|
||||
}
|
||||
|
|
@ -521,12 +532,13 @@ std::pair<Buffer*, uint64_t> BufferCache::ObtainBufferForImage(uint64_t vaddr, u
|
|||
return ObtainBuffer(vaddr, size, false, false);
|
||||
}
|
||||
|
||||
auto [staging, stage_offset] = m_staging_buffer.Map(size, 16);
|
||||
if (staging == nullptr || !Libs::LibKernel::Memory::TryReadSparseBacking(vaddr, staging, size)) {
|
||||
EXIT("BufferCache: failed to read mapped guest image backing\n");
|
||||
}
|
||||
m_staging_buffer.Commit();
|
||||
return {&m_staging_buffer, stage_offset};
|
||||
return UploadMemory(m_graphics, m_scheduler, m_staging_buffer, size, 16,
|
||||
vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eStorageBuffer,
|
||||
[&](uint8_t* mapped, uint64_t) {
|
||||
if (!Libs::LibKernel::Memory::TryReadSparseBacking(vaddr, mapped, size)) {
|
||||
EXIT("BufferCache: failed to read mapped guest image backing\n");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void BufferCache::FillBuffer(uint64_t vaddr, uint64_t size, uint32_t value, bool is_gds) {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
#include <mutex>
|
||||
#include <span>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vulkan/vulkan_format_traits.hpp>
|
||||
|
||||
namespace Libs::Graphics {
|
||||
|
|
@ -36,7 +37,9 @@ constexpr uint64_t NumFramesBeforeRemoval = 32;
|
|||
[[nodiscard]] bool DecodeColorClear(const TextureCache::ImageDesc& desc, uint8_t code,
|
||||
vk::ClearColorValue& clear) {
|
||||
const auto& metadata = desc.info.metadata;
|
||||
const auto format = desc.view_info.format;
|
||||
const auto format = desc.info.IsDepth()
|
||||
? TextureGetSurfaceFormatInfo(desc.info.guest_format).vk_format
|
||||
: desc.view_info.format;
|
||||
const bool cmask = metadata.kind == ImageMetadataKind::Cmask;
|
||||
if (cmask ? code == 0 : code == 0x20) {
|
||||
// Register clears belong to the color buffer; the texture pipe cannot decode them.
|
||||
|
|
@ -249,14 +252,11 @@ void TextureCache::DeleteImage(ImageId id) {
|
|||
if (image == nullptr || !image->registered) {
|
||||
return;
|
||||
}
|
||||
if (!image->depth_id) {
|
||||
std::vector<ImageId> associations;
|
||||
m_slot_images.ForEach([&](ImageId candidate, const Image& associated) {
|
||||
if (associated.depth_id == id) {
|
||||
associations.push_back(candidate);
|
||||
}
|
||||
});
|
||||
for (const auto association: associations) {
|
||||
if (image->depth_id) {
|
||||
std::erase(m_slot_images[image->depth_id].stencil_ids, id);
|
||||
} else {
|
||||
// Detach first: deleting each association also unlinks it from its depth owner.
|
||||
for (const auto association: std::exchange(image->stencil_ids, {})) {
|
||||
FreeImage(association);
|
||||
}
|
||||
}
|
||||
|
|
@ -297,9 +297,6 @@ void TextureCache::TouchImage(Image& image) {
|
|||
|
||||
void TextureCache::MarkAsMaybeDirty(ImageId id, Image& image) {
|
||||
image.MarkMaybeCpuDirty();
|
||||
if (image.NeedsMaybeCpuHash()) {
|
||||
image.SetMaybeCpuHash(image.HashGuestEdges());
|
||||
}
|
||||
UntrackImage(id);
|
||||
}
|
||||
|
||||
|
|
@ -631,6 +628,9 @@ void TextureCache::CopyImage(ImageId destination_id, ImageId source_id) {
|
|||
void TextureCache::CopyImageMip(ImageId destination_id, ImageId source_id, uint32_t mip,
|
||||
uint32_t layer) {
|
||||
RefreshCopySource(source_id);
|
||||
if (m_slot_images[destination_id].IsBufferModified()) {
|
||||
RefreshImage(destination_id);
|
||||
}
|
||||
auto& destination = m_slot_images[destination_id];
|
||||
auto& source = m_slot_images[source_id];
|
||||
TrackImage(destination_id);
|
||||
|
|
@ -751,11 +751,63 @@ ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested, BindingTyp
|
|||
return replacement_id;
|
||||
}
|
||||
|
||||
namespace BcAlias {
|
||||
|
||||
bool Matches(const ImageInfo& requested, const ImageInfo& cached) {
|
||||
return requested.guest_format != cached.guest_format &&
|
||||
requested.IsBlock() != cached.IsBlock() &&
|
||||
requested.tile_mode == cached.tile_mode &&
|
||||
requested.bytes_per_block == cached.bytes_per_block &&
|
||||
requested.samples == 1 && cached.samples == 1 &&
|
||||
(requested.data != cached.data || requested.resources != cached.resources ||
|
||||
requested.BlockExtent() != cached.BlockExtent()) &&
|
||||
!TextureCache::SameBacking(cached, requested, false);
|
||||
}
|
||||
|
||||
bool Publish(TextureCache& cache, Image& image) {
|
||||
if (!cache.CanDownload(image)) {
|
||||
return false;
|
||||
}
|
||||
// ObtainBuffer preserves padding and records ownership before the GPU-only transfer.
|
||||
auto [buffer, offset] =
|
||||
cache.m_buffer_cache.ObtainBuffer(image.info.data.address, image.info.data.size, true);
|
||||
cache.DownloadImage(image, *buffer, offset, image.info.data.size, image.info.resources.levels);
|
||||
image.ClearGpuModified();
|
||||
image.MarkBufferModified();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Resolve(TextureCache& cache, ImageId cached_id, ImageId merged_id) {
|
||||
auto& cached = cache.m_slot_images[cached_id];
|
||||
auto* merged = merged_id ? &cache.m_slot_images[merged_id] : nullptr;
|
||||
// PS5 raw block images can span several packed BC mips. Transfer their complete
|
||||
// guest bytes, including tail padding, before retiring the incompatible backing.
|
||||
if (cached.SafeToDownload()) {
|
||||
if (merged && merged->SafeToDownload() && !Publish(cache, *merged)) {
|
||||
return;
|
||||
}
|
||||
if (!Publish(cache, cached)) {
|
||||
return;
|
||||
}
|
||||
if (merged) {
|
||||
merged->MarkBufferModified();
|
||||
}
|
||||
}
|
||||
cached.binding.needs_rebind |= cached.binding.is_bound || cached.binding.is_target;
|
||||
cache.FreeImage(cached_id);
|
||||
}
|
||||
|
||||
} // namespace BcAlias
|
||||
|
||||
TextureCache::OverlapResult TextureCache::ResolveOverlap(const ImageInfo& requested,
|
||||
BindingType binding, ImageId cached_id,
|
||||
ImageId merged_id) {
|
||||
auto owner = m_slot_images.try_get(cached_id);
|
||||
if (owner == nullptr) {
|
||||
if (owner == nullptr || !owner->registered) {
|
||||
return {merged_id};
|
||||
}
|
||||
if (BcAlias::Matches(requested, owner->info) && !owner->IsCpuDirty()) {
|
||||
BcAlias::Resolve(*this, cached_id, merged_id);
|
||||
return {merged_id};
|
||||
}
|
||||
auto& cached = *owner;
|
||||
|
|
@ -1059,8 +1111,15 @@ void TextureCache::MaterializeColorClear(ImageId id, const ImageDesc& desc,
|
|||
}
|
||||
{
|
||||
std::scoped_lock lock {m_lock};
|
||||
ClearImage(m_scheduler.Current(), id, view.format,
|
||||
{vk::ImageAspectFlagBits::eColor, view.base_level, view.level_count,
|
||||
const auto& image = m_slot_images[id];
|
||||
const bool depth = image.info.IsDepth();
|
||||
if (depth) {
|
||||
const auto value = clear.color.float32[0];
|
||||
clear.depthStencil = {value, 0u};
|
||||
}
|
||||
ClearImage(m_scheduler.Current(), id, depth ? image.backing.format : view.format,
|
||||
{depth ? vk::ImageAspectFlagBits::eDepth : vk::ImageAspectFlagBits::eColor,
|
||||
view.base_level, view.level_count,
|
||||
image_first + slice, 1}, clear);
|
||||
}
|
||||
// Publish the conversion's expanded keys without treating them as guest writes
|
||||
|
|
@ -1082,12 +1141,7 @@ void TextureCache::RefreshImage(ImageId id) {
|
|||
}
|
||||
TrackImage(id);
|
||||
if (image.IsMaybeCpuDirty()) {
|
||||
const auto hash = image.HashGuestEdges();
|
||||
if (image.NeedsMaybeCpuHash()) {
|
||||
image.SetMaybeCpuHash(hash);
|
||||
return;
|
||||
}
|
||||
(void)image.ResolveMaybeCpuHash(hash);
|
||||
(void)image.ResolveMaybeCpuHash(image.HashGuestEdges());
|
||||
}
|
||||
bool cpu_dirty = image.IsBufferModified() || image.IsDefinitelyCpuDirty();
|
||||
if (image.info.metadata.compression != VideoOutCompression::Uncompressed) {
|
||||
|
|
@ -1141,6 +1195,12 @@ ImageId TextureCache::AssociateStencil(ImageId depth_id, GuestRange stencil) {
|
|||
plane.IsTiled() ? TileGetDepthPitch(plane.extent.width, 1, 0) : depth.info.pitch;
|
||||
plane.bytes_per_block = 1;
|
||||
plane.UpdateSize();
|
||||
if (record.depth_id != depth_id) {
|
||||
if (record.depth_id) {
|
||||
std::erase(m_slot_images[record.depth_id].stencil_ids, association);
|
||||
}
|
||||
depth.stencil_ids.push_back(association);
|
||||
}
|
||||
record.info = plane;
|
||||
record.depth_id = depth_id;
|
||||
record.stencil_subresources = depth.stencil_subresources;
|
||||
|
|
@ -1166,6 +1226,7 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
|
|||
std::scoped_lock lock {m_lock};
|
||||
const auto candidates =
|
||||
FindImagesInRegion(desc.info.data.address, desc.info.data.size, false);
|
||||
bool block_alias = false;
|
||||
|
||||
for (const auto id: candidates) {
|
||||
const auto& image = m_slot_images[id];
|
||||
|
|
@ -1184,6 +1245,9 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
|
|||
}
|
||||
if (SameBacking(image.info, desc.info, exact_format)) {
|
||||
result = id;
|
||||
} else if (image.SafeToDownload() &&
|
||||
BcAlias::Matches(desc.info, image.info)) {
|
||||
block_alias = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1201,6 +1265,15 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
|
|||
view_layer = overlap.layer;
|
||||
}
|
||||
}
|
||||
} else if (block_alias) {
|
||||
for (const auto candidate: candidates) {
|
||||
const auto* owner = m_slot_images.try_get(candidate);
|
||||
if (candidate != result && owner != nullptr && owner->registered &&
|
||||
!owner->IsCpuDirty() &&
|
||||
BcAlias::Matches(m_slot_images[result].info, owner->info)) {
|
||||
BcAlias::Resolve(*this, candidate, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result && !m_slot_images[result].depth_id) {
|
||||
|
|
@ -1363,7 +1436,6 @@ vk::ImageView TextureCache::FindDepthTarget(ImageId id, const ImageDesc& desc) {
|
|||
}
|
||||
TouchImage(image);
|
||||
image.MarkGpuModified();
|
||||
image.usage.depth_target = true;
|
||||
if (desc.info.HasStencil()) {
|
||||
const auto layers = image.info.resources.layers;
|
||||
EXIT_IF(layers == 0 || image.info.data.size % layers != 0 ||
|
||||
|
|
@ -1779,7 +1851,7 @@ void TextureCache::InvalidateCpuAliases(uint64_t address, uint64_t size) {
|
|||
continue;
|
||||
}
|
||||
if (owner->Overlaps(address, size)) {
|
||||
owner->InvalidateCpuWrite(address, size);
|
||||
owner->MarkCpuDirty();
|
||||
UntrackImage(id);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -1923,6 +1995,9 @@ void TextureCache::RunGarbageCollector() {
|
|||
}
|
||||
|
||||
void TextureCache::ProcessDownloadImages() {
|
||||
if (!m_readback_linear_images) {
|
||||
return;
|
||||
}
|
||||
std::scoped_lock lock {m_lock};
|
||||
for (const auto id: m_download_images) {
|
||||
const auto owner = m_slot_images.try_get(id);
|
||||
|
|
|
|||
|
|
@ -26,8 +26,15 @@ class BufferCache;
|
|||
class CommandBuffer;
|
||||
class CommandScheduler;
|
||||
class RenderExecutor;
|
||||
class TextureCache;
|
||||
struct TextureCacheTestAccess;
|
||||
|
||||
namespace BcAlias {
|
||||
[[nodiscard]] bool Matches(const ImageInfo& requested, const ImageInfo& cached);
|
||||
[[nodiscard]] bool Publish(TextureCache& cache, Image& image);
|
||||
void Resolve(TextureCache& cache, ImageId cached, ImageId merged);
|
||||
} // namespace BcAlias
|
||||
|
||||
class TextureCache {
|
||||
public:
|
||||
enum class BindingType : uint8_t { Texture, Storage, RenderTarget, DepthTarget, VideoOut };
|
||||
|
|
@ -181,6 +188,9 @@ private:
|
|||
friend struct TextureCacheTestAccess;
|
||||
friend class BufferCache;
|
||||
friend class RenderExecutor;
|
||||
friend bool BcAlias::Matches(const ImageInfo&, const ImageInfo&);
|
||||
friend bool BcAlias::Publish(TextureCache&, Image&);
|
||||
friend void BcAlias::Resolve(TextureCache&, ImageId, ImageId);
|
||||
};
|
||||
|
||||
} // namespace Libs::Graphics
|
||||
|
|
|
|||
|
|
@ -236,7 +236,8 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
|
|||
// DCC owns the clear when both planes are enabled; single-sample CMASK stays expanded.
|
||||
desc.info.metadata.kind = has_dcc ? ImageMetadataKind::Dcc : ImageMetadataKind::Cmask;
|
||||
desc.info.metadata.range = {has_dcc ? rt.dcc_addr.addr : rt.cmask.addr, metadata_size.size};
|
||||
desc.info.metadata.clear_word = rt.clear_word0.word0;
|
||||
desc.info.metadata.clear_word =
|
||||
rt.clear_word0.word0 | (uint64_t {rt.clear_word1.word1} << 32u);
|
||||
desc.info.metadata.clear_register_valid = true;
|
||||
}
|
||||
desc.view_info.format = target_format.format;
|
||||
|
|
|
|||
|
|
@ -394,26 +394,20 @@ static void RcPrint(const char* func, const HW::RenderControl& c) {
|
|||
|
||||
LOGF("\t depth_clear_enable = %s\n"
|
||||
"\t stencil_clear_enable = %s\n"
|
||||
"\t copy_depth_to_color = %s\n"
|
||||
"\t copy_stencil_to_color = %s\n"
|
||||
"\t resummarize_enable = %s\n"
|
||||
"\t stencil_compress_disable = %s\n"
|
||||
"\t depth_compress_disable = %s\n"
|
||||
"\t copy_centroid = %s\n"
|
||||
"\t copy_sample = %" PRIu8 "\n",
|
||||
c.depth_clear_enable ? "true" : "false", c.stencil_clear_enable ? "true" : "false",
|
||||
c.copy_depth_to_color ? "true" : "false", c.copy_stencil_to_color ? "true" : "false",
|
||||
c.resummarize_enable ? "true" : "false", c.stencil_compress_disable ? "true" : "false",
|
||||
c.depth_compress_disable ? "true" : "false", c.copy_centroid ? "true" : "false",
|
||||
c.copy_sample);
|
||||
}
|
||||
|
||||
static void RcCheck(const HW::RenderControl& c) {
|
||||
// EXIT_NOT_IMPLEMENTED(c.depth_clear_enable != false);
|
||||
// EXIT_NOT_IMPLEMENTED(c.stencil_clear_enable != false);
|
||||
// EXIT_NOT_IMPLEMENTED(c.stencil_compress_disable != false);
|
||||
// EXIT_NOT_IMPLEMENTED(c.depth_compress_disable != false);
|
||||
EXIT_NOT_IMPLEMENTED(c.copy_centroid != false);
|
||||
EXIT_NOT_IMPLEMENTED(c.copy_sample != 0);
|
||||
}
|
||||
|
||||
static void McPrint(const char* func, const HW::ModeControl& c) {
|
||||
LOGF("%s\n", func);
|
||||
|
||||
|
|
@ -869,9 +863,6 @@ void hw_check(const CommandBuffer& buffer) {
|
|||
VpCheck(vp, smc);
|
||||
log_phase("clip");
|
||||
ClipCheck(c);
|
||||
log_phase("rc");
|
||||
RcCheck(rc);
|
||||
log_phase("depth");
|
||||
log_phase("mode");
|
||||
McCheck(mc);
|
||||
log_phase("blend");
|
||||
|
|
|
|||
|
|
@ -298,13 +298,20 @@ void RenderExecutor::ResolveRenderDepthTarget(CommandBuffer& buffer, RenderDepth
|
|||
}
|
||||
return;
|
||||
}
|
||||
if (rc.copy_depth_to_color || rc.copy_stencil_to_color || rc.copy_centroid ||
|
||||
rc.copy_sample != 0 || dc.zfunc > static_cast<uint8_t>(vk::CompareOp::eAlways) ||
|
||||
if (rc.copy_stencil_to_color ||
|
||||
dc.zfunc > static_cast<uint8_t>(vk::CompareOp::eAlways) ||
|
||||
(!z.depth_view.depth_write_disable && z.z_write_base_addr != z.z_read_base_addr) ||
|
||||
(has_stencil && !z.depth_view.stencil_write_disable &&
|
||||
z.stencil_write_base_addr != z.stencil_read_base_addr)) {
|
||||
DepthFatal("unsupported depth register state");
|
||||
}
|
||||
if (rc.copy_depth_to_color &&
|
||||
(z.z_info.num_samples != 0 || rc.copy_sample != 0 ||
|
||||
z.z_info.format != Prospero::DepthFormat::kZ32F ||
|
||||
dc.z_enable || dc.z_write_enable || dc.depth_bounds_enable || dc.stencil_enable ||
|
||||
rc.depth_clear_enable || rc.stencil_clear_enable)) {
|
||||
DepthFatal("unsupported depth-to-color copy state");
|
||||
}
|
||||
r.desc = MakeDepthTargetDesc(buffer, z);
|
||||
r.depth_clear_enable = rc.depth_clear_enable;
|
||||
r.depth_load_clear_enable = r.depth_clear_enable;
|
||||
|
|
|
|||
|
|
@ -29,10 +29,8 @@ struct CachedImageView {
|
|||
};
|
||||
|
||||
struct ImageUsage {
|
||||
bool texture = false;
|
||||
bool storage = false;
|
||||
bool render_target = false;
|
||||
bool depth_target = false;
|
||||
bool video_out = false;
|
||||
};
|
||||
|
||||
|
|
@ -70,40 +68,25 @@ public:
|
|||
void CopyImageWithBuffer(Image& source, Buffer& buffer, TileManager& tiler);
|
||||
void CopyMip(Image& source, uint32_t mip, uint32_t layer);
|
||||
|
||||
void InvalidateCpuWrite(uint64_t vaddr, uint64_t size) {
|
||||
if (ImageRangeOverlaps(info.data.address, info.data.size, vaddr, size)) {
|
||||
m_cpu_dirty = true;
|
||||
m_maybe_cpu_dirty = false;
|
||||
m_maybe_hash_valid = false;
|
||||
} else if (ImagePageRangesOverlap(info.data.address, info.data.size, vaddr, size)) {
|
||||
m_maybe_cpu_dirty = true;
|
||||
}
|
||||
void MarkCpuDirty() {
|
||||
m_cpu_dirty = true;
|
||||
m_maybe_cpu_dirty = false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsCpuDirty() const { return m_cpu_dirty || m_maybe_cpu_dirty; }
|
||||
[[nodiscard]] bool IsDefinitelyCpuDirty() const { return m_cpu_dirty; }
|
||||
[[nodiscard]] bool IsMaybeCpuDirty() const { return m_maybe_cpu_dirty; }
|
||||
void MarkMaybeCpuDirty() {
|
||||
if (!m_cpu_dirty) {
|
||||
if (!IsCpuDirty()) {
|
||||
m_maybe_cpu_hash = HashGuestEdges();
|
||||
m_maybe_cpu_dirty = true;
|
||||
}
|
||||
}
|
||||
[[nodiscard]] bool NeedsMaybeCpuHash() const {
|
||||
return m_maybe_cpu_dirty && !m_maybe_hash_valid;
|
||||
}
|
||||
void SetMaybeCpuHash(uint64_t hash) {
|
||||
if (!NeedsMaybeCpuHash()) {
|
||||
EXIT("image cannot initialize maybe-dirty hash\n");
|
||||
}
|
||||
m_maybe_cpu_hash = hash;
|
||||
m_maybe_hash_valid = true;
|
||||
}
|
||||
[[nodiscard]] bool ResolveMaybeCpuHash(uint64_t hash) {
|
||||
if (!m_maybe_cpu_dirty || !m_maybe_hash_valid || m_cpu_dirty) {
|
||||
if (!m_maybe_cpu_dirty || m_cpu_dirty) {
|
||||
EXIT("image cannot resolve maybe-dirty hash\n");
|
||||
}
|
||||
m_maybe_cpu_dirty = false;
|
||||
m_maybe_hash_valid = false;
|
||||
m_maybe_cpu_dirty = false;
|
||||
m_cpu_dirty |= hash != m_maybe_cpu_hash;
|
||||
return m_cpu_dirty;
|
||||
}
|
||||
|
|
@ -112,9 +95,8 @@ public:
|
|||
if (!IsCpuDirty()) {
|
||||
EXIT("clean image cannot complete a refresh\n");
|
||||
}
|
||||
m_cpu_dirty = false;
|
||||
m_maybe_cpu_dirty = false;
|
||||
m_maybe_hash_valid = false;
|
||||
m_cpu_dirty = false;
|
||||
m_maybe_cpu_dirty = false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsGpuModified() const noexcept { return m_gpu_modified; }
|
||||
|
|
@ -149,6 +131,8 @@ public:
|
|||
uint64_t track_addr = 0;
|
||||
uint64_t track_addr_end = 0;
|
||||
ImageId depth_id {};
|
||||
// Stencil associations that refer to this depth image.
|
||||
std::vector<ImageId> stencil_ids;
|
||||
// The current stencil plane's mapping into the depth image, also retained by its association.
|
||||
ImageSubresourceRange stencil_subresources;
|
||||
uint64_t tick_accessed_last = 0;
|
||||
|
|
@ -165,12 +149,11 @@ private:
|
|||
|
||||
GraphicContext& m_graphics;
|
||||
CommandScheduler& m_scheduler;
|
||||
uint64_t m_maybe_cpu_hash = 0;
|
||||
bool m_cpu_dirty = false;
|
||||
bool m_maybe_cpu_dirty = false;
|
||||
bool m_maybe_hash_valid = false;
|
||||
bool m_gpu_modified = false;
|
||||
bool m_buffer_modified = false;
|
||||
uint64_t m_maybe_cpu_hash = 0;
|
||||
bool m_cpu_dirty = false;
|
||||
bool m_maybe_cpu_dirty = false;
|
||||
bool m_gpu_modified = false;
|
||||
bool m_buffer_modified = false;
|
||||
};
|
||||
|
||||
namespace ImageOps {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ struct ImageMetadataInfo {
|
|||
GuestRange range;
|
||||
ImageMetadataKind kind = ImageMetadataKind::None;
|
||||
uint32_t control = 0;
|
||||
uint32_t clear_word = 0;
|
||||
uint64_t clear_word = 0;
|
||||
VideoOutCompression compression = VideoOutCompression::Uncompressed;
|
||||
bool stencil_compressed = false;
|
||||
bool clear_register_valid = false;
|
||||
|
|
@ -362,8 +362,17 @@ struct VideoOutFormatPolicy {
|
|||
};
|
||||
|
||||
inline constexpr uint64_t VIDEO_OUT_PIXEL_FORMAT_R10_G10_B10_A2_BT2100_PQ = 0x8100070422000000ull;
|
||||
inline constexpr uint64_t VIDEO_OUT_PIXEL_FORMAT_B10_G10_R10_A2_BT2100_PQ = 0x8100070400000000ull;
|
||||
|
||||
inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {{
|
||||
[[nodiscard]] constexpr bool IsHdrVideoOutPixelFormat(uint64_t format) {
|
||||
switch (format) {
|
||||
case VIDEO_OUT_PIXEL_FORMAT_R10_G10_B10_A2_BT2100_PQ:
|
||||
case VIDEO_OUT_PIXEL_FORMAT_B10_G10_R10_A2_BT2100_PQ: return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
inline constexpr std::array<VideoOutFormatPolicy, 8> VIDEO_OUT_FORMAT_POLICIES {{
|
||||
{0x8000000022000000ull,
|
||||
{vk::Format::eR8G8B8A8Srgb, Prospero::BufferFormat::k8_8_8_8Srgb, 4, false}},
|
||||
{0x8000000000000000ull,
|
||||
|
|
@ -374,6 +383,8 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
|
|||
{vk::Format::eA2R10G10B10UnormPack32, Prospero::BufferFormat::k10_10_10_2UNorm, 4, false}},
|
||||
{VIDEO_OUT_PIXEL_FORMAT_R10_G10_B10_A2_BT2100_PQ,
|
||||
{vk::Format::eA2B10G10R10UnormPack32, Prospero::BufferFormat::k10_10_10_2UNorm, 4, false}},
|
||||
{VIDEO_OUT_PIXEL_FORMAT_B10_G10_R10_A2_BT2100_PQ,
|
||||
{vk::Format::eA2R10G10B10UnormPack32, Prospero::BufferFormat::k10_10_10_2UNorm, 4, false}},
|
||||
{0xc001000622000000ull,
|
||||
{vk::Format::eR16G16B16A16Sfloat, Prospero::BufferFormat::k16_16_16_16Float, 8, false}},
|
||||
{0xc001000600000000ull,
|
||||
|
|
@ -403,9 +414,10 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
|
|||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline bool DecodePackedColorClear(vk::Format format, uint32_t packed,
|
||||
[[nodiscard]] inline bool DecodePackedColorClear(vk::Format format, uint64_t clear_words,
|
||||
vk::ClearColorValue& clear) {
|
||||
vk::ClearColorValue next {};
|
||||
const auto packed = static_cast<uint32_t>(clear_words);
|
||||
const auto unorm8 = [](uint32_t value) { return static_cast<float>(value & 0xffu) / 255.0f; };
|
||||
const auto srgb8 = [](uint32_t value) {
|
||||
const auto encoded = static_cast<float>(value & 0xffu) / 255.0f;
|
||||
|
|
@ -423,6 +435,15 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
|
|||
(mantissa << 13u));
|
||||
};
|
||||
switch (format) {
|
||||
case vk::Format::eB10G11R11UfloatPack32:
|
||||
next.float32[0] = float16((packed & 0x7ffu) << 4u);
|
||||
next.float32[1] = float16(((packed >> 11u) & 0x7ffu) << 4u);
|
||||
next.float32[2] = float16((packed >> 22u) << 5u);
|
||||
break;
|
||||
case vk::Format::eR16G16B16A16Sfloat:
|
||||
next.float32[2] = float16(static_cast<uint32_t>(clear_words >> 32u));
|
||||
next.float32[3] = float16(static_cast<uint32_t>(clear_words >> 48u));
|
||||
[[fallthrough]];
|
||||
case vk::Format::eR16G16Sfloat:
|
||||
next.float32[1] = float16(packed >> 16u);
|
||||
[[fallthrough]];
|
||||
|
|
@ -433,9 +454,10 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
|
|||
case vk::Format::eR16Unorm:
|
||||
next.float32[0] = static_cast<float>(packed & 0xffffu) / 65535.0f;
|
||||
break;
|
||||
// A single-plane float target carries its clear as raw float bits, the same encoding the
|
||||
// depth decoder below uses. Without this the clear is discarded and the target keeps stale
|
||||
// contents.
|
||||
// Each clear register stores one raw 32-bit float channel.
|
||||
case vk::Format::eR32G32Sfloat:
|
||||
next.float32[1] = std::bit_cast<float>(static_cast<uint32_t>(clear_words >> 32u));
|
||||
[[fallthrough]];
|
||||
case vk::Format::eR32Sfloat: next.float32[0] = std::bit_cast<float>(packed); break;
|
||||
case vk::Format::eR32Uint: next.uint32[0] = packed; break;
|
||||
case vk::Format::eR32Sint: next.int32[0] = static_cast<int32_t>(packed); break;
|
||||
|
|
@ -496,7 +518,7 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
|
|||
clear = vk::ClearColorValue {};
|
||||
return true;
|
||||
}
|
||||
return DecodePackedColorClear(format, packed, clear);
|
||||
return DecodePackedColorClear(format, uint64_t {packed} | (uint64_t {packed} << 32u), clear);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline bool DecodePackedStencilClear(uint32_t packed, uint8_t& clear) {
|
||||
|
|
|
|||
|
|
@ -179,7 +179,9 @@ static void ValidateSampledDepthBinding(const ShaderRecompiler::IR::ImageResourc
|
|||
const ShaderTextureResource& descriptor, const Image& image,
|
||||
vk::Format view_format, uint64_t size) {
|
||||
const bool resource_ok = IsSupportedSampledDepthResource(resource);
|
||||
const bool encoding_ok = IsSupportedDepthTextureEncoding(descriptor, resource.r128);
|
||||
// Comparison sampling changes the host format, not the guest metadata encoding.
|
||||
const bool encoding_ok = descriptor.TileMode() != Prospero::TileMode::kDepth ||
|
||||
IsSupportedDepthTextureEncoding(descriptor, resource.r128);
|
||||
const bool view_ok =
|
||||
IsSupportedSampledDepthView(image.info.pixel_format, view_format, descriptor.DstSelXYZW());
|
||||
if (resource_ok && encoding_ok && view_ok) {
|
||||
|
|
@ -307,8 +309,10 @@ void ValidateStorageTexture(const ShaderRecompiler::IR::ImageResource& resource,
|
|||
const bool encoding_ok = IsSupportedStorageTextureEncoding(resource, descriptor);
|
||||
const bool uint_resource = resource.numeric_class == Prospero::TextureNumericClass::Uint;
|
||||
const bool raw_sint_storage = format == Prospero::BufferFormat::k32SInt && uint_resource &&
|
||||
resource.written && !resource.read && !resource.atomic;
|
||||
const auto numeric_class = Prospero::SampledTextureNumericClass(format);
|
||||
resource.written &&
|
||||
(resource.atomic ? !resource.atomic64 : !resource.read);
|
||||
const auto numeric_class =
|
||||
Prospero::SampledTextureNumericClass(Prospero::RemapTextureFormat(format));
|
||||
const bool raw_float_atomic = format == Prospero::BufferFormat::k32Float && uint_resource &&
|
||||
resource.atomic && !resource.atomic64;
|
||||
const bool format_ok =
|
||||
|
|
@ -564,7 +568,8 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
|
|||
const bool shader_conversion =
|
||||
surface_format.conversion_format != Prospero::BufferFormat::kInvalid;
|
||||
const bool sampled_numeric_class =
|
||||
storage || resource.numeric_class == Prospero::SampledTextureNumericClass(format);
|
||||
storage || resource.numeric_class ==
|
||||
Prospero::SampledTextureNumericClass(Prospero::RemapTextureFormat(format));
|
||||
if (!storage && resource.resource_class == ShaderRecompiler::IR::ImageResourceClass::Sampled &&
|
||||
!sampled_numeric_class) {
|
||||
EXIT("sampled image numeric class mismatch: numeric=%u format=%u addr=0x%016" PRIx64 "\n",
|
||||
|
|
@ -622,8 +627,7 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
|
|||
if (storage) {
|
||||
ValidateStorageTexture(resource, descriptor, desc.info.data.size);
|
||||
}
|
||||
if (!resource.r128 && descriptor.MetaCompress() && tile != Prospero::TileMode::kDepth &&
|
||||
!desc.info.IsDepth()) {
|
||||
if (!resource.r128 && descriptor.MetaCompress() && tile != Prospero::TileMode::kDepth) {
|
||||
TileSizeAlign metadata_size {};
|
||||
(void)TileGetDccSize(width, height, volume ? depth : image_layers,
|
||||
desc.info.bytes_per_block, physical_levels, tile, metadata_size,
|
||||
|
|
@ -872,7 +876,6 @@ void RenderExecutor::RebindImages(PreparedBindings& prepared) {
|
|||
auto& image = texture_cache.GetImage(binding.image_id);
|
||||
const bool storage = binding.desc.type == TextureCache::BindingType::Storage;
|
||||
image.usage.storage |= storage;
|
||||
image.usage.texture |= !storage;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ void DumpShaderOriginal(const char* stage_name, uint64_t shader_hash,
|
|||
}
|
||||
|
||||
bool ValidateShaderSpirv(const char* label, uint64_t shader_hash,
|
||||
const std::vector<uint32_t>& spirv) {
|
||||
const std::vector<uint32_t>& spirv, bool maintenance8_enabled) {
|
||||
if (!Config::ShaderValidationEnabled()) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -160,7 +160,9 @@ bool ValidateShaderSpirv(const char* label, uint64_t shader_hash,
|
|||
static_cast<int>(position.column), static_cast<int>(position.index),
|
||||
message);
|
||||
});
|
||||
if (tools.Validate(spirv)) {
|
||||
spvtools::ValidatorOptions validator_options;
|
||||
validator_options.SetAllowOffsetTextureOperand(maintenance8_enabled);
|
||||
if (tools.Validate(spirv.data(), spirv.size(), validator_options)) {
|
||||
return true;
|
||||
}
|
||||
spvtools::SpirvTools disassembler(SPV_ENV_VULKAN_1_2);
|
||||
|
|
@ -296,14 +298,15 @@ struct PipelineCache::ProgramCache {
|
|||
std::span<const uint32_t> function_code) {
|
||||
auto result = ShaderRecompiler::CompileProgram(std::move(translated), options,
|
||||
specialization, push_data_start_dword);
|
||||
if (!ValidateShaderSpirv(options.dump_label, options.shader_hash, result.spirv)) {
|
||||
if (!ValidateShaderSpirv(options.dump_label, options.shader_hash, result.spirv,
|
||||
options.maintenance8_enabled)) {
|
||||
DumpShaderSpirv(stage_name, options.shader_hash, result.spirv);
|
||||
EXIT("%s failed hash=0x%016" PRIx64 ": SPIR-V validation failed\n", options.dump_label,
|
||||
options.shader_hash);
|
||||
}
|
||||
DumpShaderSpirv(stage_name, options.shader_hash, result.spirv);
|
||||
|
||||
const auto module = CompileSPV(result.spirv, device);
|
||||
const auto module = CompileSPV(result.spirv, graphics.device);
|
||||
EXIT_IF(module == nullptr);
|
||||
if (options.dump_ir) {
|
||||
LOGF("%s SPIR-V words=%" PRIu64 " wave_size=%u\n", options.dump_label,
|
||||
|
|
@ -384,6 +387,7 @@ struct PipelineCache::ProgramCache {
|
|||
}
|
||||
ShaderRecompiler::CompileOptions options;
|
||||
options.stage = stage;
|
||||
options.maintenance8_enabled = graphics.maintenance8_enabled;
|
||||
options.shader_hash = params.hash;
|
||||
options.user_data = user_data;
|
||||
options.back_code = params.back_code;
|
||||
|
|
@ -456,26 +460,26 @@ struct PipelineCache::ProgramCache {
|
|||
return permutation.handle;
|
||||
}
|
||||
|
||||
explicit ProgramCache(vk::Device device): device(device) {
|
||||
explicit ProgramCache(GraphicContext& graphics): graphics(graphics) {
|
||||
lookup_key.static_state.reserve(MaxStaticKeyWords);
|
||||
}
|
||||
~ProgramCache() {
|
||||
for (const auto& [key, entry]: programs) {
|
||||
(void)key;
|
||||
for (const auto& permutation: entry.permutations) {
|
||||
device.destroyShaderModule(permutation.handle.module, nullptr);
|
||||
graphics.device.destroyShaderModule(permutation.handle.module, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::unordered_map<ProgramKey, SourceEntry, ProgramKeyHash> programs;
|
||||
ProgramKey lookup_key;
|
||||
vk::Device device;
|
||||
GraphicContext& graphics;
|
||||
uint64_t next_shader_id = 0;
|
||||
};
|
||||
|
||||
PipelineCache::PipelineCache(GraphicContext& graphics)
|
||||
: m_graphics(graphics), m_program_cache(std::make_unique<ProgramCache>(graphics.device)) {
|
||||
: m_graphics(graphics), m_program_cache(std::make_unique<ProgramCache>(graphics)) {
|
||||
EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread());
|
||||
InitializeDriverCache();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,13 +39,7 @@ enum class CommandBufferDebugOp : uint32_t {
|
|||
DispatchDirect,
|
||||
DrawIndex,
|
||||
DrawIndexAuto,
|
||||
EopWrite,
|
||||
EopInterrupt,
|
||||
EopWriteBack,
|
||||
EopFlip,
|
||||
EopWriteBackFlip,
|
||||
EopOnlyFlip,
|
||||
DispatchIndirect,
|
||||
DispatchIndirect = 9,
|
||||
Unknown,
|
||||
};
|
||||
|
||||
|
|
@ -186,6 +180,8 @@ private:
|
|||
bool ignore_target_mask = false, bool exact_format = false);
|
||||
void ResolveRenderDepthTarget(CommandBuffer& buffer, RenderDepthInfo& target);
|
||||
[[nodiscard]] bool DepthStencilCopy(CommandBuffer& buffer);
|
||||
[[nodiscard]] bool CopyDepthToColor(CommandBuffer& buffer, const DrawCallInfo& draw,
|
||||
const DrawEmitInfo& emit, DrawRenderState& state);
|
||||
[[nodiscard]] bool PrepareDrawRenderState(CommandBuffer& buffer,
|
||||
const DrawCallInfo& draw,
|
||||
uint32_t render_target_slice_offset,
|
||||
|
|
@ -200,11 +196,13 @@ private:
|
|||
std::span<PreparedBindings* const> stages = {});
|
||||
[[nodiscard]] bool ResolveColorTargets(CommandBuffer& buffer,
|
||||
uint32_t render_target_slice_offset);
|
||||
[[nodiscard]] bool ConsumeMetadataColorOperation(CommandBuffer& buffer,
|
||||
uint32_t render_target_slice_offset);
|
||||
void BindImage(ImageId id, bool storage);
|
||||
void BindRenderTarget(ImageId id);
|
||||
void ResetBindings();
|
||||
[[nodiscard]] bool TryConsumeComputeMetaClear(const ShaderComputeInputInfo& input,
|
||||
const CommandBuffer& buffer);
|
||||
void UpdateComputeMetaClears(const ShaderComputeInputInfo& input,
|
||||
const CommandBuffer& buffer);
|
||||
[[nodiscard]] bool TryConsumeComputeImageClear(const ShaderComputeInputInfo& input,
|
||||
CommandBuffer& command, uint32_t group_x,
|
||||
uint32_t group_y, uint32_t group_z, uint32_t mode);
|
||||
|
|
|
|||
|
|
@ -49,8 +49,8 @@ static bool FillSourcesDisjoint(std::span<const ShaderRecompiler::IR::Descriptor
|
|||
return true;
|
||||
}
|
||||
|
||||
bool RenderExecutor::TryConsumeComputeMetaClear(const ShaderComputeInputInfo& input,
|
||||
const CommandBuffer& buffer) {
|
||||
void RenderExecutor::UpdateComputeMetaClears(const ShaderComputeInputInfo& input,
|
||||
const CommandBuffer& buffer) {
|
||||
const auto& program = *input.stage.program;
|
||||
const auto& resources = *input.stage.resources;
|
||||
if (resources.buffers.size() != program.info.buffers.size()) {
|
||||
|
|
@ -60,10 +60,9 @@ bool RenderExecutor::TryConsumeComputeMetaClear(const ShaderComputeInputInfo& in
|
|||
for (uint32_t i = 0; i < program.info.buffers.size(); i++) {
|
||||
const auto& resource = program.info.buffers[i];
|
||||
const auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(resources.buffers[i]);
|
||||
// A metadata resource that is also read is not proven to be a full overwrite. Execute it
|
||||
// conservatively instead of replacing the dispatch with a coarse full-surface clear.
|
||||
// Reads and transforms of metadata do not establish a clear state.
|
||||
if ((!resource.written || resource.read) && cache.IsMeta(descriptor.Base48())) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -73,13 +72,10 @@ bool RenderExecutor::TryConsumeComputeMetaClear(const ShaderComputeInputInfo& in
|
|||
if (resource.written) {
|
||||
const auto descriptor =
|
||||
DecodeNativeDescriptor<ShaderBufferResource>(resources.buffers[i]);
|
||||
if (cache.ClearMeta(descriptor.Base48())) {
|
||||
return true;
|
||||
}
|
||||
(void)cache.ClearMeta(descriptor.Base48());
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t group_x,
|
||||
|
|
@ -306,12 +302,14 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
|
|||
|
||||
const auto& program = *input_info.stage.program;
|
||||
const auto& resources = *input_info.stage.resources;
|
||||
if (resources.specialization_reads.empty() &&
|
||||
(TryConsumeComputeMetaClear(input_info, buffer) ||
|
||||
TryConsumeComputeImageClear(input_info, buffer, thread_group_x, thread_group_y,
|
||||
thread_group_z, mode))) {
|
||||
ResetBindings();
|
||||
return;
|
||||
if (resources.specialization_reads.empty()) {
|
||||
// Cached metadata addresses can be reused as ordinary buffers; preserve their guest writes.
|
||||
UpdateComputeMetaClears(input_info, buffer);
|
||||
if (TryConsumeComputeImageClear(input_info, buffer, thread_group_x, thread_group_y,
|
||||
thread_group_z, mode)) {
|
||||
ResetBindings();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (use_thread_dimensions) {
|
||||
|
|
|
|||
|
|
@ -608,16 +608,19 @@ enum class CbColorMode : uint8_t {
|
|||
DccDecompress = 6,
|
||||
};
|
||||
|
||||
static bool ConsumeMetadataColorOperation(const CommandBuffer& buffer) {
|
||||
bool RenderExecutor::ConsumeMetadataColorOperation(CommandBuffer& buffer,
|
||||
uint32_t render_target_slice_offset) {
|
||||
const auto& ctx = buffer.GetRegisters();
|
||||
const auto mode = ctx.GetColorControl().mode;
|
||||
// These special modes run color-buffer metadata or decompression operations. The shader is a
|
||||
// vehicle for that operation, and its exported color must not be applied as a normal draw.
|
||||
// Kyty stores expanded Vulkan images rather than compressed guest surfaces, so no equivalent
|
||||
// hardware pass is emitted. Tracked DCC clear state is materialized on attachment bind;
|
||||
// future CMask/FMask support can consume its state through the same TextureCache path.
|
||||
return mode == static_cast<uint8_t>(CbColorMode::EliminateFastClear) ||
|
||||
mode == static_cast<uint8_t>(CbColorMode::FmaskDecompress) ||
|
||||
if (mode == static_cast<uint8_t>(CbColorMode::EliminateFastClear)) {
|
||||
// FCE can precede any ordinary draw. Discover the target to materialize its native
|
||||
// clear metadata before the expanded image is sampled.
|
||||
RenderColorInfo target {};
|
||||
ResolveRenderColorTarget(buffer, target, render_target_slice_offset, 0, true);
|
||||
return true;
|
||||
}
|
||||
// Metadata/decompression shaders must not export colors into the expanded image.
|
||||
return mode == static_cast<uint8_t>(CbColorMode::FmaskDecompress) ||
|
||||
mode == static_cast<uint8_t>(CbColorMode::DccDecompress);
|
||||
}
|
||||
|
||||
|
|
@ -1029,12 +1032,71 @@ static void EmitDrawPrimitives(const HW::UserConfig& ucfg, vk::CommandBuffer vk_
|
|||
}
|
||||
}
|
||||
|
||||
bool RenderExecutor::CopyDepthToColor(CommandBuffer& buffer, const DrawCallInfo& draw,
|
||||
const DrawEmitInfo& emit, DrawRenderState& state) {
|
||||
if (!buffer.GetRegisters().GetRenderControl().copy_depth_to_color) {
|
||||
return false;
|
||||
}
|
||||
auto& ucfg = buffer.GetUserConfig();
|
||||
const auto& hw = buffer.GetRegisters();
|
||||
auto& depth = state.depth_info;
|
||||
auto& color = state.color_info[0];
|
||||
EXIT_NOT_IMPLEMENTED(!m_context.GetGraphics().maintenance8_enabled ||
|
||||
!Prospero::IsRectList(ucfg.GetPrimType()) ||
|
||||
draw.IsIndexed() || draw.index_count != 3 || draw.instance_count != 1 ||
|
||||
emit.first_vertex != 0 || emit.first_instance != 0 ||
|
||||
state.color_count != 1 || color.target_slot != 0 || !depth.image_id ||
|
||||
color.desc.info.samples != 1 ||
|
||||
depth.desc.info.extent.width != color.Extent().width ||
|
||||
depth.desc.info.extent.height != color.Extent().height ||
|
||||
depth.desc.view_info.layer_count != 1 ||
|
||||
color.desc.view_info.layer_count != 1 ||
|
||||
hw.GetBlendControl(0).enable || state.ps_input_info.ps_pixel_kill_enable ||
|
||||
state.ps_input_info.ps_depth_export_enable ||
|
||||
state.ps_input_info.ps_sample_mask_export_enable);
|
||||
vk::ImageAspectFlags feedback;
|
||||
const auto rendering = AcquireRenderTargets(buffer, &color, 1, depth, feedback);
|
||||
if (depth.depth_load_clear_enable) {
|
||||
m_context.GetCommandScheduler().BeginRendering(rendering);
|
||||
}
|
||||
auto& cache = m_context.GetTextureCache();
|
||||
auto& source = cache.GetImage(depth.image_id);
|
||||
auto& destination = cache.GetImage(color.image_id);
|
||||
EXIT_NOT_IMPLEMENTED((source.backing.format != vk::Format::eD32Sfloat &&
|
||||
source.backing.format != vk::Format::eD32SfloatS8Uint) ||
|
||||
destination.backing.format != vk::Format::eR32Sfloat);
|
||||
const auto rect = calc_final_scissor(hw.GetScreenViewport(), hw.GetScanModeControl(),
|
||||
{rendering.width, rendering.height}, 0);
|
||||
if (rect.right > rect.left && rect.bottom > rect.top) {
|
||||
const vk::ImageCopy region {
|
||||
{vk::ImageAspectFlagBits::eDepth, depth.desc.view_info.base_level,
|
||||
depth.desc.view_info.base_layer, 1}, {rect.left, rect.top, 0},
|
||||
{vk::ImageAspectFlagBits::eColor, color.desc.view_info.base_level,
|
||||
color.desc.view_info.base_layer, 1}, {rect.left, rect.top, 0},
|
||||
{static_cast<uint32_t>(rect.right - rect.left),
|
||||
static_cast<uint32_t>(rect.bottom - rect.top), 1}};
|
||||
m_context.GetCommandScheduler().EndRendering();
|
||||
const auto command = buffer.Handle();
|
||||
source.Transit(vk::ImageLayout::eTransferSrcOptimal, vk::AccessFlagBits2::eTransferRead,
|
||||
{}, command);
|
||||
destination.Transit(vk::ImageLayout::eTransferDstOptimal,
|
||||
vk::AccessFlagBits2::eTransferWrite, {}, command);
|
||||
command.copyImage(source.backing.image, vk::ImageLayout::eTransferSrcOptimal,
|
||||
destination.backing.image, vk::ImageLayout::eTransferDstOptimal,
|
||||
1, ®ion);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buffer,
|
||||
const DrawCallInfo& draw, DrawRenderState& state,
|
||||
vk::PrimitiveTopology topology, const DrawEmitInfo& emit,
|
||||
const DrawIndexBufferSource& index_source,
|
||||
bool primitive_restart_enable) {
|
||||
auto& ucfg = buffer.GetUserConfig();
|
||||
if (CopyDepthToColor(buffer, draw, emit, state)) {
|
||||
return;
|
||||
}
|
||||
const auto vertex_stages =
|
||||
std::span {state.vertex_info.data(), state.programs.VertexStageCount()};
|
||||
const bool mesh_active = state.vertex_info[0].stage.program->stage == ShaderType::Mesh;
|
||||
|
|
@ -1198,7 +1260,8 @@ void RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
|
|||
return;
|
||||
}
|
||||
|
||||
if (ConsumeMetadataColorOperation(buffer) || DepthStencilCopy(buffer) ||
|
||||
if (ConsumeMetadataColorOperation(buffer, args.render_target_slice_offset) ||
|
||||
DepthStencilCopy(buffer) ||
|
||||
ResolveColorTargets(buffer, args.render_target_slice_offset)) {
|
||||
ResetBindings();
|
||||
return;
|
||||
|
|
@ -1311,7 +1374,8 @@ void RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
|
|||
return;
|
||||
}
|
||||
|
||||
if (ConsumeMetadataColorOperation(buffer) || DepthStencilCopy(buffer) ||
|
||||
if (ConsumeMetadataColorOperation(buffer, args.render_target_slice_offset) ||
|
||||
DepthStencilCopy(buffer) ||
|
||||
ResolveColorTargets(buffer, args.render_target_slice_offset)) {
|
||||
ResetBindings();
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
#include "graphics/host_gpu/renderer/sync.h"
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "common/common.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/threads.h"
|
||||
#include "graphics/host_gpu/graphicContext.h"
|
||||
#include "graphics/host_gpu/renderer/cache/bufferCache.h"
|
||||
#include "graphics/host_gpu/renderer/render.h"
|
||||
#include "graphics/host_gpu/renderer/renderContext.h"
|
||||
|
|
@ -53,109 +49,6 @@ uint64_t ReadReferenceClock() {
|
|||
return value;
|
||||
}
|
||||
|
||||
enum class EndOfPipeWriteSize : uint32_t { Dword = 4, Qword = 8 };
|
||||
enum class EndOfPipeWriteAction { Write, WriteBack, Interrupt, InterruptWriteBack };
|
||||
|
||||
static CommandBufferDebugOp DebugOperation(EndOfPipeWriteAction action) {
|
||||
switch (action) {
|
||||
case EndOfPipeWriteAction::Write: return CommandBufferDebugOp::EopWrite;
|
||||
case EndOfPipeWriteAction::WriteBack:
|
||||
case EndOfPipeWriteAction::InterruptWriteBack: return CommandBufferDebugOp::EopWriteBack;
|
||||
case EndOfPipeWriteAction::Interrupt: return CommandBufferDebugOp::EopInterrupt;
|
||||
}
|
||||
EXIT("unsupported end-of-pipe write action\n");
|
||||
return CommandBufferDebugOp::Unknown;
|
||||
}
|
||||
|
||||
static bool TriggersInterrupt(EndOfPipeWriteAction action) {
|
||||
return action == EndOfPipeWriteAction::Interrupt ||
|
||||
action == EndOfPipeWriteAction::InterruptWriteBack;
|
||||
}
|
||||
|
||||
static void RecordEndOfPipeWrite(uint64_t submit_id, CommandBuffer& buffer, uint64_t destination,
|
||||
uint64_t value, EndOfPipeWriteSize size,
|
||||
EndOfPipeWriteAction action, int interrupt_event_id = 0,
|
||||
uint32_t context_id = 0) {
|
||||
EXIT_IF(destination == 0);
|
||||
(void)buffer.Handle();
|
||||
|
||||
const auto width = static_cast<uint32_t>(size);
|
||||
const auto value_low = static_cast<uint32_t>(value);
|
||||
const auto value_high = static_cast<uint32_t>(value >> 32u);
|
||||
const auto operation = static_cast<uint32_t>(DebugOperation(action));
|
||||
if (TriggersInterrupt(action)) {
|
||||
buffer.SetDebugInfo(operation, submit_id, width, context_id, value_low, value_high,
|
||||
destination);
|
||||
TriggerEopEventAtEndOfPipe(buffer, interrupt_event_id, context_id);
|
||||
} else {
|
||||
buffer.SetDebugInfo(operation, submit_id, width, value_low, value_high, 0, destination);
|
||||
}
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipe32(uint64_t submit_id, CommandBuffer& buffer, uint32_t* dst_gpu_addr,
|
||||
uint32_t value) {
|
||||
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
|
||||
EndOfPipeWriteSize::Dword, EndOfPipeWriteAction::Write);
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipeGds32(uint64_t submit_id, CommandBuffer& buffer, uint32_t* dst_gpu_addr,
|
||||
uint32_t dw_offset, uint32_t dw_num) {
|
||||
EXIT_IF(dst_gpu_addr == nullptr);
|
||||
(void)buffer.Handle();
|
||||
buffer.SetDebugInfo(static_cast<uint32_t>(CommandBufferDebugOp::EopWrite), submit_id,
|
||||
dw_offset, dw_num, 0, 0, reinterpret_cast<uint64_t>(dst_gpu_addr));
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipe64(uint64_t submit_id, CommandBuffer& buffer, uint64_t* dst_gpu_addr,
|
||||
uint64_t value) {
|
||||
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
|
||||
EndOfPipeWriteSize::Qword, EndOfPipeWriteAction::Write);
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipeWithWriteBack64(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint64_t* dst_gpu_addr, uint64_t value) {
|
||||
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
|
||||
EndOfPipeWriteSize::Qword, EndOfPipeWriteAction::WriteBack);
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipeWithWriteBack32(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint32_t* dst_gpu_addr, uint32_t value) {
|
||||
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
|
||||
EndOfPipeWriteSize::Dword, EndOfPipeWriteAction::WriteBack);
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipeWithInterruptWriteBack64(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint64_t* dst_gpu_addr, uint64_t value, int event_id,
|
||||
uint32_t context_id) {
|
||||
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
|
||||
EndOfPipeWriteSize::Qword, EndOfPipeWriteAction::InterruptWriteBack,
|
||||
event_id, context_id);
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipeWithInterruptWriteBack32(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint32_t* dst_gpu_addr, uint32_t value, int event_id,
|
||||
uint32_t context_id) {
|
||||
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
|
||||
EndOfPipeWriteSize::Dword, EndOfPipeWriteAction::InterruptWriteBack,
|
||||
event_id, context_id);
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipeWithInterrupt64(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint64_t* dst_gpu_addr, uint64_t value, int event_id,
|
||||
uint32_t context_id) {
|
||||
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
|
||||
EndOfPipeWriteSize::Qword, EndOfPipeWriteAction::Interrupt, event_id,
|
||||
context_id);
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipeWithInterrupt32(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint32_t* dst_gpu_addr, uint32_t value, int event_id,
|
||||
uint32_t context_id) {
|
||||
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
|
||||
EndOfPipeWriteSize::Dword, EndOfPipeWriteAction::Interrupt, event_id,
|
||||
context_id);
|
||||
}
|
||||
|
||||
uint64_t PrepareVideoOutFlip(CommandBuffer& buffer, int handle, int index, int flip_mode,
|
||||
int64_t flip_arg) {
|
||||
for (;;) {
|
||||
|
|
@ -176,65 +69,6 @@ uint64_t PrepareVideoOutFlip(CommandBuffer& buffer, int handle, int index, int f
|
|||
}
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipeWithInterruptWriteBackFlip32(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint32_t* dst_gpu_addr, uint32_t value,
|
||||
int handle, int index, int flip_mode,
|
||||
int64_t flip_arg, uint64_t request_id,
|
||||
int event_id) {
|
||||
EXIT_IF(dst_gpu_addr == nullptr);
|
||||
(void)buffer.Handle();
|
||||
buffer.SetDebugInfo(static_cast<uint32_t>(CommandBufferDebugOp::EopWriteBackFlip), submit_id,
|
||||
static_cast<uint32_t>(handle), static_cast<uint32_t>(index),
|
||||
static_cast<uint32_t>(flip_mode), value, static_cast<uint64_t>(flip_arg));
|
||||
|
||||
auto& renderer = buffer.GetContext();
|
||||
auto& scheduler = renderer.GetCommandScheduler();
|
||||
EXIT_IF(!scheduler.Active() || &buffer != &scheduler.Current());
|
||||
scheduler.DeferPriorityOperation([&renderer, event_id, request_id] {
|
||||
renderer.GetVideoOut().CompleteFlip(request_id);
|
||||
renderer.TriggerInterrupt(event_id, 0);
|
||||
});
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipeWithFlip32(uint64_t submit_id, CommandBuffer& buffer, uint32_t* dst_gpu_addr,
|
||||
uint32_t value, int handle, int index, int flip_mode,
|
||||
int64_t flip_arg, uint64_t request_id) {
|
||||
EXIT_IF(dst_gpu_addr == nullptr);
|
||||
(void)buffer.Handle();
|
||||
buffer.SetDebugInfo(static_cast<uint32_t>(CommandBufferDebugOp::EopFlip), submit_id,
|
||||
static_cast<uint32_t>(handle), static_cast<uint32_t>(index),
|
||||
static_cast<uint32_t>(flip_mode), value, static_cast<uint64_t>(flip_arg));
|
||||
|
||||
auto& renderer = buffer.GetContext();
|
||||
auto& scheduler = renderer.GetCommandScheduler();
|
||||
EXIT_IF(!scheduler.Active() || &buffer != &scheduler.Current());
|
||||
scheduler.DeferPriorityOperation(
|
||||
[&renderer, request_id] { renderer.GetVideoOut().CompleteFlip(request_id); });
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipeOnlyFlip(uint64_t submit_id, CommandBuffer& buffer, int handle, int index,
|
||||
int flip_mode, int64_t flip_arg, uint64_t request_id) {
|
||||
(void)buffer.Handle();
|
||||
buffer.SetDebugInfo(static_cast<uint32_t>(CommandBufferDebugOp::EopOnlyFlip), submit_id,
|
||||
static_cast<uint32_t>(handle), static_cast<uint32_t>(index),
|
||||
static_cast<uint32_t>(flip_mode), 0, static_cast<uint64_t>(flip_arg));
|
||||
|
||||
auto& renderer = buffer.GetContext();
|
||||
auto& scheduler = renderer.GetCommandScheduler();
|
||||
EXIT_IF(!scheduler.Active() || &buffer != &scheduler.Current());
|
||||
scheduler.DeferPriorityOperation(
|
||||
[&renderer, request_id] { renderer.GetVideoOut().CompleteFlip(request_id); });
|
||||
}
|
||||
|
||||
void TriggerEopEventAtEndOfPipe(CommandBuffer& buffer, int event_id, uint32_t context_id) {
|
||||
(void)buffer.Handle();
|
||||
auto& renderer = buffer.GetContext();
|
||||
auto& scheduler = renderer.GetCommandScheduler();
|
||||
EXIT_IF(!scheduler.Active() || &buffer != &scheduler.Current());
|
||||
scheduler.DeferPriorityOperation(
|
||||
[&renderer, event_id, context_id] { renderer.TriggerInterrupt(event_id, context_id); });
|
||||
}
|
||||
|
||||
static void InterruptEventResetFunc(LibKernel::EventQueue::KernelEqueueEvent* event) {
|
||||
EXIT_IF(event == nullptr);
|
||||
event->triggered = false;
|
||||
|
|
|
|||
|
|
@ -17,44 +17,8 @@ namespace Sync {
|
|||
uint64_t& value);
|
||||
[[nodiscard]] uint64_t ReadReferenceClock();
|
||||
|
||||
void TriggerEopEventAtEndOfPipe(CommandBuffer& buffer, int event_id, uint32_t context_id);
|
||||
|
||||
void WriteAtEndOfPipe32(uint64_t submit_id, CommandBuffer& buffer, uint32_t* dst_gpu_addr,
|
||||
uint32_t value);
|
||||
void WriteAtEndOfPipe64(uint64_t submit_id, CommandBuffer& buffer, uint64_t* dst_gpu_addr,
|
||||
uint64_t value);
|
||||
void WriteAtEndOfPipeGds32(uint64_t submit_id, CommandBuffer& buffer, uint32_t* dst_gpu_addr,
|
||||
uint32_t dw_offset, uint32_t dw_num);
|
||||
void WriteAtEndOfPipeWithWriteBack32(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint32_t* dst_gpu_addr, uint32_t value);
|
||||
void WriteAtEndOfPipeWithWriteBack64(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint64_t* dst_gpu_addr, uint64_t value);
|
||||
void WriteAtEndOfPipeWithInterrupt32(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint32_t* dst_gpu_addr, uint32_t value, int event_id,
|
||||
uint32_t context_id = 0);
|
||||
void WriteAtEndOfPipeWithInterrupt64(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint64_t* dst_gpu_addr, uint64_t value, int event_id,
|
||||
uint32_t context_id = 0);
|
||||
void WriteAtEndOfPipeWithInterruptWriteBack32(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint32_t* dst_gpu_addr, uint32_t value, int event_id,
|
||||
uint32_t context_id = 0);
|
||||
void WriteAtEndOfPipeWithInterruptWriteBack64(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint64_t* dst_gpu_addr, uint64_t value, int event_id,
|
||||
uint32_t context_id = 0);
|
||||
|
||||
[[nodiscard]] uint64_t PrepareVideoOutFlip(CommandBuffer& buffer, int handle, int index,
|
||||
int flip_mode, int64_t flip_arg);
|
||||
void WriteAtEndOfPipeOnlyFlip(uint64_t submit_id, CommandBuffer& buffer, int handle, int index,
|
||||
int flip_mode, int64_t flip_arg, uint64_t request_id);
|
||||
void WriteAtEndOfPipeWithFlip32(uint64_t submit_id, CommandBuffer& buffer, uint32_t* dst_gpu_addr,
|
||||
uint32_t value, int handle, int index, int flip_mode,
|
||||
int64_t flip_arg, uint64_t request_id);
|
||||
void WriteAtEndOfPipeWithInterruptWriteBackFlip32(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint32_t* dst_gpu_addr, uint32_t value,
|
||||
int handle, int index, int flip_mode,
|
||||
int64_t flip_arg, uint64_t request_id,
|
||||
int event_id);
|
||||
|
||||
int AddEqEvent(RenderContext& renderer, LibKernel::EventQueue::KernelEqueue eq, int id,
|
||||
void* udata);
|
||||
int DeleteEqEvent(RenderContext& renderer, LibKernel::EventQueue::KernelEqueue eq, int id);
|
||||
|
|
|
|||
|
|
@ -1,15 +1,53 @@
|
|||
#version 450
|
||||
|
||||
layout(location = 0) in vec2 uv;
|
||||
layout(location = 0) out vec4 color;
|
||||
|
||||
vec3 SdrToHdr10(vec3 rgb, bool source_linear) {
|
||||
rgb = clamp(rgb, 0.0, 1.0);
|
||||
vec3 linear = source_linear ? rgb :
|
||||
mix(rgb / 12.92, pow((rgb + 0.055) / 1.055, vec3(2.4)),
|
||||
greaterThan(rgb, vec3(0.04045)));
|
||||
// Convert primaries and scale 250-nit reference white relative to 10,000 nits.
|
||||
const mat3 to_rec2020 = (250.0 / 10000.0) *
|
||||
mat3(0.627404, 0.069097, 0.016391,
|
||||
0.329283, 0.919540, 0.088013,
|
||||
0.043313, 0.011362, 0.895595);
|
||||
vec3 powered = pow(to_rec2020 * linear, vec3(2610.0 / 16384.0));
|
||||
return pow((3424.0 / 4096.0 + (2413.0 / 128.0) * powered) /
|
||||
(1.0 + (2392.0 / 128.0) * powered), vec3(2523.0 / 32.0));
|
||||
}
|
||||
|
||||
#ifdef IMGUI_HDR
|
||||
layout(set = 0, binding = 0) uniform texture2D image;
|
||||
layout(set = 1, binding = 0) uniform sampler image_sampler;
|
||||
layout(location = 0) in struct { vec4 Color; vec2 UV; } In;
|
||||
|
||||
void main() {
|
||||
vec4 sampled = In.Color * texture(sampler2D(image, image_sampler), In.UV);
|
||||
color = vec4(SdrToHdr10(sampled.rgb, false), sampled.a);
|
||||
}
|
||||
#else
|
||||
layout(location = 0) in vec2 uv;
|
||||
layout(binding = 0) uniform sampler2D source;
|
||||
layout(push_constant) uniform AlphaControl {
|
||||
uint premultiplied;
|
||||
uint flags; // bit 0: premultiplied, bit 1: HDR target, bit 2: PQ source, bit 3: linear source
|
||||
};
|
||||
|
||||
void main() {
|
||||
color = texture(source, uv);
|
||||
if (premultiplied == 0u) {
|
||||
color.rgb *= color.a;
|
||||
const uint HDR_TARGET = 2u;
|
||||
const uint PQ_SOURCE = 4u;
|
||||
vec4 sampled = texture(source, uv);
|
||||
bool premultiplied = (flags & 1u) != 0u;
|
||||
bool convert_to_hdr = (flags & (HDR_TARGET | PQ_SOURCE)) == HDR_TARGET;
|
||||
if (convert_to_hdr && premultiplied) {
|
||||
sampled.rgb = sampled.a > 0.0 ? sampled.rgb / sampled.a : vec3(0.0);
|
||||
}
|
||||
if (convert_to_hdr) {
|
||||
sampled.rgb = SdrToHdr10(sampled.rgb, (flags & 8u) != 0u);
|
||||
}
|
||||
if (!premultiplied || convert_to_hdr) {
|
||||
sampled.rgb *= sampled.a;
|
||||
}
|
||||
color = sampled;
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -28,6 +28,14 @@ vk::Format VulkanFormat(Prospero::BufferFormat guest_format);
|
|||
void RequireVulkanSuccess(vk::Result result, const char* operation);
|
||||
vk::ShaderModule CompileSPV(std::span<const uint32_t> code, vk::Device device);
|
||||
|
||||
[[nodiscard]] constexpr bool IsHdrSwapchainFormat(vk::Format format) {
|
||||
switch (format) {
|
||||
case vk::Format::eA2B10G10R10UnormPack32:
|
||||
case vk::Format::eA2R10G10B10UnormPack32: return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Handle, typename... Args>
|
||||
void SetVulkanObjectNameF(vk::Device device, Handle handle, fmt::format_string<Args...> format,
|
||||
Args&&... args) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,137 @@
|
|||
#include "graphics/presentation/ddsImage.h"
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "common/file.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "imgui_impl_vulkan.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace Libs::Graphics {
|
||||
|
||||
DdsImage::~DdsImage() {
|
||||
if (m_descriptor != VK_NULL_HANDLE) {
|
||||
ImGui_ImplVulkan_RemoveTexture(m_descriptor);
|
||||
}
|
||||
m_graphics.device.destroyImageView(m_view, nullptr);
|
||||
if (m_image.image != nullptr) {
|
||||
m_graphics.DeleteImage(m_image);
|
||||
}
|
||||
if (m_staging != VK_NULL_HANDLE) {
|
||||
vmaDestroyBuffer(m_graphics.allocator, m_staging, m_staging_allocation);
|
||||
}
|
||||
}
|
||||
|
||||
bool DdsImage::Load(const std::filesystem::path& path) {
|
||||
EXIT_IF(m_image.image != nullptr || m_staging != VK_NULL_HANDLE);
|
||||
Common::File file(path, Common::File::Mode::Read);
|
||||
if (file.IsInvalid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::array<uint32_t, 37> header {};
|
||||
uint32_t read = 0;
|
||||
file.Read(header.data(), sizeof(header), &read);
|
||||
const auto width = header[4];
|
||||
const auto height = header[3];
|
||||
if (read != sizeof(header) || header[0] != 0x20534444 || header[1] != 124 || header[19] != 32 ||
|
||||
header[20] != 4 || header[21] != 0x30315844 || header[32] != 98 || header[33] != 3 ||
|
||||
header[34] != 0 || header[35] != 1 || header[6] != 0 || header[7] != 1 || header[28] != 0 ||
|
||||
width == 0 || height == 0 || width > 3840 || height > 2160) {
|
||||
return false;
|
||||
}
|
||||
const uint32_t size = ((width + 3) / 4) * ((height + 3) / 4) * 16;
|
||||
if (file.Size() != sizeof(header) + size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr auto format = vk::Format::eBc7UnormBlock;
|
||||
constexpr auto features = vk::FormatFeatureFlagBits::eSampledImage |
|
||||
vk::FormatFeatureFlagBits::eSampledImageFilterLinear |
|
||||
vk::FormatFeatureFlagBits::eTransferDst;
|
||||
if ((m_graphics.GetFormatProperties(format).optimalTilingFeatures & features) != features) {
|
||||
LOGF("[Splash] host does not support BC7 textures\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
vk::BufferCreateInfo buffer {};
|
||||
buffer.size = size;
|
||||
buffer.usage = vk::BufferUsageFlagBits::eTransferSrc;
|
||||
VmaAllocationCreateInfo allocation {};
|
||||
allocation.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST;
|
||||
allocation.flags =
|
||||
VMA_ALLOCATION_CREATE_MAPPED_BIT | VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
|
||||
VmaAllocationInfo mapped {};
|
||||
if (vmaCreateBuffer(m_graphics.allocator, static_cast<const VkBufferCreateInfo*>(buffer),
|
||||
&allocation, &m_staging, &m_staging_allocation, &mapped) != VK_SUCCESS) {
|
||||
LOGF("[Splash] could not allocate DDS upload buffer\n");
|
||||
return false;
|
||||
}
|
||||
file.Read(mapped.pMappedData, size, &read);
|
||||
if (read != size) {
|
||||
return false;
|
||||
}
|
||||
RequireVulkanSuccess(static_cast<vk::Result>(vmaFlushAllocation(m_graphics.allocator,
|
||||
m_staging_allocation, 0, size)),
|
||||
"flush splash image upload");
|
||||
|
||||
vk::ImageCreateInfo image {};
|
||||
image.imageType = vk::ImageType::e2D;
|
||||
image.extent = {width, height, 1};
|
||||
image.mipLevels = 1;
|
||||
image.arrayLayers = 1;
|
||||
image.format = format;
|
||||
image.tiling = vk::ImageTiling::eOptimal;
|
||||
image.usage = vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled;
|
||||
image.samples = vk::SampleCountFlagBits::e1;
|
||||
if (!m_graphics.CreateImage(image, m_image)) {
|
||||
LOGF("[Splash] could not allocate DDS image\n");
|
||||
return false;
|
||||
}
|
||||
vk::ImageViewCreateInfo view {};
|
||||
view.image = m_image.image;
|
||||
view.viewType = vk::ImageViewType::e2D;
|
||||
view.format = format;
|
||||
view.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1};
|
||||
RequireVulkanSuccess(m_graphics.device.createImageView(&view, nullptr, &m_view),
|
||||
"create splash image view");
|
||||
m_descriptor = ImGui_ImplVulkan_AddTexture(static_cast<VkImageView>(m_view),
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
return true;
|
||||
}
|
||||
|
||||
void DdsImage::Upload(vk::CommandBuffer command) {
|
||||
if (m_image.state.layout == vk::ImageLayout::eShaderReadOnlyOptimal) {
|
||||
return;
|
||||
}
|
||||
vk::ImageMemoryBarrier barrier {};
|
||||
barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite;
|
||||
barrier.oldLayout = vk::ImageLayout::eUndefined;
|
||||
barrier.newLayout = vk::ImageLayout::eTransferDstOptimal;
|
||||
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
barrier.image = m_image.image;
|
||||
barrier.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1};
|
||||
command.pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe,
|
||||
vk::PipelineStageFlagBits::eTransfer, {}, 0, nullptr, 0, nullptr, 1,
|
||||
&barrier);
|
||||
vk::BufferImageCopy copy {};
|
||||
copy.imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
|
||||
copy.imageExtent = m_image.extent;
|
||||
command.copyBufferToImage(m_staging, m_image.image, vk::ImageLayout::eTransferDstOptimal, 1,
|
||||
©);
|
||||
barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
|
||||
barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;
|
||||
barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal;
|
||||
barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
|
||||
command.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
|
||||
vk::PipelineStageFlagBits::eFragmentShader, {}, 0, nullptr, 0, nullptr,
|
||||
1, &barrier);
|
||||
m_image.state.layout = vk::ImageLayout::eShaderReadOnlyOptimal;
|
||||
}
|
||||
|
||||
ImTextureRef DdsImage::Texture() const {
|
||||
return ImTextureRef {static_cast<ImTextureID>(reinterpret_cast<uintptr_t>(m_descriptor))};
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
#ifndef EMULATOR_SRC_GRAPHICS_PRESENTATION_DDSIMAGE_H_
|
||||
#define EMULATOR_SRC_GRAPHICS_PRESENTATION_DDSIMAGE_H_
|
||||
|
||||
#include "graphics/host_gpu/graphicContext.h"
|
||||
#include "imgui.h"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
namespace Libs::Graphics {
|
||||
|
||||
class DdsImage final {
|
||||
public:
|
||||
explicit DdsImage(GraphicContext& graphics): m_graphics(graphics) {}
|
||||
~DdsImage();
|
||||
KYTY_CLASS_NO_COPY(DdsImage);
|
||||
|
||||
bool Load(const std::filesystem::path& path);
|
||||
void Upload(vk::CommandBuffer command);
|
||||
[[nodiscard]] ImTextureRef Texture() const;
|
||||
|
||||
private:
|
||||
GraphicContext& m_graphics;
|
||||
VulkanImage m_image;
|
||||
vk::ImageView m_view = nullptr;
|
||||
VkDescriptorSet m_descriptor = VK_NULL_HANDLE;
|
||||
VkBuffer m_staging = VK_NULL_HANDLE;
|
||||
VmaAllocation m_staging_allocation = nullptr;
|
||||
};
|
||||
|
||||
} // namespace Libs::Graphics
|
||||
|
||||
#endif // EMULATOR_SRC_GRAPHICS_PRESENTATION_DDSIMAGE_H_
|
||||
|
|
@ -26,11 +26,13 @@ public:
|
|||
~Presenter();
|
||||
KYTY_CLASS_NO_COPY(Presenter);
|
||||
|
||||
[[nodiscard]] Frame& PrepareFrame(CommandBuffer& command, const ImageInfo& info);
|
||||
[[nodiscard]] Frame& PrepareFrame(CommandBuffer& command, const ImageInfo& info,
|
||||
bool hdr = false);
|
||||
[[nodiscard]] Frame& PrepareBlankFrame(uint32_t width, uint32_t height, bool opaque,
|
||||
CommandBuffer* producer = nullptr);
|
||||
[[nodiscard]] bool PresentLastFrame();
|
||||
[[nodiscard]] bool IsGuestPaused() const noexcept;
|
||||
[[nodiscard]] bool IsHdrSupported() const noexcept;
|
||||
[[nodiscard]] bool NeedsSystemOverlayRefresh() const noexcept;
|
||||
[[nodiscard]] RenderContext& Renderer() const noexcept;
|
||||
void Present(Frame& frame);
|
||||
|
|
|
|||
|
|
@ -3,9 +3,13 @@
|
|||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "common/emulatorConfig.h"
|
||||
#include "common/file.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/stringUtils.h"
|
||||
#include "gpu_blit_shaders/gpu_imgui_hdr_spv.h"
|
||||
#include "graphics/host_gpu/graphicContext.h"
|
||||
#include "graphics/presentation/ddsImage.h"
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "imgui_impl_vulkan.h"
|
||||
|
|
@ -13,6 +17,7 @@
|
|||
#include "libs/dialog.h"
|
||||
#include "libs/ime.h"
|
||||
#include "libs/imeDialog.h"
|
||||
#include "loader/systemContent.h"
|
||||
#include "stb_image.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
|
@ -23,6 +28,7 @@
|
|||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <fmt/format.h>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
|
@ -37,6 +43,8 @@ namespace Libs::Graphics {
|
|||
|
||||
namespace {
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
namespace CoreIme = Libs::Ime;
|
||||
namespace DialogIme = Libs::Dialog::ImeDialog;
|
||||
namespace SystemDialog = Libs::Dialog::SystemDialog;
|
||||
|
|
@ -194,6 +202,10 @@ struct TrophyNotification {
|
|||
std::mutex g_trophy_notification_mutex;
|
||||
std::deque<TrophyNotification> g_trophy_notifications;
|
||||
std::atomic<bool> g_trophy_notification_active {false};
|
||||
std::array<std::filesystem::path, 2> g_splash_paths;
|
||||
std::atomic<bool> g_splash_active {false};
|
||||
std::atomic<Clock::duration::rep> g_splash_hidden_at {0};
|
||||
bool g_splash_fast = false;
|
||||
|
||||
std::vector<std::string_view> WrapTrophyTitle(std::string_view text, float font_size, float width) {
|
||||
std::vector<std::string_view> lines;
|
||||
|
|
@ -464,6 +476,29 @@ void CheckVulkanResult(VkResult result) {
|
|||
|
||||
} // namespace
|
||||
|
||||
void InitializeSplashScreen(const std::filesystem::path& sce_sys) {
|
||||
const auto language = Config::GetConsoleLanguage();
|
||||
for (size_t i = 0; i < g_splash_paths.size(); ++i) {
|
||||
auto& path = g_splash_paths[i];
|
||||
path = sce_sys / fmt::format("pic{}_{:02}.dds", i + 1, language);
|
||||
if (!Common::File::IsFileExisting(path)) {
|
||||
path = sce_sys / fmt::format("pic{}.dds", i + 1);
|
||||
}
|
||||
}
|
||||
int32_t attribute = 0;
|
||||
Loader::SystemContentParamSfoGetInt("ATTRIBUTE3", &attribute);
|
||||
g_splash_fast = (attribute & 0x1000) != 0;
|
||||
g_splash_hidden_at.store(0, std::memory_order_release);
|
||||
g_splash_active.store(Common::File::IsFileExisting(g_splash_paths[0]) &&
|
||||
Common::File::IsFileExisting(g_splash_paths[1]),
|
||||
std::memory_order_release);
|
||||
}
|
||||
|
||||
void HideSplashScreen() {
|
||||
Clock::duration::rep expected = 0;
|
||||
g_splash_hidden_at.compare_exchange_strong(expected, Clock::now().time_since_epoch().count());
|
||||
}
|
||||
|
||||
void InitializeSystemOverlayInput(SDL_Window* window) {
|
||||
EXIT_IF(window == nullptr);
|
||||
const Uint32 type = SDL_RegisterEvents(1);
|
||||
|
|
@ -481,6 +516,7 @@ void InitializeSystemOverlayInput(SDL_Window* window) {
|
|||
}
|
||||
|
||||
void ShutdownSystemOverlayInput() {
|
||||
g_splash_active.store(false, std::memory_order_release);
|
||||
CoreIme::SetVisibilityCallback(nullptr);
|
||||
DialogIme::SetVisibilityCallback(nullptr);
|
||||
SystemDialog::SetVisibilityCallback(nullptr);
|
||||
|
|
@ -515,8 +551,10 @@ SystemOverlayVisualState GetSystemOverlayVisualState() noexcept {
|
|||
const auto dialog = DialogIme::GetVisualState();
|
||||
const auto system = SystemDialog::GetVisualState();
|
||||
const bool trophy_active = g_trophy_notification_active.load(std::memory_order_acquire);
|
||||
return {core.active || dialog.active || system.active || trophy_active,
|
||||
core.revision + dialog.revision + system.revision + (trophy_active ? 1u : 0u)};
|
||||
const bool splash = g_splash_active.load(std::memory_order_acquire);
|
||||
return {core.active || dialog.active || system.active || trophy_active || splash,
|
||||
core.revision + dialog.revision + system.revision + (trophy_active ? 1u : 0u) +
|
||||
(splash ? 2u : 0u)};
|
||||
}
|
||||
|
||||
bool ProcessSystemOverlayInput(const SDL_Event& event) {
|
||||
|
|
@ -656,8 +694,6 @@ void NotifyTrophyUnlocked(std::string_view name, int32_t grade,
|
|||
}
|
||||
|
||||
struct SystemOverlay::Impl {
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
explicit Impl(GraphicContext& context): graphics(context) {}
|
||||
|
||||
~Impl() {
|
||||
|
|
@ -770,6 +806,65 @@ struct SystemOverlay::Impl {
|
|||
return texture;
|
||||
}
|
||||
|
||||
void FinishSplash() {
|
||||
g_splash_active.store(false, std::memory_order_release);
|
||||
if (splash_dds[0] || splash_dds[1]) {
|
||||
Common::LockGuard lock(graphics.queue_mutex);
|
||||
RequireVulkanSuccess(graphics.queue.waitIdle(), "retire splash images");
|
||||
splash_dds = {};
|
||||
}
|
||||
}
|
||||
|
||||
void DrawSplash(vk::Extent2D frame_extent) {
|
||||
if (!g_splash_active.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < g_splash_paths.size(); ++i) {
|
||||
if (splash_dds[i]) {
|
||||
continue;
|
||||
}
|
||||
const auto& path = g_splash_paths[i];
|
||||
auto image = std::make_unique<DdsImage>(graphics);
|
||||
if (!image->Load(path)) {
|
||||
LOGF("[Splash] could not load %s\n", path.string().c_str());
|
||||
FinishSplash();
|
||||
return;
|
||||
}
|
||||
splash_dds[i] = std::move(image);
|
||||
}
|
||||
const auto now = Clock::now();
|
||||
if (splash_born == Clock::time_point {}) {
|
||||
splash_born = now;
|
||||
}
|
||||
const auto hidden = g_splash_hidden_at.load(std::memory_order_acquire);
|
||||
const float fade =
|
||||
hidden == 0
|
||||
? 0.0f
|
||||
: std::chrono::duration<float>(now.time_since_epoch() - Clock::duration {hidden})
|
||||
.count() /
|
||||
0.3f;
|
||||
const float alpha = 1.0f - std::clamp(fade, 0.0f, 1.0f);
|
||||
if (alpha == 0.0f) {
|
||||
FinishSplash();
|
||||
return;
|
||||
}
|
||||
const float age = std::chrono::duration<float>(now - splash_born).count();
|
||||
const float logo = g_splash_fast ? 1.0f : std::clamp((age - 1.4f) / 2.2f, 0.0f, 1.0f);
|
||||
const ImVec2 screen {static_cast<float>(frame_extent.width),
|
||||
static_cast<float>(frame_extent.height)};
|
||||
const float fit = std::min(screen.x / 3840.0f, screen.y / 2160.0f);
|
||||
const ImVec2 size {3840.0f * fit, 2160.0f * fit};
|
||||
const ImVec2 begin {(screen.x - size.x) * 0.5f, (screen.y - size.y) * 0.5f};
|
||||
const ImVec2 end {begin.x + size.x, begin.y + size.y};
|
||||
auto* draw = ImGui::GetBackgroundDrawList();
|
||||
draw->AddRectFilled({0, 0}, screen, IM_COL32(0, 0, 0, static_cast<int>(255.0f * alpha)));
|
||||
for (size_t i = 0; i < g_splash_paths.size(); ++i) {
|
||||
draw->AddImage(
|
||||
splash_dds[i]->Texture(), begin, end, {0, 0}, {1, 1},
|
||||
IM_COL32(255, 255, 255, static_cast<int>(255.0f * alpha * (i == 0 ? 1.0f : logo))));
|
||||
}
|
||||
}
|
||||
|
||||
void EnsureVulkan(vk::Format format, uint32_t image_count) {
|
||||
EnsureContext();
|
||||
if (vulkan_initialized) {
|
||||
|
|
@ -796,6 +891,11 @@ struct SystemOverlay::Impl {
|
|||
info.PipelineInfoMain.PipelineRenderingCreateInfo.colorAttachmentCount = 1;
|
||||
info.PipelineInfoMain.PipelineRenderingCreateInfo.pColorAttachmentFormats = &color_format;
|
||||
info.CheckVkResultFn = CheckVulkanResult;
|
||||
if (IsHdrSwapchainFormat(format)) {
|
||||
info.CustomShaderFragCreateInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
|
||||
info.CustomShaderFragCreateInfo.codeSize = sizeof(GPU_IMGUI_HDR_SPV);
|
||||
info.CustomShaderFragCreateInfo.pCode = GPU_IMGUI_HDR_SPV;
|
||||
}
|
||||
EXIT_IF(!ImGui_ImplVulkan_Init(&info));
|
||||
vulkan_initialized = true;
|
||||
}
|
||||
|
|
@ -1215,7 +1315,8 @@ struct SystemOverlay::Impl {
|
|||
OverlaySnapshot snapshot;
|
||||
const bool has_overlay = GetOverlaySnapshot(&snapshot);
|
||||
const auto now = Clock::now();
|
||||
if (!has_overlay && !g_trophy_notification_active.load(std::memory_order_acquire)) {
|
||||
if (!has_overlay && !g_trophy_notification_active.load(std::memory_order_acquire) &&
|
||||
!g_splash_active.load(std::memory_order_acquire)) {
|
||||
return false;
|
||||
}
|
||||
const auto prepared_session = snapshot.session;
|
||||
|
|
@ -1245,12 +1346,9 @@ struct SystemOverlay::Impl {
|
|||
last_frame = now;
|
||||
ImGui_ImplVulkan_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
DrawSplash(frame_extent);
|
||||
GetOverlaySnapshot(&snapshot);
|
||||
if (snapshot.session != prepared_session) {
|
||||
ImGui::EndFrame();
|
||||
return false;
|
||||
}
|
||||
if (has_overlay) {
|
||||
if (has_overlay && snapshot.session == prepared_session) {
|
||||
if (snapshot.session.kind == OverlayKind::Dialog) {
|
||||
DrawDialog(snapshot.dialog, frame_extent);
|
||||
} else {
|
||||
|
|
@ -1264,6 +1362,11 @@ struct SystemOverlay::Impl {
|
|||
}
|
||||
|
||||
void Record(vk::CommandBuffer command, vk::ImageView target) {
|
||||
for (const auto& image: splash_dds) {
|
||||
if (image) {
|
||||
image->Upload(command);
|
||||
}
|
||||
}
|
||||
vk::RenderingAttachmentInfo color {};
|
||||
color.sType = vk::StructureType::eRenderingAttachmentInfo;
|
||||
color.imageView = target;
|
||||
|
|
@ -1290,6 +1393,7 @@ struct SystemOverlay::Impl {
|
|||
return;
|
||||
}
|
||||
ImGui::SetCurrentContext(imgui_context);
|
||||
splash_dds = {};
|
||||
ImGui_ImplVulkan_Shutdown();
|
||||
vulkan_initialized = false;
|
||||
}
|
||||
|
|
@ -1309,6 +1413,8 @@ struct SystemOverlay::Impl {
|
|||
std::optional<TrophyNotification> trophy_notification;
|
||||
Clock::time_point trophy_notification_born;
|
||||
std::vector<std::unique_ptr<ImTextureData>> retired_trophy_images;
|
||||
std::array<std::unique_ptr<DdsImage>, 2> splash_dds;
|
||||
Clock::time_point splash_born;
|
||||
SDL_AudioStream* trophy_sound_stream = nullptr;
|
||||
uint8_t* trophy_sound_data = nullptr;
|
||||
uint32_t trophy_sound_size = 0;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include "graphics/host_gpu/vulkanCommon.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string_view>
|
||||
|
|
@ -21,6 +22,8 @@ struct SystemOverlayVisualState {
|
|||
uint64_t revision;
|
||||
};
|
||||
|
||||
void InitializeSplashScreen(const std::filesystem::path& sce_sys);
|
||||
void HideSplashScreen();
|
||||
void InitializeSystemOverlayInput(SDL_Window* window);
|
||||
void ShutdownSystemOverlayInput();
|
||||
bool ProcessSystemOverlayInput(const SDL_Event& event);
|
||||
|
|
|
|||
|
|
@ -272,6 +272,7 @@ public:
|
|||
void Init(uint32_t width, uint32_t height);
|
||||
FlipQueue& GetFlipQueue() { return m_flip_queue; }
|
||||
Graphics::RenderContext& Renderer() const noexcept { return m_renderer; }
|
||||
bool IsHdrSupported() const noexcept { return m_presenter.IsHdrSupported(); }
|
||||
|
||||
void VblankBegin();
|
||||
void VblankEnd();
|
||||
|
|
@ -558,14 +559,6 @@ Graphics::ImageInfo BufferAttributeGroup::ImageInfo(const VideoOutBuffer& buffer
|
|||
if (!Graphics::DecodeVideoOutPixelFormat(attribute.pixel_format, pixel_format)) {
|
||||
EXIT("unsupported video-out pixel format: 0x%016" PRIx64 "\n", attribute.pixel_format);
|
||||
}
|
||||
if (attribute.pixel_format == Graphics::VIDEO_OUT_PIXEL_FORMAT_R10_G10_B10_A2_BT2100_PQ) {
|
||||
static std::atomic_flag warned = ATOMIC_FLAG_INIT;
|
||||
if (!warned.test_and_set(std::memory_order_relaxed)) {
|
||||
Log::WriteToConsoleAndLog(fmt::format(
|
||||
"Warning: HDR-to-SDR conversion is not implemented; displaying PQ video-out "
|
||||
"pixels unchanged (format=0x{:016x}).\n", attribute.pixel_format));
|
||||
}
|
||||
}
|
||||
const auto tile_mode = Graphics::Prospero::TileMode::kRenderTarget;
|
||||
Graphics::ImageInfo info {};
|
||||
info.data = {buffer.data_address, 0};
|
||||
|
|
@ -1071,31 +1064,31 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) {
|
|||
uint32_t height = 0;
|
||||
bool current = false;
|
||||
bool premultiplied_alpha = false;
|
||||
bool hdr = false;
|
||||
{
|
||||
Common::LockGuard lock(cfg->mutex);
|
||||
current = cfg->opened && !cfg->closing && cfg->generation == generation;
|
||||
if (current) {
|
||||
if (special) {
|
||||
width = cfg->width;
|
||||
height = cfg->height;
|
||||
} else {
|
||||
const auto& surface = cfg->buffers[index];
|
||||
if (!surface.Occupied()) {
|
||||
EXIT("cannot prepare flip from an unregistered surface, id=%" PRIu64
|
||||
" index=%d\n",
|
||||
request_id, index);
|
||||
}
|
||||
if (surface.group_index >= VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX ||
|
||||
!cfg->groups[surface.group_index].occupied) {
|
||||
EXIT("video-out surface references an unavailable attribute group, id=%" PRIu64
|
||||
" index=%d group=%d\n",
|
||||
request_id, index, surface.group_index);
|
||||
}
|
||||
source_info = cfg->groups[surface.group_index].ImageInfo(surface);
|
||||
premultiplied_alpha =
|
||||
(cfg->groups[surface.group_index].attribute.option &
|
||||
VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_ALPHA_PREMULTIPLIED) != 0;
|
||||
if (current && special) {
|
||||
width = cfg->width;
|
||||
height = cfg->height;
|
||||
} else if (current) {
|
||||
const auto& surface = cfg->buffers[index];
|
||||
if (!surface.Occupied()) {
|
||||
EXIT("cannot prepare flip from an unregistered surface, id=%" PRIu64
|
||||
" index=%d\n",
|
||||
request_id, index);
|
||||
}
|
||||
if (surface.group_index >= VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX ||
|
||||
!cfg->groups[surface.group_index].occupied) {
|
||||
EXIT("video-out surface references an unavailable attribute group, id=%" PRIu64
|
||||
" index=%d group=%d\n",
|
||||
request_id, index, surface.group_index);
|
||||
}
|
||||
const auto& group = cfg->groups[surface.group_index];
|
||||
source_info = group.ImageInfo(surface);
|
||||
hdr = Graphics::IsHdrVideoOutPixelFormat(group.attribute.pixel_format);
|
||||
premultiplied_alpha = (group.attribute.option &
|
||||
VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_ALPHA_PREMULTIPLIED) != 0;
|
||||
}
|
||||
}
|
||||
if (!current) {
|
||||
|
|
@ -1108,12 +1101,18 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) {
|
|||
}
|
||||
return;
|
||||
}
|
||||
static std::atomic_flag warned = ATOMIC_FLAG_INIT;
|
||||
if (hdr && !m_presenter.IsHdrSupported() && !warned.test_and_set(std::memory_order_relaxed)) {
|
||||
Log::WriteToConsoleAndLog(
|
||||
"Warning: HDR output is unavailable; HDR-to-SDR conversion is not implemented, "
|
||||
"displaying PQ video-out pixels unchanged.\n");
|
||||
}
|
||||
Graphics::Presenter::Frame* frame = nullptr;
|
||||
if (special) {
|
||||
frame = &m_presenter.PrepareBlankFrame(width, height, index == VIDEO_OUT_BUFFER_INDEX_BLACK,
|
||||
&buffer);
|
||||
} else {
|
||||
frame = &m_presenter.PrepareFrame(buffer, source_info);
|
||||
frame = &m_presenter.PrepareFrame(buffer, source_info, hdr);
|
||||
}
|
||||
|
||||
Common::LockGuard lock(m_mutex);
|
||||
|
|
@ -1790,11 +1789,14 @@ KYTY_SYSV_ABI int VideoOutGetOutputStatus(int handle, VideoOutOutputStatus* stat
|
|||
|
||||
int32_t attribute3 = 0;
|
||||
Loader::SystemContentParamSfoGetInt("ATTRIBUTE3", &attribute3);
|
||||
int32_t attribute = 0;
|
||||
const bool hdr_allowed = !Loader::SystemContentParamSfoGetInt("ATTRIBUTE", &attribute) ||
|
||||
(attribute & 0x20000000) != 0;
|
||||
ctx->mutex.Lock();
|
||||
// Primary output reports 4K unless param.json Video-out Info enables resolution detection.
|
||||
status->resolution =
|
||||
((attribute3 & 4) != 0 && ctx->width < 3840 && ctx->height < 2160 ? 1u : 2u);
|
||||
status->dynamicRange = 1;
|
||||
status->dynamicRange = hdr_allowed && DriverState().IsHdrSupported() ? 2u : 1u;
|
||||
status->refreshRate =
|
||||
(ctx->output_mode == VIDEO_OUT_OUTPUT_MODE_119_88HZ || Config::GetVblankFrequency() >= 119
|
||||
? VIDEO_OUT_REFRESH_RATE_119_88HZ
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
|
@ -31,6 +32,8 @@ struct Presenter::Frame {
|
|||
vk::ImageView view = nullptr;
|
||||
uint64_t present_tick = 0;
|
||||
bool busy = false;
|
||||
bool hdr = false;
|
||||
bool blank = false;
|
||||
|
||||
void Configure(GraphicContext& graphics, vk::Extent2D extent, vk::Format format);
|
||||
void Transit(vk::CommandBuffer command, vk::ImageLayout layout, vk::AccessFlags2 access);
|
||||
|
|
@ -263,7 +266,7 @@ public:
|
|||
void Create();
|
||||
void Recreate(bool surface_lost = false);
|
||||
[[nodiscard]] bool NeedsResize() const;
|
||||
[[nodiscard]] Status AcquireNextImage();
|
||||
[[nodiscard]] Status AcquireNextImage(CommandScheduler& scheduler);
|
||||
[[nodiscard]] bool PrepareSystemOverlay();
|
||||
void RecordPresentCommands(CommandBuffer& command, Presenter::Frame* source,
|
||||
const Presenter::Layer& overlay, bool draw_system_overlay);
|
||||
|
|
@ -274,20 +277,31 @@ public:
|
|||
return static_cast<uint32_t>(m_images.size());
|
||||
}
|
||||
[[nodiscard]] vk::Format Format() const noexcept { return m_format; }
|
||||
[[nodiscard]] bool IsHdrSupported() const noexcept { return m_hdr_supported.load(); }
|
||||
bool SetHdr(bool hdr) {
|
||||
const bool changed = m_hdr_active != (hdr && IsHdrSupported());
|
||||
m_hdr_requested = hdr;
|
||||
return changed;
|
||||
}
|
||||
|
||||
private:
|
||||
vk::SurfaceFormatKHR SelectFormat();
|
||||
void Destroy();
|
||||
void DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& layer);
|
||||
|
||||
WindowContext& m_window;
|
||||
vk::SwapchainKHR m_handle = nullptr;
|
||||
vk::Format m_format = vk::Format::eUndefined;
|
||||
std::atomic_bool m_hdr_supported {false};
|
||||
bool m_hdr_requested = false;
|
||||
bool m_hdr_active = false;
|
||||
vk::Extent2D m_extent {};
|
||||
// Drawable pixel size observed when this swapchain was created.
|
||||
vk::Extent2D m_window_extent {};
|
||||
std::vector<vk::Image> m_images;
|
||||
std::vector<vk::ImageView> m_image_views;
|
||||
std::vector<vk::Semaphore> m_image_acquired;
|
||||
std::vector<uint64_t> m_image_acquired_ticks;
|
||||
std::vector<vk::Semaphore> m_render_complete;
|
||||
std::unique_ptr<SystemOverlay> m_system_overlay;
|
||||
vk::DescriptorSetLayout m_overlay_descriptors = nullptr;
|
||||
|
|
@ -346,6 +360,40 @@ struct Presenter::Impl {
|
|||
std::atomic<uint64_t> presented_overlay_revision {0};
|
||||
};
|
||||
|
||||
vk::SurfaceFormatKHR Swapchain::SelectFormat() {
|
||||
const auto& formats = m_window.surface_capabilities.formats;
|
||||
auto& graphics = m_window.graphic_ctx;
|
||||
const auto hdr_format = std::ranges::find_if(formats, [&](const auto& candidate) {
|
||||
return candidate.colorSpace == vk::ColorSpaceKHR::eHdr10St2084EXT &&
|
||||
IsHdrSwapchainFormat(candidate.format) &&
|
||||
static_cast<bool>(
|
||||
graphics.GetFormatProperties(candidate.format).optimalTilingFeatures &
|
||||
vk::FormatFeatureFlagBits::eBlitDst);
|
||||
});
|
||||
m_hdr_supported = Config::HdrEnabled() && hdr_format != formats.end();
|
||||
m_hdr_active = m_hdr_requested && IsHdrSupported();
|
||||
if (m_hdr_active) {
|
||||
return *hdr_format;
|
||||
}
|
||||
if (Config::HdrEnabled() && !IsHdrSupported()) {
|
||||
LOGF("HDR10 surface unavailable; using SDR output\n");
|
||||
}
|
||||
if (formats.size() == 1 && formats.front().format == vk::Format::eUndefined) {
|
||||
return {vk::Format::eR8G8B8A8Unorm, vk::ColorSpaceKHR::eSrgbNonlinear};
|
||||
}
|
||||
for (const auto& candidate: formats) {
|
||||
if (candidate.colorSpace != vk::ColorSpaceKHR::eSrgbNonlinear) {
|
||||
continue;
|
||||
}
|
||||
switch (candidate.format) {
|
||||
case vk::Format::eB8G8R8A8Unorm:
|
||||
case vk::Format::eR8G8B8A8Unorm: return candidate;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
EXIT("no supported UNORM swapchain format\n");
|
||||
}
|
||||
|
||||
void Swapchain::Create() {
|
||||
auto& graphics = m_window.graphic_ctx;
|
||||
EXIT_IF(graphics.device == nullptr);
|
||||
|
|
@ -385,18 +433,7 @@ void Swapchain::Create() {
|
|||
? vk::CompositeAlphaFlagBitsKHR::eOpaque
|
||||
: vk::CompositeAlphaFlagBitsKHR::eInherit;
|
||||
|
||||
vk::SurfaceFormatKHR format {vk::Format::eR8G8B8A8Unorm, vk::ColorSpaceKHR::eSrgbNonlinear};
|
||||
if (surface.formats.size() != 1 || surface.formats.front().format != vk::Format::eUndefined) {
|
||||
const auto it = std::find_if(surface.formats.begin(), surface.formats.end(),
|
||||
[](const vk::SurfaceFormatKHR& candidate) {
|
||||
return candidate.format == vk::Format::eB8G8R8A8Unorm ||
|
||||
candidate.format == vk::Format::eR8G8B8A8Unorm;
|
||||
});
|
||||
if (it == surface.formats.end()) {
|
||||
EXIT("no supported UNORM swapchain format\n");
|
||||
}
|
||||
format = *it;
|
||||
}
|
||||
const auto format = SelectFormat();
|
||||
m_format = format.format;
|
||||
const auto swapchain_features = graphics.GetFormatProperties(m_format).optimalTilingFeatures;
|
||||
if (!static_cast<bool>(swapchain_features & vk::FormatFeatureFlagBits::eBlitDst)) {
|
||||
|
|
@ -436,6 +473,7 @@ void Swapchain::Create() {
|
|||
RequireVulkanSuccess(graphics.device.createSwapchainKHR(&create_info, nullptr, &m_handle),
|
||||
"vkCreateSwapchainKHR");
|
||||
EXIT_IF(m_handle == nullptr);
|
||||
LOGF("Vulkan presentation: %s\n", m_hdr_active ? "HDR10 (PQ)" : "SDR");
|
||||
|
||||
m_images = EnumerateVulkan<vk::Image>(
|
||||
"vkGetSwapchainImagesKHR", [&](uint32_t* count, vk::Image* images) {
|
||||
|
|
@ -464,6 +502,7 @@ void Swapchain::Create() {
|
|||
vk::SemaphoreCreateInfo semaphore_info {};
|
||||
semaphore_info.sType = vk::StructureType::eSemaphoreCreateInfo;
|
||||
m_image_acquired.resize(m_images.size());
|
||||
m_image_acquired_ticks.assign(m_images.size(), 0);
|
||||
m_render_complete.resize(m_images.size());
|
||||
for (size_t i = 0; i < m_images.size(); i++) {
|
||||
RequireVulkanSuccess(
|
||||
|
|
@ -532,6 +571,7 @@ void Swapchain::Destroy() {
|
|||
m_images.clear();
|
||||
m_image_views.clear();
|
||||
m_image_acquired.clear();
|
||||
m_image_acquired_ticks.clear();
|
||||
m_render_complete.clear();
|
||||
}
|
||||
|
||||
|
|
@ -557,8 +597,11 @@ bool Swapchain::NeedsResize() const {
|
|||
m_window_extent.height != m_window.graphic_ctx.screen_height;
|
||||
}
|
||||
|
||||
Swapchain::Status Swapchain::AcquireNextImage() {
|
||||
Swapchain::Status Swapchain::AcquireNextImage(CommandScheduler& scheduler) {
|
||||
EXIT_IF(m_handle == nullptr || m_frame_index >= m_image_acquired.size());
|
||||
// Repeated frames can wrap the semaphore ring without acquiring a new source frame.
|
||||
// Wait for the submission that consumed this slot before signaling it again.
|
||||
scheduler.Wait(m_image_acquired_ticks[m_frame_index]);
|
||||
m_image_index = static_cast<uint32_t>(-1);
|
||||
const auto result = m_window.graphic_ctx.device.acquireNextImageKHR(
|
||||
m_handle, std::numeric_limits<uint64_t>::max(), m_image_acquired[m_frame_index], nullptr,
|
||||
|
|
@ -692,9 +735,11 @@ void Swapchain::DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& l
|
|||
write.descriptorType = vk::DescriptorType::eCombinedImageSampler;
|
||||
write.pImageInfo = ℑ
|
||||
command.pushDescriptorSetKHR(vk::PipelineBindPoint::eGraphics, m_overlay_layout, 0, 1, &write);
|
||||
const uint32_t premultiplied = layer.premultiplied_alpha;
|
||||
const uint32_t flags = (layer.premultiplied_alpha ? 1u : 0u) |
|
||||
(m_hdr_active ? 2u : 0u) | (frame.hdr ? 4u : 0u) |
|
||||
(frame.image.format == vk::Format::eR16G16B16A16Sfloat ? 8u : 0u);
|
||||
command.pushConstants(m_overlay_layout, vk::ShaderStageFlagBits::eFragment, 0,
|
||||
sizeof(premultiplied), &premultiplied);
|
||||
sizeof(flags), &flags);
|
||||
command.bindPipeline(vk::PipelineBindPoint::eGraphics, m_overlay_pipeline);
|
||||
vk::RenderingAttachmentInfo attachment {};
|
||||
attachment.imageView = m_image_views[m_image_index];
|
||||
|
|
@ -828,7 +873,9 @@ uint64_t Swapchain::Submit(CommandScheduler& scheduler) {
|
|||
SubmitInfo submit;
|
||||
submit.AddWait(m_image_acquired[m_frame_index], 1, vk::PipelineStageFlagBits::eTransfer);
|
||||
submit.AddSignal(m_render_complete[m_image_index]);
|
||||
return scheduler.Submit(submit);
|
||||
const auto tick = scheduler.Submit(submit);
|
||||
m_image_acquired_ticks[m_frame_index] = tick;
|
||||
return tick;
|
||||
}
|
||||
|
||||
Swapchain::Status Swapchain::Present() {
|
||||
|
|
@ -868,7 +915,7 @@ Presenter::Presenter(WindowContext& window): m_impl(std::make_unique<Impl>(windo
|
|||
|
||||
Presenter::~Presenter() = default;
|
||||
|
||||
Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo& info) {
|
||||
Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo& info, bool hdr) {
|
||||
KYTY_PROFILER_FUNCTION();
|
||||
EXIT_IF(buffer.IsInvalid());
|
||||
auto frame_format = info.pixel_format;
|
||||
|
|
@ -878,6 +925,8 @@ Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo
|
|||
default: break;
|
||||
}
|
||||
auto* frame = m_impl->frames.Acquire({info.extent.width, info.extent.height}, frame_format);
|
||||
frame->hdr = hdr;
|
||||
frame->blank = false;
|
||||
Common::LockGuard render_lock(m_impl->renderer.GetMutex());
|
||||
auto& image = m_impl->ResolveSurface(info);
|
||||
if (image.backing.format == vk::Format::eUndefined) {
|
||||
|
|
@ -894,6 +943,8 @@ Presenter::Frame& Presenter::PrepareBlankFrame(uint32_t width, uint32_t height,
|
|||
KYTY_PROFILER_FUNCTION();
|
||||
auto format = m_impl->frames.GetFormat();
|
||||
auto* frame = m_impl->frames.Acquire({width, height}, format);
|
||||
frame->hdr = false;
|
||||
frame->blank = true;
|
||||
Common::LockGuard render_lock(m_impl->renderer.GetMutex());
|
||||
frame->Configure(m_impl->window.graphic_ctx, {width, height}, format);
|
||||
vk::ClearColorValue clear {};
|
||||
|
|
@ -922,6 +973,10 @@ bool Presenter::IsGuestPaused() const noexcept {
|
|||
return m_impl->window.loop.paused.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
bool Presenter::IsHdrSupported() const noexcept {
|
||||
return m_impl->swapchain.IsHdrSupported();
|
||||
}
|
||||
|
||||
bool Presenter::NeedsSystemOverlayRefresh() const noexcept {
|
||||
const auto visual = GetSystemOverlayVisualState();
|
||||
return visual.active ||
|
||||
|
|
@ -967,12 +1022,16 @@ void Presenter::Impl::Present() {
|
|||
KYTY_PROFILER_FUNCTION();
|
||||
|
||||
const auto overlay_visual = GetSystemOverlayVisualState();
|
||||
// Keep black/blank flips in the current color space; only real main-bus frames switch it.
|
||||
const auto* main_frame = layers[0].frame;
|
||||
const bool hdr_changed = main_frame != nullptr && !main_frame->blank &&
|
||||
swapchain.SetHdr(main_frame->hdr);
|
||||
// Some window systems keep presenting an old swapchain after a resize.
|
||||
if (swapchain.NeedsResize()) {
|
||||
if (hdr_changed || swapchain.NeedsResize()) {
|
||||
RecoverSwapchain(Swapchain::Status::Recreate);
|
||||
}
|
||||
for (uint32_t attempt = 0; attempt < 2; attempt++) {
|
||||
auto status = swapchain.AcquireNextImage();
|
||||
auto status = swapchain.AcquireNextImage(present_scheduler);
|
||||
if (status != Swapchain::Status::Success) {
|
||||
RecoverSwapchain(status);
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ vk::PhysicalDeviceVulkan12Features WindowContext::RequiredVulkan12Features() noe
|
|||
features.shaderBufferInt64Atomics = VK_TRUE;
|
||||
features.storageBuffer8BitAccess = VK_TRUE;
|
||||
features.shaderSampledImageArrayNonUniformIndexing = VK_TRUE;
|
||||
features.shaderStorageBufferArrayNonUniformIndexing = VK_TRUE;
|
||||
return features;
|
||||
}
|
||||
|
||||
|
|
@ -284,6 +285,9 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
|
|||
check_feature(features12.shaderSampledImageArrayNonUniformIndexing,
|
||||
"shaderSampledImageArrayNonUniformIndexing",
|
||||
required_features12.shaderSampledImageArrayNonUniformIndexing);
|
||||
check_feature(features12.shaderStorageBufferArrayNonUniformIndexing,
|
||||
"shaderStorageBufferArrayNonUniformIndexing",
|
||||
required_features12.shaderStorageBufferArrayNonUniformIndexing);
|
||||
check_feature(features13.robustImageAccess, "robustImageAccess");
|
||||
check_feature(features13.dynamicRendering, "dynamicRendering",
|
||||
required_features13.dynamicRendering);
|
||||
|
|
@ -478,6 +482,13 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
|
|||
provoking_vertex.pNext = supported_features2.pNext;
|
||||
supported_features2.pNext = &provoking_vertex;
|
||||
}
|
||||
const bool maintenance8_extension =
|
||||
HasExtension(device_extensions, VK_KHR_MAINTENANCE_8_EXTENSION_NAME);
|
||||
vk::PhysicalDeviceMaintenance8FeaturesKHR maintenance8 {};
|
||||
if (maintenance8_extension) {
|
||||
maintenance8.pNext = supported_features2.pNext;
|
||||
supported_features2.pNext = &maintenance8;
|
||||
}
|
||||
const bool image_atomic_int64_extension =
|
||||
HasExtension(device_extensions, VK_EXT_SHADER_IMAGE_ATOMIC_INT64_EXTENSION_NAME);
|
||||
vk::PhysicalDeviceShaderImageAtomicInt64FeaturesEXT image_atomic_int64 {};
|
||||
|
|
@ -542,6 +553,9 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
|
|||
graphics.attachment_feedback_loop_enabled =
|
||||
feedback_extensions && feedback_layout.attachmentFeedbackLoopLayout &&
|
||||
feedback_dynamic.attachmentFeedbackLoopDynamicState;
|
||||
graphics.maintenance8_enabled = maintenance8_extension && maintenance8.maintenance8;
|
||||
LOGF("Vulkan maintenance8 support: %s\n",
|
||||
graphics.maintenance8_enabled ? "true" : "false");
|
||||
LOGF("Vulkan depth feedback support: %s\n",
|
||||
graphics.attachment_feedback_loop_enabled ? "true" : "false");
|
||||
if (graphics.mesh_shader_enabled) {
|
||||
|
|
@ -638,6 +652,10 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
|
|||
image_atomic_int64.sparseImageInt64Atomics = VK_FALSE;
|
||||
create_info.pNext = &image_atomic_int64;
|
||||
}
|
||||
if (graphics.maintenance8_enabled) {
|
||||
maintenance8.pNext = const_cast<void*>(create_info.pNext);
|
||||
create_info.pNext = &maintenance8;
|
||||
}
|
||||
create_info.pQueueCreateInfos = &queue_create_info;
|
||||
create_info.queueCreateInfoCount = 1;
|
||||
create_info.enabledExtensionCount = static_cast<uint32_t>(device_extensions.size());
|
||||
|
|
@ -671,6 +689,11 @@ static void VulkanGetExtensions(VulkanExtensions& r) {
|
|||
});
|
||||
|
||||
r.enable_validation_layers = Config::VulkanValidationEnabled();
|
||||
if (Config::HdrEnabled() &&
|
||||
HasExtension(r.available_extensions, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME) &&
|
||||
!HasExtension(r.required_extensions, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME)) {
|
||||
r.required_extensions.push_back(VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME);
|
||||
}
|
||||
|
||||
if (HasExtension(r.available_extensions, VK_EXT_DEBUG_UTILS_EXTENSION_NAME)) {
|
||||
r.required_extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
|
||||
|
|
@ -1002,6 +1025,7 @@ void WindowContext::CreateVulkan() {
|
|||
graphic_ctx.memory_budget_ext_enabled = true;
|
||||
}
|
||||
for (const auto* extension: {VK_EXT_ROBUSTNESS_2_EXTENSION_NAME,
|
||||
VK_KHR_MAINTENANCE_8_EXTENSION_NAME,
|
||||
VK_EXT_SHADER_IMAGE_ATOMIC_INT64_EXTENSION_NAME,
|
||||
VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME,
|
||||
VK_EXT_MESH_SHADER_EXTENSION_NAME,
|
||||
|
|
|
|||
|
|
@ -326,6 +326,19 @@ static void GameEventController([[maybe_unused]] const EventController& f) {
|
|||
const auto button = ControllerButtonToPadButton(f.button);
|
||||
if (button != 0) {
|
||||
Controller::SetButton(f.id, button, f.down);
|
||||
} else if (f.button == SDL_GAMEPAD_BUTTON_BACK) {
|
||||
// Pads without a touchpad (Switch, Xbox, ...) have no way to click it, which
|
||||
// blocks games that require it. Use their Back/Select button as a centered click.
|
||||
auto* pad = SDL_GetGamepadFromID(f.id);
|
||||
if (pad != nullptr && SDL_GetNumGamepadTouchpads(pad) == 0) {
|
||||
if (f.down) {
|
||||
Controller::SetTouchPad(f.id, 0, true, 0.5f, 0.5f);
|
||||
}
|
||||
Controller::SetButton(f.id, Controller::PAD_BUTTON_TOUCH_PAD, f.down);
|
||||
if (f.up) {
|
||||
Controller::SetTouchPad(f.id, 0, false, 0.5f, 0.5f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -789,19 +789,20 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
|
|||
GetDumpLabel(options), StageName(options.stage), options.shader_hash,
|
||||
static_cast<uint64_t>(ir.blocks.size()), phase_ms());
|
||||
IR::RewriteToSsa(ir.blocks);
|
||||
IR::ConstantPropagationPass(ir.blocks, ir.wave_size);
|
||||
IR::ResolveControlFlowIdentities(ir);
|
||||
IR::RemoveIdentities(ir.blocks);
|
||||
IR::EliminateDeadCode(ir.blocks);
|
||||
const auto read_lane_stats = IR::EliminateReadLane(ir, ir.wave_size);
|
||||
if (read_lane_stats.rewritten_reads != 0) {
|
||||
LOGF("%s read-lane elimination: reads=%" PRIu32 "\n", GetDumpLabel(options),
|
||||
read_lane_stats.rewritten_reads);
|
||||
const auto simplify = [&ir] {
|
||||
IR::ConstantPropagationPass(ir.blocks, ir.wave_size);
|
||||
IR::ResolveControlFlowIdentities(ir);
|
||||
IR::RemoveIdentities(ir.blocks);
|
||||
IR::EliminateDeadCode(ir.blocks);
|
||||
};
|
||||
simplify();
|
||||
const auto read_lane_stats = IR::EliminateReadLane(ir, ir.wave_size);
|
||||
if (read_lane_stats.rewritten_reads != 0) {
|
||||
LOGF("%s read-lane elimination: reads=%" PRIu32 "\n", GetDumpLabel(options),
|
||||
read_lane_stats.rewritten_reads);
|
||||
}
|
||||
// Cleanup also exposes profitable folds by removing dead predicate consumers.
|
||||
simplify();
|
||||
if (decoded.has_swap_pc) {
|
||||
// Leaf returns are resolved CFG edges; native PC values must not escape into GPU data.
|
||||
for (const auto* block: ir.blocks)
|
||||
|
|
@ -817,7 +818,7 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
|
|||
MakeIrDump(cfg_dump, ir).c_str());
|
||||
}
|
||||
}
|
||||
IR::TrackResources(ir, decoded, native_cfg);
|
||||
IR::TrackResources(ir, decoded, native_cfg, options.input_info);
|
||||
TranslateResult result;
|
||||
result.program = std::move(ir);
|
||||
if (options.dump_ir) {
|
||||
|
|
@ -885,7 +886,8 @@ CompileResult CompileProgram(TranslateResult translated, const CompileOptions& o
|
|||
|
||||
LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " SPIR-V EmitProgram\n",
|
||||
GetDumpLabel(options), StageName(ir.stage), ir.shader_hash);
|
||||
auto spirv = Spirv::EmitProgram(ir, options.input_info, push_data_start_dword);
|
||||
auto spirv = Spirv::EmitProgram(ir, options.input_info, push_data_start_dword,
|
||||
options.maintenance8_enabled);
|
||||
LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " SPIR-V EmitProgram words=%" PRIu64
|
||||
" elapsed_ms=%" PRIu64 "\n",
|
||||
GetDumpLabel(options), StageName(ir.stage), ir.shader_hash,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ struct CompileOptions {
|
|||
uint64_t shader_hash = 0;
|
||||
bool dump_ir = true;
|
||||
bool early_dump = false;
|
||||
bool maintenance8_enabled = false;
|
||||
const char* dump_label = nullptr;
|
||||
std::span<const uint32_t> user_data;
|
||||
std::span<const uint32_t> back_code;
|
||||
|
|
|
|||
|
|
@ -197,7 +197,7 @@ void ValidateNativeProgram(const IR::Program& program, bool lds_storage) {
|
|||
} // namespace
|
||||
|
||||
std::vector<uint32_t> EmitProgram(IR::Program& program, ShaderStageInputInfo input_info,
|
||||
uint32_t push_data_start_dword) {
|
||||
uint32_t push_data_start_dword, bool maintenance8_enabled) {
|
||||
using namespace Emitter;
|
||||
|
||||
if (program.stage != ShaderType::Compute && program.stage != ShaderType::Vertex &&
|
||||
|
|
@ -215,7 +215,7 @@ std::vector<uint32_t> EmitProgram(IR::Program& program, ShaderStageInputInfo inp
|
|||
}
|
||||
IR::ValidateProgram(program, true);
|
||||
program.bindings = {.push_data_start_dword = push_data_start_dword};
|
||||
EmitterState state(program, input_info);
|
||||
EmitterState state(program, input_info, maintenance8_enabled);
|
||||
const auto* workgroup = ShaderWorkgroupInput(program.stage, input_info);
|
||||
state.lane_count =
|
||||
workgroup != nullptr && program.wave_size == 64u && workgroup->host_subgroup_size == 32u
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ namespace Libs::Graphics::ShaderRecompiler::Spirv {
|
|||
|
||||
// Resets and writes backend IDs and descriptor indices; do not emit the same program concurrently.
|
||||
std::vector<uint32_t> EmitProgram(IR::Program& program, ShaderStageInputInfo input_info,
|
||||
uint32_t push_data_start_dword = 0);
|
||||
uint32_t push_data_start_dword = 0,
|
||||
bool maintenance8_enabled = false);
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::Spirv
|
||||
|
||||
|
|
|
|||
|
|
@ -269,20 +269,13 @@ uint32_t ResultVector(ValueEmitContext& ctx, uint32_t value,
|
|||
result, packed[0], packed[1], packed[2], packed[3]);
|
||||
return result;
|
||||
}
|
||||
uint32_t component[4] {};
|
||||
for (uint32_t index = 0; index < 4u; index++) {
|
||||
if (dref) {
|
||||
component[index] = index == 0u ? FloatBits(ctx, value) : ConstantU32(ctx.state, 0);
|
||||
continue;
|
||||
}
|
||||
const auto scalar = ctx.state.builder.AllocateId();
|
||||
ctx.state.builder.AddFunction(
|
||||
spv::OpCompositeExtract, ImageScalarType(ctx.state, value_class), scalar, value, index);
|
||||
component[index] = SampledComponentBits(ctx, scalar, value_class);
|
||||
}
|
||||
if (!dref)
|
||||
return value_class == Prospero::TextureNumericClass::Uint
|
||||
? value : Unary(ctx.state, spv::OpBitcast, TypeU32Vector(ctx.state, 4), value);
|
||||
const auto result = ctx.state.builder.AllocateId();
|
||||
const auto zero = ConstantU32(ctx.state, 0);
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(ctx.state, 4), result,
|
||||
component[0], component[1], component[2], component[3]);
|
||||
FloatBits(ctx, value), zero, zero, zero);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -323,25 +316,26 @@ uint32_t QueryDimensions(ValueEmitContext& ctx, const IR::MemoryInfo& mem,
|
|||
return vector;
|
||||
}
|
||||
|
||||
uint32_t PackedOffset(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR::Inst& address,
|
||||
const ImageSampleLayout& layout, ImageDimension dimension) {
|
||||
uint32_t PackedOffset(ValueEmitContext& ctx, IR::Value value, ImageDimension dimension,
|
||||
uint32_t& mask) {
|
||||
const auto components = ImageDimensionInfoFor(dimension).spatial_components;
|
||||
const auto zero = ConstantI32(ctx.state, 0);
|
||||
if (layout.offset == NoImageComponent || mem.image_address_components <= layout.offset) {
|
||||
if (components == 1u) return zero;
|
||||
const auto result = ctx.state.builder.AllocateId();
|
||||
if (components == 3u) {
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(ctx.state, 3),
|
||||
result, zero, zero, zero);
|
||||
} else {
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(ctx.state, 2),
|
||||
result, zero, zero);
|
||||
uint32_t values[3] {};
|
||||
if (value.IsImmediate()) {
|
||||
// RDNA2 packs signed six-bit texel offsets into the low bits of each byte.
|
||||
for (uint32_t index = 0; index < components; index++) {
|
||||
const auto bits = (value.U32() >> (index * 8u)) & 0x3fu;
|
||||
values[index] = ConstantI32(ctx.state, static_cast<int32_t>(bits ^ 0x20u) - 0x20);
|
||||
}
|
||||
return result;
|
||||
mask |= spv::ImageOperandsConstOffsetMask;
|
||||
return components == 1u
|
||||
? values[0]
|
||||
: ctx.state.builder.Constant(spv::OpConstantComposite,
|
||||
TypeI32Vector(ctx.state, components),
|
||||
std::span(values).first(components));
|
||||
}
|
||||
const auto packed = Unary(ctx.state, spv::OpBitcast, TypeI32(ctx.state),
|
||||
AddressU32(ctx, mem, address, layout.offset));
|
||||
uint32_t values[3] = {zero, zero, zero};
|
||||
ctx.state.builder.RequireCapability(spv::CapabilityImageGatherExtended);
|
||||
mask |= spv::ImageOperandsOffsetMask;
|
||||
const auto packed = Unary(ctx.state, spv::OpBitcast, TypeI32(ctx.state), ctx.Def(value));
|
||||
for (uint32_t index = 0; index < components; index++) {
|
||||
values[index] = ctx.state.builder.AllocateId();
|
||||
ctx.state.builder.AddFunction(spv::OpBitFieldSExtract, TypeI32(ctx.state), values[index],
|
||||
|
|
@ -350,13 +344,8 @@ uint32_t PackedOffset(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR
|
|||
}
|
||||
if (components == 1u) return values[0];
|
||||
const auto result = ctx.state.builder.AllocateId();
|
||||
if (components == 3u) {
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(ctx.state, 3),
|
||||
result, values[0], values[1], values[2]);
|
||||
} else {
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(ctx.state, 2),
|
||||
result, values[0], values[1]);
|
||||
}
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(ctx.state, components),
|
||||
result, std::span(values).first(components));
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -388,17 +377,28 @@ uint32_t InverseSwizzle(uint32_t swizzle, uint32_t component) {
|
|||
|
||||
Format::BufferFormatInfo ImageConversionFormat(const EmitterState& state,
|
||||
const IR::MemoryInfo& mem) {
|
||||
const auto format = state.program.info.images[mem.resource].conversion_format;
|
||||
const auto& image = state.program.info.images[mem.resource];
|
||||
const auto format = image.conversion_format;
|
||||
if (format == Prospero::BufferFormat::kInvalid) return {};
|
||||
const auto info = Format::GetFormatInfo(format);
|
||||
EXIT_IF(Prospero::RemapTextureFormat(format) == format || info.component_count == 0u ||
|
||||
info.component_count > 4u);
|
||||
EXIT_IF(info.type != Format::ComponentType::Uscaled &&
|
||||
(info.type != Format::ComponentType::Uint || !info.packed_bitfield ||
|
||||
((info.type != Format::ComponentType::Uint &&
|
||||
info.type != Format::ComponentType::Unorm) || !info.packed_bitfield ||
|
||||
info.byte_size != sizeof(uint32_t)));
|
||||
return info;
|
||||
}
|
||||
|
||||
uint32_t ExtractPackedImageComponent(EmitterState& state, const Format::BufferFormatInfo& info,
|
||||
uint32_t packed, uint32_t type, uint32_t component) {
|
||||
const auto result = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpBitFieldUExtract, type, result, packed,
|
||||
ConstantU32(state, info.component_bit_offset[component]),
|
||||
ConstantU32(state, info.component_bits[component]));
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t ImageGatherSource(const EmitterState& state, const IR::MemoryInfo& mem) {
|
||||
const auto component = ImageGatherComponent(mem.dmask);
|
||||
const auto info = ImageConversionFormat(state, mem);
|
||||
|
|
@ -422,13 +422,11 @@ uint32_t UnpackImageTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint
|
|||
}
|
||||
uint32_t components[4] {};
|
||||
for (uint32_t component = 0; component < info.component_count; component++) {
|
||||
components[component] = ctx.state.builder.AllocateId();
|
||||
if (info.packed_bitfield) {
|
||||
ctx.state.builder.AddFunction(spv::OpBitFieldUExtract, scalar_type,
|
||||
components[component], packed,
|
||||
ConstantU32(ctx.state, info.component_bit_offset[component]),
|
||||
ConstantU32(ctx.state, info.component_bits[component]));
|
||||
components[component] =
|
||||
ExtractPackedImageComponent(ctx.state, info, packed, scalar_type, component);
|
||||
} else {
|
||||
components[component] = ctx.state.builder.AllocateId();
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeExtract, scalar_type,
|
||||
components[component], texel, component);
|
||||
// UNorm backing preserves the guest's filtering; scaling precedes swizzle constants.
|
||||
|
|
@ -488,21 +486,74 @@ uint32_t UnpackImageGather(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uin
|
|||
return Binary(ctx.state, spv::OpFMul, vector_type, gathered, scales);
|
||||
}
|
||||
|
||||
uint32_t values[4] {};
|
||||
for (uint32_t lane = 0; lane < 4u; lane++) {
|
||||
const auto packed = ctx.state.builder.AllocateId();
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), packed, gathered,
|
||||
lane);
|
||||
values[lane] = ctx.state.builder.AllocateId();
|
||||
const auto physical = (selector - 4u) % info.component_count;
|
||||
ctx.state.builder.AddFunction(spv::OpBitFieldUExtract, TypeU32(ctx.state), values[lane],
|
||||
packed,
|
||||
ConstantU32(ctx.state, info.component_bit_offset[physical]),
|
||||
ConstantU32(ctx.state, info.component_bits[physical]));
|
||||
return ExtractPackedImageComponent(ctx.state, info, gathered, TypeU32Vector(ctx.state, 4),
|
||||
(selector - 4u) % info.component_count);
|
||||
}
|
||||
|
||||
uint32_t SamplePackedUnormLz(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t coord) {
|
||||
auto& state = ctx.state;
|
||||
const auto& image = state.program.info.images[mem.resource];
|
||||
const auto info = ImageConversionFormat(state, mem);
|
||||
const auto sampled = MakeSampledImage(state, mem.resource,
|
||||
LoadSamplerDescriptor(state, mem.sampler));
|
||||
const auto gathered = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpImageGather, TypeU32Vector(state, 4), gathered, sampled,
|
||||
coord, ConstantU32(state, 0u));
|
||||
state.builder.RequireCapability(spv::CapabilityImageQuery);
|
||||
const auto size = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpImageQuerySizeLod, TypeU32Vector(state, 2), size,
|
||||
LoadImageDescriptor(state, mem.resource), ConstantU32(state, 0u));
|
||||
const auto size_f32 = Unary(state, spv::OpConvertUToF, TypeF32Vector(state, 2), size);
|
||||
const auto half = ConstantF32Value(state, 0.5f);
|
||||
const auto halves = state.builder.Constant(spv::OpConstantComposite,
|
||||
TypeF32Vector(state, 2), half, half);
|
||||
const auto fractions = state.builder.AllocateId();
|
||||
state.builder.AddFunction(
|
||||
spv::OpExtInst, TypeF32Vector(state, 2), fractions, GlslStd450(state), GLSLstd450Fract,
|
||||
Binary(state, spv::OpFSub, TypeF32Vector(state, 2),
|
||||
Binary(state, spv::OpFMul, TypeF32Vector(state, 2), coord, size_f32), halves));
|
||||
uint32_t fraction[2];
|
||||
uint32_t complement[2];
|
||||
for (uint32_t axis = 0; axis < 2u; axis++) {
|
||||
fraction[axis] = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpCompositeExtract, TypeF32(state), fraction[axis],
|
||||
fractions, axis);
|
||||
complement[axis] = Binary(state, spv::OpFSub, TypeF32(state),
|
||||
ConstantF32Value(state, 1.0f), fraction[axis]);
|
||||
}
|
||||
const auto result = ctx.state.builder.AllocateId();
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(ctx.state, 4), result,
|
||||
values[0], values[1], values[2], values[3]);
|
||||
// Gather order is (x0, y1), (x1, y1), (x1, y0), (x0, y0).
|
||||
const auto weights = state.builder.AllocateId();
|
||||
state.builder.AddFunction(
|
||||
spv::OpCompositeConstruct, TypeF32Vector(state, 4), weights,
|
||||
Binary(state, spv::OpFMul, TypeF32(state), complement[0], fraction[1]),
|
||||
Binary(state, spv::OpFMul, TypeF32(state), fraction[0], fraction[1]),
|
||||
Binary(state, spv::OpFMul, TypeF32(state), fraction[0], complement[1]),
|
||||
Binary(state, spv::OpFMul, TypeF32(state), complement[0], complement[1]));
|
||||
uint32_t decoded[4] {};
|
||||
uint32_t selected[4] {ZeroF32(state), ZeroF32(state), ZeroF32(state), ZeroF32(state)};
|
||||
for (uint32_t component = 0; component < 4u; component++) {
|
||||
if ((mem.dmask & (1u << component)) == 0u) continue;
|
||||
const auto selector = (image.shader_swizzle >> (component * 3u)) & 7u;
|
||||
if (selector == 1u) {
|
||||
selected[component] = ConstantF32Value(state, 1.0f);
|
||||
} else if (selector >= 4u) {
|
||||
const auto physical = (selector - 4u) % info.component_count;
|
||||
if (decoded[physical] == 0u) {
|
||||
const auto texels = Unary(
|
||||
state, spv::OpConvertUToF, TypeF32Vector(state, 4),
|
||||
ExtractPackedImageComponent(state, info, gathered,
|
||||
TypeU32Vector(state, 4), physical));
|
||||
decoded[physical] = Binary(
|
||||
state, spv::OpFMul, TypeF32(state),
|
||||
Binary(state, spv::OpDot, TypeF32(state), texels, weights),
|
||||
ConstantF32Value(state, 1.0f / ((1u << info.component_bits[physical]) - 1u)));
|
||||
}
|
||||
selected[component] = decoded[physical];
|
||||
}
|
||||
}
|
||||
const auto result = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 4), result,
|
||||
selected[0], selected[1], selected[2], selected[3]);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -548,47 +599,22 @@ uint32_t EmitOneDimensionalGatherLz(ValueEmitContext& ctx, const IR::MemoryInfo&
|
|||
return result;
|
||||
}
|
||||
|
||||
uint32_t PackImageTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t texel) {
|
||||
const auto info = ImageConversionFormat(ctx.state, mem);
|
||||
if (info.format == Prospero::BufferFormat::kInvalid) return texel;
|
||||
EXIT_IF(info.type != Format::ComponentType::Uint || !info.packed_bitfield);
|
||||
|
||||
auto packed = ConstantU32(ctx.state, 0u);
|
||||
for (uint32_t component = 0; component < info.component_count; component++) {
|
||||
const auto value = ctx.state.builder.AllocateId();
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), value, texel,
|
||||
component);
|
||||
const auto maximum =
|
||||
ConstantU32(ctx.state, info.component_bits[component] == 32u
|
||||
? UINT32_MAX
|
||||
: (1u << info.component_bits[component]) - 1u);
|
||||
const auto within =
|
||||
Binary(ctx.state, spv::OpULessThan, TypeBool(ctx.state), value, maximum);
|
||||
const auto clamped = ctx.state.builder.AllocateId();
|
||||
ctx.state.builder.AddFunction(spv::OpSelect, TypeU32(ctx.state), clamped, within, value,
|
||||
maximum);
|
||||
const auto shifted =
|
||||
info.component_bit_offset[component] == 0u
|
||||
? clamped
|
||||
: Binary(ctx.state, spv::OpShiftLeftLogical, TypeU32(ctx.state), clamped,
|
||||
ConstantU32(ctx.state, info.component_bit_offset[component]));
|
||||
packed = Binary(ctx.state, spv::OpBitwiseOr, TypeU32(ctx.state), packed, shifted);
|
||||
}
|
||||
const auto zero = ConstantU32(ctx.state, 0u);
|
||||
const auto result = ctx.state.builder.AllocateId();
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(ctx.state, 4), result,
|
||||
packed, zero, zero, zero);
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t StoreTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t data, bool integer) {
|
||||
const auto swizzle = ctx.state.program.info.images[mem.resource].shader_swizzle;
|
||||
uint32_t values[4] {};
|
||||
const auto info = ImageConversionFormat(ctx.state, mem);
|
||||
const bool packed = info.format != Prospero::BufferFormat::kInvalid;
|
||||
EXIT_IF(packed && (!info.packed_bitfield ||
|
||||
(info.type != Format::ComponentType::Uint &&
|
||||
info.type != Format::ComponentType::Unorm)));
|
||||
const auto zero = ConstantU32(ctx.state, 0);
|
||||
uint32_t values[4] {zero, zero, zero, zero};
|
||||
auto packed_value = zero;
|
||||
const auto dmask = mem.dmask != 0u ? mem.dmask : 1u;
|
||||
for (uint32_t component = 0; component < 4u; component++) {
|
||||
for (uint32_t component = 0; component < (packed ? info.component_count : 4u); component++) {
|
||||
const auto source = InverseSwizzle(swizzle, component);
|
||||
uint32_t raw = ConstantU32(ctx.state, 0);
|
||||
if (source < 4u && ((dmask >> source) & 1u) != 0u) {
|
||||
const bool selected = source < 4u && ((dmask >> source) & 1u) != 0u;
|
||||
uint32_t raw = zero;
|
||||
if (selected) {
|
||||
const auto packed_index = DmaskComponentIndex(dmask, source);
|
||||
raw = ctx.state.builder.AllocateId();
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), raw, data,
|
||||
|
|
@ -598,21 +624,39 @@ uint32_t StoreTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t d
|
|||
raw = Binary(ctx.state, spv::OpShiftRightLogical, TypeU32(ctx.state), raw,
|
||||
ConstantU32(ctx.state, 16u));
|
||||
}
|
||||
raw = Binary(ctx.state, spv::OpBitwiseAnd, TypeU32(ctx.state), raw,
|
||||
ConstantU32(ctx.state, 0xffffu));
|
||||
if (!packed) {
|
||||
raw = Binary(ctx.state, spv::OpBitwiseAnd, TypeU32(ctx.state), raw,
|
||||
ConstantU32(ctx.state, 0xffffu));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (packed) {
|
||||
if (!selected) continue;
|
||||
auto value = EncodeFormattedStoreComponent(ctx.state, info, component, raw, mem.data_bits);
|
||||
if (info.type == Format::ComponentType::Uint) {
|
||||
value = EmitUMin32(ctx.state, value,
|
||||
ConstantU32(ctx.state, (1u << info.component_bits[component]) - 1u));
|
||||
}
|
||||
if (info.component_bit_offset[component] != 0u) {
|
||||
value = Binary(ctx.state, spv::OpShiftLeftLogical, TypeU32(ctx.state), value,
|
||||
ConstantU32(ctx.state, info.component_bit_offset[component]));
|
||||
}
|
||||
packed_value = packed_value == zero ? value
|
||||
: Binary(ctx.state, spv::OpBitwiseOr, TypeU32(ctx.state), packed_value, value);
|
||||
continue;
|
||||
}
|
||||
values[component] = integer ? raw
|
||||
: mem.data_bits == 16u
|
||||
? EmitF16BitsToF32(ctx.state, raw)
|
||||
: Unary(ctx.state, spv::OpBitcast, TypeF32(ctx.state), raw);
|
||||
}
|
||||
if (packed) values[0] = packed_value;
|
||||
const auto texel = ctx.state.builder.AllocateId();
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeConstruct,
|
||||
integer ? TypeU32Vector(ctx.state, 4)
|
||||
: TypeF32Vector(ctx.state, 4),
|
||||
texel, values[0], values[1], values[2], values[3]);
|
||||
return PackImageTexel(ctx, mem, texel);
|
||||
return texel;
|
||||
}
|
||||
|
||||
template <typename Emit>
|
||||
|
|
@ -699,6 +743,87 @@ uint32_t EmitImageAccess(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t r
|
|||
result_type, EmitRun);
|
||||
}
|
||||
|
||||
template <typename Emit>
|
||||
uint32_t EmitImageResult(ValueEmitContext& ctx, const IR::Inst& inst, const Emit& emit) {
|
||||
auto& state = ctx.state;
|
||||
const auto& mem = ctx.Memory(inst);
|
||||
const auto& image = state.program.info.images[mem.resource];
|
||||
const bool gather = inst.GetOpcode() == IR::ValueOpcode::ImageGatherRaw;
|
||||
const bool dref = HasFlag(mem, Decoder::ImageSampleFlagCompare);
|
||||
const bool scalar = dref && !gather;
|
||||
const bool mixed = std::ranges::any_of(image.indirect_resources, [&](uint32_t resource) {
|
||||
return state.program.info.images[resource].numeric_class != image.numeric_class;
|
||||
});
|
||||
const auto native_type = [&](uint32_t resource) {
|
||||
return scalar ? TypeF32(state) : ImageVectorType(
|
||||
state, dref ? Prospero::TextureNumericClass::Float
|
||||
: state.program.info.images[resource].numeric_class, 4);
|
||||
};
|
||||
const auto result_bits = [&](uint32_t resource, uint32_t value) {
|
||||
auto candidate_mem = mem;
|
||||
candidate_mem.resource = resource;
|
||||
if (!scalar) {
|
||||
value = gather ? UnpackImageGather(ctx, candidate_mem, value)
|
||||
: UnpackImageTexel(ctx, candidate_mem, value);
|
||||
}
|
||||
return ResultVector(ctx, value,
|
||||
dref ? Prospero::TextureNumericClass::Float
|
||||
: state.program.info.images[resource].numeric_class,
|
||||
scalar, candidate_mem, gather);
|
||||
};
|
||||
// RDNA returns register bits. Convert per typed run only when the join mixes
|
||||
// numeric classes; homogeneous accesses keep their single conversion after it.
|
||||
const auto value = EmitImageAccess(
|
||||
ctx, inst, mixed ? TypeU32Vector(state, 4) : native_type(mem.resource),
|
||||
[&](uint32_t resource, uint32_t array_index) {
|
||||
const auto native = emit(resource, array_index, native_type(resource));
|
||||
return mixed ? result_bits(resource, native) : native;
|
||||
});
|
||||
return mixed ? value : result_bits(mem.resource, value);
|
||||
}
|
||||
|
||||
struct ImageSampler {
|
||||
uint32_t id = 0;
|
||||
bool dynamic = false;
|
||||
};
|
||||
|
||||
std::array<ImageSampler, 2> LoadImageSamplers(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
auto& state = ctx.state;
|
||||
const auto& mem = ctx.Memory(inst);
|
||||
const auto& samplers = state.program.info.samplers;
|
||||
const auto& root = samplers.at(mem.sampler);
|
||||
std::array<ImageSampler, 2> result {};
|
||||
const auto load = [&](const IR::ImageResource& image) {
|
||||
const bool integer = image.numeric_class != Prospero::TextureNumericClass::Float;
|
||||
auto& sampler = result[integer];
|
||||
if (sampler.id != 0u) return;
|
||||
uint32_t index = mem.sampler;
|
||||
if (root.integer_border != integer) {
|
||||
const auto found = std::ranges::find_if(samplers, [&](const auto& candidate) {
|
||||
return candidate.snapshot_index == root.snapshot_index &&
|
||||
candidate.integer_border == integer;
|
||||
});
|
||||
if (found == samplers.end()) ctx.Fail(inst, "has no typed sampler variant");
|
||||
index = static_cast<uint32_t>(found - samplers.begin());
|
||||
}
|
||||
uint32_t array_index = 0;
|
||||
const auto& candidates = samplers[index].indirect_resources;
|
||||
if (!candidates.empty()) {
|
||||
const auto* handle = inst.Arg(1).ResolveInstruction();
|
||||
if (handle == nullptr || handle->GetOpcode() != IR::ValueOpcode::GetSamplerResource)
|
||||
ctx.Fail(inst, "has invalid finite sampler key provenance");
|
||||
array_index = EmitIndexSwitch(
|
||||
state, ctx.Def(handle->Arg(0)), static_cast<uint32_t>(candidates.size()),
|
||||
TypeU32(state), [&](uint32_t ordinal) { return ConstantU32(state, candidates[ordinal]); });
|
||||
}
|
||||
sampler = {LoadSamplerDescriptor(state, index, array_index), array_index != 0u};
|
||||
};
|
||||
const auto& image = state.program.info.images[mem.resource];
|
||||
load(image);
|
||||
for (const auto candidate: image.indirect_resources) load(state.program.info.images[candidate]);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
|
|
@ -715,6 +840,22 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
|||
ctx.Define(inst, QueryDimensions(ctx, mem, *address));
|
||||
return;
|
||||
}
|
||||
if (image.resource_class == IR::ImageResourceClass::Sampled &&
|
||||
image.conversion_format == Prospero::BufferFormat::k11_11_10UNorm) {
|
||||
if (op != IR::ValueOpcode::ImageSampleRaw ||
|
||||
image.dimension != ImageDimension::Dim2D ||
|
||||
mem.image_dimension != ImageDimension::Dim2D || mem.data_bits != 32u ||
|
||||
mem.image_sample_flags != Decoder::ImageSampleFlagLevelZero ||
|
||||
image.indirect_root != IR::ImageResource::NoIndirectImage ||
|
||||
!state.program.info.samplers.at(mem.sampler).indirect_resources.empty()) {
|
||||
ctx.Fail(inst, "packed UNorm sampling requires a direct 2D sample_lz");
|
||||
return;
|
||||
}
|
||||
const auto coord = CoordF32(ctx, mem, *address, Layout(mem).coord, 2u);
|
||||
ctx.Define(inst, ResultVector(ctx, SamplePackedUnormLz(ctx, mem, coord),
|
||||
Prospero::TextureNumericClass::Float, false, mem));
|
||||
return;
|
||||
}
|
||||
if (op == IR::ValueOpcode::ImageQueryLod) {
|
||||
state.builder.RequireCapability(spv::CapabilityImageQuery);
|
||||
const auto dimension = image.dimension;
|
||||
|
|
@ -740,16 +881,14 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
|||
return;
|
||||
}
|
||||
if (op == IR::ValueOpcode::ImageRead) {
|
||||
const auto numeric_class = image.numeric_class;
|
||||
const auto condition = ctx.Arg(inst, 2);
|
||||
ctx.Define(
|
||||
inst,
|
||||
EmitValueOrDefaultIfCondition(
|
||||
state, condition, TypeU32Vector(state, 4), ConstantU32CompositeZero(state, 4),
|
||||
[&]() {
|
||||
const auto color = EmitImageAccess(
|
||||
ctx, inst, ImageVectorType(state, numeric_class, 4),
|
||||
[&](uint32_t resource, uint32_t array_index) {
|
||||
return EmitImageResult(
|
||||
ctx, inst, [&](uint32_t resource, uint32_t array_index, uint32_t result_type) {
|
||||
const auto& candidate = state.program.info.images[resource];
|
||||
const auto dimension = candidate.dimension;
|
||||
const auto& dimension_info = ImageDimensionInfoFor(dimension);
|
||||
|
|
@ -758,19 +897,17 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
|||
const auto coord = CoordU32(ctx, mem, *address, dimension);
|
||||
if (dimension_info.multisampled != 0u) {
|
||||
state.builder.AddFunction(
|
||||
spv::OpImageFetch, ImageVectorType(state, candidate.numeric_class, 4), fetched,
|
||||
spv::OpImageFetch, result_type, fetched,
|
||||
descriptor, coord, spv::ImageOperandsSampleMask,
|
||||
AddressU32(ctx, mem, *address, dimension_info.coordinate_components));
|
||||
} else {
|
||||
state.builder.AddFunction(
|
||||
spv::OpImageFetch, ImageVectorType(state, candidate.numeric_class, 4), fetched,
|
||||
spv::OpImageFetch, result_type, fetched,
|
||||
descriptor, coord, spv::ImageOperandsLodMask,
|
||||
LodU32(ctx, mem, *address, mem.image_dimension));
|
||||
}
|
||||
return fetched;
|
||||
});
|
||||
return ResultVector(ctx, UnpackImageTexel(ctx, mem, color), numeric_class,
|
||||
false, mem);
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
|
@ -794,21 +931,28 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
|||
const auto layout = Layout(mem);
|
||||
const auto numeric_class = image.numeric_class;
|
||||
const bool dref = HasFlag(mem, Decoder::ImageSampleFlagCompare);
|
||||
IR::Value offset_value;
|
||||
if (layout.offset != NoImageComponent) {
|
||||
const auto value = address->Arg(layout.offset).Resolve();
|
||||
offset_value = IR::ResolveActiveU32(value, inst.Arg(inst.NumArgs() - 1u).Resolve());
|
||||
if (offset_value.IsEmpty()) offset_value = value;
|
||||
}
|
||||
if (dref && state.program.info.images[mem.resource].conversion_format !=
|
||||
Prospero::BufferFormat::kInvalid) {
|
||||
ctx.Fail(inst, "uses depth comparison with a converted image");
|
||||
return;
|
||||
}
|
||||
if (op == IR::ValueOpcode::ImageGatherRaw) {
|
||||
const auto coord = CoordF32(ctx, mem, *address, layout.coord,
|
||||
dimension_info.coordinate_components, image.cube);
|
||||
if (dimension == ImageDimension::Dim1D) {
|
||||
if (dref || !HasFlag(mem, Decoder::ImageSampleFlagLevelZero) ||
|
||||
if (image.indirect_root == mem.resource || dref ||
|
||||
!HasFlag(mem, Decoder::ImageSampleFlagLevelZero) ||
|
||||
HasFlag(mem, Decoder::ImageSampleFlagOffset) ||
|
||||
HasFlag(mem, Decoder::ImageSampleFlagGatherHorizontal)) {
|
||||
ctx.Fail(inst, "has an unsupported 1D gather variant");
|
||||
return;
|
||||
}
|
||||
const auto coord = CoordF32(ctx, mem, *address, layout.coord,
|
||||
dimension_info.coordinate_components, image.cube);
|
||||
const auto sample = EmitOneDimensionalGatherLz(ctx, mem, coord, numeric_class);
|
||||
ctx.Define(inst, ResultVector(ctx, UnpackImageGather(ctx, mem, sample),
|
||||
numeric_class, false, mem, true));
|
||||
|
|
@ -818,9 +962,6 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
|||
ctx.Fail(inst, "has an unsupported 1D-array gather");
|
||||
return;
|
||||
}
|
||||
const auto result_numeric_class = dref ? Prospero::TextureNumericClass::Float
|
||||
: numeric_class;
|
||||
const auto result_type = ImageVectorType(state, result_numeric_class, 4);
|
||||
uint32_t component_or_dref;
|
||||
if (dref) {
|
||||
component_or_dref = layout.dref != NoImageComponent
|
||||
|
|
@ -829,34 +970,40 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
|||
} else {
|
||||
component_or_dref = ConstantU32(state, ImageGatherSource(state, mem));
|
||||
}
|
||||
uint32_t operand_mask = 0;
|
||||
uint32_t offset = 0;
|
||||
if (HasFlag(mem, Decoder::ImageSampleFlagGatherHorizontal)) {
|
||||
operand_mask = spv::ImageOperandsConstOffsetsMask;
|
||||
offset = HorizontalOffsets(state, dimension);
|
||||
} else if (layout.offset != NoImageComponent) {
|
||||
operand_mask = spv::ImageOperandsOffsetMask;
|
||||
offset = PackedOffset(ctx, mem, *address, layout, dimension);
|
||||
}
|
||||
const auto sampler_id = LoadSamplerDescriptor(state, mem.sampler);
|
||||
const auto EmitGather = [&](uint32_t mip) {
|
||||
const auto sampled = MakeSampledImage(state, mem.resource, sampler_id, mip);
|
||||
const auto sample = state.builder.AllocateId();
|
||||
const std::array operands {operand_mask, offset};
|
||||
state.builder.AddFunction(
|
||||
dref ? spv::OpImageDrefGather : spv::OpImageGather, result_type,
|
||||
sample, sampled, coord, component_or_dref,
|
||||
std::span(operands).first(operand_mask != 0u ? 2u : 0u));
|
||||
return sample;
|
||||
const auto samplers = LoadImageSamplers(ctx, inst);
|
||||
const auto EmitGather = [&](uint32_t resource, uint32_t array_index, uint32_t result_type) {
|
||||
const auto& candidate = state.program.info.images[resource];
|
||||
const auto sampler = samplers[candidate.numeric_class != Prospero::TextureNumericClass::Float];
|
||||
const auto dimension = candidate.dimension;
|
||||
const auto coord = CoordF32(
|
||||
ctx, mem, *address, layout.coord,
|
||||
ImageDimensionInfoFor(dimension).coordinate_components, candidate.cube);
|
||||
uint32_t operand_mask = 0;
|
||||
uint32_t offset = 0;
|
||||
if (HasFlag(mem, Decoder::ImageSampleFlagGatherHorizontal)) {
|
||||
operand_mask = spv::ImageOperandsConstOffsetsMask;
|
||||
offset = HorizontalOffsets(state, dimension);
|
||||
} else if (layout.offset != NoImageComponent) {
|
||||
offset = PackedOffset(ctx, offset_value, dimension, operand_mask);
|
||||
}
|
||||
const auto EmitMip = [&](uint32_t mip) {
|
||||
const auto sampled = MakeSampledImage(state, resource, sampler.id, mip, array_index,
|
||||
sampler.dynamic);
|
||||
const auto sample = state.builder.AllocateId();
|
||||
const std::array operands {operand_mask, offset};
|
||||
state.builder.AddFunction(
|
||||
dref ? spv::OpImageDrefGather : spv::OpImageGather, result_type,
|
||||
sample, sampled, coord, component_or_dref,
|
||||
std::span(operands).first(operand_mask != 0u ? 2u : 0u));
|
||||
return sample;
|
||||
};
|
||||
return candidate.mip_mode == IR::ImageMipMode::Dynamic && candidate.mip_count > 1u
|
||||
? EmitIndexSwitch(
|
||||
state, GatherMip(ctx, inst, mem, *address, layout, candidate.mip_count),
|
||||
candidate.mip_count, result_type, EmitMip)
|
||||
: EmitMip(0u);
|
||||
};
|
||||
const auto sample = image.mip_mode == IR::ImageMipMode::Dynamic && image.mip_count > 1u
|
||||
? EmitIndexSwitch(
|
||||
state, GatherMip(ctx, inst, mem, *address, layout,
|
||||
image.mip_count),
|
||||
image.mip_count, result_type, EmitGather)
|
||||
: EmitGather(0);
|
||||
ctx.Define(inst, ResultVector(ctx, UnpackImageGather(ctx, mem, sample),
|
||||
result_numeric_class, false, mem, true));
|
||||
ctx.Define(inst, EmitImageResult(ctx, inst, EmitGather));
|
||||
return;
|
||||
}
|
||||
const bool explicit_lod = HasFlag(mem, Decoder::ImageSampleFlagDerivative) ||
|
||||
|
|
@ -869,16 +1016,14 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
|||
} else if (dref) {
|
||||
opcode = spv::OpImageSampleDrefImplicitLod;
|
||||
}
|
||||
uint32_t result_type = ImageVectorType(state, numeric_class, 4);
|
||||
uint32_t dref_value = 0;
|
||||
if (dref) {
|
||||
result_type = TypeF32(state);
|
||||
dref_value = ZeroF32(state);
|
||||
if (layout.dref != NoImageComponent) {
|
||||
dref_value = AddressF32(ctx, mem, *address, layout.dref);
|
||||
}
|
||||
}
|
||||
std::array<uint32_t, 3> operands {};
|
||||
std::array<uint32_t, 4> operands {};
|
||||
size_t operand_count = 0;
|
||||
if (HasFlag(mem, Decoder::ImageSampleFlagDerivative)) {
|
||||
operands[0] = spv::ImageOperandsGradMask;
|
||||
|
|
@ -898,36 +1043,34 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
|||
operands[1] = AddressF32(ctx, mem, *address, layout.bias);
|
||||
operand_count = 2;
|
||||
}
|
||||
const auto& samplers = state.program.info.samplers.at(mem.sampler).indirect_resources;
|
||||
uint32_t sampler_index = 0;
|
||||
if (!samplers.empty()) {
|
||||
const auto* handle = inst.Arg(1).ResolveInstruction();
|
||||
if (handle == nullptr || handle->GetOpcode() != IR::ValueOpcode::GetSamplerResource)
|
||||
ctx.Fail(inst, "has invalid finite sampler key provenance");
|
||||
sampler_index = EmitIndexSwitch(
|
||||
state, ctx.Def(handle->Arg(0)), static_cast<uint32_t>(samplers.size()),
|
||||
TypeU32(state), [&](uint32_t ordinal) { return ConstantU32(state, samplers[ordinal]); });
|
||||
if (layout.offset != NoImageComponent &&
|
||||
!offset_value.IsImmediate() && !state.maintenance8_enabled) {
|
||||
ctx.Fail(inst, "requires maintenance8 for a dynamic sample texel offset");
|
||||
}
|
||||
const auto sampler_id = LoadSamplerDescriptor(state, mem.sampler, sampler_index);
|
||||
const auto EmitSample = [&](uint32_t resource, uint32_t array_index) {
|
||||
const auto samplers = LoadImageSamplers(ctx, inst);
|
||||
const auto EmitSample = [&](uint32_t resource, uint32_t array_index, uint32_t result_type) {
|
||||
const auto& candidate = state.program.info.images[resource];
|
||||
const auto sampler = samplers[candidate.numeric_class != Prospero::TextureNumericClass::Float];
|
||||
auto sample_operands = operands;
|
||||
auto sample_operand_count = operand_count;
|
||||
if (layout.offset != NoImageComponent) {
|
||||
sample_operand_count = std::max(size_t{1}, sample_operand_count);
|
||||
sample_operands[sample_operand_count++] = PackedOffset(
|
||||
ctx, offset_value, candidate.dimension, sample_operands[0]);
|
||||
}
|
||||
const auto coord =
|
||||
CoordF32(ctx, mem, *address, layout.coord,
|
||||
ImageDimensionInfoFor(candidate.dimension).coordinate_components,
|
||||
candidate.cube);
|
||||
const auto sampled = MakeSampledImage(state, resource, sampler_id, 0u, array_index,
|
||||
sampler_index != 0u);
|
||||
const auto sampled = MakeSampledImage(state, resource, sampler.id, 0u, array_index,
|
||||
sampler.dynamic);
|
||||
const auto sample = state.builder.AllocateId();
|
||||
state.builder.AddFunction(opcode, result_type, sample, sampled, coord,
|
||||
std::span(&dref_value, dref ? 1u : 0u),
|
||||
std::span<const uint32_t>(operands).first(operand_count));
|
||||
std::span<const uint32_t>(sample_operands).first(sample_operand_count));
|
||||
return sample;
|
||||
};
|
||||
auto result = EmitImageAccess(ctx, inst, result_type, EmitSample);
|
||||
if (!dref) {
|
||||
result = UnpackImageTexel(ctx, mem, result);
|
||||
}
|
||||
ctx.Define(inst, ResultVector(ctx, result, numeric_class, dref, mem));
|
||||
ctx.Define(inst, EmitImageResult(ctx, inst, EmitSample));
|
||||
return;
|
||||
}
|
||||
if (image_info.access == IR::ImageAccess::Atomic) {
|
||||
|
|
|
|||
|
|
@ -5,15 +5,17 @@
|
|||
#include <bit>
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter {
|
||||
uint32_t EmitIdentity(ValueEmitContext& ctx, uint32_t value);
|
||||
|
||||
// Operand signatures distinguish SPIR-V IDs (uint32_t), raw literals/values
|
||||
// (IR::Value), and instructions whose metadata or operand loads must stay lazy.
|
||||
#define EMIT_NATIVE(name, opcode, type, ...) \
|
||||
inline constexpr auto Emit##name = EmitNative<spv::opcode, IR::Type::type, __VA_ARGS__>;
|
||||
EMIT_NATIVE(BitCastU16F16, OpBitcast, U32, uint32_t)
|
||||
inline constexpr auto EmitBitCastU16F16 = EmitIdentity;
|
||||
inline constexpr auto EmitBitCastF16U16 = EmitBitCastU16F16;
|
||||
inline constexpr auto EmitConvertU32U16 = EmitBitCastU16F16;
|
||||
inline constexpr auto EmitConvertU32U8 = EmitBitCastU16F16;
|
||||
inline constexpr auto EmitBitCastU32F32 = EmitBitCastU16F16;
|
||||
EMIT_NATIVE(BitCastU32F32, OpBitcast, U32, uint32_t)
|
||||
EMIT_NATIVE(BitCastF32U32, OpBitcast, F32, uint32_t)
|
||||
EMIT_NATIVE(BitCastU64F64, OpBitcast, U64, uint32_t)
|
||||
EMIT_NATIVE(BitCastF64U64, OpBitcast, F64, uint32_t)
|
||||
|
|
@ -207,7 +209,6 @@ uint32_t EmitFPSin(EmitterState& state, uint32_t arg0);
|
|||
uint32_t EmitFPCos(EmitterState& state, uint32_t arg0);
|
||||
#undef EMIT_NATIVE
|
||||
|
||||
uint32_t EmitIdentity(ValueEmitContext& ctx, uint32_t value);
|
||||
void EmitVoid(ValueEmitContext&);
|
||||
inline constexpr auto EmitReference = EmitVoid;
|
||||
inline constexpr auto EmitReferenceU32 = EmitVoid;
|
||||
|
|
@ -321,6 +322,8 @@ inline constexpr auto EmitSharedAtomicDec32 = EmitSharedIncDec;
|
|||
inline constexpr auto EmitSharedAtomicSMin32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicUMin32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicSMax32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicUMax64 = EmitSharedAtomic64;
|
||||
inline constexpr auto EmitSharedAtomicCmpSwap64 = EmitSharedAtomic64;
|
||||
inline constexpr auto EmitSharedAtomicUMax32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicAnd32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicOr32 = EmitAtomic32;
|
||||
|
|
|
|||
|
|
@ -73,9 +73,9 @@ struct ImageDefinition {
|
|||
};
|
||||
|
||||
struct EmitterState {
|
||||
EmitterState(IR::Program& program_, ShaderStageInputInfo input_info_)
|
||||
EmitterState(IR::Program& program_, ShaderStageInputInfo input_info_, bool maintenance8_enabled_)
|
||||
: builder(program_.stage == ShaderType::Mesh ? 0x00010400u : 0x00010300u),
|
||||
program(program_), input_info(input_info_) {
|
||||
program(program_), input_info(input_info_), maintenance8_enabled(maintenance8_enabled_) {
|
||||
if (ShaderWorkgroupInput(program.stage, input_info) != nullptr) {
|
||||
lds_storage_class = spv::StorageClassWorkgroup;
|
||||
}
|
||||
|
|
@ -88,6 +88,7 @@ struct EmitterState {
|
|||
Builder builder;
|
||||
IR::Program& program;
|
||||
ShaderStageInputInfo input_info;
|
||||
bool maintenance8_enabled;
|
||||
uint32_t void_type = 0;
|
||||
uint32_t bool_type = 0;
|
||||
uint32_t u32_type = 0;
|
||||
|
|
@ -398,6 +399,8 @@ uint32_t StorageBufferPackedStride(const EmitterState& state, const IR::MemoryIn
|
|||
|
||||
Prospero::BufferFormat StorageBufferFormat(const EmitterState& state, const IR::MemoryInfo& mem);
|
||||
|
||||
bool UsesSharedIndirectBufferStore(const EmitterState& state, uint32_t resource);
|
||||
|
||||
void EmitMemoryOffsets(EmitterState& state);
|
||||
|
||||
uint32_t LdsDwordCount(const EmitterState& state);
|
||||
|
|
@ -409,6 +412,7 @@ struct MemoryResourceAccess {
|
|||
uint32_t length = 0;
|
||||
uint32_t byte_offset = 0;
|
||||
uint32_t element_bits = 32;
|
||||
bool non_uniform = false;
|
||||
spv::MemoryAccessMask memory_access = spv::MemoryAccessMaskNone;
|
||||
};
|
||||
|
||||
|
|
@ -418,7 +422,8 @@ MemoryResourceAccess PrepareMemoryResourceAccess(EmitterState& state, const IR::
|
|||
|
||||
MemoryResourceAccess PrepareStorageBufferResourceAccess(EmitterState& state,
|
||||
const IR::MemoryInfo& mem,
|
||||
BufferDefinition& buffer);
|
||||
BufferDefinition& buffer,
|
||||
uint32_t array_index = 0);
|
||||
|
||||
uint32_t EmitMemoryElementInBounds(EmitterState& state, const MemoryResourceAccess& access,
|
||||
uint32_t index);
|
||||
|
|
@ -441,6 +446,9 @@ uint32_t EmitUFloatToF32Bits(EmitterState& state, uint32_t raw, uint32_t bits);
|
|||
uint32_t NormalizeFormatComponent(EmitterState& state, const Format::BufferFormatInfo& info,
|
||||
uint32_t component, uint32_t raw);
|
||||
|
||||
uint32_t EncodeFormattedStoreComponent(EmitterState& state, const Format::BufferFormatInfo& info,
|
||||
uint32_t component, uint32_t data, uint32_t data_bits);
|
||||
|
||||
spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode);
|
||||
|
||||
uint32_t EmitAtomicOperation(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t pointer,
|
||||
|
|
|
|||
|
|
@ -249,10 +249,11 @@ uint32_t LoadBda(ValueEmitContext& ctx, uint32_t address, uint32_t active, uint3
|
|||
});
|
||||
}
|
||||
|
||||
uint32_t ByteAddress(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem) {
|
||||
uint32_t ByteAddress(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem,
|
||||
uint32_t prefix = 0) {
|
||||
if (mem.kind == IR::ResourceKind::Buffer) {
|
||||
const auto resource = ctx.state.program.info.buffers.at(mem.resource).descriptor_index;
|
||||
const auto prefix = ctx.state.memory_byte_offsets[resource];
|
||||
if (prefix == 0u) prefix = ctx.state.memory_byte_offsets[resource];
|
||||
const auto address = Binary(ctx.state, spv::OpIAdd, TypeU32(ctx.state),
|
||||
BufferByteAddress(ctx, inst, mem), prefix);
|
||||
// The host binding prefix must not wrap an out-of-range guest address into the buffer.
|
||||
|
|
@ -273,15 +274,10 @@ uint32_t ByteAddress(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Memo
|
|||
return ScratchByteAddress(ctx, mem, ctx.Arg(inst, 1), ctx.Arg(inst, 2));
|
||||
}
|
||||
|
||||
uint32_t DwordIndex(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem) {
|
||||
auto address = ByteAddress(ctx, inst, mem);
|
||||
if (mem.kind == IR::ResourceKind::Lds || mem.kind == IR::ResourceKind::Gds) {
|
||||
// RDNA2 DS region addresses retain bits [15:2] after adding the byte offset.
|
||||
address = Binary(ctx.state, spv::OpBitwiseAnd, TypeU32(ctx.state), address,
|
||||
ConstantU32(ctx.state, 0xffffu));
|
||||
}
|
||||
uint32_t DwordIndex(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem,
|
||||
uint32_t prefix = 0) {
|
||||
return Binary(ctx.state, spv::OpShiftRightLogical, TypeU32(ctx.state),
|
||||
address, ConstantU32(ctx.state, 2));
|
||||
ByteAddress(ctx, inst, mem, prefix), ConstantU32(ctx.state, 2));
|
||||
}
|
||||
|
||||
uint32_t LoadWordInBounds(ValueEmitContext& ctx, const MemoryResourceAccess& resource,
|
||||
|
|
@ -540,8 +536,30 @@ void StoreLocalFlat(ValueEmitContext& ctx, const IR::Inst& inst) {
|
|||
|
||||
void StoreWordPrepared(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem,
|
||||
const MemoryResourceAccess& resource, uint32_t data) {
|
||||
const auto index = DwordIndex(ctx, inst, mem);
|
||||
EmitIfCondition(ctx.state, EmitMemoryElementInBounds(ctx.state, resource, index), [&]() {
|
||||
auto& state = ctx.state;
|
||||
const auto source = mem.kind == IR::ResourceKind::Buffer
|
||||
? state.program.info.buffers[mem.resource].source : UINT32_MAX;
|
||||
const auto& sources = state.program.descriptor_sources;
|
||||
const auto* indirect = source < sources.size() && sources[source].indirect_descriptor
|
||||
? &*sources[source].indirect_descriptor : nullptr;
|
||||
uint32_t index = 0;
|
||||
uint32_t in_bounds = 0;
|
||||
if (indirect != nullptr && indirect->indexed_store) {
|
||||
// The resource plan proves stride-four, index-only stores. Check the original
|
||||
// index before any byte multiplication can wrap it into the buffer.
|
||||
const auto prefix = Binary(state, spv::OpShiftRightLogical, TypeU32(state),
|
||||
resource.byte_offset, ConstantU32(state, 2));
|
||||
const auto guest_index = ctx.Arg(inst, 1);
|
||||
const auto records = Binary(state, spv::OpISub, TypeU32(state), resource.length, prefix);
|
||||
in_bounds = AndCondition(state,
|
||||
Binary(state, spv::OpULessThanEqual, TypeBool(state), prefix, resource.length),
|
||||
Binary(state, spv::OpULessThan, TypeBool(state), guest_index, records));
|
||||
index = EmitAddU32(state, guest_index, prefix);
|
||||
} else {
|
||||
index = DwordIndex(ctx, inst, mem, resource.byte_offset);
|
||||
in_bounds = EmitMemoryElementInBounds(state, resource, index);
|
||||
}
|
||||
EmitIfCondition(state, in_bounds, [&]() {
|
||||
StoreWordInBounds(ctx, resource, index, data);
|
||||
});
|
||||
}
|
||||
|
|
@ -576,6 +594,15 @@ void StoreWord(ValueEmitContext& ctx, const IR::Inst& inst, IR::MemoryInfo mem)
|
|||
const auto selected = EmitIndirectResourceIndex(
|
||||
state, ctx.Arg(*handle, 0), buffer.indirect_mapping_offset,
|
||||
buffer.indirect_search_iterations, UINT32_MAX);
|
||||
if (UsesSharedIndirectBufferStore(state, mem.resource)) {
|
||||
EmitIfCondition(state, Binary(state, spv::OpULessThan, TypeBool(state), selected,
|
||||
ConstantU32(state, buffer.indirect_resources.size())), [&]() {
|
||||
const auto slot = EmitAddU32(state, selected, ConstantU32(state, buffer.descriptor_index));
|
||||
const auto access = PrepareStorageBufferResourceAccess(state, mem, state.storage_buffers[2], slot);
|
||||
StoreWordPrepared(ctx, inst, mem, access, data);
|
||||
});
|
||||
return;
|
||||
}
|
||||
const auto merge = state.builder.AllocateId();
|
||||
std::vector<uint32_t> labels(buffer.indirect_resources.size());
|
||||
std::vector<uint32_t> branches {spv::OpSwitch, selected, merge};
|
||||
|
|
@ -599,7 +626,12 @@ template <typename Fn>
|
|||
uint32_t EmitAtomicAccess(ValueEmitContext& ctx, const IR::Inst& inst,
|
||||
const IR::MemoryInfo& mem, Fn&& operation) {
|
||||
return EmitValueOrZeroIfCondition(ctx.state, ctx.Arg(inst, inst.NumArgs() - 1), [&]() {
|
||||
const auto index = DwordIndex(ctx, inst, mem);
|
||||
auto index = DwordIndex(ctx, inst, mem);
|
||||
if (mem.kind == IR::ResourceKind::Lds || mem.kind == IR::ResourceKind::Gds) {
|
||||
// Keep region-address truncation local to DS atomics.
|
||||
index = Binary(ctx.state, spv::OpBitwiseAnd, TypeU32(ctx.state), index,
|
||||
ConstantU32(ctx.state, 0x3fffu));
|
||||
}
|
||||
const auto resource = PrepareMemoryResourceAccess(ctx.state, mem);
|
||||
return EmitValueOrZeroIfCondition(
|
||||
ctx.state, EmitMemoryElementInBounds(ctx.state, resource, index), [&]() {
|
||||
|
|
@ -735,53 +767,6 @@ uint32_t FormattedLoad(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Me
|
|||
});
|
||||
}
|
||||
|
||||
uint32_t EncodeFormattedStoreComponent(ValueEmitContext& ctx,
|
||||
const Format::BufferFormatInfo& info,
|
||||
uint32_t component, uint32_t data, uint32_t data_bits) {
|
||||
auto& state = ctx.state;
|
||||
const auto bits = info.component_bits[component];
|
||||
if (data_bits == 16u) {
|
||||
if (info.type == Format::ComponentType::Uint) {
|
||||
data = EmitBitFieldUExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16));
|
||||
} else if (info.type == Format::ComponentType::Sint) {
|
||||
data = EmitBitFieldSExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16));
|
||||
} else {
|
||||
data = EmitBitCastU32F32(state, EmitF16BitsToF32(state, data));
|
||||
}
|
||||
}
|
||||
const bool normalized = info.type == Format::ComponentType::Unorm ||
|
||||
info.type == Format::ComponentType::Snorm;
|
||||
const bool scaled = info.type == Format::ComponentType::Uscaled ||
|
||||
info.type == Format::ComponentType::Sscaled;
|
||||
if (normalized || scaled) {
|
||||
// Two-bit packed channels are unsigned even in signed formats.
|
||||
const bool is_signed = bits != 2u && (info.type == Format::ComponentType::Snorm ||
|
||||
info.type == Format::ComponentType::Sscaled);
|
||||
const auto max_value = static_cast<float>((1u << (bits - (is_signed ? 1u : 0u))) - 1u);
|
||||
const auto lower = normalized ? (is_signed ? -1.0f : 0.0f)
|
||||
: (is_signed ? -max_value - 1.0f : 0.0f);
|
||||
const auto upper = normalized ? 1.0f : max_value;
|
||||
auto value = Select(state, TypeF32(state), EmitClassifyF32Bits(state, data).nan,
|
||||
ConstantF32Value(state, 0.0f), EmitBitCastF32U32(state, data));
|
||||
value = EmitGlsl<GLSLstd450FClamp, IR::Type::F32>(
|
||||
state, value, ConstantF32Value(state, lower), ConstantF32Value(state, upper));
|
||||
if (normalized) {
|
||||
value = EmitFPRoundEven32(
|
||||
state, EmitFPMul32(state, value, ConstantF32Value(state, max_value)));
|
||||
}
|
||||
// Float-to-integer conversion truncates scaled values toward zero.
|
||||
value = Unary(state, is_signed ? spv::OpConvertFToS : spv::OpConvertFToU,
|
||||
is_signed ? TypeI32(state) : TypeU32(state), value);
|
||||
return is_signed ? Unary(state, spv::OpBitcast, TypeU32(state), value) : value;
|
||||
}
|
||||
if (bits == 16u && info.type == Format::ComponentType::Float) {
|
||||
const auto pair = EmitCompositeConstructF32x2(
|
||||
state, EmitBitCastF32U32(state, data), ConstantF32Value(state, 0.0f));
|
||||
return EmitPackHalf2x16(state, pair);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
void StoreFormattedPrepared(ValueEmitContext& ctx, const IR::Inst& inst,
|
||||
const IR::MemoryInfo& mem, const MemoryResourceAccess& resource,
|
||||
const Format::BufferFormatInfo& info, uint32_t data,
|
||||
|
|
@ -800,7 +785,7 @@ void StoreFormattedPrepared(ValueEmitContext& ctx, const IR::Inst& inst,
|
|||
value, data, component);
|
||||
}
|
||||
}
|
||||
value = EncodeFormattedStoreComponent(ctx, info, component, value, mem.data_bits);
|
||||
value = EncodeFormattedStoreComponent(ctx.state, info, component, value, mem.data_bits);
|
||||
const auto bits = info.component_bits[component];
|
||||
if (info.packed_bitfield) {
|
||||
packed = EmitBitFieldInsert(
|
||||
|
|
@ -926,10 +911,9 @@ uint32_t IndirectBufferInBounds(EmitterState& state, const IndirectBufferAccess&
|
|||
state, spv::OpIAdd, TypeU32(state), buffer.offset, ConstantU32(state, displacement));
|
||||
const auto has_payload = [&](uint32_t size) {
|
||||
return AndCondition(
|
||||
state, Binary(state, spv::OpUGreaterThanEqual, TypeBool(state), size,
|
||||
ConstantU32(state, bytes)),
|
||||
state, Binary(state, spv::OpUGreaterThanEqual, TypeBool(state), size, bytes),
|
||||
Binary(state, spv::OpULessThanEqual, TypeBool(state), offset,
|
||||
Binary(state, spv::OpISub, TypeU32(state), size, ConstantU32(state, bytes))));
|
||||
Binary(state, spv::OpISub, TypeU32(state), size, bytes)));
|
||||
};
|
||||
const auto structured = AndCondition(
|
||||
state, buffer.index_in_bounds,
|
||||
|
|
@ -970,7 +954,8 @@ uint32_t LoadIndirectBuffer(ValueEmitContext& ctx, const IR::Inst& inst, uint32_
|
|||
ConstantU64(state, component * 4u));
|
||||
values[component] = EmitValueOrZeroIfCondition(
|
||||
state, AndCondition(state, valid_format,
|
||||
IndirectBufferInBounds(state, buffer, component * 4u, 4u, false)),
|
||||
IndirectBufferInBounds(state, buffer, component * 4u,
|
||||
ConstantU32(state, 4u), false)),
|
||||
[&]() { return LoadBdaDword(ctx, address); });
|
||||
}
|
||||
return ConstructU32Composite(state, components, values);
|
||||
|
|
@ -980,9 +965,54 @@ uint32_t LoadIndirectFormattedX(ValueEmitContext& ctx, const IR::Inst& inst) {
|
|||
auto& state = ctx.state;
|
||||
return EmitValueOrZeroIfCondition(state, ctx.Arg(inst, inst.NumArgs() - 1), [&]() {
|
||||
const auto buffer = PrepareIndirectBuffer(ctx, inst);
|
||||
constexpr auto format_count =
|
||||
static_cast<uint32_t>(Prospero::BufferFormat::k32_32_32_32Float) + 1u;
|
||||
const auto geometry = EmitIndexSwitch(
|
||||
state, buffer.format, format_count, TypeU32Composite(state, 4), [&](uint32_t format) {
|
||||
const auto info = Format::GetFormatInfo(static_cast<Prospero::BufferFormat>(format));
|
||||
return state.builder.Constant(
|
||||
spv::OpConstantComposite, TypeU32Composite(state, 4),
|
||||
ConstantU32(state, info.byte_size),
|
||||
ConstantU32(state, std::max(1u, info.component_count)),
|
||||
ConstantU32(state, info.packed_bitfield ? 0u : info.component_bits[0] / 8u),
|
||||
ConstantU32(state, info.byte_size == 0u ? 0u : std::min(4u, info.byte_size) - 1u));
|
||||
});
|
||||
const auto field = [&](uint32_t index) {
|
||||
const auto value = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), value, geometry, index);
|
||||
return value;
|
||||
};
|
||||
const auto bytes = field(0);
|
||||
const auto component = Binary(
|
||||
state, spv::OpUMod, TypeU32(state),
|
||||
Binary(state, spv::OpISub, TypeU32(state), buffer.selector, ConstantU32(state, 4)), field(1));
|
||||
const auto alignment = Unary(state, spv::OpUConvert, TypeU64(state), field(3));
|
||||
const auto base = Binary(state, spv::OpBitwiseAnd, TypeU64(state), buffer.address,
|
||||
Unary(state, spv::OpNot, TypeU64(state), alignment));
|
||||
const auto address = Binary(
|
||||
state, spv::OpIAdd, TypeU64(state), base,
|
||||
Unary(state, spv::OpUConvert, TypeU64(state),
|
||||
Binary(state, spv::OpIMul, TypeU32(state), component, field(2))));
|
||||
const auto memory = AndCondition(
|
||||
state, Binary(state, spv::OpINotEqual, TypeBool(state), bytes, ConstantU32(state, 0)),
|
||||
Binary(state, spv::OpUGreaterThanEqual, TypeBool(state), buffer.selector,
|
||||
ConstantU32(state, 4)));
|
||||
const auto raw = EmitValueOrZeroIfCondition(
|
||||
state, AndCondition(state, memory, IndirectBufferInBounds(state, buffer, 0u, bytes, true)),
|
||||
[&]() {
|
||||
// Format alignment and component spacing keep every selected component within one DWORD.
|
||||
const auto word = LoadBdaDword(
|
||||
ctx, Binary(state, spv::OpBitwiseAnd, TypeU64(state), address,
|
||||
ConstantU64(state, ~uint64_t {3})));
|
||||
const auto byte = Binary(
|
||||
state, spv::OpBitwiseAnd, TypeU32(state),
|
||||
Unary(state, spv::OpUConvert, TypeU32(state), address), ConstantU32(state, 3));
|
||||
return Binary(state, spv::OpShiftRightLogical, TypeU32(state), word,
|
||||
Binary(state, spv::OpShiftLeftLogical, TypeU32(state), byte,
|
||||
ConstantU32(state, 3)));
|
||||
});
|
||||
return EmitIndexSwitch(
|
||||
state, buffer.format, static_cast<uint32_t>(Prospero::BufferFormat::k32_32_32_32Float) + 1u,
|
||||
TypeU32(state), [&](uint32_t format) {
|
||||
state, buffer.format, format_count, TypeU32(state), [&](uint32_t format) {
|
||||
const auto info = Format::GetFormatInfo(static_cast<Prospero::BufferFormat>(format));
|
||||
if (info.type == Format::ComponentType::Unknown) return ConstantU32(state, 0);
|
||||
const auto constant = Select(
|
||||
|
|
@ -993,28 +1023,16 @@ uint32_t LoadIndirectFormattedX(ValueEmitContext& ctx, const IR::Inst& inst) {
|
|||
state, Binary(state, spv::OpUGreaterThanEqual, TypeBool(state), buffer.selector,
|
||||
ConstantU32(state, 4)),
|
||||
TypeU32(state), constant, [&]() {
|
||||
const auto in_bounds = IndirectBufferInBounds(state, buffer, 0u, info.byte_size, true);
|
||||
const auto base = Binary(
|
||||
state, spv::OpBitwiseAnd, TypeU64(state), buffer.address,
|
||||
ConstantU64(state, ~(uint64_t(std::min(4u, info.byte_size)) - 1u)));
|
||||
const auto load = [&](uint32_t component, uint32_t bits) {
|
||||
const auto offset = Format::GetFormatComponentByteOffset(info, component);
|
||||
const auto address = offset == 0u
|
||||
? base
|
||||
: Binary(state, spv::OpIAdd, TypeU64(state),
|
||||
base, ConstantU64(state, offset));
|
||||
return LoadBda(ctx, address, in_bounds, bits);
|
||||
};
|
||||
const auto emit_component = [&](uint32_t component) {
|
||||
return LoadFormattedComponent(
|
||||
ctx, info, {FormattedSourceKind::Memory, component},
|
||||
[&](uint32_t source) { return load(source, 32u); }, load);
|
||||
[&](uint32_t) { return raw; },
|
||||
[&](uint32_t, uint32_t bits) {
|
||||
return Binary(state, spv::OpBitwiseAnd, TypeU32(state), raw,
|
||||
ConstantU32(state, bits == 8u ? 0xffu : 0xffffu));
|
||||
});
|
||||
};
|
||||
if (info.component_count == 1u) return emit_component(0u);
|
||||
const auto component = Binary(
|
||||
state, spv::OpUMod, TypeU32(state),
|
||||
Binary(state, spv::OpISub, TypeU32(state), buffer.selector, ConstantU32(state, 4)),
|
||||
ConstantU32(state, info.component_count));
|
||||
if (!info.packed_bitfield) return emit_component(0u);
|
||||
return EmitIndexSwitch(state, component, info.component_count, TypeU32(state),
|
||||
emit_component);
|
||||
});
|
||||
|
|
@ -1222,32 +1240,48 @@ void EmitSharedAtomic64(ValueEmitContext& ctx, const IR::Inst& inst) {
|
|||
auto& state = ctx.state;
|
||||
const auto& mem = ctx.Memory(inst);
|
||||
EnsureLdsStorage(state);
|
||||
EmitIfCondition(state, ctx.Arg(inst, 2), [&]() {
|
||||
const auto address = Binary(state, spv::OpBitwiseAnd, TypeU32(state),
|
||||
ByteAddress(ctx, inst, mem), ConstantU32(state, 0xfff8u));
|
||||
const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address,
|
||||
ConstantU32(state, 3u));
|
||||
const auto in_bounds = Binary(state, spv::OpULessThan, TypeBool(state), index,
|
||||
ConstantU32(state, LdsDwordCount(state) / 2u));
|
||||
EmitIfCondition(state, in_bounds, [&]() {
|
||||
auto native_index = index;
|
||||
auto semantics = spv::MemorySemanticsWorkgroupMemoryMask;
|
||||
if (state.lds_storage_class == spv::StorageClassStorageBuffer) {
|
||||
native_index = EmitAddU32(state, index, EmitBinaryU32(
|
||||
state, spv::OpShiftRightLogical, state.lds_base_dwords, ConstantU32(state, 1)));
|
||||
semantics = spv::MemorySemanticsUniformMemoryMask;
|
||||
}
|
||||
const auto pointer = state.builder.AllocateId();
|
||||
const auto address = Binary(state, spv::OpBitwiseAnd, TypeU32(state),
|
||||
ByteAddress(ctx, inst, mem), ConstantU32(state, 0xfff8u));
|
||||
const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address,
|
||||
ConstantU32(state, 3u));
|
||||
const auto in_bounds = Binary(state, spv::OpULessThan, TypeBool(state), index,
|
||||
ConstantU32(state, LdsDwordCount(state) / 2u));
|
||||
const auto active = EmitLogicalAndBool(state, ctx.Arg(inst, inst.NumArgs() - 1), in_bounds);
|
||||
const auto emit = [&]() {
|
||||
auto native_index = index;
|
||||
auto semantics = spv::MemorySemanticsWorkgroupMemoryMask;
|
||||
if (state.lds_storage_class == spv::StorageClassStorageBuffer) {
|
||||
native_index = EmitAddU32(state, index, EmitBinaryU32(
|
||||
state, spv::OpShiftRightLogical, state.lds_base_dwords, ConstantU32(state, 1)));
|
||||
semantics = spv::MemorySemanticsUniformMemoryMask;
|
||||
}
|
||||
const auto pointer = state.builder.AllocateId();
|
||||
state.builder.AddFunction(
|
||||
spv::OpAccessChain, TypePointer(state, state.lds_storage_class, TypeU64(state)),
|
||||
pointer, state.lds_u64_variable, ConstantU32(state, 0), native_index);
|
||||
const auto old = state.builder.AllocateId();
|
||||
const auto opcode = SpirvAtomicOpcode(inst.GetOpcode());
|
||||
const auto scope = ConstantU32(state, spv::ScopeWorkgroup);
|
||||
const auto acquire_release =
|
||||
ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | semantics);
|
||||
if (opcode == spv::OpAtomicCompareExchange) {
|
||||
// RDNA2 DS compares DATA and stores DATA2, opposite to BUFFER atomics.
|
||||
state.builder.AddFunction(
|
||||
spv::OpAccessChain, TypePointer(state, state.lds_storage_class, TypeU64(state)),
|
||||
pointer, state.lds_u64_variable, ConstantU32(state, 0), native_index);
|
||||
const auto value = ctx.Arg(inst, 1);
|
||||
state.builder.AddFunction(
|
||||
SpirvAtomicOpcode(inst.GetOpcode()), TypeU64(state), state.builder.AllocateId(),
|
||||
pointer, ConstantU32(state, spv::ScopeWorkgroup),
|
||||
ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | semantics), value);
|
||||
});
|
||||
});
|
||||
opcode, TypeU64(state), old, pointer, scope, acquire_release,
|
||||
ConstantU32(state, spv::MemorySemanticsAcquireMask | semantics),
|
||||
ctx.Arg(inst, 2), ctx.Arg(inst, 1));
|
||||
} else {
|
||||
state.builder.AddFunction(opcode, TypeU64(state), old, pointer, scope,
|
||||
acquire_release, ctx.Arg(inst, 1));
|
||||
}
|
||||
return old;
|
||||
};
|
||||
if (inst.HasUses()) {
|
||||
ctx.Define(inst, EmitValueOrDefaultIfCondition(
|
||||
state, active, TypeU64(state), ConstantU64(state, 0), emit));
|
||||
} else {
|
||||
EmitIfCondition(state, active, emit);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t EmitBufferFloatAtomic(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
#include "graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h"
|
||||
#include "graphics/shader/recompiler/backend/spirv/spirvEmitterInstructions.h"
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter {
|
||||
|
||||
uint32_t EmitShaderDataDwordLoad(EmitterState& state, uint32_t dword_index) {
|
||||
static uint32_t EmitShaderDataDwordLoadAt(EmitterState& state, uint32_t dword_index) {
|
||||
if (state.program.bindings.UsesPushData()) {
|
||||
dword_index += state.program.bindings.push_data_start_dword;
|
||||
const auto pointer = state.builder.AllocateId();
|
||||
const auto value = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpAccessChain, TypePushConstantElementPointer(state),
|
||||
pointer, state.push_constant_variable, ConstantU32(state, 0),
|
||||
ConstantU32(state, dword_index));
|
||||
dword_index);
|
||||
state.builder.AddFunction(spv::OpLoad, TypeU32(state), value, pointer);
|
||||
return value;
|
||||
}
|
||||
|
|
@ -18,13 +17,20 @@ uint32_t EmitShaderDataDwordLoad(EmitterState& state, uint32_t dword_index) {
|
|||
const auto value = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state),
|
||||
pointer, state.shader_data_storage_variable,
|
||||
ConstantU32(state, 0), ConstantU32(state, dword_index));
|
||||
ConstantU32(state, 0), dword_index);
|
||||
state.builder.AddFunction(spv::OpLoad, TypeU32(state), value, pointer);
|
||||
return value;
|
||||
}
|
||||
return ConstantU32(state, 0);
|
||||
}
|
||||
|
||||
uint32_t EmitShaderDataDwordLoad(EmitterState& state, uint32_t dword_index) {
|
||||
if (state.program.bindings.UsesPushData()) {
|
||||
dword_index += state.program.bindings.push_data_start_dword;
|
||||
}
|
||||
return EmitShaderDataDwordLoadAt(state, ConstantU32(state, dword_index));
|
||||
}
|
||||
|
||||
uint32_t EmitBinaryU32(EmitterState& state, spv::Op opcode, uint32_t lhs, uint32_t rhs) {
|
||||
const auto ret = state.builder.AllocateId();
|
||||
state.builder.AddFunction(opcode, TypeU32(state), ret, lhs, rhs);
|
||||
|
|
@ -102,12 +108,38 @@ Prospero::BufferFormat StorageBufferFormat(const EmitterState& state, const IR::
|
|||
return state.program.info.buffers[mem.resource].descriptor_format;
|
||||
}
|
||||
|
||||
bool UsesSharedIndirectBufferStore(const EmitterState& state, uint32_t resource) {
|
||||
const auto& buffers = state.program.info.buffers;
|
||||
const auto& root = buffers[resource];
|
||||
if (root.indirect_root != resource || root.descriptor_index == UINT32_MAX ||
|
||||
root.packed_stride == 0u || root.indirect_resources.empty()) {
|
||||
return false;
|
||||
}
|
||||
uint32_t slot = root.descriptor_index;
|
||||
for (const auto candidate: root.indirect_resources) {
|
||||
if (buffers[candidate].packed_stride != root.packed_stride ||
|
||||
buffers[candidate].descriptor_index != slot++) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void EmitMemoryOffsets(EmitterState& state) {
|
||||
std::array<bool, IR::ShaderInfo::MaxBuffers> dynamic_offsets {};
|
||||
for (uint32_t resource = 0; resource < state.program.info.buffers.size(); ++resource) {
|
||||
if (!UsesSharedIndirectBufferStore(state, resource)) continue;
|
||||
for (const auto candidate: state.program.info.buffers[resource].indirect_resources) {
|
||||
dynamic_offsets[state.program.info.buffers[candidate].descriptor_index] = true;
|
||||
}
|
||||
}
|
||||
const auto count = state.program.bindings.descriptor_counts[static_cast<size_t>(IR::DescriptorBindingKind::Buffers)];
|
||||
for (uint32_t i = 0; i < count; i += 4u) {
|
||||
const auto lanes = std::min(4u, count - i);
|
||||
if (std::all_of(dynamic_offsets.begin() + i, dynamic_offsets.begin() + i + lanes,
|
||||
[](bool dynamic) { return dynamic; })) continue;
|
||||
const auto word =
|
||||
EmitShaderDataDwordLoad(state, state.program.bindings.memory_offset_dword + i / 4u);
|
||||
for (uint32_t lane = 0; lane < std::min(4u, count - i); ++lane) {
|
||||
for (uint32_t lane = 0; lane < lanes; ++lane) {
|
||||
if (dynamic_offsets[i + lane]) continue;
|
||||
state.memory_byte_offsets[i + lane] = EmitNative<spv::OpBitFieldUExtract, IR::Type::U32>(
|
||||
state, word, ConstantU32(state, lane * 8u), ConstantU32(state, 8u));
|
||||
}
|
||||
|
|
@ -152,7 +184,8 @@ void EnsureLdsStorage(EmitterState& state) {
|
|||
|
||||
MemoryResourceAccess PrepareStorageBufferResourceAccess(EmitterState& state,
|
||||
const IR::MemoryInfo& mem,
|
||||
BufferDefinition& buffer) {
|
||||
BufferDefinition& buffer,
|
||||
uint32_t array_index) {
|
||||
if (buffer.variable == 0) {
|
||||
ExitDescriptorBindingFailure(state, IR::DescriptorBindingKind::Buffers, mem.resource,
|
||||
"storage buffer descriptor array was not emitted");
|
||||
|
|
@ -160,15 +193,34 @@ MemoryResourceAccess PrepareStorageBufferResourceAccess(EmitterState& state,
|
|||
if (buffer.pointer_type == 0) {
|
||||
buffer.pointer_type = TypePointer(state, spv::StorageClassStorageBuffer, buffer.type);
|
||||
}
|
||||
const auto array_index =
|
||||
state.program.info.buffers.at(mem.resource).descriptor_index;
|
||||
const bool non_uniform = array_index != 0u;
|
||||
const auto static_index = state.program.info.buffers.at(mem.resource).descriptor_index;
|
||||
if (!non_uniform) array_index = ConstantU32(state, static_index);
|
||||
MemoryResourceAccess access {
|
||||
.kind = mem.kind,
|
||||
.non_uniform = non_uniform,
|
||||
.memory_access = mem.coherent ? spv::MemoryAccessVolatileMask : spv::MemoryAccessMaskNone};
|
||||
access.object_pointer = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpAccessChain, buffer.pointer_type, access.object_pointer,
|
||||
buffer.variable, ConstantU32(state, array_index));
|
||||
access.byte_offset = state.memory_byte_offsets[array_index];
|
||||
buffer.variable, array_index);
|
||||
if (non_uniform) {
|
||||
state.builder.RequireExtension("SPV_EXT_descriptor_indexing");
|
||||
state.builder.RequireCapability(spv::CapabilityShaderNonUniform);
|
||||
state.builder.RequireCapability(spv::CapabilityStorageBufferArrayNonUniformIndexing);
|
||||
state.builder.AddAnnotation(spv::OpDecorate, access.object_pointer, spv::DecorationNonUniform);
|
||||
const auto offsets_start = state.program.bindings.memory_offset_dword +
|
||||
(state.program.bindings.UsesPushData() ? state.program.bindings.push_data_start_dword : 0u);
|
||||
const auto word = EmitShaderDataDwordLoadAt(state, EmitAddU32(
|
||||
state, ConstantU32(state, offsets_start),
|
||||
Binary(state, spv::OpShiftRightLogical, TypeU32(state), array_index, ConstantU32(state, 2))));
|
||||
const auto shift = Binary(state, spv::OpShiftLeftLogical, TypeU32(state),
|
||||
Binary(state, spv::OpBitwiseAnd, TypeU32(state), array_index, ConstantU32(state, 3)),
|
||||
ConstantU32(state, 3));
|
||||
access.byte_offset = EmitNative<spv::OpBitFieldUExtract, IR::Type::U32>(
|
||||
state, word, shift, ConstantU32(state, 8));
|
||||
} else {
|
||||
access.byte_offset = state.memory_byte_offsets[static_index];
|
||||
}
|
||||
access.length = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpArrayLength, TypeU32(state), access.length,
|
||||
access.object_pointer, 0);
|
||||
|
|
@ -260,6 +312,9 @@ uint32_t EmitStorageBufferElementPointer(EmitterState& state,
|
|||
const auto pointer = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpAccessChain, pointer_type, pointer, access.object_pointer,
|
||||
ConstantU32(state, 0), index);
|
||||
if (access.non_uniform) {
|
||||
state.builder.AddAnnotation(spv::OpDecorate, pointer, spv::DecorationNonUniform);
|
||||
}
|
||||
return pointer;
|
||||
}
|
||||
|
||||
|
|
@ -362,8 +417,54 @@ uint32_t NormalizeFormatComponent(EmitterState& state, const Format::BufferForma
|
|||
}
|
||||
}
|
||||
|
||||
uint32_t EncodeFormattedStoreComponent(EmitterState& state, const Format::BufferFormatInfo& info,
|
||||
uint32_t component, uint32_t data, uint32_t data_bits) {
|
||||
const auto bits = info.component_bits[component];
|
||||
if (data_bits == 16u) {
|
||||
if (info.type == Format::ComponentType::Uint) {
|
||||
data = EmitBitFieldUExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16));
|
||||
} else if (info.type == Format::ComponentType::Sint) {
|
||||
data = EmitBitFieldSExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16));
|
||||
} else {
|
||||
data = EmitBitCastU32F32(state, EmitF16BitsToF32(state, data));
|
||||
}
|
||||
}
|
||||
const bool normalized = info.type == Format::ComponentType::Unorm ||
|
||||
info.type == Format::ComponentType::Snorm;
|
||||
const bool scaled = info.type == Format::ComponentType::Uscaled ||
|
||||
info.type == Format::ComponentType::Sscaled;
|
||||
if (normalized || scaled) {
|
||||
// Two-bit packed channels are unsigned even in signed formats.
|
||||
const bool is_signed = bits != 2u && (info.type == Format::ComponentType::Snorm ||
|
||||
info.type == Format::ComponentType::Sscaled);
|
||||
const auto max_value = static_cast<float>((1u << (bits - (is_signed ? 1u : 0u))) - 1u);
|
||||
const auto lower = normalized ? (is_signed ? -1.0f : 0.0f)
|
||||
: (is_signed ? -max_value - 1.0f : 0.0f);
|
||||
const auto upper = normalized ? 1.0f : max_value;
|
||||
auto value = Select(state, TypeF32(state), EmitClassifyF32Bits(state, data).nan,
|
||||
ConstantF32Value(state, 0.0f), EmitBitCastF32U32(state, data));
|
||||
value = EmitGlsl<GLSLstd450FClamp, IR::Type::F32>(
|
||||
state, value, ConstantF32Value(state, lower), ConstantF32Value(state, upper));
|
||||
if (normalized) {
|
||||
value = EmitFPRoundEven32(
|
||||
state, EmitFPMul32(state, value, ConstantF32Value(state, max_value)));
|
||||
}
|
||||
// Float-to-integer conversion truncates scaled values toward zero.
|
||||
value = Unary(state, is_signed ? spv::OpConvertFToS : spv::OpConvertFToU,
|
||||
is_signed ? TypeI32(state) : TypeU32(state), value);
|
||||
return is_signed ? Unary(state, spv::OpBitcast, TypeU32(state), value) : value;
|
||||
}
|
||||
if (bits == 16u && info.type == Format::ComponentType::Float) {
|
||||
const auto pair = EmitCompositeConstructF32x2(
|
||||
state, EmitBitCastF32U32(state, data), ConstantF32Value(state, 0.0f));
|
||||
return EmitPackHalf2x16(state, pair);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
|
||||
switch (opcode) {
|
||||
case IR::ValueOpcode::SharedAtomicCmpSwap64:
|
||||
case IR::ValueOpcode::ImageAtomicCompareSwap32:
|
||||
case IR::ValueOpcode::BufferAtomicCmpSwap32: return spv::OpAtomicCompareExchange;
|
||||
case IR::ValueOpcode::ImageAtomicSwap32:
|
||||
|
|
@ -391,7 +492,8 @@ spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
|
|||
case IR::ValueOpcode::ImageAtomicUMax32:
|
||||
case IR::ValueOpcode::ImageAtomicUMax64:
|
||||
case IR::ValueOpcode::BufferAtomicUMax32:
|
||||
case IR::ValueOpcode::SharedAtomicUMax32: return spv::OpAtomicUMax;
|
||||
case IR::ValueOpcode::SharedAtomicUMax32:
|
||||
case IR::ValueOpcode::SharedAtomicUMax64: return spv::OpAtomicUMax;
|
||||
case IR::ValueOpcode::ImageAtomicAnd32:
|
||||
case IR::ValueOpcode::ImageAtomicAnd64:
|
||||
case IR::ValueOpcode::BufferAtomicAnd32:
|
||||
|
|
|
|||
|
|
@ -351,6 +351,9 @@ void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
|||
if (a16) {
|
||||
inst.image_sample_flags |= ImageSampleFlagA16;
|
||||
}
|
||||
if ((sample != nullptr || gather != nullptr) && (word0 & (1u << 12u)) != 0u) {
|
||||
inst.image_sample_flags |= ImageSampleFlagUnnormalized;
|
||||
}
|
||||
inst.image_dimension = dimension;
|
||||
inst.image_r128 = r128;
|
||||
inst.image_nsa_dwords = nsa_dwords;
|
||||
|
|
|
|||
|
|
@ -116,7 +116,8 @@ constexpr MemoryOpcodeInfo DS_OPCODE_LIST[] = {
|
|||
{0x3fu, Opcode::DS_ORDERED_COUNT, 1, 32},
|
||||
{0x40u, Opcode::DS_ADD_U64, 2, 32}, {0x4au, Opcode::DS_OR_B64, 2, 32},
|
||||
{0x4du, Opcode::DS_WRITE_B64, 2, 32}, {0x4eu, Opcode::DS_WRITE2_B64, 4, 32},
|
||||
{0x4fu, Opcode::DS_WRITE2ST64_B64, 4, 32}, {0x76u, Opcode::DS_READ_B64, 2, 32},
|
||||
{0x4fu, Opcode::DS_WRITE2ST64_B64, 4, 32}, {0x68u, Opcode::DS_MAX_RTN_U64, 2, 32},
|
||||
{0x70u, Opcode::DS_CMPST_RTN_B64, 2, 32}, {0x76u, Opcode::DS_READ_B64, 2, 32},
|
||||
{0x77u, Opcode::DS_READ2_B64, 4, 32}, {0x78u, Opcode::DS_READ2ST64_B64, 4, 32},
|
||||
{0xa0u, Opcode::DS_WRITE_B8_D16_HI, 1, 8},
|
||||
{0xa1u, Opcode::DS_WRITE_B16_D16_HI, 1, 16},
|
||||
|
|
@ -192,6 +193,8 @@ bool IsDsAtomicOpcode(Opcode opcode) {
|
|||
case Opcode::DS_MIN_RTN_U32:
|
||||
case Opcode::DS_MAX_U32:
|
||||
case Opcode::DS_MAX_RTN_U32:
|
||||
case Opcode::DS_MAX_RTN_U64:
|
||||
case Opcode::DS_CMPST_RTN_B64:
|
||||
case Opcode::DS_AND_B32:
|
||||
case Opcode::DS_AND_RTN_B32:
|
||||
case Opcode::DS_OR_B32:
|
||||
|
|
@ -211,6 +214,7 @@ uint32_t DsSourceCount(Opcode opcode) {
|
|||
case Opcode::DS_WRITE2ST64_B32:
|
||||
case Opcode::DS_WRITE2_B64:
|
||||
case Opcode::DS_WRITE2ST64_B64:
|
||||
case Opcode::DS_CMPST_RTN_B64:
|
||||
case Opcode::DS_MSKOR_B32: return 3u;
|
||||
case Opcode::DS_MIN_F32:
|
||||
case Opcode::DS_MAX_F32:
|
||||
|
|
|
|||
|
|
@ -708,6 +708,8 @@ std::string InstructionToString(const Instruction& inst) {
|
|||
case Opcode::DS_MIN_RTN_U32:
|
||||
case Opcode::DS_MAX_U32:
|
||||
case Opcode::DS_MAX_RTN_U32:
|
||||
case Opcode::DS_MAX_RTN_U64:
|
||||
case Opcode::DS_CMPST_RTN_B64:
|
||||
case Opcode::DS_AND_B32:
|
||||
case Opcode::DS_AND_RTN_B32:
|
||||
case Opcode::DS_OR_B32:
|
||||
|
|
|
|||
|
|
@ -624,6 +624,8 @@ enum class Opcode {
|
|||
DS_MIN_RTN_U32,
|
||||
DS_MAX_U32,
|
||||
DS_MAX_RTN_U32,
|
||||
DS_MAX_RTN_U64,
|
||||
DS_CMPST_RTN_B64,
|
||||
DS_AND_B32,
|
||||
DS_AND_RTN_B32,
|
||||
DS_OR_B32,
|
||||
|
|
@ -762,6 +764,7 @@ enum ImageSampleFlag : uint32_t {
|
|||
ImageSampleFlagCd = 1u << 8u,
|
||||
ImageSampleFlagGatherHorizontal = 1u << 9u,
|
||||
ImageSampleFlagAdjust = 1u << 10u,
|
||||
ImageSampleFlagUnnormalized = 1u << 11u,
|
||||
};
|
||||
|
||||
enum class ImageDimension : uint32_t {
|
||||
|
|
|
|||
|
|
@ -578,11 +578,13 @@ void Translator::DS_ATOMIC(const Decoder::Instruction& inst, IR::ValueOpcode opc
|
|||
bool returns_value) {
|
||||
const auto memory = MemoryInfoFromDecoded(inst);
|
||||
const auto address = ReadU32(inst.src0);
|
||||
const IR::Value value =
|
||||
memory.data_dwords == 2u ? IR::Value(ReadU64(inst.src1)) : IR::Value(ReadU32(inst.src1));
|
||||
const auto read = [&](const Decoder::Operand& operand) -> IR::Value {
|
||||
return memory.data_dwords == 2u ? IR::Value(ReadU64(operand)) : IR::Value(ReadU32(operand));
|
||||
};
|
||||
const auto value = read(inst.src1);
|
||||
const auto flags = AddMemoryInfo(memory, inst.pc);
|
||||
const auto result = inst.src_count == 3u
|
||||
? ir.Emit(opcode, {address, value, ReadU32(inst.src2), ir.GetExec()}, flags)
|
||||
? ir.Emit(opcode, {address, value, read(inst.src2), ir.GetExec()}, flags)
|
||||
: ir.Emit(opcode, {address, value, ir.GetExec()}, flags);
|
||||
if (returns_value) {
|
||||
WriteOperand(inst.dst, result);
|
||||
|
|
@ -687,7 +689,8 @@ void Translator::IMAGE_SAMPLE(const Decoder::Instruction& inst) {
|
|||
const auto resource = GetImageResource(memory);
|
||||
const auto sampler = GetSamplerResource(memory);
|
||||
const auto address = MakeImageAddress(inst, MemorySourceAt(inst, 0));
|
||||
const auto result = ir.Emit(IR::ValueOpcode::ImageSampleRaw, {resource, sampler, address},
|
||||
const auto result = ir.Emit(IR::ValueOpcode::ImageSampleRaw,
|
||||
{resource, sampler, address, ir.GetExec()},
|
||||
AddMemoryInfo(memory, inst.pc));
|
||||
const bool dref = (memory.image_sample_flags & Decoder::ImageSampleFlagCompare) != 0u;
|
||||
if (dref && memory.data_bits != 16u) {
|
||||
|
|
@ -714,7 +717,7 @@ void Translator::IMAGE_GATHER(const Decoder::Instruction& inst) {
|
|||
has_lod ? resource.Instruction()->Arg(1) : IR::Value(0u),
|
||||
has_lod ? resource.Instruction()->Arg(3) : IR::Value(0u),
|
||||
has_lod ? sampler.Instruction()->Arg(1) : IR::Value(0u),
|
||||
has_lod ? sampler.Instruction()->Arg(2) : IR::Value(0u)},
|
||||
has_lod ? sampler.Instruction()->Arg(2) : IR::Value(0u), ir.GetExec()},
|
||||
AddMemoryInfo(memory, inst.pc));
|
||||
for (uint32_t index = 0; index < memory.data_dwords; index++) {
|
||||
WriteOperand(OffsetOperand(inst.dst, index),
|
||||
|
|
@ -1072,6 +1075,10 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
|||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicUMax32, false);
|
||||
case Decoder::Opcode::DS_MAX_RTN_U32:
|
||||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicUMax32, true);
|
||||
case Decoder::Opcode::DS_MAX_RTN_U64:
|
||||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicUMax64, true);
|
||||
case Decoder::Opcode::DS_CMPST_RTN_B64:
|
||||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicCmpSwap64, true);
|
||||
case Decoder::Opcode::DS_AND_B32:
|
||||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicAnd32, false);
|
||||
case Decoder::Opcode::DS_AND_RTN_B32:
|
||||
|
|
|
|||
|
|
@ -948,9 +948,14 @@ void IncludeInstructionVectorRegisters(const Decoder::Instruction& inst, uint32_
|
|||
switch (inst.opcode) {
|
||||
case Decoder::Opcode::DS_ADD_U64:
|
||||
case Decoder::Opcode::DS_OR_B64:
|
||||
case Decoder::Opcode::DS_MAX_RTN_U64:
|
||||
case Decoder::Opcode::DS_WRITE_B64:
|
||||
case Decoder::Opcode::DS_WRITE_B96:
|
||||
case Decoder::Opcode::DS_WRITE_B128: include_vector(inst.src1, inst.data_dwords); break;
|
||||
case Decoder::Opcode::DS_CMPST_RTN_B64:
|
||||
include_vector(inst.src1, 2u);
|
||||
include_vector(inst.src2, 2u);
|
||||
break;
|
||||
case Decoder::Opcode::DS_WRITE2_B32:
|
||||
case Decoder::Opcode::DS_WRITE2ST64_B32:
|
||||
case Decoder::Opcode::DS_WRITE2_B64:
|
||||
|
|
@ -1163,9 +1168,11 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
|
|||
}
|
||||
entry_ir.SetExec(initial_exec);
|
||||
const auto initial_mask = entry_ir.Emit(IR::ValueOpcode::Ballot, {initial_exec});
|
||||
entry_ir.SetExecLo(entry_ir.CompositeExtract(initial_mask, 0));
|
||||
entry_ir.SetExecHi(options.wave_size == 64u ? entry_ir.CompositeExtract(initial_mask, 1)
|
||||
: IR::U32(IR::Value(0u)));
|
||||
const auto initial_lo = entry_ir.CompositeExtract(initial_mask, 0);
|
||||
const auto initial_hi = options.wave_size == 64u ? entry_ir.CompositeExtract(initial_mask, 1)
|
||||
: IR::U32(IR::Value(0u));
|
||||
entry_ir.SetExecLo(initial_lo);
|
||||
entry_ir.SetExecHi(initial_hi);
|
||||
if (options.stage == ShaderType::Compute) {
|
||||
const auto* cs = options.input_info.compute;
|
||||
const auto thread_ids =
|
||||
|
|
@ -1381,11 +1388,16 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
|
|||
}
|
||||
} else if (options.stage == ShaderType::Vertex) {
|
||||
// Vulkan owns primitive assembly; each vertex subgroup is one NGG wave.
|
||||
// Keep its full lane extent: mbcnt(-1) uses lane ordinals, not active counts.
|
||||
// ESVertCount describes input vertices, independently of lane ordinals.
|
||||
auto vertex_count = IR::U32(entry_ir.Emit(IR::ValueOpcode::BitCount32, {initial_lo}));
|
||||
if (options.wave_size == 64u) {
|
||||
vertex_count = entry_ir.IAdd(vertex_count, IR::U32(entry_ir.Emit(
|
||||
IR::ValueOpcode::BitCount32, {initial_hi})));
|
||||
}
|
||||
entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(2),
|
||||
IR::U32(IR::Value(options.wave_size << 12u)));
|
||||
entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(3),
|
||||
IR::U32(IR::Value((1u << 28u) | options.wave_size)));
|
||||
entry_ir.BitwiseOr(vertex_count, IR::U32(IR::Value(1u << 28u))));
|
||||
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(5),
|
||||
builtin(IR::StageInputKind::VertexIndex));
|
||||
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(8),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include "graphics/shader/recompiler/BufferFormat.h"
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <xbyak/xbyak.h>
|
||||
#include <map>
|
||||
#include <new>
|
||||
#include <unordered_map>
|
||||
|
|
@ -99,6 +100,9 @@ bool EquivalentValue(const ResourcePlan& program, Value left, Value right,
|
|||
|
||||
} // namespace
|
||||
|
||||
ResourcePlan::ResourcePlan() = default;
|
||||
ResourcePlan::ResourcePlan(ResourcePlan&&) noexcept = default;
|
||||
|
||||
ResourcePlan::~ResourcePlan() {
|
||||
for (auto& inst: value_storage) {
|
||||
inst.Invalidate();
|
||||
|
|
|
|||
|
|
@ -19,10 +19,20 @@
|
|||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Xbyak {
|
||||
class CodeGenerator;
|
||||
}
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::IR {
|
||||
|
||||
class SrtWalker;
|
||||
|
||||
// Finite descriptor domains also bound the arithmetic used to prove their indices.
|
||||
constexpr uint32_t MaxIndirectDescriptorProbes = 65536u;
|
||||
|
||||
enum class ResourceKind {
|
||||
None,
|
||||
ScalarBuffer,
|
||||
|
|
@ -497,9 +507,23 @@ struct ShaderInfo {
|
|||
struct DescriptorSource {
|
||||
struct IndirectDescriptor {
|
||||
struct SelectorRead {
|
||||
struct CountedRows {
|
||||
uint32_t source = UINT32_MAX;
|
||||
uint32_t stride = 0; // Byte spacing between selector rows, not their length.
|
||||
|
||||
bool operator==(const CountedRows& other) const = default;
|
||||
};
|
||||
uint32_t source = UINT32_MAX;
|
||||
uint32_t stride = 0;
|
||||
uint32_t offset = 0;
|
||||
uint32_t maximum = UINT32_MAX;
|
||||
uint32_t bit_offset = 0;
|
||||
uint32_t bit_mask = UINT32_MAX;
|
||||
bool global = false;
|
||||
// Four DWORD bitmap words per record; set-bit ranks subtract from the last key.
|
||||
bool bitset = false;
|
||||
std::optional<CountedRows> rows;
|
||||
std::vector<std::pair<uint32_t, uint32_t>> byte_ranges;
|
||||
|
||||
bool operator==(const SelectorRead& other) const = default;
|
||||
};
|
||||
|
|
@ -510,8 +534,11 @@ struct DescriptorSource {
|
|||
uint32_t table_stride = 0;
|
||||
uint32_t table_record_bytes = 0;
|
||||
bool table_scalar = false;
|
||||
bool indexed_store = false;
|
||||
bool signed_count = false;
|
||||
uint32_t workgroup_axis = UINT32_MAX;
|
||||
Value key_count;
|
||||
// First selector index, or first descriptor key for a direct or bitmap range.
|
||||
Value selector_first;
|
||||
Value selector_mask;
|
||||
std::vector<uint32_t> sources;
|
||||
|
|
@ -571,14 +598,17 @@ struct ResourcePlan {
|
|||
|
||||
std::vector<Entry> values;
|
||||
uint64_t generation = 0;
|
||||
const SrtWalker* session = nullptr;
|
||||
Value active_mask;
|
||||
uint8_t read_policy = 0;
|
||||
};
|
||||
|
||||
ResourcePlan() = default;
|
||||
ResourcePlan();
|
||||
~ResourcePlan();
|
||||
|
||||
ResourcePlan(const ResourcePlan&) = delete;
|
||||
ResourcePlan& operator=(const ResourcePlan&) = delete;
|
||||
ResourcePlan(ResourcePlan&&) noexcept = default;
|
||||
ResourcePlan(ResourcePlan&&) noexcept;
|
||||
ResourcePlan& operator=(ResourcePlan&& other) noexcept;
|
||||
|
||||
ShaderType stage = ShaderType::Unknown;
|
||||
|
|
@ -589,6 +619,7 @@ struct ResourcePlan {
|
|||
std::vector<MemoryInfo> memory_info;
|
||||
std::vector<DescriptorSource> descriptor_sources;
|
||||
std::vector<ResourceBlock> control_flow;
|
||||
std::unordered_map<const Inst*, std::vector<uint32_t>> control_phi_predecessors;
|
||||
std::vector<SrtRead> srt_reads;
|
||||
bool requires_specialization_memory = false;
|
||||
std::vector<std::pair<uint64_t, uint64_t>> source_reads;
|
||||
|
|
@ -597,12 +628,13 @@ struct ResourcePlan {
|
|||
bool resource_tracking_complete = false;
|
||||
ShaderInfo info;
|
||||
UniformFillPlan uniform_fill;
|
||||
std::unique_ptr<Xbyak::CodeGenerator> srt_code;
|
||||
// GPU-thread scratch for nested clean/EXEC memos, activity and material keys.
|
||||
mutable std::deque<EvaluationContext> evaluation_contexts;
|
||||
mutable uint32_t evaluation_value_count = 0;
|
||||
mutable uint32_t evaluation_depth = 0;
|
||||
mutable std::vector<uint8_t> active_sources;
|
||||
mutable std::vector<uint8_t> visited_blocks;
|
||||
mutable std::vector<uint8_t> reachable_blocks;
|
||||
mutable std::vector<uint32_t> pending_blocks;
|
||||
mutable std::vector<uint32_t> material_keys;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -175,6 +175,8 @@ SharedAccess SharedAccessOf(ValueOpcode opcode) {
|
|||
case ValueOpcode::SharedAtomicUMin32:
|
||||
case ValueOpcode::SharedAtomicSMax32:
|
||||
case ValueOpcode::SharedAtomicUMax32:
|
||||
case ValueOpcode::SharedAtomicUMax64:
|
||||
case ValueOpcode::SharedAtomicCmpSwap64:
|
||||
case ValueOpcode::SharedAtomicAnd32:
|
||||
case ValueOpcode::SharedAtomicOr32:
|
||||
case ValueOpcode::SharedAtomicOr64:
|
||||
|
|
@ -190,6 +192,8 @@ uint32_t SharedComponentCount(ValueOpcode opcode) {
|
|||
switch (opcode) {
|
||||
case ValueOpcode::SharedAtomicIAdd64:
|
||||
case ValueOpcode::SharedAtomicOr64:
|
||||
case ValueOpcode::SharedAtomicUMax64:
|
||||
case ValueOpcode::SharedAtomicCmpSwap64:
|
||||
case ValueOpcode::LoadSharedU32x2:
|
||||
case ValueOpcode::WriteSharedU32x2: return 2u;
|
||||
case ValueOpcode::LoadSharedU32x3:
|
||||
|
|
|
|||
|
|
@ -303,6 +303,8 @@ VALUE_OPCODE(SharedAtomicSMin32, U32, U32, U32, U1)
|
|||
VALUE_OPCODE(SharedAtomicUMin32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicSMax32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicUMax32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicUMax64, U64, U32, U64, U1)
|
||||
VALUE_OPCODE(SharedAtomicCmpSwap64, U64, U32, U64, U64, U1)
|
||||
VALUE_OPCODE(SharedAtomicAnd32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicOr32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicOr64, Void, U32, U64, U1)
|
||||
|
|
@ -317,8 +319,8 @@ VALUE_OPCODE(BvhIntersect, U32x4, U32x4, ImageAddress, U1)
|
|||
VALUE_OPCODE(ImageQueryLod, U32x4, ImageResource, SamplerResource, ImageAddress)
|
||||
VALUE_OPCODE(ImageRead, U32x4, ImageResource, ImageAddress, U1)
|
||||
VALUE_OPCODE(ImageWrite, Void, ImageResource, ImageAddress, U32x4, U1)
|
||||
VALUE_OPCODE(ImageSampleRaw, U32x4, ImageResource, SamplerResource, ImageAddress)
|
||||
VALUE_OPCODE(ImageGatherRaw, U32x4, ImageResource, SamplerResource, ImageAddress, U32, U32, U32, U32)
|
||||
VALUE_OPCODE(ImageSampleRaw, U32x4, ImageResource, SamplerResource, ImageAddress, U1)
|
||||
VALUE_OPCODE(ImageGatherRaw, U32x4, ImageResource, SamplerResource, ImageAddress, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicCompareSwap32, U32, ImageResource, ImageAddress, U32, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicSwap32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicSwap64, U64, ImageResource, ImageAddress, U64, U1)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,66 @@ void Replace(Inst& inst, Value value) {
|
|||
inst.ReplaceUsesWith(value.Resolve());
|
||||
}
|
||||
|
||||
bool HasOneUse(const Inst& inst) {
|
||||
// Replacement aliases preserve external Values but emit no additional operation.
|
||||
return std::ranges::count_if(inst.Uses(), [](const Use& use) {
|
||||
return use.user->GetOpcode() != ValueOpcode::Identity;
|
||||
}) == 1;
|
||||
}
|
||||
|
||||
class FoldWorklist {
|
||||
public:
|
||||
struct Location {
|
||||
Block::iterator instruction;
|
||||
uint64_t visited = 0;
|
||||
bool queued = false;
|
||||
};
|
||||
|
||||
void Register(Block::iterator instruction) {
|
||||
locations.emplace(&*instruction, Location {instruction});
|
||||
Queue(&*instruction);
|
||||
}
|
||||
|
||||
void Queue(Inst* inst) {
|
||||
const auto found = locations.find(inst);
|
||||
if (found != locations.end() && !found->second.queued) {
|
||||
found->second.queued = true;
|
||||
pending.push_back(inst);
|
||||
}
|
||||
}
|
||||
|
||||
void QueueUsers(const std::vector<Use>& users) {
|
||||
++generation;
|
||||
affected.clear();
|
||||
for (const auto& use: users) affected.push_back(use.user);
|
||||
// Pattern folds inspect nested operands even when intermediate operations
|
||||
// stay dynamic. Visit consumers once per change, including cyclic Phi edges.
|
||||
for (size_t i = 0; i < affected.size(); ++i) {
|
||||
auto* inst = affected[i];
|
||||
const auto found = locations.find(inst);
|
||||
if (found == locations.end() || found->second.visited == generation) continue;
|
||||
found->second.visited = generation;
|
||||
Queue(inst);
|
||||
for (const auto& use: inst->Uses()) affected.push_back(use.user);
|
||||
}
|
||||
}
|
||||
|
||||
Block::iterator Prepend(Block& block, Block::iterator before, ValueOpcode opcode,
|
||||
std::initializer_list<Value> args = {}) {
|
||||
const auto result = block.PrependNewInst(before, opcode, args);
|
||||
Register(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::unordered_map<Inst*, Location> locations;
|
||||
std::vector<Inst*> pending;
|
||||
std::unordered_set<Inst*> lowered_ancillary;
|
||||
|
||||
private:
|
||||
std::vector<Inst*> affected;
|
||||
uint64_t generation = 0;
|
||||
};
|
||||
|
||||
template <typename Function>
|
||||
bool FoldU32(Inst& inst, Function function) {
|
||||
const auto lhs = Arg(inst, 0);
|
||||
|
|
@ -195,8 +255,42 @@ bool FoldCompositeExtract(Inst& inst, ValueOpcode construct, size_t components)
|
|||
return false;
|
||||
}
|
||||
|
||||
void FoldZeroComparison(Block& block, Block::iterator instruction, FoldWorklist& worklist) {
|
||||
auto& inst = *instruction;
|
||||
Value value;
|
||||
for (uint32_t arg = 0; arg < 2u; ++arg)
|
||||
if (Arg(inst, arg) == Value(0u)) value = Arg(inst, arg ^ 1u);
|
||||
const auto* source = value.TryInstruction();
|
||||
if (source == nullptr) return;
|
||||
const bool equal = inst.GetOpcode() == ValueOpcode::IEqual32;
|
||||
const auto boolean_select = [](const Inst* select) {
|
||||
return select != nullptr && select->GetOpcode() == ValueOpcode::SelectU32 &&
|
||||
IsImmediate(Arg(*select, 1), Type::U32) &&
|
||||
IsImmediate(Arg(*select, 2), Type::U32);
|
||||
};
|
||||
if (boolean_select(source)) {
|
||||
const bool when_true = (Arg(*source, 1).U32() == 0u) == equal;
|
||||
const bool when_false = (Arg(*source, 2).U32() == 0u) == equal;
|
||||
if (when_true == when_false) Replace(inst, Value(when_true));
|
||||
else if (when_true) Replace(inst, Arg(*source, 0));
|
||||
else Replace(inst, Value(&*worklist.Prepend(block, instruction, ValueOpcode::LogicalNot,
|
||||
{Arg(*source, 0)})));
|
||||
} else if (source->GetOpcode() == ValueOpcode::BitwiseOr32 &&
|
||||
(boolean_select(Arg(*source, 0).TryInstruction()) ||
|
||||
boolean_select(Arg(*source, 1).TryInstruction()))) {
|
||||
// SCC encoded as a DWORD needs no select or bitwise operation when only
|
||||
// its zero predicate is consumed. Keep arbitrary DWORDs as zero comparisons.
|
||||
const auto lhs = worklist.Prepend(block, instruction, inst.GetOpcode(),
|
||||
{Arg(*source, 0), Value(0u)});
|
||||
const auto rhs = worklist.Prepend(block, instruction, inst.GetOpcode(),
|
||||
{Arg(*source, 1), Value(0u)});
|
||||
Replace(inst, Value(&*worklist.Prepend(block, instruction,
|
||||
equal ? ValueOpcode::LogicalAnd : ValueOpcode::LogicalOr, {Value(&*lhs), Value(&*rhs)})));
|
||||
}
|
||||
}
|
||||
|
||||
void FoldInstruction(Block& block, Block::iterator instruction,
|
||||
std::unordered_set<Inst*>& lowered_ancillary) {
|
||||
FoldWorklist& worklist) {
|
||||
auto& inst = *instruction;
|
||||
switch (inst.GetOpcode()) {
|
||||
case ValueOpcode::StoreBufferU32: {
|
||||
|
|
@ -208,7 +302,7 @@ void FoldInstruction(Block& block, Block::iterator instruction,
|
|||
case ValueOpcode::SelectU1:
|
||||
if (!FoldSelect(inst) && IsImmediate(Arg(inst, 2), Type::U1) &&
|
||||
!Arg(inst, 2).U1()) {
|
||||
auto result = block.PrependNewInst(instruction, ValueOpcode::LogicalAnd,
|
||||
auto result = worklist.Prepend(block, instruction, ValueOpcode::LogicalAnd,
|
||||
{Arg(inst, 0), Arg(inst, 1)});
|
||||
Replace(inst, Value(&*result));
|
||||
}
|
||||
|
|
@ -224,8 +318,8 @@ void FoldInstruction(Block& block, Block::iterator instruction,
|
|||
case 0u: Replace(inst, Value(false)); break;
|
||||
case 0x3ffu: Replace(inst, Value(true)); break;
|
||||
case 3u: {
|
||||
const auto result = block.PrependNewInst(
|
||||
instruction, ValueOpcode::FPIsNan32, {Arg(inst, 0)});
|
||||
const auto result = worklist.Prepend(
|
||||
block, instruction, ValueOpcode::FPIsNan32, {Arg(inst, 0)});
|
||||
Replace(inst, Value(&*result));
|
||||
break;
|
||||
}
|
||||
|
|
@ -280,12 +374,12 @@ void FoldInstruction(Block& block, Block::iterator instruction,
|
|||
if (offset.U32() >= field.start && offset.U32() < field.end &&
|
||||
count.U32() <= field.end - offset.U32()) {
|
||||
// Preserve extraction and sign extension while exposing only the used field.
|
||||
const auto input = block.PrependNewInst(
|
||||
instruction, ValueOpcode::GetBuiltin,
|
||||
const auto input = worklist.Prepend(
|
||||
block, instruction, ValueOpcode::GetBuiltin,
|
||||
{Value(static_cast<uint32_t>(field.kind)), Value(0u)});
|
||||
inst.SetArg(0, Value(&*input));
|
||||
inst.SetArg(1, Value(offset.U32() - field.start));
|
||||
lowered_ancillary.insert(source);
|
||||
worklist.lowered_ancillary.insert(source);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -510,8 +604,20 @@ void FoldInstruction(Block& block, Block::iterator instruction,
|
|||
});
|
||||
return;
|
||||
case ValueOpcode::BitwiseAnd32:
|
||||
if (!FoldU32(inst, [](uint32_t a, uint32_t b) { return a & b; })) {
|
||||
ReplaceBinaryIdentity(inst, Type::U32, 0xffffffffu);
|
||||
if (FoldU32(inst, [](uint32_t a, uint32_t b) { return a & b; }) ||
|
||||
ReplaceBinaryIdentity(inst, Type::U32, UINT32_MAX)) return;
|
||||
for (uint32_t arm = 0; arm < 2u; ++arm) {
|
||||
const auto mask = Arg(inst, arm);
|
||||
const auto* shifted = Arg(inst, arm ^ 1u).TryInstruction();
|
||||
if (!IsImmediate(mask, Type::U32) || shifted == nullptr ||
|
||||
shifted->GetOpcode() != ValueOpcode::ShiftLeftLogical32 || !HasOneUse(*shifted)) continue;
|
||||
const auto shift = Arg(*shifted, 1);
|
||||
if (!IsImmediate(shift, Type::U32) || shift.U32() >= 32u) continue;
|
||||
const auto masked = worklist.Prepend(block, instruction, ValueOpcode::BitwiseAnd32,
|
||||
{Arg(*shifted, 0), Value(mask.U32() >> shift.U32())});
|
||||
Replace(inst, Value(&*worklist.Prepend(block, instruction, ValueOpcode::ShiftLeftLogical32,
|
||||
{Value(&*masked), shift})));
|
||||
return;
|
||||
}
|
||||
return;
|
||||
case ValueOpcode::BitwiseAnd64:
|
||||
|
|
@ -577,10 +683,12 @@ void FoldInstruction(Block& block, Block::iterator instruction,
|
|||
FoldU32(inst, [](uint32_t a, uint32_t b) { return std::max(a, b); });
|
||||
return;
|
||||
case ValueOpcode::IEqual32:
|
||||
FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a == b; });
|
||||
if (!FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a == b; }))
|
||||
FoldZeroComparison(block, instruction, worklist);
|
||||
return;
|
||||
case ValueOpcode::INotEqual32:
|
||||
FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a != b; });
|
||||
if (!FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a != b; }))
|
||||
FoldZeroComparison(block, instruction, worklist);
|
||||
return;
|
||||
case ValueOpcode::ULessThan32:
|
||||
FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a < b; });
|
||||
|
|
@ -642,19 +750,43 @@ void FoldInstruction(Block& block, Block::iterator instruction,
|
|||
const auto lhs = Arg(inst, 0);
|
||||
const auto rhs = Arg(inst, 1);
|
||||
const auto simplify = [&](Value assumption, Value expression) {
|
||||
const auto* disjunction = expression.TryInstruction();
|
||||
if (disjunction == nullptr || disjunction->GetOpcode() != ValueOpcode::LogicalOr) {
|
||||
return false;
|
||||
const auto* test = expression.TryInstruction();
|
||||
if (test == nullptr) return false;
|
||||
const auto replace = [&](Value value) {
|
||||
inst.SetArg(assumption == lhs ? 1u : 0u, value);
|
||||
return true;
|
||||
};
|
||||
const auto negate = [&](Value value) {
|
||||
return Value(&*worklist.Prepend(block, instruction, ValueOpcode::LogicalNot, {value}));
|
||||
};
|
||||
const bool inverse = test->GetOpcode() == ValueOpcode::LogicalNot;
|
||||
if (inverse) {
|
||||
test = Arg(*test, 0).TryInstruction();
|
||||
if (test == nullptr) return false;
|
||||
if (test->GetOpcode() == ValueOpcode::LogicalAnd && HasOneUse(*test))
|
||||
for (uint32_t arg = 0; arg < 2u; ++arg)
|
||||
if (Arg(*test, arg) == assumption)
|
||||
return replace(negate(Arg(*test, arg ^ 1u)));
|
||||
}
|
||||
for (uint32_t i = 0; i < 2u; ++i) {
|
||||
const auto* inverse = disjunction->Arg(i).Resolve().TryInstruction();
|
||||
if (inverse != nullptr && inverse->GetOpcode() == ValueOpcode::LogicalNot &&
|
||||
inverse->Arg(0).Resolve() == assumption) {
|
||||
inst.SetArg(assumption == lhs ? 1u : 0u,
|
||||
disjunction->Arg(i ^ 1u));
|
||||
return true;
|
||||
if (test->GetOpcode() == ValueOpcode::IEqual32 ||
|
||||
test->GetOpcode() == ValueOpcode::INotEqual32) {
|
||||
for (uint32_t arg = 0; arg < 2u; ++arg) {
|
||||
if (Arg(*test, arg) != Value(0u)) continue;
|
||||
const auto original = Arg(*test, arg ^ 1u);
|
||||
const auto active = ResolveActiveU32(original, assumption);
|
||||
if (!active.IsEmpty() && active != original) {
|
||||
const auto comparison = Value(&*worklist.Prepend(block, instruction,
|
||||
test->GetOpcode(), {active, Value(0u)}));
|
||||
return replace(inverse ? negate(comparison) : comparison);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (inverse || test->GetOpcode() != ValueOpcode::LogicalOr) return false;
|
||||
for (uint32_t arg = 0; arg < 2u; ++arg) {
|
||||
const auto* negation = Arg(*test, arg).TryInstruction();
|
||||
if (negation != nullptr && negation->GetOpcode() == ValueOpcode::LogicalNot &&
|
||||
Arg(*negation, 0) == assumption) return replace(Arg(*test, arg ^ 1u));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (simplify(lhs, rhs) || simplify(rhs, lhs)) return;
|
||||
|
|
@ -673,6 +805,25 @@ void FoldInstruction(Block& block, Block::iterator instruction,
|
|||
Replace(inst, lhs.U1() ? lhs : rhs);
|
||||
} else if (IsImmediate(rhs, Type::U1)) {
|
||||
Replace(inst, rhs.U1() ? rhs : lhs);
|
||||
} else {
|
||||
const auto* left = lhs.TryInstruction();
|
||||
const auto* right = rhs.TryInstruction();
|
||||
if (left == nullptr || right == nullptr ||
|
||||
left->GetOpcode() != ValueOpcode::LogicalAnd ||
|
||||
right->GetOpcode() != ValueOpcode::LogicalAnd ||
|
||||
(!HasOneUse(*left) && !HasOneUse(*right))) return;
|
||||
// Factor a shared execution predicate without retaining more operations.
|
||||
for (uint32_t a = 0; a < 2u; ++a) {
|
||||
for (uint32_t b = 0; b < 2u; ++b) {
|
||||
const auto common = Arg(*left, a);
|
||||
if (common != Arg(*right, b)) continue;
|
||||
const auto either = Value(&*worklist.Prepend(block, instruction,
|
||||
ValueOpcode::LogicalOr, {Arg(*left, a ^ 1u), Arg(*right, b ^ 1u)}));
|
||||
Replace(inst, Value(&*worklist.Prepend(block, instruction,
|
||||
ValueOpcode::LogicalAnd, {common, either})));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
|
@ -706,14 +857,13 @@ void FoldInstruction(Block& block, Block::iterator instruction,
|
|||
}
|
||||
}
|
||||
|
||||
// In wave32, extracting this invocation's bit from a ballot recovers its predicate.
|
||||
// Keep that identity through scalar EXEC operations and their loop-carried mask Phis.
|
||||
// Full and empty masks have a constant predicate in either wave size. In wave32,
|
||||
// ballot projection also survives scalar EXEC operations and loop-carried mask Phis.
|
||||
class LaneMaskProjection {
|
||||
public:
|
||||
explicit LaneMaskProjection(std::unordered_set<Inst*>& lowered_ancillary)
|
||||
: m_lowered_ancillary(lowered_ancillary) {}
|
||||
explicit LaneMaskProjection(FoldWorklist& worklist): m_worklist(worklist) {}
|
||||
|
||||
void Fold(Inst& inst) {
|
||||
void Fold(Inst& inst, uint32_t wave_size) {
|
||||
if (inst.GetOpcode() != ValueOpcode::INotEqual32 || !Immediate(Arg(inst, 1), 0u)) return;
|
||||
const auto* bit = Arg(inst, 0).TryInstruction();
|
||||
if (bit == nullptr || bit->GetOpcode() != ValueOpcode::BitwiseAnd32 ||
|
||||
|
|
@ -725,9 +875,15 @@ public:
|
|||
!Immediate(Arg(*index, 1), 31u)) return;
|
||||
const auto* lane = Arg(*index, 0).TryInstruction();
|
||||
if (lane == nullptr || lane->GetOpcode() != ValueOpcode::LaneId) return;
|
||||
const auto mask = Arg(*shift, 0);
|
||||
if (Immediate(mask, 0u) || Immediate(mask, UINT32_MAX)) {
|
||||
Replace(inst, Value(mask.U32() != 0u));
|
||||
return;
|
||||
}
|
||||
if (wave_size != 32u) return;
|
||||
m_visited.clear();
|
||||
m_grounded = false;
|
||||
if (CanProject(Arg(*shift, 0)) && m_grounded) Replace(inst, Project(Arg(*shift, 0)));
|
||||
if (CanProject(mask) && m_grounded) Replace(inst, Project(mask));
|
||||
}
|
||||
|
||||
private:
|
||||
|
|
@ -772,19 +928,18 @@ private:
|
|||
if (const auto found = m_values.find(source); found != m_values.end()) return found->second.Resolve();
|
||||
if (const auto predicate = BallotPredicate(*source); !predicate.IsEmpty()) return predicate;
|
||||
auto* block = source->Parent();
|
||||
const auto where = std::find_if(block->begin(), block->end(),
|
||||
[source](const Inst& candidate) { return &candidate == source; });
|
||||
const auto where = m_worklist.locations.at(source).instruction;
|
||||
const auto op = source->GetOpcode() == ValueOpcode::BitwiseAnd32 ? ValueOpcode::LogicalAnd
|
||||
: source->GetOpcode() == ValueOpcode::BitwiseOr32 ? ValueOpcode::LogicalOr
|
||||
: source->GetOpcode() == ValueOpcode::BitwiseNot32 ? ValueOpcode::LogicalNot
|
||||
: source->GetOpcode() == ValueOpcode::SelectU32 ? ValueOpcode::SelectU1
|
||||
: ValueOpcode::Phi;
|
||||
auto result = op == ValueOpcode::Phi ? block->PrependNewInst(where, op)
|
||||
auto result = op == ValueOpcode::Phi ? m_worklist.Prepend(*block, where, op)
|
||||
: op == ValueOpcode::LogicalNot
|
||||
? block->PrependNewInst(where, op, {Value(false)})
|
||||
? m_worklist.Prepend(*block, where, op, {Value(false)})
|
||||
: op == ValueOpcode::SelectU1
|
||||
? block->PrependNewInst(where, op, {Arg(*source, 0), Value(false), Value(false)})
|
||||
: block->PrependNewInst(where, op, {Value(false), Value(false)});
|
||||
? m_worklist.Prepend(*block, where, op, {Arg(*source, 0), Value(false), Value(false)})
|
||||
: m_worklist.Prepend(*block, where, op, {Value(false), Value(false)});
|
||||
m_values.emplace(source, Value(&*result));
|
||||
if (op == ValueOpcode::Phi) result->SetFlags(Type::U1);
|
||||
for (size_t arg = op == ValueOpcode::SelectU1 ? 1u : 0u; arg < source->NumArgs(); ++arg) {
|
||||
|
|
@ -792,11 +947,11 @@ private:
|
|||
if (op == ValueOpcode::Phi) result->AddPhiOperand(source->PhiBlock(arg), projected);
|
||||
else result->SetArg(arg, projected);
|
||||
}
|
||||
FoldInstruction(*block, result, m_lowered_ancillary);
|
||||
FoldInstruction(*block, result, m_worklist);
|
||||
return Value(&*result).Resolve();
|
||||
}
|
||||
|
||||
std::unordered_set<Inst*>& m_lowered_ancillary;
|
||||
FoldWorklist& m_worklist;
|
||||
std::unordered_set<const Inst*> m_visited;
|
||||
std::unordered_map<const Inst*, Value> m_values;
|
||||
bool m_grounded = false;
|
||||
|
|
@ -805,29 +960,59 @@ private:
|
|||
} // namespace
|
||||
|
||||
void ConstantPropagationPass(const BlockList& blocks, uint32_t wave_size) {
|
||||
std::unordered_set<Inst*> lowered_ancillary;
|
||||
LaneMaskProjection mask_projection(lowered_ancillary);
|
||||
for (auto* block: blocks) {
|
||||
for (auto inst = block->begin(); inst != block->end(); ++inst) {
|
||||
if (wave_size == 32u) mask_projection.Fold(*inst);
|
||||
FoldInstruction(*block, inst, lowered_ancillary);
|
||||
FoldWorklist worklist;
|
||||
LaneMaskProjection mask_projection(worklist);
|
||||
for (auto* block: blocks)
|
||||
for (auto inst = block->begin(); inst != block->end(); ++inst) worklist.Register(inst);
|
||||
std::vector<Use> users;
|
||||
std::vector<Value> operands;
|
||||
const auto drain = [&] {
|
||||
for (size_t i = 0; i < worklist.pending.size(); ++i) {
|
||||
auto* inst = worklist.pending[i];
|
||||
auto& location = worklist.locations.at(inst);
|
||||
location.queued = false;
|
||||
const auto opcode = inst->GetOpcode();
|
||||
if (opcode == ValueOpcode::Identity || opcode == ValueOpcode::Void) continue;
|
||||
const auto where = location.instruction;
|
||||
users.assign(inst->Uses().begin(), inst->Uses().end());
|
||||
operands.clear();
|
||||
for (size_t arg = 0; arg < inst->NumArgs(); ++arg) operands.push_back(inst->Arg(arg));
|
||||
mask_projection.Fold(*inst, wave_size);
|
||||
FoldInstruction(*inst->Parent(), where, worklist);
|
||||
bool changed = opcode != inst->GetOpcode() || operands.size() != inst->NumArgs();
|
||||
for (size_t arg = 0; !changed && arg < operands.size(); ++arg)
|
||||
changed = operands[arg] != inst->Arg(arg);
|
||||
if (changed) {
|
||||
worklist.QueueUsers(users);
|
||||
worklist.Queue(inst);
|
||||
}
|
||||
}
|
||||
}
|
||||
worklist.pending.clear();
|
||||
};
|
||||
drain();
|
||||
// Normalize retained PHI/select values only after every supported field read has
|
||||
// been lowered; direct raw consumers remain unsupported.
|
||||
for (auto* source: lowered_ancillary) {
|
||||
const bool retained_only = std::ranges::all_of(source->Uses(), [](const Use& use) {
|
||||
const auto& user = *use.user;
|
||||
if (!user.HasUses() && !user.MayHaveSideEffects()) {
|
||||
return true;
|
||||
bool normalized;
|
||||
do {
|
||||
normalized = false;
|
||||
for (auto* source: worklist.lowered_ancillary) {
|
||||
if (source->GetOpcode() == ValueOpcode::Identity) continue;
|
||||
const bool retained_only = std::ranges::all_of(source->Uses(), [](const Use& use) {
|
||||
const auto& user = *use.user;
|
||||
if (!user.HasUses() && !user.MayHaveSideEffects()) {
|
||||
return true;
|
||||
}
|
||||
return user.GetOpcode() == ValueOpcode::Phi ||
|
||||
(user.GetOpcode() == ValueOpcode::SelectU32 && use.operand == 2u);
|
||||
});
|
||||
if (retained_only) {
|
||||
worklist.QueueUsers(source->Uses());
|
||||
Replace(*source, Value(0u));
|
||||
normalized = true;
|
||||
}
|
||||
return user.GetOpcode() == ValueOpcode::Phi ||
|
||||
(user.GetOpcode() == ValueOpcode::SelectU32 && use.operand == 2u);
|
||||
});
|
||||
if (retained_only) {
|
||||
Replace(*source, Value(0u));
|
||||
}
|
||||
}
|
||||
drain();
|
||||
} while (normalized);
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::IR
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ namespace Libs::Graphics::ShaderRecompiler::IR {
|
|||
namespace {
|
||||
|
||||
constexpr uint64_t AddressMask = 0x0000ffffffffffffull;
|
||||
constexpr uint64_t MaxIndirectDescriptorProbes = 65536u;
|
||||
|
||||
bool SpecializationFail(std::string_view message) {
|
||||
std::fprintf(stderr, "shader resource specialization failed: %.*s\n",
|
||||
|
|
@ -106,16 +105,46 @@ Prospero::BufferFormat ImageConversionFormat(Prospero::BufferFormat format) {
|
|||
: Prospero::BufferFormat::kInvalid;
|
||||
}
|
||||
|
||||
enum class SamplerClass : uint8_t { Float, Integer, PointInteger };
|
||||
bool ValidatePackedUnormSampling(const ResourcePlan& program, const ResourceSnapshot& snapshot,
|
||||
uint32_t image_index) {
|
||||
ShaderTextureResource texture;
|
||||
std::copy_n(snapshot.images[image_index].dwords.begin(), std::size(texture.fields),
|
||||
texture.fields);
|
||||
if (texture.Type() != Prospero::ImageType::kColor2D ||
|
||||
texture.BaseLevel() != texture.LastLevel() || texture.MinLod() != 0u ||
|
||||
texture.CornerSample()) {
|
||||
return SpecializationFail("packed UNorm sampling requires a centered, single-mip 2D image");
|
||||
}
|
||||
for (const auto& pair: program.info.sampled_pairs) {
|
||||
if (pair.image != image_index) continue;
|
||||
if (pair.sampler >= snapshot.samplers.size() ||
|
||||
!program.info.samplers[pair.sampler].indirect_resources.empty()) {
|
||||
return SpecializationFail("packed UNorm sampling requires a direct sampler");
|
||||
}
|
||||
ShaderSamplerResource sampler;
|
||||
std::copy_n(snapshot.samplers[pair.sampler].dwords.begin(), std::size(sampler.fields),
|
||||
sampler.fields);
|
||||
const auto linear = static_cast<uint32_t>(Prospero::SamplerFilter::kBilinear);
|
||||
const auto clamp = static_cast<uint32_t>(Prospero::SamplerClampMode::kClampLastTexel);
|
||||
if (sampler.XyMagFilter() != linear || sampler.XyMinFilter() != linear ||
|
||||
sampler.ClampX() != clamp || sampler.ClampY() != clamp ||
|
||||
sampler.ForceUnormCoords() || sampler.MaxAnisoRatio() != 0u ||
|
||||
sampler.FilterMode() != 0u || sampler.MinLod() != 0u || sampler.ForceSrgb() ||
|
||||
sampler.TruncCoord() || (sampler.fields[0] & (1u << 19u)) != 0u) { // MC_COORD_TRUNC
|
||||
return SpecializationFail(fmt::format(
|
||||
"packed UNorm image {} requires normalized bilinear clamp sampling (sampler {})",
|
||||
image_index, pair.sampler));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
enum class SamplerClass : uint8_t { Float, Integer };
|
||||
|
||||
SamplerClass ClassifySampler(const ImageResource& image) {
|
||||
if (image.numeric_class == Prospero::TextureNumericClass::Sint ||
|
||||
(image.numeric_class == Prospero::TextureNumericClass::Uint &&
|
||||
image.conversion_format != Prospero::BufferFormat::kInvalid)) {
|
||||
return SamplerClass::PointInteger;
|
||||
}
|
||||
return image.numeric_class == Prospero::TextureNumericClass::Uint ? SamplerClass::Integer
|
||||
: SamplerClass::Float;
|
||||
return image.numeric_class == Prospero::TextureNumericClass::Uint ||
|
||||
image.numeric_class == Prospero::TextureNumericClass::Sint
|
||||
? SamplerClass::Integer : SamplerClass::Float;
|
||||
}
|
||||
|
||||
bool DescriptorIsCube(const DescriptorValue& descriptor) {
|
||||
|
|
@ -176,16 +205,14 @@ void MarkCleanReads(const std::unordered_set<const Inst*>& planned_reads,
|
|||
}
|
||||
}
|
||||
|
||||
bool ReadScalarTable(uint64_t base, uint64_t size, uint64_t dynamic_offset,
|
||||
const SrtRuntime& runtime, std::span<uint32_t> words) {
|
||||
const auto offset = dynamic_offset & ~uint64_t {3};
|
||||
bool ReadTable(uint64_t base, uint64_t size, uint64_t offset,
|
||||
const SrtRuntime& runtime, std::span<uint32_t> words) {
|
||||
const auto count = std::min<uint64_t>(words.size(), offset < size ? (size - offset) / 4u : 0u);
|
||||
std::ranges::fill(words.subspan(count), 0u);
|
||||
if (count == 0u) {
|
||||
return true;
|
||||
}
|
||||
base &= AddressMask & ~uint64_t {3};
|
||||
if (offset > AddressMask - base) {
|
||||
if (base > AddressMask || offset > AddressMask - base) {
|
||||
return false;
|
||||
}
|
||||
const auto address = base + offset;
|
||||
|
|
@ -195,15 +222,24 @@ bool ReadScalarTable(uint64_t base, uint64_t size, uint64_t dynamic_offset,
|
|||
runtime.read_specialization_memory(runtime.userdata, address, prefix);
|
||||
}
|
||||
|
||||
bool NormalizeIndirectStoreBuffer(DescriptorValue& value) {
|
||||
bool ReadScalarTable(uint64_t base, uint64_t size, uint64_t offset,
|
||||
const SrtRuntime& runtime, std::span<uint32_t> words) {
|
||||
return ReadTable(base & AddressMask & ~uint64_t {3}, size, offset & ~uint64_t {3}, runtime, words);
|
||||
}
|
||||
|
||||
bool NormalizeIndirectStoreBuffer(DescriptorValue& value, bool indexed, bool formatted) {
|
||||
ShaderBufferResource descriptor;
|
||||
if (!DecodeBufferDescriptor(value, descriptor)) return false;
|
||||
if (descriptor.Type() != 0u || descriptor.Format() == Prospero::BufferFormat::kInvalid ||
|
||||
descriptor.Base48() == 0u || descriptor.GetSize() == 0u) {
|
||||
value.dwords.fill(0u);
|
||||
} else if (descriptor.Stride() != 1u || descriptor.SwizzleEnabled() || descriptor.AddTid() ||
|
||||
descriptor.OutOfBounds() != 3u) {
|
||||
return SpecializationFail("indirect buffer stores require linear byte bounds");
|
||||
} else if (descriptor.Stride() != (indexed ? 4u : 1u) || descriptor.SwizzleEnabled() ||
|
||||
descriptor.AddTid() || descriptor.OutOfBounds() != (indexed ? 0u : 3u)) {
|
||||
return SpecializationFail("indirect buffer stores require exact linear DWORD or byte bounds");
|
||||
} else if (formatted && (descriptor.Format() != Prospero::BufferFormat::k32UInt ||
|
||||
descriptor.DstSelXYZW() != DstSel(4, 0, 0, 0) ||
|
||||
(descriptor.Base48() & 3u) != 0u)) {
|
||||
return SpecializationFail("indirect formatted X stores require an aligned 32-bit UInt identity mapping");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -230,6 +266,11 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
|||
auto& keys = program.material_keys;
|
||||
const auto children_begin = descriptors.size();
|
||||
const auto root_resource = specializations[resource_index];
|
||||
const auto evaluate_count = [&](uint32_t& count) {
|
||||
if (!clean.Evaluate(indirect.key_count, count)) return false;
|
||||
if (indirect.signed_count && std::bit_cast<int32_t>(count) < 0) count = 0;
|
||||
return true;
|
||||
};
|
||||
const auto intern_candidate = [&](const DescriptorValue& candidate, uint32_t& ordinal) {
|
||||
ordinal = 0u;
|
||||
if (candidate == descriptors[resource_index]) return true;
|
||||
|
|
@ -244,14 +285,19 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
|||
}
|
||||
return true;
|
||||
};
|
||||
const auto read_keys = [&](const ShaderBufferResource& material, uint64_t first,
|
||||
const auto read_selector = [&](uint64_t base, uint64_t size, uint64_t offset,
|
||||
std::span<uint32_t> words) {
|
||||
return selector->global ? ReadTable(base, size, offset, runtime, words)
|
||||
: ReadScalarTable(base, size, offset, runtime, words);
|
||||
};
|
||||
const auto read_keys = [&](uint64_t base, uint64_t size, uint64_t first,
|
||||
uint64_t step, uint64_t count) {
|
||||
keys.resize(count);
|
||||
if (step == 4u) {
|
||||
return ReadScalarTable(material.Base48(), material.GetSize(), first, runtime, keys);
|
||||
return read_selector(base, size, first, keys);
|
||||
}
|
||||
for (auto& key: keys) {
|
||||
if (!ReadScalarTable(material.Base48(), material.GetSize(), first, runtime, {&key, 1}))
|
||||
if (!read_selector(base, size, first, {&key, 1}))
|
||||
return false;
|
||||
first += step;
|
||||
}
|
||||
|
|
@ -320,29 +366,133 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
|||
}
|
||||
if (selector == nullptr) {
|
||||
uint32_t key_count = 0;
|
||||
uint32_t first_key = 0;
|
||||
if (indirect.workgroup_axis != UINT32_MAX) {
|
||||
if (indirect.workgroup_axis >= runtime.workgroup_counts.size()) return false;
|
||||
key_count = runtime.workgroup_counts[indirect.workgroup_axis];
|
||||
if (key_count == 0u) return false;
|
||||
} else {
|
||||
if (table_value.dword_count != 2u || !clean.Evaluate(indirect.key_count, key_count))
|
||||
if (table_value.dword_count != 2u || !evaluate_count(key_count))
|
||||
return false;
|
||||
if (std::bit_cast<int32_t>(key_count) <= 0) key_count = 0;
|
||||
}
|
||||
if (!indirect.selector_first.IsEmpty() && !clean.Evaluate(indirect.selector_first, first_key))
|
||||
return false;
|
||||
if (indirect.table_stride == 0u || key_count > MaxIndirectDescriptorProbes ||
|
||||
(key_count != 0u && uint64_t {indirect.table_offset} + indirect.table_immediate +
|
||||
(indirect.selector_first.IsEmpty() && key_count != 0u &&
|
||||
uint64_t {indirect.table_offset} + indirect.table_immediate +
|
||||
uint64_t {key_count - 1u} * indirect.table_stride + descriptor_bytes >
|
||||
UINT32_MAX + 1ull)) {
|
||||
return false;
|
||||
}
|
||||
keys.resize(key_count);
|
||||
std::iota(keys.begin(), keys.end(), 0u);
|
||||
} else if (!indirect.selector_first.IsEmpty()) {
|
||||
std::iota(keys.begin(), keys.end(), first_key);
|
||||
if (!keys.empty() && keys.back() < keys.front())
|
||||
std::rotate(keys.begin(), keys.begin() + (UINT32_MAX - first_key + 1ull), keys.end());
|
||||
} else if (selector->rows) {
|
||||
const auto& rows = *selector->rows;
|
||||
DescriptorValue count_value;
|
||||
ShaderBufferResource counts, material;
|
||||
if (!clean.EvaluateDescriptor(rows.source, count_value) ||
|
||||
!DecodeBufferDescriptor(count_value, counts) ||
|
||||
!DecodeBufferDescriptor(material_value, material) || selector->stride != 4u ||
|
||||
selector->offset != 0u || rows.stride == 0u || (rows.stride & 3u) != 0u ||
|
||||
counts.GetSize() / 4u > MaxIndirectDescriptorProbes) return false;
|
||||
std::vector<uint32_t> lengths(counts.GetSize() / 4u);
|
||||
if (!ReadScalarTable(counts.Base48(), counts.GetSize(), 0u, runtime, lengths)) return false;
|
||||
uint32_t total = 0;
|
||||
for (size_t row = 0; row < lengths.size(); ++row) {
|
||||
const auto length = lengths[row];
|
||||
if (length > MaxIndirectDescriptorProbes - total ||
|
||||
(length != 0u && uint64_t {row} * rows.stride + uint64_t {length} * 4u >
|
||||
UINT32_MAX + 1ull)) return false;
|
||||
total += length;
|
||||
}
|
||||
keys.reserve(total);
|
||||
std::vector<uint32_t> batch;
|
||||
for (size_t row = 0; row < lengths.size();) {
|
||||
if (lengths[row] == 0u) { ++row; continue; }
|
||||
const auto first = uint64_t {row} * rows.stride / 4u;
|
||||
auto end = first + lengths[row];
|
||||
size_t next = row + 1u;
|
||||
for (; next < lengths.size(); ++next) {
|
||||
if (lengths[next] == 0u) continue;
|
||||
const auto candidate_end =
|
||||
std::max(end, uint64_t {next} * rows.stride / 4u + lengths[next]);
|
||||
if (candidate_end - first > MaxIndirectDescriptorProbes) break;
|
||||
end = candidate_end;
|
||||
}
|
||||
// Coalesce small gaps, but interpret only selected prefixes. The strict
|
||||
// reader captures the entire fetched span for writable-alias rejection.
|
||||
batch.resize(end - first);
|
||||
if (!ReadScalarTable(material.Base48(), material.GetSize(), first * 4u, runtime, batch))
|
||||
return false;
|
||||
for (; row < next; ++row) {
|
||||
if (lengths[row] == 0u) continue;
|
||||
const auto begin = batch.begin() + (uint64_t {row} * rows.stride / 4u - first);
|
||||
keys.insert(keys.end(), begin, begin + lengths[row]);
|
||||
}
|
||||
}
|
||||
} else if (selector->bitset) {
|
||||
ShaderBufferResource material;
|
||||
uint32_t first = 0, count = 0;
|
||||
std::array<uint32_t, 4> masks {};
|
||||
uint32_t first = 0, count = 0;
|
||||
if (!DecodeBufferDescriptor(material_value, material) ||
|
||||
!clean.Evaluate(indirect.selector_first, first) ||
|
||||
!clean.Evaluate(indirect.key_count, count) || count > MaxIndirectDescriptorProbes)
|
||||
!evaluate_count(count) || count != masks.size() * 32u ||
|
||||
selector->stride != sizeof(masks) || selector->offset != 0u ||
|
||||
material.Type() != 0u || material.Stride() != sizeof(masks) ||
|
||||
material.SwizzleEnabled() || material.AddTid() || material.OutOfBounds() != 0u ||
|
||||
(material.Base48() & 3u) != 0u) return false;
|
||||
if (material.Format() != Prospero::BufferFormat::kInvalid) {
|
||||
const auto words = material.GetSize() / sizeof(uint32_t);
|
||||
// Bitmap storage can grow with resolution while its candidate domain stays
|
||||
// at 128 keys. Bound scratch memory, preserving each coherent read's capture.
|
||||
for (uint64_t first_word = 0; first_word < words; first_word += keys.size()) {
|
||||
keys.resize(std::min<uint64_t>(words - first_word, MaxIndirectDescriptorProbes));
|
||||
if (!ReadScalarTable(material.Base48(), material.GetSize(), first_word * 4u,
|
||||
runtime, keys)) return false;
|
||||
for (size_t word = 0; word < keys.size(); ++word) masks[word % masks.size()] |= keys[word];
|
||||
if (std::ranges::all_of(masks, [](uint32_t mask) { return mask == UINT32_MAX; })) break;
|
||||
}
|
||||
keys.clear();
|
||||
for (uint32_t word = 0; word < masks.size(); ++word) {
|
||||
auto mask = masks[word];
|
||||
while (mask != 0u) {
|
||||
const auto rank = word * 32u + std::countr_zero(mask);
|
||||
keys.push_back(first + count - 1u - rank);
|
||||
mask &= mask - 1u;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!selector->byte_ranges.empty()) {
|
||||
if (material_value.dword_count != 2u) return false;
|
||||
const auto base = (uint64_t {material_value.dwords[1]} << 32u) | material_value.dwords[0];
|
||||
std::array<bool, 256> selected {};
|
||||
for (const auto [offset, size]: selector->byte_ranges) {
|
||||
if (size > MaxIndirectDescriptorProbes || ((offset | size) & 3u) != 0u) return false;
|
||||
keys.resize(size / 4u);
|
||||
if (!ReadScalarTable(base, UINT64_MAX, offset, runtime, keys)) return false;
|
||||
for (const auto word: keys)
|
||||
for (uint32_t shift = 0; shift < 32u; shift += 8u)
|
||||
selected[(word >> shift) & 255u] = true;
|
||||
}
|
||||
keys.clear();
|
||||
for (uint32_t key = 0; key < selected.size(); ++key)
|
||||
if (selected[key]) keys.push_back(key);
|
||||
} else if (!indirect.selector_first.IsEmpty()) {
|
||||
ShaderBufferResource material;
|
||||
uint64_t base = 0, size = UINT64_MAX;
|
||||
uint32_t first = 0, count = 0;
|
||||
if (material_value.dword_count == 2u) {
|
||||
if (selector->stride == 0u) return false;
|
||||
base = (uint64_t {material_value.dwords[1]} << 32u) | material_value.dwords[0];
|
||||
} else {
|
||||
if (!DecodeBufferDescriptor(material_value, material)) return false;
|
||||
base = material.Base48();
|
||||
size = material.GetSize();
|
||||
}
|
||||
if (!clean.Evaluate(indirect.selector_first, first) ||
|
||||
!evaluate_count(count) || count > MaxIndirectDescriptorProbes)
|
||||
return false;
|
||||
// SMEM uses its instruction's byte stride; V# stride only supplies its bounds.
|
||||
const auto step = selector->stride != 0u ? selector->stride : material.Stride();
|
||||
|
|
@ -352,16 +502,16 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
|||
uint64_t {first} + count > material.NumRecords())) return false;
|
||||
const auto end = count != 0u
|
||||
? (uint64_t {first} + count - 1u) * step + selector->offset + 4u : 0u;
|
||||
if (end > uint64_t {UINT32_MAX} + 1u || end > material.GetSize())
|
||||
if (end > uint64_t {UINT32_MAX} + 1u || end > size)
|
||||
return false;
|
||||
if (!read_keys(material, uint64_t {first} * step + selector->offset, step, count))
|
||||
if (!read_keys(base, size, uint64_t {first} * step + selector->offset, step, count))
|
||||
return false;
|
||||
} else if (!indirect.selector_mask.IsEmpty()) {
|
||||
uint32_t mask = 0;
|
||||
uint32_t count = 0;
|
||||
if (material_value.dword_count != 2u || table_value.dword_count != 2u ||
|
||||
!clean.Evaluate(indirect.selector_mask, mask) ||
|
||||
!clean.Evaluate(indirect.key_count, count) || count == 0u || count > 32u) {
|
||||
!evaluate_count(count) || count == 0u || count > 32u) {
|
||||
return false;
|
||||
}
|
||||
if (count < 32u) mask &= (1u << count) - 1u;
|
||||
|
|
@ -374,13 +524,31 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
|||
static_cast<uint64_t>(index) * selector->stride;
|
||||
if (offset > UINT32_MAX) return false;
|
||||
uint32_t key = 0;
|
||||
if (!ReadScalarTable(material_base, UINT64_MAX, static_cast<uint32_t>(offset),
|
||||
runtime, {&key, 1})) return false;
|
||||
if (!read_selector(material_base, UINT64_MAX, static_cast<uint32_t>(offset),
|
||||
{&key, 1})) return false;
|
||||
keys.push_back(key);
|
||||
mask &= mask - 1u;
|
||||
}
|
||||
} else return false;
|
||||
if (selector != nullptr) {
|
||||
} else {
|
||||
ShaderBufferResource material;
|
||||
if (!DecodeBufferDescriptor(material_value, material) || selector->stride < 4u ||
|
||||
!std::has_single_bit(selector->stride) || (selector->offset & 3u) != 0u)
|
||||
return false;
|
||||
// Empty selector bounds denote the complete U32 congruence class. SMEM
|
||||
// adds its aligned immediate after the dynamic offset, without U32 wrap.
|
||||
const auto domain_count = (uint64_t {1} << 32u) / selector->stride;
|
||||
const auto available = material.GetSize() > selector->offset
|
||||
? material.GetSize() - selector->offset : 0u;
|
||||
const auto count = std::min(domain_count,
|
||||
available < 4u ? 0u : (available - 4u) / selector->stride + 1u);
|
||||
if (count > MaxIndirectDescriptorProbes ||
|
||||
!read_keys(material.Base48(), material.GetSize(), selector->offset, selector->stride, count)) return false;
|
||||
if (count < domain_count) keys.push_back(0u); // An out-of-bounds scalar read returns zero.
|
||||
}
|
||||
if (selector != nullptr && selector->byte_ranges.empty()) {
|
||||
if (selector->bit_offset != 0u || selector->bit_mask != UINT32_MAX || selector->maximum != UINT32_MAX)
|
||||
for (auto& key: keys)
|
||||
key = std::min((key >> selector->bit_offset) & selector->bit_mask, selector->maximum);
|
||||
std::ranges::sort(keys);
|
||||
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
|
||||
}
|
||||
|
|
@ -433,7 +601,7 @@ struct SamplerPlan {
|
|||
uint32_t source;
|
||||
SamplerClass type;
|
||||
};
|
||||
std::array<std::array<uint32_t, 3>, ShaderInfo::MaxSamplers> mapping;
|
||||
std::array<std::array<uint32_t, 2>, ShaderInfo::MaxSamplers> mapping;
|
||||
std::array<Binding, ShaderInfo::MaxSamplers> bindings;
|
||||
uint32_t sampler_count = 0;
|
||||
};
|
||||
|
|
@ -520,6 +688,7 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
|
|||
if (base.atomic &&
|
||||
(base.atomic64 ? format != Prospero::BufferFormat::k32_32UInt
|
||||
: format != Prospero::BufferFormat::k32UInt &&
|
||||
format != Prospero::BufferFormat::k32SInt &&
|
||||
format != Prospero::BufferFormat::k32Float)) {
|
||||
return SpecializationFail(
|
||||
fmt::format("atomic image descriptor {} uses unsupported format {}", i,
|
||||
|
|
@ -540,8 +709,15 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
|
|||
image.shader_swizzle = DescriptorImageSwizzle(descriptor);
|
||||
}
|
||||
const bool raw_sint_storage = storage && format == Prospero::BufferFormat::k32SInt &&
|
||||
base.written && !base.read && !base.atomic;
|
||||
image.numeric_class = Prospero::SampledTextureNumericClass(format);
|
||||
base.written && (base.atomic ? !base.atomic64 : !base.read);
|
||||
image.numeric_class = Prospero::SampledTextureNumericClass(
|
||||
Prospero::RemapTextureFormat(format));
|
||||
if (!storage && format == Prospero::BufferFormat::k11_11_10UNorm) {
|
||||
if (image.indirect_root != ImageResource::NoIndirectImage) {
|
||||
return SpecializationFail("packed UNorm sampling requires a direct image");
|
||||
}
|
||||
if (!ValidatePackedUnormSampling(program, snapshot, i)) return false;
|
||||
}
|
||||
if (storage) {
|
||||
if ((!raw_sint_storage && image.numeric_class == Prospero::TextureNumericClass::Sint) ||
|
||||
image.numeric_class == Prospero::TextureNumericClass::Unsupported) {
|
||||
|
|
@ -609,8 +785,7 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
|
|||
}
|
||||
const bool same_coordinates = image.dimension == image_class.dimension &&
|
||||
image.cube == image_class.cube;
|
||||
if (image.numeric_class != image_class.numeric_class ||
|
||||
(!same_coordinates && !(is_2d(image.dimension) && is_2d(image_class.dimension))) ||
|
||||
if ((!same_coordinates && !(is_2d(image.dimension) && is_2d(image_class.dimension))) ||
|
||||
image.mip_count != image_class.mip_count ||
|
||||
image.conversion_format != image_class.conversion_format ||
|
||||
image.shader_swizzle != image_class.shader_swizzle) {
|
||||
|
|
@ -635,6 +810,10 @@ bool BuildSamplerPlan(const ShaderInfo& base, SamplerPlan& plan) {
|
|||
return false;
|
||||
}
|
||||
usage[pair.sampler] |= 1u << static_cast<uint32_t>(ClassifySampler(base.images[pair.image]));
|
||||
for (const auto candidate: base.images[pair.image].indirect_resources) {
|
||||
if (candidate >= base.images.size()) return false;
|
||||
usage[pair.sampler] |= 1u << static_cast<uint32_t>(ClassifySampler(base.images[candidate]));
|
||||
}
|
||||
for (const auto candidate: base.samplers[pair.sampler].indirect_resources) {
|
||||
if (candidate >= base.samplers.size()) return false;
|
||||
usage[candidate] |= usage[pair.sampler];
|
||||
|
|
@ -751,55 +930,16 @@ static bool IsFullDispatchPredicate(Value value, uint32_t depth = 0) {
|
|||
return axis.IsImmediate() && axis.U32() < 3 && extent->Arg(0).Resolve() == axis;
|
||||
}
|
||||
|
||||
// Nonnegative affine coefficients for constant, local and workgroup coordinates. Reject modular
|
||||
// arithmetic that could wrap; runtime coverage also bounds the largest invocation index.
|
||||
static std::optional<std::array<uint64_t, 3>> FillIndex(Value value, uint32_t axis, Value guard,
|
||||
uint32_t depth = 0) {
|
||||
value = ResolveActiveU32(value, guard);
|
||||
if (depth > 32 || value.GetType() != Type::U32) {
|
||||
return {};
|
||||
}
|
||||
if (value.IsImmediate()) {
|
||||
return std::array<uint64_t, 3> {value.U32(), 0, 0};
|
||||
}
|
||||
const auto* inst = value.TryInstruction();
|
||||
if (inst == nullptr) {
|
||||
return {};
|
||||
}
|
||||
const auto op = inst->GetOpcode();
|
||||
if (op == ValueOpcode::GetBuiltin && inst->Arg(1) == Value(axis)) {
|
||||
if (inst->Arg(0) == Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId))) {
|
||||
return std::array<uint64_t, 3> {0, 1, 0};
|
||||
}
|
||||
if (inst->Arg(0) == Value(static_cast<uint32_t>(StageInputKind::WorkgroupId))) {
|
||||
return std::array<uint64_t, 3> {0, 0, 1};
|
||||
}
|
||||
}
|
||||
if (op != ValueOpcode::IAdd32 && op != ValueOpcode::IMul32 &&
|
||||
op != ValueOpcode::ShiftLeftLogical32) {
|
||||
return {};
|
||||
}
|
||||
auto left = FillIndex(inst->Arg(0), axis, guard, depth + 1);
|
||||
auto right = FillIndex(inst->Arg(1), axis, guard, depth + 1);
|
||||
if (!left || !right) {
|
||||
return {};
|
||||
}
|
||||
if (op == ValueOpcode::IMul32 && ((*right)[1] != 0 || (*right)[2] != 0)) {
|
||||
std::swap(left, right);
|
||||
}
|
||||
if (op != ValueOpcode::IAdd32 && ((*right)[1] != 0 || (*right)[2] != 0)) {
|
||||
return {};
|
||||
}
|
||||
if (op == ValueOpcode::ShiftLeftLogical32) {
|
||||
if ((*right)[0] >= 32) return {};
|
||||
(*right)[0] = uint64_t {1} << (*right)[0];
|
||||
}
|
||||
for (uint32_t i = 0; i < left->size(); ++i) {
|
||||
(*left)[i] =
|
||||
op == ValueOpcode::IAdd32 ? (*left)[i] + (*right)[i] : (*left)[i] * (*right)[0];
|
||||
if ((*left)[i] > UINT32_MAX) return {};
|
||||
}
|
||||
return left;
|
||||
static std::optional<std::array<uint64_t, 3>> FillIndex(Value value, uint32_t axis, Value guard) {
|
||||
return AffineIndex<3>(value, [&](Value at) { return ResolveActiveU32(at, guard); },
|
||||
[&](Value at) -> std::optional<size_t> {
|
||||
const auto* inst = at.TryInstruction();
|
||||
if (inst == nullptr || inst->GetOpcode() != ValueOpcode::GetBuiltin ||
|
||||
inst->Arg(1) != Value(axis)) return {};
|
||||
if (inst->Arg(0) == Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId))) return 1u;
|
||||
if (inst->Arg(0) == Value(static_cast<uint32_t>(StageInputKind::WorkgroupId))) return 2u;
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
static UniformFillPlan AnalyzeUniformFill(const Program& program) {
|
||||
|
|
@ -985,43 +1125,79 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
|
|||
for (const auto& read: program.srt_reads) {
|
||||
plan.srt_reads.push_back({Clone(read.value), read.flat_offset});
|
||||
}
|
||||
// A proven uniform factor can decide a branch even when its other lanes are unknown.
|
||||
// Keep only that Boolean structure, never the varying shader dependency graph.
|
||||
Value unknown;
|
||||
// Retain integer control expressions with opaque varying leaves. Their identities
|
||||
// matter when an EXEC factor also guards a selected value; never clone float work.
|
||||
bool capture_control_reads = false;
|
||||
bool uniform_control = false;
|
||||
const auto control_value_count = plan.value_storage.size();
|
||||
struct ControlValue {
|
||||
Value value;
|
||||
bool complete = false;
|
||||
};
|
||||
std::unordered_map<const Inst*, ControlValue> predicates;
|
||||
std::function<Value(Value)> ClonePredicate = [&](Value value) -> Value {
|
||||
value = value.Resolve();
|
||||
if (value.IsEmpty()) return {};
|
||||
const auto* inst = value.TryInstruction();
|
||||
if (inst == nullptr) {
|
||||
uniform_control |= value.GetType() == Type::U1;
|
||||
return ValidateRuntimeValue(program, value, RuntimeValueType::Integer) ? value : Value {};
|
||||
}
|
||||
const auto [found, inserted] = predicates.try_emplace(inst);
|
||||
auto& entry = found->second;
|
||||
auto& result = entry.value;
|
||||
const auto opaque = [&]() -> Value {
|
||||
if (value.GetType() != Type::U1 && value.GetType() != Type::U32) return {};
|
||||
return result = Value(&plan.value_storage.emplace_back(
|
||||
value.GetType() == Type::U1 ? ValueOpcode::UndefU1 : ValueOpcode::UndefU32));
|
||||
};
|
||||
// A recursive projection remains opaque, including unstructured loop Phis.
|
||||
if (!inserted) return entry.complete || !result.IsEmpty() ? result : opaque();
|
||||
const auto finish = [&](Value projected) {
|
||||
entry.complete = true;
|
||||
return result = projected;
|
||||
};
|
||||
if (ValidateRuntimeValue(program, value, RuntimeValueType::Integer)) {
|
||||
uniform_control = true;
|
||||
capture_control_reads |=
|
||||
!ValidateRuntimeValue(program, value, RuntimeValueType::ImmutableInteger);
|
||||
return Clone(value);
|
||||
return finish(Clone(value));
|
||||
}
|
||||
const auto* inst = value.TryInstruction();
|
||||
if (inst == nullptr) return {};
|
||||
const auto op = inst->GetOpcode();
|
||||
if (op == ValueOpcode::ConditionRef || op == ValueOpcode::LogicalNot) {
|
||||
const auto operand = ClonePredicate(inst->Arg(0));
|
||||
if (operand.IsEmpty() || op == ValueOpcode::ConditionRef) return operand;
|
||||
auto& node = plan.value_storage.emplace_back(op);
|
||||
node.SetArg(0, operand);
|
||||
return Value(&node);
|
||||
}
|
||||
if (op != ValueOpcode::LogicalAnd && op != ValueOpcode::LogicalOr) return {};
|
||||
auto left = ClonePredicate(inst->Arg(0));
|
||||
auto right = ClonePredicate(inst->Arg(1));
|
||||
if (left.IsEmpty() && right.IsEmpty()) return {};
|
||||
if (left.IsEmpty() || right.IsEmpty()) {
|
||||
if (unknown.IsEmpty()) unknown = Value(&plan.value_storage.emplace_back(ValueOpcode::UndefU1));
|
||||
if (left.IsEmpty()) left = unknown;
|
||||
if (right.IsEmpty()) right = unknown;
|
||||
if (op == ValueOpcode::ConditionRef) return finish(ClonePredicate(inst->Arg(0)));
|
||||
if (op == ValueOpcode::Phi) {
|
||||
if (inst->Parent() == nullptr || inst->Parent()->terminator.loop_header ||
|
||||
inst->NumPhiBlocks() != inst->NumArgs() || inst->NumArgs() == 0u ||
|
||||
(value.GetType() != Type::U1 && value.GetType() != Type::U32)) return finish(opaque());
|
||||
std::vector<uint32_t> predecessors;
|
||||
for (size_t index = 0; index < inst->NumArgs(); ++index) {
|
||||
const auto predecessor = std::ranges::find(program.blocks, inst->PhiBlock(index));
|
||||
if (predecessor == program.blocks.end()) return finish(opaque());
|
||||
predecessors.push_back(static_cast<uint32_t>(predecessor - program.blocks.begin()));
|
||||
}
|
||||
auto& node = plan.value_storage.emplace_back(op, inst->Flags<uint64_t>());
|
||||
plan.control_phi_predecessors.emplace(&node, std::move(predecessors));
|
||||
for (size_t index = 0; index < inst->NumArgs(); ++index)
|
||||
node.AddPhiOperand(nullptr, ClonePredicate(inst->Arg(index)));
|
||||
return finish(Value(&node));
|
||||
}
|
||||
if (op != ValueOpcode::LogicalAnd && op != ValueOpcode::LogicalOr &&
|
||||
op != ValueOpcode::LogicalNot && op != ValueOpcode::IEqual32 &&
|
||||
op != ValueOpcode::INotEqual32 && op != ValueOpcode::ULessThanEqual32 &&
|
||||
op != ValueOpcode::SelectU1 && op != ValueOpcode::SelectU32) return finish(opaque());
|
||||
auto& node = plan.value_storage.emplace_back(op);
|
||||
node.SetArg(0, left);
|
||||
node.SetArg(1, right);
|
||||
return Value(&node);
|
||||
for (size_t index = 0; index < inst->NumArgs(); ++index)
|
||||
node.SetArg(index, ClonePredicate(inst->Arg(index)));
|
||||
return finish(Value(&node));
|
||||
};
|
||||
plan.control_flow = ResourceControlFlow(program, ClonePredicate);
|
||||
if (!uniform_control) {
|
||||
plan.control_flow.clear();
|
||||
plan.control_phi_predecessors.clear();
|
||||
auto first = std::next(plan.value_storage.begin(), control_value_count);
|
||||
for (auto it = first; it != plan.value_storage.end(); ++it) it->Invalidate();
|
||||
plan.value_storage.erase(first, plan.value_storage.end());
|
||||
}
|
||||
plan.uniform_fill = AnalyzeUniformFill(program);
|
||||
for (uint32_t i = 0; i < plan.uniform_fill.fill.words; ++i) {
|
||||
plan.uniform_fill.values[i] = Clone(plan.uniform_fill.values[i]);
|
||||
|
|
@ -1041,8 +1217,11 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
|
|||
for (const auto& source: plan.descriptor_sources) {
|
||||
if (!source.indirect_descriptor.has_value()) continue;
|
||||
const auto& indirect = *source.indirect_descriptor;
|
||||
if (indirect.selector)
|
||||
if (indirect.selector) {
|
||||
MarkCleanReads(planned_reads, Source(plan, indirect.selector->source));
|
||||
if (indirect.selector->rows)
|
||||
MarkCleanReads(planned_reads, Source(plan, indirect.selector->rows->source));
|
||||
}
|
||||
MarkCleanReads(planned_reads, nullptr, indirect.selector_mask);
|
||||
MarkCleanReads(planned_reads, nullptr, indirect.selector_first);
|
||||
MarkCleanReads(planned_reads, nullptr, indirect.key_count);
|
||||
|
|
@ -1064,6 +1243,7 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
|
|||
}
|
||||
if (capture_indirect_reads || !plan.source_reads.empty())
|
||||
plan.resource_tracking_complete &= !program.has_address_writes;
|
||||
CompileSrtWalker(plan);
|
||||
return plan;
|
||||
}
|
||||
|
||||
|
|
@ -1125,7 +1305,10 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
|||
if (!MaterializeIndirectDescriptor(
|
||||
program, *source->indirect_descriptor, i, 4u, observed, clean, snapshot,
|
||||
snapshot.buffers, specialization.buffers, ShaderInfo::MaxBuffers,
|
||||
NormalizeIndirectStoreBuffer))
|
||||
[&](DescriptorValue& value) {
|
||||
return NormalizeIndirectStoreBuffer(
|
||||
value, source->indirect_descriptor->indexed_store, base.formatted);
|
||||
}))
|
||||
return false;
|
||||
}
|
||||
} else if (!evaluate(base.source, snapshot.buffers[i], base.written)) {
|
||||
|
|
@ -1300,7 +1483,7 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
|
|||
samplers.push_back(samplers[binding.source]);
|
||||
}
|
||||
samplers[index].snapshot_index = binding.source;
|
||||
samplers[index].force_point_filtering = binding.type == SamplerClass::PointInteger;
|
||||
samplers[index].force_point_filtering = binding.type == SamplerClass::Integer;
|
||||
samplers[index].integer_border = binding.type != SamplerClass::Float;
|
||||
}
|
||||
for (uint32_t index = 0; index < sampler_plan.sampler_count; index++) {
|
||||
|
|
@ -1328,6 +1511,16 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
|
|||
inst.SetFlags(image_remap[inst.Flags<uint32_t>()]);
|
||||
continue;
|
||||
}
|
||||
if (inst.GetOpcode() == ValueOpcode::StoreBufferU32) {
|
||||
auto& memory = memory_info[inst.Flags<MemoryFlags>().index];
|
||||
if (memory.kind == ResourceKind::Buffer && memory.formatted &&
|
||||
program.descriptor_sources[buffers[memory.resource].source].indirect_descriptor) {
|
||||
// Candidate validation proved an index-only k32UInt identity store.
|
||||
// Reuse finite raw-store dispatch and its original-index bounds check.
|
||||
memory.formatted = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (BufferAccessOf(inst.GetOpcode()) == BufferAccess::Read) {
|
||||
const auto& memory = memory_info[inst.Flags<MemoryFlags>().index];
|
||||
if (memory.kind == ResourceKind::Buffer &&
|
||||
|
|
@ -1405,6 +1598,7 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
|
|||
}
|
||||
EXIT_IF(image.indirect_root == memory.resource &&
|
||||
inst.GetOpcode() != ValueOpcode::ImageSampleRaw &&
|
||||
inst.GetOpcode() != ValueOpcode::ImageGatherRaw &&
|
||||
inst.GetOpcode() != ValueOpcode::ImageRead);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -6,7 +6,8 @@
|
|||
namespace Libs::Graphics::ShaderRecompiler::IR {
|
||||
|
||||
// Resolves native descriptor sources, plans their scalar reads, and assigns dense resource bindings.
|
||||
void TrackResources(Program& program, const Decoder::Program& decoded, const CFG::Graph& native_cfg);
|
||||
void TrackResources(Program& program, const Decoder::Program& decoded, const CFG::Graph& native_cfg,
|
||||
ShaderStageInputInfo input = {});
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::IR
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
#include "graphics/shader/recompiler/ir/passes/SrtWalker.h"
|
||||
|
||||
#include "common/abi.h"
|
||||
#include "common/assert.h"
|
||||
#include "graphics/shader/recompiler/ir/ShaderIR.h"
|
||||
|
||||
#include <xbyak/xbyak.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::IR {
|
||||
|
|
@ -136,6 +141,7 @@ bool IsRuntimeUniformOp(ValueOpcode op) {
|
|||
case ValueOpcode::IMul64:
|
||||
case ValueOpcode::UMulHi:
|
||||
case ValueOpcode::UMin32:
|
||||
case ValueOpcode::UMax32:
|
||||
case ValueOpcode::ShiftLeftLogical32:
|
||||
case ValueOpcode::ShiftLeftLogical64:
|
||||
case ValueOpcode::ShiftRightLogical32:
|
||||
|
|
@ -172,8 +178,9 @@ bool IsRuntimeUniformOp(ValueOpcode op) {
|
|||
|
||||
class RuntimeValidator {
|
||||
public:
|
||||
explicit RuntimeValidator(const ResourcePlan& program, RuntimeValueType type)
|
||||
: m_program(program), m_type(type) {}
|
||||
explicit RuntimeValidator(const ResourcePlan& program, RuntimeValueType type,
|
||||
bool wave_uniform = false)
|
||||
: m_program(program), m_type(type), m_wave_uniform(wave_uniform) {}
|
||||
|
||||
bool Run(Value value) { return Validate(value); }
|
||||
|
||||
|
|
@ -272,6 +279,12 @@ private:
|
|||
}
|
||||
return finish(true);
|
||||
}
|
||||
if (m_wave_uniform && op == ValueOpcode::GetBuiltin) {
|
||||
const auto axis = inst->NumArgs() == 2u ? inst->Arg(1).Resolve() : Value {};
|
||||
return finish(m_program.stage == ShaderType::Compute && inst->NumArgs() == 2u &&
|
||||
inst->Arg(0) == Value(static_cast<uint32_t>(StageInputKind::WorkgroupId)) &&
|
||||
axis.IsImmediate() && axis.GetType() == Type::U32 && axis.U32() < 3u);
|
||||
}
|
||||
if (op == ValueOpcode::Phi) {
|
||||
if (m_type != RuntimeValueType::Any && !ValidateArguments(*inst, false)) {
|
||||
return finish(false);
|
||||
|
|
@ -351,6 +364,7 @@ private:
|
|||
|
||||
const ResourcePlan& m_program;
|
||||
RuntimeValueType m_type;
|
||||
bool m_wave_uniform;
|
||||
Value m_active_mask;
|
||||
std::unordered_set<const Inst*> m_visiting;
|
||||
std::unordered_set<const Inst*> m_validated_dependencies;
|
||||
|
|
@ -359,13 +373,150 @@ private:
|
|||
|
||||
} // namespace
|
||||
|
||||
SrtWalker::SrtWalker(const ResourcePlan& program, const SrtRuntime& runtime,
|
||||
SrtWalker* clean_evaluator, Value active_mask)
|
||||
: m_program(program), m_runtime(runtime), m_clean_evaluator(clean_evaluator),
|
||||
m_active_mask(active_mask.Resolve()),
|
||||
m_context(AcquireContext(program)) {}
|
||||
void CompileSrtWalker(ResourcePlan& program) {
|
||||
if (!program.resource_tracking_complete || !program.srt_plan_complete ||
|
||||
program.capture_specialization_reads || !program.control_flow.empty() ||
|
||||
program.srt_reads.empty()) return;
|
||||
|
||||
SrtWalker::~SrtWalker() { --m_program.evaluation_depth; }
|
||||
using Reads = std::map<int32_t, const SrtRead*>;
|
||||
std::map<uint32_t, Reads> roots;
|
||||
std::unordered_map<const Inst*, Reads> pointer_uses;
|
||||
std::unordered_map<const Inst*, int32_t> offsets;
|
||||
for (const auto& read: program.srt_reads) {
|
||||
const auto* inst = read.value.Resolve().TryInstruction();
|
||||
if (inst == nullptr || inst->GetOpcode() != ValueOpcode::LoadAddressU32 ||
|
||||
inst->NumArgs() < 2 || !IsRawRead(program, *inst) ||
|
||||
read.flat_offset >= program.srt_reads.size()) return;
|
||||
const auto offset = inst->Arg(1).Resolve();
|
||||
if (!offset.IsImmediate() || offset.GetType() != Type::U32) return;
|
||||
const auto& memory = program.memory_info[inst->Flags<SrtReadFlags>().index];
|
||||
const auto relative =
|
||||
(static_cast<int64_t>(static_cast<int32_t>(memory.offset)) & ~int64_t {3}) +
|
||||
static_cast<int64_t>(offset.U32() & ~3u);
|
||||
if (relative < INT32_MIN || relative > INT32_MAX ||
|
||||
!offsets.emplace(inst, static_cast<int32_t>(relative)).second) return;
|
||||
}
|
||||
uint32_t user_data_count = 0;
|
||||
for (const auto& read: program.srt_reads) {
|
||||
const auto* inst = read.value.ResolveInstruction();
|
||||
const auto* handle = inst->Arg(0).Resolve().TryInstruction();
|
||||
if (handle == nullptr || handle->GetOpcode() != ValueOpcode::GetAddressResource ||
|
||||
handle->NumArgs() != 2) return;
|
||||
const auto* low = handle->Arg(0).Resolve().TryInstruction();
|
||||
const auto* high = handle->Arg(1).Resolve().TryInstruction();
|
||||
if (low == nullptr || high == nullptr) return;
|
||||
Reads* uses = nullptr;
|
||||
if (low->GetOpcode() == ValueOpcode::GetUserData &&
|
||||
high->GetOpcode() == ValueOpcode::GetUserData && low->NumArgs() == 1 &&
|
||||
high->NumArgs() == 1 && low->Arg(0).GetType() == Type::ScalarReg &&
|
||||
high->Arg(0).GetType() == Type::ScalarReg) {
|
||||
const auto reg = RegIndex(low->Arg(0).ScalarRegister());
|
||||
if (reg < program.user_data_base ||
|
||||
RegIndex(high->Arg(0).ScalarRegister()) != reg + 1u) return;
|
||||
const auto base = reg - program.user_data_base;
|
||||
user_data_count = std::max(user_data_count, base + 2u);
|
||||
uses = &roots[base];
|
||||
} else {
|
||||
const auto lo = offsets.find(low);
|
||||
const auto hi = offsets.find(high);
|
||||
if (lo == offsets.end() || hi == offsets.end() ||
|
||||
static_cast<int64_t>(lo->second) + 4 != hi->second ||
|
||||
!EquivalentValue(program, low->Arg(0), high->Arg(0))) return;
|
||||
uses = &pointer_uses[low];
|
||||
}
|
||||
if (!uses->emplace(offsets.at(inst), &read).second) return;
|
||||
}
|
||||
std::unordered_set<const Inst*> visited;
|
||||
const auto reachable = [&](auto&& self, const Reads& reads) -> bool {
|
||||
for (const auto& [offset, read]: reads) {
|
||||
const auto* inst = read->value.ResolveInstruction();
|
||||
const auto child = pointer_uses.find(inst);
|
||||
if (child != pointer_uses.end() &&
|
||||
(!visited.insert(inst).second || !self(self, child->second))) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
for (const auto& [reg, reads]: roots) {
|
||||
if (!reachable(reachable, reads)) return;
|
||||
}
|
||||
if (visited.size() != pointer_uses.size()) return;
|
||||
|
||||
program.srt_code = std::make_unique<Xbyak::CodeGenerator>(4096, Xbyak::AutoGrow);
|
||||
auto& code = *program.srt_code;
|
||||
Xbyak::Label failed, finished;
|
||||
code.push(code.rbp);
|
||||
code.mov(code.rbp, code.rsp);
|
||||
code.cmp(code.r8, user_data_count);
|
||||
code.jb(failed, Xbyak::CodeGenerator::T_NEAR);
|
||||
code.mov(code.r9, AddressMask & ~uint64_t {3});
|
||||
// Follow shadPS4's VisitPointer: copy one table level in offset order, then
|
||||
// descend through its child pointers. Seed the descriptor evaluator's memo.
|
||||
const auto visit = [&](auto&& self, int32_t pointer_offset, const Reads& reads) -> void {
|
||||
code.push(code.rdi);
|
||||
code.mov(code.rdi, code.qword[code.rdi + pointer_offset]);
|
||||
code.and_(code.rdi, code.r9);
|
||||
const auto first = reads.begin()->first;
|
||||
const auto last = reads.rbegin()->first;
|
||||
if (first < 0) {
|
||||
code.mov(code.r10, -static_cast<int64_t>(first));
|
||||
code.cmp(code.rdi, code.r10);
|
||||
code.jb(failed, Xbyak::CodeGenerator::T_NEAR);
|
||||
}
|
||||
if (last > 0) {
|
||||
code.mov(code.r10, AddressMask - last);
|
||||
code.cmp(code.rdi, code.r10);
|
||||
code.ja(failed, Xbyak::CodeGenerator::T_NEAR);
|
||||
}
|
||||
for (const auto& [offset, read]: reads) {
|
||||
const auto index = read->value.ResolveInstruction()->EvaluationIndex(program.evaluation_value_count);
|
||||
using Entry = ResourcePlan::EvaluationContext::Entry;
|
||||
const auto slot = code.rdx + index * sizeof(Entry);
|
||||
code.mov(code.r10d, code.dword[code.rdi + offset]);
|
||||
code.mov(code.dword[code.rsi + read->flat_offset * sizeof(uint32_t)], code.r10d);
|
||||
code.mov(code.qword[slot + offsetof(Entry, value)], code.r10);
|
||||
code.mov(code.qword[slot + offsetof(Entry, generation)], code.rcx);
|
||||
}
|
||||
for (const auto& [offset, read]: reads) {
|
||||
const auto child = pointer_uses.find(read->value.ResolveInstruction());
|
||||
if (child == pointer_uses.end()) continue;
|
||||
self(self, offset, child->second);
|
||||
}
|
||||
code.pop(code.rdi);
|
||||
};
|
||||
for (const auto& [reg, reads]: roots) {
|
||||
visit(visit, reg * sizeof(uint32_t), reads);
|
||||
}
|
||||
code.mov(code.eax, 1);
|
||||
code.jmp(finished, Xbyak::CodeGenerator::T_NEAR);
|
||||
code.L(failed);
|
||||
code.xor_(code.eax, code.eax);
|
||||
code.L(finished);
|
||||
code.mov(code.rsp, code.rbp);
|
||||
code.pop(code.rbp);
|
||||
code.ret();
|
||||
code.readyRE();
|
||||
EXIT_IF(Xbyak::GetError() != 0);
|
||||
}
|
||||
|
||||
SrtWalker::SrtWalker(const ResourcePlan& program, const SrtRuntime& runtime,
|
||||
SrtWalker* clean_evaluator)
|
||||
: m_program(program), m_runtime(runtime), m_clean_evaluator(clean_evaluator),
|
||||
m_session(this),
|
||||
m_read_policy(clean_evaluator != nullptr ? 2u : 0u), m_context(AcquireContext()) {}
|
||||
|
||||
SrtWalker::SrtWalker(SrtWalker& parent, Value active_mask, bool clean,
|
||||
SrtWalker* clean_evaluator)
|
||||
: m_program(parent.m_program), m_runtime(clean ? CleanRuntime(parent.m_runtime) : parent.m_runtime),
|
||||
m_clean_evaluator(clean_evaluator), m_active_mask(active_mask.Resolve()),
|
||||
m_reachable_blocks(parent.m_reachable_blocks), m_session(parent.m_session),
|
||||
m_read_policy((parent.m_read_policy & 1u) | (clean ? 1u : 0u) |
|
||||
(clean_evaluator != nullptr ? 2u : 0u)), m_context(AcquireContext()) {}
|
||||
|
||||
SrtWalker::~SrtWalker() {
|
||||
if (m_session != this) return;
|
||||
for (auto& context: m_program.evaluation_contexts)
|
||||
if (context.session == this) context.session = nullptr;
|
||||
}
|
||||
|
||||
bool SrtWalker::Evaluate(Value value, uint32_t& result) {
|
||||
uint64_t wide = 0;
|
||||
|
|
@ -376,15 +527,29 @@ bool SrtWalker::Evaluate(Value value, uint32_t& result) {
|
|||
return true;
|
||||
}
|
||||
|
||||
ResourcePlan::EvaluationContext& SrtWalker::AcquireContext(const ResourcePlan& program) {
|
||||
if (program.evaluation_depth == program.evaluation_contexts.size()) {
|
||||
program.evaluation_contexts.emplace_back();
|
||||
ResourcePlan::EvaluationContext& SrtWalker::AcquireContext() {
|
||||
ResourcePlan::EvaluationContext* unused = nullptr;
|
||||
for (auto& context: m_program.evaluation_contexts) {
|
||||
if (context.session == m_session && context.active_mask == m_active_mask &&
|
||||
context.read_policy == m_read_policy) return context;
|
||||
if (context.session == nullptr && unused == nullptr) unused = &context;
|
||||
}
|
||||
auto& context = program.evaluation_contexts[program.evaluation_depth++];
|
||||
context.generation += 2;
|
||||
auto& context = unused != nullptr ? *unused : m_program.evaluation_contexts.emplace_back();
|
||||
context.session = m_session;
|
||||
context.active_mask = m_active_mask;
|
||||
context.read_policy = m_read_policy;
|
||||
context.generation += 4;
|
||||
return context;
|
||||
}
|
||||
|
||||
void SrtWalker::ResetUnknowns() {
|
||||
for (auto& context: m_program.evaluation_contexts) {
|
||||
if (context.session != m_session) continue;
|
||||
for (auto& entry: context.values)
|
||||
if (entry.generation == (context.generation | 2u)) entry.generation = 0;
|
||||
}
|
||||
}
|
||||
|
||||
float SrtWalker::Float32(uint64_t bits) {
|
||||
return std::bit_cast<float>(static_cast<uint32_t>(bits));
|
||||
}
|
||||
|
|
@ -422,8 +587,10 @@ bool SrtWalker::EvaluateWide(Value value, uint64_t& result) {
|
|||
result = m_context.values[index].value;
|
||||
return true;
|
||||
}
|
||||
// The low generation bit marks an instruction that is still being evaluated.
|
||||
if (m_context.values[index].generation == (m_context.generation | 1u)) {
|
||||
// Visiting and completed-unknown entries both stop repeated evaluation in this
|
||||
// context. Only completed unknowns are retried when reachability becomes tighter.
|
||||
if (m_context.values[index].generation == (m_context.generation | 1u) ||
|
||||
m_context.values[index].generation == (m_context.generation | 2u)) {
|
||||
return false;
|
||||
}
|
||||
m_context.values[index].generation = m_context.generation | 1u;
|
||||
|
|
@ -432,7 +599,7 @@ bool SrtWalker::EvaluateWide(Value value, uint64_t& result) {
|
|||
// Recursive evaluation may grow the dense memo vector.
|
||||
auto& memo = m_context.values[index];
|
||||
if (!evaluated) {
|
||||
memo.generation = 0;
|
||||
memo.generation = m_context.generation | 2u;
|
||||
return false;
|
||||
}
|
||||
memo.value = out;
|
||||
|
|
@ -446,6 +613,19 @@ bool SrtWalker::Arg(const Inst& inst, size_t index, uint64_t& result) {
|
|||
}
|
||||
|
||||
bool SrtWalker::EvaluatePhi(const Inst& inst, uint64_t& result) {
|
||||
if (const auto control = m_program.control_phi_predecessors.find(&inst);
|
||||
control != m_program.control_phi_predecessors.end()) {
|
||||
bool found = false;
|
||||
for (size_t index = 0; index < inst.NumArgs(); ++index) {
|
||||
if (m_reachable_blocks != nullptr &&
|
||||
!m_reachable_blocks->at(control->second[index])) continue;
|
||||
uint64_t incoming = 0;
|
||||
if (!Arg(inst, index, incoming) || (found && incoming != result)) return false;
|
||||
result = incoming;
|
||||
found = true;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
const auto value = ResolveInvariantPhi(m_program, Value(const_cast<Inst*>(&inst)));
|
||||
return !value.IsEmpty() && EvaluateWide(value, result);
|
||||
}
|
||||
|
|
@ -543,7 +723,8 @@ bool SrtWalker::EvaluateRawRead(const Inst& inst, uint64_t& result) {
|
|||
? static_cast<uint64_t>(static_cast<uint32_t>(records))
|
||||
: static_cast<uint64_t>(stride) * static_cast<uint32_t>(records);
|
||||
if (byte_offset > size || size - byte_offset < sizeof(uint32_t)) {
|
||||
return false;
|
||||
result = 0u;
|
||||
return true;
|
||||
}
|
||||
address = (base & ~uint64_t {3}) + byte_offset;
|
||||
} else {
|
||||
|
|
@ -591,9 +772,8 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
|
|||
if (!m_active_mask.IsEmpty() && inst.Arg(1).Resolve() == m_active_mask) {
|
||||
return EvaluateWide(inst.Arg(0), result);
|
||||
}
|
||||
const auto clean_runtime = CleanRuntime(m_runtime);
|
||||
SrtWalker clean_active(m_program, clean_runtime, nullptr, inst.Arg(1));
|
||||
SrtWalker active(m_program, m_runtime, &clean_active, inst.Arg(1));
|
||||
SrtWalker clean_active(*this, inst.Arg(1), true);
|
||||
SrtWalker active(*this, inst.Arg(1), false, &clean_active);
|
||||
return active.EvaluateWide(inst.Arg(0), result);
|
||||
}
|
||||
case ValueOpcode::BitCastU32F32:
|
||||
|
|
@ -663,8 +843,11 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
|
|||
}
|
||||
return false;
|
||||
case ValueOpcode::UMin32:
|
||||
case ValueOpcode::UMax32:
|
||||
if (binary()) {
|
||||
result = std::min(static_cast<uint32_t>(a), static_cast<uint32_t>(b));
|
||||
result = inst.GetOpcode() == ValueOpcode::UMin32
|
||||
? std::min(static_cast<uint32_t>(a), static_cast<uint32_t>(b))
|
||||
: std::max(static_cast<uint32_t>(a), static_cast<uint32_t>(b));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -877,7 +1060,13 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
|
|||
}
|
||||
return false;
|
||||
case ValueOpcode::ULessThanEqual32:
|
||||
if (binary()) {
|
||||
// A zero lower bound accepts every U32, including a varying lane value.
|
||||
if (!Arg(inst, 0, a)) return false;
|
||||
if (a == 0u) {
|
||||
result = 1u;
|
||||
return true;
|
||||
}
|
||||
if (Arg(inst, 1, b)) {
|
||||
result = static_cast<uint32_t>(a) <= static_cast<uint32_t>(b);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -901,9 +1090,21 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
|
|||
result = 0u;
|
||||
return true;
|
||||
}
|
||||
if (!Arg(inst, 1, b) || (b != 0u && !left)) return false;
|
||||
result = b != 0u;
|
||||
return true;
|
||||
if (Arg(inst, 1, b) && (b == 0u || left)) {
|
||||
result = b != 0u;
|
||||
return true;
|
||||
}
|
||||
if (!left) {
|
||||
// Only lanes satisfying the left EXEC factor can make the conjunction
|
||||
// true. Use a separate memo so this assumption cannot escape its RHS.
|
||||
SrtWalker active(m_clean_evaluator != nullptr ? *m_clean_evaluator : *this,
|
||||
inst.Arg(0));
|
||||
if (active.EvaluateWide(inst.Arg(1), b) && b == 0u) {
|
||||
result = 0u;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case ValueOpcode::LogicalOr: {
|
||||
const bool left = Arg(inst, 0, a);
|
||||
|
|
@ -954,6 +1155,15 @@ bool SrtWalker::EvaluateDescriptor(uint32_t source, DescriptorValue& result) {
|
|||
|
||||
bool SrtWalker::RefreshFlatBuffer(std::vector<uint32_t>& flat) {
|
||||
if (!m_program.srt_plan_complete) return false;
|
||||
if (m_program.srt_code && m_runtime.read_memory == nullptr) {
|
||||
flat.resize(m_program.srt_reads.size());
|
||||
m_program.active_sources.clear();
|
||||
m_context.values.resize(m_program.evaluation_value_count);
|
||||
using Walker = bool KYTY_SYSV_ABI (*)(const uint32_t*, uint32_t*,
|
||||
ResourcePlan::EvaluationContext::Entry*, uint64_t, size_t);
|
||||
return m_program.srt_code->getCode<Walker>()(m_runtime.user_data.data(), flat.data(),
|
||||
m_context.values.data(), m_context.generation, m_runtime.user_data.size());
|
||||
}
|
||||
const auto refresh = [&](uint32_t slot) {
|
||||
if (slot >= m_program.srt_reads.size()) return false;
|
||||
const auto& read = m_program.srt_reads[slot];
|
||||
|
|
@ -973,35 +1183,54 @@ bool SrtWalker::RefreshFlatBuffer(std::vector<uint32_t>& flat) {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
auto& visited = m_program.visited_blocks;
|
||||
auto& reachable = m_program.reachable_blocks;
|
||||
auto& pending = m_program.pending_blocks;
|
||||
visited.assign(m_program.control_flow.size(), 1u);
|
||||
ResetUnknowns();
|
||||
m_reachable_blocks = &visited;
|
||||
if (m_clean_evaluator != nullptr) {
|
||||
m_clean_evaluator->ResetUnknowns();
|
||||
m_clean_evaluator->m_reachable_blocks = &visited;
|
||||
}
|
||||
for (;;) {
|
||||
reachable.assign(m_program.control_flow.size(), 0u);
|
||||
pending.clear();
|
||||
pending.push_back(0u);
|
||||
while (!pending.empty()) {
|
||||
const auto index = pending.back();
|
||||
pending.pop_back();
|
||||
if (reachable.at(index)) continue;
|
||||
reachable[index] = 1u;
|
||||
const auto& block = m_program.control_flow[index];
|
||||
uint32_t condition = 0;
|
||||
auto& predicate = m_clean_evaluator != nullptr ? *m_clean_evaluator : *this;
|
||||
if (!block.condition.IsEmpty() &&
|
||||
(m_runtime.read_specialization_memory != nullptr || !m_program.capture_specialization_reads) &&
|
||||
predicate.Evaluate(block.condition, condition)) {
|
||||
pending.push_back(block.successors[condition != 0u ? 0u : 1u]);
|
||||
} else {
|
||||
pending.insert(pending.end(), block.successors.begin(), block.successors.end());
|
||||
}
|
||||
}
|
||||
if (reachable == visited) break;
|
||||
visited.swap(reachable);
|
||||
if (m_program.control_phi_predecessors.empty()) break;
|
||||
// Only unknown values need another evaluation: successful Phi facts remain
|
||||
// true as their possible predecessors shrink. Keep uniform-read memos stable.
|
||||
ResetUnknowns();
|
||||
if (m_clean_evaluator != nullptr) m_clean_evaluator->ResetUnknowns();
|
||||
}
|
||||
flat.assign(m_program.srt_reads.size(), 0u);
|
||||
active.assign(m_program.descriptor_sources.size(), 1u);
|
||||
for (const auto& block: m_program.control_flow) {
|
||||
for (const auto& block: m_program.control_flow)
|
||||
for (const auto source: block.sources) active.at(source) = 0u;
|
||||
}
|
||||
auto& visited = m_program.visited_blocks;
|
||||
auto& pending = m_program.pending_blocks;
|
||||
visited.assign(m_program.control_flow.size(), 0u);
|
||||
pending.clear();
|
||||
pending.push_back(0u);
|
||||
while (!pending.empty()) {
|
||||
const auto index = pending.back();
|
||||
pending.pop_back();
|
||||
if (visited.at(index)) continue;
|
||||
visited[index] = 1u;
|
||||
for (uint32_t index = 0; index < visited.size(); ++index) {
|
||||
if (!visited[index]) continue;
|
||||
const auto& block = m_program.control_flow[index];
|
||||
for (const auto source: block.sources) active[source] = 1u;
|
||||
for (const auto slot: block.srt_reads) {
|
||||
for (const auto slot: block.srt_reads)
|
||||
if (!refresh(slot)) return false;
|
||||
}
|
||||
uint32_t condition = 0;
|
||||
auto& predicate = m_clean_evaluator != nullptr ? *m_clean_evaluator : *this;
|
||||
if (!block.condition.IsEmpty() &&
|
||||
(m_runtime.read_specialization_memory != nullptr || !m_program.capture_specialization_reads) &&
|
||||
predicate.Evaluate(block.condition, condition)) {
|
||||
pending.push_back(block.successors[condition != 0u ? 0u : 1u]);
|
||||
} else {
|
||||
pending.insert(pending.end(), block.successors.begin(), block.successors.end());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1010,5 +1239,9 @@ bool ValidateRuntimeValue(const ResourcePlan& program, Value value, RuntimeValue
|
|||
return RuntimeValidator(program, type).Run(value);
|
||||
}
|
||||
|
||||
bool ValidateWaveUniformValue(const ResourcePlan& program, Value value) {
|
||||
return RuntimeValidator(program, RuntimeValueType::Any, true).Run(value);
|
||||
}
|
||||
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::IR
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
#include "graphics/shader/recompiler/ir/ShaderIR.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <span>
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::IR {
|
||||
|
|
@ -45,14 +46,60 @@ enum class RuntimeValueType { Any, Integer, ImmutableInteger };
|
|||
|
||||
bool ValidateRuntimeValue(const ResourcePlan& program, Value value,
|
||||
RuntimeValueType type = RuntimeValueType::Any);
|
||||
// Uniform across a compute wave, including values derived from its workgroup ID.
|
||||
// This does not imply that the host can evaluate the value.
|
||||
bool ValidateWaveUniformValue(const ResourcePlan& program, Value value);
|
||||
void CompileSrtWalker(ResourcePlan& program);
|
||||
// Uses the strict reader for values that affect shader specialization.
|
||||
SrtRuntime CleanRuntime(SrtRuntime runtime);
|
||||
|
||||
// Nonnegative affine coefficients: element zero is constant, remaining elements are
|
||||
// caller-selected leaves. Runtime range proofs must additionally exclude U32 wrap.
|
||||
template <size_t Size>
|
||||
std::optional<std::array<uint64_t, Size>> AffineIndex(
|
||||
Value value, const auto& resolve, const auto& leaf, uint32_t depth = 0) {
|
||||
value = resolve(value);
|
||||
if (depth > 32u || value.GetType() != Type::U32) return {};
|
||||
std::array<uint64_t, Size> result {};
|
||||
if (value.IsImmediate()) {
|
||||
result[0] = value.U32();
|
||||
return result;
|
||||
}
|
||||
if (const auto index = leaf(value)) {
|
||||
result[*index] = 1u;
|
||||
return result;
|
||||
}
|
||||
const auto* inst = value.TryInstruction();
|
||||
if (inst == nullptr) return {};
|
||||
const auto op = inst->GetOpcode();
|
||||
if (op != ValueOpcode::IAdd32 && op != ValueOpcode::IMul32 &&
|
||||
op != ValueOpcode::ShiftLeftLogical32) return {};
|
||||
auto left = AffineIndex<Size>(inst->Arg(0), resolve, leaf, depth + 1u);
|
||||
auto right = AffineIndex<Size>(inst->Arg(1), resolve, leaf, depth + 1u);
|
||||
if (!left || !right) return {};
|
||||
const auto constant = [](const auto& coefficients) {
|
||||
return std::ranges::all_of(std::span(coefficients).subspan(1u),
|
||||
[](uint64_t coefficient) { return coefficient == 0u; });
|
||||
};
|
||||
if (op == ValueOpcode::IMul32 && !constant(*right)) std::swap(left, right);
|
||||
if (op != ValueOpcode::IAdd32 && !constant(*right)) return {};
|
||||
if (op == ValueOpcode::ShiftLeftLogical32) {
|
||||
if ((*right)[0] >= 32u) return {};
|
||||
(*right)[0] = uint64_t {1} << (*right)[0];
|
||||
}
|
||||
for (size_t index = 0; index < Size; ++index) {
|
||||
(*left)[index] = op == ValueOpcode::IAdd32
|
||||
? (*left)[index] + (*right)[index] : (*left)[index] * (*right)[0];
|
||||
if ((*left)[index] > UINT32_MAX) return {};
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
// One memoized evaluation session shared by the entire shader resource refresh.
|
||||
class SrtWalker {
|
||||
public:
|
||||
SrtWalker(const ResourcePlan& program, const SrtRuntime& runtime,
|
||||
SrtWalker* clean_evaluator = nullptr, Value active_mask = {});
|
||||
SrtWalker* clean_evaluator = nullptr);
|
||||
~SrtWalker();
|
||||
SrtWalker(const SrtWalker&) = delete;
|
||||
SrtWalker& operator=(const SrtWalker&) = delete;
|
||||
|
|
@ -63,7 +110,10 @@ public:
|
|||
bool RefreshFlatBuffer(std::vector<uint32_t>& flat);
|
||||
|
||||
private:
|
||||
static ResourcePlan::EvaluationContext& AcquireContext(const ResourcePlan& program);
|
||||
SrtWalker(SrtWalker& parent, Value active_mask, bool clean = false,
|
||||
SrtWalker* clean_evaluator = nullptr);
|
||||
ResourcePlan::EvaluationContext& AcquireContext();
|
||||
void ResetUnknowns();
|
||||
static float Float32(uint64_t bits);
|
||||
bool EvaluateWide(Value value, uint64_t& result);
|
||||
bool Arg(const Inst& inst, size_t index, uint64_t& result);
|
||||
|
|
@ -76,6 +126,9 @@ private:
|
|||
SrtRuntime m_runtime;
|
||||
SrtWalker* m_clean_evaluator = nullptr;
|
||||
Value m_active_mask;
|
||||
const std::vector<uint8_t>* m_reachable_blocks = nullptr;
|
||||
const SrtWalker* m_session;
|
||||
uint8_t m_read_policy;
|
||||
ResourcePlan::EvaluationContext& m_context;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -72,6 +72,9 @@ constexpr uint64_t DEFAULT_FLEXIBLE_MEMORY_SIZE = 1ull * 1024ull * 1024ull * 102
|
|||
static uint64_t g_flexible_memory_size = DEFAULT_FLEXIBLE_MEMORY_SIZE;
|
||||
static bool g_flexible_memory_size_frozen = false;
|
||||
static Graphics::RenderContext* g_gpu_resources = nullptr;
|
||||
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
||||
static void (*g_test_gpu_range)(bool, uint64_t, size_t) = nullptr;
|
||||
#endif
|
||||
|
||||
static Graphics::RenderContext& GetGpuResources() {
|
||||
EXIT_IF(g_gpu_resources == nullptr);
|
||||
|
|
@ -83,6 +86,9 @@ static bool IsGpuAddressRange(uint64_t vaddr, uint64_t size) {
|
|||
}
|
||||
|
||||
static void MapGpuRange(uint64_t vaddr, uint64_t size) {
|
||||
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
||||
if (g_test_gpu_range != nullptr) g_test_gpu_range(true, vaddr, size);
|
||||
#endif
|
||||
if (g_gpu_resources == nullptr || !IsGpuAddressRange(vaddr, size)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -90,6 +96,9 @@ static void MapGpuRange(uint64_t vaddr, uint64_t size) {
|
|||
}
|
||||
|
||||
static void UnmapGpuRange(uint64_t vaddr, uint64_t size) {
|
||||
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
||||
if (g_test_gpu_range != nullptr) g_test_gpu_range(false, vaddr, size);
|
||||
#endif
|
||||
if (g_gpu_resources == nullptr || !IsGpuAddressRange(vaddr, size)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -962,7 +971,11 @@ static bool IsInPrtAperture(uint64_t address, uint64_t size = 1) {
|
|||
}
|
||||
|
||||
bool TryReadSparseBacking(uint64_t vaddr, void* data, uint64_t size) {
|
||||
return g_guest_address_space->TryReadSparseBacking(vaddr, data, size);
|
||||
return g_guest_address_space->CopySparseMemory<false>(vaddr, data, size);
|
||||
}
|
||||
|
||||
bool CopySparseMemory(uint64_t vaddr, void* data, uint64_t size) {
|
||||
return g_guest_address_space->CopySparseMemory<true>(vaddr, data, size);
|
||||
}
|
||||
|
||||
static bool SelfTestSub64SharedPlaceholderAlias() {
|
||||
|
|
@ -2524,20 +2537,7 @@ static bool FreeGuestMemoryOwner(uint64_t vaddr, uint64_t size) {
|
|||
g_virtual_ranges->Remove(vaddr, size);
|
||||
}
|
||||
|
||||
static int UnmapMemoryRange(uint64_t vaddr, size_t len) {
|
||||
if (len == 0 || UINT64_MAX - vaddr < len) {
|
||||
return KERNEL_ERROR_EINVAL;
|
||||
}
|
||||
|
||||
VirtualRanges::Range range {};
|
||||
if (!g_virtual_ranges->Query(vaddr, 0, &range)) {
|
||||
return KERNEL_ERROR_EACCES;
|
||||
}
|
||||
const auto chunk_len = std::min<uint64_t>(len, range.size - (vaddr - range.start));
|
||||
if (chunk_len < len) {
|
||||
const int ret = UnmapMemoryRange(vaddr, chunk_len);
|
||||
return ret == OK ? UnmapMemoryRange(vaddr + chunk_len, len - chunk_len) : ret;
|
||||
}
|
||||
static int UnmapMemoryOwner(const VirtualRanges::Range& range, uint64_t vaddr, size_t len) {
|
||||
if (IsReservedRangeType(range.type)) {
|
||||
if (!g_guest_address_space->ReleaseFree(vaddr, len)) {
|
||||
return KERNEL_ERROR_EACCES;
|
||||
|
|
@ -2601,6 +2601,13 @@ static int UnmapMemoryRange(uint64_t vaddr, size_t len) {
|
|||
return OK;
|
||||
}
|
||||
|
||||
static int UnmapMemoryRange(const VirtualRanges::Range& range, uint64_t vaddr, size_t len) {
|
||||
UnmapGpuRange(vaddr, len);
|
||||
const int result = UnmapMemoryOwner(range, vaddr, len);
|
||||
if (result != OK && IsCommittedRangeType(range.type)) MapGpuRange(vaddr, len);
|
||||
return result;
|
||||
}
|
||||
|
||||
// The ABI entry point and batch operations hold g_memory_operation_mutex.
|
||||
static int UnmapMemory(uint64_t vaddr, size_t len) {
|
||||
LOGF("\t start = 0x%016" PRIx64 "\n"
|
||||
|
|
@ -2614,8 +2621,11 @@ static int UnmapMemory(uint64_t vaddr, size_t len) {
|
|||
if (!g_virtual_ranges->QuerySpan(vaddr, len, &ranges)) {
|
||||
return KERNEL_ERROR_EACCES;
|
||||
}
|
||||
UnmapGpuRange(vaddr, len);
|
||||
return UnmapMemoryRange(vaddr, len);
|
||||
for (const auto& range: ranges) {
|
||||
const int result = UnmapMemoryRange(range, range.start, range.size);
|
||||
if (result != OK) return result;
|
||||
}
|
||||
return OK;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI KernelMunmap(uint64_t vaddr, size_t len) {
|
||||
|
|
@ -2624,6 +2634,31 @@ int KYTY_SYSV_ABI KernelMunmap(uint64_t vaddr, size_t len) {
|
|||
return UnmapMemory(vaddr, len);
|
||||
}
|
||||
|
||||
static bool IsAmmMemoryRange(uint64_t vaddr, size_t size) {
|
||||
constexpr uint64_t page_size = 0x4000;
|
||||
return size != 0 && ((vaddr | size) & (page_size - 1)) == 0 &&
|
||||
vaddr >= kExtendedMemoryBase && vaddr < kExtendedMemoryBase + kExtendedMemorySize &&
|
||||
size <= kExtendedMemoryBase + kExtendedMemorySize - vaddr;
|
||||
}
|
||||
|
||||
int UnmapAmmMemory(uint64_t vaddr, size_t size) {
|
||||
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
|
||||
if (!IsAmmMemoryRange(vaddr, size)) return KERNEL_ERROR_EINVAL;
|
||||
|
||||
const auto end = vaddr + size;
|
||||
while (vaddr < end) {
|
||||
VirtualRanges::Range range {};
|
||||
// AMM unmap accepts holes, including a completely unmapped range.
|
||||
if (!g_virtual_ranges->Query(vaddr, 1, &range) || range.start >= end) break;
|
||||
vaddr = std::max(vaddr, range.start);
|
||||
const auto chunk = std::min(end, range.start + range.size) - vaddr;
|
||||
const int result = UnmapMemoryRange(range, vaddr, chunk);
|
||||
if (result != OK) return result;
|
||||
vaddr += chunk;
|
||||
}
|
||||
return OK;
|
||||
}
|
||||
|
||||
size_t KYTY_SYSV_ABI KernelGetDirectMemorySize() {
|
||||
PRINT_NAME();
|
||||
|
||||
|
|
@ -3225,11 +3260,7 @@ int KYTY_SYSV_ABI KernelMapDirectMemory2(void** addr, size_t len, int type, int
|
|||
int MapAutomaticMemory(uint64_t vaddr, size_t size, int type, int prot) {
|
||||
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
|
||||
constexpr uint64_t page_size = 0x4000;
|
||||
if (size == 0 || (vaddr & (page_size - 1)) != 0 || (size & (page_size - 1)) != 0 ||
|
||||
vaddr < kExtendedMemoryBase || vaddr >= kExtendedMemoryBase + kExtendedMemorySize ||
|
||||
size > kExtendedMemoryBase + kExtendedMemorySize - vaddr) {
|
||||
return KERNEL_ERROR_EINVAL;
|
||||
}
|
||||
if (!IsAmmMemoryRange(vaddr, size)) return KERNEL_ERROR_EINVAL;
|
||||
|
||||
PhysicalMemory::PhysicalRanges ranges;
|
||||
if (!g_physical_memory->ReserveAutomatic(size, &ranges)) {
|
||||
|
|
@ -3567,6 +3598,10 @@ int KYTY_SYSV_ABI KernelReserveVirtualRange(void** addr, size_t len, int flags,
|
|||
}
|
||||
|
||||
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
||||
void TestSetGpuRangeCallback(void (*callback)(bool, uint64_t, size_t)) {
|
||||
g_test_gpu_range = callback;
|
||||
}
|
||||
|
||||
void TestBeforeNextBackingMap(callback_func_t callback) {
|
||||
g_test_before_backing_map = callback;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,10 +112,12 @@ void SetFlexibleMemorySize(uint64_t size);
|
|||
int AllocateDirectMemory(int64_t search_start, int64_t search_end, size_t size, size_t alignment,
|
||||
int memory_type, int64_t* phys_addr_out, bool automatic = false);
|
||||
int MapAutomaticMemory(uint64_t vaddr, size_t size, int type, int prot);
|
||||
int UnmapAmmMemory(uint64_t vaddr, size_t size);
|
||||
bool TryWriteBacking(uint64_t vaddr, const void* data, uint64_t size);
|
||||
bool TryReadBacking(uint64_t vaddr, void* data, uint64_t size);
|
||||
bool TryReadBufferBacking(uint64_t vaddr, void* data, uint64_t size);
|
||||
bool TryReadSparseBacking(uint64_t vaddr, void* data, uint64_t size);
|
||||
bool CopySparseMemory(uint64_t vaddr, void* data, uint64_t size);
|
||||
[[nodiscard]] uint64_t ClampRangeSize(uint64_t vaddr, uint64_t size);
|
||||
void WriteBacking(uint64_t vaddr, const void* data, uint64_t size) noexcept;
|
||||
void InvalidateMemory(uint64_t vaddr, uint64_t size);
|
||||
|
|
@ -191,6 +193,7 @@ bool ProtectGuestHostMemory(uint64_t vaddr, uint64_t size, Common::VirtualMemory
|
|||
bool FreeGuestMemory(uint64_t vaddr, uint64_t size);
|
||||
|
||||
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
||||
void TestSetGpuRangeCallback(void (*callback)(bool mapped, uint64_t addr, size_t size));
|
||||
void TestBeforeNextBackingMap(callback_func_t callback);
|
||||
void TestSetBackingReadCallback(callback_func_t callback);
|
||||
void TestFailNextPhysicalMemoryUnmap();
|
||||
|
|
|
|||
|
|
@ -28,6 +28,35 @@ static bool CoalescePlaceholders(uint64_t vaddr, uint64_t size) {
|
|||
}
|
||||
#endif
|
||||
|
||||
template <typename Regions, typename Source, typename Gap>
|
||||
static void CopySparseRange(const Regions& regions, uint64_t vaddr, void* data, uint64_t size,
|
||||
Source&& source, Gap&& gap) {
|
||||
const auto end = vaddr + size;
|
||||
auto mapping = regions.lower_bound(vaddr);
|
||||
if (mapping != regions.begin()) {
|
||||
auto previous = std::prev(mapping);
|
||||
if (previous->second.vaddr + previous->second.size > vaddr) {
|
||||
mapping = previous;
|
||||
}
|
||||
}
|
||||
|
||||
auto* destination = static_cast<uint8_t*>(data);
|
||||
auto cursor = vaddr;
|
||||
for (; mapping != regions.end() && mapping->second.vaddr < end; ++mapping) {
|
||||
const auto& entry = mapping->second;
|
||||
const auto start = std::max(vaddr, entry.vaddr);
|
||||
const auto bytes = std::min(end, entry.vaddr + entry.size) - start;
|
||||
if (cursor < start) {
|
||||
gap(cursor, destination + (cursor - vaddr), start - cursor);
|
||||
}
|
||||
std::memcpy(destination + (start - vaddr), source(entry, start), bytes);
|
||||
cursor = start + bytes;
|
||||
}
|
||||
if (cursor < end) {
|
||||
gap(cursor, destination + (cursor - vaddr), end - cursor);
|
||||
}
|
||||
}
|
||||
|
||||
class GuestBackingStore {
|
||||
public:
|
||||
enum class FailureReason {
|
||||
|
|
@ -247,7 +276,8 @@ public:
|
|||
return true;
|
||||
}
|
||||
|
||||
bool TryReadSparseBacking(uint64_t vaddr, void* data, uint64_t size) {
|
||||
template <typename Gap>
|
||||
bool TryReadSparseBacking(uint64_t vaddr, void* data, uint64_t size, Gap&& gap) {
|
||||
if (!IsAvailable() || data == nullptr || size == 0 || UINT64_MAX - vaddr < size) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -258,31 +288,11 @@ public:
|
|||
g_test_backing_read(vaddr, size);
|
||||
}
|
||||
#endif
|
||||
const auto end = vaddr + size;
|
||||
auto mapping = m_maps.lower_bound(vaddr);
|
||||
if (mapping != m_maps.begin()) {
|
||||
auto previous = std::prev(mapping);
|
||||
if (previous->second.vaddr + previous->second.size > vaddr) {
|
||||
mapping = previous;
|
||||
}
|
||||
}
|
||||
|
||||
auto* destination = static_cast<uint8_t*>(data);
|
||||
auto cursor = vaddr;
|
||||
for (; mapping != m_maps.end() && mapping->second.vaddr < end; ++mapping) {
|
||||
const auto& entry = mapping->second;
|
||||
const auto start = std::max(vaddr, entry.vaddr);
|
||||
const auto bytes = std::min(end, entry.vaddr + entry.size) - start;
|
||||
const auto offset = entry.backing_offset + start - entry.vaddr;
|
||||
if (cursor < start) {
|
||||
std::memset(destination + (cursor - vaddr), 0, start - cursor);
|
||||
}
|
||||
std::memcpy(destination + (start - vaddr), m_backing_base + offset, bytes);
|
||||
cursor = start + bytes;
|
||||
}
|
||||
if (cursor < end) {
|
||||
std::memset(destination + (cursor - vaddr), 0, end - cursor);
|
||||
}
|
||||
CopySparseRange(m_maps, vaddr, data, size,
|
||||
[this](const auto& entry, uint64_t start) {
|
||||
return m_backing_base + (entry.backing_offset + start - entry.vaddr);
|
||||
},
|
||||
gap);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -773,22 +783,41 @@ public:
|
|||
return m_backing->TryReadBacking(vaddr, data, size);
|
||||
}
|
||||
|
||||
bool TryReadSparseBacking(uint64_t vaddr, void* data, uint64_t size) {
|
||||
if (!Owns(vaddr, size)) {
|
||||
template <bool copy_private>
|
||||
bool CopySparseMemory(uint64_t vaddr, void* data, uint64_t size) {
|
||||
if (data == nullptr || !Owns(vaddr, size)) {
|
||||
return false;
|
||||
}
|
||||
std::shared_lock lock(m_mutex);
|
||||
auto mapping = m_mapped.upper_bound(vaddr);
|
||||
if (mapping != m_mapped.begin()) {
|
||||
--mapping;
|
||||
}
|
||||
for (; mapping != m_mapped.end() && mapping->second.vaddr < vaddr + size; ++mapping) {
|
||||
const auto& region = mapping->second;
|
||||
if (region.type == MappingType::Private && region.vaddr + region.size > vaddr) {
|
||||
return false;
|
||||
if constexpr (!copy_private) {
|
||||
auto mapping = m_mapped.upper_bound(vaddr);
|
||||
if (mapping != m_mapped.begin()) {
|
||||
--mapping;
|
||||
}
|
||||
for (; mapping != m_mapped.end() && mapping->second.vaddr < vaddr + size; ++mapping) {
|
||||
const auto& region = mapping->second;
|
||||
if (region.type == MappingType::Private && region.vaddr + region.size > vaddr) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return m_backing->TryReadSparseBacking(vaddr, data, size);
|
||||
return m_backing->TryReadSparseBacking(
|
||||
vaddr, data, size, [&](uint64_t start, uint8_t* destination, uint64_t bytes) {
|
||||
const auto zero = [](uint64_t, uint8_t* output, uint64_t length) {
|
||||
std::memset(output, 0, length);
|
||||
};
|
||||
if constexpr (copy_private) {
|
||||
// Backing gaps can contain private mappings or reservations only.
|
||||
CopySparseRange(m_mapped, start, destination, bytes,
|
||||
[](const auto& region, uint64_t address) {
|
||||
EXIT_IF(region.type != MappingType::Private);
|
||||
return reinterpret_cast<const void*>(address);
|
||||
},
|
||||
zero);
|
||||
} else {
|
||||
zero(start, destination, bytes);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool BackingContains(uint64_t vaddr, uint64_t size) { return m_backing->Contains(vaddr, size); }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
|
||||
find_package(Qt6 COMPONENTS Concurrent Network Widgets REQUIRED)
|
||||
add_subdirectory("${KYTY_SOURCE_DIR}/updater" "${CMAKE_CURRENT_BINARY_DIR}/updater")
|
||||
|
||||
set(CMAKE_AUTOUIC_SEARCH_PATHS forms)
|
||||
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||
|
|
@ -42,7 +43,13 @@ elseif(LINUX)
|
|||
set_target_properties(launcher PROPERTIES LINK_FLAGS "${KYTY_LD_OPTIONS}")
|
||||
endif()
|
||||
|
||||
add_dependencies(launcher kyty_emulator)
|
||||
add_custom_target(stage_updater
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_FILE_DIR:launcher>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:kyty_updater>
|
||||
$<TARGET_FILE_DIR:launcher>/$<TARGET_FILE_NAME:kyty_updater>
|
||||
DEPENDS kyty_updater
|
||||
VERBATIM)
|
||||
add_dependencies(launcher kyty_emulator stage_updater)
|
||||
|
||||
if(KYTY_CLANG_CL)
|
||||
add_custom_command(TARGET launcher POST_BUILD
|
||||
|
|
@ -96,6 +103,7 @@ install(TARGETS launcher
|
|||
|
||||
if(APPLE)
|
||||
install(TARGETS kyty_emulator DESTINATION "KytyPS5.app/Contents/MacOS")
|
||||
install(TARGETS kyty_updater DESTINATION "KytyPS5.app/Contents/MacOS")
|
||||
install(FILES "${KYTY_TROPHY_UNLOCK_SOUND}" DESTINATION "KytyPS5.app/Contents/Resources/assets/sounds")
|
||||
# Deploy and ad-hoc sign the Qt frameworks and plugins in the bundle.
|
||||
find_program(KYTY_MACDEPLOYQT NAMES macdeployqt PATHS "${Qt6_DIR}/../../../bin" NO_DEFAULT_PATH)
|
||||
|
|
@ -112,6 +120,10 @@ if(APPLE)
|
|||
")
|
||||
endif()
|
||||
|
||||
if(NOT APPLE)
|
||||
install(TARGETS kyty_updater RUNTIME DESTINATION .)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
install(CODE "
|
||||
execute_process(
|
||||
|
|
|
|||
|
|
@ -16,394 +16,446 @@
|
|||
<property name="locale">
|
||||
<locale language="English" country="UnitedStates"/>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="horizontalSpacing">
|
||||
<number>8</number>
|
||||
</property>
|
||||
<property name="verticalSpacing">
|
||||
<number>8</number>
|
||||
</property>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<layout class="QGridLayout" name="sectionsLayout" columnstretch="1,1">
|
||||
<property name="horizontalSpacing">
|
||||
<number>8</number>
|
||||
</property>
|
||||
<property name="verticalSpacing">
|
||||
<number>8</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QGroupBox" name="user_profile_group">
|
||||
<property name="title">
|
||||
<string>User profile</string>
|
||||
<layout class="QVBoxLayout" name="dialogLayout">
|
||||
<property name="spacing"><number>8</number></property>
|
||||
<item>
|
||||
<widget class="QTabWidget" name="settings_tabs">
|
||||
<property name="currentIndex"><number>0</number></property>
|
||||
<widget class="QWidget" name="settings_tab">
|
||||
<attribute name="title"><string>General</string></attribute>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="horizontalSpacing">
|
||||
<number>8</number>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="userProfileLayout">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_user_name">
|
||||
<property name="text"><string>User name:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="MandatoryLineEdit" name="lineEdit_user_name">
|
||||
<property name="toolTip"><string>Local user name exposed to games</string></property>
|
||||
<property name="clearButtonEnabled"><bool>true</bool></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_user_id">
|
||||
<property name="text"><string>User ID:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QSpinBox" name="spinBox_user_id">
|
||||
<property name="toolTip"><string>Local user ID exposed to games</string></property>
|
||||
<property name="maximum"><number>2147483647</number></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_console_language">
|
||||
<property name="text"><string>Console language:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="comboBox_console_language">
|
||||
<property name="toolTip"><string>Language</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QGroupBox" name="audio_group">
|
||||
<property name="title"><string>Audio</string></property>
|
||||
<layout class="QFormLayout" name="audioLayout">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_audio_input_device">
|
||||
<property name="text"><string>Microphone:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="comboBox_audio_input_device"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1" rowspan="3">
|
||||
<widget class="QGroupBox" name="graphics_group">
|
||||
<property name="title">
|
||||
<string>Graphics && display</string>
|
||||
<property name="verticalSpacing">
|
||||
<number>8</number>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="graphicsLayout">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_screen_resolution">
|
||||
<property name="text"><string>Screen resolution:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="comboBox_screen_resolution">
|
||||
<property name="toolTip"><string>Window resolution</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_present_mode">
|
||||
<property name="text"><string>Present mode:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="comboBox_present_mode">
|
||||
<property name="toolTip"><string>Vulkan swapchain presentation mode</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_vblank_frequency">
|
||||
<property name="text"><string>Vblank frequency:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QSpinBox" name="spinBox_vblank_frequency">
|
||||
<property name="toolTip"><string>Virtual display refresh rate used for frame pacing</string></property>
|
||||
<property name="minimum"><number>30</number></property>
|
||||
<property name="maximum"><number>360</number></property>
|
||||
<property name="value"><number>60</number></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_shader_optimization">
|
||||
<property name="text"><string>Shader optimization:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QComboBox" name="comboBox_shader_optimization_type">
|
||||
<property name="toolTip"><string>Optimize shaders for code size or performance</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label_gpu">
|
||||
<property name="text"><string>GPU:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QComboBox" name="comboBox_gpu">
|
||||
<property name="toolTip"><string>Graphics device used by the emulator</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="QCheckBox" name="checkBox_fullscreen">
|
||||
<property name="toolTip"><string>Run in borderless desktop fullscreen</string></property>
|
||||
<property name="text"><string>Fullscreen</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<widget class="QCheckBox" name="checkBox_readback">
|
||||
<property name="toolTip"><string>Read back writable linear images</string></property>
|
||||
<property name="text"><string>Enable readback</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<widget class="QCheckBox" name="checkBox_tessellation">
|
||||
<property name="toolTip"><string>Draw tessellated geometry. Disabled by default; unsupported tessellation may cause crashes.</string></property>
|
||||
<property name="text"><string>Enable tessellation support (experimental)</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<widget class="QCheckBox" name="checkBox_hide_cursor">
|
||||
<property name="toolTip"><string>Hide the host cursor after 2 seconds without mouse activity</string></property>
|
||||
<property name="text"><string>Auto-hide cursor</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="1">
|
||||
<widget class="QCheckBox" name="checkBox_skip_notice_screen">
|
||||
<property name="toolTip"><string>Skip startup logos and notices in supported games.</string></property>
|
||||
<property name="text"><string>Skip boot logos</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="1">
|
||||
<widget class="QCheckBox" name="checkBox_sync_raw_image_buffers">
|
||||
<property name="toolTip"><string>Can fix missing textures and incorrect transparency in some games. May reduce performance.</string></property>
|
||||
<property name="text"><string>Accurate texture synchronization</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QGroupBox" name="notifications_group">
|
||||
<property name="title"><string>Notifications</string></property>
|
||||
<layout class="QVBoxLayout" name="notificationsLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_trophy_notifications">
|
||||
<property name="toolTip"><string>Show trophy unlock notifications and play their sound.
|
||||
To use your own sound, replace the emulator's assets/sounds/trophy-unlock.wav with a WAV file and restart the game.</string></property>
|
||||
<property name="text"><string>Enable trophy notifications</string></property>
|
||||
<property name="checked"><bool>true</bool></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0" colspan="2">
|
||||
<widget class="QGroupBox" name="debug_group">
|
||||
<property name="title">
|
||||
<string>Debugging && logs</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="debugLayout" columnstretch="0,1,0,1">
|
||||
<property name="horizontalSpacing"><number>8</number></property>
|
||||
<property name="verticalSpacing"><number>6</number></property>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="checkBox_vulkan_validation">
|
||||
<property name="toolTip"><string>Enable Vulkan validation layers</string></property>
|
||||
<property name="text"><string>Vulkan validation</string></property>
|
||||
<property name="checked"><bool>true</bool></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2" colspan="2">
|
||||
<widget class="QCheckBox" name="checkBox_shader_validation">
|
||||
<property name="toolTip"><string>Validate SPIR-V binary</string></property>
|
||||
<property name="text"><string>Shader validation</string></property>
|
||||
<property name="checked"><bool>true</bool></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="checkBox_renderdoc_capture">
|
||||
<property name="toolTip"><string>Enable RenderDoc capture</string></property>
|
||||
<property name="text"><string>RenderDoc capture</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2" colspan="2">
|
||||
<widget class="QCheckBox" name="checkBox_cmd_dump">
|
||||
<property name="toolTip"><string>Dump command buffers</string></property>
|
||||
<property name="text"><string>Command buffer dump</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_shader_log_direction">
|
||||
<property name="text"><string>Shader logging:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="comboBox_shader_log_direction">
|
||||
<property name="toolTip"><string>Dump shaders to file or console window. If enabled may decrease emulator performance</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QLabel" name="label_shader_log_folder">
|
||||
<property name="text"><string>Shader log folder:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="3">
|
||||
<widget class="MandatoryLineEdit" name="lineEdit_shader_log_folder">
|
||||
<property name="toolTip"><string>Specify directory to dump shaders</string></property>
|
||||
<property name="clearButtonEnabled"><bool>true</bool></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_printf_direction">
|
||||
<property name="text"><string>Printf output:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QComboBox" name="comboBox_printf_direction">
|
||||
<property name="toolTip"><string>Print logs to file or console window. If enabled may decrease emulator performance</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="QLabel" name="label_printf_file">
|
||||
<property name="text"><string>Printf output file:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="3">
|
||||
<widget class="MandatoryLineEdit" name="lineEdit_printf_file">
|
||||
<property name="toolTip"><string>Specify file to dump logs</string></property>
|
||||
<property name="clearButtonEnabled"><bool>true</bool></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QCheckBox" name="checkBox_profiler">
|
||||
<property name="toolTip"><string>Enable the Tracy profiler. Profiling may decrease emulator performance</string></property>
|
||||
<property name="text"><string>Tracy profiler</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<widget class="QLabel" name="label_cmd_dump_folder">
|
||||
<property name="text"><string>Command buffer folder:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="3">
|
||||
<widget class="MandatoryLineEdit" name="lineEdit_cmd_dump_folder">
|
||||
<property name="toolTip"><string>Specify directory to dump command buffers</string></property>
|
||||
<property name="clearButtonEnabled"><bool>true</bool></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QGroupBox" name="compatibility_group">
|
||||
<property name="title">
|
||||
<string>Compatibility</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="compatibilityLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_amd_cpu">
|
||||
<property name="toolTip"><string>Patch CPU instructions that differ between AMD and the host CPU.</string></property>
|
||||
<property name="text"><string>AMD CPU patch (experimental)</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_red_zone_protection">
|
||||
<property name="toolTip"><string>Helps prevent crashes in some games on Windows</string></property>
|
||||
<property name="text"><string>Windows SysV red zone crash protection (experimental)</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QGroupBox" name="controller_group">
|
||||
<property name="title"><string>Controller</string></property>
|
||||
<layout class="QFormLayout" name="controllerLayout">
|
||||
<property name="fieldGrowthPolicy"><enum>QFormLayout::AllNonFixedFieldsGrow</enum></property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_controller_color">
|
||||
<property name="text"><string>DualSense lightbar:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<layout class="QHBoxLayout" name="controllerColorLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_controller_color">
|
||||
<property name="toolTip"><string>Choose and preview a fixed LED color. Bluetooth DualSense may take several seconds to update.</string></property>
|
||||
<property name="text"><string>Custom</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_controller_color_reset">
|
||||
<property name="toolTip"><string>Let the game control the lightbar</string></property>
|
||||
<property name="text"><string>Default</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_controller_vibration">
|
||||
<property name="text"><string>Vibration intensity:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<layout class="QHBoxLayout" name="controllerVibrationLayout">
|
||||
<item>
|
||||
<widget class="QSlider" name="slider_controller_vibration">
|
||||
<property name="toolTip"><string>Scale DualSense vibration over USB and Bluetooth</string></property>
|
||||
<property name="maximum"><number>100</number></property>
|
||||
<property name="value"><number>100</number></property>
|
||||
<property name="orientation"><enum>Qt::Horizontal</enum></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_controller_vibration_value">
|
||||
<property name="minimumSize"><size><width>40</width><height>0</height></size></property>
|
||||
<property name="text"><string>100%</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_controller_volume">
|
||||
<property name="text"><string>Speaker volume:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<layout class="QHBoxLayout" name="controllerVolumeLayout">
|
||||
<item>
|
||||
<widget class="QSlider" name="slider_controller_volume">
|
||||
<property name="toolTip"><string>Scale the DualSense speaker over USB and Bluetooth. 50% is the previous default; 100% adds 6 dB of gain.</string></property>
|
||||
<property name="maximum"><number>100</number></property>
|
||||
<property name="value"><number>50</number></property>
|
||||
<property name="orientation"><enum>Qt::Horizontal</enum></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_controller_volume_value">
|
||||
<property name="minimumSize"><size><width>40</width><height>0</height></size></property>
|
||||
<property name="text"><string>50%</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<layout class="QGridLayout" name="sectionsLayout" columnstretch="1,1">
|
||||
<property name="horizontalSpacing">
|
||||
<number>8</number>
|
||||
</property>
|
||||
<property name="verticalSpacing">
|
||||
<number>8</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QGroupBox" name="user_profile_group">
|
||||
<property name="title">
|
||||
<string>User profile</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="userProfileLayout">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_user_name">
|
||||
<property name="text"><string>User name:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="MandatoryLineEdit" name="lineEdit_user_name">
|
||||
<property name="toolTip"><string>Local user name exposed to games</string></property>
|
||||
<property name="clearButtonEnabled"><bool>true</bool></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_user_id">
|
||||
<property name="text"><string>User ID:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QSpinBox" name="spinBox_user_id">
|
||||
<property name="toolTip"><string>Local user ID exposed to games</string></property>
|
||||
<property name="maximum"><number>2147483647</number></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_console_language">
|
||||
<property name="text"><string>Console language:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="comboBox_console_language">
|
||||
<property name="toolTip"><string>Language</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QGroupBox" name="audio_group">
|
||||
<property name="title"><string>Audio</string></property>
|
||||
<layout class="QFormLayout" name="audioLayout">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_audio_input_device">
|
||||
<property name="text"><string>Microphone:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="comboBox_audio_input_device"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1" rowspan="3">
|
||||
<widget class="QGroupBox" name="graphics_group">
|
||||
<property name="title">
|
||||
<string>Graphics && display</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="graphicsLayout">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_screen_resolution">
|
||||
<property name="text"><string>Screen resolution:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="comboBox_screen_resolution">
|
||||
<property name="toolTip"><string>Window resolution</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_present_mode">
|
||||
<property name="text"><string>Present mode:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="comboBox_present_mode">
|
||||
<property name="toolTip"><string>Vulkan swapchain presentation mode</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_vblank_frequency">
|
||||
<property name="text"><string>Vblank frequency:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QSpinBox" name="spinBox_vblank_frequency">
|
||||
<property name="toolTip"><string>Virtual display refresh rate used for frame pacing</string></property>
|
||||
<property name="minimum"><number>30</number></property>
|
||||
<property name="maximum"><number>360</number></property>
|
||||
<property name="value"><number>60</number></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_shader_optimization">
|
||||
<property name="text"><string>Shader optimization:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QComboBox" name="comboBox_shader_optimization_type">
|
||||
<property name="toolTip"><string>Optimize shaders for code size or performance</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label_gpu">
|
||||
<property name="text"><string>GPU:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QComboBox" name="comboBox_gpu">
|
||||
<property name="toolTip"><string>Graphics device used by the emulator</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="QCheckBox" name="checkBox_readback">
|
||||
<property name="toolTip"><string>Read back writable linear images</string></property>
|
||||
<property name="text"><string>Enable readback</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<widget class="QCheckBox" name="checkBox_tessellation">
|
||||
<property name="toolTip"><string>Draw tessellated geometry. Disabled by default; unsupported tessellation may cause crashes.</string></property>
|
||||
<property name="text"><string>Enable tessellation support (experimental)</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<widget class="QCheckBox" name="checkBox_sync_raw_image_buffers">
|
||||
<property name="toolTip"><string>Can fix missing textures and incorrect transparency in some games. May reduce performance.</string></property>
|
||||
<property name="text"><string>Accurate texture synchronization</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<widget class="QCheckBox" name="checkBox_hdr">
|
||||
<property name="toolTip"><string>Enable HDR10 output for supported games. Requires HDR enabled on the host display.</string></property>
|
||||
<property name="text"><string>Enable HDR display support</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QGroupBox" name="notifications_group">
|
||||
<property name="title"><string>Notifications</string></property>
|
||||
<layout class="QVBoxLayout" name="notificationsLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_trophy_notifications">
|
||||
<property name="toolTip"><string>Show trophy unlock notifications and play their sound.
|
||||
To use your own sound, replace the emulator's assets/sounds/trophy-unlock.wav with a WAV file and restart the game.</string></property>
|
||||
<property name="text"><string>Enable trophy notifications</string></property>
|
||||
<property name="checked"><bool>true</bool></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QGroupBox" name="general_group">
|
||||
<property name="title"><string>General</string></property>
|
||||
<layout class="QVBoxLayout" name="generalLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_fullscreen">
|
||||
<property name="toolTip"><string>Run in borderless desktop fullscreen</string></property>
|
||||
<property name="text"><string>Run in fullscreen</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_hide_cursor">
|
||||
<property name="toolTip"><string>Hide the host cursor after 2 seconds without mouse activity</string></property>
|
||||
<property name="text"><string>Auto-hide cursor</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_skip_notice_screen">
|
||||
<property name="toolTip"><string>Skip startup logos and notices in supported games.</string></property>
|
||||
<property name="text"><string>Skip boot logos</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QGroupBox" name="compatibility_group">
|
||||
<property name="title">
|
||||
<string>Compatibility</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="compatibilityLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_amd_cpu">
|
||||
<property name="toolTip"><string>Patch CPU instructions that differ between AMD and the host CPU.</string></property>
|
||||
<property name="text"><string>AMD CPU patch (experimental)</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_red_zone_protection">
|
||||
<property name="toolTip"><string>Helps prevent crashes in some games on Windows</string></property>
|
||||
<property name="text"><string>Windows SysV red zone crash protection (experimental)</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QGroupBox" name="controller_group">
|
||||
<property name="title"><string>Controller</string></property>
|
||||
<layout class="QFormLayout" name="controllerLayout">
|
||||
<property name="fieldGrowthPolicy"><enum>QFormLayout::AllNonFixedFieldsGrow</enum></property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_controller_color">
|
||||
<property name="text"><string>DualSense lightbar:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<layout class="QHBoxLayout" name="controllerColorLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_controller_color">
|
||||
<property name="toolTip"><string>Choose and preview a fixed LED color. Bluetooth DualSense may take several seconds to update.</string></property>
|
||||
<property name="text"><string>Custom</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_controller_color_reset">
|
||||
<property name="toolTip"><string>Let the game control the lightbar</string></property>
|
||||
<property name="text"><string>Default</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_controller_vibration">
|
||||
<property name="text"><string>Vibration intensity:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<layout class="QHBoxLayout" name="controllerVibrationLayout">
|
||||
<item>
|
||||
<widget class="QSlider" name="slider_controller_vibration">
|
||||
<property name="toolTip"><string>Scale DualSense vibration over USB and Bluetooth</string></property>
|
||||
<property name="maximum"><number>100</number></property>
|
||||
<property name="value"><number>100</number></property>
|
||||
<property name="orientation"><enum>Qt::Horizontal</enum></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_controller_vibration_value">
|
||||
<property name="minimumSize"><size><width>40</width><height>0</height></size></property>
|
||||
<property name="text"><string>100%</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_controller_volume">
|
||||
<property name="text"><string>Speaker volume:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<layout class="QHBoxLayout" name="controllerVolumeLayout">
|
||||
<item>
|
||||
<widget class="QSlider" name="slider_controller_volume">
|
||||
<property name="toolTip"><string>Scale the DualSense speaker over USB and Bluetooth. 50% is the previous default; 100% adds 6 dB of gain.</string></property>
|
||||
<property name="maximum"><number>100</number></property>
|
||||
<property name="value"><number>50</number></property>
|
||||
<property name="orientation"><enum>Qt::Horizontal</enum></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_controller_volume_value">
|
||||
<property name="minimumSize"><size><width>40</width><height>0</height></size></property>
|
||||
<property name="text"><string>50%</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="network_tab">
|
||||
<attribute name="title"><string>Network</string></attribute>
|
||||
<layout class="QVBoxLayout" name="networkTabLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_network_connected">
|
||||
<property name="toolTip"><string>Let games use your computer's local network connection.</string></property>
|
||||
<property name="text"><string>Enable network connection</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="networkSpacer">
|
||||
<property name="orientation"><enum>Qt::Vertical</enum></property>
|
||||
<property name="sizeHint" stdset="0"><size><width>20</width><height>40</height></size></property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="debug_tab">
|
||||
<attribute name="title"><string>Debug</string></attribute>
|
||||
<layout class="QVBoxLayout" name="debugTabLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="debug_group">
|
||||
<property name="title">
|
||||
<string>Debugging && logs</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="debugLayout" columnstretch="0,1,0,1">
|
||||
<property name="horizontalSpacing"><number>8</number></property>
|
||||
<property name="verticalSpacing"><number>6</number></property>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="checkBox_vulkan_validation">
|
||||
<property name="toolTip"><string>Enable Vulkan validation layers</string></property>
|
||||
<property name="text"><string>Vulkan validation</string></property>
|
||||
<property name="checked"><bool>true</bool></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2" colspan="2">
|
||||
<widget class="QCheckBox" name="checkBox_shader_validation">
|
||||
<property name="toolTip"><string>Validate SPIR-V binary</string></property>
|
||||
<property name="text"><string>Shader validation</string></property>
|
||||
<property name="checked"><bool>true</bool></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="checkBox_renderdoc_capture">
|
||||
<property name="toolTip"><string>Enable RenderDoc capture</string></property>
|
||||
<property name="text"><string>RenderDoc capture</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2" colspan="2">
|
||||
<widget class="QCheckBox" name="checkBox_cmd_dump">
|
||||
<property name="toolTip"><string>Dump command buffers</string></property>
|
||||
<property name="text"><string>Command buffer dump</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_shader_log_direction">
|
||||
<property name="text"><string>Shader logging:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="comboBox_shader_log_direction">
|
||||
<property name="toolTip"><string>Dump shaders to file or console window. If enabled may decrease emulator performance</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QLabel" name="label_shader_log_folder">
|
||||
<property name="text"><string>Shader log folder:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="3">
|
||||
<widget class="MandatoryLineEdit" name="lineEdit_shader_log_folder">
|
||||
<property name="toolTip"><string>Specify directory to dump shaders</string></property>
|
||||
<property name="clearButtonEnabled"><bool>true</bool></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_printf_direction">
|
||||
<property name="text"><string>Printf output:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QComboBox" name="comboBox_printf_direction">
|
||||
<property name="toolTip"><string>Print logs to file or console window. If enabled may decrease emulator performance</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="QLabel" name="label_printf_file">
|
||||
<property name="text"><string>Printf output file:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="3">
|
||||
<widget class="MandatoryLineEdit" name="lineEdit_printf_file">
|
||||
<property name="toolTip"><string>Specify file to dump logs</string></property>
|
||||
<property name="clearButtonEnabled"><bool>true</bool></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QCheckBox" name="checkBox_profiler">
|
||||
<property name="toolTip"><string>Enable the Tracy profiler. Profiling may decrease emulator performance</string></property>
|
||||
<property name="text"><string>Tracy profiler</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<widget class="QLabel" name="label_cmd_dump_folder">
|
||||
<property name="text"><string>Command buffer folder:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="3">
|
||||
<widget class="MandatoryLineEdit" name="lineEdit_cmd_dump_folder">
|
||||
<property name="toolTip"><string>Specify directory to dump command buffers</string></property>
|
||||
<property name="clearButtonEnabled"><bool>true</bool></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="debugSpacer">
|
||||
<property name="orientation"><enum>Qt::Vertical</enum></property>
|
||||
<property name="sizeHint" stdset="0"><size><width>20</width><height>40</height></size></property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="buttonLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="clear_button">
|
||||
|
|
|
|||
|
|
@ -117,12 +117,14 @@ public:
|
|||
PresentMode present_mode = PresentMode::Mailbox;
|
||||
int gpu_index = -1;
|
||||
bool fullscreen_enabled = false;
|
||||
bool hdr_enabled = false;
|
||||
bool hide_cursor_enabled = false;
|
||||
bool readback_linear_images = false;
|
||||
bool sync_raw_image_buffers = false;
|
||||
bool tessellation_enabled = false;
|
||||
bool trophy_enabled = true;
|
||||
bool skip_notice_screen = false;
|
||||
bool network_connected = false;
|
||||
int vblank_frequency = 60;
|
||||
int console_language = DEFAULT_CONSOLE_LANGUAGE;
|
||||
bool vulkan_validation_enabled = false;
|
||||
|
|
@ -152,12 +154,14 @@ public:
|
|||
present_mode = other.present_mode;
|
||||
gpu_index = other.gpu_index;
|
||||
fullscreen_enabled = other.fullscreen_enabled;
|
||||
hdr_enabled = other.hdr_enabled;
|
||||
hide_cursor_enabled = other.hide_cursor_enabled;
|
||||
readback_linear_images = other.readback_linear_images;
|
||||
sync_raw_image_buffers = other.sync_raw_image_buffers;
|
||||
tessellation_enabled = other.tessellation_enabled;
|
||||
trophy_enabled = other.trophy_enabled;
|
||||
skip_notice_screen = other.skip_notice_screen;
|
||||
network_connected = other.network_connected;
|
||||
vblank_frequency = other.vblank_frequency;
|
||||
console_language = other.console_language;
|
||||
vulkan_validation_enabled = other.vulkan_validation_enabled;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,21 @@
|
|||
#ifndef UPDATE_CHECKER_H
|
||||
#define UPDATE_CHECKER_H
|
||||
|
||||
#include "updateRelease.h"
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QFile>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QPointer>
|
||||
#include <QProcess>
|
||||
|
||||
class QByteArray;
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
class QNetworkReply;
|
||||
class QProgressDialog;
|
||||
class QTemporaryDir;
|
||||
class QWidget;
|
||||
|
||||
class UpdateChecker final: public QObject {
|
||||
|
|
@ -14,28 +23,43 @@ class UpdateChecker final: public QObject {
|
|||
|
||||
public:
|
||||
explicit UpdateChecker(QWidget* parent);
|
||||
~UpdateChecker() override;
|
||||
|
||||
[[nodiscard]] static bool IsSupported();
|
||||
void Check(bool manual);
|
||||
static void ShowPreviousResult(QWidget* parent);
|
||||
void Check(bool manual);
|
||||
void SetGameRunning(bool running) { m_game_running = running; }
|
||||
[[nodiscard]] bool IsInstalling() const { return m_installing; }
|
||||
|
||||
signals:
|
||||
void CheckingChanged(bool checking);
|
||||
void InstallingChanged(bool installing);
|
||||
|
||||
private:
|
||||
struct UpdateInfo;
|
||||
void FetchRelease(const QUrl& url, std::function<void(UpdateRelease::Info)> done);
|
||||
void CheckCurrent(const UpdateRelease::Info& current, bool manual);
|
||||
void OfferUpdate();
|
||||
void Download();
|
||||
void ReadDownload(QNetworkReply* reply);
|
||||
void PreparePackage();
|
||||
void RunPreparation(const QString& program, const QStringList& arguments, bool validate);
|
||||
void Restart();
|
||||
void Finish(QString error = {}, bool report = true);
|
||||
|
||||
static UpdateInfo ParseUpdateInfo(const QByteArray& data);
|
||||
void FetchUpdateInfo(const char* url, bool fallback, bool manual);
|
||||
void ShowUpdateResult(const UpdateInfo& info, bool manual);
|
||||
|
||||
QWidget* m_parent = nullptr;
|
||||
QNetworkAccessManager m_network;
|
||||
bool m_checking_updates = false;
|
||||
|
||||
// Answer from the primary update feed, kept while the GitHub fallback feed is
|
||||
// being consulted, so it can still be shown if the fallback request fails.
|
||||
QString m_primary_tag;
|
||||
QUrl m_primary_page_url;
|
||||
QWidget* m_parent;
|
||||
QNetworkAccessManager m_network;
|
||||
QPointer<QNetworkReply> m_reply;
|
||||
QPointer<QProgressDialog> m_progress;
|
||||
QProcess m_prepare;
|
||||
std::unique_ptr<QTemporaryDir> m_workspace;
|
||||
QFile m_download;
|
||||
QCryptographicHash m_hash {QCryptographicHash::Sha256};
|
||||
UpdateRelease::Info m_release;
|
||||
QString m_download_error;
|
||||
bool m_busy = false;
|
||||
bool m_installing = false;
|
||||
bool m_game_running = false;
|
||||
bool m_canceled = false;
|
||||
};
|
||||
|
||||
#endif // UPDATE_CHECKER_H
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
#ifndef UPDATE_RELEASE_H
|
||||
#define UPDATE_RELEASE_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QDateTime>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
namespace UpdateRelease {
|
||||
|
||||
inline const QString LATEST_URL =
|
||||
QStringLiteral("https://api.github.com/repos/KytyPS5/KytyPS5/releases/latest");
|
||||
inline const QString TAG_URL =
|
||||
QStringLiteral("https://api.github.com/repos/KytyPS5/KytyPS5/releases/tags/%1");
|
||||
inline const QString PAGE_URL =
|
||||
QStringLiteral("https://github.com/KytyPS5/KytyPS5/releases/tag/%1");
|
||||
inline const QString DOWNLOAD_URL =
|
||||
QStringLiteral("https://github.com/KytyPS5/KytyPS5/releases/download/%1/%2");
|
||||
|
||||
struct Info {
|
||||
QString tag;
|
||||
QUrl page_url;
|
||||
QUrl download_url;
|
||||
QByteArray sha256;
|
||||
qint64 size = 0;
|
||||
QDateTime published_at;
|
||||
QString error;
|
||||
};
|
||||
|
||||
enum class VersionRelation { Current, Newer, Older, Unknown };
|
||||
|
||||
[[nodiscard]] QString PlatformSuffix();
|
||||
[[nodiscard]] Info Parse(const QByteArray& data, const QString& platform_suffix);
|
||||
[[nodiscard]] VersionRelation Compare(const Info& latest, const Info& current);
|
||||
|
||||
} // namespace UpdateRelease
|
||||
|
||||
#endif // UPDATE_RELEASE_H
|
||||
|
|
@ -10,12 +10,14 @@ QVariantMap Configuration::GameSettings() const {
|
|||
KYTY_CFG_SET(present_mode);
|
||||
KYTY_CFG_SET(gpu_index);
|
||||
KYTY_CFG_SET(fullscreen_enabled);
|
||||
KYTY_CFG_SET(hdr_enabled);
|
||||
KYTY_CFG_SET(hide_cursor_enabled);
|
||||
KYTY_CFG_SET(readback_linear_images);
|
||||
KYTY_CFG_SET(sync_raw_image_buffers);
|
||||
KYTY_CFG_SET(tessellation_enabled);
|
||||
KYTY_CFG_SET(trophy_enabled);
|
||||
KYTY_CFG_SET(skip_notice_screen);
|
||||
KYTY_CFG_SET(network_connected);
|
||||
KYTY_CFG_SET(vblank_frequency);
|
||||
KYTY_CFG_SET(console_language);
|
||||
KYTY_CFG_SET(vulkan_validation_enabled);
|
||||
|
|
@ -55,12 +57,14 @@ void Configuration::ReadGameSettingsValues(const Settings& s) {
|
|||
present_mode = PresentMode::Mailbox;
|
||||
}
|
||||
KYTY_CFG_GET(fullscreen_enabled);
|
||||
KYTY_CFG_GET(hdr_enabled);
|
||||
KYTY_CFG_GET(hide_cursor_enabled);
|
||||
KYTY_CFG_GET(readback_linear_images);
|
||||
KYTY_CFG_GET(sync_raw_image_buffers);
|
||||
KYTY_CFG_GET(tessellation_enabled);
|
||||
trophy_enabled = s.value("trophy_enabled", trophy_enabled).toBool();
|
||||
KYTY_CFG_GET(skip_notice_screen);
|
||||
KYTY_CFG_GET(network_connected);
|
||||
vblank_frequency = s.value("vblank_frequency", vblank_frequency).toInt();
|
||||
console_language = s.value("console_language", console_language).toInt();
|
||||
if (console_language < 0 || console_language > MAX_CONSOLE_LANGUAGE) {
|
||||
|
|
|
|||
|
|
@ -273,11 +273,13 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
|
|||
info.gpu_index >= 0 && info.gpu_index < m_ui->comboBox_gpu->count() - 1 ? info.gpu_index + 1
|
||||
: 0);
|
||||
m_ui->checkBox_fullscreen->setChecked(info.fullscreen_enabled);
|
||||
m_ui->checkBox_hdr->setChecked(info.hdr_enabled);
|
||||
m_ui->checkBox_hide_cursor->setChecked(info.hide_cursor_enabled);
|
||||
m_ui->checkBox_readback->setChecked(info.readback_linear_images);
|
||||
m_ui->checkBox_sync_raw_image_buffers->setChecked(info.sync_raw_image_buffers);
|
||||
m_ui->checkBox_tessellation->setChecked(info.tessellation_enabled);
|
||||
m_ui->checkBox_skip_notice_screen->setChecked(info.skip_notice_screen);
|
||||
m_ui->checkBox_network_connected->setChecked(info.network_connected);
|
||||
m_ui->checkBox_trophy_notifications->setChecked(info.trophy_enabled);
|
||||
m_ui->spinBox_vblank_frequency->setValue(info.vblank_frequency);
|
||||
m_ui->comboBox_console_language->clear();
|
||||
|
|
@ -434,11 +436,13 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo
|
|||
TextToEnum<Configuration::PresentMode>(ui.comboBox_present_mode->currentText());
|
||||
info.gpu_index = ui.comboBox_gpu->currentIndex() - 1;
|
||||
info.fullscreen_enabled = ui.checkBox_fullscreen->isChecked();
|
||||
info.hdr_enabled = ui.checkBox_hdr->isChecked();
|
||||
info.hide_cursor_enabled = ui.checkBox_hide_cursor->isChecked();
|
||||
info.readback_linear_images = ui.checkBox_readback->isChecked();
|
||||
info.sync_raw_image_buffers = ui.checkBox_sync_raw_image_buffers->isChecked();
|
||||
info.tessellation_enabled = ui.checkBox_tessellation->isChecked();
|
||||
info.skip_notice_screen = ui.checkBox_skip_notice_screen->isChecked();
|
||||
info.network_connected = ui.checkBox_network_connected->isChecked();
|
||||
info.trophy_enabled = ui.checkBox_trophy_notifications->isChecked();
|
||||
info.vblank_frequency = ui.spinBox_vblank_frequency->value();
|
||||
info.console_language = ui.comboBox_console_language->currentIndex();
|
||||
|
|
@ -463,6 +467,14 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo
|
|||
}
|
||||
|
||||
void ConfigurationEditDialog::save() {
|
||||
for (auto* field: {m_ui->lineEdit_shader_log_folder, m_ui->lineEdit_printf_file,
|
||||
m_ui->lineEdit_cmd_dump_folder}) {
|
||||
if (field->isEnabled() && field->text().isEmpty()) {
|
||||
m_ui->settings_tabs->setCurrentWidget(m_ui->debug_tab);
|
||||
field->setFocus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (MandatoryLineEdit::FindEmpty(this)) {
|
||||
QMessageBox::critical(this, tr("Save failed"), tr("Please fill all mandatory fields"));
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
|
|||
[this](const QString&) { m_update_checker->Check(true); });
|
||||
connect(m_update_checker, &UpdateChecker::CheckingChanged, m_ui->check_updates_link,
|
||||
&QLabel::setDisabled);
|
||||
connect(m_update_checker, &UpdateChecker::InstallingChanged, this, &MainDialogPrivate::Update);
|
||||
connect(m_ui->check_updates_on_startup, &QCheckBox::toggled, this, [this](bool checked) {
|
||||
g_check_updates_on_startup = checked;
|
||||
m_ui->widget->WriteSettings();
|
||||
|
|
@ -146,6 +147,7 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
|
|||
});
|
||||
|
||||
connect(&m_process, &QProcess::stateChanged, this, [this](QProcess::ProcessState state) {
|
||||
m_update_checker->SetGameRunning(state != QProcess::NotRunning);
|
||||
if (state == QProcess::NotRunning) {
|
||||
if (m_running_item != nullptr) {
|
||||
m_running_item->SetRunning(false);
|
||||
|
|
@ -162,6 +164,7 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
|
|||
}
|
||||
|
||||
void MainDialogPrivate::FindInterpreter() {
|
||||
UpdateChecker::ShowPreviousResult(m_main_dialog);
|
||||
QDir search_dir(QApplication::applicationDirPath());
|
||||
m_interpreter = search_dir.absoluteFilePath(EMULATOR_EXE);
|
||||
|
||||
|
|
@ -242,6 +245,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
|
|||
if (info.fullscreen_enabled) {
|
||||
args << "--fullscreen";
|
||||
}
|
||||
if (info.hdr_enabled) {
|
||||
args << "--hdr";
|
||||
}
|
||||
if (info.hide_cursor_enabled) {
|
||||
args << "--hide-cursor";
|
||||
}
|
||||
|
|
@ -249,6 +255,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
|
|||
args << "--sync-raw-image-buffers" << BoolArg(info.sync_raw_image_buffers);
|
||||
args << "--trophy-notifications" << BoolArg(info.trophy_enabled);
|
||||
args << "--skip-notice-screen" << BoolArg(info.skip_notice_screen);
|
||||
args << "--network-connected" << BoolArg(info.network_connected);
|
||||
if (info.tessellation_enabled) {
|
||||
args << "--tessellation";
|
||||
}
|
||||
|
|
@ -509,6 +516,9 @@ void MainDialogPrivate::ReadSettings(QSettings& s) {
|
|||
}
|
||||
|
||||
void MainDialogPrivate::Run() {
|
||||
if (m_update_checker->IsInstalling()) {
|
||||
return;
|
||||
}
|
||||
m_running_item = m_ui->widget->GetSelectedItem();
|
||||
if (m_running_item == nullptr) {
|
||||
return;
|
||||
|
|
@ -526,7 +536,8 @@ void MainDialogPrivate::Run() {
|
|||
void MainDialogPrivate::Update() {
|
||||
const auto* item = m_ui->widget->GetSelectedItem();
|
||||
|
||||
bool run_enabled = (m_process.state() == QProcess::NotRunning && item != nullptr);
|
||||
bool run_enabled = (m_process.state() == QProcess::NotRunning && item != nullptr &&
|
||||
!m_update_checker->IsInstalling());
|
||||
|
||||
if (run_enabled) {
|
||||
const auto& info = item->GetInfo();
|
||||
|
|
|
|||
|
|
@ -2,143 +2,418 @@
|
|||
|
||||
#include "kytyGitVersion.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QAbstractButton>
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
#include <QDesktopServices>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QMessageBox>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QWidget>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QProgressDialog>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char DEFAULT_FEED_URL[] = "https://kytyps5.github.io/data/updates.json";
|
||||
constexpr char FALLBACK_FEED_URL[] =
|
||||
"https://api.github.com/repos/KytyPS5/KytyPS5/releases/latest";
|
||||
constexpr qint64 MAX_METADATA_SIZE = 2 * 1024 * 1024;
|
||||
|
||||
QString LauncherRelativePath() {
|
||||
#if defined(_WIN32)
|
||||
return QStringLiteral("launcher.exe");
|
||||
#elif defined(__APPLE__)
|
||||
return QStringLiteral("KytyPS5.app/Contents/MacOS/KytyPS5");
|
||||
#else
|
||||
return QStringLiteral("launcher");
|
||||
#endif
|
||||
}
|
||||
|
||||
QString InstallDirectory() {
|
||||
QDir dir(QCoreApplication::applicationDirPath());
|
||||
#if defined(__APPLE__)
|
||||
// Official macOS releases are a bundle; never update a flat development build.
|
||||
if (!dir.cdUp() || !dir.cdUp() || dir.dirName() != QStringLiteral("KytyPS5.app") ||
|
||||
!dir.cdUp()) {
|
||||
return {};
|
||||
}
|
||||
#endif
|
||||
return dir.absolutePath();
|
||||
}
|
||||
|
||||
QString HelperName() {
|
||||
#if defined(_WIN32)
|
||||
return QStringLiteral("kyty_updater.exe");
|
||||
#else
|
||||
return QStringLiteral("kyty_updater");
|
||||
#endif
|
||||
}
|
||||
|
||||
QNetworkRequest Request(const QUrl& url) {
|
||||
QNetworkRequest request(url);
|
||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
|
||||
QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
request.setRawHeader("User-Agent", "KytyPS5-Updater");
|
||||
request.setTransferTimeout(30000);
|
||||
return request;
|
||||
}
|
||||
|
||||
QString TarProgram() {
|
||||
#if defined(_WIN32)
|
||||
// Use the OS tool, not a tar.exe from the download or current directory.
|
||||
return QDir(qEnvironmentVariable("SystemRoot", QStringLiteral("C:/Windows")))
|
||||
.filePath(QStringLiteral("System32/tar.exe"));
|
||||
#else
|
||||
return QStringLiteral("/usr/bin/tar");
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct UpdateChecker::UpdateInfo {
|
||||
QString tag;
|
||||
QUrl page_url;
|
||||
QString error;
|
||||
};
|
||||
|
||||
UpdateChecker::UpdateChecker(QWidget* parent): QObject(parent), m_parent(parent), m_network(this) {}
|
||||
|
||||
UpdateChecker::~UpdateChecker() {
|
||||
if (m_reply) {
|
||||
m_reply->disconnect(this);
|
||||
m_reply->abort();
|
||||
}
|
||||
// Extraction must stop before QTemporaryDir removes its output.
|
||||
m_prepare.disconnect(this);
|
||||
if (m_prepare.state() != QProcess::NotRunning) {
|
||||
m_prepare.kill();
|
||||
m_prepare.waitForFinished();
|
||||
}
|
||||
}
|
||||
|
||||
bool UpdateChecker::IsSupported() {
|
||||
#if defined(KYTY_OFFICIAL_BUILD) && defined(NDEBUG)
|
||||
return !QString::fromLatin1(KYTY_GIT_HASH).endsWith(QStringLiteral("-dirty"));
|
||||
return !QString::fromLatin1(KYTY_GIT_HASH).endsWith(QStringLiteral("-dirty")) &&
|
||||
!UpdateRelease::PlatformSuffix().isEmpty();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
UpdateChecker::UpdateInfo UpdateChecker::ParseUpdateInfo(const QByteArray& data) {
|
||||
const auto document = QJsonDocument::fromJson(data);
|
||||
if (!document.isObject()) {
|
||||
return {{}, {}, tr("Invalid update feed")};
|
||||
}
|
||||
|
||||
const auto root = document.object();
|
||||
UpdateInfo info {root.value(QStringLiteral("tag")).toString(),
|
||||
QUrl(root.value(QStringLiteral("html_url")).toString()), {}};
|
||||
if (info.tag.isEmpty()) {
|
||||
info.tag = root.value(QStringLiteral("tag_name")).toString();
|
||||
}
|
||||
if (info.tag.isEmpty() || !info.page_url.isValid() ||
|
||||
info.page_url.scheme() != QStringLiteral("https")) {
|
||||
info.error = tr("Incomplete update feed");
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
void UpdateChecker::Check(bool manual) {
|
||||
if (!IsSupported() || m_checking_updates) {
|
||||
void UpdateChecker::ShowPreviousResult(QWidget* parent) {
|
||||
const auto directory = InstallDirectory();
|
||||
QFile result(QDir(directory).filePath(QStringLiteral(".kyty-update-result")));
|
||||
if (directory.isEmpty() || !result.open(QIODevice::ReadOnly)) {
|
||||
return;
|
||||
}
|
||||
m_checking_updates = true;
|
||||
emit CheckingChanged(true);
|
||||
FetchUpdateInfo(DEFAULT_FEED_URL, false, manual);
|
||||
const auto status = result.readLine().trimmed();
|
||||
auto message = QString::fromUtf8(result.read(64 * 1024)).trimmed();
|
||||
result.close();
|
||||
result.remove();
|
||||
if (status != "success" && status != "failure") {
|
||||
return;
|
||||
}
|
||||
const bool success = status == "success";
|
||||
if (message.isEmpty()) {
|
||||
message = success ? tr("The update was installed successfully.")
|
||||
: tr("The update could not be installed.");
|
||||
}
|
||||
QMessageBox(success ? QMessageBox::Information : QMessageBox::Warning, tr("KytyPS5 Update"),
|
||||
message, QMessageBox::Ok, parent)
|
||||
.exec();
|
||||
}
|
||||
|
||||
void UpdateChecker::FetchUpdateInfo(const char* url, bool fallback, bool manual) {
|
||||
QNetworkRequest request(QUrl(QString::fromLatin1(url)));
|
||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
|
||||
QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
request.setRawHeader("User-Agent", "Kyty-Launcher");
|
||||
void UpdateChecker::FetchRelease(const QUrl& url, std::function<void(UpdateRelease::Info)> done) {
|
||||
auto request = Request(url);
|
||||
request.setRawHeader("Accept", "application/vnd.github+json");
|
||||
request.setRawHeader("X-GitHub-Api-Version", "2022-11-28");
|
||||
request.setTransferTimeout(15000);
|
||||
|
||||
auto* reply = m_network.get(request);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, fallback, manual]() {
|
||||
UpdateInfo info;
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
info = ParseUpdateInfo(reply->readAll());
|
||||
} else {
|
||||
m_reply = reply;
|
||||
reply->setReadBufferSize(MAX_METADATA_SIZE + 1);
|
||||
connect(reply, &QNetworkReply::readyRead, this, [reply]() {
|
||||
if (reply->bytesAvailable() > MAX_METADATA_SIZE) {
|
||||
reply->abort();
|
||||
}
|
||||
});
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, done = std::move(done)]() {
|
||||
UpdateRelease::Info info;
|
||||
if (reply->bytesAvailable() > MAX_METADATA_SIZE) {
|
||||
info.error = tr("The update response is too large.");
|
||||
} else if (reply->error() != QNetworkReply::NoError) {
|
||||
info.error = reply->errorString();
|
||||
} else {
|
||||
info = UpdateRelease::Parse(reply->readAll(), UpdateRelease::PlatformSuffix());
|
||||
}
|
||||
m_reply = nullptr;
|
||||
reply->deleteLater();
|
||||
|
||||
if (!fallback) {
|
||||
const bool up_to_date =
|
||||
info.error.isEmpty() && info.tag == QString::fromLatin1(KYTY_RELEASE_TAG);
|
||||
if (!up_to_date) {
|
||||
if (info.error.isEmpty()) {
|
||||
// The primary feed is reachable but disagrees with the build tag
|
||||
// (e.g. a stale mirror); remember its answer in case the fallback
|
||||
// request also fails.
|
||||
m_primary_tag = info.tag;
|
||||
m_primary_page_url = info.page_url;
|
||||
}
|
||||
FetchUpdateInfo(FALLBACK_FEED_URL, true, manual);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (fallback && !info.error.isEmpty() && !m_primary_tag.isEmpty()) {
|
||||
// The fallback feed failed, but the primary feed already returned a
|
||||
// usable answer; prefer it over reporting a network error.
|
||||
info = {m_primary_tag, m_primary_page_url, {}};
|
||||
}
|
||||
m_primary_tag.clear();
|
||||
m_primary_page_url.clear();
|
||||
|
||||
m_checking_updates = false;
|
||||
emit CheckingChanged(false);
|
||||
ShowUpdateResult(info, manual);
|
||||
done(info);
|
||||
});
|
||||
}
|
||||
|
||||
void UpdateChecker::ShowUpdateResult(const UpdateInfo& info, bool manual) {
|
||||
if (!info.error.isEmpty()) {
|
||||
qWarning() << "Could not check for updates:" << info.error;
|
||||
if (manual) {
|
||||
QMessageBox::warning(m_parent, tr("Update Check"),
|
||||
tr("Could not check for updates:\n%1").arg(info.error));
|
||||
}
|
||||
void UpdateChecker::Check(bool manual) {
|
||||
if (!IsSupported() || m_busy) {
|
||||
return;
|
||||
}
|
||||
m_busy = true;
|
||||
emit CheckingChanged(true);
|
||||
FetchRelease(QUrl(UpdateRelease::LATEST_URL), [this, manual](UpdateRelease::Info latest) {
|
||||
m_release = std::move(latest);
|
||||
if (!m_release.published_at.isValid()) {
|
||||
Finish(m_release.error, manual);
|
||||
} else if (m_release.tag == QString::fromLatin1(KYTY_RELEASE_TAG)) {
|
||||
CheckCurrent(m_release, manual);
|
||||
} else {
|
||||
// Date/hash tags cannot order releases published on the same day.
|
||||
const auto tag = QUrl::toPercentEncoding(QString::fromLatin1(KYTY_RELEASE_TAG));
|
||||
FetchRelease(QUrl(UpdateRelease::TAG_URL.arg(QString::fromLatin1(tag))),
|
||||
[this, manual](const UpdateRelease::Info& current) {
|
||||
CheckCurrent(current, manual);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const bool current = info.tag == QString::fromLatin1(KYTY_RELEASE_TAG);
|
||||
if (current) {
|
||||
if (manual) {
|
||||
QMessageBox::information(m_parent, tr("Update Check"),
|
||||
tr("You are using the latest version (%1).").arg(info.tag));
|
||||
void UpdateChecker::CheckCurrent(const UpdateRelease::Info& current, bool manual) {
|
||||
if (current.tag != QString::fromLatin1(KYTY_RELEASE_TAG) || !current.published_at.isValid()) {
|
||||
Finish(tr("Could not verify the currently installed release. Please try again later."),
|
||||
manual);
|
||||
return;
|
||||
}
|
||||
const auto relation = UpdateRelease::Compare(m_release, current);
|
||||
if (relation == UpdateRelease::VersionRelation::Newer) {
|
||||
if (m_release.error.isEmpty()) {
|
||||
OfferUpdate();
|
||||
} else {
|
||||
Finish(m_release.error, manual);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const auto message =
|
||||
tr("An update is available.\n\nCurrent: %1\nLatest: %2\n\n"
|
||||
"Open the release page?")
|
||||
.arg(QString::fromLatin1(KYTY_RELEASE_TAG), info.tag);
|
||||
if (QMessageBox::question(m_parent, tr("KytyPS5 Update"), message,
|
||||
QMessageBox::Open | QMessageBox::Cancel,
|
||||
QMessageBox::Open) == QMessageBox::Open) {
|
||||
QDesktopServices::openUrl(info.page_url);
|
||||
if (relation == UpdateRelease::VersionRelation::Unknown) {
|
||||
Finish(tr("Could not determine whether the available release is newer."), manual);
|
||||
return;
|
||||
}
|
||||
Finish();
|
||||
if (manual) {
|
||||
QMessageBox::information(m_parent, tr("Update Check"),
|
||||
tr("No newer release is available."));
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateChecker::OfferUpdate() {
|
||||
QMessageBox question(QMessageBox::Information, tr("KytyPS5 Update"),
|
||||
tr("Version %1 is available.\n\nCurrent: %2\n\n"
|
||||
"Download and install it now? The launcher will restart. Your settings "
|
||||
"and games are kept.")
|
||||
.arg(m_release.tag, QString::fromLatin1(KYTY_RELEASE_TAG)),
|
||||
QMessageBox::Yes | QMessageBox::Open | QMessageBox::Cancel, m_parent);
|
||||
question.button(QMessageBox::Yes)->setText(tr("Install Update"));
|
||||
question.button(QMessageBox::Open)->setText(tr("Release Notes"));
|
||||
question.setDefaultButton(QMessageBox::Yes);
|
||||
const auto answer = question.exec();
|
||||
if (answer == QMessageBox::Yes) {
|
||||
Download();
|
||||
return;
|
||||
}
|
||||
if (answer == QMessageBox::Open) {
|
||||
QDesktopServices::openUrl(m_release.page_url);
|
||||
}
|
||||
Finish();
|
||||
}
|
||||
|
||||
void UpdateChecker::Download() {
|
||||
if (m_game_running) {
|
||||
Finish(tr("Close the running game before installing an update."));
|
||||
return;
|
||||
}
|
||||
const auto directory = InstallDirectory();
|
||||
const auto helper = QDir(QCoreApplication::applicationDirPath()).filePath(HelperName());
|
||||
if (directory.isEmpty() || !QFileInfo::exists(helper) || !QFileInfo::exists(TarProgram())) {
|
||||
Finish(tr("This installation cannot update itself. Download the release from GitHub and "
|
||||
"extract it manually."));
|
||||
return;
|
||||
}
|
||||
m_workspace = std::make_unique<QTemporaryDir>(
|
||||
QDir(directory).filePath(QStringLiteral(".kyty-update-XXXXXX")));
|
||||
if (!m_workspace->isValid()) {
|
||||
Finish(tr("The installation folder is not writable. Move KytyPS5 to a writable folder and "
|
||||
"try again."));
|
||||
return;
|
||||
}
|
||||
if (!QFile::copy(helper, m_workspace->filePath(HelperName())) ||
|
||||
!QDir(m_workspace->path()).mkdir(QStringLiteral("payload"))) {
|
||||
Finish(tr("Could not prepare the update folder."));
|
||||
return;
|
||||
}
|
||||
m_download.setFileName(
|
||||
m_workspace->filePath(QStringLiteral("package") + UpdateRelease::PlatformSuffix()));
|
||||
if (!m_download.open(QIODevice::WriteOnly)) {
|
||||
Finish(m_download.errorString());
|
||||
return;
|
||||
}
|
||||
m_hash.reset();
|
||||
m_download_error.clear();
|
||||
m_canceled = false;
|
||||
m_installing = true;
|
||||
emit InstallingChanged(true);
|
||||
m_progress = new QProgressDialog(tr("Downloading %1…").arg(m_release.tag), tr("Cancel"), 0, 100,
|
||||
m_parent);
|
||||
m_progress->setWindowTitle(tr("KytyPS5 Update"));
|
||||
m_progress->setWindowModality(Qt::ApplicationModal);
|
||||
m_progress->setAutoClose(false);
|
||||
m_progress->setAutoReset(false);
|
||||
m_progress->setMinimumDuration(0);
|
||||
connect(m_progress, &QProgressDialog::canceled, this, [this]() {
|
||||
m_canceled = true;
|
||||
if (m_reply) {
|
||||
m_reply->abort();
|
||||
} else if (m_prepare.state() != QProcess::NotRunning) {
|
||||
m_prepare.kill();
|
||||
}
|
||||
});
|
||||
auto* reply = m_network.get(Request(m_release.download_url));
|
||||
m_reply = reply;
|
||||
reply->setReadBufferSize(1024 * 1024);
|
||||
connect(reply, &QNetworkReply::readyRead, this, [this, reply]() { ReadDownload(reply); });
|
||||
connect(reply, &QNetworkReply::downloadProgress, this, [this](qint64 received, qint64) {
|
||||
if (m_progress) {
|
||||
m_progress->setValue(
|
||||
static_cast<int>(qMin<qint64>(99, received * 100 / m_release.size)));
|
||||
}
|
||||
});
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
ReadDownload(reply);
|
||||
const auto error = reply->error();
|
||||
const auto error_text = reply->errorString();
|
||||
m_reply = nullptr;
|
||||
reply->deleteLater();
|
||||
const auto size = m_download.size();
|
||||
const bool flushed = m_download.flush();
|
||||
m_download.close();
|
||||
if (m_canceled) {
|
||||
Finish();
|
||||
} else if (!m_download_error.isEmpty()) {
|
||||
Finish(m_download_error);
|
||||
} else if (error != QNetworkReply::NoError) {
|
||||
Finish(error_text);
|
||||
} else if (!flushed || size != m_release.size || m_hash.result() != m_release.sha256) {
|
||||
Finish(tr("The update failed its size or SHA-256 verification. Please try again."));
|
||||
} else {
|
||||
PreparePackage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void UpdateChecker::ReadDownload(QNetworkReply* reply) {
|
||||
const auto data = reply->readAll();
|
||||
if (data.isEmpty() || m_canceled || !m_download_error.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (m_download.size() + data.size() > m_release.size) {
|
||||
m_download_error = tr("The downloaded file is larger than the release asset.");
|
||||
} else if (m_download.write(data) != data.size()) {
|
||||
m_download_error = m_download.errorString();
|
||||
} else {
|
||||
m_hash.addData(data);
|
||||
}
|
||||
if (!m_download_error.isEmpty() && !reply->isFinished()) {
|
||||
reply->abort();
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateChecker::PreparePackage() {
|
||||
m_progress->setLabelText(tr("Preparing the update…"));
|
||||
m_progress->setRange(0, 0);
|
||||
RunPreparation(TarProgram(),
|
||||
{QStringLiteral("-xf"), m_download.fileName(), QStringLiteral("-C"),
|
||||
m_workspace->filePath(QStringLiteral("payload")),
|
||||
QStringLiteral("--no-same-owner"), QStringLiteral("--no-same-permissions")},
|
||||
false);
|
||||
}
|
||||
|
||||
void UpdateChecker::RunPreparation(const QString& program, const QStringList& arguments,
|
||||
bool validate) {
|
||||
m_prepare.disconnect(this);
|
||||
m_prepare.setProgram(program);
|
||||
m_prepare.setArguments(arguments);
|
||||
m_prepare.setWorkingDirectory(m_workspace->path());
|
||||
auto environment = QProcessEnvironment::systemEnvironment();
|
||||
environment.remove(QStringLiteral("TAR_OPTIONS"));
|
||||
m_prepare.setProcessEnvironment(environment);
|
||||
connect(&m_prepare, &QProcess::errorOccurred, this, [this](QProcess::ProcessError error) {
|
||||
if (error == QProcess::FailedToStart) {
|
||||
Finish(m_prepare.errorString());
|
||||
}
|
||||
});
|
||||
connect(&m_prepare, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this,
|
||||
[this, validate](int exit_code, QProcess::ExitStatus status) {
|
||||
if (m_canceled) {
|
||||
Finish();
|
||||
} else if (exit_code != 0 || status != QProcess::NormalExit) {
|
||||
Finish(
|
||||
tr("Could not prepare the update:\n%1")
|
||||
.arg(QString::fromUtf8(m_prepare.readAllStandardError()).left(4096)));
|
||||
} else if (!validate) {
|
||||
RunPreparation(m_workspace->filePath(HelperName()),
|
||||
{QStringLiteral("--validate"),
|
||||
m_workspace->filePath(QStringLiteral("payload")),
|
||||
InstallDirectory(), LauncherRelativePath()},
|
||||
true);
|
||||
} else {
|
||||
Restart();
|
||||
}
|
||||
});
|
||||
m_prepare.start();
|
||||
}
|
||||
|
||||
void UpdateChecker::Restart() {
|
||||
if (m_game_running || m_canceled) {
|
||||
Finish(tr("The update was canceled. Close any running game before trying again."));
|
||||
return;
|
||||
}
|
||||
m_progress->setLabelText(tr("Restarting to install the update…"));
|
||||
m_progress->setCancelButton(nullptr);
|
||||
QProcess helper;
|
||||
helper.setProgram(m_workspace->filePath(HelperName()));
|
||||
helper.setArguments({QStringLiteral("--install"),
|
||||
m_workspace->filePath(QStringLiteral("payload")), InstallDirectory(),
|
||||
LauncherRelativePath(),
|
||||
QString::number(QCoreApplication::applicationPid()), QDir::currentPath()});
|
||||
helper.setWorkingDirectory(m_workspace->path());
|
||||
helper.setStandardOutputFile(m_workspace->filePath(QStringLiteral("helper.log")));
|
||||
helper.setStandardErrorFile(m_workspace->filePath(QStringLiteral("helper.log")),
|
||||
QIODevice::Append);
|
||||
if (!helper.startDetached()) {
|
||||
Finish(tr("Could not start the update installer."));
|
||||
return;
|
||||
}
|
||||
m_workspace->setAutoRemove(false);
|
||||
QApplication::quit();
|
||||
}
|
||||
|
||||
void UpdateChecker::Finish(QString error, bool report) {
|
||||
const auto release_page = m_release.page_url;
|
||||
m_release = {};
|
||||
m_download.close();
|
||||
m_workspace.reset();
|
||||
if (m_progress) {
|
||||
m_progress->disconnect(this);
|
||||
m_progress->hide();
|
||||
m_progress->deleteLater();
|
||||
m_progress = nullptr;
|
||||
}
|
||||
m_busy = false;
|
||||
m_installing = false;
|
||||
emit CheckingChanged(false);
|
||||
emit InstallingChanged(false);
|
||||
if (error.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
qWarning() << "KytyPS5 updater:" << error;
|
||||
if (!report) {
|
||||
return;
|
||||
}
|
||||
QMessageBox message(QMessageBox::Warning, tr("KytyPS5 Update"), error, QMessageBox::Close,
|
||||
m_parent);
|
||||
if (!release_page.isEmpty()) {
|
||||
message.addButton(QMessageBox::Open);
|
||||
message.button(QMessageBox::Open)->setText(tr("Open Release Page"));
|
||||
}
|
||||
if (message.exec() == QMessageBox::Open) {
|
||||
QDesktopServices::openUrl(release_page);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
#include "updateRelease.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
#include <QRegularExpression>
|
||||
|
||||
namespace UpdateRelease {
|
||||
|
||||
QString PlatformSuffix() {
|
||||
#if defined(_WIN32) && (defined(_M_X64) || defined(__x86_64__))
|
||||
return QStringLiteral("-Windows-x64.zip");
|
||||
#elif defined(__linux__) && defined(__x86_64__)
|
||||
return QStringLiteral("-Linux-x86_64.tar.gz");
|
||||
#elif defined(__APPLE__) && defined(__x86_64__)
|
||||
return QStringLiteral("-macOS-x86_64.zip");
|
||||
#else
|
||||
return {};
|
||||
#endif
|
||||
}
|
||||
|
||||
Info Parse(const QByteArray& data, const QString& platform_suffix) {
|
||||
Info info;
|
||||
const auto document = QJsonDocument::fromJson(data);
|
||||
if (!document.isObject()) {
|
||||
info.error = QObject::tr("Invalid GitHub release response.");
|
||||
return info;
|
||||
}
|
||||
|
||||
const auto root = document.object();
|
||||
info.tag = root.value(QStringLiteral("tag_name")).toString();
|
||||
const auto published_at =
|
||||
QDateTime::fromString(root.value(QStringLiteral("published_at")).toString(), Qt::ISODate);
|
||||
if (info.tag.isEmpty() || !published_at.isValid() ||
|
||||
root.value(QStringLiteral("draft")).toBool() ||
|
||||
root.value(QStringLiteral("prerelease")).toBool()) {
|
||||
info.error = QObject::tr("The release is not a published stable release.");
|
||||
return info;
|
||||
}
|
||||
info.published_at = published_at;
|
||||
const auto encoded_tag = QString::fromLatin1(QUrl::toPercentEncoding(info.tag));
|
||||
info.page_url = QUrl(PAGE_URL.arg(encoded_tag));
|
||||
|
||||
if (platform_suffix.isEmpty()) {
|
||||
info.error = QObject::tr("Built-in updates are unavailable for this platform.");
|
||||
return info;
|
||||
}
|
||||
|
||||
const auto asset_name = info.tag + platform_suffix;
|
||||
for (const auto& value: root.value(QStringLiteral("assets")).toArray()) {
|
||||
const auto asset = value.toObject();
|
||||
if (asset.value(QStringLiteral("name")).toString() != asset_name) {
|
||||
continue;
|
||||
}
|
||||
const auto digest = asset.value(QStringLiteral("digest")).toString();
|
||||
const auto size = asset.value(QStringLiteral("size")).toInteger();
|
||||
static const QRegularExpression digest_pattern(
|
||||
QStringLiteral("\\Asha256:[A-Fa-f0-9]{64}\\z"));
|
||||
if (!digest_pattern.match(digest).hasMatch() || size <= 0) {
|
||||
info.error = QObject::tr("The update package size or SHA-256 checksum is invalid.");
|
||||
return info;
|
||||
}
|
||||
const auto encoded_asset = QString::fromLatin1(QUrl::toPercentEncoding(asset_name));
|
||||
info.download_url = QUrl(DOWNLOAD_URL.arg(encoded_tag, encoded_asset));
|
||||
info.sha256 = QByteArray::fromHex(digest.mid(7).toLatin1());
|
||||
info.size = size;
|
||||
return info;
|
||||
}
|
||||
info.error = QObject::tr("This release has no update package for your platform.");
|
||||
return info;
|
||||
}
|
||||
|
||||
VersionRelation Compare(const Info& latest, const Info& current) {
|
||||
if (latest.tag.isEmpty() || current.tag.isEmpty()) {
|
||||
return VersionRelation::Unknown;
|
||||
}
|
||||
if (latest.tag == current.tag) {
|
||||
return VersionRelation::Current;
|
||||
}
|
||||
if (!latest.published_at.isValid() || !current.published_at.isValid() ||
|
||||
latest.published_at == current.published_at) {
|
||||
return VersionRelation::Unknown;
|
||||
}
|
||||
return latest.published_at > current.published_at ? VersionRelation::Newer
|
||||
: VersionRelation::Older;
|
||||
}
|
||||
|
||||
} // namespace UpdateRelease
|
||||
184
src/libs/agc.cpp
184
src/libs/agc.cpp
|
|
@ -57,7 +57,6 @@ struct DriverDmem {
|
|||
};
|
||||
|
||||
static DriverDmem g_driver_dmem;
|
||||
static int InitializeDmem();
|
||||
static void ReleaseDmem();
|
||||
|
||||
} // namespace Gen5Driver
|
||||
|
|
@ -82,6 +81,8 @@ void Initialize() {
|
|||
auto& video_out = VideoOut::VideoOutInit(width, height, presenter);
|
||||
g_renderer = &presenter.Renderer();
|
||||
g_renderer->InitializeGpu(&video_out);
|
||||
// Native driver module startup reserves its DMEM before the guest can allocate it.
|
||||
EXIT_IF(Gen5Driver::Initialize() != OK);
|
||||
ShaderInit();
|
||||
}
|
||||
|
||||
|
|
@ -258,29 +259,6 @@ struct CommandBuffer {
|
|||
void* user_data;
|
||||
uint32_t reserved_dw;
|
||||
|
||||
void DbgDump() const {
|
||||
if (!Config::GraphicsDebugDumpEnabled() ||
|
||||
Config::GetPrintfDirection() == Config::LogDirection::Silent) {
|
||||
return;
|
||||
}
|
||||
static std::atomic<uint32_t> log_count {0};
|
||||
if (log_count.fetch_add(1) >= 64) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOGF("\t bottom = 0x%016" PRIx64 "\n"
|
||||
"\t top = 0x%016" PRIx64 "\n"
|
||||
"\t cursor_up = 0x%016" PRIx64 "\n"
|
||||
"\t cursor_down = 0x%016" PRIx64 "\n"
|
||||
"\t callback = 0x%016" PRIx64 "\n"
|
||||
"\t user_data = 0x%016" PRIx64 "\n"
|
||||
"\t reserved_dw = %" PRIu32 "\n",
|
||||
reinterpret_cast<uint64_t>(bottom), reinterpret_cast<uint64_t>(top),
|
||||
reinterpret_cast<uint64_t>(cursor_up), reinterpret_cast<uint64_t>(cursor_down),
|
||||
reinterpret_cast<uint64_t>(callback), reinterpret_cast<uint64_t>(user_data),
|
||||
reserved_dw);
|
||||
}
|
||||
|
||||
[[nodiscard]] KYTY_SYSV_ABI uint64_t GetAvailableSizeDW() const {
|
||||
// Interpret the signed cursor distance as an unsigned 64-bit DWORD count.
|
||||
const auto distance = static_cast<int64_t>(reinterpret_cast<uintptr_t>(cursor_down) -
|
||||
|
|
@ -301,7 +279,6 @@ struct CommandBuffer {
|
|||
"\t command buffer exhausted and has no grow callback: requested = %" PRIu32
|
||||
", remaining = %" PRIu64 ", reserved_dw = %" PRIu32 "\n",
|
||||
num_dw, remaining, reserved_dw);
|
||||
DbgDump();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -311,7 +288,6 @@ struct CommandBuffer {
|
|||
"\t command buffer grow callback failed: requested = %" PRIu32
|
||||
", remaining = %" PRIu64 ", reserved_dw = %" PRIu32 "\n",
|
||||
num_dw, remaining, reserved_dw);
|
||||
DbgDump();
|
||||
return false;
|
||||
}
|
||||
if (GetAvailableSizeDW() < num_dw) {
|
||||
|
|
@ -320,7 +296,6 @@ struct CommandBuffer {
|
|||
"requested = %" PRIu32 ", remaining = %" PRIu64
|
||||
", reserved_dw = %" PRIu32 "\n",
|
||||
num_dw, GetAvailableSizeDW(), reserved_dw);
|
||||
DbgDump();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -356,38 +331,6 @@ int KYTY_SYSV_ABI AgcInit(uint32_t* state, uint32_t ver) {
|
|||
}
|
||||
|
||||
printf("version = %u\n", ver);
|
||||
std::scoped_lock lock {Gen5Driver::g_driver_dmem.mutex};
|
||||
if (Gen5Driver::g_driver_dmem.physical_offset >= 0) {
|
||||
return OK;
|
||||
}
|
||||
if (const auto result = Gen5Driver::InitializeDmem(); result != OK) {
|
||||
return result;
|
||||
}
|
||||
struct FunctionShaderBindings {
|
||||
uint64_t code;
|
||||
uint32_t user_data[2];
|
||||
};
|
||||
static_assert(sizeof(FunctionShaderBindings) == 16);
|
||||
// Ordered-count queue validator, including its return to the caller.
|
||||
static constexpr std::array<uint32_t, 16> validator {
|
||||
0xbeeb03ff, 0x00000021, 0xb96a1818, 0xbf06106a,
|
||||
0xbf800000, 0x8590807e, 0xb96a0a18, 0xbf066a80,
|
||||
0xbf800000, 0x85ea807e, 0x88ea106a, 0xbf870003,
|
||||
0xbefc03ff, 0x8a6ca000, 0xbf920009, 0xbefd210e,
|
||||
};
|
||||
const auto base = Gen5Driver::DriverDmemBase + Gen5Driver::AgcDmemOffset;
|
||||
const auto function_table = base + 0x60;
|
||||
const auto code_address = base + 0x100; // Shader code requires 256-byte alignment.
|
||||
std::array<FunctionShaderBindings, 8> functions {};
|
||||
functions[2].code = code_address;
|
||||
const std::array<uint32_t, 4> descriptor {
|
||||
static_cast<uint32_t>(function_table),
|
||||
static_cast<uint32_t>(function_table >> 32u) | 0x00100000u,
|
||||
static_cast<uint32_t>(functions.size()), 0x5204u,
|
||||
};
|
||||
std::memcpy(reinterpret_cast<void*>(function_table), functions.data(), sizeof(functions));
|
||||
std::memcpy(reinterpret_cast<void*>(code_address), validator.data(), sizeof(validator));
|
||||
std::memcpy(reinterpret_cast<void*>(base), descriptor.data(), sizeof(descriptor));
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
|
@ -1629,8 +1572,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbContextStateOp(CommandBuffer* buf, uint32_t operat
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
uint32_t* first = nullptr;
|
||||
const auto append = [&](uint32_t segment_size_dw) {
|
||||
auto* segment = buf->AllocateDW(segment_size_dw);
|
||||
|
|
@ -1933,8 +1874,6 @@ uint32_t* KYTY_SYSV_ABI AgcCbSetShRegisterRangeDirect(CommandBuffer* buf, uint32
|
|||
|
||||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(num_values + 2);
|
||||
|
||||
if (cmd == nullptr) {
|
||||
|
|
@ -1976,8 +1915,6 @@ uint32_t* KYTY_SYSV_ABI AgcCbSetShRegistersDirect(CommandBuffer*
|
|||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
EXIT_NOT_IMPLEMENTED(regs == nullptr);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
std::vector<ShaderRegister> local_regs(num_regs);
|
||||
for (uint32_t i = 0; i < num_regs; i++) {
|
||||
local_regs[i].offset = regs[i].offset;
|
||||
|
|
@ -2043,8 +1980,6 @@ uint32_t* KYTY_SYSV_ABI AgcCbSetUcRegistersDirect(CommandBuffer*
|
|||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
EXIT_NOT_IMPLEMENTED(regs == nullptr);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
// Original implementation stages register values in a temporary DWORD array. It reads each new run only
|
||||
// after the preceding run has been allocated, so preserve that ordering around grow callbacks.
|
||||
std::vector<uint32_t> values(num_regs);
|
||||
|
|
@ -2135,8 +2070,6 @@ uint32_t* KYTY_SYSV_ABI AgcCbReleaseMem(CommandBuffer* buf, uint8_t action, uint
|
|||
data_sel != 5);
|
||||
EXIT_NOT_IMPLEMENTED(interrupt > 4);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(8);
|
||||
|
||||
if (cmd == nullptr) {
|
||||
|
|
@ -2189,8 +2122,6 @@ uint32_t* KYTY_SYSV_ABI AgcAcbResetQueue(CommandBuffer* buf, uint32_t op) {
|
|||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
EXIT_NOT_IMPLEMENTED((op & ~0x1c2u) != 0);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(2);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
|
@ -2211,8 +2142,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbResetQueue(CommandBuffer* buf, uint32_t op, uint32
|
|||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
EXIT_NOT_IMPLEMENTED((op & ~0xfffu) != 0);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(2);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
|
@ -2234,8 +2163,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbWaitUntilSafeForRendering(CommandBuffer* buf,
|
|||
|
||||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(7);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
|
@ -2288,8 +2215,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetWorkloadsActive(CommandBuffer* buf, uint32_t st
|
|||
}
|
||||
}
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(WORKLOAD_ACTIVE_PACKET_SIZE_DW);
|
||||
if (cmd == nullptr) {
|
||||
return nullptr;
|
||||
|
|
@ -2325,8 +2250,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetWorkloadComplete(CommandBuffer* buf, uint32_t s
|
|||
}
|
||||
}
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(WORKLOAD_COMPLETE_PACKET_SIZE_DW);
|
||||
if (cmd == nullptr) {
|
||||
return nullptr;
|
||||
|
|
@ -2350,8 +2273,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetShRegisterDirect(CommandBuffer* buf, ShaderRegi
|
|||
|
||||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(3);
|
||||
|
||||
if (cmd == nullptr) {
|
||||
|
|
@ -2371,8 +2292,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetCxRegisterDirect(CommandBuffer* buf, ShaderRegi
|
|||
|
||||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(3);
|
||||
|
||||
if (cmd == nullptr) {
|
||||
|
|
@ -2398,8 +2317,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetUcRegisterDirect(CommandBuffer* buf, ShaderRegi
|
|||
|
||||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(3);
|
||||
|
||||
if (cmd == nullptr) {
|
||||
|
|
@ -2425,8 +2342,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetCxRegistersIndirect(CommandBuffer*
|
|||
|
||||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(RegIndirectPacketSizeDw);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
|
@ -2454,8 +2369,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetShRegistersIndirect(CommandBuffer*
|
|||
|
||||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(RegIndirectPacketSizeDw);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
|
@ -2484,8 +2397,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetUcRegistersIndirect(CommandBuffer*
|
|||
|
||||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(RegIndirectPacketSizeDw);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
|
@ -2506,8 +2417,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetIndexSize(CommandBuffer* buf, uint8_t index_siz
|
|||
index_size, cache_policy);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(3);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
|
@ -2527,8 +2436,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetIndexBuffer(CommandBuffer* buf, uint64_t index_
|
|||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
EXIT_NOT_IMPLEMENTED((index_addr & 1u) != 0);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(3);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
|
@ -2547,8 +2454,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetIndexCount(CommandBuffer* buf, uint32_t index_c
|
|||
|
||||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(2);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
|
@ -2568,8 +2473,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetNumInstances(CommandBuffer* buf, uint32_t num_i
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(2);
|
||||
|
||||
if (cmd == nullptr) {
|
||||
|
|
@ -2666,8 +2569,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndex(CommandBuffer* buf, uint32_t index_count
|
|||
EXIT_NOT_IMPLEMENTED(index_addr == nullptr);
|
||||
EXIT_NOT_IMPLEMENTED((reinterpret_cast<uint64_t>(index_addr) & 1u) != 0);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(6);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
|
@ -2709,8 +2610,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndexMultiInstanced(CommandBuffer* buf, uint32
|
|||
EXIT_NOT_IMPLEMENTED(object_ids == nullptr);
|
||||
EXIT_NOT_IMPLEMENTED((reinterpret_cast<uint64_t>(index_addr) & 1u) != 0);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(9);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
|
@ -2778,8 +2677,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndexAuto(CommandBuffer* buf, uint32_t index_c
|
|||
|
||||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(3);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
|
@ -2808,8 +2705,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndexOffset(CommandBuffer* buf, uint32_t index
|
|||
|
||||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(5);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
|
@ -2839,8 +2734,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetBaseIndirectArgs(CommandBuffer* buf, uint32_t s
|
|||
|
||||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(4);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
|
@ -2865,8 +2758,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndexIndirect(CommandBuffer* buf, uint32_t dat
|
|||
|
||||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(5);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
|
@ -2894,8 +2785,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndirect(CommandBuffer* buf, uint32_t data_off
|
|||
|
||||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(5);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
|
@ -2932,8 +2821,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndirectMulti(CommandBuffer* buf,
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
constexpr uint32_t draw_packet_size_dw = 10u;
|
||||
constexpr uint32_t total_size_dw =
|
||||
AGC_INTERNAL_DATA_PACKET_SIZE_DW + draw_packet_size_dw +
|
||||
|
|
@ -2994,8 +2881,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndexIndirectMulti(CommandBuffer* buf,
|
|||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
EXIT_NOT_IMPLEMENTED((count_indirect & ~1u) != 0);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(10);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
|
@ -3044,8 +2929,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDispatchIndirect(CommandBuffer* buf, uint32_t data
|
|||
|
||||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(3);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
|
@ -3077,8 +2960,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbEventWrite(CommandBuffer* buf, uint8_t event_type,
|
|||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
EXIT_NOT_IMPLEMENTED(event_type > 0x3fu);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
const bool addressed_event = ((event_type & 0xfeu) == 0x38u);
|
||||
auto* cmd = buf->AllocateDW(addressed_event ? 4u : 2u);
|
||||
|
||||
|
|
@ -3174,8 +3055,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbAcquireMem(CommandBuffer* buf, uint8_t engine, uin
|
|||
}
|
||||
}
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(8);
|
||||
|
||||
if (cmd == nullptr) {
|
||||
|
|
@ -3234,8 +3113,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbCondExec(CommandBuffer* buf, const volatile uint32
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(5);
|
||||
|
||||
if (cmd == nullptr) {
|
||||
|
|
@ -3407,8 +3284,6 @@ uint32_t* KYTY_SYSV_ABI AgcAcbDispatchIndirect(CommandBuffer* buf,
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(4);
|
||||
|
||||
if (cmd == nullptr) {
|
||||
|
|
@ -3457,8 +3332,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbStallCommandBufferParser(CommandBuffer* buf) {
|
|||
|
||||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(2);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
|
@ -3745,6 +3618,20 @@ int KYTY_SYSV_ABI AgcSetPacketPredication(uint32_t* packet, uint32_t predication
|
|||
return OK;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI AgcSetNop(uint32_t* packet) {
|
||||
PRINT_NAME();
|
||||
|
||||
const auto size = AgcGetPacketSize(packet);
|
||||
|
||||
packet[0] = (packet[0] & 0xffff0003u) | (Pm4::IT_NOP << 8u);
|
||||
// Prevent the disabled payload from being treated as an executable marker.
|
||||
if (size > 1 && (packet[1] & 0xffff0000u) == 0x68750000u) {
|
||||
packet[1] &= 0xffffu;
|
||||
}
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI AgcSetRangePredication(uint32_t* start, const volatile uint32_t* end,
|
||||
uint32_t predication) {
|
||||
PRINT_NAME();
|
||||
|
|
@ -3864,8 +3751,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbWriteData(CommandBuffer* buf, uint8_t dst, uint8_t
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(4 + num_dwords);
|
||||
|
||||
if (cmd == nullptr) {
|
||||
|
|
@ -3918,8 +3803,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbGetLodStats(CommandBuffer* buf, uint8_t cache_poli
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(5);
|
||||
|
||||
if (cmd == nullptr) {
|
||||
|
|
@ -4122,8 +4005,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbWaitRegMem(CommandBuffer* buf, uint8_t size, uint8
|
|||
cache_policy);
|
||||
}
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto address_value = reinterpret_cast<uint64_t>(address);
|
||||
bool wait32 = (size == 0);
|
||||
auto poll = wait_reg_mem_poll_cycles_to_packet(poll_cycles);
|
||||
|
|
@ -4253,8 +4134,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetFlip(CommandBuffer* buf, uint32_t video_out_han
|
|||
|
||||
EXIT_NOT_IMPLEMENTED(buf == nullptr);
|
||||
|
||||
buf->DbgDump();
|
||||
|
||||
auto* cmd = buf->AllocateDW(6);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
|
@ -4275,7 +4154,11 @@ namespace Gen5Driver {
|
|||
|
||||
LIB_NAME("Graphics5Driver", "Graphics5Driver");
|
||||
|
||||
static int InitializeDmem() {
|
||||
int Initialize() {
|
||||
std::scoped_lock lock {g_driver_dmem.mutex};
|
||||
if (g_driver_dmem.physical_offset >= 0) {
|
||||
return OK;
|
||||
}
|
||||
int64_t physical_offset = -1;
|
||||
auto result = LibKernel::Memory::KernelAllocateDirectMemory(
|
||||
0, LibKernel::Memory::KernelGetDirectMemorySize(), DriverDmemSize, DriverDmemSize,
|
||||
|
|
@ -4292,6 +4175,31 @@ static int InitializeDmem() {
|
|||
}
|
||||
EXIT_IF(reinterpret_cast<uint64_t>(address) != DriverDmemBase);
|
||||
std::memset(address, 0, DriverDmemSize);
|
||||
struct FunctionShaderBindings {
|
||||
uint64_t code;
|
||||
uint32_t user_data[2];
|
||||
};
|
||||
static_assert(sizeof(FunctionShaderBindings) == 16);
|
||||
// Ordered-count queue validator, including its return to the caller.
|
||||
static constexpr std::array<uint32_t, 16> validator {
|
||||
0xbeeb03ff, 0x00000021, 0xb96a1818, 0xbf06106a,
|
||||
0xbf800000, 0x8590807e, 0xb96a0a18, 0xbf066a80,
|
||||
0xbf800000, 0x85ea807e, 0x88ea106a, 0xbf870003,
|
||||
0xbefc03ff, 0x8a6ca000, 0xbf920009, 0xbefd210e,
|
||||
};
|
||||
const auto base = DriverDmemBase + AgcDmemOffset;
|
||||
const auto function_table = base + 0x60;
|
||||
const auto code_address = base + 0x100; // Shader code requires 256-byte alignment.
|
||||
std::array<FunctionShaderBindings, 8> functions {};
|
||||
functions[2].code = code_address;
|
||||
const std::array<uint32_t, 4> descriptor {
|
||||
static_cast<uint32_t>(function_table),
|
||||
static_cast<uint32_t>(function_table >> 32u) | 0x00100000u,
|
||||
static_cast<uint32_t>(functions.size()), 0x5204u,
|
||||
};
|
||||
std::memcpy(reinterpret_cast<void*>(function_table), functions.data(), sizeof(functions));
|
||||
std::memcpy(reinterpret_cast<void*>(code_address), validator.data(), sizeof(validator));
|
||||
std::memcpy(reinterpret_cast<void*>(base), descriptor.data(), sizeof(descriptor));
|
||||
g_driver_dmem.physical_offset = physical_offset;
|
||||
return OK;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -270,6 +270,7 @@ uint32_t KYTY_SYSV_ABI AgcGetPacketSize(uint32_t* packet);
|
|||
int KYTY_SYSV_ABI AgcSetPacketPredication(uint32_t* packet, uint32_t predication);
|
||||
int KYTY_SYSV_ABI AgcSetRangePredication(uint32_t* start, const volatile uint32_t* end,
|
||||
uint32_t predication);
|
||||
int KYTY_SYSV_ABI AgcSetNop(uint32_t* packet);
|
||||
int KYTY_SYSV_ABI AgcRewindPatchSetRewindState(uint32_t* cmd, uint8_t state);
|
||||
int KYTY_SYSV_ABI AgcCondExecPatchSetEnd(uint32_t* cmd, const volatile uint32_t* buffer);
|
||||
int KYTY_SYSV_ABI AgcCondExecPatchSetCommandAddress(uint32_t* cmd,
|
||||
|
|
@ -307,6 +308,8 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetFlip(CommandBuffer* buf, uint32_t video_out_han
|
|||
|
||||
namespace Gen5Driver {
|
||||
|
||||
int Initialize();
|
||||
|
||||
struct Packet {
|
||||
uint32_t* addr;
|
||||
uint32_t dw_num;
|
||||
|
|
|
|||
|
|
@ -978,11 +978,7 @@ public:
|
|||
return false;
|
||||
}
|
||||
if (current_video) {
|
||||
retired_video.push_back(std::move(current_video->buffer));
|
||||
if (retired_video.size() >= static_cast<size_t>(max_video_buffers - 1)) {
|
||||
video_buffers.Push(std::move(retired_video.front()));
|
||||
retired_video.pop_front();
|
||||
}
|
||||
video_buffers.Push(std::move(current_video->buffer));
|
||||
}
|
||||
current_video = std::move(*frame);
|
||||
*out = current_video->info;
|
||||
|
|
@ -1060,7 +1056,6 @@ private:
|
|||
video_buffers.Clear();
|
||||
audio_buffers.Clear();
|
||||
if (!retain_output_buffers) {
|
||||
retired_video.clear();
|
||||
current_video.reset();
|
||||
current_audio.reset();
|
||||
}
|
||||
|
|
@ -1154,7 +1149,7 @@ private:
|
|||
bool AllocateBuffers() {
|
||||
if (video_id) {
|
||||
auto size = VideoBufferSize(fmt->streams[video_id.value()]);
|
||||
auto retained = retired_video.size() + (current_video ? 1u : 0u);
|
||||
auto retained = current_video ? 1u : 0u;
|
||||
for (size_t i = retained; i < static_cast<size_t>(max_video_buffers); i++) {
|
||||
auto buffer = std::make_unique<GuestBuffer>(mem, 0x100, size, true);
|
||||
if (!buffer->Valid()) {
|
||||
|
|
@ -1676,7 +1671,6 @@ private:
|
|||
WorkQueue<ReadyFrame> audio_frames;
|
||||
std::optional<ReadyFrame> current_video;
|
||||
std::optional<ReadyFrame> current_audio;
|
||||
std::deque<std::unique_ptr<GuestBuffer>> retired_video;
|
||||
LibKernel::Pthread demux_thread = nullptr;
|
||||
LibKernel::Pthread video_thread = nullptr;
|
||||
LibKernel::Pthread audio_thread = nullptr;
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ LIB_DEFINE(InitAgcDriver_1) {
|
|||
LIB_FUNC("Lkf86B98qPc", Gen5::AgcGetPacketSize);
|
||||
LIB_FUNC("w6Dj1VJt5qY", Gen5::AgcSetPacketPredication);
|
||||
LIB_FUNC("n8vgpaQg6dA", Gen5::AgcSetRangePredication);
|
||||
LIB_FUNC("K2mciNVxUCE", Gen5::AgcSetNop);
|
||||
LIB_FUNC("i1jyy49AjXU", Gen5::AgcDcbWriteData);
|
||||
LIB_FUNC("p9tI+yTvx68", Gen5::AgcDcbWriteDataGetSize);
|
||||
LIB_FUNC("vuSXe69VILM", Gen5::AgcDcbGetLodStats);
|
||||
|
|
|
|||
|
|
@ -868,6 +868,10 @@ static uint64_t AprReadFileRecordSize(uint64_t file_offset) {
|
|||
return (file_offset >> 32u) != 0 ? READ_FILE_RECORD_SIZE_EXT : READ_FILE_RECORD_SIZE;
|
||||
}
|
||||
|
||||
static uint64_t WriteAddressRecordSize(uint64_t value) {
|
||||
return value <= 3 ? 8 : value < (uint64_t {1} << 34) ? 12 : 16;
|
||||
}
|
||||
|
||||
static uint64_t AprReadGatherRecordSize(uint64_t file_offset) {
|
||||
return file_offset > 0x3ffffu ? READ_GATHER_RECORD_SIZE_EXT : READ_GATHER_RECORD_SIZE;
|
||||
}
|
||||
|
|
@ -1207,7 +1211,7 @@ static int ExecuteCommand(const CommandBufferState::Command& entry) {
|
|||
const auto& command = *payload;
|
||||
int result = OK;
|
||||
if (command.kind == AmmCommandKind::Unmap) {
|
||||
result = LibKernel::Memory::KernelMunmap(command.va, command.size);
|
||||
result = LibKernel::Memory::UnmapAmmMemory(command.va, command.size);
|
||||
} else {
|
||||
result = ExecuteAmmMapCommand(command);
|
||||
}
|
||||
|
|
@ -1617,11 +1621,10 @@ static uint64_t KYTY_SYSV_ABI MeasureCommandSizeFixed32(uint64_t, uint64_t, uint
|
|||
return 0x20;
|
||||
}
|
||||
|
||||
static uint64_t KYTY_SYSV_ABI MeasureCommandSizeWriteAddressOnCompletion(volatile uint64_t*,
|
||||
uint64_t) {
|
||||
static uint64_t KYTY_SYSV_ABI MeasureCommandSizeWriteAddress(volatile uint64_t*, uint64_t value) {
|
||||
PRINT_NAME();
|
||||
|
||||
return 0x20;
|
||||
return WriteAddressRecordSize(value);
|
||||
}
|
||||
|
||||
static uint64_t KYTY_SYSV_ABI
|
||||
|
|
@ -1950,7 +1953,7 @@ static int KYTY_SYSV_ABI CommandBufferWaitOnCounter(void* command_buffer, uint8_
|
|||
}
|
||||
|
||||
static int AppendWriteAddressCommand(void* command_buffer, volatile uint64_t* address,
|
||||
uint64_t value) {
|
||||
uint64_t value, uint64_t record_size) {
|
||||
if (command_buffer == nullptr || address == nullptr ||
|
||||
(reinterpret_cast<uint64_t>(address) & 7) != 0) {
|
||||
return LibKernel::KERNEL_ERROR_EINVAL;
|
||||
|
|
@ -1958,7 +1961,7 @@ static int AppendWriteAddressCommand(void* command_buffer, volatile uint64_t* ad
|
|||
return AppendCommandRecord(
|
||||
reinterpret_cast<uint64_t>(command_buffer),
|
||||
CommandBufferState::WriteAddressCommand {reinterpret_cast<uint64_t>(address), value},
|
||||
0x20)
|
||||
record_size)
|
||||
? OK
|
||||
: LibKernel::KERNEL_ERROR_EBUSY;
|
||||
}
|
||||
|
|
@ -1967,7 +1970,7 @@ static int KYTY_SYSV_ABI CommandBufferWriteAddress(void* command_buffer, volatil
|
|||
uint64_t value, uint32_t) {
|
||||
PRINT_NAME();
|
||||
|
||||
return AppendWriteAddressCommand(command_buffer, address, value);
|
||||
return AppendWriteAddressCommand(command_buffer, address, value, WriteAddressRecordSize(value));
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI CommandBufferWriteAddressOnCompletion(void* command_buffer,
|
||||
|
|
@ -1975,7 +1978,7 @@ static int KYTY_SYSV_ABI CommandBufferWriteAddressOnCompletion(void*
|
|||
uint64_t value) {
|
||||
PRINT_NAME();
|
||||
|
||||
return AppendWriteAddressCommand(command_buffer, address, value);
|
||||
return AppendWriteAddressCommand(command_buffer, address, value, WriteAddressRecordSize(value));
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI CommandBufferWriteCounter(void* command_buffer, uint8_t index,
|
||||
|
|
@ -2007,14 +2010,14 @@ static int KYTY_SYSV_ABI CommandBufferWriteAddressFromTimeCounter(void*
|
|||
uint32_t) {
|
||||
PRINT_NAME();
|
||||
|
||||
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0});
|
||||
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0}, 0x20);
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI CommandBufferWriteAddressFromTimeCounterOnCompletion(
|
||||
void* command_buffer, volatile uint64_t* address) {
|
||||
PRINT_NAME();
|
||||
|
||||
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0});
|
||||
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0}, 0x20);
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI CommandBufferWriteAddressFromCounter(void* command_buffer,
|
||||
|
|
@ -2022,14 +2025,14 @@ static int KYTY_SYSV_ABI CommandBufferWriteAddressFromCounter(void*
|
|||
uint32_t) {
|
||||
PRINT_NAME();
|
||||
|
||||
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0});
|
||||
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0}, 0x20);
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI CommandBufferWriteAddressFromCounterOnCompletion(
|
||||
void* command_buffer, volatile uint64_t* address, uint8_t) {
|
||||
PRINT_NAME();
|
||||
|
||||
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0});
|
||||
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0}, 0x20);
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI CommandBufferWriteAddressFromCounterPair(void* command_buffer,
|
||||
|
|
@ -2037,14 +2040,14 @@ static int KYTY_SYSV_ABI CommandBufferWriteAddressFromCounterPair(void*
|
|||
uint8_t, uint32_t) {
|
||||
PRINT_NAME();
|
||||
|
||||
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0});
|
||||
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0}, 0x20);
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI CommandBufferWriteAddressFromCounterPairOnCompletion(
|
||||
void* command_buffer, volatile uint64_t* address, uint8_t) {
|
||||
PRINT_NAME();
|
||||
|
||||
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0});
|
||||
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0}, 0x20);
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI AprCommandBufferReadFileGather(void* command_buffer, uint64_t, uint64_t,
|
||||
|
|
@ -2452,14 +2455,14 @@ LIB_DEFINE(InitAmpr_1) {
|
|||
LIB_FUNC("vWU-odnS+fU", Ampr::MeasureCommandSizeReadFile);
|
||||
LIB_FUNC("sSAUCCU1dv4", Ampr::MeasureCommandSizeWriteKernelEventQueue);
|
||||
LIB_FUNC("Zi3dBUjgyXI", Ampr::MeasureCommandSizeWriteKernelEventQueue);
|
||||
LIB_FUNC("C+IEj+BsAFM", Ampr::MeasureCommandSizeWriteAddressOnCompletion);
|
||||
LIB_FUNC("C+IEj+BsAFM", Ampr::MeasureCommandSizeWriteAddress);
|
||||
LIB_FUNC("x7SQEXfeovg", Ampr::MeasureCommandSizeWriteAddressFromTimeCounterOnCompletion);
|
||||
LIB_FUNC("32AcaTaBPSY", Ampr::MeasureCommandSizeWriteAddressFromCounterOnCompletion);
|
||||
LIB_FUNC("vxC58+DRk-U", Ampr::MeasureCommandSizeWriteAddressFromCounterPairOnCompletion);
|
||||
LIB_FUNC("4muPEJ-x5N8", Ampr::MeasureCommandSizeWriteCounterOnCompletion);
|
||||
LIB_FUNC("0BMj1hgG+kE", Ampr::MeasureCommandSizeFixed32);
|
||||
LIB_FUNC("ClnsFLLLcss", Ampr::MeasureCommandSizeFixed32);
|
||||
LIB_FUNC("4fgtGfXDrFc", Ampr::MeasureCommandSizeFixed32);
|
||||
LIB_FUNC("4fgtGfXDrFc", Ampr::MeasureCommandSizeWriteAddress);
|
||||
LIB_FUNC("gAtc79UTt5E", Ampr::MeasureCommandSizeFixed32);
|
||||
LIB_FUNC("JYd9g9L+TmE", Ampr::MeasureCommandSizeFixed32);
|
||||
LIB_FUNC("2Hw8gjMdwSY", Ampr::MeasureCommandSizeFixed32);
|
||||
|
|
|
|||
|
|
@ -54,11 +54,7 @@ struct CContext {
|
|||
std::list<atexit_func_t> atexit;
|
||||
};
|
||||
|
||||
struct InitEnvParams {
|
||||
int argc;
|
||||
uint32_t pad;
|
||||
const char* argv[3];
|
||||
};
|
||||
using InitEnvParams = Loader::EntryParams;
|
||||
|
||||
static int g_argc = 0;
|
||||
static const char* const* g_argv = nullptr;
|
||||
|
|
@ -271,9 +267,7 @@ static KYTY_SYSV_ABI void init_env(const InitEnvParams* params) {
|
|||
return;
|
||||
}
|
||||
|
||||
constexpr int argv_capacity = static_cast<int>(sizeof(params->argv) / sizeof(params->argv[0]));
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(params->argc < 0 || params->argc >= argv_capacity);
|
||||
EXIT_NOT_IMPLEMENTED(params->argc < 0);
|
||||
|
||||
g_argc = params->argc;
|
||||
g_argv = params->argv;
|
||||
|
|
|
|||
|
|
@ -3117,6 +3117,7 @@ LIB_DEFINE(InitLibKernel_1_FS) {
|
|||
LIB_FUNC("mBd4AfLP+u8", FileSystem::KernelPwritev);
|
||||
LIB_FUNC("eV9wAD2riIA", FileSystem::KernelStat);
|
||||
LIB_FUNC("kBwCPsYX-m4", FileSystem::KernelFstat);
|
||||
LIB_FUNC("VW3TVZiM4-E", FileSystem::KernelFtruncate);
|
||||
LIB_FUNC("AUXVxWeJU-A", FileSystem::KernelUnlink);
|
||||
LIB_FUNC("52NcYU9+lEo", FileSystem::KernelRename);
|
||||
LIB_FUNC("taRWhTJFTgE", FileSystem::KernelGetdirentries);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
#include "common/emulatorConfig.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/stringUtils.h"
|
||||
#include "emulator.h"
|
||||
#include "graphics/presentation/systemOverlay.h"
|
||||
#include "libs/dialog.h"
|
||||
#include "libs/errno.h"
|
||||
#include "libs/libs.h"
|
||||
|
|
@ -94,10 +96,15 @@ struct SystemServiceHdrToneMapLuminance {
|
|||
|
||||
static int KYTY_SYSV_ABI SystemServiceHideSplashScreen() {
|
||||
PRINT_NAME();
|
||||
|
||||
Graphics::HideSplashScreen();
|
||||
return OK;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI SystemServiceLoadExec(const char* path, const char* const argv[]) {
|
||||
PRINT_NAME();
|
||||
return Emulator::LoadExec(path, argv);
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI SystemServiceParamGetInt(int param_id, int* value) {
|
||||
PRINT_NAME();
|
||||
|
||||
|
|
@ -558,6 +565,7 @@ LIB_DEFINE(InitSystemGesture_1) {
|
|||
} // namespace SystemGesture
|
||||
|
||||
LIB_DEFINE(InitSystemService_1) {
|
||||
LIB_FUNC("JoBqSQt1yyA", SystemService::SystemServiceLoadExec);
|
||||
LIB_FUNC("Vo5V8KAwCmk", SystemService::SystemServiceHideSplashScreen);
|
||||
LIB_FUNC("fZo48un7LK4", SystemService::SystemServiceParamGetInt);
|
||||
LIB_FUNC("SsC-m-S9JTA", SystemService::SystemServiceParamGetString);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,459 @@
|
|||
#include "common/abi.h"
|
||||
#include "libs/libs.h"
|
||||
#include "libs/videoDec2Decoder.h"
|
||||
#include "loader/symbolDatabase.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <new>
|
||||
#include <optional>
|
||||
#include <shared_mutex>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace Libs::Vdecsw {
|
||||
|
||||
LIB_VERSION("Vdecsw", 1, "Vdecsw", 1, 1);
|
||||
|
||||
namespace Decoder = VideoDec2::Decoder;
|
||||
|
||||
enum class Error : uint32_t {
|
||||
ApiFail = 0x0100,
|
||||
StructSize,
|
||||
ArgumentPointer,
|
||||
DecoderInstance,
|
||||
MemorySize,
|
||||
MemoryPointer,
|
||||
FrameBufferSize,
|
||||
FrameBufferPointer,
|
||||
FrameBufferAlignment,
|
||||
AccessUnitSize = 0x010d,
|
||||
AccessUnitPointer,
|
||||
OutputInfo,
|
||||
ComputeQueue,
|
||||
InputQueueFull = 0x0113,
|
||||
OutputBufferFull,
|
||||
InputQueueEmpty = 0x0116,
|
||||
DecodePending,
|
||||
OutputBufferEmpty,
|
||||
ConfigInfo = 0x0200,
|
||||
ComputePipeId,
|
||||
ComputeQueueId,
|
||||
ResourceType,
|
||||
CodecType,
|
||||
InputQueueDepth = 0x0206,
|
||||
DpbFrameCount = 0x0209,
|
||||
FrameWidthHeight,
|
||||
ExtraConfigInfo,
|
||||
AccessUnit = 0x0301,
|
||||
OversizeDecode,
|
||||
};
|
||||
|
||||
constexpr int32_t Fail(Error error) {
|
||||
return static_cast<int32_t>(0x81510000u | static_cast<uint32_t>(error));
|
||||
}
|
||||
|
||||
struct ComputeMemoryInfo {
|
||||
size_t this_size;
|
||||
size_t cpu_gpu_memory_size;
|
||||
void* cpu_gpu_memory;
|
||||
};
|
||||
|
||||
struct ComputeConfigInfo {
|
||||
size_t this_size;
|
||||
uint16_t pipe_id;
|
||||
uint16_t queue_id;
|
||||
bool check_memory_type;
|
||||
uint8_t reserved0;
|
||||
uint16_t reserved1;
|
||||
};
|
||||
|
||||
struct ComputeQueue {
|
||||
uint16_t pipe_id;
|
||||
uint16_t queue_id;
|
||||
uint32_t decoders = 0;
|
||||
};
|
||||
|
||||
struct ConfigInfo {
|
||||
size_t this_size;
|
||||
uint32_t resource_type;
|
||||
uint32_t codec_type;
|
||||
uint32_t profile;
|
||||
uint32_t max_level;
|
||||
int32_t max_width;
|
||||
int32_t max_height;
|
||||
int32_t max_dpb_frames;
|
||||
uint32_t input_queue_depth;
|
||||
ComputeQueue* compute_queue;
|
||||
uint64_t cpu_affinity_mask;
|
||||
int32_t cpu_thread_priority;
|
||||
bool optimize_progressive;
|
||||
bool check_memory_type;
|
||||
uint8_t reserved0;
|
||||
int8_t extra_dpb_frames;
|
||||
void* extra_config;
|
||||
bool disable_sync_input;
|
||||
uint32_t max_pending_sync;
|
||||
};
|
||||
|
||||
struct MemoryInfo {
|
||||
size_t this_size;
|
||||
size_t cpu_memory_size;
|
||||
void* cpu_memory;
|
||||
size_t gpu_memory_size;
|
||||
void* gpu_memory;
|
||||
size_t cpu_gpu_memory_size;
|
||||
void* cpu_gpu_memory;
|
||||
size_t max_frame_buffer_size;
|
||||
uint32_t frame_buffer_alignment;
|
||||
uint32_t reserved0;
|
||||
};
|
||||
|
||||
struct InputData {
|
||||
size_t this_size;
|
||||
void* au_data;
|
||||
size_t au_size;
|
||||
uint64_t pts;
|
||||
uint64_t dts;
|
||||
uint64_t attached_data;
|
||||
};
|
||||
|
||||
struct InputResult {
|
||||
size_t this_size;
|
||||
void* decoded_au;
|
||||
uint32_t output_frame_count;
|
||||
uint32_t reserved0;
|
||||
};
|
||||
|
||||
struct FrameBuffer {
|
||||
size_t this_size;
|
||||
void* data;
|
||||
size_t size;
|
||||
};
|
||||
|
||||
struct OutputInfo {
|
||||
size_t this_size;
|
||||
bool valid;
|
||||
bool last_frame;
|
||||
bool error_frame;
|
||||
uint8_t picture_count;
|
||||
uint32_t codec_type;
|
||||
uint32_t width;
|
||||
uint32_t pitch;
|
||||
uint32_t height;
|
||||
bool discarded_frame;
|
||||
void* frame_buffer;
|
||||
size_t frame_buffer_size;
|
||||
uint32_t format;
|
||||
uint32_t pitch_in_bytes;
|
||||
};
|
||||
|
||||
static_assert(sizeof(ComputeMemoryInfo) == 24);
|
||||
static_assert(sizeof(ComputeConfigInfo) == 16);
|
||||
static_assert(sizeof(ConfigInfo) == 80);
|
||||
static_assert(offsetof(ConfigInfo, extra_dpb_frames) == 63);
|
||||
static_assert(offsetof(ConfigInfo, disable_sync_input) == 72);
|
||||
static_assert(sizeof(MemoryInfo) == 72);
|
||||
static_assert(sizeof(InputData) == 48);
|
||||
static_assert(sizeof(InputResult) == 24);
|
||||
static_assert(sizeof(FrameBuffer) == 24);
|
||||
static_assert(sizeof(OutputInfo) == 56);
|
||||
static_assert(offsetof(OutputInfo, discarded_frame) == 28);
|
||||
static_assert(offsetof(OutputInfo, frame_buffer) == 32);
|
||||
|
||||
struct State;
|
||||
static std::shared_mutex g_registry_mutex;
|
||||
static std::unordered_set<ComputeQueue*> g_queues;
|
||||
static std::unordered_set<State*> g_decoders;
|
||||
|
||||
struct State {
|
||||
ConfigInfo config;
|
||||
Decoder::Instance* decoder;
|
||||
std::mutex mutex;
|
||||
std::deque<InputResult> completed_inputs;
|
||||
std::optional<FrameBuffer> output_buffer;
|
||||
|
||||
~State() {
|
||||
Decoder::Destroy(decoder);
|
||||
--config.compute_queue->decoders;
|
||||
}
|
||||
};
|
||||
|
||||
// The shared registry lock keeps guest-backed state alive until each API call returns.
|
||||
// Deletion takes the exclusive lock before releasing that memory to the guest.
|
||||
struct StateAccess {
|
||||
std::shared_lock<std::shared_mutex> lock {g_registry_mutex};
|
||||
State* value;
|
||||
explicit StateAccess(State* handle): value(g_decoders.contains(handle) ? handle : nullptr) {}
|
||||
explicit operator bool() const { return value != nullptr; }
|
||||
State* operator->() const { return value; }
|
||||
};
|
||||
|
||||
static int32_t Result(Decoder::Result result) {
|
||||
switch (result) {
|
||||
case Decoder::Result::Ok: return 0;
|
||||
case Decoder::Result::ApiFail: return Fail(Error::ApiFail);
|
||||
case Decoder::Result::AccessUnit: return Fail(Error::AccessUnit);
|
||||
case Decoder::Result::FrameBufferSize: return Fail(Error::FrameBufferSize);
|
||||
case Decoder::Result::OversizeDecode: return Fail(Error::OversizeDecode);
|
||||
case Decoder::Result::InputFull: return Fail(Error::OutputBufferFull);
|
||||
case Decoder::Result::NoFrame: return Fail(Error::DecodePending);
|
||||
}
|
||||
return Fail(Error::ApiFail);
|
||||
}
|
||||
|
||||
static int32_t ValidateConfig(const ConfigInfo* config) {
|
||||
if (config == nullptr) return Fail(Error::ArgumentPointer);
|
||||
if (config->this_size != sizeof(ConfigInfo)) return Fail(Error::StructSize);
|
||||
if (config->resource_type != 1) return Fail(Error::ResourceType);
|
||||
if (config->codec_type != 1) return Fail(Error::CodecType);
|
||||
if (config->reserved0 != 0) return Fail(Error::ConfigInfo);
|
||||
if (config->extra_config != nullptr) return Fail(Error::ExtraConfigInfo);
|
||||
if (config->input_queue_depth < 1 || config->input_queue_depth > 8)
|
||||
return Fail(Error::InputQueueDepth);
|
||||
if (config->max_dpb_frames < 1 || config->extra_dpb_frames < 0)
|
||||
return Fail(Error::DpbFrameCount);
|
||||
if (config->max_width <= 0 || config->max_height <= 0) return Fail(Error::FrameWidthHeight);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static size_t FrameSize(const ConfigInfo& config) {
|
||||
// AVC output: 256-byte pitch, 16-line maximum height, and 4096 bytes of metadata.
|
||||
const auto pitch = (static_cast<size_t>(config.max_width) + 255u) & ~size_t {255};
|
||||
const auto height = (static_cast<size_t>(config.max_height) + 15u) & ~size_t {15};
|
||||
return pitch * height * 3 / 2 + 4096;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI QueryComputeMemoryInfo(ComputeMemoryInfo* memory) {
|
||||
PRINT_NAME();
|
||||
if (memory == nullptr) return Fail(Error::ArgumentPointer);
|
||||
if (memory->this_size != sizeof(ComputeMemoryInfo)) return Fail(Error::StructSize);
|
||||
memory->cpu_gpu_memory_size = sizeof(ComputeQueue);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI AllocateComputeQueue(const ComputeConfigInfo* config,
|
||||
const ComputeMemoryInfo* memory,
|
||||
ComputeQueue** output) {
|
||||
PRINT_NAME();
|
||||
if (config == nullptr || memory == nullptr || output == nullptr)
|
||||
return Fail(Error::ArgumentPointer);
|
||||
if (config->this_size != sizeof(ComputeConfigInfo) ||
|
||||
memory->this_size != sizeof(ComputeMemoryInfo))
|
||||
return Fail(Error::StructSize);
|
||||
if (config->reserved0 != 0 || config->reserved1 != 0) return Fail(Error::ConfigInfo);
|
||||
if (config->pipe_id > 4) return Fail(Error::ComputePipeId);
|
||||
if (config->queue_id > 7) return Fail(Error::ComputeQueueId);
|
||||
if (memory->cpu_gpu_memory_size < sizeof(ComputeQueue)) return Fail(Error::MemorySize);
|
||||
if (memory->cpu_gpu_memory == nullptr ||
|
||||
reinterpret_cast<uintptr_t>(memory->cpu_gpu_memory) % alignof(ComputeQueue) != 0)
|
||||
return Fail(Error::MemoryPointer);
|
||||
std::scoped_lock lock(g_registry_mutex);
|
||||
for (const auto* queue: g_queues)
|
||||
if ((queue->pipe_id == config->pipe_id && queue->queue_id == config->queue_id) ||
|
||||
queue == memory->cpu_gpu_memory)
|
||||
return Fail(Error::ComputeQueue);
|
||||
auto* queue = new (memory->cpu_gpu_memory) ComputeQueue {config->pipe_id, config->queue_id};
|
||||
g_queues.insert(queue);
|
||||
*output = queue;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI ReleaseComputeQueue(ComputeQueue* queue) {
|
||||
PRINT_NAME();
|
||||
std::scoped_lock lock(g_registry_mutex);
|
||||
if (!g_queues.contains(queue) || queue->decoders != 0) return Fail(Error::ComputeQueue);
|
||||
g_queues.erase(queue);
|
||||
queue->~ComputeQueue();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI QueryDecoderMemoryInfo(const ConfigInfo* config, MemoryInfo* memory) {
|
||||
PRINT_NAME();
|
||||
if (const auto error = ValidateConfig(config); error != 0) return error;
|
||||
if (memory == nullptr) return Fail(Error::ArgumentPointer);
|
||||
if (memory->this_size != sizeof(MemoryInfo)) return Fail(Error::StructSize);
|
||||
memory->cpu_memory_size = sizeof(State);
|
||||
// The FFmpeg backend needs no guest GPU workspace.
|
||||
memory->gpu_memory_size = 0;
|
||||
memory->cpu_gpu_memory_size = 0;
|
||||
memory->max_frame_buffer_size = FrameSize(*config);
|
||||
memory->frame_buffer_alignment = 256;
|
||||
memory->reserved0 = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI CreateDecoder(const ConfigInfo* config, const MemoryInfo* memory,
|
||||
State** output) {
|
||||
PRINT_NAME();
|
||||
if (const auto error = ValidateConfig(config); error != 0) return error;
|
||||
if (memory == nullptr || output == nullptr) return Fail(Error::ArgumentPointer);
|
||||
if (memory->this_size != sizeof(MemoryInfo)) return Fail(Error::StructSize);
|
||||
if (memory->cpu_memory_size < sizeof(State)) return Fail(Error::MemorySize);
|
||||
if (memory->cpu_memory == nullptr ||
|
||||
reinterpret_cast<uintptr_t>(memory->cpu_memory) % alignof(State) != 0)
|
||||
return Fail(Error::MemoryPointer);
|
||||
std::scoped_lock lock(g_registry_mutex);
|
||||
if (!g_queues.contains(config->compute_queue)) return Fail(Error::ComputeQueue);
|
||||
if (g_decoders.contains(static_cast<State*>(memory->cpu_memory)))
|
||||
return Fail(Error::MemoryPointer);
|
||||
auto* decoder = Decoder::Create(
|
||||
{config->codec_type, config->max_width, config->max_height,
|
||||
std::max(2u, static_cast<uint32_t>(config->max_dpb_frames) + config->extra_dpb_frames)});
|
||||
if (decoder == nullptr) return Fail(Error::ApiFail);
|
||||
auto* state = new (memory->cpu_memory) State {*config, decoder};
|
||||
++config->compute_queue->decoders;
|
||||
g_decoders.insert(state);
|
||||
*output = state;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI DeleteDecoder(State* handle) {
|
||||
PRINT_NAME();
|
||||
std::scoped_lock lock(g_registry_mutex);
|
||||
if (g_decoders.erase(handle) == 0) return Fail(Error::DecoderInstance);
|
||||
handle->~State();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI SetDecodeInput(State* handle, const InputData* input) {
|
||||
PRINT_NAME();
|
||||
const StateAccess state(handle);
|
||||
if (!state) return Fail(Error::DecoderInstance);
|
||||
if (input == nullptr) return Fail(Error::ArgumentPointer);
|
||||
if (input->this_size != sizeof(InputData)) return Fail(Error::StructSize);
|
||||
if (input->au_data == nullptr) return Fail(Error::AccessUnitPointer);
|
||||
if (input->au_size == 0) return Fail(Error::AccessUnitSize);
|
||||
std::scoped_lock lock(state->mutex);
|
||||
const auto& config = state->config;
|
||||
if (!config.disable_sync_input &&
|
||||
state->completed_inputs.size() >=
|
||||
uint64_t {config.input_queue_depth} + config.max_pending_sync)
|
||||
return Fail(Error::InputQueueFull);
|
||||
uint32_t produced = 0;
|
||||
const auto result = Decoder::Submit(
|
||||
state->decoder,
|
||||
{input->au_data, input->au_size, input->pts, input->dts, input->attached_data}, &produced);
|
||||
if (result != Decoder::Result::Ok) return Result(result);
|
||||
if (!config.disable_sync_input)
|
||||
state->completed_inputs.push_back({sizeof(InputResult), input->au_data, produced, 0});
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI TrySyncDecodeInput(State* handle, InputResult* output) {
|
||||
PRINT_NAME();
|
||||
const StateAccess state(handle);
|
||||
if (!state) return Fail(Error::DecoderInstance);
|
||||
if (output == nullptr) return Fail(Error::ArgumentPointer);
|
||||
if (output->this_size != sizeof(InputResult)) return Fail(Error::StructSize);
|
||||
std::scoped_lock lock(state->mutex);
|
||||
if (state->config.disable_sync_input) return Fail(Error::ApiFail);
|
||||
if (state->completed_inputs.empty()) return Fail(Error::InputQueueEmpty);
|
||||
*output = state->completed_inputs.front();
|
||||
state->completed_inputs.pop_front();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI SetDecodeOutput(State* handle, FrameBuffer* buffer) {
|
||||
PRINT_NAME();
|
||||
const StateAccess state(handle);
|
||||
if (!state) return Fail(Error::DecoderInstance);
|
||||
if (buffer == nullptr) return Fail(Error::ArgumentPointer);
|
||||
if (buffer->this_size != sizeof(FrameBuffer)) return Fail(Error::StructSize);
|
||||
if (buffer->data == nullptr) return Fail(Error::FrameBufferPointer);
|
||||
if (reinterpret_cast<uintptr_t>(buffer->data) % 256 != 0)
|
||||
return Fail(Error::FrameBufferAlignment);
|
||||
if (buffer->size < FrameSize(state->config)) return Fail(Error::FrameBufferSize);
|
||||
std::scoped_lock lock(state->mutex);
|
||||
if (state->output_buffer) return Fail(Error::OutputBufferFull);
|
||||
state->output_buffer = *buffer;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI TrySyncDecodeOutput(State* handle, OutputInfo* output) {
|
||||
PRINT_NAME();
|
||||
const StateAccess state(handle);
|
||||
if (!state) return Fail(Error::DecoderInstance);
|
||||
if (output == nullptr) return Fail(Error::ArgumentPointer);
|
||||
if (output->this_size != sizeof(OutputInfo)) return Fail(Error::StructSize);
|
||||
std::scoped_lock lock(state->mutex);
|
||||
if (!state->output_buffer) return Fail(Error::OutputBufferEmpty);
|
||||
const auto& buffer = *state->output_buffer;
|
||||
Decoder::Output decoded {};
|
||||
const auto result = Decoder::Receive(state->decoder, {buffer.data, buffer.size}, &decoded);
|
||||
if (result != Decoder::Result::Ok) return Result(result);
|
||||
*output = {sizeof(OutputInfo),
|
||||
true,
|
||||
decoded.last_frame,
|
||||
decoded.error_frame,
|
||||
1,
|
||||
decoded.codec_type,
|
||||
decoded.width,
|
||||
decoded.pitch,
|
||||
decoded.height,
|
||||
false,
|
||||
decoded.buffer,
|
||||
decoded.buffer_size,
|
||||
0,
|
||||
decoded.pitch};
|
||||
state->output_buffer.reset();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI FinalizeDecodeSequence(State* handle) {
|
||||
PRINT_NAME();
|
||||
const StateAccess state(handle);
|
||||
if (!state) return Fail(Error::DecoderInstance);
|
||||
std::scoped_lock lock(state->mutex);
|
||||
return Result(Decoder::Finalize(state->decoder));
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI ResetDecoder(State* handle) {
|
||||
PRINT_NAME();
|
||||
const StateAccess state(handle);
|
||||
if (!state) return Fail(Error::DecoderInstance);
|
||||
std::scoped_lock lock(state->mutex);
|
||||
Decoder::Reset(state->decoder);
|
||||
state->completed_inputs.clear();
|
||||
state->output_buffer.reset();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI GetAvcPictureInfo(const OutputInfo* output,
|
||||
Decoder::AvcPictureInfo* first,
|
||||
Decoder::AvcPictureInfo* second) {
|
||||
PRINT_NAME();
|
||||
if (output == nullptr || first == nullptr) return Fail(Error::ArgumentPointer);
|
||||
if (output->this_size != sizeof(OutputInfo) || first->this_size != sizeof(*first) ||
|
||||
(second != nullptr && second->this_size != sizeof(*second)))
|
||||
return Fail(Error::StructSize);
|
||||
if (!output->valid || output->codec_type != 1 || output->picture_count == 0)
|
||||
return Fail(Error::OutputInfo);
|
||||
Decoder::PictureInfo decoded {};
|
||||
if (!Decoder::GetPictureInfo(output->frame_buffer, &decoded) || decoded.codec_type != 1)
|
||||
return Fail(Error::OutputInfo);
|
||||
*first = Decoder::MakeAvcPictureInfo(decoded, sizeof(*first));
|
||||
if (second != nullptr) *second = {.this_size = sizeof(*second)};
|
||||
return 0;
|
||||
}
|
||||
|
||||
LIB_DEFINE(InitVdecsw_1) {
|
||||
PRINT_NAME_ENABLE(true);
|
||||
LIB_FUNC("0moTubWCsTM", QueryComputeMemoryInfo);
|
||||
LIB_FUNC("hIgrg5h4V6s", AllocateComputeQueue);
|
||||
LIB_FUNC("A+2M7EivuOU", QueryDecoderMemoryInfo);
|
||||
LIB_FUNC("+L5ArV1tPGA", CreateDecoder);
|
||||
LIB_FUNC("ecUtPX+dBYk", DeleteDecoder);
|
||||
LIB_FUNC("fX-zOOefbbs", ReleaseComputeQueue);
|
||||
LIB_FUNC("aqMiF0AgUYI", SetDecodeInput);
|
||||
LIB_FUNC("veb-YBrOqo0", ResetDecoder);
|
||||
LIB_FUNC("kMBw37oH8nI", TrySyncDecodeOutput);
|
||||
LIB_FUNC("rgtMCOpyBSc", SetDecodeOutput);
|
||||
LIB_FUNC("ihNT-uuEAr4", GetAvcPictureInfo);
|
||||
LIB_FUNC("5Y6nZqIZvBg", FinalizeDecodeSequence);
|
||||
LIB_FUNC("l4sQYy5wPkc", TrySyncDecodeInput);
|
||||
}
|
||||
|
||||
} // namespace Libs::Vdecsw
|
||||
|
|
@ -111,69 +111,7 @@ struct Videodec2FrameBuffer {
|
|||
bool is_accepted;
|
||||
};
|
||||
|
||||
struct Videodec2AvcPictureInfo {
|
||||
size_t this_size;
|
||||
bool is_valid;
|
||||
|
||||
uint64_t pts_data;
|
||||
uint64_t dts_data;
|
||||
uint64_t attached_data;
|
||||
|
||||
uint8_t idr_picture_flag;
|
||||
uint8_t profile_idc;
|
||||
uint8_t level_idc;
|
||||
uint32_t pic_width_in_mbs_minus1;
|
||||
uint32_t pic_height_in_map_units_minus1;
|
||||
uint8_t frame_mbs_only_flag;
|
||||
|
||||
uint8_t frame_cropping_flag;
|
||||
uint32_t frame_crop_left_offset;
|
||||
uint32_t frame_crop_right_offset;
|
||||
uint32_t frame_crop_top_offset;
|
||||
uint32_t frame_crop_bottom_offset;
|
||||
|
||||
uint8_t aspect_ratio_info_present_flag;
|
||||
uint8_t aspect_ratio_idc;
|
||||
uint16_t sar_width;
|
||||
uint16_t sar_height;
|
||||
|
||||
uint8_t video_signal_type_present_flag;
|
||||
uint8_t video_format;
|
||||
uint8_t video_full_range_flag;
|
||||
uint8_t colour_description_present_flag;
|
||||
uint8_t colour_primaries;
|
||||
uint8_t transfer_characteristics;
|
||||
uint8_t matrix_coefficients;
|
||||
|
||||
uint8_t timing_info_present_flag;
|
||||
uint32_t num_units_in_tick;
|
||||
uint32_t time_scale;
|
||||
uint8_t fixed_frame_rate_flag;
|
||||
|
||||
uint8_t bitstream_restriction_flag;
|
||||
uint8_t max_dec_frame_buffering;
|
||||
|
||||
uint8_t pic_struct_present_flag;
|
||||
uint8_t pic_struct;
|
||||
uint8_t field_pic_flag;
|
||||
uint8_t bottom_field_flag;
|
||||
|
||||
uint8_t sequence_parameter_set_present_flag;
|
||||
uint8_t picture_parameter_set_present_flag;
|
||||
uint8_t au_delimiter_present_flag;
|
||||
uint8_t end_of_sequence_present_flag;
|
||||
uint8_t end_of_stream_present_flag;
|
||||
uint8_t filler_data_present_flag;
|
||||
uint8_t picture_timing_sei_present_flag;
|
||||
uint8_t buffering_period_sei_present_flag;
|
||||
|
||||
uint8_t constraint_set0_flag;
|
||||
uint8_t constraint_set1_flag;
|
||||
uint8_t constraint_set2_flag;
|
||||
uint8_t constraint_set3_flag;
|
||||
uint8_t constraint_set4_flag;
|
||||
uint8_t constraint_set5_flag;
|
||||
};
|
||||
using Videodec2AvcPictureInfo = Decoder::AvcPictureInfo;
|
||||
|
||||
struct Videodec2ComputeMemoryInfo {
|
||||
size_t this_size;
|
||||
|
|
@ -235,6 +173,8 @@ static void FillNoPictureOutput(const Videodec2FrameBuffer* frame_buffer,
|
|||
static int32_t MapDecoderResult(Decoder::Result result) {
|
||||
switch (result) {
|
||||
case Decoder::Result::Ok: return OK;
|
||||
case Decoder::Result::InputFull:
|
||||
case Decoder::Result::NoFrame:
|
||||
case Decoder::Result::ApiFail: return VIDEODEC2_ERROR_API_FAIL;
|
||||
case Decoder::Result::AccessUnit: return VIDEODEC2_ERROR_ACCESS_UNIT;
|
||||
case Decoder::Result::FrameBufferSize: return VIDEODEC2_ERROR_FRAME_BUFFER_SIZE;
|
||||
|
|
@ -599,40 +539,7 @@ static int32_t KYTY_SYSV_ABI GetPictureInfo(const Videodec2OutputInfo* output_in
|
|||
return VIDEODEC2_ERROR_STRUCT_SIZE;
|
||||
}
|
||||
|
||||
Videodec2AvcPictureInfo picture {};
|
||||
picture.this_size = requested_size;
|
||||
picture.is_valid = true;
|
||||
picture.pts_data = decoded.pts;
|
||||
picture.dts_data = decoded.dts;
|
||||
picture.attached_data = decoded.attached_data;
|
||||
picture.idr_picture_flag = decoded.key_frame ? 1 : 0;
|
||||
picture.profile_idc = static_cast<uint8_t>(decoded.profile);
|
||||
picture.level_idc = static_cast<uint8_t>(decoded.level);
|
||||
picture.pic_width_in_mbs_minus1 = (decoded.width + 15u) / 16u - 1u;
|
||||
picture.pic_height_in_map_units_minus1 = (decoded.height + 15u) / 16u - 1u;
|
||||
picture.frame_mbs_only_flag = 1;
|
||||
picture.frame_cropping_flag = decoded.crop_left != 0 || decoded.crop_right != 0 ||
|
||||
decoded.crop_top != 0 || decoded.crop_bottom != 0
|
||||
? 1
|
||||
: 0;
|
||||
picture.frame_crop_left_offset = decoded.crop_left;
|
||||
picture.frame_crop_right_offset = decoded.crop_right;
|
||||
picture.frame_crop_top_offset = decoded.crop_top;
|
||||
picture.frame_crop_bottom_offset = decoded.crop_bottom;
|
||||
picture.aspect_ratio_info_present_flag =
|
||||
decoded.sar_width != 0 && decoded.sar_height != 0 ? 1 : 0;
|
||||
picture.aspect_ratio_idc = picture.aspect_ratio_info_present_flag ? 255 : 0;
|
||||
picture.sar_width = decoded.sar_width;
|
||||
picture.sar_height = decoded.sar_height;
|
||||
picture.video_signal_type_present_flag = 1;
|
||||
picture.video_format = 5;
|
||||
picture.video_full_range_flag = decoded.color_range == 2 ? 1 : 0;
|
||||
picture.colour_description_present_flag =
|
||||
decoded.color_primaries != 0 || decoded.color_trc != 0 || decoded.color_space != 0 ? 1
|
||||
: 0;
|
||||
picture.colour_primaries = decoded.color_primaries;
|
||||
picture.transfer_characteristics = decoded.color_trc;
|
||||
picture.matrix_coefficients = decoded.color_space;
|
||||
const auto picture = Decoder::MakeAvcPictureInfo(decoded, requested_size);
|
||||
std::memcpy(first_picture_info, &picture, requested_size);
|
||||
} else {
|
||||
const auto result = fill_common(first_picture_info, true);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ namespace VideoDec2 {
|
|||
LIB_DEFINE(InitVideoDec2_1);
|
||||
} // namespace VideoDec2
|
||||
|
||||
namespace Vdecsw {
|
||||
LIB_DEFINE(InitVdecsw_1);
|
||||
} // namespace Vdecsw
|
||||
|
||||
namespace LibMouse {
|
||||
LIB_DEFINE(InitMouse_1);
|
||||
} // namespace LibMouse
|
||||
|
|
@ -148,6 +152,7 @@ void InitAll(Loader::SymbolDatabase* s) {
|
|||
LibUlt::InitUlt_1(s);
|
||||
InitUserService_1(s);
|
||||
VideoDec2::InitVideoDec2_1(s);
|
||||
Vdecsw::InitVdecsw_1(s);
|
||||
LibGen5::InitVideoOut_1(s);
|
||||
LibGen5::VrrStatus::InitVideoOutVrrStatus_1(s);
|
||||
InitWebBrowserDialog_1(s);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@
|
|||
#else
|
||||
#include <arpa/inet.h>
|
||||
#include <fcntl.h>
|
||||
#include <ifaddrs.h>
|
||||
#include <net/if.h>
|
||||
#include <net/route.h>
|
||||
#include <netdb.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <sys/select.h>
|
||||
|
|
@ -41,10 +44,12 @@
|
|||
#include <cstring>
|
||||
#include <fmt/format.h>
|
||||
#include <fmt/ranges.h>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
|
@ -3386,7 +3391,6 @@ constexpr int NET_CTL_STATE_DISCONNECTED = 0;
|
|||
constexpr int NET_CTL_STATE_IPOBTAINED = 3;
|
||||
constexpr int NET_CTL_DEVICE_WIRED = 0;
|
||||
constexpr int NET_CTL_DEVICE_WIRELESS = 1;
|
||||
constexpr int NET_CTL_LINK_DISCONNECTED = 0;
|
||||
constexpr int NET_CTL_LINK_CONNECTED = 1;
|
||||
constexpr int NET_CTL_EVENT_DISCONNECTED = 1;
|
||||
constexpr int NET_CTL_EVENT_IPOBTAINED = 3;
|
||||
|
|
@ -3448,8 +3452,6 @@ struct NetCtlCallbackSlot {
|
|||
|
||||
static Common::Mutex g_net_ctl_callbacks_mutex;
|
||||
static std::array<NetCtlCallbackSlot, 8> g_net_ctl_callbacks;
|
||||
static std::atomic_bool g_net_ctl_connected = false;
|
||||
static std::atomic_bool g_net_ctl_status_initialized = false;
|
||||
|
||||
struct HostNetworkInfo {
|
||||
bool connected = false;
|
||||
|
|
@ -3463,22 +3465,16 @@ struct HostNetworkInfo {
|
|||
};
|
||||
|
||||
static void CopyIpv4String(char* dst, size_t dst_size, const sockaddr* addr) {
|
||||
#if defined(_WIN32)
|
||||
if (dst == nullptr || dst_size == 0 || addr == nullptr || addr->sa_family != AF_INET) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* in = reinterpret_cast<const sockaddr_in*>(addr);
|
||||
inet_ntop(AF_INET, &in->sin_addr, dst, static_cast<socklen_t>(dst_size));
|
||||
#else
|
||||
(void)dst;
|
||||
(void)dst_size;
|
||||
(void)addr;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void CopyIpv4Netmask(char* dst, size_t dst_size, uint8_t prefix_len) {
|
||||
#if defined(_WIN32)
|
||||
static void CopyIpv4Netmask(char* dst, size_t dst_size, uint8_t prefix_len) {
|
||||
if (dst == nullptr || dst_size == 0 || prefix_len > 32) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -3487,26 +3483,22 @@ static void CopyIpv4Netmask(char* dst, size_t dst_size, uint8_t prefix_len) {
|
|||
in_addr in {};
|
||||
in.S_un.S_addr = htonl(mask);
|
||||
inet_ntop(AF_INET, &in, dst, static_cast<socklen_t>(dst_size));
|
||||
#else
|
||||
(void)dst;
|
||||
(void)dst_size;
|
||||
(void)prefix_len;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
static HostNetworkInfo QueryHostNetworkInfo() {
|
||||
HostNetworkInfo info;
|
||||
#if defined(_WIN32)
|
||||
ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST;
|
||||
ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_INCLUDE_GATEWAYS;
|
||||
ULONG size = 0;
|
||||
auto ret = GetAdaptersAddresses(AF_UNSPEC, flags, nullptr, nullptr, &size);
|
||||
auto ret = GetAdaptersAddresses(AF_INET, flags, nullptr, nullptr, &size);
|
||||
if (ret != ERROR_BUFFER_OVERFLOW || size == 0) {
|
||||
return info;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> buffer(size);
|
||||
auto* adapters = reinterpret_cast<IP_ADAPTER_ADDRESSES*>(buffer.data());
|
||||
ret = GetAdaptersAddresses(AF_UNSPEC, flags, nullptr, adapters, &size);
|
||||
ret = GetAdaptersAddresses(AF_INET, flags, nullptr, adapters, &size);
|
||||
if (ret != NO_ERROR) {
|
||||
return info;
|
||||
}
|
||||
|
|
@ -3557,40 +3549,76 @@ static HostNetworkInfo QueryHostNetworkInfo() {
|
|||
}
|
||||
return info;
|
||||
}
|
||||
} else if (addr->sa_family == AF_INET6) {
|
||||
const auto* in6 = reinterpret_cast<const sockaddr_in6*>(addr);
|
||||
if (!IN6_IS_ADDR_LOOPBACK(&in6->sin6_addr) &&
|
||||
!IN6_IS_ADDR_UNSPECIFIED(&in6->sin6_addr)) {
|
||||
info.connected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
#else
|
||||
info.connected = true;
|
||||
ifaddrs* addresses = nullptr;
|
||||
if (getifaddrs(&addresses) != 0) {
|
||||
return info;
|
||||
}
|
||||
const auto usable = [](const ifaddrs* address) {
|
||||
return address->ifa_addr != nullptr && address->ifa_addr->sa_family == AF_INET &&
|
||||
(address->ifa_flags & (IFF_UP | IFF_RUNNING | IFF_LOOPBACK)) ==
|
||||
(IFF_UP | IFF_RUNNING) &&
|
||||
reinterpret_cast<const sockaddr_in*>(address->ifa_addr)->sin_addr.s_addr != 0;
|
||||
};
|
||||
const ifaddrs* selected = nullptr;
|
||||
for (auto* address = addresses; address != nullptr; address = address->ifa_next) {
|
||||
if (usable(address)) {
|
||||
selected = address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::ifstream routes("/proc/net/route");
|
||||
std::string line;
|
||||
uint32_t best_metric = std::numeric_limits<uint32_t>::max();
|
||||
uint32_t gateway = 0;
|
||||
bool has_default_route = false;
|
||||
while (std::getline(routes, line)) {
|
||||
std::istringstream fields(line);
|
||||
std::string name;
|
||||
uint32_t destination = 0, candidate_gateway = 0, flags = 0, refs = 0, uses = 0;
|
||||
uint32_t metric = 0, mask = 0;
|
||||
if (!(fields >> name >> std::hex >> destination >> candidate_gateway >> flags >>
|
||||
std::dec >> refs >> uses >> metric >> std::hex >> mask) ||
|
||||
destination != 0 || mask != 0 || (flags & (RTF_UP | RTF_REJECT)) != RTF_UP ||
|
||||
(has_default_route && metric >= best_metric)) {
|
||||
continue;
|
||||
}
|
||||
for (auto* address = addresses; address != nullptr; address = address->ifa_next) {
|
||||
if (usable(address) && name == address->ifa_name) {
|
||||
selected = address;
|
||||
gateway = candidate_gateway;
|
||||
best_metric = metric;
|
||||
has_default_route = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (selected != nullptr) {
|
||||
CopyIpv4String(info.ip_address, sizeof(info.ip_address), selected->ifa_addr);
|
||||
CopyIpv4String(info.netmask, sizeof(info.netmask), selected->ifa_netmask);
|
||||
info.connected = info.ip_address[0] != '\0';
|
||||
if (has_default_route) {
|
||||
in_addr address {gateway};
|
||||
inet_ntop(AF_INET, &address, info.default_route, sizeof(info.default_route));
|
||||
}
|
||||
}
|
||||
freeifaddrs(addresses);
|
||||
return info;
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool NetCtlConnected() {
|
||||
if (!g_net_ctl_status_initialized.load()) {
|
||||
g_net_ctl_connected = false;
|
||||
g_net_ctl_status_initialized = true;
|
||||
LOGF("\t host network connected = false (forced offline)\n");
|
||||
}
|
||||
|
||||
return g_net_ctl_connected.load();
|
||||
return Config::IsNetworkConnected() && QueryHostNetworkInfo().connected;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI NetCtlInit() {
|
||||
PRINT_NAME();
|
||||
|
||||
g_net_ctl_connected = false;
|
||||
g_net_ctl_status_initialized = true;
|
||||
LOGF("\t host network connected = false (forced offline)\n");
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
|
|
@ -3651,7 +3679,7 @@ int KYTY_SYSV_ABI NetCtlGetStateV6(int* state) {
|
|||
return NET_CTL_ERROR_INVALID_ADDR;
|
||||
}
|
||||
|
||||
*state = (NetCtlConnected() ? NET_CTL_STATE_IPOBTAINED : NET_CTL_STATE_DISCONNECTED);
|
||||
*state = NET_CTL_STATE_DISCONNECTED;
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
|
@ -3720,24 +3748,28 @@ int KYTY_SYSV_ABI NetCtlGetResult(int event_type, int* error_code) {
|
|||
int KYTY_SYSV_ABI NetCtlGetInfo(int code, NetCtlInfo* info) {
|
||||
PRINT_NAME();
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(info == nullptr);
|
||||
|
||||
if (info == nullptr) {
|
||||
return NET_CTL_ERROR_INVALID_ADDR;
|
||||
}
|
||||
if (code < 1 || code > 21) {
|
||||
return NET_CTL_ERROR_INVALID_CODE;
|
||||
}
|
||||
if (!Config::IsNetworkConnected()) {
|
||||
return NET_CTL_ERROR_NOT_CONNECTED;
|
||||
}
|
||||
const auto host = QueryHostNetworkInfo();
|
||||
if (!host.connected || (code == 16 && host.default_route[0] == '\0')) {
|
||||
return NET_CTL_ERROR_NOT_CONNECTED;
|
||||
}
|
||||
memset(info, 0, sizeof(NetCtlInfo));
|
||||
|
||||
// Online/host-backed info responses are left below for testing, but disabled
|
||||
// to preserve the disconnected-console behavior.
|
||||
return NET_CTL_ERROR_NOT_CONNECTED;
|
||||
|
||||
switch (code) {
|
||||
case 1:
|
||||
info->device =
|
||||
(QueryHostNetworkInfo().wireless ? NET_CTL_DEVICE_WIRELESS : NET_CTL_DEVICE_WIRED);
|
||||
info->device = (host.wireless ? NET_CTL_DEVICE_WIRELESS : NET_CTL_DEVICE_WIRED);
|
||||
break;
|
||||
case 2: info->ether_addr = QueryHostNetworkInfo().ether_addr; break;
|
||||
case 2: info->ether_addr = host.ether_addr; break;
|
||||
case 3: info->mtu = 1500; break;
|
||||
case 4:
|
||||
info->link = (NetCtlConnected() ? NET_CTL_LINK_CONNECTED : NET_CTL_LINK_DISCONNECTED);
|
||||
break;
|
||||
case 4: info->link = NET_CTL_LINK_CONNECTED; break;
|
||||
case 5: break;
|
||||
case 6: break;
|
||||
case 7: info->wifi_security = 0; break;
|
||||
|
|
@ -3748,28 +3780,25 @@ int KYTY_SYSV_ABI NetCtlGetInfo(int code, NetCtlInfo* info) {
|
|||
case 12: break;
|
||||
case 13: break;
|
||||
case 14:
|
||||
std::strncpy(info->ip_address, QueryHostNetworkInfo().ip_address,
|
||||
std::strncpy(info->ip_address, host.ip_address,
|
||||
sizeof(info->ip_address) - 1);
|
||||
break;
|
||||
case 15:
|
||||
std::strncpy(info->netmask, QueryHostNetworkInfo().netmask, sizeof(info->netmask) - 1);
|
||||
std::strncpy(info->netmask, host.netmask, sizeof(info->netmask) - 1);
|
||||
break;
|
||||
case 16:
|
||||
std::strncpy(info->default_route, QueryHostNetworkInfo().default_route,
|
||||
std::strncpy(info->default_route, host.default_route,
|
||||
sizeof(info->default_route) - 1);
|
||||
break;
|
||||
case 17:
|
||||
std::strncpy(info->primary_dns, QueryHostNetworkInfo().primary_dns,
|
||||
sizeof(info->primary_dns) - 1);
|
||||
std::strncpy(info->primary_dns, host.primary_dns, sizeof(info->primary_dns) - 1);
|
||||
break;
|
||||
case 18:
|
||||
std::strncpy(info->secondary_dns, QueryHostNetworkInfo().secondary_dns,
|
||||
sizeof(info->secondary_dns) - 1);
|
||||
std::strncpy(info->secondary_dns, host.secondary_dns, sizeof(info->secondary_dns) - 1);
|
||||
break;
|
||||
case 19: info->http_proxy_config = NET_CTL_HTTP_PROXY_OFF; break;
|
||||
case 20: break;
|
||||
case 21: info->http_proxy_port = 0; break;
|
||||
default: LOGF("\t unknown NetCtl info code: %d\n", code); return NET_CTL_ERROR_INVALID_CODE;
|
||||
}
|
||||
|
||||
return OK;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ LIB_NAME("Ngs2", "Ngs2");
|
|||
|
||||
constexpr int32_t NGS2_ERROR_INVALID_OUT_ADDRESS =
|
||||
static_cast<int32_t>(0x804a8010u);
|
||||
constexpr int32_t NGS2_ERROR_INVALID_NUM_WAVEFORM_BLOCKS =
|
||||
static_cast<int32_t>(0x804a8053u);
|
||||
constexpr int32_t NGS2_ERROR_FULL_WAVEFORM_BLOCKS =
|
||||
static_cast<int32_t>(0x804a8054u);
|
||||
constexpr int32_t NGS2_ERROR_INVALID_WAVEFORM_DATA =
|
||||
static_cast<int32_t>(0x804a8430u);
|
||||
constexpr int32_t NGS2_ERROR_INVALID_WAVEFORM_FORMAT =
|
||||
|
|
@ -549,6 +553,7 @@ struct Ngs2VoiceInternal {
|
|||
uint64_t sample_step = 0;
|
||||
uint64_t decoded_samples = 0;
|
||||
uint64_t decoded_bytes = 0;
|
||||
uint64_t source_generation = 0;
|
||||
uint32_t duration_remaining = 0;
|
||||
uint32_t skip_remaining = 0;
|
||||
const uint8_t* waveform_end = nullptr;
|
||||
|
|
@ -574,6 +579,19 @@ struct Ngs2VoiceInternal {
|
|||
filters.clear();
|
||||
}
|
||||
|
||||
void ClearWaveformBlocks() {
|
||||
++source_generation;
|
||||
blocks.clear();
|
||||
sample_phase = 0;
|
||||
duration_remaining = 0;
|
||||
skip_remaining = 0;
|
||||
compressed_input.clear();
|
||||
frame_cursor = 0;
|
||||
frame_samples = 0;
|
||||
next_frame_samples = 0;
|
||||
waveform_end = nullptr;
|
||||
}
|
||||
|
||||
void SetupSampler(const Ngs2WaveformFormat& format) {
|
||||
const bool deferred = format.waveform_type == 0 && format.num_channels == 0 &&
|
||||
format.sample_rate == 0 && format.config_data == 0 &&
|
||||
|
|
@ -582,23 +600,15 @@ struct Ngs2VoiceInternal {
|
|||
(format.frame_margin != 0 || format.frame_offset != 0 ||
|
||||
format.sample_rate == 0 || format.num_channels == 0));
|
||||
ResetSetupState();
|
||||
channels = format.num_channels;
|
||||
sample_rate = format.sample_rate;
|
||||
sample_phase = 0;
|
||||
sample_step = uint64_t(sample_rate) << 32u;
|
||||
decoded_samples = 0;
|
||||
decoded_bytes = 0;
|
||||
duration_remaining = 0;
|
||||
skip_remaining = 0;
|
||||
waveform_end = nullptr;
|
||||
blocks.clear();
|
||||
ClearWaveformBlocks();
|
||||
channels = format.num_channels;
|
||||
sample_rate = format.sample_rate;
|
||||
sample_step = uint64_t(sample_rate) << 32u;
|
||||
decoded_samples = 0;
|
||||
decoded_bytes = 0;
|
||||
decoder.reset();
|
||||
decoded_frame.clear();
|
||||
next_decoded_frame.clear();
|
||||
compressed_input.clear();
|
||||
frame_cursor = 0;
|
||||
frame_samples = 0;
|
||||
next_frame_samples = 0;
|
||||
accepts_blocks = true;
|
||||
if (deferred) {
|
||||
return;
|
||||
|
|
@ -625,6 +635,63 @@ struct Ngs2VoiceInternal {
|
|||
}
|
||||
}
|
||||
|
||||
int AddWaveformBlocks(const uint8_t* data, uint32_t flags, uint32_t count,
|
||||
const Ngs2WaveformBlock* waveform_blocks) {
|
||||
const auto capacity = rack->type == Ngs2RackType::Sampler
|
||||
? rack->option.sampler.max_waveform_blocks
|
||||
: rack->option.custom_sampler.max_waveform_blocks;
|
||||
if (count > capacity) {
|
||||
return NGS2_ERROR_INVALID_NUM_WAVEFORM_BLOCKS;
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(!accepts_blocks || (flags & ~0x17u) != 0);
|
||||
const bool clear = (flags & 4u) != 0;
|
||||
const bool only_data = (flags & 2u) != 0;
|
||||
const bool join = only_data && (flags & 0x10u) == 0;
|
||||
EXIT_NOT_IMPLEMENTED(only_data && decoder == nullptr);
|
||||
const auto retained = clear ? 0 : blocks.size();
|
||||
Block tail = retained == 0 ? Block {} : blocks.back();
|
||||
std::vector<Block> pending;
|
||||
pending.reserve(count);
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
auto info = waveform_blocks[i];
|
||||
if (!only_data && info.num_samples == 0 && info.data_size == 0) {
|
||||
continue;
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(!only_data && info.num_samples == 0);
|
||||
if (only_data) {
|
||||
info.num_repeats = 0;
|
||||
info.num_skip_samples = 0;
|
||||
info.num_samples = 0;
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(info.num_repeats != 0 && rack->type != Ngs2RackType::Sampler);
|
||||
EXIT_NOT_IMPLEMENTED(decoder == nullptr &&
|
||||
(uint64_t(info.num_skip_samples) + info.num_samples) *
|
||||
channels * sizeof(int16_t) >
|
||||
info.data_size);
|
||||
Block block {data + info.data_offset, info};
|
||||
auto* previous = pending.empty() ? (retained == 0 ? nullptr : &tail)
|
||||
: &pending.back();
|
||||
if (join && previous != nullptr &&
|
||||
previous->data + previous->info.data_size == block.data) {
|
||||
previous->info.data_size += block.info.data_size;
|
||||
} else {
|
||||
pending.push_back(block);
|
||||
}
|
||||
}
|
||||
// Validate the complete addition before clearing, extending, or closing the stream.
|
||||
if (retained + pending.size() > capacity) {
|
||||
return NGS2_ERROR_FULL_WAVEFORM_BLOCKS;
|
||||
}
|
||||
if (clear) {
|
||||
ClearWaveformBlocks();
|
||||
} else if (retained != 0) {
|
||||
blocks.back().info.data_size = tail.info.data_size;
|
||||
}
|
||||
blocks.insert(blocks.end(), pending.begin(), pending.end());
|
||||
accepts_blocks = (flags & 1u) != 0;
|
||||
return OK;
|
||||
}
|
||||
|
||||
void SetMatrix(uint32_t index, const float* levels, uint32_t count) {
|
||||
EXIT_NOT_IMPLEMENTED(index >= matrices.size());
|
||||
matrices[index].assign(levels, levels + count);
|
||||
|
|
@ -1425,9 +1492,10 @@ static_assert(offsetof(Ngs2VoiceCallbackInfo, size) == 40 &&
|
|||
offsetof(Ngs2VoiceCallbackInfo, repeats) == 44 &&
|
||||
offsetof(Ngs2VoiceCallbackInfo, attributes) == 48);
|
||||
|
||||
static void Ngs2FinishBlock(Ngs2VoiceInternal& voice) {
|
||||
auto& block = voice.blocks.front();
|
||||
const bool repeat = block.info.num_repeats != 0;
|
||||
static bool Ngs2FinishBlock(Ngs2VoiceInternal& voice) {
|
||||
const auto generation = voice.source_generation;
|
||||
auto& block = voice.blocks.front();
|
||||
const bool repeat = block.info.num_repeats != 0;
|
||||
voice.waveform_end = voice.WaveformData();
|
||||
if (repeat) {
|
||||
if (block.info.num_repeats != UINT32_MAX) {
|
||||
|
|
@ -1458,32 +1526,41 @@ static void Ngs2FinishBlock(Ngs2VoiceInternal& voice) {
|
|||
reinterpret_cast<void KYTY_SYSV_ABI (*)(const Ngs2VoiceCallbackInfo*)>(voice.callback)(
|
||||
&info);
|
||||
}
|
||||
return voice.source_generation == generation;
|
||||
}
|
||||
|
||||
static void Ngs2AdvancePcm(Ngs2VoiceInternal& voice) {
|
||||
const auto output_rate = uint64_t(voice.rack->ngs->option.sample_rate) << 32u;
|
||||
// Completion callbacks may interrupt advancement across multiple blocks.
|
||||
static bool Ngs2DecodePcmFrame(Ngs2VoiceInternal& voice, std::vector<float>& frame,
|
||||
uint32_t& frame_samples) {
|
||||
// PCM has no encoded frame size. Use one system grain as a bounded staging
|
||||
// quantum, keeping source decoding separate from resampled output consumption.
|
||||
const auto quantum = std::max(voice.rack->ngs->option.num_grain_samples, 1u);
|
||||
frame.resize(size_t(quantum) * voice.channels);
|
||||
frame_samples = 0;
|
||||
while (voice.state == Ngs2VoicePlayState::Playing && !voice.blocks.empty() &&
|
||||
voice.sample_phase >= output_rate) {
|
||||
auto& block = voice.blocks.front();
|
||||
const auto available = block.info.num_samples - block.cursor;
|
||||
const auto advance = std::min(voice.sample_phase / output_rate,
|
||||
uint64_t(available));
|
||||
block.cursor += static_cast<uint32_t>(advance);
|
||||
voice.decoded_samples += advance;
|
||||
voice.decoded_bytes += advance * voice.channels * sizeof(int16_t);
|
||||
voice.sample_phase -= advance * output_rate;
|
||||
if (block.cursor == block.info.num_samples) {
|
||||
Ngs2FinishBlock(voice);
|
||||
frame_samples < quantum) {
|
||||
auto& block = voice.blocks.front();
|
||||
const auto count = std::min(quantum - frame_samples, block.info.num_samples - block.cursor);
|
||||
const auto* pcm = reinterpret_cast<const int16_t*>(voice.WaveformData());
|
||||
for (size_t i = 0; i < size_t(count) * voice.channels; ++i) {
|
||||
frame[size_t(frame_samples) * voice.channels + i] = pcm[i] / 32768.0f;
|
||||
}
|
||||
block.cursor += count;
|
||||
frame_samples += count;
|
||||
voice.decoded_samples += count;
|
||||
voice.decoded_bytes += size_t(count) * voice.channels * sizeof(int16_t);
|
||||
if (block.cursor == block.info.num_samples && !Ngs2FinishBlock(voice)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (voice.blocks.empty() && !voice.accepts_blocks) {
|
||||
voice.sample_phase = 0;
|
||||
}
|
||||
return frame_samples != 0;
|
||||
}
|
||||
|
||||
static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector<float>& frame,
|
||||
uint32_t& frame_samples) {
|
||||
if (voice.decoder == nullptr) {
|
||||
return Ngs2DecodePcmFrame(voice, frame, frame_samples);
|
||||
}
|
||||
frame_samples = 0;
|
||||
while (voice.state == Ngs2VoicePlayState::Playing && !voice.blocks.empty()) {
|
||||
auto& block = voice.blocks.front();
|
||||
if (block.info.num_samples != 0 && block.data_cursor == 0) {
|
||||
|
|
@ -1491,7 +1568,9 @@ static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector<float>& frame,
|
|||
voice.skip_remaining = block.info.num_skip_samples;
|
||||
}
|
||||
if (voice.duration_remaining == 0) {
|
||||
Ngs2FinishBlock(voice);
|
||||
if (!Ngs2FinishBlock(voice)) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const auto skip =
|
||||
|
|
@ -1512,7 +1591,9 @@ static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector<float>& frame,
|
|||
block.data_cursor += bytes;
|
||||
voice.decoded_bytes += bytes;
|
||||
if (voice.compressed_input.size() < info.next_frame_size) {
|
||||
Ngs2FinishBlock(voice);
|
||||
if (!Ngs2FinishBlock(voice)) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
@ -1529,7 +1610,9 @@ static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector<float>& frame,
|
|||
block.data + block.info.data_size);
|
||||
voice.decoded_bytes += block.info.data_size - block.data_cursor;
|
||||
block.data_cursor = block.info.data_size;
|
||||
Ngs2FinishBlock(voice);
|
||||
if (!Ngs2FinishBlock(voice)) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(result.result != OK || result.input_consumed == 0);
|
||||
|
|
@ -1547,13 +1630,16 @@ static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector<float>& frame,
|
|||
EXIT_NOT_IMPLEMENTED(decoded_samples > voice.duration_remaining);
|
||||
frame_samples = decoded_samples;
|
||||
voice.duration_remaining -= decoded_samples;
|
||||
voice.decoded_samples += decoded_samples;
|
||||
}
|
||||
if (voice.duration_remaining == 0 ||
|
||||
(block.data_cursor == block.info.data_size && voice.compressed_input.empty())) {
|
||||
if (voice.duration_remaining == 0) {
|
||||
voice.compressed_input.clear();
|
||||
}
|
||||
Ngs2FinishBlock(voice);
|
||||
if (!Ngs2FinishBlock(voice)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (decoded_samples != 0) {
|
||||
return true;
|
||||
|
|
@ -1562,8 +1648,10 @@ static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector<float>& frame,
|
|||
return false;
|
||||
}
|
||||
|
||||
static bool Ngs2CurrentAtracFrame(Ngs2VoiceInternal& voice) {
|
||||
static bool Ngs2CurrentFrame(Ngs2VoiceInternal& voice) {
|
||||
if (voice.frame_cursor == voice.frame_samples) {
|
||||
voice.frame_cursor = 0;
|
||||
voice.frame_samples = 0;
|
||||
if (voice.next_frame_samples != 0) {
|
||||
std::swap(voice.decoded_frame, voice.next_decoded_frame);
|
||||
voice.frame_samples = voice.next_frame_samples;
|
||||
|
|
@ -1571,21 +1659,21 @@ static bool Ngs2CurrentAtracFrame(Ngs2VoiceInternal& voice) {
|
|||
} else if (!Ngs2DecodeFrame(voice, voice.decoded_frame, voice.frame_samples)) {
|
||||
return false;
|
||||
}
|
||||
voice.frame_cursor = 0;
|
||||
}
|
||||
return voice.state == Ngs2VoicePlayState::Playing &&
|
||||
voice.frame_cursor < voice.frame_samples;
|
||||
}
|
||||
|
||||
static void Ngs2AdvanceAtrac9(Ngs2VoiceInternal& voice) {
|
||||
static void Ngs2AdvanceSamples(Ngs2VoiceInternal& voice) {
|
||||
const auto output_rate = uint64_t(voice.rack->ngs->option.sample_rate) << 32u;
|
||||
while (voice.state == Ngs2VoicePlayState::Playing && voice.sample_phase >= output_rate) {
|
||||
if (!Ngs2CurrentAtracFrame(voice)) {
|
||||
if (!Ngs2CurrentFrame(voice)) {
|
||||
break;
|
||||
}
|
||||
++voice.frame_cursor;
|
||||
++voice.decoded_samples;
|
||||
voice.sample_phase -= output_rate;
|
||||
const auto advance = std::min(voice.sample_phase / output_rate,
|
||||
uint64_t(voice.frame_samples - voice.frame_cursor));
|
||||
voice.frame_cursor += static_cast<uint32_t>(advance);
|
||||
voice.sample_phase -= advance * output_rate;
|
||||
}
|
||||
if (voice.blocks.empty() && voice.frame_cursor == voice.frame_samples &&
|
||||
voice.next_frame_samples == 0 && !voice.accepts_blocks) {
|
||||
|
|
@ -1594,42 +1682,14 @@ static void Ngs2AdvanceAtrac9(Ngs2VoiceInternal& voice) {
|
|||
}
|
||||
|
||||
static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) {
|
||||
uint32_t output = 0;
|
||||
const auto generation = voice.source_generation;
|
||||
uint32_t output = 0;
|
||||
const auto output_rate = uint64_t(voice.rack->ngs->option.sample_rate) << 32u;
|
||||
while (voice.state == Ngs2VoicePlayState::Playing && output < grain &&
|
||||
(!voice.blocks.empty() || voice.frame_cursor < voice.frame_samples ||
|
||||
voice.next_frame_samples != 0)) {
|
||||
if (voice.decoder == nullptr) {
|
||||
Ngs2AdvancePcm(voice);
|
||||
if (voice.state != Ngs2VoicePlayState::Playing || voice.blocks.empty()) {
|
||||
break;
|
||||
}
|
||||
const auto& block = voice.blocks.front();
|
||||
const auto* pcm = reinterpret_cast<const int16_t*>(voice.WaveformData());
|
||||
const auto* next = pcm;
|
||||
if (block.cursor + 1 < block.info.num_samples) {
|
||||
next += voice.channels;
|
||||
} else if (block.info.num_repeats != 0) {
|
||||
next = reinterpret_cast<const int16_t*>(block.data) +
|
||||
block.info.num_skip_samples * voice.channels;
|
||||
} else if (voice.blocks.size() > 1) {
|
||||
const auto& next_block = voice.blocks[1];
|
||||
next = reinterpret_cast<const int16_t*>(next_block.data) +
|
||||
next_block.info.num_skip_samples * voice.channels;
|
||||
}
|
||||
const float fraction = static_cast<float>(voice.sample_phase) /
|
||||
static_cast<float>(output_rate);
|
||||
for (uint32_t c = 0; c < voice.channels; ++c) {
|
||||
voice.samples[c * grain + output] =
|
||||
std::lerp(static_cast<float>(pcm[c]), static_cast<float>(next[c]), fraction) /
|
||||
32768.0f;
|
||||
}
|
||||
voice.sample_phase += voice.sample_step;
|
||||
++output;
|
||||
continue;
|
||||
}
|
||||
Ngs2AdvanceAtrac9(voice);
|
||||
if (!Ngs2CurrentAtracFrame(voice)) {
|
||||
Ngs2AdvanceSamples(voice);
|
||||
if (voice.source_generation != generation || !Ngs2CurrentFrame(voice)) {
|
||||
break;
|
||||
}
|
||||
if (voice.frame_cursor + 1 == voice.frame_samples && voice.sample_phase != 0) {
|
||||
|
|
@ -1639,7 +1699,8 @@ static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) {
|
|||
}
|
||||
Ngs2DecodeFrame(voice, voice.next_decoded_frame, voice.next_frame_samples);
|
||||
}
|
||||
if (voice.state != Ngs2VoicePlayState::Playing ||
|
||||
if (voice.source_generation != generation ||
|
||||
voice.state != Ngs2VoicePlayState::Playing ||
|
||||
voice.frame_cursor >= voice.frame_samples) {
|
||||
break;
|
||||
}
|
||||
|
|
@ -1657,10 +1718,13 @@ static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) {
|
|||
voice.sample_phase += voice.sample_step;
|
||||
++output;
|
||||
}
|
||||
if (voice.decoder == nullptr) {
|
||||
Ngs2AdvancePcm(voice);
|
||||
} else {
|
||||
Ngs2AdvanceAtrac9(voice);
|
||||
if (voice.source_generation == generation) {
|
||||
Ngs2AdvanceSamples(voice);
|
||||
}
|
||||
if (voice.source_generation != generation) {
|
||||
// A callback replaced the source, possibly changing its channel layout.
|
||||
voice.samples.assign(size_t(grain) * voice.channels, 0.0f);
|
||||
output = 0;
|
||||
}
|
||||
voice.has_samples = output != 0;
|
||||
}
|
||||
|
|
@ -2370,41 +2434,10 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
|
|||
const Ngs2WaveformBlock* blocks;
|
||||
};
|
||||
const auto& blocks = *reinterpret_cast<const BlocksParam*>(param);
|
||||
EXIT_NOT_IMPLEMENTED(!voice->accepts_blocks ||
|
||||
(blocks.flags & ~0x17u) != 0);
|
||||
if ((blocks.flags & 0x4u) != 0) {
|
||||
voice->blocks.clear();
|
||||
voice->sample_phase = 0;
|
||||
voice->duration_remaining = 0;
|
||||
voice->skip_remaining = 0;
|
||||
voice->compressed_input.clear();
|
||||
voice->frame_cursor = 0;
|
||||
voice->frame_samples = 0;
|
||||
voice->next_frame_samples = 0;
|
||||
voice->waveform_end = nullptr;
|
||||
}
|
||||
voice->accepts_blocks = (blocks.flags & 1u) != 0;
|
||||
const bool only_data = (blocks.flags & 2u) != 0;
|
||||
EXIT_NOT_IMPLEMENTED(only_data && voice->decoder == nullptr);
|
||||
for (uint32_t i = 0; i < blocks.count; ++i) {
|
||||
auto block = blocks.blocks[i];
|
||||
if (!only_data && block.num_samples == 0 && block.data_size == 0) {
|
||||
continue;
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(!only_data && block.num_samples == 0);
|
||||
if (only_data) {
|
||||
block.num_repeats = 0;
|
||||
block.num_skip_samples = 0;
|
||||
block.num_samples = 0;
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(block.num_repeats != 0 &&
|
||||
voice->rack->type != Ngs2RackType::Sampler);
|
||||
EXIT_NOT_IMPLEMENTED(
|
||||
voice->decoder == nullptr &&
|
||||
(uint64_t(block.num_skip_samples) + block.num_samples) *
|
||||
voice->channels * 2 >
|
||||
block.data_size);
|
||||
voice->blocks.push_back({blocks.data + block.data_offset, block});
|
||||
const auto result = voice->AddWaveformBlocks(
|
||||
blocks.data, blocks.flags, blocks.count, blocks.blocks);
|
||||
if (result != OK) {
|
||||
return result;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -2511,6 +2544,15 @@ int KYTY_SYSV_ABI Ngs2VoiceRunCommands(uintptr_t voice_handle, const void* comma
|
|||
return OK;
|
||||
}
|
||||
|
||||
template <typename State>
|
||||
static void Ngs2FillSamplerState(const Ngs2VoiceInternal& voice, State& state) {
|
||||
state.voice_state.state_flags = Ngs2GetStateFlags(&voice);
|
||||
state.num_decoded_samples = voice.decoded_samples;
|
||||
state.decoded_data_size = voice.decoded_bytes;
|
||||
state.waveform_data = voice.WaveformData();
|
||||
state.user_data = voice.blocks.empty() ? 0 : voice.blocks.front().info.user_data;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI Ngs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* state,
|
||||
size_t state_size) {
|
||||
PRINT_NAME();
|
||||
|
|
@ -2547,7 +2589,7 @@ int KYTY_SYSV_ABI Ngs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* stat
|
|||
EXIT_NOT_IMPLEMENTED(state_size != configured_size);
|
||||
std::memset(state, 0, state_size);
|
||||
auto* sampler = reinterpret_cast<Ngs2CustomSamplerVoiceState*>(state);
|
||||
sampler->voice_state.state_flags = Ngs2GetStateFlags(voice);
|
||||
Ngs2FillSamplerState(*voice, *sampler);
|
||||
break;
|
||||
}
|
||||
case Ngs2RackType::Sampler: {
|
||||
|
|
@ -2568,12 +2610,7 @@ int KYTY_SYSV_ABI Ngs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* stat
|
|||
sampler->envelope_height = 1.0f;
|
||||
sampler->peak_height = 0.0f;
|
||||
sampler->reserved = 0;
|
||||
sampler->num_decoded_samples = voice->decoded_samples;
|
||||
sampler->decoded_data_size = voice->decoded_bytes;
|
||||
sampler->waveform_data = voice->WaveformData();
|
||||
if (!voice->blocks.empty()) {
|
||||
sampler->user_data = voice->blocks.front().info.user_data;
|
||||
}
|
||||
Ngs2FillSamplerState(*voice, *sampler);
|
||||
break;
|
||||
}
|
||||
default: EXIT("unknown type: %s\n", magic_enum::enum_name(voice->rack->type));
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
|
@ -77,6 +78,7 @@ public:
|
|||
explicit Instance(const Config& config): m_config(config) {}
|
||||
|
||||
~Instance() {
|
||||
ClearFrames();
|
||||
ClearPictureMetadata();
|
||||
if (m_sws != nullptr) {
|
||||
sws_freeContext(m_sws);
|
||||
|
|
@ -119,116 +121,138 @@ public:
|
|||
|
||||
[[nodiscard]] uint32_t CodecType() const { return m_config.codec_type; }
|
||||
|
||||
[[nodiscard]] Result Submit(const Input& input, uint32_t* produced) {
|
||||
std::scoped_lock lock(m_mutex);
|
||||
return SubmitInput(input, produced);
|
||||
}
|
||||
|
||||
[[nodiscard]] Result Receive(const FrameBuffer& frame_buffer, Output* output) {
|
||||
std::scoped_lock lock(m_mutex);
|
||||
return ReceiveOutput(frame_buffer, output);
|
||||
}
|
||||
|
||||
[[nodiscard]] Result Finalize() {
|
||||
std::scoped_lock lock(m_mutex);
|
||||
if (m_finalized) return Result::Ok;
|
||||
const auto drained = DrainFrames();
|
||||
if (drained != Result::Ok) return drained;
|
||||
const int result = avcodec_send_packet(m_codec, nullptr);
|
||||
if (result == AVERROR(EAGAIN)) return Result::InputFull;
|
||||
if (result < 0 && result != AVERROR_EOF) return Result::ApiFail;
|
||||
m_finalized = true;
|
||||
return DrainFrames();
|
||||
}
|
||||
|
||||
[[nodiscard]] Result DecodeInput(const Input& input, const FrameBuffer& frame_buffer,
|
||||
Output* output) {
|
||||
std::scoped_lock lock(m_mutex);
|
||||
*output = {};
|
||||
|
||||
AVPacket* packet = av_packet_alloc();
|
||||
AVFrame* frame = av_frame_alloc();
|
||||
if (packet == nullptr || frame == nullptr ||
|
||||
input.size > static_cast<size_t>(std::numeric_limits<int>::max())) {
|
||||
av_packet_free(&packet);
|
||||
av_frame_free(&frame);
|
||||
return Result::ApiFail;
|
||||
uint32_t produced = 0;
|
||||
auto result = SubmitInput(input, &produced);
|
||||
if (result == Result::InputFull) {
|
||||
// The combined API supplies an output buffer together with each AU.
|
||||
result = ReceiveOutput(frame_buffer, output);
|
||||
if (result != Result::Ok) return result;
|
||||
return SubmitInput(input, &produced);
|
||||
}
|
||||
|
||||
int result = av_new_packet(packet, static_cast<int>(input.size));
|
||||
if (result < 0) {
|
||||
LOGF("Videodec2: av_new_packet failed: %s (%d)\n", AvErrorString(result), result);
|
||||
av_packet_free(&packet);
|
||||
av_frame_free(&frame);
|
||||
return Result::ApiFail;
|
||||
}
|
||||
std::memcpy(packet->data, input.data, input.size);
|
||||
packet->pts = ToAvTimestamp(input.pts);
|
||||
packet->dts = ToAvTimestamp(input.dts);
|
||||
|
||||
packet->opaque_ref = av_buffer_alloc(sizeof(PacketMetadata));
|
||||
if (packet->opaque_ref == nullptr) {
|
||||
av_packet_free(&packet);
|
||||
av_frame_free(&frame);
|
||||
return Result::ApiFail;
|
||||
}
|
||||
const PacketMetadata metadata {input.pts, input.dts, input.attached_data};
|
||||
std::memcpy(packet->opaque_ref->data, &metadata, sizeof(metadata));
|
||||
|
||||
bool have_pending_frame = false;
|
||||
result = avcodec_send_packet(m_codec, packet);
|
||||
if (result == AVERROR(EAGAIN)) {
|
||||
result = avcodec_receive_frame(m_codec, frame);
|
||||
if (result < 0) {
|
||||
LOGF("Videodec2: decoder rejected an AU while no output was available: %s (%d)\n",
|
||||
AvErrorString(result), result);
|
||||
av_packet_free(&packet);
|
||||
av_frame_free(&frame);
|
||||
return Result::AccessUnit;
|
||||
}
|
||||
have_pending_frame = true;
|
||||
result = avcodec_send_packet(m_codec, packet);
|
||||
}
|
||||
if (result < 0) {
|
||||
LOGF("Videodec2: avcodec_send_packet failed: %s (%d)\n", AvErrorString(result), result);
|
||||
av_packet_free(&packet);
|
||||
av_frame_free(&frame);
|
||||
return Result::AccessUnit;
|
||||
}
|
||||
|
||||
Result decode_result = Result::Ok;
|
||||
if (!have_pending_frame) {
|
||||
result = avcodec_receive_frame(m_codec, frame);
|
||||
if (result != AVERROR(EAGAIN) && result != AVERROR_EOF) {
|
||||
if (result < 0) {
|
||||
LOGF("Videodec2: avcodec_receive_frame failed: %s (%d)\n",
|
||||
AvErrorString(result), result);
|
||||
decode_result = Result::AccessUnit;
|
||||
} else {
|
||||
decode_result = CopyFrame(frame, frame_buffer, output);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
decode_result = CopyFrame(frame, frame_buffer, output);
|
||||
}
|
||||
|
||||
av_packet_free(&packet);
|
||||
av_frame_free(&frame);
|
||||
return decode_result;
|
||||
if (result != Result::Ok) return result;
|
||||
result = ReceiveOutput(frame_buffer, output);
|
||||
return result == Result::NoFrame ? Result::Ok : result;
|
||||
}
|
||||
|
||||
[[nodiscard]] Result FlushOutput(const FrameBuffer& frame_buffer, Output* output) {
|
||||
std::scoped_lock lock(m_mutex);
|
||||
*output = {};
|
||||
|
||||
AVFrame* frame = av_frame_alloc();
|
||||
if (frame == nullptr) {
|
||||
return Result::ApiFail;
|
||||
}
|
||||
|
||||
// Guest Flush collects available pictures between AUs while retaining reference frames.
|
||||
const int receive_result = avcodec_receive_frame(m_codec, frame);
|
||||
if (receive_result == AVERROR(EAGAIN) || receive_result == AVERROR_EOF) {
|
||||
av_frame_free(&frame);
|
||||
return Result::Ok;
|
||||
}
|
||||
if (receive_result < 0) {
|
||||
LOGF("Videodec2: receiving a flushed frame failed: %s (%d)\n",
|
||||
AvErrorString(receive_result), receive_result);
|
||||
av_frame_free(&frame);
|
||||
return Result::ApiFail;
|
||||
}
|
||||
|
||||
const auto result = CopyFrame(frame, frame_buffer, output);
|
||||
av_frame_free(&frame);
|
||||
return result;
|
||||
const auto result = ReceiveOutput(frame_buffer, output);
|
||||
return result == Result::NoFrame ? Result::Ok : result;
|
||||
}
|
||||
|
||||
void ResetDecoder() {
|
||||
std::scoped_lock lock(m_mutex);
|
||||
avcodec_flush_buffers(m_codec);
|
||||
ClearFrames();
|
||||
ClearPictureMetadata();
|
||||
m_finalized = m_eof = false;
|
||||
}
|
||||
|
||||
private:
|
||||
struct PendingFrame {
|
||||
AVFrame* frame;
|
||||
PictureInfo picture;
|
||||
};
|
||||
|
||||
void ClearFrames() {
|
||||
for (auto& pending: m_frames) av_frame_free(&pending.frame);
|
||||
m_frames.clear();
|
||||
}
|
||||
|
||||
[[nodiscard]] Result DrainFrames(uint32_t* produced = nullptr) {
|
||||
while (!m_eof && m_frames.size() < m_config.max_pending_frames) {
|
||||
AVFrame* frame = av_frame_alloc();
|
||||
if (frame == nullptr) return Result::ApiFail;
|
||||
const int result = avcodec_receive_frame(m_codec, frame);
|
||||
if (result < 0) {
|
||||
av_frame_free(&frame);
|
||||
if (result == AVERROR_EOF) m_eof = true;
|
||||
if (result == AVERROR(EAGAIN) || m_eof) return Result::Ok;
|
||||
LOGF("Video decoder: avcodec_receive_frame failed: %s (%d)\n",
|
||||
AvErrorString(result), result);
|
||||
return Result::AccessUnit;
|
||||
}
|
||||
m_frames.push_back({frame, MakePictureInfo(frame)});
|
||||
if (produced != nullptr) ++*produced;
|
||||
}
|
||||
return Result::Ok;
|
||||
}
|
||||
|
||||
[[nodiscard]] Result SubmitInput(const Input& input, uint32_t* produced) {
|
||||
*produced = 0;
|
||||
if (m_finalized) return Result::ApiFail;
|
||||
const auto drained = DrainFrames();
|
||||
if (drained != Result::Ok) return drained;
|
||||
if (m_frames.size() >= m_config.max_pending_frames) return Result::InputFull;
|
||||
if (input.size > static_cast<size_t>(std::numeric_limits<int>::max())) return Result::ApiFail;
|
||||
AVPacket* packet = av_packet_alloc();
|
||||
if (packet == nullptr) return Result::ApiFail;
|
||||
int result = av_new_packet(packet, static_cast<int>(input.size));
|
||||
if (result < 0) {
|
||||
av_packet_free(&packet);
|
||||
return Result::ApiFail;
|
||||
}
|
||||
std::memcpy(packet->data, input.data, input.size);
|
||||
packet->pts = ToAvTimestamp(input.pts);
|
||||
packet->dts = ToAvTimestamp(input.dts);
|
||||
packet->opaque_ref = av_buffer_alloc(sizeof(PacketMetadata));
|
||||
if (packet->opaque_ref == nullptr) {
|
||||
av_packet_free(&packet);
|
||||
return Result::ApiFail;
|
||||
}
|
||||
const PacketMetadata metadata {input.pts, input.dts, input.attached_data};
|
||||
std::memcpy(packet->opaque_ref->data, &metadata, sizeof(metadata));
|
||||
result = avcodec_send_packet(m_codec, packet);
|
||||
av_packet_free(&packet);
|
||||
if (result == AVERROR(EAGAIN)) return Result::InputFull; // The AU was not accepted.
|
||||
if (result < 0) {
|
||||
LOGF("Video decoder: submitting an AU failed: %s (%d)\n", AvErrorString(result), result);
|
||||
return Result::AccessUnit;
|
||||
}
|
||||
// Once accepted, a full output queue cannot turn this AU into a retry.
|
||||
return DrainFrames(produced);
|
||||
}
|
||||
|
||||
[[nodiscard]] Result ReceiveOutput(const FrameBuffer& frame_buffer, Output* output) {
|
||||
*output = {};
|
||||
const auto drained = DrainFrames();
|
||||
if (drained != Result::Ok) return drained;
|
||||
if (m_frames.empty()) return Result::NoFrame;
|
||||
auto& pending = m_frames.front();
|
||||
const auto result = CopyFrame(pending.frame, pending.picture, frame_buffer, output);
|
||||
if (result != Result::Ok) return result;
|
||||
output->last_frame = m_finalized && m_eof && m_frames.size() == 1u;
|
||||
av_frame_free(&pending.frame);
|
||||
m_frames.pop_front();
|
||||
return Result::Ok;
|
||||
}
|
||||
|
||||
[[nodiscard]] PictureInfo MakePictureInfo(const AVFrame* frame) const {
|
||||
PictureInfo result {};
|
||||
if (frame->opaque_ref != nullptr && frame->opaque_ref->size >= sizeof(PacketMetadata)) {
|
||||
|
|
@ -268,8 +292,8 @@ private:
|
|||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] Result CopyFrame(const AVFrame* frame, const FrameBuffer& frame_buffer,
|
||||
Output* output) {
|
||||
[[nodiscard]] Result CopyFrame(const AVFrame* frame, const PictureInfo& picture,
|
||||
const FrameBuffer& frame_buffer, Output* output) {
|
||||
if (frame->width <= 0 || frame->height <= 0) {
|
||||
return Result::ApiFail;
|
||||
}
|
||||
|
|
@ -329,7 +353,7 @@ private:
|
|||
|
||||
{
|
||||
std::scoped_lock lock(g_picture_mutex);
|
||||
g_picture_infos[frame_buffer.data] = {this, MakePictureInfo(frame)};
|
||||
g_picture_infos[frame_buffer.data] = {this, picture};
|
||||
m_picture_buffers.insert(frame_buffer.data);
|
||||
}
|
||||
return Result::Ok;
|
||||
|
|
@ -346,6 +370,9 @@ private:
|
|||
m_picture_buffers.clear();
|
||||
}
|
||||
|
||||
std::deque<PendingFrame> m_frames;
|
||||
bool m_finalized = false;
|
||||
bool m_eof = false;
|
||||
Config m_config;
|
||||
AVCodecContext* m_codec = nullptr;
|
||||
SwsContext* m_sws = nullptr;
|
||||
|
|
@ -358,7 +385,7 @@ bool IsCodecSupported(uint32_t codec_type) {
|
|||
}
|
||||
|
||||
Instance* Create(const Config& config) {
|
||||
if (!IsCodecSupported(config.codec_type)) {
|
||||
if (!IsCodecSupported(config.codec_type) || config.max_pending_frames < 2u) {
|
||||
return nullptr;
|
||||
}
|
||||
auto* instance = new Instance(config);
|
||||
|
|
@ -377,6 +404,18 @@ uint32_t GetCodecType(const Instance* instance) {
|
|||
return instance->CodecType();
|
||||
}
|
||||
|
||||
Result Submit(Instance* instance, const Input& input, uint32_t* produced) {
|
||||
return instance->Submit(input, produced);
|
||||
}
|
||||
|
||||
Result Receive(Instance* instance, const FrameBuffer& frame_buffer, Output* output) {
|
||||
return instance->Receive(frame_buffer, output);
|
||||
}
|
||||
|
||||
Result Finalize(Instance* instance) {
|
||||
return instance->Finalize();
|
||||
}
|
||||
|
||||
Result Decode(Instance* instance, const Input& input, const FrameBuffer& frame_buffer,
|
||||
Output* output) {
|
||||
return instance->DecodeInput(input, frame_buffer, output);
|
||||
|
|
@ -390,6 +429,44 @@ void Reset(Instance* instance) {
|
|||
instance->ResetDecoder();
|
||||
}
|
||||
|
||||
AvcPictureInfo MakeAvcPictureInfo(const PictureInfo& decoded, size_t size) {
|
||||
AvcPictureInfo picture {};
|
||||
picture.this_size = size;
|
||||
picture.is_valid = true;
|
||||
picture.pts_data = decoded.pts;
|
||||
picture.dts_data = decoded.dts;
|
||||
picture.attached_data = decoded.attached_data;
|
||||
picture.idr_picture_flag = decoded.key_frame ? 1 : 0;
|
||||
picture.profile_idc = static_cast<uint8_t>(decoded.profile);
|
||||
picture.level_idc = static_cast<uint8_t>(decoded.level);
|
||||
picture.pic_width_in_mbs_minus1 = (decoded.width + 15u) / 16u - 1u;
|
||||
picture.pic_height_in_map_units_minus1 = (decoded.height + 15u) / 16u - 1u;
|
||||
picture.frame_mbs_only_flag = 1;
|
||||
picture.frame_cropping_flag = decoded.crop_left != 0 || decoded.crop_right != 0 ||
|
||||
decoded.crop_top != 0 || decoded.crop_bottom != 0
|
||||
? 1
|
||||
: 0;
|
||||
picture.frame_crop_left_offset = decoded.crop_left;
|
||||
picture.frame_crop_right_offset = decoded.crop_right;
|
||||
picture.frame_crop_top_offset = decoded.crop_top;
|
||||
picture.frame_crop_bottom_offset = decoded.crop_bottom;
|
||||
picture.aspect_ratio_info_present_flag =
|
||||
decoded.sar_width != 0 && decoded.sar_height != 0 ? 1 : 0;
|
||||
picture.aspect_ratio_idc = picture.aspect_ratio_info_present_flag ? 255 : 0;
|
||||
picture.sar_width = decoded.sar_width;
|
||||
picture.sar_height = decoded.sar_height;
|
||||
picture.video_signal_type_present_flag = 1;
|
||||
picture.video_format = 5;
|
||||
picture.video_full_range_flag = decoded.color_range == 2 ? 1 : 0;
|
||||
picture.colour_description_present_flag =
|
||||
decoded.color_primaries != 0 || decoded.color_trc != 0 || decoded.color_space != 0 ? 1
|
||||
: 0;
|
||||
picture.colour_primaries = decoded.color_primaries;
|
||||
picture.transfer_characteristics = decoded.color_trc;
|
||||
picture.matrix_coefficients = decoded.color_space;
|
||||
return picture;
|
||||
}
|
||||
|
||||
bool GetPictureInfo(void* frame_buffer, PictureInfo* picture_info) {
|
||||
std::scoped_lock lock(g_picture_mutex);
|
||||
const auto it = g_picture_infos.find(frame_buffer);
|
||||
|
|
|
|||
|
|
@ -14,12 +14,15 @@ enum class Result {
|
|||
AccessUnit,
|
||||
FrameBufferSize,
|
||||
OversizeDecode,
|
||||
InputFull,
|
||||
NoFrame,
|
||||
};
|
||||
|
||||
struct Config {
|
||||
uint32_t codec_type = 0;
|
||||
int32_t max_width = -1;
|
||||
int32_t max_height = -1;
|
||||
uint32_t codec_type = 0;
|
||||
int32_t max_width = -1;
|
||||
int32_t max_height = -1;
|
||||
uint32_t max_pending_frames = 2;
|
||||
};
|
||||
|
||||
struct Input {
|
||||
|
|
@ -37,6 +40,7 @@ struct FrameBuffer {
|
|||
|
||||
struct Output {
|
||||
bool valid = false;
|
||||
bool last_frame = false;
|
||||
bool error_frame = false;
|
||||
bool buffer_accepted = false;
|
||||
uint32_t codec_type = 0;
|
||||
|
|
@ -69,12 +73,83 @@ struct PictureInfo {
|
|||
bool key_frame = false;
|
||||
};
|
||||
|
||||
struct AvcPictureInfo {
|
||||
size_t this_size;
|
||||
bool is_valid;
|
||||
|
||||
uint64_t pts_data;
|
||||
uint64_t dts_data;
|
||||
uint64_t attached_data;
|
||||
|
||||
uint8_t idr_picture_flag;
|
||||
uint8_t profile_idc;
|
||||
uint8_t level_idc;
|
||||
uint32_t pic_width_in_mbs_minus1;
|
||||
uint32_t pic_height_in_map_units_minus1;
|
||||
uint8_t frame_mbs_only_flag;
|
||||
|
||||
uint8_t frame_cropping_flag;
|
||||
uint32_t frame_crop_left_offset;
|
||||
uint32_t frame_crop_right_offset;
|
||||
uint32_t frame_crop_top_offset;
|
||||
uint32_t frame_crop_bottom_offset;
|
||||
|
||||
uint8_t aspect_ratio_info_present_flag;
|
||||
uint8_t aspect_ratio_idc;
|
||||
uint16_t sar_width;
|
||||
uint16_t sar_height;
|
||||
|
||||
uint8_t video_signal_type_present_flag;
|
||||
uint8_t video_format;
|
||||
uint8_t video_full_range_flag;
|
||||
uint8_t colour_description_present_flag;
|
||||
uint8_t colour_primaries;
|
||||
uint8_t transfer_characteristics;
|
||||
uint8_t matrix_coefficients;
|
||||
|
||||
uint8_t timing_info_present_flag;
|
||||
uint32_t num_units_in_tick;
|
||||
uint32_t time_scale;
|
||||
uint8_t fixed_frame_rate_flag;
|
||||
|
||||
uint8_t bitstream_restriction_flag;
|
||||
uint8_t max_dec_frame_buffering;
|
||||
|
||||
uint8_t pic_struct_present_flag;
|
||||
uint8_t pic_struct;
|
||||
uint8_t field_pic_flag;
|
||||
uint8_t bottom_field_flag;
|
||||
|
||||
uint8_t sequence_parameter_set_present_flag;
|
||||
uint8_t picture_parameter_set_present_flag;
|
||||
uint8_t au_delimiter_present_flag;
|
||||
uint8_t end_of_sequence_present_flag;
|
||||
uint8_t end_of_stream_present_flag;
|
||||
uint8_t filler_data_present_flag;
|
||||
uint8_t picture_timing_sei_present_flag;
|
||||
uint8_t buffering_period_sei_present_flag;
|
||||
|
||||
uint8_t constraint_set0_flag;
|
||||
uint8_t constraint_set1_flag;
|
||||
uint8_t constraint_set2_flag;
|
||||
uint8_t constraint_set3_flag;
|
||||
uint8_t constraint_set4_flag;
|
||||
uint8_t constraint_set5_flag;
|
||||
};
|
||||
|
||||
static_assert(sizeof(AvcPictureInfo) == 120);
|
||||
|
||||
[[nodiscard]] AvcPictureInfo MakeAvcPictureInfo(const PictureInfo& decoded, size_t size);
|
||||
|
||||
class Instance;
|
||||
|
||||
[[nodiscard]] bool IsCodecSupported(uint32_t codec_type);
|
||||
[[nodiscard]] Instance* Create(const Config& config);
|
||||
void Destroy(Instance* instance);
|
||||
[[nodiscard]] uint32_t GetCodecType(const Instance* instance);
|
||||
[[nodiscard]] Result Submit(Instance* instance, const Input& input, uint32_t* produced);
|
||||
[[nodiscard]] Result Receive(Instance* instance, const FrameBuffer& frame_buffer, Output* output);
|
||||
[[nodiscard]] Result Finalize(Instance* instance);
|
||||
[[nodiscard]] Result Decode(Instance* instance, const Input& input, const FrameBuffer& frame_buffer,
|
||||
Output* output);
|
||||
[[nodiscard]] Result Flush(Instance* instance, const FrameBuffer& frame_buffer, Output* output);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
#include "common/file.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "emulator.h"
|
||||
#include "kernel/fileSystem.h"
|
||||
#include "libs/errno.h"
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#elif defined(__linux__)
|
||||
#include <linux/close_range.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace Emulator {
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
static std::wstring QuoteArgument(std::wstring_view argument) {
|
||||
std::wstring quoted = L"\"";
|
||||
size_t slashes = 0;
|
||||
for (wchar_t c: argument) {
|
||||
if (c == L'\\') {
|
||||
++slashes;
|
||||
continue;
|
||||
}
|
||||
quoted.append(c == L'"' ? slashes * 2 + 1 : slashes, L'\\');
|
||||
quoted += c;
|
||||
slashes = 0;
|
||||
}
|
||||
quoted.append(slashes * 2, L'\\');
|
||||
return quoted + L'"';
|
||||
}
|
||||
#endif
|
||||
|
||||
int LoadExec(const char* path, const char* const argv[]) {
|
||||
using namespace Libs::SystemService;
|
||||
if (path == nullptr || !std::string_view(path).starts_with("/app0/") ||
|
||||
!Common::File::IsFileExisting(Libs::LibKernel::FileSystem::GetRealFilename(path))) {
|
||||
return SYSTEM_SERVICE_ERROR_PARAMETER;
|
||||
}
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS || defined(__linux__)
|
||||
std::vector<std::string> arguments {"--guest-exec", path};
|
||||
size_t bytes = 0;
|
||||
for (size_t i = 0; argv != nullptr && argv[i] != nullptr; ++i) {
|
||||
const auto size = strnlen(argv[i], 4096 - bytes) + 1;
|
||||
if (size > 4096 - bytes) {
|
||||
return SYSTEM_SERVICE_ERROR_PARAMETER;
|
||||
}
|
||||
bytes += size;
|
||||
arguments.emplace_back(argv[i], size - 1);
|
||||
}
|
||||
LOGF("LoadExec: %s\n", path);
|
||||
Log::Flush();
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
wchar_t executable[32768] {};
|
||||
const auto length = GetModuleFileNameW(nullptr, executable, 32768);
|
||||
if (length == 0 || length >= 32768) {
|
||||
return SYSTEM_SERVICE_ERROR_INTERNAL;
|
||||
}
|
||||
if (__wargv == nullptr) {
|
||||
return SYSTEM_SERVICE_ERROR_INTERNAL;
|
||||
}
|
||||
std::wstring command = QuoteArgument(executable);
|
||||
for (int i = 1; i < __argc && __wargv[i] != nullptr; ++i) {
|
||||
if (std::wstring_view(__wargv[i]) == L"--guest-exec") {
|
||||
break;
|
||||
}
|
||||
command += L" " + QuoteArgument(__wargv[i]);
|
||||
}
|
||||
for (const auto& argument: arguments) {
|
||||
const int size =
|
||||
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, argument.c_str(), -1, nullptr, 0);
|
||||
if (size == 0) {
|
||||
return SYSTEM_SERVICE_ERROR_PARAMETER;
|
||||
}
|
||||
std::wstring wide(size, L'\0');
|
||||
if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, argument.c_str(), -1, wide.data(),
|
||||
size) == 0) {
|
||||
return SYSTEM_SERVICE_ERROR_PARAMETER;
|
||||
}
|
||||
wide.resize(size - 1);
|
||||
command += L" " + QuoteArgument(wide);
|
||||
}
|
||||
STARTUPINFOW startup {};
|
||||
startup.cb = sizeof(startup);
|
||||
PROCESS_INFORMATION process {};
|
||||
if (!CreateProcessW(executable, command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW,
|
||||
nullptr, nullptr, &startup, &process)) {
|
||||
LOGF("LoadExec: process creation failed: %lu\n", GetLastError());
|
||||
return SYSTEM_SERVICE_ERROR_INTERNAL;
|
||||
}
|
||||
CloseHandle(process.hThread);
|
||||
CloseHandle(process.hProcess);
|
||||
std::fflush(nullptr);
|
||||
std::quick_exit(0);
|
||||
#else
|
||||
std::ifstream command("/proc/self/cmdline", std::ios::binary);
|
||||
std::vector<std::string> host_arguments;
|
||||
for (std::string argument;
|
||||
std::getline(command, argument, '\0') && argument != "--guest-exec";) {
|
||||
host_arguments.push_back(std::move(argument));
|
||||
}
|
||||
if (host_arguments.empty()) {
|
||||
return SYSTEM_SERVICE_ERROR_INTERNAL;
|
||||
}
|
||||
host_arguments.insert(host_arguments.end(), arguments.begin(), arguments.end());
|
||||
std::vector<char*> host_argv;
|
||||
for (auto& argument: host_arguments) {
|
||||
host_argv.push_back(argument.data());
|
||||
}
|
||||
host_argv.push_back(nullptr);
|
||||
if (close_range(3, ~0u, CLOSE_RANGE_UNSHARE | CLOSE_RANGE_CLOEXEC) == 0) {
|
||||
execvp(host_argv[0], host_argv.data());
|
||||
}
|
||||
LOGF("LoadExec failed: %s\n", std::strerror(errno));
|
||||
#endif
|
||||
return SYSTEM_SERVICE_ERROR_INTERNAL;
|
||||
#else
|
||||
return SYSTEM_SERVICE_ERROR_UNAVAILABLE;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace Emulator
|
||||
|
|
@ -25,6 +25,7 @@
|
|||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
|
@ -88,16 +89,6 @@ ThreadLocalStorage::~ThreadLocalStorage() {
|
|||
}
|
||||
}
|
||||
|
||||
#pragma pack(1)
|
||||
|
||||
struct EntryParams {
|
||||
int argc;
|
||||
uint32_t pad;
|
||||
const char* argv[3];
|
||||
};
|
||||
|
||||
#pragma pack()
|
||||
|
||||
using atexit_func_t = KYTY_SYSV_ABI void (*)();
|
||||
using entry_func_t = KYTY_SYSV_ABI void (*)(EntryParams* params, atexit_func_t atexit_func);
|
||||
using module_ini_fini_func_t = KYTY_SYSV_ABI int (*)(size_t args, const void* argp,
|
||||
|
|
@ -1283,11 +1274,16 @@ void RuntimeLinker::Execute(const std::filesystem::path& game_patch) {
|
|||
LOGF_COLOR(Log::Color::BrightYellow, "---\n--- Execute: %s\n---\n", "Main");
|
||||
|
||||
if (auto entry = GetEntry(); entry != 0) {
|
||||
const auto& arguments = Config::GetGuestArguments();
|
||||
const auto params_size = offsetof(EntryParams, argv) +
|
||||
(arguments.size() + 2) * sizeof(const char*);
|
||||
auto* params = reinterpret_cast<EntryParams*>(
|
||||
(reinterpret_cast<uintptr_t>(main_stack_top) - 0x100u) & ~static_cast<uintptr_t>(0x0f));
|
||||
std::memset(params, 0, sizeof(EntryParams));
|
||||
params->argc = 1;
|
||||
params->argv[0] = "KytyEmu";
|
||||
(reinterpret_cast<uintptr_t>(main_stack_top) - params_size - 0x100u) &
|
||||
~static_cast<uintptr_t>(0x0f));
|
||||
std::memset(params, 0, params_size);
|
||||
for (const auto& argument: arguments) {
|
||||
params->argv[params->argc++] = argument.c_str();
|
||||
}
|
||||
|
||||
LOGF("stack_addr = %" PRIx64 "\n", reinterpret_cast<uint64_t>(params));
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@
|
|||
|
||||
namespace Loader {
|
||||
|
||||
struct EntryParams {
|
||||
int argc;
|
||||
uint32_t pad;
|
||||
const char* argv[1];
|
||||
};
|
||||
|
||||
class Elf64;
|
||||
struct Elf64_Sym;
|
||||
struct Elf64_Rela;
|
||||
|
|
|
|||
|
|
@ -164,6 +164,8 @@ void Psf::OpenJson(const std::filesystem::path& file_name) {
|
|||
add_string_param("contentId", "CONTENT_ID");
|
||||
add_string_param("contentVersion", "APP_VER");
|
||||
add_string_param("appVersion", "APP_VER");
|
||||
AddParamInt("ATTRIBUTE", 0);
|
||||
add_int_param("attribute", "ATTRIBUTE");
|
||||
add_int_param("attribute3", "ATTRIBUTE3");
|
||||
add_int_param("userDefinedParam1", "USER_DEFINED_PARAM_1");
|
||||
add_int_param("userDefinedParam2", "USER_DEFINED_PARAM_2");
|
||||
|
|
@ -360,10 +362,12 @@ void PlayGo::Open(const std::filesystem::path& file_name) {
|
|||
}
|
||||
|
||||
uint32_t magic1 = 0;
|
||||
constexpr uint32_t PLGO_MAGIC = 0x6f676c70;
|
||||
constexpr uint32_t PLGX_MAGIC = 0x78676c70;
|
||||
|
||||
m_f.Read(&magic1, 4);
|
||||
|
||||
if (magic1 != 0x6f676c70) {
|
||||
if (magic1 != PLGO_MAGIC && magic1 != PLGX_MAGIC) {
|
||||
LOGF("invalid file: magic1 = %08" PRIx32 "\n", magic1);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
22
src/main.cpp
22
src/main.cpp
|
|
@ -61,13 +61,16 @@ static void PrintUsage() {
|
|||
::printf(
|
||||
" --gpu <index> Vulkan physical device index. Default: auto.\n");
|
||||
::printf(" --fullscreen Run in borderless desktop fullscreen.\n");
|
||||
::printf(" --hdr Enable HDR10 output when supported.\n");
|
||||
::printf(
|
||||
" --hide-cursor Hide the cursor after 2 s idle. Default: off.\n");
|
||||
::printf(" --no-splash Disable the startup splash screen.\n");
|
||||
::printf(" --vr Enable the virtual VR headset.\n");
|
||||
::printf(" --amd-cpu Apply AMD CPU instruction patches.\n");
|
||||
::printf(" --vblank-frequency <num> Virtual vblank frequency. Default: 60.\n");
|
||||
::printf(" --console-language <0-29> Console language. Default: 1 (English US).\n");
|
||||
::printf(" --vulkan-validation <true|false> Enable Vulkan validation.\n");
|
||||
::printf(" --network-connected <true|false> Use the host local network (default: false).\n");
|
||||
::printf(" --gpu-assisted-validation <t|f> Bounds-check shader accesses on the GPU.\n"
|
||||
" Implies --vulkan-validation; very slow.\n");
|
||||
::printf(" --shader-validation <true|false> Enable shader validation.\n");
|
||||
|
|
@ -214,11 +217,21 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
|
|||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--hdr") {
|
||||
options.config.hdr_enabled = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--hide-cursor") {
|
||||
options.config.hide_cursor_enabled = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--no-splash") {
|
||||
options.config.splash_enabled = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--vr") {
|
||||
options.config.vr_enabled = true;
|
||||
continue;
|
||||
|
|
@ -294,6 +307,10 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
|
|||
value.c_str());
|
||||
return false;
|
||||
}
|
||||
} else if (arg == "--guest-exec") {
|
||||
options.elf = Common::PathFromUtf8(Common::FixFilenameSlash(value));
|
||||
options.config.guest_arguments.assign(argv + i + 1, argv + argc);
|
||||
break;
|
||||
} else if (arg == "--game-patch") {
|
||||
if (!options.game_patch.empty()) {
|
||||
::printf("--game-patch can only be specified once\n");
|
||||
|
|
@ -372,6 +389,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
|
|||
::printf("invalid console language: %s\n", value.c_str());
|
||||
return false;
|
||||
}
|
||||
} else if (arg == "--network-connected") {
|
||||
if (!ParseBool(value, options.config.network_connected)) {
|
||||
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
|
||||
return false;
|
||||
}
|
||||
} else if (arg == "--vulkan-validation") {
|
||||
if (!ParseBool(value, options.config.vulkan_validation_enabled)) {
|
||||
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
# The installer is copied outside the application before it runs. Keep it free of
|
||||
# Qt and application DLL dependencies so it can replace those files on Windows.
|
||||
if(MSVC)
|
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
||||
endif()
|
||||
add_executable(kyty_updater main.cpp UpdateInstaller.cpp)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(kyty_updater PRIVATE /EHsc)
|
||||
else()
|
||||
target_compile_options(kyty_updater PRIVATE -fexceptions)
|
||||
endif()
|
||||
if(MINGW)
|
||||
target_link_options(kyty_updater PRIVATE -static -municode)
|
||||
endif()
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
#include "UpdateInstaller.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace Kyty::Updater {
|
||||
namespace {
|
||||
namespace fs = std::filesystem;
|
||||
struct Entry {
|
||||
fs::path relative;
|
||||
fs::file_type type;
|
||||
};
|
||||
struct Plan {
|
||||
fs::path source;
|
||||
fs::path target;
|
||||
std::vector<Entry> entries;
|
||||
};
|
||||
|
||||
bool IsWithin(const fs::path& path, const fs::path& root) {
|
||||
auto part = path.begin();
|
||||
for (const auto& component: root) {
|
||||
if (part == path.end() || *part++ != component) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsRelative(const fs::path& path) {
|
||||
if (path.empty() || path.is_absolute() || path.has_root_name()) {
|
||||
return false;
|
||||
}
|
||||
for (const auto& component: path) {
|
||||
if (component == ".." || component == "." || component.empty()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
fs::file_type Type(const fs::path& path) {
|
||||
std::error_code error;
|
||||
const auto status = fs::symlink_status(path, error);
|
||||
if (error && error != std::errc::no_such_file_or_directory) {
|
||||
throw fs::filesystem_error("Cannot inspect update path", path, error);
|
||||
}
|
||||
return status.type();
|
||||
}
|
||||
|
||||
void CheckLink(const fs::path& path, const fs::path& root) {
|
||||
const auto link = fs::read_symlink(path);
|
||||
if (link.is_absolute() || !IsWithin(fs::canonical(path), root)) {
|
||||
throw std::runtime_error("Update contains a link outside its installation: " +
|
||||
path.string());
|
||||
}
|
||||
}
|
||||
|
||||
void CheckDestination(const fs::path& target, const Entry& entry) {
|
||||
const auto destination = target / entry.relative;
|
||||
const auto type = Type(destination);
|
||||
if (type == fs::file_type::not_found) {
|
||||
return;
|
||||
}
|
||||
if ((entry.type == fs::file_type::directory) != (type == fs::file_type::directory) ||
|
||||
(type != fs::file_type::regular && type != fs::file_type::directory &&
|
||||
type != fs::file_type::symlink)) {
|
||||
throw std::runtime_error("Update would replace a different file type: " +
|
||||
destination.string());
|
||||
}
|
||||
if (type == fs::file_type::symlink) {
|
||||
CheckLink(destination, target);
|
||||
}
|
||||
}
|
||||
|
||||
Plan MakePlan(const fs::path& source, const fs::path& target, const fs::path& launcher) {
|
||||
if (!IsRelative(launcher) || Type(source) != fs::file_type::directory ||
|
||||
Type(target) != fs::file_type::directory) {
|
||||
throw std::runtime_error(
|
||||
"Update source, installation directory, or launcher path is invalid.");
|
||||
}
|
||||
Plan plan {fs::canonical(source), fs::canonical(target), {}};
|
||||
if (IsWithin(plan.target, plan.source)) {
|
||||
throw std::runtime_error("Update source must not contain the installation directory.");
|
||||
}
|
||||
// macOS releases also have flat runtime files; install only the signed bundle.
|
||||
fs::path scope;
|
||||
if (launcher.begin()->extension() == ".app") {
|
||||
scope = *launcher.begin();
|
||||
}
|
||||
const auto source_root = scope.empty() ? plan.source : plan.source / scope;
|
||||
if (Type(source_root) != fs::file_type::directory) {
|
||||
throw std::runtime_error("The release does not contain the application bundle.");
|
||||
}
|
||||
const auto binary_directory = launcher.parent_path();
|
||||
#ifdef _WIN32
|
||||
const fs::path suffix = ".exe";
|
||||
#else
|
||||
const fs::path suffix;
|
||||
#endif
|
||||
for (const auto& required: {launcher, binary_directory / ("kyty_emulator" + suffix.string()),
|
||||
binary_directory / ("kyty_updater" + suffix.string())}) {
|
||||
if (Type(plan.source / required) != fs::file_type::regular) {
|
||||
throw std::runtime_error("Release is missing an executable: " + required.string());
|
||||
}
|
||||
#ifndef _WIN32
|
||||
const auto executable =
|
||||
fs::perms::owner_exec | fs::perms::group_exec | fs::perms::others_exec;
|
||||
if ((fs::status(plan.source / required).permissions() & executable) == fs::perms::none) {
|
||||
throw std::runtime_error("Release executable lacks execute permissions: " +
|
||||
required.string());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
if (!scope.empty()) {
|
||||
plan.entries.push_back({scope, fs::file_type::directory});
|
||||
}
|
||||
for (const auto& item: fs::recursive_directory_iterator(source_root)) {
|
||||
const auto relative = item.path().lexically_relative(plan.source);
|
||||
const auto type = item.symlink_status().type();
|
||||
if (relative.begin()->string().starts_with(".kyty-update")) {
|
||||
throw std::runtime_error("Release uses a reserved updater filename.");
|
||||
}
|
||||
if (type == fs::file_type::symlink) {
|
||||
CheckLink(item.path(), source_root);
|
||||
} else if (type != fs::file_type::directory && type != fs::file_type::regular) {
|
||||
throw std::runtime_error("Release contains an unsupported file: " + relative.string());
|
||||
}
|
||||
plan.entries.push_back({relative, type});
|
||||
}
|
||||
// Checking directories before their children prevents destination symlink traversal.
|
||||
std::sort(plan.entries.begin(), plan.entries.end(),
|
||||
[](const Entry& left, const Entry& right) { return left.relative < right.relative; });
|
||||
for (const auto& entry: plan.entries) {
|
||||
CheckDestination(plan.target, entry);
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
|
||||
struct InstallLock {
|
||||
fs::path path;
|
||||
void Acquire(const fs::path& target) {
|
||||
auto candidate = target / ".kyty-update-lock";
|
||||
if (!fs::create_directory(candidate)) {
|
||||
throw std::runtime_error(
|
||||
"Another update is running, or a previous update was interrupted. "
|
||||
"Close all updater processes, inspect retained backups, then remove " +
|
||||
candidate.string());
|
||||
}
|
||||
path = std::move(candidate);
|
||||
}
|
||||
~InstallLock() {
|
||||
if (!path.empty()) {
|
||||
std::error_code ignored;
|
||||
// An incomplete rollback leaves backups here and keeps the lock for recovery.
|
||||
fs::remove(path, ignored);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct Change {
|
||||
fs::path relative;
|
||||
bool old_moved = false;
|
||||
bool installed = false;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
Result Validate(const fs::path& source, const fs::path& target, const fs::path& launcher_relative) {
|
||||
try {
|
||||
MakePlan(source, target, launcher_relative);
|
||||
// Check actual write access, including ACLs, and interrupted/concurrent updates.
|
||||
InstallLock lock;
|
||||
lock.Acquire(target);
|
||||
return {true, {}};
|
||||
} catch (const std::exception& error) {
|
||||
return {false, error.what()};
|
||||
}
|
||||
}
|
||||
|
||||
Result Install(const fs::path& source, const fs::path& target, const fs::path& launcher_relative) {
|
||||
InstallLock lock;
|
||||
fs::path backup;
|
||||
std::vector<Change> changes;
|
||||
std::vector<fs::path> created_directories;
|
||||
try {
|
||||
const auto plan = MakePlan(source, target, launcher_relative);
|
||||
lock.Acquire(plan.target);
|
||||
backup = lock.path / "backup";
|
||||
changes.reserve(plan.entries.size());
|
||||
created_directories.reserve(plan.entries.size());
|
||||
// The launcher extracts beside the installation, so files can move directly
|
||||
// from that staging directory without another full copy of the release.
|
||||
for (const auto& entry: plan.entries) {
|
||||
CheckDestination(plan.target, entry);
|
||||
const auto destination = plan.target / entry.relative;
|
||||
if (entry.type == fs::file_type::directory) {
|
||||
if (fs::create_directory(destination)) {
|
||||
created_directories.push_back(destination);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
changes.push_back({entry.relative});
|
||||
auto& change = changes.back();
|
||||
if (Type(destination) != fs::file_type::not_found) {
|
||||
fs::create_directories((backup / entry.relative).parent_path());
|
||||
fs::rename(destination, backup / entry.relative);
|
||||
change.old_moved = true;
|
||||
}
|
||||
fs::rename(plan.source / entry.relative, destination);
|
||||
change.installed = true;
|
||||
}
|
||||
std::error_code cleanup_error;
|
||||
fs::remove_all(backup, cleanup_error);
|
||||
return {true, cleanup_error ? "Update installed; backup cleanup failed: " + backup.string()
|
||||
: std::string {}};
|
||||
} catch (const std::exception& error) {
|
||||
std::string message = error.what();
|
||||
bool restored = true;
|
||||
for (auto change = changes.rbegin(); change != changes.rend(); ++change) {
|
||||
try {
|
||||
const auto destination = target / change->relative;
|
||||
if (change->installed) {
|
||||
fs::rename(destination, source / change->relative);
|
||||
}
|
||||
if (change->old_moved) {
|
||||
fs::rename(backup / change->relative, destination);
|
||||
}
|
||||
} catch (const std::exception& rollback_error) {
|
||||
restored = false;
|
||||
message += "\nRollback failed: " + std::string(rollback_error.what());
|
||||
}
|
||||
}
|
||||
for (auto directory = created_directories.rbegin(); directory != created_directories.rend();
|
||||
++directory) {
|
||||
std::error_code ignored;
|
||||
fs::remove(*directory, ignored);
|
||||
}
|
||||
if (!backup.empty()) {
|
||||
if (restored) {
|
||||
std::error_code ignored;
|
||||
fs::remove_all(backup, ignored);
|
||||
} else {
|
||||
message += "\nBackup retained at: " + backup.string();
|
||||
}
|
||||
}
|
||||
return {false, message};
|
||||
}
|
||||
}
|
||||
} // namespace Kyty::Updater
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace Kyty::Updater {
|
||||
struct Result {
|
||||
bool success;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
// Only release files are replaced. Files absent from the release are preserved.
|
||||
// Successful installation consumes staged files; stage on the target filesystem.
|
||||
Result Validate(const std::filesystem::path& source, const std::filesystem::path& target,
|
||||
const std::filesystem::path& launcher_relative);
|
||||
Result Install(const std::filesystem::path& source, const std::filesystem::path& target,
|
||||
const std::filesystem::path& launcher_relative);
|
||||
} // namespace Kyty::Updater
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue