Compare commits

...

29 Commits

Author SHA1 Message Date
nmzik 39ee9972b8 shader: V_CMPX_CLASS_F32 2026-08-30 14:32:07 +04:00
YuhiAida e500333bcd fix(agc): implement AgcDcbCopyDataGetSize (#410)
Register b5u0Jzm8TF8 for SpongeBob PPSA26893 using the native zero-argument, uint64_t size-helper ABI.

Adapted from PR #410.
2026-08-30 13:59:16 +04:00
nmzik 4498d75b9b add agc ver 13 2026-08-30 13:29:49 +04:00
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
61 changed files with 1965 additions and 417 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/
+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
+1 -1
View File
@@ -59,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;
+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
@@ -942,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();
+31 -4
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"
@@ -28,6 +29,7 @@
#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,13 +41,12 @@
#include "loader/systemContent.h"
#include <cstdlib>
#include <fmt/format.h>
#include <memory>
#include <string>
#include <vector>
#include <vulkan/vk_platform.h>
#include <fmt/format.h>
#define STB_IMAGE_IMPLEMENTATION
#define STBI_NO_SIMD
#include "stb_image.h"
@@ -506,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;
}
@@ -782,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());
}
@@ -811,6 +819,24 @@ static void WindowCreate(WindowContext& context) {
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.
@@ -858,6 +884,7 @@ void WindowRun() {
EXIT_IF(g_window == nullptr);
g_window->Run();
g_window->render_context->GetPipelineCache().Save();
}
void WindowShutdown() {
@@ -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,
@@ -348,6 +351,7 @@ enum class Opcode {
V_CMP_GE_I32,
V_CMP_T_I32,
V_CMP_CLASS_F32,
V_CMPX_CLASS_F32,
V_CMP_LT_I16,
V_CMP_EQ_I16,
V_CMP_LE_I16,
@@ -391,6 +395,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 +495,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 +503,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 +514,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);
@@ -221,7 +227,8 @@ constexpr VopcOpcodeInfo VOPC_OPCODE_LIST[] = {
{0x8eu, Opcode::V_CMP_GE_I16}, {0x91u, Opcode::V_CMPX_LT_I32},
{0x92u, Opcode::V_CMPX_EQ_I32}, {0x93u, Opcode::V_CMPX_LE_I32},
{0x94u, Opcode::V_CMPX_GT_I32}, {0x95u, Opcode::V_CMPX_NE_I32},
{0x96u, Opcode::V_CMPX_GE_I32}, {0xa9u, Opcode::V_CMP_LT_U16},
{0x96u, Opcode::V_CMPX_GE_I32}, {0x98u, Opcode::V_CMPX_CLASS_F32},
{0xa9u, Opcode::V_CMP_LT_U16},
{0xaau, Opcode::V_CMP_EQ_U16}, {0xabu, Opcode::V_CMP_LE_U16},
{0xacu, Opcode::V_CMP_GT_U16}, {0xadu, Opcode::V_CMP_NE_U16},
{0xaeu, Opcode::V_CMP_GE_U16}, {0xbcu, Opcode::V_CMPX_GT_U16},
@@ -234,7 +241,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 +487,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 +525,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 +555,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 +843,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;
@@ -878,7 +896,8 @@ bool IsVopcFloatCompareOpcode(Opcode opcode) {
case Opcode::V_CMPX_NGT_F16:
case Opcode::V_CMPX_NEQ_F16:
case Opcode::V_CMPX_NLT_F16:
case Opcode::V_CMP_CLASS_F32: return true;
case Opcode::V_CMP_CLASS_F32:
case Opcode::V_CMPX_CLASS_F32: return true;
default: return false;
}
}
@@ -940,8 +959,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]) ==
@@ -1501,6 +1520,7 @@ bool IsVopcCompareExec(Opcode opcode) {
case Opcode::V_CMPX_GT_I32:
case Opcode::V_CMPX_NE_I32:
case Opcode::V_CMPX_GE_I32:
case Opcode::V_CMPX_CLASS_F32:
case Opcode::V_CMPX_LT_U32:
case Opcode::V_CMPX_EQ_U32:
case Opcode::V_CMPX_LE_U32:
@@ -55,11 +55,11 @@ void Translator::EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool
EmitCompareResult(inst, ordered ? ir.LogicalNot(unordered) : unordered, false, false);
}
void Translator::V_CMP_CLASS_F32(const Decoder::Instruction& inst) {
void Translator::EmitFloatClassCompare(const Decoder::Instruction& inst, bool cmpx) {
const auto value = ReadOperand(inst.src0, IR::Type::F32);
const auto mask = ReadOperand(inst.src1, IR::Type::U32);
EmitCompareResult(inst, IR::U1(ir.Emit(IR::ValueOpcode::FPCmpClass32, {value, mask})), false,
false);
cmpx);
}
} // namespace Libs::Graphics::ShaderRecompiler::Frontend
@@ -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,
@@ -126,7 +127,7 @@ private:
void EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool half,
bool cmpx);
void EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered);
void V_CMP_CLASS_F32(const Decoder::Instruction& inst);
void EmitFloatClassCompare(const Decoder::Instruction& inst, bool cmpx);
void V_CVT_F32_UBYTE(const Decoder::Instruction& inst, uint32_t byte_index);
void V_CVT_F32_U32(const Decoder::Instruction& inst);
void V_CVT_F32_I32(const Decoder::Instruction& inst);
@@ -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:
@@ -270,7 +271,8 @@ bool Translator::EmitVector(const Decoder::Instruction& inst, std::string* error
return true;
case O::V_CMP_O_F32: EmitFloatOrderedCompare(inst, true); return true;
case O::V_CMP_U_F32: EmitFloatOrderedCompare(inst, false); return true;
case O::V_CMP_CLASS_F32: V_CMP_CLASS_F32(inst); return true;
case O::V_CMP_CLASS_F32: EmitFloatClassCompare(inst, false); return true;
case O::V_CMPX_CLASS_F32: EmitFloatClassCompare(inst, true); return true;
case O::V_CVT_F32_UBYTE0: V_CVT_F32_UBYTE(inst, 0); return true;
case O::V_CVT_F32_UBYTE1: V_CVT_F32_UBYTE(inst, 1); return true;
@@ -364,6 +366,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 +494,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>
+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 -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) {
+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."));
}
}
+9 -2
View File
@@ -25,6 +25,7 @@
#include "libs/libs.h"
#include <algorithm>
#include <array>
#include <atomic>
#include <cmath>
#include <cstddef>
@@ -142,8 +143,8 @@ struct RegisterDefaultsStorage {
#include <fmt/format.h>
static constexpr uint32_t GRAPHICS_REGISTER_DEFAULTS_MAX_VERSION = 12;
static constexpr uint32_t GRAPHICS_REGISTER_DEFAULTS_FALLBACK_VERSION = 11;
static constexpr uint32_t GRAPHICS_REGISTER_DEFAULTS_MAX_VERSION = 13;
static constexpr uint32_t GRAPHICS_REGISTER_DEFAULTS_FALLBACK_VERSION = 13;
static constexpr uint32_t GRAPHICS_INIT_NO_FEATURE_STATE = 0;
static std::mutex g_register_defaults_mutex;
@@ -3439,6 +3440,12 @@ uint32_t* KYTY_SYSV_ABI AgcDcbCopyData(CommandBuffer* buf, uint8_t dst, uint8_t
return cmd;
}
uint64_t KYTY_SYSV_ABI AgcDcbCopyDataGetSize() {
PRINT_NAME();
return 6 * sizeof(uint32_t);
}
uint32_t* KYTY_SYSV_ABI AgcDcbDmaData(CommandBuffer* buf, uint8_t engine, uint8_t dst,
uint8_t dst_cache_policy, uint64_t dst_address_or_offset,
uint8_t src, uint8_t src_cache_policy,
+1
View File
@@ -233,6 +233,7 @@ uint32_t* KYTY_SYSV_ABI AgcDcbCopyData(CommandBuffer* buf, uint8_t dst, uint8_t
uint64_t dst_address, uint8_t src, uint8_t src_cache_policy,
uint64_t src_address_or_immediate, uint8_t item_size,
uint8_t write_confirm);
uint64_t KYTY_SYSV_ABI AgcDcbCopyDataGetSize();
uint32_t* KYTY_SYSV_ABI AgcDcbDmaData(CommandBuffer* buf, uint8_t engine, uint8_t dst,
uint8_t dst_cache_policy, uint64_t dst_address_or_offset,
uint8_t src, uint8_t src_cache_policy,
+85 -3
View File
@@ -1,4 +1,4 @@
// No-feature, non-0x840f00 AgcGetRegisterDefaults2 dispatch, versions 0..12.
// No-feature, non-0x840f00 AgcGetRegisterDefaults2 dispatch, versions 0..13.
// Type entries keep the pointer index in bits 0..9; higher bits are AGC flags.
// g_agc_internal_reg_defaults_v0: regs=(1, 12, 0, 4), ptrs=(1, 12, 0, 2), types=15.
@@ -185,6 +185,45 @@ static CompactRegisterDefaults g_agc_internal_reg_defaults_v11 = {
g_agc_internal_reg_defaults_v11_types, 24,
};
// Version 13 takes the version-11 fallback and selects the >=13 state-0 lookup bank.
// Only two internal table-1 values differ in that bank.
static auto g_agc_internal_reg_defaults_v13_tbl1_regs = [] {
constexpr auto count = sizeof(g_agc_internal_reg_defaults_v11_tbl1_regs) /
sizeof(g_agc_internal_reg_defaults_v11_tbl1_regs[0]);
std::array<ShaderRegister, count> regs {};
std::copy_n(g_agc_internal_reg_defaults_v11_tbl1_regs, count, regs.begin());
for (auto& reg: regs) {
if (reg.offset == 0x0081) {
reg.value = 0x00000001;
} else if (reg.offset == 0x0101) {
reg.value = 0x00000003;
}
}
return regs;
}();
static CompactRegisterDefaults g_agc_internal_reg_defaults_v13 = {
g_agc_internal_reg_defaults_v11_tbl0_regs,
8,
g_agc_internal_reg_defaults_v11_tbl0_ptrs,
8,
g_agc_internal_reg_defaults_v13_tbl1_regs.data(),
12,
g_agc_internal_reg_defaults_v11_tbl1_ptrs,
12,
g_agc_internal_reg_defaults_v11_tbl2_regs,
1,
g_agc_internal_reg_defaults_v11_tbl2_ptrs,
1,
g_agc_internal_reg_defaults_v11_tbl3_regs,
6,
g_agc_internal_reg_defaults_v11_tbl3_ptrs,
3,
g_agc_internal_reg_defaults_v11_types,
24,
};
// g_agc_internal_reg_defaults_v4: regs=(2, 15, 0, 4), ptrs=(2, 15, 0, 2), types=19.
static ShaderRegister g_agc_internal_reg_defaults_v4_tbl0_regs[] = {
{0x02af, 0x00040000},
@@ -1366,8 +1405,28 @@ static uint32_t g_agc_public_reg_defaults_v11_types[] = {
0x036ac8a6, 0x000c0452, 0x00000000,
};
template <std::size_t Size>
static std::array<ShaderRegister, Size>
make_v11_plus_public_tbl0(const ShaderRegister (&source)[Size]) {
std::array<ShaderRegister, Size> regs {};
std::copy_n(source, Size, regs.begin());
for (auto& reg: regs) {
if (reg.offset == 0x0201) {
reg.value |= 0x00010000;
}
}
return regs;
}
// The common setup helper forces this table-0 patch for every version >= 11.
static auto g_agc_public_reg_defaults_v11_tbl0_effective =
make_v11_plus_public_tbl0(g_agc_public_reg_defaults_v11_tbl0_regs);
static auto g_agc_public_reg_defaults_v12_tbl0_effective =
make_v11_plus_public_tbl0(g_agc_public_reg_defaults_v10_tbl0_regs);
static CompactRegisterDefaults g_agc_public_reg_defaults_v11 = {
g_agc_public_reg_defaults_v11_tbl0_regs,
g_agc_public_reg_defaults_v11_tbl0_effective.data(),
488,
g_agc_public_reg_defaults_v11_tbl0_ptrs,
84,
@@ -1387,6 +1446,27 @@ static CompactRegisterDefaults g_agc_public_reg_defaults_v11 = {
137,
};
static CompactRegisterDefaults g_agc_public_reg_defaults_v12 = {
g_agc_public_reg_defaults_v12_tbl0_effective.data(),
482,
g_agc_public_reg_defaults_v10_tbl0_ptrs,
79,
g_agc_public_reg_defaults_v10_tbl1_regs,
159,
g_agc_public_reg_defaults_v10_tbl1_ptrs,
29,
g_agc_public_reg_defaults_v10_tbl2_regs,
55,
g_agc_public_reg_defaults_v10_tbl2_ptrs,
20,
nullptr,
0,
nullptr,
0,
g_agc_public_reg_defaults_v10_types,
128,
};
// g_agc_public_reg_defaults_v4: regs=(488, 158, 43, 0), ptrs=(80, 28, 42, 0), types=150.
static ShaderRegister g_agc_public_reg_defaults_v4_tbl0_regs[] = {
{0x0202, 0x00cc0010}, {0x0109, 0x00000008}, {0x0104, 0x00000000}, {0x008f, 0x00000000},
@@ -2825,7 +2905,8 @@ static CompactRegisterDefaults* g_agc_public_reg_defaults_by_version[] = {
&g_agc_public_reg_defaults_v9, // version 9
&g_agc_public_reg_defaults_v10, // version 10
&g_agc_public_reg_defaults_v11, // version 11
&g_agc_public_reg_defaults_v10, // version 12
&g_agc_public_reg_defaults_v12, // version 12
&g_agc_public_reg_defaults_v11, // version 13: v11 fallback with the >=13 state-0 bank
};
static CompactRegisterDefaults* g_agc_internal_reg_defaults_by_version[] = {
@@ -2842,4 +2923,5 @@ static CompactRegisterDefaults* g_agc_internal_reg_defaults_by_version[] = {
&g_agc_internal_reg_defaults_v10, // version 10
&g_agc_internal_reg_defaults_v11, // version 11
&g_agc_internal_reg_defaults_v10, // version 12
&g_agc_internal_reg_defaults_v13, // version 13
};
+46
View File
@@ -1,5 +1,6 @@
#include "libs/controller.h"
#include "SDL.h"
#include "common/assert.h"
#include "common/common.h"
#include "common/emulatorConfig.h"
@@ -22,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;
@@ -57,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);
@@ -260,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 {};
@@ -269,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);
@@ -347,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();
@@ -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();
+1
View File
@@ -121,6 +121,7 @@ LIB_DEFINE(InitAgcDriver_1) {
LIB_FUNC("57labkp+rSQ", Gen5::AgcDcbAcquireMem);
LIB_FUNC("-vnlTPPXPrw", Gen5::AgcDcbAcquireMemGetSize);
LIB_FUNC("1rZSWUv1IRc", Gen5::AgcDcbCopyData);
LIB_FUNC("b5u0Jzm8TF8", Gen5::AgcDcbCopyDataGetSize); // SpongeBob PPSA26893
LIB_FUNC("WmAc2MEj6Io", Gen5::AgcDcbDmaData);
LIB_FUNC("xSAR0LTcRKM", Gen5::AgcDcbJump);
LIB_FUNC("VEGu4dixjUg", Gen5::AgcDcbJumpGetSize);
+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);
+25
View File
@@ -101,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);
@@ -291,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);
@@ -1231,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();
+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);
+3 -3
View File
@@ -43,15 +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(
" --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");
+482 -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:
@@ -11913,6 +11931,7 @@ CoverageClass ClassifyOpcode(ShaderOpcode opcode,
case Opcode::V_CMPX_NEQ_F32:
case Opcode::V_CMPX_NLT_F32:
case Opcode::V_CMP_CLASS_F32:
case Opcode::V_CMPX_CLASS_F32:
case Opcode::V_CMP_LT_F16:
case Opcode::V_CMP_EQ_F16:
case Opcode::V_CMP_LE_F16:
@@ -12013,11 +12032,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 +12047,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 +13377,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 +13961,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 +14492,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 +15549,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;
@@ -15574,6 +15786,38 @@ TestCase VectorCompareClassF32() {
O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
}
TestCase VectorVopcSdwaCmpxClassF32CapturedExecMask() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendVMovLiteral(&code, 13, 0x7fc00000u); // Quiet NaN.
AppendVMovU32(&code, 3, 7);
AppendVMovU32(&code, 31, 0);
code.push_back(EncodeSMovB32(106, InlineU32(2))); // qNaN class mask.
code.insert(code.end(), {0x7d30d4f9u, 0x8606000du});
AppendBufferStoreDword(&code, 3, 31);
code.push_back(EncodeSMovB32(126, InlineU32(1))); // Restore lane-zero EXEC.
AppendVMovLiteral(&code, 13, 0x3f800000u); // Positive normal.
AppendVMovU32(&code, 3, 9);
AppendVMovU32(&code, 31, 4);
code.push_back(EncodeSMovB32(106, InlineU32(2))); // qNaN class mask.
code.insert(code.end(), {0x7d30d4f9u, 0x8606000du});
AppendBufferStoreDword(&code, 3, 31);
AppendEnd(&code);
TestCase test{
"VectorVopcSdwaCmpxClassF32CapturedExecMask",
code,
{0x11111111u, 0x22222222u},
{7u, 0x22222222u},
{O::S_MOV_B32, O::V_MOV_B32, O::V_CMPX_CLASS_F32,
O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
test.decoded_counts = {
{"V_CMPX_CLASS_F32 exec_lo, v13, vcc_lo", 2}};
return test;
}
TestCase VectorCompareF16Ops() {
using O = ShaderOpcode;
@@ -17992,6 +18236,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 +18679,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 +19321,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 +20773,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 +20808,8 @@ std::vector<TestCase> MakeCases() {
AddCase(VectorCvtU16F16Sdwa);
AddCase(VectorMinMaxMed3F16Ops);
AddCase(VectorSpecialF16Ops);
AddCase(VectorCosF16CapturedSdwaAndEdges);
AddCase(VectorSinF16SdwaAndEdges);
AddCase(VectorWritelaneIgnoresExecMask);
AddCase(VectorReadlaneFromInactiveWrittenLane);
AddCase(VectorLaneWave32RuntimeSelectorWraps);
@@ -20411,11 +20839,13 @@ std::vector<TestCase> MakeCases() {
AddCase(VectorSinCosMaxFiniteSpecialCases);
AddCase(VectorCompareOps);
AddCase(VectorVop3CompareEqI64OnGpu);
AddCase(VectorVop3CompareEqU64OnGpu);
AddCase(VectorVop3CompareGtU64OnGpu);
AddCase(VectorVop3CompareNeU64OnGpu);
AddCase(VectorVopcCmpxNeU64CapturedExecMask);
AddCase(VectorVop3CmpxNeI64CapturedExecMask);
AddCase(VectorCompareClassF32);
AddCase(VectorVopcSdwaCmpxClassF32CapturedExecMask);
AddCase(VectorCompareF16Ops);
AddCase(Vop2SdwaCndmaskSourceModifier);
AddCase(Vop2SdwaCndmaskCapturedByte3Source);
@@ -20507,7 +20937,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 +20953,8 @@ std::vector<TestCase> MakeCases() {
AddCase(DsMiscVariants);
AddCase(DsFloatMinMaxUsesSeparateCompareOperand);
AddCase(DsSwizzleInvalidSourceLaneZero);
AddCase(DsBpermuteCapturedExecOffsetAndWrap);
AddCase(DsBpermuteWave64UsesIndependentHalves);
AddCase(BufferAtomicVariants);
AddCase(BufferAtomicGlc0DoesNotReturnOldValue);
AddCase(BufferAtomicFMinExactRawGlcModes);
@@ -20535,6 +20969,7 @@ std::vector<TestCase> MakeCases() {
AddCase(ImageSamplePackedUintConvertsSampleAndGather);
AddCase(ImageLoadR128IgnoresAdjacentMaskSgprs);
AddCase(ImageLoad1DUsesScalarCoordinate);
AddCase(ImageGather2DInstructionWith1DDescriptor);
AddCase(ImageLoad1DArrayUsesLayerCoordinate);
AddCase(ImageLoad1DArrayDescriptorUsesSelectedLayer);
AddCase(ImageLoadMipUsesVaddr2Lod2D);
@@ -24290,6 +24725,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 +24831,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 +24954,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();
+194 -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
@@ -4107,6 +4231,67 @@ void TestNewShaderRecompilerRejectsDppOn64BitCompares() {
}
}
void TestNewShaderRecompilerCapturedVopcSdwaCmpxClass() {
using namespace ShaderRecompiler;
const uint32_t shader[] = {
0x7d30d4f9u, 0x8606000du, // v_cmpx_class_f32 exec, v13, vcc_lo (SDWA)
EncodeSopp(0x01),
};
Decoder::Instruction decoded;
std::string error;
Check(Decoder::DecodeInstruction(shader, 0u, decoded, &error),
error.c_str());
Check(decoded.family == Decoder::Family::VOPC &&
decoded.opcode == Decoder::Opcode::V_CMPX_CLASS_F32 &&
decoded.opcode_id == 0x98u && decoded.word_count == 2u &&
decoded.raw_count == 2u &&
decoded.dst.kind == Decoder::OperandKind::ExecLo &&
decoded.src_count == 2u &&
decoded.src0.kind == Decoder::OperandKind::Vgpr &&
decoded.src0.reg == 13u && decoded.src0.sdwa_sel == 6u &&
decoded.src1.kind == Decoder::OperandKind::VccLo &&
decoded.src1.sdwa_sel == 6u,
"decoder rejected captured SDWA V_CMPX_CLASS_F32 fields");
Decoder::Program program;
CFG::Graph graph;
IR::Program ir;
ShaderPixelInputInfo pixel{};
Frontend::TranslateOptions translate_options{};
translate_options.stage = ShaderType::Pixel;
translate_options.wave_size = 64u;
translate_options.pixel = &pixel;
Check(Decoder::DecodeProgram(shader, program, &error) &&
CFG::BuildGraph(program, graph, &error) &&
Frontend::TranslateProgram(program, graph, translate_options, ir,
&error),
error.c_str());
uint32_t class_compares = 0u;
uint32_t dynamic_exec_writes = 0u;
for (const auto *block : ir.blocks) {
for (const auto &inst : *block) {
class_compares +=
inst.GetOpcode() == IR::ValueOpcode::FPCmpClass32 ? 1u : 0u;
if (inst.GetOpcode() == IR::ValueOpcode::SetExec &&
!inst.Arg(0).Resolve().IsImmediate()) {
dynamic_exec_writes++;
}
}
}
Check(class_compares == 1u && dynamic_exec_writes == 1u,
"captured V_CMPX_CLASS_F32 did not lower to class compare plus EXEC update");
auto options = MakeCompileOptions(ShaderType::Pixel);
CompileResult result;
Check(TryRecompile(shader, options, result, &error), error.c_str());
Check(Common::ContainsStr(result.decoded_dump,
"V_CMPX_CLASS_F32 exec_lo, v13, vcc_lo"),
"captured SDWA V_CMPX_CLASS_F32 was not present in the decoded dump");
CheckSpirvBinaryValidates(result.spirv);
}
void TestNewShaderRecompilerIrLookupMissFailsExplicitly() {
ShaderRecompiler::Decoder::Program decoded;
ShaderRecompiler::Decoder::Instruction missing;
@@ -5054,30 +5239,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 +12188,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() {
@@ -12370,6 +12537,7 @@ int main() {
// ShaderRecompilerComputeTests; keep the distinct decoder contract checks
// here.
TestNewShaderDecoderArchitecture();
TestNewShaderRecompilerCapturedVopcSdwaCmpxClass();
TestNewShaderRecompilerRejectsDppOn64BitCompares();
TestNewShaderRecompilerIrLookupMissFailsExplicitly();
TestPsInputCountRegisterDecode();