mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-29 08:41:20 +00:00
Compare commits
65
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b77686566 | ||
|
|
a6144f794b | ||
|
|
bab1c3614c | ||
|
|
de1545e19a | ||
|
|
ce629e0391 | ||
|
|
6c03b4aaa5 | ||
|
|
0627e23e92 | ||
|
|
4e6f4f99d3 | ||
|
|
f30afc184e | ||
|
|
228cf3fbf4 | ||
|
|
47957faa16 | ||
|
|
235e24d44f | ||
|
|
e0ecffed70 | ||
|
|
fa913ec1a7 | ||
|
|
49b2eedd1c | ||
|
|
63fb822f8a | ||
|
|
f38d738ee2 | ||
|
|
eda6ba37ec | ||
|
|
b594f8c834 | ||
|
|
bc45aaf604 | ||
|
|
fb94a819c7 | ||
|
|
8589731a27 | ||
|
|
c1dccd7db1 | ||
|
|
af3a3f4bf6 | ||
|
|
349fae00d9 | ||
|
|
10cae50b34 | ||
|
|
5b541375a3 | ||
|
|
ded6964953 | ||
|
|
9b2111dc01 | ||
|
|
71f52f9d90 | ||
|
|
86e5e01403 | ||
|
|
77680ce9d2 | ||
|
|
32e6a433e8 | ||
|
|
7c9148dd47 | ||
|
|
957452d6f9 | ||
|
|
e6156bef05 | ||
|
|
441367b1a1 | ||
|
|
629ae1ac05 | ||
|
|
0d9e95d96a | ||
|
|
9cf7963f44 | ||
|
|
acd7435d31 | ||
|
|
7af28c267d | ||
|
|
79120d53ec | ||
|
|
6a2987a7b2 | ||
|
|
d1372abf46 | ||
|
|
98b5d88d67 | ||
|
|
86414760e6 | ||
|
|
a1fbc41146 | ||
|
|
5702cfc935 | ||
|
|
7b5a33f878 | ||
|
|
ea092a9472 | ||
|
|
18a1e0fadc | ||
|
|
437e69ef1b | ||
|
|
dd408ffd00 | ||
|
|
bce8924dd6 | ||
|
|
fe26c1de17 | ||
|
|
01df42a5f3 | ||
|
|
968cf3c03f | ||
|
|
586cbd4a0d | ||
|
|
940e42f130 | ||
|
|
3f80151a6f | ||
|
|
7e89b97daf | ||
|
|
d63fb62367 | ||
|
|
29e9ea6344 | ||
|
|
9a74ef15c3 |
No files matched your search
@@ -0,0 +1,37 @@
|
||||
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
|
||||
|
||||
# Keep inherited review profiles, automatic reviews, tools, and pre-merge checks.
|
||||
inheritance: true
|
||||
|
||||
reviews:
|
||||
# Trim supplemental output and keep the remaining walkthrough collapsed.
|
||||
high_level_summary: false
|
||||
review_status: false
|
||||
review_details: false
|
||||
collapse_walkthrough: true
|
||||
changed_files_summary: false
|
||||
sequence_diagrams: false
|
||||
estimate_code_review_effort: false
|
||||
related_issues: false
|
||||
related_prs: false
|
||||
in_progress_fortune: false
|
||||
poem: false
|
||||
enable_prompt_for_ai_agents: false
|
||||
|
||||
# Hide standard finishing-touch controls; this also disables their commands.
|
||||
finishing_touches:
|
||||
docstrings:
|
||||
enabled: false
|
||||
unit_tests:
|
||||
enabled: false
|
||||
simplify:
|
||||
enabled: false
|
||||
autofix:
|
||||
enabled: false
|
||||
fix_ci:
|
||||
enabled: false
|
||||
resolve_merge_conflict:
|
||||
enabled: false
|
||||
|
||||
chat:
|
||||
art: false
|
||||
@@ -6,7 +6,12 @@ on:
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
KYTY_BUILD_ORIGIN: ${{ github.repository != 'KytyPS5/KytyPS5' && 'Fork' || github.event_name == 'push' && 'Official' || 'Source' }}
|
||||
KYTY_BUILD_REPOSITORY: ${{ github.repository }}
|
||||
|
||||
@@ -78,6 +83,9 @@ jobs:
|
||||
arch: win64_msvc2022_64
|
||||
cache: true
|
||||
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.11
|
||||
|
||||
- name: Verify toolchain
|
||||
shell: cmd
|
||||
run: |
|
||||
@@ -100,6 +108,8 @@ jobs:
|
||||
-DKYTY_RELEASE_TAG=${{ needs.metadata.outputs.release_tag }} ^
|
||||
-DCMAKE_C_COMPILER=clang-cl ^
|
||||
-DCMAKE_CXX_COMPILER=clang-cl ^
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=sccache ^
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=sccache ^
|
||||
-DCMAKE_PREFIX_PATH="%Qt6_DIR%"
|
||||
|
||||
- name: Build
|
||||
@@ -156,6 +166,9 @@ jobs:
|
||||
arch: clang_64
|
||||
cache: true
|
||||
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.11
|
||||
|
||||
- name: Verify toolchain
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -180,6 +193,8 @@ jobs:
|
||||
-DCMAKE_OSX_ARCHITECTURES=x86_64 \
|
||||
-DCMAKE_C_COMPILER=clang \
|
||||
-DCMAKE_CXX_COMPILER=clang++ \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=sccache \
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=sccache \
|
||||
-DCMAKE_PREFIX_PATH="$QT_ROOT_DIR"
|
||||
|
||||
- name: Build
|
||||
@@ -304,6 +319,7 @@ jobs:
|
||||
libxss-dev \
|
||||
lld \
|
||||
ninja-build \
|
||||
pkg-config \
|
||||
wayland-protocols
|
||||
|
||||
- name: Install Qt 6.10.3
|
||||
@@ -315,6 +331,9 @@ jobs:
|
||||
arch: linux_gcc_64
|
||||
cache: true
|
||||
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.11
|
||||
|
||||
- name: Verify toolchain
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -339,6 +358,8 @@ jobs:
|
||||
-DKYTY_RELEASE_TAG="${{ needs.metadata.outputs.release_tag }}" \
|
||||
-DCMAKE_C_COMPILER=clang \
|
||||
-DCMAKE_CXX_COMPILER=clang++ \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=sccache \
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=sccache \
|
||||
-DCMAKE_PREFIX_PATH="$Qt6_DIR" 2>&1 | tee _Build/configure.log
|
||||
exit "${PIPESTATUS[0]}"
|
||||
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@
|
||||
url = https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git
|
||||
[submodule "3rdparty/ffmpeg-core"]
|
||||
path = 3rdparty/ffmpeg-core
|
||||
url = https://github.com/shadps4-emu/ext-ffmpeg-core.git
|
||||
url = https://github.com/KytyPS5/ext-ffmpeg-core.git
|
||||
shallow = true
|
||||
[submodule "3rdparty/fmt"]
|
||||
path = 3rdparty/fmt
|
||||
|
||||
Vendored
+6
-4
@@ -2,15 +2,17 @@
|
||||
"cmake.sourceDirectory": "${workspaceFolder}",
|
||||
"cmake.buildDirectory": "${workspaceFolder}/_Build/vscode-clang",
|
||||
"cmake.generator": "Ninja",
|
||||
"cmake.useVsDeveloperEnvironment": "always",
|
||||
"cmake.environment": {
|
||||
"CC": "clang-cl",
|
||||
"CXX": "clang-cl",
|
||||
"PATH": "${env:ProgramFiles}/Microsoft Visual Studio/18/Community/VC/Tools/Llvm/x64/bin;${env:ProgramFiles}/Microsoft Visual Studio/18/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/Ninja;${env:PATH}"
|
||||
"CXX": "clang-cl"
|
||||
},
|
||||
"cmake.configureSettings": {
|
||||
"CMAKE_C_COMPILER": "clang-cl",
|
||||
"CMAKE_CXX_COMPILER": "clang-cl",
|
||||
"CMAKE_PREFIX_PATH": "C:/Qt/6.11.0/msvc2022_64"
|
||||
}
|
||||
},
|
||||
"C_Cpp.default.intelliSenseMode": "windows-clang-x64",
|
||||
"C_Cpp.default.compileCommands": "${workspaceFolder}/_Build/vscode-clang/compile_commands.json",
|
||||
"C_Cpp.default.cppStandard": "c++20"
|
||||
}
|
||||
|
||||
Vendored
+1
-1
Submodule 3rdparty/ffmpeg-core updated: 94dde08c8a...9ac4cfd195.
+16
-15
@@ -317,24 +317,10 @@ endif()
|
||||
|
||||
set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL2-static imgui xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version Zydis::Zydis xbyak::xbyak)
|
||||
|
||||
# Linux system libraries required by the static FFmpeg archive.
|
||||
# Linux system libraries.
|
||||
if(LINUX)
|
||||
find_package(Threads REQUIRED)
|
||||
list(APPEND kyty_emulator_link_libraries m ${CMAKE_DL_LIBS} Threads::Threads)
|
||||
|
||||
# Optional FFmpeg dependencies.
|
||||
find_package(ZLIB)
|
||||
if(ZLIB_FOUND)
|
||||
list(APPEND kyty_emulator_link_libraries ZLIB::ZLIB)
|
||||
endif()
|
||||
find_library(KYTY_BZ2_LIBRARY bz2)
|
||||
if(KYTY_BZ2_LIBRARY)
|
||||
list(APPEND kyty_emulator_link_libraries ${KYTY_BZ2_LIBRARY})
|
||||
endif()
|
||||
find_library(KYTY_LZMA_LIBRARY lzma)
|
||||
if(KYTY_LZMA_LIBRARY)
|
||||
list(APPEND kyty_emulator_link_libraries ${KYTY_LZMA_LIBRARY})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(inc_headers
|
||||
@@ -615,6 +601,21 @@ if(BUILD_TESTING)
|
||||
)
|
||||
endif()
|
||||
|
||||
# Test Darwin signal-context restoration for SSE4a instructions.
|
||||
if(APPLE AND (CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64" OR
|
||||
(NOT CMAKE_OSX_ARCHITECTURES AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64)$")))
|
||||
add_executable(macos_sse4a_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/macosSse4aTests.cpp"
|
||||
"${KYTY_SOURCE_DIR}/loader/x64InstructionEmulator.cpp"
|
||||
)
|
||||
target_link_libraries(macos_sse4a_tests common fmt::fmt Zydis::Zydis)
|
||||
target_include_directories(macos_sse4a_tests PRIVATE ${inc_headers})
|
||||
if(BUILD_TESTING)
|
||||
add_test(NAME macos_sse4a COMMAND $<TARGET_FILE:macos_sse4a_tests>)
|
||||
set_tests_properties(macos_sse4a PROPERTIES TIMEOUT 15)
|
||||
add_dependencies(kyty_tests macos_sse4a_tests)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_executable(kyty_emulator "${KYTY_SOURCE_DIR}/main.cpp" ${kyty_emulator_src})
|
||||
configure_macos_guest_address_space(kyty_emulator)
|
||||
|
||||
@@ -137,6 +137,7 @@ the Vulkan/SPIR-V validation rules.
|
||||
- Visual Studio 2022 or Build Tools 2022 with the **Desktop development with C++** workload and
|
||||
**C++ Clang tools for Windows** component
|
||||
- Qt 6 for MSVC 2022 64-bit, including Concurrent, Network, and Widgets
|
||||
- [glslang](https://github.com/KhronosGroup/glslang/releases) (`glslangValidator`) on `PATH`
|
||||
|
||||
The Microsoft C++ compiler (`cl.exe`) is not supported; use `clang-cl`.
|
||||
|
||||
@@ -171,7 +172,7 @@ build has no working sound and no gamepad hotplug:
|
||||
|
||||
```bash
|
||||
sudo apt-get install --no-install-recommends \
|
||||
clang lld ninja-build cmake git glslang-tools \
|
||||
clang lld ninja-build cmake git glslang-tools pkg-config \
|
||||
libgl1-mesa-dev libx11-dev libxcursor-dev libxext-dev libxfixes-dev \
|
||||
libxi-dev libxrandr-dev libxss-dev libxkbcommon-dev \
|
||||
libasound2-dev libpulse-dev libudev-dev libdbus-1-dev libwayland-dev wayland-protocols
|
||||
@@ -192,13 +193,37 @@ 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.
|
||||
`_Build/linux/install` runs without a matching system Qt. 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.
|
||||
|
||||
As on Windows, the MSVC compiler is not used; Clang is required. `cl.exe` is rejected at configure
|
||||
time.
|
||||
|
||||
The CMake source root is the repository root.
|
||||
|
||||
### Building on NixOS
|
||||
|
||||
A development shell provides Clang, CMake, Ninja, Qt 6, the Vulkan headers, and the SDL2 backend
|
||||
libraries. Enter it and configure exactly as on other Linux distributions; the shell exports
|
||||
`CMAKE_PREFIX_PATH` and `QT_PLUGIN_PATH`, so the `-DCMAKE_PREFIX_PATH="$Qt6_DIR"` argument is not
|
||||
needed:
|
||||
|
||||
```bash
|
||||
nix-shell # or: nix develop
|
||||
git submodule update --init --recursive
|
||||
|
||||
cmake -S . -B _Build/linux -G Ninja -DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
|
||||
|
||||
cmake --build _Build/linux --target launcher --parallel
|
||||
cmake --install _Build/linux --prefix _Build/linux/install
|
||||
```
|
||||
|
||||
The configure step downloads the FFmpeg prebuilts and the `xbyak`/`zydis` sources, so it needs
|
||||
network access; a fully sandboxed `nix build` would require vendoring those inputs. A Vulkan 1.3
|
||||
driver must be available at runtime (on NixOS, `hardware.graphics.enable = true`).
|
||||
|
||||
### Building on macOS
|
||||
|
||||
macOS builds target x86-64 and run under Rosetta 2 on Apple Silicon, so the PS5's x86-64 game
|
||||
|
||||
Generated
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1789426278,
|
||||
"narHash": "sha256-GnotcvKtbTMnoVx6G4E7ZdUX6487Dky74dqq3pU7iRk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "efe6f071ede9d21c37462d2d6682d5e670099684",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
description = "KytyPS5 development shell";
|
||||
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
|
||||
outputs = { self, nixpkgs }:
|
||||
let
|
||||
systems = [ "x86_64-linux" ];
|
||||
forAllSystems = nixpkgs.lib.genAttrs systems;
|
||||
in
|
||||
{
|
||||
devShells = forAllSystems (system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
in
|
||||
{
|
||||
default = import ./nix/devshell.nix { inherit pkgs; };
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
{ pkgs }:
|
||||
|
||||
let
|
||||
# Build-time dependencies. Every -dev package the bundled SDL2 probes for must
|
||||
# be listed here, otherwise SDL2 quietly drops the audio/Wayland/udev backends.
|
||||
buildDeps = with pkgs; [
|
||||
qt6.qtbase # Concurrent, Network, Widgets (launcher)
|
||||
vulkan-headers
|
||||
vulkan-loader
|
||||
vulkan-tools
|
||||
vulkan-validation-layers
|
||||
libglvnd
|
||||
# X11 / input
|
||||
libX11
|
||||
libXcursor
|
||||
libXext
|
||||
libXfixes
|
||||
libXi
|
||||
libXrandr
|
||||
libXScrnSaver
|
||||
libxkbcommon
|
||||
# Wayland
|
||||
wayland
|
||||
wayland-protocols
|
||||
libdecor
|
||||
# audio + device/dbus
|
||||
alsa-lib
|
||||
libpulseaudio
|
||||
systemd # libudev
|
||||
dbus
|
||||
];
|
||||
|
||||
# Libraries the built binaries need at runtime.
|
||||
runtimeLibs = with pkgs; [
|
||||
stdenv.cc.cc.lib # libstdc++ / libgcc_s
|
||||
libgcc.lib
|
||||
qt6.qtbase # libQt6Concurrent/Network/Widgets
|
||||
vulkan-loader
|
||||
libglvnd
|
||||
alsa-lib
|
||||
libpulseaudio
|
||||
libX11
|
||||
libXcursor
|
||||
libXext
|
||||
libXfixes
|
||||
libXi
|
||||
libXrandr
|
||||
libXScrnSaver
|
||||
libxkbcommon
|
||||
wayland
|
||||
libdecor
|
||||
systemd
|
||||
dbus
|
||||
];
|
||||
in
|
||||
pkgs.mkShell {
|
||||
name = "kytyps5";
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
clang
|
||||
lld
|
||||
cmake
|
||||
ninja
|
||||
pkg-config
|
||||
git
|
||||
glslang # provides glslangValidator for the bundled shaders
|
||||
];
|
||||
|
||||
buildInputs = buildDeps;
|
||||
|
||||
shellHook = ''
|
||||
export CMAKE_PREFIX_PATH="${pkgs.qt6.qtbase}:''${CMAKE_PREFIX_PATH:-}"
|
||||
export QT_PLUGIN_PATH="${pkgs.qt6.qtbase}/lib/qt-6/plugins:${pkgs.qt6.qtbase}/lib/qt6/plugins:''${QT_PLUGIN_PATH:-}"
|
||||
export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath runtimeLibs}:''${LD_LIBRARY_PATH:-}"
|
||||
|
||||
if [ -z "''${KYTY_SHELL_QUIET:-}" ]; then
|
||||
echo "KytyPS5 dev shell"
|
||||
echo " cmake -S . -B _Build/linux -G Ninja -DCMAKE_BUILD_TYPE=Release \\"
|
||||
echo " -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++"
|
||||
echo " cmake --build _Build/linux --target launcher --parallel"
|
||||
echo " cmake --install _Build/linux --prefix _Build/linux/install"
|
||||
fi
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
let
|
||||
pkgs = import <nixpkgs> { };
|
||||
in
|
||||
import ./nix/devshell.nix { inherit pkgs; }
|
||||
@@ -43,6 +43,10 @@ int32_t GetUserId() {
|
||||
return g_config->user_id;
|
||||
}
|
||||
|
||||
const std::string& GetAudioInputDevice() {
|
||||
return g_config->audio_input_device;
|
||||
}
|
||||
|
||||
PresentMode GetPresentMode() {
|
||||
return g_config->present_mode;
|
||||
}
|
||||
@@ -59,6 +63,10 @@ bool VrEnabled() {
|
||||
return g_config->vr_enabled;
|
||||
}
|
||||
|
||||
bool AmdCpuEnabled() {
|
||||
return g_config->amd_cpu_enabled;
|
||||
}
|
||||
|
||||
uint32_t GetVblankFrequency() {
|
||||
return std::clamp(g_config->vblank_frequency, 30u, 360u);
|
||||
}
|
||||
|
||||
@@ -43,10 +43,12 @@ struct ConfigOptions {
|
||||
uint32_t screen_height = 720;
|
||||
std::string user_name = "Kyty";
|
||||
int32_t user_id = DEFAULT_USER_ID;
|
||||
std::string audio_input_device;
|
||||
PresentMode present_mode = PresentMode::Mailbox;
|
||||
int32_t gpu_index = -1;
|
||||
bool fullscreen_enabled = false;
|
||||
bool vr_enabled = false;
|
||||
bool amd_cpu_enabled = false;
|
||||
uint32_t vblank_frequency = 60;
|
||||
uint32_t console_language = DEFAULT_CONSOLE_LANGUAGE;
|
||||
bool vulkan_validation_enabled = false;
|
||||
@@ -77,10 +79,12 @@ uint32_t GetScreenWidth();
|
||||
uint32_t GetScreenHeight();
|
||||
const std::string& GetUserName();
|
||||
int32_t GetUserId();
|
||||
const std::string& GetAudioInputDevice();
|
||||
PresentMode GetPresentMode();
|
||||
int32_t GetGpuIndex();
|
||||
bool FullscreenEnabled();
|
||||
bool VrEnabled();
|
||||
bool AmdCpuEnabled();
|
||||
uint32_t GetVblankFrequency();
|
||||
uint32_t GetConsoleLanguage();
|
||||
bool VulkanValidationEnabled();
|
||||
|
||||
@@ -129,6 +129,14 @@ static void WriteImpl(std::string_view text, fmt::text_style style = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
void WriteToConsoleAndLog(std::string_view text) {
|
||||
WriteImpl(text);
|
||||
if (g_initialized && g_direction != Direction::Console) {
|
||||
WriteStdout(text);
|
||||
}
|
||||
Flush();
|
||||
}
|
||||
|
||||
void WriteFatal(std::string_view text) {
|
||||
if (g_direction == Direction::Silent || !g_initialized) {
|
||||
WriteStdout(text);
|
||||
|
||||
@@ -25,6 +25,7 @@ Direction GetDirection();
|
||||
bool IsSilent();
|
||||
void Write(std::string_view text);
|
||||
void Write(fmt::text_style style, std::string_view text);
|
||||
void WriteToConsoleAndLog(std::string_view text);
|
||||
void WriteFatal(std::string_view text);
|
||||
void WriteFatal(fmt::text_style style, std::string_view text);
|
||||
void Flush();
|
||||
|
||||
+8
-46
@@ -8,7 +8,6 @@
|
||||
#include <chrono> // IWYU pragma: keep
|
||||
#include <condition_variable> // IWYU pragma: keep
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS && KYTY_COMPILER == KYTY_COMPILER_CLANG
|
||||
#define KYTY_WIN_CS
|
||||
@@ -197,37 +196,7 @@ struct CondVarPrivate {
|
||||
#endif
|
||||
};
|
||||
|
||||
static std::recursive_mutex g_cond_waiters_mutex;
|
||||
static std::vector<std::pair<int, CondVarPrivate*>> g_cond_waiters;
|
||||
static wait_poll_func_t g_cond_wait_poll_callback = nullptr;
|
||||
|
||||
static void WakeCondVar(CondVarPrivate* cond_var) {
|
||||
#ifdef KYTY_WIN_CS
|
||||
static auto func = ResolveWakeAllConditionVariable();
|
||||
EXIT_NOT_IMPLEMENTED(func == nullptr);
|
||||
func(&cond_var->m_cv);
|
||||
#else
|
||||
cond_var->m_cv.notify_all();
|
||||
#endif
|
||||
}
|
||||
|
||||
static void RegisterCondWaiter(CondVarPrivate* cond_var) {
|
||||
std::lock_guard lock(g_cond_waiters_mutex);
|
||||
g_cond_waiters.emplace_back(Thread::GetThreadIdUnique(), cond_var);
|
||||
}
|
||||
|
||||
static void UnregisterCondWaiter(CondVarPrivate* cond_var) {
|
||||
const auto thread_id = Thread::GetThreadIdUnique();
|
||||
std::lock_guard lock(g_cond_waiters_mutex);
|
||||
|
||||
const auto it = std::find_if(g_cond_waiters.begin(), g_cond_waiters.end(),
|
||||
[thread_id, cond_var](const auto& waiter) {
|
||||
return waiter.first == thread_id && waiter.second == cond_var;
|
||||
});
|
||||
if (it != g_cond_waiters.end()) {
|
||||
g_cond_waiters.erase(it);
|
||||
}
|
||||
}
|
||||
static wait_poll_func_t g_cond_wait_poll_callback = nullptr;
|
||||
|
||||
struct ThreadPrivate {
|
||||
ThreadPrivate(thread_func_t f, void* a): func(f), arg(a), m_thread(&Run, this) {}
|
||||
@@ -365,7 +334,6 @@ CondVar::~CondVar() {
|
||||
}
|
||||
|
||||
void CondVar::Wait(Mutex* mutex) {
|
||||
RegisterCondWaiter(m_cond_var.get());
|
||||
#ifndef KYTY_WIN_CS
|
||||
std::unique_lock<std::recursive_mutex> cpp_lock(mutex->m_mutex->m_mutex, std::adopt_lock_t());
|
||||
#endif
|
||||
@@ -406,7 +374,6 @@ void CondVar::Wait(Mutex* mutex) {
|
||||
}
|
||||
cpp_lock.release();
|
||||
#endif
|
||||
UnregisterCondWaiter(m_cond_var.get());
|
||||
}
|
||||
|
||||
void CondVar::SetWaitPollCallback(wait_poll_func_t callback) {
|
||||
@@ -415,7 +382,6 @@ void CondVar::SetWaitPollCallback(wait_poll_func_t callback) {
|
||||
|
||||
bool CondVar::WaitFor(Mutex* mutex, uint32_t micros) {
|
||||
bool ok = false;
|
||||
RegisterCondWaiter(m_cond_var.get());
|
||||
#ifndef KYTY_WIN_CS
|
||||
std::unique_lock<std::recursive_mutex> cpp_lock(mutex->m_mutex->m_mutex, std::adopt_lock_t());
|
||||
#endif
|
||||
@@ -430,7 +396,6 @@ bool CondVar::WaitFor(Mutex* mutex, uint32_t micros) {
|
||||
std::cv_status::no_timeout);
|
||||
cpp_lock.release();
|
||||
#endif
|
||||
UnregisterCondWaiter(m_cond_var.get());
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -445,16 +410,13 @@ void CondVar::Signal() {
|
||||
}
|
||||
|
||||
void CondVar::SignalAll() {
|
||||
WakeCondVar(m_cond_var.get());
|
||||
}
|
||||
|
||||
void CondVar::SignalThread(int thread_id) {
|
||||
std::lock_guard lock(g_cond_waiters_mutex);
|
||||
for (const auto& waiter: g_cond_waiters) {
|
||||
if (waiter.first == thread_id) {
|
||||
WakeCondVar(waiter.second);
|
||||
}
|
||||
}
|
||||
#ifdef KYTY_WIN_CS
|
||||
static auto func = ResolveWakeAllConditionVariable();
|
||||
EXIT_NOT_IMPLEMENTED(func == nullptr);
|
||||
func(&m_cond_var->m_cv);
|
||||
#else
|
||||
m_cond_var->m_cv.notify_all();
|
||||
#endif
|
||||
}
|
||||
|
||||
int Thread::GetThreadIdUnique() {
|
||||
|
||||
@@ -77,7 +77,6 @@ public:
|
||||
void Signal();
|
||||
void SignalAll();
|
||||
|
||||
static void SignalThread(int thread_id);
|
||||
static void SetWaitPollCallback(wait_poll_func_t callback);
|
||||
|
||||
KYTY_CLASS_NO_COPY(CondVar);
|
||||
|
||||
@@ -196,6 +196,10 @@ void Run(const RunOptions& options) {
|
||||
ClearDebugTextureFolder();
|
||||
|
||||
PrintSystemInfo();
|
||||
std::string title_id;
|
||||
if (Loader::SystemContentParamSfoGetString("TITLE_ID", &title_id) && !title_id.empty()) {
|
||||
Log::WriteToConsoleAndLog(fmt::format("Title ID: {}\n", title_id));
|
||||
}
|
||||
|
||||
int ok = atexit(KytyClose);
|
||||
EXIT_NOT_IMPLEMENTED(ok != 0);
|
||||
|
||||
@@ -32,11 +32,12 @@ private:
|
||||
|
||||
struct BufferCursor {
|
||||
std::span<const uint32_t> commands;
|
||||
uint32_t offset_dw = 0;
|
||||
uint32_t deferred_advance_dw = 0;
|
||||
uint32_t offset_dw = 0;
|
||||
};
|
||||
|
||||
std::vector<BufferCursor> m_buffer_stack;
|
||||
std::span<const uint32_t> m_next_buffer;
|
||||
bool m_chain = false;
|
||||
bool m_suspended = false;
|
||||
bool m_made_progress = false;
|
||||
};
|
||||
@@ -129,7 +130,7 @@ public:
|
||||
[[nodiscard]] bool ShouldSkipPredicatedPackets() const { return m_predicate_skip; }
|
||||
|
||||
Pm4ProcessResult Process(Pm4Execution& execution, std::span<const uint32_t> commands);
|
||||
void ProcessIndirectBuffer(std::span<const uint32_t> commands);
|
||||
void ProcessIndirectBuffer(std::span<const uint32_t> commands, bool chain);
|
||||
|
||||
void SetFlip(const FlipInfo& flip) { m_flip = flip; }
|
||||
|
||||
@@ -143,7 +144,7 @@ private:
|
||||
uint32_t cache_action, uint32_t event_index, uint32_t event_write_source,
|
||||
void* dst_gpu_addr, T value, uint32_t interrupt_selector,
|
||||
uint32_t interrupt_context_id);
|
||||
void ProcessPm4(Pm4Execution& execution, size_t stop_depth);
|
||||
void ProcessPm4(Pm4Execution& execution);
|
||||
void SuspendPm4();
|
||||
CommandScheduler& GetScheduler() const { return m_renderer.GetCommandScheduler(); }
|
||||
CommandBuffer& CurrentBuffer() { return GetScheduler().Current(); }
|
||||
|
||||
@@ -1495,10 +1495,10 @@ KYTY_CP_OP_PARSER(CpOpBranch) {
|
||||
reinterpret_cast<uint64_t>(else_buffer), else_num_dw);
|
||||
|
||||
if (take_then) {
|
||||
cp.ProcessIndirectBuffer({then_buffer, then_num_dw});
|
||||
cp.ProcessIndirectBuffer({then_buffer, then_num_dw}, true);
|
||||
} else if (mode == 2 && else_num_dw != 0) {
|
||||
EXIT_NOT_IMPLEMENTED(else_buffer == nullptr);
|
||||
cp.ProcessIndirectBuffer({else_buffer, else_num_dw});
|
||||
cp.ProcessIndirectBuffer({else_buffer, else_num_dw}, true);
|
||||
}
|
||||
|
||||
return payload_dw;
|
||||
@@ -1959,7 +1959,7 @@ KYTY_CP_OP_PARSER(CpOpIndirectBuffer) {
|
||||
|
||||
GraphicsDbgDumpDcb("ci", indirect_num_dw, indirect_buffer);
|
||||
|
||||
cp.ProcessIndirectBuffer({indirect_buffer, indirect_num_dw});
|
||||
cp.ProcessIndirectBuffer({indirect_buffer, indirect_num_dw}, (control & (1u << 20u)) != 0);
|
||||
|
||||
return 3;
|
||||
}
|
||||
@@ -3635,6 +3635,11 @@ void GraphicsInitJmpTablesShIndirect() {
|
||||
g_hw_sh_indirect_func[Pm4::SPI_SHADER_PGM_RSRC4_GS] = [](KYTY_HW_SH_INDIRECT_ARGS) {
|
||||
HwShIgnoreShaderRegister(cmd_offset, value);
|
||||
};
|
||||
// Toolkit state restoration emits the compiler's GS-front allocation
|
||||
// metadata here. Native GS resource registers already carry that allocation.
|
||||
g_hw_sh_indirect_func[0x0ca] = [](KYTY_HW_SH_INDIRECT_ARGS) {
|
||||
HwShIgnoreShaderRegister(cmd_offset, value);
|
||||
};
|
||||
g_hw_sh_indirect_func[Pm4::SPI_GRAPHICS_SHADER_CONTROL_GS] = [](KYTY_HW_SH_INDIRECT_ARGS) {
|
||||
HwShIgnoreShaderRegister(cmd_offset, value);
|
||||
};
|
||||
|
||||
@@ -386,8 +386,12 @@ void CommandProcessor::WriteReferenceClock(uint64_t dst_address, uint32_t num_by
|
||||
}
|
||||
const auto value = Sync::ReadReferenceClock();
|
||||
std::memcpy(reinterpret_cast<void*>(dst_address), &value, num_bytes);
|
||||
LOGF("\t copy_data reference clock: dst=0x%016" PRIx64 " value=0x%016" PRIx64 " size=%u\n",
|
||||
dst_address, value, num_bytes);
|
||||
static std::atomic<uint32_t> clock_log_count {0};
|
||||
if (clock_log_count.fetch_add(1) < 64) {
|
||||
LOGF("\t copy_data reference clock: dst=0x%016" PRIx64 " value=0x%016" PRIx64
|
||||
" size=%u\n",
|
||||
dst_address, value, num_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
void CommandProcessor::DmaData(uint8_t engine, uint8_t dst_sel, uint8_t dst_cache_policy,
|
||||
@@ -672,20 +676,16 @@ Pm4ProcessResult CommandProcessor::Process(Pm4Execution& execution,
|
||||
Pm4Execution* previous_execution;
|
||||
} execution_scope(*this, execution);
|
||||
|
||||
ProcessPm4(execution, 0);
|
||||
ProcessPm4(execution);
|
||||
return execution.m_buffer_stack.empty() ? Pm4ProcessResult::Complete
|
||||
: Pm4ProcessResult::Blocked;
|
||||
}
|
||||
|
||||
void CommandProcessor::ProcessIndirectBuffer(std::span<const uint32_t> commands) {
|
||||
void CommandProcessor::ProcessIndirectBuffer(std::span<const uint32_t> commands, bool chain) {
|
||||
EXIT_IF(g_current_execution == nullptr);
|
||||
if (commands.empty()) {
|
||||
return;
|
||||
}
|
||||
auto& execution = *g_current_execution;
|
||||
const auto stop_depth = execution.m_buffer_stack.size();
|
||||
execution.m_buffer_stack.push_back({commands});
|
||||
ProcessPm4(execution, stop_depth);
|
||||
EXIT_IF(!g_current_execution->m_next_buffer.empty());
|
||||
g_current_execution->m_next_buffer = commands;
|
||||
g_current_execution->m_chain = chain;
|
||||
}
|
||||
|
||||
void CommandProcessor::SuspendPm4() {
|
||||
@@ -693,21 +693,13 @@ void CommandProcessor::SuspendPm4() {
|
||||
g_current_execution->m_suspended = true;
|
||||
}
|
||||
|
||||
void CommandProcessor::ProcessPm4(Pm4Execution& execution, size_t stop_depth) {
|
||||
while (execution.m_buffer_stack.size() > stop_depth) {
|
||||
void CommandProcessor::ProcessPm4(Pm4Execution& execution) {
|
||||
while (!execution.m_buffer_stack.empty()) {
|
||||
if (g_gpu_state != nullptr) {
|
||||
g_gpu_state->ProcessCommands();
|
||||
}
|
||||
const auto buffer_index = execution.m_buffer_stack.size() - 1;
|
||||
auto& cursor = execution.m_buffer_stack[buffer_index];
|
||||
auto& cursor = execution.m_buffer_stack.back();
|
||||
EXIT_IF(cursor.offset_dw > cursor.commands.size());
|
||||
if (cursor.deferred_advance_dw != 0) {
|
||||
EXIT_IF(cursor.deferred_advance_dw > cursor.commands.size() - cursor.offset_dw);
|
||||
cursor.offset_dw += cursor.deferred_advance_dw;
|
||||
cursor.deferred_advance_dw = 0;
|
||||
execution.m_made_progress = true;
|
||||
continue;
|
||||
}
|
||||
if (cursor.offset_dw == cursor.commands.size()) {
|
||||
execution.m_buffer_stack.pop_back();
|
||||
continue;
|
||||
@@ -783,14 +775,19 @@ void CommandProcessor::ProcessPm4(Pm4Execution& execution, size_t stop_depth) {
|
||||
handler(*this, packet_header & ~1u, packet + 1, remaining_dw, total_dw) + 1;
|
||||
EXIT_IF(packet_dw > remaining_dw);
|
||||
if (execution.m_suspended) {
|
||||
if (execution.m_buffer_stack.size() > buffer_index + 1) {
|
||||
execution.m_buffer_stack[buffer_index].deferred_advance_dw = packet_dw;
|
||||
}
|
||||
return;
|
||||
}
|
||||
EXIT_IF(execution.m_buffer_stack.size() != buffer_index + 1);
|
||||
execution.m_buffer_stack[buffer_index].offset_dw += packet_dw;
|
||||
cursor.offset_dw += packet_dw;
|
||||
execution.m_made_progress = true;
|
||||
if (!execution.m_next_buffer.empty()) {
|
||||
// Chains and taken branches reuse the fetcher; only calls retain a return cursor.
|
||||
if (execution.m_chain) {
|
||||
cursor = {execution.m_next_buffer};
|
||||
} else {
|
||||
execution.m_buffer_stack.push_back({execution.m_next_buffer});
|
||||
}
|
||||
execution.m_next_buffer = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -825,36 +822,55 @@ void CommandProcessor::SetNumInstances(uint32_t num_instances) {
|
||||
|
||||
void CommandProcessor::SetPredication(uint32_t condition, uint32_t op, uint32_t wait_op,
|
||||
const volatile void* address, uint32_t count_in_dwords) {
|
||||
if (wait_op != 0) {
|
||||
BufferFlushAndWait();
|
||||
}
|
||||
|
||||
(void)count_in_dwords;
|
||||
uint64_t value = 0;
|
||||
|
||||
switch (op) {
|
||||
case 0x00: {
|
||||
case 0x00:
|
||||
m_predicate_skip = false;
|
||||
} break;
|
||||
case 0x03: {
|
||||
return;
|
||||
case 0x01: {
|
||||
EXIT_NOT_IMPLEMENTED(address == nullptr);
|
||||
|
||||
auto value = *reinterpret_cast<const volatile uint64_t*>(address);
|
||||
|
||||
switch (condition) {
|
||||
case 0x00: m_predicate_skip = (value != 0); break;
|
||||
case 0x01: m_predicate_skip = (value == 0); break;
|
||||
default: EXIT("unknown predication condition: 0x%08" PRIx32 "\n", condition);
|
||||
}
|
||||
static std::atomic<uint32_t> log_count {0};
|
||||
if (log_count.fetch_add(1) < 128) {
|
||||
LOGF("\t bool predication: addr=0x%016" PRIx64 ", value=0x%016" PRIx64
|
||||
", condition=%" PRIu32 ", skip=%u, wait_op=%" PRIu32 "\n",
|
||||
reinterpret_cast<uint64_t>(address), value, condition,
|
||||
m_predicate_skip ? 1u : 0u, wait_op);
|
||||
// One begin/end pair per DB; bit 63 marks each counter ready.
|
||||
constexpr uint64_t ready_bit = 1ull << 63u;
|
||||
const auto* results = reinterpret_cast<const volatile uint64_t*>(address);
|
||||
for (uint32_t db = 0; db < 16u; db++) {
|
||||
const auto begin = results[db * 2u];
|
||||
const auto end = results[db * 2u + 1u];
|
||||
if ((begin & end & ready_bit) == 0) {
|
||||
if (wait_op == 0) {
|
||||
SuspendPm4();
|
||||
} else {
|
||||
m_predicate_skip = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
value += end - begin;
|
||||
}
|
||||
} break;
|
||||
case 0x03:
|
||||
if (wait_op != 0) {
|
||||
BufferFlushAndWait();
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(address == nullptr);
|
||||
value = *reinterpret_cast<const volatile uint64_t*>(address);
|
||||
break;
|
||||
default: EXIT("unknown predication op: 0x%08" PRIx32 "\n", op);
|
||||
}
|
||||
switch (condition) {
|
||||
case 0x00: m_predicate_skip = (value != 0); break;
|
||||
case 0x01: m_predicate_skip = (value == 0); break;
|
||||
default: EXIT("unknown predication condition: 0x%08" PRIx32 "\n", condition);
|
||||
}
|
||||
if (op == 0x03) {
|
||||
static std::atomic<uint32_t> log_count {0};
|
||||
if (log_count.fetch_add(1) < 128) {
|
||||
LOGF("\t bool predication: addr=0x%016" PRIx64 ", value=0x%016" PRIx64
|
||||
", condition=%" PRIu32 ", skip=%u, wait_op=%" PRIu32 "\n",
|
||||
reinterpret_cast<uint64_t>(address), value, condition,
|
||||
m_predicate_skip ? 1u : 0u, wait_op);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CommandProcessor::DrawIndex(DrawIndexArgs args) {
|
||||
@@ -1090,17 +1106,6 @@ void CommandProcessor::DispatchDirect(uint32_t thread_group_x, uint32_t thread_g
|
||||
// local_x = std::max(cs.num_thread_x, 1u);
|
||||
// local_y = std::max(cs.num_thread_y, 1u);
|
||||
// local_z = std::max(cs.num_thread_z, 1u);
|
||||
if (cs.wave_size == 64u) {
|
||||
static std::atomic_bool logged_wave64_shader {false};
|
||||
if (!logged_wave64_shader.exchange(true, std::memory_order_relaxed)) {
|
||||
LOGF("warning: executing wave64 compute shader cs=0x%016" PRIx64 "\n",
|
||||
cs.data_addr);
|
||||
std::printf("warning: executing wave64 compute shader cs=0x%016" PRIx64 "\n",
|
||||
cs.data_addr);
|
||||
std::fflush(stdout);
|
||||
}
|
||||
}
|
||||
|
||||
m_renderer.GetRenderExecutor().DispatchDirect(m_submit_id, CurrentBuffer(), thread_group_x,
|
||||
thread_group_y, thread_group_z, mode);
|
||||
}
|
||||
@@ -1292,6 +1297,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
||||
break;
|
||||
case 0x14:
|
||||
case 0x28:
|
||||
case 0x2f:
|
||||
if (event_index == 0x00) {
|
||||
write64(false);
|
||||
return;
|
||||
@@ -1299,7 +1305,6 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
||||
break;
|
||||
case 0x2b:
|
||||
case 0x2d:
|
||||
case 0x2f:
|
||||
case 0x30:
|
||||
if (event_index == 0x00 && !with_interrupt) {
|
||||
write64(false);
|
||||
|
||||
@@ -86,6 +86,7 @@ struct TileMipLayout {
|
||||
uint32_t padded_height = 0;
|
||||
uint32_t tail_x = 0;
|
||||
uint32_t tail_y = 0;
|
||||
bool operator==(const TileMipLayout&) const = default;
|
||||
};
|
||||
|
||||
struct TileSurfaceLayout {
|
||||
|
||||
+1
-6
@@ -44,12 +44,7 @@ FaultManager::FaultManager(GraphicContext& graphics, CommandScheduler& scheduler
|
||||
&m_fault_process_desc_layout),
|
||||
"create fault-buffer descriptor layout");
|
||||
|
||||
vk::ShaderModuleCreateInfo module_info {};
|
||||
module_info.codeSize = std::size(FAULT_BUFFER_PROCESS_SPV) * sizeof(uint32_t);
|
||||
module_info.pCode = FAULT_BUFFER_PROCESS_SPV;
|
||||
vk::ShaderModule module = nullptr;
|
||||
RequireVulkanSuccess(m_graphics.device.createShaderModule(&module_info, nullptr, &module),
|
||||
"create fault-buffer shader module");
|
||||
const auto module = CompileSPV(FAULT_BUFFER_PROCESS_SPV, m_graphics.device);
|
||||
|
||||
vk::PipelineLayoutCreateInfo pipeline_layout_info {};
|
||||
pipeline_layout_info.setLayoutCount = 1;
|
||||
|
||||
+104
-119
@@ -33,22 +33,24 @@ namespace {
|
||||
|
||||
constexpr uint64_t NumFramesBeforeRemoval = 32;
|
||||
|
||||
[[nodiscard]] bool DecodeDccClear(const TextureCache::ImageDesc& desc, vk::Format format,
|
||||
uint32_t fill, vk::ClearColorValue& clear) {
|
||||
const auto code = static_cast<uint8_t>(fill);
|
||||
if (fill != static_cast<uint32_t>(code) * 0x01010101u) {
|
||||
return false;
|
||||
[[nodiscard]] bool DecodeDccClear(const TextureCache::ImageDesc& desc, uint8_t code,
|
||||
vk::ClearColorValue& clear) {
|
||||
switch (code) {
|
||||
case 0x00:
|
||||
case 0x20:
|
||||
case 0x40:
|
||||
case 0x80:
|
||||
case 0xc0: break;
|
||||
default: return false;
|
||||
}
|
||||
const auto& metadata = desc.info.metadata;
|
||||
const auto format = desc.view_info.format;
|
||||
if (code == 0x20) {
|
||||
// Clear-to-register is a color-buffer operation; the texture pipe cannot decode it.
|
||||
return desc.type == TextureCache::BindingType::RenderTarget &&
|
||||
metadata.dcc_clear_register_valid &&
|
||||
DecodePackedColorClear(format, metadata.dcc_clear_word, clear);
|
||||
}
|
||||
if (code != 0x00 && code != 0x40 && code != 0x80 && code != 0xc0) {
|
||||
return false;
|
||||
}
|
||||
clear = {};
|
||||
if (code == 0x00) {
|
||||
return true;
|
||||
@@ -206,6 +208,10 @@ bool TextureCache::SameBacking(const ImageInfo& cached, const ImageInfo& request
|
||||
if (cached.extent != requested.extent) {
|
||||
return false;
|
||||
}
|
||||
if (cached.resources.levels < requested.resources.levels ||
|
||||
cached.resources.layers < requested.resources.layers) {
|
||||
return false;
|
||||
}
|
||||
if (cached.samples != requested.samples) {
|
||||
return false;
|
||||
}
|
||||
@@ -323,10 +329,8 @@ void TextureCache::DeleteImage(ImageId id) {
|
||||
if (image->info.HasMetadata()) {
|
||||
const auto metadata = m_surface_metas.find(image->info.metadata.range.address);
|
||||
if (metadata != m_surface_metas.end() &&
|
||||
((image->info.metadata.kind == ImageMetadataKind::Dcc &&
|
||||
metadata->second.type == MetaDataInfo::Type::Dcc) ||
|
||||
(image->info.metadata.kind == ImageMetadataKind::Htile &&
|
||||
metadata->second.type == MetaDataInfo::Type::HTile))) {
|
||||
image->info.metadata.kind == ImageMetadataKind::Htile &&
|
||||
metadata->second.type == MetaDataInfo::Type::HTile) {
|
||||
// A later binding may have reused this address for another metadata type.
|
||||
m_surface_metas.erase(metadata);
|
||||
}
|
||||
@@ -843,6 +847,16 @@ TextureCache::OverlapResult TextureCache::ResolveOverlap(const ImageInfo& reques
|
||||
ImageViewOps::FormatsCompatible(cached.info.pixel_format, requested.pixel_format)) {
|
||||
return {ExpandImage(requested, cached_id)};
|
||||
}
|
||||
// PS5 mip tails can expose more levels without increasing the guest allocation.
|
||||
if (requested.pixel_format == cached.info.pixel_format &&
|
||||
requested.type == cached.info.type && requested.resources > cached.info.resources &&
|
||||
(requested.data.size > cached.info.data.size ||
|
||||
(requested.data.size == cached.info.data.size &&
|
||||
requested.extent == cached.info.extent &&
|
||||
cached.info.resources.levels > 1 &&
|
||||
requested.resources.layers == cached.info.resources.layers))) {
|
||||
return {ExpandImage(requested, cached_id)};
|
||||
}
|
||||
if (requested.pixel_format != cached.info.pixel_format ||
|
||||
requested.data.size <= cached.info.data.size) {
|
||||
const auto result_id = merged_id ? merged_id : cached_id;
|
||||
@@ -852,9 +866,6 @@ TextureCache::OverlapResult TextureCache::ResolveOverlap(const ImageInfo& reques
|
||||
? result_id
|
||||
: ImageId {}};
|
||||
}
|
||||
if (requested.type == cached.info.type && requested.resources > cached.info.resources) {
|
||||
return {ExpandImage(requested, cached_id)};
|
||||
}
|
||||
EXIT("TextureCache: unresolvable equal-address image overlap, address=0x%016" PRIx64
|
||||
" requested=%ux%u "
|
||||
"cached=%ux%u requested_size=0x%016" PRIx64 " cached_size=0x%016" PRIx64
|
||||
@@ -1124,53 +1135,69 @@ void TextureCache::InitializeImage(ImageId id) {
|
||||
}
|
||||
}
|
||||
|
||||
void TextureCache::PrepareDccClear(ImageId id, const ImageDesc& desc) {
|
||||
void TextureCache::MaterializeDccClear(ImageId id, const ImageDesc& desc,
|
||||
uint32_t metadata_base_layer) {
|
||||
if (desc.info.metadata.kind != ImageMetadataKind::Dcc) {
|
||||
return;
|
||||
}
|
||||
auto& image = m_slot_images[id];
|
||||
image.info.metadata = desc.info.metadata;
|
||||
auto [entry, inserted] = m_surface_metas.try_emplace(
|
||||
desc.info.metadata.range.address, MetaDataInfo {.type = MetaDataInfo::Type::Dcc});
|
||||
auto& metadata = entry->second;
|
||||
if (!inserted && metadata.type == MetaDataInfo::Type::PendingDcc) {
|
||||
metadata.type = MetaDataInfo::Type::Dcc;
|
||||
} else if (metadata.type != MetaDataInfo::Type::Dcc) {
|
||||
EXIT("TextureCache: image reuses non-DCC metadata\n");
|
||||
const auto range = desc.info.metadata.range;
|
||||
{
|
||||
std::scoped_lock lock {m_lock};
|
||||
auto& image = m_slot_images[id];
|
||||
image.info.metadata = desc.info.metadata;
|
||||
// A native DCC allocation must not retain a reused HTile/CMask/FMask interpretation.
|
||||
m_surface_metas.erase(range.address);
|
||||
if (range.size == 0 || desc.info.resources.levels != 1 || image.info.resources.levels != 1) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (metadata.clear_mask == 0 || image.info.resources.levels != 1 ||
|
||||
desc.info.metadata.range.size == 0 || metadata.fill_size < desc.info.metadata.range.size) {
|
||||
return;
|
||||
}
|
||||
vk::ClearValue clear {};
|
||||
if (!DecodeDccClear(desc, image.backing.format, metadata.fill_value, clear.color)) {
|
||||
return;
|
||||
const auto layers = desc.info.TransferLayers();
|
||||
// These one-mip surfaces use complete 4 KiB DCC metadata blocks.
|
||||
constexpr uint64_t MetadataBlockSize = 0x1000;
|
||||
if (!range.Valid() || range.address % MetadataBlockSize != 0 || layers == 0 ||
|
||||
range.size % layers != 0 || (range.size / layers) % MetadataBlockSize != 0) {
|
||||
EXIT("TextureCache: DCC slices must contain aligned 4 KiB blocks\n");
|
||||
}
|
||||
const auto& view = desc.view_info;
|
||||
const bool volume_texture = image.info.IsVolume() && view.type == vk::ImageViewType::e3D;
|
||||
const auto first = volume_texture ? 0u : view.base_layer;
|
||||
const auto count = volume_texture ? std::max(image.info.extent.depth >> view.base_level, 1u)
|
||||
: view.layer_count;
|
||||
if (first >= 32 || count > 32 - first) {
|
||||
return;
|
||||
const bool volume_texture = desc.info.IsVolume() && view.type == vk::ImageViewType::e3D;
|
||||
const auto first = volume_texture ? 0u : metadata_base_layer;
|
||||
const auto image_first = volume_texture ? 0u : view.base_layer;
|
||||
const auto count = volume_texture ? desc.info.extent.depth : view.layer_count;
|
||||
if (first >= layers || count > layers - first) {
|
||||
EXIT("TextureCache: DCC view exceeds its native metadata slices\n");
|
||||
}
|
||||
// The metadata fill covers the complete allocation. Consume each layer only after its
|
||||
// native image contents exist; already materialized layers may have been rendered since.
|
||||
for (uint32_t layer = first; layer < first + count;) {
|
||||
if ((metadata.clear_mask & (1u << layer)) == 0) {
|
||||
layer++;
|
||||
// Finish native metadata writes before reading backing bytes. This can submit the scheduler,
|
||||
// so discovery runs before final draw uploads and never holds the texture lock across it.
|
||||
if (m_buffer_cache.IsRegionGpuModified(range.address, range.size)) {
|
||||
m_buffer_cache.ReadMemory(range.address, range.size, false);
|
||||
}
|
||||
const auto slice_size = range.size / layers;
|
||||
for (uint32_t slice = 0; slice < count; slice++) {
|
||||
const auto address = range.address + slice_size * (first + slice);
|
||||
uint8_t code = 0;
|
||||
if (!LibKernel::Memory::TryReadBacking(address, &code, sizeof(code))) {
|
||||
EXIT("TextureCache: failed to read DCC metadata backing\n");
|
||||
}
|
||||
vk::ClearValue clear {};
|
||||
if (!DecodeDccClear(desc, code, clear.color)) {
|
||||
continue;
|
||||
}
|
||||
const auto start = layer;
|
||||
uint32_t mask = 0;
|
||||
do {
|
||||
mask |= 1u << layer++;
|
||||
} while (layer < first + count && (metadata.clear_mask & (1u << layer)) != 0);
|
||||
ClearImage(m_scheduler.Current(), id,
|
||||
{vk::ImageAspectFlagBits::eColor, view.base_level, view.level_count, start,
|
||||
layer - start},
|
||||
clear);
|
||||
metadata.clear_mask &= ~mask;
|
||||
std::vector<uint8_t> bytes(slice_size);
|
||||
if (!LibKernel::Memory::TryReadBacking(address, bytes.data(), bytes.size())) {
|
||||
EXIT("TextureCache: failed to read DCC metadata slice\n");
|
||||
}
|
||||
if (!std::all_of(bytes.begin(), bytes.end(), [code](uint8_t byte) { return byte == code; })) {
|
||||
continue;
|
||||
}
|
||||
{
|
||||
std::scoped_lock lock {m_lock};
|
||||
ClearImage(m_scheduler.Current(), id, view.format,
|
||||
{vk::ImageAspectFlagBits::eColor, view.base_level, view.level_count,
|
||||
image_first + slice, 1}, clear);
|
||||
}
|
||||
// Native expanded keys own consumption. Existing buffer tracking publishes this CPU
|
||||
// write to future GPU readers; FillBuffer can fault and must run outside the texture lock.
|
||||
m_buffer_cache.FillBuffer(address, slice_size, UINT32_MAX, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1210,7 +1237,8 @@ void TextureCache::AssociateStencil(ImageId depth_id, GuestRange stencil) {
|
||||
ImageId association {};
|
||||
for (const auto id: FindImagesInRegion(stencil.address, stencil.size, false)) {
|
||||
const auto owner = m_slot_images.try_get(id);
|
||||
if (owner != nullptr && owner->info.data.address == stencil.address) {
|
||||
if (owner != nullptr && owner->info.data == stencil &&
|
||||
owner->info.extent == depth.info.extent) {
|
||||
association = id;
|
||||
}
|
||||
}
|
||||
@@ -1235,6 +1263,7 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
|
||||
std::scoped_lock lock {m_lock};
|
||||
return GetNullImage(desc);
|
||||
}
|
||||
const auto metadata_base_layer = desc.view_info.base_layer;
|
||||
|
||||
ImageId result {};
|
||||
{
|
||||
@@ -1302,6 +1331,7 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
|
||||
image.tick_accessed_last = m_scheduler.CurrentTick();
|
||||
TouchImage(image);
|
||||
}
|
||||
MaterializeDccClear(result, desc, metadata_base_layer);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1365,7 +1395,6 @@ vk::ImageView TextureCache::FindTexture(ImageId id, const ImageDesc& desc) {
|
||||
image.MarkGpuModified();
|
||||
}
|
||||
if (!image.info.data.Empty()) {
|
||||
PrepareDccClear(id, desc);
|
||||
RefreshImage(id);
|
||||
}
|
||||
switch (desc.type) {
|
||||
@@ -1395,7 +1424,6 @@ vk::ImageView TextureCache::FindRenderTarget(ImageId id, const ImageDesc& desc)
|
||||
TouchImage(image);
|
||||
image.MarkGpuModified();
|
||||
image.usage.render_target = true;
|
||||
PrepareDccClear(id, desc);
|
||||
RefreshImage(id);
|
||||
CommitGpuWrite(image);
|
||||
TrackImageDownload(id, image);
|
||||
@@ -1419,16 +1447,9 @@ vk::ImageView TextureCache::FindDepthTarget(ImageId id, const ImageDesc& desc) {
|
||||
RefreshImage(id);
|
||||
if (desc.info.HasMetadata()) {
|
||||
image.info.metadata = desc.info.metadata;
|
||||
auto [metadata, inserted] =
|
||||
m_surface_metas.try_emplace(desc.info.metadata.range.address,
|
||||
MetaDataInfo {.type = MetaDataInfo::Type::HTile,
|
||||
.clear_mask = image.info.htile_clear_mask});
|
||||
if (!inserted && metadata->second.type != MetaDataInfo::Type::HTile) {
|
||||
// PS5 allocations can reuse DCC storage as HTile while the old color image is cached.
|
||||
// The depth binding defines the new type; incompatible fill state cannot carry over.
|
||||
metadata->second = {.type = MetaDataInfo::Type::HTile,
|
||||
.clear_mask = image.info.htile_clear_mask};
|
||||
}
|
||||
m_surface_metas.emplace(desc.info.metadata.range.address,
|
||||
MetaDataInfo {.type = MetaDataInfo::Type::HTile,
|
||||
.clear_mask = image.info.htile_clear_mask});
|
||||
}
|
||||
CommitGpuWrite(image);
|
||||
if (desc.info.HasStencil()) {
|
||||
@@ -1516,12 +1537,12 @@ bool TextureCache::ClearImageFromBuffer(CommandBuffer& command, uint64_t address
|
||||
}
|
||||
clear.depthStencil.stencil = stencil_clear;
|
||||
}
|
||||
ClearImage(command, selected,
|
||||
ClearImage(command, selected, image.backing.format,
|
||||
{aspect, 0, image.info.resources.levels, 0, image.info.TransferLayers()}, clear);
|
||||
return true;
|
||||
}
|
||||
|
||||
void TextureCache::ClearImage(CommandBuffer& command, ImageId id,
|
||||
void TextureCache::ClearImage(CommandBuffer& command, ImageId id, vk::Format format,
|
||||
const vk::ImageSubresourceRange& range, const vk::ClearValue& clear) {
|
||||
auto& image = m_slot_images[id];
|
||||
const auto aspects = image.info.IsDepth() ? ImageViewOps::DepthAspectMask(image.backing.format)
|
||||
@@ -1546,11 +1567,12 @@ void TextureCache::ClearImage(CommandBuffer& command, ImageId id,
|
||||
}
|
||||
}
|
||||
command.EndRendering();
|
||||
if (image.info.IsVolume() && !full_image) {
|
||||
// Transfer clears use the backing format; aliased clears must encode through their view.
|
||||
if (format != image.backing.format || (image.info.IsVolume() && !full_image)) {
|
||||
EXIT_NOT_IMPLEMENTED(range.aspectMask != vk::ImageAspectFlagBits::eColor ||
|
||||
range.levelCount != 1);
|
||||
ImageViewInfo view {};
|
||||
view.format = image.backing.format;
|
||||
view.format = format;
|
||||
view.type = range.layerCount == 1 ? vk::ImageViewType::e2D : vk::ImageViewType::e2DArray;
|
||||
view.base_level = range.baseMipLevel;
|
||||
view.base_layer = range.baseArrayLayer;
|
||||
@@ -1861,72 +1883,32 @@ void TextureCache::InvalidateCpuAliases(uint64_t address, uint64_t size) {
|
||||
bool TextureCache::IsMeta(uint64_t address) {
|
||||
std::scoped_lock lock {m_lock};
|
||||
const auto found = m_surface_metas.find(address);
|
||||
return found != m_surface_metas.end() && found->second.type != MetaDataInfo::Type::PendingDcc;
|
||||
return found != m_surface_metas.end();
|
||||
}
|
||||
|
||||
bool TextureCache::IsMetaCleared(uint64_t address, uint32_t slice, uint32_t* fill_value) {
|
||||
bool TextureCache::IsMetaCleared(uint64_t address, uint32_t slice) {
|
||||
std::scoped_lock lock {m_lock};
|
||||
const auto found = m_surface_metas.find(address);
|
||||
if (found == m_surface_metas.end() || found->second.type == MetaDataInfo::Type::PendingDcc ||
|
||||
slice >= 32) {
|
||||
if (found == m_surface_metas.end() || slice >= 32) {
|
||||
return false;
|
||||
}
|
||||
if (fill_value != nullptr) {
|
||||
*fill_value = found->second.fill_value;
|
||||
}
|
||||
return (found->second.clear_mask & (1u << slice)) != 0;
|
||||
}
|
||||
|
||||
bool TextureCache::ClearMeta(uint64_t address) {
|
||||
std::scoped_lock lock {m_lock};
|
||||
const auto found = m_surface_metas.find(address);
|
||||
if (found == m_surface_metas.end() || found->second.type == MetaDataInfo::Type::PendingDcc ||
|
||||
found->second.type == MetaDataInfo::Type::Dcc) {
|
||||
// Preserve the broad metadata-clear operation for CMask/FMask/HTile. DCC requires a
|
||||
// validated fill value, so an arbitrary compute write must not clear it.
|
||||
if (found == m_surface_metas.end()) {
|
||||
return false;
|
||||
}
|
||||
found->second.clear_mask = UINT32_MAX;
|
||||
return true;
|
||||
}
|
||||
|
||||
void TextureCache::TrackDccFill(uint64_t address, uint64_t size, uint32_t fill_value) {
|
||||
if (!GuestRange {address, size}.Valid()) {
|
||||
EXIT("TextureCache: invalid DCC fill range\n");
|
||||
}
|
||||
// DCC fills use a repeated byte code. Require all four bytes of the detected dword to agree,
|
||||
// and mark only recognized deferred-clear encodings as logically clear.
|
||||
const auto dcc_clear_mask = [fill_value] {
|
||||
const auto code = static_cast<uint8_t>(fill_value);
|
||||
if (fill_value != static_cast<uint32_t>(code) * 0x01010101u) {
|
||||
return 0u;
|
||||
}
|
||||
switch (code) {
|
||||
case 0x00:
|
||||
case 0x20:
|
||||
case 0x40:
|
||||
case 0x80:
|
||||
case 0xc0: return UINT32_MAX;
|
||||
default: return 0u;
|
||||
}
|
||||
}();
|
||||
std::scoped_lock lock {m_lock};
|
||||
// The guest dispatch still writes metadata. An unknown address remains PendingDcc until an
|
||||
// image descriptor confirms its role; never reinterpret CMask/FMask/HTile as DCC.
|
||||
const auto found = m_surface_metas.try_emplace(address).first;
|
||||
if (found->second.type == MetaDataInfo::Type::PendingDcc ||
|
||||
found->second.type == MetaDataInfo::Type::Dcc) {
|
||||
found->second.clear_mask = dcc_clear_mask;
|
||||
found->second.fill_value = fill_value;
|
||||
found->second.fill_size = size;
|
||||
}
|
||||
}
|
||||
|
||||
bool TextureCache::TouchMeta(uint64_t address, uint32_t slice, bool is_clear) {
|
||||
std::scoped_lock lock {m_lock};
|
||||
const auto found = m_surface_metas.find(address);
|
||||
if (found == m_surface_metas.end() || found->second.type == MetaDataInfo::Type::PendingDcc ||
|
||||
slice >= 32) {
|
||||
if (found == m_surface_metas.end() || slice >= 32) {
|
||||
return false;
|
||||
}
|
||||
if (is_clear) {
|
||||
@@ -1942,10 +1924,13 @@ void TextureCache::UnmapMemory(uint64_t address, uint64_t size) {
|
||||
EXIT("TextureCache: invalid unmap range\n");
|
||||
}
|
||||
std::scoped_lock lock {m_lock};
|
||||
const auto end = address + size;
|
||||
for (auto metadata = m_surface_metas.lower_bound(address);
|
||||
metadata != m_surface_metas.end() && metadata->first < end;) {
|
||||
metadata = m_surface_metas.erase(metadata);
|
||||
for (auto metadata = m_surface_metas.begin(); metadata != m_surface_metas.end();) {
|
||||
const auto base = metadata->first;
|
||||
if (base >= address && base < address + size) {
|
||||
metadata = m_surface_metas.erase(metadata);
|
||||
} else {
|
||||
++metadata;
|
||||
}
|
||||
}
|
||||
auto images = FindImagesInRegion(address, size, false);
|
||||
for (const auto id: images) {
|
||||
|
||||
+8
-17
@@ -64,11 +64,8 @@ public:
|
||||
[[nodiscard]] bool IsRegionGpuModified(uint64_t address, uint64_t size);
|
||||
|
||||
[[nodiscard]] bool IsMeta(uint64_t address);
|
||||
[[nodiscard]] bool IsMetaCleared(uint64_t address, uint32_t slice,
|
||||
uint32_t* fill_value = nullptr);
|
||||
[[nodiscard]] bool IsMetaCleared(uint64_t address, uint32_t slice);
|
||||
[[nodiscard]] bool ClearMeta(uint64_t address);
|
||||
// Record deferred DCC state while the original guest dispatch writes the metadata.
|
||||
void TrackDccFill(uint64_t address, uint64_t size, uint32_t fill_value);
|
||||
[[nodiscard]] bool TouchMeta(uint64_t address, uint32_t slice, bool is_clear);
|
||||
|
||||
void UnmapMemory(uint64_t address, uint64_t size);
|
||||
@@ -81,17 +78,10 @@ private:
|
||||
struct ImageDownload;
|
||||
|
||||
struct MetaDataInfo {
|
||||
// A guest metadata-fill dispatch may initialize DCC before its render target is bound.
|
||||
// PendingDcc retains that exact fill until an image binding classifies the address,
|
||||
// without exposing an unconfirmed buffer address to the normal metadata heuristics.
|
||||
// Keep all surface metadata in one entry so CMask/FMask can be
|
||||
// registered beside HTile and DCC without introducing parallel tracking paths.
|
||||
enum class Type : uint8_t { PendingDcc, CMask, FMask, HTile, Dcc };
|
||||
enum class Type : uint8_t { CMask, FMask, HTile };
|
||||
|
||||
Type type = Type::PendingDcc;
|
||||
uint32_t clear_mask = 0;
|
||||
uint32_t fill_value = 0xffffffffu;
|
||||
uint64_t fill_size = 0;
|
||||
Type type;
|
||||
uint32_t clear_mask = UINT32_MAX;
|
||||
};
|
||||
|
||||
struct OverlapResult {
|
||||
@@ -149,7 +139,8 @@ private:
|
||||
ImageId cached);
|
||||
[[nodiscard]] ImageId ExpandImage(const ImageInfo& info, ImageId source);
|
||||
void RefreshImage(ImageId id);
|
||||
void PrepareDccClear(ImageId id, const ImageDesc& desc);
|
||||
void MaterializeDccClear(ImageId id, const ImageDesc& desc,
|
||||
uint32_t metadata_base_layer);
|
||||
void InitializeImage(ImageId id);
|
||||
[[nodiscard]] TextureTransfer
|
||||
BuildTextureTransfer(const Image& image, BindingType binding, TransferDirection direction) const;
|
||||
@@ -160,8 +151,8 @@ private:
|
||||
void DownloadDepth(Image& image, Buffer& destination, uint64_t destination_offset);
|
||||
void CommitGpuWrite(Image& image);
|
||||
// Caller holds m_lock. Volume layer ranges select depth slices.
|
||||
void ClearImage(CommandBuffer& command, ImageId id, const vk::ImageSubresourceRange& range,
|
||||
const vk::ClearValue& clear);
|
||||
void ClearImage(CommandBuffer& command, ImageId id, vk::Format format,
|
||||
const vk::ImageSubresourceRange& range, const vk::ClearValue& clear);
|
||||
void PrepareImageCopy(Image& image);
|
||||
void RefreshCopySource(ImageId id);
|
||||
[[nodiscard]] bool CopyD16(Image& destination, Image& source);
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "graphics/host_gpu/vulkanCommon.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstdarg>
|
||||
@@ -45,8 +46,12 @@ static vk::StencilOp ConvertStencilOp(uint8_t value, uint8_t write_mask, uint8_t
|
||||
switch (static_cast<Prospero::StencilOp>(value)) {
|
||||
case Prospero::StencilOp::kKeep: return vk::StencilOp::eKeep;
|
||||
case Prospero::StencilOp::kZero: return vk::StencilOp::eZero;
|
||||
case Prospero::StencilOp::kReplaceTest:
|
||||
case Prospero::StencilOp::kReplaceOp: return vk::StencilOp::eReplace;
|
||||
case Prospero::StencilOp::kReplaceTest: return vk::StencilOp::eReplace;
|
||||
case Prospero::StencilOp::kReplaceOp:
|
||||
if ((op_value & write_mask) == 0) {
|
||||
return vk::StencilOp::eZero;
|
||||
}
|
||||
return vk::StencilOp::eReplace;
|
||||
case Prospero::StencilOp::kAddClamp: return vk::StencilOp::eIncrementAndClamp;
|
||||
case Prospero::StencilOp::kSubClamp: return vk::StencilOp::eDecrementAndClamp;
|
||||
case Prospero::StencilOp::kInvert: return vk::StencilOp::eInvert;
|
||||
@@ -66,9 +71,38 @@ static vk::StencilOp ConvertStencilOp(uint8_t value, uint8_t write_mask, uint8_t
|
||||
}
|
||||
}
|
||||
|
||||
static bool UsesStencilOpValue(uint8_t fail, uint8_t pass, uint8_t depth_fail) {
|
||||
constexpr auto replace_op = static_cast<uint8_t>(Prospero::StencilOp::kReplaceOp);
|
||||
return fail == replace_op || pass == replace_op || depth_fail == replace_op;
|
||||
static vk::StencilOpState ConvertStencilState(
|
||||
uint8_t compare, const std::array<uint8_t, 3>& operations, uint8_t op_value,
|
||||
const vk::StencilOpState& state) {
|
||||
const auto test_value = state.reference;
|
||||
auto reference = test_value;
|
||||
auto required_bits = state.compareMask;
|
||||
if (compare == static_cast<uint8_t>(vk::CompareOp::eAlways) ||
|
||||
compare == static_cast<uint8_t>(vk::CompareOp::eNever)) {
|
||||
required_bits = 0;
|
||||
}
|
||||
std::array<vk::StencilOp, 3> converted {};
|
||||
for (size_t i = 0; i < operations.size(); i++) {
|
||||
converted[i] = ConvertStencilOp(operations[i], state.writeMask, op_value);
|
||||
if (converted[i] != vk::StencilOp::eReplace) {
|
||||
continue;
|
||||
}
|
||||
auto replacement = test_value;
|
||||
if (static_cast<Prospero::StencilOp>(operations[i]) == Prospero::StencilOp::kReplaceOp) {
|
||||
replacement = op_value;
|
||||
}
|
||||
if (((reference ^ replacement) & required_bits & state.writeMask) != 0) {
|
||||
DepthFatal("unsupported stencil replacement: compare=%u, compare mask=0x%02" PRIx32
|
||||
", write mask=0x%02" PRIx32 ", operation value=0x%02" PRIx8
|
||||
", test value=0x%02" PRIx32,
|
||||
compare, state.compareMask, state.writeMask, op_value, test_value);
|
||||
}
|
||||
// Vulkan shares one reference between comparison and every replacement on this face.
|
||||
reference = (reference & ~state.writeMask) | (replacement & state.writeMask);
|
||||
required_bits |= state.writeMask;
|
||||
}
|
||||
return {converted[0], converted[1], converted[2], static_cast<vk::CompareOp>(compare),
|
||||
state.compareMask, state.writeMask, reference};
|
||||
}
|
||||
|
||||
[[nodiscard]] static vk::Format ResolveHostDepthAttachmentFormat(const CommandBuffer& buffer,
|
||||
@@ -125,8 +159,8 @@ static TextureCache::ImageDesc MakeDepthTargetDesc(const CommandBuffer& buffer,
|
||||
DepthFatal("invalid depth view: base=%u last=%u", z.depth_view.slice_start,
|
||||
z.depth_view.slice_max);
|
||||
}
|
||||
if (z.z_info.expclear_enabled || z.stencil_info.expclear_enabled ||
|
||||
z.z_info.partially_resident ||
|
||||
// EXPCLEAR permits an HTile acceleration state; the host attachment is already expanded.
|
||||
if (z.z_info.partially_resident ||
|
||||
z.stencil_info.partially_resident || z.z_info.max_mip_level != 0 ||
|
||||
z.depth_view.current_mip_level != 0 || unsupported_shading_rate_encoding ||
|
||||
depth_address == 0 || (depth_address & 0xffffu) != 0) {
|
||||
@@ -296,31 +330,20 @@ void RenderExecutor::ResolveRenderDepthTarget(CommandBuffer& buffer, RenderDepth
|
||||
const uint8_t back_write_mask = stencil_ops_disabled ? 0 : sm.stencil_writemask_bf;
|
||||
if (dc.stencilfunc > static_cast<uint8_t>(vk::CompareOp::eAlways) ||
|
||||
(dc.backface_enable &&
|
||||
dc.stencilfunc_bf > static_cast<uint8_t>(vk::CompareOp::eAlways)) ||
|
||||
(front_write_mask != 0 &&
|
||||
UsesStencilOpValue(sc.stencil_fail, sc.stencil_zpass, sc.stencil_zfail) &&
|
||||
sm.stencil_opval != sm.stencil_testval) ||
|
||||
(dc.backface_enable && back_write_mask != 0 &&
|
||||
UsesStencilOpValue(sc.stencil_fail_bf, sc.stencil_zpass_bf, sc.stencil_zfail_bf) &&
|
||||
sm.stencil_opval_bf != sm.stencil_testval_bf)) {
|
||||
DepthFatal("unsupported stencil compare or replacement state");
|
||||
dc.stencilfunc_bf > static_cast<uint8_t>(vk::CompareOp::eAlways))) {
|
||||
DepthFatal("unsupported stencil compare state");
|
||||
}
|
||||
r.stencil_static_front = {
|
||||
ConvertStencilOp(sc.stencil_fail, front_write_mask, sm.stencil_opval),
|
||||
ConvertStencilOp(sc.stencil_zpass, front_write_mask, sm.stencil_opval),
|
||||
ConvertStencilOp(sc.stencil_zfail, front_write_mask, sm.stencil_opval),
|
||||
static_cast<vk::CompareOp>(dc.stencilfunc)};
|
||||
r.stencil_dynamic_front = {sm.stencil_mask, front_write_mask, sm.stencil_testval};
|
||||
r.stencil_front = ConvertStencilState(
|
||||
dc.stencilfunc, {sc.stencil_fail, sc.stencil_zpass, sc.stencil_zfail},
|
||||
sm.stencil_opval, {.compareMask = sm.stencil_mask, .writeMask = front_write_mask,
|
||||
.reference = sm.stencil_testval});
|
||||
if (dc.backface_enable) {
|
||||
r.stencil_static_back = {
|
||||
ConvertStencilOp(sc.stencil_fail_bf, back_write_mask, sm.stencil_opval_bf),
|
||||
ConvertStencilOp(sc.stencil_zpass_bf, back_write_mask, sm.stencil_opval_bf),
|
||||
ConvertStencilOp(sc.stencil_zfail_bf, back_write_mask, sm.stencil_opval_bf),
|
||||
static_cast<vk::CompareOp>(dc.stencilfunc_bf)};
|
||||
r.stencil_dynamic_back = {sm.stencil_mask_bf, back_write_mask, sm.stencil_testval_bf};
|
||||
r.stencil_back = ConvertStencilState(
|
||||
dc.stencilfunc_bf, {sc.stencil_fail_bf, sc.stencil_zpass_bf, sc.stencil_zfail_bf},
|
||||
sm.stencil_opval_bf, {.compareMask = sm.stencil_mask_bf, .writeMask = back_write_mask,
|
||||
.reference = sm.stencil_testval_bf});
|
||||
} else {
|
||||
r.stencil_static_back = r.stencil_static_front;
|
||||
r.stencil_dynamic_back = r.stencil_dynamic_front;
|
||||
r.stencil_back = r.stencil_front;
|
||||
}
|
||||
}
|
||||
auto& cache = m_context.GetTextureCache();
|
||||
@@ -406,14 +429,13 @@ vk::ImageAspectFlags RenderDepthInfo::AttachmentWriteAspects() const {
|
||||
return writes;
|
||||
}
|
||||
|
||||
const auto face_writes = [&](const PipelineStencilStaticState& state,
|
||||
const PipelineStencilDynamicState& dynamic) {
|
||||
if (dynamic.writeMask == 0) {
|
||||
const auto face_writes = [&](const vk::StencilOpState& state) {
|
||||
if (state.writeMask == 0) {
|
||||
return false;
|
||||
}
|
||||
bool can_pass = state.compareOp != vk::CompareOp::eNever;
|
||||
bool can_fail = state.compareOp != vk::CompareOp::eAlways;
|
||||
if (dynamic.compareMask == 0) {
|
||||
if (state.compareMask == 0) {
|
||||
switch (state.compareOp) {
|
||||
case vk::CompareOp::eEqual:
|
||||
case vk::CompareOp::eLessOrEqual:
|
||||
@@ -439,8 +461,7 @@ vk::ImageAspectFlags RenderDepthInfo::AttachmentWriteAspects() const {
|
||||
(can_pass && depth_fail && state.depthFailOp != vk::StencilOp::eKeep);
|
||||
};
|
||||
if (stencil_clear_enable ||
|
||||
(stencil_test_enable && (face_writes(stencil_static_front, stencil_dynamic_front) ||
|
||||
face_writes(stencil_static_back, stencil_dynamic_back)))) {
|
||||
(stencil_test_enable && (face_writes(stencil_front) || face_writes(stencil_back)))) {
|
||||
writes |= vk::ImageAspectFlagBits::eStencil;
|
||||
}
|
||||
return writes;
|
||||
|
||||
@@ -34,10 +34,8 @@ struct RenderDepthInfo {
|
||||
bool stencil_clear_enable = false;
|
||||
uint8_t stencil_clear_value = 0;
|
||||
bool stencil_test_enable = false;
|
||||
PipelineStencilStaticState stencil_static_front;
|
||||
PipelineStencilStaticState stencil_static_back;
|
||||
PipelineStencilDynamicState stencil_dynamic_front;
|
||||
PipelineStencilDynamicState stencil_dynamic_back;
|
||||
vk::StencilOpState stencil_front;
|
||||
vk::StencilOpState stencil_back;
|
||||
ImageId image_id;
|
||||
|
||||
[[nodiscard]] vk::ImageAspectFlags AttachmentWriteAspects() const;
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <iterator>
|
||||
|
||||
namespace Libs::Graphics {
|
||||
|
||||
@@ -37,9 +36,8 @@ BlitHelper::BlitHelper(GraphicContext& graphics, CommandScheduler& scheduler)
|
||||
m_graphics.device.createPipelineLayout(&layout_info, nullptr, &m_pipeline_layout),
|
||||
"create BlitHelper pipeline layout");
|
||||
|
||||
m_vertex_shader = CreateShader(GPU_BLIT_FS_TRIANGLE_SPV, std::size(GPU_BLIT_FS_TRIANGLE_SPV));
|
||||
m_fragment_shader =
|
||||
CreateShader(GPU_BLIT_COLOR_TO_MS_DEPTH_SPV, std::size(GPU_BLIT_COLOR_TO_MS_DEPTH_SPV));
|
||||
m_vertex_shader = CompileSPV(GPU_BLIT_FS_TRIANGLE_SPV, m_graphics.device);
|
||||
m_fragment_shader = CompileSPV(GPU_BLIT_COLOR_TO_MS_DEPTH_SPV, m_graphics.device);
|
||||
}
|
||||
|
||||
BlitHelper::~BlitHelper() {
|
||||
@@ -60,17 +58,6 @@ BlitHelper::~BlitHelper() {
|
||||
}
|
||||
}
|
||||
|
||||
vk::ShaderModule BlitHelper::CreateShader(const uint32_t* code, size_t words) const {
|
||||
EXIT_IF(code == nullptr || words == 0);
|
||||
vk::ShaderModuleCreateInfo create {};
|
||||
create.codeSize = words * sizeof(uint32_t);
|
||||
create.pCode = code;
|
||||
vk::ShaderModule module = nullptr;
|
||||
RequireVulkanSuccess(m_graphics.device.createShaderModule(&create, nullptr, &module),
|
||||
"create BlitHelper shader module");
|
||||
return module;
|
||||
}
|
||||
|
||||
vk::Pipeline BlitHelper::GetPipeline(PipelineKey key) {
|
||||
const auto cached = std::ranges::find(m_pipelines, key, &Pipeline::key);
|
||||
if (cached != m_pipelines.end()) {
|
||||
|
||||
@@ -37,8 +37,7 @@ private:
|
||||
vk::Pipeline handle = nullptr;
|
||||
};
|
||||
|
||||
[[nodiscard]] vk::ShaderModule CreateShader(const uint32_t* code, size_t words) const;
|
||||
[[nodiscard]] vk::Pipeline GetPipeline(PipelineKey key);
|
||||
[[nodiscard]] vk::Pipeline GetPipeline(PipelineKey key);
|
||||
|
||||
GraphicContext& m_graphics;
|
||||
CommandScheduler& m_scheduler;
|
||||
|
||||
@@ -159,6 +159,7 @@ struct ImageViewInfo {
|
||||
vk::ImageAspectFlags aspect = vk::ImageAspectFlagBits::eColor;
|
||||
uint32_t base_level = 0;
|
||||
uint32_t level_count = 1;
|
||||
uint32_t min_lod = 0; // U4.8 clamp relative to base_level.
|
||||
uint32_t base_layer = 0;
|
||||
uint32_t layer_count = 1;
|
||||
vk::ComponentMapping mapping = {};
|
||||
@@ -167,9 +168,10 @@ struct ImageViewInfo {
|
||||
[[nodiscard]] bool operator==(const ImageViewInfo& rhs) const noexcept {
|
||||
return format == rhs.format && type == rhs.type && aspect == rhs.aspect &&
|
||||
base_level == rhs.base_level && level_count == rhs.level_count &&
|
||||
base_layer == rhs.base_layer && layer_count == rhs.layer_count &&
|
||||
mapping.r == rhs.mapping.r && mapping.g == rhs.mapping.g &&
|
||||
mapping.b == rhs.mapping.b && mapping.a == rhs.mapping.a && usage == rhs.usage;
|
||||
min_lod == rhs.min_lod && base_layer == rhs.base_layer &&
|
||||
layer_count == rhs.layer_count && mapping.r == rhs.mapping.r &&
|
||||
mapping.g == rhs.mapping.g && mapping.b == rhs.mapping.b &&
|
||||
mapping.a == rhs.mapping.a && usage == rhs.usage;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -362,6 +362,12 @@ vk::ImageView Image::FindView(const ImageViewInfo& view_info) {
|
||||
if (!is_storage) {
|
||||
usage.usage &= ~vk::ImageUsageFlagBits::eStorage;
|
||||
}
|
||||
vk::ImageViewMinLodCreateInfoEXT min_lod {};
|
||||
if (normalized.min_lod != 0) {
|
||||
min_lod.minLod = static_cast<float>(normalized.base_level) +
|
||||
static_cast<float>(normalized.min_lod) / 256.0f;
|
||||
usage.pNext = &min_lod;
|
||||
}
|
||||
vk::ImageViewCreateInfo create {};
|
||||
create.pNext = &usage;
|
||||
create.image = image.image;
|
||||
|
||||
@@ -260,12 +260,8 @@ vk::Pipeline TileManager::GetPipeline(uint32_t slot) {
|
||||
const uint32_t values[] {1u << element_index, direction_index};
|
||||
const vk::SpecializationMapEntry entries[] {{0, 0, 4}, {1, 4, 4}};
|
||||
const vk::SpecializationInfo specialization {2, entries, sizeof(values), values};
|
||||
vk::ShaderModuleCreateInfo module_info {};
|
||||
module_info.codeSize = shaders[family_index].words * sizeof(uint32_t);
|
||||
module_info.pCode = shaders[family_index].code;
|
||||
vk::ShaderModule module = nullptr;
|
||||
RequireVulkanSuccess(m_graphics.device.createShaderModule(&module_info, nullptr, &module),
|
||||
"create TileManager shader module");
|
||||
const auto module =
|
||||
CompileSPV({shaders[family_index].code, shaders[family_index].words}, m_graphics.device);
|
||||
vk::PipelineShaderStageCreateInfo stage {};
|
||||
stage.stage = vk::ShaderStageFlagBits::eCompute;
|
||||
stage.module = module;
|
||||
@@ -472,12 +468,7 @@ void TileManager::ConvertD16(Result source, Result target, D16Direction directio
|
||||
code = GPU_TILER_DEMOTE_D16_SPV;
|
||||
words = std::size(GPU_TILER_DEMOTE_D16_SPV);
|
||||
}
|
||||
vk::ShaderModuleCreateInfo module_info {};
|
||||
module_info.codeSize = words * sizeof(uint32_t);
|
||||
module_info.pCode = code;
|
||||
vk::ShaderModule module = nullptr;
|
||||
RequireVulkanSuccess(m_graphics.device.createShaderModule(&module_info, nullptr, &module),
|
||||
"create D16 conversion shader module");
|
||||
const auto module = CompileSPV({code, words}, m_graphics.device);
|
||||
vk::PipelineShaderStageCreateInfo stage {};
|
||||
stage.stage = vk::ShaderStageFlagBits::eCompute;
|
||||
stage.module = module;
|
||||
@@ -611,12 +602,7 @@ void TileManager::ConvertD16(Result source, Result target, D16Direction directio
|
||||
|
||||
void TileManager::SwapBgra16(Result input, Result output, uint32_t pixels) {
|
||||
if (m_swap_bgra16 == nullptr) {
|
||||
vk::ShaderModuleCreateInfo module_info {};
|
||||
module_info.codeSize = std::size(GPU_TILER_SWAP_BGRA16_SPV) * sizeof(uint32_t);
|
||||
module_info.pCode = GPU_TILER_SWAP_BGRA16_SPV;
|
||||
vk::ShaderModule module = nullptr;
|
||||
RequireVulkanSuccess(m_graphics.device.createShaderModule(&module_info, nullptr, &module),
|
||||
"create BGRA16 swap shader module");
|
||||
const auto module = CompileSPV(GPU_TILER_SWAP_BGRA16_SPV, m_graphics.device);
|
||||
vk::PipelineShaderStageCreateInfo stage {};
|
||||
stage.stage = vk::ShaderStageFlagBits::eCompute;
|
||||
stage.module = module;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "graphics/guest_gpu/tile.h"
|
||||
#include "graphics/host_gpu/graphicContext.h"
|
||||
#include "graphics/host_gpu/hostMemory.h"
|
||||
#include "graphics/host_gpu/renderer/colorRenderTarget.h"
|
||||
#include "graphics/host_gpu/renderer/debug.h"
|
||||
#include "graphics/host_gpu/renderer/image/imageView.h"
|
||||
#include "graphics/host_gpu/renderer/image/textureCommon.h"
|
||||
@@ -147,7 +148,7 @@ NativeStorageBuffer(RenderContext& context, const PreparedBindings::BufferSource
|
||||
}
|
||||
buffer_offset = static_cast<uint32_t>(adjustment);
|
||||
const vk::DescriptorBufferInfo result {buffer->Handle(), aligned_offset, size + adjustment};
|
||||
if (resource.formatted && resource.written) {
|
||||
if (resource.written) {
|
||||
context.GetTextureCache().InvalidateMemoryFromGPU(address, size);
|
||||
}
|
||||
const char* access = "Read";
|
||||
@@ -292,13 +293,8 @@ static bool IsSupportedStorageTextureDescriptor(const ShaderRecompiler::IR::Imag
|
||||
const bool supported_swizzle =
|
||||
IsValidImageSwizzle(swizzle) &&
|
||||
(swizzle == DstSel(4, 5, 6, 7) || !resource.read || resource.atomic);
|
||||
const auto max_mip = resource.r128 ? descriptor.LastLevel() : descriptor.MaxMip();
|
||||
const auto view_last_level =
|
||||
resource.mip_mode == ShaderRecompiler::IR::ImageMipMode::DynamicStorage
|
||||
? descriptor.LastLevel()
|
||||
: std::min(descriptor.LastLevel(), max_mip);
|
||||
return (is_1d || is_1d_array || is_2d || is_2d_array || is_3d) && supported_tile &&
|
||||
descriptor.BaseLevel() <= view_last_level && view_last_level <= max_mip &&
|
||||
descriptor.BaseLevel() <= descriptor.LastLevel() &&
|
||||
descriptor.MinLod() == 0 && supported_swizzle && descriptor.BCSwizzle() == 0 &&
|
||||
!descriptor.MsaaDepth();
|
||||
}
|
||||
@@ -456,6 +452,14 @@ static ImageViewInfo TextureViewInfo(const ShaderRecompiler::IR::ImageResource&
|
||||
view.aspect = vk::ImageAspectFlagBits::eColor;
|
||||
view.base_level = descriptor.BaseLevel();
|
||||
view.level_count = view_levels;
|
||||
if (descriptor.MinLod() > descriptor.LastLevel() * 256u) {
|
||||
EXIT("texture minimum LOD exceeds last mip level: min_lod=%u last_level=%u\n",
|
||||
descriptor.MinLod(), descriptor.LastLevel());
|
||||
}
|
||||
const auto base_lod = view.base_level * 256u;
|
||||
if (descriptor.MinLod() > base_lod) {
|
||||
view.min_lod = descriptor.MinLod() - base_lod;
|
||||
}
|
||||
view.usage = storage ? vk::ImageUsageFlagBits::eStorage : vk::ImageUsageFlagBits::eSampled;
|
||||
view.mapping =
|
||||
storage || surface_format.conversion_format != Prospero::BufferFormat::kInvalid
|
||||
@@ -506,6 +510,21 @@ static ImageViewInfo TextureViewInfo(const ShaderRecompiler::IR::ImageResource&
|
||||
return view;
|
||||
}
|
||||
|
||||
static bool TextureViewPreservesMipLayout(const TileSurfaceDescription& description,
|
||||
uint32_t view_levels) {
|
||||
TileSurfaceLayout physical {};
|
||||
TileSurfaceLayout view {};
|
||||
auto view_description = description;
|
||||
view_description.levels = view_levels;
|
||||
return TileGetTiledTextureLayout(description, physical) &&
|
||||
TileGetTiledTextureLayout(view_description, view) &&
|
||||
physical.first_tail_level == view.first_tail_level &&
|
||||
physical.block_slice_size == view.block_slice_size &&
|
||||
physical.total_size == view.total_size &&
|
||||
std::equal(std::begin(physical.mips), std::begin(physical.mips) + description.levels,
|
||||
std::begin(view.mips));
|
||||
}
|
||||
|
||||
TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageResource& resource,
|
||||
const ShaderRecompiler::IR::DescriptorValue& value) {
|
||||
auto descriptor = DecodeNativeDescriptor<ShaderTextureResource>(value);
|
||||
@@ -522,24 +541,22 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
|
||||
return {id, nullptr, std::move(desc)};
|
||||
}
|
||||
|
||||
const auto address = descriptor.Base40();
|
||||
const auto width = static_cast<uint32_t>(descriptor.Width5()) + 1u;
|
||||
const auto height = static_cast<uint32_t>(descriptor.Height5()) + 1u;
|
||||
const auto base_level = descriptor.BaseLevel();
|
||||
const auto last_level = descriptor.LastLevel();
|
||||
const auto type = TextureType(descriptor);
|
||||
const bool multisampled = IsMultisampledTexture(type);
|
||||
const auto max_mip = resource.r128 ? last_level : descriptor.MaxMip();
|
||||
const auto levels = multisampled ? 1u : static_cast<uint32_t>(max_mip) + 1u;
|
||||
const bool dynamic_storage =
|
||||
storage && resource.mip_mode == ShaderRecompiler::IR::ImageMipMode::DynamicStorage;
|
||||
const auto view_last_level =
|
||||
!multisampled && !dynamic_storage ? std::min(last_level, max_mip) : last_level;
|
||||
const auto address = descriptor.Base40();
|
||||
const auto width = static_cast<uint32_t>(descriptor.Width5()) + 1u;
|
||||
const auto height = static_cast<uint32_t>(descriptor.Height5()) + 1u;
|
||||
const auto base_level = descriptor.BaseLevel();
|
||||
const auto last_level = descriptor.LastLevel();
|
||||
const auto type = TextureType(descriptor);
|
||||
const bool multisampled = IsMultisampledTexture(type);
|
||||
const auto max_mip = resource.r128 ? last_level : descriptor.MaxMip();
|
||||
const auto physical_levels = multisampled ? 1u : static_cast<uint32_t>(max_mip) + 1u;
|
||||
const auto levels =
|
||||
multisampled ? 1u : std::max(physical_levels, static_cast<uint32_t>(last_level) + 1u);
|
||||
const auto tile = descriptor.TileMode();
|
||||
const bool depth_tile = tile == Prospero::TileMode::kDepth;
|
||||
const bool msaa_tile = depth_tile || tile == Prospero::TileMode::kRenderTarget;
|
||||
const bool msaa_array = type == Prospero::ImageType::kColor2DMsaaArray;
|
||||
if ((!multisampled && (base_level > view_last_level || view_last_level >= levels)) ||
|
||||
if ((!multisampled && base_level > last_level) ||
|
||||
(multisampled &&
|
||||
(base_level != 0 || last_level == 0 || last_level > 3 || max_mip != last_level ||
|
||||
!msaa_tile || (descriptor.MsaaDepth() && !depth_tile) ||
|
||||
@@ -558,7 +575,7 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
|
||||
}
|
||||
const auto samples = multisampled ? 1u << last_level : 1u;
|
||||
const auto view_levels =
|
||||
multisampled ? 1u : static_cast<uint32_t>(view_last_level - base_level) + 1u;
|
||||
multisampled ? 1u : static_cast<uint32_t>(last_level - base_level) + 1u;
|
||||
const auto depth = static_cast<uint32_t>(descriptor.Depth()) + 1u;
|
||||
const auto format = descriptor.Format();
|
||||
const auto surface_format = TextureGetSurfaceFormatInfo(format);
|
||||
@@ -577,7 +594,19 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
|
||||
type == Prospero::ImageType::kColor2DArray ||
|
||||
type == Prospero::ImageType::kColor2DMsaaArray;
|
||||
const auto image_layers = layered ? depth : 1u;
|
||||
uint32_t pitch = 0;
|
||||
if (levels > physical_levels) {
|
||||
const TileSurfaceDescription physical {
|
||||
format, tile, volume ? TileSurfaceDimension::Dim3D : TileSurfaceDimension::Dim2D,
|
||||
width, height, volume ? depth : 1u, physical_levels, image_layers};
|
||||
// Texture mip views take precedence over the resource count, but must keep its storage layout.
|
||||
if (!TextureViewPreservesMipLayout(physical, levels)) {
|
||||
EXIT("unsupported texture mip view changes physical layout: base=%u last=%u max=%u "
|
||||
"extent=%ux%ux%u tile=%u\n",
|
||||
base_level, last_level, max_mip, width, height, depth,
|
||||
static_cast<uint32_t>(tile));
|
||||
}
|
||||
}
|
||||
uint32_t pitch = 0;
|
||||
TileSizeAlign size {};
|
||||
if (multisampled) {
|
||||
const auto bytes = Prospero::NumBytesPerElement(format);
|
||||
@@ -590,8 +619,8 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
|
||||
size.size *= image_layers;
|
||||
} else {
|
||||
pitch = TileGetTexturePitch(format, width, tile);
|
||||
TileGetTextureTotalSize(format, width, height, volume ? depth : image_layers, levels, tile,
|
||||
volume, size);
|
||||
TileGetTextureTotalSize(format, width, height, volume ? depth : image_layers,
|
||||
physical_levels, tile, volume, size);
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(size.size == 0 || size.align == 0 ||
|
||||
(address & (static_cast<uint64_t>(size.align) - 1u)) != 0);
|
||||
@@ -628,7 +657,7 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
|
||||
!desc.info.IsDepth()) {
|
||||
TileSizeAlign metadata_size {};
|
||||
(void)TileGetDccSize(width, height, volume ? depth : image_layers,
|
||||
desc.info.bytes_per_block, levels, tile, metadata_size,
|
||||
desc.info.bytes_per_block, physical_levels, tile, metadata_size,
|
||||
std::countr_zero(samples));
|
||||
desc.info.metadata.kind = ImageMetadataKind::Dcc;
|
||||
desc.info.metadata.range = {descriptor.MetaAddr() << 8u, metadata_size.size};
|
||||
@@ -850,7 +879,8 @@ void RenderExecutor::RebindImages(PreparedBindings& prepared) {
|
||||
}
|
||||
}
|
||||
|
||||
void RenderExecutor::PrepareGraphicsBindings(std::span<PreparedBindings* const> stages) {
|
||||
void RenderExecutor::PrepareGraphicsBindings(std::span<PreparedBindings* const> stages,
|
||||
std::span<RenderColorInfo> colors) {
|
||||
bool uses_dma = false;
|
||||
for (auto* stage: stages) {
|
||||
FindBuffers(*stage);
|
||||
@@ -859,12 +889,29 @@ void RenderExecutor::PrepareGraphicsBindings(std::span<PreparedBindings* const>
|
||||
if (uses_dma) {
|
||||
m_context.PrepareBda();
|
||||
}
|
||||
for (auto* stage: stages) {
|
||||
RebindBuffers(*stage);
|
||||
}
|
||||
for (auto* stage: stages) {
|
||||
RebindImages(*stage);
|
||||
}
|
||||
auto& cache = m_context.GetTextureCache();
|
||||
for (auto& target: colors) {
|
||||
EXIT_IF(!target.image_id);
|
||||
const auto old_image = cache.m_slot_images.try_get(target.image_id);
|
||||
if (old_image == nullptr || (!old_image->registered && !old_image->info.data.Empty()) ||
|
||||
old_image->binding.needs_rebind) {
|
||||
if (old_image != nullptr) {
|
||||
old_image->binding = {};
|
||||
}
|
||||
target.desc.view_info.base_level = target.guest_mip_level;
|
||||
target.desc.view_info.base_layer = target.guest_array_layer;
|
||||
target.image_id = cache.FindImage(target.desc);
|
||||
BindRenderTarget(target.image_id);
|
||||
}
|
||||
}
|
||||
// Discovery can read back PS5 metadata and submit the scheduler. Reserve draw buffers only
|
||||
// after image identities are final; attachment layout transitions follow buffer alias copies.
|
||||
for (auto* stage: stages) {
|
||||
RebindBuffers(*stage);
|
||||
}
|
||||
}
|
||||
|
||||
void RenderExecutor::CommitBindings(CommandBuffer& buffer,
|
||||
|
||||
@@ -89,12 +89,7 @@ template <typename... Args>
|
||||
void PipelineCacheLog(fmt::format_string<Args...> format, Args&&... args) {
|
||||
auto message = fmt::format(format, std::forward<Args>(args)...);
|
||||
message += '\n';
|
||||
if (Log::GetDirection() != Log::Direction::Console) {
|
||||
std::fwrite(message.data(), 1, message.size(), stdout);
|
||||
std::fflush(stdout);
|
||||
}
|
||||
Log::Write(message);
|
||||
Log::Flush();
|
||||
Log::WriteToConsoleAndLog(message);
|
||||
}
|
||||
|
||||
bool ReadShaderGuestMemory(void*, uint64_t address, uint32_t* value) {
|
||||
@@ -253,12 +248,7 @@ struct PipelineCache::ProgramCache {
|
||||
}
|
||||
DumpShaderSpirv(stage_name, options.shader_hash, result.spirv);
|
||||
|
||||
vk::ShaderModuleCreateInfo create_info {};
|
||||
create_info.codeSize = result.spirv.size() * sizeof(uint32_t);
|
||||
create_info.pCode = result.spirv.data();
|
||||
vk::ShaderModule module = nullptr;
|
||||
RequireVulkanSuccess(device.createShaderModule(&create_info, nullptr, &module),
|
||||
"create recompiled shader module");
|
||||
const auto module = CompileSPV(result.spirv, device);
|
||||
EXIT_IF(module == nullptr);
|
||||
if (options.dump_ir) {
|
||||
LOGF("%s SPIR-V words=%" PRIu64 " wave_size=%u\n", options.dump_label,
|
||||
@@ -351,7 +341,7 @@ struct PipelineCache::ProgramCache {
|
||||
options.user_data_base = 0;
|
||||
options.wave_size = stage == ShaderType::Mesh ? input_info.mesh.wave_size : 64u;
|
||||
}
|
||||
} else if constexpr (std::is_same_v<InputInfo, ShaderComputeInputInfo>) {
|
||||
} else {
|
||||
options.wave_size = input_info.wave_size;
|
||||
}
|
||||
auto translated = ShaderRecompiler::TranslateProgram(params.code, options);
|
||||
@@ -676,13 +666,16 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(
|
||||
key.ps_shader_id = ps_id;
|
||||
auto& static_params = key.static_params;
|
||||
auto& rendering = key.rendering;
|
||||
rendering.color_count = color_count;
|
||||
rendering.color_count = 0;
|
||||
uint32_t attachment_samples = 0;
|
||||
for (uint32_t i = 0; i < color_count; i++) {
|
||||
const auto slot = colors[i].target_slot;
|
||||
EXIT_IF(slot >= RENDER_COLOR_ATTACHMENTS_MAX);
|
||||
rendering.color_count = std::max(rendering.color_count, slot + 1);
|
||||
EXIT_IF(!colors[i].image_id || colors[i].desc.view_info.format == vk::Format::eUndefined);
|
||||
static_params.color_mask[i] = colors[i].export_mapping.ApplyMask(
|
||||
static_params.color_mask[slot] = colors[i].export_mapping.ApplyMask(
|
||||
render_target_mask_slot(ctx.GetRenderTargetMask(), colors[i].target_slot));
|
||||
rendering.color_formats[i] = colors[i].desc.view_info.format;
|
||||
rendering.color_formats[slot] = colors[i].desc.view_info.format;
|
||||
if (attachment_samples == 0) {
|
||||
attachment_samples = colors[i].desc.info.samples;
|
||||
} else if (attachment_samples != colors[i].desc.info.samples) {
|
||||
@@ -691,14 +684,14 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(
|
||||
}
|
||||
const auto& rt = ctx.GetRenderTarget(colors[i].target_slot);
|
||||
const auto& bc = ctx.GetBlendControl(colors[i].target_slot);
|
||||
static_params.color_srcblend[i] = bc.color_srcblend;
|
||||
static_params.color_comb_fcn[i] = bc.color_comb_fcn;
|
||||
static_params.color_destblend[i] = bc.color_destblend;
|
||||
static_params.alpha_srcblend[i] = bc.alpha_srcblend;
|
||||
static_params.alpha_comb_fcn[i] = bc.alpha_comb_fcn;
|
||||
static_params.alpha_destblend[i] = bc.alpha_destblend;
|
||||
static_params.separate_alpha_blend[i] = bc.separate_alpha_blend;
|
||||
static_params.blend_enable[i] = bc.enable && !rt.info.blend_bypass;
|
||||
static_params.color_srcblend[slot] = bc.color_srcblend;
|
||||
static_params.color_comb_fcn[slot] = bc.color_comb_fcn;
|
||||
static_params.color_destblend[slot] = bc.color_destblend;
|
||||
static_params.alpha_srcblend[slot] = bc.alpha_srcblend;
|
||||
static_params.alpha_comb_fcn[slot] = bc.alpha_comb_fcn;
|
||||
static_params.alpha_destblend[slot] = bc.alpha_destblend;
|
||||
static_params.separate_alpha_blend[slot] = bc.separate_alpha_blend;
|
||||
static_params.blend_enable[slot] = bc.enable && !rt.info.blend_bypass;
|
||||
}
|
||||
const bool with_depth =
|
||||
depth.desc.view_info.format != vk::Format::eUndefined && static_cast<bool>(depth.image_id);
|
||||
@@ -747,9 +740,6 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(
|
||||
static_params.depth_bounds_test_enable = depth.depth_bounds_test_enable;
|
||||
static_params.depth_min_bounds = depth.depth_min_bounds;
|
||||
static_params.depth_max_bounds = depth.depth_max_bounds;
|
||||
static_params.stencil_test_enable = depth.stencil_test_enable;
|
||||
static_params.stencil_front = depth.stencil_static_front;
|
||||
static_params.stencil_back = depth.stencil_static_back;
|
||||
const bool rect_list =
|
||||
command.GetUserConfig().GetPrimType() == Prospero::PrimitiveType::kRectList;
|
||||
static_params.cull_back = !rect_list && mc.cull_back;
|
||||
|
||||
@@ -42,9 +42,6 @@ struct PipelineStaticParameters {
|
||||
bool depth_bounds_test_enable = false;
|
||||
float depth_min_bounds = 0.0f;
|
||||
float depth_max_bounds = 0.0f;
|
||||
bool stencil_test_enable = false;
|
||||
PipelineStencilStaticState stencil_front;
|
||||
PipelineStencilStaticState stencil_back;
|
||||
uint32_t color_mask[RENDER_COLOR_ATTACHMENTS_MAX] = {};
|
||||
bool cull_front = false;
|
||||
bool cull_back = false;
|
||||
@@ -68,7 +65,7 @@ struct PipelineStaticParameters {
|
||||
static_assert(std::is_trivially_copyable_v<PipelineStaticParameters>);
|
||||
static_assert(std::is_standard_layout_v<PipelineStaticParameters>);
|
||||
static_assert(alignof(PipelineStaticParameters) == 1);
|
||||
static_assert(sizeof(PipelineStaticParameters) == 158);
|
||||
static_assert(sizeof(PipelineStaticParameters) == 125);
|
||||
|
||||
struct PipelineRenderingState {
|
||||
std::array<vk::Format, RENDER_COLOR_ATTACHMENTS_MAX> color_formats {};
|
||||
|
||||
@@ -231,30 +231,20 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
vk::ShaderModule tess_control_shader_module = nullptr;
|
||||
vk::ShaderModule tess_eval_shader_module = nullptr;
|
||||
|
||||
vk::ShaderModuleCreateInfo create_info {};
|
||||
vk::Result result {};
|
||||
if (rect_list) {
|
||||
const auto shaders =
|
||||
BuildRectListShaders(vs_input_info, ps_active ? ps_input_info : nullptr);
|
||||
create_info.codeSize = shaders.control.size() * 4;
|
||||
create_info.pCode = shaders.control.data();
|
||||
result =
|
||||
graphics.device.createShaderModule(&create_info, nullptr, &tess_control_shader_module);
|
||||
tess_control_shader_module = CompileSPV(shaders.control, graphics.device);
|
||||
if (graphics_debug_dump_enabled()) {
|
||||
LOGF("PipelineTrace: vkCreateShaderModule RectList TCS done result=%s module=%p\n",
|
||||
vk::to_string(result).c_str(), static_cast<void*>(tess_control_shader_module));
|
||||
LOGF("PipelineTrace: vkCreateShaderModule RectList TCS done module=%p\n",
|
||||
static_cast<void*>(tess_control_shader_module));
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(result != vk::Result::eSuccess);
|
||||
|
||||
create_info.codeSize = shaders.evaluation.size() * 4;
|
||||
create_info.pCode = shaders.evaluation.data();
|
||||
result =
|
||||
graphics.device.createShaderModule(&create_info, nullptr, &tess_eval_shader_module);
|
||||
tess_eval_shader_module = CompileSPV(shaders.evaluation, graphics.device);
|
||||
if (graphics_debug_dump_enabled()) {
|
||||
LOGF("PipelineTrace: vkCreateShaderModule RectList TES done result=%s module=%p\n",
|
||||
vk::to_string(result).c_str(), static_cast<void*>(tess_eval_shader_module));
|
||||
LOGF("PipelineTrace: vkCreateShaderModule RectList TES done module=%p\n",
|
||||
static_cast<void*>(tess_eval_shader_module));
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(result != vk::Result::eSuccess);
|
||||
}
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(
|
||||
@@ -470,8 +460,8 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
" set_layouts=1 push_constants=%" PRIu32 "\n",
|
||||
vertex_program.id, ps_active ? pixel_program.id : 0, 1u);
|
||||
}
|
||||
result = graphics.device.createPipelineLayout(&pipeline_layout_info, nullptr,
|
||||
&pipeline.pipeline_layout);
|
||||
auto result = graphics.device.createPipelineLayout(&pipeline_layout_info, nullptr,
|
||||
&pipeline.pipeline_layout);
|
||||
if (graphics_debug_dump_enabled()) {
|
||||
LOGF("PipelineTrace: vkCreatePipelineLayout done result=%s layout=%p\n",
|
||||
vk::to_string(result).c_str(), static_cast<void*>(pipeline.pipeline_layout));
|
||||
@@ -487,15 +477,6 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
#else
|
||||
(static_params.depth_bounds_test_enable ? VK_TRUE : VK_FALSE);
|
||||
#endif
|
||||
depth_stencil_info.stencilTestEnable = (static_params.stencil_test_enable ? VK_TRUE : VK_FALSE);
|
||||
depth_stencil_info.front.failOp = static_params.stencil_front.failOp;
|
||||
depth_stencil_info.front.passOp = static_params.stencil_front.passOp;
|
||||
depth_stencil_info.front.depthFailOp = static_params.stencil_front.depthFailOp;
|
||||
depth_stencil_info.front.compareOp = static_params.stencil_front.compareOp;
|
||||
depth_stencil_info.back.failOp = static_params.stencil_back.failOp;
|
||||
depth_stencil_info.back.passOp = static_params.stencil_back.passOp;
|
||||
depth_stencil_info.back.depthFailOp = static_params.stencil_back.depthFailOp;
|
||||
depth_stencil_info.back.compareOp = static_params.stencil_back.compareOp;
|
||||
depth_stencil_info.minDepthBounds = static_params.depth_min_bounds;
|
||||
depth_stencil_info.maxDepthBounds = static_params.depth_max_bounds;
|
||||
|
||||
@@ -508,6 +489,8 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
vk::DynamicState::eDepthCompareOp,
|
||||
vk::DynamicState::eDepthBiasEnable,
|
||||
vk::DynamicState::eDepthBias,
|
||||
vk::DynamicState::eStencilTestEnable,
|
||||
vk::DynamicState::eStencilOp,
|
||||
vk::DynamicState::eStencilCompareMask,
|
||||
vk::DynamicState::eStencilReference,
|
||||
vk::DynamicState::eStencilWriteMask,
|
||||
|
||||
@@ -176,7 +176,8 @@ private:
|
||||
|
||||
[[nodiscard]] TextureBinding ResolveTexture(const ShaderRecompiler::IR::ImageResource& resource,
|
||||
const ShaderRecompiler::IR::DescriptorValue& value);
|
||||
void PrepareGraphicsBindings(std::span<PreparedBindings* const> stages);
|
||||
void PrepareGraphicsBindings(std::span<PreparedBindings* const> stages,
|
||||
std::span<RenderColorInfo> colors);
|
||||
void ResolveRenderColorTarget(CommandBuffer& buffer, RenderColorInfo& target,
|
||||
uint32_t render_target_slice_offset, uint32_t render_target_slot,
|
||||
bool ignore_target_mask = false, bool exact_format = false);
|
||||
|
||||
@@ -172,7 +172,7 @@ bool RenderExecutor::TryConsumeComputeImageClear(const ShaderComputeInputInfo& i
|
||||
1, view.base_layer, view.layer_count};
|
||||
vk::ClearValue clear {};
|
||||
clear.depthStencil = vk::ClearDepthStencilValue {0.0f, fill.value};
|
||||
cache.ClearImage(command, binding.image_id, range, clear);
|
||||
cache.ClearImage(command, binding.image_id, image.backing.format, range, clear);
|
||||
return true;
|
||||
}
|
||||
ShaderBufferResource descriptor;
|
||||
@@ -183,14 +183,6 @@ bool RenderExecutor::TryConsumeComputeImageClear(const ShaderComputeInputInfo& i
|
||||
return false;
|
||||
}
|
||||
if (!cache.ClearImageFromBuffer(command, descriptor.Base48(), size, packed_clear)) {
|
||||
// Track deferred DCC state while the original dispatch writes the metadata allocation.
|
||||
cache.TrackDccFill(descriptor.Base48(), size, packed_clear);
|
||||
static std::atomic<uint32_t> logged_metadata_clears {0};
|
||||
if (logged_metadata_clears.fetch_add(1, std::memory_order_relaxed) < 32) {
|
||||
LOGF("GraphicsRenderDispatchDirect: metadata fill shader=0x%016" PRIx64
|
||||
" addr=0x%016" PRIx64 " size=0x%016" PRIx64 " value=0x%08" PRIx32 "\n",
|
||||
input.stage.program->shader_hash, descriptor.Base48(), size, packed_clear);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static std::atomic<uint32_t> logged_clears {0};
|
||||
@@ -214,6 +206,17 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
thread_group_x, thread_group_y, thread_group_z, mode,
|
||||
sh_ctx.GetCs().cs_regs.data_addr);
|
||||
|
||||
if (thread_group_x == 0 || thread_group_y == 0 || thread_group_z == 0) {
|
||||
static std::atomic<uint32_t> log_count {0};
|
||||
if (log_count.fetch_add(1, std::memory_order_relaxed) < 32) {
|
||||
LOGF("GraphicsRenderDispatchDirect: skipping zero-sized dispatch groups=%ux%ux%u "
|
||||
"mode=0x%08" PRIx32 " shader=0x%016" PRIx64 "\n",
|
||||
thread_group_x, thread_group_y, thread_group_z, mode,
|
||||
sh_ctx.GetCs().cs_regs.data_addr);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Common::LockGuard lock(m_context.GetMutex());
|
||||
if (sh_ctx.GetCs().cs_regs.data_addr == 0) {
|
||||
LOGF("GraphicsRenderDispatchDirect: temporary: ignoring dispatch with null CS shader, "
|
||||
@@ -355,17 +358,6 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
}
|
||||
}
|
||||
|
||||
if (thread_group_x == 0 || thread_group_y == 0 || thread_group_z == 0) {
|
||||
static std::atomic<uint32_t> log_count {0};
|
||||
if (log_count.fetch_add(1, std::memory_order_relaxed) < 32) {
|
||||
LOGF("GraphicsRenderDispatchDirect: skipping zero-sized dispatch groups=%ux%ux%u "
|
||||
"mode=0x%08" PRIx32 " shader=0x%016" PRIx64 "\n",
|
||||
thread_group_x, thread_group_y, thread_group_z, mode,
|
||||
sh_ctx.GetCs().cs_regs.data_addr);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
buffer.EndRendering();
|
||||
auto& pipeline =
|
||||
m_context.GetPipelineCache().GetComputePipeline(input_info, compute_program);
|
||||
@@ -374,8 +366,8 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
if (program.info.uses_dma) {
|
||||
m_context.PrepareBda();
|
||||
}
|
||||
RebindBuffers(bindings);
|
||||
RebindImages(bindings);
|
||||
RebindBuffers(bindings);
|
||||
|
||||
auto vk_buffer = buffer.Handle();
|
||||
PreparedBindings* descriptor_stage = &bindings;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "graphics/host_gpu/renderer/colorRenderTarget.h"
|
||||
#include "graphics/host_gpu/renderer/debug.h"
|
||||
#include "graphics/host_gpu/renderer/depthRenderTarget.h"
|
||||
#include "graphics/host_gpu/renderer/image/textureCommon.h"
|
||||
#include "graphics/host_gpu/renderer/pipeline/pipelineCache.h"
|
||||
#include "graphics/host_gpu/renderer/pipeline/shaderResourceBarrier.h"
|
||||
#include "graphics/host_gpu/renderer/render.h"
|
||||
@@ -308,24 +309,12 @@ static void LogDrawInputState(const CommandBuffer& buffer, const RenderColorInfo
|
||||
|
||||
static void SetGraphicsDynamicParams(const CommandBuffer& buffer, vk::CommandBuffer vk_buffer,
|
||||
const ShaderVertexInputInfo& vs_input_info,
|
||||
const RenderColorInfo* colors, uint32_t color_count,
|
||||
const RenderDepthInfo& depth) {
|
||||
const RenderDepthInfo& depth, const RenderState& rendering) {
|
||||
KYTY_PROFILER_FUNCTION();
|
||||
|
||||
EXIT_IF(colors == nullptr);
|
||||
const auto& ctx = buffer.GetRegisters();
|
||||
|
||||
const auto& vp = ctx.GetScreenViewport();
|
||||
vk::Extent2D framebuffer_extent {};
|
||||
if (color_count > 0 && colors[0].image_id) {
|
||||
framebuffer_extent = colors[0].Extent();
|
||||
} else if (depth.image_id) {
|
||||
framebuffer_extent = {depth.desc.info.extent.width, depth.desc.info.extent.height};
|
||||
} else {
|
||||
const auto& limits = buffer.GetGraphics().GetPhysicalDeviceProperties().limits;
|
||||
framebuffer_extent = {limits.maxFramebufferWidth, limits.maxFramebufferHeight};
|
||||
}
|
||||
|
||||
const auto& vp = ctx.GetScreenViewport();
|
||||
const vk::Extent2D framebuffer_extent {rendering.width, rendering.height};
|
||||
const auto& outputs = vs_input_info.stage.program->info.outputs;
|
||||
const bool indexed_viewports =
|
||||
std::any_of(outputs.begin(), outputs.end(), [](const auto& output) {
|
||||
@@ -403,19 +392,16 @@ static void SetGraphicsDynamicParams(const CommandBuffer& buffer, vk::CommandBuf
|
||||
vk_buffer.setDepthBias(constant_factor, poly_offset.clamp, slope_factor);
|
||||
}
|
||||
|
||||
vk_buffer.setStencilTestEnable(depth.stencil_test_enable ? VK_TRUE : VK_FALSE);
|
||||
if (depth.stencil_test_enable) {
|
||||
vk_buffer.setStencilCompareMask(vk::StencilFaceFlagBits::eFront,
|
||||
depth.stencil_dynamic_front.compareMask);
|
||||
vk_buffer.setStencilCompareMask(vk::StencilFaceFlagBits::eBack,
|
||||
depth.stencil_dynamic_back.compareMask);
|
||||
vk_buffer.setStencilWriteMask(vk::StencilFaceFlagBits::eFront,
|
||||
depth.stencil_dynamic_front.writeMask);
|
||||
vk_buffer.setStencilWriteMask(vk::StencilFaceFlagBits::eBack,
|
||||
depth.stencil_dynamic_back.writeMask);
|
||||
vk_buffer.setStencilReference(vk::StencilFaceFlagBits::eFront,
|
||||
depth.stencil_dynamic_front.reference);
|
||||
vk_buffer.setStencilReference(vk::StencilFaceFlagBits::eBack,
|
||||
depth.stencil_dynamic_back.reference);
|
||||
const auto set_stencil = [&](vk::StencilFaceFlagBits face, const vk::StencilOpState& state) {
|
||||
vk_buffer.setStencilOp(face, state.failOp, state.passOp, state.depthFailOp, state.compareOp);
|
||||
vk_buffer.setStencilCompareMask(face, state.compareMask);
|
||||
vk_buffer.setStencilWriteMask(face, state.writeMask);
|
||||
vk_buffer.setStencilReference(face, state.reference);
|
||||
};
|
||||
set_stencil(vk::StencilFaceFlagBits::eFront, depth.stencil_front);
|
||||
set_stencil(vk::StencilFaceFlagBits::eBack, depth.stencil_back);
|
||||
}
|
||||
|
||||
#if defined(__APPLE__)
|
||||
@@ -423,15 +409,11 @@ static void SetGraphicsDynamicParams(const CommandBuffer& buffer, vk::CommandBuf
|
||||
// eColorWriteEnableEXT dynamic state and relies on the static colorWriteMask instead.
|
||||
#else
|
||||
vk::Bool32 enable[RENDER_COLOR_ATTACHMENTS_MAX] = {};
|
||||
// Color-control operation selects special color-buffer paths, not the normal component write
|
||||
// mask. Attachment availability therefore follows the target write mask.
|
||||
for (uint32_t i = 0; i < color_count; i++) {
|
||||
enable[i] = render_target_mask_slot(ctx.GetRenderTargetMask(), colors[i].target_slot) != 0
|
||||
? VK_TRUE
|
||||
: VK_FALSE;
|
||||
for (uint32_t slot = 0; slot < rendering.num_color_attachments; slot++) {
|
||||
enable[slot] = rendering.color_attachments[slot].image_view != nullptr;
|
||||
}
|
||||
if (color_count != 0) {
|
||||
vk_buffer.setColorWriteEnableEXT(color_count, enable);
|
||||
if (rendering.num_color_attachments != 0) {
|
||||
vk_buffer.setColorWriteEnableEXT(rendering.num_color_attachments, enable);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -477,19 +459,15 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
|
||||
state.width = std::numeric_limits<uint32_t>::max();
|
||||
state.height = std::numeric_limits<uint32_t>::max();
|
||||
state.num_layers = std::numeric_limits<uint32_t>::max();
|
||||
state.num_color_attachments = color_count;
|
||||
state.num_color_attachments = 0;
|
||||
uint32_t attachment_samples = 0;
|
||||
for (uint32_t i = 0; i < color_count; i++) {
|
||||
auto& target = colors[i];
|
||||
EXIT_IF(!target.image_id);
|
||||
const auto old_image = cache.m_slot_images.try_get(target.image_id);
|
||||
if (old_image == nullptr || (!old_image->registered && !old_image->info.data.Empty()) ||
|
||||
old_image->binding.needs_rebind) {
|
||||
if (old_image != nullptr) {
|
||||
old_image->binding = {};
|
||||
}
|
||||
target.image_id = cache.FindImage(target.desc);
|
||||
BindRenderTarget(target.image_id);
|
||||
const auto owner = cache.m_slot_images.try_get(target.image_id);
|
||||
if (owner == nullptr || (!owner->registered && !owner->info.data.Empty()) ||
|
||||
owner->binding.needs_rebind) {
|
||||
EXIT("color target changed after render-state discovery\n");
|
||||
}
|
||||
const auto image_view = cache.FindRenderTarget(target.image_id, target.desc);
|
||||
auto& image = cache.GetImage(target.image_id);
|
||||
@@ -523,7 +501,8 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
|
||||
state.width = std::min(state.width, extent.width);
|
||||
state.height = std::min(state.height, extent.height);
|
||||
state.num_layers = std::min(state.num_layers, view.layer_count);
|
||||
auto& attachment = state.color_attachments[i];
|
||||
state.num_color_attachments = std::max(state.num_color_attachments, target.target_slot + 1);
|
||||
auto& attachment = state.color_attachments[target.target_slot];
|
||||
attachment.image_view = image_view;
|
||||
attachment.image_layout = layout;
|
||||
}
|
||||
@@ -709,6 +688,7 @@ struct PreparedVertexBuffers {
|
||||
|
||||
std::array<vk::Buffer, MaxBuffers> buffers {};
|
||||
std::array<vk::DeviceSize, MaxBuffers> offsets {};
|
||||
std::array<vk::DeviceSize, MaxBuffers> sizes {};
|
||||
uint32_t count = 0;
|
||||
};
|
||||
|
||||
@@ -793,6 +773,7 @@ static PreparedVertexBuffers AcquireVertexBuffers(CommandBuffer& b
|
||||
|
||||
prepared.buffers[i] = range->binding.first->Handle();
|
||||
prepared.offsets[i] = range->binding.second + vertex.addr - range->base_address;
|
||||
prepared.sizes[i] = std::min(size, range->acquired_end - vertex.addr);
|
||||
SetVulkanObjectNameF(
|
||||
buffer.GetContext().GetGraphics().device, prepared.buffers[i],
|
||||
"Kyty.VertexBuffer[slot={} guest=0x{:016x} size=0x{:x} stride={} records={}]", i,
|
||||
@@ -896,42 +877,6 @@ static bool ResolvePrimitiveRestart(const CommandBuffer& buffer,
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RenderExecutor::PrepareDrawRenderState(CommandBuffer& buffer, const DrawCallInfo& draw,
|
||||
uint32_t render_target_slice_offset,
|
||||
DrawRenderState& state) {
|
||||
auto& ctx = buffer.GetRegisters();
|
||||
|
||||
if (ResolveColorTargets(buffer, render_target_slice_offset)) {
|
||||
return false;
|
||||
}
|
||||
if (draw.IsIndexed()) {
|
||||
LogDrawPhase(draw.Name(), "ResolveRenderColorTarget");
|
||||
}
|
||||
for (uint32_t slot = 0; slot < RENDER_COLOR_ATTACHMENTS_MAX; slot++) {
|
||||
if (slot == 0 || (render_target_mask_slot(ctx.GetRenderTargetMask(), slot) != 0 &&
|
||||
ctx.GetRenderTarget(slot).base.addr != 0)) {
|
||||
ResolveRenderColorTarget(buffer, state.color_info[state.color_count],
|
||||
render_target_slice_offset, slot);
|
||||
if (state.color_info[state.color_count].image_id) {
|
||||
state.color_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (draw.IsIndexed()) {
|
||||
LogDrawPhase(draw.Name(), "ResolveRenderDepthTarget");
|
||||
}
|
||||
ResolveRenderDepthTarget(buffer, state.depth_info);
|
||||
|
||||
state.ps_active = DrawHasActivePixelShader(buffer);
|
||||
if (state.color_count == 0 && !state.depth_info.image_id && !state.ps_active) {
|
||||
LogFramebufferSkip(draw.Name(), state.color_info[0], state.depth_info, buffer,
|
||||
draw.index_count, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void RefreshShaders(CommandBuffer& buffer, const DrawCallInfo& draw,
|
||||
DrawRenderState& state) {
|
||||
auto& ctx = buffer.GetRegisters();
|
||||
@@ -945,8 +890,14 @@ static void RefreshShaders(CommandBuffer& buffer, const DrawCallInfo& draw,
|
||||
state.ps_input_info = {};
|
||||
std::array<Prospero::ColorComponentMapping, RENDER_COLOR_ATTACHMENTS_MAX>
|
||||
target_export_mapping {};
|
||||
for (uint32_t i = 0; i < state.color_count; i++) {
|
||||
target_export_mapping[state.color_info[i].target_slot] = state.color_info[i].export_mapping;
|
||||
for (uint32_t slot = 0; slot < RENDER_COLOR_ATTACHMENTS_MAX; slot++) {
|
||||
const auto& rt = ctx.GetRenderTarget(slot);
|
||||
if (rt.base.addr != 0 && render_target_mask_slot(ctx.GetRenderTargetMask(), slot) != 0) {
|
||||
target_export_mapping[slot] =
|
||||
TextureGetRenderTargetFormat(rt.info.format, rt.info.channel_type,
|
||||
rt.info.channel_order)
|
||||
.export_mapping;
|
||||
}
|
||||
}
|
||||
auto& pipeline_cache = buffer.GetContext().GetPipelineCache();
|
||||
if (draw.IsIndexed()) {
|
||||
@@ -957,6 +908,45 @@ static void RefreshShaders(CommandBuffer& buffer, const DrawCallInfo& draw,
|
||||
target_export_mapping, state.ps_active, state.vertex_info, state.ps_input_info);
|
||||
}
|
||||
|
||||
bool RenderExecutor::PrepareDrawRenderState(CommandBuffer& buffer, const DrawCallInfo& draw,
|
||||
uint32_t render_target_slice_offset,
|
||||
DrawRenderState& state) {
|
||||
state.ps_active = DrawHasActivePixelShader(buffer);
|
||||
RefreshShaders(buffer, draw, state);
|
||||
uint32_t mrt_mask = 0;
|
||||
if (state.ps_active) {
|
||||
for (const auto& output: state.ps_input_info.stage.program->info.outputs) {
|
||||
if (output.kind == ShaderRecompiler::IR::StageOutputKind::Mrt) {
|
||||
mrt_mask |= 1u << output.index;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (draw.IsIndexed()) {
|
||||
LogDrawPhase(draw.Name(), "ResolveRenderColorTarget");
|
||||
}
|
||||
for (uint32_t slot = 0; slot < RENDER_COLOR_ATTACHMENTS_MAX; slot++) {
|
||||
if ((mrt_mask & (1u << slot)) != 0) {
|
||||
ResolveRenderColorTarget(buffer, state.color_info[state.color_count],
|
||||
render_target_slice_offset, slot);
|
||||
if (state.color_info[state.color_count].image_id) {
|
||||
state.color_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (draw.IsIndexed()) {
|
||||
LogDrawPhase(draw.Name(), "ResolveRenderDepthTarget");
|
||||
}
|
||||
ResolveRenderDepthTarget(buffer, state.depth_info);
|
||||
|
||||
if (state.color_count == 0 && !state.depth_info.image_id && !state.ps_active) {
|
||||
LogFramebufferSkip(draw.Name(), state.color_info[0], state.depth_info, buffer,
|
||||
draw.index_count, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static PreparedIndexBuffer PrepareIndexBuffer(CommandBuffer& buffer,
|
||||
const DrawIndexBufferSource& source) {
|
||||
PreparedIndexBuffer prepared;
|
||||
@@ -989,8 +979,9 @@ static void CommitVertexBuffers(vk::CommandBuffer vk_buffer,
|
||||
EXIT_IF(prepared.buffers[i] == nullptr);
|
||||
}
|
||||
if (prepared.count != 0) {
|
||||
vk_buffer.bindVertexBuffers(0, prepared.count, prepared.buffers.data(),
|
||||
prepared.offsets.data());
|
||||
// Guest descriptor bounds must survive allocation merging in the cache.
|
||||
vk_buffer.bindVertexBuffers2(0, prepared.count, prepared.buffers.data(),
|
||||
prepared.offsets.data(), prepared.sizes.data(), nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1077,7 +1068,13 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
|
||||
uint32_t mesh_groups = 0;
|
||||
if (mesh_active) {
|
||||
const auto& mesh = state.vertex_info[0].mesh;
|
||||
if (primitive_restart_enable || mesh.primitives_per_group == 0) {
|
||||
static std::atomic_bool restart_warned = false;
|
||||
if (primitive_restart_enable && !restart_warned.exchange(true, std::memory_order_relaxed)) {
|
||||
std::printf("Warning: primitive restart is not implemented for mesh shaders; "
|
||||
"continuing draw (primitive=%u indexed=%u)\n",
|
||||
static_cast<uint32_t>(ucfg.GetPrimType()), draw.IsIndexed());
|
||||
}
|
||||
if (mesh.primitives_per_group == 0) {
|
||||
EXIT("unsupported mesh draw: primitive=%u indexed=%u restart=%u\n",
|
||||
static_cast<uint32_t>(ucfg.GetPrimType()), draw.IsIndexed(), primitive_restart_enable);
|
||||
}
|
||||
@@ -1116,7 +1113,7 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
|
||||
descriptor_stages[stage_count++] = &*bindings.pixel;
|
||||
}
|
||||
const auto stages = std::span {descriptor_stages.data(), stage_count};
|
||||
PrepareGraphicsBindings(stages);
|
||||
PrepareGraphicsBindings(stages, std::span {state.color_info, state.color_count});
|
||||
PreparedVertexBuffers vertex_bindings;
|
||||
PreparedIndexBuffer index_binding;
|
||||
if (!mesh_active) {
|
||||
@@ -1164,8 +1161,7 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
|
||||
CommitIndexBuffer(vk_buffer, index_binding);
|
||||
}
|
||||
|
||||
SetGraphicsDynamicParams(buffer, vk_buffer, vertex_stages.back(), state.color_info,
|
||||
state.color_count, state.depth_info);
|
||||
SetGraphicsDynamicParams(buffer, vk_buffer, vertex_stages.back(), state.depth_info, rendering);
|
||||
if (m_context.GetGraphics().attachment_feedback_loop_enabled) {
|
||||
vk_buffer.setAttachmentFeedbackLoopEnableEXT(
|
||||
rendering.depth_stencil_attachment.image_layout ==
|
||||
@@ -1230,7 +1226,8 @@ void RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
|
||||
return;
|
||||
}
|
||||
|
||||
if (ConsumeMetadataColorOperation(buffer) || DepthStencilCopy(buffer)) {
|
||||
if (ConsumeMetadataColorOperation(buffer) || DepthStencilCopy(buffer) ||
|
||||
ResolveColorTargets(buffer, args.render_target_slice_offset)) {
|
||||
ResetBindings();
|
||||
return;
|
||||
}
|
||||
@@ -1304,8 +1301,6 @@ void RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
|
||||
return;
|
||||
}
|
||||
|
||||
RefreshShaders(buffer, draw, state);
|
||||
|
||||
LogDrawStateIfNeeded(buffer, draw, state, args.index_type_and_size,
|
||||
args.index_addr);
|
||||
|
||||
@@ -1342,7 +1337,8 @@ void RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
|
||||
return;
|
||||
}
|
||||
|
||||
if (ConsumeMetadataColorOperation(buffer) || DepthStencilCopy(buffer)) {
|
||||
if (ConsumeMetadataColorOperation(buffer) || DepthStencilCopy(buffer) ||
|
||||
ResolveColorTargets(buffer, args.render_target_slice_offset)) {
|
||||
ResetBindings();
|
||||
return;
|
||||
}
|
||||
@@ -1371,18 +1367,16 @@ void RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
|
||||
const DrawCallInfo draw {CommandBufferDebugOp::DrawIndexAuto,
|
||||
args.vertex_count, args.instance_count, args.first_instance};
|
||||
|
||||
DrawRenderState state {};
|
||||
if (!PrepareDrawRenderState(buffer, draw, args.render_target_slice_offset, state)) {
|
||||
ResetBindings();
|
||||
return;
|
||||
}
|
||||
|
||||
vk::PrimitiveTopology topology = vk::PrimitiveTopology::ePointList;
|
||||
if (!GetDrawTopology(ucfg, true, topology)) {
|
||||
ResetBindings();
|
||||
return;
|
||||
}
|
||||
RefreshShaders(buffer, draw, state);
|
||||
DrawRenderState state {};
|
||||
if (!PrepareDrawRenderState(buffer, draw, args.render_target_slice_offset, state)) {
|
||||
ResetBindings();
|
||||
return;
|
||||
}
|
||||
|
||||
const bool rect_list = ucfg.GetPrimType() == Prospero::PrimitiveType::kRectList;
|
||||
if (rect_list && state.vertex_info[0].buffers_num == 0 &&
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <type_traits>
|
||||
|
||||
namespace Libs::Graphics {
|
||||
|
||||
@@ -67,33 +66,6 @@ inline constexpr TargetViewInfo ResolveTargetViewInfo(uint32_t base_layer, uint3
|
||||
base_layer, last_layer - base_layer + 1u, last_layer + 1u};
|
||||
}
|
||||
|
||||
#pragma pack(push, 1)
|
||||
|
||||
struct PipelineStencilStaticState {
|
||||
vk::StencilOp failOp = vk::StencilOp::eKeep;
|
||||
vk::StencilOp passOp = vk::StencilOp::eKeep;
|
||||
vk::StencilOp depthFailOp = vk::StencilOp::eKeep;
|
||||
vk::CompareOp compareOp = vk::CompareOp::eNever;
|
||||
};
|
||||
|
||||
struct PipelineStencilDynamicState {
|
||||
uint32_t compareMask = 0;
|
||||
uint32_t writeMask = 0;
|
||||
uint32_t reference = 0;
|
||||
};
|
||||
|
||||
#pragma pack(pop)
|
||||
|
||||
static_assert(std::is_trivially_copyable_v<PipelineStencilStaticState>);
|
||||
static_assert(std::is_standard_layout_v<PipelineStencilStaticState>);
|
||||
static_assert(alignof(PipelineStencilStaticState) == 1);
|
||||
static_assert(sizeof(PipelineStencilStaticState) ==
|
||||
sizeof(vk::StencilOp) * 3 + sizeof(vk::CompareOp));
|
||||
static_assert(std::is_trivially_copyable_v<PipelineStencilDynamicState>);
|
||||
static_assert(std::is_standard_layout_v<PipelineStencilDynamicState>);
|
||||
static_assert(alignof(PipelineStencilDynamicState) == 1);
|
||||
static_assert(sizeof(PipelineStencilDynamicState) == sizeof(uint32_t) * 3);
|
||||
|
||||
} // namespace Libs::Graphics
|
||||
|
||||
#endif // EMULATOR_SRC_GRAPHICS_HOST_GPU_RENDERER_RENDERTARGET_H_
|
||||
@@ -123,4 +123,14 @@ void RequireVulkanSuccess(vk::Result result, const char* operation) {
|
||||
}
|
||||
}
|
||||
|
||||
vk::ShaderModule CompileSPV(std::span<const uint32_t> code, vk::Device device) {
|
||||
vk::ShaderModuleCreateInfo create_info {};
|
||||
create_info.codeSize = code.size_bytes();
|
||||
create_info.pCode = code.data();
|
||||
vk::ShaderModule module = nullptr;
|
||||
RequireVulkanSuccess(device.createShaderModule(&create_info, nullptr, &module),
|
||||
"create SPIR-V shader module");
|
||||
return module;
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <fmt/format.h>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -27,6 +28,7 @@ using VulkanMemoryBarrier = vk::MemoryBarrier;
|
||||
|
||||
vk::Format VulkanFormat(Prospero::BufferFormat guest_format);
|
||||
void RequireVulkanSuccess(vk::Result result, const char* operation);
|
||||
vk::ShaderModule CompileSPV(std::span<const uint32_t> code, vk::Device device);
|
||||
|
||||
template <typename Handle, typename... Args>
|
||||
void SetVulkanObjectNameF(vk::Device device, Handle handle, fmt::format_string<Args...> format,
|
||||
|
||||
@@ -220,6 +220,8 @@ uint32_t DefaultKeyboardButton(int key_code) {
|
||||
case SDLK_l: return Controller::PAD_BUTTON_CIRCLE;
|
||||
case SDLK_q: return Controller::PAD_BUTTON_L1;
|
||||
case SDLK_e: return Controller::PAD_BUTTON_R1;
|
||||
case SDLK_z: return Controller::PAD_BUTTON_L2;
|
||||
case SDLK_c: return Controller::PAD_BUTTON_R2;
|
||||
case SDLK_LSHIFT: return Controller::PAD_BUTTON_L3;
|
||||
case SDLK_LCTRL: return Controller::PAD_BUTTON_R3;
|
||||
case SDLK_RETURN:
|
||||
|
||||
@@ -201,6 +201,8 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
|
||||
vk::PhysicalDeviceVulkan13Features features13 {};
|
||||
|
||||
vk::PhysicalDeviceColorWriteEnableFeaturesEXT color_write_ext {};
|
||||
vk::PhysicalDeviceImageViewMinLodFeaturesEXT image_view_min_lod {};
|
||||
color_write_ext.pNext = &image_view_min_lod;
|
||||
|
||||
vk::PhysicalDeviceDepthClipEnableFeaturesEXT depth_clip_enable {};
|
||||
depth_clip_enable.pNext = &color_write_ext;
|
||||
@@ -235,6 +237,10 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
|
||||
skip_device = true;
|
||||
#endif
|
||||
}
|
||||
if (image_view_min_lod.minLod != VK_TRUE) {
|
||||
LOGF("image view minLod is not supported\n");
|
||||
skip_device = true;
|
||||
}
|
||||
|
||||
if (depth_clip_control.depthClipControl != VK_TRUE) {
|
||||
LOGF("depthClipControl is not supported\n");
|
||||
@@ -514,11 +520,14 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
|
||||
depth_clip_enable.depthClipEnable = VK_TRUE;
|
||||
|
||||
vk::PhysicalDeviceDepthClipControlFeaturesEXT depth_clip_control {};
|
||||
vk::PhysicalDeviceImageViewMinLodFeaturesEXT image_view_min_lod {};
|
||||
image_view_min_lod.minLod = VK_TRUE;
|
||||
depth_clip_control.pNext = &image_view_min_lod;
|
||||
// MoltenVK lacks VK_EXT_depth_clip_enable and VK_EXT_color_write_enable, so drop those
|
||||
// feature structs from the chain on macOS (the renderer falls back to default depth
|
||||
// clipping and static color-write masks).
|
||||
#if !defined(__APPLE__)
|
||||
depth_clip_control.pNext = &depth_clip_enable;
|
||||
image_view_min_lod.pNext = &depth_clip_enable;
|
||||
#endif
|
||||
depth_clip_control.depthClipControl = VK_TRUE;
|
||||
|
||||
@@ -984,7 +993,8 @@ void WindowContext::CreateVulkan() {
|
||||
|
||||
std::vector<const char*> device_extensions = {
|
||||
VK_KHR_SWAPCHAIN_EXTENSION_NAME, VK_EXT_DEPTH_CLIP_CONTROL_EXTENSION_NAME,
|
||||
VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME, "VK_KHR_maintenance1"};
|
||||
VK_EXT_IMAGE_VIEW_MIN_LOD_EXTENSION_NAME, VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME,
|
||||
"VK_KHR_maintenance1"};
|
||||
|
||||
#if defined(__APPLE__)
|
||||
// MoltenVK lacks VK_EXT_depth_clip_enable and VK_EXT_color_write_enable; the renderer
|
||||
|
||||
@@ -713,6 +713,20 @@ void WindowContext::ProcessEvent(double time_s) {
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_CONTROLLERSENSORUPDATE: {
|
||||
const auto& sensor = event->csensor;
|
||||
if (sensor.sensor == SDL_SENSOR_ACCEL || sensor.sensor == SDL_SENSOR_GYRO) {
|
||||
Controller::SetSensor(sensor.which,
|
||||
sensor.sensor == SDL_SENSOR_ACCEL ? Controller::Sensor::Accel
|
||||
: Controller::Sensor::Gyro,
|
||||
sensor.data,
|
||||
sensor.timestamp_us != 0
|
||||
? sensor.timestamp_us
|
||||
: static_cast<uint64_t>(sensor.timestamp) * 1000);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case SDL_CONTROLLERDEVICEADDED:
|
||||
case SDL_CONTROLLERDEVICEREMOVED:
|
||||
case SDL_CONTROLLERDEVICEREMAPPED: {
|
||||
|
||||
@@ -87,7 +87,6 @@ struct EmbeddedFetchSgprInfo {
|
||||
EmbeddedFetchValueType type = EmbeddedFetchValueType::Unknown;
|
||||
int attrib_id = 0;
|
||||
uint32_t value = 0;
|
||||
std::vector<uint32_t> prolog_loads;
|
||||
};
|
||||
|
||||
using EmbeddedFetchVectorLanes = std::map<uint64_t, EmbeddedFetchSgprInfo>;
|
||||
@@ -326,7 +325,6 @@ EmbeddedFetchData DetectEmbeddedVertexFetch(const Decoder::Program& decoded
|
||||
auto& dst = sgprs[DecodedSgprReg(inst.dst)];
|
||||
dst.type = EmbeddedFetchValueType::Constant;
|
||||
dst.value = value;
|
||||
dst.prolog_loads.clear();
|
||||
} else {
|
||||
ClearEmbeddedFetchSgprs(sgprs, inst.dst, 1);
|
||||
}
|
||||
@@ -337,7 +335,6 @@ EmbeddedFetchData DetectEmbeddedVertexFetch(const Decoder::Program& decoded
|
||||
auto& dst = sgprs[DecodedSgprReg(inst.dst)];
|
||||
dst.type = EmbeddedFetchValueType::Constant;
|
||||
dst.value = inst.src0.value;
|
||||
dst.prolog_loads.clear();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -354,7 +351,6 @@ EmbeddedFetchData DetectEmbeddedVertexFetch(const Decoder::Program& decoded
|
||||
auto& dst = sgprs[register_id + i];
|
||||
dst.type = EmbeddedFetchValueType::Attrib;
|
||||
dst.attrib_id = index + static_cast<int>(i);
|
||||
dst.prolog_loads = {inst.pc};
|
||||
}
|
||||
} else {
|
||||
ClearEmbeddedFetchSgprs(sgprs, inst.dst, DecodedDstSize(inst));
|
||||
@@ -372,7 +368,6 @@ EmbeddedFetchData DetectEmbeddedVertexFetch(const Decoder::Program& decoded
|
||||
dst.type = EmbeddedFetchValueType::Buffer;
|
||||
dst.attrib_id =
|
||||
BufferTableAttribFromOffset(raw_offset, static_cast<int>(i));
|
||||
dst.prolog_loads = {inst.pc};
|
||||
}
|
||||
} else if (IsDecodedSgpr(inst.src1) &&
|
||||
DecodedSgprReg(inst.src1) < sgprs.size() &&
|
||||
@@ -384,8 +379,6 @@ EmbeddedFetchData DetectEmbeddedVertexFetch(const Decoder::Program& decoded
|
||||
auto& dst = sgprs[register_id + i];
|
||||
dst.type = EmbeddedFetchValueType::Buffer;
|
||||
dst.attrib_id = sgprs[DecodedSgprReg(inst.src1)].attrib_id;
|
||||
dst.prolog_loads = sgprs[DecodedSgprReg(inst.src1)].prolog_loads;
|
||||
dst.prolog_loads.push_back(inst.pc);
|
||||
}
|
||||
} else {
|
||||
ClearEmbeddedFetchSgprs(sgprs, inst.dst, DecodedDstSize(inst));
|
||||
@@ -424,7 +417,6 @@ EmbeddedFetchData DetectEmbeddedVertexFetch(const Decoder::Program& decoded
|
||||
break;
|
||||
default: dst.value = src0 + src1; break;
|
||||
}
|
||||
dst.prolog_loads.clear();
|
||||
} else {
|
||||
ClearEmbeddedFetchSgprs(sgprs, inst.dst, 1);
|
||||
}
|
||||
@@ -447,7 +439,6 @@ EmbeddedFetchData DetectEmbeddedVertexFetch(const Decoder::Program& decoded
|
||||
load.pc = inst.pc;
|
||||
load.attrib_id = buffer.attrib_id;
|
||||
load.components = DecodedDstSize(inst);
|
||||
load.prolog_loads = buffer.prolog_loads;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#include "graphics/shader/recompiler/backend/spirv/spirvEmitterInstructions.h"
|
||||
|
||||
#include "common/logging/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter {
|
||||
namespace {
|
||||
@@ -38,7 +41,9 @@ uint32_t EmitBuiltinU32(EmitterState& state, IR::StageInputKind kind, uint32_t c
|
||||
return EmitAddU32(state, local,
|
||||
EmitBinaryU32(state, spv::OpIMul, group, ConstantU32(state, size)));
|
||||
}
|
||||
const auto variable = InputVariableForKind(state, kind);
|
||||
const bool centroid = kind == IR::StageInputKind::BaryCoordSmoothCentroid;
|
||||
const auto variable = InputVariableForKind(
|
||||
state, centroid ? IR::StageInputKind::BaryCoordSmooth : kind);
|
||||
if (variable == 0) {
|
||||
return ConstantU32(state, 0);
|
||||
}
|
||||
@@ -46,8 +51,9 @@ uint32_t EmitBuiltinU32(EmitterState& state, IR::StageInputKind kind, uint32_t c
|
||||
const auto value = state.builder.AllocateId();
|
||||
const auto bits = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpLoad, TypeBool(state), value, variable);
|
||||
state.builder.AddFunction(spv::OpSelect, TypeU32(state), bits, value, ConstantU32(state, 1),
|
||||
ConstantU32(state, 0));
|
||||
// PS5 initializes v_front_face with float +1.0/-1.0 bits.
|
||||
state.builder.AddFunction(spv::OpSelect, TypeU32(state), bits, value,
|
||||
ConstantU32(state, 0x3f800000u), ConstantU32(state, 0xbf800000u));
|
||||
return bits;
|
||||
}
|
||||
if (kind == IR::StageInputKind::VertexIndex || kind == IR::StageInputKind::InstanceIndex ||
|
||||
@@ -70,15 +76,24 @@ uint32_t EmitBuiltinU32(EmitterState& state, IR::StageInputKind kind, uint32_t c
|
||||
state.builder.AddFunction(spv::OpBitcast, TypeU32(state), bits, value);
|
||||
return bits;
|
||||
}
|
||||
if (kind == IR::StageInputKind::BaryCoordSmooth ||
|
||||
if (centroid || kind == IR::StageInputKind::BaryCoordSmooth ||
|
||||
kind == IR::StageInputKind::BaryCoordNoPerspective) {
|
||||
const auto pointer = state.builder.AllocateId();
|
||||
const auto value = state.builder.AllocateId();
|
||||
const auto bits = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpAccessChain,
|
||||
TypePointer(state, spv::StorageClassInput, TypeF32(state)),
|
||||
pointer, variable, ConstantU32(state, component + 1u));
|
||||
state.builder.AddFunction(spv::OpLoad, TypeF32(state), value, pointer);
|
||||
if (centroid) {
|
||||
const auto coordinates = state.builder.AllocateId();
|
||||
state.builder.RequireCapability(spv::CapabilityInterpolationFunction);
|
||||
state.builder.AddFunction(spv::OpExtInst, TypeF32Vector(state, 3), coordinates,
|
||||
GlslStd450(state), GLSLstd450InterpolateAtCentroid, variable);
|
||||
state.builder.AddFunction(spv::OpCompositeExtract, TypeF32(state), value,
|
||||
coordinates, component + 1u);
|
||||
} else {
|
||||
const auto pointer = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpAccessChain,
|
||||
TypePointer(state, spv::StorageClassInput, TypeF32(state)),
|
||||
pointer, variable, ConstantU32(state, component + 1u));
|
||||
state.builder.AddFunction(spv::OpLoad, TypeF32(state), value, pointer);
|
||||
}
|
||||
state.builder.AddFunction(spv::OpBitcast, TypeU32(state), bits, value);
|
||||
return bits;
|
||||
}
|
||||
@@ -499,6 +514,31 @@ void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
state.builder.AddFunction(spv::OpStore, MeshOutputPointer(state, kind, exp.index),
|
||||
value);
|
||||
} else if (exp.kind == IR::ExportTargetKind::Position) {
|
||||
if (state.invalid_position_clip_distance != UINT32_MAX) {
|
||||
const auto zero = state.builder.Constant(spv::OpConstantNull, TypeF32Vector(state, 4));
|
||||
const auto equal = state.builder.AllocateId();
|
||||
const auto invalid = state.builder.AllocateId();
|
||||
const auto distance = state.builder.AllocateId();
|
||||
const auto distance_pointer = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpFOrdEqual, TypeBoolVector(state, 4), equal,
|
||||
value, zero);
|
||||
state.builder.AddFunction(spv::OpAll, TypeBool(state), invalid, equal);
|
||||
// Zero at valid vertices makes a primitive containing an invalid position
|
||||
// collapse to its remaining edge, before the undefined 0/0 perspective divide.
|
||||
state.builder.AddFunction(spv::OpSelect, TypeF32(state), distance, invalid,
|
||||
ConstantF32Value(state, -1.0f),
|
||||
ConstantF32Value(state, 0.0f));
|
||||
state.builder.AddFunction(
|
||||
spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeF32(state)),
|
||||
distance_pointer, state.clip_distance_variable,
|
||||
ConstantU32(state, state.invalid_position_clip_distance));
|
||||
state.builder.AddFunction(spv::OpStore, distance_pointer, distance);
|
||||
static std::atomic_bool logged = false;
|
||||
if (!logged.exchange(true, std::memory_order_relaxed)) {
|
||||
Log::WriteToConsoleAndLog(
|
||||
"Shader: emitted zero-position clip guard\n");
|
||||
}
|
||||
}
|
||||
const auto pointer = state.builder.AllocateId();
|
||||
state.builder.AddFunction(
|
||||
spv::OpAccessChain,
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
#include "graphics/shader/recompiler/frontend/decode/ImageOps.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <bit>
|
||||
#include <cstdio>
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter {
|
||||
namespace {
|
||||
@@ -646,6 +648,13 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
const auto coord =
|
||||
CoordF32(ctx, mem, *address, layout.coord, dimension_info.coordinate_components);
|
||||
if (op == IR::ValueOpcode::ImageGatherRaw) {
|
||||
if (HasFlag(mem, Decoder::ImageSampleFlagLod)) {
|
||||
static std::atomic_flag warned = ATOMIC_FLAG_INIT;
|
||||
if (!warned.test_and_set(std::memory_order_relaxed)) {
|
||||
std::fputs("Warning: approximating IMAGE_GATHER4_L at mip level 0; explicit LOD is ignored.\n",
|
||||
stderr);
|
||||
}
|
||||
}
|
||||
if (dimension == ImageDimension::Dim1D) {
|
||||
if (dref || !HasFlag(mem, Decoder::ImageSampleFlagLevelZero) ||
|
||||
HasFlag(mem, Decoder::ImageSampleFlagOffset) ||
|
||||
|
||||
@@ -116,6 +116,7 @@ struct EmitterState {
|
||||
uint32_t per_vertex_variable = 0;
|
||||
uint32_t point_size_variable = 0;
|
||||
uint32_t clip_distance_variable = 0;
|
||||
uint32_t invalid_position_clip_distance = UINT32_MAX;
|
||||
uint32_t cull_distance_variable = 0;
|
||||
uint32_t layer_variable = 0;
|
||||
uint32_t viewport_index_variable = 0;
|
||||
|
||||
@@ -388,6 +388,19 @@ void DefineInputs(EmitterState& state) {
|
||||
}
|
||||
}
|
||||
for (auto& input: state.inputs) {
|
||||
if (state.program.stage == ShaderType::Pixel &&
|
||||
input.kind == IR::StageInputKind::Parameter) {
|
||||
const auto location = PixelParameterLocation(state, input.location);
|
||||
const auto alias = std::ranges::find_if(state.inputs, [&](const InputBinding& other) {
|
||||
return other.kind == IR::StageInputKind::Parameter && other.variable_id != 0 &&
|
||||
PixelParameterLocation(state, other.location) == location;
|
||||
});
|
||||
if (alias != state.inputs.end()) {
|
||||
EXIT_IF(alias->per_vertex != input.per_vertex);
|
||||
input.variable_id = alias->variable_id;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
uint32_t type = TypeU32(state);
|
||||
switch (input.kind) {
|
||||
case IR::StageInputKind::VertexIndex:
|
||||
@@ -476,6 +489,15 @@ void DefineOutputs(EmitterState& state) {
|
||||
DefineMeshOutputs(state);
|
||||
return;
|
||||
}
|
||||
if (state.program.stage == ShaderType::Vertex && clip_distance_count + cull_distance_count < 8u &&
|
||||
std::ranges::any_of(state.outputs, [](const OutputBinding& output) {
|
||||
return output.kind == IR::StageOutputKind::Position;
|
||||
})) {
|
||||
// Reserve one plane for the enabled PA_CL_CLIP_CNTL clipping-error cull.
|
||||
state.invalid_position_clip_distance = clip_distance_count++;
|
||||
state.outputs.push_back({{IR::StageOutputKind::ClipDistance,
|
||||
state.invalid_position_clip_distance, 0, "gl_ClipDistance"}});
|
||||
}
|
||||
const auto BuiltIn = [&](uint32_t& variable, uint32_t type, const char* name,
|
||||
spv::BuiltIn builtin) {
|
||||
if (variable == 0) {
|
||||
|
||||
@@ -1206,13 +1206,12 @@ uint32_t FindSelectionMerge(const Graph& graph, const BasicBlock& block) {
|
||||
return false_target;
|
||||
}
|
||||
// A return can leave a selection without reaching its merge. Keep the
|
||||
// continuing arm as the merge instead of joining live state with a return.
|
||||
if (graph.Dominates(block.id, false_target) &&
|
||||
HasLinearPathToTerminal(graph, true_target)) {
|
||||
// continuing arm as the merge. Shared continuations receive a dedicated
|
||||
// gateway in SplitSharedMergeBlock before structured control is emitted.
|
||||
if (HasLinearPathToTerminal(graph, true_target)) {
|
||||
return false_target;
|
||||
}
|
||||
if (graph.Dominates(block.id, true_target) &&
|
||||
HasLinearPathToTerminal(graph, false_target)) {
|
||||
if (HasLinearPathToTerminal(graph, false_target)) {
|
||||
return true_target;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +175,7 @@ constexpr MimgSampleInfo MIMG_SAMPLE_OPCODE_LIST[] = {
|
||||
};
|
||||
|
||||
constexpr MimgGatherInfo MIMG_GATHER_OPCODE_LIST[] = {
|
||||
{0x44u, Opcode::IMAGE_GATHER4_L, ImageSampleFlagLod},
|
||||
{0x47u, Opcode::IMAGE_GATHER4_LZ, ImageSampleFlagLevelZero},
|
||||
{0x48u, Opcode::IMAGE_GATHER4_C, ImageSampleFlagCompare},
|
||||
{0x4fu, Opcode::IMAGE_GATHER4_C_LZ,
|
||||
|
||||
@@ -24,7 +24,8 @@ constexpr OpcodeMap SOP2_OPCODE_LIST[] = {
|
||||
{0x1cu, Opcode::S_XNOR_B32}, {0x1du, Opcode::S_XNOR_B64},
|
||||
{0x1eu, Opcode::S_LSHL_B32}, {0x1fu, Opcode::S_LSHL_B64},
|
||||
{0x20u, Opcode::S_LSHR_B32}, {0x21u, Opcode::S_LSHR_B64},
|
||||
{0x22u, Opcode::S_ASHR_I32}, {0x24u, Opcode::S_BFM_B32},
|
||||
{0x22u, Opcode::S_ASHR_I32}, {0x23u, Opcode::S_ASHR_I64},
|
||||
{0x24u, Opcode::S_BFM_B32},
|
||||
{0x25u, Opcode::S_BFM_B64}, {0x26u, Opcode::S_MUL_I32},
|
||||
{0x27u, Opcode::S_BFE_U32}, {0x28u, Opcode::S_BFE_I32},
|
||||
{0x29u, Opcode::S_BFE_U64}, {0x2cu, Opcode::S_ABSDIFF_I32},
|
||||
@@ -105,6 +106,7 @@ constexpr OpcodeMap SOPP_OPCODE_LIST[] = {
|
||||
{0x10u, Opcode::S_SENDMSG},
|
||||
{0x12u, Opcode::S_TRAP},
|
||||
{0x16u, Opcode::S_TTRACEDATA},
|
||||
{0x17u, Opcode::S_CBRANCH_CDBGSYS},
|
||||
{0x20u, Opcode::S_INST_PREFETCH},
|
||||
{0x23u, Opcode::S_WAITCNT_DEPCTR},
|
||||
};
|
||||
@@ -136,6 +138,10 @@ void DecodeSop1(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
||||
inst.family = Family::SOP1;
|
||||
inst.opcode_id = opcode;
|
||||
inst.opcode = Detail::LookupOpcode(SOP1_OPS, opcode);
|
||||
if (opcode == 0x21u && sdst == 125u) {
|
||||
// S_SWAPPC_B64 with NULL discards the return PC, so it is a plain jump.
|
||||
inst.opcode = Opcode::S_SETPC_B64;
|
||||
}
|
||||
SetRawWords(inst, code, word_index, 1);
|
||||
|
||||
if (inst.opcode == Opcode::UNSUPPORTED) {
|
||||
|
||||
@@ -144,6 +144,7 @@ std::string FormatMimg(const Instruction& inst) {
|
||||
}
|
||||
switch (inst.opcode) {
|
||||
case Opcode::IMAGE_SAMPLE:
|
||||
case Opcode::IMAGE_GATHER4_L:
|
||||
case Opcode::IMAGE_GATHER4_LZ:
|
||||
case Opcode::IMAGE_GATHER4_C:
|
||||
case Opcode::IMAGE_GATHER4_C_LZ:
|
||||
@@ -187,6 +188,8 @@ std::string FormatExp(const Instruction& inst) {
|
||||
|
||||
bool IsConditionalBranch(Opcode opcode) {
|
||||
switch (opcode) {
|
||||
// DevKit NGG should be disabled.
|
||||
case Opcode::S_CBRANCH_CDBGSYS: return false;
|
||||
case Opcode::S_CBRANCH_SCC0:
|
||||
case Opcode::S_CBRANCH_SCC1:
|
||||
case Opcode::S_CBRANCH_VCCZ:
|
||||
@@ -554,6 +557,7 @@ std::string InstructionToString(const Instruction& inst) {
|
||||
case Opcode::S_CBRANCH_VCCNZ:
|
||||
case Opcode::S_CBRANCH_EXECZ:
|
||||
case Opcode::S_CBRANCH_EXECNZ:
|
||||
case Opcode::S_CBRANCH_CDBGSYS:
|
||||
return WithUnsupportedReason(inst, fmt::format("0x{:08x}: {} 0x{:08x}", inst.pc,
|
||||
magic_enum::enum_name(inst.opcode),
|
||||
inst.branch_target));
|
||||
@@ -577,6 +581,7 @@ std::string InstructionToString(const Instruction& inst) {
|
||||
case Opcode::IMAGE_LOAD_MIP:
|
||||
case Opcode::IMAGE_GET_RESINFO:
|
||||
case Opcode::IMAGE_GET_LOD:
|
||||
case Opcode::IMAGE_GATHER4_L:
|
||||
case Opcode::IMAGE_GATHER4_LZ:
|
||||
case Opcode::IMAGE_GATHER4_C:
|
||||
case Opcode::IMAGE_GATHER4_C_LZ:
|
||||
|
||||
@@ -106,6 +106,7 @@ enum class Opcode {
|
||||
S_LSHR_B32,
|
||||
S_LSHR_B64,
|
||||
S_ASHR_I32,
|
||||
S_ASHR_I64,
|
||||
S_MUL_I32,
|
||||
S_MUL_HI_U32,
|
||||
S_MUL_HI_I32,
|
||||
@@ -377,6 +378,7 @@ enum class Opcode {
|
||||
V_CMP_GT_F16,
|
||||
V_CMP_LG_F16,
|
||||
V_CMP_GE_F16,
|
||||
V_CMP_NGT_F16,
|
||||
V_CMP_NEQ_F16,
|
||||
V_CMPX_LT_F16,
|
||||
V_CMPX_EQ_F16,
|
||||
@@ -559,6 +561,7 @@ enum class Opcode {
|
||||
IMAGE_ATOMIC_OR,
|
||||
IMAGE_ATOMIC_XOR,
|
||||
IMAGE_SAMPLE,
|
||||
IMAGE_GATHER4_L,
|
||||
IMAGE_GATHER4_LZ,
|
||||
IMAGE_GATHER4_C,
|
||||
IMAGE_GATHER4_C_LZ,
|
||||
@@ -581,6 +584,7 @@ enum class Opcode {
|
||||
S_CBRANCH_VCCNZ,
|
||||
S_CBRANCH_EXECZ,
|
||||
S_CBRANCH_EXECNZ,
|
||||
S_CBRANCH_CDBGSYS,
|
||||
S_SENDMSG,
|
||||
S_SETREG_B32,
|
||||
S_SLEEP,
|
||||
|
||||
@@ -250,7 +250,8 @@ constexpr VopcOpcodeInfo VOPC_OPCODE_LIST[] = {
|
||||
{0xf5u, Opcode::V_CMPX_NE_U64, false}, {0xc9u, Opcode::V_CMP_LT_F16},
|
||||
{0xcau, Opcode::V_CMP_EQ_F16}, {0xcbu, Opcode::V_CMP_LE_F16},
|
||||
{0xccu, Opcode::V_CMP_GT_F16}, {0xcdu, Opcode::V_CMP_LG_F16},
|
||||
{0xceu, Opcode::V_CMP_GE_F16}, {0xedu, Opcode::V_CMP_NEQ_F16},
|
||||
{0xceu, Opcode::V_CMP_GE_F16}, {0xebu, Opcode::V_CMP_NGT_F16},
|
||||
{0xedu, Opcode::V_CMP_NEQ_F16},
|
||||
{0xd9u, Opcode::V_CMPX_LT_F16}, {0xdau, Opcode::V_CMPX_EQ_F16},
|
||||
{0xdbu, Opcode::V_CMPX_LE_F16}, {0xdcu, Opcode::V_CMPX_GT_F16},
|
||||
{0xdeu, Opcode::V_CMPX_GE_F16}, {0xfbu, Opcode::V_CMPX_NGT_F16},
|
||||
@@ -516,7 +517,7 @@ struct Vop1SdwaRule {
|
||||
};
|
||||
|
||||
constexpr Vop1SdwaRule VOP1_SDWA_RULES[] = {
|
||||
{Opcode::V_MOV_B32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), SdwaSelWords(),
|
||||
{Opcode::V_MOV_B32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), SdwaSelBytes() | SdwaSelWords(),
|
||||
SdwaSelWords() | SdwaSelFull(), false},
|
||||
{Opcode::V_CVT_F32_U32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
|
||||
{Opcode::V_CVT_F32_I32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
|
||||
@@ -824,6 +825,7 @@ bool IsVopcFloatCompareOpcode(Opcode opcode) {
|
||||
case Opcode::V_CMP_GT_F16:
|
||||
case Opcode::V_CMP_LG_F16:
|
||||
case Opcode::V_CMP_GE_F16:
|
||||
case Opcode::V_CMP_NGT_F16:
|
||||
case Opcode::V_CMP_NEQ_F16:
|
||||
case Opcode::V_CMPX_LT_F16:
|
||||
case Opcode::V_CMPX_EQ_F16:
|
||||
|
||||
@@ -168,6 +168,21 @@ bool Translator::SimpleInteger(const Decoder::Instruction& inst, IR::ValueOpcode
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Translator::S_ASHR_I64(const Decoder::Instruction& inst) {
|
||||
// Signed 64-bit sources sign-extend literals; generic B64 operands zero-extend them.
|
||||
const auto source =
|
||||
inst.src0.kind == Decoder::OperandKind::LiteralConstant
|
||||
? ir.ConstructU64(
|
||||
ReadU32(inst.src0),
|
||||
IR::U32(IR::Value((inst.src0.value & 0x80000000u) ? 0xffffffffu : 0u)))
|
||||
: ReadU64(inst.src0);
|
||||
const auto result =
|
||||
ir.Emit(IR::ValueOpcode::ShiftRightArithmetic64, {source, ReadU32(inst.src1)});
|
||||
WriteOperand(inst.dst, result);
|
||||
ir.SetScc(IR::U1(ir.Emit(IR::ValueOpcode::INotEqual64, {result, IR::Value(uint64_t {0})})));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Translator::ComposedIntegerBinary(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
|
||||
bool negate_rhs, bool negate_result, bool update_scc) {
|
||||
const auto lhs = ReadU32(inst.src0);
|
||||
|
||||
@@ -1034,6 +1034,7 @@ bool Translator::EmitMemory(const Decoder::Instruction& inst) {
|
||||
case Decoder::Opcode::IMAGE_STORE:
|
||||
case Decoder::Opcode::IMAGE_STORE_MIP: return IMAGE_STORE(inst);
|
||||
case Decoder::Opcode::IMAGE_SAMPLE: return IMAGE_SAMPLE(inst);
|
||||
case Decoder::Opcode::IMAGE_GATHER4_L:
|
||||
case Decoder::Opcode::IMAGE_GATHER4_LZ:
|
||||
case Decoder::Opcode::IMAGE_GATHER4_C:
|
||||
case Decoder::Opcode::IMAGE_GATHER4_C_LZ:
|
||||
|
||||
@@ -179,6 +179,7 @@ bool Translator::EmitScalar(const Decoder::Instruction& inst) {
|
||||
case O::S_LSHR_B64:
|
||||
return SimpleInteger(inst, IR::ValueOpcode::ShiftRightLogical64, IR::Type::U64, false,
|
||||
false, true);
|
||||
case O::S_ASHR_I64: return S_ASHR_I64(inst);
|
||||
|
||||
case O::S_ANDN2_B32:
|
||||
return ComposedIntegerBinary(inst, IR::ValueOpcode::BitwiseAnd32, true, false, true);
|
||||
@@ -227,6 +228,7 @@ bool Translator::EmitScalar(const Decoder::Instruction& inst) {
|
||||
case O::S_CBRANCH_VCCNZ:
|
||||
case O::S_CBRANCH_EXECZ:
|
||||
case O::S_CBRANCH_EXECNZ:
|
||||
case O::S_CBRANCH_CDBGSYS:
|
||||
case O::S_ENDPGM: return true;
|
||||
default: return false;
|
||||
}
|
||||
|
||||
@@ -789,15 +789,6 @@ const EmbeddedFetchLoad* FindEmbeddedFetchLoad(const EmbeddedFetchPlan* plan, ui
|
||||
return found != plan->loads.end() ? &*found : nullptr;
|
||||
}
|
||||
|
||||
bool IsEmbeddedFetchPrologLoad(const EmbeddedFetchPlan* plan, uint32_t pc) {
|
||||
if (plan == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return std::ranges::any_of(plan->loads, [pc](const auto& load) {
|
||||
return std::ranges::find(load.prolog_loads, pc) != load.prolog_loads.end();
|
||||
});
|
||||
}
|
||||
|
||||
int ResolveEmbeddedFetchResource(const ShaderVertexInputInfo& input,
|
||||
const EmbeddedFetchLoad& load) {
|
||||
if (load.attrib_id >= 0 && load.attrib_id < input.resources_num &&
|
||||
@@ -819,22 +810,6 @@ int ResolveEmbeddedFetchResource(const ShaderVertexInputInfo& input,
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool IsScalarMemoryLoad(Decoder::Opcode opcode) {
|
||||
switch (opcode) {
|
||||
case Decoder::Opcode::S_LOAD_DWORD:
|
||||
case Decoder::Opcode::S_LOAD_DWORDX2:
|
||||
case Decoder::Opcode::S_LOAD_DWORDX4:
|
||||
case Decoder::Opcode::S_LOAD_DWORDX8:
|
||||
case Decoder::Opcode::S_LOAD_DWORDX16:
|
||||
case Decoder::Opcode::S_BUFFER_LOAD_DWORD:
|
||||
case Decoder::Opcode::S_BUFFER_LOAD_DWORDX2:
|
||||
case Decoder::Opcode::S_BUFFER_LOAD_DWORDX4:
|
||||
case Decoder::Opcode::S_BUFFER_LOAD_DWORDX8:
|
||||
case Decoder::Opcode::S_BUFFER_LOAD_DWORDX16: return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool IsBufferDwordLoad(Decoder::Opcode opcode) {
|
||||
switch (opcode) {
|
||||
case Decoder::Opcode::BUFFER_LOAD_DWORD:
|
||||
@@ -1127,26 +1102,32 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
|
||||
entry_ir.BitwiseOr(wave_info, entry_ir.BitwiseOr(entry_ir.ShiftLeftLogical(
|
||||
primitive_count, u32(8)),
|
||||
vertex_count)));
|
||||
// GS adjacency addresses local ES records in LDS. Strip winding alternates
|
||||
// with the global primitive number, including across subgroup boundaries.
|
||||
const auto parity = mesh.input_primitive ==
|
||||
static_cast<uint32_t>(Prospero::PrimitiveType::kTriStrip)
|
||||
? entry_ir.BitwiseAnd(entry_ir.IAdd(primitive_chunk, local), u32(1))
|
||||
: u32(0);
|
||||
// GS adjacency addresses local ES records in LDS. Fans retain the draw's
|
||||
// center in every subgroup; strip winding follows the global primitive.
|
||||
const auto vertex = entry_ir.IMul(local, step);
|
||||
const auto first = entry_ir.IAdd(vertex, parity);
|
||||
const auto second = mesh.InputPrimitiveSize() >= 2u
|
||||
? entry_ir.ISub(entry_ir.IAdd(vertex, u32(1)), parity)
|
||||
: u32(0);
|
||||
const auto third = mesh.InputPrimitiveSize() == 3u
|
||||
? entry_ir.IAdd(vertex, u32(2))
|
||||
: u32(0);
|
||||
auto first = vertex;
|
||||
auto second = u32(0);
|
||||
auto third = u32(0);
|
||||
if (mesh.InputPrimitiveSize() >= 2u) {
|
||||
second = entry_ir.IAdd(vertex, u32(1));
|
||||
}
|
||||
if (mesh.InputPrimitiveSize() == 3u) {
|
||||
third = entry_ir.IAdd(vertex, u32(2));
|
||||
}
|
||||
auto input_vertex = entry_ir.IAdd(chunk, local);
|
||||
if (mesh.input_primitive == static_cast<uint32_t>(Prospero::PrimitiveType::kTriFan)) {
|
||||
first = u32(0);
|
||||
input_vertex = entry_ir.Select(entry_ir.IEqual(local, u32(0)), u32(0), input_vertex);
|
||||
} else if (mesh.input_primitive == static_cast<uint32_t>(Prospero::PrimitiveType::kTriStrip)) {
|
||||
const auto parity = entry_ir.BitwiseAnd(entry_ir.IAdd(primitive_chunk, local), u32(1));
|
||||
first = entry_ir.IAdd(first, parity);
|
||||
second = entry_ir.ISub(second, parity);
|
||||
}
|
||||
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(0),
|
||||
entry_ir.BitwiseOr(entry_ir.ShiftLeftLogical(first, u32(2)),
|
||||
entry_ir.ShiftLeftLogical(second, u32(18))));
|
||||
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(1),
|
||||
entry_ir.ShiftLeftLogical(third, u32(2)));
|
||||
const auto input_vertex = entry_ir.IAdd(chunk, local);
|
||||
const auto index_bytes = draw(3);
|
||||
const auto indexed = entry_ir.INotEqual(index_bytes, u32(0));
|
||||
const auto index_low = draw(4);
|
||||
@@ -1209,13 +1190,15 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
|
||||
builtin(IR::StageInputKind::PrimitiveId));
|
||||
} else if (options.stage == ShaderType::Pixel) {
|
||||
const auto* ps = options.input_info.pixel;
|
||||
if (ps->ps_perspective_center_vgpr != UINT32_MAX) {
|
||||
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(ps->ps_perspective_center_vgpr),
|
||||
builtin(IR::StageInputKind::BaryCoordSmooth, 0));
|
||||
entry_ir.SetVectorReg(
|
||||
static_cast<IR::VectorReg>(ps->ps_perspective_center_vgpr + 1u),
|
||||
builtin(IR::StageInputKind::BaryCoordSmooth, 1));
|
||||
}
|
||||
const auto barycentric_pair = [&](uint32_t reg, IR::StageInputKind kind) {
|
||||
if (reg != UINT32_MAX) {
|
||||
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(reg), builtin(kind, 0));
|
||||
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(reg + 1u), builtin(kind, 1));
|
||||
}
|
||||
};
|
||||
barycentric_pair(ps->ps_perspective_center_vgpr, IR::StageInputKind::BaryCoordSmooth);
|
||||
barycentric_pair(ps->ps_perspective_centroid_vgpr,
|
||||
IR::StageInputKind::BaryCoordSmoothCentroid);
|
||||
uint32_t reg = ps->ps_system_input_base;
|
||||
if (ps->ps_pos_x) {
|
||||
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(reg++),
|
||||
@@ -1258,10 +1241,6 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
|
||||
if (IsCodeTableLoad(cfg, instruction.pc)) {
|
||||
continue;
|
||||
}
|
||||
if (IsScalarMemoryLoad(instruction.opcode) &&
|
||||
IsEmbeddedFetchPrologLoad(options.embedded_fetch, instruction.pc)) {
|
||||
continue;
|
||||
}
|
||||
const auto* embedded = FindEmbeddedFetchLoad(options.embedded_fetch, instruction.pc);
|
||||
if (embedded != nullptr && IsBufferDwordLoad(instruction.opcode) &&
|
||||
instruction.data_dwords == embedded->components &&
|
||||
|
||||
@@ -13,7 +13,6 @@ struct EmbeddedFetchLoad {
|
||||
uint32_t pc = 0;
|
||||
int attrib_id = -1;
|
||||
uint32_t components = 0;
|
||||
std::vector<uint32_t> prolog_loads;
|
||||
};
|
||||
|
||||
struct EmbeddedFetchPlan {
|
||||
|
||||
@@ -176,6 +176,7 @@ private:
|
||||
bool negate_result);
|
||||
bool SimpleInteger(const Decoder::Instruction& inst, IR::ValueOpcode opcode, IR::Type type,
|
||||
bool reverse, bool mask_shift_count, bool update_scc);
|
||||
bool S_ASHR_I64(const Decoder::Instruction& inst);
|
||||
bool ComposedIntegerBinary(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
|
||||
bool negate_rhs, bool negate_result, bool update_scc);
|
||||
bool V_AND_OR_B32(const Decoder::Instruction& inst);
|
||||
|
||||
@@ -264,6 +264,9 @@ bool Translator::EmitVector(const Decoder::Instruction& inst) {
|
||||
case O::V_CMPX_LE_F16:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, true, true);
|
||||
return true;
|
||||
case O::V_CMP_NGT_F16:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, true, false);
|
||||
return true;
|
||||
case O::V_CMPX_NGT_F16:
|
||||
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, true, true);
|
||||
return true;
|
||||
|
||||
@@ -112,6 +112,10 @@ ResourcePlan& ResourcePlan::operator=(ResourcePlan&& other) noexcept {
|
||||
}
|
||||
|
||||
Program::~Program() {
|
||||
// Planning expressions can refer to block values but outlive block storage in the base class.
|
||||
for (auto& inst: value_storage) {
|
||||
inst.Invalidate();
|
||||
}
|
||||
// Values may cross block boundaries. Detach all arguments before any block starts destroying
|
||||
// its instruction storage so reverse-use links always point to live definitions.
|
||||
for (auto* block: blocks) {
|
||||
@@ -184,6 +188,22 @@ Value ResolveInvariantPhi(const ResourcePlan& program, Value value) {
|
||||
return invariant;
|
||||
}
|
||||
|
||||
bool HasShaderMemoryWrites(const Program& program) {
|
||||
for (const auto* block: program.blocks) {
|
||||
for (const auto& inst: *block) {
|
||||
const auto op = inst.GetOpcode();
|
||||
const auto buffer = BufferAccessOf(op);
|
||||
const auto image = ImageOpcodeInfoOf(op).access;
|
||||
if (buffer == BufferAccess::Write || buffer == BufferAccess::Atomic ||
|
||||
image == ImageAccess::Write || image == ImageAccess::Atomic ||
|
||||
AddressOpcodeInfoOf(op).access == AddressAccess::Write) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ValidateProgram(const Program& program, bool require_ssa) {
|
||||
if (program.blocks.size() != program.block_info.size() ||
|
||||
program.blocks.size() != program.block_storage.size()) {
|
||||
|
||||
@@ -171,6 +171,7 @@ enum class StageInputKind {
|
||||
Layer,
|
||||
SampleId,
|
||||
BaryCoordSmooth,
|
||||
BaryCoordSmoothCentroid,
|
||||
BaryCoordNoPerspective,
|
||||
WorkgroupId,
|
||||
LocalInvocationId,
|
||||
@@ -562,6 +563,7 @@ struct Program: ResourcePlan {
|
||||
};
|
||||
|
||||
std::string ProgramToString(const Program& program);
|
||||
bool HasShaderMemoryWrites(const Program& program);
|
||||
|
||||
void ValidateProgram(const Program& program, bool require_ssa);
|
||||
void ResolveControlFlowIdentities(Program& program);
|
||||
|
||||
@@ -647,7 +647,8 @@ bool BuildSamplerPlan(const ShaderInfo& base, const Images& images, SamplerPlan&
|
||||
}
|
||||
|
||||
static std::vector<ResourceBlock> ResourceControlFlow(const Program& program) {
|
||||
if (program.blocks.size() != program.block_info.size()) {
|
||||
// Any shader write may alias a scalar predicate read, including on a later loop visit.
|
||||
if (program.blocks.size() != program.block_info.size() || HasShaderMemoryWrites(program)) {
|
||||
return {};
|
||||
}
|
||||
std::unordered_map<uint32_t, uint32_t> indices;
|
||||
@@ -687,12 +688,6 @@ static std::vector<ResourceBlock> ResourceControlFlow(const Program& program) {
|
||||
const auto op = inst.GetOpcode();
|
||||
const auto buffer = BufferAccessOf(op);
|
||||
const auto image = ImageOpcodeInfoOf(op);
|
||||
// Any shader write may alias a scalar predicate read, including on a later loop visit.
|
||||
if (buffer == BufferAccess::Write || buffer == BufferAccess::Atomic ||
|
||||
image.access == ImageAccess::Write || image.access == ImageAccess::Atomic ||
|
||||
AddressOpcodeInfoOf(op).access == AddressAccess::Write) {
|
||||
return {};
|
||||
}
|
||||
if (buffer == BufferAccess::None && image.access == ImageAccess::None) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ public:
|
||||
m_info.samplers.clear();
|
||||
m_info.sampled_pairs.clear();
|
||||
m_info.uses_dma = false;
|
||||
m_shader_writes = HasShaderMemoryWrites(program);
|
||||
}
|
||||
|
||||
void Run() {
|
||||
@@ -168,15 +169,85 @@ private:
|
||||
std::abort();
|
||||
}
|
||||
|
||||
Value LowerDescriptorPhi(Value value) {
|
||||
value = value.Resolve();
|
||||
const auto* phi = value.TryInstruction();
|
||||
if (m_shader_writes || phi == nullptr || phi->GetOpcode() != ValueOpcode::Phi ||
|
||||
phi->NumArgs() != 2u || phi->NumPhiBlocks() != 2u || phi->GetType() != Type::U32 ||
|
||||
m_program.blocks.size() != m_program.block_info.size()) {
|
||||
return value;
|
||||
}
|
||||
for (const auto& [original, selected]: m_descriptor_selections) {
|
||||
if (original == phi) {
|
||||
return selected;
|
||||
}
|
||||
}
|
||||
const auto* merge = phi->Parent();
|
||||
const auto* branch = phi->PhiBlock(0);
|
||||
if (merge == nullptr || branch == nullptr || phi->PhiBlock(1) == nullptr ||
|
||||
branch == phi->PhiBlock(1)) {
|
||||
return value;
|
||||
}
|
||||
if (branch->ImmSuccessors().size() != 2u) {
|
||||
if (branch->ImmPredecessors().size() != 1u) {
|
||||
return value;
|
||||
}
|
||||
branch = branch->ImmPredecessors()[0];
|
||||
}
|
||||
if (branch == merge || branch->ImmSuccessors().size() != 2u) {
|
||||
return value;
|
||||
}
|
||||
std::array<uint32_t, 2> target_ids;
|
||||
for (uint32_t arm = 0; arm < 2; arm++) {
|
||||
const auto* incoming = phi->PhiBlock(arm);
|
||||
if (incoming == merge ||
|
||||
(incoming != branch &&
|
||||
(incoming->ImmPredecessors().size() != 1u ||
|
||||
incoming->ImmPredecessors()[0] != branch ||
|
||||
incoming->ImmSuccessors().size() != 1u ||
|
||||
incoming->ImmSuccessors()[0] != merge))) {
|
||||
return value;
|
||||
}
|
||||
const auto* target = incoming == branch ? merge : incoming;
|
||||
const auto it = std::ranges::find(m_program.blocks, target);
|
||||
if (it == m_program.blocks.end()) {
|
||||
return value;
|
||||
}
|
||||
target_ids[arm] = m_program.block_info[it - m_program.blocks.begin()].id;
|
||||
}
|
||||
const auto branch_it = std::ranges::find(m_program.blocks, branch);
|
||||
if (branch_it == m_program.blocks.end()) {
|
||||
return value;
|
||||
}
|
||||
const auto& info = m_program.block_info[branch_it - m_program.blocks.begin()];
|
||||
const auto& term = info.terminator;
|
||||
if (term.kind != CFG::TerminatorKind::ConditionalBranch ||
|
||||
!((term.true_block == target_ids[0] && term.false_block == target_ids[1]) ||
|
||||
(term.false_block == target_ids[0] && term.true_block == target_ids[1])) ||
|
||||
!ValidateRuntimeValue(m_program, info.condition, RuntimeValueType::Integer) ||
|
||||
!ValidateRuntimeValue(m_program, phi->Arg(0)) ||
|
||||
!ValidateRuntimeValue(m_program, phi->Arg(1))) {
|
||||
return value;
|
||||
}
|
||||
// Retain a host expression; replacing the GPU Phi would break SSA dominance.
|
||||
const auto true_arg = term.true_block == target_ids[0] ? 0u : 1u;
|
||||
auto& selected = m_program.value_storage.emplace_back(ValueOpcode::SelectU32);
|
||||
selected.SetArg(0, info.condition);
|
||||
selected.SetArg(1, phi->Arg(true_arg));
|
||||
selected.SetArg(2, phi->Arg(true_arg ^ 1u));
|
||||
m_descriptor_selections.emplace_back(phi, Value(&selected));
|
||||
return Value(&selected);
|
||||
}
|
||||
|
||||
void MakeSource(const Inst& handle, uint32_t width, bool sampler, bool sample_adjust,
|
||||
DescriptorSource& descriptor, uint32_t pc) const {
|
||||
DescriptorSource& descriptor, uint32_t pc) {
|
||||
if (handle.NumArgs() != width) {
|
||||
Fail(pc, fmt::format("{} has {} descriptor dwords, expected {}",
|
||||
ValueOpcodeName(handle.GetOpcode()), handle.NumArgs(), width));
|
||||
}
|
||||
descriptor.dword_count = width;
|
||||
for (uint32_t i = 0; i < width; i++) {
|
||||
descriptor.dwords[i] = handle.Arg(i).Resolve();
|
||||
descriptor.dwords[i] = LowerDescriptorPhi(handle.Arg(i));
|
||||
}
|
||||
if (sample_adjust) {
|
||||
descriptor.dwords[3] = CanonicalizeSampleAdjustDword3(descriptor.dwords[3]);
|
||||
@@ -744,12 +815,14 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
Program& m_program;
|
||||
ShaderInfo m_info;
|
||||
std::vector<DescriptorSource> m_sources;
|
||||
std::vector<HandlePatch> m_handle_patches;
|
||||
std::vector<MemoryPatch> m_memory_patches;
|
||||
std::vector<IndirectImagePlan> m_indirect_images;
|
||||
Program& m_program;
|
||||
ShaderInfo m_info;
|
||||
std::vector<DescriptorSource> m_sources;
|
||||
std::vector<HandlePatch> m_handle_patches;
|
||||
std::vector<MemoryPatch> m_memory_patches;
|
||||
std::vector<IndirectImagePlan> m_indirect_images;
|
||||
std::vector<std::pair<const Inst*, Value>> m_descriptor_selections;
|
||||
bool m_shader_writes = false;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -143,6 +143,7 @@ void ValidateValueReferences(const Program& program, ShaderStageInputInfo input_
|
||||
}
|
||||
break;
|
||||
case StageInputKind::BaryCoordSmooth:
|
||||
case StageInputKind::BaryCoordSmoothCentroid:
|
||||
case StageInputKind::BaryCoordNoPerspective:
|
||||
if (component >= 2u) {
|
||||
return Fail("typed barycentric component is out of range");
|
||||
@@ -225,6 +226,18 @@ void CollectPixelInputs(const Program& program, const ShaderPixelInputInfo* pixe
|
||||
}
|
||||
}
|
||||
}
|
||||
// Aliases of a vertex output share one SPIR-V interface variable. If any
|
||||
// alias reads raw vertices, interpolate the other aliases from those too.
|
||||
for (uint32_t input = 0; input < pixel->input_num; input++) {
|
||||
for (uint32_t alias = 0; alias < pixel->input_num; alias++) {
|
||||
if (ShaderPixelParameterMappedLocation(*pixel, input) ==
|
||||
ShaderPixelParameterMappedLocation(*pixel, alias) &&
|
||||
ShaderPixelParameterIsFlat(*pixel, input) ==
|
||||
ShaderPixelParameterIsFlat(*pixel, alias)) {
|
||||
per_vertex[input] = per_vertex[input] || per_vertex[alias];
|
||||
}
|
||||
}
|
||||
}
|
||||
for (uint32_t input = 0; input < pixel->input_num; input++) {
|
||||
AddInput(info, StageInputKind::Parameter, input, 4, fmt::format("in_param_{}", input),
|
||||
per_vertex[input]);
|
||||
@@ -287,7 +300,8 @@ void CollectBuiltinInputs(const Program& program, ShaderInfo& info) {
|
||||
case StageInputKind::Layer: AddInput(info, kind, 0, 1, "gl_Layer"); break;
|
||||
case StageInputKind::SampleId: AddInput(info, kind, 0, 1, "gl_SampleID"); break;
|
||||
case StageInputKind::BaryCoordSmooth:
|
||||
AddInput(info, kind, 0, 3, "gl_BaryCoordKHR");
|
||||
case StageInputKind::BaryCoordSmoothCentroid:
|
||||
AddInput(info, StageInputKind::BaryCoordSmooth, 0, 3, "gl_BaryCoordKHR");
|
||||
break;
|
||||
case StageInputKind::BaryCoordNoPerspective:
|
||||
AddInput(info, kind, 0, 3, "gl_BaryCoordNoPerspKHR");
|
||||
|
||||
@@ -16,6 +16,13 @@ namespace {
|
||||
|
||||
constexpr uint64_t AddressMask = 0x0000ffffffffffffull;
|
||||
|
||||
SrtRuntime CleanRuntime(SrtRuntime runtime) {
|
||||
runtime.read_memory = runtime.read_specialization_memory != nullptr
|
||||
? runtime.read_specialization_memory
|
||||
: +[](void*, uint64_t, uint32_t*) { return false; };
|
||||
return runtime;
|
||||
}
|
||||
|
||||
const char* StageName(ShaderType stage) {
|
||||
switch (stage) {
|
||||
case ShaderType::Vertex: return "vertex";
|
||||
@@ -646,8 +653,10 @@ private:
|
||||
case ValueOpcode::GetShaderBase: result = m_runtime.shader_base; return true;
|
||||
case ValueOpcode::Phi: return EvaluatePhi(inst, result);
|
||||
case ValueOpcode::ReadFirstLane: {
|
||||
Evaluator active(m_program, m_runtime, m_clean_flat_slots, m_clean_evaluator,
|
||||
inst.Arg(1));
|
||||
const auto clean_runtime = CleanRuntime(m_runtime);
|
||||
Evaluator clean_active(m_program, clean_runtime, {}, nullptr, inst.Arg(1));
|
||||
Evaluator active(m_program, m_runtime, m_clean_flat_slots, &clean_active,
|
||||
inst.Arg(1));
|
||||
return active.EvaluateWide(inst.Arg(0), result);
|
||||
}
|
||||
case ValueOpcode::BitCastU32F32:
|
||||
@@ -892,12 +901,13 @@ private:
|
||||
}
|
||||
case ValueOpcode::SelectU32:
|
||||
case ValueOpcode::SelectU1:
|
||||
case ValueOpcode::SelectF32:
|
||||
if (ternary()) {
|
||||
result = a != 0u ? b : c;
|
||||
return true;
|
||||
case ValueOpcode::SelectF32: {
|
||||
auto& predicate = m_clean_evaluator != nullptr ? *m_clean_evaluator : *this;
|
||||
if (predicate.EvaluateWide(inst.Arg(0), a)) {
|
||||
return Arg(inst, a != 0u ? 1u : 2u, result);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case ValueOpcode::IEqual32:
|
||||
if (binary()) {
|
||||
result = static_cast<uint32_t>(a) == static_cast<uint32_t>(b);
|
||||
@@ -977,7 +987,7 @@ bool EvaluateRuntimeSourcesImpl(const ResourcePlan& program, std::span<const uin
|
||||
const SrtRuntime& runtime, std::vector<DescriptorValue>& results,
|
||||
std::vector<uint32_t>& flat, bool evaluate_flat,
|
||||
std::span<const uint8_t> clean_flat_slots,
|
||||
std::vector<uint8_t>& active_sources) {
|
||||
std::vector<uint8_t>& active_sources) {
|
||||
if (!program.srt_plan_complete) {
|
||||
return false;
|
||||
}
|
||||
@@ -985,8 +995,7 @@ bool EvaluateRuntimeSourcesImpl(const ResourcePlan& program, std::span<const uin
|
||||
runtime.read_specialization_memory == nullptr) {
|
||||
return false;
|
||||
}
|
||||
SrtRuntime clean_runtime = runtime;
|
||||
clean_runtime.read_memory = runtime.read_specialization_memory;
|
||||
const auto clean_runtime = CleanRuntime(runtime);
|
||||
Evaluator clean_evaluator(program, clean_runtime);
|
||||
Evaluator evaluator(program, runtime, clean_flat_slots, &clean_evaluator);
|
||||
std::vector<uint8_t> active;
|
||||
@@ -1081,11 +1090,8 @@ bool EvaluateUniformValues(const ResourcePlan& program, std::span<const Value> v
|
||||
if (values.size() != results.size()) {
|
||||
return false;
|
||||
}
|
||||
auto clean = runtime;
|
||||
clean.read_memory = runtime.read_specialization_memory != nullptr
|
||||
? runtime.read_specialization_memory
|
||||
: +[](void*, uint64_t, uint32_t*) { return false; };
|
||||
Evaluator evaluator(program, clean);
|
||||
const auto clean = CleanRuntime(runtime);
|
||||
Evaluator evaluator(program, clean);
|
||||
for (size_t i = 0; i < values.size(); ++i) {
|
||||
if (!evaluator.Evaluate(values[i], results[i])) {
|
||||
return false;
|
||||
|
||||
@@ -40,12 +40,15 @@ std::vector<Parameter> GetParameters(const ShaderVertexInputInfo& vertex_info,
|
||||
}
|
||||
|
||||
std::vector<Parameter> parameters;
|
||||
std::array<bool, 32> output_locations {};
|
||||
for (const auto input: active_inputs) {
|
||||
const auto input_location = ShaderPixelParameterMappedLocation(*pixel_info, input);
|
||||
if ((vertex_info.stage.program->param_export_mask & (1u << input_location)) != 0) {
|
||||
parameters.push_back({input_location,
|
||||
ShaderPixelParameterLocation(*pixel_info, active_inputs, input),
|
||||
const auto output_location = ShaderPixelParameterLocation(*pixel_info, active_inputs, input);
|
||||
if ((vertex_info.stage.program->param_export_mask & (1u << input_location)) != 0 &&
|
||||
!output_locations[output_location]) {
|
||||
parameters.push_back({input_location, output_location,
|
||||
ShaderPixelParameterIsFlat(*pixel_info, input)});
|
||||
output_locations[output_location] = true;
|
||||
}
|
||||
}
|
||||
return parameters;
|
||||
|
||||
@@ -573,15 +573,20 @@ static void ShaderGetStaticInputInfoPS(
|
||||
ps_info = {};
|
||||
ps_info.scratch_size_dwords = data.scratch_size_dwords;
|
||||
|
||||
// SPI_PS_IN_CONTROL.NUM_INTERP occupies bits 5:0. Keep the remaining control
|
||||
// flags in the hardware state and extract only the input count here.
|
||||
// SPI_PS_IN_CONTROL: NUM_INTERP occupies bits 5:0 and PS_W32_EN is bit 15.
|
||||
ps_info.input_num = sh.ps_in_control & 0x3fu;
|
||||
if ((sh.ps_in_control & 0x8000u) != 0) {
|
||||
ps_info.wave_size = 32;
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(ps_info.input_num > std::size(ps_info.interpolator_settings));
|
||||
ps_info.ps_system_input_base = ShaderCalcPsSystemInputBase(sh);
|
||||
const uint32_t active_inputs = sh.ps_input_ena & sh.ps_input_addr;
|
||||
if ((active_inputs & 0x00000002u) != 0) {
|
||||
ps_info.ps_perspective_center_vgpr = (active_inputs & 0x00000001u) != 0 ? 2u : 0u;
|
||||
}
|
||||
if ((active_inputs & 0x00000004u) != 0) {
|
||||
ps_info.ps_perspective_centroid_vgpr = 2u * std::popcount(active_inputs & 0x3u);
|
||||
}
|
||||
for (uint32_t i = 0; i < data.num_input_semantics && i < ps_info.input_num && i < 32u; i++) {
|
||||
const auto& semantic = data.input_semantics[i];
|
||||
if (semantic.is_custom != 0 && semantic.is_f16 == 0) {
|
||||
@@ -701,9 +706,11 @@ void BuildStageStaticKey(const ShaderPixelInputInfo& info, std::vector<uint32_t>
|
||||
key.clear();
|
||||
key.push_back(info.scratch_size_dwords);
|
||||
key.push_back(info.input_num);
|
||||
key.push_back(info.wave_size);
|
||||
key.push_back(info.ps_system_input_base);
|
||||
key.push_back(info.custom_interpolation_mask);
|
||||
key.push_back(info.ps_perspective_center_vgpr);
|
||||
key.push_back(info.ps_perspective_centroid_vgpr);
|
||||
key.push_back(static_cast<uint32_t>(info.ps_pos_x));
|
||||
key.push_back(static_cast<uint32_t>(info.ps_pos_y));
|
||||
key.push_back(static_cast<uint32_t>(info.ps_pos_z));
|
||||
@@ -790,6 +797,7 @@ ShaderParams PrepareProgram(const HW::VertexShaderInfo& regs, const HW::Context&
|
||||
const auto& group = user_config.GetGeControl();
|
||||
if ((user_config.GetPrimType() != Prospero::PrimitiveType::kPointList &&
|
||||
user_config.GetPrimType() != Prospero::PrimitiveType::kLineList &&
|
||||
user_config.GetPrimType() != Prospero::PrimitiveType::kTriFan &&
|
||||
user_config.GetPrimType() != Prospero::PrimitiveType::kTriStrip &&
|
||||
user_config.GetPrimType() != Prospero::PrimitiveType::kTriList) ||
|
||||
sh.m_vgtGsOutPrimType != 2u || sh.m_vgtGsMaxVertOut < 3u ||
|
||||
@@ -955,6 +963,7 @@ void ShaderDbgDumpInputInfo(const ShaderPixelInputInfo& info) {
|
||||
"\t ps_system_input_base = %u\n"
|
||||
"\t custom_interpolation_mask = 0x%08" PRIx32 "\n"
|
||||
"\t ps_perspective_center_vgpr = %" PRIu32 "\n"
|
||||
"\t ps_perspective_centroid_vgpr = %" PRIu32 "\n"
|
||||
"\t ps_pos_x = %s\n"
|
||||
"\t ps_pos_y = %s\n"
|
||||
"\t ps_pos_z = %s\n"
|
||||
@@ -967,7 +976,8 @@ void ShaderDbgDumpInputInfo(const ShaderPixelInputInfo& info) {
|
||||
"\t ps_early_z = %s\n"
|
||||
"\t ps_execute_on_noop = %s\n",
|
||||
info.input_num, info.ps_system_input_base, info.custom_interpolation_mask,
|
||||
info.ps_perspective_center_vgpr, info.ps_pos_x ? "true" : "false",
|
||||
info.ps_perspective_center_vgpr, info.ps_perspective_centroid_vgpr,
|
||||
info.ps_pos_x ? "true" : "false",
|
||||
info.ps_pos_y ? "true" : "false", info.ps_pos_z ? "true" : "false",
|
||||
info.ps_pos_w ? "true" : "false", info.ps_front_face ? "true" : "false",
|
||||
info.ps_ancillary ? "true" : "false",
|
||||
|
||||
@@ -97,8 +97,11 @@ struct ShaderMeshInputInfo: ShaderWorkgroupInputInfo {
|
||||
}
|
||||
}
|
||||
[[nodiscard]] constexpr uint32_t InputPrimitiveStep() const {
|
||||
return input_primitive == static_cast<uint32_t>(Prospero::PrimitiveType::kTriStrip)
|
||||
? 1u : InputPrimitiveSize();
|
||||
switch (static_cast<Prospero::PrimitiveType>(input_primitive)) {
|
||||
case Prospero::PrimitiveType::kTriFan:
|
||||
case Prospero::PrimitiveType::kTriStrip: return 1u;
|
||||
default: return InputPrimitiveSize();
|
||||
}
|
||||
}
|
||||
[[nodiscard]] constexpr uint32_t InputPrimitiveCount(uint32_t vertices) const {
|
||||
const auto size = InputPrimitiveSize();
|
||||
@@ -153,9 +156,11 @@ struct ShaderComputeInputInfo: ShaderWorkgroupInputInfo {
|
||||
struct ShaderPixelInputInfo {
|
||||
uint32_t interpolator_settings[32] = {0};
|
||||
uint32_t input_num = 0;
|
||||
uint32_t wave_size = 64;
|
||||
uint32_t ps_system_input_base = 0;
|
||||
uint32_t custom_interpolation_mask = 0;
|
||||
uint32_t ps_perspective_center_vgpr = UINT32_MAX;
|
||||
uint32_t ps_perspective_centroid_vgpr = UINT32_MAX;
|
||||
uint8_t target_output_mode[8] = {};
|
||||
std::array<Prospero::ColorComponentMapping, 8> target_export_mapping = {};
|
||||
uint32_t scratch_size_dwords = 0;
|
||||
|
||||
@@ -19,19 +19,31 @@ uint32_t ShaderPixelParameterLocation(const ShaderPixelInputInfo& info,
|
||||
std::span<const uint32_t> active_inputs, uint32_t input) {
|
||||
std::array<bool, 32> used_locations {};
|
||||
for (const auto active_input: active_inputs) {
|
||||
auto location = ShaderPixelParameterMappedLocation(info, active_input);
|
||||
if (location < used_locations.size() && used_locations[location]) {
|
||||
location = active_input;
|
||||
while (location < used_locations.size() && used_locations[location]) {
|
||||
location++;
|
||||
used_locations[ShaderPixelParameterMappedLocation(info, active_input)] = true;
|
||||
}
|
||||
std::array<uint32_t, 64> group_locations;
|
||||
group_locations.fill(UINT32_MAX);
|
||||
for (const auto active_input: active_inputs) {
|
||||
const auto mapped = ShaderPixelParameterMappedLocation(info, active_input);
|
||||
const auto group = mapped * 2u + ShaderPixelParameterIsFlat(info, active_input);
|
||||
auto& location = group_locations[group];
|
||||
if (location == UINT32_MAX) {
|
||||
location = mapped;
|
||||
// Smooth and custom interpolation read the same vertex output. Only
|
||||
// differing flat/smooth rectangle outputs need separate locations.
|
||||
if (group_locations[group ^ 1u] != UINT32_MAX) {
|
||||
location = 0;
|
||||
while (location < used_locations.size() && used_locations[location]) {
|
||||
location++;
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(location >= used_locations.size());
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(location >= used_locations.size());
|
||||
used_locations[location] = true;
|
||||
}
|
||||
|
||||
if (active_input == input) {
|
||||
return location;
|
||||
}
|
||||
used_locations[location] = true;
|
||||
}
|
||||
return ShaderPixelParameterMappedLocation(info, input);
|
||||
}
|
||||
|
||||
@@ -3093,10 +3093,6 @@ Pthread PthreadSwapSelfForSignal(Pthread thread) {
|
||||
return previous;
|
||||
}
|
||||
|
||||
int PthreadGetUniqueId(Pthread thread) {
|
||||
return thread != nullptr ? thread->unique_id : 0;
|
||||
}
|
||||
|
||||
uint64_t PthreadGetHostThreadId(Pthread thread) {
|
||||
return thread != nullptr ? thread->host_thread_id : 0;
|
||||
}
|
||||
|
||||
@@ -110,7 +110,6 @@ int KYTY_SYSV_ABI PthreadRename(Pthread thread, const char* name);
|
||||
void KYTY_SYSV_ABI PthreadYield();
|
||||
int KYTY_SYSV_ABI PthreadGetthreadid();
|
||||
int KYTY_SYSV_ABI PthreadGetaffinity(Pthread thread, KernelCpumask* mask);
|
||||
int PthreadGetUniqueId(Pthread thread);
|
||||
uint64_t PthreadGetHostThreadId(Pthread thread);
|
||||
void PthreadWakeForSignal(Pthread thread);
|
||||
void PthreadQueuePendingSignal(Pthread thread, int signum);
|
||||
|
||||
@@ -20,7 +20,7 @@ add_executable(launcher WIN32
|
||||
)
|
||||
|
||||
|
||||
target_link_libraries(launcher Qt6::Concurrent Qt6::Network Qt6::Widgets Vulkan::Headers kyty_git_version)
|
||||
target_link_libraries(launcher Qt6::Concurrent Qt6::Network Qt6::Widgets Vulkan::Headers SDL2-static kyty_git_version)
|
||||
if(WIN32)
|
||||
target_link_libraries(launcher setupapi)
|
||||
endif()
|
||||
|
||||
@@ -75,7 +75,25 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1" rowspan="2">
|
||||
<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>
|
||||
@@ -152,7 +170,7 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<item row="3" column="0" colspan="2">
|
||||
<widget class="QGroupBox" name="debug_group">
|
||||
<property name="title">
|
||||
<string>Debugging && logs</string>
|
||||
@@ -253,21 +271,19 @@
|
||||
<property name="title">
|
||||
<string>Compatibility</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="compatibilityLayout">
|
||||
<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>
|
||||
<item>
|
||||
<spacer name="compatibilitySpacer">
|
||||
<property name="orientation"><enum>Qt::Horizontal</enum></property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size><width>40</width><height>20</height></size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -313,12 +329,4 @@
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>cancel_button</sender>
|
||||
<signal>clicked()</signal>
|
||||
<receiver>ConfigurationEditDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
@@ -87,6 +87,7 @@ public:
|
||||
Resolution screen_resolution = Resolution::R1280X720;
|
||||
QString user_name = "Kyty";
|
||||
int user_id = Config::DEFAULT_USER_ID;
|
||||
QString audio_input_device;
|
||||
PresentMode present_mode = PresentMode::Mailbox;
|
||||
int gpu_index = -1;
|
||||
bool fullscreen_enabled = false;
|
||||
@@ -104,6 +105,7 @@ public:
|
||||
QString printf_output_file = "_kyty.txt";
|
||||
bool profiler_enabled = false;
|
||||
bool renderdoc_enabled = false;
|
||||
bool amd_cpu_enabled = false;
|
||||
#if defined(_WIN32)
|
||||
bool red_zone_protection_enabled = false;
|
||||
#endif
|
||||
@@ -115,6 +117,7 @@ public:
|
||||
screen_resolution = other.screen_resolution;
|
||||
user_name = other.user_name;
|
||||
user_id = other.user_id;
|
||||
audio_input_device = other.audio_input_device;
|
||||
present_mode = other.present_mode;
|
||||
gpu_index = other.gpu_index;
|
||||
fullscreen_enabled = other.fullscreen_enabled;
|
||||
@@ -132,6 +135,7 @@ public:
|
||||
printf_output_file = other.printf_output_file;
|
||||
profiler_enabled = other.profiler_enabled;
|
||||
renderdoc_enabled = other.renderdoc_enabled;
|
||||
amd_cpu_enabled = other.amd_cpu_enabled;
|
||||
#if defined(_WIN32)
|
||||
red_zone_protection_enabled = other.red_zone_protection_enabled;
|
||||
#endif
|
||||
@@ -160,6 +164,7 @@ public:
|
||||
KYTY_CFG_SET(screen_resolution);
|
||||
KYTY_CFG_SET(user_name);
|
||||
KYTY_CFG_SET(user_id);
|
||||
KYTY_CFG_SET(audio_input_device);
|
||||
KYTY_CFG_SET(present_mode);
|
||||
KYTY_CFG_SET(gpu_index);
|
||||
KYTY_CFG_SET(fullscreen_enabled);
|
||||
@@ -177,6 +182,7 @@ public:
|
||||
KYTY_CFG_SET(printf_output_file);
|
||||
KYTY_CFG_SET(profiler_enabled);
|
||||
KYTY_CFG_SET(renderdoc_enabled);
|
||||
KYTY_CFG_SET(amd_cpu_enabled);
|
||||
#if defined(_WIN32)
|
||||
KYTY_CFG_SET(red_zone_protection_enabled);
|
||||
#endif
|
||||
@@ -196,6 +202,7 @@ public:
|
||||
user_id = user_id_ok && Config::IsConfiguredUserIdValid(saved_user_id)
|
||||
? saved_user_id
|
||||
: Config::DEFAULT_USER_ID;
|
||||
audio_input_device = s->value("audio_input_device", audio_input_device).toString();
|
||||
KYTY_CFG_GET(present_mode);
|
||||
gpu_index = s->value("gpu_index", -1).toInt();
|
||||
if (EnumToText(present_mode).isEmpty()) {
|
||||
@@ -219,6 +226,7 @@ public:
|
||||
KYTY_CFG_GET(printf_output_file);
|
||||
KYTY_CFG_GET(profiler_enabled);
|
||||
KYTY_CFG_GET(renderdoc_enabled);
|
||||
amd_cpu_enabled = s->value("amd_cpu_enabled", false).toBool();
|
||||
#if defined(_WIN32)
|
||||
red_zone_protection_enabled =
|
||||
s->value("red_zone_protection_enabled", red_zone_protection_enabled).toBool();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "common/emulatorConfig.h"
|
||||
#include "configuration.h"
|
||||
#include "mandatoryLineEdit.h"
|
||||
#include "SDL.h"
|
||||
|
||||
#include <QAbstractItemView>
|
||||
#include <QCheckBox>
|
||||
@@ -107,6 +108,7 @@ ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* p
|
||||
InitGameDirectories();
|
||||
|
||||
connect(m_ui->ok_button, &QPushButton::clicked, this, &ConfigurationEditDialog::save);
|
||||
connect(m_ui->cancel_button, &QPushButton::clicked, this, &QDialog::reject);
|
||||
connect(m_ui->clear_button, &QPushButton::clicked, this, &ConfigurationEditDialog::clear);
|
||||
connect(m_ui->comboBox_shader_log_direction, &QComboBox::currentTextChanged, this,
|
||||
[this](const QString& text) {
|
||||
@@ -167,6 +169,31 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
|
||||
m_ui->lineEdit_user_name->setMaxLength(static_cast<int>(Config::MAX_USER_NAME_LENGTH));
|
||||
m_ui->lineEdit_user_name->setText(info.user_name);
|
||||
m_ui->spinBox_user_id->setValue(info.user_id);
|
||||
auto* microphone = m_ui->comboBox_audio_input_device;
|
||||
microphone->clear();
|
||||
microphone->addItem(tr("None"), QString {});
|
||||
microphone->setToolTip(tr("Microphone used by games. None supplies silence."));
|
||||
SDL_SetMainReady();
|
||||
if (SDL_InitSubSystem(SDL_INIT_AUDIO) == 0) {
|
||||
const int device_count = SDL_GetNumAudioDevices(SDL_TRUE);
|
||||
for (int i = 0; i < device_count; i++) {
|
||||
if (const auto* device = SDL_GetAudioDeviceName(i, SDL_TRUE); device != nullptr) {
|
||||
const auto name = QString::fromUtf8(device);
|
||||
if (microphone->findData(name) < 0) {
|
||||
microphone->addItem(name, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
} else {
|
||||
microphone->setToolTip(tr("Microphones could not be listed: %1")
|
||||
.arg(QString::fromUtf8(SDL_GetError())));
|
||||
}
|
||||
if (microphone->findData(info.audio_input_device) < 0) {
|
||||
microphone->addItem(tr("%1 (unavailable)").arg(info.audio_input_device),
|
||||
info.audio_input_device);
|
||||
}
|
||||
microphone->setCurrentIndex(microphone->findData(info.audio_input_device));
|
||||
ListInit(m_ui->comboBox_screen_resolution, info.screen_resolution);
|
||||
ListInit(m_ui->comboBox_present_mode, info.present_mode);
|
||||
m_ui->comboBox_gpu->clear();
|
||||
@@ -187,7 +214,7 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
|
||||
#endif
|
||||
QVulkanInstance instance;
|
||||
instance.setApiVersion(QVersionNumber(1, 3, 0));
|
||||
#if !defined(__APPLE__)
|
||||
#if !defined(__APPLE__) && QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
|
||||
instance.setFlags(QVulkanInstance::NoPortabilityDrivers);
|
||||
#endif
|
||||
if (instance.create()) {
|
||||
@@ -213,6 +240,10 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
|
||||
m_ui->checkBox_shader_validation->setChecked(info.shader_validation_enabled);
|
||||
m_ui->checkBox_vulkan_validation->setChecked(info.vulkan_validation_enabled);
|
||||
m_ui->checkBox_renderdoc_capture->setChecked(info.renderdoc_enabled);
|
||||
m_ui->checkBox_amd_cpu->setChecked(info.amd_cpu_enabled);
|
||||
#if defined(__APPLE__)
|
||||
m_ui->checkBox_amd_cpu->setVisible(false);
|
||||
#endif
|
||||
#if defined(_WIN32)
|
||||
m_ui->checkBox_red_zone_protection->setChecked(info.red_zone_protection_enabled);
|
||||
#else
|
||||
@@ -340,6 +371,7 @@ void ConfigurationEditDialog::resizeEvent(QResizeEvent* event) {
|
||||
static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui) {
|
||||
info.user_name = ui.lineEdit_user_name->text().trimmed();
|
||||
info.user_id = ui.spinBox_user_id->value();
|
||||
info.audio_input_device = ui.comboBox_audio_input_device->currentData().toString();
|
||||
info.screen_resolution =
|
||||
TextToEnum<Configuration::Resolution>(ui.comboBox_screen_resolution->currentText());
|
||||
info.present_mode =
|
||||
@@ -352,6 +384,7 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui) {
|
||||
info.vulkan_validation_enabled = ui.checkBox_vulkan_validation->isChecked();
|
||||
info.shader_validation_enabled = ui.checkBox_shader_validation->isChecked();
|
||||
info.renderdoc_enabled = ui.checkBox_renderdoc_capture->isChecked();
|
||||
info.amd_cpu_enabled = ui.checkBox_amd_cpu->isChecked();
|
||||
#if defined(_WIN32)
|
||||
info.red_zone_protection_enabled = ui.checkBox_red_zone_protection->isChecked();
|
||||
#endif
|
||||
|
||||
@@ -46,8 +46,8 @@ constexpr PadControl PAD_CONTROLS[] = {
|
||||
{"Square", "Square", "K"},
|
||||
{"L1", "L1", "Q"},
|
||||
{"R1", "R1", "E"},
|
||||
{"L2", "L2", ""},
|
||||
{"R2", "R2", ""},
|
||||
{"L2", "L2", "Z"},
|
||||
{"R2", "R2", "C"},
|
||||
{"L3", "L3", "Left Shift"},
|
||||
{"R3", "R3", "Left Ctrl"},
|
||||
{"Options", "Options", "Return"},
|
||||
|
||||
@@ -228,6 +228,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
|
||||
args << "--screen-height" << r.at(1);
|
||||
args << "--user-name" << info.user_name;
|
||||
args << "--user-id" << QString::number(info.user_id);
|
||||
if (!info.audio_input_device.isEmpty()) {
|
||||
args << "--mic" << info.audio_input_device;
|
||||
}
|
||||
args << "--present-mode" << EnumToText(info.present_mode);
|
||||
if (info.gpu_index >= 0) {
|
||||
args << "--gpu" << QString::number(info.gpu_index);
|
||||
@@ -251,6 +254,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
|
||||
args << "--profile";
|
||||
}
|
||||
args << "--spirv-debug-printf" << "false";
|
||||
if (info.amd_cpu_enabled) {
|
||||
args << "--amd-cpu";
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
if (info.red_zone_protection_enabled) {
|
||||
args << "--redzone";
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
@@ -129,8 +130,11 @@ void PatchesDialog::Save() {
|
||||
document.setObject(root);
|
||||
|
||||
QSaveFile output(path);
|
||||
if (output.open(QIODevice::WriteOnly) && output.write(document.toJson()) >= 0 &&
|
||||
output.commit()) {
|
||||
m_status->setText(tr("Cheat selection saved."));
|
||||
if (!output.open(QIODevice::WriteOnly) || output.write(document.toJson()) < 0 ||
|
||||
!output.commit()) {
|
||||
qWarning() << "Could not save cheat file:" << output.errorString();
|
||||
m_status->setText(tr("Could not save cheat file: %1").arg(output.errorString()));
|
||||
return;
|
||||
}
|
||||
m_status->setText(tr("Cheat selection saved."));
|
||||
}
|
||||
+10
-20
@@ -262,35 +262,25 @@ struct CommandBuffer {
|
||||
reserved_dw);
|
||||
}
|
||||
|
||||
[[nodiscard]] KYTY_SYSV_ABI uint32_t GetAvailableSizeDW() const {
|
||||
if (cursor_up == nullptr || cursor_down == nullptr || cursor_down <= cursor_up) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto available = static_cast<uint64_t>(cursor_down - cursor_up);
|
||||
[[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) -
|
||||
reinterpret_cast<uintptr_t>(cursor_up));
|
||||
const auto available = static_cast<uint64_t>(distance >> 2);
|
||||
if (available <= reserved_dw) {
|
||||
return 0;
|
||||
}
|
||||
if (available - reserved_dw > UINT32_MAX) {
|
||||
LOGF_COLOR(
|
||||
Log::Color::Red,
|
||||
"\t command buffer has suspiciously large free space: cursor_up = 0x%016" PRIx64
|
||||
", cursor_down = 0x%016" PRIx64 ", reserved_dw = %" PRIu32 "\n",
|
||||
reinterpret_cast<uint64_t>(cursor_up), reinterpret_cast<uint64_t>(cursor_down),
|
||||
reserved_dw);
|
||||
return UINT32_MAX;
|
||||
}
|
||||
return static_cast<uint32_t>(available - reserved_dw);
|
||||
return available - reserved_dw;
|
||||
}
|
||||
|
||||
KYTY_SYSV_ABI bool ReserveDW(uint32_t num_dw) {
|
||||
uint32_t remaining = GetAvailableSizeDW();
|
||||
const uint64_t remaining = GetAvailableSizeDW();
|
||||
if (num_dw > remaining) {
|
||||
if (callback == nullptr) {
|
||||
LOGF_COLOR(
|
||||
Log::Color::Red,
|
||||
"\t command buffer exhausted and has no grow callback: requested = %" PRIu32
|
||||
", remaining = %" PRIu32 ", reserved_dw = %" PRIu32 "\n",
|
||||
", remaining = %" PRIu64 ", reserved_dw = %" PRIu32 "\n",
|
||||
num_dw, remaining, reserved_dw);
|
||||
DbgDump();
|
||||
return false;
|
||||
@@ -300,7 +290,7 @@ struct CommandBuffer {
|
||||
if (!result) {
|
||||
LOGF_COLOR(Log::Color::Red,
|
||||
"\t command buffer grow callback failed: requested = %" PRIu32
|
||||
", remaining = %" PRIu32 ", reserved_dw = %" PRIu32 "\n",
|
||||
", remaining = %" PRIu64 ", reserved_dw = %" PRIu32 "\n",
|
||||
num_dw, remaining, reserved_dw);
|
||||
DbgDump();
|
||||
return false;
|
||||
@@ -308,7 +298,7 @@ struct CommandBuffer {
|
||||
if (GetAvailableSizeDW() < num_dw) {
|
||||
LOGF_COLOR(Log::Color::Red,
|
||||
"\t command buffer grow callback did not provide enough space: "
|
||||
"requested = %" PRIu32 ", remaining = %" PRIu32
|
||||
"requested = %" PRIu32 ", remaining = %" PRIu64
|
||||
", reserved_dw = %" PRIu32 "\n",
|
||||
num_dw, GetAvailableSizeDW(), reserved_dw);
|
||||
DbgDump();
|
||||
|
||||
+208
-58
@@ -3,6 +3,7 @@
|
||||
#include "SDL.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/common.h"
|
||||
#include "common/emulatorConfig.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/magicEnum.h"
|
||||
#include "common/stringUtils.h"
|
||||
@@ -38,6 +39,9 @@ constexpr int AUDIO_OUT_PORT_TYPE_AUX = 127;
|
||||
|
||||
constexpr uint32_t AUDIO_OUT_PARAM_FORMAT_MASK = 0x000000ffu;
|
||||
|
||||
constexpr int AUDIO_IN_SILENT_STATE_DEVICE_NONE = 0x1;
|
||||
constexpr uint32_t AUDIO_IN_GRAIN_MAX_ASYNC = 384;
|
||||
|
||||
static bool audio_out_port_type_is_valid(int type) {
|
||||
return (type >= AUDIO_OUT_PORT_TYPE_MAIN && type <= AUDIO_OUT_PORT_TYPE_PADSPK) ||
|
||||
type == AUDIO_OUT_PORT_TYPE_VIBRATION || type == AUDIO_OUT_PORT_TYPE_AUDIO3D ||
|
||||
@@ -89,9 +93,10 @@ public:
|
||||
uint32_t AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking = true);
|
||||
bool AudioOutGetStatus(Id handle, int* type, int* channels_num);
|
||||
|
||||
Id AudioInOpen(uint32_t type, uint32_t samples_num, uint32_t freq, Format format);
|
||||
bool AudioInValid(Id handle);
|
||||
uint32_t AudioInInput(Id handle, void* dest);
|
||||
Id AudioInOpen(uint32_t samples_num, uint32_t freq, Format format, bool asynchronous);
|
||||
int AudioInClose(Id handle);
|
||||
int AudioInGetSilentState(Id handle);
|
||||
int AudioInInput(Id handle, void* dest);
|
||||
|
||||
static constexpr int OUT_PORTS_MAX = 32;
|
||||
static constexpr int IN_PORTS_MAX = 8;
|
||||
@@ -112,14 +117,18 @@ private:
|
||||
};
|
||||
|
||||
struct PortIn {
|
||||
bool used = false;
|
||||
uint32_t type = 0;
|
||||
uint32_t samples_num = 0;
|
||||
uint32_t freq = 0;
|
||||
Format format = Format::Unknown;
|
||||
uint64_t last_input_time = 0;
|
||||
bool used = false;
|
||||
bool busy = false;
|
||||
bool asynchronous = false;
|
||||
uint32_t samples_num = 0;
|
||||
uint32_t freq = 0;
|
||||
uint32_t bytes_per_frame = 0;
|
||||
uint64_t last_input_time = 0;
|
||||
SDL_AudioDeviceID audio_device = 0;
|
||||
};
|
||||
|
||||
PortIn* GetAudioInPort(Id handle); // Caller holds m_mutex.
|
||||
|
||||
Common::Mutex m_mutex;
|
||||
PortOut m_out_ports[OUT_PORTS_MAX];
|
||||
PortIn m_in_ports[IN_PORTS_MAX];
|
||||
@@ -131,6 +140,8 @@ private:
|
||||
static SDL_AudioFormat SdlFormat(Format format);
|
||||
static bool OpenSdlDevice(PortOut* port);
|
||||
static void CloseSdlDevice(PortOut* port);
|
||||
static void OpenSdlDevice(PortIn* port, Format format);
|
||||
static void CloseSdlDevice(PortIn* port);
|
||||
static const void* PrepareOutputBuffer(const PortOut& port, const void* data,
|
||||
std::vector<uint8_t>* buffer);
|
||||
static bool QueueSdlAudio(PortOut* port, const void* data, bool blocking);
|
||||
@@ -198,6 +209,9 @@ Audio::~Audio() {
|
||||
for (auto& port: m_out_ports) {
|
||||
CloseSdlDevice(&port);
|
||||
}
|
||||
for (auto& port: m_in_ports) {
|
||||
CloseSdlDevice(&port);
|
||||
}
|
||||
}
|
||||
|
||||
bool Audio::FormatIsFloat(Format format) {
|
||||
@@ -566,24 +580,55 @@ uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking
|
||||
return first_port.samples_num;
|
||||
}
|
||||
|
||||
Audio::Id Audio::AudioInOpen(uint32_t type, uint32_t samples_num, uint32_t freq, Format format) {
|
||||
void Audio::OpenSdlDevice(PortIn* port, Format format) {
|
||||
const auto& name = Config::GetAudioInputDevice();
|
||||
if (name.empty()) {
|
||||
return;
|
||||
}
|
||||
if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
|
||||
LOGF("AudioIn: SDL init failed: %s\n", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
SDL_AudioSpec desired {};
|
||||
desired.freq = static_cast<int>(port->freq);
|
||||
desired.format = SdlFormat(format);
|
||||
desired.channels = static_cast<Uint8>(port->bytes_per_frame / BytesPerSample(format));
|
||||
desired.samples = static_cast<Uint16>(port->samples_num);
|
||||
// SDL converts capture data to the guest format when allowed_changes is zero.
|
||||
port->audio_device = SDL_OpenAudioDevice(name.c_str(), 1, &desired, nullptr, 0);
|
||||
if (port->audio_device == 0) {
|
||||
LOGF("AudioIn: cannot open '%s': %s; using silence\n", name.c_str(), SDL_GetError());
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
return;
|
||||
}
|
||||
LOGF("AudioIn: opened '%s' (%u Hz, %u ch)\n", name.c_str(), port->freq, desired.channels);
|
||||
}
|
||||
|
||||
void Audio::CloseSdlDevice(PortIn* port) {
|
||||
if (port->audio_device != 0) {
|
||||
SDL_CloseAudioDevice(port->audio_device);
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
port->audio_device = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Audio::Id Audio::AudioInOpen(uint32_t samples_num, uint32_t freq, Format format, bool asynchronous) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
|
||||
for (int id = 0; id < IN_PORTS_MAX; id++) {
|
||||
if (!m_in_ports[id].used) {
|
||||
auto& port = m_in_ports[id];
|
||||
|
||||
port.used = true;
|
||||
port.type = type;
|
||||
port.samples_num = samples_num;
|
||||
port.freq = freq;
|
||||
port.format = format;
|
||||
|
||||
switch (format) {
|
||||
case Format::Signed16bitMono:
|
||||
case Format::Signed16bitStereo: break;
|
||||
default: EXIT("unknown format");
|
||||
port = {};
|
||||
port.used = true;
|
||||
port.asynchronous = asynchronous;
|
||||
port.samples_num = samples_num;
|
||||
port.freq = freq;
|
||||
port.bytes_per_frame = BytesPerSample(format);
|
||||
if (format == Format::Signed16bitStereo || format == Format::FloatStereo) {
|
||||
port.bytes_per_frame *= 2;
|
||||
}
|
||||
OpenSdlDevice(&port, format);
|
||||
|
||||
return Id::Create(id);
|
||||
}
|
||||
@@ -592,31 +637,111 @@ Audio::Id Audio::AudioInOpen(uint32_t type, uint32_t samples_num, uint32_t freq,
|
||||
return Id::Invalid();
|
||||
}
|
||||
|
||||
bool Audio::AudioInValid(Id handle) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
|
||||
return (handle.GetId() >= 0 && handle.GetId() < IN_PORTS_MAX &&
|
||||
m_in_ports[handle.GetId()].used);
|
||||
Audio::PortIn* Audio::GetAudioInPort(Id handle) {
|
||||
if (handle.GetId() < 0 || handle.GetId() >= IN_PORTS_MAX ||
|
||||
!m_in_ports[handle.GetId()].used) {
|
||||
return nullptr;
|
||||
}
|
||||
return &m_in_ports[handle.GetId()];
|
||||
}
|
||||
|
||||
uint32_t Audio::AudioInInput(Id handle, void* dest) {
|
||||
EXIT_NOT_IMPLEMENTED(!AudioInValid(handle));
|
||||
EXIT_NOT_IMPLEMENTED(dest == nullptr);
|
||||
int Audio::AudioInClose(Id handle) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
auto* port = GetAudioInPort(handle);
|
||||
if (port == nullptr) {
|
||||
return AUDIO_IN_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
if (port->busy) {
|
||||
return AUDIO_IN_ERROR_BUSY;
|
||||
}
|
||||
CloseSdlDevice(port);
|
||||
*port = {};
|
||||
return OK;
|
||||
}
|
||||
|
||||
const auto& port = m_in_ports[handle.GetId()];
|
||||
int Audio::AudioInGetSilentState(Id handle) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
auto* port = GetAudioInPort(handle);
|
||||
if (port == nullptr) {
|
||||
return AUDIO_IN_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
if (port->audio_device == 0 ||
|
||||
SDL_GetAudioDeviceStatus(port->audio_device) == SDL_AUDIO_STOPPED) {
|
||||
return AUDIO_IN_SILENT_STATE_DEVICE_NONE;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint64_t block_time = (1000000 * port.samples_num) / port.freq;
|
||||
uint64_t current_time = LibKernel::KernelGetProcessTime();
|
||||
int Audio::AudioInInput(Id handle, void* dest) {
|
||||
PortIn snapshot;
|
||||
{
|
||||
Common::LockGuard lock(m_mutex);
|
||||
auto* port = GetAudioInPort(handle);
|
||||
if (port == nullptr) {
|
||||
return AUDIO_IN_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
if (port->busy) {
|
||||
return AUDIO_IN_ERROR_BUSY;
|
||||
}
|
||||
port->busy = true;
|
||||
snapshot = *port;
|
||||
}
|
||||
if (snapshot.audio_device != 0 && dest != nullptr) {
|
||||
SDL_PauseAudioDevice(snapshot.audio_device, 0);
|
||||
}
|
||||
|
||||
uint64_t next_time = m_in_ports[handle.GetId()].last_input_time + block_time;
|
||||
uint64_t wait_time = (next_time > current_time ? next_time - current_time : 0);
|
||||
|
||||
// TODO(): Audio input is not yet implemented, so simulate audio delay
|
||||
const uint64_t block_time = (1000000ULL * snapshot.samples_num) / snapshot.freq;
|
||||
uint64_t wait_time = 0;
|
||||
if (snapshot.asynchronous) {
|
||||
if (dest != nullptr) {
|
||||
wait_time = block_time;
|
||||
}
|
||||
} else if (snapshot.last_input_time != 0 && (snapshot.audio_device == 0 || dest == nullptr)) {
|
||||
const auto now = LibKernel::KernelGetProcessTime();
|
||||
const auto next = snapshot.last_input_time + block_time;
|
||||
if (next > now) {
|
||||
wait_time = next - now;
|
||||
}
|
||||
}
|
||||
Common::Thread::SleepMicro(wait_time);
|
||||
|
||||
m_in_ports[handle.GetId()].last_input_time = LibKernel::KernelGetProcessTime();
|
||||
|
||||
return port.samples_num;
|
||||
uint32_t frames = snapshot.samples_num;
|
||||
bool failed = false;
|
||||
if (dest != nullptr) {
|
||||
if (snapshot.audio_device != 0) {
|
||||
while (!snapshot.asynchronous &&
|
||||
SDL_GetQueuedAudioSize(snapshot.audio_device) <
|
||||
frames * snapshot.bytes_per_frame &&
|
||||
SDL_GetAudioDeviceStatus(snapshot.audio_device) == SDL_AUDIO_PLAYING) {
|
||||
Common::Thread::SleepMicro(1000);
|
||||
}
|
||||
failed = SDL_GetAudioDeviceStatus(snapshot.audio_device) != SDL_AUDIO_PLAYING;
|
||||
if (!failed) {
|
||||
if (snapshot.asynchronous) {
|
||||
frames = AUDIO_IN_GRAIN_MAX_ASYNC;
|
||||
}
|
||||
frames = SDL_DequeueAudio(snapshot.audio_device, dest,
|
||||
frames * snapshot.bytes_per_frame) /
|
||||
snapshot.bytes_per_frame;
|
||||
}
|
||||
}
|
||||
if (snapshot.audio_device == 0 || failed) {
|
||||
std::memset(dest, 0, frames * snapshot.bytes_per_frame);
|
||||
}
|
||||
} else if (snapshot.audio_device != 0) {
|
||||
SDL_PauseAudioDevice(snapshot.audio_device, 1);
|
||||
SDL_ClearQueuedAudio(snapshot.audio_device);
|
||||
}
|
||||
{
|
||||
Common::LockGuard lock(m_mutex);
|
||||
auto& port = m_in_ports[handle.GetId()];
|
||||
if (failed) {
|
||||
LOGF("AudioIn: capture stopped; using silence\n");
|
||||
CloseSdlDevice(&port);
|
||||
}
|
||||
port.last_input_time = dest != nullptr ? LibKernel::KernelGetProcessTime() : 0;
|
||||
port.busy = false;
|
||||
}
|
||||
return dest != nullptr ? static_cast<int>(frames) : 0;
|
||||
}
|
||||
|
||||
namespace AudioOut {
|
||||
@@ -808,27 +933,36 @@ namespace AudioIn {
|
||||
|
||||
LIB_NAME("AudioIn", "AudioIn");
|
||||
|
||||
constexpr int AUDIO_IN_SILENT_STATE_DEVICE_NONE = 0x1;
|
||||
|
||||
int KYTY_SYSV_ABI AudioInOpen(int user_id, uint32_t type, uint32_t index, uint32_t len,
|
||||
uint32_t freq, uint32_t param) {
|
||||
PRINT_NAME();
|
||||
|
||||
static int OpenPort(int user_id, int type, int index, uint32_t len, uint32_t freq,
|
||||
uint32_t param, bool asynchronous) {
|
||||
LOGF("\t user_id = %d\n"
|
||||
"\t type = %u\n"
|
||||
"\t type = %d\n"
|
||||
"\t index = %d\n"
|
||||
"\t len = %u\n"
|
||||
"\t freq = %u\n",
|
||||
user_id, type, index, len, freq);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(type != 1);
|
||||
EXIT_NOT_IMPLEMENTED(index != 0);
|
||||
if (type != 0 && type != 1) {
|
||||
return AUDIO_IN_ERROR_INVALID_TYPE;
|
||||
}
|
||||
if (index != 0) {
|
||||
return AUDIO_IN_ERROR_INVALID_PARAM;
|
||||
}
|
||||
if (len != 128 && (asynchronous || len != 256)) {
|
||||
return AUDIO_IN_ERROR_INVALID_SIZE;
|
||||
}
|
||||
if (freq != 48000 && (asynchronous || freq != 16000)) {
|
||||
return AUDIO_IN_ERROR_INVALID_FREQ;
|
||||
}
|
||||
|
||||
Audio::Format format = Audio::Format::Unknown;
|
||||
|
||||
switch (param) {
|
||||
case 1: format = Audio::Format::Signed16bitMono; break;
|
||||
case 2: format = Audio::Format::Signed16bitStereo; break;
|
||||
case 0x10:
|
||||
case 0x11: format = Audio::Format::FloatMono; break;
|
||||
case 0x12: format = Audio::Format::FloatStereo; break;
|
||||
default: return AUDIO_IN_ERROR_INVALID_PARAM;
|
||||
}
|
||||
|
||||
@@ -836,7 +970,7 @@ int KYTY_SYSV_ABI AudioInOpen(int user_id, uint32_t type, uint32_t index, uint32
|
||||
|
||||
EXIT_IF(g_audio == nullptr);
|
||||
|
||||
auto id = g_audio->AudioInOpen(type, len, freq, format);
|
||||
auto id = g_audio->AudioInOpen(len, freq, format, asynchronous);
|
||||
|
||||
if (!id.IsValid()) {
|
||||
return AUDIO_IN_ERROR_PORT_FULL;
|
||||
@@ -845,18 +979,35 @@ int KYTY_SYSV_ABI AudioInOpen(int user_id, uint32_t type, uint32_t index, uint32
|
||||
return id.ToInt();
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI AudioInOpen(int user_id, int type, int index, uint32_t len,
|
||||
uint32_t freq, uint32_t param) {
|
||||
PRINT_NAME();
|
||||
return OpenPort(user_id, type, index, len, freq, param, false);
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI AudioInHqOpen(int user_id, int type, int index, uint32_t len,
|
||||
uint32_t freq, uint32_t param) {
|
||||
PRINT_NAME();
|
||||
return OpenPort(user_id, type, index, len, freq, param, true);
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI AudioInClose(int handle) {
|
||||
PRINT_NAME();
|
||||
EXIT_IF(g_audio == nullptr);
|
||||
if (handle <= 0) {
|
||||
return AUDIO_IN_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
return g_audio->AudioInClose(Audio::Id(handle));
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI AudioInInput(int handle, void* dest) {
|
||||
PRINT_NAME();
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(dest == nullptr);
|
||||
|
||||
EXIT_IF(g_audio == nullptr);
|
||||
|
||||
if (!g_audio->AudioInValid(Audio::Id(handle))) {
|
||||
if (handle <= 0) {
|
||||
return AUDIO_IN_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
|
||||
return static_cast<int>(g_audio->AudioInInput(Audio::Id(handle), dest));
|
||||
return g_audio->AudioInInput(Audio::Id(handle), dest);
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI AudioInGetSilentState(int handle) {
|
||||
@@ -864,12 +1015,11 @@ int KYTY_SYSV_ABI AudioInGetSilentState(int handle) {
|
||||
|
||||
EXIT_IF(g_audio == nullptr);
|
||||
|
||||
if (!g_audio->AudioInValid(Audio::Id(handle))) {
|
||||
if (handle <= 0) {
|
||||
return AUDIO_IN_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
|
||||
// Audio input has no device backend yet, so every valid port receives silence.
|
||||
return AUDIO_IN_SILENT_STATE_DEVICE_NONE;
|
||||
return g_audio->AudioInGetSilentState(Audio::Id(handle));
|
||||
}
|
||||
|
||||
} // namespace AudioIn
|
||||
|
||||
+4
-1
@@ -96,8 +96,11 @@ int KYTY_SYSV_ABI AudioOut2MasteringTerm();
|
||||
|
||||
namespace AudioIn {
|
||||
|
||||
int KYTY_SYSV_ABI AudioInOpen(int user_id, uint32_t type, uint32_t index, uint32_t len,
|
||||
int KYTY_SYSV_ABI AudioInOpen(int user_id, int type, int index, uint32_t len,
|
||||
uint32_t freq, uint32_t param);
|
||||
int KYTY_SYSV_ABI AudioInHqOpen(int user_id, int type, int index, uint32_t len,
|
||||
uint32_t freq, uint32_t param);
|
||||
int KYTY_SYSV_ABI AudioInClose(int handle);
|
||||
int KYTY_SYSV_ABI AudioInInput(int handle, void* dest);
|
||||
int KYTY_SYSV_ABI AudioInGetSilentState(int handle);
|
||||
|
||||
|
||||
+12
-2
@@ -384,6 +384,16 @@ static bool codec_is_supported_for_source(AvPlayerSourceType source_type, AVMedi
|
||||
}
|
||||
}
|
||||
|
||||
static const AVCodec* find_decoder(AVCodecID codec_id) {
|
||||
const auto* decoder = avcodec_find_decoder(codec_id);
|
||||
if (decoder == nullptr && codec_id == AV_CODEC_ID_VP9) {
|
||||
Log::WriteToConsoleAndLog(
|
||||
"WARNING: AvPlayer: The game requested VP9 video, but FFmpeg has no VP9 "
|
||||
"decoder. Use FFmpeg with VP9 decoding enabled.\n");
|
||||
}
|
||||
return decoder;
|
||||
}
|
||||
|
||||
struct PacketDeleter {
|
||||
void operator()(AVPacket* packet) const { av_packet_free(&packet); }
|
||||
};
|
||||
@@ -1064,7 +1074,7 @@ private:
|
||||
static_cast<int>(s->codecpar->codec_id), static_cast<uint32_t>(source_type));
|
||||
return false;
|
||||
}
|
||||
if (avcodec_find_decoder(s->codecpar->codec_id) == nullptr) {
|
||||
if (find_decoder(s->codecpar->codec_id) == nullptr) {
|
||||
LOGF("\t decoder not found: stream=%d codec=%d\n", id,
|
||||
static_cast<int>(s->codecpar->codec_id));
|
||||
return false;
|
||||
@@ -1073,7 +1083,7 @@ private:
|
||||
}
|
||||
bool OpenCodec(int id, AVCodecContext** out) {
|
||||
auto* s = fmt->streams[id];
|
||||
auto* dec = avcodec_find_decoder(s->codecpar->codec_id);
|
||||
auto* dec = find_decoder(s->codecpar->codec_id);
|
||||
if (dec == nullptr) {
|
||||
LOGF("\t avcodec_find_decoder failed: stream=%d codec=%d\n", id,
|
||||
static_cast<int>(s->codecpar->codec_id));
|
||||
|
||||
+112
-2
@@ -13,6 +13,8 @@
|
||||
#include "libs/padData.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
@@ -87,6 +89,9 @@ struct ControllerState {
|
||||
uint32_t buttons = 0;
|
||||
int axes[static_cast<int>(Axis::AxisMax)] = {128, 128, 128, 128, 0, 0};
|
||||
Touch touch[2];
|
||||
std::array<float, 3> accel {0.0f, 1.0f, 0.0f};
|
||||
std::array<float, 3> gyro {};
|
||||
std::array<float, 4> orientation {0.0f, 0.0f, 0.0f, 1.0f};
|
||||
};
|
||||
|
||||
class GameController {
|
||||
@@ -101,6 +106,9 @@ public:
|
||||
void Axis(int id, Axis axis, int value);
|
||||
void RightStick(int id, int x, int y);
|
||||
void TouchPad(int id, int finger, bool down, float x, float y);
|
||||
void Motion(int id, Sensor sensor, const float* data, uint64_t time_us);
|
||||
void SetMotionSensorState(bool enable);
|
||||
void ResetOrientation();
|
||||
void ResetInputState();
|
||||
void ReleaseHostPads();
|
||||
void GetConnectionInfo(bool* flag, int* count);
|
||||
@@ -121,6 +129,8 @@ private:
|
||||
int m_active_id = -1;
|
||||
bool m_connected = false;
|
||||
int m_connected_count = 0;
|
||||
bool m_motion_enabled = true;
|
||||
uint64_t m_gyro_time = 0;
|
||||
ControllerState m_state;
|
||||
ControllerState m_states[STATES_MAX];
|
||||
bool m_obtained[STATES_MAX] {};
|
||||
@@ -144,7 +154,16 @@ static void pad_fill_data(PadData* data, const ControllerState& state, bool conn
|
||||
data->right_stick_y = state.axes[static_cast<int>(Axis::RightY)];
|
||||
data->analog_buttons_l2 = state.axes[static_cast<int>(Axis::TriggerLeft)];
|
||||
data->analog_buttons_r2 = state.axes[static_cast<int>(Axis::TriggerRight)];
|
||||
data->orientation_w = 1.0f;
|
||||
data->acceleration_x = state.accel[0];
|
||||
data->acceleration_y = state.accel[1];
|
||||
data->acceleration_z = state.accel[2];
|
||||
data->angular_velocity_x = state.gyro[0];
|
||||
data->angular_velocity_y = state.gyro[1];
|
||||
data->angular_velocity_z = state.gyro[2];
|
||||
data->orientation_x = state.orientation[0];
|
||||
data->orientation_y = state.orientation[1];
|
||||
data->orientation_z = state.orientation[2];
|
||||
data->orientation_w = state.orientation[3];
|
||||
for (const auto& touch: state.touch) {
|
||||
if (touch.down) {
|
||||
auto& output = data->touch_data.touch[data->touch_data.touch_num++];
|
||||
@@ -273,6 +292,18 @@ void GameController::Connect(int id) {
|
||||
|
||||
m_connected_ids.push_back(id);
|
||||
|
||||
if (id != HOST_INPUT_CONTROLLER_ID) {
|
||||
if (auto* pad = SDL_GameControllerFromInstanceID(static_cast<SDL_JoystickID>(id));
|
||||
pad != nullptr) {
|
||||
for (auto sensor: {SDL_SENSOR_ACCEL, SDL_SENSOR_GYRO}) {
|
||||
if (SDL_GameControllerHasSensor(pad, sensor) &&
|
||||
SDL_GameControllerSetSensorEnabled(pad, sensor, SDL_TRUE) != 0) {
|
||||
LOGF("\t enabling controller sensor failed: %s\n", SDL_GetError());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CheckActive();
|
||||
}
|
||||
|
||||
@@ -311,6 +342,7 @@ void GameController::CheckActive() {
|
||||
m_active_id = new_active_id;
|
||||
m_connected = new_connected;
|
||||
m_state = {};
|
||||
m_gyro_time = 0;
|
||||
m_states_num = 0;
|
||||
m_first_state = 0;
|
||||
m_next_touch_id = 1;
|
||||
@@ -402,9 +434,81 @@ void GameController::TouchPad(int id, int finger, bool down, float x, float y) {
|
||||
}
|
||||
}
|
||||
|
||||
void GameController::Motion(int id, Sensor sensor, const float* data, uint64_t time_us) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
if (id != m_active_id || !m_motion_enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert acceleration to G; angular velocity is already in rad/s.
|
||||
if (sensor == Sensor::Accel) {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
m_state.accel[i] = data[i] / SDL_STANDARD_GRAVITY;
|
||||
}
|
||||
} else {
|
||||
std::copy_n(data, 3, m_state.gyro.begin());
|
||||
// Do not extrapolate a single sample across lost reports (e.g. loss of window focus).
|
||||
constexpr uint64_t max_gyro_interval_us = 100000;
|
||||
if (m_gyro_time != 0 && time_us > m_gyro_time &&
|
||||
time_us - m_gyro_time <= max_gyro_interval_us) {
|
||||
const float dt = static_cast<float>(time_us - m_gyro_time) * 0.000001f;
|
||||
const float speed =
|
||||
std::sqrt(data[0] * data[0] + data[1] * data[1] + data[2] * data[2]);
|
||||
const float half_angle = speed * dt * 0.5f;
|
||||
const float scale = speed > 0.0f ? std::sin(half_angle) / speed : 0.0f;
|
||||
const float x = data[0] * scale;
|
||||
const float y = data[1] * scale;
|
||||
const float z = data[2] * scale;
|
||||
const float w = std::cos(half_angle);
|
||||
const auto q = m_state.orientation;
|
||||
// Accumulate body-local rotation relative to connection / orientation reset.
|
||||
m_state.orientation = {q[3] * x + q[0] * w + q[1] * z - q[2] * y,
|
||||
q[3] * y - q[0] * z + q[1] * w + q[2] * x,
|
||||
q[3] * z + q[0] * y - q[1] * x + q[2] * w,
|
||||
q[3] * w - q[0] * x - q[1] * y - q[2] * z};
|
||||
float length = 0.0f;
|
||||
for (float value: m_state.orientation) {
|
||||
length += value * value;
|
||||
}
|
||||
length = std::sqrt(length);
|
||||
for (float& value: m_state.orientation) {
|
||||
value /= length;
|
||||
}
|
||||
}
|
||||
m_gyro_time = time_us;
|
||||
}
|
||||
m_state.time = LibKernel::KernelGetProcessTime();
|
||||
AddState();
|
||||
}
|
||||
|
||||
void GameController::SetMotionSensorState(bool enable) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
if (m_motion_enabled != enable) {
|
||||
m_motion_enabled = enable;
|
||||
m_gyro_time = 0;
|
||||
m_state.accel = {0.0f, 1.0f, 0.0f};
|
||||
m_state.gyro = {};
|
||||
m_state.time = LibKernel::KernelGetProcessTime();
|
||||
AddState();
|
||||
}
|
||||
}
|
||||
|
||||
void GameController::ResetOrientation() {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
m_state.orientation = {0.0f, 0.0f, 0.0f, 1.0f};
|
||||
m_gyro_time = 0;
|
||||
m_state.time = LibKernel::KernelGetProcessTime();
|
||||
AddState();
|
||||
}
|
||||
|
||||
void GameController::ResetInputState() {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
m_state = {};
|
||||
m_state.buttons = 0;
|
||||
std::fill_n(m_state.axes, 4, 128);
|
||||
std::fill_n(m_state.axes + 4, 2, 0);
|
||||
for (auto& touch: m_state.touch) {
|
||||
touch = {};
|
||||
}
|
||||
m_state.time = LibKernel::KernelGetProcessTime();
|
||||
m_states_num = 0;
|
||||
m_first_state = 0;
|
||||
@@ -580,6 +684,10 @@ void SetTouchPad(int id, int finger, bool down, float x, float y) {
|
||||
g_controller->TouchPad(id, finger, down, x, y);
|
||||
}
|
||||
|
||||
void SetSensor(int id, Sensor sensor, const float* data, uint64_t time_us) {
|
||||
g_controller->Motion(id, sensor, data, time_us);
|
||||
}
|
||||
|
||||
void ResetInputState() {
|
||||
g_controller->ResetInputState();
|
||||
}
|
||||
@@ -646,6 +754,7 @@ int KYTY_SYSV_ABI PadSetMotionSensorState(int handle, bool enable) {
|
||||
}
|
||||
|
||||
LOGF("\t enable = %s\n", (enable ? "true" : "false"));
|
||||
g_controller->SetMotionSensorState(enable);
|
||||
|
||||
return OK;
|
||||
}
|
||||
@@ -669,6 +778,7 @@ int KYTY_SYSV_ABI PadResetOrientation(int handle) {
|
||||
return PAD_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
|
||||
g_controller->ResetOrientation();
|
||||
return OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ enum class Axis {
|
||||
AxisMax
|
||||
};
|
||||
|
||||
enum class Sensor { Accel, Gyro };
|
||||
|
||||
struct PadControllerInformation;
|
||||
struct PadData;
|
||||
struct PadVibrationParam;
|
||||
@@ -69,6 +71,7 @@ void SetButton(int id, uint32_t button, bool down);
|
||||
void SetAxis(int id, Axis axis, int value);
|
||||
void SetRightStick(int id, int x, int y);
|
||||
void SetTouchPad(int id, int finger, bool down, float x, float y);
|
||||
void SetSensor(int id, Sensor sensor, const float* data, uint64_t time_us);
|
||||
void ResetInputState();
|
||||
|
||||
int KYTY_SYSV_ABI PadInit();
|
||||
|
||||
@@ -527,7 +527,12 @@ static int kyty_printf_internal(bool sn, char* sn_s, size_t sn_n, const char* fo
|
||||
} else if (*format == '*') {
|
||||
// const int prec = (int)va_arg(va, int);
|
||||
const int prec = VaArg_int(va_list);
|
||||
precision = prec > 0 ? static_cast<unsigned int>(prec) : 0U;
|
||||
if (prec < 0) {
|
||||
// A negative dynamic precision is treated as omitted.
|
||||
flags &= ~FLAGS_PRECISION;
|
||||
} else {
|
||||
precision = static_cast<unsigned int>(prec);
|
||||
}
|
||||
format++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +179,8 @@ namespace AudioIn = Audio::AudioIn;
|
||||
|
||||
LIB_DEFINE(InitAudio_1_AudioIn) {
|
||||
LIB_FUNC("5NE8Sjc7VC8", AudioIn::AudioInOpen);
|
||||
LIB_FUNC("nya-R5gDYhM", AudioIn::AudioInHqOpen);
|
||||
LIB_FUNC("Jh6WbHhnI68", AudioIn::AudioInClose);
|
||||
LIB_FUNC("LozEOU8+anM", AudioIn::AudioInInput);
|
||||
LIB_FUNC("BohEAQ7DlUE", AudioIn::AudioInGetSilentState);
|
||||
}
|
||||
|
||||
+11
-1
@@ -38,7 +38,12 @@
|
||||
#endif
|
||||
|
||||
extern "C" {
|
||||
#include <libavutil/version.h>
|
||||
#if LIBAVUTIL_VERSION_MAJOR < 61
|
||||
extern const uint8_t avpriv_vga16_font[4096];
|
||||
#else
|
||||
const uint8_t* avpriv_vga16_font_get(void);
|
||||
#endif
|
||||
}
|
||||
|
||||
namespace Libs {
|
||||
@@ -950,10 +955,15 @@ static void init_image(std::array<uint8_t, FONT_BITMAP_MAX_DIM * FONT_BITMAP_MAX
|
||||
|
||||
const auto width = static_cast<uint32_t>(scaled_font_width(font));
|
||||
const auto height = static_cast<uint32_t>(scaled_font_height(font));
|
||||
#if LIBAVUTIL_VERSION_MAJOR < 61
|
||||
const auto* vga_font = avpriv_vga16_font;
|
||||
#else
|
||||
const auto* vga_font = avpriv_vga16_font_get();
|
||||
#endif
|
||||
|
||||
for (uint32_t y = 0; y < height; y++) {
|
||||
const uint32_t src_y = std::min<uint32_t>((y * 16u) / height, 15u);
|
||||
const uint8_t row = avpriv_vga16_font[code * 16 + src_y];
|
||||
const uint8_t row = vga_font[code * 16 + src_y];
|
||||
for (uint32_t x = 0; x < width; x++) {
|
||||
const uint32_t src_x = std::min<uint32_t>((x * 8u) / width, 7u);
|
||||
if ((row & (0x80u >> src_x)) != 0) {
|
||||
|
||||
+61
-1
@@ -41,6 +41,10 @@ struct JsonArray {
|
||||
std::vector<JsonValue*>* impl;
|
||||
};
|
||||
|
||||
struct JsonArrayIterator {
|
||||
JsonValue* const* position;
|
||||
};
|
||||
|
||||
struct JsonObject {
|
||||
std::map<std::string, JsonValue*>* impl;
|
||||
};
|
||||
@@ -62,6 +66,7 @@ struct JsonValue {
|
||||
};
|
||||
|
||||
static_assert(sizeof(JsonArray) == 8);
|
||||
static_assert(sizeof(JsonArrayIterator) == 8);
|
||||
static_assert(sizeof(JsonValue) == 32 && offsetof(JsonValue, type) == 28);
|
||||
|
||||
struct JsonInitParameter2 {
|
||||
@@ -751,7 +756,11 @@ static const int64_t* KYTY_SYSV_ABI JsonValueGetInteger(const JsonValue* self) {
|
||||
PRINT_NAME();
|
||||
|
||||
static const int64_t zero = 0;
|
||||
return (self != nullptr && self->type == JsonValueTypeInteger ? &self->integer : &zero);
|
||||
if (self != nullptr &&
|
||||
(self->type == JsonValueTypeInteger || self->type == JsonValueTypeUInteger)) {
|
||||
return &self->integer;
|
||||
}
|
||||
return &zero;
|
||||
}
|
||||
|
||||
static const double* KYTY_SYSV_ABI JsonValueGetReal(const JsonValue* self) {
|
||||
@@ -906,6 +915,51 @@ static size_t KYTY_SYSV_ABI JsonArraySize(const JsonArray* self) {
|
||||
return JsonArrayImpl(self)->size();
|
||||
}
|
||||
|
||||
// The non-trivial guest iterator is returned through an explicit result pointer.
|
||||
static JsonArrayIterator* KYTY_SYSV_ABI JsonArrayBegin(JsonArrayIterator* result,
|
||||
const JsonArray* self) {
|
||||
PRINT_NAME();
|
||||
|
||||
result->position = JsonArrayImpl(self)->data();
|
||||
return result;
|
||||
}
|
||||
|
||||
static JsonArrayIterator* KYTY_SYSV_ABI JsonArrayEnd(JsonArrayIterator* result,
|
||||
const JsonArray* self) {
|
||||
PRINT_NAME();
|
||||
|
||||
const auto* impl = JsonArrayImpl(self);
|
||||
result->position = impl->data();
|
||||
if (!impl->empty()) {
|
||||
result->position += impl->size();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool KYTY_SYSV_ABI JsonArrayIteratorNotEqual(const JsonArrayIterator* self,
|
||||
const JsonArrayIterator* other) {
|
||||
PRINT_NAME();
|
||||
|
||||
return self->position != other->position;
|
||||
}
|
||||
|
||||
static JsonValue* KYTY_SYSV_ABI JsonArrayIteratorDereference(const JsonArrayIterator* self) {
|
||||
PRINT_NAME();
|
||||
|
||||
return *self->position;
|
||||
}
|
||||
|
||||
static JsonArrayIterator* KYTY_SYSV_ABI JsonArrayIteratorIncrement(JsonArrayIterator* self) {
|
||||
PRINT_NAME();
|
||||
|
||||
++self->position;
|
||||
return self;
|
||||
}
|
||||
|
||||
static void KYTY_SYSV_ABI JsonArrayIteratorDtor(JsonArrayIterator*) {
|
||||
PRINT_NAME();
|
||||
}
|
||||
|
||||
static JsonObject* KYTY_SYSV_ABI JsonObjectCtor(JsonObject* self) {
|
||||
PRINT_NAME();
|
||||
|
||||
@@ -1080,6 +1134,12 @@ LIB_DEFINE(InitNet_1_Json2) {
|
||||
LIB_FUNC("3qrge7L-AU4", LibJson2::JsonValueGetReal);
|
||||
LIB_FUNC("sOmU4vnx3s0", LibJson2::JsonValueDoubleCtor);
|
||||
LIB_FUNC("rQGJeNjOuUk", LibJson2::JsonArraySize);
|
||||
LIB_FUNC("bcH5EnFE2xY", LibJson2::JsonArrayBegin);
|
||||
LIB_FUNC("WXF2ihRF+B8", LibJson2::JsonArrayEnd);
|
||||
LIB_FUNC("5AZPp99ogrc", LibJson2::JsonArrayIteratorNotEqual);
|
||||
LIB_FUNC("wcgr5mte7T8", LibJson2::JsonArrayIteratorDereference);
|
||||
LIB_FUNC("w5+VCznos5E", LibJson2::JsonArrayIteratorIncrement);
|
||||
LIB_FUNC("9yLjn46Ypfs", LibJson2::JsonArrayIteratorDtor);
|
||||
LIB_FUNC("5yHuiWXo2gg", LibJson2::JsonValueSetBool);
|
||||
LIB_FUNC("QxVVYhP-mvg", LibJson2::JsonValueSetInt);
|
||||
LIB_FUNC("SIe1ZmW7e7s", LibJson2::JsonValueSetUInt);
|
||||
|
||||
+91
-166
@@ -29,11 +29,9 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
@@ -43,9 +41,11 @@
|
||||
#include <windows.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include <csignal>
|
||||
#include <pthread.h>
|
||||
#include <sys/ucontext.h>
|
||||
#else
|
||||
#include <csignal>
|
||||
#include <pthread.h>
|
||||
#include <ucontext.h>
|
||||
#endif
|
||||
|
||||
@@ -1168,7 +1168,6 @@ static int KYTY_SYSV_ABI KernelRaiseException(Pthread thread, int signum) {
|
||||
return KERNEL_ERROR_EINVAL;
|
||||
}
|
||||
|
||||
Common::CondVar::SignalThread(PthreadGetUniqueId(thread));
|
||||
PthreadWakeForSignal(thread);
|
||||
CloseHandle(target_thread);
|
||||
return OK;
|
||||
@@ -1193,7 +1192,6 @@ static int KYTY_SYSV_ABI KernelRaiseException(Pthread thread, int signum) {
|
||||
return KERNEL_ERROR_EINVAL;
|
||||
}
|
||||
|
||||
Common::CondVar::SignalThread(PthreadGetUniqueId(thread));
|
||||
PthreadWakeForSignal(thread);
|
||||
WaitForSignalDispatch(thread, signum);
|
||||
return OK;
|
||||
@@ -2358,132 +2356,71 @@ static bool FiberIsValid(const FiberObject* fiber) {
|
||||
fiber->magic_end == FIBER_MAGIC_END;
|
||||
}
|
||||
|
||||
static thread_local FiberObject* g_current_fiber = nullptr;
|
||||
static thread_local FiberObject* g_thread_return_fiber = nullptr;
|
||||
static thread_local FiberObject* g_starting_fiber = nullptr;
|
||||
static thread_local FiberObject* g_pending_idle_fiber = nullptr;
|
||||
static thread_local FiberCpuContext g_thread_fiber_context {};
|
||||
static std::mutex g_fiber_owner_mutex;
|
||||
static std::unordered_map<FiberObject*, uint64_t> g_fiber_owner_thread;
|
||||
static std::unordered_map<uint64_t, FiberObject*> g_fiber_current_by_thread;
|
||||
struct FiberThreadContext {
|
||||
FiberObject* current_fiber;
|
||||
FiberObject* returned_fiber;
|
||||
FiberObject* pending_idle_fiber;
|
||||
FiberCpuContext cpu_context;
|
||||
};
|
||||
|
||||
static const auto g_fiber_context_key = [] {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
const auto key = TlsAlloc();
|
||||
if (key == TLS_OUT_OF_INDEXES) {
|
||||
std::abort();
|
||||
}
|
||||
#else
|
||||
pthread_key_t key;
|
||||
if (pthread_key_create(&key, nullptr) != 0) {
|
||||
std::abort();
|
||||
}
|
||||
#endif
|
||||
return key;
|
||||
}();
|
||||
|
||||
// Native lookup must stay fresh when a fiber resumes on another host thread.
|
||||
static FiberThreadContext* FiberGetThreadContext() {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
return static_cast<FiberThreadContext*>(TlsGetValue(g_fiber_context_key));
|
||||
#else
|
||||
return static_cast<FiberThreadContext*>(pthread_getspecific(g_fiber_context_key));
|
||||
#endif
|
||||
}
|
||||
|
||||
static void FiberSetThreadContext(FiberThreadContext* context) {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
if (TlsSetValue(g_fiber_context_key, context) == 0) {
|
||||
#else
|
||||
if (pthread_setspecific(g_fiber_context_key, context) != 0) {
|
||||
#endif
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
static uint32_t FiberLoadState(const FiberObject* fiber) {
|
||||
auto& state = const_cast<uint32_t&>(fiber->state);
|
||||
return std::atomic_ref<uint32_t>(state).load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
static uint64_t FiberCurrentHostThreadId() {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
return static_cast<uint64_t>(::GetCurrentThreadId());
|
||||
#else
|
||||
return std::hash<std::thread::id> {}(std::this_thread::get_id());
|
||||
#endif
|
||||
}
|
||||
|
||||
static void FiberSetOwner(FiberObject* fiber) {
|
||||
std::lock_guard lock(g_fiber_owner_mutex);
|
||||
g_fiber_owner_thread[fiber] = FiberCurrentHostThreadId();
|
||||
}
|
||||
|
||||
static void FiberClearOwner(FiberObject* fiber) {
|
||||
std::lock_guard lock(g_fiber_owner_mutex);
|
||||
g_fiber_owner_thread.erase(fiber);
|
||||
}
|
||||
|
||||
static uint64_t FiberGetOwner(FiberObject* fiber) {
|
||||
std::lock_guard lock(g_fiber_owner_mutex);
|
||||
auto it = g_fiber_owner_thread.find(fiber);
|
||||
return it != g_fiber_owner_thread.end() ? it->second : 0;
|
||||
}
|
||||
|
||||
static void FiberSetCurrentFiber(FiberObject* fiber) {
|
||||
g_current_fiber = fiber;
|
||||
|
||||
std::lock_guard lock(g_fiber_owner_mutex);
|
||||
const auto thread_id = FiberCurrentHostThreadId();
|
||||
if (fiber != nullptr) {
|
||||
g_fiber_current_by_thread[thread_id] = fiber;
|
||||
} else {
|
||||
g_fiber_current_by_thread.erase(thread_id);
|
||||
}
|
||||
}
|
||||
|
||||
static void FiberStoreState(FiberObject* fiber, uint32_t state) {
|
||||
std::atomic_ref<uint32_t>(fiber->state).store(state, std::memory_order_release);
|
||||
if (state == FIBER_STATE_RUNNING) {
|
||||
FiberSetOwner(fiber);
|
||||
} else {
|
||||
FiberClearOwner(fiber);
|
||||
}
|
||||
}
|
||||
|
||||
static void FiberDeferIdle(FiberObject* fiber) {
|
||||
g_pending_idle_fiber = fiber;
|
||||
}
|
||||
|
||||
static void FiberCommitDeferredIdle() {
|
||||
auto* fiber = g_pending_idle_fiber;
|
||||
g_pending_idle_fiber = nullptr;
|
||||
static void FiberCompleteSwitch(FiberObject* current) {
|
||||
auto* context = FiberGetThreadContext();
|
||||
context->current_fiber = current;
|
||||
// Only publish IDLE after switching away from the departing fiber's stack.
|
||||
auto* fiber = context->pending_idle_fiber;
|
||||
context->pending_idle_fiber = nullptr;
|
||||
if (fiber != nullptr) {
|
||||
FiberStoreState(fiber, FIBER_STATE_IDLE);
|
||||
}
|
||||
}
|
||||
|
||||
static bool FiberCompareExchangeState(FiberObject* fiber, uint32_t expected, uint32_t desired) {
|
||||
const bool ok = std::atomic_ref<uint32_t>(fiber->state)
|
||||
.compare_exchange_strong(expected, desired, std::memory_order_acq_rel,
|
||||
std::memory_order_acquire);
|
||||
if (ok) {
|
||||
if (desired == FIBER_STATE_RUNNING) {
|
||||
FiberSetOwner(fiber);
|
||||
} else {
|
||||
FiberClearOwner(fiber);
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool FiberWaitAndEnterRunning(FiberObject* fiber, uint32_t* observed_state) {
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
uint32_t spin = 0;
|
||||
auto& state = fiber->state;
|
||||
auto ref = std::atomic_ref<uint32_t>(state);
|
||||
|
||||
for (;;) {
|
||||
uint32_t expected = FIBER_STATE_IDLE;
|
||||
if (ref.compare_exchange_strong(expected, FIBER_STATE_RUNNING, std::memory_order_acq_rel,
|
||||
std::memory_order_acquire)) {
|
||||
FiberSetOwner(fiber);
|
||||
return true;
|
||||
}
|
||||
if (observed_state != nullptr) {
|
||||
*observed_state = expected;
|
||||
}
|
||||
if (expected != FIBER_STATE_RUNNING) {
|
||||
return false;
|
||||
}
|
||||
if (std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(5)) {
|
||||
return false;
|
||||
}
|
||||
if (spin++ < 64) {
|
||||
std::this_thread::yield();
|
||||
} else {
|
||||
std::this_thread::sleep_for(std::chrono::microseconds(100));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool FiberRepairStaleRunningOnThisThread(FiberObject* fiber, uint32_t observed_state) {
|
||||
if (observed_state != FIBER_STATE_RUNNING || fiber == g_current_fiber) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto owner = FiberGetOwner(fiber);
|
||||
if (owner != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return FiberCompareExchangeState(fiber, FIBER_STATE_RUNNING, FIBER_STATE_IDLE);
|
||||
return std::atomic_ref<uint32_t>(fiber->state)
|
||||
.compare_exchange_strong(expected, desired, std::memory_order_acq_rel,
|
||||
std::memory_order_acquire);
|
||||
}
|
||||
|
||||
static void FiberSetContextValid(FiberObject* fiber, bool valid) {
|
||||
@@ -2540,45 +2477,36 @@ static void FiberRestoreContext(FiberCpuContext* ctx, uint64_t ret) {
|
||||
}
|
||||
#endif
|
||||
|
||||
[[noreturn]] static void FiberStartTrampoline();
|
||||
[[noreturn]] static KYTY_SYSV_ABI void FiberStartTrampoline(FiberObject* fiber);
|
||||
|
||||
[[noreturn]] static void FiberStartOnGuestStack(FiberObject* fiber) {
|
||||
[[noreturn]] static KYTY_SYSV_ABI void FiberStartOnGuestStack(FiberObject* fiber) {
|
||||
FiberCpuContext ctx {};
|
||||
const auto stack_top = reinterpret_cast<uintptr_t>(fiber->addr_context) +
|
||||
static_cast<uintptr_t>(fiber->size_context);
|
||||
auto rsp = (stack_top & ~static_cast<uintptr_t>(0x0f));
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
rsp -= 4u * sizeof(uint64_t);
|
||||
#endif
|
||||
rsp -= sizeof(uint64_t);
|
||||
*reinterpret_cast<uint64_t*>(rsp) = 0;
|
||||
|
||||
ctx.rsp = rsp;
|
||||
ctx.rip = reinterpret_cast<uint64_t>(&FiberStartTrampoline);
|
||||
ctx.rdi = reinterpret_cast<uint64_t>(fiber);
|
||||
|
||||
g_starting_fiber = fiber;
|
||||
FiberRestoreContext(&ctx, 1);
|
||||
}
|
||||
|
||||
[[noreturn]] static void FiberStartTrampoline() {
|
||||
auto* fiber = g_starting_fiber;
|
||||
g_starting_fiber = nullptr;
|
||||
if (fiber == nullptr) {
|
||||
EXIT("Fiber start without current fiber\n");
|
||||
}
|
||||
|
||||
FiberCommitDeferredIdle();
|
||||
FiberSetCurrentFiber(fiber);
|
||||
[[noreturn]] static KYTY_SYSV_ABI void FiberStartTrampoline(FiberObject* fiber) {
|
||||
FiberCompleteSwitch(fiber);
|
||||
|
||||
fiber->entry(fiber->arg_on_initialize, fiber->arg_on_run);
|
||||
|
||||
FiberStoreState(fiber, FIBER_STATE_TERMINATED);
|
||||
FiberSetContextValid(fiber, false);
|
||||
fiber->arg_on_return = 0;
|
||||
g_thread_return_fiber = fiber;
|
||||
FiberSetCurrentFiber(nullptr);
|
||||
auto* context = FiberGetThreadContext();
|
||||
context->returned_fiber = fiber;
|
||||
context->current_fiber = nullptr;
|
||||
|
||||
FiberRestoreContext(&g_thread_fiber_context, 1);
|
||||
FiberRestoreContext(&context->cpu_context, 1);
|
||||
}
|
||||
|
||||
int32_t KYTY_SYSV_ABI FiberInitialize(FiberObject* fiber, const char* name, FiberEntry entry,
|
||||
@@ -2688,34 +2616,39 @@ int32_t KYTY_SYSV_ABI FiberRun(FiberObject* fiber, uint64_t arg_on_run, uint64_t
|
||||
if (!FiberIsValid(fiber)) {
|
||||
return FIBER_ERROR_INVALID;
|
||||
}
|
||||
if (g_current_fiber != nullptr) {
|
||||
if (FiberGetThreadContext() != nullptr) {
|
||||
return FIBER_ERROR_PERMISSION;
|
||||
}
|
||||
if (!FiberCompareExchangeState(fiber, FIBER_STATE_IDLE, FIBER_STATE_RUNNING)) {
|
||||
return FIBER_ERROR_STATE;
|
||||
}
|
||||
|
||||
fiber->arg_on_run = arg_on_run;
|
||||
fiber->arg_on_return = 0;
|
||||
g_thread_return_fiber = nullptr;
|
||||
fiber->arg_on_run = arg_on_run;
|
||||
fiber->arg_on_return = 0;
|
||||
// Register on the host stack; switching fibers only reads the native TLS slot.
|
||||
FiberThreadContext context {};
|
||||
FiberSetThreadContext(&context);
|
||||
|
||||
if (FiberSaveContext(&g_thread_fiber_context) == 0) {
|
||||
if (FiberSaveContext(&context.cpu_context) == 0) {
|
||||
if (fiber->context_valid) {
|
||||
FiberRestoreContext(&fiber->saved_context, 1);
|
||||
}
|
||||
FiberStartOnGuestStack(fiber);
|
||||
}
|
||||
|
||||
FiberCommitDeferredIdle();
|
||||
FiberSetCurrentFiber(nullptr);
|
||||
auto* returned_fiber = (g_thread_return_fiber != nullptr ? g_thread_return_fiber : fiber);
|
||||
g_thread_return_fiber = nullptr;
|
||||
auto* returned_fiber = (context.returned_fiber != nullptr ? context.returned_fiber : fiber);
|
||||
const auto return_value = returned_fiber->arg_on_return;
|
||||
const auto return_code =
|
||||
FiberLoadState(returned_fiber) == FIBER_STATE_TERMINATED ? FIBER_ERROR_STATE : OK;
|
||||
// Copy results before another thread can acquire and resume the fiber.
|
||||
FiberCompleteSwitch(nullptr);
|
||||
FiberSetThreadContext(nullptr);
|
||||
|
||||
if (arg_on_return != nullptr) {
|
||||
*arg_on_return = returned_fiber->arg_on_return;
|
||||
*arg_on_return = return_value;
|
||||
}
|
||||
|
||||
return (FiberLoadState(returned_fiber) == FIBER_STATE_TERMINATED ? FIBER_ERROR_STATE : OK);
|
||||
return return_code;
|
||||
}
|
||||
|
||||
int32_t KYTY_SYSV_ABI FiberSwitch(FiberObject* fiber, uint64_t arg_on_run,
|
||||
@@ -2725,38 +2658,30 @@ int32_t KYTY_SYSV_ABI FiberSwitch(FiberObject* fiber, uint64_t arg_on_run,
|
||||
if (!FiberIsValid(fiber)) {
|
||||
return FIBER_ERROR_INVALID;
|
||||
}
|
||||
if (g_current_fiber == nullptr) {
|
||||
auto* context = FiberGetThreadContext();
|
||||
if (context == nullptr) {
|
||||
return FIBER_ERROR_PERMISSION;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
uint32_t observed_state = 0;
|
||||
if (FiberWaitAndEnterRunning(fiber, &observed_state)) {
|
||||
break;
|
||||
}
|
||||
if (FiberRepairStaleRunningOnThisThread(fiber, observed_state)) {
|
||||
continue;
|
||||
}
|
||||
if (!FiberCompareExchangeState(fiber, FIBER_STATE_IDLE, FIBER_STATE_RUNNING)) {
|
||||
return FIBER_ERROR_STATE;
|
||||
}
|
||||
|
||||
auto* caller = g_current_fiber;
|
||||
auto* caller = context->current_fiber;
|
||||
|
||||
fiber->arg_on_run = arg_on_run;
|
||||
fiber->arg_on_return = 0;
|
||||
|
||||
if (FiberSaveContext(&caller->saved_context) == 0) {
|
||||
FiberSetContextValid(caller, true);
|
||||
FiberDeferIdle(caller);
|
||||
context->pending_idle_fiber = caller;
|
||||
if (fiber->context_valid) {
|
||||
FiberRestoreContext(&fiber->saved_context, 1);
|
||||
}
|
||||
FiberStartOnGuestStack(fiber);
|
||||
}
|
||||
|
||||
FiberCommitDeferredIdle();
|
||||
FiberSetCurrentFiber(caller);
|
||||
FiberStoreState(caller, FIBER_STATE_RUNNING);
|
||||
FiberCompleteSwitch(caller);
|
||||
|
||||
if (arg_on_return != nullptr) {
|
||||
*arg_on_return = caller->arg_on_run;
|
||||
@@ -2772,7 +2697,8 @@ int32_t KYTY_SYSV_ABI FiberGetSelf(FiberObject** fiber) {
|
||||
return FIBER_ERROR_NULL;
|
||||
}
|
||||
|
||||
*fiber = g_current_fiber;
|
||||
const auto* context = FiberGetThreadContext();
|
||||
*fiber = context != nullptr ? context->current_fiber : nullptr;
|
||||
|
||||
return OK;
|
||||
}
|
||||
@@ -2780,23 +2706,22 @@ int32_t KYTY_SYSV_ABI FiberGetSelf(FiberObject** fiber) {
|
||||
int32_t KYTY_SYSV_ABI FiberReturnToThread(uint64_t arg_on_return, uint64_t* arg_on_run) {
|
||||
PRINT_NAME();
|
||||
|
||||
if (g_current_fiber == nullptr) {
|
||||
auto* context = FiberGetThreadContext();
|
||||
if (context == nullptr) {
|
||||
return FIBER_ERROR_PERMISSION;
|
||||
}
|
||||
|
||||
auto* fiber = g_current_fiber;
|
||||
auto* fiber = context->current_fiber;
|
||||
fiber->arg_on_return = arg_on_return;
|
||||
|
||||
if (FiberSaveContext(&fiber->saved_context) == 0) {
|
||||
FiberSetContextValid(fiber, true);
|
||||
g_thread_return_fiber = fiber;
|
||||
FiberDeferIdle(fiber);
|
||||
FiberRestoreContext(&g_thread_fiber_context, 1);
|
||||
context->returned_fiber = fiber;
|
||||
context->pending_idle_fiber = fiber;
|
||||
FiberRestoreContext(&context->cpu_context, 1);
|
||||
}
|
||||
|
||||
FiberCommitDeferredIdle();
|
||||
FiberSetCurrentFiber(fiber);
|
||||
FiberStoreState(fiber, FIBER_STATE_RUNNING);
|
||||
FiberCompleteSwitch(fiber);
|
||||
if (arg_on_run != nullptr) {
|
||||
*arg_on_run = fiber->arg_on_run;
|
||||
}
|
||||
|
||||
@@ -583,22 +583,22 @@ int KYTY_SYSV_ABI SaveDataTransferringMount(const SaveDataTransferringMount* mou
|
||||
SaveDataMountResult* mount_result) {
|
||||
PRINT_NAME();
|
||||
|
||||
if (mount == nullptr || mount_result == nullptr || mount->dir_name == nullptr) {
|
||||
if (mount == nullptr || mount_result == nullptr || mount->title_id == nullptr ||
|
||||
mount->dir_name == nullptr) {
|
||||
return SAVE_DATA_ERROR_PARAMETER;
|
||||
}
|
||||
|
||||
LOGF("\t user_id = %" PRId32 "\n"
|
||||
"\t title_id = %s\n"
|
||||
"\t dir_name = %s\n",
|
||||
mount->user_id, mount->title_id != nullptr ? mount->title_id->data : "<null>",
|
||||
mount->dir_name->data);
|
||||
mount->user_id, mount->title_id->data, mount->dir_name->data);
|
||||
|
||||
*mount_result = {};
|
||||
|
||||
Common::LockGuard lock(g_mount_mutex);
|
||||
const std::string dir_name = mount->dir_name->data;
|
||||
const std::string mount_dir =
|
||||
std::string(SAVE_DATA_DIR) + "/" + get_title_id() + "/" + dir_name;
|
||||
std::string(SAVE_DATA_DIR) + "/" + mount->title_id->data + "/" + dir_name;
|
||||
const int slot = g_mount_slots.FindAvailable(dir_name);
|
||||
if (slot == SaveDataMountSlots::BUSY) {
|
||||
return SAVE_DATA_ERROR_BUSY;
|
||||
@@ -608,10 +608,10 @@ int KYTY_SYSV_ABI SaveDataTransferringMount(const SaveDataTransferringMount* mou
|
||||
}
|
||||
|
||||
if (!Common::File::IsDirectoryExisting(mount_dir)) {
|
||||
Common::File::CreateDirectories(mount_dir);
|
||||
return SAVE_DATA_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
return mount_save_data(slot, dir_name, mount_dir, 1, mount_result);
|
||||
return mount_save_data(slot, dir_name, mount_dir, 0, mount_result);
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI SaveDataUmount2(uint32_t mode, const SaveDataMountPoint* mount_point) {
|
||||
|
||||
@@ -92,6 +92,11 @@ public:
|
||||
[[nodiscard]] bool Initialize() {
|
||||
const AVCodec* decoder = avcodec_find_decoder(GetAvCodecId(m_config.codec_type));
|
||||
if (decoder == nullptr) {
|
||||
if (m_config.codec_type == CODEC_TYPE_VP9) {
|
||||
Log::WriteToConsoleAndLog(
|
||||
"WARNING: Videodec2: The game requested VP9 video, but FFmpeg has no VP9 "
|
||||
"decoder. Use FFmpeg with VP9 decoding enabled.\n");
|
||||
}
|
||||
LOGF("Videodec2: FFmpeg decoder is unavailable for codec type %u\n",
|
||||
m_config.codec_type);
|
||||
return false;
|
||||
|
||||
+528
-447
File diff suppressed because it is too large.
Load diff
@@ -14,7 +14,7 @@ namespace Loader {
|
||||
struct RedZonePatchResult {
|
||||
uint64_t function_count = 0;
|
||||
uint64_t instruction_count = 0;
|
||||
uint64_t red_zone_function_count = 0;
|
||||
uint64_t red_zone_function_count = 0;
|
||||
uint64_t memory_instruction_count = 0;
|
||||
uint64_t short_memory_instruction_count = 0;
|
||||
uint64_t patched_memory_instruction_count = 0;
|
||||
@@ -22,14 +22,17 @@ struct RedZonePatchResult {
|
||||
uint64_t control_flow_memory_instruction_count = 0;
|
||||
uint64_t unrelocatable_memory_instruction_count = 0;
|
||||
uint64_t indirect_red_zone_function_count = 0;
|
||||
uint64_t reciprocal_sqrt_instruction_count = 0;
|
||||
};
|
||||
|
||||
void RegisterRedZonePatchModule(void* module_ptr, uint64_t module_size, void* trampoline_area_ptr,
|
||||
uint64_t trampoline_area_size);
|
||||
void UnregisterRedZonePatchModule(void* module_ptr);
|
||||
|
||||
RedZonePatchResult PatchRedZoneMemoryInstructions(uint64_t segment_addr, uint64_t segment_size,
|
||||
std::span<const uintptr_t> function_starts);
|
||||
// Analyze and protect fault sites before replacing reciprocal roots with traps.
|
||||
RedZonePatchResult PatchGuestInstructions(uint64_t segment_addr, uint64_t segment_size,
|
||||
std::span<const uintptr_t> function_starts,
|
||||
bool protect_memory, bool emulate_rsqrt);
|
||||
|
||||
bool DecodeEhFrameFunctionStarts(uint64_t eh_frame_header_addr, uint64_t eh_frame_header_size,
|
||||
std::vector<uintptr_t>* function_starts);
|
||||
|
||||
@@ -2014,9 +2014,11 @@ void RuntimeLinker::LoadProgramToMemory(Program* program) {
|
||||
uint64_t tls_handler_size = is_shared ? 0 : Jit::SafeCall::GetSize();
|
||||
EXIT_IF(tls_handler_size > UINT64_MAX - program->base_size_aligned);
|
||||
program->mapped_size = program->base_size_aligned + tls_handler_size;
|
||||
const bool emulate_rsqrt = Config::AmdCpuEnabled();
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
const bool use_red_zone_protection = Config::RedZoneProtectionEnabled();
|
||||
const bool protect_memory_faults = Config::RedZoneProtectionEnabled();
|
||||
const bool use_red_zone_protection = protect_memory_faults || emulate_rsqrt;
|
||||
constexpr uint64_t RED_ZONE_TRAMPOLINE_SIZE = 8u * 1024u * 1024u;
|
||||
if (use_red_zone_protection) {
|
||||
EXIT_IF(RED_ZONE_TRAMPOLINE_SIZE > UINT64_MAX - program->mapped_size);
|
||||
@@ -2062,9 +2064,8 @@ void RuntimeLinker::LoadProgramToMemory(Program* program) {
|
||||
EXIT("Failed to install the required vectored exception handler\n");
|
||||
}
|
||||
|
||||
// program->elf->SetBaseVAddr(program->base_vaddr);
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
std::vector<std::pair<uint64_t, uint64_t>> executable_segments;
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
uint64_t eh_frame_header_addr = 0;
|
||||
uint64_t eh_frame_header_size = 0;
|
||||
#endif
|
||||
@@ -2090,11 +2091,7 @@ void RuntimeLinker::LoadProgramToMemory(Program* program) {
|
||||
|
||||
if (Common::VirtualMemory::IsExecute(mode)) {
|
||||
PatchProgram(program, segment_addr, segment_memory_size);
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
if (use_red_zone_protection) {
|
||||
executable_segments.emplace_back(segment_addr, segment_file_size);
|
||||
}
|
||||
#endif
|
||||
executable_segments.emplace_back(segment_addr, segment_file_size);
|
||||
}
|
||||
|
||||
if (!skip_protect) {
|
||||
@@ -2139,16 +2136,22 @@ void RuntimeLinker::LoadProgramToMemory(Program* program) {
|
||||
}
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
std::vector<uintptr_t> function_starts;
|
||||
if (use_red_zone_protection) {
|
||||
std::vector<uintptr_t> function_starts;
|
||||
if (!DecodeEhFrameFunctionStarts(eh_frame_header_addr, eh_frame_header_size,
|
||||
&function_starts)) {
|
||||
LOGF("Windows guest red-zone patching could not decode function boundaries for %s\n",
|
||||
Common::PathToString(program->file_name).c_str());
|
||||
}
|
||||
for (const auto& [segment_addr, segment_size]: executable_segments) {
|
||||
}
|
||||
#endif
|
||||
for (const auto& [segment_addr, segment_size]: executable_segments) {
|
||||
uint64_t reciprocal_sqrt_count = 0;
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
if (use_red_zone_protection) {
|
||||
const auto result =
|
||||
PatchRedZoneMemoryInstructions(segment_addr, segment_size, function_starts);
|
||||
PatchGuestInstructions(segment_addr, segment_size, function_starts,
|
||||
protect_memory_faults, emulate_rsqrt);
|
||||
LOGF("Windows guest red-zone patching: %s, functions=%" PRIu64 ", red_zone=%" PRIu64
|
||||
", memory=%" PRIu64 ", patched=%" PRIu64 ", short=%" PRIu64 ", stack=%" PRIu64
|
||||
", control=%" PRIu64 ", unrelocatable=%" PRIu64 "\n",
|
||||
@@ -2158,12 +2161,20 @@ void RuntimeLinker::LoadProgramToMemory(Program* program) {
|
||||
result.stack_dependent_memory_instruction_count,
|
||||
result.control_flow_memory_instruction_count,
|
||||
result.unrelocatable_memory_instruction_count);
|
||||
reciprocal_sqrt_count = result.reciprocal_sqrt_instruction_count;
|
||||
}
|
||||
#else
|
||||
if (emulate_rsqrt) {
|
||||
reciprocal_sqrt_count =
|
||||
X64InstructionEmulator::PatchReciprocalSquareRoots(segment_addr, segment_size);
|
||||
Common::VirtualMemory::FlushInstructionCache(segment_addr, segment_size);
|
||||
}
|
||||
Common::VirtualMemory::FlushInstructionCache(program->red_zone_trampoline_vaddr,
|
||||
program->red_zone_trampoline_size);
|
||||
}
|
||||
#endif
|
||||
if (reciprocal_sqrt_count != 0) {
|
||||
LOGF("Guest VRSQRTPS emulation: %s, instructions=%" PRIu64 "\n",
|
||||
Common::PathToString(program->file_name.filename()).c_str(), reciprocal_sqrt_count);
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_shared) {
|
||||
SetupTlsHandler(program);
|
||||
|
||||
@@ -2,11 +2,19 @@
|
||||
|
||||
#include "common/common.h"
|
||||
|
||||
#include <Zydis/Zydis.h>
|
||||
#include <bit>
|
||||
#include <cstring>
|
||||
#if !defined(__APPLE__)
|
||||
#include <emmintrin.h>
|
||||
#include <xmmintrin.h>
|
||||
#endif
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
#include <windows.h> // IWYU pragma: keep
|
||||
#elif !defined(__APPLE__)
|
||||
#elif defined(__APPLE__)
|
||||
#include <sys/ucontext.h>
|
||||
#else
|
||||
#include <sched.h>
|
||||
#include <ucontext.h>
|
||||
#endif
|
||||
@@ -400,109 +408,170 @@ static bool ExecuteShaNiInsn(const ShaNiInsn& insn, const XmmWords& src2, const
|
||||
}
|
||||
}
|
||||
|
||||
// Keep instruction semantics shared; only access to the saved host context differs.
|
||||
struct Context {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
PCONTEXT native;
|
||||
|
||||
static void LoadXmmWordsWin(const M128A* xmm, XmmWords& out) {
|
||||
out.w[0] = static_cast<uint32_t>(xmm->Low);
|
||||
out.w[1] = static_cast<uint32_t>(xmm->Low >> 32u);
|
||||
out.w[2] = static_cast<uint32_t>(xmm->High);
|
||||
out.w[3] = static_cast<uint32_t>(xmm->High >> 32u);
|
||||
}
|
||||
[[nodiscard]] uint64_t Rip() const { return native->Rip; }
|
||||
void Advance(size_t length) { native->Rip += length; }
|
||||
[[nodiscard]] void* Xmm(uint8_t index) const { return &native->Xmm0 + index; }
|
||||
|
||||
static void StoreXmmWordsWin(M128A* xmm, const XmmWords& in) {
|
||||
xmm->Low = static_cast<uint64_t>(in.w[0]) | (static_cast<uint64_t>(in.w[1]) << 32u);
|
||||
xmm->High = static_cast<uint64_t>(in.w[2]) | (static_cast<uint64_t>(in.w[3]) << 32u);
|
||||
void LoadGprs(uint64_t (&gpr)[16]) const {
|
||||
const uint64_t registers[] = {native->Rax, native->Rcx, native->Rdx, native->Rbx,
|
||||
native->Rsp, native->Rbp, native->Rsi, native->Rdi,
|
||||
native->R8, native->R9, native->R10, native->R11,
|
||||
native->R12, native->R13, native->R14, native->R15};
|
||||
std::memcpy(gpr, registers, sizeof(gpr));
|
||||
}
|
||||
|
||||
void ClearUpperYmm(uint8_t index) const {
|
||||
if ((native->ContextFlags & CONTEXT_XSTATE) != CONTEXT_XSTATE) {
|
||||
return;
|
||||
}
|
||||
DWORD64 features = 0;
|
||||
if (!GetXStateFeaturesMask(native, &features) || (features & XSTATE_MASK_AVX) == 0) {
|
||||
return; // An absent AVX component restores zeroes.
|
||||
}
|
||||
DWORD size = 0;
|
||||
auto* ymm = static_cast<M128A*>(LocateXStateFeature(native, XSTATE_AVX, &size));
|
||||
if (ymm != nullptr && size >= (index + 1u) * sizeof(M128A)) {
|
||||
ymm[index] = {};
|
||||
}
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
ucontext_t* native;
|
||||
|
||||
[[nodiscard]] uint64_t Rip() const {
|
||||
return static_cast<uint64_t>(native->uc_mcontext->__ss.__rip);
|
||||
}
|
||||
void Advance(size_t length) {
|
||||
native->uc_mcontext->__ss.__rip += static_cast<uint64_t>(length);
|
||||
}
|
||||
// Darwin names the XMM file __fpu_xmm0..__fpu_xmm15 instead of exposing an array.
|
||||
[[nodiscard]] void* Xmm(uint8_t index) const {
|
||||
auto* fs = &native->uc_mcontext->__fs;
|
||||
switch (index) {
|
||||
case 0: return &fs->__fpu_xmm0;
|
||||
case 1: return &fs->__fpu_xmm1;
|
||||
case 2: return &fs->__fpu_xmm2;
|
||||
case 3: return &fs->__fpu_xmm3;
|
||||
case 4: return &fs->__fpu_xmm4;
|
||||
case 5: return &fs->__fpu_xmm5;
|
||||
case 6: return &fs->__fpu_xmm6;
|
||||
case 7: return &fs->__fpu_xmm7;
|
||||
case 8: return &fs->__fpu_xmm8;
|
||||
case 9: return &fs->__fpu_xmm9;
|
||||
case 10: return &fs->__fpu_xmm10;
|
||||
case 11: return &fs->__fpu_xmm11;
|
||||
case 12: return &fs->__fpu_xmm12;
|
||||
case 13: return &fs->__fpu_xmm13;
|
||||
case 14: return &fs->__fpu_xmm14;
|
||||
case 15: return &fs->__fpu_xmm15;
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
#else
|
||||
ucontext_t* native;
|
||||
|
||||
[[nodiscard]] uint64_t Rip() const {
|
||||
return static_cast<uint64_t>(native->uc_mcontext.gregs[REG_RIP]);
|
||||
}
|
||||
void Advance(size_t length) {
|
||||
native->uc_mcontext.gregs[REG_RIP] += static_cast<greg_t>(length);
|
||||
}
|
||||
[[nodiscard]] void* Xmm(uint8_t index) const {
|
||||
if (native->uc_mcontext.fpregs == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return native->uc_mcontext.fpregs->_xmm[index].element;
|
||||
}
|
||||
|
||||
void LoadGprs(uint64_t (&gpr)[16]) const {
|
||||
constexpr int registers[] = {REG_RAX, REG_RCX, REG_RDX, REG_RBX, REG_RSP, REG_RBP,
|
||||
REG_RSI, REG_RDI, REG_R8, REG_R9, REG_R10, REG_R11,
|
||||
REG_R12, REG_R13, REG_R14, REG_R15};
|
||||
for (size_t i = 0; i < 16; ++i) {
|
||||
gpr[i] = static_cast<uint64_t>(native->uc_mcontext.gregs[registers[i]]);
|
||||
}
|
||||
}
|
||||
|
||||
void ClearUpperYmm(uint8_t index) const {
|
||||
// Linux signal frames use the standard XSAVE layout. An absent AVX component
|
||||
// already restores the architectural initial value (all zeroes).
|
||||
auto* state = reinterpret_cast<uint8_t*>(native->uc_mcontext.fpregs);
|
||||
uint32_t magic = 0;
|
||||
uint32_t size = 0;
|
||||
uint64_t features = 0;
|
||||
std::memcpy(&magic, state + 464, sizeof(magic));
|
||||
if (magic != 0x46505853) {
|
||||
return;
|
||||
}
|
||||
std::memcpy(&size, state + 480, sizeof(size));
|
||||
if (size < 832) {
|
||||
return;
|
||||
}
|
||||
std::memcpy(&features, state + 512, sizeof(features));
|
||||
if ((features & 4) != 0) {
|
||||
std::memset(state + 576 + index * 16, 0, 16);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
};
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
|
||||
static bool TryEmulateShaNi(Context& context) {
|
||||
const auto* rip = reinterpret_cast<const uint8_t*>(context.Rip());
|
||||
ShaNiInsn insn {};
|
||||
if (!DecodeShaNiInsn(rip, insn)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t modrm_byte = rip[insn.modrm_offset];
|
||||
const uint8_t dest_index = ShaNiRegIndex(modrm_byte, insn.rex, true);
|
||||
auto* dest_xmm = context.Xmm(dest_index);
|
||||
auto* xmm0 = context.Xmm(0);
|
||||
if (dest_xmm == nullptr || xmm0 == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
XmmWords dest {};
|
||||
XmmWords src2 {};
|
||||
XmmWords xmm0_words {};
|
||||
std::memcpy(&dest, dest_xmm, sizeof(dest));
|
||||
std::memcpy(&xmm0_words, xmm0, sizeof(xmm0_words));
|
||||
|
||||
if (ShaNiModrmIsRegister(modrm_byte)) {
|
||||
const uint8_t src_index = ShaNiRegIndex(modrm_byte, insn.rex, false);
|
||||
auto* src_xmm = context.Xmm(src_index);
|
||||
if (src_xmm == nullptr) {
|
||||
return false;
|
||||
}
|
||||
std::memcpy(&src2, src_xmm, sizeof(src2));
|
||||
} else {
|
||||
uint64_t gpr[16] {};
|
||||
const void* source = nullptr;
|
||||
context.LoadGprs(gpr);
|
||||
if (!ResolveShaNiMemoryAddress(rip, insn, gpr, source)) {
|
||||
return false;
|
||||
}
|
||||
std::memcpy(&src2, source, sizeof(src2));
|
||||
}
|
||||
|
||||
if (!ExecuteShaNiInsn(insn, src2, xmm0_words, dest)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::memcpy(dest_xmm, &dest, sizeof(dest));
|
||||
context.Advance(insn.length);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
|
||||
static M128A* GetContextXmm(PCONTEXT context, uint8_t index) {
|
||||
if (context == nullptr || index >= 16) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return &context->Xmm0 + index;
|
||||
}
|
||||
|
||||
static void LoadContextGprsWin(PCONTEXT context, uint64_t (&gpr)[16]) {
|
||||
gpr[0] = context->Rax;
|
||||
gpr[1] = context->Rcx;
|
||||
gpr[2] = context->Rdx;
|
||||
gpr[3] = context->Rbx;
|
||||
gpr[4] = context->Rsp;
|
||||
gpr[5] = context->Rbp;
|
||||
gpr[6] = context->Rsi;
|
||||
gpr[7] = context->Rdi;
|
||||
gpr[8] = context->R8;
|
||||
gpr[9] = context->R9;
|
||||
gpr[10] = context->R10;
|
||||
gpr[11] = context->R11;
|
||||
gpr[12] = context->R12;
|
||||
gpr[13] = context->R13;
|
||||
gpr[14] = context->R14;
|
||||
gpr[15] = context->R15;
|
||||
}
|
||||
|
||||
static bool TryEmulateShaNi(PCONTEXT context) {
|
||||
if (context == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto* rip = reinterpret_cast<const uint8_t*>(context->Rip);
|
||||
ShaNiInsn insn {};
|
||||
if (!DecodeShaNiInsn(rip, insn)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t modrm_byte = rip[insn.modrm_offset];
|
||||
const uint8_t dest_index = ShaNiRegIndex(modrm_byte, insn.rex, true);
|
||||
auto* dest_xmm = GetContextXmm(context, dest_index);
|
||||
auto* xmm0 = GetContextXmm(context, 0);
|
||||
if (dest_xmm == nullptr || xmm0 == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
XmmWords dest {};
|
||||
XmmWords src2 {};
|
||||
XmmWords xmm0_words {};
|
||||
LoadXmmWordsWin(dest_xmm, dest);
|
||||
LoadXmmWordsWin(xmm0, xmm0_words);
|
||||
|
||||
if (ShaNiModrmIsRegister(modrm_byte)) {
|
||||
const uint8_t src_index = ShaNiRegIndex(modrm_byte, insn.rex, false);
|
||||
auto* src_xmm = GetContextXmm(context, src_index);
|
||||
if (src_xmm == nullptr) {
|
||||
return false;
|
||||
}
|
||||
LoadXmmWordsWin(src_xmm, src2);
|
||||
} else {
|
||||
uint64_t gpr[16] {};
|
||||
const void* source = nullptr;
|
||||
LoadContextGprsWin(context, gpr);
|
||||
if (!ResolveShaNiMemoryAddress(rip, insn, gpr, source)) {
|
||||
return false;
|
||||
}
|
||||
std::memcpy(&src2, source, sizeof(src2));
|
||||
}
|
||||
|
||||
if (!ExecuteShaNiInsn(insn, src2, xmm0_words, dest)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
StoreXmmWordsWin(dest_xmm, dest);
|
||||
context->Rip += insn.length;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TryEmulateSse4a(PCONTEXT context) {
|
||||
if (context == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto* rip = reinterpret_cast<const uint8_t*>(context->Rip);
|
||||
|
||||
static bool TryEmulateSse4a(Context& context) {
|
||||
const auto* rip = reinterpret_cast<const uint8_t*>(context.Rip());
|
||||
const uint8_t prefix = rip[0];
|
||||
if (prefix != 0x66 && prefix != 0xf2) {
|
||||
return false;
|
||||
@@ -511,277 +580,212 @@ static bool TryEmulateSse4a(PCONTEXT context) {
|
||||
size_t offset = 1;
|
||||
uint8_t rex = 0;
|
||||
if ((rip[offset] & 0xf0u) == 0x40u) {
|
||||
rex = rip[offset];
|
||||
offset++;
|
||||
rex = rip[offset++];
|
||||
}
|
||||
|
||||
if (rip[offset] != 0x0f || rip[offset + 1] != 0x78) {
|
||||
if (rip[offset] != 0x0f) {
|
||||
return false;
|
||||
}
|
||||
const bool register_extract = prefix == 0x66 && rip[offset + 1] == 0x79;
|
||||
if (rip[offset + 1] != 0x78 && !register_extract) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto modrm = rip[offset + 2];
|
||||
const uint8_t modrm = rip[offset + 2];
|
||||
if ((modrm & 0xc0u) != 0xc0u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t reg = ((modrm >> 3u) & 0x07u) | ((rex & 0x04u) << 1u);
|
||||
const uint8_t rm = (modrm & 0x07u) | ((rex & 0x01u) << 3u);
|
||||
const uint8_t length = rip[offset + 3];
|
||||
const uint8_t index = rip[offset + 4];
|
||||
const uint8_t reg = ((modrm >> 3u) & 0x07u) | ((rex & 0x04u) << 1u);
|
||||
const uint8_t rm = (modrm & 0x07u) | ((rex & 0x01u) << 3u);
|
||||
|
||||
// AMD SSE4a immediate-form EXTRQ/INSERTQ. PS5 code can execute these natively on AMD hardware,
|
||||
// while Intel hosts raise an illegal-instruction exception.
|
||||
if (prefix == 0x66) {
|
||||
auto* dst = GetContextXmm(context, rm);
|
||||
if (dst == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
dst->Low = ExtractBitField(dst->Low, length, index);
|
||||
dst->High = 0;
|
||||
context->Rip += offset + 5;
|
||||
return true;
|
||||
// Immediate EXTRQ encodes its destination in r/m; the two-register form uses reg.
|
||||
uint8_t dest_index = reg;
|
||||
if (prefix == 0x66 && !register_extract) {
|
||||
dest_index = rm;
|
||||
}
|
||||
|
||||
auto* dst = GetContextXmm(context, reg);
|
||||
auto* src = GetContextXmm(context, rm);
|
||||
if (dst == nullptr || src == nullptr) {
|
||||
auto* dest_xmm = context.Xmm(dest_index);
|
||||
auto* src_xmm = context.Xmm(rm);
|
||||
if (dest_xmm == nullptr || src_xmm == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
dst->Low = InsertBitField(dst->Low, src->Low, length, index);
|
||||
context->Rip += offset + 5;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TryEmulateMonitorxMwaitx(PCONTEXT context) {
|
||||
if (context == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto* rip = reinterpret_cast<const uint8_t*>(context->Rip);
|
||||
if (rip[0] != 0x0f || rip[1] != 0x01 || (rip[2] != 0xfa && rip[2] != 0xfb)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// AMD MONITORX/MWAITX are used by PS5 code in wait loops. Intel hosts can raise an illegal-
|
||||
// instruction exception, so approximate them as a no-op/yield pair.
|
||||
if (rip[2] == 0xfb) {
|
||||
SwitchToThread();
|
||||
}
|
||||
context->Rip += 3;
|
||||
return true;
|
||||
}
|
||||
|
||||
#elif !defined(__APPLE__)
|
||||
|
||||
// Linux signal contexts expose registers through ucontext_t.
|
||||
|
||||
static uint32_t* GetContextXmm(ucontext_t* context, uint8_t index) {
|
||||
if (context == nullptr || index >= 16) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto* fpregs = context->uc_mcontext.fpregs;
|
||||
if (fpregs == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return static_cast<uint32_t*>(fpregs->_xmm[index].element);
|
||||
}
|
||||
|
||||
static void LoadContextGprsLin(ucontext_t* context, uint64_t (&gpr)[16]) {
|
||||
gpr[0] = static_cast<uint64_t>(context->uc_mcontext.gregs[REG_RAX]);
|
||||
gpr[1] = static_cast<uint64_t>(context->uc_mcontext.gregs[REG_RCX]);
|
||||
gpr[2] = static_cast<uint64_t>(context->uc_mcontext.gregs[REG_RDX]);
|
||||
gpr[3] = static_cast<uint64_t>(context->uc_mcontext.gregs[REG_RBX]);
|
||||
gpr[4] = static_cast<uint64_t>(context->uc_mcontext.gregs[REG_RSP]);
|
||||
gpr[5] = static_cast<uint64_t>(context->uc_mcontext.gregs[REG_RBP]);
|
||||
gpr[6] = static_cast<uint64_t>(context->uc_mcontext.gregs[REG_RSI]);
|
||||
gpr[7] = static_cast<uint64_t>(context->uc_mcontext.gregs[REG_RDI]);
|
||||
gpr[8] = static_cast<uint64_t>(context->uc_mcontext.gregs[REG_R8]);
|
||||
gpr[9] = static_cast<uint64_t>(context->uc_mcontext.gregs[REG_R9]);
|
||||
gpr[10] = static_cast<uint64_t>(context->uc_mcontext.gregs[REG_R10]);
|
||||
gpr[11] = static_cast<uint64_t>(context->uc_mcontext.gregs[REG_R11]);
|
||||
gpr[12] = static_cast<uint64_t>(context->uc_mcontext.gregs[REG_R12]);
|
||||
gpr[13] = static_cast<uint64_t>(context->uc_mcontext.gregs[REG_R13]);
|
||||
gpr[14] = static_cast<uint64_t>(context->uc_mcontext.gregs[REG_R14]);
|
||||
gpr[15] = static_cast<uint64_t>(context->uc_mcontext.gregs[REG_R15]);
|
||||
}
|
||||
|
||||
static void LoadXmmWordsLin(const uint32_t* xmm, XmmWords& out) {
|
||||
out.w[0] = xmm[0];
|
||||
out.w[1] = xmm[1];
|
||||
out.w[2] = xmm[2];
|
||||
out.w[3] = xmm[3];
|
||||
}
|
||||
|
||||
static void StoreXmmWordsLin(uint32_t* xmm, const XmmWords& in) {
|
||||
xmm[0] = in.w[0];
|
||||
xmm[1] = in.w[1];
|
||||
xmm[2] = in.w[2];
|
||||
xmm[3] = in.w[3];
|
||||
}
|
||||
|
||||
static bool TryEmulateShaNi(ucontext_t* context) {
|
||||
if (context == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& rip_reg = context->uc_mcontext.gregs[REG_RIP];
|
||||
const auto* rip = reinterpret_cast<const uint8_t*>(rip_reg);
|
||||
ShaNiInsn insn {};
|
||||
if (!DecodeShaNiInsn(rip, insn)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t modrm_byte = rip[insn.modrm_offset];
|
||||
const uint8_t dest_index = ShaNiRegIndex(modrm_byte, insn.rex, true);
|
||||
auto* dest_xmm = GetContextXmm(context, dest_index);
|
||||
auto* xmm0 = GetContextXmm(context, 0);
|
||||
if (dest_xmm == nullptr || xmm0 == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
XmmWords dest {};
|
||||
XmmWords src2 {};
|
||||
XmmWords xmm0_words {};
|
||||
LoadXmmWordsLin(dest_xmm, dest);
|
||||
LoadXmmWordsLin(xmm0, xmm0_words);
|
||||
|
||||
if (ShaNiModrmIsRegister(modrm_byte)) {
|
||||
const uint8_t src_index = ShaNiRegIndex(modrm_byte, insn.rex, false);
|
||||
auto* src_xmm = GetContextXmm(context, src_index);
|
||||
if (src_xmm == nullptr) {
|
||||
return false;
|
||||
}
|
||||
LoadXmmWordsLin(src_xmm, src2);
|
||||
uint64_t dest[2] {};
|
||||
uint64_t source = 0;
|
||||
std::memcpy(dest, dest_xmm, sizeof(dest));
|
||||
std::memcpy(&source, src_xmm, sizeof(source));
|
||||
uint8_t length = 0;
|
||||
uint8_t index = 0;
|
||||
size_t instruction_length = offset + 3;
|
||||
if (register_extract) {
|
||||
length = static_cast<uint8_t>(source);
|
||||
index = static_cast<uint8_t>(source >> 8u);
|
||||
} else {
|
||||
uint64_t gpr[16] {};
|
||||
const void* source = nullptr;
|
||||
LoadContextGprsLin(context, gpr);
|
||||
if (!ResolveShaNiMemoryAddress(rip, insn, gpr, source)) {
|
||||
return false;
|
||||
}
|
||||
std::memcpy(&src2, source, sizeof(src2));
|
||||
length = rip[offset + 3];
|
||||
index = rip[offset + 4];
|
||||
instruction_length += 2;
|
||||
}
|
||||
|
||||
if (!ExecuteShaNiInsn(insn, src2, xmm0_words, dest)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
StoreXmmWordsLin(dest_xmm, dest);
|
||||
rip_reg += static_cast<greg_t>(insn.length);
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint64_t GetXmmLow(const uint32_t* xmm) {
|
||||
return static_cast<uint64_t>(xmm[0]) | (static_cast<uint64_t>(xmm[1]) << 32u);
|
||||
}
|
||||
|
||||
static void SetXmmLow(uint32_t* xmm, uint64_t value) {
|
||||
xmm[0] = static_cast<uint32_t>(value);
|
||||
xmm[1] = static_cast<uint32_t>(value >> 32u);
|
||||
}
|
||||
|
||||
static void SetXmmHigh(uint32_t* xmm, uint64_t value) {
|
||||
xmm[2] = static_cast<uint32_t>(value);
|
||||
xmm[3] = static_cast<uint32_t>(value >> 32u);
|
||||
}
|
||||
|
||||
static bool TryEmulateSse4a(ucontext_t* context) {
|
||||
if (context == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& rip_reg = context->uc_mcontext.gregs[REG_RIP];
|
||||
|
||||
const auto* rip = reinterpret_cast<const uint8_t*>(rip_reg);
|
||||
|
||||
const uint8_t prefix = rip[0];
|
||||
if (prefix != 0x66 && prefix != 0xf2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t offset = 1;
|
||||
uint8_t rex = 0;
|
||||
if ((rip[offset] & 0xf0u) == 0x40u) {
|
||||
rex = rip[offset];
|
||||
offset++;
|
||||
}
|
||||
|
||||
if (rip[offset] != 0x0f || rip[offset + 1] != 0x78) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto modrm = rip[offset + 2];
|
||||
if ((modrm & 0xc0u) != 0xc0u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t reg = ((modrm >> 3u) & 0x07u) | ((rex & 0x04u) << 1u);
|
||||
const uint8_t rm = (modrm & 0x07u) | ((rex & 0x01u) << 3u);
|
||||
const uint8_t length = rip[offset + 3];
|
||||
const uint8_t index = rip[offset + 4];
|
||||
|
||||
// AMD SSE4a immediate-form EXTRQ/INSERTQ.
|
||||
if (prefix == 0x66) {
|
||||
auto* dst = GetContextXmm(context, rm);
|
||||
if (dst == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SetXmmLow(dst, ExtractBitField(GetXmmLow(dst), length, index));
|
||||
SetXmmHigh(dst, 0);
|
||||
rip_reg += static_cast<greg_t>(offset + 5);
|
||||
return true;
|
||||
dest[0] = ExtractBitField(dest[0], length, index);
|
||||
dest[1] = 0;
|
||||
} else {
|
||||
dest[0] = InsertBitField(dest[0], source, length, index);
|
||||
}
|
||||
|
||||
auto* dst = GetContextXmm(context, reg);
|
||||
auto* src = GetContextXmm(context, rm);
|
||||
if (dst == nullptr || src == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SetXmmLow(dst, InsertBitField(GetXmmLow(dst), GetXmmLow(src), length, index));
|
||||
rip_reg += static_cast<greg_t>(offset + 5);
|
||||
std::memcpy(dest_xmm, dest, sizeof(dest));
|
||||
context.Advance(instruction_length);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TryEmulateMonitorxMwaitx(ucontext_t* context) {
|
||||
if (context == nullptr) {
|
||||
return false;
|
||||
}
|
||||
#if !defined(__APPLE__)
|
||||
|
||||
auto& rip_reg = context->uc_mcontext.gregs[REG_RIP];
|
||||
|
||||
const auto* rip = reinterpret_cast<const uint8_t*>(rip_reg);
|
||||
static bool TryEmulateMonitorxMwaitx(Context& context) {
|
||||
const auto* rip = reinterpret_cast<const uint8_t*>(context.Rip());
|
||||
if (rip[0] != 0x0f || rip[1] != 0x01 || (rip[2] != 0xfa && rip[2] != 0xfb)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Approximate AMD MONITORX/MWAITX as no-op/yield.
|
||||
if (rip[2] == 0xfb) {
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
SwitchToThread();
|
||||
#else
|
||||
::sched_yield();
|
||||
#endif
|
||||
}
|
||||
rip_reg += 3;
|
||||
context.Advance(3);
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint32_t ReciprocalSquareRoot(uint32_t bits) {
|
||||
const uint32_t magnitude = bits & 0x7fffffffu;
|
||||
const uint32_t exponent = magnitude & 0x7f800000u;
|
||||
if (exponent == 0) {
|
||||
// RSQRT treats denormals as signed zero regardless of MXCSR.DAZ.
|
||||
return (bits & 0x80000000u) | 0x7f800000u;
|
||||
}
|
||||
if (magnitude > 0x7f800000u) {
|
||||
return bits | 0x00400000u; // Quiet NaNs without raising an exception.
|
||||
}
|
||||
if ((bits & 0x80000000u) != 0) {
|
||||
return 0xffc00000u;
|
||||
}
|
||||
if (magnitude == 0x7f800000u) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// A deterministic accurate estimate meets the instruction's relative-error
|
||||
// bound without relying on the host vendor's approximation table.
|
||||
const __m128d input = _mm_set_sd(static_cast<double>(std::bit_cast<float>(bits)));
|
||||
const __m128d result = _mm_div_sd(_mm_set_sd(1.0), _mm_sqrt_sd(input, input));
|
||||
return std::bit_cast<uint32_t>(_mm_cvtss_f32(_mm_cvtsd_ss(_mm_setzero_ps(), result)));
|
||||
}
|
||||
|
||||
static bool TryEmulateReciprocalSquareRoot(Context& context) {
|
||||
const auto* rip = reinterpret_cast<const uint8_t*>(context.Rip());
|
||||
size_t prefix_size = 0;
|
||||
uint8_t dest_extension = 0;
|
||||
uint8_t src_extension = 0;
|
||||
if (rip[0] == 0xc5 && (rip[1] & 0x7fu) == 0x70u) {
|
||||
prefix_size = 2;
|
||||
dest_extension = (~rip[1] & 0x80u) >> 4u;
|
||||
} else if (rip[0] == 0xc4 && (rip[1] & 0x1fu) == 1 && (rip[2] & 0x7fu) == 0x70u) {
|
||||
prefix_size = 3;
|
||||
dest_extension = (~rip[1] & 0x80u) >> 4u;
|
||||
src_extension = (~rip[1] & 0x20u) >> 2u;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (rip[prefix_size] != 0x52 || (rip[prefix_size + 1] & 0xc0u) != 0xc0u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t modrm = rip[prefix_size + 1];
|
||||
const uint8_t dest = ((modrm >> 3u) & 7u) | dest_extension;
|
||||
const uint8_t source = (modrm & 7u) | src_extension;
|
||||
auto* dest_xmm = context.Xmm(dest);
|
||||
auto* src_xmm = context.Xmm(source);
|
||||
if (dest_xmm == nullptr || src_xmm == nullptr) {
|
||||
return false;
|
||||
}
|
||||
XmmWords result {};
|
||||
std::memcpy(&result, src_xmm, sizeof(result));
|
||||
// RSQRT ignores the rounding mode and never changes guest exception flags.
|
||||
// Mask host exceptions while calculating, then restore the handler's state.
|
||||
const uint32_t mxcsr = _mm_getcsr();
|
||||
_mm_setcsr(0x1f80);
|
||||
for (auto& word: result.w) {
|
||||
word = ReciprocalSquareRoot(word);
|
||||
}
|
||||
_mm_setcsr(mxcsr);
|
||||
std::memcpy(dest_xmm, &result, sizeof(result));
|
||||
context.ClearUpperYmm(dest);
|
||||
context.Advance(prefix_size + 2);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
bool IsReciprocalSquareRoot(const ZydisDecodedInstruction& instruction,
|
||||
const ZydisDecodedOperand* operands) {
|
||||
return instruction.mnemonic == ZYDIS_MNEMONIC_VRSQRTPS &&
|
||||
instruction.encoding == ZYDIS_INSTRUCTION_ENCODING_VEX &&
|
||||
instruction.raw.vex.offset == 0 && operands[0].size == 128 &&
|
||||
operands[1].type == ZYDIS_OPERAND_TYPE_REGISTER;
|
||||
}
|
||||
|
||||
uint64_t PatchReciprocalSquareRoots(uint64_t address, uint64_t size) {
|
||||
uint64_t patched = 0;
|
||||
#if !defined(__APPLE__)
|
||||
ZydisDecoder decoder {};
|
||||
if (!ZYAN_SUCCESS(
|
||||
ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64))) {
|
||||
return 0;
|
||||
}
|
||||
for (uint64_t offset = 0; offset < size;) {
|
||||
auto* code = reinterpret_cast<uint8_t*>(address + offset);
|
||||
ZydisDecodedInstruction instruction {};
|
||||
ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT] {};
|
||||
if (!ZYAN_SUCCESS(
|
||||
ZydisDecoderDecodeFull(&decoder, code, size - offset, &instruction, operands))) {
|
||||
++offset;
|
||||
continue;
|
||||
}
|
||||
if (IsReciprocalSquareRoot(instruction, operands)) {
|
||||
// vvvv is reserved (must be 1111b). Clear one bit to route this
|
||||
// otherwise intact instruction through the illegal-instruction emulator.
|
||||
code[instruction.raw.vex.size - 1] &= ~0x08u;
|
||||
++patched;
|
||||
}
|
||||
offset += instruction.length;
|
||||
}
|
||||
#else
|
||||
(void)address;
|
||||
(void)size;
|
||||
#endif
|
||||
return patched;
|
||||
}
|
||||
|
||||
bool TryEmulate(void* native_context) {
|
||||
if (native_context == nullptr) {
|
||||
return false;
|
||||
}
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
auto* context = static_cast<PCONTEXT>(native_context);
|
||||
return TryEmulateMonitorxMwaitx(context) || TryEmulateSse4a(context) ||
|
||||
TryEmulateShaNi(context);
|
||||
#elif !defined(__APPLE__)
|
||||
auto* context = static_cast<ucontext_t*>(native_context);
|
||||
Context context {static_cast<PCONTEXT>(native_context)};
|
||||
#elif defined(__APPLE__)
|
||||
auto* saved_context = static_cast<ucontext_t*>(native_context);
|
||||
if (saved_context->uc_mcontext == nullptr) {
|
||||
return false;
|
||||
}
|
||||
Context context {saved_context};
|
||||
#else
|
||||
Context context {static_cast<ucontext_t*>(native_context)};
|
||||
#endif
|
||||
#if !defined(__APPLE__)
|
||||
if (TryEmulateReciprocalSquareRoot(context)) {
|
||||
return true;
|
||||
}
|
||||
return TryEmulateMonitorxMwaitx(context) || TryEmulateSse4a(context) ||
|
||||
TryEmulateShaNi(context);
|
||||
#else
|
||||
(void)native_context;
|
||||
return false;
|
||||
return TryEmulateSse4a(context);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
#ifndef KYTY_LOADER_X64_INSTRUCTION_EMULATOR_H_
|
||||
#define KYTY_LOADER_X64_INSTRUCTION_EMULATOR_H_
|
||||
|
||||
#include <Zydis/DecoderTypes.h>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Loader::X64InstructionEmulator {
|
||||
|
||||
[[nodiscard]] bool IsReciprocalSquareRoot(const ZydisDecodedInstruction& instruction,
|
||||
const ZydisDecodedOperand* operands);
|
||||
uint64_t PatchReciprocalSquareRoots(uint64_t address, uint64_t size);
|
||||
[[nodiscard]] bool TryEmulate(void* native_context);
|
||||
|
||||
} // namespace Loader::X64InstructionEmulator
|
||||
|
||||
Loaded 100 of 106 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user