mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-28 08:13:05 +00:00
Compare commits
39
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
679be25c37 | ||
|
|
fa56c5eae9 | ||
|
|
3b9e176b70 | ||
|
|
053f5fd3cc | ||
|
|
f3836bb831 | ||
|
|
2f4d0f383e |
No files matched your search
@@ -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" .
|
||||
|
||||
|
||||
+17
-13
@@ -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
|
||||
@@ -467,7 +471,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 +484,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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -1072,8 +1072,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 +1081,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 +1101,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 +1116,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 +1125,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 +1141,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 +1149,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 +1157,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 +1172,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 +1194,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 +1247,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 +1341,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 +1357,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 +1383,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 depth_color_copy_enabled = false;
|
||||
bool supports_block_texel_view = false;
|
||||
bool mesh_shader_enabled = false;
|
||||
vk::PhysicalDeviceMeshShaderPropertiesEXT mesh_shader_properties = {};
|
||||
|
||||
+15
-3
@@ -36,7 +36,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.
|
||||
@@ -1059,8 +1061,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
|
||||
@@ -1923,6 +1932,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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
@@ -622,8 +624,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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1029,12 +1029,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().depth_color_copy_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;
|
||||
|
||||
@@ -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);
|
||||
@@ -274,14 +277,24 @@ 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 {};
|
||||
@@ -346,6 +359,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 +432,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 +472,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) {
|
||||
@@ -692,9 +729,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];
|
||||
@@ -868,7 +907,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 +917,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 +935,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 +965,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,8 +1014,12 @@ 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++) {
|
||||
|
||||
@@ -478,6 +478,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 +549,9 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
|
||||
graphics.attachment_feedback_loop_enabled =
|
||||
feedback_extensions && feedback_layout.attachmentFeedbackLoopLayout &&
|
||||
feedback_dynamic.attachmentFeedbackLoopDynamicState;
|
||||
graphics.depth_color_copy_enabled = maintenance8_extension && maintenance8.maintenance8;
|
||||
LOGF("Vulkan depth/color copy support: %s\n",
|
||||
graphics.depth_color_copy_enabled ? "true" : "false");
|
||||
LOGF("Vulkan depth feedback support: %s\n",
|
||||
graphics.attachment_feedback_loop_enabled ? "true" : "false");
|
||||
if (graphics.mesh_shader_enabled) {
|
||||
@@ -638,6 +648,10 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
|
||||
image_atomic_int64.sparseImageInt64Atomics = VK_FALSE;
|
||||
create_info.pNext = &image_atomic_int64;
|
||||
}
|
||||
if (graphics.depth_color_copy_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 +685,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 +1021,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -115,6 +115,16 @@ DppTargetLane EmitDppTargetLane(EmitterState& state, const IR::DppMoveFlags& fla
|
||||
if (control == 0x141u) {
|
||||
return EmitDppMirrorTargetLane(state, subid, true);
|
||||
}
|
||||
// row_share:N reads lane N of the caller's row of 16.
|
||||
if (control >= 0x150u && control <= 0x15fu) {
|
||||
const auto row = state.builder.AllocateId();
|
||||
const auto target = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), row, subid,
|
||||
ConstantU32(state, 0xfffffff0u));
|
||||
state.builder.AddFunction(spv::OpBitwiseOr, TypeU32(state), target, row,
|
||||
ConstantU32(state, control & 0xfu));
|
||||
return {target, ConstantBool(state, true)};
|
||||
}
|
||||
if (control >= 0x160u && control <= 0x16fu) {
|
||||
const auto target = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpBitwiseXor, TypeU32(state), target, subid,
|
||||
|
||||
@@ -323,25 +323,25 @@ 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};
|
||||
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 +350,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;
|
||||
}
|
||||
|
||||
@@ -794,21 +789,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));
|
||||
@@ -829,32 +831,38 @@ 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 EmitGather = [&](uint32_t resource, uint32_t array_index) {
|
||||
const auto& candidate = state.program.info.images[resource];
|
||||
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);
|
||||
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);
|
||||
const auto sample = EmitImageAccess(ctx, inst, result_type, EmitGather);
|
||||
ctx.Define(inst, ResultVector(ctx, UnpackImageGather(ctx, mem, sample),
|
||||
result_numeric_class, false, mem, true));
|
||||
return;
|
||||
@@ -878,7 +886,7 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
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;
|
||||
@@ -908,9 +916,20 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
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()) {
|
||||
ctx.Fail(inst, "has a nonconstant 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& candidate = state.program.info.images[resource];
|
||||
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,
|
||||
@@ -920,7 +939,7 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
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);
|
||||
|
||||
@@ -47,8 +47,12 @@ constexpr OpcodeMap SOP1_OPCODE_LIST[] = {
|
||||
{0x0au, Opcode::S_WQM_B64},
|
||||
{0x0bu, Opcode::S_BREV_B32},
|
||||
{0x0cu, Opcode::S_BREV_B64},
|
||||
{0x0du, Opcode::S_BCNT0_I32_B32},
|
||||
{0x0eu, Opcode::S_BCNT0_I32_B64},
|
||||
{0x0fu, Opcode::S_BCNT1_I32_B32},
|
||||
{0x10u, Opcode::S_BCNT1_I32_B64},
|
||||
{0x11u, Opcode::S_FF0_I32_B32},
|
||||
{0x12u, Opcode::S_FF0_I32_B64},
|
||||
{0x13u, Opcode::S_FF1_I32_B32},
|
||||
{0x14u, Opcode::S_FF1_I32_B64},
|
||||
{0x15u, Opcode::S_FLBIT_I32_B32},
|
||||
@@ -112,7 +116,9 @@ constexpr OpcodeMap SOPP_OPCODE_LIST[] = {
|
||||
{0x12u, Opcode::S_TRAP},
|
||||
{0x16u, Opcode::S_TTRACEDATA},
|
||||
{0x17u, Opcode::S_CBRANCH_CDBGSYS},
|
||||
{0x18u, Opcode::S_CBRANCH_CDBGUSER},
|
||||
{0x19u, Opcode::S_CBRANCH_CDBGSYS_OR_USER},
|
||||
{0x1au, Opcode::S_CBRANCH_CDBGSYS_AND_USER},
|
||||
{0x20u, Opcode::S_INST_PREFETCH},
|
||||
{0x23u, Opcode::S_WAITCNT_DEPCTR},
|
||||
};
|
||||
|
||||
@@ -186,7 +186,9 @@ bool IsConditionalBranch(Opcode opcode) {
|
||||
switch (opcode) {
|
||||
// Conditional shader debugging is disabled.
|
||||
case Opcode::S_CBRANCH_CDBGSYS:
|
||||
case Opcode::S_CBRANCH_CDBGSYS_OR_USER: return false;
|
||||
case Opcode::S_CBRANCH_CDBGUSER:
|
||||
case Opcode::S_CBRANCH_CDBGSYS_OR_USER:
|
||||
case Opcode::S_CBRANCH_CDBGSYS_AND_USER: return false;
|
||||
case Opcode::S_CBRANCH_SCC0:
|
||||
case Opcode::S_CBRANCH_SCC1:
|
||||
case Opcode::S_CBRANCH_VCCZ:
|
||||
@@ -510,6 +512,10 @@ std::string InstructionToString(const Instruction& inst) {
|
||||
case Opcode::S_SEXT_I32_I16:
|
||||
case Opcode::S_BREV_B32:
|
||||
case Opcode::S_BREV_B64:
|
||||
case Opcode::S_BCNT0_I32_B32:
|
||||
case Opcode::S_BCNT0_I32_B64:
|
||||
case Opcode::S_FF0_I32_B32:
|
||||
case Opcode::S_FF0_I32_B64:
|
||||
case Opcode::S_BCNT1_I32_B32:
|
||||
case Opcode::S_FLBIT_I32_B32:
|
||||
case Opcode::S_FF1_I32_B32:
|
||||
@@ -571,7 +577,9 @@ std::string InstructionToString(const Instruction& inst) {
|
||||
case Opcode::S_CBRANCH_EXECZ:
|
||||
case Opcode::S_CBRANCH_EXECNZ:
|
||||
case Opcode::S_CBRANCH_CDBGSYS:
|
||||
case Opcode::S_CBRANCH_CDBGUSER:
|
||||
case Opcode::S_CBRANCH_CDBGSYS_OR_USER:
|
||||
case Opcode::S_CBRANCH_CDBGSYS_AND_USER:
|
||||
return WithUnsupportedReason(inst, fmt::format("0x{:08x}: {} 0x{:08x}", inst.pc,
|
||||
magic_enum::enum_name(inst.opcode),
|
||||
inst.branch_target));
|
||||
|
||||
@@ -50,8 +50,12 @@ enum class Opcode {
|
||||
S_ABSDIFF_I32,
|
||||
S_BREV_B32,
|
||||
S_BREV_B64,
|
||||
S_BCNT0_I32_B32,
|
||||
S_BCNT0_I32_B64,
|
||||
S_BCNT1_I32_B32,
|
||||
S_BCNT1_I32_B64,
|
||||
S_FF0_I32_B32,
|
||||
S_FF0_I32_B64,
|
||||
S_FF1_I32_B32,
|
||||
S_FF1_I32_B64,
|
||||
S_FLBIT_I32_B32,
|
||||
@@ -710,7 +714,9 @@ enum class Opcode {
|
||||
S_CBRANCH_EXECZ,
|
||||
S_CBRANCH_EXECNZ,
|
||||
S_CBRANCH_CDBGSYS,
|
||||
S_CBRANCH_CDBGUSER,
|
||||
S_CBRANCH_CDBGSYS_OR_USER,
|
||||
S_CBRANCH_CDBGSYS_AND_USER,
|
||||
S_SENDMSG,
|
||||
S_GETREG_B32,
|
||||
S_SETREG_B32,
|
||||
|
||||
@@ -39,7 +39,7 @@ constexpr Vop2OpcodeInfo VOP2_OPCODE_LIST[] = {
|
||||
{0x0bu, Opcode::V_MUL_U32_U24, Vop2SdwaProfile::IntegerPartialDestination},
|
||||
{0x0fu, Opcode::V_MIN_F32},
|
||||
{0x10u, Opcode::V_MAX_F32},
|
||||
{0x11u, Opcode::V_MIN_I32},
|
||||
{0x11u, Opcode::V_MIN_I32, Vop2SdwaProfile::IntegerFullDestination},
|
||||
{0x12u, Opcode::V_MAX_I32, Vop2SdwaProfile::IntegerFullDestination},
|
||||
{0x13u, Opcode::V_MIN_U32, Vop2SdwaProfile::IntegerPartialDestination},
|
||||
{0x14u, Opcode::V_MAX_U32, Vop2SdwaProfile::IntegerFullDestination},
|
||||
@@ -776,9 +776,10 @@ void DecodeVop1Sdwa(uint32_t pc, std::span<const uint32_t> code, uint32_t word_i
|
||||
|
||||
void ApplyDppModifier(Operand& operand, uint32_t modifier, uint32_t encoding) {
|
||||
operand.dpp = true;
|
||||
if (encoding == 233u) {
|
||||
operand.dpp8 = true;
|
||||
operand.dpp_ctrl = modifier >> 8u;
|
||||
if (encoding == 233u || encoding == 234u) {
|
||||
operand.dpp8 = true;
|
||||
operand.dpp_ctrl = modifier >> 8u;
|
||||
operand.dpp_fetch_inactive = encoding == 234u;
|
||||
return;
|
||||
}
|
||||
operand.negate = ((modifier >> 20u) & 0x1u) != 0u;
|
||||
@@ -1686,7 +1687,8 @@ void DecodeVop2(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
||||
return;
|
||||
}
|
||||
switch (src0) {
|
||||
case 233u: DecodeVop2Dpp(pc, code, word_index, opcode, vdst, vsrc1, inst); return;
|
||||
case 233u:
|
||||
case 234u: DecodeVop2Dpp(pc, code, word_index, opcode, vdst, vsrc1, inst); return;
|
||||
case 249u: DecodeVop2Sdwa(pc, code, word_index, opcode, vdst, vsrc1, inst); return;
|
||||
case 250u: DecodeVop2Dpp(pc, code, word_index, opcode, vdst, vsrc1, inst); return;
|
||||
default: break;
|
||||
@@ -1721,7 +1723,8 @@ void DecodeVop1(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
||||
return;
|
||||
}
|
||||
switch (src0) {
|
||||
case 233u: DecodeVop1Dpp(pc, code, word_index, opcode, vdst, inst); return;
|
||||
case 233u:
|
||||
case 234u: DecodeVop1Dpp(pc, code, word_index, opcode, vdst, inst); return;
|
||||
case 249u: DecodeVop1Sdwa(pc, code, word_index, opcode, vdst, inst); return;
|
||||
case 250u: DecodeVop1Dpp(pc, code, word_index, opcode, vdst, inst); return;
|
||||
default: break;
|
||||
|
||||
@@ -221,8 +221,12 @@ void Translator::V_XOR3_B32(const Decoder::Instruction& inst) {
|
||||
WriteOperand(DestinationOperand(inst), result);
|
||||
}
|
||||
|
||||
void Translator::S_FF1_I32_B64(const Decoder::Instruction& inst) {
|
||||
const auto source = ExtractU64(ReadU64(inst.src0));
|
||||
void Translator::S_FF_I32_B64(const Decoder::Instruction& inst, bool find_zero) {
|
||||
auto source = ReadU32Pair(inst.src0);
|
||||
if (find_zero) {
|
||||
source[0] = ir.BitwiseNot(source[0]);
|
||||
source[1] = ir.BitwiseNot(source[1]);
|
||||
}
|
||||
const auto low_lsb = IR::U32(ir.Emit(IR::ValueOpcode::FindILsb32, {source[0]}));
|
||||
const auto high_lsb = IR::U32(ir.Emit(IR::ValueOpcode::FindILsb32, {source[1]}));
|
||||
const auto high_position = ir.IAdd(high_lsb, IR::U32(IR::Value(32u)));
|
||||
|
||||
@@ -687,7 +687,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 +715,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),
|
||||
|
||||
@@ -178,12 +178,29 @@ void Translator::EmitScalar(const Decoder::Instruction& inst) {
|
||||
IR::U32(ir.Emit(IR::ValueOpcode::BitReverse32, {value[0]}))});
|
||||
return;
|
||||
}
|
||||
case O::S_BCNT0_I32_B32:
|
||||
case O::S_FF0_I32_B32: {
|
||||
const auto value = ir.BitwiseNot(ReadU32(inst.src0));
|
||||
const bool count = inst.opcode == O::S_BCNT0_I32_B32;
|
||||
const auto result = IR::U32(ir.Emit(count ? IR::ValueOpcode::BitCount32
|
||||
: IR::ValueOpcode::FindILsb32, {value}));
|
||||
WriteOperand(inst.dst, result);
|
||||
if (count) {
|
||||
ir.SetScc(ir.INotEqual(result, IR::U32(IR::Value(0u))));
|
||||
}
|
||||
return;
|
||||
}
|
||||
case O::S_BCNT1_I32_B32:
|
||||
return SimpleInteger(inst, IR::ValueOpcode::BitCount32, IR::Type::U32, false, false,
|
||||
true);
|
||||
case O::S_BCNT0_I32_B64:
|
||||
case O::S_BCNT1_I32_B64: {
|
||||
// Vulkan bit counts operate on 32-bit words; avoid packing only to split again.
|
||||
const auto value = ReadU32Pair(inst.src0);
|
||||
auto value = ReadU32Pair(inst.src0);
|
||||
if (inst.opcode == O::S_BCNT0_I32_B64) {
|
||||
value[0] = ir.BitwiseNot(value[0]);
|
||||
value[1] = ir.BitwiseNot(value[1]);
|
||||
}
|
||||
const auto low = IR::U32(ir.Emit(IR::ValueOpcode::BitCount32, {value[0]}));
|
||||
const auto high = IR::U32(ir.Emit(IR::ValueOpcode::BitCount32, {value[1]}));
|
||||
const auto result = ir.IAdd(low, high);
|
||||
@@ -221,7 +238,8 @@ void Translator::EmitScalar(const Decoder::Instruction& inst) {
|
||||
return ComposedIntegerBinary(inst, IR::ValueOpcode::BitwiseOr32, false, true, true);
|
||||
case O::S_XNOR_B32:
|
||||
return ComposedIntegerBinary(inst, IR::ValueOpcode::BitwiseXor32, false, true, true);
|
||||
case O::S_FF1_I32_B64: return S_FF1_I32_B64(inst);
|
||||
case O::S_FF0_I32_B64: return S_FF_I32_B64(inst, true);
|
||||
case O::S_FF1_I32_B64: return S_FF_I32_B64(inst, false);
|
||||
case O::S_FLBIT_I32_B32: return V_FFBH_32(inst, false);
|
||||
case O::S_FLBIT_I32_B64: return S_FLBIT_I32_B64(inst);
|
||||
|
||||
@@ -261,7 +279,9 @@ void Translator::EmitScalar(const Decoder::Instruction& inst) {
|
||||
case O::S_CBRANCH_EXECZ:
|
||||
case O::S_CBRANCH_EXECNZ:
|
||||
case O::S_CBRANCH_CDBGSYS:
|
||||
case O::S_CBRANCH_CDBGUSER:
|
||||
case O::S_CBRANCH_CDBGSYS_OR_USER:
|
||||
case O::S_CBRANCH_CDBGSYS_AND_USER:
|
||||
case O::S_ENDPGM: return;
|
||||
default: return FailMissingTranslation(inst);
|
||||
}
|
||||
|
||||
@@ -1163,9 +1163,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 +1383,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),
|
||||
|
||||
@@ -187,7 +187,7 @@ private:
|
||||
void V_AND_OR_B32(const Decoder::Instruction& inst);
|
||||
void V_OR3_B32(const Decoder::Instruction& inst);
|
||||
void V_XOR3_B32(const Decoder::Instruction& inst);
|
||||
void S_FF1_I32_B64(const Decoder::Instruction& inst);
|
||||
void S_FF_I32_B64(const Decoder::Instruction& inst, bool find_zero);
|
||||
void V_FFBH_32(const Decoder::Instruction& inst, bool sign);
|
||||
void S_FLBIT_I32_B64(const Decoder::Instruction& inst);
|
||||
void Integer24(const Decoder::Instruction& inst, bool sign, bool addend);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace Xbyak {
|
||||
class CodeGenerator;
|
||||
}
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::IR {
|
||||
|
||||
enum class ResourceKind {
|
||||
@@ -573,12 +577,12 @@ struct ResourcePlan {
|
||||
uint64_t generation = 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;
|
||||
@@ -597,6 +601,7 @@ 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;
|
||||
|
||||
@@ -317,8 +317,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)
|
||||
|
||||
@@ -989,23 +989,29 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
|
||||
// Keep only that Boolean structure, never the varying shader dependency graph.
|
||||
Value unknown;
|
||||
bool capture_control_reads = false;
|
||||
std::unordered_map<const Inst*, Value> predicates;
|
||||
std::function<Value(Value)> ClonePredicate = [&](Value value) -> Value {
|
||||
value = value.Resolve();
|
||||
if (value.IsEmpty()) return {};
|
||||
const auto* inst = value.TryInstruction();
|
||||
if (inst == nullptr)
|
||||
return ValidateRuntimeValue(program, value, RuntimeValueType::Integer) ? value : Value {};
|
||||
// Preserve shared EXEC predicates, including subgraphs with no uniform factor.
|
||||
const auto [found, inserted] = predicates.try_emplace(inst);
|
||||
if (!inserted) return found->second;
|
||||
auto& result = found->second;
|
||||
if (ValidateRuntimeValue(program, value, RuntimeValueType::Integer)) {
|
||||
capture_control_reads |=
|
||||
!ValidateRuntimeValue(program, value, RuntimeValueType::ImmutableInteger);
|
||||
return Clone(value);
|
||||
return result = 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;
|
||||
if (operand.IsEmpty() || op == ValueOpcode::ConditionRef) return result = operand;
|
||||
auto& node = plan.value_storage.emplace_back(op);
|
||||
node.SetArg(0, operand);
|
||||
return Value(&node);
|
||||
return result = Value(&node);
|
||||
}
|
||||
if (op != ValueOpcode::LogicalAnd && op != ValueOpcode::LogicalOr) return {};
|
||||
auto left = ClonePredicate(inst->Arg(0));
|
||||
@@ -1019,7 +1025,7 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
|
||||
auto& node = plan.value_storage.emplace_back(op);
|
||||
node.SetArg(0, left);
|
||||
node.SetArg(1, right);
|
||||
return Value(&node);
|
||||
return result = Value(&node);
|
||||
};
|
||||
plan.control_flow = ResourceControlFlow(program, ClonePredicate);
|
||||
plan.uniform_fill = AnalyzeUniformFill(program);
|
||||
@@ -1064,6 +1070,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;
|
||||
}
|
||||
|
||||
@@ -1405,6 +1412,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -359,6 +364,131 @@ private:
|
||||
|
||||
} // namespace
|
||||
|
||||
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;
|
||||
|
||||
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, Value active_mask)
|
||||
: m_program(program), m_runtime(runtime), m_clean_evaluator(clean_evaluator),
|
||||
@@ -954,6 +1084,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];
|
||||
|
||||
@@ -45,6 +45,7 @@ enum class RuntimeValueType { Any, Integer, ImmutableInteger };
|
||||
|
||||
bool ValidateRuntimeValue(const ResourcePlan& program, Value value,
|
||||
RuntimeValueType type = RuntimeValueType::Any);
|
||||
void CompileSrtWalker(ResourcePlan& program);
|
||||
// Uses the strict reader for values that affect shader specialization.
|
||||
SrtRuntime CleanRuntime(SrtRuntime runtime);
|
||||
|
||||
|
||||
@@ -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
|
||||
+46
-138
@@ -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);
|
||||
|
||||
+18
-15
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
+2
-8
@@ -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);
|
||||
|
||||
+86
-57
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,167 @@
|
||||
#include "UpdateInstaller.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <cerrno>
|
||||
#include <csignal>
|
||||
#include <spawn.h>
|
||||
#include <unistd.h>
|
||||
extern char** environ;
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
bool WaitForParent(unsigned long pid) {
|
||||
#ifdef _WIN32
|
||||
HANDLE process = OpenProcess(SYNCHRONIZE, FALSE, pid);
|
||||
if (!process) {
|
||||
return GetLastError() == ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
const auto result = WaitForSingleObject(process, 120000);
|
||||
CloseHandle(process);
|
||||
return result == WAIT_OBJECT_0;
|
||||
#else
|
||||
if (pid > static_cast<unsigned long>(std::numeric_limits<pid_t>::max())) {
|
||||
return false;
|
||||
}
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::minutes(2);
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
if (kill(static_cast<pid_t>(pid), 0) != 0) {
|
||||
return errno == ESRCH;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Launch(const fs::path& launcher, const fs::path& directory) {
|
||||
#ifdef _WIN32
|
||||
std::wstring command = L"\"" + launcher.wstring() + L"\"";
|
||||
STARTUPINFOW startup {};
|
||||
startup.cb = sizeof(startup);
|
||||
PROCESS_INFORMATION process {};
|
||||
if (!CreateProcessW(launcher.c_str(), command.data(), nullptr, nullptr, FALSE, 0, nullptr,
|
||||
directory.c_str(), &startup, &process)) {
|
||||
return false;
|
||||
}
|
||||
CloseHandle(process.hThread);
|
||||
CloseHandle(process.hProcess);
|
||||
return true;
|
||||
#else
|
||||
if (chdir(directory.c_str()) != 0) {
|
||||
return false;
|
||||
}
|
||||
setsid();
|
||||
pid_t child;
|
||||
char* arguments[] = {const_cast<char*>(launcher.c_str()), nullptr};
|
||||
return posix_spawn(&child, launcher.c_str(), nullptr, nullptr, arguments, environ) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void WriteResult(const fs::path& workspace, const fs::path& target,
|
||||
const Kyty::Updater::Result& result) try {
|
||||
const auto temporary = workspace / "update-result.txt";
|
||||
std::ofstream stream(temporary, std::ios::trunc);
|
||||
stream << (result.success ? "success\n" : "failure\n") << result.message;
|
||||
stream.close();
|
||||
if (!stream) {
|
||||
throw std::runtime_error("Cannot write the update result.");
|
||||
}
|
||||
const auto destination = target / ".kyty-update-result";
|
||||
// Never open the destination: rename safely replaces even an existing symlink.
|
||||
std::error_code ignored;
|
||||
fs::remove(destination, ignored);
|
||||
fs::rename(temporary, destination);
|
||||
} catch (const std::exception& error) {
|
||||
// Reporting must never prevent the original or updated launcher restarting.
|
||||
std::cerr << "Cannot record update result: " << error.what() << '\n';
|
||||
}
|
||||
|
||||
void CleanupWorkspace(const fs::path& source, const fs::path& target) try {
|
||||
const auto workspace = source.parent_path();
|
||||
if (source.filename() != "payload" ||
|
||||
!workspace.filename().string().starts_with(".kyty-update-") ||
|
||||
fs::canonical(workspace.parent_path()) != fs::canonical(target)) {
|
||||
return;
|
||||
}
|
||||
// Windows cannot unlink the running helper. Remove the archive and payload
|
||||
// separately so that only this small executable can remain after success.
|
||||
for (const auto& entry: fs::directory_iterator(workspace)) {
|
||||
std::error_code ignored;
|
||||
fs::remove_all(entry.path(), ignored);
|
||||
}
|
||||
std::error_code ignored;
|
||||
fs::remove(workspace, ignored);
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << "Update installed; temporary files could not be removed: " << error.what() << '\n';
|
||||
}
|
||||
|
||||
int Run(const std::vector<fs::path>& arguments) {
|
||||
const bool validate = arguments.size() == 5 && arguments[1] == "--validate";
|
||||
const bool install = arguments.size() == 7 && arguments[1] == "--install";
|
||||
if (!validate && !install) {
|
||||
std::cerr
|
||||
<< "Usage: kyty_updater --validate SOURCE TARGET LAUNCHER_REL\n"
|
||||
" kyty_updater --install SOURCE TARGET LAUNCHER_REL PARENT_PID WORKDIR\n";
|
||||
return 2;
|
||||
}
|
||||
const auto source = fs::absolute(arguments[2]);
|
||||
const auto target = fs::absolute(arguments[3]);
|
||||
const auto launcher = arguments[4];
|
||||
if (validate) {
|
||||
const auto result = Kyty::Updater::Validate(source, target, launcher);
|
||||
if (!result.success) {
|
||||
std::cerr << result.message << '\n';
|
||||
}
|
||||
return result.success ? 0 : 1;
|
||||
}
|
||||
const auto pid_string = arguments[5].string();
|
||||
std::size_t consumed = 0;
|
||||
const auto pid = std::stoul(pid_string, &consumed);
|
||||
if (pid <= 1 || consumed != pid_string.size() || pid_string.front() == '-') {
|
||||
throw std::runtime_error("Invalid launcher process identifier.");
|
||||
}
|
||||
if (!WaitForParent(pid)) {
|
||||
WriteResult(source.parent_path(), target,
|
||||
{false, "Update cancelled: the launcher did not exit within two minutes."});
|
||||
return 1;
|
||||
}
|
||||
auto result = Kyty::Updater::Install(source, target, launcher);
|
||||
WriteResult(source.parent_path(), target, result);
|
||||
if (!Launch(target / launcher, fs::absolute(arguments[6]))) {
|
||||
result.message += "\nThe launcher could not restart. Open it manually.";
|
||||
WriteResult(source.parent_path(), target, result);
|
||||
std::cerr << result.message << '\n';
|
||||
return 1;
|
||||
}
|
||||
if (result.success) {
|
||||
CleanupWorkspace(source, target);
|
||||
}
|
||||
return result.success ? 0 : 1;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
#ifdef _WIN32
|
||||
int wmain(int argc, wchar_t* argv[]) {
|
||||
#else
|
||||
int main(int argc, char* argv[]) {
|
||||
#endif
|
||||
try {
|
||||
return Run(std::vector<fs::path>(argv, argv + argc));
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,27 @@
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <new>
|
||||
#include <thread>
|
||||
|
||||
namespace {
|
||||
std::filesystem::path content_root;
|
||||
}
|
||||
|
||||
namespace Libs::LibKernel {
|
||||
int KYTY_SYSV_ABI PthreadCreate(Pthread*, const PthreadAttr*, pthread_entry_func_t, void*, const char*) {
|
||||
std::abort();
|
||||
struct PthreadPrivate {
|
||||
std::thread thread;
|
||||
};
|
||||
int KYTY_SYSV_ABI PthreadCreate(Pthread* thread, const PthreadAttr*, pthread_entry_func_t entry,
|
||||
void* arg, const char*) {
|
||||
*thread = new PthreadPrivate {std::thread([=] { entry(arg); })};
|
||||
return 0;
|
||||
}
|
||||
int KYTY_SYSV_ABI PthreadJoin(Pthread thread, void**) {
|
||||
thread->thread.join();
|
||||
delete thread;
|
||||
return 0;
|
||||
}
|
||||
int KYTY_SYSV_ABI PthreadJoin(Pthread, void**) { std::abort(); }
|
||||
namespace FileSystem {
|
||||
std::filesystem::path GetRealFilename(const std::string& path) {
|
||||
return path.starts_with("/app0/") ? content_root / path.substr(6) : std::filesystem::path {};
|
||||
@@ -82,6 +93,76 @@ int KYTY_SYSV_ABI Read(void* object, uint8_t* buffer, uint64_t offset, uint32_t
|
||||
return static_cast<int>(bytes);
|
||||
}
|
||||
uint64_t KYTY_SYSV_ABI Size(void* object) { return static_cast<Callbacks*>(object)->data.size(); }
|
||||
|
||||
struct VideoMemory {
|
||||
unsigned allocations = 0;
|
||||
unsigned deallocations = 0;
|
||||
void* protected_buffer = nullptr;
|
||||
bool released_protected = false;
|
||||
};
|
||||
void* KYTY_SYSV_ABI Allocate(void* object, uint32_t alignment, uint32_t size) {
|
||||
Check(alignment == 0x100, "video allocation alignment");
|
||||
++static_cast<VideoMemory*>(object)->allocations;
|
||||
return ::operator new(size, std::align_val_t(alignment), std::nothrow);
|
||||
}
|
||||
void KYTY_SYSV_ABI Deallocate(void* object, void* data) {
|
||||
auto& memory = *static_cast<VideoMemory*>(object);
|
||||
++memory.deallocations;
|
||||
memory.released_protected |= data == memory.protected_buffer;
|
||||
::operator delete(data, std::align_val_t(0x100));
|
||||
}
|
||||
|
||||
void CheckVideoReadAhead(const std::filesystem::path& root) {
|
||||
// A 16x16 red MPEG-4 intra frame at 30 Hz; repeat it with increasing VOP times.
|
||||
std::array<uint8_t, 50> frame {
|
||||
0x00, 0x00, 0x01, 0xb0, 0x01, 0x00, 0x00, 0x01, 0xb5, 0x89,
|
||||
0x13, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x20, 0x00,
|
||||
0xc4, 0x8d, 0x88, 0x00, 0xf5, 0x00, 0x84, 0x02, 0x14, 0x63,
|
||||
0x00, 0x00, 0x01, 0xb3, 0x00, 0x10, 0x07, 0x00, 0x00, 0x01,
|
||||
0xb6, 0x10, 0x60, 0x51, 0x85, 0x06, 0xd8, 0x2c, 0x81, 0xe0};
|
||||
{
|
||||
Common::File output;
|
||||
Check(output.Create(root / "movie.m4v"), "create video fixture");
|
||||
for (unsigned i = 0; i < 24; ++i) {
|
||||
frame[41] = static_cast<uint8_t>(0x10 + i / 2);
|
||||
frame[42] = i % 2 == 0 ? 0x60 : 0xe0;
|
||||
output.Write(frame.data(), frame.size());
|
||||
}
|
||||
}
|
||||
VideoMemory memory;
|
||||
const AvPlayerMemAllocator allocator {&memory, Allocate, Deallocate, Allocate, Deallocate};
|
||||
Source source(allocator, {}, {}, 6, false, 0);
|
||||
Check(source.Init("/app0/movie.m4v", AvPlayerSourceFileMp4) == 0, "open video fixture");
|
||||
source.SetSync(1);
|
||||
Check(source.Start() == 0, "start video workers");
|
||||
AvPlayerFrameInfoEx info {};
|
||||
for (unsigned i = 0; i < 8; ++i) {
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1);
|
||||
while (!source.Video(&info)) {
|
||||
Check(std::chrono::steady_clock::now() < deadline, "decode steady-state video");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
}
|
||||
// Let the producer refill, then model the game's immediate late-frame retries.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
for (unsigned i = 0; i < 5; ++i) {
|
||||
const auto previous = info.time_stamp;
|
||||
Check(source.Video(&info), "six output buffers keep five video frames ready");
|
||||
Check(info.time_stamp > previous, "video retries return successive frames");
|
||||
}
|
||||
Check(source.Pause() == 0, "pause video");
|
||||
memory.protected_buffer = info.data;
|
||||
std::array<uint8_t, 256> current {};
|
||||
std::memcpy(current.data(), info.data, current.size());
|
||||
Check(!source.Video(&info) && info.data == memory.protected_buffer,
|
||||
"failed video get preserves output");
|
||||
Check(source.Start() == 0, "restart video with retained current output");
|
||||
Check(!memory.released_protected &&
|
||||
std::memcmp(current.data(), memory.protected_buffer, current.size()) == 0,
|
||||
"restart preserves output until the next successful video get");
|
||||
Check(source.Stop() == 0 && memory.allocations == memory.deallocations,
|
||||
"stop releases all video buffers");
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
@@ -127,6 +208,8 @@ int main() {
|
||||
FileStreamer missing({});
|
||||
Check(!missing.Init("/unmounted/movie.bin"), "reject unmapped media");
|
||||
}
|
||||
content_root = root;
|
||||
CheckVideoReadAhead(root);
|
||||
std::error_code error;
|
||||
std::filesystem::remove_all(root, error);
|
||||
Check(!error, "release all archive handles");
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
#include <vector>
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <arpa/inet.h>
|
||||
#include <ifaddrs.h>
|
||||
#include <net/if.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
@@ -898,6 +901,11 @@ void CheckAmprOrdering(Loader::SymbolDatabase &symbols, uint32_t file_id) {
|
||||
using WriteCounter = int (KYTY_SYSV_ABI *)(void *, uint8_t, uint8_t, uint64_t,
|
||||
uint8_t, uint32_t);
|
||||
using WriteAddress = int (KYTY_SYSV_ABI *)(void *, volatile uint64_t *, uint64_t);
|
||||
using WriteImmediately = int (KYTY_SYSV_ABI *)(void *, volatile uint64_t *, uint64_t,
|
||||
uint32_t);
|
||||
using MeasureWriteAddress = uint64_t (KYTY_SYSV_ABI *)(volatile uint64_t *, uint64_t);
|
||||
using MeasureWriteImmediately = uint64_t (KYTY_SYSV_ABI *)(volatile uint64_t *, uint64_t,
|
||||
uint32_t);
|
||||
using ReadFile = int (KYTY_SYSV_ABI *)(void *, uint64_t, uint64_t, uint32_t,
|
||||
void *, uint64_t, uint64_t);
|
||||
using WriteKernelEvent = int (KYTY_SYSV_ABI *)(void *, uint64_t, uint64_t, uint64_t,
|
||||
@@ -916,6 +924,9 @@ void CheckAmprOrdering(Loader::SymbolDatabase &symbols, uint32_t file_id) {
|
||||
const auto wait_counter = reinterpret_cast<WaitCounter>(find("cQb8Zr8Q0Y0"));
|
||||
const auto write_counter = reinterpret_cast<WriteCounter>(find("jK+yuYCI7MA"));
|
||||
const auto write_address = reinterpret_cast<WriteAddress>(find("sJXyWHjP-F8"));
|
||||
const auto write_immediately = reinterpret_cast<WriteImmediately>(find("j0+3uJMxYJY"));
|
||||
const auto measure_write_address = reinterpret_cast<MeasureWriteAddress>(find("C+IEj+BsAFM"));
|
||||
const auto measure_write_immediately = reinterpret_cast<MeasureWriteImmediately>(find("4fgtGfXDrFc"));
|
||||
const auto read_file = reinterpret_cast<ReadFile>(find("mQ16-QdKv7k"));
|
||||
const auto write_event = reinterpret_cast<WriteKernelEvent>(find("H896Pt-yB4I"));
|
||||
const auto submit_apr = reinterpret_cast<SubmitApr>(find("ASoW5WE-UPo"));
|
||||
@@ -927,7 +938,7 @@ void CheckAmprOrdering(Loader::SymbolDatabase &symbols, uint32_t file_id) {
|
||||
std::array<uint32_t, 256> data {};
|
||||
};
|
||||
std::array<Buffer, 4> buffers;
|
||||
const auto reset = [&](size_t apr_count) {
|
||||
const auto reset = [&](size_t apr_count, uint32_t data_size = sizeof(Buffer::data)) {
|
||||
for (size_t i = 0; i < buffers.size(); ++i) {
|
||||
auto &buffer = buffers[i];
|
||||
construct(buffer.header.data());
|
||||
@@ -936,10 +947,60 @@ void CheckAmprOrdering(Loader::SymbolDatabase &symbols, uint32_t file_id) {
|
||||
} else {
|
||||
construct_amm(buffer.header.data());
|
||||
}
|
||||
Check(set_buffer(buffer.header.data(), buffer.data.data(), sizeof(buffer.data)) == OK,
|
||||
Check(set_buffer(buffer.header.data(), buffer.data.data(), data_size) == OK,
|
||||
"initialize AMPR command buffer");
|
||||
}
|
||||
};
|
||||
struct LiteralWrite { uint64_t value; uint32_t size; };
|
||||
constexpr std::array literal_writes {
|
||||
LiteralWrite{0, 8}, LiteralWrite{3, 8}, LiteralWrite{4, 12},
|
||||
LiteralWrite{(uint64_t{1} << 34) - 1, 12},
|
||||
LiteralWrite{uint64_t{1} << 34, 16}, LiteralWrite{UINT64_MAX, 16}};
|
||||
for (const auto &literal : literal_writes) {
|
||||
for (const bool immediately : std::array{false, true}) {
|
||||
reset(1, literal.size);
|
||||
auto *command_buffer = buffers[0].header.data();
|
||||
uint64_t fence = ~literal.value;
|
||||
const auto append = [&](uint64_t value) {
|
||||
return immediately ? write_immediately(command_buffer, &fence, value, 1)
|
||||
: write_address(command_buffer, &fence, value);
|
||||
};
|
||||
Check(measure_write_address(&fence, literal.value) == literal.size &&
|
||||
measure_write_immediately(&fence, literal.value, 1) == literal.size &&
|
||||
append(literal.value) == OK && offset(command_buffer) == literal.size,
|
||||
"literal write fits exactly its measured command size");
|
||||
Check(append(~literal.value) == Libs::LibKernel::KERNEL_ERROR_EBUSY &&
|
||||
offset(command_buffer) == literal.size,
|
||||
"rejected write preserves the full command buffer offset");
|
||||
Result result {1234, 5678};
|
||||
uint32_t submission = 0;
|
||||
Check(submit_apr(command_buffer, 3, &result, &submission) == OK &&
|
||||
wait_apr(submission) == OK && result.result == OK &&
|
||||
fence == literal.value,
|
||||
"bounded write preserves all sixty-four literal bits");
|
||||
}
|
||||
}
|
||||
reset(1, 768);
|
||||
std::array<std::array<char, 3>, 16> batch_output {};
|
||||
std::array<uint64_t, 16> batch_fences {};
|
||||
auto *batch = buffers[0].header.data();
|
||||
for (size_t i = 0; i < batch_fences.size(); ++i) {
|
||||
Check(read_file(batch, reinterpret_cast<uint64_t>(&buffers[0].header[3]),
|
||||
reinterpret_cast<uint64_t>(&buffers[0].header[4]), file_id,
|
||||
batch_output[i].data(), batch_output[i].size(), 0) == OK &&
|
||||
write_address(batch, &batch_fences[i], i + 1) == OK,
|
||||
"sixteen APR read/fence pairs fit the guest's 768-byte command buffer");
|
||||
}
|
||||
Result batch_result {1234, 5678};
|
||||
uint32_t batch_submission = 0;
|
||||
Check(submit_apr(batch, 3, &batch_result, &batch_submission) == OK &&
|
||||
wait_apr(batch_submission) == OK && batch_result.result == OK,
|
||||
"complete the bounded APR read batch");
|
||||
for (size_t i = 0; i < batch_fences.size(); ++i) {
|
||||
Check(batch_fences[i] == i + 1 &&
|
||||
std::memcmp(batch_output[i].data(), "APR", 3) == 0,
|
||||
"every read publishes its data and completion, including requests fourteen and fifteen");
|
||||
}
|
||||
// WaitCompare order: ==, unsigned >/<, !=, wrapped >=, signed >/<.
|
||||
struct Comparison { uint8_t compare; uint64_t blocked, reference, released; };
|
||||
constexpr std::array comparisons {
|
||||
@@ -1401,6 +1462,84 @@ void CheckSocketReceiveBuffer(int reader, int writer) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__linux__)
|
||||
void KYTY_SYSV_ABI RecordNetCtlEvent(int event, void* arg) {
|
||||
auto& events = *static_cast<std::array<int, 2>*>(arg);
|
||||
++events[0];
|
||||
events[1] = event;
|
||||
}
|
||||
|
||||
void TestNetCtlHostIpv4() {
|
||||
using namespace Libs::Network::NetCtl;
|
||||
ifaddrs* interfaces = nullptr;
|
||||
Check(getifaddrs(&interfaces) == 0, "enumerate host IPv4 addresses");
|
||||
std::vector<uint32_t> host_addresses;
|
||||
for (auto* interface = interfaces; interface != nullptr; interface = interface->ifa_next) {
|
||||
if (interface->ifa_addr == nullptr || interface->ifa_addr->sa_family != AF_INET ||
|
||||
(interface->ifa_flags & IFF_UP) == 0 || (interface->ifa_flags & IFF_RUNNING) == 0 ||
|
||||
(interface->ifa_flags & IFF_LOOPBACK) != 0) {
|
||||
continue;
|
||||
}
|
||||
const auto address = reinterpret_cast<const sockaddr_in*>(interface->ifa_addr)->sin_addr;
|
||||
if (address.s_addr != 0 && (ntohl(address.s_addr) >> 24) != 127) {
|
||||
host_addresses.push_back(address.s_addr);
|
||||
}
|
||||
}
|
||||
freeifaddrs(interfaces);
|
||||
|
||||
Config::ConfigOptions options;
|
||||
options.printf_direction = Config::LogDirection::Silent;
|
||||
Check(!options.network_connected, "host networking requires explicit opt-in");
|
||||
alignas(uint32_t) std::array<char, 256> storage {};
|
||||
auto* info = reinterpret_cast<NetCtlInfo*>(storage.data());
|
||||
constexpr int NotConnected = static_cast<int>(0x80412108u);
|
||||
Check(NetCtlGetInfo(14, nullptr) == static_cast<int>(0x80412107u) &&
|
||||
NetCtlGetInfo(0, info) == static_cast<int>(0x80412106u) &&
|
||||
NetCtlGetInfo(22, info) == static_cast<int>(0x80412106u),
|
||||
"NetCtl reports invalid output addresses and information codes");
|
||||
for (bool enabled : {false, true}) {
|
||||
options.network_connected = enabled;
|
||||
Config::Load(options);
|
||||
Check(NetCtlInit() == OK, "initialize NetCtl for selected network mode");
|
||||
const bool connected = enabled && !host_addresses.empty();
|
||||
int state = -1;
|
||||
Check(NetCtlGetState(&state) == OK && state == (connected ? 3 : 0),
|
||||
"NetCtl IPv4 state matches opt-in and host connectivity");
|
||||
Check(NetCtlGetStateV6(&state) == OK && state == 0,
|
||||
"IPv4 connectivity does not advertise IPv6 support");
|
||||
|
||||
Check(NetCtlGetInfo(14, info) == (connected ? OK : NotConnected),
|
||||
"NetCtl IPv4 query respects network mode and host state");
|
||||
in_addr address {};
|
||||
if (connected) {
|
||||
Check(inet_pton(AF_INET, storage.data(), &address) == 1 &&
|
||||
std::find(host_addresses.begin(), host_addresses.end(), address.s_addr) !=
|
||||
host_addresses.end(),
|
||||
"NetCtl reports an IPv4 address assigned to an active host interface");
|
||||
}
|
||||
Check(NetCtlGetInfo(15, info) == (connected ? OK : NotConnected) &&
|
||||
(!connected || inet_pton(AF_INET, storage.data(), &address) == 1),
|
||||
"NetCtl returns the active IPv4 netmask only in connected mode");
|
||||
const int gateway_result = NetCtlGetInfo(16, info);
|
||||
Check(gateway_result == NotConnected ||
|
||||
(connected && gateway_result == OK &&
|
||||
inet_pton(AF_INET, storage.data(), &address) == 1),
|
||||
"NetCtl reports a valid IPv4 gateway or its absence");
|
||||
|
||||
std::array<int, 2> events {};
|
||||
int callback_id = -1;
|
||||
Check(NetCtlRegisterCallback(RecordNetCtlEvent, &events, &callback_id) == OK &&
|
||||
NetCtlCheckCallback() == OK && events[0] == 1 &&
|
||||
events[1] == (connected ? 3 : 1) && NetCtlCheckCallback() == OK && events[0] == 1,
|
||||
"NetCtl delivers the matching connection event once while state is unchanged");
|
||||
Check(NetCtlUnregisterCallback(callback_id) == OK, "release NetCtl callback");
|
||||
NetCtlTerm();
|
||||
}
|
||||
options.network_connected = false;
|
||||
Config::Load(options);
|
||||
}
|
||||
#endif
|
||||
|
||||
void CheckEtherAddressFormatting() {
|
||||
Loader::SymbolDatabase symbols;
|
||||
Libs::LibNet::InitNet_1_Net(&symbols);
|
||||
@@ -1732,6 +1871,9 @@ int main(int, char**) {
|
||||
FileSystem::Shutdown();
|
||||
CheckSocketWakeup();
|
||||
CheckEtherAddressFormatting();
|
||||
#if defined(__linux__)
|
||||
TestNetCtlHostIpv4();
|
||||
#endif
|
||||
TestNpWebApi2Memory();
|
||||
TestNpCommerceDialog();
|
||||
graphics.reset();
|
||||
|
||||
@@ -168,6 +168,117 @@ void TestMappedSrtUsesDirectReaderByDefault() {
|
||||
"cache rematerialization did not use the direct reader by default");
|
||||
}
|
||||
|
||||
void TestNativeNestedSrtRefresh() {
|
||||
using namespace Libs::Graphics::ShaderRecompiler::IR;
|
||||
std::array<uint32_t, 3> first{0u, 0x12345678u, 0xabcdef01u};
|
||||
std::array<uint32_t, 3> second{0u, 0x87654321u, 0x10fedcbau};
|
||||
const auto pointer_words = [](const void *pointer) {
|
||||
const auto address = reinterpret_cast<uint64_t>(pointer);
|
||||
return std::array<uint32_t, 2>{static_cast<uint32_t>(address) | 3u,
|
||||
static_cast<uint32_t>(address >> 32u) | 0xabcd0000u};
|
||||
};
|
||||
auto table = pointer_words(first.data());
|
||||
const auto user_data = pointer_words(table.data());
|
||||
auto program = SrtProgram(0u);
|
||||
program.user_data_base = 4;
|
||||
program.user_data_count = 2;
|
||||
auto &block = *program.blocks[0];
|
||||
const auto emit = [&](ValueOpcode opcode, std::initializer_list<Value> args) {
|
||||
return Value(&block.AppendNewInst(opcode, args));
|
||||
};
|
||||
const auto low = emit(ValueOpcode::GetUserData, {Value(ScalarReg(4))});
|
||||
const auto high = emit(ValueOpcode::GetUserData, {Value(ScalarReg(5))});
|
||||
const auto root = emit(ValueOpcode::GetAddressResource, {low, high});
|
||||
const auto read = [&](Value address, uint32_t offset) {
|
||||
const auto value = emit(ValueOpcode::LoadAddressU32,
|
||||
{address, Value(offset), Value(0u), Value(true)});
|
||||
value.Instruction()->SetFlags(MemoryFlags{.index = 0});
|
||||
program.srt_reads.push_back(
|
||||
{value, static_cast<uint32_t>(program.srt_reads.size())});
|
||||
return value;
|
||||
};
|
||||
const auto pointer_low = read(root, 0u);
|
||||
const auto pointer_high = read(root, 4u);
|
||||
auto *leaf = program.srt_reads[0].value.Instruction();
|
||||
leaf->Arg(0).Instruction()->SetArg(0, pointer_low);
|
||||
leaf->Arg(0).Instruction()->SetArg(1, pointer_high);
|
||||
leaf->SetArg(1, Value(7u));
|
||||
read(leaf->Arg(0), 11u);
|
||||
auto plan = ExtractResourcePlan(program);
|
||||
Check(plan.srt_code != nullptr, "nested user-data tree did not compile an x86 walker");
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
const SrtRuntime runtime{.user_data = user_data};
|
||||
{
|
||||
SrtWalker walker(plan, runtime);
|
||||
Check(walker.RefreshFlatBuffer(snapshot.flattened_srt),
|
||||
"native SRT refresh failed");
|
||||
const auto loaded = first[1];
|
||||
first[1] ^= 0xffffffffu;
|
||||
uint32_t value = 0;
|
||||
Check(walker.Evaluate(plan.srt_reads[0].value, value) && value == loaded,
|
||||
"descriptor evaluation reloaded a value already copied by the native walker");
|
||||
first[1] = loaded;
|
||||
}
|
||||
for (const auto *data : {&first, &second}) {
|
||||
table = pointer_words(data->data());
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
snapshot.flattened_srt ==
|
||||
std::vector<uint32_t>{(*data)[1], table[0], table[1], (*data)[2]},
|
||||
"native SRT refresh lost pointer masking, alignment, or current values");
|
||||
}
|
||||
Check(!MaterializeResources(plan, {.user_data = std::span(user_data).first(1)},
|
||||
snapshot, specialization),
|
||||
"native SRT walk accepted truncated user data");
|
||||
const std::array<uint32_t, 2> fake_user_data{0x1003u, 0xabcd0000u};
|
||||
uint32_t reads = 0;
|
||||
const SrtRuntime callback_runtime{
|
||||
.user_data = fake_user_data,
|
||||
.read_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
|
||||
++*static_cast<uint32_t *>(data);
|
||||
switch (address) {
|
||||
case 0x1000u: words[0] = 0x2003u; break;
|
||||
case 0x1004u: words[0] = 0xabcd0000u; break;
|
||||
case 0x2004u: words[0] = 0x1234u; break;
|
||||
case 0x2008u: words[0] = 0x5678u; break;
|
||||
default: return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
.userdata = &reads};
|
||||
Check(MaterializeResources(plan, callback_runtime, snapshot, specialization) && reads == 4 &&
|
||||
snapshot.flattened_srt ==
|
||||
std::vector<uint32_t>{0x1234u, 0x2003u, 0xabcd0000u, 0x5678u},
|
||||
"native-eligible SRT bypassed its callback or repeated nested pointer reads");
|
||||
leaf->SetArg(0, root);
|
||||
leaf->SetArg(1, Value(0u));
|
||||
program.srt_reads.resize(1);
|
||||
for (const int32_t offset : {-1, 7}) {
|
||||
program.memory_info[0].offset = static_cast<uint32_t>(offset);
|
||||
auto bounded = ExtractResourcePlan(program);
|
||||
auto base = pointer_words(second.data() + 1);
|
||||
Check(bounded.srt_code != nullptr &&
|
||||
MaterializeResources(bounded, {.user_data = base}, snapshot, specialization) &&
|
||||
snapshot.flattened_srt[0] == second[offset < 0 ? 0 : 2],
|
||||
"native SRT applied a signed immediate offset incorrectly");
|
||||
base = offset < 0 ? std::array<uint32_t, 2>{0u, 0u}
|
||||
: std::array<uint32_t, 2>{0xfffffffcu, 0xffffu};
|
||||
Check(!MaterializeResources(bounded, {.user_data = base}, snapshot, specialization),
|
||||
"native SRT accepted address underflow or 48-bit overflow");
|
||||
}
|
||||
high.Instruction()->SetArg(0, Value(ScalarReg(6)));
|
||||
Check(ExtractResourcePlan(program).srt_code == nullptr,
|
||||
"native SRT accepted nonadjacent user-data pointer words");
|
||||
high.Instruction()->SetArg(0, Value(ScalarReg(5)));
|
||||
program.memory_info[0].offset = 0;
|
||||
program.srt_reads.push_back({pointer_low, 1});
|
||||
program.srt_reads.push_back({pointer_high, 2});
|
||||
leaf->SetArg(0, emit(ValueOpcode::GetAddressResource, {pointer_low, pointer_high}));
|
||||
pointer_high.Instruction()->SetArg(1, Value(8u));
|
||||
Check(ExtractResourcePlan(program).srt_code == nullptr,
|
||||
"native SRT accepted nonadjacent nested pointer words");
|
||||
}
|
||||
|
||||
void TestIntegerRuntimeValueFollowsSrtReads() {
|
||||
using namespace Libs::Graphics::ShaderRecompiler::IR;
|
||||
auto plan = SrtProgram(0x10000);
|
||||
@@ -243,7 +354,7 @@ void TestSrtAliasesRetainReadPolicy() {
|
||||
program.descriptor_sources.push_back({.dwords = {Value(&first), Value(0u)},
|
||||
.dword_count = 2});
|
||||
DescriptorSource indirect;
|
||||
indirect.indirect_descriptor.emplace().sources = {0, 1};
|
||||
indirect.indirect_descriptor.emplace(DescriptorSource::IndirectDescriptor{}).sources = {0, 1};
|
||||
program.descriptor_sources.push_back(indirect);
|
||||
auto plan = ExtractResourcePlan(program);
|
||||
for (const auto &inst : plan.value_storage) {
|
||||
@@ -256,6 +367,8 @@ void TestSrtAliasesRetainReadPolicy() {
|
||||
plan.srt_reads[1].value.Instruction()->Flags<SrtReadFlags>().clean == 0u &&
|
||||
plan.srt_reads[2].value.Instruction()->Flags<SrtReadFlags>().clean == 1u,
|
||||
"alias normalization lost read identity or retained discarded EXEC provenance");
|
||||
Check(plan.srt_code == nullptr,
|
||||
"captured strict reads compiled an unused x86 walker");
|
||||
struct Reads { uint32_t strict = 0; uint32_t ordinary = 0; bool dirty = false; } reads;
|
||||
const SrtRuntime runtime{
|
||||
.read_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
|
||||
@@ -527,6 +640,64 @@ void TestFailedMaterializationRejectsStage() {
|
||||
"missing runtime user data did not reject the cached stage");
|
||||
}
|
||||
|
||||
void TestSharedControlPredicates() {
|
||||
using namespace Libs::Graphics::ShaderRecompiler::IR;
|
||||
using Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind;
|
||||
constexpr uint32_t depth = 8;
|
||||
for (const auto operation : {ValueOpcode::LogicalAnd, ValueOpcode::LogicalOr}) {
|
||||
Program program;
|
||||
program.user_data_count = 1;
|
||||
program.srt_plan_complete = program.resource_tracking_complete = true;
|
||||
auto &block = AddValueBlock(program);
|
||||
const auto emit = [&](ValueOpcode opcode, std::initializer_list<Value> args) {
|
||||
return Value(&block.AppendNewInst(opcode, args));
|
||||
};
|
||||
const auto user = emit(ValueOpcode::GetUserData, {Value(ScalarReg(0))});
|
||||
const auto uniform = emit(ValueOpcode::INotEqual32, {user, Value(0u)});
|
||||
const auto lane = emit(ValueOpcode::GetBuiltin,
|
||||
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(0u)});
|
||||
auto varying = emit(ValueOpcode::INotEqual32, {lane, Value(0u)});
|
||||
// Both failed and partially uniform subgraphs share their operands exponentially.
|
||||
for (uint32_t i = 0; i < depth; ++i)
|
||||
varying = emit(ValueOpcode::LogicalOr, {varying, varying});
|
||||
auto condition = emit(operation, {varying, uniform});
|
||||
for (uint32_t i = 0; i < depth; ++i)
|
||||
condition = emit(ValueOpcode::LogicalOr, {condition, condition});
|
||||
condition = emit(ValueOpcode::LogicalNot, {condition});
|
||||
condition = emit(ValueOpcode::ConditionRef, {condition});
|
||||
for (uint32_t i = 0; i < 3; ++i) AddValueBlock(program);
|
||||
for (uint32_t i = 0; i < 2; ++i) {
|
||||
program.blocks[i]->condition = condition;
|
||||
program.blocks[i]->terminator.kind = TerminatorKind::ConditionalBranch;
|
||||
program.blocks[i]->terminator.true_block = program.blocks[i == 0 ? 1 : 3];
|
||||
program.blocks[i]->terminator.false_block = program.blocks[2];
|
||||
}
|
||||
auto plan = ExtractResourcePlan(program);
|
||||
Check(plan.value_storage.size() == depth + 5 &&
|
||||
plan.control_flow.size() == 4 &&
|
||||
plan.control_flow[0].condition == plan.control_flow[1].condition &&
|
||||
!plan.capture_specialization_reads,
|
||||
"shared predicates expanded into trees or retained varying dependencies");
|
||||
for (const uint32_t flag : {0u, 1u}) {
|
||||
const std::array<uint32_t, 1> user_data{flag};
|
||||
SrtWalker walker(plan, {.user_data = user_data});
|
||||
uint32_t result = 99;
|
||||
const bool conjunction = operation == ValueOpcode::LogicalAnd;
|
||||
const bool known = conjunction ? flag == 0 : flag != 0;
|
||||
Check(walker.Evaluate(plan.control_flow[0].condition, result) == known &&
|
||||
(!known || result == uint32_t{conjunction}),
|
||||
"shared predicate changed its uniform factor or cached runtime values");
|
||||
std::vector<uint32_t> flat;
|
||||
Check(walker.RefreshFlatBuffer(flat) &&
|
||||
plan.visited_blocks == (known
|
||||
? (conjunction ? std::vector<uint8_t>{1, 1, 0, 1}
|
||||
: std::vector<uint8_t>{1, 0, 1, 0})
|
||||
: std::vector<uint8_t>{1, 1, 1, 1}),
|
||||
"shared predicate skipped an unknown branch or followed a rejected branch");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TestFiniteImageRefreshReusesScalarReads() {
|
||||
using namespace Libs::Graphics::ShaderRecompiler::IR;
|
||||
Program program;
|
||||
@@ -671,6 +842,7 @@ void DbgExit(int) { std::abort(); }
|
||||
|
||||
int main() {
|
||||
TestMappedSrtUsesDirectReaderByDefault();
|
||||
TestNativeNestedSrtRefresh();
|
||||
TestIntegerRuntimeValueFollowsSrtReads();
|
||||
TestSrtAliasesRetainReadPolicy();
|
||||
TestUniformVectorDescriptorRead();
|
||||
@@ -679,6 +851,7 @@ int main() {
|
||||
for (const bool memory_condition : {false, true})
|
||||
TestWrittenDescriptorPredicateReads(memory_condition);
|
||||
TestFailedMaterializationRejectsStage();
|
||||
TestSharedControlPredicates();
|
||||
TestFiniteImageRefreshReusesScalarReads();
|
||||
TestMixedSamplerVariantsShareRuntimeDescriptor();
|
||||
std::puts("ResourceMaterializationTests: all cases passed");
|
||||
|
||||
@@ -270,7 +270,7 @@ MakeIndirectImageFixture(bool malformed, uint32_t table_stride = 32, uint32_t ta
|
||||
sample.kind = ResourceKind::Image;
|
||||
sample.image_dimension = Decoder::ImageDimension::Dim2D;
|
||||
const auto sampled = fixture->Emit(ValueOpcode::ImageSampleRaw,
|
||||
{image, sampler, fixture->ImageAddress()},
|
||||
{image, sampler, fixture->ImageAddress(), Value(true)},
|
||||
fixture->AddMemory(sample, 0x10f0));
|
||||
const auto sampled_x =
|
||||
fixture->Emit(ValueOpcode::CompositeExtractU32x4, {sampled, Value(0u)});
|
||||
@@ -486,7 +486,8 @@ void TestWaterfallImageTable() {
|
||||
memory.kind = ResourceKind::Image;
|
||||
memory.image_dimension = Decoder::ImageDimension::Dim2D;
|
||||
const auto value = fixture->Emit(ValueOpcode::ImageSampleRaw,
|
||||
{image, sampler, fixture->ImageAddress()}, fixture->AddMemory(memory, 0x92cu), block);
|
||||
{image, sampler, fixture->ImageAddress(), block == entry ? active : guard},
|
||||
fixture->AddMemory(memory, 0x92cu), block);
|
||||
fixture->Emit(ValueOpcode::ReferenceU32,
|
||||
{fixture->Emit(ValueOpcode::CompositeExtractU32x4, {value, Value(0u)}, 0, block)}, 0, block);
|
||||
};
|
||||
@@ -641,7 +642,7 @@ void TestGuardedDirectImageTable() {
|
||||
MemoryInfo memory;
|
||||
memory.kind = ResourceKind::Image;
|
||||
memory.image_dimension = Decoder::ImageDimension::Dim2D;
|
||||
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
|
||||
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress(), Value(true)},
|
||||
fixture.AddMemory(memory, 0x128));
|
||||
fixture.PlanAndTrack();
|
||||
const auto source = fixture.program.info.images[0].source;
|
||||
@@ -989,7 +990,7 @@ void TestBoundedComputeImageLoop() {
|
||||
sample.kind = ResourceKind::Image;
|
||||
sample.image_dimension = Decoder::ImageDimension::Dim2D;
|
||||
fixture.Emit(ValueOpcode::ImageSampleRaw,
|
||||
{image, sampler, fixture.ImageAddress()},
|
||||
{image, sampler, fixture.ImageAddress(), Value(true)},
|
||||
fixture.AddMemory(sample, 0x29c));
|
||||
fixture.PlanAndTrack();
|
||||
const auto source = fixture.program.info.images[0].source;
|
||||
@@ -1325,7 +1326,9 @@ void TestUniformizedMaterialImageKeys() {
|
||||
MemoryInfo image_memory;
|
||||
image_memory.kind = ResourceKind::Image;
|
||||
image_memory.image_dimension = Decoder::ImageDimension::Dim2D;
|
||||
const auto sampled = fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, address},
|
||||
const auto sampled = fixture.Emit(ValueOpcode::ImageSampleRaw,
|
||||
{image, sampler, address,
|
||||
variant == Variant::EmptyFirstEntry ? material_guard : sample_guard},
|
||||
fixture.AddMemory(image_memory, 0x1aecu), sample);
|
||||
if (first_lane) {
|
||||
const auto component = fixture.Emit(
|
||||
@@ -1477,7 +1480,7 @@ void TestImageDescriptorFields() {
|
||||
memory.kind = ResourceKind::Image;
|
||||
memory.image_dimension = Decoder::ImageDimension::Dim2D;
|
||||
memory.image_r128 = r128;
|
||||
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
|
||||
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress(), Value(true)},
|
||||
fixture.AddMemory(memory, 0x80));
|
||||
fixture.PlanAndTrack();
|
||||
auto plan = ExtractResourcePlan(fixture.program);
|
||||
@@ -1571,7 +1574,7 @@ void TestUniformScalarBufferImage() {
|
||||
sample.kind = ResourceKind::Image;
|
||||
sample.image_dimension = Decoder::ImageDimension::Dim2D;
|
||||
fixture.Emit(ValueOpcode::ImageSampleRaw,
|
||||
{image, sampler, fixture.ImageAddress()}, fixture.AddMemory(sample, 0x228));
|
||||
{image, sampler, fixture.ImageAddress(), Value(true)}, fixture.AddMemory(sample, 0x228));
|
||||
fixture.PlanAndTrack();
|
||||
const auto plan = ExtractResourcePlan(fixture.program);
|
||||
|
||||
@@ -1866,7 +1869,7 @@ void TestImagesSamplersAndAliases() {
|
||||
memory.kind = ResourceKind::Image;
|
||||
memory.image_dimension = Decoder::ImageDimension::Dim2D;
|
||||
memory.image_sample_flags = sample_flags;
|
||||
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, image_address},
|
||||
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, image_address, Value(true)},
|
||||
fixture.AddMemory(memory, pc));
|
||||
return std::pair{image, sampler};
|
||||
};
|
||||
@@ -1938,14 +1941,14 @@ void TestSampleAdjustSamplerScratch() {
|
||||
memory.image_dimension = Decoder::ImageDimension::Dim2D;
|
||||
memory.image_sample_flags = Decoder::ImageSampleFlagAdjust;
|
||||
fixture.Emit(ValueOpcode::ImageSampleRaw,
|
||||
{image, sampler, fixture.ImageAddress()},
|
||||
{image, sampler, fixture.ImageAddress(), Value(true)},
|
||||
fixture.AddMemory(memory, 0x1ec));
|
||||
// PPSA24156's zero border payload leaves the reserved scratch shift as
|
||||
// the entire DWORD after the frontend folds its OR with zero.
|
||||
const auto shift_sampler = fixture.Sampler(
|
||||
{Value(0x36u), Value(0xfff000u), Value(0x02500000u), scratch}, 0x200);
|
||||
fixture.Emit(ValueOpcode::ImageSampleRaw,
|
||||
{image, shift_sampler, fixture.ImageAddress()},
|
||||
{image, shift_sampler, fixture.ImageAddress(), Value(true)},
|
||||
fixture.AddMemory(memory, 0x200));
|
||||
fixture.PlanAndTrack();
|
||||
auto plan = ExtractResourcePlan(fixture.program);
|
||||
@@ -1994,7 +1997,7 @@ void TestSampleAdjustSamplerScratch() {
|
||||
rejected_memory.image_dimension = Decoder::ImageDimension::Dim2D;
|
||||
rejected_memory.image_sample_flags = flags;
|
||||
rejected.Emit(ValueOpcode::ImageSampleRaw,
|
||||
{rejected_image, rejected_sampler, rejected.ImageAddress()},
|
||||
{rejected_image, rejected_sampler, rejected.ImageAddress(), Value(true)},
|
||||
rejected.AddMemory(rejected_memory, 0x200));
|
||||
|
||||
CheckFatal([&] { rejected.PlanAndTrack(); },
|
||||
@@ -2112,7 +2115,7 @@ void TestGatherLodSamplerValidation() {
|
||||
fixture.Emit(ValueOpcode::ImageGatherRaw,
|
||||
{fixture.Image(image_words), fixture.Sampler(sampler_words),
|
||||
fixture.ImageAddress(), image_words[1], image_words[3],
|
||||
sampler_words[1], sampler_words[2]},
|
||||
sampler_words[1], sampler_words[2], Value(true)},
|
||||
fixture.AddMemory(memory, i * 4));
|
||||
}
|
||||
fixture.PlanAndTrack();
|
||||
@@ -2514,7 +2517,7 @@ ResourcePlan ConditionalSamplerPlan(bool diamond, bool reverse, bool reverse_phi
|
||||
sample.kind = ResourceKind::Image;
|
||||
sample.image_dimension = Decoder::ImageDimension::Dim2D;
|
||||
const auto result =
|
||||
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, address},
|
||||
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, address, Value(true)},
|
||||
fixture.AddMemory(sample, 0x4d4));
|
||||
fixture.Emit(ValueOpcode::ReferenceU32,
|
||||
{fixture.Emit(ValueOpcode::CompositeExtractU32x4,
|
||||
@@ -2619,7 +2622,7 @@ void TestFiniteImagePhiCycle() {
|
||||
MemoryInfo memory;
|
||||
memory.kind = ResourceKind::Image;
|
||||
memory.image_dimension = Decoder::ImageDimension::Dim2D;
|
||||
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
|
||||
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress(), Value(true)},
|
||||
fixture.AddMemory(memory, 4));
|
||||
fixture.PlanAndTrack();
|
||||
auto plan = ExtractResourcePlan(fixture.program);
|
||||
@@ -2685,12 +2688,12 @@ void TestFiniteInlineSampler() {
|
||||
memory.kind = ResourceKind::Image;
|
||||
memory.image_dimension = Decoder::ImageDimension::Dim2D;
|
||||
if (format == F::k32Float)
|
||||
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, previous, fixture.ImageAddress()},
|
||||
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, previous, fixture.ImageAddress(), Value(true)},
|
||||
fixture.AddMemory(memory, 0x440u));
|
||||
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
|
||||
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress(), Value(true)},
|
||||
fixture.AddMemory(memory, 0x1dd0u));
|
||||
if (format == F::k32Float)
|
||||
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, direct, fixture.ImageAddress()},
|
||||
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, direct, fixture.ImageAddress(), Value(true)},
|
||||
fixture.AddMemory(memory, 0x1dd8u));
|
||||
}
|
||||
fixture.PlanAndTrack();
|
||||
@@ -2764,7 +2767,7 @@ void TestFiniteInlineSampler() {
|
||||
memory.kind = ResourceKind::Image;
|
||||
memory.image_dimension = Decoder::ImageDimension::Dim2D;
|
||||
memory.image_sample_flags = flags;
|
||||
adjusted.Emit(ValueOpcode::ImageSampleRaw, {image, shared, adjusted.ImageAddress()},
|
||||
adjusted.Emit(ValueOpcode::ImageSampleRaw, {image, shared, adjusted.ImageAddress(), Value(true)},
|
||||
adjusted.AddMemory(memory, 0x1dd0u));
|
||||
}
|
||||
CheckFatal([&] { adjusted.PlanAndTrack(); }, "not a valid runtime value",
|
||||
@@ -2817,7 +2820,7 @@ void TestFiniteImageBitScanSentinel() {
|
||||
MemoryInfo memory;
|
||||
memory.kind = ResourceKind::Image;
|
||||
memory.image_dimension = Decoder::ImageDimension::Dim2D;
|
||||
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
|
||||
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress(), Value(true)},
|
||||
fixture.AddMemory(memory, 4));
|
||||
if (!nonzero) {
|
||||
CheckFatal([&] { fixture.PlanAndTrack(); }, "not a valid runtime value",
|
||||
|
||||
@@ -11083,6 +11083,78 @@ public:
|
||||
context.ShutdownGpu();
|
||||
LibKernel::Memory::InstallGpuResources(nullptr);
|
||||
}
|
||||
{
|
||||
// PPSA01593 samples a DCC R32 color surface through a comparison sampler.
|
||||
constexpr uint64_t comparison_dcc = base + 0x10000;
|
||||
std::memset(mapped, 0, 0x10000);
|
||||
std::memset(reinterpret_cast<void *>(comparison_dcc), 0xff, 0x1000);
|
||||
RenderContext context(m_runtime_context);
|
||||
context.InitializeGpu(nullptr);
|
||||
LibKernel::Memory::InstallGpuResources(&context);
|
||||
auto &scheduler = context.GetCommandScheduler();
|
||||
HW::Context registers{};
|
||||
HW::UserConfig user_config{};
|
||||
HW::Shader shaders{};
|
||||
scheduler.Begin(registers, user_config, shaders);
|
||||
context.MapMemory(base, allocation_size);
|
||||
auto &cache = context.GetTextureCache();
|
||||
auto &executor = context.GetRenderExecutor();
|
||||
const ShaderTextureResource descriptor{{static_cast<uint32_t>(base >> 8u),
|
||||
0xc1600000u, 0x000fc00fu, 0x91b00204u, 0, 0x00700000u,
|
||||
0x002b0000u | (static_cast<uint32_t>(comparison_dcc >> 8u) << 24u),
|
||||
static_cast<uint32_t>(comparison_dcc >> 16u)}};
|
||||
ShaderRecompiler::IR::DescriptorValue value{};
|
||||
value.dword_count = 8;
|
||||
std::copy_n(descriptor.fields, 8, value.dwords.begin());
|
||||
ShaderRecompiler::IR::ImageResource resource{};
|
||||
resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Storage;
|
||||
resource.numeric_class = Prospero::TextureNumericClass::Float;
|
||||
resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D;
|
||||
resource.written = true;
|
||||
const auto color = RenderExecutorTestAccess::ResolveTexture(executor, resource, value);
|
||||
(void)cache.FindTexture(color.image_id, color.desc);
|
||||
vk::ClearValue paint{};
|
||||
paint.color.float32 = std::array{0.375f, 0.f, 0.f, 0.f};
|
||||
TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), color.image_id,
|
||||
{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, paint);
|
||||
resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Sampled;
|
||||
resource.written = false;
|
||||
resource.read = true;
|
||||
resource.depth_compare = true;
|
||||
auto comparison = RenderExecutorTestAccess::ResolveTexture(executor, resource, value);
|
||||
(void)cache.FindTexture(comparison.image_id, comparison.desc);
|
||||
const auto &depth = cache.GetImage(comparison.image_id);
|
||||
Require(name, "expanded DCC comparison alias",
|
||||
comparison.image_id != color.image_id &&
|
||||
depth.info.pixel_format == vk::Format::eD32Sfloat &&
|
||||
depth.info.guest_format == Prospero::BufferFormat::k32Float &&
|
||||
depth.info.tile_mode == Prospero::TileMode::kRenderTarget &&
|
||||
depth.info.extent == vk::Extent3D{64, 64, 1} &&
|
||||
depth.info.metadata.kind == ImageMetadataKind::Dcc &&
|
||||
depth.info.metadata.range == GuestRange{comparison_dcc, 0x1000} &&
|
||||
ReadCachedTexel(name, context, comparison.image_id) ==
|
||||
std::vector<u32>{std::bit_cast<u32>(0.375f)},
|
||||
"comparison promotion lost native color data or guest DCC layout");
|
||||
for (const u32 code : {0x40404040u, 0x80808080u}) {
|
||||
WriteMetadata(context, comparison_dcc, 0x1000, code);
|
||||
comparison = RenderExecutorTestAccess::ResolveTexture(executor, resource, value);
|
||||
(void)cache.FindTexture(comparison.image_id, comparison.desc);
|
||||
Require(name, "DCC clear on a comparison backing",
|
||||
ReadCachedTexel(name, context, comparison.image_id, {63, 63, 0}) ==
|
||||
std::vector<u32>{code == 0x40404040u ? 0u : 0x3f800000u},
|
||||
"DCC clear used the native depth format or color aspect");
|
||||
std::array<uint8_t, 0x1000> metadata{};
|
||||
Require(name, "comparison clear expands metadata",
|
||||
LibKernel::Memory::TryReadBacking(comparison_dcc, metadata.data(),
|
||||
metadata.size()) &&
|
||||
std::ranges::all_of(metadata, [](uint8_t byte) { return byte == 0xff; }),
|
||||
"comparison clear retained the native DCC clear key");
|
||||
}
|
||||
context.UnmapMemory(base, allocation_size);
|
||||
scheduler.Finish();
|
||||
context.ShutdownGpu();
|
||||
LibKernel::Memory::InstallGpuResources(nullptr);
|
||||
}
|
||||
Require(name, "unmap",
|
||||
Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0,
|
||||
"captured scene mapping release failed");
|
||||
@@ -16439,6 +16511,60 @@ public:
|
||||
wave_ids[0] != wave_ids[1] && wave_keys[0] != wave_keys[1],
|
||||
"wave32 and wave64 pixel programs shared a cache key");
|
||||
|
||||
static const auto waterfall_vertex = [&] {
|
||||
std::vector<u32> code;
|
||||
AppendVMovU32(&code, 9, 0);
|
||||
// d3e1ae284c4dea3b reconstructs EXEC twice from packed NGG s3.
|
||||
code.insert(code.end(), {0xbefe04c1u, 0x906a8803u, 0x81ea6a80u,
|
||||
0x90fe6ac1u, 0x81ea0380u, 0x90fe6ac1u,
|
||||
EncodeSop1(0x04, 10, 126)});
|
||||
const auto loop = code.size();
|
||||
code.push_back(EncodeSop1(0x14, 4, 10));
|
||||
AppendVop3(&code, 0x360, 6, Vgpr(5), 4);
|
||||
code.push_back(EncodeVopc(0xc2, 6, 5));
|
||||
code.push_back(EncodeSop1(0x24, 18, 106));
|
||||
AppendVMovLiteral(&code, 9, 0x3f800000u);
|
||||
code.push_back(EncodeSop2(0x15, 10, 10, 106));
|
||||
code.push_back(EncodeSop1(0x04, 126, 18));
|
||||
code.push_back(EncodeSopp(0x05, static_cast<int16_t>(loop - code.size() - 1u)));
|
||||
const auto base = code.size();
|
||||
code.insert(code.end(), native_vertex.begin(), native_vertex.end());
|
||||
const auto position = std::find(code.begin() + base, code.end(), EncodeExp0(0x0c, 0xf));
|
||||
// Unprocessed vertices keep W=0, making waterfall progress observable.
|
||||
*(position + 1) = EncodeExp1(3, 4, 0, 9);
|
||||
return code;
|
||||
}();
|
||||
native_vertex_regs.es_regs.data_addr = reinterpret_cast<uint64_t>(waterfall_vertex.data());
|
||||
ShaderMapUserData(native_vertex_regs.es_regs.data_addr,
|
||||
{.type = Prospero::ShaderBinaryType::kGs, .user_data = &native_user_data,
|
||||
.code_size_bytes = static_cast<u32>(waterfall_vertex.size() * sizeof(u32))});
|
||||
registers.SetPsInControl(0x8008);
|
||||
const auto saved_stages = registers.GetShaderStages();
|
||||
for (const u32 wave : {32u, 64u}) {
|
||||
registers.SetShaderStages((saved_stages & ~0x00400000u) |
|
||||
(wave == 32u ? 0x00400000u : 0u));
|
||||
const auto programs = context.GetPipelineCache().GetGraphicsPrograms(
|
||||
native_vertex_regs, native_pixel_regs, registers.GetShaderRegisters(),
|
||||
registers, user_config, export_mapping, true, native_vertex_info, pixel);
|
||||
vertex_shader = programs.vertex[0];
|
||||
pixel_shader = programs.pixel;
|
||||
vertex = native_vertex_info[0];
|
||||
Require(name, "native NGG waterfall wave",
|
||||
vertex.stage.program->stage == ShaderType::Vertex &&
|
||||
vertex.stage.program->wave_size == wave,
|
||||
"NGG waterfall bypassed the native vertex path or lost its guest wave width");
|
||||
for (const u32 count : {3u, 6u, 33u}) {
|
||||
draw(pipeline(true, 2, 2), count);
|
||||
const auto pixels = read_color();
|
||||
for (size_t component = 0; component < pixels.size(); ++component)
|
||||
Require(name, "NGG partial-wave waterfall output",
|
||||
pixels[component] == (component % 4 == 3 ? 0x3f800000u : 0x3e800000u),
|
||||
"native vertex waterfall did not terminate with every input vertex processed");
|
||||
}
|
||||
}
|
||||
registers.SetShaderStages(saved_stages);
|
||||
native_vertex_regs.es_regs.data_addr = vertex_address;
|
||||
|
||||
// Reversed RGBA targets must use logical Sa for RGB, with a separate
|
||||
// zero or unit source factor for separate alpha attenuation or accumulation.
|
||||
static const auto blend_pixel = [] {
|
||||
@@ -17744,39 +17870,47 @@ public:
|
||||
}
|
||||
|
||||
// Exercise the real texture-layout/info-building seam for every format
|
||||
// admitted by each standard tile mode. Formats sharing a byte/block width
|
||||
// admitted by each texture tile mode. Formats sharing a byte/block width
|
||||
// intentionally share a shader, but this loop still validates their
|
||||
// texel-to-element conversion (notably every BCn format).
|
||||
struct StandardMode {
|
||||
struct TextureMode {
|
||||
Prospero::TileMode tile;
|
||||
TileBlockFamily family;
|
||||
};
|
||||
constexpr StandardMode standard_modes[] = {
|
||||
constexpr TextureMode texture_modes[] = {
|
||||
{Prospero::TileMode::kStandard256B, TileBlockFamily::Standard256B},
|
||||
{Prospero::TileMode::kStandard4KB, TileBlockFamily::Standard4KB},
|
||||
{Prospero::TileMode::kStandard64KB, TileBlockFamily::Standard64KB},
|
||||
{Prospero::TileMode::kPrt, TileBlockFamily::Prt64KB},
|
||||
{Prospero::TileMode::kRenderTarget, TileBlockFamily::RenderTarget64KB},
|
||||
{Prospero::TileMode::kDepth, TileBlockFamily::Depth64KB},
|
||||
};
|
||||
struct RenderTargetFormatCase {
|
||||
struct TextureTileFormatCase {
|
||||
Prospero::BufferFormat format;
|
||||
u32 bytes_per_element;
|
||||
bool depth_supported;
|
||||
};
|
||||
constexpr RenderTargetFormatCase render_target_formats[] = {
|
||||
{Prospero::BufferFormat::k8Srgb, 1},
|
||||
{Prospero::BufferFormat::k8_8Srgb, 2},
|
||||
{Prospero::BufferFormat::k9_9_9_5Float, 0},
|
||||
constexpr TextureTileFormatCase texture_tile_formats[] = {
|
||||
{Prospero::BufferFormat::k8Srgb, 1, true},
|
||||
{Prospero::BufferFormat::k8_8Srgb, 2, true},
|
||||
{Prospero::BufferFormat::k9_9_9_5Float, 4, true},
|
||||
{Prospero::BufferFormat::kBc1Srgb, 8, false},
|
||||
{Prospero::BufferFormat::kBc7Srgb, 16, false},
|
||||
{Prospero::BufferFormat::k32_32_32_32Float, 16, false},
|
||||
};
|
||||
for (const auto &test : render_target_formats) {
|
||||
for (const auto &test : texture_tile_formats) {
|
||||
for (const auto tile :
|
||||
{Prospero::TileMode::kDepth, Prospero::TileMode::kRenderTarget}) {
|
||||
TileTextureBlockLayout texture{};
|
||||
const bool supported =
|
||||
TileGetTextureBlockLayout(test.format, tile, false, texture);
|
||||
Require(name, "RT format policy",
|
||||
supported == (test.bytes_per_element != 0) &&
|
||||
const bool expected = tile == Prospero::TileMode::kRenderTarget ||
|
||||
test.depth_supported;
|
||||
Require(name, "texture tile format policy",
|
||||
supported == expected &&
|
||||
(!supported || texture.block.bytes_per_element ==
|
||||
test.bytes_per_element),
|
||||
"RT/depth tile format support or element size is incorrect");
|
||||
"texture tile policy used attachment format restrictions");
|
||||
}
|
||||
}
|
||||
{
|
||||
@@ -17809,7 +17943,7 @@ public:
|
||||
if (Prospero::IsFmaskTextureFormat(format)) {
|
||||
continue;
|
||||
}
|
||||
for (const auto &mode : standard_modes) {
|
||||
for (const auto &mode : texture_modes) {
|
||||
TileTextureBlockLayout texture{};
|
||||
if (!TileGetTextureBlockLayout(format, mode.tile, false, texture)) {
|
||||
continue;
|
||||
@@ -17846,7 +17980,27 @@ public:
|
||||
}
|
||||
}
|
||||
Require(name, "format coverage", format_cases != 0,
|
||||
"no CPU-supported standard formats were tested");
|
||||
"no CPU-supported texture formats were tested");
|
||||
|
||||
{
|
||||
constexpr auto format = Prospero::BufferFormat::kBc1Srgb;
|
||||
constexpr auto tile = Prospero::TileMode::kRenderTarget;
|
||||
constexpr u32 layers = 8;
|
||||
const auto captured =
|
||||
MakeTilingImage(format, 2048, 2048, 1, layers, tile, 0, false);
|
||||
Require(name, "PPSA19577 BC1 array footprint",
|
||||
captured.data.size == 0x1000000 &&
|
||||
captured.tiled_slice_stride == 0x200000 &&
|
||||
captured.first_tail_level == 1 &&
|
||||
captured.tiling.block.block_width == 128 &&
|
||||
captured.tiling.block.block_height == 64 &&
|
||||
captured.mip_layout[0].pitch == 512 &&
|
||||
captured.mip_layout[0].height == 512,
|
||||
"R64 BC1 texture lost its compressed blocks or array stride");
|
||||
const auto small_array =
|
||||
MakeTilingImage(format, 67, 51, 1, layers, tile, 0, false);
|
||||
check_round_trip("R64 BC1 array", small_array.data.size, small_array);
|
||||
}
|
||||
|
||||
{
|
||||
constexpr auto format = Prospero::BufferFormat::k11_11_10UInt;
|
||||
@@ -17949,7 +18103,7 @@ public:
|
||||
check_round_trip("array", total.size, layout);
|
||||
}
|
||||
|
||||
for (const auto &mode : standard_modes) {
|
||||
for (const auto &mode : texture_modes) {
|
||||
constexpr auto format = Prospero::BufferFormat::k32Float;
|
||||
constexpr u32 levels = 2;
|
||||
TileBlockLayout block{};
|
||||
@@ -20860,6 +21014,66 @@ TestCase ScalarBrevB32PreservesScc() {
|
||||
O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
|
||||
}
|
||||
|
||||
TestCase ScalarZeroBitOps() {
|
||||
using O = ShaderOpcode;
|
||||
constexpr uint64_t inputs[] = {0, UINT64_MAX, 0xaaaaaaaa55555555ull,
|
||||
0xffffffff7fffffffull, 0xfffffffeffffffffull, 0x7fffffffffffffffull,
|
||||
0x00000000ffffffffull, 0xffffffff00000000ull};
|
||||
TestCase test;
|
||||
test.name = "ScalarZeroBitOps";
|
||||
for (const auto input : inputs)
|
||||
test.initial.insert(test.initial.end(), {u32(input), u32(input >> 32)});
|
||||
test.expected = test.initial;
|
||||
auto &code = test.code;
|
||||
const auto check = [&](u32 opcode, u32 source, uint64_t input, bool scc) {
|
||||
const u32 width = opcode == 0x0e || opcode == 0x12 ? 64 : 32;
|
||||
u32 count = 0, first = UINT32_MAX;
|
||||
for (u32 bit = 0; bit < width; ++bit) {
|
||||
if (((input >> bit) & 1u) == 0) {
|
||||
++count;
|
||||
if (first == UINT32_MAX) first = bit;
|
||||
}
|
||||
}
|
||||
const bool counting = opcode == 0x0d || opcode == 0x0e;
|
||||
code.push_back(EncodeSopc(0x06, InlineU32(1), InlineU32(scc)));
|
||||
code.push_back(EncodeSop1(opcode, 20, source));
|
||||
if (source == 255) code.push_back(u32(input));
|
||||
code.push_back(EncodeSMovB32(22, 253)); // Capture SCC before readback.
|
||||
AppendStoreSgpr(&code, 20, u32(test.expected.size()));
|
||||
AppendStoreSgpr(&code, 21, u32(test.expected.size() + 1));
|
||||
AppendStoreSgpr(&code, 22, u32(test.expected.size() + 2));
|
||||
test.expected.insert(test.expected.end(),
|
||||
{counting ? count : first, u32(input >> 32), u32(counting ? count != 0 : scc)});
|
||||
};
|
||||
for (u32 index = 0; index < std::size(inputs); ++index) {
|
||||
for (u32 word = 0; word < 2; ++word) {
|
||||
AppendVMovU32(&code, 30, (index * 2 + word) * sizeof(u32));
|
||||
AppendBufferLoadDword(&code, 1, 30);
|
||||
code.push_back(EncodeVop1(0x02, 8 + word, Vgpr(1)));
|
||||
}
|
||||
for (const u32 opcode : {0x0du, 0x0eu, 0x11u, 0x12u}) {
|
||||
for (const bool scc : {false, true}) {
|
||||
code.push_back(EncodeSop1(0x04, 20, 8)); // Destination aliases the source.
|
||||
check(opcode, 20, inputs[index], scc);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const u32 opcode : {0x0du, 0x0eu, 0x11u, 0x12u}) {
|
||||
for (const u32 source : {128u, 193u, 255u}) {
|
||||
const uint64_t input = source == 128 ? 0 : source == 193 ? UINT64_MAX : 0xffffffffull;
|
||||
AppendSMovLiteral(&code, 21, u32(input >> 32));
|
||||
check(opcode, source, input, true); // Only inline -1 sign extends to B64.
|
||||
}
|
||||
}
|
||||
AppendEnd(&code);
|
||||
test.initial.resize(test.expected.size());
|
||||
test.opcodes = {O::S_BCNT0_I32_B32, O::S_BCNT0_I32_B64,
|
||||
O::S_FF0_I32_B32, O::S_FF0_I32_B64, O::S_CMP_EQ_U32, O::S_MOV_B32,
|
||||
O::S_MOV_B64, O::V_MOV_B32, O::V_READFIRSTLANE_B32,
|
||||
O::BUFFER_LOAD_DWORD, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase ScalarSextMasks(u32 bits, u32 wave_size) {
|
||||
using O = ShaderOpcode;
|
||||
TestCase test;
|
||||
@@ -22675,6 +22889,51 @@ TestCase Vop2SdwaMaxI32CapturedHighWord(u32 wave_size) {
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase Vop2SdwaMinI32CapturedByte0(u32 wave_size) {
|
||||
using O = ShaderOpcode;
|
||||
struct MinCase { u32 lhs, packed_rhs, expected, exec = 1; };
|
||||
constexpr std::array<MinCase, 9> cases{{
|
||||
{0xfffffffbu, 0x12345678u, 0xfffffffbu},
|
||||
{0x00000064u, 0xffffff80u, 0x00000064u},
|
||||
{0x000000c8u, 0xffffff80u, 0x00000080u},
|
||||
{0x7fffffffu, 0x000000ffu, 0x000000ffu},
|
||||
{0x80000000u, 0x7fffffffu, 0x80000000u},
|
||||
{0x00000100u, 0x00000105u, 0x00000005u},
|
||||
{0xffffffffu, 0x00000000u, 0xffffffffu},
|
||||
{0x00000010u, 0xdeadbe0fu, 0x0000000fu},
|
||||
{0x80000001u, 0x7fff0123u, 0x7fff0123u, 0},
|
||||
}};
|
||||
TestCase test;
|
||||
test.name = wave_size == 64 ? "Vop2SdwaMinI32Byte0Wave64"
|
||||
: "Vop2SdwaMinI32Byte0Wave32";
|
||||
for (const auto &entry : cases) {
|
||||
test.initial.insert(test.initial.end(), {entry.lhs, entry.packed_rhs});
|
||||
}
|
||||
test.initial.resize(cases.size() * 3u, 0xdeadbeefu);
|
||||
test.expected = test.initial;
|
||||
auto &code = test.code;
|
||||
for (u32 i = 0; i < cases.size(); ++i) {
|
||||
AppendVMovU32(&code, 30, i * 8u);
|
||||
AppendBufferLoadDword(&code, 5, 30);
|
||||
AppendVMovU32(&code, 30, i * 8u + 4u);
|
||||
AppendBufferLoadDword(&code, 2, 30);
|
||||
code.push_back(EncodeSMovB32(126, InlineU32(cases[i].exec)));
|
||||
code.insert(code.end(), {0x220404f9u, 0x00060605u});
|
||||
code.push_back(EncodeSMovB32(126, InlineU32(1)));
|
||||
const u32 out = cases.size() * 2u + i;
|
||||
AppendStoreVgpr(&code, 2, out);
|
||||
test.expected[out] = cases[i].expected;
|
||||
}
|
||||
AppendEnd(&code);
|
||||
test.opcodes = {O::V_MOV_B32, O::S_MOV_B32, O::BUFFER_LOAD_DWORD,
|
||||
O::V_MIN_I32, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
test.decoded_counts = {{"V_MIN_I32 v2, v5, v2.sdwa(sel=0,sext=0)", cases.size()}};
|
||||
test.required_spirv = {"OpBitFieldUExtract"};
|
||||
test.compute_info.wave_size = wave_size;
|
||||
test.has_compute_info = true;
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase Vop2SdwaMulI24Destinations(u32 wave_size) {
|
||||
using O = ShaderOpcode;
|
||||
constexpr std::array<u32, 5> sources{
|
||||
@@ -24741,11 +25000,12 @@ TestCase VectorPermlane16FetchInactiveFi() {
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase VectorDpp8Captured(bool masked_exec) {
|
||||
TestCase VectorDpp8Captured(bool masked_exec, u32 wave_size = 64, bool fi = false) {
|
||||
using O = ShaderOpcode;
|
||||
constexpr u32 sentinel = 0xaaaaaaaau;
|
||||
const u32 masks[] = {masked_exec ? 0x0f0f0f0fu : 0xffffffffu,
|
||||
masked_exec ? 0xf0f0f0f0u : 0xffffffffu};
|
||||
// Both wave halves contain active and inactive lane-3 sources for the ADD.
|
||||
const u32 masks[] = {masked_exec ? 0x0f0ff0f0u : 0xffffffffu,
|
||||
masked_exec ? 0xf0f00f0fu : 0xffffffffu};
|
||||
std::vector<u32> code;
|
||||
AppendVMovU32(&code, 5, 100);
|
||||
code.push_back(EncodeVop2(0x25, 5, Vgpr(0), 5));
|
||||
@@ -24754,20 +25014,21 @@ TestCase VectorDpp8Captured(bool masked_exec) {
|
||||
AppendVMovLiteral(&code, 3, sentinel);
|
||||
AppendVMovLiteral(&code, 8, sentinel);
|
||||
AppendSMovLiteral(&code, 126, masks[0]);
|
||||
AppendSMovLiteral(&code, 127, masks[1]);
|
||||
if (wave_size == 64) AppendSMovLiteral(&code, 127, masks[1]);
|
||||
// Captured DPP8 selectors broadcast each group's upper/lower four lanes.
|
||||
code.push_back(EncodeVop1(0x01, 2, 233));
|
||||
const u32 encoding = fi ? 234u : 233u;
|
||||
code.push_back(EncodeVop1(0x01, 2, encoding));
|
||||
code.push_back(0xfacfac05u);
|
||||
code.push_back(EncodeVop1(0x01, 3, 233));
|
||||
code.push_back(EncodeVop1(0x01, 3, encoding));
|
||||
code.push_back(0x68868805u);
|
||||
// CS 6babb2a8915d35c4, pc 0xcc: add lane 3 of each group to its own v7.
|
||||
code.push_back(0x4a100ee9u);
|
||||
code.push_back(EncodeVop2(0x25, 8, encoding, 7));
|
||||
code.push_back(0x6db6db07u);
|
||||
AppendSMovLiteral(&code, 126, 0xffffffffu);
|
||||
AppendSMovLiteral(&code, 127, 0xffffffffu);
|
||||
if (wave_size == 64) AppendSMovLiteral(&code, 127, 0xffffffffu);
|
||||
code.push_back(EncodeVop2(0x1a, 6, InlineU32(2), 0));
|
||||
AppendBufferStoreDword(&code, 2, 6);
|
||||
AppendVMovU32(&code, 7, 64u * sizeof(u32));
|
||||
AppendVMovU32(&code, 7, wave_size * sizeof(u32));
|
||||
code.push_back(EncodeVop2(0x25, 6, Vgpr(7), 6));
|
||||
AppendBufferStoreDword(&code, 3, 6);
|
||||
code.push_back(EncodeVop2(0x25, 6, Vgpr(7), 6));
|
||||
@@ -24775,11 +25036,13 @@ TestCase VectorDpp8Captured(bool masked_exec) {
|
||||
AppendEnd(&code);
|
||||
|
||||
TestCase test;
|
||||
test.name = masked_exec ? "VectorDpp8CapturedMaskedExec"
|
||||
: "VectorDpp8Captured";
|
||||
const char *names[2][2] = {
|
||||
{"VectorDpp8Wave32MaskedExec", "VectorDpp8Wave64MaskedExec"},
|
||||
{"VectorDpp8FiWave32MaskedExec", "VectorDpp8FiWave64MaskedExec"}};
|
||||
test.name = masked_exec ? names[fi][wave_size == 64] : "VectorDpp8Captured";
|
||||
test.code = std::move(code);
|
||||
test.expected.resize(192, sentinel);
|
||||
for (u32 lane = 0; lane < 64; ++lane) {
|
||||
test.expected.resize(3 * wave_size, sentinel);
|
||||
for (u32 lane = 0; lane < wave_size; ++lane) {
|
||||
const auto mask = masks[lane / 32u];
|
||||
if ((mask & (1u << (lane % 32u))) == 0) {
|
||||
continue;
|
||||
@@ -24788,20 +25051,20 @@ TestCase VectorDpp8Captured(bool masked_exec) {
|
||||
(lane & ~7u) | (lane & 3u)};
|
||||
for (u32 permutation = 0; permutation < 2; ++permutation) {
|
||||
const auto source = sources[permutation];
|
||||
test.expected[permutation * 64u + lane] =
|
||||
(mask & (1u << (source % 32u))) != 0 ? 100u + source : 0u;
|
||||
test.expected[permutation * wave_size + lane] =
|
||||
fi || (mask & (1u << (source % 32u))) != 0 ? 100u + source : 0u;
|
||||
}
|
||||
const u32 source = (lane & ~7u) | 3u;
|
||||
test.expected[128u + lane] = 100u + lane;
|
||||
if ((mask & (1u << (source % 32u))) != 0) {
|
||||
test.expected[128u + lane] += 100u + source;
|
||||
test.expected[2 * wave_size + lane] = 100u + lane;
|
||||
if (fi || (mask & (1u << (source % 32u))) != 0) {
|
||||
test.expected[2 * wave_size + lane] += 100u + source;
|
||||
}
|
||||
}
|
||||
test.opcodes = {O::V_MOV_B32, O::V_ADD_NC_U32, O::S_MOV_B32,
|
||||
O::V_LSHLREV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
test.decoded_counts = {{".dpp8(", 3}};
|
||||
test.compute_info.wave_size = 64;
|
||||
test.compute_info.threads_num[0] = 64;
|
||||
test.compute_info.wave_size = wave_size;
|
||||
test.compute_info.threads_num[0] = wave_size;
|
||||
test.compute_info.threads_num[1] = 1;
|
||||
test.compute_info.threads_num[2] = 1;
|
||||
test.compute_info.thread_ids_num = 1;
|
||||
@@ -24859,6 +25122,56 @@ TestCase VectorDppRowXmask() {
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase VectorDppRowShare(u32 wave_size) {
|
||||
using O = ShaderOpcode;
|
||||
constexpr u32 sentinel = 0xaaaaaaaau;
|
||||
TestCase test;
|
||||
test.name = wave_size == 32 ? "VectorDppRowShareWave32" : "VectorDppRowShareWave64";
|
||||
auto &code = test.code;
|
||||
AppendVMovU32(&code, 1, 100);
|
||||
code.push_back(EncodeVop2(0x25, 1, Vgpr(0), 1)); // v1 = 100 + lane.
|
||||
code.push_back(EncodeSop1(0x04, 10, 126));
|
||||
for (u32 selector = 0; selector < 16; ++selector) {
|
||||
for (const bool masked : {false, true}) {
|
||||
const u32 row_mask = masked ? 0xau : 0xfu;
|
||||
const u32 bank_mask = masked ? 0x5u : 0xfu;
|
||||
// Keep the selected source active in every row while masking destination
|
||||
// lanes. Row and bank masks affect destinations, not the shared source.
|
||||
const u32 source_lanes = 0x00010001u << selector;
|
||||
const u32 exec[] = {masked ? 0xaaaaaaaau | source_lanes : 0xffffffffu,
|
||||
masked ? 0x55555555u | source_lanes : 0xffffffffu};
|
||||
AppendVMovLiteral(&code, 2, sentinel);
|
||||
AppendVMovLiteral(&code, 3, sentinel);
|
||||
AppendSMovLiteral(&code, 126, exec[0]);
|
||||
if (wave_size == 64) AppendSMovLiteral(&code, 127, exec[1]);
|
||||
code.push_back(EncodeVop1(0x01, 2, 250));
|
||||
code.push_back(EncodeVop1Dpp(1, 0x150 + selector, row_mask, bank_mask));
|
||||
code.push_back(EncodeVop2(0x25, 3, 250, 0));
|
||||
code.push_back(EncodeVop2Dpp(1, 0x150 + selector, row_mask, bank_mask));
|
||||
code.push_back(EncodeSop1(0x04, 126, 10));
|
||||
for (const u32 reg : {2u, 3u}) {
|
||||
AppendStoreVgprAtLaneDwordOffset(&code, reg, 0, u32(test.expected.size()));
|
||||
for (u32 lane = 0; lane < wave_size; ++lane) {
|
||||
const bool write = ((row_mask >> (lane / 16)) & 1u) &&
|
||||
((bank_mask >> ((lane / 4) % 4)) & 1u) &&
|
||||
((exec[lane / 32] >> (lane % 32)) & 1u);
|
||||
const u32 source = (lane / 16) * 16 + selector;
|
||||
test.expected.push_back(write ? 100 + source + (reg == 3 ? lane : 0) : sentinel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AppendEnd(&code);
|
||||
test.opcodes = {O::V_MOV_B32, O::V_ADD_NC_U32, O::S_MOV_B32, O::S_MOV_B64,
|
||||
O::V_LSHLREV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
test.compute_info.wave_size = wave_size;
|
||||
test.compute_info.threads_num[0] = wave_size;
|
||||
test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1;
|
||||
test.compute_info.thread_ids_num = 1;
|
||||
test.has_compute_info = true;
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase VectorDppBankMaskPreservesDestination() {
|
||||
using O = ShaderOpcode;
|
||||
|
||||
@@ -33983,10 +34296,11 @@ TestCase ImageSamplePackedUintConvertsSampleAndGather() {
|
||||
return test;
|
||||
}
|
||||
|
||||
template <bool rg> TestCase ImageSampleUScaled8() {
|
||||
template <bool rg, bool snorm = false> TestCase ImageSamplePacked8() {
|
||||
using O = ShaderOpcode;
|
||||
TestCase test;
|
||||
test.name = rg ? "ImageSampleRG8UScaled" : "ImageSampleR8UScaled";
|
||||
test.name = snorm ? "ImageSampleR8SNorm"
|
||||
: rg ? "ImageSampleRG8UScaled" : "ImageSampleR8UScaled";
|
||||
AppendVMovLiteral(&test.code, 20, std::bit_cast<u32>(0.375f));
|
||||
AppendVMovLiteral(&test.code, 21, std::bit_cast<u32>(0.375f));
|
||||
test.code.push_back(EncodeMimg0(0x20, 0xf));
|
||||
@@ -33999,26 +34313,37 @@ template <bool rg> TestCase ImageSampleUScaled8() {
|
||||
AppendStoreVgpr(&test.code, component, component);
|
||||
}
|
||||
AppendEnd(&test.code);
|
||||
// R varies horizontally; G varies vertically in the opposite direction.
|
||||
// Bilinear samples must retain fractions despite host UNorm filter precision.
|
||||
test.image_width = test.image_height = 2;
|
||||
test.sampled_image_rgba = rg ? std::vector<u32>{0xffffff00u, 0x00ff0000u}
|
||||
: std::vector<u32>{0xff00ff00u};
|
||||
test.sampled_image_format = rg ? vk::Format::eR8G8Unorm : vk::Format::eR8Unorm;
|
||||
test.sampled_image_rgba =
|
||||
snorm ? std::vector<u32>{0x7fc08180u}
|
||||
: rg ? std::vector<u32>{0xffffff00u, 0x00ff0000u}
|
||||
: std::vector<u32>{0xff00ff00u};
|
||||
test.sampled_image_format =
|
||||
snorm ? vk::Format::eR8Snorm
|
||||
: rg ? vk::Format::eR8G8Unorm : vk::Format::eR8Unorm;
|
||||
test.sampled_image_dwords_per_pixel = 1;
|
||||
test.sampler_filter = vk::Filter::eLinear;
|
||||
test.expected_float_tolerance = 0.01f;
|
||||
test.user_data = MakeSampledTextureData(rg ? Prospero::BufferFormat::k8_8UScaled
|
||||
: Prospero::BufferFormat::k8UScaled);
|
||||
test.sampler_filter = snorm ? vk::Filter::eNearest : vk::Filter::eLinear;
|
||||
test.expected_float_tolerance = snorm ? 0.00002f : 0.01f;
|
||||
test.user_data = MakeSampledTextureData(
|
||||
snorm ? Prospero::BufferFormat::k8SNorm
|
||||
: rg ? Prospero::BufferFormat::k8_8UScaled
|
||||
: Prospero::BufferFormat::k8UScaled);
|
||||
test.user_data[1] |= 1u << 30u;
|
||||
test.user_data[2] = 1u << 14u;
|
||||
test.user_data[50] = 12u * sizeof(u32);
|
||||
test.user_data[51] = 3u << 28u;
|
||||
test.has_user_data = true;
|
||||
test.image_descriptor_swizzle = DstSel(rg ? 5 : 4, 4, 0, 1);
|
||||
for (float value : {rg ? 191.25f : 63.75f, 63.75f, 0.0f, 1.0f,
|
||||
0.0f, rg ? 0.0f : 255.0f, 255.0f, rg ? 255.0f : 0.0f,
|
||||
1.0f, 1.0f, 1.0f, 1.0f}) {
|
||||
test.image_descriptor_swizzle = snorm ? DstSel(4, 5, 6, 7)
|
||||
: DstSel(rg ? 5 : 4, 4, 0, 1);
|
||||
constexpr float midpoint = -64.0f / 127.0f;
|
||||
const std::array<float, 12> expected = snorm
|
||||
// -128 and -127 both clamp to -1; the interior signed value normalizes by 127.
|
||||
? std::array<float, 12>{-1, 0, 0, 1, midpoint, 1, -1, -1, 1, 1, 1, 1}
|
||||
// R varies horizontally; G varies vertically in the opposite direction.
|
||||
: std::array<float, 12>{rg ? 191.25f : 63.75f, 63.75f, 0.0f, 1.0f,
|
||||
0.0f, rg ? 0.0f : 255.0f, 255.0f, rg ? 255.0f : 0.0f,
|
||||
1.0f, 1.0f, 1.0f, 1.0f};
|
||||
for (float value : expected) {
|
||||
test.expected.push_back(std::bit_cast<u32>(value));
|
||||
}
|
||||
test.opcodes = {O::V_MOV_B32, O::IMAGE_SAMPLE, O::IMAGE_GATHER4_LZ,
|
||||
@@ -34635,12 +34960,11 @@ TestCase ImageSampleOpcodeAliasUsesNormalCoords() {
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu() {
|
||||
template <u32 wave_size> TestCase ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu() {
|
||||
using O = ShaderOpcode;
|
||||
|
||||
std::vector<u32> code;
|
||||
AppendVMovU32(&code, 20,
|
||||
1); // Non-constant +1 X offset is not a SPIR-V ConstOffset.
|
||||
AppendVMovU32(&code, 20, 1); // +1 X remains a full dword with A16 coordinates.
|
||||
AppendVMovLiteral(&code, 21, 0x36003900u); // x=0.625, y=0.375 packed as f16.
|
||||
AppendVMovU32(&code, 22, 0);
|
||||
code.push_back(EncodeMimg0(0x30, 0xf));
|
||||
@@ -34651,18 +34975,63 @@ TestCase ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu() {
|
||||
AppendEnd(&code);
|
||||
|
||||
auto image = MakeRgbaImage(4, 4);
|
||||
SetRgbaPixel(&image, 4, 2, 1, 0x3f800000u, 0x40000000u, 0x40400000u,
|
||||
SetRgbaPixel(&image, 4, 2, 1, 0x3f000000u, 0, 0, 0);
|
||||
SetRgbaPixel(&image, 4, 3, 1, 0x3f800000u, 0x40000000u, 0x40400000u,
|
||||
0x40800000u);
|
||||
|
||||
TestCase test;
|
||||
test.name = "ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu";
|
||||
test.name = wave_size == 32 ? "ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu32"
|
||||
: "ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu64";
|
||||
test.code = code;
|
||||
test.compute_info.wave_size = wave_size;
|
||||
test.has_compute_info = true;
|
||||
test.expected = {0x3f800000u, 0x40000000u, 0x40400000u, 0x40800000u};
|
||||
test.opcodes = {O::V_MOV_B32, O::IMAGE_SAMPLE, O::BUFFER_STORE_DWORD,
|
||||
O::S_ENDPGM};
|
||||
test.sampled_image_rgba = image;
|
||||
test.required_spirv = {"UnpackHalf2x16"};
|
||||
test.forbidden_spirv = {"OpBitFieldSExtract"};
|
||||
test.required_spirv = {"UnpackHalf2x16", "Lod|ConstOffset"};
|
||||
test.forbidden_spirv = {"OpBitFieldSExtract", "OpImageQuerySizeLod", "OpFDiv"};
|
||||
return test;
|
||||
}
|
||||
|
||||
template <u32 wave_size> TestCase ImageSampleOffsetsReadFourNeighbors() {
|
||||
using O = ShaderOpcode;
|
||||
TestCase test;
|
||||
test.name = wave_size == 32 ? "ImageSampleOffsetsReadFourNeighbors32"
|
||||
: "ImageSampleOffsetsReadFourNeighbors64";
|
||||
test.compute_info.wave_size = wave_size;
|
||||
test.has_compute_info = true;
|
||||
constexpr std::array offsets{0x01u, 0x3fu, 0x3f00u, 0x0100u};
|
||||
constexpr std::array opcodes{0x37u, 0x37u, 0x34u, 0x32u};
|
||||
for (u32 sample = 0; sample < offsets.size(); ++sample) {
|
||||
AppendVMovLiteral(&test.code, 20, offsets[sample]);
|
||||
u32 coordinate = 21;
|
||||
if (opcodes[sample] == 0x32u) {
|
||||
for (float gradient : {0.25f, 0.f, 0.f, 0.25f}) {
|
||||
AppendVMovLiteral(&test.code, coordinate++, std::bit_cast<u32>(gradient));
|
||||
}
|
||||
}
|
||||
// All instructions address the center of texel (2, 1) before applying the offset.
|
||||
AppendVMovLiteral(&test.code, coordinate, std::bit_cast<u32>(0.625f));
|
||||
AppendVMovLiteral(&test.code, coordinate + 1, std::bit_cast<u32>(0.375f));
|
||||
if (opcodes[sample] == 0x34u) AppendVMovU32(&test.code, coordinate + 2, 0);
|
||||
test.code.push_back(EncodeMimg0(opcodes[sample], 0x1));
|
||||
test.code.push_back(EncodeMimg1(sample, 20));
|
||||
AppendStoreVgpr(&test.code, sample, sample);
|
||||
}
|
||||
AppendEnd(&test.code);
|
||||
test.sampled_image_rgba = MakeRgbaImage(4, 4);
|
||||
SetRgbaPixel(&test.sampled_image_rgba, 4, 2, 1, std::bit_cast<u32>(16.f), 0, 0, 0);
|
||||
constexpr std::array<std::array<u32, 2>, 4> neighbors{{{3, 1}, {1, 1}, {2, 0}, {2, 2}}};
|
||||
for (u32 sample = 0; sample < neighbors.size(); ++sample) {
|
||||
const auto value = std::bit_cast<u32>(float(sample + 1));
|
||||
SetRgbaPixel(&test.sampled_image_rgba, 4, neighbors[sample][0], neighbors[sample][1],
|
||||
value, 0, 0, 0);
|
||||
test.expected.push_back(value);
|
||||
}
|
||||
test.opcodes = {O::V_MOV_B32, O::IMAGE_SAMPLE, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
test.required_spirv = {"Lod|ConstOffset", "Grad|ConstOffset"};
|
||||
test.forbidden_spirv = {"OpImageQuerySizeLod", "OpFDiv", "OpBitFieldSExtract", "OpSwitch"};
|
||||
return test;
|
||||
}
|
||||
|
||||
@@ -34733,7 +35102,8 @@ TestCase ImageGatherCompareOpcodes() {
|
||||
O::V_MOV_B32, O::IMAGE_GATHER4_C, O::IMAGE_GATHER4_C_LZ,
|
||||
O::IMAGE_GATHER4_C_O, O::IMAGE_GATHER4_C_LZ_O, O::BUFFER_STORE_DWORD,
|
||||
O::S_ENDPGM};
|
||||
test.required_spirv = {"OpImageDrefGather", "OpBitFieldSExtract"};
|
||||
test.required_spirv = {"OpImageDrefGather", "ConstOffset"};
|
||||
test.forbidden_spirv = {"OpBitFieldSExtract"};
|
||||
test.compile_only = true;
|
||||
return test;
|
||||
}
|
||||
@@ -35285,7 +35655,7 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
|
||||
std::memcpy(&memory_flag_bits, &memory_flags, sizeof(memory_flags));
|
||||
auto &sample = block->AppendNewInst(
|
||||
ValueOpcode::ImageSampleRaw,
|
||||
{Value(&image), Value(&sampler), Value(&address)}, memory_flag_bits);
|
||||
{Value(&image), Value(&sampler), Value(&address), Value(true)}, memory_flag_bits);
|
||||
auto &sample_x = block->AppendNewInst(ValueOpcode::CompositeExtractU32x4,
|
||||
{Value(&sample), Value(0u)});
|
||||
block->AppendNewInst(ValueOpcode::ReferenceU32, {Value(&sample_x)});
|
||||
@@ -35496,29 +35866,63 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
|
||||
resource == 4u ? vk::ImageViewType::e2DArray : vk::ImageViewType::e2D, layers));
|
||||
}
|
||||
const auto native_sampler = vulkan.CreateSampler(test.name);
|
||||
for (const bool fetch : {false, true}) {
|
||||
enum class Access { Sample, Gather, GatherHomogeneous, Fetch };
|
||||
auto *access = &sample;
|
||||
for (const auto mode : {Access::Sample, Access::Gather,
|
||||
Access::GatherHomogeneous, Access::Fetch}) {
|
||||
const bool fetch = mode == Access::Fetch;
|
||||
const bool gather = mode == Access::Gather || mode == Access::GatherHomogeneous;
|
||||
const bool homogeneous = mode == Access::GatherHomogeneous;
|
||||
if (mode == Access::Gather) {
|
||||
test.name = "IndirectImageGatherContiguousRuns";
|
||||
const auto position = std::ranges::find_if(*block, [&](const Inst &inst) {
|
||||
return &inst == access;
|
||||
});
|
||||
auto &read = *block->PrependNewInst(
|
||||
position, ValueOpcode::ImageGatherRaw,
|
||||
{Value(&image), Value(&sampler), Value(&address), Value(0u), Value(0u),
|
||||
Value(0u), Value(0u), Value(true)}, memory_flag_bits);
|
||||
access->ReplaceUsesWith(Value(&read), false);
|
||||
access = &read;
|
||||
compiled.program.info.images[4].cube = false;
|
||||
auto &gather_memory = compiled.program.memory_info[0];
|
||||
gather_memory.dmask = 1;
|
||||
gather_memory.image_sample_flags = ShaderRecompiler::Decoder::ImageSampleFlagLevelZero |
|
||||
ShaderRecompiler::Decoder::ImageSampleFlagOffset;
|
||||
// GATHER4_LZ_O places its packed signed offset before the coordinates.
|
||||
address.SetArg(0, Value(0x3fu));
|
||||
address.SetArg(1, Value(std::bit_cast<u32>(0.75f)));
|
||||
address.SetArg(2, Value(std::bit_cast<u32>(0.25f)));
|
||||
address.SetArg(3, Value(0u));
|
||||
}
|
||||
auto &selected_resources = compiled.program.info.images[0].indirect_resources;
|
||||
selected_resources = homogeneous ? std::vector<u32>{0u, 2u, 3u, 5u, 6u}
|
||||
: root.indirect_resources;
|
||||
if (homogeneous) test.name = "IndirectImageGatherHomogeneous";
|
||||
compiled.resources.flattened_srt[6] = homogeneous ? 4u : ordinals[5];
|
||||
if (fetch) {
|
||||
test.name = "IndirectImageFetchContiguousRuns";
|
||||
const auto position = std::ranges::find_if(*block, [&](const Inst &inst) {
|
||||
return &inst == &sample;
|
||||
return &inst == access;
|
||||
});
|
||||
auto &read = *block->PrependNewInst(
|
||||
position, ValueOpcode::ImageRead,
|
||||
{Value(&image), Value(&address), Value(true)}, memory_flag_bits);
|
||||
sample.ReplaceUsesWith(Value(&read), false);
|
||||
access->ReplaceUsesWith(Value(&read), false);
|
||||
sampler.Invalidate();
|
||||
compiled.program.info.samplers.clear();
|
||||
compiled.program.info.sampled_pairs.clear();
|
||||
compiled.program.info.images[4].cube = false;
|
||||
compiled.program.memory_info[0].dmask = 0xf;
|
||||
compiled.program.memory_info[0].image_sample_flags = 0;
|
||||
compiled.program.memory_info[0].image_has_mip = true;
|
||||
for (u32 component = 0; component < 4u; ++component)
|
||||
address.SetArg(component, Value(0u));
|
||||
// Native DIM=2DArray places LOD after the layer, even for a 2D candidate.
|
||||
address.SetArg(3, Value(1u));
|
||||
compiled.program.shader_info_complete = false;
|
||||
CollectShaderInfo(compiled.program, {.compute = &compute});
|
||||
}
|
||||
compiled.program.shader_info_complete = false;
|
||||
CollectShaderInfo(compiled.program, {.compute = &compute});
|
||||
for (const u32 wave_size : {32u, 64u}) {
|
||||
compute.wave_size = wave_size;
|
||||
compute.host_subgroup_size = vulkan.SubgroupSize();
|
||||
@@ -35529,31 +35933,38 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
|
||||
compiled.packed_user_data.resize(compiled.program.bindings.ShaderDataDwords());
|
||||
ValidateSpirv(test.name, compiled.spirv);
|
||||
const auto halves = wave_size > compute.host_subgroup_size ? 2u : 1u;
|
||||
const auto image_opcode = fetch ? "OpImageFetch" : gather ? "OpImageGather"
|
||||
: "OpImageSampleExplicitLod";
|
||||
Require(test.name, "native array image runs", tools.Disassemble(compiled.spirv, &text) &&
|
||||
CountText(text, fetch ? "OpImageFetch" : "OpImageSampleExplicitLod") == 3u * halves,
|
||||
CountText(text, image_opcode) == (homogeneous ? 1u : 3u) * halves,
|
||||
"mixed runs expanded into per-image accesses");
|
||||
if (fetch) {
|
||||
if (homogeneous) {
|
||||
Require(test.name, "homogeneous gather array", text.find("OpSwitch") == std::string::npos,
|
||||
"homogeneous gather emitted a descriptor switch");
|
||||
}
|
||||
if (fetch || gather) {
|
||||
std::vector<u32> decorated;
|
||||
std::vector<u32> fetched_images;
|
||||
std::vector<u32> image_operands;
|
||||
for (size_t offset = 5; offset < compiled.spirv.size();) {
|
||||
const auto words = std::span<const u32>(compiled.spirv).subspan(
|
||||
offset, compiled.spirv[offset] >> 16u);
|
||||
const auto opcode = static_cast<spv::Op>(words[0] & 0xffffu);
|
||||
if (opcode == spv::OpDecorate && words[2] == spv::DecorationNonUniform)
|
||||
decorated.push_back(words[1]);
|
||||
if (opcode == spv::OpImageFetch) fetched_images.push_back(words[3]);
|
||||
if (opcode == (fetch ? spv::OpImageFetch : spv::OpImageGather))
|
||||
image_operands.push_back(words[3]);
|
||||
offset += words.size();
|
||||
}
|
||||
Require(test.name, "nonuniform fetch operands",
|
||||
std::ranges::count_if(fetched_images, [&](u32 operand) {
|
||||
Require(test.name, "nonuniform image operands",
|
||||
std::ranges::count_if(image_operands, [&](u32 operand) {
|
||||
return std::ranges::find(decorated, operand) != decorated.end();
|
||||
}) == 2u * halves,
|
||||
"a dynamically selected fetch image lacks its nonuniform decoration");
|
||||
}) == (homogeneous ? 1u : 2u) * halves,
|
||||
"a dynamically selected image operand lacks its nonuniform decoration");
|
||||
}
|
||||
test.initial.assign(wave_size, 0xdeadbeefu);
|
||||
test.expected.assign(wave_size, 0u);
|
||||
for (u32 lane = 0; lane < ordinals.size(); ++lane) {
|
||||
const auto resource = root.indirect_resources[ordinals[lane]];
|
||||
const auto resource = selected_resources[compiled.resources.flattened_srt[1u + lane]];
|
||||
test.expected[lane] = std::bit_cast<u32>(float(resource + (fetch && resource != 0u ? 10u : 0u)));
|
||||
}
|
||||
auto buffer = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size());
|
||||
@@ -35561,7 +35972,7 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
|
||||
fetch ? vk::Sampler{} : native_sampler, textures);
|
||||
const auto actual = vulkan.ReadBuffer(test.name, buffer, test.expected.size());
|
||||
vulkan.DestroyBuffer(&buffer);
|
||||
CompareWords(test, "root, native slots, cube and mapping bounds", test.expected, actual);
|
||||
CompareWords(test, "root, native slots, array runs and mapping bounds", test.expected, actual);
|
||||
}
|
||||
std::printf("[compute] %-32s ok\n", test.name);
|
||||
}
|
||||
@@ -36962,6 +37373,7 @@ std::vector<TestCase> MakeCases() {
|
||||
cases.push_back(ScalarSubvectorLoops(64));
|
||||
AddCase(ScalarGetpcWritesNextInstructionPc);
|
||||
AddCase(ScalarBitfieldPack);
|
||||
AddCase(ScalarZeroBitOps);
|
||||
AddCase(ScalarBitcmpB64DynamicOperands);
|
||||
AddCase(ScalarBitcmpB64IntegerConstants);
|
||||
AddCase(ScalarBrevB32PreservesScc);
|
||||
@@ -37017,6 +37429,8 @@ std::vector<TestCase> MakeCases() {
|
||||
AddCase(Vop2SdwaAshrrevCapturedWord0SignExtends);
|
||||
cases.push_back(Vop2SdwaMaxI32CapturedHighWord(32));
|
||||
cases.push_back(Vop2SdwaMaxI32CapturedHighWord(64));
|
||||
cases.push_back(Vop2SdwaMinI32CapturedByte0(32));
|
||||
cases.push_back(Vop2SdwaMinI32CapturedByte0(64));
|
||||
cases.push_back(Vop2SdwaMulI24Destinations(32));
|
||||
cases.push_back(Vop2SdwaMulI24Destinations(64));
|
||||
cases.push_back(Vop2SdwaMulU24Destinations(32));
|
||||
@@ -37074,9 +37488,13 @@ std::vector<TestCase> MakeCases() {
|
||||
AddCase(VectorPermlane16FetchInactiveZero);
|
||||
AddCase(VectorPermlane16FetchInactiveFi);
|
||||
cases.push_back(VectorDpp8Captured(false));
|
||||
cases.push_back(VectorDpp8Captured(true));
|
||||
for (const u32 wave_size : {32u, 64u})
|
||||
for (const bool fi : {false, true})
|
||||
cases.push_back(VectorDpp8Captured(true, wave_size, fi));
|
||||
AddCase(VectorDppQuadPermuteReverse);
|
||||
AddCase(VectorDppRowXmask);
|
||||
cases.push_back(VectorDppRowShare(32));
|
||||
cases.push_back(VectorDppRowShare(64));
|
||||
AddCase(VectorDppBankMaskPreservesDestination);
|
||||
AddCase(VectorDppBoundsControlZeroPreservesDestination);
|
||||
AddCase(Vop3FmacF32NegatedSourceAccumulates);
|
||||
@@ -37371,8 +37789,9 @@ std::vector<TestCase> MakeCases() {
|
||||
AddCase(ImageLoadR32SintUsesSignedSampledImage);
|
||||
AddCase(ImageLoadPackedUintUnpacksAndSwizzles);
|
||||
AddCase(ImageSamplePackedUintConvertsSampleAndGather);
|
||||
AddCase(ImageSampleUScaled8<false>);
|
||||
AddCase(ImageSampleUScaled8<true>);
|
||||
AddCase(ImageSamplePacked8<false>);
|
||||
AddCase(ImageSamplePacked8<true>);
|
||||
AddCase(ImageSamplePacked8<false, true>);
|
||||
AddCase(ImageLoadR128IgnoresAdjacentMaskSgprs);
|
||||
AddCase(ImageLoad1DUsesScalarCoordinate);
|
||||
AddCase(ImageGather2DInstructionWith1DDescriptor);
|
||||
@@ -37398,7 +37817,10 @@ std::vector<TestCase> MakeCases() {
|
||||
AddCase(ImageD16GatherPacksHalfPairs);
|
||||
AddCase(ImageSampleA16SamplerCoordsOnGpu);
|
||||
AddCase(ImageSampleOpcodeAliasUsesNormalCoords);
|
||||
AddCase(ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu);
|
||||
AddCase(ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu<32>);
|
||||
AddCase(ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu<64>);
|
||||
AddCase(ImageSampleOffsetsReadFourNeighbors<32>);
|
||||
AddCase(ImageSampleOffsetsReadFourNeighbors<64>);
|
||||
AddCase(ImageSampleA16CompareBiasRdna2AddressOrder);
|
||||
AddCase(ImageGatherCompareOpcodes);
|
||||
AddCase(ImageStoreVariants);
|
||||
@@ -39809,7 +40231,7 @@ void CheckImageSamplerSpecialization() {
|
||||
sizeof(signed_memory_flags));
|
||||
mixed_sampler_block->AppendNewInst(
|
||||
ValueOpcode::ImageSampleRaw,
|
||||
{Value(&image_handle), Value(&sampler_handle), Value(&image_address)},
|
||||
{Value(&image_handle), Value(&sampler_handle), Value(&image_address), Value(true)},
|
||||
signed_memory_flag_bits);
|
||||
|
||||
ShaderRecompiler::IR::DescriptorValue native_image_descriptor{};
|
||||
@@ -41737,9 +42159,24 @@ void CheckAgcSystemTable(RenderContext &renderer) {
|
||||
0xbf800000, 0x85ea807e, 0x88ea106a, 0xbf870003,
|
||||
0xbefc03ff, 0x8a6ca000, 0xbf920009, 0xbefd210e};
|
||||
LibKernel::Memory::InstallGpuResources(&renderer);
|
||||
Require(name, "driver module startup", Gen5Driver::Initialize() == 0,
|
||||
"driver module startup failed before guest initialization");
|
||||
const auto direct_memory_size = LibKernel::Memory::KernelGetDirectMemorySize();
|
||||
int64_t available_offset = -1;
|
||||
size_t available_size = 0;
|
||||
Require(name, "remaining direct pool",
|
||||
LibKernel::Memory::KernelAvailableDirectMemorySize(
|
||||
0, direct_memory_size, 0, &available_offset, &available_size) == 0 &&
|
||||
available_size == direct_memory_size - 0x200000,
|
||||
"driver startup did not reserve its backing in the reported direct pool");
|
||||
int64_t guest_allocation = -1;
|
||||
Require(name, "guest consumes remaining direct pool",
|
||||
LibKernel::Memory::KernelAllocateDirectMemory(
|
||||
0, direct_memory_size, available_size, 0x4000, 0, &guest_allocation) == 0,
|
||||
"guest could not allocate all remaining direct memory");
|
||||
uint32_t state = 0xabcdef01u;
|
||||
Require(name, "driver startup", Gen5::AgcInit(&state, 13) == 0 && state == 0xabcdef01u,
|
||||
"Agc startup failed or changed its ignored public state pointer");
|
||||
"Agc startup required free guest memory or changed its ignored public state pointer");
|
||||
LibKernel::Memory::VirtualQueryInfo mapping{};
|
||||
Require(name, "shared driver backing",
|
||||
LibKernel::Memory::KernelVirtualQuery(reinterpret_cast<void *>(driver_base), 0,
|
||||
@@ -41766,10 +42203,14 @@ void CheckAgcSystemTable(RenderContext &renderer) {
|
||||
Require(name, "default instrumentation", *instrumentation == 0,
|
||||
"unused vertex validation was enabled at startup");
|
||||
*instrumentation = 0x12340000u;
|
||||
Require(name, "repeat startup", Gen5::AgcInit(nullptr, 13) == 0 &&
|
||||
Require(name, "repeat startup", Gen5Driver::Initialize() == 0 &&
|
||||
Gen5::AgcInit(nullptr, 13) == 0 &&
|
||||
*instrumentation == 0x12340000u,
|
||||
"repeat Agc initialization reset existing system state");
|
||||
*instrumentation = 0;
|
||||
Require(name, "release guest allocation",
|
||||
LibKernel::Memory::KernelCheckedReleaseDirectMemory(guest_allocation, available_size) == 0,
|
||||
"guest allocation release failed");
|
||||
LibKernel::Memory::InstallGpuResources(nullptr);
|
||||
std::printf("[host] %-32s ok\n", name);
|
||||
}
|
||||
@@ -42350,6 +42791,43 @@ void CheckPm4IndirectControlFlow(RenderContext &renderer) {
|
||||
Gen5::AgcCbNop(reinterpret_cast<Gen5::CommandBuffer*>(&payload_dcb), 3) == payload.data() &&
|
||||
payload == std::array<uint32_t, 3>{0xc0011000u, 0x12345678u, 0x9abcdef0u},
|
||||
"NOP emitter overwrote the caller's reserved payload");
|
||||
alignas(8) uint64_t released = 0;
|
||||
std::array<uint32_t, 20> disabled_commands{};
|
||||
const auto disabled_write = write(&returned, 99);
|
||||
std::copy(disabled_write.begin(), disabled_write.end(), disabled_commands.begin());
|
||||
disabled_commands[5] = KYTY_PM4(2, Pm4::IT_NUM_INSTANCES, 0);
|
||||
disabled_commands[6] = 0x68750123u;
|
||||
CommandBufferLayout release_dcb{disabled_commands.data() + 7, disabled_commands.data() + 15,
|
||||
disabled_commands.data() + 7, disabled_commands.data() + 15, nullptr, nullptr, 0};
|
||||
auto* release = Gen5::AgcCbReleaseMem(
|
||||
reinterpret_cast<Gen5::CommandBuffer*>(&release_dcb), 0x28, 0, 0, 0,
|
||||
reinterpret_cast<const volatile Gen5::Label*>(&released), 2, 77, 0, 0, 0, 0);
|
||||
const auto enabled_write = write(&selected, 88);
|
||||
std::copy(enabled_write.begin(), enabled_write.end(), disabled_commands.begin() + 15);
|
||||
const auto unpatched_commands = disabled_commands;
|
||||
Require(name, "SetNop packets",
|
||||
release == disabled_commands.data() + 7 &&
|
||||
Gen5::AgcSetNop(disabled_commands.data()) == 0 &&
|
||||
Gen5::AgcSetNop(disabled_commands.data() + 5) == 0 && Gen5::AgcSetNop(release) == 0 &&
|
||||
disabled_commands[0] == ((disabled_write[0] & 0xffff00ffu) | (Pm4::IT_NOP << 8u)) &&
|
||||
disabled_commands[5] == KYTY_PM4(2, Pm4::IT_NOP, Pm4::R_ZERO) &&
|
||||
disabled_commands[6] == 0x123u &&
|
||||
release[0] == KYTY_PM4(8, Pm4::IT_NOP, Pm4::R_ZERO) &&
|
||||
unpatched_commands[7] == KYTY_PM4(8, Pm4::IT_NOP, Pm4::R_RELEASE_MEM) &&
|
||||
Gen5::AgcGetPacketSize(disabled_commands.data()) == disabled_write.size() &&
|
||||
Gen5::AgcGetPacketSize(disabled_commands.data() + 5) == 2 &&
|
||||
Gen5::AgcGetPacketSize(release) == 8 &&
|
||||
std::equal(disabled_commands.begin() + 1, disabled_commands.begin() + 5,
|
||||
unpatched_commands.begin() + 1) &&
|
||||
std::equal(disabled_commands.begin() + 8, disabled_commands.end(),
|
||||
unpatched_commands.begin() + 8),
|
||||
"SetNop changed a packet length, kept its operation or marker, or touched its payload");
|
||||
selected = returned = 0;
|
||||
Pm4Execution disabled_execution;
|
||||
Require(name, "SetNop execution",
|
||||
processor.Process(disabled_execution, disabled_commands) == Pm4ProcessResult::Complete &&
|
||||
returned == 0 && released == 0 && selected == 88,
|
||||
"a disabled packet still executed or hid the packet after it");
|
||||
const auto then_commands = write(&selected, 11);
|
||||
const auto else_commands = write(&selected, 33);
|
||||
const auto branch_suffix = write(&selected, 44);
|
||||
@@ -42753,9 +43231,13 @@ int main(int argc, char **argv) {
|
||||
if (argc == 2 && std::strcmp(argv[1], "--dpp-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
RunCase(&vulkan, VectorDpp8Captured(false));
|
||||
RunCase(&vulkan, VectorDpp8Captured(true));
|
||||
for (const u32 wave_size : {32u, 64u})
|
||||
for (const bool fi : {false, true})
|
||||
RunCase(&vulkan, VectorDpp8Captured(true, wave_size, fi));
|
||||
RunCase(&vulkan, VectorDppQuadPermuteReverse());
|
||||
RunCase(&vulkan, VectorDppRowXmask());
|
||||
RunCase(&vulkan, VectorDppRowShare(32));
|
||||
RunCase(&vulkan, VectorDppRowShare(64));
|
||||
RunCase(&vulkan, VectorDppBankMaskPreservesDestination());
|
||||
RunCase(&vulkan, VectorDppBoundsControlZeroPreservesDestination());
|
||||
return 0;
|
||||
@@ -42783,6 +43265,12 @@ int main(int argc, char **argv) {
|
||||
RunCase(&vulkan, Vop3pInt16Ops());
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--scalar-zero-bit-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
RunCase(&vulkan, ScalarZeroBitOps());
|
||||
RunCase(&vulkan, ScalarBitfieldPack());
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--buffer-format-store-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
RunCase(&vulkan, BufferStoreFormatXyzwPackedUnormSkinningVectors());
|
||||
@@ -42913,8 +43401,9 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--scaled-texture-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
RunCase(&vulkan, ImageSampleUScaled8<false>());
|
||||
RunCase(&vulkan, ImageSampleUScaled8<true>());
|
||||
RunCase(&vulkan, ImageSamplePacked8<false>());
|
||||
RunCase(&vulkan, ImageSamplePacked8<true>());
|
||||
RunCase(&vulkan, ImageSamplePacked8<false, true>());
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--packed-texture-only") == 0) {
|
||||
@@ -42999,6 +43488,16 @@ int main(int argc, char **argv) {
|
||||
RunCase(&vulkan, FlatVirtualAddressRebasesGuestAllocation());
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--image-sample-offset-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
RunCase(&vulkan, ImageSampleOffsetsReadFourNeighbors<32>());
|
||||
RunCase(&vulkan, ImageSampleOffsetsReadFourNeighbors<64>());
|
||||
RunCase(&vulkan, ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu<32>());
|
||||
RunCase(&vulkan, ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu<64>());
|
||||
RunCase(&vulkan, ImageSampleAndGather());
|
||||
RunCase(nullptr, ImageGatherCompareOpcodes());
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--image-gather-lod-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
RunCase(&vulkan, ImageGatherExplicitLod());
|
||||
|
||||
@@ -1316,7 +1316,7 @@ void TestShaderFeatureCollection() {
|
||||
{Value(0u), Value(0u), Value(0u)});
|
||||
block->AppendNewInst(ValueOpcode::ImageGatherRaw,
|
||||
{Value(0u), Value(0u), Value(0u), Value(0u),
|
||||
Value(0u), Value(0u), Value(0u)});
|
||||
Value(0u), Value(0u), Value(0u), Value(true)});
|
||||
auto &shared = block->AppendNewInst(ValueOpcode::LoadSharedU32,
|
||||
{Value(0u), Value(true)});
|
||||
shared.SetFlags(MemoryFlags{.index = 0});
|
||||
@@ -1945,14 +1945,18 @@ void TestSopkCompareImmediateExtension() {
|
||||
|
||||
void TestDisabledDebugBranches() {
|
||||
using namespace ShaderRecompiler;
|
||||
// Both debug branches fall through when conditional shader debugging is disabled.
|
||||
// Debug branches fall through when conditional shader debugging is disabled.
|
||||
// Retain the post-ENDPGM helper displacement from PPSA08709 and exercise the
|
||||
// OR_USER variant encountered in PPSA03671 with the same body.
|
||||
for (const auto opcode : {Decoder::Opcode::S_CBRANCH_CDBGSYS,
|
||||
Decoder::Opcode::S_CBRANCH_CDBGSYS_OR_USER}) {
|
||||
const std::pair<uint32_t, Decoder::Opcode> branches[] = {
|
||||
{0x17, Decoder::Opcode::S_CBRANCH_CDBGSYS},
|
||||
{0x18, Decoder::Opcode::S_CBRANCH_CDBGUSER},
|
||||
{0x19, Decoder::Opcode::S_CBRANCH_CDBGSYS_OR_USER},
|
||||
{0x1a, Decoder::Opcode::S_CBRANCH_CDBGSYS_AND_USER}};
|
||||
for (const auto [encoding, opcode] : branches) {
|
||||
std::array<uint32_t, 274> shader;
|
||||
shader.fill(EncodeSopp(0x00));
|
||||
shader[0] = EncodeSopp(opcode == Decoder::Opcode::S_CBRANCH_CDBGSYS ? 0x17 : 0x19, 0x10f);
|
||||
shader[0] = EncodeSopp(encoding, 0x10f);
|
||||
shader[1] = EncodeVop1(0x01, 1, 129);
|
||||
shader[2] = EncodeMubuf0(0x1c, 0, false);
|
||||
shader[3] = EncodeMubuf1(1, 0, 0);
|
||||
@@ -1962,6 +1966,7 @@ void TestDisabledDebugBranches() {
|
||||
Decoder::Program decoded;
|
||||
Decoder::DecodeProgram(shader, decoded);
|
||||
Check(decoded.instructions.front().opcode == opcode &&
|
||||
decoded.instructions.front().opcode_id == encoding &&
|
||||
decoded.instructions.front().branch_target == 0x440u &&
|
||||
decoded.instructions.back().pc == 0x43cu &&
|
||||
decoded.instructions.back().opcode == Decoder::Opcode::S_ENDPGM,
|
||||
|
||||
Reference in new issue
Block a user