Compare commits

...

34 Commits

Author SHA1 Message Date
Brandon Strong 1df53b169f kernel: stop sign-extending batch-map protection and type (#405) 2026-08-30 01:46:43 +04:00
Mehmet Cambaz ff61c1cecd shader: implement V_SIN_F16 (#399)
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-08-30 01:32:49 +04:00
nmzik 60a01817ce loader: add support for etaHEN cheats 2026-08-29 20:48:05 +04:00
nmzik 1a87cfedb5 shader: implement V_COS_F16 2026-08-29 19:23:19 +04:00
nmzik 5bd447d2a2 shader: implement DS_BPERMUTE_B32
Co-authored-by: Techx3 <56268269+Techx3@users.noreply.github.com>
2026-08-29 19:23:19 +04:00
Rogelio J Gomes a4fc58e908 input: PC-style DualSense mouse/keyboard mapping (#213)
input: add mouse-to-right-stick mapping and sensitivity

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-08-29 17:27:23 +04:00
nacimoualla 4e8e335b7b macOS: full support — MoltenVK auto-discovery, portability fix, and KytyPS5.app bundle (#394)
* macOS: auto-discover bundled MoltenVK on startup

On macOS the Vulkan loader comes from MoltenVK. SDL's default
probe only searches system paths, so a bundled libMoltenVK.dylib
next to the executable is invisible under the hardened runtime.

Probe it explicitly via SDL_GetBasePath() before falling back to
SDL's default search. Respect SDL_VULKAN_LIBRARY overrides and
log which loader is used. Update README to reflect that no
environment variable is required for bundled builds.

Co-Authored-By: internal-model

* window: fix Vulkan portability enumeration on macOS

MoltenVK requires VK_KHR_portability_enumeration and
VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR to surface
portability devices via enumeratePhysicalDevices. Without the flag
the adapter is hidden on some driver versions, causing
"Could not find suitable device" failures.

Add the instance extension and flag when available, and add
diagnostic logs for all MoltenVK fallbacks (depthClipEnable,
colorWriteEnable, depthBounds, fragmentShaderBarycentric)
so black-screen diagnostics are actionable.

* macOS: add KytyPS5.app bundle packaging

Add a proper macOS application bundle when the launcher is built.
launcher becomes KytyPS5.app (CFBundleIdentifier org.kytyps5.kytyps5)
with KytyPS5 as the executable, and kyty_emulator is installed as a
helper inside Contents/MacOS so QCoreApplication::applicationDirPath()
finds it. MoltenVK is auto-discovered via SDL_GetBasePath() in that
directory, so no SDL_VULKAN_LIBRARY is required.

- src/launcher/Info.plist.in: minimal bundle plist (version from
  PROJECT_VERSION, LSMinimumSystemVersion 13.0).
- src/launcher/CMakeLists.txt: set MACOSX_BUNDLE, BUNDLE DESTINATION,
  install helper, re-sign helper with JIT entitlements, and invoke
  macdeployqt when available. Keep flat kyty_emulator for CLI.
- .github/workflows/build.yml: bundle MoltenVK into
  KytyPS5.app/Contents/MacOS when present, verify bundle artifacts
  (file/lipo/codesign), and handle bundle paths in the release zip.
- README.md: document bundle double-click and MoltenVK locations.

* launcher: fix bundle rpath for Qt frameworks

The bundle executable had @rpath references to Qt frameworks but no
LC_RPATH, causing dyld to abort at launch:

  Library not loaded: @rpath/QtConcurrent.framework
  Reason: no LC_RPATH's found

Set INSTALL_RPATH to @executable_path/../Frameworks and
BUILD_WITH_INSTALL_RPATH so the built and installed bundles
resolve Qt from Contents/Frameworks. Verified via otool -l
(LC_RPATH now present) and macdeployqt now succeeds without
"Cannot resolve rpath" errors.

* launcher: add Import game button

Add a prominent Import button to ConfigurationListWidget that lets
users select a game folder (containing eboot.bin) or a folder
containing multiple games via QFileDialog. Handles:

- Single game folder selected: adds its parent to game_dirs (Scan
  searches subdirectories, so parent is needed)
- Folder with multiple games: adds the selected folder
- Deduplication via PathKey/NormalizeGameDirectories, writes
  settings, rescans, and selects the imported game if found
- Shows info/warning dialogs when no games are found

UI: QToolButton with SP_DirOpenIcon and text Import beside the
existing toolbar, styled with a blue background for visibility.
Logic in ConfigurationListWidget::import_game().

* macOS: fix bundled loader and artifact packaging

* Revert "launcher: add Import game button"

This reverts commit 2f670ef95c.

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-08-29 17:08:54 +04:00
Mehmet Cambaz 947576363e kernel: explain a commit-limit failure at startup (#395)
When CreateFileMapping cannot reserve the guest backing store the log only
carries the raw Win32 error, which gives no clue that the paging file is the
thing to change. Print the size that was asked for and say so directly when the
failure is ERROR_NO_SYSTEM_RESOURCES or ERROR_COMMITMENT_LIMIT.
2026-08-29 16:20:36 +04:00
nmzik 9da390bc0a general: explicit SEC_COMMIT (Windows), default printf logging to silent 2026-08-29 15:31:20 +04:00
nmzik ef639610e7 shader: allow SDWA add-with-carry 2026-08-29 15:31:20 +04:00
Brandon Strong 6c3b562822 graphics: support Standard64KB color target arrays (#315)
graphics: support Standard64KB render target arrays

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-08-29 15:18:44 +04:00
nmzik f1413569ce fix windows build 2026-08-29 14:46:54 +04:00
nmzik fcc9dff5b9 shader: DS_WRITE_B16_D16_HI 2026-08-29 14:40:13 +04:00
nmzik d9ae3ed64c shader: V_FFBH_I32 2026-08-29 14:03:57 +04:00
nmzik 618926b124 network: implement resolver destruction 2026-08-29 13:40:56 +04:00
nmzik dca7ece2ca shader: legalize 1D level-zero gathers 2026-08-29 13:32:20 +04:00
nmzik ccecebfeff loader: report unhandled host exceptions 2026-08-29 13:01:57 +04:00
nmzik 1752f1382f graphical: add per-game Vulkan pipeline caching (driver-side improvements only for now)
Co-authored-by: Ferran <ferran.duarri@me.com>
2026-08-29 12:28:48 +04:00
nmzik 53a59434d8 shader: implement V_CMP_EQ_U64 2026-08-29 11:26:05 +04:00
GodsWill Dev 18b6c9059b shader: support DS_READ_U16_D16_HI (#356)
Decode the high-half D16 LDS read and route it through the existing unsigned-short load lowering. Represent the destination as a high-half partial write so the low 16 bits of the VGPR remain intact. Add captured-instruction decode coverage and a Vulkan execution regression that verifies the preserved half.

Co-authored-by: SueryRD <the.ai.tester2@gmail.com>
2026-08-29 10:39:45 +04:00
GodsWill Dev 715539804b shader: accept SDWA for VOP1 bit operations (#360)
Allow byte, word, and full-dword SDWA source selection for V_NOT_B32 and V_FFBL_B32. Add captured instruction tests that verify decoding, IR extraction, SPIR-V generation, and runtime results.

Co-authored-by: SueryRD <the.ai.tester2@gmail.com>
2026-08-29 10:28:28 +04:00
nmzik b02eb28fd1 shader: support SDWA byte left-shift counts 2026-08-29 10:03:13 +04:00
nmzik 07f1e77d23 shader: support SDWA byte shift counts 2026-08-29 10:03:13 +04:00
Mehmet Cambaz 6980e0863d controller: forward guest vibration to SDL rumble (#365)
Scale the SDK 8-bit motor levels to SDL’s 16-bit range and use SDL’s maximum effect duration. Keep lookup and rumble under the controller lock so hot unplug cannot invalidate the SDL pointer.
2026-08-29 04:49:20 +04:00
Mehmet Cambaz 42bfeec2e9 emulator: run cleanup handlers on the quick_exit path (#334)
Run() registers KytyClose with atexit(), but Execute() ends the process with
std::quick_exit(), which invokes at_quick_exit handlers only. Nothing registered
one, so the cleanup never ran and neither did the emergency shutdown handlers
that close open guest files and flush the log.

Register a quick-exit handler that runs the emergency shutdown. It deliberately
skips RuntimeLinker::Clear(): guest threads are still live at that point and
tearing the linker down there deadlocks against a thread parked in a condition
wait.
2026-08-29 04:44:40 +04:00
GodsWill Dev 795743ce22 graphics: accept the encoded GE_CNTL group range (#363)
graphics: stop rejecting valid GE_CNTL group sizes

GE_CNTL group sizes control guest geometry-engine scheduling rather than host wave width. Do not reject the supported vertex path for compiler-selected values above 64, and remove the corresponding stale debug warnings.
2026-08-29 04:31:35 +04:00
GodsWill Dev ccddd6fb94 user: make the local profile configurable (#347)
user: make the local user ID configurable

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-08-29 04:27:49 +04:00
nmzik e559771a08 libc: isolate guest setenv from host environment. Fix linux debugging 2026-08-29 03:57:09 +04:00
nmzik b870a77176 network: enable IPv4 UDP sockets on POSIX 2026-08-29 03:57:09 +04:00
nmzik 62eaead3a3 add configurable Vulkan present modes
Co-authored-by: Techx3 <56268269+Techx3@users.noreply.github.com>
2026-08-29 03:57:09 +04:00
nmzik d7808d9cb4 add configurable local user name
Co-authored-by: Techx3 <56268269+Techx3@users.noreply.github.com>
2026-08-29 03:57:09 +04:00
nmzik 8267c422b5 launcher: add 1440p/2160p
Co-authored-by: Techx3 <56268269+Techx3@users.noreply.github.com>
2026-08-29 03:57:09 +04:00
nmzik b46e141762 dev: add Linux overrides to VS Code launch configurations 2026-08-29 03:55:29 +04:00
nmzik cfa140fb12 window: create Vulkan surface visible on main thread 2026-08-29 01:31:02 +04:00
68 changed files with 2127 additions and 653 deletions
+47 -22
View File
@@ -208,34 +208,66 @@ jobs:
run: |
archive="$RUNNER_TEMP/MoltenVK-macos.tar"
package="$RUNNER_TEMP/MoltenVK"
install_dir="_Build/macos/install"
app_dir="$install_dir/KytyPS5.app"
moltenvk="$install_dir/libMoltenVK.dylib"
curl --fail --location --retry 3 \
--output "$archive" \
"https://github.com/KhronosGroup/MoltenVK/releases/download/$MOLTENVK_VERSION/MoltenVK-macos.tar"
echo "$MOLTENVK_SHA256 $archive" | shasum -a 256 --check
tar -xf "$archive" -C "$RUNNER_TEMP"
test -d "$app_dir"
install -m 755 \
"$package/MoltenVK/dynamic/dylib/macOS/libMoltenVK.dylib" \
_Build/macos/install/libMoltenVK.dylib
install -m 644 "$package/LICENSE" _Build/macos/install/LICENSE.MoltenVK
codesign --force --sign - --timestamp=none _Build/macos/install/libMoltenVK.dylib
"$moltenvk"
install -m 644 "$package/LICENSE" "$install_dir/LICENSE.MoltenVK"
codesign --force --sign - --timestamp=none "$moltenvk"
install -d -m 755 "$app_dir/Contents/Frameworks" "$app_dir/Contents/Resources"
install -m 755 "$moltenvk" "$app_dir/Contents/Frameworks/libMoltenVK.dylib"
install -m 644 "$package/LICENSE" "$app_dir/Contents/Resources/LICENSE.MoltenVK"
codesign --force --sign - --timestamp=none "$app_dir"
- name: Verify artifacts
shell: bash
run: |
file _Build/macos/install/launcher
file _Build/macos/install/kyty_emulator
file _Build/macos/install/libMoltenVK.dylib
lipo _Build/macos/install/launcher -verify_arch x86_64
lipo _Build/macos/install/kyty_emulator -verify_arch x86_64
lipo _Build/macos/install/libMoltenVK.dylib -verify_arch x86_64
codesign --verify --strict _Build/macos/install/kyty_emulator
codesign --verify --strict _Build/macos/install/libMoltenVK.dylib
install_dir="_Build/macos/install"
app_dir="$install_dir/KytyPS5.app"
test -d "$app_dir"
for binary in \
"$install_dir/kyty_emulator" \
"$install_dir/libMoltenVK.dylib" \
"$app_dir/Contents/MacOS/KytyPS5" \
"$app_dir/Contents/MacOS/kyty_emulator" \
"$app_dir/Contents/Frameworks/libMoltenVK.dylib"; do
file "$binary"
lipo "$binary" -verify_arch x86_64
done
for binary in \
"$install_dir/kyty_emulator" \
"$install_dir/libMoltenVK.dylib" \
"$app_dir/Contents/MacOS/KytyPS5" \
"$app_dir/Contents/MacOS/kyty_emulator" \
"$app_dir/Contents/Frameworks/libMoltenVK.dylib"; do
codesign --verify --strict "$binary"
done
codesign --verify --deep --strict --verbose=2 "$app_dir"
- name: Package macOS artifacts
shell: bash
run: |
install_dir="_Build/macos/install"
archive="$RUNNER_TEMP/KytyPS5-macOS-x86_64.zip"
(
cd "$install_dir"
zip -qry "$archive" .
)
unzip -t "$archive"
- name: Upload macOS artifacts
uses: actions/upload-artifact@v4
with:
name: KytyPS5-macOS-x86_64
path: _Build/macos/install/**
path: ${{ runner.temp }}/KytyPS5-macOS-x86_64.zip
if-no-files-found: error
linux:
@@ -424,16 +456,9 @@ jobs:
if: needs.macos.result == 'success'
shell: bash
run: |
macos_dir="artifacts/KytyPS5-macOS-x86_64"
test -d "$macos_dir"
chmod a+x \
"$macos_dir/launcher" \
"$macos_dir/kyty_emulator" \
"$macos_dir/libMoltenVK.dylib"
(
cd "$macos_dir"
zip -r "$GITHUB_WORKSPACE/$RELEASE_NAME-macOS-x86_64.zip" .
)
macos_archive="artifacts/KytyPS5-macOS-x86_64/KytyPS5-macOS-x86_64.zip"
test -f "$macos_archive"
cp "$macos_archive" "$RELEASE_NAME-macOS-x86_64.zip"
- name: Package Linux build
if: needs.linux.result == 'success'
+1
View File
@@ -5,3 +5,4 @@ build/
_Build/vscode-clang/
_Build/
cmake-build-debug/
_PipelineCache/
+37 -3
View File
@@ -10,7 +10,14 @@
"stopAtEntry": false,
"cwd": "${workspaceFolder}\\_Build\\vscode-clang\\launcher",
"environment": [],
"externalConsole": false
"externalConsole": false,
"linux": {
"type": "cppdbg",
"program": "${workspaceFolder}/_Build/vscode-clang/launcher/launcher",
"cwd": "${workspaceFolder}/_Build/vscode-clang/launcher",
"MIMode": "gdb",
"miDebuggerPath": "/usr/bin/gdb"
}
},
{
"name": "Debug kyty_emulator",
@@ -25,13 +32,40 @@
"stopAtEntry": false,
"cwd": "${workspaceFolder}\\_Build\\vscode-clang",
"environment": [],
"externalConsole": true
"externalConsole": true,
"linux": {
"type": "cppdbg",
"program": "${workspaceFolder}/_Build/vscode-clang/kyty_emulator",
"cwd": "${workspaceFolder}/_Build/vscode-clang",
"externalConsole": false,
"MIMode": "gdb",
"miDebuggerPath": "/usr/bin/gdb",
"setupCommands": [
{
"description": "Pass Kyty's internal signals to its handlers",
"text": "handle SIG37 SIGSEGV SIGBUS SIGILL nostop noprint pass",
"ignoreFailures": false
}
]
}
},
{
"name": "Attach to running kyty_emulator",
"type": "cppvsdbg",
"request": "attach",
"processId": "${command:pickProcess}"
"processId": "${command:pickProcess}",
"linux": {
"type": "cppdbg",
"MIMode": "gdb",
"miDebuggerPath": "/usr/bin/gdb",
"setupCommands": [
{
"description": "Pass Kyty's internal signals to its handlers",
"text": "handle SIG37 SIGSEGV SIGBUS SIGILL nostop noprint pass",
"ignoreFailures": false
}
]
}
}
]
}
+10
View File
@@ -524,6 +524,16 @@ if(NOT KYTY_CLANG_CL)
endif()
if(BUILD_TESTING)
add_test(NAME emulator_user_name_cli
COMMAND $<TARGET_FILE:kyty_emulator> --help --user-name 1234567890123456)
add_test(NAME emulator_user_name_too_long
COMMAND $<TARGET_FILE:kyty_emulator> --help --user-name 12345678901234567)
set_tests_properties(emulator_user_name_too_long PROPERTIES WILL_FAIL TRUE)
add_test(NAME emulator_present_mode_cli
COMMAND $<TARGET_FILE:kyty_emulator> --help --present-mode Immediate)
add_test(NAME emulator_present_mode_invalid
COMMAND $<TARGET_FILE:kyty_emulator> --help --present-mode Invalid)
set_tests_properties(emulator_present_mode_invalid PROPERTIES WILL_FAIL TRUE)
add_test(NAME ime_dialog COMMAND $<TARGET_FILE:ime_dialog_tests>)
add_test(NAME shader_cfg COMMAND $<TARGET_FILE:shader_cfg_tests>)
add_test(NAME scalar_provenance COMMAND $<TARGET_FILE:scalar_provenance_tests>)
+22 -7
View File
@@ -220,17 +220,23 @@ cmake --install _Build/macos --prefix _Build/macos/install
```
The build re-signs `kyty_emulator` with the JIT entitlements it needs to execute translated
guest code; no manual signing step is required.
guest code; no manual signing step is required. When the launcher is built, the install
also produces `_Build/macos/install/KytyPS5.app` — double-click to launch the GUI.
A flat `kyty_emulator` is kept for CLI usage.
Vulkan comes from MoltenVK. Download `MoltenVK-macos.tar` from the
[MoltenVK releases](https://github.com/KhronosGroup/MoltenVK/releases), then copy
`MoltenVK/dynamic/dylib/macOS/libMoltenVK.dylib` next to `kyty_emulator` and ad-hoc sign it:
`MoltenVK/dynamic/dylib/macOS/libMoltenVK.dylib` next to the flat `kyty_emulator`
(and, for the bundle, into `KytyPS5.app/Contents/Frameworks/`) and ad-hoc sign it:
```bash
codesign --force --sign - _Build/macos/install/libMoltenVK.dylib
# For the bundle (if present):
codesign --force --sign - _Build/macos/install/KytyPS5.app/Contents/Frameworks/libMoltenVK.dylib
codesign --force --sign - _Build/macos/install/KytyPS5.app
```
Release archives already include a signed `libMoltenVK.dylib`.
Release archives already include a signed `libMoltenVK.dylib` (both flat and inside the bundle).
### Regression tests
@@ -274,6 +280,10 @@ To use the graphical launcher:
./_Build/linux/install/launcher
```
```bash
open _Build/macos/install/KytyPS5.app # or double-click in Finder
```
On first launch, add one or more game folders in the global settings. The launcher searches those
folders recursively for game directories containing `eboot.bin`. Select a detected game and run it
from the game list.
@@ -288,12 +298,17 @@ The emulator can also be started directly with a legally obtained game directory
./_Build/linux/install/kyty_emulator --game "/games/ExampleGame"
```
On macOS, point SDL at the MoltenVK library explicitly; the hardened runtime prevents it from
being picked up from the executable's directory:
On macOS, the adjacent flat or app-bundled `libMoltenVK.dylib` is found automatically; no
environment variable is required:
```bash
cd _Build/macos/install
SDL_VULKAN_LIBRARY="$PWD/libMoltenVK.dylib" ./kyty_emulator --game "/games/ExampleGame"
./_Build/macos/install/kyty_emulator --game "/games/ExampleGame"
```
To override the Vulkan loader, set `SDL_VULKAN_LIBRARY`:
```bash
SDL_VULKAN_LIBRARY=/path/to/libMoltenVK.dylib ./kyty_emulator --game "/games/ExampleGame"
```
Run `kyty_emulator --help` to see the available graphics, logging, validation, profiling, and
+14
View File
@@ -21,6 +21,8 @@ void Shutdown() {
void Load(const ConfigOptions& cfg) {
EXIT_IF(g_config == nullptr);
EXIT_IF(cfg.user_name.empty() || cfg.user_name.size() > MAX_USER_NAME_LENGTH);
EXIT_IF(!IsConfiguredUserIdValid(cfg.user_id));
*g_config = cfg;
}
@@ -33,6 +35,18 @@ uint32_t GetScreenHeight() {
return g_config->screen_height;
}
const std::string& GetUserName() {
return g_config->user_name;
}
int32_t GetUserId() {
return g_config->user_id;
}
PresentMode GetPresentMode() {
return g_config->present_mode;
}
bool FullscreenEnabled() {
return g_config->fullscreen_enabled;
}
+18 -1
View File
@@ -3,6 +3,7 @@
#include "common/common.h"
#include <cstddef>
#include <filesystem>
#include <string>
#include <vector>
@@ -26,14 +27,27 @@ enum class ProfilerDirection { None, Network };
enum class OutputDirection { Silent, Console, File };
enum class PresentMode { Fifo, Mailbox, Immediate };
using Keymap = std::vector<std::string>;
constexpr uint32_t DEFAULT_CONSOLE_LANGUAGE = 1;
constexpr uint32_t MAX_CONSOLE_LANGUAGE = 29;
constexpr std::size_t MAX_USER_NAME_LENGTH = 16;
constexpr int32_t DEFAULT_USER_ID = 1000;
constexpr bool IsConfiguredUserIdValid(int32_t user_id) {
constexpr int32_t USER_ID_EVERYONE = 0xfe;
constexpr int32_t USER_ID_SYSTEM = 0xff;
return user_id >= 0 && user_id != USER_ID_EVERYONE && user_id != USER_ID_SYSTEM;
}
struct ConfigOptions {
uint32_t screen_width = 1280;
uint32_t screen_height = 720;
std::string user_name = "Kyty";
int32_t user_id = DEFAULT_USER_ID;
PresentMode present_mode = PresentMode::Fifo;
bool fullscreen_enabled = false;
uint32_t vblank_frequency = 60;
uint32_t console_language = DEFAULT_CONSOLE_LANGUAGE;
@@ -45,7 +59,7 @@ struct ConfigOptions {
bool command_buffer_dump_enabled = false;
std::filesystem::path command_buffer_dump_folder = "_Buffers";
bool graphics_debug_dump_enabled = false;
OutputDirection printf_direction = OutputDirection::Console;
OutputDirection printf_direction = OutputDirection::Silent;
std::filesystem::path printf_output_file = "_kyty.txt";
ProfilerDirection profiler_direction = ProfilerDirection::None;
bool spirv_debug_printf_enabled = false;
@@ -63,6 +77,9 @@ void Load(const ConfigOptions& cfg);
uint32_t GetScreenWidth();
uint32_t GetScreenHeight();
const std::string& GetUserName();
int32_t GetUserId();
PresentMode GetPresentMode();
bool FullscreenEnabled();
uint32_t GetVblankFrequency();
uint32_t GetConsoleLanguage();
+3 -6
View File
@@ -25,9 +25,6 @@
#ifdef CopyFile
#undef CopyFile
#endif
#ifdef MoveFile
#undef MoveFile
#endif
#endif
namespace Common {
@@ -544,14 +541,14 @@ bool File::CopyFile(const std::filesystem::path& src,
return SysFileCopyFile(src, dst);
}
bool File::MoveFile(const std::filesystem::path& src,
const std::filesystem::path& dst) // @suppress("Member declaration not found")
bool File::RenameFile(const std::filesystem::path& src,
const std::filesystem::path& dst) // @suppress("Member declaration not found")
{
if (IsFileExisting(dst)) {
DeleteFile(dst); // @suppress("Invalid arguments")
}
return SysFileMoveFile(src, dst);
return SysFileRenameFile(src, dst);
}
void File::RemoveReadonly(const std::filesystem::path& name) {
+1 -4
View File
@@ -20,9 +20,6 @@
#ifdef CopyFile
#undef CopyFile
#endif
#ifdef MoveFile
#undef MoveFile
#endif
#endif
namespace Common {
@@ -107,7 +104,7 @@ public:
static std::vector<FindInfo> FindFiles(const std::filesystem::path& path);
static std::vector<DirEntry> GetDirEntries(const std::filesystem::path& path);
static bool CopyFile(const std::filesystem::path& src, const std::filesystem::path& dst);
static bool MoveFile(const std::filesystem::path& src, const std::filesystem::path& dst);
static bool RenameFile(const std::filesystem::path& src, const std::filesystem::path& dst);
static void RemoveReadonly(const std::filesystem::path& name);
KYTY_CLASS_NO_COPY(File);
+1 -1
View File
@@ -80,7 +80,7 @@ void SysFileFindFiles(const std::filesystem::path& path, std::vector<sys_file_fi
// NOLINTNEXTLINE(google-runtime-references)
void SysFileGetDents(const std::filesystem::path& path, std::vector<sys_dir_entry_t>& out);
bool SysFileCopyFile(const std::filesystem::path& src, const std::filesystem::path& dst);
bool SysFileMoveFile(const std::filesystem::path& src, const std::filesystem::path& dst);
bool SysFileRenameFile(const std::filesystem::path& src, const std::filesystem::path& dst);
void SysFileRemoveReadonly(const std::filesystem::path& name);
#endif /* KYTY_COMMON_PLATFORM_SYSFILEIO_H_ */
+1 -1
View File
@@ -693,7 +693,7 @@ bool SysFileCopyFile(const std::filesystem::path& src, const std::filesystem::pa
!error;
}
bool SysFileMoveFile(const std::filesystem::path& src, const std::filesystem::path& dst) {
bool SysFileRenameFile(const std::filesystem::path& src, const std::filesystem::path& dst) {
auto real_src = get_internal_name(src);
auto real_dst = get_internal_name(dst);
+1 -1
View File
@@ -565,7 +565,7 @@ bool SysFileCopyFile(const std::filesystem::path& src, const std::filesystem::pa
return CopyFileW(src_wide.c_str(), dst_wide.c_str(), FALSE) != 0;
}
bool SysFileMoveFile(const std::filesystem::path& src, const std::filesystem::path& dst) {
bool SysFileRenameFile(const std::filesystem::path& src, const std::filesystem::path& dst) {
auto src_wide = src.wstring();
auto dst_wide = dst.wstring();
return MoveFileW(src_wide.c_str(), dst_wide.c_str()) != 0;
+4
View File
@@ -200,6 +200,10 @@ void Run(const RunOptions& options) {
int ok = atexit(KytyClose);
EXIT_NOT_IMPLEMENTED(ok != 0);
// Guest threads are still running, so skip KytyClose() and only flush emergency state.
ok = at_quick_exit(Common::Subsystems::EmergencyShutdownActive);
EXIT_NOT_IMPLEMENTED(ok != 0);
Libs::LibKernel::FileSystem::Mount(options.app0_dir, "/app0");
Libs::LibKernel::FileSystem::Mount(options.app0_dir, "/hostapp");
+5 -2
View File
@@ -1,5 +1,6 @@
set(KYTY_GIT_VERSION "unknown")
set(KYTY_GIT_HASH "unknown")
set(KYTY_GIT_REVISION "unknown")
if(GIT_EXECUTABLE)
execute_process(
COMMAND "${GIT_EXECUTABLE}" describe --tags --always --dirty
@@ -14,16 +15,18 @@ if(GIT_EXECUTABLE)
endif()
execute_process(
COMMAND "${GIT_EXECUTABLE}" rev-parse --short=7 HEAD
COMMAND "${GIT_EXECUTABLE}" rev-parse HEAD
WORKING_DIRECTORY "${GIT_WORKING_DIRECTORY}"
OUTPUT_VARIABLE KYTY_GIT_HASH
OUTPUT_VARIABLE KYTY_GIT_REVISION
OUTPUT_STRIP_TRAILING_WHITESPACE
RESULT_VARIABLE GIT_HASH_RESULT
ERROR_QUIET
)
if(NOT GIT_HASH_RESULT EQUAL 0)
set(KYTY_GIT_HASH "unknown")
set(KYTY_GIT_REVISION "unknown")
else()
string(SUBSTRING "${KYTY_GIT_REVISION}" 0 7 KYTY_GIT_HASH)
execute_process(
COMMAND "${GIT_EXECUTABLE}" diff-index --quiet HEAD --
WORKING_DIRECTORY "${GIT_WORKING_DIRECTORY}"
@@ -219,8 +219,9 @@ void RenderExecutor::ResolveRenderColorTarget(uint64_t submit_id, CommandBuffer&
rt.attrib.num_fragments, bytes_per_element, rt.cmask.addr, rt.fmask.addr,
rt.dcc_addr.addr);
}
if ((standard64 || depth_tile) && (rt.attrib3.dimension != 1 || rt.attrib3.depth != 0 ||
view.base_layer != 0 || view.image_layers != 1)) {
if ((standard64 || depth_tile) &&
(rt.attrib3.dimension != 1 || rt.attrib3.depth != 0 ||
(depth_tile && (view.base_layer != 0 || view.image_layers != 1)))) {
EXIT("unsupported 64KB texture-tiled render-target view: dimension=%u depth=%u"
" layer=%u/%u\n",
rt.attrib3.dimension, rt.attrib3.depth, view.base_layer, view.image_layers);
-9
View File
@@ -84,17 +84,8 @@ void uc_print(const char* func, const HW::UserConfig& uc) {
}
void uc_check(const HW::UserConfig& uc) {
const auto& ge_cntl = uc.GetGeControl();
const auto& user_en = uc.GetGeUserVgprEn();
if (ge_cntl.primitive_group_size > 0x0040) {
LOGF("\t warning: unsupported GE_CNTL primitive_group_size = 0x%04" PRIx16 "\n",
ge_cntl.primitive_group_size);
}
if (ge_cntl.vertex_group_size > 0x0040) {
LOGF("\t warning: unsupported GE_CNTL vertex_group_size = 0x%04" PRIx16 "\n",
ge_cntl.vertex_group_size);
}
EXIT_NOT_IMPLEMENTED(user_en.vgpr1 != false);
EXIT_NOT_IMPLEMENTED(user_en.vgpr2 != false);
EXIT_NOT_IMPLEMENTED(user_en.vgpr3 != false);
@@ -1,6 +1,7 @@
#include "graphics/host_gpu/renderer/pipeline/pipelineCache.h"
#include "common/assert.h"
#include "common/file.h"
#include "common/logging/log.h"
#include "common/profiler.h"
#include "graphics/guest_gpu/hardwareContext.h"
@@ -11,18 +12,64 @@
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/renderer/renderContext.h"
#include "graphics/shader/shaderCompiler.h"
#include "kytyGitVersion.h"
#include "loader/systemContent.h"
#include <algorithm>
#include <atomic>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <limits>
#include <span>
#include <string_view>
#include <utility>
#include <vector>
#include <fmt/format.h>
#include <xxhash.h>
namespace Libs::Graphics {
namespace {
std::string DriverCacheSignature(const vk::PhysicalDeviceProperties& properties) {
constexpr char hex[] = "0123456789abcdef";
std::string uuid(VK_UUID_SIZE * 2, '0');
for (size_t i = 0; i < VK_UUID_SIZE; i++) {
uuid[i * 2] = hex[properties.pipelineCacheUUID[i] >> 4u];
uuid[i * 2 + 1] = hex[properties.pipelineCacheUUID[i] & 0xfu];
}
return fmt::format("KytyPC1:{}:{:08x}:{:08x}:{:08x}:{}\n", KYTY_GIT_REVISION,
properties.vendorID, properties.deviceID, properties.driverVersion, uuid);
}
std::string PipelineCacheTitleId() {
std::string title_id;
if ((!Loader::SystemContentParamSfoGetString("TITLE_ID", &title_id) || title_id.empty()) &&
(!Loader::SystemContentParamSfoGetString("CONTENT_ID", &title_id) || title_id.empty())) {
return {};
}
if (!std::ranges::all_of(title_id, [](unsigned char c) {
return std::isalnum(c) != 0 || c == '-' || c == '_';
})) {
return {};
}
return title_id;
}
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();
}
void NormalizeStaticParamsForDynamicState(PipelineStaticParameters& static_params) {
static_params.viewport_scale[0] = 0.5f;
static_params.viewport_scale[1] = 0.5f;
@@ -119,9 +166,11 @@ struct PipelineCache::ProgramCache {
PipelineCache::PipelineCache(GraphicContext& graphics)
: m_graphics(graphics), m_program_cache(std::make_unique<ProgramCache>(graphics.device)) {
EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread());
InitializeDriverCache();
}
PipelineCache::~PipelineCache() {
Save();
auto destroy = [this](const auto& pipelines) {
for (const auto& [key, pipeline]: pipelines) {
(void)key;
@@ -138,6 +187,155 @@ PipelineCache::~PipelineCache() {
m_graphics.device.destroyShaderModule(permutation.handle.module, nullptr);
}
}
if (m_driver_cache != nullptr) {
m_graphics.device.destroyPipelineCache(m_driver_cache, nullptr);
}
}
void PipelineCache::InitializeDriverCache() {
const auto title_id = PipelineCacheTitleId();
if (title_id.empty()) {
return;
}
if (KYTY_BUILD != KYTY_BUILD_RELEASE) {
PipelineCacheLog("Vulkan pipeline cache: disabled (non-Release build)");
return;
}
const std::string_view git_hash = KYTY_GIT_HASH;
const std::string_view git_revision = KYTY_GIT_REVISION;
if (git_hash == "unknown" || git_revision == "unknown") {
PipelineCacheLog("Vulkan pipeline cache: disabled (unknown git revision)");
return;
}
if (git_hash.ends_with("-dirty")) {
PipelineCacheLog("Vulkan pipeline cache: disabled (dirty build)");
return;
}
m_driver_cache_path = std::filesystem::path("_PipelineCache") / (title_id + ".bin");
const auto path = Common::PathToString(m_driver_cache_path);
const bool cache_exists = Common::File::IsFileExisting(m_driver_cache_path);
if (cache_exists) {
PipelineCacheLog("Vulkan pipeline cache: loading {}", path);
} else {
PipelineCacheLog("Vulkan pipeline cache: initializing {}", path);
}
std::vector<uint8_t> initial_data;
if (cache_exists) {
Common::File file(m_driver_cache_path, Common::File::Mode::Read);
const auto file_size = file.IsInvalid() ? 0 : file.Size();
const auto signature = DriverCacheSignature(m_graphics.GetPhysicalDeviceProperties());
if (file_size >= signature.size() + sizeof(uint64_t) &&
file_size <= std::numeric_limits<uint32_t>::max()) {
std::string cached_signature(signature.size(), '\0');
uint64_t payload_hash = 0;
initial_data.resize(file_size - signature.size() - sizeof(payload_hash));
uint32_t signature_read = 0;
uint32_t hash_read = 0;
uint32_t payload_read = 0;
file.Read(cached_signature.data(), static_cast<uint32_t>(cached_signature.size()),
&signature_read);
file.Read(&payload_hash, sizeof(payload_hash), &hash_read);
file.Read(initial_data.data(), static_cast<uint32_t>(initial_data.size()), &payload_read);
file.Close();
if (signature_read != cached_signature.size() || hash_read != sizeof(payload_hash) ||
payload_read != initial_data.size() || cached_signature != signature ||
XXH3_64bits(initial_data.data(), initial_data.size()) != payload_hash) {
initial_data.clear();
PipelineCacheLog(
"Vulkan pipeline cache: invalidating {} (driver, emulator, or data mismatch)",
path);
}
} else {
file.Close();
PipelineCacheLog("Vulkan pipeline cache: invalidating {} (invalid file size)",
path);
}
}
vk::PipelineCacheCreateInfo create {};
create.sType = vk::StructureType::ePipelineCacheCreateInfo;
create.initialDataSize = initial_data.size();
create.pInitialData = initial_data.empty() ? nullptr : initial_data.data();
auto result = m_graphics.device.createPipelineCache(&create, nullptr, &m_driver_cache);
if (result != vk::Result::eSuccess && !initial_data.empty()) {
PipelineCacheLog("Vulkan pipeline cache: driver rejected {} ({}); starting empty", path,
VulkanToString(result));
initial_data.clear();
create.initialDataSize = 0;
create.pInitialData = nullptr;
result = m_graphics.device.createPipelineCache(&create, nullptr, &m_driver_cache);
}
if (result != vk::Result::eSuccess) {
PipelineCacheLog("Vulkan pipeline cache: disabled ({})", VulkanToString(result));
m_driver_cache = nullptr;
return;
}
if (!initial_data.empty()) {
PipelineCacheLog("Vulkan pipeline cache: loaded {} bytes from {}", initial_data.size(),
path);
} else {
PipelineCacheLog("Vulkan pipeline cache: initialized empty");
}
}
void PipelineCache::Save() {
Common::LockGuard lock(m_mutex);
if (m_driver_cache == nullptr) {
return;
}
size_t size = 0;
vk::Result result;
std::vector<uint8_t> payload;
for (uint32_t attempt = 0; attempt < 3; attempt++) {
size = 0;
result = m_graphics.device.getPipelineCacheData(m_driver_cache, &size, nullptr);
if (result != vk::Result::eSuccess || size == 0 ||
size > std::numeric_limits<uint32_t>::max()) {
break;
}
payload.resize(size);
result = m_graphics.device.getPipelineCacheData(m_driver_cache, &size, payload.data());
if (result != vk::Result::eIncomplete) {
break;
}
}
if (result != vk::Result::eSuccess || size == 0 ||
size > std::numeric_limits<uint32_t>::max()) {
PipelineCacheLog("Vulkan pipeline cache: save failed ({}, {} bytes)",
VulkanToString(result), size);
return;
}
payload.resize(size);
auto prefix = DriverCacheSignature(m_graphics.GetPhysicalDeviceProperties());
const auto payload_hash = XXH3_64bits(payload.data(), payload.size());
prefix.append(reinterpret_cast<const char*>(&payload_hash), sizeof(payload_hash));
if (!Common::File::CreateDirectories(m_driver_cache_path.parent_path())) {
PipelineCacheLog("Vulkan pipeline cache: failed to create cache directory");
return;
}
auto temp_path = m_driver_cache_path;
temp_path += ".tmp";
Common::File file;
uint32_t prefix_written = 0;
uint32_t payload_written = 0;
if (file.Create(temp_path)) {
file.Write(prefix.data(), static_cast<uint32_t>(prefix.size()), &prefix_written);
file.Write(payload.data(), static_cast<uint32_t>(payload.size()), &payload_written);
}
const bool flushed = !file.IsInvalid() && file.Flush();
file.Close();
if (prefix_written != prefix.size() || payload_written != payload.size() || !flushed ||
!Common::File::RenameFile(temp_path, m_driver_cache_path)) {
PipelineCacheLog("Vulkan pipeline cache: failed to write {}",
Common::PathToString(m_driver_cache_path));
return;
}
PipelineCacheLog("Vulkan pipeline cache: saved {} bytes to {}", payload.size(),
Common::PathToString(m_driver_cache_path));
m_graphics.device.destroyPipelineCache(m_driver_cache, nullptr);
m_driver_cache = nullptr;
}
ShaderProgram PipelineCache::GetVertexProgram(const HW::VertexShaderInfo& regs,
@@ -324,7 +522,7 @@ PipelineCache::GraphicsPipeline& PipelineCache::CreateGraphicsPipeline(
auto cached = std::make_unique<GraphicsPipeline>(p);
LogPipelineTrace("CreatePipelineInternal begin", vs_id, ps_id);
CreatePipelineInternal(m_graphics, *cached, rendering, vs_input_info, vertex_program.module,
ps_input_info, pixel_program.module, static_params);
ps_input_info, pixel_program.module, static_params, m_driver_cache);
LogPipelineTrace("CreatePipelineInternal done", vs_id, ps_id);
EXIT_NOT_IMPLEMENTED(cached->pipeline == nullptr);
@@ -360,7 +558,7 @@ PipelineCache::CreateComputePipeline(ShaderComputeInputInfo& input_info,
}
auto cached = std::make_unique<ComputePipeline>(p);
CreatePipelineInternal(m_graphics, *cached, input_info, compute_program.module);
CreatePipelineInternal(m_graphics, *cached, input_info, compute_program.module, m_driver_cache);
EXIT_NOT_IMPLEMENTED(cached->pipeline == nullptr);
EXIT_NOT_IMPLEMENTED(cached->pipeline_layout == nullptr);
@@ -10,6 +10,7 @@
#include "graphics/shader/shader.h"
#include <cstddef>
#include <filesystem>
#include <memory>
#include <span>
#include <type_traits>
@@ -109,6 +110,7 @@ public:
explicit PipelineCache(GraphicContext& graphics);
~PipelineCache();
KYTY_CLASS_NO_COPY(PipelineCache);
void Save();
struct Pipeline {
vk::PipelineLayout pipeline_layout = nullptr;
@@ -214,12 +216,16 @@ private:
GraphicContext& m_graphics;
std::unique_ptr<ProgramCache> m_program_cache;
vk::PipelineCache m_driver_cache = nullptr;
std::filesystem::path m_driver_cache_path;
std::unordered_map<GraphicsPipelineKey, std::unique_ptr<GraphicsPipeline>,
GraphicsPipelineKeyHash>
m_graphics_pipelines;
std::unordered_map<ComputePipelineKey, std::unique_ptr<ComputePipeline>, ComputePipelineKeyHash>
m_compute_pipelines;
Common::Mutex m_mutex;
void InitializeDriverCache();
};
void LogPipelineTrace(const char* phase, uint64_t vertex_program_id, uint64_t pixel_program_id);
@@ -227,10 +233,11 @@ void CreatePipelineInternal(
GraphicContext& graphics, PipelineCache::GraphicsPipeline& pipeline,
const PipelineRenderingState& rendering, const ShaderVertexInputInfo& vs_input_info,
vk::ShaderModule vertex_module, const ShaderPixelInputInfo* ps_input_info,
vk::ShaderModule pixel_module, const PipelineStaticParameters& static_params);
vk::ShaderModule pixel_module, const PipelineStaticParameters& static_params,
vk::PipelineCache driver_cache);
void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::ComputePipeline& pipeline,
const ShaderComputeInputInfo& input_info,
vk::ShaderModule compute_module);
vk::ShaderModule compute_module, vk::PipelineCache driver_cache);
} // namespace Libs::Graphics
@@ -423,7 +423,8 @@ void CreatePipelineInternal(
GraphicContext& graphics, PipelineCache::GraphicsPipeline& pipeline,
const PipelineRenderingState& rendering, const ShaderVertexInputInfo& vs_input_info,
vk::ShaderModule vertex_module, const ShaderPixelInputInfo* ps_input_info,
vk::ShaderModule pixel_module, const PipelineStaticParameters& static_params) {
vk::ShaderModule pixel_module, const PipelineStaticParameters& static_params,
vk::PipelineCache driver_cache) {
const bool ps_active = ps_input_info != nullptr;
EXIT_IF(vertex_module == nullptr || (ps_active && pixel_module == nullptr));
@@ -940,7 +941,7 @@ void CreatePipelineInternal(
viewport.y, viewport.width, viewport.height, scissor.offset.x, scissor.offset.y,
scissor.extent.width, scissor.extent.height);
}
result = graphics.device.createGraphicsPipelines(nullptr, 1, &pipeline_info, nullptr,
result = graphics.device.createGraphicsPipelines(driver_cache, 1, &pipeline_info, nullptr,
&pipeline.pipeline);
if (graphics_debug_dump_enabled()) {
LOGF("PipelineTrace: vkCreateGraphicsPipelines done result=%s pipeline=%p\n",
@@ -961,7 +962,7 @@ void CreatePipelineInternal(
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::ComputePipeline& pipeline,
const ShaderComputeInputInfo& input_info,
vk::ShaderModule compute_module) {
vk::ShaderModule compute_module, vk::PipelineCache driver_cache) {
EXIT_IF(compute_module == nullptr);
vk::PipelineShaderStageCreateInfo comp_shader_stage_info {};
@@ -1024,7 +1025,8 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::ComputePipe
LOGF("PipelineTrace: vkCreateComputePipelines begin layout=%p\n",
static_cast<void*>(pipeline.pipeline_layout));
result = graphics.device.createComputePipelines(nullptr, 1, &info, nullptr, &pipeline.pipeline);
result = graphics.device.createComputePipelines(driver_cache, 1, &info, nullptr,
&pipeline.pipeline);
LOGF("PipelineTrace: vkCreateComputePipelines done result=%s pipeline=%p\n",
VulkanToString(result).c_str(), static_cast<void*>(pipeline.pipeline));
EXIT_NOT_IMPLEMENTED(result != vk::Result::eSuccess);
@@ -450,15 +450,14 @@ static bool ShouldSkipGeShader(const CommandBuffer& buffer) {
const bool unsupported_stage_mask = (stages != 0 && stages != 0x02002000);
const bool unsupported_gs_stage = (vertex_info.es_regs.data_addr != 0 &&
vertex_info.gs_regs.data_addr != 0 && !ps5_ngg_vertex_path);
const bool ge_group_size =
ge_cntl.primitive_group_size > 0x0040 || ge_cntl.vertex_group_size > 0x0040;
// GE_CNTL group sizes control guest scheduling and do not constrain the host vertex path.
const bool ge_shader_regs =
(sh_regs.m_geNggSubgrpCntl != 0x00000000 && sh_regs.m_geNggSubgrpCntl != 0x00000001) ||
sh_regs.m_vgtGsMaxVertOut != 0x00000000 ||
!is_known_gs_out_prim_type(sh_regs.m_vgtGsOutPrimType) ||
sh_regs.m_geMaxOutputPerSubgroup > 0x00000040;
if (unsupported_stage_mask || unsupported_gs_stage || ge_group_size || ge_shader_regs) {
if (unsupported_stage_mask || unsupported_gs_stage || ge_shader_regs) {
static std::once_flag warning_once;
std::call_once(warning_once, [] {
std::printf("Warning: game uses unsupported graphics pipelines; some draw calls were "
+120 -9
View File
@@ -1,18 +1,24 @@
#include "graphics/presentation/window/hostInput.h"
#include "SDL_error.h"
#include "SDL_events.h"
#include "SDL_keyboard.h"
#include "SDL_keycode.h"
#include "SDL_mouse.h"
#include "SDL_timer.h"
#include "SDL_video.h"
#include "common/assert.h"
#include "common/emulatorConfig.h"
#include "common/logging/log.h"
#include "libs/controller.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdlib>
#include <string>
#include <string_view>
#include <utility>
namespace Libs::Graphics {
@@ -60,6 +66,17 @@ struct Binding {
std::size_t control = INVALID_CONTROL;
};
constexpr int MOUSE_POLL_INTERVAL_MS = 33;
constexpr std::string_view MOUSE_SENSITIVITY = "MouseSensitivity=";
struct MouseJoystickState {
bool enabled = false;
bool output = false;
uint64_t next_poll = 0;
};
MouseJoystickState g_mouse;
std::size_t ControlFromName(std::string_view name) {
const auto info = std::find_if(CONTROL_INFO.begin(), CONTROL_INFO.end(),
[name](const auto& item) { return item.name == name; });
@@ -103,10 +120,17 @@ bool Conflicts(const Binding& first, const Binding& second) {
class InputMap {
public:
InputMap(): m_custom(!Config::GetKeymap().empty()) {
InputMap() {
for (const auto& value: Config::GetKeymap()) {
const std::string_view entry = value;
const auto split = entry.find('=');
if (entry.starts_with(MOUSE_SENSITIVITY)) {
const float sensitivity =
std::strtof(value.c_str() + MOUSE_SENSITIVITY.size(), nullptr);
m_mouse_sensitivity =
std::clamp(std::isfinite(sensitivity) ? sensitivity : 1.0f, 0.1f, 5.0f);
continue;
}
const auto split = entry.find('=');
Binding binding;
if (split != std::string_view::npos) {
@@ -118,9 +142,9 @@ public:
}
}
const bool reserved =
binding.key == SDLK_ESCAPE || binding.key == SDLK_SPACE || binding.key == SDLK_F1 ||
binding.key == SDLK_F11;
const bool reserved = binding.key == SDLK_ESCAPE || binding.key == SDLK_SPACE ||
binding.key == SDLK_F1 || binding.key == SDLK_F7 ||
binding.key == SDLK_F11;
if (binding.control == INVALID_CONTROL || reserved ||
(binding.key == SDLK_UNKNOWN && binding.mouse_button == 0)) {
EXIT("Invalid input mapping: %s\n", value.c_str());
@@ -128,7 +152,8 @@ public:
Add(binding);
}
}
[[nodiscard]] bool Custom() const { return m_custom; }
[[nodiscard]] bool Custom() const { return m_size != 0; }
[[nodiscard]] float MouseSensitivity() const { return m_mouse_sensitivity; }
[[nodiscard]] std::size_t FindKey(int key_code) const {
key_code = NormalizeKey(static_cast<SDL_Keycode>(key_code));
@@ -159,8 +184,8 @@ private:
}
std::array<Binding, CONTROL_INFO.size()> m_bindings {};
std::size_t m_size = 0;
bool m_custom;
std::size_t m_size = 0;
float m_mouse_sensitivity = 1.0f;
};
const InputMap& GetInputMap() {
@@ -281,6 +306,33 @@ void DefaultKeyboardInput(int key_code, bool down) {
default: SetButton(DefaultKeyboardButton(key_code), down); return;
}
}
void MouseToJoystick(int delta_x, int delta_y) {
const double distance = std::hypot(delta_x, delta_y);
const double scale =
std::clamp(distance * GetInputMap().MouseSensitivity() + 16.0, 64.0, 128.0) / distance;
const auto map_axis = [scale](int delta) {
return std::clamp(128 + static_cast<int>(std::lround(delta * scale)), 0, 255);
};
Controller::ControllerRightStick(Controller::HOST_INPUT_CONTROLLER_ID, map_axis(delta_x),
map_axis(delta_y));
}
void CenterMouseStick() {
if (!g_mouse.output) {
return;
}
Controller::ControllerRightStick(Controller::HOST_INPUT_CONTROLLER_ID, 128, 128);
g_mouse.output = false;
}
bool SetRelativeMouseMode(bool enabled) {
if (SDL_SetRelativeMouseMode(enabled ? SDL_TRUE : SDL_FALSE) == 0) {
return true;
}
LOGF("Mouse-to-joystick relative mode failed: %s\n", SDL_GetError());
return false;
}
} // namespace
void HostInputInit() {
@@ -303,4 +355,63 @@ void HostInputMouseButton(uint8_t mouse_button, bool down) {
}
}
void HostInputToggleMouseToJoystick() {
if (g_mouse.enabled) {
SetRelativeMouseMode(false);
CenterMouseStick();
g_mouse = {};
LOGF("Mouse to right stick: disabled\n");
return;
}
if (!SetRelativeMouseMode(true)) {
return;
}
int ignored_x = 0;
int ignored_y = 0;
SDL_GetRelativeMouseState(&ignored_x, &ignored_y);
g_mouse.enabled = true;
g_mouse.next_poll = SDL_GetTicks64() + MOUSE_POLL_INTERVAL_MS;
LOGF("Mouse to right stick: enabled (F7 to release)\n");
}
int PollMouse(uint64_t now_ms) {
if (now_ms < g_mouse.next_poll) {
return static_cast<int>(g_mouse.next_poll - now_ms);
}
g_mouse.next_poll = now_ms + MOUSE_POLL_INTERVAL_MS;
int delta_x = 0;
int delta_y = 0;
SDL_GetRelativeMouseState(&delta_x, &delta_y);
if (delta_x == 0 && delta_y == 0) {
CenterMouseStick();
return MOUSE_POLL_INTERVAL_MS;
}
MouseToJoystick(delta_x, delta_y);
g_mouse.output = true;
return MOUSE_POLL_INTERVAL_MS;
}
bool HostInputWaitEvent(SDL_Event* event) {
if (!g_mouse.enabled || SDL_GetKeyboardFocus() == nullptr) {
CenterMouseStick();
if (SDL_WaitEvent(event) == 0) {
EXIT("%s\n", SDL_GetError());
}
return true;
}
const int timeout_ms = PollMouse(SDL_GetTicks64());
SDL_ClearError();
if (SDL_WaitEventTimeout(event, timeout_ms) != 0) {
return true;
}
if (SDL_GetError()[0] != '\0') {
EXIT("%s\n", SDL_GetError());
}
return false;
}
} // namespace Libs::Graphics
+7 -3
View File
@@ -3,11 +3,15 @@
#include <cstdint>
union SDL_Event;
namespace Libs::Graphics {
void HostInputInit();
void HostInputKey(int key_code, bool down);
void HostInputMouseButton(uint8_t mouse_button, bool down);
void HostInputInit();
void HostInputKey(int key_code, bool down);
void HostInputMouseButton(uint8_t mouse_button, bool down);
void HostInputToggleMouseToJoystick();
[[nodiscard]] bool HostInputWaitEvent(SDL_Event* event);
} // namespace Libs::Graphics
+20 -71
View File
@@ -1,30 +1,9 @@
#include "SDL.h"
#include "SDL_error.h"
#include "SDL_events.h"
#include "SDL_gamecontroller.h"
#include "SDL_hints.h"
#include "SDL_joystick.h"
#include "SDL_keyboard.h"
#include "SDL_keycode.h"
#include "SDL_mouse.h"
#include "SDL_pixels.h"
#include "SDL_rwops.h"
#include "SDL_stdinc.h"
#include "SDL_surface.h"
#include "SDL_thread.h"
#include "SDL_touch.h"
#include "SDL_video.h"
#include "SDL_vulkan.h"
#include "common/assert.h"
#include "common/common.h"
#include "common/emulatorConfig.h"
#include "common/file.h"
#include "common/logging/log.h"
#include "common/profiler.h"
#include "common/stringUtils.h"
#include "common/systemInfo.h"
#include "common/threads.h"
#include "common/timer.h"
#include "graphics/host_gpu/graphicContext.h"
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/renderer/renderContext.h"
@@ -34,19 +13,11 @@
#include "graphics/presentation/presenter.h"
#include "graphics/presentation/videoOut.h"
#include "graphics/presentation/window/windowInternal.h"
#include "libs/controller.h"
#include "loader/systemContent.h"
#include <algorithm>
#include <array>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <fmt/format.h>
#include <limits>
#include <memory>
#include <string>
#include <vector>
#include <vulkan/vk_platform.h>
@@ -334,7 +305,6 @@ public:
private:
void Destroy();
void RefreshSurfaceSize();
WindowContext& m_window;
vk::SwapchainKHR m_handle = nullptr;
@@ -396,12 +366,12 @@ struct Presenter::Impl {
void Swapchain::Create() {
auto& graphics = m_window.graphic_ctx;
EXIT_IF(graphics.screen_width == 0);
EXIT_IF(graphics.screen_height == 0);
EXIT_IF(graphics.device == nullptr);
EXIT_IF(m_window.surface == nullptr);
Common::LockGuard lock(m_window.mutex);
EXIT_IF(graphics.screen_width == 0);
EXIT_IF(graphics.screen_height == 0);
const auto& surface = m_window.surface_capabilities;
EXIT_NOT_IMPLEMENTED(surface.formats.empty());
@@ -459,7 +429,21 @@ void Swapchain::Create() {
create_info.imageSharingMode = vk::SharingMode::eExclusive;
create_info.preTransform = transform;
create_info.compositeAlpha = composite;
create_info.presentMode = vk::PresentModeKHR::eFifo;
switch (Config::GetPresentMode()) {
case Config::PresentMode::Mailbox:
create_info.presentMode = vk::PresentModeKHR::eMailbox;
break;
case Config::PresentMode::Immediate:
create_info.presentMode = vk::PresentModeKHR::eImmediate;
break;
case Config::PresentMode::Fifo:
default: create_info.presentMode = vk::PresentModeKHR::eFifo; break;
}
if (std::find(surface.present_modes.begin(), surface.present_modes.end(),
create_info.presentMode) == surface.present_modes.end()) {
LOGF("warning: requested present mode is unavailable; falling back to Fifo\n");
create_info.presentMode = vk::PresentModeKHR::eFifo;
}
create_info.clipped = VK_TRUE;
RequireVulkanSuccess(graphics.device.createSwapchainKHR(&create_info, nullptr, &m_handle),
"vkCreateSwapchainKHR");
@@ -554,30 +538,18 @@ void Swapchain::Destroy() {
m_render_complete.clear();
}
void Swapchain::RefreshSurfaceSize() {
int width = 0;
int height = 0;
SDL_Vulkan_GetDrawableSize(m_window.window, &width, &height);
if (width > 0 && height > 0) {
m_window.graphic_ctx.screen_width = static_cast<uint32_t>(width);
m_window.graphic_ctx.screen_height = static_cast<uint32_t>(height);
}
m_window.RefreshSurfaceCapabilities();
}
void Swapchain::Recreate(bool surface_lost) {
Destroy();
if (surface_lost) {
#if defined(__APPLE__)
// Surface recreation goes through SDL_Vulkan_CreateSurface, which touches the
// window's view/layer and must run on the main thread on macOS.
m_window.RunOnMainThread([this] { m_window.RecreateSurface(); }, true);
m_window.RunOnMainThread([this] { m_window.RecreateSurface(); });
#else
m_window.RecreateSurface();
#endif
}
RefreshSurfaceSize();
m_window.RefreshSurfaceCapabilities();
Create();
}
@@ -803,29 +775,6 @@ void Presenter::Present(Frame& frame, bool reuse) {
KYTY_PROFILER_FUNCTION();
m_impl->frames.ValidateForPresent(&frame, reuse);
auto& window = m_impl->window;
if (window.window_hidden) {
#if defined(__APPLE__)
// AppKit traps if a window is shown off the main thread; marshal and wait so the
// swapchain below is recreated against a visible window.
window.RunOnMainThread(
[&window] {
window.UpdateIcon();
SDL_ShowWindow(window.window);
SDL_RaiseWindow(window.window);
},
true);
#else
window.UpdateIcon();
SDL_ShowWindow(window.window);
SDL_RaiseWindow(window.window);
#endif
window.window_hidden = false;
m_impl->RecoverSwapchain(Swapchain::Status::Recreate);
}
const auto ime_visual = GetImeVisualState();
auto& swapchain = m_impl->swapchain;
for (uint32_t attempt = 0; attempt < 2; attempt++) {
@@ -848,7 +797,7 @@ void Presenter::Present(Frame& frame, bool reuse) {
}
m_impl->presented_ime_revision.store(ime_visual.revision, std::memory_order_release);
window.UpdateTitle();
m_impl->window.UpdateTitle();
m_impl->frames.Release(&frame, true);
return;
}
@@ -22,7 +22,6 @@
#include "common/logging/log.h"
#include "common/profiler.h"
#include "common/stringUtils.h"
#include "common/systemInfo.h"
#include "common/threads.h"
#include "common/timer.h"
#include "graphics/host_gpu/graphicContext.h"
@@ -943,6 +942,18 @@ void WindowContext::CreateVulkan() {
inst_info.sType = vk::StructureType::eInstanceCreateInfo;
inst_info.pNext = (r.enable_validation_layers ? &dbg_create_info : nullptr);
inst_info.flags = {};
#if defined(__APPLE__)
// MoltenVK requires VK_KHR_portability_enumeration + flag to surface
// portability devices. Without this, enumeratePhysicalDevices hides the
// MoltenVK adapter on some driver versions.
if (HasExtension(r.available_extensions, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME)) {
if (!HasExtension(r.required_extensions, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME)) {
r.required_extensions.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME);
}
inst_info.flags |= vk::InstanceCreateFlagBits::eEnumeratePortabilityKHR;
LOGF("Vulkan instance: enabled %s\n", VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME);
}
#endif
inst_info.pApplicationInfo = &app_info;
inst_info.enabledExtensionCount = static_cast<uint32_t>(r.required_extensions.size());
inst_info.ppEnabledExtensionNames = r.required_extensions.data();
@@ -1037,10 +1048,6 @@ void WindowContext::CreateVulkan() {
}
}
memcpy(device_name, device_properties.deviceName, sizeof(device_name));
std::snprintf(processor_name, sizeof(processor_name), "%s",
Common::GetSystemInfo().ProcessorName.c_str());
VulkanInitSubgroupSizeControl(graphic_ctx.physical_device, graphic_ctx);
graphic_ctx.device = VulkanCreateDevice(graphic_ctx.physical_device, r, queue_family,
@@ -1079,7 +1086,6 @@ void WindowContext::RecreateSurface() {
EXIT("Could not recreate the Vulkan surface: %s\n", SDL_GetError());
}
surface = native_surface;
RefreshSurfaceCapabilities();
}
WindowContext::~WindowContext() {
+47 -32
View File
@@ -3,6 +3,7 @@
#include "SDL.h"
#include "SDL_error.h"
#include "SDL_events.h"
#include "SDL_filesystem.h"
#include "SDL_gamecontroller.h"
#include "SDL_hints.h"
#include "SDL_joystick.h"
@@ -23,12 +24,12 @@
#include "common/file.h"
#include "common/logging/log.h"
#include "common/profiler.h"
#include "common/stringUtils.h"
#include "common/systemInfo.h"
#include "common/threads.h"
#include "common/timer.h"
#include "graphics/host_gpu/graphicContext.h"
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/renderer/renderContext.h"
#include "graphics/host_gpu/vma.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include "graphics/presentation/imeOverlay.h"
@@ -39,10 +40,8 @@
#include "libs/controller.h"
#include "loader/systemContent.h"
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fmt/format.h>
#include <memory>
#include <string>
#include <vector>
@@ -52,8 +51,6 @@
#define STBI_NO_SIMD
#include "stb_image.h"
#include <fmt/format.h>
// IWYU pragma: no_include <intrin.h>
#define KYTY_ENABLE_DEBUG_PRINTF
@@ -197,8 +194,6 @@ std::unique_ptr<WindowContext> g_window;
} // namespace
constexpr const char* KYTY_SDL_WINDOW_CAPTION = "Game";
constexpr uint32_t KYTY_SDL_WINDOW_FLAGS =
(static_cast<uint32_t>(SDL_WINDOW_HIDDEN) | static_cast<uint32_t>(SDL_WINDOW_VULKAN));
constexpr int KYTY_SDL_WINDOWPOS_CENTERED = SDL_WINDOWPOS_CENTERED; /*NOLINT(hicpp-signed-bitwise)*/
static void SetPause(WindowLoopState& game, bool flag) {
@@ -407,6 +402,7 @@ static void GameEventDidEnterForeground(WindowLoopState& game) {
void WindowContext::Resize(uint32_t new_width, uint32_t new_height) {
EXIT_IF(new_width == 0 || new_height == 0);
Common::LockGuard lock(mutex);
graphic_ctx.screen_width = new_width;
graphic_ctx.screen_height = new_height;
}
@@ -511,6 +507,13 @@ void WindowContext::ProcessEvent(double time_s) {
auto& game = loop;
auto* event = &game.event;
EXIT_IF(SDL_GetEventState(SDL_DISPLAYEVENT) != SDL_ENABLE);
if ((event->type == SDL_KEYDOWN || event->type == SDL_KEYUP) &&
event->key.keysym.sym == SDLK_F7) {
if (event->type == SDL_KEYDOWN && event->key.repeat == 0) {
HostInputToggleMouseToJoystick();
}
return;
}
if (ProcessImeInput(*event)) {
return;
}
@@ -728,8 +731,7 @@ void WindowContext::ProcessEvent(double time_s) {
}
}
#if defined(__APPLE__)
void WindowContext::RunOnMainThread(std::function<void()> task, bool wait) {
void WindowContext::RunOnMainThread(std::function<void()> task) {
if (Common::Thread::IsMainThread()) {
task();
return;
@@ -747,9 +749,6 @@ void WindowContext::RunOnMainThread(std::function<void()> task, bool wait) {
event.type = SDL_USEREVENT;
SDL_PushEvent(&event);
if (!wait) {
return;
}
Common::LockGuard lock(main_task_mutex);
while (main_tasks_run < ticket) {
main_task_done.Wait(&main_task_mutex);
@@ -772,7 +771,6 @@ void WindowContext::DrainMainThreadTasks() {
main_tasks_run += tasks.size();
main_task_done.SignalAll();
}
#endif
void WindowContext::Run() {
Common::Timer timer;
@@ -783,9 +781,7 @@ void WindowContext::Run() {
loop.paused.store(false, std::memory_order_release);
while (!loop.need_exit) {
#if defined(__APPLE__)
DrainMainThreadTasks();
#endif
if (loop.paused.load(std::memory_order_acquire)) {
if (!timer.IsPaused()) {
timer.Pause();
@@ -794,8 +790,8 @@ void WindowContext::Run() {
timer.Resume();
}
if (SDL_WaitEvent(&loop.event) == 0) {
EXIT("%s\n", SDL_GetError());
if (!HostInputWaitEvent(&loop.event)) {
continue;
}
ProcessEvent(timer.GetTimeS());
}
@@ -821,11 +817,26 @@ static void WindowCreate(WindowContext& context) {
LOGF("WindowCreate(): width = %d, height = %d\n", width, height);
uint32_t window_flags = KYTY_SDL_WINDOW_FLAGS;
if (Config::FullscreenEnabled()) {
window_flags |= static_cast<uint32_t>(SDL_WINDOW_FULLSCREEN_DESKTOP);
}
uint32_t window_flags = WindowContext::InitialWindowFlags(Config::FullscreenEnabled());
#if defined(__APPLE__)
// SDL loads Vulkan while creating a Vulkan window, so select the bundled
// MoltenVK loader before calling SDL_CreateWindow. Keep an explicit user
// override, and fall back to SDL's normal loader search when no bundle is present.
if (std::getenv("SDL_VULKAN_LIBRARY") == nullptr) {
if (char* base_path = SDL_GetBasePath(); base_path != nullptr) {
const std::string base_path_str = base_path;
SDL_free(base_path);
std::string moltenvk_path = base_path_str + "libMoltenVK.dylib";
if (!Common::File::IsFileExisting(moltenvk_path)) {
moltenvk_path = base_path_str + "../Frameworks/libMoltenVK.dylib";
}
if (Common::File::IsFileExisting(moltenvk_path) &&
SDL_setenv("SDL_VULKAN_LIBRARY", moltenvk_path.c_str(), 0) == 0) {
LOGF("Vulkan loader: %s\n", moltenvk_path.c_str());
}
}
}
// macOS 26 window chrome (CoreUI asset decode, SwiftUI titlebar) has been observed
// throwing NSExceptions under Rosetta during the first CATransaction commit. A
// borderless window skips that machinery entirely.
@@ -836,13 +847,20 @@ static void WindowCreate(WindowContext& context) {
context.window = SDL_CreateWindow(KYTY_SDL_WINDOW_CAPTION, KYTY_SDL_WINDOWPOS_CENTERED,
KYTY_SDL_WINDOWPOS_CENTERED, width, height, window_flags);
context.window_hidden = true;
if (context.window == nullptr) {
EXIT("%s\n", SDL_GetError());
}
SDL_SetWindowResizable(context.window, SDL_FALSE);
context.UpdateIcon();
}
uint32_t WindowContext::InitialWindowFlags(bool fullscreen) noexcept {
auto flags = static_cast<uint32_t>(SDL_WINDOW_VULKAN);
if (fullscreen) {
flags |= static_cast<uint32_t>(SDL_WINDOW_FULLSCREEN_DESKTOP);
}
return flags;
}
Presenter& WindowInit(uint32_t width, uint32_t height) {
@@ -866,6 +884,7 @@ void WindowRun() {
EXIT_IF(g_window == nullptr);
g_window->Run();
g_window->render_context->GetPipelineCache().Save();
}
void WindowShutdown() {
@@ -959,6 +978,7 @@ void WindowContext::UpdateTitle() {
Loader::SystemContentParamSfoGetString("TITLE_ID", title_id, sizeof(title_id));
static bool has_app_ver =
Loader::SystemContentParamSfoGetString("APP_VER", app_ver, sizeof(app_ver));
static const std::string processor_name = Common::GetSystemInfo().ProcessorName;
static uint64_t fps_start = Common::Timer::QueryPerformanceCounter();
static uint64_t frame_num = 0;
static uint64_t fps_frames = 0;
@@ -983,19 +1003,14 @@ void WindowContext::UpdateTitle() {
fps_frames = 0;
}
auto fps = fmt::format(
const auto* device_name = graphic_ctx.GetPhysicalDeviceProperties().deviceName.data();
auto text = fmt::format(
"[{} | {}] {}{}{}{}{}{}[{}] [{}], frame: {}, fps: {:f}", KYTY_BUILD_LABEL, build_type,
(has_title ? title : ""), (has_title ? ", " : ""), (has_title_id ? title_id : ""),
(has_title_id ? ", " : ""), (has_app_ver ? app_ver : ""), (has_app_ver ? " " : ""),
device_name, processor_name, frame_num, current_fps);
#if defined(__APPLE__)
// AppKit traps on title changes off the main thread; fire-and-forget keeps present pacing.
RunOnMainThread([this, fps = std::move(fps)] { SDL_SetWindowTitle(window, fps.c_str()); },
false);
#else
SDL_SetWindowTitle(window, fps.c_str());
#endif
RunOnMainThread([this, text = std::move(text)] { SDL_SetWindowTitle(window, text.c_str()); });
}
} // namespace Libs::Graphics
@@ -9,13 +9,10 @@
#include <atomic>
#include <cstdint>
#include <functional>
#include <memory>
#include <vector>
#if defined(__APPLE__)
#include <functional>
#endif
namespace Libs::Graphics {
class Presenter;
@@ -39,6 +36,7 @@ struct WindowContext {
KYTY_CLASS_NO_COPY(WindowContext);
[[nodiscard]] static vk::PhysicalDeviceVulkan13Features RequiredVulkan13Features() noexcept;
[[nodiscard]] static uint32_t InitialWindowFlags(bool fullscreen) noexcept;
void CreateVulkan();
void RecreateSurface();
void RefreshSurfaceCapabilities();
@@ -49,36 +47,25 @@ struct WindowContext {
void ProcessDisplayEvent(const SDL_DisplayEvent& event);
void ProcessEvent(double time_seconds);
void Run();
#if defined(__APPLE__)
// AppKit only allows window operations (show, icon, title, view/layer changes) on the
// main thread; the present thread marshals them through the SDL main loop with these.
// wait=true blocks until the task has run on the main thread.
void RunOnMainThread(std::function<void()> task, bool wait);
// SDL window operations must complete on the main thread.
void RunOnMainThread(std::function<void()> task);
void DrainMainThreadTasks();
#endif
GraphicContext graphic_ctx;
SDL_Window* window = nullptr;
bool window_hidden = true;
vk::SurfaceKHR surface = nullptr;
SurfaceCapabilities surface_capabilities;
std::unique_ptr<RenderContext> render_context;
std::unique_ptr<Presenter> presenter;
WindowLoopState loop;
char device_name[VK_MAX_PHYSICAL_DEVICE_NAME_SIZE] = {0};
char processor_name[64] = {0};
Common::Mutex mutex;
#if defined(__APPLE__)
Common::Mutex main_task_mutex;
Common::CondVar main_task_done;
std::vector<std::function<void()>> main_tasks; // guarded by main_task_mutex
uint64_t main_tasks_queued = 0; // guarded by main_task_mutex
uint64_t main_tasks_run = 0; // guarded by main_task_mutex
#endif
};
} // namespace Libs::Graphics
@@ -266,7 +266,8 @@ bool AnalyzeProgramRequirements(IR::Program& program, std::string* error) {
requirements.subgroup_local_invocation_id = true;
break;
}
case IR::ValueOpcode::SwizzleU32: {
case IR::ValueOpcode::SwizzleU32:
case IR::ValueOpcode::BpermuteU32: {
MarkBallot();
requirements.subgroup_shuffle = true;
requirements.subgroup_local_invocation_id = true;
@@ -439,6 +439,49 @@ uint32_t UnpackImageGather(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uin
return result;
}
uint32_t EmitOneDimensionalGatherLz(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t pc,
uint32_t coord, bool integer) {
auto& state = ctx.state;
state.builder.RequireCapability(CapabilityImageQuery);
const auto image = LoadSampledImageDescriptor(state, mem, pc, ImageViewKind::Dim1D);
const auto width = state.builder.AllocateId();
state.builder.AddFunction(
{OpImageQuerySizeLod, TypeU32(state), width, image, ConstantU32(state, 0)});
const auto width_f32 = state.builder.AllocateId();
state.builder.AddFunction({OpConvertUToF, TypeF32(state), width_f32, width});
const auto left = state.builder.AllocateId();
state.builder.AddFunction(
{OpExtInst, TypeF32(state), left, GlslStd450(state), GlslFloor,
Binary(state, OpFSub, TypeF32(state),
Binary(state, OpFMul, TypeF32(state), coord, width_f32),
ConstantF32(state, 0x3f000000u))});
const auto sampled = MakeSampledImage(state, mem, pc, ImageViewKind::Dim1D);
const auto vector_type = integer ? TypeU32Vector(state, 4) : TypeF32Vector(state, 4);
const auto scalar_type = integer ? TypeU32(state) : TypeF32(state);
const auto component = ImageConversionFormat(state, mem).format == Prospero::BufferFormat::kInvalid
? ImageGatherComponent(mem.dmask)
: 0u;
uint32_t values[2] {};
for (uint32_t index = 0; index < 2u; index++) {
const auto sample_coord = Binary(
state, OpFDiv, TypeF32(state),
Binary(state, OpFAdd, TypeF32(state), left,
ConstantF32(state, index == 0u ? 0x3f000000u : 0x3fc00000u)),
width_f32);
const auto texel = state.builder.AllocateId();
state.builder.AddFunction({OpImageSampleExplicitLod, vector_type, texel, sampled, sample_coord,
ImageOperandsLodMask, ZeroF32(state)});
values[index] = state.builder.AllocateId();
state.builder.AddFunction(
{OpCompositeExtract, scalar_type, values[index], texel, component});
}
const auto result = state.builder.AllocateId();
state.builder.AddFunction({OpCompositeConstruct, vector_type, result, values[0], values[1],
values[1], values[0]});
return result;
}
uint32_t PackImageTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t texel) {
const auto info = ImageConversionFormat(ctx.state, mem);
if (info.format == Prospero::BufferFormat::kInvalid) return texel;
@@ -613,6 +656,22 @@ bool EmitValueImage(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto coord =
CoordF32(ctx, mem, *address, layout.coord, ImageViewCoordinateComponents(view));
if (op == IR::ValueOpcode::ImageGatherRaw) {
if (view == ImageViewKind::Dim1D) {
if (dref || !HasFlag(mem, Decoder::ImageSampleFlagLevelZero) ||
HasFlag(mem, Decoder::ImageSampleFlagOffset) ||
HasFlag(mem, Decoder::ImageSampleFlagGatherHorizontal)) {
ctx.Fail(inst, "has an unsupported 1D gather variant");
return true;
}
const auto sample = EmitOneDimensionalGatherLz(ctx, mem, pc, coord, integer);
ctx.Define(inst, ResultVector(ctx, UnpackImageGather(ctx, mem, sample), integer,
false, mem, true));
return true;
}
if (view == ImageViewKind::Dim1DArray) {
ctx.Fail(inst, "has an unsupported 1D-array gather");
return true;
}
const auto sampled = MakeSampledImage(state, mem, pc, view);
const auto sample = state.builder.AllocateId();
std::vector<uint32_t> words =
@@ -30,6 +30,19 @@ uint32_t AndCondition(EmitterState& state, uint32_t lhs, uint32_t rhs) {
return Binary(state, OpLogicalAnd, TypeBool(state), lhs, rhs);
}
uint32_t EmitDsMaskedLaneRead(EmitterState& state, uint32_t source, uint32_t target,
uint32_t exec) {
const auto shuffled = state.builder.AllocateId();
state.builder.AddFunction({OpGroupNonUniformShuffle, TypeU32(state), shuffled,
ConstantU32(state, ScopeSubgroup), source, target});
const auto source_exec = state.builder.AllocateId();
state.builder.AddFunction({OpGroupNonUniformShuffle, TypeBool(state), source_exec,
ConstantU32(state, ScopeSubgroup), exec, target});
const auto source_active =
AndCondition(state, source_exec, EmitSubgroupLaneActiveBool(state, target));
return Select(state, TypeU32(state), source_active, shuffled, ConstantU32(state, 0));
}
uint32_t BufferByteAddress(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem,
uint32_t index, uint32_t offset, uint32_t soffset) {
auto& state = ctx.state;
@@ -1181,24 +1194,27 @@ bool EmitValueMemory(ValueEmitContext& ctx, const IR::Inst& inst) {
ctx.Define(inst, AppendConsume(ctx, inst, op == IR::ValueOpcode::DataAppend));
return true;
}
if (op == IR::ValueOpcode::SwizzleU32) {
ctx.state.builder.AddFunction({OpStore, ctx.scratch_u32_variable, ctx.Arg(inst, 0)});
const auto source = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(
{OpLoad, TypeU32(ctx.state), source, ctx.scratch_u32_variable});
const auto target =
EmitDsSwizzleTargetLane(state, EmitSubgroupLocalInvocationId(state),
inst.Arg(1).IsImmediate() ? inst.Arg(1).U32() : 0);
const auto shuffled = state.builder.AllocateId();
state.builder.AddFunction({OpGroupNonUniformShuffle, TypeU32(state), shuffled,
ConstantU32(state, ScopeSubgroup), source, target});
const auto source_exec = state.builder.AllocateId();
state.builder.AddFunction({OpGroupNonUniformShuffle, TypeBool(state), source_exec,
ConstantU32(state, ScopeSubgroup), ctx.Arg(inst, 2), target});
const auto source_active =
AndCondition(state, source_exec, EmitSubgroupLaneActiveBool(state, target));
ctx.Define(inst,
Select(state, TypeU32(state), source_active, shuffled, ConstantU32(state, 0)));
if (op == IR::ValueOpcode::SwizzleU32 || op == IR::ValueOpcode::BpermuteU32) {
uint32_t source = ctx.Arg(inst, 0);
uint32_t target = 0;
if (op == IR::ValueOpcode::SwizzleU32) {
state.builder.AddFunction({OpStore, ctx.scratch_u32_variable, source});
source = state.builder.AllocateId();
state.builder.AddFunction({OpLoad, TypeU32(state), source, ctx.scratch_u32_variable});
target = EmitDsSwizzleTargetLane(state, EmitSubgroupLocalInvocationId(state),
inst.Arg(1).IsImmediate() ? inst.Arg(1).U32() : 0);
} else {
const auto index = Binary(
state, OpBitwiseAnd, TypeU32(state),
Binary(state, OpShiftRightLogical, TypeU32(state), ctx.Arg(inst, 1),
ConstantU32(state, 2)),
ConstantU32(state, 31));
const auto base =
Binary(state, OpBitwiseAnd, TypeU32(state), EmitSubgroupLocalInvocationId(state),
ConstantU32(state, ~31u));
target = Binary(state, OpBitwiseOr, TypeU32(state), base, index);
}
ctx.Define(inst, EmitDsMaskedLaneRead(state, source, target, ctx.Arg(inst, 2)));
return true;
}
return false;
@@ -103,8 +103,11 @@ constexpr MemoryOpcodeInfo DS_OPCODE_LIST[] = {
{0x4du, Opcode::DS_WRITE_B64, 2, 32}, {0x4eu, Opcode::DS_WRITE2_B64, 4, 32},
{0x4fu, Opcode::DS_WRITE2ST64_B64, 4, 32}, {0x76u, Opcode::DS_READ_B64, 2, 32},
{0x77u, Opcode::DS_READ2_B64, 4, 32}, {0x78u, Opcode::DS_READ2ST64_B64, 4, 32},
{0xa1u, Opcode::DS_WRITE_B16_D16_HI, 1, 16},
{0xa6u, Opcode::DS_READ_U16_D16, 1, 16},
{0xa7u, Opcode::DS_READ_U16_D16_HI, 1, 16},
{0xb0u, Opcode::DS_WRITE_ADDTID_B32, 1, 32}, {0xb1u, Opcode::DS_READ_ADDTID_B32, 1, 32},
{0xb3u, Opcode::DS_BPERMUTE_B32, 1, 32},
{0xdeu, Opcode::DS_WRITE_B96, 3, 32}, {0xdfu, Opcode::DS_WRITE_B128, 4, 32},
{0xfeu, Opcode::DS_READ_B96, 3, 32}, {0xffu, Opcode::DS_READ_B128, 4, 32},
};
@@ -147,6 +150,7 @@ bool IsDsWriteOpcode(Opcode opcode) {
switch (opcode) {
case Opcode::DS_WRITE_B8:
case Opcode::DS_WRITE_B16:
case Opcode::DS_WRITE_B16_D16_HI:
case Opcode::DS_WRITE2_B32:
case Opcode::DS_WRITE2ST64_B32:
case Opcode::DS_WRITE2_B64:
@@ -192,6 +196,7 @@ uint32_t DsSourceCount(Opcode opcode) {
case Opcode::DS_WRITE2ST64_B64:
case Opcode::DS_MIN_F32:
case Opcode::DS_MAX_F32: return 3u;
case Opcode::DS_BPERMUTE_B32: return 2u;
case Opcode::DS_READ_ADDTID_B32:
case Opcode::DS_CONSUME:
case Opcode::DS_APPEND: return 0u;
@@ -392,9 +397,10 @@ void DecodeDs(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
SetUnsupported(inst, Family::DS, opcode, "DS swizzle FFT mode is not implemented");
}
if (inst.gds &&
(inst.opcode == Opcode::DS_SWIZZLE_B32 || inst.opcode == Opcode::DS_WRITE_ADDTID_B32 ||
(inst.opcode == Opcode::DS_SWIZZLE_B32 || inst.opcode == Opcode::DS_BPERMUTE_B32 ||
inst.opcode == Opcode::DS_WRITE_ADDTID_B32 ||
inst.opcode == Opcode::DS_READ_ADDTID_B32)) {
SetUnsupported(inst, Family::DS, opcode, "DS swizzle/addtid is available only for LDS");
SetUnsupported(inst, Family::DS, opcode, "DS lane operation is available only for LDS");
}
if (inst.opcode == Opcode::DS_WRITE_ADDTID_B32 && data1 != 0u) {
SetUnsupported(inst, Family::DS, opcode,
@@ -421,13 +427,17 @@ void DecodeDs(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
}
DecodeVectorGpr(vdst, inst.dst);
if (inst.opcode == Opcode::DS_READ_U16_D16) {
if (inst.opcode == Opcode::DS_READ_U16_D16 ||
inst.opcode == Opcode::DS_READ_U16_D16_HI) {
// Native D16 reads update only the selected destination half. Reuse the partial
// destination representation so typed IR translation preserves the upper word.
inst.dst.sdwa_sel = 4u;
// destination representation so typed IR translation preserves the other half.
inst.dst.sdwa_sel = inst.opcode == Opcode::DS_READ_U16_D16 ? 4u : 5u;
}
DecodeVectorGpr(addr, inst.src0);
DecodeVectorGpr(data0, inst.src1);
if (inst.opcode == Opcode::DS_WRITE_B16_D16_HI) {
inst.src1.sdwa_sel = 5u;
}
DecodeVectorGpr(data1, inst.src2);
inst.src_count = DsSourceCount(inst.opcode);
}
@@ -661,6 +661,7 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::DS_MIN_F32:
case Opcode::DS_MAX_F32:
case Opcode::DS_SWIZZLE_B32:
case Opcode::DS_BPERMUTE_B32:
case Opcode::DS_READ_I8:
case Opcode::DS_READ_U8:
case Opcode::DS_READ_I16:
@@ -675,6 +676,7 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::DS_READ_B128:
case Opcode::DS_WRITE_B8:
case Opcode::DS_WRITE_B16:
case Opcode::DS_WRITE_B16_D16_HI:
case Opcode::DS_WRITE2_B32:
case Opcode::DS_WRITE2ST64_B32:
case Opcode::DS_WRITE2_B64:
@@ -179,12 +179,15 @@ enum class Opcode {
V_CEIL_F16,
V_TRUNC_F16,
V_RNDNE_F16,
V_SIN_F16,
V_COS_F16,
V_SIN_F32,
V_COS_F32,
V_NOT_B32,
V_BFREV_B32,
V_FFBH_U32,
V_FFBL_B32,
V_FFBH_I32,
V_ADD_F32,
V_SUB_F32,
V_SUBREV_F32,
@@ -391,6 +394,7 @@ enum class Opcode {
V_CMP_GE_U32,
V_CMP_T_U32,
V_CMP_EQ_I64,
V_CMP_EQ_U64,
V_CMP_GT_U64,
V_CMP_NE_U64,
V_CMPX_NE_I64,
@@ -490,6 +494,7 @@ enum class Opcode {
DS_MIN_F32,
DS_MAX_F32,
DS_SWIZZLE_B32,
DS_BPERMUTE_B32,
DS_CONSUME,
DS_APPEND,
DS_READ_I8,
@@ -497,6 +502,7 @@ enum class Opcode {
DS_READ_I16,
DS_READ_U16,
DS_READ_U16_D16,
DS_READ_U16_D16_HI,
DS_READ2_B32,
DS_READ2ST64_B32,
DS_READ_B32,
@@ -507,6 +513,7 @@ enum class Opcode {
DS_READ_B128,
DS_WRITE_B8,
DS_WRITE_B16,
DS_WRITE_B16_D16_HI,
DS_WRITE2_B32,
DS_WRITE2ST64_B32,
DS_WRITE2_B64,
@@ -59,7 +59,7 @@ constexpr Vop2OpcodeInfo VOP2_OPCODE_LIST[] = {
{0x23u, Opcode::V_MBCNT_LO_U32_B32},
{0x24u, Opcode::V_MBCNT_HI_U32_B32},
{0x25u, Opcode::V_ADD_NC_U32, Vop2SdwaProfile::IntegerPartialDestination},
{0x28u, Opcode::V_ADDC_U32},
{0x28u, Opcode::V_ADDC_U32, Vop2SdwaProfile::IntegerFullDestination},
{0x2au, Opcode::V_SUBREV_CO_CI_U32},
{0x26u, Opcode::V_SUB_NC_U32, Vop2SdwaProfile::IntegerPartialDestination},
{0x27u, Opcode::V_SUBREV_NC_U32, Vop2SdwaProfile::IntegerFullDestination},
@@ -119,6 +119,7 @@ constexpr OpcodeMap VOP1_OPCODE_LIST[] = {
{0x38u, Opcode::V_BFREV_B32},
{0x39u, Opcode::V_FFBH_U32},
{0x3au, Opcode::V_FFBL_B32},
{0x3bu, Opcode::V_FFBH_I32},
{0x3fu, Opcode::V_FREXP_EXP_I32_F32},
{0x40u, Opcode::V_FREXP_MANT_F32},
{0x42u, Opcode::V_MOVRELD_B32},
@@ -136,6 +137,8 @@ constexpr OpcodeMap VOP1_OPCODE_LIST[] = {
{0x5cu, Opcode::V_CEIL_F16},
{0x5du, Opcode::V_TRUNC_F16},
{0x5eu, Opcode::V_RNDNE_F16},
{0x60u, Opcode::V_SIN_F16},
{0x61u, Opcode::V_COS_F16},
};
constexpr auto VOP1_OPS = Detail::MakeOpcodeTable<0x100>(VOP1_OPCODE_LIST);
@@ -169,6 +172,7 @@ constexpr OpcodeMap VOP3_ENCODED_VOP1_OPCODE_LIST[] = {
{0x38u, Opcode::V_BFREV_B32},
{0x39u, Opcode::V_FFBH_U32},
{0x3au, Opcode::V_FFBL_B32},
{0x3bu, Opcode::V_FFBH_I32},
{0x3fu, Opcode::V_FREXP_EXP_I32_F32},
{0x40u, Opcode::V_FREXP_MANT_F32},
{0x42u, Opcode::V_MOVRELD_B32},
@@ -186,6 +190,8 @@ constexpr OpcodeMap VOP3_ENCODED_VOP1_OPCODE_LIST[] = {
{0x5cu, Opcode::V_CEIL_F16},
{0x5du, Opcode::V_TRUNC_F16},
{0x5eu, Opcode::V_RNDNE_F16},
{0x60u, Opcode::V_SIN_F16},
{0x61u, Opcode::V_COS_F16},
};
constexpr auto VOP3_ENCODED_VOP1_OPS = Detail::MakeOpcodeTable<0x80>(VOP3_ENCODED_VOP1_OPCODE_LIST);
@@ -234,7 +240,8 @@ constexpr VopcOpcodeInfo VOPC_OPCODE_LIST[] = {
{0xd1u, Opcode::V_CMPX_LT_U32}, {0xd2u, Opcode::V_CMPX_EQ_U32},
{0xd3u, Opcode::V_CMPX_LE_U32}, {0xd4u, Opcode::V_CMPX_GT_U32},
{0xd5u, Opcode::V_CMPX_NE_U32}, {0xd6u, Opcode::V_CMPX_GE_U32},
{0xe4u, Opcode::V_CMP_GT_U64, false}, {0xe5u, Opcode::V_CMP_NE_U64, false},
{0xe2u, Opcode::V_CMP_EQ_U64, false}, {0xe4u, Opcode::V_CMP_GT_U64, false},
{0xe5u, Opcode::V_CMP_NE_U64, false},
{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},
@@ -479,6 +486,8 @@ bool IsVop1FloatSourceOpcode(Opcode opcode) {
case Opcode::V_CEIL_F16:
case Opcode::V_TRUNC_F16:
case Opcode::V_RNDNE_F16:
case Opcode::V_SIN_F16:
case Opcode::V_COS_F16:
case Opcode::V_SIN_F32:
case Opcode::V_COS_F32: return true;
default: return false;
@@ -515,6 +524,8 @@ constexpr Vop1SdwaRule VOP1_SDWA_RULES[] = {
{Opcode::V_CVT_F32_U32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_CVT_F32_I32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_CVT_F32_UBYTE0, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_NOT_B32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_FFBL_B32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_CVT_F32_F16, SdwaSelWords() | SdwaSelFull(), 0, 0, true},
{Opcode::V_CVT_F16_F32, SdwaSelFull(), SdwaSelWords(), SdwaSelFull(), false},
{Opcode::V_CVT_F16_U16, SdwaSelWords() | SdwaSelFull(), SdwaSelWords(),
@@ -543,6 +554,10 @@ constexpr Vop1SdwaRule VOP1_SDWA_RULES[] = {
SdwaSelWords() | SdwaSelFull(), false},
{Opcode::V_RNDNE_F16, SdwaSelWords() | SdwaSelFull(), SdwaSelWords(),
SdwaSelWords() | SdwaSelFull(), false},
{Opcode::V_SIN_F16, SdwaSelWords() | SdwaSelFull(), SdwaSelWords(),
SdwaSelWords() | SdwaSelFull(), true},
{Opcode::V_COS_F16, SdwaSelWords() | SdwaSelFull(), SdwaSelWords(),
SdwaSelWords() | SdwaSelFull(), true},
{Opcode::V_CVT_U32_F32, SdwaSelFull(), SdwaSelBytes() | SdwaSelWords(), SdwaSelFull(), false},
{Opcode::V_CVT_I32_F32, SdwaSelFull(), SdwaSelBytes() | SdwaSelWords(), SdwaSelFull(), false},
{Opcode::V_CVT_RPI_I32_F32, SdwaSelFull(), SdwaSelBytes() | SdwaSelWords(), SdwaSelFull(),
@@ -827,6 +842,8 @@ bool IsVop1FloatResultOpcode(Opcode opcode) {
case Opcode::V_RSQ_F16:
case Opcode::V_LOG_F16:
case Opcode::V_EXP_F16:
case Opcode::V_SIN_F16:
case Opcode::V_COS_F16:
case Opcode::V_SIN_F32:
case Opcode::V_COS_F32: return true;
default: return false;
@@ -940,8 +957,8 @@ constexpr Vop2SdwaRule VOP2_SDWA_RULES[] = {
true, true},
{SdwaSelFull(), SdwaSelAll(), SdwaSelAll(), false, false},
{SdwaSelAll(), SdwaSelAll(), SdwaSelAll(), true, false},
{SdwaSelFull(), SdwaSelFull(), SdwaSelWords() | SdwaSelFull(), false, false},
{SdwaSelFull(), SdwaSelFull(), SdwaSelAll(), false, false},
{SdwaSelFull(), SdwaSelAll(), SdwaSelWords() | SdwaSelFull(), false, false},
{SdwaSelFull(), SdwaSelAll(), SdwaSelAll(), false, false},
{SdwaSelWords() | SdwaSelFull(), SdwaSelAll(), SdwaSelAll(), true, false},
};
static_assert(sizeof(VOP2_SDWA_RULES) / sizeof(VOP2_SDWA_RULES[0]) ==
@@ -63,6 +63,42 @@ bool Translator::Float16Unary(const Decoder::Instruction& inst, IR::ValueOpcode
return true;
}
bool Translator::Float16Trig(const Decoder::Instruction& inst, IR::ValueOpcode opcode) {
const auto argument = ReadF16AsF32(inst.src0);
const auto magnitude =
ir.BitwiseAnd(ir.BitCastU32(argument), IR::U32(IR::Value(0x7fffffffu)));
auto result = IR::F32(ir.Emit(opcode, {argument}));
if (opcode == IR::ValueOpcode::FPSin) {
const auto fraction = IR::F32(ir.Emit(IR::ValueOpcode::FPFract32, {argument}));
const auto whole = IR::U1(
ir.Emit(IR::ValueOpcode::FPOrdEqual32, {fraction, IR::Value::F32(0.0f)}));
const auto half = IR::U1(
ir.Emit(IR::ValueOpcode::FPOrdEqual32, {fraction, IR::Value::F32(0.5f)}));
const auto nonzero = ir.INotEqual(magnitude, IR::U32(IR::Value(0u)));
const auto cardinal = ir.LogicalOr(ir.LogicalAnd(whole, nonzero), half);
result = IR::F32(ir.Emit(
IR::ValueOpcode::SelectF32, {cardinal, IR::Value::F32(0.0f), result}));
} else {
// The architectural quarter-cycle result is exactly zero. Evaluating cos(pi/2) with a
// rounded F32 PI can otherwise produce a half-precision subnormal instead.
const auto fraction = IR::F32(ir.Emit(IR::ValueOpcode::FPFract32, {argument}));
const auto quarter = IR::U1(
ir.Emit(IR::ValueOpcode::FPOrdEqual32, {fraction, IR::Value::F32(0.25f)}));
const auto three_quarters = IR::U1(
ir.Emit(IR::ValueOpcode::FPOrdEqual32, {fraction, IR::Value::F32(0.75f)}));
result = IR::F32(
ir.Emit(IR::ValueOpcode::SelectF32,
{ir.LogicalOr(quarter, three_quarters), IR::Value::F32(0.0f), result}));
}
const auto infinite = ir.IEqual(magnitude, IR::U32(IR::Value(0x7f800000u)));
result = ApplyF32ResultModifiers(inst.dst, result);
const auto bits = PackHalf2x16(result, IR::F32(IR::Value::F32(0.0f)));
const auto invalid = IR::U32(IR::Value(inst.dst.clamp ? 0u : 0xfe00u));
WriteU16(DestinationOperand(inst), ir.Select(infinite, invalid, bits));
return true;
}
bool Translator::Float16Binary(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
bool reverse) {
const auto lhs = ReadF16AsF32(reverse ? inst.src1 : inst.src0);
@@ -224,11 +224,14 @@ bool Translator::S_FF1_I32_B64(const Decoder::Instruction& inst) {
return true;
}
bool Translator::V_FFBH_U32(const Decoder::Instruction& inst) {
const auto source = ReadU32(inst.src0);
const auto msb = IR::U32(ir.Emit(IR::ValueOpcode::FindUMsb32, {source}));
bool Translator::V_FFBH_32(const Decoder::Instruction& inst, bool sign) {
const auto source = ReadU32(inst.src0);
const auto value = sign ? ir.BitwiseXor(
source, ir.ShiftRightArithmetic(source, IR::U32(IR::Value(31u))))
: source;
const auto msb = IR::U32(ir.Emit(IR::ValueOpcode::FindUMsb32, {value}));
const auto position = ir.ISub(IR::U32(IR::Value(31u)), msb);
const auto result = ir.Select(ir.INotEqual(source, IR::U32(IR::Value(0u))), position,
const auto result = ir.Select(ir.INotEqual(value, IR::U32(IR::Value(0u))), position,
IR::U32(IR::Value(0xffffffffu)));
WriteOperand(DestinationOperand(inst), result);
return true;
@@ -257,6 +257,7 @@ Decoder::Operand MemorySourceAt(const Decoder::Instruction& decoded, uint32_t in
return index == 0u ? decoded.src1 : index == 1u ? decoded.src0 : decoded.src2;
case Decoder::Opcode::DS_WRITE_B8:
case Decoder::Opcode::DS_WRITE_B16:
case Decoder::Opcode::DS_WRITE_B16_D16_HI:
case Decoder::Opcode::DS_WRITE_B32:
case Decoder::Opcode::DS_WRITE_B64:
case Decoder::Opcode::DS_WRITE_B96:
@@ -877,6 +878,13 @@ bool Translator::DS_SWIZZLE_B32(const Decoder::Instruction& inst) {
return true;
}
bool Translator::DS_BPERMUTE_B32(const Decoder::Instruction& inst) {
const auto address = ir.IAdd(ReadU32(inst.src0), IR::U32(IR::Value(inst.offset)));
WriteOperand(inst.dst, ir.Emit(IR::ValueOpcode::BpermuteU32,
{ReadU32(inst.src1), address, ir.GetExec()}));
return true;
}
bool Translator::EmitMemory(const Decoder::Instruction& inst, std::string* error) {
switch (inst.opcode) {
case Decoder::Opcode::S_LOAD_DWORD:
@@ -1035,6 +1043,7 @@ bool Translator::EmitMemory(const Decoder::Instruction& inst, std::string* error
case Decoder::Opcode::DS_MAX_F32:
return DS_MINMAX_F32(inst, IR::ValueOpcode::SharedAtomicFMax32);
case Decoder::Opcode::DS_SWIZZLE_B32: return DS_SWIZZLE_B32(inst);
case Decoder::Opcode::DS_BPERMUTE_B32: return DS_BPERMUTE_B32(inst);
case Decoder::Opcode::DS_CONSUME:
return DS_APPEND_CONSUME(inst, IR::ValueOpcode::DataConsume);
case Decoder::Opcode::DS_APPEND:
@@ -1050,6 +1059,7 @@ bool Translator::EmitMemory(const Decoder::Instruction& inst, std::string* error
case Decoder::Opcode::DS_READ_I16:
case Decoder::Opcode::DS_READ_U16:
case Decoder::Opcode::DS_READ_U16_D16:
case Decoder::Opcode::DS_READ_U16_D16_HI:
case Decoder::Opcode::DS_READ_B32:
case Decoder::Opcode::DS_READ_B64:
case Decoder::Opcode::DS_READ_B96:
@@ -1060,6 +1070,7 @@ bool Translator::EmitMemory(const Decoder::Instruction& inst, std::string* error
case Decoder::Opcode::DS_WRITE2ST64_B64: return DS_WRITE2(inst);
case Decoder::Opcode::DS_WRITE_B8:
case Decoder::Opcode::DS_WRITE_B16:
case Decoder::Opcode::DS_WRITE_B16_D16_HI:
case Decoder::Opcode::DS_WRITE_B32:
case Decoder::Opcode::DS_WRITE_B64:
case Decoder::Opcode::DS_WRITE_B96:
@@ -109,6 +109,7 @@ private:
bool DS_APPEND_CONSUME(const Decoder::Instruction& inst, IR::ValueOpcode opcode);
bool DS_ADDTID(const Decoder::Instruction& inst, bool write);
bool DS_SWIZZLE_B32(const Decoder::Instruction& inst);
bool DS_BPERMUTE_B32(const Decoder::Instruction& inst);
IR::F32 SelectF32(IR::U1 condition, IR::F32 true_value, IR::F32 false_value);
IR::U32 ConvertF32ToU32Saturated(IR::F32 value, float upper_bound, float safe_upper,
@@ -148,6 +149,7 @@ private:
bool quiet_snan);
bool Float16Unary(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
bool invalid_negative);
bool Float16Trig(const Decoder::Instruction& inst, IR::ValueOpcode opcode);
bool Float16Binary(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool reverse);
bool Float16Ternary(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool accumulator,
bool mix);
@@ -182,7 +184,7 @@ private:
bool V_OR3_B32(const Decoder::Instruction& inst);
bool V_XOR3_B32(const Decoder::Instruction& inst);
bool S_FF1_I32_B64(const Decoder::Instruction& inst);
bool V_FFBH_U32(const Decoder::Instruction& inst);
bool V_FFBH_32(const Decoder::Instruction& inst, bool sign);
bool S_FLBIT_I32_B64(const Decoder::Instruction& inst);
bool Integer24(const Decoder::Instruction& inst, bool sign, bool addend);
bool V_MAD_U64_U32(const Decoder::Instruction& inst);
@@ -96,6 +96,7 @@ bool Translator::EmitVector(const Decoder::Instruction& inst, std::string* error
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual32, IR::Type::U32, false, true);
return true;
case O::V_CMP_EQ_I64:
case O::V_CMP_EQ_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, false, false);
return true;
case O::V_CMP_GT_U64:
@@ -364,6 +365,8 @@ bool Translator::EmitVector(const Decoder::Instruction& inst, std::string* error
case O::V_CEIL_F16: return Float16Unary(inst, IR::ValueOpcode::FPCeil32, false);
case O::V_TRUNC_F16: return Float16Unary(inst, IR::ValueOpcode::FPTrunc32, false);
case O::V_RNDNE_F16: return Float16Unary(inst, IR::ValueOpcode::FPRoundEven32, false);
case O::V_SIN_F16: return Float16Trig(inst, IR::ValueOpcode::FPSin);
case O::V_COS_F16: return Float16Trig(inst, IR::ValueOpcode::FPCos);
case O::V_MIN3_F16: return Float16Ternary(inst, IR::ValueOpcode::FPMinTri32, false, false);
case O::V_MAX3_F16: return Float16Ternary(inst, IR::ValueOpcode::FPMaxTri32, false, false);
case O::V_MED3_F16: return Float16Ternary(inst, IR::ValueOpcode::FPMedTri32, false, false);
@@ -490,7 +493,8 @@ bool Translator::EmitVector(const Decoder::Instruction& inst, std::string* error
case O::V_AND_OR_B32: return V_AND_OR_B32(inst);
case O::V_OR3_B32: return V_OR3_B32(inst);
case O::V_XOR3_B32: return V_XOR3_B32(inst);
case O::V_FFBH_U32: return V_FFBH_U32(inst);
case O::V_FFBH_U32: return V_FFBH_32(inst, false);
case O::V_FFBH_I32: return V_FFBH_32(inst, true);
case O::V_MAD_I32_I24: return Integer24(inst, true, true);
case O::V_MAD_U32_U24: return Integer24(inst, false, true);
@@ -188,6 +188,7 @@ VALUE_OPCODE(ReadFirstLane, U32, U32, U1)
VALUE_OPCODE(ReadLane, U32, U32, U32)
VALUE_OPCODE(WriteLane, U32, U32, U32, U32)
VALUE_OPCODE(Permlane16U32, U32, U32, U32, U32, U1)
VALUE_OPCODE(BpermuteU32, U32, U32, U32, U1)
// Typed shader resources and memory operations. Descriptor state is represented by SSA values;
// dense bindings are assigned by resource tracking.
+1 -1
View File
@@ -1066,7 +1066,7 @@ int KYTY_SYSV_ABI KernelRename(const char* from, const char* to) {
Common::File::DeleteFile(real_to);
}
if (!Common::File::MoveFile(real_from, real_to)) {
if (!Common::File::RenameFile(real_from, real_to)) {
return KERNEL_ERROR_EIO;
}
+2 -2
View File
@@ -3804,8 +3804,8 @@ int KYTY_SYSV_ABI KernelBatchMap2(KernelBatchMapEntry* entries, int num_entries,
LOGF("\t [%d] start = %p, offset = 0x%016" PRIx64 ", length = 0x%016" PRIx64
", prot = 0x%02" PRIx32 ", type = 0x%02" PRIx32 ", op = %d\n",
i, entry->start, entry->offset, entry->length,
static_cast<uint32_t>(static_cast<unsigned char>(entry->protection)),
static_cast<uint32_t>(static_cast<unsigned char>(entry->type)), entry->operation);
static_cast<uint32_t>(entry->protection), static_cast<uint32_t>(entry->type),
entry->operation);
if (entry->length == 0 || entry->operation < MAP_OP_MAP_DIRECT ||
entry->operation > MAP_OP_TYPE_PROTECT) {
+7 -7
View File
@@ -47,13 +47,13 @@ struct VirtualQueryInfo {
static_assert(sizeof(VirtualQueryInfo) == 72, "VirtualQueryInfo struct size is incorrect");
struct KernelBatchMapEntry {
void* start;
uint64_t offset;
uint64_t length;
char protection;
char type;
int16_t reserved;
int32_t operation;
void* start;
uint64_t offset;
uint64_t length;
unsigned char protection;
unsigned char type;
int16_t reserved;
int32_t operation;
};
static_assert(sizeof(KernelBatchMapEntry) == 32, "KernelBatchMapEntry struct size is incorrect");
+13 -4
View File
@@ -44,9 +44,9 @@ public:
explicit GuestBackingStore(uint64_t size): m_size(size) {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
m_handle = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE,
static_cast<DWORD>(m_size >> 32u),
static_cast<DWORD>(m_size & 0xffffffffu), nullptr);
m_handle = CreateFileMappingA(
INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_COMMIT,
static_cast<DWORD>(m_size >> 32u), static_cast<DWORD>(m_size & 0xffffffffu), nullptr);
if (m_handle == nullptr) {
LOGF_COLOR(Log::Color::Red,
"\t direct-memory backing: CreateFileMapping failed: 0x%08" PRIx32 "\n",
@@ -659,7 +659,16 @@ public:
#endif
ReserveGuestRegions();
m_backing = new GuestBackingStore(backing_size);
EXIT_IF(!m_backing->IsAvailable());
if (!m_backing->IsAvailable()) {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
EXIT("could not reserve %" PRIu64 " MB for guest direct memory. Windows commits\n"
"this up front, so the paging file is usually what needs to be larger.\n",
backing_size / (1024U * 1024U));
#else
EXIT("could not reserve %" PRIu64 " MB for guest direct memory\n",
backing_size / (1024U * 1024U));
#endif
}
}
~GuestAddressSpace() {
+3
View File
@@ -1,7 +1,10 @@
#include "cmake_config.h"
#define KYTY_GIT_VERSION "@KYTY_GIT_VERSION@"
// Short, human-facing commit name used in build labels.
#define KYTY_GIT_HASH "@KYTY_GIT_HASH@"
// Full commit identity used for compatibility checks.
#define KYTY_GIT_REVISION "@KYTY_GIT_REVISION@"
/* Build provenance, shown in the window title and in crash reports. */
#if defined(KYTY_OFFICIAL_BUILD)
+37 -1
View File
@@ -66,10 +66,46 @@ if(WIN32)
endif()
set(launcher_name "launcher")
if(APPLE)
set(launcher_name "KytyPS5")
endif()
set_target_properties(launcher PROPERTIES OUTPUT_NAME ${launcher_name})
install(TARGETS launcher DESTINATION .)
if(APPLE)
# macOS bundle: KytyPS5.app with launcher as main executable and
# kyty_emulator as helper inside Contents/MacOS. MoltenVK is found
# next to a flat executable or in the bundle's Contents/Frameworks;
# no rpath fixup is needed because SDL loads it by absolute path.
# Qt deployment (Frameworks + plugins) is handled by macdeployqt.
set_target_properties(launcher PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in"
INSTALL_RPATH "@executable_path/../Frameworks"
BUILD_WITH_INSTALL_RPATH TRUE
)
endif()
install(TARGETS launcher
BUNDLE DESTINATION .
RUNTIME DESTINATION .)
if(APPLE)
install(TARGETS kyty_emulator DESTINATION "KytyPS5.app/Contents/MacOS")
# Deploy and ad-hoc sign the Qt frameworks and plugins in the bundle.
find_program(KYTY_MACDEPLOYQT NAMES macdeployqt PATHS "${Qt6_DIR}/../../../bin" NO_DEFAULT_PATH)
if(NOT KYTY_MACDEPLOYQT)
message(FATAL_ERROR "Could not find macdeployqt")
endif()
install(CODE "
execute_process(COMMAND \"${KYTY_MACDEPLOYQT}\" \"\${CMAKE_INSTALL_PREFIX}/KytyPS5.app\"
-codesign=-
RESULT_VARIABLE _kyty_deploy_result)
if(NOT _kyty_deploy_result EQUAL 0)
message(FATAL_ERROR \"macdeployqt failed with \${_kyty_deploy_result}\")
endif()
")
endif()
if(WIN32)
install(CODE "
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key><string>KytyPS5</string>
<key>CFBundleDisplayName</key><string>KytyPS5</string>
<key>CFBundleIdentifier</key><string>org.kytyps5.kytyps5</string>
<key>CFBundleVersion</key><string>@PROJECT_VERSION@</string>
<key>CFBundleShortVersionString</key><string>@PROJECT_VERSION@</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleSignature</key><string>????</string>
<key>CFBundleExecutable</key><string>KytyPS5</string>
<key>LSMinimumSystemVersion</key><string>13.0</string>
<key>NSHighResolutionCapable</key><true/>
<key>NSRequiresAquaSystemAppearance</key><false/>
</dict>
</plist>
+67 -19
View File
@@ -102,27 +102,75 @@
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_user_name">
<property name="text">
<string>User name:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="MandatoryLineEdit" name="lineEdit_user_name">
<property name="toolTip">
<string>Local user name exposed to games</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_user_id">
<property name="text">
<string>User ID:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="spinBox_user_id">
<property name="toolTip">
<string>Local user ID exposed to games</string>
</property>
<property name="maximum">
<number>2147483647</number>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_16">
<property name="text">
<string>Screen resolution:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<item row="3" column="1">
<widget class="QComboBox" name="comboBox_screen_resolution">
<property name="toolTip">
<string>Window resolution</string>
</property>
</widget>
</item>
<item row="2" column="0">
<item row="4" column="0">
<widget class="QLabel" name="label_present_mode">
<property name="text">
<string>Present mode:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="comboBox_present_mode">
<property name="toolTip">
<string>Vulkan swapchain presentation mode</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_vblank_frequency">
<property name="text">
<string>Vblank frequency:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<item row="5" column="1">
<widget class="QSpinBox" name="spinBox_vblank_frequency">
<property name="toolTip">
<string>Virtual display refresh rate used for frame pacing</string>
@@ -138,56 +186,56 @@
</property>
</widget>
</item>
<item row="3" column="0">
<item row="6" column="0">
<widget class="QLabel" name="label_console_language">
<property name="text">
<string>Console language:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<item row="6" column="1">
<widget class="QComboBox" name="comboBox_console_language">
<property name="toolTip">
<string>Language</string>
</property>
</widget>
</item>
<item row="4" column="0">
<item row="7" column="0">
<widget class="QLabel" name="label_20">
<property name="text">
<string>Shader optimization type:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<item row="7" column="1">
<widget class="QComboBox" name="comboBox_shader_optimization_type">
<property name="toolTip">
<string>Optimize shaders for code size or performance</string>
</property>
</widget>
</item>
<item row="5" column="0">
<item row="8" column="0">
<widget class="QLabel" name="label_21">
<property name="text">
<string>Shader log direction:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<item row="8" column="1">
<widget class="QComboBox" name="comboBox_shader_log_direction">
<property name="toolTip">
<string>Dump shaders to file or console window. If enabled may decrease emulator performance</string>
</property>
</widget>
</item>
<item row="6" column="0">
<item row="9" column="0">
<widget class="QLabel" name="label_22">
<property name="text">
<string>Shader log folder:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<item row="9" column="1">
<widget class="MandatoryLineEdit" name="lineEdit_shader_log_folder">
<property name="toolTip">
<string>Specify directory to dump shaders</string>
@@ -197,14 +245,14 @@
</property>
</widget>
</item>
<item row="7" column="0">
<item row="10" column="0">
<widget class="QLabel" name="label_24">
<property name="text">
<string>Command buffer dump folder:</string>
</property>
</widget>
</item>
<item row="7" column="1">
<item row="10" column="1">
<widget class="MandatoryLineEdit" name="lineEdit_cmd_dump_folder">
<property name="toolTip">
<string>Specify directory to dump command buffers</string>
@@ -214,28 +262,28 @@
</property>
</widget>
</item>
<item row="8" column="0">
<item row="11" column="0">
<widget class="QLabel" name="label_25">
<property name="text">
<string>Printf direction:</string>
</property>
</widget>
</item>
<item row="8" column="1">
<item row="11" column="1">
<widget class="QComboBox" name="comboBox_printf_direction">
<property name="toolTip">
<string>Print logs to file or console window. If enabled may decrease emulator performance</string>
</property>
</widget>
</item>
<item row="9" column="0">
<item row="12" column="0">
<widget class="QLabel" name="label_26">
<property name="text">
<string>Printf output file:</string>
</property>
</widget>
</item>
<item row="9" column="1">
<item row="12" column="1">
<widget class="MandatoryLineEdit" name="lineEdit_printf_file">
<property name="toolTip">
<string>Specify file to dump logs</string>
@@ -245,14 +293,14 @@
</property>
</widget>
</item>
<item row="10" column="0">
<item row="13" column="0">
<widget class="QLabel" name="label_27">
<property name="text">
<string>Profiler direction:</string>
</property>
</widget>
</item>
<item row="10" column="1">
<item row="13" column="1">
<widget class="QComboBox" name="comboBox_profiler_direction">
<property name="toolTip">
<string>Enable or disable profiler. If enabled may decrease emulator performance</string>
+25
View File
@@ -2,6 +2,7 @@
#define LAUNCHER_INCLUDE_CONFIGURATION_H_
#include "common.h"
#include "common/emulatorConfig.h"
#include <QByteArray>
#include <QChar>
@@ -54,12 +55,17 @@ public:
enum class Resolution {
R1280X720,
R1920X1080,
R2560X1440,
R3840X2160,
};
Q_ENUM(Resolution)
enum class ShaderOptimizationType { None, Size, Performance };
Q_ENUM(ShaderOptimizationType)
enum class PresentMode { Fifo, Mailbox, Immediate };
Q_ENUM(PresentMode)
enum class ShaderLogDirection { Silent, Console, File };
Q_ENUM(ShaderLogDirection)
@@ -85,6 +91,9 @@ public:
QString game_comment;
Resolution screen_resolution = Resolution::R1280X720;
QString user_name = "Kyty";
int user_id = Config::DEFAULT_USER_ID;
PresentMode present_mode = PresentMode::Fifo;
bool fullscreen_enabled = false;
int vblank_frequency = 60;
int console_language = DEFAULT_CONSOLE_LANGUAGE;
@@ -108,6 +117,9 @@ public:
void CopyEmulatorSettingsFrom(const Configuration& other) {
screen_resolution = other.screen_resolution;
user_name = other.user_name;
user_id = other.user_id;
present_mode = other.present_mode;
fullscreen_enabled = other.fullscreen_enabled;
vblank_frequency = other.vblank_frequency;
console_language = other.console_language;
@@ -148,6 +160,9 @@ public:
KYTY_CFG_SET(game_path);
KYTY_CFG_SET(custom_settings);
KYTY_CFG_SET(screen_resolution);
KYTY_CFG_SET(user_name);
KYTY_CFG_SET(user_id);
KYTY_CFG_SET(present_mode);
KYTY_CFG_SET(fullscreen_enabled);
KYTY_CFG_SET(vblank_frequency);
KYTY_CFG_SET(console_language);
@@ -175,6 +190,16 @@ public:
KYTY_CFG_GET(game_path);
KYTY_CFG_GET(custom_settings);
KYTY_CFG_GET(screen_resolution);
user_name = s->value("user_name", user_name).toString();
bool user_id_ok = false;
auto saved_user_id = s->value("user_id", user_id).toInt(&user_id_ok);
user_id = user_id_ok && Config::IsConfiguredUserIdValid(saved_user_id)
? saved_user_id
: Config::DEFAULT_USER_ID;
KYTY_CFG_GET(present_mode);
if (EnumToText(present_mode).isEmpty()) {
present_mode = PresentMode::Fifo;
}
KYTY_CFG_GET(fullscreen_enabled);
vblank_frequency = s->value("vblank_frequency", vblank_frequency).toInt();
console_language = s->value("console_language", console_language).toInt();
+6 -4
View File
@@ -5,6 +5,7 @@
#include <QStringList>
class QPushButton;
class QDoubleSpinBox;
class QTreeWidget;
class QTreeWidgetItem;
class QWidget;
@@ -22,10 +23,11 @@ private:
void SetBinding(QTreeWidgetItem* item, const QString& binding);
void UpdateButtons();
QTreeWidget* m_bindings = nullptr;
QPushButton* m_change_button = nullptr;
QPushButton* m_clear_button = nullptr;
bool m_customized = false;
QTreeWidget* m_bindings = nullptr;
QPushButton* m_change_button = nullptr;
QPushButton* m_clear_button = nullptr;
QDoubleSpinBox* m_sensitivity = nullptr;
bool m_custom_bindings = false;
};
#endif /* LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_ */
@@ -1,5 +1,6 @@
#include "configurationEditDialog.h"
#include "common/emulatorConfig.h"
#include "configuration.h"
#include "mandatoryLineEdit.h"
@@ -12,6 +13,7 @@
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLayout>
#include <QLineEdit>
#include <QListView>
#include <QListWidget>
#include <QMessageBox>
@@ -152,7 +154,11 @@ static void ListInit(QComboBox* combo, T value) {
}
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);
ListInit(m_ui->comboBox_screen_resolution, info.screen_resolution);
ListInit(m_ui->comboBox_present_mode, info.present_mode);
m_ui->checkBox_fullscreen->setChecked(info.fullscreen_enabled);
m_ui->spinBox_vblank_frequency->setValue(info.vblank_frequency);
m_ui->comboBox_console_language->clear();
@@ -283,8 +289,12 @@ void ConfigurationEditDialog::moveEvent(QMoveEvent* 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.screen_resolution =
TextToEnum<Configuration::Resolution>(ui.comboBox_screen_resolution->currentText());
info.present_mode =
TextToEnum<Configuration::PresentMode>(ui.comboBox_present_mode->currentText());
info.fullscreen_enabled = ui.checkBox_fullscreen->isChecked();
info.vblank_frequency = ui.spinBox_vblank_frequency->value();
info.console_language = ui.comboBox_console_language->currentIndex();
@@ -322,6 +332,19 @@ void ConfigurationEditDialog::save() {
return;
}
const auto user_name = m_ui->lineEdit_user_name->text().trimmed();
if (user_name.isEmpty() || user_name.toUtf8().size() > Config::MAX_USER_NAME_LENGTH) {
QMessageBox::critical(this, tr("Save failed"),
tr("User name must contain 1-16 UTF-8 bytes"));
return;
}
m_ui->lineEdit_user_name->setText(user_name);
if (!Config::IsConfiguredUserIdValid(m_ui->spinBox_user_id->value())) {
QMessageBox::critical(this, tr("Save failed"),
tr("User ID cannot be 254 (everyone) or 255 (system)"));
return;
}
update_info();
emit accept();
+1 -1
View File
@@ -889,7 +889,7 @@ void ConfigurationListWidget::show_context_menu(const QPoint& pos) {
style()->standardIcon(QStyle::SP_FileDialogContentsView), tr("View trophies..."));
connect(action_view_trophies, &QAction::triggered, this,
&ConfigurationListWidget::ViewTrophies);
QAction* action_patches = menu.addAction(tr("Patches (experimental)..."));
QAction* action_patches = menu.addAction(tr("Cheats (experimental)..."));
connect(action_patches, &QAction::triggered, this, [this, item]() {
if (item != nullptr) {
auto* dialog = new PatchesDialog(item->GetInfo(), this);
+57 -19
View File
@@ -2,6 +2,7 @@
#include <QAbstractItemView>
#include <QDialogButtonBox>
#include <QDoubleSpinBox>
#include <QHBoxLayout>
#include <QHash>
#include <QHeaderView>
@@ -16,7 +17,9 @@
namespace {
constexpr int BINDING_COLUMN = 1;
constexpr int BINDING_COLUMN = 1;
constexpr auto DEFAULT_MOUSE_SENSITIVITY = 1.0;
constexpr char MOUSE_SENSITIVITY[] = "MouseSensitivity=";
struct PadControl {
const char* id;
@@ -128,7 +131,8 @@ public:
auto* layout = new QVBoxLayout(this);
m_label = new QLabel(
tr("Press a key or mouse button.\nSpace and F1 are reserved; Esc cancels."), this);
tr("Press a key or mouse button.\nSpace, F1, F7, and F11 are reserved; Esc cancels."),
this);
m_label->setAlignment(Qt::AlignCenter);
layout->addWidget(m_label);
}
@@ -144,7 +148,8 @@ protected:
reject();
return;
}
if (event->key() == Qt::Key_Space || event->key() == Qt::Key_F1) {
if (event->key() == Qt::Key_Space || event->key() == Qt::Key_F1 ||
event->key() == Qt::Key_F7 || event->key() == Qt::Key_F11) {
m_label->setText(tr("That key is reserved by the emulator."));
return;
}
@@ -177,6 +182,9 @@ private:
QHash<QString, QString> ParseMapping(const QStringList& mapping) {
QHash<QString, QString> result;
for (const auto& entry: mapping) {
if (entry.startsWith(QLatin1String(MOUSE_SENSITIVITY))) {
continue;
}
const auto separator = entry.indexOf(QLatin1Char('='));
if (separator > 0 && separator + 1 < entry.size()) {
const auto binding = entry.mid(separator + 1);
@@ -193,15 +201,42 @@ QHash<QString, QString> ParseMapping(const QStringList& mapping) {
return result;
}
double ParseMouseSensitivity(const QStringList& mapping) {
for (const auto& entry: mapping) {
if (entry.startsWith(QLatin1String(MOUSE_SENSITIVITY))) {
return entry.mid(sizeof(MOUSE_SENSITIVITY) - 1).toDouble();
}
}
return DEFAULT_MOUSE_SENSITIVITY;
}
} // namespace
InputMappingDialog::InputMappingDialog(const QStringList& mapping, QWidget* parent)
: QDialog(parent), m_customized(!mapping.isEmpty()) {
: QDialog(parent) {
setWindowTitle(tr("Input Mapping"));
resize(460, 650);
auto* layout = new QVBoxLayout(this);
layout->addWidget(new QLabel(tr("Map keyboard or mouse input to DualSense controls."), this));
layout->addWidget(
new QLabel(tr("Map keyboard or mouse buttons to DualSense controls.\n"
"Press F7 in-game to toggle mouse movement on the right stick."),
this));
const auto parsed = ParseMapping(mapping);
m_custom_bindings = !parsed.isEmpty();
auto* sensitivity_layout = new QHBoxLayout;
sensitivity_layout->addWidget(new QLabel(tr("Mouse sensitivity"), this));
m_sensitivity = new QDoubleSpinBox(this);
m_sensitivity->setRange(0.1, 5.0);
m_sensitivity->setSingleStep(0.1);
m_sensitivity->setDecimals(1);
m_sensitivity->setSuffix(QStringLiteral("x"));
m_sensitivity->setValue(ParseMouseSensitivity(mapping));
sensitivity_layout->addWidget(m_sensitivity);
sensitivity_layout->addStretch();
layout->addLayout(sensitivity_layout);
m_bindings = new QTreeWidget(this);
m_bindings->setColumnCount(2);
@@ -212,13 +247,12 @@ InputMappingDialog::InputMappingDialog(const QStringList& mapping, QWidget* pare
m_bindings->header()->setSectionResizeMode(1, QHeaderView::Stretch);
layout->addWidget(m_bindings);
const auto parsed = ParseMapping(mapping);
for (const auto& control: PAD_CONTROLS) {
auto* item = new QTreeWidgetItem(m_bindings);
item->setText(0, tr(control.label));
item->setData(0, Qt::UserRole, QString::fromLatin1(control.id));
SetBinding(item, m_customized ? parsed.value(QString::fromLatin1(control.id))
: QString::fromLatin1(control.default_binding));
SetBinding(item, m_custom_bindings ? parsed.value(QString::fromLatin1(control.id))
: QString::fromLatin1(control.default_binding));
}
m_bindings->setCurrentItem(m_bindings->topLevelItem(0));
@@ -248,16 +282,19 @@ InputMappingDialog::InputMappingDialog(const QStringList& mapping, QWidget* pare
QStringList InputMappingDialog::Mapping() const {
QStringList result;
if (!m_customized) {
return result;
}
for (int index = 0; index < m_bindings->topLevelItemCount(); index++) {
const auto* item = m_bindings->topLevelItem(index);
const auto binding = item->data(BINDING_COLUMN, Qt::UserRole).toString();
if (!binding.isEmpty()) {
result.append(item->data(0, Qt::UserRole).toString() + QLatin1Char('=') + binding);
if (m_custom_bindings) {
for (int index = 0; index < m_bindings->topLevelItemCount(); index++) {
const auto* item = m_bindings->topLevelItem(index);
const auto binding = item->data(BINDING_COLUMN, Qt::UserRole).toString();
if (!binding.isEmpty()) {
result.append(item->data(0, Qt::UserRole).toString() + QLatin1Char('=') + binding);
}
}
}
if (m_sensitivity->value() != DEFAULT_MOUSE_SENSITIVITY) {
result.append(QLatin1String(MOUSE_SENSITIVITY) +
QString::number(m_sensitivity->value(), 'f', 1));
}
return result;
}
@@ -281,12 +318,12 @@ void InputMappingDialog::ChangeBinding() {
}
}
SetBinding(item, dialog.Binding());
m_customized = true;
m_custom_bindings = true;
}
void InputMappingDialog::ClearBinding() {
SetBinding(m_bindings->currentItem(), {});
m_customized = true;
m_custom_bindings = true;
}
void InputMappingDialog::RestoreDefaults() {
@@ -294,7 +331,8 @@ void InputMappingDialog::RestoreDefaults() {
SetBinding(m_bindings->topLevelItem(index),
QString::fromLatin1(PAD_CONTROLS[index].default_binding));
}
m_customized = false;
m_sensitivity->setValue(DEFAULT_MOUSE_SENSITIVITY);
m_custom_bindings = false;
}
void InputMappingDialog::SetBinding(QTreeWidgetItem* item, const QString& binding) {
+3
View File
@@ -204,6 +204,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
args << "--screen-width" << r.at(0);
args << "--screen-height" << r.at(1);
args << "--user-name" << info.user_name;
args << "--user-id" << QString::number(info.user_id);
args << "--present-mode" << EnumToText(info.present_mode);
if (info.fullscreen_enabled) {
args << "--fullscreen";
}
+49 -37
View File
@@ -18,7 +18,7 @@
PatchesDialog::PatchesDialog(const Configuration& game, QWidget* parent)
: QDialog(parent), m_title_id(game.title_id.trimmed().toUpper()) {
setAttribute(Qt::WA_DeleteOnClose);
setWindowTitle(tr("Patches (Experimental) - %1").arg(game.name));
setWindowTitle(tr("Cheats (Experimental) - %1").arg(game.name));
resize(640, 480);
auto* layout = new QVBoxLayout(this);
@@ -54,46 +54,50 @@ QString PatchesDialog::PatchPlanPath(const QString& title_id) {
void PatchesDialog::Load() {
QFile file(PatchPlanPath(m_title_id));
if (!file.open(QIODevice::ReadOnly)) {
m_status->setText(tr("No local patch file: %1").arg(file.fileName()));
m_status->setText(tr("No local cheat file: %1").arg(file.fileName()));
m_apply->setEnabled(false);
return;
}
const auto document = QJsonDocument::fromJson(file.readAll());
if (document.isNull() || !document.isObject()) {
m_status->setText(tr("Invalid patch JSON in %1.").arg(file.fileName()));
m_status->setText(tr("Invalid cheat JSON in %1.").arg(file.fileName()));
m_apply->setEnabled(false);
return;
}
const auto root = document.object();
const auto patches = root.value(QStringLiteral("patches")).toArray();
for (const auto& value: patches) {
const auto patch = value.toObject();
auto* item = new QListWidgetItem(patch.value(QStringLiteral("name")).toString(), m_patches);
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(patch.value(QStringLiteral("enabled")).toBool(true) ? Qt::Checked
: Qt::Unchecked);
}
m_apply->setEnabled(!patches.isEmpty());
if (patches.isEmpty()) {
// shadPS4/ETAHen cheat files use "mods"; Kyty only reads a "patches" array.
if (root.contains(QStringLiteral("mods"))) {
m_status->setText(
tr("This file uses a \"mods\" list. Kyty expects a top-level \"patches\" "
"array in %1.")
.arg(file.fileName()));
} else if (!root.contains(QStringLiteral("patches"))) {
m_status->setText(
tr("No \"patches\" array in %1.").arg(file.fileName()));
} else {
m_status->setText(tr("Loaded 0 patch(es) from %1.").arg(file.fileName()));
}
const auto root = document.object();
if (!root.value(QStringLiteral("mods")).isArray()) {
m_status->setText(tr("Cheat JSON has no \"mods\" array in %1.").arg(file.fileName()));
m_apply->setEnabled(false);
return;
}
m_status->setText(tr("Loaded %1 patch(es) from %2.").arg(patches.size()).arg(file.fileName()));
const auto mods = root.value(QStringLiteral("mods")).toArray();
for (const auto& value: mods) {
if (!value.isObject()) {
m_status->setText(tr("Invalid cheat entry in %1.").arg(file.fileName()));
m_apply->setEnabled(false);
m_patches->clear();
return;
}
const auto mod = value.toObject();
const auto name = mod.value(QStringLiteral("name"));
const auto enabled = mod.value(QStringLiteral("enabled"));
if (!name.isString() || name.toString().isEmpty() ||
(!enabled.isUndefined() && !enabled.isBool())) {
m_status->setText(tr("Invalid cheat entry in %1.").arg(file.fileName()));
m_apply->setEnabled(false);
m_patches->clear();
return;
}
auto* item = new QListWidgetItem(name.toString(), m_patches);
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(enabled.toBool(true) ? Qt::Checked : Qt::Unchecked);
}
m_apply->setEnabled(!mods.isEmpty());
m_status->setText(tr("Loaded %1 cheat(s) from %2.").arg(mods.size()).arg(file.fileName()));
}
void PatchesDialog::Save() {
@@ -105,20 +109,28 @@ void PatchesDialog::Save() {
auto document = QJsonDocument::fromJson(input.readAll());
input.close();
auto root = document.object();
auto patches = root.value(QStringLiteral("patches")).toArray();
for (int index = 0; index < patches.size(); index++) {
auto patch = patches[index].toObject();
patch.insert(QStringLiteral("enabled"),
m_patches->item(index)->checkState() == Qt::Checked);
patches[index] = patch;
const auto changed_message = tr("Cheat file changed; reload the dialog.");
if (!document.isObject()) {
m_status->setText(changed_message);
return;
}
root.insert(QStringLiteral("patches"), patches);
auto root = document.object();
auto mods = root.value(QStringLiteral("mods")).toArray();
if (mods.size() != m_patches->count()) {
m_status->setText(changed_message);
return;
}
for (int index = 0; index < mods.size(); index++) {
auto mod = mods[index].toObject();
mod.insert(QStringLiteral("enabled"), m_patches->item(index)->checkState() == Qt::Checked);
mods[index] = mod;
}
root.insert(QStringLiteral("mods"), mods);
document.setObject(root);
QSaveFile output(path);
if (output.open(QIODevice::WriteOnly) && output.write(document.toJson()) >= 0 &&
output.commit()) {
m_status->setText(tr("Patch selection saved."));
m_status->setText(tr("Cheat selection saved."));
}
}
+48 -2
View File
@@ -1,7 +1,9 @@
#include "libs/controller.h"
#include "SDL.h"
#include "common/assert.h"
#include "common/common.h"
#include "common/emulatorConfig.h"
#include "common/logging/log.h"
#include "common/stringUtils.h"
#include "common/threads.h"
@@ -21,6 +23,9 @@ LIB_NAME("Pad", "Pad");
constexpr int PAD_ERROR_INVALID_ARG = -2137915391; /* 0x80920001 */
constexpr int PAD_ERROR_INVALID_HANDLE = -2137915389; /* 0x80920003 */
// SDL limits rumble commands to 0xffff ms; zero strengths stop immediately.
constexpr uint32_t RUMBLE_DURATION_MS = 0xffff;
struct PadControllerInformation {
float touch_pixel_density;
uint16_t touch_resolution_x;
@@ -56,8 +61,10 @@ public:
void Disconnect(int id);
void Button(int id, uint32_t button, bool down);
void Axis(int id, Axis axis, int value);
void RightStick(int id, int x, int y);
void ResetInputState();
void GetConnectionInfo(bool* flag, int* count);
void SetVibration(uint8_t large_motor, uint8_t small_motor);
void ReadState(ControllerState* state, bool* flag, int* count);
int ReadStates(ControllerState* states, int states_num, bool* flag, int* count);
@@ -259,6 +266,18 @@ void GameController::Axis(int id, Controller::Axis axis, int value) {
}
}
void GameController::RightStick(int id, int x, int y) {
Common::LockGuard lock(m_mutex);
if (m_active_id == id || id == HOST_INPUT_CONTROLLER_ID) {
auto state = GetLastState();
state.time = LibKernel::KernelGetProcessTime();
state.axes[static_cast<int>(Axis::RightX)] = x;
state.axes[static_cast<int>(Axis::RightY)] = y;
AddState(state);
}
}
void GameController::ResetInputState() {
Common::LockGuard lock(m_mutex);
ControllerState state {};
@@ -268,6 +287,25 @@ void GameController::ResetInputState() {
AddState(state);
}
void GameController::SetVibration(uint8_t large_motor, uint8_t small_motor) {
Common::LockGuard lock(m_mutex);
if (m_active_id == HOST_INPUT_CONTROLLER_ID) {
return;
}
auto* pad = SDL_GameControllerFromInstanceID(static_cast<SDL_JoystickID>(m_active_id));
if (pad == nullptr) {
return;
}
const auto large = static_cast<uint16_t>(large_motor * 0x101U);
const auto small = static_cast<uint16_t>(small_motor * 0x101U);
if (SDL_GameControllerRumble(pad, large, small, RUMBLE_DURATION_MS) != 0) {
LOGF("\t rumble failed: %s\n", SDL_GetError());
}
}
void GameController::GetConnectionInfo(bool* flag, int* count) {
EXIT_IF(flag == nullptr);
EXIT_IF(count == nullptr);
@@ -346,6 +384,12 @@ void ControllerAxis(int id, Axis axis, int value) {
g_controller->Axis(id, axis, value);
}
void ControllerRightStick(int id, int x, int y) {
EXIT_IF(g_controller == nullptr);
g_controller->RightStick(id, x, y);
}
void ControllerResetInputState() {
EXIT_IF(g_controller == nullptr);
g_controller->ResetInputState();
@@ -358,13 +402,12 @@ int KYTY_SYSV_ABI PadInit() {
}
static bool PadOpenArgsAreValid(int user_id, int type, int index) {
constexpr int user_id_initial = 1000;
constexpr int user_id_system = 0xff;
constexpr int port_type_standard = 0;
constexpr int port_type_special = 2;
constexpr int port_type_remote = 16;
const bool personal_port =
user_id == user_id_initial && (type == port_type_standard || type == port_type_special);
user_id == Config::GetUserId() && (type == port_type_standard || type == port_type_special);
const bool system_remote_control = user_id == user_id_system && type == port_type_remote;
return index == 0 && (personal_port || system_remote_control);
}
@@ -544,6 +587,9 @@ int KYTY_SYSV_ABI PadSetVibration(int handle, const PadVibrationParam* param) {
"\t small_motor = %d\n",
static_cast<int>(param->large_motor), static_cast<int>(param->small_motor));
EXIT_IF(g_controller == nullptr);
g_controller->SetVibration(param->large_motor, param->small_motor);
return OK;
}
+1
View File
@@ -58,6 +58,7 @@ void ControllerConnect(int id);
void ControllerDisconnect(int id);
void ControllerButton(int id, uint32_t button, bool down);
void ControllerAxis(int id, Axis axis, int value);
void ControllerRightStick(int id, int x, int y);
void ControllerResetInputState();
int KYTY_SYSV_ABI PadInit();
+5 -5
View File
@@ -1,5 +1,6 @@
#include "libs/dialog.h"
#include "common/emulatorConfig.h"
#include "common/logging/log.h"
#include "common/stringUtils.h"
#include "libs/errno.h"
@@ -41,7 +42,6 @@ constexpr int LOGIN_RESULT_OK = 0;
constexpr int LOGIN_MODE_ALL_USERS = 0;
constexpr int LOGIN_MODE_NOT_LOGGED_IN = 1;
constexpr int LOGIN_USER_INVALID = -1;
constexpr int LOGIN_USER_ID = 1000;
constexpr int LOGIN_ERROR_NOT_INITIALIZED = static_cast<int>(0x81340001u);
constexpr int LOGIN_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x81340002u);
@@ -67,7 +67,7 @@ static_assert(sizeof(LoginDialogResult) == 16);
static_assert(sizeof(LoginDialogParam) == 64);
static int g_login_status = LOGIN_STATUS_NONE;
static int g_login_selected_user = LOGIN_USER_ID;
static int g_login_selected_user = LOGIN_USER_INVALID;
int KYTY_SYSV_ABI LoginDialogInitialize() {
PRINT_NAME();
@@ -77,7 +77,7 @@ int KYTY_SYSV_ABI LoginDialogInitialize() {
}
g_login_status = LOGIN_STATUS_INITIALIZED;
g_login_selected_user = LOGIN_USER_ID;
g_login_selected_user = Config::GetUserId();
return OK;
}
@@ -90,7 +90,7 @@ int KYTY_SYSV_ABI LoginDialogTerminate() {
}
g_login_status = LOGIN_STATUS_NONE;
g_login_selected_user = LOGIN_USER_ID;
g_login_selected_user = LOGIN_USER_INVALID;
return OK;
}
@@ -112,7 +112,7 @@ int KYTY_SYSV_ABI LoginDialogOpen(const void* param) {
}
g_login_selected_user =
p->initial_focus != LOGIN_USER_INVALID ? p->initial_focus : LOGIN_USER_ID;
p->initial_focus != LOGIN_USER_INVALID ? p->initial_focus : Config::GetUserId();
LOGF("\t size = %d\n"
"\t mode = %d\n"
+1 -18
View File
@@ -327,25 +327,8 @@ static KYTY_SYSV_ABI int setenv(const char* name, const char* value, int overwri
return -1;
}
if (overwrite == 0 && std::getenv(name) != nullptr) {
return 0;
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
const auto ret = ::_putenv_s(name, value);
if (ret != 0) {
*Posix::GetErrorAddr() = ret;
return -1;
}
// Guest environment changes must not leak into emulator host libraries.
return 0;
#else
const auto ret = ::setenv(name, value, overwrite);
if (ret != 0) {
*Posix::GetErrorAddr() = errno;
return -1;
}
return 0;
#endif
}
static KYTY_SYSV_ABI int64_t libc_time(int64_t* timer) {
+5
View File
@@ -113,6 +113,10 @@ int KYTY_SYSV_ABI NetResolverCreate(const char* name, int memid, int flags) {
return NET_CALL(Net::NetResolverCreate(name, memid, flags));
}
int KYTY_SYSV_ABI NetResolverDestroy(int rid) {
return NET_CALL(Net::NetResolverDestroy(rid));
}
int KYTY_SYSV_ABI NetResolverStartNtoa(int rid, const char* hostname, void* addr, int timeout,
int retry, int flags) {
return NET_CALL(Net::NetResolverStartNtoa(rid, hostname, addr, timeout, retry, flags));
@@ -194,6 +198,7 @@ LIB_DEFINE(InitNet_1_Net) {
LIB_FUNC("dgJBaeJnGpo", LibNet::NetPoolCreate);
LIB_FUNC("K7RlrTkI-mw", LibNet::NetPoolDestroy);
LIB_FUNC("C4UgDHHPvdw", LibNet::NetResolverCreate);
LIB_FUNC("kJlYH5uMAWI", LibNet::NetResolverDestroy);
LIB_FUNC("Nd91WaWmG2w", LibNet::NetResolverStartNtoa);
LIB_FUNC("8Kcp5d-q1Uo", LibNet::NetInetPton);
LIB_FUNC("9vA2aW+CHuA", LibNet::NetInetNtop);
+16 -16
View File
@@ -1,11 +1,10 @@
#include "common/abi.h"
#include "common/assert.h"
#include "common/stringUtils.h"
#include "common/emulatorConfig.h"
#include "libs/errno.h"
#include "libs/libs.h"
#include "loader/symbolDatabase.h"
#include <cinttypes>
#include <cstdio>
#include <cstring>
@@ -57,7 +56,7 @@ static KYTY_SYSV_ABI int UserServiceGetInitialUser(int* user_id) {
EXIT_NOT_IMPLEMENTED(user_id == nullptr);
*user_id = 1000;
*user_id = Config::GetUserId();
return OK;
}
@@ -72,7 +71,7 @@ static KYTY_SYSV_ABI int UserServiceGetEvent(SceUserServiceEvent* event) {
if (!logged_in) {
logged_in = true;
event->event_type = UserServiceEventTypeLogin;
event->user_id = 1000;
event->user_id = Config::GetUserId();
return OK;
}
@@ -84,7 +83,7 @@ static KYTY_SYSV_ABI int UserServiceGetLoginUserIdList(UserServiceLoginUserIdLis
EXIT_NOT_IMPLEMENTED(user_id_list == nullptr);
user_id_list->user_id[0] = 1000;
user_id_list->user_id[0] = Config::GetUserId();
user_id_list->user_id[1] = -1;
user_id_list->user_id[2] = -1;
user_id_list->user_id[3] = -1;
@@ -93,10 +92,11 @@ static KYTY_SYSV_ABI int UserServiceGetLoginUserIdList(UserServiceLoginUserIdLis
}
static KYTY_SYSV_ABI int UserServiceGetUserName(int user_id, char* name, size_t size) {
EXIT_NOT_IMPLEMENTED(user_id != 1000);
EXIT_NOT_IMPLEMENTED(user_id != Config::GetUserId());
EXIT_NOT_IMPLEMENTED(size < 5);
int s = snprintf(name, size, "%s", "Kyty");
const auto& user_name = Config::GetUserName();
int s = snprintf(name, size, "%s", user_name.c_str());
EXIT_NOT_IMPLEMENTED(static_cast<size_t>(s) >= size);
@@ -109,7 +109,7 @@ static KYTY_SYSV_ABI int UserServiceGetUserNumber(int user_id, int32_t* number)
if (number == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
}
if (user_id != 1000) {
if (user_id != Config::GetUserId()) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
}
@@ -124,7 +124,7 @@ static KYTY_SYSV_ABI int UserServiceGetGamePresets(int user_id, UserServiceGameP
if (presets == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
}
if (user_id != 1000) {
if (user_id != Config::GetUserId()) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
}
@@ -145,7 +145,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityVibration(int user_id, int32
if (vibration == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
}
if (user_id != 1000) {
if (user_id != Config::GetUserId()) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
}
@@ -161,7 +161,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityTriggerEffect(int user_
if (trigger_effect == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
}
if (user_id != 1000) {
if (user_id != Config::GetUserId()) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
}
@@ -176,7 +176,7 @@ static KYTY_SYSV_ABI int UserServiceGetAgeLevel(int user_id, uint32_t* age_level
if (age_level == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
}
if (user_id != 1000) {
if (user_id != Config::GetUserId()) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
}
@@ -192,7 +192,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityChatTranscription(int u
if (chat_transcription == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
}
if (user_id != 1000) {
if (user_id != Config::GetUserId()) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
}
@@ -208,7 +208,7 @@ UserServiceGetAccessibilityPressAndHoldDelay(int user_id, int32_t* press_and_hol
if (press_and_hold_delay == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
}
if (user_id != 1000) {
if (user_id != Config::GetUserId()) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
}
@@ -224,7 +224,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityZoomEnabled(int user_id
if (zoom_enabled == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
}
if (user_id != 1000) {
if (user_id != Config::GetUserId()) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
}
@@ -240,7 +240,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityZoomFollowFocus(int use
if (zoom_follow_focus == nullptr) {
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
}
if (user_id != 1000) {
if (user_id != Config::GetUserId()) {
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
}
+98 -63
View File
@@ -16,11 +16,6 @@
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
// POSIX uses plain int file descriptors for sockets; provide the Winsock spellings
// the shared (non-guarded) code paths reference.
using SOCKET = int;
static constexpr SOCKET INVALID_SOCKET = -1;
#endif
#include "common/assert.h"
@@ -106,6 +101,7 @@ public:
int PoolCreate(const char* name, int size);
bool PoolDestroy(int memid);
int ResolverCreate(const char* name, int memid);
bool ResolverDestroy(int rid);
bool ResolverValid(int rid);
Id SslInit(uint64_t pool_size);
@@ -296,6 +292,17 @@ int Network::ResolverCreate(const char* name, int memid) {
return -1;
}
bool Network::ResolverDestroy(int rid) {
Common::LockGuard lock(m_mutex);
if (rid >= 0 && rid < RESOLVERS_MAX && m_resolvers[rid].used) {
m_resolvers[rid] = {};
return true;
}
return false;
}
bool Network::ResolverValid(int rid) {
Common::LockGuard lock(m_mutex);
@@ -823,9 +830,11 @@ struct NetEtherAddr {
#if defined(_WIN32)
using NativeSocket = SOCKET;
static constexpr NativeSocket INVALID_NATIVE_SOCKET = INVALID_SOCKET;
using SocketLength = int;
#else
using NativeSocket = int;
static constexpr NativeSocket INVALID_NATIVE_SOCKET = -1;
using SocketLength = socklen_t;
#endif
struct SocketSlot {
@@ -898,7 +907,12 @@ static bool EnsureSocketBackend() {
return true;
}
static int SetPosixSocketError(int error) {
static int SetGuestSocketError(int error) {
*Posix::GetErrorAddr() = error;
return -1;
}
static int SetHostSocketError(int error) {
int posix_error = Posix::POSIX_EIO;
#if defined(_WIN32)
switch (error) {
@@ -933,17 +947,31 @@ static int SetPosixSocketError(int error) {
default: break;
}
#else
posix_error = error;
switch (error) {
case EACCES: posix_error = Posix::POSIX_EACCES; break;
case EADDRINUSE: posix_error = Posix::POSIX_EADDRINUSE; break;
case EADDRNOTAVAIL: posix_error = Posix::POSIX_EADDRNOTAVAIL; break;
case EAFNOSUPPORT: posix_error = Posix::POSIX_EAFNOSUPPORT; break;
case EBADF: posix_error = Posix::POSIX_EBADF; break;
case EFAULT: posix_error = Posix::POSIX_EFAULT; break;
case EINVAL: posix_error = Posix::POSIX_EINVAL; break;
case EMFILE: posix_error = Posix::POSIX_EMFILE; break;
case ENFILE: posix_error = Posix::POSIX_ENFILE; break;
case ENOBUFS: posix_error = Posix::POSIX_ENOBUFS; break;
case ENOMEM: posix_error = Posix::POSIX_ENOMEM; break;
case ENOTSOCK: posix_error = Posix::POSIX_ENOTSOCK; break;
case EPROTONOSUPPORT: posix_error = Posix::POSIX_EPROTONOSUPPORT; break;
default: break;
}
#endif
*Posix::GetErrorAddr() = posix_error;
return -1;
return SetGuestSocketError(posix_error);
}
static int SetPosixSocketError() {
static int SetHostSocketError() {
#if defined(_WIN32)
return SetPosixSocketError(WSAGetLastError());
return SetHostSocketError(WSAGetLastError());
#else
return SetPosixSocketError(errno);
return SetHostSocketError(errno);
#endif
}
@@ -991,9 +1019,8 @@ static int ConvertMessageFlags(int flags) {
#endif
}
#if defined(_WIN32)
static int ConvertGuestSockaddr(const void* addr, uint32_t addrlen, sockaddr_storage* out,
int* out_len) {
SocketLength* out_len) {
EXIT_IF(out == nullptr);
EXIT_IF(out_len == nullptr);
@@ -1028,6 +1055,7 @@ static int ConvertGuestSockaddr(const void* addr, uint32_t addrlen, sockaddr_sto
return 0;
}
#if defined(_WIN32)
static int ConvertHostSockaddr(const sockaddr_storage* addr, int addrlen, void* out,
uint32_t* out_len) {
EXIT_IF(addr == nullptr);
@@ -1215,6 +1243,19 @@ int KYTY_SYSV_ABI NetResolverCreate(const char* name, int memid, int flags) {
return id;
}
int KYTY_SYSV_ABI NetResolverDestroy(int rid) {
PRINT_NAME();
LOGF("\t rid = %d\n", rid);
EXIT_IF(g_net == nullptr);
if (!g_net->ResolverDestroy(rid)) {
return NET_ERROR_EBADF;
}
return OK;
}
int KYTY_SYSV_ABI NetResolverStartNtoa(int rid, const char* hostname, void* addr, int timeout,
int retry, int flags) {
PRINT_NAME();
@@ -1364,7 +1405,7 @@ int KYTY_SYSV_ABI NetGetSockInfo(int s, void* info, int n, int flags) {
int KYTY_SYSV_ABI EpollCreate(const char* name, int flags) {
if (name == nullptr || flags != 0) {
return SetPosixSocketError(Posix::POSIX_EINVAL);
return SetGuestSocketError(Posix::POSIX_EINVAL);
}
std::lock_guard lock(g_epoll_mutex);
@@ -1378,7 +1419,7 @@ int KYTY_SYSV_ABI EpollCreate(const char* name, int flags) {
}
}
return SetPosixSocketError(Posix::POSIX_EMFILE);
return SetGuestSocketError(Posix::POSIX_EMFILE);
}
int KYTY_SYSV_ABI EpollControl(int eid, int op, int id, const NetEpollEvent* event) {
@@ -1387,22 +1428,22 @@ int KYTY_SYSV_ABI EpollControl(int eid, int op, int id, const NetEpollEvent* eve
constexpr int EPOLL_CTL_DEL = 3;
if ((op == EPOLL_CTL_ADD || op == EPOLL_CTL_MOD) && event == nullptr) {
return SetPosixSocketError(Posix::POSIX_EINVAL);
return SetGuestSocketError(Posix::POSIX_EINVAL);
}
if (op == EPOLL_CTL_DEL && event != nullptr) {
return SetPosixSocketError(Posix::POSIX_EINVAL);
return SetGuestSocketError(Posix::POSIX_EINVAL);
}
if (op < EPOLL_CTL_ADD || op > EPOLL_CTL_DEL) {
return SetPosixSocketError(Posix::POSIX_EINVAL);
return SetGuestSocketError(Posix::POSIX_EINVAL);
}
if (!IsSocket(id)) {
return SetPosixSocketError(Posix::POSIX_EBADF);
return SetGuestSocketError(Posix::POSIX_EBADF);
}
std::lock_guard lock(g_epoll_mutex);
auto* slot = GetEpollSlot(eid);
if (slot == nullptr) {
return SetPosixSocketError(Posix::POSIX_EBADF);
return SetGuestSocketError(Posix::POSIX_EBADF);
}
auto registration = std::find_if(slot->registrations.begin(), slot->registrations.end(),
@@ -1410,19 +1451,19 @@ int KYTY_SYSV_ABI EpollControl(int eid, int op, int id, const NetEpollEvent* eve
switch (op) {
case EPOLL_CTL_ADD:
if (registration != slot->registrations.end()) {
return SetPosixSocketError(Posix::POSIX_EEXIST);
return SetGuestSocketError(Posix::POSIX_EEXIST);
}
slot->registrations.push_back({id, *event});
break;
case EPOLL_CTL_MOD:
if (registration == slot->registrations.end()) {
return SetPosixSocketError(Posix::POSIX_ENOENT);
return SetGuestSocketError(Posix::POSIX_ENOENT);
}
registration->event = *event;
break;
case EPOLL_CTL_DEL:
if (registration == slot->registrations.end()) {
return SetPosixSocketError(Posix::POSIX_ENOENT);
return SetGuestSocketError(Posix::POSIX_ENOENT);
}
slot->registrations.erase(registration);
break;
@@ -1435,10 +1476,10 @@ int KYTY_SYSV_ABI EpollControl(int eid, int op, int id, const NetEpollEvent* eve
int KYTY_SYSV_ABI EpollWait(int eid, NetEpollEvent* events, int maxevents, int timeout) {
if (events == nullptr) {
return SetPosixSocketError(Posix::POSIX_EFAULT);
return SetGuestSocketError(Posix::POSIX_EFAULT);
}
if (maxevents <= 0 || timeout < -1) {
return SetPosixSocketError(Posix::POSIX_EINVAL);
return SetGuestSocketError(Posix::POSIX_EINVAL);
}
std::vector<EpollRegistration> registrations;
@@ -1446,7 +1487,7 @@ int KYTY_SYSV_ABI EpollWait(int eid, NetEpollEvent* events, int maxevents, int t
std::unique_lock lock(g_epoll_mutex);
auto* slot = GetEpollSlot(eid);
if (slot == nullptr) {
return SetPosixSocketError(Posix::POSIX_EBADF);
return SetGuestSocketError(Posix::POSIX_EBADF);
}
if (slot->registrations.empty() && timeout != 0) {
@@ -1461,7 +1502,7 @@ int KYTY_SYSV_ABI EpollWait(int eid, NetEpollEvent* events, int maxevents, int t
}
slot = GetEpollSlot(eid);
if (slot == nullptr) {
return SetPosixSocketError(Posix::POSIX_EBADF);
return SetGuestSocketError(Posix::POSIX_EBADF);
}
}
registrations = slot->registrations;
@@ -1492,7 +1533,7 @@ int KYTY_SYSV_ABI EpollWait(int eid, NetEpollEvent* events, int maxevents, int t
continue;
}
if (host_registrations.size() >= FD_SETSIZE) {
return SetPosixSocketError(Posix::POSIX_EINVAL);
return SetGuestSocketError(Posix::POSIX_EINVAL);
}
if ((registration.event.events & EPOLL_IN) != 0) {
FD_SET(socket, &host_read);
@@ -1518,7 +1559,7 @@ int KYTY_SYSV_ABI EpollWait(int eid, NetEpollEvent* events, int maxevents, int t
const int result = ::select(0, &host_read, &host_write, &host_except, host_timeout_ptr);
if (result == SOCKET_ERROR) {
return SetPosixSocketError();
return SetHostSocketError();
}
if (result == 0) {
return 0;
@@ -1552,7 +1593,7 @@ int KYTY_SYSV_ABI EpollWait(int eid, NetEpollEvent* events, int maxevents, int t
return count;
#else
(void)timeout;
return SetPosixSocketError(Posix::POSIX_ENOSYS);
return SetGuestSocketError(Posix::POSIX_ENOSYS);
#endif
}
@@ -1560,7 +1601,7 @@ int KYTY_SYSV_ABI EpollDestroy(int eid) {
std::lock_guard lock(g_epoll_mutex);
auto* slot = GetEpollSlot(eid);
if (slot == nullptr) {
return SetPosixSocketError(Posix::POSIX_EBADF);
return SetGuestSocketError(Posix::POSIX_EBADF);
}
slot->used = false;
@@ -1582,14 +1623,15 @@ int KYTY_SYSV_ABI SocketClose(int s) {
RemoveSocketFromEpolls(s);
#if defined(_WIN32)
if (closesocket(socket) == SOCKET_ERROR) {
const int result = closesocket(socket);
#else
const int result = ::close(socket);
#endif
if (result != 0) {
return NET_ERROR_EBADF;
}
return OK;
#else
return NET_ERROR_ENOSYS;
#endif
}
int KYTY_SYSV_ABI Socket(int family, int type, int protocol) {
@@ -1601,7 +1643,7 @@ int KYTY_SYSV_ABI Socket(int family, int type, int protocol) {
family, type, protocol);
if (!EnsureSocketBackend()) {
return SetPosixSocketError(Posix::POSIX_ENETDOWN);
return SetGuestSocketError(Posix::POSIX_ENETDOWN);
}
const int host_family = ConvertFamily(family);
@@ -1610,25 +1652,24 @@ int KYTY_SYSV_ABI Socket(int family, int type, int protocol) {
return -1;
}
#if defined(_WIN32)
NativeSocket socket = ::socket(host_family, type, protocol);
if (socket == INVALID_NATIVE_SOCKET) {
return SetPosixSocketError();
return SetHostSocketError();
}
const int fd = AllocSocketFd(socket);
if (fd < 0) {
#if defined(_WIN32)
closesocket(socket);
#else
::close(socket);
#endif
*Posix::GetErrorAddr() = Posix::POSIX_EMFILE;
return -1;
}
LOGF("\t fd = %d\n", fd);
return fd;
#else
*Posix::GetErrorAddr() = Posix::POSIX_ENOSYS;
return -1;
#endif
}
int KYTY_SYSV_ABI Bind(int s, const void* addr, uint32_t addrlen) {
@@ -1645,23 +1686,17 @@ int KYTY_SYSV_ABI Bind(int s, const void* addr, uint32_t addrlen) {
return -1;
}
#if defined(_WIN32)
sockaddr_storage host_addr {};
int host_addrlen = 0;
SocketLength host_addrlen = 0;
if (ConvertGuestSockaddr(addr, addrlen, &host_addr, &host_addrlen) != 0) {
return -1;
}
if (::bind(socket, reinterpret_cast<const sockaddr*>(&host_addr), host_addrlen) ==
SOCKET_ERROR) {
return SetPosixSocketError();
if (::bind(socket, reinterpret_cast<const sockaddr*>(&host_addr), host_addrlen) != 0) {
return SetHostSocketError();
}
return 0;
#else
*Posix::GetErrorAddr() = Posix::POSIX_ENOSYS;
return -1;
#endif
}
int KYTY_SYSV_ABI Connect(int s, const void* addr, uint32_t addrlen) {
@@ -1687,7 +1722,7 @@ int KYTY_SYSV_ABI Connect(int s, const void* addr, uint32_t addrlen) {
if (::connect(socket, reinterpret_cast<const sockaddr*>(&host_addr), host_addrlen) ==
SOCKET_ERROR) {
return SetPosixSocketError();
return SetHostSocketError();
}
return 0;
@@ -1712,7 +1747,7 @@ int KYTY_SYSV_ABI Listen(int s, int backlog) {
#if defined(_WIN32)
if (::listen(socket, backlog) == SOCKET_ERROR) {
return SetPosixSocketError();
return SetHostSocketError();
}
return 0;
@@ -1742,7 +1777,7 @@ int KYTY_SYSV_ABI Accept(int s, void* addr, uint32_t* addrlen) {
NativeSocket accepted =
::accept(socket, reinterpret_cast<sockaddr*>(&host_addr), &host_addrlen);
if (accepted == INVALID_NATIVE_SOCKET) {
return SetPosixSocketError();
return SetHostSocketError();
}
const int fd = AllocSocketFd(accepted);
@@ -1791,7 +1826,7 @@ int KYTY_SYSV_ABI Shutdown(int s, int how) {
#if defined(_WIN32)
if (::shutdown(socket, how) == SOCKET_ERROR) {
return SetPosixSocketError();
return SetHostSocketError();
}
return 0;
@@ -1821,7 +1856,7 @@ int KYTY_SYSV_ABI Getsockname(int s, void* addr, uint32_t* addrlen) {
int host_addrlen = sizeof(host_addr);
if (::getsockname(socket, reinterpret_cast<sockaddr*>(&host_addr), &host_addrlen) ==
SOCKET_ERROR) {
return SetPosixSocketError();
return SetHostSocketError();
}
return ConvertHostSockaddr(&host_addr, host_addrlen, addr, addrlen);
@@ -1850,7 +1885,7 @@ int KYTY_SYSV_ABI Getsockopt(int s, int level, int optname, void* optval, uint32
int len = static_cast<int>(*optlen);
if (::getsockopt(socket, ConvertSocketOptionLevel(level), optname, static_cast<char*>(optval),
&len) == SOCKET_ERROR) {
return SetPosixSocketError();
return SetHostSocketError();
}
*optlen = static_cast<uint32_t>(len);
return 0;
@@ -1881,14 +1916,14 @@ int KYTY_SYSV_ABI Setsockopt(int s, int level, int optname, const void* optval,
optlen >= sizeof(int)) {
u_long enabled = (*static_cast<const int*>(optval) != 0 ? 1 : 0);
if (ioctlsocket(socket, FIONBIO, &enabled) == SOCKET_ERROR) {
return SetPosixSocketError();
return SetHostSocketError();
}
return 0;
}
if (::setsockopt(socket, ConvertSocketOptionLevel(level), optname,
static_cast<const char*>(optval), static_cast<int>(optlen)) == SOCKET_ERROR) {
return SetPosixSocketError();
return SetHostSocketError();
}
return 0;
@@ -1941,7 +1976,7 @@ int64_t KYTY_SYSV_ABI Sendto(int s, const void* buf, uint64_t len, int flags, co
reinterpret_cast<const sockaddr*>(&host_addr), host_addrlen);
}
if (result == SOCKET_ERROR) {
return SetPosixSocketError();
return SetHostSocketError();
}
return result;
@@ -2000,7 +2035,7 @@ int64_t KYTY_SYSV_ABI Recvfrom(int s, void* buf, uint64_t len, int flags, void*
}
}
if (result == SOCKET_ERROR) {
return SetPosixSocketError();
return SetHostSocketError();
}
return result;
@@ -2101,7 +2136,7 @@ int KYTY_SYSV_ABI Select(int nfds, void* readfds, void* writefds, void* exceptfd
const int result = ::select(0, read_ptr, write_ptr, except_ptr, host_timeout_ptr);
if (result == SOCKET_ERROR) {
return SetPosixSocketError();
return SetHostSocketError();
}
GuestFdZero(readfds, nfds);
+1
View File
@@ -40,6 +40,7 @@ int KYTY_SYSV_ABI NetTerm();
int KYTY_SYSV_ABI NetPoolCreate(const char* name, int size, int flags);
int KYTY_SYSV_ABI NetPoolDestroy(int memid);
int KYTY_SYSV_ABI NetResolverCreate(const char* name, int memid, int flags);
int KYTY_SYSV_ABI NetResolverDestroy(int rid);
int KYTY_SYSV_ABI NetResolverStartNtoa(int rid, const char* hostname, void* addr, int timeout,
int retry, int flags);
int KYTY_SYSV_ABI NetInetPton(int af, const char* src, void* dst);
+211 -139
View File
@@ -8,207 +8,235 @@
#include "loader/systemContent.h"
#include <algorithm>
#include <charconv>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <memory>
#include <nlohmann/json.hpp>
#include <string>
#include <string_view>
#include <system_error>
#include <utility>
#include <vector>
namespace Loader::GamePatch {
namespace {
constexpr uint64_t kPageSize = 0x4000;
struct Write {
std::string patch_name;
uint64_t address = 0;
std::vector<uint8_t> expected;
std::vector<uint8_t> replacement;
uint64_t source_address = 0;
uint64_t target_address = 0;
std::vector<uint8_t> off;
std::vector<uint8_t> on;
};
struct Plan {
std::string title_id;
std::string game_version;
std::string version;
std::string process;
std::vector<Write> writes;
std::vector<std::string> patch_names;
std::vector<std::string> mod_names;
std::vector<uint64_t> cave_pages;
};
using Json = nlohmann::json;
std::unique_ptr<Plan> g_pending_plan;
std::vector<uint64_t> g_applied_cave_pages;
bool Fail(std::string* error, std::string message) {
*error = std::move(message);
return false;
}
const Json::string_t* StringField(const Json& object, const char* name) {
const auto value = object.find(name);
return value == object.end() ? nullptr : value->get_ptr<const Json::string_t*>();
}
const Json::array_t* ArrayField(const Json& object, const char* name) {
const auto value = object.find(name);
return value == object.end() ? nullptr : value->get_ptr<const Json::array_t*>();
}
bool ParseBytes(std::string_view text, std::vector<uint8_t>* bytes) {
bool ParseBytes(const std::string& text, std::vector<uint8_t>* bytes) {
if (text.empty() || (text.size() % 2) != 0) {
return false;
}
bytes->resize(text.size() / 2);
for (size_t index = 0; index < bytes->size(); index++) {
unsigned int value = 0;
const char* begin = text.data() + index * 2;
const auto [end, error] = std::from_chars(begin, begin + 2, value, 16);
if (error != std::errc {} || end != begin + 2) {
return false;
}
(*bytes)[index] = static_cast<uint8_t>(value);
(*bytes)[index] = static_cast<uint8_t>(std::stoul(text.substr(index * 2, 2), nullptr, 16));
}
return true;
}
bool ReadJson(const std::filesystem::path& path, Json* root, std::string* error) {
bool LoadPlan(const std::filesystem::path& path, Plan* plan, std::string* error) {
std::ifstream file(path, std::ios::binary);
if (!file) {
return Fail(error, "could not read patch plan");
return Fail(error, "could not read cheat file");
}
*root = Json::parse(file, nullptr, false);
return root->is_object() || Fail(error, "patch plan is not valid JSON");
}
bool LoadPlan(const std::filesystem::path& path, Plan* plan, std::string* error) {
Json root;
if (!ReadJson(path, &root, error)) {
return false;
const auto root = Json::parse(file, nullptr, false);
if (!root.is_object() || !root.contains("id") || !root.contains("version") ||
!root.contains("process") || !root.contains("mods")) {
return Fail(error, "expected a GoldHEN-style mods JSON file");
}
plan->title_id = root["id"].get<std::string>();
plan->version = root["version"].get<std::string>();
plan->process = root["process"].get<std::string>();
const auto* title_id = StringField(root, "title_id");
const auto* game_version = StringField(root, "game_version");
const auto* process = StringField(root, "process");
const auto* patches = ArrayField(root, "patches");
if (title_id == nullptr || game_version == nullptr || process == nullptr ||
patches == nullptr) {
return Fail(error, "invalid patch plan");
}
plan->title_id = *title_id;
plan->game_version = *game_version;
plan->process = *process;
for (const auto& patch_json: *patches) {
const auto enabled = patch_json.find("enabled");
if (enabled != patch_json.end() && enabled->is_boolean() && !enabled->get<bool>()) {
for (const auto& mod: root["mods"]) {
if (!mod.value("enabled", true)) {
continue;
}
const auto* name = StringField(patch_json, "name");
const auto* writes = ArrayField(patch_json, "writes");
if (name == nullptr || writes == nullptr) {
return Fail(error, "invalid patch entry");
}
plan->patch_names.push_back(*name);
for (const auto& write_json: *writes) {
const auto* expected = StringField(write_json, "expected");
const auto* replacement = StringField(write_json, "replacement");
if (expected == nullptr || replacement == nullptr) {
return Fail(error, "invalid patch write");
const auto name = mod["name"].get<std::string>();
plan->mod_names.push_back(name);
for (const auto& entry: mod["memory"]) {
Write write;
write.source_address = std::stoull(entry["offset"].get<std::string>(), nullptr, 16);
if (!ParseBytes(entry["off"].get<std::string>(), &write.off) ||
!ParseBytes(entry["on"].get<std::string>(), &write.on) ||
write.off.size() != write.on.size()) {
return Fail(error, "invalid mod memory bytes");
}
Write write {
.patch_name = *name,
};
if (!ParseBytes(*expected, &write.expected) ||
!ParseBytes(*replacement, &write.replacement) ||
write.expected.size() != write.replacement.size()) {
return Fail(error, "invalid patch bytes");
}
plan->writes.push_back(std::move(write));
}
}
return true;
}
bool ValidateTarget(const Plan& plan, const Program* program, std::string* error) {
if (program == nullptr || program->elf == nullptr || program->base_vaddr == 0) {
bool ValidateGame(const Plan& plan, const Program* main_program, std::string* error) {
if (main_program == nullptr || main_program->base_vaddr == 0) {
return Fail(error, "main executable is not loaded");
}
std::string title_id;
std::string game_version;
std::string version;
if (!SystemContentParamSfoGetString("TITLE_ID", &title_id) ||
!SystemContentParamSfoGetString("APP_VER", &game_version)) {
return Fail(error, "game metadata is incomplete");
}
if (!Common::EqualNoCase(program->file_name.filename().string(), plan.process) ||
!Common::EqualNoCase(title_id, plan.title_id) || game_version != plan.game_version) {
return Fail(error, "patch plan does not match the loaded game");
!SystemContentParamSfoGetString("APP_VER", &version) ||
!Common::EqualNoCase(main_program->file_name.filename().string(), plan.process) ||
!Common::EqualNoCase(title_id, plan.title_id) || version != plan.version) {
return Fail(error, "cheat file does not match the loaded game");
}
return true;
}
bool ResolveWrite(const Program& program, Write* write, std::string* error) {
const auto* ehdr = program.elf->GetEhdr();
const auto* phdr = program.elf->GetPhdr();
uint64_t match = 0;
size_t match_count = 0;
for (Elf64_Half index = 0; index < ehdr->e_phnum; index++) {
const auto& segment = phdr[index];
const bool loaded = segment.p_type == PT_LOAD || segment.p_type == PT_OS_RELRO;
if (!loaded || segment.p_filesz < write->expected.size()) {
continue;
}
const auto segment_address = program.base_vaddr + segment.p_vaddr;
const auto* begin = reinterpret_cast<const uint8_t*>(segment_address);
const auto* end = begin + segment.p_filesz;
for (auto* current = begin; current < end;) {
const auto* found =
std::search(current, end, write->expected.begin(), write->expected.end());
if (found == end) {
break;
}
const auto address = reinterpret_cast<uint64_t>(found);
if (match == 0) {
match = address;
}
match_count++;
current = found + 1;
}
void Translate(const Program& program, uint64_t source_base, uint64_t source_address,
uint64_t* target_address) {
if (source_address >= source_base) {
*target_address = program.base_vaddr + source_address - source_base;
return;
}
::printf("Game patch: found %zu entries for '%s'\n", match_count, write->patch_name.c_str());
if (match == 0) {
return Fail(error, "original bytes not found for '" + write->patch_name + "'");
}
write->address = match;
return true;
*target_address = program.base_vaddr - (source_base - source_address);
}
bool PrepareWrites(Plan* plan, const Program& program, std::string* error) {
bool IsInsideProgram(const Program& program, uint64_t address, size_t size) {
return size != 0 && address >= program.base_vaddr &&
address + size <= program.base_vaddr + program.mapped_size;
}
bool IsZero(const std::vector<uint8_t>& bytes) {
return std::all_of(bytes.begin(), bytes.end(), [](uint8_t byte) { return byte == 0; });
}
bool TranslateAndValidate(Plan* plan, const Program& program, uint64_t source_base) {
for (auto& write: plan->writes) {
if (!ResolveWrite(program, &write, error)) {
Translate(program, source_base, write.source_address, &write.target_address);
if (!IsZero(write.off) &&
(!IsInsideProgram(program, write.target_address, write.off.size()) ||
std::memcmp(reinterpret_cast<const void*>(write.target_address), write.off.data(),
write.off.size()) != 0)) {
return false;
}
}
return true;
}
bool ApplyWrites(Plan* plan, std::string* error) {
for (auto& write: plan->writes) {
const auto size = write.replacement.size();
std::memcpy(reinterpret_cast<void*>(write.address), write.replacement.data(), size);
if (!Common::VirtualMemory::FlushInstructionCache(write.address, size)) {
return Fail(error, "could not finalize patch");
bool Matches(Plan* plan, const Program& program, uint64_t* source_base) {
const auto anchor = std::find_if(plan->writes.begin(), plan->writes.end(),
[](const Write& write) { return !IsZero(write.off); });
if (anchor == plan->writes.end() || program.mapped_size < anchor->off.size()) {
return false;
}
const auto* ehdr = program.elf->GetEhdr();
const auto* phdr = program.elf->GetPhdr();
for (Elf64_Half index = 0; index < ehdr->e_phnum; index++) {
const auto& segment = phdr[index];
if ((segment.p_type != PT_LOAD && segment.p_type != PT_OS_RELRO) ||
segment.p_filesz < anchor->off.size()) {
continue;
}
const auto* begin = reinterpret_cast<const uint8_t*>(program.base_vaddr + segment.p_vaddr);
const auto* end = begin + segment.p_filesz;
for (auto* current = begin; current < end;) {
const auto* found = std::search(current, end, anchor->off.begin(), anchor->off.end());
if (found == end) {
break;
}
const auto rva = static_cast<uint64_t>(found - begin) + segment.p_vaddr;
if (anchor->source_address >= rva) {
*source_base = anchor->source_address - rva;
if (TranslateAndValidate(plan, program, *source_base)) {
return true;
}
}
current = found + 1;
}
}
return false;
}
void ReleaseCaves(std::vector<uint64_t>* cave_pages) {
for (auto page = cave_pages->rbegin(); page != cave_pages->rend(); ++page) {
EXIT_IF(!Libs::LibKernel::Memory::FreeGuestMemory(*page, kPageSize));
}
cave_pages->clear();
}
bool AllocateCave(Plan* plan, const Program& program, std::string* error) {
std::vector<uint64_t> pages;
for (const auto& write: plan->writes) {
if (IsInsideProgram(program, write.target_address, write.off.size())) {
continue;
}
if (!IsZero(write.off)) {
return Fail(error, "non-zero write is outside the matched module");
}
const auto write_end = write.target_address + write.off.size();
for (auto page = write.target_address & ~(kPageSize - 1); page < write_end;
page += kPageSize) {
if (std::find(pages.begin(), pages.end(), page) == pages.end()) {
pages.push_back(page);
}
}
}
for (const auto page: pages) {
const auto allocated = Libs::LibKernel::Memory::AllocateRuntimeMemory(
page, kPageSize, Common::VirtualMemory::Mode::ExecuteReadWrite, "game_cheat_code_cave",
true);
if (allocated != page) {
if (allocated != 0) {
Libs::LibKernel::Memory::FreeGuestMemory(allocated, kPageSize);
}
ReleaseCaves(&plan->cave_pages);
return Fail(error, "could not allocate code-cave page");
}
plan->cave_pages.push_back(page);
}
return true;
}
bool ApplyWrites(Plan* plan, const Program& program, std::string* error) {
if (!AllocateCave(plan, program, error)) {
return false;
}
for (int external = 1; external >= 0; external--) {
for (const auto& write: plan->writes) {
if (static_cast<int>(
!IsInsideProgram(program, write.target_address, write.on.size())) != external) {
continue;
}
std::memcpy(reinterpret_cast<void*>(write.target_address), write.on.data(),
write.on.size());
if (!Common::VirtualMemory::FlushInstructionCache(write.target_address,
write.on.size())) {
return Fail(error, "could not finalize cheat write");
}
}
}
return true;
@@ -216,19 +244,63 @@ bool ApplyWrites(Plan* plan, std::string* error) {
} // namespace
bool Apply(const std::filesystem::path& plan_path, Program* program) {
bool Apply(const std::filesystem::path& plan_path, Program* main_program,
const std::vector<Program*>& programs) {
Plan plan;
std::string error;
if (LoadPlan(plan_path, &plan, &error) && ValidateTarget(plan, program, &error) &&
PrepareWrites(&plan, *program, &error) && ApplyWrites(&plan, &error)) {
for (const auto& name: plan.patch_names) {
::printf("Successfully applied patch: %s\n", name.c_str());
}
if (!LoadPlan(plan_path, &plan, &error) || !ValidateGame(plan, main_program, &error)) {
::printf("Game cheat error: %s\n", error.c_str());
return false;
}
if (plan.writes.empty()) {
return true;
}
::printf("Game patch error: %s\n", error.c_str());
return false;
g_pending_plan = std::make_unique<Plan>(std::move(plan));
for (auto* program: programs) {
if (!ApplyPending(program)) {
return false;
}
if (g_pending_plan == nullptr) {
return true;
}
}
::printf("Game cheat: waiting for the matching module\n");
return true;
}
bool ApplyPending(Program* program) {
uint64_t source_base = 0;
if (g_pending_plan == nullptr || program == nullptr || program->elf == nullptr ||
!Matches(g_pending_plan.get(), *program, &source_base)) {
return true;
}
::printf("Game cheat: matched %s with source base 0x%llx\n",
program->file_name.filename().string().c_str(),
static_cast<unsigned long long>(source_base));
std::string error;
if (!ApplyWrites(g_pending_plan.get(), *program, &error)) {
ReleaseCaves(&g_pending_plan->cave_pages);
g_pending_plan.reset();
::printf("Game cheat error: %s\n", error.c_str());
return false;
}
for (const auto& name: g_pending_plan->mod_names) {
::printf("Successfully applied cheat: %s\n", name.c_str());
}
g_applied_cave_pages = std::move(g_pending_plan->cave_pages);
g_pending_plan.reset();
return true;
}
void Clear() {
if (g_pending_plan != nullptr) {
ReleaseCaves(&g_pending_plan->cave_pages);
}
g_pending_plan.reset();
ReleaseCaves(&g_applied_cave_pages);
}
} // namespace Loader::GamePatch
+5 -1
View File
@@ -4,6 +4,7 @@
#include "common/common.h"
#include <filesystem>
#include <vector>
namespace Loader {
@@ -11,7 +12,10 @@ struct Program;
namespace GamePatch {
bool Apply(const std::filesystem::path& plan_path, Program* program);
bool Apply(const std::filesystem::path& plan_path, Program* main_program,
const std::vector<Program*>& programs);
bool ApplyPending(Program* program);
void Clear();
} // namespace GamePatch
+12 -2
View File
@@ -801,7 +801,10 @@ static bool KytyExceptionHandler(const Common::HostException::ExceptionInfo& exc
return true;
}
}
return false;
EXIT("Unhandled host exception: type=%u code=%u pc=0x%016" PRIx64
" access=%u address=0x%016" PRIx64 "\n",
static_cast<unsigned>(info->type), info->native_code, info->exception_address,
static_cast<unsigned>(info->access_violation_type), info->access_violation_vaddr);
}
static void EncodeId64(uint16_t in_id, std::string* out_id) {
@@ -1286,6 +1289,9 @@ void RuntimeLinker::RelocateProgram(Program* program) {
EXIT_IF(std::find(m_programs.begin(), m_programs.end(), program) == m_programs.end());
Relocate(program);
if (!GamePatch::ApplyPending(program)) {
EXIT("Failed to apply pending game cheat\n");
}
}
void RuntimeLinker::UnloadProgram(Program* program) {
@@ -1409,7 +1415,10 @@ void RuntimeLinker::Execute(const std::filesystem::path& game_patch) {
RelocateAll();
if (!game_patch.empty()) {
GamePatch::Apply(game_patch, m_programs.empty() ? nullptr : m_programs.front());
if (!GamePatch::Apply(game_patch, m_programs.empty() ? nullptr : m_programs.front(),
m_programs)) {
EXIT("Failed to apply game cheat\n");
}
}
StartAllModules();
@@ -1433,6 +1442,7 @@ void RuntimeLinker::Clear() {
// EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread());
Common::LockGuard lock(m_mutex);
GamePatch::Clear();
for (auto* p: m_programs) {
DeleteProgram(p);
+35 -2
View File
@@ -43,10 +43,15 @@ static void PrintUsage() {
::printf("kyty_emulator --game <dir|elf> [options]\n\n");
::printf("Options:\n");
::printf(" --game <dir|elf> Game directory or ELF to load.\n");
::printf(
" --game-patch <json> Validated patch plan to apply before entry.\n");
::printf(" --game-patch <json> ETAHen cheat file.\n");
::printf(" --screen-width <num> Window width. Default: 1280.\n");
::printf(" --screen-height <num> Window height. Default: 720.\n");
::printf(
" --user-name <name> Local user name (1-16 bytes). Default: Kyty.\n");
::printf(" --user-id <num> Local user ID. Default: %d.\n",
Config::DEFAULT_USER_ID);
::printf(
" --present-mode <value> Fifo, Mailbox, or Immediate. Default: Fifo.\n");
::printf(" --fullscreen Run in borderless desktop fullscreen.\n");
::printf(" --vblank-frequency <num> Virtual vblank frequency. Default: 60.\n");
::printf(" --console-language <0-29> Console language. Default: 1 (English US).\n");
@@ -122,6 +127,17 @@ static bool ParseConsoleLanguage(const std::string& value, uint32_t& out) {
return true;
}
static bool ParseUserId(const std::string& value, int32_t& out) {
int32_t user_id = 0;
auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), user_id);
if (error != std::errc {} || end != value.data() + value.size() ||
!Config::IsConfiguredUserIdValid(user_id)) {
return false;
}
out = user_id;
return true;
}
static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_help) {
show_help = false;
@@ -201,6 +217,23 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
options.config.screen_width = static_cast<uint32_t>(Common::ToInt32(value));
} else if (arg == "--screen-height") {
options.config.screen_height = static_cast<uint32_t>(Common::ToInt32(value));
} else if (arg == "--user-name") {
if (value.empty() || value.size() > Config::MAX_USER_NAME_LENGTH) {
::printf("invalid user name: must contain 1-%zu bytes\n",
Config::MAX_USER_NAME_LENGTH);
return false;
}
options.config.user_name = value;
} else if (arg == "--user-id") {
if (!ParseUserId(value, options.config.user_id)) {
::printf("invalid user ID: %s\n", value.c_str());
return false;
}
} else if (arg == "--present-mode") {
if (!ParseEnum(value, options.config.present_mode)) {
::printf("invalid present mode: %s\n", value.c_str());
return false;
}
} else if (arg == "--vblank-frequency") {
const int32_t vblank_frequency = Common::ToInt32(value);
options.config.vblank_frequency =
+448 -42
View File
@@ -7283,26 +7283,30 @@ public:
std::printf("[gpu] %-32s ok\n", name);
}
void CheckRenderExecutorColorStandard4TileDiscovery() {
constexpr const char *name = "RenderExecutorColorStandard4Tile";
void CheckRenderExecutorColorStandardTileDiscovery() {
constexpr const char *name = "RenderExecutorColorStandardTile";
constexpr uintptr_t base = 0x0000000203b00000ull;
constexpr uint64_t target_address = base + 0x4000;
constexpr uint64_t target_size = 0x180000;
constexpr uint64_t allocation_size = 0x184000;
constexpr uint64_t allocation_alignment = 0x4000;
constexpr uint64_t allocation_size = 0x190000;
constexpr uint64_t allocation_alignment = 0x10000;
constexpr auto format = Prospero::BufferFormat::k32Float;
constexpr auto tile = Prospero::TileMode::kStandard4KB;
struct TileCase {
Prospero::TileMode tile;
uint64_t target_offset;
uint64_t alignment;
};
constexpr std::array cases{
TileCase{Prospero::TileMode::kStandard4KB, 0x4000, 0x1000},
TileCase{Prospero::TileMode::kStandard64KB, 0x10000, 0x10000},
};
EnsureRuntimeContext();
const auto pitch = TileGetTexturePitch(format, 256, tile);
TileSizeAlign slice{};
TileGetTextureSize(format, 256, 256, 1, tile, &slice, nullptr, nullptr);
const auto odd_pitch = TileGetTexturePitch(format, 257, tile);
const auto odd_pitch =
TileGetTexturePitch(format, 257, Prospero::TileMode::kStandard4KB);
TileSizeAlign odd_slice{};
TileGetTextureSize(format, 257, 131, 1, tile, &odd_slice, nullptr, nullptr);
TileGetTextureSize(format, 257, 131, 1, Prospero::TileMode::kStandard4KB,
&odd_slice, nullptr, nullptr);
Require(name, "tile layout",
pitch == 256 && slice.size == 0x40000 && slice.align == 0x1000 &&
odd_pitch == 288 && odd_slice.size == 0x2d000 &&
odd_pitch == 288 && odd_slice.size == 0x2d000 &&
odd_slice.align == 0x1000,
"Standard4KB pitch, footprint, or block alignment is incorrect");
@@ -7311,17 +7315,29 @@ public:
Libs::LibKernel::Memory::KernelAllocateDirectMemory(
0, Libs::LibKernel::Memory::KernelGetDirectMemorySize(),
allocation_size, allocation_alignment, 0, &direct_offset) == 0,
"Standard4KB color allocation failed");
"standard-tiled color allocation failed");
void *mapped = reinterpret_cast<void *>(base);
Require(name, "direct mapping",
Libs::LibKernel::Memory::KernelMapDirectMemory(
&mapped, allocation_size, 0x3, 0x10, direct_offset,
allocation_alignment) == 0 &&
mapped == reinterpret_cast<void *>(base),
"Standard4KB color mapping failed");
"standard-tiled color mapping failed");
std::memset(mapped, 0, allocation_size);
{
for (const auto &tile_case : cases) {
const auto pitch = TileGetTexturePitch(format, 256, tile_case.tile);
TileSizeAlign slice{};
TileGetTextureSize(format, 256, 256, 1, tile_case.tile, &slice, nullptr,
nullptr);
Require(
name, "tile layout",
pitch == 256 && slice.size == 0x40000 &&
slice.align == tile_case.alignment,
"standard tile pitch, footprint, or block alignment is incorrect");
const auto target_address = base + tile_case.target_offset;
const auto target_size = slice.size * 6ull;
RenderContext context(m_runtime_context);
auto &scheduler = context.GetCommandScheduler();
HW::Context registers{};
@@ -7335,7 +7351,7 @@ public:
registers.SetColorView(
0, {.base_array_slice_index = 5, .last_array_slice_index = 5});
registers.SetColorAttrib2(0, {.height = 255, .width = 255});
registers.SetColorAttrib3(0, {.tile_mode = tile,
registers.SetColorAttrib3(0, {.tile_mode = tile_case.tile,
.dimension = 1,
.cmask_pipe_aligned = true,
.dcc_pipe_aligned = true});
@@ -7358,7 +7374,7 @@ public:
color.base_array_layer == 5 &&
color.desc.info.data.address == target_address &&
color.desc.info.data.size == target_size &&
color.desc.info.tile_mode == tile &&
color.desc.info.tile_mode == tile_case.tile &&
color.desc.info.extent == vk::Extent3D{256, 256, 1} &&
color.desc.info.resources == ImageSubresources{1, 6} &&
color.desc.info.pitch == pitch &&
@@ -7367,9 +7383,9 @@ public:
color.desc.view_info.type == vk::ImageViewType::e2D &&
color.desc.view_info.base_layer == 5 &&
color.desc.view_info.layer_count == 1 &&
(target_address & 0xffffu) == 0x4000 &&
(target_address & (tile_case.alignment - 1)) == 0 &&
image.usage.render_target && image.IsGpuModified(),
"slice 5 Standard4KB render target was not resolved");
"slice 5 standard-tiled render target was not resolved");
RenderExecutorTestAccess::ResetBindings(executor);
resources.UnmapMemory(base, allocation_size);
@@ -7378,11 +7394,11 @@ public:
Require(name, "unmap direct backing",
Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0,
"Standard4KB color mapping release failed");
"standard-tiled color mapping release failed");
Require(name, "release direct backing",
Libs::LibKernel::Memory::KernelReleaseDirectMemory(
direct_offset, allocation_size) == 0,
"Standard4KB color allocation release failed");
"standard-tiled color allocation release failed");
std::printf("[gpu] %-32s ok\n", name);
}
@@ -11880,6 +11896,8 @@ CoverageClass ClassifyOpcode(ShaderOpcode opcode,
case Opcode::V_RSQ_F16:
case Opcode::V_LOG_F16:
case Opcode::V_EXP_F16:
case Opcode::V_SIN_F16:
case Opcode::V_COS_F16:
case Opcode::V_MAD_MIXLO_F16:
case Opcode::V_MAD_MIXHI_F16:
case Opcode::DS_MIN_F32:
@@ -12013,11 +12031,13 @@ CoverageClass ClassifyOpcode(ShaderOpcode opcode,
case Opcode::DS_XOR_RTN_B32:
case Opcode::DS_WRXCHG_RTN_B32:
case Opcode::DS_SWIZZLE_B32:
case Opcode::DS_BPERMUTE_B32:
case Opcode::DS_READ_I8:
case Opcode::DS_READ_U8:
case Opcode::DS_READ_I16:
case Opcode::DS_READ_U16:
case Opcode::DS_READ_U16_D16:
case Opcode::DS_READ_U16_D16_HI:
case Opcode::DS_READ2_B32:
case Opcode::DS_READ_B32:
case Opcode::DS_READ_B64:
@@ -12026,6 +12046,7 @@ CoverageClass ClassifyOpcode(ShaderOpcode opcode,
case Opcode::DS_READ_B128:
case Opcode::DS_WRITE_B8:
case Opcode::DS_WRITE_B16:
case Opcode::DS_WRITE_B16_D16_HI:
case Opcode::DS_WRITE2_B32:
case Opcode::DS_WRITE2ST64_B32:
case Opcode::DS_WRITE_B32:
@@ -13355,6 +13376,83 @@ TestCase VectorIntegerOps() {
O::S_ENDPGM}};
}
TestCase VectorFfbhI32NativeAndVop3OnGpu() {
using O = ShaderOpcode;
constexpr std::array inputs = {0x00000000u, 0x40000000u, 0x80000000u,
0x0fffffffu, 0xffff0000u, 0xfffffffeu,
0xffffffffu, 0x80000000u};
std::vector<u32> code;
for (u32 i = 0; i < inputs.size() - 1u; i++) {
AppendVMovU32(&code, 30, i * 4u);
AppendBufferLoadDword(&code, 14, 30);
code.push_back(0x7e1c770eu); // v_ffbh_i32 v14, v14
AppendStoreVgpr(&code, 14, i);
}
AppendVMovU32(&code, 30, (inputs.size() - 1u) * 4u);
AppendBufferLoadDword(&code, 14, 30);
AppendVop3(&code, 0x1bbu, 15, Vgpr(14), 0);
AppendStoreVgpr(&code, 15, inputs.size() - 1u);
AppendEnd(&code);
TestCase test;
test.name = "VectorFfbhI32NativeAndVop3OnGpu";
test.code = std::move(code);
test.initial.assign(inputs.begin(), inputs.end());
test.expected = {0xffffffffu, 1u, 1u, 4u, 16u, 31u, 0xffffffffu, 1u};
test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_FFBH_I32,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.decoded_counts = {{"V_FFBH_I32 v14, v14", inputs.size() - 1u},
{"V_FFBH_I32 v15, v14", 1u}};
return test;
}
TestCase Vop1SdwaFfblCapturedHighWordSource() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendBufferLoadDword(&code, 2, 30);
code.push_back(0x7e2074f9u);
code.push_back(0x00050602u); // v_ffbl_b32 v16, v2.word1
AppendStoreVgpr(&code, 16, 0);
AppendEnd(&code);
TestCase test;
test.name = "Vop1SdwaFfblCapturedHighWordSource";
test.code = std::move(code);
test.initial = {0x00400000u};
test.expected = {6u};
test.opcodes = {O::BUFFER_LOAD_DWORD, O::V_FFBL_B32, O::BUFFER_STORE_DWORD,
O::S_ENDPGM};
test.decoded_counts = {{"V_FFBL_B32 v16, v2.sdwa(sel=5,sext=0)", 1}};
test.ir_counts = {{" = BitFieldUExtract ", 1}, {" = FindILsb32 ", 1}};
test.required_spirv = {"OpBitFieldUExtract"};
return test;
}
TestCase Vop1SdwaNotCapturedByte0Source() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendBufferLoadDword(&code, 2, 30);
code.push_back(0x7e046ef9u);
code.push_back(0x00000602u); // v_not_b32 v2, v2.byte0
AppendStoreVgpr(&code, 2, 0);
AppendEnd(&code);
TestCase test;
test.name = "Vop1SdwaNotCapturedByte0Source";
test.code = std::move(code);
test.initial = {0x12345678u};
test.expected = {0xffffff87u};
test.opcodes = {O::BUFFER_LOAD_DWORD, O::V_NOT_B32, O::BUFFER_STORE_DWORD,
O::S_ENDPGM};
test.decoded_counts = {{"V_NOT_B32 v2, v2.sdwa(sel=0,sext=0)", 1}};
test.ir_counts = {{" = BitFieldUExtract ", 1}, {" = BitwiseNot32 ", 1}};
test.required_spirv = {"OpBitFieldUExtract", "OpNot"};
return test;
}
TestCase Vop2SdwaSubNcExactByte2Destination() {
using O = ShaderOpcode;
@@ -13862,37 +13960,35 @@ TestCase VectorAddcUsesPerLaneCarryIn() {
TestCase VectorAddcWritesPerLaneCarryOut() {
using O = ShaderOpcode;
std::vector<u32> code = {
EncodeVop1(0x01, 1, 0),
EncodeVop2(0x1a, 1, InlineU32(2), 1),
EncodeVop2(0x25, 1, Vgpr(0), 1),
EncodeVopc(0xc0, Vgpr(0), 0),
};
AppendVMovLiteral(&code, 2, 0xffffffffu);
code.push_back(EncodeVop1(0x01, 3, InlineU32(1)));
code.push_back(EncodeVop2(0x28, 5, Vgpr(2), 3));
std::vector<u32> code = {EncodeVop1(0x01, 1, Vgpr(0)),
EncodeVop2(0x1a, 4, InlineU32(2), 1),
EncodeVopc(0xc7, Vgpr(0), 0)};
AppendBufferLoadDword(&code, 26, 4);
code.push_back(0x500434f9u);
code.push_back(0x0c860680u);
code.push_back(EncodeVop1(0x01, 6, 106));
code.push_back(EncodeVop2(0x1a, 4, InlineU32(2), 1));
AppendBufferStoreDword(&code, 6, 4);
AppendEnd(&code);
TestCase test;
test.name = "VectorAddcWritesPerLaneCarryOut";
test.code = code;
test.initial = {0x0000ffffu, 0u, 0x0000ffffu, 0u};
// VCC is represented as the current invocation's carry bit, not a shared
// four-lane ballot word.
test.expected = std::vector<u32>(8, 1u);
test.opcodes = {O::V_MOV_B32, O::V_LSHLREV_B32, O::V_ADD_NC_U32,
O::V_CMP_F_U32, O::V_ADDC_U32, O::BUFFER_STORE_DWORD,
test.expected = {1u, 0u, 1u, 0u};
test.opcodes = {O::V_MOV_B32, O::V_LSHLREV_B32, O::V_CMP_T_U32,
O::BUFFER_LOAD_DWORD, O::V_ADDC_U32, O::BUFFER_STORE_DWORD,
O::S_ENDPGM};
test.decoded_counts = {
{"V_ADDC_U32 v2, vcc_lo, 0, v26.sdwa(sel=4,sext=1), vcc_lo", 1}};
test.ir_counts = {{" = BitFieldSExtract ", 1}, {" = IAddCarry32 ", 2}};
test.required_spirv = {"OpBitFieldSExtract"};
test.compute_info.threads_num[0] = 4;
test.compute_info.threads_num[1] = 1;
test.compute_info.threads_num[2] = 1;
test.compute_info.group_id[0] = true;
test.compute_info.thread_ids_num = 1;
test.compute_info.workgroup_register = 0;
test.has_compute_info = true;
test.dispatch_x = 2;
return test;
}
@@ -14395,6 +14491,91 @@ TestCase VectorSpecialF16Ops() {
O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
}
TestCase VectorCosF16CapturedSdwaAndEdges() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendVMovLiteral(&code, 4, 0x34001234u); // high=+0.25h, low sentinel
code.push_back(0x7e08c2f9u);
code.push_back(0x00051504u); // captured v_cos_f16 v4.word1, v4.word1
AppendVMovLiteral(&code, 5, 0x00008000u); // -0.0h
code.push_back(EncodeVop1(0x61, 6, Vgpr(5)));
AppendVMovLiteral(&code, 7, 0x00007bffu); // max finite
code.push_back(EncodeVop1(0x61, 8, Vgpr(7)));
AppendVMovLiteral(&code, 9, 0x00007c00u); // +inf
code.push_back(EncodeVop1(0x61, 10, Vgpr(9)));
AppendVMovLiteral(&code, 11, 0x00000001u); // minimum positive denormal
code.push_back(EncodeVop1(0x61, 12, Vgpr(11)));
AppendVMovLiteral(&code, 13, 0x00003800u); // +0.5h
code.push_back(EncodeVop1(0x61, 14, Vgpr(13)));
for (u32 i = 0; i < 6; i++) {
AppendStoreVgpr(&code, 4 + i * 2, i);
}
AppendEnd(&code);
TestCase test{"VectorCosF16CapturedSdwaAndEdges",
code,
{},
{0x00001234u, 0x00003c00u, 0x00003c00u, 0x0000fe00u,
0x00003c00u, 0x0000bc00u},
{O::V_MOV_B32, O::V_COS_F16, O::BUFFER_STORE_DWORD,
O::S_ENDPGM}};
test.decoded_counts = {{"V_COS_F16", 6},
{"V_COS_F16 v4.sdwa(sel=5,sext=0), "
"v4.sdwa(sel=5,sext=0)",
1}};
test.ir_counts = {{" = FPCos ", 6}};
test.required_spirv = {" Fract ", " Cos ", " PackHalf2x16 "};
test.forbidden_spirv = {"OpCapability Float16"};
return test;
}
TestCase VectorSinF16SdwaAndEdges() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendVMovLiteral(&code, 4, 0x34001234u); // high=+0.25h, low sentinel
code.push_back(0x7e08c0f9u);
code.push_back(0x00051504u); // v_sin_f16 v4.word1, v4.word1
AppendVMovLiteral(&code, 5, 0x00008000u); // -0.0h
code.push_back(EncodeVop1(0x60, 6, Vgpr(5)));
AppendVMovLiteral(&code, 7, 0x00007bffu); // max finite
code.push_back(EncodeVop1(0x60, 8, Vgpr(7)));
AppendVMovLiteral(&code, 9, 0x0000fbffu); // minimum finite
code.push_back(EncodeVop1(0x60, 10, Vgpr(9)));
AppendVMovLiteral(&code, 11, 0x00007c00u); // +inf
code.push_back(EncodeVop1(0x60, 12, Vgpr(11)));
AppendVMovLiteral(&code, 13, 0x0000fc00u); // -inf
code.push_back(EncodeVop1(0x60, 14, Vgpr(13)));
AppendVMovLiteral(&code, 15, 0x00003800u); // +0.5h
code.push_back(EncodeVop1(0x60, 16, Vgpr(15)));
AppendVMovLiteral(&code, 17, 0x00003a00u); // +0.75h
code.push_back(EncodeVop1(0x60, 18, Vgpr(17)));
AppendVMovLiteral(&code, 19, 0x00003400u); // +0.25h
AppendVop3(&code, 0x1e0, 20, Vgpr(19), 0);
for (u32 i = 0; i < 9; i++) {
AppendStoreVgpr(&code, 4 + i * 2, i);
}
AppendEnd(&code);
TestCase test{"VectorSinF16SdwaAndEdges",
code,
{},
{0x3c001234u, 0x00008000u, 0x00000000u, 0x00000000u,
0x0000fe00u, 0x0000fe00u, 0x00000000u, 0x0000bc00u,
0x00003c00u},
{O::V_MOV_B32, O::V_SIN_F16, O::BUFFER_STORE_DWORD,
O::S_ENDPGM}};
test.decoded_counts = {{"V_SIN_F16", 9},
{"V_SIN_F16 v4.sdwa(sel=5,sext=0), "
"v4.sdwa(sel=5,sext=0)",
1}};
test.ir_counts = {{" = FPSin ", 9}};
test.required_spirv = {" Fract ", " Sin ", " PackHalf2x16 "};
test.forbidden_spirv = {"OpCapability Float16"};
return test;
}
TestCase VectorWritelaneIgnoresExecMask() {
using O = ShaderOpcode;
@@ -15367,6 +15548,36 @@ TestCase VectorCompareOps() {
O::V_CMP_T_U32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
}
TestCase VectorVop3CompareEqU64OnGpu() {
using O = ShaderOpcode;
std::vector<u32> code;
code.push_back(EncodeVop1(0x01, 1, InlineU32(1)));
code.push_back(EncodeSop1(0x04, 106, 126)); // s_mov_b64 vcc, exec
code.push_back(0xd4e2006au);
code.push_back(0x0000d47eu); // v_cmp_eq_u64 vcc, exec, vcc
code.push_back(EncodeVop2(0x01, 2, InlineU32(0), 1));
AppendStoreVgpr(&code, 2, 0);
AppendSMovLiteral(&code, 107, 1); // high-dword mismatch
code.push_back(0xd4e2006au);
code.push_back(0x0000d47eu); // v_cmp_eq_u64 vcc, exec, vcc
code.push_back(EncodeVop2(0x01, 3, InlineU32(0), 1));
AppendStoreVgpr(&code, 3, 1);
AppendEnd(&code);
TestCase test{"VectorVop3CompareEqU64OnGpu",
code,
{},
{1, 0},
{O::V_MOV_B32, O::S_MOV_B64, O::V_CMP_EQ_U64,
O::V_CNDMASK_B32, O::S_MOV_B32, O::BUFFER_STORE_DWORD,
O::S_ENDPGM}};
test.decoded_counts = {{"V_CMP_EQ_U64 vcc_lo, exec_lo, vcc_lo", 2}};
return test;
}
TestCase VectorVop3CompareNeU64OnGpu() {
using O = ShaderOpcode;
@@ -17992,6 +18203,64 @@ TestCase DsReadU16D16CapturedPreservesHighHalf() {
return test;
}
TestCase DsWriteB16D16HiCapturedUsesHighHalf() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendVMovU32(&code, 1, 0);
AppendVMovLiteral(&code, 3, 0xdeadbeefu);
code.push_back(EncodeDs0(0x0d));
code.push_back(EncodeDs1(0, 3, 1)); // ds_write_b32 v1, v3
AppendVMovU32(&code, 20, 2);
AppendVMovLiteral(&code, 2, 0x1234abcdu);
AppendVMovLiteral(&code, 0, 0x87654321u);
code.push_back(0xda840000u);
code.push_back(0x00000214u); // captured ds_write_b16_d16_hi v20, v2
code.push_back(EncodeDs0(0x36));
code.push_back(EncodeDs1(4, 0, 1)); // ds_read_b32 v4, v1
AppendStoreVgpr(&code, 4, 0);
AppendStoreVgpr(&code, 0, 1);
AppendEnd(&code);
TestCase test{"DsWriteB16D16HiCapturedUsesHighHalf",
code,
std::vector<u32>(2, 0),
{0x1234beefu, 0x87654321u},
{O::V_MOV_B32, O::DS_WRITE_B32, O::DS_WRITE_B16_D16_HI,
O::DS_READ_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
test.decoded_counts = {{"DS_WRITE_B16_D16_HI", 1}};
return test;
}
TestCase DsReadU16D16HiCapturedPreservesLowHalf() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendVMovU32(&code, 1, 0);
AppendVMovLiteral(&code, 2, 0x89abcdefu);
code.push_back(EncodeDs0(0x0d));
code.push_back(EncodeDs1(0, 2, 1)); // ds_write_b32 v2, v1
AppendVMovLiteral(&code, 5, 0x12345678u);
AppendVMovU32(&code, 6, 0);
code.push_back(0xda9c0000u);
code.push_back(0x05000006u); // captured ds_read_u16_d16_hi v5, v6
AppendStoreVgpr(&code, 5, 0);
AppendEnd(&code);
TestCase test{"DsReadU16D16HiCapturedPreservesLowHalf",
code,
std::vector<u32>(1, 0),
{0xcdef5678u},
{O::V_MOV_B32, O::DS_WRITE_B32, O::DS_READ_U16_D16_HI,
O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
test.decoded_counts = {{"DS_READ_U16_D16_HI", 1}};
return test;
}
TestCase DsReadWrite2Variants() {
using O = ShaderOpcode;
@@ -18377,6 +18646,89 @@ TestCase DsSwizzleInvalidSourceLaneZero() {
return test;
}
TestCase DsBpermuteCapturedExecOffsetAndWrap() {
using O = ShaderOpcode;
std::vector<u32> code;
code.push_back(EncodeVop2(0x1a, 30, InlineU32(2), 0));
AppendBufferLoadDword(&code, 17, 30);
AppendVMovLiteral(&code, 1, 0xdeadbeefu);
AppendVMovU32(&code, 3, 100);
code.push_back(EncodeVop2(0x25, 3, Vgpr(0), 3));
code.push_back(EncodeSop1(0x04, 4, 126));
code.push_back(EncodeVop2(0x1b, 18, InlineU32(1), 0));
code.push_back(EncodeVopc(0xc2, InlineU32(1), 18));
code.push_back(EncodeSop1(0x04, 126, 106));
code.push_back(0xdacc0000u);
code.push_back(0x01000311u); // ds_bpermute_b32 v1, v17, v3
code.push_back(EncodeSop1(0x04, 126, 4));
AppendStoreVgprAtLaneDwordOffset(&code, 1, 0, 4);
AppendVMovU32(&code, 17, 124);
code.push_back(EncodeDs0(0xb3, 4));
code.push_back(EncodeDs1(2, 3, 17));
AppendStoreVgprAtLaneDwordOffset(&code, 2, 0, 8);
AppendEnd(&code);
TestCase test;
test.name = "DsBpermuteCapturedExecOffsetAndWrap";
test.code = code;
test.initial = {0, 0, 12, 4, 0, 0, 0, 0, 0, 0, 0, 0};
test.expected = {0, 0, 12, 4,
0xdeadbeefu, 0, 0xdeadbeefu, 101,
100, 100, 100, 100};
test.opcodes = {O::V_LSHLREV_B32, O::BUFFER_LOAD_DWORD,
O::V_MOV_B32, O::V_ADD_NC_U32,
O::V_AND_B32, O::V_CMP_EQ_U32,
O::S_MOV_B64,
O::DS_BPERMUTE_B32, O::BUFFER_STORE_DWORD,
O::S_ENDPGM};
test.required_spirv = {"OpGroupNonUniformShuffle",
"OpGroupNonUniformBallot"};
test.forbidden_spirv = {" Workgroup", "OpControlBarrier",
"OpMemoryBarrier"};
test.decoded_counts = {{"DS_BPERMUTE_B32", 2}};
test.ir_counts = {{"BpermuteU32", 2}};
test.compute_info.threads_num[0] = 4;
test.compute_info.threads_num[1] = 1;
test.compute_info.threads_num[2] = 1;
test.compute_info.wave_size = 32;
test.compute_info.thread_ids_num = 1;
test.has_compute_info = true;
return test;
}
TestCase DsBpermuteWave64UsesIndependentHalves() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendVMovU32(&code, 3, 100);
code.push_back(EncodeVop2(0x25, 3, Vgpr(0), 3));
AppendVMovU32(&code, 17, 128);
code.push_back(0xdacc0000u);
code.push_back(0x01000311u); // ds_bpermute_b32 v1, v17, v3
AppendStoreVgprAtLaneDwordOffset(&code, 1, 0, 0);
AppendEnd(&code);
TestCase test;
test.name = "DsBpermuteWave64UsesIndependentHalves";
test.code = code;
test.initial = std::vector<u32>(64, 0);
test.expected = std::vector<u32>(32, 100);
test.expected.insert(test.expected.end(), 32, 132);
test.opcodes = {O::V_MOV_B32, O::V_ADD_NC_U32,
O::DS_BPERMUTE_B32, O::V_LSHLREV_B32,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.required_spirv = {"OpGroupNonUniformShuffle"};
test.compute_info.threads_num[0] = 64;
test.compute_info.threads_num[1] = 1;
test.compute_info.threads_num[2] = 1;
test.compute_info.wave_size = 64;
test.compute_info.thread_ids_num = 1;
test.has_compute_info = true;
return test;
}
TestCase BufferAtomicVariants() {
using O = ShaderOpcode;
@@ -18936,6 +19288,44 @@ TestCase ImageLoad1DUsesScalarCoordinate() {
return test;
}
TestCase ImageGather2DInstructionWith1DDescriptor() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendVMovLiteral(&code, 20, 0x3f000000u);
AppendVMovLiteral(&code, 21, 0x3f000000u);
code.push_back(EncodeMimg0(0x47, 0x1));
code.push_back(EncodeMimg1(0, 20));
for (u32 i = 0; i < 4u; i++) {
AppendStoreVgpr(&code, i, i);
}
AppendEnd(&code);
auto image = MakeRgbaImage(4, 1);
SetRgbaPixel(&image, 4, 1, 0, 0x40000000u, 0, 0, 0);
SetRgbaPixel(&image, 4, 2, 0, 0x40400000u, 0, 0, 0);
TestCase test;
test.name = "ImageGather2DInstructionWith1DDescriptor";
test.code = code;
test.expected = {0x40000000u, 0x40400000u, 0x40400000u, 0x40000000u};
test.opcodes = {O::V_MOV_B32, O::IMAGE_GATHER4_LZ, O::BUFFER_STORE_DWORD,
O::S_ENDPGM};
test.image_width = 4;
test.image_height = 1;
test.sampled_image_rgba = image;
test.sampled_image_type = vk::ImageType::e1D;
test.sampled_image_view_type = vk::ImageViewType::e1D;
test.user_data = MakeSampledTextureData(Prospero::BufferFormat::k32Float);
test.user_data[3] = static_cast<uint32_t>(Prospero::ImageType::kColor1D)
<< 28u;
test.has_user_data = true;
test.required_spirv = {"sampled_1d", "OpImageQuerySizeLod",
"OpImageSampleExplicitLod", "Floor"};
test.forbidden_spirv = {"OpImageGather"};
return test;
}
TestCase ImageLoad1DArrayUsesLayerCoordinate() {
using O = ShaderOpcode;
@@ -20350,6 +20740,9 @@ std::vector<TestCase> MakeCases() {
AddCase(VectorMoves);
AddCase(VectorVop3MoveAppliesFloatSourceModifiers);
AddCase(VectorIntegerOps);
AddCase(VectorFfbhI32NativeAndVop3OnGpu);
AddCase(Vop1SdwaFfblCapturedHighWordSource);
AddCase(Vop1SdwaNotCapturedByte0Source);
AddCase(Vop2SdwaSubNcExactByte2Destination);
AddCase(Vop2SdwaAddNcCapturedHighWordDestination);
AddCase(Vop2SdwaAshrrevCapturedWord0SignExtends);
@@ -20382,6 +20775,8 @@ std::vector<TestCase> MakeCases() {
AddCase(VectorCvtU16F16Sdwa);
AddCase(VectorMinMaxMed3F16Ops);
AddCase(VectorSpecialF16Ops);
AddCase(VectorCosF16CapturedSdwaAndEdges);
AddCase(VectorSinF16SdwaAndEdges);
AddCase(VectorWritelaneIgnoresExecMask);
AddCase(VectorReadlaneFromInactiveWrittenLane);
AddCase(VectorLaneWave32RuntimeSelectorWraps);
@@ -20411,6 +20806,7 @@ std::vector<TestCase> MakeCases() {
AddCase(VectorSinCosMaxFiniteSpecialCases);
AddCase(VectorCompareOps);
AddCase(VectorVop3CompareEqI64OnGpu);
AddCase(VectorVop3CompareEqU64OnGpu);
AddCase(VectorVop3CompareGtU64OnGpu);
AddCase(VectorVop3CompareNeU64OnGpu);
AddCase(VectorVopcCmpxNeU64CapturedExecMask);
@@ -20507,7 +20903,9 @@ std::vector<TestCase> MakeCases() {
AddCase(ScratchIsPrivatePerInvocation);
AddCase(FlatStoreVariants);
AddCase(DsReadWriteVariants);
AddCase(DsWriteB16D16HiCapturedUsesHighHalf);
AddCase(DsReadU16D16CapturedPreservesHighHalf);
AddCase(DsReadU16D16HiCapturedPreservesLowHalf);
AddCase(DsAppendConsumeUsesEncodedLdsSelector);
AddCase(DsAppendUsesEncodedGdsSelector);
AddCase(DsGdsSubdwordAndAtomicWrites);
@@ -20521,6 +20919,8 @@ std::vector<TestCase> MakeCases() {
AddCase(DsMiscVariants);
AddCase(DsFloatMinMaxUsesSeparateCompareOperand);
AddCase(DsSwizzleInvalidSourceLaneZero);
AddCase(DsBpermuteCapturedExecOffsetAndWrap);
AddCase(DsBpermuteWave64UsesIndependentHalves);
AddCase(BufferAtomicVariants);
AddCase(BufferAtomicGlc0DoesNotReturnOldValue);
AddCase(BufferAtomicFMinExactRawGlcModes);
@@ -20535,6 +20935,7 @@ std::vector<TestCase> MakeCases() {
AddCase(ImageSamplePackedUintConvertsSampleAndGather);
AddCase(ImageLoadR128IgnoresAdjacentMaskSgprs);
AddCase(ImageLoad1DUsesScalarCoordinate);
AddCase(ImageGather2DInstructionWith1DDescriptor);
AddCase(ImageLoad1DArrayUsesLayerCoordinate);
AddCase(ImageLoad1DArrayDescriptorUsesSelectedLayer);
AddCase(ImageLoadMipUsesVaddr2Lod2D);
@@ -24290,6 +24691,11 @@ int main(int argc, char **argv) {
RunCase(&vulkan, Vop2SdwaAshrrevCapturedWord0SignExtends());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--sdwa-addc-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, VectorAddcWritesPerLaneCarryOut());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--cvt-pk-i16-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, Vop3CvtPkI16I32Captured());
@@ -24391,9 +24797,9 @@ int main(int argc, char **argv) {
CheckStandard64RenderTargetTileRoundTrip();
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--standard4-rt-only") == 0) {
if (argc == 2 && std::strcmp(argv[1], "--standard-tile-rt-only") == 0) {
VulkanHarness vulkan;
vulkan.CheckRenderExecutorColorStandard4TileDiscovery();
vulkan.CheckRenderExecutorColorStandardTileDiscovery();
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--image-view-only") == 0) {
@@ -24514,7 +24920,7 @@ int main(int argc, char **argv) {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
vulkan.CheckRenderExecutorColor1DDiscovery();
vulkan.CheckRenderExecutorColorVolumeDiscovery();
vulkan.CheckRenderExecutorColorStandard4TileDiscovery();
vulkan.CheckRenderExecutorColorStandardTileDiscovery();
vulkan.CheckRenderExecutorColorDepthTileDiscovery();
vulkan.CheckRenderExecutorStencilBindingDiscovery();
vulkan.CheckUnifiedTextureCacheFlow();
+132 -26
View File
@@ -4007,6 +4007,62 @@ void TestNewShaderDecoderArchitecture() {
program_direct.word_count,
"program decoder diverged from the single-instruction decoder");
const uint32_t cmp_eq_u64_code[] = {0xd4e2006au, 0x0000d47eu};
Instruction cmp_eq_u64;
Check(DecodeInstruction(cmp_eq_u64_code, 0u, cmp_eq_u64, &error),
error.c_str());
Check(cmp_eq_u64.family == Family::VOP3 &&
cmp_eq_u64.opcode == Opcode::V_CMP_EQ_U64 &&
cmp_eq_u64.dst.kind == OperandKind::VccLo &&
cmp_eq_u64.src_count == 2u &&
cmp_eq_u64.src0.kind == OperandKind::ExecLo &&
cmp_eq_u64.src1.kind == OperandKind::VccLo,
"decoder rejected the captured VOP3 V_CMP_EQ_U64 instruction");
const uint32_t ffbh_i32_code[] = {0x7e1c770eu};
Instruction ffbh_i32;
Check(DecodeInstruction(ffbh_i32_code, 0u, ffbh_i32, &error),
error.c_str());
Check(ffbh_i32.family == Family::VOP1 &&
ffbh_i32.opcode == Opcode::V_FFBH_I32 &&
ffbh_i32.dst.kind == OperandKind::Vgpr && ffbh_i32.dst.reg == 14u &&
ffbh_i32.src_count == 1u &&
ffbh_i32.src0.kind == OperandKind::Vgpr &&
ffbh_i32.src0.reg == 14u,
"decoder rejected the captured VOP1 V_FFBH_I32 instruction");
const uint32_t cos_f16_code[] = {0x7e08c2f9u, 0x00051504u};
Instruction cos_f16;
Check(DecodeInstruction(cos_f16_code, 0u, cos_f16, &error), error.c_str());
Check(cos_f16.family == Family::VOP1 &&
cos_f16.opcode == Opcode::V_COS_F16 &&
cos_f16.word_count == 2u && cos_f16.src_count == 1u &&
cos_f16.dst.kind == OperandKind::Vgpr && cos_f16.dst.reg == 4u &&
cos_f16.dst.sdwa_sel == 5u &&
cos_f16.dst.sdwa_dst_unused == 2u && !cos_f16.dst.clamp &&
cos_f16.dst.omod == 0u &&
cos_f16.src0.kind == OperandKind::Vgpr &&
cos_f16.src0.reg == 4u && cos_f16.src0.sdwa_sel == 5u &&
!cos_f16.src0.sdwa_sext && !cos_f16.src0.negate &&
!cos_f16.src0.absolute,
"decoder rejected the captured VOP1 SDWA V_COS_F16 instruction");
const uint32_t sin_f16_code[] = {0x7e08c0f9u, 0x00051504u};
Instruction sin_f16;
Check(DecodeInstruction(sin_f16_code, 0u, sin_f16, &error), error.c_str());
Check(sin_f16.family == Family::VOP1 &&
sin_f16.opcode == Opcode::V_SIN_F16 &&
sin_f16.word_count == 2u && sin_f16.src_count == 1u &&
sin_f16.dst.kind == OperandKind::Vgpr && sin_f16.dst.reg == 4u &&
sin_f16.dst.sdwa_sel == 5u &&
sin_f16.dst.sdwa_dst_unused == 2u && !sin_f16.dst.clamp &&
sin_f16.dst.omod == 0u &&
sin_f16.src0.kind == OperandKind::Vgpr &&
sin_f16.src0.reg == 4u && sin_f16.src0.sdwa_sel == 5u &&
!sin_f16.src0.sdwa_sext && !sin_f16.src0.negate &&
!sin_f16.src0.absolute,
"decoder rejected the VOP1 SDWA V_SIN_F16 instruction");
const uint32_t literal_code[] = {EncodeVop1(0x01, 2, 255u), 0x12345678u};
Instruction literal;
Check(DecodeInstruction(literal_code, 0u, literal, &error), error.c_str());
@@ -4043,6 +4099,40 @@ void TestNewShaderDecoderArchitecture() {
"decoder rejected or misdecoded captured partial-destination "
"V_ADD_NC_U32");
const uint32_t vop2_sdwa_byte_shift[] = {
0x2c0910f9u,
0x86000613u, // v_lshrrev_b32 v4, v19.byte0, 8
};
Instruction byte_shift;
Check(DecodeInstruction(vop2_sdwa_byte_shift, 0u, byte_shift, &error),
error.c_str());
Check(byte_shift.family == Family::VOP2 &&
byte_shift.opcode == Opcode::V_LSHRREV_B32 &&
byte_shift.word_count == 2u && byte_shift.dst.reg == 4u &&
byte_shift.src0.kind == OperandKind::Vgpr &&
byte_shift.src0.reg == 19u && byte_shift.src0.sdwa_sel == 0u &&
byte_shift.src1.kind == OperandKind::IntegerInlineConstant &&
byte_shift.src1.value == 8u,
"decoder rejected or misdecoded captured V_LSHRREV_B32 byte shift");
const uint32_t vop2_sdwa_byte_left_shift[] = {
0x340d02f9u,
0x86030607u, // v_lshlrev_b32 v6, v7.byte3, 1
};
Instruction byte_left_shift;
Check(DecodeInstruction(vop2_sdwa_byte_left_shift, 0u, byte_left_shift,
&error),
error.c_str());
Check(byte_left_shift.family == Family::VOP2 &&
byte_left_shift.opcode == Opcode::V_LSHLREV_B32 &&
byte_left_shift.word_count == 2u && byte_left_shift.dst.reg == 6u &&
byte_left_shift.src0.kind == OperandKind::Vgpr &&
byte_left_shift.src0.reg == 7u &&
byte_left_shift.src0.sdwa_sel == 3u &&
byte_left_shift.src1.kind == OperandKind::IntegerInlineConstant &&
byte_left_shift.src1.value == 1u,
"decoder rejected or misdecoded captured V_LSHLREV_B32 byte shift");
const uint32_t mimg_nsa[] = {EncodeMimg0(0x20, 0xf) | (3u << 1u),
EncodeMimg1(4, 0, 1, 8), 0x03020100u,
0x07060504u, 0x0b0a0908u};
@@ -4065,6 +4155,40 @@ void TestNewShaderDecoderArchitecture() {
Check(boot.opcode == Opcode::DS_SWIZZLE_B32 && boot.offset == 0xc480u,
"DS decoder rejected a captured boot-shader instruction");
const uint32_t bpermute_ds[] = {0xdacc0000u, 0x01000311u};
Instruction bpermute;
Check(DecodeInstruction(bpermute_ds, 0u, bpermute, &error), error.c_str());
Check(bpermute.family == Family::DS &&
bpermute.opcode == Opcode::DS_BPERMUTE_B32 &&
bpermute.word_count == 2u && bpermute.src_count == 2u &&
bpermute.data_dwords == 1u && bpermute.data_bits == 32u &&
bpermute.offset == 0u && !bpermute.gds &&
bpermute.dst.reg == 1u && bpermute.src0.reg == 17u &&
bpermute.src1.reg == 3u,
"DS decoder rejected or misdecoded captured DS_BPERMUTE_B32");
const uint32_t d16_hi_ds[] = {0xda9c0000u, 0x05000006u};
Instruction d16_hi;
Check(DecodeInstruction(d16_hi_ds, 0u, d16_hi, &error), error.c_str());
Check(d16_hi.opcode == Opcode::DS_READ_U16_D16_HI &&
d16_hi.src_count == 1u && d16_hi.data_bits == 16u &&
d16_hi.dst.reg == 5u && d16_hi.dst.sdwa_sel == 5u &&
d16_hi.dst.sdwa_dst_unused == 2u && d16_hi.src0.reg == 6u,
"DS decoder rejected the captured high-half D16 read");
const uint32_t d16_hi_write_ds[] = {0xda840000u, 0x00000214u};
Instruction d16_hi_write;
Check(DecodeInstruction(d16_hi_write_ds, 0u, d16_hi_write, &error),
error.c_str());
Check(d16_hi_write.family == Family::DS &&
d16_hi_write.opcode == Opcode::DS_WRITE_B16_D16_HI &&
d16_hi_write.word_count == 2u && d16_hi_write.src_count == 2u &&
d16_hi_write.data_dwords == 1u && d16_hi_write.data_bits == 16u &&
d16_hi_write.offset == 0u && !d16_hi_write.gds &&
d16_hi_write.src0.reg == 20u && d16_hi_write.src1.reg == 2u &&
d16_hi_write.src1.sdwa_sel == 5u,
"DS decoder rejected the captured high-half D16 write");
constexpr uint32_t packed_source_selectors[][2] = {
{0xcc0e0000u, 0x0c0a0300u}, // Source 0: instruction bit 59.
{0xcc0e0000u, 0x140a0300u}, // Source 1: instruction bit 60.
@@ -4083,8 +4207,8 @@ void TestNewShaderDecoderArchitecture() {
void TestNewShaderRecompilerRejectsDppOn64BitCompares() {
const uint32_t opcodes[] = {
0xa2u, 0xb5u, 0xe4u, 0xe5u,
0xf5u}; // eq_i64, cmpx_ne_i64, gt_u64, ne_u64, cmpx_ne_u64
0xa2u, 0xb5u, 0xe2u, 0xe4u, 0xe5u,
0xf5u}; // eq_i64, cmpx_ne_i64, eq_u64, gt_u64, ne_u64, cmpx_ne_u64
for (const auto opcode : opcodes) {
const uint32_t shader[] = {
EncodeVopc(opcode, 250u, 0u), // DPP escape in SRC0
@@ -5054,30 +5178,6 @@ void TestNewShaderRecompilerNullImageUsesCanonical2DView() {
"null image retained the decoded 1D descriptor binding");
}
void TestNewShaderRecompilerRejectsOneDimensionalGather() {
constexpr std::array cases{
std::pair{0u, Prospero::ImageType::kColor1D},
std::pair{4u, Prospero::ImageType::kColor1DArray},
};
for (const auto &[dimension, type] : cases) {
const uint32_t shader[] = {
EncodeMimg0(0x47, 0x1, false, dimension),
EncodeMimg1(0, 0, 1, 0),
0xbf810000u,
};
auto user_data = ImageTestUserData(type);
auto options = MakeCompileOptions(ShaderType::Compute);
options.user_data = user_data.data();
ShaderRecompiler::CompileResult result;
std::string error;
Check(!ShaderRecompiler::TryRecompile(shader, options, result, &error) &&
Common::ContainsStr(error,
"1D image gather is not supported by SPIR-V"),
"1D image gather escaped as invalid SPIR-V");
}
}
void TestNewShaderRecompilerImageGatherVariants() {
const uint32_t shader[] = {
EncodeMimg0(0x47, 0x1),
@@ -12027,6 +12127,12 @@ void TestNewShaderRecompilerUnsupportedMemoryDecode() {
CheckNewDecoderUnsupported(mtbuf_unknown,
static_cast<uint32_t>(std::size(mtbuf_unknown)),
"MTBUF", "opcode=0x08");
const uint32_t bpermute_gds[] = {
EncodeDs0(0xb3) | (1u << 17u), EncodeDs1(1, 3, 17), 0xbf810000u};
CheckNewDecoderUnsupported(bpermute_gds,
static_cast<uint32_t>(std::size(bpermute_gds)),
"DS", "available only for LDS");
}
void TestNewShaderRecompilerFlatUserPointerUsesDma() {