mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-24 12:56:53 +02:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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" .
|
||||
|
||||
|
||||
+8
-3
@@ -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)
|
||||
@@ -399,7 +404,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 +472,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 +485,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.
|
||||
|
||||
@@ -182,4 +182,8 @@ const Keymap& GetKeymap() {
|
||||
return g_config->keymap;
|
||||
}
|
||||
|
||||
const std::vector<std::string>& GetGuestArguments() {
|
||||
return g_config->guest_arguments;
|
||||
}
|
||||
|
||||
} // namespace Config
|
||||
|
||||
@@ -54,6 +54,7 @@ struct ConfigOptions {
|
||||
int32_t gpu_index = -1;
|
||||
bool fullscreen_enabled = false;
|
||||
bool hide_cursor_enabled = false;
|
||||
bool splash_enabled = true;
|
||||
bool vr_enabled = false;
|
||||
bool amd_cpu_enabled = false;
|
||||
uint32_t vblank_frequency = 60;
|
||||
@@ -82,6 +83,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);
|
||||
@@ -135,6 +138,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
|
||||
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -186,6 +186,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;
|
||||
|
||||
@@ -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_
|
||||
@@ -3,9 +3,12 @@
|
||||
#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 "graphics/host_gpu/graphicContext.h"
|
||||
#include "graphics/presentation/ddsImage.h"
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "imgui_impl_vulkan.h"
|
||||
@@ -13,6 +16,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 +27,7 @@
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <fmt/format.h>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -37,6 +42,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 +201,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 +475,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 +515,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 +550,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 +693,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 +805,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) {
|
||||
@@ -1215,7 +1309,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 +1340,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 +1356,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 +1387,7 @@ struct SystemOverlay::Impl {
|
||||
return;
|
||||
}
|
||||
ImGui::SetCurrentContext(imgui_context);
|
||||
splash_dds = {};
|
||||
ImGui_ImplVulkan_Shutdown();
|
||||
vulkan_initialized = false;
|
||||
}
|
||||
@@ -1309,6 +1407,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);
|
||||
|
||||
@@ -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());
|
||||
@@ -1002,6 +1016,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,
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1064,6 +1064,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,423 @@
|
||||
<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>
|
||||
</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="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">
|
||||
|
||||
@@ -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
|
||||
@@ -463,6 +463,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);
|
||||
|
||||
@@ -509,6 +512,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 +532,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);
|
||||
|
||||
+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;
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include "common/emulatorConfig.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/stringUtils.h"
|
||||
#include "emulator.h"
|
||||
#include "graphics/presentation/systemOverlay.h"
|
||||
#include "libs/dialog.h"
|
||||
#include "libs/errno.h"
|
||||
#include "libs/libs.h"
|
||||
@@ -94,10 +96,15 @@ struct SystemServiceHdrToneMapLuminance {
|
||||
|
||||
static int KYTY_SYSV_ABI SystemServiceHideSplashScreen() {
|
||||
PRINT_NAME();
|
||||
|
||||
Graphics::HideSplashScreen();
|
||||
return OK;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI SystemServiceLoadExec(const char* path, const char* const argv[]) {
|
||||
PRINT_NAME();
|
||||
return Emulator::LoadExec(path, argv);
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI SystemServiceParamGetInt(int param_id, int* value) {
|
||||
PRINT_NAME();
|
||||
|
||||
@@ -558,6 +565,7 @@ LIB_DEFINE(InitSystemGesture_1) {
|
||||
} // namespace SystemGesture
|
||||
|
||||
LIB_DEFINE(InitSystemService_1) {
|
||||
LIB_FUNC("JoBqSQt1yyA", SystemService::SystemServiceLoadExec);
|
||||
LIB_FUNC("Vo5V8KAwCmk", SystemService::SystemServiceHideSplashScreen);
|
||||
LIB_FUNC("fZo48un7LK4", SystemService::SystemServiceParamGetInt);
|
||||
LIB_FUNC("SsC-m-S9JTA", SystemService::SystemServiceParamGetString);
|
||||
|
||||
@@ -0,0 +1,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;
|
||||
|
||||
@@ -360,10 +360,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;
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ static void PrintUsage() {
|
||||
::printf(" --fullscreen Run in borderless desktop fullscreen.\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");
|
||||
@@ -219,6 +220,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--no-splash") {
|
||||
options.config.splash_enabled = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--vr") {
|
||||
options.config.vr_enabled = true;
|
||||
continue;
|
||||
@@ -294,6 +300,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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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) {
|
||||
@@ -671,6 +784,7 @@ void DbgExit(int) { std::abort(); }
|
||||
|
||||
int main() {
|
||||
TestMappedSrtUsesDirectReaderByDefault();
|
||||
TestNativeNestedSrtRefresh();
|
||||
TestIntegerRuntimeValueFollowsSrtReads();
|
||||
TestSrtAliasesRetainReadPolicy();
|
||||
TestUniformVectorDescriptorRead();
|
||||
|
||||
@@ -16439,6 +16439,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 +17798,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 +17871,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 +17908,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 +18031,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 +20942,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 +22817,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 +24928,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 +24942,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 +24964,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 +24979,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 +25050,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 +34224,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 +34241,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,
|
||||
@@ -36962,6 +37215,7 @@ std::vector<TestCase> MakeCases() {
|
||||
cases.push_back(ScalarSubvectorLoops(64));
|
||||
AddCase(ScalarGetpcWritesNextInstructionPc);
|
||||
AddCase(ScalarBitfieldPack);
|
||||
AddCase(ScalarZeroBitOps);
|
||||
AddCase(ScalarBitcmpB64DynamicOperands);
|
||||
AddCase(ScalarBitcmpB64IntegerConstants);
|
||||
AddCase(ScalarBrevB32PreservesScc);
|
||||
@@ -37017,6 +37271,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 +37330,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 +37631,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);
|
||||
@@ -41737,9 +41998,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 +42042,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 +42630,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 +43070,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 +43104,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 +43240,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) {
|
||||
|
||||
@@ -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